React-CRA 多頁面配置(npm run eject)

更新時間:2019-01-05
版本信息:CRA v2.1.1 + Webpack v4.19.1

1、create-react-app 多頁面配置

爲何要進行多頁面配置

在使用 React 進行開發的過程當中,咱們一般會使用 create-react-app 腳手架命令來搭建項目,避免要本身配置 webpack,提升咱們的開發效率。可是使用 create-react-app 搭建的項目是單頁面應用,若是咱們是作中後臺管理頁面或 SPA,這樣是知足要求的,但若是項目有多入口的需求,就須要咱們進行一些配置方面的修改。javascript

通常有如下兩種方式將腳手架搭建的項目改造爲多頁面入口編譯:css

  1. 執行 npm run eject 命令,暴露配置文件,進行自定義配置。
  2. 使用 react-app-rewired 修改腳手架配置,在項目中安裝 react-app-rewired 後,能夠經過建立一個 config-overrides.js 文件來對 webpack 配置進行擴展。請參見:React-CRA 多頁面配置(react-app-rewired)

本文對第 1 種方法給出具體配置方案。html

webpack 基礎

本文對 React 多頁面應用配置的探討是基於使用 create-react-app 腳手架命令構建的項目,並非 webpack 多頁面配置教程。但上述兩種方案都應該具備必定的 webpack 的基礎,實際上當你決定要改造或加強項目的構建打包配置的時候,你應該先對 webpack 進行必定的瞭解。java

本項目使用的是 webpack v4.19.1 版本,對於 webpack v3.x 版本並不徹底適用,主要區別在於 optimize.splitChunksmini-css-extract-plugin 的使用,這裏附上 webpack v4.0.0 更新日誌 以及 webpack4升級徹底指南node

方案說明

經過執行 npm run eject 命令暴露腳手架項目的配置文件,而後根據項目須要進行自定義配置,從而實現擴展 webpack 配置,增長對 less 文件的支持、增長 antd 組件的按需加載、處理 html 文檔中的圖片路徑問題等,甚至能夠將單頁面入口編譯修改成多頁面入口編譯的方式。react

固然,咱們也能夠經過使用 react-app-rewired 在不暴露配置文件的狀況下達到擴展項目配置的目的,原則上經過執行 npm run eject 命令能夠實現的配置改造,經過使用 react-app-rewired 也一樣能夠實現,包括多頁面配置的改造。咱們在上文中已經給出了使用 react-app-rewired 進行多頁面配置的方案:React-CRA 多頁面配置(react-app-rewired)webpack

可是,咱們並不建議使用 react-app-rewired 這種方式進行多頁面入口編譯的配置,若是隻是對腳手架項目進行配置擴展,好比增長 less 文件的支持或增長 antd 組件的按需加載,這種不暴露配置文件的方式是比較合適的,但若是是須要進行多頁面配置,最好仍是使用 npm run eject 暴露配置文件的方式。git

主要緣由是,經過 react-app-rewired 來實現多頁面配置,是須要對腳手架原來的配置具備必定了解的,相較於 npm run eject 暴露配置文件的方式來講,這種方式是不太具備透明度的,後面維護的難度較大。實際上,React-CRA 多頁面配置(react-app-rewired)方案自己就是基於 npm run eject 這種方案來完成的。github

本文方案主要實現了兩個方面的功能,一是將項目改形成多頁面入口編譯,二是在腳手架項目原有基礎上擴展經常使用配置,咱們的測試方案包含 index.htmladmin.html 兩個頁面(測試多頁面打包),是一個使用了 Ant Design、Redux、Less、echarts-for-react(數據可視化)、react-intl(多語言)、react-lazyload(延遲加載)等 npm 包的多頁面項目,也對這些主要功能實現過程當中出現的錯誤在 5、錯誤排查 章節中進行了羅列,好比 Redux 版本問題、proxy 本地代理設置方法等。web

版本的變更

使用 CRA 腳手架命令生成的項目免去了咱們本身配置 webpack 的麻煩,其內置的各類配置所須要的插件和依賴包的版本都是肯定的,是通過了檢驗的成熟配置,不會由於其中某個依賴包版本的問題形成構建出錯。但當咱們決定要本身動手配置 webpack 的時候,就意味着咱們要本身根據須要安裝一些 plugin 或 npm 依賴包,而這些插件和依賴包的版本可能不適用於當前 CRA 的版本,從而形成構建過程當中出現一些不可預期的錯誤。

所以,咱們須要查看 CRA 腳手架項目的 package.json 文件,對於其中已經列出的 dependencies 依賴包,咱們不該該改變這些依賴包的版本,而對於未列出的 npm 包,咱們在使用的過程當中須要逐個驗證,不要一次性安裝不少個 npm 包,不然執行構建命令的時候若是出錯就會很難排查,最好是根據咱們須要的功能逐個的安裝相應的 npm 包,肯定沒有問題,再進行下一個功能的擴展。

正是因爲版本的變更會對配置產生很大的影響,所以當咱們肯定了一個配置方案以後,不要再輕易去改動其中涉及到的 npm 包的版本,經過 package-lock.json 文件鎖定版本,防止配置方案錯亂。

在本文編輯的時候(2019-01-05),CRA 的最新版本爲 v2.1.2,從這個版本的腳手架項目開始,其配置文件已經發生了本質的改變,在這以前的版本中,其配置文件包含 webpack.config.dev.js(開發環境)和 webpack.config.prod.js(生產環境)兩個配置文件,但最新版本的腳手架項目中配置文件只有 webpack.config.js 一個文件。從這個版本開始,react-app-rewired v1.6.2 已經沒法使用,具體信息能夠參見 CRA >=2.1.2 breaking issue 2.1.2,至少在本文編輯的時候(2019.01.05),是沒法適用於 CRA >=2.1.2 版本的。

前面咱們已經提到,本文配置的多頁面入口編譯方案是兩種方案中的其中一種,兩種方案是基於同一版本的 CRA 項目改造的,從而能夠相互印證。所以本方案是基於最後一個能夠通用的 CRA v2.1.1 版原本作的,方案的 package.json 文件會附在文末。

2019-01-11 補充:上面提到的版本基本都是指 package.json 文件中列出的依賴包的版本,可是嚴格來說還應包含一些構建配置中使用的 node.js 工具包,好比 globbydir-glob等,這些工具包的版本更新也有可能會形成構建出錯。這種狀況的出現每每是沒法預期的,它們形成的影響通常比較普遍,而不只僅是出如今咱們方案配置的過程當中,這種錯誤基本上都會在 Github 上有相應的 issue 以及解決方法或修復措施,本文中也列出了遇到的一個這種類型的錯誤,詳見 5、錯誤排查 章節中的內容。

2、準備工做

關於 Nodejs 及 Webpack 基礎

  1. 瞭解 Node.js 相關工具模塊,咱們修改的配置文件都是在 node 環境中執行的,其中涉及到一些 node 模塊和 npm 上的一些包,好比 path 路徑模塊globby (加強版的 glob)等。這些模塊和庫能夠幫助咱們處理文件路徑方面的問題,在開始以前,能夠先粗略瞭解一下它們的用法。
  2. 瞭解 webpack 入口(entry)概念HtmlWebpackPlugin 插件,這有助於理解接下來的對頁面入口配置的修改。如前所述,若是要進行多頁面的配置,應該已經對於這些有了必定的瞭解了。

如何建立 CRA 腳手架項目

咱們的配置方案是基於 CRA v2.1.1 腳手架項目進行改造的,所以首先咱們要先建立一個 CRA 項目,詳見官方文檔:Create React App

可是這樣建立出來的項目默認是最新版本的 CRA 項目,咱們在上文中已經說明,咱們的配置方案是要求特定版本的 CRA 項目的,那麼如何建立特定的 CRA v2.1.1 版本項目?

