2018年2月1日星期四

React 导读(一)

前言

写这篇文章的主要目标是让初学者更快的上手 React 的项目开发,能有一个循循渐进的理解过程。需要有一定的 JavaScript 基础和 NPM 的使用经验。不多说了,下面会按这个顺序进行介绍:

  1. React 如何编写 Hello World!
  2. React 中三个最基础、最重要的东西
  3. React 中的 JSX
  4. 你的第一个 Web 组件
  5. React 中最开始需要关注的生命周期
  6. React 一个组件集合的简单交互
  7. React 开始一个项目的一点建议
  8. React 简单的项目结构组织

开始前需要安装的环境:node.js、yarn

一、React 如何编写 Hello World!

1.使用脚手架直接避开环境搭建的问题

// 安装脚手架
➜ npm install -g create-react-app

2.使用脚手架创建项目

// react-study 是项目的根文件夹
➜ create-react-app react-study

// 执行后的第一行提示语,会提示创建的完整路径
Creating a new React app in /Users/lulin/Desktop/react-study.

// 安装成功后会提示下面的内容
Success! Created react-study at /Users/lulin/Desktop/react-study
Inside that directory, you can run several commands:

// 使用 yarn 启动项目
yarn start
    Starts the development server.
// 要发布项目的时候运行
yarn build
    Bundles the app into static files for production.
// 做测试的时候执行,目前没用
yarn test
    Starts the test runner.
// 可以自定义配置,目前也不用
yarn eject
    Removes this tool and copies build dependencies, configuration files
    and scripts into the app directory. If you do this, you can’t go back!

We suggest that you begin by typing:
  cd react-study
  yarn start

Happy hacking!

3.使用 Visual Studio Code 打开 react-study

先只需要关注 src 目录中的 index.js,如下:

├── src
│   ├── App.css
│   ├── App.js
│   ├── App.test.js
│   ├── index.css
│   ├── index.js // 代码的入口文件
│   ├── logo.svg
│   └── registerServiceWorker.js

4.修改 index.js

删除 index.js 中所有的内容,贴以下代码运行:

import React from 'react';
import ReactDOM from 'react-dom';

ReactDOM.render(
    <h1>Hello World!</h1>,
    document.getElementById('root')
);

ReactDOM.render 方法2个参数,第一个参数就是要渲染的界面结构或者一个 React 组件,第二个参数是要把这个结构渲染到真实网页 DOM 的什么位置。所以这份代码的结果就是在 id=root 的DOM节点下渲染出来 Hello World!

二、React 中三个最基础、最重要的东西

一中介绍了 React 能够渲染一个 HTML 到指定的 DOM 中,但是 React 发明出来主要不是做这个事情的,因为这个事情可以直接通过 原生 JavaScript 的 innerHTML 也能实现。

React 主要作用可以认为是以下几点:
(1) 使用数据来驱动界面更新;
(2) 使用单向变化的数据来让 BUG 更好调试一点;
(3) 更方便、更声明式的编写 Web 组件;

那么这节主要介绍三个东西:
(1) state
(2) props
(3) setState 方法

如果要实现一个简单的加法器,这三个东西已经可以很好实现:

根据上面标的数字:
(1) this.state 里面有一个属性叫 count,这个属性能够通过 this.setState 方法重新传入一个对象来重新赋值。赋值的同时 render() 方法中 this.state.count 也会跟随自动变化,最后体现到网页上。这就是 state 属性的作用。
(2) this.setState() 方法接收一个新对象来重新赋值 this.state
(3) this.setState() 也接收一个函数,这个回调函数这里我默认有一个 prevState 参数,表示之前的 this.state 的值,这个函数的返回值就是最新的 this.state

大家还应该注意 button 上绑定的 onClick 事件,这就是跟 DOM 上直接绑定事件的写法一样(目前先这样理解),不过需要都写成驼峰标识。

那么 statesetState 都介绍了,props 又是什么呢?你可以暂时理解成一个组件外面传递的属性。
还是以计数器的代码为例子,简单修改一下:

class Counter extends React.Component {
    state = {
        count: 0
    };
    // 加 1
    onAdd() {
        this.setState({
            count: this.state.count + 1
        });
    }
    // 减 1
    onSub() {
        this.setState(prevState => {
            return {
                count: prevState.count - 1
            };
        });
    }
    render() {
        return (
            <div>
                {/* 这里的 this.props 属性 */}
                <h1>{this.props.name}</h1>
                <button onClick={this.onSub.bind(this)}>-</button>
                <span>{this.state.count}</span>
                <button onClick={this.onAdd.bind(this)}>+</button>
            </div>
        );
    }
}

// Counter 组件传了一个 name 属性
ReactDOM.render(
    <Counter name="计数器" />,
    document.getElementById('root')
);

这里注意代码中的注释,应该很直观了,我们在组件上添加的属性,都能在组件里面通过 this.props 属性获取到,拿一个其他方式来比喻,就相当于函数的参数,参数传递进函数,函数内部可以使用。但是不同的是 this.props 在组件内部是只读的。

看到这里,其实你已经能够使用 React 来构造一个网页了,是不是很简单,就三个东西,加上一个 ReactDOM.render 方法。当然,如果 ES6 不熟悉的话可能还是麻烦,但是这是必须要去学习和克服的,因为目前已经是主流而且是进入了规范的东西。

三、React 中的 JSX

接触 React 你肯定会问为什么要用 JSX,JSX 到底是什么。其实非官方的我只想这么解释,就是一个编写视图的模版而已,语法也不复杂,列下:
(1) 基本上是使用原始的 HTML;
(2) 事件绑定方法使用驼峰方式;
(3) 要插入 JavaScript 代码需要 {} 包裹,里面的代码就是原生的 JavaScript 代码;
(4) 避开一些 JavaScript 关键字,比如:class 要写成 className。

上面第二节的计数器中,render() 方法就是编写 JSX 的主要位置,其实 JSX 可以编写在 React 代码中的任意位置,但是推荐不要写得太过于零散。

其实理解这几点就已经足够了,具体可以看一下官方文档 JSX-简介

JSX 最终编译后也就是原生的 JavaScript 代码

四、你的第一个 Web 组件

学习了上面这些知识后,其实我们就已经能够封装一个简单的组件的,第一个我这里先以 CheckBox 为例子,比较简单也很常用,应该比较适合。先来分析下这个小组件最基础的功能:就是点击选中和取消选中,模拟的话可以通过换图来实现,就是切换 DOM class。那么我们开始动手吧!

class CheckBox extends React.Component {
    state = {
        checked: false // 默认没有选中
    };
    // 交替(选中/没有选中)的状态
    onClickCheckbox() {
        this.setState(prevState => {
            return {
                checked: !prevState.checked
            };
        });
    }
    render() {
        const checkboxClassArr = ['ui-checkbox'];
        // 如果选中,就添加一个 checked class 来给 css 做样式
        if(this.state.checked) {
            checkboxClassArr.push('checked');
        }

        // 组合最后的 class 结果
        const checkboxClass = checkboxClassArr.join(' ');

        return (
            <div onClick={this.onClickCheckbox.bind(this)}>
                {/* 这里模拟图标 */}
                <span className={checkboxClass}>
                    <i className="icon-checkbox"></i>
                </span>
                {/* 这里模拟图标内容 */}
                <span>{this.state.checked ? '选中' : '没有选中'}</span>
            </div>
        );
    }
}

这样一个简单的组件就已经完成了!效果如下:

今天就写到这里睡觉吧~可以动手试试,来点感觉哦~这基本是最常用的一些概念和意义了,有兴趣可以提前阅读中文官方文档,已经改版体验好多了,慢慢读下来应该很好理解。

2017年10月23日星期一

组件化通用模式

一、前言

模式是一种规律或者说有效的方法,所以掌握某一种实践总结出来的模式是快速学习和积累的较好方法,模式的对错需要自己去把握,但是只有量的积累才会发生质的改变,多思考总是好的。(下面的代码实例更多是 React 类似的伪代码,不一定能够执行,函数类似的玩意更容易简单描述问题)

二、前端的关注点迁移

这篇文章主要介绍现在组件化的一些模式,以及设计组件的一些思考,那么为什么是思考组件呢?因为现在前端开发过程是以组件为基本单位来开发。在组件化被普及(因为提及的时间是很早的或者说有些厂实现了自己的一套但是在整个前端还未是一种流行编写页面的单元)前,我们的大多数聚焦点是资源的分离,也就是 HTML、CSS、JavaScript,分别负责页面信息、页面样式、页面行为,现在我们编程上的聚焦点更多的是聚焦在数据组件

但是有时候会发现只关心到这一个层级的事情在某些业务情况下搞不定,比如组件之间的关系、通信、可扩展性、复用的粒度、接口的友好度等问题,所以需要在组件上进行进一步的延伸,扩展一下组件所参考的视角,延伸到组件模块组件系统的概念来指导我们编写代码。

概念可能会比较生硬,但是你如果有趣的理解成搭积木的方式可能会更好扩展思路一点。

三、数据之于组件

在说组件之前,先来说下数据的事情,因为现在数据对于前端是很重要的,其实这是一个前、后端技术和工作方式演变形成的,以前的数据行为和操作都是后端处理完成之后,前端基本拿到的就是直接可用的 View 展示数据,但是随着后端服务化,需要提供给多个端的数据以及前后端分离工作模式的形成,前端就变得越来越复杂了,其实 SPA 的形成也跟这些有一定关系,一是体验可能对于用户好,二是演变决定了这种方式。此时,前端的数据层就需要设计以及复用一些后端在这一层级的成熟模式,在这里就产生了一种思想的交集。

比如现在有一个 RadioGroup 组件,然后有下面 2 种数据结构可以选择:

items = [{
    id: 1,
    name: 'A',
    selected: true
}, {
    id: 2,
    name: 'B',
    selected: false
}];
data = {
    selected: 1
    items: [{
        id: 1,
        name: 'A'
    }, {
        id: 2,
        name: 'B'
    }]
};

那么我们的组件描述(JSX)会怎么写呢?
第一种:

items.map(item =>
    return <CheckBox key={`checkbox-${item.id}`}
                label={item.name}
                selected={item.selected}
                onClick={this.handleClick} />
);

第二种:

data.items.map(item => 
    const isSelected = item.id === data.selected;
    
    return <Checkbox key={`checkbox-${item.id}`
                label={item.name}
                selected={isSelected}
                onClick={this.handleClick}/>
);

当然,数据结构的选择上是根据需求,因为不同的数据结构有不同的优势,比如这里第二种类似 Dict 的查询很方便,数据也很干净,第一种渲染是比较直接的,但是要理解组件的编写方式其实很大程度上会跟数据产生一种关系,有时候编写发现问题可以返过来思考是否换种结构就变简单了。

数据就谈这些吧,不然都能单独开话题了,接下来看下组件,如果要学习模式就需要采集样本然后去学习与总结,这里我们来看下 Android && iOS 中的组件长什么样子,然后看是否能给我们日常编写 Web 组件提供点灵感,篇幅有限,本来是应该先看下 GUI 的方式。

四、iOS 端的组件概览

假设,先摒弃到 Web 组件的形态比其他端丰富,如果不假设那么这套估计不是那么适用。

4.1 iOS

iOS 的 View 声明能够通过一个故事板的方式,特别爽,比如这里给按钮的状态设定高亮、选中、失效这种,方便得很。

看完界面,直接的感觉下,然后我们来看下这个故事板的源码,上面是 XML 的描述,描述了组件的 View 有哪些部件以及 ViewController 里面映射的属性,用来将 View 和 ViewController 进行解耦。

<!-- 结构描述 -->
<scenes>
  <scene sceneID="tne-QT-ifu">
    <objects>
      <viewController title=“Login" customClass="ViewController">
        ...
        <view key="view" contentMode="scaleToFill"></view>
        ...
        <!-- 这里就是描述 vm 关联对象的地方,ios 里面可能称之为 outlet -->
        <connections>
          <outlet property="passwordTextField"/>
          <outlet property="tipValidLabel"/>
        </connections>
      </viewController>
    </objects>
  </scene>
</scenes>

<!-- 状态 & 样式描述 -->
<!-- 单独一个 button 组件描述 -->
<button>
  <state key="normal" title="Login">
      <color key="titleColor" red="1" green="1" blue="1" alpha="1"/>
  </state>
</button>

我这里定义的按钮状态、颜色都在这里,分别给他们命名:结构描述样式描述

那么具体怎么给用户交互,比较编程化的东西在 ViewController,来看下代码:

// 数据行为描述
// connection 中关联的钩子
@IBOutlet private weak var passwordTextField: UITextField!
@IBOutlet private weak var tipValidLabel: UILabel!

// 一个密码输入框的验证逻辑,最后绑定给 tipValidLabel、loginButton 组件状态上
let passwordValid: Observable<Bool> = passwordTextField.rx.text.orEmpty
    .map { newPassword in newPassword.characters.count > 5 }

passwordValid
    .bind(to: tipValidLabel.rx.isHidden)
    .disposed(by: disposeBag)

passwordValid
    .bind(to: loginButton.rx.isEnabled)
    .disposed(by: disposeBag)

上面代码整体可以看做是响应式的对象,绑定3个组件之间的交互,密码不为空以及大于5个字符就执行 bind 地方,主要是同步另外2个组件的状态。其实也不需要看懂代码,这只是为了体会客户端组件的方式的例子,ViewController 我这里就叫:数据行为描述。这样就有组件最基本的三个描述了:结构、样式、数据行为,虽然样本不多,但是这里直接描述它们就是一个组件的基本要素,整个故事板和 swift 代码很好的描述。

五、什么是组件?

5.1 组件描述
  1. 结构描述
  2. 样式描述
  3. 数据描述

对于组件来说,也是一份代码的集合,基本组成要素还是需要的,但是这三种要素存在和以前的 HTML, CSS, JS 三种资源的分离是不一样的,到了组件开发,更多的是关注如何将这些要素连接起来,形成我们需要的组件。

比如 React 中对这三要素的描述用一个 .js 文件全部描述或者将结构、数据包裹在一起,样式描述分离成 .<style> 文件,这里就可能会形成下面 2 种形式的组件编写。

=> 3 -> (JSX + styled-components)

// 组件样式
const Title = styled.h1`
    font-size: 1.5em;
    text-align: center;
`;

// 组件内容
<Title>Hello World!</Title>

=> 2 + 1 -> (JSX + CSS Module)

export default function Button(props) {
    // 分离的样式,通过结构化 className 来实现连接
    const buttonClass = getClassName(['lv-button', 'primary']);

    return (
        <button onClick={props.onClick} className={buttonClass}>
            {props.children}
        </button>
    );
}

可能最开始很多不习惯这样写,或者说不接受这类理念,那么再看下 Angular 的实现方式,也有 2 种:

(1) 采用元数据来装饰一个组件行为,然后样式和结构能够通过导入的方式连接具体实现文件。

@Component({
    selector: 'app-root',
    // 结构模板
    templateUrl: './app.component.html',
    // 样式模板
    styleUrls: ['./app.component.css']
})
// 等同于上面描述的 iOS 组件的 ViewController
export class AppComponent { }

(2) 与第一种方式不同的地方是能够直接将结构和样式写到元数据中。

@Component({
    selector: 'app-root',
    template: `
        <style>
            h1 { font-weight: normal; }
        </style>
        
        <h1>{{title}}</h1>
        <ul>
            <li *ngFor="let item of items">{{ item }}</li>
        </ul>
    `,
    // styles: ['h1 { font-weight: normal; }']
})
export class AppComponent {
    title = 'Hello Angular';
    items: number[] = [1, 2, 3];
}

无论实现的形式如何,其实基本不会影响太多写代码的逻辑,样式是目前前端工程化的难点和麻烦点,所以适合自己思维习惯即可。这里需要理解的是学习一门以组件为核心的技术,都能够先找到要素进行理解和学习,构造最简单的部分。

5.2 组件特性

虽然有了描述一个组件的基本要素,但是还远不足以让我们开发一个中大型应用,需要关注其他更多的点。这里提取组件基本都有的特性:

1. 注册组件

将组件拖到故事板

2. 组件接口(略)

别人家的代码能够修改组件的部分

3. 组件自属性

组件创建之初,就有的一些固定属性

4. 组件生命周期

组件存在到消失如何控制以及资源的整合

5. 组件 Zone

组件存在于什么空间下,或者说是上下文,很可能会影响到设计的接口和作用范围,比如 React.js 可用于写浏览器中的应用,React Native 可以用来写类似原生的 App,在设计上大多数能雷同,但是平台的特殊地方也许就会出现对应的代码措施)

