吐槽一下
webpack 自出现时,一向备受喜爱。作为壮大的打包东西,它只是出如今项目初始或优化的阶段。假如没有介入项目的构建,打仗的时机险些为零。纵然是介入了,但也会由于项目的周期短,从网上东拼西凑敷衍了事。
纵观网上的 webpack 教程,要么是走马观花,科普了一些通例设置项;要么是过于深切道理,于实际操作无益。近来一段时刻,我把 webpack 的官方文档来来回回地看了几遍,效果发明,真香。中文版的官方文档,通俗易懂,很谢谢翻译组的辛劳贡献。看完以后,虽然达不到出神入化的田地,但也不会左支右绌,疲于敷衍。
关于这类东西类的博文,依旧因循 用Type驯化JavaScript 的作风,串连各个观点。至于细节,就是官方文档的事了。
本文基于 webpack v4.31.0 版本。
Tapable
Tapable 是一个小型的库,许可你对一个 javascript 模块增加和运用插件。它能够被继续或混入到其他模块中。相似于 NodeJS 的 EventEmitter 类,专注于自定义事宜的触发和处置惩罚。除此之外,Tapable 还许可你经由历程回调函数的参数,接见事宜的“触发者(emittee)”或“供应者(producer)”。
tapable 是 webpack 的中心,webpack 中的许多对象(compile, compilation等)都扩大自tapable,包含 webpack 也是 tapable 的实例。扩大自 tapable 的对象内部会有许多钩子,它们贯串了 webpack 构建的悉数历程。我们能够应用这些钩子,在其被触发时,做一些我们想做的事变。
抛开 webpack 不谈,先看看 tapable 的简朴运用。
// Main.js
const {
SyncHook
} = require("tapable");
class Main {
constructor(options) {
this.hooks = {
init: new SyncHook(['init'])
};
this.plugins = options.plugins;
this.init();
}
init() {
this.beforeInit();
if (Array.isArray(this.plugins)) {
this.plugins.forEach(plugin => {
plugin.apply(this);
})
}
this.hooks.init.call('初始化中。。。');
this.afterInit();
}
beforeInit() {
console.log('初始化前。。。');
}
afterInit() {
console.log('初始化后。。。');
}
}
module.exports = Main;
// MyPlugin.js
class MyPlugin {
apply(main) {
main.hooks.init.tap('MyPlugin', param => {
console.log('init 钩子,做些啥;', param);
});
}
};
module.exports = MyPlugin;
// index.js
const Main = require('./Main');
const MyPlugin = require('./MyPlugin');
let myPlugin = new MyPlugin();
new Main({ plugins: [myPlugin] });
// 初始化前。。。
// init 钩子,做些啥; 初始化中。。。
// 初始化后。。。
明白起来很简朴,就是在 init
处触发钩子,this.hooks.init.call(params)
相似于我们熟习的 EventEmitter.emit('init', params)
。main.hooks.init.tap
相似于 EventEmitter.on('init', callback)
,在 init
钩子上绑定一些我们想做的事变。在后面将要说的 webpack 自定义插件,就是在 webpack 中的某个钩子处,插进去自定义的事。
理清观点
依靠图
在单页面运用中,只需有一个进口文件,就能够把散落在项面前目今的各个文件整合到一同。何谓依靠,当前文件须要什么,什么就是当前文件的依靠。依靠引入的情势有以下:- ES2015
import
语句 - CommonJS
require()
语句 - AMD
define
和require
语句 - 款式(
url(...)
)或 HTML 文件(<img src=...>
)中的图片链接
- ES2015
- 进口(entry)
进口出发点(entry point)指导 webpack 应当运用哪一个模块,来作为构建其内部依靠图(dependency graph)的最先。 - 输出(output)
output 属性通知 webpack 在那里输出它所建立的 bundle,以及怎样定名这些文件。 - 模块(module)
决议了怎样处置惩罚项目中的差别范例的模块。比方设置 loader,处置惩罚种种模块。设置 noParse,疏忽无需 webpack 剖析的模块。 - 剖析(resolve)
设置模块怎样被剖析。援用依靠时,须要晓得依靠间的途径关联,应遵照何种剖析划定规矩。比方给途径设置别号(alias),剖析模块的搜刮目次(modules),剖析 loader 包途径(resolveLoader)等。 - 外部扩大(externals)
防备将某些 import 的包(package)打包到 bundle 中,而是在运行时(runtime)再去从外部猎取这些扩大依靠。比方说,项目中援用了 jQuery 的CDN资本,在运用import $ from 'jquery';
时,webpack 会把 jQuery 打包进 bundle,实在这是没有必要的,此时须要设置externals: {jquery: 'jQuery'}
,将其剔除 bundle。 - 插件(plugins)
用于以种种体式格局自定义 webpack 构建历程。能够应用 webpack 中的钩子,做些优化或许搞些小动作。 - 开辟设置(devServer)
望文生义,就是开辟时用到的选项。比方,开辟效劳根途径(contentBase),模块热替代(hot,需合营HotModuleReplacementPlugin
运用),代办(proxy)等。 - 形式(mode)
供应 mode 设置选项,示知 webpack 运用响应环境的内置优化。详细可见 形式(mode) - 优化(optimization)
从 webpack 4 最先,会依据你挑选的 mode 来实行差别的优化,不过一切的优化照样能够手动设置和重写。比方,CommonsChunkPlugin
被optimization.splitChunks
庖代。
webpack 差不多就是这几个设置项,搞清楚这几个观点,上手照样比较轻易的。
代码星散
如今的前端项目愈来愈庞杂,假如终究导出为一个 bundle,会极大地影响加载速率。切割 bundle,掌握资本加载优先级,按需加载或并行加载,合理运用就会大大收缩加载时刻。官方文档供应了三种罕见的代码星散要领:
进口出发点
设置多个进口文件,然后将终究天生的过个 bundle 相差到 HTML 中。// webpack.config.js entry: { index: './src/index.js', vendor: './src/vendor.js' } output: { filename: '[name].bundle.js', }, plugins: [ new HtmlWebpackPlugin({ chunks: ['vendor', 'index'] }) ]
不过假如这两个文件中存在雷同的模块,这就意味着雷同的模块被加载了两次。此时,我们就须要提掏出反复的模块。
- 防备反复
在 webpack 老的版本中,CommonsChunkPlugin
常用来提取大众的模块。新版本中SplitChunksPlugin
取而代之,能够经由历程optimization.splitChunks
设置,多见于多页面运用。 动态导入
就是在须要时再去加载模块,而不是一股脑的悉数加载。webpack 还供应了预取和预加载的体式格局。非进口 chunk,我们能够经由历程 chunkFilename 为其定名。罕见的如,vue 路由动态导入。// webpack.config.js output: { chunkFilename: '[name].bundle.js', } // index.js import(/* webpackChunkName: "someJs" */ 'someJs'); import(/* webpackPrefetch: true */ 'someJs'); import(/* webpackPreload: true */ 'someJs');
缓存
基于浏览器的缓存战略,我们晓得假如当地缓存掷中,则无需再次要求资本。关于修改不频仍或基本不会再做修改的模块,能够剥离出来。
// webpack.config.js
output: {
filename: '[name].[contenthash].js',
}
根据我们的主意,只需模块的内容没有变化,对应的名字也就不会发生变化,如许缓存就会起作用了。事实上并不是如此,webpack 打包后的文件,并不是只要用户本身的代码,还包含治理用户代码的代码,如 runtime 和 manifest。
模块依靠间的整合并不是简朴的代码拼接,个中包含模块的加载和剖析逻辑。注入的 runtime 和 manifest 在每次构建后都邑发生变化。这就致使了即运用户代码没有变化,某些 hash 照样发生了转变。经由历程 optimization.runtimeChunk
提取 runtime 代码。经由历程 optimization.splitChunks
剥离第三方库。比方, react,react-dom。
module.exports = {
//...
optimization: {
splitChunks: {
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/,
name: 'vendor',
chunks: 'all',
}
}
}
}
};
末了运用 HashedModuleIdsPlugin
来消弭因模块 ID 更改带来的影响。
loader
loader 用于对模块的源代码举行转换。loader 是导出为一个函数的 node 模块。该函数在 loader 转换资本的时刻挪用。给定的函数将挪用 loader API,并经由历程 this 上下文接见。
// loader API;
this.callback(
err: Error | null,
content: string | Buffer,
sourceMap?: SourceMap,
meta?: any
);
// sync loader
module.exports = function(content, map, meta){
this.callback(null, syncOperation(content, map, meta));
return;
}
// async loader
module.exports = function(content, map, meta){
let callback = this.async();
asyncOperation(content, (error, result) => {
if(error) callback(error);
callback(null, result, map, meta);
return;
})
}
多个 loader 串行时,在从右向左实行 loader 之前,会向从左到右挪用 loader 上的 pitch 要领。假如在 pitch 中返回了效果,则会跳过后续 loader。
|- a-loader `pitch`
|- b-loader `pitch`
|- c-loader `pitch`
|- requested module is picked up as a dependency
|- c-loader normal execution
|- b-loader normal execution
|- a-loader normal execution
<!-- pitch 中返回效果 -->
|- a-loader `pitch`
|- b-loader `pitch` returns a module
|- a-loader normal execution
plugins
webpack 的自定义插件和本文开首 Tapable 中的差不多。webpack 插件是一个具有 apply 要领的 JavaScript 对象。apply 要领会被 webpack compiler 挪用,而且 compiler 对象可在悉数编译生命周期接见。钩子有同步的,也有异步的,这须要依据 webpack 供应的 API 文档。
// 官方例子
class FileListPlugin {
apply(compiler) {
// emit 是异步 hook,运用 tapAsync 触及它,还能够运用 tapPromise/tap(同步)
compiler.hooks.emit.tapAsync('FileListPlugin', (compilation, callback) => {
// 在天生文件中,建立一个头部字符串:
var filelist = 'In this build:\n\n';
// 遍历一切编译过的资本文件,
// 关于每一个文件名称,都增加一行内容。
for (var filename in compilation.assets) {
filelist += '- ' + filename + '\n';
}
// 将这个列表作为一个新的文件资本,插进去到 webpack 构建中:
compilation.assets['filelist.md'] = {
source: function() {
return filelist;
},
size: function() {
return filelist.length;
}
};
callback();
});
}
}
module.exports = FileListPlugin;
ProvidePlugin
自动加载模块,无需到处援用。有点相似expose-loader
。// webpack.config.js new webpack.ProvidePlugin({ $: 'jquery', }) // some.js $('#item');
DllPlugin
将基本模块打包进动态链接库,当依靠的模块存在于动态链接库中时,无需再次打包,而是直接从动态链接库中猎取。DLLPlugin 担任打包出动态链接库,DllReferencePlugin 担任从重要设置文件中引入 DllPlugin 插件打包好的动态链接库文件。// webpack-dll-config.js // 先实行该设置文件 output: { path: path.join(__dirname, "dist"), filename: "MyDll.[name].js", library: "[name]_[hash]" }, plugins: [ new webpack.DllPlugin({ path: path.join(__dirname, "dist", "[name]-manifest.json"), name: "[name]_[hash]" }) ] // webpack-config.js // 后实行该设置文件 plugins: [ new webpack.DllReferencePlugin({ manifest: require("../dll/dist/alpha-manifest.json") }), ]
HappyPack
启动子历程处置惩罚使命,充分应用资本。不过历程间的通信比较耗资本,要酌情处置惩罚。const HappyPack = require('happypack'); // loader { test: /\.js$/, use: ['happypack/loader?id=babel'], exclude: path.resolve(__dirname, 'node_modules'), }, // plugins new HappyPack({ id: 'babel', loaders: ['babel-loader?cacheDirectory'], }),
- webpack-bundle-analyzer
webpack 打包后的剖析东西。
webpack 告一段落,浅尝辄止。