Dfinity 前端开发教程

Dfinity 前端开发教程

作者:ddd009 转载请注明出处

前段时间Uniswap对某些币种的前端限制引起了很大的争议,Dfinity是一个很好的全栈去中心化解决方案,本篇教程从 dfx new helloworld 开始讲起,详细的介绍了如何在 dfinity 上进行前端开发,并讲解了如何使用官方提供的 identity 服务(identity.ic0.app),一起来加入去中心化前端开发的世界吧!
阅读本文前需熟悉 dfx 开发工具的使用

教程目录:

  • 从 hello world 入手
  • 前端脚手架
  • 关于 webpack 配置和 dev server
  • 如何调用第三方canister
  • 如何使用 Internet Identity 服务

从 hello world 入手

在任意目录下输入dfx new helloworld,你会得到一个初始项目。运行dfx start & dfx deploy

在浏览器输入前端id.localhost:8000(推荐) 或 localhost:8000/?canisterId=前端id可以得到类似于下图的界面

截屏2021-07-28 下午5.54.03.png

js代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { Actor, HttpAgent } from "@dfinity/agent";
import {
idlFactory as helloworld_idl,
canisterId as helloworld_id,
} from "dfx-generated/helloworld";

const agent = new HttpAgent();
const helloworld = Actor.createActor(helloworld_idl, {
agent,
canisterId: helloworld_id,
});

document.getElementById("clickMeBtn").addEventListener("click", async () => {
const name = document.getElementById("name").value.toString();
const greeting = await helloworld.greet(name);
document.getElementById("greeting").innerText = greeting;
});

这段代码先是创建了一个HttpAgent,用于发送网络请求。这里agent默认的host是你网络环境的地址,例如你在localhost:8000部署了前端,这里agent就会向localhost:8000发送请求。所以当你使用dev server时一定要注意手动设置host,下文中会讲解dev server的使用。
如下是设置host的代码(生产环境中不要使用)

1
2
3
const agent = new HttpAgent({
host: "http://localhost:8000",
})

因为dfinity中的canister 使用的Actor模型,前端在调用canister时需要先创建一个actor,这里你可以简单理解为调用后端的工具。

接下来的就是创建actor的代码,这里需要三个参数,IDL,agnet,canisterId,agent就是我们刚才所创建的HttpAgent,另外两个参数将在如何调用第三方canister中介绍

当我们完成了actor创建后就可以通过actor对后端的public method进行调用,注意这里的调用都是异步调用。对于异步调用的处理本篇教程就不再展开,比较推荐的方法是使用async/await语法。这段代码调用了greeting方法并返回了一段文本。

以上就是一个最简单的dfinity前端应用程序,了解了这些过程就可以进行前后端的交互。

前端脚手架

我们使用vue,react等框架进行前端开发时,一般会使用脚手架。dfinity社区中有许多可以使用的脚手架,但目前大多处于早期阶段,使用起来可能会遇到一些问题
create-ic-app
create-ic-app使用了新型前端构建工具Vite,支持react、vue、typescript、vanilla等,并还在持续更新,Vite的dev server用起来比webpack快很多,推荐尝试
dfinity-vue
vue脚手架,在一个分支上集成了vuetify,如需使用切换到vuetify分之即可
cra-template-dfx
react脚手架,上次更新是七个月前,仅供参考不推荐使用

关于 webpack 配置和 dev server

dfinity项目默认使用webpack打包,并自动生成了配置文件,我们需要做的其实非常少。
在讲webpack之前我们需要打开dfx.json来了解一下默认配置,如果你经常使用dfx这个工具对于dfx.json应该很熟悉,不过这里还是简单讲一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
"canisters": {
...
//assets 是前端canister的名字,在dfinity中所有的代码都部署在canister中
"assets": {
//前端canister的dependency
"dependencies": [
"backend"
],
//frontend.entrypoint是前端文件的entry point,这个我们要在webpack中用到
"frontend": {
"entrypoint": "dist/index.html"
},
//source指定dist和src目录
"source": [
"dist/"
],
//指定canister的类型为前端canister
"type": "assets"
}
}
}

下面介绍一下如何在项目中配置Typescript,其他配置也可以参考。
首先是安装typescript并配置好tsconfig.json,安装ts-loader,这些就不再展开。接下来找到webpack.config.js中这段被注释掉的代码,取消掉注释,就可以在项目中使用typescript了
截屏2021-07-28 下午7.47.19.png

如果你用过dfx就会知道它的开发体验是非常差的,每改一下代码就要重新编译部署,对于前端开发来说我们一定是需要一个dev server

安装webpack-dev-server直接输入npm i webpack-dev-server

安装完成后就可以通过 webpack serve 或者是 webpack-dev-server 命令启动Dev Server, 我的版本是3.11.2 , 需要使用 webpack serve启动。

也可以在package.json中配置 dev选项

截屏2021-07-30 下午12.24.21.png

这样就可以输入npm run dev 启动Dev server

在webpack中添加pluginnew webpack.HotModuleReplacementPlugin(),

然后添加dev server配置

1
2
3
devServer: {
hot: true,
}

就能实现修改文件后自动重新编译

如何调用第三方canister

内容参考kyle的博客

调用canister需要两个参数,一个IDL接口描述,一个canister ID

IDL是对canister数据类型和接口的描述,在本地部署canister时会自动生成一个canister.did.js文件

截屏2021-07-31 下午2.33.38.png

1
2
3
4
export default ({ IDL }) => {
return IDL.Service({ 'greet' : IDL.Func([IDL.Text], [IDL.Text], []) });
};
export const init = ({ IDL }) => { return []; };

