搜索
首页web前端html教程新版卖家中心 Bigpipe 实践(二)_html/css_WEB-ITnose

自从上次通过 新版卖家中心 Bigpipe 实践(一) 阐述了 Bigpipe 实现思路和原理之后,一转眼春天就来了。而整个实践过程,从开始冬天迎着冷风前行,到现在逐渐回暖。其中感受和收获良多,和大家分享下。代码偏多,请自带编译器。

核心问题

一切技术的产生或者使用都是为了解决问题,所以开始前,看下要解决的问题:

  • 同步加载首屏模块,服务端各个模块并行生成内容,客户端渲染内容依赖于最后一个内容的生成时间。这里的痛点是 同步 。因为要多模块同步,所以难免浏览器要等待,浏览器等待也就是用户等待。
  • 于是我们采用了滚动异步加载模块,页面框架优先直出,几朵菊花旋转点缀,然后首屏模块通过异步请求逐个展现出来。虽然拿到什么就能在客户端渲染显示,但还是有延迟感。这里的痛点是 请求 ,每个模块都需要多一个请求,也需要时间。
  • Facebook 的工程师们会不会是这样想的:一次请求,各个首屏模块服务端并行处理生成内容,生成的内容能直接传输给客户端渲染,用户能马上看到内容,这样好猴赛雷~
  • 其实 Bigpipe 的思路是从 微处理器的流水线 中受到启发

技术突破口

卖家中心主体也是功能模块化,和 Facebook 遇到的问题是一致的。核心的问题换个说法: 通过一个请求链接,服务端能否将动态内容分块传输到客户端实时渲染展示,直到内容传输结束,请求结束。

概念

  • 技术点:HTTP 协议的分块传输(在 HTTP 1.1 提供) 概念入口
  • 如果一个 HTTP 消息(请求消息或应答消息)的 Transfer-Encoding 消息头的值为 chunked ,那么,消息体由数量未定的块组成,并以最后一个大小为 0 的块为结束。
  • 这种机制使得网页内容分成多个内容块,服务器和浏览器建立管道并管理他们在不同阶段的运行。

实现

如何实现数据分块传输,各个语言的方式并不一样。

PHP 的方式

<html><head>    <title>php chunked</title></head><body>    <?php sleep(1); ?>    <div id="moduleA"><?php echo 'moduleA' ?></div>    <?php ob_flush(); flush(); ?>        <?php sleep(3); ?>    <div id="moduleB"><?php echo 'moduleB' ?></div>    <?php ob_flush(); flush(); ?>        <?php sleep(2); ?>    <div id="moduleC"><?php echo 'moduleC' ?></div>    <?php ob_flush(); flush(); ?></body></html>

  • PHP 利用 ob_flush 和 flush 把页面分块刷新缓存到浏览器,查看 network ,页面的 Transfer-Encoding=chunked ,实现内容的分块渲染。
  • PHP 不支持线程,所以服务器无法利用多线程去并行处理多个模块的内容。
  • PHP 也有并发执行的方案,这里不做扩展,有兴趣地可以去深入研究下。

Java 的方式

  • Java 也有类似于 flush 的函数 实现简单页面的分块传输。
  • Java 是多线程的,方便并行地处理各个模块的内容。

flush 的思考

  • Yahoo 34 条性能优化 Rules 里面提到 flush 时机是 head 之后,可以让浏览器先行下载 head 中引入的 CSS/js。
  • 我们会把内容分成一块一块 flush 到浏览器端,flush 的内容优先级应该是用户关心的。比如 Yahoo 之前优先 flush 的就是搜索框,因为这个是核心功能。
  • flush 的内容大小需要进行有效地拆分,大内容可以拆成小内容。

Node.js 实现

通过对比 PHP 和 Java 在实现 Bigpipe 上的优势和劣势,很容易在 Node.js 上找到幸福感。

  • Node.js 的异步特性可以很容易地处理并行的问题。
  • View 层全面控制,对于需要服务端处理数据和客户端渲染有天然的优势。
  • Node.js 中的 HTTP 接口的设计支持许多 HTTP 协议中原本用起来很困难的特性。

回到 HelloWorld

var http = require('http');http.createServer(function (request, response){  response.writeHead(200, {'Content-Type': 'text/html'});  response.write('hello');  response.write(' world ');  response.write('~ ');  response.end();}).listen(8080, "127.0.0.1");

  • HTTP 头 Transfer-Encoding=chunked ,我的天啊,太神奇了!
  • 如果只是 response.write 数据,没有指示 response.end ,那么这个响应就没有结束,浏览器会保持这个请求。在没有调用 response.end 之前,我们完全可以通过 response.write 来 flush 内容。
  • 把 Bigpipe Node.js 实现是从 HelloWorld 开始,心情有点小激动。

完整点

layout.html

<!DOCTYPE html><html><head>	<!-- css and js tags -->    <link rel="stylesheet" href="index.css" />    <script> function renderFlushCon(selector, html) { document.querySelector(selector).innerHTML = html; } </script></head><body>    <div id="A"></div>    <div id="B"></div>    <div id="C"></div>

  • head 里面放我们要加载的 assets
  • 输出页面框架,A/B/C 模块的占位

var http = require('http');var fs = require('fs');http.createServer(function(request, response) {  response.writeHead(200, { 'Content-Type': 'text/html' });  // flush layout and assets  var layoutHtml = fs.readFileSync(__dirname + "/layout.html").toString();  response.write(layoutHtml);    // fetch data and render  response.write('<script>renderFlushCon("#A","moduleA");</script>');  response.write('<script>renderFlushCon("#C","moduleC");</script>');  response.write('<script>renderFlushCon("#B","moduleB");</script>');    // close body and html tags  response.write('</body></html>');  // finish the response  response.end();}).listen(8080, "127.0.0.1");

页面输出:

moduleAmoduleBmoduleC

  • flush layout 的内容 包含浏览器渲染的函数
  • 然后进入核心的取数据、模板拼装,将可执行的内容 flush 到浏览器
  • 浏览器进行渲染(此处还未引入并行处理)
  • 关闭 body 和 HTML 标签
  • 结束响应 完成一个请求

express 实现

var express = require('express');var app = express();var fs = require('fs');app.get('/', function (req, res) {  // flush layout and assets  var layoutHtml = fs.readFileSync(__dirname + "/layout.html").toString();  res.write(layoutHtml);    // fetch data and render  res.write('<script>renderFlushCon("#A","moduleA");</script>');  res.write('<script>renderFlushCon("#C","moduleC");</script>');  res.write('<script>renderFlushCon("#B","moduleB");</script>');    // close body and html tags  res.write('</body></html>');  // finish the response  res.end();});app.listen(3000);

页面输出:

moduleAmoduleBmoduleC

  • express 建立在 Node.js 内置的 HTTP 模块上,实现的方式差不多

koa 实现

var koa = require('koa');var app = koa();app.use(function *() {    this.body = 'Hello world';});app.listen(3000);

  • Koa 不支持 直接调用底层 res 进行响应处理。 res.write()/res.end() 就是个雷区,有幸踩过。
  • koa 中,this 这个上下文对 Node.js 的 request 和 response 对象的封装。this.body 是 response 对象的一个属性。
  • 感觉 koa 的世界就剩下了 generator 和 this.body ,怎么办?继续看文档~
  • this.body 可以设置为字符串, buffer 、stream 、 对象 、 或者 null 也行。
  • stream stream stream 说三遍可以变得很重要。

流的意义

关于流,推荐看 @愈之的 通通连起来 – 无处不在的流 ,感触良多,对流有了新的认识,于是接下来连连看。

var koa = require('koa');var View = require('./view');var app = module.exports = koa();app.use(function* () {  this.type = 'html';  this.body = new View(this);});app.listen(3000);

view.js

var Readable = require('stream').Readable;var util = require('util');var co = require('co');var fs = require('fs');module.exports = Viewutil.inherits(View, Readable);function View(context) {  Readable.call(this, {});  // render the view on a different loop  co.call(this, this.render).catch(context.onerror);}View.prototype._read = function () {};View.prototype.render = function* () {  // flush layout and assets  var layoutHtml = fs.readFileSync(__dirname + "/layout.html").toString();  this.push(layoutHtml);    // fetch data and render  this.push('<script>renderFlushCon("#A","moduleA");</script>');  this.push('<script>renderFlushCon("#C","moduleC");</script>');  this.push('<script>renderFlushCon("#B","moduleB");</script>');    // close body and html tags  this.push('</body></html>');  // end the stream  this.push(null);};

页面输出:

moduleAmoduleBmoduleC

  • Transfer-Encoding:chunked
  • 服务端和浏览器端建立管道,通过 this.push 将内容从服务端传输到浏览器端

并行的实现

目前我们已经完成了 koa 和 express 分块传输的实现,我们知道要输出的模块 A 、模块 B 、模块 C 需要并行在服务端生成内容。在这个时候来回顾下传统的网页渲染方式,A / B / C 模块同步渲染:

采用分块传输的模式,A / B / C 服务端顺序执行,A / B / C 分块传输到浏览器渲染:

时间明显少了,然后把服务端的顺序执行换成并行执行的话:

通过此图,并行的意义是显而易见的。为了寻找并行执行的方案,就不得不 追溯异步编程 的历史。(读史可以明智,可以知道当下有多不容易)

callback 的方式

  • 首先 过多 callback 嵌套 实现异步编程是地狱
  • 第二 选择绕过地狱,选择成熟的模块来取代

async 的方式

  • async 算是异步编码流程控制中的元老。
  • parallel(tasks, [callback]) 并行执行多个函数,每个函数都是立即执行,不需要等待其它函数先执行。传给最终 callback 的数组中的数据按照 tasks 中声明的顺序,而不是执行完成的顺序。

var Readable = require('stream').Readable;var inherits = require('util').inherits;var co = require('co');var fs = require('fs');var async = require('async');inherits(View, Readable);function View(context) {  Readable.call(this, {});  // render the view on a different loop  co.call(this, this.render).catch(context.onerror);}View.prototype._read = function () {};View.prototype.render = function* () {  // flush layout and assets  var layoutHtml = fs.readFileSync(__dirname + "/layout.html").toString();  this.push(layoutHtml);  var context = this;  async.parallel([    function(cb) {      setTimeout(function(){        context.push('<script>renderFlushCon("#A","moduleA");</script>');        cb();      }, 1000);    },    function(cb) {      context.push('<script>renderFlushCon("#C","moduleC");</script>');      cb();    },    function(cb) {      setTimeout(function(){        context.push('<script>renderFlushCon("#B","moduleB");</script>');        cb();      }, 2000);    }  ], function (err, results) {    // close body and html tags    context.push('</body></html>');    // end the stream    context.push(null);  });  };module.exports = View;

页面输出:

moduleCmoduleAmoduleB

  • 模块显示的顺序是 C>A>B ,这个结果也说明了 Node.js IO 不阻塞
  • 优先 flush layout 的内容
  • 利用 async.parallel 并行处理 A 、B 、C ,通过 cb() 回调来表示该任务执行完成
  • 任务执行完成后 执行结束回调,此时关闭 body/html 标签 并结束 stream

每个 task 函数执行中,如果有出错,会直接最后的 callback。此时会中断,其他未执行完的任务也会停止,所以这个并行执行的方法处理异常的情况需要比较谨慎。

另外 async 里面有个 each 的方法也可以实现异步编程的并行执行:

each(arr, iterator(item, callback), callback(err))

稍微改造下:

var options = [  {id:"A",html:"moduleA",delay:1000},  {id:"B",html:"moduleB",delay:0},  {id:"C",html:"moduleC",delay:2000}];async.forEach(options, function(item, callback) {   setTimeout(function(){    context.push('<script>renderFlushCon("#'+item.id+'","'+item.html+'");</script>');    callback();  }, item.delay);  }, function(err) {   // close body and html tags  context.push('</body></html>');  // end the stream  context.push(null);});

  • 结果和 parallel 的方式是一致的,不同的是这种方式关注执行过程,而 parallel 更多的时候关注任务数据

我们会发现在使用 async 的时候,已经引入了 co ,co 也是异步编程的利器,看能否找到更简便的方法。

co

co 作为一个异步流程简化工具,能否利用强大的生成器特性实现我们的并行执行的目标。其实我们要的场景很简单:

多个任务函数并行执行,完成最后一个任务的时候可以进行通知执行后面的任务。

var Readable = require('stream').Readable;var inherits = require('util').inherits;var co = require('co');var fs = require('fs');// var async = require('async');inherits(View, Readable);function View(context) {  Readable.call(this, {});  // render the view on a different loop  co.call(this, this.render).catch(context.onerror);}View.prototype._read = function () {};View.prototype.render = function* () {  // flush layout and assets  var layoutHtml = fs.readFileSync(__dirname + "/layout.html").toString();  this.push(layoutHtml);  var context = this;  var options = [    {id:"A",html:"moduleA",delay:100},    {id:"B",html:"moduleB",delay:0},    {id:"C",html:"moduleC",delay:2000}  ];  var taskNum = options.length;  var exec = options.map(function(item){opt(item,function(){    taskNum --;    if(taskNum === 0) {      done();    }   })});  function opt(item,callback) {    setTimeout(function(){      context.push('<script>renderFlushCon("#'+item.id+'","'+item.html+'");</script>');      callback();    }, item.delay);  }  function done() {    context.push('</body></html>');      // end the stream    context.push(null);  }  co(function* () {     yield exec;  });  };module.exports = View;

  • yield array 并行执行数组内的任务。
  • 为了不使用 promise 在数量可预知的情况 ,加了个计数器来判断是否已经结束,纯 co 实现还有更好的方式?
  • 到这个时候,才发现生成器的特性并不能应运自如,需要补一补。

co 结合 promise

这个方法由@大果同学赞助提供,写起来优雅很多。

var options = [  {id:"A",html:"moduleAA",delay:100},  {id:"B",html:"moduleBB",delay:0},  {id:"C",html:"moduleCC",delay:2000}];var exec = options.map(function(item){ return opt(item); });function opt(item) {  return new Promise(function (resolve, reject) {  setTimeout(function(){      context.push('<script>renderFlushCon("#'+item.id+'","'+item.html+'");</script>');      resolve(item);    }, item.delay);  });}function done() {  context.push('</body></html>');    // end the stream  context.push(null);}co(function* () {   yield exec;}).then(function(){  done();});

ES 7 async/wait

如果成为标准并开始引入,相信代码会更精简、可读性会更高,而且实现的思路会更清晰。

async function flush(Something) {  	await Promise.all[moduleA.flush(), moduleB.flush(),moduleC.flush()]	context.push('</body></html>');      // end the stream    context.push(null);}

  • 此段代码未曾跑过验证,思路和代码摆在这里,ES 7 跑起来 ^_^。

Midway

写到这里太阳已经下山了,如果在这里来个“预知后事如何,请听下回分解”,那么前面的内容就变成一本没有主角的小说。

Midway 是好东西,是前后端分离的产物。分离不代表不往来,而是更紧密和流畅。因为职责清晰,前后端有时候可以达到“你懂的,懂!”,然后一个需求就可以明确了。用 Node.js 代替 Webx MVC 中的 View 层,给前端实施 Bigpipe 带来无限的方便。

>Midway 封装了 koa 的功能,屏蔽了一些复杂的元素,只暴露出最简单的 MVC 部分给前端使用,降低了很大一部分配置的成本。

一些信息

  • Midway 其实支持 express 框架和 koa 框架,目前主流应该都是 koa,Midway 5.1 之后应该不会兼容双框架。
  • Midway 可以更好地支持 generators 特性
  • midway-render this.render(xtpl,data) 内容直接通过 this.body 输出到页面。

function renderView(basePath, viewName, data) {  var me = this;  var filepath = path.join(basePath, viewName);  data = utils.assign({}, me.state, data);  return new Promise(function(resolve, reject) {    function callback(err, ret) {      if (err) {        return reject(err);      }      // 拼装后直接赋值this.body      me.body = ret;      resolve(ret);    }    render(filepath, data, callback);  });}

MVC

  • Midway 的专注点是做前后端分离,Model 层其实是对后端的 Model 做一层代理,数据依赖后端提供。
  • View 层 模板使用 xtpl 模板,前后端的模板统一。
  • Controller 把路由和视图完整的结合在了一起,通常在 Controller 中实现 this.render。

Bigpipe 的位置

了解 Midway 这些信息,其实是为了弄清楚 Bigpipe 在 Midway 里面应该在哪里接入会比较合适:

  • Bigpipe 方案需要实现对内容的分块传输,所以也是在 Controller 中使用。
  • 拼装模板需要 midway-xtpl 实现拼装好字符串,然后通过 Bigpipe 分块输出。
  • Bigpipe 可以实现对各个模块进行取数据和拼装模块内容的功能。

建议在 Controller 中作为 Bigpipe 模块引入使用,取代原有 this.render 的方式进行内容分块输出

场景

什么样的场景比较适合 Bigpipe,结合我们现有的东西和开发模式。

  • 类似于卖家中心,模块多,页面长,首屏又是用户核心内容。
  • 每个模块的功能相对独立,模板和数据都相对独立。
  • 非首屏模块还是建议用滚动加载,减少首屏传输量。
  • 主框架输出 assets 和 bigpipe 需要的脚本,主要的是需要为模块预先占位。
  • 首屏模块是可以固定或者通过计算确认。
  • 模块除了分块输出,最好也支持异步加载渲染的方式。

封装

最后卖家中心的使用和 Bigpipe 的封装,我们围绕着前面核心实现的分块传输和并行执行,目前的封装是这样的:

由于 Midway this.render 除了拼装模板会直接 将内容赋值到 this.body,这种时候回直接中断请求,无法实现我们分块传输的目标。所以做了一个小扩展:

midway-render 引擎里面 添加只拼装模板不输出的方法 this.Html

// just output html no render; app.context.Html = utils.partial(engine.renderViewText, config.path);

renderViewText

function renderViewText(basePath, viewName, data) {  var me = this;  var filepath = path.join(basePath, viewName);  data = utils.assign({}, me.state, data);  return new Promise(function(resolve, reject) {    render(filepath, data, function(err, ret){      if (err) {        return reject(err);      }      //此次 去掉了 me.body=ret      resolve(ret);    });  });}

  • midway-render/midway-xtpl 应该有扩展,但是没找到怎么使用,所以选择这样的方式。

View.js 模块

'use strict';var util = require('util');var async = require('async');var Readable = require('stream').Readable;var midway = require('midway');var DataProxy = midway.getPlugin('dataproxy');// 默认主体框架var defaultLayout = '<!DOCTYPE html><html><head></head><body></body>';exports.createView = function() {  function noop() {};  util.inherits(View, Readable);  function View(ctx, options) {    Readable.call(this);    ctx.type = 'text/html; charset=utf-8';    ctx.body = this;    ctx.options = options;    this.context = ctx;    this.layout = options.layout || defaultLayout;    this.pagelets = options.pagelets || [];    this.mod = options.mod || 'bigpipe';    this.endCB = options.endCB || noop;  }  /** * * @type {noop} * @private */  View.prototype._read = noop;  /** * flush 内容 */  View.prototype.flush = function* () {    // flush layout    yield this.flushLayout();    // flush pagelets    yield this.flushPagelets();  };  /** * flush主框架内容 */  View.prototype.flushLayout = function* () {    this.push(this.layout);  }  /** * flushpagelets的内容 */  View.prototype.flushPagelets = function* () {    var self = this;    var pagelets = this.pagelets;    // 并行执行    async.each(pagelets, function(pagelet, callback) {      self.flushSinglePagelet(pagelet, callback);    }, function(err) {      self.flushEnd();    });  }  /** * flush 单个pagelet * @param pagelet * @param callback */  View.prototype.flushSinglePagelet = function(pagelet, callback) {    var self = this,      context = this.context;    this.getDataByDataProxy(pagelet,function(data){      var data = pagelet.formateData(data, pagelet) || data;      context.Html(pagelet.tpl, data).then(function(html) {        var selector = '#' + pagelet.id;        var js = pagelet.js;        self.arrive(selector,html,js);        callback();      });    });  }  /** * 获取后端数据 * @param pagelet * @param callback */  View.prototype.getDataByDataProxy = function(pagelet, callback) {    var context = this.context;    if (pagelet.proxy) {      var proxy = DataProxy.create({        getData: pagelet.proxy      });      proxy.getData()        .withHeaders(context.request.headers)        .done(function(data) {          callback && callback(data);        })        .fail(function(err) {          console.error(err);        });    }else {      callback&&callback({});    }  }  /** * 关闭html结束stream */  View.prototype.flushEnd = function() {    this.push('</html>');    this.push(null);  }  // Replace the contents of `selector` with `html`.  // Optionally execute the `js`.  View.prototype.arrive = function (selector, html, js) {      this.push(wrapScript(          'BigPipe(' +              JSON.stringify(selector) + ', ' +              JSON.stringify(html) +              (js ? ', ' + JSON.stringify(js) : '') + ')'      ))  }  function wrapScript(js) {    var id = 'id_' + Math.random().toString(36).slice(2)    return '<script id="' + id + '">'      + js      + ';remove(\'#' + id + '\');</script>'  }  return View;}

  • context.html 拼装各个 pagelet 的内容

Controller 调用

var me = this;var layoutHtml = yield this.Html('p/seller_admin_b/index', data);yield new View(me, {  layout: layoutHtml, // 拼装好layout模板  pagelets: pageletsConfig,  mod: 'bigpie'  // 预留模式选择}).flush();

  • layoutHtml 拼装好主框架模板
  • 每个 pagelets 的配置

{	id: 'seller_info',//该pagelet的唯一id    proxy: 'Seller.Module.Data.seller_info', // 接口配置    tpl: 'sellerInfo.xtpl', //需要的模板    js: '' //需要执行的js}

  • proxy 和 tpl 获取数据和拼装模板需要并行执行
  • js 通常进行模块的初始化

改进

思路和代码实现都基于现有的场景和技术背景,目前只有实现的思路和方案尝试,还没形成统一的解决方案,需要更多的场景来支持。目前有些点还可以改进的:

  • 代码可以采用 ES6/ES7 新特性进行改造会更优雅,时刻结合 Midway 的升级进行改进。
  • 分块传输机制存在一些低版本浏览器不兼容的情况,最好实现异步加载模块的方案,分双路由,根据用户设备切换路由。
  • 对于每个模块和内容进行异常处理,设置一个请求的时间限制,达到限制时间,关闭链接,不要让页面挂起。此时把本来需要进行分块传输的模块通过异步的方式引入。
  • 并行的实现方案目前采用 async.each,需要从性能上进行各方案的对比
声明
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
HTML:建立网页的结构HTML:建立网页的结构Apr 14, 2025 am 12:14 AM

HTML是构建网页结构的基石。1.HTML定义内容结构和语义,使用、、等标签。2.提供语义化标记,如、、等,提升SEO效果。3.通过标签实现用户交互,需注意表单验证。4.使用、等高级元素结合JavaScript实现动态效果。5.常见错误包括标签未闭合和属性值未加引号,需使用验证工具。6.优化策略包括减少HTTP请求、压缩HTML、使用语义化标签等。

从文本到网站:HTML的力量从文本到网站:HTML的力量Apr 13, 2025 am 12:07 AM

HTML是一种用于构建网页的语言,通过标签和属性定义网页结构和内容。1)HTML通过标签组织文档结构,如、。2)浏览器解析HTML构建DOM并渲染网页。3)HTML5的新特性如、、增强了多媒体功能。4)常见错误包括标签未闭合和属性值未加引号。5)优化建议包括使用语义化标签和减少文件大小。

了解HTML,CSS和JavaScript:初学者指南了解HTML,CSS和JavaScript:初学者指南Apr 12, 2025 am 12:02 AM

WebDevelovermentReliesonHtml,CSS和JavaScript:1)HTMLStructuresContent,2)CSSStyleSIT和3)JavaScriptAddSstractivity,形成thebasisofmodernWebemodernWebExexperiences。

HTML的角色:构建Web内容HTML的角色:构建Web内容Apr 11, 2025 am 12:12 AM

HTML的作用是通过标签和属性定义网页的结构和内容。1.HTML通过到、等标签组织内容,使其易于阅读和理解。2.使用语义化标签如、等增强可访问性和SEO。3.优化HTML代码可以提高网页加载速度和用户体验。

HTML和代码:仔细观察术语HTML和代码:仔细观察术语Apr 10, 2025 am 09:28 AM

htmlisaspecifictypefodyfocusedonstructuringwebcontent,而“代码” badlyLyCludEslanguagesLikeLikejavascriptandPytyPythonForFunctionality.1)htmldefineswebpagertuctureduseTags.2)“代码”代码“ code” code code code codeSpassSesseseseseseseseAwiderRangeLangeLangeforLageforLogageforLogicIctInterract

HTML,CSS和JavaScript:Web开发人员的基本工具HTML,CSS和JavaScript:Web开发人员的基本工具Apr 09, 2025 am 12:12 AM

HTML、CSS和JavaScript是Web开发的三大支柱。1.HTML定义网页结构,使用标签如、等。2.CSS控制网页样式,使用选择器和属性如color、font-size等。3.JavaScript实现动态效果和交互,通过事件监听和DOM操作。

HTML,CSS和JavaScript的角色:核心职责HTML,CSS和JavaScript的角色:核心职责Apr 08, 2025 pm 07:05 PM

HTML定义网页结构,CSS负责样式和布局,JavaScript赋予动态交互。三者在网页开发中各司其职,共同构建丰富多彩的网站。

HTML容易为初学者学习吗?HTML容易为初学者学习吗?Apr 07, 2025 am 12:11 AM

HTML适合初学者学习,因为它简单易学且能快速看到成果。1)HTML的学习曲线平缓,易于上手。2)只需掌握基本标签即可开始创建网页。3)灵活性高,可与CSS和JavaScript结合使用。4)丰富的学习资源和现代工具支持学习过程。

See all articles

热AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover

AI Clothes Remover

用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool

Undress AI Tool

免费脱衣服图片

Clothoff.io

Clothoff.io

AI脱衣机

AI Hentai Generator

AI Hentai Generator

免费生成ai无尽的。

热门文章

R.E.P.O.能量晶体解释及其做什么(黄色晶体)
3 周前By尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.最佳图形设置
3 周前By尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.如果您听不到任何人,如何修复音频
3 周前By尊渡假赌尊渡假赌尊渡假赌
WWE 2K25:如何解锁Myrise中的所有内容
1 个月前By尊渡假赌尊渡假赌尊渡假赌

热工具

ZendStudio 13.5.1 Mac

ZendStudio 13.5.1 Mac

功能强大的PHP集成开发环境

SublimeText3 Mac版

SublimeText3 Mac版

神级代码编辑软件(SublimeText3)

记事本++7.3.1

记事本++7.3.1

好用且免费的代码编辑器

SublimeText3 Linux新版

SublimeText3 Linux新版

SublimeText3 Linux最新版

螳螂BT

螳螂BT

Mantis是一个易于部署的基于Web的缺陷跟踪工具,用于帮助产品缺陷跟踪。它需要PHP、MySQL和一个Web服务器。请查看我们的演示和托管服务。