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

倉庫地址地址:github.com/ccokl/webpa…javascript

內附一個完整的 Webpack + React + Ant Design 配置css

本文涉及Ant Design + React 的 Webpack配置優化,實踐發現對於大型後臺系統來講React腳手架配置太重,修改其默認配置成本高,建議自建Webpack配置。

1、背景

按照 Ant Design 官網用 React 腳手構建的後臺項目,剛接手項目的時候大概30條路由左右,個人用的機子是 Mac 8G 內存,打包完成須要耗時2分鐘左右,決定優化一下。主要實踐了兩種方式,一種是修改腳手架配置,一種是自定義配置。html

項目技術棧: React + React Router + TypeScript + Ant Designjava

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

  1. React 腳手架默認打包構建出來的文件包含 map 文件
  2. Ant Design 以及項目中使用的第三方模塊太大
  3. babel-loader 編譯過程慢

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

  1. npm run rject 暴露出 Webpack 配置信息,直接進行修改。
  2. 使用 react-app-rewired (一個對 create-react-app 進行自定義配置的社區解決方案)Ant Design 官網推薦。

自定義Webpack配置步驟:webpack

  1. 基礎配置
  2. 開發環境配置
  3. 生產環境配置

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

一、準備工做git

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

npm i react-app-rewired customize-cra --save-dev
複製代碼

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

"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-pluginAnt 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用來抽離複用模塊;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腳手架配置太麻煩

3、自定義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']
      }
    })
  ]
})

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
相關文章
相關標籤/搜索