Nest.js从0到1搭建博客系统---基于Vite5.x+React18+TS+Jotai+Antd搭建企业级中后台管理架构(10)

309 阅读6分钟

前言

代码的世界,只有0和1,没有中间值。如同人生的黑白,没有灰色地带。埋头于字节中,探寻着未知的奥秘,我是程序员,这是我的世界。

技术选型

勾选的是必要的,其他项目需求酌情处理

vite的优势

  • 按需编译:esbuild 预编译、模块请求时编译、SFC module按需编译
  • 打包产物小,加载运行速度快:没有runtime、没有模板代码
  • 更好的代码分包实现:共同依赖、npm按需打包、tree-shaking
  • 其他:客户端墙缓存、静态资源优化、远程ESM的HMR
  • 初始化简单:代码模块丰富
  • 内置功能全:css/HTML预处理器、HRM、异步加载、分包、chunk
  • 兼容性丰富:React\Svelte\Preact\Vue\TS\Rollup等接口生态

安装项目

 npm create vite@latest react-vhen-blog-admin --template react
  • 选择React

image.png

  • 选择TS

image.png

  • 最初项目结构

image.png

  • 进入项目react-vhen-blog-admin根目录安装插件npm i,然后运行项目
npm run dev

image.png

搭建项目结构

  • mock:模拟接口数据
  • apis:统一接口模块,便于维护
  • assets:静态资源,比如说静态资源、字体、图片、svg、scss等
  • business:业务组件
  • components:公共组件
  • hooks:自定义Hook
  • http:网络请求相关模块
  • layout:后台基础框架模块存放文件:如:顶部导航栏、左侧菜单栏、面包屑等
  • locales:语言包
  • router:路由模块
  • services:与后端交互服务模块
  • store:状态管理
  • tests:测试
  • types:ts 存放类型
  • utils:工具函数
  • views:视图

image.png

Ant Design 企业级中后台产品

  • 安装
npm i -S antd 
  • main.tsx配置
import { ConfigProvider } from "antd";
import zhCN from 'antd/locale/zh_CN';
import ReactDOM from "react-dom/client";

import type { ThemeConfig } from 'antd';
import App from "./App";

const config: ThemeConfig = {
  token: {
    colorPrimary: '#1890ff',
  },
};

ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
  <ConfigProvider
    locale={zhCN}
    theme={config}
  >
    <App/>
  </ConfigProvider>
);
  • 使用
import { Rate } from 'antd';
import { useState } from 'react';

const Home: React.FC = () =>{
  const [name, setName] = useState<string>('Vhen');
  const setNewName = () => {
    setName('Vhen6666');
  };
  return (
    <div onClick={setNewName}>
      Home - {name}
      <Rate allowHalf defaultValue={2.5} />
    </div>
  );
}

export default Home;

image.png

React Router V6 路由配置

  • 安装插件
npm i react-router-dom 或者 pnpm add react-router-dom
  • 封装路由加载动画
import { Spin } from "antd";
import { ReactNode, Suspense } from "react";

const LazyLoad = (Comp: React.FC): ReactNode => {
  return (
      <Suspense
        fallback={
          <Spin
            size="large"
            style={{
              display: "flex",
              alignItems: "center",
              justifyContent: "center",
              height: "100vh",
            }}
          />
        }
      >
        <Comp/>
    </Suspense>
  );
};
export default LazyLoad;
  • 封装路由守卫RouterGuard
import { App } from 'antd';
import { useLocation } from "react-router-dom";

const RouterGuard = ({ children }: { children: JSX.Element }) => {
  const {pathname} = useLocation();
  const {message} = App.useApp();
  // 根据需求加载逻辑,后期处理
  console.log(pathname);
  console.log(message);
  return children;
}

export default RouterGuard;
  • index.tsx
/*
 * @Author: vhen
 * @Date: 2024-01-17 18:39:11
 * @LastEditTime: 2024-01-18 16:42:54
 * @Description: 现在的努力是为了小时候吹过的牛逼!
 * @FilePath: \react-vhen-blog-admin\src\router\index.tsx
 *
 */
import { lazy } from 'react';

import LazyLoad from './LazyLoad';



interface IRouteObject{
  name?: string;
  path: string;
  children?: IRouteObject[];
  element: React.ReactNode;
}

