2016年12月8日星期四

Angular 组件交流方式

以下的测试例子都可以在 github 找到,但是最近好像不太稳定。
其实 ng2 在这方面做得挺好的,用起来也很简单,所以看完基本就可以动手写一写。强大并不止是这一方面,在写这些的过程中,通过一些配置,让开发很纯粹,有时间再录一个新手入门的开发教程。

(1) 父组件向子组件流入数据

这种方式是最简单的,在 ng2 中处理得非常完美,通过在子组件中标记 @Input() 输入接口的方式进行接收父组件的值,我下面的 demo 主要分了几种场景,尽可能的多覆盖不同情况吧。

基本上例子中覆盖了常见的情况:

  • 直接传入一个字符串的情况,不需要绑定父组件的一个变量
  • 绑定父组件变量的情况,然后可以在父组件中不断修改
  • 输入别名的情况,可以在子组件中对输入的变量名进行重新设置
  • ngOnChanges() 在子组件中监听属性的修改
  • 特殊情况下,我们需要对父组件传入的数据进行过滤
  • @ViewChild() 注解的跨多层子组件的观察方式

说了这么多,来看一下实际的代码吧。


// Parent component
import { Component, OnInit } from '@angular/core';
    
@Component({
    selector: 'app-parent',
    templateUrl: './parent.component.html',
    styleUrls: ['./parent.component.css']
})
export class ParentComponent implements OnInit {
    
    baby: string = '你的名字';
    
    constructor() { }
    
    ngOnInit() {
    }
    
}


// Parent html
<h3>请输入 Baby 的名字:</h3>
<input [(ngModel)]="baby" type="text"> 
<app-child babyName="hello" [inputBabyName]="baby" aliasBabyName="我是别名"></app-child>
    

// Child component
import { Component, OnInit, Input, SimpleChange } from '@angular/core';

@Component({
    selector: 'app-child',
    templateUrl: './child.component.html',
    styleUrls: ['./child.component.css']
})
export class ChildComponent implements OnInit {
    
    @Input() babyName: string;
    @Input() inputBabyName: string;
    @Input('aliasBabyName') aliasName: string;
    
    changes: string;
    
    constructor() { }
    
    ngOnInit() {
    }
    
    ngOnChanges(changes: SimpleChange) {
        this.changes = JSON.stringify(changes);
    }
}


// Child html
<h3>我是子组件的属性(babyName) => {{babyName}}</h3>
<h3 style="color:red;">我是跟父组件来:{{inputBabyName}}</h3>
<h3>我是 aliasBabyName => aliasName:{{aliasName}}</h3>

那么我需要过滤一下值要怎么弄呢?

这样我们就可以用到 setter 和 getter 的特性来做,具体如下:


// Child component
_filterName: string = '';
    
@Input()
set filterName(n: string) {
    this._filterName = n + 'wowo~~~';
}
    
get filterName() {
    return this._filterName;
}


// Parent html
<app-child [filterName]="babyName"></app-child>

这个其实也是用 @Input() 这个注解来做的,有点类似 computed 的概念吧,但是这样做对于习惯 Java 的小伙伴是很友好的,其实通过一些权限的设置,还能够更加的强大。

@ViewChild() 的方式

这种方式我觉得更多的是,我的沟通逻辑存在于 TS 中的时候就很实用。并且是描述性的定义方式,所以逻辑也是清晰的。


// Parent component
// 方式1,定义了 `#` 的钩子也是可以引用的
@ViewChild('child') cc: ChildComponent;
    
// 直接观察某一个子组件
@ViewChild(ChildComponent)
cc_other: ChildComponent;
    
// 调用的时候
this.cc.name = '变身啦!超级赛亚人';
this.cc_other.name = '变身啦!超级赛亚人 4';

可以思考一下,是否任何形式的父组件流入子组件的方式,都可以触发 ngOnChanges() 方法。

(2) 子组件向父组件通信

从软件的结构上来讲,是上层抽象对底层的具体实现是隐藏的,所以具体层的东西最好尽可能少的知道抽象层的事情,也许表达方式不一样,但是这样的话封闭性会好很多,更多的暴露是以某一个权限开放的接口形式。但是通信是很复杂的东西,就好像人与人之间的联系是一样的。好吧,我们来具体说一下子组件怎么访问父组件。主要通过的方式是:

  • 在子组件定义一个 @Output()EventEmitter<T> 对象,这个对象可以是 Subject 的形式存在,也就是可以使用 RxJS 的思想来做,其中 T 范型表示定义需要传入的数据具体类型。
  • 父组件中定义一个自己的函数来修改自身的信息,或者再传入其他子组件使用。

// Parent component
import { Component, OnInit } from '@angular/core';
    
@Component({
    selector: 'app-parent',
    templateUrl: './parent.component.html',
    styleUrls: ['./parent.component.css']
})
export class ParentComponent implements OnInit {
    
    babyName: string;
    
    constructor() { }
    
    ngOnInit() {
    this.babyName = '小撸一号';
    }
    
    changeBabyName(newBabyName) {
        this.babyName = newBabyName;
    }
 
}


// Parent html
<h3>BabyName:{{babyName}}</h3>
<app-child (changeBabyName)="changeBabyName($event)"></app-child>


import { Component, OnInit, Output, EventEmitter } from '@angular/core';
    
@Component({
    selector: 'app-child',
    templateUrl: './child.component.html',
    styleUrls: ['./child.component.css']
})
export class ChildComponent implements OnInit {
    
    @Output()
    changeBabyName: EventEmitter<string> = new EventEmitter<string>();
    
    rhashcode = /\d\.\d{4}/;
    
    constructor() { }
    
    ngOnInit() {
    }
    
    getNewBabyName(e) {
        let newName = this.makeHashCode('小撸新号');
        this.changeBabyName.next(newName);
    }
    
    /* UUID http://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid-in-javascript */
    makeHashCode(prefix) {
        prefix = prefix || '60sky';
        return String(Math.random() + Math.random()).replace(this.rhashcode, prefix);
    }
}


<button (click)="getNewBabyName($event)">我要改我自己的名字</button>

其中需要注意的是父组件中方法注入的 $event 对象,这个对象在这里注入的是子组件传入的值,所以在父组件中就可以直接使用了,我这里定义了 string 类型的数据,所以传入后定义接口的参数类型也是相对应的。

(3) 无关组件的通信

ng2 在无关组件的处理上,真的处理得很干脆,给你一个钩子,你用吧!就是这种简单的思路。这里我只介绍部分,因为官方文档有更加详细的介绍,不然我这篇文章就写得太长了~因为方式有很多种,发挥小聪明就能发现很多。

  • 事件回调传来传去的方式
  • Service 的注入
  • # 钩子的方式

这里介绍的是一个 # 钩子的方式来做,直接来代码吧,很方便的。
其中,需要注意的是作用域的隔离,子组件可以很好的隔离作用域。


// Parent component
import { Component, OnInit } from '@angular/core';
    
@Component({
    selector: 'app-parent',
    templateUrl: './parent.component.html',
    styleUrls: ['./parent.component.css']
})
export class ParentComponent implements OnInit {
    
    babyName: string = '小撸一号';
    
    constructor() { }
    
    ngOnInit() {
    }
    
}


// Parent html
<input [(ngModel)]="babyName" type="text">
    
<app-child #child [childName]="babyName"></app-child>
<app-otherChild helloBaby="child.childName"></app-otherChild>


// Child component
import { Component, OnInit, Input } from '@angular/core';
    
@Component({
    selector: 'app-child',
    templateUrl: './child.component.html',
    styleUrls: ['./child.component.css']
})
export class ChildComponent implements OnInit {
    
    @Input() childName: string;
    
    constructor() { }
    
    ngOnInit() {
    }
    
}


<h3 style="color:red;">Child:{{childName}}</h3>


// OtherChild component
import { Component, OnInit, Input } from '@angular/core';
    
@Component({
    selector: 'app-otherChild',
    templateUrl: './otherChild.component.html',
    styleUrls: ['./otherChild.component.css']
})
export class OtherChildComponent implements OnInit {
    
    @Input() helloBaby: string;
    
    constructor() { }
    
    ngOnInit() {
    }
    
    changeChildName(e) {
        this.helloBaby = '小撸新号';
    }
}


// OtherChild html
<h3 style="color:blue;">otherChild:{{helloBaby}}</h3>
<button (click)="changeChildName($event)">我来统一修改一下</button>

其实还有一些方式和特殊场景下的处理,所以总体来说,ng2 在这方面是不错的~

2016年11月30日星期三

二叉树简单两三下

前言

树和图一样,是常用的数据结构模型,但是我的理解树是图的一个用于更具体的数据结构。今天温习的是树中比较简单、常用的二叉树。因为一个简单固定的结构更有利于查询,所以有了二叉查找树的概念。

简单介绍下

研究依然以点-线模型的分析理解,不过树是从一个方向顺势的分支结构。这里可以是自上向下,也可以自下向上。

树的常见术语有几个:

  • 根节点
  • 子节点
  • 叶子节点
  • 子树
  • 路径
  • 键(这里是抽象主要的数据)

二叉树:

  • 子节点不超过两个
  • 左节点
  • 右节点

如果具备查找特性:

较小值在左,较大值在右

这里准备一组值来构建树:

[ 23, 45, 16, 37, 3, 99, 22 ]

然后我需要构建的树的成品是:

树

具体实现

  1. 首先需要构造一个节点对象,来表示节点这一个描述元素
class Node {
    constructor (data, left, right) {
        this.data = data;
        this.left = left;
        this.right = right;
    }

    getData () {
        return this.data;
    }

    output() {
        console.log(this.data);
    }
}

主要包含:

  • data: 节点的键
  • left: 左节点
  • right: 右节点
  • getData: 获取键
  • output: 辅助输出函数

  1. 树的对象以及树的操作
class Tree {
    constructor () {
        // 根节点默认是 null
        this.root = null;
    }

    // 插入节点
    insert (data) {
        const node = new Node(data, null, null);

        if(this.root === null) {
            this.root = node;
        } else {
            let current = this.root;
            let parent = null;

            while(1) {
                parent = current;
                if(data < current.data) {

                    current = current.left;
                    if(current === null) {
                        parent.left = node;
                        break;
                    }
                } else {

                    current = current.right;
                    if(current === null) {
                        parent.right = node;
                        break;
                    }
                }
            }
        }

        return this;
    }

    // 中序遍历
    ascOrder (node) {
        if(node !== null) {
            this.ascOrder(node.left);
            node.output();
            this.ascOrder(node.right);
        }
    }

    // 先序遍历
    rootOrder (node) {
        if(node !== null) {
            node.output();
            this.rootOrder(node.left);
            this.rootOrder(node.right);
        }
    }

    // 后序遍历
    leafOrder (node) {
        if(node !== null) {
            this.leafOrder(node.left);
            this.leafOrder(node.right);
            node.output();
        }
    }

    // 执行路径的 action: left or right
    action (path) {
        let node = this.root;

        while(node[path] !== null) {
            node = node[path];
        }

        return node;
    }

    // 最小值
    min () {
        return this.action('left');
    }

    // 最大值
    max () {
        return this.action('right');
    }

    // 查找固定值
    find (data) {
        let node = this.root;

        while(node !== null) {
            if(data === node.data) {
                return node;
            } else if(data < node.data) {
                node = node.left;
            } else {
                node = node.right;
            }
        }

        return node;
    }
}

最后我在 Node 环境下测试,所以导出一下 Tree 类:

module.exports = Tree;

对于每一种排序后的结果是不一样的,可以用图形来表示一下:

中序遍历的过程:

中

先序遍历的过程:

14805224572732

后序遍历的过程:

14805226761054

其实看图是最直观的,其实算法这玩意最好的就是能够体会思想,然后根据实际的场景进行映射建立数据结构模型,以最优或更平衡的去解决问题。

测试代码如下:

const Tree = require('./binTree');

const log = s => console.log(s);

const tree = new Tree();
[23, 45, 16, 37, 3, 99, 22].forEach(n => tree.insert(n));

log('中-排序:');
tree.ascOrder(tree.root);

log('先-排序:');
tree.rootOrder(tree.root);

log('后-排序:');
tree.leafOrder(tree.root);

console.log('=====================');

log('最小值:');
log( tree.min() );

log('最大值:');
log( tree.max() );

log('查找 3:');
log( tree.find(3) );

log('查找 11:');
log( tree.find(11) );

终端跑的结果如下:

测试结果

无厘头 Graph

前言

今天晚上无意翻到一个图的文章,查了一下感觉网上实现和其他都好复杂,所以自己按理解搞了一下,不知道是我实现是不是错了...感觉还好~进入正题,先还是来点理论知识,不过大多是自己的想法,不一定都对,可以纠正。

图是一种数学模型,和数学挂勾一般都会比较复杂,所以形象的理解成最简单的模型,点-线 模型。其实最简单的是 1 个点的模型,涉及 2 个点还好,3 个点过后模型就会作出相应的改变。

这里用简单的语言来说图中的二元关系,不过还是先假设一点数学符号:

G => 表示所有的顶点集合

V => 表示顶点

E => 表示边,抽象意义上是无向边

那么用数学来表示就是:G=<V, E>

其实根本不用理解数学的模型,我这里理解是只需要知道这是一个点-线模型就可以了。

如何表示图呢?

这里有两种表示方法:表和矩阵,其间都是邻接关系

这里我有一个测试图,在网上弄的,虽然是无向图,其实在我们代码中,肯定是有向的,是入口的问题:

图

图的结构确定过后,就可以做出表的结构了,这里我没有用方向,因为我理解的图是一个不能简单表示的,理解成坐标系更好理解一点。分为:x, y 轴的方式。其中,x0 表示开始,后面表示相邻的点,按顺时针排列(不一定按这个顺序)。

表

代码中的图

在代码中表示,没有图形那么直观,所以需要映射成代码模型,这里简单实现一下,但是不具备很多功能。

假设:

class G => 一个图的类,包括图的定义和常用遍历方法

this.V => 表示点集合的个数,但是这里我舍弃了 0 的位置

this.T => 我按数据库表的方式理解命名的,关系的集合

this.E => 边的个数

this.visited => 访问过的 bool 集合,其实就是标记

this.defined => 工具小函数,是否定义过,与图无关

所以最后有关的符号有:

G、V、T、E

是不是感觉一下子变简单了,不过程序的抽象有一个上层,那就是类。
然后我这里按计算机的方式,定义了输入、输出函数:input、output

class G {
    constructor(V) {
        this.V = V;
        this.T = [];
        this.E = 0;
        this.visited = [];

        for (let v = 0; v < this.V; ++v) {
            this.T[v] = [];
            this.T[v].push(-1);
        }

        this.defined = s => s !== void 0;
    }
    input(v, w) {
        this.T[v].push(w);
        this.T[w].push(v);

        this.E++;

        return this;
    }
    output() {
        console.table(this.T);
    }
}

然后能够看出,其实边是由点的连接组成的,刚好符合数学的定义,并且与相邻有相关性。

那么,实现了结构,还应该有其他作用,那么接下来看一下遍历算法:深度遍历(DFS) 和 广度遍历(BFS)。准确来说应该是优先采用什么策略的遍历方式。其实我这里的实现感觉...不好,和树关系大了点,不过树的大集合就能够上升到图。

dfs(v) {
    this.visited[v] = true;

    if (this.defined( this.T[v] )) {
        console.log('老孙到此一游:' + v);
    }

    this.T[v].forEach(t => {
        if (t !== -1 && !this.visited[t]) {
            this.dfs(t);
        }
    });
}

对于深度遍历,是将图按一个固定方向,纵向的结果,所以是一个递归的结构。

bfs(node) {
    this.visited[node] = true;

    var queue = [];
    queue.push(node);
    while(queue.length > 0) {
        var v = queue.shift();
        if(this.defined( this.T[v] )) {
            console.log('老孙到此一游:' + v);
        }

        this.T[v].forEach(t => {
            if(t !== -1 && !this.visited[t]) {
                this.visited[t] = true;
                queue.push(t);
            }
        });
    }
}

对于广度遍历,是将图按一个固定方向,横向的结果,所以是一个链式进出的关系,这里是用队列,在 JS 中做队列这种先进先出比较简单。

// 测试代码
var v = [1, 2, 3, 4, 5];
let g = new G( v.length + 1 );
g.input(1, 2).input(1, 5)
    .input(2, 4).input(2, 5).input(2, 3)
    .input(3, 4)
    .input(4, 5)
    .output();
g.dfs(1);
console.log('------------');
// 让它失忆一下
g.visited = [];
g.bfs(1);

执行结果

……-_-# 简单玩一下,睡觉了 zZZ

2016年11月3日星期四

JavaScript - EventEmitter 背后的秘密

什么是 Event Emitter?

Event emitter 听起来只是触发一个事件,这个事件任何东西都能监听。

想象一下这样的场景,在你的异步代码中,去“呼叫”一些事件的发生,以及让你其他部分都要听到你的“呼叫”并且注册他们的想法。

为了不同的目的,对于 Event Emitter 模式有大量不同的实现,但是基本的想法是为了给一个框架提供事件的管理以及能够去订阅他们。

在这里,我们的目标创建属于我们自己的 Event Emitter 去理解背后的秘密。所以,让我们看一下下面的代码是怎么工作的。

let input = document.querySelector('input[type="text"]');
let button = document.querySelector('button');
let h1 = document.querySelector('h1');

