【Vue核心篇Ⅶ】智慧园区后台管理、前台大屏可视化、乾坤微前端、Vue2+Vue3全覆盖

528 阅读7分钟

项目总结

问题梳理

为什么要将token在Vuex和localStorage中各存一份

Vuex 中的 token 存储在内存中,存取快但刷新即丢失
localStorage 中的 token 永久保存,但存取速度较慢
存的时候往 Vuex 和 localStorage 中都存一份,初始化时从Vuex中取

为什么要在axios拦截器中统一添加请求头

很多接口调用都要求用户已登录,在请求拦截器中统一添加更方便

为什么要创建多个axios实例

使用 `axios.create({url})` 可以创建一个axios实例
后台接口地址可能有多个,相应地,我们可以创建多个 axios 实例,一个axios实例对应一个服务器地址,彼此独立互不影响

axios 拦截器也可以有多个,前一个拦截器会把处理的结果返回给后一拦截器进行再次处理

Vue2 新增属性无响应式问题

给对象中没有定义的属性赋值,或使用数组下标修改元素的值,不会触发视图更新

使用 $set 代替直接赋值:

this.$set(对象,新属性,值) // 代替 对象.新属性 = 值

$refs无法获取子组件实例问题

如果模版中 ref 外层有 v-for,那么 this.$refs 获取的是一个数组,可以循环遍历得到每个子组件实例

初始化渲染无数据的问题

在组件初始化时,有时明明在 onMounted 中发起了数据请求,并且成功获得了响应,但页面始终无法正常渲染
解决方法:await 获取数据的请求,保证请求完成后再执行后面的代码去渲染页面

访问空对象深层属性报错问题

image-20231102144802963.png

由于请求是异步的,页面首次渲染时,可能后台数据还没返回,导致读取 undefined 的深层属性报错
解决办法:
1. `parkInfo.base?.buildingTotal` 可选链操作符
2. `parkInfo.base && parkInfo.base.buildingTotal` 逻辑与操作符
等到后台返回数据,响应式状态发生变化后,Vue会再次渲染

模块导入导出错误

image-20231023211301938.png

这个错误一般是模块名、路径拼写错误导致的

重点概念

什么是Saas云服务

SaaSSoftware-as-a-Service 软件即服务)
和传统的买卖软件、程序不同,Saas 交易的不是程序,而是账号,相当于只提供解决方案

浏览器本地存储方案对比

# localStorage
- 容量5MB
- 纯前端操作

# Cookie
- 容量4kb
- 前后端均可操作
- 请求时自动携带
- 需引入 js-cookie 库

axios请求简写

// 查询
axios.get(url, { params })
// 添加
axios.post(url, data)
// 修改
axios.put(url, data)
// 删除
axios.delete(url, { data })

请求时,会携带查询参数中的空字符串,而 null 不会。因此,如果值是可选的,初始化时最好赋值为 null

async 函数返回一个 Promise,可以 await 等待函数返回结果,然后再执行一些操作

新增编辑的复用

# 两种实现方式
- 新页面路由跳转
- 弹层

# 实现过程
新增和修改的功能逻辑相似,请求体基本一致(修改通常多一个 id 字段)
- 页面/弹层打开时,根据传参有无 id 决定是否回填数据
- 提交时,根据传参有无 id 决定调用哪个接口

# 关闭弹层必做
- 清空表单校验规则
- 重置表单数据
- 关闭弹层

侧边栏菜单路由划分

路由表是侧边栏菜单的数据依赖。一级菜单、二级菜单实际上都是二级路由,共用首页的公共样式和布局(SideBar、NavBar)

多个项目共享token

- 两个项目只要域名相同就能共享 cookie 数据
- 两个项目域名相同,端口也相同才能共享 localStorage 数据

组件拆分 vs 逻辑拆分

- 组件拆分的代码有 HTML + JS + CSS
- 逻辑拆分只拆分 JS 代码

自定义hooks

1.把功能相关的逻辑代码封装在一个 `useXxx()` 函数中,return 需要在组件中使用的变量或方法,最后导出
2.在组件的setup语法糖中,通过导入并调用` useXxx()` 函数,配合解构赋值把函数内部的数据和方法添加在组件中(注意函数调用要加小括号)

3D模型渲染和大屏适配

# 3d模型渲染流程
- 设计师建模完成后,将最终文件交给后端,由后端上传到服务器,并提供接口
- 拉取并渲染3d模型:spline.load(url)

# 大屏适配
- 完美适配贴合UI设计稿的尺寸,在其它设备上保证正常渲染和显示

模型解析包和制作3D的软件配套,才能正确解析(如 spline -> @splinetool/runtime)

重要代码

数组去重

const resArr = [...new Set(arr)]

创建一个纯对象(无原型链,安全性高)

const obj = Object.create(null)

生产环境移除console输出

// process.env.NODE_ENV 获取项目所处环境 开发or生产
if (process.env.NODE_ENV === 'production') {
    // 重写log方法
    console.log = () => {}
}

组件内置render函数用法

// render() 用来生成虚拟DOM
// 相当于 createElement(元素,元素的属性,子元素)
export default {
  render(h) {
    return h('h1', { class: 'pink' }, '智慧园区')
  }
}

Vue.use内部原理

// Vue.use() 是组件全局注册的插件写法,内部实现如下:
export default {
  install(Vue) {
    Vue.component('xxx', xxx)
  }
}

Vue3中使用echarts

import * as echarts from 'echarts'
import { ref, onMounted } from 'vue'

// 获取DOM实例 <div ref="pieChart"></div>
const pieChart = ref(null)
// 初始化渲染
const initPieChart = () => {
    // 初始化 echarts 实例
    const myPieChart = echarts.init(pieChart.value)
    // 生成图表
    myPieChart.setOption(option)
}
onMounted(async ()=>{
    await getData()
    initPieChart()
})

块元素全屏显示

html,
body,
#app {
  height: 100vh;
  overflow: hidden;
}

动态类名实现tab栏切换

<div
     v-for="(item,index) in list" :key="item.id"
     :class="{ active: index === activeIndex }"
     @click="activeIndex = index"
></div>

SheetJS库实现Excel导出

async exportToExcel() {
    const res = await getBuildingListAPI(this.params)
    const tableHeader = ['name', 'floors', 'area', 'propertyFeePrice', 'status']
    // 处理要导出的数据
    const sheetData = res.data.rows.map((item) => {
        const obj = {}
        tableHeader.forEach(key => {
            obj[key] = item[key]
        })
        return obj
    })
    // 创建一个工作表
    const worksheet = utils.json_to_sheet(sheetData)
    // 创建一个新的工作簿
    const workbook = utils.book_new()
    // 把工作表添加到工作簿
    utils.book_append_sheet(workbook, worksheet, '楼宇信息表')
    // 改写表头
    utils.sheet_add_aoa(worksheet, [['楼宇名称', '层数', '在管面积(㎡)', '物业费(㎡)', '状态']], { origin: 'A1' })
    // 导出Excel
    writeFileXLSX(workbook, `智慧园区-${+new Date()}.xlsx`)
}

清除Vue组件多词警告

// .eslintrc.cjs
module.exports = {
  rules: {
    "vue/multi-word-component-names": "off",
  }
}

Prettier插件格式化代码

// setting.json
module.exports = {
  "semi": true, // 结尾使用分号
  "tabWidth": 2,  // tab的宽度 2个字符
  "singleQuote": false,  // 使用双引号,要想使用单引号,改成true即可
  "printWidth": 120,  // 每行最多显示的字符数,超过这个就换行显示
  "trailingComma": "none",  // 结尾是否添加逗号
  "bracketSpacing": true,  // 对象括号两边是否用空格隔开
  "jsxBracketSameLine": true  // 在jsx中把'>'是否单独放一行
}

Better Comments插件注释增强

image-20231020180026824.png

// *红色注释
// ?红色注释
// !红色注释
// TODO 待办事项

RBAC权限控制

什么是RBAC

# RBAC(Role Based Access Control)
- 基于角色的权限控制方案 `用户--角色--权限点`
- 包含 `菜单权限控制(页面访问控制)` 和 `按钮权限控制(按钮的显示与隐藏)` 两部分
- 权限标识组成:`一级菜单:二级菜单:功能` 如 `sys:role:add_edit`
- 实际上,后台接口调用也有权限控制(根据请求头中的 Authorization 属性)

菜单权限控制

image-20231030082442333.png

不同角色的员工进入到系统中看到的左侧菜单是不一样的,根据不同的员工登录控制显示与之对应的左侧菜单

# 实现步骤
在路由前置守卫中:
1. 登录成功后,获取用户信息和权限点数组,保存在Vuex中(如果有,就不要再获取了)
2. 根据用户的权限点,划分一级权限点列表和二级权限点列表(去重后)
3. 把动态路由单独划分出来
4. 过滤出用户可访问的动态路由
5. 在路由表中挨个添加动态路由 router.addRoute(route)
6. 渲染侧边栏菜单
7. 管理员权限特别处理
8. 退出登录(重置菜单、重置路由、重置用户信息)

# 路由表拆分
- 动态路由:需要做权限控制,不同用户的动态路由不同
- 静态路由:不需要权限控制,谁都可以访问

按钮权限控制

方式一 自定义指令(推荐)

Vue指令可用来操作DOM,利用这个特点控制按钮的显示与隐藏

Vue.directive('auth-btn', {
  inserted(el, binding) {
    const { permissions } = store.state.user.profile
	console.log(permissions) // ["park:building:list","park:building:add_edit" ...]
    
    // 管理员 特别处理
    if (permissions.includes('*:*:*')) return

    // 其他角色 如果没有权限,移除相关元素
    if (!permissions.includes(binding.value)) {
      el.remove()
    }
  }
})
<!-- 使用自定义指令,这里以添加楼宇举例 -->
<el-button v-auth-btn="'park:building:add_edit'" type="primary" @click="openDialog()">添加楼宇</el-button>

方式二 高阶组件

用高阶组件(自定义)包裹每个按钮,在高阶组件中编写逻辑控制按钮的显示与隐藏

// AuthBtn.vue
export default {
  props: {
    perms: {
      type: String,
      default: ''
    }
  },
  computed: {
    isShow() {
      // 如果权限满足或是管理员,返回true,以显示插槽中的按钮
      const { permissions } = this.$store.state.user.profile
      return permissions.includes('*:*:*') || permissions.includes(this.perms)
    }
  },
  render(h) {
    return this.isShow && this.$slots.default
  }
}
<!-- 记得先在main.js中注册高阶组件 -->
<AuthBtn perms="park:building:add_edit">
    <el-button type="primary" @click="openDialog(null)">添加楼宇</el-button>
</AuthBtn>

微前端接入

什么是微前端

微前端架构旨在解决单体应用在一个相对长的时间跨度下,由于参与的人员、团队的增多、变迁,从一个普通应用演变成一个巨石应用后,随之而来的应用不可维护的问题。

# 特点
- 技术栈无关
- 独立开发
- 增量升级

# 好处
- 增强团队独立性
- 提高可维护性
- 提高性能

# 微前端解决方案
- 原生iframe
- 乾坤 https://qiankun.umijs.org/zh/guide
- 无界 https://wujie-micro.github.io/doc/

微前端的实现

乾坤微前端-主应用

npm i qiankun -S
// main.js 注册子应用
import { registerMicroApps, start } from 'qiankun'
registerMicroApps([
  {
    name: 'big-screen', // 子应用名称
    container: '#container', // 挂载容器(子应用显示位置 <div id="container" />)
    entry: '//localhost:5173', // 子应用本地运行地址(pnpm dev)
    activeRule: '/big-screen' // 激活路由(在访问哪个路由时加载子应用,需要和子应用的路由名称相对应)
  }
])
start()
<!-- APP.vue -->
<div id="app">
    <router-view />
    <!-- 子应用挂载点 -->
    <div id="container" />
</div>
<!-- NavBar.vue 触发路由跳转 -->
<!-- 按钮跳转 -->
<el-button size="small" plain @click="$router.push('/big-screen')">可视化大屏</el-button>

乾坤微前端-子应用

子应用技术栈采用 Vue3,由于乾坤默认不支持 Vite,需要借助插件:

pnpm i vite-plugin-qiankun -D
// vite.config.js
import qiankun from 'vite-plugin-qiankun'
export default defineConfig({
  plugins: [
    vue(),
    // 这里的名称要和注册子应用的name保持一致
    qiankun('big-screen', {
      useDevMode: true
    })
  ],
  server: {
    // 防止开发阶段子应用的 assets 静态资源加载问题
    origin: '//localhost:5173'
  }
})
// main.js
// 使用乾坤代替Vue渲染子应用
import { renderWithQiankun, qiankunWindow } from 'vite-plugin-qiankun/dist/helper'
renderWithQiankun({
  mount (props) {
    console.log('mount')
    render(props)
  },
  bootstrap () {
    console.log('bootstrap')
  },
  unmount (props) {
    console.log('unmount', props)
  },
})
if (!qiankunWindow.__POWERED_BY_QIANKUN__) {
  render({})
}
// 挂载子应用实例
function render (props = {}) {
  const { container } = props
  const app = createApp(App)
  app.use(router)
  app.mount(container ? container.querySelector("#app") : "#app")
}

最终效果

在主应用中点击按钮会进行路由跳转,加载子应用,同时域名端口不会改变,对用户是无感的(用户以为只是页面间的跳转)

微前端项目部署

环境变量

项目中一个地方的值是可变的,在不同环境下可以有不同的值

环境变量的使用场景:

  • 接口请求基地址
  • 微前端子应用入口
  • 生产环境中移除控制台打印

image.png

在环境变量中配置不同的接口请求基地址:

# .env.development文件
# 开发环境 => npm run dev
ENV = 'development'
# 这里 http://localhost:7001 只是个示例,工作中要看后端同学电脑IP
VUE_APP_BASE_URL = 'http://localhost:7001'
# .env.production文件
# 生产环境 => npm run build
ENV = 'production'
# 这里写后端接口上线后的基地址
VUE_APP_BASE_URL= 'https://api-hmzs.itheima.net/api'

使用环境变量动态切换:

// utils/request.js
const service = axios.create({
  baseURL: process.env.VUE_APP_BASE_URL
})

在 src 目录下使用环境变量,只能识别键为 NODE_ENVBASE_URLVUE_APP_xxx 的环境变量。其中 VUE_APP_xxx 是自定义的环境变量

部署上线流程

前端要做的就是配合上线,把开发完毕的分支给到测试/运维人员

  • 主应用:根据环境变量切换接口基地址、子应用的入口地址
  • 子应用:配置静态资源路径为线上地址(vite.config.js > base选项)
  • 主应用和子应用分别打包、部署

使用PM2本地部署

pm2 可以启动一个本地服务,部署打包后的代码,模拟线上环境

切换到打包好的 dist 目录:

# 启动主应用
pm2 serve --spa ./ 8086 --name hmzs-admin
# 启动子应用
pm2 serve --spa ./ 8089 --name hmzs-screen

其他pm2命令:

pm2 list # 查看已启动的本地服务列表
pm2 serve --spa ./ 8086 --name <name> #在 localhost:8086 启动一个静态服务器,将当前目录作为静态服务器根目录,端口为8086
pm2 stop <name> # 停止一个本地服务
pm2 kill # 释放资源,终止对文件夹的占用