Summary
搭建 React 项目的几种方案介绍。以及 React
生态圈内会常用到的一些技术方案选择,如:样式方案、CSS-in-JS、路由、网络请求、全局状态管理、项目开发体验和工程化(eslint, prettier, husky + lint-staged)、多语言国际化、动画、暗黑模式
- nodejs
- git
- VS Code (prettier, eslint 等插件)
- React
常见的线上编辑代码工具
- CodePen
- CodeSandbox(丰富的初始化模版快速创建项目,可在线编辑并允许,可分享链接)
- StackBlitz (丰富的初始化模版快速创建项目,可在线编辑并允许,可分享链接) https://vite.new/
- github.dev (在任意
Github
项目点击 . 可以直接在网页打开,方便查看项目源码)React 语法
React
和Vue
是目前最流行的前端UI
框架,由于React
是一个蛮大的话题,可能需要自行花费一定时间学习才能正确明白和适用。所以语法部分这里不多介绍,就简单介绍一些基本的语法。
- JSX
JSX
是 JavaScript
语言的扩展,类似于 html
标签,可以用来结构化的展示页面组件构成,具于 JavaScript
灵活的特点和运算能力。
const element = <h1>Hello, world!</h1>;
Hello World
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<h1>Hello, world!</h1>);
参数
const element = <a href="https://www.reactjs.org"> link </a>;
表达式
function formatName(user) {
return user.firstName + ' ' + user.lastName;
}
const user = {
firstName: 'Harper',
lastName: 'Perez'
};
const element = (
<h1>
Hello, {formatName(user)}!
</h1>
);
最终 JSX
也会被编译成 JavaScript
React.createElement('div', {style: 'color: red;'})
- 组件
在 React
中,最重要的概念就是组件,每个页面可以由数个组件组成,每个组件可以拥有自己独特的功能特性和界面展示。
函数组件
function Button() {
return <button className="button">Hello</button>
}
类组件
class Button extends React.Component {
// ...省略生命周期等方法
render() {
return <button className="button">Hello</button>
}
}
props
和state
给组件传参数和组件间通信通过 props
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
组件内部可以维护自由状态 state
class Clock extends React.Component {
constructor(props) {
super(props);
this.state = {date: new Date()};
}
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
props
和 state
的改变都会触发组件的 rerender
事件处理
<button onClick={activateLasers}>
Activate Lasers
</button>
生命周期和
hooks
类组件才有生命周期,在 React 16.8
未引入 hook
概念之前,函数组件无法由于 state
,所以功能单一并且只能用于展示组件。但是 hooks
使得函数组件能够维护自身 state
并且将各个逻辑抽取封装成 hooks
函数使得组件逻辑更加易读、代码复用高、非常有利于项目维护,目前都是会采用函数组件加 hooks
写法编写 React
项目。
- https://reactjs.org/docs/hooks-intro.html
- https://www.youtube.com/watch?v=dpw9EHDh2bM ```javascript import React, { useState } from ‘react’;
function Example() { // Declare a new state variable, which we’ll call “count” const [count, setCount] = useState(0);
return (
You clicked {count} times
<a name="35d038c8"></a>
## 方案一:Create React app
`create-react-app` 是 `React` 团队官方提供的一套快速搭建和初始化 `React` 项目的脚手架工具。基于 `Webpack` (前端最成熟、适用范围最广的项目打包工具) 无需开发者手动从零开始配置,也提供一定的项目配置能力。
<a name="6e11ac76"></a>
### 初始化项目
- [https://create-react-app.dev/docs/getting-started](https://create-react-app.dev/docs/getting-started)
```bash
npx create-react-app my-app
cd my-app
npm start
# typescript
npx create-react-app my-app --template typescript
# 其他模版
npx create-react-app my-app --template [template-name]
项目目录结构
my-app
├── README.md
├── node_modules # 依赖
├── package.json # 项目信息、运行命令、依赖管理
├── .gitignore
├── public # 公共资源路径
│ ├── favicon.ico
│ ├── index.html # html 模版文件
│ ├── logo192.png
│ ├── logo512.png
│ ├── manifest.json
│ └── robots.txt
└── src # 源代码目录
├── App.css
├── App.js # 主组件入口
├── App.test.js
├── index.css
├── index.js # 项目入口文件
├── logo.svg
├── serviceWorker.js
└── setupTests.js
命令
# 运行项目
npm start
# 打包
npm run build
样式方案
- css/less/scss
css
和常见的css
预处理器都是默认支持的,这也是最常见的写法。只需要新建样式文件,然后组件内引入该样式文件通过className
来对应样式,基本相同于html
原生的写法
```javascript import ‘./button.less’;.button {
color: red;
}
function Button() { return }
2. `CSS-in-JS` 方案之 `emotion`
`CSS-in-JS` 方案的优势在于样式和组件在同一个文件内,编写组件时不需要不停切换文件。并且由于使用 `JavaScript` 编写样式可以利用其灵活性,变量,主题切换,样式复用都会变得更简单。各种 `CSS-in-JS` 库的出现都简化了样式和组件编写的过程。常见的有 [styled-components](https://styled-components.com/),[Emotion](https://emotion.sh/), [vanilla-extract](https://vanilla-extract.style/) 等
<a name="a45c480d"></a>
### Create React App 优势
1. 官方团队提供和维护,较稳定
2. 无需配置
<a name="3e98931d"></a>
### Create React App 缺点
1. 缺少新功能
2. 项目较大之后打包速度慢,需要一定优化手段
3. 虽然无需配置,但是其实是将各种工具的配置入口隐藏,需要一些自定义配置时可能无法做到,比如配置 `postcss` 插件
4. 周边生态缺少维护,如用来自定义配置的 `craco` 再 `create-react-app` 更新到 5 之后很长时间都没有跟进支持,[主开发者无精力继续维护项目等](https://github.com/dilanx/craco/issues/415)
<a name="a965b140"></a>
## 方案二:Vite
<a name="be407156"></a>
### Vite 优势
1. 快。开发模式快,编译快(`ESM` + `esbuild`(`go` 语言写的 `JavaScript` 编译器) + 依赖预编译等)
2. 与框架无关,可以用于任意前端框架项目
3. 配置灵活,功能丰富
4. 打包基于 `rollup`,有丰富的插件
<a name="6e11ac76-1"></a>
### 初始化项目
```shell
npm create vite@latest
npm create vite@latest my-app -- --template react-ts
创建目录结构
cd src
mkdir -p assets assets/icons assets/images components constants pages pages/home styles utils
工程化
npm i -D eslint eslint-config-react-app eslint-config-prettier prettier lint-staged rollup-plugin-visualizer @types/node@16 cross-env
npx husky-init && npm install
touch .eslintrc .eslintignore .prettierrc .prettierignore
.eslintrc
{
"extends": [
"react-app",
"react-app/jest",
"prettier"
]
}
.prettierrc
{
"singleQuote": true,
}
package.json
{
"scripts": {
"analyze": "tsc && cross-env ANALYZE=true vite build",
},
"lint-staged": {
"**/*.{js,jsx,ts,tsx}": "eslint --fix --ext .js,.jsx,.ts,.tsx",
"**/*.{js,jsx,tsx,ts,less,css,json}": [
"prettier --write"
]
}
}
.husky/pre-commit
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx lint-staged
vite.config.ts
import { visualizer } from 'rollup-plugin-visualizer';
import { defineConfig, splitVendorChunkPlugin } from 'vite';
// 打包生产环境才引入的插件
if (process.env.NODE_ENV === 'production') {
process.env.ANALYZE &&
plugins.push(
visualizer({
open: true,
gzipSize: true,
brotliSize: true,
})
);
}
// https://vitejs.dev/config/
export default defineConfig({
plugins: [splitVendorChunkPlugin(), react(), ...plugins],
resolve: {
alias: [
{
find: '@',
replacement: '/src',
},
],
}
});
组件库
选项
npm i react-vant@next
使用
import { Button } from 'react-vant';
样式方案/样式覆盖/暗黑模式
样式方案还是采用 css
+ less
+ className
。需要考虑暗黑模式,所有样式都用 CSS 变量
,然后根据 html
标签属性切换
由于是 H5
项目,需要考虑不同设备屏幕大小适配,引入插件 postcss-px-to-viewport
可以自动将 px
单位转换为 vw
单位
npm i -D less postcss-px-to-viewport
cd styles
touch css-variable.less global.less index.less react-vant.less variables.less common.less
postcss.config.js
module.exports = {
plugins: {
'postcss-px-to-viewport': {
viewportWidth: 375,
},
},
}
index.less
@import './css-variables.less';
@import './react-vant.less';
@import './global.less';
@import './common.less';
css-variables.less
:root {
--primary-color: #333;
--page-bg: #f5f5f5;
}
:root[data-theme='dark'] {
--primary-color: #fff;
--page-bg: #0a1929;
}
图标引入
图标都采用 SVG
可以方便使用、修改大小、颜色,下载到项目本地,通过 svgr 和 vite-plugin-svgr 可以当成 React
组件直接引入
npm i -D vite-plugin-svgr
vite.config.ts
import svgr from 'vite-plugin-svgr';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [svgr({
// 这个选项将组件导出到 default 而不是 ReactComponent
// exportAsDefault: true,
svgrOptions: {
icon: true,
// 删除 svg fill 颜色
replaceAttrValues: {
none: 'currentColor',
black: 'currentColor',
'#62626B': 'currentColor',
'#686872': 'currentColor',
'#0055FF': 'currentColor',
'#3F7FFF': 'currentColor',
'#A8A8A8': 'currentColor',
'#1A1A1A': 'currentColor',
'#EA4D44': 'currentColor',
},
},
}),],
});
vite.env.d.ts
增加这个类型可以帮助 TS
识别 svg
导入
/// <reference types="vite-plugin-svgr/client" />
使用
import { ReactComponent as Logo } from './logo.svg'
网络请求
使用熟悉的 axios
通过拦截器封装网络请求,网络请求代码部分使用工具 openapi2typescript
通过后端提供的 Swagger
文档自动生成
npm i -D @umijs/openapi
touch openapi.config.js
# 在 package.json 中增加了命令之后可以每次都通过此命令生成网络请求代码
npm run openapi
package.json
{
"scripts": {
"openapi": "node openapi.config.js",
}
}
openapi.config.js
const { generateService } = require('@umijs/openapi')
generateService({
schemaPath: 'http://petstore.swagger.io/v2/swagger.json',
serversPath: './servers',
})
/**
* 生成app端接口
*/
generateService({
requestLibPath: "import request from '@/utils/request'",
schemaPath: 'http://192.168.2.147:9088/v3/api-docs?group=app端汇总',
serversPath: './src/services',
projectName: 'app',
});
src/utils/request.ts
import { store } from '@/store';
import { toggleIsLogin } from '@/store/reducers/user';
import axios, { AxiosRequestConfig } from 'axios';
import { Toast } from 'react-vant';
import { getUserToken, removeUserToken, TOKEN_KEY } from './auth';
export const API_CODES = {
SUCCESS: 200,
UNAUTHORIZED: 401,
FORBIDDEN: 403,
};
export const isRequestSuccess = (res: API.ApiResponse) => {
return res && res.code === API_CODES.SUCCESS;
};
export const isNeedLogin = (res: API.ApiResponse) => {
return res && res.code === API_CODES.SUCCESS;
};
const request = axios.create({});
request.interceptors.request.use(
(config) => {
const token = getUserToken();
if (token && config.headers) {
config.headers[TOKEN_KEY] = token;
}
return config;
},
(error) => Promise.reject(error)
);
request.interceptors.response.use(
(response) => {
const res = response.data;
if (isRequestSuccess(res)) {
return response; // axios 类型不支持返回 response 中的 data,将 response 整个返回,后续取数据需要 res.data 中获取
}
// 如果用户未登录跳转至登录页面
if (isNeedLogin(res)) {
store.dispatch(toggleIsLogin(false));
removeUserToken();
window.location.reload(); // TODO: toLogin()
}
const errMsg = res.msg || '请求错误';
Toast(errMsg);
return Promise.reject(response);
},
async (error) => {
let errMsg = '请求错误';
if (error.response) {
const { data } = error.response;
errMsg = data.msg || errMsg;
}
Toast(errMsg);
return Promise.reject(error);
}
);
/**
* 网络请求,封装后的 axios
* 1. AxiosInstance 不支持泛型,openapi2typescript 生成的请求会带上请求接口返回数据类型,
* 2. openapi2typescript 默认采用 umi_request 模板,会添加额外参数 requestType, axios 不支持该参数
* @param {string} url
* @param {AxiosRequestConfig<R>} config
* @returns
*/
function requestMethod<T>(
url: string,
config: AxiosRequestConfig<any> & { requestType?: string }
) {
const _config = {
...config,
};
if (config.requestType === 'json') {
_config.headers = {
Accept: 'application/json',
'Content-Type': 'application/json;charset=UTF-8',
..._config.headers,
};
} else if (config.requestType === 'form') {
_config.headers = {
Accept: 'application/json',
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
..._config.headers,
};
}
delete _config.requestType;
return request.request<T>({
url,
..._config,
});
}
export default requestMethod;
全局状态管理
用于保存一些需要全局存储的信息,比用用户登录状态、用户信息等
选项
- Redux
- Mobx
- zustand
useReducer
,Context
- …
npm i redux react-redux @reduxjs/toolkit redux-logger
mkdir -p src/store src/store/reducers
touch src/store/index.ts src/store/hooks.ts
src/store/index.ts
```typescript import { Action, configureStore, ThunkAction } from ‘@reduxjs/toolkit’; import logger from ‘redux-logger’; import counterReducer from ‘./reducers/counterSlice’; import user from ‘./reducers/user’;
const middlewares: any[] = [];
if (import.meta.env.DEV) { middlewares.push(logger); }
export const store = configureStore({ reducer: { counter: counterReducer, user }, middleware: (getDefaultMiddleware) => { return getDefaultMiddleware().concat(middlewares); }, });
export type AppDispatch = typeof store.dispatch;
export type RootState = ReturnType
;
`src/store/hooks.ts`
typescript import type { AppDispatch, RootState } from ‘@/store’; import { TypedUseSelectorHook, useDispatch, useSelector } from ‘react-redux’;
// Use throughout your app instead of plain useDispatch
and useSelector
export const useAppDispatch = () => useDispatch
`main.tsx`
```tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import { Provider } from 'react-redux';
import App from './App';
import { store } from './store';
import './styles/index.less';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>
);
使用
import { useAppDispatch, useAppSelector } from '@/store/hooks';
import { fetchUserInfo } from '@/store/reducers/user';
import { useEffect } from 'react';
import { Navigate, useLocation } from 'react-router-dom';
function RequireAuth({ children }: { children: JSX.Element }) {
const location = useLocation();
const isLogin = useAppSelector((state) => state.user.isLogin);
const userInfo = useAppSelector((state) => state.user.userInfo);
const dispatch = useAppDispatch();
useEffect(() => {
if (isLogin && !userInfo) {
dispatch(fetchUserInfo());
}
}, [dispatch, isLogin, userInfo]);
if (!isLogin) {
// Redirect them to the /login page, but save the current location they were
// trying to go to when they were redirected. This allows us to send them
// along to that page after they login, which is a nicer user experience
// than dropping them off on the home page.
return <Navigate to="/login" state={{ from: location }} replace />;
}
return children;
}
export default RequireAuth;
路由
npm install react-router-dom@6
App.tsx
import { lazy, Suspense, useEffect, useState } from 'react';
import { Route, Routes } from 'react-router-dom';
import { ConfigProvider } from 'react-vant';
import { Theme, useTheme } from './components/ThemeProvider';
import PageChannel from './pages/channel';
import PageHome from './pages/home';
import PageSubscribe from './pages/subscribe';
import PageUser from './pages/user';
// const PageSubscribe = lazy(() => import('./pages/subscribe'))
// const PageChannel = lazy(() => import('./pages/channel'))
// const PageUser = lazy(() => import('./pages/user'))
const PageMember = lazy(() => import('./pages/user/member'));
function App() {
return (
<div className="App">
<Suspense fallback={<>...</>}>
<Routes>
<Route path="/" element={<PageHome />} />
<Route path="subscribe" element={<PageSubscribe />} />
<Route path="channel" element={<PageChannel />} />
<Route path="user" element={<PageUser />} />
<Route path="user/member" element={<PageMember />} />
</Routes>
</Suspense>
</div>
);
}
export default App;
main.tsx
import { BrowserRouter } from 'react-router-dom';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</React.StrictMode>
);
多语言国际化
选项
- i18next
- formatjs
- lingui
npm install react-i18next i18next i18next-browser-languagedetector --save
i18n.ts
```typescript import i18n from ‘i18next’; import LanguageDetector from ‘i18next-browser-languagedetector’; import { initReactI18next } from ‘react-i18next’; import en from ‘./i18n/en-US.json’; import zhTw from ‘./i18n/zh-TW.json’;
i18n // detect user language // learn more: https://github.com/i18next/i18next-browser-languageDetector .use(LanguageDetector) // pass the i18n instance to react-i18next. .use(initReactI18next) // init i18next // for all options read: https://www.i18next.com/overview/configuration-options .init({ debug: import.meta.env.DEV, fallbackLng: ‘zhTw’, interpolation: { escapeValue: false, // not needed for react as it escapes by default }, resources: { en: { translation: en, }, zhTw: { translation: zhTw, }, }, });
export default i18n;
`i18n/en-US.json`
```json
{
"title": "Title",
}
i18n/zh-TW.json
{
"title": "标题",
}
使用
import PageContainer from '@/components/PageContainer';
import PageContainerContent from '@/components/PageContainer/PageContainerContent';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import { NavBar } from 'react-vant';
function PageMessageGroups() {
const { t } = useTranslation();
const navigate = useNavigate();
const handleClickLeft = () => {
navigate(-1);
};
return (
<PageContainer>
<NavBar
title={t('app.navbar.group')}
fixed
placeholder
border={false}
onClickLeft={handleClickLeft}
/>
<PageContainerContent>PageMessageGroups</PageContainerContent>
</PageContainer>
);
}
export default PageMessageGroups;
template
配置太复杂了,每个项目都要这样重头做一遍吗?不用!直接从 GitLab Template
生成项目!
方案三:Next.js / Remix / Gatsby
Next.js
是成熟的 React
框架,优势在于开箱即用的 SSR
,约定式目录结构、文件路径路由、丰富的性能优化等等,比较适用于官网、强 SEO
、页面性能要求较高场景,也可用于任意通用 React
项目。
- https://nextjs.org/
- https://remix.run/
-
方案四:Umi + Ant Design Pro
Ant Design
是阿里开源的一套非常优秀的React
组件库,Umi
是类似于Next.js
的前端开发框架,也是开箱即用,内置多种最佳开发实践,可以专注于业务开发。Ant Design Pro
在基于Ant Design
组件库和Umi
框架之上,封装了一套开箱即用的中台前端/设计解决方案 - https://umijs.org/
-
初始化项目
https://pro.ant.design/zh-CN/docs/getting-started
# 使用 npm
npx create-umi myapp
按照 umi 脚手架的引导,第一步先选择 ant-design-pro:
? Select the boilerplate type (Use arrow keys)
❯ ant-design-pro - Create project with a layout-only ant-design-pro boilerplate, use together with umi block.
app - Create project with a easy boilerplate, support typescript.
block - Create a umi block.
library - Create a library with umi.
plugin - Create a umi plugin.
安装依赖:
$ cd myapp && npm install
启动项目
npm start