button.addEventListener('click', () => {
    emitter.emit('event:name-changed', { name: input.value });
});

let emitter = new EventEmitter();
emitter.subscribe('event:name-changed', data => {
    h1.innerHTML = `Your name is: ${data.name}`;
});

让我们开始。

class EventEmitter {
    constructor() {
        this.events = {};
    }
}

我们先创建一个 EventEmiiter 类以及初始化 events 空对象属性。这个 events 属性的目的是为了存储我们的事件集合,这个 events 对象使用事件名当做 key,用订阅者集合当做 value。(可以把每个订阅者看作是一个函数)。

订阅函数

subscribe(eventName, fn) {
    if (!this.events[eventName]) {
        this.events[eventName] = [];
    }

    this.events[eventName].push(fn);
}

这个订阅函数获取事件名称,在我们之前的例子中,它是 "event:name-changed" 以及传入一个回调,当有人调用 emit(或尖叫)事件的时候调用回调。

在 JavaScript 函数的优点之一是函数是第一对象,所以我们能像之前我们的订阅方法一样,通过函数作为另一个函数的参数。

如果未注册这个事件,我们需要在第一次为它设置一个初始值,事件名称作为 key 以及初始化一个空数组赋值给它,然后我们将函数放入这个数组,以便我们想通过 emit 去调用这个事件。

调用函数

emit(eventName, data) {
    const event = this.events[eventName];
    if (event) {
        event.forEach(fn => {
            fn.call(null, data);
        });
    }
}

这个调用函数接受事件名,这个事件名是我们想“呼叫”的名称,以及我们想传递给这个事件的数据。如果在我们的 events 中存在这个事件,我们将带上数据循环调用所有订阅的方法。

使用上面的代码能做我们所说的全部的事情。但我们仍然有一个问题。当我们不再需要它们的时候,我们需要一种方法来取消注册这些订阅,因为如果你不这样做,将造成内存泄漏。

让我们来解决这个问题,通过在订阅函数中返回一个取消注册的方法。

subscribe(eventName, fn) {
    if (!this.events[eventName]) {
        this.events[eventName] = [];
    }

    this.events[eventName].push(fn);

    return () => {
        this.events[eventName] = this.events[eventName].filter(eventFn => fn !== eventFn);
    }
}

因为 JavaScript 函数是第一对象,你能在一个函数中返回一个函数。因此现在我们能调用这个取消注册函数,如下:

let unsubscribe = emitter.subscribe('event:name-changed', data => console.log(data));

unsubscribe();

当我们调用取消注册函数的时候,我们删除的功能依赖于对订阅函数集合的筛选方法(Array filter)。

和内存泄露说再见!👋👋

你能运行这份代码,所有代码都在这里。

注:这份代码可能需要翻墙或者特别慢,所以我放到了 github 上,大家可以下载。(⊙o⊙)…暂时放我的账号下,如果有合适的地方请联系我。

原文出自:https://medium.com/@NetanelBasal/javascript-the-magic-behind-event-emitter-cce3abcbcef9#.nzgbagnxe

2016年10月26日星期三

思考 CSS 架构

你有没有在一个逐渐膨胀的 CSS 项目中感到混乱呢?保持样式风格统一和 HTML 的影响是比较困难的:尽管修改一个较小的问题,都可能创建更多丑陋的 hack,也可能 CSS 的小改变会影响 JavaScript 的功能。但是这些问题能在我们的项目开始的时候静心规划,就能很大程度上避免这些问题。让我们开始说 CSS 架构吧。

思考一个好的架构

一个好的 CSS 架构是具有良好的可扩展性。可扩展性是在任何项目开发中都是具有挑战性的,范围的扩大或者团队人数的增加,这些挑战在 CSS 中也不例外。层级和全局的特性使 CSS 很强大,反之在开发过程中也是脆弱的。如果你写了一段时间 CSS,你会发现当改变一行 CSS 来修复一个功能的时候会破坏一批其他的 CSS 功能,这将让你很头痛。细心的规划能提供下列的好处:

  • 更少的样式
  • 更少的样式冲突
  • 更长的维护周期
  • 更快的提升新团队成员
  • 团队成员之间更容易交流
  • 更平稳的项目交接

CSS 规则的类型

Jonathan Snook 对 CSS 规则进行分类的概念在他的书中很受欢迎 Scalable and Modular Architecture for CSS-可扩展的模块化 CSS 架构(SMACSS)。我们的结构按下面的类型进行分类能够帮助我们以及我们的团队更好的理解每一个样式的用途。我使用 7 种分类来定义规则集合,大多数是基于 SMACSS 推荐的做法,确保每一个风格都适合这些类别之一。

  • 基本样式 (Base Styles)
  • 对象 (Objects)
  • 组件 (Components)
  • 状态 (State)
  • 主题 (Themes)
  • 工具集 (Utilities)
  • JavaScript 勾子 (JavaScript hooks)

理解这些分类和目的,将有助于你写出更高抽象层次的样式。

基本样式 (Base Styles)

基本样式规则是创建在元素上。他们是你的网站想设置的全局默认样式。典型的,包括像:排版、box-sizing、你可能想统一所有浏览器元素的正常值。一个常见的错误,你并不想要太重的基本元素风格,并创建默认值。你真的想移除无序列表的标准强调样式或者只是在某些情况下移除它?

对象 (Objects)

对象规则只关注结构和布局。不被允许修饰类的样式。对象类的概念是 Nicole Sullivan 推广的,为了重用相同结构和布局的页面模式。在你的设计中寻找页面结构的模式,并创建可以在网站的多个组件或部分使用的对象类。通过把这些样式映射为对象类,你就可能避免冗余,减少你的CSS的大小。网格系统,无论是手工编写的或采用的一个框架,都适合使用对象类型。

组件 (Components)

