lrh21g

lrh21g 查看完整档案

长沙编辑湖南工学院  |  信息与计算科学 编辑湖南三湘银行  |  web前端工程师 编辑填写个人主网站
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 个人简介什么都没有

个人动态

lrh21g 收藏了文章 · 2020-02-16

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

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

一、create-react-app 多页面配置

为什么要进行多页面配置

在使用 React 进行开发的过程中,我们通常会使用 create-react-app 脚手架命令来搭建项目,避免要自己配置 webpack,提高我们的开发效率。但是使用 create-react-app 搭建的项目是单页面应用,如果我们是做中后台管理页面或 SPA,这样是满足要求的,但如果项目有多入口的需求,就需要我们进行一些配置方面的修改。

一般有以下两种方式将脚手架搭建的项目改造为多页面入口编译:

  1. 执行 npm run eject 命令,暴露配置文件,进行自定义配置。
  2. 使用 react-app-rewired 修改脚手架配置,在项目中安装 react-app-rewired 后,可以通过创建一个 config-overrides.js 文件来对 webpack 配置进行扩展。请参见:React-CRA 多页面配置(react-app-rewired)

本文对第 1 种方法给出具体配置方案。

webpack 基础

本文对 React 多页面应用配置的探讨是基于使用 create-react-app 脚手架命令构建的项目,并不是 webpack 多页面配置教程。但上述两种方案都应该具有一定的 webpack 的基础,实际上当你决定要改造或增强项目的构建打包配置的时候,你应该先对 webpack 进行一定的了解。

本项目使用的是 webpack v4.19.1 版本,对于 webpack v3.x 版本并不完全适用,主要区别在于 optimize.splitChunksmini-css-extract-plugin 的使用,这里附上 webpack v4.0.0 更新日志 以及 webpack4升级完全指南

方案说明

通过执行 npm run eject 命令暴露脚手架项目的配置文件,然后根据项目需要进行自定义配置,从而实现扩展 webpack 配置,增加对 less 文件的支持、增加 antd 组件的按需加载、处理 html 文档中的图片路径问题等,甚至可以将单页面入口编译修改为多页面入口编译的方式。

当然,我们也可以通过使用 react-app-rewired 在不暴露配置文件的情况下达到扩展项目配置的目的,原则上通过执行 npm run eject 命令可以实现的配置改造,通过使用 react-app-rewired 也同样可以实现,包括多页面配置的改造。我们在上文中已经给出了使用 react-app-rewired 进行多页面配置的方案:React-CRA 多页面配置(react-app-rewired)

但是,我们并不建议使用 react-app-rewired 这种方式进行多页面入口编译的配置,如果只是对脚手架项目进行配置扩展,比如增加 less 文件的支持或增加 antd 组件的按需加载,这种不暴露配置文件的方式是比较合适的,但如果是需要进行多页面配置,最好还是使用 npm run eject 暴露配置文件的方式。

主要原因是,通过 react-app-rewired 来实现多页面配置,是需要对脚手架原来的配置具有一定了解的,相较于 npm run eject 暴露配置文件的方式来说,这种方式是不太具有透明度的,后面维护的难度较大。实际上,React-CRA 多页面配置(react-app-rewired)方案本身就是基于 npm run eject 这种方案来完成的。

本文方案主要实现了两个方面的功能,一是将项目改造成多页面入口编译,二是在脚手架项目原有基础上扩展常用配置,我们的测试方案包含 index.htmladmin.html 两个页面(测试多页面打包),是一个使用了 Ant Design、Redux、Less、echarts-for-react(数据可视化)、react-intl(多语言)、react-lazyload(延迟加载)等 npm 包的多页面项目,也对这些主要功能实现过程中出现的错误在 五、错误排查 章节中进行了罗列,比如 Redux 版本问题、proxy 本地代理设置方法等。

版本的变动

使用 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 以及解决方法或修复措施,本文中也列出了遇到的一个这种类型的错误,详见 五、错误排查 章节中的内容。

二、准备工作

关于 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

三、具体方案

  1. 创建一个 CRA v2.1.1 版本脚手架项目
    请参照 二、准备工作 中的 如何创建 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,
    },

