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’ 

Written by J
雖然大學唸的是生物,但持著興趣與熱情自學,畢業後轉戰硬體工程師,與宅宅工程師們一起過著沒日沒夜的生活,做著台灣最薄的 intel 筆電,要與 macbook air 比拼。 離開後,憑著一股傻勁與朋友創業,再度轉戰軟體工程師,一手扛起前後端、雙平台 app 開發,過程中雖跌跌撞撞,卻也累計不少經驗。 可惜不是那 1% 的成功人士,於是加入其他成功人士的新創公司,專職開發後端。沒想到卻在採前人坑的過程中,拓寬了眼界,得到了深層的領悟。