Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Angular1.x + ES6 开发风格指南 #34

Open
kuitos opened this issue May 26, 2016 · 21 comments
Open

Angular1.x + ES6 开发风格指南 #34

kuitos opened this issue May 26, 2016 · 21 comments

Comments

@kuitos
Copy link
Owner

kuitos commented May 26, 2016

Angular1.x + ES6 开发风格指南

阅读本文之前,请确保自己已经读过民工叔的这篇blog
Angular 1.x和ES6的结合

大概年初开始在我的忽悠下我厂启动了Angular1.x + ES6的切换准备工作,第一个试点项目是公司内部的组件库(另有seed项目)。目前已经实施了三个多月,期间也包括一些其它新开产品的试点。中间也经历的一些痛苦及反复(组件库代码经历过几次调整,现在还在重构ing),总结了一些经验分享给大家。(实际上民工叔的文章中提到了大部分实践指南,我这里尝试作一定整理及补充,包括一些自己的思考及解决方案)

开始之前务必再次明确一件事情,就是我们使用ES6来开发Angular1.x的目的。总结一下大概三点:

  1. 框架的选型在这几年是很头痛的事情,你无法肯定某个框架会是终极解决方案。但是有一点毫无疑问,就是使用ES6来写业务代码是势在必行的。
  2. 我们可以借助ES6的一些新的语法特性,更清晰的划分我们应用的层级及结构。典型的就是module跟class语法。
  3. 同样的,在ES6语法的帮助下,我们能较容易的将数据层跟业务模型层实现成框架无关的,这能有效的提升整个应用的可移植性及演化能力。从另一方面讲,数据层跟业务模型能脱离view独立测试,是一个纯数据驱动的web应用应该具备的基本素质。

其中第1点是技术投资需要,第2、3点是架构需要。

我们先来看看要达到这些要求,具体要如何一步步实现。

Module

在ES6 module的帮助下,ng的模块机制就变成了纯粹的迎合框架的语法了。
实践准则就是:

  1. 各业务层及数据层代码理想状态下应该看不出框架痕迹。
  2. ng module最后作为一个壳子将所有的业务逻辑包装进框架内。
  3. 每个ng module export出module name以便module之间相互引用。

example:

// moduleA.js 
import angular from 'angular';
import Controller from './Controller';

export default angular.module('moduleA', [])
    .controller('AppController', Controller)
    .name;

// moduleB.js 需要依赖module A
import angular from 'angular';
import moduleA from './moduleA';

angular.module('moduleB', [moduleA]);

通过这种方式,无论被依赖的模块的模块名怎么改变都不会对其他模块造成影响。

Best Practice
index.js作为框架语法包装器生成angular module外壳,同时将module.name export出去。对于整个系统而言,理想状态下只有index.js中可以出现框架语法,其他地方应该是看不到框架痕迹的。

Controller

ng1.2版本开始提供了一个controllerAs语法,自此Controller终于能变成一个纯净的ViewModel(视图模型)了,而不是像之前一样混入过多的$scope痕迹(供angular框架使用)。
example

<div ng-controller="AppCtrl as app">
    <div ng-bind="app.name"></div>
    <button ng-click="app.getName">get app name</button>
</div>
// Controller AppCtrl.js
export default class AppCtrl {
    constructor() { 
        this.name = 'angular&es6';
    }

    getName() {
        return this.name;
    }
}
// module
import AppCtrl from './AppCtrl';

export default angular.module('app', [])
    .controller('AppCtrl', AppCtrl)
    .name;

这种方式写controller等同于ES5中这样去写:

function AppCtrl() {
    this.name = 'angular&es6';
}

AppCtrl.prototype.getName = function() {
    return this.name;
};

....
.controller('AppCtrl', AppCtrl)

不过ES6的class语法糖会让整个过程更自然,再加上ES6 Module提供的模块化机制,业务逻辑会变得更清晰独立。

Best Practice
在所有地方使用controllerAs语法,保证ViewModel(Controller)的纯净。

Component(Directive)

以datepicker组件为例

// 目录结构
+ date-picker
    - _date-picker.scss
    - date-picker.tpl.html
    - DatePickerCtrl.js
    - index.js
// DatePickerCtrl.js
export default class DatePickerCtrl {

