复杂 Web 前端项目的构建工具优化实践

标签: tuicool | 发表时间:2016-11-16 08:00 | 作者:
出处:http://itindex.net/admin/pagedetail

前言:本人于 2014 年底开始供职于百度贴吧(以下简称“贴吧”)。贴吧作为中国最大规模的 UGC 产品之一,在PC和移动端上承载了数亿用户的访问。在过去十几年的运营中,贴吧积累了十分复杂的业务模式。在 Web 前端,一度有超过40名工程师同时开发、提交和上线,为此,贴吧建设了非常复杂和完备的开发体系。但随着业界技术的不断进步,贴吧的技术架构也在不断尝试和调整,我们在此过程中也不断遭遇了新的挑战,相应地也就引出了本文的内容,而它的意义远远不限于贴吧这一产品本身。

一、背景

项目构建,或者称之为 编译,早已经成为了 Web 前端项目在发布过程中的一个必不可少的环节。从最早的 JavaScript 与 CSS 压缩合并,发展到今天 ES2015、ES2016、Less、Sass 等预处理语言的转换,构建的压力越来越大,其流程也越来越复杂,简单的 Shell、Make、Ant 等单纯的任务处理工具早已经不能很好地满足需求,一个是效率问题,一个是可扩展性问题。因此,业界依托 NodeJS,发展出了多个优秀的开源构建工具,它们有的专注于 Web 前端项目集成化,提供简单易用的编程接口,有的专注于柔韧性,容易扩展,已经不再仅仅局限于能处理特定的项目类型。

但是,随着项目复杂度的提升,这些构建工具也会慢慢暴露出一些不足,主要体现在性能和效率上,这会让原本已经由于复杂的流程导致的慢构建雪上加霜。本文以一个同构 JavaScript 应用为例,来说明典型的大型复杂前端项目构建中遇到的效率瓶颈以及解决问题的一些思路。同构 JavaScript 应用的特点是,同一份 JavaScript 源代码既需要运行在 NodeJS 服务端,又需要运行在浏览器客户端,同时还要考虑到其它静态资源如 CSS、图片和多媒体。

下图是该项目构建流程的一个简化版本,可以看到,不同来源的 JavaScript 文件都有两个构建分支,即 流程分解,并且还存在不同源文件的构建 流程合并的情况。此外,对于不需要任何构建环节的其它(遗留的)文件,比如可能的配置文件、二进制静态资源文件、自定义文件类型等等,我们要求它们只有拷贝到目标目录就可以了。以此为例,并考虑到性能和效率问题,我们对构建工具的最低要求包括:

  1. 支持对任意文件的任意数量、任意流程的构建:Web 前端项目的架构变幻无穷,相对应的构建流程可能是任意的,并且无法预测,事实是,我们的前端架构确实多种多样。如上图中,不同文件的构建流程已经不再是单纯的平行序列图,而是一个有向的拓扑图。这就要求构建工具对流程的定制非常灵活,只要是合理的(包括但不限于不含有向环),就应该可以实现。

  2. 支持文件级别的构建增量:构建分为全量构建和增量构建,顾名思义,全量构建是完全从源文件读取后进行构建,增量构建为仅构建最少的必要文件,是全量构建之后应对部分文件被其它进程修改的策略。相比于全量构建,增量构建在速度上有显著的优势,通常用于开发者本地的实时预览和部署,提升开发效率。增量构建要求尽可能避免不必要的文件构建,同时要求对于可能受影响的所有文件,都必须连带构建,保证实时性。

  3. 支持遗留文件的提取:构建一般是针对特定类型的文件的,这通过枚举来实现。在项目中,往往还存在着一些难以枚举的文件资源,这些资源可能不需要任何处理,只要保证在项目构建之后保留原样即可,包括文件内容和文件路径。如何需要对它们也进行构建操作,则不必挨个执行文件选择,因为这可能非常繁琐。

下面我们先讨论现有开源构建工具在满足以上需求时的不足之处,再想方设法予以改进。

二、现有方案

