前端工程化--webpack5+react+ts搭建项目

288 阅读20分钟

参考:juejin.cn/post/711192… juejin.cn/post/723638…

image.png

一、初始化项目

先手动初始化一个react+ts项目,新建文件夹webpack-react-ts,在终端打开该项目,执行:yarn init -y,然后文件夹的根目录会多一个package.json文件,再在项目下新增以下目录结构和文件:

├── config
|   ├── webpack.base.js # 公共配置
|   ├── webpack.dev.js  # 开发环境配置
|   └── webpack.prod.js # 生产环境配置
├── public
│   └── index.html # html模板
├── src
|   ├── App.tsx 
│   └── index.tsx # react应用入口页面
├── tsconfig.json  # ts配置
└── package.json

1、安装依赖、内容初始化

(1)安装webpack、react依赖

// 安装webpack依赖,-D表示依赖装到devDependencies
yarn add webpack webpack-cli -D
// 安装react依赖,dependencies
yarn add react react-dom -S
// 安装react类型依赖,devDependencies
yarn add @types/react @types/react-dom -D

(2)添加public/index.html内容

<!DOCTYPE html>
<html lang="en">
<head>
    <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>
<body>
    <!-- 容器节点 -->
    <div id="root"></div>
</body>
</html>

(3)添加tsconfig.json内容

注意:如果用了webpack使用了alias,那么导致baseUrl不会生效,从而paths也不会生效,所以paths岂不是没用了吗?其实从实际作用来说确实是没用了,不过可以将paths的配置和alias配置成一样的。配置 webpack.common.js 是为了 build 打包时能够识别路径,配置 tsconfig.json 是为了让我们在本地开发调试时 ESLint 不会报错。

{
    "compilerOptions": {
        "sourceMap": true, // 生成一个.map.js的文件,用于其他工具来debugg,类似于webpack的sourceMap
        "allowSyntheticDefaultImports": true, // 用来指定允许从没有默认导出的模块中默认导入。
        // - 为false的时候,引入模块的时候必须以*as的形式,例如引入react`import * as React from 'react'`
        // - 为true的时候,`import React from 'react'`,要注意,他要配合module是esModule的格式或者--esModuleInterop为true的时候,因为react是commonjs写的,并没有default,所以import React from 这种default引入是不对的
        "moduleResolution": "node", // 默认为node10。用于选择模块解析策略,有'node'和'classic'两种类型' ,ts默认用node,即相对的方式导入,node 策略在 typescript 中又称之为node10 的解析策略。
        /** strict checks */
        "noImplicitAny": true,                 /* true或false,如果我们没有为一些值设置明确的类型,编译器会默认认为这个值为any,如果noImplicitAny的值为true的话。则没有明确的类型会报错。默认值为false */
        "strictNullChecks": true,              /* true时,null和undefined值不能赋给非这两种类型的值,别的类型也不能赋给他们,除了any类型。还有个例外就是undefined可以赋值给void类型 */
        "strictFunctionTypes": true,           /* 值为true或false,用于指定是否使用函数参数双向协变检查 */
        "strictBindCallApply": true,           /* 设为true后会对bind、call和apply绑定的方法的参数的检测是严格检测的 */
        /** project options */
        "module": "esnext", // 用来指定要使用生成代码的模块标准: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'.
        "target": "ES5", // 用于指定编译之后的版本目标: 'ES3' , 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'
        "jsx": "react", // 指定jsx代码用于的开发环境: 'preserve', 'react-native', or 'react',指定为react可以使用tsx文件
        "allowJs": false, // 值为true或false,用来指定是否允许编译js文件,默认是false,即不编译js文件
        "experimentalDecorators": true, // 用于指定是否启用实验性的装饰器特性
        "downlevelIteration": true, // 当target为'ES5' or 'ES3'时,为'for-of', spread, and destructuring'中的迭代器提供完全支持
        // "checkJs": true,
        "removeComments": false, // 默认false,用于指定是否将编译后的文件中的注释删掉,设为true的话即删掉注释
        "noEmit": true, // 不生成编译文件,这个一般比较少用
        "skipLibCheck": true, // 跳过检测.d.ts文件
        // "outDir": "./dist/",
        "baseUrl": "./", // 设置解析非相对模块名称的基本目录,相对模块不会受 baseUrl 的影响
        "paths": { // 设置模块名到基于 baseUrl 的路径映射
            "@api": ["src/api"],
            "@api/*": ["src/api/*"],
            "@components": ["src/components"],
            "@components/*": ["src/components/*"],
            "@layouts/*": ["src/layouts/*"],
            "@pages/*": ["src/pages/*"],
            "@router": ["src/router/index.tsx"],
            "@router/*": ["src/router/*"],
            "@style/*": ["src/assets/style/*"],
            "@images/*": ["src/assets/images/*"],
            "@utils/*": ["src/utils/*"],
            "@constants": ["src/constants/index.ts"],
            "@constants/*": ["src/constants/*"],
            "@boundarys/*": ["src/boundarys/*"],
            "@decorators/*": ["src/decorators/*"],
            "@store": ["src/store"],
            "@store/*": ["src/store/*"],
            "@interfaces/*": ["src/interfaces/*"],
            "@types/*": ["src/types/*"],
            "@hooks": ["src/hooks/index.ts"],
            "@hooks/*": ["src/hooks/*"],
        },
    },
    // 指定要包含在编译中的库文件
    "lib": [
        "dom",
        "dom.iterable",
        "esnext"
    ],
    "include": [
        "src/**/*",
        "../../packages/shared/**/*",
    ],
    // 默认值为node_modules 、bower_components、jspm_packages 和编译选项 outDir 指定的路径。
    "exclude": [
        "node_modules",
        "build",
    ]
    //优先级files > exclude > include
    // include, exclude语法支持glob通配符: *匹配0或多个字符(不包括目录分隔符);? 匹配一个任意字符(不包括目录分隔符);*/ 递归匹配任意子目录
}

(4)添加src/App.tsx内容

import React from 'react'

function App() {
  return <h2>webpack-react-ts</h2>
}
export default App

(5)添加src/index.tsx内容

import React from 'react';
import * as ReactDOM from 'react-dom';

import App from './App';

ReactDOM.render(<App />, document.getElementById('root'));

项目业务代码已经初始化好,接下来进行webpack配置。

二、配置基础react+ts环境

2.1 webpack公共配置

1.配置入口、出口文件

const path = require('path');

module.exports = {
    // 入口文件
    entry: path.join(__dirname, '../src/index.tsx'),
    // 打包文件出口
    output: {
        filename: 'static/js/[name].js', // 每个输出js的名称
        path: path.join(__dirname, '../dist'), // 打包结果输出路径
        clean: true, // webpack4需要配置clean-webpack-plugin来删除dist文件,webpack5内置了
        publicPath: '/' // 打包后文件的公共前缀路径
    },
}

