业务记录和项目难点

229 阅读20分钟
  • bug:禅道
  • ui:蓝湖
  • 接口:yapi
  • 需求文档:confluence,文档组织,富文本编辑,
  • 第三方错误管理:sentry
  • 接口测试:postman
  • vscode,cursor
  • node版本:20.1

项目服务配置:接口请求和响应处理

项目研发流程

项目整体研发流程

  1. 团队共同确认目标和规划:开会讨论,产出目标和规划文档
  2. 产品调研和需求分析:产出调研报告和需求分析文档
  3. 需求评审:粗评和细评:明确要做的需求和工作,评估工作量并明确时间节点
  4. 方案设计:产出方案设计文档,比如数据库表设计、页面设计、接口设计等。
  5. 开发阶段:各自开发、单元测试、前后端联调等
  6. 测试和验收:研发自测、产品验收、组内验收等
  7. 代码提交:单人开发或多人开发啊,合并代码
  8. 部署上线:将代码发布到服务器上,组内进行上线通知并更新上线文档,上线后需回归测试
  9. 产品迭代:持续收集用户对新功能的反馈、并进行数据分析,从而验证改动效果,便于下一轮的更新迭代

开发规范

开发前注意事项
  1. 确保自己充分理解了业务和需求,需要先进行整体的方案设计;尤其是对于重要需求和核心业务,必须先跟组内同学核对方案并通过后,才能下手开发,避免重复工作
  2. 先熟悉项目再开发,阅读项目文档、项目代码、接口文档、前端组件文档等
  3. 熟悉团队已实现的功能和代码,尽量复用,避免重复开发。
  4. 熟悉团队内部的研发规范,并在 IDE 中进行相应的配置,比如前端配置 ESLint、Prettier 等代码规范插件
开发中注意事项
  1. git pull和创建分支的规范
  2. 开发时,遵循团队内部的研发规范,尽量参考现有项目代码的写法,尤其是不要使用和原项目不一致的格式、命名、写法,避免特立独行
  3. 开发过程中,有任何不明确的地方,不要凭空猜测,及时去联系项目的其他成员或负责人确认
  4. 开发时每天都pull一下最新的主分支代码,防止合并代码冲突
  5. 注意整体时间进度的把控,先完成再完美,有风险时及时反馈
  6. 部分功能完成后进行自测,提测之前确保bug数在最小

代码提交规范

  1. git提交规范
  2. 每次提交时,需要在 commit 信息中提供代码改动说明,还可以通过关联需求文档、测试用例、方案文档、效果截图等方式进行补充说明
  3. 除非特殊情况,否则所有的代码必须经过至少一位项目负责人 Code Review 审核通过后,才能合并;并且只有合并到主分支的代码才允许发布上线

后端给1w条数据,前端怎么展示

虚拟列表

  • 是按需显示的一种实现,即只对可见区域进行渲染,对非可见区域中的数据不渲染或部分渲染的技术,从而达到极高的渲染性能
  • 获取数据本身其实并没有那么消耗性能,渲染的过程才消耗时间,所以我们可以把渲染这一部分抽离出来,这样消耗的时间就减少了许多 juejin.cn/post/730191…

juejin.cn/post/684490…

大文件上传

使用场景:

  • 大文件加速上传:当文件大小超过预期大小时,使用分片上传可实现并行上传多个 Part, 以加快上传速度

  • 网络环境较差:建议使用分片上传。当出现上传失败的时候,仅需重传失败的Part

  • 流式上传:可以在需要上传的文件大小还不确定的情况下开始上传。这种场景在视频监控等行业应用中比较常见 要考虑的更多场景:

  • 切片上传失败怎么办

  • 上传过程中刷新页面怎么办

  • 如何进行并行上传

  • 切片什么时候按数量切,什么时候按大小切

  • 如何结合 Web Worker 处理大文件上传

  • 如何实现秒传

www.bilibili.com/video/BV142…

juejin.cn/post/735681…

juejin.cn/post/732388…

juejin.cn/post/684490…

切片上传、断点续传

大文件上传问题:

采用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]}

截屏2024-07-29 14.26.53.png

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…

Vue中常用的一些自定义指令

简单实现el-dialog的拖拽功能

全面解析offsetLeft、offsetTop

鼠标拖拽窗口效果(不允许拖出屏幕的可视区域)

vue实现拖拽的简单案例 不超出可视区域

HTML元素拖拽功能的实现

直播会议中弹窗在可视区域内的拖拽: 考虑边界情况

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
      }
    }
  }
})

网络监测

截屏2023-11-13 12.49.52.png 用的html5 原生network api实现网络检测

//ononline:网络连接的时候触发
window.addEventListener('online',function(){...})
//onoffline:网络断开的时候触发
window.addEventListener('offline',function(){...})

只是这个网络质量 好像检测不到

那个网络质量底层我记得是用 根据数据丢包字节 和传输字节数算的 好像

具体在深 你就说不知道了 不是我写的 在sdk 那一层封装的

如何封装组件和遇到的问题

单点登录怎么判断是否登录

vue3js.cn/interview/J…

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.getFullYear(){queryDate.getFullYear()}-{

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

依旧以前端性能优化为例,可能会遇到的提问:

  1. 你把这个⼿机端的⽩屏时间减少了150%以上,是从哪些⽅⾯⼊⼿优化的?这个问题即使你没做过前端性能优化也能回答个七七⼋⼋,⽆⾮是组件分割、缓存、tree shaking等等,这是第⼀重⽐较浅的问题。

  2. 我看你⽤webpack中SplitChunksPlugin这个插件进⾏分chunk的,你分chunk的取舍是什么?哪些库分在同⼀个chunk,哪些应该分开你是如何考虑的?如果你提到了SplitChunksPlugin插件可能会有类似的追问,如果没有实际操作过的候选⼈这个时候就难以招架了,这个过程⼀定是需要⼀定的试错和取舍的.

  3. 在分chunk的过程中有没有遇到什么坑?怎么解决的?其实SplitChunksPlugin这个插件有⼀个暗坑,那就是chunid⾃增性导致id不固定唯⼀,很可能⼀个新依赖就导致id全部打乱,使得http缓存失效.

⽐如你说你优化了⼀个前端项⽬的⾸屏性能,降低了⽩屏时间,那么⾯试官对这个性能优化问题会进⾏深挖,来考察候选⼈的实际⽔平:

  1. 你的性能优化指标是怎么确定的?平均下来时间减短了多少?

  2. 你的性能是如何测试的?有两种主流的性能测试⽅法你是怎么选的?

  3. 你是根据哪些指标进⾏针对性优化的?

  4. 除了你说的这些优化⽅法还有没有想过通过xx来解决?

  5. 你的这个优化⽅法在实际操作中碰到过什么问题吗?有没有进⼀步做过测试?

  6. 我们假设这么⼀种情况,⽐如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

按照开发需要的几要素去说,先大后小先主后次,层层分明,逻辑条理