为之漫笔

Webpack最详解

为之漫笔 · 2017-05-27翻译 · 5270阅读 原文链接

JavaScript模块打包的概念已经出现一段时间了。RequireJS在2009年首次发声,之后Browserify粉墨登场。接着各种打包工具如雨后春笋纷纷涌现。而webpack以其优异的特性脱颖而出。如果你还不了解它,希望这篇文章可以带你熟悉这一款强大的工具。

什么是模块打包工具

我们见过的多数编程语言(包括ECMAScript 2015+这个JavaScript的最新版本,但没有得到浏览器广泛支持),都可以把代码分别保存在多个文件里,然后我们可以导入这些文件以使用它们的功能。但浏览器本身没有这个功能,因此才有了打包工具帮我们实现它,有两种形式:异步加载模块并在加载完毕后运行,以及把所有必要的文件汇总到一个JavaScript文件里通过<script>标签加载。

在没有模块加载和打包工具前,我们要么手工合并文件,要么使用一堆<script>标签,可是这么做有问题:

  • 你必须保证文件加载的顺序没错,包括知道哪些文件依赖另外一些文件,以及不包含不需要的文件。

  • 多个<script>标签意味着对服务器发送多次请求,性能会受影响。

  • 显然,这些工作量都靠你纯手工完成,计算机一点也帮不上你。

多数模块打包工具也会直接与npm或Bower集成,以便于在应用中添加第三方依赖。只要安装一下,再写一行导入它们的代码即可。然后,运行模块打包工具,第三方代码就合并到你的应用代码里了。要不然,如果你的配置正确,还可以让这些第三方代码保存在单独的文件里。这样以后更新应用代码时,用户就不需要再次下载这些第三方代码了。

为什么用Webpack?

了解了webpack的基本用途之后,接下来再谈谈为什么它会从竞争中脱颖而出?下面是几点原因:

  • 相对新,因此具有后发优势,能绕过或避免先驱们的缺陷和问题。

  • 上手容易。如果你只想把一堆JavaScript文件打包到一起,并不想要其他花哨的东西,那你甚至都不需要配置文件。

  • 它的插件系统让它能干的事多了去了,因此它也变得非常强大。这样一来,你可能只要用它这一个构建工具就行了。

我见过的打包和构建工具中能同时具备上述优点的不多。但webpack好像还要更胜一筹:那就是关键时候能帮你解决问题的庞大社区。Browserify的社区虽然可能更大,但却缺少webpack的一些潜质。为webpack说了那么多好话,相信你一定急不可待地想让我上代码了。那就来吧。

安装webpack

使用webpack之前,先要安装它。但要安装它,必须先安装Node.js和npm,我假设你已经安装它们了。要是你还没安装,那先到Node.js的网站上看看怎么安装。

好了,有两种方式安装webpack(其他CLI包也一样):全局和本地。全局安装后在任何文件夹下都可以使用,但是就不能作为依赖包含在你的项目中了。此外,也不能在不同的项目下切换不同的webpack版本(要升级到较新版本对有的项目来说可能很麻烦,因此这些项目就得等着)。所以,我一般会在本地方式安装CLI,然后再通过相对路径或者npm scripts来运行。如果你不习惯在本地安装CLI包,可以参考一下我的另一篇文章,教你怎么摆脱全局npm包。

不管怎么,下面的例子会使用npm scripts来运行安装的包,所以我们暂时选择本地安装。最重要的:先为项目创建一个目录,在这里头可以试验和学习webpack。我有一个GitHub仓库,可以clone下来,然后结合后面的教程切换分支。或者你也可以创建一个空项目,然后拿我的GitHub仓库作对照。

通过终端进入项目目录,首先是通过npm init初始化项目。初始化过程中输入什么信息不重要,除非你打算把它发布到npm上。

现在有了一个package.json文件(npm init创建的),就可以在里面声明依赖了。下面我们使用npm把webpack安装为一个依赖,命令是npm install webpack -D。(-D的意思是在package.json中把它保存为开发依赖;也可以使用--save-dev。)

在使用webpack之前,应该有一个简单的应用使用它。说简单,是真的简单。首先,来安装Lodash,以便我们简单的应用能有个依赖可加载:npm install lodash -S-S--save一样)。之后,创建一个目录src,再在里面创建一个文件main.js,内容如下:

var map = require('lodash/map');

function square(n) {
    return n*n;
}

console.log(map([1,2,3,4,5,6], square));

很简单,是吧?我们就是写一个包含整数1到6的数组,然后使用Lodash的map创建了一个新数组,其中每个元素都是原始数组中对应值的平方。最后,把新数据输出到控制台这个文件都可以通过Node.js运行,试试node src/main.js,结果你会看到输出:[ 1,4,9,16,25,36 ]

现在我们希望把这几行代码跟我们用到的Lodash代码打包在一起,让它们可以在浏览器中运行,webpack怎么帮我们?怎么做?

使用webpack命令行

使用webpack的最简单方式,就是不考虑配置文件,直接在命令行中运行它。不使用配置文件的情况下,webpack命令至少需要一个输入文件和一个输出文件。Webpack会读取输入文件,跟踪其中的依赖,把所有关联文件打包到一个文件,最后把这个文件输出到你指定的输出路径。在我们这个例子里,输入路径是src/main.js,输出路径是dist/bundle.js。下面我们就创建一个npm脚本来做这件事(因为没有全局安装webpack,所以不能直接在命令行里运行它)。在package.json中,修改"scripts"如下:

…
  "scripts": {
    "build": "webpack src/main.js dist/bundle.js",
  }
…

现在,运行npm run build,webpack就帮你干活了。完事以后,花不了多长时间,应该会有一个新的dist/bundle.js文件。这时可以通过Node.js运行它(node dist/bundle.js),也可以在浏览器里通过简单的HTML运行它,可以在控制台看到相同的输出。

进一步了解webpack之前,我们先把构建脚本改造得更专业一些,也就是在重新构建之前先删除dist目录及其内容,另外再添加几条执行打包文件的脚本。为此,先得安装del-cli,以便删除文件时不用麻烦跟我们使用不同操作系统的人(我用Windows,不要鄙视我):npm install del-cli -D。然后,把npm脚本改成这样:

…
  "scripts": {
    "prebuild": "del-cli dist -f",
    "build": "webpack src/main.js dist/bundle.js",
    "execute": "node dist/bundle.js",
    "start": "npm run build -s && npm run execute -s"
  }
…

"build"跟以前一样,但现在有了预先清理的"prebuild",它会在每次运行"build"之前自动运行。而且有了"execute",它会调用Node.js执行打包后的脚本。关键是我们可以通过"start"这一个命令完成所有操作(其中的)-s选项是不让npm脚本在控制台输出太多没用的信息)。来,跑一下npm start。应该能在控制台看到webpack的输入,紧跟着平方之后的数组。恭喜!你已经完成了前面提到的仓库中example1分支的所有练习。

使用配置文件

我们通过命令行了解了webpack,很有意思。可要是你想使用webpack的更多功能,就会觉得通过命令行传递各种选项很麻烦,这时候就要用到配置文件了。配置文件更强,而且可读性好,因为它是用JavaScript写的。

好,那我们就来创建这个配置文件在项目根目录下创建一个名叫webpack.config.js的新文件。这个名字的文件是webpack默认会找的。如果你想给配置文件起个不一样的名字,或者想把它放在别的目录下,可以给webpack传递--config [filename]选项。

咱们这个教程就使用默认的文件名。而现在,我们要做的就是通过配置文件实现之前通过命令行做到的一切。为此,需要在配置文件中添加如下代码:

module.exports = {
    entry: './src/main.js',
    output: {
        path: './dist',
        filename: 'bundle.js'
    }
};

这里我们指定了输入文件和输出文件,跟在命令行里类似。这是一个JavaScript文件,不是JSON文件,因此需要导出配置对象,也就是module.exports。似乎并不比在命令行中指定这些选项好多少,但看完这篇文章,你就会觉得还是把选项都放在这里好。

现在可以把package.json文件中传递给webpack的选项都删掉(在build中)。删掉选项的scripts对象如下所示:

