ts+vue3+vite+pinia+vue-router 踩坑合集(持續(xù)更新中...)

前言

  • 抽空整理練手項(xiàng)目升級(jí)后的一些坑點(diǎn); 各位項(xiàng)目中有問題遇到困難也可以留言;我會(huì)盡快回復(fù)! 后續(xù)還會(huì)繼續(xù)更新問題點(diǎn)!

1. 預(yù)處理器

安裝sass以及配置全局scss

npm install node-sass
npm install sass-loader

node 版本不能過高; 目前成功版本為10.15.0; 可通過安裝"n"來管理node版本

配置全局mixin.scss
根目錄找到vite.config.js

// vite.config.js 
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import { resolve } from 'path';
// https://vitejs.dev/config/
export default defineConfig({
...
  plugins: [
    vue(),
  ],
  resolve: {
    // 配置別名
    alias: [
      { find: '@', replacement: resolve(__dirname, './src') },
      { find: 'views', replacement: resolve(__dirname, './src/views') },
    ],
  },
  css: {
    // 添加全局mixin.scss
    preprocessorOptions: {
      scss: {
        additionalData: '@import "@/style/libs.scss";',
      },
    },
  },
});
scss參與計(jì)算 (內(nèi)置模塊)

原來的寫法直接計(jì)算是不行的, 需要使用內(nèi)置模塊math.div()方法參與計(jì)算

//libs.scss
//使用math 需要先聲明使用; 添加在文件頂部
@use "sass:math" 

// 原來的寫法
border-width: $w/2;

// 新寫法
border-width: math.div($w/2)

新寫法; 切記math需要聲明使用(https://sass-lang.com/documentation/modules); 不然就會(huì)提示:
Error: There is no module with the namespace "math".

2. require 批量引入

  • 問題: 想引入某個(gè)目錄下的所有文件? 下面例子
// 批量引入.vue
const file = require.context('@/views/', true, /\.vue$/)
// 批量引入路由
const asyncFiles = require.context('./permissionModules', true, /\.ts$/)
let permissionModules: Array<RouteRecordRaw> = []
asyncFiles.keys().forEach((key) => {
  if (key === './index.ts') return
  permissionModules = permissionModules.concat(asyncFiles(key).default)
})

如果提示找不到名稱“require”。是否需要為節(jié)點(diǎn)安裝類型定義? 請(qǐng)嘗試使用 npm i --save-dev @types/node,然后將 “node” 添加到類型字段。

解決辦法:
按照提示安裝庫
npm i --save-dev @types/node
然后在tsconfig.json types字段中添加node; 然后重啟一下; 就行了

// tsconfig.json
{
  "compilerOptions": {
    "target": "esnext",
    "module": "esnext",
    "strict": false,
    "jsx": "preserve",
    "moduleResolution": "node",
    "types": [
      "vite/client",
      "node",
    ],
     ...
    }
  }
}

3.別名配置

  • 問題: 當(dāng)你使用別名@, 你會(huì)發(fā)現(xiàn)別名框架并沒有配置 會(huì)出現(xiàn)提示 找不到模塊“@/router/index”或其相應(yīng)的類型聲明
import router from '@/router/index';

解決辦法:
vite.config.js 配置別名

// vite.config.js 
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import { resolve } from 'path';
// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    vue(),
  ],
  resolve: {
    // 配置別名
    alias: [
      { find: '@', replacement: resolve(__dirname, './src') },
      { find: 'views', replacement: resolve(__dirname, './src/views') },
    ],
  },
  css: {
    // 添加全局mixin.scss
    preprocessorOptions: {
      scss: {
        additionalData: '@import "@/style/libs.scss";',
      },
    },
  },
});

很多人以為這樣就結(jié)束了;在js 文件中確實(shí)就已經(jīng)可以用了; 但是在.vue 文件中還是不行,其實(shí)還需要在tsconfig.json里面配置對(duì)應(yīng)的別名; 詳情見http://www.itdecent.cn/p/1477b68b2d69

// tsconfig.json 
{ 
  "compilerOptions": {
      ... 
      "paths": { 
        "@/*":[ "./src/*" ], 
        "views/*":[ "./src/views/*" ], 
      } 
    } 
}

4.pinia 使用

注釋:pinia跟vuex一樣也是一個(gè)基于vue的狀態(tài)管理庫;使用pinia時(shí),涉及到扁平架構(gòu)可以理解為它每一個(gè)store 都是動(dòng)態(tài)的、獨(dú)立的;刪除了mutations,更好的支持typescript。搭配TS一起使用時(shí)有靠譜的類型推斷支持。

