20200223のReactに関する記事は5件です。

Gatsbyのアクセシビリティテスト用プラグイン「gatsby-plugin-a11y-report」

Gatsbyのアクセシビリティテスト用のプラグインを書きました。
gatsby-plugin-a11y-reportです。

gatsby develop 時の表示イメージ。
gatsby-plugin-a11y-reportの実行画面。ブラウザにトーストでチェック結果数のトースト表示する。ConsoleにViolationsとincompleteを表示している。

もともとGatsbyにはgatsby-plugin-react-a11yというreact-axeを使ったプラグインがあるのですが、その元となったreact-axeがviolationsしか表示してくれないので、incompleteも表示するように書き直したりしているうちに差分が大きくなりすぎたので独立したような感じです。

また、gatsby develop 時だけでなく、build時にはaxeのレポートを以下のようにlogsディレクトリに出力します。

{"title":"Gatsby Build: A11y Check Start","pages":14,"logging":["violations","incomplete"],"ignore":["/404*","/tag/*"],"device":{"name":"Chrome","userAgent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/80.0.3987.0 Safari/537.36","viewport":{"width":1280,"height":600,"deviceScaleFactor":1,"isMobile":false,"hasTouch":false,"isLandscape":false}},"level":"info","service":"axe-report","timestamp":"2020-02-23T08:20:12.697Z"}
{"path":"/offline-plugin-app-shell-fallback","result":"violations","id":"document-title","impact":"serious","description":"各HTMLドキュメントに空ではない<title>要素が含まれていることを確認します","html":"<html>","target":["html"],"level":"error","service":"axe-report","timestamp":"2020-02-23T08:20:19.766Z"}
{"path":"/offline-plugin-app-shell-fallback","result":"violations","id":"html-has-lang","impact":"serious","description":"すべてのHTMLドキュメントにlang属性が存在することを確認します","html":"<html>","target":["html"],"level":"error","service":"axe-report","timestamp":"2020-02-23T08:20:19.767Z"}
~省略~
{"path":"/posts/humane-typography-in-the-digital-age","result":"violations","id":"region","impact":"moderate","description":"ページのすべてのコンテンツがlandmarkに含まれていることを確認します","html":"<div class=\"Post-module--post__footer--1BvmJ\">","target":[".Post-module--post__footer--1BvmJ"],"level":"error","service":"axe-report","timestamp":"2020-02-23T08:20:20.840Z"}
{"title":"Gatsby Build: A11y Check Complete","complete":true,"violations":45,"incomplete":3,"level":"info","service":"axe-report","timestamp":"2020-02-23T08:20:20.855Z"}

gatsby build をGitのpush時などに自動実行しているような場合にviolationsの数をチェックしてCIするのに便利に使えるかなと思います。

使い方

インストール

通常のGatsbyプラグイン同様、セットアップしたGatsbyサイトのディレクトリで以下のようにプラグインをインストールします。
PuppeteerexpressWinstonを内部で利用しているので結構重いプラグインになっています。

npm install --save gatsby-plugin-a11y-report

あるいはYarnを利用してモジュールを追加。

yarn add gatsby-plugin-a11y-report

gatsby.configに設定を追加

インストール出来たらプラグインを利用するようにGatsbyを設定します。
gatsby-config.js に以下のように設定します。

// gatsby-config.js

module.exports = {
  plugins: [
    {
      resolve: 'gatsby-plugin-a11y-report',
      options: {
        showInProduction: false,
        toastAutoClose: false,
        query: `
          {
            allSitePage(
              filter: {
                path: { regex: "/^(?!/404/|/404.html|/dev-404-page/)/" }
              }
            ) {
              edges {
                node {
                  path
                }
              }
            }
          }
        `,
        ignoreCheck: [
          '/404*',
          '/tag/*'
        ],
        serverOptions: {
          host: 'localhost',
          port: '8341'
        },
        axeOptions: {
          locale: 'ja',
        },
        loggingOptions: {
          result: ['violations', 'incomplete']
        }
      },
    },
  ],
}