CRA 項目版本的核心其實就是 react-scripts 的版本,咱們能夠先建立一個最新版本的腳手架項目,而後更改成 v2.1.1 版本,具體以下:

  1. 建立一個最新版本的 CRA 項目,參見官方文檔:Create React App
  2. 刪除 node_modules 文件夾,若是有 package-lock.json 文件或 yarn.lock 文件,也要一併刪除,不然從新安裝 node_modules 依賴包仍然會被鎖定爲原來的版本;
  3. 修改 package.json 文件,從新指定 react-scripts 的版本:

    "dependencies": {
        - "react": "^16.7.0",
        + "react": "^16.6.3",
        - "react-dom": "^16.7.0",
        + "react-dom": "^16.6.3",
        - "react-scripts": "2.1.3"
        + "react-scripts": "2.1.1"
      },
  4. 執行 yarn installnpm install 從新安裝項目依賴。

這樣,咱們就建立了一個 CRA v2.1.1 項目,這將做爲咱們進行多頁面入口編譯改造的基礎。

項目文件組織結構

在開始進行多頁面入口配置以前,須要先明確項目的文件組織結構,這關係到咱們如何準確獲取全部的入口文件,通常有如下幾種作法:

  1. 定義一個入口文件名稱的數組, 遍歷這個數組以獲取全部的入口文件。例如:APP_ENTRY=["index","admin"],這樣每增長一個入口就要相應的在這個數組裏增長一個元素。

    my-app
    └── src
        ├── index
        │   ├── index.js
        │   ├── index.less
        │   ├── components
        │   └── ...
        └── admin
            ├── index.js
            ├── index.less
            ├── components
            └── ...
  2. 在 my-app/public/ 下爲全部入口文件新建對應的 .html 文件,經過獲取 public 下全部的 .html 文件,肯定全部的入口文件。一樣的,每增長一個入口就須要在 public 下增長相應的 html 文件。

    my-app
    ├── public
    │   ├── index.html
    │   └── admin.html
    └── src
        ├── index
        │   ├── index.js
        │   ├── index.less
        │   ├── components
        │   └── ...
        └── admin
            ├── index.js
            ├── index.less
            ├── components
            └── ...
  3. 經過遍歷 src 下全部的 index.js 文件,肯定全部的入口文件,即每一個頁面所在的子文件夾下都有惟一的一個 index.js 文件,只要聽從這種文件組織規則,就能夠不用每次新增入口的時候再去修改配置了(固然,通常來講項目變更不大的狀況下配置文件完成以後幾乎不用修改)。

    my-app
    ├── public
    │   ├── index.html
    └── src
        ├── index
        │   ├── index.js
        │   ├── index.less
        │   ├── components
        │   └── ...
        └── admin
            ├── index.js
            ├── index.less
            ├── components
            └── ...

固然,文件組織結構還能夠有不少其餘的方式,只須要肯定其中一種文件組織規則,而後按照這個規則去修改和定義配置文件便可,主要就是肯定入口文件、指定入口文件對應的 html 模板文件、指定輸出文件等。可是,文件組織規則應該項目內保持統一,咱們在作項目時須要加一些限制,不然沒有哪一種配置文件能夠徹底只能的匹配全部的需求。

「若是你願意限制作事方式的靈活度,你幾乎總會發現能夠作得更好。」 ——John Carmark

