GithubHelp home page GithubHelp logo

articles's Issues

小程序异常监控收集

前言

你是否经常碰到业务反馈,线上的小程序某个页面打不开了,订单没法结算了,但是你当时测试的时候都是好好的。

由于线上环境复杂,一些问题只会在特定网络环境或者设备上发生,对于这类问题,异常信息的收集就显得格外重要了,我们不但希望收集错误的堆栈信息,还需要用户操作流程,设备信息等,以便复现错误。

简单收集

小程序App()生命周期里提供了onError函数,可以通过在onError里收集异常信息

App({
  // 监听错误
  onError: function (err) {
    // 上报错误
    wx.request({
      url: "https://url", // 自行定义报告服务器
      method: "POST",
      errMsg: err
    })
  }
})

用户操作路径收集

一些较隐蔽的错误如果只有错误栈信息,排查起来会比较难,如果有用户操作的路径,在排查时就方便多了。

方法一:暴力打点方法收集

优点:简单直接

缺点:污染业务代码,造成较多垃圾代码

方法二:函数劫持(推荐使用)

需要在App函数中的onLaunch、onShow、onHide生命周期插入监控代码,我们通过重写App生命周期函数来实现。

App = function(app) {
    ["onLaunch", "onShow", "onHide"].forEach(methodName => {
        app[methodName] = function(options) {
          // 构造访问日志对象
          var breadcrumb = {
            type: "function",
            time: utils.now(),
            belong: "App", // 来源
            method: methodName,
            path: options && options.path, // 页面路径
            query: options && options.query, // 页面参数
            scene: options && options.scene // 场景编号
          };
          self.pushToBreadcrumb(breadcrumb); // 把执行对象加入到面包屑中
    })
}

但是这样写,会把用户自定义的内容给覆盖掉,所以我们还需要把用户定义的函数和监控代码合并。

 var originApp = App // 保存原对象
 App = function(app) {
 	// .... 此处省略监控代码
 	// .... 此处省略监控代码
 	originApp(app) // 执行用户定义的方法
 }

记录结果

可以从下面的json看出,用户到了detail页面,执行了onLoad => getDetail => onReady => buy 当执行buy方法的时候报错。

[{"method":"onLoad","route":"pages/film/detail","options":{"id":"4206"}},
{"method":"getDetail","route":"pages/film/detail","options":{"id":"4206"}},{"method":"onReady","route":"pages/film/detail","options":{"id":"4206"}},{"method":"buy","route":"pages/film/detail","options":{"id":"4206"}}]

上报策略

考虑到在大型应用中,日志量比较大,我们采取抽样,合并,过滤三个方法减少日志的输出,代码实现可以参考lib/report.js

代码组织

项目使用rollup作为构建工作,实现ES6转ES5,模块加载功能。

项目目录如下:

config.js  // 配置文件
core.js	 // 劫持小程序核心代码
events.js  // 监听自定义事件
report.js // 上报类
utils.js // 工具类

🌟喜欢的点个star:

https://github.com/zhengguorong/xbossdebug-wechat

参考资料

fundebug

前端异常监控系统落地

利用端到端测试实现自动批改作业

背景

目前就职的培训机构,老师会给学生布置每日作业或者阶段考试。检查学生作业时非常耗时的,你要不断的打开学生完成的页面,验证功能的完整性,例如一个涉及到ajax请求的增删改查功能,平均需要花费5分钟检查一个,一个班平均50人每次检查作业就要250分钟,如果持续做这件事情,会耗费太多时间。

问题分析

检查作业的目的就是验证功能的完整性,那在企业中验证功能的完整性,一般会利用单元测试或者e2e测试验证。单元测试多用于对函数或者代码块的验证,在该场景中不适用,因为作业中包含html页面,页面中包含js逻辑,所以e2e测试会更适合该场景的适用。我们可以把学生的代码当成黑盒子,只验证最终实现效果即可。

解决方案

e2e测试框架有很多,但是由于我以前只使用过jest编写单元测试,对jest的语法比较熟悉,所以这次编写e2e测试,也使用jest这个框架来实现。由于需要进行dom操作,jest提供了一个叫jest-puppeteer的库用来处理dom,它其实是基于puppeteer做的二次封装,使用API和puppeteer差不多,但是做了一些优化。

jest.config.js配置运行环境

jest-puppeteer给我们提供了jest与puppeteer快速整合的能力,它会自动在我们的测试文件自动注入pagebrowser 对象,你只需要调用page对象打开页面即可,省去不少麻烦事,当然如果你需要自行配置,也是可以的。
如果你需要让jest-puppeteer帮你配置,需要在根目录创建一个jest.config.js文件,写入preset: "jest-puppeteer"这句即可。

module.exports = {
  preset: "jest-puppeteer",
  testEnvironment: "./custom-environment.js"
};

testEnvironment自定义启动脚本

在运行测试前,往往需要一些准备工作,例如,我测试的内容是一些静态页面,而puppeteer只能访问http页面(可能访问本地路径也可以,没试过),我就需要在测试前启动一个静态服务,那我们可以利用setup设置运行脚本前做的事情,teardown设置结束脚本时做的事情。

const PuppeteerEnvironment = require('jest-environment-puppeteer')
const express = require('express');

class CustomEnvironment extends PuppeteerEnvironment {
  async setup() {
    const app = express();
    app.use(express.static('英雄作业'));
    this.server = app.listen(3000);
    await super.setup()
  }

  async teardown() {
    // Your teardown
    this.server.close();
    await super.teardown()
  }
}

module.exports = CustomEnvironment

测试首页脚本

下面脚本演示首页功能的测试,首页包含列表和删除两个功能。每个功能的测试其实只是模拟人为的点击或输入,然后判定得到的行为是否符合预期。
写脚本的时候需要注意每个断言应该是无依赖的,否则容易因为移动代码而导致测试无法通过,你可以在执行断言前初始化数据,在测试完毕清理数据。

const axios = require('axios');
const { stringify } = require('querystring');
const fs = require('fs');
const path = require('path');

// 动态获取参数。例如jest ----runInBand --verbose --name='陈莲'
const name = require("minimist")(process.argv.slice(2))["name"]; // "陈莲"

const baseUrl = 'http://localhost:3000/' + name;

const mockData = {
  name: 'test',
  gender: '男',
  img: 'http://test.com/test.png'
}

describe('英雄首页', () => {
  beforeAll(async () => {
    // 测试前准备环境,先给系统添加一个模拟数据
    await axios.post('http://127.0.0.1:3001/addHero', stringify(mockData), {
      headers: {
        'content-type': 'application/x-www-form-urlencoded',
      },
    });
    await page.goto(baseUrl + '/index.html');
    await page.waitFor(1000);
  });
  it('显示英雄列表', async () => {
    // 判断是否正确循环数据
    const herosElementLength = await page.evaluate(
      () => document.querySelectorAll('tbody tr').length
    );
    expect(herosElementLength).toBe(1);

    // 判断元素是否正确渲染出来
    const bodyHTML = await page.evaluate(() => document.body.innerHTML);
    expect(bodyHTML).toContain(mockData.name);
    expect(bodyHTML).toContain(mockData.img);
    expect(bodyHTML).toContain(mockData.gender);
  });
  it('删除英雄', async () => {
    // 如果点击删除后,有提示框,可以通过监听dialog事件处,dialog.accpet方法用于模拟点击确定
    page.on('dialog', async dialog => {
      await dialog.accept();
    });
    await page.evaluate(() => {
      var aTags = document.getElementsByTagName('a');
      var searchText = '删除';
      var found;
      for (var i = 0; i < aTags.length; i++) {
        if (aTags[i].textContent == searchText) {
          found = aTags[i];
          break;
        }
      }
      found.click()
    });

    await page.waitFor(1000);
    
    const res = await axios.get('http://127.0.0.1:3001/getHeroList')
    const { data } = res.data;
    expect(data.length).toBe(0);
  });
  afterAll(async () => {
    // 注意测试完毕,需要清除数据,否则会导致影响下一个测试
    fs.writeFileSync(path.join(__dirname, '../server/heimaHero.json'), '[]')
  })
}, 20000);

运行测试脚本

jest ----runInBand --verbose --name='陈莲'

----runInBand jest为了提升执行测试用例的速度,会把不同的测试文件放在不同线程同时执行,但这如果你的测试是有依赖关系,不能同时执行的,可以加入这个参数。
--verbose 表示输出详细的测试结果
--name 是自定义参数

测试结果

截图显示,该同学完成了6个需求,1个需求没有通过。经过复查该同学代码,发现和我们jest测试结果一致
image

用nginx负载均衡,提高并发

上篇文章说到用ab做压力测试,单台服务器出现cpu瓶颈。
为了提高并发,可以从两方面扩展,纵向扩展(提升单台服务器性能),横向扩展(增加机器)。
纵向扩展,成本是比较大的,而且容易到顶,随着业务增加,还是撑不住。
所以我们要做分布式方案,这样可以随着业务扩展,租用更多机器来扛住压力。

目前软负载比较简单的方式就是用nginx了,当然你也可以硬负载,不过我没接触过,只是听过而已,据说很贵。

那我下面就介绍nginx配置方法。
我两台机器配置都没1核1G内存。
直接看nginx配置吧

upstream backend // 这个名字随便起,用于下面做反向代理
{
  server 10.104.136.40; // A机器的地址
  server 10.104.243.10; // B机器的地址
}

// 监听80端口,做反向代理
server {
    listen       80 default_server;
#    listen       [::]:80 default_server;
    server_name  _;
    root         /usr/share/nginx/html;

    # Load configuration files for the default server block.
    include /etc/nginx/default.d/*.conf;

        location / {
            proxy_pass  http://backend;  // upstream里定义的名字
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            root   html;
            index  index.html index.htm;
        }

    error_page 404 /404.html;
        location = /40x.html {
    }

    error_page 500 502 503 504 /50x.html;
        location = /50x.html {
    }

}

就这样,reload nginx就生效了,为了测试是否成功负载,可以在两个应用返回不同信息,用浏览器访问,看是否会自动切换。

如果测试时,出现较多的异常,可以查看nginx 的error log,定位问题
如果出现1024 worker_connections are not enough
可以修改/etc/nginx/nginx.conf

events {
    worker_connections 20000;
}

下面为3000个并发,用分布式方案的结果,比单机平均处理时间降低了1秒。

Concurrency Level:      3000
Time taken for tests:   11.291 seconds
Complete requests:      20000
Failed requests:        0
Total transferred:      2400000 bytes
HTML transferred:       0 bytes
Requests per second:    1771.31 [#/sec] (mean)
Time per request:       1693.658 [ms] (mean)
Time per request:       0.565 [ms] (mean, across all concurrent requests)
Transfer rate:          207.58 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0  204 892.5      0    7028
Processing:     1  279 587.7    134    7020
Waiting:        1  279 587.7    134    7020
Total:          1  483 1111.1    175    8710

Percentage of the requests served within a certain time (ms)
  50%    175
  66%    245
  75%    343
  80%    411
  90%   1187
  95%   1972
  98%   4294
  99%   7279
 100%   8710 (longest request)

自动化持续集成系列 -- github + travis ci

前言

如果是个人项目或者是开源项目,可以利用travis ci部署持续集成,同时travis ci的配置比gitlab CI简单,也不需要自己维护服务器,是个不错的选择。

下面讲解如何进行集成。

step 1、travis ci绑定github项目

登陆travis ci网站,并使用github账户授权登陆,添加需要集成的项目,如下图。

step 2、配置构建任务

在项目根目录创建.travis.yml文件。
配置执行环境和执行命令,配置如下。

language: node_js
node_js:
  - '8'
install:
  - npm i npminstall && npminstall
script:
  - npm run lint
  - npm run test
  - npm run dev
  - npm run build

配置完成后,提交一个commit后,travis会自动执行构建,界面如下。

image

获取覆盖率报告

我们使用jest运行单元测试后,一般会输出覆盖率目录coverage,我们可以利用codecov进行在线查看。

1、在package.json加入依赖

    "codecov": "^3.1.0",

2、.travis.yml加入构建任务

 - npm run codecov

3、执行构建后,可以在线查看覆盖情况。

image

代码质量管理

sonarcloud提供travis ci快速集成方案,在sonarclound创建项目后,会提供对应token,在.travis.yml加入任务即可。
最后项目完整配置如下。

addons:
  sonarcloud:
    organization: "zhengguorong-github" 
    token:
      secure: XXXXXXXXXXXXXX
language: node_js
node_js:
  - '8'
install:
  - npm i npminstall && npminstall
script:
  - npm run lint
  - npm run test
  - npm run dev
  - npm run build
  - npm run codecov
  - sonar-scanner

构建完毕后,可以在sonarcloud查看报告。

image

最后

获取项目的状态,放在最醒目的位置,让别人放心的使用你的开源项目吧🎉

image

jwt在node中的应用

什么是jwt

这个文章已经解释得很清楚了传送门

jwt和session的区别

session:一般用于服务端存储信息,其生命周期会随服务器重启而终止,或者由代码清除。
常常用于web应用登录状态的保存,但是在ios/android应用中,网络请求不包含session信息,因此服务端session无法使用,这是就产生了token。
token:作为用户状态的凭证。用户登录成功后,服务端生成一条token信息,该token可以包含用户id,过期时间等信息,经过加密算法返回给客户端,客户端访问时将该token带上,服务端做权限校验。

废话少说,直接上代码

auth.service.js核心代码,用于生成token和验证token:
1、安装express-jwt github地址
2、定义产生token方法,role为附加信息,用来做角色权限控制
config.secrets.session为密钥,一个字符串。

module.exports.signToken = (id, role) => {
    return jwt.sign({_id: id, role}, config.secrets.session, {
        expiresIn: 60 * 60 * 5 // 过期时间 表示5小时过期
    })
}

3、定义验证token方法isAuthenticated,以下是截取部分关键代码。

// 从请求头获取token
var token = req.headers.authorization.split('Bearer ')[1]
      // 查找数据库是否有该token
      UserController.findByToken(token).then((user) => {
      if (user) {
       //验证token是否过期
         validateJwt(req, res, next);
       }else{
         return res.status(401).end();
      }
 })

开发登录和权限验证功能:
1、定义登录方法

module.exports.login = (req, res) => {
  var loginId = req.body.loginId
  var password = req.body.password
  let token
// 数据库查找用户
  return User.findOne({ loginId: loginId }).exec()
    .then(user => {
   // 验证密码是否正确
      if (user && user.authenticate(password)) {
   // 产生token
        token = jwt.sign({ _id: user._id }, config.secrets.session, {
          expiresIn: 60 * 60 * 5
        })
        user.token = token
        var updateUser = JSON.parse(JSON.stringify(user))
        delete updateUser._id
        User.findOneAndUpdate({ _id: user._id }, updateUser).exec()
        // 返回给客户端
        res.status(200).json({ token }).end()
      } else {
        return res.status(401).end()
      }
    })
}

2、验证用户身份,参考auth.service.js isAuthenticated方法

问题来了

如何刷新token?
express-jwt这个库没有提供刷新token过期时间的方法,因此我们需要后端重新创建token,让客户端更新token。为了保证安全性,我们不能因token失效就刷新,那这样token就有可能被黑客嗅探利用。
因此,解决方案应该是当token过期时间小于一小时,后端返回新的token,token放在返回头,当前端发现返回头带有token信息,即更新token。
当token已经过期,就返回401,提示用户重新登录。

项目源码 github地址

项目演示

项目演示

自动化持续集成系列 -- gitlab CI安装和任务配置

gitlab安装

安装比较简单,跟着官网的教程,复制粘贴命令
参考地址:https://about.gitlab.com/installation/#ubuntu
安装完毕后浏览器输入服务器地址,gitlab默认使用80端口,如果正常打开,输入密码,默认账户为root。

gitlab-runner安装

因为gitlab CI需要依赖gitlab-runner执行任务,我们也还是按官方教程安装。
参考地址:https://docs.gitlab.com/runner/install/linux-repository.html

docker安装

gitlab-runner需要依赖docker作为构建环境,当然这不是非必要,你可以选择其他依赖,不过个人建议还是用docker比较方便。
docker安装参考地址:https://docs.docker.com/install/linux/docker-ce/ubuntu/

配置runner

gitlab-runner需要和gitlab做绑定,因此需要执行register操作。
1、执行以下命令

sudo gitlab-runner register

2、输入gitlab实例地址

Please enter the gitlab-ci coordinator URL (e.g. https://gitlab.com )
https://gitlab.com(这个地址是你安装gitlab服务器地址)

3、输入token,可以在CI/CD菜单下Runners查看token

Please enter the gitlab-ci token for this runner
xxx

4、给你的runner输入个描述内容,随便填就好

Please enter the gitlab-ci description for this runner
[hostame] my-runner

5、给你的runner设置tag,随便输入

Please enter the gitlab-ci description for this runner
[hostame] my-runner

6、输入runner执行环境,我们用docker,你也可以用其他环境。

Please enter the executor: ssh, docker+machine, docker-ssh+machine, kubernetes, docker, parallels, virtualbox, docker-ssh, shell:
docker

7、配置默认docker镜像,用于当.gitlab-ci.yml没有配置时的镜像

Please enter the Docker image (eg. ruby:2.1):
alpine:latest

注册成功后,你可以在gitlab的settings -- CI/CD -- Runners看到关联的runner

配置构建任务

在项目根目录新建.gitlab-ci.yml文件

image: node:8
stages:
  - test
  # - build
  
test:
  stage: test
  before_script:
    - npm install
  script:
    - npm run lint
    - npm run test
  cache:
    key: "$CI_BUILD_REF_NAME"
    paths:
      - 'node_modules/'

配置完毕后,当提交代码或者进行RP的时候,CI会自动执行构建任务,我们的配置文件执行两个任务,检查代码风格和执行单元测试。

提交代码后,CI开始运行,如下图:

image

或者提交RP,走code review流程:

image

代码质量扫描

gitlab 9.3之后支持静态代码检测,可以添加以下任务
官方文档:https://docs.gitlab.com/ee/user/project/merge_requests/code_quality.html

code_quality:
  image: docker:stable
  variables:
    DOCKER_DRIVER: overlay2
  allow_failure: true
  services:
    - docker:stable-dind
  script:
    - export SP_VERSION=$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/')
    - docker run
        --env SOURCE_CODE="$PWD"
        --volume "$PWD":/code
        --volume /var/run/docker.sock:/var/run/docker.sock
        "registry.gitlab.com/gitlab-org/security-products/codequality:$SP_VERSION" /code
  artifacts:
    paths: [gl-code-quality-report.json]

如果执行任务时,提示权限问题,可以修改runner配置
文件:/etc/gitlab-runner/config.toml
privileged = true

concurrent = 1
check_interval = 0

[[runners]]
  name = "kms"
  url = "http://example.org/ci"
  token = "3234234234234"
  executor = "docker"
  [runners.docker]
    tls_verify = false
    image = "alpine:3.4"
    privileged = true
    disable_cache = false
    volumes = ["/cache"]
  [runners.cache]
    Insecure = false

提交代码运行结果:

image

覆盖率报告

单元测试完毕生成的覆盖率报告,我们希望可以在RP的时候体现或者在构建完毕后显示,可以在setting--CI/CD -- General pipelines -- Test coverage parsing 输入

^All files\s+\|\s+\d+\.*\d*\s+\|\s*(\d+\.*\d*)

当提交RP或者提交代码构建时,就会显示覆盖率报告
image

从《怪诞行为学》思考团队管理

为什么企业中管理模式分为等级管理和扁平化管理呢?这两种模式优缺点在哪里,在哪个阶段应该采取哪种模式?
  书中作者认为,等级制度能够让社会团队的行为具有高度的一致性,但是过强的等级制度可能会限制底层个体的创造力,很可能会扼杀明知的**。根据需求灵活地运用等级制度,可以提升一个团队的整体竞争力。作者发现,在强等级制度的约束下,天才多了反而会影响团队的整体战斗力,因为他们会相互竞争,使效率下降;在弱等级制度的约束下,天才成员就越多越好,因为他们基本都是独立表现,不太依靠他人。

  我个人认为,在团队创建初期,可以形成一定的等级制度,例如一名技术比较资深的开发担任主管角色,管理6个人以下开发团队,同时因为组织赋予了资深开发一定权力,他可以利用该权力较方便的制定开发规范,代码审查,开发流程等。

  如果一开始团队都是资深的开发,没有等级制度的加持,可能会造成谁也不听谁的局面,代码风格各种各样,维护成本将极高,同时损害合作关系。

  团队成熟后,可以采用扁平化管理,充分发挥团队成员的创造力,因为负责该项目的成员,最清楚他的项目,也最清楚他的客户,此时对其权力下发,对项目开发会有较大好处。

  目前B公司实行严格的等级制度,虽然能够快速执行上级的命令,但是创造力严重缺失。在目前同质化严重的市场环境下,未能对客户作出快速响应与定制化市场营销,造成发展滞后。

  在人才管理上,因为采用等级制度,员工较多时间在执行上级命令,导致个人成就感偏低;同时薪酬因等级的限制,员工工作几年后,未能升迁,就会产生人员流失。

自动化持续集成系列 -- 方案对比

前言

面对持续集成两套方案github + travis ci + sonar和自建gitlab ci + sonar两套解决方案,我们到底如何选择呢。他们两者各自有什么优势呢。

我们从成本、应用场景、上手难度三个维度做对比

成本对比

我们假设一个小型开发团队5名前端开发,5名后端开发

方案一:使用github + travis ci + sonarcloud

github私有仓库:¥420 / 月

travis CI:¥774 / 月

sonarcloud 按250K行代码计算:¥450 / 月

总计:1644元 / 月

方案二:gitlab ci + sonar

一台云服务器:2核8G内存阿里云 370元 / 月

总计:370元 / 月

每月差价为1274元,随着团队和项目的增加,这个差距会继续拉大。

应用场景

github方案:

1、小型团队,代码仓库较少
2、代码保密等级不算太高

gitlab方案:

1、较大型团队,代码仓库较多
2、部署脚本较复杂,部署服务器只允许内部访问

上手难度

《自动化持续集成》-- github + travis ci
《自动化持续集成》-- gitlab CI

附录

sonarcloud价格
image

travis CIji价格
image

GitHub
image

单元测试系列 -- 作用远超你想象

单元测试概念

在计算机编程中,单元测试(英语:Unit Testing)又称为模块测试, 是针对程序模块(软件设计的最小单位)来进行正确性检验的测试工作。 -- 维基百科

我们会发现,一些优秀的开源项目,都有较全面的单元测试,但是我们维护的业务系统,却少有单元测试的踪影,难道单元测试实施成本真的这么高吗,下面我们将讨论单元测试有什么优点。

防止产生遗留代码

《修改代码的艺术》中说作者认为,遗留代码就是那些没有编写相应测试的代码,没有编写测试的代码是糟糕的代码。不管我们有多细心地去编写它们,不管它们有多漂亮、面向对象或封装良好,只要没有编写测试,我们实际上就不知道修改后的代码是变得更好还是更糟了。反之,有了测试,我们就能迅速、可验证地修改代码的行为。

降低开发维护成本

当提到单元测试应用到业务系统开发流程时,也许大多数听到的反馈是成本太高,开发周期过长,维护困难。
我们得出这样的结论,也许是因为没搞清楚成本的概念,软件的成本远远不止开发成本,我理解的成本应该像下面这条公式的。

软件成本=开发成本(20%)+ 维护成本(30%)+ 异常成本(50%)

如果你正在维护一套开发超过两年的系统,当你需要添加一个需求或者改动旧代码的时候,你总是小心翼翼的,你不得不认真的理解整块代码,生怕一个改动,会导致旧的功能无法运行。

你担心的事情,总是会发生的,不知道会在什么时候,也许是在业务最关键的时刻爆发,造成业务中断,给企业造成的损失将会远远超出开发成本。

如果你的业务代码包含完整的单元测试,你的公式就变为下面:

软件成本=开发成本(20%)+ 单元测试成本(10%)+ 维护成本(10%)+ 异常成本(10%)

虽然开发过程中需要时间来编写单元测试代码,但是在以后改动需求的时候,你就可以大胆的修改了,单元测试可以帮助你控制软件的质量,你也可以信心满满的把应用部署到线上,万一出现意外,也可以通过单元测试帮助你定位问题。

下面为引用《单元测试的艺术》中的一个段落,两个团队分别使用和不使用单元测试,各项指标的差异。

image

下面为《有效单元测试》中关于bug修复成本的片段

image

提升代码质量

单元测试另外一个很重要的作用是提升代码的质量,表面上单元测试的代码和业务代码是没有关联的,但实际上,测试代码会反推业务代码变得健壮。
我们写业务代码,是否会经常碰到一个函数上百行代码的情况,函数把整个业务逻辑都写在一起,没有分层和分模块。

如果要对上百行的函数进行单元测试,是非常困难的,你需要写非常多的mock代码和非常多的分支判断。
为了能够通过测试,你需要对函数进行拆分,将函数变成一个个小的工作单元。
越是小的工作单元,越是容易进行测试,在编写单元测试的过程中,相当于对代码进行了一次重构。

支持自动化持续集成

自动化持续集成通常包含自动化单元测试,用来保证软件的质量,如果没有单元测试保障代码的可工作性,对于持续发布的软件,会有较大风险。
同时,因为每次提交都进行测试和构建,出现异常时距离开发时间较近,问题的修复成本会比较低,软件的技术债务会被有效的控制。

vue实现dom元素拖动布局

项目演示

完整代码请访问github

源码地址
在线demo账号admin 密码admin

核心对象

下面的操作都基于Element对象,element由父组件传递给子组件,子组件负责监听鼠标实现,修改element对象。
elememt对象属性

export default class Element {
  constructor (ele = {}) {
    this.type = ele.type || 'pic'
    this.imgSrc = ele.imgSrc || ''
    this.left = ele.left || 0
    this.top = ele.top || 0
    this.width = ele.width || 0
    this.height = ele.height || 0
    this.lineHeight = ele.lineHeight || 0
    this.animatedName = ele.animatedName || ''
    this.duration = ele.duration || 1
    this.delay = ele.delay || 0
    this.playing = false
    this.loop = false
    this.opacity = ele.opacity || 100
    this.transform = ele.transform || 0
    this.text = ele.text || ''
    this.textAlign = ele.textAlign || 'left'
    this.iconKey = ele.iconKey || ''
    this.bg = ele.bg || ''
    this.fontSize = ele.fontSize || 18
    this.fontFamily = ele.fontFamily || '微软雅黑'
    this.fontWeight = ele.fontWeight || 'normal'
    this.color = ele.color || '#000000'
    this.zindex = ele.zindex || 1
  }
}

元素实现拖动

监听鼠标移动事件,计算移动的距离并修改对象属性
参考代码:src/components/Element/PicElement.vue

// 这里监听的是editor这个类的鼠标移动,就是最外层容器,防止元素超出画布没反应
document.querySelector('.editor').onmousemove = (event) => {
            var e = event || window.event
            // 锁判断,当释放鼠标的时候,鼠标移动不执行操作
            if (this.flag) {
              let nowX = e.clientX
              let nowY = e.clientY
              let disX = nowX - this.currentX
              let disY = nowY - this.currentY
              this.element.top = parseInt(this.top) + disY
              this.element.left = parseInt(this.left) + disX
            }
          }

元素拖动边角放大缩少

1、给元素加入编辑边框,通过绝对定位,绑定到需要编辑器的元素。
参考代码:src/components/Operate.vue

  <div class="operate">
    <div class="operate-hor-line"></div>
    <div class="operate-ver-line"></div>
    <div class="scale scale-nw" data-direction="nw"></div>
    <div class="scale scale-ne" data-direction="ne"></div>
    <div class="scale scale-sw" data-direction="sw"></div>
    <div class="scale scale-se" data-direction="se"></div>
    <div class="scale scale-n" data-direction="n"></div>
    <div class="scale scale-e" data-direction="e"></div>
    <div class="scale scale-s" data-direction="s"></div>
    <div class="scale scale-w" data-direction="w"></div>
  </div>

2、监听鼠标点击元素和移动的方向,实现元素放大
参考代码:src/components/Element/PicElement.vue

          document.querySelector('.editor').onmousemove = (event) => {
            var e = event || window.event
            if (this.scaleFlag) {
              let nowX = e.clientX
              let nowY = e.clientY
              let disX = nowX - this.currentX
              let disY = nowY - this.currentY
              switch (this.direction) {
                // 左边
                case 'w':
                  this.element.width = parseInt(this.width) - disX
                  this.element.left = parseInt(this.left) + disX
                  break
                // 右边
                case 'e':
                  this.element.width = parseInt(this.width) + disX
                  break
                // 上边
                case 'n':
                  this.element.height = parseInt(this.height) - disY
                  this.element.top = parseInt(this.top) + disY
                  break
                // 下边
                case 's':
                  this.element.height = parseInt(this.height) + disY
                  break
                // 左上
                case 'nw':
                  this.element.width = parseInt(this.width) - disX
                  this.element.left = parseInt(this.left) + disX
                  this.element.height = parseInt(this.height) - disY
                  this.element.top = parseInt(this.top) + disY
                  break
                // 左下
                case 'sw':
                  this.element.width = parseInt(this.width) - disX
                  this.element.left = parseInt(this.left) + disX
                  this.element.height = parseInt(this.height) + disY
                  break
                // 右上
                case 'ne':
                  this.element.height = parseInt(this.height) - disY
                  this.element.top = parseInt(this.top) + disY
                  this.element.width = parseInt(this.width) + disX
                  break
                // 右下
                case 'se':
                  this.element.height = parseInt(this.height) + disY
                  this.element.width = parseInt(this.width) + disX
                  break
              }
            }
          }

单元测试系列 -- 测试小程序

小程序的测试和web应用测试区别不大,可以利用jest进行测试,但是由于jest只提供了nodejs和浏览器执行环境,因此小程序的api我们需要mock,下面讲解小程序测试的一些mock技巧。

mock小程序API

我们测试小程序时,经常会调用微信api,例如wx.showLoading方法,但是因为我们的执行环境未定义该方法,会出现调用错误。

我们可以通过jest提供的global设置全局变量,可以在测试文件中单独编写,或者在package.json的jest块设置setupFiles属性,让jest自动加载。

  "jest": {
    "setupFiles": ["./__tests__/wx.js"]
  },

./tests/wx.js文件内容如下,表示将小程序的api方法定义为mock方法。

global.wx = {
  showLoading: jest.fn(),
  hideLoading: jest.fn(),
  showModal: jest.fn(),
  request: jest.fn(),
  getStorageSync: jest.fn(),
  showShareMenu: jest.fn(),
};

测试小程序页面

// 空白的小程序页面代码
Page({
 onLoad () {
    // your code
 }
})

一个空白的小程序页面,代码会被Page方法包裹,同时Page初始化后,会执行onLoad、onReady等生命周期方法,而且当前对象还能调用setData方法对页面data数据进行修改。

我们需要mock Page方法的实现,代码如下。

export const noop = () => {};
export const isFn = fn => typeof fn === 'function';
let wId = 0;
global.Page = ({ data, ...rest }) => {
  const page = {
    data,
    setData: jest.fn(function (newData, cb) {
      this.data = {
        ...this.data,
        ...newData,
      };

      cb && cb();
    }),
    onLoad: noop,
    onReady: noop,
    onUnLoad: noop,
    __wxWebviewId__: wId++,
    ...rest,
  };
  global.wxPageInstance = page;
  return page;
};

举个例子

假设我们的小程序页面是一个电影列表展示,业务代码如下。

const filmServer = require('../../server/film.js');

Page({
  data: {
    comingFilms: [],
  },
  onLoad() {
    this.getComingFilm();
  },
  // 获取即将上映电影列表
  getComingFilm() {
    return filmServer.getComingSoon(1, 5).then((data) => {
      data.films.forEach((film) => {
        const displayDate = `${new Date(film.premiereAt).getMonth() + 1}月${new Date(film.premiereAt).getDate()}日`;
        film.displayDate = displayDate;
      });
      this.setData({ comingFilms: data.films });
    });
  },
});

我们的编写两个测试用例保证代码的正确运行。

1、保证onLoad时执行getComingFilm方法。

2、保证getComingFilm后日期数据进行格式化。

import '../../pages/film'; // 加载需要测试的页面

 // 获取当前初始化的page对象,后续可用来调用setData等方法,类似小程序页面里的this。
const page = global.wxPageInstance;
// mock网络请求
jest.mock('../../server/film.js');

describe('电影首页', () => {
  describe('onLoad', () => {
    beforeAll(() => {
      // spyOn后可使方法具有mock属性,同时不影响方法调用。
      jest.spyOn(page, 'getComingFilm');
      // 执行页面onLoad生命周期。
      page.onLoad();
    });
    it('should getComingFilm', () => {
      // 断言onLoad后,是否执行了getComingFilm方法。因为我们前面已经将getComingFilm进行spyOn了,所以可以执行toBeCalled判断,否则会出错。
      expect(page.getComingFilm).toBeCalled();
    });
  });
  describe('getComingFilm', () => {
    it('should format premiereAt as MM月DD日 ', () => page.getComingFilm().then(() => {
        // 断言获取数据后,原始数据增加displayDate属性,格式化为MM月DD日
        expect(page.data.comingFilms[0].displayDate).toEqual('9月12日');
      }));
  });
});

🌟由于代码较多,上面只截取了部分,完整代码可以访问github获取

自动化持续集成系列 -- 我真的需要吗?

什么是持续集成

持续集成(英语:Continuous integration,缩写CI)是一种软件工程流程,是将所有软件工程师对于软件的工作副本持续集成到共用主线(mainline)的一种举措。——维基百科

请注意,持续集成只是一种工作流程,它的宗旨是避免集成问题,无论你是手动还是通过工具,只要你开发的软件每天不断的集成,就可以认为是持续集成。

持续集成解决的问题

探究持续集成解决的问题前,我们先想一下,如果不使用持续集成,会有什么问题。

1、集成风险

如果你开发的软件存在多个团队开发不同模块,在上线前夕进行集成往往都存在较大风险,有可能你调用其他模块的接口已经被修改,或者合并代码时存在较大量的冲突在解决冲突时无意引入的bug。
问题越早修改成本越低,因此我们可以使用持续集成每次进行小批量的集成,让问题更早的暴露出来,降低集成与上线的风险。

2、处理异常问题成本

我们通常把系统遗留问题和不规范的代码比作债务,债务会随着时间不断累积,最终导致无法偿还。
因此,问题在产生的初期去解决是最好的时机,通过持续集成保证系统随时可运行,遇到问题随时解决。

3、软件质量

自动化持续集成一般会包含自动化测试,自动测试可以帮助我们提高代码质量,开发者也可以大胆的重构或者添加新功能而不需要担心破坏原有系统。

4、自动构建/部署

我们需要部署web项目到生产环境,一般需要本地进行构建,拷贝到服务器,重启服务器。
这一系列手动的操作,难免会存在一定风险,例如一些复杂的项目,需要修改配置文件,万一构建时忘记修改,会引起线上异常。
如果要避免这些风险,可以利用自动构建,让机器帮我们做这些繁琐的事情。

自动化持续集成方案

目前自动化持续集成方案已经比较成熟了,下面介绍三款较常用到的解决方案。

1、jenkins

jenkins是一个较早出现的基于java编写的开源解决方案。
插件比较齐全,可以通过安装插件关联其他系统,例如gitlab、github,监听提交代码,自动触发构建。

2、gitlab CI

gitlab是一套开源的代码管理软件,较多应用于企业内部自建代码仓库。
gitlab CI是gitlab 8.0后引入的,它需要依赖gitlab runner进行任务构建,gitlab runner可以选择docker作为构建容器,任务的配置是比较方便。同时因为gitlab CI和gitlab本来就是一体的,不需要像jenkins那样配置hook,使用起来相对方案。
具体安装和使用方法可以参考:《自动化持续集成》-- gitlab CI

3、travis CI

travis CI对开源项目提供免费构建服务,如果你的项目是在github开放的开源项目,建议使用travis CI体验整体操作流程,配置和构建的过程都比较简单,也比较容易集成其他工具。
可以参考我之前写的 《自动化持续集成》-- github + travis CI进行配置

最后,如果你决定使用自动持续集成了,可以看看我之前写的方案对比
《自动化持续集成》-- 方案对比

单元测试系列 -- 取个好名字

《代码整洁之道》中说,你的测试代码应该像业务代码一样认真编写,因为测试代码会随着业务代码一块演进,糟糕的测试代码会使你的项目难以维护。

下面聊一下如何给测试用例取一个好名字,当该用例不通过时,可以帮助我们快速定位问题。

准确的断言

假设我们的业务代码是一个网络请求的封装。

function _request(url, data, method = 'GET') {
  return new Promise((resolve, reject) => {
    wx.showLoading({ title: '加载中' });
    wx.request({
      url: url,
      data,
      method,
      success: (res) => {
        wx.hideLoading();
        resolve(res.data);
      },
      fail: (res) => {
        wx.hideLoading();
        reject(res);
      },
    });
  });
}

我们编写测试用例保证以下功能的正确
1、_request方法返回Promise对象。
2、成功时调用wx.hideLoading和reslove方法。
3、失败时调用wx.hideLoading和reject方法。

下面展示不同风格的测试代码,看下哪种更利于我们阅读。

it('_request return', () => {
  // 省略测试代码
})
it('request success', () => {
  // 省略测试代码
})
it('request fail', () => {
  // 省略测试代码
})

😔上面的写法你可以知道失败的位置,但是不知道失败的原因

为了同时满足错误包含位置和原因两个信息,我们换一种写法。

it('should return a Promise when request', () => {
  // 省略测试代码
})
it('should hideLoading when request success', () => {
  // 省略测试代码
})
it('should reslove when request success', () => {
  // 省略测试代码
})
it('should hideLoading when request fail', () => {
  // 省略测试代码
})
it('should reject when request fail', () => {
  // 省略测试代码
})

😄上面代码,我们把_request return 修改为 should return a Promise when request,用例不通过时,就算不查看业务代码,也可以知道是因为_request方法没有返回Promise对象导致异常。

另外,request success修改为两个单元测试,should hideLoading when request success和should reslove when request success,因为成功回调做了两件关联性不高的事情,而且hideLoading对于应用较为重要,万一移除会导致页面loading无法消失,因此这里对他进行单独测试覆盖。

无论使用哪种风格断言说明,建议包含位置原因两个信息,能帮助你快速定位问题的断言就是好的断言。

模块化测试代码

我们发现改造后的代码比较啰嗦,例如下面出现了两次when request success

it('should hideLoading when request success', () => {
  // 省略测试代码
})
it('should reslove when request success', () => {
  // 省略测试代码
})

我们可以进行模块化

describe('request success', () => {
  it('should hideLoading', () => {
    // 省略测试代码
  })
  it('should reslove', () => {
    // 省略测试代码
  })
})

使用中文还是英文

我们对比下面两种语言断言

describe('[excute]', () => {
   it('should [unit test]', () => {})
   it('should [unit test]', () => {})
})
it('将调用 hideLoading 当请求成功', () => {
  // 省略测试代码
})
it('将调用 reslove 当请求成功', () => {
  // 省略测试代码
})

我个人感觉经过中文转换的断言,反而不能直观的表达该单元测试的意图。
同时在编写的时来回切换输入法也是挺麻烦的,所以我个人还是喜欢使用英文断言。

小程序从手动埋点到自动埋点

前言

小程序由于封闭性较强,要像web应用一样实现灵活的数据收集,会有一定难度。目前开源的埋点SDK,一般采用手动埋点的方式,这种方式有较强的侵入型,为了解决这个问题就有了该文章。

手动埋点

以腾讯移动分析的SDK为例,如果要记录埋点信息,只要插入一句代码即可

// 例如,记录搜索行为
search(keyword) {
   if (keyword) {
       ...业务代码
   }
   mta.Event.stat("ico_search", {"query":keyword});
}

示例代码看起来是比较简洁的,但是埋点需要收集的数据往往不是单一的,复杂的埋点代码插入业务代码,会影响代码的阅读体验,而且埋点代码散落在各个地方,不方便管理

由于手动埋点必须插入到函数中,有时候我们为了获取页面某一元素点击信息,产生了一种叫无业务相关埋点,简单来说就是你的函数定义,就只有埋点代码,当这种埋点频繁出现,代码会被严重污染

// wxml
<view bindtap="track">这只是一个展示view</view>

//js 
track() {
  mta.Event.stat("eleClick", {"name":xxxxx});
}

另外,由于PM会频繁调整埋点信息,而埋点是一个繁琐又无聊的工作,基于Don't Repeat Yourself 原则,手动埋带要不得。

总结以上,手动埋点有下列问题

  1. 影响代码的阅读体验
  2. 埋点代码散落在各个地方,不方便管理
  3. 代码会被污染
  4. 埋点是一个繁琐又无聊的工作

自动埋点

实现思路:监听用户点击-->读取埋点配置JOSN,判断是否需要上报--> 上报数据

1、小程序监听用户点击行为

web应用监听用户点击行为是比较容易,但是小程序没有提供Dom的事件监听,不过我们可以通过事件冒泡的方式捕获。

// web监听页面点击
document.addEventListener('click',(e) => {console.log(e)})

// 小程序监听页面点击,用户的点击行为都会执行elementTracker方法
<view catchtap='elementTracker'>
  <view class='buy-now'>
     <button bindtap='buy' animation="{{scaleAnim}}">立即购票</button>
  </view>
</view>

2、判断点击位置是否落在监听元素中

假设需要监听用户是否点击class为buy-now元素,可以通过获取buy-now元素长宽,定位和点击位置坐标判断是否出现重叠,以判断是否被点击。

/**
 * 判断点击是否落在目标元素
 * @param {Object} clickInfo 用户点击坐标
 * @param {Object} boundingClientRect 目标元素信息
 * @param {Object} scrollOffset 页面位置信息
 * @returns {Boolean}
 */
export const isClickTrackArea = function (clickInfo, boundingClientRect, scrollOffset) {
    if (!boundingClientRect) return false;
    const { x, y } = clickInfo.detail; // 点击的x y坐标
    const { left, right, top, height } = boundingClientRect;
    const { scrollTop } = scrollOffset;
    if (left < x && x < right && scrollTop + top < y && y < scrollTop + top + height) {
        return true;
    }
    return false;
};

3、通过配置表声明埋点

为了解决代码入侵问题,可以将所有埋点信息统一管理,通过配置表的方式,除了方便管理,以后还可以做到动态配置,在服务端配置完毕下发到客户端。

const tracks = {
  path: 'pages/film/detail',
  elementTracks: [
    {
      element: '.buy-now',  // 声明需要监听的元素
      dataKeys: ['film.filmId'], // 声明需要获取Data下的哪些数据
    },
  ]
};

4、对页面函数埋点

有些场景我们除了对页面元素点击埋点,还要对页面函数进行埋点,例如用户下拉刷新的时候,可以对原方法进行包装,插入埋点代码。

 rewritePage() {
    const originPage = Page;
    Page = (page) => {
      Object.keys(page).forEach((methodName) => {
       // 执行埋点逻辑
        typeof page[methodName] === 'function' && this.recordPageFn(page, methodName);
      });
      // 执行原Page对象
      return originPage(page);
    };
  }

最后

当触发记录的时候,将会看到以下数据
image
完整的代码已经封装成SDK了,可以快速集成到项目
https://github.com/zhengguorong/xbosstrack-wechat

代码质量之破窗效应

破窗效应是犯罪心理学的一个理论,指如果一个建筑,当出现小量破窗的时候,会诱发更多的人为破坏。如果一个建筑出现破窗的时候及时修复,会受到更少破坏。

我们是否有这样的经历,当接手一个代码质量较差的项目,例如一个函数有上百行的代码,函数里有大量的if else,如果让你增加一个功能,你更倾向于直接在目标函数上加入你的改动代码,而不是通读该方法,再进行封装修改呢。

其实这样的修改方式,并没有错,也和个人能力没有关系,因为这种修改方式是最保险,最快捷的,他不但维持代码原有功能正常运行,还添加了新的功能。

但是,这样的项目,就是典型的破窗效应,因为第一个人产生了破窗,没有及时修复,后面来的人,就会更大胆的破坏,最终项目没法维护。

那如何避免项目形成破窗效应呢?

1、制定代码规范:尽量使用自动化工具实时检测,不要试图通过文档规范,文档一般都不会有人看的,前端集成eslint就最好了,vs code可以安装相应插件实时提醒。另外,可以加入git hook,对不符合规范对代码,拒绝进入代码仓库。

2、code review: 静态代码检测,只能检查语法问题,但是对于代码设计问题,需要code review进行纠正,code review可以利用gitlab 或者 github的 pull request模式,对需要合并到主分支的代码进行review。

3、控制圈复杂度:代码好坏的一个重要标准是,函数的圈复杂度,如果一个函数有大量分支,会让人很难理解,所以控制圈复杂度可以有效提高代码质量,vs code可以利用CodeMetrics这款插件实时检查。

4、控制代码重复率:《编写有效的单元测试》书中提到,重复的代码是造成代码bug提高很重要的因素,我们可以利用SonarQube对代码重复率进行扫描,删除重复代码或者对相近的代码进行封装。

5、自动化单元测试:很多人可能会觉得单元测试成本太高,为什么会产生这样的感觉呢,我觉得主要是大家把开发成本理解为开发功能所需要的时间,但是实际上,项目的成本,包含开发成本,维护成本,bug处理成本,代码二次开发成本。开发成本也许只占项目成本的10%,后面功能越是复杂,维护成本将会出现指数增长,前期不去控制复杂度,会给后期带来巨大的成本开销。
自动测试就像房子的管家,哪个坏小子想要搞破坏,立刻就会收到警告。

下面为引用《单元测试的艺术》中的一个段落,两个团队分别使用和不使用单元测试,各项指标的差异。
image

那如果已经是一个破烂不堪的项目,又如何拯救它呢?

1、eslint自动修复:还是上面说的代码规范,我们需要利用eslint进行老代码清理,使用eslint --fix命令,可以实行自动格式化。

2、质量仪表盘:如果你在一个团队工作,必须让大家形成共同目标,并能实时感知项目的状态,否则,你清理代码的速度远比不上创造坏代码的速度。SonarQube是你的好帮手,你可以利用他的仪表盘功能,每天查看项目是否有新增坏代码。

3、重构你的核心模块:如果你要经常修改的模块又是核心模块,建议你对其进行重构,重构时,利用单元测试进行覆盖,保障代码质量,同时,团队成本要进行review,防止把代码修改为你喜好的代码,而非大家能理解的代码。

4、使用率低的模块:对于使用率或者修改率较低的代码,就让他随风去把,时间会带走他的。

低效团队的几个特征

流程不规范

不知从什么时候,流程规范变成了一个贬义词,流程规范=死板和低效。

频繁的“沟通”

无意义的会议

缺少自动化

整洁代码系列 -- 会讲故事的代码才是好代码

写这篇文章是因为最近在看业务代码,发现存在大量意大利面代码,一个函数上百行代码一个js有一千多行代码,理解的过程会让人非常痛苦。
我们就这个问题,讨论下如何编写让人能快速理解的代码。

从小明的一天开始

我们以小明的一天为故事编写两份代码,对比两个阅读体验。

class Person {
    constructor(name) {
        this.name = name;
        this.walkSetp(10)
        this.turnLeft()
        this.pickUpToothpaste()
        this.pickUpToothbrush()
        this.walkSetp(20);
        this.trunRight(10);
        this.turnLeft(10);
        this.sit();
        this.pickUpChopsticks();
        this.pinchUpFood();
        this.putInMouth();
    }
}
const xiaoMing = new Person("小明");

上面这段代码我猜大家阅读的时候是很吃力的,因为他把大量细节暴露出来,你无法理解小明为什么往前走了10步然后左转了,你看到后面执行了pickUpToothpaste,猜测是去刷牙了吧。这段代码就是因为缺少封装,把细节暴露在外面,导致理解成本增加。

我们做个改造。

class Person {
    constructor(name) {
        this.name = name;
        this.brushTeeth();
        this.eatBreakfast()
    }
    // 刷牙
    brushTeeth() {
        this.walkSetp(10)
        this.turnLeft()
        this.pickUpToothpaste() // 拿起牙膏
        this.pickUpToothbrush() // 拿起牙刷
    }
    // 吃早餐
    eatBreakfast() {
        this.walkSetp(20);
        this.trunRight(10);
        this.turnLeft(10);
        this.sit();
        this.pickUpChopsticks();
        this.pinchUpFood();
        this.putInMouth();
    }
}
const xiaoMing = new Person("小明");

现在好多了吧,你只要阅读构造函数里调用的两个方法,分别是brushTeeth和eatBreakfast,就知道小明是去了刷牙和吃早餐,具体怎么刷牙,你根本不需要关注。

如果这时候有个需求,我希望小明吃早餐前去洗手。

例一中,我们最快的方式是直接插入代码,但是随着时间的增加,代码会不断膨胀,同时因为人员流动,这段代码会无人理解,最终导致项目bug率和维护成本激增。

...省略代码
this.pickUpToothbrush()
this.washHands()
this.walkSetp(20);
...省略代码

例二中,我们不需要阅读brushTeeth方法,直接找到eatBreakfast方法修改即可。
虽然例二代码看起来没什么高级的编程技巧,也没用设计模式,但是就因为做了简单封装,把细节封装到一个方法里面,使方法具有语意化,我们在修改代码的时候能快速找到需要修改的位置,也能把影响范围降低降低出错率。

提高代码封装性手段

1、code review
2、检查函数圈复杂度
3、写单元测试

创新的核心法则

以下内容参考《让大象飞》总结

技术不一定是创新最重要的部分

很多创业者也许会有个误区,认为创新就是技术创新,在我们通常的认知里,大到航天飞机,小到我们日常生活中使用的种种物品,都和技术的变革息息相关。霍夫曼认为,技术仅仅是创新的一部分,在创新的过程中,学习和模仿是创新的必经环节。

事实上历史上大多数的发明创新,在成为具有活力的商品前,通常会被埋没数十年,从电灯到内燃机再到电子计算机都是这样的。比如:我们通常认为电灯泡是爱迪生发明的,但是实际上,一位叫亨利戈培尔早数十年前就发明了使用相同原理跟物料的灯泡,爱迪生只是在他的基础上做了一些改进。他最先发现了电灯泡的商业价值,并收购其专利,改良灯丝,并在其实验室经过长时间实验,最终推上市场。从这个案例看,虽然电灯泡不是爱迪生发明的,但是恰恰因为他将技术与商业连结在一起,最终使得电灯泡得到普及应用,而只关注技术的最初的发明者,却慢慢被人们遗忘了。

独创性只是明智的模仿 —— 伏尔泰

他的意思是当人们有能力模仿一个东西,并肯定他能成功的时候,人们是不会主动承担创新的风险的。很多我们熟知的品牌,其成功并不是技术,而是技术的应用模式。比如:苹果手机,里面的零部件大多数来自第三方,但是这并不影响其价值,他的价值体现在对用户需求的理解和生态系统的设计,这些都不是用单纯的技术创新解释的。

两个披萨原则

初创型企业,团队的人数尤为关键,一般都以小团队为宜,因为团队越小,成员之间的合作就越顺畅,也能以大团队不能完成的协作方式完成工作,相反,当团队变大后,成员间的合作就会谨小慎微,导致行动缓慢,当面对创新时,就不能迅速反应。贝索斯把披萨的数量当作衡量团队大小的标准,如果两个披萨不足以喂饱一个项目团队,那么这个团队可能就显得太大了。

团队怎么分工?

1、全面理解业务、客户和市场的人,是一位出色的团队管理则,并能制定愿景,把愿景推销出去的人,就是我们常说的CEO(首席执行官)。

2、对技术具有极度的热爱或者是痴迷的人,能够将想法实现为产品,并能亲自卷起袖子干,比如,写代码、做测试,这个人就是CTO(首席技术官)。

3、创意设计负责人:对于一个好的创新项目,设计往往是核心,对于一个细微改变就会产生巨大影响,比如苹果公司。设计思路的重要性绝对不能低估。

4、特定领域的专家:研究型人员,当需要关键领域技术突破的时候,能发挥纠正方向的作用。

了解用户的真正需求

真正好的产品设计,要站在用户的角度思考问题,重要的是个人体验,而不是功能。要用更加理性、辩证的思维去看待产品。

例如:苹果知道在硬件上的竞争,永远也不会获胜,所以把思路的重点放在用户的真正需求上,他创造了一个完整的生态系统,是用户在购买音乐和播放音乐成为愉悦的体验。同时,关注用户使用的细节。比如,我们经常购买一款新产品的时候,打开包装盒以后,由于需要充电,无法立即使用。苹果就在出厂前,把产品充满电,用户开箱后即可使用,苹果不但满足了用户需求,还把体验做到了极致。

打造核心竞争力

核心竞争力不是某一件产品,可以是生产流程、专有知识产权或者分销关系网。如果能做到让某一关键环节,对竞争对手来说几乎是无法复制的,或者即使可以复制但要耗费很大精力成本,就可能成为核心竞争力。

单元测试系列 -- jest常用mock

进行单元测试时,我们希望得到的数据是稳定可控的,例如应用涉及调用网络接口数据,可以通过mock的方法控制数据的返回。

下面为获取电影列表数据的业务代码
页面在onLoad时执行getComingFilm方法,getComingFilm方法获取网络数据。

const filmServer = require('../../server/film.js');

Page({
  data: {
    comingFilms: [],
  },
  onLoad() {
    this.getComingFilm();
  },
  // 获取即将上映电影列表
  getComingFilm() {
    return filmServer.getComingSoon(1, 5).then((data) => {
      data.films.forEach((film) => {
        const displayDate = `${new Date(film.premiereAt).getMonth() + 1}月${new Date(film.premiereAt).getDate()}日`;
        film.displayDate = displayDate;
      });
      this.setData({ comingFilms: data.films });
    });
  },
});

我们需要编写两个单元测试保障代码。
1、保障onLoad时执行getComingFilm方法。
2、保障getComingFilm后日期数据进行格式化。

使用spyOn保留方法原定义逻辑和增加mock方法

下面的单元测试用来保障onLoad时调用getComingFilm方法,但是你发现jest会提示getComingFilm为非mock对象,不能调用toBeCalled函数。

it('should getComingFilm', () => {
    page.onLoad();
    expect(page.getComingFilm).toBeCalled();
});

那我们把getComingFilm方法转换成mock对象

it('should getComingFilm', () => {
    page.getComingFilm = jest.fn();
    page.onLoad();
    expect(page.getComingFilm).toBeCalled();
});

以上写法虽然能调用toBeCalled方法了,但原来的getComingFilm方法被覆盖,如果后续要用到该方法,将无法执行正确逻辑。
可以使用spyOn方法,既保留方法原先定义,又让方法具备mock方法。

it('should getComingFilm', () => {
    jest.spyOn(page, 'getComingFilm');
    page.onLoad();
    expect(page.getComingFilm).toBeCalled();
});

mock网络请求

const filmServer = require('../../server/film.js');

getComingFilm() {
  return filmServer.getComingSoon(1, 5).then((data) => {

  });
});

上面的业务代码,通过filmServer进行网络请求,获取相应业务数据。
我们希望测试代码中使用mock数据,可以在server目录下创建__mocks__文件夹并创建film.js文件。

server/mocks/film.js 文件代码如下

const filmService = jest.mock('../film.js');

const comingSoonRes = { films: [{ id: 1, name: 'test', premiereAt: Date.now() }], page: { total: 10, current: 1 } };
filmService.getComingSoon = jest.fn(() => Promise.resolve(comingSoonRes));

module.exports = {
  getComingSoon: filmService.getComingSoon,
  comingSoonRes,
};

在你的测试文件加入mock声明,原网络请求接口就会被替换。

// 使用mock文件
jest.mock('../../server/film.js');
// getComingFilm将返回server/__mocks__/film.js中定义的数据
it('should format premiereAt as MM月DD日 ', () => page.getComingFilm().then(() => {
    expect(page.data.comingFilms[0].displayDate).toEqual('9月12日');
}));

注意:premiereAt字段使用了Date.now() 创建时间戳,但是因为该方法会随着执行时间变化,需要控制该方法的返回值

通过global设置方法的固定返回。

global.Date.now = jest.fn(() => 1536708613825);

🌟由于代码较多,上面只截取了部分,完整代码可以访问github获取

单元测试系列 -- jest快速入门

jest简介

jest为facebook推出的开源单元测试框架,提供了完善的工具链,包含mock,断言,覆盖率报告。

如果你以前有接触过单元测试,应该也有听过karam、chai、mocha等等框架,以前我们通常组合多个框架来实现单元测试功能,因为繁杂的集成和配置,让上手单元测试变得困难。
现在我们可以利用jest快速配置单元测试环境,下面介绍jest如何配置和编写一个简单的单元测试。

step 1、安装依赖

安装jest

yarn add --dev jest

step2、配置babel(如果不需要支持es6语法,可以忽略)

添加babel依赖,并执行yarn install 安装依赖

  "devDependencies": {
    "babel-core": "^6.26.3",
    "babel-jest": "^23.4.2",
    "babel-preset-es2015": "^6.24.1"
  }

添加.babelrc文件到项目根目录,配置如下

{
  "presets": ["es2015"]
}

step3、编写第一个测试

添加sum.js到项目

function sum(a, b) {
  return a + b;
}
module.exports = sum;

添加sum.test.js,加入测试代码

const sum = require('./sum');

test('adds 1 + 2 to equal 3', () => {
  expect(sum(1, 2)).toBe(3);
});

进入根目录,执行jest命令,运行单元测试

jest

rollup和webpack使用场景分析

简介

Rollup官方解析: Rollup 是一个 JavaScript 模块打包器 ,可以将小块代码编译成大块复杂的代码,例如 library 或应用程序

webpack官方解析: webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler)。当 webpack 处理应用程序时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 bundle。