オプション設定

以下のオプションはquery以外すべて省略可能です。

showInProduction

デフォルト:false
トーストとConsoleへの出力をproductionでも行う場合、trueにしてください。(gatsby-react-axe互換。まぁ、一般的にはfalseのまま使うかと思いますが。)

toastAutoClose

デフォルト:false
gatsby develop時に表示するトーストを一定時間で自動的に閉じる設定です。
エラーにすぐ気が付くようにviolationsの件数などをトースト表示するようにしたのですが、そもそもトーストがaxeのチェックでコントラスト不足だったり、画面遷移時にうまく消えてくれなかったりアクセシビリティ的によろしくない状況でして、この機能は将来廃止するかデフォルトでは非表示にするなどしようかと考えています。
もう少し調整してみるつもりですが。

query(必須)

GatsbyJSの特徴でもあるGraphQLで、Build時にチェックするサイトのパスを指定します。
filterによりテストでログに残したい対象ページを柔軟に絞り込むことができます。

ignoreCheck

queryのfilterで絞り込めることに気が付かなかったときに作ってしまった機能ですが、正規表現でパスを記述するのこっちのほうが楽な気がするので残してます。作っていきなり後方互換みたいになっていますが、たぶんGraphQL上手な人はいらないやつですが、GraphQLで絞り込んだあとに追加で絞り込みが実行されますので、使い方次第で便利かも。

serverOptions

デフォルト: { host: 'localhost', port: '8341' }
プラグインはBuild時にExpressの検証用サーバを立ち上げますが、その検証サーバのホストとポートを指定します。ちなみにプラグインはPuppeteerで検証サーバにアクセスしてaxeのレポートをログとして出力します。

axeOptions

axeのオプションを指定します。
通常はログに出力するレポートの言語を指定します。指定がない場合はブラウザの設定から推測してロケール設定します。
そのほかの指定可能なオプションはaxe-coreのドキュメントを参照してください。

loggingOptions

デフォルト:result: ['violations']
Build時に出力するログの種類を配列で指定します。指定可能なのは'violations', 'incomplete', 'inapplicable', 'passes'の4つです。デフォルトではviolationsだけ出力します。

今後の予定

より柔軟にロギングの設定がしやすいようにしたいのですが、gatsby-configの設定が肥大化するのは避けたいので何か方法を検討中。ほかのプラグインでよさげな方法をとっているものがないか調べてみるつもりですが、何か情報をお持ちの方がいれば教えてください。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Reactのアプリを簡単にDocker上のnginxに搭載してみた

目標

Reactのcreate-react-appで作成したアプリをnginxに載せてみました。
簡単にできたのでメモしておきます。

Reactアプリの作成

これはcreate-react-appでサクッと作成しましょう。
DockerでReactの環境を作成してみたでDocker上で構築できるはずです。
アプリが作成できたらyarn buildでnginxに載せるファイルを出力しましょう。

niginxのDockerfile

nginxのDockerfileです。
yarn buildで出力したファイルとnginxの設定ファイルをコピーしています。

FROM nginx:1.17

COPY ./app/build /opt/app/

COPY ./nginx.conf /etc/nginx/nginx.conf

CMD ["nginx", "-g", "daemon off;", "-c", "/etc/nginx/nginx.conf"]

nginxの設定ファイル

locationはbuildしたファイルを置く場所に設定しましょう。

nginx.conf
user  nginx;
worker_processes  auto;

error_log  /var/log/nginx/error.log warn;
pid        /var/run/nginx.pid;

events {
    worker_connections  1024;
}

http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;

    sendfile        on;

    keepalive_timeout  75;

    server {
        listen 80;
        charset utf-8;

        location /{
            root   /opt/app/;
            index  index.html;
        }
    }
}

まとめ

あとは、nginxのDockerfileをbuildしてコンテナを立ち上げれば完了です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

dynamic importでnamed exportにする際に躓いた点

named exportする点で行うとエラーになる理由と解決策

理由

  • Poor Discoverabilityの回避
  • Autocompleteなどで保管性を保ちたい

