avatar

Vite2.x + React + TS起步

Vite2.x + react 18.x + TS起步

创建项目

Requires Node.js ^14.17.0, 16.0.0 or later. 因为部分依赖在低版本node中不兼容

1
2
3
4
yarn create vite vite-react --template react-ts
cd vite-react
yarn
yarn dev

修改语法检查与格式化配置

  1. 安装依赖

    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
  2. 修改.eslintignore文件

    1
    2
    3
    4
    /dist/
    /*.js
    /*.zip
    /*.rar
  3. 修改.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
    91
    module.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',
    },
    }
  4. 修改.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'
    }
  5. 修改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" }]
    }
  6. 修改tsconfig.node.json文件

    1
    2
    3
    4
    5
    6
    7
    8
    {
    "compilerOptions": {
    "composite": true,
    "module": "esnext",
    "moduleResolution": "node"
    },
    "include": ["vite.config.ts"]
    }
  7. 修改vite.config.ts文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    import { 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. 先安装依赖

    1
    2
    yarn add -D lint-staged
    npx husky-init && yarn
  2. 修改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"
    ]
    }
    }
  3. 修改.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. 先安装依赖

    1
    yarn add less less-loader @types/node -D
  2. 创建全局变量文件src/assets/styles/variables.less

    1
    @baseFontSize: 14px;
  3. 修改vite.config.ts文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    import { 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')}";`
    }
    }
    }
    }
    })
  4. 创建全局样式文件src/assets/styles/global.less

    1
    2
    3
    body {
    margin: 0;
    }
  5. main.tsx加载全局样式文件

    1
    2
    3
    4
    5
    import ReactDOM from 'react-dom/client'
    import App from './App'
    import './assets/styles/global.less'

    ReactDOM.createRoot(document.getElementById('root')!).render(<App />)
  6. 在组件内使用less

    1
    2
    3
    4
    /* src/components/MyButton/my-button.module.less */
    .my-button {
    font-size: @baseFontSize;
    }

修改环境变量配置

  1. 创建.env.development文件

    1
    2
    3
    VITE_ENV = DEV
    VITE_APP_BASE_URL = 'http://localhost/api'
    VITE_APP_TITLE = 'index'
  2. 创建.env.production文件

    1
    2
    3
    VITE_ENV = PROD
    VITE_APP_BASE_URL = 'https://www.pengwf.com'
    VITE_APP_TITLE = '首页'
  3. 修改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"
    }
    }
  4. 修改vite.config.ts文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    import { 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. 先安装依赖

    1
    2
    yarn add antd prop-types
    yarn add vite-plugin-imp @babel/plugin-proposal-class-properties @babel/plugin-proposal-decorators -D
  2. 修改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
    import { 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. 安装依赖

    1
    yarn add mobx mobx-react
  2. 创建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
    55
    import { 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)
    }

    @observable token = ''
    @observable network = true
    @observable userinfo: UserinfoType = {
    id: '',
    name: '',
    date: '',
    }

    @action('setToken')
    setToken = (token: string) => {
    this.token = token
    localStorage.setItem('token', token)
    }

    @action('clearToken')
    clearToken = () => {
    this.token = ''
    localStorage.removeItem('token')
    }

    @action('setNetwork')
    setNetwork(flag: boolean) {
    this.network = flag
    }

    @action('setUserinfo')
    setUserinfo(userinfo: UserinfoType) {
    this.userinfo = userinfo
    }
    }
    export default new mainStore()
  3. 创建src/store/index.ts

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    import mainStore, { MainStore } from './main'

    class Store {
    mainStore: MainStore
    constructor() {
    this.mainStore = mainStore
    }
    }

    export default new Store()
  4. 修改src/App.tsx

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    import { 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
  5. 在函数组件中使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    import { 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
  6. 在类组件中使用

    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
    import { Component } from 'react'
    import { inject, observer } from 'mobx-react'
    import { MainStore } from '@/store/main'

    interface IProps {
    mainStore?: MainStore
    }

    @inject('mainStore')
    @observer
    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. 安装依赖

    1
    yarn add react-router-dom@6 history
  2. 定义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()

  3. 修改src/App.tsx

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    import { 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
  4. 在函数组件中使用

    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
    import { 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
  5. 在类组件中使用

    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
    import { Component } from 'react'
    import { inject, observer } from 'mobx-react'
    import { MainStore } from '@/store/main'
    import { withRouter, WithRouterProps } from '@/utils/basicProps'

    interface IProps {
    mainStore?: MainStore
    }

    @inject('mainStore')
    @observer
    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. 安装依赖

    1
    yarn add react-i18next i18next
  2. 定义语言包

    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'
    }
  3. 修改main.tsx

    1
    2
    3
    4
    5
    6
    import ReactDOM from 'react-dom/client'
    import App from './App'
    import './i18n'
    import './assets/styles/global.less'

    ReactDOM.createRoot(document.getElementById('root')!).render(<App />)
  4. 在函数组件中使用

    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
    import { 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
  5. 在类组件中使用

    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
    import { 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
    }

    @inject('mainStore')
    @observer
    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压缩图片

  1. 修改package.json文件

    国内不搭梯子的话,基本上都会遇到安装失败的问题,需要切换到淘宝的源,并且在package.json文件增加以下配置

1
2
3
4
5
6
{
"scripts": {},
"resolutions": {
"bin-wrapper": "npm:bin-wrapper-china"
}
}
  1. 安装依赖

    1
    yarn add vite-plugin-imagemin -D
  2. 修改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
    36
    import { 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自定义入口文件

  1. 安装依赖

    新版本在我的mac上测试有bug,所以指定了一个比较低的版本

1
yarn add vite-plugin-html@3.1.0 -D
  1. 修改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
    import { 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>`
    }
    }
    }),
    ]
    }
    })
  2. 修改入口文件pages/index.html

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    <!DOCTYPE html>
    <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. 安装依赖

    1
    2
    yarn add axios js-cookie nprogress qs ts-md5
    yarn add @types/js-cookie @types/nprogress @types/qs -D
  2. 创建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
  3. 定义请求接口

    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
    })
    }
  4. 在组件中使用

    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
    import { 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. 安装依赖

    1
    yarn add mockjs vite-plugin-mock -D
  2. 修改vite.config.ts文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    import { 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
    })
    ]
    }
    })
  3. 实例化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. 安装依赖

    1
    yarn add -D vite-plugin-svg-icons
  2. 修改tsconfig.json

    1
    2
    3
    4
    5
    6
    {
    "types": [
    "node",
    "vite-plugin-svg-icons/client"
    ]
    }
  3. 修改vite.config.ts

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    import { 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]',
    }),
    ]
    }
    })
  4. main.tsx中注册

    1
    2
    3
    4
    5
    import ReactDOM from 'react-dom/client'
    import 'virtual:svg-icons-register'
    import App from './App'

    ReactDOM.createRoot(document.getElementById('root')!).render(<App />)
  5. 定义组件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
2
3
4
5
6
7
8
/* src/components/SvgIcon/svg-icon.module.less */
.svg-icon {
width: 1em;
height: 1em;
fill: currentColor;
overflow: hidden;
vertical-align: -0.15em;
}
  1. 创建src/icons目录,并将svg文件存放到该目录下

  2. 在组件中使用

    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
2
3
4
// src/views/dashboard/index.module.less
.icon {
font-size: 2em;
}
文章作者: pengweifu
文章链接: https://www.pengwf.com/2022/06/13/web/JS-Vite-React/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 麦子的博客
打赏
  • 微信
    微信
  • 支付宝
    支付宝

评论