[Ingeniería de front-end] Explicación detallada de vite (2)--barril de la familia vue3 + ts para construir el sistema de gestión posterior

Instalar el paquete base

npm create vite@latest
# 这里选择的是Vue+Typescript的组合
cd vue-admin
npm install

# 先安装基础包
npm install vue-router@4
npm i pinia
npm i axios
npm install sass --save-dev
npm install element-plus --save
npm install @element-plus/icons-vue
npm install -D unplugin-vue-components unplugin-auto-import
npm i eslint -D

# 提交规范
npm i lint-staged husky  --save-dev
npm install @commitlint/cli @commitlint/config-conventional -D

especificación de código

npm init @eslint/config

Habrá un montón de indicaciones, las opciones son las siguientes:

Need to install the following packages:
  @eslint/create-config
Ok to proceed? (y)
√ How would you like to use ESLint? · style       
√ What type of modules does your project use? · esm
√ Which framework does your project use? · vue
√ Does your project use TypeScript? · No / Yes
√ Where does your code run? · browser
√ How would you like to define a style for your project? · guide
√ Which style guide do you want to follow? · standard-with-typescript
√ What format do you want your config file to be in? · JavaScript
Checking peerDependencies of eslint-config-standard-with-typescript@latest
The config that you've selected requires the following dependencies:

eslint-plugin-vue@latest eslint-config-standard-with-typescript@latest @typescript-eslint/eslint-plugin@^5.50.0 eslint@^8.0.1 eslint-plugin-import@^2.25.2 eslint-plugin-n@^15.0.0 eslint-plugin-promise@^6.0.0 typescript@*
√ Would you like to install them now? · No / Yes
√ Which package manager do you want to use? · npm
Installing eslint-plugin-vue@latest, eslint-config-standard-with-typescript@latest, @typescript-eslint/eslint-plugin@^5.50.0, eslint@^8.0.1, eslint-plugin-import@^2.25.2, eslint-plugin-n@^15.0.0, eslint-plugin-promise@^6.0.0, typescript@*

Se generará un archivo en el proyecto .eslintrc.cjs, y luego configure el script para verificarlo:

"lint": "eslint src/**/*.{js,jsx,vue,ts,tsx} --fix"

Sin embargo, se informó un error al ejecutar. Dado que mi rango "typescript": "^5.1.3",de versión @typescript-eslint/typescript-estreecompatible actual tses: =3.3.1 <5.1.0, entonces tengo que degradar: [email protected], hay muchos problemas en la forma de configurar eslint, y la solución se proporciona directamente:

Lo primero es modificar .eslintrc.cjs:

module.exports = {
    
    
  env: {
    
    
    browser: true,
    es2021: true
  },
  extends: [
    'plugin:vue/vue3-essential',
    'standard-with-typescript'
  ],
  parser: "vue-eslint-parser",
  overrides: [
  ],
  parserOptions: {
    
    
    ecmaVersion: 'latest',
    sourceType: 'module',
    project: ["./tsconfig.json"],
    parser: "@typescript-eslint/parser",
    extraFileExtensions: ['.vue']
  },
  plugins: [
    'vue'
  ],
  rules: {
    
    
    'space-before-function-paren': [2, {
    
    
      anonymous: 'always',
      named: 'never',
      asyncArrow: 'always'
    }],
    'vue/multi-word-component-names': 0,
    "space-before-function-paren": 0,
    "@typescript-eslint/consistent-type-assertions": 0,
    "@typescript-eslint/ban-types": [
      "error",
      {
    
    
        "extendDefaults": true,
        "types": {
    
    
          "{}": false
        }
      }
    ]
  }
}

Agregue una línea de comentarios para vite-env.d.tsignorar el cheque:

// eslint-disable-next-line @typescript-eslint/triple-slash-reference
/// <reference types="vite/client" />

Con respecto eslinta los problemas encontrados en la configuración, puede consultar el escrito escrito por este tipo grande para obtener más detalles: Eslint: agregue eslint (reglas estándar) al proyecto vue3

especificación de confirmación

git init

Agregue el siguiente código en package.json, utilícelo para llamar a eslint y stylelint para verificar el código en el área de almacenamiento temporal

"lint-staged": {
    
    
    "*.{vue,js}": [
      "npm run lint"
    ]
  }

implementar:

npm pkg set scripts.postinstall="husky install"
# 等同于执行npm i,执行过程中会生成.husky文件夹
npm run postinstall

npx husky add .husky/pre-commit "npm lint"
git add .husky/pre-commit

git commitDe esta forma, se ejecutará automáticamente cuando lo ejecutemos npm lint.

Es vergonzoso Durante el proceso de ejecución, se informó un error que indica que el nodo no es un comando interno o externo. No hay problema, probablemente sea el problema de la herramienta nvm, por lo que voltanode -v se cambió más tarde para hacer el control de versiones de node.

npx husky add .husky/commit-msg "npx --no -- commitlint --edit $1"

nuevo commitlint.config.cjs:

module.exports = {
    
    
  extends: ['@commitlint/config-conventional'],
  rules: {
    
    
    'type-enum': [2, 'always', [
      'feat', // 新增功能
      'update', // 更新功能
      'ui', // 样式改动
      'fix', // 修复功能bug
      'merge', // 合并分支
      'refactor', // 重构功能
      'perf', // 性能优化
      'revert', // 回退提交
      'style', // 不影响程序逻辑的代码修改(修改空白字符,格式缩进,补全缺失的分号等)
      'build', // 修改项目构建工具(例如 glup,webpack,rollup 的配置等)的提交
      'docs', // 文档新增、改动
      'test', // 增加测试、修改测试
      'chore' // 不修改src或者test的其余修改,例如构建过程或辅助工具的变动
    ]],
    'scope-empty': [0],
    // 'scope-empty': [2, 'never'], 作用域不为空
    'scope-case': [0],
    'subject-full-stop': [0],
    'subject-case': [0]
  }
}

modificar tsconfig.json:

"include": [
  //...
  "commitlint.config.cjs"
 ],

modificar .eslintrc.cjs:

project: ["./tsconfig.json", "./commitlint.config.cjs"],
git add .
# 失败
git commit -m "commit校验"
# 成功
git commit -m "feat: commit校验"

