LoginSignup
23
10

More than 3 years have passed since last update.

GoでChrome拡張を作った話

Last updated at Posted at 2019-06-28

GoでChrome拡張を作りました!

🤔ん?と思った方もいらっしゃると思いますが、正確にいうと

Go+ WebAssemblyでChrome拡張を作りました!

お気付きの方、そうです。wasmを使いました。

作ったもの

どういうものを作ったかというと 

:point_up:の記事に載ってる選択したGoのコードをThe Go Playgroundで簡単に実行できるChrome拡張に
Goのフォーマット機能を追加しました!!

Demo

こんな感じで動きます
clipgo_demo1.1.0min.gif

追加機能

  • 選択したコードのフォーマットが崩れていたらフォーマットしてくれる

  • 選択ミスした(Goとしての構文が崩れている)場合、教えてくれる(コンテキストメニュー内にエラーメッセージを出してくれる)

GitHubはこちら

ダウンロードはこちら

Go + WebAssemblyとは

  • Go1.11から実験的に導入された

  • syscall/jsパッケージでJavaScript側の操作も可能

  • GOOS=js GOARCH=wasmgo buildすると.wasmファイルが生成される

  • テストもGOOS=js GOARCH=wasm go test -exec="$(go env GOROOT)/misc/wasm/go_js_wasm_exec"のようにexecで実行環境を渡してあげれば可能

詳しくはこちらを

構造

ディレクトリ構造

.
├── background
│   ├── background.js    // 新しいタブでPlaygroundを開く
│   ├── formatter.go     // フォーマットする関数を生成・登録
│   ├── init_go.js       // Goのインスタンス生成
│   └── wasm_exec.js     // Go + wasmを実行
├── contents
│   └── contents.js      // 選択した文字列をChromeAPIでbackground.jsに送る
└── manifest.json

全体の流れ
clipgo_flow.png
簡単な図はこんな感じです

ソースコード

contents

contents.js
document.addEventListener("selectionchange", () => {
  chrome.runtime.sendMessage({ code: window.getSelection().toString() });
});

background.jsに選択した文字列をChromeAPIで送ります

background

formatter.go
package main

import (
    "go/format"
    "syscall/js"
)

type error interface {
    Error() string
}

// 与えられた文字列をフォーマットする関数
func formatter(this js.Value, args []js.Value) interface{} {
    code := args[0].String()
    newCode, err := format.Source([]byte(code))
    if err != nil {
        return map[string]interface{}{"output": err.Error(), "err": true}
    }
    return map[string]interface{}{"output": string(newCode), "err": false}
}

// jsのプロパティとして登録
func registerCallback() {
    js.Global().Set("formatter", js.FuncOf(formatter))
}

func main() {
    c := make(chan struct{}, 0)
    registerCallback()
    <-c // ブロックしてmain関数を終わらせないようにしている
}

Goのコードをフォーマットする関数です。syscall/jsを用いて関数を定義しています。go-wasmでは、main関数を終了させないためにあえてチャネルでブロックさせます。main関数が終了してしまうと、せっかくjsに登録した関数も呼べなくなってしまいます。

background.js
const id = "clip_go";
const title = "Clip Go";
const type = "normal";
const contexts = ["selection"];

let code;

chrome.runtime.onMessage.addListener(message => {
  // フォーマット
  const result = formatter(message.code);

  // フォーマット結果からコンテキストメニューを更新
  if (result.err) {
    chrome.contextMenus.update(id, {
      title: `${title} [Error] ${result.output}`,
      enabled: false
    });
  } else {
    code = result.output;
    chrome.contextMenus.update(id, {
      title: title,
      enabled: true
    });
  }
});

// コンテキストメニューにClip Goを定義
chrome.contextMenus.create({
  id: id,
  title: title,
  type: type,
  contexts: contexts
});

// Clip Goがクリックされると新しいタブでThe Go Playgroundを表示
chrome.contextMenus.onClicked.addListener(() => {
  chrome.tabs.create({ url: "https://play.golang.org/" }, tab => {
    chrome.tabs.executeScript(tab.id, {
      code: `document.getElementById("code").value = \`${code}\`;document.getElementById("run").click();`
    });
  });
});

background.jsは以下の役割を担っています。

  • コンテキストメニューにClip Goを登録
  • contents.jsから送られてきた文字列をGoで作った関数に渡して、フォーマット結果からコンテキストメニューの状態を更新
  • Clip Goがクリックされると、新しいタブでThe Go Playgroundを開き、フォーマットした文字列を貼って実行する
init.go
const go = new Go();

WebAssembly.instantiateStreaming(fetch("formatter.wasm"), go.importObject).then(
  result => {
    go.run(result.instance);
  }
);

Goのインスタンスを生成

インスタンス化するためにはwasm_exec.jsが必要となります。

manifest

manifest.json
{
  "manifest_version": 2,
  "name": "Clip Go",
  "author": "ramenjuniti",
  "description": "This is a chrome extension for running a selected golang code in The Go Playground.",
  "version": "1.1.0",
  "content_scripts": [
    {
      "matches": ["<all_urls>"],
      "js": ["contents.bundle.js"]
    }
  ],
  "background": {
    "scripts": ["background.bundle.js"],
    "persistent": false
  },
  "permissions": [
    "activeTab",
    "contextMenus",
    "tabs",
    "https://play.golang.org/"
  ],
  "content_security_policy": "script-src 'self' 'wasm-eval'; object-src 'self'",
  "icons": {
    "16": "icon16.png",
    "32": "icon32.png",
    "48": "icon48.png",
    "128": "icon128.png"
  }
}

wasmを利用する場合manifest.json

"content_security_policy": "script-src 'self' 'wasm-eval'; object-src 'self'"を追加しないといけないみたい

その他のコードはこちらをhttps://github.com/ramenjuniti/clipgo

つらみ

formatter.wasmのサイズが3.7MBで、すごくでかくなってしまった。

Go + wasmではここらへん問題はまだまだあるみたい

まとめ

  • GoでChrome拡張は作れる

  • Goでしかできないことをブラウザに持ち込める

  • まだまだ課題もある

みなさんも是非、GoでChrome拡張を作ってみてはいかがでしょうか

Clip Goぜひ使ってみて下さい!!

23
10
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
23
10