- bug:禅道
- ui:蓝湖
- 接口:yapi
- 需求文档:confluence,文档组织,富文本编辑,
- 第三方错误管理:sentry
- 接口测试:postman
- vscode,cursor
- node版本:20.1
项目服务配置:接口请求和响应处理
项目研发流程
项目整体研发流程
- 团队共同确认目标和规划:开会讨论,产出目标和规划文档
- 产品调研和需求分析:产出调研报告和需求分析文档
- 需求评审:粗评和细评:明确要做的需求和工作,评估工作量并明确时间节点
- 方案设计:产出方案设计文档,比如数据库表设计、页面设计、接口设计等。
- 开发阶段:各自开发、单元测试、前后端联调等
- 测试和验收:研发自测、产品验收、组内验收等
- 代码提交:单人开发或多人开发啊,合并代码
- 部署上线:将代码发布到服务器上,组内进行上线通知并更新上线文档,上线后需回归测试
- 产品迭代:持续收集用户对新功能的反馈、并进行数据分析,从而验证改动效果,便于下一轮的更新迭代
开发规范
开发前注意事项
- 确保自己充分理解了业务和需求,需要先进行整体的方案设计;尤其是对于重要需求和核心业务,必须先跟组内同学核对方案并通过后,才能下手开发,避免重复工作
- 先熟悉项目再开发,阅读项目文档、项目代码、接口文档、前端组件文档等
- 熟悉团队已实现的功能和代码,尽量复用,避免重复开发。
- 熟悉团队内部的研发规范,并在 IDE 中进行相应的配置,比如前端配置 ESLint、Prettier 等代码规范插件
开发中注意事项
- git pull和创建分支的规范
- 开发时,遵循团队内部的研发规范,尽量参考现有项目代码的写法,尤其是不要使用和原项目不一致的格式、命名、写法,避免特立独行
- 开发过程中,有任何不明确的地方,不要凭空猜测,及时去联系项目的其他成员或负责人确认
- 开发时每天都pull一下最新的主分支代码,防止合并代码冲突
- 注意整体时间进度的把控,先完成再完美,有风险时及时反馈
- 部分功能完成后进行自测,提测之前确保bug数在最小
代码提交规范
- git提交规范
- 每次提交时,需要在 commit 信息中提供代码改动说明,还可以通过关联需求文档、测试用例、方案文档、效果截图等方式进行补充说明
- 除非特殊情况,否则所有的代码必须经过至少一位项目负责人 Code Review 审核通过后,才能合并;并且只有合并到主分支的代码才允许发布上线
后端给1w条数据,前端怎么展示
虚拟列表
- 是按需显示的一种实现,即只对可见区域进行渲染,对非可见区域中的数据不渲染或部分渲染的技术,从而达到极高的渲染性能
- 获取数据本身其实并没有那么消耗性能,渲染的过程才消耗时间,所以我们可以把渲染这一部分抽离出来,这样消耗的时间就减少了许多 juejin.cn/post/730191…
大文件上传
使用场景:
-
大文件加速上传:当文件大小超过预期大小时,使用分片上传可实现并行上传多个 Part, 以加快上传速度
-
网络环境较差:建议使用分片上传。当出现上传失败的时候,仅需重传失败的Part
-
流式上传:可以在需要上传的文件大小还不确定的情况下开始上传。这种场景在视频监控等行业应用中比较常见 要考虑的更多场景:
-
切片上传失败怎么办
-
上传过程中刷新页面怎么办
-
如何进行并行上传
-
切片什么时候按数量切,什么时候按大小切
-
如何结合 Web Worker 处理大文件上传
-
如何实现秒传
切片上传、断点续传
大文件上传问题:
采用file或者blob上传文件,前端会把文件全部读取到内存再上传,还是发送upload请求之后流式上传文件内容?会先读取
那这样的话,如果文件很大,会不会导致内存不够用的问题?这个不用担心,现代浏览器的性能应付的过来的
断点续传还有另外一个更好方案,用全局变量把切片存起来,50个切片,就50个请求,每次发送请求成功后,删除当前切片,这样不需要使用本地储存
本地储存是为了保存进度,比如电脑断电了,再开还能继续传
判断文件类型有问题啊,如果我手动改了后缀名不就绕过限制了吗?只是防止你选错,特意绕过前端阻止不了的
突停电了,断点续传记录的 当前位置 还能准确吗? 记录在localstore的没事的
为什么分片上传的时候进度条没到100呢 那上传的文件会不会残缺?浮点没有计算对而已或者向上取整
可以说一下如果后端传了多个文件给前端的话这种怎么做吗?按文件依次用a标签下载,混在一个文件流前端分不开
上传到后端一般有两种方案: 二进制Blob传输:formData传输 base64传输:转为base64传输
跟文件相关的对象: files:通过input标签读过来的文件对象,包含文件的各种信息,也是blob的一个子类 blob:不可变的二进制内容,包含很多操作方法 formData:用于后端传输的对象 fileReader:多用于把文件读取为某种形式,如base64,text文本
<input type='file' name='file' @change='fileChange'></input>
//方法中重要的三项:size,type,name
文件大小:1M:1*1024*1024
fileChange(e){let file = e.target.file[0]}
files和blob可以相互转化、两者可以通过FileReader读取出base64和text文本、或者可以通过Append到formData,然后传给接口(base64,text文本,formData格式)
多文件上传需要forEach循环上传,
切片上传
文件若是一次性上传,耗时会比较久,切片带来的好处还有个就是你可以得知进度,比如一个文件切成5份,发一份过去就是20%
发请求之前其实还需要对 chunkList 进行一个处理,刚才打印 chunkList 时,里面的每一个切片仅仅只有大小信息,没有其他参数,后端是需要其他信息的,因为网络原因,切片不可能是按照顺序接收的,这里我给 chunkList 再加上下标,还有文件名,切片名
<input type="file" @change="handleChange">
文件就在事件参数中,`e.target.files[0]`
将文件传输给后端,就是这个 file 对象
将这个 file 对象进行响应式处理存起来,拿到文件后进行切片,也就是点击上传时触发这个函数,那就再写一个点击事件 handleUpload
const handleUpload = () => {
if (!uploadFile.value) return
const chunkList = createChunk(uploadFile.value)
}
const createChunk = (file, size = 1 * 1024 * 1024) => {
const chunkList = []//用于存放切片
let cur = 0 //是切的进度
while (cur < file.size) {//while循环切,当切完时 `cur = file.size` 跳出循环
chunkList.push({ file: file.slice(cur, cur + size) })
cur += size
}
return chunkList
}
//发请求之前其实还需要对 `chunkList` 进行一个处理,刚才打印 `chunkList` 时,里面的每一个切片仅仅只有大小信息,没有其他参数,后端是需要其他信息的,因为网络原因,切片不可能是按照顺序接收的,这里我给 `chunkList` 再加上下标,还有文件名,切片名,如下
const handleUpload = () => {
if (!uploadFile.value) return
const chunkList = createChunk(uploadFile.value)
// console.log(chunkList);
// 另外切片需要打上标记,保证后端正确合并
uploadChunkList.value = chunkList.map(({ file }, index) => {
return {
file,
size: file.size,
percent: 0,
chunkName: `${uploadFile.value.name}-${index}`,
fileName: uploadFile.value.name,
index
}
})
console.log(uploadChunkList.value);
// 发请求 把切片一个一个地给后端
uploadChunks()
}
//`chunkList` 的每一项都是个对象,里面的 `file` 才是我们需要的,因此进行解构
`uploadFile` 里面是有 `name` 属性的,就是文件名
`uploadChunkList` 就是封装好的切片,这个切片比 `chunkList`多了其他后端需要的信息, `uploadChunkList` 被 `map` 赋值后就直接发请求
发请求并不是直接将封装好的切片数组 uploadChunkList 交给后端,因为后端并不认识你这个对象格式,我们需要先将其转换成数据流
const uploadChunks = () => {
const formateList = uploadChunkList.value.map(({ file, fileName, index, chunkName }) => {
// 对象需要转成二进制数据流进行传输
const formData = new FormData() // 创建表单格式的数据流
// 将切片转换成了表单的数据流
formData.append('file', file)
formData.append('fileName', fileName)
formData.append('chunkName', chunkName)
return { formData, index }
})
}
//`formateList` 只拿封装好的切片数组中的重要信息 `file` , `fileName` ,`index` ,`chunkName` ,并且在 `map` 中创建一个二进制表单数据流,将这些信息挂到 `formData` 中,最终赋值给 `formateList`
格式问题弄好后,现在对每一个 `form` 表单格式的切片进行发请求,依旧用 `map` 遍历,每一个表单切片都进行调用方才封装好的请求函数 `requestUpload` ,这个函数的里面有个进度条回调函数
const uploadChunks = () => {
const formateList = uploadChunkList.value.map(({ file, fileName, index, chunkName }) => {
// 对象需要转成二进制数据流进行传输
const formData = new FormData() // 创建表单格式的数据流
// 将切片转换成了表单的数据流
formData.append('file', file)
formData.append('fileName', fileName)
formData.append('chunkName', chunkName)
return { formData, index }
})
const requestList = formateList.map(({ formData, index }) => {
return requestUpload({
url: 'http://localhost:3000/upload',
data: formData,
onUploadProgress: createProgress(uploadChunkList.value[index]) // 进度条函数拿出来写
})
})
}
uploadChunkList 已经准备好了 percent ,createProgress 函数就是用于更改这个 percent 的,拿出来写
const createProgress = (item) => {
return (e) => {
// 为何函数需要return出来,因为axios的onUploadProgress就是个函数体
// 并且这个函数体参数e就是进度
item.percent = parseInt(String(e.loaded / e.total) * 100) // axios提供的
}
}
- 后端拿到的切片顺序是乱的,所以不会直接合并,会先把切片,文件名,切片名都拿到,
- 因为操作系统的限制,只能传6个切片???
- 然后后端合并切片,有以下几个方案
- 后端监听上传请求,当所有的请求都上传完毕时,出发合并请求操作
- 前端发完切片后,最后发一个合并请求
- 后端设置一个预期切片数量,达标后合并切片
前端用 map 格式化好 formateList 切片数组后得到的 requestList 就是一个一个的切片请求数组,刚好放入 Promise.all 中实现并发, then 中写入请求函数 mergeChunks
const uploadChunks = () => {
……
const requestList = formateList.map(({ formData, index }) => {
……
})
Promise.all(requestList).then(mergeChunks())
}//这也就是切片为何速度更快, `Promise.all` 实现并发请求
const mergeChunks = () => {
requestUpload({
url: 'http://localhost:3000/merge',
data: JSON.stringify({
fileName: uploadFile.value.name,
size: 2 * 1024 * 1024
})
})
}
万一有个切片失败了怎么办,,后端 fse 用 promise 实现了封装,里面可以捕获错误,如果切片上传失败,我可以记录好这个失败切片的索引,告诉前端让其重传
- 实现过程
前端拿到整个文件后利用文件 Blob 原型上的 slice 方法进行切割,将得到的切片数组 chunkList 添加一些信息,比如文件名和下标,得到 uploadChunkList ,但是 uploadChunkList 想要传给后端还需要将其转换成表单数据格式,通过 Promise.all 并发发给后端,传输完毕后发送一个合并请求,合并请求带上文件名和切片大小信息
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<title>Document</title>
</head>
<body>
<div id="app">
<input type="file" @change="handleChange">
<button @click="handleUpload">上传</button>
</div>
<script>
const { createApp, ref } = Vue
createApp({
setup() {
const uploadFile = ref(null) // 文件
const uploadChunkList = ref([])
const handleChange = (e) => {
if (!e.target.files[0]) return
uploadFile.value = e.target.files[0]
}
const handleUpload = () => {
if (!uploadFile.value) return
const chunkList = createChunk(uploadFile.value)
// console.log(chunkList);
// 另外切片需要打上标记,保证后端正确合并
uploadChunkList.value = chunkList.map(({ file }, index) => {
return {
file,
size: file.size,
percent: 0,
chunkName: `${uploadFile.value.name}-${index}`,
fileName: uploadFile.value.name,
index
}
})
console.log(uploadChunkList.value);
// 发请求 把切片一个一个地给后端
uploadChunks()
}
// 上传切片
const uploadChunks = () => {
const formateList = uploadChunkList.value.map(({ file, fileName, index, chunkName }) => {
// 对象需要转成二进制数据流进行传输
const formData = new FormData() // 创建表单格式的数据流
// 将切片转换成了表单的数据流
formData.append('file', file)
formData.append('fileName', fileName)
formData.append('chunkName', chunkName)
return { formData, index }
})
console.log(formateList); // 浏览器不给你展示二进制流,但是得清楚确实拿到了
// 发接口请求
const requestList = formateList.map(({ formData, index }) => {
return requestUpload({
url: 'http://localhost:3000/upload',
data: formData,
onUploadProgress: createProgress(uploadChunkList.value[index]) // 进度条函数拿出来写
})
})
// 合并切片请求
Promise.all(requestList).then(mergeChunks())
}
// 合并切片
const mergeChunks = () => {
requestUpload({
url: 'http://localhost:3000/merge',
data: JSON.stringify({
fileName: uploadFile.value.name,
size: 2 * 1024 * 1024
})
})
}
// 上传的进度
const createProgress = (item) => {
return (e) => {
// 为何函数需要return出来,因为axios的onUploadProgress就是个函数体
// 并且这个函数体参数e就是进度
item.percent = parseInt(String(e.loaded / e.total) * 100) // axios提供的
}
}
// 为了实现进度条,封装请求
const requestUpload = ({ url, method = 'post', data, headers = {}, onUploadProgress = (e) => e }) => {
return new Promise((resolve, reject) => {
// axios支持在请求中传入一个回调onUploadProgress,其目的就是为了知道请求的进度
axios[method](url, data, { headers, onUploadProgress })
.then(res => {
resolve(res)
})
.catch(err => {
reject(err)
})
})
}
const createChunk = (file, size = 2 * 1024 * 1024) => {
const chunkList = []
let cur = 0 // 当前切片
while (cur < file.size) {
chunkList.push({ file: file.slice(cur, cur + size) })
cur += size
}
return chunkList
}
return {
handleChange,
handleUpload,
createChunk
}
}
}).mount('#app')
</script>
</body>
</html>
- 一旦断网就要重传,如何解决? 断点续传,它允许传输文件时,若中断或失败,可以从上一次中断的地方继续传输,而非重新上传
弹窗拖拽
对clientx,offsetTop,,,,,的理解 自定义指令实现拖拽 element-ui弹窗在可视区域内拖拽 js实现拖拽 ie兼容性 cn.vuejs.org/v2/guide/cu…
直播会议中弹窗在可视区域内的拖拽: 考虑边界情况
import Vue from 'vue'
Vue.directive('dialogDrag', {
bind(el, binding, vnode, oldVnode) {
const dialogHeaderEl = el.querySelector('.el-dialog__header');
const dragDom = el.querySelector('.el-dialog-drag');
// dialogHeaderEl.style.cursor = 'move';
dialogHeaderEl.style.cssText += ';cursor:move;';
dragDom.style.cssText += ';top:0px;';
// 获取原有属性 ie dom元素.currentStyle 火狐谷歌 window.getComputedStyle(dom元素, null);
const sty = (function() {
if (window.document.currentStyle) {
return (dom, attr) => dom.currentStyle[attr];
} else {
return (dom, attr) => getComputedStyle(dom, false)[attr];
}
})();
dialogHeaderEl.onmousedown = e => {
// 鼠标按下,计算当前元素距离可视区的距离
const disX = e.clientX - dialogHeaderEl.offsetLeft;
const disY = e.clientY - dialogHeaderEl.offsetTop;
const screenWidth = document.body.clientWidth; // body当前宽度
const screenHeight = document.documentElement.clientHeight; // 可见区域高度(应为body高度,可某些环境下无法获取)
const dragDomWidth = dragDom.offsetWidth; // 对话框宽度
const dragDomheight = dragDom.offsetHeight; // 对话框高度
const minDragDomLeft = dragDom.offsetLeft;
const maxDragDomLeft = screenWidth - dragDom.offsetLeft - dragDomWidth;
const minDragDomTop = dragDom.offsetTop;
const maxDragDomTop = screenHeight - dragDom.offsetTop - dragDomheight;
// 获取到的值带px 正则匹配替换
let styL = sty(dragDom, 'left');
let styT = sty(dragDom, 'top');
// 注意在ie中 第一次获取到的值为组件自带50% 移动之后赋值为px
if (styL.includes('%')) {
// eslint-disable-next-line no-useless-escape
styL = +document.body.clientWidth * (+styL.replace(/\%/g, '') / 100);
// eslint-disable-next-line no-useless-escape
styT = +document.body.clientHeight * (+styT.replace(/\%/g, '') / 100);
} else {
styL = +styL.replace(/\px/g, '');
styT = +styT.replace(/\px/g, '');
}
document.onmousemove = function(e) {
// 通过事件委托,计算移动的距离
let left = e.clientX - disX;
let top = e.clientY - disY;
// 边界处理
if (-left > minDragDomLeft) {
left = -minDragDomLeft;
} else if (left > maxDragDomLeft) {
left = maxDragDomLeft;
}
if (-top > minDragDomTop) {
top = -minDragDomTop;
} else if (top > maxDragDomTop) {
top = maxDragDomTop;
}
// 移动当前元素
dragDom.style.cssText += `;left:${left + styL}px;top:${top + styT}px;`;
};
document.onmouseup = function(e) {
document.onmousemove = null;
document.onmouseup = null;
};
return false;
};
}
})
不考虑边界,可随意移动
Vue.directive('dialogDrag', {
bind(el, binding, vnode, oldVnode) {
const dialogHeaderEl = el.querySelector('.el-dialog__header')
const dragDom = el.querySelector('.el-dialog-drag')
dialogHeaderEl.style.cursor = 'move'
// 获取原有属性 ie dom元素.currentStyle 火狐谷歌 window.getComputedStyle(dom元素, null);
const sty = dragDom.currentStyle || window.getComputedStyle(dragDom, null)
dialogHeaderEl.onmousedown = (e) => {
console.log(dialogHeaderEl.offsetLeft)
console.log(dialogHeaderEl.offsetTop)
// 鼠标按下,计算当前元素距离可视区的距离
const disX = e.clientX - dialogHeaderEl.offsetLeft
const disY = e.clientY - dialogHeaderEl.offsetTop
// 获取到的值带px 正则匹配替换
let styL, styT
// 注意在ie中 第一次获取到的值为组件自带50% 移动之后赋值为px
if (sty.left.includes('%')) {
styL = +document.body.clientWidth * (+sty.left.replace(/\%/g, '') / 100)
styT = +document.body.clientHeight * (+sty.top.replace(/\%/g, '') / 100)
} else {
styL = +sty.left.replace(/\px/g, '')
styT = +sty.top.replace(/\px/g, '')
}
document.onmousemove = function(e) {
// 通过事件委托,计算移动的距离
const l = e.clientX - disX
const t = e.clientY - disY
// 移动当前元素
dragDom.style.left = `${l + styL}px`
dragDom.style.top = `${t + styT}px`
// 将此时的位置传出去
// binding.value({x:e.pageX,y:e.pageY})
}
document.onmouseup = function(e) {
document.onmousemove = null
document.onmouseup = null
}
}
}
})
网络监测
用的html5 原生network api实现网络检测
//ononline:网络连接的时候触发
window.addEventListener('online',function(){...})
//onoffline:网络断开的时候触发
window.addEventListener('offline',function(){...})
只是这个网络质量 好像检测不到
那个网络质量底层我记得是用 根据数据丢包字节 和传输字节数算的 好像
具体在深 你就说不知道了 不是我写的 在sdk 那一层封装的
如何封装组件和遇到的问题
单点登录怎么判断是否登录
im聊天功能
- 引入的引入开源项目,把im的sdk引入到项目中,做的修改,
- 聊天需要的信息;房间信息,在线用户信息,昵称和头像,获取聊天记录
- 进入到聊天界面ws打开
- 异常关闭重连,5秒重连1次,最多重连4次
- 初始化历史记录:
- 文本类型
- 时间展示:
- 文案长度判读
- 收到文本消息回调
- 收到图片消息回调
- 收到提示消息回调
- 系统通知(直播状态改变)
一些难题:
消息的可靠性、有序性
高并发场景下的消息实时推送,以及消息拉取
所有弹窗可进行拖拽
对金额精度的处理
测试使用库或第三方插件的情况
为什么0.1 + 0.2 不等于0.3
计算机中所有数据都以二进制存储的,计算时计算机要把数据先转换成二进制进行计算,然后在把计算结果转换成十进制。
在计算0.1+0.2时,二进制计算发生了精度丢失,导致再转换成十进制后和预计的结果不符
js对二进制小数的存储方式:使用64位固定长度来表示
为什么0.1 + 0.2 === 0.3为fasle,0.2+0.3===0.5为true
number类型运算都要想将其转化为二进制,将二进制运算,运算的结果在转化为十进制,因为number是64双精度,小数部分只有52位,但0.1转化为二进制是无限循环的,所以四舍五入了,所以就发生了精度丢失,0.1的二进制和0.2的二进制想家需要保留有效数字,所以又发生了精度丢失,所以结果为0.30000000004,所以为false,而0.2+0.3恰好两个转化成为二进制和相加过程都不会发生精度丢失,所以为true
add: function (arg1, arg2) { //返回值:arg1加arg2
var obj = this.trans(arg1, arg2);
var rM = Math.max(obj.r1, obj.r2);
return this.scale(this.scale(obj.n1, rM) + this.scale(obj.n2, rM), -rM);
},
sub: function (arg1, arg2) { //返回值:arg1减arg2
var obj = this.trans(arg1, arg2);
var rM = Math.max(obj.r1, obj.r2);
return this.scale(this.scale(obj.n1, rM) - this.scale(obj.n2, rM), -rM);
},
mul: function (arg1, arg2) { //返回值:arg1乘arg2
var obj = this.trans(arg1, arg2);
var rM = obj.r1 + obj.r2;
return this.scale(this.scale(obj.n1) * this.scale(obj.n2), -rM);
},
div: function (arg1, arg2) { //返回值:arg1除arg2
var obj = this.trans(arg1, arg2);
var rM = obj.r1 - obj.r2;
return this.scale(this.scale(obj.n1) / this.scale(obj.n2), -rM);
},
//1、定义一个对象,把数字放在对象里,
//2、把小数点前后的切割成两部分放在数组里['0','1']
//3、取出小数点后的数字以及数字长度
//最后对象里就是两个数字,以及小数点后数字的长度
//在用Math.max求两个数字小数点后的最大长度
trans: function (arg1, arg2) { //返回值:
var obj = {},
tmp;
obj.n1 = Number(arg1);
tmp = obj.n1.toString().split(".");
obj.r1 = tmp[1] ? tmp[1].length : 0;
obj.n2 = Number(arg2);
tmp = obj.n2.toString().split(".");
obj.r2 = tmp[1] ? tmp[1].length : 0;
return obj;
},
scale: function (num, pos) { //返回值:num缩放pos倍,不传pos则将num转成整数
if (pos === undefined) {
return Number(num.toString().replace(".", ""));
} else if (num === 0 || pos === 0) {
return num;
}
var parts = num.toString().split('.');
var integerLen = parts[0].length;
var decimalLen = parts[1] ? parts[1].length : 0;
if (pos > 0) {
var zeros = pos - decimalLen;
while (zeros > 0) {
zeros -= 1;
parts.push(0);
}
} else {
let zeros = Math.abs(pos) - integerLen;
while (zeros > 0) {
zeros -= 1;
parts.unshift(0);
}
}
var numLen = integerLen + pos;
parts = parts.join('').split('');
parts.splice(numLen > 0 ? numLen : 0, 0, '.');
return Number(parts.join(''));
},
对时间操作
获取当前日期的前后N天:(日期准确)
getTime(AddDayCount){
let result = ''
var dd = new Date();
dd.setDate(dd.getDate() + AddDayCount);//获取AddDayCount天后的日期
var y = dd.getFullYear();
var m = dd.getMonth()+1;//获取当前月份的日期
var d = dd.getDate();
var h = dd.getHours();
var s = dd.getSeconds();
var min = dd.getMinutes();
result = y+'-'+(m<10?'0'+m:m)+'-'+(d<10?'0'+d:d) + ' '+(h!=0?'00':'00')+ ':' + (s!=0?'00':'00') + ':' + (min!=0?'00':'00');
return result
},
使用的时候:
this.getTime(-1):前一天
this.getTime(0):当天
this.createdTime = [this.getTime(-1), this.getTime(0)]
获取当前日期(最优):
let queryDate = new Date();
queryDate.setDate(queryDate.getDate() - 1);
queryDate.setMonth(queryDate.getMonth() + 1);
let time = `{
queryDate.getMonth() < 10 ? "0" + queryDate.getMonth() : queryDate.getMonth()
}-${
queryDate.getDate() < 10 ? "0" + queryDate.getDate() : queryDate.getDate()
}`;
使用时直接获取time即可:dailoccurTimeStart: time
permission.js文件
permission主要负责全局路由守卫和登录判断
jekin发布流程
移动端兼容性问题
支付流程
程序分包
ci/cd
依旧以前端性能优化为例,可能会遇到的提问:
-
你把这个⼿机端的⽩屏时间减少了150%以上,是从哪些⽅⾯⼊⼿优化的?这个问题即使你没做过前端性能优化也能回答个七七⼋⼋,⽆⾮是组件分割、缓存、tree shaking等等,这是第⼀重⽐较浅的问题。
-
我看你⽤webpack中SplitChunksPlugin这个插件进⾏分chunk的,你分chunk的取舍是什么?哪些库分在同⼀个chunk,哪些应该分开你是如何考虑的?如果你提到了SplitChunksPlugin插件可能会有类似的追问,如果没有实际操作过的候选⼈这个时候就难以招架了,这个过程⼀定是需要⼀定的试错和取舍的.
-
在分chunk的过程中有没有遇到什么坑?怎么解决的?其实SplitChunksPlugin这个插件有⼀个暗坑,那就是chunid⾃增性导致id不固定唯⼀,很可能⼀个新依赖就导致id全部打乱,使得http缓存失效.
⽐如你说你优化了⼀个前端项⽬的⾸屏性能,降低了⽩屏时间,那么⾯试官对这个性能优化问题会进⾏深挖,来考察候选⼈的实际⽔平:
-
你的性能优化指标是怎么确定的?平均下来时间减短了多少?
-
你的性能是如何测试的?有两种主流的性能测试⽅法你是怎么选的?
-
你是根据哪些指标进⾏针对性优化的?
-
除了你说的这些优化⽅法还有没有想过通过xx来解决?
-
你的这个优化⽅法在实际操作中碰到过什么问题吗?有没有进⼀步做过测试?
-
我们假设这么⼀种情况,⽐如xxxx,你会这么进⾏优化?
1、浏览器兼容:
从浏览器的内核,版本判断
比如es6支持哪些浏览器,哪些版本
css样式上的,js上的
2、对技术的沉淀和业务的沉淀
项目上的:从0-1.技术选型,性能优化,对webpack的配置,需求怎么快速实现,
不止是webpack,还有别的打包的gulp,rollup这些,而且别人还有可能会打断你问你细节,你可以多准备一些,而且但凡你说到了你都要多了解,要不然你说了你不知道你直接就没机会了,还有补充就是写代码的规范,规范很重要,一般一开始就得想到,接到0-1的项目就是先了解诉求,接别人的项目就是先了解项目及规范
3、项目中的难点和亮点
这个是发散性问题,你可以说业务,你给他选了个什么方案,然后节约了多少人力或者效率提升了多少;如果是技术,那就说一下技术难点,然后把解决说了,或者更简单的,就是浏览器兼容,怎么讨巧处理了,最好有点借鉴之处,不要太菜了,很简单的也暴露了你技术的深度
节约多少人:浏览器可以看到调接口的速度,或者首页加载速度,都是可以看的,可以说以前打开页面会崩溃,优化后打开速度变慢了。说他变快的原因是处理了什么
4、你在项目中做过哪些性能优化:
小点:公共组件提取,forEach和map呆的选择,v-if/v-show的使用、防抖节流、
webpack上:
按需加载、
plugin组件的配置:减少体积
使用的loader
按照开发需要的几要素去说,先大后小先主后次,层层分明,逻辑条理