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,
    };
相关文章
相关标签/搜索