2.配置loader解析ts和jsx

webpack默认只能识别js文件,不能识别ts和jsx语法,需要配置loader的预设@babel/preset-typescript来先将ts语法转换成js语法,再通过@babel/preset-react来识别jsx语法。

安装babel核心模块和babel预设

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

webpack中添加babel配置

在webpack.base.js中添加module.rules配置

const path = require('path');

module.exports = {
    // ...
    module: {
        rules: [
            {
                test: /\.(ts|tsx)$/,
                exclude: /node_modules/,
                use: [
                    {
                        loader: 'babel-loader',
                        // 可以直接配置或者提取到babel.config.js文件,会自动读取
                        // options: {
                        //     // 预设执行顺序由右往左,所以先处理ts,再处理jsx
                        //     presets: [
                        //         '@babel/preset-react',
                        //         '@babel/preset-typescript'
                        //     ]
                        // }
                    }
                ]
            }
        ]
    }
}

babel.config.js文件配置

module.exports = (api) => {
    api.cache(true);

    const presets = [
        '@babel/preset-env',
        '@babel/preset-react',
        '@babel/preset-typescript',
    ];
    const plugins = [
        // 根据browserslist浏览器兼容性和代码中使用到的es6新语法进行pollyfill语法转换,转换成es5语法
        ['@babel/plugin-transform-runtime', {
            'corejs': 3,
        }],
    ];

    return {
        presets,
        plugins,
    };
};

babel相关

(1)babel本身配置

babel是一个工具链,帮助开发者将es6语法转换成es5语法,以便能够运行在当前和旧版本的浏览器或其他环境中。但是默认的babel配置只能转换像const, let, class等这些syntax语法。对新的内置函数,如Promise或WeakMap、静态方法(如Array.from或Object.assign)、实例方法(如Array.prototype.includes)和生成器函数(前提是您使用再生器插件)这些语法无法转换,所以需要进行进一步的配置。

Babel大体由两个功能组成:

  1. 语法转换:编译ES6+最新语法(let、class、() => {}等)
  2. polyfill:实现旧版本浏览器不支持的ES6+的API(Promise、Symbol、Array.prototype.includes等)

相关插件:

-   **@babel/core**:@babel/core是babel的**核心库**,所有的核心Api都在这个库里,这些Api**供babel-loader调用**(平常说的`Babel 6``Babel 7`指的就是`@babele/core`的版本)

-   **@babel/cli**`Babel`自带了一个内置的`CLI`命令行工具,可通过命令行编译文件,就是让我们可以在**终端**里使用命令来编译

-   **@babel/preset-env**:这是一个预设的**语法**插件集合,包含了一组相关的插件,Bable中是通过各种插件来指导如何进行代码转换。该插件包含所有es6转化为es5的翻译规则。功能:

    -**只编译`ES6+`语法**(只针对语法阶段的转译,箭头函数,`const/let`等语法。针对一些`API`或者`ES6+的内置模块`是无法进行 `polyfill`的,例如`Promise``Map``Proxy``[].find()`等)
    -   它并不提供`polyfill`,但是可以通过**配置**我们代码运行的**目标环境**,从而控制`polyfill`的导入跟语法编译,使`ES6+`的新特性可以在我们想要的**目标环境**中顺利运行
    -   说明:`Babel`编译`ES6+`**语法**,是通过一个个插件`plugin`去实现的。每年都会有不同新的提案、新的语法,但我们不可能一个个插件去配置,所以就有了`preset`这个东西。因此我们可以理解成`preset`就是一个**语法插件集合包**,这样我们只用安装这一个包,不需要一个个配插件,就可以很方便的编译最新的语法了。
    -`Babel 7`以后,`@babel/preset-env`舍弃了`Stage presets``@babel/preset-stage-x`)这种预设
    -   查看当前`@babel/preset-env`包含了的预设:`@babel/preset-env` --> `package.json` --> `dependencies`里面可以找到

-   @**babel/runtime-corejs3**:引入对应helpers函数,支持了实例方法的兼容(基于2,支持全局变量和静态方法),同时可以避免采用`core-js`引发的全局环境污染,在不污染全局环境的情况下转译API 以及 实例 方法

    -   polyfill相关:转译最新的api以及实例/静态方法,例如 浏览器不支持Promise,那么polyfill会添加window.Promise来支持promise;或者通过向Array、String等其原型链上添加方法来实现。也就是说,`**polyfill`存放了这些ES6+`API`的方法与实现**,所以它可以使得这些不支持的浏览器,支持这些`API`

        相关概念:

        -   最新ES语法:例如箭头函数,`const/let`等等
        -   最新ES API: 例如`Promise`,`Proxy`等等
        -   最新ES实例/静态方法:例如`String.prototype.include()`等等

-   **@babel/plugin-transform-runtime**:一个插件,可以重用`Babel`注入的帮助程序代码以**节省代码大小**(上面的runtime-corejs3提供,当使用了一些`ES6+`的语法糖时,`Babel`会生成一些辅助函数来编译这些语法糖,并以**内联的方式插入**到代码中)。transform-runtime的转换是非侵入性的,也就是它不会污染你的原有的方法。遇到需要转换的方法它会另起一个名字,否则会直接影响使用库的业务代码。

    -   辅助函数会以`require`引用的方式加到我们的代码中
    -   打包后,辅助函数只用了一次,而且不是插入三次,很好的实现了**复用**
    -   打包出来的体积也会缩小,因为不用生成和插入多次辅助函数

-   **@babel/preset-react**:将`jsx`进行转译的作用

-   **@babel/preset-typescript**:编译`ts`代码

**PS:**

1.  最新的babel7已经废弃了旧的@babel/polyfill,改用import "core-js/stable";import "regenerator-runtime/runtime";只需要安装core-js这个包即可,不需要安装@babel/polyfill,当然安装了也没有问题。
2.  @babel/preset-env中useBuiltIns实现的代码polyfill,注入的代码是全局的,比如Promise,是全局变量,这样可能和其他第三方库产生冲突,存在全局变量污染的问题。而@babel/plugin-transform-runtime是将所有的变量以闭包的形式注入,是局部变量,不存在全局变量污染。所以,如果是开发第三发库,使用@babel/plugin-transform-runtime,项目开发可以直接使用@babel/preset-env中useBuiltIns配置

实际配置支持ES6语法+API+静态/实例方法可以配置:`@babel/preset-env` + `@babel/runtime-corejs3`

配置文件:babel.config.js,使用`js`作为配置文件,是因为可以访问到`process.env.NODE_ENV`环境变量来区分是开发还是打包模式

```
module.exports = (api) => {
    api.cache(true);

    const presets = [
        '@babel/preset-env',
        '@babel/preset-react',
        '@babel/preset-typescript',
    ];
    const plugins = [
        // 根据browserslist浏览器兼容性和代码中使用到的es6新语法进行pollyfill语法转换,转换成es5语法
        ['@babel/plugin-transform-runtime', {
						// 对应依赖:@babel/runtime-corejs3
            'corejs': 3, // 以不污染全局局部变量方式polyfill(垫平)
        }],
    ];

    return {
        presets,
        plugins,
    };
};
```

