使用webpack已经很长一段时间了,对于React的开发,它确实是非常适用的工具。不过webpack官方提供的文档,确实有点混乱,不好理解。最近看到了一篇介绍webpack的文章,说的非常的详细,由浅入深,相信可以解答一些关于webpack的困惑。抽时间翻译了一下,原文连接A Detailed Introduction To Webpack

前言

JavaScript模块打包已经出现有一段时间了,RequireJs在2009第一次实现,然后有Browserify登台,之后又一些其他的打包工具流传开来。在这些工具中,做为最好之一的webpack引起了人们的关注。如果你不熟悉它,我希望这篇文章对你有开始使用它所帮助。

什么是模块打包工具

在大多数的编程语言中(包括最新的没有被浏览器全部实现的ES6+),你可以将你的代码拆分成多个文件(一般一个文件代表一个模块),之后在你的应用中引入这些模块,以使用这些模块所提供的功能。但浏览器没有内置类似的功能,所以需要模块打包工具来实现响应的功能,这些功能主要包括:异步的加载这些模块并且在加载完后允许它们,或者将所有依赖的模块合并成一个单一的JavaScript文件并以HTMl的<script>标签的形式加载.

如果没有这些模块打包的工具,我们就需要自己进行手动的文件合并或者按顺序在HTML中加载相应的<script>标签,但这样做有很多坏处:

  1. 首先你得记录文件加载的正确顺序,包括文件依赖了哪些文件,而且不要加载一些你不会用的文件。
  2. 过多的<script>标签意味着更多的服务端的请求,影响性能。
  3. 很明显这需要很多的人工劳动,而原本这些都可以交给计算机来帮我们完成的。

大多数的模块打包工具还与npm,Bower集成方便你轻松的加载需要的第三方依赖。只需要安装这些依赖然,之后在你的应用中引入它们就可以使用它们提供的功能了。如果你能正确的配置你打包工具,你可以确保你依赖的第三方工具都是独立的文件,这样当你更新了你的应用的代码之后,用户那边更新缓存时也不用再下载这些文件了,因为这些文件本身没有发生变化(当然这还得看你的http缓存指令)。

为什么使用webpack

现在你明白了我们使用模块打包工具的一些基本的原因,那么为什么在这么多工具中我们要选择webpack呢?有一下几点原因:

  1. 相对较新,有了其他打包工具的经验,使得它避免一些老的打包工具的缺陷和问题。
  2. 入手简单,如果你只需要将一些JavaScript文件打包不需要其他配置,你可以连配置文件都不用写。
  3. 插件系统,让它做更多的事情,扩展出更多的功能,所以它能成为你需要的唯一打包工具。

我只看过几个模块打包和构建工具可以做到相同的事情,但比起它们webpack还有一个优势:一个可以非常大的社区,它可以在你遇到问题时帮助你。Browserify的社区也很大,但它缺少一些webpack包含的潜在的基本功能。听了我这么多对webpack的赞扬,你肯定迫不及待的想看看具体的实例。所以Let’do that。

安装webpack

在使用webpack之前我们需要安装它。安装之前,我们需要Node.js和npm,我假设你都已经安装好了,如果你还有没有安装,Node.js官网是一个很好的开始的地方。

安装webpack有两种方式:全局安装和本地安装。如果你使用全局安装,你可以在任何地方使用它,但这样做webpack就不会作为你项目的依赖而包含在内,所以你就不能在不同的项目中使用不同版本的webpack(比如你开始一个新的项目,需要更多的功能,所以你升级到版本,而那些老的项目又不能兼容,这种情况很常见),因为你的webpack是全局的。所以,我建议本地安装CLI包,并使用相对路径或者npm script来运行。如果你还没有本地安装CLI包,你可以阅读这篇文章了解详细。

后面我们会使用npm script的方式,所以你可以先不用看本地安装的内容。首先:创建一个文件夹作为你项目的根目录,作为我们学习和实验webpack的地方,GitHub有相应的代码你可以clone使用,当然你也可以自己跟着创建相应的文件。

