回顾整理Node

#node

我大概从 15 年开始使用 node,最早花了大概10 天吧。基于 ThinkJS(因为当时我们技术栈是 ThinkPHP,为了更好了解 ThinkPHP 就选择了这个) 写了第一版 blog,前端包括后台。ThinkJS + MongoDB,服务器是阿里云CentOS。功能主要就是文章的 CURD。

在之后创业过程中我们的产品里,所有的 mock 服务,全是本地自建 node,来完成 mock server(前端团队最多达到4人。)

之后忘记过了多久好像是 16 年吧,微服务很火。我就又用 Express 又起了个服务,做我blog 邮件订阅。相当于 ThinkJK 跑在 3000, Express 跑在 3001。 文章写好后,我可以通过一个按钮选择要不要发邮件通知所有订阅者。(不过后面发现发邮件的服务其实有并发限制挂了,就没维护了,否则起个队列发就好了)

而在最近,我又用 Next 把 blog 重构成了 SSR 渲染。

项目经验

在 17 年的时候临近毕业还陆续做了一些产品。使用 采用 express 作为中间件转发 Java 接口,通过 node 服务端渲染 HTML(多页应用)。用户不过没多少好像几百几千吧。也依旧可以独立完成部署,运行监控,错误钉钉报警。

在这些过程中积累的知识涵盖了

  • 购买域名
  • 万网配置域名
  • Nginx 反向代理
  • Nginx 常用 access/error日志查看
  • PM2 部署上线,PM2 热更新,PM2 日志查看定位服务器问题
  • htop 服务器运行状态查看
  • SSH 远程登录
  • MongoDB 的持久化启动(不开的话,SSH 断了数据库就关了)
  • rsync 同步文件
  • Node 项目的TS的支持(目前 TS 经验也快 2 年了)
  • 运行时错误钉钉实时报警

未来

虽然目前工作中都用不到 node,但是平时我也没有少关注 node 的一些发展动向和生态,比如node12 版本后的性能提升。比如现有的一些比较火的 node框架,typeorm,nest,egg,midway,之前也稍微感兴趣去看了 midway 和 nest 之间的依赖注入实现,等等

所以对于未来来说,我认为,直接让我上手 node 的业务应该也是 OK 的。

  • 不过还需要进一步学习正规的规范的大规模的 node 使用方式。涵盖部署,监控,报警,运维等。

  • 容器(Docker)集群(K8S)的方式部署 node。

  • 以及逐渐过渡到serverless。

PS: 在这之前应该可以尝试自己独立跑起 serverless,感受一下。只要有场景有项目,感觉都没有什么太大的问题吧

我们是如何做视图和视图逻辑分离的

#react

本文将分享一下过去一年里,我们项目是如何做视图与视图逻辑抽离的。

什么是视图?什么是逻辑?

正所谓视图就是身为一个用户可见到的图像,对于这个图像来说它正是广为流传的 view = f(data)。这个公式精确的表达了视图就像是一个函数一般,输入即输出,所见即所得,没有任何副作用。

<div>
    <input placeholder="修改名字" onChange={handleChange} />
    <p>姓名:{username}</p>
<div>

以上这样一段DOM代码,它就是我们所见到的视图,它是纯粹的,给了什么就会渲染什么。当用户有了交互的事件,数据有了变化,就会渲染新的视图。

而视图逻辑就是我们前端工程师对视图的显示,对视图的 data 进行的处理。它可能是来自于服务端,可能是来自于本地,亦或者是来自于用户自有的操作行为。一切让数据改变,或者对数据进行操作的行为等等,这些都是我们的业务逻辑。

username = api.getUserName()

handleChange(e) {
    this.username = e.target.value;
}

过去的组件

按照惯例,我们会习惯于把视图和业务逻辑都写在一起,当视图越来越庞大或者逻辑越来越复杂。就会让我们的代码越来越不易维护和测试。比如这样的代码

class Demo extends React.Component<Props> {

  constructor(props: Props) {
    super(props);
    this.state = {
      data: null,
    }
  }

  async componentDidMount() {
    const ret = http.get(`/api/xx/${this.props.id}`);
    this.setState({
      data: ret.data,
    })
  }

  handleClick = () => {
    // do something...
  }

  // other methods...

  render() {

    return (
      <div>
        <p onClick={this.handleClick}>click</p>
        {this.state.data}
      </div>
    )
  }
}

通常我们一个组件的实现大致都长这样,随着业务逻辑复杂,我们Demo组件需要存放的属性和方法也越来越多。我们的dom结构也越来越大。如何抽象封装这样的组件,如何提取我们的业务逻辑,组织出更加可维护易测试的代码,成为大型项目的关键。

改进方案

首先我们可以基于MVP或者MVC的思想,把视图和逻辑抽离分别分为两个文件。(以下的示例代码仅作为思想,不一定能实际运行。)

import { DemoPresenter } from './Demo.presenter';

class Demo extends React.Component<Props> {

  constructor(props: Props) {
    super(props);
    this.presenter = new DemoPresenter(props);
  }

  async componentDidMount() {
    const { fetchData } = this.presenter;
    await fetchData();
  }

  render() {
    const { handleClick, data } = this.presenter;
    return (
      <div>
        <p onClick={handleClick}>click</p>
        {data}
      </div>
    )
  }
}
class DemoPresenter {

  data = {};

  constructor(props: Props) {
    this.props = props;
  }

  fetchData = async () => {
    const { id } = this.props;
    const ret = await http.get(`/api/xx/${id}`);
    this.data = ret.data;
  }

  handleClick = () => {
    // do something...
  }
}

自此我们尝试着把视图和业务逻辑抽离成了两个文件,分别用两个class来维护。降低了视图和逻辑之间的耦合。另外从测试的角度来说,我们可以剥离视图,单独为我们的业务逻辑写UT,这就极大的降低了测试的成本。

看到这里大家有没有发现这特别像某一种代码?其实这不就像是mobx里的inject store吗?把我们手动new Presenter的过程通过inject来完成。

@inject('demoPresenter')
class AppComp extends React.Component<Props> {
  render() {
    const { demoPresenter } = this.props;
    const { handleClick, data } = demoPresenter;

    return (
      <div>
        <p onClick={handleClick}>click</p>
        {data}
      </div>
    )
  }
}

我们是否可以根据这个启发继续摸索呢?

改进一下

mobx是基于providerinject来实现的。而providerinject又是基于react的context。这里就有一个问题,mobx主要为我们做全局状态管理。而我们需要的仅仅是局部的视图和逻辑抽离。又应该如何做呢?

站在mobx的肩膀上,我们来实现一个高阶组件做我们presenter注入。而这两年MVVM这么火,再加上presenter这单词着实麻烦,我们换个名称好了。把我们的视图逻辑层就叫ViewModel吧,当然这里指的是广义上的VM

