0%

背景

新建构是基于Ice.js搭建的,在写单元测试的时候遇到一些问题,此篇作为填坑记录。

一开始ice是不支持运行单元测试,因为暴露出来的ice是alias出来的虚包,所以直接在单元测试里import是没法运行的,遂到社区提了issue,前几天传来利好,有了alpha版本能支持写单元测试了,所以下面实践一把。

实操

services

测试services(ice框架)里的方法

比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { request } from "ice";

const mockId = "00000000-0d00-0000-0000-000000000000";

export default {
async fetchList(params, config={}) {
return await request.post(
"/api/v1/domain/getResourcesByDomainId",
{
...params,
id: mockId,
},
{ ...config }
);
},
};

测试用例:

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
/**
* @jest-environment node
*/

//@jest-environment node 表示测试环境你,默认为jsdom(类浏览器),node表示为node服务的方式(测试跨域请求时需要用到)
import listService from '../../../../src/pages/ResourceCenter/services/list'
import getCookie from '../../../_helper/getCookie'

let headers = {}
beforeEach(async () => {
const Cookie = await getCookie()
headers = { Cookie }
})

describe('fetchList', () => {
test('listService fetchList', async () => {
const params = { pageNum: 1, pageSize: 10 }
const config = {
withCredentials: true,
headers: { ...headers },
}
const data = await listService.fetchList(params, config)
expect(data).not.toBeNull()
})
})

带store的组件

比如:

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
import React, { useEffect } from 'react';
import { Grid, Tab } from '@alifd/next';
import { store as pageStore } from 'ice/ResourceCenter';
import ResourceList from './components/list.jsx';
import styles from './index.module.scss';
import ResourceTypeTree from './components/tree.jsx';
import DetailConfig from './components/detail_config.jsx';

const { Row, Col } = Grid;

const AlarmAnalyze = () => {
const [treeState, treeDispatchers] = pageStore.useModel('tree');
const [listState, listDispatchers] = pageStore.useModel('list');
useEffect(() => {
treeDispatchers.fetchTree();
listDispatchers.fetchList();
}, []);

return (
<div>
<Row>
<Col span="4">
<div className={styles['layout-aside']}>
<ResourceTypeTree data={treeState.data} />
</div>
</Col>
<Col span="20">
<div className="layout-content">
<Tab>
<Tab.Item title="xxx" key="1">
<ResourceList data={listState} />
</Tab.Item>
<Tab.Item title="xxx" key="2">
<DetailConfig data={listState} />
</Tab.Item>
</Tab>
</div>
</Col>
</Row>
</div>
);
};

export default AlarmAnalyze;
AlarmAnalyze.pageConfig={
auth:["/resourceCenter"]
}

测试用例:

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
import React from 'react'
import AlarmAnalyze from '../../../src/pages/ResourceCenter/index'
import { store as pageStore } from 'ice/ResourceCenter'
import { shallow, mount } from 'enzyme'
//测试内容中需要处理store时,需要用provider包裹
const PageProvider = pageStore.Provider

const WithPageModel = (props) => {
return (
<PageProvider>
<AlarmAnalyze {...props} />
</PageProvider>
)
}

let wrapper
beforeEach(() => {
wrapper = mount(<WithPageModel />)
})

describe('render', () => {
test('Test ResourceList ', () => {
const cwrapper = wrapper.find('ResourceList')
expect(cwrapper.prop('data')).toEqual({ ciType: '', current: 1, keyword: '', list: [], pageSize: 10, total: 0 })
//取消挂载
wrapper.unmount()
})

test('Test pagination ', () => {
const pagination = wrapper.find('Pagination')
expect(pagination.prop('total')).toBe(0)
//取消挂载
wrapper.unmount()
})

test('Test ResourceTypeTree ', () => {
const cwrapper = wrapper.find('ResourceTypeTree')
expect(cwrapper.prop('data')).toEqual(expect.arrayContaining([]))
//取消挂载
wrapper.unmount()
})
})

命令

注意只能用icejs来运行,想想也是应该的,因为你要测的东西就是ice那一套的,不可能随便拽个Jest就能跑,Jest咋知道哪跟哪呢。

1
2
"icetest:watch": "icejs test --jest-watch",
"ice:coverage": "icejs test --jest-coverage",

背景

我们的产品通常进入客户现场之前都会有一系列的安全扫描(公司级的、客户级的),去年我们团队就解决了好几个安全问题,不过通常都是后端的方式来解决,但其实某些安全问题前端也能解,有些问题前端解更合适,这不借着重新出发的春风,往前端工程化中加了一步安全

此篇文章收获颇多。WEB前端安全自查和加固

实操

对于安全方面的共识
  1. 对于第三方包的引入,必须经过npm audit+snyk安全扫描后且经过小组内评审通过后方才能引入。
  2. 对于我们自己发布的包,必须去掉敏感信息。
  3. 每次更新了package.json之后都需要进行安全扫描。

如下图,是我们项目里我用snyk执行扫描之后报出问题的包信息。

小结

后续会把考虑把该步骤拿到流水线里去做,因为虽然大家意识层面共识了,落地的时候可能还是会有各种遗漏,所以干脆直接交给机器来做。

安全无小事,先动起来!

本文引用的内容,如有侵权请联系我删除,给您带来的不便我很抱歉。

背景

其实老早以前就想在产品中引入单元测试,领导也让我好好做一个分享主题,分享好做花功夫就行,但是我觉得此主题的分享最终目的是为了落地,所以需要一个契机,不然大家听了就完了也没啥x用。

刚好大佬们上个月做了个决定,决定从前到后整体重新架构,重新梳理,做一个带中台属性的系统,从而是产品能有更多平台能力,也能更好的和另外的产线想结合。当然我说的比较云淡风轻的,干起来你才知道这是一个多么有挑战的目标,扯远了,下面说说前端测试这块的内容。

名词定义

基础组件:antd、next等提供的组件。

通用组件:咱们系统中抽取出来的组件公用的组件(完全自定义的以及基于基础组件改造的)=>单元测试(UI测试可选)

业务组件:通过基础组件、通用组件等组合起来的组件(区块)=>单元测试、集成测试(UI测试可选)

为什么需要测试?

  • 提高代码质量
  • 倒逼开发对编码的设计更加低耦合以及合理,方便后续的迭代/重构
  • 最大程度保证产品符合预期(因为各种场景都测试)
  • 减少测试的投入(比如回归)
  • 提升开发的信心(bug少)

前端测试的类型

单元测试(Unit Test)集成测试(Integration Test)UI 测试(UI Test)

单元测试应用:
  • 代码中多个组件共用的工具类库、多个组件共用的子组件等。

  • 能进行单元测试的函数/组件,一定是低耦合的,这也从一定程度上保证了我们的代码质量。

    比如我们的工具库、组件库这类通用低耦合的内容就比较适用单元测试。

「通常情况下,在公共函数/组件中一定要有单元测试来保证代码能够正常工作。单元测试也应该是项目中数量最多、覆盖率最高的。」

集成测试应用:
  • 耦合度较高的函数/组件、经过二次封装的函数/组件、多个函数/组件组合而成的函数/组件等。
  • 集成测试的目的在于,测试经过单元测试后的各个模块组合在一起是否能正常工作。会对组合之后的代码整体暴露在外接口进行测试,查看组合后的代码工作是否符合预期。

「集成测试是安全感较高的测试,能很大程度提升开发者的信心,集成测试用例设计合理且测试都通过能够很大程度保证产品符合预期。」

UI测试应用:

先做个说明,大家不要把UI 测试(UI Test)和端到端测试(E2E Test)混为一谈,认为是同一个测试类型。

事实上,UI 测试(UI Test)和端到端测试(E2E Test)是稍有区别的:

UI 测试(UI Test)只是对于前端的测试,是脱离真实后端环境的,仅仅只是将前端放在真实环境中运行,而后端和数据都应该使用 Mock 的。

端到端测试(E2E Test)则是将整个应用放到真实的环境中运行,包括数据在内也是需要使用真实的。

就前端而言,UI 测试(UI Test)更贴近于我们的开发流程。在前后端分离的开发模式中,前端开发通常会使用到 Mock 的服务器和数据。因而我们需要在开发基本完成后进行相应的 UI 测试(UI Test)。

