搭建自己的React+Typescript环境(二)

3,810 阅读8分钟

前言

上一篇文章介绍了React+Typescript的基础环境搭建,并没有做任何优化配置,以及根据不同的开发环境拆分配置,这篇文章主要就是介绍这些,并且所有配置都是在上篇文章的基础上,如果有什么问题或者不对的地方,希望大佬们能及时指出,最后有项目地址~

要用到的几个依赖

  • webpack-merge:合并webpack配置
  • webpack.DefinePlugin:在编译时创建一些全局变量
  • webpack.HotModuleReplacementPlugin:用于启用局部模块热重载,开发环境用的
  • html-webpack-plugin:根据webpack打包生成的bundle,来生成html
  • add-asset-html-webpack-plugin:跟html-webpack-plugin配合使用,把资源文件引用到它生成的html中
  • mini-css-extract-plugin:把css抽取到不同的文件中
  • terser-webpack-plugin:新的压缩js代码插件
  • optimize-css-assets-webpack-plugin:在webpack打包时优化压缩css代码,主要使用 cssnano 压缩器。
  • webpack.runtimeChunk:与持久化缓存有关
  • webpack.splitChunks:webpack 4 最大的改动就是废除了 CommonsChunkPlugin 引入了 optimization.splitChunks,用来配置分包策略。
  • webpack.DllPlugin:将模块预先编译,它会在第一次编译的时候将配置好的需要预先编译的模块编译在缓存中,第二次编译的时候,解析到这些模块就直接使用缓存
  • webpack.DllReferencePlugin:将预先编译好的模块关联到当前编译中,当 webpack 解析到这些模块时,会直接使用预先编译好的模块
  • webpack-bundle-analyzer:webpack打包分析器,可以直观看到各bundle占比
  • clean-webpack-plugin:清理打包文件夹

公共配置

在上篇webpack.common.js中继续添加和更新我们的配置。

定义可能用到的全局变量

有的时候需要在不同的环境定义不同的变量,就像vue-cli3创建的项目中的.env文件一样。

首先在 build 文件夹下新建一个 env.json 文件夹,并在里面写上你可能用到的全局变量。

{
  "dev": {
    "APP_ENVO": "dev",
    "BASEURL": "https://xxxx.xxxx.com/api/"
  },
  "test": {
    "APP_ENVO": "test",
    "BASEURL": "https://xxxx.xxxx.com/api/"
  },
  "pre": {
    "APP_ENVO": "pre",
    "BASEURL": "https://xxxx.xxxx.com/api/"
  },
  "prod": {
    "APP_ENVO": "prod",
    "BASEURL": "https://xxxx.xxxx.com/api/"
  }
}

接下来需要用到 yargs-parser 这个插件,yargs-parser: 用于将我们的npm scripts中的命令行参数转换成键值对的形式如 --mode development会被解析成键值对的形式mode: "development",便于在配置文件中获取参数。

然后在 package.json 中的scripts 脚本中加上我们的环境参数 --env test 等,例如:

  "scripts": {
    "dev": "webpack-dev-server --config build/webpack.dev.js --mode development --open",
    "test-build": "webpack --config build/webpack.prod.js --mode production --env test",
    "pre-build": "webpack --config build/webpack.prod.js --mode production --env pre",
    "prod-build": "webpack --config build/webpack.prod.js --mode production --env prod"
  },

然后在 webpack.common.js 中拿到这个参数,并利用 webpack.DefinePlugin 这个插件将这些变量配置进去

const argv = require('yargs-parser')(process.argv.slice(4))
const APP_ENV = argv.env || 'dev'

const env = require('./env.json')
const oriEnv = env[config.APP_ENV]
Object.assign(oriEnv, {
	APP_ENV: config.APP_ENV
})

const defineEnv = {}
for (let key in oriEnv) {
	defineEnv[`process.env.${key}`] = JSON.stringify(oriEnv[key])
}

module.exports={
  // ... 省略了其他配置
  plugins: [
    new webpack.DefinePlugin(defineEnv)
  ]
}

之后在项目启动后就可以通过 process.env.${key} 对应的键,拿到相应的值了。

修改输出 output

修改我们打包后的 js 输出目录以及名称,让它看起来清晰一些。

module.exports={
  output: {
    filename: 'js/[name].[chunkhash].js',
    path: path.join(__dirname, '../dist')
  }
}

开发环境配置

首先在 build 下新建一个 webpack.dev.js,然后需要安装 webpack-merge 来合并配置。

yarn add webpack-merge -D

