webpack配置详解

webpack配置方式及如何优化

entry & output

webpack.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
28
29
30
31
module.exports = {
// 入口文件
entry: './index.js'
// 等同于
entry: {
main: './index.js'
},

// 输出
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'src')
},

// 多文件打包
entry: {
main: './index.js',
sub: './index.js',
},
output: {
filename: '[name].js',
path: path.resolve(__dirname, 'src')
},

// js上传到cdn
output: {
publicPath: 'http://cdn.com/',
filename: '[name].js',
path: path.resolve(__dirname, 'src')
},
}

source-map

源代码和生成代码的映射 webpack.config.js

1
2
3
4
5
6
7
8
// 出错后可以定位
// inline: map文件base64到main.js,不单独拿出来
// cheap: 精确到行,只map业务代码
// module: map所有模块代码
// eval: 不翻译为base64,存在eval的方式执行
module.exports = {
devtool: 'source-map',
}

最佳实践:

  • 开发环境: cheap-module-eval-source-map
  • 线上: cheap-module-source-map

loader

webpack.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
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
// 指定路径用node的path方法,__dirname为wepack.config目录
const path = require('path')

module.exports = {
// 打包环境
mode: 'production'
// 此环境下不压缩
mode: 'development'

// loader,执行顺序-从下到上
module: {
rules: [
{
test: /\.jpg$/,
use: {
// https://webpack.js.org/loaders/file-loader/
loader: 'file-loader',
options: {
// placeholder 占位符,打包后的文件名
// [name]: 老文件名
// [hash]: hash值
// [ext]: 文件后缀
name: '[name]_[hash].[ext]',
// 打包到目录下
outputPath: 'img/'
}
}
},
{
test: /\.jpg$/,
use: {
// https://webpack.js.org/loaders/url-loader/
// 和file-loader类似,但是打包的图片会转为base64
loader: 'url-loader',
options: {
// placeholder 占位符,打包后的文件名
// [name]: 老文件名
// [hash]: hash值
// [ext]: 文件后缀
name: '[name]_[hash].[ext]',
// 打包到目录下
outputPath: 'img/',
// 小于2kb的才打包到JS
limit: 2048
}
}
},
{
test: /\.css$/,
use: [
// style-loader css内容挂载到head里
// css-loader 分析多个css文件关系,合并
'style-loader', 'css-loader'
]
},
{
test: /\.scss$/,
use: [
// sass-loader 对sass分析
// postcss-loader 配合插件 autoprefixer 添加css前缀,-webkit-
'style-loader',
{
loader: 'css-loader',
options: {
// import的css里import其他的css时,在打包前,需要用下面2个loader处理
importLoaders: 2,
// css-modules,使用方式如下
// import style from 'x.scss'
// style.a
modules: true
}
},
'sass-loader',
'postcss-loader'
]
},
// 字体文件
{
test: /\.(eot|ttf|svg)$/,
use: [
// style-loader css内容挂载到head里
// css-loader 分析多个css文件关系,合并
'file-loader'
]
},
]
}
}

plugin

npm i html-webpack-plugin -D

webpack.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const HtmlWebpackPlugin = require('html-webpack-plugin')
const CleanWebpackPlugin = require('clean-webpack-plugin')

module.exports = {
plugin: [
// 打包结束后自动生成index.html,并把打包后的js自动引入
new HtmlWebpackPlugin({
// 作为模板
template: 'src/index.html',
}),
// 打包前删除dist目录下的文件
new CleanWebpackPlugin(['dist'])

]
}

script命令

package.json

1
2
3
script: {
"bundle": "webpack"
}

postcss-config.js

当webpack配置用到时,需要此配置文件

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

监听文件变化

简单方式

package.json

1
2
3
"scripts": {
"watch": "webpack --watch"
}

一般方式

npm i webpack-dev-server -D

webpack.config.js

