授权自@谭光志 链接:https://segmentfault.com/a/1190000037752931,也可点击阅读原文

本文将分成以下 7 个小节:

  1. 技术选型

  2. 统一规范

  3. 测试

  4. 部署

  5. 监控

  6. 性能优化

  7. 重构

部分小节提供了非常详细的实战教程,让大家动手实践。

另外我还写了一个前端工程化 demo 放在github上。这个 demo 包含了 js、css、git 验证,其中 js、css 验证需要安装 VSCode,具体教程在下文中会有提及。

技术选型

对于前端来说,技术选型挺简单的。就是做选择题,三大框架中选一个。个人认为可以依据以下两个特点来选:

  1. 选你或团队最熟的,保证在遇到棘手的问题时有人能填坑。

  2. 选市场占有率高的。换句话说,就是选好招人的。

第二点对于小公司来说,特别重要。本来小公司就不好招人,要是还选一个市场占有率不高的框架(例如 Angular),简历你都看不到几个...UI 组件库更简单,github 上哪个 star 多就用哪个。star 多,说明用的人就多,很多坑别人都替你踩过了,省事。

统一规范

代码规范

先来看看统一代码规范的好处:

  • 规范的代码可以促进团队合作

  • 规范的代码可以降低维护成本

  • 规范的代码有助于 code review(代码审查)

  • 养成代码规范的习惯,有助于程序员自身的成长

当团队的成员都严格按照代码规范来写代码时,可以保证每个人的代码看起来都像是一个人写的,看别人的代码就像是在看自己的代码。更重要的是我们能够认识到规范的重要性,并坚持规范的开发习惯。

如何制订代码规范

建议找一份好的代码规范,在此基础上结合团队的需求作个性化修改。下面列举一些 star 较多的 js 代码规范:

  • airbnb (101k star 英文版),airbnb-中文版

  • standard (24.5k star) 中文版

  • 百度前端编码规范 3.9k

css 代码规范也有不少,例如:

  • styleguide 2.3k

  • spec 3.9k

如何检查代码规范

使用 eslint 可以检查代码符不符合团队制订的规范,下面来看一下如何配置 eslint 来检查代码。

  1. 下载依赖
    
// eslint-config-airbnb-base 使用 airbnb 代码规范
npm i -D babel-eslint eslint eslint-config-airbnb-base eslint-plugin-import
  1. package.jsonscripts加上这行代码"lint": "eslint --ext .js test/ src/ bin/"。然后执行npm run lint即可开始验证代码。

不过这样检查代码效率太低,每次都得手动检查。并且报错了还得手动修改代码。

为了改善以上缺点,我们可以使用 VSCode。使用它并加上适当的配置可以在每次保存代码的时候,自动验证代码并进行格式化,省去了动手的麻烦。

css 检查代码规范则使用stylelint插件。

由于篇幅有限,具体如何配置请看我的另一篇文章ESlint + stylelint + VSCode自动格式化代码(2020)。

git 规范

git 规范包括两点:分支管理规范、git commit 规范。

分支管理规范

一般项目分主分支(master)和其他分支。

当有团队成员要开发新功能或改 BUG 时,就从 master 分支开一个新的分支。例如项目要从客户端渲染改成服务端渲染,就开一个分支叫 ssr,开发完了再合并回 master 分支。

如果改一个 BUG,也可以从 master 分支开一个新分支,并用 BUG 号命名(不过我们小团队嫌麻烦,没这样做,除非有特别大的 BUG)。

git commit 规范

<type>(<scope>): <subject>
<BLANK LINE>
<body>
<BLANK LINE>
<footer>

大致分为三个部分(使用空行分割):

  1. 标题行: 必填, 描述主要修改类型和内容

  2. 主题内容: 描述为什么修改, 做了什么样的修改, 以及开发的思路等等

  3. 页脚注释: 可以写注释,BUG 号链接

type: commit 的类型

  • feat: 新功能、新特性

  • fix: 修改 bug

  • perf: 更改代码,以提高性能

  • refactor: 代码重构(重构,在不影响代码内部行为、功能下的代码修改)

  • docs: 文档修改

  • style: 代码格式修改, 注意不是 css 修改(例如分号修改)

  • test: 测试用例新增、修改

  • build: 影响项目构建或依赖项修改

  • revert: 恢复上一次提交

  • ci: 持续集成相关文件修改

  • chore: 其他修改(不在上述类型中的修改)

  • release: 发布新版本

  • workflow: 工作流相关文件修改

  1. scope: commit 影响的范围, 比如: route, component, utils, build...

  2. subject: commit 的概述

  3. body: commit 具体修改内容, 可以分为多行.

  4. footer: 一些备注, 通常是 BREAKING CHANGE 或修复的 bug 的链接.

示例

fix(修复BUG)

如果修复的这个BUG只影响当前修改的文件,可不加范围。如果影响的范围比较大,要加上范围描述。

例如这次 BUG 修复影响到全局,可以加个 global。如果影响的是某个目录或某个功能,可以加上该目录的路径,或者对应的功能名称。