const routes: IRouteObject[] = [
  {
    path: "/login",
    element: LazyLoad(lazy(() => import(`@/views/login`))),
  },
  {
    path: "/home",
    element: LazyLoad(lazy(() => import(`@/views/home`))),
  },
]

export default routes;
  • App.tsx
import { App } from "antd";
import { FC, ReactElement } from 'react';
import { HashRouter, useRoutes } from "react-router-dom";

import allRoutes from "@/router";
import RouterGuard from "@/router/RouterGuard";

const Router = ({ routes }: { routes: any }) => useRoutes(routes)

const AppWrapper: FC=(): ReactElement=>{
  return (
    <App>
       <HashRouter >
        {/* 路由守卫鉴权与拦截器 */}
        <RouterGuard key="appraisal">
          <Router routes={allRoutes} />
        </RouterGuard>
       </HashRouter>
    </App>
  )
}
export default AppWrapper;

image.png

React Query 网络请求

  • 安装
npm i react-query
  • 安装工具
npm i @tanstack/react-query-devtools
  • 配置 react-query-devtools
import "@/assets/scss/index.scss";
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { ConfigProvider } from "antd";
import zhCN from 'antd/locale/zh_CN';
import { Provider } from "jotai";
import ReactDOM from "react-dom/client";

import type { ThemeConfig } from 'antd';
import App from "./App";

import store from '@/store';

const config: ThemeConfig = {
  token: {
    colorPrimary: '#1890ff',
  },
};
const queryClient = new QueryClient()
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
  <ConfigProvider
    locale={zhCN}
    theme={config}
  >
    <QueryClientProvider client={queryClient}>
      <ReactQueryDevtools initialIsOpen={false} />
      <Provider store={store}>
        <App />
      </Provider>
    </QueryClientProvider>
  </ConfigProvider>
);

image.png

  • 测试请求
 const { isLoading, error, data } = useQuery({
    queryKey: ['posts'], // 用来缓存数据的key
    queryFn: async () => {
      const response = await axios.get('https://jsonplaceholder.typicode.com/posts');
      return response.data;
    },
  })
  if (isLoading) {
    console.log('Loading...');
  }

  if (error) {
    console.log(`An error has occurred: ${error.message}`);
  }
  if (data) {
    console.log('data', data);
  }

image.png

sass配置

   npm i -D sass

image.png

  • variables.scss
$primary-color: #1890ff;
$primary-color-rgb: 24 144 255;
$primary-color-rgba: rgba(24, 144, 255, 0.9);
$primary-color-rgba-1: rgba(24, 144, 255, 0.1);


/**暴露变量*/
:export {
  primaryColor: $primary-color;
}
  • base.scss
.bg{
  width: 100px;
  height: 100px;
  background-color: $primary-color;
}
  • index.scss
@import 'antd/dist/reset.css';
@import './base.scss';
  • vite.config.ts配置variables全局变量
css: {
  preprocessorOptions: {
    scss: {
      additionalData: `@import "./src/assets/scss/variables.scss";`
    }
  }
}
  • main.tsx引入全局样式
import "@/assets/scss/index.scss";
  • 测试一下结果

image.png

image.png

image.png

tailwindcss配置

  • 安装
npm i -D tailwindcss
  • 安装完成后,执行npx tailwindcss init -p,根目录会生成tailwind.config.jspostcss.config配置文件
/** @type {import('tailwindcss').Config} */
export default {
  content: ["index.html", "./src/**/*.{react,js,ts,jsx,tsx}"],
  theme: {
    extend: {},
  },
  plugins: [],
}
  • 公共样式文件引入
@tailwind base;
@tailwind components;
@tailwind utilities;
  • 测试效果
  <div className="bg-black text-center text-white w-20 h-30">6666</div>

image.png

Jotai状态管理

  • 安装
npm i jotai immer jotai-immer jotai-signal
  • UserService.ts
import { atom } from 'jotai';
import { atomWithStorage } from 'jotai/utils';


export interface IUser {
  id: number;
  userName: string;
}
const userState: IUser = {
  id: 1,
  userName: 'test',
};

const userInfo = atomWithStorage<IUser>('userInfo', userState);