1
2
3
4
5
6
7
8
9
10
11
12
module.exports = {
devServer: {
// 监听目录
contentBase: './dist',
// 打开端口后自动访问地址
open: true,
// 发到api的请求转到 loaclhost:3000
proxy: {
'/api': 'http://loaclhost:3000'
}
}
}

package.json

1
2
3
"scripts": {
"start": "webpack-dev-server"
}

热模块更新 HMR

改变样式代码时只改变样式,不刷新页面

webpack.config.js

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

module.exports = {
devServer: {
hot: true,
// 即使不生效,也不自动刷新
hotOnly: true
},
plugins: [
new webpack.HotModuleReplacementPlugin()
]
}

index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
import a from './a'
import b from './b'

a()
b()

// 点击页面,改变了a在页面上的数据,此时修改b文件里的数据,页面不会刷新
if (module.hot) {
// params: 引入的文件, 回调
module.hot.accept('./b', () => {
b()
})
}

处理es6 babel

npm i --save-dev babel-loader @babel/core
npm install @babel/preset-env --save-dev

webpack.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

module.exports = {
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
loader: "babel-loader",
options: {
presets: [["@babel/preset-env", {
// 运行的环境,浏览器版本
targets: {
// 只做对chrome版本>67的兼容
chrome: "67"
},
// poltfill时不加所有,只加业务代码用到的
useBuiltIns: 'usage'
}]]
}
}
]
}
}

babel polifill

将Promise转es5 npm install --save @babel/polyfile

如果规则放在 babelrc 内则不用在项目中写 index.js

1
import "@babel/polyfill";

组件库问题

写组件库的话会污染全局变量
npm install --save-dev @babel/plugin-transform-runtime
npm install --save @babel/runtime
npm install --save @babel/runtime-corejs2

webpack.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

module.exports = {
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
loader: "babel-loader",
// 下面内容也可以新建 .babelrc 放进去,这里就不用写了
options: {
"plugins": [
[
"@babel/plugin-transform-runtime",
{
"absoluteRuntime": false,
"corejs": 2,
"helpers": true,
"regenerator": true,
"useESModules": false
}
]
]
}
}
]
}
}

react

npm install --save-dev @babel/preset-react npm i react react-dom --save

webpack.config.js

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

Tree Shaking

打包时模块内没用到的东西去掉
只支持ES Module方式 (import)

webpack.config.js

1
2
3
4
5
6
7
8
module.exports = {
module: {
optimization: {
// 开发环境加这个
usedExports: true
}
}
}

package.json

1
2
3
4
5
6
// polyfile可能导出的兼容未使用,导致Tree Shaking 把整个polyfile去掉,所以加入白名单
"sideEffects": ["@babel/polyfile"]
// import './css.css'可能会去掉,加入名单
"sideEffects": ["*.css"]
// 不在文件直接Import,改为false
"sideEffects": false

分环境打包

webpack.dev.js拷贝一份webpack.pro.js,对webpack.pro.js重新配置

package.json

1
2
3
4
"script": {
"dev": "webpack-dev-server --config webpack.dev.js",
"build": "webpack --config webpack.pro.js",
}

抽离通用的配置项 npm i wepack-merge -D

webpack.common.js

1
// 通用的配置

webpack.dev.js

1
2
3
4
5
const merge = require('webpack-merge')
const commonConfig = require(./webpack.common.js)

const devConfig = {}
module.exports = merge(commonConfig, devConfig)

Code Splitting代码分割

假设要引入lodash,不分割会导致整个库打包至main.js
拆分后,业务逻辑变更插件不会重新加载

第一种方式

webpack

1
2
3
entry: {
lodash: './src/lodash.js'
}

src/loadsh.js

1
2
import _ from 'lodash'
window._ = _

第二种方式

自动做分割
src/index.js

1
2
import _ from 'lodash'

webpack

1
2
3
4
5
optimization: {
splitChunks: {
chunks: 'all'
}
}

异步加载

实验性语法,babel转换 npm install babel-plugin-dynamic-import-webpack --save-dev

.babelrc

