倉庫地址地址:github.com/ccokl/webpa…javascript
內附一個完整的 Webpack + React + Ant Design 配置css
按照 Ant Design 官網用 React 腳手構建的後臺項目,剛接手項目的時候大概30條路由左右,個人用的機子是 Mac 8G 內存,打包完成須要耗時2分鐘左右,決定優化一下。主要實踐了兩種方式,一種是修改腳手架配置,一種是自定義配置。html
項目技術棧: React + React Router + TypeScript + Ant Designjava
構建時間慢可能的緣由:node
React 腳手架修改 Webpack 配置方案:react
自定義Webpack
配置步驟:webpack
一、準備工做git
安裝 react-app-rewired
,customize-cra
,它提供一些修改 React 腳手架默認配置函數,具體參見:github.com/arackaf/cus…github
npm i react-app-rewired customize-cra --save-dev
複製代碼
安裝後,修改 package.json
文件的 scripts
web
"scripts": {
"start": "react-app-rewired start",
"build": "react-app-rewired build",
"test": "react-app-rewired test",
}
複製代碼
項目根目錄建立一個 config-overrides.js
用於修改Webpack
配置。
Ant Design
提供了一個按需加載的 babel 插件 babel-plugin-import
antd-dayjs-webpack-plugin
是Ant Design
官方推薦的插件,用於替換moment.js
安裝 npm i babel-plugin-import --save-dev
,並修改config-overrides.js
配置文件
override
函數用來覆蓋React
腳手架Webpack
配置;fixBabelImports
修改babel
配置
const { override, fixBabelImports,addWebpackPlugin } = require('customize-cra');
const AntdDayjsWebpackPlugin = require('antd-dayjs-webpack-plugin');
module.exports = override(
fixBabelImports('import', {
libraryName: 'antd',
libraryDirectory: 'es',
style: 'css',
}),
addWebpackPlugin(new AntdDayjsWebpackPlugin())
);
複製代碼
以上是Ant Design
推薦的作法。
二、首屏加載優化
npm i react-loadable customize-cra --save
安裝react-loadable
模塊,而後在路由文件裏使用以下,loading
組件能夠自定義。這樣打包的時候會爲每一個路由生成一個chunk
,以此來實現組件的動態加載。
須要安裝"@babel/plugin-syntax-dynamic-import
這個插件,編譯import()
這種語法
import Loadable from 'react-loadable';
const Index = Loadable({
loader:() => import('../components/Index'),
loading:SpinLoading
});
複製代碼
三、去掉 map 文件
process.env.GENERATE_SOURCEMAP = "false";
用來去掉打包後的map
文件
const { override, fixBabelImports } = require('customize-cra');
const { StatsWriterPlugin } = require("webpack-stats-plugin");
const AntdDayjsWebpackPlugin = require('antd-dayjs-webpack-plugin');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
let startTime = Date.now()
if(process.env.NODE_ENV === 'production') process.env.GENERATE_SOURCEMAP = "false"
// 自定義生產環境配置
const productionConfig = (config) =>{
if(config.mode === 'production'){
config.plugins.push(...[
new StatsWriterPlugin({
fields: null,
transform: (data) => {
let endTime = Date.now()
data = {
Time: (endTime - startTime)/1000 + 's'
}
return JSON.stringify(data, null, 2);
}
}),
new BundleAnalyzerPlugin()
])
}
return config
}
module.exports = override(
productionConfig,
fixBabelImports('import', {
libraryName: 'antd',
libraryDirectory: 'es',
style: 'css',
}),
addWebpackPlugin(new AntdDayjsWebpackPlugin())
);
複製代碼
去掉map
文件的打包時間,大約60s左右。查看打包後生成的分析圖發現Ant Design
的一些組件被重複打包,打包出來一共有13M多。
四、更細化分包
在productionConfig
配置添加,在入口文件添加vendors
用來分離穩定不變的模塊;common
用來抽離複用模塊;styles
將css
文件抽離成一個文件;
// 針對生產環境修改配置
const productionConfig = (config) =>{
if(config.mode === 'production'){
const splitChunksConfig = config.optimization.splitChunks;
if (config.entry && config.entry instanceof Array) {
config.entry = {
main: config.entry,
vendors: ["react", "react-dom", "react-router-dom", "react-router"]
}
} else if (config.entry && typeof config.entry === 'object') {
config.entry.vendors = ["react", "react-loadable","react-dom", "react-router-dom","react-router"];
}
Object.assign(splitChunksConfig, {
cacheGroups: {
vendors: {
test: "vendors",
name: 'vendors',
priority:10,
},
common: {
name: 'common',
minChunks: 2,
minSize: 30000,
chunks: 'all'
},
styles: {
name: 'styles',
test: /\.css$/,
chunks: 'all',
priority: 9,
enforce: true
}
}
})
config.plugins.push(...[
new StatsWriterPlugin({
fields: null,
transform: (data) => {
let endTime = Date.now()
data = {
Time: (endTime - startTime)/1000 + 's'
}
return JSON.stringify(data, null, 2);
}
}),
new BundleAnalyzerPlugin()
])
}
return config
}
複製代碼
以上實際打包運行大約35S左右,實際打包後的模塊一共2.41M,打包後生成的分析圖發現Ant Design
有個圖標庫特別大,大約有520kb,可是實際項目中用到的圖標特別少。到此不想繼續折騰React
腳手架了,還不如從新配置一套Webpack
替換腳手架。
五、總結
React
腳手架配置太重,對於龐大的後臺系統不實用Ant Design
的圖標庫沒有按需加載的功能React
腳手架配置太麻煩Webpack
配置實踐一、結果: 替換掉腳手架後,陸陸續續新增路由到100條左右,打包耗時大概20s-30S之間,業務代碼打包後1.49M,能夠接受。
二、優化點:
autodll-webpack-plugin
插件,生產環境經過預編譯的手段將Ant
React
等穩定的模塊所有先抽離出來,只打包編譯業務代碼。babel-loader
開啓緩存happypack
加快編譯速度devtool
三、問題:
zip
大概也有800多Kb
,首屏加載比較慢。若是結合externals
屬性將這些靜態資源放置到CDN
上或許加載會更快。四、基礎配置:
放於webpack.base.config.js
文件
一、安裝babel
模塊,使用的是babel7.0
版本。
npm i install babel-loader @babel/core @babel/preset-env @babel/preset-react @babel/preset-typescript @babel/plugin-transform-runtime --save-dev
npm i @babel/runtime-corejs3 --save
複製代碼
在根目錄下建立babel.config.js
babel
的配置文件
module.exports = function (api) {
api.cache(true);
const presets = ["@babel/env","@babel/preset-react","@babel/preset-typescript"];
const plugins = [
["@babel/plugin-transform-runtime", {
corejs: 3,
}],
"@babel/plugin-syntax-dynamic-import",
"@babel/plugin-proposal-class-properties",
];
if (process.env.NODE_ENVN !== "production") {
plugins.push(["import", {
"libraryName": "antd", // 引入庫名稱
"libraryDirectory": "es", // 來源,default: lib
"style": "css" // 所有,or 按需'css'
}]);
}
return {
presets,
plugins
};
}
複製代碼
二、babel-loader
配置:
const os = require('os');
const HappyPack = require('happypack');
const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length });
module.exports = {
.....
module: {
rules: [
{
test: /\.(ts|tsx|js|jsx)$/,
exclude: /node_modules/,
loaders: ['happypack/loader?id=babel']
}
},
plugins: [
new HappyPack({
id: 'babel',
threadPool: happyThreadPool,
loaders: [{
loader:'babel-loader',
options: {
cacheDirectory: true
}
}]
})
]
}
複製代碼
三、mini-css-extract-plugin
插件打包抽離CSS到單獨的文件
var MiniCssExtractPlugin = require('mini-css-extract-plugin');
var NODE_ENV = process.env.NODE_ENV
var devMode = NODE_ENV !== 'production';
var utils = require('./utils')
module.exports = {
....
module: {
rules: [
{
test: /\.css$/,
use: [
{
loader: devMode ? 'style-loader' : MiniCssExtractPlugin.loader,
options: {
// only enable hot in development
hmr: devMode,
// if hmr does not work, this is a forceful method.
reloadAll: true,
},
},
"css-loader"
],
},
]
},
plugins: [
new MiniCssExtractPlugin({
//utils.assetsPath 打包後存放的地址
filename: devMode ? '[name].css' : utils.assetsPath('css/[name].[chunkhash].css'),
chunkFilename: devMode ? '[name].css' : utils.assetsPath('css/[name].[chunkhash].css'),
ignoreOrder: false, // Enable to remove warnings about conflicting order
})
]
}
複製代碼
四、html-webpack-plugin
生產html
文件
const HtmlWebpackPlugin = require('html-webpack-plugin')
var htmlTplPath = path.join(__dirname, '../public/')
module.exports = {
....
plugins: [
new HtmlWebpackPlugin({
filename: 'index.html',
template: htmlTplPath + 'index.html',
inject: true,
})
]
}
複製代碼
五、webpack.DefinePlugin
生成業務代碼能夠獲取的變量,能夠區分環境
const webpack = require('webpack')
module.exports = {
....
plugins: [
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify(devMode ? 'development' : 'production'),
'perfixerURL': JSON.stringify('//yzadmin.111.com.cn')
}),
}
複製代碼
五、開發環境配置:
放於webpack.development.config.js
文件
var path = require('path');
var webpack = require('webpack');
var merge = require('webpack-merge');
var FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin')
var entryScriptPath = path.join(__dirname, '../src/')
var base = require('./webpack.base.config');
module.exports = merge(base, {
entry: {
app: [entryScriptPath+'index'] // Your appʼs entry point
},
output: {
path: path.join(__dirname, '../dist/'),
filename: '[name].js',
chunkFilename: '[name].[chunkhash].js'
},
module: {
},
plugins: [
new webpack.ContextReplacementPlugin(/moment[\/\\]locale$/, /zh-cn/),
new webpack.HotModuleReplacementPlugin(),
new FriendlyErrorsPlugin(),
]
});
複製代碼
start.js
文件,用於npm start
啓動本地服務
var webpack = require('webpack');
var opn = require('opn')
var WebpackDevServer = require('webpack-dev-server');
var config = require('./webpack.development.config');
config.entry.app.unshift("webpack-dev-server/client?http://127.0.0.1:9000/", "webpack/hot/dev-server");
new WebpackDevServer(webpack(config), {
publicPath: config.output.publicPath,
hot: true,
historyApiFallback: {
index: '/public'
}
}).listen(9000, '127.0.0.1', function (err, result) {
if (err) {
return console.log(err);
}
opn('http://127.0.0.1:9000/')
});
複製代碼
六、生產環境配置:
放於webpack.production.config.js
文件
const path = require('path')
const merge = require('webpack-merge')
const baseWebpackConfig = require('./webpack.base.config');
const config = require('./webpack.env.config')
const utils = require('./utils')
const AutoDllPlugin = require('autodll-webpack-plugin');
const TerserJSPlugin = require('terser-webpack-plugin');
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
function resolve(dir) {
return path.join(__dirname, '..', dir)
}
const webpackConfig = merge(baseWebpackConfig, {
module: {
},
devtool: config.build.productionSourceMap ? '#source-map' : false,
entry: {
app: resolve('src/index'),
},
output: {
path: config.build.assetsRoot,
filename: utils.assetsPath('js/[name].[chunkhash].js'),
chunkFilename: utils.assetsPath('js/[name].[chunkhash].js')
},
optimization: {
minimizer: [new TerserJSPlugin({}), new OptimizeCSSAssetsPlugin({})],
splitChunks: {
cacheGroups: {
common: {
test: /[\\/]src[\\/]/,
name: 'common',
chunks: 'all',
priority: 2,
minChunks: 2,
},
// 分離css到一個css文件
styles: {
name: 'styles',
test: /\.css$/,
chunks: 'all',
priority: 9,
enforce: true,
}
}
},
runtimeChunk: {
name:"manifest"
}
},
plugins: [
new AutoDllPlugin({
inject: true, // will inject the DLL bundles to index.html
filename: '[name].dll.js',
path: './dll',
entry: {
// 第三方庫
react: ["react","react-dom","react-router", "react-router-dom",'react-loadable'],
antd: ['antd/es']
}
})
]
})
if (config.build.productionGzip) {
const CompressionWebpackPlugin = require('compression-webpack-plugin')
webpackConfig.plugins.push(
new CompressionWebpackPlugin({
filename: '[path].gz[query]',
algorithm: 'gzip',
test: new RegExp(
'\\.(' +
config.build.productionGzipExtensions.join('|') +
')$'
),
threshold: 10240,
minRatio: 0.8
})
)
}
if (config.build.bundleAnalyzerReport) {
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
webpackConfig.plugins.push(new BundleAnalyzerPlugin())
}
module.exports = webpackConfig
複製代碼
build.js
用於npm run build
構建
// https://github.com/shelljs/shelljs
process.env.NODE_ENV = 'production'
var ora = require('ora')
var path = require('path')
var chalk = require('chalk')
var shell = require('shelljs')
var webpack = require('webpack')
var config = require('./webpack.env.config')
var webpackConfig = require('./webpack.production.config')
var spinner = ora('building for production...')
spinner.start()
var assetsPath = path.join(config.build.assetsRoot, config.build.assetsSubDirectory)
shell.config.silent = true
shell.rm('-rf', assetsPath)
shell.mkdir('-p', assetsPath)
shell.cp('-R', 'static/*', assetsPath)
shell.config.silent = false
webpack(webpackConfig, function (err, stats) {
spinner.stop()
if (err) throw err
process.stdout.write(stats.toString({
colors: true,
modules: false,
children: false,
chunks: false,
chunkModules: false
}) + '\n\n')
console.log(chalk.cyan(' Build complete.\n'))
console.log(chalk.yellow(
' Tip: built files are meant to be served over an HTTP server.\n' +
' Opening index.html over file:// won\'t work.\n'
))
})
複製代碼
修改package.json
"scripts": {
"start": "node webpack/start.js",
"build": "node webpack/build.js"
},
複製代碼
七、總結
Webpack
有了初步瞭解,但關於Webpack
的內部原理沒有仔細探究過Webpack
基礎內容speed-measure-webpack-plugin
,webpack-bundle-analyzer
等typeScript
但並無很好的利用typeScript