import React from 'react';

function withViewModel<P = {}>(
  Component: React.ComponentType<any>,
  ViewModel: new (...args: any[]) => any,
) {
  return class withViewModelComp extends React.Component<Omit<P, 'vm'>> {
    vm: any;
    constructor(props: Omit<P, 'vm'>) {
      super(props);
      this.vm = new ViewModel(props);
    }

    render() {
      return <Component {...this.props} vm={this.vm} />;
    }
  };
}

export { withViewModel };

看下我们这个高阶组件的实现非常简单,自动帮我们new一下VM,然后传递给我们需要的组件。我们的组件就可以这样使用

import React from 'react';
import { observer } from 'mobx-react';
import { withViewModel } from '../../hoc';
import { TestVM } from './TestVM';
import { Props } from './types';

@observer
class TestComp extends React.Component<Props> {
  render() {
    const { vm } = this.props;

    return <div onClick={vm.setUserName}>{vm.userName}</div>;
  }
}

// 绑定我们的组件和VM
const Test = withViewModel<Props>(TestComp, TestVM);

export { Test };
// Test.VM.ts
import { observable, action, computed } from 'mobx';

class TestVM {
  @observable userName = '二哲1号';

  @action
  setUserName = () => {
    this.userName = '二哲2号';
  };
}

export { TestVM };

基于mobx,我们现在达到了视图与视图逻辑抽离的目标了。但是有没有发现这样的代码其实还是有问题的?我们虽然在hoc里new VM的时候把props传递进去了,但那是静态的,如果我们写了一段computed是不会生效的。

// Test.VM.ts
import { observable, action, computed } from 'mobx';

class TestVM {
  @observable userName = '二哲1号';
  @observable props: any;

  constructor(props: any) {
    this.props = props;
  }

  @computed
  get someValue() {
    return this.props.value + this.userName;
  }

  @action
  setUserName = () => {
    this.userName = '二哲2号';
  };
}

export { TestVM };

如果我们父组件传递的value props变化了,someValue是拿不到最新的值的。接着我们来修复这个问题。

进阶版

在我初次思考这个问题的时候,我本以为是无解的。因为我们如论如何都需要把props传递给我们VM才行,那一定就是静态的。但如何能与我们的 VM绑定起来就成为了一个关键

这就意味着要处理两件事情。第一个问题是收集props依赖,第二个则是当props变化了,我们传递进VM里的props需要得到相应。

最后我在mobx源码中得到了灵感。https://github.com/mobxjs/mobx-react-lite/blob/master/src/useAsObservableSource.ts#L20-L30

import React from 'react';
import { observable, runInAction, IObservableObject } from 'mobx';

function withViewModel<P = {}>(
  Component: React.ComponentType<any>,
  ViewModel: new (...args: any[]) => any,
) {
  return class withViewModelComp extends React.Component<Omit<P, 'vm'>> {
    vm: any;
    vmProps: IObservableObject;
    constructor(props: Omit<P, 'vm'>) {
      super(props);
      // 转为mobx 观察对象
      this.vmProps = observable(props, {}, { deep: false });
      // 传递引用
      this.vm = new ViewModel(this.vmProps);
    }

    componentDidUpdate() {
      // props变化的时候,重新更新一下我们的观察对象
      runInAction(() => {
        Object.assign(this.vmProps, this.props);
      });
    }

    render() {
      return <Component {...this.props} vm={this.vm} />;
    }
  };
}

export { withViewModel };

新增三行代码,通过mobx的observable和runInAction我们很容易就可以完成我们的目的。

hook

刚刚我们都是基于class实现的,hook这么火爆,当然也少不了我们hook版本。hook实现相对来说就简单许多了。

import { useMemo } from 'react';
import { useAsObservableSource } from 'mobx-react-lite';

function useVM<T>(VM: new (...args: any[]) => T, props: any = {}) {
  const source = useAsObservableSource(props);
  return useMemo(() => new VM(source), []);
}
const HookComponent = (props: Props) => {
  const vm = useVM<HookVM>(HookVM, props);

  return (
    <div onClick={vm.setUserName}>
      hook 组件 组件内部数据 = {vm.userName} 父组件传入数据 = {vm.name111}
    </div>
  );
};

总结

  1. 本文我们通过两个例子对比,可以深刻意识到视图和逻辑分离的重要性
  2. 通过mobx的启发,我们分别实现了基于Class/SFC和hook的视图逻辑分离方案
  3. 视图和逻辑分离可以更好的锻炼我们封装抽象思维,写出可维护性更强的代码
  4. 视图和逻辑分离更加易于测试,可以单独测试视图或者逻辑

代码示例地址在:https://github.com/MeCKodo/view-and-view-logic 个人网站:http://www.meckodo.com

深入理解webpack的require.context

#webpack

前言

require.context 其实是一个非常实用的 api。但是 3-4 年过去了,却依旧还有很多人不知道如何使用。

而这个 api 主要为我们做什么样的事情?它可以帮助我们动态加载我们想要的文件,非常灵活和强大(可递归目录)。可以做 import 做不到的事情。今天就带大家一起来分析一下,webpack 的 require.context是如何实现的。

准备工作

在分析这个 api 之前呢,我们需要先了解一下一个最简单的文件,webpack 会编译成啥样。

-- src
    -- index.ts
// index.ts
console.log(123)

编译之后,我们可以看见 webpack 会编译成如下代码

// 源码 https://github.com/MeCKodo/require-context-sourece/blob/master/simple-dist/bundle-only-index.js
 (function(modules) { // webpackBootstrap
     // The module cache
     var installedModules = {};
     // The require function
     function __webpack_require__(moduleId) {
         // Check if module is in cache
         if(installedModules[moduleId]) {
             return installedModules[moduleId].exports;
         }
         // Create a new module (and put it into the cache)
         var module = installedModules[moduleId] = {
             i: moduleId,
             l: false,
             exports: {}
         };
         // Execute the module function
         modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
         // Flag the module as loaded
         module.l = true;
         // Return the exports of the module
         return module.exports;
     }
     // expose the modules object (__webpack_modules__)
     __webpack_require__.m = modules;
     // expose the module cache
     __webpack_require__.c = installedModules;
     // define getter function for harmony exports
     __webpack_require__.d = function(exports, name, getter) {
         if(!__webpack_require__.o(exports, name)) {
             Object.defineProperty(exports, name, {
                 configurable: false,
                 enumerable: true,
                 get: getter
             });
         }
     };
     // define __esModule on exports
     __webpack_require__.r = function(exports) {
         Object.defineProperty(exports, '__esModule', { value: true });
     };
     // getDefaultExport function for compatibility with non-harmony modules
     __webpack_require__.n = function(module) {
         var getter = module && module.__esModule ?
             function getDefault() { return module['default']; } :
             function getModuleExports() { return module; };
         __webpack_require__.d(getter, 'a', getter);
         return getter;
     };
     // Object.prototype.hasOwnProperty.call
     __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
     // __webpack_public_path__
     __webpack_require__.p = "";
     // Load entry module and return exports
     return __webpack_require__(__webpack_require__.s = "./src/index.ts");
 })
 ({
 "./src/index.ts": (function(module, exports) {
      console.log('123');
    })
 });