問題

  • export defaultでないためimportする際にエラーになる

解決策

- https://stackoverflow.com/questions/54318485/dynamic-import-named-export-using-webpack

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

7年間使ってきたWordPressを捨ててContentful+Gatsby+Netlifyにしたら爆速になったし経緯とか教訓とか語る

こんにちは、古都ことと言います。普段はブログやらなんやらをやっているのですが、今回ブログのお引っ越しをしたのでその経緯などについてお話ししたいと思います。

先にまとめ

  • ブログをVPSとWordPressで7年間運営してきた
  • 速度面やメンテナンス面でそろそろガタがきていた
  • Contentful+Gastby+Netlifyの構成に移行した
  • Lighthouseで高スコア叩き出せた
  • 技術選択って難しいね

スクリーンショット

運営しているブログ

Subterranean Flower Blogというブログをやってます。

https://sbfl.net/blog/

主にフロントエンド周りのことを取り扱っており、たまにマリオ64の記事や、転職の記事などでもバズってます。

7年間頑張ってくれたWordPress

私がブログを始めたのは2013年でした。当時は大学4年生でDartにハマっており、「この良さをなんとか世に広めたい!」という気持ちで記事を書いてました。

採用したのはさくらVPSとWordPressでした。AWSも検討しましたが「インスタンス立てて管理するのめんどい…」という気持ちが先行してVPSになりました。WordPressを選択した理由は深くはありません。ただなんとなくCMSならWordPressかな、と思って採用しました。jekyllも既に登場してはいたのですが、難しそうという理由で採用を見送りました。

当時はページのパフォーマンスには無頓着でしたし、なによりWordPressの使いやすさには助けられました。プラグインを入れるだけでなんでもできて、テーマを差し替えるだけでガラッと見た目が変わるというのは、ブログ運営の素人にとっては天からの恵みでした。

徐々に表面化してくる問題

初めは問題なかったのです。全てがうまく行っており、まったくの幸せでした。ですが徐々に問題が出てきます。

まず問題になったのは管理の大変さでした。VPSなので自分で管理せねばならず、apacheの設定をがんばったり死活監視ツール入れたりLet's Encryptのcron動くようにしたり、難しい部分は少ないですがかなり面倒でした。設定ミスでphp-fpmがメモリ馬鹿食いしてるの発見するまでは定期的に再起動とかしてました。

次にパフォーマンスの問題が出てきました。WordPressでは動的にページを生成しているため、生のまま使うとかなり遅いし、負荷も高くなります。同時アクセス3ケタでもろくに捌けなかったり、スパイクが発生したときにはブログが落ちるなんてことも何度かありました。

最後に問題になったのは記事のポータビリティの低さでした。WordPressにはGutenbergという優秀なWYSIWYGエディタがついているのですが、これが吐き出すHTMLはポータビリティが大して高くないです。また様々なプラグインの介入で更に混沌としたHTMLが吐き出され、WordPressへの依存度がどんどん高くなっていました。

マイナスからゼロへ向かう作業のつらさ

様々な問題に対して場当たり的に対応していました。Let's Encrypt動かすのつらいからmod_md採用したり、PHPの速度が出ないからiniいじったりVPSのスペックあげたり、通信速度出すためにHTTP/2やBrotli導入したり、負荷減らすためにページキャッシュ系のプラグイン入れたり、高速な表示を謳うテーマを導入してみたり。

でもどれも本質的ではないよなと思っていました。私がやりたいのはブログの運営であって泥臭いチューニング作業ではないと感じていました。問題点を発見して弄ってパフォーマンスを計測する技術的な楽しさはあるけど、徐々に疲れてきました。

これらの作業ってマイナスが出発点なんですよね。「証明書が更新されない」「表示が遅い」「サイトごと落ちる」、どれもマイナスです。「できて当たり前」ができていない。よって必然的にマイナスからゼロに戻す作業になります。これらも大切な作業ではあるのですが、個人的にはマイナスからゼロに戻す作業は疲労感の方が勝ります。正直つらかったです。