Web 前端领域常用的几款构建工具,包括 GruntGulpWebpack

  1. Grunt 资历最深,因此也发展出了非常繁荣的插件生态。它基于 glob选择文件集合并执行配置好的构建流程。Grunt 是基于文件构建的,因此在构建中的每一个环节都必须读取和写入磁盘文件,这是 Grunt 最为人诟病的地方,因为这意味你要想办法为每一个构建环节设计文件的输入输出目录,保证不与其它流程发出冲突;再一点就是读取写入磁盘文件过于消耗时间,导致 Grunt 在构建大型复杂项目时比较缓慢。在增量构建上,Grunt大多以“任务”为单位,这其中会包含很多不必要文件的构建。除非在必要的环节中手动设计缓存,否则 Grunt 也不显式支持缓存。此外,Grunt 不能提取遗留文件。

  2. Gulp 与 Grunt 比较类似,定位也近乎相同,与 Grunt 的最大的不同是 Gulp 是基于流( Stream)构建的而不是文件。这个特性节省了对文件的大量磁盘读写操作,使得 Gulp 的构建速度有了明显的提升。更重要的是,Gulp 可以让构建流程配置起来更清晰简单,支持流程分解,但不支持流程合并。Gulp 也有与 Grunt 一样的增量构建粒度的问题,同时也不能提取遗留文件。

  3. Webpack 与其说是构建工具,倒不如说是模块打包器,官方的说法是叫做 “Module Bundler”。它与 Gulp 和 Grunt 并没有太多可比性,也不能很好地完成我们的任务。但 Webpack 有两个能力是值得借鉴的,即 “loader” 中缓存单个文件构建结果的能力,以及对文件之间依赖关系的定制能力。熟悉编写 Webpack loader 的人可能会对 addDependency方法比较熟悉,这个方法用于声明文件之间的依赖关系。这在增量构建时非常有用,例如 a依赖于 b,那么如果 b文件需要重新构建,那么显然, a也必须舍弃缓存,重新构建

可见 Grunt、Gulp 和 Webpack 虽然都是 Web 前端构建常用的优秀开源工具,但在应用于复杂项目时,仍存在一些可优化之处。构建工具本身的能力、效率和性能提升将让任意架构更加容易实现,不再让大胆的设计受限于工具。

三、关键改进

基于同构 JavaScript 应用的实际构建需要,针对我们刚刚分析的现存构建工具的不足,我们实施了从以下几点出发的改进。为便于语义表述,本文使用 TypeScript语言编写代码示例,但在实际实现时一般直接由 JavaScript 语言来承担所有任务。

1. 节点缓存

项目构建的本质是对文件变换操作的有序排列集合。变换一般是针对文件内容的,如空白压缩、语法转换,于是,我们将变换抽象为一个函数 transform,它的输入是文件的的内容,可以是二进制的 Buffer,也可以是文本的字符串(String),输出则是转换后新的文件内容。用接口语法表示即如下面的代码块。将多个 Transformer串联起来就是常规的构建过程。

interface Transformer {
  transform (content: string): string;
}

显然,对于相同 content的值,如果 transform的返回值一直保持相同,那么第一次以后的操作都是不必要的,因为无论重复多少遍返回都是一样。如果熟悉 redux,那么一定对其中的 reducer概念印象深刻,它是一个“纯函数(pure function)”,无论什么时候,同样的输入总是有同样的输出。该特性也是 Transformer能够实现缓存的基础,也就是说,以 content为索引,只要索引不变,缓存就可以一直有效,构建速度也会大幅度提升。在实际的操作中,可以取 content的 md5 值作为实际的索引,于是一个典型的可缓存 Transformer可以是这样的:

class FooTransformer implements Transformer {
  transform (content: string) {
    let key = md5(content);
    if(cache.has(key)) {
      return cache.get(key);
    } else {
      const newContent = this._transform(content);
      key = md5(newContent);
      cache.set(key, newContent);
      return newContent;
    }
  }
}

然而并非所有 transform方法都有一个入参,例如 Transformer可能需要一个 options构造参数,它极有可能导致在相同的 content上产生不一致的输出。在这情况下,不建议再使用缓存机制,如果一定要使用的话,那么 key值的算法将会开始变得复杂起来,并且容易导致出错。所以,明确你的 transform到底做了什么,再使用缓存机制提升构建速度。