现在我们进入你项目的根目录,你应该会运行npm init来初始化你项目,后面需要填写的内容不是很重要,除非你希望在npm上发布你包。

现在你有了一个package.json的文件(当你执行npm init创建时创建的),你项目所有的依赖信息都在这个文件里。现在让我们将webpack作为我们项目的第三方依赖进行安装,执行:npm install webpack -D。(-D 意思是将依赖的信息写入到package.json,你也可以使用–save-dev)。

在我们可以使用webpack之前,我们应该有一个简单的应用去测试它。我说简单的应用,因为它确实很简单。不过在这之前我们先安装Lodash以便我们可以在我们的应用中测试第三方的依赖:nmp 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 script来实现(我们没有全局的安装webpack,所以我们不能直接运行webpack命令)。在package.json,我们像下面这样编写”script”字段:

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

然后,如果你运行npm run build,webpack便开始工作了。当执行完之后,我们变会得到一个新的dist/bundle.js的文件。现在你可以执行这个文件了,不论是Node.js还是浏览器得到的结果应该都是一样的。

在我们进一步探索webpack之前,让我们先编写一个稍微专业一点的npm script,实现在打包之前先删除dist目录以及其中的文件的功能,以及增加一下脚本来执行我们的打包。首先我们需要做的是安装del-cli,这样我们就可以实现删除目录,而且不用担心那些和我们使用不同操作系统的人删除命令是否有效的问题了。npm install del-cli -D。之后更新我们的npm script脚本:

…
 "script": {
   "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的部分,但是提供了清除dist目录的prebuild命令。每次我们执行build命令前都会先执行这里的prebuild命令。我们还加入了execute命令,使用Node.js执行打包命令。当然我们可以使用start一条命令执行所以的任务(-s参数目的是在console中不产生其他没有用的信息)。当我们执行npm start,你就能很快看见webpack的产出了,也就是命令行打印出来的新的数组。如果你得到这样的结果,那么恭喜你,你已经完成了我们的第一个示例了。

使用配置文件

和上面我们使用命令行开始学习使用webpack一样有趣,如果你想了解更多的关于webpack的特性,命令很多的时候,你就会希望把你命令移到一个配置文件中去,而不是全部使用命令行的方式,这样做有很多原因,主要就是配置文件的方式更加灵活,功能更强大,而且可读性更强,因为它是用JavaScript编写的。

所以我们先创建一个配置文件,只需要在你的项目的根目录中新建一个叫webpack.config.js的文件就行了。webpack会默认寻找这个文件进行配置,当然你也可以自定义默认配置文件的文件名和路径,只需要在使用命令时传入一个参数--config [filename]就行了。

在这篇教程中,我们使用标准的文件名。现在我们尝试将上面使用过的示例通过配置文件的方式来实现,为了实现它,我们需要在我们的webpack.config.js文件中添加如下代码:

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

在配置文件中,我们说明了入口文件和产出文件,和我们在上面使用命令行的方式很相似。注意这是一个JavaScript文件,而不是一个JSON文件,所以我需要导出配置对象(configuration object),及这里的module.export。虽然这样看起来并没有我们使用命令好看,但不要着急,在我们的教程结尾时,你就会发现这很有用了。

现在我们就可以删除package.json中那些传给webpack的参数了,你的脚本看起来应该差不多像这样:

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

你可以象上面那样运行npm start,之后的输出你应该很熟悉。这就是我们的第二个示例了。

使用Loaders

通常来讲我们有两种方式扩展webpack的功能:loaders(加载器) 和 plugins(插件)。插件我们会在后面讨论,我们先看一下loaders。loaders的主要功能是对给定类型的文件进行转换或是操作得到相应的输出。对单一类型的文件你可以链式的调用多个loader进行处理,如果你使用过gulp你就很容易明白,,它有点类似gulp中的task,就像我们在工厂中经常看到的流水线一样,我们的loader就像流水线上的各类工序,经过链式的层层加工处理,最终得到我们想要的产出。例如,你可以指定我们的.js文件,先使用ESLint进行预处理,之后在使用Babel将es6解析成es5。如果ESLint的执行过程中出现了错误,将会在console中打印出相应的信息,一旦链式中的一环出现了问题,webpack就会停止工作了。

对于我们这里的这个小应用程序来说,为了能尽量先简单起见,我暂时不在webpack中加入linting的功能,只是想是webpack能利用Babel将ES6解析为ES5。当然,为了测试,我们先得有一些ES6的代码对吧,所以我们将我们的main.js用ES6改写一下:

  import {map} from 'lodash';

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

上面的代码在功能上还是一样的,不同的是我们使用了箭头函数来代替square函数,另一方面,我使用了import关键字来加载lodash模块,这些都是ES6中的语法。这样实际上会加载的是一个全量的Lodash的文件在我们进行打包的时候,而不是只是按需要的只加载lodash/map。当然你可以重新写一下第一行代码,改成import {map} from 'lodash'来改善。我这里之所没有这样去改,主要是因为:
  1. 在一些大型的应用中,你可能会使用Lodash库中很多功能,所以你可能需要加载全部。
  2. 如果你在使用Backbone.js,你就会知道,分开单独的加载所需的函数是非常困难的,因为没有文档告诉你它需要的依赖是哪些。
  3. 在下一个版本的webpack中,开发者计划包含构建文件依赖树之类的功能,这样就可以去掉那些没用被使用的模块了,所以我们这样写在下一个版本中仍可以正常工作。
  4. 我想让它作为一个示例来强调一下上面关于打包的一些要点。

(注意:这两种加载Lodash的方式都可以正常工作的原因是,Lodash的开发者提供了这样的功能,并不是所有的库都可以这样工作的)。

总之,我们有了ES6的代码了,现在我们的任务是将它解析为ES5,以便可以在我们的浏览器中使用。首先,我们需要Babel以及Babel关于webpack的一些依赖,这样Babel才能在webpack中正常运行起来。最少的话,我们需要babel-core(Babel的核心功能,执行绝大多数的工作),babel-loader(webpack loader可以调用babel-core的接口),babel-preset-es2015(包含Bable将ES6转换为ES5的转码规则)。同时我们也会安装bable-plugin-transform-runtimbabel-polyfill两个模块。具体可以查看这里Babel入门教程和这里介绍Babel各种插件的Bable全家桶

好了,现在让我们来安装这些依赖吧:npm i -D babel-core bable-loader bable-preset-es2015 bable-plugin-transform-runtime bable-polyfill。之后我们来编写配置文件webpack.config.js,首先我们需要在文件中找个地方添加我们的loader,修改webpack.config.js文件之后像这样。

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

这里我们添加了一个命名module的字段,module中包含值一个rules的字段,rules是一个数组,这个数组包含了每个loader的配置内容。这是我们将要添加babel-loader的地方。对于每一个loader我们最少需要配置两个参数:testloader,test通常是一个说明每个文件的绝对路径的正则表达式。这些正则表达式通常用来匹配文件的类型,比如/\.js$/匹配.js文件,在我们后面的示例文件中,我们编写为/\.jsx?$/匹配jsjsx文件,如果你希望在项目中加入react代码。之后我们需要配置loader参数,告诉webpack使用那些loader来处理test匹配出来的文件。

这里我们只需依次传入loader的名称就可以了,名称直接使用!连接,就像这样:bable-loader!eslint-loader。webpack会从右往左的读取配置,所以这里eslint-loader会先执行,而bable-loader后执行。如果你想为一些loader需要指定特定的参数,你可以使用query语法,比如为Bable指定fakeoption参数为truebable-loader?fakeopton=true!eslint-loader。你也可以使用use代替loader来指定需要的loaders,use使用数组而不是字符串,这样看起来可能会清晰一些:use:['bable-loader?fakeoption=true', 'eslint-loader'],这样有很多的loader的时候,更方便阅读。

因为Bable是我们示例里唯一的loader,所以我们关于loader的配置大概像这样:

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

如果你像我们这里一样只使用了一个loader,那么除了上面说的query的方式指定loader参数外,你还可以使用options对象,采用键值对的方式,比如上面的例子,还可以这样写:

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

我们这里采用了这样的语法,为Bable指定了几个参数:

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

首先需要配置ES6的语法规则,这样才能将ES6解析为ES5,同时我们也配置了transform-runtime插件,当然这个插件不是必须的。还有一种可选的方案就是使用.babelrc文件来配置这些参数,但后面我不会告诉你怎么做,通常我建议使用.babelrc进行配置,但为了更好的说明webpack的配置,我们采用在webpack的配置文件中配置。

配置到这里,我们只差一件是就可以完成loader的配置了。我们需要告诉Babelnode_modules目录下的文件是不需要进行解析的,这样可以提高我们打包的效率。我们可以使用exclude来指定哪些目录下的文件不需要做处理。exclude的值也是一个正则表达式,我们的项目中可以这样配置:

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

或者,我们可以使用include来指定只解析src目录,道理差不多。现在,你运行npm start可以发现ES6代码解析成了ES5,执行的结果同上。如果你希望使用polyfill插件而不是transform-runtime插件,你只需要修改两处就行了,先删除['transform-runtime']然后在entry中加入babel-polyfill即可。

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

除了使用webpack的配置文件,我们也可以在src/main.js文件中加入import 'babel-polyfill',最后的效果都是一样的。我们这里采用了在webpack的配置文件中添加的方式,主要是因为这我们后面的示例需要这样做,而且这样也是很好的例子,说明了如何将多个文件打包到一起。总之,这是我们的第三个示例,你可以运行npm start,结果应该和上面的结果是一致的。

使用Handlebars-loader

让我再加入一个新的的loader:handlebars-loader。handlebars-loader会将Handlebars模板文件解析为一个JavaScript函数,当我们在其他JavaScript文件中import这个模板文件时(import template from './handlebars.hbs')。这个函数会被导入到JavaScript中。这是我喜欢webpack loader的一个重要原因:你可以import非JavaScript文件,并将它们全部打到一个包中,这些文件会被处理成能被JavaScript处理的形式。还有一个经常使用到的例子,就是我们可以使用loader将图片解析为base64编码,最后利用JavaScript在页面中输出图片。如果你合理利用多种loader,你甚至可以做到减少图片的大小,提升性能。

照例,我们首先需要安装这个loader:npm i -D handlebars-loader。如果你尝试使用他,你会发现它还不能工作,因为你还需要Handlebars本身:npm i -D handlebars。这里你可以自行控制Hanldebars的版本,不需要和handlebars-loader的版本保持同步,因为它们是相互独立的。

在我们已经成功安装上面两个模块之后,我们现在需要一个handlebars模板来进行后面的工作。所以让我们来编写一个handlebars模板文件,numberlist.hbs,放到我们的src目录中就可以了。

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

这个模板的功能是很明显的,就是产生一个无序列表ul,列表的每一项的值是数组numbers的各个项。

好了,现在我们修改一下我们的main.js文件,这次是使用模板文件来产生一个无序列表,而不是像原来一样只是输出数组而已。修改完了之后,你的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}))