可以理解为交付测试之前的自测阶段我们做的就是UI测试。而测试做的就是端到端测试。

开始

此篇介绍的是单元测试的应用。

先简单画下边界,此篇只说明基于工具库、组件库的分享示例。高耦合的组件或者自动化测试应用不在此次讨论范围内。

选择何种测试思想

其实本不用做这个说明,因为其实我们已经选好了(BDD)。

但是避免有像我一样懵懂的同学,所以这儿咱们也简单说明一下。因为测试框架对不同规范的支持可能不一样。所以我们先搞明白测试思想有哪些?

测试驱动开发(TDD)行为驱动开发(BDD)

TDD是一种开发技术,更多地侧重于功能的实现。

TDD代表测试驱动开发。 在这种软件开发技术中,我们首先创建测试用例,然后编写这些测试用例的基础代码。 尽管TDD是一种开发技术,但它也可以用于自动化测试开发。

实施TDD的团队需要花费更多的时间进行开发,但是,他们发现的缺陷很少。 TDD可以提高代码质量,并提高代码的可重用性和灵活性。

TDD还有助于实现大约90-100%的高测试覆盖率。 对于遵循TDD的开发人员而言,最具挑战性的事情是在编写代码之前先编写测试用例。

BDD是一种专注于系统行为的开发技术。

BDD是TDD的扩展,它不是编写测试用例,而是从编写行为开始。 后来,我们开发了应用程序执行该行为所需的代码。

BDD方法中定义的方案使开发人员,测试人员和业务用户易于协作

当涉及自动测试时,BDD被认为是最佳实践,因为它专注于应用程序的行为,而不是考虑代码的实现。

应用程序的行为是BDD的关注重点,它迫使开发人员和测试人员去了解产品以及用户。

TDD BDD
该过程从编写测试用例开始。 该过程从编写测试用例开始。
TDD专注于功能的实现方式。 BDD专注于最终用户的应用程序行为。
测试用例是用编程语言编写的。 与TDD相比,场景以简单的Given、When、Then关键字编写,因此更具可读性。
应用程序功能的变化对TDD中的测试用例有很大影响。 BDD方案基本不受功能更改的影响。
仅在开发人员之间需要协作。 所有利益相关者之间都需要合作。
只有具有编程知识的人才能理解TDD中的测试。 任何人都可以理解BDD中的测试,包括那些没有任何编程知识的人。
TDD降低了测试中出现错误的可能性。 与TDD相比,测试中的错误很难跟踪。
小结:

我简单总结下大家有个简单的概念,具体有兴趣大家再下去查资料。

TDD主体是开发,说白了就是测试用例也是开发来写,先写测试用例,很显然测试用例肯定都过不了因为还没编码,那怎么办呢,就通过编码的方式不断的丰富功能然后再回去跑测试用例,如此循环最终达到所有测试用例都通过。比较适用后端API的测试。

BDD没有主体,因为所有人都需要参与(测试、开发、产品),大家用同一套语言来理解产品的功能和用户行为,大家的思考角度会更侧重用户方。比较适用前端测试的场景,因为前端是直接面对用户的。

Jest用的断言库就是BDD的思维。eg:expect(wrapper.find(‘.bar’)).toHaveLength(3);一品就是given、when、then的路数

找测试框架

诉求

  1. 能跑JavaScript也能跑React。
  2. 最好能开箱即用,不需要做很多配置。
  3. 社区生态良好,能及时支持新特性且能及时响应issue。

Jest、Enzyme

Jest:

其实测试框架茫茫多,最终选择它的原因就是其完美匹配上诉的所有诉求,Facebook亲生的,star 接近32K,React不用说了,同时也能跑JavaScript。另外Jest还具备Mock等能力,所以Jest不仅仅是个测试框架而且还是一个集合多种能力的结合体,开箱即用。

Enzyme:

用于测试的React工具库,可看作是一个集成测试框架,与Jest组合也是备受推崇的。它提供获取元素的能力。

Enzyme渲染一个或多个组件,以及查找元素以及与元素进行交互的能力,触发事件处理程序等能力。

首先任何框架都有优点和缺点,那为什么选这两个呢:

  1. ice用的就是这两个,有大厂(阿里)的前面探路,对于我们没有经验的团队来说作为第一次的试水我觉得跟风不是一个贬义词。
  2. 基于我们的诉求我也做了一下简单的调研,因为我们主要还是以React为主所以选择Jest是自然而然的过程,另外Enzyme+Jest也是国内外热度都比较高的,两则的社区也比较活跃,所以很显然两者搭配是没有问题的,刚好ice就是用的这两个所以没什么好纠结的。

Enzyme只是提供了额外的能力对React组件进行测试,不用Enzyme只用Jest可测试,但是只有Enzyme则不能测试,Enzyme是在测试框架上能力的扩展。

  • Jest主要提供测试框架(提供测试需要的所有能力)
  • Enzyme(React测试工具库)基于Jest等测试框架增加DOM操作的能力,提供了强大的 API 能力支持 UI 交互测试

实操

Jest

1
2
3
npm install jest
npm install babel-plugin-transform-class-properties --save-dev
npm install react-test-renderer

修改根目录下.babelrc

1
2
3
4
{
"presets": ["@babel/preset-env", "@babel/preset-react"],
"plugins": ["@babel/plugin-syntax-jsx","transform-class-properties"]
}

学习文档

icejs测试

jest-community/awesome-jest

提供了很多能力,增强测试的体验。

以下几个是比较有代表性的

Snapshot

Jest使用 Snapshot 进行 UI 测试

Snapshot 测试是 Jest 提供的能力,可以自动生成组件 UI 输出的描述文件,确保你的 UI 不会发生意外的改变。

1
2
//更新快照
jest --updateSnapshot

组件实现代码:

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
 import React from "react";
import { Card, Button } from "@alifd/next";

const RCard = (props) => {
const { title, subTitle, content, deleteFunc } = props;
const commonProps = {
subTitle: subTitle || "",
};
return (
<div>
<Card free style={{ width: 300 }}>
<Card.Header title={title || ""} {...commonProps} />
<Card.Content>{content || ""}</Card.Content>
<Card.Actions>
<Button type="primary" onClick={deleteFunc || null}>
删除
</Button>
</Card.Actions>
</Card>
</div>
);
};

export default RCard;

组件测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import React from 'react'
import renderer from 'react-test-renderer'
import RCard from '../../../src/components/Card/index'
import { shallow } from 'enzyme'

const props = {
title: 'firstRCard',
subTitle: '测试',
content: '这是第一个测试demo',
deleteFunc: jest.fn((e) => {
// throw 'error'
return 'OK'
}),
}

// UI快照测试
test('jest renders Snapshot', () => {
const tree = renderer.create(<RCard {...props} />).toJSON()
expect(tree).toMatchSnapshot()
})

在第一次运行后,Jest Snapshot 将会生成对应的 .snap 文件。

对于一个React组件而言, 传入相同的props,我们是期望得到相同的输出,所以后续如果组件的输出内容发生变更,则会导致测试用例无法通过。

ice结合 Jest CLI参数

–watch

1
eg:"test": "icejs test --jest-watch",

监视文件是否有更改,并重新运行与已更改文件相关的测试。 如果要在文件更改后重新运行所有测试,请改用–watchAll选项。

–watchAll

1
eg:"test": "icejs test --jest-watchAll",

监视文件中的更改,并在发生更改时重新运行所有测试。 如果只想重新运行依赖于已更改文件的测试,请使用–watch选项。

也可单独运行

1
2
3
//package.json
"coverage": "jest --colors --coverage",
"test": "jest --watch",

安装

vscode插件

  • jest

  • jest runner

编辑根目录下的jest.config.js

使其支持jsx、es6等

1
2
3
4
5
6
module.exports = {
verbose: true,
roots: ['<rootDir>/src/', '<rootDir>/test/'],
testMatch: ['<rootDir>/test/**/*.js'],
setupFilesAfterEnv: ['<rootDir>/src/setupTests.js'],
}

示例

Jest具体的概念说明看Jest

expect,各种断言。

随便看几个实例

Number

Jest生命周期钩子:

异步代码测试