pinia 安裝:
npm install pinia
or
yarn add pinia

// src/store/index.ts 
import {createPinia} from "pinia"
const pinia = createPinia() 
export default pinia
//main.ts 
.... 
import pinia from "./store/index"
createApp(App).use(router).use(pinia).mount('#app')
定義store
// src/store/modules/user/index.ts 
import {defineStore} from "pinia" 
impor {UserInfoType} from "./type"
import {userInfo} from "@/server/user"
// 封裝的路由
// defineStore的第一個(gè)參數(shù)是應(yīng)用程序中 store 的唯一id;這些注解官網(wǎng)都有 
const userStore = defineStore('userStore',{
     // 類似于data
    state:()=>({ 
       user: {} as UserInfoType
     }),
     // 類似于computed
    getters:{ 
      getUser: state=>state.user, 
       ...
   }, 
   // 沒有mutations!! 
   //actions類似于組件中methods, 可以定義一些業(yè)務(wù)邏輯; 或者復(fù)雜的$patch
   actions: { 
      async  getUserInfo () {
          const {data} = await userInfo()
          // this.user = data
          或者
         this.$patch(state=>{
             state.user = data
          })
      }
   }
}) 
export userStore
使用useStore
 // src/layout/index.vue 
<script lang="ts" setup> 
  import {userStore} from "@/store/***/user"
  import {storeToRefs} from "pinia" 
  const store = useStore() 
  // 使參數(shù)變?yōu)轫憫?yīng)式 ,如果不解構(gòu)直接使用useStore() 也是響應(yīng)式的。
 // storeToRefs()只針對(duì)解構(gòu)后需要數(shù)據(jù)保持響應(yīng)式狀態(tài)。
  const {user} = storeToRefs(store)
</script>
 <template>
   <div class="info">{{user.name}}</div> 
  </template>
狀態(tài)更新的幾種方式
// .vue
<script lang="ts" setup>
import {storeToRefs} from "pinia"
import {userStore} from "@/store"
const store = userStore()
// 改變?yōu)轫憫?yīng)式數(shù)據(jù)
const {user} = storeToRefs(store)
const handleChangeStore = ()=>{
// 方式一:  最簡單的方法,
// 如果修改多個(gè)參數(shù)不推薦,意味著會(huì)多次提交,影響性能
    userStore().$state.name = 'zhangsi'
    userStore().$state.sex = 2
    user.phone = 1234
    
// 方式二: 如果需要修改多個(gè)數(shù)據(jù),建議使用$patch批量更新,
    store.$patch({
        user: {
            name: 'zhangsan',
            sex: 2,
            phone:456
        },
        remark: '備注1233'
    })
    
// 方式三: $patch 一個(gè)函數(shù),最推薦的批量更新方式, 
//  如果只更改一個(gè),可以使用第一種方式
    store.$patch((state)=>{
        state.user.name = 'lisi'
        state.remark = '更新備注'        
    })

 // 第四種: 使用actions,
// store/module/user/index.ts
export const userStore = defineStore('userStore', {
     // 類似于data,定義數(shù)據(jù)最初始狀態(tài)
     state:() => {
         return {
             ...
       }
     },
     // 類似于組件的computed,用于封裝計(jì)算屬性,有緩存的功能
     getters: {},
     // 類似于組件methods, 用與封裝業(yè)務(wù)邏輯,state修改等等
     actions:{
         getContent(){
             // action中可以通過this訪問整個(gè)store事例
             console.log(this.content)
         },
         async user(){ // 異步
            let result = await userInfo()
            this.user = result.data 
         }
         //第四種更新方式: 邏輯比較多的時(shí)候建議使用actions 做處理
         changeState(){
             this.user.name = 'zhangsan'
             
             // 一次性修改多個(gè)參數(shù)
             this.$patch({
                 user: {
                     name:'123'
                     ....
                 }
             })
             
             this.$patch(state=>{
                 state.user.name = '1212'
                 ....
             })
         }
     }
 })
</script>


頁面更新后提示報(bào)錯(cuò)信息:

問題: Uncaught Error: [??]: getActivePinia was called with no active Pinia. Did you forget to install pinia?
什么意思呢? 你當(dāng)前正在使用pinia; 但是pinia 還沒有掛載成功!!!! what? 我使用vuex就不會(huì)這樣呀!!!! 戲精

解決方案:

無意間在廖雪峰的官網(wǎng)看到過一句評(píng)論!!所謂的對(duì)象、函數(shù)在瀏覽器內(nèi)存中就是一個(gè)地址;找個(gè)地址只想某個(gè)函數(shù)或者對(duì)象或者方法、誰拿到這個(gè)地址就擁有操作的權(quán)利; 所謂的函數(shù)作為參數(shù)傳入別的函數(shù); 其實(shí)就是把地址給了它; 讓它擁有的操作的權(quán)利! 我覺得挺精辟!!

使用時(shí)引入pinia

// src/store/index.ts 
import {createPinia} from "pinia"
const pinia = createPinia() 
export default pinia 
//_______________分割線__________________ // 

//src/store/asyncRoute.ts 
import routes from "@/mockjs/mock.routes"
// 路由元信息 
import useStore from "@/store/createRouteInfo"
// 封裝的路由 
import pinia from "./index"
// 引入pinia 
const store = useStore(pinia) // 把pinia 傳進(jìn)去 就可以任意任意使用里面的屬性了

6.Vue-router

6.1.動(dòng)態(tài)路由遇到的問題合集 (后面附有完整動(dòng)態(tài)路由案例)
  • 問題: Invalid route component at extractComponentsGuards ? 這個(gè)問題不陌生吧!! 有倆個(gè)因素;

1. 如果你用JSON.stringify()轉(zhuǎn)為string 后存儲(chǔ); 因?yàn)?code>JSON.stringify()在序列化的過程中function、undefined、symbol這幾種類型是不可枚舉屬性; 序列化后這個(gè)鍵值對(duì)會(huì)消失; addRoute()的時(shí)候就會(huì)發(fā)現(xiàn)沒有這個(gè)component.

2. 使用component:()=>import('views/Order/index.vue')懶加載方式引入;也會(huì)提示Invalid route component;

tips: 調(diào)試路由時(shí)可通過router.getRoutes(); 查看動(dòng)態(tài)路由是否已經(jīng)完整注入進(jìn)去

解決辦法1. 使用Glob Import 動(dòng)態(tài)導(dǎo)入

// src/mockjs/mock.routes.ts
export default routes = [
    {
      path: '/',
      name: 'home',
      component: 'views/Home/index.vue',
      meta: {
        // 頁面標(biāo)題
        title: '首頁',
        // 身份認(rèn)證
        userAuth: false,
      },
    },
]
// src/router/index.ts
import routes from "@/mockjs/mock.routes"http:// 路由元信息

// 不推薦這種用法,這里只是為了踩坑,
const modules = import.meta.golb("../views/**/**.vue") //使用golb ++
routes.forEach(item=>{
    router.addRoute("home", {
          path: item.path,
          name: item.name,
          meta: ...item.meta,
          component:
             //本地能使用,上生產(chǎn)直接GG
            //()=>import(/* @vite-ignore */ `/@views/${itemRouter.component}`),
        //使用modules
            modules[/* @vite-ignore */ `../views/${item.component}`],
        })
})

解決辦法2 : 在聲明路由數(shù)據(jù)時(shí)使用 RouteRecordRaw; 下面是RouteRecordRaw的注解

當(dāng)用戶通過 routes option 或者 router.addRoute() 來添加路由時(shí),可以得到路由記錄。 有三種不同的路由記錄:

  1. 單一視圖記錄:有一個(gè) component 配置
  2. 多視圖記錄 (命名視圖) :有一個(gè) components 配置
  3. 重定向記錄:沒有 component 或 components 配置,因?yàn)橹囟ㄏ蛴涗浻肋h(yuǎn)不會(huì)到達(dá)。
// src/mockjs/mock.routes.ts 
export default routes:RouteRecordRaw[] = [ 
  { 
    path: '/', 
    name: 'home', 
    component:  component: () => import("views/Home/index.vue"),
    meta: { 
        // 頁面標(biāo)題 
        title: '首頁', 
        // 身份認(rèn)證 
        userAuth: false, 
    }, 
  }, 
]

// src/router/index.ts 
import routes from "@/mockjs/mock.routes"
// 路由元信息  確保自己的地址是否正確
routes.forEach(item=>{ router.addRoute(item}) })

完整的動(dòng)態(tài)路由權(quán)限

