webpack4.x从基础到实战

DellLee 老师的 webpack 学习与记录

知识点:

Loader HMR Create React App Caching Plugin SourceMap Vue Cli 3.0 Shimming WebpackDevServer TreeShaking CodeSplitting Babel React Library Eslint PWA Vue Mode 性能优化 多页应用 原理 PreLoading PreFetching 环境变量 TypeScript

Webpack 是什么

本质上,webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler)。当 webpack 处理应用程序时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 bundle

扩展阅读-Modules

webapack 打包浅析

1
2
3
4
5
6
7
8
9
10
11
const path = require('path');

module.exports = {
// production 模式下代码压缩,development 代码不压缩
mode: 'production',
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
}
}

Loader

module 处理方案,loader 有先后顺序,从下到上,从右到左

file-loader

处理图片

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
module.exports = {
// 开发还是生产环境
mode: 'production',
// 进口
entry: './src/index.js',
// 模块处理
module: {
// 规则
rules: [{
// 匹配
test: /\.jpg$/,
// loader 运用
use: {
loader: 'file-loader',
// 占位符
options: {
// 定义文件的名字:原来的名字_哈希值.原来的后缀
name: '[name]_[hash].[ext]',
// 图片输出路径
outputPath:'./images/'
}
}
}]
},
// 输出
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
}
}
// 或者是用方法的方式
module.exports = {
// 开发还是生产环境
mode: 'production',
// 进口
entry: './src/index.js',
// 模块处理
module: {
// 规则
rules: [{
// 匹配
test: /\.jpg$/,
// loader 运用
use: {
loader: 'file-loader',
// 占位符
options: {
// 定义文件的名字:原来的名字_哈希值.原来的后缀
name(file){
if(process.env.NODE_ENV === 'development'){
return '[path][name].[ext]',
}
return '[hash][ext]'
},
// 图片输出路径
outputPath:'./images/'
}
}
}]
},
// 输出
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
}
}

url-loader

会把图片打包到js 文件中,如果图片很小(1-2kb)的话就适合以base64的形式打包到js里面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
module.exports = {
mode: 'development',
entry: './src/index.js',
module: {
rules: [{
test: /\.(jpg|png|gif)$/,
use: {
loader: 'url-loader',
options: {
name: '[name]_[hash].[ext]',
outputPath: 'images/',
// 限制图片的大小(Kb),小于的会被打包到js里面,大于则打包到指定目录下
limit: 2048
}
}
}]
},
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
}
}

也可以用来处理字体文件

1
2
3
4
5
6
7
8
module: {
rules: [{
test:/\.(eot|ttf|svg|woff)$/,
use:{
loader:'file-loader'
}
}]
},

style-loadercss-loader

处理css 样式

css-loader 分析有几个css文件以及它们之间的关系是怎么样的然后合并为一个css

style-loader 会把 css-loader 合并的内容挂载到页面上

node-sass、sass-loader:应对 sass 文件,上面的组合处理sass 文件不会报错,但是不会转义sass 特殊的语法

1
2
3
4
5
6
module: {
rules: [ {
test: /\.scss$/,
use: ['style-loader', 'css-loader', 'sass-loader']
}]
},

postcss-loaderautoprefixer

自动添加css前缀

在文件的根目录配置 postcss.config.js

1
2
3
4
5
module.exports = {
plugins:[
require('autoprefixer'),
]
}

webapck.config.js 更改

1
2
3
4
5
6
module: {
rules: [ {
test: /\.scss$/,
use: ['style-loader', 'css-loader', 'sass-loader','postcss-loader']
}]
},

如果有样式引用其他样式的情况要重新定义 css-loader 使得被引入的 scss 等有语义的css 文件可以重新跑多一次全部的 loader

1
2
3
4
5
6
7
8
9
10
11
12
module: {
rules: [ {
test: /\.scss$/,
// 当 loader 有参数配置的时候使用对象
use: ['style-loader', {
loader:'css-loader',
options:{
importLoaders:2
}
}, 'sass-loader','postcss-loader']
}]
},

开启模块化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
module: {
rules: [ {
test: /\.scss$/,
// 当 loader 有参数配置的时候使用对象
use: ['style-loader', {
loader:'css-loader',
options:{
importLoaders:2,
// css 模块化
modules:true
}
}, 'sass-loader','postcss-loader']
}]
},

其他具体的loader

Plugins

可以在webpack 运行打包到某个时刻帮你实现一些事情

html-webpack-plugin

会在打包结束后自动生成一个 html 文件,并把打包生成的 js 自动引入到这个 html 文件中

1
2
3
4
5
6
module.exports = {
plugins: [new HtmlWebpackPlugin({
// 设置默认的模板
template: 'src/index.html'
})]
}

clean-webpack-plugin

打包之前去掉旧的dist 项目

1
2
3
4
plugins: [
new HtmlWebpackPlugin({ template: 'src/index.html' }),
new CleanWebpackPlugin()
],

clean-webpack-plugin2.0 的新参数

Output、Entry

多文件输出预设的名字

1
2
3
4
5
6
7
8
9
10
11
12
module.export = {
entry:{
main:'./src/index.js',
sub:'./src/index.js'
},
output:{
// 可以定义输出的跟路径,如果图片是cdn上面的,可以在从这里设置,html 引入这个js会自动加上
publicPath:'xx',
filename:'[name].js'
path:path.resolve(__dirname,'dist')
}
}

SouceMap

development 模式下 devtool 默认是true,开启souceMap 通过映射当代码出错的时候可以找到源文件是哪里出错而不是编译后的文件哪里出错.

当然建立这种映射会影响打包速度

具体内容

1
2
3
4
5
6
7
8
module.exports = {
mode: 'development',
// 这里可以关闭 sourceMap,开启则是'source-map'
devtool: 'none',
entry: {
main: './src/index.js'
}
}