使用 Enzyme 测试组件

Enzyme 是 Airbnb 提供的测试类库,它提供了一套简洁强大的 API。能够灵活操作 DOM,是 React 社区推荐的测试方案。

准备工作

安装测试相关依赖包

1
$ npm install --save-dev enzyme enzyme-adapter-react-16 react-test-renderer

基于 React 开发的测试,需要安装对应的 React Adapter 来保证 enzyme 渲染的版本和项目中使用的版本一致,以 react 16 版本为例,需要进行如下设置:

1
2
3
import { configure } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
configure({ adapter: new Adapter() });

如果不想每个测试用例都去定义一遍,可以将上述内容保存至 src/setupTests.js 文件中,并自定义 Jest 配置中的 setupFilesAfterEnv

1
2
// jest.config.js
module.exports = { setupFilesAfterEnv: ['<rootDir>/src/setupTests.js'],};

Mount, Shallow, Render

1
import { mount, shallow, render } from ‘enzyme';

为了具有要测试的组件,必须使用上述之一,如上面的示例所示。

mount(常用)

  • 完整的DOM渲染,包括子组件
  • 当您具有可能与DOM API交互的组件或使用React生命周期方法以完全测试组件的用例时,它是理想的选择
  • 由于它实际上是将组件安装在DOM中,因此应在每次测试后调用unmount(),以停止相互影响的测试
  • 允许访问直接传递到根组件中的props(包括默认props)和传递到子组件中的props

Shallow(常用)

  • 仅渲染单个组件,不包括其子组件。 这对于隔离组件进行纯单元测试很有用。 它可以防止子组件中的更改影响测试输出。
  • 默认情况下,浅层组件可以访问生命周期方法
  • 无法访问传递到根组件的props (因此也不是默认props ),但是可以访问传递到子组件的props ,并且可以测试传递到根组件的props 的效果。

Render

  • 渲染为静态HTML,包括子级
  • 没有访问React生命周期方法的权限
  • 比mount简单,但功能较少
总结

react测试利器enzyme有三种渲染方式:shallow, mount, render。

shallow渲染叫浅渲染,仅仅对当前jsx结构内的顶级组件进行渲染,而不对这些组件的内部子组件进行渲染,因此,它的性能上最快的,大部分情况下,如果不深入组件内部测试,那么可以使用shallow渲染。Shallow Rendering (浅渲染)指的是,将一个组件渲染成虚拟DOM对象,但是只渲染第一层,不渲染所有子组件,所以处理速度非常快。它不需要DOM环境,因为根本没有加载进DOM。

mount则会进行完整渲染,而且完全依赖DOM API,也就是说mount渲染的结果和浏览器渲染结果说一样的,结合jsdom这个工具,可以对上面提到的有内部子组件实现复杂交互功能的组件进行测试。

render也会进行完整渲染,但不依赖DOM API,而是渲染成HTML结构,并利用cheerio实现html节点的选择,它相当于只调用了组件的render方法,得到jsx并转码为html,所以组件的生命周期方法内的逻辑都测试不到,所以render常常只用来测试一些数据(结构)一致性对比的场景。

这里还提到,shallow实际上也测试不到componentDidMount/componentDidUpdate这两个方法内的逻辑。

shallow和mount对组件的渲染结果不是html的dom树,而是react树,如果你chrome装了react devtool插件,
他的渲染结果就是react devtool tab下查看的组件结构,而render函数的结果是element tab下查看的结果。

render: render采用的是第三方库Cheerio的渲染,渲染结果是普通的html结构,对于snapshot使用render比较合适。

这些只是渲染结果上的差别,更大的差别是shallow和mount的结果是个被封装的 ReactWrapper
可以进行多种操作,譬如find()、parents()、children()等选择器进行元素查找;
state()、props()进行数据查找,setState()、setprops()操作数据;
simulate()模拟事件触发。

常用方法示例

常用函数

  • simulate(event, mock):用来模拟事件触发,event为事件名称,mock为一个event object;
  • instance():返回测试组件的实例;
  • find(selector):根据选择器查找节点,selector可以是CSS中的选择器,也可以是组件的构造函数,以及组件的display name等;
  • at(index):返回一个渲染过的对象;
  • get(index):返回一个react node,要测试它,需要重新渲染;
  • contains(nodeOrNodes):当前对象是否包含参数重点 node,参数类型为react对象或对象数组;
  • text():返回当前组件的文本内容;
  • html(): 返回当前组件的HTML代码形式;
  • props():返回根组件的所有属性;
  • prop(key):返回根组件的指定属性;
  • state():返回根组件的状态;
  • setState(nextState):设置根组件的状态;
  • setProps(nextProps):设置根组件的属性;
.find(selector) => ShallowWrapper

selector选择器

从渲染树中查找与提供的选择器匹配的每个节点。

Arguments

  1. selector (EnzymeSelector): The selector to match.

Examples

CSS Selectors:

1
2
3
4
5
6
7
8
9
const wrapper = shallow(<MyComponent />);
expect(wrapper.find('.foo')).toHaveLength(1);
expect(wrapper.find('.bar')).toHaveLength(3);

// compound selector
expect(wrapper.find('div.some-class')).toHaveLength(3);

// CSS id selector
expect(wrapper.find('#foo')).toHaveLength(1);
simulate(event[, mock])

在对应的元素节点上模拟事件。

Arguments

  1. event (String): 事件名称
  2. mock (Object [optional]): 模拟事件对象,它将与传递给处理程序的事件对象合并

Returns

ReactWrapper: Returns itself.

Example class component
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
class Foo extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
}

render() {
const { count } = this.state;
return (
<div>
<div className={`clicks-${count}`}>
{count} clicks
</div>
<a href="url" onClick={() => { this.setState({ count: count + 1 }); }}>
Increment
</a>
</div>
);
}
}

const wrapper = shallow(<Foo />);

expect(wrapper.find('.clicks-0').length).to.equal(1);
wrapper.find('a').simulate('click');
expect(wrapper.find('.clicks-1').length).to.equal(1);
Example functional component

component

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
50
51
52
53
54
55
56
57
58
59
60
import React, { useState } from "react";
import PropTypes from "prop-types";
import { Form, Input, Select, Button } from "@alifd/next";

const FormItem = Form.Item;

const formItemLayout = {
labelCol: {
span: 6,
},
wrapperCol: {
span: 14,
},
};
const SearchInput = ({ placeholder, onSearch }) => {
const submitHandle = (values) => {
if (Object.keys(values).length === 0) {
return;
}
onSearch(values.search);
};
return (
<div
style={{
marginBottom: "10px",
marginTop: "5px",
marginLeft: "5px",
width: "420px",
}}
>
<Form inline>
<Form.Item
label="策略名称:"
key="search"
required
requiredTrigger="onBlur"
hasFeedback
requiredMessage="请输入"
>
<Input placeholder={placeholder} name="search" />
</Form.Item>
<FormItem wrapperCol={{ offset: 5 }}>
<Form.Submit
validate
type="primary"
onClick={submitHandle}
style={{ marginRight: 10 }}
>
搜索
</Form.Submit>
</FormItem>
</Form>
</div>
);
};
SearchInput.propTypes = {
onChange: PropTypes.func,
onClick: PropTypes.func,
};
export default SearchInput;

测试用例

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
import SearchInput from '../index'
import { mount } from 'enzyme'
import React from 'react'

//describe相当于一个块,可以将测试用例放在一个块中
describe('SearchInput unit test', () => {
// case1 标签选择器+修改props
test('SearchInput tag selector', () => {
const wrapper = mount(<SearchInput placeholder="please input" />)
expect(wrapper.find('input').at(0).prop('placeholder')).toEqual('please input')
//设置属性
wrapper.setProps({ placeholder: '请输入' })
expect(wrapper.find('input').at(0).prop('placeholder')).toEqual('请输入')
})

// case2属性选择器+修改props
test('SearchInput props', () => {
const wrapper = mount(<SearchInput placeholder="" />)
// 判断是否存在输入框
expect(wrapper.exists('input[name="search"]')).toBeTruthy
expect(wrapper.find('input[name="search"]')).toHaveLength(1)
//设置属性
wrapper.setProps({ placeholder: 'serach' })
expect(wrapper.find('input#search').prop('placeholder')).toBe('serach')
})
//修改state
test('SearchInput state', () => {
const wrapper = mount(<SearchInput placeholder="" />)
// 判断是否存在输入框
expect(wrapper.exists('input[name="search"]')).toBeTruthy
expect(wrapper.find('input[name="search"]')).toHaveLength(1)
//设置state,只有class components才可以使用
// wrapper.setState({ loading: true })
// expect(wrapper.find('.next-btn next-medium next-btn-primary next-btn-loading')).toHaveLength(1)
})
})