组件是分离的、自成一体的 UI 零件。他们是面包和黄油的原子设计,将组成你的大多数样式。一个组件能小到一个按钮,或大到一个切换。创建鲁棒性的组件的关键是使它们独立于页面的任何其他部分,并自成一体。你应该能把一个组件放到页面的任何位置,它都将保持它的结构和设计。

状态 (State)

状态累是帮助修改一个组件的状态。定义手风琴的展开或收起,链接的有效或无效,元素的隐藏或显示(visible)。在 JavaScript 中常见的添加、删除状态 class。你只能更新一个状态 class 和允许样式像定义的样子,而不是用 JavaScript 去直接操作样式。

主题 (Themes)

主题类只需改变一个组件,使用唯一的颜色、字体或其他装饰。主题类能用来修改整个页面或只是一个的组件。主题不是在每一个项目中都必须的,但当你需要的时候再使用它们。

<blockquote class="c-pullquote t-light">
    <p>A great quote from someone special.</p>
</blockquote>

工具集 (Utilities)

工具集类是单一目的,帮助我们应用一个特殊样式规则。它们能用来修改间隙、增加字体大小、居中文本、添加一个浮动清除 (clearfix),隐藏等等。工具集能帮助你对布局进行微调,像添加组件之间的间隙或者清除浮动。它们也可以用来对现有组件进行微调,而不需要创建一个新的组件。

.u-sp {
    margin-bottom: 1em !important;
}

.u-clearfix:after {
    content: " ";
    display: block;
    clear: both;
    visibility: hidden;
    height: 0;
    font-size: 0;
}

.u-txt-center {
    text-align: center !important;
}

.u-txt-larger {
    font-size: 130% !important;
}
<div class="promo u-sp"></div>
<div class="promo u-sp"></div>
<div class="promo"></div>

JavaScript Hooks

无论什么时候,解除你的 JavaScript 和样式的耦合。将类名既用于样式又用于 DOM 选择的 JavaScript 勾子,当 CSS 被重构的时候,JavaScript 的依赖就不清晰明了。相反,JavaScript 勾子类就是用来专注处理这类问题的。

<button class="btn btn--buy js-buy-now"></button>

类的命名

当你对类进行命名的时候,确保你的名字足够长,能够清晰表达(.pullquote 而不是缩写的 .pq 来表达意思),但是有一些是不需要的(.nav 不用 .navigation)。类命名的可读性能帮助团队成员现在和将来对你表达背后的逻辑的理解有重要的影响。

在编写 CSS 的时候创建具有描述性的、有意义的名称是最棘手的问题,如果能仔细考虑将很有帮助。本文有限不能够对命名的事情讨论得太深入,但是能看一下我们非常棒的文章,命名 CSS 真的很难-Ethan Muller,这篇文章对这个事情讲解得更详细。

BEM 的命名规范

BEM(Block Element Modifier)命名 CSS 组件的方式是非常流行、特别有帮助的规范。该规范来自于俄罗斯的搜索引擎-Yandex。这种命名方式非常简单:

[BLOCK]__[ELEMENT]-[MODIFIER]

你可能使用这种冗长的类名称很不习惯,但在你的项目中使用 BEM 将很快去掉这种担心。下面是一个使用 BEM 来命名组件的例子:

<div class="alert alert--warning">
    <h1 class="alert__title">
        <span class="alert__icon"></span>
        Alert Title
    </h1>
    <p class="alert__description">The password you entered is invalid.</p>
</div>

BEM 命名有三个主要的好处:

  • 可读性:大多数元素的类名使用清晰的描述,能使其他人更容易阅读你的 HTML 或者 CSS 文件。
  • 自描述:使用分等级的类命名,能使它非常清晰的描述这个组件属于哪一个基本组件。
  • 特殊性:它可能在你的组件的每一个元素添加一个类的方式有点过了,但是,通过这样做,你能保持你的每个选择器都有特异性,让覆盖权重更加直接。

命名空间

另外最好的实践就是当命名你的类名的时候,使用命名空间前缀来进行分类。这些前缀会在你的命名前添加一组字符,但是这个值能立刻标记每一个类的目的,在你看 HTML 或者样式的时候是很需要的。我使用下面这些命名空间:

  • 对象类:.o-
  • 组件:.c-
  • 状态类:.is- 或者 .has-
  • 主题:.t-
  • 工具类:.u-
  • JavaScript 勾子:.js-
<footer class="c-footer">
    <div class="o-container-wide">
        <a class="c-footer__logo" href="/">The Assist</a>
        <div class="c-social c-social--follow">
            <div class="c-social__label u-txt-center">Join the conversation</div>
            <ul class="c-social__list">
                <li class="c-social__item"></li>
                <li class="c-social__item is-active"></li>
                <li class="c-social__item"></li>
            </ul>
        </div>
        <p class="c-footer__credit"></p>
    </div>
</footer>

关于更多关于命名空间的信息,可以看 Harry Roberts's post on the subject

代码风格

像其他代码一样,你的 CSS 项目使用一致的编码风格是很重要的。这包括空格,缩进,命名约定,注释,等你能阅读一些好的规范,比如:GoogleGithub 或者 Nicolas Gallagher。使用他们或者创建你自己更适合的规范。

组织代码

对于较好的代码组织,您应该使用预处理工具(SassLessStylus)或者后处理器(PostCSS)去编译你的代码。他们有很多优点,包括一些功能,比如:变量、函数、混淆、导入以及嵌套。这些功能将能让你有更多的方式去组织 CSS 架构,比你只用 CSS 来做更容易。

使用导入,你能将你的样式分解到多个文件中。

@import "variables";
@import "mixins";
@import "normalize";
@import "typography";
@import "headings";
@import "headings";
@import "layout";
@import "carousel";

任何值需要多次使用的时候能定义变量。给你的变量名添加前缀,能帮助识别他们的目的,也为了使代码完成更有用。

// Colors
$c-warning: Red;
$c-primary: Blue;
$c-background: White;

