11
2018.3.1更:

有赞·微商城(base杭州)部门招前端啦,最近的前端hc有十多个,跪求大佬扔简历,我直接进行内推实时反馈进度,有兴趣的邮件 lvdada#youzan.com,或直接微信勾搭我 wsldd225 了解跟多

有赞开源组件库·zanUI


webpack文档翻译(完)

动机

现在的网站都向webapp进化:

  • 在页面中有越来越多的js
  • 现在浏览器能做更多的事情
  • 更少的页面重载刷新 在页面中有更多的代码

结论是在浏览器端存在大量的代码。

大段的代码需要被组织,模块系统提供了这个机会把代码分割成模块。

模块系统的风格

有许多中引入依赖导出值的标准:

  • <script>标签风格(没有模块系统)
  • commonjs
  • AMD及其相似
  • ES6
  • 。。。

<script>标签风格

如果没有使用模块系统你将会按如下方式处理模块化的代码

<script src="module1.js"></script>
<script src="module2.js"></script>
<script src="libraryA.js"></script>
<script src="module3.js"></script>

各个模块把接口暴露给全局对象,比如window对象。各个模块之间可以通过全局对象进行访问互相依赖的接口。

普遍的问题:

  • 全局对象的冲突
  • 加载的顺序是重要的
  • 开发者需要解决模块的依赖问题
  • 在大项目中模块引入的数目将会非常长并且难以维护

CommonJs:同步的require

这种风格使用同步的require方法来加载依赖和返回暴露的接口。一个模块可以通过给exports对象添加属性,或者设置module.exports的值来描述暴露对象。

require("module");
require("../file.js");
exports.doStuff = function() {};
module.exports = someValue;

CommonJs规范也在服务端nodejs中使用。

利:

  • 服务端代码可以被重用
  • npm中有大量模块
  • 简易使用

弊:

  • 阻塞调用在网络中无法应用,网络请求是异步的
  • 不能同时引入多个模块

AMD:异步的require

异步的模块定义

其他一些模块系统(针对浏览器)对同步的require有问题,引出了异步的版本(一种定义模块暴露值的方式)

require(["module", "../file"], function(module, file) { /* ... */ });
define("mymodule", ["dep1", "dep2"], function(d1, d2) {
  return someExportedValue;
});

利:

  • 符合了在网络中异步请求的风格
  • 可以同时加载多个模块

弊:

  • 编码成本 难读难写
  • 看起来像是一种权宜之计

实践:

  • require.js
  • curl

ES6 模块

ecmascript6添加了一些语言结构。

import "jquery";
export function doStuff() {}
module "localModule" {}

利:

  • 静态分析方便
  • 作为es的标准未来是有保证的。

弊:

  • 本地浏览器支持还需要时间
  • 这种风格几乎没有成型的模块。

无偏见的解决方案

开发者可以选择自己的模块风格,确保现存的代码和包能工作。添加习惯的模块风格是方便的。

传输

模块要在浏览器端运行,所以他们必须要从后端传输到浏览器端。

传输模块会出现两个极端:

  • 每个模块是一个请求
  • 一个请求包含了所有模块

这两种使用方法都是疯狂的,但并不理想:

  • 一个模块对应一个请求

    • 利:只有需要的模块被传输
    • 弊:多请求意味着更多的开销
    • 弊:因为请求延迟会降低应用初始化时间。
  • 一个请求包含所有模块

    • 利:更少的请求开销,更少的延迟
    • 弊:不需要的模块也会被传输

分块的传输

一个更灵活的传输方式会更好,两个极端方式的折中方案在大多数例子中会更好。

当编译所有模块时,把一系列模块分割成许多更小的块。

这就允许更多更小更快的请求,初始阶段不需要的模块可以通过命令加载,这样就可以加速初始加载,当你实际需要代码时也能加载相应的代码块。

「分割点」取决于开发者

阅读更多

为什么只有javascript可以模块化?

为什么模块系统只能帮助开发者解决javascript的模块问题?还有许多其他的资源需要处理:

  • 样式stylesheets
  • 图片images
  • 字体webfonts
  • html模板
  • 。。

编译:

  • coffeescript > javascript
  • elm > javascript
  • less > css
  • jade templates > 生成html的js

使用起来应该跟下列一样方便:

require("./style.less");
require("./template.jade");
require("./image.png");

使用loaders
loaders

静态分析

当编译这些模块时,静态分析尝试寻找模块的所有依赖。

传统上静态分析寻找只能填写字符串(不带变量),但是require('./template/' + templateName + '.jade')是很普遍的结构。

许多第三方库都有不同的书写风格,一些非常诡异。

对策

一个聪明强大的解析器允许几乎所有现存的代码运行,但是开发者写了一些奇怪的代码,这需要找到最合适的解决方法。

什么是webpack

webpack是一个模块打包工具

webpack把有依赖的模块产出代表这些模块的静态资源。

为什么要用模块打包器?

现存的模块打包工具不适用大型项目(大型的单页应用)。开发新的模块打包工具最令人激动的原因就是代码分割,并且静态资源跟模块化也是无缝对接的。

我尝试过拓展现有的模块打包工具,但是无法实现所有目标。

目标

  • 将依赖树分割成可以命令加载的块
  • 保持初始加载时间短
  • 每个静态资源应该能够成为模块
  • 能够将第三方库结合成模块的能力
  • 自定义模块加载器几乎各个部分的能力
  • 适合大项目

webpack有什么不同

代码分割

webpack有两种依赖类型:同步和异步。异步的依赖充当分割的点并形成新的块。当块的树被优化,一个文件就发射每一个块。

loaders

webpack默认处理javascript,但是loaders用来把资源转变为javascript,这样做每个资源都能构成模块。

强大的解析能力

webpack有一个非常强大的解析器用来处理几乎所有第三方的类库。它甚至允许在依赖引入时使用表达式require("./templates/" + name + ".jade")。webpack能处理普遍的模块风格: CommonJs和AMD

插件系统

webpack产出了丰富的插件系统。多数内部特征都在插件系统基础上开发,这允许你根据自己需求自定义webpack,同时也支持贡献通用的开源插件。

使用loaders

什么是loaders

