https://www.bilibili.com/video/BV1RUxWemEEn?spm_id_from=333.788.player.switch&vd_source=eecc2c8229facae905b6daed1650b44b&p=3

项目技术栈

项目创建

npm 创建

npm create vue@latest 

pnpm 创建

pnpm create vue

目录结构

├── README.md
├── env.d.ts
├── eslint.config.ts
├── index.html
├── package.json
├── pnpm-lock.yaml
├── public
│   └── favicon.ico
├── src
│   ├── App.vue
│   ├── assets
│   │   ├── base.css
│   │   ├── logo.svg
│   │   └── main.css
│   ├── components
│   │   ├── HelloWorld.vue
│   │   ├── TheWelcome.vue
│   │   ├── WelcomeItem.vue
│   │   └── icons
│   │       ├── IconCommunity.vue
│   │       ├── IconDocumentation.vue
│   │       ├── IconEcosystem.vue
│   │       ├── IconSupport.vue
│   │       └── IconTooling.vue
│   ├── main.ts
│   ├── router
│   │   └── index.ts
│   ├── stores
│   │   └── counter.ts
│   └── views
│       ├── AboutView.vue
│       └── HomeView.vue
├── tsconfig.app.json
├── tsconfig.json
├── tsconfig.node.json
└── vite.config.ts

项目配置

基本配置

配置页面icon

public -> favicon.ico 替换成自己的Icon图标即可,若名字不同,则需要在index.html中进行路径和名称调整

配置页面tltle

同样在 index.html修改 <title></title>标签中的内容即可!

配置路径别名

vite.config.ts配置文件中,添加resolve路径解析对象来添加路径别名!

export default defineConfig({
  plugins: [
    vue(),
    vueDevTools(),
  ],
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url))
    },
  },
})

代码规范配置

editorconfig配置

EditorConfig 可以帮助在不同编辑器下的实现代码风格统一配置!

https://editorconfig.org/
root = true

[*] # 表示所有文件
charset = utf-8 # 表示字符集编码
indent_style = space # 表示缩进风格 [tab | space]
indent_size = 2 # 表示缩进大小
end_of_line = lf # 控制换行类型 [lf | cr | crlf ]
trim_traling_whitespace = true # 去除行尾的空白字符
inset_final_newline = true # 始终在文件末尾插入新的一行

[*.md] # 表示 md 文档适用以下规则
max_line_length = off
trim_traling_whitespace = false

vscode 需要安装EditorConfig插件!

prettier代码格式化

prettier 是一个可配置的代码格式化工具,可以通过自定义格式配置,结合编辑器实现保存自动格式化代码!

  1. 安装prettier插件:

    pnpm i prettier -D
    
  2. 配置prettier:

    新建 .prettierrc

    {
      "useTabs": false, // 使用tab缩进还是空格缩进
      "tabWidth": 2, // 缩进的大小
      "printWidth": 100, // 当前行字符串的长度
      "singleQuote": true, // 单引号还是双引号,true未单引号
      "trailingComma": "none", // 对象属性末尾是否 追加逗号
      "semi": false // 语句末尾是否添加 分号
    }
    
  3. 配置prettier忽略文件:

    新建 .prettierignore

    /dist/*
    /node_modules/**
    /build/*
    /public/*
    
    **/*.md
    **/*.svg
    

可结合 vscode 完成编写代码时,保存自动格式化代码 ,首先需要在vscode扩展市场中安装插件

然后使用alt+shift+f快捷方式来选择prettier进行代码格式化!

配置保存代码时,自动格式化,在settings -> 搜索 format on save -> 勾选即可!

设置默认格式化插件, 在settings -> 搜索 -> default format -> 选择默认格式化插件即可!

eslint代码语法检查

创建项目时,已经选择eslint插件使用,因此package.json中已经有了有eslint插件的安装!

eslint 是一个代码语法检测工具,主要用来检测一些不规范的代码片段,会进行警告、报错 ,提示!

  1. vscode中安装eslint扩展插件:

    它可以帮我们在编写代码时,来实时检测语法问题,并进行提示和反馈!

  2. 解决eslintprettier插件冲突问题

    pnpm i eslint-plugin-prettier -D
    

    eslint 配置文件中添加 prettier插件

项目开发准备

开发目录划分

├── api                # api 业务接口请求
├── assets             # 静态资源文件 打包时,会对静态资源进行打包处理,生成文件指纹
│   ├── base.css
│   ├── logo.svg
│   └── main.css
├── components         # 业务公共组件
├── config             # 公共配置文件
│   └── index.ts
├── hooks              # 公共hooks
├── http               # axios http 请求
│   ├── index.ts
│   └── intersptor.ts
├── layout             # 布局相关
│   ├── header
│   ├── index.vue
│   └── sidebar
├── main.ts
├── router             # 路由封装
│   ├── index.ts
│   └── intersptor.ts
├── stores             # pinia 存储封装
│   └── counter.ts
├── styles             # style 样式封装
│   ├── elementui.scss
│   ├── index.scss
│   └── varibales.scss
├── utils              # 全局工具 
│   └── index.ts
└── views              # 页面
    └── LoginView.vue

初始化css 样式

初始化css样式,就是重置浏览器css默认的样式,内外边距,以及ul无序列表a标签的一些默认样式规则!

配置vscode代码片段

配置vscode代码片段,可以在新建vue文件时,通过快捷方式快速生成代码片段减少重复代码编写!

https://snippet-generator.app/

以上是配置代码片段在线工具,将模版生成vscode代码片段所需要的格式,并在vscode新建代码片段将生成的代码进行配置即可!

vscode中 在首选项中选择 -> 配置代码片段 -> 在提示框中 -> 选择新建代码片段

{
	"vue3 typescript": {
		"prefix": "tsvue",
		"body": [
			"<template>",
			"  <div class=\"${1:home}\">",
			"      <center><h2>${1:home}</h2></center>",
			"  </div>",
			"</template>",
			"",
			"<script setup lang=\"ts\">",
			"</script>",
			"",
			"<style lang=\"scss\" scoped>",
			"${1:home}{",
			"  border: 1px solid #333;",
			"}",
			"</style>"
		],
		"description": "vue3 typescript"
	}
}

代码封装

router

https://router.vuejs.org/zh/installation.html

路由管理代码进行封装管理,包括路由全局导航守卫配置等!

  1. 安装vue-router

    pnpm install vue-router@4 -S
    
  2. src目录下创建router文件夹

    src->router 并在文件夹内部创建index.ts文件以及全局守卫文件intersptor.ts,以及routes.ts路由元信息! src->router-> modules 用来存储各个模块下不同路由的元信息!

    • index.ts

      import { createRouter, createWebHistory } from 'vue-router'
      import { setRouterIntersptor } from './intersptor'
      import type { App } from 'vue'
      import routes from './routes'
      
      const router = createRouter({
        history: createWebHistory(import.meta.env.BASE_URL),
        routes: routes,
      })
      // 设置路由拦截器
      setRouterIntersptor(router)
      export function setupRouter(app: App) {
        app.use(router)
      }
      
    • intersptor.ts

      import type { RouteLocationNormalizedGeneric, Router } from 'vue-router'
      /*
        不能再这里直接引入 useAuthStore,因为它还没有被定义,所以这里应该使用异步导入的方式,
        import { useAuthStore } from '../stores/modules/auth'  ❎错的
        在 app.use(pinia) 注册之前不能导入store模块
      
        需要在拦截器中通过动态导入的方式来加载 store 模块
        // const { useAuthStore } = await import('../stores/modules/auth') ✅正确
      */
      
      export async function setRouterIntersptor(router: Router) {
        const whiteList = ['/login']
        router.beforeEach(
          async (to: RouteLocationNormalizedGeneric, from: RouteLocationNormalizedGeneric) => {
            const { useAuthStore } = await import('../stores/modules/auth')
            console.log('from', from)
            const authStore = useAuthStore()
            // 当前访问的是登录页,且已登录,直接跳转到首页
            if (whiteList.includes(to.path) && authStore.isLogin) {
              return '/'
            }
            // 当前访问的是白名单路径时,即使登录状态为未登录,也可访问
            if (whiteList.includes(to.path) && !authStore.isLogin) {
              return true
            }
            // 当前访问的不是登录页,且登录状态为未登录时,直接跳转到 login 页面
            if (!whiteList.includes(to.path) && !authStore.isLogin) {
              return '/login'
            }
            return true
          },
        )
      }
      
    • routes.ts

      使用import.meta.glob方法来批量导入modules下路由原信息

      import type { RouteRecordRaw } from 'vue-router'
      /**
       * @description 导入模块路由
       * @param routes 路由数组
       * @returns  Array<RouteRecordRaw>
       */
      function importRouteModules(routes: Array<RouteRecordRaw> = []) {
        const modules: Record<string, any> = import.meta.glob('./modules/*.ts', { eager: true })
        Object.keys(modules).forEach((key) => {
          const route = modules[key].default
          if (Array.isArray(route)) {
            routes.push(...route)
          } else {
            routes.push(route)
          }
        })
        return routes
      }
      // 生成路由
      const routes: Array<RouteRecordRaw> = importRouteModules([])
      
      export default routes
      
  3. main.ts入口文件中引入router

     import { createApp } from 'vue'
     import App from './App.vue'
     import router from './router'
     const app = createApp(App)
     app.use(router)
     app.mount('#app')
    

pinia

https://prazdevs.github.io/pinia-plugin-persistedstate/zh/guide/config.htmlhttps://pinia.vuejs.org/zh/introduction.html

pinia全局状态管理插件,可以帮我们实现全局数据存储,只需要pinia是不太行的,当页面刷新的时候,丢失数据,所以还需要配合其它插件(pinia-plugin-persistedstate)来完成数据持久存储!

  1. 安装pinia持久化插件

    pnpm install pinia -S
    pnpm install pinia-plugin-persistedstate -D
    
  2. src目录下创建stores文件夹

    src->stores其中包含modules模块文件夹,以及index.ts文件 index.ts用来创建pinia实例,并实现持久化配置! modules文件夹用来存放其它子模块,并在index.ts中分别导入!

    • index.ts

       import { createPinia } from 'pinia'
       import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
       import type { App } from 'vue'
       // 引入持久化插件
       const pinia = createPinia()
      
       export function setupPinia(app: App<Element>) {
         app.use(pinia)
       }
      
       // 持久化
       pinia.use(piniaPluginPersistedstate)
      
       // 用户
       export * from './modules/counter'
       // 系统
       // export * from './modules/system'
      
       export { pinia }
      
    • ./modules/counter.ts

      import { ref, computed } from 'vue'
      import { defineStore } from 'pinia'
      
      export const useCounterStore = defineStore('counter', () => {
        const count = ref(0)
        const doubleCount = computed(() => count.value * 2)
        function increment() {
          count.value++
        }
      
        return { count, doubleCount, increment }
      },
      {
        persist: true,
      })
      
  3. main.ts入口文件中引入pinia

     import { createApp } from 'vue'
     import { setupPinia } from '@/stores/index.ts'
     import App from './App.vue'
     const app = createApp(App)
     setupPinia(app)
     app.mount('#app')
    
https://pinia.vuejs.org/zh/core-concepts/outside-component-usage.html

axios

axios是网络通信插件,关于axios的代码封装,简单将axios拦截器部分单独拆分出来,另外对axios的做一个公共的配置!

  1. 安装axios插件

    pnpm install axios -S
    
  2. src目录下新建http文件夹

    并在文件夹内部分别创建 index.ts 以及 interspetor.ts 文件 index.ts 是创建axios实例一部分和公共配置的相关代码! interspetor.ts 时从主文件中,抽离出拦截器相关的文件,对拦截器做一个单独逻辑编写!

    • index.ts

      import axios from 'axios'
      import { setupInterceptor } from './intersptor'
      
      const instance = axios.create({
        baseURL: import.meta.env.VITE_BASE_API,
        timeout: 10000,
        headers: {
          'Content-Type': 'application/json;charset=UTF-8',
        },
      })
      
      // 注册拦截器
      setupInterceptor(instance)
      export default instance
      
    • interspector.ts

       import type { AxiosInstance } from 'axios'
      
       export function setupInterceptor(instance: AxiosInstance) {
         // 请求拦截器
         instance.interceptors.request.use(
           (config) => {
             // 在发送请求之前做些什么
             return config
           },
           (error) => {
             // 对请求错误做些什么
             return Promise.reject(error)
           },
         )
      
         // 响应拦截器
         instance.interceptors.response.use(
           (response) => {
             // 对响应数据做点什么
             return response
           },
           (error) => {
             // 对响应错误做点什么
             return Promise.reject(error)
           },
         )
       }
      

styles

layout

ElementPlus

https://element-plus.org/zh-CN/

安装插件

pnpm install element-plus -S

全局引入

main.ts入口文件中引入element-plus以及全局样式!

import ElementUi from 'element-plus'
import 'element-plus/dist/index.css'
import App from './App.vue'
const app = createApp(App)
app.use(ElementUi)

全局引入会将所有的组件以及样式入口文件进行引入,会影响打包速度以及体积大小!

按需引入

按需引入,需要安装自动导入插件,来帮我们实现自动组件导入!

pnpm install -D unplugin-vue-components unplugin-auto-import unplugin-element-plus
https://npmmirror.com/package/unplugin-vue-components

插件地址,可以查看具体使用方式!

vite中,引入引入两个自动导入组件插件!