这些主要就是拿来帮助去看一门不懂的技术的时候,只要是组件的范围,就先看看有没有这些东西的概念能不能联想帮助理解。

具体来看下代码是如何来落地这些模式的。

1.组件注册,其实注册就是让代码识别你写的组件

(1) 声明即定义,导入即注册

export SomeOneComponent {};
import {SomeOneComponent} from 'SomeOneComponent';

(2) 直接了当的体现注册的模式

AppRegistry.registerComponent('ReactNativeApp', () => SomeComponent);

(3) 拥有模块来划分组件,以模块为单位启动组件

@NgModule({
    // 声明要用的组件
    declarations: [
        AppComponent,
        TabComponent,
        TableComponent
    ],
    // 导入需要的组件模块
    imports: [
        BrowserModule,
        HttpModule
    ],
    providers: [],
    // 启动组件, 每种平台的启动方式可能不一样
    bootstrap: [AppComponent]
})
export class AppModule { }

2.组件的自属性

比如 Button 组件,在平时场景下使用基本需要绑定一些自身标记的属性,这些属性能够认为是一个 Component Model 所应该拥有,下面用伪代码进行描述。

// 将用户的 touch, click 等行为都抽象成 pointer 的操作
~PointerOperateModel {
    selected: boolean;
    disabled: boolean;
    highlighted: boolean;
    active: boolean;
}

ButtonModel extends PointerOperateModel { }
LinkModel extends PointerOperateModel { }
TabModel extends PointerOperateModel { }
...

// 或者是具有对立的操作模型
~ToggleModel {
    on: boolean;
}

OnOffModel extends ToggleModel { }
SwitchModel extends ToggleModel { }
MenuModel extends ToggleModel { }

...

// 组件的使用
this.ref.attribute = value;
this.ref.attribute = !value;

这些操作如果需要更少的代码,也许能够这样:

~ObserverState<T> {
    set: (value: T) => void;
    get: (value: T) => T;
    changed: () => void;
    cacheQueue: Map<string, T>;
    private ___observe: Observe;
}

Model extends ObserverState { }

基本上组件的这些属性是遍布在我们整个代码开发过程中,所以是很重要的点。这里还有一个比较重要的思考,那就是表单的模型,这里不扩展开来,可以单独立一篇文章分析。

3.组件的声明周期

与其说是生命周期,更多的是落地时候的代码钩子,因为我们要让组件与数据进行连接,也许需要在特定的时候去操作一份数据。在浏览器(宿主)中,要知道具体是否已经可用是一个关键的点,所以任何在这个平台的组件都会有这类周期,如果没有的话用的时候就会很蛋疼。

最简单的路线是:

mounted => update => destory

但是往往实际项目会至少加一个东西,那就是异常,所以就能够开分支了,但是更清晰的应该是平行的周期方式。

mounted => is error => update => destory

4.组件 Zone

组件在不同的 Zone 下可能会呈现不同的状态,这基本上是受外界影响的,然后自己做出反应。这里可以针对最基本的组件使用场景举例,但是这个 Zone 是一种泛化概念。

比如我们要开发一个弹框组件:Modal,先只考虑一个最基本需求:弹框的位置,这个弹框到底挂载到哪儿?

  1. 挂载到组件内部;
  2. 挂载到最近的容器节点下;
  3. 挂载到更上层的容器,以至于 DOM 基础节点。

每一种场景下的弹框,对于每种组件的方案影响是不同的:

  1. 组件内部,如果组件产生了 render,很可能受到影响;
  2. 挂载到最近的容器组件,看似问题不大,但是业务组件的拆、合是不定的,对于不定的需求很可能代码会改变,但是这种方案是不错的,不用写太远,当然在 React 16 有了新的方案;
  3. 挂载到更高的层级,这种方案适合项目对弹框需求依赖比较强的情况吧,因为受到的影响更小,弹框其实对于前端更强调的是一种渲染或者说是一种交互。

5.组件的递归特性

组件能够拥有递归是一个很重要的纵向扩展的特性,每一种库或者框架都会支持,就要看支持对于开发的自然度,比如:

// React
this.props.children

// Angular
<ng-content></ng-content>

基本上可以认为现在面向组件的开发是更加贴近追求的设计即实现的理想,因为这是面向对象方法论不容易具备的,组件是一种更高抽象的方法,一个组件也许会有对象分析的插入,但是对外的表现是组件,一切皆组件后经过积累,这将大大提升开发的效率。

六、如何设计组件?

经过前面的描述,知道了组件的概念和简单组件的编写方法,但是掌握了这些东西在实际项目中还是容易陷入蛋痛的地步,因为组件只是组成一个组件模块的基础单元,慢慢的开发代码的过程中,我们需要良好的去组织这些组件让我们的模块即实现效果的同时也拥有一定的鲁棒性和可扩展性。这里将组件的设计方法分为 2 个打点:

  1. 横向分类
  2. 纵向分层

其实这种思路是一直以来都有的,这里套用到平时自己的组件设计过程中,让它帮助我们更容易去设计组件。

这种设计的方法论是一个比较容易掌握和把握的,因为它的模型是一个二维的(x, y)两个方向去拆、合自己的组件。注意,这里基本上的代码操作单元是组件,因为这里我们要组装的目标是模块^0^感觉很好玩的样子,举例来描述一下。

比如我们现在来设计比较常用的下拉列表组件(DropDownList),最简单的有如下做法:

class DropDownList {
    render() {
        return (
            <div>
                <div>
                    <Button onClick={this.handleClick}>请选择</Button>
                </div>
                <DropDownItems>
                {this.props.dataSource.map((itemData, index) => <DropDownItem></DropDownItem>)}
                </DropDownItems>
            </div>
        );
    }
}

现在自己玩的往上加点需求,现在我需要加一个列表前面都加一个统一的 icon, 首先我们要做的肯定是要有一个 Icon 的组件,这个设计也比较依赖场景,目前我们先设计下拉。现在就有2种方案:

  1. 在 DropDownList 组件里面加一个判断,动态加一个组件就行;
  2. 重新写一个组件叫 DropDownIconList。

第一种方案比较省事,但是其实写个 if...else... 算是一个逻辑分支的代码,以后万一要加一个 CheckBox 或者 Radio 组件在前面...

第二种方案看上去美好,但是容易出现代码变多的情况,这时候就需要再重新分析需求变化以及变化的趋势。

这时候按垂直和水平功能上,这里拆分 DropDownIconList 组件可以看成一个水平的划分,从垂直的情况来看,将下拉这一个行为做成一个组件叫 DropDown,最后就变成了下面的样子:

class DropDown  {
    render() {
        <div>
            <div>
                <p onClick={this.handleClick}>请选择</p>
            </div>
            <div>{this.props.children}</div>
        </div>
    }
}

class DropDownList {
    render() {
        return (
            <DropDown onClick={this.handleClick} selected={selectedItems}>
                <DropDownItems>
                    {this.props.dataSource.map((itemData, index) => <DropDownItem></DropDownItem>)}
                </DropDownItems>
            </DropDown>
        );
    }
}

class DropDownIconList {
    render() {
        return (
            <DropDown onClick={this.handleClick} selected={selectedItems}>
                <DropDownItems>
                    {this.props.dataSource.map((itemData, index) => <DropDownIconItem></DropDownIconItem>)}
                </DropDownItems>
            </DropDown>
        );
    }
}

这样的缺点就是存在多个组件,也许会有冗余代码,优点就是以后增加类似组件,不会将代码的复杂度都加到一份代码中,比如我要再加一个下拉里面分页、加入选中的项、下拉内容分页、下拉的无限滚动等等,都是不用影响之前那份代码的扩展。

七、让组件连接起来

组件化的开发在结构上是一种分形架构的体现,是一个应用引向有序组件构成的过程。组件系统的复杂度可以理解成 f(x) = g(x) + u(x), g(x) 表示特有功能,u(x)表示功能的交集或者说有一定重合度的集合。组件弹性体现在 u(x) -> 0(趋近)的过程中,这个论点可参考:面向积木(BO)的方法论与分形架构

上面的过程中,有了组件组件模块,既然有了基础的实体,那么他们或多或少会有沟通的需求(活的模块)。基本上现在主流的方案可以用下面的图来表示。

我们提取一下主要的元素:

  1. Component 实体
  2. Component 实体的集合:Container
  3. Action
  4. Action 的操作流
  5. Service Or Store
  6. 状态同步中心
  7. Observable Effect

如果要说单向数据流和双向绑定的体现基本可以理解成体现在虚线框选的位置,如果组件或者Store是一个观察的模型,那么方案实现后就很可能往双向绑定靠近。如果是手动党连接 ViewValue 和 ModelValue,按照一条流下来可以理解成单向流。虽然没有按定义完全约束,但是代码的落地上会形成这种模式,这块细讲也会是一个单独的话题,等之后文章再介绍各种模式。

组件的关系能够体现在包含、组合、继承、依赖等方面,如果要更好的松耦合,一般就体现在配置上,配置就是一种自然的声明式,这是声明式的优势同时也是缺点。

八、组件系统体系

模块的管理中心是看组件系统,这块涉及到更高一层的概念或者说微服务化前端的理念,最后可连接到 FAAS,这里不深入讲解,因为还没有具体案例,暂时还未发现一个更好的落地方法,但是这应该是一个趋势。简单的图形方式表示一个实现关系:

代码可以借鉴这里体会一下:micro-frontends

但是这个方案只能认为它是一个 DEMO,并没有实际的项目价值,要运用的话还需要再结合场景重新思考。

以上是一些对组件的思考,不一定很深入,但是希望能够帮助到刚踏入组件化前端开发的小伙伴~

2017年4月2日星期日

React + Rx 模仿 Angular 模式

一、前言

其实对于软件开发模式来说,Angular 有着整套的一条龙服务,而 React 只是单纯的解决 View 层的问题,如果只是使用 React 开发项目会或多或少有点麻烦。下面主要讨论下数据层的东西,之前出了一个 mobx 来解决这个问题,但是这里想换个思路套用一下。其实 mobx 主要是声明了 @action, @computed, @observable 等元素来做到将 Store 作为一个可监控的源头,自动做到 VM 的效果,下面我也是,不过我采用 Rx 来做这个事情,简单利用下这种思想,能不能不污染我的原始数据(因为如果是双向的一种数据结构,那么就会被包装成监听类型的数据结构),这样我们调试的时候依然看到的是清晰的数据,并且还可以解决最初 Flux 库的一些麻烦,经过 Rx 改造的结果,可能比 mobx 的可测试性更好一些,具体就不说了,先谈 DEMO。

如果不想看文章的可以直接看代码 GitHub

2017年3月2日星期四

Rx 的编程方式(一)

1. Observables & Reactive

先来一个简单直观的例子:

const { Observable } = require('rxjs');

const source$ = Observable.of([1, 2, 3]);
source$.subscribe(x => console.log(x));

过滤器节点:subscribe

2. Declarative Transformation( 声明式转换 )

如果我们想要平时开发的数据转换功能,可以使用一些类似管道的工具来做。

Observable.of(1, 2, 3)
    .map(n => n * 2)
    .subscribe(x => console.log(x));

过滤器节点:map、subscribe。我所理解的就像一条溪流流动的水,然后 map 这类 API 就像水桶,将水装入进行加工,当然,其他 map 的特性先不用细致了解。

3. Lazy Transformation( 懒执行透明特性 )

Observable 能够在流动的过程中进行选择,所谓的懒特性就是不会像 Promise 一样,给了一个数据承认,就一定会让你接受,你可以选择不接受或者现在不接受,先这样子理解。

Observable.range(1, 100)
    // 转换管道
    .map(n => n * 2)
    // 拦截节点
    .filter(n => n > 4)
    // 懒获取
    .take(2)
    .subscribe(x => {
       console.log(x);
    });

4. DOM Event

Rx 其实是可以独立与任何框架使用了,也可以按需加载想用的方法,对于 Rx,其实和 Zone 一样,有了自己的保护圈和玩法,如果你要玩它,就需要进入它的圈子,大多数的相关 API 就是 from 的特性,对于 Rx 5.0 模块化做的更好,以及渲染 Timeline 上更可控。

<button id="btn">加</button>
<h1 id="out">0</h1>
const btn = document.getElementById('btn');
const out = document.getElementById('out');

/* eslint-disable no-undef */
const { Observable } = Rx;
Observable.fromEvent(btn, 'click')
    // mapTo 能映射一个常量,map 需要一个函数
    .mapTo(1)
    .scan((acc, cur) => acc + cur, 0)
    .subscribe(count => {
        out.innerHTML = count;
    });

其中主要依赖了:

rxjs/Observable.js
rxjs/observable/fromEvent.js
rxjs/operator/mapTo.js
rxjs/operator/scan.js

如果是打包的方式可以按需加载,如果是 script 可以直接引入 rxjs/bundles/Rx.js,不过会比较大,所以最好是按需提取到公用的 external 文件中,也能使用 lite 的版本,是常用的精简版。

如果有两个事件怎么办呢?利用流的合并特性可以做:

// 加法流,然后转换成数据 1 流入给下一个输入
const { Observable } = Rx;
const inSource$ = Observable.fromEvent(inBtn, 'click').mapTo(1);
// 减法流,然后转换成数据 -1 流入给下一个输入
const outSource$ = Observable.fromEvent(outBtn, 'click').mapTo(-1);
// 合并成最后需要操作的流
const source$ = Observable.merge(inSource$, outSource$);
// 操作流并且订阅流的 next 函数
source$.scan((acc, cur) => {
    if(acc + cur < 0) {
     return 0;
    }
    return acc + cur;
}, 0).subscribe(count => {            
    print.innerHTML = count;
});

5. Async HTTP

前端主要除了这种简单的数据操作,更多的是要看异步场景下的复杂度,下面用 Promiseasync/awaitObserable 三者的写法来对比。

(1) Promise方案

const data = fetchOrders().then(res => {
    if(res.status === 200) {
        return res.data;
    }
});
   
data.then(orders => orders.filter(order => order.text === 'Bob'))
    .then(orders => orders.map(order => order.id))
    .then(ids => console.log(ids));

function fetchOrders() {
    return axios.post('orders.json');
}

(2) async/await

function fetchOrders() {
    return axios.post('orders.json');
}

renderOrders();
async function renderOrders() {
    const res = await fetchOrders();
    if(res.status === 200) {
        const data = res.data;
        data.filter(order => order.text === 'Bob')
            .map(order => order.id)
            .forEach(id => console.log(id));
    }
}