establecer alias de ruta

import {
    
     defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'

const resolve = (dist) => path.resolve(__dirname, dist)

export default defineConfig({
    
    
  plugins: [vue()],
  resolve: {
    
    
    alias: {
    
    
      '@': resolve('src')
    },
    // 顺便把可以省略的后缀配置一下,在vite中不支持省略.vue
    extensions: [".js", ".ts", ".tsx", ".jsx"]
  }
})

Modificado tsconfig.jsony añadido:

"compilerOptions": {
    
    
    // ...
    "baseUrl": ".",
    "paths": {
    
    
      "@/*": ["src/*"]
    }
  },

restablecer estilo

assetsCrear una nueva carpeta en styles/reset.css:

/**
 * Eric Meyer's Reset CSS v2.0 (http://meyerweb.com/eric/tools/css/reset/)
 * http://cssreset.com
 */
 
 html, body, div, span, applet, object, iframe,
 h1, h2, h3, h4, h5, h6, p, blockquote, pre,
 a, abbr, acronym, address, big, cite, code,
 del, dfn, em, img, ins, kbd, q, s, samp,
 small, strike, strong, sub, sup, tt, var,
 b, u, i, center,
 dl, dt, dd, ol, ul, li,
 fieldset, form, label, legend,
 table, caption, tbody, tfoot, thead, tr, th, td,
 article, aside, canvas, details, embed, 
 figure, figcaption, footer, header, hgroup, 
 menu, nav, output, ruby, section, summary,
 time, mark, audio, video{
    
    
   margin: 0;
   padding: 0;
   border: 0;
   font-size: 100%;
   font: inherit;
   font-weight: normal;
   vertical-align: baseline;
 }
 /* HTML5 display-role reset for older browsers */
 article, aside, details, figcaption, figure, 
 footer, header, hgroup, menu, nav, section{
    
    
   display: block;
 }
 ol, ul, li{
    
    
   list-style: none;
 }
 blockquote, q{
    
    
   quotes: none;
 }
 blockquote:before, blockquote:after,
 q:before, q:after{
    
    
   content: '';
   content: none;
 }
 table{
    
    
   border-collapse: collapse;
   border-spacing: 0;
 }
  
 /* custom */
 a{
    
    
   color: #7e8c8d;
   text-decoration: none;
   backface-visibility: hidden;
   -webkit-backface-visibility: hidden;
 }
 ::-webkit-scrollbar{
    
    
   width: 5px;
   height: 5px;
 }
 ::-webkit-scrollbar-track-piece{
    
    
   background-color: rgba(0, 0, 0, 0.2);
   border-radius: 6px;
   -webkit-border-radius: 6px;
 }
 ::-webkit-scrollbar-thumb:vertical{
    
    
   height: 5px;
   background-color: rgba(125, 125, 125, 0.7);
   border-radius: 6px;
   -webkit-border-radius: 6px;
 }
 ::-webkit-scrollbar-thumb:horizontal{
    
    
   width: 5px;
   background-color: rgba(125, 125, 125, 0.7);
   border-radius: 6px;
   -webkit-border-radius: 6px;
 }
 html, body{
    
    
   width: 100%;
   font-family: "Arial", "Microsoft YaHei", "黑体", "宋体", "微软雅黑", sans-serif;
 }
 body{
    
    
   line-height: 1;
   -webkit-text-size-adjust: none;
   -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
 }
 html{
    
    
   overflow-y: scroll;
 }
  
 /*清除浮动*/
 .clearfix:before,
 .clearfix:after{
    
    
   content: " ";
   display: inline-block;
   height: 0;
   clear: both;
   visibility: hidden;
 }
 .clearfix{
    
    
   *zoom: 1;
 }
  
 /*隐藏*/
 .dn{
    
    
   display: none;
 }
 

Usar SCSS

Vite proporciona soporte integrado para .scss, .sass, y archivos. No es necesario instalar complementos específicos para ellos, pero sí deben instalarse las dependencias de preprocesador correspondientes..less.styl.stylusVite

Generalmente, definiremos algunos colores de tema en el proyecto:

// variable.scss
$font-color-gray:rgb(147,147,147);

O algunos estilos de colección encapsulados:

// mixins.scss
@mixin line-clamp($lines) {
    
    
  word-break: break-all;
  display: -webkit-box;
  overflow: hidden;
  text-overflow: ellipsis;
  -webkit-line-clamp: $lines;
  -webkit-box-orient: vertical;
}

@mixin ellipsis() {
    
    
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}


Luego vite.config.jsconfiguramos en:

css: {
    
    
    preprocessorOptions: {
    
    
      scss: {
    
    
        additionalData: '@import "@/assets/styles/variable.scss";@import "@/assets/styles/mixins.scss";'
      }
    }
  }

Elimine el directorio style.cssy cree uno nuevo styles/common.scss:

// common.scss
@import url('./reset.css');

Luego main.tsimporta en:

import '@/assets/styles/common.scss'

De esta forma se inicializa el estilo global, luego se prueba si la suma funciona, variable.scssy mixins.scssse modifica HelloWorld.vuepara ver si es gris y se omiten las dos líneas:

<script setup lang="ts">
</script>

<template>
  <div class="box">没错,这里要使用浏览器的获取媒体设备的 api 来拿到摄像头的视频流,设置到 video 上,然后对 video 做下镜像反转,加点模糊就好了。</div>
</template>

<style scoped lang="scss">
.box {
      
      
  color: $font-color-gray;
  height: 40px;
  line-height: 20px;
  width: 200px;
  @include line-clamp(2);
}
</style>

configurar el enrutamiento

Ahora para configurar el enrutamiento, espero tener este resultado:

- 登录页
- 带菜单栏的框架
  - 主页
  - 人员管理
    - 客户管理
    - 员工管理
- 404

Así que crea los siguientes archivos:

inserte la descripción de la imagen aquí

En este proceso nos encontraremos con varios problemas:

  • Al escribir rutas, introducimos componentes, que deben tener .vuesufijos;
  • import xxx from '@/xxx'Se informará un error porque no ha tsconfg.jsonestablecido el valor baseUrlde la suma pathscomo se mencionó anteriormente.

Dado que layoutlas subrutas deben estar anidadas en el archivo, es layoutnecesario agregar router-view:

<template>
  <div>布局</div>
  <router-view></router-view>
</template>

Otros archivos solo necesitan ser escritos así:

<template>
  <p>主页</p>
</template>

Nueva routercarpeta:

- router
  -hooks # 后期做登录校验和鉴权用的
  - routes
    - index.ts # 总输出文件
    - others.ts # 不需要layout这一层的路由均可以放在这里
    - person.ts # 人员管理模块
  - index.ts   # 总输出文件

El siguiente es el contenido de cada archivo:

// person.ts
export default [
  {
    
    
    path: '/person',
    name: 'Person',
    meta: {
    
     title: '人员管理' },
    redirect: '/person/customer',
    children: [
      {
    
    
        path: '/person/customer',
        name: 'PersonCustomer',
        meta: {
    
     title: '客户管理' },
        component: () => import('@/views/person/customer/index.vue')
      },
      {
    
    
        path: '/person/staff',
        name: 'PersonStaff',
        meta: {
    
     title: '员工管理' },
        component: () => import('@/views/person/staff/index.vue')
      }
    ]
  }
];

// others.ts
export default [
  {
    
    
    path: '/login',
    name: 'Login',
    meta: {
    
     title: '登录' },
    component: () => import('@/views/login/index.vue')
  }
];

// router/routes/index.ts
import Layout from '@/views/layout/index.vue';
import personRoutes from './person';
import otherRoutes from './others';

export default [
  {
    
    
    path: '/',
    name: 'Layout',
    component: Layout,
    children: [
      {
    
    
        path: '/',
        name: 'Index',
        meta: {
    
     title: '主页' },
        component: () => import('@/views/index/index.vue')
      },

      ...personRoutes,
    ]
  },
  ...otherRoutes,
  {
    
    
    path: '/404',
    name: 'NotFound',
    meta: {
    
     title: '404' },
    component: () => import('@/views/404/index.vue')
  },
  {
    
    
    path: "/:pathMatch(.*)",
    redirect: "/404",
    name:'ErrorPage',
    meta: {
    
     title: '' },
  }
];

Deje a un lado la carpeta primero hooks, simplemente escriba index.ts:

// router/index.ts
import routes from "./routes";
export default routes;

Crear uno nuevo src/plugins/index.tsCuando registramos el contenido antes, lo pusimos directamente en main.ts, que no es fácil de mantener, por lo que lo montaremos aquí de manera uniforme en el futuro:

// src/plugins/index.ts
import {
    
     createRouter, createWebHashHistory } from 'vue-router';
import routes from '@/router/index';

export default (app: any) => {
    
    
  // 注册路由
  const router = createRouter({
    
    
    history: createWebHashHistory(),
    routes
  })

  app.use(router);
}

No olvides modificar App.vue:

<template>
  <router-view></router-view>
</template>
import {
    
     createApp } from 'vue'
import '@/assets/styles/common.scss'
import App from './App.vue'
import installPlugins from '@/plugins';

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

Esto permitirá probar:

http://127.0.0.1:5173/#/
http://127.0.0.1:5173/#/login
http://127.0.0.1:5173/#/person
http://127.0.0.1:5173/#/person/customer
http://127.0.0.1:5173/#/person/staff

Si se encuentra un error durante el proceso de configuración: xxx.vueno se puede encontrar el módulo " " o su declaración de tipo correspondiente, agregue vite-env.d.ts:

declare module '*.vue' {
    
    
  import type {
    
     DefineComponent } from 'vue';
  const vueComponent: DefineComponent<{
    
    }, {
    
    }, any>;
  export default vueComponent;
} 

usar elemento plus

Si usa Volar, especifique el tipo de componente global tsconfig.jsona través de compilerOptions.type.

// tsconfig.json
{
    
    
  "compilerOptions": {
    
    
    // ...
    // 然而这个配置在后期打包的时候报错了...
    "types": ["element-plus/global"]
  }
}

Aquí, se adopta el método de importación bajo demanda, si no persigue el volumen, puede utilizar la importación completa:

// vite.config.ts
import {
    
     defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'

// 新增
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import {
    
     ElementPlusResolver } from 'unplugin-vue-components/resolvers'

const resolve = (dist) => path.resolve(__dirname, dist)

export default defineConfig({
    
    
  plugins: [
    vue(),
    // 新增
    AutoImport({
    
    
      resolvers: [ElementPlusResolver()]
    }),
    // 新增
    Components({
    
    
      resolvers: [ElementPlusResolver()]
    })
  ],
  resolve: {
    
    
    alias: {
    
    
      '@': resolve('./src')
    },
    extensions: [".js", ".ts", ".tsx", ".jsx"]
  },
  css: {
    
    
    preprocessorOptions: {
    
    
      scss: {
    
    
        additionalData: '@import "@/assets/styles/variable.scss";@import "@/assets/styles/mixins.scss";'
      }
    }
  }
})


element plusLos componentes como la fecha en chino están en inglés de forma predeterminada, por lo que cambiamos los componentes a chino:

inserte la descripción de la imagen aquí

modificar App.vue:

<script setup lang="ts">
import locale from 'element-plus/lib/locale/lang/zh-cn'
</script>

<template>
  <el-config-provider :locale="locale">
    <router-view></router-view>
  </el-config-provider>
</template>

Esto aparecerá en chino.

inserte la descripción de la imagen aquí

Con respecto a los dos complementos presentados, aquí hay una explicación:

  • unplugin-vue-componentsSe utiliza para identificar automáticamente los componentes utilizados en la plantilla de Vue, importar y registrar automáticamente bajo demanda;
  • unplugin-auto-importvite、webpackLas bibliotecas de configuración de uso común se pueden importar automáticamente bajo demanda en otros entornos.Por APIejemplo Vue, refno se requiere trabajo manual import, por lo que podemos configurarlo y eliminar la introducción de algunas API:
export default defineConfig({
    
    
  plugins: [
    // ...
    AutoImport({
    
    
      imports: [
        'vue',
        'vue-router',
        'pinia'
      ],
      eslintrc: {
    
    
        enabled: true,
        filepath: './.eslintrc-auto-import.json',
        globalsPropValue: true
      },
      resolvers: [ElementPlusResolver()]
    }),
    // ...
  ],
})

Una vez que se guarde, auto-imports.d.tsel contenido se completará automáticamente y .eslintrc-auto-import.jsonla configuración de la variable global eslint se generará en el directorio raíz del proyecto.

Luego modifique tsconfg.jsony .eslintrc.cjs:

// tsconfg.json
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue", "commitlint.config.cjs", "auto-imports.d.ts"],

// .eslintrc.cjs
project: ["./tsconfig.json", "./commitlint.config.cjs", './.eslintrc-auto-import.json'],

Ignorar auto-imports.d.tsla validación de ESLint

# .eslintignore
auto-imports.d.ts

Aquí debes prestar atención:

  1. No todas las API , como las de Vue Router, createRouterno se importarán. Para las API específicas que se pueden importar automáticamente, consulte unplugin-auto-import/src/presets
  2. .eslintrc-auto-import.jsonSi no necesita agregar la configuración después de generar el archivo, se recomienda enabled: trueconfigurarlo false, de lo contrario, este archivo se generará cada vez.

Después de la configuración, puede eliminar algunas referencias en la página y encontró que no hay problema.

<script lang='ts' setup>
// import { storeToRefs } from 'pinia'
// import { useRouter } from 'vue-router'
// ...
</script>

Pruebe el componente:

<template>
  <p><el-button>测试</el-button></p>
</template>

Esto mostrará el botón en la página.

El principio de la importación automática bajo demanda es importar <template>automáticamente los componentes utilizados en la identificación, que es similar a ElMessagelos componentes que llaman directamente a los métodos en JS. El complemento no reconocerá ni completará la importación automática, por lo que aún debe importarlo manualmente (se recomienda que aún importe archivos de estilo completos para evitar tales problemas de límites):

Modificar vite-env.d.ts, de lo contrario, informará de un error cuando se introduzca en ts element plus:

declare module "element-plus";

plugins/element-plus.tsPruébalo en pequeñas y medianas:

import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import {
    
     ElLoading, ElMessage, ElMessageBox, ElNotification } from 'element-plus'
import 'element-plus/theme-chalk/index.css'

const options = {
    
    
  size: 'small',
  zIndex: 3000
}

const components = [
  ElLoading,
  ElMessage,
  ElMessageBox, 
  ElNotification
]

export default function install (app: any): void {
    
    
  for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
    
    
    app.component(key, component)
  }

  components.forEach((component) => {
    
    
    app.use(component, options)
  })
}


// plugins/index.ts
export default (app: any) => {
    
    
  // ...

  // 注册element-plus
  installElementPlus(app);
}

tener una prueba:

<template>
  <p><el-button @click="onTest">测试</el-button></p>
</template>

<script setup lang="ts">
const onTest = () => {
      
      
  ElLoading.service({
      
       fullscreen: true });
}
</script>

propiedades globales

De acuerdo con el hábito anterior, loadingla invocación de definitivamente no es el método anterior, sino que está montado en el global Vue.prototype. Sin embargo, en este proyecto, usamos la importación bajo demanda, y en Vue3, la forma de escribir ha cambiado. Es posible que desee escribir así:

app.config.globalProperties.$loading = ElLoading;
app.config.globalProperties.$message = ElMessage;
app.config.globalProperties.$msgBox = ElMessageBox;
app.config.globalProperties.$notification = ElNotification;

Luego, en el proceso de uso:

<script setup lang="ts">
const instance = getCurrentInstance()

onMounted(() => {
    
    
  instance.proxy.$message.success('setup - getCurrentInstance() 成功使用')
  // 也可以使用 appContext
  console.log(instance.appContext.config.globalProperties.$message === instance.proxy.$message) // true
})
</script>

Sin embargo, después de consultar los documentos oficiales, no existe getCurrentInstancetal método, que probablemente no cumpla con las especificaciones. Entonces, la inyección del método global, la adopté provide/inject.

App.vue

<script setup lang="ts">
import locale from 'element-plus/lib/locale/lang/zh-cn'
import {
      
       ElLoading, ElMessage, ElMessageBox, ElNotification } from 'element-plus'

provide('$loading', ElLoading)
provide('$message', ElMessage)
provide('$messagebox', ElMessageBox)
provide('$notification', ElNotification)
</script>

<template>
  <el-config-provider :locale="locale">
    <router-view></router-view>
  </el-config-provider>
</template>

modificar element-plus.ts:

import {
    
     App } from 'vue'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
// import { ElLoading, ElMessage, ElMessageBox, ElNotification } from 'element-plus'
import 'element-plus/theme-chalk/index.css'

// const options = {
    
    
//   size: 'small',
//   zIndex: 3000
// }

// const components = [
//   ElLoading,
//   ElMessage,
//   ElMessageBox, 
//   ElNotification
// ]

export default function install (app: App): void {
    
    
  for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
    
    
    app.component(key, component)
  }

  // components.forEach((component) => {
    
    
  //   app.use(component, options)
  // })
}