另外, transform方法的入参很可能不是仅仅一个 content,而是一个集合,这种情况下,文件名也要用来区分它们,你需要的不是一个 transform,而是一个 transformAll,入参则是 File的集合 FileCollection,而 File对象,至少必须包括 filenamecontent两个属。 Transformer接口的定义则为:

interface File {
  filename: string;
  content: any;
}

interface FileCollection {
  [index: number]: File;
}

interface Transformer {
  transform (file: File): File;
  transformAll (files: FileCollection): FileCollection;
}

针对这两种类型的 Transformer,我们以一个属性来做区分:

interface Transformer {
  isTorrential(): boolean;
}

isTorrential()方法返回真时, transformAll方法将代替 transform被调用。显然,由于入参变得非常多,设置缓存也变得困难重重,索性直接抛弃不要。需要指出的是, transformAll的内部可以调用 transform

最后, transformtransformAll可能都不是同步的,返回一个 Promise比返回一个 StringFile对象更合适。

总结一下,本节分析了项目构建的实质和基本单元,在基本单元 Tranformer中设置缓存可以有限提升构建的速度。但要清楚地知道什么时候该使用缓存,什么时候不该使用,始终让构建行为符合预期。

2. 拓扑路径

前面提到, 项目构建是由 Transformer的有序集合构成的Transformer可以分解也可以合并,这非常像水流,因此我们形象地称之为 Stream。每一个 Stream对象内部维护最多一个 Transformer实例成员,并以 File对象的集合作为 Stream的有效载荷,一般地,它们往往在某种意义上是同类型的文件。

interface Stream {
  tranformer: Transformer;
}

由于流与流之间的关系构成了一个有向的拓扑图,因此 Stream对象需要维护与其它 Stream之间的关系,添加 upriversdownrivers成员以实现,分别称为直属“ 上游流”和直属“ 下游流”:

interface Stream {
  uprivers: StreamCollection;
  downrivers: StreamCollection;
}

interface StreamCollection {
  [index: number]: Stream;
  push(stream: Stream):number;
}

以下图为例,图中序号 ① 的 downrivers为 [②,⑤], ② 的 uprivers为 [①,④],依此类推。

downrivers数量大于1的流称为“ 分解流”, uprivers数量大于1的流称为“ 合并流”。上图中,① 为分解流,② 为合并流。顾名思义,分解流是一个以上构建流程具有相同上游公共部分的简单表述,但合并流却并不总是代表一个以上的构建流程具有相同的下游公共部分,更重要的是, 合并流的输入是其所有上游流的输出。对于合并流来讲,它的输入文件极有可能有着不同的来源。举一个现实的例子,输入 browserify的文件极有可能来自 node_modules目录内,但更多来自于你在 src目录编写的源代码。显然,这两种来源的文件很容易有着不同的前期构建流程,比如源代码是由 ES2015编写的,但 node_moduels则为 ES5语法。

在构筑流的拓扑图的时候,我们可以定义一个 connect方法来建立它们之间的联系:

interface Stream {
  connect(downriver: Stream): Stream;
}

class FooStream implements Stream {
  connect (downriver: Stream) {
    this.downrivers.push(downriver);
    downriver.uprivers.push(this);
    return downriver;
  }
}

那么建设上图中的拓扑的代码就可以是:

s1.connect(s2).connect(s6).connect(s3)
s1.connect(s5).connect(s4).connect(s2);

实际的流拓扑图大多数是二维的,不过达到三维的复杂度也是合理的,请看下图。先不论此图是否反应了项目构建的一个真实流程,至少它表达了构建流程可能达到的复杂度。

下面我们就以上图为例探讨流拓扑图的正确工作方式。首先,基于性能和功能考量,我们的目标包括,在完整的一次构建流程中:

  • 包括分解流在内的所有流,都只能运行一次。这意味着,同一个文件不会被传入 Streamtransformer成员一次以上;
  • 合并流的所有上游流运行完毕后,合并流才能开始运行,原因刚刚已经说明,它需要上游流的所有输出 加和同时作为输入

我们把 Stream的入口 API 方法称为 flow

interface Stream {
  flow (files: FileCollection): Promise;
}

上一节提到, transform/transfromAll方法的返回值应该是 Promise,那么 flow的返回值也同样是 Promise

每个流在执行 flow操作时,其实质都是在调用其内部的 transformer成员,根据 isTorrential()返回值的不同来决定调用它的 transform或者 transformAll方法。从这里可以看到,其实 Transformer内部的缓存也可以移至 Stream中存储,在 Transformer会更灵活一些。由于构建行为的任意性, Transformer将是非常重要的扩展点,注意保持它的轻量性。

class FooStream implements Stream {
  public flow (files: FileCollection) {
    if (this.transformer.isTorrential()) {
      return this.transformer.transformAll(files);
    } else {
      return Promise.all(files.map(file => this.transformer.transform(file)));
    }
  }
}

但一个流完成后,它可能还要把自身的输出传递给下游流(注意:也可以不传递,但仍然要通知)。因此,在 flow中,最后还需要依次通知每一个下游流,通知的接口不妨称之为 notify

interface Stream {
  notify (files: FileCollection): Promise;
}

注意各个下游流是 依次notify的,如果能保证下游流之间没有先后的依赖关系,那么它们确实可以并行,这种依赖关系可能不仅仅体现在拓扑图上,因为拓扑图只表达了文件传递方向的关系。除此之外,你还可以实现其它维度的依赖,为此,下游流以串行的方式运行会更保险一些。

注意,传递给下游流的数据一定是拷贝,避免下游流修改上游流的缓存。

当一个流的所有上游流都完成后,它才开始运行,所以, notify十分有必要在适当的时机激活自身 flow

class FooStream implements Stream {
  public notify (files: FileCollection) {
    this.files.push(...files);
    if (this.isUpriversAllReady) {
      return this.flow(this.files);
    }
  }
}

这样,我们能够保证合并流在正确的时机去运行,也就实现了这类特殊的构建任务。回过头来看上面的那张拓扑图,箭头代表了依赖关系,节点的数字就代表了执行的顺序。读者可以自行思考为什么是这样的顺序。需要注意的是,根据依赖关系定义的先后,最终的执行顺序也是不同的,只要分支没有依赖关系,下游流的顺序先后就无所谓了。

总结一下,本节我们讨论的是如何定义和实现 API 以支持任意的构建流程。构建的灵活性始终是本文讨论的重点,因为你无法想象半年甚至三个月后你的项目会复杂到什么程度,构建系统的建设是一个成本很大的投入,并且会经常随着项目架构的变化而变化,保证充分的可扩展性,会让你面对快速变化时伸缩自如。相比于 Grunt 和 Gulp,我们期望以一种更高效和更直观的方式定义自己的构建流程。

3. 依赖图谱

一个项目中所包含的文件类型可能非常多,特别对于 Web 前端而言,HTML、JavaScript、CSS 和各种图片是必不可少的,近年来,Less、Sass、JSX、ES2015 等前处理语法越来越多,每一种都需要特殊的构建流程。而这些流程的顺序显然不是任意的。在配置 Grunt、Gulp 任务时,我们会下意识地先去处理图片等二进制资源,再处理 CSS 和 JavaScript,最后处理 HTML。这是因为不同种类的文件之间有着微妙的依赖关系:现代的前端构建中,对于静态资源,都会采用文件名加时间戳的方式来规避缓存的影响,这个过程一般是该类型文件的之后一个构建步骤,如果要引用它的最终路径,就必须等到它完全构建完毕。眼光放宽广的话,依赖关系远远不止引用路径这一种形式,如果需要的话,你可以把一些构建的结果信息存储于内存中,作为其它类型文件构建的一种输入。总之,不要将文件之间的依赖关系固化,HTML 依赖 JavaScript 不止有 标签这一种形式,CSS 引用图片也不止 background(-image)这一种。

想达到支持任意的构建流程,定制文件之间的依赖关系是必不可少的,就像 Webpack 的 addDependency方法一样,你可以以任意方式实现这样一个 API:

addDependency('a.css', 'd.png');

有了它,你可以自定义多种依赖关系,举个例子,在任意文件中,使用 __uri()的语法来引用其它静态资源文件的最终线上路径,这不是标准的引用关系,但也许你用得着它。