3、具體方案

  1. 建立一個 CRA v2.1.1 版本腳手架項目
    請參照 2、準備工做 中的 如何建立 CRA 腳手架項目 一節,以及官方文檔:Create React App
  2. 執行 npm run eject 暴露配置文件
    咱們建立好了 CRA 項目以後,執行 npm run eject 命令暴露配置文件,eject 以後文件組織結構以下:

    Note: this is a one-way operation. Once you eject, you can’t go back!
    my-app
    ├── config
    │   ├── jest
    │   ├── env.js
    │   ├── paths.js
    │   ├── webpack.config.dev.js
    │   ├── webpack.config.prod.js
    │   └── webpackDevServer.config.js
    ├── node_modules
    ├── public
    ├── scripts
    │   ├── build.js
    │   ├── start.js
    │   └── test.js
    ├── package.json
    ├── README.md
    └── src

    其中 config 和 scripts 是咱們須要重點關注的,咱們的修改也主要集中在這兩個文件夾中的文件。執行 yarn start 命令,查看 http://localhost:3000/,驗證 npm run eject 的操做結果。

  3. 修改文件組織結構
    CRA 項目執行 npm run eject 以後文件組織結構爲:

    my-app
    ├── config
    │   ├── jest
    │   ├── env.js
    │   ├── paths.js
    │   ├── webpack.config.dev.js
    │   ├── webpack.config.prod.js
    │   └── webpackDevServer.config.js
    ├── node_modules
    ├── public
    ├── scripts
    │   ├── build.js
    │   ├── start.js
    │   └── test.js
    ├── package.json
    ├── .gitignore
    ├── README.md
    └── src
        ├── App.css
        ├── App.js
        ├── App.test.js
        ├── index.css
        ├── index.js
        ├── logo.svg
        └── serviceWorker.js

    咱們按照上文 項目文件組織結構 一節中的第 3 種文件組織方式將其修改成多頁面入口編譯的文件組織結構:

    // 本方案示例項目有兩個頁面 index.html & admin.html
    my-app
      ├── config
      ├── node_modules
      ├── package.json
      ├── .gitignore
      ├── README.md
      ├── scripts
      ├── public
      │   ├── favicon.ico
      │   ├── index.html // 做爲全部頁面的 html 模板文件
      │   └── manifest.json
      └── src
          ├── index.js // 空白文件, 爲了不構建報錯, 詳見下文
          ├── setupProxy.js // proxy 設置, 詳見下文(在當前操做步驟中能夠缺失)
          ├── index // index.html 頁面對應的文件夾
          │   ├── App.less
          │   ├── App.js
          │   ├── App.test.js
          │   ├── index.less // 使用 less 編寫樣式文件
          │   ├── index.js
          │   ├── logo.svg
          │   └── serviceWorker.js
          └── admin // admin.html 頁面對應的文件夾
              ├── App.less
              ├── App.js
              ├── App.test.js
              ├── index.less // 使用 less 編寫樣式文件
              ├── index.js
              ├── logo.svg
              └── serviceWorker.js

    在上述這種文件組織結構中,獲取全部入口文件的方法以下:

    const globby = require('globby');
    const entriesPath = globby.sync([resolveApp('src') + '/*/index.js']);

    這個示例項目是以 my-app/public/index.html 做爲全部頁面的 html 模板文件的,固然也能夠分別指定不一樣的 html 模板文件,這是根據項目須要和項目文件組織結構決定的。在這個示例項目中,因爲做爲模板的 html 文件只須要有個根元素便可,所以將其做爲全部入口的 html 模板文件。這樣的話,每一個頁面的 <title></title> 就須要在各自頁面中分別指定,通常能夠在頁面掛載以後進行操做,好比:

    class App extends Component {
      componentDidMount() {
        document.title = 'xxx';
      }
      render() {
        return (
          ...
        );
      }
    }
  4. 修改 Paths.js 文件
    因爲入口文件路徑在修改開發環境配置和修改生環境配置中都會用到,咱們將入口文件路徑的獲取放在 Paths.js 文件中進行獲取和導出,這樣開發環境和生產環境就均可以使用了。

    修改 my-app/config/paths.js 文件:

    // 引入 globby 模塊
    const globby = require('globby');
    // 入口文件路徑
    const entriesPath = globby.sync([resolveApp('src') + '/*/index.js']);
    // 在導出對象中添加 entriesPath
    module.exports = {
      dotenv: resolveApp('.env'),
      appPath: resolveApp('.'),
      appBuild: resolveApp('build'),
      ...
      entriesPath,
    };
  5. 修改開發環境配置
    修改 my-app/config/webpack.config.dev.js 文件

    • 增長如下代碼:
    // 獲取指定路徑下的入口文件
    function getEntries(){
      const entries = {};
      const files = paths.entriesPath;
      files.forEach(filePath => {
        let tmp = filePath.split('/');
        let name = tmp[tmp.length - 2];
        entries[name] = [
          // require.resolve('./polyfills'),
          require.resolve('react-dev-utils/webpackHotDevClient'),
          filePath,
        ];
      });
      return entries;
    }
    
    // 入口文件對象
    const entries = getEntries();
    
    // 配置 HtmlWebpackPlugin 插件, 指定入口生成對應的 html 文件,有多少個頁面就須要 new 多少個 HtmllWebpackPlugin
    // webpack配置多入口後,只是編譯出多個入口的JS,同時入口的HTML文件由HtmlWebpackPlugin生成,也需作配置。
    // chunks,指明哪些 webpack入口的JS會被注入到這個HTML頁面。若是不配置,則將全部entry的JS文件都注入HTML。
    // filename,指明生成的HTML路徑,若是不配置就是build/index.html,admin 配置了新的filename,避免與第一個入口的index.html相互覆蓋。
    const htmlPlugin = Object.keys(entries).map(item => {
      return new HtmlWebpackPlugin({
        inject: true,
        template: paths.appHtml,
        filename: item + '.html',
        chunks: [item],
      });
    });
    • 修改 module.exports 中的 entry、output 和 plugins:
    module.exports = {
      ...
      // 修改入口
      entry: entries,
      // 修改出口
      output: {
        pathinfo: true,
        // 指定不一樣的頁面模塊文件名
        filename: 'static/js/[name].js',
        chunkFilename: 'static/js/[name].chunk.js',
        publicPath: publicPath,
        devtoolModuleFilenameTemplate: info =>
          path.resolve(info.absoluteResourcePath).replace(/\\/g, '/'),
      },
      ...
      plugins: [
        ...
        // 替換 HtmlWebpackPlugin 插件配置
        // new HtmlWebpackPlugin({
        //   inject: true,
        //   template: paths.appHtml,
        // }),
        ...htmlPlugin,
      ],
    };
  6. 修改生產環境配置
    修改 my-app/config/webpack.config.prod.js 文件
    生產環境和開發環境的修改基本相同,只是入口對象和 HtmlWebpackPlugin 插件配置稍有不一樣(JS、css文件是否壓縮等)。

    • 增長如下代碼:
    // 獲取指定路徑下的入口文件
    function getEntries(){
      const entries = {};
      const files = paths.entriesPath;
      files.forEach(filePath => {
        let tmp = filePath.split('/');
        let name = tmp[tmp.length - 2];
        entries[name] = [
          // require.resolve('./polyfills'),
          filePath,
        ];
      });
      return entries;
    }
    
    // 入口文件對象
    const entries = getEntries();
    
    // 配置 HtmlWebpackPlugin 插件, 指定入口文件生成對應的 html 文件
    const htmlPlugin = Object.keys(entries).map(item => {
      return new HtmlWebpackPlugin({
        inject: true,
        template: paths.appHtml,
        filename: item + '.html',
        chunks: [item],
        minify: {
          removeComments: true,
          collapseWhitespace: true,
          removeRedundantAttributes: true,
          useShortDoctype: true,
          removeEmptyAttributes: true,
          removeStyleLinkTypeAttributes: true,
          keepClosingSlash: true,
          minifyJS: true,
          minifyCSS: true,
          minifyURLs: true,
        },
      });
    });
    • 修改 module.exports 中的 entry 和 plugins:
    module.exports = {
      ...
      // 修改入口
      entry: entries,
      ...
      
      plugins: [
        ...
        // 替換 HtmlWebpackPlugin 插件配置
        // new HtmlWebpackPlugin({
        //   inject: true,
        //   template: paths.appHtml,
        //   minify: {
        //     removeComments: true,
        //     collapseWhitespace: true,
        //     removeRedundantAttributes: true,
        //     useShortDoctype: true,
        //     removeEmptyAttributes: true,
        //     removeStyleLinkTypeAttributes: true,
        //     keepClosingSlash: true,
        //     minifyJS: true,
        //     minifyCSS: true,
        //     minifyURLs: true,
        //   },
        // }),
        ...htmlPlugin,
      ],
    };
  7. 修改 webpackDevServer 配置
    上述配置完成後,理論上就已經能夠打包出多入口的版本,可是在查找資料的過程當中,不少人提到了關於 historyApiFallback 設置的問題,問題描述以下:

    在完成以上配置後,使用 npm start 啓動項目,發現不管輸入 /index.html 仍是 /admin.html,顯示的都是 index.html。輸入不存在的 /xxxx.html,也顯示爲 index.html 的內容。

    (這個問題只在開發環境中會出現,生產環境不用考慮。本文示例項目並無遇到這個問題,不清楚是否與版本有關,或是配置不一樣的緣故,未深究。)

    本文的示例項目在完成上述配置以後,在開發環境中是能夠經過 url 路徑訪問不一樣頁面的,可是當訪問一個不存的地址時,會重定向爲 index.html 頁面。重定向這個現象與 devServer.historyApiFallback 有關:

    當使用 HTML5 History API 時,任意的 404 響應均可能須要被替代爲 index.html。

    若是遇到了上文中描述的關於開發環境中不能經過地址訪問不一樣頁面的狀況,解決方法以下:
    修改 my-app/config/webpackDevServer.config.js 文件

    • 增長以下代碼:
    // 在開發環境中若是要經過地址訪問不一樣的頁面, 須要增長如下配置
    const files = paths.entriesPath;
    const rewrites = files.map(v => {
      const fileParse = path.parse(v);
      return {
        from: new RegExp(`^\/${fileParse.base}`),
        to: `/build/${fileParse.base}`,
      };
    });
    • 修改 historyApiFallback
    historyApiFallback: {
      disableDotRule: true,
      rewrites: rewrites,
    },

    若是不但願 404 響應被重定向爲 index.html 頁面,而是如實的在頁面展現 404 Not Found 錯誤,能夠直接修改 historyApiFallback:

    historyApiFallback: {
      disableDotRule: false,
    },

4、擴展配置

