BLOOK

GP's blog and book

webpack-chain源码 vue-cli配置webpack

获取源码

https://github.com/neutrinojs/webpack-chain

1
git clone https://github.com/neutrinojs/webpack-chain.git

知识准备

需要先预习 下列方法

  • ES6 Set 结构
  • ES6 Map 结构
  • ES6 Class 类继承
  • 箭头函数
  • 数组的一些方法: includes reduce map forEach
  • 对象的一些方法: Object.keys Object.assign
  • 展开运算符 ...

如果对这些知识生疏,点此学习 (http://es6.ruanyifeng.com/#docs/class)
把这些掌握后,阅读很easy

目录结构

目标是把src 里面的方法都了解会用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
└── webpack-chain
├── src
│   ├── Chainable.js
│   ├── ChainedMap.js
│   ├── ChainedSet.js
│   ├── Config.js
│   ├── DevServer.js
│   ├── Module.js
│   ├── Optimization.js
│   ├── Orderable.js
│   ├── Output.js
│   ├── Performance.js
│   ├── Plugin.js
│   ├── Resolve.js
│   ├── ResolveLoader.js
│   ├── Rule.js
│   └── Use.js

文件名全是首字母大写,从命名看,每个里面全是类的定义,用extends来实现继承

Chainable ChainedMap.js ChainedSet.js

ChainedMap.js ChainedSet.js 都继承了 Chainable

Chainable 以able 为后缀表示具有什么能力。这个库维护了 parent ,用end来实现链式。很像jq的end().

ChainedMap.js ChainedSet.js 这两个里面分别包装了 js 原生的 Map, Set 结构。

ChainedMap
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
// 维护了parent 链,由上文提到的`Chainable`来做的
// 实例化了一个store (仓库) 拥有Map结构的方法
constructor(parent) {
super(parent);
this.store = new Map();
}
// 从 Map 移除所有 配置.
clear()
// 通过键值从 Map 移除单个配置.
// key: *
delete(key)
// 获取 Map 中相应键的值
// key: *
// returns: value
get(key)
// 都是map的原生方法封装
has(key) + set(key, value)
// 传入key 和 函数两个参数
当拥有key的时候 返回获取到的值
如果key 没有定义过,调用函数生成键值对
getOrCompute(key, fn) {
if (!this.has(key)) {
this.set(key, fn());
}
return this.get(key);
}
把对象进行清洗,传参数{a:undefined,
b: [],
c:{},
d:0
}
输出{ d: 0 },会把undefined,[],{}这些过滤掉。
clean(obj)
// when的用法
// when 有断言作用,第2,3参数是函数。函数参数就是this。
// config
// .when(process.env.NODE_ENV === 'production',
// config => config.plugin('minify').use(BabiliWebpackPlugin),
// config => config.devtool('source-map')
// );
when(
condition,
whenTruthy = Function.prototype,
whenFalsy = Function.prototype
) {
if (condition) {
whenTruthy(this);
} else {
whenFalsy(this);
}
return this;
}
extend 参数是数组,会批量往this上绑定一些方法,
绑定方法是用的set,说明再次调用会覆盖掉上次。
例如: this.extend(['watch'])
会生成 this.watch = value => this.set(method, value);
理解了ChainedMap,那ChainedSet也容易

实战配置vue-cli 项目

用vue-cli3生成项目后

1
vue inspect > default.json

会生成默认配置到default.json

这些配置是从这个包node_modules/@vue/cli-service/lib/config/base.js 生成的

打开这个文件看一下webpack-chain的用法.

vue引入webpack-chain的文件是node_modules/@vue/cli-service/lib/Service.js

在此文章搜索下面三行

1
2
3
4
5
6
7
const Config = require('webpack-chain') // 会引入webpack-chain库"src/Config.js"文件
// 只实例化一次,chainWebpack 的config参数就是这个实例
const chainableConfig = new Config()
// 会生成配置
let config = chainableConfig.toConfig()

我们看一下webpack-chain 源码 Config.js 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
toConfig() {
// 入口entry
const entryPoints = this.entryPoints.entries() || {};
// clean 方法上文Map结构有讲
return this.clean(
// this.entries() 全部this.store的值。包含this.extend()方法生成的速记方法
Object.assign(this.entries() || {}, {
node: this.node.entries(),
output: this.output.entries(),
resolve: this.resolve.toConfig(),
resolveLoader: this.resolveLoader.toConfig(),
devServer: this.devServer.toConfig(),
module: this.module.toConfig(),
optimization: this.optimization.toConfig(),
plugins: this.plugins.values().map(plugin => plugin.toConfig()),
performance: this.performance.entries(),
entry: Object.keys(entryPoints).reduce(
(acc, key) =>
Object.assign(acc, { [key]: entryPoints[key].values() }),
{}
),
})
);
}

修改 entry
1
2
3
4
5
6
7
8
9
10
11
12
chainWebpack: config => {
config.entryPoints.clear() // 会把默认的入口清空
config.entry('main').add('./src/main.js')
config.entry('routes').add('./src/app-routes.js')
}
链式调用:end方法
clear方法会把vue-cli默认的.entry('app')清空。可以在同一个chunk,add多个模块。
config.entryPoints.clear().end()
.entry('main').add('./src/main.js').end()
.entry('routes').add('./src/app-routes.js')

vue inspect > entry.json 对比entry.json和default.json的entry字段,成功修改。

  • 速记方法 看上文Map extend 介绍
    用速记方法修改简单的字段
1
2
3
4
5
6
config.mode('production')
config.watch(true)
生成文件,查看已变成
mode: 'production',
watch: true,
用速记方法修改proxy
1
2
3
4
5
6
7
8
9
10
11
12
chainWebpack: config => {
config.devServer.port(9898)
.open(true)
.proxy({'/dev': {
target: 'http://123.57.153.106:8080/',
changeOrigin: true,
pathRewrite: {
'^/dev': ''
}
}
})
}
修改别名 resolve.alias
1
2
3
4
5
6
7
chainWebpack: config => {
config.resolve.alias
.set('assets', '@/assets')
.set('fetch', '@/config/http.config')
.delete('fetch') // 删掉指定的
// .clear() 会把所有别名都删掉
}
添加7牛plugin
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
const QiniuPlugin = require('qn-webpack')
chainWebpack: config => {
config.plugin('7niu')
.use(QiniuPlugin,[{
accessKey: '1234567654356',
secretKey: '2344344545',
bucket: 'busi-cdn',
path: 'cdn-finance/dist/',
exclude: /index\.html$/ // 排除特定文件,正则表达式
}]);
}
通过阅读Plugin.js源码toConfig方法,发现还可以传字符串类型,还有对象实例
const init = this.get('init'); // 会调用init(plugin, args); 把插件函数和参数传入
let plugin = this.get('plugin'); // 获取plugin
const args = this.get('args');// 获取参数
config
.plugin('7niu')
.use('qn-webpack',[{
accessKey: '1234567654356',
secretKey: '2344344545',
bucket: 'busi-cdn',
path: 'cdn-finance/dist/',
exclude: /index\.html$/ // 排除特定文件,正则表达式
}]);
生成的效果
/* config.plugin('7niu') */
new (require('qn-webpack'))(
{
accessKey: '1234567654356',
secretKey: '2344344545',
bucket: 'busi-cdn',
path: 'cdn-finance/dist/',
exclude: /index\.html$/
}
)
添加clear plugin

用对象实例的方式

1
2
3
4
5
6
7
const CleanPlugin = require("clean-webpack-plugin");
const clean = new CleanPlugin()
config
.plugin('clean').use(clean)

删除loader

把vue默认添加的loader 都删掉.

用use添加的在uses,用oneOf添加的在oneOfs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
config.module
.rule('vue').uses.clear()
config.module
.rule('scss').oneOfs.clear()
输出后效果:
/* config.module.rule('scss') */
{
test: /\.scss$/
},
/* config.module.rule('vue') */
{
test: /\.vue$/
},

添加loader

@vue/cli-service/lib/config/base.js 的一段代码来参考

1
2
3
4
5
6
7
8
9
10
11
12
13
14
webpackConfig.module
.rule('vue') // 会得到Rule实例 new Rule()
.test(/\.vue$/) // 速记方法调用的set,每次test()会覆盖掉之前的
.use('cache-loader') // new Use()实例参数是this.name。 loader和options是shorthands 调用set赋值
.loader('cache-loader')
.options(vueLoaderCacheConfig)
.end()
.use('vue-loader')
.loader('vue-loader')
.options(Object.assign({
compilerOptions: {
preserveWhitespace: false
}
}, vueLoaderCacheConfig))

Rule源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
const Rule = Orderable(
class extends ChainedMap {
constructor(parent, name) {
super(parent);
this.name = name;
this.names = [];
this.uses = new ChainedMap(this); // 所有的use
this.include = new ChainedSet(this); // 包含
this.exclude = new ChainedSet(this); // 排除
this.oneOfs = new ChainedMap(this); // 所有的oneOf
// 速记方法,使用Map的Set 方法再次调用会覆盖掉上次
this.extend([
'enforce',
'parser',
'resource',
'resourceQuery',
'sideEffects',
'test',
'type',
]);
}
use(name) {
// Use 是 ChainedMap,有就get返回,木有就调用会在this.uses注册一个
return this.uses.getOrCompute(name, () => new Use(this, name));
}
oneOf(name) {
// Rule 是 ChainedMap,有就get返回,木有就调用会在this.oneOfs注册一个
return this.oneOfs.getOrCompute(name, () => new Rule(this, name));
}

单独调试

1
2
3
4
5
6
7
8
9
10
11
const Config = require('webpack-chain');
const config = new Config();
const CleanPlugin = require("clean-webpack-plugin");
config
.plugin('clean')
.use(CleanPlugin);
let r = config.toConfig()
console.log(r)