使用webpack4一步步搭建react項目(二)

前面已經實現了一個簡單webpack配置,接下來須要在前面的基礎上對webpack.config.js進行拆分。javascript

第二章 拆分webpack配置

項目開發時,咱們須要用webpack-dev-server啓動開發服務器,當咱們修改文件時,它能自動從新打包項目並刷新頁面。css

項目打包上線時,咱們但願webpack能進行更多的處理來優化打包後的代碼。html

針對不一樣的需求,咱們須要將配置文件拆分爲webpack.common.js webpack.dev.js webpack.prod.js前端

項目結構

項目結構v2

代碼

項目代碼 Github 倉庫java

配置webpack.common.js

這個文件是共用的配置。node

const path = require("path");

module.exports = {
  // 入口文件改成 .jsx文件
  entry: "./src/index.jsx",
  resolve: {
    extensions: [".js", ".json", ".jsx"]
  },
  module: {
    rules: [
      {
        test: /\.jsx?$/,
        // include告訴webpack只對src下的
        // js、jsx文件進行babel轉譯
        // 加快webpack的打包速度
        include: path.resolve(__dirname, "src"),
        use: "babel-loader"
      }
    ]
  }
};
複製代碼

配置webpack.dev.js

咱們須要使用webpack-merge將前面配好的webpack.comm.js合併進來。並且須要webpack-dev-server來啓動開發服務器。react

安裝:webpack

  • webpack-merge
  • webpack-dev-server
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const merge = require("webpack-merge");
const common = require("./webpack.common");

// 使用webpack-merge將webpack.common.js合併進來
module.exports = merge(common, {
  // 設置爲開發(development)模式
  mode: "development",
  // 設置source map,方便debugger
  devtool: "inline-source-map",
  output: {
    filename: "main.js",
    path: path.resolve(__dirname, "dist"),
    publicPath: "/"
  },
  devServer: {
    // 單頁應用的前端路由使用history模式時,這個配置很重要
    // webpack-dev-server服務器接受的請求路徑沒有匹配的資源時
    // 他會返回index.html而不是404頁面
    historyApiFallback: true
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        use: ["style-loader", "css-loader"]
      },
      {
        test: /\.(png|jpg|jpeg|svg|gif)$/,
        use: "file-loader"
      }
    ]
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: "./src/template.html",
      favicon: "./src/assets/favicon-32x32-next.png"
    })
  ]
});

複製代碼

配置webpack.prod.js

打包生產環境使用的代碼須要很是多的優化和處理,因此這個文件的配置會很是複雜。git

配置思路

  • 將css樣式從main.js內抽離到單獨的.css文件
  • 緩存處理
  • 將第三方庫和webpack的runtime從main.js內抽離出來
  • js、css、html代碼壓縮
  • 使用source-map替代inline-source-map
  • 懶加載(lazy loading)
  • 每次打包前先清空dist目錄

抽離css樣式

前面的webpack.dev.js只是簡單的使用了css-loaderstyle-loadergithub

css-loader將項目導入的css樣式轉爲js模塊,打包到main.js內。

style-loadermain.js內提供了一個能將css動態插入到html內的方法。

當用戶打開頁面時,會先加載html,而後加載main.js,最後運行js腳本將樣式插入到style標籤內。

這樣有多個缺點:

  • css被打包到了main.js內,增長了它的文件大小,並且也不方便對css作緩存
  • css樣式得等到main.js腳本運行,並插入到html時纔會有效果。這個空檔期雖然很短,但它會形成界面閃爍。

優勢:

  • 打包速度快

因此style-loadercss-loader的組合僅適用於開發。咱們須要使用mini-css-extract插件,並用該插件提供的loader來替代style-loader

// ...
plugins:[
  // 配置mini-css-extract插件
  new MiniCssExtractPlugin({
    // 設置抽取出來的css名字
    filename: "[name].[contentHash].css",
    chunkFilename: "[id].[contentHash].css"
  }),
  // ...
],
// ...
複製代碼
// ...
module:{
  rules:[
    {
      test: /\.css$/,
      // 使用MiniCssExtractPlugin.loader替代style-loader
      use: [MiniCssExtractPlugin.loader, "css-loader"]
    },
    // ...
  ]
},
// ...
複製代碼

緩存處理

緩存是前端頁面性能優化的重點。咱們但願瀏覽器能長久緩存資源,同時又能在第一時間獲取更新後的資源。

具體思路是:後端不對index.html作任何緩存處理,對css、js、圖片等資源作持久緩存。將output.filename配置爲"main.[contentHash].js",這樣打包後的main.js中間會加上一段contentHashcontentHash是根據打包文件內容產生的,內容改變它纔會發生改變。發佈時,因爲哈希值不一樣,服務器能同時保存着不一樣哈希版本的資源。這樣保證了發佈過程當中,用戶仍然可以訪問到舊資源,而且新用戶會訪問到新資源。

