显示标签为“blog”的博文。显示所有博文
显示标签为“blog”的博文。显示所有博文

2018年8月14日星期二

iTerm2 免密登录 SSH

Windows 上有 xshell 工具可以做收藏,但是 Mac 配置后可能更加方便和强大,网上有很多这方面的文章,这里自己也记录一下,方便以后查阅。下面介绍 2 种方式:

  1. sshpass
  2. expect

一、使用 sshpass

(1)安装
第一次需要安装 sshpass 执行脚本,这里使用 brew 进行安装。

brew install http://git.io/sshpass.rb

(2)验证

在终端输入 sshpass 如下输出帮助说明即安装正确:

$ sshpass

(3)查看安装路径

where sshpass
# 或者
which sshpass

# 我本地输出如下
/usr/local/bin/sshpass

通过上面的步骤后,下面进行 iTerm2 的相关配置。


(1)基本配置

打开 iTerm2 的设置,然后点击 Profiles,这里的 Profiles 就是我们自己收藏的一些配置,我这里以自己的腾讯云服务器 SSH 登录为例子。配置列表如下:

  • 其中最重要的就是 Command 表示启动 Profiles 时候自动执行的命令,这里我们使用 sshpass 来登录;
  • Name 表示配置的昵称,自己配置即可;
  • Tags 表示对 Profiles 进行分组,我这里设置为 云服务器
  • Badge 表示在 SSH 登录后,在终端的右上角显示登录的服务器ID,这里我设置为 QQ,主要用于标记(先要打开 Profile Name),可看下图。



基本配置就是这些,下面看一下 sshpass 的用法:

/usr/local/bin/sshpass -p <password> ssh -p<port> <username>@<ip>

这里 -p <password> 的方式能够使用 -f 读取配置文件的方式,可以将密码放在一个文件中,这里我建立一个:

touch ~/ssh_pass/qq_pass && echo "mypassword" > qq_pass

然后上面的 Command 配置就变成了:

/usr/local/bin/sshpass -f ~/ssh_pass/qq_pass ssh -p<port> <username>@<ip>

port 一般默认 22。这样就配置完成了,启动的时候就很方便了,直接点击 Profiles 即可登录。

其中 云服务器 就是分组,QQ-yun 就是具体的 Profile,点击即可登录。
也能够点击 Open Profiles 来查看配置列表,然后双击也能够登录,这能自己尝试。

这里要注意的是,第一次登录需要去授权 ssh,可以手动在终端登录一次,如果出现授权问题可以删除 known_hosts,然后重新授权,一般存在于:~/.ssh/known_hosts

二、使用 shell 脚本

上面是使用了别人写的东西,下面介绍一下简单的脚本来实现。这里不会只是使用 shell 执行的方式,会采用 expect,这个工具很实用,能够做终端的自动打包、备份、重启等功能。

(1)安装

brew install expect

(2)查看

# /usr/bin/expect
which expect

(3)实现一个 SSH 登录云服务器,命名为 qq.sh

#!/usr/bin/expect -f

# 设置超时时间,可以设置 -1 永不超时
set timeout 3
# 基本的服务器信息
set password <server_password>
set port 22
set user <your_username>
set ip <your_ip>

# 执行的命令
spawn ssh -p $port $user@$ip
# 交互式输出
expect "$user$ip's password:"
# 模拟输入密码,并且回车登录,这里有一定延迟,也可以手动自己回车
send "$password\r"
# 表示操作完成,用户继续可以交互操作
interact

(4)设置当前 qq.sh 为可执行文件

sudo chmod a+x qq.sh

(5)在终端输入 ./qq.sh 即可自动登录

注:如果提示 spawn 不存在,那么需要理解下 sh 的方式。如果是使用 sh qq.sh 的方式会让 #! 第一行命令声明失效,所以要采用可执行脚本的方式 ./qq.sh

(6)结合 iTerm2 来使用 shell

这个可以结合交互式参数来输入,也可以配置好 shell 过后直接在 Profiles 中的 Command 直接引入该 shell。

其实也有最方便的形式,就是写一个 shell 直接帮忙跑 ssh 即可,不过结合 iTerm2 应该是最方便管理的方式,还有分组、标记等功能。

2018年5月27日星期日

Angular6.x 热更新模式配置

前言

因为 angular-cli 默认是不会有热更新的效果,所以需要我们自己做一些配置。官方的 Wiki 更新其实没有完全的跟上,所以按照配置可能也不一定能正确实现,所以这里会将成功的配置说明一下。GitHub

版本

本地的版本信息如下:

Angular CLI: 6.0.5
Node: 8.10.0
OS: darwin x64
Angular: 6.0.3

具体配置

一、需要修改的文件

angular.json
package.json
src/environments/environment.prod.ts
src/environments/environment.ts
src/main.ts

二、需要新增的文件

src/environments/environment.hmr.ts
src/hmr.ts
src/typings.d.ts

三、修改的具体内容以及说明

  • 利用配置分离的方式来配置 hmr 的参数
// environment.prod.ts
export const environment = {
  production: true,
  hmr: false // 把热更新在生产模式关闭,当然你也可以不用,最好是统一配置
};
// environment.ts
export const environment = {
  production: false,
  hmr: false // 跟 prod 意思相同
};
// environment.hmr.ts
export const environment = {
  production: false,
  hmr: true // 开启热更新,这个配置是给 main.ts 代码使用来判断的参数
};

这里就修改好了参数和不同运行环境下的配置了,接下来要配置在不同的启动方式应用不同的配置文件。

  • 映射不同启动环境的配置文件

这部分需要修改 angular.json 文件。

// 1. 修改 JSON 路径:projects/<your_project_name>/architect/build/configurations
// 在这个配置下添加 hmr 属性的配置
// 这里应该看名字很直观,就是在运行的时候把默认配置用 .hmr.ts 文件的内容做替换来启动
"hmr": {
  "fileReplacements": [
    {
      "replace": "src/environments/environment.ts",
      "with": "src/environments/environment.hmr.ts"
    }
  ]
}

// 2. 修改 JSON 路径:projects/<your_project_name>/architect/serve/configurations
// 也是添加 hmr 属性的配置
"hmr": {
  "browserTarget": "angular-guide:build:hmr"
}

这里你会想,为什么不把 fileReplacements 配置到 serve 属性下,你可以试试,看下报错信息,会报错的哦~

在这里基本我们配置上就已经完成了,剩下是根据 webpack-hot 规则修改热加载的模块。这里使用最粗暴的形式,在应用最顶端进行热加载(这句话如果不能理解不影响最后结果)。

  • 映射热更新的代码

(1) 代码上有提供 @angularclass/hmr,所以需要先安装一下:

yarn add @angularclass/hmr --dev

(2) 接下来是模板代码,COPY 即可,就是接受热更新模块,直接替换我们应用中的组件模块。

// src/hmr.ts
import { NgModuleRef, ApplicationRef } from '@angular/core';
import { createNewHosts } from '@angularclass/hmr';

export const hmrBootstrap = (module: any, bootstrap: () => Promise<NgModuleRef<any>>) => {
  let ngModule: NgModuleRef<any>;
  module.hot.accept();
  bootstrap().then(mod => ngModule = mod);
  module.hot.dispose(() => {
    const appRef: ApplicationRef = ngModule.injector.get(ApplicationRef);
    const elements = appRef.components.map(c => c.location.nativeElement);
    const makeVisible = createNewHosts(elements);
    ngModule.destroy();
    makeVisible();
  });
};

(3) 修改 main.ts 的启动代码