(3) Observable

const { Observable } = Rx;

function fetchOrders() {
    const promise = axios.post('orders.json');
    return Observable.fromPromise(promise);
}
   
const hook =
    fetchOrders()
        .switchMap(res => {
            let data = [];
            if(res.status === 200) {
                data = res.data;
            }
            
            return Observable.from(data);
        })
        .filter(order => order.text === 'Bob')
        .map(order => order.id)
        .filter(id => id !== void 0)
        .subscribe(id => {
            console.log(id);
        });

6. Cancellation( 可取消的特性 )

其实这个特性很好用,可以随时取消你订阅的流,这是 Promise 做不到的,一旦发起承认就无法取消。

hook.unsubscribe();

7. Create HTTP Request

上面的 ajax 使用的是 axios 封装的,然后返回的是 Promise 的对象,如果要用 Observable 的生态,那么就需要利用 fromPromise 进行转化,这看上去不是特别好,所以能够直接利用 Observable 生态下的 ajax 进行操作。

与之前方案需要修改的地方有 2 个:
1. fetchOrders 函数请求 ajax 的方式
2. ajax 响应返回的对象不一样,毕竟之前使用的 axios

// 1. 使用 Observable.ajax
function fetchOrders() {
    return Observable.ajax.post('orders.json');
}

// 2. 修改一下 ajax 响应基础数据的处理
if(res.status === 200) {
    // res.data => res.response, ng2 如果不是 json 需要调用 json() 方法
    data = res.response;
}

8. Observable.create 的一些细节

(1) Obserable 的组成
我之前仿造过一个简单 Obserable 的实现:Obserable
这里用简单的结构来说一下:

class Observable {
    constructor (fn) {
        this.fn = fn;
    }
    
    static create(fn) {
        return new Observable(fn);
    }

    subscribe(next, error, complete) {
        if (typeof next !== 'function') {
            return this.fn(next);
        }
        
        return this.fn({
            next,
            error: error || () => {},
            complete: complete || () => {}
        });
    }
}

从上面代码可以看出来,构造器主要接收一个函数,然后 create 方法主要返回的是一个 Obserable 实例。然后 subscribe 方法接收的是三个参数,如果我们传入的是一个 function 那么就会调用 this.fn 传入一个对象,这个对象有三个属性值:next、error、complete,默认情况下 next 是必须传入的,其余的是可选,为了安全,自己手动创建的最好调用一下 complete

这样看上来其实也挺简单的。下来回到 rxjs 本身来看一下 create 的用法:

function fetchSomeone() {
    return Observable.create(observer => {
        observer.next('ok');
        observer.complete();
    });
}

fetchSomeone().subscribe(x => console.log(x));
// 或者如下写法
fetchSomeone().subscribe({
    next: (x) => console.log(x),
    complete: () => {}
});

这里用法比较简单,就是传入一个常量字符串 ok,然后在这个 Observable 被订阅的时候传给订阅者。

剩下的一些内容单独来看:
(1) Error Handle( 错误的处理机制 )
(2) Hot and Cold 数据流以及它们之间的转换关系
(3) 流节点之间的转换
(4) 流节点的并发问题

后面可能会比较细节,如果有兴趣可以直接看下面的 PPT,我主要也是按 PPT 的例子和思路来写。

参考的PPT: https://speakerdeck.com/jfairbank/devnexus-2017-the-rise-of-async-javascript

2017年2月16日星期四

编程

Everybody in this country should learn how to program a computer, because it teaches you how to think. - Steve Jobs

作为乔帮主的粉,很认同他对于自己产品热爱的那份情感。对于编程也一样,如果没有兴趣是肯定不行的,但是兴趣又是培养的,在编程过程中体验乐趣是很重要的。

读书的时候老师会教你程序设计的课程,我们这里把它叫做编程。比如可以抽象是:
Programs = Algorithms + Data Structures

I. 对于数据结构,可以用一个简单的例子来理解一下。

一堆书有两种简单放的方式:
1. 重叠放
2. 摊开放
语言的表达太淡,来个简单的图:

那么对于这里的数据结构就是这三本书放的方式,每一种方式都有各自的好处。

II. 那么算法是什么呢?

算法就是解决处理问题的策略或者说对信息的处理过程。我理解的算法是逻辑相关过程式的。
接着上面取书的例子,我要拿第二本书,具体过程是什么呢?
第一种方式我们要取第二本书的话(严谨一点的是在不移动当前书的空间位置的情况下),必须要把第一本书拿开。
第二种方式的话,我们只需要拿中间那本书就行了。

// 第一种方式
books: ["第一本书", "第二本书", "第三本书"]
needTakeBook: 2
takeBook: 1

while(takeBook < needTakeBook)
    do books -> take // 不断地拿书

books: ["第二本书", "第三本书"]
books -> take // 拿到第二本书
// 第二种方式
books: ["第一本书", "第二本书", "第三本书"]
books -> takeTheSecond

其实上面拿书的方式的代码就是实现了算法,算法本质上就是思考这个场景下的策略。
编程本身是抽象的,学会了抽象就学会了编程。

那么要一个好看能看的东西我们就需要换一些描述来显示出来。

生病了,随便写点吧~休息了。

2017年1月16日星期一

为什么需要 KeyMirror

前言

今天有朋友问了 “KeyMirror” 这个库有什么用的问题,其实这个问题并不难,这里扫一下盲区。

会按照下面这个逻辑来展开,彻底理解一下:

  1. KeyMirror 有什么用?
  2. Google Closure Compiler 是什么?
  3. KeyMirror 解决了什么问题,好处是什么?
  4. KeyMirror 的源码是什么样子?
  5. 用 Gulp 配置一个压缩任务,测试一下 Google Closure Compiler.

一、KeyMirror 有什么用

直观的来看一下,测试代码如下:

var kv = {
    GET_USER: null,
    SET_USER: null,
    REMOVE_USER: null
};

// keyMirror 是对应的测试方法
var kv_new = keyMirror(kv);
console.log(kv);
console.log(kv_new);

最后输出的结果如下:

kv_test1

然后就是相当于重新生成了一个 key == value 的结构。但是肯定就会想,为毛要多此一举呢?其实这个跟 Google Closure CompilerAdvanced 模式有关。接下来我们来看一下它是什么。

二、Google Closure Compiler 是什么

如果有兴趣的朋友可以直接跳到文章后面,使用 Gulp 把环境搭起来测试,因为下面的地址都要翻墙!

有官方文档,需要翻墙:文档

大致的意思就是说,Closure Compiler 是一个工具,这个工具能够编译 JavaScript 代码,编译后的代码能下载更快并且执行也更快。它能够解析你的 JS 代码,并且去分析它,能移除没有使用到的代码,重写、压缩得到最终的生产环境下的 JS 代码。它拥有检测语法、变量声明、类型定义以及对 JS 语言缺陷做一些检查。

总之,这就是做 JS 编译并且做一些常用检测的一款工具。

具体能够在线体验,也需要翻墙,在线体验地址

这个工具有三种模式:
1. 只去空格( Whitespace only )
2. 简单处理( Simple )
3. 最优处理( Advanced )

用 KeyMirror 的原因就是因为第三种(Advanced,最优处理)模式下,会将 Map<K, V> 格式的 K 进行压缩,比如:

// 源代码
var kv = {
    GET_USER: null,
    SET_USER: null,
    REMOVE_USER: null
};

// 编译后( 整理了一下格式,实际情况下会再添加压缩 )
var a = {
    a: null,
    c: null,
    b: null
};

在引用的时候就变成:

// 源代码
var kv_new = keyMirror(kv);
console.log(kv_new.GET_USER);

// 编译后
var a = keyMirror({ a: null, c: null, b: null });
console.log(a.a);

这样如果在没有进行 KeyMirror 处理的时候,引用就会错误了,这种编译模式破坏了我们的代码,要避免这个编译导致的 Key 改变,可以给 Key 添加引号(单、双均可),其实能够分析的就是静态的属性,动态基本上是不好做好的,可以这样理解。

// 源代码
var kv = {
    'STOP_USER': null
};

// 编译后
var a = {
    STOP_USER: null
};

然而我们这样做了之后,代码就得不到更有效的压缩,这样 Closure Compiler 的功能就被削弱了,所以引入 KeyMirror 既能保证代码前后的功能一致,也能享受压缩带来的性能提升。

三、KeyMirror 的源码是什么样子

既然知道了上面的背景和原因,我们来看下如何实现一个这玩意,其实特别简单的功能,就是让 K,V 相等。

var keyMirror = function(obj) {
    var ret = {};
    var key;
    
    // 对参数的控制,必须是对象
    if (!(obj instanceof Object && !Array.isArray(obj))) {
        throw new Error('keyMirror(...): Argument must be an object.');
    }
    
    // 简单的遍历,将对应 K 赋值给 Map[K]
    for (key in obj) {
        // 只拷贝自己的属性
        if (!obj.hasOwnProperty(key)) {
            continue;
        }
        ret[key] = key;
    }
    
    return ret;
};

四、Gulp 配置测试 Closure Compiler

这里需要用到两个东西:gulp、google-closure-compiler-js

直接上代码:

var gulp = require('gulp');
var compiler = require('google-closure-compiler-js').gulp();

gulp.task('go', function () {
    return gulp.src('./index.js')
        .pipe(compiler({
            // 编译等级,不区分大小写哈
            compilation_level: 'advanced',
            warning_level: 'VERBOSE',
            output_wrapper: '(function(){\n%output%\n}).call(this)',
            js_output_file: 'index.advanced.min.js',
            create_source_map: true
        }))
        .pipe(gulp.dest('.'));
});
// 测试代码
var kv = {
    GET_USER: null,
    SET_USER: null,
    REMOVE_USER: null
};
// 必须要和 keyMirror 代码一起,不然会被提示 error。
// Error: Compilation error, 1 errors
var kv_new = keyMirror(kv);
console.log(kv);
console.log(kv_new);

亲测非常的慢,不知道是不是我姿势不对,advanced 模式都是花费 12s 左右,simple 模式也花费 8s 左右,第一次测试我还卡挂了,所以基本上代码量上去了感觉是不适用的。

参考地址:

[1] https://developers.google.com/closure/compiler/

[2] https://github.com/facebook/react/issues/1639

[3] https://gist.github.com/zpao/d25251b139647a79cddf

[4] https://www.npmjs.com/package/keymirror

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 在这方面是不错的~