FrankKai / FrankKai.github.io

FE blog
https://frankkai.github.io/
363 stars 39 forks source link

Webpack Plugin那些事儿 #94

Closed FrankKai closed 4 years ago

FrankKai commented 5 years ago

内容包括:

FrankKai commented 5 years ago

1.几种常见的插件类型

DefinePlugin(compile time plugins)

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.[plugin-name]

webpack built-in plugins

module.exports = {
    plugins: [
        new webpack.HotModuleReplacementPlugin(),
    ]
}
community plugin

webpack enhancement plugins

import DashboardPlugin = require('webpack-dashboard/plugin')
module.exports = {
    plugins: [
        new DashboardPlugin(),
    ]
}
FrankKai commented 5 years ago

2.DefinePlugin的正确使用方法

DefinePlugin中的每个键,是一个标识符或者通过.作为多个标识符。

这些值将内联到代码中,压缩减少冗余。

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"},

在代码中,我们一般会有以下几种用途:

Feature Flag

可以控制新特性和实验特性的开关。

new webpack.DefinePlugin({
    'NICE_FEATURE': JSON.stringify(true),
    'EXPERIMENTAL': JSON.stringify(false),
})
process.env.NODE_ENV的正确配置方式是什么?
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值进行修改,不会破坏完整进程,也不会破坏兼容性。

FrankKai commented 5 years ago

3.如何使用DefinePlugin添加配置文件,构建期间自动检测环境变化,也就是如何根据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中,使用使用DefinePlugin添加配置文件,构建期间自动检测环境变化,也就是如何根据NODE_ENV引入配置文件?

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个环境已经足以满足大多数的开发情况。

为什么vue-cli 3.0中的DefinePlugin可以用config.plugin('define')修改入参?

源码文件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定义的名字直接引用,否则会修改失败。

FrankKai commented 5 years ago

4.常用的webpack插件

从手上的项目做起,CopyWebpackPlugin,HtmlWebpackPlugin。

CopyWebpackPlugin

将单个文件或整个目录复制到构建目录。

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: ['.*'],
      },
    ]),
]
HtmlWebpackPlugin

简易创建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标签。
    })
]

思考:如何才能掌握这些插件的精髓,以后很灵活地应用到?

webpack官方插件痛点一览表

加粗的是我自己用过的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这些前端资源文件,比如像下图这样。 image

思考:这些插件到底做了什么事情? chrome有插件,增强了chrome的能力;webstorm有插件,增强了webstorm的能力;vue有插件,增强的是vue的能力;webpack也有插件,增强的也是webpack的能力。因此所谓插件,其实就是为原有的引擎或者工具,按需增加一些特性上去,并且在使用这些插件后,会有一些明显的实质性的变化,无论是从感官还是从客观的角度去看,都会有变化,简单的痛点的描述信息远远不够,我们必须通过“使用前”和“使用后”的角度,加深对插件的印象。

插件"使用前""使用后"对比

基于vue-cli3.0的vue.config.js进行配置并且进行实验。

BabelMinifyWebpackPlugin(基于babel-minify)
// 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;
BannerPlugin

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 */

CommonsChunkPlugin(SplitChunkPlugin)

主要用来拆分出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加以理解。对几种模式做了很详细的讲解。 几种模式的区别:

此处同步与异步的区别是:

可以通过webpack-bundle-analyzer插件分析优化结果。

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渲染出来的分析页面。

image

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
    }

CompressionWebpackPlugin

这是一个通过报文头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服务端就不做深入。

CompressionWebpackPlugin配置

无底洞,学习老哥代码去了。

HashedModuleIdsPlugin插件

FrankKai commented 5 years ago

5.从零写一个Webpack插件

创建一个Plugin

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();
            }
        );
    }
}
基本的插件架构

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

一些插件的hook钩子是异步的。为了挖掘他们,我们可以使用tap这个同步方法,亦或是tapAsynctapPromise这两个异步方法。

tapAsync

当我们在使用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

在使用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;

不同的插件shape

插件可以根据接入的事件hook分类。每个事件hook是预先定义好的,有同步型,有异步型,也有瀑布的,平行的hook,hook在内部被call/callAsync方法调用。支持的或者接入的hook列表,通常在this.hooks属性。

例如:

this.hooks = {
    shouldEmit: new SyncBailHook(['compilation'])
};

它代表唯一支持的hook是shouldEmit,它是SyncBailHook类型,唯一的可以传入到任何插件shouldEmit hook是compilation。

不同的hook类型有以下几种:

Synchronous Hooks

在这些不同类型的hooks中,每个plugin的callback将会在特殊的args参数上调用。如果任何插件返回的值是undefined,这个value会由这个hook返回,而且不会再有任何其它的plugin callback被调用。许多有用的类似optimizeChunksoptimizeChunkModules是SyncBailHooks。

这里的每个插件,都是从之前的plugin的返回值的arguments中队列式调用的。插件必须按照顺序去执行。它必须接受来自之前的插件执行后的arguments值。第一个plugin的value是init。因此至少一个param需要被应用到waterfall hook。这个模式多用于Tapable实例,它与以下的2种webpack模板关联,例如ModuleTemplate,ChunkTemplate等等。

Asynchronous Hooks

FrankKai commented 5 years ago

6. 新时代的宠儿webpack-chain插件

命令式编程->声明式编程(函数式编程)

使用前的声明式编程:

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-chain插件?

webpack-plugin使用示例

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();

一些常用的webpack-chain 缩写方法

ChainedMap的有些key,可以直接作为方法调用,这些缩写方法也同样会返回原始实例,方便后续的链式调用。

devServer.hot(true);
devServer.set('hot', true);

引入webpack-chain后如何配置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)
某个插件前调用插件odering before

不能在同一个插件上既使用before又使用after。

config
    .plugin(name)
      .before(otherName)
// 例子
config
    .plugin('html-template')
        .use(HtmlWebpackTemplate)
        .end()
    .plugin('script-ext')
        .use(ScriptExtWebpackPlugin)
        before('html-template')
某个插件后调用插件ordering after

不能在同一个插件上既使用before又使用after。

config
    .plugin(name)
      .after(otherName)
// 例子
config
    .plugin('html-template')
        .use(HtmlWebpackTemplate)
       .after('script-ext')
        .end()
    .plugin('script-ext')
        .use(ScriptExtWebpackPlugin)

vue-cli3.0引入webpack-chain后,如何配置最常用的DefinePlugin呢?

参阅comment3的最后一部分内容。

如何检查配置是否正确呢?

config.toString();