--- 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 ``` 如一切无误,你在终端会看到类似如下的输出: ![Substrate 输出](https://i.imgur.com/2bDNG8V.png) 按下 `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,你会见到以下页面: ![01-前端截图](https://i.imgur.com/6TGcfR9.png) 而图中右上方的当下区块生成数也和 Substrate 节点终端所显示的一致 (请确定本机 Substrate 节点在运行着,即上面 `target/release/node-template --dev` 那句)。如果是这样,那恭喜你,你已成功跑起一个 React 的前端,并成功连接到本机 Substrate 节点上。 如果你打开浏覧器的开发视窗 (Developer Console), 你会注意到一行 console log 说明已连接的远端 socket。 ![02-开发视窗](https://i.imgur.com/Crlf3EN.png) ## 调整远端连接的 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)。 ![Template Module 模块](https://i.imgur.com/MlxoGtl.png) 这个模块虽然看似简单,但已包含了读取链上数据,提交交易,及监听事件。这前端模块对应着后端 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日。