使用 webpack-chain 来创建 webpack 配置

10,360 阅读4分钟

前言

平时用的都是 vue-cli脚手架,vue-cli4 对于 webpack 的修改采用链式的方式

Vue CLI 内部的 webpack 配置是通过 webpack-chain 维护的。这个库提供了一个 webpack 原始配置的上层抽象,使其可以定义具名的 loader 规则和具名插件,并有机会在后期进入这些规则并对它们的选项进行修改。

如果我们有其他项目(例如:react框架)不使用 vue-cli 自己该如何通过 webpack-chain 来管理自己的webpack配置呢?

常用的 webpack.config 基本结构

常用的 webpack.config 结构主要有 entry、output、resolve、module、plugin、optimization 这几项配置。我们来看看在 webpack-chain 中这些都是怎么配置的。

module.exports = {
  entry: {
    main: resolve('../src/main.js')
  },
  output: {
    path: resolve('../dist'),
    filename: 'bundle.[hash:6].js'
  },
  resolve: {
    extensions: ['.ts', '.tsx', '.js', '.vue', '.json'],
    alias: {
      '@': path.resolve(__dirname, '../src')
    }
  },
  module: {
    rules: [
       {
        test: /\.tsx?$/,
        loader: 'ts-loader',
        options: {
          appendTsSuffixTo: [/\.vue$/]
        }
      },
    ]
  }
  plugins: [new VueLoaderPlugin()],
  optimization: {
    runtimeChunk: true
  }
}

如何使用 webpack-chain

创建配置实例

