做者 DBCdouble
项目源码demo:点击这里javascript
随着2018年2月15号webpack4.0.0出来已经有一段时间了,webpack依靠着“零配置”,“最高可提高98%的速度”成功吸粉无数,对于饱受项目打包时间过长的我,无疑是看到了曙光,因而决定开始试水。
css
升级后:html
随着项目的不断迭代,样式文件和js文件的数量愈来愈多,形成webpack的打包花费的时间愈来愈多,在开发环境下,常常须要频繁调试某一段代码ctrl+s会出现长时间等待的现象(等得好烦),日积月累,浪费了太多的时间在等待打包上。生产环境就更不用说了,平均时长100s~120s左右,一般状况状况下,输入npm run deploy打包以后,我会选择出去抽根烟。而若是状况是要解决线上的bug,则是分秒必争,因此优化打包时间势在必行java
webpack2.x生产环境花费时间: 104.145snode
webpack2.x开发环境花费时间: 68099msreact
虽然能直观得看到webpack2打包所花费的时间,但咱们并不知道webpack打包通过了哪些步骤,在哪一个环节花费了大量时间。这里可使用speed-measure-webpack-plugin来检测webpack打包过程当中各个部分所花费的时间,在终端输入如下命令进行安装。webpack
npm install speed-measure-webpack-plugin -D复制代码
安装完成以后,咱们再webpack的配置文件中配置它git
webpack.config.jses6
参考speed-measure-webpack-plugin的使用方式,查看这里github
配置好以后,启动项目(这里只对开发环境进行分析了)后,以下图
从上图能够看出,webpack打包过程当中绝大部分时间花在了loader上,也就是webpack构建流程的第二个环节,编译阶段。注意上面还能看到ProgressPlugin花费了28.87s,因此在咱们不须要分析webpack打包流程花费的时间后,可在webpack.config.js中注释掉
先删除以前的webpack、webpack-cli、webpack-dev-server
npm uninstall webpack webpack-dev-server webpack-cli && npm uninstall webpacl-cli -g复制代码
安装最新版本的webpack、webpack-cli(webpack4把脚手架webpack-cli从webpack中抽离出来的,因此必须安装webpack-cli)、webpack-dev-server
npm install webpack webpack-dev-server webpack-cli -D复制代码
我这里顺便再把webpack的相关插件更新到最新版本,由于webpack作了很大的改动相对webpakc2,以防以前老版本的插件不兼容webpack4,因此我这边将项目中的webpack相关插件的模块都先删除掉,以便更新的时候分析错误
npm uninstall extract-text-webpack-plugin html-webpack-plugin webpack-dev-middleware webpack-hot-middleware复制代码
删除以前的babel相关模块
npm uninstall babel-core babel-loader babel-cli babel-eslint babel-plugin-react-transform babel-plugin-transform-runtime babel-preset-es2015 babel-preset-react babel-preset-stage-0 babel-runtime复制代码
安装babel7
npm install @babel/cli @babel/core babel-loader @babel/plugin-proposal-class-properties @babel/plugin-proposal-decorators @babel/plugin-proposal-export-default-from @babel/plugin-transform-runtime @babel/preset-env @babel/preset-react复制代码
@babel/plugin-proposal-class-properties: 解析class类的属性
@babel/plugin-proposal-decorators: 解析装饰器模式语法,如使用react-redux的@connect
@babel/plugin-proposal-export-default-from: 解析export xxx from 'xxx'语法
.babelrc 文件为babel的配置文件(我这边是直接在webpack.config.js的babel-loader的options下配置的,.babelrc文件中注意须要转换为json格式,须要将属性名加双引号)
在项目的根目录下,安装eslint
和eslint-loader
npm install eslint eslint-loader -D复制代码
.eslintrc
是ESlint的配置文件,咱们须要在项目的根目录下增长.eslintrc
文件。
{
"parser": "babel-eslint",
"env": {
"browser": true,
"es6": true,
"node": true
},
"globals" : {
"Action" : false,
"__DEV__" : false,
"__PROD__" : false,
"__DEBUG__" : false,
"__DEBUG_NEW_WINDOW__" : false,
"__BASENAME__" : false
},
"parserOptions": {
"ecmaVersion": 6,
"sourceType": "module"
},
"extends": "airbnb",
"rules": {
"semi": [0],
"react/jsx-filename-extension": [0]
}}
复制代码
在webpack.config.js
中,为须要检测的文件添加eslint-loader
加载器。通常咱们是在代码编译前进行检测。
webpack.config.js
注意,这里的isEslint是经过npm scripts传的参数eslint来判断当前环境是否须要进行代码格式检查,以便开发者有更多选择,而且eslint-loader必须配置在babel-loader以前,因此这里用unshift来添加eslint-loader
packack.json
在package.json文件中添加以下命令
{
"scripts": {
"eslint": "eslint --ext .js --ext .jsx src/"
}
}复制代码
到这里,就能够经过执行 npm run eslint来检测src文件下的代码格式了
npm install webpack-merge yargs-parser clean-webpack-plugin progress-bar-webpack-plugin webpack-build-notifier html-webpack-plugin mini-css-extract-plugin add-asset-html-webpack-plugin uglifyjs-webpack-plugin optimize-css-assets-webpack-plugin friendly-errors-webpack-plugin happypack复制代码
mini-css-extract-plugin: webpack打包样式文件中的默认会把样式文件代码打包到bundle.js中,mini-css-extract-plugin这个插件能够将样式文件从bundle.js抽离出来一个文件,而且支持chunk css
add-asset-html-webpack-plugin: 从命名能够看出,它的做用是能够将静态资源css或者js引入到html-webpack-plugin生成的html文件中
uglifyjs-webpack-plugin: 代码丑化,用于js压缩(能够调用系统的线程进行多线程压缩,优化webpack的压缩速度)
optimize-css-assets-webpack-plugin: css压缩,主要使用 cssnano 压缩器(webpack4的执行环境内置了cssnano,因此不用安装)
happypack: 多线程编译,加快编译速度(加快loader的编译速度),注意,thread-loader不能够和 mini-css-extract-plugin 结合使用
如下文件直接在你的项目copy就能使用
webpack.config.js
const path = require('path')
const webpack = require('webpack')
const os = require('os')
const merge = require('webpack-merge')
const argv = require('yargs-parser')(process.argv.slice(2))
const mode = argv.mode || 'development'
const interface = argv.interface || 'development'
const isEslint = !!argv.eslint
const isDev = mode === 'development'
const mergeConfig = require(`./config/webpack.${mode}.js`)
const CleanWebpackPlugin = require('clean-webpack-plugin')
const ProgressBarPlugin = require('progress-bar-webpack-plugin')
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin")
const WebpackBuildNotifierPlugin = require('webpack-build-notifier')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const AddAssetHtmlPlugin = require('add-asset-html-webpack-plugin')
const FirendlyErrorePlugin = require('friendly-errors-webpack-plugin')
const HappyPack = require('happypack')
const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length })
const smp = new SpeedMeasurePlugin()
const loading = { html:"加载中..."}
const apiConfig = {
development: 'http://xxxxx/a',
production: 'http://xxx/b'
}
let commonConfig = {
module: {
rules: [{
test: /\.js$/,
loaders: ['happypack/loader?id=babel'],
include: path.resolve(__dirname, 'src'),
exclude: /node_modules/
},{
test: /\.css$/,
loaders: [
MiniCssExtractPlugin.loader,
'css-loader'
]
},{
test: /\.less$/,
loaders: [
isDev ? 'style-loader' : MiniCssExtractPlugin.loader,
'css-loader',
{
loader:'less-loader?sourceMap=true',
options:{
javascriptEnabled: true
},
}
// include: path.resolve(__dirname, 'src')
]
},{
test: /\.(png|svg|jpg|gif)$/,
use: [
'url-loader'
]
},{
test: /\.(woff|woff2|eot|ttf|otf|ico)$/,
use: [
'file-loader'
]
},{
test: /\.(csv|tsv)$/,
use: [
'csv-loader'
]
},{
test: /\.xml$/,
use: [
'xml-loader'
]
},{
test: /\.md$/,
use: [
"html-loader",
"markdown-loader"
]
}]
},
//解析 resolve: {
extensions: ['.js', '.jsx'], // 自动解析肯定的扩展
},
plugins: [
new HappyPack({
id: 'babel',
loaders: [{
loader: 'babel-loader',
options: {
cacheDirectory: true,
presets: ['@babel/preset-env', '@babel/preset-react'],
plugins: [
['@babel/plugin-proposal-decorators', { "legacy": true }],
'@babel/plugin-proposal-class-properties',
'@babel/plugin-proposal-export-default-from',
'@babel/plugin-transform-runtime',
// 'react-hot-loader/babel',
// 'dynamic-import-webpack',
['import',{
libraryName:'antd',
libraryDirectory: 'es',
style:true
}]
]
}
}],
//共享进程池
threadPool: happyThreadPool,
//容许 HappyPack 输出日志
verbose: true,
}),
new CleanWebpackPlugin(['dist']),
new ProgressBarPlugin(),
new WebpackBuildNotifierPlugin({
title: "xxx后台管理系统🍎",
logo: path.resolve(__dirname, "src/static/favicon.ico"),
suppressSuccess: true
}),
new webpack.DefinePlugin({
'process.env' : {
'NODE_ENV' : JSON.stringify(mode)
},
'NODE_ENV' : JSON.stringify(mode),
'baseUrl': JSON.stringify(apiConfig[interface]),
'__DEV__' : mode === 'development',
'__PROD__' : mode === 'production',
'__TEST__' : mode === 'test',
'__DEBUG__' : mode === 'development' && !argv.no_debug,
'__DEBUG_NEW_WINDOW__' : !!argv.nw,
'__BASENAME__' : JSON.stringify(process.env.BASENAME || '')
}),
new FirendlyErrorePlugin(),
new HtmlWebpackPlugin({
template: path.resolve(__dirname, 'public/index.html'),
favicon: path.resolve(__dirname, 'public/favicon.ico'),
filename: 'index.html',
loading
}),
new MiniCssExtractPlugin({
filename: isDev ? 'styles/[name].[hash:4].css' : 'styles/[name].[hash:8].css',
chunkFilename:isDev ? 'styles/[name].[hash:4].css' : 'styles/[name].[hash:8].css'
}),
// 告诉 Webpack 使用了哪些动态连接库
new webpack.DllReferencePlugin({
// 描述 vendor 动态连接库的文件内容
manifest: require('./public/vendor/vendor.manifest.json')
}),
// 该插件将把给定的 JS 或 CSS 文件添加到 webpack 配置的文件中,并将其放入资源列表 html webpack插件注入到生成的 html 中。
new AddAssetHtmlPlugin([
{
// 要添加到编译中的文件的绝对路径,以及生成的HTML文件。支持 globby 字符串
filepath: require.resolve(path.resolve(__dirname, 'public/vendor/vendor.dll.js')),
// 文件输出目录
outputPath: 'vendor',
// 脚本或连接标记的公共路径
publicPath: 'vendor'
}
]),
new webpack.HotModuleReplacementPlugin()
],
devServer: {
host: 'localhost',
port: 8080,
historyApiFallback: true,
overlay: {//当出现编译器错误或警告时,就在网页上显示一层黑色的背景层和错误信息
errors: true
},
inline: true,
open: true,
hot: true
},
performance: {
// false | "error" | "warning" // 不显示性能提示 | 以错误形式提示 | 以警告...
hints: false, // 开发环境设置较大防止警告
// 根据入口起点的最大致积,控制webpack什么时候生成性能提示,整数类型,以字节为单位
maxEntrypointSize: 50000000,
// 最大单个资源体积,默认250000 (bytes)
maxAssetSize: 30000000
}
}
if (isEslint) {
commonConfig.module.rules.unshift[{
//前置(在执行编译以前去执行eslint-loader检查代码规范,有报错就不执行编译)
enforce: 'pre',
test: /.(js|jsx)$/,
loaders: ['eslint-loader'],
exclude: /node_modules/
}]
}
module.exports = merge(commonConfig, mergeConfig)复制代码
注意:这里在最后导出配置的时候并无使用speed-measure-webpack-plugin,由于会报错,不知道是否是由于跟happypack不兼容的缘由。interface用来判断当前打包js网络请求的地址,isEslint判断是否须要执行代码检测,isDev用来判断当前执行环境是development仍是production,具体问题看代码
webpack.config.dll.js
const path = require('path');
const webpack = require('webpack');
const CleanWebpaclPlugin = require('clean-webpack-plugin');
const FirendlyErrorePlugin = require('friendly-errors-webpack-plugin');
module.exports = {
mode: 'production',
entry: {
// 将 lodash 模块做为入口编译成动态连接库
vendor: ['react', 'react-dom', 'react-router', 'react-redux', 'react-router-redux']
},
output: {
// 指定生成文件所在目录
// 因为每次打包生产环境时会清空 dist 文件夹,所以这里我将它们存放在了 public 文件夹下
path: path.resolve(__dirname, 'public/vendor'),
// 指定文件名
filename: '[name].dll.js',
// 存放动态连接库的全局变量名称,例如对应 vendor 来讲就是 vendor_dll_lib // 这个名称须要与 DllPlugin 插件中的 name 属性值对应起来
// 之因此在前面 _dll_lib 是为了防止全局变量冲突
library: '[name]_dll_lib'
},
plugins: [
new CleanWebpaclPlugin(['vendor'], {
root: path.resolve(__dirname, 'public')
}),
new FirendlyErrorePlugin(), // 接入 DllPlugin
new webpack.DllPlugin({
// 描述动态连接库的 manifest.json 文件输出时的文件名称
// 因为每次打包生产环境时会清空 dist 文件夹,所以这里我将它们存放在了 public 文件夹下
path: path.join(__dirname, 'public', 'vendor', '[name].manifest.json'),
// 动态连接库的全局变量名称,须要和 output.library 中保持一致
// 该字段的值也就是输出的 manifest.json 文件 中 name 字段的值
// 例如 vendor.manifest.json 中就有 "name": "vendor_dll_lib" name: '[name]_dll_lib'
})
],
performance: {
// false | "error" | "warning" // 不显示性能提示 | 以错误形式提示 | 以警告...
hints: "warning", // 开发环境设置较大防止警告
// 根据入口起点的最大致积,控制webpack什么时候生成性能提示,整数类型,以字节为单位
maxEntrypointSize: 5000000, // 最大单个资源体积,默认250000 (bytes)
maxAssetSize: 3000000
}}复制代码
运行 npm run dll
指令以后,能够看到项目中 public 目录下多出了一个 vendor 的文件夹,能够看到其中包含两个文件:
vendor.dll.js
里面包含 react react-dom react-router react-redux react-router-redux
的基础运行环境,将这些基础模块打到一个包里,只要这些包的包的版本没升级,之后每次打包就不须要再编译这些模块,提升打包的速率vendor.manifest.json
也是由 DllPlugin 生成出,用于描述动态连接库文件中包含哪些模块config/webpack.development.js
module.exports = {
mode: 'development',
//devtool: 'cheap-module-source-map',
devtool: 'eval',
output: {
filename: 'scripts/[name].bundle.[hash:4].js'
}
}复制代码
在开发环境下,咱们不作js压缩和css压缩,来提升开发环境下调试保存页面打包的速度
config/webpack.production.js
const UglifyJsPlugin = require('uglifyjs-webpack-plugin'); //开启多核压缩
const OptmizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin')
const os = require('os');
module.exports = {
mode: 'production',
devtool: 'hidden-source-map',
output: {
filename: 'scripts/[name].bundle.[hash:8].js'
},
optimization: {
splitChunks: {
chunks: 'all', // initial、async和all
minSize: 30000, // 造成一个新代码块最小的体积
maxAsyncRequests: 5, // 按需加载时候最大的并行请求数
maxInitialRequests: 3, // 最大初始化请求数
automaticNameDelimiter: '~', // 打包分割符
name: true,
cacheGroups: {
vendors: { // 项目基本框架等
chunks: 'all',
test: /antd/,
priority: 100,
name: 'vendors',
}
}
},
minimizer: [
new UglifyJsPlugin({
parallel: os.cpus().length,
cache:true,
sourceMap:true,
uglifyOptions: {
compress: {
// 在UglifyJs删除没有用到的代码时不输出警告
warnings: false,
// 删除全部的 `console` 语句,能够兼容ie浏览器
drop_console: true,
// 内嵌定义了可是只用到一次的变量
collapse_vars: true,
// 提取出出现屡次可是没有定义成变量去引用的静态值
reduce_vars: true,
},
output: {
// 最紧凑的输出
beautify: false,
// 删除全部的注释
comments: false,
}
}
}),
new OptmizeCssAssetsWebpackPlugin({
assetNameRegExp: /\.css$/g,
cssProcessor: require('cssnano'),
cssProcessorOptions: {
safe: true,
discardComments: {
removeAll: true
}
}
})
],
}}复制代码
在生产环境的配置中,作了js的压缩和css压缩,还有从打包的入口文件中使用splitChunks分离出来了antd来减少bundle.js的大小
public/index.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title> </head> <body> </body> </html>复制代码
package.json
使用异步加载组件的分割代码的方式进行体积优化见《Webpack按需加载秒开应用》(最重要的一步)