Vue 进阶
MVC
关于后端 MVC:
M:Model数据模型(模型层),操作数据库(增删改查)V:View视图层,显示视图或视图模板C:Controller控制器层(逻辑层),数据和视图关联挂载和基本的逻辑操作
服务端渲染:
视图 View需要数据去找 Controller对应的方法,调用 Model的方法,获取数据,返回给 Contronller对应的方法,渲染 render到视图 View中
前端渲染:
API层,前端请求的 API对应的是控制器中的方法,前端异步请求 URL对应控制器中的方法,调用 Model层的方法,操作数据库,然后获取数据返回给控制器方法,控制器方法响应回前端
关于前端 MVC:
Model:需要管理视图所需要的数据,数据与视图的关联View:管理 HTML 模板和视图渲染Controller:管理事件逻辑
MVC 实现
案例:用前端 MVC思想做一个计算器
//Model层data = {a: 1,b: 2,add,result},//监听data数据watch -> data change -> updata view
//View层template模板 -> render渲染
//Controller层event trigger事件触发 -> model data更改数据
controller控制层 操作 -> model模型层 操作 -> view视图层
view视图层 -> 通过 controller操作 -> model模型层
//MVC.js(function () {function init() {//组织数据/数据监听操作/数据代理model.init();//组织HTML模板/渲染html模板view.render();//事件处理函数定义与绑定controller.init();}//管理视图所需要的数据,数据与视图的关联var model = {data: {a: 0,b: 0,s: '+',res: 0},//做一个代理,劫持数据并监听数据init: function () {var _this = this;for (var k in _this.data) {(function (k) {//重新定义每个属性//希望model能直接访问底下的属性Object.defineProperty(_this, k, {get: function () {//访问model.a -> getreturn _this.data[k];},set: function (newValue) {//model.a = 123; -> set//更改数据_this.data[k] = newValue;//每次数据更改都会触发渲染view.render({[k]: newValue});}});})(k);}}}//管理HTML模板和视图渲染var view = {el: '#app',template: `<p><span class="cal-a">{{a}}</span><span class="cal-s">{{s}}</span><span class="cal-b">{{b}}</span><span> = </span><span class="cal-res">{{res}}</span></p><p><input type="text" placeholder="Number a" class="cal-input a"/><input type="text" placeholder="Number b" class="cal-input b"/></p><p><button class="cal-btn">+</button><button class="cal-btn">-</button><button class="cal-btn">*</button><button class="cal-btn">/</button></p>`,//参数:mutedData变化的datarender: function (mutedData) {//情况1:没有对象初始化渲染模板到页面显示if (!mutedData) {//解析template,替换{{}}里面的内容this.template = this.template.replace(/{{(.*?)}}/g, function (node, key) {return model[key.trim()];})// console.log(this.template);//打印出替换好的template模板//页面挂载var container = document.createElement('div');container.innerHTML = this.template;document.querySelector(this.el).appendChild(container);} else {//情况2:有对象更新for (var k in mutedData) {//class="cal-a" -> class="cal-k"//textContent返回指定节点的文本内容document.querySelector('.cal-' + k).textContent = mutedData[k];}}}}//数据和视图关联挂载和基本的逻辑操作var controller = {init: function () {//选出所以input并绑定事件varoCalInputs = document.querySelectorAll('.cal-input'),oCalBtns = document.querySelectorAll('.cal-btn'),inputItem,btnItem;for (var i = 0; i < oCalInputs.length; i++) {inputItem = oCalInputs[i];inputItem.addEventListener('input', this.handleInput, false);}for (var i = 0; i < oCalBtns.length; i++) {btnItem = oCalBtns[i];btnItem.addEventListener('click', this.handleBtnClick, false);}},//输入框文本更改事件处理函数handleInput: function (e) {vartar = e.target,value = Number(tar.value),//拿到类名 class="cal-input a" 的inputfield = tar.className.split(' ')[1];//将input输入的值赋值给model数据里的a变量model[field] = value;//计算公式 1 + 1 = 2model.res = eval('model.a' + model.s + 'model.b');},//点击加减乘除按钮事件处理函数handleBtnClick: function (e) {vartype = e.target.textContent;//更改页面显示的运算符号model.s = type;//计算with(model) {res = eval('a' + s + 'b');}}}init();})()
总结:
代码看出 MVC是 MVVM的雏形,MVVM解决了驱动不集中,不内聚的方式,更加解决了视图与模型之间完全隔离开来的一种关系
从而演变成 MVVM的形式,将 ViewModel隔离出来,剩下 M data 和 V view供开发者使用,更加说明vue `是只关注于视图渲染
ViewModel里有收集依赖,模板编译,数据劫持等重要方法
MVVM

MVVM 实现
//MVC -> 驱动被MVC分离成三部分//跟M V 逻辑混合在一起了//MVVM -> 驱动ViewModel//M -> Model 数据保存和处理的层//V -> View 视图
目录结构:
├─index.html├─package-lock.json├─package.json├─src| └App.js├─MVVM| ├─index.js - ViewModel入口文件| ├─render.js - 负责渲染与更新| ├─shared| | └utils.js - 工具函数集合| ├─reactive - 负责负责响应式数据| | ├─index.js - 创建响应式数据| | └mutableHandler.js - 负责代理/劫持响应式数据| ├─compiler - 负责编译模板| | ├─event.js - 负责编译事件字符串并且绑定事件处理函数| | └state.js - 负责视图变量的替换,打上标识补丁,变成一个有标识的节点结构
源码地址:
https://gitee.com/kevinleeeee/vue-mvvm-demo
案例:实现 MVVM
Mini Vue 的组成部分:
observe监听器:数据劫持reactive实现响应式:属性代理Dep依赖管理器:负责将视图中所有依赖收集管理,包括依赖添加和通知更新watcher监听器:具体更新的执行者,将模板编译和数据劫持联系起来Compile编译器:扫描模板中所有依赖(指令,插值,绑定,事件等),创建更新函数和监听器(watcher)
技术:es6
功能:
模板编译/数据劫持/观察者 watcher关联数据和视图/发布订阅模式
实现步骤:
- 模板编译
Compile - 数据劫持
Observer - 监听器
Watcher - 发布订阅
Observer > Dep
源码地址:
https://gitee.com/kevinleeeee/vue2.x-mvvm-demo
数据劫持
铺垫是数组变更检测方案
var vm = {data:{a: 1,b: 2,list: [1,2,3,4,5]}}for(var key in vm.data){(function(key){Object.defineProperty(vm, key, {get(){console.log('数据获取');return vm.data[key];},set(newValue){console.log('数据设置');vm.data[key] = newValue;//视图渲染 失败//然而push等方法操作没有办法让程序走到这里}});})(key);}//缺点:1.set()方法并没有执行2.Object.defineProperty()没有办法监听下列方法对数组的操作变更 push/pop/shift/splice/sort/reverse 等方法不返回新的数组
基于以上缺点,Vue对以上方法进行包裹封装一层,重写一遍方法,但是操作方法没有变化
function push(){vm.list.push(6);//视图更新}
数据劫持实现:
基于 vue2.x 版本的数据劫持实现
技术:
JavaScript ES5/webpack/数据响应式/模板编译
希望在数据变化时增加额外的视图变化的代码
利用数据劫持给对象和数组属性新增 getter/setter 方法
源码地址:
https://gitee.com/kevinleeeee/data-hijacked-vue2.x-demo
props
单项数据流是一种组件化中数据流向的规范,从父组件流动向子组件
遵循规范:子组件不可改变父组件流入的数据
问题:为什么不可改变父传子流入的数据?
如果子组件去更改数据,数据属于父组件定义的,父组件的属性会受影响(引用)
//这里传递的是字符串<mt-test num="1"></my-teset>//这里传递的是表达式//v-bind实现传递各种数据类型string/boolean/array/object...<mt-test :num="1"></my-teset>
子组件注册属性:
//简单注册写法://存在弊端:没有办法验证传过来的属性是否符合当前组件的要求的类型props:['num','arr','obj',...]
问题:如何解决要求类型的属性注册?
用对象的方式进行注册属性并定义类型
//对象的方式进行属性注册//props接收的数据类型有:null/undefined/object/number/array/function/promiseprops: {num: Number,arr: Array,obj: Object,test: Function,xxx: 构造函数,p: Promise,...}
props验证
遇到什么样的验证用什么样的方法
null/undefined
子组件接收null/undefined 可以通过任何的数据类型检测,因为有些不明确的数据是后端传递过来的,有可能是 null/undefined
联合类型
有可能是字符串,也有可能是数值
props: {status: [Number, String]}
必填属性
在必要的选属性时加 require
props: {article: {type: String,require: true}}
默认值
当父组件没有传入具体的数据是子组件定义一个默认值
props: {article: {type: String,default: 'xxx'}}
当定义的默认值是对象时必须返回一个工厂函数
props: {article: {type: String,default(){//必须导出一个新的引用return {title: 'This is my DEFAULT_TITLE',content: 'This is my DEFAULT_CONTENT'}}}}
自定义验证函数
//vue提供一个验证函数props: {btnType: {//验证是否含有指定字段validator(value){return ['primary','warning','success','danger'].includes(value);}}}
注意:组件实例
props里default和validator函数里定义的属性是不能被data/methods所访问的,因为prop验证是在当前组件实例创建之前工作的,不是在创建之后/挂载之后才做的
attributes
vue文档定义是非 props的 attribute
//id和class均为div标签里的属性<div id="app" class="xxx"></div>
问题:如何利用在组件上传到 attributes让子组件能够获取并使用他们?
解决方案 1:
利用事件传递机制(较为复杂)
//子组件<select @change="changeOption"><option value="1">选项1</option><option value="2">选项2</option><option value="3">选项3</option></select>export default {name:'MySelector'data(){return {selectorValue: this.value}},methods:{changeOption(){this.$emit('changeOption', this.selectorValue);}}}
//父组件<my-selector @change-option="changeOption"></my-selector>export default {name: 'App',data(){return {selectorValue: '3'}},methods:{//通过子组件事件传到过来的value参数changeOption(value){//console.log(value);}}}
解决方案 2:
非 props的 attribute
//子组件<select><option value="1">选项1</option><option value="2">选项2</option><option value="3">选项3</option></select>export default {name:'MySelector'create(){console.log(this.$attrs);//Proxy{ model: '123' }}}
//父组件//在组件上进行传递属性//单个的根元素,使用组件时传递的所有属性,都会增加到根元素上<my-selector model="123"></my-selector>export default {name: 'App',data(){return {selectorValue: '3'}},}
继承是可以被禁用的
export default {name:'MySelector',inheritAttrs: false,create(){console.log(this.$attrs);//Proxy{ model: '123' }}}
mixin
混入,跟普通的组件区别并不大,相当于一个公共的组件,在组件化中引入到组件内部就可以访问组件里面的属性和方法和视图
其实是一个组件类高度复用的工具
vuex

Vuex工作流:
组件 ->
dispatch->actiondispatch->type(actionType)-> 某一个action
action->comit->mutationmutation->change->staterender方案:state-> 数据流 -> 视图
文件目录:
actionTypes:action类型actions: 调用mutation的方法mutations: 更改state的方法state: 中央数据管理池store出口:actions,mutation,state统一到仓库里进行管理
配置:
//"vuex": "^4.0.0-alpha.1"//入口文件import store from './store'
//store > index.jsimport Vuex from 'vuex';import state from './states';import mutations from './mutations';export default Vuex.createStore({state,mutations});
使用:
//获取仓库import {useStore} from 'vuex';export default {setup(){const store = useStore(),state = store.state;//注意:在vue2.x中是通过computed里 ...mapState(['xxx'])方法拿到里面的属性//1.所以这里不能直接访问state.xxx//2.所以需要用computed函数取出state里的属性//实际上返回的是state里面的对象return computed(() => state).value;}};
案例:TodoList
技术:
typescript/vue3.x/vuex/自定义 hooks
功能和组件:
//组件划分:- TodoList- TodoInput -> 输入的组件- TodoList -> 列表组件- TodoItem -> 列表项功能1:checkbox - 完成或未完成的选择功能2:button - 删除该项的功能功能3:button - 正在做的确认按钮
//项目目录├─package.json├─README.md├─tsconfig.json├─src| ├─App.vue - 页面挂载时加载列表| ├─main.ts| ├─typings| | └index.ts - 管理类型接口的出口文件 interface| ├─store| | ├─actions.ts - commit调用mutation的方法| | ├─actionTypes.ts - 管理定义store里方法名称的变量| | ├─index.ts - 出口文件| | ├─mutations.ts - 管理操作state数组逻辑| | └state.ts - 中央数据管理池| ├─hooks - 自定义hook 带有两个钩子/钩子底下各有些dispatch方法| | └index.ts| ├─components| | ├─TodoList| | | ├─index.vue| | | └Item.vue| | ├─TodoInput| | | └index.vue
项目流程:
列表展现页面:
- 输入框组件通过键盘事件拿到输入的文本内容
- 当有输出内容时执行自定义
hooks底下的钩子里setTodo方法 setTodo方法dispatch传入组装好的带有输入框内容的对象- 在
actionType模块里定义大写字符串方法名称为大写变量 - 变量定义的
actions名字的方法触发commit变量名称的事件,并传入组装后的对象 mutations底下定义变量名称的方法里实现操作并保存对象到state.list数组里- 在自定义 hooks 里的钩子定义
setLocalList方法实现将state.list数组存入localStorage里 watch监听每当state.list数据有变化就调用hooks里的钩子定义setLocalList方法并传入从getLocalList方法获取到的localStorage数组- 在自定义 hooks 里的钩子定义
setTodoList方法实现将localStorage里的数组新增合并至state.list数组里 - 在根组件
onMounted页面挂载时执行setTodoList方法拿到state.list的数据 - 列表组件根据数据遍历并绑定视图实现页面展示列表
点击列表内容修改页面展示:
- item 子组件绑定点击事件
emit传递并带有id参数 - list 组件点击事件关联钩子里定义的
distpatch方法 - 通过
mutations里定义的逻辑实现修改state.list里的属性 - 子组件绑定视图根据
state.list里实时更改的数据展示变化
- item 子组件绑定点击事件
问题:在 typescript中,枚举的作用是什么?
在项目里,枚举非常常用,一个变量含有几个固定的几个值时,需要枚举出来,并枚举访问,枚举可以当类型也可取值
问题:在 typescript中,类型type和接口interface有什么区别?
都可以针对对象定义,type相对少用,因为interface是可以通过extends继承另外的接口,扩展性比较好,一般对对象的定义都是用interface
注意:
- 所有接口命名都是以 ‘I’ 开头
- 声明枚举一般大写
//定义接口interface ITodo{id: number,content: string,status: TODO_STATUS}
//声明枚举enum TODO_STATUS{WILLDO = 'willdo',DOING = 'doing',FINISHED = 'finished'}
问题:为什么在 actionTypes里不用字符串而是用变量?
- 为了不用维护字符串
- 变量能够很好的管理字符串,在调用方法的时候直接用变量去调用
//actionTypes.ts//把字符串转为变量export const SET_TODO: string = 'SET_TODO';
问题:在 typescript里如何保存数据到 state里?
定义
actionType把字符串转为变量mutations定义修改state的方法自定义
hooks里的方法执行hooks 里定义的方法
dispatch派发并传入数据到actions通过
actions调用commit派发到mutationsmutations更改state
设计观:
TodoList是一个方法集合,增加/删除/展示列表/更改状态,实际上都在操作列表,应该自定义一个 hook API,来管理这些方法的解决方案,让 TodoList形成一个方案集合,导出一些方法,让其在各个组件都可以单独导入某些方法去执行相应的程序
//src > hooks > index.tsfunction useTodo() {function setTodo() {}function setTodoList(){}function removeTodo(){}function setStatus(){}function setDoing(){}//返回导出解决方案return {//方案1:修改Todo,设置到列表里setTodo,//方案2:刷新页面列表读取loacalStorage里的todoList显示在页面的列表项里setTodoList,//方案3:删除removeTodo,//方案4:更改待办/未做的状态setStatus,//方案5:更改正在做的状态setDoing}}
问题:如何刷新页面(app组件挂载)拿到数据?
通过把 state里的数据存入localStorage,将从localStorage里读取的数组数据修改为state.list的数据,在根组件挂载时onMounted时执行读取state.list数据
补充:
vue子组件props属性的类型注解可以使用PropTypeAPI 来断言,作为 typescript 的泛型
props: {todoList: Array as PropType<ITodo[]>,},
源码地址:
https://gitee.com/kevinleeeee/vue3-typescript-todolist-vuex-demo
案例:tab 栏切换
技术:
vuex负责:兄弟组件之间的数据通讯mixin负责:构建公共的UI组件库filter负责:给视图中数据绑定时再做文字加工directives负责:给所有的项清除类/给点选的nav项添加类
自定义指令:
大量的指令方便代码阅读,易于维护
mixins: 利用公共代码全局注册可以用公共 UI组件
//项目结构├─src| ├─App.vue| ├─main.js| ├─store| | ├─index.js| | ├─mutation.js| | └state.js| ├─router| | └index.js| ├─pages| | └Index.vue| ├─libs| | ├─myUI| | | ├─index.js| | | ├─NavBar| | | | ├─index.vue| | | | └Item.vue| | | ├─directives| | | | └tabCurrent.js| ├─filters| | ├─index.js| | └replaceNumToChs.js| ├─directives| | ├─index.js| | └tabCurrent.js| ├─data| | ├─content.js| | └nav.js| ├─components| | ├─Tab| | | ├─index.vue| | | ├─Nav| | | ├─Content| | | | └index.vue├─public| └index.html
源码地址:
https://gitee.com/kevinleeeee/vuex-filter-mixin-tab-demo
vuex机制:
只有 mutation里的方法操作 state里面的属性
//state是一个对象{bool: false}
//mutation装载以下方法{setBool(state, bool){state.bool = bool;}}
//注册使用Vue.use(Vuex);//实例化Vuex里的Store实例export default new Vuex.Store({state: {bool: false},mutations: {setBool(state, bool){state.bool = bool;}}});
getter可以去 state里的属性的时候可以加工一下
export default new Vuex.Store({...,getters: {getMyInfo(state){return `我的名字是${state.name}, 今年${state.age}岁`;}}});//执行调用this.myInfo = this.$store.getter.getMyInfo;
vuex中央状态管理器
state仓库 放数据action行为 事件mutation处理方法getter加工数据modules处理大型项目

流程闭环:
component(dispatch form backend API)-> actions(commit)-> mutation(mutate)-> state-> render component(dispatch)-> actions...
actions如果更改数据的时候涉及到异步时,必须使用actions
//Vuecomponent(dispatch) -> actionsexport default new Vuex.Store({...,actions: {getData(ctx, payload){const { key, testType, model, subject } = payload;axios(...).then(res => console.log(res));}}});//组件里执行调用//this.$store.dispatch(调用的函数, 函数参数集合);this.$store.dispatch('getData', {...});
//actions(commit) -> mutations//缓存dispatch后的数据export default new Vuex.Store({...,state: {data: []},mutations: {setData(state, data){state.data = data;}},actions: {getData(ctx, payload){const { key, testType, model, subject } = payload;//执行ctx底下的commit,传入mutation执行的方法和后端拿到的数据axios(...).then(res => {//将后端数据存入data里console.log(ctx.commit('setData', res.data.result));});}}});
modules
//模块1 store > count1 > index.jsexport default {//开启命名空间namespaced: true,state,mutations}//模块2 store > count2 > index.jsexport default {//开启命名空间namespaced: true,state,mutations}
//store > index.jsexport default new Vuex.Store({...,//大型项目分模块//模拟两个状态modules: {count1,count2}});
案例:小米购物车移动端
技术:
vue2.x/vuex/请求数据函数封装
功能:
- 同步
localStorage数据到state数据里 - 手机列表渲染
- 设置购物车总价和总数量
- 设置购物车列表数据,增减商品的数量和价格同步关联
localStorage - 根据
localStorage数据渲染购物车列表 - 购物车加减计算器组件
//项目目录:├─vue.config.js - 配置代理解决跨域├─src| ├─App.vue - 根组件挂载路由占位| ├─main.js - 入口文件| ├─views| | ├─Cart.vue - 购物车页面| | └Home.vue - 首页页面| ├─utils| | ├─config.js - 配置请求地址API| | ├─https.js - 封装axiosGet函数| | └tools.js - 工具:数据格式化/同步localStorage函数| ├─store| | ├─actions.js - 管理commit方法和payload| | ├─index.js| | ├─mutations.js - 管理逻辑方法操作state数据对其增删改| | └state.js - 中央状态池| ├─services| | ├─index.js - 将getData函数请求回来的数据进行再加工| | └request.js - 封装getData函数可以自定义参数修改url| ├─router| | └index.js| ├─components| | ├─TotalPanel - 购物车页面底下的总价组件| | | └index.vue - 绑定视图/组件更新时同步localStorage| | ├─PhoneList - 手机列表组件| | | ├─index.vue - 遍历子项/拿到state数据并传子组件| | | └Item.vue - 绑定视图/点击事件/派发任务到mutations| | ├─Header 公共的头部组件| | | ├─BackIcon.vue - 后退组件| | | ├─CartIcon.vue - 购物车(0) 组件/组件更新时同步localStorage| | | └index.vue| | ├─CartList 购物车列表组件| | | ├─index.vue - 遍历子项/拿到state数据并传子组件| | | ├─Item.vue - 绑定视图/导入加减计算器组件| | | ├─Calculator - 加减计算器组件| | | | └index.vue - 绑定视图/点击事件/派发任务到mutations
问题:为什么要设置购物车总价和总数量?
子组件的单击加入购物车按钮会dispacth到mutations里操作state.totalPrice实现价格和数量的累加或累减
问题:如何实现点击加入购物车时购物车页面显示相关的商品信息?
子组件的单击加入购物车按钮会将相应的商品信息加入到state.cartData数组里实现根据数组属性渲染列表
问题:当重复商品增加数量或减少数量时如何同步总数量和总价格数据?
找到点击当前商品索引index,给当前重复添加的商品state.cartData[idx]累加数量和价格
问题:为什么在 header 组件或购物车页面底部总价组件更新时同步localStorage?
因为点击按钮时会触发修改state数据,而 header 组件和总价组件刚好是根据state绑定了视图, 所以视图会随着state数据修改而实时渲染更新,通过updated生命钩子函数可以同步localStorage数据
源码地址:
https://gitee.com/kevinleeeee/vue-xiaomi-shoppingcart-mobile
路由
前端权限控制
问题:为什么前端也要进行权限控制?
web 交互方式也跟数据密不可分的,而数据库最紧密接触的是后端程序,在前后端分离开发时,越来越多的项目也需要在前端进行权限控制
关于后端的权限设计有:
用户/角色/权限
问题:前端的权限控制有什么好处?
- 降低非法操作的可能性
- 减少不必要的请求,减轻服务器压力
- 提高用户体验
问题:前端的权限控制有哪些?
视图层的展示和前端所发送的请求
问题:前端的权限控制解决方案有哪些?
- 菜单的控制:如侧边栏,根据请求到的数据展示对应的菜单,点击菜单查看相关的界面
界面的控制:
- 如用户没有登录,手动输入地址栏时自动跳回登录页面
- 如用户以及登录,输入非权限的地址则跳转到 404 页面
- 按钮的控制:根据用户的权限,是否显示或隐藏该按钮以及对按钮功能的限制
- 请求和响应的控制:对没有权限的用户在进行一些非法操作时不提交数据请求减轻服务器的压力
问题:如何动态添加路由规则?
- 后端返回用户对应路由权限列表
- 将路由权限列表转为路由规则的树形化格式
- 插入路由规则到路由配置里
问题:给路由规则添加自定义的原数据有什么作用?
对路由规则的属性进行拓展属性名称和属性值实现对用户权限底下具体可以实现哪些操作
问题:如何给当前组件的路由规则添加自定义的属性数据?
//给路由规则新增原数据const userRule = { path: '/users', component: Users, 自定义属性: xxx }//当前组件的路由规则//console.log(router.currentRoute);{name: undefined,path: '/users',hash: '',query: {},params: {},fullPath: '/users',matched: [...],自定义的属性: 'xxx'}
- 通过自定义指令
v-mydir="{action:'add'}" - 通过
binding.value拿到自己定义在视图上的值{action: 'add'} - 在动态添加路由规则的同时给路由规则新增一个原数据属性
判断用户是否具备
action的权限- 拿到当前组件路由的新增属性
router.currentRoute.xxx - 判断
router.currentRoute.xxx.indexOf(action) == -1底下是否含有跟自定义指令定义的action - 如果找不到就修改
el如删除节点不显示点击添加按钮
- 拿到当前组件路由的新增属性
案例:后台路由权限管理
技术:
koa2/vue/前后端
案例图片:

原理:
- 用户
uid-> 后端API-> 路由权限API - 后端 -> 用户对应路由权限列表 -> 前端 ->
JSON JSON-> 树型结构化- 树型结构化的数据 ->
vue路由结构 - 路由结构动态 -> 静态路由
- 根据树型结构化的数据 -> 菜单组件
//JSON[{id: 2,//parent idpid: 3,path:name:link:title:}]
问题:如何做后端跨域?
npm i koa2-cors -S
//app.jsconst cors = require('koa2-cors');app.use(cors({origin: function (ctx) {return 'http://localhost:8080'}}));
前端项目顺序:
- 编写后台界面
- 请求后端接口
- 封装请求函数
- 将请求到的数据存入
vuex actions里异步获取后端数据- 将数据进行树型结构化格式化
mutations里定义方法存储state- 前端动态生成路由
- 将数据生成树状结构路由配置对象
配置路由守卫
beforeEach没有权限时:
- 请求后端数据
- 格式化后的树形结构的路由规则对象数组
addroute新增到路由列表实现动态添加路由- 编写各个地址的组件文件
- 给
next回调分支处理实现访问不同地址显示不同的页面
有权限时:
- 直接访问不做守卫拦截
- 编写
SideBar组件视图根据路由渲染路由列表 - 实现点击路由导航名称跳转到路由页面显示路由组件
//注册一个全局前置守卫const router = new VueRouter({ ... })router.beforeEach((to, from, next) => {// 注意:确保 next 函数在任何给定的导航守卫中都被严格调用一次。它可以出现多于一次,但是只能在所有的逻辑路径都不重叠的情况下,否则钩子永远都不会被解析或报错。})
后端项目顺序:
- 编写用户表(包含权限的数组容器)
- 编写路由数据表
- 遍历用户表和路由数据表
- 将某个用户符合条件的权限路由对象放入容器返回给前端
//前端项目目录:├─package.json├─src| ├─App.vue - 根组件/管理布局组件| ├─main.js - 入口文件/请求路由数据/动态生成路由列表/拼接路由列表/路由守卫| ├─views - 各路由视图组件| | ├─Course.vue| | ├─CourseAdd.vue| | ├─CourseInfoData.vue| | ├─CourseOperate.vue| | ├─Home.vue| | ├─NotFound.vue| | ├─Student.vue| | ├─StudentAdd.vue| | └StudentOperate.vue| ├─store| | ├─actions.js - 定义请求路由列表数据函数/权限函数| | ├─index.js| | ├─mutations.js - 定义操作state的方法| | └state.js - 中央状态管理池/hasAuth/userRouters数组| ├─services| | └index.js - axios函数封装| ├─router| | └index.js - 路由入口文件| ├─libs| | └utils.js - 格式化路由列表为树形结构化/生成树状结构路由配置对象| ├─components - 页面布局组件| | ├─Header.vue| | ├─MenuItem.vue| | ├─PageBoard.vue| | └SideBar.vue| ├─assets| | ├─css| | | └common.css
前端源码地址:
https://gitee.com/kevinleeeee/vue-router-admin-frondend-demo
后端源码地址:
https://gitee.com/kevinleeeee/vue-router-admin-backend-demo
vue3路由用法:
设置
//入口文件import router from './router'createApp(App).use(router).use(store).mount('#app')
//"vue-router": "^4.0.0-alpha.6",import {createRouter,createWebHistory} from 'vue-router';const routes = [{path: '/',name: 'day',component: DayPage},{path: '/month',name: 'month',//动态导入页面组件component: () => import('../views/Month.vue')},...]const router = createRouter({history: createWebHistory(process.env.BASE_URL),routes})export default router;
访问路由里面的属性和方法可以用useRouter
import { useRouter } from 'vue-router';const router = useRouter();router.push('/');
性能优化
方法一
v-for key通过设置 key 值,更快定位数据与 diff
流程:
- 用户操作数据
- 派发通知
- 打补丁(
vnode)
方法二
模块化组件化
- 封装具有高度复用性的模块
- 拆分高度复用性的组件
- 组件可配置性强
方法三
路由懒加载
- 首屏加快渲染
方法四
productionSourceMap
false- 生成
map文件,定位源码
方法五
productionGzip
true- 启用
gzip压缩功能,打包体积更小
方法六
keep-alive
缓存组件
方法七
插件用 cdn加载
方法八
图片 cdn,图片懒加载,使用 css图标
- 图片使用远程
CDN地址 - 图标使用
CSS图标
方法九
组件按需导入
