项目手记

环境搭建

1# 方式一:推荐(可选安装router、pinia等)
2npm create vue@latest
3# 初始化配置
4✔ Project name: … vue3-vite-app
5✔ Add TypeScript? … Yes
6✔ Add JSX Support? … No
7✔ Add Vue Router for Single Page Application development? … Yes
8✔ Add Pinia for state management? … Yes
9✔ Add Vitest for Unit Testing? … No
10✔ Add an End-to-End Testing Solution? › No
11✔ Add ESLint for code quality? … Yes
12✔ Add Prettier for code formatting? … Yes
13
14
15# 方式二:vite初始化项目(简洁版)
16npm create vite@latest
17# 初始化配置
18✔ Project name: … vue3-vite-app
19✔ Select a framework: › Vue
20✔ Select a variant: › TypeScript

1.安装依赖

1npm i vue-router@next -S
2npm i axios
3npm i less
4npm i pinia
5npm i @types/node -D
6# element-plus
7npm install element-plus --save
8# 按需引入
9npm install -D unplugin-vue-components unplugin-auto-import
10
11# cookie
12npm i js-cookie # js版
13npm i @types/js-cookie	# ts版

2.搭建路由

/src/router/index.ts

1import { createRouter, createWebHashHistory } from "vue-router";
2
3const routes = [
4    {
5        path: "/",
6        name: "home",
7        component :() => import("../views/home/home.vue")
8    },
9    {
10        path: "/login",
11        name: "login",
12        component: () => import('../views/login/login.vue')
13
14    }
15]
16
17const router = createRouter({
18    history: createWebHashHistory(),
19    routes,
20})
21
22export default router

/src/main.ts

1import { createApp } from 'vue'
2import App from './App.vue'
3import router from './router'
4
5createApp(App).use(router).mount('#app')

找不到模块“…/views/xxxx.vue”或其相应的类型声明。

env.d.ts

1declare module "*.vue" {
2    import type { DefineComponent } from "vue";
3    // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
4    const component: DefineComponent<{}, {}, any>;
5    export default component;
6}

3.封装请求

1// api/index.ts
2import axios from 'axios'
3const instance = axios.create({
4    baseURL: '',
5    timeout: 1000,
6    headers: { 'X-Custom-Header': '' }
7});
8
9instance.interceptors.request.use(config => {
10    // 判断token
11    return config
12}, err => {
13    return Promise.reject(err)
14})
15instance.interceptors.response.use(res => {
16    return res.data
17}, err => {
18    return Promise.reject(err)
19})
20
21export default instance

4.支持@符

1// vite.config.ts
2import { defineConfig } from 'vite'
3import vue from '@vitejs/plugin-vue'
4import * as path from 'path'
5const resolve = (dir: string) => path.join(__dirname, dir)
6
7export default defineConfig({
8  plugins: [ vue() ],
9  resolve: {
10    alias: {
11      '@': resolve('src'),
12      comps: resolve('src/components'),
13      views: resolve('src/views'),
14    }
15  }
16})
1// tsconfig.json
2{
3  "compilerOptions": {
4    "target": "ESNext",
5    "useDefineForClassFields": true,
6    "module": "ESNext",
7    "moduleResolution": "Node",
8    "strict": true,
9    "jsx": "preserve",
10    "resolveJsonModule": true,
11    "isolatedModules": true,
12    "esModuleInterop": true,
13    "lib": ["ESNext", "DOM"],
14    "skipLibCheck": true,
15    "noEmit": true,
16    "baseUrl": "",
17    "paths": {
18      "@/*":["src/*"]
19    }
20
21  },
22  "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
23  "references": [{ "path": "./tsconfig.node.json" }]
24}

5.配置跨域

1// vite.config.ts
2import { defineConfig } from 'vite'
3import vue from '@vitejs/plugin-vue'
4
5export default defineConfig({
6  plugins: [ vue() ],
7  server:{
8    // Vue3在这里配置跨域
9    proxy: {
10      "/api": {
11        target: "",
12        changeOrigin: true,
13        rewrite: (path) => path.replace(/^\/api/, "")
14      }
15    }
16  }
17})

6.Element-Plus

1// vite.config.ts
2import { defineConfig } from 'vite'
3import vue from '@vitejs/plugin-vue'
4
5// element-plus 按需自动导入
6import AutoImport from 'unplugin-auto-import/vite'
7import Components from 'unplugin-vue-components/vite'
8import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
9
10
11
12// https://vitejs.dev/config/
13export default defineConfig({
14  plugins: [
15    vue(),
16    AutoImport({
17      resolvers: [ElementPlusResolver()],
18    }),
19    Components({
20      resolvers: [ElementPlusResolver()],
21    }),
22  ]
23})

本地存储方式

localStorage

localStorage中只能存字符串,不能存对象,否则拿不到值