hello world的IDL长这样

1
2
3
import { idlFactory } from "dfx-generated/helloworld";

import { idlFactory } from './helloworld.did.js';

导入IDL

如果是第三方canister你可以到ic.rocks找到对应的IDL文件,以identity服务为例

截屏2021-07-31 下午2.46.28.png
找到对应的canister

截屏2021-07-31 下午2.46.52.png

选择javascript,新建一个identity.did.js,复制粘贴进去(ts 是identity.did.d.ts)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// src/declarations/identity/index.js
import { Actor, HttpAgent } from "@dfinity/agent";

// Imports and re-exports candid interface
import { idlFactory } from './identity.did.js';
export { idlFactory } from './identity.did.js';
// CANISTER_ID is replaced by webpack based on node environment
export const canisterId = process.env.IDENTITY_CANISTER_ID;

/**
*
* @param {string | import("@dfinity/principal").Principal} canisterId Canister ID of Agent
* @param {{agentOptions?: import("@dfinity/agent").HttpAgentOptions; actorOptions?: import("@dfinity/agent").ActorConfig}} [options]
* @return {import("@dfinity/agent").ActorSubclass<import("./identity.did.js")._SERVICE>}
*/
export const createActor = (canisterId, options) => {
const agent = new HttpAgent({ ...options?.agentOptions });

// Fetch root key for certificate validation during development
if(process.env.NODE_ENV !== "production") {
agent.fetchRootKey().catch(err=>{
console.warn("Unable to fetch root key. Check to ensure that your local replica is running");
console.error(err);
});
}

// Creates an actor with using the candid interface and the HttpAgent
return Actor.createActor(idlFactory, {
agent,
canisterId,
...options?.actorOptions,
});
};

/**
* A ready-to-use agent for the identity canister
* @type {import("@dfinity/agent").ActorSubclass<import("./identity.did.js")._SERVICE>}
*/
export const identity = createActor(canisterId);

创建一个用于调用identity服务的actor

如果只有candid文件,可以通过dfx工具生成一份did.js,不过这种方式比较繁琐,还有一种更简单的方式是通过官方提供的didc工具

自动安装脚本:(复制保存为sh文件运行)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
unameOut="$(uname -s)"
case "${unameOut}" in
Linux*) machine=Linux;;
Darwin*) machine=Mac;;
*) machine="UNKNOWN:${unameOut}"
esac

release=$(curl --silent "https://api.github.com/repos/dfinity/candid/releases/latest" | grep -e '"tag_name"' | cut -c 16-25)

if [ ${machine} = "Mac" ]
then
echo "Downloading didc for Mac to ~/bin/didc"
curl -fsSL https://github.com/dfinity/candid/releases/download/${release}/didc-macos > /usr/local/bin/didc
elif [ ${machine} = "Linux" ]
then
echo "Downloading didc for Linux to ~/bin/didc"
curl -fsSL https://github.com/dfinity/candid/releases/download/${release}/didc-linux64 > ~/bin/didc
else
echo "Could not detect a supported operating system. Please note that didc is currently only supported for Mac and Linux"
fi

date

安装完成后输入didc bind ./identity.did -t js > ./identity.did.js就可以生成did.js文件(didc bind ./identity.did -t ts > ./identity.did.d.ts ts版本)

如何使用 Internet Identity 服务

Internet Identity(简称II) 是dfinity官方推出的DID服务,基于WebAuth。在dfinity上的应用一般会支持使用II登录。

如果想要在本地的开发环境调试II,需要把Ineternet Identity clone 到本地,并部署到你本地用于开发的dfx网络环境中

1
2
git clone git@github.com:dfinity/internet-identity.git
cd internet-identity
1
dfx start --clean

这里推荐使用–clean参数启动,因为II服务的默认canister id与本地钱包的默认id相同,所以先清扫一下环境避免冲突。注意因为本地能启动多个dfx网络,一定要保证II部署的网络与项目的网络相同(端口相同)

1
2
npm install
II_ENV=development dfx deploy --no-wallet --argument '(null)'

这样就完成了II的本地部署

打开package.json确保你安装了@dfinity/auth-client、@dfinity/authentication

下面是前端调用II服务的代码

1
2
3
4
5
6
7
8
9
10
11
const init = async () => {
authClient = await AuthClient.create();
await authClient.login({
maxTimeToLive: BigInt("0x7f7f7f7f7f"),
identityProvider:
"http://localhost:8000/?canisterId=rwlgt-iiaaa-aaaaa-aaaaa-cai"
onSuccess: async () => {
handleAuthenticated(authClient);
},
});
}

这段代码同样非常容易理解,先是创建authclient,然后调用login方法。
maxTimeToLive是设置委托密钥的有效时间,identityProvider就是II部署到的canister,如果在主网的话就是identity.ic0.app,onSuccess是认证完成后的回调函数。这里还有很多参数可以指定,具体可以查看Agent JS Docs,不再展开。

下面讲一下如何发起认证过的调用,前面helloworld的例子中讲过如何发起调用,不过使用的是随机生成的匿名身份。当用户完成认证后我们就能获取到他们的身份,如何使用用户的身份发起认证过的调用呢,如下,在创建agent时传入identity即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
async function handleAuthenticated(authClient) {
identity = await authClient.getIdentity();
const agent = new HttpAgent({
identity: identity,
host: "http://localhost:8000",
});
//本地开发时需要获取Root Key
if(process.env.NODE_ENV !== "production") await agent.fetchRootKey();
let actor = Actor.createActor(idlFactory, {
agent,
canisterId: canisterId,
});
const greeting = await actor.greet(name);
}