很多人做動(dòng)態(tài)路由時(shí) 數(shù)據(jù)存在pinia; 頁面刷新后頁面空白或者進(jìn)入404頁面; 并且使用router.getRoutes() 查看新增的路由已經(jīng)消失; 是因?yàn)閟tore是存在瀏覽器的堆棧內(nèi)存里面; 刷新后頁面數(shù)據(jù)被釋放掉; 那么如何解決呢? 想一想 頁面刷新后我們的框架是會(huì)重新掛載的, 思考??,是不是可以在路由權(quán)限里面訪問actions addRoute()

公開路由和權(quán)限路由一般我會(huì)分開,方便維護(hù)

// src/router/permissions.ts
import { permissionRoutes } from "@/router"
import pinia, { userStore } from "@/store/index"
import { ElMessage } from 'element-plus';
//路由實(shí)例
import router from "./index"
import { RouteLocationNormalized } from "vue-router"
// 白名單
import { routeWhitelist } from "@/config/whitelist"
const store = userStore(pinia)
router.beforeEach(async (to: RouteLocationNormalized, from: RouteLocationNormalized, next: any) => {
    // 防止token未獲取到就執(zhí)行或者取到一個(gè)失效的token
    let meta = to.meta, token = await store.getToken(); 
    //登錄后
    if (!!token) {
        // 如果當(dāng)前是登錄頁
        if (to.path === '/login') {
            next({ path: '/' })
        } else {
            if (store.$state.routeModules.length === 0) {
                try {
                    // 獲取用戶信息
                    const { result } = await store.getUsers()
                    // 當(dāng)前地址需要登錄 && 權(quán)限為admin
                    if (result?.roles === 'admin') {
                        //添加動(dòng)態(tài)路由
                        store.$patch(state => {
                            state.routeModules = permissionRoutes
                        })
                        // 添加異步就是跟token一樣的道理, 這是為什么會(huì)404或者白屏的原因
                        await store.addRouteModules()
                        next({...to, replace: true})
                    } else {
                        if (result?.roles === 'client') {
                            ElMessage.warning("普通用戶暫不開放管理權(quán)限");
                            return
                        }
                        // 如果當(dāng)前路由不是登錄頁,則執(zhí)行跳轉(zhuǎn)到登錄頁面
                        if (routeWhitelist.indexOf(to.path) !== -1 || to.name == 'NotFound') {
                            next()
                        } else {
                            next({ path: `/login?redirect=${to.path}`, replace: true })
                        }
                    }
                } catch (error) {
                    next(`/login?redirect=${to.path}`)
                }
            } else {
                next()
            }
        }
    } else {
        // 如果是前往登陸就正常跳轉(zhuǎn); 不是則強(qiáng)制到登陸頁
        if (to.path == '/login') {
            next()
        } else {
            // 是否需要登錄
            if ('userAuth' in meta && !meta.userAuth) {
                next()
            } else {
                next({ path: `/login?redirect=${to.path}`, replace: true })
            }
        }
    }

})

router.afterEach((to: RouteLocationNormalized) => {
    // title
    document.title = to.meta.title.toString()
})
// store/user/index.ts
import {RouteRecordRaw} from "vue-router"
import { loginType, UserInfoType } from './type';
import { toRaw } from '@vue/reactivity';
const userStore = defineStore('userStore', {
    state: () => ({
        routeModules: [] as RouteRecordRaw [] , // route
        userInfo: {} as UserInfoType, 
        token: '', // 單個(gè)數(shù)據(jù)ts會(huì)參與類型推斷,除非你手欠中途強(qiáng)制改類型,
        ...
    }),
    getters: {
        ...
    },
    actions: {
        // token 獲取
        async getToken() {
            return this.token = this.token ? this.token : localStorage.getItem(Storage.TOKEN) || null
        },
        // 用戶信息獲取
        async getUsers() {
            const res = await getUserProfile()
            if (res?.code === 200) {
                const { result } = res
                this.userInfo = result
                LocalStorage.set(Storage.USER, JSON.stringify(result))
            }
            return res
        },
        // 設(shè)置路由權(quán)限
        async addRouteModules() {
            toRaw(this.routeModules).forEach(el => {
                router.addRoute(el)
            })
            // console.log(router.getRoutes())
        }
    },
})

export default userStore
// main.js
import router from "./router/index"
 ..... 
import "./router/permissions"
 ..... 
createApp(App).use(router).use(pinia).mount('#app')
未完待續(xù), 后續(xù)還會(huì)更新; 這些只是一部分; 歡迎大家參與留言討論
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容