初次一看是很乱的,所以为了梳理结构,我帮大家去除一些跟本文无关紧要的。其实主要结构就是这样而已,代码不多为了之后的理解,一定要仔细看下每一行

// 源码地址 https://github.com/MeCKodo/require-context-sourece/blob/master/simple-dist/webpack-main.js

(function(modules) {
  // 缓存所有被加载过的模块(文件)
  var installedModules = {};
  // 模块(文件)加载器 moduleId 一般就是文件路径
  function __webpack_require__(moduleId) {
    // 走 cache
    if (installedModules[moduleId]) {
      return installedModules[moduleId].exports;
    }
    // Create a new module (and put it into the cache) 解释比我清楚
    var module = (installedModules[moduleId] = {
      i: moduleId,
      l: false,
      exports: {}
    });
    // 执行我们的模块(文件) 目前就是 ./src/index.ts 并且传入 3 个参数
    modules[moduleId].call(
      module.exports,
      module,
      module.exports,
      __webpack_require__
    );
    // Flag the module as loaded 解释比我清楚
    module.l = true;
    // Return the exports of the module 解释比我清楚
    return module.exports;
  }
  // 开始加载入口文件
  return __webpack_require__((__webpack_require__.s = './src/index.ts'));
})({
  './src/index.ts': function(module, exports, __webpack_require__) {
    console.log('123');
  }
});

__webpack_require__ 就是一个模块加载器,而我们所有的模块都会以对象的形式被读取加载

modules = {
    './src/index.ts': function(module, exports, __webpack_require__) {
       console.log('123');
    }
}

我们把这样的结构先暂时称之为 模块结构对象

正片

了解了主体结构之后我们就可以写一段require.context来看看效果。我们先新增 2 个 ts 文件并且修改一下我们的 index.ts,以便于测试我们的动态加载。

--- src
    --- demos
        --- demo1.ts
        --- demo2.ts
    index.ts
// index.ts
// 稍后我们通过源码分析为什么这样写
function importAll(contextLoader: __WebpackModuleApi.RequireContext) {
  contextLoader.keys().forEach(id => console.log(contextLoader(id)));
}

const contextLoader = require.context('./demos', true, /\.ts/);
importAll(contextLoader);

查看我们编译后的源码,发现多了这样一块的 模块结构对象