如果你现在运行npm start,你会发现它还不能正常工作,因为我们的webpack还不知道怎样加载numberlist.hbs这个文件,numberlist.hbs不是JavaScript文件,如果想让它能正常工作,我们可以对import稍作修改,告诉webpack使用handlebars-loader处理。

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}))

这里我们在numberlist.hbs文件路径的前面添加了一个loader名称作为前缀,并使用!隔开,目的就是告诉webpack使用handlebars-loader来加载numberlist.hbs文件,这样我们就可以不必修改webpack配置文件了。但是在大一些的项目中,我们可能会有非常多的模板,使用配置文件来进行配置就显得更有意义了,这样我们就不必在每个文件中都加入这样的前缀了,更容易维护。所以让我们再修改一下webpack.config.js

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

这个示例非常简单,我们的目的就是让webpack知道使用handerbars-loader来处理.hbs类型的文件。运行npm start,输出的结果大概像这样:

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

使用插件

除loaders外,插件也是一种为webpack提供自定义功能的方式。相交于loaders,插件在扩展webpack的工作流上更加自由,因为插件没有指定文件类型的限制。因此它可以在我们工作流的任意位置注入,所以能做的事情也就更多。很难从概念上说明插件能做多少。在npm的安装包中,我们搜索webpack-plugin,可以看到一个相关的列表,从这里我们可以大概看出webpack能做的事情。