1
plugins: ["babel-plugin-dynamic-import-webpack"]

index.js

1
2
3
4
5
function getComponents() {
return import('lodash').then(({default: _}) => {
// 异步加载
})
}

splitChunksPlugin

index.js

1
2
3
4
5
6
function getComponents() {
// 魔法注释 代码分割对lodash打包时起名为lodash
return import(/* webpackChunkName:"lodash" */ 'lodash').then(({default: _}) => {

})
}

npm uninstall babel-plugin-dynamic-import-webpack
npm i @babel/plugin-syntax-dynamic-import --save-dev

  • 使打包的文件前缀无vendors webpack
    1
    2
    3
    4
    5
    6
    7
    8
    9
    optimization: {
    splitChunks: {
    chunks: 'all',
    cacheGroups: {
    vendors: false,
    default: false,
    }
    }
    }

splitChunks默认选项

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
splitChunks: {
// 代码分割只对异步代码生效
chunks: "async",
// 对所有代码生效
chunks: 'all',
// 对同步代码生效
chunks: 'initial',

// 引入的模块大于30kb才做分割
minSize: 30000,

// 对分割的模块做50kb的分文件分割,一般不用
maxSize: 50000,

// 当模块用了多少次的时候才分割
minChunks: 1,

// 最大分割的模块数,只分割前5个
maxAsyncRequests: 5,

// 入口文件分割的模块数,最大3个
maxInitialRequests: 3,

// 文件生成的连接符
automaticNameDelimiter: '~',

// 分割的文件名在cacheGroups里定义是否有效
name: true,

// 缓存组,从上面的规则走完后模块缓存到缓存组内,然后分割
cacheGroups: {
// 当为所有代码生效时,此项意思为,对node_modules内的文件生效
// 打包后为vendors~main.js
// vendors为下面配置的组的名字(key)
// main意思是该分割出的文件的入口文件为main.js,对应webpack的entry配置项
vendors: {
test: /[\\/]node_modules[\\/]/,
// 值越大优先级越高,模块就会被分割到此文件内
priority: -10,
// 分割出的文件名为vendors.js
filename: 'vendors.js',
},
default: {
minChunks: 2,
priority: -20,
// A,B文件中A引用了B,但也有其他文件引用了B,则不会重复添加到分割文件中
reuseExistingChunk: true
}
}
}

LazyLoading懒加载

使用import('xxx')加载的代码,加载时间不固定,具体看触发时机

1
2
3
function getComponents() {
return import('lodash').then()
}

打包分析

analyse

package.json

1
2
3
4
5
6
"script": {
"dev-build": "webpack --config .....",
"//": "新加",
"//": "打包过程中,把打包的描述添加到stats里",
"dev-build": "webpack --profile --json > stats.json --config .....",
}

上传文件到

webpack.github.io/analyse/

可以看到分析

其他分析工具

https://webpack.js.org/guides/code-splitting/#bundle-analysis

webpack打包推荐JS写法

以前的写法

1
2
3
4
// 给元素添加click事件
document.addEventListener('click', () => {
// 干活
})

推荐写法 index.js

1
2
3
4
5
document.addEventListener('click', () => {
import('./click.js').then(func => {
func()
})
})

click.js

1
export default function handleClick() {}

查看代码复用率

控制台 -> Sources -> ctrl+shift+p -> 输入coverage

https://zhuanlan.zhihu.com/p/26281581

preloading\prefetching

https://webpack.js.org/guides/code-splitting/#prefetchingpreloading-modules

等核心代码加载完成后,页面空闲时加载异步代码

1
2
3
import(/* webpackPrefetch: true */ './click.js').then(func => {
func()
})

异步文件命名

webpack.common.js

1
2
3
4
5
6
7
8
9
10
11
// 入口文件走下面的filename配置
entry: {
main: './src/index.js'
}
// 省略
output: {
filename: '[name],js',
// 被入口文件引用的文件走下面的配置项
chunkFilename: '[name].chunk.js',
path: path.resolve(__dirname, '../dist')
}

