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思想做一个计算器

  1. //Model层
  2. data = {
  3. a: 1,
  4. b: 2,
  5. add,
  6. result
  7. },
  8. //监听data数据
  9. watch -> data change -> updata view
  1. //View层
  2. template模板 -> render渲染
  1. //Controller层
  2. event trigger事件触发 -> model data更改数据

controller控制层 操作 -> model模型层 操作 -> view视图层

view视图层 -> 通过 controller操作 -> model模型层

  1. //MVC.js
  2. (function () {
  3. function init() {
  4. //组织数据/数据监听操作/数据代理
  5. model.init();
  6. //组织HTML模板/渲染html模板
  7. view.render();
  8. //事件处理函数定义与绑定
  9. controller.init();
  10. }
  11. //管理视图所需要的数据,数据与视图的关联
  12. var model = {
  13. data: {
  14. a: 0,
  15. b: 0,
  16. s: '+',
  17. res: 0
  18. },
  19. //做一个代理,劫持数据并监听数据
  20. init: function () {
  21. var _this = this;
  22. for (var k in _this.data) {
  23. (function (k) {
  24. //重新定义每个属性
  25. //希望model能直接访问底下的属性
  26. Object.defineProperty(_this, k, {
  27. get: function () {
  28. //访问model.a -> get
  29. return _this.data[k];
  30. },
  31. set: function (newValue) {
  32. //model.a = 123; -> set
  33. //更改数据
  34. _this.data[k] = newValue;
  35. //每次数据更改都会触发渲染
  36. view.render({
  37. [k]: newValue
  38. });
  39. }
  40. });
  41. })(k);
  42. }
  43. }
  44. }
  45. //管理HTML模板和视图渲染
  46. var view = {
  47. el: '#app',
  48. template: `
  49. <p>
  50. <span class="cal-a">{{a}}</span>
  51. <span class="cal-s">{{s}}</span>
  52. <span class="cal-b">{{b}}</span>
  53. <span> = </span>
  54. <span class="cal-res">{{res}}</span>
  55. </p>
  56. <p>
  57. <input type="text" placeholder="Number a" class="cal-input a"/>
  58. <input type="text" placeholder="Number b" class="cal-input b"/>
  59. </p>
  60. <p>
  61. <button class="cal-btn">+</button>
  62. <button class="cal-btn">-</button>
  63. <button class="cal-btn">*</button>
  64. <button class="cal-btn">/</button>
  65. </p>
  66. `,
  67. //参数:mutedData变化的data
  68. render: function (mutedData) {
  69. //情况1:没有对象初始化渲染模板到页面显示
  70. if (!mutedData) {
  71. //解析template,替换{{}}里面的内容
  72. this.template = this.template.replace(/{{(.*?)}}/g, function (node, key) {
  73. return model[key.trim()];
  74. })
  75. // console.log(this.template);
  76. //打印出替换好的template模板
  77. //页面挂载
  78. var container = document.createElement('div');
  79. container.innerHTML = this.template;
  80. document.querySelector(this.el).appendChild(container);
  81. } else {
  82. //情况2:有对象更新
  83. for (var k in mutedData) {
  84. //class="cal-a" -> class="cal-k"
  85. //textContent返回指定节点的文本内容
  86. document.querySelector('.cal-' + k).textContent = mutedData[k];
  87. }
  88. }
  89. }
  90. }
  91. //数据和视图关联挂载和基本的逻辑操作
  92. var controller = {
  93. init: function () {
  94. //选出所以input并绑定事件
  95. var
  96. oCalInputs = document.querySelectorAll('.cal-input'),
  97. oCalBtns = document.querySelectorAll('.cal-btn'),
  98. inputItem,
  99. btnItem;
  100. for (var i = 0; i < oCalInputs.length; i++) {
  101. inputItem = oCalInputs[i];
  102. inputItem.addEventListener('input', this.handleInput, false);
  103. }
  104. for (var i = 0; i < oCalBtns.length; i++) {
  105. btnItem = oCalBtns[i];
  106. btnItem.addEventListener('click', this.handleBtnClick, false);
  107. }
  108. },
  109. //输入框文本更改事件处理函数
  110. handleInput: function (e) {
  111. var
  112. tar = e.target,
  113. value = Number(tar.value),
  114. //拿到类名 class="cal-input a" 的input
  115. field = tar.className.split(' ')[1];
  116. //将input输入的值赋值给model数据里的a变量
  117. model[field] = value;
  118. //计算公式 1 + 1 = 2
  119. model.res = eval('model.a' + model.s + 'model.b');
  120. },
  121. //点击加减乘除按钮事件处理函数
  122. handleBtnClick: function (e) {
  123. var
  124. type = e.target.textContent;
  125. //更改页面显示的运算符号
  126. model.s = type;
  127. //计算
  128. with(model) {
  129. res = eval('a' + s + 'b');
  130. }
  131. }
  132. }
  133. init();
  134. })()

总结:

代码看出 MVCMVVM的雏形,MVVM解决了驱动不集中,不内聚的方式,更加解决了视图与模型之间完全隔离开来的一种关系

从而演变成 MVVM的形式,将 ViewModel隔离出来,剩下 M data 和 V view供开发者使用,更加说明vue `是只关注于视图渲染

ViewModel里有收集依赖,模板编译,数据劫持等重要方法

MVVM

3.进阶 - 图1

MVVM 实现

  1. //MVC -> 驱动被MVC分离成三部分
  2. //跟M V 逻辑混合在一起了
  3. //MVVM -> 驱动ViewModel
  4. //M -> Model 数据保存和处理的层
  5. //V -> View 视图

目录结构:

  1. ├─index.html
  2. ├─package-lock.json
  3. ├─package.json
  4. ├─src
  5. | App.js
  6. ├─MVVM
  7. | ├─index.js - ViewModel入口文件
  8. | ├─render.js - 负责渲染与更新
  9. | ├─shared
  10. | | utils.js - 工具函数集合
  11. | ├─reactive - 负责负责响应式数据
  12. | | ├─index.js - 创建响应式数据
  13. | | mutableHandler.js - 负责代理/劫持响应式数据
  14. | ├─compiler - 负责编译模板
  15. | | ├─event.js - 负责编译事件字符串并且绑定事件处理函数
  16. | | state.js - 负责视图变量的替换,打上标识补丁,变成一个有标识的节点结构

源码地址:

https://gitee.com/kevinleeeee/vue-mvvm-demo


案例:实现 MVVM

Mini Vue 的组成部分:

  • observe监听器:数据劫持
  • reactive实现响应式:属性代理
  • Dep依赖管理器:负责将视图中所有依赖收集管理,包括依赖添加和通知更新
  • watcher监听器:具体更新的执行者,将模板编译和数据劫持联系起来
  • Compile编译器:扫描模板中所有依赖(指令,插值,绑定,事件等),创建更新函数和监听器(watcher)

技术:es6

功能:

模板编译/数据劫持/观察者 watcher关联数据和视图/发布订阅模式

实现步骤:

  1. 模板编译 Compile
  2. 数据劫持 Observer
  3. 监听器 Watcher
  4. 发布订阅Observer > Dep

源码地址:

https://gitee.com/kevinleeeee/vue2.x-mvvm-demo

数据劫持

铺垫是数组变更检测方案

  1. var vm = {
  2. data:{
  3. a: 1,
  4. b: 2,
  5. list: [1,2,3,4,5]
  6. }
  7. }
  8. for(var key in vm.data){
  9. (function(key){
  10. Object.defineProperty(vm, key, {
  11. get(){
  12. console.log('数据获取');
  13. return vm.data[key];
  14. },
  15. set(newValue){
  16. console.log('数据设置');
  17. vm.data[key] = newValue;
  18. //视图渲染 失败
  19. //然而push等方法操作没有办法让程序走到这里
  20. }
  21. });
  22. })(key);
  23. }
  24. //缺点:
  25. 1.set()方法并没有执行
  26. 2.Object.defineProperty()没有办法监听下列方法对数组的操作变更 push/pop/shift/splice/sort/reverse 等方法不返回新的数组

基于以上缺点,Vue对以上方法进行包裹封装一层,重写一遍方法,但是操作方法没有变化

  1. function push(){
  2. vm.list.push(6);
  3. //视图更新
  4. }

数据劫持实现:

基于 vue2.x 版本的数据劫持实现

技术:

JavaScript ES5/webpack/数据响应式/模板编译

希望在数据变化时增加额外的视图变化的代码

利用数据劫持给对象和数组属性新增 getter/setter 方法

源码地址:

https://gitee.com/kevinleeeee/data-hijacked-vue2.x-demo

props

单项数据流是一种组件化中数据流向的规范,从父组件流动向子组件

遵循规范:子组件不可改变父组件流入的数据

问题:为什么不可改变父传子流入的数据?

如果子组件去更改数据,数据属于父组件定义的,父组件的属性会受影响(引用)

  1. //这里传递的是字符串
  2. <mt-test num="1"></my-teset>
  3. //这里传递的是表达式
  4. //v-bind实现传递各种数据类型string/boolean/array/object...
  5. <mt-test :num="1"></my-teset>

子组件注册属性:

  1. //简单注册写法:
  2. //存在弊端:没有办法验证传过来的属性是否符合当前组件的要求的类型
  3. props:['num','arr','obj',...]

问题:如何解决要求类型的属性注册?

用对象的方式进行注册属性并定义类型

  1. //对象的方式进行属性注册
  2. //props接收的数据类型有:null/undefined/object/number/array/function/promise
  3. props: {
  4. num: Number,
  5. arr: Array,
  6. obj: Object,
  7. test: Function,
  8. xxx: 构造函数,
  9. p: Promise,
  10. ...
  11. }

props验证

遇到什么样的验证用什么样的方法

null/undefined

子组件接收null/undefined 可以通过任何的数据类型检测,因为有些不明确的数据是后端传递过来的,有可能是 null/undefined

联合类型

有可能是字符串,也有可能是数值

  1. props: {
  2. status: [Number, String]
  3. }

必填属性

在必要的选属性时加 require

  1. props: {
  2. article: {
  3. type: String,
  4. require: true
  5. }
  6. }

默认值

当父组件没有传入具体的数据是子组件定义一个默认值

  1. props: {
  2. article: {
  3. type: String,
  4. default: 'xxx'
  5. }
  6. }

当定义的默认值是对象时必须返回一个工厂函数

  1. props: {
  2. article: {
  3. type: String,
  4. default(){
  5. //必须导出一个新的引用
  6. return {
  7. title: 'This is my DEFAULT_TITLE',
  8. content: 'This is my DEFAULT_CONTENT'
  9. }
  10. }
  11. }
  12. }

自定义验证函数

  1. //vue提供一个验证函数
  2. props: {
  3. btnType: {
  4. //验证是否含有指定字段
  5. validator(value){
  6. return [
  7. 'primary',
  8. 'warning',
  9. 'success',
  10. 'danger'
  11. ].includes(value);
  12. }
  13. }
  14. }

注意:组件实例 propsdefaultvalidator函数里定义的属性是不能被 data/methods 所访问的,因为 prop验证是在当前组件实例创建之前工作的,不是在创建之后/挂载之后才做的

attributes

vue文档定义是非 propsattribute

  1. //id和class均为div标签里的属性
  2. <div id="app" class="xxx"></div>

问题:如何利用在组件上传到 attributes让子组件能够获取并使用他们?

解决方案 1:

利用事件传递机制(较为复杂)

  1. //子组件
  2. <select @change="changeOption">
  3. <option value="1">选项1</option>
  4. <option value="2">选项2</option>
  5. <option value="3">选项3</option>
  6. </select>
  7. export default {
  8. name:'MySelector'
  9. data(){
  10. return {
  11. selectorValue: this.value
  12. }
  13. },
  14. methods:{
  15. changeOption(){
  16. this.$emit('changeOption', this.selectorValue);
  17. }
  18. }
  19. }
  1. //父组件
  2. <my-selector @change-option="changeOption"></my-selector>
  3. export default {
  4. name: 'App',
  5. data(){
  6. return {
  7. selectorValue: '3'
  8. }
  9. },
  10. methods:{
  11. //通过子组件事件传到过来的value参数
  12. changeOption(value){
  13. //console.log(value);
  14. }
  15. }
  16. }

解决方案 2:

propsattribute

  1. //子组件
  2. <select>
  3. <option value="1">选项1</option>
  4. <option value="2">选项2</option>
  5. <option value="3">选项3</option>
  6. </select>
  7. export default {
  8. name:'MySelector'
  9. create(){
  10. console.log(this.$attrs);
  11. //Proxy{ model: '123' }
  12. }
  13. }
  1. //父组件
  2. //在组件上进行传递属性
  3. //单个的根元素,使用组件时传递的所有属性,都会增加到根元素上
  4. <my-selector model="123"></my-selector>
  5. export default {
  6. name: 'App',
  7. data(){
  8. return {
  9. selectorValue: '3'
  10. }
  11. },
  12. }

继承是可以被禁用的

  1. export default {
  2. name:'MySelector',
  3. inheritAttrs: false,
  4. create(){
  5. console.log(this.$attrs);
  6. //Proxy{ model: '123' }
  7. }
  8. }

mixin

混入,跟普通的组件区别并不大,相当于一个公共的组件,在组件化中引入到组件内部就可以访问组件里面的属性和方法和视图

其实是一个组件类高度复用的工具

vuex

3.进阶 - 图2

Vuex工作流:

  • 组件 -> dispatch-> action

    • dispatch-> type(actionType) -> 某一个 action
  • action-> comit-> mutation
  • mutation-> change-> state
  • render方案: state-> 数据流 -> 视图

文件目录:

  • actionTypes: action类型
  • actions: 调用 mutation的方法
  • mutations: 更改 state的方法
  • state: 中央数据管理池
  • store出口:actions,mutation,state 统一到仓库里进行管理

配置:

  1. //"vuex": "^4.0.0-alpha.1"
  2. //入口文件
  3. import store from './store'
  1. //store > index.js
  2. import Vuex from 'vuex';
  3. import state from './states';
  4. import mutations from './mutations';
  5. export default Vuex.createStore({
  6. state,
  7. mutations
  8. });

使用:

  1. //获取仓库
  2. import {useStore} from 'vuex';
  3. export default {
  4. setup(){
  5. const store = useStore(),
  6. state = store.state;
  7. //注意:在vue2.x中是通过computed里 ...mapState(['xxx'])方法拿到里面的属性
  8. //1.所以这里不能直接访问state.xxx
  9. //2.所以需要用computed函数取出state里的属性
  10. //实际上返回的是state里面的对象
  11. return computed(() => state).value;
  12. }
  13. };

案例:TodoList

技术:

typescript/vue3.x/vuex/自定义 hooks

功能和组件:

  1. //组件划分:
  2. - TodoList
  3. - TodoInput -> 输入的组件
  4. - TodoList -> 列表组件
  5. - TodoItem -> 列表项
  6. 功能1checkbox - 完成或未完成的选择
  7. 功能2button - 删除该项的功能
  8. 功能3button - 正在做的确认按钮
  1. //项目目录
  2. ├─package.json
  3. ├─README.md
  4. ├─tsconfig.json
  5. ├─src
  6. | ├─App.vue - 页面挂载时加载列表
  7. | ├─main.ts
  8. | ├─typings
  9. | | index.ts - 管理类型接口的出口文件 interface
  10. | ├─store
  11. | | ├─actions.ts - commit调用mutation的方法
  12. | | ├─actionTypes.ts - 管理定义store里方法名称的变量
  13. | | ├─index.ts - 出口文件
  14. | | ├─mutations.ts - 管理操作state数组逻辑
  15. | | state.ts - 中央数据管理池
  16. | ├─hooks - 自定义hook 带有两个钩子/钩子底下各有些dispatch方法
  17. | | index.ts
  18. | ├─components
  19. | | ├─TodoList
  20. | | | ├─index.vue
  21. | | | Item.vue
  22. | | ├─TodoInput
  23. | | | index.vue

项目流程

  1. 列表展现页面:

    1. 输入框组件通过键盘事件拿到输入的文本内容
    2. 当有输出内容时执行自定义 hooks底下的钩子里setTodo方法
    3. setTodo方法dispatch传入组装好的带有输入框内容的对象
    4. actionType模块里定义大写字符串方法名称为大写变量
    5. 变量定义的actions名字的方法触发commit变量名称的事件,并传入组装后的对象
    6. mutations底下定义变量名称的方法里实现操作并保存对象到state.list数组里
    7. 在自定义 hooks 里的钩子定义setLocalList方法实现将state.list数组存入localStorage
    8. watch监听每当state.list数据有变化就调用 hooks里的钩子定义setLocalList方法并传入从getLocalList方法获取到的localStorage数组
    9. 在自定义 hooks 里的钩子定义setTodoList方法实现将localStorage里的数组新增合并至state.list数组里
    10. 在根组件onMounted页面挂载时执行setTodoList方法拿到state.list的数据
    11. 列表组件根据数据遍历并绑定视图实现页面展示列表
  2. 点击列表内容修改页面展示:

    1. item 子组件绑定点击事件emit传递并带有id参数
    2. list 组件点击事件关联钩子里定义的distpatch方法
    3. 通过mutations里定义的逻辑实现修改state.list里的属性
    4. 子组件绑定视图根据state.list里实时更改的数据展示变化

问题:在 typescript中,枚举的作用是什么?

在项目里,枚举非常常用,一个变量含有几个固定的几个值时,需要枚举出来,并枚举访问,枚举可以当类型也可取值

问题:在 typescript中,类型type和接口interface有什么区别?

都可以针对对象定义,type相对少用,因为interface是可以通过extends继承另外的接口,扩展性比较好,一般对对象的定义都是用interface

注意:

  • 所有接口命名都是以 ‘I’ 开头
  • 声明枚举一般大写
  1. //定义接口
  2. interface ITodo{
  3. id: number,
  4. content: string,
  5. status: TODO_STATUS
  6. }
  1. //声明枚举
  2. enum TODO_STATUS{
  3. WILLDO = 'willdo',
  4. DOING = 'doing',
  5. FINISHED = 'finished'
  6. }

问题:为什么在 actionTypes里不用字符串而是用变量?

  1. 为了不用维护字符串
  2. 变量能够很好的管理字符串,在调用方法的时候直接用变量去调用
  1. //actionTypes.ts
  2. //把字符串转为变量
  3. export const SET_TODO: string = 'SET_TODO';

问题:在 typescript里如何保存数据到 state里?

  1. 定义actionType把字符串转为变量

  2. mutations定义修改state的方法

  3. 自定义 hooks里的方法执行

  4. hooks 里定义的方法dispatch派发并传入数据到actions

  5. 通过actions调用commit派发到mutations

  6. mutations更改state

设计观:

TodoList是一个方法集合,增加/删除/展示列表/更改状态,实际上都在操作列表,应该自定义一个 hook API,来管理这些方法的解决方案,让 TodoList形成一个方案集合,导出一些方法,让其在各个组件都可以单独导入某些方法去执行相应的程序

  1. //src > hooks > index.ts
  2. function useTodo() {
  3. function setTodo() {}
  4. function setTodoList(){}
  5. function removeTodo(){}
  6. function setStatus(){}
  7. function setDoing(){}
  8. //返回导出解决方案
  9. return {
  10. //方案1:修改Todo,设置到列表里
  11. setTodo,
  12. //方案2:刷新页面列表读取loacalStorage里的todoList显示在页面的列表项里
  13. setTodoList,
  14. //方案3:删除
  15. removeTodo,
  16. //方案4:更改待办/未做的状态
  17. setStatus,
  18. //方案5:更改正在做的状态
  19. setDoing
  20. }
  21. }

问题:如何刷新页面(app组件挂载)拿到数据?

通过把 state里的数据存入localStorage,将从localStorage里读取的数组数据修改为state.list的数据,在根组件挂载时onMounted时执行读取state.list数据

补充: vue子组件props属性的类型注解可以使用PropTypeAPI 来断言,作为 typescript 的泛型

  1. props: {
  2. todoList: Array as PropType<ITodo[]>,
  3. },

源码地址:

https://gitee.com/kevinleeeee/vue3-typescript-todolist-vuex-demo


案例:tab 栏切换

技术:

  • vuex负责:兄弟组件之间的数据通讯
  • mixin负责:构建公共的 UI组件库
  • filter负责:给视图中数据绑定时再做文字加工
  • directives负责:给所有的项清除类/给点选的 nav项添加类

自定义指令:

大量的指令方便代码阅读,易于维护

mixins: 利用公共代码全局注册可以用公共 UI组件

  1. //项目结构
  2. ├─src
  3. | ├─App.vue
  4. | ├─main.js
  5. | ├─store
  6. | | ├─index.js
  7. | | ├─mutation.js
  8. | | state.js
  9. | ├─router
  10. | | index.js
  11. | ├─pages
  12. | | Index.vue
  13. | ├─libs
  14. | | ├─myUI
  15. | | | ├─index.js
  16. | | | ├─NavBar
  17. | | | | ├─index.vue
  18. | | | | Item.vue
  19. | | | ├─directives
  20. | | | | tabCurrent.js
  21. | ├─filters
  22. | | ├─index.js
  23. | | replaceNumToChs.js
  24. | ├─directives
  25. | | ├─index.js
  26. | | tabCurrent.js
  27. | ├─data
  28. | | ├─content.js
  29. | | nav.js
  30. | ├─components
  31. | | ├─Tab
  32. | | | ├─index.vue
  33. | | | ├─Nav
  34. | | | ├─Content
  35. | | | | index.vue
  36. ├─public
  37. | index.html

源码地址:

https://gitee.com/kevinleeeee/vuex-filter-mixin-tab-demo


vuex机制:

只有 mutation里的方法操作 state里面的属性

  1. //state是一个对象
  2. {
  3. bool: false
  4. }
  1. //mutation装载以下方法
  2. {
  3. setBool(state, bool){
  4. state.bool = bool;
  5. }
  6. }
  1. //注册使用
  2. Vue.use(Vuex);
  3. //实例化Vuex里的Store实例
  4. export default new Vuex.Store({
  5. state: {
  6. bool: false
  7. },
  8. mutations: {
  9. setBool(state, bool){
  10. state.bool = bool;
  11. }
  12. }
  13. });

getter可以去 state里的属性的时候可以加工一下

  1. export default new Vuex.Store({
  2. ...,
  3. getters: {
  4. getMyInfo(state){
  5. return `我的名字是${state.name}, 今年${state.age}岁`;
  6. }
  7. }
  8. });
  9. //执行调用
  10. this.myInfo = this.$store.getter.getMyInfo;

vuex中央状态管理器

  • state仓库 放数据

  • action行为 事件

  • mutation处理方法

  • getter加工数据

  • modules处理大型项目

3.进阶 - 图3

流程闭环

  1. component(dispatch form backend API)
  2. -> actions(commit)
  3. -> mutation(mutate)
  4. -> state
  5. -> render component(dispatch)
  6. -> actions...

actions如果更改数据的时候涉及到异步时,必须使用actions

  1. //Vuecomponent(dispatch) -> actions
  2. export default new Vuex.Store({
  3. ...,
  4. actions: {
  5. getData(ctx, payload){
  6. const { key, testType, model, subject } = payload;
  7. axios(...).then(res => console.log(res));
  8. }
  9. }
  10. });
  11. //组件里执行调用
  12. //this.$store.dispatch(调用的函数, 函数参数集合);
  13. this.$store.dispatch('getData', {...});
  1. //actions(commit) -> mutations
  2. //缓存dispatch后的数据
  3. export default new Vuex.Store({
  4. ...,
  5. state: {
  6. data: []
  7. },
  8. mutations: {
  9. setData(state, data){
  10. state.data = data;
  11. }
  12. },
  13. actions: {
  14. getData(ctx, payload){
  15. const { key, testType, model, subject } = payload;
  16. //执行ctx底下的commit,传入mutation执行的方法和后端拿到的数据
  17. axios(...).then(res => {
  18. //将后端数据存入data里
  19. console.log(ctx.commit('setData', res.data.result));
  20. });
  21. }
  22. }
  23. });

modules

  1. //模块1 store > count1 > index.js
  2. export default {
  3. //开启命名空间
  4. namespaced: true
  5. state,
  6. mutations
  7. }
  8. //模块2 store > count2 > index.js
  9. export default {
  10. //开启命名空间
  11. namespaced: true
  12. state,
  13. mutations
  14. }
  1. //store > index.js
  2. export default new Vuex.Store({
  3. ...,
  4. //大型项目分模块
  5. //模拟两个状态
  6. modules: {
  7. count1,
  8. count2
  9. }
  10. });

案例:小米购物车移动端

技术:

vue2.x/vuex/请求数据函数封装

功能:

  • 同步localStorage数据到state数据里
  • 手机列表渲染
  • 设置购物车总价和总数量
  • 设置购物车列表数据,增减商品的数量和价格同步关联localStorage
  • 根据localStorage数据渲染购物车列表
  • 购物车加减计算器组件
  1. //项目目录:
  2. ├─vue.config.js - 配置代理解决跨域
  3. ├─src
  4. | ├─App.vue - 根组件挂载路由占位
  5. | ├─main.js - 入口文件
  6. | ├─views
  7. | | ├─Cart.vue - 购物车页面
  8. | | Home.vue - 首页页面
  9. | ├─utils
  10. | | ├─config.js - 配置请求地址API
  11. | | ├─https.js - 封装axiosGet函数
  12. | | tools.js - 工具:数据格式化/同步localStorage函数
  13. | ├─store
  14. | | ├─actions.js - 管理commit方法和payload
  15. | | ├─index.js
  16. | | ├─mutations.js - 管理逻辑方法操作state数据对其增删改
  17. | | state.js - 中央状态池
  18. | ├─services
  19. | | ├─index.js - getData函数请求回来的数据进行再加工
  20. | | request.js - 封装getData函数可以自定义参数修改url
  21. | ├─router
  22. | | index.js
  23. | ├─components
  24. | | ├─TotalPanel - 购物车页面底下的总价组件
  25. | | | index.vue - 绑定视图/组件更新时同步localStorage
  26. | | ├─PhoneList - 手机列表组件
  27. | | | ├─index.vue - 遍历子项/拿到state数据并传子组件
  28. | | | Item.vue - 绑定视图/点击事件/派发任务到mutations
  29. | | ├─Header 公共的头部组件
  30. | | | ├─BackIcon.vue - 后退组件
  31. | | | ├─CartIcon.vue - 购物车(0) 组件/组件更新时同步localStorage
  32. | | | index.vue
  33. | | ├─CartList 购物车列表组件
  34. | | | ├─index.vue - 遍历子项/拿到state数据并传子组件
  35. | | | ├─Item.vue - 绑定视图/导入加减计算器组件
  36. | | | ├─Calculator - 加减计算器组件
  37. | | | | index.vue - 绑定视图/点击事件/派发任务到mutations

问题:为什么要设置购物车总价和总数量?

子组件的单击加入购物车按钮会dispacthmutations里操作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 交互方式也跟数据密不可分的,而数据库最紧密接触的是后端程序,在前后端分离开发时,越来越多的项目也需要在前端进行权限控制

关于后端的权限设计有:

用户/角色/权限

问题:前端的权限控制有什么好处?

  • 降低非法操作的可能性
  • 减少不必要的请求,减轻服务器压力
  • 提高用户体验

问题:前端的权限控制有哪些?

视图层的展示和前端所发送的请求

问题:前端的权限控制解决方案有哪些?

  1. 菜单的控制:如侧边栏,根据请求到的数据展示对应的菜单,点击菜单查看相关的界面
  2. 界面的控制:

    1. 如用户没有登录,手动输入地址栏时自动跳回登录页面
    2. 如用户以及登录,输入非权限的地址则跳转到 404 页面
  3. 按钮的控制:根据用户的权限,是否显示或隐藏该按钮以及对按钮功能的限制
  4. 请求和响应的控制:对没有权限的用户在进行一些非法操作时不提交数据请求减轻服务器的压力

问题:如何动态添加路由规则?

  1. 后端返回用户对应路由权限列表
  2. 将路由权限列表转为路由规则的树形化格式
  3. 插入路由规则到路由配置里

问题:给路由规则添加自定义的原数据有什么作用?

对路由规则的属性进行拓展属性名称和属性值实现对用户权限底下具体可以实现哪些操作

问题:如何给当前组件的路由规则添加自定义的属性数据?

  1. //给路由规则新增原数据
  2. const userRule = { path: '/users', component: Users, 自定义属性: xxx }
  3. //当前组件的路由规则
  4. //console.log(router.currentRoute);
  5. {
  6. name: undefined,
  7. path: '/users',
  8. hash: '',
  9. query: {},
  10. params: {},
  11. fullPath: '/users',
  12. matched: [...],
  13. 自定义的属性: 'xxx'
  14. }
  1. 通过自定义指令v-mydir="{action:'add'}"
  2. 通过binding.value拿到自己定义在视图上的值{action: 'add'}
  3. 在动态添加路由规则的同时给路由规则新增一个原数据属性
  4. 判断用户是否具备action的权限

    1. 拿到当前组件路由的新增属性router.currentRoute.xxx
    2. 判断router.currentRoute.xxx.indexOf(action) == -1底下是否含有跟自定义指令定义的action
    3. 如果找不到就修改el如删除节点不显示点击添加按钮

案例:后台路由权限管理

技术:

koa2/vue/前后端

案例图片:

3.进阶 - 图4

原理:

  1. 用户 uid -> 后端 API -> 路由权限 API
  2. 后端 -> 用户对应路由权限列表 -> 前端 -> JSON
  3. JSON -> 树型结构化
  4. 树型结构化的数据 -> vue路由结构
  5. 路由结构动态 -> 静态路由
  6. 根据树型结构化的数据 -> 菜单组件
  1. //JSON
  2. [
  3. {
  4. id: 2,
  5. //parent id
  6. pid: 3,
  7. path:
  8. name:
  9. link:
  10. title:
  11. }
  12. ]

问题:如何做后端跨域?

  1. npm i koa2-cors -S
  1. //app.js
  2. const cors = require('koa2-cors');
  3. app.use(cors({
  4. origin: function (ctx) {
  5. return 'http://localhost:8080'
  6. }
  7. }));

前端项目顺序:

  1. 编写后台界面
  2. 请求后端接口
  3. 封装请求函数
  4. 将请求到的数据存入 vuex
  5. actions里异步获取后端数据
  6. 将数据进行树型结构化格式化
  7. mutations里定义方法存储state
  8. 前端动态生成路由
  9. 将数据生成树状结构路由配置对象
  10. 配置路由守卫beforeEach

    1. 没有权限时:

      1. 请求后端数据
      2. 格式化后的树形结构的路由规则对象数组
      3. addroute新增到路由列表实现动态添加路由
      4. 编写各个地址的组件文件
      5. next回调分支处理实现访问不同地址显示不同的页面
    2. 有权限时:

      1. 直接访问不做守卫拦截
  11. 编写 SideBar组件视图根据路由渲染路由列表
  12. 实现点击路由导航名称跳转到路由页面显示路由组件
  1. //注册一个全局前置守卫
  2. const router = new VueRouter({ ... })
  3. router.beforeEach((to, from, next) => {
  4. // 注意:确保 next 函数在任何给定的导航守卫中都被严格调用一次。它可以出现多于一次,但是只能在所有的逻辑路径都不重叠的情况下,否则钩子永远都不会被解析或报错。
  5. })

后端项目顺序:

  1. 编写用户表(包含权限的数组容器)
  2. 编写路由数据表
  3. 遍历用户表和路由数据表
  4. 将某个用户符合条件的权限路由对象放入容器返回给前端
  1. //前端项目目录:
  2. ├─package.json
  3. ├─src
  4. | ├─App.vue - 根组件/管理布局组件
  5. | ├─main.js - 入口文件/请求路由数据/动态生成路由列表/拼接路由列表/路由守卫
  6. | ├─views - 各路由视图组件
  7. | | ├─Course.vue
  8. | | ├─CourseAdd.vue
  9. | | ├─CourseInfoData.vue
  10. | | ├─CourseOperate.vue
  11. | | ├─Home.vue
  12. | | ├─NotFound.vue
  13. | | ├─Student.vue
  14. | | ├─StudentAdd.vue
  15. | | StudentOperate.vue
  16. | ├─store
  17. | | ├─actions.js - 定义请求路由列表数据函数/权限函数
  18. | | ├─index.js
  19. | | ├─mutations.js - 定义操作state的方法
  20. | | state.js - 中央状态管理池/hasAuth/userRouters数组
  21. | ├─services
  22. | | index.js - axios函数封装
  23. | ├─router
  24. | | index.js - 路由入口文件
  25. | ├─libs
  26. | | utils.js - 格式化路由列表为树形结构化/生成树状结构路由配置对象
  27. | ├─components - 页面布局组件
  28. | | ├─Header.vue
  29. | | ├─MenuItem.vue
  30. | | ├─PageBoard.vue
  31. | | SideBar.vue
  32. | ├─assets
  33. | | ├─css
  34. | | | common.css

前端源码地址:

https://gitee.com/kevinleeeee/vue-router-admin-frondend-demo

后端源码地址:

https://gitee.com/kevinleeeee/vue-router-admin-backend-demo


vue3路由用法:

设置

  1. //入口文件
  2. import router from './router'
  3. createApp(App)
  4. .use(router)
  5. .use(store)
  6. .mount('#app')
  1. //"vue-router": "^4.0.0-alpha.6",
  2. import {
  3. createRouter,
  4. createWebHistory
  5. } from 'vue-router';
  6. const routes = [{
  7. path: '/',
  8. name: 'day',
  9. component: DayPage
  10. },
  11. {
  12. path: '/month',
  13. name: 'month',
  14. //动态导入页面组件
  15. component: () => import(
  16. '../views/Month.vue'
  17. )
  18. },
  19. ...
  20. ]
  21. const router = createRouter({
  22. history: createWebHistory(process.env.BASE_URL),
  23. routes
  24. })
  25. export default router;

访问路由里面的属性和方法可以用useRouter

  1. import { useRouter } from 'vue-router';
  2. const router = useRouter();
  3. router.push('/');

性能优化

方法一

v-for key通过设置 key 值,更快定位数据与 diff

流程:

  1. 用户操作数据
  2. 派发通知
  3. 打补丁(vnode)

方法二

模块化组件化

  • 封装具有高度复用性的模块
  • 拆分高度复用性的组件
  • 组件可配置性强

方法三

路由懒加载

  • 首屏加快渲染

方法四

productionSourceMap

  • false
  • 生成 map文件,定位源码

方法五

productionGzip

  • true
  • 启用 gzip压缩功能,打包体积更小

方法六

keep-alive

缓存组件

方法七

插件用 cdn加载

方法八

图片 cdn,图片懒加载,使用 css图标

  • 图片使用远程 CDN地址
  • 图标使用 CSS图标

方法九

组件按需导入