# vue3_71 **Repository Path**: ifercarly/v3_71 ## Basic Information - **Project Name**: vue3_71 - **Description**: Vue3 71 期上课笔记 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 11 - **Forks**: 1 - **Created**: 2022-04-01 - **Last Updated**: 2023-01-05 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README ## 0. 脑图链接 Vue3 day01: https://www.processon.com/view/link/6246d3ec0e3e74078d609f46 ## 1. 如何介绍一个项目 项目背景、功能、技术栈、我负责的模块(心里面一定要准备碰到的问题以及解决方案) ## 2. 如何封装组件的 编辑/添加,公告头部的封装,每个页面都会用到的面包屑...考虑过哪些技术点:传值和校验、插槽和作用域插槽、自定义事件 ## 3. 登录怎么做的 收集数据 => 前端进行校验 => 通过 Axios 提交到后端 => 后端校验成功后返回 Token 到前端 => 前端拿到 Token 后做了 2 件事件(存储到 Vuex 并持久化到本地) 有权限的接口会通过请求拦截器给请求头统一携带 Token。 界面访问控制:在路由全局前置导航守卫通过判断 Token 去实现的。 Token:过期... ## 4. APP Web APP、可以安装的应用程序(Android、IOS、Android/IOS 提供 Webview + 套(网页))、uni-app 可以把网页打包成可以安装的应用程序 ## 5. Git 工作流 **Gitlab** 公司内部私有部署的代码管理平台。 master => 打包上线 release => 测试分支 develop => 开发分支(普通开发者具有权限的分支) 功能分支 => 一般我在开发新功能的时候,会基于 develop 开一个自己的功能分支,在上面写代码,写完之后再合并到 develop ## 6. 这个项目接口请求是怎么做的/你对 axios 封装过哪些东西 首先我们会划分三个模块:`utils/request.js`、`api/*`、`组件.vue` 一般封装 axios 的时候会封装:首先创建一个 axios 实例、baseURL、timeout、请求拦截器(统一携带 Token、Token 过期时候的前端主动介入)、响应拦截器(成功的时候对数据进行脱壳、失败的时候统一错误提示、Token 过期处理)、transformResponse Token 过期时候的前端主动介入:登录成功存一个时间戳、请求拦截器中,用当前时间戳 - 登录成功时候的那个时间戳,如果大于超时时间了,就直接拦截到登录页。 ## 7. 每次发请求的时候,希望全局开启一个进度条,请求完毕关闭进度条 问题:在那做,做什么? 请求拦截器里面开启进度条,响应拦截器关闭进度条。 ## 8. SEO 对外的,一般都要考虑 SEO,像商城网站(SSR、Nuxt.js)。 对内的,不需要,像后台管理系统。 ## 9. CDN 内容分发网络:公司花钱 => 七牛 CDN => 买空间 => 把自己的资源上传到此空间 => 得到 CDN ## 10. 和 Vue 相关的性能优化的手段都考虑过哪些 - v-for 循环的时候加 key - v-for 和 v-if 不要放一行(计算属性) - v-if 和 v-show 区分使用场景 - 路由懒加载、组件动态加载 - KeepAlive - computed 和 方法区分使用场景(优先 computed) - ... ## 11. Websocket 轮询或 Websocket ## 12. 你们公司接口文档是怎么交付的 SwaggerUI:一套接口生成的集成方案,是后端生成接口文档的框架,一般后端会部署一个地址,提供给我。 ## 13. 开发项目的时候,有没有碰到过数据明明变了,但是视图没有变,这种情况? - 什么情况? Vue2 有 2 种情况修改数据不是响应式的:给对象不存在的 key 进行赋值;通过索引修改数组的内容; ``` const obj = {} obj.age = 18 ``` ``` const arr = ['a', 'b', 'c'] arr[0] = 'd' ``` - 为什么? 给对象不存在的 key 进行赋值不是响应式的为什么? 由于 Vue2 是通过 Object.defineProperty 递归劫持的 data 里面的每一个【属性】,一上来就进行了递归劫持的操作,所以后续添加的属性当然就没有被劫持到! 通过索引修改数组的内容为什么不是响应式的? 性能![issue](https://github.com/vuejs/vue/issues/8562) - 怎么解决的? ```js // const obj = {} // obj.age = 18 // 解决 this.$set(this.obj, 'age', 19) Vue.set(this.obj, 'age', 19) ``` ```js // const arr = ['a', 'b', 'c'] // arr[0] = 'd' // 解决 this.$set(this.arr, 0, 888) // 使用数组的变更方法 this.arr.splice(0, 1, 888) ``` - Vue3 呢? Vue3 对数组通过索引修改就是响应式的,因为 Proxy 对数组的处理不存在性能问题! Vue3 给对象不存在的 key 进行赋值也是响应式的啦,因为 Proxy 它劫持的直接就是整个对象(而不是一个个的属性),所以就无所谓你这个属性是不是后续添加的啦! ## 14. 项目开发的流程是怎样的 ## 15. 小程序和网页开发的差异 - 宿主环境不一样(网页=>浏览器,小程序=>微信软件) - 开发方式不一样(微信开发者工具) - 上线流程不一样 ## 16. 上传图片怎么做的 1. 准备一个 `` 并通过 hidden 属性隐藏 2. 准备一个按钮,给按钮绑定点击事件,在事件的回调里面主动触发 input:file 的 click 事件 3. 监听 input 的 onchange 事件,在回调里面通过 e.target.files 拿到文件信息 4. 创建 formData 对象,把文件信息放进去通过 axios 传到后端 ```js const form = new FormData() form.append('avatar', e.target.files[0]) ``` 5. 图片预览:URL.createObjectURL(Blob) 或 FileReader(base64) ```html ``` ## 17. 后端接口还没有开发好,作为前端你是怎么进行下去的 Mock: 模拟数据;拦截请求; ```js // mock/index.js Mock.mock('/api/users', 'get', (req, res) => { // 通过 req 拿到前端的信息 // 根据此信息返回对应的数据(Mock 的数据) res.send({ mock 的数据 }) }) ``` ```js // main.js import './mock' ``` ## 18. 组件传值 ### 父传子 1、父传子的第一种方法 父组件通过自定义属性传递,子组件通过 props 接收? 接收到的数据可以改吗?简单数据类型不能改,复杂数据类型引用不能改内容可以改,但是即便如此,也不建议直接改,为什么? 单项数据流思想:如果任何一个地方都可以修改数据,应用复杂出现错误的时候,意味着这个错误可能是任何一个地方导致的,不方便追溯! 怎么处理比较好?数据在哪来的就在哪改! 2、父传子的第二种方式 父亲通过 ref 获取子组件实例,调用此实例的方法的同时并传递参数,儿子方法中接收到参数做对应的修改! 3、父传子的第三种方式 ```js this.$children[0].changeAge(this.age) ``` 4、父传子的第四种方式 ```js // 非 props 属性 this.$attrs ``` ### 子传父 1、儿子通过 $emit 触发父亲自定义事件的同时并传递数据,父亲监听自定义事件的同时在回调里面进行数据修改的操作 2、this.$parent 拿到父组件实例的同时,调用方法并传参 3、例如父传子,只不过传递的是一个方法,子组件调用这个方法的同时并传递参数 4、通过 this.$listeners 拿到父亲的自定义事件,调用并传参 ### 兄弟 1、状态(数据)提升 A 修改 B,把 B 中的数据提升到公共的父组件里面,A 通过**子传父**修改父亲的数据,父亲通过**父传子**传递把数据传递到 B 2、EventBus(事件中心、发布订阅) Vue3 中 EventBus 被废弃了,你觉得为什么? 太过于灵活,大型项目不太方便追溯问题;由于 EventBus 本身实现起来**足够简单**,Vue3 处于自身体积更小的考虑,所以被废弃了! 如果我还想再用怎么办?官方推荐了两个包,例如 [mitt](https://github.com/developit/mitt) 或 [tiny-emitter](https://github.com/scottcorgan/tiny-emitter) 或者**自己实现一个**。 ```js export default class EventBus { constructor() { this.subs = {} } // 订阅 => 订阅的是一个回调函数 on(eventType, callback) { this.subs[eventType] ? this.subs[eventType].push(callback) : (this.subs[eventType] = [callback]) } // 发布 => 发布事件让其(事件)对应的回到函数执行 emit(eventType, ...args) { this.subs[eventType].forEach((callback) => callback(...args)) } } ``` ### 全局的数据通信和共享(Vuex) 回答 Vuex 分两方面:配置项;触发流程; mutation 里面可以放异步吗? 非严格模式确实是可以放异步,代码也可以正常执行!严格模式下不能这样操作(写异步),会有警告。 不建议放异步代码,目的是为了形成数据快照(拿到当时的那个数据状态),为了配合 DevTools 调试。 ## 19. 说一下权限管理怎么做的 ### 路由级别的权限 用户登录成功之后,后端返回当前用户的标识;`['/user', '/news']` 前端拿到标识之后筛选出【有权限的路由】;`[{path: '/user', component: User}, {path: '/news', component: News}, ],#1` 然后做了 2 件事情! 第 1 件事情:通过 addRoutes/addRoute 把【有权限的路由】添加到路由实例;`router.addRoutes(#1)`,这样当前用户就具有了访问某个路由的权限啦; 第 2 件事情:把【有权限的路由】也添加到了 Vuex 一份,目的是为了给侧边栏或其他地方使用(通过 addRoutes 后续添加的路由,不能直接通过 router 获取到,官方推荐放到 Vuex 存储); ### 按钮级别的权限 封装一个全局的方法/指令,这个方法呢?只做一件事件,接收一个标识,内部进行判断,看一下这个标识这不在后端返回的功能列表里面,在就返回 true,不再就返回 false。 ```js function isFn(flag) { return ['a', 'b', 'c', 'DELETE'].includes(flag) } ``` 在需要做按钮权限控制的时候,调用这个方法,根据返回的是 true 还是 false,对这个按钮做禁用/启用或显示/隐藏的操作。 ```html ``` ## 20. 继承 ```js // #1 Call 式继承 / 构造函数继承:继承的是属性 Person.call(this, name, age) // #2 原型继承:继承的是方法 Star.prototype = new Person() Star.prototype.constructor = Star // 组合继承 = 构造函数继承 + 原型继承 ``` 不过,现在呢,我更喜欢使用 ES6 的 extends... ```js class Person { constructor(name, age) { this.name = name this.age = age } say() { console.log('!!!') } } class Star extends Person {} const s = new Star('尼古拉斯', 40) console.log(s.name, s.age) s.say() ``` extends ```js class Person { constructor(name, age) { this.name = name this.age = age } say() { console.log('!!!') } } class Star extends Person { // 儿子有 constructor 一定要有 super,有 constructor 的目的是为了添加 Star 自己的属性 constructor(name, age, money) { super(name, age) this.money = money } } const s = new Star('尼古拉斯', 40, 1000) console.log(s.name, s.age, s.money) s.say() ``` ## 深拷贝 1. JSON.stringify 的问题 ```js // ..........JSON.stringify 的确点............ // 1. 会忽略值为 undefined、函数、Symbol // 2. 当值为 bigint 或循环引用的时候会报错 // 3. 日期会被转成字符串、正则会被转成空对象 // 4. ... ``` 2. 三个点和 Object.assign 都是浅拷贝 ```js 它们都是用来进行浅拷贝的,好的地方是能拷贝 undefined、函数、Symbol、正则... ``` 3. 自己实现深拷贝:递归浅拷贝 ```js // const copy = (target) => { // const type = Object.prototype.toString.call(target) // // 正则、日期 // if (/(regexp|date)/i.test(type)) return new target.constructor(target) // // 错误对象 // if (/error/i.test(type)) return new target.constructor(target.message) // // 函数 // if (/function/i.test(type)) return new Function('return ' + target.toString())() // // null 和 简单数据类型 // if (target === null || typeof target !== 'object') return target // // 数组和对象 // /* const arr = [] // const obj = {} */ // const result = new target.constructor() // for (const attr in target) { // result[attr] = copy(target[attr]) // } // return result // } ``` 4. 如何循环引用 ```js // const copy = (target, m = new Map()) => { // const type = Object.prototype.toString.call(target) // // 正则、日期 // if (/(regexp|date)/i.test(type)) return new target.constructor(target) // // 错误对象 // if (/error/i.test(type)) return new target.constructor(target.message) // // 函数 // if (/function/i.test(type)) return new Function('return ' + target.toString())() // // null 和 简单数据类型 // if (target === null || typeof target !== 'object') return target // // 数组和对象 // /* const arr = [] // const obj = {} */ // // #2 m 里面存储了 target 就直接返回 // // console.log(target, 233) // if (m.get(target)) return m.get(target) // const result = new target.constructor() // // #3 // m.set(target, result) // for (const attr in target) { // // #4 传递 m // result[attr] = copy(target[attr], m) // } // return result // }; ``` 5. 实际我怎么做的 ```js const o = _.cloneDeep(obj1) ``` ## 防抖和节流 ```js // 是什么? // 防抖和节流都是性能优化的一种手段。 // 防抖:持续触发不执行,不触发的一段时间后才执行。 // 节流:持续触发也执行,只不过,执行的频率变低了。 // 实现一个? // 匈牙利命名法的简版 // 类型 + 具体的含义 // const iNum = 8 // const bBar = false // const aDiv = document.querySelctorAll('div') // 把鼠标相对于盒子位置放到盒子里面 /* oDiv.onmousemove = function (e) { let x = e.pageX - this.offsetLeft let t = e.pageY - this.offsetTop this.innerHTML = `x: ${x}, y: ${t}` }; */ // 防抖一下 /* let timer = null oDiv.onmousemove = function (e) { clearTimeout(timer) timer = setTimeout(() => { let x = e.pageX - this.offsetLeft let t = e.pageY - this.offsetTop this.innerHTML = `x: ${x}, y: ${t}` }, 200) }; */ // 封装一个防抖/节流函数? /* const debounce = (callback, time) => { let timer = null return function (e) { clearTimeout(timer) timer = setTimeout(() => { callback.call(this, e) // window.callback(e) }, time) } } */ /* oDiv.onmousemove = _.debounce(function (e) { let x = e.pageX - this.offsetLeft let t = e.pageY - this.offsetTop this.innerHTML = `x: ${x}, y: ${t}` }, 200) */ // 生活中的例子? // 王者荣耀英雄回城是防抖还是节流 // 打散弹枪,节流 // 应用场景? // 根据输入的内容请求接口?防抖 // 获取窗口缩放的大小,滚动位置?节流 // 实际开发怎么做? oDiv.onmousemove = _.throttle(function (e) { let x = e.pageX - this.offsetLeft let t = e.pageY - this.offsetTop this.innerHTML = `x: ${x}, y: ${t}` }, 1000) ``` ## 原型链 定义:多个对象之间通过 `__proto__` 链接起来的这种关系就是原型链。 ## Promise `.catch() 之后还能再触发 then 吗?` ```js const p = new Promise((resolve, reject) => { reject(new Error('233')) }) p.catch((r) => { console.log(r) // return Promise.resolve(undefined) }).then((r) => { console.log(r) }) ``` 如何实现并发请求,按顺序拿到结果? ```js Promise.all([p1, p2]).then((r) => { // r => [p1 的结果, p2 的结果] }) // const p1 的结果 = await p1 // const p2 的结果 = await p2 ``` ## Vue2 双向数据绑定原理 ```js // 需求:双向数据绑定(数据变了,视图改变;视图改变,数据也变) const data = { name: 'ifer' } /* const tempData = { ...data } Object.defineProperty(data, 'name', { get() { // 获取 name 的时候会走这儿 return tempData.name }, set(newValue) { // 设置 name 的时候会走这儿 tempData.name = newValue } }) // console.log(data.name) // 'ifer' data.name = 'elser' console.log(data.name) */ /* const tempData = { ...data } Object.defineProperty(data, 'name', { get() { // 获取 name 的时候会走这儿 return tempData.name }, set(newValue) { // model => view => Data bindings // #2 设置 name 的时候会走这儿 tempData.name = newValue oP.innerHTML = newValue oInput.value = newValue } }) // view => model => DOM Listeners oInput.oninput = function (e) { // #1 data.name = e.target.value } */ /* oInput.oninput = function (e) { oP.innerHTML = e.target.value } */ ``` Vue3 ```html Document

``` ## 插件 ```js Vue.use({ install(Vue) { // 干了哪些事情 Vue.mixin() Vue.component() Vue.directive() Vue.prototype.bus = new Vue() } }) Vue.use(function (Vue) {}) ``` ## 难点 1. 路由的缓存问题。 点击 用户 1 按钮 和 用户 2 按钮 共用了同一个路由组件 User,我在 User 里面根据 userId 发请求的时候,发现永远是第一次发起的那个请求,好奇怪,当时找不到原因。 template 里面获取数据是实时更新的,但是 created 拿不到最新的数据,后来发现原因是公用了同一个组件,而 created 这个钩子也只会在组件创建完毕执行一次,所以 ... 解决方式 1 ```html ``` 解决方式 2 ```html ``` 2. 大数字问题。 根据文章 ID 请求文章详情的时候,有的时候正常,有的时候会出现 404,找不到详情,反复检查了传参和接口都没有问题,百思不得其解... 原因,后端给我返回的文章列表,当文章 ID 包含大数字的时候出事了。 `{"name":"xxx", "age": 18}` 本质上来说,后端返回的数据都是 JSON 格式的字符串,那么前端使用为什么可以直接当做对象去用呢? 就是因为 axios 帮我们进行了内部处理,它为了方便我们前端使用,内部进行了 JSON.parse 的操作转成了对象。 但是!!!当 JSON 格式的字符串里面包含了大数字的时候,JSON.parse 就搞不定了(转换出来的结果不对) 所以也就是当后端返回的 JSON 格式的字符串里面包含大数字的时候,axios 进行内部 JSON.parse 的时候,把那个大数字(文章 ID)转换成了另外一个结果,所以 404... ```json JSON.parse('{ "ID": 9007199254740999 }') ``` 怎么解决?2 种方法。 找后端,那个 ID 不要用数字表示,用字符串表示文章 ID。 ```json JSON.parse('{ "ID": "90071992547409998888888888888888" }') ``` 自己搞定 ```json import jsonBigInt from 'json-bigint' // transformResponse axios.create({ transformResponse(data) { data => JSON 格式的字符串 // 默认 // return JSON.parse(data) // 内部用一种算法,如果是大数字转出来的是一个对象,这个对象里面的信息经过运算可以得到大数字 return jsonBigInt.parse(data) } }) ```