Web3.js 是以太坊推出的官方 library,可以透過他開發與區塊鏈互動的應用。IPFS 是一個點對點傳輸協議,可以透過他架設去中心化網站。但使用前端框架架設時會有什麼坑要避免呢?在這邊分享幾個經驗讓大家避免踩坑
目錄
使用 IPFS 託管 DApp 需注意的地方
如果使用 IPFS 架站,有兩點需要特別注意:
- build 出來的靜態檔案必須在同一個資料夾下,且資料夾中要有
index.html
檔案 - 所有靜態檔案連結需要使用相對連結
原因是 IPFS 託管網站時,上傳資料夾會得到一個 CID,如
QmV3NBfZLSFq8oQgiEKBraiEWEyrpPcDeifWUEahUuzRfc
要查看網站時,則在 IPFS Gateway 後面加上 /ipfs/<CID>
,如
https://cloudflare-ipfs.com/ipfs/QmPyTGEnfmAtYr9LcfCsLQnYyFvE49PJHYL4g8SFLq2qvq
此時 IPFS Gateway 會去 CID 對應目錄下尋找 index.html
檔案,如果找不到,會像 Apache 一樣把整個目錄回傳。
同時我們觀察到,網站根目錄不再是 "/"
,而是
/ipfs/QmPyTGEnfmAtYr9LcfCsLQnYyFvE49PJHYL4g8SFLq2qvq/
這和以往我們開發網頁的習慣不一樣,為了讓靜態檔案能夠順利載入,必須使用相對連結,同時需要一些手法讓前端路由能正確操作。
動態設定路由根目錄
Angular
在 index.html
用以下取代原本 <bash href>
中的內容
<script>
document.write('<base href="'+window.location.pathname+'"/>');
</script>
因為無法在 deploy 前知道 IPFS 的 CID,只能在網頁載入時動態使用 window.location.pathname
設定路由根目錄。
參考 Pinata 提供的 Angular-IPFS-Example
ReactJS
在 package.json
加入這一行即可
"homepage": "./",
參考 Pinata 提供的 React-IPFS-Example
VueJS
在 vue.config.js
中加入以下
module.exports = {
publicPath: './'
};
參考 Pinata 提供的 Vue-IPFS-Example
使用 Hash URL style 前端路由
一般會使用預設的 HTML5 pushState style 作為前端路由,比如說進入 DApp 的搜尋頁面,可能會用以下路由
/search
若以 nginx 做 reverse proxy 時,會加上以下設定,在後端找不到 /search
路徑時,直接回傳 index.html
給 browser,前端起來後就能自己用前端路由切換頁面
location / {
try_files $uri $uri/ /index.html;
}
但用 IPFS hosting 時,沒辦法添加如 nginx 的設定,會導致無法在 CID 對應的目錄下找到 search 資料夾,因而回傳 404。
此時可以改用 Hash URL style 前端路由,如
/#/search
對 IPFS 而言,hash 以後的字串可以當作看不到,如此 IPFS 能正確找到 index.html
回傳,前端起來後也能正確切換頁面。
以 angular 為例,只需要在 app-routing.module.ts
中 imports RouterModule.forRoot()
增加參數 { useHash: true }
即可
@NgModule({
imports: [RouterModule.forRoot(routes, { useHash: true })],
exports: [RouterModule],
})
export class AppRoutingModule {}
舉例:
使用 Hash URL style 可以正常顯示網頁
https://cloudflare-ipfs.com/ipfs/
QmPyTGEnfmAtYr9LcfCsLQnYyFvE49PJHYL4g8SFLq2qvq/
#/contract/0x79fcdef22feed20eddacbb2587640e45491b757f
使用 HTML 5 push style
https://cloudflare-ipfs.com/ipfs/
QmV3NBfZLSFq8oQgiEKBraiEWEyrpPcDeifWUEahUuzRfc/
contract/0x79fcdef22feed20eddacbb2587640e45491b757f
則會回傳錯誤訊息
<code>ipfs resolve -r /ipfs/QmV3NBfZLSFq8oQgiEKBraiEWEyrpPcDeifWUEahUuzRfc/contract/0x79fcdef22feed20eddacbb2587640e45491b757f: no link named "contract" under QmV3NBfZLSFq8oQgiEKBraiEWEyrpPcDeifWUEahUuzRfc</code>
瀏覽器不一定支援 Web3
使用 Web3 起手第一步,初始化 Web3 instance
this.web3 = new Web3(Web3.givenProvider);
但 Web3.givenProvider
會有東西的前提是瀏覽器支援 Web3 或者有裝錢包外掛,否則其為空。
此時要考量你的 DApp 是否要讓一般瀏覽器也能運作(比如說純 Read,不能提交 Transaction),若想要向下支援,可以考慮使用託管的節點,比如說 infura
if (Web3.givenProvider) {
this.web3 = new Web3(Web3.givenProvider);
} else {
console.log('web3 provider not found');
this.web3 = new Web3('https://mainnet.infura.io/v3/<YOUR PROJ ID>');
}
如何引入 ABI json
在 TypeScript 中要引用 contract 的 ABI json 靜態檔案,先在 tsconfig.json
中設定 resolveJsonModule
"compilerOptions": {
"resolveJsonModule": true
}
在程式中即可直接 import 並用來初始化 Contract,比如
import ABI_ERC721 from 'ABI/ERC721.json';
this.contract = new this.web3.eth.Contract(ABI_ERC721 as AbiItem[], this._address);
參考我的 github ERC721 contract class 的實作。
引用 window.ethereum 時發生 type not found
這個問題只有在 TypeScript 會發生。當引用 window.ethereum
時 compiler 會立刻抱怨找不到 type define
Property 'ethereum' does not exist on type 'Window & typeof globalThis'.
可以安裝 Metamask 提供的套件 @metamask/providers
npm install @metamask/providers
並加上宣告
import { MetaMaskInpageProvider } from '@metamask/providers';
declare global {
interface Window {
ethereum: MetaMaskInpageProvider;
}
}
如此即可安撫 compiler。
Build Code 時發生 Can’t resolve ‘crypto’
用 angular 開發時,import web3 library 後 compiler 會立刻不爽抱怨:
ERROR in ../node_modules/eth-lib/lib/bytes.js
Module not found: Error: Can't resolve 'crypto' in '*/node_modules/eth-lib/lib'
ERROR in ../node_modules/web3-eth-accounts/node_modules/eth-lib/lib/bytes.js
Module not found: Error: Can't resolve 'crypto' in '*/node_modules/web3-eth-accounts/node_modules/eth-lib/lib'
解法是,先安裝必要套件
npm install crypto-browserify stream-browserify assert stream-http https-browserify os-browserify
在 tsconfig.json
中將套件路徑加入 compilerOptions.paths
{
"compilerOptions": {
"paths" : {
"crypto": ["./node_modules/crypto-browserify"],
"stream": ["./node_modules/stream-browserify"],
"assert": ["./node_modules/assert"],
"http": ["./node_modules/stream-http"],
"https": ["./node_modules/https-browserify"],
"os": ["./node_modules/os-browserify"],
}
}
}
同時需要調整 polyfill.ts
(window as any).global = window;
global.Buffer = global.Buffer || require('buffer').Buffer;
global.process = require('process');
這樣 compiler 就會開心了!
另外一個解法是在 webpack.config.js
中加入以下設定,但這方法在 Angular 12 以後不適用,但還是記錄在這邊提供參考:
module.exports = {
node: {
crypto: true,
path: true,
os: true,
stream: true,
buffer: true
}
}
希望大家能順利完成自己第一個 DApp,有任何問題也歡迎在下面討論!
延伸閱讀:
如何用 IPFS 架設去中心化網站?
實測!用 Unstoppable Domains 鑄造區塊鏈域名並架站
自己寫 NFT 吧!- 實現 ERC-721 – 以 Python Brownie 開發
參考資料:
How to Easily Host a Website on IPFS
IPFS and Angular 6
Error: Can’t resolve ‘crypto’