技术栈

文章中涉及的技术栈大致包含以下:

  • React18:UI 框架
  • TypeScript:类型语言
  • Vite4:构建工具
  • ReactRouter:路由
  • zustand:状态管理
  • ESlint,Prettier:代码规范
  • Axios:请求
  • antd:组件库
  • TailwindCSS3:css 解决方案
  • i18next,i18n-ally:多语言
  • ahooks:工具方法库
  • pnpm: 依赖管理

由于项目的规模、复杂性和要求都有所不一样,在技术选型上都会有所差异,我这边指出的是常见的后台管理类项目的需求架构。

技术选型是项目架构中占比比较大的一部分,下面简述一下,我是如何对比选择的技术栈:

React vs Vue

两者并没有谁好谁差的说法,一些论坛上吵来吵去谁也没办法说服谁。如果是个人项目,当然可以主观用哪个都是不错的,但从团队的角度,可能就需要考虑以下这些点了:

  • 对 React 的掌握程度:至少要有一个核心成员熟练 React,避免出现有问题不知道问谁的情况。

  • 前端团队成员的意向 or 招聘需求

  • 设计趋向 antd or element,能复用就能提高开发效率

  • 社区生态,例如你想要使用的必要的库只支持 React

TypeScript vs JavaScript

其实本质就是看对类型看不看重;

TypeScript 相对于 JavaScript 的优势在于它引入了静态类型检查,使得代码更加健壮和易于维护。

vite vs webpack

我个人有个项目是从 webpack 升级到 vite 的,升级成本并不高,一些项目甚至直接复制社区的最佳实践代码,稍加修改就能有不错的效果。

使用成本只是一个入门要求,只要能解决痛点,稍微学习一下当然值得。

vite 解决了什么问题呢? webpack 的启动速度慢,热更新速度慢的问题。

  • webpack 为什么慢:webpack 的运行原理导致的,在使用 webpack 启动项目时,webpack 会根据我们配置文件(webpack.config.js) 中的入口文件(entry),分析出项目项目所有依赖关系,然后打包成一个文件(bundle.js),交给浏览器去加载渲染。

  • vite 为什么快:利用了 ES module 浏览器遇到内部的 import 引用时,会自动发起 http 请求,去加载对应的模块 的特性,使用 vite 运行项目时,首先会用 esbuild 进行预构建,将所有模块转换为 es module,不需要对我们整个项目进行编译打包,而是在浏览器需要加载某个模块时,拦截浏览器发出的请求,根据请求进行按需编译,然后返回给浏览器。

redux vs zustand

这里对比 redux 的最佳实践 Redux Toolkit 与 zustand。

其实两者都是不错的选择,我从我的角度来讲讲为什么我选择 zustand。

  • 简单易用:Zustand 使用 React 的钩子机制作为状态管理的基础,通过创建自定义 Hook 来提供对状态的访问和更新,与函数式组件和钩子的编程模型紧密配合,使得状态管理变得非常自然和无缝。

  • 轻量级的状态管理库:对比 redux 体积,Redux 的体积相对较大,通常在几十 KB 到 100KB 左右,Zustand 的体积非常小,压缩后不到 1KB。

  • Zustand 提供了中间件 (middleware) 的概念,允许你通过插件的方式扩展其功能。中间件可以用于处理日志记录、持久化存储、异步操作等需求,使得状态管理更加灵活和可扩展。

Redux Toolkit 是官方推荐的编写 Redux 逻辑的方法。

Zustand 的优点包括:

轻量级,使用起来更简单,适用于中小型应用或者对于快速原型开发。无需繁琐的配置,减少了样板代码,提高了开发效率。 Zustand 的缺点包括:

可能不适用于大型复杂的应用程序,对于更多的状态管理需求可能不够灵活。社区资源和文档相对较少,可能需要更多的自行探索和摸索。

最后再对比一下写法(以持久化存储语言为例):

zustand:

import { create } from 'zustand'
import { persist } from 'zustand/middleware'

export type LngType = 'zh-CN' | 'en'

interface LngState {
lng: LngType
setLng: (value: LngType) => void
}

const useLngStore = create<LngState>()(
persist(
(set) => ({
lng: 'zh-CN',
setLng: (value) => set(() => ({ lng: value }))
}),
{
name: 'lng'
}
)
)

export default useLngStore

redux:

import { configureStore, createSlice } from "@reduxjs/toolkit";
import { persistReducer, persistStore } from "redux-persist";
import storage from "redux-persist/lib/storage";

// 创建一个 Redux Toolkit 的 slice
import { configureStore, createSlice, PayloadAction } from "@reduxjs/toolkit";
import { persistReducer, persistStore } from "redux-persist";
import storage from "redux-persist/lib/storage";

// 创建一个 Redux Toolkit 的 slice
interface LngState {
lng: "zh-CN" | "en";
}

const initialState: LngState = {
lng: "zh-CN",
};

const lngSlice = createSlice({
name: "lng",
initialState,
reducers: {
setLng: (state, action: PayloadAction<"zh-CN" | "en">) => {
state.lng = action.payload;
},
},
});

// 创建持久化配置
const persistConfig = {
key: "lng",
storage,
};

// 创建持久化 reducer
const persistedReducer = persistReducer(persistConfig, lngSlice.reducer);

// 创建 Redux store
const store = configureStore({
reducer: persistedReducer,
});

// 创建持久化存储
let persistor = persistStore(store);

// 导出 action creators 和 store
export const { setLng } = lngSlice.actions;
export { store, persistor };

pnpm vs npm 和 yarn

pnpm 对比 npm 和 yarn 的优势在于:包安装速度快和磁盘空间利用高效。

主要原因:npm/yarn 在不同项目依赖同一个包的情况下,会将这个包安装多次在每个项目中,而 pnpm 安装的包会存储在可寻址的磁盘中,在多个项目同时引用时,只需要用一个硬链接指向该地址就可以使用,大大节约了磁盘空间

pnpm

pnpm 采用硬链接和符号链接到全局磁盘上的内容可寻址存储来管理 node_modules,从而减少磁盘空间使用,同时保持项目的 node_modules 目录整洁。

关于什么是 hard link(硬链接),这里简单说下,在 Linux 文件系统中,磁盘中的文件都有一个索引编号(Inode Index),在 Linux 中,可以多个文件名指向同一索引节点,这种就是硬链接。

硬链接的机制可以让多个不同的位置寻址到相同的空间,也就说硬链接文件和原始文件其实是同一份文件,所以在 pnpm 管理的项目,100 个项目的相同依赖只需占用一份依赖的空间。

而且后续安装依赖时,如果该依赖之前已经安装过了,在 store 中已经有了该依赖,这时候就会直接使用 hard link,大大减少安装时间。

所以,总的来说,pnpm 比起 npm 和 yarn 不仅节省了磁盘空间,并且安装速度更快。

npm

在早期的 npm1 和 npm2 中 node_modules 的目录结构是嵌套的结构,例如有三个包结构,a 包,b 包引用 a 包,c 包引用 b 包,则会出现嵌套结构:

node_modules
└─ kai_npm_test_c
├─ index.js
├─ package.json
└─ node_modules
└─ kai_npm_test_b
├─ index.js
├─ package.json
└─ node_modules
└─ kai_npm_test_a
├─ index.js
└─ package.json

带来的问题:

  • windows 系统文件路径名限制:嵌套结构非常深的情况下,越深层的依赖的文件路径就越长,这会带来很多麻烦,在 window 系统下,很多程序无法处理超过 260 个字符的文件路径名。

  • 相同的依赖会重复安装造成浪费:比如上面的例子在安装了 C 包后又再安装了其他依赖包(例如叫它 D 包),假如 D 包也引用了测试 B 包,则 B 包会重复安装,并出现在 C 包和 D 包下的 node_modules 中。、

npm 从版本 3 和 yarn 一样开始维护一个扁平化的依赖树,这导致了更少的磁盘空间膨胀,但会导致 node_modules 目录混乱。

node_modules
├─ kai_npm_test_a
│ ├─ index.js
│ └─ package.json

├─ kai_npm_test_b
│ ├─ index.js
│ └─ package.json

└─ kai_npm_test_c
├─ index.js
└─ package.json

yarn

  • 使用 yarn.lock 文件解决那时候 npm 安装的不确定性(那时候 npm 还没有 lock 文件,npm5 才有)
  • yarn 并行安装的机制比 npm 的顺序安装速度更快,
  • 带来了可以从缓存中获取的离线模式。
  • 更简洁的命令行输出
  • 更好的语义化命令,比如 yarn add/remove 等

文件夹结构

├─ public                     # 静态资源
│ ├─ favicon.ico # favicon图标
├─ src # 项目源代码
│ ├─ components # 全局公用组件
│ ├─ layout # 布局组件
│ ├─ pages # pages所有页面
│ ├─ routers # 路由配置
│ ├─ services # api接口
│ ├─ static # 静态资源
│ ├─ stores # 全局 store管理
│ ├─ utils # 全局公用方法
│ ├─ App.tsx # 入口页面
│ ├─ global.d.ts # 全局声明文件
│ └─ index.tsx # 源码入口
└── .eslintignore # eslint忽略文件
└── .eslintrc.js # eslint配置
└── .gitignore.js # git忽略文件
└── .prettierrc.js # prettier配置
└── index.html # html模板
└── package.json # package.json
└── tailwind.config.js # tailwind配置
└── vite.config.js # vite打包配置

注意名称规范:文件小写(小驼峰),组件大驼峰,index

CSS 解决方案

  • tailwindcss 更高效好用的原子化的 CSS
  • CSS Modules 局部作用域 CSS
  • Sass/Less CSS 预处理器
  • Styled Components CSS in JS

路由方案

  • React Router DOM 路由

代码规范

  • 命名规范
  • 文件组织规范
  • css 规范
  • js 规范

Git 管理

  • GitFlow 分支管理策略
  • Commit message 规范