loaders是应用在app中静态资源的转换程序。他们是一些能把资源文件中的源代码作为参数并且返回出新的源代码的函数。

举个例子:你可以使用loaders告诉webpack去加载coffeescript或者jsx

loader 特征

  • loaders可以链式调用,他们可以应用到资源的管道系统。最后一个loader期待返回js,余下的其他loader可以返回任意形式的源代码,都会被传递到下一个loader。
  • loaders可以异步或同步。
  • loaders跑在nodejs
  • loaders接受参数。这可以用来传递配置给loaders
  • loaders可以在配置中绑定拓展后缀/正则
  • loaders可以在npm中发布下载
  • 在loader的package.json的main字段可以导出loader
  • loader能连接配置
  • 插件可以给loaders更多特征
  • loaders可以发射额外的随意文件

解析loaders

loaders与模块有想同的解析方式。一个loader模块预计暴露出一个函数并且使用js在nodejs中编写。通常情况下你在npm中管理loaders,但你也可以在app中管理loaders文件。

引用loader

按照约定(并不强制)loaders通常命名为xxx-loader,xxx是一名字。比如json-loader

你可以通过全名引用loaders(json-loader),或者通过缩略名(json)

loader的名字的约定和搜索优先顺序由webpack配置api中的resolveLoader.moduleTemplates定义。

loader名字的约定是方便的,尤其是用require()进行loader引用的时候。看下列使用方法。

安装loaders

npm install xxx-loader --save-dev

使用方法

有许多中在app中使用loaders的方法:

  • require
  • 在配置文件中进行配置
  • 通过CLI配置

使用require

注意: 尽可能的避免使用require,如果你打算把scripts标签放入环境无关的环境。对特定的loaders使用配置约定。

用require语句指定loaders是可行的,只需要使用!把loaders跟资源分隔开,每一部分都相对于当前目录进行解析。

require("./loader!./dir/file.txt");
// => uses the file "loader.js" in the current directory to transform
//    "file.txt" in the folder "dir".

require("jade!./template.jade");
// => uses the "jade-loader" (that is installed from npm to "node_modules")
//    to transform the file "template.jade"
//    If configuration has some transforms bound to the file, they will still be applied.

require("!style!css!less!bootstrap/less/bootstrap.less");
// => the file "bootstrap.less" in the folder "less" in the "bootstrap"
//    module (that is installed from github to "node_modules") is
//    transformed by the "less-loader". The result is transformed by the
//    "css-loader" and then by the "style-loader".
//    If configuration has some transforms bound to the file, they will not be applied.

配置

你可以把loaders通过配置与正则绑定:

{
    module: {
        loaders: [
            { test: /\.jade$/, loader: "jade" },
            // => "jade" loader is used for ".jade" files

            { test: /\.css$/, loader: "style!css" },
            // => "style" and "css" loader is used for ".css" files
            // Alternative syntax:
            { test: /\.css$/, loaders: ["style", "css"] },
        ]
    }
}

cli

你可以通过cli的拓展绑定loaders:

$ webpack --module-bind jade --module-bind 'css=style!css'

jade loader处理.jade文件,style cssloader处理.css文件

查询参数

loaders穿衣通过查询字符串传递查询参数(跟web一样)。查询字符串使用添加到loader,类似url-loader?mimetype=image/png

