【山竹记账前端-react】1.搭建前端项目


大纲链接 §

[toc]


1. 山竹记账 React 版项目介绍 ⇧

山竹项目技术栈

  • 容器 Docker
    • oh-my-env2
    • 本地开发
    • 远程部署
  • 前端 React@19.3.0
  • 后端 Rails@7.2+
  • 数据库 PostgreSQL
  • 云服务器 阿里云/腾讯云
    • 购买、配置、域名等
  • 一键部署 Bash Script

山竹项目亮点

  • 开发模式:前后端分离架构,完成前后端联调
  • 前端技术
    • 技术栈:React + TypeScript + TSX + UnoCss + Vite
    • React 技术栈最佳实践
    • 使用 React Router 做路由
    • 使用 Zustand 做状态:实现全局状态管理
    • 网络处理:
      • 使用 SWR A React Hooks library for data fetching
      • 对 Axios 进行二次封装
      • 自研 Axios Mock 模块
      • 处理跨域(CORS)问题
    • 封装 Hooks、通用业务组件
    • Vite 工程化
    • 代码分割
    • 工程化环境配置
  • 后端 & 部署相关
    • 后端框架:Rails
    • 使用 JWT 做身份认证
    • 数据库 PostgreSQL
    • 部署:Docker 容器化
    • Nginx 反向代理
    • 部署到云服务器 CDN 加速
  • 工程化与文档、开发规范
    • 开发模式:后端接口 TDD 测试驱动开发
    • 文档:编写需求文档、系统设计文档、后端自动生成接口文档
    • 最佳实践:整套项目遵循工程化最佳实践
  • 需求简单,但包含的技术流行 最佳实践 最快最小项目实践验证

2. 项目预览 ⇧

功能

  • 邮箱验证 登录 退出
  • 添加标签
  • 记账
  • 查看收入 支出
  • 统计图表

3. 项目步骤 ⇧

项目实践步骤

  • 配置开发环境
    • 前端:浏览器、Node.js、VSCode;都是跨平台 兼容性好
  • 部署到 GitHub;前端的最简部署流程
  • 创建 Snippet
  • 引入 React Router
  • 页面路由划分
  • 引入 CSS Modules 和 UnoCSS
  • 完成第一个页面
  • 手机端调试

更多步骤

  • 封装组件、封装自定义 Hook
  • 制作页面
  • 使用 JWT
  • 引入 Zustand
  • 请求库 SWR
  • 封装 Axios、封装 Mock
  • 前后端联调、处理快鱼
  • 性能优化
  • 项目总结梳理

代码实践总结

  • 每一节
    • 小结思路,再敲代码
    • 使用提供的初始代码
    • 删掉之前自己的代码,防止错误积累

4. 项目架构 ⇧

  • 主要面向手机页面
    • 记账
    • 云同步 登录
    • 可视图表

React-架构

  • 用户 -> Nginx
    • /api/v1/resources -> Rails Controller <- Modes <- PostgerSQL
    • 静态资源 /index.html 、/styles-xxx.css、/main-xxx.js -> 前端页面服务地址
    • 动态路由 /tags、/records/new

5. 开发环境搭建 ⇧

开发环境搭建

有两种搭建本项目开发环境的方式:

  • 使用 oh-my-env
    • 这是一种基于 Docker 的开发环境,你可以通过这种方式得到跟我「完全一样」的开发环境
    • 兼容 Windows、macOS、Linux
    • 基于新版 Docker
    • 已内置安装 node、pnpm、npm、zsh(部分已升级至稳定版)
  • 自己安装 node 18、VSCode 最新版、Bash 等开发工具
    • 这种方式跟你平时的开发方式没有区别,只要求版本跟我的差不多即可

容器开发环境

容器内 Linux 查找文件

  • cd ~/repos
  • 运行命令 f 回车,会搜索当前目录下的所有文件
    • 继续输入 index.tsx 会模糊查找所有匹配的文件
    • 按上下选择,按回车确认,就会在 VSCode 中打开
  • 运行命令 fd 回车,会搜索当前目录下的所有目录文件夹
    • 输入 main.tsx 选择一个回车
    • 会在命令行中自动跳转到该目录,并自动缩短显示路径 pwd
  • 执行成功:显示蓝色的点;执行失败,显示红色的点
  • 启动前端服务,自动转发端口到宿主机

6. 使用vite创建项目 ⇧

🛠️ 环境准备

1. 检查 Node.js 版本

请确保你的本地 Node.js 版本符合项目要求: * 要求版本:>=22.23.2 * 检查命令:node -v

2. 开启官方 Corepack

本项目利用 Node.js 自带的 Corepack 来管理包管理器版本,无需你全局手动安装 pnpm。请在终端执行以下命令开启它:

1
2
corepack enable
corepack prepare pnpm@11.27.0 --active

开始搭架子

  • 用 React 实现山竹前端
  • 运行 pnpm create vite 按脚手架选项选择
    • 查看 create-vite 版本,运行 npm info create-vite versions
    • 或者运行 pnpm create vite@9.2.1 react-mangosteen-1 --template react-ts
    • 可选 oxlint 或 eslint
    • 直接安装依赖即可 pnpm i
  • 运行命令 code ./react-mangosteen-1 使用一个新的窗口打开容器项目
  • 锁死版本号
    • 去除依赖包版本前缀 ^
    • 运行 pnpm config set save-prefix='' 以后安装的依赖也去除 ^
  • 安装 sass 运行命令 pnpm add -D sass-embedded

微调 package.json

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
{
  "name": "react-mangosteen-1",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "engines": {
    "node": ">=22.23.2",
    "pnpm": ">=11.27.0"
  },
  "scripts": {
    "dev": "vite --host",
    "build": "tsc -b && vite build",
    "lint": "oxlint",
    "preview": "vite preview --host",
    "preinstall": "npx only-allow pnpm"
  },
  "dependencies": {
    "react": "19.3.0",
    "react-dom": "19.3.0"
  },
  "devDependencies": {
    "@babel/core": "8.0.6",
    "@rolldown/plugin-babel": "0.2.4",
    "@types/babel__core": "7.20.5",
    "@types/node": "24.19.0",
    "@types/react": "19.3.0",
    "@types/react-dom": "19.3.0",
    "@vitejs/plugin-react": "6.1.1",
    "babel-plugin-react-compiler": "1.0.0",
    "oxlint": "1.85.0",
    "sass-embedded": "1.104.1",
    "typescript": "7.0.2",
    "vite": "8.3.1"
  }
}
  • 注意 engines 字段,限制 node 版本
  • 注意 scripts.preinstall 字段,限制包管理器为 pnpm
  • 注意启动项目添加 --host

📦 依赖安装与启动 配置完成后,你可以直接在项目根目录下执行以下命令:

安装依赖、本地开发、本地打包与预览 ⇧

⚠️ 注意:请勿使用 npm install 或 yarn install。项目中配置了拦截脚本,使用非 pnpm 命令将会导致安装失败。

1
2
3
4
5
6
7
# 进入项目中
corepack use pnpm@11.27.0
pnpm install

pnpm dev
pnpm build
pnpm preview

创建两个版本固定文件 ⇧

  • touch .nvmrc && echo "22.23.2" >> .nvmrc
  • touch .npmrc && echo 'engine-strict=true' >> .npmrc

主要依赖技术栈 ⇧

  • vite@8.3.1
  • typescript@7.0.2
  • react@19.3.0
  • react-router
  • zustand
  • uno-css
  • sass(sass-embedded@1.104.1)

7. 每个目录的作用 ⇧

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
.
├── README.md               # 项目自述文件(说明文档、运行与部署指南)
├── dist                    # 打包构建输出目录(执行 build 命令后生成的静态资源,用于生产环境部署)
├── env.d.ts                # TypeScript 环境变量声明文件(让 TS 识别 .vue 文件和 import.meta.env 的类型)
├── .oxlintrc.json          # oxlint 配置文件(定义代码代码规范、统一团队代码风格)
├── index.html              # 项目的入口 HTML 文件(Vite 应用的挂载主页)
├── package.json            # 项目元数据及依赖管理文件(定义脚本、开发依赖、生产依赖等)
├── pnpm-lock.yaml          # pnpm 依赖锁定文件(确保团队成员安装完全相同的依赖版本)
├── pnpm-workspace.yaml     # pnpm 工作区配置文件(用于多包管理/Monorepo 架构,定义子项目路径)
├── public                  # 静态资源公共目录(存放不需要 Webpack/Vite 编译的资源,如网页图标 favicon.ico)
├── script                  # 自定义脚本目录
│   └── deploy_to_github.sh # 自动化部署脚本(用于一键将项目打包并推送到 GitHub Pages 等平台)
├── src                     # 核心源代码目录
│   ├── App.tsx             # 根组件(所有页面的外壳,页面入口)
│   ├── main.tsx            # 项目核心入口文件(初始化 React 实例、挂载路由、状态管理等插件)
│   ├── pages               # 页面组件
│   ├── const               # 通用常量
│   ├── modules             # 模块化业务逻辑
│   ├── components          # 通用组件
│   ├── shared              # 共用文件
│   ├── utils               # 通用工具函数
│   ├── styles              # 全局或公共样式
│   ├── assets              # 静态资源:矢量图或位图
│   ├── apis                # 公共接口
│   ├── hooks               # 钩子文件
│   ├── types               # 通用类型
│   ├── router              # 路由配置目录
│   └── stores              # Zustand 状态管理目录(全局数据共享)
├── tsconfig.app.json       # 针对前端应用代码(src 目录)的 TypeScript 编译配置文件
├── tsconfig.json           # TypeScript 主配置文件(作为引用其它子配置的根配置)
├── tsconfig.node.json      # 针对构建工具环境(如 vite.config.ts)的 TypeScript 编译配置文件
└── vite.config.ts          # Vite 构建工具配置文件(配置插件、别名、代理、打包规则等)

# 模块化业务逻辑
modules/
└── order/                         # 以“订单模块”为例
    ├── docs/                      # 给人员和ai阅读的文档
    ├── api.ts                     # [可选] 仅属于该模块的后端接口 订单相关的请求函数(如 getOrderList)
    ├── components/                # 仅属于该模块的私有业务组件
    ├── styles/                    # 仅属于该模块的私有业务样式
    ├── hooks                      # 仅属于该模块的组合式逻辑(解耦 View 层)
    │   └── useXxx.ts              # 封装逻辑
    ├── useOrderStore | stores     # [可选] 该模块独享的全局状态管理
    ├── types/                     # 该模块涉及的 TypeScript 类型定义
    ├── utils/                     # 仅属于该模块的内部工具函数
    └── views/                     # 该模块的路由页面入口
        ├── ExampleOrderList.vue   # 订单列表页
        └── ExampleOrderDetail.vue # 订单详情页

Vite 项目在 VSCode 中根目录别名配置 @ ⇧

VSCode 重要但很少提及的别名配置

  • 按 Ctrl + Shift + P 搜索 settings
  • 打开用户编辑 settings.json
  • 添加以下字段配置

    1
    2
    3
    4
    
    {
    "js/ts.preferences.importModuleSpecifier": "non-relative",
    "js/ts.preferences.importModuleSpecifierEnding": "minimal"
    }
  • 或者按 Ctrl + Shift + P 董凯配置面板,搜索 importModule

  • 将如图两项分别设置为 non-relative 和 minimal

  • Prefers a non-relative import based on the baseUr1 or paths configured in your jsconfig.json tsconfig.json

  • 过时的答案,丼可以根据新的提示修改 Always use alias for automatic imports

alias-1 alias-2

添加路径别名 @,配置 vite.config.ts

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
import { fileURLToPath, URL } from 'node:url'
import react, { reactCompilerPreset } from '@vitejs/plugin-react'
import babel from '@rolldown/plugin-babel'
import { defineConfig } from 'vite'

// https://vite.dev/config/
export default defineConfig({
  plugins: [
    react(),
    babel({ presets: [reactCompilerPreset()] })
  ],
  server: {
    host: true
  },
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url)),
    }
  }
})

添加路径别名 @,配置 tsconfig.app.json

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
{
  "compilerOptions": {
    "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
    "target": "es2023",
    "lib": [
      "ES2023",
      "DOM"
    ],
    "module": "esnext",
    "types": [
      "vite/client"
    ],
    "allowArbitraryExtensions": true,
    "skipLibCheck": true,
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "verbatimModuleSyntax": true,
    "moduleDetection": "force",
    "noEmit": true,
    "jsx": "react-jsx",
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "erasableSyntaxOnly": true,
    "noFallthroughCasesInSwitch": true,
    "paths": {
      "@/*": [
        "./src/*"
      ]
    }
  },
  "include": [
    "src"
  ]
}

初始化改造部分文件,清除默认结构 ⇧

重命名 index.css 为 main.scss 清空默认样式

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
:root {
  --placeholder: #c7c5c5;
}

@media (prefers-color-scheme: dark) {
  :root {
    --placeholder: #c7c5c5;
  }

}
  • 对应修改引用 main.tsx

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    
    import { StrictMode } from 'react'
    import { createRoot } from 'react-dom/client'
    import './main.scss'
    import { App } from './App.tsx'
    
    const div = document.getElementById('root')!
    const root = createRoot(div)
    root.render(
    <StrictMode>
    <App />
    </StrictMode>,
    )
    

重命名 src/App.scss 清空默认样式

1
2
3
#App {
  display: flex;
}

src/App.tsx 清空默认结构代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import './App.scss'

export function App() {
  return (
    <section id="center">
      Hi
    </section>
  )
}

export default App

提交初始化代码


8. 使用GitHub Pages部署页面 ⇧

前提

  • 已有 GitHub 账号,创建一个仓库:xxx-preview 或者 xxx-publish
  • 在 oh-my-env1 容器环境中创建 .ssh 的密钥对
  • 其他略

初始提交命令

1
2
3
4
5
6
7
8
pnpm build
cd dist
git init
git add .
git commit -m 'publish'
git remote add origin git@github.com:xxx/react-mangosteen-1-preview.git
# git push -u origin master
git push -f origin master:master

仓库页面 Settings 面板

  • 搜索 Pages
  • Branch 下的选项选择 master
  • 点击 Save

等待 GitHub 自动部署完毕

  • GitHub Pages 出现网址 https://xmasuhai.xyz/react-mangosteen-1-preview/
  • 访问该网站,页面空白,打开控制台查看报错 index-xxx.js 报 404
  • 访问的资源是 <script type="module" crossorigin="" src="/assets/index-CWcRINx3.js"></script>
  • 而首页地址为 xxx/react-mangosteen-1-preview/index.html
  • 正确路径为 /react-mangosteen-1-preview/assets/index-xxx.js
  • 尝试运行 pnpm run build --base=fyh
  • 查看 index.html 中资源的路径多了前缀 /fyh/assets/index-CWcRINx3.js
  • 可以改为该项目的仓库名,注意前后添加斜杠:/react-mangosteen-1-preview/
    • 在 package.json 中修改 "build": "tsc -b && vite build --base=/react-mangosteen-1-preview/"
  • 点击 index.html 确保当前目录在最外层,创建目录 bin/deploy_to_github.sh

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    
    #!/usr/bin/env bash
    
    rm -rf dist
    pnpm build
    cd dist
    git init
    git add .
    git commit -m 'deploy'
    git remote add origin git@github.com:xmasuhai/react-mangosteen-1-preview.git
    git push -f origin master:master
    cd -
    echo "https://xmasuhai.xyz/react-mangosteen-1-preview/"
  • 运行 sh bin/deploy_to_github.sh

  • 添加可执行权限 chmod -x bin/deploy_to_github.sh

  • 写入package.json脚本 "deploy": "sh bin/deploy_to_github.sh",之后运行 pnpm deploy 即可

  • 将网址写到仓库的描述中 https://xmasuhai.xyz/react-mangosteen-1-preview/

  • 查看网页就消除报错了

正确的配置: ``


9. Snippets 之 typescriptreact.json ⇧

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
{
  "React.FC": {
    "prefix": "fc",
    "body": [
      "export const $1: React.FC = () => {",
      "  return (",
      "    <div>$1 $2</div>",
      "  )",
      "}"
    ]
  },
  "Vue Component": {
    "prefix": "vc",
    "body": [
      "import { defineComponent } from 'vue';",
      "export const $1 = defineComponent({",
      "  setup: (props, context) => {",
      "    return () => (",
      "      <div>$2</div>",
      "    )",
      "  }",
      "})",
    ]
  },
  "Vue Component With Props": {
    "prefix": "vcp",
    "body": [
      "import { defineComponent, PropType } from 'vue';",
      "export const $1 = defineComponent({",
      "  props: {",
      "    name: {",
      "      type: String as PropType<string>",
      "    }",
      "  },",
      "  setup: (props, context) => {",
      "    return () => (",
      "      <div>$2</div>",
      "    )",
      "  }",
      "})",
    ]
  },
  "Vue Component With Props and Styles": {
    "prefix": "vcps",
    "body": [
      "import { defineComponent, PropType } from 'vue';",
      "import s from './$1.module.scss';",
      "export const $1 = defineComponent({",
      "  props: {",
      "    name: {",
      "      type: String as PropType<string>",
      "    }",
      "  },",
      "  setup: (props, context) => {",
      "    return () => (",
      "      <div class={s.wrapper}>$2</div>",
      "    )",
      "  }",
      "})",
    ]
  },
  "Import Module SCSS": {
    "prefix": "ims",
    "body": [
      "import s from './$1.module.scss';",
    ]
  }
}

我的最终版

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
{
	"React.FC": {
    "prefix": "fc",
    "description": "React Functional Component",
    "body": [
      "export const $TM_FILENAME_BASE: React.FC = () => {",
      "",
      "  return (",
      "    <div>$1</div>",
      "  )",
      "}"
    ]
  },
  "Import Module SCSS": {
    "prefix": "ims",
    "body": [
      "import s from './$TM_FILENAME_BASE.module.scss';"
    ]
  }
}

10. 使用Snippet加速开发 ⇧


11. 使用ESLint规范你的代码 ⇧


12. 使用Husky规范你的代码提交 ⇧

参考


·未完待续·

参考文章

相关文章


  • 作者: Joel
  • 文章链接:
  • 版权声明
  • 非自由转载-非商用-非衍生-保持署名