从零搭建React脚手架(Webpack5+Typescript+Eslint)
前言
React是当前最流行的前端开发框架之一,React可以使前端开发更加便利,有着组件化,虚拟dom,高性能,单向数据流,生态系统丰富等优势。因此应运而生的react脚手架也有很多,一些官方推荐的脚手架可以做到开箱即用,不过如果我们对项目搭建不熟悉,遇到一些第三方脚手架不支持的某些场景,我们将变得束手无策,因此我们需要学习自己搭建脚手架,了解各个配置的原理,更好的处理需求变化。
准备工作
在搭建项目之前,我们需要先学习了解Webpack5,Typescript,Eslint等相关知识 项目代码Github地址
项目结构搭建
初始化项目
npm init -y
搭建项目结构
项目结构解析
config文件夹
config文件夹下面新建webpack.config.js是我们整个项目打包的所有配置,其中包括开发环境和生产环境
public文件夹
public文件夹放置项目入口html文件和网页图标,内容如下,html中只需要创建一个id为root的根节点即可
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" type="image/x-icon" href="./favicon.ico" />
<title>webpack for react cli</title>
</head>
<body>
<div id="root"></div>
</body>
</html>
src文件夹
src文件夹是我们项目代码的总文件夹,我们所有的项目代码都放在该文件夹下其中index.tsx文件是项目打包的入口文件,其他文件夹作用如下:
- asset:存放所有资源文件
- common:存放所有公共文件,例如自定义hooks和公共方法文件
- components:存放所有公共组件
- language:存放国际化文案等相关文件
- pages:存放项目主要页面
- redux:存放共享状态相关文件
- router:存放路由相关文件
其他文件
- .eslintignore:eslint检查的忽略文件
- .eslintrc.js:eslint的配置文件
- package-lock.json:安装第三包自动生成的文件
- package.json:第三方包配置文件
- tsconfig.json:ts配置文件
package.json文件具体内容
该文件配置了我们项目中所有需要安装的第三方依赖,后续会逐一讲解
{
"name": "webpack_react_cli",
"version": "1.0.0",
"description": "",
"main": "src/index.tsx",
"scripts": {
"serve": "cross-env NODE_ENV=development webpack server --config ./config/webpack.config.js",
"build": "cross-env NODE_ENV=production webpack --config ./config/webpack.config.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@babel/core": "^7.22.8",
"@babel/plugin-transform-runtime": "^7.22.9",
"@babel/preset-env": "^7.22.9",
"@babel/preset-react": "^7.22.5",
"@babel/preset-typescript": "^7.22.5",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.10",
"@types/react": "^18.2.15",
"@types/react-dom": "^18.2.7",
"@types/react-redux": "^7.1.25",
"@types/react-router-dom": "^5.3.3",
"@types/react-router-redux": "^5.0.22",
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
"babel-loader": "^9.1.3",
"copy-webpack-plugin": "^11.0.0",
"cross-env": "^7.0.3",
"css-loader": "^6.8.1",
"css-minimizer-webpack-plugin": "^5.0.1",
"eslint": "^8.44.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-jsx-a11y": "^6.7.1",
"eslint-plugin-promise": "^6.1.1",
"eslint-plugin-react": "^7.32.2",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-webpack-plugin": "^4.0.1",
"html-webpack-plugin": "^5.5.3",
"less": "^4.1.3",
"less-loader": "^11.1.3",
"mini-css-extract-plugin": "^2.7.6",
"postcss": "^8.4.25",
"postcss-loader": "^7.3.3",
"postcss-preset-env": "^9.0.0",
"react-refresh": "^0.14.0",
"style-loader": "^3.3.3",
"style-resources-loader": "^1.5.0",
"typescript": "^5.1.6",
"webpack": "^5.88.1",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^4.15.1",
"webpackbar": "^5.0.2"
},
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"browserslist": [
"> 1%",
"last 2 versions",
"not dead"
]
}
项目打包配置
创建脚手架,打包配置是核心内容,我们通过配置webpack.config.js文件,实现项目打包
安装webpack基础包
使用webpack打包,需要安装三个基础包,安装到开发依赖
- webpack:webpack核心包
- webpack-cli:webpack的命令行工具
- webpack-dev-server:webpack运行开发环境包
npm i webpack webpack-cli webpack-dev-server --save-dev
配置打包命令
我们可以在package.json中配置快捷打包命令,当我们运行开发环境使,需要指定当前环境,由于不同系统(Windows,Mac,Linux)指定环境方式不同,我们需要安装一个node工具,它可以在任意平台设置正确的环境变量
- 安装cross-env
npm i cross-env --save-dev
- 配置打包命令
我们使用cross-env配置打包命令并指定环境,后续我们运行npm run serve将运行开发环境,运行npm run build将运行生产环境,进行打包输出文件。
{
"name": "webpack_react_cli",
"version": "1.0.0",
"description": "",
"main": "src/index.tsx",
"scripts": {
"serve": "cross-env NODE_ENV=development webpack server --config ./config/webpack.config.js",
"build": "cross-env NODE_ENV=production webpack --config ./config/webpack.config.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {},
"dependencies": {},
"browserslist": []
}
配置webpack.config.js
配置入口,出口,运行环境
由于我们将开发环境和生成环境的配置文件整合成一个文件,因此一些开发环境和生产环境的不同配置我们需要区分,之前我们安装的cross-env就可以让我们区分当前环境,我们只需要判断process.env.NODE_ENV的值即可
- 获取当前环境值
const isProduction = process.env.NODE_ENV === 'production'
- 配置出入口和运行环境
入口文件及我们在src创建的index.tsx文件,出口配置,如果是开发环境我们没有具体的打包文件输出,webpack-dev-server将直接把打包文件存在内存中,因此不需要指定具体路径,生产环境我们将输出文件输出到根目录的build文件夹中,配置如下:__dirname为当前文件绝对路径
module.exports = {
mode: isProduction ? 'production' : 'development',
entry: './src/index.tsx',
output: {
path: isProduction ? path.resolve(__dirname, '../build') : undefined,
filename: 'js/[name].[contenthash:8].js',
chunkFilename: 'js/[name].[contenthash:8].chunk.js',
clean: true
}
}
处理js,jsx,ts,tsx文件配置
由于我们该项目将使用ts开发,因此我们需要配置相应Babel去处理各种文件。为了实现开发环境中的热更新,我们还需要配置实现热更新的插件和Babel,最后我们使用@babel/plugin-transform-runtime插件解决代码冗余和兼容性问题,这个插件可以防止babel转换es6语法时生成的辅助函数重复引用的问题。以下是需要安装的包,下面所有包使用npm i 进行安装,下载到开发依赖中--save-dev或者-D(特别注意:当我们使用react-refresh的时候,切记一定要关闭整个webpack的缓存并且开启devServer中的hot,否则热更新将会失效,直接在webpack中添加cache:false)
- @babel/core:Babel 的核心模块,负责将代码解析成 AST(抽象语法树),并对 AST 进行转换和生成新的代码
- @babel/preset-env:Babel 预设,用于根据目标环境自动确定需要进行的转换和插件,以达到兼容性的目的
- @babel/preset-react: Babel 预设,用于转换 JSX 语法为普通的 JavaScript 代码。它提供了一系列的转换规则,包括将 JSX 转换为 React.createElement 或其他等效形式的代码,并处理一些 React 相关的语法和特性
- @babel/preset-typescript: Babel 预设,用于转换 TypeScript 代码为普通的 JavaScript 代码
- @babel/plugin-transform-runtime: Babel 插件,用于减少编译过程中的代码冗余。它将一些辅助函数和工具函数抽离出来,通过引入 @babel/runtime 模块,避免在每个文件中重复生成这些函数,从而减小编译后代码的体积
- @pmmmwh/react-refresh-webpack-plugin:Webpack 插件,用于支持 React 组件的热模块替换(Hot Module Replacement,HMR)。它基于 React Fast Refresh 技术,能够在开发过程中实现在不刷新整个页面的情况下,对 React 组件进行快速更新,提高开发体验和效率
- react-refresh:Babel 插件,与 @pmmmwh/react-refresh-webpack-plugin 配合使用,用于在开发环境下实现 React 组件的热模块替换
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin')
const TerserWebpackPlugin = require('terser-webpack-plugin')
module.exports = {
cache: false,
module: {
rules: [
{
oneOf: [
{
test: /\.(js|ts)x?$/,
exclude: /node_modules/,
use: [
{
loader: 'babel-loader',
options: {
presets: [
'@babel/preset-env',
'@babel/preset-react',
'@babel/preset-typescript'
],
cacheDirectory: true,
cacheCompression: false,
plugins: [
!isProduction && 'react-refresh/babel',
'@babel/plugin-transform-runtime'
].filter(Boolean)
}
}
]
},
]
}
]
},
plugins: [
!isProduction && new ReactRefreshWebpackPlugin()
].filter(Boolean), // 将数组中的false类型过滤掉
optimization: {
// 当运行环境是开发环境时minimizer不加载
minimize: isProduction,
minimizer: [
new TerserWebpackPlugin()
]
}
}
其中cacheDirectory和cacheCompression是开启Babel缓存,并且不开启缓存压缩,这样可以优化除第一次以外的打包速度。TerserWebpackPlugin是webpack内置的一个插件,用来压缩js文件。
处理样式文件配置
项目中的样式文件,webpack自身无法解析打包,我们需要配置相应的解析器进行打包,以下是配置样式文件解析器所需要的包,以less,css文件为例,其中7,8两个插件需要分别在plugin和optimization中进行注册。oneof是用于优化打包速度,当文件被其中一个loader匹配,则不会继续遍历其他loader。下面所有包使用npm i 进行安装,下载到开发依赖中--save-dev或者-D
- less:less核心包
- less-loader:将less转为css
- style-resources-loader:将一个样式文件导入所有样式文件中,用于less定义全局变量
- css-loader:用于解析css文件
- style-loader:用于将样式注入到html的style标签中,在开发环境中使用
- postcss,postcss-loader,postcss-preset-env:用于兼容老版本浏览器,可以将css3最新的一些熟悉,转成之前浏览器可识别熟悉
- mini-css-extract-plugin:将css文件提取到单独文件中,生产环境使用
- css-minimizer-webpack-plugin:将提取的css文件压缩,减小打包体积
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const CssMinimizerWebpackPlugin = require('css-minimizer-webpack-plugin')
module.exports = {
module: {
rules: [
{
oneOf: [
{
test: /\.(css|less)$/,
use: [
isProduction ? MiniCssExtractPlugin.loader : 'style-loader',
'css-loader',
{
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: ['postcss-preset-env']
}
}
},
{
loader: 'less-loader',
options: {
lessOptions: {
javascriptEnabled: true
}
}
},
{
loader: 'style-resources-loader',
options: {
patterns: path.resolve(__dirname, '../src/assets/style/variables.less')
}
}
]
}
]
}
]
},
plugins: [
isProduction && new MiniCssExtractPlugin({
filename: 'style/[name].[contenthash:8].css',
chunkFilename: 'style/[name].[contenthash:8].chunk.css'
})
].filter(Boolean), // 将数组中的false类型过滤掉
optimization: {
// 当运行环境是开发环境时minimizer不加载
minimize: isProduction,
minimizer: [
new CssMinimizerWebpackPlugin()
]
}
}
处理HTML文件配置
我们处理项目入口html文件public文件夹下的html时,需要用到HtmlWebpackPlugin将html输出到build目录下,HtmlWebpackPlugin它的作用是简化在Webpack构建过程中生成HTML文件的过程,并且自动将生成的JS或CSS文件注入到HTML中,方便开发者构建SPA(单页面应用)或多页面应用
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html',
filename: 'index.html',
})
].filter(Boolean), // 将数组中的false类型过滤掉
}
处理图片配置
图片类型文件无需安装其他包,webpack5中内置的asset资源加载器即可处理,还可以根据图片大小,将图片分别转成base64格式还是直接输出文件
module.exports = {
module: {
rules: [
{
oneOf: [
{
test: /\.(png|jpe?g|gif|webp|svg|ico)$/,
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 8 * 1024
}
},
// 输出文件的文件夹和文件名
generator: {
filename: 'image/[name].[hash:8][ext]'
}
}
]
}
]
}
}
处理其他资源配置
当我们项目中还有一些比如字体文件,音视频文件等,此时我们只需要将其原格式输出就好,因此可以使用webpack5内置资源加载器asset/resource,它可以直接将文件输出并对应修改项目中的引用地址,我们将其统一放在media文件夹中
module.exports = {
module: {
rules: [
{
oneOf: [
{
test: /\.(woff2?|eot|ttf|otf|mp3|mp4|avi|mkv)$/,
type: 'asset/resource',
generator: {
filename: 'media/[name].[hash:8][ext]'
}
}
]
}
]
}
}
devServer配置
开发环境中devServer配置是必须的,但是此时不需要进行运行环境的判断,因为只有当你执行webpack server时才会执行该模块,因此直接配置即可,以下是每个配置项的解析
- historyApiFallback: true:这个配置项是用于启用HTML5 History API的回退选项。当使用React Router或其他前端路由库时,通常会使用浏览器的History API来处理路由跳转。但是,如果在开发环境下刷新页面或直接访问某个路由,可能会导致404错误。启用historyApiFallback后,Webpack Dev Server会将所有404请求都指向index.html,从而避免在前端路由中出现404错误
- compress: true:这个配置项用于启用gzip压缩。在开发环境下,开启gzip压缩可以减少传输的数据量,加快网络传输速度,从而提高开发服务器的响应速度。这对于加快开发过程中的资源加载是非常有益的。
- host: '0.0.0.0':这个配置项指定了Webpack Dev Server的主机地址。在开发环境中,默认情况下,Webpack Dev Server会在本地主机(localhost)上运行。但是,如果你希望在其他设备上访问开发服务器(如手机或其他电脑),可以将host设置为0.0.0.0
- port: 8000:这个配置项指定了Webpack Dev Server监听的端口号。默认情况下,Webpack Dev Server会在8080端口上运行。如果该端口已经被占用,你可以通过设置port选项来指定一个未被占用的端口号
- hot: true:这个配置项用于启用热模块替换(Hot Module Replacement,HMR)功能。使用热更新该配置必须启用
module.exports = {
devServer: {
historyApiFallback: true,
compress: true,
host: '0.0.0.0',
port: 8000,
hot: true
}
}
resolve配置
在我们导入模块时,我们习惯于将js,jsx,ts,tsx,json文件最后不写扩展名,此时我们需要在resolve的extensions中指定哪些文件导入时不需要指定扩展名。我们在配置一些路径别名时也需要在resolve的alias进行指定,具体配置如下
module.exports = {
resolve: {
extensions: ['.js', '.jsx', '.ts', '.tsx', '.json'],
alias: {
'@src': path.resolve(__dirname, '../src'),
'@assets': path.resolve(__dirname, '../src/assets'),
'@components': path.resolve(__dirname, '../src/components'),
'@pages': path.resolve(__dirname, '../src/pages'),
'@language': path.resolve(__dirname, '../src/language'),
'@redux': path.resolve(__dirname, '../src/redux'),
'@router': path.resolve(__dirname, '../src/router')
}
}
}
使用CopyWebpackPlugin处理public文件夹
我们之前使用HtmlWebpackPlugin将html文件输出到build目录下后,此时我们引用的网页图标并没有处理,所以我们打包后的文件将找不到该资源,因此我们可以把public文件夹下的文件处理html之外直接复制到build目录下
const CopyWebpackPlugin = require('copy-webpack-plugin')
module.exports = {
plugins: [
new CopyWebpackPlugin({
patterns: [
{
from: path.resolve(__dirname, '../public'),
to: path.resolve(__dirname, '../build'),
globOptions: {
// 忽略html文件不进行复制
ignore: ['**/index.html']
}
}
]
})
].filter(Boolean), // 将数组中的false类型过滤掉
}
代码分割配置
我们使用Webpack中的splitChunks和runtimeChunk选项的配置,用于控制代码分割和生成运行时代码的过程。这些配置有助于优化构建结果,减少重复代码的加载,提高应用的性能和加载速度。该配置在optimization进行配置
- **splitChunks:**用于将代码分割成不同的块,根据配置的策略将公共代码提取到单独的文件中,避免重复加载和提高缓存利用率。
- **runtimeChunk:**runtimeChunk用于生成运行时代码块,将webpack的runtime代码提取到一个单独的文件中。runtime代码包含了模块映射、模块加载和其他构建代码,该配置有助于提高构建结果的缓存利用率,同时可以避免每次构建都改变chunk的hash值。runtimeChunk的配置选项为一个函数,根据入口文件的名称生成运行时chunk的名称,这里将运行时chunk命名为runtime-入口文件名称。
module.exports = {
optimization: {
splitChunks: {
chunks: 'all',
name: false,
cacheGroups: {
reactBase: {
name: 'reactBase',
test: /[\\/]node_modules[\\/](react|react-dom|react-router-dom)[\\/]/,
priority: 10,
chunks: 'all'
},
antdBase: {
name: 'antdBase',
test: /[\\/]node_modules[\\/](antd|@ant-design)[\\/]/,
priority: 9,
chunks: 'all'
},
common: {
name: 'common',
minChunks: 2,
priority: 5,
chunks: 'all'
}
}
},
runtimeChunk: {
name: entrypoint => `runtime-${entrypoint.name}`
}
}
}
- chunks: 'all':表示代码分割的范围,指定为'all'表示将对所有的代码块进行分割,包括同步和异步代码。
- name: false:表示不为分割出的chunk生成名称,而是使用默认的名称。如果设置为true,则会为分割出的chunk生成名称。
- cacheGroups:用于配置缓存组,每个缓存组可以控制一类模块的代码分割策略。
- reactBase:一个缓存组,用于将node_modules目录下的React相关的包(react、react-dom、react-router-dom)打包到名为reactBase的chunk中。
- antdBase:一个缓存组,用于将node_modules目录下的Ant Design相关的包(antd、@ant-design)打包到名为antdBase的chunk中。
- common:一个缓存组,用于将被至少两个入口文件引用的模块打包到名为common的chunk中。
其他配置
- 配置devtool
该配置可以将打包后的文件和源文件进行映射,使我们在开发的时候方便定位问题所在
module.exports = {
devtool: isProduction ? 'source-map' : 'eval-cheap-module-source-map'
}
- 配置performance
该配置可以忽略打包文件产生的警告,并设置打包文件大小超过多少时进行警告提示
module.exports = {
performance: {
hints: false,
maxEntrypointSize: 512000,
maxAssetSize: 512000
}
}
- 配置打包进度显示
使用webpackbar插件可以实时看到打包进度
const WebpackBar = require('webpackbar')
module.exports = {
plugins: [
new WebpackBar()
].filter(Boolean), // 将数组中的false类型过滤掉
}
引入使用TypeScript
项目中引入ts一共需要三步
安装typescript核心包
npm i typescript --save-dev
安装Type Declaration
Type Declaration文件提供了类型信息,让TypeScript编译器知道库中各个模块、组件、函数等的类型和参数结构,从而在开发过程中提供类型检查和代码提示,以下是需要安装的类型定义文件
- "@types/react": 这个包提供了用于React库的类型声明文件。它包含了React库中各个模块、组件、函数等的类型定义,让TypeScript在项目中使用React时能够进行类型检查和提供代码提示。
- "@types/react-dom": 这个包提供了用于React DOM库的类型声明文件。React DOM是用于在Web浏览器中渲染React组件的库,它与React密切相关。"@types/react-dom"让TypeScript能够识别React DOM相关的类型和API,从而更好地支持在Web环境下开发React应用。
- "@types/react-redux": 这个包提供了用于React Redux库的类型声明文件。React Redux是用于在React应用中实现状态管理的库。"@types/react-redux"使得TypeScript能够了解React Redux库中各个函数、组件、连接器等的类型,提供类型检查和代码提示,从而更好地进行React应用的状态管理。
- "@types/react-router-dom": 这个包提供了用于React Router DOM库的类型声明文件。React Router DOM是用于在React应用中进行路由管理的库,支持在Web环境下进行前端路由。"@types/react-router-dom"让TypeScript能够了解React Router DOM库中的类型和API,提供类型检查和代码提示,更好地支持前端路由的开发。
- "@types/react-router-redux": 这个包提供了用于React Router Redux库的类型声明文件。React Router Redux是将React Router和Redux集成在一起的库,用于在React应用中实现路由状态的同步。"@types/react-router-redux"使得TypeScript能够了解React Router Redux库中各个函数、组件等的类型,提供类型检查和代码提示,更好地进行React应用的路由状态管理
npm i @types/react @types/react-dom @types/react-redux @types/react-router-dom @types/react-router-redux --save-dev
创建tsconfig.json文件并进行配置
在项目根目录创建tsconfig.json文件,tsconfig.json文件的配置有一百多项,我这里自定义配置了常用的几项,仅供参考,你们可以根据具体的项目需求和代码风格进行个性化调整
配置文件总览
{
"compilerOptions": {
"module": "ES2015",
"target": "ES2015",
"allowJs": true,
"removeComments": false,
"noImplicitAny": false,
"strictNullChecks": true,
"noEmit": true,
"jsx": "react",
"sourceMap": true,
"esModuleInterop": true,
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"lib": [
"es2022",
"dom",
"dom.iterable"
],
"paths": {
"@src/*": [
"./src/*"
],
"@asset/*": [
"./src/asset/*"
],
"@components/*": [
"./src/components/*"
],
"@pages/*": [
"./src/pages/*"
],
"@common/*": [
"./src/common/*"
],
"@language/*": [
"./src/language/*"
],
"@redux/*": [
"./src/redux/*"
],
"@router/*": [
"./src/router/*"
],
},
},
"include": [
"src/**/*"
],
"exclude": [
"node_modules",
"build"
]
}
配置文件解析
compilerOptions用于指定TypeScript编译器的设置和选项。通过配置compilerOptions,你可以影响TypeScript编译过程的行为和输出结果。include和exclude指定了ts检测包含的文件和排除的文件,src/**/*代表检测src文件夹目录下的所有文件。以下是compilerOptions详细解析
- "module": "ES2015":指定模块化的格式为ES2015(ES6),即使用import和export来导入和导出模块。
- "target": "ES2015":指定编译后的目标JavaScript版本为ES2015。TypeScript编译器会将TypeScript代码转换为相应的ES2015语法。
- "allowJs": true:允许在项目中包含JavaScript文件(.js)。这样可以让TypeScript编译器对JavaScript文件进行类型检查。
- "removeComments": false:保留编译后的JavaScript代码中的注释。
- "noImplicitAny": false:允许不明确指定类型的表达式的隐式any类型。当为true时,TypeScript编译器会报错,强制要求明确指定类型。
- "strictNullChecks": true:启用严格的null检查。当为true时,TypeScript编译器会对可能为null或undefined的值进行警告,从而避免潜在的null引用错误。
- "noEmit": true:不生成输出文件。这通常在只进行类型检查而不生成编译结果的情况下使用,如在代码构建流程的中间步骤。
- "jsx": "react":指定JSX的处理方式为React。这样可以使得TypeScript编译器正确处理React的JSX语法。
- "sourceMap": true:生成对应的source map文件,方便在调试时追踪到TypeScript源代码。
- "esModuleInterop": true:允许以ES模块的形式导入CommonJS模块。这样可以方便地在TypeScript中使用CommonJS模块。
- "moduleResolution": "node":指定模块解析方式为Node.js风格的解析。这样可以使得TypeScript编译器按照Node.js模块解析规则来查找模块。
- "allowSyntheticDefaultImports": true:允许从没有默认导出的模块中默认导入。这样可以方便地处理没有默认导出的模块。
- "lib": ["es2022", "dom", "dom.iterable"]:指定代码中可以使用的库。这里列出了使用的标准库,包括es2022、dom和dom.iterable。这样可以让TypeScript编译器识别相应的库中的类型和API
- "paths":和webpack中的resolve.alias作用类似,指定路径别名,这里指定之后可以防止ts路径检查报错
引入使用Eslint
在项目中引入ESLint有许多优势,它是一种静态代码分析工具,可以帮助团队维持一致的代码风格、发现潜在的问题和错误,并提高代码的质量和可读性,引入eslint我们可以分成以下几步
- 安装eslintWebpackPlugin插件并在webpack.config.json中进行配置
- 创建.eslintrc.js文件,并对eslint进行配置
- 创建.eslintignore文件,指定eslint需要忽略的文件和文件夹
安装配置EslintWebpackPlugin
- 安装EslintWebpackPlugin和Eslint核心包
npm i eslint-webpack-plugin eslint --save-dev
- 配置EslintWebpackPlugin
const EslintWebpackPlugin = require('eslint-webpack-plugin')
module.exports = {
plugins: [
new EslintWebpackPlugin({
context: path.resolve(__dirname, '../src'),
// 开启eslint检查缓存,只检查已修改的文件,默认开启
cache: true
})
].filter(Boolean), // 将数组中的false类型过滤掉
}
创建并配置.eslintrc.js文件
配置文件总览
module.exports = {
"env": {
"browser": true,
"es2022": true,
"node": true,
"commonjs": true
},
"extends": [
// "airbnb": 这是由 Airbnb 团队维护的一组 JavaScript 代码规范,
// 它提供了一套严格的、可读性高的代码规则,用于保持代码质量和一致性
"airbnb",
// 这个规则集合由 eslint-plugin-import 提供,它包含一些用于检查和约束 ES6 模块导入和导出的规则
"plugin:import/recommended",
// 这个规则集合扩展了 "plugin:import/recommended",添加了一些用于 TypeScript 中的导入和导出的规则
"plugin:import/typescript",
// 这个规则集合由 eslint-plugin-promise 提供,它提供了一些规则用于检查和约束 Promise 的使用
"plugin:promise/recommended",
// 这个规则集合由 eslint-plugin-react 提供,它包含一些用于检查和约束 React 代码的规则
"plugin:react/recommended",
// 这个规则集合由 eslint-plugin-react-hooks 提供,它包含一些用于检查和约束 React Hooks 的规则
"plugin:react-hooks/recommended",
// 这个规则集合由 eslint-plugin-jsx-a11y 提供,它包含一些用于检查和约束 JSX 元素上可访问性的规则
"plugin:jsx-a11y/recommended",
// 这个规则集合由 @typescript-eslint/eslint-plugin 提供,它包含一些用于检查和约束 TypeScript 代码的规则
"plugin:@typescript-eslint/recommended",
// 这个规则集合扩展了 "plugin:@typescript-eslint/recommended",添加了一些需要类型检查的 TypeScript 规则
"plugin:@typescript-eslint/recommended-requiring-type-checking"
],
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module",
"ecmaFeatures": {
"jsx": true
},
"project": "./tsconfig.json"
},
"plugins": [
"import",
"promise",
"react",
"react-hooks",
"jsx-a11y",
"@typescript-eslint"
],
"rules": {
// 缩进4个空格
"indent": ["error", 4],
// jsx缩进4个空格
"react/jsx-indent": ["error", 4],
// jsx属性缩进4个空格
"react/jsx-indent-props": ["error", 4],
// 禁止使用分号
"semi": ["error", "never"],
// 只能使用单引号
"quotes": ["error", "single"],
// 忽略换行符类型
"linebreak-style": "off",
// 可以在tsx文件中使用拓展名为jsx的文件
"react/jsx-filename-extension": "off",
// 禁止使用 @ts-ignore
"@typescript-eslint/ban-ts-ignore": "off",
// 忽略导入时的文件扩展名
"import/no-unresolved": "off",
// 忽略文件结尾必须有一行空行
"eol-last": "off",
// 忽略表达式必须单独一行
"react/jsx-one-expression-per-line": "off",
// 忽略button必须有类型
"react/button-has-type": "off"
},
"globals": {},
"overrides": [],
}
配置文件解析
- "env":指定eslint检测环境,当我们代码中使用未定义变量时,eslint会报错,当我们声明了检测环境时,例如browser:true,我们代码中再出现window全局变量,就不会报错。
- extends:指定继承规则,eslint规则配置有200多项,我们很难完全记住并单个配置,此时我们就可以按照我们项目情况,继承合适的第三方规则,例如我们这里就继承了react项目中比较适用的一些第三方规则,规则作用注释有说明,继承这些规则我们还需要安装对应的包,以下是需要安装的包名,我们可以使用npm i 包名 --save-dev进行安装:
"eslint-config-airbnb": "^19.0.4",
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-jsx-a11y": "^6.7.1",
"eslint-plugin-promise": "^6.1.1",
"eslint-plugin-react": "^7.32.2",
"eslint-plugin-react-hooks": "^4.6.0",
- parserOptions:用于指定ESLint解析代码时的选项和特性。
"ecmaVersion": "latest": 指定了要解析的ECMAScript版本。"latest"表示要使用最新的ECMAScript语法,ESLint将会识别和解析最新的JavaScript语法特性。"sourceType": "module": 指定代码中使用的模块化格式。在这里,设置为"module"表示代码使用的是ES模块化规范。这样,ESLint将会按照ES模块的语法规则解析代码中的导入和导出。"ecmaFeatures": { "jsx": true }: 启用了JSX语法支持。JSX是用于在React中编写组件的语法扩展,设置为true表示ESLint会允许并正确解析JSX语法。"project": "./tsconfig.json": 指定TypeScript项目的tsconfig.json配置文件的路径。这个选项告诉ESLint去读取TypeScript项目的配置,从而在TypeScript文件中进行类型检查和识别模块导入。
- plugins:用于指定要使用的ESLint插件,对上述继承规则进行插件指定
- rules:用于对继承规则中一些自身不喜欢的规则进行重写,我们继承的规则有时候并不能满足自身代码风格要求,此时我们需要根据自身需求自定义一些风格。例如我习惯于代码缩进为四个空格,我就修改了缩进样式。
- globals:用来定义一些全局变量,防止其报错,例如如果想在项目中使用jQuery,那我们需要把$符号在globals中声明,否则会无法识别该变量
- overrides:重写继承规则,作用和rules差不多,可在重写规则时指定文件类型,这里不做详细介绍,需要的可以查一下官网。
创建配置.eslintignore文件
eslintignore文件用户忽略文件检查,因为我们只需要检查src目录下的文件,因此我们需要把根目录下的一些文件忽略掉,防止其报错,特别是当我们在tsconfig中指定了检查目录后,eslint没有把tsconfig中指定的检查目录之外的文件忽略,就会出现冲突报错,以下是需要忽略的文件和文件夹
/build
/node_moduls
/config
.eslintrc.js
创建示例文件,运行测试
安装react核心包
react相关包需要安装到生产依赖中
npm i react react-dom
创建入口文件
我们需要在src下面创建个index.tsx作为文件的入口,在文件中我们创建一个react的根(root),并在根上渲染我们的入口文件app.tsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from '@pages/app'
const rootElement = document.getElementById('root') as HTMLElement
const root = ReactDOM.createRoot(rootElement)
root.render(<App />)
创建app及测试文件
我们在pages下创建app.tsx和demo01文件夹,并在demo01文件夹下创建demo01.less和demo01.tsx,用于测试文件打包运行
- app.tsx
import React from 'react'
import Demo01 from '@pages/demo/demo01'
function App() {
return (
<div>
<h1>React App</h1>
<Demo01 />
</div>
)
}
export default App
- demo01.tsx
import React, { memo, useState } from 'react'
import '@pages/demo/demo01.less'
function Demo01() {
// 实现一个简单使用useState的组件
// 1. 引入useState
// 2. 使用useState
// 3. 返回一个数组,第一个元素是状态,第二个元素是修改状态的函数
// 4. 使用状态
// 5. 修改状态
const [count, setCount] = useState(0)
return (
<div>
<h1>Demo Number {count}</h1>
<button
className="add-button"
onClick={() => setCount(count + 1)}
>
Add
</button>
</div>
)
}
export default memo(Demo01)
- demo01.less
.add-button {
width: 80px;
height: 40px;
cursor: pointer;
}
生产开发环境运行
开发环境运行
- 项目根目录下终端运行npm run serve(vscode为例),将出现以下内容,则运行成功
- 访问在浏览器中访问Loopback或者Network (IPv4)的任意链接将出现以下界面,点击button,数字会进行累加,测试修改代码,页面会进行热更新,而不是刷新整个页面。
生产环境打包
- 项目根目录下终端运行npm run build(vscode为例),将出现以下内容,则运行成功
- 打包生成的代码如下,则打包成功
webpack.config.js完整代码
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const EslintWebpackPlugin = require('eslint-webpack-plugin')
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin')
const CssMinimizerWebpackPlugin = require('css-minimizer-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const WebpackBar = require('webpackbar')
const TerserWebpackPlugin = require('terser-webpack-plugin')
const isProduction = process.env.NODE_ENV === 'production'
module.exports = {
mode: isProduction ? 'production' : 'development',
cache: false,
resolve: {
extensions: ['.js', '.jsx', '.ts', '.tsx', '.json'],
alias: {
'@src': path.resolve(__dirname, '../src'),
'@assets': path.resolve(__dirname, '../src/assets'),
'@components': path.resolve(__dirname, '../src/components'),
'@pages': path.resolve(__dirname, '../src/pages'),
'@language': path.resolve(__dirname, '../src/language'),
'@redux': path.resolve(__dirname, '../src/redux'),
'@router': path.resolve(__dirname, '../src/router')
}
},
entry: './src/index.tsx',
output: {
path: isProduction ? path.resolve(__dirname, '../build') : undefined,
filename: 'js/[name].[contenthash:8].js',
chunkFilename: 'js/[name].[contenthash:8].chunk.js',
clean: true
},
module: {
rules: [
{
oneOf: [
{
test: /\.(css|less)$/,
use: [
isProduction ? MiniCssExtractPlugin.loader : 'style-loader',
'css-loader',
{
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: ['postcss-preset-env']
}
}
},
{
loader: 'less-loader',
options: {
lessOptions: {
javascriptEnabled: true
}
}
},
{
loader: 'style-resources-loader',
options: {
patterns: path.resolve(__dirname, '../src/assets/style/variables.less')
}
}
]
},
{
test: /\.(png|jpe?g|gif|webp|svg|ico)$/,
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 8 * 1024
}
},
generator: {
filename: 'image/[name].[hash:8][ext]'
}
},
{
test: /\.(woff2?|eot|ttf|otf|mp3|mp4|avi|mkv)$/,
type: 'asset/resource',
generator: {
filename: 'media/[name].[hash:8][ext]'
}
},
{
test: /\.(js|ts)x?$/,
exclude: /node_modules/,
use: [
{
loader: 'babel-loader',
options: {
presets: [
'@babel/preset-env',
'@babel/preset-react',
'@babel/preset-typescript'
],
cacheDirectory: true,
cacheCompression: false,
plugins: [
!isProduction && 'react-refresh/babel',
'@babel/plugin-transform-runtime'
].filter(Boolean)
}
}
]
},
]
}
]
},
plugins: [
new EslintWebpackPlugin({
context: path.resolve(__dirname, '../src'),
cache: true
}),
new HtmlWebpackPlugin({
template: './public/index.html',
filename: 'index.html',
}),
new WebpackBar(),
new CopyWebpackPlugin({
patterns: [
{
from: path.resolve(__dirname, '../public'),
to: path.resolve(__dirname, '../build'),
globOptions: {
ignore: ['**/index.html']
}
}
]
}),
isProduction && new MiniCssExtractPlugin({
filename: 'style/[name].[contenthash:8].css',
chunkFilename: 'style/[name].[contenthash:8].chunk.css'
}),
!isProduction && new ReactRefreshWebpackPlugin()
].filter(Boolean),
optimization: {
minimize: isProduction,
minimizer: [
new CssMinimizerWebpackPlugin(),
new TerserWebpackPlugin(),
],
splitChunks: {
chunks: 'all',
name: false,
cacheGroups: {
reactBase: {
name: 'reactBase',
test: /[\\/]node_modules[\\/](react|react-dom|react-router-dom)[\\/]/,
priority: 10,
chunks: 'all'
},
antdBase: {
name: 'antdBase',
test: /[\\/]node_modules[\\/](antd|@ant-design)[\\/]/,
priority: 9,
chunks: 'all'
},
common: {
name: 'common',
minChunks: 2,
priority: 5,
chunks: 'all'
}
}
},
runtimeChunk: {
name: entrypoint => `runtime-${entrypoint.name}`
}
},
devServer: {
historyApiFallback: true,
compress: true,
host: '0.0.0.0',
port: 8000,
hot: true
},
devtool: isProduction ? 'source-map' : 'eval-cheap-module-source-map',
performance: {
hints: false,
maxEntrypointSize: 512000,
maxAssetSize: 512000
}
}
总结
这里只是react项目的最基本结构,后续还需要引入router,react-redux,国际化等,有需要的可以查阅我个人中的react专栏,里面有详细介绍,如果现在去还没有看到,那说明我还没写好,可以点个关注,会持续更新。创作不易,点赞收藏不迷路
转载自:https://juejin.cn/post/7257558539719295031