likes
comments
collection
share

vite-react-ts从0到1搭建

作者站长头像
站长
· 阅读数 11

前言:React项目一般都是基于create-react-app等脚手架快速搭建,初始化项目。现在有点空闲的时间,琢磨如何从0到1进行搭建,加深自己对搭建项目的流程和熟悉。

搭建基础项目

一、初始化&基本配置

npm init && git init

修改package.json

{
  "name": "react-ts-web",
  "version": "1.0.0",
  "description": "a react typescript web template",
  "type": "module",
  "scripts": {
    "test": "echo "Error: no test specified" && exit 1"
  },
  "keywords": [
    "react",
    "typescript",
    "vite"
  ],
  "homepage": "https://github.com/xxx/xxx",
  "bugs": {
    "url": "https://github.com/xxx/xxx/issues"
  },
  "author": "xxx <xxx@xx.com>",
  "license": "ISC"
}

二、安装核心依赖,搭建开发环境

1. 安装依赖

pnpm add react react-dom && pnpm add -D vite typescript @types/react-dom @types/react @types/node

2. tsconfig.json初始化,修改

npx tsc --init
{
  "references": [{ "path": "./tsconfig.vite.json" }], // 指定工程引用依赖
  "compilerOptions": {
    "target": "ES2020", // 指定编译的ECMAScript目标版本
    "lib": ["ES2020", "DOM", "DOM.Iterable"], // 编译过程中需要引入的库文件的列表
    "jsx": "react-jsx", // 指定 jsx 代码的生成,无需在每个jsx文件中引入React
    "module": "ESNext", // 指定生成哪个模块系统代码
    "moduleResolution": "Node", // 决定如何处理模块
    "allowImportingTsExtensions": true, // 允许TypeScript文件通过TypeScript特定的扩展名如.ts, .mts, 或 .tsx互相导入
    "resolveJsonModule": true, // 允许引入 JSON 文件
    "allowJs": true, // 是否允许编译javascript文件
    "noEmit": true, // 设置是否输出 js 文件,一般是设置为 false,将打包等工作交给 vite/webpack 等工具
    "isolatedModules": true, // 将每个文件做为单独的模块
    "esModuleInterop": true, // 支持合成模块的默认导入
    "forceConsistentCasingInFileNames": true, // forceConsistentCasingInFileNames
    "strict": true, // 启用所用严格的类型检查
    "skipLibCheck": true, // 跳过对 .d.ts 文件的类型检查
    "types": ["node", "vite/client"],
    "baseUrl": ".", // 用于设置解析非相对模块名称的基本目录,相对模块不会受到baseUrl的影响
    "paths": {
      // 用于设置模块名到基于baseUrl的路径映射
      "@/*": ["./src/*"]
    }
  },
  "include": ["src", "@types"],
  "exclude": ["node_modules"]
}

3. 新增tsconfig.vite.json

{
  "compilerOptions": {
    "composite": true,
    "skipLibCheck": true,
    "module": "ESNext",
    "moduleResolution": "bundler",
    "allowSyntheticDefaultImports": true
  },
  "include": ["vite.config.ts"]
}

作用: tsconfig.vite.json 专门为vite.config.ts提供的ts配置文件。 单独配置原因: 运行环境不同,项目的代码运行在浏览器环境,而vite.config.ts运行在Node环境。相应的两者需要的接口类型也各不一样。

4. 按照所示,搭建基础架子

vite-react-ts从0到1搭建

App.tsx

export default function App() {
  return (
    <div>
      <h1>Welcome to React Web.</h1>
    </div>
  );
}

main.tsx

import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';

const root = createRoot(document.querySelector('#app')!);
root.render(
  <StrictMode>
    <App />
  </StrictMode>,
);

使用 StrictMode 来启用组件树内部的额外开发行为和警告

vite.config.ts

import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";

export default defineConfig({
  plugins: [react()],
});