import { fileURLToPath, URL } from 'node:url'

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueDevTools from 'vite-plugin-vue-devtools'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
// https://vite.dev/config/
export default defineConfig({
  plugins: [
    vue(),
    vueDevTools(),
    // element plus 样式自动导入
    ElementPlus({
      useSource: true,
    }),
    // 配置自动导入,ref reactive 时自动导入vue
    AutoImport({
      imports: ['vue', 'vue-router'],
      dts: true,
      resolvers: [ElementPlusResolver()],
    }),
    // 配置组件自动导入
    Components({
      dts: true,
      resolvers: [ElementPlusResolver()],
      extensions: ['vue'],
      dirs: ['src/components/'],
    }),
  ],
})

配置 types 类型声明,在使用自动导入时,会帮我们创建两个声明文件

"auto-imports.d.ts", "components.d.ts"

需要在 tsconfig.app.json 中加入两个声明文件!

{
  "extends": "@vue/tsconfig/tsconfig.dom.json",
  "include": ["env.d.ts", "auto-imports.d.ts", "components.d.ts", "src/**/*", "src/**/*.vue"],
  "exclude": ["src/**/__tests__/*"],
  "compilerOptions": {
    "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
    "noImplicitThis": true,
    "paths": {
      "@/*": ["./src/*"]
    }
  }
}

加入声明文件后,对于unknow未知类型的标签,就会有对应的类型提示

组件封装

封装常用的全局组件

svg-icon

全局icon组件, 需要安装的插件 vite-plugin-svg-icons | unplugin-icons

pnpm install -D vite-plugin-svg-icons | unplugin-icons

vite.config.ts中引入插件,并配置icon路径!

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueDevTools from 'vite-plugin-vue-devtools'
import Icons from 'unplugin-icons/vite'
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
export default defineConfig({
  plugins: [
    vue(),
    vueDevTools(),
    // 配置icons
    Icons({
      autoInstall: true,
    }),
    createSvgIconsPlugin({
      // 指定需要缓存的图标文件夹
      iconDirs: [fileURLToPath(new URL('./src/icons', import.meta.url))],
      // 指定symbolId格式
      symbolId: 'icon-[dir]-[name]',
    }),
  ],
)}

创建svg-icon 组件src -> components -> svg-icon -> index.vue

<template>
  <svg aria-hidden="true" class="svg-icon" :style="'width:' + size + ';height:' + size">
    <use :xlink:href="symbolId" :fill="color" />
  </svg>
</template>

<script setup lang="ts" name="SvgIcon">
import { computed } from 'vue'

const props = defineProps({
  prefix: {
    type: String,
    default: 'icon',
  },
  iconClass: {
    type: String,
    required: false,
    default: '',
  },
  color: {
    type: String,
    default: '',
  },
  size: {
    type: String,
    default: '1em',
  },
})

const symbolId = computed(() => `#${props.prefix}-${props.iconClass}`)
</script>

<style scoped>
.svg-icon {
  display: inline-block;
  width: 1em;
  height: 1em;
  overflow: hidden;
  vertical-align: -0.15em; /* 因icon大小被设置为和字体大小一致,而span等标签的下边缘会和字体的基线对齐,故需设置一个往下的偏移比例,来纠正视觉上的未对齐效果 */
  outline: none;
  fill: currentColor; /* 定义元素的颜色,currentColor是一个变量,这个变量的值就表示当前元素的color值,如果当前元素未设置color值,则从父元素继承 */
}
</style>

src 目录下 -> 创建 icons 文件夹,用来存放 svg icon图标!

main.ts 中导本地icons

import 'virtual:svg-icons-register'

import 'normalize.css'
import '@/styles/index.scss'
/* import ElementUi from 'element-plus'
import 'element-plus/dist/index.css' */
import { createApp } from 'vue'
import { setupPinia } from '@/stores/index.ts'
import App from './App.vue'
import router from './router'

// 本地SVG图标
import 'virtual:svg-icons-register'

const app = createApp(App)
setupPinia(app)
app.use(router)
// app.use(ElementUi)
app.mount('#app')

知识点汇总

子组件实例类型定义

当通过 ref获取子组件实例类型时,通过InstanceType定义该组件实例的类型!

<template>
  <div id="layout">
    <el-container>
      <Sidebar ref="sidebarRef" />
      <el-container class="is-vertical">
        <Navbar />
        <Main />
      </el-container>
    </el-container>
  </div>
</template>

<script setup lang="ts">
import Navbar from './navbar/Navbar.vue'
import Sidebar from './sidebar/Sidebar.vue'
import Main from './main/Main.vue'
const sidebarRef = ref<InstanceType<typeof Sidebar>>()
</script>

<style lang="scss" scoped></style>

以上获取Sidebar子组件实例,通过InstanceType来定义该组件实例类型!

const sidebarRef = ref<InstanceType<typeof Sidebar>>()