看前面的一堆代码可能削微有点绕啊

简单总结一下:

expect是Jest提供的断言,所以当你需要该判断结果是否正确的时候去Jest查expect的文档就行。

wrapper可看做是enzyme提供了一套类似Jquery的API。所以Jquery做的她都能做,如:查找元素、获取元素及其属性、触发元素行为等。对应的也直接去enzyme查文档就行

测试报告解读

File 测试用例文件
% Stmts 是语句覆盖率(statement coverage):是否每个语句都执行了
% Branch 分支覆盖率(branch coverage):是否每个分支代码块都执行了(if,||,?:)
% Funcs 函数覆盖率(function coverage):是否每个函数都调用了
% Lines 行覆盖率(line coverage):覆盖的行
% Uncovered Line #s 行覆盖率(line coverage):是否每一行都执行了
Test Suites 测试用例文件个数
Tests 测试用例个数
Snapshots UI快照个数

本文引用的图片,如有侵权请联系我删除。

测试外传

测试框架有很多基于不同的需求不同的语言选择不同,比如Puppeteer、@testing-library库等等,有机会咱也试一试。

现阶段在团队里落地的案例:核心的方法(覆盖率100%)、核心的组件(覆盖率>80%)

感谢

背景

公司从去年引入TRIZ,以激发产品创新,上半年宗磊带着RELAX几位架构师参加了一个课题。从目前公司的成果来看,还是一个非常不错的创新方法,公司也有非常大的决心去推进这个方法论的落地。

之前认为TRIZ不适用软件,上期陆续有多个软件课题获奖,包括一些持续集成,测试,算法等方面的课题,为了让我们未来的产品更有竞争力,我们将会加大这方面的投入。

学习

当我们遇到实际问题的时候,会产生截然不同的解决问题的思路。如果是使用传统的试错法,通常是根据问题的现状,利用自己的专业知识和行业知识,尝试不同的办法来寻找问题的解决方案。

img

图1 问题导向做法(图片选自网络并修改)

如图1所示,由于并非人人都是专家,都能遇到自己熟悉的问题,所以以往的经验未必对解决问题有太大的帮助。这种从问题出发,典型的问题导向做法往往不知道问题的解决办法在何处,有点类似“脚踩西瓜皮,划到哪里算哪里”的感觉。有时虽然能解决问题,但效率较低,且其解决办法是否是最优的也未必知晓。

如果是采用TRIZ创新方法,比如用到TRIZ的工具——最终理想结果(IFR,Ideal Final Result,也称最终理想解),这种从解决问题所追求的结果出发,目标明确,则是典型的结果导向做法,通过对系统的最终理想结果(IFR)的确定,来判断具体问题的解决方案的空间和解决问题的入手点。

如图2所示,就像在黑夜里航行的船舶看到航灯,其好处是既知晓问题解决的理想方向,在解决问题的时候始终不会迷失方向,又能找到合理的解决问题的入手机会,导向性非常明确,这是以往多数的创新方法所不具备的优点。

img

图2结果导向做法(图片选自网络并修改)

在实际的培训、咨询和辅导解题中,经常会遇到研发人员不太会正确定义最终理想结果(IFR)和合理使用此工具,不能准确锁定问题,严重影响了问题的解决。那么,如何指导研发人员快速、准确的确定最终理想结果(IFR)呢?

首先,我们来看看最终理想结果(IFR)的概念,最终理想结果(IFR)是对发明问题的最好解决方案模型它使系统完全消除了问题,并且没有让系统的参数发生恶化,且对系统的改变最小。它是解决方案的模型,可以指引着我们去解决发明问题。

通过上述概念,可以了解三个信息:

1)IFR是最好的解决方案模型,注意不是之一,可以用来引导和把握问题的解题方向;

2)通过IFR模型,既要解决系统存在的问题,又不能使系统的参数发生恶化,也就是要消除矛盾;

3)是对系统的最小改变,也就是最小问题,不要考虑对系统进行较大的改变,这不符合IFR的理念。

img

其次,最终理想结果(IFR)的定义过程中,要注意系统的最终目标和最终理想结果的区别。

系统的最终目标是指所设计的系统应该达成的结果,即客户之声****(VOC,Voice of Customer)。例如,客户需要清洁衣物,又不能过于劳累,所以设计了洗衣机,则洗衣机的最终目标就是清洁衣物,至于生产商用什么工作原理、什么系统组成来满足客户的需求,则会有不同的系统(产品)产生。

所以,最终目标往往是客户很模糊的需求,需要认真加以甄别,有相应的工具和流程,这里不加赘述。

再次,为了打破研发人员的惯性思维,通常建议定义IFR时把握两个原则:

**
**

1)以系统的作用对象(又称制品、目标)为入手来定义;

2)定义的IFR应用“自身”或“自我实现”的字眼。

img

例如,清洁衣物,可以用灰尘等污物自身脱离衣物来作为IFR。当然,如果能做到这一点,系统就是非常理想的系统,但是,这往往是不可能的,否则系统就没有存在的必要了。

既然IFR是不太容易实现,为什么还要这样定义呢?这里主要是基于以下考虑:

1)在解决问题之初,先抛开各种客观限制条件,把最终理想结果作为终极追求目标

2)针对问题情境,结合最终理想结果设立各种理想模型,即最优的模型结构来分析问题;

3)这样定义的IFR在后续的分析很容易达成共识,不会变来变去,干扰解题过程。

这时,我们就可以在最终理想结果(IFR)与系统问题现状之间不断选择不同的次理想结果,方便找到解决问题的入手点。

还是拿清洁衣物来举例说明,让灰尘等污物自己脱离衣物纤维这一IFR无法实现,那么就尝试改变工具(或执行机构),这样次理想结果就是衣物纤维自己能与灰尘等污物分离。

通常应注意:

1)技术物体比天然物体易于改变;

2)工具比制品易于改变;

3)如果系统中无易于改变的要素,即应指出”外部介质”,可以借助中介物原理。

此时需要考虑的就是衣物纤维能不能像荷叶那样,具有自洁能力,灰尘等污物无法附着在衣物纤维上,故而就可以实现清洁衣物的目的。

还有,当衣物纤维不具备自洁功能时,可以将下一个次生理想结果定义为衣物纤维借助外力实现与灰尘等污物分离,这时问题就转变为利用什么力(或场)能实现两者的分离,能达到清洁衣物的目的。

img

图3 后退收敛定义IFR的过程(图片选自法思诺FASINNO课件)

如此这般,通过逐步后退一小步收敛问题的不同理想结果(次生、次次生……),更易发现解决问题的机会

可以参照如图3所示的收敛过程进行定义,把不明确的问题通过IFR的多次定义、分析,将问题转化为明确的问题,寻找到解决问题的入手点。

  • 什么是TRIZ创新理论

TRIZ意译为发明问题的解决理论。TRIZ理论成功地揭示了创造发明的内在规律和原理,着力于澄清和强调系统中存在的矛盾,其目标是完全解决矛盾,获得最终的理想解。它不是采取折中或者妥协的做法,而且它是基于技术的发展演化规律研究整个设计与开发过程, 而不再是随机的行为。实践证明,运用TRIZ理论,可大大加快人们创造发明的进程而且能得到高质量的创新产品。

专利

TRIZ这个强而有力的工具消除在不同性能测量之间的冲突所引起对妥协和交换的需要,为创新带来了可执行的方法论。

特点

  • 降低尝试次数和错误迭代
  • 降低对共同的资源的需要
  • 使用演化的趋势打开历史,为新产品想法证明方向
  • 利用专利规避,突破竞争对手的防御

优越性