souce-map会在dist 自动生成一个 js.map 映射文件,而inline-souce-map 则是将这个关系包含在 打包的js文件里面。

使用 cheap-xxx–xxx 的参数会加快打包方式,但是只会提示到几行不会精确到几列。

开发环境(development)推荐使用:cheap-module-eval-source-map(module代表module里面的错误也可以检测到,eval 可以加快编译速度)

生产环境(pruduction)则是:cheap-module-source-map

WebpackDevServer

webpack-dev-server 帮助打包后的运行在自动打开的服务器,并会跟随文件的改变而改变

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
module.exports = {
devServer:{
// 要打开的文件地址
contentBase:'./dist',
// 是否自动打开默认浏览器
open:true,
// 端口号,默认端口是8080
port:8080

}
}

// package.json
{
"scripts": {
"start": "webpack-dev-server",
"watch": "webpack --watch"
},
}

webpack-dev-server 打包的时候会把打包的 dist 目录内置到内存里面而不会显示出来,可以提高编译速度

相关内容

一开始的时候webpack-dev-server 因为配置不是很完善,一般人都会自己创建一个 server.js 文件来自己建一个服务器

这里使用 express 和 webpack-dev-middleware 来实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const express = require('express');
const webpack = require('webpack');
// 中间件可以监听,随着页面内容的改变而更改
const webpackDevMiddleware = require('webpack-dev-middleware');
const config = require('./webpack.config.js');
// 使用 webpack 的配置来返回一个编译器
const complier = webpack(config);

const app = express();
app.use(webpackDevMiddleware(complier,{
publicPath:config.output.publicPath
}));

app.listen(3000,()=>{
console.log('server is running on port 3000');
});

// package.json
{
"scripts": {
"middleware": "node server.js"
},
}

但是上面的内容是没有实现浏览器自动加载的。

Hot Module Replacement(HMR)

热模块更新,不刷新页面,重新加载css文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
module.exports = {
devServer: {
contentBase: './dist',
open: true,
port: 8080,
// 启动热模块加载
hot: true,
// 即便 html 不生效也不会自动刷新
hotOnly: true
},
plugins:[
new webpack.HotModuleReplacementPlugin()
]
}

只修改对应的 js 文件

1
2
3
4
5
if(module.hot){
module.hot.accept('./文件名.js',()=>{
// 运用页面的函数
})
}

而css 对应的代码是在 css-loader 里面底层实现了,所以不用手写 module.hot

Babel 处理 ES6 语法

babel-loader 与 @babel-core(webpack与 babel 桥梁)

@babel/preset-env

1
2
3
4
5
6
7
8
9
10
11
12
module.exports = {
module:{
rules:[{
text:/\.js$/,
exclude:/node_modules/,
loader:'babel-loader',
options:{
presets:['@babel/preset-env']
}
}]
}
}

这里可以将 ES6 语法转换为 ES5 语法,但是还不完善,接着在index.js里面直接引入

1
2
// index.js
import '@babel/polyfill'

@babel/polyfill 可以自动添加内容实现兼容低版本的浏览器。打包之后会发现打包的js会很大,因为它把所有低版本可能要兼容的代码都写了进去,而不是按需要来增加内容。这个时候可以通过配置来达到按需

1
2
3
4
5
6
7
8
9
10
11
12
13
14
module.exports = {
module:{
rules:[{
text:/\.js$/,
exclude:/node_modules/,
loader:'babel-loader',
options:{
presets:[['@babel/preset-env'],{
useBuiltIns:'usage'
}]
}
}]
}
}

webpack 进行打包的时候发现会报错

1
Module not found: Error: Can't resolve 'core-js/modules/es6.array.map'

之类的错误,通过查阅发现要下载一个 core-js 的插件便可以解决问题,具体可以查看 core-js

而当你使用这个按需加载的时候,就会提示你去掉文件中 import ‘@babel/polyfill’ ,因为会自动加载,另外可以配置要兼容的浏览器版本来判断需要引入那些兼容代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
module.exports = {
module:{
rules:[{
text:/\.js$/,
exclude:/node_modules/,
loader:'babel-loader',
options:{
presets:[['@babel/preset-env'],{
useBuiltIns:'usage',
targets:{
chrome:'67'
}
}]
}
}]
}
}

而上面的版本的谷歌浏览器是支持 ES6 语法的,因为打包出来的文件就会很小。可以看出来上面的方式有可能会全局污染(适合小项目),可以使用另外一种方式去配置(适合多模块)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
module.exports = {
module:{
rules:[{
test:/\.js$/,
exclude:/node_modules/,
loader:'babel-loader',
options:{
plugins:[['@babel/plugin-transform-runtime',{
corejs:2,
helpers:true,
regenerator:true,
useESModules:false
}]]
}
}]
}
}

配置上面的内容之前需要安装以下几个依赖

1
npm i -D @babel/runtime @babel/plugin-transfrom-runtime @babel/runtime-corejs2

具体可以查看这里

另外可以把 babel-loader options 里面的内容放在一个单独名为 .babelrc 的文件中

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"plugins": [
[
"@babel/plugin-transform-runtime",
{
"corejs": 2,
"helpers": true,
"regenerator": true,
"useESModules": false
}
]
]
}

打包 React 代码

安装依赖包

1
npm i -D @babel/preset-react

配置 .babelrc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"presets": [
[
"@babel/preset-env",
{
"targets": {
"chrome": "67"
},
"useBuiltIns": "usage"
}
],
[
"@babel/preset-react"
]
]
}

这里的顺序也是从下往上

具体点击这里

Tree Shaking

development 环境,按需加载,只支持 ES Module ,底层是一个静态引入实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// index.js 只引入 add 的打包结果
/*! exports provided: add, minus */
/*! exports used: add */

