React-Webpack5-TypeScript打造工程化多页面应用

React-Webpack5-TypeScript打造工程化多页面应用

多页面应用打包

日常工作中大部分场景下我们都是在使用 webpack 构建传统单页面 spa 应用。

所谓的单页面应用也就是说打包后的代码仅仅生成一份 html 文件,基于前端路由 js 去控制渲染不同的页面。

当然所谓的多页面应用简单来说也就是打包后生成多个 html 文件。

这篇文章中我们来重点介绍多页面应用,文章中涉及的内容纯干货。我们废话不多说,一篇文章让你彻底搞懂所谓工程化的多页面应用构建。

文章中涉及的模板配置可以点击这里查看 戳这里

不要忘记给一个 star 呀大佬们(祈求脸.jpg)

前边部分是基于基础配置从零开始搭建一个 React+TypeScript+Webpack 的讲解部分,如果这块你已经足够了解了,可以直接跳到 切入多页面应用 去查看动态多页面部分的配置。

最终我们达到的效果如下 :

原谅我实在是不会 gif...

初始化目录结构

让我们来先初始化最基础的工程目录:

github.com/19Qingfeng/p

让我们先来安装 webpack 以及 React :

yarn add -D webpack webpack-cli

yarn add react react-dom

webpack-cli webpack 的命令行工具,用于在命令行中使用 webpack

接下来让我们去分别创建不同的页面目录,假设我们存在两个多页面应用。

一个 editor 编辑器页面,一个 home 主页。

安装完成之后让我来改变改变目录文件:

创建的项目配置如下,我们分别先来讲讲这两个基础文件夹

  • containers 文件夹中存放不同项目中的业务逻辑
  • packages 文件夹中存放不同项目中的入口文件
这两个文件中的内容我们先不关心,仅建立好最基础的文件目录。

配置 react 支持

接下来让我们的项目先支持最原始的 jsx 文件,让项目支持 react jsx

支持 jsx 需要额外配置 babel 去处理 jsx 文件,将 jsx 转译成为浏览器可以识别的 js

这里我们需要用到如下几个库:

  • babel-loader
  • @babel/core
  • @babel/preset-env
  • @babel/plugin-transform-runtime
  • @babel/preset-react

我们来稍微梳理一下这几个 babel 的作用,具体 babel 原理这里我不进行过分深究。

babel-loader

首先对于我们项目中的 jsx 文件我们需要通过一个"转译器"将项目中的 jsx 文件转化成 js 文件, babel-loader 在这里充当的就是这个转译器。

@babel/core

但是 babel-loader 仅仅识别出了 jsx 文件,内部核心转译功能需要 @babel/core 这个核心库, @babel/core 模块就是负责内部核心转译实现的。

@babel/preset-env

@babel/prest-env babel 转译过程中的一些预设,它负责将一些基础的 es 6+ 语法,比如 const/let... 转译成为浏览器可以识别的低级别兼容性语法。

这里需要注意的是 @babel/prest-ent 并不会对于一些 es6+ 并没有内置一些高版本语法的实现比如 Promise polyfill ,你可以将它理解为语法层面的转化不包含高级别模块( polyfill )的实现。

@babel/plugin-transform-runtime

@babel/plugin-transform-runtime ,上边我们提到了对于一些高版本内置模块,比如 Promise/Generate 等等 @babel/preset-env 并不会转化,所以 @babel/plugin-transform-runtime 就是帮助我们来实现这样的效果的,他会在我们项目中如果使用到了 Promise 之类的模块之后去实现一个低版本浏览器的 polyfill

其实与 @babel/plugin-transform-runtime 达到相同的效果还可以直接安装引入 @babel/polyfill ,不过相比之下这种方式不被推荐,他存在污染全局作用域,全量引入造成提及过大以及模块之间重复注入等缺点。

此时这几个插件我们已经可以实现将 es6+ 代码进行编译成为浏览器可以识别的低版本兼容性良好的 js 代码了,不过我们还缺少最重要一点。

目前这些插件处理的都是 js 文件,我们得让她能够识别并处理 jsx 文件。

@babel/preset-react

此时就引入了我们至关重要的 @babel/preset-react 这个插件,在 jsx 中我们使用的 jsx 标签实质上最终会被编译成为:

有兴趣的朋友可以看看我之前的这篇文章 ef ="h ttps://juejin.cn/post/6998859047524892709">React中的jsx原理解析

最终我们希望将 .jsx 文件转化为 js 文件同时将 jsx 标签转化为 React.createElement 的形式,此时我们就需要额外使用 babel 的另一个插件- @babel/preset-react

@babel/preset-react 是一组预设,所谓预设就是内置了一系列 babel plugin 去转化 jsx 代码成为我们想要的 js 代码。

babel 所需要的配置这里我们已经讲完了需要用到的包和对应的作用,因为 babel 涉及的编译原理部分的直接特别多所以我们这里仅仅了解如何配置就可以了,有兴趣的朋友可以移步 babel 官网去详细查看。

项目 babel 配置

接下来让我们来安装这5个插件,并且在 webpack 中进行配置:

yarn add -D @babel/core @babel/preset-env babel-loader @babel/plugin-transform-runtime @babel/preset-react 

创建基础 webpack 配置

当我们安装完成上边的编译工具后,我们就来创建一个基础的 webpack.config.js 来使用它来转译我们的 jsx 文件:

我们来在项目跟目录下创建一个 scripts/webpack.base.js 文件。

关于 webpack 对于代码的转译,所谓转译直白来讲也就是 webpack 默认只能处理基于 js json 的内容。

如果我们想让 webpack 处理我们的 jsx 内容,就需要配置 loader 告诉它,

"嘿, webpack 碰到 .jsx 后缀的文件使用这个 loader 来处理。"

我们来编写基础的 babel-loader 配置:

webpack.base.js

// scripts/webpack.base.js
const path = require('path');
module.exports = {
  // 入口文件,这里之后会着重强调
  entry: {
    main: path.resolve(__dirname, '../src/packages/home/index.jsx'),
  module: {
    rules: [
        test: /\.jsx?$/,
        use: 'babel-loader',

此时我们已经告诉 webpack ,如果遇到 jsx 或者 js 代码,我们需要使用 babel-loader 进行处理,通过 baebel 将项目中的 js/jsx 文件处理成为低版本浏览器可以识别的代码。

.babelrc

上边我们讲到了 babel-loader 仅仅是一个桥梁,真正需要转译作用的其他的插件。接下来就让我们来使用它:

babel-loader 提供了两种配置方式,一种是直接在 webpack 配置文件中编写 options ,另一个是官方推荐的在项目目录下建立 .babelrc 文件单独配置 babel

这里我们采用第二种推荐的配置:

// .babelrc
  "presets": [
    "@babel/preset-env",
    "@babel/preset-react"
  "plugins": [
      "@babel/plugin-transform-runtime",
        "regenerator": true

packages/home 创建入口文件 index.jsx

接下来让我们在 packages/home 目录下创建 home 业务的入口文件

// packages/home/index.jsx
import ReactDom from 'react-dom'
const Element = <div>hello</div>
ReactDom.render(<Element />,document.getElementById('root'))

到这一步相关基础配置文件已经初具模型了,我们需要做的就是当我们调用 webpack 打包我们项目的时候使用我们刚才书写的 webpack.base.js 这个配置文件。

pacakge.json 增加 build 脚本

我们来稍微修改一下 pacakge.json :

{
  "name": "pages",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "build": "webpack --config ./scripts/webpack.base.js"
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@babel/core": "^7.15.5",
    "@babel/plugin-transform-runtime": "^7.15.0",
    "@babel/preset-env": "^7.15.6",
    "@babel/preset-react": "^7.14.5",
    "babel-loader": "^8.2.2",
    "webpack": "^5.53.0",
    "webpack-cli": "^4.8.0"
  "dependencies": {
    "react": "^17.0.2",
    "react-dom": "^17.0.2"
这里我们删除了初始化的 test 脚本,增加了 build 命令。

接下来让我们运行 npm run build :

这一步我们已经成功让 webpack 识别 jsx 代码并且支持将高版本 js 转化为低版本 javascript 代码了。

如果有疑问的话,可以停留下来在想一想大概流程。主要就是:

  1. 创建 babel 配置转译 jsx/js 内容。
  2. 创建入口文件。
  3. webpack 中对于 jsx/js 内容使用 babel-loader 调用 babel 配置好的预设和插件进行转译。

接下来让我们继续来支持 TypeScript 吧!

配置 TypeScript 支持

针对 TypeScript 代码的支持其实业内存在两种编译方式:

  1. 直接通过 TypeScript 去编译 ts/tsx 代码。
  2. 通过 babel 进行转译。

其实这两种方式都是可以达到编译 TypeScript 代码成为 JavaScript 并且兼容低版本浏览器代码的。

有兴趣的朋友可以自行搜索这两种方式的差异,平常在一些类库的打包时我会直接使用 tsc 结合 tsconfig.js 编译 ts 代码。

日常工作中,大部分情况我个人还是会使用 babel 进行转译,因为涉及到业务往往是需要 css 等静态资源与 ts 代码一起打包,那么使用 babel + webpack 其实可以很好的一次性囊括了所有的资源编译过程。

babel 支持 Typescirpt

babel 内置了一组预设去转译 TypeScript 代码 -- @babel/preset-typescript

接下来让我们来使用 @babel/preset-typescript 预设来支持 TypeScript 语法吧。

npm install --save-dev @babel/preset-typescript

安装完成之后让我们一步一步来修改之前的配置:

首先我们先来修改之前 .babelrc 配置文件,让 babel 支持转译的 ts 文件:

{
  "presets": [
    "@babel/preset-env",
    "@babel/preset-react",
+   "@babel/preset-typescript"
  "plugins": [
      "@babel/plugin-transform-runtime",
        "regenerator": true

这里我们在 presets 添加了 @babel/preset-typescrpt 预设去让 babel 支持 typescript 语法。

此时我们的 babel 已经可以识别 TypeScript 语法了

webpack 支持 ts/tsx 文件

不要忘记同时修改我们的 webpack babel-loader 的匹配规则:

// webpack.base.jf
const path = require('path');
module.exports = {
  // 入口文件,这里之后会着重强调
  entry: {
    // 这里修改`jsx`为`tsx`
    main: path.resolve(__dirname, '../src/packages/home/index.tsx'),
  module: {
    rules: [
        // 同时认识ts jsx js tsx 文件
        test: /\.(t|j)sx?$/,
        use: 'babel-loader',

这里我们将 ts,js,tsx,jsx 文件都交给 babel-loader 处理。

初始化 tsconfig.json

现在,我们项目中已经可以支持 tsx 文件的编写,同时也支持编译 ts 文件为低版本 js 文件了。

在使用 Ts 时,通常我们需要配置 typescript 的配置文件,没错就是 tsconfig.json

也许你已经见到过很多次 tsconfig.json 了,接下来让我们去安装 typescript 并且初始化吧~

项目内安装 Ts :

yarn add -D typescript

调用 tsc --init 命令初始化 tsconfig.json :

npx tsc --init

关于 npx 命令和 npm 的关系,之后我会在另一篇文章中细细讲述。了解他在这里的用途:调用当前项目内 node_modules/typescript/bin 的可执行文件执行 init 命令就可以了。

现在我们的目录应该是这样的:

虽然说我们使用 babel 进行的编译, tsconfig.json 并不会在编译时生效。但是 tsconfig.json 中的配置非常影响我们的开发体验,接下来我们就来稍微修改一下一下它吧。

配置 tsconfig.json

首先我们来找到对应的 jsx 选项:

他的作用是指定 jsx 的生成什么样的代码,简单来说也就是 jsx 代码将被转化成为什么。

这里我们将它修改为 react

接下来我们来修改一下 ts 中的模块解析规则,将它修改为 node :

"moduleResolution": "node",

这里暂时我们先修改这两个配置,后续配置我们会在后边的讲解中渐进式的进行配置。

推荐一本开源的电子书,这里罗列了大部分 tsconfig.json配置信息

处理报错

我们已经在项目中完美支持了 typescript ,接下里让我们把 pacakges/home/index.jsx 改为 packages/home/index.tsx 吧.

修改完成后缀后我们再来看看我们想项目文件:

我们来一个一个解决这些报错:

首先我们引用第三方包在 TypeScript 文件时,简单来说它会寻找对应包的 package.json 中的 type 字段查找对应的类型定义文件。

react react-dom 这两个包代码中都不存在对应的类型声明,所以我们需要单独安装他们对应的类型声明文件:

yarn add -D @types/react-dom @types/react

大多数额外的类型定义包,你可在 这里 找到

安装完成之后,我们重新再看看当前的 index.tsx 文件:

此时,仅剩下一个报错了。让我们来仔细定位一下错误。 ts 告诉我们 ReactDom.render 方法中传入的参数类型不兼容。嗯,本质上是我们 react 语法写错了。修改后的代码如下:

此时我们的项目已经可以完成支持 typescript react 了。

webpack 配置静态资源支持

一个成熟的项目只能有 ts 怎么能够呢? 毕竟一个成熟的业务仔怎么脱离 css 的魔抓呢

也许你之前接触过 webpack5 之前的静态资源处理, file-loader,url-loader,row-loader 这些 loader 是不是听起来特别熟悉。

webpack 默认是不支持非 js 文件的,所以在 webpack5 之前我们通过 loader 的方式返回可执行的 js 脚本文件,内部将处理这些 webpack 不认识的文件。

webpack 5+ 版本之后,这些 loader 的作用都已经被内置了~

接下来我们来看看应该如何配置,具体对应的作用可以查看 ="https://webpack.docschina.org/guides/asset-modules/#inlining-assets">webpack资源模块

处理图片,文件资源文件

资源模块类型(asset module type),通过添加 4 种新的模块类型,来替换所有这些 loader:
  • asset/resource 发送一个单独的文件并导出 URL。之前通过使用 file-loader 实现。
  • asset/inline 导出一个资源的 data URI。之前通过使用 url-loader 实现。
  • asset/source 导出资源的源代码。之前通过使用 raw-loader 实现。
  • asset 在导出一个 data URI 和发送一个单独的文件之间自动选择。之前通过使用 url-loader ,并且配置资源体积限制实现。

当在 webpack 5 中使用旧的 assets loader (如 file-loader / url-loader / raw-loader 等)和 asset 模块时,你可能想停止当前 asset 模块的处理,并再次启动处理,这可能会导致 asset 重复,你可以通过将 asset 模块的类型设置为 'javascript/auto' 来解决。

关于配置 type:'asset' 后,webpack 将按照默认条件,自动地在 resource inline 之间进行选择:小于 8kb 的文件,将会视为 inline 模块类型,否则会被视为 resource 模块类型。

我们可以通过设置 Rule.parser.dataUrlCondition.maxSize 选项来修改此条件

其实 maxSize 就相当于 url-loader 中的 limit 属性,资源大小在 maxSize 之内使用行内 asset/inline 处理,超过之后就使用 resource 导出资源。当然这个配置也支持导出一个函数自定义配置实现。

了解了 assets 模块的用途之后,我们来试着配置它来处理静态资源:

const path = require('path');
module.exports = {
  // 入口文件,这里之后会着重强调
  entry: {
    main: path.resolve(__dirname, '../src/packages/home/index.tsx'),
  module: {
    rules: [
        // 同时认识ts jsx js tsx 文件
        test: /\.(t|j)sx?$/,
        use: 'babel-loader',
        test: /\.(png|jpe?g|svg|gif)$/,
        type:'asset/inline'
        test: /\.(eot|ttf|woff|woff2)$/,
        type: 'asset/resource',
        generator: {
          filename: 'fonts/[hash][ext][query]',

这一步我们已经关于图片和字体文件配置已经配置完毕了,接下来我们来修改一下目录结构验证一下我们的配置是否生效:

验证配置效果

修改 packages

首先让我们先来修改 packages packages 文件夹之前讲过是存放多页面应用中每个页面的入口文件的。让我们在 home 文件夹下先新建一个 app.tsx :

// src/packages/home/app.tsx
import React from 'react'
import ReactDom from 'react-dom'
// 这里App仅接着就会降到 它就是就是一个React FunctionComponent
import { App } from '../../containers/home/app.tsx'
ReactDom.render(<App />, document.getElementById('root'))

修改 containers

containers 文件夹中的内容是存放每个页面应用不同的业务逻辑的部分。

让我们在 containers 中新建 app.tsx 作为跟应用,以及同级目录下新建 assets , styles , views 三个目录:

  • assets 存放当前模块相关静态资源,比如图片,字体文件等。
  • styles 存在当前模块相关样式文件,我们还没有配置相关样式文件的处理,之后会详细介绍。
  • views 存放当前模块下相关页面逻辑页面拆分

我们给 assets 中新建一个 images 文件夹,放入一张 logo ,图片。

修改 app.tsx 内容如下:

import React from 'react'
import Banner from './assets/image/banner.png'
const App: React.FC = () => {
  return <div>
    <p>Hello,This is pages!</p>
    <img src={Banner} alt="" />
export {
}

yarn build

我们当前的目录结构如下:

基本的目录结构我们已经搭建成功,接下来让我们运行 yarn build

首先根据 webpack 中的入口文件会去寻找 packages/home/index.tsx ,我们在 index.tsx 中引入了对应的 containers/app.tsx webpack 会基于我们的 import 语法去处理模块依赖关系构建模块。

同时因为我们在 app.tsx 中引入了图片

// webpack.base.js
        test: /\.(png|jpe?g|svg|gif)$/,
        type: 'asset/inline'

此时 type:'assets/inline' 针对图片的处理就会生效了!

让我们来看看 build 之后的文件:

asset/inline 会讲资源文件内置成为 base64 url-loader 是相同的作用。

当前目前 webpack asset/inline 模块会将所有资源转化为 base64 在行内迁入,如果要达到 url-loader limit 的配置需要禁用 asset/inline 使用 url-loader 处理或者使用 type:'asset' 来处理.

解决报错

细心的你可能已经发现了,目前我们项目中存在两个问题:

  1. ts 文件中针对 image 的引入, ts 并不能正确的识别。

解决这个问题的方式其实很简单,我们定义一个 image.d.ts 在根目录下:

declare module '*.svg';
declare module '*.png';
declare module '*.jpg';
declare module '*.jpeg';
declare module '*.gif';
declare module '*.bmp';
declare module '*.tiff';
declare 为声明语法,意思为声明 ts 全局模块,这样我们就可以正常引入对应的资源了。
  1. 我们在 index.tsx 中引入了对应的 app.tsx ,当存在后缀时 ts 会进行报错提示:

接下来让我们来解决这个问题吧。其实无法就是引入文件时默认后缀名的问题:

  • 目前 webpack 不支持默认后缀名 .tsx
  • tsconfig.json 中是支持后缀名 .tsx ,所以显示声明会提示错误。

我们来统一这两个配置:

别名统一

修改 webpack 别名配置

// webpack.base.js
const path = require('path');
module.exports = {
  // 入口文件,这里之后会着重强调
  entry: {
    main: path.resolve(__dirname, '../src/packages/home/index.tsx'),
  resolve: {
    alias: {
      '@src': path.resolve(__dirname, '../src'),
      '@packages': path.resolve(__dirname, '../src/packages'),
      '@containers': path.resolve(__dirname, '../src/containers'),
    mainFiles: ['index', 'main'],
    extensions: ['.ts', '.tsx', '.scss', 'json', '.js'],
  module: {
    rules: [
        // 同时认识ts jsx js tsx 文件
        test: /\.(t|j)sx?$/,
        use: 'babel-loader',
        test: /\.(png|jpe?g|svg|gif)$/,
        type: 'asset/inline'
        test: /\.(eot|ttf|woff|woff2)$/,
        type: 'asset/resource',
        generator: {
          filename: 'fonts/[hash][ext][query]',

这里我们添加了 resolve 的参数,配置了别名 @src , @package , @container

以及当我们不书写文件后缀时,默认的解析规则 extensions 规则。

同时还配置了 mainFiles ,解析文件夹路径~

这个三个配置都比较基础,就不过多深入了哈。如果仍然还是不是很懂,用到的时候多翻翻慢慢也就记住啦!

让我们来尝试修改 index.tsx ,使用别名来引入:

此时我们发现并没有路径提示,这个!是真的无法接受!

原因是我们是基于 typescript 开发,所以 ts 文件中并不知道我们在 webpack 中配置的别名路径。

所以我们需要做的就是同步修改 tsconfig.json ,让 tsconfig.json 也支持别名以及刚才的配置,达到最佳的开发体验。

修改 tsconfig.json 别名配置

我们来修改 tsconfig.json ,让 ts 同步支持引入:

// tsconfig.json
     "baseUrl": "./",
    /* Specify the base directory to resolve non-relative module names. */
    "paths": {
      "@src/*": ["./src/*"],
      "@packages/*": ["./src/packages/*"],
      "@containers/*": ["./src/containers/*"],

如果要配置 paths 那么一定是要配置 baseUrl 的,所谓 baseUrl 就是我们的 paths 是相对于那个路径开始的。

所以我们在 paths 中添加对应的别名路径就可以完成配置,让 ts 也可以合理解析出我们的类型别名。

此时我们再来看看:

已经可以正确出现路径提示了,是不是非常 nice

针对路径/文件目录解析规则配置我们目前就告一段落了~

配置 css/sass

接下来我们给项目添加一些样式来美化它。

这里其实 React 项目有太多有关 css 的争吵了,但是无论如何我们是都要在 webpack 中针对 css 进行处理的。

这里我选择使用 sass 预处理器进行演示,其他 less 等都是同理。

针对于 sass 文件,同样是 webpack 不认识的文件。咱们同样是需要 loader 去处理。

这里用到的 loader 如下:

  • sass-loader
  • resolve-url-loader
  • postcss-loader
  • css-loader
  • MiniCssExtractPlugin.loader

我们来一个一个来分析这些 loader 的作用的对应的配置:

sass-loader

针对于 sass 文件我们首先一定是要使用 sass 编译成为 css 的,所以我们首先需要对 .scss 结尾的文件进行编译成为 css 文件。

这里我们需要安装:

yarn add -D sass-loader sass 
sass-loader 需要预先安装 Dart Sass Node Sass (可以在这两个链接中找到更多的资料)。这可以控制所有依赖的版本, 并自由的选择使用的 Sass 实现。

sass-loader 的作用就类似我们之前讲到过的 babel-loader ,可以将它理解成为一个桥梁, sass 转译成为 css 的核心是由 node-sass 或者 dart-sass 去进行编译工作的。

resolve-url-loader

上一步我们已经讲到过 sass-loader sass 文件转化为 css 文件。

但是这里有一个致命的问题,就是关于 webpack scss 文件中

由于 Saass 的实现没有提供 url 重写 的功能,所以相关的资源都必须是相对于输出文件( ouput )而言的。

  • 如果生成的 CSS 传递给了 css-loader ,则所有的 url 规则都必须是相对于入口文件的(例如: main.scss )。
  • 如果仅仅生成了 CSS 文件,没有将其传递给 css-loader ,那么所有的 url 都是相对于网站的根目录的。

所以针对于 sass 编译后的 css 文件中的路径是不正确的,并不是我们想要的相对路径模式。

想要解决路径引入的问题业内有很多现成的办法,比如通过

  1. 路径变量定义引入路径
  2. 定义别名, sass 中使用别名引入路径
  3. resolve-url-loader
这里我们采用 resolve-url-loader 来处理文件引入路径问题。

不要忘记 yarn add -D resolve-url-loader

postcss-loader

PostCSS是什么?或许,你会认为它是预处理器、或者后处理器等等。其实,它什么都不是。它可以理解为一种插件系统。

针对于 postcss 其实我这里并不打算深入去讲解,它是 babel 一样都是两个庞然大物。拥有自己独立的体系,在这里你需要清楚的是我们使用 postcss-loader 处理生成的 css

第一步首先安装 post-css 对应的内容:

yarn add -D postcss-loader postcss

postcss-loader 同时支持直接在 loader 中配置规则选项,也支持单独建立文件配置,这里我们选择单独使用文件进行配置:

我们在项目根目录下新建一个 postcss.config.js 的文件:

module.exports = {
  plugins: [
    require('autoprefixer'),
    require('cssnano')({
      preset: 'default',

这里我们使用到了两个 postcss 的插件:

  • autoprefixer 插件的作用是为我们的 css 内容添加浏览器厂商前缀兼容。
  • cssnano 的作用是尽可能小的压缩我们的 css 代码。

接下来我们去安装这两个插件:

yarn add -D cssnano autoprefixer@latest

css-loader

css-loader 是解析我们 css 文件中的 @import/require 语句分析的.

yarn add -D css-loader

MiniCssExtractPlugin.loader

这个插件将 CSS 提取到单独的文件中。它为每个包含 CSS JS 文件创建一个 CSS 文件。它支持按需加载 CSS 和 SourceMaps

这里需要提一下他和 style-loader 的区别,这里我们使用了 MiniCssExtractPlugin 代替了 style-loader

style-loader 会将生成的 css 添加到 html header 标签内形成内敛样式,这显然不是我们想要的。所以这里我们使用 MiniCssExtractPlugin.loader 的作用就是拆分生成的 css 成为独立的 css 文件。

yarn add -D mini-css-extract-plugin

生成 sass 最终配置文件

接下来我们来生成 sass 文件的最终配置文件:

// webapck.base.js
const path = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
module.exports = {
  // 入口文件,这里之后会着重强调
  entry: {
    main: path.resolve(__dirname, '../src/packages/home/index.tsx'),
  resolve: {
    alias: {
      '@src': path.resolve(__dirname, '../src'),
      '@packages': path.resolve(__dirname, '../src/packages'),
      '@containers': path.resolve(__dirname, '../src/containers'),
    mainFiles: ['index', 'main'],
    extensions: ['.ts', '.tsx', '.scss', 'json', '.js'],
  module: {
    rules: [
        test: /\.(t|j)sx?$/,
        use: 'babel-loader',
        test: /\.(sa|sc)ss$/,
        use: [
            loader: MiniCssExtractPlugin.loader,
          'css-loader',
          'postcss-loader',
            loader: 'resolve-url-loader',
            options: {
              keepQuery: true,
            loader: 'sass-loader',
            options: {
              sourceMap: true
        test: /\.(png|jpe?g|svg|gif)$/,
        type: 'asset/inline'
        test: /\.(eot|ttf|woff|woff2)$/,
        type: 'asset/resource',
        generator: {
          filename: 'fonts/[hash][ext][query]',
  plugins: [
    new MiniCssExtractPlugin({
      filename: 'assets/[name].css',
// postcss.config.js
module.exports = {
  plugins: [
    require('autoprefixer'),
    require('cssnano')({
      preset: 'default',

完成这些配置之后,同时我们在 containers/src/home/styles 目录中新建一个 sass 文件 index.scss :

让我们来写一些简单的样式文件:

.body {
 background-color: red;

这之后,让我们重新运行 yarn build

检查生成的 dist 文件我们发现,我们的 sass 被成功的编译成为了 css 文件并且删除了多于空格(进行了压缩)

这一步我们 scss 的基础配置也已经完成了!

配置 html 页面

当前我们所有涉及的都是针对单页面应用的配置,此时我们迫切需要一个 html 展示页面。

此时就引入我们的主角,我们后续的多页面应用也需要机遇这个插件生成 html 页面

html-webpack-plugin ,其实看到这里我相信大家对这个插件原本就已经耳熟能详了。

简单介绍一下它的作用: 这个插件为我们生成 HTML 文件,同时可以支持自定义 html 模板。

多页面应用主要基于它的 chunks 这个属性配置,我们这里先买个关子。

让我们来使用一下这个插件:

yarn add --dev html-webpack-plugin

我们在项目根目录下创建一个 public/index.html

<!DOCTYPE html>
<html lang="en">
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
  <div id='root'></div>
</body>
</html>

我们使用这个文件作为插件的模板文件,同时与入口文件中的 ReactDom.reander(...,document.getElementById('root')) 进行呼应,在页面创建一个 id=root div 作为渲染节点。

// webpack.base.js
const path = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const htmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
  // 入口文件,这里之后会着重强调
  entry: {
    main: path.resolve(__dirname, '../src/packages/home/index.tsx'),
  resolve: {
    alias: {
      '@src': path.resolve(__dirname, '../src'),
      '@packages': path.resolve(__dirname, '../src/packages'),
      '@containers': path.resolve(__dirname, '../src/containers'),
    mainFiles: ['index', 'main'],
    extensions: ['.ts', '.tsx', '.scss', 'json', '.js'],
  module: {
    rules: [
        test: /\.(t|j)sx?$/,
        use: 'babel-loader',
        test: /\.(sa|sc)ss$/,
        use: [
            loader: MiniCssExtractPlugin.loader,
          'css-loader',
          'postcss-loader',
            loader: 'resolve-url-loader',
            options: {
              keepQuery: true,
            loader: 'sass-loader',
            options: {
              sourceMap: true,
        test: /\.(png|jpe?g|svg|gif)$/,
        type: 'asset/inline',
        test: /\.(eot|ttf|woff|woff2)$/,
        type: 'asset/resource',
        generator: {
          filename: 'fonts/[hash][ext][query]',
  plugins: [
    new MiniCssExtractPlugin({
      filename: 'assets/[name].css',
    // 生成html名称为index.html
    // 生成使用的模板为public/index.html
    new htmlWebpackPlugin({
      filename: 'index.html',
      template: path.resolve(__dirname, '../public/index.html'),

此时,当我们再次运行 yarn build 时,我们生成的 dist 目录下会多出一个 html 文件,这个 html 文件会注入我们打包生成后的的 js css 内容。

打开 index.html ,就会展示出我们代码中书写的页面啦~

配置开发环境预览

上边的长篇大论已经能满足一个 SPA 单页面应用的构建了,但是我们总不能每次修改代码都需要执行一次打包命令在预览吧。

这样的话也太过于麻烦了,别担心 webpack 为我们提供了 devServer 配置,支持我们每次更新代码 热重载

我们来使用一下这个功能吧~

首先让我们在 scripts 目录下新建一个 webpack.dev.js 文件,表示专门用于开发环境下的打包预览:

虽然 devServer 已经内置了 hot:true 达到热重载,但是我们仍然需要安装 webpack-dev-server
// webpack.dev.js
const { merge } = require('webpack-merge');
const baseConfig = require('./webpack.base');
const path = require('path');
const devConfig = {
  mode: 'development',
  devServer: {
    // static允许我们在DevServer下访问该目录的静态资源
    // 简单理解来说 当我们启动DevServer时相当于启动了一个本地服务器
    // 这个服务器会同时以static-directory目录作为跟路径启动
    // 这样的话就可以访问到static/directory下的资源了
    static: {
      directory: path.join(__dirname, '../public'),
    // 默认为true
    hot: true,
    // 是否开启代码压缩
    compress: true,
    // 启动的端口
    port: 9000,
module.exports = merge(devConfig, baseConfig);

关于 devServer 的基础配置在代码中进行了注释讲解,当然还存在一些其他的 proxy , onlistening 等配置需要的小朋友可以去 这里查阅

这里需要提到的是 webpack-merge 这个插件是基于 webpack 配置合并的,这里我们基于 webpack.base.js webpack.dev.js 合并导出了一个配置对象。

接下里再让我们修改一下 pacakge.json 下的 scripts 命令。

devServer 需要使用 webpack serve 启动。
...
 "scripts": {
+   "dev": "webpack serve --config ./scripts/webpack.dev.js",
    "build": "webpack --config ./scripts/webpack.base.js",
    "test": "echo \"Error: no test specified\" && exit 1"

接下来让我们运行 yarn dev 就可以在 localhost:9000 访问到我们刚才需要的页面并且实时支持热重载了。

支持端口被占用启动

这里有一个小 tip 当我们的 devServer 端口被占用的时候我们再次启动项目会因为相同的端口被占用而报错

解决端口占用的问题我们需要借助一个第三方库 node-portfinder

它的用法很简答有兴趣的同学可以点击上方库名移步官网来查看用法:

首先先让我们来给 scripts/utils/constant.js 中添加一个常量为固定的启动端口:

// scripts/utils/constant.js
// 固定端口
const BASE_PROT = 9000
module.exports = {
  MAIN_FILE,
  separator,
  BASE_PROT

接下来让我们来修改一下上边的 webpack.dev.js :

const { merge } = require('webpack-merge')
const baseConfig = require('./webpack.base')
const portfinder = require('portfinder')
const path = require('path')
const { BASE_PROT } = require('./utils/constant')
portfinder.basePort = BASE_PROT
const devConfig = {
  mode: 'development',
  devServer: {
    // static允许我们在DevServer下访问该目录的静态资源
    // 简单理解来说 当我们启动DevServer时相当于启动了一个本地服务器
    // 这个服务器会同时以static-directory目录作为跟路径启动
    // 这样的话就可以访问到static/directory下的资源了
    static: {
      directory: path.join(__dirname, '../public'),
    // 默认为true
    hot: true,
    // 是否开启代码压缩
    compress: true,
    // 启动的端口
    port: BASE_PROT,
module.exports = async function () {
  try {
    // 端口被占用时候 portfinder.getPortPromise 返回一个新的端口(往上叠加)
    const port = await portfinder.getPortPromise()
    devConfig.devServer.port = port
    return merge(devConfig, baseConfig)
  } catch (e) {
    throw new Error(e)

我们把导出从对象变成了导出一个函数, webpack 中配置的导出支持一个对象的同时也支持一个函数~

然后函数中调用 portfinder.getPortPromise() 判断当前端口是否占用,如果占用 portfinder 会返回一个新的端口,此时我们修改 devConfig 中的端口并且返回最新的配置进行启动就可以了!

让我们重新运行 yarn dev ~

切入多页面应用

接下来正式进入我们的多入口文件部分讲解。

原理

拆分 js

所谓基于 webpack 实现多页面应用,简单来将就是基于多个 entry ,将 js 代码分为多个 entry 入口文件。

比如我在 src/packages/editor 新建一个入口文件 index.tsx ,同时修改 webpack 中的 entry 配置为两个入口文件, webpack 就会基于入口文件的个数自动进行不同 chunk 之间的拆分。

简单来说就是 webapck 会基于入口文件去拆分生成的 js 代码成为一个一个 chunk

上边的配置我们运行 yarn build 之后生成的 dist 目录如下:

我们可以看到根据不同的入口文件生成了两份 js 文件,一份是 main.js 一份是 editor.js

我们第一步已经完成了,基于不同的入口文件打包生成不同的 js

拆分 html

但是现在我们现在拆分出来的 js 还是在同一个 index.html 中进行引入,我们想要达到的效果是 main.js main.html 中引入成为一个页面。

editor.js editor.html 中引入成为一个单独的页面。

要实现这种功能,我们需要在 html-webpack-plugin 上做手脚。

不知道大家还记不记得我们之前留下的 chunks 这个关子。

所谓的 chunks 配置指的是生成的 html 文件仅仅包含指定的 chunks 块。

这不正是我们想要的嘛!

现在我们打包生成了两份 js 文件分别是 editor.js main.js ,现在我们想生成两份单独的 html 文件,两个 html 文件中分别引入不同的 editor.js main.js

此时我们每次打包只需要调用两次 htmlWebpackPlugin ,一份包含 editor 这个 chunk ,一份包含 main 这个 chunk 不就可以了嘛。

让我们来试一试:

接下来我们运行 yarn build 来看一看生成的文件:

来看看我们生成的 html js 结构,大功告成没一点问题!这样的确能解决,这也是基于 webpack 打包多页面应用的原理。

可是如果我们存在很多个页面的话,首先我们每次都需要手动修改入口文件然后在进行添加一个 htmlWebpackPlugin 这显然是不人性的。

同时如果这个项目下有很多个多页应用,但是我每次开发仅仅关心某一个应用进行开发,比如我负责的是 home 模块,我并不想使用和关心 editor 模块。那么每次我还需要在 dev 环境下进行打包吗?显然是不需要的。

接下来就让我们尝试来修改这些配置将它变成自动化且按需打包的工程化配置吧。

工程化多页配置

工程化原理

我们之前已经讲清楚了 webpack 中的原理了,接下来我们需要实现的过程是:

  1. 每次打包通过 node 脚本去执行打包命令。
  2. 每次打包通过命令行交互命令,读取 pacakges 下的目录让用户选择需要打包的页面。
  3. 当用户选中对应需要打包的目录后,通过环境变量注入的方式动态进行打包不同的页面。

这里我们需要额外用到一下几个库,还不太清楚的同学可以点击去查看一下文档:

这个库改进了 node 的源生模块 child_process ,用于开启一个 node 子进程。

inquirer 提供一些列 api 用于 nodejs 中和命令行的交互。

chalk 为我们的打印带上丰富的颜色.

yarn add -D chalk inquirer execa      

实现代码

首先让我们在 scripts 下创建一个 utils 的文件夹。

utils/constant.js

constant.js 中存放我们关于调用脚本声明的一些常量:

// 规定固定的入口文件名 packages/**/index.tsx
const MAIN_FILE = 'index.tsx'
const chalk = require('chalk')
// 打印时颜色
const error = chalk.bold.red
const warning = chalk.hex('#FFA500')
const success = chalk.green
const maps = {
  success,
  warning,
  error,
// 因为环境变量的注入是通过字符串方式进行注入的
// 所以当 打包多个文件时 我们通过*进行连接 比如 home和editor 注入的环境变量为home*editor
// 注入多个包环境变量时的分隔符
const separator = '*'
const log = (message, types) => {
  console.log(maps[types](message))
module.exports = {
  MAIN_FILE,
  separator,
  BASE_PORT,

utils/helper.js

const path = require('path')
const fs = require('fs')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { MAIN_FILE } = require('./constant')
// 获取多页面入口文件夹中的路径
const dirPath = path.resolve(__dirname, '../../src/packages')
// 用于保存入口文件的Map对象
const entry = Object.create(null)
// 读取dirPath中的文件夹个数
// 同时保存到entry中  key为文件夹名称 value为文件夹路径
fs.readdirSync(dirPath).filter(file => {
  const entryPath = path.join(dirPath, file)
  if (fs.statSync(entryPath)) {
    entry[file] = path.join(entryPath, MAIN_FILE)
// 根据入口文件list生成对应的htmlWebpackPlugin
// 同时返回对应wepback需要的入口和htmlWebpackPlugin
const getEntryTemplate = packages => {
  const entry = Object.create(null)
  const htmlPlugins = []
  packages.forEach(packageName => {
    entry[packageName] = path.join(dirPath, packageName, MAIN_FILE)
    htmlPlugins.push(
      new HtmlWebpackPlugin({
        template: path.resolve(__dirname, '../../public/index.html'),
        filename: `${packageName}.html`,
        chunks: ['manifest', 'vendors', packageName],
  return { entry, htmlPlugins }
module.exports = {
  entry,
  getEntryTemplate,

helper.js 中其实主要导出的 getEntryTemplate 方法,这个方法输入一个 package 的数组,同时返回对应 webpack 需要的 entry html-wepback-plugin 组成的数组。

utils/dev.js

当我们定义好两个辅助文件之后,接下来我们就要实现和"用户"交互的部分了,也就是当用户调用我们这个脚本之后。

  • 首先动态读取 packages 下的目录,获取当前项目下所有的页面文件。
  • 通过命令交互罗列当前所有页面,提供给用户选择。
  • 用户选中后,通过 execa 调用 webpack 命令同时注入环境变量进行根据用户选中内容打包。
const inquirer = require('inquirer')
const execa = require('execa')
const { log, separator } = require('./constant')
const { entry } = require('./helper')
// 获取packages下的所有文件
const packagesList = [...Object.keys(entry)]
// 至少保证一个
if (!packagesList.length) {
  log('不合法目录,请检查src/packages/*/main.tsx', 'warning')
  return
// 同时添加一个全选
const allPackagesList = [...packagesList, 'all']
// 调用inquirer和用户交互
inquirer
  .prompt([
      type: 'checkbox',
      message: '请选择需要启动的项目:',
      name: 'devLists',
      choices: allPackagesList, // 选项
      // 校验最少选中一个
      validate(value) {
        return !value.length ? new Error('至少选择一个项目进行启动') : true
      // 当选中all选项时候 返回所有packagesList这个数组
      filter(value) {
        if (value.includes('all')) {
          return packagesList
        return value
  .then(res => {
    const message = `当前选中Package: ${res.devLists.join(' , ')}`
    // 控制台输入提示用户当前选中的包
    log(message, 'success')
    runParallel(res.devLists)
// 调用打包命令
async function runParallel(packages) {
  // 当前所有入口文件
  const message = `开始启动: ${packages.join('-')}`
  log(message, 'success')
  log('\nplease waiting some times...', 'success')
  await build(packages)
// 真正打包函数
async function build(buildLists) {
  // 将选中的包通过separator分割
  const stringLists = buildLists.join(separator)
  // 调用通过execa调用webapck命令
  // 同时注意路径是相对 执行node命令的cwd的路径 
  // 这里我们最终会在package.json中用node来执行这个脚本
  await execa('webpack', ['server', '--config', './scripts/webpack.dev.js'], {
    stdio: 'inherit',
    env: {
      packages: stringLists,

dev.js 中的逻辑其实很简单,就是通过命令行和用户交互获得用户想要启动的项目之后通过用户选中的 packages 然后通过 execa 执行 webpack 命令同时动态注入一个环境变量。

注入的环境变量是 * 分割的包名。比如用户如果选中 app editor 那么就会注入一个 packages 的环境变量为 app*editor

修改 webpack.base.js

我们已经可以在命令行中和用户交互,并且获得用户选中的 pacakge

此时我们就基于 wepback.base.js 来修改,达到读取用户环境变量进行动态打包的效果:

const path = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const htmlWebpackPlugin = require('html-webpack-plugin');
const { separator } = require('./utils/constant')
const { getEntryTemplate } = require('./utils/helper')
// 将packages拆分成为数组 ['editor','home']
const packages = process.env.packages.split(separator)
// 调用getEntryTemplate 获得对应的entry和htmlPlugins
const { entry, htmlPlugins } = getEntryTemplate(packages)
module.exports = {
  // 动态替换entry
  entry,
  resolve: {
    alias: {
      '@src': path.resolve(__dirname, '../src'),
      '@packages': path.resolve(__dirname, '../src/packages'),
      '@containers': path.resolve(__dirname, '../src/containers'),
    mainFiles: ['index', 'main'],
    extensions: ['.ts', '.tsx', '.scss', 'json', '.js'],
  module: {
    rules: [
        test: /\.(t|j)sx?$/,
        use: 'babel-loader',
        test: /\.(sa|sc)ss$/,
        use: [
            loader: MiniCssExtractPlugin.loader,
          'css-loader',
          'postcss-loader',
            loader: 'resolve-url-loader',
            options: {
              keepQuery: true,
            loader: 'sass-loader',
            options: {
              sourceMap: true,
        test: /\.(png|jpe?g|svg|gif)$/,
        type: 'asset/inline',
        test: /\.(eot|ttf|woff|woff2)$/,
        type: 'asset/resource',
        generator: {
          filename: 'fonts/[hash][ext][query]',
  plugins: [
    new MiniCssExtractPlugin({
      filename: 'assets/[name].css',
    // 同时动态生成对应的htmlPlugins
    ...htmlPlugins

因为我们的目录结构是固定的,所以通过环境变量中的 packages 传入 getEntryTemplate 方法可以获得对应的入口文件,以及生成对应的 htmlWebpackPlugin

到这一步其实我们已经实现了动态打包的所有逻辑了。

接下来让我们来替换一下 package.json 中的脚本

修改 package.json

 "scripts": {
 -  "dev": "webpack serve --config ./scripts/webpack.dev.js",
 +  "dev": "node ./scripts/utils/dev.js",
    "build": "webpack --config ./scripts/webpack.base.js",
    "test": "echo \"Error: no test specified\" && exit 1"

我们将 dev 命令替换成为执行 scripts/utils/dev.js

接下来我们来试一下运行 yarn dev :

我们选中 home 进行打包:

可以看到这次生成的打包结果就真的只有 home.html home.js 相关的内容了,让我们打开 localhost:9000/home.html 可以看到页面上出现了我们想要的内容~

此时尝试去访问 http://localhost:9000/editor.html 会得到 Cannot GET /editor.html

这一步我们大功告成啦~但是当前只有 dev 环境下,让我们接下来来改造 production 环境下的配置。

改造 production 环境

production 环境的代码和 dev 环境的流程思路是一摸一样的,只是针对于 webpack 配置有所不同。

所以我们在 scripts 新建一个 webpack.prod.js 文件:

// 这里我使用了默认的`webpack`production下的配置
// 如果你需要额外的配置,可以额外添加配置。
// 这里提供动态多页应用的流程 具体压缩/优化插件和配置 各位小哥可以去官网查看配置~
// 之后我也会在文章开头的github仓库中提供不同branch去实践最佳js代码压缩优化
const path = require('path')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const FriendlyErrorsWebpackPlugin = require('friendly-errors-webpack-plugin')
const { merge } = require('webpack-merge')
const baseConfig = require('./webpack.base')
const prodConfig = {
  mode: 'production',
  devtool: 'source-map',
  output: {
    filename: 'js/[name].js',
    path: path.resolve(__dirname, '../dist'),
  plugins: [
    new CleanWebpackPlugin(),
    new FriendlyErrorsWebpackPlugin()
module.exports = merge(prodConfig, baseConfig)

scripts/build.js ,这里和 dev 下的思路是一模一样的,公用代码逻辑其实可以拆分~

const inquirer = require('inquirer')
const execa = require('execa')
const { log, separator } = require('./constant')
const { entry } = require('./helper')
const packagesList = [...Object.keys(entry)]
if (!packagesList.length) {
  log('不合法目录,请检查src/packages/*/main.tsx', 'warning')
  return
const allPackagesList = [...packagesList, 'all']
inquirer
  .prompt([
      type: 'checkbox',
      message: '请选择需要打包的项目:',
      name: 'buildLists',
      choices: allPackagesList, // 选项
      validate(value) {
        return !value.length ? new Error('至少选择一个内容进行打包') : true
      filter(value) {
        if (value.includes('all')) {
          return packagesList
        return value
  .then(res => {
    // 拿到所有结果进行打包
    const message = `当前选中Package: ${res.buildLists.join(' , ')}`
    log(message, 'success')
    runParallel(res.buildLists)
function runParallel(packages) {
  const message = `开始打包: ${packages.join('-')}`
  log(message, 'warning')
  build(packages)
async function build(buildLists) {
  const stringLists = buildLists.join(separator)
  await execa('webpack', ['--config', './scripts/webpack.prod.js'], {
    stdio: 'inherit',
    env: {
      packages: stringLists,

这一步其实我们已经完成了动态多页应用的实现了,原理部分之前也已经讲清楚了。

其实核心就是把我环境变量通过 execa inquirer 进行命令行交互动态注入环境变量打包对应选中文件。

如果对 webpack 中环境变量还是不太熟悉的同学可以点击这篇文章, Wepback中环境变量的各种姿势

Eslint & prettier

完成了核心的应用流程打包代码,接下来我们来聊一些轻松的代码检查。

一份良好的工程架构代码规范检查是必不可少的配置。

prettier

yarn add --dev --exact prettier

安装完成之后我们在项目根目录下:

echo {}> .prettierrc.js

我们来个这个 js 内容添加一些基础配置

module.exports = {
  printWidth: 100, // 代码宽度建议不超过100字符
  tabWidth: 2, // tab缩进2个空格
  semi: false, // 末尾分号
  singleQuote: true, // 单引号
  jsxSingleQuote: true, // jsx中使用单引号
  trailingComma: 'es5', // 尾随逗号
  arrowParens: 'avoid', // 箭头函数仅在必要时使用()
  htmlWhitespaceSensitivity: 'css', // html空格敏感度

我们再来添加一份 .prettierignore prettier 忽略检查一些文件:

//.prettierignore 
**/*.min.js
**/*.min.css
.idea/
node_modules/
dist/
build/

同时让我们为我们的代码基于 husky lint-staged 添加 git hook

具体配置可以参照这里 husky&list-staged

安装完成后,在我们每次 commit 时候都会触发 lit-staged 自动修复我们匹配的文件:

因为我们项目中是 ts 文件,所以要稍微修改一下他支持的后缀文件:

// package.json
  "lint-staged": {
    "*.{js,css,md,ts,tsx,jsx}": "prettier --write"

ESlint

Eslint 其实就不用多说了,大名鼎鼎嘛。

yarn add eslint --dev

初始化 eslint

npx eslint --init

eslint 回和我们进行一些列的交互提示,按照提示进行选择我们需要的配置就可以了:

prettier eslint 共同工作时,他们可能会冲突。我们需要安装 yarn add -D eslint-config-prettie 插件并且覆盖 eslint 部分规则。

安装完成之后,我们稍微修改一下 eslint 的配置文件,让冲突时,优先使用 prettier 覆盖 eslint 规则:

// .eslint.js
module.exports = {
    "env": {
        "browser": true,
        "es2021": true
    "extends": [
        "eslint:recommended",
        "plugin:react/recommended",
        "plugin:@typescript-eslint/recommended",
        // 添加`prettier`拓展 用于和`prettier`冲突时覆盖`eslint`规则
        "prettier"
    "parser": "@typescript-eslint/parser",
    "parserOptions": {
        "ecmaFeatures": {
            "jsx": true
        "ecmaVersion": 12,
        "sourceType": "module"
    "plugins": [
        "react",
        "@typescript-eslint"