// 示例1
fix(global):修复checkbox不能复选的问题
// 示例2 下面圆括号里的 common 为通用管理的名称
fix(common): 修复字体过小的BUG,将通用管理下所有页面的默认字体大小修改为 14px
// 示例3
fix: value.length -> values.length
feat(添加新功能或新页面)
feat: 添加网站主页静态页面这是一个示例,假设对点检任务静态页面进行了一些描述。这里是备注,可以是放BUG链接或者一些重要性的东西。
chore(其他修改)

chore 的中文翻译为日常事务、例行工作,顾名思义,即不在其他 commit 类型中的修改,都可以用 chore 表示。

chore: 将表格中的查看详情改为详情

其他类型的 commit 和上面三个示例差不多,就不说了。

验证 git commit 规范

验证 git commit 规范,主要通过 git 的pre-commit钩子函数来进行。当然,你还需要下载一个辅助工具来帮助你进行验证。

下载辅助工具

npm i -D husky

package.json加上下面的代码

"husky": {"hooks": {"pre-commit": "npm run lint","commit-msg": "node script/verify-commit.js","pre-push": "npm test"}
}

然后在你项目根目录下新建一个文件夹script,并在下面新建一个文件verify-commit.js,输入以下代码:

const msgPath = process.env.HUSKY_GIT_PARAMS
const msg = require('fs')
.readFileSync(msgPath, 'utf-8')
.trim()const commitRE = /^(feat|fix|docs|style|refactor|perf|test|workflow|build|ci|chore|release|workflow)(\(.+\))?: .{1,50}/if (!commitRE.test(msg)) {console.log()console.error(`不合法的 commit 消息格式。请查看 git commit 提交规范:https://github.com/woai3c/Front-end-articles/blob/master/git%20commit%20style.md`)process.exit(1)
}

现在来解释下各个钩子的含义:

  1. "pre-commit": "npm run lint",在git commit前执行npm run lint检查代码格式。

  2. "commit-msg": "node script/verify-commit.js",在git commit时执行脚本verify-commit.js验证 commit 消息。如果不符合脚本中定义的格式,将会报错。

  3. "pre-push": "npm test",在你执行git push将代码推送到远程仓库前,执行npm test进行测试。如果测试失败,将不会执行这次推送。

项目规范

主要是项目文件的组织方式和命名方式。

用我们的 Vue 项目举个例子。

├─public
├─src
├─test

一个项目包含 public(公共资源,不会被 webpack 处理)、src(源码)、test(测试代码),其中 src 目录,又可以细分。

├─api (接口)
├─assets (静态资源)
├─components (公共组件)
├─styles (公共样式)
├─router (路由)
├─store (vuex 全局数据)
├─utils (工具函数)
└─views (页面)

文件名称如果过长则用 - 隔开。

UI 规范

UI 规范需要前端、UI、产品沟通,互相商量,最后制定下来,建议使用统一的 UI 组件库。

制定 UI 规范的好处:

  • 统一页面 UI 标准,节省 UI 设计时间

  • 提高前端开发效率

测试

测试是前端工程化建设必不可少的一部分,它的作用就是找出 bug,越早发现 bug,所需要付出的成本就越低。并且,它更重要的作用是在将来,而不是当下。

设想一下半年后,你的项目要加一个新功能。在加完新功能后,你不确定有没有影响到原有的功能,需要测试一下。由于时间过去太久,你对项目的代码已经不了解了。在这种情况下,如果没有写测试,你就得手动一遍一遍的去试。而如果写了测试,你只需要跑一遍测试代码就 OK 了,省时省力。

写测试还可以让你修改代码时没有心理负担,不用一直想着改这里有没有问题?会不会引起 BUG?而写了测试就没有这种担心了。

在前端用得最多的就是单元测试(主要是端到端测试我用得很少,不熟),这里着重讲解一下。

单元测试

单元测试就是对一个函数、一个组件、一个类做的测试,它针对的粒度比较小。它应该怎么写呢?

  1. 根据正确性写测试,即正确的输入应该有正常的结果。

  2. 根据异常写测试,即错误的输入应该是错误的结果。

对一个函数做测试

例如一个取绝对值的函数abs(),输入1,2,结果应该与输入相同;输入-1,-2,结果应该与输入相反。如果输入非数字,例如"abc",应该抛出一个类型错误。

对一个类做测试

假设有这样一个类:

class Math {abs() {}sqrt() {}pow() {}...
}

单元测试,必须把这个类的所有方法都测一遍。

对一个组件做测试

组件测试比较难,因为很多组件都涉及了 DOM 操作。

例如一个上传图片组件,它有一个将图片转成 base64 码的方法,那要怎么测试呢?一般测试都是跑在 node 环境下的,而 node 环境没有 DOM 对象。

我们先来回顾一下上传图片的过程:

  1. 点击<input type="file" />,选择图片上传。

  2. 触发inputchange事件,获取file对象。

  3. FileReader将图片转换成 base64 码。

这个过程和下面的代码是一样的:

document.querySelector('input').onchange = function fileChangeHandler(e) {const file = e.target.files[0]const reader = new FileReader()reader.onload = (res) => {const fileResult = res.target.resultconsole.log(fileResult) // 输出 base64 码}reader.readAsDataURL(file)
}

上面的代码只是模拟,真实情况下应该是这样使用

document.querySelector('input').onchange = function fileChangeHandler(e) {const file = e.target.files[0]tobase64(file)
}function tobase64(file) {return new Promise((resolve, reject) => {const reader = new FileReader()reader.onload = (res) => {const fileResult = res.target.resultresolve(fileResult) // 输出 base64 码}reader.readAsDataURL(file)})
}

可以看到,上面代码出现了 window 的事件对象eventFileReader。也就是说,只要我们能够提供这两个对象,就可以在任何环境下运行它。所以我们可以在测试环境下加上这两个对象:

// 重写 File
window.File = function () {}// 重写 FileReader
window.FileReader = function () {this.readAsDataURL = function () {this.onload&& this.onload({target: {result: fileData,},})}
}

然后测试可以这样写:

// 提前写好文件内容
const fileData = 'data:image/test'// 提供一个假的 file 对象给 tobase64() 函数
function test() {const file = new File()const event = { target: { files: [file] } }file.type = 'image/png'file.name = 'test.png'file.size = 1024it('file content', (done) => {tobase64(file).then(base64 => {expect(base64).toEqual(fileData) // 'data:image/test'done()})})
}// 执行测试
test()

通过这种 hack 的方式,我们就实现了对涉及 DOM 操作的组件的测试。我的vue-upload-imgs库就是通过这种方式写的单元测试,有兴趣可以了解一下。

TDD 测试驱动开发

TDD 就是根据需求提前把测试代码写好,然后根据测试代码实现功能。

TDD 的初衷是好的,但如果你的需求经常变(你懂的),那就不是一件好事了。很有可能你天天都在改测试代码,业务代码反而没怎么动。

所以到现在为止,三年多的程序员生涯,我还没尝试过 TDD 开发。

虽然环境如此艰难,但有条件的情况下还是应该试一下 TDD 的。例如在你自己负责一个项目又不忙的时候,可以采用此方法编写测试用例。

测试框架推荐

我常用的测试框架是jest,好处是有中文文档,API 清晰明了,一看就知道是干什么用的。

部署

在没有学会自动部署前,我是这样部署项目的:

  1. 执行测试npm run test

  2. 推送代码git push

  3. 构建项目npm run build

  4. 将打包好的文件放到静态服务器。

一次两次还行,如果天天都这样,就会把很多时间浪费在重复的操作上。所以我们要学会自动部署,彻底解放双手。自动部署(又叫持续部署 Continuous Deployment,英文缩写 CD)一般有两种触发方式:

  1. 轮询。

  2. 监听webhook事件。

轮询

轮询,就是构建软件每隔一段时间自动执行打包、部署操作。

这种方式不太好,很有可能软件刚部署完我就改代码了。为了看到新的页面效果,不得不等到下一次构建开始。

另外还有一个副作用,假如我一天都没更改代码,构建软件还是会不停的执行打包、部署操作,白白的浪费资源。

所以现在的构建软件基本采用监听webhook事件的方式来进行部署。

监听webhook事件

webhook 钩子函数,就是在你的构建软件上进行设置,监听某一个事件(一般是监听push事件),当事件触发时,自动执行定义好的脚本。

例如Github Actions,就有这个功能。对于新人来说,仅看我这一段讲解是不可能学会自动部署的。为此我特地写了一篇自动化部署教程,不需要你提前学习自动化部署的知识,只要照着指引做,就能实现前端项目自动化部署。

前端项目自动化部署——超详细教程(Jenkins、Github Actions),教程已经奉上,各位大佬看完后要是觉得有用,不要忘了点赞,感激不尽。

监控

监控,又分性能监控和错误监控,它的作用是预警和追踪定位问题。

性能监控

性能监控一般利用window.performance来进行数据采集。

Performance 接口可以获取到当前页面中与性能相关的信息,它是 High Resolution Time API 的一部分,同时也融合了 Performance Timeline API、Navigation Timing API、 User Timing API 和 Resource Timing API。

这个 API 的属性timing,包含了页面加载各个阶段的起始及结束时间。
为了方便大家理解timing各个属性的意义,我在知乎找到一位网友对于timing写的简介(忘了姓名,后来找不到了,见谅),在此转载一下。

timing: {// 同一个浏览器上一个页面卸载(unload)结束时的时间戳。如果没有上一个页面,这个值会和fetchStart相同。navigationStart: 1543806782096,// 上一个页面unload事件抛出时的时间戳。如果没有上一个页面,这个值会返回0。unloadEventStart: 1543806782523,// 和 unloadEventStart 相对应,unload事件处理完成时的时间戳。如果没有上一个页面,这个值会返回0。unloadEventEnd: 1543806782523,// 第一个HTTP重定向开始时的时间戳。如果没有重定向,或者重定向中的一个不同源,这个值会返回0。redirectStart: 0,// 最后一个HTTP重定向完成时(也就是说是HTTP响应的最后一个比特直接被收到的时间)的时间戳。// 如果没有重定向,或者重定向中的一个不同源,这个值会返回0. redirectEnd: 0,// 浏览器准备好使用HTTP请求来获取(fetch)文档的时间戳。这个时间点会在检查任何应用缓存之前。fetchStart: 1543806782096,// DNS 域名查询开始的UNIX时间戳。//如果使用了持续连接(persistent connection),或者这个信息存储到了缓存或者本地资源上,这个值将和fetchStart一致。domainLookupStart: 1543806782096,// DNS 域名查询完成的时间.//如果使用了本地缓存(即无 DNS 查询)或持久连接,则与 fetchStart 值相等domainLookupEnd: 1543806782096,// HTTP(TCP) 域名查询结束的时间戳。//如果使用了持续连接(persistent connection),或者这个信息存储到了缓存或者本地资源上,这个值将和 fetchStart一致。connectStart: 1543806782099,// HTTP(TCP) 返回浏览器与服务器之间的连接建立时的时间戳。// 如果建立的是持久连接,则返回值等同于fetchStart属性的值。连接建立指的是所有握手和认证过程全部结束。connectEnd: 1543806782227,// HTTPS 返回浏览器与服务器开始安全链接的握手时的时间戳。如果当前网页不要求安全连接,则返回0。secureConnectionStart: 1543806782162,// 返回浏览器向服务器发出HTTP请求时(或开始读取本地缓存时)的时间戳。requestStart: 1543806782241,// 返回浏览器从服务器收到(或从本地缓存读取)第一个字节时的时间戳。//如果传输层在开始请求之后失败并且连接被重开,该属性将会被数制成新的请求的相对应的发起时间。responseStart: 1543806782516,// 返回浏览器从服务器收到(或从本地缓存读取,或从本地资源读取)最后一个字节时//(如果在此之前HTTP连接已经关闭,则返回关闭时)的时间戳。responseEnd: 1543806782537,// 当前网页DOM结构开始解析时(即Document.readyState属性变为“loading”、相应的 readystatechange事件触发时)的时间戳。domLoading: 1543806782573,// 当前网页DOM结构结束解析、开始加载内嵌资源时(即Document.readyState属性变为“interactive”、相应的readystatechange事件触发时)的时间戳。domInteractive: 1543806783203,// 当解析器发送DOMContentLoaded 事件,即所有需要被执行的脚本已经被解析时的时间戳。domContentLoadedEventStart: 1543806783203,// 当所有需要立即执行的脚本已经被执行(不论执行顺序)时的时间戳。domContentLoadedEventEnd: 1543806783216,// 当前文档解析完成,即Document.readyState 变为 'complete'且相对应的readystatechange 被触发时的时间戳domComplete: 1543806783796,// load事件被发送时的时间戳。如果这个事件还未被发送,它的值将会是0。loadEventStart: 1543806783796,// 当load事件结束,即加载事件完成时的时间戳。如果这个事件还未被发送,或者尚未完成,它的值将会是0.loadEventEnd: 1543806783802
}

通过以上数据,我们可以得到几个有用的时间

// 重定向耗时
redirect: timing.redirectEnd - timing.redirectStart,
// DOM 渲染耗时
dom: timing.domComplete - timing.domLoading,
// 页面加载耗时
load: timing.loadEventEnd - timing.navigationStart,
// 页面卸载耗时
unload: timing.unloadEventEnd - timing.unloadEventStart,
// 请求耗时
request: timing.responseEnd - timing.requestStart,
// 获取性能信息时当前时间
time: new Date().getTime(),

还有一个比较重要的时间就是白屏时间,它指从输入网址,到页面开始显示内容的时间。

将以下脚本放在</head>前面就能获取白屏时间。

<script>whiteScreen = new Date() - performance.timing.navigationStart
</script>

通过这几个时间,就可以得知页面首屏加载性能如何了。

另外,通过window.performance.getEntriesByType('resource')这个方法,我们还可以获取相关资源(js、css、img...)的加载时间,它会返回页面当前所加载的所有资源。

它一般包括以下几个类型

  • sciprt

  • link

  • img

  • css

  • fetch

  • other

  • xmlhttprequest

我们只需用到以下几个信息

// 资源的名称
name: item.name,
// 资源加载耗时
duration: item.duration.toFixed(2),
// 资源大小
size: item.transferSize,
// 资源所用协议
protocol: item.nextHopProtocol,

现在,写几行代码来收集这些数据。

// 收集性能信息
const getPerformance = () => {if (!window.performance) returnconst timing = window.performance.timingconst performance = {// 重定向耗时redirect: timing.redirectEnd - timing.redirectStart,// 白屏时间whiteScreen: whiteScreen,// DOM 渲染耗时dom: timing.domComplete - timing.domLoading,// 页面加载耗时load: timing.loadEventEnd - timing.navigationStart,// 页面卸载耗时unload: timing.unloadEventEnd - timing.unloadEventStart,// 请求耗时request: timing.responseEnd - timing.requestStart,// 获取性能信息时当前时间time: new Date().getTime(),}return performance
}// 获取资源信息
const getResources = () => {if (!window.performance) returnconst data = window.performance.getEntriesByType('resource')const resource = {xmlhttprequest: [],css: [],other: [],script: [],img: [],link: [],fetch: [],// 获取资源信息时当前时间time: new Date().getTime(),}data.forEach(item => {const arry = resource[item.initiatorType]arry && arry.push({// 资源的名称name: item.name,// 资源加载耗时duration: item.duration.toFixed(2),// 资源大小size: item.transferSize,// 资源所用协议protocol: item.nextHopProtocol,})})return resource
}

小结

通过对性能及资源信息的解读,我们可以判断出页面加载慢有以下几个原因:

  1. 资源过多

  2. 网速过慢

  3. DOM元素过多

除了用户网速过慢,我们没办法之外,其他两个原因都是有办法解决的,性能优化将在下一节《性能优化》中会讲到。

错误监控

现在能捕捉的错误有三种。

  1. 资源加载错误,通过addEventListener('error', callback, true)在捕获阶段捕捉资源加载失败错误。

  2. js 执行错误,通过window.onerror捕捉 js 错误。

  3. promise 错误,通过addEventListener('unhandledrejection', callback)捕捉 promise 错误,但是没有发生错误的行数,列数等信息,只能手动抛出相关错误信息。

我们可以建一个错误数组变量errors在错误发生时,将错误的相关信息添加到数组,然后在某个阶段统一上报,具体如何操作请看代码

// 捕获资源加载失败错误 js css img...
addEventListener('error', e => {const target = e.targetif (target != window) {monitor.errors.push({type: target.localName,url: target.src || target.href,msg: (target.src || target.href) + ' is load error',// 错误发生的时间time: new Date().getTime(),})}
}, true)// 监听 js 错误
window.onerror = function(msg, url, row, col, error) {monitor.errors.push({type: 'javascript',row: row,col: col,msg: error && error.stack? error.stack : msg,url: url,// 错误发生的时间time: new Date().getTime(),})
}// 监听 promise 错误 缺点是获取不到行数数据
addEventListener('unhandledrejection', e => {monitor.errors.push({type: 'promise',msg: (e.reason && e.reason.msg) || e.reason || '',// 错误发生的时间time: new Date().getTime(),})
})

小结

通过错误收集,可以了解到网站错误发生的类型及数量,从而可以做相应的调整,以减少错误发生。
完整代码和 DEMO 请看我另一篇文章前端性能和错误监控的末尾,大家可以复制代码(HTML文件)在本地测试一下。

数据上报

性能数据上报

性能数据可以在页面加载完之后上报,尽量不要对页面性能造成影响。

window.onload = () => {// 在浏览器空闲时间获取性能及资源信息// https://developer.mozilla.org/zh-CN/docs/Web/API/Window/requestIdleCallbackif (window.requestIdleCallback) {window.requestIdleCallback(() => {monitor.performance = getPerformance()monitor.resources = getResources()})} else {setTimeout(() => {monitor.performance = getPerformance()monitor.resources = getResources()}, 0)}
}

当然,你也可以设一个定时器,循环上报。不过每次上报最好做一下对比去重再上报,避免同样的数据重复上报。

错误数据上报

我在DEMO里提供的代码,是用一个errors数组收集所有的错误,再在某一阶段统一上报(延时上报)。

其实,也可以改成在错误发生时上报(即时上报)。这样可以避免在收集完错误延时上报还没触发,用户却已经关掉网页导致错误数据丢失的问题。

// 监听 js 错误
window.onerror = function(msg, url, row, col, error) {const data = {type: 'javascript',row: row,col: col,msg: error && error.stack? error.stack : msg,url: url,// 错误发生的时间time: new Date().getTime(),}// 即时上报axios.post({ url: 'xxx', data, })
}

SPA

window.performanceAPI 是有缺点的,在 SPA 切换路由时,window.performance.timing的数据不会更新。

所以我们需要另想办法来统计切换路由到加载完成的时间。

拿 Vue 举例,一个可行的办法就是切换路由时,在路由的全局前置守卫beforeEach里获取开始时间,在组件的mounted钩子里执行vm.$nextTick函数来获取组件的渲染完毕时间。

router.beforeEach((to, from, next) => {store.commit('setPageLoadedStartTime', new Date())
})
mounted() {this.$nextTick(() => {this.$store.commit('setPageLoadedTime', new Date() - this.$store.state.pageLoadedStartTime)})
}

除了性能和错误监控,其实我们还可以做得更多。

用户信息收集

navigator

使用window.navigator可以收集到用户的设备信息,操作系统,浏览器信息...

UV(Unique visitor)

是指通过互联网访问、浏览这个网页的自然人。访问您网站的一台电脑客户端为一个访客。00:00-24:00内相同的客户端只被计算一次。一天内同个访客多次访问仅计算一个UV。
在用户访问网站时,可以生成一个随机字符串+时间日期,保存在本地。在网页发生请求时(如果超过当天24小时,则重新生成),把这些参数传到后端,后端利用这些信息生成 UV 统计报告。

PV(Page View)

即页面浏览量或点击量,用户每1次对网站中的每个网页访问均被记录1个PV。用户对同一页面的多次访问,访问量累计,用以衡量网站用户访问的网页数量。

页面停留时间

传统网站
用户在进入 A 页面时,通过后台请求把用户进入页面的时间捎上。过了 10 分钟,用户进入 B 页面,这时后台可以通过接口捎带的参数可以判断出用户在 A 页面停留了 10 分钟。
SPA
可以利用 router 来获取用户停留时间,拿 Vue 举例,通过router.beforeEach``destroyed这两个钩子函数来获取用户停留该路由组件的时间。

浏览深度

通过document.documentElement.scrollTop属性以及屏幕高度,可以判断用户是否浏览完网站内容。

页面跳转来源

通过document.referrer属性,可以知道用户是从哪个网站跳转而来。

小结

通过分析用户数据,我们可以了解到用户的浏览习惯、爱好等等信息,想想真是恐怖,毫无隐私可言。

前端监控部署教程

前面说的都是监控原理,但要实现还是得自己动手写代码。为了避免麻烦,我们可以用现有的工具 sentry 去做这件事。sentry 是一个用 python 写的性能和错误监控工具,你可以使用 sentry 提供的服务(免费功能少),也可以自己部署服务。现在来看一下如何使用 sentry 提供的服务实现监控。

注册账号

打开https://sentry.io/signup/网站,进行注册。选择项目,我选的 Vue。

安装 sentry 依赖

选完项目,下面会有具体的 sentry 依赖安装指南。根据提示,在你的 Vue 项目执行这段代码npm install --save @sentry/browser @sentry/integrations @sentry/tracing,安装 sentry 所需的依赖。再将下面的代码拷到你的main.js,放在new Vue()之前。

import * as Sentry from "@sentry/browser";
import { Vue as VueIntegration } from "@sentry/integrations";
import { Integrations } from "@sentry/tracing";Sentry.init({dsn: "xxxxx", // 这里是你的 dsn 地址,注册完就有integrations: [new VueIntegration({Vue,tracing: true,}),new Integrations.BrowserTracing(),],// We recommend adjusting this value in production, or using tracesSampler// for finer controltracesSampleRate: 1.0,
});

然后点击第一步中的skip this onboarding,进入控制台页面。如果忘了自己的 DSN,请点击左边的菜单栏选择Settings->Projects-> 点击自己的项目 ->Client Keys(DSN)

创建第一个错误

在你的 Vue 项目执行一个打印语句console.log(b)。这时点开 sentry 主页的 issues 一项,可以发现有一个报错信息b is not defined这个报错信息包含了错误的具体信息,还有你的 IP、浏览器信息等等。但奇怪的是,我们的浏览器控制台并没有输出报错信息。这是因为被 sentry 屏蔽了,所以我们需要加上一个选项logErrors: true然后再查看页面,发现控制台也有报错信息了:

上传 sourcemap

一般打包后的代码都是经过压缩的,如果没有 sourcemap,即使有报错信息,你也很难根据提示找到对应的源码在哪。下面来看一下如何上传 sourcemap。首先创建 auth token。这个生成的 token 一会要用到。安装sentry-cli@sentry/webpack-plugin

npm install sentry-cli-binary -g
npm install --save-dev @sentry/webpack-plugin

安装完上面两个插件后,在项目根目录创建一个.sentryclirc文件(不要忘了在.gitignore把这个文件添加上,以免暴露 token),内容如下:

[auth]
token=xxx[defaults]
url=https://sentry.io/
org=woai3c
project=woai3c

把 xxx 替换成刚才生成的 token。org是你的组织名称。project是你的项目名称,根据下面的提示可以找到。在项目下新建vue.config.js文件,把下面的内容填进去:

const SentryWebpackPlugin = require('@sentry/webpack-plugin')const config = {configureWebpack: {plugins: [new SentryWebpackPlugin({include: './dist', // 打包后的目录ignore: ['node_modules', 'vue.config.js', 'babel.config.js'],}),],},
}// 只在生产环境下上传 sourcemap
module.exports = process.env.NODE_ENV == 'production'? config : {}

填完以后,执行npm run build,就可以看到sourcemap的上传结果了。我们再来看一下没上传 sourcemap 和上传之后的报错信息对比。未上传 sourcemap已上传 sourcemap可以看到,上传 sourcemap 后的报错信息更加准确。

切换中文环境和时区

选完刷新即可。

性能监控

打开 performance 选项,就能看到你每个项目的运行情况。具体的参数解释请看文档Performance Monitoring。

性能优化

性能优化主要分为两类:

  1. 加载时优化

  2. 运行时优化

例如压缩文件、使用 CDN 就属于加载时优化;减少 DOM 操作,使用事件委托属于运行时优化。在解决问题之前,必须先找出问题,否则无从下手。所以在做性能优化之前,最好先调查一下网站的加载性能和运行性能。

手动检查

检查加载性能

一个网站加载性能如何主要看白屏时间和首屏时间。

  • 白屏时间:指从输入网址,到页面开始显示内容的时间。

  • 首屏时间:指从输入网址,到页面完全渲染的时间。

将以下脚本放在</head>前面就能获取白屏时间。

<script>new Date() - performance.timing.navigationStart
</script>

首屏时间比较复杂,得考虑有图片和没有图片的情况。如果没有图片,则在window.onload事件里执行new Date() - performance.timing.navigationStart即可获取首屏时间。如果有图片,则要在最后一个在首屏渲染的图片的onload事件里执行new Date() - performance.timing.navigationStart获取首屏时间,实施起来比较复杂,在这里限于篇幅就不说了。

检查运行性能

配合 chrome 的开发者工具,我们可以查看网站在运行时的性能。

打开网站,按 F12 选择 performance,点击左上角的灰色圆点,变成红色就代表开始记录了。这时可以模仿用户使用网站,在使用完毕后,点击 stop,然后你就能看到网站运行期间的性能报告。如果有红色的块,代表有掉帧的情况;如果是绿色,则代表 FPS 很好。

另外,在 performance 标签下,按 ESC 会弹出来一个小框。点击小框左边的三个点,把 rendering 勾出来。

这两个选项,第一个是高亮重绘区域,另一个是显示帧渲染信息。把这两个选项勾上,然后浏览网页,可以实时的看到你网页渲染变化。

利用工具检查

监控工具

可以部署一个前端监控系统来监控网站性能,上一节中讲到的 sentry 就属于这一类。

chrome 工具 Lighthouse

如果你安装了 Chrome 52+ 版本,请按 F12 打开开发者工具。
它不仅会对你网站的性能打分,还会对 SEO 打分。使用 Lighthouse 审查网络应用

如何做性能优化

网上关于性能优化的文章和书籍多不胜数,但有很多优化规则已经过时了。所以我写了一篇性能优化文章前端性能优化 24 条建议(2020),分析总结出了 24 条性能优化建议,强烈推荐。

重构

《重构2》一书中对重构进行了定义:

所谓重构(refactoring)是这样一个过程:在不改变代码外在行为的前提下,对代码做出修改,以改进程序的内部结构。重构是一种经千锤百炼形成的有条不紊的程序整理方法,可以最大限度地减小整理过程中引入错误的概率。本质上说,重构就是在代码写好之后改进它的设计。

重构和性能优化有相同点,也有不同点。

相同的地方是它们都在不改变程序功能的情况下修改代码;不同的地方是重构为了让代码变得更加易读、理解,性能优化则是为了让程序运行得更快。

重构可以一边写代码一边重构,也可以在程序写完后,拿出一段时间专门去做重构。没有说哪个方式更好,视个人情况而定。

如果你专门拿一段时间来做重构,建议你在重构一段代码后,立即进行测试。这样可以避免修改代码太多,在出错时找不到错误点。

重构的原则

  1. 事不过三,三则重构。即不能重复写同样的代码,在这种情况下要去重构。

  2. 如果一段代码让人很难看懂,那就该考虑重构了。

  3. 如果已经理解了代码,但是非常繁琐或者不够好,也可以重构。

  4. 过长的函数,需要重构。

  5. 一个函数最好对应一个功能,如果一个函数被塞入多个功能,那就要对它进行重构了。

重构手法

在《重构2》这本书中,介绍了多达上百个重构手法。但我觉得有两个是比较常用的:

  1. 提取重复代码,封装成函数

  2. 拆分太长或功能太多的函数

提取重复代码,封装成函数

假设有一个查询数据的接口/getUserData?age=17&city=beijing。现在需要做的是把用户数据:{ age: 17, city: 'beijing' }转成 URL 参数的形式:

let result = ''
const keys = Object.keys(data)  // { age: 17, city: 'beijing' }
keys.forEach(key => {result += '&' + key + '=' + data[key]
})result.substr(1) // age=17&city=beijing

如果只有这一个接口需要转换,不封装成函数是没问题的。但如果有多个接口都有这种需求,那就得把它封装成函数了:

function JSON2Params(data) {let result = ''const keys = Object.keys(data)keys.forEach(key => {result += '&' + key + '=' + data[key]})return result.substr(1)
}

拆分太长或功能太多的函数

假设现在有一个注册功能,用伪代码表示:

function register(data) {// 1. 验证用户数据是否合法/*** 验证账号* 验证密码* 验证短信验证码* 验证身份证* 验证邮箱*/// 2. 如果用户上传了头像,则将用户头像转成 base64 码保存/*** 新建 FileReader 对象* 将图片转换成 base64 码*/// 3. 调用注册接口// ...
}

这个函数包含了三个功能,验证、转换、注册。其中验证和转换功能是可以提取出来单独封装成函数的:

function register(data) {// 1. 验证用户数据是否合法// verify()// 2. 如果用户上传了头像,则将用户头像转成 base64 码保存// tobase64()// 3. 调用注册接口// ...
}

如果你对重构有兴趣,强烈推荐你阅读《重构2》这本书。参考资料:

  • 《重构2》

总结

写这篇文章主要是为了对我这一年多工作经验作总结,因为我基本上都在研究前端工程化以及如何提升团队的开发效率。希望这篇文章能帮助一些对前端工程化没有经验的新手,通过这篇文章入门前端工程化。

如果这篇文章对你有帮助,请点一下赞,感激不尽。

求职启事

本人具有三年+前端工作经验,32岁,高中学历,现寻求天津、北京地区的前端工作机会。下面是我掌握的一些技能:

  1. 熟练掌握 HTML、CSS、JavaScript。

  2. 熟练掌握 Vue 全家桶并研究过 Vue1.0 源码及 Vue3.0 部分源码。

  3. 使用 nodejs 写过脚本和个人博客,没有开发过企业应用。

  4. 学习计算机原理并实现一个简单的 cpu 和内存模块运行在模拟器上(github 项目地址)。

  5. 学习操作系统并做实验实现了一个简单的内核(github 项目地址)。

  6. 学习编译原理写过一个简单编译器(github 项目地址)。

  7. 对计算机网络应用层和传输层的知识比较了解。

  8. 数据结构与算法有学习过,还刷了 300+ 道 leetcode,但效果不是很好。

社交网站

  • Github

  • 知乎

如果您觉得我的条件还可以,可以私信我或在评论区留言,谢谢。

❤️爱心三连击1.看到这里了就点个在看支持下吧,你的「点赞,在看」是我创作的动力。
2.关注公众号程序员成长指北,回复「1」加入Node进阶交流群!「在这里有好多 Node 开发者,会讨论 Node 知识,互相学习」!
3.也可添加微信【ikoala520】,一起成长。“在看转发”是最大的支持

手把手带你入门前端工程化——超详细教程相关推荐

  1. 手把手带你入门前端工程化——超详细教程(高级前端必备)

    本文将分成以下 7 个小节: 技术选型 统一规范 测试 部署 监控 性能优化 重构 部分小节提供了非常详细的实战教程,让大家动手实践. 另外我还写了一个前端工程化 demo 放在 github 上.这 ...

  2. 手把手从零开始搭建k8s集群超详细教程

    本教程根据B站课程云原生Java架构师的第一课K8s+Docker+KubeSphere+DevOps同步所做笔记教程 k8s集群搭建超详细教程 1. 基本环境搭建 1. 创建私有网络 2. 创建服务 ...

  3. Webpack5快速入门 (二) CSS相关loader的使用及兼容性处理 手把手带你打开前端工程化的大门

    目录 一.CSS-Loader

  4. linux 中国-新手村,从新手村开始,手把手带你入门梳理内核代码

    原标题:从新手村开始,手把手带你入门梳理内核代码 在上一期内容中,Java离Linux内核有多远? 我们介绍了从 JVM 到内核的编译原理,告诉大家应用和系统工程师如何接触到内核. 本文将 从一个简单 ...

  5. 手把手带你入门深度学习(一):保姆级Anaconda和PyTorch环境配置指南

    手把手带你入门深度学习(一):保姆级Anaconda和PyTorch环境配置指南 一. 前言和准备工作 1.1 python.anaconda和pytorch的关系 二. Anconda安装 2.1 ...

  6. Java并发编程(中下篇)从入门到深入 超详细笔记

    接上一篇博客笔记:Java并发编程(中上篇)从入门到深入 超详细笔记_未来很长,别只看眼前的博客-CSDN博客https://blog.csdn.net/weixin_53142722/article ...

  7. RPA之家手把手带你入门Blue Prism教程系列7_深入了解Data Item

    RPA之家手把手带你入门Blue Prism 1. Data Item类型 2. Data Item的表现形式 2.1 Environment Variable(环境变量) 2.2 Session V ...

  8. RPA之家手把手带你入门Blue Prism教程系列8_创建并运行一段Process

    RPA之家手把手带你入门Blue Prism 1. 创建一个新Process 2. 新建Data Item & Calculation Stage 运行Process -本文章由RPA之家(r ...

  9. 教你快速入门ElasticSearch,超详细简单~

    教你快速入门ElasticSearch,超详细简单~ 一. 初探ElasticSearch 1.1 什么是ElasticSearch? ElasticSearch,简称为ES,它是一个开源的高扩展的分 ...

最新文章

  1. idea下,Jetty采用main方法启动web项目
  2. 《Go并发编程实战》第2版 紧跟Go的1.8版本
  3. 3亿美元,腾讯第三次领投行业AI独角兽明略,这次还有淡马锡领投,快手跟投...
  4. 2021-03-20 数据挖掘算法—SVM算法 python
  5. Angular 页面元素的DOM级别的删除过程
  6. 朴素贝叶斯--文档分类
  7. QT SQL使用指南
  8. IBM发布人工智能显微镜,可检测海洋水资源
  9. su必备插件_会了这10个插件,Sketchup也能玩飞起来!
  10. 2021.10.07高一Python语法入门课堂检测题解
  11. 如何从Oracle官网上下载JDK
  12. html高难度拼图,张馨月婚后生活太悠闲,宅家挑战高难度拼图
  13. 【FinalIK】Full Body Biped IK
  14. c++虚指针及相应对象的数据结构
  15. 「镁客早报」苹果将在德国停售iPhone 7&8;“刘强东事件”正式结案...
  16. js 根据日期转换星期
  17. Android软键盘工具类
  18. 推荐3个网页设计在线配色网站
  19. MySQL数据库——数据库逻辑结构设计阶段(非常重要)
  20. mysql选取除某一列的所有行_在 MySQL 中选择除了某一列以外的所有列

热门文章

  1. 北京供销大数据集团构建高效、安全、可靠的银行灾备体系
  2. 软考信管高级——人力资源管理
  3. zte怎么开虚拟服务器,zte路由器设置虚拟服务器
  4. 【第44天】Servlet初学---编写最基本的Servlet工程的必要步骤
  5. Maltab GUI课程设计——贪吃蛇小游戏
  6. IxChariot测试路由LAN到WAN和WAN到LAN吞吐量操作指南
  7. WIN10系统右下角网络连接图标消失解决
  8. 理解逻辑回归中的ROC曲线和KS值
  9. java类名可以是数字吗_在 Java 中,一个类可同时定义许多同名的方法,这些方法的形式参数的个数、类型或顺序各不相同,传回的值也可以不相同。这种面向对象程序特性称为( )。_学小易找答案...
  10. 多约束的MPC ACC模型预测ACC设计 自适应巡航