    $onInit() {
        this.date = `${this.year}-${this.month}`;
    }

    getMonth() {
        ...
    }

    getYear() {
        ...
    }
}

注意,这里我们先写的controller而不是指令的link/compile方法,原因在于一个数据驱动的组件体系下,我们应该尽量减少DOM操作,因此理想状态下,组件是不需要link或compile方法的,而且controller在语义上更贴合mvvm架构。

// index.js
import template from './date-picker.tpl.html';
import controller from './DatePickerCtrl';

const ddo = {
    restrict: 'E',
    template,
    controller,
    controllerAs: '$ctrl',
    bindToContrller: {
        year: '=',
        month: '='
    }
};

export default angular.module('components.datePicker', [])
    .directive('datePicker', ddo)
    .name;

**注意,这里跟民工叔的做法有点不一样。**叔叔的做法是把指令做成class然后在index.js中import并初始化,like this:

// Directive.js
export default class Directive {
    constructor() {
    }

    getXXX() {
    }
}

// index.js
import Directive from './Directive';

export default angular.module('xxx', [])
    .directive('directive', () => new Directive())
    .name;

但是我的意见是,整个系统设计中index.js作为angular的包装器使得代码变成框架可识别的,换句话说就是只有index.js中是可以出现框架的影子的,其他地方都应该是框架无关的使用原生代码编写的业务模型。

1.5之后提供了一个新的语法moduleInstance.component,它是moduleInstance.directive的高级封装版,提供了更语义更简洁的语法,同时也是为了顺应基于组件的应用架构的趋势(之前也能做只是语法稍啰嗦且官方没有给出best practice导向)。比如上面的例子用component语法重写的话:

// index.js
import template from './date-picker.tpl.html';
import controller from './DatePickerCtrl';

const ddo = {
    template,
    controller,
    bindings: {
        year: '=',
        month: '='
    }
};

export default angular.module('components.datePicker', [])
    .component('datePicker', ddo)
    .name;

component语义更简洁明了,比如 bindToController -> bindings的变化,而且默认controllerAs = '$ctrl'。还有一个重要的差异点就是,component语法只能定义自定义标签,不能定义增强属性,而且component定义的组件都是isolated scope。

另外angular1.5版本有一个大招就是,它给组件定义了相对完整的生命周期钩子(虽然之前我们能用其他的一些手段来模拟init到destroy的钩子,但是实现的方式框架痕迹太重,后面会详细讲到)!而且提供了单向数据流实现方式!
example

// DirectiveController.js
export class DirectiveController {

    $onInit() {
    }

    $onChanges(changesObj) {
    }

    $onDestroy() {
    }

    $postLink() {
    }
}

// index.js
import template from './date-picker.tpl.html';
import controller from './DatePickerCtrl';

const ddo = {
    template,
    controller,
    bindings: {
        year: '<',
        month: '<'
    }
};

export default angular.module('components.datePicker', [])
    .component('datePicker', ddo)
    .name;

component相关详细看这里:angular component guide

从angular的这些api变化来看,ng的开发团队正在越来越多的吸取了一些其他社区的思路,这也从侧面上印证了前端框架正在趋于同质化的事实(至少在同类型问题领域,方案趋于同质)。顺带帮vue打个广告,不论是进化速度还是方案落地速度,vue都已经赶超angular了。推荐大家都去关注下vue。

Best Practice
在场景符合(只要你的指令是可以作为自定义标签存在就算符合)的情况下都应该用component语法,在$onInit回调中做初始化处理(而不是constructor,原因见下文),$onDestroy中作组件销毁回调。没有link方法,只有组件Controller(ViewModel).这样能帮助你从component-base的应用架构方向去思考问题。

Deprecation warning: although bindings for non-ES6 class controllers are currently bound to this before the controller constructor is called, this use is now deprecated. Please place initialization code that relies upon bindings inside a $onInit method on the controller, instead.

Service、Filter

自定义服务 provider、service、factory、constant、value

angular1.x中有五种不同类型的服务定义方式,但是如果我们以功能归类,大概可以归出两种类型:

  1. 工具类/工具方法
  2. 一些应用级别的常量或存储单元

