【deprecated】IPFS On Angular(ブラウザでIPFSのノードを動かす)


【追記】内容が古くなったのでこちらを参照してください。IPFS On Angular その2

初めに

ブラウザ上でIPFSのノードを動かします。型も使いたいです。

IPFSについては他のところでも書いているので紹介とかは割愛します。公式サイトを読むのが一番良いかと思います。
https://ipfs.io/

さてIPFSのJSの実装js-IPFS 0.51.0でtypescriptの型定義が追加されましたね。
https://blog.ipfs.io/2020-10-29-js-ipfs-0-50/

これまでは有志が作った型定義ファイルしかなかったのでめでたいです。Angularもtypescriptで書かれたフロントエンドフレームワークなので、Angular上でIPFSを型安全に動かす機運が高まってきました。
結構つまづいたところも多かったので他の人の参考になればということでエラーコードと、対応方法を載せています。

ちなみにIPFSをブラウザで動かすサンプルは公式に結構あり参考にしています。
https://github.com/ipfs/js-ipfs/tree/master/examples

まとめ

環境

angular-cli: 10.2.0
angular: 10~11
js-ipfs: 0.52.1

手順

アプリの作成

angular-cliでさくっと

ng new

あとは指示に従って作っていきます。
アプリケーションの名前は今回はangular-ipfs-sample-appとしました。

IPFSのインストール

cd angular-ipfs-sample-app
yarn add ipfs

今回はipfsをまるごと入れましたが0.50からipfs-coreだけでも入れられるらしいのでそちらだけのほうがバンドルサイズを節約できそう。

参考: https://blog.ipfs.io/2020-10-29-js-ipfs-0-50/

呼び出し箇所の実装

最終的にはサービスにしてDIできるように実装したのですが、呼び出すだけなら以下のような実装で十分です。

app.component.ts
import { Component, OnInit } from '@angular/core';

import * as ipfs from 'ipfs';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit {
  title = 'Connected to IPFS';
  id = '';
  agentVersion = '';

  async ngOnInit(): Promise<void> {
    console.log('IPFS Starting');
    console.time('IPFS Started');
    try {
      // ipfsのノードを作成
      const node = await ipfs.create();
      // 作成したノードのデータを取得
      const { id, agentVersion } = await node.id();
      this.id = id;
      this.agentVersion = agentVersion;
    } catch (error) {
      console.error(error);
    }
    console.timeEnd('IPFS Started');
  }
}

viewも最終的にはいろいろきれいにしたんですが一番素朴に書くとこんな感じになります。

app.component.html
<h1>IPFS Angular</h1>
<span class="title">{{title}}</span>
<div>id: {{id}}</div>
<div>Agent Version: {{agentVersion}}</div>

最終形はこちらを見てください。ipfsのサービスを作ってDIしたりスタイルを修正したりしてますが、今回は枝葉なので省略します。

設定などの変更点

このまま動けば一番楽なのですが、js-ipfsはnodejsのライブラリに依存しているのでそこの依存を解決してあげないとビルドエラーか実行時エラーで動いてくれません。型をちゃんと使えて動く最低限必要な設定を書いておきます。エラーが出る順番とかは忘れてしまったので、順不同ですが参考にしてください。

angular.json

IPFSはcommonjsのモジュールにも依存しているので以下のような警告が出ます。

Warning: /home/ocknamo/workspace/ipfs/angular-ipfs-sample-app/src/app/services/ipfs.service.ts depends on 'ipfs'. CommonJS or AMD dependencies can cause optimization bailouts.
For more info see: https://angular.io/guide/build#configuring-commonjs-dependencies

angular.jsonを修正してcommonjsも許容します。

angular.json
"allowedCommonJsDependencies": [
              "ipfs"
            ]

tsconfig.json

ビルドエラーが出るので型を解決します。

まず、

// error
An export assignment cannot be used in a module with other exported elements.

こちらのエラーが大量に出るのでskipLibCheckを有効にする必要があります。

次にnodejsのstreamが無いよと怒られます。

// error
Error: ./node_modules/cbor/vendor/binary-parse-stream/index.js
Module not found: Error: Can't resolve 'stream' in ...

pathを通してあげましょう。

"stream": [
  "node_modules/stream-browserify"
  ]

間違ってもyarn add streamとかで適当に解決してはいけません。(npmにあるstreamパッケージは偶然同じ名前なだけでメンテもされていない)

dnsも同様にエラーが出ます。

// error
Warning: ./node_modules/multiaddr/src/resolvers/dns.js
Module not found: Error: Can't resolve 'dns' in ...

dnsはブラウザでは関係ないはずなので(多分)モックで解決しておきます。

"dns": [
  "node_modules/node-libs-browser/mock/dns.js"
],

tsconfig.jsonは最終的にこうなります。

tsconfig.json
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
  "compileOnSave": false,
  "compilerOptions": {
    // 省略
    "skipLibCheck": true,
    "paths": {
      "stream": [
        "node_modules/stream-browserify"
      ],
      "dns": [
        "node_modules/node-libs-browser/mock/dns.js"
      ]
    }
  }
}

src/polyfills.ts

まだ実行時にエラーがたくさん出て辛いので、ポリフィルをいろいろ設定しましょう。

// errors
Uncaught ReferenceError: global is not defined
// AND OR
Uncaught ReferenceError: Buffer is not defined
// AND OR
Uncaught ReferenceError: process is not defined
// AND OR
Uncaught TypeError: process.nextTick is not a function

polyfills.tsの末尾に以下を追加。

polyfills.ts
(window as any).global = window;
global.Buffer = global.Buffer || require('buffer').Buffer;
(window as any).process = {
  env: { DEBUG: undefined },
  version: [],
  nextTick: require('next-tick')
  };

ちなみに手を抜いてnextTickを適当なアロー関数とかでモックしようとするとエラーも出なくて詰むからちゃんと依存を解決してあげましょう。これで私はかなりの時間を溶かしました。

↓だめなパターン

  nextTick: () => {},

その他

window.ipfsを呼び出す必要がある場合は型を上書きする必要があるのでsrc/@types/以下に型定義ファイルを作っておいて読み込ませましょう。

src/@types/window.d.ts
interface Window {
  ipfs: any;
}

tsconfig.app.jsonにもともとincludeが設定されているのでこれだけで読み込まれます。

tsconfig.app.json
"include": [
  "src/**/*.d.ts"
]

終わり

もっといい対応方法があるよとかコメントいただけると助かります。

次回は「Angular ON IPFS」を書く予定です!