// 通过下面的配置
// webpack.config.js
module.exports = {
optimization:true
}
// package.json
{
// 因为只是单纯引入,挂载在全局而不用去使用里面具体的,以防止被去掉,使用这个参数可以做一个过滤不作 tree Shaking
"sideEffects":["@babel/polyi-fill","*.css"]
}

production 环境甚至需要 optimization 配置,会自动配置,但是 package.json 中的 sideEffects 需要保留

Development 和 Production 模式的区分打包

1
2
3
4
5
6
{
"scripts": {
"dev": "webpack-dev-server --config webpack.dev.js",
"build": "webpack --config webpack.prod.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
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
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
// webpack.dev.js
const webpack = require('webpack');
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');

module.exports = {
mode: 'development',
devtool: 'cheap-module-eval-source-map',
entry: {
main: './src/index.js'
},
devServer: {
contentBase: './dist',
open: true,
port: 8080,
hot: true,
hotOnly: true
},
module: {
rules: [{
test:/\.js$/,
exclude:/node_modules/,
loader:'babel-loader'
},{
test: /\.(jpg|png|gif)$/,
use: {
loader: 'url-loader',
options: {
name: '[name]_[hash].[ext]',
outputPath: 'images/',
limit: 2048
}
}
}, {
test: /\.scss$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
importLoaders: 2,
// modules: true
}
}, 'sass-loader',
'postcss-loader'
]
}, {
test: /\.css$/,
use: ['style-loader', 'css-loader', 'postcss-loader']
}, {
test: /\.(eot|ttf|svg|woff)$/,
use: {
loader: 'file-loader'
}
}]
},
plugins: [
new HtmlWebpackPlugin({ template: 'src/index.html' }),
new CleanWebpackPlugin(),
new webpack.HotModuleReplacementPlugin()
],
optimization:{
usedExports:true
},
output: {
publicPath: '/',
filename: '[name].js',
path: path.resolve(__dirname, 'dist')
}
}
// webpack.prod.js
const webpack = require('webpack');
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');

module.exports = {
mode: 'production',
devtool: 'cheap-module-source-map',
entry: {
main: './src/index.js'
},
module: {
rules: [{
test:/\.js$/,
exclude:/node_modules/,
loader:'babel-loader'
},{
test: /\.(jpg|png|gif)$/,
use: {
loader: 'url-loader',
options: {
name: '[name]_[hash].[ext]',
outputPath: 'images/',
limit: 2048
}
}
}, {
test: /\.scss$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
importLoaders: 2,
// modules: true
}
}, 'sass-loader',
'postcss-loader'
]
}, {
test: /\.css$/,
use: ['style-loader', 'css-loader', 'postcss-loader']
}, {
test: /\.(eot|ttf|svg|woff)$/,
use: {
loader: 'file-loader'
}
}]
},
plugins: [
new HtmlWebpackPlugin({ template: 'src/index.html' }),
new CleanWebpackPlugin(),
],
output: {
publicPath: '/',
filename: '[name].js',
path: path.resolve(__dirname, 'dist')
}
}

可以看出上面有很多重复的代码,可以进行抽取优化,用 webpack-merge 进行合并

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
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
// webpack.common.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');

module.exports = {
entry: {
main: './src/index.js'
},
module: {
rules: [{
test:/\.js$/,
exclude:/node_modules/,
loader:'babel-loader'
},{
test: /\.(jpg|png|gif)$/,
use: {
loader: 'url-loader',
options: {
name: '[name]_[hash].[ext]',
outputPath: 'images/',
limit: 2048
}
}
}, {
test: /\.scss$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
importLoaders: 2,
// modules: true
}
}, 'sass-loader',
'postcss-loader'
]
}, {
test: /\.css$/,
use: ['style-loader', 'css-loader', 'postcss-loader']
}, {
test: /\.(eot|ttf|svg|woff)$/,
use: {
loader: 'file-loader'
}
}]
},
plugins: [
new HtmlWebpackPlugin({ template: 'src/index.html' }),
new CleanWebpackPlugin(),
],
output: {
publicPath: '/',
filename: '[name].js',
path: path.resolve(__dirname, 'dist')
}
}
// webpack.dev.js
const webpack = require('webpack');
const merge = require('webpack-merge');
const commonConfig = require('./webpack.common.js');

const devConfig = {
mode: 'development',
devtool: 'cheap-module-eval-source-map',
devServer: {
contentBase: './dist',
open: true,
port: 8080,
hot: true
},
plugins: [
new webpack.HotModuleReplacementPlugin()
],
optimization:{
usedExports:true
}
}

module.exports = merge(commonConfig,devConfig);
// webpack.prod.js
const merge = require('webpack-merge');
const commonConfig = require('./webpack.common.js');

const prodConfig = {
mode: 'production',
devtool: 'cheap-module-source-map'
}
module.exports = merge(commonConfig,prodConfig);

Webpack 和 Code Splitting

同步代码

遇到公共类库,会自动打包出来一个文件,例如 vendors~main.js

1
2
3
4
5
module.exports = {
splitChunks:{
chunks:'all'
}
}

异步代码

安装 babel-plugin-dynamic-import-webpack 后在 .babelrc 里面配置,这个插件不是官方的,所以不支持魔法注释

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"presets": [
[
"@babel/preset-env",
{
"targets": {
"chrome": "67"
},
"useBuiltIns": "usage"
}
],
[
"@babel/preset-react"
]
],
"plugins":["dynamic-import-webpack"]
}

官方的 @babel/plugin-syntax-dynamic-import 可以支持魔术注释

重新配置 .babelrc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"presets": [
[
"@babel/preset-env",
{
"targets": {
"chrome": "67"
},
"useBuiltIns": "usage"
}
],
[
"@babel/preset-react"
]
],
"plugins":["@babel/plugin-syntax-dynamic-import"]
}

webpack.common.js

