10天彻底搞定webpack4.0-笔记(1)

npm script 命令给 webpack 传参

1
npm run build -- --config webpack.config.js

加上两个杠,表示后面是字符串,是参数。

dev-server

1
2
3
devServer:{
contentBase:'./build/' ,// 指定server启动的目录
}

Html-webpack-plugin

1
2
3
4
5
6
7
8
9
10
new HtmlWebpackPlugin({
template:'',
filename:'',
hash:tue, //在每个html后面加上hash
//压缩html
minify:{
removeAttributeQuotes:true // 删除属性值上的双引号
collapseWhitespace:true,// 压缩html成一行
}
})

loader

css-loader 主要是解决@import、图片路径等这些语法

1
2
3
4
5
6
7
8
9
10
11
{
test: /\.css$/;
use: [
{
loader: "style-loader",
options: {
insertAt: "top", // 设置style标签插入的地方
},
},
];
}

抽离 css

使用插件mini-css-extract-plugin

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const MniCssExtractPlugin = riquire("mini-css-extract-plugin");

plugins: [
new MiniCssExtractPlugin({
filename: "main.css",
}),
];

//这是就不要放到style里面,所以style-loader就不要了
{
test: /\.css$/;
use: [
// css-loader执行后执行minicssextractplugin的loader
MiniCssExtractPlugin.loader,
"css-loader",
];
}

css 自动加前缀

使用 autoprefixer 插件,但是需要 postcss-loader 对它进行解析。
在 css-loader 之前 less-loader 使用 postcss-loader

压缩 css

默认情况下,webpack 使用uglifyjs-webpack-plugin插件在生产环境打包时已经压缩了 js,但如果使用了mini-css-extract-plugin,则需要自己单独再配置压缩优化相关项,可查看官网
压缩 js 可以使用uglifyjs-webpack-plugin,也可以使用下面的。压缩 css 需要插件optimize-css-assets-webpack-plugin

1
2
3
4
5
6
7
8
9
10
11
const TerserJSPlugin = require('terser-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');


module.exports = {
optimization: {// 优化项
minimizer: [new TerserJSPlugin({}), new OptimizeCSSAssetsPlugin({})],
},
....
}

转换 es6 语法

babel-loader
babel-core
babel/preset-env

@babel/plugin-transform-runtime 和 @babel/runtime 解决更高级语法 api 没有的问题,比如 generator

js 规范

使用 eslint-loader 和 eslint
如果只是使用 eslint,帮助在开发过程中进行检测,会提示文件中语法不符合规则的部分。但是在打包的时候不会做校验。使用 eslint-loader,在 webpack 进行打包的时候进行校验。
eslint-loader 通常单独配置,不建议和 bable 等其他处理 js 的 loader 写在一块。可以通过enforce设置执行顺序,

1
2
3
4
5
6
 {
enforce: "pre",//在其他loader之前执行,使用post在其他loader之后执行
test: /\.js$/,
exclude: /node_modules/,
loader: "eslint-loader"
},

暴露第三方模块给全局

比如我们安装了 jquery,我们可以在每个模块中 import 进来,然后使用,这样是没问题的,但是如果想通过 window 访问这些包,就不行了,可以通过下面的方法来实现。

下面简单说下 loader 的分类:

  • pre 前置 loader,前面执行的 loader
  • normal 普通的 loader
  • 内联 loader
  • post 后置 loader

比如使用expose-loader

  1. 通过内联 loader 的方式来实现把 jquery 暴露给 window。
1
2
3
import $ from "expose-loader?$!jquery";

console.log(window.$); // window上存在$

但是上面的写法不美观,可以在配置文件中配置。

  1. 在配置文件中配置
1
2
3
4
5
6
7
8
{
test:require.resolve('jquery'),
use:'expose-loader?$'
}

//使用
import $ from 'jquery'
console.log(window.$)

不用引用直接使用呢?

  1. 在每个模块中注入$对象
    通过webpackProviderPlugin插件
1
2
3
4
5
6
7
8
9
10
11
const webpack = require("webpack");

plugins: [
// 在每个模块中都注入
new webpack.ProviderPlugin({
$: "jquery",
}),
];