以上的操做已經完成了多頁面入口編譯的配置,但在實際項目中咱們還須要擴展一些功能,好比增長對 less 的支持、設置別名路徑、更改輸出的文件名、使用 babel-plugin-import 按需加載組件等,這裏對一些經常使用功能給出具體配置方法。

  1. 使用 babel-plugin-import 按需加載組件
    在 React 項目中咱們一般會使用 Ant Design,這時咱們就須要設置按需加載,antd 官方文檔也給出了按需加載的方法:antd 按需加載

    // babel-loader option
    {
      plugins: [
        [
          require.resolve('babel-plugin-named-asset-import'),
          {
            loaderMap: {
              svg: {
                ReactComponent: '@svgr/webpack?-prettier,-svgo![path]',
              },
            },
          },
        ],
        // 按需加載
        ["import", {
          "libraryName": "antd",
          "libraryDirectory": "es",
          "style": true // `style: true` 會加載 less 文件
        }],
      ],
    }
  2. 增長 less 支持(安裝 less、less-loader)

    // style files regexes
    // const cssRegex = /\.css$/;
    const cssRegex = /\.(css|less)$/; // 增長對 less 的正則匹配
    const getStyleLoaders = (cssOptions, preProcessor) => {
      const loaders = [
        require.resolve('style-loader'),
        {
          loader: require.resolve('css-loader'),
          options: cssOptions,
        },
        {
          loader: require.resolve('postcss-loader'),
          options: {
            ident: 'postcss',
            plugins: () => [
              require('postcss-flexbugs-fixes'),
              require('postcss-preset-env')({
                autoprefixer: {
                  flexbox: 'no-2009',
                },
                stage: 3,
              }),
            ],
          },
        },
        // 編譯 less 文件
        {
          loader: require.resolve('less-loader'),
        }
      ];
      if (preProcessor) {
        loaders.push(require.resolve(preProcessor));
      }
      return loaders;
    };
  3. 設置別名路徑
    在開發過程當中,有些文件的路徑較深,當其引入一些公共模塊時,路徑嵌套就會比較多,而且當這些工具函數模塊或其餘公共模塊路徑變動的時候,涉及到的修改比較多,所以能夠經過設置別名路徑的方式減小這些工做。關於別名路徑,請參考:webpack-resolve.alias

    // 增長別名路徑
    alias: {
      'react-native': 'react-native-web',
      '@src': paths.appSrc, // 在使用中有些 Eslint 規則會報錯, 禁用這部分代碼的 Eslint 檢測便可
    },
  4. 增長對 html 文檔中圖片路徑的處理(安裝 html-withimg-loader)

    // 在 file-loader 下方添加如下代碼
    {
      // 處理 html 文檔中圖片路徑問題
      test: /\.html$/,
      loader: 'html-withimg-loader'
    },
  5. 輔助分析打包內容(安裝 webpack-bundle-analyzer)
    webpack-bundle-analyzer 是 webpack 可視化工具,它能夠將打包後的內容展現爲直觀的可交互樹狀圖,讓咱們瞭解構建包中真正引入的內容,以及各個文件由哪些模塊組成,從而幫助咱們優化項目打包策略,提高頁面性能。

    // 引入 BundleAnalyzerPlugin
    const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
    
    // 在 module.exports.plugins 中添加 BundleAnalyzerPlugin 插件實例
    // 輔助分析打包內容
    new BundleAnalyzerPlugin(),
  6. 更改生產模式輸出的文件名(build 版本)
    當咱們執行 yarn start 命令構建生產版本的時候,會發現構建出的文件比較多或者命名不符合咱們的要求,好比咱們會看到相似於 index.32837849.chunk.jsstyles.3b14856c.chunk.cssprecache-manifest.f4cdb7773e8c0f750c8d9d2e5166d629.js 這種形式的文件,咱們能夠根據項目的須要對相關的配置進行更改。如下只是給出一個示例,開發中應該根據項目的須要進行配置。

    // 更改輸出的腳本文件名
    output: {
      // filename: 'static/js/[name].[chunkhash:8].js',
      // chunkFilename: 'static/js/[name].[chunkhash:8].chunk.js',
      filename: 'static/js/[name].js?_v=[chunkhash:8]',
      chunkFilename: 'static/js/[name].chunk.js?_v=[chunkhash:8]',
    },
    
    // 更改輸出的樣式文件名
    new MiniCssExtractPlugin({
      // filename: 'static/css/[name].[contenthash:8].css',
      // chunkFilename: 'static/css/[name].[contenthash:8].chunk.css',
      filename: 'static/css/[name].css?_v=[contenthash:8]',
      chunkFilename: 'static/css/[name].chunk.css?_v=[contenthash:8]',
    }),
    
    // 更改 precacheManifestFilename
    new WorkboxWebpackPlugin.GenerateSW({
      clientsClaim: true,
      exclude: [/\.map$/, /asset-manifest\.json$/],
      importWorkboxFrom: 'cdn',
      navigateFallback: publicUrl + '/index.html',
      navigateFallbackBlacklist: [
        new RegExp('^/_'),
        new RegExp('/[^/]+\\.[^/]+$'),
      ],
      // 更改輸出的文件名
      precacheManifestFilename: 'precache-manifest.js?_v=[manifestHash]',
    }),
  7. 更改代碼拆分規則(build 版本)

    // 修改代碼拆分規則,詳見 webpack 文檔:https://webpack.js.org/plugins/split-chunks-plugin/#optimization-splitchunks
    // 這裏只是給出一個示例,開發中應該根據項目的須要進行配置
    splitChunks: {
      // chunks: 'all',
      // name: false,
      cacheGroups: {
        // 經過正則匹配,將 react react-dom echarts-for-react 等公共模塊拆分爲 vendor
        // 這裏僅做爲示例,具體須要拆分哪些模塊須要根據項目須要進行配置
        // 能夠經過 BundleAnalyzerPlugin 幫助肯定拆分哪些模塊包
        vendor: {
          test: /[\\/]node_modules[\\/](react|react-dom|echarts-for-react)[\\/]/,
          name: 'vendor',
          chunks: 'all', // all, async, and initial
        },
    
        // 將 css|less 文件合併成一個文件, mini-css-extract-plugin 的用法請參見文檔:https://www.npmjs.com/package/mini-css-extract-plugin
        // MiniCssExtractPlugin 會將動態 import 引入的模塊的樣式文件也分離出去,將這些樣式文件合併成一個文件能夠提升渲染速度
        // 其實若是能夠不使用 mini-css-extract-plugin 這個插件,即不分離樣式文件,可能更適合本方案,可是我沒有找到方法去除這個插件
        styles: {            
          name: 'styles',
          test: /\.css|less$/,
          chunks: 'all',    // merge all the css chunk to one file
          enforce: true
        }
      },
    },
    // runtimeChunk: true,
    runtimeChunk: false, // 構建文件中不產生 runtime chunk

5、錯誤排查

這裏對配置過程當中可能會出現的錯誤或異常進行記錄,這裏主要有兩類錯誤,一類是多頁面入口改造中能夠預期的常規錯誤,另外一類是因爲基礎工具版本的變更形成的不肯定錯誤。

常規錯誤

  1. Could not find a required file.

    Could not find a required file.
      Name: index.js
      Searched in: C:\Users\xxx\my-app\src
    error Command failed with exit code 1.
    info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.

    經過 create-react-app 腳手架搭建的項目以 my-app/src/index.js 做爲應用入口,當執行構建腳本時若是檢測到缺失了這個必要文件,node 進程會退出。在本方案中,根據咱們設定的文件組織結構的規則,如今 src 下不會直接存在 index.js 文件,但每一個獨立頁面的子文件夾下會存在 index.js 文件,如 my-app/src/index/index.js my-app/src/admin/index.js。解決方法以下:

    • 方法一:將關於這個必要文件的檢測語句註釋掉
      修改 my-app/scripts/start.js 文件(開發環境)、my-app/scripts/build.js 文件(生產環境)

      // Warn and crash if required files are missing
      // if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) {
      //   process.exit(1);
      // }
    • 方法二:在 my-app/src 下保留一個空白的 index.js 文件
  2. Failed to load resource: net::ERR_FILE_NOT_FOUND(build 版本)
    錯誤描述:執行 yarn buildnpm run build 構建生產版本時,構建出的頁面未能正確加載樣式和腳本文件,chrome 檢查工具報路徑錯誤。
    解決方法:修改 package.json 文件,指定 homepage 字段的值,本項目這裏指定爲相對路徑。

    "homepage": "./",
  3. When specified, "proxy" in package.json must be a string.

    When specified, "proxy" in package.json must be a string.
    Instead, the type of "proxy" was "object".
    Either remove "proxy" from package.json, or make it a string.

    錯誤描述:咱們在開發過程當中通常會在 package.json 文件中配置 proxy 代理服務器,可是在 CRA 2.x 升級之後對 proxy 的設置作了修改,具體請參見官方升級文檔:Move advanced proxy configuration to src/setupProxy.js
    解決方法:移除 package.json 文件中有關 proxy 的設置,使用 http-proxy-middleware,在 src 目錄下建立 setupProxy.js 文件。詳細方法請參見上述文檔。

其餘錯誤