自分が頑張ってチューニングしたブログより友達が適当に借りてるFC2ブログの方が圧倒的に早いって状況、耐えられなかったです。

移行の検討

「あっ、これもう無理だな」

そう思ったのはここ2年ぐらいです。何度かサイトが落ちて、世界中の技術者がウェブのパフォーマンス改善に注力し始めて、検索エンジンの評価でも速度が重視されて。

運用面も不安でした。ど素人がどんなにがんばったところでセキュリティホールはできてしまうし、何かトラブルのたびにメンテできるかと言われると、正直そこまでのやる気はないとしか言いようがないです。

もう逃げられんな、と。そしてシステムの移行を検討し始めました。

私は「やりたい」と思ってから実際にやるまで1年か2年ぐらいのタイムラグがあるのでだいぶ遅くはなりましたが、今年に入ってからContentful+Gatsby+Netlifyに決めました。できるだけマネージドなサービスを採用し、データのポータビリティを確保しつつページ表示速度を稼ぐ、ということを考えていたらこうなりました。

WordPressを捨てるとはいえ何かしらのCMSは欲しかったのでContentfulを採用しました。コンテンツ管理だけを担当するHeadlessCMSなのでフロントエンド等は後から付け替えられます。WYSIWYGエディタもついているのですが絶対に使わずマークダウンだけを使うようにしました。無料だと5000レコードまでしか使えませんが、そもそもそんなに記事書かないしメディアもアップロードしないので5000も行かないです。実際に移行済の今見てもまだ500レコードも行ってない。

ページの生成には静的サイトジェネレータであるGatsbyを採用しました。前々からjekyll採用しなかったのは後悔していましたし、今度は今までのReactの知見が使えるGatsbyかなと。あとGraphQL触ってみたかったという気持ち。そしてMarkdownなら最悪何かあっても他の形式に変換して脱出しやすいですし、フレームワークにベッタリすることはないのかなと。

デプロイ先はNetlifyです。ここはあんまり深く考えてません。正直自分で管理しなくていいならどこでもいい。

移行作業

WordPressはREST APIが生えてるのでデータをぶっこぬくのは簡単でした。管理画面にはエクスポート機能もあるのですが、WordPressからWordPressへの引っ越し用なので役に立たんです。118記事しかなかったのでAPIからサクっと引っこ抜いてきました。タグやカテゴリも同様に。

メディアの類はsshでログインしてフォルダをzipで固めて落としてきて作業です。WordPressのメディアは 2020/02/23/sample.jpg みたいな階層構造になってるのでシェルスクリプト書いていったんフラット構造に変換しました。 2020_02_23_sample.jpg みたいなファイルに。またリサイズ済画像も保管されてるので正規表現で適当にフィルタかけて破棄しました。

さあデータぶっこぬいた後が大変。WordPressのRESTが返す構造、結構ごちゃごちゃしてます。その中から必要そうなものだけをピックアップするスクリプトをNode.js+JavaScriptで作成。そしてその中のHTMLはそのままでは使えないので、いらない要素やclassなどを削るコードと、メディアへの参照をWordPressのものからContentfulのURLに差し替えるコードを書きました。

Contentful側でのデータモデルは以下のような感じにしました。

スクリーンショット 2020-02-23 14.46.42.png

compatは互換性を表すための識別子です。移行した記事での値は sbfl_wp_2013 で固定にしてます。この値が入っていたらHTMLとしてそのままレンダリングするようにしてます。このあたりは完全なる妥協の産物ですが、これ以上のことはちょっと思いつかなかったです。

あとはメディアをContentfulにアップロードするスクリプトと、記事をアップロードするスクリプトを書いて実行。118記事中117記事が移行成功です。1記事はContentfulの5万文字制限を超えた7万文字の記事だったので諦めました。

あとはGatsbyでテンプレートをゴリゴリ書いて終わり。

