简介

0. 前言

create-react-app是用来创建React项目的脚手架工具,使用它可以迅速创建出一个React项目。它还支持我们自定义项目构建、本地服务和单测部分,并且支持自定义项目模板。另外它也支持我们创建项目完成后,修改项目的构建的实现。

create-react-app使用了React作为开发框架,使用webpack及一些插件、loader作为构建支持,使用jest提供单测的能力。

本文简单介绍create-react-app,包括

  1. create-react-app的使用
  2. cra项目结构
  3. cra执行流程
  4. 并介绍如何基于create-react-app做扩展,实现满足自己项目需求的脚手架或项目的初始架子
  5. 开发过程中的一些实用细节
  6. 新版本的变化

本文主要针对3.3.0及以上版本进行说明。

1. create-react-app的使用

首先安装create-react-app到全局,然后就可以使用它创建项目脚手架了。如果不全局安装,也可以使用npx命令来执行。

注意老版本的create-react-app和新版本的react-scripts不兼容,因此如果本地安装过create-react-app,可以卸载重装最新版本或者用npx加--ignore-existing参数来使用。如果卸载不了,mac下可以用which creact-react-app命令找到其位置,rm -rf <path-to-cra>删除。

最初级的用法就是,使用create-react-app这个命令创建一个支持React的项目,同时这个项目提供start命令来进行本地调试、提供build命令进行构建、提供test命令支持单测的功能。

如果项目初始化好了之后,需要修改构建相关的功能(比如需要修改webpack配置以支持某些css预编译语言),可以通过eject命令来将构建和本地开发相关的代码“弹出”,这样就可以修改相关代码来满足自己项目的相关需求了。

如果需要定制自己的命令工具,比如希望定制自己的build的功能、定制start本地开发调试的功能和配置、定制test单测的功能,可以通过自己实现react-scripts,然后在使用create-react-app命令创建项目时候添加 —scripts-version 来实现。定制自己的react-scripts时候,最好基于cra提供的react-scripts添加一些自己需要的支持,而不要自己单独实现一套,因为cra提供的react-scripts中包含了很多通用的支持和最佳实践,比如打包策略和开发环境的一些常用配置的最佳实践。

如果需要定制自己的项目模板,比如希望将React生态,包括redux、react-router整合到项目里,并且封装一些开发常用的工具,将这些代码作为一个更定制化也更完善的项目模板,可以通过自己实现一个template,然后在使用create-react-app命令创建项目时候添加 —template 来实现。另外,也可以在自己实现react-scripts中的init命令中,指定模板下载并拷贝到我们的项目目录中。

我们接下来分析一下create-react-app的源码

2. create-react-app的项目架构

严格地说,create-react-app是一个脚手架生成工具,它用可以来创建一个脚手架。

create-react-app项目可以分成3层

  1. 创建脚手架层
  2. 脚手架命令行层
  3. 项目开发模板层

cra的设计为什么要分成3层呢?如果让我们自己设计一个简单的脚手架,就是先写一个项目模板,然后写一个命令行工具,执行命令后从远程将模板下载到本地。可以看出来,我们的这个设计扩展性很差,一个命令行只能下载对应的模板,而模板里已经把构建、本地开发等支持写死了,很难基于这样的架构做一些扩展。

cra这种架构设计可以提供非常大的扩展能力。

cra的架构设计基于以下考量

  1. 一个React脚手架的基本能力,就是安装React相关的依赖,包括react,react-dom。这也是所有React脚手架的通用能力。
  2. 不同的React脚手架提供的构建、本地开发等命令行支持可以根据项目需求自己定制。
  3. 对于React脚手架,相同的构建、本地开发支持也可能对应不同的开发模板,即工具库、组件库、ajax、路由、状态管理等这些开发中需要的支持。

是以,cra分成3个层,第一层是create-react-app这个命令,它只提供基本的react,react-dom的依赖安装,并且使用可自定义的react-scripts来执行项目初始化工作,我们可以通过实现自己的react-scripts来定制自己的构建、本地开发、单测的命令行支持,这是第二层。第三层是项目的开发模板,可以通过在create-react-app命令的 —template参数中指定模板,这样一个命令行支持可能对应不同的开发模板。

对于第二层和第三层,cra都提供了默认的支持。即react-scripts和cra-template,可以满足大部分的项目的基本需求。当然处于通用性考虑,cra-template中并没有状态管理、路由等支持,而react-scripts里面也只有sass的支持而没有stylus和less和其他css模块化方案的支持,如果项目有相关需求,就需要自己定制了。

create-react-app项目使用lerna管理,它将多个库放在一个项目中管理,不同的库可以单独发布和进行git管理。核心代码都在/packages目录下

create-react-app的项目中,关键的目录有3个

  • create-react-app
  • react-scripts
  • cra-template

其中create-react-app是创建脚手架的命令行实现;react-scripts是cra提供的默认的项目命令行支持,cra-template是cra提供的默认的项目开发模板。