1
2
3
4
5
6
7
8
9
10
module.exports = {
splitChunks:{
chunks:'all',
cacheGroups:{
// 取消名字前面的默认名称
vendors:false,
default:false
}
}
}

index.js

1
2
3
4
5
6
7
8
9
10
11
12
// 使用魔法注释
function getComponent(){
return import(/* webpackChunkName:"lodash" */'lodash').then(({default:_}) =>{
const element = document.createElement('div');
element.innerHTML = _.join(['Dell','Lee'],'_');
return element;
})
}

getComponent().then(element=>{
document.body.appendChild(element);
});

打包编译之后就可以看到异步引入的库被自己定义的名字打包出来了

splitChunksPlugins

默认配置:

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
module.exports = {
//...
optimization: {
splitChunks: {
// 只对异步代码生效 all 则是全部代码,inintal 则是同步代码
chunks: 'async',
// 引入的模块/包/库 大于 30kb 则会代码分割
minSize: 30000,
// 会尝试将 大于这个尺寸的库重新拆分为n个 maxSize 大的库,但是一般这个参数不会生效
maxSize: 0,
// 打包生成的库的次数少于 minChunks次 则不会分割
minChunks: 1,
maxAsyncRequests: 5,
maxInitialRequests: 3,
// 自动生成的文件的中间符
automaticNameDelimiter: '~',
name: true, // 配合 cacheGroups 使用
// 缓存组
cacheGroups: {
vendors: {
// 如果引入的库在 node_modules 里面,符合这个组打包出来的文件前缀会有vendors~入口.js
test: /[\\/]node_modules[\\/]/,
// 优先级 符合 在 node_modules 优先级大于在默认的,那么打包就会打包在 vendors 这个相关配置里面
priority: -10,
// 强制定义在同一个文件里面,可以不配置
filename:'vendors.js'
},
// 默认处理途径
default: {
minChunks: 2,
priority: -20,
// 如果 模块在前面打包过了就不会重复打包
reuseExistingChunk: true
filename:'common.js'
}
}
}
}
};

Lazy loading 懒加载

通过 import 语法来异步加载,什么时候要使用则是取决于个人

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
function getComponent(){
return import(/* webpackChunkName:"lodash" */'lodash').then(({default:_}) =>{
const element = document.createElement('div');
element.innerHTML = _.join(['Dell','Lee'],'_');
return element;
})
}

document.addEventListener('click',()=>{
getComponent().then(element=>{
document.body.appendChild(element);
});
})
// async await 改进
async function getComponent(){
const {default:_} = await import(/* webpackChunkName:"lodash" */'lodash');
const element = document.createElement('div');
element.innerHTML = _.join(['Dell','Lee'],'_');
return element;
}

document.addEventListener('click',()=>{
getComponent().then(element=>{
document.body.appendChild(element);
});
})

打包分析

配置 package.json

1
2
3
4
5
{
"scripts":{
"dev-build":"webpack --profile --json > stats.json --config ./build/webpack.dev.js"
}
}

上面代码的意思是分析webapck 打包的文件生成后生成 stats.json 。

点击这个网站可以将这个json 文件上传,会帮你自动分析版本,时长以及相关错误,模块之间的关系等等

更多相关工具

Preloading,Prefetching

在谷歌浏览器 ctrl+shift+p 输入show Coverage 点击录制后可以看到一个网站首屏加载 js 文件的利用率,以及可以优化的可能。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 当浏览器空闲的时候,可以先加载这个文件
document.addEventListener('click',()=>{
import(/* webpackPrefetch:true*/'./click.js').then(({default:func})=>{
func();
})
})

// 这个则是和核心文件一起加载
document.addEventListener('click',()=>{
import(/* webpackPreloading:true*/'./click.js').then(({default:func})=>{
func();
})
})

性能优化在缓存上面可以优化的地方有限,可以考虑更多是代码的使用率,将一开始不会用的代码尽量使用异步加载的方式来加载

CSS 文件的代码分割

mini-css-extract-plugin,没有实现 HMR 需要手动配置,所以一般是适用在线上环境使用。

配置如下:

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
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
// webpack.common.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');

module.exports = {
entry: {
main: './src/index.js'
},
module: {
rules: [{
test:/\.js$/,
exclude:/node_modules/,
loader:'babel-loader'
},{
test: /\.(jpg|png|gif)$/,
use: {
loader: 'url-loader',
options: {
name: '[name]_[hash].[ext]',
outputPath: 'images/',
limit: 2048
}
}
},
{
test: /\.(eot|ttf|svg|woff)$/,
use: {
loader: 'file-loader'
}
}]
},
plugins: [
new HtmlWebpackPlugin({ template: 'src/index.html' }),
new CleanWebpackPlugin({
cleanOnceBeforeBuildPatterns:[path.resolve(__dirname,'../dist')],
}),
],
optimization:{
usedExports:true,
splitChunks:{
chunks:'all'
}
},
output: {
publicPath: './',
filename: '[name].js',
path: path.resolve(__dirname, '../dist')
}
}
// webpack.dev.js
const webpack = require('webpack');
const merge = require('webpack-merge');
const commonConfig = require('./webpack.common.js');

const devConfig = {
mode: 'development',
devtool: 'cheap-module-eval-source-map',
devServer: {
contentBase: './dist',
open: true,
port: 8080,
hot: true
},
module:{
rules:[
{
test: /\.scss$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
importLoaders: 2,
}
}, 'sass-loader',
'postcss-loader'
]
}, {
test: /\.css$/,
use: ['style-loader', 'css-loader', 'postcss-loader']
}
]
},
plugins: [
new webpack.HotModuleReplacementPlugin()
],
optimization:{
}
}

module.exports = merge(commonConfig,devConfig);

// webpack.prod.js
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const merge = require('webpack-merge');
const commonConfig = require('./webpack.common.js');