//因为是注入到每个模块中的,所以在每个模块中可以直接使用,但window.$不存在
console.log($);

不打包指定模块

比如在index.html中手动引入了jquery包,那就不需要对jquery进行打包。通过设置webpackexternals属性可以实现该功能。

1
2
3
externals:{
jquery:"$"
}

即使在代码中使用 import 的方式引入,也不会进行打包。

图片处理

file-loader 默认会在内部生成一张图片到输出目录下,把生成的图片的名字返回回来。 ### 在 html 中使用 img
因为 webpack 会对模块中引用的 img 进行打包转换等处理,所以如果直接在 index.html 中使用 img 是识别不了的。如下

1
<img src="./logo.png" />

因为打包后原来的 logo.png 的名字和路径都发生了变化,所以这种写法是肯定拿不到的。
使用html-withimg-loader可以解决这个问题。

1
2
3
4
{
test: /\.html/;
use: "html-withimg-loader";
}

小图片的处理

使用url-loader可以使用 base64 的方式减少对图片的请求。

资源分类

  1. 图片
1
2
3
4
5
6
7
8
9
10
{
test:/\.(jpg|png|jpeg|svg)/
use:{
lader:'url-loader',
options:{
limit:2*1024,
outputPath:'img/'
}
}
}
  1. css
1
2
3
new MiniCssExdtractPlugin({
filename: "css/main.css",
});

就是在各个资源前面加上文件夹名即可

如果需要在每个资源前面统一加上一个前缀,比如 cdn,可以配置 webpack 输出路径的 publicpath

1
2
3
4
5
6
7
{
output:{
filename:'bundle.js'.
path:path.resolve(__dirname,'build'),
publicPath:'https://www.baiducdn.com/'
}
}

如果需要在不同的资源前面家不同的 cdn,可以在各自 loader 上加上 publicPath

1
2
3
4
5
6
7
8
9
10
11
{
test:/\.(jpg|png|jpeg|svg)/
use:{
lader:'url-loader',
options:{
limit:2*1024,
outputPath:'img/',
publicPath:'https://www.baidu.com/'
}
}
}

多页面应用

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
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const webpack = require("webpack");

const config = {
mode: "development",
entry:{
home:'./src/home.js',
other:'./src/other.js'
},
output: {
path: path.join(__dirname, "dist/"),
//输出时只要根据每个入口的名字输出就行
filename: "js/[name].js",
},
plugins:[
// 模板输出html文件无法使用入口定义的名字,
//只能使用多个模板实例来解决多个的问题。
//但需要使用chunk来指定加载打包的js
new HtmlWebpackPlugin({
template:'./home.html',
filename:'home.html',
chunks:['home']
});
new HtmlWebpackPlugin({
template:'./home.html',
filename:'home.html',
chunks:['home']
});
]
}

代码映射

配置了 sourcemap 不仅仅是开发的时候有映射,生产环境也会有(当然生产环境不配置就没有了)。

1
2
3
{
"devtool": "source-map"
}
  • source-map
    源码映射,会单独生成一个 source map 文件,出错了会列出报错的列和行号。
  • eval-sorce-map
    不会产生单独的文件,但是可以显示出错的行和列;并且在浏览器中也能看到源码。eval 标明,这种方式是使用 eval 的方式把源码生成字符串,集成在打包后的文件中,然后 eval 出来。
  • cheap-module-source-map
    不显示出错的行和列,但是生成一个单独的文件,并且和调试没有关联起来。
  • cheap-module-eval-source-map
    不生成文件,集成在打包后的文件中, 也不会产生列

监控

1
2
3
4
5
6
7
8
{
"watch": true,
"watchOptions": {
"poll": 100, // 每秒检测的次数
"aggregateTimeout": 500, // 防抖,500毫秒内的变化
"ignored": "/node_modules/"
}
}

小插件

  • cleanWebpackPlugin
    每次打包,先清空指定目录中的旧文件。
1
new cleanWebpackPlugin("./dist");
  • copyWebpackPlugin
    打包的时候,有些静态资源可能不会被打包,但也是需要放到打包目录中,使用这个插件,就可以实现该功能。