下面看下cra的执行过程。

3. create-react-app的执行流程

create-react-app命令中有几个关键的函数

  • createApp(参数校验和处理)
  • run(执行主流程)
  • getInstallPackage(获取命令行支持(react-scripts)包名)
  • getTemplateInstallPackage(获取自定义模板包信息包名)
  • getPackageInfo(根据包名获取包信息)
  • install(安装依赖)
  • executeNodeScript(执行node命令)

create-react-app的执行过程可以描述如下

  1. 执行createApp函数,进行参数校验和处理,然后调用run函数执行主流程,下面的流程都是在run函数中执行的。
  2. run函数首先调用getInstallPackage和getTemplateInstallPackage,根据传参得到处理过的命令行支持包的包名和模板包名。
  3. 然后调用getPackageInfo根据包名得到包的信息。
  4. 然后调用install安装必要的依赖(包括react,react-dom和命令行支持包、模板包等)到我们指定的项目目录。
  5. 安装依赖完成后,执行react-scripts包中的init.js文件,进行项目初始化。
  6. react-scripts命令执行init.js方法,这个方法用来进行项目的内容的初始化,包括package.json、gitignore、README.md等文件的初始化,还会将模板从安装的目录中拷贝到我们指定的项目目录中,然后根据模板中package.json中的dependencies字段安装所需依赖,最后卸载掉这个模板。

由于init.js将模板拷贝到项目中后还进行了依赖安装,因此初始化项目完成后可以直接运行项目了。

以上就是我们执行create-react-app命令后的整个过程。

4. 如何基于create-react-app做扩展

主要有3中方式,可以根据项目需求选择方式定制自己的项目脚手架。

  1. 我们可以定制自己的react-scripts,然后执行create-react-app命令时候指定我们自己的react-scripts版本,create-react-app --scripts-version @custom/react-scripts,这样create-react-app会根据传入的 --scripts-version参数下载相应的包,然后执行包中的/scripts/init.js文件,并把相应的参数传进去,也就说,需要我们实现一个init.js,它导出一个方法,执行整个初始化流程。
  2. 我们可以定制自己的template,然后执行create-react-app命令时候指定我们自己的template,create-react-app --template my-template
  3. 我们使用create-react-app命令初始化好项目后,执行npm run eject命令将构建和本地开发等命令行支持及配置“弹出”到项目里面,我们在项目里修改这些配置文件和脚本,即可满足我们自己的相关需求。

5. 开发过程中的一些实用细节

  1. 环境变量
    react-scripts支持我们配置一些环境变量来控制构建和本地调试过程。我们可以在项目根目录创建.env.env.development文件,在其中加入一些环境变量配置,比如PUBLIC_URL用来控制webpack的发布地址、SKIP_PREFLIGHT_CHECK用来控制build之前是否需要进行依赖检查。
    实现的原理是,react-scripts使用dotenv这个库解析了根目录下的.env.env.development文件(如果存在),解析成key-value形式挂载到process.env上面,然后根据这些变量进行构建过程的处理(比如根据PUBLIC_URL设置webpack的output.publicPath值)。
    react-scripts解析环境变量后,不仅用于构建过程处理,还会将所有的env变量用webpack.DefinePlugin插件导入到项目运行时代码中,js通过process.env.PUBLIC_URL来访问,html中通过%PUBLIC_URL%来访问。所以我们可以自定义一些环境变量,用于实现某些业务逻辑。
  2. react-scripts中的babelrc-loader设置的.babelrc为false,因此项目中配置的.babelrc不会生效。
  3. 我们如果自定义模板,可以考虑在模板中添加.editorconfig配置文件,以实现多人开发时候的代码格式统一,然后在编辑器中安装editorconfig插件,编辑器就可以根据配置做一些操作(比如自动在尾部加空行、自动trim尾部空格、tab)。
  4. react-scripts中的build.js在构建中,会将public目录下的文件拷贝到输出目录,因此线上都是通过根目录访问的。这些文件的访问要注意加参数去缓存。
  5. react-scripts支持模块热重载(在webpack.config.js中的development环境中开启了HotModuleReplacementPlugin插件,并且devserver中配置hot为true)。对于stylus等样式预编译和style-loader,实现了HMR接口,因此改变stylus文件并保存后,可以不刷新浏览器就能看到结果,注意不能使用mini-css-extract-plugin。而对于React也实现了HMR接口,在导出组件时候,用相应的高阶组件hot包裹即可实现热重载功能。
  1. import {hot} from "react-hot-loader/root";
  2. export default hot(() => <div id="app">hello, world</div>);

init.js

init.js做了些什么

1. 计算package.json并写入package.json文件

脚本根据 1. 模板项目目录下的template.json文件中的package字段和 2. 初始化的项目(myapp)中的package.json文件,将两者按照一定规则合并,得到最中的myapp中的package.json文件内容

  • dependencies
  • scripts
  • eslintConfig
  • browserslist

2. 写README.md

3. 拷贝项目模板文件

将模板项目中的template目录拷贝到myapp目录中

4. 写.gitignore文件

myapp目录中的gitignore文件和.gitignore文件合并

5. 初始化git,git init

6. 安装依赖

7. 移除安装的模板

8. git commit

如何修改init.js加载自定义模板

如果我们希望定制自己的项目模板,可以通过以下步骤来实现

  1. 首先,在我们的react-scripts目录中增加自己的项目模板目录my-template
  2. 其中创建目录和文件template/package.jsontemplate.jsonREADME.md
  3. template/目录中创建文件gitignore并在目录中实现自己定制模板的目录结构和必要依赖和一些模板文件,如redux、router等等
  4. 修改init.js文件,让template读取地址指向我们自己的react-scripts下的my-template目录。

build.js

build.js的操作

  1. 先检查包依赖树,检查版本匹配情况
  2. 检查browserList是否正常设置
  3. 计算构建前资源的大小
  4. 清空构建目录,准备开始构建
  5. 拷贝public目录到输出目录,这样public中的静态资源可以通过输出目录部署的url地址访问
  6. 开始构建,使用webpack的node cli,加载webpack.config.js模块,传入env = 'production'生成构建用的webpack配置
  7. 构建完后,打印相关信息

start.js

