使用 webpack-chain 来创建 webpack 配置
前言
平时用的都是 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 后将起转化成匿名函数
参考
转载自:https://juejin.cn/post/6866756948704886791