加上contentHash後打包文件名變成main.xxxxxx.js,其中xxxxxx表明一串很長的哈希值。

可是,如今咱們的業務代碼、引用的第三方庫,還有webpack生成的runtime都被捆綁打包到了main.xxxxxx.js內。第三方庫和webpack的runtime變更的頻率很是低,因此咱們不但願每次業務代碼的改動致使用戶得連同它們一塊兒從新下載一遍。所以咱們須要將它們從main.xxxxxx.js內抽離出來。

還有一點須要注意,webpack之前的版本有個小小的問題。在打包文件內容沒發生變化的狀況下contentHash任然會發生改變。此時須要使用webpack.HashedModuleIdsPlugin插件來替代默認的哈希生成。雖然webpack4修復了這個問題,可是官方文檔仍是推薦咱們使用webpack.HashedModuleIdsPlugin插件。

抽離第三方庫與webpack runtime

前面已經說明了爲何要抽離他們。

之前的版本使用commons-chunk-plugin插件來抽離第三方庫,webpack 4經過配置optimization.splitChunks來抽取。內部其實使用了split-chunks-plugin插件。

optimization.runtimeChunk選項配置爲single,能夠將webpack runtime抽離到單文件中。

// ...
optimization: {
  // 抽離webpack runtime到單文件
  runtimeChunk: "single",
  splitChunks: {
    chunks: "all",
    // 最大初始請求數量
    maxInitialRequests: Infinity,
    // 抽離體積大於80kb的chunk
    minSize: 80 * 1024,
    // 抽離被多個入口引用次數大於等於1的chunk
    minChunks: 1,
    cacheGroups: {
      // 抽離node_modules下面的第三方庫
      vendor: {
        test: /[\\/]node_modules[\\/]/,
        // 從模塊的路徑地址中得到庫的名稱
        name: function(module, chunks, chacheGroupKey) {
          const packageName = module.context.match(
            /[\\/]node_modules[\\/](.*?)([\\/]|$)/
          )[1];
          return `vendor_${packageName.replace("@", "")}`;
        }
      }
    }
  },
  // ...
},
// ...
複製代碼

js、css、html代碼壓縮

打包後的js、css、html會有註釋、空格、換行,開啓代碼壓縮能夠大幅度減小資源的體積。

mode設爲"production"時,會默認使用terser-webpack-plugin插件對js進行壓縮。咱們還要開啓html與css的壓縮,因此要重寫optimization.minimizer選項。

// ...
optimization: {
  minimizer: [
    // 壓縮css
    new OptimizeCssAssetsWebpackPlugin(),
    // 壓縮js,記得sourceMap設爲true
    new TerserWebpackPlugin({ sourceMap: true }),
    // 該插件還能對html進行壓縮
    new HtmlWebpackPlugin({
      template: "./src/template.html",
      favicon: "./src/assets/favicon-32x32-next.png",
      minify: {
        // 摺疊空白符(去除換行符和空格)
        collapseWhitespace: true,
        // 移除註釋
        removeComments: true,
        // 移除屬性上沒必要要的引號
        removeAttributeQuotes: true
      }
    })
  ],
  // ...
}
// ...
複製代碼

使用source-map替代inline-source-map

發生bug時,咱們很難經過打包後的代碼找出錯誤的源頭。因此咱們須要source map將代碼映射爲原來咱們手寫時候的樣子。

前面的webpack.dev.js內使用的是inline-source-map。它的缺點是將map內斂到了代碼內,這樣用戶會連同資源將map一塊兒下載。

因此咱們使用source-map,它會給打包後的每一個js單獨生成.map文件。

懶加載(lazy loading)

懶加載也叫按需加載。咱們當前打包的全部js會在頁面加載過程當中被加載運行。可是大多數狀況下,用戶並不會訪問應用的全部頁面與功能。咱們能夠將每一個頁面的代碼或一些不常使用的功能模塊作成按需加載,這樣能夠大大減少用戶初次訪問時所要加載的資源大小。

懶加載是webpack4默認支持的,不須要任何配置。前端人員須要在開發時使用dynamic import按需引入模塊。webpack會自動將dynamic import引入的模塊單獨打包爲一個chunk(注意:dynamic import語法上須要babel插件的支持,會在下一章節提到該插件)。

webpack官網提供的例子:

// print.js

console.log('The print.js module has loaded! See the network tab in dev tools...');

export default () => {
  console.log('Button Clicked: Here\'s "some text"!');
};
複製代碼
// src/index.js

// ...
button.onclick = e => import(/* webpackChunkName: "print" */ './print').then(module => {
  var print = module.default;

  print();
});
// ...
複製代碼

經過點擊按鈕,觸發加載print模塊。/* webpackChunkName: "print" */這個註釋告訴webpack該模塊打包成的chunk名字叫print

清空dist目錄

使用clean-webpack-plugin在打包前清空dist目錄。

webpack.prod.js的完整配置