(2)webpack配置babel

-   **babel-loader**:它作为一个中间桥梁,通过调用babel/core中的api来告诉webpack要如何处理js

-   热更新:开发模式下修改`css`和`less`文件,页面样式可以在**不刷新浏览器**的情况实时生效,因为此时样式都在`style`标签里面,`style-loader`做了替换样式的热替换功能。但是修改`App.tsx`,浏览器会自动刷新后再显示修改后的内容,但我们想要的不是刷新浏览器,而是在不需要刷新浏览器的前提下模块热更新,并且能够保留`react`组件的状态,需要使用@pmmmwh/react-refresh-webpack-plugin插件,该插件依赖于react-refresh。

```
const ReactRefreshPlugin = require('@pmmmwh/react-refresh-webpack-plugin');

module: {
        rules: [
            {
	            test: /\.(ts|tsx)$/,
	            exclude: /node_modules/,
	            use: [
	                {
	                    loader: 'babel-loader',
	                    options: {
                            // 开发环境时热更新
	                        plugins: [
                                process.env.NODE_ENV === 'development' && require.resolve('react-refresh/babel')
	                    },
	                },
	            ],
	        },
		...
        ],
},
plugins: [isDevelopment && new ReactRefreshWebpackPlugin()].filter(Boolean),
```

(3)eslint配置babel

-   eslint-babel:ESLint的默认解析器和核心规则只支持最新的最终ECMAScript标准,不支持Babel提供的实验性(如新特性)和non-standard(如流或TypeScript类型)语法。babel-eslint是一个解析器,它允许ESLint在Babel转换的源代码上运行。

3、配置extensions

extensionswebpackresolve解析配置下的选项,在引入模块时不带文件后缀时,会来该配置数组里面依次添加后缀查找文件,因为ts不支持引入以 .ts, tsx为后缀的文件,所以要在extensions中配置,而第三方库里面很多引入js文件没有带后缀,所以也要配置下js

修改webpack.base.js,注意把高频出现的文件后缀放在前面,参考的文章说:这里只配置jstsxts,其他文件引入都要求带后缀,可以提升构建速度?(待验证)

// webpack.base.js
module.exports = {
  // ...
  resolve: {
      extensions: ['.tsx', '.ts', '.js', '.jsx', '.less'],
  }
}

4、添加html-webpack-plugin插件

webpack需要把最终构建好的静态资源都引入到一个html文件中,这样才能在浏览器中运行,html-webpack-plugin就是来做这件事情的,安装依赖yarn add html-webpack-plugin -D,因为该插件在开发和构建打包模式都会用到,所以还是放在公共配置webpack.base.js里面:

// webpack.base.js
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
  // ...
  plugins: [
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, '../public/index.html'), // 模板取定义root节点的模板
      inject: true, // 自动注入静态资源
    })
  ]
}

到这里一个最基础的react基本公共配置就已经配置好了,需要在此基础上分别配置开发环境和打包环境了。

2.2 webpack开发环境配置

1. 安装webpack-dev-server

开发环境配置代码在webpack.dev.js中,需要借助 webpack-dev-server在开发环境启动服务器来辅助开发,还需要依赖webpack-merge来合并基本配置,安装依赖:yarn add webpack-dev-server webpack-merge -D 修改webpack.dev.js代码,合并公共配置,并添加开发模式配置

// webpack.dev.js
const path = require('path')
// webpack-merge将配置合并导出
const { merge } = require('webpack-merge')
const baseConfig = require('./webpack.base.js')

// 合并公共配置,并添加开发环境配置
module.exports = merge(baseConfig, {
    mode: 'development', // 开发模式,打包更加快速,省了代码优化步骤
    // dev环境开启缓存,来缓存生成的 webpack 模块和 chunk,改善下一次打包的构建速度,可提速 90% 左右,加速项目启动
    cache: {
        type: 'filesystem',
    },
    devtool: 'eval-cheap-module-source-map', // 配置source-map,方便源代码调试
    devServer: {
        port: 3000, // 服务端口号
        compress: false, // gzip压缩,开发环境不开启,提升热更新速度
        hot: true, // 开启热更新,后面会讲react模块热替换具体配置
        historyApiFallback: true, // 解决history路由404问题
        static: {
            directory: path.join(__dirname, "../public"), //托管静态资源public文件夹
        }
    }
});

2. package.json添加dev脚本

package.jsonscripts中添加

// package.json
"scripts": {
  "dev": "webpack-dev-server -c config/webpack.dev.js"
},

执行yarn dev,就能看到项目已经启动起来了,访问http://localhost:3000/,就可以看到项目界面,具体完善的react模块热替换在下面会讲到。

2.3 webpack生产环境配置

1. 修改webpack.prod.js代码

const { merge } = require('webpack-merge')
const baseConfig = require('./webpack.base.js')
module.exports = merge(baseConfig, {
  mode: 'production', // 生产模式,会开启tree-shaking和压缩代码,以及其他优化
})

2. package.json添加build打包命令脚本

    "build": "webpack -c config/webpack.prod.js"

执行yarn build,最终打包在dist文件中,打包结果:

dist                    
├── static
|   ├── js
|     ├── main.js
├── index.html

打包后的dist文件可以在本地借助node服务器serve打开,全局安装serveyarn add serve -g,然后在项目根目录命令行执行serve -s dist,就可以启动打包后的项目了。

到现在一个基础的支持reacttswebpack5就配置好了,但只有这些功能是远远不够的,还需要继续添加其他配置。

三、基础功能配置

1. 配置环境变量

环境变量按作用来分分两种:

  1. 区分是开发模式还是打包构建模式
  2. 区分项目业务环境,开发/测试/预测/正式环境

区分开发模式还是打包构建模式可以用process.env.NODE_ENV,因为很多第三方包里面判断都是采用的这个环境变量。

区分项目接口环境可以自定义一个环境变量process.env.BASE_ENV,设置环境变量可以借助cross-envwebpack.DefinePlugin来设置。

  • cross-env:兼容各系统的设置环境变量的包
  • webpack.DefinePluginwebpack内置的插件,可以为业务代码注入环境变量

需要把process.env.BASE_ENV注入到业务代码里面,就可以通过该环境变量设置对应环境的接口地址和其他数据,要借助webpack.DefinePlugin插件。

// webpack.base.js
// ...
const webpack = require('webpack')
module.export = {
  // ...
  plugins: [
    // ...
    new webpack.DefinePlugin({
      'process.env.BASE_ENV': JSON.stringify(process.env.BASE_ENV)
    })
  ]
}