咱們在上文 版本的變更 一節中已經對此有所說起,這類錯誤主要是由 npm 包版本的升級形成的,這些錯誤通常是不可預期的,也沒法在這裏所有涵蓋,只能就當前遇到的問題進行簡要記錄,可能隨着時間的推移,還會出現其餘的相似問題,也可能這些錯誤已經在後續的版本中被修復了,所以請勿糾結於這裏記錄的錯誤,若是遇到了這類錯誤,就查閱資料進行修正,若是沒有遇到,則無須理會。

  1. TypeError: Expected cwd to be of type string but received type undefined

    C:xxxmy-appnode_modulesdir-globindex.js:59
    throw new TypeError( Expected \cwd` to be of type `string` but received type `${typeof opts.cwd}``);
    TypeError: Expected cwd to be of type string but received type undefined

    錯誤描述:本文的寫做開始於 2019-01-05,在 2019-01-15 從新審覈本文方案的時候,遇到了這個錯誤,主要是因爲 dir-glob 版本的升級形成的,咱們在配置腳本中使用了 globby 的 sync 方法,dir-glob 版本升級以後,這個方法的調用會使得 dir-glob 拋出上述錯誤。詳細信息參見:Broken build do to major change from 2.0 to 2.2 以及 globby will pass opts.cwd = undefined to dir-glob, which leads to TypeError.
    解決方法:這裏給出的解決方法是限定於當前時間的,由於在本文編輯的時候(2019-01-15)這個 issue 尚未給出最終的解決方案,我的以爲可能會由 globby 進行修復。

    /* paths.js */
    // 修改獲取入口文件路徑的代碼
    - const entriesPath = globby.sync([resolveApp('src') + '/*/index.js']);
    + const entriesPath = globby.sync([resolveApp('src') + '/*/index.js'], {cwd: process.cwd()});
  2. Redux 版本錯誤:TypeError: Cannot read property 'state' of undefined(頁面報錯)

    錯誤描述:編譯構建過程沒有報錯,但頁面報錯:TypeError: Cannot read property 'state' of undefined。
    解決方法:redux 版本錯誤,在本文的配置方案中,應當使用 redux <=3.7.2 版本。

  3. Inline JavaScript is not enabled. Is it set in your options?

    // https://github.com/ant-design...
    .bezierEasingMixin();^ Inline JavaScript is not enabled. Is it set in your options?
    in C:xxxsrcmy-appnode_modulesantdesstylecolorbezierEasing.less (line 110, column 0)

    錯誤描述:less、less-loader 配置問題,提示須要容許行內 js 的執行,好比當咱們在項目中使用 antd 組件時,若是引入的樣式文件是 less 文件,構建時就會報上述錯誤。
    解決方法:在增長 less 文件支持時,設置 javascriptEnabled 爲 true。

    /* webpack.config.dev.js & webpack.config.prod.js */
    // 編譯 less 文件
    {
      loader: require.resolve('less-loader'),
      options: {
        // 解決報錯: Inline JavaScript is not enabled. Is it set in your options?
        javascriptEnabled: true,
      },
    },

6、源碼附錄