…
  "scripts": {
    "prebuild": "del-cli dist -f",
    "build": "webpack",
    "execute": "node dist/bundle.js",
    "start": "npm run build -s && npm run execute -s"
  }
…

可以像以前一样运行npm start,结果应该一样!这就是example2分支的练习了。

使用加载器

增加webpack功能主要有两种途径:加载器和插件。后面再讨论插件。我们先看加载器,它用于对指定类型的文件应用变换或执行操作。可以连缀多个加载器对同一类型的文件进行多道处理。比如,可以指定.js扩展名的文件都要通过ESLint检查,然后再通过Babel将它们由ES2015编译为ES5。如果ESLint遇到警告,那警告信息会输出到控制台,如果遇到了错误,则会阻止webpack继续执行后续操作。

对于我们这个小应用,这次就不安排代码检查了,只设置通过Babel把代码编译为ES5。当然,我们首先得有ES2015的代码,对不?把main.js文件中的代码改成这样:

import { map } from 'lodash';

console.log(map([1,2,3,4,5,6], n => n*n));

代码做的事还一样,只是(1)把原来的square函数改写成了箭头函数,(2)使用ES2015的import'lodash'加载为map。这样其实会把整个Lodash文件都打包到我们的输出文件里,而不像前面使用'lodash/map'一样只导入map。你可以自己把第一行改成import map from 'lodash/map',但我这样做有几个理由:

  • 在大型应用中,应该会用到Lodash库中的大部分代码,因此最终可能还是要全部加载它。

  • 如果你使用Backbone.js,很难单独加载你需要的所有功能,因为没办法搞清楚你需要多少

  • 在webpack的下一个主版本中,开发者打算支持所谓的“摇树”(tree-shaking)功能,可用来清除模块中用不着的代码。因此,无论如何结果都一样。

  • 我想用它作为例子来说明我想强调的重点

(注意:之所以可以对Lodash采用这两种方式,是因为其开发者允许这样加载。并非对所有库都可以这样做。)

无论如何,既然我们有ES2015代码,就必须把它编译成ES5才能在老旧浏览器里跑(最新浏览器对ES2015的支持情况好像还不错)。为此,需要通过webpack调用Babel及其所需的全部代码。至少,我们需要babel-core(Babel的核心功能,负责大部分工作)、babel-loader(webpack的加载器,调用babel-core)和babel-preset-es2015(包含Babel从ES2015向ES5编译时遵循的规则)。还需要babel-plugin-transform-runtimebabel-polyfill,这两个用于修改Babel向最终代码中添加腻子脚本和辅助函数的方式,当然方式有所不同,因此适合不同的项目。两个都用上并没有太大用,也许你觉得都不用也一样,但在此我们把它们都用上,主要是为了说明怎么做到。如果你想了解更多相关信息,可以查看关于polyfillruntime transform的文档

闲话少说,运行如下命令,安装:npm i -D babel-core babel-loader babel-preset-es2015 babel-plugin-transform-runtime babel-polyfill。接着配置webpack来使用它们。首先,需要一个地方添加加载器。好,把webpack.config.js修改成这样:

module.exports = {
    entry: './src/main.js',
    output: {
        path: './dist',
        filename: 'bundle.js'
    },
    module: {
        rules: [
            …
        ]
    }
};

我们又增加了一个属性叫module,其中是rules属性,是一个数组,保存每一个要用到的加载器的配置。我们就在这里添加babel-loader。对每一个加载器,至少要配置两个选项:testloadertest一般是一个正则表达式,用于测试每个文件的绝对路径。多数情况下,这里的正则表达式都是用于检测文件的扩展名,比如/\.js$/检测文件名是不是以.js结尾。对我们而言,这里使用/\.jsx?$/,既匹配.js,又匹配.jsx,万一你要在应用里使用React呢。然后需要指定loader,也就是指定对通过test检测的文件应用什么加载器。

怎么指定呢?可以传入字符串形式的加载器名称,以叹号分隔,比如'babel-loader!eslint-loader'。webpack从右向左读取这个字符串,因此会先运行eslint-loader,后运行babel-loader。如果要给某个加载器指定选项,可以使用查询字符串语法。比如,要给Babel设置值为truefakeoption选项,可以把前面的字符串修改为'babel-loader?fakeoption=true!eslint-loader。此外,也可以用use代替loader,这样就可以传入一个加载器数组,相对更方便阅读和维护。比如,前面的字符串改成数组就是use:['babel-loader?fakeoption=true','eslint-loader']