修改 package.json, 添加 vite 启动、打包脚本和 ts 类型检测,内容太多就只展示新增内容

{
  ...
  "scripts": {
    "serve": "vite",
    "build": "vite build",
    "tsc": "tsc --noEmit"
  },
}

新增 .gitignore。git提交时,忽略文件提交

node_modules
dist
.DS_Store

三、配置ESLint + prettier

eslint: 代码检查工具,主要用来发现代码错误、统一代码风格。 prettier: 对代码进行格式化,并不关注代码质量潜在问题的检查,倾向于团队的代码风格的规范或统一。

1. ESLint 配置

这里的 eslint 配置个人比较喜欢用json,就采用了json格式

2. 将 prettier 作为 ESLint 的规则来使用,让 ESLint 托管 prettier。

pnpm add -D eslint-config-prettier prettier eslint-plugin-prettier eslint-config-prettier

修改 .eslintrc.json

{
  "root": true,
  "parserOptions": {
    "project": ["./tsconfig.json", "./tsconfig.vite.json"]
  },
  "settings": {
    "react": {
      "version": "detect"
    }
  },
  "extends": ["alloy", "alloy/react", "alloy/typescript", "prettier"],
  "plugins": ["prettier"],
  "env": {
    "browser": true
  },
  "globals": {},
  "rules": {
    "prettier/prettier": "error"
  },
  "ignorePatterns": ["dist", "node_modules"]
}

新增 .prettierrc

"eslint-config-alloy/.prettierrc.js"

3. vite.config.ts配置ESLint

import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
+ import eslint from 'vite-plugin-eslint'

export default defineConfig({
-   plugins: [react()]
+   plugins: [react(), eslint()],
});

四、配置husky + lint-staged

husky: 给git添加hook钩子,在特定的 Git 操作触发之前或之后执行预定义的脚本 lint-staged: 在 Git 暂存区中运行指定脚本的工具。它通常与 Husky 一起使用

1. 安装husky、lint-staged依赖

pnpm dlx husky-init && pnpm install
pnpm add -D lint-staged

2. 新增 .lintstagedrc.json

{
  "*.{js,jsx}": ["eslint --fix"],
  "*.{ts,tsx}": ["eslint --fix", "bash -c tsc"]
}

通过bash -c tsc,让tsc识别项目中的tsconfig.json配置

五、git commit 规范提交

1. 全局安装 commitizen

pnpm add commitizen -g

2. 项目根目录安装

pnpm add -D cz-git

3. package.json新增内容

{
  ...
  "scripts": {
    "commit": "git-cz"  
  },
 "config": {
   "commitizen": {
     "path": "node_modules/cz-git"
   }
  },  
}

4. 添加commitlint.config.cjs

/** @type {import('cz-git').UserConfig} */

