Summary

搭建 React 项目的几种方案介绍。以及 React 生态圈内会常用到的一些技术方案选择,如:样式方案、CSS-in-JS、路由、网络请求、全局状态管理、项目开发体验和工程化(eslint, prettier, husky + lint-staged)、多语言国际化、动画、暗黑模式

  1. create-react-app
  2. vite
  3. ant-design-pro, umi
  4. Next.js / Remix / Gatsby

    Prerequisites

  1. CodePen
  2. CodeSandbox(丰富的初始化模版快速创建项目,可在线编辑并允许,可分享链接)
  3. StackBlitz (丰富的初始化模版快速创建项目,可在线编辑并允许,可分享链接) https://vite.new/
  4. github.dev (在任意 Github 项目点击 . 可以直接在网页打开,方便查看项目源码)

    React 语法

    ReactVue是目前最流行的前端 UI 框架,由于 React 是一个蛮大的话题,可能需要自行花费一定时间学习才能正确明白和适用。所以语法部分这里不多介绍,就简单介绍一些基本的语法。
  1. JSX

JSXJavaScript语言的扩展,类似于 html 标签,可以用来结构化的展示页面组件构成,具于 JavaScript 灵活的特点和运算能力。

  1. const element = <h1>Hello, world!</h1>;

Hello World

  1. const root = ReactDOM.createRoot(document.getElementById('root'));
  2. root.render(<h1>Hello, world!</h1>);

参数

  1. const element = <a href="https://www.reactjs.org"> link </a>;

表达式

  1. function formatName(user) {
  2. return user.firstName + ' ' + user.lastName;
  3. }
  4. const user = {
  5. firstName: 'Harper',
  6. lastName: 'Perez'
  7. };
  8. const element = (
  9. <h1>
  10. Hello, {formatName(user)}!
  11. </h1>
  12. );

最终 JSX 也会被编译成 JavaScript

  1. React.createElement('div', {style: 'color: red;'})
  1. 组件

React 中,最重要的概念就是组件,每个页面可以由数个组件组成,每个组件可以拥有自己独特的功能特性和界面展示。
函数组件

  1. function Button() {
  2. return <button className="button">Hello</button>
  3. }

类组件

  1. class Button extends React.Component {
  2. // ...省略生命周期等方法
  3. render() {
  4. return <button className="button">Hello</button>
  5. }
  6. }
  1. propsstate

给组件传参数和组件间通信通过 props

  1. function Welcome(props) {
  2. return <h1>Hello, {props.name}</h1>;
  3. }

组件内部可以维护自由状态 state

  1. class Clock extends React.Component {
  2. constructor(props) {
  3. super(props);
  4. this.state = {date: new Date()};
  5. }
  6. render() {
  7. return (
  8. <div>
  9. <h1>Hello, world!</h1>
  10. <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
  11. </div>
  12. );
  13. }
  14. }

propsstate 的改变都会触发组件的 rerender

  1. 事件处理

    1. <button onClick={activateLasers}>
    2. Activate Lasers
    3. </button>
  2. 生命周期和 hooks

类组件才有生命周期,在 React 16.8 未引入 hook 概念之前,函数组件无法由于 state,所以功能单一并且只能用于展示组件。但是 hooks 使得函数组件能够维护自身 state 并且将各个逻辑抽取封装成 hooks 函数使得组件逻辑更加易读、代码复用高、非常有利于项目维护,目前都是会采用函数组件加 hooks 写法编写 React 项目。

function Example() { // Declare a new state variable, which we’ll call “count” const [count, setCount] = useState(0);

return (

You clicked {count} times

); }

  1. <a name="35d038c8"></a>
  2. ## 方案一:Create React app
  3. `create-react-app` 是 `React` 团队官方提供的一套快速搭建和初始化 `React` 项目的脚手架工具。基于 `Webpack` (前端最成熟、适用范围最广的项目打包工具) 无需开发者手动从零开始配置,也提供一定的项目配置能力。
  4. <a name="6e11ac76"></a>
  5. ### 初始化项目
  6. - [https://create-react-app.dev/docs/getting-started](https://create-react-app.dev/docs/getting-started)
  7. ```bash
  8. npx create-react-app my-app
  9. cd my-app
  10. npm start
  11. # typescript
  12. npx create-react-app my-app --template typescript
  13. # 其他模版
  14. npx create-react-app my-app --template [template-name]

项目目录结构

  1. my-app
  2. ├── README.md
  3. ├── node_modules # 依赖
  4. ├── package.json # 项目信息、运行命令、依赖管理
  5. ├── .gitignore
  6. ├── public # 公共资源路径
  7. ├── favicon.ico
  8. ├── index.html # html 模版文件
  9. ├── logo192.png
  10. ├── logo512.png
  11. ├── manifest.json
  12. └── robots.txt
  13. └── src # 源代码目录
  14. ├── App.css
  15. ├── App.js # 主组件入口
  16. ├── App.test.js
  17. ├── index.css
  18. ├── index.js # 项目入口文件
  19. ├── logo.svg
  20. ├── serviceWorker.js
  21. └── setupTests.js

命令

  1. # 运行项目
  2. npm start
  3. # 打包
  4. npm run build

样式方案

  1. css/less/scss
    css 和常见的 css 预处理器都是默认支持的,这也是最常见的写法。只需要新建样式文件,然后组件内引入该样式文件通过 className 来对应样式,基本相同于 html 原生的写法
    1. .button {
    2. color: red;
    3. }
    ```javascript import ‘./button.less’;

function Button() { return }

  1. 2. `CSS-in-JS` 方案之 `emotion`
  2. `CSS-in-JS` 方案的优势在于样式和组件在同一个文件内,编写组件时不需要不停切换文件。并且由于使用 `JavaScript` 编写样式可以利用其灵活性,变量,主题切换,样式复用都会变得更简单。各种 `CSS-in-JS` 库的出现都简化了样式和组件编写的过程。常见的有 [styled-components](https://styled-components.com/),[Emotion](https://emotion.sh/), [vanilla-extract](https://vanilla-extract.style/) 等
  3. <a name="a45c480d"></a>
  4. ### Create React App 优势
  5. 1. 官方团队提供和维护,较稳定
  6. 2. 无需配置
  7. <a name="3e98931d"></a>
  8. ### Create React App 缺点
  9. 1. 缺少新功能
  10. 2. 项目较大之后打包速度慢,需要一定优化手段
  11. 3. 虽然无需配置,但是其实是将各种工具的配置入口隐藏,需要一些自定义配置时可能无法做到,比如配置 `postcss` 插件
  12. 4. 周边生态缺少维护,如用来自定义配置的 `craco` `create-react-app` 更新到 5 之后很长时间都没有跟进支持,[主开发者无精力继续维护项目等](https://github.com/dilanx/craco/issues/415)
  13. <a name="a965b140"></a>
  14. ## 方案二:Vite
  15. <a name="be407156"></a>
  16. ### Vite 优势
  17. 1. 快。开发模式快,编译快(`ESM` + `esbuild`(`go` 语言写的 `JavaScript` 编译器) + 依赖预编译等)
  18. 2. 与框架无关,可以用于任意前端框架项目
  19. 3. 配置灵活,功能丰富
  20. 4. 打包基于 `rollup`,有丰富的插件
  21. <a name="6e11ac76-1"></a>
  22. ### 初始化项目
  23. ```shell
  24. npm create vite@latest
  25. npm create vite@latest my-app -- --template react-ts

创建目录结构

  1. cd src
  2. mkdir -p assets assets/icons assets/images components constants pages pages/home styles utils

工程化

  1. eslint
  2. prettier
  3. husky + lint-staged
  4. rollup-plugin-visualizer 用于分析打包依赖大小
  1. npm i -D eslint eslint-config-react-app eslint-config-prettier prettier lint-staged rollup-plugin-visualizer @types/node@16 cross-env
  2. npx husky-init && npm install
  3. touch .eslintrc .eslintignore .prettierrc .prettierignore

.eslintrc

  1. {
  2. "extends": [
  3. "react-app",
  4. "react-app/jest",
  5. "prettier"
  6. ]
  7. }

.prettierrc

  1. {
  2. "singleQuote": true,
  3. }

package.json

  1. {
  2. "scripts": {
  3. "analyze": "tsc && cross-env ANALYZE=true vite build",
  4. },
  5. "lint-staged": {
  6. "**/*.{js,jsx,ts,tsx}": "eslint --fix --ext .js,.jsx,.ts,.tsx",
  7. "**/*.{js,jsx,tsx,ts,less,css,json}": [
  8. "prettier --write"
  9. ]
  10. }
  11. }

.husky/pre-commit

  1. #!/usr/bin/env sh
  2. . "$(dirname -- "$0")/_/husky.sh"
  3. npx lint-staged

vite.config.ts

  1. import { visualizer } from 'rollup-plugin-visualizer';
  2. import { defineConfig, splitVendorChunkPlugin } from 'vite';
  3. // 打包生产环境才引入的插件
  4. if (process.env.NODE_ENV === 'production') {
  5. process.env.ANALYZE &&
  6. plugins.push(
  7. visualizer({
  8. open: true,
  9. gzipSize: true,
  10. brotliSize: true,
  11. })
  12. );
  13. }
  14. // https://vitejs.dev/config/
  15. export default defineConfig({
  16. plugins: [splitVendorChunkPlugin(), react(), ...plugins],
  17. resolve: {
  18. alias: [
  19. {
  20. find: '@',
  21. replacement: '/src',
  22. },
  23. ],
  24. }
  25. });

组件库

选项

  1. npm i react-vant@next

使用

  1. import { Button } from 'react-vant';

样式方案/样式覆盖/暗黑模式

样式方案还是采用 css + less + className。需要考虑暗黑模式,所有样式都用 CSS 变量,然后根据 html 标签属性切换
由于是 H5 项目,需要考虑不同设备屏幕大小适配,引入插件 postcss-px-to-viewport 可以自动将 px 单位转换为 vw 单位

  1. npm i -D less postcss-px-to-viewport
  2. cd styles
  3. touch css-variable.less global.less index.less react-vant.less variables.less common.less

postcss.config.js

  1. module.exports = {
  2. plugins: {
  3. 'postcss-px-to-viewport': {
  4. viewportWidth: 375,
  5. },
  6. },
  7. }

index.less

  1. @import './css-variables.less';
  2. @import './react-vant.less';
  3. @import './global.less';
  4. @import './common.less';

css-variables.less

  1. :root {
  2. --primary-color: #333;
  3. --page-bg: #f5f5f5;
  4. }
  5. :root[data-theme='dark'] {
  6. --primary-color: #fff;
  7. --page-bg: #0a1929;
  8. }

图标引入

图标都采用 SVG 可以方便使用、修改大小、颜色,下载到项目本地,通过 svgrvite-plugin-svgr 可以当成 React 组件直接引入

  1. npm i -D vite-plugin-svgr

vite.config.ts

  1. import svgr from 'vite-plugin-svgr';
  2. // https://vitejs.dev/config/
  3. export default defineConfig({
  4. plugins: [svgr({
  5. // 这个选项将组件导出到 default 而不是 ReactComponent
  6. // exportAsDefault: true,
  7. svgrOptions: {
  8. icon: true,
  9. // 删除 svg fill 颜色
  10. replaceAttrValues: {
  11. none: 'currentColor',
  12. black: 'currentColor',
  13. '#62626B': 'currentColor',
  14. '#686872': 'currentColor',
  15. '#0055FF': 'currentColor',
  16. '#3F7FFF': 'currentColor',
  17. '#A8A8A8': 'currentColor',
  18. '#1A1A1A': 'currentColor',
  19. '#EA4D44': 'currentColor',
  20. },
  21. },
  22. }),],
  23. });

vite.env.d.ts 增加这个类型可以帮助 TS 识别 svg 导入

  1. /// <reference types="vite-plugin-svgr/client" />

使用

  1. import { ReactComponent as Logo } from './logo.svg'

网络请求

使用熟悉的 axios 通过拦截器封装网络请求,网络请求代码部分使用工具 openapi2typescript 通过后端提供的 Swagger 文档自动生成

  1. npm i -D @umijs/openapi
  2. touch openapi.config.js
  3. # 在 package.json 中增加了命令之后可以每次都通过此命令生成网络请求代码
  4. npm run openapi

package.json

  1. {
  2. "scripts": {
  3. "openapi": "node openapi.config.js",
  4. }
  5. }

openapi.config.js

  1. const { generateService } = require('@umijs/openapi')
  2. generateService({
  3. schemaPath: 'http://petstore.swagger.io/v2/swagger.json',
  4. serversPath: './servers',
  5. })
  6. /**
  7. * 生成app端接口
  8. */
  9. generateService({
  10. requestLibPath: "import request from '@/utils/request'",
  11. schemaPath: 'http://192.168.2.147:9088/v3/api-docs?group=app端汇总',
  12. serversPath: './src/services',
  13. projectName: 'app',
  14. });

src/utils/request.ts

  1. import { store } from '@/store';
  2. import { toggleIsLogin } from '@/store/reducers/user';
  3. import axios, { AxiosRequestConfig } from 'axios';
  4. import { Toast } from 'react-vant';
  5. import { getUserToken, removeUserToken, TOKEN_KEY } from './auth';
  6. export const API_CODES = {
  7. SUCCESS: 200,
  8. UNAUTHORIZED: 401,
  9. FORBIDDEN: 403,
  10. };
  11. export const isRequestSuccess = (res: API.ApiResponse) => {
  12. return res && res.code === API_CODES.SUCCESS;
  13. };
  14. export const isNeedLogin = (res: API.ApiResponse) => {
  15. return res && res.code === API_CODES.SUCCESS;
  16. };
  17. const request = axios.create({});
  18. request.interceptors.request.use(
  19. (config) => {
  20. const token = getUserToken();
  21. if (token && config.headers) {
  22. config.headers[TOKEN_KEY] = token;
  23. }
  24. return config;
  25. },
  26. (error) => Promise.reject(error)
  27. );
  28. request.interceptors.response.use(
  29. (response) => {
  30. const res = response.data;
  31. if (isRequestSuccess(res)) {
  32. return response; // axios 类型不支持返回 response 中的 data,将 response 整个返回,后续取数据需要 res.data 中获取
  33. }
  34. // 如果用户未登录跳转至登录页面
  35. if (isNeedLogin(res)) {
  36. store.dispatch(toggleIsLogin(false));
  37. removeUserToken();
  38. window.location.reload(); // TODO: toLogin()
  39. }
  40. const errMsg = res.msg || '请求错误';
  41. Toast(errMsg);
  42. return Promise.reject(response);
  43. },
  44. async (error) => {
  45. let errMsg = '请求错误';
  46. if (error.response) {
  47. const { data } = error.response;
  48. errMsg = data.msg || errMsg;
  49. }
  50. Toast(errMsg);
  51. return Promise.reject(error);
  52. }
  53. );
  54. /**
  55. * 网络请求,封装后的 axios
  56. * 1. AxiosInstance 不支持泛型,openapi2typescript 生成的请求会带上请求接口返回数据类型,
  57. * 2. openapi2typescript 默认采用 umi_request 模板,会添加额外参数 requestType, axios 不支持该参数
  58. * @param {string} url
  59. * @param {AxiosRequestConfig<R>} config
  60. * @returns
  61. */
  62. function requestMethod<T>(
  63. url: string,
  64. config: AxiosRequestConfig<any> & { requestType?: string }
  65. ) {
  66. const _config = {
  67. ...config,
  68. };
  69. if (config.requestType === 'json') {
  70. _config.headers = {
  71. Accept: 'application/json',
  72. 'Content-Type': 'application/json;charset=UTF-8',
  73. ..._config.headers,
  74. };
  75. } else if (config.requestType === 'form') {
  76. _config.headers = {
  77. Accept: 'application/json',
  78. 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
  79. ..._config.headers,
  80. };
  81. }
  82. delete _config.requestType;
  83. return request.request<T>({
  84. url,
  85. ..._config,
  86. });
  87. }
  88. export default requestMethod;

全局状态管理

用于保存一些需要全局存储的信息,比用用户登录状态、用户信息等

选项

  • Redux
  • Mobx
  • zustand
  • useReducer, Context
    1. npm i redux react-redux @reduxjs/toolkit redux-logger
    2. mkdir -p src/store src/store/reducers
    3. 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; export type AppThunk = ThunkAction< ReturnType, RootState, unknown, Action

; `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(); export const useAppSelector: TypedUseSelectorHook = useSelector;

  1. `main.tsx`
  2. ```tsx
  3. import React from 'react';
  4. import ReactDOM from 'react-dom/client';
  5. import { Provider } from 'react-redux';
  6. import App from './App';
  7. import { store } from './store';
  8. import './styles/index.less';
  9. ReactDOM.createRoot(document.getElementById('root')!).render(
  10. <React.StrictMode>
  11. <Provider store={store}>
  12. <App />
  13. </Provider>
  14. </React.StrictMode>
  15. );

使用

  1. import { useAppDispatch, useAppSelector } from '@/store/hooks';
  2. import { fetchUserInfo } from '@/store/reducers/user';
  3. import { useEffect } from 'react';
  4. import { Navigate, useLocation } from 'react-router-dom';
  5. function RequireAuth({ children }: { children: JSX.Element }) {
  6. const location = useLocation();
  7. const isLogin = useAppSelector((state) => state.user.isLogin);
  8. const userInfo = useAppSelector((state) => state.user.userInfo);
  9. const dispatch = useAppDispatch();
  10. useEffect(() => {
  11. if (isLogin && !userInfo) {
  12. dispatch(fetchUserInfo());
  13. }
  14. }, [dispatch, isLogin, userInfo]);
  15. if (!isLogin) {
  16. // Redirect them to the /login page, but save the current location they were
  17. // trying to go to when they were redirected. This allows us to send them
  18. // along to that page after they login, which is a nicer user experience
  19. // than dropping them off on the home page.
  20. return <Navigate to="/login" state={{ from: location }} replace />;
  21. }
  22. return children;
  23. }
  24. export default RequireAuth;

路由

  1. npm install react-router-dom@6

App.tsx

  1. import { lazy, Suspense, useEffect, useState } from 'react';
  2. import { Route, Routes } from 'react-router-dom';
  3. import { ConfigProvider } from 'react-vant';
  4. import { Theme, useTheme } from './components/ThemeProvider';
  5. import PageChannel from './pages/channel';
  6. import PageHome from './pages/home';
  7. import PageSubscribe from './pages/subscribe';
  8. import PageUser from './pages/user';
  9. // const PageSubscribe = lazy(() => import('./pages/subscribe'))
  10. // const PageChannel = lazy(() => import('./pages/channel'))
  11. // const PageUser = lazy(() => import('./pages/user'))
  12. const PageMember = lazy(() => import('./pages/user/member'));
  13. function App() {
  14. return (
  15. <div className="App">
  16. <Suspense fallback={<>...</>}>
  17. <Routes>
  18. <Route path="/" element={<PageHome />} />
  19. <Route path="subscribe" element={<PageSubscribe />} />
  20. <Route path="channel" element={<PageChannel />} />
  21. <Route path="user" element={<PageUser />} />
  22. <Route path="user/member" element={<PageMember />} />
  23. </Routes>
  24. </Suspense>
  25. </div>
  26. );
  27. }
  28. export default App;

main.tsx

  1. import { BrowserRouter } from 'react-router-dom';
  2. ReactDOM.createRoot(document.getElementById('root')!).render(
  3. <React.StrictMode>
  4. <BrowserRouter>
  5. <App />
  6. </BrowserRouter>
  7. </React.StrictMode>
  8. );

多语言国际化

选项

  • i18next
  • formatjs
  • lingui
    1. 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;

  1. `i18n/en-US.json`
  2. ```json
  3. {
  4. "title": "Title",
  5. }

i18n/zh-TW.json

  1. {
  2. "title": "标题",
  3. }

使用

  1. import PageContainer from '@/components/PageContainer';
  2. import PageContainerContent from '@/components/PageContainer/PageContainerContent';
  3. import { useTranslation } from 'react-i18next';
  4. import { useNavigate } from 'react-router-dom';
  5. import { NavBar } from 'react-vant';
  6. function PageMessageGroups() {
  7. const { t } = useTranslation();
  8. const navigate = useNavigate();
  9. const handleClickLeft = () => {
  10. navigate(-1);
  11. };
  12. return (
  13. <PageContainer>
  14. <NavBar
  15. title={t('app.navbar.group')}
  16. fixed
  17. placeholder
  18. border={false}
  19. onClickLeft={handleClickLeft}
  20. />
  21. <PageContainerContent>PageMessageGroups</PageContainerContent>
  22. </PageContainer>
  23. );
  24. }
  25. export default PageMessageGroups;

template

配置太复杂了,每个项目都要这样重头做一遍吗?不用!直接从 GitLab Template 生成项目!

方案三:Next.js / Remix / Gatsby

Next.js 是成熟的 React 框架,优势在于开箱即用的 SSR,约定式目录结构、文件路径路由、丰富的性能优化等等,比较适用于官网、强 SEO、页面性能要求较高场景,也可用于任意通用 React 项目。

  • https://nextjs.org/
  • https://remix.run/
  • https://www.gatsbyjs.com/

    方案四:Umi + Ant Design Pro

    Ant Design 是阿里开源的一套非常优秀的 React 组件库,Umi 是类似于 Next.js 的前端开发框架,也是开箱即用,内置多种最佳开发实践,可以专注于业务开发。
    Ant Design Pro 在基于 Ant Design 组件库和 Umi 框架之上,封装了一套开箱即用的中台前端/设计解决方案

  • https://ant.design/index-cn

  • https://umijs.org/
  • https://pro.ant.design/

    初始化项目

  • https://pro.ant.design/zh-CN/docs/getting-started

    1. # 使用 npm
    2. npx create-umi myapp

    按照 umi 脚手架的引导,第一步先选择 ant-design-pro:

    1. ? Select the boilerplate type (Use arrow keys)
    2. ant-design-pro - Create project with a layout-only ant-design-pro boilerplate, use together with umi block.
    3. app - Create project with a easy boilerplate, support typescript.
    4. block - Create a umi block.
    5. library - Create a library with umi.
    6. plugin - Create a umi plugin.

    安装依赖:

    1. $ cd myapp && npm install

    启动项目

    1. npm start