GoでChrome拡張を作った話

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

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

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

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

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

http://ramenjuniti.hatenablog.com/entry/2019/04/29/034537

:point_up:の記事に載ってる選択したGoのコードを[The Go Playground](https://play.golang.org/)で簡単に実行できるChrome拡張に
**Goのフォーマット機能を追加しました!!**

### Demo
こんな感じで動きます
![clipgo_demo1.1.0min.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184505/53be2bc1-5fee-bc75-daee-9e48141bdeff.gif)

### 追加機能

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

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

### GitHubはこちら

https://github.com/ramenjuniti/clipgo

### ダウンロードはこちら

[![](https://developer.chrome.com/webstore/images/ChromeWebStore_BadgeWBorder_v2_340x96.png)](https://chrome.google.com/webstore/detail/clip-go/khkfegmjjbijinlbmffohmiofhpnjlja)

## Go + WebAssemblyとは

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

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

* ```GOOS=js GOARCH=wasm```で```go build```すると```.wasm```ファイルが生成される

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

詳しくはこちらを

https://github.com/golang/go/wiki/WebAssembly

## 構造

ディレクトリ構造

```
.
├── 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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184505/ac9ea88c-9cc4-56e6-0c1f-2b7f6227b79f.png)
簡単な図はこんな感じです

## ソースコード

### contents

```js:contents.js
document.addEventListener("selectionchange", () => {
  chrome.runtime.sendMessage({ code: window.getSelection().toString() });
});
```
`background.js`に選択した文字列をChromeAPIで送ります

### background

```go: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に登録した関数も呼べなくなってしまいます。

```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](https://play.golang.org)を開き、フォーマットした文字列を貼って実行する

```js:init.go
const go = new Go();

WebAssembly.instantiateStreaming(fetch("formatter.wasm"), go.importObject).then(
  result => {
    go.run(result.instance);
  }
);
```
Goのインスタンスを生成

インスタンス化するためには[wasm_exec.js](https://github.com/golang/go/blob/master/misc/wasm/wasm_exec.js)が必要となります。

### manifest

```json: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ぜひ使ってみて下さい！！