// 编译后代码地址 https://github.com/MeCKodo/require-context-sourece/blob/master/simple-dist/contex-sync.js#L82-L113
{
'./src/demos sync recursive \\.ts': function(module, exports, __webpack_require__) {
  var map = {
    './demo1.ts': './src/demos/demo1.ts',
    './demo2.ts': './src/demos/demo2.ts'
  };

  // context 加载器,通过之前的模块加载器 加载模块(文件) 
  function webpackContext(req) {
    var id = webpackContextResolve(req);
    var module = __webpack_require__(id);
    return module;
  }

  // 通过 moduleId 查找模块(文件)真实路径
  // 个人在这不喜欢 webpack 内部的一些变量命名,moduleId 它都会编译为 request
  function webpackContextResolve(req) {
    // id 就是真实文件路径
    var id = map[req];
    // 说实话这波操作没看懂,目前猜测是 webpack 会编译成 0.js 1.js 这样的文件 如果找不到误加载就出个 error
    if (!(id + 1)) {
      // check for number or string
      var e = new Error('Cannot find module "' + req + '".');
      e.code = 'MODULE_NOT_FOUND';
      throw e;
    }
    return id;
  }

  // 遍历得到所有 moduleId
  webpackContext.keys = function webpackContextKeys() {
    return Object.keys(map);
  };
  // 获取文件真实路径方法
  webpackContext.resolve = webpackContextResolve;
  // 该模块就是返回一个 context 加载器
  module.exports = webpackContext;
  // 该模块的 moduleId 用于 __webpack_require__ 模块加载器
  webpackContext.id = './src/demos sync recursive \\.ts';
}

我在源码中写了很详细的注释。看完这段代码就不难理解文档中所说的require.context 会返回一个带有 3 个API的函数(webpackContext)了。

require.context api

接着我们看看编译后 index.ts 的源码

'./src/index.ts': function(module, exports, __webpack_require__) {
  function importAll(contextLoader) {
    contextLoader.keys().forEach(function(id) {
      // 拿到所有 moduleId,在通过 context 加载器去加载每一个模块
      return console.log(contextLoader(id));
    });
  }
  var contextLoader = __webpack_require__(
    './src/demos sync recursive \\.ts'
  );
  importAll(contextLoader);
}

很简单,可以发现 require.context 编译为了 __webpack_require__加载器并且加载了 id 为./src/demos sync recursive \\.ts 的模块,sync表明我们是同步加载这些模块(之后我们在介绍这个参数),recursive 表示需要递归目录查找。自此,我们就完全能明白 webpack 是如何构建所有模块并且动态加载的了。

进阶深入探究 webpack 源码

我们知道 webpack 在 2.6 版本后,在加载模块时,可以指定 webpackMode 模块加载模式,我们能使用几种方式来控制我们要加载的模块。常用的 mode一般为sync lazy lazy-once eager

所以在 require.context 是一样适用的,我们如果查看一下@types/webpack-env就不难发现它还有第四个参数。

简要来说

  • sync 直接打包到当前文件,同步加载并执行
  • lazy 延迟加载会分离出单独的 chunk 文件
  • lazy-once 延迟加载会分离出单独的 chunk 文件,加载过下次再加载直接读取内存里的代码。
  • eager 不会分离出单独的 chunk 文件,但是会返回 promise,只有调用了 promise 才会执行代码,可以理解为先加载了代码,但是我们可以控制延迟执行这部分代码。

文档在这里 https://webpack.docschina.org/api/module-methods/#magic-comments。

这部分文档很隐晦,也可能是文档组没有跟上,所以如果我们去看 webpack 的源码的话,可以发现真正其实是有 6 种 mode。

mode类型定义 https://github.com/webpack/webpack/blob/master/lib/ContextModule.js#L13

那 webpack 到底是如何做到可递归获取我们的文件呢?在刚刚上面的源码地址里我们能发现这样一行代码。

这一看就是去寻找我们所需要的模块。所以我们跟着这行查找具体的源码。

这就是 require.context 是如何加载到我们文件的具体逻辑了。其实就是fs.readdir而已。最后获取到文件之后在通过 context 加载器来生成我们的模块结构对象。比如这样的代码就是负责生成我们sync类型的context加载器。大家可以具体在看别的5种类型。

6种类型加载逻辑并且生成 context 加载器的模块结构对象 https://github.com/webpack/webpack/blob/master/lib/ContextModule.js

总结

1.学习了解 webpack 是如何组织加载一个模块的,webpack 的加载器如何运作,最后如何生成编译后的代码。

2.本来仅仅只是想了解 require.context 如何实现的,却发现了它第三个参数有 6 种mode,这部分却也是 webpack 文档上没有的。

3.从一个实用的 API 出发,探索了该 api 的实现原理,并且一起阅读了部分 webpack 源码。

4.探索本质远比你成为 API 的搬运工更重要。只有你不断地探寻本质,才可以发现世界的奥秘。

最后抛砖引玉,可以按照这样的思路再去学习另外 6 种 mode 编译后的代码。

文章里编译后的代码,都在这里 >>> https://github.com/MeCKodo/require-context-source

GMTC - 前端的思考

#js

前端今天的机会

第一个部分要跟大家分享的是目前阿里在前端领域所关心的领域和方向。在当下主要有以下 四个个方向。

  • 搭建服务
  • Serverless
  • 前端智能化
  • IDE

搭建服务

搭建服务首先由三个部分组成 框架标准化 模块标准化 服务标准化

在我们目前当下的开发模式里,是越来越偏向低代码/无代码的体系。而低代码无代码的体系是通过什么样的方式来完成?就是基于今天前端的各种框架,中台框架,组件,元件,页面,模块,等等。都是让我们写出更加少的代码,更加标准化的代码,更加抽象封装的代码。甚至,它可能是通过搭建的方式,组装的方式,可视化的方式。在阿里的目前业务里,越来越多的服务其实都是高度抽象化的,无论是淘宝里的频道,活动,还是业务的中台。他们都会有大量相似的场景存在,大量高度抽象化的场景存在。而如何快速的完成这些业务,我们就可以通过搭建服务。需要将框架标准化,模块标准化,希望把这样的服务,能够服务于今天所有的中后台,C 端的业务也好,通过这样的体系,打通所有的体系。尽可能的服务于更多的业务面。

不难看出,统一标准化,提升效率,这可能是永恒的一个方向。

Serverless

首先思考一个问题为什么 Serverless 会火?

今天我们大量的谈论 Serverless。而 serverless 可以从两个角度来说。一个是对内,今天站在公司的角度 serverless 对我的影响是什么,一个是对外,在云上又如何提供,提供什么样的能力出去。

对内,它可以让前端更加贴近业务,让前端的能力更加的下沉,上层的能力能够让前端更加的 focus 在里面。

Serverless 最大的好处是什么?虽然 node 兴起,让前端赋予了更多的能力,甚至操作服务端的能力。但一个前端并不是有了 node 就拥有了上层的能力,前端并没有良好的部署,运维,监控,这样的意识。而到了 Serverless 这样的体系里面。身为前端就不需要去关注它的部署,运维,不用关注所有的 devops。可以释放前端更多的能力,更加专注于业务逻辑层面,仅仅通过脚本的编排就可以完成今天的业务逻辑。不用去关注更加底层的数据库状态,服务器并发,等等。

它可以让我们前后端整个的体系,像当年前后端分离一样,更往前迈进一部。但 Serverless 的体系不是前端就能完成的,它需要后端把能力平台化,把整个领域模型,数据模型,底层数据模型,能力模型高度抽象化。其实是对后台整个体系的一个重塑,在借助于 Serverless 的体系,让前端更加的 focus 业务,更加贴近于业务。这才是今天整个前后端体系的变革。

另一方面对外来说意味着什么?对外其实就是云+端。云上是什么?云上就是通过 Serverless 的方式提供今天函数式的编程,让所有的东西更加简单快捷。端是什么?端其实就是今天的小程序,从微信小程序兴起后。包括阿里头条百度等等都在做自家的小程序。小程序就是一种跨端的解决方案。它有一定的收敛,安全可控。云上它一定是以 Serverless 的方式更加跟端做连接。让这个体系运转的很好。

而 Serverless 今天需要做什么?今天底层的容器是怎么样的能做到函数的隔离?runtime 是怎样的?配套的开发工程,研发平台,上层数据的接入以及网关编排是怎么样的?这就是今天 Serverless 需要去聚焦的几大基础设施的建设。

前端智能化

AI * Front-End。今天我们能否实现一个设计稿出来后,我们就能直接转换为代码?也就是今天的 Design to code。智能化一定会带来前端未来的格局的变化。今天我们有大量的设计稿,大量的组件,元件,大量的设计语言,大量的源代码。这些都可是可学习的。未来的 Design to code 可以解放大量的前端生产力,解决大量低效的工作,手工的工作。

IDE

今天阿里的前端有一个工程中台。做到了前端代码的提交到发布的管控。编译,构建,检测,code review,发布,发布到源站。整个的通路。但是前台的每个团队都有一个工具,工具是割裂的没法复用。

工程不仅仅是提交到发布,前端的工程化应该从编码开始到发布,都是一个完整的链路,完整的流程。那如何去收敛整个的开发状态,就是通过一个 IDE 的内核,构建整个的基础,把整个体系打通。所有团队所有的能力作为组件的形式存在,组件是可以让更多人基于同样的架构,进行高质量的交互。这样才能形成一个完整的生态。

但如果仅仅是这个,它还是达不到整个集团的方向。为什么?比如我们今天还有很多 IXV,今天的小程序的开发,互动的开发,装修的开发。其实我们也是提供了一个客户端的 IDE。但是那个体系是一个内部的阉割版,整个体系是不通的,是割裂的。如果我们今天把我们整个 IDE 的架构从内到外全部打通。它就是基于一套完整的架构基于上面的能力我内外的能力都可以互通。可以带来整个生态的丰富性和能力的提升。

但仅仅这个依旧不够。

是否还能把本地的 IDE 和云的 IDE 完全打通。通过一套架构来完成所有的体系,通过组件的生态全部的打通来形成一个完整的闭环。在往外,回到今天云的体系。其实也是需要提供一个云的 IDE 来给更多人来使用。IDE 是可以把今天的内外,今天的线上线下全都打通。所有开发者的生态完全打通。这也是为什么把 IDE 作为很重要的战略。

未来的机会

对于未来,前端依旧还有许多分裂的领域。

  • 新交互(VR)
  • IoT
  • 5G

新交互

IoT

5G

咸鱼哲的2018

#奇心怪谈

点击链接快速查看! 咸鱼哲的2018

React应该如何优雅的绑定事件?

#react

前言

由于JS的灵活性,我们在React中其实有很多种绑定事件的方式,然而,其实有许多我们常见的事件绑定,其实并不是高效的。所以本文想给大家介绍一下React绑定事件的正确姿势。

常见两种种错误绑定事件

class ErrorExample1 extends Component {

    balabala(e) {console.log(e)}

    render() {
        const { text } = this.state;

        return (
          <Wrapper>
            {text}
            <Balabala onClick={(e) => this.balabala(e)}></Balabala>
          </Wrapper>
        )
    }
}
class ErrorExample2 extends Component {
    balabala(e) {console.log(e)}

    render() {
        const { text } = this.state;
        return (
          <Wrapper>
            {text}
            <Balabala onClick={this.balabala.bind(this)}></Balabala>
          </Wrapper>
        )
    }
}

这是两种最常见的React绑定事件代码,但它为什么是错误的?

每当你的text发生变化,就会rerender,只要组件重新render,那就会重新创建新的事件函数,进而绑定事件,这个过程的开销就是极大极大的浪费。相当于,每次rerender都会创建一次事件函数。

这据说是 Best Practice

class Balabala extends Component {
    constructor(p) {
        suprt(p);
        this.balabala = this.balabala.bind(this);
    }
    render() {
        const { text } = this.state;
        return (
          <Wrapper>
            {text}
            <Balabala onClick={this.balabala}></Balabala>
          </Wrapper>
        )
    }
}

然而我更喜欢的姿势

class Balabala extends Component {
    constructor(p) {
        suprt(p);
    }
    醒来记得想我 = (e) => {
        alert('想你了');
    }
    render() {
        const { text } = this.state;
        return (
          <Wrapper>
            {text}
            <Balabala onClick={this.醒来记得想我}></Balabala>
          </Wrapper>
        )
    }
}

利用箭头函数,帮我们bind this。避免了在构造函数里生命一堆的变量。减少键盘的敲击,延长生命。

当然,还有人会问,这样的写法如何传参呢?以前别人会这样写

class 渣男 extends Component {
    constructor(p) {
        suprt(p);
    }
    醒来记得想我 = (e, text) => {
        alert(text); // alert 滚吧,渣男
    }
    render() {
        const { text } = this.state;
        return (
          <Wrapper>
            {text}
            <Balabala onClick={this.醒来记得想我.bind(e, '滚吧,渣男')}></Balabala>
          </Wrapper>
        )
    }
}   

但是其实,我们可以这样传参,更加简洁明了

class 渣男 extends Component {
    constructor(p) {
        suprt(p);
    }
    醒来记得想我 = (text) => (event) => {
        alert(text); // 你渣我也喜欢,因为是你
    }
    render() {
        const { text } = this.state;
        return (
          <Wrapper>
            {text}
            <Balabala onClick={this.醒来记得想我( '你渣我也喜欢,因为是你')}></Balabala>
          </Wrapper>
        )
    }
}

动态绑定

基于这个我们还可以针对同一事件修改多个input值,进行事件优化

class ChangeMyName extends Component {
  修改渣男名称 = name => {
    if (!this.handlers[name]) {
      this.handlers[name] = event => {
        this.setState({ [name]: event.target.value });
      };
    }
    return this.handlers[name];  
  }

  render() {
    return (
        <>
          <input onChange={this.修改渣男名称('男神1号')}/>
          <input onChange={this.修改渣男名称('渣男2号')}/>
        </>
    )
  }
}

旁门左道,邪教!(个人不喜欢而已)

import autoBind from 'react-autobind';

class Balabala extends Component {
    constructor() {
       autoBind(this);
    }
    醒来记得想我 (e) {
        alert('想你了');
    }
    render() {
        const { text } = this.state;
        return (
          <Wrapper>
            {text}
            <Balabala onClick={this.醒来记得想我}></Balabala>
          </Wrapper>
        )
    }
}
import { BindAll } from 'lodash-decorators';

@BindAll()
class Bbb extends Component {}
// 这种写法等同于 bind
class Bbb extends Component {
    balabala(){}
    render() {
        const { text } = this.state;
        return (
          <Wrapper>
            {text}
            <Balabala onClick={::this.balabala}></Balabala>
          </Wrapper>
        )
    }
}

基本都大同小异吧,就不过多介绍了。看到这里,你也知道到底应该如何绑定事件了。

五分钟 Styled-components 高级实用技巧

#react

写在前面的废话

回到2013年,React凭空出世。但是在那时,我们会想,oh shit! 我们好不容易分离了HTML/CSS/JS, 为什么出现了JSX,我们又需要把HTML和JS耦合在一起?React 创造了 HTML in JS. 在React中,我们知道,一切即组件。那既然HTML能在js里写,为什么我们不把CSS也一起写呢?这样不才是一个真正的组件吗?

Styled-components就是为React而生的,它是CSS in JS的下一代解决方案。以往我们想要做到css scope都需要在webpack中各种配置,或者使用js的解决方案。而styled-components你只需要import styled from 'styled-components';即可。

甚至React完美的结合,不仅是从TagName上,还有Props上。使我们的代码有更好的语义化,可维护性更强,效率更高。当然我们无需考虑它的学习成本,只要你用过CSS或者SASS都可以立刻上手,因为它本身就是一种超集的存在。

接下来,我会逐步的介绍一些这段时间以来,我非常喜欢的独有的特性。

开胃菜

const Button = styled.button`
  background: #abcdef;
  border-radius: 3px;
  border: none;
  color: white;
`;
console.log(Button); //styled component
console.log(new Button()); // react component 

export default CustomButton extends React.component {
    render() {
        return <Button {...props} />
    }
}

styled-components 用了tagged template语法,直接为我们编写样式创建组件。

继承

styled-components继承样式有两种写法如下

const Button = styled.button`
  background: #abcdef;
  border-radius: 3px;
  border: none;
  color: white;
`;

const OtherButton1 = styled(button)``;
const OtherButton2 = button.extend``; // 老的写法,不推荐,未来会被废弃

写法一的继承,仅仅只会创建不一样的css rule,而第二种写法会复制一遍base component的css rule,然后在添加不一样的进行css 权重覆盖。不推荐

当然,还有一种有趣的“继承” withComponent,我们可以利用withComponent改变渲染的标签

const Li = styled.li`
    color:#abcdef;
`;
const A = Li.withComponent('a'); // 将会渲染a标签

编译后他们会使用不同的className,这对我们想用同个样式,但是不同标签非常有用。

样式覆盖

这里所说的样式覆盖,主要是一些交互上的行为(hover, active)覆盖。其实组件继承也算是覆盖的一种。

以往我们的覆盖写法如下:

const ListItem = styled.li`
  padding: 0;
  height: 48px;

  &.left-item-focus {
    .left-link {
       background: ${props => props.color};
    }
  }
  &:hover {
     .left-icon {
        color: #9e9e9e; // 500
     }
  }
`;

而在styled中,我们可以使用styled-components 组件方式对我们的DOM进行引用,从而覆盖样式,如下

const Icon = styled.span`
    color: red;
`;

const ListItem = styled.li`

    &:hover ${Icon} {
        color: green;
    }
`;

这依旧是我们过去的思路来覆盖样式,只是我们把选择器直接使用styled组件引用罢了。拥有这样的接口,就更加让我们无需去思考需要给组件取什么className或者id,从而达到覆盖样式的做法。然而还有我最喜欢的另外一种写法。

TIPS:组件的引用必须是styled-components包装后的组件,直接是react的会报错

const ListItem = styled.li``;

const Icon = styled.span`
    color: red;

    ${ListItem}:hover & { // & 代表icon组件
        color: green;
    }
`;

这段代码实现的是一样的功能,只是我们思路转换了一下。可以发现这样的代码更加没有侵入性。更加符合开放封闭原则,当我们不需要这个Icon组件时,直接把这个Icon删除即可,我们不用去父组件里寻找与该组件有关的样式,不容易造成样式污染。突然觉得眼前一亮,有木有!

当然这种“子组件引用父级”的功能,还有更加广泛的引用。你可以选择该DOM任何parent,再对自己进行样式的覆盖。如下:

const Icon = styled.span`
    color: red;

    html.ie-8 & {
        // fuck ie8
        color: blue;
    }
    body.xxx & {
        color: green;
    }
`;

当任何父级带有class都会覆盖Icon的样式。这种“子组件引用父级”的功能也是我最喜欢的功能没有之一。

在上面可以看见我们大量使用了&作为选择器,而&还有另外的技巧。

const Example = styled.li`
    color: red; 
    & {
        color:blue;
    }

    && {
        color: green;
    }
`;

大家可以猜猜,这最终会渲染成什么?

<li class='sc-gzVnrw fmpfVE'></li>

最终会编译成如下class,但是我们的一个&就代表一个class权重也就是说我们最后会渲染原谅色,原因是li被作用于了.fmpfVE.fmpfVE样式表。这个功能非常有用,比如在你使用第三方组件想要覆盖它的样式的时候,我们就可以加多个&来提高样式权重,从而覆盖第三方组件的样式

Theme

关于Theme只想说一点,那就是结合第三方组件应该如何传入Theme呢?我们有一个简单的技巧。比如使用了Material-UI,如果我们需要基于它拓展我们自己的组件,并且需要样式。

const ThemeProvider: React.SFC<ThemeProviderProps> = ({ themeName, children }) => {
  const theme = themes[themeName];
  return (
    <StyledThemeProvider theme={theme}>
      <MuiThemeProvider theme={theme}>
        {React.Children.only(children)}
      </MuiThemeProvider>
    </StyledThemeProvider>
  );
};

之后只需要把我们需要调用的组件使用styled-components提供的withTheme包装一下我们的组件来获取我们的theme。

这样既可以在我们的styled-components里取到theme,material里也可以了。

以上就是我们所有的技巧了, 看了这么多有意思的黑科技,难道你还不爱上styled-components吗?

没用到React为啥我写jsx还需要import它啊喂?

#react

提问

当我写一个纯函数组件的时候,为什么还特么需要import React from 'react' 代码如下

import React from 'react';

export default () => <div>你是傻X,爱我吗?</div>

从JSX说起

const Fool = (
    <div className="fool"><b>I'm fool, But I love you</b></div>
)

我们还知道想要编译这样的语法有一个Babel插件,babel-plugin-transform-react-jsx。如上JSX会编译成以下数据结构

既然能知道它会被Babel编译成啥样,那我们只需要解析这个对象即可组装成真正的DOM。

所以React有一个createElement方法,用来解析这编译后的对象

React.createElement(
  "div",
  { className: "fool" },
  "I'm fool, But I love you"
);

假装实现一个假的React?那我就叫她Tcaer

在这我们把createElement方法改写成与Vue一样(在Vue里创建Vdom用得是h方法。虽然我不知道为什么叫h。这么带有颜色的字母) 同时我们需要配置以下我们的.babelrc,否则不支持这样的语法编译。

{
  "presets": ["env"],
  "plugins": [
    ["transform-react-jsx", {
      "pragma": "Tcaer.h" // 这里改成你想要的,比如Love.you
    }]
  ]
}
// tcaer.js
const Tcaer = {
  h,
};

/**
React.createElement(
  "div",
  { className: "fool" },
  "I'm fool, But I love you"
);
**/

// 通过上面的样本代码,我们可以发现也就3个参数罢了。
function h( tag, attrs, ...children ) {
  return {
    tag,
    attrs,
    children
  }
}

export default Tcaer;

接着我们知道React是通过ReactDOM.render(<Component>, root),挂载我们组装后的DOM。那我们也需要实现一个TcaerDOM.render方法。

再看一遍这张图,我们来实现render方法。

<div onClick={test} a="aaa" 
     className={'sdf'} 
     style={{width: 10, color: 'red'}} 
     id='id1'
>
    I'm fool, But I love you
</div>

这里其实要做两件事,第一件事是要把vnode转换为DOM,第二件事则是处理attrs设置到DOM上。

关于处理vnode,我们只需要获取tag,然后递归渲染children即可。如下

  // 处理Node为text的情况
  if (typeof vnode === 'string') {
    const textNode = document.createTextNode(vnode);
    return container.appendChild(textNode);
  }

  const parentDOM = document.createElement(vnode.tag);

  if (vnode.attrs) {
    Object.keys(vnode.attrs).forEach((item) => {
      // TODO 设置样式,之后实现
      setAttributes()
    });
  }

  vnode.children.length && vnode.children.forEach((child) => render(child, parentDOM));

DOM处理好后,我们来实现以下setAttributes。其实要做的处理很简单,className转换为class,解析onXXX事件类型,处理style对象。剩下的就只是单纯的DOM属性设置了,我们统统setAttribute即可搞定。

于是我们的逻辑代码就类似如下。

function setAttributes(dom, key, value) {
    // 1.转换className
    if (name === 'className') name = 'class';

    // 2.处理事件类型
    if (/on\w+/.test(name)) {
      name = name.toLowerCase();
      dom[name] = value;
      return;
    }

    // 3.处理style对象
    if (name === 'style') {
        Object.keys(value).forEach((item) => {
          const styleValue = value[item];
          dom.style[item] = typeof styleValue === 'number' ? `${styleValue}px` : styleValue;
        });
      return;
    }

    // 4.直接粗暴处理通常的DOM属性
    dom.setAttribute(name, value);
}

把这两个过程实现了之后,我们就可以实现TcaerDom.render了。以下是完整代码

function setAttributes(dom, name, value) {
  if (name === 'className') name = 'class';

  // event
  if (/on\w+/.test(name)) {
    name = name.toLowerCase();
    dom[name] = value;
    return;
  }

  // style
  if (name === 'style') {
    Object.keys(value).forEach((item) => {
      const styleValue = value[item];
      dom.style[item] = typeof styleValue === 'number' ? `${styleValue}px` : styleValue;
    });
    return;
  }

  dom.setAttribute(name, value);
}

function render(vnode) {
  if (typeof vnode === 'string') {
    return document.createTextNode( vnode );
  }

  const parentDOM = document.createElement(vnode.tag);

  if (vnode.attrs) {
    Object.keys(vnode.attrs).forEach((item) => {
      setAttributes(parentDOM, item, vnode.attrs[item]);
    });
  }

  vnode.children.length && vnode.children.forEach((child) => render(child, parentDOM));

  return parentDOM;
}

const TcaerDom = {
  // 没想到把,render还有个callback方法,很多人会遗忘它。这里我们暂时没有实现得与react一样。只会回调一次。
  render(vnode, container, callback) {
    container.innerHTML = ''; // 每次渲染需要清空一次root DOM

    container.appendChild(render(vnode,container));

    callback && callback();
  },
};

export default tcaerDom;

最后我们把它拿去测试一下就好,可以发现效果与React官网上的类似。

function tick() {
   const element = (
    <div>
      <h1>Hello, world!</h1>
      <h2>It is {new Date().toLocaleTimeString()}.</h2>
    </div>
   );
   TcaerDOM.render(element, document.getElementById('root'));
}

setInterval(tick, 1000);

整理后的代码都在这里 >> Tcaer <<

「CI集成」基于Jest Mock API对业务逻辑集成测试

#jest

有时候我们需要不发送请求就能完成前端的业务逻辑测试,而许多的业务逻辑都会需要调用到后端的API接口。那如何能mock我们所需要的data就是一个问题。当我们能有一个良好的测试环境之后,只要保证后端的接口没有问题,那我们就可以保证业务逻辑也没有问题。

所以我们对API的集成测试有以下几个要求

1.不发送请求,返回本地假数据

2.发布前通过CI跑unit test,通过则发布上线

如何实现?

首先一般我们在network部分都会进行封装,假设在project中封装了如下的请求工具

// http tool
export default function http() {
    // some implement
}

既然我们不能发送真实请求,那我们就需要类似能拦截的东西,拦截也可以通过mock代替。于是我们可以通过jest.mock方法来做。

jest.mock

// api/http.js
// real fn
export default function http() {
    console.log('real');
    //...
}
// api/__mocks__/http.js
// fake fn
export default function http() {
    console.log('fake');
}
// some.test.js
jest.mock('../http')
import http from '../http'
http() // 这里log的是fake,而不是real

这个就是jest.mock的作用。

正事

明白了这个后就好办了。项目目录如下:

-- api
    |-- __mockData__
        |-- user.data.js
    |-- __mocks__
        |-- http.js
    |-- __tests__
        |-- user.test.js
    |-- http.js
    |-- profile // profile 业务模块
        |-- user.js // 获取用户信息

而我们的fake文件其实主要做的事情就是根据请求url,method,status等,去读取对应的本地假数据。大致如下。

// ./api/__mocks__/http.js
// 直接读取本地假数据
let statusCode;
export function setStatus(code) {
  statusCode = code;
}

export default function http({ url = "", data = {}, method = "get" }) {
  return new Promise((resolve, reject) => {
    const lastSlash = url.lastIndexOf("/");
    const module = url.substring(lastSlash + 1);
    const mockData = require(`../__mockData__/${module}.data`).default;
    const result = mockData[`${method.toUpperCase()} ${statusCode}`];

    process.nextTick(
      () => (statusCode === 200 ? resolve(result) : reject(result))
    );
  });
}

mockData文件夹则就是放我们的假数据,在这我们可以假设定义如下数据结构,来模拟我们的response

// ./api/__mockData__/user.js
export default {
  'GET 200': {
    code: 0,
    msg: 'ok',
    data: {
      username: '二哲',
      age: 18,
    },
  },
  'POST 200': {
    code: 0,
    msg: 'xxx',
  },
  'GET 400': {
    msg: 'invald params',
    code: -1,
  },
  'GET 401': {},
};

最后看下我们的 unit test 如何写

// ./api/__test__/user.test.js
jest.mock('../http') // jest 会自动搜索目录下的 __mocks__里的文件
import http from '../http';

describe('user api test', () => {
    it("user GET should be 200", async () => {
    setStatus(200);

    const result = await http({
      url,
      method: "get"
    });
    expect(result.data.username).toBe("Kodo");
  });

})

实现了这个有什么用?

假设/user接口返回得数据可能是这样

{
    "username": "二哲",
    "age": 18,
}

而我们前端service层为UI层提供了一个initUserData的方法,initUserData方法里的操作是当age为18,那就要返回19。

所以我们在Jest则可以直接这样测试

// ./api/__test__/user.test.js
jest.mock('../http') // jest 会自动搜索目录下的 __mocks__里的文件
import { setStatus } from './http';
import { initUserData } from '../user'

describe('user api test', () => {
  it("if user age is 18, age should be 19", async () => {
    expect.assertions(1);
    setStatus(200);
    const result = await initUserData();
    // console.log(result);
    expect(result.data.age).toBe(19);
  });

  // test catch
  it("initUserData 400", async () => {
    expect.assertions(1);
    setStatus(400);
    const result = await initUserData();
    expect(result.msg).toBe("invald params");
  });

})

这样我们使用Jest就可以完成对业务逻辑的测试,Unit test在大型项目中非常需要,每当提交一个feature时,可以跑完所有测试,会让你非常有安全感,极大提升了项目的稳定性。

TIP

真正的方法(http),与mock的方法http,文件必须同名,然后放在mocks文件夹下即可。如果不同名使用jest.mock()则会失败。

以上例子都在这 jest-api-test

使用Jest对原生TypeScript项目进行UI测试

#ts#jest

前戏

最近写了一个 wechat-colorpicker 小项目。 主要是为了练习下TS。既然写了一个小库,我就想着顺便学下如何写测试吧,这是一件蛮有意思的事情。

从选型到搭建环境,前前后后用了近2个小时。不得不说一个合格的前端必然是一个合格的配置工程师。再次列举下,这个项目中所需要搭建配置的工具。

  • webpack.config 自动编译ts+css
  • tsconfig.config ts的配置文件
  • tslint.json tslint的配置文件
  • jest.config 配置jest
  • .babelrc jest解析js时还会需要用到的插件
  • circle.yml CircleCI 配置文件

如果大家有什么不懂的,自行百度

Jest + TS 入门

第一个问题,我项目都是TS写的,自然会有 import 这样的语法怎么办?

通过官网的Getting started 我们可以在最下方找到 ts-jest 不难理解,我们需要配的其实就是jest加载到什么样类型的文件,使用什么预处理来处理文件。

transform: {
    '.*\\.(ts)$': '<rootDir>/node_modules/ts-jest/preprocessor.js',
},

transform 就是专门用来匹配各种文件后缀,然后进行对应的预处理,你可以理解为webpack里的loader

我在TS中引入了.css文件咋办?同上

既然有transform,那我们任何文件都可以通过transform进行预处理了。

transform: {
    '^.+\\.js$': '<rootDir>/node_modules/babel-jest',
    '.*\\.(ts)$': '<rootDir>/node_modules/ts-jest/preprocessor.js',
    '^.+\\.(css)$':"<rootDir>/node_modules/jest-css-modules"
},

如果是js文件我通过babel-jest处理,css则使用jest-css-modules。假如没有这些配置,那import了你的库,库里有引入了高特性的js文件,或者css文件就会编译报错。

关于rootDir

在进行技术选型的过程中,我看了最新版本的vue-cli里推荐用哪些框架进行测试,一个是jest,还一个是krama+mocha。 我选择了jest,jest本身是fb出的,对于react非常友好。本身也做了许多环境上的封装切换jsdom环境或者node环境非常方便。我最后选择了这个。

刚刚开始看vue-cli里的jest配置我是拒绝的,第一个最显眼的关键字就是<rootDir>这种像XML得东西。但是你慢慢静下心来去理解就很容易了,其实就是一个basePath的感觉。我们可以看下文档怎么说 rootDir

我的目录如下

--- __test__
    jest.config.js
--- dist
--- node_modules
--- src
//jest.config.js
module.exports = {
      rootDir: path.resolve(__dirname, '../'),
}

<rootDir>其实就代表根目录了

setupFiles 选项

setupFiles是一个AOP的配置,我觉得这个非常好用!因为jest是通过jsdom是模拟了一个document的执行环境,那必然还有很多可能是没有的,比如localStorage,那我们可以通过该配置来设置我们启动前,需要加载什么,比如vue-cli中就是设置了Vue.config.productionTip = false,而我们可以让环境支持localStorage。

setupFiles: ["jest-localstorage-mock"]

不难发现,其实jest的生态还是很丰富的,我本次遇到的问题谷歌几个关键字很快都能解决

UI Test 该怎么写?

test应该是像纯函数一样保证输入输出都是一样的,UI test一方面与Dom耦合,另一方面又用户交互耦合,那具体应该怎么写呢?

思路是:模拟用户操作,再通过Dom进行判断是否渲染正确。

比如这个实例化的测试,我们可以测试是否初始化是否正常,通过jquery来辅助判断

// 实例化测试
import WeChatColorPicker from '../src';
import $ from 'jquery';

export function newInstance() {
  document.body.innerHTML = `<div id="container"></div>`;
  return new WeChatColorPicker(pickerOptions);
}

const baseColorArr = [ ... ];

test('test new instance', () => {
  const instance = newInstance();
  expect(instance.baseComponent.baseColorArr).toEqual(baseColorArr);
  expect($('.wechat-colorpicker')).not.toBeNull();
});

比如这个是点击【基本色】【更多颜色】我们会切换class,那就可以像这样

// tab class切换的交互测试
import $ from 'jquery';
import { newInstance } from './utils';

describe('change tab test', () => {
  newInstance();
  test('use base color', () => {
    $('.wechat-picker-box p i').eq(0).click();
    expect($('.wechat-colorpicker').hasClass('base-color')).toBe(true);
  });

  test('use picker', () => {
    $('.wechat-picker-box p i').eq(1).click();
    expect($('.wechat-colorpicker').hasClass('more-color')).toBe(true);
  });
});

是不是突然就觉得非常简单了?并且是唯一性的,测试用例可靠性也有保障。 之后我们就只需要配合一个CI,每次提交前跑一边我们的测试代码,所有用例测试成功即可pr,否则直接被拒绝。

写完了测试,给我们的jest.config 多加一行配置,来生成我们的测试报告(Jest内置了istanbul)

module.exports = {
  // ...
  collectCoverage: true,
  // ...
}

接着执行下 npm t 查看测试结果如下 wechat-colorpicker-06

  • % Stmts 是语句覆盖率(statement coverage):是否每个语句都执行了?
  • % Branch 分支覆盖率(branch coverage):是否每个if代码块都执行了?
  • % Funcs 函数覆盖率(function coverage):是否每个函数都调用了?
  • % Lines 行覆盖率(line coverage):是否每一行都执行了?

更多测试用例前往 >>> repo-wechat-colorpicker <<< 查看

CricleCI(番外篇)

我们可以通过CI的工具来完善我们的wordflow,在这我选用了CricleCi。进入官网我们直接github登入后,setup 我们的项目。

wechat-colorpicker-02

然后根据它的推荐走,在我们项目根目录添加一个cricle.yml,复制黏贴它的推荐配置即可。

然后我们push测试一下,在这里我写错了我的文件路径,所以构建报错了。

wechat-colorpicker-03

重新修复了问题后,就可以正常运行工作了。

wechat-colorpicker-04

由于本文不是重点介绍CI,这里就不过多展开了,有兴趣的朋友可以自己摸索下

后面真的没有了

至此,你应该对前端UI测试应该大致有一个宏观的了解。

本文没有过多得介绍Jest的用法或者语法,希望可以给不知道如何做测试的朋友们一点方向,自己去尝试找到适合自己项目的才是最好的。

刚刚开始可能很难,无从下手,成本很大。实际上做起来,其实都是慢慢的套路,写熟练了后应该会上瘾,毕竟最后跑完测试的那感觉会让你十分高潮。

对于一个毕竟稳定的项目来说,写测试的确非常有好处,我还有一个v-tap插件,那个小项目就是没有写测试每次有什么问题,修复了bug,我都得人工测试一遍,惨痛的教训。不过现在!我想,我可以释放出我的双手了。

1/5