const prodConfig = {
mode: 'production',
devtool: 'cheap-module-source-map',
plugins:[
new MiniCssExtractPlugin({
// template 要引入的
filename:'[name].css',
// 间接引入的走这个
chunkFilename:'[name].chunk.css'
})
],
module:{
rules:[
{
test: /\.scss$/,
use: [
MiniCssExtractPlugin.loader,
{
loader: 'css-loader',
options: {
importLoaders: 2,
}
}, 'sass-loader',
'postcss-loader'
]
}, {
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader']
}
]
}
}
module.exports = merge(commonConfig,prodConfig);

将之前共同配置的 webpack,common.js module rules 里面关于css 的配置移动到 webpack.dev.js 里面不作修改。然后同样复制一遍到 webpack.prod.js 里面,把关于css-loader 里面的的 style-loader 换成 MiniCssExtractPlugin.loader 作为最后一步来处理。

一个要注意的点是在公共的 optimization 里面加了一个 usedExports:true,同时也要在 package.json 里面配置 sideEffects:[‘@babel/polyfill’,’*.css’]

运行 npm run build 便可以打包分割开 css 文件了

压缩 css 代码

optimize-css-assets-webpack-plugin

1
2
3
4
5
6
7
8
9
// webpack.prod.js
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
module.exports ={
optimization:{
minimizer:[
new OptimizeCSSAssetPlugin({})
]
}
}

多个css文件打包到同一个css里面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// webpack.prod.js
module.exports ={
optimization:{
// 底层依旧是 splitChunks 实现的
splitChunks:{
cacheGroups:{
styles:{
name:'styles',
test:/\.css$/,
chunks:'all',
enforce:true
}
}
}
}
}

多个css文件按入口文件来打包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// webpack.prod.js
module.exports = {
optimization:{
splitChunks:{
cacheGroups:{
fooStyles:{
name:'foo',
test: (m, c, entry = 'foo') => m.constructor.name === 'CssModule' && recursiveIssuer(m) === entry,
chunks:'all',
enforce:true
},
barStyles:{
name:'bar',
test: (m, c, entry = 'foo') => m.constructor.name === 'CssModule' && recursiveIssuer(m) === entry,
chunks:'all',
enforce:true
}
}
}
}
}

Webpack 与浏览器缓存(Caching)

增加 hash 值,如果文件没有改变的话,那么打包前后几次打包出来的文件里面的哈希值就不会发生变化,浏览器的缓存也就起到了作用。如果改变了,浏览器则会去请求先的文件而不是继续用原来的缓存文件

1
2
3
4
5
6
7
// webpack.prod.js
module.exports ={
output:{
filename:'[name].[contenthash].js',
chunkFilename:'[name].[contenthash].js'
}
}

上面的配置是新版本的webpack直接有的,但是旧版本的 webpack 每次打包 hash 值都会发生变化,可以在 webpack.common.js 里面配置一个参数

1
2
3
4
5
6
7
module.exports = {
optimization:{
runtimeChunk:{
name:'runtime'
}
}
}

这个参数会把旧版本中的 manifest 单独抽离出来一个 runtime 开头的 js文件里面,这个 文件主要描述的是库与业务逻辑代码之间的一些关系,旧版本webpack 中这个关系会因为每次打包而发生变化。

Shimming

自动引入某个库

1
2
3
4
5
6
7
8
9
module.exports ={
plugins:[
new webpack.ProvidePlugin({
_:'lodash',
// 花式引用
_join:['lodash','join']
})
]
}

全局this 指向 window

imports-loader

1
2
3
4
5
6
7
8
9
10
11
12
13
module.exports ={
module:{
rules:[{
test:/\.js$/,
exclude:/node_modules/,
use:[{
loader:'babel-loader'
},{
loader:'imports-loader?this=>window'
}]
}]
}
}

环境变量

可以考虑使用,用个人,具体是在 package.json 里面传递一个 env 参数

1
2
3
4
5
6
7
{
"scripts": {
"dev-build": "webpack --profile --json > stats.json --config ./build/webpack.common.js",
"dev": "webpack-dev-server --config ./build/webpack.common.js",
"build": "webpack --env.production --config ./build/webpack.common.js"
},
}

而 开发与生产环境的webpack 则是都放到 common 里面来判断分发

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
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
// webpack.dev.js
const webpack = require('webpack');
const path = require('path');


const devConfig = {
mode: 'development',
devtool: 'cheap-module-eval-source-map',
devServer: {
contentBase: './dist',
open: true,
port: 8080,
hot: true
},
module:{
rules:[
{
test: /\.scss$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
importLoaders: 2,
}
}, 'sass-loader',
'postcss-loader'
]
}, {
test: /\.css$/,
use: ['style-loader', 'css-loader', 'postcss-loader']
}
]
},
plugins: [
new webpack.HotModuleReplacementPlugin()
],
output: {
publicPath: './',
filename: '[name].js',
chunkFilename:'[name].js',
path: path.resolve(__dirname, '../dist')
}
}

module.exports = devConfig;

// webpack.prod.js
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');

const prodConfig = {
mode: 'production',
devtool: 'cheap-module-source-map',
module:{
rules:[
{
test: /\.scss$/,
use: [
MiniCssExtractPlugin.loader,
{
loader: 'css-loader',
options: {
importLoaders: 2,
}
}, 'sass-loader',
'postcss-loader'
]
}, {
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader']
}
]
},
optimization:{
minimizer:[
new OptimizeCSSAssetsPlugin({})
]
},
plugins:[
new MiniCssExtractPlugin({
filename:'[name].css',
chunkFilename:'[name].chunk.css'
})
],
output: {
publicPath: './',
filename: '[name].[contenthash].js',
chunkFilename:'[name].[contenthash].js',
}
}
module.exports = prodConfig;

