Go言語でJavaScriptを使ってオリジナルなコマンドシェルを作る（Webインターフェースを追加）

# 概要

今まではコンソールから入力するだけでしたが、いろいろ機能を追加してテストするときJavaScriptの入力が大変になってきます。`load`関数はありますが。そこで、今回はWebインターフェースを追加して楽に開発しましょう。Webポートを開いてサーバー側のJavaScriptが実行できるのでセキュリティに十分配慮してください。

- [Go言語でJavaScriptを使ってオリジナルなコマンドシェルを作る](https://qiita.com/h6591/items/6dec513868360becd02e)
- [Go言語でJavaScriptを使ってオリジナルなコマンドシェルを作る（EXCELの読み込みオブジェクトの追加)](https://qiita.com/h6591/items/abf5ef386f92db01a01c)

の知識を前提としています。

# 実装

## Webインターフェースを実装する前の準備

今まで作成したプログラムの一部をWebインターフェースに合わせて変更します。

`jsRuntime`に`out io.Writer`を追加します。これは`jsprint`の出力先がコンソールかブラウザかを決めるためです。

```go
type jsRuntime struct {
	runtime   *goja.Runtime
	stringify goja.Callable
	program   *goja.Program
	out       io.Writer  //<==追加
}
```

`func initialSetting`の一部を変更します。

```go	
	//変更前
	js := &jsRuntime{runtime: rt} 

	//変更後
	js := &jsRuntime{runtime: rt, out: os.Stdout} 
```

さらに、`jsprint`関数内で `fmt.Fprintf`に変更します。`//web`がついているところが変更箇所です。

```go
func (js *jsRuntime) jsprint(vals ...goja.Value) {
	rt := js.runtime
	format := "%v"
	for _, val := range vals {
		str, ok := val.Export().(string)
		if ok {
			fmt.Fprintf(js.out, format, str) //web
			format = " %v"
			continue
		}
		v, err := js.stringify(goja.Undefined(), val)
		if err != nil {
			rt.Interrupt(rt.NewGoError(err))
			return
		}
		fmt.Fprintf(js.out, format, v) //web
		format = " %v"
	}
	fmt.Fprintln(js.out) //web
}
```

## Webインターフェースの実装

まず,`func initialSetting`でJavaScriptにGo言語のWebインターフェースの関数をセットします。

```go
	rt.Set("web", js.execWebServer)
```

続いて`execWebServer`や関連する関数です.

```go
import (
	"bytes"
	"fmt"
	"net/http"
	"os"
)

var isExecWeb = false

func (js *jsRuntime) execWebServer(port int) {
	if isExecWeb {
		js.runtime.Interrupt("already executed")
		return
	}
	isExecWeb = true
	go js.webServer(port)
}

func (js *jsRuntime) webServer(port int) {
	http.Handle("/html/", http.StripPrefix("/html/", http.FileServer(http.Dir("html/"))))
	http.HandleFunc("/script", js.routeScript)
	http.ListenAndServe(fmt.Sprintf("localhost:%d", port), nil)
}

func (js *jsRuntime) routeScript(w http.ResponseWriter, r *http.Request) {
	js.out = w
	defer func() { js.out = os.Stdout }()
	bufferBody := new(bytes.Buffer)
	bufferBody.ReadFrom(r.Body)
	reqBody := bufferBody.String()
	val, err := js.runtime.RunScript("web", reqBody)
	if err == nil {
		js.jsprint(val)
	} else {
		js.jsprint(js.runtime.ToValue(fmt.Sprintf("%v\n", err)))
	}
}
```

JavaScriptからの引数はポート番号です。 カレントディレクトリの下に`html`というディレクトリを作ってそこに必要なHTMLを置きます。JavaScriptを実行させるURLは`/script`としました。
`js.out = w`でJavaScriptの表示がブラウザになります。処理終了時には`os.Stdout`に戻します。これでコンソールからもWebからも入力できます。ただし、JavaScriptの`Runtime`は共有していますので、同時には実行しないでください。Webインターフェース同士の同時処理もしないように注意が必要です。

続いてHTMLです。ファイル名は`script.html`として`html`ディレクトリに置きます。また、`jQuery`を使用しています。

```html
<!DOCTYPE html>
<html lang="ja">

<head>
    <meta charset="UTF-8">
    <title>JavaScript Shell</title>
    <script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
</head>

<body>
    <form>
        <textarea cols="60" rows="5" id="query" spellcheck="false" wrap="off"></textarea>
    </form>

    <button id="ajax" onclick="postAjax()">run</button>
    <div id="output"></div>

    <script>
        function postAjax() {
            sendData = $('#query').val()
            $.ajax({
                url: '/script',
                type: 'POST',
                data: sendData
            }).done((data) => {
                ajaxSuccess(data);
            }).fail((data) => {
                ajaxFail(data);
            });
        }

        function ajaxSuccess(result) {
            document.getElementById("output").innerHTML = `<pre>${result}</pre>`
        }

        function ajaxFail(result) {
            document.getElementById("output").innerHTML = result
        }
    </script>
</body>
</html>
```

実行

```javascript
go run main.go
> web(8080)
undefined
> 
```

ブラウザで実行した結果です

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/258007/dcb7b4e1-990d-7a94-1180-b67c2f33a720.png)

次のJavaScriptでHTMLを生成してそれをブラウザで表示させます。残念ながら`HTML`をエンコードする関数は用意していないのでそのまま出力します。


```html:script.html
document.getElementById("output").innerHTML = `<pre>${result}</pre>`
```
を次のように変更してHTMLを有効にします。

```html:script.html
document.getElementById("output").innerHTML = result
```

下記のJavaScriptを実行しています。最後の`""`は`undefined`がでないように戻り値を空文字にしています。一度作成したScriptはファイルにしてブラウザからは`load`で実行することもできます。

```javascript
print ("<table border=1><th>A</th><th>B</th><th>C</th></tr>")
var asum = 0
var bsum = 0
excel("Book1.xlsx", "Sheet1").ForEachArray(function(ix, row) {
   print("<tr>")
   asum += row[0]
   bsum += row[1]
   row.forEach(function(cell){
     print("<td>", cell, "</td>")
   })
   print("</tr>")
})
print("<tr><td>",asum, "</tb><td>", bsum ,"</td><td>合計</td></tr>")
""
```

実行結果です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/258007/03df7c92-70a8-fccf-8b48-0c40522a1dff.png)

2次元データの文字列にしてデータだけブラウザに返しブラウザ側で`eval`を実行、それからJavaScriptのデータとしてブラウザ側で処理することも可能でしょう。

# まとめ

これで開発も楽になります。また、大量の出力データもコンソールよりはブラウザの方が見やすいです。
Web機能は実行できましたがWeb機能だけ止める方法はよくわかりませんでした。それらしいパッケージはありましたが試していません。なのでWeb機能を停止したいときは一度プログラムを終了してから再起動してください。これで十分だと思っています。