SPARQLエンドポイント簡易検索アプリをjQueryからVue.jsに書き直しました

RDFデータベースをSPARQLと呼ばれるクエリ言語で検索できるWeb API「SPARQLエンドポイント」を公開するオープンデータサイトが行政を中心に増えています。例えば、

- [ジャパンサーチ](https://jpsearch.go.jp/api/sparql-explain/)
- [統計LOD](http://data.e-stat.go.jp/lodw/sparqlendpoint)
- [gBizInfo](https://info.gbiz.go.jp/api/index.html)
- [大阪市オープンデータ](https://data.city.osaka.lg.jp/api/)
- [京都市オープンデータ](https://data.city.kyoto.lg.jp/%E4%BA%AC%E9%83%BD%E5%B8%82%EF%BD%93%EF%BD%90%EF%BD%81%EF%BD%92%EF%BD%91%EF%BD%8C%E3%82%A8%E3%83%B3%E3%83%89%E3%83%9D%E3%82%A4%E3%83%B3%E3%83%88%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6)
- [埼玉県オープンデータ](https://opendata.pref.saitama.lg.jp/events/264.html)
- [青森県オープンデータ](https://opendata.pref.aomori.lg.jp/sparql/index.html)
など

これらのサイトの多くは、Webブラウザ上でSPARQLクエリを実行して、表形式で検索結果を見られるようになっています。
しかし、Web APIなので本来は仕様に基づいた形式でエスケープ処理をしたSPARQLクエリをGETもしくはPOSTしなけれならないので、上記のような気の利いたページを公開しないSPARQLエンドポイントもあります。

そこでだいぶ前にWebブラウザ上でSPARQLエンドポイントへ簡単にSPARQLクエリを実行できて、その結果を見やすい表形式で表示するWebアプリをjQueryで作りました。

JavaScriptによるSPARQL利用サンプル(クエリ検索アプリ)
https://github.com/uedayou/simple-sparqlsearch-js

![JavaScriptによるSPARQL利用サンプル(クエリ検索アプリ)](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62575/45077fd2-b98a-5533-e6d2-418de0d43372.jpeg)

かなり古いアプリですが、今でも問題なく動きます。ただ、今見るとコードの可読性が悪いなとおもいます。とても短いコードでもあるので、jQueryを使わずに書き直すことにしました。

## Vue.js で書き直し

今回は [Vue.js](https://jp.vuejs.org/index.html) を使うことにしました。React, AngularでもWebアプリを書いたことがありますが、今回はできるだけコード量を減らして、Webpack等でのビルドを行わない形にしたかったので、Vue.jsを使いました。

Vue.jsによるSPARQLエンドポイント検索アプリ
https://github.com/uedayou/simple-sparqlsearch-vue

![Vue.jsによるSPARQLエンドポイント検索アプリ](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62575/ee3afc95-ac3e-2107-911e-b25b67cc1ebe.jpeg)

機能としては全く同じですが、検索した結果をJSONファイルでダウンロードできるようにしました。

![ダウンロード](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62575/ec2c0e25-b06d-6417-832f-46beeb064108.jpeg)

検索結果が表示されているときに、ダウンロードボタンを押すとJSONファイルがダウンロードできます。

## Vue.js vs jQuery

Vue.js で書き直してみて、jQueryのコードよりも個人的には格段に可読性が上がったと思います。
jQueryは、DOM操作をコード内で行わないといけなかったのが、Vue.jsだとDOMにデータをバインディングできるので書き直すのも楽にできました。

```javascript:Vue.jsのコード
Vue.use(VueLoading);
Vue.component('loading', VueLoading);

var app = new Vue({
  el: '#app',
  data: {
    query: 'select * where {?s ?p ?o} LIMIT 10',
    results: {
      data: null,
      head: [],
      body: [],
    },
  },
  methods: {
    doSearch: function() {
      var loader = this.$loading.show();
      var that = this;
      axios.get(
        endpoint+"?query="+encodeURIComponent(this.query),
        { headers: {'Accept': 'application/sparql-results+json'} })
      .then(function(res) {
        that.results.data = res.data;
        that.results.head = res.data.head.vars;
        that.results.body = res.data.results.bindings;
      })
      .catch(function(error) {
        console.log(error);
        alert("Error!");
      })
      .then(function() {
        loader.hide();
      });
    },
    downloadData: function() {
      var filename = "results.json";
      var a = document.createElement('a');
      var uriContent = 'data:application/octet-stream,'+encodeURIComponent(JSON.stringify(this.results.data));
      a.setAttribute('href', uriContent);
      a.setAttribute('download', filename);
      document.body.appendChild(a);
      a.click();
      document.body.removeChild(a);
    }
  }
})

```

```javascript:jQueryのコード
(function(){
  $.fn.modal.defaults.spinner = $.fn.modalmanager.defaults.spinner = '<div class="loading-spinner" style="width: 200px; margin-left: -100px;"><div class="progress progress-striped active"><div class="progress-bar" style="width: 100%;"></div></div></div>';
  $('#find_query').click(function(){
    $('body').modalmanager('loading').find('.modal-scrollable').off('click.modalmanager');
    qr = sendQuery(endpoint,encodeURIComponent($('#query_area').val().replace(/[\n\r]/g,"")));
    qr.fail(
      function (xhr, textStatus, thrownError) {
        $('body').modalmanager('removeLoading');
        alert("Error: A '" + textStatus+ "' occurred.");
      }
    );
    qr.done(
      function (d) {
        $('body').modalmanager('removeLoading');
        $('body').removeClass('modal-open');
        result_table(d.results.bindings);
      }
    );
  });
  $('#result_div').hide();
}());

var result_table = function(data){
  var result_div = $('#result_div');
  var table = $('#result_list')[0];
  if (table == undefined) {
    result_div.append($('<table></table>').attr({
      'id' : 'result_list',
      'class' : 'table'
    }));
    table = $('#result_list')[0];
  }
  while (table.rows.length > 0) { table.deleteRow(0); }
  if (data instanceof Array) {
    result_div.show();
    var header = table.createTHead();
    var headerRow = header.insertRow(0);
    id = 1;
    for (var d = 0; d < data.length; d++) {
      var row1 = table.insertRow(d + 1);
      if (d == 0) {
        for ( var key in data[0]) {
          var th = document.createElement('th');
          var label = key;
          th.innerHTML = key;
          headerRow.appendChild(th);
        }
      }
      var i = 0;
      for ( var key in data[d]) {
        var cell = row1.insertCell(i++);
        var value = data[d][key];
        if (value.value != undefined){value = value.value;}
        if (value == null) {value = '';}
        var link = true;
        if (link) {
          if (value != null && value.indexOf("http://") == 0) {
            value = '<a href="'+value+'" target="_blank">'+value+'</a>';
          }
        }
        cell.innerHTML = value;
      }
    }
  }
};
```