1
new copyWebpackPlugin([{ from: "doc", to: "/" }]);
  • bannerPlugin(内置的 )
    给打包后的代码加上版权信息。
1
new webpack.BannerPlugin("make by wenmu 2019 ");

跨域问题

就是设置devServerproxy

1
2
3
4
5
devServer:{
proxy:{
'/api':"http://localhost:4567"
}
}

上面就把以/api开头的请求链接都转到http://localhost:4567的服务上。比如/api/user/getDetail,发出的请求就是http://localhost:4567/api/user/getDetail
如果服务端的链接不带/api怎么办?

1
2
3
4
5
6
7
8
devServer: {
proxy: {
'/api': {
target: 'http://localhost:3000',
pathRewrite: {'^/api' : ''}
}
}
}

前端自己 mock 数据,不借助服务端

我们知道,其实 devServer 本身就是一个 node 服务。我们可以使用这个服务直接处理请求。这需要借助 devServer 的 before 钩子函数

1
2
3
4
5
6
7
devServer: {
before: function(app, server) {
app.get('/some/path', function(req, res) {
res.json({ custom: 'response' });
});
}
}

app就是devServer中的express实例。
如果一个一个的请求都写到这里面,这个文件会比较大,所以可以把相关的请求函数封装到一起,在这里调用即可。通常使用第三方封装的组件mocker-api帮助 mock 数据。mocker-api就是这个原理。

服务端 node 启动 webpack

使用node启动webpack,这样在node中写的mock数据就和webpack启动的前端是一个端口,就不存在跨域问题了。
在服务端中启动webpack,需要使用webpack的中间件webpack-dev-middleware

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const express = require("express");
const webpack = require("webpack");
const middleware = require("webpack-dev-middleware");
const config = require("./webpack.config.js");

const compiler = webpack(config);
const app = new express();
app.use(middleware(compiler));

app.get("/user", (req, res) => {
res.json({ name: "test data." });
});

app.listen(3000);

resolve

resolve 的作用就是解析,项目中的各种解析都可以在这里做相关的配置。

  • 第三方包的查找
    默认情况下,当引入一个包时,首先在node_modules中查找,如果找不到就再去全局包安装目录查找,再找不到就报错了。我们可以通过配置 modules来强制指定只在哪些目录中进行查找,比如只在node_modules
1
2
3
4
resolve:{
modules:[path.resolve('node_modules')]
},
devServer:{}
  • 别名
    如果一个路径的比较长,引用起来比较麻烦,可以通过在 resolve 中对路径设置别名来解决。
    比如应用 bootstrap 的样式,bootstrap/dist/css/bootstrap.css
1
2
3
4
5
resolve: {
alias: {
BootstrapStyle: "bootstrap/dist/css/bootstrap.css";
}
}

这是可以直接引用BootstrapStyle

1
import BootstrapStyle;
  • 优先读取的 package 字段
    在引用一个包时,默认是读取包中package.jsonmain字段配置的文件,如下是bootstrappackage.json,当import bootstrap时,读取的是 js 文件。
1
2
style:'/dist/css/bootstrap.css',
main:'/dist/js/bootstrap.js'

我们可以设置引入包时,优先读取的字段,比如优先读取style字段,找不到了再读取main字段对应的文件。

1
2
3
4
resolve: {
mainFields: ['style', 'main'],
mainFiles:['index'] // 入口文件的名字
}

这样当import bootstrap时,bootstrap 的样式就被加载了。

  • 默认扩展名
    在引入一些文件时,比如 js,后缀名是不需要写的,但是 css 的后缀是必须写的,我们是可以设置一些类型不需要些扩展名的
1
2
3
resolve: {
extensions: [".js", ".css", ".jsx", ".json"];
}

引入一个文件时,会先找 js,找不到再找 css,依次 jsx、json 等等。当匹配到一个时,就不会继续再匹配了。因此存在多种类型的文件使用一个文件名时,当引入这个文件名时,只会加载设置的第一个类型。如果引用 css,还是需要加上后缀。

环境变量

webpack 提供了一个插件可以帮助我们定义一些环境变量;webpack.DefinePlugin

1
2
3
4
5
new webpack.DefinePlugin({
NAME: "'wenmu'",
DEV: JSON.stringify(process.env.NODE_ENV),
Express: "1+1",
});

这样上面定义的变量就可以直接使用了。
细心的同学发现,为什么NAME的值要用双引号把包括单引号呢?
因为在读取变量时,会把后面单引号中的直接复制给变量,那读取上面的NAME就成了wenmu,而不是'wenmu'那就会报错,说 wenmu 未定义了,当成一个变量了。比如 Express 就是 2,不是1+1字符串。
可以使用双引号再包裹一下,但推荐使用第二种的写法,可读性强。

区分不同的环境

通常开发和生产使用的 webapck 配置文件是不一样的,但是大部分的配置是一样的,因此写一个 base 文件,再写两个开发和生产的“继承”base 即可。继承就用到了webpack-merge.

1
2
3
4
5
6
const { smart } = require("webapck-merge");
const base = require("./webapck.base.js");

modules.export = smart(base, {
mode: "production",
});

webpack 优化

  1. noParse
    noParse 指定不需要解析的包,当一个包比较大,并且没有依赖其他包时,可以使用 noParse 指定,这样可以减少打包时间。
1
2
3
4
5
6
module.exports = {
//...
module: {
noParse: /jquery|lodash/,
},
};
  1. 设置 loader 解析的范围
1
2
3
4
5
6
7
8
9
10
11
12
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/, //排除不需要解析的
include: path.resolve("src"), // 设置需要解析的
use: {
loader: ["babel-loader"],
},
},
];
}
  1. ignorePlugin
    在使用一个写第三方包时,包可能会引入很多文件,但是一些文件可能对我们的项目是没用的。
    比如有一个 moment,这个包做了国际化处理,默认它会把所有的语言包都加载,如果我们只使用了中文,这样打包的时候就比较大,这时可以使用 webpack 提供的 IgnorePlugin 来指定这个包哪些文件不需要加载。
1
plugins: [new webpack.IgnorePlugin(/\.\/locale/, /moment/)];

这时国际化就不管用了,中文的语言包需要手动引入。

1
2
3
import "moment/locale/zh-cn";

moment.locale("zh-cn");

动态链接库

默认在打包时,所有的包都会被到包在一起,包括第三方库,这样每次打包的时候,第三方库和有些公共的文件也需要每次都重新进行打包,这样不仅打出的包比较大,并且打包会比较慢。
因此我们可以把第三方库和一些公共的文件抽离出来,先单独进行打包。比如 react、react-dom 等等。然后开发的时候引用我们单独打好的包,这样每次打包的时候,单独抽离出来的包就不会再次进行打包了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/******/ var abc= (function(modules) { // webpackBootstrap
/******/ // The module cache
/******/ var installedModules = {};
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
....
/******/ return module.exports;
/******/ }
/******/ ....
/******/ // Load entry module and return exports
/******/ return __webpack_require__(__webpack_require__.s = "./src/aaa.js");
/******/ })
/******/ ({
/***/ "./src/aaa.js":
!*** ./src/aaa.js ***!
/*! no static exports found */
/***/ (function(module, exports) {
eval("module.exports = \"this is a value\";\n\n\n//# sourceURL=webpack:///./src/aaa.js?");
/***/ })
/******/ });

上面是打包后的代码结构,大部分删除了。var abc=是手动写上的。

简单讲解下打包后文件的代码结构。
 一个模块打包后,原来的模块会被封装成一个立即执行 function,js 中其实类或模块都是 function 的语法糖吗,这个 function 会把打包的模块做一个对象返回。我们在引用模块的时候,其实就是接受这个 function 的返回值。
 我们把这个函数的返回值赋给一个变量,然后

基于上面的讲解,我们可以在配置文件中指定,接收打包后返回值的变量名。

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