TRIZ 是一套以人为导向的知识系统之系统化创新问题解决方法。它有别于传统的脑力激荡,TRIZ强调发明或创新可依一定的程序与步骤进行,而非仅是随机或天马行空的脑力刺激。TRIZ 发展至今,从 300 万个专利分析中提取了 1000 个样式等,这个广泛的知识库可使用户在开发实际产品时,面对困难的局面和问题得以迅速获得可能的答案或思路。

img

示例

TRIZ具体项目的落地成果,取决于几个关键要素:

1. 能识别出关键问题

2. 能想到创新的方案

3. 能把这些方案做出来

因此,在这个过程中需要几个关键要素:

第一:大家能够整合不同的观点,找到关键问题和关键解决方案;

第二:能死磕,把方案落地过程中的关键问题能够解决掉;

第三:落地过程中的具体工作和资源支持。

在这个过程中,我们需要参与者付出三方面的努力:

\1. 积极参与小组讨论活动,因为协作的力量搭配上TRIZ流程才能发挥比较大的作用;

\2. 积极提出方案;

\3. 积极完成分工布置的主要任务。

那么,如何通过考核与激励来实现这一点呢?

我觉得可以从两个角度来实施:

从业务部门的角度,可以从项目管理的角度来推进落地,考核整个TRIZ小组的项目进度和流程的应用。可以通过定期的项目评审来完成,考核与激励指标都基于项目成果来设置。

从人力资源的角度,可以考核项目组里每个成员的参与度和贡献度,考核与激励指标基于出席率、创意的数量与质量等。

最终,业务考核可以与项目奖金挂钩,人事考核可以与年终评奖、职称等挂钩。

用图片来总结就是:

img

如果你对TRIZ感兴趣,并且是产品、研发部门的小伙伴,欢迎扫码添加小助手,发送暗号“TRIZ”,经过验证后,会邀请你进入【研发专属群】,精品直播课+大咖分享+专业咨询… 等着你!

Jest

skip,only,each修饰符

describetest可以连接skiponlyeach修饰符。如describe.skip('something', testFunction),会在测试时跳过这一个describeonly会使测试只运行指定的测试用例,这在某个测试用例出错Debug时非常好用。each修饰符可以执行多次参数不同的测试,它接受一个数组table和一个测试函数,table里的元素会作为参数传入测试函数。具体语法可以参见文档

beforeAll,afterAll,beforeEach,afterAll钩子函数

Jest也支持在执行测试用例之前以及之后执行一些代码来做一些工作,像在测试前设置好测试数据、在测试后清理测试数据。这些工作可以作为beforeAllafterAllbeforeEachafterAll的回调函数。

断言

Jest支持expect式的断言,像expect(1).toBe(1),其中toBe就是断言部分。Jest支持很丰富的断言。

相等断言

断言两个基本类型的值相等使用expect(val1).toBe(val2)。注意toBe断言使用Object.is()判断相等。它与==以及===都有不同。

如果要断言数组或者Object相等,使用toEqual断言。它会递归地判断每个属性/元素是否是相等的。

如果要断言数组或者Object相等,使用toEqual断言。它会递归地判断每个属性/元素是否是相等的。

toStrictEqual(value)

测试对象具有相同的类型和结构

.toEqual不同在于:

  1. 检查具有未定义属性的键。 例如 使用.toStrictEqual时,{a:未定义,b:2}与{b:2}不匹配。
  2. 检查数组。 例如 使用.toStrictEqual时,[,1]与[undefined,1]不匹配
  3. 检查对象类型是否相等。 例如 具有字段a和b的类实例将不等于具有字段a和b的文字对象。
数字大小断言
1
2
3
4
5
6
7
8
9
10
11
test('two plus two', () => {
const value = 2 + 2;
expect(value).toBeGreaterThan(3);//大于
expect(value).toBeGreaterThanOrEqual(3.5);//大于等于
expect(value).toBeLessThan(5);//小于
expect(value).toBeLessThanOrEqual(4.5);//小于等于

//等于
expect(value).toBe(4);
expect(value).toEqual(4);
});

对于浮点数,不能使用toBe或者toEqual进行相等断言。Jest提供了toBeCloseTo断言,可以在忽略一定误差的情况下,断言浮点数相等。

1
2
3
4
5
test('adding floating point numbers', () => {
const value = 0.1 + 0.2;
//expect(value).toBe(0.3); This won't work because of rounding error
expect(value).toBeCloseTo(0.3); // This works.
});
真值断言(Truthiness)
  • toBeNull 用于 null

  • toBeUndefined 用于undefined

  • toBeDefinedtoBeUndefined 相反

  • toBeTruthy 用于值为 true

  • toBeFalsy 用于值为 false

    1
    2
    3
    4
    5
    6
    test('null', () => {
    const n = null;
    expect(n).toBeNull();//pass
    expect(n).toBeDefined();//pass
    expect(n).toBeUndefined();// faild
    });
字符串相关

Jest提供toMatch断言被测试的字符串是否匹配给定正则表达式。

复制

1
2
3
test('but there is a "stop" in Christoph', () => {
expect('Christoph').toMatch(/stop/) // pass
});
数组

要断言数组中包含某个子项可以使用toContain断言。

复制

1
2
3
4
5
6
7
8
9
10
11
const shoppingList = [
'diapers',
'kleenex',
'trash bags',
'paper towels',
'beer',
];

test('购物清单(shopping list)里面有啤酒(beer)', () => {
expect(shoppingList).toContain('beer');
});
抛出异常

要断言对函数的某些操作会抛出异常可以使用toThrow断言。

复制

1
2
3
4
5
test('throws on octopus', () => {
expect(() => {
drinkFlavor('octopus');
}).toThrow();
});
not修饰符

not修饰符可以把所有的断言反向,像expect(1).not.toBe(2)

Jest提供的断言不止上面提到那么多。常用到的还有像断言长度的toHaveLength,断言对象有某个属性以及属性的值的toHaveProperty。更多断言的可以参见Expect文档

背景

最近整理产品中积累下来的工具类,因为要用新框架了要对之前的代码进行移植。当然咱是个追求完美的人,实在是团队小伙伴们一路疯狂操作下来,工具类是茫茫多啊,基本上想要维护是注定两行泪,所以咱们借着这次机会把工具类好好整理一下。

开始

诉求:

  1. 可维护性强。
  2. 能进行包管理。
  3. 每个方法有详细的说明,且能友好的查看。

具象化要求

  • 工具方法需要分门别类,不能一个文件一把梭,一定要控制代码量,不能让人一打开文件,心里就是wc。
  • 更新(删除、新增、修改)某个工具方法时不影响其它方法的使用,达到我改我的,你别瞪我的效果。
  • 不能发生函数名、变量名冲突。
  • 每个方法的注释遵循统一规范,根据统一规范可生产类似swagger那样的api文档(有张好的面皮大家爱看也耐看,不用不断打扰写方法的人)。

解决办法

秉着能抄袭就不自己动手的精神,看了很多star上千的工具库,总结了一下方式方法。

  • 基本上大家都采用的方式是一个方法一个文件,这一个文件暴露出来的就一个方法也只有一个方法,如果该方法需求其他辅助方法,直接通过import引用,而不直接写到该文件中,整个文件特别整洁,如果方法比较复杂则会建立一个文件夹从而存放辅助方法的文件,辅助方法的文件里也只有一个方法。虽然看上去可能这样文件有点多了,但是确实很清晰很整洁,对于维护来说很友好。所以就用这种方式了
  • 看了下生产api的几种库,比如jsdoc、ddoc等,大体是jsdoc还是多一些,用起来也挺简单,所以就选jsdoc了
编码方式

一个方法一个文件

rollup

打包工具用rollup,以为是工具库所以用更简单的rollup。

  • 自动 Tree-shaking(Tree-shaking, 也被称为 “live code inclusion,” 它是清除实际上并没有在给定项目中使用的代码的过程,但是它可以更加高效。)
  • 打包速度快
  • 配置简单
Jest

jest开箱即用挺好,另外我们多用react,jest是Facebook出的,与react具有天然亲和力。

Api Doc

选择了jsdoc,使用简单且较普遍。

实操

目录

关键手法

编码

进行编码后记得在index.js处暴露方法。