安裝:

  • mini-css-extract-plugin
  • clean-webpack-plugin
  • terser-webpack-plugin optimize-css-assets-webpack-plugin
  • url-loader
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const merge = require("webpack-merge");
const common = require("./webpack.common");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const CleanWebpackPlugin = require("clean-webpack-plugin");
const TerserWebpackPlugin = require("terser-webpack-plugin");
const OptimizeCssAssetsWebpackPlugin = require("optimize-css-assets-webpack-plugin");
const webpack = require("webpack");

module.exports = merge(common, {
  // 設置爲生產(production)模式
  mode: "production",
  // 在生產環境中使用"source-map"而不是"inline-source-map"
  devtool: "source-map",
  output: {
    // 這裏添加contentHash
    // 因爲咱們的entry中沒有配置入口的名稱
    // webpack會默認取名爲main
    // 所以這裏的配置會生成"main.xxxxxx.js"
    filename: "[name].[contentHash].js",
    // 經過splitChunks抽離的js文件名格式
    chunkFilename: "[name].[contentHash].chunk.js",
    path: path.resolve(__dirname, "dist"),
    publicPath: "/"
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        // 這裏使用MiniCssExtractPlugin.loader替代style-loader
        use: [MiniCssExtractPlugin.loader, "css-loader"]
      },
      {
        test: /\.(png|jpg|jpeg|svg|gif)$/,
        use: {
          // 這裏使用url-loader替代file-loader
          loader: "url-loader",
          options: {
            // 當圖片小於8kb時,url-loader會將圖片轉爲base64
            // 這樣能夠減小http請求的數量
            // 若是大於8kb的話,url-loader會將圖片交給file-loader處理
            // 因此url-loader須要依賴file-loader
            limit: 1024 * 8,
            name: "img/[name].[hash:8].[ext]"
          }
        }
      }
    ]
  },
  optimization: {
    // 抽離webpack runtime到單文件
    runtimeChunk: "single",
    // 壓縮器
    minimizer: [
      // 壓縮css
      new OptimizeCssAssetsWebpackPlugin(),
      // 壓縮js,記得將sourceMap設爲true
      // 不然會沒法生成source map
      new TerserWebpackPlugin({ sourceMap: true }),
      // 該插件還能壓縮html
      new HtmlWebpackPlugin({
        template: "./src/template.html",
        favicon: "./src/assets/favicon-32x32-next.png",
        minify: {
          // 摺疊空白符
          collapseWhitespace: true,
          // 移除註釋
          removeComments: true,
          // 移除屬性多餘的引號
          removeAttributeQuotes: true
        }
      })
    ],
    splitChunks: {
      chunks: "all",
      // 最大初始請求數
      maxInitialRequests: Infinity,
      // 80kb以上的chunk抽離爲單獨的js文件
      // 配合上面的 maxInitialRequests: Infinity
      // 小於80kb的全部chunk會被打包一塊兒
      // 這樣能夠減小初始請求數
      // 你們能夠根據本身的狀況設置
      minSize: 80 * 1024,
      // 抽離多入口引用次數1以上的chunk
      minChunks: 1,
      cacheGroups: {
        // 抽離node_modules內的第三方庫
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          // 根據路徑得到第三方庫的名稱
          // 並將抽離的chunk以"vendor_thirdPartyLibrary"格式命名
          name: function(module, chunks, chacheGroupKey) {
            const packageName = module.context.match(
              /[\\/]node_modules[\\/](.*?)([\\/]|$)/
            )[1];
            return `vendor_${packageName.replace("@", "")}`;
          }
        }
      }
    }
  },
  plugins: [
    // 每次打包前,先清除輸出目錄
    new CleanWebpackPlugin(),
    // 抽離css
    new MiniCssExtractPlugin({
      filename: "[name].[contentHash].css",
      chunkFilename: "[id].[contentHash].css"
    }),
    // 確保在文件沒發生改變時,contentHash也不會變化
    new webpack.HashedModuleIdsPlugin()
  ]
});

複製代碼

配置npm腳本

// ...
"scripts": {
  "start": "webpack-dev-server --config webpack.dev.js",
  "build": "webpack --config webpack.prod.js"
},
// ...
複製代碼

結尾

基本配置完成,能夠安裝react-router redux進行單頁應用開發了

npm i react-router-dom redux react-redux redux-thunk
複製代碼

下章節內容:添加babel插件支持decorator、類屬性與dynamic import;添加sass預處理;添加postcss Autoprefixer自動補充瀏覽器廠商前綴;使用.browserslistrc配置須要兼容的瀏覽器範圍;添加Prettier ESLint來規範與格式化代碼;

其餘章節

參考

Learn Webpack

Webpack 的 Bundle Split 和 Code Split 區別和應用

webpack guides

learn Webpack step by step

webpack 持久化緩存實踐

大公司裏怎樣開發和部署前端代碼?

相關文章
相關標籤/搜索