对于项目初次的全量构建,文件之间的依赖只要人工保证其顺序就可以了,但对于之后的 watch增量构建,依赖关系就变得极为重要。试想你是如何定义 Grunt 或 Gulp 的 watch 动作的,是不是需要配置监听的目录以及要执行的任务?后面的任务由于无法得知到底是哪些文件被修改了,因而不得不都要重新构建一遍。相比之下,Webpack 的增量构建就要迅速得多,几乎是瞬时的,因为它掌握了哪些文件不需要再构建的信息——被修改的文件以及依赖它的所有其它文件都需要重新构建,其它都不需要再构建。这也是一个比较抽象的拓扑图,幸运地是,这个算法非常简单,你只需要递归地收集各个拓扑图上的节点就可以了。

总结一下,文件之间精确的依赖关系是实现高速增量构建的基石,它将增量的维度控制在了文件级别。实时修改和预览是开发过程中的一个重要特性,尤其对于 Web 前端。如果设计合理的话,全量构建的逻辑也可以弱化为一种增量构建,这样看来,全量和增量并没有本质区别。

4. 全量搜索

我们希望构建工具能够覆盖到项目中的所有文件,而不仅仅是 Grunt 和 Gulp 中任务覆盖的那些。这样做有什么好处呢?答案是你的构建逻辑可以不必维护对这些文件资源细节的处理,同时还能轻易实现它们的发布。举个例子,有一些静态资源,包括各种格式的音频、视频和图片,以及其它自定义的特殊格式,它们不需要经过任何的构建处理,只需要发布到特定的目录下,以供 Web 浏览者访问。在 Grunt 和 Gulp 中,你需要时刻注意你的选择器是否选中了它们,否则就会被遗漏。当文件的路径和类型不可枚举时,维护构建的选择器就成了一项负担,也是一项风险,你不能保证所有文件都被选中了,特别是引用了第三方的组件,比如 node_modules

gulp.src('assets/*.{mp4,rmvb,mp3,avi,srt,png,jpg,bmp,ico,webp,zip,rar,gz,pptx,docx,xlsx}')

为了解决这个问题,我们将项目目录下的所有文件都找出来,包括 node_modulesbower_componentsjspm_packages等依赖,一个都不要落下。由于项目构建还是要基于文件选择器的,毕竟同一类的资源才能走同一个构建流程。我们针对每一个选择器来创建一个文件集合,凡是匹配该选择器的文件都放到这个集合中:

interface MatchPair {
  files: FileCollection;
  selector: any;
  headStream: Stream;
}

我们这里使用的选择器语法是 multimatch,因此支持数组或字符串。

现在,创建一个新的文件集合,把所有不匹配任何选择器的文件放到里面。这是一个特殊的集合,专用来收集“遗留的”文件。不论是全量还得增量,构建工具都应该始终维护这些 N+1个文件集合的正确性。 headStream是一个空的流,它没有任何 transformer成员,它的作用仅仅是衔接其它流。于是,项目构建的实质就变成了 files通过各自配对的 headStream。如果你愿意对遗留文件也进行构建,那么也是可行的。

总结一下,本节讨论了一种选择遗留文件的方法。这项功能对于 Grunt 和 Gulp 都不具备,但却是工程实践中非常贴心的,如果你不必关心遗留文件,大可弃之不管,如果需要拷贝等操作,将非常地方便。

四、附加优化与问题

1.磁盘读取优化

显然,读取磁盘文件是构建中比较慢的一环,特别是全量搜索并都读取的话。在初次的构建中,对被选中文件的读取无可厚非,不过,要知道一个文件可以被多个选择器命中,这可能导致一个文件被多次读取。完全优化它其实很简单,向所有读取文件的流传递相同的 File对象(直接从 MatchPairfiles中取出即可),只要 content属性不为 null,则不必再次读取。当监听到该文件被修改后,清除 content,最先匹配这个文件的读取流就会再次读取磁盘,跟在它后面的则仍然不必再次读取,也保证了文件内容的实时性。

2.内存拷贝优化

