现在做的项目还是基于早期的ant-design-pro那套东西,技术栈是 react + antd + dva + roadhog。随着项目的迭代,不知从何时起,发现项目打包很慢,每次jenkins上部署都要5、6分钟的样子。正好最近项目需求较少,正好有时间可以捣鼓一下这个打包慢的问题。

去roadhog的github下面搜索issues,发现有好多人和我遇到同样的问题,解决方法大概就是把roadhog换成原汁原味的webpack4,于是我就开始着手改造了。

修改package.json

  1. 删除roadhog相关依赖

    "roadhog": "^2.5.0-beta.1",
    "roadhog-api-doc": "^1.0.3",
    复制代码
  2. 在devDependencies添加webpack4需要用到相关依赖

    "webpack": "^4.8.1",
    "webpack-cli": "^3.1.1",
    "webpack-dev-server": "^3.1.4"
    复制代码

我完整的devDependencies是这样的

 		"@hot-loader/react-dom": "^16.8.6",
    "@webassemblyjs/ast": "^1.3.1",
    "@webassemblyjs/wasm-edit": "^1.3.1",
    "address": "^1.0.3",
    "babel-core": "^6.26.3",
    "babel-eslint": "^8.2.3",
    "babel-loader": "^7.1.4",
    "babel-plugin-import": "^1.7.0",
    "babel-plugin-transform-class-properties": "^6.24.1",
    "babel-plugin-transform-decorators-legacy": "^1.3.4",
    "babel-plugin-transform-remove-console": "^6.9.2",
    "babel-plugin-transform-runtime": "^6.23.0",
    "babel-polyfill": "^6.26.0",
    "babel-preset-env": "^1.6.1",
    "babel-preset-react": "^6.24.1",
    "babel-preset-stage-0": "^6.24.1",
    "babel-runtime": "^6.26.0",
    "body-parser": "^1.18.3",
    "clean-webpack-plugin": "^0.1.19",
    "copy-webpack-plugin": "^4.5.1",
    "cross-env": "^5.1.1",
    "cross-port-killer": "^1.0.1",
    "css-hot-loader": "^1.4.4",
    "css-loader": "^0.28.11",
    "cssnano": "^3.10.0",
    "eslint": "^4.19.1",
    "eslint-config-airbnb": "^16.1.0",
    "eslint-plugin-compat": "^2.2.0",
    "eslint-plugin-jsx-a11y": "^6.0.3",
    "eslint-plugin-react": "^7.7.0",
    "estraverse": "^4.2.0",
    "file-loader": "^1.1.11",
    "happypack": "^5.0.0-beta.4",
    "hard-source-webpack-plugin": "^0.8.0",
    "html-webpack-plugin": "^4.0.0-beta.5",
    "husky": "^1.0.0-rc.4",
    "less": "^3.0.4",
    "less-loader": "^4.1.0",
    "mini-css-extract-plugin": "^0.4.1",
    "mockjs": "^1.0.1-beta3",
    "optimize-css-assets-webpack-plugin": "^4.0.1",
    "pro-download": "^1.0.1",
    "react-hot-loader": "^4.8.4",
    "redbox-react": "^1.5.0",
    "redux-devtools": "^3.4.1",
    "redux-devtools-dock-monitor": "^1.1.3",
    "redux-devtools-log-monitor": "^1.4.0",
    "regenerator-runtime": "^0.11.1",
    "sass-loader": "^7.0.1",
    "serve-index": "^1.9.1",
    "style-loader": "^0.21.0",
    "stylelint": "^9.2.0",
    "stylelint-config-standard": "^18.2.0",
    "type-is": "^1.6.15",
    "uglifyjs-webpack-plugin": "^1.2.5",
    "url-loader": "^1.0.1",
    "webpack": "^4.8.1",
    "webpack-bundle-analyzer": "^2.11.2",
    "webpack-cli": "^3.1.1",
    "webpack-dev-server": "^3.1.4"

复制代码
  1. 修改scripts里的启动、打包命令

    本地启动命令:

    "start": "cross-env ESLINT=none webpack-dev-server --config=webpack.config.development.js --mode development"
    复制代码

    打包命令:

    "build": "cross-env ESLINT=none webpack --config=webpack.config.production.js --mode production"
    复制代码

添加webpack配置文件

刚刚应该有注意到我上面的脚本里有用到webpack.config.development.jswebpack.config.production.js这两个文件。

这两个文件是需要我们手动新增到根目录下面的。

  • webpack.config.development.js是用来给本地启动用的,下面是这个文件里的完整内容
const webpack = require('webpack');
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const theme = require('./src/theme');