デプロイはGitHubへのpushまたはContentfulのpublishをフックにNetlifyのビルドを動かします。通知はZapier使ってDiscordに投げます(Zapier通さない場合はNetlifyのProプランじゃないとダメでした)。

スクリーンショット

できた!!

そんなこんなでできました: https://sbfl.net/blog/
ソースはこれです: https://github.com/subterraneanflower/sbfl-site

計測!速い!

Lightouseの計測結果は以下のような感じです。

diff.jpg

なんと96点。TTIがだいぶ速くなってます。

個別記事ページは以下のスコアでした。

single.png

Prism.jsによるコードハイライティングとかあるにもかかわらずだいぶ速いです。Gatsbyいいですね。なおページ遷移も高速で、リンクをクリックすると一瞬で切り替わります。私のブログはほぼ検索流入なのであんまり意味はないですが……。

さいごに

技術選択の重みを知った

2013年の当時は自分のブログが7年続くとか考えてなかったです。とりあえず立ち上げてとりあえず動けばいいやと。そんな中で雑に選んだ技術が徐々に牙を剥き始め、トラブルの対応に追われ、泣きながら別のプラットフォームに逃げ出すことになるとは思ってもいなかったです。

自分では安定している選択肢をとったつもりが、7年も経てば立派な負債となっていました。7年も先のことなんて考えられませんが、考えられるかどうかではなく時代は確実に流れていくのです。どういう理由で選択したにしろ、必ず自分の選択と向き合わなければいけない時がきます。

現実問題としては7年後にもきれいに動作する選択なんて無理なので、逃げたいときにいつでも逃げられるように作っておくのが大事なのかなと思ってます。今回のContentful+Gatsby+Netlifyも、どこかに逃げられるように作っています。ContenfulはWYSIWYGエディタを持っているのですが、それ使うとまた同じはめになるのでMarkdownのみを使います。そしてGatsbyは変換と表示するだけ。加工はしない。Netlifyも固有の機能はほぼ使わずビルドだけです。

失敗しないことよりも、失敗したときにいかに逃げ出せるようにしておくかが重要かもですね。

WordPressにはお世話になりました

なんだかんだ言ってWordPressにはお世話になりました。よくわからなくても管理画面いけばだいたいどうにかなるお手軽さと、プラグインの豊富さには助けられました。テーマのソースいじってのチューニングとかもいろいろやってたので愛着は湧いてました。

今のウェブの思想にはマッチしないなとは思いつつも、そのとっつきやすさとか情報量の多さとかで役立つ部分は多いはずなので、適材適所で頑張って欲しいです。

まとめ

VPS+WordPressで運用していてここ数年間ずっと苦しんでいましたが、なんとか良い感じの形で脱出できました。こっちはこっちでまたいろいろなトラブルあるんだろうなーとは思いつつも、現段階では結構綺麗にまとまったのではないかなと。

苦しみながらここまでずるずる来てしまいましたが、もっと早くに決断すべきだったのかなとも感じています。一度組んだらもうそこから動かしたくないという気持ちが邪魔して何もできなかったのですが、勇気を持ってより迅速な行動ができたほうがいいよなーと実感しました。

実はまだ終わりではなく、VPSにはDiscordのbotを相乗りさせていたのでそれを移行する作業なんかも残っています。こっちはもうちょっと大変な作業になるのですが、頑張ってきます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

wasm(Rust) + Web Audio API + Reactで音を鳴らしてみた

やほー✨アッキーたよ!今回はReactでガワを作ってWeb Audio APIで音を鳴らして見たいと思いまーっす♫ただ鳴らすんじゃつまらないんで、Rustでwasmを書いたりしてみましたっ⚙

デモはこちら
コードはこちら

準備

まずはこちらを参考に下準備から。
wasm-bindgen + wasm-pack + webpack で フロントエンド - Qiita
ただRustのコードがネスト深くなるのが嫌だったので浅くしてます。
あとは

$ yarn add react react-dom

$ yarn add -D @types/react @types/react-dom

しておき、必要なファイルも入れると構成はこんな感じ。