module.exports = {
  rules: {
    // @see: https://commitlint.js.org/#/reference-rules
  },
  prompt: {
    alias: { fd: 'docs: fix typos' },
    messages: {
      type: '选择你要提交的类型 :',
      scope: '选择一个提交范围(可选):',
      customScope: '请输入自定义的提交范围 :',
      subject: '填写简短精炼的变更描述 :\n',
      body: '填写更加详细的变更描述(可选)。使用 "|" 换行 :\n',
      breaking: '列举非兼容性重大的变更(可选)。使用 "|" 换行 :\n',
      footerPrefixesSelect: '选择关联issue前缀(可选):',
      customFooterPrefix: '输入自定义issue前缀 :',
      footer: '列举关联issue (可选) 例如: #31, #I3244 :\n',
      confirmCommit: '是否提交或修改commit ?'
    },
    types: [
      { value: 'feat', name: 'feat:     新增功能 | A new feature' },
      { value: 'fix', name: 'fix:      修复缺陷 | A bug fix' },
      { value: 'docs', name: 'docs:     文档更新 | Documentation only changes' },
      { value: 'style', name: 'style:    代码格式 | Changes that do not affect the meaning of the code' },
      { value: 'refactor', name: 'refactor: 代码重构 | A code change that neither fixes a bug nor adds a feature' },
      { value: 'perf', name: 'perf:     性能提升 | A code change that improves performance' },
      { value: 'test', name: 'test:     测试相关 | Adding missing tests or correcting existing tests' },
      { value: 'build', name: 'build:    构建相关 | Changes that affect the build system or external dependencies' },
      { value: 'ci', name: 'ci:       持续集成 | Changes to our CI configuration files and scripts' },
      { value: 'revert', name: 'revert:   回退代码 | Revert to a commit' },
      { value: 'chore', name: 'chore:    其他修改 | Other changes that do not modify src or test files' },
    ],
    useEmoji: false,
    emojiAlign: 'center',
    themeColorCode: '',
    scopes: [],
    allowCustomScopes: true,
    allowEmptyScopes: true,
    customScopesAlign: 'bottom',
    customScopesAlias: 'custom',
    emptyScopesAlias: 'empty',
    upperCaseSubject: false,
    markBreakingChangeMode: false,
    allowBreakingChanges: ['feat', 'fix'],
    breaklineNumber: 100,
    breaklineChar: '|',
    skipQuestions: [],
    issuePrefixes: [
      // 如果使用 gitee 作为开发管理
      { value: 'link', name: 'link:     链接 ISSUES 进行中' },
      { value: 'closed', name: 'closed:   标记 ISSUES 已完成' }
    ],
    customIssuePrefixAlign: 'top',
    emptyIssuePrefixAlias: 'skip',
    customIssuePrefixAlias: 'custom',
    allowCustomIssuePrefix: true,
    allowEmptyIssuePrefix: true,
    confirmColorize: true,
    maxHeaderLength: Infinity,
    maxSubjectLength: Infinity,
    minSubjectLength: 0,
    scopeOverrides: undefined,
    defaultBody: '',
    defaultIssues: '',
    defaultScope: '',
    defaultSubject: ''
  }
}

六、添加changelog

pnpm add standard-version

1. package.json添加脚本

{
  ...
  "release": "standard-version"
}

2. 首次生成changelog

version:1.0.0版本

npx standard-version --first-release

功能

一、SASS支持

pnpm add -D sass

1. 新增文件 src/App.scss

.main {
  h1 {
    color: red;
  }
}

2. src/App.tsx修改

import './App.scss';

export default function App() {
  return (
    <div className="main">
      <h1>Welcome to React Web.</h1>
    </div>
  );
}

二、图片支持

1. 新增 ./src/assets/react.png

2. src/App.tsx修改

import './App.scss';
import ReactLogo from './assets/React.png';

export default function App() {
  return (
    <div className="main">
      <h1>Welcome to React Web.</h1>
      <img src={ReactLogo} alt="logo" />
    </div>
  );
}

3. 新增@types/assets.d.ts

declare module '*.png';
declare module '*.svg';
declare module '*.jpeg';
declare module '*.jpg';

三、设置路径别名

1. vite.config.ts修改

import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import eslint from 'vite-plugin-eslint';
+ import path from 'path';

export default defineConfig({
+ resolve: {
+   alias: {
+     '@': path.resolve(__dirname, 'src'),
+   },
+   extensions: ['.js', '.jsx', '.ts', '.tsx'],
+ },
  plugins: [react(), eslint()],
});

2. eslint 修改

pnpm add eslint-import-resolver-alias

.eslintrc.json

{
  "root": true,
  "parserOptions": {
    "project": ["./tsconfig.json", "./tsconfig.vite.json"]
  },
  "settings": {
    "react": {
      "version": "detect"
    },
+   "import/resolver": {
+     "alias": [["@", "./src"]]
+   }
  },
  "extends": ["alloy", "alloy/react", "alloy/typescript", "prettier"],
  "plugins": ["prettier"],
  "env": {
    "browser": true
  },
  "globals": {},
  "rules": {
    "prettier/prettier": "error",
    "no-return-assign": "off"
  },
  "ignorePatterns": ["dist", "node_modules"]
}