Añadido src/types/global.d.ts:

import {
    
     ElLoading, ElMessage, ElMessageBox, ElNotification } from 'element-plus'

export interface ComponentCustomProperties {
    
    
  $message: typeof ElMessage
  $msgBox: typeof ElMessageBox
  $loading: typeof ElLoading
  $notification: typeof ElNotification
}


prueba:

<template>
  <p><el-button @click="onTest">测试</el-button></p>
</template>

<script setup lang="ts">
const $loading = inject('$loading') as any 
const onTest = () => {
      
      
  $loading.service({
      
      
    lock: true,
    text: 'Loading',
  })
}
</script>

Revise algunos métodos comunes de escritura de ts

El servidor de desarrollo y el empaquetador solo tsrealizarán escapes de sintaxis en los archivos sin ningún tipo de verificación, lo que garantiza que viteel servidor de desarrollo tsseguirá siendo ultrarrápido cuando se use.

Estos son algunos ejemplos comunes:

<!-- 对props的类型声明和默认值 -->
<script setup lang="ts">
interface Props {
      
      
  msg?: string
  labels?: string[]
}
const props = withDefaults(defineProps<Props>(), {
      
      
   msg: 'hello',
   labels: () => ['one', 'two']
})
</script>


<!-- 另一种方式 -->
<script setup lang="ts">
interface Book {
      
      
  title?: string
  author: string
}
const props = defineProps({
      
      
  book: Object as PropType<Book>
})
</script>


<!-- 对emits进行声明 -->
<script setup lang="ts">
// 1.运行时
const emit = defineEmits(['change', 'update'])

// 2.基于类型
const emit = defineEmits<{
      
      
  (e: 'change', id: number): void
  (e: 'update', value: string): void
}>()
</script>


<!-- 为computed指定返回类型 --->
<script setup lang="ts">
const double = computed<number>(() => {
      
       /** return number */ })
</script>

<!-- 为函数参数标注类型 -->
<script setup lang="ts">
const onClick = (e: Event) => {
      
      
  console.log((event.target as HTMLInputElement).value)
}
</script>

<!-- project和inject 然而我很少用到 -->
<script setup lang="ts">
import {
      
       provide, inject } from 'vue'
import type {
      
       InjectionKey } from 'vue'

const key = Symbol() as InjectionKey<string>
provide(key, 'foo')

const foo = inject<string>('foo', 'bar')
</script>

<!-- 为模板引用标注类型 -->
<script setup lang="ts">
import {
      
       ref, onMounted } from 'vue'
const el = ref<HTMLInputElement | null>(null)

onMounted(() => {
      
      
  el.value?.focus()
})
</script>

<template>
  <input ref="el" />
</template>


<!-- 为组件模板引用标注类型 -->
<script setup lang="ts">
import MyModal from './MyModal.vue'

const modal = ref<InstanceType<typeof MyModal> | null>(null)

const openModal = () => {
      
      
  modal.value?.open()
}
</script>

Iniciar sesión página estática

Los componentes se utilizarán en la página de inicio de sesión el-input. Generalmente, para este tipo de componentes de formulario, la frecuencia de gestión de publicaciones sigue siendo muy alta, por lo que tiende a ser reempaquetado y reutilizado. Generalmente, sus espacios frontal y posterior se optimizarán y desmontarán textarea:

<script lang='ts' setup>
import {
      
       computed } from 'vue';
const emits = defineEmits<{
      
      
  (e: 'update:value', value: string): void;
  (e: 'blur'): void;
  (e: 'focus'): void;
  (e: 'change', value: string): void;
  (e: 'clear'): void;
}>()

const props = defineProps({
      
      
  type: {
      
      
    type: String,
    default: 'string'
  },
  value: {
      
      
    type: [String, Number],
    default: '',
    required: true
  },
  maxlength: [String, Number],
  minlength: [String, Number],
  placeholder: {
      
      
    type: String,
    default: '请输入',
  },
  clearable: {
      
      
    type: Boolean,
    default: true
  },
  showPassword: {
      
      
    type: Boolean,
    default: false
  },
  disabled: {
      
      
    type: Boolean,
    default: false
  },
  prefixIcon: {
      
      
    type: String,
    default: ''
  },
  suffixIcon: {
      
      
    type: String,
    default: ''
  },
  inputStyle: [String, Object],
  showWordLimit: {
      
      
    type: Boolean,
    default: true
  },
  rows: Number,
  autosize: [Boolean, Object]
})

const input = computed({
      
      
  get(){
      
      
    console.log(props.value)
    return props.value;
  },

  set(val: any){
      
      
    if(typeof val === 'string') {
      
      
      val = val ? val.trim() : val;
    }
    emits('update:value', val);
  }
})

const onFocus = () => {
      
      
  emits('focus');
}

const onBlur = () => {
      
      
  emits('blur');
}

const onClear = () => {
      
      
  emits('clear');
}

const onChange = (val: string) => {
      
      
  emits('change', val);
}

</script>

<template>
  <el-input
    v-if="type === 'textarea'"
    v-model="input"
    :rows="rows"
    type="textarea"
    :placeholder="placeholder"
    :maxlength="maxlength"
    :minlength="minlength"
    :show-word-limit="showWordLimit"
    :disabled="disabled"
    :prefixIcon="prefixIcon"
    :suffixIcon="suffixIcon"
    :autosize="autosize"
    :inputStyle="inputStyle"
    @focus="onFocus"
    @blur="onBlur"
    @change="onChange"
  />
  <el-input 
    v-else
    v-model="input" 
    :type="type"
    :placeholder="placeholder"
    :maxlength="maxlength"
    :minlength="minlength"
    :clearable="clearable"
    :showPassword="showPassword"
    :disabled="disabled"
    :prefixIcon="prefixIcon"
    :suffixIcon="suffixIcon"
    :inputStyle="inputStyle"
    @focus="onFocus"
    @blur="onBlur"
    @clear="onClear"
    @change="onChange" />
    
</template>

<style scoped lang='scss'>

</style>

plugins/components.tsRegístrese globalmente en

import type {
    
     Component } from 'vue'
import ArInput from '@/components/form/input/index.vue';

const componentObj: {
    
    [propName: string]: Component} = {
    
    
  ArInput
};

export default function install(app: any) {
    
    
  Object.keys(componentObj).forEach((key) => {
    
    
    app.component(key, componentObj[key])
  })
}

Recuerda plugins/index.tsagregar:

import installComponents from './components'

export default (app: any) => {
    
     
  // ...

  // 注册自定义组件
  installComponents(app);
}

login.vuecódigo fuente:

<script lang="ts" setup>
import type {
      
       FormInstance, FormRules } from 'element-plus'
import {
      
       ref, reactive } from 'vue'


const formRef = ref<FormInstance>();
const form = reactive({
      
      
  name: '',
  password: ''
})
const rules = ref<FormRules>({
      
      
  name: [
    {
      
       required: true, message: '请输入账号', trigger: 'blur' },
    {
      
       min: 8, max: 12, message: '账号长度为8-12', trigger: 'blur' },
  ],
  password: [
  {
      
       required: true, message: '请输入密码', trigger: 'blur' },
  ]
})

const onSubmit = async() => {
      
      
  await formRef.value.validate((valid, fields) => {
      
      
    if (valid) {
      
      
      console.log('submit!')
    } else {
      
      
      console.log('error submit!', fields)
    }
  })
}

</script>

<template>
  <div class="login">
    <div class="login-inner">
      <el-form ref="formRef" :model="form" :rules="rules">
        <el-form-item label="账号" prop="name" required>
          <ArInput v-model:value="form.name" prefix-icon="User" />
        </el-form-item>
        <el-form-item label="密码" prop="password" required>
          <ArInput v-model:value="form.password" type="password" prefix-icon="Lock" />
        </el-form-item>
        <el-button type="primary" class="login-btn" @click="onSubmit">登录</el-button>
      </el-form>
    </div>
  </div>
</template>

<style lang="scss" scoped>
.login {
      
      
  width: 100vw;
  height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  background-color: #ccc;

  &-inner{
      
      
    margin-top: 20%;
    width: 300px;
    padding: 32px;
    background-color: #fff;
    border-radius: 4px;
  }

  &-btn {
      
      
    width: 100%;
  }
}
</style>

El efecto de página final es:

inserte la descripción de la imagen aquí

Variable ambiental

Antes de completar la acción de envío de la página, primero resuelva el problema de las variables de entorno. Siempre tenemos algunas variables que son diferentes en el servicio de prueba, prelanzamiento o producción, por lo que debemos distinguir el entorno.

Cree una nueva envcarpeta y agregue tres o cuatro archivos dentro:

.env                # 所有情况下都会加载
.env.development     # 开发环境
.env.release         # 预发布环境
.env.production      # 正服环境

Por ejemplo:

# .env.development 
VITE_ENV = devalopment
# 请求接口
VITE_API_URL = https://api.vvhan.com/testapi/saorao

De esta manera, las variables se pueden configurar en diferentes entornos y luego modificamos el comando del script para distinguir el entorno:

"scripts": {
    
    
    "watch": "vite",
    "watch:release": "vite --mode release",
    "watch:production": "vite --mode production",
    "build:development": "vue-tsc && vite build --mode development",
    "build:release": "vue-tsc && vite build --mode release",
    "build:production": "vue-tsc && vite build --mode production",
    // ...
  },

Cree un nuevo archivo en el directorio raíz build/utils.ts:

// Read all environment variable configuration files to process.env
export function wrapperEnv(envConf: Recordable) {
    
    
  const result: any = {
    
    }

  for (const envName of Object.keys(envConf)) {
    
    
    let realName = envConf[envName].replace(/\\n/g, '\n')
    realName =
      realName === 'true' ? true : realName === 'false' ? false : realName

    result[envName] = realName
    if (typeof realName === 'string') {
    
    
      process.env[envName] = realName
    } else if (typeof realName === 'object') {
    
    
      process.env[envName] = JSON.stringify(realName)
    }
  }
  return result
}

Luego modifique vite.config.jse inyecte las variables que definimos en él:

import {
    
     defineConfig, loadEnv, UserConfig, ConfigEnv  } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import {
    
     ElementPlusResolver } from 'unplugin-vue-components/resolvers'
import {
    
     wrapperEnv } from './build/utils'

const resolve = (dist) => path.resolve(__dirname, dist)

// https://vitejs.dev/config/
export default ({
    
     command, mode }: ConfigEnv): UserConfig => {
    
    
  const env = loadEnv(mode, './env')
  wrapperEnv(env)

  return {
    
    
    plugins: [
      vue(),
      AutoImport({
    
    
        imports: [
          'vue',
          'vue-router',
          'pinia'
        ],
        eslintrc: {
    
    
          enabled: false,
          filepath: './.eslintrc-auto-import.json',
          globalsPropValue: true
        },
        resolvers: [ElementPlusResolver()]
      }),
      Components({
    
    
        resolvers: [ElementPlusResolver()]
      })
    ],
    resolve: {
    
    
      alias: {
    
    
        '@': resolve('./src')
      },
      extensions: [".js", ".ts", ".tsx", ".jsx"]
    },
    css: {
    
    
      preprocessorOptions: {
    
    
        scss: {
    
    
          additionalData: '@import "@/assets/styles/variable.scss";@import "@/assets/styles/mixins.scss";'
        }
      }
    }
  }
}

De esta manera podemos axiosusarlo en el paquete a continuación. Oh, sí, puede haber un pequeño error de TS, modifique tsconfig.node.json:

"include": ["vite.config.ts", "build**/*.ts"]

paquete axios

nuevo src/utils/request.ts:

import axios, {
    
     AxiosInstance, InternalAxiosRequestConfig } from 'axios'
import {
    
     ElMessage } from 'element-plus'

const request: AxiosInstance | any = axios.create({
    
    
  timeout: 100000,
  headers: {
    
    
    post: {
    
    
      'Content-Type': 'application/x-www-form-urlencoded'
    }
  },
  withCredentials: true
});

request.interceptors.request.use((config: InternalAxiosRequestConfig) => {
    
    
  let token = localStorage.getItem("token");
  if (token && token !== '') {
    
    
    config.headers['Authorization'] = token;
  }

  // 获取环境变量!!!
  const projectUrlPrefix = import.meta.env.VITE_API_URL;
  // 这样更支持多域名接口的情况
  if (config && config.url && !/^(http(|s):\/\/)|^\/\//.test(config.url)) {
    
    
    config.url = projectUrlPrefix + config.url;
  }
  return config;
});

request.interceptors.response.use((res: any) => {
    
    
  if (res.data.status && res.data.status !== 200) {
    
    
    ElMessage.error(res.data.msg || '请求失败,请稍后重试')
    return Promise.reject(res.data)
  }
  // 如果这里是登录信息过期,那么应该给个弹窗提示什么的,最后都应该重定向到登录页面
  return res.data
}, (error: any) => {
    
    
  console.log(`%c 接口异常 `, 'background-color:orange;color: #FFF;border-radius: 4px;', error);
})

export default request;
export const $get = (url: string, params = {
    
    }) => {
    
    
  return request.get(url, {
    
    
    params
  })
}
export const $post = (url: string, params = {
    
    }) => {
    
    
  return request.post(url, params)
}

Vaya a la página de inicio de sesión y haga una prueba:

<script lang="ts" setup>
const $post = inject('$post') as any
// ...

const onSubmit = async () => {
  // ...
  const res = await $post('/login', form)
  // ...
}
</script >

