---
tags: substrate-中文
---
# 用 Substrate Front-end Template 轻松打造你的 React 应用
*Substrate 前端开发系列 - 2/2*
## 前言
前端开发系列第一篇讲了如何用 Polkadot JS API (简称 JS API) 来搭建前端。如果你的前端是用[React](https://reactjs.org/) 或它家族的框架来写,那可参考今天要深入讨论的另一个官方项目 [Substrate Front-end Template](https://github.com/substrate-developer-hub/substrate-front-end-template)。这个项目是官方支持的,它把 JS API 封装好在 [React 应用]里,并对常用的操作进行了封装,放在不同的组件内使用。使你在前端开发中更专注页面和用户的互动,省却一些力量处理㡳层如何和 Substrate 节点交互。
接下来,我们会手把手在本机跑起 Substrate 节点,前端模版。然后查看模版代码是如何查询链上数据,提交交易,最后也说明如何查询链上自定义的数据类型。
## 连接到本机开发节点
因为我们也需要一个 Substrate 节点,我们会同时 git 克隆一个 [Node Template](https://github.com/substrate-developer-hub/substrate-node-template) 及 [Front-end Template](https://github.com/substrate-developer-hub/substrate-front-end-template) 下来。
```bash
mkdir ui-tutorial
cd ui-tutorial
# -- 安装 Rust 及 Substrate
# 更详细的安装指引可参考:
# https://substrate.dev/docs/en/overview/getting-started
curl https://getsubstrate.io -sSf | bash
# -- 下载 Substrate Node Template,编译并运行起来
git clone https://github.com/substrate-developer-hub/substrate-node-template node-template
cd node-template
# 这指令会编译 Substrate, 须时 30 - 45 分钟不等
cargo build --release
# 运行以下指令会占据整个终端,直到 Cmd+C / Ctrl+C 停止
target/release/node-template --dev
```
如一切无误,你在终端会看到类似如下的输出:

按下 `Ctrl+C` / `Cmd+C` 退出。
要重置本机区块链,则输入:
```bash
target/release/node-template purge-chain --dev
```
接下来安装 Front-end Template:
```bash
# -- 安装 NodeJS v12 (https://nodejs.org/en/download/),
# 及 yarn 工具 (https://classic.yarnpkg.com/en/docs/install)
# -- 下载 Substrate 前端模版并跑起来
cd ..
git clone https://github.com/substrate-developer-hub/substrate-front-end-template front-end-template
cd front-end-template
yarn install
yarn start
```
接着,访问 http://localhost:8000,你会见到以下页面:

而图中右上方的当下区块生成数也和 Substrate 节点终端所显示的一致 (请确定本机 Substrate 节点在运行着,即上面 `target/release/node-template --dev` 那句)。如果是这样,那恭喜你,你已成功跑起一个 React 的前端,并成功连接到本机 Substrate 节点上。
如果你打开浏覧器的开发视窗 (Developer Console), 你会注意到一行 console log 说明已连接的远端 socket。

## 调整远端连接的 Substrate 节点
如果需要调整要连接的终端节点,可打开 `src/config/development.json` 来指定你的终端。
*源码:[`src/config/development.json`](https://github.com/substrate-developer-hub/substrate-front-end-template/blob/master/src/config/development.json)*
```json
{
"PROVIDER_SOCKET": "ws://<你的 ws/wss 地址>"
}
```
## SubstrateContext 与 useSubstrate
与 Substrate 节点的交互是封装在 [React Context](https://reactjs.org/docs/context.html) 内, 以 [Hook](https://reactjs.org/docs/hooks-reference.html#usecontext) 形式供开发使用. 所以首先我们在主要 `<App>` 外包一层 Context Provider.
*源码:[`src/App.js`](https://github.com/substrate-developer-hub/substrate-front-end-template/blob/master/src/App.js)*
```javascript=
//...
import { SubstrateContextProvider, useSubstrate } from './substrate-lib';
import { DeveloperConsole } from './substrate-lib/components';
//...
function Main() {
const [accountAddress, setAccountAddress] = useState(null);
const { apiState, keyring, keyringState } = useSubstrate();
const accountPair =
accountAddress &&
keyringState === 'READY' &&
keyring.getPair(accountAddress);
//...
return (
)
}
export default function App () {
return (
<SubstrateContextProvider>
<Main />
</SubstrateContextProvider>
);
}
```
在 `<SubstrateContextProvider>` 的子组件内,就能用 `useSubstrate()` 来取得整个 Substrate Context, 当中包含了:
- `socket`: 对应现在连接的远端
- `types`: Substrate 网络内的自定义结构组
- `keyring`: 储存着用户帐号(用户公钥),也开放出接口来为数据和交易签名
- `keyringState`: 用户帐号状态,为 [`null`, `'READY'`, `'ERROR'`] 其中一个
- `api`: Polkadot-JS API
- `apiState`: Polkadot-JS API 对远端的连接状态,为 [`null`, `'CONNECTING'`, `'READY'`, `'ERROR'`] 其中一个
我们检查着 `apiState` 及 `keyringState`,当它们的值都为 `'READY'` 时,我们就可以开始读取链上数据。
## 读取及订阅链上数据 (Queries)
接下来我们会专门讨论 在 Front-end Template 最下面的一个模块 [`src/TemplateModule.js`](https://github.com/substrate-developer-hub/substrate-front-end-template/blob/master/src/TemplateModule.js)。

这个模块虽然看似简单,但已包含了读取链上数据,提交交易,及监听事件。这前端模块对应着后端 Substrate 节点的一个模块 Template Pallet ([查看Pallet 源码](https://github.com/substrate-developer-hub/substrate-node-template/blob/master/runtime/src/template.rs))。它有着:
- 一个存取项 **`Something`**
- 一个读取接口 **`something`**
- 一个外部交易接口 **`do_something`**
我们继续看回前端源码。先看介面部份:
*源码:[`src/TemplateModule.js`](https://github.com/substrate-developer-hub/substrate-front-end-template/blob/master/src/TemplateModule.js)*
```javascript=
import { useSubstrate } from './substrate-lib';
import { TxButton } from './substrate-lib/components';
//...
function main (props) {
//...
return (
<Grid.Column>
<h1>Template Module</h1>
<Card>
<Card.Content textAlign='center'>
<Statistic
label='Current Value'
value={currentValue}
/>
</Card.Content>
</Card>
<Form>
<Form.Field>
<Input
type='number'
id='new_value'
state='newValue'
label='New Value'
onChange={(_, { value }) => setFormValue(value)}
/>
</Form.Field>
<Form.Field>
<TxButton
accountPair={accountPair}
label='Store Something'
setStatus={setStatus}
type='TRANSACTION'
attrs={{
params: [formValue],
tx: api.tx.templateModule.doSomething
}}
/>
</Form.Field>
<div style={{ overflowWrap: 'break-word' }}>{status}</div>
</Form>
</Grid.Column>
)
}
```
从上面能看到显示的数值是存在 `currentValue` (第15行)内,可用 `setCurrentValue()` 来更改此值。(这是基于 [React 的 State Hook](https://reactjs.org/docs/hooks-state.html)。而此 `currentValue` 的窍门在于它是如何被初始化及修改的。那得跟着看接下来的 `useEffect`。
```javascript=
useEffect(() => {
let unsubscribe;
api.query.templateModule.something(newValue => {
// The storage value is an Option<u32>
// So we have to check whether it is None first
// There is also unwrapOr
if (newValue.isNone) {
setCurrentValue('<None>');
} else {
setCurrentValue(newValue.unwrap().toNumber());
}
}).then(unsub => {
unsubscribe = unsub;
})
.catch(console.error);
return () => unsubscribe && unsubscribe();
}, [api.query.templateModule]);
```
`api` 是从 `useSubstrate()` 取得的 JS API 接口。 `api.query.templateModule.something` 就是我们从 Substrate 节点处取得数据的方法。而 api query 的用法法则是:
```javascript
api.query.<pallet_名字>.<pallet 存储名字>(回调函数)
```
这是对应着 Substrate 后端的 runtime 有个 TemplateModule 为名字的 pallet 模块。而这模块的存储内有个 `something()` 的读取函数。因为在 node-template 内是如下定义的:
*源码: [`node-template/pallets/template/src/template.rs`](https://github.com/substrate-developer-hub/substrate-node-template/blob/master/runtime/src/template.rs) (以下是 Rust 语法)*
```rust=
// This module's storage items.
decl_storage! {
trait Store for Module<T: Trait> as TemplateModule {
Something get(fn something): Option<u32>;
}
}
```
JS API 提供两种读取数据的方法。
1. 一是基于 JS Promise 的方法,可以这样读取数值:
```javascript=
const val = await api.query.templateModule.something();
```
2. 另一方法,如果想收听该数值,并每次该值在远端作出变更时都收到回调,则用现在 `TemplateModule.js` 里的写法。
*源码:[`src/TemplateModule.js`](https://github.com/substrate-developer-hub/substrate-front-end-template/blob/master/src/TemplateModule.js)*
```javascript
let unsubscribe;
api.query.templateModule.something(function(val) {
// 回调函数
// 在这里设置 UI 里使用的变量。
}).then(unsub => {
//取消订阅函数
unsubscribe = unsub;
})
```
最后把取消函数在 `useEffect()` 内返回即可。这是 [React Effect Hook 的清理方法](https://reactjs.org/docs/hooks-effect.html#effects-with-cleanup)。
另外值得注意一点是 `something` 在 Substrate 内是一个 `Option<u32>` 的格式。因为 Rust 和 Javascript 的数据类型没有一对一映射,因此无符号整数回到 JS 时是以封装的对象呈现。得使用跟着的 `.unwrap().toNumber()` 来把对象转化回 JS 内的数值。
## 提交外部交易 (Extrinsics)
接下来看一下这模块是如何提交外部交易,在 Substrate,所有从外部提交的交易都叫 Extrinsics。在这里,我们用一个封装好的组件 `<TxButton>`。
```htmlembedded=
<TxButton
accountPair={accountPair}
label='Store Something'
setStatus={setStatus}
type='TRANSACTION'
attrs={{
params: [formValue],
tx: api.tx.templateModule.doSomething
}}
/>
```
这个组件会生成一个按钮。点击就会向 Substrate 远端发送外部交易。需要以下参数:
- `accountPair`: 对交易进行签名的用户帐号(公钥)
- `label`:显示在按钮上的文字
- `setStatus`:提交交易后的状态更新回调函数
- `type`: [`'QUERY'`, `'TRANSACTION'`] 其中一个。如果是作写入交易,则选 `'TRANSACTION'`。
- `attrs`: 这里传入一个对象:
```javascript
{
//外部交易函数
// 这个参看回 `pallet/template/src/lib.rs` 的名字
tx: api.tx.<模块名字>.<extrinsic 名字>
//外部交易函数的输入参数数组
params: [...]
}
```
- `style`: [React 组件的 style](https://reactjs.org/docs/dom-elements.html#style)
- `disabled`: [`true`, `false`]。如果是 `true`,按钮会进入屏闭状态。
用这个组件,基本上能处理大部份点击按钮来触发的外部交易。在下一篇文章,我们会提到如何运用它底层的 JS API 作直接交易。
## 帐号管理/签署 (Keyring)
`keyring` 内包含了你的帐号 (也就是你的公钥),以及这帐号签名所需的函数。用 `.getPair()`, 以一个字符串作输入参数, 取得 `accountPair` 对象。
```javascript
const accountPair = keyring.getPair(accountAddress);
```
然后就以以下方法来签署你的交易:
```javascript
api.tx.my_pallet.dispatch_call(params).signAndSend(accountPair, 回调函数)
```
不过这个逻辑已封装在 [`<TxButton>`](https://github.com/substrate-developer-hub/substrate-front-end-template/blob/master/src/substrate-lib/components/TxButton.js) 内。 如果你是用 `<TxButton>` 组件来提交外部交易,就不需要顾虑这事了。
## 收听自定义类型 (Custom Types)
若你要收听的 pallet 内有自定义类型,则在 Substrate 客户端也需要提供这个自定义类型的结构。这自定义结构可放在 `src/config/common.json` 内。比如:
*源码:[`src/config/common.json`](https://github.com/substrate-developer-hub/substrate-front-end-template/blob/master/src/config/common.json)*
```jsonld=
{
...,
"CUSTOM_TYPES": {
"Price": {
"dollars": "u32",
"cents": "u32",
"currency": "Vec<u8>"
},
}
}
```
`u32`, `Vec<u8>`,这些都是 Rust 里的数值类型。上面例子是 Price 结构内分别有 `dollars`, `cents`, 及 `currency` 的栏位。开发者在 Substrate Node Template 代码内怎样定义这结构,也把这结构复制到前端来。
## 小结
读到这里,我们已经展示了如何利用 Front-end Template 及其封装好的 API 和组件,连接到 Substrate 节点,读取及收听链上数据,提交外部交易,及收听自定义结构数据。
用以上知识,已经足够制作一个简单的前端应用与 Substrate 网络交互。这方面的知识可与我们上一篇用 Polkadot-JS API 与 Substrate 作交互的知识配搭着使用。作起前端开发起来能更得心应手。
本篇读后有什么意见,欢迎在下方留言。对了,如果这篇文章你已读到这里,可能你会有兴趣再知道两个消息:
- Parity 在亚洲正招聘开发推广及工程师,[详情看这里,欢迎报名](https://mp.weixin.qq.com/s/K6oTeLAiKDBiwYBO6-UByw)。
- 如果你和小伙伴们有个主意,或已经开始在 Substrate/Polkadot 生态中打造一个产品/平台出来,也可报名我们的 [Substrate Bootcamp](http://bootcamp.web3.foundation/zh-cn),截止报名日期为 3月15日。