module.exports = {
mode: "development",
entry: {
test: "./src/aaa.js",
},
output: {
filename: "[name].js",
path: path.resolve(__dirname, "dist"),
library: "abc", // 接收的变量名
// 指定接收的方式,可以是变量的方式var(默认 ),也可以是commonjs,amd等其他方式。
libraryTarget: "var",
},
};

加上library的配置后,上面的手动写的var abc=1就不用手动写了,打包出来的就是这样的。

动态链接库就是基于上面的原理来做的,只是我们需要打包的是 react、react-dom 等包。

  1. 如何让 webpack 知道上面生成的文件就是动态链接库呢?
    这就需要用到webpack的自带插件DllPlugin,它能指定把哪个文件打包成动态链接库,并且用一个清单(manifest.json)的方式进行管理。manifest.json记录了如何查找动态链接库中的文件。
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: "development",
entry: {
react: ["react", "react-dom"],
},
output: {
filename: "_dll_[name].js",
path: path.resolve(__dirname, "dist"),
library: "_dll_[name]",
},
plugins: [
new webpack.DllPlugin({
// name==library
name: "_dll_[name]",
path: path.resolve(__dirname, "dist", "manifest.json"),
}),
],
};

使用 webpack 编译后,会生成一个_dll_react.jsmanifest.json两个文件。

  • _dll_react.js中就是打包后的reactreact-dom
  • manifest.json,记录了_dll_react中的依赖关系。这个主要是被DllReferencePlugin使用。

打包完以后,就需要在入口 index.html 文件中引用。

1
<script src="/_dll_react.js "></script>

虽然引用了,但是打包的时候,react、react-dom 默认还是去node_modules中找,找到了然后就又被打包了,那怎么办,上面的工作不是白做了吗,这就需要用到webpack的另一个插件DllReferencePlugin

1
2
3
new webpack.DllReferencePlugin({
manifest: path.resolve(__dirname, "dist", "manifest.json"),
}),

注意:这个和DllPlugin不在一个配置文件中,这个是开发打包使用的webpack配置文件,而DllPlugin是专门用来提取的公共包的webpack配置文件。

这样配置后,打包的时候会先去清单中查找包,找不到了再把包打包进当前的代码中。这样打包出来的文件往往很小,因为公用包都已经在_dll_react.js中了

多线程打包

多现成打包主要用到了happypack插件。这个很简单,主要就是把原来的 loader 换成 happypack 的 loader,然后在 plugin 中配置 happypack 即可。

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
exports.plugins = [
new HappyPack({
id: "jsx",
threads: 4,
loaders: ["babel-loader"],
}),

new HappyPack({
id: "styles",
threads: 2,
loaders: ["style-loader", "css-loader", "less-loader"],
}),
];

exports.module.rules = [
{
test: /\.js$/,
use: "happypack/loader?id=jsx",
},

{
test: /\.less$/,
use: "happypack/loader?id=styles",
},
];

非常 easy.

webpack 自带的优化功能

  1. tree-shaking
    当我们使用 import 的方式引用一个模块时,如果只是引用了模块的部分方法,那在生产环境打包的时候,那些没有引用的方法会被删除掉。不会被打包进来。
    但当使用 require 的方式引用模块时不会删除没用到的代码。
    这是由requireimport的机制相关的。
  2. scope hosting 作用域提升
1
2
3
4
let a = 1;
let b = 2;
let c = 3;
console.log(a + b + c);

上面的代码打包的时候,不会把 a、b、c 都打包进来,打包进来的是一个结果,console.log(6),类似的模块引用等都会做类似的处理。

抽取公共代码

抽取公共代码,是当有多个页面的时候(不都是单页面应用 ),多个个页面之间有公共的模块引用,打包的时候,默认会把公用的包在每个引用的页面中都打一份,造成公共代码的重复;
比如现在有多个入口

1
2
3
4
entry: {
index: './src/index.js',
other:'./src/other.js',
},

当 index.js 和 other.js 中引用的有相同的模块时,可以把这些公用的模块抽离出来,这样打包的时候每个页面只有对公共模块的引用,不会在每个包中都包含一份,并且在访问 index 的时候就会被缓存,再访问 other 的时候,就不需要再下载了,从而提高性能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
module.exports = {
optimization: {
splitChunks: {
// 分割代码块
cacheGroups: {
// 缓存组
common: {
minChunks: 2,
chunks: "initial", //只优化同步加载的模块
reuseExistingChunk: true,
},
},
},
},
};

