前言
代码的世界,只有0和1,没有中间值。如同人生的黑白,没有灰色地带。埋头于字节中,探寻着未知的奥秘,我是程序员,这是我的世界。
技术选型
勾选的是必要的,其他项目需求酌情处理
- Vite文档
- React中文文档&React英文文档
- React Router V6 文档
- Jotai 状态管理文档
- Ant Design 文档
- ahooks 文档
- TypeScript中文文档&ES6英文文档
- Day.js 文档
- react-i18next、i18next
- SASS文档
- React Query :ahooks中自带网络请求
useRequest,本项目用React Query - Framer-motion 动画库
- tailwindcss 文档
- storybook 文档
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
- 选择TS
- 最初项目结构
- 进入项目
react-vhen-blog-admin根目录安装插件npm i,然后运行项目
npm run dev
搭建项目结构
mock:模拟接口数据apis:统一接口模块,便于维护assets:静态资源,比如说静态资源、字体、图片、svg、scss等business:业务组件components:公共组件hooks:自定义Hookhttp:网络请求相关模块layout:后台基础框架模块存放文件:如:顶部导航栏、左侧菜单栏、面包屑等locales:语言包router:路由模块services:与后端交互服务模块store:状态管理tests:测试types:ts 存放类型utils:工具函数views:视图
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;
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;
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>
);
- 测试请求
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);
}
sass配置
npm i -D sass
- 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";
- 测试一下结果
tailwindcss配置
- 安装
npm i -D tailwindcss
- 安装完成后,执行
npx tailwindcss init -p,根目录会生成tailwind.config.js和postcss.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>
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;
- 页面初始化读取默认数据
- 点击获取数据
dayjs日期插件
- 安装
npm i dayjs
- 如果您的
tsconfig.json包含以下配置,您必须使用import dayjs from 'dayjs'的 default import 模式:
{
//tsconfig.json
"compilerOptions": {
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
}
}
- 封装date日期工具类
中引文插件 react-i18next
- 安装
npm i react-i18next i18next i18next-browser-languagedetector
- 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;
- 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>
<Button onClick={() => toggleLanguage('en')}>英文</Button>
<div>{t('home.title')}</div>
</div>
);
}
export default Home;
storybook 使用
npx storybook init --builder vite
-
执行完成后,项目根目录会生成storybook文件目录
-
更改文档目录为
docs
- 在项目
components组件创建Button组件测试
- 在
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;
- 效果
这样就完美的搭建了自己项目文档了
github
项目地址:nest_vhen_blog