配置后会把值注入到业务代码里面去,webpack解析代码匹配到process.env.BASE_ENV,就会设置到对应的值。测试一下,在src/index.tsx打印一下两个环境变量

// src/index.tsx 
// ... 
console.log('NODE_ENV', process.env.NODE_ENV) 
console.log('BASE_ENV', process.env.BASE_ENV)

2. 处理css、less文件

在前端开发项目的时候另外一个重要的配置就是cssless文件处理,webpack默认只认识js,是不识别css文件的,所以需要安装依赖来处理cssless文件,不然当入口文件index.jsx引入css文件时,运行yarn dev控制台就会报错,因此需要使用loader来解析css

(1)css文件

在src下新增app.css

h2 {
    color: red;
    transform: translateY(100px);
}

src/App.tsx中引入app.css

import React from 'react'
import './app.css'

function App() {
  return <h2>webpack5-rea11ct-ts</h2>
}
export default App

屏幕快照 2023-06-26 下午1.49.35.png 安装依赖yarn add style-loader css-loader -D

  • style-loader: 把解析后的css代码从js中抽离,放到头部的style标签中(在运行时做的)
  • css-loader:  解析css文件代码

因为解析css的配置开发和打包环境都会用到,所以加在公共配置webpack.base.js中:

// webpack.base.js
// ...
module.exports = {
 // ...
 module: { 
   rules: [
     // ...
     {
        test: /.css$/, //匹配 css 文件
        use: ['style-loader','css-loader']
     }
   ]
 },
 // ...
}

loader执行顺序是从右往左,从下往上的,匹配到css文件后先用css-loader解析css, 最后借助style-loadercss插入到头部style标签中。

配置完成后再yarn dev打包启动后在浏览器查看,这时可以看到控制台不报错了,并且样式生效了。

(3)less文件

项目开发中,为了更好的提升开发体验,一般会使用css超集less或者scss,对于这些超集也需要对应的loader来识别解析,我们以less为例,首先安装解析less的相关依赖yarn add less less-loader -D

  • less-loader: 解析less文件代码,把less编译为css
  • lessless核心依赖

实现支持less也很简单,只需要在rules中添加less文件解析,遇到less文件,使用less-loader解析为css,再进行css解析流程,修改webpack.base.js(ps: 如果是scss的话使用sass-loader):

// webpack.base.js
module.exports = {
  // ...
  module: {
    // ...
    rules: [
      // ...
      {
        test: /.(css|less)$/, //匹配 css和less 文件
        use: ['style-loader','css-loader', 'less-loader']
      }
    ]
  },
  // ...
}

测试下,新增src/app.less

#root {
  h2 {
    font-size: 20px;
  }
}

App.tsx中引入app.less,执行yarn start:dev启动之后,可以看到less文件编写的样式编译css后也插入到style标签了。

(3)处理css3前缀兼容

虽然css3现在浏览器支持率已经很高了, 但有时候需要兼容一些低版本浏览器,需要给css3加前缀,可以借助插件来自动加前缀, postcss-loader就是来给css3加浏览器前缀的,安装依赖yarn add postcss-loader autoprefixer -D

  • postcss-loader:处理css时自动加前缀
  • autoprefixer:决定添加哪些浏览器前缀到css

修改webpack.base.js, 在解析cssless的规则中添加配置:

module.exports = {
  // ...
  module: { 
    rules: [
      // ...
      {
        test: /.(css|less)$/, //匹配 css和less 文件
        use: [
          'style-loader',
          'css-loader',
          // 新增
          {
            loader: 'postcss-loader',
            options: {
              postcssOptions: {
                plugins: ['autoprefixer']
              }
            }
          },
          'less-loader'
        ]
      }
    ]
  },
  // ...
}

上面可以看到解析cssless有很多重复配置,可以进行提取postcss-loader配置优化一下

postcss.config.jspostcss-loader的配置文件,会自动读取配置,根目录新建postcss.config.js:

module.exports = {
  plugins: ['autoprefixer']
}

修改webpack.base.js, 取消postcss-loaderoptions配置:

// webpack.base.js
// ...
module.exports = {
  // ...
  module: { 
    rules: [
      // ...
      {
        test: /.(css|less)$/, //匹配 css和less 文件
        use: [
          'style-loader',
          'css-loader',
          'postcss-loader',
          'less-loader'
        ]
      },
    ]
  },
  // ...
}

配置完成后,需要有一份兼容浏览器的清单,让postcss-loader知道哪些浏览器的前缀,在根目录创建.browserslistrc文件或者在package.json文件中添加:

"browserslist": [
      ">0.2%",
      "not dead",
      "not op_mini all",
      "IE>=9" # 兼容ie9
 ]

执行yarn dev,可以看到打包后的css文件已经加上ie和谷歌内核的前缀了:

屏幕快照 2023-06-26 下午2.47.16.png

3. 处理js的语法兼容

已在前面的babel配置

4. 处理图片文件

对于图片文件,webpack4使用file-loaderurl-loader来处理的,但webpack5不使用这两个loader了,而是采用自带的asset-module来处理

修改webpack.base.js,添加图片解析配置

module.exports = {
    module: {
        rules: [
            //...
            {
                // 处理图片
                test: /\.(jpg|jpeg|png|gif)$/,
                type: 'asset',
                parser: {
                    dataUrlCondition: {
                        maxSize: 4 * 1024, // 小于4kb转base64位
                    },
                },
                generator: {
                    filename: 'static/images/[name]_[contenthash:8].[ext]', // // 文件输出目录和命名
            },
        },
        ]
    }
}

测试一下,分别准备两张小于、大于4kb的图片,放在src/assets/imgs目录下,修改App.tsx:

import React from 'react';
import smallImg from './assets/imgs/smallImg.png'
import bigImg from './assets/imgs/bigImg.png'
import './app.css';
import './app.less';

function App() {
  return (
    <>
      <h2>webpack-react-ts</h2>
      <img src={smallImg} alt="小于4kb的图片" />
      <img src={bigImg} alt="大于于4kb的图片" />
    </>
  )
}
export default App

这个时候在引入图片的地方会报:找不到模块“./assets/imgs/bigImg.png”或其相应的类型声明,需要添加一个图片的声明文件: 新增src/typings/extensions.d.ts文件,添加内容:

// 引入图片文件
declare module '*.jpg';
declare module '*.jpeg';
declare module '*.png';
declare module '*.webp';
declare module '*.gif';
declare module '*.svg';

然后就可以正常引入图片了,yarn dev后在浏览器可以看到,小于4kb的图片被转成了base64位格式的(css中的背景图片background: urlxxx也可以解析):

屏幕快照 2023-06-26 下午3.07.41.png

5. 处理字体和媒体文件

字体文件和媒体文件这两种资源处理方式和处理图片是一样的,只需要把匹配的路径和打包后放置的路径修改一下就可以了。修改webpack.base.js文件:

// webpack.base.js
module.exports = {
  module: {
    rules: [
      // ...
      {
        test:/.(woff2?|eot|ttf|otf)$/, // 匹配字体图标文件
        type: "asset", // type选择asset
        parser: {
          dataUrlCondition: {
            maxSize: 10 * 1024, // 小于10kb转base64位
          }
        },
        generator:{ 
          filename:'static/fonts/[name][ext]', // 文件输出目录和命名
        },
      },
      {
        test:/.(mp4|webm|ogg|mp3|wav|flac|aac)$/, // 匹配媒体文件
        type: "asset", // type选择asset
        parser: {
          dataUrlCondition: {
            maxSize: 10 * 1024, // 小于10kb转base64位
          }
        },
        generator:{ 
          filename:'static/media/[name][ext]', // 文件输出目录和命名
        },
      },
    ]
  }
}

四、配置react模块热更新

热更新上面已经在devServer中配置hottrue, 在webpack4中,还需要在插件中添加了HotModuleReplacementPlugin,在webpack5中,只要devServer.hottrue了,该插件就已经内置了。

现在开发模式下修改cssless文件,页面样式可以在不刷新浏览器的情况实时生效,因为此时样式都在style标签里面,style-loader做了替换样式的热替换功能。但是修改App.tsx,浏览器会自动刷新后再显示修改后的内容,但我们想要的不是刷新浏览器,而是在不需要刷新浏览器的前提下模块热更新,并且能够保留react组件的状态。

可以借助@pmmmwh/react-refresh-webpack-plugin插件来实现,该插件又依赖于react-refresh, 安装依赖:

yarn add @pmmmwh/react-refresh-webpack-plugin react-refresh -D

配置react热更新插件,修改webpack.dev.js

// webpack.dev.js
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');

module.exports = merge(baseConfig, {
  // ...
  plugins: [
    new ReactRefreshWebpackPlugin(), // 添加热更新插件
  ]
})

babel-loader配置react-refesh刷新插件,修改babel.config.js文件

const isDEV = process.env.NODE_ENV === 'development' // 是否是开发模式
module.exports = {
  // ...
  "plugins": [
    isDEV && require.resolve('react-refresh/babel'), // 如果是开发模式,就启动react热更新插件
    // ...
  ].filter(Boolean) // 过滤空值
}

测试一下,修改App.tsx代码

import React, { useState } from 'react'
import './App.less'

function App() {
  const [ count, setCounts ] = useState('')
  const onChange = (e: any) => {
    setCounts(e.target.value)
  }
  return (
    <>
      <h2>webpack5+react+ts</h2>
      <p>受控组件</p>
      <input type="text" value={count} onChange={onChange} />
      <br />
      <p>非受控组件</p>
      <input type="text" />
    </>
  )
}
export default App

在两个输入框分别输入内容后,修改App.tsxh2标签的文本,会发现在不刷新浏览器的情况下,页面内容进行了热更新,并且react组件状态也会保留。修改less样式也会热更新。

注意:新增或者删除页面hooks时,热更新时组件状态不会保留。

image.png

屏幕快照 2023-06-26 下午3.24.26.png

五、优化构建速度

1. 配置alias别名

webpack支持设置别名alias,设置别名可以让后续引用的地方减少路径的复杂度。

修改webpack.base.js注意:修改tsconfig.json,中也要同样配置baseUrlpaths

module.export = {
  // ...
   resolve: {
    // ...
    alias: {
      '@': path.join(__dirname, '../src')
    }
  }
}

配置修改完成后,在项目中使用@/xxx.xx,就会指向项目中src.xxx.xx,在js/ts文件和css文件中都可以用。 src/App.tsx可以修改为:

import React from 'react'
import smallImg from '@/assets/imgs/5kb.png'
import bigImg from '@/assets/imgs/22kb.png'
import '@/app.css'
import '@/app.less'

function App() {
  return (
    <>
      <img src={smallImg} alt="小于10kb的图片" />
      <img src={bigImg} alt="大于于10kb的图片" />
      <div className='smallImg'></div> {/* 小图片背景容器 */}
      <div className='bigImg'></div> {/* 大图片背景容器 */}
    </>
  )
}
export default App

2. 缩小loader作用范围

一般第三库都是已经处理好的,不需要再次使用loader去解析,可以按照实际情况合理配置loader的作用范围,来减少不必要的loader解析,节省时间,通过使用 includeexclude 两个配置项,可以实现这个功能,常见的例如:

  • include:只解析该选项配置的模块
  • exclude:不解该选项配置的模块,优先级更高

修改webpack.base.js

{
                test: /\.(ts|tsx)$/,
                exclude: /node_modules/,
                use: [
                    {
                        loader: 'babel-loader',
                        options: {
                            plugins: [require.resolve('react-refresh/babel')].filter(Boolean),
                        },
                    }
                ]
            },

3. devtool 配置

开发过程中或者打包后的代码都是webpack处理后的代码,如果进行调试肯定希望看到源代码,而不是编译后的代码, source map就是用来做源码映射的,不同的映射模式会明显影响到构建和重新构建的速度, devtool选项就是webpack提供的选择源码映射方式的配置。

devtool的命名规则为 ^(inline-|hidden-|eval-)?(nosources-)?(cheap-(module-)?)?source-map$

关键字描述
inline代码内通过 dataUrl 形式引入 SourceMap
hidden生成 SourceMap 文件,但不使用
evaleval(...) 形式执行代码,通过 dataUrl 形式引入 SourceMap
nosources不生成 SourceMap
cheap只需要定位到行信息,不需要列信息
module展示源代码中的错误位置

开发环境推荐:eval-cheap-module-source-map

  • 本地开发首次打包慢点没关系,因为 eval 缓存的原因, 热更新会很快
  • 开发中,我们每行代码不会写的太长,只需要定位到行就行,所以加上 cheap
  • 我们希望能够找到源代码的错误,而不是打包后的,所以需要加上 module

修改webpack.dev.js

// webpack.dev.js
module.exports = {
  // ...
  devtool: 'eval-cheap-module-source-map'
}

打包环境推荐:none(就是不配置devtool选项了,不是配置devtool: 'none'),

// webpack.prod.js
module.exports = {
  // ...
  // devtool: '', // 不用配置devtool此项
}

  • none话调试只能看到编译后的代码,也不会泄露源代码,打包速度也会比较快。
  • 只是不方便线上排查问题, 但一般都可以根据报错信息在本地环境很快找出问题所在。

4. 其他优化配置

除了上面的配置外,webpack还提供了其他的一些优化方式,本次搭建没有使用到,所以只简单罗列下

  • externals: 外包拓展,打包时会忽略配置的依赖,会从上下文中寻找对应变量
  • module.noParse: 匹配到设置的模块,将不进行依赖解析,适合jquery,boostrap这类不依赖外部模块的包
  • ignorePlugin: 可以使用正则忽略一部分文件,常在使用多语言的包时可以把非中文语言包过滤掉