四、扩展配置

以上的操作已经完成了多页面入口编译的配置,但在实际项目中我们还需要扩展一些功能,比如增加对 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

五、错误排查

这里对配置过程中可能会出现的错误或异常进行记录,这里主要有两类错误,一类是多页面入口改造中可以预期的常规错误,另一类是由于基础工具版本的变动造成的不确定错误。

常规错误

  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.jsmy-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,
      },
    },

六、源码附录

以下附录 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,
    };
查看原文

lrh21g 关注了用户 · 2019-12-11

林小志 @linxz

关注 180

lrh21g 关注了专栏 · 2019-12-06

边城客栈

全栈技术专栏

关注 3168

lrh21g 关注了用户 · 2019-12-06

边城 @jamesfancy

从事软件开发 20 年,在软件分析、设计、架构、开发及软件开发技术研究和培训等方面有着非常丰富的经验,近年主要在研究 Web 前端技术、基于 .NET 的后端开发技术和相关软件架构。

关注 11020

lrh21g 收藏了文章 · 2019-11-06

koa2 使用passport权限认证中间件

做后端系统避免不了要做权限认证,比如本地用户登录,第三方登录。
权限认证的思路也极其简单,不外乎就是登录,登出,路由守护三部分。

那么有没有现成的轮子可用呢?答案是肯定的,node发展了这么迅速,各种npm包层出不穷,总有那么几款厉害的。
今天要讲的权限认证中间件那就是:passport

passport目前有很多已经写好的登录策略,比如github登录,微信登录,Facebook登录,google等等。

官网 http://passportjs.org/docs/

官网是英文的,英文差的话不建议看了,去找个demo撸起来才是正确的学习思路。

通过一阵摸索,本文决定记录下koa2具体的使用步骤。

安装包

koa2中使用的是 koa-passport 这个包。
本地验证用的是 passport-local这个策略

npm install -S koa-passport

代码

先来看代码,稍后再做解释。
这里使用 passport-local 策略(本地权限认证)为例子。
因为passport使用之前要定义策略及序列化与反序列化操作,所以把 passport 的配置及策略写到一个文件passport.js

定义策略

// passport.js
const passport = require('koa-passport')
var LocalStrategy = require('passport-local').Strategy


// 序列化ctx.login()触发
passport.serializeUser(function(user, done) {
  console.log('serializeUser: ', user)
  done(null, user.id)
})
// 反序列化(请求时,session中存在"passport":{"user":"1"}触发)
passport.deserializeUser(async function(id, done) {
  console.log('deserializeUser: ', id)
  var user = {id: 1, username: 'admin', password: '123456'}
  done(null, user)
})
// 提交数据(策略)
passport.use(new LocalStrategy({
  // usernameField: 'email',
  // passwordField: 'passwd'
}, function(username, password, done) {
  console.log('LocalStrategy', username, password)
  var user = {id: 1, username: username, password: password}
  done(null, user, {msg: 'this is a test'})
  // done(err, user, info)
}))


module.exports = passport

记得文件末 module.exports = passport 导出 passport

入口载入

然后在 koa 入口 app.js 中载入 passport.js 文件

const passport = require('./passport')

并在适当位置(看下边 app.js)使用passport中间件

app.use(passport.initialize())
app.use(passport.session())

passport 中间件需要用到 session ()所以,你的app.js入口文件类似这样

// app.js
const Koa = require('koa')
const bodyParser = require('koa-bodyparser')
const static = require('koa-static')
const session = require('koa-session')
const RedisStore = require('koa-redis')
const app = new Koa()


const passport = require('./libs/passport')
const baseConf = require('./config/base')
const redisConf = require('./config/redis')

// 基础中间件
app.use(async (ctx, next) => {
  const start = Date.now()
  await next()
  const ms = Date.now() - start
  console.log(`${ctx.method} ${ctx.status} ${ctx.url} - ${ms} ms`)
})


app.keys = ['123456']
app.use(bodyParser())
app.use(session({
  cookie: {secure: false, maxAge:86400000},
  store: RedisStore(redisConf.session)
}, app))