接下来引入它以及公共配置文件,把之前的 devServer 移到这里,并引入 webpack.HotModuleReplacementPlugin 用于启用局部模块热重载方便我们开发,如果要配置代理的话,需要配置 devServer 下的 proxy,具体每个字段的意思,可以参照官网

关于 source-map 的话,可以理解它为你的源码与打包后代码的一个映射,因为打包后的代码都是经过压缩的,寻找错误调试会很麻烦,所以需要它,这里使用 eval-source-map ,对应的配置 devtool 选项。

const webpack=require('webpack')
const merge = require('webpack-merge')
const baseConfig=require('./webpack.common')
const HtmlWebpackPlugin = require('html-webpack-plugin')

const devConfig={
  mode: 'development', 
  devtool: 'eval-source-map',
  plugins: [
    new HtmlWebpackPlugin({
      filename: 'index.html',
      template: 'public/index.html',
      inject: true
    }),
    new webpack.HotModuleReplacementPlugin()
  ],
  devServer: {
    host: 'localhost',
    port: 3000,
    historyApiFallback: true,
    overlay: {//当出现编译器错误或警告时,就在网页上显示一层黑色的背景层和错误信息
      errors: true
    },
    inline: true,
    hot: true,
    // proxy: {
    //   '/api/v1': {
    //     target: '',
    //     ws: true,
    //     changeOrigin: true,
    //     pathRewrite: {
    //       '^/api/v1': '/api/v1'
    //     }
    //   }
    // }
  },
}

module.exports=merge(baseConfig,devConfig)

然后在 package.json 中 scripts 添加我们启动开发环境的命令,之后就可以启动项目了。

"dev": "webpack-dev-server --config build/webpack.dev.js --mode development --open"

生产环境配置

首先在 build 下新建一个 webpack.prod.js,跟开发环境一样,都需要引入公共配置,然后一点点的引入插件。

const merge = require('webpack-merge')
const baseConfig = require('./webpack.common')
const webpack = require('webpack')

const prodConfig = {
  mode: 'production',
  devtool: 'source-map'
}
module.exports = merge(baseConfig, prodConfig)

html-webpack-plugin

开头介绍过它,用于自动生成html,并默认将打包生成的js、css引入到html文件中,其中minify 配置项有很多,具体可以参照html-minifier

const HtmlWebpackPlugin = require('html-webpack-plugin')

const prodConfig = {
  plugins: [
    new HtmlWebpackPlugin({
      filename: 'index.html',
      template: 'public/index.html',
      inject: true,
      minify: {
        removeComments: true, // 去掉注释
        collapseWhitespace: true, // 去掉多余空白
        removeAttributeQuotes: true // 去掉一些属性的引号,例如id="moo" => id=moo
      }
    })
  ]
}

mini-css-extract-plugin

使用mini-css-extract-plugin来将css从js里分离出来,并且支持chunk css。

const MiniCssExtractPlugin = require('mini-css-extract-plugin')

// ...
const prodConfig = {
  // ...
  plugins: [
    // ...
    new MiniCssExtractPlugin({
      // Options similar to the same options in webpackOptions.output
      // both options are optional
      filename: assetsPath('css/[name].[contenthash].css'),
      chunkFilename: assetsPath('css/[name].[id].[contenthash].css')
    })
  ]
}

除此之外还要配置 webpack.commom.js, 把 style-loader 换成这个插件提供的 loader,使用的时候区分环境,MiniCssExtractPlugin不支持本地开发热重载,开发环境仍然使用 style-loader,以 css 文件为例。

  {
    test: /\.css$/, // 正则匹配文件路径
    exclude: /node_modules/,
    use: [
      // 
      APP_ENV !== 'dev' ? MiniCssExtractPlugin.loader : 'style-loader',
      {
        loader: 'css-loader', // 解析 @import 和 url() 为 import/require() 方式处理
        options: {
          importLoaders: 1 // 0 => 无 loader(默认); 1 => postcss-loader; 2 => postcss-loader, sass-loader
        }
      },
      'postcss-loader'
    ]
  }

clean-webpack-plugin

用于清除本地文件,在进行生产环境打包的时候,如果不清除dist文件夹,那么每次打包都会生成不同的js文件或者css文件堆积在文件夹中,注意版本带来的使用不同

const { CleanWebpackPlugin } = require('clean-webpack-plugin')

// ...
const prodConfig = {
  // ...
  plugins: [
    // ...
    new CleanWebpackPlugin(),
  ]
}

optimize-css-assets-webpack-plugin

在webpack打包时优化压缩css代码,主要使用 cssnano 压缩器,这个就不是配置在 plugins 里了,而是 optimization 下的 minimizer

const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin')