这里的实例对象 config 可以理解为上面 module.exports = { // webpack.config }

// 导入 webpack-chain 模块,该模块导出了一个用于创建一个webpack配置API的单一构造函数。
const Config = require('webpack-chain');

// 对该单一构造函数创建一个新的配置实例
const config = new Config();

查看 webpack-chain 源码: src/Config.js 主文件中暴露的构造函数结构如下: 可以看到这个构造函数中有很多属性和原型上的方法,接下来我们的配置都需要通过 config 实例上的这些方法和属性来实现。

config.entry 和 config.output

源码的入口文件和输出 bundles 的路径设置。
这里需要注意的是需要传递绝对路径,防止命令行中执行命令时所在路径影响。这里可以使用 path.resolve(__dirname,src/index.js) 或者 require.resolve('src/index.js') 两者效果相同。

const Config = require('webpack-chain')
const path = require('path')
const resolve = file => path.resolve(__dirname, file);
const config = new Config()
// 修改 entry 配置
config.entry('index')
      .add(resolve('src/index.js'))
      .end()
      // 修改 output 配置
      .output
        .path(resolve('out'))
        .filename('[name].bundle.js');

来看下 webpack-chain 链式的语法。

  • .entry(index) 这里设置的是 file 的 name 也就是和 .filename('[name].bunle.js') 中的name对应
  • .add() 添加入口文件的路径
  • .output.path() 添加出口文件的路径
  • .filename() 设置 bundle 文件名

为啥这些方法可以一直链式操作,继续来看下 webpack-chain 的源码

当我执行 config.entry('index') 传入 bundle 文件名时候,命中下面的方法

  // Config.js 
  constructor(){
  	 this.entryPoints = new ChainedMap(this);
  }
  entry(name) {
    return this.entryPoints.getOrCompute(name, () => new ChainedSet(this));
  }

entry方法首次执行时会走 this.set(key,fn())
src/chainedMap.js 中主要逻辑是创建一个 store ,原型上有 clear、order、set、getOrCompute 等方法对这个 store Map对象做一些增删改查的操作。
当执行完 set 后 **return this ** 将 ChainedMap 实例再次返回,有了 ChainedMap 实例对象,我们可以去操作它本身拥有的方法。

// chainedMap.js
module.exports = class extends Chainable {
  constructor(parent) {
    super(parent);
    this.store = new Map();
  }
  getOrCompute(key, fn) {
    if (!this.has(key)) {
      this.set(key, fn());
    }
    return this.get(key);
  }
  set(key, value) {
    this.store.set(key, value);
    return this;
  }
}

但是 chainedMap.js 本身并没有定义 output 这个属性的,它是如果可以链式操作 .output 的呢?

通过 super(parent) 将 Config.js **this.entryPoints = new ChainedMap(this);**的 Config.js 实例传递给 Chainable.js

// src/ChainedMap.js 
const Chainable = require('./Chainable');
module.exports = class extends Chainable {
	constructor(parent){
    	super(parent)
    }
}

来查看 Chainable.js ,通过 .end() 将 Config.js 实例返回。
最终得出结论:如果你想操作 Config.js 实例上的方法,你需要通过 .end() 来返回实例本身。

// src/Chainable.js 
module.exports = class {
  constructor(parent) {
    this.parent = parent;
  }

  batch(handler) {
    handler(this);
    return this;
  }

  end() {
    return this.parent;
  }
};

添加 loader

以 css loaders 为例。loader 是从右到左执行的,也就是先进后后出的方式。
这里先执行 css-loader 来将匹配到的 css 文件进行处理,然后交给 extrat-css-loader 抽离出单独的 css 文件。

config.module
    .rule('css')
      .test(/\.(le|c)ss$/)
        .use('extract-css-loader')
          .loader(require('mini-css-extract-plugin').loader)
          .options({
            publicPath: './'
          })
          .end()
        .use('css-loader')
          .loader('css-loader')
          .options({});

来看下这里的语法:

  • .rule() 给后续的 loaders 一个具名的title类似,叫什么无所谓(最好见名知意)
  • .test() 接受一个正则规则
  • .use() 具名 loader 在使用 .loader 前必须添加否则会报错
  • .loader() 接受loader字符串,或者loader的绝对路径
  • .end() 前面分析源码说到,这里是为了拿到 Config 实例对象
  • .options() loader 额外配置

需要注意的是:

  • 多个loader公用一个 .rule() 一个.test(),也就是说你的 test 规则相同,你可以写在一起,但是每个 loader 直接要通过 .end() 来重新获取 Config 实例对象,因为 .loader() 或者 .options() 操作后拿到的已经不是 Config 实例对象了。
  • 如果你的 test 规则不同,你可以重新 config.module.rule().test() 再起一个就行

添加 plugin

const MiniCssExtractPlugin = require('mini-css-extract-plugin');
config.plugin('MiniCssExtractPlugin').use(MiniCssExtractPlugin)
  • config.plugin(name) 这里的 name 是webpack-chain里的key,也是随便取的。
  • 如果你前面注册过这个key值,那么后面就可以通过 config.plugin(name) 拿到这个 plugin,进行其他配置操作
const htmlPlugin = require('html-webpack-plugin')
// 注册 plugin 名为 html
config.plugin('html').use(htmlPlugin,[{}])
// 通过 html 拿到前面注册的插件,通过 tap 来操作 options 
config.plugin('html').tap(args => {
  args[0].title = '这里是标题'
  args[0].template = resolve('public/index.html')
  return args
})
  • .use(WebpackPlugin,args) 添加配置以及 [options1,options2] 配置数组
  • .tap(function(args):args{}) 回调函数接受 args 为参数,也就是 .use() 传递的第二个参数,你可以对这个 args 数组做一些修改,最终需要将它返回。

config.resolve

config.resolve.extensions.merge(['.ts','.js', '.jsx', '.vue', '.json'])
config.resolve.alias
      .set('SRC', resolve('src'))
      .set('ASSET', resolve('src/assets'))

config.optimization

config.optimization
	.runtimeChunk(true)

config.toConfig()

导出配置 和 在传递给webpack之前调用 .toConfig() 方法将配置导出给webpack使用。

const chalk = require('chalk')
const config = require('./base')
const webpack = require('webpack')
webpack(config.toConfig(),(err,stats) => {
  if (stats.hasErrors()) {
    console.log(chalk.red('构建失败\n'))
    // 返回描述编译信息 ,查看错误信息
    console.log(stats.toString())
    process.exit(1)
  }

  console.log(chalk.cyan('build完成\n'))
})

常用的配置

css loaders

config.module
    .rule('css')
      .test(/\.(le|c|postc)ss$/)
      	// 提取 css 到单独文件
        .use('extract-css-loader')
          .loader(require('mini-css-extract-plugin').loader)
          .options({
            publicPath: './'
          })
          .end()
        .use('css-loader')
          .loader('css-loader')
          .options({})
          .end()
         // 可以引入多个插件
         // autoprefixer 自动添加兼容 css 前缀
         // stylelint css 规范化
        .use('postcss-loader')
          .loader('postcss-loader')
          .options({
            plugins: function() {
              return [
                require('autoprefixer')({
                  overrideBrowserslist: ['>0.25%', 'not dead']
                }),
                require("stylelint")({
                  /* your options */
                }),
                require("postcss-reporter")({ clearReportedMessages: true })
              ]
            }
          })
          .end()
        .use('less-loader')
          .loader('less-loader')
          .end()

html-webpack-plugin

const htmlPlugin = require('html-webpack-plugin')
config.plugin('html').use(htmlPlugin,[
	{ title:'标题党',template:require.resolve('public/index.html') }
])

babel 解析 .ts 文件

通过 babel-loader + @babel/preset-typescript 配置后可以解析 ts文件

// webpack.config
config.module
      .rule('babel-loader')
        .test(/\.tsx?$/)
          .use('babel-loader')
            .loader('babel-loader')
            .options({
              presets: ['@babel/preset-env']
            })
            .end()
            
// .babelrc
{
  "presets": ["@babel/preset-typescript"]
}

ts-loader + tsconfig.json

如果使用 ts-loader 默认会读取 tsconfig.json 配置来解析 ts文件

// webpack.config
config.module
      .rule('ts-loader')
        .test(/\.tsx?$/)
          .use('babel-loader')
            .loader('babel-loader')
            .options({
              presets: ['@babel/preset-env']
            })
            .end()
          .use('ts-loader')
            .loader('ts-loader')
            .options({
              appendTsSuffixTo: [/\.vue$/]
            })
            .end()
// tsconfig.json
{
  "compilerOptions": {
    // Target latest version of ECMAScript.
    "target": "esnext",
    // Search under node_modules for non-relative imports.
    "moduleResolution": "node",
    // Process & infer types from .js files.
    "allowJs": true,
    // 不生成输出文件
    // "noEmit": true,
    // Enable strictest settings like strictNullChecks & noImplicitAny.
    "strict": true,
    // Disallow features that require cross-file information for emit.
    "isolatedModules": false,
    // Import non-ES modules as default imports.
    "esModuleInterop": true
  },
  "include": [
    "test"
  ]
}

tsconfig.json 和 .babelrc 的关系

  • 两者都可以设置将 ts 转化成 js
  • tsconfig.json 是针对 ts 项目,对于所有 ts 文件设定的规则如果使用 ts-loader 就需要声明该文件。 ts-loader 转化后的结果交给 babel-loader

如果仅仅使用了 ts-loader 可以看编译后的结果需要 polyfill 的箭头函数被直接放到打包后的 bundle 中 添加了 babel-loader 后将起转化成匿名函数

参考

webpack-chain
workflow
@babel/preset-typescript