六、优化构建结果文件

1. webpack包分析工具

webpack-bundle-analyzer是分析webpack打包后文件的插件,使用交互式可缩放树形图可视化 webpack 输出文件的大小。通过该插件可以对打包后的文件进行观察和分析,可以方便我们对不完美的地方针对性的优化,安装依赖

yarn add webpack-bundle-analyzer -D

修改webpack.analyze.js

const { merge } = require('webpack-merge');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const productionWebpackConfig = require('./webpack.prod');

const analyzaWebpackConfig = merge(productionWebpackConfig, {
    plugins: [
        new BundleAnalyzerPlugin() // 配置分析打包结果插件
      ]
})

module.exports = analyzaWebpackConfig;

配置好后,执行yarn analyze命令,打包完成后浏览器会自动打开窗口,看到打包文件的分析结果页面,可以看到各个文件所占的资源大小

屏幕快照 2023-06-26 下午3.47.59.png

2. 抽取css样式文件

在开发环境我们希望css嵌入在style标签里面,方便样式热替换,但打包时我们希望把css单独抽离出来,方便配置缓存策略。而插件mini-css-extract-plugin就是来帮我们做这件事的,安装依赖yarn add mini-css-extract-plugin -D,修改webpack.base.js, 根据环境变量设置开发环境使用style-looader,打包模式抽离css

// webpack.base.js
// ...
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const isDev = process.env.NODE_ENV === 'development' // 是否是开发模式
module.exports = {
  // ...
  module: { 
    rules: [
      // ...
      {
        test: /.css$/, //匹配所有的 css 文件
        include: [path.resolve(__dirname, '../src')],
        use: [
          isDev ? 'style-loader' : MiniCssExtractPlugin.loader, // 开发环境使用style-looader,打包模式抽离css
          'css-loader',
          'postcss-loader'
        ]
      },
      {
        test: /.less$/, //匹配所有的 less 文件
        include: [path.resolve(__dirname, '../src')],
        use: [
          isDev ? 'style-loader' : MiniCssExtractPlugin.loader, // 开发环境使用style-looader,打包模式抽离css
          'css-loader',
          'postcss-loader',
          'less-loader'
        ]
      },
    ]
  },
  // ...
}

再修改webpack.prod.js, 打包时添加抽离css插件

// webpack.prod.js
// ...
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
module.exports = merge(baseConfig, {
  mode: 'production',
  plugins: [
    // ...
    // 抽离css插件
    new MiniCssExtractPlugin({
      filename: 'static/css/[name].css' // 抽离css的输出目录和名称
    }),
  ]
})

配置完成后,在开发模式css会嵌入到style标签里面,方便样式热替换,打包时会把css抽离成单独的css文件。

3. 压缩css文件

上面配置了打包时把css抽离为单独css文件的配置,打开打包后的文件查看,可以看到默认css是没有压缩的,需要手动配置一下压缩css的插件。 可以借助css-minimizer-webpack-plugin来压缩css,安装依赖:yarn add css-minimizer-webpack-plugin -D。修改webpack.prod.js文件, 需要在优化项optimization下的minimizer属性中配置:

// webpack.prod.js
// ...
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')
module.exports = {
  // ...
  optimization: {
    minimizer: [
      new CssMinimizerPlugin(), // 压缩css
    ],
  },
}

再次执行打包就可以看到css已经被压缩了。

4. 压缩js文件

设置modeproduction时,webpack会使用内置插件terser-webpack-plugin压缩js文件,该插件默认支持多线程压缩,但是上面配置optimization.minimizer压缩css后,js压缩就失效了,需要手动再添加一下,webpack内部安装了该插件,由于pnpm解决了幽灵依赖问题,如果用的pnpm的话,需要手动再安装一下依赖yarn add terser-webpack-plugin -D 修改webpack.prod.js文件:

// ...
const TerserPlugin = require('terser-webpack-plugin')
module.exports = {
  // ...
  optimization: {
    minimizer: [
      // ...
      new TerserPlugin({ // 压缩js
        parallel: true, // 开启多线程压缩
        terserOptions: {
          compress: {
            pure_funcs: ["console.log"] // 删除console.log
          }
        }
      }),
    ],
  },
}

配置完成后再打包,cssjs就都可以被压缩了。

5. 合理配置打包文件hash

项目维护的时候,一般只会修改一部分代码,可以合理配置文件缓存,来提升前端加载页面速度和减少服务器压力,而hash就是浏览器缓存策略很重要的一部分。webpack打包的hash分三种:

  • hash:跟整个项目的构建相关,只要项目里有文件更改,整个项目构建的hash值都会更改,并且全部文件都共用相同的hash
  • chunkhash:不同的入口文件进行依赖文件解析、构建对应的chunk,生成对应的哈希值,文件本身修改或者依赖文件修改,chunkhash值会变化
  • contenthash:每个文件自己单独的 hash 值,文件的改动只会影响自身的 hash

hash是在输出文件时配置的,格式是filename: "[name].[chunkhash:8][ext]" , [xx] 格式是webpack提供的占位符, :8是生成hash的长度。

占位符解释
ext文件后缀名
name文件名
path文件相对路径
folder文件所在文件夹
hash每次构建生成的唯一 hash 值
chunkhash根据 chunk 生成 hash 值
contenthash根据文件内容生成hash 值

因为js我们在生产环境里会把一些公共库和程序入口文件区分开,单独打包构建,采用chunkhash的方式生成哈希值,那么只要我们不改动公共库的代码,就可以保证其哈希值不会受影响,可以继续使用浏览器缓存,所以js适合使用chunkhash

css和图片资源媒体资源一般都是单独存在的,可以采用contenthash,只有文件本身变化后会生成新hash值。

修改webpack.base.js,把js输出的文件名称格式加上chunkhash,把css和图片媒体资源输出格式加上contenthash

// webpack.base.js
// ...
module.exports = {
  // 打包文件出口
  output: {
    filename: 'static/js/[name].[chunkhash:8].js', // // 加上[chunkhash:8]
    // ...
  },
  module: {
    rules: [
      {
        test:/.(png|jpg|jpeg|gif|svg)$/, // 匹配图片文件
        // ...
        generator:{ 
          filename:'static/images/[name].[contenthash:8][ext]' // 加上[contenthash:8]
        },
      },
      {
        test:/.(woff2?|eot|ttf|otf)$/, // 匹配字体文件
        // ...
        generator:{ 
          filename:'static/fonts/[name].[contenthash:8][ext]', // 加上[contenthash:8]
        },
      },
      {
        test:/.(mp4|webm|ogg|mp3|wav|flac|aac)$/, // 匹配媒体文件
        // ...
        generator:{ 
          filename:'static/media/[name].[contenthash:8][ext]', // 加上[contenthash:8]
        },
      },
    ]
  },
  // ...
}

