Closed FrankKai closed 4 years ago
global constants , configured at compile time , different behavior build in different environments: production, development, preproduction, test, etc
module.exports = {
plugins: [
new webpack.DefinePlugin({
'process.env.NODE_ENV': '"production"',
}),
]
}
webpack built-in plugins
module.exports = {
plugins: [
new webpack.HotModuleReplacementPlugin(),
]
}
webpack enhancement plugins
import DashboardPlugin = require('webpack-dashboard/plugin')
module.exports = {
plugins: [
new DashboardPlugin(),
]
}
DefinePlugin中的每个键,是一个标识符或者通过.
作为多个标识符。
typeof
前缀,它只是对typeof 调用定义的。这些值将内联到代码中,压缩减少冗余。
new webpack.DefinePlugin({
PRODUCTION: JSON.stringify(true),
VERSION: JSON.stringify('5fa3b9'),
BROWSER_SUPPORTS_HTML5: true,
TWO: '1+1',
'typeof window': JSON.stringify('object'),
'process.env': {
NODE_ENV: JSON.stringify(process.env.NODE_ENV)
}
});
console.log('Running App version' + VERSION);
plugin不是直接的文本值替换,它的值在字符串内部必须包括实际引用。典型的情况是用双引号或者JSON.stringify()进行引用,'"production"',JSON.stringify('production')。
重点:在vue-cli创建的项目中,凡是src下的文件,都可以访问到VERSION这个变量,例如main.js,App.vue等等
我们现在看一下上面的几种类型的key值,在代码中的输出。
console.log(PRODUCTION, VERSION, BROWSER_SUPPORTS_HTML5, TWO, typeof window, process.env);
PRODUCTION: true,
VERSION: "5fa3b9",
BROWSER_SUPPORTS_HTML5: true,
TWO: 2,
typeof window: "object",
process.env: {NODE_ENV: "development"},
在代码中,我们一般会有以下几种用途:
可以控制新特性和实验特性的开关。
new webpack.DefinePlugin({
'NICE_FEATURE': JSON.stringify(true),
'EXPERIMENTAL': JSON.stringify(false),
})
process: {
env: {
NODE_ENV: JSON.stringify('production')
}
}
评价:非常不好,会overwrite整个process对象,仅仅保留新的NODE_ENV,破坏进程。 原始的process对象包含如下内容 ,包含了当前进程的很多信息。
process {
title: 'node',
version: 'v8.11.2',
moduleLoadList:
[ 'Binding contextify',],
versions:
{ http_parser: '2.8.0'},
arch: 'x64',
platform: 'darwin',
release:
{ name: 'node' },
argv: [ '/usr/local/bin/node' ],
execArgv: [],
env:
{ TERM: 'xterm-256color'},
pid: 14027,
features:
{ debug: false},
ppid: 14020,
execPath: '/usr/local/bin/node',
debugPort: 9229,
_startProfilerIdleNotifier: [Function: _startProfilerIdleNotifier],
_stopProfilerIdleNotifier: [Function: _stopProfilerIdleNotifier],
_getActiveRequests: [Function: _getActiveRequests],
_getActiveHandles: [Function: _getActiveHandles],
reallyExit: [Function: reallyExit],
abort: [Function: abort],
chdir: [Function: chdir],
cwd: [Function: cwd],
umask: [Function: umask],
getuid: [Function: getuid],
geteuid: [Function: geteuid],
setuid: [Function: setuid],
seteuid: [Function: seteuid],
setgid: [Function: setgid],
setegid: [Function: setegid],
getgid: [Function: getgid],
getegid: [Function: getegid],
getgroups: [Function: getgroups],
setgroups: [Function: setgroups],
initgroups: [Function: initgroups],
_kill: [Function: _kill],
_debugProcess: [Function: _debugProcess],
_debugPause: [Function: _debugPause],
_debugEnd: [Function: _debugEnd],
hrtime: [Function: hrtime],
cpuUsage: [Function: cpuUsage],
dlopen: [Function: dlopen],
uptime: [Function: uptime],
memoryUsage: [Function: memoryUsage],
binding: [Function: binding],
_linkedBinding: [Function: _linkedBinding],
_events:
{ newListener: [Function],
removeListener: [Function],
warning: [Function],
SIGWINCH: [ [Function], [Function] ] },
_rawDebug: [Function],
_eventsCount: 4,
domain: [Getter/Setter],
_maxListeners: undefined,
_fatalException: [Function],
_exiting: false,
assert: [Function],
config: {},
emitWarning: [Function],
nextTick: [Function: nextTick],
_tickCallback: [Function: _tickDomainCallback],
_tickDomainCallback: [Function: _tickDomainCallback],
stdout: [Getter],
stderr: [Getter],
stdin: [Getter],
openStdin: [Function],
exit: [Function],
kill: [Function],
_immediateCallback: [Function: processImmediate],
argv0: 'node' }
'process.env': {
NODE_ENV: JSON.stringify('production')
}
评价:不好,会overwrite整个process.env对象,破坏进程环境,导致破坏兼容性。 原始的process.env对象包含如下内容 ,包含了当前进程的很多信息。
{ TERM: 'xterm-256color',
SHELL: '/bin/bash',
TMPDIR: '/var/folders/lw/rl5nyyrn4lb0rrpspv4szc3c0000gn/T/',
Apple_PubSub_Socket_Render: '/private/tmp/com.apple.launchd.dEPuHtiDsx/Render',
USER: 'frank',
SSH_AUTH_SOCK: '/private/tmp/com.apple.launchd.MRVOOE7lpI/Listeners',
__CF_USER_TEXT_ENCODING: '0x1F5:0x19:0x34',
PATH: '/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Applications/Wireshark.app/Contents/MacOS',
PWD: '/Users/frank/Desktop/corporation/weidian-crm',
XPC_FLAGS: '0x0',
XPC_SERVICE_NAME: '0',
SHLVL: '1',
HOME: '/Users/frank',
LOGNAME: 'frank',
LC_CTYPE: 'zh_CN.UTF-8',
_: '/usr/local/bin/node' }
'process.env.NODE_ENV': JSON.stringify('production')
评价:好。因为仅仅对NODE_ENV值进行修改,不会破坏完整进程,也不会破坏兼容性。
情景:开发阶段的接口地址往往与生产阶段的接口地址是不一致的。例如开发时是development.foo.com,而生产时是production.foo.com,如果需要打包发布,那么需要手动去替换域名或者是一个分支维护一个专门的配置文件,这两种方式是非常笨重的。
webpack的DefinePlugin正是为我们解决这样一个问题,它维护一个全局的配置文件,在编译期间会自动检测process.env.NODE_ENV,根据当前的环境变量去替换我们的接口域名。
下面我将以一个实例来介绍如何正确使用webpack.DefinePlugin。
/config/api.js
const NODE_ENV = process.env.NODE_ENV;
const config = {
production: {
FOO_API: 'production.foo.api.com',
BAR_API: 'production.bar.api.com',
BAZ_API: 'production.baz.api.com',
},
development: {
FOO_API: 'development.foo.api.com',
BAR_API: 'development.bar.api.com',
BAZ_API: 'development.baz.api.com',
},
test: {
FOO_API: 'test.foo.api.com',
BAR_API: 'test.bar.api.com',
BAZ_API: 'test.baz.api.com',
}
}
module.exports = config[NODE_ENV];
webpack.dev.conf.js/webpack.prod.conf.js/webpack.test.conf.js
const apiConfig = require('./config/api');
const webpackConfig = {
plugins: [
new webpack.DefinePlugin({
API_CONFIG: JSON.stringify(apiConfig);
})
]
}
...
custom.component.vue
<template>
...
</template>
<script>
// 这里也可以访问到API_CONFIG
export default {
// 这里无论是data函数,methods对象,computed对象,watch对象,都可以访问到API_CONFIG;
data() {
return {
fooApi: API_CONFIG.FOO_API,
user:{
id: '',
name: '',
},
hash: '',
}
},
computed: {
userAvator() {
return `${API_CONFIG.BAR_API}?id=${user.id}&name=${user.name}`
}
},
methods: {
uploadImage() {
api.uploadImage({user: `${API_CONFIG.BAZ}\${hash}`})
.then(()=>{})
.catch(()=>{})
}
}
}
</script>
上述仅仅适用于vue-cli2.0时代,vue-cli3.0引入了webpack-chain,配置方式大大不同,下文将给出示例。
vue.config.js
const apiConfig = require('./config/api');
module.exports = {
chainWebpack: config => {
config
.plugin('define')
.tap(args => {
args[0].API_CONFIG = JSON.stringify(apiConfig)
return args
})
}
}
需要注意的是,在vue-cli3.0中,我们不能直接SET NODE_ENV=production或者EXPORT NODE_ENV=production。 因为vue-cli-servive有3种模式,serve默认为development,build为production,若想修改vue-cli-service包中的NODE_ENV,需要通过vue-cli-service serve --mode production进行切换。 就像下面这样:
{
"scripts": {
"dev": "vue-cli-service serve", // mode默认为development
"production": "vue-cli-service serve --mode production",
},
}
注意:我们只能在development, production或者test 3个模式下进行切换,不能引入类似preproduction之类的自定义node环境,但是实际上这3个环境已经足以满足大多数的开发情况。
在源码文件base.js中,有下面的代码:
webpackConfig
.plugin('define')
.use(require('webpack/lib/DefinePlugin'), [
resolveClientEnv(options)
])
这一点很关键!我们在vue.config.js中拿到的config.plugin('define'),实际上时vue-service内部创建的webpack.DefinePlugin实例的引用 ! 明确了这一点,我们在以后增强webpack默认插件配置时,需要先到vue-service的源码中寻找一番,看看有没有对应plugin的引用,若有,必须根据vue-service定义的名字直接引用,否则会修改失败。
从手上的项目做起,CopyWebpackPlugin,HtmlWebpackPlugin。
将单个文件或整个目录复制到构建目录。
plugins: [
// copy custom static assets
new CopyWebpackPlugin([
{
from: path.resolve(__dirname, '../static'), // /Users/frank/Desktop/corporation/foo/static
to: config.dev.assetsSubDirectory, // /Users/frank/Desktop/corporation/foo/dist/static
ignore: ['.*'],
},
]),
]
简易创建HTML文件去服务我们的bundle。这对于每次编译都在文件名上带一个hash是很有用的。你也可以用plugin生成一个HTML文件,应用到loadsh template或者使用自己的loader。
plugins: [
new HtmlWebpackPlugin({
filename: 'index.html', // 要将HTML写入的文件,可以指定类似assets/index.html的子目录
template: 'index.html', // webpack需要的模板的路径
inject: true, // true|| 'head'|| 'body' || false 注入所有的assets到给定的template或者templateContent。当值为true或者'body',所有的javascript资源将放在body元素的底部。'head'时将会在head标签上放置一个script标签。
})
]
思考:如何才能掌握这些插件的精髓,以后很灵活地应用到?
加粗的是我自己用过的Plugin。
Name | 痛点 |
---|---|
BabelMinifyWebpackPlugin | 用babel-minify压缩 |
BannerPlugin | 在每一个生成的chunk顶部添加一个banner |
CommonsChunkPlugin | 抽取chunk间的通用module |
CompressionWebpackPlugin | 为带着Content-Encoding报文头的它们准备压缩版本的静态资源 |
ContextReplacementPlugin | 使用require表达式替换推测出的context |
CopyWebpackPlugin | 复制单独文件或者整个目录到构建目录 |
DefinePlugin | 编译期间允许全局配置的常量 |
DllPlugin | 拆分bundle从而彻底地加速build时间 |
EnvironmentPlugin | DefinePlugin的process.env key值的缩写 |
ExtractTextWebpackPlugin | 从bundle抽取text(CSS)到一个分离的文件 |
HotModuleReplacementPlugin | 开启Hot Module Replacement(HMR) |
HtmlWebpackPlugin | 轻松地创建HTM文件到bundle |
I18nWebpackPlugin | 添加i18n支持到我们的bundle |
IgnorePlugin | 从bundle分离出特定的module |
LimitChunkCountPlugin | 设置最小/最大chunking限制去更好地控制chunking |
LoaderOptionsPlugin | 用于从webpack 1迁移到webpack 2 |
MinChunkSizePlugin | 在特定的限制下保持chunk的大小 |
MiniCssExtractPlugin | 为每一个需要CSS的JS文件创建CSS文件 |
NoEmitOnErrorsPlugin | 当有编译错误时跳过发射出来的错误信息提示 |
NormalModuleReplacementPlugin | 根据正则替换资源 |
NpmInstallWebpackPlugin | 开发期间自动安装缺失的dependency |
ProcessPlugin | 报告编译进度 |
ProvidePlugin | 在不使用import/require的情况下使用module |
SourceMapDevToolPlugin | 开启一个更加细粒度的source map |
EvalSourceMapDevToolPlugin | 开启一个更加细粒度的eval source map |
UglifyjsWebpackPlugin | 开启项目中的UglifyJS的版本控制 |
ZopfliWebpackPlugin | 使用node-zopfli准备压缩版本的assets |
思考:为什么会有这么多插件,他们围绕的核心是什么? 核心是编译期间/构建期间,对静态资源,也就是对chunk,或者是bundle,进行细粒度的控制。他们围绕的核心在于html,css和js,assets这些前端资源文件,比如像下图这样。
思考:这些插件到底做了什么事情? chrome有插件,增强了chrome的能力;webstorm有插件,增强了webstorm的能力;vue有插件,增强的是vue的能力;webpack也有插件,增强的也是webpack的能力。因此所谓插件,其实就是为原有的引擎或者工具,按需增加一些特性上去,并且在使用这些插件后,会有一些明显的实质性的变化,无论是从感官还是从客观的角度去看,都会有变化,简单的痛点的描述信息远远不够,我们必须通过“使用前”和“使用后”的角度,加深对插件的印象。
基于vue-cli3.0的vue.config.js进行配置并且进行实验。
// Example ES2015 Code
class Mangler {
constructor(program) {
this.program = program;
}
}
new Mangler(); // without this it would just output nothing since Mangler isn't used
使用前:
// ES2015+ code -> Babel -> BabelMinify/Uglify -> Minified ES5 Code
var a=function a(b){_classCallCheck(this,a),this.program=b};new a;
使用后:
// ES2015+ code -> BabelMinify -> Minified ES2015+ Code
class a{constructor(b){this.program=b}}new a;
vue.config.js
const webpack = require('webpack');
module.exports = {
configureWebpack: {
plugins: [
new webpack.BannerPlugin({
banner: 'hello KevinPearson'
})
]
}
}
app.js,vendors.js等所有chunk都会包含固定格式的banner信息
/*! hello KevinPearson */
/******/ (function(modules) { // webpackBootstrap
/******/ function hotDisposeChunk(chunkId) {
/******/ delete installedChunks[chunkId];
/******/ }
...
当banner传入'hash:[hash], chunkhash:[chunkhash], name:[name], filebase:[filebase], query:[query], file:[file]'
,输出的首行内容为/*! hash:284b5cffb5ce84757f89, chunkhash:bbd6c2181b4c190607bf, name:app, filebase:app.js, query:, file:app.js */
主要用来拆分出common.js,vendors.js和app.js,每次应用的代码更新,通用模块(iview,vue,axios等等)不变时,只会更新app.[hash].js。 这是一个在webpack层面进行前端性能优化的插件。因为通过从bundle分离出通用的module,生成的chunked文件在初始化会被加载,之后可以被存入缓存供后续使用。这样可以使得浏览器加载速度更快,而不是每次都加载非常大的文件。
new webpack.optimize.CommonsChunkPlugin(options);
但是webpack4.0不再支持这个插件,而是由splitChunks代替。SplitPlugin性能更好,并且在修改了配置文件后,不会像CommonsChunkPlugin手动去修改script标签中的文件名,会自动根据一些生成策略生成。 这些策略保证了浏览器加载页面时性能更优。 策略如下(虽枯燥但很重要):
对于以下2种情况,SplitPlugin可以发挥出很大的作用,并行加载,缓存加载。
缓存第三方依赖 index.js
import('./a'); // dynamic import
a.js
```js
import 'react';
...
一个包含react的分离的chunk会被创建,与此同时,导入包时,index.js中的import('./a')也会并行加载。因为react来自node_moduels,react大于30KB,并行请求数是2,不会影响页面初始加载。 减少加载次数
// entry.js
import('./a');
import('./b');
// a.js
import './helpers'; // helpers is 40kb in size
// b.js
import './helpers';
import './more-helpers'; // more-helpers is also 40kb in size
创建要给单独的包含./helpers及其所有依赖的chunk。此chunk与原始chunk并行加载。使用插件后,我们仅仅加载一次这个分离的chunk,不用每次import都加载。 通过官方示例,我仅仅弄明白了如何生成单独的vendors.[chunkhash].js。
module.exports = {
configureWebpack: {
optimization: {
splitChunks: {
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
name: "vendors",
chunks: "all"
}
}
}
}
}
};
但是对于chunks的几种模式'async','initial'以及'all'都不是很理解,因此找来下面的文章Webpack 4 — Mysterious SplitChunks Plugin加以理解。对几种模式做了很详细的讲解。 几种模式的区别:
此处同步与异步的区别是:
import ('vue')
异步加载import 'vue'
同步加载可以通过webpack-bundle-analyzer插件分析优化结果。
webpack.config.js
if (config.build.bundleAnalyzerReport) {
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
webpackConfig.plugins.push(new BundleAnalyzerPlugin());
}
config/index.js
build: {
bundleAnalyzerReport: process.env.npm_config_report,
}
package.json
scripts: {
"build": "npm_config_report=true node build/build.js",
}
npm run build
可以将webpack-bundle-analyzer开启,在http://127.0.0.1:8888/打开一个由canvas渲染出来的分析页面。
treemap size分为以下3种:
这3种treemap类型有什么区别呢?可以参考这个issue:Stat size, parsed size...? Parsed和Gzipped两种模式的区分在下面源码 https://github.com/th0r/webpack-bundle-analyzer/blob/v2.3.1/src/analyzer.js#L80-L83 里,
if (bundlesSources) {
asset.parsedSize = bundlesSources[statAsset.name].length;// minification
asset.gzipSize = gzipSize.sync(bundlesSources[statAsset.name]); // minification + gzip
}
这是一个通过报文头Content-Encoding:gzip进行资源压缩的前端性能优化插件,需要nginx,node等支持。在前端CompressionWebpackPlugin插件配置完毕的基础上,修改nginx的配置,开启node服务,去完成压缩资源的分发。
因为当使用插件将css,js文件打包成css.gz,js.gz的gzip格式后,需要服务端的支持。因为当浏览器通过Accept-Encoding: gzip, deflate;
请求头声明浏览器支持的压缩文件的方式,而服务端通过Content-Encoding:gzip
响应头声明文件的压缩方式。
此处的服务端就可以是nginx这个伪服务端,或者是node服务端,由于对java没有涉猎,因此tomcat服务端就不做深入。
无底洞,学习老哥代码去了。
webpack的插件是一个具名的JavaScript类:
class MyExampleWebpackPlugin {
// 定义apply方法
apply(compiler) {
// 指明event hook
compiler.hooks.compile.tapAsync(
'MyExampleWebpackPlugin',
(compilation, callback) => {
console.log('这个compilation对象代表了一个单独的assets构建', compilation);
// 使用webpack提供的plugin API执行构建
compilation.addModule(/*...*/);
callback();
}
);
}
}
class HelloWorldPlugin {
constructor(options) {
this.options = options;
}
apply(complier) {
compiler.hooks.done.tap('HelloWorldPlugin', ()=>{
console.log('Hello World!');
console.log(this.options);
});
}
}
module.exports = HelloWorldPlugin
在webpack的配置文件的plugins数组中,包含实例就好:
const HelloWorldPlugin = require('hello-world');
module.exports = { plugins: [ new HelloWorldPlugin({setting: true}) // 传入到插件内部的options中 ] };
##### Compiler和Compilation
开发插件过程中最重要的2个对象是compiler和compilation。理解了他们的角色,对于理解webpack引擎来说是非常重要的第一步。
- `compiler`对象代表了整个配置过的
webpack的环境。这个对象在启动webpack时构建一次,并且将所有的options,loaders和plugins这些可操作的设置进行配置。当在webpack环境中应用一个插件时,插件将接收一个这个compiler的引用。这个compiler拥有webpack环境的权限。
- `compilation`对象代表了一个控制版本的静态资源的单独构建。当运行webpack development middleware时,每次文件发生变化都会被检测到,然后一个新的compilation将会创建,从而生成一个编译后的静态资源集合。每次Compilation都会使得当前的模块资源状态,已编译静态文件,修改过的文件,以及监视中的依赖扁平化。compilation同样提供了许多hook,这些hook允许插件执行自定义的操作。
这2个组件在任何webpack插件,都作为一个完整的部分存在。(尤其是`compilation`),所以开发者通过阅读下面的源代码才能增进对`compiler`和`compilation`对象的了解。
- [Compiler源码](https://github.com/webpack/webpack/blob/master/lib/Compiler.js)
- [Compilation源码](https://github.com/webpack/webpack/blob/master/lib/Compilation.js)
### Accessing Compilation
Compiler暴露了很多hook,这些hook为每一个新的compilation都提供了一个引用。轮到Compilation时,提供了一个额外的事件hook,从而接近构建进程的步骤中。
```js
class HelloCompilatinPlugin {
apply(compiler){
// 创建一个cb来接收一个compilation
compiler.hooks.compilation.tap('HelloCompilationPlugin', (compilation) => {
// 创建cbs来执行compilation构建步骤
compilation.hooks.optimize.tap('HelloCompilationPlugin', () => {
console.log('Hello compilation'!);
})
})
}
}
module.exports = HelloCompilationPlugin;
一些插件的hook钩子是异步的。为了挖掘他们,我们可以使用tap
这个同步方法,亦或是tapAsync
和tapPromise
这两个异步方法。
当我们在使用tapAsync
方法去接进插件时,我们需要调用一个回调,它作为我们的函数的最后一个参数传入。
class HelloAsyncPlugin {
apply(compiler) {
compiler.hooks.emit.tapAsync('HelloAsyncPlugin', (compilation, callback) => {
// 做一些异步的事情
setTimeout(function(){
console.log('Done with async work...');
callback();
}, 1000)
});
}
}
module.exports = HelloAsyncPlugin;
在使用tapPromise
去接进插件时,我们需要返回一个promise并且resolve我们的异步任务完成的结果。
class HelloAsyncPlugin {
apply(compiler) {
compiler.hooks.emit.tapPromise('HelloAsyncPlugin', compilation => {
// 返回一个Promise对象
return new Promise((resolve, reject) => {
setTimeout(function() {
console.log('Done with async work...');
resolve();
}, 1000);
})
})
}
}
module.exports = HelloAsyncPlugin;
一旦我们想彻底了解webpack compiler和每个独立的compilation,我们用webpack引擎想干啥就干啥。我们可以reformat已经存在的文件,创建派生文件,或者创建全部的新的assets。
让我们写一个简单的example插件,它会生成一个新的构建文件,叫做filelist.md;在我们的构建中,我们会列举出所有的asset文件的内容。这个插件就像下面这样:
class FileListPlugin {
apply(compiler) {
// emit 是一个异步的hook,使用tapAsync会接入到插件内部,也可以用tapPromise/tap
compiler.hooks.emit.tapAsync('FileListPlugin', (compiler, callback) => {
// 创建一个生成的文件的header string
let filelist = 'In this build:\n\n';
// 循环所有的已经编译的文件
// 为每一个文件都添加一个新行
for (var filename in compilation.assets) {
filelist += '- ' + filename + '\n';
}
// 将这个列表作为一个新的asset文件插入到webpack构建
compilation.assets['filelist.md'] = {
source: function() {
return filelist;
},
size: function() {
return filelist.length;
}
};
callback();
});
}
}
module.exports = FileListPlugin;
插件可以根据接入的事件hook分类。每个事件hook是预先定义好的,有同步型,有异步型,也有瀑布的,平行的hook,hook在内部被call/callAsync方法调用。支持的或者接入的hook列表,通常在this.hooks属性。
例如:
this.hooks = {
shouldEmit: new SyncBailHook(['compilation'])
};
它代表唯一支持的hook是shouldEmit,它是SyncBailHook类型,唯一的可以传入到任何插件shouldEmit hook是compilation。
不同的hook类型有以下几种:
在这些不同类型的hooks中,每个plugin的callback将会在特殊的args参数上调用。如果任何插件返回的值是undefined,这个value会由这个hook返回,而且不会再有任何其它的plugin callback被调用。许多有用的类似optimizeChunks
,optimizeChunkModules
是SyncBailHooks。
这里的每个插件,都是从之前的plugin的返回值的arguments中队列式调用的。插件必须按照顺序去执行。它必须接受来自之前的插件执行后的arguments值。第一个plugin的value是init
。因此至少一个param需要被应用到waterfall hook。这个模式多用于Tapable实例,它与以下的2种webpack模板关联,例如ModuleTemplate
,ChunkTemplate
等等。
AsyncSeriesHook[params]
(err?: Error) -> void
型的callback 函数。handle函数会按照注册的顺序调用。callback
会在所有的handler后调用。这类似于时间的方式,比如emit
,run
.(err?: Error, nextValue: any) -> void
型的callback 函数。当调用nextValue
是当前的下一个handler的当前值。第一个handler的当前值是init
。在所有的handler都被应用后,callback会使用最后一个值调用。如果任何handler返回了err,callback将会终止,不会有更多handler被调用。这个plugin模式是以下的事件期待的类型:before-resolve
和after-resolve
.命令式编程->声明式编程(函数式编程)
使用前的声明式编程:
const path = require('path');
const config = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'my-first-webpack.bundle.js'
},
module: {
rules: [
{
test: /\.(js)$/,
use: 'babel-loader'
}
]
},
};
module.exports = config;
使用后的声明式编程(函数式编程):
const Config = require('webpack-chain');
const config = new Config();
config.
.entry('index')
.add('src/index.js')
.end()
.output
.path('dist')
filename('my-first-webpack.bundle.js');
config.module
.rule('compile')
.test(/\.js$/)
.use('babel')
.loader('babel-loader')
module.exports = config;
通过对比我们发现webpack-chain有以下优点:
吐槽:webpack-chain的引入进一步提高了webpack的学习门槛,这对于努力的英语好的前端工程师来说是件好事。对于英语不好,从不上Github的前端工程师来说是一场噩梦,虽然他们还没有发觉。
vue-cli3.0和webpack4.0时代,webpack-chain插件是必会插件,因为在vue.config.js中,configureWebpack和chainWebpack中的cb中注入的config对象,其实都是一个webpack-chain实例,如果想对webpack的一些插件的配置做修改,那么就必须先理解webpack-chain。
webpack.core.js
const Config = require('webpack-chain');
const config = new Config();
// 入口出口文件配置
config.
.entry('index')
.add('src/index.js')
.end()
.output
.path('dist')
filename('[name].bundle.js');
// 创建之后可以修改的命名规则
config.module
.rule('lint')
.test(/\.js$/)
.pre()
.include
.add('src')
end()
.use('eslint')
.loader('eslint-loader')
options({
rules: {
semi: 'off'
}
});
config.module
.rule('compile')
.test(/\.js$/)
.include
.add('src')
.add('test')
end()
.use('babel')
.loader('babel-loader')
.options([
presets: [
['@babel/preset-env', {modules: false }]
]
]);
config
.plugin('clean')
.use(cleanPlugin, [['dist'], { root: '/dir' }]);
module.exports = config;
webpack.dev.js
const config = require('./webpack.core');
// ...
module.exports = config.toConfig();
webpack.prod.js
const config = require('./webpack.core');
// ...
module.exports = config.toConfig();
ChainedMap的有些key,可以直接作为方法调用,这些缩写方法也同样会返回原始实例,方便后续的链式调用。
devServer.hot(true);
devServer.set('hot', true);
.end()
通过.end()
可以返回到更高层级的上下文,但是仅向上一个层级,并且返回一个mutate后的实例。或者是直接通过config
是获取的顶级上下文。.entry()
config.entryPoints.get()的缩写 。可以通过config.entry(name).add(value)
的.entry()缩写方法配置
,也可以通过config.entryPoints.get(name).add(value)
配置。.add()
这是一个ChainedSet方法,它可以将值添加在Set的尾部。output
这是一个ChainedMap对象,有很多方法,例如path(),filename(),publicPath()等常用的方法。module
也是一个ChinedMap,主要方法为rules(),配置loader的规则,config.module.rule(name).use(name).loader(loader).options(options),或者config.module.rule(name).use(name).tap(options => newOptions)plugin
也是ChinedMap,主要是对plugin配置,config.plugin(name).use(WebpackPlugin, args)。重点对plugin做深入学习。config
.plugin(name)
.use(WebpackPlugin, args)
// 直接引入
config
.plugin('hot')
.use(webpack.HotModuleReplacementPlugin);
// 可以通过requrire('')的方式引入插件。
config
.plugin('env')
.use(require.resolve('webpack/lib/EnvironmentPlugin'), [{ 'VAR': false }]);
config
.plugin(name)
.tap(args => newArgs)
// 为arguments新增一个'SECRET_KEY'
config
.plugin('env')
.tap(args => [...args, 'SECRET_KEY'])
config
.plugin(name)
.init((Plugin, args) => new Plugin(...args));
config.plugins.delete(name)
不能在同一个插件上既使用before又使用after。
config
.plugin(name)
.before(otherName)
// 例子
config
.plugin('html-template')
.use(HtmlWebpackTemplate)
.end()
.plugin('script-ext')
.use(ScriptExtWebpackPlugin)
before('html-template')
不能在同一个插件上既使用before又使用after。
config
.plugin(name)
.after(otherName)
// 例子
config
.plugin('html-template')
.use(HtmlWebpackTemplate)
.after('script-ext')
.end()
.plugin('script-ext')
.use(ScriptExtWebpackPlugin)
参阅comment3的最后一部分内容。
config.toString();
内容包括: