众所周知,webpack做为主流的前端项目利器,从编译到打包提供了不少方便的功能。本文主要从编译和体积两个篇章阐述笔者总结的实践心得,但愿对你们有帮助。css
vendor文件即依赖库文件,通常在项目中不多改动。单独打包能够在后续的项目迭代过程当中,保证vendor文件可从客户端缓存读取,提高客户端的访问体验。html
解决方案:经过在vendor.config.js文件中定义,在webpack.config.{evn}.js中引用来使用。 vendor.config.js示例前端
module.exports = {
entry: {
vendor: ['react', 'react-dom', 'react-redux', 'react-router-dom', 'react-loadable', 'axios'],
}
};
复制代码
vendor单独打包以后,仍是有一个问题。编译的过程当中,每次都须要对vendor文件进行打包,其实这一块要是能够提早打包好,那后续编译的时候,就能够节约这部分的时间了。vue
解决方案:定义webpack.dll.config.js,使用 DLLPlugin
提早执行打包,而后在webpack.config.{evn}.js经过 DLLReferencePlugin
引入打包好的文件,最后使用AddAssetHtmlPlugin
往html里注入vendor文件路径 webpack.dll.config.js示例node
const TerserPlugin = require('terser-webpack-plugin');
const CleanWebpackPlugin = require("clean-webpack-plugin");
const webpack = require('webpack');
const path = require('path');
const dllDist = path.join(__dirname, 'dist');
module.exports = {
entry: {
vendor: ['react', 'react-dom', 'react-redux', 'react-router-dom', 'react-loadable', 'axios', 'moment'],
},
output: {
path: const dllDist = path.join(__dirname, 'dist'),
filename: '[name]-[hash].js',
library: '[name]',
},
optimization: {
minimizer: [
new TerserPlugin({
terserOptions: {
parse: {
ecma: 8,
},
compress: {
ecma: 5,
warnings: false,
comparisons: false,
inline: 2,
},
mangle: {
safari10: true,
},
output: {
ecma: 5,
comments: false,
ascii_only: true,
},
},
parallel: true,
cache: true,
sourceMap: false,
}),
],
},
plugins: [
new CleanWebpackPlugin(["*.js"], { // 清除以前的dll文件
root: dllDist,
}),
new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
new webpack.DllPlugin({
path: path.join(__dirname, 'dll', '[name]-manifest.json'),
name: '[name]',
}),
]
};
复制代码
webpack.config.prod.js片断react
const manifest = require('./dll/vendor-manifest.json');
const AddAssetHtmlPlugin = require('add-asset-html-webpack-plugin');
...
plugins: [
// webpack读取到vendor的manifest文件对于vendor的依赖不会进行编译打包
new webpack.DllReferencePlugin({
manifest,
}),
// 往html中注入vendor js
new AddAssetHtmlPlugin([{
publicPath: "/view/static/js", // 注入到html中的路径
outputPath: "../build/static/js", // 最终输出的目录
filepath: path.resolve(__dirname, './dist/*.js'),
includeSourcemap: false,
typeOfAsset: "js"
}]),
]
复制代码
webpack对文件的编译处理是单进程的,但实际上咱们的编译机器一般是多核多进程,若是能够充分利用cpu的运算力,能够提高很大的编译速度。webpack
解决方案:使用happypack进行多进程构建,使用webpack4内置的TerserPlugin并行模式进行js的压缩。ios
说明:happypack原理可参考http://taobaofed.org/blog/2016/12/08/happypack-source-code-analysis/git
webpack.config.prod.js片断github
const HappyPack = require('happypack');
// 采用多进程,进程数由CPU核数决定
const happThreadPool = HappyPack.ThreadPool({ size: os.cpus().length });
...
optimization: {
minimizer: [
new TerserPlugin({
terserOptions: {
parse: {
ecma: 8,
},
compress: {
ecma: 5,
warnings: false,
comparisons: false,
inline: 2,
},
mangle: {
safari10: true,
},
output: {
ecma: 5,
comments: false,
ascii_only: true,
},
},
parallel: true,
cache: true,
sourceMap: false,
}),
]
},
module: {
rules: [
{
test: /.css$/,
oneOf: [
{
test: /\.(js|mjs|jsx)$/,
include: paths.appSrc,
loader: 'happypack/loader',
options: {
cacheDirectory: true,
},
},
]
}
]
},
plugins: [
new HappyPack({
threadPool: happThreadPool,
loaders: [{
loader: 'babel-loader',
}]
}),
]
复制代码
当js页面特别多的时候,若是都打包成一个文件,那么很影响访问页面访问的速度。理想的状况下,是到相应页面的时候才下载相应页面的js。
解决方案:使用import('path/to/module') -> Promise。调用 import() 之处,被做为分离的模块起点,意思是,被请求的模块和它引用的全部子模块,会分离到一个单独的 chunk 中。
说明: 老版本使用require.ensure(dependencies, callback)进行按需加载,webpack > 2.4 的版本此方法已经被import()取代
按需加载demo,在非本地的环境下开启监控上报
if (process.env.APP_ENV !== 'local') {
import("./utils/emonitor").then(({emonitorReport}) => {
emonitorReport();
});
}
复制代码
react页面按需加载,可参考http://react.html.cn/docs/code-splitting.html,里面提到的React.lazy,React.Suspense是在react 16.6版本以后才有的新特性,对于老版本,官方依然推荐使用react-loadable
实现路由懒加载
react-loadable示例
import React, { Component } from 'react';
import { Route, Switch } from 'react-router-dom';
import Loadable from 'react-loadable';
import React, { Component } from 'react';
// 通过包装的组件会在访问相应的页面时才异步地加载相应的js
const Home = Loadable({
loader: () => import('./page/Home'),
loading: (() => null),
delay: 1000,
});
import NotFound from '@/components/pages/NotFound';
class CRouter extends Component {
render() {
return (
<Switch>
<Route exact path='/' component={Home}/>
{/* 若是没有匹配到任何一个Route, <NotFound>会被渲染*/}
<Route component={NotFound}/>
</Switch>
)
}
}
export default CRouter
复制代码
vue页面按需加载,可参考https://router.vuejs.org/zh/guide/advanced/lazy-loading.html
示例
// 下面2行代码,没有指定webpackChunkName,每一个组件打包成一个js文件。
const ImportFuncDemo1 = () => import('../components/ImportFuncDemo1')
const ImportFuncDemo2 = () => import('../components/ImportFuncDemo2')
// 下面2行代码,指定了相同的webpackChunkName,会合并打包成一个js文件。
// const ImportFuncDemo = () => import(/* webpackChunkName: 'ImportFuncDemo' */ '../components/ImportFuncDemo')
// const ImportFuncDemo2 = () => import(/* webpackChunkName: 'ImportFuncDemo' */ '../components/ImportFuncDemo2')
export default new Router({
routes: [
{
path: '/importfuncdemo1',
name: 'ImportFuncDemo1',
component: ImportFuncDemo1
},
{
path: '/importfuncdemo2',
name: 'ImportFuncDemo2',
component: ImportFuncDemo2
}
]
})
复制代码
作完按需加载以后,假如定义的分离点里包含了css文件,那么相关css样式也会被打包进js chunk里,并经过URL.createObjectURL(blob)的方式加载到页面中。
解决方案:把分离点里的页面css引用(包括less和sass)提炼到index.less中,在index.js文件中引用。假如使用到库的less文件特别多,能够定义一个cssVendor.js,在index.js中引用,并在webpack config中添加一个entry以配合MiniCssExtractPlugin作css抽离。
P.S. 假如用到antd或其余第三方UI库,按需加载的时候记得把css引入选项取消,把 style: true
选项删掉
示例
cssVendor片断
// 全局引用的组件的样式预加载,按需引用,可优化异步加载的chunk js体积
// Row
import 'antd/es/row/style/index.js';
// Col
import 'antd/es/col/style/index.js';
// Card
import 'antd/es/card/style/index.js';
// Icon
import 'antd/es/icon/style/index.js';
// Modal
import 'antd/es/modal/style/index.js';
// message
import 'antd/es/message/style/index.js';
...
复制代码
webpack.config.production片断
entry:
{
main: [paths.appIndexJs, paths.cssVendorJs]
},
plugins: [
new HappyPack({
threadPool: happThreadPool,
loaders: [{
loader: 'babel-loader',
options: {
customize: require.resolve(
'babel-preset-react-app/webpack-overrides'
),
plugins: [
[
require.resolve('babel-plugin-named-asset-import'),
{
loaderMap: {
svg: {
ReactComponent: '@svgr/webpack?-prettier,-svgo![path]',
},
},
},
],
['import',
{ libraryName: 'antd', libraryDirectory: 'es' },
],
],
cacheDirectory: true,
cacheCompression: true,
compact: true,
},
}],
})]
复制代码
咱们在项目的开发中常常会引用一些第三方库,例如antd,lodash。这些库在咱们的项目中默认是全量引入的,但其实咱们只用到库里的某些组件或者是某些函数,那么按需只打包咱们引用的组件或函数就能够减小js至关大一部分的体积。
解决方案:使用babel-plugin-import插件来实现按需打包,具体使用方式可参考https://github.com/ant-design/babel-plugin-import
示例
{
test: /\.(js|jsx)$/,
include: paths.appSrc,
loader: require.resolve('babel-loader'),
exclude: /node_modules/,
options: {
plugins: [
['import', [
{ libraryName: 'lodash', libraryDirectory: '', "camel2DashComponentName": false, },
{ libraryName: 'antd', style: true }
]
],
],
compact: true,
},
}
复制代码
有些包含多语言的库会将全部本地化内容和核心功能一块儿打包,因而打包出来的js里会包含不少多语言的配置文件,这些配置文件若是不打包进来,也能够减小js的体积。
解决方案:使用IgnorePlugin插件忽略指定资源路径的打包
示例
new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/)
复制代码
压缩是一道常规的生产工序,前端项目编译出来的文件通过压缩混淆,能够把体积进一步缩小。
解决方案:使用TerserPlugin插件进行js压缩,使用OptimizeCSSAssetsPlugin插件css压缩
说明:webpack4以前js压缩推荐使用ParalleUglifyPlugin插件,它在UglifyJsPlugin的基础上作了多进程并行处理的优化,速度更快;css压缩推荐使用cssnano,它基于PostCSS。由于css-loader已经将其内置了,要开启cssnano去压缩代码只须要开启css-loader的minimize选项。
示例
minimizer: [
new TerserPlugin({
terserOptions: {
parse: {
ecma: 8,
},
compress: {
ecma: 5,
warnings: false,
comparisons: false,
inline: 2,
},
mangle: {
safari10: true,
},
output: {
ecma: 5,
comments: false,
ascii_only: true,
},
},
parallel: true,
cache: true,
sourceMap: shouldUseSourceMap,
}),
new OptimizeCSSAssetsPlugin({
cssProcessorOptions: {
parser: safePostCssParser,
map: shouldUseSourceMap
? {
inline: false,
annotation: true,
}
: false,
},
}),
]
复制代码
在不少chunks里,有相同的依赖,把这些依赖抽离为一个公共的文件,则能够有效地减小资源的体积,并能够充分利用浏览器缓存。
解决方案:使用SplitChunksPlugin抽离共同文件
P.S. webpack4使用SplitChunksPlugin代替了CommonsChunkPlugin 示例
optimization: {
splitChunks: {
chunks: 'all',
name: false
}
}
复制代码
SplitChunksPlugin的具体配置可参考 juejin.im/post/5af15e…
Scope Hoisting 是webpack3中推出的新功能,能够把依赖的代码直接注入到入口文件里,减小了函数做用域的声明,也减小了js体积和内存开销
举个栗子 假如如今有两个文件分别是 util.js:
export default 'Hello,Webpack';
复制代码
和入口文件 main.js:
import str from './util.js';
console.log(str);
复制代码
以上源码用 Webpack 打包后输出中的部分代码以下:
[
(function (module, __webpack_exports__, __webpack_require__) {
var __WEBPACK_IMPORTED_MODULE_0__util_js__ = __webpack_require__(1);
console.log(__WEBPACK_IMPORTED_MODULE_0__util_js__["a"]);
}),
(function (module, __webpack_exports__, __webpack_require__) {
__webpack_exports__["a"] = ('Hello,Webpack');
})
]
复制代码
在开启 Scope Hoisting 后,一样的源码输出的部分代码以下:
[
(function (module, __webpack_exports__, __webpack_require__) {
var util = ('Hello,Webpack');
console.log(util);
})
]
复制代码
从中能够看出开启 Scope Hoisting 后,函数申明由两个变成了一个,util.js 中定义的内容被直接注入到了 main.js 对应的模块中。
解决方案:webpack4 production mode会自动开启ModuleConcatenationPlugin,实现做用域提高。
tree shaking 是一个术语,一般用于描述移除 JavaScript 上下文中的未引用代码(dead-code)
有的时候,代码里或者引用的模块里包含里一些没被使用的代码块,打包的时候也被打包到最终的文件里,增长了体积。这种时候,咱们可使用tree shaking技术来安全地删除文件中未使用的部分。
使用方法:
在体积优化的路上,咱们可使用工具来分析咱们打包出的体积最终优化成怎样的效果。 经常使用的工具备两个:
plugins: [
new BundleAnalyzerPlugin()
]
复制代码
执行完build后会打开网页,效果图以下:
devtool: true
,而后执行source-map-explorer build/static/js/main.js
则能够去分析指定js的体积。效果图以下: