🚀

爆速でBubbleのプラグインを作って公開してみた!

2024/01/19に公開

はじめに

弊社ispecでは、スピード感をもったプロトタイプ開発を提供するために、Bubbleというノーコードツールを活用しています。
その中で、プラグインを開発する機会があったので、その手順や注意点をまとめました!

今回作成したプラグインは、下記で公開しています!
よかったら使ってください!🥳
https://bubble.io/plugin/webm-audio-recorder-1705472943817x923229175745347600

プラグイン開発のきっかけ

Bubbleでは、様々な個人や団体が開発したプラグインが公開されています。Bubbleの基本機能では求める体験が実現ができない場合、これらのプラグインを組み合わせて実装します。

今回、録音のできるプラグインを探していたのですが、スマホでは長時間の録画に対応していなかったり、求めるファイル形式で出力できなかったりと、なかなか理想のプラグインが見つからなかったため、自作することにしました。

https://bubble.io/plugins

今回開発したプラグイン

以下の機能を持ったシンプルなプラグインを開発しました。

  • 録音の開始、停止ができる
  • 録音の一時停止、再開ができる
  • 録音した音声を、WebM形式で出力できる
  • 出力した音声ファイルを、File Managerに保存できる
  • 出力した音声ファイルのURLを、プロジェクト側で参照できる

1. プラグインの箱を作成する

早速、プラグインを作っていきます!

  1. ログインした状態で、プラグインページにアクセスします
  2. 画面右上の「Create a plugin」をクリックします
  3. プラグインの名前を入力してください。今回は「WebM Audio Recorder」にしました

2. Elementを作成する

Bubbleのキャンバス上に表示される要素を作成します。

今回、表示する情報は特に無いのですが、Elementがないと、プロジェクト側から呼び出すアクションやステートを定義できないので作成します。

  1. 画面左のサイドメニューで「Elements」をクリックします
  2. 「Add a new element」をクリックします
  3. Elementの名前を入力してください。今回はプラグイン名と同じ「WebM Audio Recorder」にしました

作成すると、以下の画面が表示されます。
上の方ではElementの表示に関する設定を行なえますが、そのままでも問題ありません。

今回は、表示する情報の無いElementなので、プロジェクト側で1x1で画面の端に置いておけるよう、Bubble Properties > Resizableだけチェックしました。

3. 各種アクションやステートを定義する

ここでは、あくまでアクションやステートの名前と型を定義するだけです。
実際の値の挿入やイベントの発火は、次のセクションで実装します。

Fields

プロジェクト側からプラグインに、設定値などを渡すことができます。
今回は使用しませんが、ここで定義した項目は、以下のよく見るモーダルに表示されます。

Exposed states

プラグインから、プロジェクト側に値を渡すことができます。
プロジェクト側では、${Element名}'s ${定義したステート名}で参照できるようになります。

今回は、audioUrlというステートを定義して、録音した音声ファイルのURLを参照できるようにします。この場合、プロジェクト側ではWebMAudioRecorder A's audioUrlで参照できます。

Events

任意のタイミングでイベントを発火することができます。
プロジェクト側のWorkflowで、これをトリガーにして処理を実行することができます。

今回は、uploadedというイベントを定義して、録音したファイルをFile Managerにアップロードできたタイミングで発火するようにします。

Element actions - triggered in workflows

プロジェクト側から呼び出せるアクションを定義します。
今回は、StartStopPauseResumeの4つを定義します。

4. プラグインの動作を実装する

画面を下にスクロールしていくと、先程定義したアクションのJavaScriptの関数が用意されています。ここに、各アクションが呼び出された際の処理を書いていきます。

Start
function(instance, properties, context) {
    // instance.data.hogeで、他の関数と値を共有することができます
    instance.data.audioChunks = []
    
    navigator.mediaDevices.getUserMedia({
        audio: true,
        video: false,
    }).then(stream => { // async/awaitでは、エラーになり動作しませんでした
        instance.data.stream = stream
        instance.data.mediaRecorder = new MediaRecorder(instance.data.stream)
        
        instance.data.mediaRecorder.ondataavailable = (e) => {
            instance.data.audioChunks.push(e.data)
        }
        
        instance.data.mediaRecorder.start()
    }).catch(err => {
        // エラー時の対応は、今後アップデートしていきたいです
        console.error('録音が許可されませんでした: ', err)
    })
}
Stop
function(instance, properties, context) {
    if (
        !instance.data.mediaRecorder ||
        !instance.data.audioChunks ||
        !instance.data.stream ||
        instance.data.mediaRecorder.state === 'inactive'
    ) {
        console.error('録音を開始してから停止してください')
        return
    }
    
    const onFileUploaded = (err, url) => {
        if (err) {
            console.error('オーディオファイルのアップロードに失敗しました: ', err)
        } else {
            // 事前に定義したステートに値を入れることができます
            instance.publishState('audiourl', url)
            // 同様に、定義したイベントを発火することができます
            instance.triggerEvent('uploaded', () => {})
        }
    }
    
    const onFileLoaded = (e) => {
        // 以下のリンク先に詳細がありますが、splitしないと正しくファイルが作成されません
        // https://forum.bubble.io/t/important-information-about-context-uploadcontent/198088
        const dataUrl = e.target.result.split(',')[1]
        const timeStamp = Date.now()
        
        context.uploadContent(
            `audio${timeStamp}.webm`,
            dataUrl,
            onFileUploaded
        )
    }
    
    instance.data.mediaRecorder.onstop = () => {
        const audioBlob = new Blob(
            instance.data.audioChunks,
            { type: 'audio/webm' },
        )
        const reader = new FileReader()
        
        reader.readAsDataURL(audioBlob)
        reader.onload = onFileLoaded
    }
    
    instance.data.mediaRecorder.stop()
    instance.data.stream.getTracks()
        .forEach(track => track.stop())
}
Pause
function(instance, properties, context) {
    if (
        !instance.data.mediaRecorder ||
        instance.data.mediaRecorder.state === 'inactive'
    ) {
        console.error('録音を開始してから一時停止してください')
        return
    }
    
    instance.data.mediaRecorder.pause()
}
Resume
function(instance, properties, context) {
    if (
        !instance.data.mediaRecorder ||
        instance.data.mediaRecorder.state === 'inactive'
    ) {
        console.error('録音を開始してから再開してください')
        return
    }
    
    instance.data.mediaRecorder.resume()
}

5. 動作を確認する

デバッグしてみましょう!

画面左のメニューに「App to test the plugin」という項目があるので、試したいBubbleのプロジェクト名を入力して、「Go to test app」をクリックしてください。

すると、指定したプロジェクトが開き、今回作成したプラグインが(testing)という名前付きで表示されています。ここからはBubbleでいつもやっているように、このElementとボタンをキャンバスに配置し、Workflowでボタンにアクションを割り当てることで、デバッグができます。

6. プラグインを公開する

正常に動作することが確認できたら、プラグインを公開しましょう!

1. 必須項目を入力する

  • General > Primary information > Public name
  • General > Primary information > Public logo
    今回は、ChatGPTに作成してもらいました😙
  • General > Additional information > Plugin categories
  • General > Additional information > Description

2. ライセンスを設定する

Settings > Publishing and license > Plugin distribution license で「Open source (MIT)」を選択します。

3. 変更内容を入力して公開する!

「Submit a new version」をクリックして、変更を内容を入力したら、公開完了です!🎉

4. Public pageを確認する

公開ができたら、プラグインのPublic pageを確認して、情報に間違い等がないか確認しておきましょう。

さいごに

今回はじめてBubbleのプラグインを開発したのですが、思ったより簡単に作成、公開ができました。プラグインを作ることで、自分が関わるプロダクトだけでなく、他の多くのプロダクトにも貢献できるので、積極的に開発してけたらなと思いました!

ispecでは、Bubbleなどのノーコードツールを使用したプロトタイプ開発を主導していただける、エンジニアやPdMの方を絶賛募集中です!興味のある方は、こちらから!

https://www.wantedly.com/companies/tech7/projects

ispec inc.

Discussion