基於 Ant Desigin 的後臺管理項目打包優化實踐

背景

按照 Ant Design 官網用 React 腳手構建的後臺項目,剛接手項目的時候大概30條路由左右,個人用的機子是 Mac 8G 內存,打包完成須要耗時2分鐘左右,決定優化一下。css

項目技術棧: React + React Router + TypeScript + Ant Design html

 

構建時間慢可能的緣由:node

  1. React 腳手架默認打包構建出來的文件包含 map 文件react

  2. Ant Desigin 以及項目中使用的第三方模塊太大webpack

  3. babel-loader 編譯過程慢git

React 腳手架修改 Webpack 配置方案:github

  1. npm run rject 暴露出 Webpack 配置信息,直接進行修改。web

  2. 使用 react-app-rewired (一個對 create-react-app 進行自定義配置的社區解決方案)Ant Design 官網推薦。typescript

自定義Webpack配置步驟:shell

  1. 基礎配置

  2. 開發環境配置

  3. 生產環境配置

使用 customize-cra 修改React 腳手架配置實踐

一、準備工做

npm i react-app-rewired customize-cra --save-dev 安裝 react-app-rewired ,customize-cra,它提供一些修改 React 腳手架默認配置函數,具體參見:https://github.com/arackaf/customize-cra

安裝後,修改 package.json 文件的 scripts

"scripts": { "start": "react-app-rewired start", "build": "react-app-rewired build", "test": "react-app-rewired test", }

項目根目錄建立一個 config-overrides.js 用於修改Webpack 配置。

Ant Desigin 提供了一個按需加載的 babel 插件 babel-plugin-import

antd-dayjs-webpack-pluginAnt Desigin 官方推薦的插件,用於替換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 Desigin推薦的作法。

二、首屏加載優化

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 文件

首先安裝依賴包webpack-stats-plugin webpack-bundle-analyzer 前者爲了統計打包時間會在打包後的文件夾裏生成一個stats.json文件,後者用來分析打包後的各個模塊的大小。

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 Desigin的一些組件被重複打包,打包出來一共有13M多。

四、更細化分包

productionConfig配置添加,在入口文件添加vendors用來分離穩定不變的模塊;common用來抽離複用模塊;stylescss文件抽離成一個文件;

// 針對生產環境修改配置
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

  • 細化分包

  • 針對項目輕量級配置(後臺項目,基本只在 Chrome 瀏覽器下使用)

問題:

  • 抽離出來的第三方模塊大概有3M多,通過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'], untils: ['qs','qrcode'], plugins:['braft-editor','js-export-excel'] } }) ] }) 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構建

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

最後

Webpack配置demo地址:https://github.com/ccokl/webpack/tree/master/_webpack_for_ant

相關文章
相關標籤/搜索