├── Cargo.lock
├── Cargo.toml
├── LICENSE
├── client
│   ├── components
│   │   ├── App.tsx
│   │   ├── Slidebar.tsx
│   │   └── Switch.tsx
│   ├── index.html
│   ├── index.tsx
│   └── reducer.ts
├── package.json
├── src
│   └── lib.rs
├── tsconfig.json
├── webpack.config.js
└── yarn.lock

目標

image.png
こんな感じの簡素なUI。Playを押すと音がなってスライドバーを動かすと音の大きさが変わる。これを目標にしましょう。

Reducer

Reduxは使いませんが、状態管理にReact hooksのuseReducerを使っていこうかと思います。というわけでReducerはいドーン。

client/reducer.ts
import { Oscillator } from '../pkg'

export type State = {
  playing: boolean,
  gain: number,
  osc: Oscillator,
}

export const SwitchPlaying = Symbol("SwitchPlaying")
export const ChangeGain = Symbol("ChangeGain")
export const StoreOsc = Symbol("StoreOsc")

export const switchPlaying = () => ({
  type: SwitchPlaying,
})
export const changeGain= (payload: number) => ({
  type: ChangeGain,
  payload,
})
export const storeOsc = (payload: Oscillator | null) => ({
  type: StoreOsc,
  payload,
})
export type Actions = ReturnType<
  typeof switchPlaying |
  typeof changeGain |
  typeof storeOsc>

export const initial: State = {
  playing: false,
  gain: 0.2,
  osc: null,
}

export const reducer = (state: State | null, action: Actions): State => {
  if(!state) {
    return initial
  }

  switch (action.type) {
    case SwitchPlaying:
      if(state.playing) {
        return {...state, playing: false}
      } else {
        return {...state, playing: true}
      }
    case ChangeGain:
      let gain = action.payload
      if(gain > 1.0) {
        gain = 1.0
      } else if (gain < 0.0) {
        gain = 0.0
      }
      return {...state, gain: gain}
    case StoreOsc:
      if(action.payload){
        return {...state, osc: action.payload}
      } else {
        return {...state, osc: null}
      }
    default:
      throw new Error('Invalid action type')
  }
}

状態として再生中かどうかを表すplaying、Gainの数値gain、あとこれは後で説明するのですがWeb Audio APIで使うAudioSourceNodeというやつを格納しておきます。あとは更新するActionsとReducer。

UI Components

Switch

再生・停止を切り替えます。

client/components/Switch.tsx
import React, {Dispatch} from "react"
import { SwitchPlaying, Actions } from "../reducer"

type Props = {
  playing: boolean;
  dispatch: Dispatch<Actions>;
}

export const Switch: React.FC = ({playing, dispatch}: Props) => {
  return(
    <button onClick={() => dispatch({type: SwitchPlaying})}>{playing ? 'stop' : 'play'}</button>
  )
}

渡されたplayingによって表示を変えてるだけですね。dispatchを受け取るやり方はここを参考にしました。
useReducerの本質:良いパフォーマンスのためのロジックとコンポーネント設計 - Qiita

Slidebar

Gainを調節するやつです。Gainは小数なら何でも入れてよいのですが、大きい数字を入れるとそのまま倍された音が出てきてうるさいので0.0から1.0の値に収める処理を入れてやります。1

client/components/Slidebar.tsx
import React, {Dispatch} from "react"
import { useState, useCallback } from "react"
import {Actions, ChangeGain} from "../reducer"

type Props = {
  gain: number;
  dispatch: Dispatch<Actions>;
}

export const Slidebar: React.FC = ({gain, dispatch}: Props) => {
  const [scale, setScale] = useState(Math.round(gain * 255))
  const handler = useCallback(
    (event: React.ChangeEvent<HTMLInputElement>) => {
      event.persist()
      const newScale = Number(event.target.value)
      setScale(newScale)
      const newGain = newScale / 255.0
      dispatch({type: ChangeGain, payload: newGain})
    }, [event]
  )

  return(
    <label>Gain: <input 
      type="range" 
      min="0"
      max="255" 
      value={scale}
      onChange={handler}
      />[{gain}]</label>
  )
}

