0%

image.png

起因

最近在项目线上环境中收集到客户反馈数值输入后展示值少了0.01,收到反馈后立刻定位代码中问题所在,原因就是上一个版本中此数值输入框改成非四舍五入截取两位小数,使用的方法是

1
Math.floor(floatNum*100)/100

这个看似没有什么问题的方法当floatNum的值为1.15时,就出现了 Js 浮点数精度引起的异常情况,看下图。

image.png

可以看到 1.15*100 在 Js 中计算结果并非为 115而是114.99999999999999,由此通过 Math.floor()向下取整并除以100时得到的最终结果是1.14,与预期结果正好相差0.01

定位到问题所在后立刻进行修复,决定抛弃的浮点数计算的方式,使用转换成字符串截取的方案。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 保留小数(不四舍五入)
* @param floatNum 输入值
* @param decimal 保留浮点数精度位数
*/
formatDecimal(floatNum, decimal) {
let result = floatNum.toString();
const index = result.indexOf('.');
if (index !== -1) {
result = result.substring(0, decimal + index + 1);
} else {
result = result.substring(0);
}
return parseFloat(result);
}

在问题解决后,决定对 Js 浮点数进行梳理,并列举一些可能遇到的精度问题。

Js 浮点数

标准与储存方式

Js 没有单独的浮点型,浮点数与整数都是通过 Number 类型表示,是遵循 IEEE754标准的 64 位双精度值,在二进制中 64 位由三个部分组成。

  • sign bit(符号S):用来表示正负号,0代表正数,1代表负数

  • exponent(指数E):用来表示次方数,中间11位

  • mantissa(尾数M):用来表示精确度, 超出的部分自动进一舍零,最后52位

image.png

在二进制的科学记数法中,IEEE754标准的 64 位双精度值数字被公式表示为:

image.png

在十进制的科学记数法中 1 <= M < 10,同理在二进制科学记数法中 1 <= M < 2,所以 M 的整数部分只能是 1, 可以舍去,M 只存储后面小数部分。指数 E 是 11 位,在大能表示的数是 2047 (2^11 - 1),由于指数包含正与负,所以取中间值 1023 临界值,[1024, 2047] 表示正指数,[0, 1022] 表示负指数。

以 4.5 为例转换成二进制是 100.1,用二进制科学记数法表示就是 1.001 * 2^2。 代入公式 1.001 舍去 1 尾数 M 是 001,指数 E 是 2 + 1023 = 1025 (二进制 10000000001),正数 S 取 0,所以组合在一起 4.5 在 IEEE754 双精度64位标准表示如下。

image.png
图片源自于binaryconvert

哪些常见的精度问题及背后原理

明白了 Js 中浮点数的标准与储存方式,再回到实际场景中看看有哪些常见的浮点数精度问题极其原因。

场景一 0.1 + 0.2 != 0.3

0.1、0.2 以及相加的和的二进制表示如下

1
2
3
0.1 -> 0.0001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1010 (1001循环 进一舍零)
0.2 -> 0.0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 010 (0011循环 进一舍零)
相加 -> 0.0100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 111

相加所得的值转换成十进制恰好为 0.30000000000000004

(十进制小数转化为二进制小数计算方式)

(十进制小数转化为二进制小数计算工具)

场景二 (1.335).toFixed(2) == 1.33

浮点数精度和 toFixed 其实属于同一类问题,都是由于浮点数无法精确表示引起的,如下:

1
(1.335).toPrecision(20);    // "1.3349999999999999645"

toFixed() 方法实则是对 1.3349999999999999645 四舍五入保留两位小数

场景三 大数问题61453901951867050 + 5 == 61453901951867060

因为 Javascript 的数字存储使用了 IEEE 754 中规定的双精度浮点数数据类型,而这一数据类型能够安全存储 (2^53 - 1) 到 -(2^53 - 1) 之间的数值(包含边界值)。

1
2
Number.MAX_SAFE_INTEGER === Math.pow(2, 53) - 1 // true 最大安全整数
Number.MIN_SAFE_INTEGER === -(Math.pow(2, 53) - 1) // true 最小安全整数

这里安全存储的意思是指能够准确区分两个不相同的值,例如:

1
Number.MAX_SAFE_INTEGER + 1 === Number.MAX_SAFE_INTEGER + 2 // true

将得到 true 的结果,而这在数学上是错误的
因为遵循 IEEE754 标准 JavaScript 中最大的安全整数 Number.MAX_SAFE_INTEGER

在场景三中,其数值 61453901951867050 超过最大安全整数,导致相加结果不准确。

1
61453901951867050 > Number.MAX_SAFE_INTEGER // true

解决精度问题的方案

假如我们要判断 0.1 + 0.2 是否等于 0.3