angular原本设计service的目的是提供一个应用级别的共享单元,单例且私有,也就是只能在框架内部使用(通过依赖注入)。在ES5的无模块化系统下,这是一个很好的设计,但是它的问题也同样明显:

  1. 随着系统代码量的增长,出现服务重名的几率会越来越大。
  2. 查找一个服务的定义代码比较困难,尤其是一个多人开发的集成系统(当然你也可以把原因归咎于 编辑器/IDE 不够强大)。

很显然,ES6 Module并不会出现这些问题。举例说明,我们之前使用一个服务是这样的:

index.js

import angular from 'angular';
import Service from './Service';
import Controller from './Controller';

export default angular.module('services', [])
    .service('service', Service)
    .controller('controller', Controller)
    .name;

Service.js

export default class Service {
    getName() {
        return 'kuitos';
    }
}

Controller.js 这里使用了工具库angular-es-utils来简化ES6中使用依赖注入的方式。

import {Inject} from 'angular-es-utils/decorators';

@Inject('service')
export default class Controller {

    getUserName() {
        return this._service.getName();
    }
}

假如哪天在调用controller.getUserName()时报错了,而且错误出在service.getName方法,那么查错的方式是?我是只能全局搜了不知道你们有没有更好的办法。。。

如果我们使用依赖注入,直接基于ES6 Module来做,改造一下会变成这样:

Service.js

export default {

    getName() {
        return 'kuitos';
    }
}

Controller.js

import Service from './Service';

export default class Controller {

    getUserName() {
        return Service.getName();
    }
}

这样定位问题是不是容易很多!!
从这个案例上来看,我们能完美模拟基础的 Service、Factory 了,那么还有Provider、Constant、Value呢?
Provider跟Service、Factory差异在于Provider在ng启动阶段可配置,脱离ng使用ES6 Module的方式,服务之间其实没什么区别。。。:

Provider.js

let apiPrefix = '';

export function setPrefix(prefix) {
    apiPrefix = prefix;
}

export function genResource(url) {
    return resource(apiPrefix + url);
}

应用入口时配置:
app.js

import {setPrefix} from './Provider';

setPrefix('/rest/1.0');

Contant跟Value呢?其实如果我们忘掉angular,它们倆完全没区别:

Constant.js

export const VERSION = '1.0.0';
使用ng内置服务

上面我们提到我们所有的服务其实都可以脱离angular来写以消除依赖注入,但是有一种状况比较难搞,就是假如我们自定义的工具方法中需要使用到angular的built-in服务怎么办?要获取ng内置服务我们就绕不开依赖注入。但是好在angular有一个核心服务$injector,通过它我们可以获取任何应用内的服务(Service、Factory、Value、Constant)。但是$injector也是ng内置的服务啊,我们如何避开依赖注入获取它?我封装了个小工具可以做这个事:

import injector from 'angular-es-utils/injector';

export default {

    getUserName() {
        return injector.get('$http').get('/users/kuitos');
    }
};

这样做确实可以但总觉得不够优雅,不过好在大部分场景下我们需要用到built-in service的场景比较少,而且对于$http这类基础服务,调用者不应该直接去用,而是提供一个更高级的封装出去,对调用着而言内部使用的技术是透明,可以是$http也可以是fetch或者whatever。

import injector from 'angular-es-utils/injector';
import {FetchHttp} from 'es6-http-utils';

export const HttpClient {

    get(url) {
        return injector.get('$http').get(url);
    }

    save(url, payload) {
        return FetchHttp.post(url, payload);
    }
}

// Controller.js
import {HttpClient} from './HttpClient';
class Controller {
    saveUser(user) {
        HttpClient.save('/users', user);
    }
}

通过这些手段,对于业务代码而言基本上是看不到依赖注入的影子的。

Filter

angular中filter做的事情有两类:过滤和格式化。归结起来它做的就是一种数据变换的工作。filter的问题不仅仅在于DI的弊端,还有更多其他的问题。vue2中甚至取消了filter的设计,参见[Suggestion]Vue 2.0 - Bring back filters please。其中有一点我特别认可:过度使用filter会让你的代码在不自知的情况下走向混乱的状态。我们可以自己去写一系列的transformer(或者使用underscore之类的工具)来做数据处理,并在vm中显式的调用它。

import {dateFormatter} from './transformers';

export default class Controller {

    constructor() {

        this.data = [1,2,3,4];

        this.currency = this.data
            .filter(v => v < 4)
            .map(v => '$' + v);

        this.date = Date.now();
        this.today = dateFormatter(this.date);
    }
}

Best Practice
理想状态下,Service & Filter的语法在一个不需要跟其他系统共享代码单元的业务系统里是完全可以抹除掉的,我们完全通过ES6 Module来代替依赖注入。同时,对于一些基础服务,如$http$q之类的,我们最好能提供更上层的封装,确保业务代码不会直接接触到built-in service。

一步步淡化框架概念

如果想将业务模型彻底从框架中抽离出来,下面这几件事情是必须解决的。

依赖注入

前面提到过,通过一系列手段我们可以最大程度消除依赖注入。但是总有那些edge case,比如我们要用$stateParams或者服务来自路由配置中注入的local service。我写了一个工具可以帮助我们更舒服的应对这类边缘案例 Link to Controller

依赖属性计算

对于需要监控属性变化的场景,之前我们都是用$scope.$watch,但是这又跟框架耦合了。民工叔的文章里提供了一个基于accessor的写法:

class Controller {

    get fullName() {
        return `${this.firstName} ${this.lastName}`;
    }
}

template

<input type="text" ng-model="$ctrl.firstName">
<input type="text" ng-model="$ctrl.lastName">

<span ng-bind="$ctrl.fullName"></span>

这样当firstName/lastName发生变化时,fullName也会相应的改变。基于的原理是Object.defineProperty。但是民工叔也指出了一个由于某种不知名的原因导致绑定失效,不得不用$watch的场景。这个时候$onChanges就派上用场了。但是$onChanges回调有个限制就是,它的变更检测时基于reference的而不是值的内容的,也就是说绑定primitive没问题,但是绑定引用类型(Object/Array等)那么内容的变化并不会被捕获到,例如:

class Controller {
    $onChanges(objs) {
        this.userCount = objs.users.length;
    }
}

const ddo = {
    controller: Controller,
    template: '<span ng-bind="$ctrl.listTitle"></span><span ng-bind="$ctrl.userCount"></span>'
    bindings: {
        title: '<',
        users: '<'
    }
};

angular.module('component', [])
    .component('userList', ddo);

template

<div ng-controller="ctrl as app">
    <user-list title="app.title" users="app.users" ng-click="app.change()"></user-list>
</div>
class Controller {
    contructor() {
        this.title = 'hhhh';
        this.users = [];
    }

    change() {
        this.users.push('s');
    }
}

angular.module('app', [])
    .controller('ctrl', Controller);

点击user-list组件时,userCount值并不会变化,因为$onChanges并没有被触发。对于这种情况呢,你可能需要引入immutable方案了。。。怎么感觉事情越来越复杂了。。。

组件生命周期

组件新增的四个生命周期对于我而言可以说是最重大的变化了。虽然之前我们也能通过一些手段来模拟生命周期:比如用compile模拟init,postLink模拟didMounted,$scope.$on('$destroy')模拟unmounted。
但是它们最大的问题就是身上携带了太多框架的气息,并不能服务文明剥离框架的初衷。具体做法不赘述了,看上面组件部分的介绍Link To Component.

事件通知

以前我们在ng中使用事件模型有 $broadcast$emit$on这几个api用,现在没了它们我们要怎么玩?
我的建议是,我们只在必要的场景使用事件机制,因为事件滥用和不及时的卸载很容易造成事件爆炸的情况发生。必要的场景就是,当我们需要在兄弟节点、或依赖关系不大的组件间触发式通信时,我们可以使用自制的 事件总线/中介者 来帮我们完成(可以使用我的这个工具库angular-es-utils/EventBus)。在非必要的场景下,我们应该尽量使用inline-event的方式来达成通信目标:

const ddo = {
    template: '<button type="button" ng-click="$ctrl.change('kuitos')">click me</button>',
    controller: class {
        click(userName) {
            this.onClick({userName});
        }   
    },  
    bindings: {
        onClick: '&'
    }
};

angular.module('app', [])
    .component('user', ddp);

useage

<user on-click="logUserName(userName)"></user>

总结

