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

前言

  • 抽空整理练手项目升级后的一些坑点; 各位项目中有问题遇到困难也可以留言;我会尽快回复! 后续还会继续更新问题点!

1. 预处理器

安装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参与计算 (内置模块)

原来的写法直接计算是不行的, 需要使用内置模块math.div()方法参与计算

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

// 原来的写法
border-width: $w/2;

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

新写法; 切记math需要声明使用(https://sass-lang.com/documentation/modules); 不然就会提示:
Error: There is no module with the namespace "math".

2. require 批量引入

  • 问题: 想引入某个目录下的所有文件? 下面例子
// 批量引入.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”。是否需要为节点安装类型定义? 请尝试使用 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.别名配置

  • 问题: 当你使用别名@, 你会发现别名框架并没有配置 会出现提示 找不到模块“@/router/index”或其相应的类型声明
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";',
      },
    },
  },
});

很多人以为这样就结束了;在js 文件中确实就已经可以用了; 但是在.vue 文件中还是不行,其实还需要在tsconfig.json里面配置对应的别名; 详情见https://www.jianshu.com/p/1477b68b2d69

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

4.pinia 使用

注释:pinia跟vuex一样也是一个基于vue的状态管理库;使用pinia时,涉及到扁平架构可以理解为它每一个store 都是动态的、独立的;删除了mutations,更好的支持typescript。搭配TS一起使用时有靠谱的类型推断支持。

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的第一个参数是应用程序中 store 的唯一id;这些注解官网都有 
const userStore = defineStore('userStore',{
     // 类似于data
    state:()=>({ 
       user: {} as UserInfoType
     }),
     // 类似于computed
    getters:{ 
      getUser: state=>state.user, 
       ...
   }, 
   // 没有mutations!! 
   //actions类似于组件中methods, 可以定义一些业务逻辑; 或者复杂的$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() 
  // 使参数变为响应式 ,如果不解构直接使用useStore() 也是响应式的。
 // storeToRefs()只针对解构后需要数据保持响应式状态。
  const {user} = storeToRefs(store)
</script>
 <template>
   <div class="info">{{user.name}}</div> 
  </template>
状态更新的几种方式
// .vue
<script lang="ts" setup>
import {storeToRefs} from "pinia"
import {userStore} from "@/store"
const store = userStore()
// 改变为响应式数据
const {user} = storeToRefs(store)
const handleChangeStore = ()=>{
// 方式一:  最简单的方法,
// 如果修改多个参数不推荐,意味着会多次提交,影响性能
    userStore().$state.name = 'zhangsi'
    userStore().$state.sex = 2
    user.phone = 1234
    
// 方式二: 如果需要修改多个数据,建议使用$patch批量更新,
    store.$patch({
        user: {
            name: 'zhangsan',
            sex: 2,
            phone:456
        },
        remark: '备注1233'
    })
    
// 方式三: $patch 一个函数,最推荐的批量更新方式, 
//  如果只更改一个,可以使用第一种方式
    store.$patch((state)=>{
        state.user.name = 'lisi'
        state.remark = '更新备注'        
    })

 // 第四种: 使用actions,
// store/module/user/index.ts
export const userStore = defineStore('userStore', {
     // 类似于data,定义数据最初始状态
     state:() => {
         return {
             ...
       }
     },
     // 类似于组件的computed,用于封装计算属性,有缓存的功能
     getters: {},
     // 类似于组件methods, 用与封装业务逻辑,state修改等等
     actions:{
         getContent(){
             // action中可以通过this访问整个store事例
             console.log(this.content)
         },
         async user(){ // 异步
            let result = await userInfo()
            this.user = result.data 
         }
         //第四种更新方式: 逻辑比较多的时候建议使用actions 做处理
         changeState(){
             this.user.name = 'zhangsan'
             
             // 一次性修改多个参数
             this.$patch({
                 user: {
                     name:'123'
                     ....
                 }
             })
             
             this.$patch(state=>{
                 state.user.name = '1212'
                 ....
             })
         }
     }
 })
</script>


页面更新后提示报错信息:

问题: Uncaught Error: [🍍]: getActivePinia was called with no active Pinia. Did you forget to install pinia?
什么意思呢? 你当前正在使用pinia; 但是pinia 还没有挂载成功!!!! what? 我使用vuex就不会这样呀!!!! 戏精

解决方案:

无意间在廖雪峰的官网看到过一句评论!!所谓的对象、函数在浏览器内存中就是一个地址;找个地址只想某个函数或者对象或者方法、谁拿到这个地址就拥有操作的权利; 所谓的函数作为参数传入别的函数; 其实就是把地址给了它; 让它拥有的操作的权利! 我觉得挺精辟!!

使用时引入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 传进去 就可以任意任意使用里面的属性了

6.Vue-router

6.1.动态路由遇到的问题合集 (后面附有完整动态路由案例)
  • 问题: Invalid route component at extractComponentsGuards ? 这个问题不陌生吧!! 有俩个因素;

1. 如果你用JSON.stringify()转为string 后存储; 因为JSON.stringify()在序列化的过程中function、undefined、symbol这几种类型是不可枚举属性; 序列化后这个键值对会消失; addRoute()的时候就会发现没有这个component.

2. 使用component:()=>import('views/Order/index.vue')懒加载方式引入;也会提示Invalid route component;

tips: 调试路由时可通过router.getRoutes(); 查看动态路由是否已经完整注入进去

解决办法1. 使用Glob Import 动态导入

// src/mockjs/mock.routes.ts
export default routes = [
    {
      path: '/',
      name: 'home',
      component: 'views/Home/index.vue',
      meta: {
        // 页面标题
        title: '首页',
        // 身份认证
        userAuth: false,
      },
    },
]
// src/router/index.ts
import routes from "@/mockjs/mock.routes"// 路由元信息

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

解决办法2 : 在声明路由数据时使用 RouteRecordRaw; 下面是RouteRecordRaw的注解

当用户通过 routes option 或者 router.addRoute() 来添加路由时,可以得到路由记录。 有三种不同的路由记录:

  1. 单一视图记录:有一个 component 配置
  2. 多视图记录 (命名视图) :有一个 components 配置
  3. 重定向记录:没有 component 或 components 配置,因为重定向记录永远不会到达。
// src/mockjs/mock.routes.ts 
export default routes:RouteRecordRaw[] = [ 
  { 
    path: '/', 
    name: 'home', 
    component:  component: () => import("views/Home/index.vue"),
    meta: { 
        // 页面标题 
        title: '首页', 
        // 身份认证 
        userAuth: false, 
    }, 
  }, 
]

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

完整的动态路由权限

很多人做动态路由时 数据存在pinia; 页面刷新后页面空白或者进入404页面; 并且使用router.getRoutes() 查看新增的路由已经消失; 是因为store是存在浏览器的堆栈内存里面; 刷新后页面数据被释放掉; 那么如何解决呢? 想一想 页面刷新后我们的框架是会重新挂载的, 思考🤔,是不是可以在路由权限里面访问actions addRoute()

公开路由和权限路由一般我会分开,方便维护

// src/router/permissions.ts
import { permissionRoutes } from "@/router"
import pinia, { userStore } from "@/store/index"
import { ElMessage } from 'element-plus';
//路由实例
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未获取到就执行或者取到一个失效的token
    let meta = to.meta, token = await store.getToken(); 
    //登录后
    if (!!token) {
        // 如果当前是登录页
        if (to.path === '/login') {
            next({ path: '/' })
        } else {
            if (store.$state.routeModules.length === 0) {
                try {
                    // 获取用户信息
                    const { result } = await store.getUsers()
                    // 当前地址需要登录 && 权限为admin
                    if (result?.roles === 'admin') {
                        //添加动态路由
                        store.$patch(state => {
                            state.routeModules = permissionRoutes
                        })
                        // 添加异步就是跟token一样的道理, 这是为什么会404或者白屏的原因
                        await store.addRouteModules()
                        next({...to, replace: true})
                    } else {
                        if (result?.roles === 'client') {
                            ElMessage.warning("普通用户暂不开放管理权限");
                            return
                        }
                        // 如果当前路由不是登录页,则执行跳转到登录页面
                        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 {
        // 如果是前往登陆就正常跳转; 不是则强制到登陆页
        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: '', // 单个数据ts会参与类型推断,除非你手欠中途强制改类型,
        ...
    }),
    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
        },
        // 设置路由权限
        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')
未完待续, 后续还会更新; 这些只是一部分; 欢迎大家参与留言讨论
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,290评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,399评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,021评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,034评论 0 207
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,412评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,651评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,902评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,605评论 0 199
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,339评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,586评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,076评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,400评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,060评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,083评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,851评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,685评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,595评论 2 270

推荐阅读更多精彩内容