3. tsconfig.json 修改

{
  "references": [{ "path": "./tsconfig.vite.json" }], // 指定工程引用依赖
  "compilerOptions": {
    "target": "ES2020", // 指定编译的ECMAScript目标版本
    "lib": ["ES2020", "DOM", "DOM.Iterable"], // 编译过程中需要引入的库文件的列表
    "jsx": "react-jsx", // 指定 jsx 代码的生成,无需在每个jsx文件中引入React
    "module": "ESNext", // 指定生成哪个模块系统代码
    "moduleResolution": "Node", // 决定如何处理模块
    "allowImportingTsExtensions": true, // 允许TypeScript文件通过TypeScript特定的扩展名如.ts, .mts, 或 .tsx互相导入
    "resolveJsonModule": true, // 允许引入 JSON 文件
    "allowJs": true, // 是否允许编译javascript文件
    "noEmit": true, // 设置是否输出 js 文件,一般是设置为 false,将打包等工作交给 vite/webpack 等工具
    "isolatedModules": true, // 将每个文件做为单独的模块
    "esModuleInterop": true, // 支持合成模块的默认导入
    "forceConsistentCasingInFileNames": true, // forceConsistentCasingInFileNames
    "strict": true, // 启用所用严格的类型检查
    "skipLibCheck": true, // 跳过对 .d.ts 文件的类型检查
+   "baseUrl": ".", // 用于设置解析非相对模块名称的基本目录,相对模块不会受到baseUrl的影响
+   "paths": { // 用于设置模块名到基于baseUrl的路径映射
+     "@/*": ["./src/*"]
+   }
  },
  "include": ["src"],
  "exclude": ["node_modules"]
}

四、React路由配置

1. 安装依赖

pnpm add react-router-dom

2. 新增页面

src/pages/404.tsx

import { useNavigate } from 'react-router-dom';

export default () => {
  const navigate = useNavigate();
  const goBack = () => {
    navigate(-1);
  };
  return (
    <>
      <h1>About Page</h1>
      <button onClick={goBack}>返回</button>
    </>
  );
};

src/pages/Home.tsx

import { Link } from 'react-router-dom';

export default () => {
  return (
    <>
      <h1>Home Page</h1>
      <Link to="/about">页面跳转</Link>
    </>
  );
};

src/pages/About.tsx

import { useNavigate } from 'react-router-dom';

export default () => {
  const navigate = useNavigate();
  const goBack = () => {
    navigate(-1);
  };
  return (
    <>
      <h1>About Page</h1>
      <button onClick={goBack}>返回</button>
    </>
  );
};

3. 移除src/App.tsx

4. 定义路由表src/router/index.ts

import About from '../pages/About';
import Home from '../pages/Home';
import NoFound from '../pages/404';

import { RouteObject } from 'react-router-dom';

const routeConfig: RouteObject[] = [
  {
    path: '/',
    Component: Home,
  },
  {
    path: '/about',
    Component: About,
  },
  {
    path: '*',
    Component: NoFound,
  },
];

export default routeConfig;

5. 入口文件main.tsx调整

import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { RouterProvider, createBrowserRouter } from 'react-router-dom';
import routeConfig from './router';

const router = createBrowserRouter(routeConfig);

const root = createRoot(document.querySelector('#app')!);
root.render(
  <StrictMode>
    <RouterProvider router={router} />
  </StrictMode>,
);

五、状态管理机制

目前 React 的状态管理机制解决方案是百花齐放,如:reduxreact-toolkitzustandrecoil等。为了降低上手成本,这里使用目前比较火的 zustand。具体就不展开对比各种状态管理机制的不同了。