css代码分割

https://webpack.js.org/plugins/mini-css-extract-plugin/

此插件可以对多入口文件打包,具体看官网文档
不对css做Tree Shaking

package.json

1
2
3
4
5
{
"sideEffects": [
"*.css"
]
}

wepack.common.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
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
// css压缩
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
// css压缩
optimization: {
minimizer: [new TerserJSPlugin({}), new OptimizeCSSAssetsPlugin({})],
},

module: {
rules: [
{
test: /\.css$/,
use: [
{
loader: MiniCssExtractPlugin.loader,
options: {
publicPath: '../',
hmr: process.env.NODE_ENV === 'development',
},
},
'css-loader',
],
},
],
},
plugins: [
new MiniCssExtractPlugin({
// 被页面直接引用的配置
filename: '[name].css',
// 被间接引用的配置
chunkFilename: '[id].css',
}),
],

浏览器缓存

文件源码不变,文件名不会变 webpack.prod.js

1
2
3
4
output: {
filename: '[name].[contenthash].js',
chunkFilename: '[name].[contenthash].js'
}

webpack3以下可能出现每次打包即使源码不变,hash也会变

webpack.common.js

1
2
3
4
5
optimization: {
runtimeChunk: {
name: 'runtime'
}
}

shimming 垫片

模块化导致写的模块引入的库可能不在package里
这里加入配置,可以自动加载

webpack.common.js

1
2
3
4
5
6
7
8
9
10
import webpack from 'webpack'

plugins: [
new webpack.ProvidePlugin({
// 命名: 库
$: 'jquery',
// 将lodash的join方法命名为_join
_join: ['lodash', 'join']
})
]

模块

1
2
3
import 'jquery'
// 自动完成命名
// $()...

改变模块的this

npm i imports-loader --save-dev

webpack.common.js

1
2
3
4
5
6
7
8
9
10
11
12
rules: [
{
test: /\.js$/,
// ……
use: [{
loader: 'babel-loader',
}, {
// 改变模块内的this为window
loader: 'imports-loader?this=>window'
}],
}
]

环境变量

webpack.common.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const merge = require('webpack-merge')
const devConfig = require('./webpack.dev.js')
const proConfig = require('./webpack.pro.js')

// webpack配置
const commonConfig = {}

module.exports = (env) => {
if (env && env.production) {
// 生产环境
return merge(commonConfig, proConfig)
} else {
// 开发环境
return merge(commonConfig, devConfig)
}
}

在打包命令里添加属性

package.json

1
2
3
"scripts": {
"build": "webpack --env.production --config"
}

也可以写
webpack --env.production=abc --config
这样的话环境变量就有值
env.production === abc

开发框架的打包方式

  • library npm init

  • package.json

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    {
    "name": "library",
    "version": "1.0.0",
    "description": "",
    "main": "index.js",
    "scripts": {
    "build": "webpack"
    },
    "author": "",
    "license": "MIT",
    "dependencies": {
    "webpack": "^4.35.2",
    "webpack-cli": "^3.3.5"
    }
    }
  • webpack.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
    28
    29
    const path = require('path')

    module.exports = {
    mode: 'production',
    entry: './src/index.js',
    // 库中引用了lodash,外部也用了loadsh则会打包两份,这里提取出来
    externals: ["lodash"],

    externals: {
    lodash: {
    // 如果库在commonjs环境使用,加载时必须为lodash不能为_
    commonjs: 'lodash',
    // 如果为script标签引入,则lodash全局变量为_
    root: '_'
    }
    },

    output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'library.js',
    // 打包的代码挂在到全局变量中
    // script标签引入,library全局变量
    library: 'library',
    // 通用导出,可被多种方式引入
    libraryTarget: 'umd',
    // 只标签导入,全局为this.library
    libraryTarget: 'this',
    }
    }
  • /src

  • math.js

    1
    export function add(a, b) { return a + b }
  • index.js

    1
    2
    3
    import * as math from './math'

    export default { math }