const bootstrap = () => platformBrowserDynamic().bootstrapModule(AppModule);
console.log(environment); // 打印看环境文件是否加载正确
console.log('is hot: ', module.hot); // 热更新的参数

if (environment.hmr) {
  if (module.hot) {
    hmrBootstrap(module, bootstrap);
  } else {
    console.error('HMR is not enabled for webpack-dev-server!');
    console.log('Are you using the --hmr flag for ng serve?');
  }
} else {
  bootstrap();
}

这里你会发现会提示 module 这个变量不存在,所以需要安装一下全局类型定义 @types/webpack-env

yarn add @types/webpack-env --dev

最后在 src/typings.d.ts 文件中引用类型:

/// <reference types="webpack-env" />

这样就不会提示报错了,因为找到了类型声明文件。

  • 添加 npm script

配置其实是完整了,最后启动命令还是简单点:

"hmr": "ng serve --hmr --configuration=hmr"

2018年4月24日星期二

KMP(Knuth-Morris-Pratt)

前言

下面我将按这个目录来介绍 KMP 算法:

  • 字符串『前綴』|『後綴』
  • PMT(Partial Match Table)『最長公共前後綴』
  • Next 表
  • KMP 算法描述
  • KMP 程式

一、字符串『前綴』|『後綴』

KPM 其實是一種字符串匹配的算法,也就是說檢索字符串,那麼在學習它之前先了解一下『前綴』和『後綴』這 2 個概念。

1. 字符串『前綴』

a. 简单的解释
聲明 2 個字符串: A 和 B
定義它們的關係: A = BS (S 为任意的非空字符)
那麼就稱: B 是 A 的前缀

b. 舉例來看
現在有一個字符串:Hello
那麼它的前綴有:H, He, Hel, Hell

那麼把這個結果更結構化的表達一下:
"Hello" = { "H", "He", "Hel", "Hell" }

{} 這裡我們稱它為集合 => 字符串前綴集合

2. 字符串『后綴』

a. 简单的解释
聲明 2 個字符串: A 和 B
定義它們的關係: A = SB (S 为任意的非空字符)
那麼就稱: B 是 A 的后缀

b. 舉例來看
現在有一個字符串:Hello
那麼它的后綴有:ello, llo, lo, o

那麼把這個結果更結構化的表達一下:
"Hello" = { "ello", "llo", "lo", "o" }

{} 這裡我們稱它為集合 => 字符串后綴集合

其实这个概念还是挺简单的,我这里参考的是知乎的一个回答来解释一些概念,也许不严谨,但是我认为是易懂的。

不過按我查詢資料來理解 KMP 的時候,有外鏈最先不要跳躍出去,這樣保持一個思維來理解。

接下來將再看一個概念『最長公共前綴』,這個概念才是真正我們編程的時候需要尋找的部分。

二、PMT(Partial Match Table)『最長公共前後綴』

a. 解释和推导

這個概念用文字其實不是那麼好理解,如果硬要用漢字理解的話,就用拆字的方式:最長、公共、前綴、後綴,就是首先要找前綴和後綴,並且要是公共的,並且公共字符串的長度要是最長。

其實我覺得這個漢字解釋是錯的,既然那麼不好解釋,我們就用數學的方式來解釋看看,比較代數式的抽象度更高,更容易看懂。


前提:有一個字符串 \( P_j \)
目的:查找 \( P_j \) 中最長且相等的 k 前缀和 k 後綴

即:滿足條件的最大 k 值,使得,

\( P_0P_1P_2...P_{k-1}P_k = P_{j-k}P_{j-k+1}...P_{j-2}P_{j-1}P{j} \)


这样解释应该比较清晰了,当然这个东西也不是我自己写的,来自于算法老师 July CSDN 中的推导简化的。

b. 实例

模式串: abaabcaba

字符串 前綴 後綴 最长公共字符串 最長公共字符串長度
a 0
ab a b 0
aba ab ba a 1
abaab abaa baab ab 2
abaabc abaab baabc 0
abaabca abaabc baabca a 1
abaabcab abaabca baabcab ab 2
abaabcaba abaabcab baabcaba aba 3

根据上面的分析表,得出最终的表:

模式串 a b a a b c a b a
最大前缀后缀公共元素长度 0 0 1 1 2 0 1 2 3

c. 为什么需要 PMT?

看完這裡應該有個疑問:為什麼是這個步驟找出這樣一張表出來?這個就需要先了解一下由 PMT 表產生的 Next 表。下面我們先來看看 Next 表的相關東西。

三、Next 表

Next 表是由 PMT 表得到的,做到就是將 PMT 表中的每一個值向后移动 1 位,第一位賦值為 -1,為什麼是這樣得到 Next 表呢?接著看下去吧。

先按結論補充一下之前的表格,添加 Next 表

模式串 a b a a b c a b a
最大前缀后缀公共元素长度 0 0 1 1 2 0 1 2 3
Next -1 0 0 1 1 2 0 1 2

下面這張圖能很好的解釋為什麼我們需要 PMT 得到的 Next 表,這個表意味著我們能夠跳過一些不必要的字符,通過推斷的結構根據 Next 表的數據跳到已經確定的下一個比較位置,這里的 k 就是最長公共子綴的位置。

next

當我們在進行比較的時候,其實最後的『失配字符』(匹配失敗的字符)是 \( P_k \),所以真正相同的字符串是 \( P_0P_1...P_{k-1} \),那么我们能得到下面这个公式:

模式串向右移動的位數 := 已匹配字符數 - 失配字符的上一位字符所對應的最大長度值

模式串向右移動的位數 := j - next[j]

这个公式的解释依然出自于 July 的 CSDN 博客

以下是文字版公式:

\( S_0S_1 \qquad\qquad\qquad\qquad S_{i-k}S_{i-k+1}...S_{i-1} \quad S_i \\\)
\( \qquad P_0P_1...P_{k-1}P_k...P_{j-k}P_{j-k+1}...P_{j-1} \quad P_j \\\)
\( \qquad\qquad\qquad\qquad\qquad P_0 \quad P_1 \quad\ ...P_{k-1} \quad\ P_kP_{j-k}P_{j-k+1}...P_{j-1}P_j \)

這裡就能看出 KMP 的优势的,比如暴力的字符串匹配过程是需要每个字符都循环遍历进行比较的,如果遇到不匹配的时候总是移动一位然后再进行比较的过程。而 KMP 是可以跳跃一些不必要的匹配步骤的,时间就节约在这个地方,等會兒後面會分析一下最差情況的時間複雜度。

那么这里描述一下暴力匹配字符串的过程,有這麼 2 個字符串:

匹配串T: ababababab abaabcaba
模式串P: abaabcaba

1

這個很簡單,直接上代碼吧~因為要對比理解才能看出 KMP 節約的地方:

def index(source_string, type_string, pos):
    i = pos 
    j = 0

    while i <= len(source_string) - 1 and j <= len(type_string) - 1:
        if source_string[i] == type_string[j]:
            i += 1
            j += 1
        else:
            i = i - j + 1
            j = 0

    if j == len(type_string):
        return i - len(type_string)
    else:
        return -1

" output: 8 "
print index('abababababca', 'abca', 0)

這個代碼也是參考的博客python 的程式很直觀,就不翻譯成 JavaScript了。

四、KMP 算法描述

上面的準備工作已經差不多了,現在我們根據上面的 Next 表來描述一下算法過程:

  1. 如果 j = -1, 或者當前字符匹配成功(即S[i] == P[j]),都令 i++, j++,继续依次进行比较;
  2. 如果 j != -1, 且当前字符匹配失败(即S[i] != P[j]),令 i 不变,j = Next[j]。这个时候就要涉及到按照之前的公式进行移动,移动的位数为:j - Next[j]。

从上面的过程可以看出,我们算法是在解决 \( P_j \) 失配时候的问题。那麼這裡就是我們推斷算法複雜度的地方,算法最差的情況是模式串(P)在 i - j 的位置匹配完成,那么按匹配串(S)來看複雜度就是 O(n),如果算上 Next 表的過程 O(m),最後整體的算法複雜度為:O(n+m)。

五、KMP 程式

經過前面那麼多文字和公式的鋪墊,其實實現最後的程式并不複雜,程式包括 2 部分:

  1. 總的 KMP 算法實現過程
  2. Next 表的收集

Next 表的收集是有優化的地方的,優化點將以後再分享,這裡先實現它。

下面的 i 表示後綴索引,j 表示前綴索引。

def get_next_table(P):
    i = 0
    j = -1
    next_table = [-1]

    while i < len(P) - 1:
        if j == -1 or P[i] == P[j]:
            i += 1
            j += 1
            next_table.insert(i, j)
        else:
            j = next_table[j]

    return next_table

def index_kmp(S, P, pos):
    next_table = get_next_table(P)
    print next_table
    i = pos
    j = 0

    while i <= len(S) -1 and j <= len(P) - 1:
        if j == -1 or S[i] == P[j]:
            i += 1
            j += 1
        else:
            j = next_table[j]

    if j == len(P):
        return i - j
    else:
        return -1

"""
next_table: [-1, 0, 0, 1, 2, 3, 4, 0]
return: 2
"""
print index_kmp('ababababca', 'abababca', 0)

這裡的程式也是參考的網上的一篇文章

参考的太多了,贴一些列表吧:

這篇先這樣吧,之後再寫一些優化和自動機相關的內容呢。

2018年3月20日星期二

React 导读(六)

阅读完之前的第四和第五章,分享了 DialogTable 组件的一点设计,还有一些小组件的代码都已经上传到 Github-smarty 上去了,能够自己翻阅看一下,接着我们开始整合这些组件来合成一个业务模块。

一、理解容器组件、展示组件

在现在流行的方案中,Redux 的出现迎来了容器组件、展示组件等概念的流行,其实在这之前 Flux 的方案已经有了这类划分,Redux 应该是让它更知名了。

容器组件(containers/employeeManage/index.jsx):又称充血组件,主要用于一个顶层组件的数据获取,以及一些副作用数据、业务逻辑的处理,我这里的状态订阅代码都放在此处。

展示组件(components/employee/**/*.jsx):又可以称贫血组件,跟容器组件不同的是它几乎没有业务逻辑,只是通过传递的 props 进行渲染,更像一个纯函数,(props) => component

这里多联想一下,如果把这里的组件类型,脱离 View 层的限制去连接后端的分层设计,其实他们也是有充血模型和贫血模型的,但是唯一不同的是,后端的分层只是对数据的操作,可以简单分为:
充血模型:(Domain + Bussiness Logic) => End
贫血模型:(Bussiness Logic + Service) => (Domain Model) => End

上面主要是领域模型(Domain)以及业务逻辑(Bussiness Logic)的关系,区别的话简单可以理解成业务逻辑存在的位置,这里就跟前端的组件划分是一样的,如何更好的划分[业务逻辑]就是设计和开发组件的重点。

二、容器组件代码

容器就有一点打包的趣味,容器+组件,就是打包一些组件在一起,那么我们需要打包哪些东西呢?
我们需要打包业务模块的所有功能的入口组件,比如这里我的功能有:

  • 模块名称
  • 搜索、按钮等操作栏
  • 人员表格
  • 删除、添加、编辑弹框

入口代码就这样子:

<div className="mod">
    <ModTitle>员工管理</ModTitle>
    <EmployeeHeader onAdd={this.handleAdd} />
    <EmployeeTable loading={this.state.isLoadingData}
        data={this.state.list}
        onEdit={this.handleEdit}
        onDelete={this.handleDelete} />

    {/* 各种弹框 */}
    <EmployeeDeleteDialog visible={this.state.visibleDeleteDialog}
        data={this.state.currentSelected}
        onClose={this.handleCloseDeleteDialog}/>
    <EmployeeAddDialog visible={this.state.visibleAddDialog}
        onClose={this.handleCloseAddDialog} />
    <EmployeeEditDialog visible={this.state.visibleEditDialog}
        data={this.state.currentSelected}
        onClose={this.handleCloseEditDialog} />
</div>

从上面的代码可以看出,我这里的容器组件 render 的内容都是一些包装了的自定义组件,有很多 props 的传递。那么这些 props 的值在容器组件中是如何获取的呢?那就要用到之前我们解释数据流程的内容了,这里温习一下主要代码:

// 1. 先看一下我们容器组件依赖的状态 state
const state = {
    list: [],
    currentSelected: null,
    visibleDeleteDialog: false,
    visibleAddDialog: false,
    visibleEditDialog: false,
    isLoadingData: false,
};

// 2. 这里的 state 我是都放在 Store 中进行托管的,所以我们在容器组件构造器里面需要获取初始值
constructor() {
    super();
    // Store 有一个获取状态的 getter
    this.state = EmployeeStore.getState();
}

这样我们的数据就初始化好了,接下来就是监听来更改状态,然后 props 里面的值就有了更新。

componentDidMount() {
    EmployeeStore.on('loadingData', this.handleLoadingData);
    EmployeeStore.on('updateList', this.handleUpdateList);
    
    // 第一次先请求一次员工列表
    EmployeeStore.getList();
}

// 只要 emit('updateList', newData) 就会触发 React 的更新方法 setState 这样整个组件数据就重新更新了
handleUpdateList(list) {
    this.setState(prevState => {
        return {
            list: list,
            isLoadingData: false,
        };
    });
}

三、弹框

其实容器组件要做的工作我这里就差不多了,因为功能很简单。那么我这里只是处理了列表更新,对于列表数据的删除和添加等功能都没有做,这些副作用其实我是放在了弹框里面去做,无论你放在哪里都是可以的,看你项目的情况。我们这里只看一下添加弹框的内容,添加弹框是一个业务类型的组件,依赖的是 Dialog 基础组件的功能。

// addDialog/index.jsx
render() {
    if(!this.props.visible) {
        return null;
    }

    const footer = (
        <DialogFooter
            onSubmit={this.handleSubmit}
            onClose={this.handleClose}
            submitText="添加"
            closeText="关闭" />
    );

    return (
        <Dialog className="employeeAddDialog"
            renderHeader={() => <DialogHeader title="添加成员" />}
            renderFooter={() => footer}>
            <div className="addDialog">
                <Input onChange={this.handleChangeName} placeholder="请输入姓名" />
                <Input onChange={this.handleChangeDays} placeholder="请输入天数" />
                <Input onChange={this.handleChangeAge} placeholder="请输入年龄" />
            </div>
        </Dialog>
    );
}

上面的代码可以看出来,其实主要就是多了三个输入框来进行交互,最后这个添加弹框是有自己的状态的,不是一个纯展示组件,他的 state 如下:

state = {
    formData: {
        name: '',
        age: 0,
        sex: null,
        days: 0,
    }
};

这里我喜欢包裹一层 formData,用来单独就值我要跟后端对接的数据,可能需求还有变化,需要添加的数据并不想影响我的表单提交数据,而且更新也相对方便一点。

那么弹框的提交和关闭是怎么弄的呢?

handleSubmit(e) {
    const {formData} = this.state;
    // addEmployee 方法会 emit('updateList', newData) 来更新容器里面的状态
    EmployeeStore.addEmployee(formData);
    // 乐观交互
    this.handleClose();
}

handleClose(e) {
    // 把关闭状态给外面控制
    this.props.onClose();
}

这里可以看到关闭是通过 props 传递的一个容器方法来做的,因为弹框总体上来说,都是不具备重业务逻辑的一个组件,所以渲染的控制对外开放会更好,因为有时候万一你弹框是默认就要显示的就很别扭:this.state.visible = this.props.visible 这种代码就很蛋疼。所以干脆控制都让外面来做,要改变的给一个 onChange 的接口就行。

这里需要注意的就是添加弹框继承的是 React.PureComponent 来减少不必要的渲染,会进行一个浅的数据 Diff 控制更新。

细心的朋友可能会发现,我的 Input 组件写得比较恶心,为什么呢?我绑定了三个不同的 handleChangeXXX 这种方法,那么如何优化呢?可以思考一下哦~

四、展示表格组件

在这里我将表格做成了纯粹靠外界数据进行渲染的展示组件,将业务逻辑和行为几乎都给了容器组件去做,先来看一下目录结构:

├── dataTable
│   ├── config.js // 表格的配置,主要是 columns、格式化方法
│   ├── dataTable.css
│   └── index.jsx // 表格组件

根据之前的需求,表格这里具有:表头内容 两部分,这里表头能够根据我们的配置进行初始化:

// 具体的配置内容能够在 Github 上看一下
import config from './config';

const headers = config.table.map((row, index) => {
    let width = row.width;
    return <th onClick={row.onSort} style={row.style} className={row.className} width={width} key={`header-${index}`}>{row.title}</th>;
});
headers.push(<th width="160" key="opts">操作</th>);

表头初始化好了,开始初始化内容,表格的内容这里按行进行初始化,依赖了一个 SimpleRow 的业务组件来减少业务表格的代码量:

const rows = this.props.data.map((item, i) => {
    return (
        <SimpleRow key={item.id} tableConfig={config.table} row={item} index={i}>
            <td key={`opt-${i}`}>
                <Button style={{marginRight: 5}} color="blue"
                    onClick={() => this.props.onEdit(item)}>编辑</Button>
                <Button color="red" onClick={() => this.props.onDelete(item)}>删除</Button>
            </td>
        </SimpleRow>
    );
});

这样我们的一个表格就搞定了~加入 Table 组件就能够渲染一个简单职工列表了:

<div className="mod-table employeeTable">
    <TableLoading loading={loading} />
    <Table>
        <Table.Header>
            {headers}
        </Table.Header>
        <Table.Body>
            {rows}
        </Table.Body>
    </Table>
</div>

这里可以看到还有一个 TableLoading 组件,这也是一个偏业务的组件,主要就是用于表格加载数据时候的一个 Loading 行为。最后这个表格是搞定了,但是代码还是复杂了一点,那么怎么才能更简单的让别人用呢?让代码更少,O__O "…这个可以再思考一下哦~可以再进行一次封装。

PS: 有什么东西是分层搞不定的...如果搞不定,再分一层。哈哈,当然这是一个段子。

今天就写到这里吧,其实导读系列就先结束了,这里涉及的东西不多,但是对最初用 React 来编写代码还算比较有帮助的,后面会继续深入一点的话题,比如

  • 库类:热火朝天的数据状态管理、纯函数、流式、单向数据流、表单、验证器等等吧~
  • 脚手架:如何搭建一个自己心仪的玩具~
  • 框架:如何更工程的去开发 React 等内容~
  • 好玩的一些开发组件的理念~
  • 当 TypeScript 遇上 React 呀这种。

想到的时候有时间就写写。

PS: 系列写了六集 =.= 还蛮神奇的,我姓协音就是六。

2018年3月19日星期一

React 导读(四)

一、前言

React 导读(三) 中介绍了项目的背景、功能需求、项目结构以及组件的划分层次,接下来我们就来看下实际的代码,这一篇文章会主要分享用到的基础组件的封装。

二、基础组件设计

我们在设计组件之前本来是有一个流程和过程的,这里我写的组件并不会像社区内的组件库一样完善或者说一定考虑很完整,但是这样也会有一个好处,可以按照自己项目的需求进行定制、扩展以及冗余的代码会更少,当然很多时候节约的这点代码可以忽略不计(特别是项目业务代码和库的代码比例上升到一定比例过后,所以一切不说场景就说某某库太大的观点都是不正确的),因为大家都有按需加载的配置可选。这不是绝对的,不一定说你自己花时间和精力去开发一个这样的库就更好,因为随着项目规模的扩大,组件的种类和需求会越来越多,即使是一个不错的工程师利用技巧保障项目持续迭代,但是人的时间和精力是有限的,更合理的利用现有资源去提高效率才是最优先考虑的事情。

我这里的基础组件实现了这么几个:
Button, Dialog, Input, Loading, Table

然后分别来介绍一下如何基础开始封装和拆合组件。其实基础组件的设计是很杀脑细胞的,如果要考虑很周全的话,因为要兼顾别人用的爽,也尽可能要保留可扩展性,基础组件如果扩展性太弱,基本等于废了。其实如果有学习设计模式是可以相互连接的,因为设计模式是成熟的经验,不是说非要在写某种逻辑代码或者做架构设计的时候才能使用,它是能够贯穿在整个软件周期内的。

(1) 思考想要如何去组织组件样式

首先这里的组件是 css 和 js 最好能够分开使用的(这里的分开使用不是指传统意义的分离,而是保持独立,可进可退),拿以前 UI 需求来看,就是在同一个结构的 HTML,加上 class 都是能够正常展现的,我这里的 css 结构是以前用过的,也没做什么改动直接拿过来使用的。这种设计思路其实和现在的组件化开发是不冲突的,组件化后还能够使得这种模式更简单的被实现,因为你只需要考虑组件这个作用域内的样式。

(2) Dialog 组件
这里先拿 Dialog 组件先举例,这里我将弹框组件分成了三部分:DialogHeader, DialogFooter, Dialog 拆分上也没什么理由,这是一种简单直接的拆分,因为很多弹框都具有这么几部分:标题、内容、按钮区域。

而且不只是这样的才叫弹框,弹框如其名:弹出的框,所以都是可以的,比如下面这种像个 Alert 一样的弹框:

我理解的好扩展的组件就像小时候玩的玩具一样,各部分都是可拆解可组合的,所以弹框的这三部分都需要有一定灵活的地方。来看看代码,其实蛮简单的。

class Dialog extends React.Component {
    render() {
        const {
            renderHeader,
            renderFooter,
            className,
        } = this.props;

        const header = renderHeader ? renderHeader() : null;
        const footer = renderFooter ? renderFooter() : null;

        const wrapClassName = cx('st-dialog', className);

        return (
            <div className={wrapClassName}>
                <div className="st-dialogContent">
                    {header}
                    {this.props.children}
                    {footer}
                </div>
                <div className="st-dialogMask"></div>
            </div>
        );
    }
}

上面就是一个我这里 Dialog 的结构,headerfooter采用的是render-props的方式来实现具体的插入,为什么能够采用这种方式其实不难理解,因为在 React 中,一个函数就自然就是一个组件声明,返回值就能是一个组件实例。我这里更直接,你要组件返回值,我就给你一个组件...使用上就是这样:

// 这里结构稍微代码有点多,就不要揉在一起了,给一个变量存一下更清晰,在 React 中组件的使用是自由的。
const footer = (
    <DialogFooter
        onSubmit={this.handleSubmit}
        onClose={this.handleClose}
        submitText="添加"
        closeText="关闭" />
);