一些变量是需要全局定义的,应该存储在单独的变量文件中,但其他变量只被用于单个的组件,这就应该在使用它们的文件中定义。在 SASS 中,变量可以包含在嵌套的局部范围内。

.alert {
    $background-color: Red;
    $foreground-color: Cream;
    background-color: $background-color;
    color: $foreground-color;
}

注:其实这里准备来应该是 SCSS 语法,因为 SASS 语法是不需要花括号的。

源代码顺序

由于 CSS 层级样式的性质,你的样式顺序可能是一个问题。如果你不确定样式导入的顺序,你将发现你自己始终在级联问题上是头疼的。

最近,Harry Roberts 发布了一篇 a sensible method for ordering your styles (一种使用的排序方法)他称为 ITCSS (Inverted Triangle CSS),目的在于防止命名冲突、特殊性的问题,不好的风格以及无意的回退(可以看他的 Slide in-depth slides)。这个概念很简单:以影响最小范围和最低特殊性的样式规则作为排序的开始,一直到最靠近选择元素的和最高特殊性的样式结尾。这意味着你的变量定义和基本元素规则将总是在最开始,然而你的工具类和 IE hack 将总是在最后。

Harry 定义了七组,适合我们文件应该排序的分组:

  • 设置(Settings):变量和其他设置
  • 工具(Tools):自定义函数和混淆
  • 常用的(Generic):Font-face,box-sizing,normalize 等等
  • 元素(Elements):纯元素默认值,比如标题和链接
  • 对象类(Objects):布局和结构类
  • 组件(Componets):独立组件
  • (Trumps):工具类和其他规则,这意味着在其他一切都是放到最后的
@import "settings.global";
@import "settings.colors";
@import "tools.functions";
@import "tools.mixins";
@import "generic.box-sizing";
@import "generic.normalize";
@import "elements.headings";
@import "elements.links";
@import "objects.wrappers";
@import "objects.grid";
@import "components.nav";
@import "components.buttons";
@import "components.promos";
@import "trumps.utilities";
@import "trumps.ie8";

更深的挖掘

本文只介绍了从深度和广度上大量的主题,但是我希望它能够让你在组织和设计项目中的 CSS 的时候更多的思考。如果你想在以后深入这个主题,这篇文章有大量的资源链接,以及关注更多在这个领域的思想领袖的资源。

这篇文章主要是基于最近在相同主题的分享。能检出 slides for the presentation 或者观看下面的视频。

视频容易挂,这里我直接贴链接,大家可以直接点看在浏览器播放就行了。

https://player.vimeo.com/video/172444121?byline=0&portrait=0

原文出自:https://seesparkbox.com/foundry/thoughtful_css_architecture

2016年10月16日星期日

如何存储 React 组件的数据

主要讲解这几个部分:state、store、static、this、module-global data

前言

随着 React 和 Redux 的到来,一个共同的问题被问到:

我应该将数据保存在 Redux Store 中呢?还是应该保存成本地 state?

其实这个问题是说的不完整( 或者原文说的太简单 ),因为你能在组件中存储数据的方式还有两种:static 和 this.(其实就是静态数据,还是类的实例数据)

让我们来依次讨论一下,你应该在什么时候使用它们。

一、本地 state ( Local state )

ps:下面翻译的时候将 local state => 直接翻译成 state 吧,对于 state 其实的作用域是与组件挂钩的,直接翻译成本地 state 其实是不准确的。

当 React 在第一次被介绍的时候,我们就知道state。我们知道一个很重要的事情,就是当 state 的值改变的时候将触发组件的 re-render(重新渲染)。

这个 state 也能传递给子组件,子组件中通过 props 来获取传递的数据,这就允许你能够将你的组件分为:smart data-components(智能数据组件)and dumb presentational-components (填鸭式组件)。

这里有一个使用 state 来编写的 counter app(计数 APP):

import React from 'react'

class App extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      counter: 0
    }
    this.addOne = this.addOne.bind(this)
  }
  
  addOne() {
    this.setState({
      counter: this.state.counter + 1
    })
  }
  
  render() {
    return (
      <div>
        <button
          onClick={ this.addOne }>
          Increment
        </button>
        { this.state.counter }
      </div>
    )
  }
}

export default App

你的数据( counter 的值 )是存储在 App 组件中,也能向下传递给子组件。

& 应用场景

假如 counter 数据在你的 app 中是很重要的,以及这个数据会存储下来用于其他组件,那么你将不希望使用 state 来保存这个值。

这最好的实践是处理用户接口 (UI, User Interface) 的状态数据。比如:使用一个 交互组件 去填写表单,这时候使用 state 是不错的选择。

另外的例子,就是在 UI 交互相关的数据中使用,比如你将在一个列表中记录当前选中的 tab (选项卡)的状态,你就能存储一个 state

在你选择 state 的使用时机,也能够这样考虑:你存储的数据是否会在其他组件中使用。如果一个值是只有一个组件(或者也许只有一个子组件),这时使用 state 来保持这个值(或者说状态)都是安全的。

总结:保持 UI 状态和暂时的数据(比如:表单的输入),能够使用 state

二、Redux store

随着发展,每个人开始选择单向数据流, 我们选择 Redux。

对于 Redux,我们将获得一个全局的 store。这个 store 将绑定在一个最高等级的组件中,然后将 App 的数据流入所有的子组件(其实整个 App 就已经是这个最高等级组件的子组件了)。你能 connect 全局 store,使用:connect wrap 和 mapStateToProps 方法.

首先,人们就将任何事情都交给了 Redux store。比如:Users, modals, forms, sockets...,主要你知道的。

下面是一个和之前相同的计数 App,然后使用了 Redux。主要需要注意的是 counter 数据,使用 mapStateToProps 映射了数据,并且使用 connect 方法包裹组件,this.state.counter 现在就变成了 this.props.counter,然后这个值就是通过全局 store 获取的,这个全局 store 将值传递给当前组件的 props。(如果想知道具体,我在 React.js 模式的文章中有介绍原理)。