start.js的操作

  1. 引入环境变量
  2. 检查依赖树
  3. 检查必需的文件(项目目录下的public/index.htmlsrc/index.js
  4. 检查package.json文件中的browserList字段是否有效
  5. 选择一个端口,如果默认的端口号被占用,则递增端口号,直到找到一个未被占用的端口
  6. 调用webpack,根据配置文件(webpackDevServer.config.js)生成compiler
  7. 计算serverConfig
  8. 根据compiler和serverConfig,调用webpack-dev-server创建dev-server实例,并监听相应端口,启动本地服务。
  9. 清除console的log日志

webpack.config.js

webpack.config.js中的配置

cra的react-scripts中的webpack.config.js提供的不是一个webpack的配置对象,而是一个产生工厂方法,根据环境参数返回一个webpack的配置对象

cra的react-scripts中提供的webpack.config.js中的常用配置,包括

  • mode // 设置构建的模式,取值决定一些插件的使用
  • bail // 值为true的话,在第一个错误出现时候,抛出失败结果
  • devtool // 控制source-map
  • entry // 打包入口文件,这里是个数组,即把数组中的几个入口文件的依赖树打包成一个文件
  • output // 输出的一些配置
  • optimization // 优化的配置
  • resolve // 配置模块如何解析
  • resolveLoader // 配置webpack的loader的解析
  • module // 决定如何处理项目中的模块
  • plugins // webpack插件列表
  • node // 提供nodejs的polyfill配置
  • performance // 关于是否打印资源超过指定限制的配置

1. mode

mode是根据传入的环境参数判断是生产环境还是开发环境。

2. bail

3. devtool

如果是生产环境,默认不使用source map,但是如果在.env文件中配置了GENERATE_SOURCEMAP的话,就会配置成source-map

对于开发环境,用cheap-module-source-map。【eval-cheap-module-source-map的重构建速度应该更快一点】

4. entry

entry中引入了react-dev-utils/webpackHotDevClient这个模块,这个文件是wds的客户端代码,主要有几个作用,一个是处理wds文件变化,进行reload、热重载等操作,它代替了wds默认的客户端代码。还有一个作用是用来展示错误信息的。它连接了WDS的websocket,并且在重编译时候的信息,包括报错等事件触发时候,做出展示相关错误的操作。错误提示是引入了react-error-overlay模块,这个模块创建了一个无src的iframe,并操作其中的元素来渲染错误信息。

path.appIndexJs是项目目录里面的index.js文件。

paths.js文件提供了几个必要的文件的绝对路径。路径的计算的方法是,通过fs.realPath(process.cwd())计算出命令执行时候的目录(即指定的初始化app的目录)的绝对路径 appDirectory,然后通过path.resolve,根据一些文件的相对app根目录的路径拼出一些文件的绝对路径

5. output

这里也有几个参数

  1. path
    构建后资源的输出目录
    如果是生产环境,就是项目目录下的build目录,如果是生产环境,就是`undefined
  2. pathinfo
    告诉webpack在bundle中引入所包含模块信息的相关注释,default: false
    生产环境false,开发环境true
  3. filename
    这个选项控制每个bundle的命名
    生产环境用static/js/[name].[contenthash:8].js。注意,hash是构建相关的hash,chunkhash是每次构建
    关于hash、chunkhash和contenthash,有一篇文章写的比较详细
    —— webpack hash chunkhash contenthash浅析
    【cra的这项配置成contenthash,应该是考虑到可能有分包优化可能将一个入口打包成多个切片,这样只有使用contenthash才能保证输出的bundle的最佳缓存效果】

hash:在 webpack 一次构建中会产生一个 compilation 对象,该 hash 值是对 compilation 内所有的内容计算而来的。

chunkhash:每一个 chunk 都根据自身的内容计算而来。它根据不同的入口文件(Entry)进行依赖文件解析、构建对应的chunk,生成对应的哈希值。

contenthash:根据文件内容计算而来。

  1. futureEmitAssets
    不知道干啥用的
  2. chunkFilename
    非入口文件的bundle命名,比如动态import()引入的异步模块
  3. publicPath
    这个选项配置输出的静态资源发布路径,webpack会在代码运行时静态资源的引用链接前拼接此参数,以便可以正确访问到资源
    cra的react-scripts中的webpack.config.js中的这个选项是在paths文件中,通过getPublicUrlOrPath方法计算得到的
    这个参数可以在.env文件中配置,也可以在项目模板中的package.jsonhomepage字段中配置,但是development环境中只使用pathname配置publicPath
    配置会移除末尾的"/",这样访问静态资源时候不需要在末尾加"/"
    生产环境的publicPath就根据开发者指定的路径配置就行,可以是完整的url,也可以是相对路径或者绝对路径。而开发环境,cra认为最好是绝对路径,因为开发环境和生产环境不同的是,本地server serve的是打包后放在内存中的内容和contentBase指定的目录,和url中的path无关。所以开发环境的publicPath最佳实践就是使用默认值"/"
  4. devtoolModuleFilenameTemplate
    这个配置项用来控制source,map的访问路径,配置完之后,构建一下,可以在source map文件的sources数组中看到效果。
    比如项目需要把source map文件放到内网,防止代码被泄露,这时候可以通过配置该选项实现。这种需求除了需要配置这个属性外,还需要配置打包后的代码文件的末尾的source map指向sourceMappingURL,这个可以使用插件SourceMapDevToolPlugin来实现,这个插件用来更细粒度地配置devtool。
    [FE] SourceMapDevToolPlugin 和 optimization.minimizer
  5. jsonpFunction
    这个选项用来防止多个webpack运行时运行在同一个页面上的时候可能会有冲突。
    webpack运行时是会在window上挂一个变量,用来进行异步模块加载。如果每个webpack打包的运行时文件的这个全局变量名都一样的话,那么可能会导致冲突。jsonpFunction这个选项定义了webpack运行时代码的异步加载全局变量名称。
    cra的react-scripts这里取值是webpackJsonp${appPackageJson.name}和当前包名关联,这样就在很大程度上避免了冲突。
  6. globalObject
    当打包库文件为UMD规范时候,这个选项用来指定全局变量名称,默认是’window’。如果在nodejs环境而不是浏览器环境,应该配置为’this’。
    这里这个选项配置为’this’,对于打包项目没有影响,打包UMD库时候,则可以兼容浏览器环境和node环境。
    【webpack为什么默认不设置成’this’?】

6. optimization

  1. minimize
    告诉webpack压缩bundle,默认使用TerserPlugin进行压缩,也可以在minimizer选项中指定插件进行压缩。默认production环境为true
    这里配置的是生产环境为true。【是否有些多余?】
  2. minimizer
    这个选项可以指定插件来进行代码压缩,或者指定TerserPlugin的一些配置。
    这里使用了两个插件:TerserPlugin用来压缩js代码,OptimizeCSSAssetsPlugin用来压缩css代码。
    首先看一下TerserPlugin,相对于uglifyjs,TerserPlugin支持es6的压缩。这里配置了几个TerserPlugin的选项
    1. terserOptions
      这是用来配置TerserPlugin压缩的几个选项
      • parse
        配置TerserPlugin的解析规则,这里配置了ecma为8,即支持解析es2017的语法
      • compress
        配置此选项以控制压缩过程的一些处理规则
        • ecma
          用来设置转换后的结果的语法标准,比如如果项目适配的浏览器都支持es6,那可以配置该选项为2015,这样压缩后的结果更小。这里设置的是5,即最终转换为es5语法。【默认是5,是不是多余?】
        • warnings
          是否展示压缩提示。当压缩过程中遇到警告(比如不可达代码、声明代码未使用)是否展示。这里配置为false。【默认也是false】
        • comparisons
          对比较运算符的优化,默认是true,这里配置的false。原因是terser有已知的bug,会导致正常的代码解析问题,进而导致压缩失败。
        • inline
          函数内联用来降低运行时计算开销,因为每次函数调用都会耗费计算资源,所以代码压缩过程中会考虑将简单函数进行内联
          比如
          会被改造成
          terset提供的这个选项支持配置内联函数级别
          false: 和0相同
          0: 不内联
          1: 内联无参函数
          2: 内联带参函数
          3: 内联带参数和变量的函数
          true: 和3相同
          这里配置了2,默认是true
  1. function square(x) {
  2. return x * x;
  3. }
  4. function f(x) {
  5. var sum = 0;
  6. for (var i = 0; i < x; i++) {
  7. sum += square(i);
  8. }
  9. return sum;
  10. }
  1. function f(x) {
  2. var sum = 0;
  3. for (var i = 0; i < x; i++) {
  4. sum += i * i;
  5. }
  6. return sum;
  7. }
  1. * mangle

这个选项控制混淆相关的操作。它下面有几个配置项,这里只配置了safari10

  1. + safari10

这个选项用来让terser兼容safari10的一些bug,默认为false,这里配置true

  1. * keep_classnames

这个选项用来防止class names被丢弃或压缩。这里的配置是在生产环境构建加上--profile参数时候为true,以便给devTools做性能分析用。

  1. * keep_fnames

防止函数名被压缩,这个配置和keep_classnames一样

  1. * output

代码美化的一些操作

  1. + ecma: 5

output阶段不会进行语法转换,即不会将es6转换成es5。
这个选项用来控制输出的语法规范,如果配置成es6,会将{a:a}转换成{a}
【不清楚和compress中的ecma有啥区别,可能compress和output是terser处理的不同阶段,所以需要两个配置项】

  1. + comments: false

这个用来控制注释的压缩操作,false是删除所有注释。默认是”some”,保留JSDoc-style的注释(包括”@license”等)

  1. + ascii_only: true

只处理ascii的字符串和正则表达式
默认是false,会将emoji这种非ASCII字符压缩导致乱码

  1. 2. sourceMap

控制terser插件生成source map的操作
这里判断了,只要生产环境未配置不用source map就生成source map

然后看下OptimizeCSSAssetsPlugin插件配置
这个插件使用cssnanocss压缩工具,支持自定义处理器

  1. 1. cssProcessorOptions
  2. * parser: safePostCssParser

safePostCssParser 是一个postCss的容错解析器,可以发现并修复css语法

  1. * map

控制source map文件的生成

  1. 2. cssProcessorPluginOptions

不知道干啥用的

  1. splitChunks
    这里很简单地配置了一下,用了webpack的默认配置,因为webpack默认做了很多优化工作
    • chunks: ‘all’
      入口和异步模块都进行分包处理
    • name: false
      不改变切片名称
  2. runtimeChunk
    • name: ``entrypoint =>runtime-${entrypoint.name}````
      将运行时文件单独打包以充分利用缓存

7. resolve

配置webpack解析模块相关的选项

  1. modules
    这个选项控制webpack解析模块时候的fallback。这里添加’node_modules’、应用的’node_modules’目录和通过应用目录中的.jsconfig.json中的compilerOptions.baseUrl来指定。
  2. extensins
    自动解析扩展,让用户引入文件时候不带后缀
    这里添加了.js,.json,.tx(根据是否支持ts来决定是否需要),.mjs等等
  3. alias
    这个选项用来配置模块引入的路径别名。
    这里加了一个常用的路径,另外,还根据jsconfig.json文件中的compilerOptions.baseUrl选项判断,如果baseUrl和项目目录相同,就增加
    的配置
  1. {
  2. src: paths.appSrc // 应用项目目录下的src目录
  3. }
  1. plugins
    配置一些解析模块的插件
    • PnpWebpackPlugin
      使用yarn pnp功能(主要解决node_modules模块加载机制带来的node运行开销过大的问题)时候需要引入此插件
    • ModuleScopePlugin
      这个是create-react-app项目中的react-dev-utils项目提供的一个插件,用来防止模块引用src目录外的模块(因为babel只处理src中的模块)。如果应用的项目中有引入src目录外的模块的情况,就会编译报错并给出提示。

8. resolveLoader

这个配置项和resolve完全相同,只是它是用来解析loader用的

  • plugins
    配置解析插件
    这里配置了yarn Plug’n’Play相关的插件PnpWebpackPlugin.moduleLoader(module)
    【看来使用pnp配置挺麻烦的】

9. module

这个选项决定了如何处理项目中不同的模块

  1. strictExportPresence
    配置这个选项让未导出模块报错而不是只给一个警告,即引入的路径的文件必须导出一个合法模块
  2. rules
    创建模块时候,匹配请求的规则数组。这些规则可以指定对相应的模块应用哪种loader,或者更改解析器
    rule.test:这个是匹配模块的规则
    rule.enforce:这个是用来指定loader类型的。loader的类型有“前置”、“行内”、“普通”、“后置”,webpack应用loader时候就会按照这个顺序对模块进行解析。
    rule.use:指定loader
    rule.include:指定模块解析时候包含的目录,模块解析时候只解析include指定目录下的文件
    rule.options:传给loader的参数
    rule.exclude:指定模块解析时候不包含的目录
    rule.sideEffects:声明是否有副作用。什么是有副作用呢?比如如果某个模块A加载时候定义了一些全局变量挂在window下,或者定义了一些css样式,就是有副作用,即其他模块引入A时候,不直接调用A的方法也会影响到我们的项目。这个选项的作用是什么?就是用来告诉webpack是否可以放心摇树(比如sideEffects配置成false的话,就是声明没有副作用,webpack将把引入但未使用的模块抹除)。有些第三方包,可能在package.json中配置了sideEffectsfalse,这时候webpack会认为其没有副作用而放心大胆地摇树,但是可能包里面有副作用代码,导致摇树后表现不正常。尤其是css,如果第三方包中有css相关样式,但是包声明了sideEffectsfalse,由于引入这个第三方模块时候未调用其中的方法,可能这些样式就会被webpack摇掉。因此需要在loader中配置sideEffectstrue,告诉webpack不要随便摇树。
    rule.oneOf:当规则匹配时候,只用第一个匹配到的规则,如果都没有匹配到,则使用最后一个loader
    下面看下cra中react-scripts的webpack.config.js的配置
    • {parser: {requireEnsure: false}}
      配置模块解析器禁用require.ensure语法,cra的解释是,因为require.ensure不是标准语法
    • test: /\.(js|mjs|jsx|ts|tsx)$/ eslint-loader
      首先配置eslint-loader解析相应模块,进行代码检查
      这里配置了enforce为”pre”,即先进行eslint检查
      options中配置了一堆相关的配置参数,注意其中有个useEslintrc: false,忽略掉eslintrc配置文件
      这里include配置为应用项目src目录
    • oneOf
      这里配置了一系列的loader,fallback是一个file-loader
      1. url-loader
        解析 bmppngjpgjpeggif图。设置文件名为static/media/[name].[hash:8].[ext],图片都放在static/media下面
      2. babel-loader
        这个配置用来解析应用中的js(注意include中配置了只解析应用中src目录下的文件),由于babel配置比较复杂,cra的react-scripts有个包专门处理babel配置:babel-preset-react-app
      3. babel-loader
        这个配置用来处理应用之外的js(比如第三方包)解析,这个只处理标准的js,因为默认第三方包已经经过编译处理
      4. 下面有4个loader用来解析样式模块
        这几个处理样式的loader支持解析css、sass,并且支持css module
        第一个配置解析css,并且exclude了css module相关的模块
        第二个配置解析css的css module
        第三个配置解析sass,并且exclude了sass的css module相关模块
        第四个配置解析sass的css module
        loader的配置也比较负责,因此这里有个函数专门处理getStyleLoaders
        有这么几个loader:
        • style-loader
          用来开发环境将样式动态写到style标签中
        • css-loader
          解析css
        • sass-loader
          解析sass
        • postcss-loader
          后期处理,加prefix等等
        • resolve-url-loader
          这个是用来解析路径的,因为有预处理loader(这里是sass-loader)的话,如果一个.sass文件引用其他资源是用相对路径,那么其实是相对于根.sass的引用,这可能导致路径错误。这个resolve-url-loadr就是用来解决这个问题的。
          官方文档中给了具体的例子
          官方文档
        • MiniCssExtractPlugin.loader
          用来提取css
      5. file-loader
        用来加载其他未匹配到的资源

10. plugins

  1. HtmlWebpackPlugin
    用来处理html生成入口html
    注意这里使用的是4.0版本,这个版本的资源内联使用的是ejs的模板语法,之前的版本都是使用es6的字符串模板语法
  2. InlineChunkHtmlPlugin
    这个是cra的react-dev-utils包中提供的插件,将webpack运行时代码内联到html中,用来减少http请求
  3. InterpolateHtmlPlugin
    这个也是cra的react-dev-utils中提供的插件,使用该插件可以在html中使用环境变量,如“%PUBLIC_URL%”
  4. ModuleNotFoundPlugin
    这个也是cra的react-dev-utils中提供的插件,用来在引入模块失败时候给出友好提示。
  5. webpack.DefinePlugin
    这个用来定义一些项目中可用的变量,以供js使用,这里将环境变量都注入到了项目里面。
    原理是webpack打包时候会将这里定义的key都替换成相应的变量。
  6. webpack.HotModuleReplacementPlugin
    开发环境使用这个插件,支持模块热重载
  7. CaseSensitivePathsPlugin
    路径大小写敏感。mac操作系统查找文件路径时候,对大小写不敏感,这可能会导致目录和引入路径不匹配但是可以正常运行。这个插件强制要求路径大小写完全匹配,不匹配会报错。
  8. WatchMissingNodeModulesPlugin
    这个插件也是react-dev-utils中提供的,它解决了如果开发者引入一个新包时候需要重启webpack-dev-server的问题。它会自动监听npm包引入情况,用户新引入npm包后install一下就可以了,不用重启本地服务。
  9. MiniCssExtractPlugin
    用来提取css
  10. ManifestPlugin
    这个插件控制webpack打包的manifest输出文件。
    【为什么这么配置还不清楚】
  11. IgnorePlugin
    这个插件用来解析模块时候忽略指定的模块。这里的配置是为了给momentjs做优化,忽略了momentjs中的本地化部分。
  12. WorkboxWebpackPlugin.GenerateSW
    用来产生service-worker,开发pwa时候用的
  13. ForkTsCheckerWebpackPlugin
    这个插件在解析typescript时候开启独立进程,用来提高构建的性能

11. node

这个选项模拟node环境,即可以在非node环境提供一些node api的polyfill,可以让原本为node写的代码可以运行在浏览器端。

这里的配置了一堆empty和mock,目的是考虑可能有第三方的模块引入了node模块但是没有在浏览器里使用,那么如果不增加空的polyfill,这些第三方模块由于引用node模块失败就会不能执行,这里配置了一些空的polyfill就可以让这些第三方的模块正常运行。

12. performance

这里关闭了webpack提供的文件大小超限时候的警告。因为cra自己实现了一个FileSizeReporter用来做这件事,在build.js中使用FileSizeReporter来进行必要警告。

webpack.config.js提供的能力

1. 基本能力

  • js解析,babel的完善配置可以让我们轻松使用esnext的各种语法
  • sass及样式解析
  • postcss
  • css module
  • css文件提取
  • 图片解析
  • 其他文件解析(file-loader)
  • 优秀的压缩效果(Terser)
  • 分包(利用webpack的默认配置)
  • 模块热重载
  • service-worker
  • yarn pnp
  • html编译注入静态资源引用
  • typescript
  • html和js中使用环境变量
  • eslint代码检查

2. 优化支持

  • 友好的错误提示,包括路径大小写、编译错误的iframe展示、构建体积超限警告、未找到模块时候的友好提示等
  • node polyfill,友好地支持可能引入node模块的第三方库
  • 一些优化工作
    • webpack运行时代码打包
    • webpack运行时代码内联

3. 自定义配置

  • 支持配置图片最小打包体积
  • 支持配置public_url
  • 支持配置是否需要内联runtime chunk
  • 支持配置是否产生source map文件
  • 支持配置扩展eslint
  • env配置环境变量
  • jsconfig配置编译选项控制增加模块解析路径和目录别名(src)

webpack.config.js欠缺的支持

  • less、stylus
  • css nib
  • babel的包按需加载
  • webpack的自定义配置
  • optional chaining语法支持
  • nullish-coalescing-operator的支持
  • mock

webpackDevServer.config.js

0. 前言

在梳理这个配置之前需要先了解一些wds的一些主要的概念

  1. inline modeiframe mode
    在实时重载的场景下,使用inline模式或者iframe模式将会让wds在reload的提示展示和客户端运行时代码的打包位置这两个方面有所不同。
    inline mode下:reload提示在控制台;客户端处理重载的代码将被打包到每个入口bundle中,即运行时代码将“内联”到每个bundle中(这样可能造成不必要的bundle体积增大)。
    iframe mode下:reload通过iframe的形式展示在浏览器;客户端处理重载的运行时代码会被单独打包并加载。
  2. proxy
    wds提供了代理的能力,它可以将请求到wds服务器的请求代理到其它指定的地址。这给开发者开发调试提供了极大的便利性。

了解了上面这些概念后,我们看下webpackDevServer.config.js这个文件

webpackDevServer.config.js对外暴露一个方法,这个方法接收两个参数,返回wds配置。

这两个参数分别是proxyallowedHost

下面详细分析webpackDevServer.config.js文件的内容

1. webpackDevServer.config.js方法的两个参数

  1. proxy
    这个参数是start.js调用方法时候传进来的,实参是通过app的package.json中的proxy字段进行处理(/react-dev-utils/WebpackDevServerUtils.js - prepareProxy()),处理得到的proxy配置直接传给wds配置。
    从这个proxy的处理方式来看,我们可以通过在app的package.json中配置proxy为一个字符串(cra要求这个开发者配置的proxy必须是字符串)来指定要代理到的服务器。
  2. allowedHost
    这个参数最终被赋值给wds配置的public字段。

2. wds配置项

  1. disableHostCheck
    是否禁用host检查。
    处于安全考虑,wds默认检查请求的host,即如果打到wds的请求,会做host检查,目的是为了防止不安全的请求,比如,如果wds配置了代理,请求打到wds后可以请求到代理的服务器,比如一些后端接口,那么可能造成私密数据泄露。而如果没有配置代理,因为wds只serve了public中的静态文件(通过contentBase属性配置),就没有什么严重的安全问题,因此cra这里的配置值是!proxy || process.env.DANGEROUSLY_DISABLE_HOST_CHECK === 'true',即没有配置代理可以禁用host检查,另外还支持脚手架使用者通过环境变量来控制。
  2. compress
    启用gzip压缩,这里配置为true。给所有静态资源开启gzip压缩
  3. clientLogLevel
    客户端日志等级,cra认为wds自带的日志没什么卵用,所以配置了none。这个配置值不会影响编译警告和报错,这些信息依然会展示。
  4. contentBase
    这个属性指定wds serve的目录。wds默认serve的内容包含两部分:内存中的构建产物和当前目录。cra认为,由于本地开发其实就是模拟生产环境的场景,即让生产环境和本地开发环境保持相同的效果。因为wds构建产物已经从内存中被serve,那么既然wds还额外serve了当前目录,由于本地和生产环境一致,所以生产环境也应该将当前目录的所有文件拷贝过去,但是,拷贝所有的当前目录文件会让一些隐秘的文件暴露。因此cra的选择是,配置contentBasepublic目录,即一些不经过构建的文件。这样由于cra构建时候会将public目录下的文件都拷贝到输出目录,因此生成环境和本地环境都可以通过请求根路径直接访问到public下的文件。
  5. watchContentBase
    默认对于contentBase下的文件改动,wds不会触发reload,因此cra配置这个选项为true,当public目录下的文件改动,也会触发重新编译和reload。
  6. hot
    wds默认监听相关文件改动,进行自动刷新的。而如果想要开启模块热重载而不是使用wds简单的默认的自动reload功能,就需要配置这个属性为true,cra在此配置了hottrue。如果wds配置了这个属性,默认会引入webpack.HotModuleReplacementPlugin,这样就完全开启模块热重载功能了。
    cra在这里配置后,css已经可以做到模块热重载了,因为在development环境下,使用style-loader处理样式,将编译好的样式代码作为style元素插入到页面中,【可能是由于sass-loaderstyle-loader实现了wds的HMR接口】因此css已经有模块热重载的效果了,即改变sass代码后style标签上的样式也会相应变化。但是js还并没有模块热重载的效果,因为js代码变动后,webpackbabel-loaderbabel插件等并不知道一次变化应该对应什么样的操作。所以我们需要在项目中加入react的热重载的代码(react-hot-loader/root),这样才能让文件变化后,react根据文件改动计算出变化后的组件和dom节点,然后进行dom替换。
  7. transportMode
    这个属性用来控制wds的websocket模式,默认使用sockjs,这是一个提供全双工通信的js库,提供node端(sockjs-node)和客户端支持,cra在这里配置为ws,原因是cra在development环境下,引入了wds客户端代码以在浏览器上展示错误提示,这个wds客户端代码使用原生的websocket来连接服务器,因此要求wds提供的全双工通信也是websocket。
  8. injectClient
    是否给打包后的代码中注入wds客户端代码,cra配置这个为false,是因为cra加载了自定义的客户端代码,因此不需要默认的客户端代码。
  9. sockHost sockPath sockPort
    这3个用来自定义访问wds的websocket的host、path和port,都是根据环境变量配置的
    【不知道这个具体在什么场景下有用】
  10. publicPath
    这个字段定义了wds支持的访问路径,默认是"/"
    wds配置中的publicPathwebpack.config.js中的output.publicPath不同。wds的这个配置定义了wds serve资源的访问路径,而webpack.config.js中的配置会让webpack打包时候将静态资源的引用路径增加这个前缀。官网推荐webpack.config.js中的配置和wds中的这个配置保持一致。
  11. quiet
    禁用wds默认日志,cra使用自己的日志
  12. watchOptions.ignored
    排除监听一些目录下的文件,以减少cpu损耗
    cra忽略了node_modules,但是保留了appPath中src目录下的node_modules
  13. https
    wds支持https,也支持使用默认的证书
    这个选项是使用https访问wds的配置项,cra配置的是根据环境变量的配置决定是否支持https,也可以根据环境变量提供自己的证书和秘钥文件
  14. host
    host默认0.0.0.0,这样就支持外部使用ip访问。host也支持通过环境变量配置。
  15. overlay
    错误或者警告时候,浏览器端全屏展示相关信息
    cra禁用了此功能(overlay: false),因为cra自己提供了相关的能力
  16. historyApiFallback
    disableDotRuletrue支持dot路由,否则dot路由会访问失败
    index设置为发布路径,保证访问fallback时候wds能正确查找到入口html
  1. {
  2. disableDotRule: true,
  3. index: paths.publicUrlOrPath,
  4. }
  1. public
    告诉内联模式的客户端代码连接地址。
    【不了解具体应用场景】
  2. proxy
    参考上面说明的参数
  3. beforeafter
    wds提供了这两个钩子注册dev-server的中间件,before是在需要在dev-server自带中间件之前注册的中间件,after是在之后的。
    这里需要注意的是,wds支持用户在项目src目录下的setupProxy.js中配置代理中间件,来进行代理的设置。
    【还有几个其他的中间件,暂时不了解作用】