1. 安装依赖

pnpm add zustand

2. 新增 src/store/counter.ts

import { create } from 'zustand';

interface CounterStore {
  num: number;
  increase: () => void;
}

const useCounterStore = create<CounterStore>()((set) => ({
  num: 0,
  increase: () => set((state) => ({ num: (state.num += 1) })),
}));

export default useCounterStore;

3. 新增组件 src/components/CountNum.tsx

import useCounterStore from '@/store/counter';

export default () => {
  const num = useCounterStore((state) => state.num);
  return <p>{num} </p>;
};

4. 修改 src/pages/Home.tsx

import { Link } from 'react-router-dom';
+ import useCounterStore from '@/store/counter';
+ import CountNum from '@/components/CountNum';

export default () => {
+ const increase = useCounterStore((state) => state.increase);

  return (
    <>
      <h1>Home Page</h1>
+     <CountNum />
+     <button onClick={increase}>加一</button>
+     <br />
      <Link to="/about">页面跳转</Link>
    </>
  );
};

六、 多环境打包

可以为每个不同的环境,设置特定的内容:接口请求地址、OSS配置等

1. 新增配置文件

env/.env.development

VITE_PROJECT_ENV=development
VITE_APP_TITLE=My App (development)

env/.env.production

VITE_PROJECT_ENV=production
VITE_APP_TITLE=My App (production)

env/.env.staging

VITE_PROJECT_ENV=staging
VITE_APP_TITLE=My App (staging)

env/.env.testing

VITE_PROJECT_ENV=testing
VITE_APP_TITLE=My App (testing)

2. vite.config.ts调整

import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import eslint from 'vite-plugin-eslint';
import path from 'path';

export default defineConfig({
+ envDir: './env',
  resolve: {
    alias: {
      '@': path.resolve(__dirname, 'src'),
    },
    extensions: ['.js', '.jsx', '.ts', '.tsx'],
  },
  plugins: [react(), eslint()],
});

3. package.json添加脚本命令

...
"scripts": {
 ...
 "start:dev": "vite --mode development",
 "start:test": "vite --mode testing",
 "start:staging": "vite --mode staging",
 "start:prod": "vite --mode production",
 "build": "pnpm build:prod",
 "build:dev": "pnpm tsc && vite build --mode development",
 "build:test": "pnpm tsc && vite build --mode testing",
 "build:staging": "pnpm tsc && vite build --mode staging",
 "build:prod": "pnpm tsc && vite build --mode production",
}

4. 页面模版修改

import { Link } from 'react-router-dom';
import useCounterStore from '@/store/counter';
import CountNum from '@/components/CountNum';
+ import { useState } from 'react';

export default () => {
  const increase = useCounterStore((state) => state.increase);
+ const title = useState(import.meta.env.VITE_APP_TITLE);

  return (
    <>
-     <h1>Home Page</h1>
+     <p>{title}</p>
      <CountNum />
      <button onClick={increase}>加一</button>
      <br />
      <Link to="/about">页面跳转</Link>
    </>
  );
};

七、CSS模块化

TIPS:后续内容,因为调整内容过多,只展示关键内容,具体修改请看下方github链接

React CSS模块化的方案有css in jscss module。两者各有优点,个人经常使用vue,比较习惯css module的方案。这里就只用css module

1. vite.config.ts修改

export default defineConfig({
  ...
  css: {
    preprocessorOptions: {
      scss: {
        additionalData: '@import "@/assets/scss/mixin.scss";',
      },
    },
    modules: {
      localsConvention: 'camelCaseOnly',
    },
  }
});

additionalData:指项目的引入的scss变量(主题色等)

2.目录调整整合

每个组件/页面一个文件夹维护,存放index.tsxindes.module.scss; 对应的**.scss -> **.module.scss

3. 安装插件

使用vscode的同学,可以安装CSS Modules的插件,提供语法高亮和智能提示,使用起来比较方便