上面是一个很简单的配置,可以去官网查看比较全面的配置,这里需要讲解一下chunks的字段含义。
chunks有三个可选值,allinitialasync

  • async:指定抽离动态引入的公共模块
  • initial: 指定抽离非动态引入的公共模块
  • all: 所有符合条件的公共模块都进行抽离
    在引入模块时,通常都是非动态的引入,比如import $ from 'jquery';有时候也需要动态的引入一些模块,比如:import ('lodash')

抽离第三方模块

上面讲解了抽离公用的模块,但没有区分是第三方包还是程序中开发的,我们可以把这两种情况分开,把第三方包重复使用的抽离到一起,自己开发的抽离到一起。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
module.exports = {
optimization: {
splitChunks: {
// 分割代码块
cacheGroups: {
// 缓存组
common: {
minSize: 0,
minChunks: 2,
chunks: "initial", //只优化同步加载的模块
reuseExistingChunk: true,
priority: 1,
},
vendors: {
test: /[\\/]node_modules[\\/]/,
minSize: 0,
minChunks: 2,
chunks: "initial", //只优化同步加载的模块
priority: 2,
},
},
},
},
};

其实就是再配置一个抽离方案,但要注意加上priority,因为程序是从上往下执行的,如果不加priority,下面的方案就不会被执行。
我们需要让vendors的权重比common的大,才能先把第三方的抽离,然后再抽离公共的。当然,也可以把vendors放到第一个。总之把条件范围小的权重大点,让先执行。

懒加载

懒加载说白就是使用 es6 草案的import ('jquery')语法动态的加载 js。

热更新

1
2
3
4
5
6
7
8
9
10
{
devServer: {
hot: true,
}
// ...
plugins: [
new webpack.NamedModulesPlugin(), // 打印更新的模块路径
new webpack.HotModuleReplacementPlugin() // 热更新插件
]
}

tapable

Webpack本质上是一种事件流的机制,它的工作流程就是将各个插件串联起来,而实现这一切的核心就是TapableTapable有点类似于nodejsevents库,核心原理也是依赖订阅发布模式

  • SyncHook
    这个比较简单,订阅的钩子一个一个都执行。

  • syncBailHook
    任何一个钩子的返回值不是undefined,后面的钩子就不执行了。

  • SyncWaterfallHook
    上一个钩子的返回结果传给下一个。

  • SyncLoopHook
    钩子的返回值不是undefined,就一直执行。
    上面三个都是同步的 Hook,下面介绍几个异步的 Hook。

  • AsyncParallelHook
    异步并行的钩子,当所有的异步都执行完后,才执行最后的回调。

    这提供了判断“所有异步都结束”的方案,就是在每个钩子的回调函数必须接收一个回调的参数,这个回调的参数就是一个计数的 function,每个钩子结束后必须执行这个计数的 function。当计数和钩子的个数相等时,说明所有异步钩子执行完了。
    现在使用`promise.all 实现起来就更方便了。

  • AsyncSeriesHook
    异步串行,当上一个异步执行完以后,再执行第二个异步钩子。
    promise 版本实现原理

1
2
3
4
5
6
promise(...args){
let [first,...others]=this.tasks;
return others.reduce((p,n)=>{
return p.then(()=>n(...args))
},first(...args))
}

就是使用数组的 reduce 方法和 promise 返回是仍是 promise 的特性。redux 的源码也是这个原理。

  • AsyncSeriesWaterfallHook
    异步串行瀑布,上一个异步执行完以后再执行下一个,但是上一个钩子的结果传给下一个,如果有错误,则停止后 面钩子的执行。
文章作者: wenmu
文章链接: http://blog.wangpengpeng.site/2020/01/09/10%E5%A4%A9%E5%BD%BB%E5%BA%95%E6%90%9E%E5%AE%9Awebpack4-0-%E7%AC%94%E8%AE%B0-1/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 温木的博客
微信打赏