import React from 'react'
import { connect } from 'react-redux'
import Actions from './Actions.js'

class App extends React.Component {
  constructor(props) {
    super(props)
    this.addOne = this.addOne.bind(this)
  }
  
  addOne() {
    this.props.dispatch(Actions.addOne())
  }
  
  render() {
    return (
      <div>
        <button
          onClick={ this.addOne }>
          Increment
        </button>
        { this.props.counter }
      </div>
    )
  }
}

const mapStateToProps = store => {
  return {
    counter: store.counter
  }
}

export default connect(mapStateToProps)(App)

现在当你点击按钮的时候,通过一个 action => dispatched 使全局 store 更新。然后这个数据通过外层组件传递到当前组件。

值得注意的是:当 props 被更新的时候,这也将触发组件的 re-render(重新渲染)=> 这与你更新 state 的时候一样。

& 应用场景

对于 Redux store 是很好的保持了除 UI 状态数据外的应用状态。有一个不错的例子,比如用户的登录状态。对于在在登录状态改变的同时,多数组件需要访问这个数据信息做出响应。这些组件(至少有一个被渲染)将需要重新渲染与更新的信息。

Redux 触发事件在你需要跨组件或者跨路由的情况下是很有用的。比如有一个登录弹框,当你的 App 中有多个按钮点击都将触发弹出它的时候。而不是在你需要渲染这个弹框的多个地方做判断,你能通过一个顶层的 App 组件去使用 Redux action 去触发它并且修改 store 的值。

总结:你想将跨组件的保持数据的时候能够使用 store

三、this.

在 React 的开发中,使用 this 来保存数据的场景很少。人们经常忘记了 React 使用的是 JavaScript 的 ES2015 的语法。任何你能够用 JavaScript 做的事情,你都能在 React 做(这也是我非常喜欢 React 的原因呢 0^ freedom)。

下面的例子依然是一个计数应用,与之前的例子有点类似。

import React from 'react'

class App extends React.Component {
  constructor(props) {
    super(props)
    this.counter = 0
    this.addOne = this.addOne.bind(this)
  }
  
  addOne() {
    this.counter += 1
    this.forceUpdate()
  }
  
  render() {
    return (
      <div>
        <button
          onClick={ this.addOne }>
          Increment
        </button>
        { this.counter }
      </div>
    )
  }
}

export default App

我们是在组件中使用 this 存储 counter 变量的值,并且使用 forceUpdate() 方法来触发组件的重新渲染。这是因为没有任何的 state 和 props 修改,因此没有自动触发重新渲染机制。

这个例子实际上不应该使用 this。如果你发现你必须使用 forceUpdate() 才能满足需求,你可能代码哪里出了问题。如果想值修改的时候自动触发重新渲染,你应该使用 state 或者 props/Redux store(其实严格一点来说,作者这里的表述是不清的,其实重新渲染与 Redux 并无关系,本质上就是 props 的更新流入组件)。

& 应用场景

使用 this 来保存数据的时候,能够在改变的时候不需要去触发重新渲染的场景。比如:sockets 是很好的一个使用 this 保存数据的场景。

import React from 'react'
import { Socket } from 'phoenix'

class App extends React.Component {
  componentDidMount() {
    this.socket = new Socket('http://localhost:4000/socket')
    this.socket.connect()
    this.configureChannel("lobby")
  }
  
  componentWillUnmount() {
    this.socket.leave()
  }
  
  configureChannel(room) {
    this.channel = this.socket.channel(`rooms:${room}`)
    this.channel.join()
      .receive("ok", () => {
        console.log(`Succesfully joined the ${room} chat room.`)
      })
      .receive("error", () => {
        console.log(`Unable to join the ${room} chat room.`)
      })
  }
  
  render() {
    return (
      <div>
        My App
      </div>
    )
  }
}

export default App

大多数人们没有他们之前一直在使用 this 定义方法。放你定义 render(),你实际上是:this.prototype.render = function () { },但是这在 ES2015 的 class 语法机制下是隐式的。

总结:在不需要数据改变的时候去触发重新渲染机制的时候,能够使用 this 去保存数据。

四、Static(静态的方式)

静态方法和属性也许是最少使用的(静下来,我知道他们不是真正在 class 下的一个机制),大多数是因为他们没有被频繁使用。但是他们不是很复杂。如果你用过 PropTypes,你就定义了一个 static 属性。

下面有两份代码块实际上是长得一样的。人们经常使用的方式是第一种,第二种是你能使用 static 关键字来定义。

class App extends React.Component {
  render() {
    return (<div>{ this.props.title }</div>)
  }
}

App.propTypes = {
  title: React.PropTypes.string.isRequired
}
class App extends React.Component {
  static propTypes {
    title: React.PropTypes.string.isRequired
  }
  
  render() {
    return (<div>{ this.props.title }</div>)
  }
}

正如你看到的,static 并不复杂。他们是一种另外的方式去声明一个类的值。这主要的不同是 static 不要像 this 那样去实例化一个类再去访问值。

class App extends React.Component {
  constructor() {
    super()
    this.prototypeProperty = {
      baz: "qux"
    }
  }
  static staticProperty = {
    foo: "bar"
  };
  
  render() {
    return (<div>My App</div>)
  }
}

const proto = new App();
const proto2 = proto.prototypeProperty // => { baz: "qux" }

const stat = App.staticProperty // => { foo: "bar" }

在这个例子中,你能看获取 staticProperty 的值,我们只组要直接通过类名去访问,而不是实例后。但是访问 proto.prototypeProperty 就必须要 new App().

& 应用场景

静态方法和属性是很少被使用,主要被用来定义工具方法或者特定类型的所有组件。

propTypes 是一个工具例子,比如一个 Button 组件,每一个按钮的渲染都需要相似的值。