app.use(passport.initialize())
app.use(passport.session())

var router = require('./routes')

router.all('404', '*', ctx => {
  ctx.status = 404
  ctx.body = '404'
})

app.use(router.routes())
app.use(router.allowedMethods())

var log = require('./libs/log')
app.on('error', (err, ctx) => {
  log.error(`${ctx.method} ${ctx.url}`, 'Error: ')
  log.error(err)
  console.log(err)
})

app.listen(baseConf.port)
console.log('listening on ' + baseConf.port)
module.exports = app

编写路由

编写路由及守护中间件。

  • 登录
POST /login
router.post('/login', ctx => {
  // 会调用策略
  return passport.authenticate('local',
    function(err, user, info, status) {
      ctx.body = {user, err, info, status}
      return ctx.login({id: 1, username: 'admin', password: '123456'})
    })(ctx)
})
  • 登出
GET /logout
router.get('/logout', ctx => {
  ctx.logout()
  ctx.body = {auth: ctx.isAuthenticated(), user: ctx.state.user}
})
  • 路由守护中间件

比如你/api/*的路由需要用户认证才能访问

router.use('/api/*', (ctx, next) => {
   if(ctx.isAuthenticated()) {
     next()
   } else {
    ctx.status = 401
    ctx.body = {
      msg: 'auth fail'
    }
  }
})

到这里,本地权限认证基本完成了,post请求 /login 并且提交表单username,和 password即可登录一个用户。

/logout 退出当前登录。

解释

使用 passport 这个中间件,必须了解其运行步骤和知识点。

passport 以策略来扩展验证,什么是策略呢?

比如:本地策略,github登录策略,微信登录策略

passport 中间件使用前,需要注册策略,及实习序列化与反序列化操作。

序列化

通过 passport.serializeUser 函数定义序列化操作。

// 序列化
passport.serializeUser(function(user, done) {
  done(null, user.id)
})

在调用 ctx.login() 时会触发序列化操作。

反序列化

通过 passport.deserializeUser 函数定义反序列化操作。

// 反序列化
passport.deserializeUser(async function(id, done) {
  console.log('deserializeUser: ', id)
  var user = {id: 1, username: 'admin', password: '123456'}
  done(null, user)
})

在请求时,session中如果存在 "passport":{"user":"xxx"}时会触发定义的反序列化操作。

注册策略

// 策略
passport.use(new LocalStrategy({
  // usernameField: 'email',
  // passwordField: 'passwd'
}, function(username, password, done) {
  var user = {id: 1, username: username, password: password}
  done(null, user)
}))

在使用 passport.authenticate('策略', ...) 的时候,会执行策略

其他

app.use(passport.initialize()) 会在请求周期ctx对象挂载以下方法与属性

  • ctx.state.user 认证用户
  • ctx.login(user) 登录用户(序列化用户)
  • ctx.isAuthenticated() 判断是否认证

github

另外附上github的认证代码

安装包

npm install -S passport-github

passport.js载入

var GitHubStrategy = require('passport-github').Strategy

passport.js 增加代码

passport.use(new GitHubStrategy({
    clientID: githubConf.clientId,
    clientSecret: githubConf.secret,
    callbackURL: githubConf.callback
  },
  function(accessToken, refreshToken, profile, done) {
    // console.log(accessToken, refreshToken, profile)
    return done(null, {accessToken, refreshToken, profile})
  }
))

添加两个路由

// 调用授权页面
router.get('/auth/github', ctx => {
  return passport.authenticate('github', {scope: ['user:email']})(ctx)
})
// 授权回调得到code
router.get('/auth/github/callback', async ctx => {
  return passport.authenticate('github', (err, user, info, status) => {
    ctx.body = {err, user, info, status}
    return ctx.login(user)
  })(ctx)
})

以上例子只是模拟,并没有涉及数据库的操作,具体的实现还需要自己按照业务需求实现。

passport使用session来维护会话。对于token验证的来说,并不能用,所以要实现token验证的话还需要另外编写策略才行。

更多详细用法,请自行到官网查看文档。

查看原文

lrh21g 收藏了文章 · 2019-10-31

css文本换行总结

到文本过长的问题,元素的宽度不足以容那文本的内容,文本超出元素显示,遇到这种情况,一般有两种处理方式,一种是超出省略不显示,很暴力,很直接,还有一种就是换行显示,下面我将会介绍几种css种常用的换行方法。

word-break

相信这个属性大家都不陌生,在介绍这个属性之前先介绍一个缩写CJK:中日韩统一表意文字,在下面的介绍中将会用到这个名词,让我们先来简单介绍一下word-break的几个属性:

  • normal:使用默认断行规则
  • break-all:对于非CJK文本,可在任意字符间断行
  • keep-all:CJK文本不断行,非CJK文本表现同normal一样

下面我们来看几个例子:

<div>abashdgsgdjagdjasddnsadvadfjasdgagdagsjdgasgdajsghsa</div>
<div>测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试</div>
<div>test test Strawberry test test test test test test test test</div>
div {
  width: 100px;
  word-break: normal;
}

clipboard.png

当word-break为normal时,即使用默认规则时可以看到CJK文本会自动行,非CJK文本不会自动换行,会超出容器,但是在查找到空格这类可以作为换行依据的内容时,非CJK内容也能实现换行

div {
  width: 100px;
  word-break: break-all;
}

clipboard.png

从上面可以看到:break-all属性很霸道,只要容器装不下了,就会立即换行,不管是CJK内容还是非CJK内容,也可以理解为break-all将非CJK内容作为CJK处理

div {
  width: 100px;
  word-break: keep-all;
}

clipboard.png

从上面可以看到:keep-all属性恰好和break-all相反,将CJK内容当成非CJK内容处理

overflow-wrap

有可能有人看到这个属性有点陌生,但是我一说word-wrap大家一定不陌生,其实它就是word-wrap换了个名字而已,属性值一摸一样,下面就来介绍一下它的属性值:

  • normal:表示在正常的单词结束处换行
  • break-word:表示如果行内没有多余的地方容纳该单词到结尾,则那些正常的不能被分割的单词会被强制分割换行

让我们来看个例子:

div {
  width: 100px;
  overflow-wrap: break-word;
}

clipboard.png

从上面例子可以看到:当属性值为break-word时,当到达容器边界时,如果单词还没有结束,也会自动换行,如果找到空格明确单词结束时,就会判断当前行剩余宽度是否还能容纳下一个单词,如果不行就会执行换行,以避免单词溢出

white-space

这个属性在文本超出显示省略号的时候大家一定用过,下面就来看一下这个属性的属性值:

  • normal:连续的空白符会被合并,换行符会被当作空白符处理。填充line盒子时,必要的话会换行。
  • nowrap:和normal一样,连续的空白符会被合并。但文本内的换行无效。
  • pre:连续的空白符会被保留。在遇到换行符或者<br>元素时才会换行。
  • pre-wrap:连续的空白符会被保留。在遇到换行符或者<br>元素,或者需要为了填充line盒子时才会换行。
  • pre-line:连续的空白符会被合并。在遇到换行符或者<br>元素,或者需要为了填充line盒子时会换行。

来看几个具体的例子表现:

<div>  test test test test
test test test test test test test</div>
div {
  width: 100px;
  white-space: normal;
}

clipboard.png

上面例子中,为了看出是否会合并空白符,特意在div前面加了两个空格。可以看到值为normal时,会合并连续空白符,并且会换行符作为空白符处理

div {
  width: 100px;
  white-space: nowrap;
}

clipboard.png

nowrap属性就和大家常见的一样文本内换行无效,但是会合并连续空白符

div {
  width: 100px;
  white-space: pre;
}

clipboard.png

pre值可以看到,会保留连续空白符,且只有在遇到换行符的时候才会换行

div {
  width: 100px;
  white-space: pre-wrap;
}

clipboard.png

pre-wrap值可以看到,会保留连续空白符,且在遇到换行符和需要换行的时候都会换行

div {
  width: 100px;
  white-space: pre-line;
}

clipboard.png

pre-line值可以看到,会合并连续空白符,且在遇到换行符和需要换行的时候都会换行

line-break

此属性用于指定如何断行
属性值:auto | loose | normal | strict | anywhere
目前还是一个处在Working Draft的阶段,w3c地址
在chrome中测试发现这几个属性值并未想官方阐述的那样对文本产生换行的影响,需要持续关注未来的进展

hyphens

此属性告诉浏览器在换行时使用连字符连接单词
属性值:none | manual | auto
来看具体例子:

<div>abashdgsgdjagdjasddnsadvadfjasdgagdagsjdgasgdajsghsa</div>
<div>test test Strawberry test test test test test test test test</div>
div {
  width: 100px;
  hyphens: none;
}

clipboard.png

当属性值为none时,只会在有空白符的地方换行

<div>abashdgsgdjagdjasddnsad-vadfjasdgagdagsjdgasgdajsghsa</div>
<div>test test Strawberry test test test test test test test test</div>
div {
  width: 100px;
  hyphens: manual;
}

clipboard.png

从例子中可以看出,当属性值为manual的时候,只有在空白符和连接符的时候会换行

<div>abashdgsgdjagdjasddnsadvadfjasdgagdagsjdgasgdajsghsa</div>
<div>test test Strawberry test test test test test test test test</div>
div {
  width: 100px;
  hyphens: auto;
}

clipboard.png

当值为auto时,浏览器会自己去判断可以在哪个位置断行,然后自动在断行的位置加上连接符
目前hyphens属性也处在Working Draft的阶段,w3c地址,需要大家后续的持续关注

总结

以上对css中的文本换行会用的一些属性做了简单的总结,要想处理好文本换行的问题,还需要花更多的精力去配合其他属性的使用,需要更多的时间去探索,这也是css有趣的地方,不同的组合有不同的可能。
这篇文章如果有错误或不严谨的地方,欢迎批评指正,如果喜欢,欢迎点赞收藏

查看原文

lrh21g 关注了专栏 · 2019-10-25

每天一点canvas动画

编码需要乐趣

关注 1015

lrh21g 收藏了文章 · 2019-08-29

JavaScript深入系列15篇正式完结!

写在前面

JavaScript 深入系列共计 15 篇已经正式完结,这是一个旨在帮助大家,其实也是帮助自己捋顺 JavaScript 底层知识的系列。重点讲解了如原型、作用域、执行上下文、变量对象、this、闭包、按值传递、call、apply、bind、new、继承等 JS 语言中的比较难懂的概念。

JavaScript 深入系列自 4 月 6 日发布第一篇文章,到 5 月 12 日发布最后一篇,感谢各位朋友的收藏、点赞,鼓励、指正。

顺便宣传一下该博客的 Github 仓库:https://github.com/mqyqingfeng/Blog,欢迎 star,鼓励一下作者。

而此篇,作为深入系列的总结篇,除了汇总各篇文章,作为目录篇之外,还希望跟大家聊聊,我为什么要写这个系列?

我为什么要写深入系列?

讲一个对我学技术的态度很有影响的一件事情。

曾经团队邀请过 Nodejs 领域一个非常著名的大神来分享,这里便不说是谁了。当知道是他后,简直是粉丝的心情。但是课讲得确实一般,也许是第一次讲,准备不是很充足吧,以至于我都觉得我能讲得比他好,但是有两次,让我觉得这是真正的大神。一次就是,当有同事问到今年有什么流行的前端框架吗?这些框架有怎样的适用场景?该如何抉择?我以为大神一定会回答当时正火的 React、以及小鲜肉 Vue 之类,然后老生常谈的比较一番,但是他回答道:“I dont't care!因为这些并不重要,真正重要的是底层,当你了解了底层,你就能很轻松的明白这些框架的原理,当你明白了原理,这些框架又有什么意思呢?”

虽然这段话因为过去太久,已经不记得确切的表述,但是给了我非常深刻的印象,自己一路学习过来,新的东西不停的冒出,但是学的再多感觉自己也只是学了一堆 API,如果仅仅是为了解决工作上的问题,或许已经足够,但是内心经常还会冒出一种不安定感,这种不安定感或许来自于对 JavaScript 未知部分的恐惧,或许来自于解决问题却不明所以的尴尬,或许来自于屡次学习语言难点却不得门道的失败……代码写的越久,这种感觉就越是鲜明。

当然了,大家也不要过分解读底层,各种计算机语言追究到底层都是编译原理之类,如果是有这方面的兴趣,固然可以,但是如果本质上还是为了解决上层问题,倒不必一定要深究到这个层面。用 JavaScript 了解这门语言本身的使用和原理,用 jQuery 看看 jQuery 的源码实现,用 React 技术栈,写写 React、Redux 简单的模拟实现,诸如此类,都是对底层的一种追求。

这样讲的话,底层这个词,更像是一个方向,一种学习的态度吧。

为了更加深入的了解 JavaScript 这门语言,我将之前记录的一些要学习的关键词作为课题进行研究,后来研究的差不多了,才决定动笔写下这个系列。尽管这个系列很多地方上依然不够所谓的“深入”,但就跟学习这些内容之前的我相比,已然多了份安定感,解决一些问题时也多了份得心应手,也希望大家能从这个系列中有所收获。

然而即便已经写了 15 篇,也只是漫长路途的开始,在我 Github 博客仓库的描述中有写到,我预计写 4 个系列,JavaScript 深入系列,JavaScript 专题系列,ES6 系列,React 系列,其实从“深入系列”到“专题系列”再到“ React 系列”,就是原来写着上层的我决定从语言层面开始一步一步走回上层的记录,而现在,我也只是迈出了第一步。

重新修订

在发布完最后一篇后,我花了一周时间,根据大家的评论和留言,并且参照阮一峰老师的《中文技术文档的写作规范》对所有的文章进行了一次修订。

说起来,改的最多的就是给英文单词两边加个空格……

此外,大家有疑问或指正或鼓励或感谢,尽管留言回复哈 []~( ̄▽ ̄)~* 。

全目录

  1. JavaScirpt深入之从原型到原型链

  2. JavaScript深入之词法作用域和动态作用域

  3. JavaScript深入之执行上下文栈

  4. JavaScript深入之变量对象

  5. JavaScript深入之作用域链

  6. JavaScript深入之从ECMAScript规范解读this

  7. JavaScript深入之执行上下文

  8. JavaScript深入之闭包

  9. JavaScript深入之参数按值传递

  10. JavaScript深入之call和apply的模拟实现

  11. JavaScript深入之bind的模拟实现

  12. JavaScript深入之new的模拟实现

  13. JavaScript深入之类数组对象与arguments

  14. JavaScript深入之创建对象的多种方式以及优缺点

  15. JavaScript深入之继承的多种方式以及优缺点

作者推荐

在我研究一些课题的时候,有时感觉自己深受启发,颇有醍醐灌顶的感觉,我也希望这个系列的读者能感受到跟作者当初学习这些内容时的一样兴奋的感觉,所以强烈推荐以下三篇:

  1. JavaScript深入之从ECMAScript规范解读this

  2. JavaScript深入之call和apply的模拟实现

  3. JavaScript深入之new的模拟实现

真的完结?

JavaScript 底层知识哪有这么一点呐!在不断学习的过程中,还会冒出一些新的课题适合划分到深入系列,如果是这样的话,就会偶尔发布一篇,当然了,如果冒出太多的话,不保证再来一个深入系列第二季,哈哈。

下期预告

一周之内,会发布新的系列即 JavaScript 专题系列,这个系列主要研究日常开发中一些功能点的实现,比如防抖、节流、去重、拷贝、最值、扁平、柯里、递归、乱序、排序等,特点是抄袭 underscore 和 jQuery 的实现方式,而这次预计写二十篇左右。

感谢大家的阅读和支持,我是冴羽,JavaScript 专题系列再见啦![]~( ̄▽ ̄)~**

查看原文

认证与成就

  • 获得 4 次点赞
  • 获得 4 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 4 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2016-06-16
个人主页被 314 人浏览