理想状态下,对于一个业务系统而言,会用到angular语法只有 angular.controllerangular.component angular.directiveangular.config这几种。其他地方我们都可以实现成框架无关的。

对于web app架构而言,angular/vue/react 等组件框架/库 提供的只是 模板语法&胶水语法(其中胶水语法指的是框架/库 定义组件/控制器 的语法),剥离这两个外壳,我们的业务模型及数据模型应该是可以脱离框架运作的。古者有云,衡量一个完美的MV*架构的标准就是,在V随意变化的情况下,你的M*是可以不改一行代码的情况下就完成迁移的。

在MV_架构中,V层是最薄且最易变的,但是M_理应是 稳定且纯净的。虽然要做到一行代码不改实现框架的迁移是不可能的(视图层&胶水语法的修改不可避免),但是我们可以尽量将最重的 M* 做成框架无关,这样做上层的迁移时剩下的就是一些语法替换的工作了,而且对V层的改变也是代价最小的。

事实上我认为一个真正可伸缩的系统架构都应该是这样一个思路:勿论是 MV* 还是 Flux/Redux or whatever,确保下层 业务模型/数据模型 的纯净都是有必要的,这样才能提供上层随意变化的可能,任何模式下的应用开发,都应该具备这样的一个能力。

@hstarorg
Copy link

非常棒的思路呢,我们在使用中只是使用了部分ES6的功能来简化代码,连import之类的都没用上。

另外,我们当前也处在比较纠结的一个阶段,暂定目标是先搞一个基于ng2的东西出来,之前做的东西再慢慢迁移。

请问,你们针对angular2怎么看,有打算升级么?

@kuitos
Copy link
Owner Author

kuitos commented May 28, 2016

@hstarorg 我们暂时是没打算升级的,主要考虑的还是一个成本问题,毕竟ng2的新的语法跟typescript不是每个人都能快速掌握的。
原本的初衷确实是为了后面可能的框架迁移方便,但是现在我觉得,把业务模型从框架中抽离出来是任何一种模式下都应该首先考虑的,我们需要确保架构的层次清晰,只是它顺便让上层迁移变的容易了而已。

ng2带来的一些东西还是很值得研究的,比如rx跟zone,不过对于我们公司这样一个团队(angular上普遍比较熟)直接切ng2成本还是有点高,相反我可能更倾向于vue2。

@hstarorg
Copy link

@kuitos 我们也需要升级成本(学习成本,修改成本)的问题,但是又没啥好的办法。现在连使用ng1都还是老旧的语法,这个团队多了也很难处理。

我仅仅了解vue1,比较有特色的双向绑定,暂时对vue2不了解。看样子得抽时间看看了。

抛开基础框架不说,ng2比ng1简单,但是搭框架反而更复杂了。

@xiaoyu2er
Copy link

很赞!

个人也是觉得民工叔在定义 directive 的时候采用 class 的方式不如博主所用的对象字面量的方式来的直观

请教一个问题 @kuitos , 虽然我也同意博主所说的尽可能把 model 做的框架无关, 利于迁移.

但是如果很长一段时间框架基本不会迁移, 是不是还是直接采用 DI 的形式好一些.

毕竟博主所呈现出来的代码书写风格已经很不像 angular 了. 不知道这个转变对于团队来说成本有多大?

@kuitos
Copy link
Owner Author

kuitos commented Jul 3, 2016

关于DI的问题,我文中这样描述的

  1. 随着系统代码量的增长,出现服务重名的几率会越来越大。
  2. 查找一个服务的定义代码比较困难,尤其是一个多人开发的集成系统(当然你也可以把原因归咎于 编辑器/IDE 不够强大)。

假设这样一段代码

DI(使用自制的Inject decorator):

@Inject('ServiceA', 'ServiceB')
class Controller{
    getName(){
        return this._ServiceA + this._ServiceB;
    }
}

module import

import ServiceA from './ServiceA';
import ServiceB from './ServiceB';

class Controller{
    getName(){
        return this._ServiceA + this._ServiceB;
    }
}

哪种维护起来成本更高显而易见吧,DI的方式你几乎不可能知道ServiceA跟ServiceB在哪里定义的。

