如何提高webpack的编译速度?
ly2011 opened this issue · 2 comments
如何提高webpack的编译速度?
缩小文件搜索范围
优化 loader
配置
在module中,在使用 loader 时,可以通过 test
、include
、exclude
三个配置来命中 loader 要应用规则的文件。
以采用 ES6 的项目为例,在配置 babel-loader 时,可以这样:
module.exports = {
module: {
rules: [
{
// 如果项目源码中只有 js 文件就不要写成 /\.jsx?$/,提升正则表达式性能
test: /\.js$/,
// babel-loader 支持缓存转换出的结果,通过 cacheDirectory 选项开启
use: ['babel-loader?cacheDirectory'],
// 只对项目根目录下的 src 目录中的文件采用 babel-loader
include: path.resolve(__dirname, 'src'),
},
]
},
};
优化 resolve.modules
的配置
resolve.modules
用于配置 webpack 去哪些目录下寻找第三方模块。
resolve.modules
的默认目录是 [node_modules]
,含义是先去当前目录下的 ./node_modules 目录下去找想找的模块,如果没找到就去上一级目录 ../node_modules 中找,再没有就去 ../../node_modules 中找,以此类推,这和 Node.js 的模块寻找机制很相似。
当安装的第三方模块都放在项目根目录下的 ./node_modules 目录下时,没有必要按照默认的方式去一层层的寻找,可以指明存放第三方模块的绝对路径,以减少寻找,配置如下:
module.exports = {
resolve: {
// 使用绝对路径指明第三方模块存放的位置,以减少搜索步骤
// 其中 __dirname 表示当前工作目录,也就是项目根目录
modules: [path.resolve(__dirname, 'node_modules')]
},
};
优化 resolve.mainFields
配置
resolve.mainFields
用于配置第三方模块使用哪个入口文件。
安装的第三方模块中都会有一个 package.json
文件用于描述这个模块的属性,其中有些字段用于描述入口文件在哪里, resolve.mainFields
用于配置采用哪个字段作为入口文件的描述。
可以存在多个字段描述入口文件的原因是因为有些模块可以同时用在多个环境,针对不同的运行环境需要不同的代码。 以 isomorphic-fetch 为例,它是 fetch API 的一个实现,但可同时用于浏览器和 Node.js 环境。 它的 package.json 中就有2个入口文件描述字段:
{
"browser": "fetch-npm-browserify.js",
"main": "fetch-npm-node.js"
}
resolve.mainFields
的默认值和当前的 target
配置有关系,对于关系如下:
- 当
target
为web
或者webworker
时,值是['browser', 'module', 'main']
- 当
target
为其他情况时,值是['module', 'main']
以 target
等于 web 为例,Webpack 会先采用第三方模块中的 browser
字段去寻找模块的入口文件,如果不存在就采用 module
字段,以此类推。
为了减少搜索步骤,在你明确第三方模块的入口文件描述字段时,你可以把它设置的尽量少。 由于大多数第三方模块都采用 main 字段去描述入口文件的位置,可以这样配置 Webpack:
module.exports = {
resolve: {
// 只采用 main 字段作为入口文件描述字段,以减少搜索步骤
mainFields: ['main'],
},
};
使用本方法优化时,你需要考虑到所有运行时依赖的第三方模块的入口文件描述字段,就算有一个模块搞错了都可能会造成构建出的代码无法正常运行。
优化 resolve.alias
配置
resolve.alias
配置项通过别名来把原导入路径映射成一个新的导入路径。
在实战项目中,经常会依赖一些庞大的第三方模块,以React库为例,安装到 node_modules
目录下的 React 库的目录结构如下:
├── dist
│ ├── react.js
│ └── react.min.js
├── lib
│ ... 还有几十个文件被忽略
│ ├── LinkedStateMixin.js
│ ├── createClass.js
│ └── React.js
├── package.json
└── react.js
可以看到发布出去的 React 库中包含两套代码:
- 一套是采用 CommonJS 规范的模块化代码,这些文件都放在
lib
目录下,以package.json
中指定的入口文件react.js
为模块的入口。 - 一套是把 React 所有相关的代码打包好的完整代码放到一个单独的文件中,这些代码没有采用模块化可以直接执行。其中
dist/react.js
是用于开发环境,里面包含检查和警告的代码。dist/react.min.js
是用于线上环境,被最小化了。
默认情况下 Webpack 会从入口文件./node_modules/react/react.js
开始递归的解析和处理依赖的几十个文件,这会时一个耗时的操作。 通过配置resolve.alias
可以让 Webpack 在处理 React 库时,直接使用单独完整的react.min.js
文件,从而跳过耗时的递归解析操作。
相关 webpack 配置如下:
module.exports = {
resolve: {
// 使用 alias 把导入 react 的语句换成直接使用单独完整的 react.min.js 文件,
// 减少耗时的递归解析操作
alias: {
'react': path.resolve(__dirname, './node_modules/react/dist/react.min.js'),
}
},
};
除了 React 库外,大多数库发布到 Npm 仓库中时都会包含打包好的完整文件,对于这些库你也可以对它们配置 alias。
但是对于有些库使用本优化方法后会影响到后面要讲的使用 Tree-Shaking 去除无效代码的优化,因为打包好的完整文件中有部分代码你的项目可能永远用不上。 一般对整体性比较强的库采用本方法优化,因为完整文件中的代码是一个整体,每一行都是不可或缺的。 但是对于一些工具类的库,例如 lodash,你的项目可能只用到了其中几个工具函数,你就不能使用本方法去优化,因为这会导致你的输出代码中包含很多永远不会执行的代码。
优化 resolve.extensions
配置
在导入语句没有带文件后缀时,webpack会自动带上后缀去尝试询问文件是否存在。 resolve.extensions
用于配置在尝试过程中用到的后缀列表,默认是:
extensions: ['.js', '.json']
也就是说当遇到 require('./data')
这样的导入语句时,Webpack 会先去寻找 ./data.js
文件,如果该文件不存在就去寻找 ./data.json
文件,如果还是找不到就报错。
如果这个列表越长,或者正确的后缀在越后面,就会造成尝试的次数越多,所以 resolve.extensions
的配置也会影响到构建的性能。 在配置 resolve.extensions
时你需要遵守以下几点,以做到尽可能的优化构建性能:
- 后缀尝试列表要尽可能的小,不要把项目中不可能存在的情况写到后缀尝试列表中。
- 频率出现最高的文件后缀要有限放在最前面,以做到尽快的退出寻找过程。
- 在源码中写导入语句时,要尽可能的带上后缀,从而可以避免寻找过程。例如在你确定的情况下把
require('./data')写成require('./data.json')
相关 webpack 配置如下:
module.exports = {
resolve: {
// 尽可能的减少后缀尝试的可能性
extensions: ['js'],
},
};
优化 module.noParse
配置
module.noParse
配置项可以让 webpack 忽略对部分没有采用模块化的文件的递归解析处理,这样做的好处是能提高构建性能。原因是一些库,例如 jQuery、ChartJS,它们庞大又没有采用模块化标准,让 webpack 去解析这些文件耗时又没有意义。
在上面的优化 resolve.alias
配置中讲到单独完整的 react.min.js
文件就没有采用模块化,让我们来通过配置 module.noParse
忽略对 react.min.js
文件的递归解析处理,相关的webpack配置如下:
const path = require('path');
module.exports = {
module: {
// 独完整的 `react.min.js` 文件就没有采用模块化,忽略对 `react.min.js` 文件的递归解析处理
noParse: [/react\.min\.js$/],
},
};
注意被忽略掉的文件里不应该包含 import 、 require 、 define 等模块化语句,不然会导致构建出的代码中包含无法在浏览器环境下执行的模块化语句。
使用 DllPlugin
要给 Web 项目构建接入动态链接库的**,需要完成以下事情:
- 把网页依赖的基础模块抽离出来,打包到一个个单独的动态链接库中去。一个动态链接库中可以包含多个模块。
- 当需要导入的模块存在于某个动态链接库中时,这个模块不能被再次被打包,而是去动态链接库中获取。
- 当需要导入的模块存在于某个动态链接库中时,这个模块不能被再次被打包,而是去动态链接库中获取。
为什么给 Web 项目构建接入动态链接库的**后,会大大提升构建速度呢? 原因在于包含大量复用模块的动态链接库只需要编译一次,在之后的构建过程中被动态链接库包含的模块将不会在重新编译,而是直接使用动态链接库中的代码。 由于动态链接库中大多数包含的是常用的第三方模块,例如 react
、react-dom
,只要不升级这些模块的版本,动态链接库就不用重新编译。
接入 webpack
webpack 已经内置了对动态链接库的支持,需要通过2个内置的插件接入,它们分别是:
- DllPlugin 插件:用于打包出一个个单独的动态链接库文件。
- DllReferencePlugin插件:用于在主要配置文件中去引入 DllPlugin 插件打包好的动态链接库文件。
下面以基本的 React 项目为例,与其接入 DllPlugin, 在开始前先来看下最终构建出的目录结构:
├── main.js
├── polyfill.dll.js
├── polyfill.manifest.json
├── react.dll.js
└── react.manifest.json
其中包含两个动态链接库文件,分别是:
polyfill.dll.js
里面包含项目所有依赖的polyfill
,例如 Promise、fetch 等 API。react.dll.js
里面包含 React 的基础运行环境,也就是react
和react-dom
模块。
以react.dll.js
文件为例,其文件内容大致如下:
var _dll_react = (function(modules) {
// ... 此处省略 webpackBootstrap 函数代码
}([
function(module, exports, __webpack_require__) {
// 模块 ID 为 0 的模块对应的代码
},
function(module, exports, __webpack_require__) {
// 模块 ID 为 1 的模块对应的代码
},
// ... 此处省略剩下的模块对应的代码
]));
可见一个动态链接库文件中包含了大量模块的代码,这些模块存放在一个数组里,用数组的索引号作为 ID。 并且还通过 _dll_react
变量把自己暴露在了全局中,也就是可以通过 window._dll_react
可以访问到它里面包含的模块。
其中 polyfill.manifest.json
和 react.manifest.json
文件也是由 DllPlugin 生成出,用于描述动态链接库文件中包含哪些模块, 以 react.manifest.json
文件为例,其文件内容大致如下:
{
// 描述该动态链接库文件暴露在全局的变量名称
"name": "_dll_react",
"content": {
"./node_modules/process/browser.js": {
"id": 0,
"meta": {}
},
// ... 此处省略部分模块
"./node_modules/react-dom/lib/ReactBrowserEventEmitter.js": {
"id": 42,
"meta": {}
},
"./node_modules/react/lib/lowPriorityWarning.js": {
"id": 47,
"meta": {}
},
// ... 此处省略部分模块
"./node_modules/react-dom/lib/SyntheticTouchEvent.js": {
"id": 210,
"meta": {}
},
"./node_modules/react-dom/lib/SyntheticTransitionEvent.js": {
"id": 211,
"meta": {}
},
}
}
可见 manifest.json
文件清楚地描述了与其对应的 dll.js
文件中包含了哪些模块,以及每个模块的路径和 ID。
main.js
文件是编译出来的执行入口文件,当遇到其依赖的模块在 dll.js
文件中时,会直接通过 dll.js
文件暴露出的全局变量去获取打包在 dll.js
文件的模块。 所以在 index.html
文件中需要把依赖的两个 dll.js
文件给加载进去,index.html
内容如下:
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
<div id="app"></div>
<!--导入依赖的动态链接库文件-->
<script src="./dist/polyfill.dll.js"></script>
<script src="./dist/react.dll.js"></script>
<!--导入执行入口文件-->
<script src="./dist/main.js"></script>
</body>
</html>
以上就是所有接入 DllPlugin 后最终编译出来的代码,接下来教你如何实现。
构建出动态链接库文件
构建输出的以下这四个文件:
├── polyfill.dll.js
├── polyfill.manifest.json
├── react.dll.js
└── react.manifest.json
和以下这一个文件:
├── main.js
是由两份不同的构建分别输出的。
动态链接库文件相关的文件需要由一份独立的构建输出,用于给主构建使用。新建一个 webpack 配置文件 webpack_dll.config.js
专门用于构建它们,文件内容如下:
const path = require('path')
const DllPlugin = require('webpack/lib/DllPlugin')
module.exports = {
// JS 执行入口文件
entry: {
// 把 React 相关模块放到一个单独的动态链接库
react: ['react', 'react-dom'],
// 把项目需要所有的 polyfill 放到一个单独的动态链接库
polyfill: ['core-js/fn/object/assign', 'core-js/fn/promise', 'whatwg-fetch']
},
output: {
// 输出的动态链接库的文件名称,[name] 代表当前动态链接库的名称,
// 也就是entry中配置的 react 和 polyfill
filename: '[name].dll.js',
// 存放的文件都存放到 dist 目录下
path: path.resolve(__dirname, 'dist'),
// 存放动态链接库的全局变量名称,例如对应 react 来说就是 _dll_react
// 之所以在前面加上 _dll_是为了防止全局变量冲突
library: '_dll_[name]'
},
plugins: [
// 接入 DllPlugin
new DllPlugin({
// 动态链接库的全局变量名称,需要和 output.library 中保持一致
// 该字段的值也就是输出的 manifest.json 文件中 name 字段的值
// 例如 react.mainfest.json 中就有 "name": "_dll_react"
name: '_dll_[name]',
// 描述动态链接库的 mainfest.json 文件输出时的文件名称
path: path.join(__dirname, 'dist', '[name].mainfest.json')
})
]
}
使用动态链接库文件
构建出的动态链接库文件用于给其他地方使用,在这里也就是给执行使用。
用于输出 mian.js
的主 webpack 配置文件内容如下:
const path = require('path');
const DllReferencePlugin = require('webpack/lib/DllReferencePlugin');
module.exports = {
entry: {
// 定义入口 Chunk
main: './main.js'
},
output: {
// 输出文件的名称
filename: '[name].js',
// 输出文件都放到 dist 目录下
path: path.resolve(__dirname, 'dist'),
},
module: {
rules: [
{
// 项目源码使用了 ES6 和 JSX 语法,需要使用 babel-loader 转换
test: /\.js$/,
use: ['babel-loader'],
exclude: path.resolve(__dirname, 'node_modules'),
},
]
},
plugins: [
// 告诉 Webpack 使用了哪些动态链接库
new DllReferencePlugin({
// 描述 react 动态链接库的文件内容
manifest: require('./dist/react.manifest.json'),
}),
new DllReferencePlugin({
// 描述 polyfill 动态链接库的文件内容
manifest: require('./dist/polyfill.manifest.json'),
}),
],
devtool: 'source-map'
};
注意:
在 webpack_dll.config.js
文件中,plugins DllPlugin 中的name
参数必须和output.library
中保持一致。原因在于 DllPlugin 中的name
参数会影响输出的mainfest.json
文件中的name
字段的值,而在webpack.config.js
文件中 DllReferencePlugin 会去mainfest.json
文件读取name
字段的值,把值的内容作为在从全局变量中获取动态链接库中内容时的全局变量名。
执行构建
在修改好以上两个 webpack 配置文件后,需要重新执行构建。重新执行构建时要注意的是需要先把动态链接库相关的文件编译出来,因为主 webpack 配置文件中定义的 DllReferencePlugin 依赖这些文件。
执行构建时流程如下:
-
如果动态链接库相关的文件还没有编译出来,就需要先把它们编译出来。方法是执行
webpack --config webpack_dll.config.js
命令。 -
在确保动态链接库存在时,才能正常的编译出入口执行文件。方法是执行 webpack 命令。
使用 HappyPack
接入 HappyPack 的相关代码如下:
const path = require('path');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const HappyPack = require('happypack');
module.exports = {
module: {
rules: [
{
test: /\.js$/,
// 把对 .js 文件的处理转交给 id 为 babel 的 HappyPack 实例
use: ['happypack/loader?id=babel'],
// 排除 node_modules 目录下的文件,node_modules 目录下的文件都是采用的 ES5 语法,没必要再通过 Babel 去转换
exclude: path.resolve(__dirname, 'node_modules'),
},
{
// 把对 .css 文件的处理转交给 id 为 css 的 HappyPack 实例
test: /\.css$/,
use: ExtractTextPlugin.extract({
use: ['happypack/loader?id=css'],
}),
},
]
},
plugins: [
new HappyPack({
// 用唯一的标识符 id 来代表当前的 HappyPack 是用来处理一类特定的文件
id: 'babel',
// 如何处理 .js 文件,用法和 Loader 配置中一样
loaders: ['babel-loader?cacheDirectory'],
// ... 其它配置项
}),
new HappyPack({
id: 'css',
// 如何处理 .css 文件,用法和 Loader 配置中一样
loaders: ['css-loader'],
}),
new ExtractTextPlugin({
filename: `[name].css`,
}),
],
};
以上代码有两点重要的修改:
- 在 Loader 配置中,所有文件的处理都交给了
happypack/loader
去处理,使用紧跟其后的querystring ?id=babel
去告诉happypack/loader
去选择哪个 HappyPack 实例去处理文件。 - 在 Plugin 配置中,新增了两个 HappyPack 实例分别用于告诉
happypack/loader
去如何处理.js
和.css
文件。选项中的id
属性的值和上面querystring
中的?id=babel
相对应,选项中的loaders
属性和 Loader 配置中一样。
在实例化 HappyPack 插件的时候,除了可以传入 id
和 loaders
两个参数外, HappyPack 还支持如下参数:
threads
代表开启几个子进程去处理这一类型的文件,默认是3
个,类型必须是整数。verbose
是否允许 HappyPack 输出日志, 默认是true
。threadPool
代表共享进程池,即多个 HappyPack 实例都使用同一个共享进程池中的子进程去处理任务,以防止资源占用过多,相关代码如下:
const HappyPack = require('happypack');
// 构造出共享进程池,进程池中包含5个子进程
const happyThreadPool = HappyPack.ThreadPool({ size: 5 });
module.exports = {
plugins: [
new HappyPack({
// 用唯一的标识符 id 来代表当前的 HappyPack 是用来处理一类特定的文件
id: 'babel',
// 如何处理 .js 文件,用法和 Loader 配置中一样
loaders: ['babel-loader?cacheDirectory'],
// 使用共享进程池中的子进程去处理任务
threadPool: happyThreadPool,
}),
new HappyPack({
id: 'css',
// 如何处理 .css 文件,用法和 Loader 配置中一样
loaders: ['css-loader'],
// 使用共享进程池中的子进程去处理任务
threadPool: happyThreadPool,
}),
new ExtractTextPlugin({
filename: `[name].css`,
}),
],
};
使用 ParallelUglifyPlugin
ParallelUglifyPlugin 则会开启多个子进程,把对多个文件的压缩工作分配给多个子进程去完成,每个子进程其实还是通过 UglifyJS 去压缩代码,但是变成了并行执行
使用 ParallelUglifyPlugin 也非常简单,把原来 Webpack 配置文件中内置的 UglifyJsPlugin 去掉后,再替换成 ParallelUglifyPlugin,相关代码如下:
const path = require('path');
const DefinePlugin = require('webpack/lib/DefinePlugin');
const ParallelUglifyPlugin = require('webpack-parallel-uglify-plugin');
module.exports = {
plugins: [
// 使用 ParallelUglifyPlugin 并行压缩输出的 JS 代码
new ParallelUglifyPlugin({
// 传递给 UglifyJS 的参数
uglifyJS: {
output: {
// 最紧凑的输出
beautify: false,
// 删除所有的注释
comments: false,
},
compress: {
// 在UglifyJs删除没有用到的代码时不输出警告
warnings: false,
// 删除所有的 `console` 语句,可以兼容ie浏览器
drop_console: true,
// 内嵌定义了但是只用到一次的变量
collapse_vars: true,
// 提取出出现多次但是没有定义成变量去引用的静态值
reduce_vars: true,
}
},
}),
],
};
优化总结
侧重优化开发体验的配置文件 webpack.config.js:
const path = require('path');
const DefinePlugin = require('webpack/lib/DefinePlugin');
const ModuleConcatenationPlugin = require('webpack/lib/optimize/ModuleConcatenationPlugin');
const CommonsChunkPlugin = require('webpack/lib/optimize/CommonsChunkPlugin');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const {AutoWebPlugin} = require('web-webpack-plugin');
const HappyPack = require('happypack');
const ParallelUglifyPlugin = require('webpack-parallel-uglify-plugin');
// 自动寻找 pages 目录下的所有目录,把每一个目录看成一个单页应用
const autoWebPlugin = new AutoWebPlugin('./src/pages', {
// HTML 模版文件所在的文件路径
template: './template.html',
// 提取出所有页面公共的代码
commonsChunk: {
// 提取出公共代码 Chunk 的名称
name: 'common',
},
// 指定存放 CSS 文件的 CDN 目录 URL
stylePublicPath: '//css.cdn.com/id/',
});
module.exports = {
// AutoWebPlugin 会找为寻找到的所有单页应用,生成对应的入口配置,
// autoWebPlugin.entry 方法可以获取到生成入口配置
entry: autoWebPlugin.entry({
// 这里可以加入你额外需要的 Chunk 入口
base: './src/base.js',
}),
output: {
// 给输出的文件名称加上 Hash 值
filename: '[name]_[chunkhash:8].js',
path: path.resolve(__dirname, './dist'),
// 指定存放 JavaScript 文件的 CDN 目录 URL
publicPath: '//js.cdn.com/id/',
},
resolve: {
// 使用绝对路径指明第三方模块存放的位置,以减少搜索步骤
// 其中 __dirname 表示当前工作目录,也就是项目根目录
modules: [path.resolve(__dirname, 'node_modules')],
// 只采用 main 字段作为入口文件描述字段,以减少搜索步骤
mainFields: ['jsnext:main', 'main'],
},
module: {
rules: [
{
// 如果项目源码中只有 js 文件就不要写成 /\.jsx?$/,提升正则表达式性能
test: /\.js$/,
// 使用 HappyPack 加速构建
use: ['happypack/loader?id=babel'],
// 只对项目根目录下的 src 目录中的文件采用 babel-loader
include: path.resolve(__dirname, 'src'),
},
{
test: /\.js$/,
use: ['happypack/loader?id=ui-component'],
include: path.resolve(__dirname, 'src'),
},
{
// 增加对 CSS 文件的支持
test: /\.css/,
// 提取出 Chunk 中的 CSS 代码到单独的文件中
use: ExtractTextPlugin.extract({
use: ['happypack/loader?id=css'],
// 指定存放 CSS 中导入的资源(例如图片)的 CDN 目录 URL
publicPath: '//img.cdn.com/id/'
}),
},
]
},
plugins: [
autoWebPlugin,
// 4-14开启ScopeHoisting
new ModuleConcatenationPlugin(),
// 4-3使用HappyPack
new HappyPack({
// 用唯一的标识符 id 来代表当前的 HappyPack 是用来处理一类特定的文件
id: 'babel',
// babel-loader 支持缓存转换出的结果,通过 cacheDirectory 选项开启
loaders: ['babel-loader?cacheDirectory'],
}),
new HappyPack({
// UI 组件加载拆分
id: 'ui-component',
loaders: [{
loader: 'ui-component-loader',
options: {
lib: 'antd',
style: 'style/index.css',
camel2: '-'
}
}],
}),
new HappyPack({
id: 'css',
// 如何处理 .css 文件,用法和 Loader 配置中一样
// 通过 minimize 选项压缩 CSS 代码
loaders: ['css-loader?minimize'],
}),
new ExtractTextPlugin({
// 给输出的 CSS 文件名称加上 Hash 值
filename: `[name]_[contenthash:8].css`,
}),
// 4-11提取公共代码
new CommonsChunkPlugin({
// 从 common 和 base 两个现成的 Chunk 中提取公共的部分
chunks: ['common', 'base'],
// 把公共的部分放到 base 中
name: 'base'
}),
new DefinePlugin({
// 定义 NODE_ENV 环境变量为 production 去除 react 代码中的开发时才需要的部分
'process.env': {
NODE_ENV: JSON.stringify('production')
}
}),
// 使用 ParallelUglifyPlugin 并行压缩输出的 JS 代码
new ParallelUglifyPlugin({
// 传递给 UglifyJS 的参数
uglifyJS: {
output: {
// 最紧凑的输出
beautify: false,
// 删除所有的注释
comments: false,
},
compress: {
// 在UglifyJs删除没有用到的代码时不输出警告
warnings: false,
// 删除所有的 `console` 语句,可以兼容ie浏览器
drop_console: true,
// 内嵌定义了但是只用到一次的变量
collapse_vars: true,
// 提取出出现多次但是没有定义成变量去引用的静态值
reduce_vars: true,
}
},
}),
]
};
参考文档
感觉还可以是
- externals
- 设置css-loader版本号("css-loader": "^0.14.5"), 相关Issue
- 替换scss-loader 为fast-sass-loader
感谢总结,很棒。