likes
comments
collection
share

从零搭建React脚手架(Webpack5+Typescript+Eslint)

作者站长头像
站长
· 阅读数 37

前言

React是当前最流行的前端开发框架之一,React可以使前端开发更加便利,有着组件化,虚拟dom,高性能,单向数据流,生态系统丰富等优势。因此应运而生的react脚手架也有很多,一些官方推荐的脚手架可以做到开箱即用,不过如果我们对项目搭建不熟悉,遇到一些第三方脚手架不支持的某些场景,我们将变得束手无策,因此我们需要学习自己搭建脚手架,了解各个配置的原理,更好的处理需求变化。

准备工作

在搭建项目之前,我们需要先学习了解Webpack5,Typescript,Eslint等相关知识 项目代码Github地址

项目结构搭建

初始化项目

npm init -y

搭建项目结构

按照如下结构搭建项目从零搭建React脚手架(Webpack5+Typescript+Eslint)

项目结构解析

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文件是项目打包的入口文件,其他文件夹作用如下:

  1. asset:存放所有资源文件
  2. common:存放所有公共文件,例如自定义hooks和公共方法文件
  3. components:存放所有公共组件
  4. language:存放国际化文案等相关文件
  5. pages:存放项目主要页面
  6. redux:存放共享状态相关文件
  7. router:存放路由相关文件

其他文件

  1. .eslintignore:eslint检查的忽略文件
  2. .eslintrc.js:eslint的配置文件
  3. package-lock.json:安装第三包自动生成的文件
  4. package.json:第三方包配置文件
  5. 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打包,需要安装三个基础包,安装到开发依赖

  1. webpack:webpack核心包
  2. webpack-cli:webpack的命令行工具
  3. webpack-dev-server:webpack运行开发环境包
npm i webpack webpack-cli webpack-dev-server --save-dev

配置打包命令

我们可以在package.json中配置快捷打包命令,当我们运行开发环境使,需要指定当前环境,由于不同系统(Windows,Mac,Linux)指定环境方式不同,我们需要安装一个node工具,它可以在任意平台设置正确的环境变量

  1. 安装cross-env
npm i cross-env --save-dev
  1. 配置打包命令

我们使用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的值即可

  1. 获取当前环境值
const isProduction = process.env.NODE_ENV === 'production'
  1. 配置出入口和运行环境