module.exports = {
  entry: path.resolve(__dirname, 'src', 'index.js'),
  devServer: {
    contentBase: path.resolve(__dirname, 'dist'),
    host: 'localhost', // 主机地址
    port: 8000, // 端口号
    open: true,
    inline: true,
    openPage: 'ioc/#/user/login',
    hot: true,
    publicPath: '/ioc/',
    historyApiFallback: true,
    overlay: {
      errors: true,
    },
  },
  output: {
    filename: '[name].js',
    path: path.resolve(__dirname, 'dist'),
    publicPath: './',
    chunkFilename: '[name].async.js',
  },
  resolve: {
    alias: {
      src: path.resolve(__dirname, 'src/'),
    },
  },
  stats: {
    children: false,
    warningsFilter: warn => warn.indexOf('Conflicting order between:') > -1,
  },

  module: {
    rules: [
      {
        test: /\.js$/,
        include: [path.resolve(__dirname, 'src')],
        loader: 'babel-loader?cacheDirectory',
      },
      {
        test: /\.css$/,
        use: [
          'css-hot-loader',
          MiniCssExtractPlugin.loader,
          {
            loader: 'css-loader',
            options: {
              importLoaders: 1,
            },
          },
        ],
      },
      {
        test: /\.less$/,
        use: [
          'css-hot-loader',
          MiniCssExtractPlugin.loader,
          {
            loader: 'css-loader',
            options: {
              importLoaders: 1,
              modules: true,
              localIdentName: '[name]_[local]-[hash:base64:5]',
            },
          },
          {
            loader: 'less-loader',
            options: {
              javascriptEnabled: true,
              modifyVars: theme,
            },
          },
        ],
        exclude: /node_modules/,
      },
      {
        test: /\.less$/,
        use: [
          MiniCssExtractPlugin.loader,
          {
            loader: 'css-loader',
            options: {
              importLoaders: 1,
            },
          },
          {
            loader: 'less-loader',
            options: {
              javascriptEnabled: true,
              modifyVars: theme,
            },
          },
        ],
        exclude: /src/,
      },
      {
        test: /\.(png|svg|jpg|gif|ttf)$/,
        use: [
          {
            loader: 'url-loader',
            options: {
              limit: 8192,
              outputPath: './assets/',
            },
          },
        ],
      },
    ],
  },
  node: {
    fs: 'empty',
    module: 'empty',
  },
  devtool: false,
  optimization: {
    splitChunks: {
      cacheGroups: {
        styles: {
          name: 'styles',
          test: /\.(css|less)/,
          chunks: 'all',
          enforce: true,
        },
      },
    },
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: '[name].css',
    }),
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, 'src', 'index.ejs'), // 模板
      filename: 'index.html',
      hash: true, // 防止缓存
    }),
    new CleanWebpackPlugin(['dist']),
    new CopyWebpackPlugin([
      {
        from: path.resolve(__dirname, 'public'),
      },
    ]),
    new webpack.HotModuleReplacementPlugin(),
  ],
};

复制代码
  • webpack.config.production.js是用来给打包用的,下面是这个文件里的完整内容
   
const webpack = require('webpack');
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const HappyPack = require('happypack');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const os = require('os');
const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length });

const theme = require('./src/theme');

module.exports = {
  entry: path.resolve(__dirname, 'src', 'index.js'),
  output: {
    filename: '[name].[chunkhash:8].js',
    path: path.resolve(__dirname, 'dist'),
    publicPath: './',
    chunkFilename: '[name].[chunkhash:8].async.js',
  },

  resolve: {
    alias: {
      src: path.resolve(__dirname, 'src/'),
    },
  },

  module: {
    rules: [
      {
        test: /\.js$/,
        include: [path.resolve(__dirname, 'src')],
        use: ['happypack/loader?id=babel'],
      },
      {
        test: /\.css$/,
        use: [
          MiniCssExtractPlugin.loader,
          {
            loader: 'css-loader',
            options: {
              importLoaders: 1,
            },
          },
        ],
      },
      {
        test: /\.less$/,
        use: [
          MiniCssExtractPlugin.loader,
          {
            loader: 'css-loader',
            options: {
              importLoaders: 1,
              modules: true,
              localIdentName: '[name]_[local]-[hash:base64:5]',
            },
          },
          {
            loader: 'less-loader',
            options: {
              javascriptEnabled: true,
              modifyVars: theme,
            },
          },
        ],
        exclude: /node_modules/,
      },
      {
        test: /\.less$/,
        use: [
          MiniCssExtractPlugin.loader,
          {
            loader: 'css-loader',
            options: {
              importLoaders: 1,
            },
          },
          {
            loader: 'less-loader',
            options: {
              javascriptEnabled: true,
              modifyVars: theme,
            },
          },
        ],
        exclude: /src/,
      },
      {
        test: /\.(png|svg|jpg|gif|ttf)$/,
        use: [
          {
            loader: 'url-loader',
            options: {
              limit: 8192,
              outputPath: './assets/',
            },
          },
        ],
      },
    ],
  },
  stats: {
    children: false,
    warningsFilter: warn => warn.indexOf('Conflicting order between:') > -1,
  },
  node: {
    fs: 'empty',
    module: 'empty',
  },
  optimization: {
    splitChunks: {
      cacheGroups: {
        styles: {
          name: 'styles',
          test: /\.(css|less)/,
          chunks: 'all',
          enforce: true,
        },
        commons: {
          name: 'commons',
          chunks: 'initial',
          minChunks: 2,
        },
        vendors: {
          name: 'vendors',
          test: /[\\/]node_modules[\\/]/,
          priority: -10,
        },
      },
    },
    runtimeChunk: true,
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: '[name].css',
    }),
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, 'src', 'index.ejs'), // 模板
      filename: 'index.html',
      hash: true, // 防止缓存
    }),
    new CleanWebpackPlugin(['dist']),
    new CopyWebpackPlugin([
      {
        from: path.resolve(__dirname, 'public'),
      },
    ]),
    new OptimizeCssAssetsPlugin({
      assetNameRegExp: /\.css$/g,
      cssProcessor: require('cssnano'),
      cssProcessorOptions: { discardComments: { removeAll: true } },
      canPrint: true,
    }),
    new HappyPack({
      id: 'babel',
      loaders: ['babel-loader?cacheDirectory'],
      threadPool: happyThreadPool,
    }),
    new webpack.HashedModuleIdsPlugin(),
  ],
};

复制代码

别小看上面这段配置,这可是我百度了一些webpack的配置模板,然后再去研究webpack4的接口文档,再结合我们的这个实际项目,不断调试报错,花了大半天时间搞出来的。

可以看到development的配置文件比production多了devServerhmr相关的配置,但是production的比development多了代码压缩、以及HappyPack相关配置。所以我觉得分成两个配置文件还是很有必要的,这样就可以根据本地调试和线上打包具体需求的差异修改不同的配置文件。

修改.babelrc

找到根目录下的.babelrc文件,稍作修改

{
  "presets": ["env", "react", "stage-0"],
  "plugins": [
    "dva-hmr",
    "transform-decorators-legacy",
    ["import", { "libraryName": "antd", "libraryDirectory": "es", "style": true }],
    "transform-class-properties",
    "transform-runtime"
  ],
  "env": {
    "production": {
      "plugins": ["transform-remove-console"]
    }
  }
}

复制代码

主要是添加了dva-hmr这个热更新插件。

启动

  1. 删除node_modules。由于 package.json中依赖改的比较多,所以建议先把原来项目中的node_modules文件夹删掉,以免造成不必要的冲突。

  2. 安装依赖

    npm i
    复制代码

    或者

    cnpm i 
    复制代码
  3. 本地启动

    npm start
    复制代码

    不出意外的话,本地应该可以启动成功,并且会自动打开浏览器页面

  4. 打包

    npm run build
    复制代码

    本地启动成功,再试着打一个线上环境的包,根目录下会多出一个dist文件夹,里面就是打包好的文件。

变化

打包时间明显缩短了,这一点不管是本地打包或者jenkins打包,都明显提升。

  • 迁移之前时间

  • 迁移之后时间

注意事项

  1. node版本最好升级到v8.11.1以上。一开始我本地可以打包成功,但是jenkens上打包失败了,看了一下log

    npm ERR! node v6.16.0
    npm ERR! npm  v3.10.10
    npm ERR! code ELIFECYCLE
    npm ERR! green-town-ioc@0.3.0 build: `cross-env ESLINT=none webpack --config=webpack.config.development.js --mode development`
    npm ERR! Exit status 1
    npm ERR! 
    npm ERR! Failed at the green-town-ioc@0.3.0 build script 'cross-env ESLINT=none webpack --config=webpack.config.development.js --mode development'.
    npm ERR! Make sure you have the latest version of node.js and npm installed.
    复制代码

    发现我本地电脑用的nodev8.11.1,但是jenkins服务器的版本是v6.16.0的,然后我让我们的运维童鞋把jenkins上的node版本升级了一下,就打包成功了。

  2. 关于HMR,用的是dva提供的babel插件dva-hmr,这样本地修改代码,页面就会自动刷新了。一开始我dev配置把sourceMap功能打开了,然后每次修改完代码,就会隔很久页面才能刷新,后来直接去掉了,热更新就快了很多

后续优化

  1. 第三方不经常变动的库,如react、antd等,打包成独立的文件引用进来,这样就不用每次都打包它们。这可能要用到DllReferencePlugin插件。
  2. hmr换成react-hot-loader,这样本地开发更新了代码后就可以保存react组件状态。当时也试过用它,但是始终没有成功,所以被迫无奈用了dva-hmr

有迁移想法的小伙伴欢迎在评论区交流。

Logo

瓜分20万奖金 获得内推名额 丰厚实物奖励 易参与易上手

更多推荐