原文发布与 抹桥的博客 -【翻译向】webpack2 指南(上)javascript
Bundle 代码包
Chunk 代码块css
npm install webpack --save-devhtml
代码分割是 webpack 中最引人注目的功能之一。它容许你把代码分割成各类能够根据需求载入的代码包,就像一个用户浏览器去匹配路由同样,或者一个用户发出的事件。这容许你小的模块,容许你控制资源的载入优先级,若是使用得当的话,能够大大影响(下降)你应用的加载时间。java
一个典型的应用会依赖不少第三方的框架和库文件。不像应用代码自己,这些第三方代码的变动很是频繁。
若是咱们保持这些库在它自己的代码包中,从应用代码自己分离出来,那么咱们就可使用浏览器的缓存策略去在一个长时间内把这些代码缓存到最终用户的机器上。node
为了达到这个效果,第三方代码的 verndor 包的 hash 部分必须保持不变,无论应用代码如何变化。学习 如何经过 CommonsChunkPlugin
来分割 verndor/libray 代码。webpack
你可能也想把样式文件分割成为一个单独的包,从应用逻辑总独立出来。这能够加强样式文件的可缓存性,而且容许浏览器在加载应用代码时并行加载你的样式文件,所以也能够避免 FOUC (一个无样式内容的闪屏)。
学习 如何去分割 CSS 经过使用 ExtractTextWebpackPlugin
.git
虽然前面的资源分割须要用户在配置文件中预先指定分割点,可是也能够在应用代码中建立动态的分割点。es6
这个功能在有不少细微颗粒代码块时会颇有用,举个例子,每个应用的路由或者按照用户的预测行为。这可使用户按需加载须要的资源。github
require.ensure()
来分割代码require.ensure
是一个 CommonJS 风格的方式去异步加载资源。经过添加 require.ensure([<fileurl>])
, 咱们能够在代码中定义一个分割点。 Webpack 能够建立一个包含在这个分割点中的全部代码的代码包。学习 如何分割代码 经过使用 require.ensure()
.web
TODO System.import()
在 webpack 中,当你使用 css-loader 而且在 JavaScript 中引入 CSS 文件,那么 CSS 文件会被打包在你的 JavaScript 文件中。这有一个很差的地方,就是你没法使用浏览器异步并行加载 CSS 的能力。相反,你的页面会等到整个 JavaScript 文件加载完成,才完成了样式文件的加载。Webpack 能够经过使用 extract-text-webpack-plugin 和 css-loader 来把样式文件分离出来去解决这个问题。
css-loader
引入 css 到你的 JavaScript 中,须要使用 css-loader 去配置 webpack 的配置文件。
//webpack.config.js modules.exports = function(env){ entry: '..', ... module: { rules: [{ test: /\.css$/, exclude: /node_modules/, loader: 'css-loader' }] } ... }
extract-text-webpack-plugin
- ExtractTextPlugin安装:
npm I --save-dev extract-text-webpack-plugin
要使用这个 ExtractTextPlugin
,须要经过两个步骤配置在 webpack.config.js
中。
从以前的 css-loader
中适配,咱们应该以下添加 ExtractTextPlugin
.
loader: ExtractTextPlugin.extract('css-loader?sourceMap') //Can be used without sourcemaps too.
new ExtractTextPlugin({ filename: 'bundle.css', disable: false, allChunks: true })
经过这两步,就能够生成一个新的包含全部 CSS 模块的代码包,而后把他们添加到 index.html
的 heade
中去。能够经过 ExtractTextPlugin 去了解关于它 api 的更多信息。
完整的配置文件以下:
var ExtractTextPlugin = require('extract-text-webpack-plugin'); module.exports = function () { return { entry: './main.js', output: { path: './dist', filename: 'bundle.js' }, module: { rules: [{ test: /\.css$/, exclude: /node_modules/, loader: ExtractTextPlugin.extract({ loader: 'css-loader?sourceMap' }) }] }, devtool: 'source-map', plugins: [ new ExtractTextPlugin({ filename: 'bundle.css', disable: false, allChunks: true }) ] } }
一个典型的应用会依赖不少第三方来提供框架或功能支持。项目中使用的固定版本的库/框架文件的代码通常不会有变更,然而应用自己的业务逻辑代码却常常会有变更。
把应用代码和库文件的代码打包在一块儿是一件很是低效的事情。这是由于浏览器能够根据缓存头缓存这些资源文件到本地而不用每次都去服务端或者 cdn 上去发请求从新获取,若是文件内容没有变更的话。为了可以享受这个好处,咱们须要保持第三方文件的 hash 不变,不管应用自己的代码如何变化。
咱们只有把应用代码和第三方代码分离开才能够达到这样的效果。
咱们考虑一个一个简单的应用,它使用了 momentjs ,一个一般用来时间格式化的库。
安装 moment
:
npm install --save moment
Index 文件会引用 moment
做为一个依赖而且打印当前的时间:
Index.js
var moment = require('moment'); console.log(moment().format());
咱们能够经过以下这个配置文件来打包这个应用
Webapck.config.js
module.exports = function(env) { return { entry: './index.js', output: { filename: '[chunkhash].[name].js', path: './dist' } } }
当运行 webapck
命令的时候,若是你检查打包后的文件,你会发现 moment
和 index.js
都被打包在了 bundle.js
中。
这不是一个很好的解决方案。若是 index.js
修改了,那么这打包文件会从新构建,浏览器就须要从新去加载这个文件,即便 moment.js 文件并无任何改动。
让咱们缓和这个问题,咱们给 moment 添加一个新的入口命名为 vendors.
Webpack.config.js
module.exports = function(env) { return { entry: { main: './index.js', vendor: 'moment' }, output: { filename: '[chunkhash].[name].js', path: './dist' } } }
如今执行 webpack 命令,咱们会看到两个打包后的文件。若是你检查里面代码的话,你会看到 moment
的代码同时出如今两个代码包中。
为了解决这个问题,咱们须要使用 CommonsChunkPlugin.
CommonsChunksPlugin
这是一个至关复杂的插件。它从根本上容许你从不一样的代码包中提取出全部的相同模块而且把它们加入到共同的代码包中。若是这个相同的代码包不存在,那么就建立一个新的。
咱们能够修改 webpack 的配置文件来使用这个 CommonsCunksPlugin
Webpack.config.js
var webpack = require('webpack'); module.exports = function(env) { return { entry: { main: './index.js', vendor: 'moment' }, output: { filename: '[chunkhash].[name].js', path: './dist' }, plugins: [ new webpack.optimize.CommonsChunkPlugin({ name: 'vendor' // Specify the common bundle's name. }) ] } }
这样的话, moment
的代码就只会出如今 vendor 代码包中了。
可是,若是咱们能够修改应用的代码而且再次执行 webpack
命令,咱们看到 vendors 文件的 hash 仍是变化了。即便咱们已经分离了 vendor
和 main
代码包,可是当应用代码发生修改的时候 vendor
仍是变化了。 这意味着咱们依旧不能享受浏览器缓存带来的好处,由于每一次从新编译都会修改 vendors 的 hash 值。
这个问题是由于每一次编译,webpack 生成一些 webpack 运行时代码,用来帮助 webpack 来作它的工做。当那里存在一个单独的代码包,运行时会驻留在其中。但当多个代码包被生成的时候,运行时代码会被提取到公共的模块中,就是这个 vendor
文件。
为了阻止这个,咱们须要提取出运行时到一个分离的清单文件(Manifest File)。虽然咱们又多建立另外一个代码包,但它的开销也被咱们在 vendor 文件上得到的长期缓存所带来的好处所抵消了。
Webpack.config.js
var webpack = require('webpack'); module.exports = function(env) { return { entry: { main: './index.js', vendor: 'moment' }, output: { filename: '[chunkhash].[name].js', path: './dist' }, plugins: [ new webpack.optimize.CommonsChunkPlugin({ names: ['vendor', 'manifest'] // Specify the common bundle's name. }) ] } };
经过上面这个配置文件,咱们会看到三个代码包被生成。vendor
,main
和 manifest
. 这样当应用代码修改的时候,从新打包后,修改的就只有 main
和 manifest
了。 manifest
被修改是由于里面有对生成文件 hash 值的引用。
在这个章节,咱们讨论 webpack 如何经过 require.ensure()
分割代码。
require.ensure()
Webpack 静态分析给 require.ensure()
在代码中当构建和添加模块到分离的代码块中。这个新的代码块会被 webpack 在须要的时候经过 jsonp 引入。
它的语法以下:
require.ensure(dependencies: String[], callback: function(require), chunkName: String)
这是一个字符串数组,用来声明全部须要在执行回掉函数以前就须要预先加载好且可用的模块。
一个回调函数会被 webpack 执行一次当全部依赖(dependencies)都被加载之后。Require 对象的实现做为一个参数传递给这个回调函数。这样,咱们能够更进一步 require
须要的依赖(dependencies)和其余须要执行的模块。
代码块名字是一个用来命名经过 require.ensrue()
建立的代码块。经过给不一样的 require.ensure()
建立的代码分割点分割出来的代码块一个相同的名字,咱们能够确保全部的依赖都被打包到同一个代码块中。
咱们来看一下以下结构的一个项目
\\ file structure | js --| | |-- entry.js | |-- a.js | |-- b.js webpack.config.js | dist
// entry.js require('a'); require.ensure([], function(require){ require('b'); }); // a.js console.log('***** I AM a *****'); // b.js console.log('***** I AM b *****');
// webpack.config.js module.exports = function(env) { return { entry: './js/entry.js', output: { filename: 'bundle.js', path: './dist' } } }
当运行 webpack 命令的时候,咱们发现 webpack 建立了两个新的代码包,bundle.js
和 0.bundle.js
.
entry.js
和 a.js
被打包到了 bundle.js
中。
b.js
被打包到了 0.bundle.js
require.ensure()
的陷阱require.ensure([], function(require){ require('./a.js'); });
上面的代码确保一个分割点被建立, a.js
会被 webpack 单独的打包成一个文件。
require.ensure(['./a.js'], function(require) { require('./b.js'); });
上面的代码,a.js
和 b.js
会被一块儿打包而且从主代码包中分离出来。可是只有 b.js
的内容被执行了。 a.js
的内容只是是可用的但并无被执行。为了执行 a.js
, 咱们须要 require 它做为一个同步的方式好比 require('./a.js)
,这样 JavaScript 就能够执行它了。
Ø es6 module Ø Commonjs Ø Amd
当你经过表达式去引入一个模块的时候,就会建立一个上下文,因此当编译的时候咱们并不知道准确的模块是哪一个。
例:
require("./template/" + name + ".ejs");
Webpack 解析 require()
的调用,而且提取出来一些信息:
Directory:./template Regularexpression:/^.*\.ejs$/
一个上下文模块被生成。它包含了在这个文件夹下全部能够被上面的正则匹配所匹配到的模块的引用。上下文模块包含了一个把请求解释到模块 id 的 map.
例:
{ "./table.ejs": 42, "./table-row.ejs": 43, "./directory/folder.ejs": 44 }
上下文模块一样包含了一些运行时代码用来访问这个 map.
这意味着动态的引用能够被支持,可是会致使全部可能被用到的模块都被打包到了最终的代码包中。
require.context
你能够经过 require.context()
方法建立你本身的上下文。它容许你传入一个用来查询的文件夹,一个用来决定是否递归查找子文件夹的标识,还有一个用来匹配文件的正则表达式。
Webpack 会在代码打包的时候解析 require.context()
它的语法以下:
require.context(directory, useSubdirectories = false, regExp = /^\.\//)
例:
require.context("./test", false, /\.test\.js$/); // a context with files from the test directory that can be required with a request endings with `.test.js`.
require.context("../", true, /\.stories\.js$/); // a context with all files in the parent folder and descending folders ending with `.stories.js`.
一个上下文模块暴露一个方法,它接收一个参数:请求的内容。
暴露出来的函数有三个属性:resolve
,key
,id
• `resolve` 是一个函数,执行后返回解析后的请求内容的模块 id • `keys`是一个函数,执行后返回一个数组,包含全部可能被上下文模块所请求的全部的模块的 id 当你想要经过正则匹配引入一个文件夹下全部模块时,这会很是有用:
function importAll (r) { r.keys().forEach(r); } importAll(require.context('../components/', true, /\.js$/))
var cache = {}; function importAll (r) { r.keys().forEach(key => cache[key] = r(key)); } importAll(require.context('../components/', true, /\.js$/)); // At build-time cache will be polulated with all required modules.
• `id` 是上下文模块生成的模块 id. 当使用 `module.hot.accept` 时,这会很是有用。