Pruebe los prefijos del nombre de dominio en diferentes entornos. Si son diferentes, la configuración es exitosa ~

https://api.vvhan.com/testapi/saorao/login
https://api.vvhan.com/releaseapi/saorao/login
https://api.vvhan.com/api/saorao/login

arreglo general

inserte la descripción de la imagen aquí

  • Convierta la ruta en el módulo de enrutamiento al menú que se muestra a la izquierda;
  • La parte anterior es información del usuario y puede salir;
  • Cambie de ruta, aparecerá una pestaña similar a un navegador, puede hacer clic en la pestaña para cambiar o cerrar la página actual;
  • Finalmente, se muestra el contenido principal de la página.

Eche un vistazo al componente del menú primero:

<script lang='ts' setup>
import {
    
     ref, computed, watch } from 'vue';
import {
    
     useRoute, useRouter } from 'vue-router'
import {
    
     storeToRefs } from 'pinia'
import routes from '@/router/index';
import {
    
     IRouterItem } from '@/types/menu'
import {
    
     useTagViewsStore } from '@/store/tagViews';

const route = useRoute();
const router = useRouter();
const isCollapse = ref(false);
const defaultActive = ref('0')
const tagViewsStore = useTagViewsStore();
const {
    
     visitedViews } = storeToRefs(tagViewsStore);

// 1. 从声明的路由中获取当前显示的菜单栏(还可以过滤一些不是菜单栏的页面)
const currentMenu = computed(() => {
    
    
  if(routes && routes.length) {
    
    
    const routesArr: any = routes.filter((route) => route.name === 'Layout');
    if(routesArr && routesArr[0] && routesArr[0].children){
    
    
      const res = routesArr[0].children as IRouterItem[];
      return res;
    }else {
    
    
      return [];
    }
  }else {
    
    
    return []
  }
});

// 获取路由对应的菜单下标(如果一打开是客户列表页,则高亮客户列表)
const currentMenuToObj = computed(() => {
    
    
  const routes = currentMenu.value;
  if(routes && routes.length) {
    
    
    let obj: {
    
    [key: string]: any} = {
    
    };
    for(let i = 0; i < routes.length; i++) {
    
    
      const item = routes[i];
      if(item.children) {
    
    
        for(let j = 0; j< item.children.length; j++) {
    
    
          const subItem = item.children[j];
          obj[subItem.path] = {
    
    
            index: `${
      
      i}-${
      
      j}`,
            item: subItem
          };
        }
      }else {
    
    
        obj[item.path] = {
    
    
          index: '' + i,
          item
        }; 
      }
    }

    return obj;
  }else {
    
    
    return {
    
    };
  }
})

// 监听路由获取当前高亮的值,store的使用在后面详细说一下
watch(
  () => route.path,
  (val: string) => {
    
    
    if(!visitedViews.value.length) {
    
    
      const item = {
    
    
        path: '/',
        name: 'Index',
        meta: {
    
     title: '主页' },
      }
      tagViewsStore.addVisitedViews(item)
      tagViewsStore.setActivitedView(item)
    }
    if(val) {
    
    
      const obj = currentMenuToObj.value[val];
      defaultActive.value = obj.index;
      tagViewsStore.addVisitedViews(obj.item)
      tagViewsStore.setActivitedView(obj.item)
    }
  }, {
    
    
    immediate: true
  }
)

// 点击菜单栏进行跳转
const onToPage = (item: IRouterItem) => {
    
    
  if(route.path === item.path) return;
  tagViewsStore.addVisitedViews(item)
  tagViewsStore.setActivitedView(item)
  router.push(item.path)
}

</script>

<template>
  <div class="menu">
    <div class="menu-logo">Logo</div>
    <div class="menu-main">
      <el-menu
        :default-active="defaultActive"
        :collapse="isCollapse"
        background-color="#191919"
        text-color="#7e7e7e"
        active-text-color="#ffffff"
      >
        <template v-for="(item, index) in currentMenu" :key="index">
          <el-menu-item :index="'' + index" v-if="!item.children || !item.children.length" @click="onToPage(item)">
            <template #title>{
    
    {
    
     item.meta.title  }}</template>
          </el-menu-item>
          <el-sub-menu :index="'' + index" v-else>
            <template #title>
              <span>{
    
    {
    
     item.meta.title  }}</span>
            </template>
            <el-menu-item v-for="(subItem, subIndex) in item.children" :key="`${index}-${subIndex}`" :index="`${index}-${subIndex}`" @click="onToPage(subItem)">{
    
    {
    
     subItem.meta.title  }}</el-menu-item>
          </el-sub-menu>
        </template>
      </el-menu>
    </div>
  </div>
</template>

<style scoped lang='scss'>
.menu {
    
    
  height: 100vh;
  max-width: 280px;
  background-color: #191919;
  
  &-logo {
    
    
    height: 60px;
    display: flex;
    align-items: center;
    justify-content: center;
    color: #fff;
  }

  &-main {
    
    
    height: calc(100vh - 100px);
  }
}
</style>

<style lang="scss">
.menu .el-menu {
    
    
  border: none !important;
}
</style>

La siguiente es la información de la barra superior:

<script lang='ts' setup>

</script>

<template>
  <div class="nav">
    <div class="nav-left"></div>
    <div class="nav-right">
      <div class="nav-right-item">
        <el-dropdown>
          <span class="el-dropdown-link">
            您好,XXX
          </span>
          <template #dropdown>
            <el-dropdown-menu>
              <el-dropdown-item>退出登录</el-dropdown-item>
            </el-dropdown-menu>
          </template>
        </el-dropdown>
      </div>
    </div>
  </div>
</template>

<style scoped lang='scss'>
.nav {
      
      
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 16px;
  height: 60px;
  border-bottom: solid 1px var(--el-menu-border-color);
  background-color: #fff;

  &-right{
      
      
    flex: 1;
    display: flex;
    align-items: center;
    justify-content: flex-end;

    &-item {
      
      
      cursor: pointer;
    }
  }
}
</style>

El siguiente es el desarrollo de la barra de navegación tab. Aquí usamos pinia para la gestión del estado. Después de la instalación, primero nos plugins/index.tsregistramos en:

import {
    
     createPinia } from 'pinia';