另外个案例就是如果你关注获取的数据。如果你是使用 GraphQL 或者 Falcor,你能具体描述需要服务端返回什么数据。这种方法你能不用接受大量组件不需要的数据。

class App extends React.Component {
  static requiredData = [
    "username",
    "email",
    "thumbnail_url"
  ]
  
  render() {
    return(<div></div>)
  }
}

因此在例子中,在特殊组件请求数据之前,你能使用 App.requiredData 快速获取必要的 query 的值。

总结:你也许几乎不会使用 static 来保存数据。

五、其他方式...

其实是有另一个选择,我没有在标题中说明,因为你不应该这样做:你可以存储在一个全局变量,然后通过一个文件进行导出。

这是一个特别的场景,然后大多数情况下你不应该这样做。

import React from 'react'

let counter = 0

class App extends React.Component {
  constructor(props) {
    super(props)
    this.addOne = this.addOne.bind(this)
  }
  
  addOne() {
    counter += 1
    this.forceUpdate()
  }
  
  render() {
    return (
      <div>
        <button
          onClick={ this.addOne }>
          Increment
        </button>
        { counter }
      </div>
    )
  }
}

export default App

你能够看到和使用 this 是类似的,除了使用的是一个全局的变量。

如果你需要跨组件的共享数据以及保存一个全局变量,大多数更好的是使用 Redux store

总结:不要使用全局变量。

原文出自:https://medium.freecodecamp.com/where-do-i-belong-a-guide-to-saving-react-component-data-in-state-store-static-and-this-c49b335e2a00#.yvhqqxdkh

2016年10月15日星期六

(译)在你的项目中编写测试-TypeScript

前言

这篇文章的原作者感觉有一些不太适合或者过期的地方,已经修正,代码放入了我的 github 中,运行成功后的代码,大家如果能直接下载看懂,就不用看文章了~节约时间嘛,如果想看看文章的可以继续。

测试源码

这篇文章和之前写的 TypeScript + TDD 那篇不一样的是更加简单,直接测试,不需要其他什么多余配置。

开始

TypeScript 是让我着迷的,我最近需要开始准备使用 TypeScript 来编写项目,并且需要测试。在这篇文章中,我将解释如何开始一个简单的测试,在我们使用 TypeScript 来编写的时候,其中,我们能使用 TypeScript 的 import 语法,以及类型接口来帮助我们。

我们有大量的 JavaScript 测试工具和框架,比如:mochajasmine等等。主要是 BDD/TDD 的方式来运行测试。他们能使使用 node 自己的assert(断言)库, 或者其他扩展的断言库,比如 chai。我当前实现选择的是 mocha 和 chai,因为他们和我想要的一样,容易配置和运行 TypeScript 的测试。我是尝试过 Jasmine 但是我不能像我想的那样工作(可能是因为我缺乏如何使用的窍门)。如果能使用 Jasmine 来配置,将是很棒的。

值得注意的是:以下的是跨平台的,因此只要你在代码中看到 $ 标记,表示是运行在你的终端中,比如 Mac 的 TerminalUbuntu/Linux 的 ShellWindows 的 CommandPrompt/PowerShell 之中。

安装开发依赖(development dependencies)

我是使用 mocha 作为我们的测试框架或者测试运行器,以及 chai 作为我们的断言库。我们将使用第三方库 ts-node 帮助我们运行 TypeScript 的测试。我们将它们作为开发依赖进行全局安装。

$ npm install mocha chai ts-node typescript --save-dev

安装类型定义文件(type definitions)

在我们写 TypeScript 测试之前(后面就不这样翻译了,直接翻译成测试吧,比较和语言不想关联太多),对于我们的测试框架和断言库,他们是有点讨厌类型声明,真是这样么?让我们用 typings 来做这个事。尽管你可能用过这个开发 TypeScript。它是一个定义类型的管理者,用来做 TypeScript 的类型判断。让我们现在来安装它:

$ npm install typings --save-dev

现在我们装好了 typings, 让我们开始建立一些必要的类型定义。我们将 mocha 作为全局模块(我们在使用它的时候不需要导入),以及 chai 直接依赖(我们需要在测试中导入它)。我们将从 DefinitelyTyped 安装 mocha 的类型定义,这是一个类型定义的库,然后我们将使用 npm registry 来安装 chai 的类型定义.

/* 不按这种方式了,不好用,使用接下来的方式 */
$ typings install dt~mocha --global --save
$ typings install npm~chai --save

在 TypeScript 2.0 中,我们能够用其他方式来添加类型定义,因此我们也能够用下面的方式来替代之前提到的 typings 安装的方式。新的方式不依赖 typings npm package 或者 module(包和模块).我们将使用下面的命令:

$ npm install @types/chai @types/mocha --save-dev

开始写我们的第一个 TypeScript 测试

为了解释测试,让我写一些基本的 hello world 函数。我只测试一个 function,因此我是指需要创建一个返回 "Hello World" 字符串的函数。

/* src/hello.ts */
export function hello(): string {
  return 'Hello World!';
}

export default hello;

让我们写一个测试,现在我们需要一个测试方法:

/* src/__test__/hello-test.ts */
import hello from '../hello';
import { expect } from 'chai';
import 'mocha';

describe('Hello function', () => {
  it('should return hello world', () => {
    const result = hello();
    expect(result).to.equal('Hello World!');
  });
});

运行我们的测试

让我们使用 nom run test 来运行我们的测试。我们只需要调用 mochats-node。编辑 package.json

{
  "scripts": {
    "test": "./node_modules/.bin/mocha -r ./node_modules/ts-node/register src/__test__/**/*.ts"
  },
}

我只给 mocha 注册了 ts-node,以及最后参数是定义 mocha 应该运行测试的 src 文件夹中的 test.ts。你能够根据你的项目情况进行修改。

简单的测试结果:

测试结果

原文出自:https://journal.artfuldev.com/write-tests-for-typescript-projects-with-mocha-and-chai-in-typescript-86e053bdb2b6#.n41jb19wz