应用场景对比

使用Rollup的开源项目:

  • vue
  • vuex
  • vue-router

使用webpack的项目:

  • 饿了么UI
  • mint-ui
  • vue脚手架项目

从上面使用场景可以大概分析出,Rollup偏向应用于js库,webpack偏向应用于前端工程,UI库;如果你的应用场景中只是js代码,希望做ES转换,模块解析,可以使用Rollup。如果你的场景中涉及到css、html,涉及到复杂的代码拆分合并,建议使用webpack。

Rollup入门

实现模块解析

// src/main.js
import foo from './foo.js';
export default function () {
  console.log(foo);
}
// src/foo.js
export default 'hello world!';
// 执行命令 -o表示输出bundle.js文件 -f cjs表示使用commonjs标准输出
rollup src/main.js -o bundle.js -f cjs
// 输出bundle.js内容
'use strict';

var foo = 'hello world!';

var main = function () {
  console.log(foo);
};

module.exports = main;

使用ab命令压力测试

由于最近在开发前端日志监控系统,对接口负载有较高的要求。
我们需要模拟高并发的环境下,接口承受的最大负载。

后端接口使用nodejs部署,服务器为1核2G腾讯云。
如果是ubuntu系统,可以用下面命令安装

sudo apt install apache2-utils
// 下面为输入ab后的帮助命令
Usage: ab [options] [http[s]://]hostname[:port]/path
Options are:
    -n requests     Number of requests to perform
    -c concurrency  Number of multiple requests to make at a time
    -t timelimit    Seconds to max. to spend on benchmarking
                    This implies -n 50000

-n 表示请求的次数,-c 表示并发数,-t 表示持续的时间

模拟500个客户端,进行20000次的请求。

ab -c 500 -c 20000 'http://127.0.0.1'

下面为压测的结果

Concurrency Level:      500 // 发送的并发数
Time taken for tests:   3.301 seconds
Complete requests:      10000 // 总的请求数
Failed requests:        0
Total transferred:      980000 bytes
HTML transferred:       0 bytes
Requests per second:    3029.78 [#/sec] (mean)  // 每秒处理的请求数
Time per request:       165.028 [ms] (mean) //  接口平均用时
Time per request:       0.330 [ms] (mean, across all concurrent requests)
Transfer rate:          289.96 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0   28 162.8      0    1002
Processing:    41   76  30.3     74     880
Waiting:       41   76  30.3     74     880
Total:         41  104 176.4     75    1279

Percentage of the requests served within a certain time (ms)
  50%     75
  66%     77
  75%     79
  80%     81
  90%     84
  95%     87
  98%   1071
  99%   1241
 100%   1279 (longest request)

结果显示,500个请求没什么压力,那我们换成3000个并发呢
如果执行请求提示

socket: Too many open files (24)

表示连接数被限制了,可以用下面命令修改

ulimit -n 65535

以下为3000并发时的测试结果

Concurrency Level:      3000
Time taken for tests:   27.179 seconds
Complete requests:      30000
Failed requests:        0
Total transferred:      2940000 bytes
HTML transferred:       0 bytes
Requests per second:    1103.80 [#/sec] (mean)
Time per request:       2717.874 [ms] (mean) // 处理时间变长
Time per request:       0.906 [ms] (mean, across all concurrent requests)
Transfer rate:          105.64 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0  400 1872.4      0   15037
Processing:    23  209 519.4    132   26165
Waiting:       23  209 519.4    132   26165
Total:         45  609 2043.7    141   27168

Percentage of the requests served within a certain time (ms)
  50%    141
  66%    194
  75%    216
  80%    246
  90%    495
  95%   3095
  98%   7318
  99%  15102
 100%  27168 (longest request)

可以看到处理能力出现了明显下降。

为了了解服务器瓶颈,需要看下云服务器的资源使用情况
cpu是满载了,还是内存用完了,还是硬盘io有问题,或者是程序没写好,出错了。进行逐一排查,榨干性能。

image.png
上图可以看出cpu出现了100%,导致处理能力下降。

由于我用的是单核处理器,所以我只开了一个进程,如果你是用多核处理器,可以用pm2开多个进程提高cpu的利用率。

vue项目在nginx配置缓存策略

nginx开启缓存机制:

在/etc/nginx/sites-available/default文件加入如下代码
表示 对图片、js和css资源实现缓存,过期时间为60秒

        location ~* \.(?:jpg|jpeg|png|gif|ico|css|js)$ {
            root /data;
            expires 60s;
            add_header Cache-Control "public";
        }

下图表示成功开启
image.png

缓存过期后都会往服务器获取资源吗?

如果缓存未过期,刷新页面时,资源会显示200,size为from disk/from memory,表示在本地缓存获取资源。

image.png

缓存过期后,浏览器会去询问服务端,检查资源是否被更新,如果没更新,服务端会返回304,资源依然在读取本地

image.png

这和cache-controller为no-cache一个意思,并不是说资源都由服务端返回,而是浏览器每次都先去询问服务端,是否有新的资源。

真的有必要开启缓存吗?

在模拟10K网络条件下,加载时间相差5秒,这五秒就是发送http请求的代价,因此建议合理都设置缓存策略。

修改资源真的被更新到了吗?

我们对app.js进行修改,刷新浏览器,如下图,会返回200,并加载了108K的新资源。其他没有改动的资源,返回304

image.png

如果在缓存有效期内,我修改app.js,会返回304,因为缓存期内,浏览器不去询问服务端。

image.png

缓存期内怎么强制更新js/css资源???

第一步:首先加入test.js脚本, 内容为alert(1)
image.png

第二步:缓存有效期内修改test.js内容为alert(2),弹框内容依然为数字1
解决方案:修改test.js文件名为test.v1.js并在index.html重新引入(因为我们的html文件没有缓存,所以可以通过更新html的引用实现资源替换)

部门化的优缺点

部门化指将组织划分至不同单位。传动将组织部门化的方法依职能(前端开发,后端开发,大数据,UI设计等)划分。将工作者根据他们的技能、专业技术或是资源使用的方式分组,使员工能够专业化、更有效率地一同工作,也可以节省成本。

优点:

1.员工可以深入发展专精技能,同时带动部门的效率与进步。

2.企业可以将所有资源集中,并在该领域中寻找各种专家,而达到规模经济。

3.提高职能内部的协调度,高层管理容易引导并控制各部门活动。

缺点:

1.不同部门间可能缺乏沟通。例如:运维部门和开发部门分离,开发部门不能快速相应用户的需求。

2.个别员工可能产生部门目标认同,而非组织整体目的。采购部门可能会采购高价值和大量的物品,但其储存成本因此增加。如此使得采购部门看似绩效良好,但会损及公司但整体获利能力。

3.公司对于外部变革的相应,可能变得缓慢。

4.由于并未接受不同管理职责的训练,人们因此容易成为眼光短小的专家。

B公司采用的就是部门化组织,例如信息化部门以职划分不同组织,UI设计,前端开发,后端开发,大数据等。

当有个小需求的时候,需要层层沟通,沟通的时间基本就耗掉一个星期了,再加上开发时间,一次小版本迭代就要半个月以上。

在跨部门合作时,也会因为专业的差异,对需求的理解产生差异,由于未能及时沟通,在上线测试时会有较大的偏差。

使用casperjs爬取淘宝商品图片

最近因为需要实现衣物识别,需要大量不同衣物的图片。
爬取淘宝/京东的产品图片应该是最快捷的收集方法。

第一次尝试

开始的时候,打算通过分析淘宝链接规则来提取页面信息,但是淘宝的URL比较复杂,计算按页面的URL请求,返回的也浏览器访问的dom结构不一样,加入请求头也没用,所以选择放弃。

换一个方法

capser是基于phatomjs封装的更高级的库,平时可以用来做自动化测试等等。
反正就是模拟一个正式的浏览器访问环境,获取dom结构,分析URL,提取图片啰。

github地址:https://github.com/zhengguorong/productPic

还是直接看代码吧,详细解释看注释

// 抓取图片的宽高
var imageSize = '_200x200'
// 抓取的分类
var queryKey = ['棕色 衬衫', '棕色 t恤', '棕色 外套', '棕色 卫衣', '棕色 裤子']

// 抓取返回页面dom结构里的图片
function getImages() {
    var images = document.querySelectorAll('#mainsrp-itemlist img');
    return Array.prototype.map.call(images, function (e) {
        // 因为淘宝用了懒加载的形式,所以图片路径放在了data-src下
        return e.getAttribute('data-src');
    });
}

// 过滤重复的图片
Array.prototype.unique = function () {
    var res = [];
    var json = {};
    for (var i = 0; i < this.length; i++) {
        if (!json[this[i]]) {
            res.push(this[i]);
            json[this[i]] = 1;
        }
    }
    return res;
}

// 爬多个关键字,直接创建多个casper实例
queryKey.forEach(function (keyword) {
    console.log(keyword)
    var casper = require('casper').create({
        pageSettings: {
            loadImages: false
        }
    })
    new process(casper, keyword)
})

// 这是爬取关键代码,第一步,在搜索框输入关键字,进行搜索
function process(casper, keyword) {
    var images = []
    casper.start('https://www.taobao.com/', function () {
        this.waitForSelector('form[action="//s.taobao.com/search"]');
    });

    casper.then(function () {
        this.fill('form[action="//s.taobao.com/search"]', { q: keyword }, true);
    });
// 等两秒钟,让页面跳转
    casper.wait(2000)

    // 收集图片地址
    for (var j = 0; j < 4; j++) {
        casper.then(function () {
            this.waitForSelector('#mainsrp-pager', function () {
                this.echo('正在爬取---' + keyword)
                if (j > 0) this.clickLabel('下一页', 'span')
                this.wait(2000, function () {
                    this.scrollToBottom()
                    images = images.concat(this.evaluate(getImages));
                })
            })
        });
    }

    // 下载图片
    casper.then(function () {
        console.log(images.length)
        images = images.unique()
        console.log(images.length)
        for (var i = 0; i < images.length; i++) {
            if (images[i]) {
                this.echo('正在下载 ' + keyword + i)
                this.download('http:' + images[i] + imageSize + '.jpg', 'data/' + keyword + '/' + i + '.jpg');
            }
        }
    });

    casper.run(function () {
        this.echo('下载完成').exit()
    });
}


[转载]使用 RAIL 模型评估页面性能

RAIL 是一种以用户为中心的性能模型。每个网络应用均具有与其生命周期有关的四个不同方面,且这些方面以不同的方式影响着性能:

image.png

TL;DR

  • 以用户为中心;最终目标不是让您的网站在任何特定设备上都能运行很快,而是使用户满意。
  • 立即响应用户;在 100 毫秒以内确认用户输入。
  • 设置动画或滚动时,在 10 毫秒以内生成帧。
  • 最大程度增加主线程的空闲时间。
  • 持续吸引用户;在 1000 毫秒以内呈现交互内容。

以用户为中心

让用户成为您的性能工作的中心。用户花在网站上的大多数时间不是等待加载,而是在使用时等待响应。了解用户如何评价性能延迟:

屏幕快照 2018-01-17 15.01.35.png

响应:在 100 毫秒以内响应

在用户注意到滞后之前您有 100 毫秒的时间可以响应用户输入。这适用于大多数输入,不管他们是在点击按钮、切换表单控件还是启动动画。但不适用于触摸拖动或滚动。

如果您未响应,操作与反应之间的连接就会中断。用户会注意到。

尽管很明显应立即响应用户的操作,但这并不总是正确的做法。使用此 100 毫秒窗口执行其他开销大的工作,但需要谨慎,以免妨碍用户。如果可能,请在后台执行工作。

对于需要超过 500 毫秒才能完成的操作,请始终提供反馈。

动画:在 10 毫秒内生成一帧

动画不只是奇特的 UI 效果。例如,滚动和触摸拖动就是动画类型。

如果动画帧率发生变化,您的用户确实会注意到。您的目标就是每秒生成 60 帧,每一帧必须完成以下所有步骤:

image.png

从纯粹的数学角度而言,每帧的预算约为 16 毫秒(1000 毫秒 / 60 帧 = 16.66 毫秒/帧)。 但因为浏览器需要花费时间将新帧绘制到屏幕上,只有 10 毫秒来执行代码

在像动画一样的高压点中,关键是不论能不能做,什么都不要做,做最少的工作。 如果可能,请利用 100 毫秒响应预先计算开销大的工作,这样您就可以尽可能增加实现 60fps 的可能性。

如需了解详细信息,请参阅渲染性能

空闲:最大程度增加空闲时间

利用空闲时间完成推迟的工作。例如,尽可能减少预加载数据,以便您的应用快速加载,并利用空闲时间加载剩余数据。

推迟的工作应分成每个耗时约 50 毫秒的多个块。如果用户开始交互,优先级最高的事项是响应用户。

要实现小于 100 毫秒的响应,应用必须在每 50 毫秒内将控制返回给主线程,这样应用就可以执行其像素管道、对用户输入作出反应,等等。

以 50 毫秒块工作既可以完成任务,又能确保即时的响应。

加载:在 1000 毫秒以内呈现内容

在 1 秒钟内加载您的网站。否则,用户的注意力会分散,他们处理任务的感觉会中断。

侧重于优化关键渲染路径以取消阻止渲染。

您无需在 1 秒内加载所有内容以产生完整加载的感觉。启用渐进式渲染和在后台执行一些工作。将非必需的加载推迟到空闲时间段(请参阅此网站性能优化 Udacity 课程,了解更多信息)。

关键 RAIL 指标汇总

要根据 RAIL 指标评估您的网站,请使用 Chrome DevTools Timeline 工具记录用户操作。然后根据这些关键 RAIL 指标检查 Timeline 中的记录时间。

屏幕快照 2018-01-17 15.03.30.png

IT团队如何做绩效

脑力工作者的产出评估是困难的,他不像体力工作者,可以通过工时或者生产数量统计。

在B公司,每周进行周汇报,每月进行月度绩效考核,但是实际效果非常差,因为程序员的绩效是不能通过具体的项来衡量,也不能通过开发了多少个需求,增加了多少行代码来衡量;

程序员的绩效是多样的,可以是解决技术的问题,优化流程管理,给团队产生良好的沟通氛围,优化架构提升开发效率,也有可能因为个人对开源社区的贡献,在招聘时有超强的吸纳人才的属性。

那为什么很多公司还这样做绩效呢?也许是因为公司从传统企业转型,不懂IT团队的管理,也可能是因为管理层通过表格、评分寻找内心的安稳吧。

通过这种绩效评估,其实对个人或团队没有任何效率提升,反而需要浪费时间填写无意义的表格,而且每月的绩效评估其实对最终薪酬的影响又是非常小的。这样的绩效就像一个框框,既不能过分打击稍后进的,也不能对先进的奖励得太过分,使团队趋向平庸。

那么如何在一家做传统绩效的公司里,让你的团队保持士气呢?

1、表格继续填,满足集团的需要。

2、责任到人:把项目分配到具体人员,强调个人职责,设定季度验收标准。例如季度目标是性能提升;指标是首页秒开: 好处是项目负责人发挥空间更大,目标感和成就感也会比较强。

3、加强奖励优秀人才:对于做得优秀的人员,应该进行更大力度的奖励,同时,需要明确指出哪里做得好,让其他人员有实际的感知,共同进步,同时也体现公平。

其实说了这么多,就是现代管理学之父德鲁克先生的目标管理理念,他是这么概括目标管理的:

目标管理是以目标为导向,以人为中心,以成果为标准,而使组织和个人取得最佳业绩的现代管理方法

请问怎么获取函数的e

博主你好,正在用您的小程序的自动埋点做参考,请问如果想获取函数的e的内容应该怎么办呀,求指教emm

小程序redux性能优化,提升三倍渲染速度

前言

最近用户反馈我们的小程序很卡,打开商品列表需要四五秒时间,带着这个疑问,我决定对小程序做个全面的性能优化,要做性能优化,必须先理清以下三个关键点。

  1. 产生性能问题的关键点
  2. 度量性能指标
  3. 寻找解决方案

在阅读案例分析前,建议能先了解小程序的工作原理和性能关键点。

工作原理 (官方说明)

小程序的视图层目前使用 WebView 作为渲染载体,而逻辑层是由独立的 JavascriptCore 作为运行环境。在架构上,WebView 和 JavascriptCore 都是独立的模块,并不具备数据直接共享的通道。当前,视图层和逻辑层的数据传输,实际上通过两边提供的 evaluateJavascript 所实现。即用户传输的数据,需要将其转换为字符串形式传递,同时把转换后的数据内容拼接成一份 JS 脚本,再通过执行 JS 脚本的形式传递到两边独立环境。

evaluateJavascript 的执行会受很多方面的影响,数据到达视图层并不是实时的。

性能关键点(官方说明)

1. 频繁的去 setData

在我们分析过的一些案例里,部分小程序会非常频繁(毫秒级)的去setData,其导致了两个后果:

  • Android 下用户在滑动时会感觉到卡顿,操作反馈延迟严重,因为 JS 线程一直在编译执行渲染,未能及时将用户操作事件传递到逻辑层,逻辑层亦无法及时将操作处理结果及时传递到视图层;
  • 渲染有出现延时,由于 WebView 的 JS 线程一直处于忙碌状态,逻辑层到页面层的通信耗时上升,视图层收到的数据消息时距离发出时间已经过去了几百毫秒,渲染的结果并不实时;

2. 每次 setData 都传递大量新数据

setData的底层实现可知,我们的数据传输实际是一次 evaluateJavascript 脚本过程,当数据量过大时会增加脚本的编译执行时间,占用 WebView JS 线程,

3. 后台态页面进行 setData

当页面进入后台态(用户不可见),不应该继续去进行setData,后台态页面的渲染用户是无法感受的,另外后台态页面去setData也会抢占前台页面的执行。

度量性能指标

我们在优化性能时,指标是非常重要的,没有指标,你没法知道优化的点是否有效。不能单凭感觉去优化,要根据指标反馈,明确优化的成果。同时,优化就像个无底洞,要注意投入产出比。

用户反馈的卡顿,要么就是js执行消耗资源过多导致处理器没响应,要么是UI渲染消耗资源过多,导致UI没法响应用户操作。

通过查看代码,我们并没有消耗大量计算资源的业务逻辑,但是出现了UI反复操作和抢占资源的现象。

如何度量

可以利用setData的第二个参数,传入callback函数,统计渲染时长。代码如下

let startTime = Date.now()
this.setData(data, () => {
    let endTime = Data.now()
    console.log(endTime - startTime, '渲染时长')
})

案例分析

检查点:是否频繁去setData

检查结果:存在

产生原因:redux中监听的是整个store,只要store变化,就会执行setData操作,这就意味着页面无关的数据改变,也会触发该页面执行setData操作,但是这个操作是无意义的。

问题代码:

// libs/redux-wechat/connect.js

// 对整个store进行subscribe。变化就执行handleChange
this.unsubscribe = this.store.subscribe(handleChange.bind(this, options));

function handleChange(options) {
    ...省略代码
    const state = this.store.getState()
    const mappedState = mapState(state, options);
    this.setData(mappedState)
}

解决方案:

  1. 只监听当前页面用到的store中的部分数据,只有该部分数据变化,才setData。(store没提供单个数据的监听,如果自己修改redux实现,难度较大,同时修改太底层,容易出不可预料的异常。)
  2. 判断页面数据与需要更新数据是否相同,如果相同,不做操作。(这个方案成本比较低,就用它吧)

代码实现:

// libs/redux-wechat/connect.js
// 如果更新的数据和页面数据相同,不做操作。
function handleChange(options) {
    ...省略代码
    const state = this.store.getState()
    const mappedState = mapState(state, options);
    // 如果更新的数据和页面数据相同,不做操作。
    if (mappedState === this.prevState) return // 新加入代码
    this.setData(mappedState)
    // 保存上一次数据
    this.prevState = mappedState // 新加入代码
}

另外一个优化:如果store数据毫秒级变化怎么办,例如更新购物车的同时,还更新了购物数量,能不能把两次变化合并起来?因为store的数据是共享的,最后一次的更新就是最新的数据,可以采用节流起对请求进行合并。

      clearTimeout(this.setDataTMO)
      this.setDataTMO = setTimeout(() => {
        this.setData(mappedState)
      }, 50); // 时间可以看情况调整

检查点:每次 setData 都传递大量新数据

检查结果:存在

产生原因:

  1. 页面存在引用没用到的store数据。
  2. 后端返回数据直接进入store,后端接口返回冗余字段。

问题代码:

/pages/user/index.js
connect(state => ({
    member: state.member,
    mycoupon: state.mycoupon,
    guessLikeList: state.recommend.guessLikeList,
    locationInfo: state.common && state.common.locationInfo, //可删除
    selectedseller: state.home.selectedseller,//可删除
    carts: state.carts.carts,//可删除
    ...state.common
  }))

解决方案:

方法一:删除页面无用的connect (老业务在使用,修改存在风险,通过后续迭代优化)
方法二: 请求后端接口后,拿到数据进行优化处理再把数据传入store(成本较高)

检查点:后台态页面进行 setData

检查结果:存在

产生原因:redux connect设计与小程序有差异

问题代码:

// libs/redux-wechat/connect.js
    function onLoad(options) {
      ...省略部分代码
      if(shouldSubscribe){
        this.unsubscribe = this.store.subscribe(handleChange.bind(this, options));
        handleChange.call(this, options)
      }
    }
    function onUnload() {
	  ...省略部分代码
      // 页面onUnload时,才解除监听
      typeof this.unsubscribe === 'function' && this.unsubscribe()
    }

小程序生命周期中,onUnload会在页面销毁时执行,例如A->B->C->D 的跳转,A页面一直在监听store的变化,如果D页面修改数据,会造成A,B,C页面也执行setData操作,抢占了D的资源,因此造成卡顿。

解决方案:

  1. 后台状态的页面在setData时直接return(目前采用该方法)
  2. 当页面隐藏时,移除监听。

代码实现:

// 因为在后台的页面setData会抢占前台资源,所以在后台的页面不要执行setData操作
if (this.route !== _getActivePage().route) return

但是由于在后台的页面数据没法更新,如果D页面修改A引用的数据,就会出现A引用旧数据问题,所以在onShow的时候做一次同步。

    // 后台的页面切换到前台的时候,做一次数据同步
    function onShow(options) {
      if(shouldSubscribe){
        handleChange.call(this, options)
      }
      if (typeof _onShow === 'function') {
        _onShow.call(this, options)
      }
    }

指标测试

做了这么多,到底有没用,拿出来溜一溜就清楚了。

测试平台:iphone7、三星s7 、小程序开发工具

测试流程:首页 -> 配送到家 -> 加入购物车 -> 结算 ->查看订单

测试指标:调用setData次数,渲染总耗时,平均单次渲染耗时

未优化指标:

平台 setData次数 渲染总耗时(ms) 平均单次渲染耗时(ms)
三星s7 204 250258 1226
iphone7 167 38260 229
小程序开发工具 193 36811 190

优化后指标:

平台 setData次数 渲染总耗时(ms) 平均单次渲染耗时(ms)
三星s7 28 11227 400
iphone7 28 3971 141
小程序开发工具 31 2489 80

差异对比:

平台 setData次数差 渲染总耗时差(ms) 平均单次渲染耗时差(ms)
三星s7 176 239031 826
iphone7 139 34289 88
小程序开发工具 162 34322 110

总结:

  1. 优化后setData次数平均下降150次。
  2. 渲染耗时越是卡顿的机器,收益越大,三星s7平均每次渲染耗时降低826ms。

我的上一份工作

已经有半年时间没写文章了,不是因为这半年太忙而是太懒,期间也冒出过很多想法,但没动力尝试。我感觉现在状态非常差,自信不足,做事敷衍了事,同事关系糟糕。
我想通过这篇文章总结自己为什么跳槽后,变成现在这样,下面的内容会有点记流水账。

文章出现这种格式为如果我作为部门leader,我会怎么解决问题

面试

4月中经过两周时间投递和面试,虽然收到的offer不少,但是普遍薪酬较低,只有两个offer薪酬能达到预期,其中一家是创业公司,刚获得天使轮投资,虽然薪酬占优,但是考虑到发展与稳定性,最终选择了另一家国内传统零售龙头公司,下面称其为Y公司。

选择

选择Y公司有两个因素:1、Y公司的传统业务在国内零售行业占比较大份额,有自己的供应链,长期处于盈利状态,稳定性较高。2、Y公司得到腾讯高额融资,试水新零售业务,需要IT系统支持,团队尚未成熟,有机会可以施展拳脚。

入职体验

入职第一天,被安排到一个临时工位,没有告诉你文档在哪里,代码仓库在哪里,去哪里开通账号,所有东西都要问同事,同时由于不同权限负责人不同,开通所有账号,花费约一周。

入职体验作为新员工接触公司的第一印象,尤为重要。B公司这方面做的比较好,在员工入职前一天,HR发送邮件让负责账号管理的同事,开通所有账号,并邮件抄送给其上级主管,员工入职当天,需要主管接待新员工,跟团队成员做一下互动,让大家互相认识。

接触代码

拿到代码后,第一时间了解项目架构与开发规范,但是发现项目由于人员变更过于频繁,同时前期没有做严格的开发规范,导致项目出现较多问题。

1、代码格式和风格多样。

引入eslint做静态检测,可以增加git hook强制提交代码时校验。统一代码格式化规则,使用prettier插件导出配置文件。

2、由于小程序早期版本不支持组件,项目存在大量应用template开发,但是template的css是全局的,为了防止css冲突,template的class命名会很长,但是也还是无法解决样式冲突。另外template还会操作setData方法,会导致数据不可控。

多人开发的项目,代码的隔离性较为重要,但是template模式明显违反了该原则。在小程序支持组件后,应该在编写新功能时使用组件开发,逐步替换template。同时规范UI组件和业务组件,维护好组件文档。但是在推行组件模式开发时,有部分开发表现得较为排斥,最终未能推进组件化,导致项目仍然有大量混合开发。

3、代码分层与职责不清晰。由于没有独立接口层,网络请求定义会被嵌入到逻辑层,不仅无法复用请求代码还会导致层级职责不清,修改代码时较难找到对应代码

加入独立的apis目录,将项目用到的所有接口统一在apis目录,同时优化http层,统一处理网络错误和服务端通用异常。

4、代码圈复杂度高。圈复杂度表示一个函数具有的分支个数,例如一个if else复杂度就是2,如果一个函数圈复杂度高,就表示该函数逻辑过于复杂,维护和理解成本高

如果使用vscode,可以安装CodeMetrics插件,时刻计算函数的复杂度。对于老旧代码,可以使用sonar扫码统计。

5、代码重复率高。使用sonar扫描,显示有4.5%代码重复,67处重复代码块。

重复的代码,说明开发时有复制粘贴代码现象,可复用代码没有提炼到公共方法,粗暴的复制代码,增加了项目代码量,也增加维护难度。解决这个问题可以用sonar扫码静态代码,根据提示具体代码块,删除重复。

管理问题

虽然上面说了很多技术问题,但我认为技术问题只是一个表象,核心问题还是企业管理问题。上面是以技术的身份分析问题,这个小节,我会以管理者的身份分析问题。

1、选对人。 在项目启动初期,选对人编写第一行代码尤为重要,他决定项目的代码风格,架构,编码规范。如果写下第一行代码的人缺乏架构能力,或管理团队的经验,容易忽略制定相关规范。
在Y公司,项目原有四名开发,两个月内离职三名,遗留的代码债务需要新开发偿还,同时由于架构过于松散,偿还债务的时候会引起大量bug,新开发为防止产生bug,会偏向复制代码进行修改,进一步恶化代码。

2、授权。 在接手项目初期,虽然发现很多问题,但是因为没有行政权,leader也没有正式授权项目由我负责。在推动改造的时候,老员工有较大排斥情绪,最终导致制定的开发规范只是一纸空文。

作为部门leader,选择项目负责人前,应该慎重考察评估该负责人的责任心和团队管理能力。一旦选中,就应该明确其责任与权利。

3、减少沟通。 沟通看着像一个褒义词,在我看来,在企业里过多的沟通是影响效率的重要因素。在Y公司,由于项目排期,接口对接,测试对接等都没有利用系统管理。会出现你依赖的资源,例如UI,接口,你需要多次询问开发进度,资源提供时间。测试在提交bug的时候,只在企业微信留言,不但会打断开发思维,还会让bug缺乏状态标记,开发修改完毕后,测试未能及时回归。

具有成熟开发流程的企业,跨职能合作时,应该遵循契约精神,在前期规划,各职能评估开发工时,输出资源有统一的平台管理,例如UI稿放到蓝湖,或者通过邮件发送。接口文档通过swigger维护或者RAP维护。测试人员发现bug,记录详细信息到jira,开发人员修复后,标记bug状态,测试人员可通过jenkins自行构建回归。

4、安排到正确的位置。 进入Y公司本以为是负责前端团队的建设的,进入后,安排的工作和初中级开发无异,每天的工作是编写业务代码,编写界面,调整几个像素。工作中毫无成就感,也对个人成长不利。

5、leader要能下得了战场。 作为小组技术负责人,如果连编码能力都不具备,不能带领团队攻坚的,是不称职的管理制。如果leader长期脱离编码环境,对组员提出的问题缺乏感知力,也就没法集中资源解决问题。如果你做到部门leader,管理上百人,请忽略这条。

离开

企业成功的重要因素是效率的提升,如果你所在的企业一直在原地打转和内耗,而且你无法改变现状,那选择离开是明智之举。

我的下一站是哪里?

这个问题思考了很久都没有得到明确的答案,在企业工作已经第七年了,重复与无趣的工作内容让人厌烦。但是迫于生计,企业是一个最保障的选择。在接下来的五年,互联网行业是不是一如既往的繁荣,还是泡沫破灭的到来?如何在动荡的时期,保住工作,更上一层楼了?我还没有答案,唯一能做的就是保持学习。

管理的四种职能

管理是通过计划、组织、领导及控制人员和其他组织资源来实现组织目的的过程。

计划:包括预测趋势的发展并决定最好的战略和对策来实现组织的目的和目标。这些目标之一是使客户满意。现代管理的趋势是通过团队来帮助监管环境、寻找生意机会并留意挑战。计划是一个关键的管理只能,因为其他只能的实现通常都非常以来一个好的计划。

组织:包括设计组织的结构,创造出让所有人与失误共同工作以实现组织的目标的环境和制度。现在的许多组织都是以客户为中心,其理念是让每个人在获利的请情况下为服务客户而工作,所以组织必须保持弹性及适应性。客户的需求会改变,组织应紧跟而上。

领导:是为组织创造愿景、交流、指导、培训、协助和激励他人,以有效地达成组织的目的和目标。领导的趋势是授权、给员工尽可能多的决策自由,让他们能够自我指导和我自激励。

控制:包括建立清晰的标准以确保组织按照既定目的和目标前进,奖励表现良好的员工,并在员工表现不当时采取纠正措施。基本上,它意味着衡量实际发生的事情是否符合组织的目的。

管理者的工作

计划:1、制定组织目标。2、发展战略达成目标。3、决定所需资源。4、订立标准

领导:1、引导和激励员工高效工作,达到组织的目标与目的。2、赋予任务。3、解释例行公事。3、说明政策。4、提供绩效反馈

组织:1、分配资源、分派工作,以及建立达成目标的程序。2、备妥一份说明职权与责任范围的结构表。3、招聘、选择、培训及发展员工。4、将员工分配到他们最能有效发挥的职位

控制:1、评估公司目标的达成情况。2、监控标准之下的绩效。3、奖励出色的表现。4、表示采取修正行动。

自动化持续集成系列 -- SonarQube代码质量管控

前言

该文章主要介绍如何把SonarQube集成到gitlab中,通过持续集成,实时获取代码质量报告,控制项目代码质量和减少技术债务。

SonarQube是什么?

SonarQube是一个代码质量管理平台,提供多语言支持,可以统计代码重复率,复杂度,覆盖率,不规范代码等指标。

官网地址,这里不介绍安装方法,可以在官网查看。其中要注意SonarQube只作为管理平台,要进行代码检测,还需要使用sonar-scanner。

代码质量的重要性

判断代码好坏的标准是在阅读别人代码时,说WTF的次数。
如果你所处的开发环境是团队开发,那必然会修改他人编写的代码,但是我们会发现,阅读不规范的代码,会影响你一天的心情,而且你有时候为了插入几行代码,需要花数天时间阅读整块业务代码,你生怕修改会引起副作用。
为了避免产生副作用,你用了最保险的方法修改,但是并不是好的设计,给项目增加了技术债务,后续维护的人继续在债务上累加,最终导致项目无法维护。
我们知道越早偿还债务,成本越低,项目质量仪表盘能在你产生债务的时候,立刻给你提醒,同时这些指标,也在推动团队的维护意识,保持代码的整洁。

集成到gitlab CI

step 1、安装SonarQube

我推荐大家使用docker安装,一条命令即可。

docker run -d --name sonarqube -p 9000:9000 -p 9092:9092 sonarqube:lts

执行完毕后,浏览器输入 http://ip:9000 账户密码为 admin 登录

step 2、安装sonar-gitlab-plugin

如截图,在Marketplace搜索gitlab安装,安装后重启

image

step3、关联gitlab CI

如截图,填写gitlab地址和token。

image

token获取方法,在user setting -- access Token自行生成

image

step4、配置CI任务

在.gitlab-ci.yml文件添加两个构建任务。

stages:
  - analysis

sonarqube:
  stage: analysis
  image: ciricihq/gitlab-sonar-scanner
  variables:
    SONAR_URL: http://47.74.243.221:9000/ # 你自己的服务器
    SONAR_ANALYSIS_MODE: preview # 表示分析报告和commit绑定
  script:
    - gitlab-sonar-scanner


sonarqube-reports:
  stage: analysis
  image: ciricihq/gitlab-sonar-scanner
  variables:
    SONAR_URL: http://47.74.243.221:9000/ # 你自己的服务器
    SONAR_ANALYSIS_MODE: publish # 表示上传分析报告
  script:
    - gitlab-sonar-scanner

step 5、提交代码,看看效果

如果配置正确,在gitlab CI 的commit下,会看到如下截图

image

在SonarQube服务器,可以看到项目质量仪表盘。

image

重构的感悟

由于最近换了新工作,新公司的代码在结构和规范上都不是很好,于是希望后续通过重构来优化代码。

但是重构的标准是什么?什么才叫好的代码呢,如果单凭经验,容易陷入个人喜好主义,那和之前的代码有何区别呢?也许别人看你重构的代码也和你现在看别人代码一样呢,这样不就变成一个死循环了吗。

因此,在重构前,我觉得有必要阅读相关资料,寻找理论依据去支撑我的行动。业界评价度颇高的一本叫《重构》的经典著作里面的观点,应该是大家达成共识最好途径。

下面摘录了部分阅读中感触比较深的段落。

《重构—改善既有代码的设计》

何时重构

最近跟同事经常的对话是:
我:小明,你知道那块代码是什么意思吗,代码有点混乱,我没法弄懂。
小明:那块代码很复杂的,现在已经能跑,你就别动它了。
下面是书上提到何时应该重构,看下面的场景,是不是和我目前的状况非常相似呢。

你的态度也许倾向于尽量少修改程序:不管怎么说,它还运行得很好。你心里牢牢记住那句古老的工程谚语:“如果它没坏,就不要动它。”这个程序业务还没坏掉,但它造成了伤害。它让你的生活比较难过,因为你发现很难完成客户所需的修改。这时候,重构技术就该粉墨登场了。

怎么跟经理讲重构

最近跟产品经理讨论,我们需要抽取时间进行代码重构,产品经理的观点是以完成工作为优先,重构工作其实没那么重要。但是我是不同意他的观点的,今天刚好读到了这一个话题,给我提供了一个完整的理论基础。

很多经理嘴巴上说自己“质量驱动”,其实更多是“进度驱动”。这种情况下我会给他们一个较有争议的建议:不要告诉经理!

软件开发者都是专业认识,我们的工作就是尽可能快速创造出高效软件。对于创造软件,重构可带来巨大帮助,如果需要添加新功能,而原本设计却又使我无法方便地修改,先重构再添加新功能会更快些。受进度驱动的经理要我尽可能快速完事,至于怎么完成,那就是我的事了。

关于技术债务

我以前理解的技术债务只是有不良的代码,需要抽时间进行偿还。书上说,债务是会产生利息的,确实是这样,不良的代码就像债务,会产生利息,时间越长,利息越高,最后将会无法偿还,直到破产。

“技术债务”:把未完成的重构形容为“债务”,很多公司都需要借债来使自己更有效的运转,但是借债就得付利息,过于复杂的代码所造成的维护和扩展的额外成本就是利息。你可以承受一定程度的利息,但如果利息太高你就会被压垮。

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.