avatar

Vite2.x + Vue.js3.x + TS起步

Vite2.x + Vue.js3.x + TS起步

创建项目

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

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

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

  1. 安装依赖

    Requires vue-eslint-parser 9.0.0 or later.

1
yarn add eslint eslint-plugin-vue vue-eslint-parser @typescript-eslint/parser @typescript-eslint/eslint-plugin eslint-config-airbnb-base eslint-plugin-import vite-plugin-eslint -D
  1. 修改.eslintignore文件

    1
    2
    3
    4
    /dist/
    /*.js
    /*.zip
    /*.rar
  2. 修改.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
    module.exports = {
    root: true,
    globals: {
    defineEmits: 'readonly',
    defineProps: 'readonly',
    },
    extends: ['plugin:@typescript-eslint/recommended', 'plugin:vue/vue3-recommended', 'airbnb-base'],
    parserOptions: {
    parser: '@typescript-eslint/parser',
    ecmaVersion: 2020,
    },
    rules: {
    'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off', // 禁用 debugger
    'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', // 禁用 console
    'no-bitwise': 'off', // 禁用按位运算符
    'no-tabs': 'off', // 禁用 tab
    'array-element-newline': ['error', 'consistent'], // 强制数组元素间出现换行
    indent: ['error', 2, { MemberExpression: 1, SwitchCase: 1, ignoredNodes: ['TemplateLiteral'] }], // 强制使用一致的缩进
    quotes: ['error', 'single'], // 强制使用一致的反勾号、双引号或单引号
    'comma-dangle': ['error', 'always-multiline'], // 要求或禁止末尾逗号
    'object-curly-spacing': ['error', 'always'], // 强制在大括号中使用一致的空格
    'arrow-body-style': 'off',
    'max-len': ['error', 120], // 强制一行的最大长度
    'no-new': 'off', // 禁止使用 new 以避免产生副作用
    'linebreak-style': 'off', // 强制使用一致的换行风格
    'import/extensions': 'off', // 确保在导入路径中统一使用文件扩展名
    'eol-last': 'off', // 要求或禁止文件末尾存在空行
    'no-shadow': 'off', // 禁止变量声明与外层作用域的变量同名
    'no-unused-vars': 'warn', // 禁止出现未使用过的变量
    'import/no-cycle': 'off', // 禁止一个模块导入一个有依赖路径的模块回到自己身上
    'arrow-parens': 'off', // 要求箭头函数的参数使用圆括号
    semi: ['error', 'never'], // 要求或禁止使用分号代替 ASI
    eqeqeq: 2, // 要求使用 === 和 !==
    'no-param-reassign': 'off', // 禁止对 function 的参数进行重新赋值
    'import/prefer-default-export': 'off', // 如果模块只输入一个名字,则倾向于默认输出
    'no-use-before-define': 2, // 禁止在变量定义之前使用它们,则倾向于默认输出
    'no-continue': 'off', // 禁用 continue 语句
    'prefer-destructuring': 'off', // 优先使用数组和对象解构
    'no-plusplus': 'off', // 禁用一元操作符 ++ 和 --
    'prefer-const': 'warn', // 要求使用 const 声明那些声明后不再被修改的变量
    'global-require': 2, // 要求 require() 出现在顶层模块作用域中
    'no-prototype-builtins': 'off', // 禁止直接调用 Object.prototypes 的内置属性
    'consistent-return': 'off', // 要求 return 语句要么总是指定返回的值,要么不指定
    'one-var-declaration-per-line': 'off', // 要求或禁止在变量声明周围换行
    'one-var': 'off', // 强制函数中的变量要么一起声明要么分开声明
    'import/named': 'off', // 确保命名导入与远程文件中的命名导出相对应
    'object-curly-newline': 'off', // 强制大括号内换行符的一致性
    'default-case': 'off', // 要求 switch 语句中有 default 分支
    'no-trailing-spaces': 2, // 禁用行尾空格
    'func-names': 'off', // 要求或禁止使用命名的 function 表达式
    radix: 'off', // 强制在 parseInt() 使用基数参数
    'no-unused-expressions': 'off', // 禁止出现未使用过的表达式
    'no-underscore-dangle': 'off', // 禁止标识符中有悬空下划线
    'no-nested-ternary': 'off', // 禁用嵌套的三元表达式
    'no-restricted-syntax': 'off', // 禁用特定的语法
    'no-await-in-loop': 'off', // 禁止在循环中出现 await
    'import/no-extraneous-dependencies': 'off', // 禁止使用外部包
    'import/no-unresolved': 'off', // 确保导入指向一个可以解析的文件/模块
    'template-curly-spacing': ['error', 'never'], // 要求或禁止模板字符串中的嵌入表达式周围空格的使用
    '@typescript-eslint/no-var-requires': 'off', // 除import语句外,禁止使用require语句
    '@typescript-eslint/no-empty-function': 'off', // 不允许空函数
    '@typescript-eslint/no-explicit-any': 'off', // 禁止使用 any 类型
    'guard-for-in': 'off', // 要求 for-in 循环中有一个 if 语句
    'class-methods-use-this': 'off', // 强制类方法使用 this
    'vue/html-indent': ['error', 2], // 在<template>中强制一致缩进
    'vue/html-self-closing': 'off', // 执行自闭合的风格
    'vue/multi-word-component-names': 'off',
    'vue/max-attributes-per-line': 'off',
    'vue/singleline-html-element-content-newline': 'off', // 要求单行元素的内容前后有一个换行符
    'import/order': 'off',
    },
    }
  3. 修改.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'
    }
  4. 修改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
    {
    "compilerOptions": {
    "target": "esnext",
    "useDefineForClassFields": true,
    "module": "esnext",
    "moduleResolution": "node",
    "strict": true,
    "jsx": "preserve",
    "sourceMap": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "esModuleInterop": true,
    "lib": ["esnext", "dom"],
    "skipLibCheck": true,
    "types": [
    "node"
    ],
    "baseUrl": "./",
    "paths": {
    "@/*": [
    "src/*"
    ],
    "@api/*": [
    "src/api/*"
    ]
    }
    },
    "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
    "exclude": ["node_modules"],
    "references": [{ "path": "./tsconfig.node.json" }]
    }
  5. 修改tsconfig.node.json文件

    1
    2
    3
    4
    5
    6
    7
    8
    {
    "compilerOptions": {
    "composite": true,
    "module": "esnext",
    "moduleResolution": "node"
    },
    "include": ["vite.config.ts"]
    }
  6. 修改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/**/*.vue', 'src/*.ts', 'src/*.vue'],
    }),
    ]
    }
    })

修改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,vue}": [
    "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.ts加载全局样式文件

    1
    2
    3
    4
    5
    6
    import { createApp } from 'vue'
    import App from '@/App.vue'
    import './assets/styles/global.less'

    const app = createApp(App)
    app.mount('#app')
  6. 在组件内使用less

    1
    2
    3
    4
    5
    <style lang="less" scoped>
    .my-button {
    font-size: @baseFontSize;
    }
    </style>

修改环境变量配置

  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 && vue-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')
    }
    }
    }
    })

批量加载全局组件

1
2
3
4
5
6
7
8
9
10
11
// src/components/index.ts
import { App, defineAsyncComponent } from "vue"

const componentsContext = import.meta.glob("./global/*.vue")

export default function install(app: App<Element>) {
for (const [key, value] of Object.entries(componentsContext)) {
const name = key.slice(key.lastIndexOf('/') + 1, key.lastIndexOf('.'))
app.component(name, defineAsyncComponent(value))
}
}
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
<!-- src/components/global/MyButton.vue -->
<script setup lang="ts">
const props = defineProps({
disabled: {
type: [Boolean],
default: false
}
})
const emit = defineEmits({
click: (evt: MouseEvent) => evt instanceof MouseEvent
})
const handleClick = (evt: MouseEvent) => {
emit('click', evt)
}
</script>

<template>
<button
:disabled="disabled"
:class="[
'my-button',
{
'is-disabled': disabled
}
]"
@click="handleClick"
>
<slot />
</button>
</template>

<style lang="less" scoped>
.my-button {
display: inline-flex;
justify-content: center;
align-items: center;
white-space: nowrap;
cursor: pointer;
line-height: 1;
text-align: center;
box-sizing: border-box;
outline: none;
transition: 0.1s;
user-select: none;
vertical-align: middle;
-webkit-appearance: none;
font-size: @baseFontSize;
background: #fff;
border: 1px solid #dcdfe6;
padding: 8px 15px;
border-radius: 4px;
}
</style>
1
2
3
4
5
6
7
8
// src/main.ts
import { createApp } from 'vue'
import App from '@/App.vue'
import components from "@/components/index"

const app = createApp(App)
app.use(components)
app.mount('#app')

使用 element-plus UI 库

  1. 先安装依赖

    1
    2
    yarn add element-plus
    yarn add unplugin-vue-components unplugin-auto-import -D
  2. 修改vite.config.ts文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    import { defineConfig } from 'vite'
    import vue from '@vitejs/plugin-vue'
    import AutoImport from 'unplugin-auto-import/vite'
    import Component from 'unplugin-vue-components/vite'
    import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'

    export default defineConfig(() => {
    return {
    plugins: [
    vue(),
    AutoImport({
    resolvers: [ElementPlusResolver()]
    }),
    Component({
    resolvers: [ElementPlusResolver()]
    })
    ]
    }
    })
  3. 在使用less而非scss的情况下需要全局加载样式文件,修改main.ts

    1
    2
    3
    4
    5
    6
    7
    import { createApp } from 'vue'
    import App from '@/App.vue'
    import './assets/styles/global.less'
    import 'element-plus/dist/index.css'

    const app = createApp(App)
    app.mount('#app')

使用pinia

  1. 安装依赖

    1
    yarn add pinia
  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
    import { defineStore } from 'pinia'

    export interface UserinfoType {
    id: string
    name: string
    date: string
    }

    export const mainStore = defineStore('main', {
    state: () => {
    return {
    token: '',
    network: true,
    userinfo: {} as UserinfoType,
    }
    },
    getters: {
    getToken(state) {
    return state.token
    },
    getNetwork(state) {
    return state.network
    },
    getUserinfo(state) {
    return state.userinfo
    },
    },
    actions: {
    setToken(token: string) {
    this.token = token
    localStorage.setItem('token', token)
    },
    clearToken() {
    this.token = ''
    localStorage.removeItem('token')
    },
    setNetwork(flag: boolean) {
    this.network = flag
    },
    setUserinfo(userinfo: UserinfoType) {
    this.userinfo = userinfo
    },
    },
    })
  3. 修改main.ts

    1
    2
    3
    4
    5
    6
    7
    8
    9
    import { createApp } from 'vue'
    import App from '@/App.vue'
    import { createPinia } from 'pinia'

    const app = createApp(App)
    const pinia = createPinia()

    app.use(pinia)
    app.mount('#app')
  4. 在组件中使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    <script setup lang="ts">
    import { mainStore } from '@/store/main'

    const store = mainStore()
    const logout = () => {
    store.clearToken()
    }
    </script>

    <template>
    <h1>首页</h1>
    <p><img alt="Vue logo" src="@/assets/images/logo.png" /></p>
    <p>
    <button @click="logout">退出登录</button>
    </p>
    </template>

使用vue-router

  1. 安装依赖

    1
    yarn add vue-router@4
  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
    // src/router/index.ts
    import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'

    const routes: RouteRecordRaw[] = [
    {
    path: '/',
    redirect: 'dashboard',
    },
    ]

    const modules = import.meta.globEager('./*.ts')
    for (const path in modules) {
    routes.push(...modules[path].default)
    }
    const router = createRouter({
    history: createWebHistory(),
    routes,
    })

    router.beforeEach((to, from) => {
    const { path: toPath } = to
    const { path: fromPath } = from
    if (toPath === fromPath) {
    return false
    }
    })

    export default router

    // src/router/dashboard.ts
    export default [
    {
    path: '/dashboard',
    name: 'dashboard',
    component: () => import('@/views/dashboard/index.vue')
    }
    ]

    // src/router/login.ts
    export default [
    {
    path: '/login',
    name: 'login',
    component: () => import('@/views/login/index.vue')
    }
    ]
  3. 修改main.ts

    1
    2
    3
    4
    5
    6
    7
    import { createApp } from 'vue'
    import App from '@/App.vue'
    import router from '@/router'

    const app = createApp(App)
    app.use(router)
    app.mount('#app')
  4. 在组件中使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    <script setup lang="ts">
    import { useRouter } from 'vue-router'
    import { mainStore } from '@/store/main'

    const router = useRouter()
    const store = mainStore()
    const logout = () => {
    store.clearToken()
    router.replace('/login')
    }
    </script>

    <template>
    <h1>首页</h1>
    <p>
    <button @click="logout">退出登录</button>
    </p>
    </template>

使用vue-i18n

  1. 安装依赖

    1
    yarn add vue-i18n
  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
    // src/i18n/index.ts
    import { createI18n } from 'vue-i18n'

    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] = value.default
    }

    const i18n = createI18n({
    locale: 'zh_CN',
    messages: language,
    })

    export default i18n

    // src/i18n/zh_CN.ts
    export default {
    hello: '你好'
    }

    // src/i18n/en_US.ts
    export default {
    hello: 'Hello'
    }
  3. 修改main.ts

    1
    2
    3
    4
    5
    6
    7
    import { createApp } from 'vue'
    import App from '@/App.vue'
    import i18n from '@/i18n'

    const app = createApp(App)
    app.use(i18n)
    app.mount('#app')
  4. 在组件中使用

    1
    2
    3
    4
    <template>
    <h1>首页</h1>
    <p>{{ $t('hello') }}</p>
    </template>

使用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
    37
    38
    import { defineConfig } from 'vite'
    import vue from '@vitejs/plugin-vue'
    import viteImagemin from 'vite-plugin-imagemin'

    export default defineConfig(() => {
    return {
    plugins: [
    vue(),
    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
    25
    26
    import { defineConfig, loadEnv } from 'vite'
    import vue from '@vitejs/plugin-vue'
    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: [
    vue(),
    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
    14
    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8" />
    <link rel="icon" href="/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title><%= title %></title>
    <%- injectScript %>
    </head>
    <body>
    <div id="app"></div>
    <script type="module" src="/src/main.ts"></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
    153
    154
    155
    156
    157
    158
    159
    // 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 { ElNotification } from 'element-plus'
    import { mainStore } from '@/store/main'
    import router from '@/router'

    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 = ''
    keys.forEach((key: string, i: number) => {
    const value: string = typeof params[key] === 'object' ? JSON.stringify(params[key]) : <string>params[key]
    paramsStr = `${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'
    }
    const store = mainStore()
    if (store.getToken) {
    config.headers.Authorization = store.getToken
    }
    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 = Date.parse(new Date().toString()) / 1000
    const sign = buildSign(config.data, timestamp, store.getToken)
    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()
    const store = mainStore()
    store.setNetwork(true)
    if (response.status === 200) {
    if (response.data.errno === 0) {
    return Promise.resolve(response.data)
    }
    if (!response.config.isHideError) {
    ElNotification({
    title: '提示',
    message: response.data.message,
    type: 'error',
    })
    }
    return Promise.reject(response)
    }
    return Promise.reject(response)
    },
    (error: any) => {
    NProgress.done()
    const store = mainStore()
    store.setNetwork(window.navigator.onLine)
    const { response } = error
    if (response) {
    switch (response.status) {
    case 401:
    case 403:
    ElNotification({
    title: '提示',
    message: '登录过期,请重新登录',
    duration: 0,
    type: 'error',
    })
    setTimeout(() => {
    router.replace('/login')
    }, 1000)
    break
    case 404:
    ElNotification({
    title: '提示',
    message: '请求的资源不存在',
    type: 'error',
    })
    break
    case 500:
    case 501:
    case 502:
    case 503:
    case 504:
    case 505:
    ElNotification({
    title: '提示',
    message: '服务器端发生错误,请稍后再试',
    type: 'error',
    })
    break
    default:
    console.error(response.data.message)
    break
    }
    return Promise.reject(response)
    }
    if (!window.navigator.onLine) {
    ElNotification({
    title: '提示',
    message: '网络连接断开,请检查网络',
    type: 'error',
    })
    }
    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
    <script setup lang="ts">
    import { onMounted, ref } from 'vue'
    import { dashboard } from '@/api/dashboard'
    import { useRouter } from 'vue-router'
    import { mainStore } from '@/store/main'

    const router = useRouter()
    const store = mainStore()
    const userinfo = store.getUserinfo
    const username = ref(userinfo.name)
    onMounted(() => {
    dashboard({ a: 'a', b: 1 })
    .then((result: any) => {
    console.log(result)
    })
    .catch((err: any) => {
    console.log(err)
    })
    })
    const logout = () => {
    store.clearToken()
    router.replace('/login')
    }
    </script>

    <template>
    <h1>首页</h1>
    <p><img alt="Vue logo" src="@/assets/images/logo.png" /></p>
    <p>{{ $t('hello') }}, {{ username }}</p>
    <p>
    <my-button @click="logout">退出登录</my-button>
    </p>
    </template>

使用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
    19
    20
    import { defineConfig, loadEnv } from 'vite'
    import vue from '@vitejs/plugin-vue'
    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: [
    vue(),
    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
    19
    20
    import { defineConfig, loadEnv } from 'vite'
    import vue from '@vitejs/plugin-vue'
    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: [
    vue(),
    createSvgIconsPlugin({
    iconDirs: [path.resolve(__dirname, 'src/icons')],
    symbolId: 'icon-[dir]-[name]',
    }),
    ]
    }
    })
  4. main.ts中注册

    1
    2
    3
    4
    5
    6
    import { createApp } from 'vue'
    import 'virtual:svg-icons-register'
    import App from '@/App.vue'

    const app = createApp(App)
    app.mount('#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
    39
    40
    41
    42
    43
    44
    45
    // src/components/global/SvgIcon.vue
    <script setup lang="ts">
    import { computed } from 'vue'

    const props = defineProps({
    prefix: {
    type: String,
    default: 'icon',
    },
    // name值为svg文件名
    name: {
    type: String,
    required: true,
    },
    color: {
    type: String,
    default: '#333',
    },
    })
    const symbolId = computed(() => `#${props.prefix}-${props.name}`)
    const emit = defineEmits({
    click: (evt: MouseEvent) => evt instanceof MouseEvent,
    })
    const handleClick = (evt: MouseEvent) => {
    emit('click', evt)
    }
    </script>

    <template>
    <span @click="handleClick">
    <svg class="svg-icon" aria-hidden="true">
    <use :href="symbolId" :fill="color" />
    </svg>
    </span>
    </template>

    <style lang="less" scoped>
    .svg-icon {
    width: 1em;
    height: 1em;
    fill: currentColor;
    overflow: hidden;
    vertical-align: -0.15em;
    }
    </style>
  6. 创建src/icons目录,并将svg文件存放到该目录下

  7. 在组件中使用

    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
    <script setup lang="ts">
    import { onMounted, ref } from 'vue'
    import { dashboard } from '@/api/dashboard'
    import { useRouter } from 'vue-router'
    import { mainStore } from '@/store/main'

    const router = useRouter()
    const store = mainStore()
    const userinfo = store.getUserinfo
    const username = ref(userinfo.name)
    onMounted(() => {
    dashboard({ a: 'a', b: 1 })
    .then((result: any) => {
    console.log(result)
    })
    .catch((err: any) => {
    console.log(err)
    })
    })
    const logout = () => {
    store.clearToken()
    router.replace('/login')
    }
    </script>

    <template>
    <h1>首页</h1>
    <p><img alt="Vue logo" src="@/assets/images/logo.png" /></p>
    <p>{{ $t('hello') }}, {{ username }}</p>
    <p>
    <svg-icon class="logout" color="red" name="account-arrow-right-outline" />
    <my-button @click="logout">退出登录</my-button>
    </p>
    </template>

    <style type="text/less" scoped>
    .logout {
    font-size: 2em;
    }
    </style>
文章作者: pengweifu
文章链接: https://www.pengwf.com/2022/05/15/web/JS-Vite/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 麦子的博客
打赏
  • 微信
    微信
  • 支付宝
    支付宝

评论