// ...
const prodConfig = {
  // ...
  optimization: { // 性能配置
    // ...
    minimizer: [
      new OptimizeCssAssetsPlugin({
        cssProcessor: require('cssnano'), // 使用 cssnano 压缩器
        cssProcessorOptions: {
          reduceIdents: false,
          autoprefixer: false,
          safe: true,
          discardComments: {
            removeAll: true
          }
        }
      })
    ]
  }
}

terser-webpack-plugin

optimize-css-assets-webpack-plugin 用于压缩 css 代码,而它用来压缩 js 代码,之前用到的是 uglifyjs-webpack-plugin 这一个,但是它好像需要 babel 的支持,而且现在官方推荐用 terser-webpack-plugin, 不过在使用上差不多,而且它不需要安装。

const TerserPlugin = require('terser-webpack-plugin')

// ...
const prodConfig = {
  // ...
  optimization: { // 性能配置
    // ...
    minimizer: [
      new TerserPlugin({
        cache: true,
        // parallel: true,
        terserOptions: {
          compress: {
            warnings: true,
            drop_console: true,
            drop_debugger: true,
            pure_funcs: ['console.log'] // 移除console
          }
        },
        sourceMap: true
      }),
    ]
  }
}

webpack.RuntimeChunk

它可以将包含chunks 映射关系的 list单独从 app.js里提取出来,因为每一个 chunk 的 id 基本都是基于内容 hash 出来的,所以你每次改动都会影响它,如果不将它提取出来的话,等于app.js每次都会改变。缓存就失效了。在 webpack4 中,无需手动引入插件,配置 runtimeChunk 即可。

const prodConfig = {
  // ...
  optimization: { // 性能配置
    // ...
    {
      runtimeChunk: true;
    }
  }
}

打包生成的 runtime.js非常的小,gzip 之后一般只有几 kb,但这个文件又经常会改变,我们每次都需要重新请求它,它的 http 耗时远大于它的执行时间了,所以建议不要将它单独拆包,有关优化就是将他将它内联到我们的 index.html 之中。

这里使用了 script-ext-html-webpack-plugin。

const ScriptExtHtmlWebpackPlugin = require("script-ext-html-webpack-plugin");

// 注意一定要在HtmlWebpackPlugin之后引用
// inline 的name 和你 runtimeChunk 的 name保持一致
new ScriptExtHtmlWebpackPlugin({
  //`runtime` must same as runtimeChunk name. default is `runtime`
  inline: /runtime\..*\.js$/
});

webpack.splitChunks

这个配置能让我们以一定规则抽离想要的包,webpack4 有一套默认的代码分包策略。

  • 新的 chunk 是否被共享或者是来自 node_modules 的模块
  • 新的 chunk 体积在压缩之前是否大于 30kb
  • 按需加载 chunk 的并发请求数量小于等于 5 个
  • 页面初始加载时的并发请求数量小于等于 3 个

关于按需加载跟页面初始加载就对应到 webpack.splitChunks.chunks 它表示将选择哪些块进行优化,async 表示只优化动态导入的包,而 initial 表示初始加载时导入的包,还有一个值 all 表示都会优化,默认是 async,也就是说如果你动态导入了一个包,压缩前大于30kb,并且你在代码中有超过5个地方引用了它,那么 webpack 就会将它单独打包出来。

通常我们需要将 node_modules 下的比较大的基础类库包抽出来,比如 vuex、vue之类的,或者像比较大的UI 组件库,比如 antd、element-ui 之类的也抽出来,以及自己写的可能会在多个页面间用到多次的组件。下面给一个我这里的配置,注意:拆包的时候不要过分的追求颗粒化,资源的加载策略并没什么完全的方案,都需要结合自己的项目找到最合适的拆包策略

const prodConfig = {
  // ...
  optimization: { // 性能配置
    // ...
    splitChunks: {
      chunks: 'async', // 提取的 chunk 类型,all: 所有,async: 异步,initial: 初始
      // minSize: 30000, // 默认值,新 chunk 产生的最小限制 整数类型(以字节为单位)
      // maxSize: 0, // 默认值,新 chunk 产生的最大限制,0为无限 整数类型(以字节为单位)
      // minChunks: 1, // 默认值,新 chunk 被引用的最少次数
      // maxAsyncRequests: 5, // 默认值,按需加载的 chunk,最大数量
      // maxInitialRequests: 3, // 默认值,初始加载的 chunk,最大数量
      // name: true, // 默认值,控制 chunk 的命名
      cacheGroups: { // 配置缓存组
        vendor: {
          name: 'vendor',
          chunks: 'initial',
          priority: 10, // 优先级
          reuseExistingChunk: false, // 允许复用已经存在的代码块
          test: /node_modules\/(.*)\.js/, // 只打包初始时依赖的第三方
        },
        common: {
          name: 'common',
          chunks: 'initial',
          // test: resolve("src/components"), // 可自定义拓展你的规则
          minChunks: 2,
          priority: 5,
          reuseExistingChunk: true
        }
      }
    }
  }
}