你说到的迁移,我一开始确实也是从这一点出发的,给他设计了一个可能迁移上层框架的‘伪需求’。但是后来思想有所转变,就是不论框架是否可能迁移,M/VM层做成框架无关都是架构上确实存在的需求。

古者有云,衡量一个完美的MV*架构的标准就是,在V随意变化的情况下,你的M*是可以不改一行代码的情况下就完成迁移的。

各层解耦独立是核心诉求,便于架构迁移只是这件事带来的‘副作用’。
成本的话主要还是在要理解MVVM架构的思路以及需要造一些用于抹平ng特性的基础上层组件(如Inject decorator),但这些都是一次性的。相反的它带来的收益却都是持续的。

另外,这年头还不用ES6写代码,就真的太土了啊!😂

@xiaoyu2er
Copy link

好的! 让我先消化消化..... 过段时间我再来向您请教!

@xufei
Copy link

xufei commented Jul 4, 2016

@xiaoyu2er 团队切换代价这个事情没有那么大,总的来说,就是把es5的controller, service之类换成了es6的class形势,其他地方都是一些配置性的变更

@hooper-hc
Copy link

兄弟劝你一句,别升级瞎整了.angular2 和 an1是断层发展的, angular1 谷歌是不是维护都难说. 你还整合 es6 . 给自己找事. 想用 es6 试着切到 react 或者 vue. react 最佳.

@xiaoyu2er
Copy link

@hcforbaidu 不是每个项目都可以从零开始的 国内外用 ng1 的公司还是很多的
用 ES6 webpack 等只不过是希望提高开发和维护效率

@xiaoyu2er
Copy link

@xufei !! 恩 自己还在摸索一套规范 好让同事可以比较不那么纠结的过渡过来, 大体上是 参考了博主和民工叔的风格. 谢谢!

@hooper-hc
Copy link

@xiaoyu2er 我的个人看法. 因为之前确实搭建了一套基于 angular1的二次开发方案,用于快速开发. an1确实有他方便的地方. 但是后来遇到太多太多坑. an1的思想和做法目前来看值得商榷的地方太多太多. 谷歌也明白这个问题,所以an2和1断层发展. 所以对 ng1的项目而言. 我们的思路是维护可用就好.对 an1的项目空费心思划不来.仅是个人看法.

@kuitos
Copy link
Owner Author

kuitos commented Jul 4, 2016

@xiaoyu2er 前阵子看到了一个post:https://github.com/toddmotto/angular-styleguide 基本想法是一样的,不过这个作者写的要细节多了😂 虽然有些地方我不是很同意(DI/Service/Filter等几块),不过还是推荐你看看

@xiaoyu2er
Copy link

@kuitos 好的! 稍微看了一下 看完交流!

@xiaoyu2er
Copy link

@kuitos 先问个问题, template 你是放在 bundle 里还是?

@kuitos
Copy link
Owner Author

kuitos commented Jul 5, 2016

@xiaoyu2er 组件模板 import 过来变成字符串模板一起打包,业务模板(通常是路由,可以理解成container) import 过来是文件路径,跟 templateUrl 关联。基于 webpack loader

@seven4x
Copy link

seven4x commented Mar 14, 2018

大神,ng-include 用ES6 该如何实现呢?

@kuitos
Copy link
Owner Author

kuitos commented Mar 15, 2018

import includingTpl from './include.html';

class Controller {
    includingTpl = includingTpl
}
<ng-include src="$ctrl.includingTpl"></ng-include>

@seven4x 是这个意思么?

@seven4x
Copy link

seven4x commented Mar 15, 2018

@kuitos 可以,可以 这操作 666 不愧是大神
这样会把 $ctrl.includingTpl 当做URL的一部分发个请求 src的值不能是HTML代码 得换个思路了

@seven4x
Copy link

seven4x commented Mar 15, 2018

还有一个疑惑请教大神,在使用ui-router的时候,如何不通过$scope访问父节点的数据呢?
现在访问父节点数据是这样写的
$scope.$parent.data

@xianghongai
Copy link

不赞同部分思想,选择了框架就要用好框架,而不是在框架的基础上再大量的去框架,如DI部分。

赞同部分思想,如组件中 index.js 作为框架语法包装器;业务逻辑采用原生ES;数据层、业务模型能脱离 View 独立测试。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

8 participants