从上面的设计可以看到,我们没有使用 NodeJS 的 Stream对象来在各个 Transformer之间传递数据(Gulp 是这样做的),不过使用的方式是非常类似的,因为毕竟缓存是以 content为索引的,即使是流也需要等到全部接收完后才开始处理,这和直接传递全部内容没什么分别。我们在 Transformer上设置的缓存,可能带来很多的文件拷贝,如果文件内容过大,则内存会被快速消耗。为了优化此问题,我们设置了一个阈值 f,文件体积超过 f,缓存则由内存写入临时文件。当然,这是基于文件读写要比 transform快的基础上的。

3.依赖关系的泄露

使用 addDependency()来声明依赖是不收敛的,也就是说你可以在任意位置(主要是 Transformer中)调用。我们没有提供一个 clearDependency()方法是因为它的行为可能是片面的——不能预测其它位置是否有相反的行为。设想下面的路径:

  • 第一次构建,声明 a.css 依赖于 a.png 和 b.png;
  • a.css 被修改,移除对 b.png 的依赖,第二次构建,声明 a.css 依赖于 a.png;
  • b.png 被修改,a.css 作为依赖方,第三次构建,声明 a.css 依赖于 a.png

可见,以后 b.png 的所有变化,都会触发 a.css 的重新构建,虽然 a.css已经不再依赖 b.png 了,我们称这是一种依赖关系泄露,就像内存泄露一样。事实上,它也确实造成了轻微的内存泄露。依赖关系泄露的后果就是,带来了不必要的重构建,如果条件合适的话,它可以像多米诺骨牌一样蔓延开来,造成重构风暴。

如果执意要规避这个问题的话,也不是不可能,但我们并没有采取任何手段,因为构建工具的运行周期都不会很长,没有太大必要为了它增加那么复杂的逻辑,何况它并不影响构建的正确性。

五、总结

(点击放大图像)

以上就是我们为了解决当前和以后复杂的前端构建而进行的努力,同构 JavaScript 只是一个应用场景,理论上,我们可以很好地应对任何构建任务,包括 Webpack 的所有功能。与 Grunt、Gulp 类似的是,上面的设计和改进都是非常底层的,如果想真正应用于一个具体的项目架构,可能需要比较多的配置。从这一点上看,这更像是一个构建引擎。附图是该引擎的核心运行架构,我们利用它建设了多个 Web 前端项目,它们在架构上有着非常大的不同,特别是在初期设计时,架构十分不稳定,该引擎很好地支撑了各种差异化的构建任务。针对该引擎的不足和缺陷,我们仍在探索和改进。Grunt 、 Gulp 和 Webpack 依然是功能非常强大的工具,我们从它们身上借鉴了很多,也有许多不谋而合之处。每种工具都有其适合的场景,最重要的不是比较它们的优劣,而是找到最适合的那一个,如果必要,则可以进行适当的改进。

感谢韩婷对本文的审校。

给InfoQ中文站投稿或者参与内容翻译工作,请邮件至[email protected]。也欢迎大家通过新浪微博(@InfoQ,@丁晓昀),微信(微信号: InfoQChina)关注我们。

相关 [复杂 web 前端] 推荐:

复杂 Web 前端项目的构建工具优化实践

- - IT瘾-tuicool
前言:本人于 2014 年底开始供职于百度贴吧(以下简称“贴吧”). 贴吧作为中国最大规模的 UGC 产品之一,在PC和移动端上承载了数亿用户的访问. 在过去十几年的运营中,贴吧积累了十分复杂的业务模式. 在 Web 前端,一度有超过40名工程师同时开发、提交和上线,为此,贴吧建设了非常复杂和完备的开发体系.

Web前端优化

- - JavaScript - Web前端 - ITeye博客
优点:直接使用浏览器内存的缓存数据,减少网站后台压力,用户体验(速度)好. 缺点:对于时时变化的动态页面,这种情况就不能容忍了,因为每次访问的都是第一次访问的内容,这样即使所请求的页面已经变化了,用户也不可能知道,所以此场景必须要消除这种缓存的影响. 延迟加载,将资源延迟到需要的时候的加载,例如detail页面,相关产品推荐,当用户浏览更多的信息往下拉动滚动时,才进行加载,异步加载可以大幅减少对后端资源的使用,在需要的时候加载,是资源合理使用常用的方式,但是也带来一个问题,当往下拉才去加载,如果性能不够好,用户的体验其实是不好的,“菊花”转动的时间会比较长,同时异步加载对前端性能的作用也是非常明显的,渲染的节点数量大幅减少.

Web 前端测试

- - Web前端 - ITeye博客
Web 网站测试流程和方法(转载). 进行正式测试之前,应先确定如何开展测试,不可盲目的测试. 一般网站的测试,应按以下流程来进行:. 1)使用HTML Link Validator将网站中的错误链接找出来;. 2)测试的顺序为:自顶向下、从左到右;. 3)查看页面title是否正确. (不只首页,所有页面都要查看);.

Web 前端攻防(2014版)

- - 博客 - 伯乐在线
外链会产生站外请求,因此可以被利用实施 CSRF 攻击. 目前国内有大量路由器存在 CSRF 漏洞,其中相当部分用户使用默认的管理账号. 通过外链图片,即可发起对路由器 DNS 配置的修改,这将成为国内互联网最大的安全隐患. 百度旅游在富文本过滤时,未考虑标签的 style 属性,导致允许用户自定义的 CSS.

Web前端优化最佳实践

- Jimmy - 中文热文榜|最新
还有 Jason, Bixuan, 曦, 推荐,查看全部 8 个推荐. 博评 - Sting的网经发表于2010-08-08 08:41:10. Google的前端优化最佳实践 Yahoo的前端优化最佳实践. Web前端优化最佳实践之Content篇. 尽量减少 HTTP 请求 (Make Fewer HTTP Requests).

Web前端浏览器兼容初探

- - 博客 - 伯乐在线
浏览器兼容是前端开发人员必须掌握的一个技能,但是初入前端的同学或者其他后台web开发同学往往容易选择忽略,而形成两个极端:. 1 我最开始都是使用IE6,IE6上没问题,其它浏览器坑爹(多出现与前端后端一起搞的同学,小生2年前就这种状态,鼓励人家用ie6.. 2 我要遵循标准,我只要ff就好,IE就是坑爹的玩意,我不必去理他(小生一年前的心态.

web前端性能优化进阶路

- - 阿里巴巴(中国站)用户体验设计部博客
Web前端性能优化WPO,相信大多数前端同学都不会陌生,在各自所负责的站点页面中,也都会或多或少的有过一定的技术实践. 可以说,这个领域并不缺乏成熟技术理论和技术牛人:例如Yahoo的web站点性能优化 黄金法则,以及大名鼎鼎的优化大师 Steve Souders. 本文并非一篇讨论性能优化技术方法的文章,而更多的是对中文站 搜索List页面持续两年多的前端性能优化实践的 思路总结.

web前端面试笔试题+优化

- - JavaScript - Web前端 - ITeye博客
前端是庞大的,包括HTML、CSS、Javascript、Image、Flash等等各种各样的资源. 前端优化是复杂的,针对方方面面的资源都有不同的方式. 那么,前端优化的目的是什么. 从用户角度而言,优化能够让页面加载得更快、对用户的操作响应得更及时,能够给用户提供更为友好的体验. 从服务商角度而言,优化能够减少页面请求数、或者减小请求所占带宽,能够节省可观的资源.

Web前端知识体系精简

- - IT瘾-geek
Web前端技术由html、css和javascript三大部分构成,是一个庞大而复杂的技术体系,其复杂程度不低于任何一门后端语言. 而我们在学习它的时候往往是先从某一个点切入,然后不断地接触和学习新的知识点,因此对于初学者很难理清楚整个体系的脉络结构. 本文将对Web前端知识体系进行简单的梳理,对应的每个知识点点到为止,不作详细介绍.

简化web开发者复杂任务的25个有用的javascript库

- - 博客园_首页
这是非常有用的,因为他们使网站对于用户更加友好. 今天,我列出了一些最有用的轻巧和独特的 JavaScript库. 此列表的JavaScript库将有很大的帮助对于Web开发人员和设计师. 你可以用来创建用户友好的,执行特定功能的代码. 在线浏览PDF文件的实用jquery插件推荐. 精心收藏的14个开发人员方便的jQuery代码片段.