入口文件及我们在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)

  1. @babel/core:Babel 的核心模块,负责将代码解析成 AST(抽象语法树),并对 AST 进行转换和生成新的代码
  2. @babel/preset-env:Babel 预设,用于根据目标环境自动确定需要进行的转换和插件,以达到兼容性的目的
  3. @babel/preset-react: Babel 预设,用于转换 JSX 语法为普通的 JavaScript 代码。它提供了一系列的转换规则,包括将 JSX 转换为 React.createElement 或其他等效形式的代码,并处理一些 React 相关的语法和特性
  4. @babel/preset-typescript: Babel 预设,用于转换 TypeScript 代码为普通的 JavaScript 代码
  5. @babel/plugin-transform-runtime: Babel 插件,用于减少编译过程中的代码冗余。它将一些辅助函数和工具函数抽离出来,通过引入 @babel/runtime 模块,避免在每个文件中重复生成这些函数,从而减小编译后代码的体积
  6. @pmmmwh/react-refresh-webpack-plugin:Webpack 插件,用于支持 React 组件的热模块替换(Hot Module Replacement,HMR)。它基于 React Fast Refresh 技术,能够在开发过程中实现在不刷新整个页面的情况下,对 React 组件进行快速更新,提高开发体验和效率
  7. 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

  1. less:less核心包
  2. less-loader:将less转为css
  3. style-resources-loader:将一个样式文件导入所有样式文件中,用于less定义全局变量
  4. css-loader:用于解析css文件
  5. style-loader:用于将样式注入到html的style标签中,在开发环境中使用
  6. postcss,postcss-loader,postcss-preset-env:用于兼容老版本浏览器,可以将css3最新的一些熟悉,转成之前浏览器可识别熟悉
  7. mini-css-extract-plugin:将css文件提取到单独文件中,生产环境使用
  8. 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时才会执行该模块,因此直接配置即可,以下是每个配置项的解析

  1. historyApiFallback: true:这个配置项是用于启用HTML5 History API的回退选项。当使用React Router或其他前端路由库时,通常会使用浏览器的History API来处理路由跳转。但是,如果在开发环境下刷新页面或直接访问某个路由,可能会导致404错误。启用historyApiFallback后,Webpack Dev Server会将所有404请求都指向index.html,从而避免在前端路由中出现404错误
  2. compress: true:这个配置项用于启用gzip压缩。在开发环境下,开启gzip压缩可以减少传输的数据量,加快网络传输速度,从而提高开发服务器的响应速度。这对于加快开发过程中的资源加载是非常有益的。
  3. host: '0.0.0.0':这个配置项指定了Webpack Dev Server的主机地址。在开发环境中,默认情况下,Webpack Dev Server会在本地主机(localhost)上运行。但是,如果你希望在其他设备上访问开发服务器(如手机或其他电脑),可以将host设置为0.0.0.0
  4. port: 8000:这个配置项指定了Webpack Dev Server监听的端口号。默认情况下,Webpack Dev Server会在8080端口上运行。如果该端口已经被占用,你可以通过设置port选项来指定一个未被占用的端口号
  5. 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进行配置

  1. **splitChunks:**用于将代码分割成不同的块,根据配置的策略将公共代码提取到单独的文件中,避免重复加载和提高缓存利用率。
  2. **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}`
        }
    }
}
  1. chunks: 'all':表示代码分割的范围,指定为'all'表示将对所有的代码块进行分割,包括同步和异步代码。
  2. name: false:表示不为分割出的chunk生成名称,而是使用默认的名称。如果设置为true,则会为分割出的chunk生成名称。
  3. cacheGroups:用于配置缓存组,每个缓存组可以控制一类模块的代码分割策略。
  4. reactBase:一个缓存组,用于将node_modules目录下的React相关的包(react、react-dom、react-router-dom)打包到名为reactBase的chunk中。
  5. antdBase:一个缓存组,用于将node_modules目录下的Ant Design相关的包(antd、@ant-design)打包到名为antdBase的chunk中。
  6. common:一个缓存组,用于将被至少两个入口文件引用的模块打包到名为common的chunk中。

其他配置

  1. 配置devtool

该配置可以将打包后的文件和源文件进行映射,使我们在开发的时候方便定位问题所在

module.exports = {
    devtool: isProduction ? 'source-map' : 'eval-cheap-module-source-map'
}
  1. 配置performance

该配置可以忽略打包文件产生的警告,并设置打包文件大小超过多少时进行警告提示

module.exports = {
    performance: {
        hints: false,
        maxEntrypointSize: 512000,
        maxAssetSize: 512000
    }
}
  1. 配置打包进度显示

使用webpackbar插件可以实时看到打包进度

const WebpackBar = require('webpackbar')

module.exports = {
    plugins: [
        new WebpackBar()
    ].filter(Boolean), // 将数组中的false类型过滤掉
}

引入使用TypeScript

项目中引入ts一共需要三步

  1. 安装typescript核心包
  2. 安装TypeScript中用于支持对相应库的类型声明的类型定义文件(Type Declaration)
  3. 创建tsconfig.json文件并进行配置

安装typescript核心包

npm i typescript --save-dev

安装Type Declaration

Type Declaration文件提供了类型信息,让TypeScript编译器知道库中各个模块、组件、函数等的类型和参数结构,从而在开发过程中提供类型检查和代码提示,以下是需要安装的类型定义文件

  1. "@types/react": 这个包提供了用于React库的类型声明文件。它包含了React库中各个模块、组件、函数等的类型定义,让TypeScript在项目中使用React时能够进行类型检查和提供代码提示。
  2. "@types/react-dom": 这个包提供了用于React DOM库的类型声明文件。React DOM是用于在Web浏览器中渲染React组件的库,它与React密切相关。"@types/react-dom"让TypeScript能够识别React DOM相关的类型和API,从而更好地支持在Web环境下开发React应用。
  3. "@types/react-redux": 这个包提供了用于React Redux库的类型声明文件。React Redux是用于在React应用中实现状态管理的库。"@types/react-redux"使得TypeScript能够了解React Redux库中各个函数、组件、连接器等的类型,提供类型检查和代码提示,从而更好地进行React应用的状态管理。
  4. "@types/react-router-dom": 这个包提供了用于React Router DOM库的类型声明文件。React Router DOM是用于在React应用中进行路由管理的库,支持在Web环境下进行前端路由。"@types/react-router-dom"让TypeScript能够了解React Router DOM库中的类型和API,提供类型检查和代码提示,更好地支持前端路由的开发。
  5. "@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详细解析

  1. "module": "ES2015":指定模块化的格式为ES2015(ES6),即使用import和export来导入和导出模块。
  2. "target": "ES2015":指定编译后的目标JavaScript版本为ES2015。TypeScript编译器会将TypeScript代码转换为相应的ES2015语法。
  3. "allowJs": true:允许在项目中包含JavaScript文件(.js)。这样可以让TypeScript编译器对JavaScript文件进行类型检查。
  4. "removeComments": false:保留编译后的JavaScript代码中的注释。
  5. "noImplicitAny": false:允许不明确指定类型的表达式的隐式any类型。当为true时,TypeScript编译器会报错,强制要求明确指定类型。
  6. "strictNullChecks": true:启用严格的null检查。当为true时,TypeScript编译器会对可能为null或undefined的值进行警告,从而避免潜在的null引用错误。
  7. "noEmit": true:不生成输出文件。这通常在只进行类型检查而不生成编译结果的情况下使用,如在代码构建流程的中间步骤。
  8. "jsx": "react":指定JSX的处理方式为React。这样可以使得TypeScript编译器正确处理React的JSX语法。
  9. "sourceMap": true:生成对应的source map文件,方便在调试时追踪到TypeScript源代码。
  10. "esModuleInterop": true:允许以ES模块的形式导入CommonJS模块。这样可以方便地在TypeScript中使用CommonJS模块。
  11. "moduleResolution": "node":指定模块解析方式为Node.js风格的解析。这样可以使得TypeScript编译器按照Node.js模块解析规则来查找模块。
  12. "allowSyntheticDefaultImports": true:允许从没有默认导出的模块中默认导入。这样可以方便地处理没有默认导出的模块。
  13. "lib": ["es2022", "dom", "dom.iterable"]:指定代码中可以使用的库。这里列出了使用的标准库,包括es2022、dom和dom.iterable。这样可以让TypeScript编译器识别相应的库中的类型和API
  14. "paths":和webpack中的resolve.alias作用类似,指定路径别名,这里指定之后可以防止ts路径检查报错

引入使用Eslint

在项目中引入ESLint有许多优势,它是一种静态代码分析工具,可以帮助团队维持一致的代码风格、发现潜在的问题和错误,并提高代码的质量和可读性,引入eslint我们可以分成以下几步

  1. 安装eslintWebpackPlugin插件并在webpack.config.json中进行配置
  2. 创建.eslintrc.js文件,并对eslint进行配置
  3. 创建.eslintignore文件,指定eslint需要忽略的文件和文件夹

安装配置EslintWebpackPlugin

  1. 安装EslintWebpackPlugin和Eslint核心包
npm i eslint-webpack-plugin eslint --save-dev
  1. 配置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": [],
}

配置文件解析

  1. "env":指定eslint检测环境,当我们代码中使用未定义变量时,eslint会报错,当我们声明了检测环境时,例如browser:true,我们代码中再出现window全局变量,就不会报错。
  2. 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",
  1. 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文件中进行类型检查和识别模块导入。

  1. plugins:用于指定要使用的ESLint插件,对上述继承规则进行插件指定
  2. rules:用于对继承规则中一些自身不喜欢的规则进行重写,我们继承的规则有时候并不能满足自身代码风格要求,此时我们需要根据自身需求自定义一些风格。例如我习惯于代码缩进为四个空格,我就修改了缩进样式。
  3. globals:用来定义一些全局变量,防止其报错,例如如果想在项目中使用jQuery,那我们需要把$符号在globals中声明,否则会无法识别该变量
  4. overrides:重写继承规则,作用和rules差不多,可在重写规则时指定文件类型,这里不做详细介绍,需要的可以查一下官网。

创建配置.eslintignore文件

eslintignore文件用户忽略文件检查,因为我们只需要检查src目录下的文件,因此我们需要把根目录下的一些文件忽略掉,防止其报错,特别是当我们在tsconfig中指定了检查目录后,eslint没有把tsconfig中指定的检查目录之外的文件忽略,就会出现冲突报错,以下是需要忽略的文件和文件夹

/build
/node_moduls
/config
.eslintrc.js

创建示例文件,运行测试

我们可以在src和pages文件夹下创建以下文件进行测试从零搭建React脚手架(Webpack5+Typescript+Eslint)

安装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,用于测试文件打包运行

  1. 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
  1. 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)
  1. demo01.less
.add-button {
    width: 80px;
    height: 40px;
    cursor: pointer;
}

生产开发环境运行

开发环境运行

  1. 项目根目录下终端运行npm run serve(vscode为例),将出现以下内容,则运行成功

从零搭建React脚手架(Webpack5+Typescript+Eslint)

  1. 访问在浏览器中访问Loopback或者Network (IPv4)的任意链接将出现以下界面,点击button,数字会进行累加,测试修改代码,页面会进行热更新,而不是刷新整个页面。

从零搭建React脚手架(Webpack5+Typescript+Eslint)

生产环境打包

  1. 项目根目录下终端运行npm run build(vscode为例),将出现以下内容,则运行成功

从零搭建React脚手架(Webpack5+Typescript+Eslint)

  1. 打包生成的代码如下,则打包成功

从零搭建React脚手架(Webpack5+Typescript+Eslint)

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
评论
请登录