概要
- サンプルデータの準備
- バックエンド実装
- HTMLテンプレート
- フロントエンド実装
モチベーション
Excelで管理されているデータベース()のデータをFZFライクに絞り込み検索できるようにします。
全員のPC内にfzfやらターミナルやらxlsx2csvのようなツールが入っていないので、ブラウザの表示を通してサーバー経由でExcelのFuzzy検索をできるようにします。
完成形
ディレクトリ構成
.
├── data
│ └── sample.xlsx
├── main.go
├── go.mod
├── go.sum
├── template
│ └── index.tmpl
└── static
├── main.js
├── main.ts
├── node_modules
├── package-lock.json
├── package.json
└── tsconfig.json
サンプルデータの準備
Sample excel data for analysisのxlsxファイルをいただきました。
ダウンロードしてdata/sample.xlsx へ入れておきます。
バックエンド実装
Go言語のginというフレームワークでRESTFUL APIサーバーを立てます。
また、Excelデータベース()を解析するため、excelizeというライブラリも使用しますので、あらかじめインストールしておきます。
$ go install github.com/gin-gonic/gin
$ go install github.com/xuri/excelize/v2
main.goはこちらです。
簡単なサンプルを目指すため、バックエンドはmain.go の1ファイルのみです。
package main
import (
"net/http"
"strings"
"github.com/gin-gonic/gin"
"github.com/xuri/excelize/v2"
)
var lines []string
// Excelデータ読み込み
// 時間がかかるとmain関数へなかなか行かない、
// つまりブラウザのページ読み込みがストップしてしまうので、
// init()の中身は非同期処理で裏で読み込みをかけておきます。
// ページを読み込んだ時点でのデータを配信するので、
// 不完全なデータになる場合もあります。
// そういうときは、ブラウザ上でリロードかけると
// 最新のデータを配信してくれます。
func init() {
go func() {
path := "./data/sample.xlsx"
f, _ := excelize.OpenFile(path)
defer f.Close()
// 行ごとに読み込み
// 2次元配列で返す
rows, _ := f.GetRows("Sheet1")
lines = make([]string, len(rows))
for i, row := range rows {
lines[i] = strings.Join(row, " ")
}
}()
}
// サーバー立ち上げ
func main() {
r := gin.Default()
r.Static("/static", "./static")
r.LoadHTMLGlob("template/*.tmpl")
// エントリポイント
r.GET("/", func(c *gin.Context) {
c.HTML(http.StatusOK, "index.tmpl", gin.H{})
})
// list形式のJSONを配信するAPI
r.GET("/list", func(c *gin.Context) {
c.IndentedJSON(http.StatusOK, lines)
})
r.Run()
}
main.goを書いたら、プロジェクトルートディレクトリにて$ go mod init && go mod tidy
してgo.mod, go.sumを生成します。
HTMLテンプレート
フロントエンドはJavaScript(TypeScript)で動作をつけるため、HTMLは簡単なものにします。
<html lang="ja">
<body>
<label for="search-form">Excel検索</label>
<!-- ここで検索 -->
<input type="text" name="search-form" id="search-form" placeholder="検索キーワードを入力" >
<!-- ここに結果を表示 -->
<div id="search-result" ></div>
<script type="module" src="/static/main.js"></script>
</body>
</html>
input要素で検索ワードを入力し、divに結果を表示します。
Goのmain関数の r.LoadHTMLGlob("template/*.tmpl")
で読み込み指定しています。
<script type="module" src="/static/main.js"></script>
でstatic/main.jsを読み込みます。
フロントエンド実装
Fuzzy検索を実装していきます。
staticにmain.tsを書きます。
staticディレクトリにて、$ tsc --init
を実行し、tsconfig.jsonを生成します。
$ npm i --save fzf
でnode_modules
にfzfをインストールしておきます。
import { Fzf } from "./node_modules/fzf/dist/fzf.es.js";
main();
async function fetchPath(url: string) {
return await fetch(url)
.then((response) => {
return response.json();
})
.catch((response) => {
return Promise.reject(
new Error(`{${response.status}: ${response.statusText}`),
);
});
}
// ほぼ公式通りの実装
function fzfSearch(list: string[], keyword: string): string[] {
const fzf = new Fzf(list);
const entries = fzf.find(keyword);
const ranking: string[] = entries.map((entry: Fzf) => entry.item);
return ranking;
}
async function main() {
const url: URL = new URL(window.location.href);
// サーバーからExcelの行を取得
const list = await fetchPath(url.origin + "/list");
const searchInput = document.getElementById("search-form");
const resultOutput = document.getElementById("search-result");
// キーを押すたびにページ内容更新
searchInput.addEventListener("keyup", () => {
// 要素クリア
while (resultOutput.firstChild) {
resultOutput.removeChild(resultOutput.firstChild);
}
// fzf検索
const result: string[] = fzfSearch(list, searchInput.value);
// 検索結果をコンソールに表示
console.log(result);
// 検索結果を結果要素に表示
result.map((line: string) => {
const p = document.createElement("p");
const text = document.createTextNode(line);
p.appendChild(text);
resultOutput.append(p);
});
});
}
main.jsがindex.tmplが読み込まれるとmain()関数が呼ばれ、
main.goのAPI localhost:8080/list
を呼び出してリスト形式のJSONに格納されたExcelデータを読み込みます。
さらに"search-form" 要素にイベントリスナーを登録し、input要素にキー入力するたびに serchInput.addEventListener()
のコールバック関数が実行されます。
これでキー入力のたびに"resultOutput"要素のリセットと、fzfSearch()
関数が働き、
fzfSearch()
の結果を"resultOutput" 要素にpタグで書き込みます。
main.tsが書けたら$ npx tsc
を実行し、main.jsを生成します.
実行
$ go run main.go
でサーバーを起動し、ブラウザのアドレス欄にlocalhost:8080
で接続します。
最後に、検索窓に適当な文字列を打って、データが絞り込まれて表示されることを確認します。