// webpack.common.js
const webpack = require('webpack')
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const merge =require('webpack-merge');
const devConfig = require('./webpack.dev.js');
const prodConfig = require('./webpack.prod.js');

const commonConfig = {
entry: {
main: './src/index.js'
},
module: {
rules: [{
test:/\.js$/,
exclude:/node_modules/,
use:[{
loader:'babel-loader'
},{
loader:'imports-loader?this=>window'
}]

},{
test: /\.(jpg|png|gif)$/,
use: {
loader: 'url-loader',
options: {
name: '[name]_[hash].[ext]',
outputPath: 'images/',
limit: 2048
}
}
},
{
test: /\.(eot|ttf|svg|woff)$/,
use: {
loader: 'file-loader'
}
}]
},
plugins: [
new HtmlWebpackPlugin({ template: 'src/index.html' }),
new CleanWebpackPlugin({
cleanOnceBeforeBuildPatterns:[path.resolve(__dirname,'../dist')],
}),
new webpack.ProvidePlugin({
_:'lodash'
})
],
optimization:{
usedExports:true,
splitChunks:{
chunks:'all'
}
}
}

module.exports = (env)=>{
if(env && env.production){
return merge(commonConfig,prodConfig)
}else{
return merge(commonConfig,devConfig)
}
}

Library 的打包

简单配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// webpack.config.js
const path = require('path');

module.exports = {
mode: 'production',
entry: './src/index.js',
externals: ['lodash'],
output: {
path:path.resolve(__dirname,'dist'),
filename:'library.js',
// 可以通过 script src=library.js 来引用这个库
library:'library',
// 通用打包 CommonJS 或者 UMD,上面文件的挂载处(可以为this 或者 window)
libraryTarget:'umd'
},
}

PWA(Progressive Web Application) 的打包

网站在被访问过一次之后,如果服务器挂掉了,浏览器还可以利用缓存来访问这个网站。

workbox-webpack-plugin

1
2
3
4
5
6
7
8
9
// 配置 webpack.prod.js
module.exports = {
plugins:[
new WorkboxPlugin.GenerateSW({
clientsClaim:true,
skipWaiting:true
})
]
}

之后打包编译的是时候会生成多一个 service-worker.js 文件,在项目中运用

1
2
3
4
5
6
7
8
// index.js
if('serviceWorker' in navigator){
navigator.serviceWorker.register('/service-worker.js').then(registration=>{
console.log('service-worker registed');
}).catch(error=>{
console.log('service-worker register error');
})
}

这样配置之后,当用户访问过一次网站之后便会进行缓存,如果当服务器挂掉的时候,用户还是可以在浏览器访问到网站

TypeScript 的打包配置

webpack基本配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const path = require('path');

module.exports = {
mode:'production',
entry:'./src/index.tsx',
module:{
rules:[{
test:/\.tsx?$/,
use:'ts-loader',
exclude:/node_modules/
}]
},
output:{
filename:'bundle.js',
path:path.resolve(__dirname,'dist');
}
}

除了webpack 配置之后,在 打包 ts 的时候还必须要配置一个 tsconfig.json

1
2
3
4
5
6
7
8
9
10
11
12
{
"compilerOptions": {
// 出口
"outDir": "./dist",
// 要处理的模块
"module": "es6",
// 编译成es5的
"target": "es5",
// 运行在文件中引入其他的js
"allowJs": true
}
}

如果要引入其他的库,例如 lodash ,需要安装对应的说明,@types/lodash ,具体的可以查看这里

WebpackDevServer

开发环境实现请求代理转发

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
// 基本配置
module.exports = {
devServer: {
contentBase: './dist',
open: true,
port: 8080,
hot: true,
hotOnly: true,
proxy: {
'/react/api': {
// 代理转发的url
target: 'https://www.dell-lee.com/',
// 当是 https 的时候可以设置这个参数
secure: false,
// 拦截
bypass: function (req, res, proxyOptions) {
if (req.headers.accept.indexOf('html') !== 1) {
console.log('Shipping proxy for browser request');
return '/index.html';
}
},
// 路径重写
pathRewrite: {
'header.json': 'demo.json'
},
// 配置 headers 主机名与cookie 等等
headers:{
host:'xx',
cookie:''
}
},
}
}
}

具体文档

底层是 webpack-dev-middleware

解决单页面应用路由问题

当配置路由发现找不到内容的时候,可以配置 historyApiFallback

ESLint 的配置

先安装

1
npm i eslint -D

接着是初始化配置

1
eslint -init

采用目前流行 airbnb 的方式

会看到项目生成一个 .eslintrc.js

如果没有使用vscode 只能用 eslint src 命令行来查看错误信息

可以进行基本配置:

1
2
3
4
5
6
7
8
9
10
module.exports = {
"extends": "airbnb",
"parser": "babel-eslint",
"rules": {
"": 0
},
globals:{
document
}
};

eslint-loader