再修改webpack.prod.js,修改抽离css文件名称格式

// webpack.prod.js
// ...
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
module.exports = merge(baseConfig, {
  mode: 'production',
  plugins: [
    // 抽离css插件
    new MiniCssExtractPlugin({
      filename: 'static/css/[name].[contenthash:8].css' // 加上[contenthash:8]
    }),
    // ...
  ],
  // ...
})

再次打包就可以看到文件后面的hash

6. 代码分割第三方包和公共模块

7. tree-shaking清理未引用js

8. tree-shaking清理未引用css

9. 资源懒加载

10. 资源预价值

七、项目规范(EditorConfig + Prettier + ESLint)

1. eslint

背景

TypeScript 的代码检查工具主要有 TSLint 和 ESLint 两种。早期的 TypeScript 项目一般采用 TSLint 进行检查。TSLint 和 TypeScript 采用同样的 AST 格式进行编译,但主要问题是对于 JavaScript 生态的项目支持不够友好,因此 TypeScript 团队在 2019 年宣布全面转向 ESLint(具体可查看 TypeScript 官方仓库的 .eslintrc.json 配置),TypeScript 和 ESLint 使用不同的 AST 进行解析,因此为了在 ESLint 中支持 TypeScript 代码检查需要制作额外的自定义解析器(Custom Parsers,ESLint 的自定义解析器功能需要基于 ESTree),目的是为了能够解析 TypeScript 语法并转成与 ESLint 兼容的 AST。

ESLint配置

从背景介绍可以知道,对于全新的ts项目直接抛弃TSLint而使用ESLint,需要饱含解析AST的解析器@typescript-eslint/parse和使用校验规则的插件@typescript-eslint/eslint-plugin,安装依赖:

yarn add eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin -D

在根目录新建.eslintrc.js配置文件:

const eslintrc = {
    root: true,
    // 在 ESLint 中使用[共享规则配置](https://link.juejin.cn?target=https%3A%2F%2Fcn.eslint.org%2Fdocs%2Fdeveloper-guide%2Fshareable-configs "https://cn.eslint.org/docs/developer-guide/shareable-configs"),其中 `eslint:recommended` 是 ESLint 内置的推荐校验规则配置(也被称作最佳规则实践),`plugin:@typescript-eslint/recommended` 是类似于 `eslint:recommended` 的 TypeScript 推荐校验规则配置。
    extends: [
        'eslint:recommended',
    ],
    env: {
        browser: true,
        node: true,
        jest: true,
        es6: true,
        commonjs: true,
    },
    parser: 'babel-eslint',
    parserOptions: {
        ecmaVersion: 6,
        ecmaFeatures: {
            experimentalObjectRestSpread: true,
        },
    },
    // // 不同格式的文件指定自定义语法
    overrides: [{
        files: ['**/*.ts', '**/*.tsx'],
        env: {
            browser: true,
            node: true,
            jest: true,
            es6: true,
            commonjs: true,
        },
        extends: [
            'eslint:recommended',
            'plugin:react/recommended',
            'plugin:@typescript-eslint/recommended',
        ],
        globals: {
            Atomics: 'readonly',
            SharedArrayBuffer: 'readonly',
        },
        parser: '@typescript-eslint/parser',
        parserOptions: {
            ecmaFeatures: {
                jsx: true,
            },
            ecmaVersion: 6,
        },
        plugins: [
            '@typescript-eslint',
            'react-hooks',
            'import'
        ],
        rules: {
            'react-hooks/rules-of-hooks': 'error', // Checks rules of Hooks
            'react-hooks/exhaustive-deps': 'warn', // Checks effect dependencies
            'object-curly-spacing': [
                'error',
                'always',
                { 'objectsInObjects': false },
            ],
            'react/prop-types': 0,
            'react/self-closing-comp': ['error', {
                'component': true,
            }],
            semi: 2, // 强制句尾使用分号
            'no-nested-ternary': 0, // 嵌套三元符
            // 'no-unused-vars': 1, // 未定义的变量警告
            'arrow-body-style': 0,
            'no-console': 1, // 代码中无console
            eqeqeq: 2, // 必须使用===
            'no-plusplus': 0, // 允许使用++或--
            'max-len': [2, { // 最大行长度
                code: 400,
            }],
            'no-underscore-dangle': 0, // 允许悬挂下划线在标识符的开头或末尾
            'no-unused-expressions': [2, { // 消除对程序状态没有影响的未使用的表达式
                allowShortCircuit: true, // 允许在表达式中使用短路评估
                allowTernary: true, // 允许在表达式中使用三元运算符
            }],
            'no-param-reassign': [1, { // 防止由功能参数的修改或重新分配引起的意外行为
                props: true, // 不允许赋值给函数参数
            }],
            'array-bracket-spacing': 2, // 在数组括号内强制实现一致的间距
            'block-spacing': 2, // 代码块间距
            'brace-style': 2, // 花括号相对于其控制语句和正文的位置
            camelcase: 0, // 驼峰命名
            'comma-spacing': 2, // 在变量声明,数组文字,对象文字,函数参数和序列中的逗号前后加上一致的间距
            'comma-style': 2, // ,在当前行结尾
            'func-names': 2, // 强制使用命名函数表达式(禁止使用匿名函数)
            indent: [2, 4, { // 强制执行一致的缩进样式(4格缩进)
                SwitchCase: 1, // 强制在switch声明中case的缩进级别
            }],
            'line-comment-position': 0, // 行注释位置可上可代码行结束位置
            'lines-between-class-members': 2, // 强制在类成员之间填充空行
            'multiline-comment-style': 0, // 不允许连续的行注释支持块注释。此外,要求块注释*在每行之前有一个对齐的字符
            'no-mixed-operators': 1, // 强制在连续使用不同运算符的表达式中使用用括号括起来以明确开发人员的意图
            'no-multiple-empty-lines': 2, // 不允许多行空行
            quotes: [2, 'single', { // 强制使用‘’,
                avoidEscape: true, // 允许转义字符串使用单引号或双引号
                allowTemplateLiterals: true, // 允许变量字符串使用反引号
            }],
            'spaced-comment': 2, // 强制注释符号与文案间距的一致性
            'prefer-destructuring': 0, // 优先使用数组和对象解构
            'react/static-property-placement': 0,
            '@typescript-eslint/no-this-alias': 0,
            '@typescript-eslint/no-explicit-any': 0,
            'import/extensions': 0,
            'import/order': [
                'warn',
                {
                    "newlines-between": "always",
                    pathGroups: [
                        {
                            pattern: '@**/**',
                            group: 'external',
                            position: 'after',
                        },
                    ],
                    pathGroupsExcludedImportTypes: ['builtin', 'type'],
                    'groups': [
                        'builtin',
                        'external',
                        "internal",
                        "parent",
                        "sibling",
                        'index',
                        'object',
                        'type',
                    ]
                }
            ],
            'no-shadow': 'off',
            '@typescript-eslint/no-shadow': 1,
            'no-use-before-define': 'off',
            '@typescript-eslint/no-use-before-define': ['error'],
            '@typescript-eslint/no-unused-vars': [1, {
                'ignoreRestSiblings': true,
            }],
            '@typescript-eslint/explicit-module-boundary-types': [
                'error',
                {
                    'allowedNames': [
                        'render',
                        'shouldComponentUpdate',
                        'getSnapshotBeforeUpdate',
                        'componentDidMount',
                        'componentDidCatch',
                        'componentDidUpdate',
                        'componentWillUnmount',
                        'componentWillMount',
                        'componentWillReceiveProps',
                    ],
                },
            ],
        },
    }],
};