App

そしてUIをまとめてAppを作ります。

client/components/App.tsx
import * as React from 'react'
import { useEffect, useReducer } from 'react'
import { Slidebar } from './Slidebar'
import { Switch } from './Switch'
import { reducer, initial, StoreOsc, SwitchPlaying } from '../reducer'

export const App = ({ wasm }) => {
  const [state, dispatch] = useReducer(reducer, initial)
  useEffect (() => {
    if(state.playing){
      if(state.osc){
        state.osc.set_gain(state.gain)
      } else {
        const osc = new wasm.Oscillator()
        osc.set_gain(state.gain)
        dispatch({type: StoreOsc, payload: osc})
      }
    } else {
      if(state.osc){
        state.osc.free()
        dispatch({type: StoreOsc, payload: null})
      }
    }
  }, [state.playing, state.gain])
  return (
    <main>
      <Slidebar gain={state.gain} dispatch={dispatch} />
      <Switch playing={state.playing} dispatch={dispatch} />
    </main>
  )
}

ここでまとめてOscillatorの初期化やgainの設定、Oscillatorの解放といった副作用を取り扱っています。これらは本来Web Audio APIに存在するものですが、今回はPropsから流し込まれたwasmモジュールからやってきています!2

index.tsxとindex.html

index.tsxではwasmを遅延ロードし、Appに流し込んで実行しています。

client/index.tsx
import * as React from "react"
import * as ReactDOM from "react-dom"

import { App } from "./components/App"
import('../pkg').then((wasm) => {
  ReactDOM.render(
      <App wasm={wasm}/>,
      document.getElementById("app")
  )
})

それとあとはindex.htmlですね。

client/index.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>wasm-audio</title>
  </head>
  <body>
    <div id="app"></div> 
  </body>
</html>

以上でReact側のコードは終わりとなります。次はRust側です。

wasm with Rust

と言ってもwasm_bindgenのおかげでかなり書くことは少ないです。

use wasm_bindgen::prelude::*;
use web_sys::{AudioContext, OscillatorType};

#[wasm_bindgen]
pub struct Oscillator {
    ctx: AudioContext,
    osc: web_sys::OscillatorNode,
    gain: web_sys::GainNode,
}

impl Drop for Oscillator {
    fn drop(&mut self) {
        let _ = self.ctx.close();
    }
}

#[wasm_bindgen]
impl Oscillator {
    #[wasm_bindgen(constructor)]
    pub fn new() -> Result<Oscillator, JsValue> {
        let ctx = web_sys::AudioContext::new()?;
        let osc = ctx.create_oscillator()?;
        let gain = ctx.create_gain()?;

        osc.set_type(OscillatorType::Sine);
        osc.frequency().set_value(440.0);
        gain.gain().set_value(0.0);

        osc.connect_with_audio_node(&gain)?;
        gain.connect_with_audio_node(&ctx.destination())?;

        osc.start()?;

        Ok(Oscillator { ctx, osc, gain })
    }

    #[wasm_bindgen]
    pub fn set_gain(&self, gain: f32) {
        let new_gain = if gain > 1.0 {
            1.0
        } else if gain < 0.0 {
            0.0
        } else {
            gain
        };
        self.gain.gain().set_value(new_gain);
    }
}

基本的にやっていることはJavaScriptでやっていることをRustのweb_sys crateを通じてやっているだけに過ぎません。なのでこれだけだとあまりwasmでやる良さがないのかも。AudioBufferに直接計算した波形を書き込んで処理をして……なんて使い方だと高速に処理できるwasmが光りそうですね!そのあたりも今後実験していきたいところです。


  1. onChangeで頻繁に値が変わりそうなのでuseCallbackを噛ませていますが効果があるのかはわかりません。詳しい人教えて。 

  2. ここのwasmに型がついてないのですが、wasm-packの吐き出すwasmモジュール自体にどうやって型をつけたらよいんでしょうか。こっそり教えてほしいです。 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む