八、React 路由懒加载,嵌套路由

1. 嵌套路由

把页面拆分成headerfootercontent。要控制的只是content的路由区域

创建一个src/layouts/basicLayout

import React from 'react';
import { Outlet } from 'react-router-dom';
import Header from '../../components/Header';
import Footer from '../../components/Footer';
import styles from './index.module.scss';

const BasicLayout: React.FC = () => {
  return (
    <>
      <Header>This is Header.</Header>
      <div className={styles.content}>
        <Outlet />
      </div>
      <Footer>This is Footer.</Footer>
    </>
  );
};

export default BasicLayout;

2. 路由表修改

import { RouteObject } from 'react-router-dom';
import { lazy } from 'react';

const routeConfig: RouteObject[] = [
  {
    path: '/',
    Component: lazy(() => import('../layouts/basicLayout')),
    children: [
      {
        path: '/',
        Component: lazy(() => import('../pages/home')),
      },
      {
        path: '/about',
        Component: lazy(() => import('../pages/about')),
      },
    ],
  },
  {
    path: '*',
    Component: lazy(() => import('../pages/noFound')),
  },
];

export default routeConfig;

3. src/main.tsx修改

import { StrictMode, Suspense } from 'react';
import { createRoot } from 'react-dom/client';
import { RouterProvider, createBrowserRouter } from 'react-router-dom';
import routeConfig from './router';
import './assets/scss/app.scss';
import './assets/scss/reset.scss';
import Loading from '@/components/Loading';

const router = createBrowserRouter(routeConfig);

const root = createRoot(document.querySelector('#app')!);
root.render(
  <StrictMode>
    <Suspense fallback={<Loading />}>
      <RouterProvider router={router} />
    </Suspense>
  </StrictMode>,
);

Suspense:路由懒加载时,页面加载过程中,会展示fallbackLoading组件

九、Axios请求库

1. 安装依赖

pnpm add axios

2.env目录添加对应环境的接口请求地址

VITE_PROJECT_ENV=development
VITE_APP_TITLE=My App (development)
VITE_BASE_API_URL=https://mock.apifox.cn/m1/3175341-0-default

这里使用apifox mock 数据来展示例子

3. src/instance.ts 封装axios实例

import axios, { AxiosResponse, AxiosError } from 'axios';

const instance = axios.create({
  baseURL: import.meta.env.VITE_BASE_API_URL,
  timeout: 10000,
  headers: {
    'Content-Type': 'application/json;charset=UTF-8',
  },
});

const errorCodeMap = new Map([
  [401, '未授权,请登录'],
  [404, '请求地址不存在'],
  [500, '服务内部错误'],
  [403, '用户不存在访问权限'],
]);

instance.interceptors.response.use(
  (response: AxiosResponse) => {
    return response.data;
  },
  (error: AxiosError) => {
    const { response } = error;

    if (!response) {
      return Promise.reject(error);
    }

    if (errorCodeMap.has(response.status)) {
      const errorMsg = errorCodeMap.get(response.status);
      // TODO: 处理HTTP码异常逻辑
      console.log(errorMsg);
    }
    return Promise.reject(response.data);
  },
);

export default instance;

这里简单的封装了下,如何处理HTTP异常错误,还是需要根据开发同学的具体业务来实现。

4. src/api.index.ts管理接口

import instance from './instance';

export const init = () => instance.get('/init');

理想状态下应该利用typescript定义入参类型和数据响应类型。那样子在调用接口的时候,就有对应的类型提示。这里也不做展开了

总结

整个搭建的过程也算是结束了,可以满足大部分的页面需求。有一些搭建内容缺少最佳实践,这部分就不做展开了,毕竟重点不在这,需要同学们慢慢琢磨。 后续项目代码可能会持续更新,但这里的文章到这里就结束了! 项目代码可以参考下面👇

项目代码

参考文献