可以判断两边之差的绝对值是否小于 Number.EPSILON(浮点数之间最小差,如下:

1
Math.abs(0.1 + 0.2 - 0.3) < Number.EPSILON // true

若小于 Number.EPSILON 则认为等式成立。

但这种方式只能判断两边等式是否成立,如需要计算浮点数的精确计算值还需要利用其他方法。

目前社区已经有了很多较为成熟的库,比如 bignumber.jsdecimal.js,以及 big.js 等。我们可以根据自己的需求来选择对应的工具。这些库不仅解决了浮点数的运算精度问题,还支持了大数运算,并且修复了原生 toFixed 结果不准确的问题。

结语

感谢阅读,若有不足,欢迎指正。

提前祝 XDM 1024 节快乐~

参考文献

Wikipedia-IEEE754

老生常谈之js浮点数精度

JS中浮点数精度问题

分支类型

  • mater 分支:主分支也是保护分支,用于部署生产环境和预发环境,只能由 release 分支、hotfix 分支合并,为保证主分支的稳定性任何人都无法在此分支直接提交代码。

  • develop 分支:开发分支同样也是保护分支,保持最新开发完成代码的同步。

  • feature 分支:新功能的开发分支,基于 develop 分支创建,功能开发完成后合并入 develop 分支,命名格式 feature/module_user_date

  • release 分支:提测分支,基于 develop 分支创建,测试过程中的bug修复均在此分支提交,测试完成后合入 master 分支,命名格式 release/module_user_date

  • hotfix 分支:紧急修复分支,基于 master 分支创建,用于修复线上紧急 bug 和 开发紧急需求,完成后合入 master 分支,命名格式 hotfix/module_user_date

部署方式

  • 联调环境:云效流水线点击运行选择远程分支部署

  • 测试环境:云效流水线点击运行选择远程分支部署

  • 预发环境:监听远程仓库 master 分支更新,自动触发部署

  • 生产环境:监听远程仓库 master 分支更新,并且需要云效流水线点击确认部署

  • 演示环境:监听远程仓库 master 分支更新,并且需要云效流水线点击确认部署

开发流程

正常排期流程

开发阶段

从 develop 分支检出 feature/module_user_date 功能分支进行开发,
开发完成后功能分支 merge 合入 develop 分支

提测阶段

从 develop 分支检出 release/module_user_date 提测分支
提测过程中在 release/module_user_date 分支提交代码
更新代码后通过云效流水线点击运行选择分支部署测试环境

预发阶段

将测试通过的 release/module_user_date 提测分支 merge 合入 master分支
自动触发预发环境部署
此时修复 bug 从 master 分支检出 hotfix/module_user_date 修复分支
修复后合回 master 分支

生产阶段

验收后的版本通过云效流水线手动触发生产环境、演示环境部署
将 master 分支合入 development 分支
并且删除远程仓库 feature/module_user_date、release/module_user_date、hotfix/module_user_date 三类分支

END

紧急需求/紧急修复流程

开发阶段

从 master 分支检出 hotfix/module_user_date 紧急功能分支进行开发

提测阶段

将多个 hotfix/module_user_date 分支合并成一个
提测过程中在 hotfix/module_user_date 分支提交代码
更新代码后通过云效流水线手动选择分支部署测试环境

预发阶段

将测试通过的 hotfix/module_user_date 提测分支 merge 合入 master分支
自动触发预发环境部署
此时修复 bug 从 master 分支检出 hotfix/module_user_date 修复分支
修复后合回 master 分支

生产阶段

验收后的版本通过云效流水线手动触发生产环境、演示环境部署
将 master 分支合入 development 分支
并且删除远程仓库 hotfix/module_user_date 分支

END

关于 Protractor

介绍 Protractor之前需要先介绍一下端到端测试(E2E测试),端到端测试验证应用中的所有层。这不仅包括你的前端代码,还包括所有相关的后端服务和基础设施,它们更能代表你的用户所处的环境。通过测试用户操作如何影响应用,端到端测试通常是提高应用是否正常运行的信心的关键。
Protractor 是 Angular 和 AngularJS 应用程序的端到端测试框架,针对在真实浏览器中运行的应用程序运行测试,并与用户进行交互,具有以下特性。

模拟用户真实测试流程

Protractor 建立在 WebDriverJS 之上,该 WebDriverJS 使用本机事件和特定于浏览器的驱动程序来与用户的应用程序进行交互。

Angular 支持友好

Protractor 支持特定于Angular的定位器策略,该策略使你无需进行任何设置即可测试特定于Angular的元素。

自动等待

不再需要为测试添加等待和休眠,Protractor 可以在网页完成待处理的任务后自动执行测试的下一步,因此你不必担心等待测试和网页同步。

安装配置

安装 Node

Protractor 是一个 Node.js 项目,你需要先安装 Node 环境,这个前端的小伙伴相信都有,这里也还是贴一下 Node 安装教程

安装 Jdk

由于官方文档教程将运行本地独立的 Selenium 服务器来控制浏览器进行测试,你将需要安装 Java 开发工具包(JDK)才能运行独立的Selenium 服务器。这个是 Jdk安装教程 , 安装完成后可以通过从命令行运行 java -version 进行检查是否生效。

安装 Protractor

完成基础环境的配置,接下来就可以全局安装 Protractor 框架主体

1
npm install -g protractor

这个命令会安装两个工具, Protractorwebdriver-manager,安装后使用命令行 protractor --version检测是否生效。
webdriver-manager 是一个辅助工具,可轻松获取正在运行的 Selenium Server 实例。你需要通过以下命令下载必要的二进制文件:

1
webdriver-manager update

现在就可以启动服务

1
webdriver-manager start

这将启动 Selenium 服务器并输出一堆信息日志, 你的 Protractor 测试将向该服务器发送请求以控制本地浏览器。保持该服务器运行,你可以在 http://localhost:4444/wd/hub 上查看有关服务器状态的信息。

官方用例

创建一个空的项目文件夹,并在其中创建两个文件, 一个测试代码文件spec.js,一个配置文件 conf.js
让我们从一个简单的测试开始,该测试跳转到示例 AngularJS 应用程序并检查其标题,这是示例“超级计算器”测试页面地址:http://juliemr.github.io/protractor-demo/

1
2
3
4
5
6
7
8
// spec.js
describe('Protractor Demo App', function() {
it('should have a title', function() {
browser.get('http://juliemr.github.io/protractor-demo/');

expect(browser.getTitle()).toEqual('Super Calculator');
});
});

describeit 语法来自 Jasmine 框架, browser是 Protractor 创建的全局变量,用于执行浏览器级别的命令,例如使用browser.get 进行导航。

1
2
3
4
5
6
// conf.js
exports.config = {
framework: 'jasmine',
seleniumAddress: 'http://localhost:4444/wd/hub',
specs: ['spec.js']
}

这个配置是告诉 Protractor 选用的测试框架(framewrok)、测试文件的名称(spec)以及 selenuim server 地址(selenuimAddress)。它指定我们将使用 Jasmine 测试框架,对所有其他配置使用默认设置,Chrome是默认浏览器。

运行 Protractor 进行测试

conf.js```
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
你应该会看到一个Chrome浏览器窗口打开并导航到“计算器”,然后自行关闭。测试输出应为1个测试,1个断言,0个失败。恭喜,你已经进行了首次 protractor 测试!


# 语法解析
网页的端到端测试的核心是查找 DOM 元素,与之交互以及获取有关应用程序当前状态的信息,以下是如何使用 Protractor 在 DOM 元素上定位和执行操作的基本语法。
## 定位器(Locators)
```javascript
// 通过 class 查找元素
by.css('.myclass')

// 通过 id 查找元素
by.id('myid')

// 通过 name 查找元素
by.name('field_name')

// 通过 ng-model 查找元素
// 请注意,目前仅AngularJS应用支持此功能
by.model('name')

// 查找绑定到给定变量的元素
// 请注意,目前仅AngularJS应用支持此功能
by.binding('bindingname')

定位器传递给 element 函数如下:

1
2
3
element(by.css('some-css'));
element(by.model('item.name'));
element(by.binding('item.name'));

使用 CSS 选择器作为定位器时,可以使用$()快捷方式表示法:

1
2
3
4
5
$('my-css');

// 这相当于:

element(by.css('my-css'));

触发事件(Action)

element()函数返回一个ElementFinder对象, ElementFinder知道如何使用您作为参数传入的定位器来定位DOM元素,但实际上尚未这样做,在调用动作方法之前它不会与浏览器联系。 以下是最常见的触发方法:

单个元素

1
2
3
4
5
6
7
8
9
10
11
12
13
var el = element(locator);

// 单击元素
el.click();

// 将键值输入到元素(通常是 Input 框)。
el.sendKeys('my text');

// 清除元素的值 (通常是 Input 框).
el.clear();

// 获取 attribute 的值, 例如获取 Input 的 value
el.getAttribute('value');

由于所有动作都是异步的,因此所有动作方法都返回一个Promise,因此要记录元素的文本,可以执行以下操作:

1
2
3
4
var el = element(locator);
el.getText().then(function(text) {
console.log(text);
});

WebFinder 上 WebDriverJS 中可用的任何操作都可以在 ElementFinder 中使用,查看完整清单

元素列表

要处理多个DOM元素,使用element.all函数,定位器作为其唯一参数。

1
2
3
element.all(by.css('.selector')).then(function(elements) {
// 查找器获取的是数组元素
});

element.all() 具有几个辅助方法:

1
2
3
4
5
6
7
8
9
// 元素的数量
element.all(locator).count();

// 根据 Index 获取元素
element.all(locator).get(index);

// 第一个和最后一个元素
element.all(locator).first();
element.all(locator).last();

将 CSS 选择器用作定位器时,可以使用快捷方式 $$()表示法:

1
2
3
4
5
$$('.selector');

// 这相当于:

element.all(by.css('.selector'));

子元素

要查找子元素,只需将 element 和 element.all 函数链接在一起,如下所示:

使用单个定位器查找:

一个元素:

  • element(by.css('some-css'));
    元素列表:

  • element.all(by.css('some-css'));
    使用链接的定位器查找:

子元素:

  • element(by.css('some-css')).element(by.tagName('tag-within-css'));
    查找子元素列表:

  • element(by.css('some-css')).all(by.tagName('tag-within-css'));
    也可以使用 get / first / last 进行链接,如下所示:

    1
    2
    3
    element.all(by.css('some-css')).first().element(by.tagName('tag-within-css'));
    element.all(by.css('some-css')).get(index).element(by.tagName('tag-within-css'));
    element.all(by.css('some-css')).first().all(by.tagName('tag-within-css'));

背景

随着业务的不断迭代,项目日渐壮大,为了给用户提供更优的体验,前端优化是我们避不开的话题。一个优秀的网站必然是拥有丰富功能的同时具有比较块的响应速度,想必我们浏览网页时都更喜欢丝般顺滑的感受。
作为前端工程师来说,项目优化主要关注一下几点:白屏时间、首屏时间、整页时间、DNS时间、CPU占用率。就我们目前项目而言采用 Angular 开发,首屏加载时间是 9.2 秒,这个速度对用户而言大大降低了使用体验,通过本次优化将首屏加载压缩至 3.6 秒,接下来将逐步阐述本次优化的过程。

PageSpeed Insights定位问题

其实早在之前就有对项目首页优化的想法,因为前端优化这个概念相对庞大,范围涉及比较广,往往对着项目无从下手。我也是在优化之前阅读并借鉴了很多优秀的文章,最终找到了一个非常好用的工具-PageSpeed Insights for Chrome。这是一个谷歌浏览器插件,可以用来定位网页存在的性能问题,安装好把它加入到 DevTools 就可以开始愉快的找问题了。

以上图为例可看到 PageSpeed Insights 会把网页存在待优化的问题分类列出,并且每个类目下可以定位到具体的文件,这样一来原本很抽象的前端优化就很形象得呈现在我们眼前了。优化的方向主要分为代码压缩、浏览器缓存、静态资源压缩,方向明确了就可以开始围绕问题开始动手改造。

前端优化总的来说还是离不开那句话,减少请求文件数量同时减小请求文件体积。

代码压缩

结合前端工程化思想,我姑且把代码压缩分为两大类,一类是项目使用打包工具构建时文件的的压缩,例如 Webpack 各种压缩 Plugin 的配置,另一类是对前端服务器传输响应的压缩配置,例如 Nginx 的 Gzip。

Webpack 配置

由于 Angular-Cli 集成了内部集成了 Webpack,并且重新进行了封装,所以在 Angular 6.0+ 项目中,想要优化或者更改Webpack打包配置需要通过 Angular-Builders,这里需要我们单独安装包。当然如果是直接用Webpack打包的项目就可以直接省略这一步。

1
2
npm install @angular-builders/custom-webpack --save-dev
npm install @angular-devkit/build-angular --save-dev

不需要再单独安装 webpack 和 webpack-dev-server,因为这两个是 @angular-devkit/build-angular 的依赖包,在安装 @angular-devkit/build-angular 会自动安装 webpack 和 webpack-dev-server。

安装完成后要更改 angular.json 文件配置,把我们之后要添加的 webpack 配置,加在运行(serve)和编译(build)命令里,那么关键的配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
"architect": {
......
"build": {
"builder": "@angular-builders/custom-webpack:browser",
"options": {
"customWebpackConfig": {
"path": "./webpack.config.js"
}
}
},
"serve": {
"builder": "@angular-builders/custom-webpack:dev-server",
"options": {
"customWebpackConfig": {
"path": "./webpack.config.js"
}
}
}
}

接着需要在根目录下创建配置文件,我这里把它命名为 webpack.config.js。到这一步链接 Angular和自定义 Webpack 配置的准备工作已经就位了,现在我们可以把需要的Webpack配置写进新建的文件中。
引入压缩插件,分别对 Js、Html、Css 进行压缩,Js压缩:UglifyJsPlugin,Html压缩HtmlWebpackPlugin,提取公共资源:CommonsChunkPlugin,提取css并压缩:ExtractTextPlugin

配置Nginx压缩

因为我们的前端项目都是通过 Nginx 部署,所以我们还需要在 Nginx 上开启 Gzip 传输压缩,这能使传输的打包文件体积压缩至原来的1/4左右,效果非常明显。

1
2
gzip on;
gzip_types text/plain application/javascriptapplication/x-javascripttext/css application/xml text/javascriptapplication/x-httpd-php application/vnd.ms-fontobject font/ttf font/opentype font/x-woff image/svg+xml;

配置好重新启动Nginx,当看到请求响应头中有 Content-Encoding: gzip,说明传输压缩配置已经生效,此时可以看到我们请求文件的大小已经压缩很多。

静态资源压缩

使用雪碧图

雪碧图的作用就是减少请求数,而且多张图片合在一起后的体积会少于多张图片的体积总和,这也是比较通用的图片压缩方案,推荐一款雪碧图生成工具
sprite-generator

使用WebP格式图片

WebP 是 Google 团队开发的加快图片加载速度的图片格式,其优势体现在它具有更优的图像数据压缩算法,能带来更小的图片体积,而且拥有肉眼识别无差异的图像质量,同时具备了无损和有损的压缩模式、Alpha 透明以及动画的特性,在 JPEG 和 PNG 上的转化效果都相当优秀、稳定和统一。
同样推荐一个生成WebP格式的工具
又拍云WebP

使用 CDN 加载

内容分发网络是指一种透过互联网互相连接的电脑网络系统,利用最靠近每位用户的服务器,更快、更可靠地将音乐、图片、视频、应用程序及其他文件发送给用户,来提供高性能、可扩展性及低成本的网络内容传递给用户。
把项目中引用的外部资源包通过 CDN 方式引入,以来能减少项目打包文件体积,二来能提高资源请求速度,一举两得。

页面渲染性能优化

减少重排重绘

以下三种情况会导致页面重新渲染

  • 修改DOM
  • 修改样式
  • 用户事件
    重新渲染就是重新布局和重新绘制,前者叫重排(reflow),后者叫重绘(repaint)。
    重排和重绘会影响页面性能以及用户使用体验,所以需要尽量降低重排和重绘的频率,需要注意以下几个方面。
  • 多个 DOM 的读操作尽量写在一起,不要在连续读操作中添加写操作。
  • 如果样式是重排得到的,尽量缓存样式。
  • 不要单一改变样式,最好通过 Class 统一改变。
  • 尽量使用离线 DOM 来改变样式,完成后再将对象写入。
  • 先将元素置为display: none,再对节点进行操作。
  • position 为 absolutefixed 的元素重排开销比较下,因为不考虑其他元素的影响。
  • display:none 的节点不会被加入Render Tree, 而 visibility:hidden则会,所以,如果某个节点最开始是不显示的,设为display:none 是更优的。

避免CSS、JS阻塞

谈论资源的阻塞时,我们要清楚,现代浏览器总是并行加载资源。例如,当 HTML 解析器被脚本阻塞时,解析器虽然会停止构建 DOM,但仍会识别该脚本后面的资源,并进行预加载。
默认情况下,CSS 被视为阻塞渲染的资源,这意味着浏览器不会渲染任何已处理的内容,直至 CSSOM 构建完毕
Javascript 不仅可以读取和修改DOM 属性,还可以读取和修改CSSOM 属性
存在阻塞的 CSS 资源时, 浏览器会延迟javascript 的执行和 Render Tree 构建。
当浏览器遇到一个script标记时,DOM 构建将暂停,直至脚本完成执行。
javascript 可以查询和修改 DOM 与 CSSOM
CSSOM 构建时,javascript 执行将暂停,直至 CSSOM 就绪。
所以,script 标签的位置很重要。实际使用时,可以遵循下面2个原则

  • CSS 资源优于 JS 资源引入
  • JS 应尽量少影响 DOM 的构建

改变 js 阻塞的方式有 defer 和 async
<script defer></script>
defer 方式加载 script, 不会阻塞 HTML 解析,等到 DOM 生成完毕且 script 加载完毕再执行 Js。
<script async></script>
async 属性表示异步执行引入的 JS,加载时不会阻塞 HTML解析,但是加载完成后立马执行,此时仍然会阻塞 load 事件。

总结

前端优化是一个长期的工程,以上也只是部分优化的内容与方向,未来也会保持对前端优化的关注,通过更全面的维度提高项目性能。

前言

仔细算来从事前端也有两年半之久,时间好快,并且感觉越来越快,因此也常常出现焦虑。这就像查理·芒格老爷子所说

人们总是担心时间过去得太快,而自己又聪明得太慢

生活在如此快节奏的环境中,特别是在一线城市竞争异常激烈(杭州在今年荣升为新一线城市),在拼搏的同时当然也需要学会慢下来思考与总结。一直以来也没有好好把年度总结以文字得形式记录下来,那就从今年开始在这里记录下自己的经历和成长。

“黑天鹅”事件

过去一年多发生了几件比较重大的事,2019年末,在我进入社会初期,当时所在行业经历了比较大得动荡,遇到了职业生涯的一次“黑天鹅”事件。当时那段时间内心也比较郁闷,但是有一句话怎么说来着,当你从低谷中走出来来的每一步都会是你未来宝贵的经验,之后我也尽快调整好了自己的状态。2020年初,新冠疫情席卷全球,这样全球性的“黑天鹅”事件,对全世界都是极大的考验,但每一个地方都在积极抗疫,我在那时报名成为社区志愿者,输出了一份绵薄之力。
经历了社会中这些的洗礼,我也有了更深刻得认知,生活中总会充斥着一些不可抗拒的“黑天鹅”,我们能做的只有不断提高自己的抗风险能力,以保证有足够的勇气去拥抱变化。

这一年做了什么

新的平台

经过一段时间的沉淀与尝试,我也开始迎接新的挑战。回望这一段时间,自己也是有很多收获,对前端的知识体系进行了系统得梳理与巩固,找到了自己得不足,让我在接下来的一年有更针对性的方案去学习补充。

尝试写文章记录自己

这一年中我尝试分享自己的成长轨迹,写文章确实是一种很好的总结方式,在这个过程中能够对所学知识进行复盘,每次看到自己写的文字还是会有小小的成就感。目前写的文章主要还是以自己记录为主,以后希望能够输出一些更高质量的文章,能够给他人分享有用的信息会是一件很有意义的事。

拥有人生中第一辆车

这一年中,我拥有了人生中第一辆车,在周末带家人出去聚个餐,节假日约上三五好友来一场说走就走的自驾游,这些日常小事能给生活增添许多幸福感。但当我还沉浸在提车的喜悦之时,发现周围很多朋友都已经开始买房,大家都步履不停,自己也得迎头赶上才行。

成长与收获

推动团队发展

跟过去相比,今年最大的改变就是从跟随团队一起发展,到如今推动团队共同进步。当我加入公司的第一天起,我就给自己定位为一名推动者,希望自己的加入不单单是分担一份工作量,而是能成为为团队赋能的角色。如何在工作中体现价值?这是疫情期间经常思考得一个问题,我想能够推动团队和产品变得更有价值,那就是自我价值最好得佐证。制定好目标是第一步,更重要得是执行,在新团队的一年中除了日常业务迭代之外,我还主导推动了一些优化。

制定Git使用规范

我加入团队时正值快速扩张时期,发现前端代码库中Commit MessageGit分支管理没有一个严格的规范,伴随着人数增加会必然会影响 Code Review 和版本分支管理效率,于是我推动团队使用 Angular 的 Commit Message 规范,并且优化的分支管理,保证团队能多任务线并行开发使用 Git 仓库,也提高了代码仓库安全性和可维护性。

引入 Prettier 和 Eslint 规范代码格式

在统一 Git 使用格式之后,编码规范得统一也得跟上,在提升个人的编码习惯同时还能给之后重构代码的人提供极大得便利, 毕竟作为开发者谁都不希望某一天接盘一堆难以阅读的代码,这一点应该能引起很多共鸣吧。于是我在项目中引入 Prettier 和 Eslint 配合使用约定编码规范,通过 husky 监听 git commit 自动格式代码以及 ts 语法检测,以此保证每次提交到远程仓库的代码都是符合规范的。

首页加载时间优化

在面向的客户的 WEB 项目中,页面加载渲染的时间直接影响到产品的使用体验,期间也对项目首页加载时间进行了一次优化。通过静态资源优化(使用 WebP 格式图片以及雪碧图)、Nginx 服务器开启 Gzip 压缩、使用浏览器缓存、使用 CDN 加载等方式,将页面首次加载时间从9.3秒提升至3.5秒。这里推荐一个好用的页面性能检测 Chrome 插件 PageSpeed,当前端优化无从下手时,这个工具可以帮助我们检测页面性能还存在哪些不足,然后就可以按提示一步步优化。

接入ARMS前端监控

伴随着项目的成长,对线上环境得代码运行状况监控也是必要的,能够帮助我们提前发现问题并及时修复,保证线上环境的健康运行。我选择在项目中接入阿里云团队的 ARMS 前端监控,实时可视化监控多环境JS异常信息、页面加载效率、API接口调用信息,并且接入钉钉机器人接收异常警报,保证团队能够第一时间接收到报警信息。

拓宽自己的视野

平时也常常会有焦虑,今年买了一些书籍送给自己,希望通过跟伟大得灵魂进行碰撞,能拓宽视野、提升认知。在阅读《穷查理宝典》时,查理的思维很多次让我感觉醍醐灌顶,价值投资的魅力也深深吸引我。在休闲放松时,最近也爱看《圆桌派》这个节目,里面前辈们探讨的话题往往就是我们生活中困扰着我们的事,他们的经验也能够为我提供一个新的角度。

强大的内心也需要强健得体魄

要说今年比较满意的事,那就是号召四位好友开始一起健身。以前也办过健身卡,但都没能坚持下来,这一次从十月份开始约定每周三天健身日,风雨无阻。每次想偷懒的时候就在内心狠狠骂自己,所以至今为止还没有缺卡记录,希望能够一直保持下去。

给2021的自己定几个小目标

  • 每月发表一篇技术文章
  • 看完六本新书
  • 坚持一周三次健身
    目标不大,定要好好执行
    俗话说流水不争先,争得是滔滔不绝

未来可期

去坚持自己认可的事
去争取自己想要的东西
2021 共勉!

为什么要引入前端监控?

用户访问业务时,整个访问过程大致可以分为三个阶段:页面生产时(服务器端状态)、页面加载时和页面运行时。为了保证线上业务稳定运行,我们会在服务器端对业务的运行状态进行各种监控。现有的服务器端监控系统相对已经很成熟,而页面加载和页面运行时的状态监控一直比较欠缺。例如:

  • 无法第一时间获知用户访问您的站点时遇到的错误。
  • 各个国家、各个地区的用户访问您的站点的真实速度未知。
  • 每个应用内有大量的异步数据调用,而它们的性能、成功率都是未知的。
    随着前端项目工程化得发展,我们对前端项目的性能要求也越来越高,如果没有引入前端监控,团队对前端线上环境的运行状况是很抽象的。在之前的工作中,经常遇到用户在使用业务得过程中出现异常,此时无法很快得定位并复现用户遇到的问题,导致在维护中消耗了较大得工作量。如果有前端监控得存在我们就可以快速定位到异常并做出修复,能够大幅提升工作效率以及用户体验。

选择一款优秀的前端监控

挑选前端监控之前,首先确定项目对监控得需求,在我们项目中需求大致分为几点:

  • JS异常信息统计
  • 页面加载效率分析
  • API接口调用信息统计
  • 监控可视化呈现
    针对这些需求,就可以有目标性地挑选一款符合项目的前端监控。目前开源的Sentry和阿里云团队的ARMS都是比较全面的监控,基本满足我们项目的需求。

    Sentry

    正如其名「哨兵」,可以实时监控生产环境上的系统运行状态,一旦发生异常会第一时间把报错的路由路径、错误所在文件等详细信息以邮件形式通知我们,并且利用错误信息的堆栈跟踪快速定位到需要处理的问题。

优点:开源对各种前端框架的友好支持 (Vue、React、Angular)支持 SourceMap。Sentry 官方提供的免费服务有次数限制,达到一定限制后继续使用就需要收费,但是我们可以利用 Sentry 的开源库在自己的服务器上搭建服务,官方已经提供了完善的操作文档。

ARMS

ARMS前端监控专注于对Web场景、Weex场景和小程序场景的监控,从页面打开速度(测速)、页面稳定性(JS Error)和外部服务调用成功率(API)这三个方面监测Web和小程序页面的健康度。

优点:重点监控页面的加载过程和运行时状态,同时将页面加载性能、运行时异常以及API调用状态和耗时等数据,上报到日志服务器。之后借助ARMS提供的海量实时日志分析和处理服务,对当前线上所有真实用户的访问情况进行监控。最后通过直观的报表展示。

可以看出Sentry更加专注于异常信息处理,ARMS则从更多的维度统计系统信息,并且通过丰富得可视化呈现,由此看来ARMS在这个项目中是更优得选择。

Angular项目接入ARMS监控

在选定监控框架后,就要开始着手接入项目了,ARMS文档有两种接入方式提供给我们选择:

cdh方式为Web应用安装ARMS前端监控探针

cdh的方式比较便捷,在阿里云后台创建好项目后,直接复制ARMS提供的<script>标签中代码至根页面的<body>标签中即可生效。cdh方式又分为异步加载和同步加载。

  • 异步加载:又称为非阻塞加载,表示浏览器在下载执行JS之后还会继续处理后续页面。若对页面性能的要求非常高,建议使用此方式。
  • 同步加载:又称为阻塞加载,表示当前JS加载完毕后才会进行后续处理。如需捕捉从页面打开到关闭的整个过程中的JS错误和资源加载错误,建议cdh引入使用此方式。

npm方式为Web应用安装ARMS前端监控探针

我们项目中选择npm方式安装探针

在npm仓库中安装alife-logger

1
npm install alife-logger --save

SDK以BrowserLogger.singleton方式初始化

arms.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import BrowserLogger from 'alife-logger';
import { environment } from 'src/environments/environment';

/**
* ARMS前端监控
*/
let logger = null;
let environmentType = '';

switch (environment.type) {
case 'dev':
environmentType = 'daily';
break;
case 'local':
environmentType = 'local';
break;
case 'pre':
environmentType = 'pre';
break;
case 'rep':
environmentType = 'gray';
break;
case 'prod':
environmentType = 'prod';
break;
default:
break;
}

try {
if (environment.type !== 'local') {
logger = BrowserLogger.singleton({
pid: 'd465464j9@4caac5459*****', // 项目唯一ID,由ARMS在创建站点时自动生成
appType: 'web', // 项目类型
imgUrl: 'https://arms-retcode.aliyuncs.com/r.png?', // 日志上传地址
sendResource: true, // 上报页面静态资源
enableLinkTrace: true, // 进行前后端链路追踪
behavior: true, // 是否为了便于排查错误而记录报错的用户行为
enableSPA: true, // 监听页面的hashchange事件并重新上报PV
useFmp: true, // 采集首屏FMP
environment: environmentType, // 当前环境
release: environment.version, // 项目版本号
});
}
} catch (e) {
console.error('init arms fail', e);
}

export default logger;

在页面初始化的时候引入arms.ts重启项目就可以看到探针已经生效。

Angular自定义异常处理

当用户进入项目时ARMS就会把前端监控日志上报,在ARMS后台可以看到访问速度、UV和PV统计、API请求日志信息。
到这里原本以为一切搞定了,但是自测过程中发现JS错误信息无法自动上报,我立刻再去看了一遍ARMS的接入文档,发现文档上也没有关于Anuglar项目的接入教程。然后开始各种Goole也没有找到关于Angular接入ARMS的攻略,最后我只能加了官方钉钉群去咨询开发团队,得到了以下解答。

根据这个线索,我终于定位到JS异常没有上报的原因,是因为Angular框架底层对JS抛出的异常进行统一捕获处理,所以ARMS无法监听到JS异常,我们需要通过ARMS提供的error()方法手动上报异常信息。

error()

调用error()接口来上报页面中的JS错误或使用者想关注的异常。一般情况下,SDK会监听页面全局的Error并调用此接口上报异常信息,但由于浏览器的同源策略往往无法获取错误的具体信息,此时就需要使用者手动上报。

1
arms.error(error, pos)
参数 类型 描述 是否必选 默认值
error Error JS的Error对象
pos Object 错误发生的位置,包含以下3个属性
pos.filename String 错误发生的文件名
pos.lineno Number 错误发生的行数
pos.colno Number 错误发生的列数

Angular提供了钩子让我们自定义异常处理,我们要做的就是创建一个类,该类实现ErrorHandler来自@angular/core程序包的接口。该类必须实现handleError()方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { ErrorHandler } from '@angular/core';
import arms from './arms';
import { environment } from 'src/environments/environment';

/**
* 全局异常处理
*/
export class GlobalErrorhandler implements ErrorHandler {
handleError(error) {
switch (environment.type) {
case 'prod':
arms.error(error);
break;
case 'local':
console.error('Error from global error handler', error);
break;
default:
console.error('Error from global error handler', error);
arms.error(error);
break;
}
}
}

然后在app.modules.ts,告诉Angular它应该使用我们的错误处理程序,可以通过在providers配置中添加以下条目来实现

1
2
3
providers: [
{ provide: ErrorHandler, useClass: GlobalErrorhandler },
],

部署项目可以看到异常信息可以上报至ARMS后台,大功告成~

最后

感谢观看,若有不足望提出指正!

背景

考虑到团队新加入的小伙伴越来越多,多人协作开发的规范也得跟上。在此之前我们引入了Angular团队的Git使用规范,获得了不错的反响,接下来就需要对风格迥异的前端代码进行格式规范。

工具

prettier是目前前端代码格式化工具中最热门的,在选择插件时也是优先选择了它。我们的方案把prettier集成进代码并且配合着vscode上的prettier插件一同使用。

项目引入prettier

首先需要在项目中安装prttier包

1
npm install prettier --save-dev --save-exact

第二步在项目根目录下新建一个.prettier配置文件,配置文件可以是JSON、JS、YAML等格式,在这里我们选用的是JSON。然后就可以根据官方文档提供的配置项来制定我们格式规范。
prettier的可配置项不多,可以看官方文档大致过一遍。

以下是我们的配置(注释只作展示使用,请勿复制进去):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"printWidth": 80, // 超过最大值换行
"tabWidth": 2, // 缩进字节数
"useTabs": false, // 不使用缩进符,而使用空格
"semi": true, // 句末添加分号
"singleQuote": true, // 使用单引号代替双引号
"proseWrap": "preserve", // 使用默认的折行标准
"arrowParens": "avoid", // (x) => {} 箭头函数参数只有一个时是否要有小括号。avoid:省略括号
"bracketSpacing": true, // 在对象,数组括号与文字之间加空格 "{ foo: bar }"
"endOfLine": "auto", // 结尾是 \n \r \n\r auto
"htmlWhitespaceSensitivity": "css", // 根据显示样式决定 html 要不要折行
"jsxBracketSameLine": false, // 在jsx中把'>' 是否单独放一行
"jsxSingleQuote": false, // 在jsx中使用单引号代替双引号
"trailingComma": "es5" // 在对象或数组最后一个元素后面是否加逗号(在ES5中加尾逗号)
}