如下附錄 package.json 文件信息、開發環境及生產環境配置文件,配置文件中已將原有的英文註釋刪除,只保留了咱們改動處的中文註釋。

  1. package.json

    {
      "name": "my-app",
      "version": "0.1.0",
      "private": true,
      "dependencies": {
        "@babel/core": "7.1.0",
        "@svgr/webpack": "2.4.1",
        "antd": "^3.12.3",
        "babel-core": "7.0.0-bridge.0",
        "babel-eslint": "9.0.0",
        "babel-jest": "23.6.0",
        "babel-loader": "8.0.4",
        "babel-plugin-import": "^1.11.0",
        "babel-plugin-named-asset-import": "^0.2.3",
        "babel-preset-react-app": "^6.1.0",
        "bfj": "6.1.1",
        "case-sensitive-paths-webpack-plugin": "2.1.2",
        "chalk": "2.4.1",
        "css-loader": "1.0.0",
        "dotenv": "6.0.0",
        "dotenv-expand": "4.2.0",
        "echarts": "^4.2.0-rc.2",
        "echarts-for-react": "^2.0.15-beta.0",
        "eslint": "5.6.0",
        "eslint-config-react-app": "^3.0.5",
        "eslint-loader": "2.1.1",
        "eslint-plugin-flowtype": "2.50.1",
        "eslint-plugin-import": "2.14.0",
        "eslint-plugin-jsx-a11y": "6.1.2",
        "eslint-plugin-react": "7.11.1",
        "file-loader": "2.0.0",
        "fork-ts-checker-webpack-plugin-alt": "0.4.14",
        "fs-extra": "7.0.0",
        "html-webpack-plugin": "4.0.0-alpha.2",
        "html-withimg-loader": "^0.1.16",
        "http-proxy-middleware": "^0.19.1",
        "identity-obj-proxy": "3.0.0",
        "jest": "23.6.0",
        "jest-pnp-resolver": "1.0.1",
        "jest-resolve": "23.6.0",
        "less": "^3.9.0",
        "less-loader": "^4.1.0",
        "mini-css-extract-plugin": "0.4.3",
        "optimize-css-assets-webpack-plugin": "5.0.1",
        "pnp-webpack-plugin": "1.1.0",
        "postcss-flexbugs-fixes": "4.1.0",
        "postcss-loader": "3.0.0",
        "postcss-preset-env": "6.0.6",
        "postcss-safe-parser": "4.0.1",
        "react": "^16.6.3",
        "react-app-polyfill": "^0.1.3",
        "react-dev-utils": "^6.1.1",
        "react-dom": "^16.6.3",
        "react-intl": "^2.8.0",
        "react-lazyload": "^2.3.0",
        "react-loadable": "^5.5.0",
        "react-redux": "^6.0.0",
        "redux": "3.7.2",
        "redux-promise-middleware": "^5.1.1",
        "resolve": "1.8.1",
        "sass-loader": "7.1.0",
        "style-loader": "0.23.0",
        "terser-webpack-plugin": "1.1.0",
        "url-loader": "1.1.1",
        "webpack": "4.19.1",
        "webpack-bundle-analyzer": "^3.0.3",
        "webpack-dev-server": "3.1.9",
        "webpack-manifest-plugin": "2.0.4",
        "workbox-webpack-plugin": "3.6.3"
      },
      "scripts": {
        "start": "node scripts/start.js",
        "build": "node scripts/build.js",
        "test": "node scripts/test.js"
      },
      "eslintConfig": {
        "extends": "react-app"
      },
      "browserslist": [
        ">0.2%",
        "not dead",
        "not ie <= 11",
        "not op_mini all"
      ],
      "jest": {
        "collectCoverageFrom": [
          "src/**/*.{js,jsx,ts,tsx}",
          "!src/**/*.d.ts"
        ],
        "resolver": "jest-pnp-resolver",
        "setupFiles": [
          "react-app-polyfill/jsdom"
        ],
        "testMatch": [
          "<rootDir>/src/**/__tests__/**/*.{js,jsx,ts,tsx}",
          "<rootDir>/src/**/?(*.)(spec|test).{js,jsx,ts,tsx}"
        ],
        "testEnvironment": "jsdom",
        "testURL": "http://localhost",
        "transform": {
          "^.+\\.(js|jsx|ts|tsx)$": "<rootDir>/node_modules/babel-jest",
          "^.+\\.css$": "<rootDir>/config/jest/cssTransform.js",
          "^(?!.*\\.(js|jsx|ts|tsx|css|json)$)": "<rootDir>/config/jest/fileTransform.js"
        },
        "transformIgnorePatterns": [
          "[/\\\\]node_modules[/\\\\].+\\.(js|jsx|ts|tsx)$",
          "^.+\\.module\\.(css|sass|scss)$"
        ],
        "moduleNameMapper": {
          "^react-native$": "react-native-web",
          "^.+\\.module\\.(css|sass|scss)$": "identity-obj-proxy"
        },
        "moduleFileExtensions": [
          "web.js",
          "js",
          "web.ts",
          "ts",
          "web.tsx",
          "tsx",
          "json",
          "web.jsx",
          "jsx",
          "node"
        ]
      },
      "babel": {
        "presets": [
          "react-app"
        ]
      },
      "homepage": "./"
    }
  2. webpack.config.dev.js

    const fs = require('fs');
    const path = require('path');
    const resolve = require('resolve');
    const webpack = require('webpack');
    const PnpWebpackPlugin = require('pnp-webpack-plugin');
    const HtmlWebpackPlugin = require('html-webpack-plugin');
    const CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin');
    const InterpolateHtmlPlugin = require('react-dev-utils/InterpolateHtmlPlugin');
    const WatchMissingNodeModulesPlugin = require('react-dev-utils/WatchMissingNodeModulesPlugin');
    const ModuleScopePlugin = require('react-dev-utils/ModuleScopePlugin');
    const getCSSModuleLocalIdent = require('react-dev-utils/getCSSModuleLocalIdent');
    const getClientEnvironment = require('./env');
    const paths = require('./paths');
    const ManifestPlugin = require('webpack-manifest-plugin');
    const ModuleNotFoundPlugin = require('react-dev-utils/ModuleNotFoundPlugin');
    const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin-alt');
    const typescriptFormatter = require('react-dev-utils/typescriptFormatter');
    const publicPath = '/';
    const publicUrl = '';
    const env = getClientEnvironment(publicUrl);
    const useTypeScript = fs.existsSync(paths.appTsConfig);
    
    // style files regexes
    // const cssRegex = /\.css$/;
    const cssRegex = /\.(css|less)$/;
    const cssModuleRegex = /\.module\.css$/;
    const sassRegex = /\.(scss|sass)$/;
    const sassModuleRegex = /\.module\.(scss|sass)$/;
    
    const getStyleLoaders = (cssOptions, preProcessor) => {
      const loaders = [
        require.resolve('style-loader'),
        {
          loader: require.resolve('css-loader'),
          options: cssOptions,
        },
        {
          loader: require.resolve('postcss-loader'),
          options: {
            ident: 'postcss',
            plugins: () => [
              require('postcss-flexbugs-fixes'),
              require('postcss-preset-env')({
                autoprefixer: {
                  flexbox: 'no-2009',
                },
                stage: 3,
              }),
            ],
          },
        },
        // 編譯 less 文件
        {
          loader: require.resolve('less-loader'),
          options: {
            // 解決報錯: Inline JavaScript is not enabled. Is it set in your options?
            javascriptEnabled: true,
          },
        }
      ];
      if (preProcessor) {
        loaders.push(require.resolve(preProcessor));
      }
      return loaders;
    };
    
    // 獲取指定路徑下的入口文件
    function getEntries(){
      const entries = {};
      const files = paths.entriesPath;
      files.forEach(filePath => {
        let tmp = filePath.split('/');
        let name = tmp[tmp.length - 2];
        entries[name] = [
          // require.resolve('./polyfills'),
          require.resolve('react-dev-utils/webpackHotDevClient'),
          filePath,
        ];
      });
      return entries;
    }
    
    // 入口文件對象
    const entries = getEntries();
    
    // 配置 HtmlWebpackPlugin 插件, 指定入口生成對應的 html 文件,有多少個頁面就須要 new 多少個 HtmllWebpackPlugin
    // webpack配置多入口後,只是編譯出多個入口的JS,同時入口的HTML文件由HtmlWebpackPlugin生成,也需作配置。
    // chunks,指明哪些 webpack入口的JS會被注入到這個HTML頁面。若是不配置,則將全部entry的JS文件都注入HTML。
    // filename,指明生成的HTML路徑,若是不配置就是build/index.html,admin 配置了新的filename,避免與第一個入口的index.html相互覆蓋。
    const htmlPlugin = Object.keys(entries).map(item => {
      return new HtmlWebpackPlugin({
        inject: true,
        template: paths.appHtml,
        filename: item + '.html',
        chunks: [item],
      });
    });
    
    module.exports = {
      mode: 'development',
      devtool: 'cheap-module-source-map',
      // 修改入口
      entry: entries,
      // 修改出口
      output: {
        pathinfo: true,
        // 指定不一樣的頁面模塊文件名
        filename: 'static/js/[name].js',
        chunkFilename: 'static/js/[name].chunk.js',
        publicPath: publicPath,
        devtoolModuleFilenameTemplate: info =>
          path.resolve(info.absoluteResourcePath).replace(/\\/g, '/'),
      },
      optimization: {
        splitChunks: {
          chunks: 'all',
          name: false,
        },
        runtimeChunk: true,
      },
      resolve: {
        modules: ['node_modules'].concat(
          process.env.NODE_PATH.split(path.delimiter).filter(Boolean)
        ),
        extensions: paths.moduleFileExtensions
          .map(ext => `.${ext}`)
          .filter(ext => useTypeScript || !ext.includes('ts')),
        // 增長別名路徑
        alias: {
          'react-native': 'react-native-web',
          '@src': paths.appSrc, // 在使用中有些 Eslint 規則會報錯, 禁用這部分代碼的 Eslint 檢測便可
        },
        plugins: [
          PnpWebpackPlugin,
          new ModuleScopePlugin(paths.appSrc, [paths.appPackageJson]),
        ],
      },
      resolveLoader: {
        plugins: [
          PnpWebpackPlugin.moduleLoader(module),
        ],
      },
      module: {
        strictExportPresence: true,
        rules: [
          { parser: { requireEnsure: false } },
          {
            test: /\.(js|mjs|jsx)$/,
            enforce: 'pre',
            use: [
              {
                options: {
                  formatter: require.resolve('react-dev-utils/eslintFormatter'),
                  eslintPath: require.resolve('eslint'),
                },
                loader: require.resolve('eslint-loader'),
              },
            ],
            include: paths.appSrc,
          },
          {
            oneOf: [
              {
                test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/],
                loader: require.resolve('url-loader'),
                options: {
                  limit: 10000,
                  name: 'static/media/[name].[hash:8].[ext]',
                },
              },
              {
                test: /\.(js|mjs|jsx|ts|tsx)$/,
                include: paths.appSrc,
                loader: require.resolve('babel-loader'),
                options: {
                  customize: require.resolve(
                    'babel-preset-react-app/webpack-overrides'
                  ),
                  plugins: [
                    [
                      require.resolve('babel-plugin-named-asset-import'),
                      {
                        loaderMap: {
                          svg: {
                            ReactComponent: '@svgr/webpack?-prettier,-svgo![path]',
                          },
                        },
                      },
                    ],
                    // 按需加載
                    ["import", {
                      "libraryName": "antd",
                      "libraryDirectory": "es",
                      "style": true // `style: true` 會加載 less 文件
                    }],
                  ],
                  cacheCompression: false,
                },
              },
              {
                test: /\.(js|mjs)$/,
                exclude: /@babel(?:\/|\\{1,2})runtime/,
                loader: require.resolve('babel-loader'),
                options: {
                  babelrc: false,
                  configFile: false,
                  compact: false,
                  presets: [
                    [
                      require.resolve('babel-preset-react-app/dependencies'),
                      { helpers: true },
                    ],
                  ],
                  cacheDirectory: true,
                  cacheCompression: false,
                  sourceMaps: false,
                },
              },
              {
                test: cssRegex,
                exclude: cssModuleRegex,
                use: getStyleLoaders({
                  importLoaders: 1,
                }),
              },
              {
                test: cssModuleRegex,
                use: getStyleLoaders({
                  importLoaders: 1,
                  modules: true,
                  getLocalIdent: getCSSModuleLocalIdent,
                }),
              },
              {
                test: sassRegex,
                exclude: sassModuleRegex,
                use: getStyleLoaders({ importLoaders: 2 }, 'sass-loader'),
              },
              {
                test: sassModuleRegex,
                use: getStyleLoaders(
                  {
                    importLoaders: 2,
                    modules: true,
                    getLocalIdent: getCSSModuleLocalIdent,
                  },
                  'sass-loader'
                ),
              },
              {
                exclude: [/\.(js|mjs|jsx|ts|tsx)$/, /\.html$/, /\.json$/],
                loader: require.resolve('file-loader'),
                options: {
                  name: 'static/media/[name].[hash:8].[ext]',
                },
              },
              {
                // 處理 html 文檔中圖片路徑問題
                test: /\.html$/,
                loader: 'html-withimg-loader'
              },
            ],
          },
        ],
      },
      plugins: [
        // 替換 HtmlWebpackPlugin 插件配置
        // new HtmlWebpackPlugin({
        //   inject: true,
        //   template: paths.appHtml,
        // }),
        ...htmlPlugin,
        new InterpolateHtmlPlugin(HtmlWebpackPlugin, env.raw),
        new ModuleNotFoundPlugin(paths.appPath),
        new webpack.DefinePlugin(env.stringified),
        new webpack.HotModuleReplacementPlugin(),
        new CaseSensitivePathsPlugin(),
        new WatchMissingNodeModulesPlugin(paths.appNodeModules),
        new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
        new ManifestPlugin({
          fileName: 'asset-manifest.json',
          publicPath: publicPath,
        }),
        useTypeScript &&
          new ForkTsCheckerWebpackPlugin({
            typescript: resolve.sync('typescript', {
              basedir: paths.appNodeModules,
            }),
            async: false,
            checkSyntacticErrors: true,
            tsconfig: paths.appTsConfig,
            compilerOptions: {
              module: 'esnext',
              moduleResolution: 'node',
              resolveJsonModule: true,
              isolatedModules: true,
              noEmit: true,
              jsx: 'preserve',
            },
            reportFiles: [
              '**',
              '!**/*.json',
              '!**/__tests__/**',
              '!**/?(*.)(spec|test).*',
              '!src/setupProxy.js',
              '!src/setupTests.*',
            ],
            watch: paths.appSrc,
            silent: true,
            formatter: typescriptFormatter,
          }),
      ].filter(Boolean),
    
      node: {
        dgram: 'empty',
        fs: 'empty',
        net: 'empty',
        tls: 'empty',
        child_process: 'empty',
      },
      performance: false,
    };
  3. webpack.config.prod.js

    const fs = require('fs');
    const path = require('path');
    const webpack = require('webpack');
    const resolve = require('resolve');
    const PnpWebpackPlugin = require('pnp-webpack-plugin');
    const HtmlWebpackPlugin = require('html-webpack-plugin');
    const InlineChunkHtmlPlugin = require('react-dev-utils/InlineChunkHtmlPlugin');
    const TerserPlugin = require('terser-webpack-plugin');
    const MiniCssExtractPlugin = require('mini-css-extract-plugin');
    const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
    const safePostCssParser = require('postcss-safe-parser');
    const ManifestPlugin = require('webpack-manifest-plugin');
    const InterpolateHtmlPlugin = require('react-dev-utils/InterpolateHtmlPlugin');
    const WorkboxWebpackPlugin = require('workbox-webpack-plugin');
    const ModuleScopePlugin = require('react-dev-utils/ModuleScopePlugin');
    const getCSSModuleLocalIdent = require('react-dev-utils/getCSSModuleLocalIdent');
    const paths = require('./paths');
    const getClientEnvironment = require('./env');
    const ModuleNotFoundPlugin = require('react-dev-utils/ModuleNotFoundPlugin');
    const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin-alt');
    const typescriptFormatter = require('react-dev-utils/typescriptFormatter');
    const publicPath = paths.servedPath;
    const shouldUseRelativeAssetPaths = publicPath === './';
    const shouldUseSourceMap = process.env.GENERATE_SOURCEMAP !== 'false';
    const shouldInlineRuntimeChunk = process.env.INLINE_RUNTIME_CHUNK !== 'false';
    const publicUrl = publicPath.slice(0, -1);
    const env = getClientEnvironment(publicUrl);
    // const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
    
    if (env.stringified['process.env'].NODE_ENV !== '"production"') {
      throw new Error('Production builds must have NODE_ENV=production.');
    }
    
    const useTypeScript = fs.existsSync(paths.appTsConfig);
    
    // style files regexes
    // const cssRegex = /\.css$/;
    const cssRegex = /\.(css|less)$/;
    const cssModuleRegex = /\.module\.css$/;
    const sassRegex = /\.(scss|sass)$/;
    const sassModuleRegex = /\.module\.(scss|sass)$/;
    
    // common function to get style loaders
    const getStyleLoaders = (cssOptions, preProcessor) => {
      const loaders = [
        {
          loader: MiniCssExtractPlugin.loader,
          options: Object.assign(
            {},
            shouldUseRelativeAssetPaths ? { publicPath: '../../' } : undefined
          ),
        },
        {
          loader: require.resolve('css-loader'),
          options: cssOptions,
        },
        {
          loader: require.resolve('postcss-loader'),
          options: {
            ident: 'postcss',
            plugins: () => [
              require('postcss-flexbugs-fixes'),
              require('postcss-preset-env')({
                autoprefixer: {
                  flexbox: 'no-2009',
                },
                stage: 3,
              }),
            ],
            sourceMap: shouldUseSourceMap,
          },
        },
        // 編譯 less 文件
        {
          loader: require.resolve('less-loader'),
          options: {
            // 解決報錯: Inline JavaScript is not enabled. Is it set in your options?
            javascriptEnabled: true,
          },
        }
      ];
      if (preProcessor) {
        loaders.push({
          loader: require.resolve(preProcessor),
          options: {
            sourceMap: shouldUseSourceMap,
          },
        });
      }
      return loaders;
    };
    
    // 獲取指定路徑下的入口文件
    function getEntries(){
      const entries = {};
      const files = paths.entriesPath;
      files.forEach(filePath => {
        let tmp = filePath.split('/');
        let name = tmp[tmp.length - 2];
        entries[name] = [
          // require.resolve('./polyfills'),
          filePath,
        ];
      });
      return entries;
    }
    
    // 入口文件對象
    const entries = getEntries();
    
    // 配置 HtmlWebpackPlugin 插件, 指定入口文件生成對應的 html 文件
    const htmlPlugin = Object.keys(entries).map(item => {
      return new HtmlWebpackPlugin({
        inject: true,
        template: paths.appHtml,
        filename: item + '.html',
        chunks: [item],
        minify: {
          removeComments: true,
          collapseWhitespace: true,
          removeRedundantAttributes: true,
          useShortDoctype: true,
          removeEmptyAttributes: true,
          removeStyleLinkTypeAttributes: true,
          keepClosingSlash: true,
          minifyJS: true,
          minifyCSS: true,
          minifyURLs: true,
        },
      });
    });
    
    module.exports = {
      mode: 'production',
      bail: true,
      devtool: shouldUseSourceMap ? 'source-map' : false,
      // 修改入口
      // entry: [paths.appIndexJs],
      entry: entries,
      output: {
        path: paths.appBuild,
        // 更改輸出的腳本文件名
        // filename: 'static/js/[name].[chunkhash:8].js',
        // chunkFilename: 'static/js/[name].[chunkhash:8].chunk.js',
        filename: 'static/js/[name].js?_v=[chunkhash:8]',
        chunkFilename: 'static/js/[name].chunk.js?_v=[chunkhash:8]',
        publicPath: publicPath,
        devtoolModuleFilenameTemplate: info =>
          path
            .relative(paths.appSrc, info.absoluteResourcePath)
            .replace(/\\/g, '/'),
      },
      optimization: {
        minimizer: [
          new TerserPlugin({
            terserOptions: {
              parse: {
                ecma: 8,
              },
              compress: {
                ecma: 5,
                warnings: false,
                comparisons: false,
                inline: 2,
              },
              mangle: {
                safari10: true,
              },
              output: {
                ecma: 5,
                comments: false,
                ascii_only: true,
              },
            },
            parallel: true,
            cache: true,
            sourceMap: shouldUseSourceMap,
          }),
          new OptimizeCSSAssetsPlugin({
            cssProcessorOptions: {
              parser: safePostCssParser,
              map: shouldUseSourceMap
                ? {
                    inline: false,
                    annotation: true,
                  }
                : false,
            },
          }),
        ],
        // 修改代碼拆分規則,詳見 webpack 文檔:https://webpack.js.org/plugins/split-chunks-plugin/#optimization-splitchunks
        splitChunks: {
          // chunks: 'all',
          // name: false,
          cacheGroups: {
            // 經過正則匹配,將 react react-dom echarts-for-react 等公共模塊拆分爲 vendor
            // 這裏僅做爲示例,具體須要拆分哪些模塊須要根據項目須要進行配置
            // 能夠經過 BundleAnalyzerPlugin 幫助肯定拆分哪些模塊包
            vendor: {
              test: /[\\/]node_modules[\\/](react|react-dom|echarts-for-react)[\\/]/,
              name: 'vendor',
              chunks: 'all', // all, async, and initial
            },
    
            // 將 css|less 文件合併成一個文件, mini-css-extract-plugin 的用法請參見文檔:https://www.npmjs.com/package/mini-css-extract-plugin
            // MiniCssExtractPlugin 會將動態 import 引入的模塊的樣式文件也分離出去,將這些樣式文件合併成一個文件能夠提升渲染速度
            // 其實若是能夠不使用 mini-css-extract-plugin 這個插件,即不分離樣式文件,可能更適合本方案,可是我沒有找到方法去除這個插件
            styles: {            
              name: 'styles',
              test: /\.css|less$/,
              chunks: 'all',    // merge all the css chunk to one file
              enforce: true
            }
          },
        },
        // runtimeChunk: true,
        runtimeChunk: false, // 構建文件中不產生 runtime chunk
      },
      resolve: {
        modules: ['node_modules'].concat(
          process.env.NODE_PATH.split(path.delimiter).filter(Boolean)
        ),
        extensions: paths.moduleFileExtensions
          .map(ext => `.${ext}`)
          .filter(ext => useTypeScript || !ext.includes('ts')),
        // 增長別名路徑
        alias: {
          'react-native': 'react-native-web',
          '@src': paths.appSrc, // 在使用中有些 Eslint 規則會報錯, 禁用這部分代碼的 Eslint 檢測便可
        },
        plugins: [
          PnpWebpackPlugin,
          new ModuleScopePlugin(paths.appSrc, [paths.appPackageJson]),
        ],
      },
      resolveLoader: {
        plugins: [
          PnpWebpackPlugin.moduleLoader(module),
        ],
      },
      module: {
        strictExportPresence: true,
        rules: [
          { parser: { requireEnsure: false } },
          {
            test: /\.(js|mjs|jsx)$/,
            enforce: 'pre',
            use: [
              {
                options: {
                  formatter: require.resolve('react-dev-utils/eslintFormatter'),
                  eslintPath: require.resolve('eslint'),
                },
                loader: require.resolve('eslint-loader'),
              },
            ],
            include: paths.appSrc,
          },
          {
            oneOf: [
              {
                test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/],
                loader: require.resolve('url-loader'),
                options: {
                  limit: 10000,
                  name: 'static/media/[name].[hash:8].[ext]',
                },
              },
              {
                test: /\.(js|mjs|jsx|ts|tsx)$/,
                include: paths.appSrc,
                loader: require.resolve('babel-loader'),
                options: {
                  customize: require.resolve(
                    'babel-preset-react-app/webpack-overrides'
                  ),
                  plugins: [
                    [
                      require.resolve('babel-plugin-named-asset-import'),
                      {
                        loaderMap: {
                          svg: {
                            ReactComponent: '@svgr/webpack?-prettier,-svgo![path]',
                          },
                        },
                      },
                    ],
                    // 按需加載
                    ["import", {
                      "libraryName": "antd",
                      "libraryDirectory": "es",
                      "style": true // `style: true` 會加載 less 文件
                    }],
                  ],
                  cacheDirectory: true,
                  cacheCompression: true,
                  compact: true,
                },
              },
              {
                test: /\.(js|mjs)$/,
                exclude: /@babel(?:\/|\\{1,2})runtime/,
                loader: require.resolve('babel-loader'),
                options: {
                  babelrc: false,
                  configFile: false,
                  compact: false,
                  presets: [
                    [
                      require.resolve('babel-preset-react-app/dependencies'),
                      { helpers: true },
                    ],
                  ],
                  cacheDirectory: true,
                  cacheCompression: true,
                  sourceMaps: false,
                },
              },
              {
                test: cssRegex,
                exclude: cssModuleRegex,
                loader: getStyleLoaders({
                  importLoaders: 1,
                  sourceMap: shouldUseSourceMap,
                }),
                sideEffects: true,
              },
              {
                test: cssModuleRegex,
                loader: getStyleLoaders({
                  importLoaders: 1,
                  sourceMap: shouldUseSourceMap,
                  modules: true,
                  getLocalIdent: getCSSModuleLocalIdent,
                }),
              },
              {
                test: sassRegex,
                exclude: sassModuleRegex,
                loader: getStyleLoaders(
                  {
                    importLoaders: 2,
                    sourceMap: shouldUseSourceMap,
                  },
                  'sass-loader'
                ),
                sideEffects: true,
              },
              {
                test: sassModuleRegex,
                loader: getStyleLoaders(
                  {
                    importLoaders: 2,
                    sourceMap: shouldUseSourceMap,
                    modules: true,
                    getLocalIdent: getCSSModuleLocalIdent,
                  },
                  'sass-loader'
                ),
              },
              {
                loader: require.resolve('file-loader'),
                exclude: [/\.(js|mjs|jsx|ts|tsx)$/, /\.html$/, /\.json$/],
                options: {
                  name: 'static/media/[name].[hash:8].[ext]',
                },
              },
              {
                // 處理 html 文檔中圖片路徑問題
                test: /\.html$/,
                loader: 'html-withimg-loader'
              },
            ],
          },
        ],
      },
      plugins: [
        // new HtmlWebpackPlugin({
        //   inject: true,
        //   template: paths.appHtml,
        //   minify: {
        //     removeComments: true,
        //     collapseWhitespace: true,
        //     removeRedundantAttributes: true,
        //     useShortDoctype: true,
        //     removeEmptyAttributes: true,
        //     removeStyleLinkTypeAttributes: true,
        //     keepClosingSlash: true,
        //     minifyJS: true,
        //     minifyCSS: true,
        //     minifyURLs: true,
        //   },
        // }),
        // 替換 HtmlWebpackPlugin 插件配置
        ...htmlPlugin,
        shouldInlineRuntimeChunk &&
          new InlineChunkHtmlPlugin(HtmlWebpackPlugin, [/runtime~.+[.]js/]),
        new InterpolateHtmlPlugin(HtmlWebpackPlugin, env.raw),
        new ModuleNotFoundPlugin(paths.appPath),
        new webpack.DefinePlugin(env.stringified),
        // 更改輸出的樣式文件名
        new MiniCssExtractPlugin({
          // filename: 'static/css/[name].[contenthash:8].css',
          // chunkFilename: 'static/css/[name].[contenthash:8].chunk.css',
          filename: 'static/css/[name].css?_v=[contenthash:8]',
          chunkFilename: 'static/css/[name].chunk.css?_v=[contenthash:8]',
        }),
        new ManifestPlugin({
          fileName: 'asset-manifest.json',
          publicPath: publicPath,
        }),
        new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
        new WorkboxWebpackPlugin.GenerateSW({
          clientsClaim: true,
          exclude: [/\.map$/, /asset-manifest\.json$/],
          importWorkboxFrom: 'cdn',
          navigateFallback: publicUrl + '/index.html',
          navigateFallbackBlacklist: [
            new RegExp('^/_'),
            new RegExp('/[^/]+\\.[^/]+$'),
          ],
          // 更改輸出的文件名
          precacheManifestFilename: 'precache-manifest.js?_v=[manifestHash]',
        }),
        // 輔助分析打包內容
        // new BundleAnalyzerPlugin(),
        fs.existsSync(paths.appTsConfig) &&
          new ForkTsCheckerWebpackPlugin({
            typescript: resolve.sync('typescript', {
              basedir: paths.appNodeModules,
            }),
            async: false,
            checkSyntacticErrors: true,
            tsconfig: paths.appTsConfig,
            compilerOptions: {
              module: 'esnext',
              moduleResolution: 'node',
              resolveJsonModule: true,
              isolatedModules: true,
              noEmit: true,
              jsx: 'preserve',
            },
            reportFiles: [
              '**',
              '!**/*.json',
              '!**/__tests__/**',
              '!**/?(*.)(spec|test).*',
              '!src/setupProxy.js',
              '!src/setupTests.*',
            ],
            watch: paths.appSrc,
            silent: true,
            formatter: typescriptFormatter,
          }),
      ].filter(Boolean),
      node: {
        dgram: 'empty',
        fs: 'empty',
        net: 'empty',
        tls: 'empty',
        child_process: 'empty',
      },
      performance: false,
    };
相關文章
相關標籤/搜索