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

大門發表於2019-06-28

前言

上一篇文章介紹了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))
    }
  } 
}
複製程式碼

最後

到這裡生產開發環境的配置基本上就結束了,如果有漏掉的或者配置不對的地方,希望大佬指出。

最後附上地址 專案地址,如果有不對的地方希望各位指出,感謝。

參考的文章:

相關文章