webpack.DllPlugin 与 webpack.DllReferencePlugin

像 React 相关基础运行环境,将这些基础模块打到一个包里,只要这些包的包的版本没升级,以后每次打包就不需要再编译这些模块,提高打包的速率,这里我们就可以用到 webpack.DllPlugin,然后使用 webpack.DllReferencePlugin 将这个 dll 包关联到当前的编译中去。

在 build 文件夹下新建一个 webpack.dll.js 文件,并写入下面的配置

const path = require('path')
const webpack = require('webpack')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')

module.exports = {
  mode:'production',
  entry: {
    // 还有redux 之类的也可以放进来
    vendor: ['react', 'react-dom', 'react-router-dom']
  },
  output: {
    filename: '[name].dll.[hash:8].js',
    path: path.join(__dirname, '../dll'),
    // 链接库输出方式 默认'var'形式赋给变量
    libraryTarget: 'var',
    // 全局变量名称 导出库将被以var的形式赋给这个全局变量 通过这个变量获取到里面模块
    library: '_dll_[name]_[hash:8]'
  },
  plugins: [
    // 每次运行时清空之前的 dll 文件
    new CleanWebpackPlugin({
      cleanOnceBeforeBuildPatterns: [path.join(__dirname, '../dll/**/*')]
    }),
    new webpack.DllPlugin({
      // path 指定manifest文件的输出路径
      path: path.join(__dirname, '../dll/[name].manifest.json'),
      // 和library 一致,输出的manifest.json中的name值
      name: '_dll_[name]_[hash:8]'
    })
  ]
}

下面修改 webpack.prod.js 使用DllReferencePlugin告诉 Webpack 使用了哪些动态链接库,然后并使用下面介绍的 add-asset-html-webpack-plugin 将其放入资源列表 html webpack插件注入到生成的 html 中。

其中 vendor.manifest.json 是由 DllPlugin 生成出,用于描述动态链接库文件中包含哪些模块。

// ...
const prodConfig = {
  // ...
  plugins: [
    // ...
    // 告诉 Webpack 使用了哪些动态链接库
    new webpack.DllReferencePlugin({
      manifest: path.join(__dirname, `../dll/vendor.manifest.json`)
    })
  ]
}

之后在 package.json 中scripts再加一个命令

  "scripts": {
    "dll": "webpack --config build/webpack.config.dll.js",
  }

然后运行它,就可以发现根目录下dll生成了两个文件 vendor.dll.xxxxxxxx.js,vendor.manifest.json

add-asset-html-webpack-plugin

我们使用它来将给定的静态资源css或者js引入到html-webpack-plugin生成的html文件中。

const AddAssetHtmlPlugin = require('add-asset-html-webpack-plugin')

// ...
const prodConfig = {
  // ...
  plugins: [
    // ...
    new AddAssetHtmlPlugin({
      filepath: resolve(`${DLL_PATH}/**/*.js`),
      includeSourcemap: false
    }),
  ]
}

webpack-bundle-analyzer

如果你想看你webpack打包之后输出文件的大小占比,可以使用这个插件,在webpack.prod.js 中加入如下配置,如果你想控制这个插件是否引入,可以使用一个变量:

if (config.bundleAnalyzerReport) {
  const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
  prodConfig.plugins.push(new BundleAnalyzerPlugin())
}

这样在打包结束后,会自动打开一个浏览器窗口,并展示输出文件的大小占比。

性能提示

如果想要在打包或者开发过程中展示一些性能提示,可以在 webpack.common.js 中加入如下配置。


module.exports={
   // ...
   performance: { // 性能提示,可以提示过大文件
    hints: "warning", // 性能提示开关 false | "error" | "warning"
    maxAssetSize: 100000, // 生成的文件最大限制 整数类型(以字节为单位)
    maxEntrypointSize: 100000, // 引入的文件最大限制 整数类型(以字节为单位)
    assetFilter: function(assetFilename) {
        // 提供资源文件名的断言函数
        return (/\.(png|jpe?g|gif|svg)(\?.*)?$/.test(assetFilename))
    }
  } 
}

最后

到这里生产开发环境的配置基本上就结束了,如果有漏掉的或者配置不对的地方,希望大佬指出。

最后附上地址 项目地址,如果有不对的地方希望各位指出,感谢。

参考的文章: