404cat / blog

1 stars 0 forks source link

干掉roadhog,自己动手搭建webpack #1

Open 404cat opened 5 years ago

404cat commented 5 years ago

1、为什么要自己动手搭建webpack?


公司老项目的技术配置为react+dva+roadhog,到我接手时已经迭代开发了好几年,项目文件和依赖都十分巨大,据交接给我的开发人员说可能由于第三方升级的原因,导致编译速度十分缓慢;开发时的热更新速度一般都是10-20s+,线上打包速度更是六七分钟以上,线上打包也不是经常操作,所以时间长点忍忍也就过去了, 但是开发时的热更新速度这么慢简直不能忍受; image

2、如何干掉roadhog&配置webpack?


2.1、首先把项目中的关于roadhog的配置全部删除:

package.json:"roadhog": "^1.1.2" 根目录:.roadhogrc.js文件(暂时先不删删除,因为需要兼容项目,所以这里面的一些配置要copy到webpack当中);

2.2、webpack配置:

首先安装webpack相关:

"webpack": "^4.8.1",
"webpack-cli": "^3.3.5",
"webpack-merge": "^4.2.1",

接着在项目根目录创建

webpack.common.js, // 用来放置一些公用的配置
webpack.dev.js // 用来放置开发配置
webpack.prod.js // 放置线上打包配置

具体可以查看webpack并结合自身项目来进行配置,这里贴一下我的配置:

common.js

const path = require('path')
const webpack = require('webpack')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')
// const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
// let bundleConfig = require('./bundle-config.json')
const theme = require('../theme.config.js')
const config = require('../src/utils/config')

const nodeEnv = process.env.NODE_ENV || 'development'
const isDev = nodeEnv !== 'production'

console.log('webpack NODE_ENV CONFIG_ENV config.publicPath', process.env.NODE_ENV, process.env.CONFIG_ENV, config.publicPath, config.baseURL)

module.exports = {
  cache: isDev,
  entry: {
    index: ['babel-polyfill', 'es6-symbol/polyfill', path.resolve(__dirname, '..', 'src', 'index.js')],
  },
  output: {
    filename: isDev ? '[name].js' : '[name].[chunkhash:8].js',
    path: path.join(__dirname, '..', 'dist'),
    publicPath: config.publicPath,
    chunkFilename: isDev ? '[name].chunk.js' : '[name].[chunkhash:8].chunk.js',
  },
  resolve: {
    alias: {
      src: path.resolve(__dirname, '..', 'src/'),
      '@config': path.resolve(__dirname, '..', './src/utils/config'),
      '@assets': path.resolve(__dirname, '..', './src/assets'),
      '@srcRoot': path.resolve(__dirname, '..', './src'),
      '@components': path.resolve(__dirname, '..', './src/components'),
      '@utils': path.resolve(__dirname, '..', './src/utils'),
      '@themes': path.resolve(__dirname, '..', './src/themes'),
      '@services': path.resolve(__dirname, '..', './src/services'),
      '@models': path.resolve(__dirname, '..', './src/models'),
      '@routes': path.resolve(__dirname, '..', './src/routes'),
      '@shared': path.resolve(__dirname, '..', './src/shared'),
    },
    extensions: ['.js', '.jsx'], // 导入没有带后缀的文件
  },
  devtool: isDev ? 'inline-source-map' : 'hidden-source-map',
  module: {
    rules: [
      {
        test: /\.(js|mjs|jsx|ts|tsx)$/,
        include: [path.resolve(__dirname, '..', 'src')],
        exclude: /node_modules/,
        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: {
    net: 'empty',
    fs: 'empty',
    module: 'empty',
    child_process: 'empty',
    tls: 'empty',
    dgram: 'empty',
  },
  optimization: {
    splitChunks: {
      chunks: 'async',
      minSize: 30000,
      minChunks: 1,
      maxAsyncRequests: 5,
      maxInitialRequests: 3,
      automaticNameDelimiter: '~',
      name: true,
      cacheGroups: {
        styles: {
          name: 'styles',
          test: /\.(css|less)/,
          chunks: 'async',
          enforce: true,
        },
        commons: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          chunks: 'async',
        },
      },
    },
  }, // 提取公共代码
  plugins: [
    new webpack.DefinePlugin({
      'process.env': {},
      'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
      'process.env.CONFIG_ENV': JSON.stringify(process.env.CONFIG_ENV),
      'process.env.THEME': JSON.stringify(theme),
      'process.env.PUBLICPATH': JSON.stringify(config.publicPath),
    }),
    new MiniCssExtractPlugin({
      filename: '[name].css',
    }),
    // new webpack.DllReferencePlugin({
    //   context: __dirname,'..',
    //   // eslint-disable-next-line global-require
    //   manifest: require('./dll/manifest.json'),
    // }),
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, '..', 'src', 'index.ejs'), // 模板
      filename: 'index.html',
      //   vendorJsName: bundleConfig.vendor.js, // 加载dll文件
      hash: true, // 防止缓存
      minify: false,
    }),
    new CleanWebpackPlugin({
      verbose: true,
    }),
    new CopyWebpackPlugin([
      {
        from: path.resolve(__dirname, '..', 'public'),
      },
    ]),
    // new BundleAnalyzerPlugin(),

  ],
}

dev.js

const path = require('path')
const webpack = require('webpack')
const merge = require('webpack-merge')
const common = require('./webpack.common')
const { baseURL } = require('../src/utils/config')
// const { publicPath, NODE_ENV } = require('../src/utils/config')
// const theme = require('../theme.config.js')
// const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin

module.exports = merge(common, {
  devServer: {
    contentBase: path.resolve(__dirname, 'dist'), // 提供静态文件内容
    host: 'localhost',
    port: 8008,
    open: true,
    inline: true,
    hot: true,
    publicPath: '/',
    historyApiFallback: true,
    clientLogLevel: 'none',
    overlay: {
      errors: true,
    },
    proxy: {
      [baseURL]: {
        target: 'http://192.168.1.87:9003/',
        changeOrigin: true,
        secure: false,
        pathRewrite: {
          [baseURL]: '/aek-mspp',
        },
      },
    },
  },
  devtool: 'eval-source-map',
  stats: {
    children: false,
    warningsFilter: warn => warn.indexOf('Conflicting order between:') > -1,
  },
  performance: {
    hints: 'warning',
  },
  plugins: [
    new webpack.HotModuleReplacementPlugin(),
    // new BundleAnalyzerPlugin(),
  ],
})

prod.js

// const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
const merge = require('webpack-merge')
const ParallelUglifyPlugin = require('webpack-parallel-uglify-plugin')
const common = require('./webpack.common')

module.exports = merge(common, {
  devtool: false,
  optimization: {
    minimizer: [
      new ParallelUglifyPlugin({ // 多进程压缩
        cacheDir: '.cache/',
        uglifyJS: {
          output: {
            comments: false,
            beautify: false,
          },
          compress: {
            // warnings: false,
            drop_console: true,
            collapse_vars: true,
            reduce_vars: true,
          },
        },
      }),
      // new UglifyJsPlugin({ // 这个不用是因为上面的ParallelUglifyPlugin也可以起到压缩代码的作用
      //   exclude: /\.min\.js$/,
      //   cache: true,
      //   parallel: true, // 开启并行压缩,充分利用cpu
      //   sourceMap: false,
      //   extractComments: false, // 移除注释
      //   uglifyOptions: {
      //     compress: {
      //       unused: true,
      //       warnings: false,
      //       drop_debugger: true,
      //     },
      //     output: {
      //       comments: false,
      //     },
      //   },
      // }), // 压缩代码
    ],
  },
})

babel配置 在根目录配置 .babelrc 文件

{
    "presets": ["env", "react", "stage-0"],
    "plugins": [
        "dva-hmr",
        "transform-decorators-legacy",
        ["import", { "libraryName": "antd", "style": true }],
        "transform-class-properties",
        "transform-runtime",
        "add-module-exports",
        "recharts",
        "lodash"
    ],
    "env": {
        "development": {
        "plugins": ["dynamic-import-node"]
        },
        "production": {
        "plugins": ["transform-remove-console"]
        }
    }
}

package.json

{
  "private": true,
  "dependencies": {
    "antd": "^2.13.6",
    "axios": "^0.19.0",
    "babel-polyfill": "^6.23.0",
    "braft-editor": "^2.3.7",
    "braft-extensions": "0.0.18",
    "braft-polyfill": "0.0.2",
    "classnames": "^2.2.5",
    "countup.js": "^1.9.2",
    "decimal.js-light": "^2.3.0",
    "dva": "^2.0.1",
    "dva-loading": "^1.0.2",
    "dva-model-extend": "^0.1.1",
    "es6-symbol": "^3.1.1",
    "form-data": "^2.3.1",
    "lodash": "^4.17.4",
    "md5": "^2.2.1",
    "nprogress": "^0.2.0",
    "parse-domain": "^1.1.0",
    "path-to-regexp": "^3.0.0",
    "prop-types": "^15.5.10",
    "qrcode.react": "^0.8.0",
    "qs": "^6.2.0",
    "rc-tween-one": "^1.0.0",
    "react": "^15.5.4",
    "react-barcode": "^1.2.0",
    "react-color": "^2.17.3",
    "react-copy-to-clipboard": "^5.0.0",
    "react-dom": "^15.5.4",
    "react-helmet": "^5.0.0",
    "react-loadable": "^5.5.0",
    "react-modal": "^3.0.0",
    "react-pdf": "^2.2.0",
    "react-scroll": "^1.5.5",
    "recharts": "^1.0.0-alpha.1",
    "sockjs-client": "1.0.0",
    "stompjs": "^2.3.3"
  },
  "devDependencies": {
    "@hot-loader/react-dom": "^16.8.6",
    "@webassemblyjs/ast": "^1.3.1",
    "@webassemblyjs/wasm-edit": "^1.3.1",
    "address": "^1.0.3",
    "assets-webpack-plugin": "^3.9.10",
    "babel-core": "^6.26.3",
    "babel-eslint": "^8.2.3",
    "babel-loader": "^7.1.4",
    "babel-plugin-add-module-exports": "^1.0.2",
    "babel-plugin-dev-expression": "^0.2.1",
    "babel-plugin-dva-hmr": "^0.3.2",
    "babel-plugin-dynamic-import-node": "^2.3.0",
    "babel-plugin-import": "^1.7.0",
    "babel-plugin-lodash": "^3.3.2",
    "babel-plugin-module-resolver": "^2.7.1",
    "babel-plugin-recharts": "^1.1.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",
    "better-npm-run": "^0.1.1",
    "body-parser": "^1.18.3",
    "clean-webpack-plugin": "^3.0.0",
    "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-import-resolver-babel-module": "^4.0.0-beta.3",
    "eslint-plugin-compat": "^2.2.0",
    "eslint-plugin-import": "^2.7.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": "^3.0.0",
    "less": "^3.0.4",
    "less-loader": "^4.1.0",
    "less-vars-to-js": "^1.1.2",
    "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",
    "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.3.5",
    "webpack-dev-server": "^3.7.2",
    "webpack-merge": "^4.2.1",
    "webpack-parallel-uglify-plugin": "^1.1.0",
    "window-size": "^1.1.1"
  },
  "pre-commit": [
    "lint"
  ],
  "scripts": {
    "start": "cross-env CONFIG_ENV=development better-npm-run start-dev",
    "startOnline": "cross-env CONFIG_ENV=production CONFIG_PROXY_ONLINE_ENV=development better-npm-run start-dev",
    "build:production": "cross-env CONFIG_ENV=production better-npm-run build",
    "build:test": "cross-env CONFIG_ENV=test better-npm-run build",
    "build:preTest": "cross-env CONFIG_ENV=preTest better-npm-run build",
    "dll": "cross-env webpack --config webpack.dll.config.js --colors --display-modules"
  },
  "betterScripts": {
    "start-dev": {
      "command": "webpack-dev-server --profile --inline --display-modules --progress --color --config=./script/webpack.dev.js",
      "env": {
        "NODE_ENV": "development"
      }
    },
    "build": {
      "command": "webpack --profile --inline --display-modules --progress --color --config=./script/webpack.prod.js",
      "env": {
        "NODE_ENV": "production"
      }
    }
  },
  "theme": "./src/theme.config.js"
}

2.3、采坑点和需要注意的点

webpack基础

路由按需加载

路由按需加载是用的dva封装的dynamic,webpack在打包的时候会根据路由表的配置进行代码切割 或者使用 react 提供的loadable来进行代码切割; 但是有个坑就是在开发环境下进行代码切割在编译Module and chunk tree optimization阶段会巨慢, 然后搜索到参考1 这个issues,答案提及的babel-plugin-dynamic-import-node这个插件,可以把import转换为require,从而在编译的时候不进行切割; 但是当时忘了只能在Dev环境下使用这个插件,然后prod打包时代码不会进行切割了,然后各种排查。。。 代码切割参考webpack import

打包成功但是dist文件夹内没有生产出文件

开发和线上配置区分没有做好。把new webpack.HotModuleReplacementPlugin(),plugin放在了common中,这样在打包prod时也启用了热更新,热更新插件会把输出到dist的文件移除。

before & after

配置完之后首次打开项目12s,热更新编译的速度1-2s,跟之前的速度项目简直爽的不要不要的;


结语

本来觉得webpack配置很复杂,一不小心就是各种报错,所以也不太想了解;但是花了时间把这套配置搭建出来后,对开发效率有了质的提升,在解决各种报错和玄学问题的过程中也收货颇大,现在对webpack的了解虽然还只是冰山一角,但是现在看到它不会再去害怕它!

chenhonghui commented 5 years ago

package.json 里面的scripts分享一下呢

404cat commented 5 years ago

package.json 里面的scripts分享一下呢

已更新

chenhonghui commented 5 years ago

然后搜索到参考1 这个issues,答案提及的babel-plugin-dynamic-import-node这个插件,可以把import转换为require,从而在编译的时候不进行切割;
什么地方的import转换为require???