注意:查询字符串的形式取决于loader。查询loader的文档。大多数loaders接受常规形式的参数(?key=value&key2=value2)或者json对象(?{"key":"value","key2":"value2"}

require

require("url-loader?mimetype=image/png!./file.png");

配置

{ test: /\.png$/, loader: "url-loader?mimetype=image/png" }

或者

{
    test: /\.png$/,
    loader: "url-loader",
    query: { mimetype: "image/png" }
}

cli

webpack --module-bind "png=url-loader?mimetype=image/png"

使用插件

使用插件添加与webpack打包有关的典型功能。举个例子,BellOnBundlerErrorPlugin插件能在打包器工作的进程中输出错误信息。

内置插件

通过使用webpack的插件属性。

// webpack should be in the node_modules directory, install if not.
var webpack = require("webpack");

module.exports = {
    plugins: [
        new webpack.ResolverPlugin([
            new webpack.ResolverPlugin.DirectoryDescriptionFilePlugin("bower.json", ["main"])
        ], ["normal", "loader"])
    ]
};

其他插件

非内置插件可以通过npm下载或者其他途径。

npm install component-webpack-plugin
var ComponentPlugin = require("component-webpack-plugin");
module.exports = {
    plugins: [
        new ComponentPlugin()
    ]
}

当通过npm安装第三方插件时我们建议使用这个工具: https://www.npmjs.com/package...

这会检查所有安装在依赖中的第三方插件然后在需要的时候进行懒加载。

getting started

欢迎

这个小教程带你过一遍简单的例子

你会学到:

  • 如何安装webpack
  • 如何使用webpack
  • 如何使用loaders
  • 如何使用开发服务器

安装webpack

你需要先安装nodejs

npm install webpack -g
这使得webpack全局命令可用

设置应用

从全新的目录开始。

webpack会分析入口文件中的依赖文件。这些文件(也称模块)也会包含在bundle.js中。webpack会给每个模块一个独立的id然后把所有凭id访问的模块存到bundle.js中。只有入口模块在初始化时会被执行。一个小型的运行时会提供一个require函数,当需要时会执行依赖的资源。

第一个loader

我们想要给应用添加css文件。

webpack默认只支持javascript,所以我们需要css-loader来处理css文件。

执行npm install css-loader style-loader下载安装loaders(他们需要安装在本地,不带-g)这会创建一个loaders存放的node_modules文件夹。

开始使用他们:

require("!style!css!./style.css");
document.write(require("./content.js"))
在引入模块时添加loader的前缀,模块会经历一个loader管道,这些loaders将模块内容以特定的方式进行改变。在所有改变完成后,最后的结果是一个javascript模块。

绑定loaders

我们不想写如此长的引用require("!style!css!./style.css")

我们可以给loaders绑定拓展所以我们只用写require("./style.css")

require("./style.css");
document.write(require("./content.js"))

跑命令

webpack ./entry.js bundle.js --module-bind 'css=style!css'

配置文件

我们想把配置信息移到一个配置文件中:

添加配置文件webpack.config.js:

module.exports = {
    entry: "./entry.js",
    output: {
        path: __dirname,
        filename: "bundle.js"
    },
    module: {
        loaders: [
            { test: /\.css$/, loader: "style!css" }
        ]
    }
};

漂亮的输出

如果项目扩大了应用编译会变慢,所以我们想增加以下进度条,并且带有颜色。。

webpack --progress --colors

监控模式

我们不想每次改动代码都手动进行编译。。

webpack --progress --colors --watch

webpack能缓存未变化的模块,在每次编译后都能产出文件。

当使用监控模式,webpack对所有文件在编译过程都安装了文件监听器。如果任何改动被捕捉到了,webpack会再次编译。当缓存开启了,webpack会把每个模块都存在内存里,如果没有变动就会重复利用。

开发服务器

开发服务器是更棒的选择。

npm install webpack-dev-server -g
webpack-dev-server --progress --colors

这绑定了一个小型express服务器在localhost:8080。作为静态资源和bundle的服务器。当重新编译时浏览器会重新更新。打开 http://localhost:8080/webpack-dev-server/bundle 。

dev 服务器会使用webpack的监控模型,这能防止webpack释放结果文件到硬盘上,安装它保持结果文件都是从内存中读取的。

/

commonjs

CommonJS团队通过确保每个模块都在自己的命名空间中执行定义了一种解决javascript作用域问题的模块形式。

commonjs通过强制模块输出正确的想要暴露在全局的变量,同时定义好正确工作所需的其他模块。

为了实现这些commonjs提供了两个工具:

  • require()函数,允许在当前作用域带入模块。
  • module对象,允许从当前作用域输出一些东西。

必须来个hello world例子:

纯javascript

这有个不使用commonjs的例子:

在salute.js文件中定义

var MySalute = "Hello";

然后在第二个文件world.js中取值

console.log(MySalute) // hello

模块定义

实际上,MySalute因为没有定义world.js不会正常工作,我们需要把每个script定义成模块。

// salute.js
var MySalute = "Hello";
module.exports = MySalute;
// world.js
var Result = MySalute + "world!";
module.exports = Result;

这里我们使用了特殊的对象module然后把变量引用赋值给了module.exports,所以commonjs模块系统知道这事我们想要暴露给世界的模块内的对象。salute.js暴露了MySalute,world.js暴露了Result

模块依赖

我们离成功就差了一步:依赖定义。我们早就把每个script定义成了独立的模块,但是world.js还需要知道谁定义了MySalute

// world.js
var MySalute = require("./salute");
var Result = MySalute + "world!";
module.exports = Result;

AMD

AMD(异步模块系统)为了适应那些觉得commonjs模块系统还没在browser准备好的人产出的,因为他们觉得commonjs的本质是同步的。

AMD提出了现代js的标准,以至于模块能异步的加载依赖,解决了同步加载的问题。

说明

模块有defined函数来定义

define

define函数用于使用AMD定义一个模块。这只是一个带有签名的函数。

define(id?: String, dependencies?: String[], factory: Function|Object);

id

指定了模块的名字,可选。

dependencies

这个参数定义了被定义的模块所依赖的模块。是一个包含了模块标识符的数组,可选项。但是如果省略,默认设置成[“require”, “exports”, “module”].

factory

最后一个参数定义了模块内容,可以是个函数(立马执行),或者对象。如果factory是个函数,返回值会变成模块的导出值。

例子

命名模块

定义一个名为myModule依赖jQuery的模块

define('myModule', ['jquery'], function($) {
    // $ is the export of the jquery module.
    $('body').text('hello world');
});
// and use it
require(['myModule'], function(myModule) {});

注意:在webpack中命名模块只在本地使用,在require.js中命名模块是全局的。

匿名模块
define(['jquery'], function($) {
    $('body').text('hello world');
});
多依赖
define(['jquery', './math.js'], function($, math) {
    // $ and math are the exports of the jquery module.
    $('body').text('hello world');
});
导出
define(['jquery'], function($) {

    var HelloWorldize = function(selector){
        $(selector).text('hello world');
    };

    return HelloWorldize;
});

code Splitting 代码分割

对于大型apps而言将所有代码都放在一个文件中是不高效的。尤其是一些代码块只要某些情况下才需要加载。webpack有一个特性可以将代码分割成可根据命令加载的块。其他一些打包器称呼为「layers」「rollups」「fragments」。这个特性叫做「code splitting」

这是一个可选择的特性,你可以在代码中定义分割点。webpack处理依赖,导出文件以及运行时。

声明一个常见的误解:代码分割不只是提取通用代码放入可分享的块。更显著的特性是可以将代码分割成按需加载的块。这可以控制初始下载更小的代码然后当应用需要的时候下载其他代码。

定义分割点

AMD和commonjs定义了不同的方法按需加载代码。他们都被支持并且充当分割的点。

commonjs require.ensure
require.ensure(dependencies, callback)

require.ensure方法确保当回调调用的时候dependencies里的每个依赖都会被同步的引入。require作为参数传递到回调函数中。

require.ensure(["module-a", "module-b"], function(require) {
    var a = require("module-a");
    // ...
});

注意:require.ensure只加载模块,并不执行。

AMD require

AMD定义了一个异步的require方法。

reuqire(dependices, callback)

当调用的时候,所有依赖都被加载,callback函数中能得到依赖暴露出来的对象。

例子:

require(["module-a", "module-b"], function (a, b) {// ...})

note: AMDrequire加载并执行模块。在webpack中模块由左向右执行

note: 省略回调函数是允许的。

es6 Modules

webpack不支持es6模块,根据你的编译器创建的格式直接使用require.ensure或者require

webpack1.x.x(2.0.0支持)原生不支持es6模块。但是你可以使用编译器得到,比如:babel。把es6的import语法编译成commonjs和amd模块。这个方法是有效的但是也有一个重要的动态加载警告。

模块额外的语法(import x from 'foo')是特意被设计为可静态分析的。这就意味着你不能动态的import。


// 这是非法的
['lodash', 'backbone'].forEach(function (item) {import item})

幸运的是,有个js api 「loader」用来处理动态使用例子:System.load(或者)System.import。这个API跟require变量的作用一样。然而多数编译器不支持转变System.load调用require.ensure。所以你想使用代码分割你可以直接使用require。

//static imports
import _ from 'lodash'

// dynamic imports
require.ensure([], function(require) {
  let contacts = require('./contacts')
})
块内容

所有在代码分割点引用的依赖会进入新的块中,其中的依赖也会递归增加。

如果传入了一个函数表达式(或者绑定了一个函数表达式)作为分割点的回调函数。webpack会将所有依赖都放到块中。

块chunk优化

如果两个块包含相同的模块,他们会被合并成一个块。

块chunk加载

根据配置选项target一个用于块加载的运行时会被添加到打包文件bundle中。举个例子,将target设置成web块会通过jsonp被加载。一个块只会被加载一次,并且平行的请求会被合并成一个。运行时会检查已经加载的块是否满足其他块。

块的类型

入口块

一个入口块包含了运行时外加一系列的模块。如果块包含了模块0,运行时会执行它。如果没有运行时会等待包含模块0的块然后执行。

普通的块

普通块不包含运行时,这只包含一系列的模块。结构取决于块加载的算法。举个例子,jsonp中模块会被包装在jsonp的回调函数中。块也会包含一系列满足的id。

初始块(不是入口)

初始的块是个普通的块。唯一的区别在于优化机制认为初始块更重要因为初始块计算初始的时间。这种块的类型会出现在commonsChunkPlugin插件的结合中。

分割app和vendor代码

把你的app分割成两个文件,叫做app.js和vendor.js。你可以在vendor.js中依赖vendor类型的文件,然后传递这些名字到commonsChunkPlugin中。

var webpack = require("webpack");

module.exports = {
  entry: {
    app: "./app.js",
    vendor: ["jquery", "underscore", ...],
  },
  output: {
    filename: "bundle.js"
  },
  plugins: [
    new webpack.optimize.CommonsChunkPlugin(/* chunkName= */"vendor", /* filename= */"vendor.bundle.js")
  ]
};

这会从app块中移除所有vendor块。bundle.js将会保留app的代码,没有任何依赖。这些移除的代码将会留在vendor.bundle.js中。在你的html中加载

<script src="vendor.bundle.js"></script>
<script src="bundle.js"></script>

多入口的块

设置多入口的点产出多入口的块是可行的。入口块包含一个运行时且当前页面只有一个运行时。(当然也有例外)

运行多个入口点

使用commonChunkPlugin插件运行时会被移到通用的块中。入口点现在在初始的块中。然而只有一个初始块会被加载,多个入口块会被加载。这就显示了在单页面执行多个入口点的可能性。

var webpack = require("webpack");
module.exports = {
    entry: { a: "./a", b: "./b" },
    output: { filename: "[name].js" },
    plugins: [ new webpack.optimize.CommonsChunkPlugin("init.js") ]
}
<script src="init.js"></script>
<script src="a.js"></script>
<script src="b.js"></script>
通用的块

CommonsChunkPlugin会把多入口的块移到一个新的入口块(通用块),运行时也会被移到通用的块。这意味着老的入口块现在变成了初始块。

优化

以下优化插件可以根据特定条件合并块。

LimitChunkCountPlugin
MinChunkSizePlugin
AggressiveMergingPlugin
命名块

require.ensure函数接受额外的第三个参数。这个参数一定是一个字符串。如果两个分割点传递了相同的字符串他们会使用相同的块。

require.include

require.include是一个webpack特定的函数用来给当前块添加一个模块,但是不执行。(表达式会从bundle中移除。)

例子:

require.ensure(["./file"], function(require) {
  require("./file2");
});

// is equal to

require.ensure([], function(require) {
  require.include("./file");
  require("./file2");
});

如果在多子块的情况下require.include是好用的,require.include在父块中会引入模块,在子块中该模块的实例会消失。

例子:

可执行的demo可以在devTools中查看网络

stylesheets

内联的样式

通过使用style-loadercss-loader将样式文件内嵌到webpack js打包文件中。通过这种方式你可以将你的样式文件和其他模块一样处理。引入样式如下方式require("./stylesheet.css")

安装

从npm中安装loaders

npm install style-loader css-loader --save-dev

配置

下面介绍一个使require()正常工作的配置例子

{
    module: {
        loaders: [
            {test: /\.css$/, loader: "style-loader!css-loader"}
        ]
    }
}
杜宇预编译的css语言可以查询对应的loader的配置例子,你可以在module中加入

请牢记管理modules的执行顺序是困难的,所以请自行设计好样式文件(你也可以依赖同一份css文件中的顺序)

使用css

// 在模块中直接引用样式文件
// 但这有一个副作用会在dom中添加额外的`style`标签
require("./stylesheet.css")

分离出来的css打包文件

结合extract-text-webpack-plugin就可以产出独立的css文件。

结合代码分割技术我们可以使用两种不同的方式:

  • 为每一份初始块生成一个css文件,在额外的块中内嵌样式信息(推荐)
  • 为每一份初始块生成一个css文件,并且包含其他块的css

推荐第一种方法主要是因为对于初始加载时间是最优的。在多入口文件的小型app中推荐第二种方法是因为考虑HTTP请求并发上限以及缓存。

插件安装

从npm中安装插件

npm install extract-text-webpack-plugin --save

常规用法

为了使用插件你需要改变配置,使用特殊的loader将样式输出到css文件。在代码编写后webpack优化阶段插件会检查哪个相关的模块需要被抽离(在第一种方法中只有初始块)。这些模块通过nodejs执行得到内容,另外模块被会重新编译到原先的包中代替空的模块。

为被抽离的模块创建了新的内容。

初始块中的样式臭历程单独的css文件

这个例子展示了多入口,但是也同样适合但入口。

// webpack.config.js
var ExtractTextPlugin = require("extract-text-webpack-plugin");
module.exports = {
    // The standard entry point and output config
    entry: {
        posts: "./posts",
        post: "./post",
        about: "./about"
    },
    output: {
        filename: "[name].js",
        chunkFilename: "[id].js"
    },
    module: {
        loaders: [
            // Extract css files
            {
                test: /\.css$/,
                loader: ExtractTextPlugin.extract("style-loader", "css-loader")
            },
            // Optionally extract less files
            // or any other compile-to-css language
            {
                test: /\.less$/,
                loader: ExtractTextPlugin.extract("style-loader", "css-loader!less-loader")
            }
            // You could also use other loaders the same way. I. e. the autoprefixer-loader
        ]
    },
    // Use the plugin to specify the resulting filename (and add needed behavior to the compiler)
    plugins: [
        new ExtractTextPlugin("[name].css")
    ]
}

所有的样式都被抽离成css文件

使用第二种方法你只用设置allChunkstrue

// ...
module.exports = {
    // ...
    plugins: [
        new ExtractTextPlugin("style.css", {
            allChunks: true
        })
    ]
}

在通用块中的样式

你可以结合CommonChunkPlugin使用独立的css文件。

// ...
module.exports = {
    // ...
    plugins: [
        new webpack.optimize.CommonsChunkPlugin("commons", "commons.js"),
        new ExtractTextPlugin("[name].css")
    ]
}

优化

最小化

去最小化你的脚本(如果你使用css-loader还有css),webpack支持一个简单的选项:

--optimize-minize或者 new webpack.optimize.UglifyJsPlugin()

这是个最简单但是最有效的优化你的webapp的方法。

正如你所知道的(如果你有持续阅读文档)webpack会给模块和块id去标识他们。webpack可以改变ids的分配去得到最短的id长度作用于常用的ids:

--optimize-occurence-order或者new webpack.optimize.OccurenceOrderPlugin()

入口块对于文件大小有更高的优先级。

删除重复数据

如果你使用一些有很多依赖的第三方库,这可能会发生一些文件会相同。webpack会发现这些文件并删除他们。这会防止你的包里包含相同的代码,相反的会在运行时应用一个函数的引用。这不影响语义,你可以这样开启:

--optimize-dedupe或者new webpack.optimize.DedupePlugin()

这个特性在入口文件中添加了一些开销。

在书写代码的时候,你可能早已经添加了许多代码分割点来按需加载代码。当编译后你可能会注意到这么多的块对于http的开销来说是还是太小体量的。幸运的是,webpack可以通过后处理的方式去合并他们。你可以提供两种选项:

  • 限制块的最大数量--optimize-max-chunks 15 new webpack.optimize.LimitChunkCountPlugin({maxChunks: 15})
  • 限制块的最小尺寸 --optimize-min-chunk-size 10000 new webpack.optimize.MinChunkSizePlugin({minChunkSize: 10000})

webpack通过合并chunk来解决这个优化问题(webpack更倾向于合并有相同模块的chunk)。没有东西会被合并到入口chunk,所以不会影响页面加载时间。

单页应用

webpack就是被设计优化像单页应用这种web应用的。

你可以将app中的代码分割成多个chunk,由路由判断来加载。入口chunk只包含路由和一些第三方资源,没有内容。当你的用户在app中操作时这会工作顺利,但是对于不同路由的初始加载你可能需要一个来回:第一步获取路由第二步获取当前页内容。

如果你使用HTML5 history的API跳转当前页面,通过客户端代码服务器能知道哪个页面被请求。为了节省桐乡服务器的来回路程你可以包含内容块到返回中。直接添加script标签是可行的。浏览器会加载平行的chunks。

<script src="entry-chunk.js" type="text/javascript" charset="utf-8"></script>
<script src="3.chunk.js" type="text/javascript" charset="utf-8"></script>

你可以从stats中提取文件名(stats-webpack-plugin能用来导出构建后的stats)

多页应用

当你编译多页面的app,你想在多页面之间共享相同的代码。事实上结合webpack这非常容易:只需要结合多个入口点进行编译:

module.exports = {
    entry: {
        p1: "./page1",
        p2: "./page2",
        p3: "./page3"
    },
    output: {
        filename: "[name].entry.chunk.js"
    }
}

这会生成多个入口chunk:p1.entry.chunk.js,p2.entry.chunk.jsp3.entry.chunk.js但是其他的chunk能通过他们分享。

如果你的入口chunks有一些相同的模块,这有个很好用的插件。CommonsChunkPlugin识别相同的模块然后将他们放入一个通用chunk。你需要在页面中加入两个script标签。一个是通用的chunk,一个是入口chunk。

var CommonsChunkPlugin = require("webpack/lib/optimize/CommonsChunkPlugin");
module.exports = {
    entry: {
        p1: "./page1",
        p2: "./page2",
        p3: "./page3"
    },
    output: {
        filename: "[name].entry.chunk.js"
    },
    plugins: [
        new CommonsChunkPlugin("commons.chunk.js")
    ]
}

这会形成多个入口chunkp1.entry.chunk.js,p2.entry.chunk.jsp3.entry.chunk.js。加上一个common.chunk.js,首先加载common.chunk.js然后加载xx.entry.chunk.js

你也可以通过选择入口chunks形成多个通用chunks,你也可以嵌套通用chunks

var CommonsChunkPlugin = require("webpack/lib/optimize/CommonsChunkPlugin");
module.exports = {
    entry: {
        p1: "./page1",
        p2: "./page2",
        p3: "./page3",
        ap1: "./admin/page1",
        ap2: "./admin/page2"
    },
    output: {
        filename: "[name].js"
    },
    plugins: [
        new CommonsChunkPlugin("admin-commons.js", ["ap1", "ap2"]),
        new CommonsChunkPlugin("commons.js", ["p1", "p2", "admin-commons.js"])
    ]
};
// <script>s required:
// page1.html: commons.js, p1.js
// page2.html: commons.js, p2.js
// page3.html: p3.js
// admin-page1.html: commons.js, admin-commons.js, ap1.js
// admin-page2.html: commons.js, admin-commons.js, ap2.js

高级用法: 你可以在通用chunk中运行代码

var CommonsChunkPlugin = require("webpack/lib/optimize/CommonsChunkPlugin");
module.exports = {
    entry: {
        p1: "./page1",
        p2: "./page2",
        commons: "./entry-for-the-commons-chunk"
    },
    plugins: [
        new CommonsChunkPlugin("commons", "commons.js")
    ]
};

multiple-entry-points exampleadvanced multiple-commons-chunks example

长期缓存

为了有效的缓存文件,文件需要带有hash或者版本号的URL。你可以人为的修改产出的文件的版本号v.1.3但是这样不是很方便。额外的人工才操作以及没有改变的文件不从缓存中加载。

webpack可以根据文件名给文件添加hash值。处理文件的loaders(worker-loader,file-loader)早已经实现,对于chunks你需要开启它,有两种等级:

  1. 计算所有chunks的hash
  2. 为每个chunk计算hash

选择1:一个bundle一个hash

选择1通过为文件名配置hash选项来开启

webpack ./entry output/[hash].bundle.js

{
    output: {
        path: path.join(__dirname, "assets", "[hash]"),
        publicPath: "assets/[hash]/",
        filename: "output.[hash].bundle.js",
        chunkFilename: "[id].[hash].bundle.js"
    }
}

选择2: 每个chunk一个hash

选项2通过添加[chunkhash]到chunkFilename配置选项来开启。

--output-chunk-file [chunkhash].js

output: { chunkFilename: "[chunkhash].bundle.js" }

记得你需要在html中引用带有hash的入口chunk。你可能想从stats提取hash或者文件名。结合热替换你必须使用选项1,但不是在publicPath配置项中。

从stats中获取文件名

你可能想得到最后的文件名字嵌入你的html中。这个信息在webpack的stats中是可以获取的。如果你是使用CLI你可以运行--json去得到以json形式输出的stats。

你可以在配置文件中加入assets-webpack-plugin插件来允许你得到stats对象。接下来是一个将此写入文件的例子。

plugins: [
  function() {
    this.plugin("done", function(stats) {
      require("fs").writeFileSync(
        path.join(__dirname, "..", "stats.json"),
        JSON.stringify(stats.toJson()));
    });
  }
]

stats json包含了好用的对象--assetsByChunkName,这个对象包含了以chunk为键名文件名为键值的对象。

多入口的点

要求:代码分割

如果你需要多个打包文件给多个html页面使用,你可以使用「多入口点」特性。这会同时生成多个打包文件。额外的chunks可以被这些入口chunks共享,模块只会被构建一次。

提示: 如果你想从模块中开始一个入口chunk,这是个错误的想法。使用代码分割!

每一个入口chunk都包含了webpack运行时,所以你只用在每个页面加载一个入口chunk(提示:可以使用commonsChunkPlugin插件去绕过这个限制将运行时放入单个chunk中。)

配置

为了使用多入口点你可以往entry选项中传入一个对象。键名代表了入口点的名字,键值代表了入口点。

当应用多入口点时必须改写默认的output.filename选项。不然每个入口点都会写入相同的文件。使用[name]得到入口点的名字。

最简单的配置例子

{
    entry: {
        a: "./a",
        b: "./b",
        c: ["./c", "./d"]
    },
    output: {
        path: path.join(__dirname, "dist"),
        filename: "[name].entry.js"
    }
}

例子

第三方库和拓展

你开发了一个第三方库然后要分到编译/打包后的版本(除了模块化的版本)。你想要允许用户在script标签或者amd加载器中使用。或者你想取决于不同的预编译器而不限制用户,把这个模块作为普通的commonjs模块。

配置选项

webpack有三个跟这个情况相关的配置选项:output.library,output.libraryTarget,externals

output.libraryTarget允许你控制输出的类型,举例:commonjs,amd,script中使用。

output.library允许你指定第三方库的名字。

externals允许你指定第三方库的不需要经过webpack处理的依赖。但是是输出文件的依赖。这也表明了他们是在运行时环境中输入的。

例子

编译在script中使用的第三方库。

  • 依赖jquery,但是jquery不应该包含在打包文件中。
  • library应该在全局上下文中的Foo中可访问。
var jQuery = require("jquery");
var math = require("math-library");

function Foo() {}

// ...

module.exports = Foo;

推荐的配置(与之相关)

{
    output: {
        // export itself to a global var
        libraryTarget: "var",
        // name of the global var: "Foo"
        library: "Foo"
    },
    externals: {
        // require("jquery") is external and available
        //  on the global var jQuery
        "jquery": "jQuery"
    }
}

打包结果

var Foo = (/* ... webpack bootstrap ... */
{
    0: function(...) {
        var jQuery = require(1);
        /* ... */
    },
    1: function(...) {
        module.exports = jQuery;
    },
    /* ... */
});

应用以及外部资源

你也可以使用externals选项向应用导出一个存在的接口。举个例子,你想使用cdn资源script引用的jquery,但又明确声明通过require('jquery')作为依赖,你可以像这样把他指定为外部资源:{externals: {jquery: "jQuery"}}

分解以及外部资源

外部资源在分解请求之前执行,这意味着你需要指定没有分解的请求。externals中不能应用loaders,所以你需要用loader具体化一个请求。require("bundle!jquery") { externals: {"bundle!jquery": "bundledJQuery"}}

垫板模块(shim)

不是所有js文件都可以直接使用webpack。此文件可能是webpack不支持的文件,甚至没有任何模块形式。

webpack提供一些loaders使这些文件可以跟webpack一起工作。

下面的例子使用require保持简洁。你通常都会在webpack中配置他们。

输入

如果一个文件的依赖不是通过require()引入的,你可以使用以下loader中的一种。

imports-loader

import loader允许你根据不同的全局变量去使用模块。

对于依赖像$或者this的第三方模块这是很方便的。imports loader会添加必要的require('whatever')调用。所以这些模块可以跟webpack工作。

例子:

file.js 需要一个全局的$变量,你也有一个应该被使用的jquery模块。

require("imports?$=jquery!./file.js")

file.js需要一个全局的配置变量xConfig,你希望是{value: 123}

require("imports?xConfig=>{value:123}!./file.js")

file.js需要一个全局的this对象。

require("imports?this=>window!./file.js") or require("imports?this=>global!./file.js")

plugin 提供插件

这个插件使得一个模块在任何模块中能称为一个变量。只有当你使用这个变量的时候才会依赖这个模块。

例子: 不需要写require("jquery")就能在任何模块中使用$和jquery变量。

new webpack.ProvidePlugin({
    $: "jquery",
    jQuery: "jquery",
    "window.jQuery": "jquery"
})

输出

不暴露值的文件。

exports-loader

这个loader暴露文件的内部变量。

例子:

这个文件在全局上下文定义了一个变量var XModule = ...

var XModule = require("exports?XModule!./file.js")

这个文件在全局上下文定义了多个变量 var XParser, Minimizer

var XModule = require("exports?Parser=XParser&Minimizer!./file.js"); XModule.Parser; XModule.Minimizer

这个文件设置了一个全局变量 XModule = ....

require("imports?XModule=>undefined!exports?XModule!./file.js") (import to not leak to the global context)

这个文件在window对象下设置了属性 window.XModule = ...

require("imports?window=>{}!exports?window.XModule!./file.js")

修复错误使用的模块风格

有些模块使用了错误的模块风格。你想去修复并且告诉webpack不要使用这种风格。

使模块风格失效

例子:

AMD失效

require("imports?define=>false!./file.js")

CommonJs失效

require("imports?require=>false!./file.js")

配置 选项 module.noParse

webpack会使解析失效,因此你不能使用依赖,这对已经打包好的第三方库比较实用。

例子:

{
    module: {
        noParse: [
            /XModule[\\\/]file\.js$/,
            path.join(__dirname, "web_modules", "XModule2")
        ]
    }
}
exports 和 module任然是可用的,你可以使用imports-loader使他们失效。

script-loader

这个loader在全局上下文中评估代码,就跟你在script中添加代码一样。这种方式每一个第三方库都能正常工作,require、module等都会失效。

注意:此文件会被当做字符串加入到bundle中,不会被webpack压缩,所以我们需要使用压缩版本。也没有作用于这种loader加载的第三方库的开发者工具。

暴露

有些情况你想一个模块暴露出自己。

除非必须不然少用(providePlugin更好)

expose-loader

这个loader将模块暴露到全局上下文中。

例子:

将file.js暴露到全局上下文中的XModule变量。

require("expose?XModule!./file.js")

另一个例子:

   require('expose?$!expose?jQuery!jquery');

   ...

   $(document).ready(function() {
   console.log("hey");
   })

通过jquery文件暴露到全局上下文,你可以在项目中的任何地方使用jquery。同理你想使用bootstrap也可以通过这种方法。

注意: 使用太多全局变量会使你的app缺少效率,如果你想使用大量命名空间,考虑在项目中加入Babel runtime

loader的顺序

在非常小的应用场景下你需要应用不只一个配置,你需要使用正确的loader顺序。内嵌:expose!imports!exports ,配置项:expose before imports before exports.。

测试

有两种方式可以测试web应用:

  • 浏览器:你可以得到更现实的测试,但是你需要准备更多的基础建设,且测试需要花费更多时间。你可以测试dom。
  • nodejs:你不能测试dom,但是测试会更快。

浏览器测试

mocha-loader

mocha-loader使用mocha框架执行你的代码。如果执行代码你会在浏览器看到测试结果。

提示:当在bash命令行使用时,你需要使用\转义。

webpack 'mocha!./test.js' testBundle.js
<!--index.html is a HTML page which loads testBundle.js-->
open index.html
webpack-dev-server

webpack-dev-server会自动的创建加载脚本的HTML页面。当文件改变时会重新执行。

webpack-dev-server 'mocha!./test.js' --hot --inline --output-filename test.js
open http://localhost:8080/test

提示:使用--hot服务器只会在该文件或该文件的依赖有变化时重新执行测试。

karma与webpack

你可以将webpack与karma一起使用,将webpack作为预处理器加进karma的配置中

nodejs

打包性能

如果你在寻找加速webpack打包的方法,你可能要通过以下几种方法去更加深入的提高你配置的webpack的打包性能。

逐步的打包

确保每次打包的时候不会全部重新打包。webpack有一个强大的缓存层允许你使用内存中早已编译好的模块,以下几种方法帮助使用:

  • webpack-dev-server: 将所有资源存到内存,性能最好。
  • webpack-dev-middleware:与webpack-dev-server有相同的性能,提供给有深层定制的用户
  • webpack --watch 或者 watch: true 有缓存,但是缓存到硬盘,性能一般。

不解析某些模块

使用noParse可以在解析时排除大的第三方库,但是会中断。

打包过程的信息

有个分析工具可以提供详细的分析和一些可以帮助你优化打包文件大小和性能的有用信息。

chunks

从内部表现生成源文件代价是高的。只有当这个chunk内部没有任何改变时,chunk都由自己缓存。大多数chunk取决于自身包含的模块,但是入口chunk不同,如果额外的chunk名字改变了,入口块同样会被认为是脏的,即被修改过的。所以使用在文件名中使用[hash]或者[chunkhash]时,入口chunk几乎会在每次修改中都重新构建。

使用HMR入口chunk会嵌入编译的hash所以每次改变也会被认为是脏的。

sourcemaps

优秀的sourceMaps会减慢build

devtool: "source-map" 不会缓存模块的sourcemaps而且会重新生成chunk的sourcemaps。这是给生产环境用的。

devtool: "eval-source-map" 跟上一种异样好,但是会缓存模块的sourcemaps,对于重复构建速度会快。

devtool: "eval-cheap-module-source-map" 只提供行的sourcemaps,速度更快

devtool: "eval-cheap-source-map" 不会生成模块的sourcemaps,例如jsx向js的mapping

devtool: "eval" 性能最好,但只生产模块的sourcemaps,在一些情况下这是足够的。使用output.pathinfo: true编译

UglifyJsPlugin插件使用sourcemaps生成对应源代码的错误信息,但是sourcemaps是慢的,在生产环境使用是ok的,如果构建时速度太慢(甚至不能完成),你需要关闭这个功能。new UglifyJsPlugin({ sourceMap: false })

RESOLVE.ROOT 对比 RESOLVE.MODULESDIRECTORIES

只在嵌套路径下使用resolve.modulesDirectories,大多数路径使用resolve.root,这可以给出性能展示讨论

优化的插件

只在生产环境使用优化用的插件

提前取得模块

prefetch

动态链接的库

如果你有大量很少改变的模块(比如说vendor),chunking不会带来多大的性能提升(commonChunkPlugin),这有两个插件可以在分隔的大包进程中创建一个打包文件,但是也会在appbundle中引用。

提前创建DLL包你需要使用Dllplugin,这是例子。这会触发公共的打包文件和私有的清单文件。

从APP打包文件中引用DLL打包文件,你需要使用DllRefencePlugin这是例子,在找到Dll打包文件之前会阻止依赖app的文件。

热加载

注意模块热替换机制目前还处于试验阶段。

介绍

模块热替换(HMR)会在应用运行时替换、增加、移除模块而不用重新加载页面。

要求

HMR如何工作

webpack在构建打包的过程中增加一个小型的HMR运行时到打包文件中。当构建完成时,webpack不会退出并保持活跃。观察源文件的变化。如果webpack检测到文件的变化,他会重新构建变化的模块。取决于webpack的设置,webpack会给HMR运行时发送一个信号,或者HMR运行时会查询webpack的变化。不管哪种形式,改变的模块会被发送到HMR运行时,然后应用热更新。首先HMR运行时会检查更新的模块能不能自我accept,如果不会那么会检查依赖这些变化模块的模块。如果这些模块也不会accept,则会冒泡到下一个层级,知道能执行accept方法,或者到达app的入口文件,这也意味着热更新是失败的。

从app的视角

app代码请求HMR检查更新,HMR运行时异步的下载更新然后告知app代码更新是可用的。然后app代码告知HMR运行时应用这些更新,然后HMR同步的应用更新。app代码要不要依赖用户的输入取决于开发者自己。

从编译器(webpack)的视角

除了普通的资源编译器还需要触发「update」以允许从之前的版本更新到当前版本,「update」包含两部分:

  1. 更新清单(json)
  2. 一个或多个更新的chunk(js)

清单包含新的hash值和更新的chunk。

更新的chunks包含了所有模块。

编译器额外确保模块和chunk的id在build之间是不变的。他使用「record」json文件去存储或者在内存中存储。

从模块视角

HMR是一个可选的特性,所以这只会影响包含HMR代码的模块。文档描述的API在模块中都是可用的。通常情况下模块开发者需要写一个handles,这个handles在这个模块的依赖更新时会被触发。他也能写一个当此模块更新时就会触发的handle。在大多数情况下在每个模块中写HMR的代码不是强制的。如果一个模块没有HMRhandles那么更新会传递到下一个层级。这就意味了一个handle能沿着模块树处理一个更新。如果一个模块更新了,整个模块树都会重载(只重载不转移)

从HMR运行时视角

对于模块系统来说运行时是额外的代码用来触发追踪父模块和子模块。

在管理方面运行时支持两个方法: checkapply

check从更新清单发起http请求,如果请求失败则没有更新的内容。不然请求来的chunks列表会跟当前已经加载的chunks进行对比。每一个已加载的chunk与之对应的更新chunk会被下载。所有的模块更新在更新的同时都会存到运行时中。此时runtime进入「ready」状态,意味着更新已经下载等待被应用。

在ready状态下每个新的chunk请求也会被下载。

apply方法标记所有更新的模块为无效。对于每一个无效的模块需要一个updatehandle或者在其父模块上需要update handle。不然无效打包文件会将所有父级都标记为无效。这个过程一直持续到没有冒泡发生。如果冒泡到入口chunk则热替换失败。

现在所有无效模块都被处理了但是没有加载。然后当前的hash更新所有的accept被调用。runtime重新回到「idle」状态。

生成文件

我能用它做什么

你可以在开发环境当成livereload去使用它。事实上webpack-dev-server支持热替换模式,尝试在重新加载整个页面之前用HMR替换。你只需要添加webpack/hot/dev-server入口点然后用--hot开启开发服务器。

webpack/hot/dev-server当HMR更细失败后会重新加载整个页面。如果你想用你自己的方式重载页面,你可以添加在入口点webpack/hot/only-dev-server

你也当做更新机制在生产环境使用。你需要写相关的代码与HMR交互。

一些loaders生产的模块已经是可以热更新的。style-loader能改变样式表。你不需要做其他特殊的东西。

使用它需要做什么

一个模块只有你accept才会更新。所以你需要写module.hot.accept在模块的父级及以上。举例:router是个好地方。

如果你只是想与webpack-dev-server一起使用,只用加webpack/hot/dev-server当做入口点。不然你需要使用能调用checkapplyHMR代码。

你需要开启编译器的record去跟踪编译过程中的id(watch方式和webpack-dev-server会将records保存到内存,所以在开发过程中不需要)

你需要在编译器开启HMR并且添加HMR运行时。

为什么看起来这么酷

  • 这是模块层级的实时更新
  • 可以在生产环境使用
  • 更新考虑代码分割,只会下载app所需部分的更新。
  • 你可以在你的app部分代码使用这个功能,并不影响其他模块。
  • 如果HMR关闭了,所有HMR代码会被移除(在if(module.not)包裹下)

练习

配合webpack使用热替换你需要4件事:

  • records (--records-path, recordsPath: ...)
  • 全局允许热替换(HotModuleReplacementPlugin)
  • 在你的代码里加入热替换module.hot.accept
  • 热替换管理代码module.hot.check, module.hot.apply

小栗子:

/* style.css */
body {
    background: red;
}
/* entry.js */
require("./style.css");
document.write("<input type='text' />");

然后就可以使用dev-server使用热替换

npm install webpack webpack-dev-server -g
npm install webpack css-loader style-loader
webpack-dev-server ./entry --hot --inline --module-bind "css=style\!css"

dev-server提供内存records,这对于开发是很好地。

--hot开关开启代码热替换。

这会添加HotModuleReplacementPlugin,确保不么添加--hot,不么就在webpack.config.js里添加HotModuleReplacementPlugin,但是永远不要同一时间两个都加。HMR插件会添加两次。

有个特殊的控制代码webpack/hot/dev-server,会被--inline自动添加。(你不用手动添加在webpack.config.js)

style-loader已经包含热替换代码。

阅读更多关于如何写热替换代码hot module replacement

联系作者微博


lv_DaDa
1.7k 声望115 粉丝