PWA

就算开启的http服务关掉,也能正常访问 npm install workbox-webpack-plugin --save-dev

webpack.prod.js

1
2
3
4
5
6
plugins: [
new WorkboxPlugin.GenerateSW({
clientsClaim: true,
skipWaiting: true
})
]

index.js

1
2
3
4
5
6
7
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/service-worker.js').then(registration => {
console.log('service-worker registed')
})
})
}

dev - server

http请求代理

webpack.config.js

1
2
3
4
5
6
devServer: {
proxy: {
// 将发送到/react/api下的请求转发到http://xxx.com
'/react/api': 'http://xxx.com'
}
}

如果测试为请求a.json,实际上线时需要请求b.json

webpack.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
devServer: {
proxy: {
'/react/api': {
target: 'http://xxx.com',
// 如果转发到https
secure: false,
pathRewrite: {
// 匹配到这里,如果需要a.json,则拿b.json
'a.json': 'b.json'
},
// 解决某些网站对changeOrigin的限制
changeOrigin: true
headers: {
host: '',
cookie: ''
}
}
}
}

单页面路由问题

如果不用router类的插件,访问a.com/b则会向后端请求此页面,从而找不到页面。

webpack.config.js

1
2
3
devServer: {
historyApiFallback: true
}

提高 webpack 打包速度

  1. 更新webpack、node版本
  2. 在尽可能少的模块上应用loader
  • include
  • exclude
  1. plugin尽可能精简,并确保可靠性
  • dev环境不需要代码压缩
  1. resolve参数
  • webpack.common.js
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    resolve: {
    // 当去目录找文件时,先找以js、jsx为结尾的
    // ./component
    // ./component.js
    // ./component.jsx
    extensions: ['.js', '.jsx'],
    // 当路径为文件夹时自动查找文件
    mainFiles: ['index'],
    // 引入别名
    alias: {
    components: path.resolve(__dirname, '../src/components')
    }
    }
  1. DLLPlugin webpack.dll.js
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    const path = require('path')
    const webpack = require('webpack')

    module.exports = {
    mode: 'production',
    entry: {
    // 对于哪些模块做dll打包
    vendors: ['react', 'react-dom']
    },
    output: {
    filename: '[name].dll.js',
    path: path.resolve(__dirname, '../dll')
    library: '[name]',
    },
    plugins: [
    new webpack.DllPlugin({
    name: '[name]',
    path: path.resolv(__dirname, '../dll/[name].manifest.json')
    })
    ]
    }

package.json

1
2
3
"scripts": {
"build:dll": "webpack --config ./build/webpack.dll.js"
}

npm i add-asset-html-webpack-plugin --save

webpack.common.js

1
2
3
4
5
6
7
8
9
10
plugin: [
// 在index.html里加入什么
new AddAssetHtmlWebpackPlugin({
filepath: path.resolve(__dirname, '../dll/vendors.dll.js')
}),
// 配合webpack,查找映射关系
new webpack.DllReferencePlugin({
manifest: path.resolve(__dirname, '../dll/vendors.manifest.json')
})
]

打包出的dll会在全局变量vendors里,其他代码从此变量获取

当模块很多时,可能会有多个dll文件 webpack.common.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const plugins = []
const fs = require('fs')
// 拿到目录下所有的文件列表
const files = fs.reddirSync(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 webpack.DllReferencePlugin({
manifest: path.resolve(__dirname, '../dll' + file)
})
)
}
})
  1. 控制包文件大小 对代码拆分,或去除不用的模块

  2. 多进程打包

  • thread-loader
  • parallel-webpack
  • happypack
  1. sourceMap 分环境打包

  2. 结合stats分析打包 webpack自带

编写Loader

这里的例子是替换字指定字符串中的文字

webpack.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
28
29
const path = require('path')

module.exports = {
mode: 'development',
entry: {
main: './src/index.js'
},
resolveLoader: {
// 使用loader时先去node_modules里找,然后去loaders里找
modules: ['node_modules', './loaders']
},
module: {
rules: [{
test: /\.js/,
use: [
{
loader: 'replaceLoader',
options: {
name: 'naaaame'
}
}
]
}]
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js'
}
}

/src/index.js

1
console.log('aaaaaa')

/loaders/replaceLoader.js

1
2
3
4
5
6
7
8
9
10
11
12
13
const loaderUtils = require('loader-utils')

// 这里不能使用箭头函数
module.exports = function (source) {
const options = loaderUtils.getOptions(this)
const callback = this.async()

// 异步返回方式
setTimeout(() => {
const result = source.replace('a', options.name)
callback(null, result)
})
}

更多配置查看官方文档

编写Plugin

这个例子是在打包最后放置文件时,创建一个新的文件

webpack.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const path = require('path')
const CopyrightWebpackPlugin = require('./plugins/copyright-webpack-plugin')

module.exports = {
mode: 'development',
entry: {
main: './src/index.js'
},
plugins: [
new CopyrightWebpackPlugin({
name: 'naaaame'
})
],
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js'
}
}

/plugins/copyright-webpack-plugin.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
class CopyrightWebpackPlugin {
constructor(options) {
// 参数
console.log(options);
}

apply(compiler) {

// 生命周期,此为同步时间点
compiler.hooks.emit.tap('CopyrightWebpackPlugin', () => {
// do
})

// 调试
debugger

// 生命周期,此为异步时间点
compiler.hooks.emit.tapAsync('CopyrightWebpackPlugin', (compilation, cb) => {
compilation.assets['copyright.txt'] = {
source: function() {
return '文件内容'
},
size: function () {
// 文件字节长度
return 12
}
}
cb()
})
}
}

module.exports = CopyrightWebpackPlugin

package.json

1
2
3
4
"scripts": {
"debug": "node --inspect --inspect-brk node_modules/webpack/bin/webpack.js",
"build": "webpack"
},

写一个类似Webpack的工具

依赖包

yarn add @babel/parser
yarn add @babel/traverse
yarn add @babel/core yarn add @babel/preset-env

文件结构

  • /src/index.js
  • /src/message.js
  • /src/word.js
  • /bundler.js

文件内容

/src/index.js

1
2
import message from './message.js';
console.log(message);

/src/message.js

1
2
3
import { word } from './word.js';
const message = `say ${word}`
export default message

/src/word.js

1
2
const word = 'hello'
export { word }

编译文件 /bundler.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
// 获取文件信息
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'
})

const dependencies = {}

// 找到import语句
traverse(ast, {
// 需要提取的语法作为函数名,之后会被执行
ImportDeclaration({ node }) {
// 根目录的绝对路径
const dirname = path.dirname(filename)
const newFile = './' + path.join(dirname, node.source.value)
// 依赖的文件
dependencies[node.source.value] = newFile
}
})

// AST转浏览器运行的代码
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 (const key in dependencies) {
if (dependencies.hasOwnProperty(key)) {
graphArray.push(moduleAnalyser(dependencies[key]))
}
}
}
}

const graph = {}
graphArray.forEach(item => {
const { dependencies, code } = item
graph[item.filename] = {
dependencies,
code
}
})
return graph
}

const generateCode = entry => {
const graph = JSON.stringify(makeDependenciesGraph(entry))

// 避免污染全局,用闭包
return `
(function(graph) {
function require(module) {
// 相对路径转换
function localRequire(relativePath) {
return require(graph[module].dependencies[relativePath])
}

// 记录导出的结果
var exports = {}

function init(require, exports, code) {
// 重写内部的require,返回绝对路径
eval(code)
}
init(localRequire, exports, graph[module].code)

return exports
}

require('${entry}')

})(${graph})
`
}

const code = generateCode('./src/index.js')

eval(code)

输出

node .\bundler.js
输出为解析index.js文件后的代码,浏览器可直接执行