到这里Prettier的基本配置就完成了,你可以测试一下配置是否生效。

1
prettier --write [file/dir/glob ...]

执行命令如果看到文件按配置被格式化,说明你的prettier已经配置成功!
但是每一次格式化都需要手动输入命令触发,这样的体验并不是很完美。

自动格式化Pre-commit Hook

prettier官方文档提供了Pre-commit Hook,你可以把git add暂存的文件在被commit提交之前自动执行prettier格式化。

首先安装pretty-quick husky插件

1
npm install pretty-quick husky --save-dev

然后在package.json中加入配置

1
2
3
4
5
6
7
{
"husky": {
"hooks": {
"pre-commit": "pretty-quick --staged"
}
}
}

使用git提交测试文件,可以看到提交的文件自动被格式化。

Vscode插件 Prettier - Code formatter

我们已经在项目中集成了prettier,并且使用钩子在git提交代码的时候自动格式化相应的文件,但是这样子只能在最后提交之后才能看到代码格式化的效果,很多同学都有使用编辑器格式化代码的习惯,我们也可以在vscode中同步prettier配置。

首先在vscode中安装插件 Prettier - Code formatter

然后在vscode的setting.json中引入prettier配置项

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
"prettier": {
"printWidth": 80,
"tabWidth": 2,
"useTabs": false,
"semi": true,
"singleQuote": true,
"proseWrap": "preserve",
"arrowParens": "avoid",
"bracketSpacing": true,
"endOfLine": "auto",
"htmlWhitespaceSensitivity": "css",
"jsxBracketSameLine": false,
"jsxSingleQuote": false,
"trailingComma": "es5"
},

大功告成,此时vscode的格式化配置已经与我们项目集成的prettier同步了,接下来就可以愉快的编码了~

结束语

在多人协作的开发过程中有强规范来约定开发格式能够提升开发效率和易维护性。随着项目越来越壮大,需要优化和规范的点还非常多,欢迎伙伴们一起交流~