工厂流水线生产的东西用久了,总想着自己手工是否也能做出来,就如同工艺品和艺术品一般,虽然效果相似,但艺术品往往比工艺品更有韵味。
作为一名前端工程师,总是用一些脚手架来快速搭建新项目的基本结构,因此今天尝试着一步步搭建一个 React 的项目环境,看看需要处理哪些问题,查漏补缺!
首先分析我们的诉求:
针对上面的诉求,其实也是绝大部分项目都会需要,因此也有了常见的解决方案:
因为是 2022 年了,所以我们的项目所有依赖项全部用最新的工具库版本,搞起来!
首先是把项目的基本构建能力搭建好,让项目先跑起来!
mkdir webpack-react
cd webpack-react
npm init --y
git init
然后稍微改改 package.json
文件如下:
{
"name": "webpack-react",
"private": true,
"version": "0.1.0",
"description": "一个基于 Webpack 构建的 React开发环境",
"main": "index.js",
"scripts": {
"dev": "",
"build": "",
"preinstall": "npx only-allow yarn"
},
"keywords": [],
"author": "DYBOY",
"license": "ISC"
}
由于没有安装一些三方库,所以该文件还比较“简陋”,所以接下来逐个安装模块,配置环境!
根据需求,我们先安装一些必要的模块
首先是 React 的基本模块
yarn add react react-dom
yarn add @types/react @types/react-dom
然后是 TypeScript 类型模块
yarn add typescript -D
有了 TypeScript,就可以直接通过 TS 生成一个 tsconfig.json 的配置文件
yarn tsc --init
根据需要,稍微改改后如下:
// tsconfig.json
{
"compilerOptions": {
"target": "ES2015",
"lib": ["DOM", "ES2015"],
"jsx": "react-jsx",
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"module": "ESNext",
"rootDir": "./src",
"moduleResolution": "node",
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
},
"resolveJsonModule": true,
"allowJs": true,
"outDir": "./dist",
"removeComments": true,
"isolatedModules": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true
},
"include": ["src/"]
}
*关于 tsconfig.json
文件的配置解析可以参阅:《会写 TypeScript 但你真的会 TS 编译配置吗?[1]》
此时可以创建文件和文件夹,有一个初步的项目结构
项目结构
其中:
dist/
: 是用于存储打包的文件public/
: 是用于存放打包的模板入口 HTML 文件src/
: 是用于开发人员主要编码的文件夹.gitignore
: 用于配置 Git 忽略哪些文件或文件夹tsconfig.json
: TypeScript 的项目配置文件yarn.lock
: 依赖模块的版本信息,用于保证开发环境一致性此时就可以简单的写支持 TS 和 React 的应用了。
因为是一个项目,我们需要通过构建工具,帮助我们快速的实现打包,以及开发环境下的预览,因此第二步就是安装和配置 Webpack
yarn add webpack webpack-cli webpack-dev-server webpack-merge -D
后两个模块分别是用于开启开发时的本地 HTTP 服务,和用于 Merge webpack 配置的工具函数
首先,先完善 package.json
中的 scripts(开发指令和构建指令):
+ "dev": "cross-env NODE_ENV=development webpack serve -c scripts/webpack.dev.js",
+ "build": "yarn ts:checker && cross-env NODE_ENV=production webpack -c scripts/webpack.prod.js",
+ "ts:checker": "tsc --noEmit",
同时安装一下 cross-env
,该模块主要是用于支持在不同的操作系统下保证环境变量正确。
yarn add cross-env -D
通过指令,我们需要三个 Webpack 的配置文件:
这是公共的 Webpack 配置,主要配置了如下几个地方
const path = require("path");
const chalk = require("chalk");
const webpack = require("webpack");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const ProgressBarPlugin = require("progress-bar-webpack-plugin");
const pkgJSON = require("../package.json");
console.log("process.env.NODE_ENV: ", process.env.NODE_ENV);
module.exports = {
entry: path.resolve(__dirname, "../src/index.tsx",
output: {
filename: "[name].[hash:8].js",
path: path.resolve(__dirname, "../dist"),
publicPath: "/",
clean: true,
},
resolve: {
extensions: [".ts", ".tsx", ".js"],
alias: {
"@": path.resolve(__dirname, "../src"),
},
},
module: {
rules: [
{
test: /\.tsx?$/,
use: ["ts-loader"],
exclude: /node_modules/,
},
{
test: /\.(jpe?g|png|svg|gif)$/i,
type: "asset",
parser: {
dataUrlCondition: {
maxSize: 25 * 1024, // 25kb
},
},
generator: {
filename: "assets/imgs/[name].[hash:8][ext]",
},
},
],
},
plugins: [
new webpack.DefinePlugin({
// 定义在代码中可以替换的一些常量
__DEV__: process.env.NODE_ENV === "development",
}),
new HtmlWebpackPlugin({
template: path.resolve(__dirname, "../public/index.html"),
title: pkgJSON.name,
meta: {
description: {
type: "description",
content: pkgJSON.description,
},
},
minify: "auto",
}),
new ProgressBarPlugin({
format: ` :msg [:bar] ${chalk.green.bold(":percent")} (:elapsed s)`,
}),
],
};
个人有一个观点,开发环境和构建环境应该在配置上相似性需要寻找平衡,开发环境寻求的是热更新快,构建环境寻求的是兼容性好,且尽可能和开发环境看到效果相同!
针对缺失的模块还需要安装到开发依赖中:
# 支持 ts 和 tsx 文件的处理
yarn add ts-loader -D
# 美化终端输出,安装特定版本是为了处理模块化包的问题
yarn add chalk@4.1.2 -D
# 将 /public/index.html 作为模板入口文件打包
yarn add html-webpack-plugin -D
# 美化 webpack 编译时候的进度条
yarn add progress-bar-webpack-plugin -D
然后再配置下开发环境下的 Webpack 配置,主要是支持热更新、本地预览功能,以及一些和生产环境差异的配置。
const { merge } = require("webpack-merge");
const common = require("./webpack.common.js");
module.exports = merge(common, {
mode: "development", // 开发模式
devServer: {
hot: true, // 热更新
open: true, // 编译完自动打开浏览器
compress: false, // 关闭gzip压缩
port: 7878, // 开启端口号
historyApiFallback: true, // 支持 history 路由重定向到 index.html 文件
},
module: {
// 插件的执行顺序从右到左
rules: [
{
test: /\.(css|scss|sass)$/,
use: [
"style-loader",
"css-loader",
{
loader: "postcss-loader",
options: {
postcssOptions: {
plugins: [["autoprefixer"]],
},
},
},
"sass-loader",
],
// 排除 node_modules 目录
exclude: /node_modules/,
},
],
},
stats: "errors-only", // Webpack 在编译的时候只输出错误日志,终端更清爽
});
这里增加了对 scss/css 文件的处理,因此还需要安装相关的模块:
# style-loader 将 css 注入到 HTML 的内联样式
# css-loader 用于加载 CSS 文件,转化 CSS 为 CommonJS
yarn add style-loader css-loader -D
# postcss 用于处理 CSS 兼容性
# autoprefixer 用于自动根据兼容需求增加 CSS 属性的前缀
yarn add postcss postcss-loader autoprefixer -D
# sass 主要是用于支持 “CSS 编程”
# sass-loader 会将 .scss 后缀文件编译成 CSS
yarn add sass sass-loader -D
讲到了 CSS 自动前缀处理兼容性,因此可以将需要兼容浏览器版本的配置放到 package.json
-> browserslist
属性下:
{
...
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"defaults",
"not ie < 11",
"last 2 versions",
"> 1%",
"iOS 9",
"last 3 iOS versions"
]
}
...
}
针对 Webpack 的构建环境下(mode: "production"
)的配置,实际上在 Webpack 5 版本中默认就集成了很多优化,更多自定义诉求可以参考:Webpack Optimization[2] 配置。
const { merge } = require("webpack-merge");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const common = require("./webpack.common.js");
module.exports = merge(common, {
mode: "production",
optimization: {
minimize: true,
minimizer: [
"...",
new TerserPlugin({
terserOptions: {
format: {
comments: false,
},
},
extractComments: false,
}),
],
},
module: {
rules: [
{
test: /\.(css|scss|sass)$/,
use: [
MiniCssExtractPlugin.loader,
"css-loader",
{
loader: "postcss-loader",
options: {
postcssOptions: {
plugins: [["autoprefixer"]],
},
},
},
"sass-loader",
],
exclude: /node_modules/,
},
],
},
plugins: [
new MiniCssExtractPlugin({
filename: "assets/css/[hash:8].css",
}),
],
});
需要安装依赖:
# 用于将 CSS 导出到单独文件
yarn add mini-css-extract-plugin -D
# 用于做源代码压缩
yarn add terser-webpack-plugin -D
弄好了上面的 Webpack 配置,就可以实际的开发了
/src/index.tsx
文件如下:
执行:yarn dev
,会自动打开浏览器页面:http://localhost:7878/
执行:yarn build
,会将项目编译打包输出到 ./dist/
文件夹下
我们的项目可能会在各种浏览器中运行,为了尽可能兼容大多数用户的设备,因此引入 Babel 来统一处理兼容性。
在 webpack.common.js
配置文件中增加:
...
rules: [
{
test: /\.tsx?$/,
use: [
+ {
+ loader: "babel-loader",
+ options: {
+ presets: [
+ [
+ "@babel/preset-env", // 预制配置
+ {
+ corejs: {
+ version: 3,
+ },
+ useBuiltIns: "usage", // 按需引入 pollyfill
+ },
+ ],
+ "@babel/preset-react", // React 环境
+ ],
+ plugins: ["@babel/plugin-transform-runtime"],
+ },
+ },
"ts-loader",
],
exclude: /node_modules/,
},
...
],
...
放到 webpack.common.js
文件下也是为了考虑在开发环境下验证引入 pollyfill 的正确性。
同时还需要安装如下依赖:
# 安装 babel 核心和加载器
yarn add @babel/core babel-loader -D
# core-js 中有各种各样的 pollyfill,用于提升兼容性
# https://github.com/zloirock/core-js
yarn add core-js -D
# 预制环境
yarn add @babel/preset-env @babel/preset-react -D
# 统一的 pollyfill,打包时候加载到代码中,减少冗余代码
yarn add @babel/plugin-transform-runtime -D
前端的页面一般是多页面的,因此我们需要一个统一的路由来方便管理,这里用到了 react-router-dom v6[3] 版本
多路由的使用方式基本相似,因此官方提炼出了 useRoutes
的 Hooks,用于便捷生成路由,相较于 V5 版本,确实方便太多了。
安装作为应用依赖:
yarn add react-router-dom
首先是配置路由 /src/config/router.tsx
文件:
import { RouteObject } from "react-router-dom";
import HomePage from "@/pages/home";
const ROUTER_CONFIG: RouteObject[] = [
{
path: "/",
element: <HomePage />,
},
{
path: "*",
element: <>404 Not Found!</>,
},
];
export { ROUTER_CONFIG };
之后如果新增任意页面,都可以在 /src/pages/
文件夹下新增任,并且都可以放到 /src/config/router.tsx
文件来统一管理,嵌套路由同样适用,只需要根据 RouteObject
类型声明规范即可:
/**
* A route object represents a logical route, with (optionally) its child
* routes organized in a tree-like structure.
*/
export interface RouteObject {
caseSensitive?: boolean; // 大小写敏感
children?: RouteObject[]; // 子路由
element?: React.ReactNode; // 组件
index?: boolean; // 在子路由中,默认为父级路由的首页
path?: string; // URL 路径
}
然后在 /src/app.tsx
文件中使用 useRoutes()
并嵌入到应用中:
import { useRoutes } from "react-router-dom";
import { ROUTER_CONFIG } from "./config/router";
const App = () => {
const appRoutesElement = useRoutes(ROUTER_CONFIG);
return appRoutesElement;
};
export default App;
最后在 /src/inde.tsx
使用 BrowserRouter
包裹 <App />
组件
import { render } from "react-dom";
import { BrowserRouter } from "react-router-dom";
import App from "./app";
render(
<BrowserRouter>
<App />
</BrowserRouter>,
document.getElementById("root")
);
此时的项目目录结构如下:
目录结构
如此就可以愉快的编写任意页面啦!
虽然路由集中管理了,但是首屏加载的 js 文件太大,会使得白屏时间较长,增加了用户等待时间。
因此考虑延迟按需加载页面方式,使用 import()
和 React.lazy()
来主动优化。
新建一个通用组件 LazyWrapper
在 /src/components/lazy-wrapper/index.tsx
文件
import { FC, lazy, Suspense } from "react";
interface LazyWrapperProps {
/** 组件路径: 在 src/pages 目录下的页面路径,eg: /home => src/pages/home/index.tsx */
path: string;
}
/**
* 懒加载组件包装器
*/
const LazyWrapper: FC<LazyWrapperProps> = ({ path }) => {
const LazyComponent = lazy(() => import(`/src/pages${path}`));
return (
<Suspense fallback={<div>loading...</div>}>
<LazyComponent />
</Suspense>
);
};
export default LazyWrapper;
此时修改 /config/router.tsx
路由配置文件:
效果如下: 当加载 Home 页面时,按需加载对应的组件
另外由于拆包之后可能组件容易因网络抖动原因加载失败,所以还需要做自动重试拉取组件的方案,这里也不赘述了,参考之前写的文章:《性能优化竟白屏,难道真是我的锅?》
通过二次封装 Errorboundary
组件,实现组件加载失败自动重试,并针对错误上报日志,便于后期针对性优化。
在一个应用中,自然是少不了全局状态管理,一般情况下如果状态比较简单,可以直接使用 React 的 useContext
和 useReducer
Hooks 组合实现简单的全局状态管理。
但通常我们的项目应该是比较庞大复杂,为了提升后期可维护性,因此使用了 Redux 作为全局状态管理
Redux 的另一大优势则是提供了 @reduxjs/toolkit[4] 辅助工具,使得状态管理更加简单。
安装:
# react-redux 是 redux 的 UI 桥接层
yarn add redux react-redux
yarn add @reduxjs/toolkit
这里就不在赘述了,对于 Redux 的状态管理方案,可以参考之前写的文章:《用 Redux 做状态管理,真的很简单🦆!》
在已有项目中接入 Redux 的成本也不高!
CSS 是前端三大件之一,在上述“打包构建”中已经引入了 SASS[5] 作为 CSS 的编写的辅助方案,另一个常用方案就是 LESS[6],两者的区别可阅读:《SASS vs LESS[7]》。
作为一个通用的开发环境,可以考虑将两者都加入进来,但建议是将 SASS 作为我们自己开发时候的方案。
样式管理主要是考虑统一处理 客户端样式重置,定义全局样式、变量 等。
首先是将客户端样式统一化,这里将:Normalize.css[8] 文件复制到 /src/assets/style/normalize.css
然后在 /src/app.scss
文件中引入:
@import "./assets/style/normalize.css";
而 /src/app.scss
则是我们约定的全局样式文件,因此在该文件中定义一些 CSS 变量如下:
:root {
// 定义主题颜色
--theme-color: #165dff;
}
效果如下:
另外,开发中不可或缺的就是 UI 组件库,这里推荐使用字节跳动旗下的 Arco Design[9],文档非常详细,UI 风格个人非常喜欢,简洁大气。
安装依赖:
yarn add @arco-design/web-react
然后在项目中就可以直接使用了:
import { Button } from "@arco-design/web-react";
const HomePage = () => {
return (
<div>
<div>这是首页</div>
<Button>Arco 按钮</Button>
</div>
);
};
export default HomePage;
组件引入了,但样式却没引入生效,这里直接使用 Arco 推荐的 Webpack 插件来 @arco-plugins/webpack-react
,当然也可以通过 babel-plugin-import
实现动态引入。
安装:
yarn add @arco-plugins/webpack-react -D
在 /scripts/webpack.common.js
文件中的 plugins
中实例化插件:
const ArcoWebpackPlugin = require("@arco-plugins/webpack-react");
...
plugins: [
...
new ArcoWebpackPlugin(),
...
],
...
发现了错误:
因为动态处理需要通过编译 LESS 文件,所以我们还需要安装处理 LESS 语法的模块:
yarn add less less-loader -D
同样的,在 /scripts/webpack.dev.js
文件里的 module.rules
里添加如下规则:
{
test: /\.less$/,
use: [
"style-loader",
"css-loader",
{
loader: "postcss-loader",
options: {
postcssOptions: {
plugins: [["autoprefixer"]],
},
},
},
"less-loader",
],
include: /node_modules/,
},
webpack.prod.js
文件下同样需要配置,差异点在于 style-loader
替换为 MiniCssExtractPlugin.loader
这里没有限制只包含 /node_modules/
文件夹,因为只需要在该文件夹下处理 Arco Design 的 *.less
样式文件,我们自己的项目时推荐使用 *.scss
来编写。
这下效果就展示 OK 了:
由于 SCSS 是编译到 CSS,并没有做样式隔离,在一个复杂的项目中,极有可能出现同名 class 样式覆盖问题,可以通过自动生成前缀 CSS 类名来解决。
通过配置 css-loader
,根据指定规则生成 “hash-css-class-name
”
这里需要配置开发时候的配置 webpack.dev.js
如下:
构建时候的配置如下:
开发环境下的类名是为了便于开发调试快速定位到对应 CSS 得文件位置,构建环境下主要是生成 Hash
做混淆,同时简化 CSS 类名。
由于类名是动态的因此需要在组件中引入。
首先声明一个全局 TS 类型定义文件,用于定义一些通用的类型:
// src/global.d.ts
declare module "*.module.scss";
declare module "*.module.sass";
declare module "*.module.css";
然后在 src/pages/home/index.module.scss
文件定义如下:
.page-home {
position: relative;
height: 100vh;
background-color: #ffc1ed;
}
然后在 HomePage
组件中引入:
import { Button } from "@arco-design/web-react";
import classes from "./index.module.scss";
const HomePage = () => {
return (
<div className={classes['page-home']}>
<div>这是首页</div>
<Button type="primary">Arco 按钮</Button>
</div>
);
};
export default HomePage;
这样即可看到 CSS 动态类名效果:
在 TypeScript 环境下,CSS 模块化随好,但编写 CSS 得类名时候没有任何提示,一定程度上影响了开发效率,有没有什么方法可以在编写的时候有 CSS 类名提示?
其实只需要引入 typescript-plugin-css-modules
插件,首先是安装:
yarn add typescript-plugin-css-modules -D
然后在 tsconfig.json
中引入该插件:
之后在项目根目录新建 .vscode/settings.json
文件,内容如下:
{
"typescript.tsdk": "node_modules/typescript/lib",
"typescript.enablePromptUseWorkspaceTsdk": true
}
其作用是告诉 VScode 编辑器,使用项目中的 TypeScript 作为编译核心。
来看看实际效果吧:
非常的 Nice!
一般来讲,在团队内部,会封装一个网络请求的模块,供各个业务方向的开发使用,但在本次搭建中我们直接使用 Axios[10] 稍加封装即可,所有的 API 定义都放到 /src/api/
目录下。
关于 Axios 的源码分析,感兴趣的同学可以阅读:《Axios 网络请求源码精读笔记》
安装 Axios:
yarn add axios
我们在 /src/utils/request.ts
文件中定义如下:
这是一个简单封装的示例,根据业务需求可做一些自定义扩展,或者统一团队的网络请求工具,造个轮子🚗
当需要扩展,可以按照业务需求 & Server 约定在该文件中设置请求和响应拦截器即可!
Axios 的生态也非常丰富,例如可以加入 axios-retry[11] 模块,扩展 Axios 请求错误自动尝试。
# 安装
yarn add axios-retry
然后只需要修改 /src/utils/request.ts
axios-retry
为了统一管理所有的请求调用,因此将相关自定义请求函数放到 /src/api/
目录下,根据业务需要取文件名,例如用户个人信息相关的,就可以放到 ./api/user.ts
文件下。
在组件中可以直接调用不同的 api 函数即可,集中管理的方式会更加便于后期维护和升级。
这里单元测试就不再赘述了,主要还是使用 Jest 作为单测工具,可以参考文档:https://jestjs.io/zh-Hans/
另外单测,我们只需要配置一些核心主流程的测试任务就好,同时在 CI/CD 中配置自动触发运行单测检查。
尽可能使得单测不占用较多的开发时间,但能帮助及时发现阻塞性问题。
因为是前端开发,首先推荐的代码编辑器肯定就是 VScode 了,当然还有其他的,其他它的我也不熟(😁),所以大家尽可能都用 VScode 呗,轻量化~
关于 ESlint + Prettier + Git Commit 规范,之前在 Rollup 环境中也有讲到:《手摸手学会搭建一个 TS+Rollup 的初始开发环境》。
这里再补充推荐 ELAB 团队的文章:《从 ESLint 开启项目格式化[12]》,全面讲了一些配置。
主要讲了搭建一个基于 Webpack 构建工具的项目的全流程,会遇到哪些问题,遇到问题又该如何去解决,这有助于进一步掌握 Webpack、把控项目设计。
整一个自己搭建的过程还是非常麻烦的,步骤较多,因此这种重复的工作可以直接放到团队项目脚手架“模板”中,其他同学在初始化一个项目就可以开箱即用,这对于统一团队的研发风格和提升质量都有好处。
Webpack 只是工具,其如何能够解决实际问题,这才是我们需要重点关注的地方。
另外跳出局限可以按照 “为什么如此设计?有没有别的方式?相较区别和优劣?” 的三步骤思考方式,相信一定可以有更多收获!
这篇文章,本应该早就完成的,既是对自己目前掌握的一些项目搭建设计知识的一个简单回顾和查漏补缺,也是希望能够帮助到更多同学。由于篇幅过长,确实也省略了一些细节的讨论,比如:Commit 规范、MR 流程、构建优化、CI/CD 等等,这些的内容其实在实践中的感受会更加具体且深刻,对于提高研发效率和质量都有明显作用,欢迎大家找我“摆农门阵”...
[1]会写 TypeScript 但你真的会 TS 编译配置吗?: https://mp.weixin.qq.com/s/i1AVCXliehk5cMLPmPueOA
[2]Webpack Optimization: https://webpack.docschina.org/configuration/optimization/
[3]react-router-dom 文档: https://reactrouter.com/docs/en/v6/getting-started/overview
[4]@reduxjs/toolkit: https://redux-toolkit.js.org/
[5]SASS: https://www.sass.hk/
[6]LESS: https://lesscss.org/usage/
[7]SASS vs LESS: https://www.educba.com/sass-vs-less/
[8]Normalize.css: https://github.com/necolas/normalize.css/blob/master/normalize.css
[9]Arco Design: https://arco.design/
[10]Axios: https://axios-http.com/
[11]axios-retry: https://www.npmjs.com/package/axios-retry
[12]从 ESLint 开启项目格式化: https://juejin.cn/post/7031506030068662285