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);
        }
    });
},

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