<Dialog className="employeeAddDialog"
    renderHeader={() => <DialogHeader title="添加成员" />}
    renderFooter={() => footer}>
    <div className="addDialog">...</div>
</Dialog>

为什么要这样设计呢?主要还是因为有时候需求不定,万一哪天 DialogFooter 组件不是这样子,我就在外面实现好组件给这个renderFooter就行了,其他部分就不需要改动,还有就是实现的时候不要吝啬div这种容器标签的使用,多一层就多一个权重,少一层就多了一份自由。

现在还有一个问题,就是我的基础弹框有了,业务弹框各种各样,这么简单的一个封装根本不靠谱啊...那么这里你就将弹框作为一个流行的渲染组件来使用,但是是否挂载到业务模块中就使用封装的一层业务弹框来控制,比如我的业务弹框叫 EmployeeAddDialog,在 render 方法中:

if(!this.props.visible) {
    return null;
}

return (<Dialog />);

通过一个 visibleprops 值来控制是否挂载 Dialog,那么这样做就有一个好处,在处理异步弹框的时候,想什么时候关闭弹框可以由业务的流程来控制。在业务组件声明业务弹框的地方就这样:

<EmployeeAddDialog visible={this.state.visibleAddDialog}
onClose={this.handleCloseAddDialog} />

然后这样就实现了一个弹框了,灵活性和扩展性都还好,最后还有一个细节就是这个EmployeeAddDialog始终都挂载在业务组件中,业务组件渲染一次这个弹框更新周期也会走一次,所以能够继承一下PureComponent来简单避免多次执行不必要代码:

class EmployeeAddDialog extends React.PureComponent { }

看上去这个弹框组件还算干净,不可能啊,业务太复杂也不会太干净,那么脏的东西去哪儿了呢?我这里有 2 个比较脏的地方:

(1) Footer,因为有按钮,每个需求的按钮是不一样的;
(2) 弹框的内容,这里就更是千奇百怪,每个产品经理的脑洞都不一样。

我这里应对上 Footer 有一定的定制又有简单的开关,比如我就支持2个按钮:提交类、关闭类。

弹框内容我控制不了,那么我就把代码是否更脏的职责交出去,使用了 this.props.children来做这个事情,使用者的代码干净度来决定最后的业务弹框干净度。

具体的全部代码能够在这里看到:

基础 Dialog

业务 Dialog

今天先到这里吧,睡觉了😪

React 导读(五)

React 导读(四)中分享了组件设计最开始考虑的一些事情,不能介绍太矛盾了,其实对于设计来讲是有正反两面的分析的,就跟评论历史事件来看都是要分两面进行分析的。今天我们接着分享剩下的内容,我觉得不一定要求多,但是一定要带着思考来写,有点意识流。

首先弹框和表格是最常用的组件,下面就介绍表格吧。

表格其实是很复杂的一个组件,我们先来看下成熟的表格,然后再来看下我实现的最简单的表格组件是什么样子的。这里就看下比较流行的 antd 吧,我不会分析他的实现和代码,因为这并不利于第一次学习一个组件的封装,一个成熟的组件一般是迭代出来的,越是迭代时间长的组件就更复杂,学习前应该是知道有一个东西,然后将这个东西简化,知道它是什么,然后进行扩展会比较好一点。

这里我将表格组件的实现分为 2 类:

  1. 配置式
  2. 组合式

2种没有哪一种好,各有优缺点:

配置式优点

  • 易于使用
  • 功能声明清晰,具有一定的整体性,代码也不分散

配置式缺点

  • 扩展、定制性其实是相对组合式较差
  • 递归性低于组合式
  • 组件结构非标记语言表达,在第一层阅读代码的时候主观上认为不够清晰

组合式优点

  • 组件结构标记语言实现,更清晰
  • 扩展性较强,不容易在代码里面形成较多的 if-else 式编程

组合式缺点

  • 使用上相比配置式复杂
  • 代码量较多

这里分的比较开,但是可以是配置+组合的方式来实现组件,做一个制衡。比如:

  • 表头无论是动态、静态构造,最后都是一个固定的配置式数据,也许你还能够自定义列展示;
  • 数据的结构 ID 与 Columns key 进行统一的配置,其实从某种意义上数据与列也是一种约定的模式;
  • 分页组件比较固定,就需要固定配置等等。

对于 antd-table 是依赖于 rc-table 组件进行封装的,props 主要有 2 个:columns、dataSource。

配置上的基础结构,我这里用自己的描述代码:

// 一个 JSON 的结构
ColumnItem {
    // String 表示 title 的数据类型是字符串
    title: String;
    key: String;
    dataIndex: String;
    // 表示 render 是一个非必填的配置,是一个函数,有 2 个值,可以返回一个 JSX 结构内容
    render?: (text, record) => JSX.Element;
}

Columns = ColumnItem[]

然后使用的时候通过把配置传递进去就返回了一个你配置的表格:

<Table columns={columns} dataSource={data} />

当然 antd 的体验是不错的,还支持将 Column 组件当作子组件的方式声明,也就是我说的配置+组合的一种结合,会将可读性增强、扩展外置的优点放大。具体可以参考官方文档,这里不能介绍太多:antd-table-cn-document

随着需求的扩展,<Table /> 组件需求逐渐增多,就会出现更多的依赖 props,也许会做更多的判断。虽然从需求上我们不能控制这种 if-else 的逻辑,但是我们能够通过平行组合的方式来减少单个组件的复杂度。这个是双刃剑,因为可能会有冗余。

平行组合的方式就好比有很多 if 的条件,根据组件的种类进行拆分,分成不同的组件,然后让使用者去决定用什么组件,这样一层一层的铺展使用,重的表格可能会有聚合的组件模式,这个又是另外一个问题了。

那么我这里例子里面用到的表格组件怎么弄的呢?我模仿的是 bootstrap-table 的配置方式,然后再配合 React 的一些组件特性拼起来实现的一个 Table 组件。

我这边实现的表格组件只有以下四个组件,其中 SimpleRow 组件是一个 Row 最基础的用法,满足基本的一个表格行。Table.HeaderTable.Body 分别是对表头和表内容的装饰,主要是样式和布局上的统一。

Table
    Table.Header
    Table.Body
    SimpleRow

具体的 Table.jsx 组件代码如下,基本等于就是一个壳:

export default class Table extends React.Component {
    render() {
        const {children, className} = this.props;
        const classNames = cx('st-table', className);

        return (
            <table className={classNames}>
                {/* 这里就意味着这个组件是层级 Wrap 类型的组件 */}
                {children}
            </table>
        );
    }
};

我们先看一下会如何使用表格吧。

<Table>
    <Table.Header>
        <th>...</th>
    </Table.Header>
    <Table.Body>
        <tr>...</tr>
    </Table.Body>
</Table>

React 组件是能够有命名空间的写法的,比如这里的 Table.<Someone> 的写法。那么我们要统一样式和布局,基本是等于就统一一下 class 就行,我们不能说让使用者自己每次去添加一些共有的代码,所以这里我们要用一个 API React.cloneElementReact.Children.map,后面 map 方法可能比较好理解,类似 Array.prototype.map,那么 React.cloneElement 的意思就跟它的表面意思一样:克隆一个元素(ReactElement),API 可以先看下官网 cloneElementApimapApi

先看看一下 Table.Body 的代码吧:

Table.Body = ({ children }) => {
    // 1. 先遍历孩子元素
    const cloneChild = React.Children.map(children, (child, i) => {
        if(!child) {
            return null;
        }
        
        // 2. 要更新的孩子组件的属性
        const wrapProps = {
            key: `st-table-row_${i}`,
            className: cx('st-table-row', child.props.className)
        };

        // 3. 返回一个新的 clone 的 React 元素,这里可以思考一下为什么要 clone
        return React.cloneElement(child, wrapProps);
    });
    
    // 4. 最后返回一个 tbody 原始的容器
    return (<tbody className="st-table-tbody">{cloneChild}</tbody>);
};

这里的代码可以看 github 上完整的源码:TableComponent

那么我们组件的结构通过组合的写法构造好了,那么我们的 Columns 在哪儿去声明呢?我们这里也是通过 js 数组对象的方式来声明,结构如下:

const table: [
    {
        field: 'id',
        title: 'ID',
        width: 50
    },
    {
        field: 'sex',
        title: '性别',
        format(value, row, index) {
            if(!value) {
                return '保密';
            }
            
            if(value === Gender.Girl) {
                return '女';
            } else if(value === Gender.Boy) {
                return '男';
            }
        }
    },
    ...
];

这里的配置怎么和表格的列连接起来呢?我这里将业务表格组件的目录划分成了这样:

├── dataTable
│   ├── config.js // 表格的配置
│   ├── dataTable.css // 表格的样式
│   └── index.jsx // 表格组件
// 初始化 header
const headers = table.map((row, index) => {
    let width = row.width;
    return <th key={`header-${index}`} width={width}>{row.title}</th>;
});
// 多一个操作列,这个操作列我这里是没有 antd 那么智能,识别到差 1 列的时候就成为了操作什么的
headers.push(<th width="160" key="opts">操作</th>);

// 初始化每行的内容
const rows = (this.props.data || []).map((item, i) => {
    return (
        <SimpleRow key={item.id} tableConfig={table} row={item} index={i}>
            {/* SimpleRow 通过传入的 TableConfig 和 Row 数据,初始化好了基本的行元素 */}
            {/* SimpleRow 也是一个 this.props.children 的结构 */}
            <td key={`opt-${i}`}>
                <Button color="blue" onClick={() => this.props.onEdit(item)}>编辑</Button>
                <Button color="red" onClick={() => this.props.onDelete(item)}>删除</Button>
            </td>
        </SimpleRow>
    );
});

这样回到最开始那个使用表格的例子,将 header, rows 都添加进入即可,最后整个表格 Row 的效果就是这样子,SimpleRow 的实现代码依然能在业务表格 Github 文件中找到:

这种写组件的方法特别费代码,所以是否可以考虑封装成一个业务通用的表格组件呢?这肯定是可以的,将变化的部分通过 props 传入即可,这个工作可以自己动手试试,在我的源码基础上进行修改。还有就是 tableConfig 里边的对象属性,你是能按自己需求进行添加的,可以在看看源码中的配置。

最后表格实现的效果,虽然没有排序、固定表头,但是用来展示信息是已经完善了,比如点击表头进行排序这个可以动手试试,在我组件的基础上应该比较好添加:


这里表格组件就实现了,你肯定会想,为什么没有 antd 那么方便实用和强大呢?

(1) antd-table 和这里介绍的 Table 组件层级是不同的,更接近的应该是 rc-table;
(2) 这只是针对最简单的表格使用场景进行设计的,按的是自由编写修改,并不是一个复杂场景的表格设计。

今天先写到这里吧~11点半了,休息一会儿。

2018年2月25日星期日

React 导读(三)

前言

React 导读(一)
React 导读(二)

在之前 2 篇文章中中学习到了写第一个 Web 组件以及常用的生命周期函数的使用,这篇文章将继续之前的目录,开始新的知识点补充:

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

这篇文章主要会介绍第6、7的知识点。

六 & 七、React 一个组件集合的简单交互以及开始一个项目的一点建议

为什么要将6、7合在一起写呢?不是因为想偷懒...其实是脱离一个场景和合适的开始去规划组件等设计都是不合理的,多多少少都有点交集,所以将这 2 点融合在一起是更利于学习和理解的,到这里就已经不是太基础的内容了,基本上代码量会有所提高,但是分析依然会很细致。

这里用一个简单的表格的添加删除编辑搜索四个功能来作为实例吧。
因为这应该是日常开发过程中遇到过程最多的,我将参考 bootstrap-table 的方式来开发一个简单的表格组件和约定配置来做,感觉比较自由,如果你动手能力好且业务稍大和复杂可以参考 antd 设计规范来实现,目前市面上应该蚂蚁这套用的比较多,但是这并不意味着我们就一定是按照他来做,实际项目不复杂的情况是可以使用更简单的方式。

做这个开始之前,首先要假设一点场景和基本需求,这样才能带着去思考如何实现以及更接近需求目标。

(1) 场景

为了更清晰的安排年前年后的工作和值班,现在要对过年期间人员请假的情况进行统计,并且进行一个简单的管理。

(2) 功能性需求

  • 添加员工的请假信息
  • 展示添加的员工请假的列表
  • 能够对信息进行修改
  • 能够删除添加的信息,由于不可恢复,所以需要一个提示
  • 能够根据员工的名字进行搜索

简单描述了一下,其实就之前说的几个功能。

最后做出来的效果如下(=.=没有设计,对齐就行哈):

看之前可以下载源代码对照着看,不过代码可能会不断修改 BUG,哈哈~有 BUG 不要虚,没有 BUG 我们可能就失业了。
源码-GitHub

(3) 准备工作

  1. 整理需要用到的技术
  2. 开发要用的基础 UI 组件
  3. 看下 bootstrap-table 的基本设计
  4. 搭建项目目录

1. 需要用到的技术
需要用到的技术:React/ES6, CSS 即可

2. 基础 UI 组件
根据我们这里的功能来看,我们只需要下面这几个基础组件即可:
Button, Dialog, Input, Table, Radio

在这个例子项目里面,组件的划分结构如下:

为什么要这样划分呢?

  • 基础组件:其实这个是每一个项目都需要的,如果说太小的项目不需要其实大多数是考虑掉了项目的迭代周期的考量以及以后代码的可复用性,顾名思义,基础组件就是你要在以后的组件编写过程中需要依赖的最基础的组件,基本是只负责 UI 层面的职责,当然你还能够再剥离,这里就不太展开了,知道这一层是为了以后写组件能够有自己的基础组件即可。

  • 业务组件、模块组件:在我们开发好基础组件过后,其实这些基础组件是不具备任何业务价值的,比如有了业务设计稿后,我们需要针对业务然后编写业务中公用的组件或者封装使用操作2次的组件代码,形成一个可复用的业务组件或者业务模块类型的组件。比如我这里会将每个模块用到的模块标题封装成一个 ModTitle 组件,这样以后修改这里样式的时候全部就在一个地方修改,或者在业务系统上会有 Layout 相关的布局组件需求,再比如系统中表格整个一块的需求,包含搜索、头部操作按钮、数据展示表格,这三者能够进行一个通用性的封装来形成业务模块上的表格使用组件,增加编写模块的效率,当然这里我并没有封装,因为封装和重构并不是软件初始开发更应该注重的,而是遇到第二次的时候再反过来思考如何避免重复或者让组件内部封装。

  • 展示组件、容器组件:这一层就是网上流行的展示型组件、容器组件的一层,我这里划分主要是跟具体业务功能有关系的一层。由于我这里没有 react-router,所以复杂度会低一些,后面有时间也可以介绍。

3. bootstrap-table 生成表格的方式

可以查看 github-bootstrap-table 的使用例子来看下使用方式,这里我用它做例子并不是说此库完全好或者不好,而是以前项目用了 bootstrap-table 然后就模仿了 columns 配置的方式,对于它 API 设计的其他部分暂时没采用。
表格组件其实是管理类系统很核心的部分,一是用的多,二是本身也比较复杂,封装太死缺少灵活性,封装太简单缺少效率,种类也比较多。大体上我会采用字段进行配置的方式,具体看后面的代码和分析。

4. 项目的目录规划
上面介绍了一些概念性的东西,那么项目主要的目录单独提一下,这里的项目目录不适合大型项目,但是需要一个这个过程,来理解每一项的意思以及为什么我们还需要其他技术来解决你遇到的问题,堆技术的做法是不可取的,至少在不疯狂 KPI 模式的情况下。

├── public
│   ├── favicon.ico
│   ├── index.html
│   └── manifest.json
├── server // 网站后端的目录,这里我们不需要关系
├── src // 前端的源代码目录
│   ├── App.js // App 的入口组件
│   ├── apis // API 请求层的相关文件,Ajax 的方法也是需要适配的,比如常见的拦截器做法
│   ├── app.css
│   ├── assets // 一些静态资源
│   ├── components // 包含了业务组件、模块组件、展示组件,这里项目较小的时候不需要划分太细,但是要有这样的分层来组织代码
│   ├── containers // 容器组件,主要的副作用等逻辑组件,基本上是数据初始化、维护一个较顶层的数据入口
│   ├── index.css
│   ├── index.js // 网站的入口 JS 文件,主要是负责组件挂载到 DOM,或者你也可以做一些全局注入的一些操作
│   ├── normalize.css
│   ├── registerServiceWorker.js
│   ├── smarty // 基础组件的目录,这里我叫它 smarty,命名空间使用 st-,这个随你高兴
│   ├── stores // 数据操作的主要聚焦地方,每一个 Store 都能是一个事件订阅者,用于连接 React View 组件
│   └── utils // 一些工具辅助函数,目前我这里没有使用,真实项目肯定会用上的

(4) 开始思考要如何开始写代码

1. 需要一个 React 的容器组件来渲染我想要的一个功能模块;
2. 功能模块的数据需要一个地方进行管理。

要解决第一个问题,假设我们的容器组件叫 EmployeeManage,那么在最外层的 App 组件中应该声明要渲染它,代码就会是这样:

class App extends Component {
    render() {
        return (
            <div className="App">
                <EmployeeManage />
            </div>
        );
    }
}

好了,假设这样会出现最初的那个效果图的样子,那么数据并不想写的太过于零散,所以我定义了一个 Store 类进行管理,为什么是类呢?现在不是流行 Redux 之类函数式的么?一是在最开始学习的时候增加太多技术栈心累,二是不一定要用 Redux 我们才能写好 React,三是感觉也不太必要就我们目前的需求来看,四是我就想最初简简单单的。
但是现在我们是数据驱动方式的编程,数据变了来通知 React 的 state 变了然后 React 去帮我们做视图的更新,所以,我们的 Store 得是一个基于事件的类,要有事件应该有的特征:监听。所以最后我需要一个 EmployeeStore

// 用下自带的,你也可以自己实现一个简单的
import EventEmitter from 'events';
import assign from 'object-assign';

const state = {};

const EmployeeStore = assign({}, EventEmitter.prototype, {
    // 把容器组件的 this.state 在这里管理
    getState() {
        return state;
    }
});

原始是原始了一点,但是应该很好理解,那就是我的 EmployeeStore 拥有了 EventEmitter.prototype 的东西,比如常用的 on(), off(), emit() 等方法来实现事件特性。
然后我们需要把 EmployeeManageEmployeeStore 连接起来,最简单的连接像这样子:

class EmployeeManage extends React.Component {
    constructor() {
        super();
        // 看这里
        this.state = EmployeeStore.getState();
    }
}

连接了这个基础的东西,我们的 EmployeeStore 不是还可以订阅事件么?然后数据修改了我们就触发一下订阅的事件去告诉 EmployeeManage 然后通过 this.setState 去更新视图即可,整个关系如下:

看图可能就更直观的知道数据和组件之间的关系了,用过 Flux 可能可以发现还比较像,但是这是两个不同的理念,我这里只是一个最基础的事件系统,所以会特别简单。

我们现在来订阅一个名为 updateList 的事件,用来表示表格中需要展示每条数据。我们需要在 EmployeeManage 中加入下面的代码:

componentDidMount() {
    EmployeeStore.on('updateList', this.handleUpdateList);
}

componentWillUnMount() {
    EmployeeStore.off('updateList', this.handleUpdateList);
}
    
handleUpdateList(list) {
    this.setState(prevState => {
        return {
            list: list,
        };
    });
}

这三个方法能跟上面的图对应一下,就对应上了 EmployeeManageComponent,那么我们的 Store 需要怎么做呢?

getList() {
    // API 请求列表数据的方法,返回一个 Promise
    EmployeeApi.get().then(result => {
        if(result.status === 200) {
            // 刚好,就通知了 EmployeeManage 说我数据获取成功了,可以更新视图了
            this.emit('updateList', result.data);
        }
    });
},

以上就完成了连接工作了,基本上剩下的就是码代码,往上累积功能。
先写到这里吧,太长看着也累,分一下章节吧~其实架子已经差不多了,剩下的就是写功能点了。如果觉得看文章太慢可以直接看源码可能会更快更直接一点,没有数据层,其实并不是太好,先理解视图和关系吧。

2018年2月3日星期六

React 导读(二)

前言

在上篇文章React 导读(一)中学习到了写第一个 Web 组件,这篇文章将继续之前的目录,开始新的知识点补充:

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

五、React 中最开始需要关注的生命周期

其实在学习 React 之前,就应该了解目前前端推荐的是组件化开发的方式,React 是让组件化更加简单的库。那么组件开发必不可少的就是生命周期,说直白一点就是运行组件的过程是 React 来做,运行过程中需要有一些代码钩子来让我们去调用,在组件执行的某一个地方去执行我们自己写的代码。这里先介绍拥有的生命周期钩子,下面的方法 constructorrender 不属于生命周期,我按功能分类了一下,也就是学习的时候不一定要按部就班,应该以学习之后能真正写一些东西为目标:

(1) 与组件挂载相关的方法,包括构造函数

  • constructor
  • componentWillMount
  • componentDidMount
  • componentWillUnmount

最常用的生命周期应该是最后 2 个,constructorcomponentWillMount 目前先理解成能满足的功能大体相同,如果这里解释太复杂不太好。

对于最开始关注的是:this.state 的初始化以及 ajax 在哪里请求。

this.state 在 constructor 进行初始化,ajax 推荐在 componentDidMount 中进行请求。

  • componentDidMount 就是在组件已经挂载到 DOM 中后的钩子,可以理解为 jQuery 中提供的 ready 方法。
  • componentWillUnmount 是在组件即将被卸载前一刻的钩子,一般用于取消 componentDidMount 中订阅的事件等作用,清理一些不要的变量等,避免内存泄漏。

下面通过一个简单的例子说明一下:

先有一个 foods.json 文件来模拟请求的后台数据。

[
    {
        "id": 1,
        "name": "香蕉"
    },
    {
        "id": 2,
        "name": "苹果"
    },
    {
        "id": 3,
        "name": "猕猴桃"
    }
]
// 1. 挂载相关的生命周期
class CycleMount extends React.Component {
    constructor() {
        super();
        this.state = {
            foods: []
        };

        console.log('1. constructor 执行了...');
    }
    componentDidMount() {
        // 这里使用原生的 fetch API 进行 ajax 请求,你也可以使用 $.ajax 进行请求,原理是一样的,重点是关注 setState 的地方
        fetch('/mock/foods.json',
            {
                method: 'GET',
                headers: new Headers({
                    'Accept': 'application/json'
                })
            }
        ).then(dataResult => {
            if(dataResult.status === 200) {
                return dataResult.json();
            } else {
                return [];
            }
        }).then(data => {
            // 这里的 data 就是 foods.json 里面的数据
            // 调用 setState 来更新 render 里面调用的 this.state 的值
            this.setState({
                foods: data
            });
        });

        console.log('2. componentDidMount 执行了...');
    }
    render() {
        // foods 是一个数组,map 方法是数组自带的方法,可以查询相关 api
        const foodItems = 
            this.state.foods.map(food => {
                return (<li key={food.id}>{food.name}</li>);
            });

        // 这里是返回的最终组件结构
        return (
            <ul>
                {foodItems}
            </ul>
        );
    }
}

上面有了完整的注释,也能看到基本上项目中可能会将代码写到何处,我也打了两个日志,来识别到底是谁先执行,结果可以自己运行一下,执行顺序就是我标记的1,2。

好了,基本的学习了,可以自己动手试试订阅一个事件,然后在卸载的时候取消这个事件。

(2) 优化相关

  • shouldComponentUpdate

这个方法比较重要,但是我这里不会介绍得太过于复杂,太复杂只会让重要的部分不那么突出。这个方法的返回值是一个 boolean 类型,分别代码的意义:

  • true 组件应该更新,执行 render 方法以及相关生命周期;
  • false 组件状态没有更新,不执行 render 等方法,意味着网页界面不会改变。

那么它直观上的作用是能够通过返回值来决定界面是否改变,实际的意义就是当我们知道当前 oldState = this.state 的值和新的 newState = this.state 值完全相等的时候(或者是新传入的 props)就不用再浪费性能去重新渲染组件了。

API 上的定义是这样的:

/* nextProps: 新的 props, nextState: 新的 state */
shouldComponentUpdate(nextProps, nextState): boolean

举个例子来直观说明一下:


class ComponentOptimize extends React.Component {
    state = {
        count: 0
    };
    shouldComponentUpdate(nextProps, nextState) {
        // 当 count > 10 的时候就不能再重新渲染组件了
        if(nextState.count > 10) {
            return false;
        }

        return true;
    }
    handleUpdateCount() {
        console.log('我点了一下哦!');
        this.setState(prevState => {
            return {
                count: prevState.count + 1
            };
        });
    }
    render() {
        return (
            <div onClick={this.handleUpdateCount.bind(this)} style={{cursor: 'pointer'}}>
                <h1>{this.state.count}</h1>
            </div>
        );
    }
}

你会发现 10 过后界面就没有再更新过了,这样应该很直观了。

(3) Props 相关的生命周期

  • componentWillReceiveProps

这个主要是在组件的 props 传入新的值后被调用,不管是不是传的一样的值或者 shouldComponentUpdate 返回了 false,看下例子吧:

class Cat extends React.Component {
    componentWillReceiveProps(nextProps) {
        console.log('改一次我执行一次!');
    }
    shouldComponentUpdate(nextProps, nextState) {
        // 改的名字一样的时候
        return this.props.name !== nextProps.name;
    }
    render() {
        console.log('猫改了一次名字!');
        return (
            <h1>我有新名字了!{this.props.name}</h1>
        );
    }
}

class App extends React.Component {
    state = {
        catName: '噜噜'
    };
    handleChangeCatName() {
        const catNames = ['噜噜', '小白', '小黄', '小黑', '皮卡丘'];
        const catIndex = this.getSomeOneIndex();

        this.setState({
            catName: catNames[catIndex]
        });
    }
    getSomeOneIndex() {
        return Math.floor(Math.random() * 5 + 0);
    }
    render() {
        return (
            <div>
                {/* 给 Cat 传新的名字 */}
                <Cat name={this.state.catName} />
                <button onClick={this.handleChangeCatName.bind(this)}>点我给猫咪取新名字!</button>
            </div>
        );
    }
}

最后肯定每次点击按钮都会输出这句的结果 console.log('改一次我执行一次!');

(4) 更新组件相关

  • componentWillUpdate
  • componentDidUpdate

因为都是讲解 API,所以国际惯例的先看下 API 的定义吧:

// 组件更新前执行
componentWillUpdate(nextProps, nextState)
// 组件更新后执行
componentDidUpdate(prevProps, prevState)

可以从定义中看出,它们都接受了两个参数:props && state,不过看变量前缀能够联想点什么。

暂时想不到什么实际项目的例子,随便假设点内容吧。不过这里需要注意的地方是:

  • 1. 这两个方法里面都不要调用 setState!
  • 2. 第一次初始化组件 render 的时候不会执行
  • 3. shouldComponentUpdate 返回 false 不会执行

第一条的原因:容易形成一个递归的调用,不作就不会死...所以尽量不要在这里调~目前还没有碰到需要在这里调的需求。
第二条的原因:额,说好的更新才调,初始化不调用是符合逻辑的。
第三条的原因:额,这 2 个钩子是与组件更新相关的,所以也符合逻辑的,组件是否更新就是靠 shouldComponentUpdate 返回值。

在上面 Cat 的例子中加入下面的代码可以看下结果:

componentWillUpdate() {
    console.log('componentWillUpdate 执行了!')
}
componentDidUpdate() {
    console.log('componentDidUpdate 执行了!')
}

(5)组件错误

  • componentDidCatch

就是在组件发生异常的时候可能会被调用的钩子,需要注意的有下面的地方:

  • 只能在父级组件捕获子组件的异常;
  • 如果异常被 try...catch 包裹父级组件的钩子就不会执行了。

看个例子吧:

class Cat extends React.Component {
    componentWillReceiveProps(nextProps) {
        // 这里手动抛一个异常,触发我们的钩子 componentDidCatch
        throw new Error('miao miao~');
    }
    render() {
        let miao = this.props.name;

        return (
            <div>
                {miao}
            </div>
        );
    }
}

class App extends React.Component {
    state = {
        catName: '噜噜',
        isError: false,
    };
    handleChangeCatName() {
        const catNames = ['噜噜', '小白', '小黄', '小黑', '皮卡丘'];
        const catIndex = this.getSomeOneIndex();

        this.setState({
            catName: catNames[catIndex]
        });
    }
    getSomeOneIndex() {
        return Math.floor(Math.random() * 5 + 0);
    }
    componentDidCatch(error, info) {
        console.log(error, info);
        if(error) {
            // 如果有错误信息,就重新渲染一下组件,可能是更好的交互
            this.setState({
                isError: true
            });
        }
    }
    render() {
        return (
            <div>
                <Cat name={this.state.catName} />
                {!this.state.isError ?
                    <button onClick={this.handleChangeCatName.bind(this)}>点我给猫咪取新名字!</button> :
                    <p>不要奴才给我取名字了!</p>
                }
            </div>
        );
    }
}

(6) 渲染相关

  • render

额...这个不想写了,先睡觉吧~应该写了这么多个小例子也差不多了~可以动手试试哦!还不清楚的可以 Google 一下,你就知道。

生命周期很重要,其实学到这里也差不多可以上手写点项目熟练一下了,其他的更多是思维和编程方面的东西,周期的篇幅单独来一篇吧~其他主题之后再继续吧!

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