在这篇教程中,我们使用了两个插件(还有一个后面可以看到)。到这里我们的教程已经很长了,所以再多几个关于插件的示例,也没什么,对吧?第一个我们要使用的插件是HTML Webpack Plugin,用来为我们的项目产生一个HTML文件,作为我们web应用的入口。

使用插件之前,我们先更新一下我们的srcipt脚本,让我们能够跑一个简单的web server来测试我们的应用。当然首先我们得安装一个server: npm i -D http-server。之后我们去掉exxcute,添加server命令:

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

webpack打包之后,npm start将会启动一个web server,你可以浏览localhost:8080查看这页面。当然在这之前,我们需要使用HTML插件构建起页面,所以首先我们需要安装插件: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()
    ]
};

这里有两处修改,第一是在配置文件的顶部import我们安装的插件,第二是在尾部添加了plugins部分。这里我们还没有向插件传递任何参数,所以插件产生的是默认的html模板,没有太多的东西,但多了一个script标签,其src为我们打包之后的bundle.js。如果运行npm start在浏览器中查看,会看见一个空白的页面,但在浏览器的开发工具中你可以看到打印出的数组。

我们应该有自己的模板而不是使用插件提供的默认模板,这样我才能在页面中显示数组,而不是在开发者工具中。所以首先,让我们来编写一个我们自己的模板,在src目录中,我们创建一个新的index.html文件。默认情况下,这个插件使用EJS作为模板,当然你可以也可以自定义use any template language。为了简单一些,我们这里使用默认的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>

不过你需要注意的有:

  • 我们通过想插件传递参数来定义title。
  • 我们并没有手动添加脚本文件,因为插件会默认在body标签最后添加。
  • 这里的包含一个带有id的div是我们随便取的,后面我们会用到。

现在我们有了自己的模板了,所以至少我们不会再看到一个空白的页面了。重新改写main.js,将其最后一行改写为:document.getElementById("app-container").innerHTML = template({numbers})。将节点插入我们模板中定义的div中,而不是打印出来。

当然我们也需要改写一下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'
        })
    ]
};

这里的tempalte参数指定模板路径,title参数被传入模板中,如果你运行npm start,浏览器显示大致如下: html image

到这里,我们的第5个示例也就结束了,如果你也一直跟着练习的话。每一个插件都有不同的参数和配置,webpack的插件非常多而且实现各种不同的功能,但我们都在webpack配置文件中plugin数组中对他们进行管理配置。

在github上我们的示例代码中,还有一个关于插件的example6分支。通过插件为webpack提供了JavaScript压缩的功能。但现在来说,这已经不是必须的了,除非你要学习了解一下UglifyJS的一些配置规则。如果你不喜欢UglifyJS的这些默认设置,你可以检出代码,修改webpack.config.js进行自定义设置。但如果这些配置符合你的需求,你可以不再使用UglifyJS插件,你需要做的只是在运行webpack时增加一个-p参数就行了,这个参数是production的缩写,等同于--optimize-minimize--optimize-occurence-order参数,第一参数压缩JavaScript代码,第二个参数优化模块脚本在包中的顺序,使包的体积更小,执行效率更高。github上面的代码已经有一段时间了,我后面才了解了-p这个参数,所以这里保留了使用了UglifyJS的示例,同时介绍了这个更简单的方法。另外一个可以使用的快捷参数是-d,可以在无需更多配置的情况下,显示更多的webpack的调试信息,并且生成一个映射树。你也可以在这里查看更多的快捷参数,如果你觉得这样更简单的话。