webpack 也有先关的配置,但是会影响打包的速度,一般会建议使用这种方式来配置。还是使用 vscode 的插件式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
module.exports = {
module: {
rules: [{
test: /\.js$/,
exclude: /node_modules/,
use: [
{ loader: 'babel-loader' },
{
loader: 'eslint-loader',
options: {
// 自动修复
fix: true
}
}
]
}
}

具体点击

devServer 配置 overlay 可以在页面提示错误信息

webpack 性能优化

1.升级工具的版本(node,npm,yarn)

2.loader(include/exclude)在尽可能少的模块上使用

3.plugin 尽可能精简可靠

4.resolve 参数合理

1
2
3
4
5
6
7
8
9
module.exports ={
resolve:{
extensions:['.js','.jsx'],
// 使用别名
alias:{
haha:path.resolve(__dirname,'../src/child/');
}
}
}

5.使用 DllPlugin 提高打包速度

add-asset-html-webpack-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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
// webpack.dll.js
// 将共用的库放到一个文件里面去单独打包,用 library 暴露出来。然后用 webpack.DllPlugin 这个插件对这些库分析出一种文件关系映射.json 文件
const path = require('path');
const webpack =require('webpack');

module.exports = {
mode: 'production',
entry: {
vendors: ['lodash'],
react: ['react', 'react-dom'],
},
output: {
filename: '[name].dll.js',
path: path.resolve(__dirname, '../dll'),
library: '[name]'
},
plugins:[
new webpack.DllPlugin({
name:'[name]',
path:path.resolve(__dirname,'../dll/[name].manifest.json')
})
]
}
// 在webpack.common.js 文件里面配置(也可以只配置生产环境),将上面生成文件挂载到 template index.html上面
module.exports = {
plugins:[
new AddAssetHtmlWebpackPlugin({
filepath:path.resolve(__dirname,'../dll/verndors.dll.js')
}),
new AddAssetHtmlWebpackPlugin({
filepath:path.resolve(__dirname,'../dll/react.dll.js')
}),
new webpack.DllRefercencePlugin({
mainfest:path.resolve(__dirname,'../dll/vendors.mainfest.json')
}),
new webpack.DllRefercencePlugin({
mainfest:path.resolve(__dirname,'../dll/react.mainfest.json')
})
]
}
// 进行优化自动注入
const fs = require('fs');
const plugins = [
new HtmlWebpackPlugin({
template:'src/index.html'
}),
new CleanWebpackPlugin({
cleanOnceBeforeBuildPatterns: [path.resolve(__dirname, '../dist')],
})
]
const files = fs.readdirSync(path.resolve(__dirname,'../dll'))
files.forEach(file=>{
if(/.*\.dll.js/.test(file)){
plugins.push(new AddAssetHtmlWebpackPlugin({
filepath:path.resolve(__dirname,'../dll',file)
}))
}
if(/.*\.mainfest.json/.test(file)){
plugins.push(new AddAssetHtmlWebpackPlugin({
filepath:path.resolve(__dirname,'../dll',file)
}))
}
})
module.exports = {
plugins,
}

6.控制包文件的大小

7.thread-loader,parallel-webpack,happypack 多进程打包

8.合理使用 sourceMap(信息越详细打包越久)

9.结合打包分析 stats.json 优化分析

10.开发环境内存编译,无用插件剔除

多页面打包配置

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
// webpack.common.js
module.exports = {
entry: {
main: './src/index.js',
list: './src/list.js',
},
plugins:[
new HtmlWebpackPlugin({
filename:'index.html',
template: 'src/index.html',
chunks:['runtime','vendors','main']
}),
new HtmlWebpackPlugin({
filename:'list.html',
template: 'src/index.html',
chunks:['runtime','vendors','list']
}),
]
}

// 优化
const configs = {
entry: {
main: './src/index.js',
list: './src/list.js',
},
// 其他所有配置
}
const makePlugins = (configs)=>{
const plugins = ['其他的plugins'];
Object.keys(configs.entry).forEach(item=>{
plugins.push(
new HtmlWebpackPlugin({
template:'src/index.html',
filename:`${item}.html`,
chunks:['runtime','vendors','item']
})
)
})

// 上面优化的代码
const files = fs.readdirSync(path.resolve(__dirname,'../dll'))
files.forEach(file=>{
if(/.*\.dll.js/.test(file)){
plugins.push(new AddAssetHtmlWebpackPlugin({
filepath:path.resolve(__dirname,'../dll',file)
}))
}
if(/.*\.mainfest.json/.test(file)){
plugins.push(new AddAssetHtmlWebpackPlugin({
filepath:path.resolve(__dirname,'../dll',file)
}))
}
})
return plugins;
}
configs.plugins = makePlugins(configs);
module.export = configs;

如何编写一个 Loader

简单的实现

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
// replaceLoader.js 
// loader-utils 是官方一个符合 loader 规范的工具
const loaderUtils = require('loader-utils');

module.exports = function (source) {
return source.replace('haha', 'world');
}

// replaceLoaderAsync.js
const loaderUtils = require('loader-utils');

module.exports = function (source) {
const options = loaderUtils.getOptions(this);
const callback = this.async();
setTimeout(() => {
const result = source.replace('lbh', options.name);
callback(null, result);
}, 1000)
}

// index.js
console.log('hello lbh');

// webpack.config.js
const path = require('path');

module.exports = {
mode: 'development',
entry: {
main: './src/index.js'
},
// loader 的位置,先查找 node_modules 如果没有就往下找,那么自定义的 loader 就不用写全路径了
resolveLoader: {
modules: ['node_modules', './loaders']
},
module: {
rules: [
{
test: /\.js?$/,
use: [{
loader: 'replaceLoader.js',
},
{
loader: 'replaceLoaderAsync.js',
options: {
name: 'haha'
}
}]
}
]
},
output: {
filename: '[name].js',
path: path.resolve(__dirname, 'dist')
}
}

自定义 loader 的用途很多,可以拿来做一个简单的错误检验

1
2
3
4
5
6
const loaderUtils = require('loader-utils');

module.exports = function (source) {
try(function(){})catch(e){}
return source.replace('haha', 'world');
}

或者是国际化,在相关页面弄一个占位符

1
2
3
4
5
6
7
module.exports = function (source) {
if(Node全局变量 === '中文'){
source.replace('{{title}}', '中文标题');
}else{
source.replace('{{title}}', 'engilsh title');
}
}

可以具体参数可以点击这里

如何编写一个 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
43
44
45
46
47
// plugin/copyright-webpack-plugin
class CopyRightWebpackPlugin {
constructor(options) {
console.log(options)
}
//compiler:webpack 的实例
apply(compiler) {
// 同步
compiler.hooks.compile.tap('CopyRightWebpackPlugin', (compilation) => {
console.log('compiler');
})
// 异步
compiler.hooks.emit.tapAsync('CopyRightWebpackPlugin', (compilation, cb) => {
debugger;
compilation.assets['copyright.txt']={
source:function(){
return 'copyright by hahaha'
},
size:function(){
return 19;
}
}
cb();
})
}
}

module.exports = CopyRightWebpackPlugin;

// 配置 webpack.config.js
const path = require('path');
const CopyRightWebpackPlugin = require('./plugin/copyright-webpack-plugin.js');
module.exports = {
mode: 'development',
entry: {
'main': './src/index.js'
},
plugins: [
new CopyRightWebpackPlugin({
name:'haha'
})
],
output: {
filename: '[name].js',
path: path.resolve(__dirname, 'dist')
}
}

调试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
"name": "plugin",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"build": "webpack",
"debug": "node --inspect --inspect-brk node_modules/webpack/bin/webpack.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"webpack": "^4.30.0",
"webpack-cli": "^3.3.0"
}
}