因为这里只使用Babel一个加载器,所以我们的配置就么简单:

…
rules: [
    { test: /\.jsx?$/, loader: 'babel-loader' }
]
…

如果像这样只有一个加载器,那么除了查询字符串语法,还有一种指定选项的替代写法:使用options对象,也就是一个键-值对。以fakeoption选项为例,就是这样写:

…
rules: [
    {
        test: /\.jsx?$/,
        loader: 'babel-loader',
        options: {
            fakeoption: true
        }
    }
]
…

我们就改成使用这种语法给Babel指定几个选项:

…
rules: [
    {
        test: /\.jsx?$/,
        loader: 'babel-loader',
        options: {
            plugins: ['transform-runtime'],
            presets: ['es2015']
        }
    }
]
…

这里要设置presets选项,以便把ES2015代码转换成ES5代码。此外还设置了使用我们安装的transform-runtime插件。前面提到过,这个插件并不是必需的,这里只是为了说明怎么使用插件。当然,也可以使用.babelrc文件来设置这些选项,但那样一来我就没法向大家展示怎么在webpack里设置这些选项了。一般来说,我推荐使用.babelrc,但对于当前项目,我们还是使用配置文件。

加载器的配置还差最后一项。我们想告诉Babel不要处理node_modules文件夹中的文件,这样可以加快打包的过程。可以添加一个exclude属性,指定要排除的文件夹。exclude的值应该是一个正则表达式,因此这里设置为/node_modules/

…
rules: [
    {
        test: /\.jsx?$/,
        loader: 'babel-loader',
        exclude: /node_modules/,
        options: {
            plugins: ['transform-runtime'],
            presets: ['es2015']
        }
    }
]
…