分块延迟加载

我喜欢RequireJS,而且不能放弃它转为Browserify(虽然现在可能可以了)的原因之一就是——懒加载。一个大体积的JavaScript文件虽然可以减少HTTP请求,但也会造成下载很多不必要的代码。

webpack有一种可以将包拆分为可以延迟加载的块的方法,而且你甚至不需要任何配置。而你需要做的就只是用下面两种方式中的一种书写你的代码,webpack会自动帮你完成后面的事情。webpack提供了两种方案,一种基于CommonJS,另一种基于AMD。使用CommonJS的方式实现延迟加载,你可以像这样写:

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

使用requier-ensure,会确保模块可用(但不会执行它),并传递一个模块名称组成的数组和一个回调函数。想要在回调中实际的使用这些模块,你需要显示的使用传递给回调函数的参数。
就我个人而言,我觉得这样有些冗长,所以我们看看ADM的方案:

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

使用AMD,你需要使用require,传入一个一拉模块组成的数组和一个回调,回调函数的参数和依赖模块数组的顺序是一样的。

Webpack2 也支持System.import,不同于上面两者,它使用的是promises而不是回调。我认为这是一个很有用的改进,将他们包入promise应该也不困难,如果你现在就要使用的话。不过需要注意的是,System.import已经落后余import()的一些新规范了。如果你使用Babel(或者TypeScript)将会抛出语法错误。你可以使用babel-plugin-dynamic-import-webpack,但这会将其转为requier.ensure而不是让Babel识别合法的import函数或者保量它让webpack来处理。到目前我还没没看到AMD或者require.ensure将很快被淘汰,并且System.import将会在version 3中被支持,在不久的将来,所以使用你喜欢的方式就好。

现在让我们扩展一下我们的代码,实现等待几秒然后,延迟加载输出Handlebars模板的功能。要实现这样的功能也很简单,只需要将顶部的import删除,然后增加一个setTimeout函数就可以实现了:

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秒之后,一个新的资源被加载执行。你可以看到,这是不难实现的,但却可以很好的减少文件的大小,并且提高用户体验。

注意,这些sub-bundles或者块包含所有的依赖模块,除了那些它们的父块已经包含的模块。(你可以有多个入口,每个入口延迟加载相应的快,这样就可以将不同的依赖关系加载到每个父级)。

创建一个Vendor Chunk

让我们来讨论一个我们能做到的进一步的优化:vendor chunks。你可以定义一个单独的包来放置一些常用(common)的模块,或者放置一些第三方(third-party)的代码,通常这些模块不会更改。这样客户端就可以将这些包与你的应用代码分离缓存了,所以当你更新你的应用代码时,客户端就不必再次在下载这些没有改变的包了。

为了实现这样的效果,我们将会用到一个名为CommonChunkPlugin的插件。因为它包含在webpack内部,所以我们不需要单独下载,我们需要做的就是改写一下webpack.confi.js文件:

var HtmlwebpackPlugin = require('html-webpack-plugin');
var CommonChunkPlugin = require('webpack/lib/optimize/CommonChunkPlugin');
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 CommonsChunkPlugin({
            name: "vendor",
            filename: "vendor.bundle.js"
        })
    ]
};

我们在第二行import这个插件,然后entry部分,我们使用了不同的设置,使用了一个对象,来指定多个入口。vendor入口标志着哪些模块将被包含到vendor chunk中,这里我们指定了polyfill和Lodash两个模块。然后我们指定main入口。之后,我们只需要在plugin中简单的配置一下CommonChunkPlugin插件,这里我们指定vendor块作为基础块,并指定将vendor的代码将被储存在一个叫vendor.bundle.js的文件中。