//  模拟接口请求数据
const getMockUserData = (): Promise<IUser> => {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve({
        id: 2,
        userName: 'vhen',
      });
    }, 1000);
  });
};


const userService = atom(
  get => get(userInfo),
  async (_get, set) => {
    const data = await getMockUserData();
    set(userInfo, data);
  },
)

export default userService;
  • 使用
import { Button, Rate } from 'antd';
import { useAtom } from 'jotai';
import { useState } from 'react';

import styles from "./index.module.scss";

import userService from '@/services/UserService';

const Home: React.FC = () =>{
  const [name, setName] = useState<string>('Vhen');
  const [asyncData, getAsyncData] =useAtom(userService)
  const setNewName = async () => {
    setName('Vhen6666');
  };
  return (
    <div className={styles.home} onClick={setNewName}>
      Home -
      {name}
      <br/>
      <Rate allowHalf defaultValue={2.5} />
      <div className="bg" />
      <div>异步数据:{asyncData?.userName}</div>
      <Button type="primary" onClick={getAsyncData}>获取数据</Button>
    </div>
  );
}

export default Home;
  • 页面初始化读取默认数据

image.png

  • 点击获取数据

image.png

dayjs日期插件

  • 安装
npm i dayjs
  • 如果您的 tsconfig.json 包含以下配置,您必须使用 import dayjs from 'dayjs' 的 default import 模式:
{ 
//tsconfig.json 
    "compilerOptions": { 
        "esModuleInterop": true,
        "allowSyntheticDefaultImports": true,
    } 
}
  • 封装date日期工具类

image.png

image.png

image.png

中引文插件 react-i18next

  • 安装
npm i react-i18next i18next i18next-browser-languagedetector

image.png

  • index.ts 配置

import i18n from 'i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
import { initReactI18next } from 'react-i18next';

import enHome from './en/home.json';
import zhHome from './zh/home.json';

const resources = {
  en: {
    home: enHome,
  },
  zh: {
    home: zhHome,
  },
};
i18n
  .use(initReactI18next)
  .use(LanguageDetector)
  .init({
    resources,
    // 默认语言  zh/en  中文/英文
    fallbackLng: 'en',
    preload: ['en', 'zh'],
    interpolation: {
      escapeValue: false,
    },
  });
export default i18n;

image.png

image.png

  • main.tsx
import "@/locales";
  • 测试
import { useTranslation } from 'react-i18next';
import styles from "./index.module.scss";

const Home: React.FC = () => {

  const { t, i18n } = useTranslation(['home']) // 选择home模块
  const toggleLanguage = (language: string) => {
    i18n.changeLanguage(language)
  }


  return (
    <div className={styles.home} onClick={setNewName}>
      <Button type="primary" onClick={() => toggleLanguage('zh')}>中文</Button>&nbsp;&nbsp;&nbsp;&nbsp;
      <Button onClick={() => toggleLanguage('en')}>英文</Button>
      <div>{t('home.title')}</div>
    </div>
  );
}

export default Home;

image.png

image.png

storybook 使用

npx storybook init --builder vite
  • 执行完成后,项目根目录会生成storybook文件目录 image.png

  • 更改文档目录为docs

image.png

  • 在项目components组件创建Button组件测试

image.png

  • docs文档创建Button.stories.ts
/*
 * @Author: vhen
 * @Date: 2024-01-19 17:56:34
 * @LastEditTime: 2024-01-19 18:10:13
 * @Description: 现在的努力是为了小时候吹过的牛逼!
 * @FilePath: \react-vhen-blog-admin\docs\Button.stories.ts
 *
 */
import { Meta, StoryObj } from '@storybook/react';
import Button from '../src/components/Button';
type Story = StoryObj<typeof Button>;
const meta = {
  title: 'Button', // 这个路径关系到你左侧的菜单显示
  component: Button,
  parameters: {
    layout: 'centered',// 组件显示的位置
  },
  tags: ['autodocs'], // 自动生成
  argTypes: {
    backgroundColor: { control: 'color' },
  },

} satisfies Meta<typeof Button>;

export const Normal: Story = {
  // 传入组件的props
  args: {
    buttonText: "删除",
    title: "确定要删除该条数据吗?",
  },
};
export default meta;
  • 效果

image.png

这样就完美的搭建了自己项目文档了

github

项目地址:nest_vhen_blog