Vite2.x + react 18.x + TS起步
创建项目
Requires Node.js ^14.17.0, 16.0.0 or later. 因为部分依赖在低版本node中不兼容
1 | yarn create vite vite-react --template react-ts |
修改语法检查与格式化配置
安装依赖
1
yarn add eslint eslint-plugin-react eslint-plugin-react-hooks @typescript-eslint/parser @typescript-eslint/eslint-plugin eslint-config-airbnb-base eslint-plugin-import vite-plugin-eslint -D
修改
.eslintignore
文件1
2
3
4/dist/
/*.js
/*.zip
/*.rar修改
.eslintrc.js
文件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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91module.exports = {
extends: ['eslint:recommended', 'plugin:react/recommended', 'airbnb-base'],
env: {
browser: true,
commonjs: true,
es6: true,
},
globals: {
$: true,
process: true,
__dirname: true,
},
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaFeatures: {
jsx: true,
modules: true,
},
sourceType: 'module',
ecmaVersion: 6,
},
plugins: ['react', 'react-hooks', '@typescript-eslint'],
settings: {
'import/ignore': ['node_modules'],
react: {
version: 'detect',
},
'import/extensions': ['.ts', '.tsx', '.js', '.jsx'],
},
rules: {
'no-alert': 'off',
'consistent-return': 'off',
'no-return-assign': 'off',
'no-unused-vars': 'off',
'lines-between-class-members': 'off',
'comma-dangle': ['error', 'always-multiline'],
'no-console': 'off',
'func-names': 'off',
'no-process-exit': 'off',
'object-shorthand': 'off',
'class-methods-use-this': 'off',
'arrow-parens': ['error', 'as-needed'],
'arrow-body-style': 'off',
'operator-linebreak': 'off',
quotes: [2, 'single', 'avoid-escape'],
semi: ['error', 'never'],
'linebreak-style': 'off',
'max-len': ['error', { code: 120 }],
'no-restricted-syntax': 'off',
'guard-for-in': 'off',
'no-restricted-properties': 'off',
'no-useless-escape': 'off',
'indent': 'off',
'new-cap': 'off',
'object-curly-newline': 'off',
radix: 'off',
camelcase: 'warn',
'no-restricted-globals': 'off',
'use-isnan': 2,
'no-plusplus': 'off',
'no-underscore-dangle': 'off',
'no-param-reassign': ['error', { props: false }],
'no-unused-expressions': ['error', { allowShortCircuit: true }],
'react/no-array-index-key': 'off',
'react/jsx-one-expression-per-line': 'off',
'react/react-in-jsx-scope': 'off',
'react/prop-types': [0, { ignore: ['children'] }],
'react/jsx-props-no-spreading': 'off',
'react/forbid-prop-types': 'off',
'react/state-in-constructor': 'off',
'react/jsx-filename-extension': 'off',
'react/require-default-props': 'off',
'react/no-unescaped-entities': 'off',
'react/no-danger': 'off',
'react/display-name': 'off',
'default-param-last': 'off',
'no-nested-ternary': 'off',
'react/function-component-definition': [
0,
{
namedComponents: 'arrow-function',
unnamedComponents: 'arrow-function',
},
],
'jsx-a11y/label-has-associated-control': 'off',
'import/extensions': [2, 'never'],
'import/no-unresolved': 'off',
'import/order': 'off',
'import/prefer-default-export': 'off',
},
}修改
.prettierrc.js
文件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// prettier.config.js or .prettierrc.js
module.exports = {
// 一行最多 120 字符
printWidth: 120,
// 使用 2 个空格缩进
tabWidth: 2,
// 不使用缩进符,而使用空格
useTabs: false,
// 行尾需要有分号
semi: false,
// 使用单引号
singleQuote: true,
// 对象的 key 仅在必要时用引号
quoteProps: 'as-needed',
// jsx 不使用单引号,而使用双引号
jsxSingleQuote: false,
// 末尾不需要逗号
trailingComma: 'all',
// 大括号内的首尾需要空格
bracketSpacing: true,
// jsx 标签的反尖括号需要换行
jsxBracketSameLine: false,
// 箭头函数,只有一个参数的时候,也需要括号
arrowParens: 'always',
// 每个文件格式化的范围是文件的全部内容
rangeStart: 0,
rangeEnd: Infinity,
// 不需要写文件开头的 @prettier
requirePragma: false,
// 不需要自动在文件开头插入 @prettier
insertPragma: false,
// 使用默认的折行标准
proseWrap: 'preserve',
// 根据显示样式决定 html 要不要折行
htmlWhitespaceSensitivity: 'css',
// 换行符使用 lf
endOfLine: 'lf'
}修改
tsconfig.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{
"compilerOptions": {
"experimentalDecorators": true,
"target": "ESNext",
"useDefineForClassFields": true,
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "ESNext",
"moduleResolution": "Node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"outDir": "./dist/",
"types": [
"node"
],
"baseUrl": "./",
"paths": {
"@/*": [
"src/*"
],
"@api/*": [
"src/api/*"
]
}
},
"include": ["src"],
"exclude": ["node_modules"],
"references": [{ "path": "./tsconfig.node.json" }]
}修改
tsconfig.node.json
文件1
2
3
4
5
6
7
8{
"compilerOptions": {
"composite": true,
"module": "esnext",
"moduleResolution": "node"
},
"include": ["vite.config.ts"]
}修改
vite.config.ts
文件1
2
3
4
5
6
7
8
9
10
11
12import { defineConfig } from 'vite'
import eslintPlugin from 'vite-plugin-eslint'
export default defineConfig(() => {
return {
plugins: [
eslintPlugin({
include: ['src/**/*.ts', 'src/**/*.tsx', 'src/*.ts', 'src/*.tsx'],
}),
]
}
})
修改git钩子
先安装依赖
1
2yarn add -D lint-staged
npx husky-init && yarn修改
package.json
文件1
2
3
4
5
6
7
8
9
10
11{
"scripts": {
"prepare": "husky install",
"lint": "lint-staged"
},
"lint-staged": {
"src/**/*.{js,ts,tsx}": [
"eslint --fix"
]
}
}修改
.husky/pre-commit
1
2
3
4
5
6!/usr/bin/env sh
在sourcetree报错,找不到npm,修改path可解决这个问题
export PATH="/usr/local/opt/node@14/bin/:$PATH"
. "$(dirname -- "$0")/_/husky.sh"
npm run lint
使用less
先安装依赖
1
yarn add less less-loader @types/node -D
创建全局变量文件
src/assets/styles/variables.less
1
@baseFontSize: 14px;
修改
vite.config.ts
文件1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16import { defineConfig } from 'vite'
const path = require('path')
export default defineConfig(() => {
return {
css: {
preprocessorOptions: {
less: {
javascriptEnabled: true,
charset: false,
additionalData: `@import (reference) "${path.resolve(__dirname, 'src/assets/styles/variables.less')}";`
}
}
}
}
})创建全局样式文件
src/assets/styles/global.less
1
2
3body {
margin: 0;
}在
main.tsx
加载全局样式文件1
2
3
4
5import ReactDOM from 'react-dom/client'
import App from './App'
import './assets/styles/global.less'
ReactDOM.createRoot(document.getElementById('root')!).render(<App />)在组件内使用less
1
2
3
4/* src/components/MyButton/my-button.module.less */
.my-button {
font-size: @baseFontSize;
}
修改环境变量配置
创建
.env.development
文件1
2
3VITE_ENV = DEV
VITE_APP_BASE_URL = 'http://localhost/api'
VITE_APP_TITLE = 'index'创建
.env.production
文件1
2
3VITE_ENV = PROD
VITE_APP_BASE_URL = 'https://www.pengwf.com'
VITE_APP_TITLE = '首页'修改
package.json
文件1
2
3
4
5
6
7{
"scripts": {
"dev": "vite --mode development",
"build": "rimraf ./dist && tsc --noEmit && vite build --mode production",
"preview": "vite preview"
}
}修改
vite.config.ts
文件1
2
3
4
5
6
7
8
9
10
11
12
13import { defineConfig } from 'vite'
const path = require('path')
export default defineConfig(() => {
return {
resolve: {
alias: {
'@': path.resolve(__dirname, 'src'),
'@api': path.resolve(__dirname, 'src/api')
}
}
}
})
使用 Ant Design 库
先安装依赖
1
2yarn add antd prop-types
yarn add vite-plugin-imp @babel/plugin-proposal-class-properties @babel/plugin-proposal-decorators -D修改
vite.config.ts
文件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
27import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import vitePluginImp from 'vite-plugin-imp'
export default defineConfig(() => {
return {
plugins: [
react({
babel: {
plugins: [
['@babel/plugin-proposal-decorators', { legacy: true }],
['@babel/plugin-proposal-class-properties', { loose: true }],
],
},
}),
vitePluginImp({
libList: [
{
libName: 'antd',
libDirectory: 'es',
style: name => `antd/es/${name}/style`,
},
],
}),
]
}
})
使用mobx
安装依赖
1
yarn add mobx mobx-react
创建
src/store/main.ts
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
55import { observable, action, makeObservable } from 'mobx'
export interface UserinfoType {
id: string
name: string
date: string
}
export interface MainStore {
token: string
network: boolean
userinfo: UserinfoType
setToken(token: string): void
clearToken(): void
setNetwork(flag: boolean): void
setUserinfo(userinfo: UserinfoType): void
}
class mainStore {
constructor() {
makeObservable(this)
}
'' token =
true network =
userinfo: UserinfoType = {
id: '',
name: '',
date: '',
}
'setToken') (
setToken = (token: string) => {
this.token = token
localStorage.setItem('token', token)
}
'clearToken') (
clearToken = () => {
this.token = ''
localStorage.removeItem('token')
}
'setNetwork') (
setNetwork(flag: boolean) {
this.network = flag
}
'setUserinfo') (
setUserinfo(userinfo: UserinfoType) {
this.userinfo = userinfo
}
}
export default new mainStore()创建
src/store/index.ts
1
2
3
4
5
6
7
8
9
10import mainStore, { MainStore } from './main'
class Store {
mainStore: MainStore
constructor() {
this.mainStore = mainStore
}
}
export default new Store()修改
src/App.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21import { Component, Suspense } from 'react'
import { unstable_HistoryRouter as HistoryRouter } from 'react-router-dom'
import { Provider } from 'mobx-react'
import rootStore from './store'
import RouterComponent from './router'
class App extends Component {
render() {
return (
<Provider {...rootStore}>
<Suspense fallback={<div>loading...</div>}>
<HistoryRouter>
<RouterComponent />
</HistoryRouter>
</Suspense>
</Provider>
)
}
}
export default App在函数组件中使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24import { FunctionComponent } from 'react'
import { inject, observer } from 'mobx-react'
import { MainStore } from '@/store/main'
interface IProps {
mainStore?: MainStore
}
const Index: FunctionComponent<IProps> = inject('mainStore')(
observer(props => {
const { mainStore } = props
const logout = () => {
mainStore?.clearToken()
}
return (
<div>
<button onClick={logout}>退出登录</button>
</div>
)
})
)
export default Index在类组件中使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25import { Component } from 'react'
import { inject, observer } from 'mobx-react'
import { MainStore } from '@/store/main'
interface IProps {
mainStore?: MainStore
}
'mainStore') (
class Login extends Component<IProps> {
logout = () => {
this.props.mainStore?.clearToken()
}
render() {
return (
<div>
<button onClick={this.logout}>退出登录</button>
</div>
)
}
}
export default Login
使用react-router
安装依赖
1
yarn add react-router-dom@6 history
定义router实例
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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88// src/router/index.tsx
// 注意此处文件后缀为tsx
import { FunctionComponent, useEffect } from 'react'
import { Route, Routes, Navigate, RouteProps } from 'react-router-dom'
import { Result } from 'antd'
interface IRoute extends RouteProps {
// 名称
name: string
// 中文描述,可用于侧栏列表
title: string
// react组件函数
component: FunctionComponent
// 页面组件创建时执行的hook
beforeCreate: (route: IRoute) => void
// 页面组件销毁时执行的hook
beforeDestroy: (route: IRoute) => void
// 属性
meta: {
navigation: string
}
}
const RouteDecorator = (props: { route: IRoute }) => {
const { route } = props
useEffect(() => {
// 自定义路由守卫
route.beforeCreate && route.beforeCreate(route)
return () => route.beforeDestroy && route.beforeDestroy(route)
}, [route])
return <route.component />
}
const routes: IRoute[] = []
const modules = import.meta.globEager('./*.ts')
for (const path in modules) {
routes.push(...modules[path].default)
}
const RouterComponent: FunctionComponent = () => (
<Routes>
<Route path="/" element={<Navigate to="/dashboard" />} />
<Route
path="*"
element={<Result status="warning" title="There are some problems with your operation." />}
/>
{routes.map(route => (
<Route
key={route.path}
path={route.path}
element={<RouteDecorator route={route} />}
/>
))}
</Routes>
)
export default RouterComponent
// src/utils/basicProps.tsx
import { ComponentType } from 'react'
import {
NavigateFunction,
Params,
useLocation,
useNavigate,
useParams,
Location,
} from 'react-router-dom'
interface RouterProps {
navigate: NavigateFunction
readonly params: Params<string>
location: Location
}
export type WithRouterProps<T> = T & RouterProps
export function withRouter<T>(Component: ComponentType<any>) {
return (props:any) => {
const location = useLocation()
const navigate = useNavigate()
const params = useParams()
return <Component location={location} navigate={navigate} params={params} {...props} />
}
}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// src/router/dashboard.ts
import { lazy } from 'react'
export default [
{
path: '/dashboard',
name: 'dashboard',
title: '首页',
component: lazy(() => import('@/views/dashboard/index')),
meta: {
navigation: '首页',
},
},
]
// src/router/login.ts
import { lazy } from 'react'
export default [
{
path: '/login',
name: 'login',
title: '登录',
component: lazy(() => import('@/views/login/index')),
meta: {
navigation: '登录',
},
},
]
// src/utils/history.ts
import { createBrowserHistory } from 'history'
export default createBrowserHistory()修改
src/App.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22import { Component, Suspense } from 'react'
import { unstable_HistoryRouter as HistoryRouter } from 'react-router-dom'
import { Provider } from 'mobx-react'
import rootStore from './store'
import RouterComponent from './router'
import history from './utils/history'
class App extends Component {
render() {
return (
<Provider {...rootStore}>
<Suspense fallback={<div>loading...</div>}>
<HistoryRouter history={history}>
<RouterComponent />
</HistoryRouter>
</Suspense>
</Provider>
)
}
}
export default App在函数组件中使用
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
27import { FunctionComponent } from 'react'
import { useNavigate } from 'react-router-dom'
import { inject, observer } from 'mobx-react'
import { MainStore } from '@/store/main'
interface IProps {
mainStore?: MainStore
}
const Index: FunctionComponent<IProps> = inject('mainStore')(
observer(props => {
const navigate = useNavigate()
const { mainStore } = props
const logout = () => {
mainStore?.clearToken()
navigate('/login', { replace: true })
}
return (
<div>
<button onClick={logout}>退出登录</button>
</div>
)
})
)
export default Index在类组件中使用
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
27import { Component } from 'react'
import { inject, observer } from 'mobx-react'
import { MainStore } from '@/store/main'
import { withRouter, WithRouterProps } from '@/utils/basicProps'
interface IProps {
mainStore?: MainStore
}
'mainStore') (
class Login extends Component<WithRouterProps<IProps>> {
logout = () => {
this.props.mainStore?.clearToken()
this.props.navigate('/dashboard')
}
render() {
return (
<div>
<button onClick={this.logout}>退出登录</button>
</div>
)
}
}
export default withRouter(Login)
使用react-i18next
安装依赖
1
yarn add react-i18next i18next
定义语言包
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// src/i18n/index.ts
import i18n from 'i18next'
import { initReactI18next } from 'react-i18next'
const language: { [key: string]: any } = {}
const langs = import.meta.globEager('./*.ts')
for (const [key, value] of Object.entries(langs)) {
const name = key.slice(key.lastIndexOf('/') + 1, key.lastIndexOf('.'))
language[name] = {
translation: value.default,
}
}
i18n.use(initReactI18next).init({
resources: language,
lng: 'zh_CN',
interpolation: {
escapeValue: false,
},
})
export default i18n
// src/i18n/zh_CN.ts
export default {
hello: '你好'
}
// src/i18n/en_US.ts
export default {
hello: 'Hello'
}修改
main.tsx
1
2
3
4
5
6import ReactDOM from 'react-dom/client'
import App from './App'
import './i18n'
import './assets/styles/global.less'
ReactDOM.createRoot(document.getElementById('root')!).render(<App />)在函数组件中使用
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
30import { FunctionComponent } from 'react'
import { useNavigate } from 'react-router-dom'
import { inject, observer } from 'mobx-react'
import { useTranslation } from 'react-i18next'
import { MainStore } from '@/store/main'
interface IProps {
mainStore?: MainStore
}
const Index: FunctionComponent<IProps> = inject('mainStore')(
observer(props => {
const navigate = useNavigate()
const { t } = useTranslation()
const { mainStore } = props
const logout = () => {
mainStore?.clearToken()
navigate('/login', { replace: true })
}
return (
<div>
<h1>{t('hello')}</h1>
<button onClick={logout}>退出登录</button>
</div>
)
})
)
export default Index在类组件中使用
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
30import { Component } from 'react'
import { inject, observer } from 'mobx-react'
import { withTranslation, WithTranslation } from 'react-i18next'
import { MainStore } from '@/store/main'
import { withRouter, WithRouterProps } from '@/utils/basicProps'
interface IProps extends WithTranslation {
mainStore?: MainStore
}
'mainStore') (
class Login extends Component<WithRouterProps<IProps>> {
logout = () => {
this.props.mainStore?.clearToken()
this.props.navigate('/dashboard')
}
render() {
const { t } = this.props
return (
<div>
<h1>{t('hello')}</h1>
<button onClick={this.logout}>退出登录</button>
</div>
)
}
}
export default withTranslation()(withRouter(Login))
使用vite-plugin-imagemin
压缩图片
- 修改
package.json
文件国内不搭梯子的话,基本上都会遇到安装失败的问题,需要切换到淘宝的源,并且在
package.json
文件增加以下配置
1 | { |
安装依赖
1
yarn add vite-plugin-imagemin -D
修改
vite.config.ts
文件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
36import { defineConfig } from 'vite'
import viteImagemin from 'vite-plugin-imagemin'
export default defineConfig(() => {
return {
plugins: [
viteImagemin({
gifsicle: {
optimizationLevel: 7,
interlaced: false,
},
optipng: {
optimizationLevel: 7,
},
mozjpeg: {
quality: 20,
},
pngquant: {
quality: [0.8, 0.9],
speed: 4,
},
svgo: {
plugins: [
{
name: 'removeViewBox',
},
{
name: 'removeEmptyAttrs',
active: false,
},
],
}
})
]
}
})
使用vite-plugin-html
自定义入口文件
- 安装依赖
新版本在我的mac上测试有bug,所以指定了一个比较低的版本
1 | yarn add vite-plugin-html@3.1.0 -D |
修改
vite.config.ts
文件1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24import { defineConfig, loadEnv } from 'vite'
import { createHtmlPlugin } from 'vite-plugin-html'
export default defineConfig(({ command, mode }) => {
const env = loadEnv(mode, process.cwd())
const isProd: boolean = env.VITE_ENV === 'PROD'
return {
plugins: [
createHtmlPlugin({
minify: isProd,
template: 'pages/index.html',
inject: {
data: {
title: env.VITE_APP_TITLE,
injectScript: isProd
? ''
: `<script src="https://cdn.jsdelivr.net/npm/vconsole@2.5.2/dist/vconsole.min.js"></script>`
}
}
}),
]
}
})修改入口文件
pages/index.html
1
2
3
4
5
6
7
8
9
10
11
12
13
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title><%= title %></title>
<%- injectScript %>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
封装axios
安装依赖
1
2yarn add axios js-cookie nprogress qs ts-md5
yarn add @types/js-cookie @types/nprogress @types/qs -D创建axios实例
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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152// src/utils/ajax.ts
import axios from 'axios'
import qs from 'qs'
import Cookies from 'js-cookie'
import NProgress from 'nprogress'
import 'nprogress/nprogress.css'
import { Md5 } from 'ts-md5/dist/md5'
import { notification } from 'antd'
import mainStore from '@/store/main'
const ajax: any = axios.create({
timeout: 60000,
headers: {
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
Accept: 'application/json',
},
})
function buildSign(params: any, timestamp: number, token: string): string {
if (!token) {
return ''
}
const keys: string[] = Object.keys(params).sort()
let paramsStr: string = ''
keys.forEach((key: string, i: number) => {
const value: string =
typeof params[key] === 'object' ? JSON.stringify(params[key]) : <string>params[key]
paramsStr += `${key}=${value}`
if (i < keys.length - 1) {
paramsStr += '&'
}
})
return Md5.hashStr(`${timestamp}${token}${paramsStr}`)
}
ajax.interceptors.request.use(
(config: any) => {
if (!config.isMock || import.meta.env.VITE_ENV === 'PROD') {
config.baseURL = import.meta.env.VITE_APP_BASE_URL
}
if (config.isFile) {
config.headers['Content-Type'] = 'multipart/form-data'
}
if (mainStore.token) {
config.headers.Authorization = mainStore.token
}
if (!config.data) {
config.data = {}
}
if (config.params) {
config.data = {
...config.data,
...config.params,
}
}
if (!config.isHideLoading) {
NProgress.start()
}
config.data.locale = Cookies.get('language') || 'zh_CN'
config.data.clientId = 'pc'
const timestamp: number = Date.parse(new Date().toString()) / 1000
const sign: string = buildSign(config.data, timestamp, mainStore.token)
config.headers['x-timestamp'] = timestamp
if (sign && !config.isHideSign) {
config.headers['x-sign'] = sign
}
if (config.data) {
if (config.method === 'get') {
config.url = `${config.url}?${qs.stringify(config.data)}`
}
if (config.headers['Content-Type'] !== 'multipart/form-data' && config.method !== 'get') {
config.data = qs.stringify(config.data, {
arrayFormat: 'indices',
allowDots: true,
})
}
}
return config
},
(error: any) => Promise.reject(error),
)
ajax.interceptors.response.use(
(response: any) => {
NProgress.done()
mainStore.setNetwork(true)
if (response.status === 200) {
if (response.data.errno === 0) {
return Promise.resolve(response.data)
}
if (!response.config.isHideError) {
notification.error({
message: '提示',
description: response.data.message,
})
}
return Promise.reject(response)
}
return Promise.reject(response)
},
(error: any) => {
NProgress.done()
mainStore.setNetwork(window.navigator.onLine)
const { response } = error
if (response) {
switch (response.status) {
case 401:
case 403:
notification.error({
message: '提示',
description: '登录过期,请重新登录',
duration: null,
})
setTimeout(() => {
mainStore.clearToken()
window.postMessage('NotAuthorized')
}, 1000)
break
case 404:
notification.error({
message: '提示',
description: '请求的资源不存在',
})
break
case 500:
case 501:
case 502:
case 503:
case 504:
case 505:
notification.error({
message: '提示',
description: '服务器端发生错误,请稍后再试',
})
break
default:
console.error(response.data.message)
break
}
return Promise.reject(response)
}
if (!window.navigator.onLine) {
notification.error({
message: '提示',
description: '网络连接断开,请检查网络',
})
}
return Promise.reject(error)
},
)
export default ajax定义请求接口
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// src/api/dashboard.ts
import ajax from '@/utils/ajax'
export function dashboard(data?: any) {
return ajax({
url: '/dashboard',
method: 'post',
isMock: true,
data
})
}
// src/api/login.ts
import ajax from '@/utils/ajax'
export function login(data: any) {
return ajax({
url: '/login',
method: 'post',
isMock: true,
data
})
}
export function getUserInfo() {
return ajax({
url: '/userinfo',
method: 'get',
isMock: true
})
}在组件中使用
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
38import { FunctionComponent } from 'react'
import { useNavigate } from 'react-router-dom'
import { inject, observer } from 'mobx-react'
import { useTranslation } from 'react-i18next'
import { MainStore } from '@/store/main'
import { dashboard } from '@/api/dashboard'
interface IProps {
mainStore?: MainStore
}
const Index: FunctionComponent<IProps> = inject('mainStore')(
observer(props => {
const navigate = useNavigate()
const { t } = useTranslation()
const { mainStore } = props
const logout = () => {
mainStore?.clearToken()
navigate('/login', { replace: true })
}
dashboard({ a: 'a', b: 1 })
.then((result: any) => {
console.log(result)
})
.catch((err: any) => {
console.log(err)
})
return (
<div>
<h1>{t('hello')}</h1>
<button onClick={logout}>退出登录</button>
</div>
)
})
)
export default Index
使用vite-plugin-mock
安装依赖
1
yarn add mockjs vite-plugin-mock -D
修改
vite.config.ts
文件1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18import { defineConfig, loadEnv } from 'vite'
import { viteMockServe } from 'vite-plugin-mock'
export default defineConfig(({ command, mode }) => {
const env = loadEnv(mode, process.cwd())
const isProd: boolean = env.VITE_ENV === 'PROD'
return {
base: './',
plugins: [
viteMockServe({
mockPath: './mock',
localEnabled: !isProd,
prodEnabled: false
})
]
}
})实例化Mock
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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115// mock/dashboard.ts
import { MockMethod } from 'vite-plugin-mock'
import Mock, { Random } from 'mockjs'
export default [
{
url: '/dashboard',
method: 'post',
// statusCode: 500,
// response: ({headers}: {headers:any}) => {
// // console.log(headers['x-sign'], headers['x-timestamp'], 'mock')
// return {
// errno: 0,
// message: 'success',
// data: Mock.mock({
// 'list|4': [
// {
// id: '@id',
// name: '@cname',
// date: Random.date('yyyy-MM-dd')
// }
// ]
// })
// }
// },
rawResponse: (req, res) => {
// console.log(req.headers, 'mock')
res.setHeader('Content-Type', 'application/json')
if (req.headers['x-sign']) {
res.statusCode = 200
res.end(
JSON.stringify({
errno: 0,
message: 'success',
data: Mock.mock({
'list|4': [
{
id: '@id',
name: '@cname',
date: Random.date('yyyy-MM-dd')
}
]
})
})
)
} else {
res.statusCode = 403
res.end('')
}
}
}
] as MockMethod[]
// mock/login.ts
import { MockMethod } from 'vite-plugin-mock'
import Mock, { Random } from 'mockjs'
import { splitQueryParams } from '@/utils/common'
export default [
{
url: '/login',
method: 'post',
rawResponse: async (req, res) => {
let reqbody = ''
await new Promise((resolve) => {
req.on('data', (chunk) => {
reqbody += chunk
})
req.on('end', () => resolve(undefined))
})
res.setHeader('Content-Type', 'application/json')
const body = splitQueryParams(reqbody)
if (!reqbody || !body || typeof body.username === 'undefined' || typeof body.password === 'undefined') {
res.statusCode = 403
res.end('')
return
}
res.statusCode = 200
if (body.username === 'admin' && body.password === 'password') {
res.end(
JSON.stringify({
errno: 0,
message: '登录成功',
data: {
token: Random.word(64)
}
})
)
} else {
res.end(
JSON.stringify({
errno: 1001,
message: '用户名或者密码错误'
})
)
}
}
},
{
url: '/userinfo',
method: 'get',
statusCode: 200,
response: () => {
return {
errno: 0,
message: 'success',
data: Mock.mock({
id: '@id',
name: '@cname',
date: Random.date('yyyy-MM-dd')
})
}
}
}
] as MockMethod[]
使用vite-plugin-svg-icons
加载svg图片
安装依赖
1
yarn add -D vite-plugin-svg-icons
修改
tsconfig.json
1
2
3
4
5
6{
"types": [
"node",
"vite-plugin-svg-icons/client"
]
}修改
vite.config.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18import { defineConfig, loadEnv } from 'vite'
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
const path = require('path')
export default defineConfig(({ command, mode }) => {
const env = loadEnv(mode, process.cwd())
const isProd: boolean = env.VITE_ENV === 'PROD'
return {
base: './',
plugins: [
createSvgIconsPlugin({
iconDirs: [path.resolve(__dirname, 'src/icons')],
symbolId: 'icon-[dir]-[name]',
}),
]
}
})在
main.tsx
中注册1
2
3
4
5import ReactDOM from 'react-dom/client'
import 'virtual:svg-icons-register'
import App from './App'
ReactDOM.createRoot(document.getElementById('root')!).render(<App />)定义组件
SvgIcon
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// src/components/SvgIcon/index.tsx
import { FunctionComponent } from 'react'
import PropTypes from 'prop-types'
import style from './svg-icon.module.less'
interface IProps {
className?: string
prefix?: string
// name值为svg文件名
name: string
color?: string
onClick?: (event: any) => void
}
const SvgIcon: FunctionComponent<IProps> = ({ className, prefix, name, color, onClick }) => {
const symbolId = `#${prefix}-${name}`
return (
<span className={className} onClick={onClick}>
<svg className={style['svg-icon']} aria-hidden="true">
<use href={symbolId} fill={color} />
</svg>
</span>
)
}
SvgIcon.propTypes = {
className: PropTypes.string,
prefix: PropTypes.string,
name: PropTypes.string.isRequired,
color: PropTypes.string,
onClick: PropTypes.func,
}
SvgIcon.defaultProps = {
prefix: 'icon',
color: '#333',
}
export default SvgIcon
1 | /* src/components/SvgIcon/svg-icon.module.less */ |
创建
src/icons
目录,并将svg文件存放到该目录下在组件中使用
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// src/views/dashboard/index.tsx
import { FunctionComponent } from 'react'
import { inject, observer } from 'mobx-react'
import { MainStore } from '@/store/main'
import style from './index.module.less'
interface IProps {
mainStore?: MainStore
}
const Index: FunctionComponent<IProps> = inject('mainStore')(
observer(props => {
const { mainStore } = props
const logout = () => {
mainStore?.clearToken()
}
return (
<div>
<SvgIcon className={style.icon} color="red" name="account-arrow-right-outline" />
<button onClick={logout}>退出登录</button>
</div>
)
})
)
export default Index
1 | // src/views/dashboard/index.module.less |