if (process.env.NODE_ENV === 'dev') {
    Object.assign(eslintrc.rules, {
        'no-console': 0,
        'no-debugger': 0,
    });
}

module.exports = eslintrc;

要在vscode中安装ESLint插件,可以实时提示ts错误信息。 当然为了防止不需要被校验的文件出现校验信息,可以通过 .eslintignore 文件进行配置(例如以下都是一些不需要格式校验的配置文件)

dist/
node_modules/
public/
mock-server/
serviceWorker.js
scripts/

2. Prettier

Prettier 是非常强大的代码格式化工具,它支持着 JavaScript、TypeScript、CSS、SCSS、Less、JSX、Angular、Vue、JSON、Markdown 等各种语言,我们前端基本上能用到的文件格式它都可以搞定,所以我们这里采用它来约束我们的代码风格规范,统一代码风格。

vs linters

ESLint已经能够规范我们的代码风格,为什么还需要Prettier?区别在于,Linters有两种类型的规则:

ESLint 的规则校验同时包含了 格式规则质量规则,但是大部分情况下只有 格式规则 可以通过 --fix 或 VS Code 插件的 Sava Auto Fix 功能一键修复,而 质量规则 更多的是发现代码可能出现的 Bug 从而防止代码出错,这类规则往往需要手动修复。因此 格式规则 并不是必须的,而 质量规则 则是必须的。Prettier 与 ESLint 的区别在于 Prettier 专注于统一的格式规则,从而减轻 ESLint 在格式规则上的校验,而对于质量规则 则交给专业的 ESLint 进行处理。总结一句话就是:Prettier for formatting and linters for catching bugs!(ESLint 是必须的,Prettier 是可选的!)

需要注意如果 ESLint(TSLint) 和 Prettier 配合使用时格式规则有重复且产生了冲突,那么在编辑器中使用 Sava Auto Fix 时会让你的一键格式化哭笑不得。此时应该让两者把各自注重的规则功能区分开,使用 ESLint 校验质量规则,使用 Prettier 校验格式规则解决eslint和prettier冲突

3. stylelint

检测 css 样式代码质量,其实很多项目都是不检测的,如果不做这步可以忽略。相关依赖:

    "stylelint": "^13.13.1",
    "stylelint-config-idiomatic-order": "^8.1.0",
    "stylelint-order": "^4.1.0", // 支持 css 样式排序

由于统一用prettier来格式化css代码,需要安装stylelint插件来避免与prettier冲突:

"stylelint-config-prettier": "^8.0.1",
"stylelint-prettier": "^1.1.2",
  • stylelint-config-prettier,和eslint-config-prettier类似,作用是关闭 stylelint 所有不必要的或可能与 prettier 冲突的规则。但是在 Stylelint v15 版本之后,Stylelint 默认关闭了所有与 prettier 相冲突的风格规则,所以不需要安装stylelint-config-prettier了。

  • stylelint-prettier,和eslint-plugin-prettier类似,开启了以 prettier 为准的规则,并将报告错误给 stylelint。

.stylelintrc.js文件:

module.exports = {
    extends: [
        'stylelint-prettier/recommended',
        'stylelint-config-idiomatic-order',
        'prettier-stylelint/config.js',
    ],
    plugins: ['stylelint-order'],
    rules: {
        'prettier/prettier': true,
        'selector-pseudo-class-no-unknown': [true, { ignorePseudoClasses: ['global', 'local', 'export'] }],
        'property-no-unknown': [true, { ignoreProperties: ['composes', '/^var/'] }],
        'indentation': 4,
    }
}

.stylelintignore文件不进行css代码检测

/node_modules
/dist
/config
/public
/scripts
/**/*.md
/**/*.ts
/**/*.tsx
/**/*.js
/**/*.jsx

4. editorconfig

在项目中引入 editorconfig 是为了在多人协作开发中保持代码的风格和一致性。不同的开发者使用不同的编辑器或IDE,可能会有不同的缩进(比如有的人喜欢4个空格,有的喜欢2个空格)、换行符、编码格式等。甚至相同的编辑器因为开发者自定义配置的不同也会导致不同风格的代码,这会导致代码的可读性降低,增加代码冲突的可能性,降低了代码的可维护性。 EditorConfig 使不同编辑器可以保持同样的配置。因此,我们得以无需在每次编写新代码时,再依靠 Prettier 来按照团队约定格式化一遍(出现保存时格式化突然改变的情况) 。当然这需要在你的 IDE 上安装了必要的 EditorConfig 插件或扩展。 vscode中有EditorConfig for VS Code插件,可以安装。

# EditorConfig is awesome: https://EditorConfig.org

# top-most EditorConfig file
root = true

[*]                   # 表示所有文件都要遵循
indent_style = space              # 缩进风格,可选配置有space和tab
indent_size = 4                   # 缩进大小
end_of_line = lf                  # 换行符,可选配置有lf、cr和crlf
charset = utf-8                   # 编码格式,通常都是选utf-8
trim_trailing_whitespace = true   # 去除多余的空格
insert_final_newline = true       # 在尾部插入一行

[*.md]                # 表示仅 md 文件适用
max_line_length = false      # 每行最长字符数校验
trim_trailing_whitespace = false  # 去除多余的空格

八、代码提交规范(husky + lint-staged + commitlint)

lint-staged

如果想要防止团队协作时开发者提交不符合 ESLint 规则的代码则可以通过 lint-staged 工具来实现。lint-staged 可以在用户提交代码之前(生成 Git Commit Message 信息之前)使用 ESLint 检查 Git 暂存区中的代码信息(git add 之后的修改代码),一旦存在 💩 一样不符合校验规则的代码,则可以终止提交行为。需要注意的是 lint-staged 不会检查项目的全量代码(全量使用 ESLint 校验对于较大的项目可能会是一个相对耗时的过程),而只会检查添加到 Git 暂存区中的代码。 在package.json中配置:

"lint-staged": {
    "*.{ts,tsx}": [
      "eslint -c .eslintrc.js"
    ],
    "*.{css,less}": [
      "stylelint .stylelintrc.js --fix"
    ]
  },