因此,在存入时要用window.localStorage.setItem('key', JSON.stringify(object))将对象转为字符串,在获取时要用JSON.parse(localStorage.getItem('key'))转换再拿到值

1// 设置
2localStorage.setItem('token', token);
3
4// 读取
5localStorage.getItem('token');
6
7// 移除指定
8localStorage.removeItem('token');
9
10// 移除所有
11localStorage.clear();

sessionStorage

1// 设置
2sessionStorage.setItem('token', token);
3
4// 读取
5sessionStorage.getItem('token');
6
7// 移除指定
8sessionStorage.removeItem('token');
9
10// 移除所有
11sessionStorage.clear();
1import Cookie from 'js-cookie'
2
3// 设置
4Cookie.set("token", token, { expires: 7 })
5
6// 读取
7Cookie.get('token')
8
9// 删除
10Cookie.remove('token')

权限管理

Vuex或pinia刷新数据丢失问题

1// 路由拦截:页面刷新100%会执行的位置
2router.beforeEach(async (to, from, next) => {
3  const token = Cookie.get("token");
4  const store = useStore()
5  if (token && store.menus.length === 0) {
6    const getAdminInfo = await getAdminInfoApi()
7    store.setMenu(getAdminInfo.data.menus);
8  }
9  next()
10});

根据权限生成动态路由

权限管理的核心就是addRoute

1import { createRouter, createWebHashHistory } from "vue-router";
2import Cookie from "js-cookie";
3
4import useStore from "@/store";
5import { getAdminInfoApi } from "@/api/api";
6
7const routes = [
8  {
9    path: "/login",
10    name: "login",
11    component: () => import(/* webpackChunkName: login" */ "../views/login/login.vue"),
12  }
13];
14
15const router = createRouter({
16  history: createWebHashHistory(), // createWebHashHistory() hash模式  createWebHistory() history
17  routes,
18});
19
20// 动态加载路由
21const getRouter = () => {
22  let children = new Array();
23  // 拿到用户权限列表
24  const store = useStore();
25  let menus = store.getMenu;
26  menus.map((item) => {
27    item.children!.map((v) => {
28      let itemRouter = {
29        path: `/${item.name}/${v.name}`,
30        name: v.name,
31        component: () => import(`../views/${item.name}/${v.name}.vue`),
32      }
33      children.push(itemRouter);
34    });
35  });
36
37  let myRoute = {
38      path: '/home',
39      name: 'home',
40      component: () => import(/* webpackChunkName: "home" */ "../views/home/home.vue"),
41      children:children
42    }
43  // 动态添加路由
44  router.addRoute(myRoute);
45};
46
47// 路由拦截 页面刷新100%会执行的位置
48router.beforeEach(async (to, from, next) => {
49  const store = useStore();
50  const token = Cookie.get("token");
51  // 用户登录了,但是页面刷新导致pinia数据丢失了
52  if (token && store.menus.length === 0) {
53    // 重新发请求
54    const getAdminInfo = await getAdminInfoApi();
55    store.setMenu(getAdminInfo.data.menus);
56    // 重新生成动态路由
57    getRouter();
58    next(to); // 这里的next()不能为空
59  } else if (
60    token &&
61    store.menus.length !== 0 &&
62    from.path == "/login" &&
63    to.path == "/home"
64  ) {
65    // 第一次刚刚成功登录的时候,只需要生成一下路由即可
66    getRouter();
67    next("/ums/admin");
68  } else if (!token && to.path !== "/login") {
69    // 未登录,就想访问其他页
70    next("/login");
71  } else if (token && to.path == "/login") {
72    // 已登录,还想访问登录页
73    next(from);
74  } else {
75    next();
76  }
77});
78export default router;

文件上传

1<!-- element ui 第一种 -->
2<el-upload
3  class="avatar-uploader"
4  action="http://kumanxuan1.f3322.net:8360/admin/upload/goodNewPic"
5  :show-file-list="false"
6  :on-success="onSuccess"
7  :before-upload="onBefore"
8  name="good_pic"
9>
10  <img v-if="imageUrl" :src="imageUrl" class="avatar" />
11  <el-icon v-else class="avatar-uploader-icon"><Plus /></el-icon>
12</el-upload>
1<!-- element ui 第二种 常用-->
2<el-upload
3  class="avatar-uploader"
4  action="http://kumanxuan1.f3322.net:8360/admin/upload/goodNewPic"
5  :show-file-list="false"
6  :http-request="twoUpload"
7>
8  <img v-if="imageUrl2" :src="imageUrl2" class="avatar" />
9  <el-icon v-else class="avatar-uploader-icon"><Plus /></el-icon>
10</el-upload>
1<!-- 第三种 终极通用版 --> 
2<input type="file" ref="file" />
3<el-button @click="lastUpload">上传</el-button>