单元测试

单元测试时,如果想打断点,记得修改上图中的配置后重新生成库。

dist说明
打包

如下图:

  • 每次修改库时,记得改3处的版本号,以及1处的CHANGELOG对本次修改进行一个简要的说明。
  • 2所示部分根据不同时期的需求可对应进行更改,从而改变打包的内容。
  • 完事之后直接npm publish,如果需要关联到fusion design 则直接iceworks sync就行。
使用说明
1
2
3
npm i @riil-frontend/utils

import { arrayToTree,UnitFormat,Units,getNumberLength } from '@riil-frontend/utils';
重复提交

加节流防止重复提交

防抖和节流

https://juejin.im/post/6844904004850286605

https://www.lodashjs.com/

最后

如果本身已经引入了第三方得工具库,比如lodash、underscore等,先看看它们得API看是否有现成得,如果没有再自己整一个,不然重复造轮子没多大意思,更何况有可能你造的轮子有可能是扁的…

本文引用的内容,如有侵权请联系我删除,给您带来的不便我很抱歉。

背景

因为团队要用fusion design,所以按我的习惯我得先知道他是什么、为什么、能做什么,我才好下手。

开始

先不负责任的下一个结论,fusion design是个撒子?

答案:

一个平台: fusion.design

两个工具:

  1. 开发者工具 Iceworks
  2. 设计师工具 FusionCool

可以看作是下图这样纠缠:

Fusion Design

fusion design = 一套基础组件库 @alifd/next + 主题定制平台 https://fusion.design + 设计师工具 FusionCool+ 物料中心 。

所以更确切的说 Fusion Design算是一套体系,是一种旨在提升设计与开发之间 UI 构建效率的工作方式 ,我认为理解这点很重要,不然可能咱们用半天还以为他就是另一个库就像我们之前用的antd一样,其实完全不是一回事。

Fusion Design能解决哪些痛点:

  1. 【协作成本】内部(UCD和研发)协作问题,不再需要因为对概念、规范、复用性等问题UCD和研发不断沟通。

  2. 用户体验一致性问题 ,不同业务功能或者不同迭代功能,同样功能的交互和组件用户体验不一致。

  3. 【时间成本】重复工作问题,比如不断的review还原度不断的修正、UCD每次都需要对高保真进行规范说明以及关键内容的标注。

    旧模式如下图:红框部分是我们经常重复的内容。

Fusion Design提供了哪些能力来解决上诉的痛点:

  • 物料中心:各种组件、区块、模板(包含官方(Next等)+其它第三方+咱们自研的)

  • UI的可定制能力,设计师根据物料中心的内容定制UI,还可沉淀设计模板。

  • 研发都能配合前端工具(iceworks等),开发模块模板更高效,沉淀业务模板,后续可直接套用模板不用再开发。

  • 快速定制、切换主题。

    应用fusion design之后,产出过程应该就会像下图:

@alifd/next

  • Next 是基于 Alibaba Fusion Design 的设计理念实现的一套骨架DPL(Design Pattern Library)类似于咱们之前使用的antd。配合 fusion.design 使用可以实现换肤的能力。
  • 基于React的组件库。 可以理解**Next**是fusion design的技术实现。

小结一下

所以综上所述,引人fusion design后,理想状态下设计师和研发产出页面(功能)的过程应该会如下面两张图所示:

物料中心

可以理解成一个仓库,类似maven仓库或者npm仓库,里面可包含用开发好的物料(区块、模板、组件),该物料中心与sketch、iceworks是互通的,相互间可上传可下载。

FusionCool

FusionCool:组件分发工具,主要面向所有设计师。当组件构建者完成组件设计发布组件后,每位设计师手上的Fusion Cool都会“自动”接收到构建者的发布的组件样式,确保无缝衔接组件更新。

FusionCool也可以简单理解为是设计端使用的sketch 插件,达到sketch 既能设计页面,又能沉淀已经设计完成的模板。即设计师使用的同一套规范的组件,产出的设计稿都是同一套规范。

IceWorks

飞冰(ICE) 是一套基于 React 的中后台应用解决方案,ICE 包含了一条从设计端到开发端的完整链路,帮助用户快速搭建属于自己的中后台应用,开发者无须关注环境的问题,并且有海量物料可用。目前已经和 Fusion 的物料体系打通,可以轻松使用 Fusion 站点的物料。

Iceworks 是淘宝飞冰团队开发的面向前端开发者的 GUI 工具,开发者无须关注环境的问题,并且有海量物料可用。目前已经和 Fusion 的物料体系打通,可以轻松使用 Fusion 站点的物料。

fusion design的御用开发者工具,基于其开发各种组件丰富fusion design站点的物料中心,当然iceworks也能轻松使用 Fusion 站点的物料,两者互通。

总之

我个人认为fusion design的价值在于提升工作效率,因为它改造了前端(设计师和研发)的工作方式,减少了重复工作的内容,减少了沟通以及甩锅的成本,通过fusion design这个平台,让设计师和研发都能深度参与产品中来且这种参与是互补共赢的,它让设计师和研发之间的一些壁垒或者冲突点慢慢的消失了。

另外

对于角色(设计、研发)来说:可能最大变化就是对于通用性的、沉淀下来的物料,UCD才是老板,这块的样式布局等需要UCD统一把关收口,研发只需要更新包就行。总体上就是UCD的工作内容会增加但是研发时间会减少,协作时间也会减少,同时体验一致性也能达到要求。我估计能达到这种状态应该就可以要自行车了吧?

但是这玩意都是线上的,不知道能不能支持本地搭一套(如果不能搭是不是又不能要自行车了)?

题外篇:iceworks server

如需使用iceworks提供的一些快捷能力,比如新建项目(基于fusion design、react、typescript)、项目管理等。

用于练手刚好。

1.安装iceworks

npm install iceworks -g –registry=https://registry.npm.taobao.org

每个命令大家都可以玩一玩,我下面只介绍start的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
D:\baymax_projects\fusion-design-one>iceworks -h
Usage: iceworks <command> [options]

Options:
-V, --version output the version number
-h, --help output usage information

Commands:
start start and open the iceworks
init [type] [npmName] init project/material/component by template
add [options] [materialType] [npmName] add block to current directory
generate generate material collection data(material.json)
sync [options] sync materials data to Fusion Material Center
use <version> specify the iceworks-core version
config [type] [key] [value] operate iceworks global config

Run iceworks <command> --help for detailed usage of given command.
2.启动 安装iceworks-server

windows:

1
iceworks  start

linux:

1
2
3
#!/bin/sh
# iceworks start
iceworks # open http://localhost:8000/

3.若提示是否安装iceworks-server 直接Enter 默认是 稍等几分钟 自动启动浏览器

iceworks server使用方法

1.打开项目,(首先你要有项目包)

2.安装依赖

如果要切换cnpm源,设置包管理工具cnpm(前提是现状了cnpm),如不需要则跳过此步。

img

img

3.启动服务

当然你也可以本地运行

4.当页面变成这样说明已经启动成功:(会自动跳转到项目页面)

5.打开编辑器

img

感谢:

背景

其实老早就想做监控,之前整理的前端埋点(一)里头有说一些相关的内容。

不痛就不慌,所以说说我们痛的地方:

  1. 因为我们的产品都是部署在客户内网的,所以对于debug及其不友好,客户现场问题排查前端几乎没有任何输入,难弄。
  2. 产品迭代了多个版本,但是没有任何客户现场的用户行为等数据,产品优化少了一些输入。

本来计划是自研,但是由于业务压力突然来袭,所以就搁置了,不过我犹不放弃,觉得自研既然短期不现实,那可以站在巨人肩膀上搞一搞。

找了两个工具Sentry+rrweb,基于两个工具做一些定制化,手里不就有东西了吗。

Sentry

Sentry的应用程序监视平台可为每位开发人员提供帮助诊断,修复和优化其代码的性能。

config

config for JavaScript

React

features

package

@sentry/tracing 性能监控

1
2
# Using npm
$ npm install --save @sentry/react @sentry/tracing