npm run debug

进入下一步,添加 watch compilation 可以对一些类似这样的参数进行调试观察

具体的可以点击这里

Bundler 源码编写(模块分析)

文件目录src 下面有三个文件,分别是 index.js message.js word.js ,引用关系是从左到右导入下一个的文件,具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// word.js
export const word = 'hello'

// message.js
import { word } from './word.js'

const message = `say ${word}`

export default message;

// index.js
import message from './message.js'

console.log(message);
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
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
// 基本配置
const fs = require('fs');
const path = require('path');
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const babel = require('@babel/core');

// 入口文件的分析
const moduleAnalyser = (filename) => {
const content = fs.readFileSync(filename, 'utf-8');
// 抽象语法树
const ast = parser.parse(content, {
sourceType: 'module'
});
// 依赖文件,键值对,键:依赖文件的相对路径,值:依赖文件的相对于bundler 的路径(绝对路径)
const dependencies = {};
// 分析抽象语法树
traverse(ast, {
// 入口声明
ImportDeclaration({ node }) {
// 路径转换
const dirname = path.dirname(filename);
const newFile = './' + path.join(dirname, node.source.value)
dependencies[node.source.value] = newFile
}
})
// 抽象语法树的转换,提取可以在浏览器运行的代码
const { code } = babel.transformFromAst(ast, null, {
// 插件的集合
presets: ["@babel/preset-env"]
})
return {
filename,
dependencies,
code
}
}
// 依赖图谱
const makeDependenciesGraph = (entry) => {
const entryModule = moduleAnalyser(entry)
const graphArray = [entryModule];
for (let i = 0; i < graphArray.length; i++) {
const item = graphArray[i];
// 获取依赖关系
const { dependencies } = item;
if (dependencies) {
for (let j in dependencies) {
graphArray.push(
moduleAnalyser(dependencies[j])
)
}
}
}
// 格式转换
const graph = {}
graphArray.forEach(item => {
graph[item.filename] = {
dependencies: item.dependencies,
code: item.code
}
})
return graph;
}

// 生成浏览器可以用的代码
const generateCode = (entry) => {
// 转为字符串
const graph = JSON.stringify(makeDependenciesGraph(entry));
// graph code 里面有 exports 跟 require 的方法,但是浏览器没有这两个函数,需要我们自己去实现
console.log(graph);
return `
(function (graph) {
function require(module) {
function localRequire(relativePath) {
return require(graph[module].dependencies[relativePath])
}
var exports = {};
(function (require, exports, code) {
eval(code)
})(localRequire, exports, graph[module].code);
return exports;
}
require('${entry}')
})(${ graph})
`;
}

const Info = generateCode('./src/index.js');
console.log(Info);

运行

1
node bundler.js | highlight

控制台输出

1
2
3
4
5
6
7
8
9
10
11
12
13
(function (graph) {
function require(module) { // ./message.js 通过相对路径来拿到绝对路径
function localRequire(relativePath) {
return require(graph[module].dependencies[relativePath])
}
var exports = {};
(function (require, exports, code) {
eval(code)
})(localRequire, exports, graph[module].code);
return exports;
}
require('./src/index.js')
})({"./src/index.js":{"dependencies":{"./message.js":"./src\\message.js"},"code":"\"use strict\";\n\nvar _message = _interopRequireDefault(require(\"./message.js\"));\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { \"default\": obj }; }\n\nconsole.log(_message[\"default\"]);"},"./src\\message.js":{"dependencies":{"./word.js":"./src\\word.js"},"code":"\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports[\"default\"] = void 0;\n\nvar _word = require(\"./word.js\");\n\nvar message = \"say \".concat(_word.word);\nvar _default = message;\nexports[\"default\"] = _default;"},"./src\\word.js":{"dependencies":{},"code":"\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.word = void 0;\nvar word = 'hello';\nexports.word = word;"}})

复制粘贴到浏览器便可以运行

graph 输出内容是

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
"./src/index.js": {
"dependencies": {
"./message.js": "./src\\message.js"
},
"code": "\"use strict\";\n\nvar _message = _interopRequireDefault(require(\"./message.js\"));\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { \"default\": obj }; }\n\nconsole.log(_message[\"default\"]);"
},
"./src\\message.js": {
"dependencies": {
"./word.js": "./src\\word.js"
},
"code": "\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports[\"default\"] = void 0;\n\nvar _word = require(\"./word.js\");\n\nvar message = \"say \".concat(_word.word);\nvar _default = message;\nexports[\"default\"] = _default;"
},
"./src\\word.js": {
"dependencies": {},
"code": "\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.word = void 0;\nvar word = 'hello';\nexports.word = word;"
}
}

可以看到 code 里面的内容有一个require 函数和一个 exports 对象,所以为了这些代码能够在浏览器运行,我们需要自己创建一个 require函数和exports 空对象。

0%