当然,也可以使用include属性,明确指定只处理src目录中的文件,这里我们就不演示了。配置完了以后,应该就可以运行npm start并得到能在浏览器中运行的ES5代码了。如果你又不想使用transform-runtime插件,想使用腻子脚本了,那就要修改两个地方。首先,删除包含plugins:['transform-runtime]的那一行代码(如果以后也不会再使用这个插件,可以使用npm删除它)。然后,把webpack配置文件的entry部分修改如下:

entry: [
    'babel-polyfill',
    './src/main.js'
],

要指定多个入口文件,可以使用数组,新添加的就是腻子脚本。先指定腻子脚本,这样它才能出现在打包后文件的开始位置,以保证后面的代码肯定能引用到它。

如果不使用webpack的配置文件,其实也可以在src/main.js中添加import 'babel-polyfill;,结果与使用前面的配置一样。这里通过webpack配置文件的entry项指定腻子脚本,也是考虑能方便后面的例子,另外也可以展示怎么把多个入口文件打包到一个文件。不管怎样,这就是example3分支的内容。同样,运行npm start可以验证没有问题。

使用Handlebars加载器

下面再看另一个加载器:Handlebars。Handlebars加载器用于把一个Handlebars模板编译到一个函数中,如果你导入一个Handlebars文件,就会把这个函数会导入JavaScript。这正是我喜欢加载器的地方:可以通过它加载非JavaScript文件,打包后,加载的文件可以被JavaScript使用。再比如可以使用加载器导入一张图片,然后将图片转换成base64编码的URL字符串。JavaScript可以用它向页面中添加行内图片。如果连缀使用多个加载器,还可以让其中一个加载器专门优化图片大小。

跟以前一样,第一件事就是安装加载器:npm install -D handlebars-loader。不过,要使用它,必须还要安装Handlebars:npm install -D handlebars。这样就做到了加载器与其自身分离,可以自由控制Handlebars的版本。两者可以独立进化。

两个都安装好之后,就可以使用Handlebars模板了。在src目录中创建一个新文件numberlist.hbs,包含如下内容:

<ul>
  {{#each numbers as |number i|}}
    <li>{{number}}</li>
  {{/each}}
</ul>

这个模板要读取一个数组(通过名字判断是一个数值数组,但即使数组中的值不是数值应该也可以),然后给每个值创建一个列表项。

下面我们修改JavaScript文件,把原来输出数组,改成输出基于该模板创建的列表。修改后的main.js文件应该是这样的:

import { map } from 'lodash';
import template from './numberlist.hbs';

let numbers = map([1,2,3,4,5,6], n => n*n);

console.log(template({numbers}));

但这样写不行,因为numberlist.hbs不是JavaScript文件,webpack不知道怎么导入它。有一个办法,就是在import语句中告诉webpack使用Handlebars加载器:

import { map } from 'lodash';
import template from 'handlebars-loader!./numberlist.hbs';

let numbers = map([1,2,3,4,5,6], n => n*n);

console.log(template({numbers}));

在路径前面加上加载器名字后跟一个叹号分隔,就可以告诉webpack使用这个加载器加载后面的文件。这样写就不用修改配置文件了。然而,对一个大项目来说,可能需要加载很多模板,那更好的方式就是在配置文件中告诉webpack对这类文件应该使用Handlebars加载器,而不是在每条导入模板的语句中手工添加handlebars-loader!。下面来修改配置:

…
rules: [
    {/* babel loader config… */},
    { test: /\.hbs$/, loader: 'handlebars-loader' }
]
…

这个新加载器很简单。就是指定我们想使用handlebars-loader来处理所有扩展名为.hbs的文件。好了!这样就完成了仓库example4分支的练习。运行npm start,就可以看到webpack打包后的输出,结果如下:

<ul>
    <li>1</li>
    <li>4</li>
    <li>9</li>
    <li>16</li>
    <li>25</li>
    <li>36</li>
</ul>

使用插件

插件跟加载器不一样,是用来扩展webpack功能的。给webpack添加插件更自由,不受指定文件类型限制。可以把它们注入到任何地方,因此它们能做的事也更多。至于现在有多少插件,你还是自己来看看这个查询吧:名字中含有webpack-plugin的npm包

这个教程只涉及两个插件(其中一个后面再讲)。既然文章都写这么长了,干嘛还非要讲两个插件的例子呢?我们要讲的第一个插件是HTML Webpack Plugin,它可以生成HTML文件,为此终于可以跟Web挂上勾了。

使用这个插件前,需要修改一下脚本,以便通过运行一个简单的Web服务器来测试应用。首先,需要安装一个服务器:npm i -D http-server。接着,修改executeserver,并相应地修改start

…
"scripts": {
  "prebuild": "del-cli dist -f",
  "build": "webpack",
  "server": "http-server ./dist",
  "start": "npm run build -s && npm run server -s"
},
…

构建完成后,npm start会启动一个Web服务器,此时可以打开localhost:8080看到页面。当然,我们需要通过插件创建这个页面,下面就来安装插件。安装插件:npm i -D html-webpack-plugin

安装完了以后,需要修改webpack.config.js

var HtmlwebpackPlugin = require('html-webpack-plugin');

module.exports = {
    entry: [
        'babel-polyfill',
        './src/main.js'
    ],
    output: {
        path: './dist',
        filename: 'bundle.js'
    },
    module: {
        rules: [
            {
                test: /\.jsx?$/, loader: 'babel-loader', exclude: /node_modules/,
                options: { plugins: ['transform-runtime'], presets: ['es2015'] }
            },
            { test: /\.hbs$/, loader: 'handlebars-loader' }
        ]
    },
    plugins: [
        new HtmlwebpackPlugin()
    ]
};

有两处修改,一个是在文件顶部导入新安装的插件,另一个是在配置对象最后增加了plugins属性,传入了一个插件的实例。

此时,并未给插件传递任何选项,因此它使用的是默认模板,除了打包后的JavaScript文件外什么也不会包含。如果你现在运行npm start,然后在浏览器中查看URL,只能看到一个空白页。但是如果你看开发者工具,能在控制台中看到输出的HTML。

我们应该有自己的模板,把HTML内容输出到页面而不是控制台上,这样才能让“正常的人”看到。首先,在src目录下创建一个index.html文件,这就是我们的模板。默认使用EJS模板,当然你可以配置成webpack支持的任何模板语言。我们使用默认的EJS,因为都差不多。这就是模板的内容:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
    <h2>This is my Index.html Template</h2>
    <div id="app-container"></div>
</body>
</html>

注意几点:

  • 我们使用了传递给插件的选项来定义页面标题(仅仅因为我们可以这么做)。

  • 没有指定应该在哪里加载脚本。因为插件默认会把脚本添加到body标签最后。

  • 随便放一个带id属性的div。接下来要用到它。

现在有了自己的模板了,至少我们不能再显示空白页了。修改main.js,把它原来输出到控制台的HTML添加到div里。为此,只要把main.js的最后一行修改成这样:document.getElementById("app-container").innerHTML = template({numbers});

此外还需要更新webpack的配置文件,向插件传递两个选项。修改后的文件如下:

var HtmlwebpackPlugin = require('html-webpack-plugin');

module.exports = {
    entry: [
        'babel-polyfill',
        './src/main.js'
    ],
    output: {
        path: './dist',
        filename: 'bundle.js'
    },
    module: {
        rules: [
            {
                test: /\.jsx?$/, loader: 'babel-loader', exclude: /node_modules/,
                options: { plugins: ['transform-runtime'], presets: ['es2015'] }
            },
            { test: /\.hbs$/, loader: 'handlebars-loader' }
        ]
    },
    plugins: [
        new HtmlwebpackPlugin({
            title: 'Intro to webpack',
            template: 'src/index.html'
        })
    ]
};

template选项指定在哪里找到模板,而title是传入模板的值。好,现在运行npm start,会在浏览器中看到这样的结果:

至此,example5分支的练习结束了。不同的插件有不同的选项和配置,毕竟插件多,能干的事也多,但终归都是通过webpack.config.js文件的plugins数组添加的。说到通过文件名生成和填充HTML,其实有很多其他方式,而如果你在打包文件名之后添加散列值以强制清除缓存,那这些方式都很方便。

看看example6分支,我在里面添加了一个JavaScript压缩插件。除非你不想用UglifyJS,否则不用管它。如果你不喜欢默认的UglifyJS,看看这个分支的代码(只要看webpack.config.js就行了),就知道怎么使用相关插件并配置它了。如果你不想修改默认配置,那么只要在运行webpack命令时传入-p参数就可以。这个参数代表“production”,等价于--optimize-minimize--optimize-occurence-order参数。前者表示压缩JavaScript文件,后者表示优化模块在最终文件中的次序。结果可以让文件更小,运行更快。因为这个分支是先做完的,后来我才研究的-p选项,因此我打算还保留使用UglifyJS,但是再告诉你一个更简单的方式。另一个快捷选项是-d,可以让webpack输出更多调试信息,而且不需要额外配置生成源码地图。更多快捷选项可以参考这里

延迟加载代码

延迟加载模块对RequireJS很容易,而Browserify却很难做到(也不是做不了)。一个庞大的JavaScript文件有助于节约HTTP请求,但却会导致用户下载当前或许并不需要的代码。

Webpack支持将打包文件分割成块,以便延迟加载,甚至不需要做任何配置。你要做的就是以某种方式来写代码,其他的webpack替你管了。有两种方式,一种基于CommonJS,一种基于AMD。要基于CommonJS实现延迟加载,代码得这样写:

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

require.ensure确保模块可用(但不执行),接收一个模块名的数组和一个回调。要在回调里使用模块,需要引用作为参数传入的require来请求。

这个我个人觉得麻烦,因此更喜欢AMD的方式:

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

基于AMD的语法是require,接收一个模块依赖数组和一个回调。回调的参数是每个依赖的引用,顺序也是数组中的顺序。

Webpack 2也支持System.import,使用promise而非回调。我觉得这个改进有用,虽然把回调包到promise里要费点事。不过,有一点要注意,由于新规范中引入了import()System.import已经被废弃。另外,Babel(还有TypeScript)会在你使用它时抛出语法错误。可以使用babel-plugin-dynamic-import-webpack,但这会把它转换成require.ensure,而不是帮Babel把新的import函数当成合法的而不去管它,从而让webpack来处理它。我觉得AMD或require.ensure短时间内不会消失,而System.import也将被支持到第3版本。这个时间已经足够长了,所以还是觉得怎么方便就怎么来吧。

下面修改代码,等两秒再加载Handlebars模板并输出内容。为此,删除顶部导入模板的import语句,然后把最后一行包在一个setTimeout和一个AMD语法 的require调用中:

import { map } from 'lodash';

let numbers = map([1,2,3,4,5,6], n => n*n);

setTimeout( () => {
    require(['./numberlist.hbs'], template => {
        document.getElementById("app-container").innerHTML = template({numbers});
    })
}, 2000);

现在运行npm start,后生成另一文件,类似1.bundle.js。打开浏览器,并打开开发者工具的Network,可以看到2秒后,会加载并执行这个新文件朋友,这种延迟加载当然不难实现,但却可以显著减少首次加载的文件大小,让用户体验变得更好。

注意这些子文件或者说代码块,都包含它们的所有依赖,但不包括包含在它们父代码块中的依赖。(可以有多个入口文件,分别延迟加载这个子文件,这个子文件进而又为每个父文件加载不同的依赖。)

创建第三方文件

我们再介绍一种优化:第三方文件打包。就是可以把不太可能变的公共或第三方代码单独打包到一个文件中。这样用户可以单独缓存应用代码中的公用部分,以后更新应用代码也不用重新下载了。

为此,我们要用到webpack自带的一个插件,叫CommonsChunkPlugin。因为这个插件已经内置了,所以不需要安装,只要修改webpack.config.js

var HtmlwebpackPlugin = require('html-webpack-plugin');
var UglifyJsPlugin = require('webpack/lib/optimize/UglifyJsPlugin');
var CommonsChunkPlugin = require('webpack/lib/optimize/CommonsChunkPlugin');

module.exports = {
    entry: {
        vendor: ['babel-polyfill', 'lodash'],
        main: './src/main.js'
    },
    output: {
        path: './dist',
        filename: 'bundle.js'
    },
    module: {
        rules: [
            {
                test: /\.jsx?$/, loader: 'babel-loader', exclude: /node_modules/,
                options: { plugins: ['transform-runtime'], presets: ['es2015'] }
            },
            { test: /\.hbs$/, loader: 'handlebars-loader' }
        ]
    },
    plugins: [
        new HtmlwebpackPlugin({
            title: 'Intro to webpack',
            template: 'src/index.html'
        }),
        new UglifyJsPlugin({
            beautify: false,
            mangle: { screw_ie8 : true },
            compress: { screw_ie8: true, warnings: false },
            comments: false
        }),
        new CommonsChunkPlugin({
            name: "vendor",
            filename: "vendor.bundle.js"
        })
    ]
};

第3行导入了这个插件。然后entry改成对象字面量,指定多个入口。其中vendor属性指定最终会被打包到第三方文件中的依赖,包括一个腻子脚本和Lodash,同时将主入口文件写在main属性中。接下来把CommonsChunkPlugin的实例添加到plugins对象上,并指定基于“vendor”单独打包,以及保存第三方代码的文件名vendor.bundle.js

通过指定的“vendor”块,这个插件会抽取出它指定的其他入口文件的所有依赖,只把它们放到第三方文件中。如果不在这里指定一个块名,webpack会根据入口文件共同的依赖创建一个独立的文件。

运行webpack,应该可以看到3个JavaScript文件:bundle.js1.bundle.jsvendor.bundle.js。运行npm start可以在浏览器中看到你期望的结果。webpack甚至会把它自己处理不同模块加载的主要代码都打包到这个第三方文件中,这是必要的。

好,example8分支的练习结束,我们的教程也结束了这篇文章讲了不少东西,但这只是webpack用途的冰山一角。通过Webpack可以使用CSS modules、cache-busting散列、图片优化,等等就算一本厚厚的书都写不完。我不可能都讲到,否则在我写完这本书的时候,多数内容估计都过时了!所以,还是现在就试试webpack吧,看看能不能帮你提高工作效率。老天保佑,编码愉快!

(rb, al, il)

相关文章