config

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Sentry.init({
dsn: 'http://cb4e9b434f004c53a51af8ab45346635@172.17.162.101:9100/2',
integrations: [new Integrations.BrowserTracing()],//性能监控配置
// beforeSend (event, hint) {
// // Check if it is an exception, and if so, show the report dialog 错误弹窗
// if (event.exception) {
// Sentry.showReportDialog({ eventId: event.event_id });
// }
// return event;
// },
debug: false,// 调试模式
attachStacktrace: false,//附上堆栈信息
tracesSampleRate: 1,// Be sure to lower this in production
environment: 'development',
// logLevel: 2,
release: 'sentryTest-0.1.0'
});
1
2
3
4
5
6
7
8
9
10
11
// 创建一个 全局scope ,可以理解为上报数据的附加信息
Sentry.configureScope((scope) => {
//标签,可用于筛选
scope.setTag("first-tag", "Guide");
//绑定用户信息
scope.setUser({
id: 1,
name: "gamehu",
email: "gamehu@yeah.net",
});
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//局部:自定义上下文,补充信息
Sentry.setContext("zhangsan", {
name: "zhangsan",
age: 19,
attack_type: "zhangsan",
});
// 创建一个零时到 scope ,配置到 context 上面
const scope = new Sentry.Scope();
scope.setTag("section", "articles");
scope.setUser({
id: 2,
name: "zhangsan",
email: "zhangsan@yeah.net",
});

sourceMap

sentry-cli releases -o 组织 -p 项目 files staging@1.0.1 upload-sourcemaps js文件所在目录 –url-prefix 线上资源URI

sentry-cli releases files sentryTest-0.1.0 upload-sourcemaps –url-prefixhttp://172.17.162.101:9100/organizations/sentry/issues/61/?project=2&query=is%3Aunresolved‘ ‘./dist/static/js’

添加一个 EventProcessor 对全局生效

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

const attachmentUrlFromDsn = (dsn, eventId) => {
const { host, path, projectId, port, protocol, user } = dsn;
return `${protocol}://${host}${port !== '' ? `:${port}` : ''}${path !== '' ? `/${path}` : ''
}/api/${projectId}/events/${eventId}/attachments/?sentry_key=${user}&sentry_version=7&sentry_client=custom-javascript`;
}

//添加一个 EventProcessor 对全局生效
Sentry.addGlobalEventProcessor((event) => {
try {
const client = Sentry.getCurrentHub().getClient();
const endpoint = attachmentUrlFromDsn(
client.getDsn(),
event.event_id
);
const formData = new FormData();
const data = JSON.stringify({ logEntries: ["sentryTest"], message: event.message, logger: event.logger });
formData.append(
'my-attachment',
new Blob([data], {
type: 'application/json',
}),
event.event_id + '.json'
);
fetch(endpoint, {
method: 'POST',
body: formData,
}).catch((ex) => {
// we have to catch this otherwise it throws an infinite loop in Sentry
console.error(ex);
});
return event;
} catch (ex) {
console.error(ex);
}
});

RRWEB/TimeCat

录制回放工具,可单独使用也可搭配Sentry使用,可对用户操作录屏,针对一些现场问题可作为排查问题得输入.

rrweb使用指南

preview

监控理论的记录

要做监控先做设计,根据产品、研发、测试等的输入,整理出监控数据类别:

  • JS 的异常错误;
  • 资源测速;
  • 接口的成功率、失败率;
  • 性能。

img

img

img

本文引用的内容,如有侵权请联系我删除,给您带来的不便我很抱歉。

背景

前面的文章已经说过很多了,因为经常会到服务器上操作,所以记录几个常用的Linux命令。

ls命令

ls命令应该说是我接触的linux的第一批命令之一,属于没事就敲个ls装逼的程度,但是用下来发现也是一个宝藏命令,实用性很强。

ls

直接输入ls,不带任何其它选项,该ls命令提供有关由命令行上给定路径指向的每个文件对象的信息:

  • 只显示路径指向的文件对象的名称;
  • 如果该路径指向目录(或指向目录的符号链接),则该ls命令还会列出该目录的内容;
  • 如果显示多个条目,则将按文件名的字母顺序对其进行排序。

最后,当在命令行上未给出路径时,这些ls命令将采用./–即当前目录。

ls -l: 长格式显示

除文件名称外,亦将日期和时间、权限、拥有者、文件大小等资讯详细列出,我们经常使用的ll其实就是它的别名。

ls -a: 显示所有文件

使用该-a选项时,在显示目录内容时ls包括隐藏文件。但是什么是隐藏文件?

隐藏文件:名称以点开头的文件被视为隐藏文件。此外,每个目录还包含两个特殊的,通常为隐藏的条目:...

在每个目录中:

  • .条目指向目录本身。这种自我指称似乎很奇怪。但这有时很有用,有点像将自己的电话号码存储到智能手机库中。
  • ..条目指向父目录。由于类Unix系统上的文件层次结构严格地组织为一棵树,因此每个目录只有一个父目录。
ls -s: 显示文件分配的大小

这里大小的单位是,在linux中一块可以看作是1024字节,该大小指的不是逻辑大小而是实际大小。如下所示,a、b都是2097152,但是通过块的方式查看b只有1028,因为b目录下由 sparse files

1
2
3
sh:~/ls$ ls -ls a b
2052 -rw-r--r-- 1 sylvain sylvain 2097152 Sep 19 22:18 a
1028 -rw-r--r-- 1 sylvain sylvain 2097152 Sep 19 22:18 b
ls -h: 可读性强的方式显示文件大小

使用-h选项,ls将使用单位后缀显示文件大小,以使其更加用户友好。如图直接使用-h是没用的,结合l和s使用。

ls -d */: 只显示目录(文件夹)
ls -i: 显示文件的索引号

有点我们说的引用地址的意思,该选项在查看文件的硬、软链接时比较有用。比如查看某几个文件是否引用同一基础文件系统对象。

比如下图,切换到根目录,然后ls -ia你会发现... 的索引号都是2,证明指向的是同一个目录,这刚好可以解释根目录的父目录就是根目录自身。

ls ../

查看父目录文件。

ls ~

查看主目录的文件。

排序

ls -t :按修改时间倒序,最近修改的在前。

ls -S: 按文件大小正序,最小在前。

ls -r:反转排序。比如ls -rS,则会变成最大的在前。

ls -R:递归列出子目录,跟find .效果类似。

时间完整显示

ls --full-time:显示完整日期及时间。

OK

结束了,以上列举的80%都是我日常经常用的,希望对大家有帮助。

感谢

背景

我们是做NPMD工具的,但是对自身的产品本身确没有监控,说起来就很惆怅了,当然主要原因还是产品从0到1这个过程,前期放荡不羁的功能造作,导致对基础设施建设这块做的很少。

这是一贯坚持基础建设重要性的我的又一篇分享。关于前端埋点。

所以我当然承认基础建设是体系化的漫长的一个过程,但是因为各种不可抗因素,现没法体系化落地,所以只能先捡最能产生价值的且能引起更多不同之能的同事关注的事开始做,比如前端埋点。

先说痛点:

  1. 整个质量体系监控缺失,前端报错后端报错,全靠经验、人肉日志和用户主动反馈。

    特别是现场问题排查,通常都会先找到前端定位问题,现场又不能远程。

  2. 业务数据的效果无从跟踪。

    如使用某功能的频率无从得知,需要人肉从客户处拿此类数据,且还不准。

  3. 用户的访问行为/设备特征/应用性能信息完全无感知。

    如活跃时间点(避开做升级),软硬件系统和设备比例(做兼容),慢页面优化等无从做起。

前端埋点对我们产品的好处:

  1. 记录访问行为/设备特征/应用性能信息。

    为产品的设计,提供参考数据,对于我们这种从0到1的ToB产品,我个人觉得是特别需要这类用户反馈数据的,避免闭门造车。

    为产品的优化(性能、产品设计…)提供参考。

  2. 记录异常。

    便于问题的排查,我们处理的问题一般是客户现场问题,通常情况下又都是客户的内网,所以是无法远程的,这类异常记录手段刚好能丰富异常记录数据,便于问题的排查。

所以可以看出,前端埋点至少会影响的到产品、UCD、研发、测试,当利益方多的时候,事情才更有推下去的可能性。

埋点

决策源于数据,而数据源于采集,采集源于规则梳理,让这一切发生源于工程师的创造力和执行力。

埋点的方式

  1. 手动埋点

    手动代码埋点比较常见,即需要在采集数据的地方调用埋点的方法。

    优点是流量可控,业务方可以根据需要在任意地点任意场景进行数据采集,采集信息也完全由业务方来控制。这样的有点也带来了一些弊端,需要业务方来写死方法,如果采集方案变了,业务方也需要重新修改代码,重新发布。

    百度统计等第三方数据统计服务商大都采用这种方案;

  2. 可视化埋点

    即通过可视化工具配置采集节点,在前端自动解析配置并上报埋点数据。

    优点业务方工作量少,缺点则是技术上推广和实现起来有点难(业务方前端代码规范是个大前提,比如唯一ID等)。

    代表方案是已经开源的Mixpanel

  3. “无痕埋点”

    无痕埋点则是前端自动采集全部事件,上报埋点数据,由后端来过滤和计算出有用的数据,优点是前端只要加载埋点脚本。缺点是流量和采集的数据过于庞大,服务器性能压力山大,主流的 GrowingIO 就是这种实现方案。

那我们产品现阶段比较适合的方式时什么呢?

手动代码埋点的方案,代码埋点虽然使用起来灵活,但是开发成本较高且对业务代码有强侵入性,并且一旦发布就很难修改,更何况我们这是部署在客户现场的。

可视化埋点也是不太可行的,我们连工程化、规范化都没做到,而且是部署在客户现场的,先天不足,再一个可视化埋点通常基于xpath的方式,只能读取页面上标签元素展示出来的属性,不能够获取上下文(通常内存里)的一些属性。而且页面的结构发生变化时,需重新标记操作。

所以几乎只有考虑无痕埋点,虽然无痕埋点流量消耗和数据计算成本很高,但是因为我们是ToB运维工具,企业内部用的,使用人数也不多,所以也还好,当然这是第一版如果确实在实验局发现压力挺大,我们可以再改进一下,比如采用手动埋点+无痕埋点的方式或者三种结合的方式。

OK先把埋点方式定下来了,我们现在考虑剩下的东西。

埋点需求整理原则

埋点不能乱埋,埋点的原则是基于一系列问题展开,并基于这些问题确定埋点需求,怎么确定埋点需求,可以照下面的问题切入进行梳理

HOW:

  • 怎样证明新功能效果?
  • 需要哪些埋点?我们要采集什么内容,进行哪些采集接口的约定?
  • 我该怎么埋这些点?
  • 部分埋点的计算逻辑是什么?
  • 数据结果怎么看?
  • 通过什么方式来调用我们的采集脚本?
  • 无埋点:考虑到数据量对于服务器的压力,我们需要对无埋点进行开关配置,可以配置进行哪些元素进行无埋点采集

WHO:

  • 我的产品设计面对的用户群里是谁?
  • 用户特征是什么?
  • 这部分特征用户对功能预期的数据结果是什么?
  • 用户习惯是什么?

WHAT:

  • 产品包含哪几个模块?

WHERE:

  • 新功能展示在产品端的哪个位置?
  • 在哪些版本生效?
  • 哪些功能的展示或作用在哪里需要跟服务端等交互?

WHEN:

  • 功能是在用户场景什么时候调用产生?
  • 调用过程中什么时候和服务端交互?
  • 功能调用正常情况下需要大概需要多长时间?
  • 什么情况会影响调用结果?
  • 调用有风险的时候,是否需要加监测?

回答了上面的问题,基本上能知道埋点的意义在哪儿以及需要收集哪些数据等,接下来就得开始指定埋点规范了。

埋点规范

埋点规范就跟编码规范一样,不按照规范就会有很大的隐患比如以下问题:

  1. 埋点混乱

  2. 常常埋错,漏埋

  3. 业务变化后,老埋点数据失去意义

  4. 埋点数据无人使用,浪费资源

  5. 数据团队、研发团队、产品团队协作配合难度大

  6. 很多时候不太重视数据,而是重视业务的快速上线

  7. 埋点语义不明确,同一个按钮存在多种描述,不易检索

  8. 无用/重复埋点太多,干扰了正常埋点数据

  9. 大量存量埋点Owner离职或者转岗,导致大量僵尸埋点信息

    所以为了避免以上问题我们需要建立一个好的规范,比如命名规范和流程规范。

    埋点命名规范

    我们当前的做法是埋点名称只能是由字母、数字、下划线组成,并保证在应用内唯一,比如sw是展示,ck是点击。

    常用规则的举例如下:
    比如行为埋点:{团队|业务|角色}_{组件|页面}_{具体元素}_{动作}
    示例:
    front_alarm_sw : front代表项目,alarm代表功能,sw是展示,ck是点击
    front_alarm1_detail_table_point_ck :front代表项目,alarm1_detail代表功能,table组件,point小圆点组件,ck点击

埋点流程规范

如果你发现每天有大量埋点错误反馈,并导致很多错误的分析结论,一个错误的分析结果还不如没有数据分析结果。造成这个问题的原因包括:

  1. 前端埋点涉及复杂的交互,难以找准埋点位置;
    1. 埋点的验收流程不完善,没有经过测试和产品经理的测试和验收,验证不完备;

好的埋点需求应该和业务功能需求同等重要,也需要经历软件开发的整个生命周期,如果能严格按照一个规范的流程处理埋点,以上问题会得到更好的解决。

规范内容

需求阶段:

确定埋点信息,核心包括五方面信息:事件ID、埋点名称、埋点描述、埋点属性以及截图。

如何落地:

如果不按照规则和流程埋点将不会上报相关数据,制定强制规定。

开发什么功能:

埋点全文检索能力、自动找到重复埋点(语义相近或者数据相近)功能。

开发阶段:

务必和开发沟通,并让开发在完全理解业务语义的情况下,在前端按照埋点代码规范进行埋点。

如何落地:

静态代码检查。

开发什么功能:

开发探测每个埋点对应到的代码文件和代码行,开发根据人均事件量级进行监控报警功能。

测试阶段:

务必和测试沟通,并让测试在完全理解业务语义的情况下,通过CodeReview和埋点测试两种方式对埋点正确性进行验证。

如何落地:

规定如果未经测试的埋点不允许上报埋点数据。

开发什么功能:

提供所见即所得的埋点测试平台。

验收阶段:

确保相关人员在完全理解业务语义的情况下,可以通过与自比较和他比较的方式来进行验证。

举例:

- 他比较验证:前端某业务点数量和对应服务端数据应该基本相当。

如何落地:

规定如果未经验证的埋点不允许上报埋点数据。

开发什么功能:

提供埋点实时数据查询。

清理阶段:

确认完全理解语义的情况下,可对业务发生巨大变化或者不在维护的埋点进行废弃。

如何落地:

3个月以上未被使用的埋点、1个月以上数据为0的埋点自动废弃。3个月后使用次日会自动开启采集。

开发什么功能:

根据规则提供针对未使用埋点的计算、并自动废弃。

可以看出,规范要落地,需要整个公司的共识,也需要从上而下的压力,还有强势的制度。比如针对全公司个部门做评分,评分规则基于埋点和数据分析抽象出来。

另外我们在前端埋点中也遇到过一些注意事项,整理如下,希望对大家有所帮助。

注意事项:

一些埋点安全性问题:

如果你使用普通的http 协议,在数据传输的过程存在被劫持(包括不限于:JS代码注入等)的可能性,造成H5页面中出现诸如:未知的广告或者原网页被重定向等问题。为了降低此类事件发生的可能性,建议最好使用https 协议(倡导全站https),以确保数据传输过程的完整性,安全性。

版本迭代的时候埋点需要注意什么?

  • 如果是公用模块,可以非常放心安全的全量迁移埋点;
    • 如果是新增模块,那就需要注意是否遵循了最新的埋点规范,有没有重复的埋点命名存在,埋点是否符合业务逻辑;
    • 如果是下线模块,那就需要评估下线后对数据的影响范围,而且要第一时间充分周知相关方,让各方参与评估;
    • 如果是更新模块,需要评估在原有埋点上需要修改的埋点内容,是否需要重新埋点,删除不需要的埋点,而且要修改功能的数据承接。

感谢