// ...
// 注册store (建议这个放在所有注册的首位,方便其他插件可能会用到它)
app.use(createPinia());

Declaración sobre navegación tab, storeen src/store/tagViews.ts:

import {
    
     defineStore } from 'pinia';
import {
    
     IRouterItem } from '@/types/menu'

export const useTagViewsStore = defineStore('tagViews', {
    
    
  state: () => {
    
    
    return {
    
    
      // 访问过的页面
      visitedViews: [] as IRouterItem[],
      // 当前访问的页面
      activitedView: {
    
    } as IRouterItem
    }
  },

  actions: {
    
    
    // 新增页面
    addVisitedViews(view: IRouterItem){
    
    
      const item = this.visitedViews.find((item) => item.path === view.path)
      if(item) return;
      this.visitedViews.push(view);
    },
    
    // 删除页面
    deleteVisitedViews(index: number) {
    
    
      this.visitedViews.splice(index, 1);
    },
    
    // 高亮某个页面
    setActivitedView(view: IRouterItem) {
    
    
      this.activitedView = view
    }
  }
})

tagViewsEl código fuente del componente es el siguiente:

<script lang='ts' setup>
import {
      
       storeToRefs } from 'pinia';
import {
      
       useRouter } from 'vue-router'
import {
      
       useTagViewsStore } from '@/store/tagViews';
import {
      
       IRouterItem } from '@/types/menu'

const router = useRouter();
const tagViewsStore = useTagViewsStore();
const {
      
       visitedViews, activitedView } = storeToRefs(tagViewsStore);

// 关闭页面
const onDel = (item: IRouterItem) => {
      
      
  const index = visitedViews.value.findIndex((view) => view.path === item.path);
  if(index === -1) return;

  tagViewsStore.deleteVisitedViews(index);
  if(item.path === activitedView.value.path) {
      
      
    const obj = visitedViews.value[index - 1]
    tagViewsStore.setActivitedView(obj);
    router.push(obj.path)
  }
}

// 切换页面
const onChange = (item: IRouterItem) => {
      
      
  tagViewsStore.setActivitedView(item)
  router.push(item.path)
}

</script>

<template>
  <el-scrollbar class="tags-scrollbar">
    <div class="tags">
      <div v-for="item in visitedViews" :key="item.path" :class="['tags-item', { active: activitedView.path === item.path }]" @click="onChange(item)" >
        <span class="tags-item-title">{
   
   { item.meta ? item.meta.title : '' }}</span>
        <el-icon v-if="item.path !== '/'" @click.stop="onDel(item)"><Close /></el-icon>
      </div>
    </div>
  </el-scrollbar>
</template>

<style scoped lang='scss'>
.tags-scrollbar {
      
      
  height: 30px;
  overflow: hidden;
}
.tags {
      
      
  display: flex;
  background: #f3f3f3;
  border: 1px solid #f2f2f2;
  border-right: none;
  margin: -1px 0 0 -1px;
  -webkit-user-select: none;
  -moz-user-select: none;
  -ms-user-select: none;
  user-select: none;
  &-item {
      
      
    display: flex;
    align-items: center;
    position: relative;
    cursor: pointer;
    height: 26px;
    line-height: 26px;
    border: 1px solid #d8dce5;
    color: #495060;
    background: #fff;
    padding: 0 8px;
    font-size: 12px;
    margin-left: 5px;
    margin-top: 4px;

    &-title {
      
      
      margin-right: 4px;
    }

    &:first-of-type {
      
      
      margin-left: 5px;
    }
    &:last-of-type {
      
      
      margin-right: 5px;
    }
    &.active {
      
      
      background-color: #e25050;
      color: #fff;
      border-color: #e25050;
      &::before {
      
      
        content: '';
        background: #fff;
        display: inline-block;
        width: 8px;
        height: 8px;
        border-radius: 50%;
        position: relative;
        margin-right: 2px;
      }
    }
  }
}
</style>

piniaLa relación de uso de vuexes mucho más simple, la mayor diferencia es que * mutations* ya no existe.

intercepción de ruta

Generalmente, después de iniciar sesión, actualizaremos piniala información sobre el usuario en el sitio web, y parte de la información se cifrará y almacenará en el sitio web localstorage. Para los usuarios que no han iniciado sesión, debemos interceptar su acceso al sistema y redirigirlos a la página de inicio de sesión. (En proyectos reales, también se debe considerar la intercepción de permisos de página)

// router/hooks/index.ts
import type {
    
     Router } from 'vue-router'
import {
    
     USERINFO } from '@/constants/localstorage'

const routerHook = (router: Router) => {
    
    
  router.beforeEach(to => {
    
    
    if(to.path === '/login') {
    
    
      // 可以做一些清空登录信息的操作, 比如跟pinia相关的等操作
      localStorage.removeItem(USERINFO);
      return true;
    }else{
    
    
      // 在这里可以判断用户是否登录,跳转的某个页面是否有权限,这里只是粗略写一下
      const info = localStorage.getItem(USERINFO);
      if(!info) {
    
    
        return {
    
     name: 'Login' }
      }
    } 
  })
}

export default routerHook;


import {
    
     createRouter, createWebHashHistory } from 'vue-router'
import routes from './routes'
import routerHook from './hooks/index'

// 注册路由
const router = createRouter({
    
    
  history: createWebHashHistory(),
  routes
})

routerHook(router)

export default router

Modifique la página de inicio de sesión y simule:

const onSubmit = async () => {
    
    
  await formRef.value.validate(async (valid: boolean, fields: {
     
     [key: string]: any}) => {
    
    
    if (valid) {
    
    
      // 一般这种情况下,localStorage中存储的信息不能太重要,且需要加密,还应该更新pinia中的用户信息
      const info = {
    
    
        name: 'Armouy'
      }
      localStorage.setItem(USERINFO, JSON.stringify(info))
      router.push('/')
    } else {
    
    
      console.log('error submit!', fields)
    }
  })
}

Si no ha iniciado sesión, será redirigido a la página de inicio de sesión.

Embalar

npm run build:production
npm run preview

Link de referencia


Si hay algún error, indíquelo, gracias por leer ~

Supongo que te gusta

Origin blog.csdn.net/qq_34086980/article/details/131371511
Recomendado
Clasificación