Vue3 day01: https://www.processon.com/view/link/6246d3ec0e3e74078d609f46
项目背景、功能、技术栈、我负责的模块(心里面一定要准备碰到的问题以及解决方案)
编辑/添加,公告头部的封装,每个页面都会用到的面包屑...考虑过哪些技术点:传值和校验、插槽和作用域插槽、自定义事件
收集数据 => 前端进行校验 => 通过 Axios 提交到后端 => 后端校验成功后返回 Token 到前端 => 前端拿到 Token 后做了 2 件事件(存储到 Vuex 并持久化到本地)
有权限的接口会通过请求拦截器给请求头统一携带 Token。
界面访问控制:在路由全局前置导航守卫通过判断 Token 去实现的。
Token:过期...
Web APP、可以安装的应用程序(Android、IOS、Android/IOS 提供 Webview + 套(网页))、uni-app 可以把网页打包成可以安装的应用程序
Gitlab 公司内部私有部署的代码管理平台。
master => 打包上线
release => 测试分支
develop => 开发分支(普通开发者具有权限的分支)
功能分支 => 一般我在开发新功能的时候,会基于 develop 开一个自己的功能分支,在上面写代码,写完之后再合并到 develop
首先我们会划分三个模块:utils/request.js
、api/*
、组件.vue
一般封装 axios 的时候会封装:首先创建一个 axios 实例、baseURL、timeout、请求拦截器(统一携带 Token、Token 过期时候的前端主动介入)、响应拦截器(成功的时候对数据进行脱壳、失败的时候统一错误提示、Token 过期处理)、transformResponse
Token 过期时候的前端主动介入:登录成功存一个时间戳、请求拦截器中,用当前时间戳 - 登录成功时候的那个时间戳,如果大于超时时间了,就直接拦截到登录页。
问题:在那做,做什么?
请求拦截器里面开启进度条,响应拦截器关闭进度条。
对外的,一般都要考虑 SEO,像商城网站(SSR、Nuxt.js)。
对内的,不需要,像后台管理系统。
内容分发网络:公司花钱 => 七牛 CDN => 买空间 => 把自己的资源上传到此空间 => 得到 CDN
v-for 循环的时候加 key
v-for 和 v-if 不要放一行(计算属性)
v-if 和 v-show 区分使用场景
路由懒加载、组件动态加载
KeepAlive
computed 和 方法区分使用场景(优先 computed)
...
轮询或 Websocket
SwaggerUI:一套接口生成的集成方案,是后端生成接口文档的框架,一般后端会部署一个地址,提供给我。
Vue2 有 2 种情况修改数据不是响应式的:给对象不存在的 key 进行赋值;通过索引修改数组的内容;
const obj = {}
obj.age = 18
const arr = ['a', 'b', 'c']
arr[0] = 'd'
给对象不存在的 key 进行赋值不是响应式的为什么?
由于 Vue2 是通过 Object.defineProperty 递归劫持的 data 里面的每一个【属性】,一上来就进行了递归劫持的操作,所以后续添加的属性当然就没有被劫持到!
通过索引修改数组的内容为什么不是响应式的?
性能!issue
// const obj = {}
// obj.age = 18
// 解决
this.$set(this.obj, 'age', 19)
Vue.set(this.obj, 'age', 19)
// const arr = ['a', 'b', 'c']
// arr[0] = 'd'
// 解决
this.$set(this.arr, 0, 888)
// 使用数组的变更方法
this.arr.splice(0, 1, 888)
Vue3 对数组通过索引修改就是响应式的,因为 Proxy 对数组的处理不存在性能问题!
Vue3 给对象不存在的 key 进行赋值也是响应式的啦,因为 Proxy 它劫持的直接就是整个对象(而不是一个个的属性),所以就无所谓你这个属性是不是后续添加的啦!
宿主环境不一样(网页=>浏览器,小程序=>微信软件)
开发方式不一样(微信开发者工具)
上线流程不一样
准备一个 <input type="file" hidden>
并通过 hidden 属性隐藏
准备一个按钮,给按钮绑定点击事件,在事件的回调里面主动触发 input:file 的 click 事件
监听 input 的 onchange 事件,在回调里面通过 e.target.files 拿到文件信息
创建 formData 对象,把文件信息放进去通过 axios 传到后端
const form = new FormData()
form.append('avatar', e.target.files[0])
<input type="file" hidden id="file" />
<img id="img" alt="" />
<button id="btn">click</button>
<script>
btn.onclick = function () {
file.click()
}
file.onchange = function (e) {
const f = e.target.files[0]
const formData = new FormData()
formData.append('avatar', f)
// axios({ url: '/xxx', data: formData })
/* const s = URL.createObjectURL(f)
img.src = s */
const fileReader = new FileReader()
// 图片 => base64
fileReader.readAsDataURL(f)
fileReader.onload = function () {
img.src = this.result
}
}
</script>
Mock: 模拟数据;拦截请求;
// mock/index.js
Mock.mock('/api/users', 'get', (req, res) => {
// 通过 req 拿到前端的信息
// 根据此信息返回对应的数据(Mock 的数据)
res.send({ mock 的数据 })
})
// main.js
import './mock'
1、父传子的第一种方法
父组件通过自定义属性传递,子组件通过 props 接收?
接收到的数据可以改吗?简单数据类型不能改,复杂数据类型引用不能改内容可以改,但是即便如此,也不建议直接改,为什么?
单项数据流思想:如果任何一个地方都可以修改数据,应用复杂出现错误的时候,意味着这个错误可能是任何一个地方导致的,不方便追溯!
怎么处理比较好?数据在哪来的就在哪改!
2、父传子的第二种方式
父亲通过 ref 获取子组件实例,调用此实例的方法的同时并传递参数,儿子方法中接收到参数做对应的修改!
3、父传子的第三种方式
this.$children[0].changeAge(this.age)
4、父传子的第四种方式
// 非 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 或 tiny-emitter 或者自己实现一个。
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 分两方面:配置项;触发流程;
mutation 里面可以放异步吗?
非严格模式确实是可以放异步,代码也可以正常执行!严格模式下不能这样操作(写异步),会有警告。
不建议放异步代码,目的是为了形成数据快照(拿到当时的那个数据状态),为了配合 DevTools 调试。
用户登录成功之后,后端返回当前用户的标识;['/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。
function isFn(flag) {
return ['a', 'b', 'c', 'DELETE'].includes(flag)
}
在需要做按钮权限控制的时候,调用这个方法,根据返回的是 true 还是 false,对这个按钮做禁用/启用或显示/隐藏的操作。
<button v-if="isFn('DELETE')">删除</button>
// #1 Call 式继承 / 构造函数继承:继承的是属性
Person.call(this, name, age)
// #2 原型继承:继承的是方法
Star.prototype = new Person()
Star.prototype.constructor = Star
// 组合继承 = 构造函数继承 + 原型继承
不过,现在呢,我更喜欢使用 ES6 的 extends...
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
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()
// ..........JSON.stringify 的确点............
// 1. 会忽略值为 undefined、函数、Symbol
// 2. 当值为 bigint 或循环引用的时候会报错
// 3. 日期会被转成字符串、正则会被转成空对象
// 4. ...
它们都是用来进行浅拷贝的,好的地方是能拷贝 undefined、函数、Symbol、正则...
// 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
// }
// 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
// };
const o = _.cloneDeep(obj1)
// 是什么?
// 防抖和节流都是性能优化的一种手段。
// 防抖:持续触发不执行,不触发的一段时间后才执行。
// 节流:持续触发也执行,只不过,执行的频率变低了。
// 实现一个?
// 匈牙利命名法的简版
// 类型 + 具体的含义
// 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__
链接起来的这种关系就是原型链。
.catch() 之后还能再触发 then 吗?
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)
})
如何实现并发请求,按顺序拿到结果?
Promise.all([p1, p2]).then((r) => {
// r => [p1 的结果, p2 的结果]
})
// const p1 的结果 = await p1
// const p2 的结果 = await p2
// 需求:双向数据绑定(数据变了,视图改变;视图改变,数据也变)
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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<p id="oP"></p>
<input id="oInput" type="text" />
<script>
// 需求:双向数据绑定(数据变了,视图改变;视图改变,数据也变)
const data = {
name: 'ifer',
address: '北京'
}
/* const tempData = { ...data }
Object.keys(data).forEach(key => {
Object.defineProperty(data, key, {
get() {
return tempData[key]
},
set(newValue) {
tempData[key] = newValue
oP.innerHTML = newValue
oInput.value = newValue
}
})
})
oInput.oninput = function (e) {
data.address = e.target.value
} */
// 上面 Vue2 的问题:如果 data 里面有 100 个属性,那可不是要循环 100 次吗?
// p 就是一个代理,我对 p 的任何操作都会影响到 data
const p = new Proxy(data, {
get(target, attr) {
// 获取的时候触发
// console.log(target, attr)
// target => data
// attr => 属性
return target[attr]
},
set(target, attr, newValue) {
target[attr] = newValue
oP.innerHTML = newValue
oInput.value = newValue
}
})
// p.name = 'xxx'
// console.log(data.name);
/* p.name = 'xxx'
console.log(p.name) */
oInput.oninput = function (e) {
// p.address = e.target.value
p.test = e.target.value
}
</script>
</body>
</html>
Vue.use({
install(Vue) {
// 干了哪些事情
Vue.mixin()
Vue.component()
Vue.directive()
Vue.prototype.bus = new Vue()
}
})
Vue.use(function (Vue) {})
点击 用户 1 按钮 和 用户 2 按钮 共用了同一个路由组件 User,我在 User 里面根据 userId 发请求的时候,发现永远是第一次发起的那个请求,好奇怪,当时找不到原因。
template 里面获取数据是实时更新的,但是 created 拿不到最新的数据,后来发现原因是公用了同一个组件,而 created 这个钩子也只会在组件创建完毕执行一次,所以 ...
解决方式 1
<template>
<div>User {{ $route.params.id }}</div>
</template>
<script>
export default {
name: 'User',
created() {
console.log(this.$route.params.id)
},
watch: {
$route(newRoute) {
console.log(newRoute.params.id)
}
}
}
</script>
解决方式 2
<router-view :key="$route.fullPath"></router-view>
根据文章 ID 请求文章详情的时候,有的时候正常,有的时候会出现 404,找不到详情,反复检查了传参和接口都没有问题,百思不得其解...
原因,后端给我返回的文章列表,当文章 ID 包含大数字的时候出事了。
{"name":"xxx", "age": 18}
本质上来说,后端返回的数据都是 JSON 格式的字符串,那么前端使用为什么可以直接当做对象去用呢?
就是因为 axios 帮我们进行了内部处理,它为了方便我们前端使用,内部进行了 JSON.parse 的操作转成了对象。
但是!!!当 JSON 格式的字符串里面包含了大数字的时候,JSON.parse 就搞不定了(转换出来的结果不对)
所以也就是当后端返回的 JSON 格式的字符串里面包含大数字的时候,axios 进行内部 JSON.parse 的时候,把那个大数字(文章 ID)转换成了另外一个结果,所以 404...
JSON.parse('{ "ID": 9007199254740999 }')
怎么解决?2 种方法。
找后端,那个 ID 不要用数字表示,用字符串表示文章 ID。
JSON.parse('{ "ID": "90071992547409998888888888888888" }')
自己搞定
import jsonBigInt from 'json-bigint'
// transformResponse
axios.create({
transformResponse(data) {
data => JSON 格式的字符串
// 默认
// return JSON.parse(data)
// 内部用一种算法,如果是大数字转出来的是一个对象,这个对象里面的信息经过运算可以得到大数字
return jsonBigInt.parse(data)
}
})
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。