项目总结
问题梳理
为什么要将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 获取数据的请求,保证请求完成后再执行后面的代码去渲染页面
访问空对象深层属性报错问题
由于请求是异步的,页面首次渲染时,可能后台数据还没返回,导致读取 undefined 的深层属性报错
解决办法:
1. `parkInfo.base?.buildingTotal` 可选链操作符
2. `parkInfo.base && parkInfo.base.buildingTotal` 逻辑与操作符
等到后台返回数据,响应式状态发生变化后,Vue会再次渲染
模块导入导出错误
这个错误一般是模块名、路径拼写错误导致的
重点概念
什么是Saas云服务
SaaS(Software-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插件注释增强
// *红色注释
// ?红色注释
// !红色注释
// TODO 待办事项
RBAC权限控制
什么是RBAC
# RBAC(Role Based Access Control)
- 基于角色的权限控制方案 `用户--角色--权限点`
- 包含 `菜单权限控制(页面访问控制)` 和 `按钮权限控制(按钮的显示与隐藏)` 两部分
- 权限标识组成:`一级菜单:二级菜单:功能` 如 `sys:role:add_edit`
- 实际上,后台接口调用也有权限控制(根据请求头中的 Authorization 属性)
菜单权限控制
不同角色的员工进入到系统中看到的左侧菜单是不一样的,根据不同的员工登录控制显示与之对应的左侧菜单
# 实现步骤
在路由前置守卫中:
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")
}
最终效果
在主应用中点击按钮会进行路由跳转,加载子应用,同时域名端口不会改变,对用户是无感的(用户以为只是页面间的跳转)
微前端项目部署
环境变量
项目中一个地方的值是可变的,在不同环境下可以有不同的值
环境变量的使用场景:
- 接口请求基地址
- 微前端子应用入口
- 生产环境中移除控制台打印
在环境变量中配置不同的接口请求基地址:
# .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_ENV、BASE_URL和VUE_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 # 释放资源,终止对文件夹的占用