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)```bashnpx create-react-app my-appcd my-appnpm start# typescriptnpx 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>### 初始化项目```shellnpm create vite@latestnpm create vite@latest my-app -- --template react-ts
创建目录结构
cd srcmkdir -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-envnpx husky-init && npm installtouch .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-viewportcd stylestouch 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/openapitouch 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-loggermkdir -p src/store src/store/reducerstouch 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````tsximport 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><NavBartitle={t('app.navbar.group')}fixedplaceholderborder={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
# 使用 npmnpx 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
