Vue3实战 --- 基本配置和页面登录

2,110 阅读5分钟

tsconfig.json

「这是我参与2022首次更文挑战的第20天,活动详情查看:2022首次更文挑战

TypeScript在编译为JavaScript的时候的配置文件

{
  // 编译配置
  "compilerOptions": {
    // 目标代码 --- 编译后的代码使用esnext的语法
    "target": "esnext",
    // 目标代码需要使用的模块化方案 --- 编译后的代码使用esnext模块化
    "module": "esnext",
    // 开启所有的严格模式
    "strict": true,
    // 对jsx进行怎么样的处理 --- preserve表示不对jsx进行处理
    "jsx": "preserve",
    // 辅助的导入功能 ---- 如果用到了polyfill的时候,使用import导入的方式,而不是直接在文件中插入
    "importHelpers": true,
    // 按照node的方式去解析模块
    "moduleResolution": "node",
    // 跳过一些库的类型检测 (如axios等)
    // 1. 提高性能 2. 避免多个库出现同名类型 从而发生冲突
    "skipLibCheck": true,
    // 以下2个选项 一般是一起使用的
    // 目的是使ESM 和 CJS 可以一起混用
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    // 要不要生成映射文件(ts -> js)
    "sourceMap": true,
    // 是否可以解析json模块 --- 也就是是否允许将json文件作为一个独立的模块来引入
    "resolveJsonModule": true,
    // 文件路径在解析时, 基本URL(即基于那个路径进行解析,一般设计为当前路径即可)
    "baseUrl": ".",
    // 指定具体要解析使用的类型
    "types": ["webpack-env"],
    // 路径解析(类似于webpack alias) --- 方便ts对我们自己配置的路径别名进行解析
    "paths": {
      // 数组表示可以对应多个,但一般配置一个即可
      "@/*": ["src/*"],
      "components/*": ["src/components/*"]
    },
    // 可以指定在项目中可以使用哪些库的类型
    "lib": ["esnext", "dom", "dom.iterable", "scripthost"],
    // 在vite中的默认根路径是项目所在的文件夹
    // 而tsc在进行解析的时候,默认的根路径是系统根路径
    // 所以需要对两者的编译行为进行统一
    "paths": {
      // ./* --- 这里的相对路径是相对于tsconfig.json所在的文件夹而言的
      "/*": ["./*"]
    }
  },
  
  // 那些文件需要经过ts的解析
  "include": [
    "src/**/*.ts",
    "src/**/*.tsx",
    "src/**/*.vue",
    "tests/**/*.ts",
    "tests/**/*.tsx"
  ],
  
  // 那些文件不需要经过TS的解析
  // 例如在一个ts后缀文件中引入了第三方库
  // 默认情况下会对引入的第三方进行类型检测
  // 有的时候,这会损失性能,所以排除node_modules
  // 但是TS依旧会对我们引入的类型,也就是我们所使用到的类型进行类型解析
  "exclude": ["node_modules"]
}

shims-vue.d.ts

默认情况下Typescript只能对TS文件进行解析,是无法解析vue的SFC文件的,所以我们需要手动对SFC文件的类型进行声明

declare module '*.vue' {
  // 引入类型 DefineComponent
  import { DefineComponent } from 'vue'
  
  // 引入component变量的类型为 DefineComponent的实例
  const component: DefineComponent<{}, {}, any>
        
  // 导出默认类型      
  export default component
}

normalize.css

默认情况下,界面会存在一些默认的样式,例如存在8px的margin和padding

为了避免这些默认样式对项目的uI产生影响,我们可以使用normalize.css来清除浏览器的一些默认样式

安装

> npm i normalize.css

main.ts

// 在全局进行使用
import 'normalize.css'

在Vuex中使用TS

store/types.ts

export interface IRootState {
  name: string
  age: number
}

store/index.ts

import { createStore } from 'vuex'

import login from './login/login'

import { IRootState } from './types'

// 在createStore的时候可以传入泛型 --- 该泛型对应的是state的类型
const store = createStore<IRootState>({
  state() {
    return {
      name: 'Klaus',
      age: 23
    }
  },
  
  modules: {
    login
  }
})
export default store

store/login/login.ts

import { Module } from 'vuex'

import { ILoginState } from './types'
import { IRootState } from '../types'

interface IAccount {
  name: string,
  password: string
}

// Module是vuex提供了用于表示子模块的数据类型
// 需要传递两个泛型 --- 必传
// 第一个泛型 --- 当前模块的state的返回值类型
// 第二个泛型 --- 根state的返回值类型
const loginModule: Module<ILoginState, IRootState> = {
  namespaced: true,
  
  state() {
    return {
      token: ''
    }
  },
 
  actions: {
    async accountLoginAction({ commit }, payload: IAccount) {
      // ... 这里放置登录的异步请求逻辑
    }
  }
}

export default loginModule

页面登录逻辑

主逻辑

import { defineStore } from 'pinia'
import Cookies from 'js-cookie'
import router from '/src/router/index'
import { login, getUserInfo, getMenu } from '../../../api/login'
import { IAccount, ILoginType, IUserInfo } from './types'
import { IResponseType } from '../../../types'

interface IState {
  token: string
  userInfo: IUserInfo | null
  menus: []
}

export default defineStore('loginStore', {
  state: (): IState => ({
    token: '',
    userInfo: null,
    menus: []
  }),

  actions: {
    async login(account: IAccount) {
      // 登录获取token
      const { data } = await login<IAccount, IResponseType<ILoginType>>(account)
      const { token, id } = data

      this.$state.token = token
      Cookies.set('token', token)
			
      // 登录后 获取用户信息
      this.fetchUserInfo(id)
      
      // 登录后 获取用户可操作的菜单列表
      this.fetchMenu(id)
			
      // 返回主页
      router.push('/home')
    },

    async fetchUserInfo(id: number) {
      const { data } = await getUserInfo<IResponseType<IUserInfo>>(id)
      this.$state.userInfo = data
      Cookies.set('userInfo', JSON.stringify(data))
    },

    // 获取动态菜单的方式有两种
    // 1. 通过用户id去服务器获取对应的菜单 --- 这里使用的方式
    // 2. 在本地定义好所有的路由对象,根据用户所对应的角色去获取所有可以访问的路由对象后 动态注册
    async fetchMenu(id: number) {
      const { data } = await getMenu(id)
      this.$state.menus = data
      Cookies.set('menus', JSON.stringify(data))
    },

    // 用户刷新后,是否已登录是通过在storage或者cookie中存储的数据来进行判断的
    // 但是因为用户刷新了界面,所以vuex或pinia中的状态会被全部置空
    // 因此需要重新进行设置
    initStore() {
      const token = Cookies.get('token')
      const userInfo = Cookies.get('userInfo')
      const menus = Cookies.get('menus')

      if (token && userInfo && menus) {
        /* eslint-disable no-param-reassign */
        this.$patch(state => {
          state.token = token
          state.userInfo = JSON.parse(userInfo ?? '{}')
          state.menus = JSON.parse(menus ?? '{}')
        })
      }
    }
  }
})

api.ts

 // 对应的请求api
 // 这里导入的api实际上是axios的实例对象
 // 所以对应的get,post等方法调用的也是axios上对应的方法
 // 对应的泛型也会传递给axios
 import api from '../utils/api'

 // Record<K, T> --- 生成一个新的对象类型,键的类型为K, 值的类型为T
 // Record<string, never> --- 等价于 空对象
 export function login<T, R>(account: T) {
   return api.post<T, R>('/login', account)
 }

 export function getUserInfo<R>(id: number) {
   return api.get<R>(`/users/${id}`)
 }

 export function getMenu<R>(id: number) {
   return api.get<R>(`/role/${id}/menu`)
 }

./types.ts

export interface IAccount {
  name: string
  password: string
}

export interface ILoginType {
  id: number
  name: string
  token: string
}

// 对于后台返回的数据类型 一般情况来说比较复杂
// 推荐使用 json to ts之类的网站直接进行转换0.
interface Role {
  id: number
  name: string
  intro: string
  createAt: Date
  updateAt: Date
}

interface Department {
  id: number
  name: string
  parentId: number
  createAt: Date
  updateAt: Date
  leader: string
}

export interface IUserInfo {
  id: number
  name: string
  realname: string
  cellphone: number
  enable: number
  createAt: Date
  updateAt: Date
  role: Role
  department: Department
}

/src/type.ts

// 自定义的后台返回值类型 
// data类型 不同请求所对应的返回值类型不同,所以定义为泛型 由调用者来决定具体的类型
export interface IResponseType<T> {
  code: number
  data: T
}

main.ts

  import { createApp } from 'vue'
  import { createPinia } from 'pinia'
  import router from './router'
  import App from './App.vue'

  import { useLoginStore } from './store/index'

  import 'normalize.css'

  const app = createApp(App)
  app.use(createPinia())
  app.use(router)

  // 在全局调用store.initStore()
  // 以确保 每次用户刷新页面的时候
  // vuex或pinia中的数据都得到有效初始化
  const store = useLoginStore()
  store.initStore()

  app.mount('#app')