ブックマークレットからFirestoreのデータを読み書きする

毎度悩むBookmarkletのjsファイルのホスト先をFirebaseにしてみたら、何かいいことあるかなと試したもの

#使ったもの
毎度のことならがWindows環境です
- Windows10
- Cmder
- Node.js (v10.15.3)
- npm (6.4.1)
- javascript

#Firebaseを使えるようにするところまで
Firebaseは知っていたが使ったことは無かったので、初めてのFirebaseです。

##やったこと
###node.jsのインストール
何も考えずデフォルトの```C:\Program Files\nodejs```　にインストール

###環境変数の編集
コンピュータ＞システムの詳細設定＞環境変数で、ユーザの環境変数のPATHに```C:\Program Files\nodejs```を追加
![image.png](https://qiita-image-store.s3.amazonaws.com/0/168315/567f855a-2faf-9344-0c9d-4c0beef8164e.png)

コマンドプロンプトでパスが通ったか確認
nodejsをインストールしたディレクトリ以外の場所で叩ければOK。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/168315/8d14a252-d12d-0f04-b3e0-bdd7fb5fc9dd.png)

大丈夫ですね

##Firebaseのツールをインストール
npmのパスが取ってれば、どこで実行してもOKです。実行場所とインストール場所は無関係です。

```
npm install -g firebase-tools
> @google-cloud/functions-emulator@1.0.0-beta.5 postinstall C:\Users\xxx\AppData\Roaming\npm\node_modules\firebase-tools\node_modules\@google-cloud\functions-emulator

(省略)

+ firebase-tools@6.5.0
added 556 packages from 274 contributors in 114.727s
```

Windowsのユーザーディレクトリの中にいろいろ保存されます。
ここまでで準備完了。

##Firebaseのプロジェクト作成
ブラウザで、[Firebase Console](https://console.firebase.google.com/?hl=ja) へ行き、プロジェクトの追加をする
![image.png](https://qiita-image-store.s3.amazonaws.com/0/168315/5f2721f6-dbeb-ec6c-4e58-9d22944c0657.png)

プロジェクト名とかは適当につける。アナリティクスに地域を日本に、ロケーションをasia-northeast1(東京)に変更しておきました。なんとなく


##Firebaseにアップロードする作業場所を準備

まず、ローカルフォルダを作る。
Windows上に、```C:\firebase_sample``` フォルダを作成。空っぽのまま

次に、コマンドプロンプトで、```C:\firebase_sample```　に行き、```firebase init``` を実行
いくつか質問を聞かれるので、それっぽく答える。

```
cd C:\firebase_sample
firebase init

Are you ready to proceed? 
YES

Which Firebase CLI features do you want to setup for this folder? Press Space to select features, then Enter to confirm your choices
Hostingを選択

Project Setup
先ほどブラウザで作っておいたプロジェクトが出てくるので、選択

What do you want to use as your public directory? 
public

Configure as a single-page app (rewrite all urls to /index.html)?
No
```

終わるとこんな感じになるはずです。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/168315/a789aeb9-62bc-ba5e-abe9-4665c4f4bf3a.png)


##ファイルを作ってFirebaseにアップロード
静的なファイルをアップロードする環境が整ったので、アップロードしてみます。
過去に作った単体で動くブックマークレットを持ってきて、```C:\firebase_sample\public\sample.js```として置きました。

```javascript:public/sample.js
javascript:(
    function(){
      title = document.title;
      href = document.location.href;
      canonical = get_canonical();
      console.log('ページタイトル=%s \n href=%s \n カノニカルURL=%s', title, href, canonical);
      alert('ページタイトル：'+title +"\n現在のURL："+href + "\nカノニカルURL：" + canonical);

      function get_canonical(){
        links = document.getElementsByTagName("link");
        for ( i in links) {
            if (links[i].rel) {
                if (links[i].rel.toLowerCase() == "canonical") {
                    return links[i].href;
                }
            }
        }
        return "";
      }
    }
  )();
```

ファイルをアップロードするには、```deploy```コマンドです。

```
C:\firebase_sample
λ firebase deploy

=== Deploying to 'bookmarklet-25d13'...

i  deploying hosting
i  hosting[bookmarklet-25d13]: beginning deploy...
i  hosting[bookmarklet-25d13]: found 3 files in public
+  hosting[bookmarklet-25d13]: file upload complete
i  hosting[bookmarklet-25d13]: finalizing version...
+  hosting[bookmarklet-25d13]: version finalized
i  hosting[bookmarklet-25d13]: releasing new version...
+  hosting[bookmarklet-25d13]: release complete

+  Deploy complete!

Project Console: https://console.firebase.google.com/project/bookmarklet-25d13/overview
Hosting URL: https://bookmarklet-25d13.firebaseapp.com
```

終わった。ブラウザでアクセスしてみる。
ファイルのURLは、https://bookmarklet-25d13.firebaseapp.com/sample.js　になります。

ちゃんと取れますね。
次は、ブックマークレット経由で```sample.js```を取ります。

```
javascript:(function(d,j,s){s=d.createElement('script');s.src=j;d.body.appendChild(s);})(document,'https://bookmarklet-25d13.firebaseapp.com/sample.js')
```

当然ながらちゃんと動きました。
ブラウザのブックマークにコードを埋め込まない場合は、closure compilerは不要ですね。そのかわり、ブックマークレットを起動するためにインターネット接続が必要になります。

ここまでなら、作ったブックマークレットのJSファイルをどこに置くかの問題の解決だけです。無理してFirebaseにしなくても、herokuでもCDNのcloudinaryでも同じです。せっかくFirebaseを使うので、ブックマークレットからFirestoreと連携させてみます。

#FireStoreとつないでみる
ブックマークレットのコードからFirestoreに繋げてみます。作るものは、楽天市場の商品検索結果に表示されている商品の情報を刈り取り、Firestoreに放り込むサンプルです。誰トクな感じです。スクレイピングでいいだろ、というのは置いときます。

実行すると、こんな感じで商品の名前、売っている店の名前、各種IDが保存されます。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/168315/81ca1f08-a0ce-2eb8-29e0-2616446b488e.png)


##Firestoreを使う前準備
Firebase ConsoleのDatabaseへ行きFirestoreを有効にします。Realtime Databaseではないので間違いないように。
Firestore内に```rakuten```という名前のコレクションを作っておきます。

##ブックマークレットのコード
楽天の検索結果ページで実行すると、表示されている商品情報がFirestoreにぶっこまれるブックマークレットです。2019年3月のマークアップに従っていますので、HTMLの構造が変わると動かなくなります。


```javascript:public/item-to-firestore.js
javascript:(function(f){

    loadFirebase();

    function loadFirebase(){
        console.log('start loading firebase.js');
        s=document.createElement('script');
        s.src='https://www.gstatic.com/firebasejs/5.7.1/firebase.js';
        s.onload=loadFirebaseApp;
        document.body.appendChild(s);
    }
    function loadFirebaseApp(s){
        console.log('start loading app.js');
        s=document.createElement('script');
        s.src='https://www.gstatic.com/firebasejs/5.7.1/firebase-app.js';
        s.onload=loadFireStore;
        document.body.appendChild(s);
    }
    function loadFireStore(s){
        console.log('start loading firebase.js');
        s=document.createElement('script');
        s.src='https://www.gstatic.com/firebasejs/5.7.1/firebase-firestore.js';
        s.onload=loadJquery;
        document.body.appendChild(s);
    }
    function loadJquery(s){
        console.log('start loading jquery');
        s=document.createElement('script');
        s.src='https://code.jquery.com/jquery-3.3.1.slim.min.js';
        s.onload=function(){f(jQuery.noConflict(true));};
        document.body.appendChild(s);
    }
})(
    function($){

        var config = {
            apiKey: "xxx", //change your ID
            authDomain: "bookmarklet-25d13.firebaseapp.com",
            databaseURL: "https://bookmarklet-25d13.firebaseio.com",
            projectId: "bookmarklet-25d13",
            storageBucket: "bookmarklet-25d13.appspot.com",
            messagingSenderId: "000"  //change your ID
        };
        firebase.initializeApp(config);
        
        var db = firebase.firestore();
        const settings = { timestampsInSnapshots: true };
        db.settings(settings);

        var config=null;

        db.collection("configs").doc("rakuten").get()
        .then((doc)=>{
            config = doc.data();
            console.log(config);
        }).then(()=>{
            showForm();
        });

        console.log('END');

        
        function showForm(){
            form = 
            '<div>'+
            '<button id="_getitem">情報取得</button>'
            '</div>';
            $('body').append(form);

            $('#_getitem').on('click', function(){
                db.collection("configs").doc("rakuten").get()
                .then((doc)=>{
                    config = doc.data();
                }).then(()=>{
                    list = getItem();
                    console.log(config.max_item, list.length);
                    
                    //ひとつずつ保存
                    for(var i=0; i<config.max_item && i<list.length; i++){
                        console.log(i);
                        saveItemToFirestore(list[i]);
                    }

                    //まとめて保存
                    saveItemsToFirestore(list);
                });

            });
        }
        function getItem(){
            itemlist =[];
            divs = $('div.dui-item.searchresultitem').each(function(i,e){
                item={};
                item.i = i;
                item.id = $(e).data("id");
                item.sid = $(e).data("shop-id");
                item.item_name = $(e).find("div.content div.description.title h2 a").first().attr('title');
                item.shop_name = $(e).find("div.content div.description.merchant a").first().text();
                item.price = $('div.content.description.price span').first().text();
                console.log(item);
                itemlist.push(item);
            });
            return itemlist;
        }

        function saveItemToFirestore(item){
            db.collection('rakuten').add(item).then(function(docRef) {
                console.log("Document written with ID: ", docRef.id);
            });
        }

        function saveItemsToFirestore(itemlist){
            var batch = db.batch();
            for(i=0; i<config.max_item && i<itemlist.length; i++){
                docid = itemlist[i].sid + '-' + itemlist[i].id;
                console.log('(%s) > %s', i, docid);
                var ref = db.collection("rakuten").doc(docid);
                batch.set(ref, itemlist[i]);
            }
            batch.commit().then(function () {
                console.log('success');
            });
        }
    }
)
```

#コードの解説
##SDK読み込み部分

```firebase.js```, ```firebase-app.js```, ```firebase-firestore.js```の順に読み込む必要があるようで、野暮ったいコードになってます。やっていることは、```loadFirebase()```で作ったscriptタグのonloadに```loadFirebaseApp()```を仕掛け、```loadFirebaseApp()```で作られるscriptタグのonloadに```loadFirestore()```関数を仕掛けているだけです。
最後にjQueryをロードしています。これはfirebaseやfirestoreと全く関係ありません。


##firebaseの初期化部分

```javascript

var config = {
    apiKey: "xxx",
    authDomain: "bookmarklet-25d13.firebaseapp.com",
    databaseURL: "https://bookmarklet-25d13.firebaseio.com",
    projectId: "bookmarklet-25d13",
    storageBucket: "bookmarklet-25d13.appspot.com",
    messagingSenderId: "000"
};
firebase.initializeApp(config);

var db = firebase.firestore();
const settings = { timestampsInSnapshots: true };
db.settings(settings);
```

firebaseのwebアプリケーションを作るときと同じようなコードです。自分のfirestoreに繋ぐためのSDKのコードは、コンソールの「アプリを追加」から左から3番目のボタンをクリックして入手します。configデータをfirebase consoleから貼り付けるだけです。TIMESTAMP系のエラーが出るので```setting```を1つ足しています。


![image.png](https://qiita-image-store.s3.amazonaws.com/0/168315/03d92912-312a-ada4-eda2-df1e8c1e0aac.png)

![image.png](https://qiita-image-store.s3.amazonaws.com/0/168315/0675dc2d-7a27-fd43-a1ac-0eb3a04815e3.png)




##firebaseからデータの読み込みをする部分

```javascript
var config=null;

db.collection("configs").doc("rakuten").get()
.then((doc)=> {
      config = doc.data();
      console.log(config);
}).then(()=>{
      showForm();
});
```

Firestoreからデータを読み込みしてみます。
ブックマークレットだからといって特別なことはなく、普通のfirebaseのWebアプリケーションと同じ仕組みで行います。今回は、検索結果のうち何件を処理するかをfirebase側に定義をし、その値に従ってスクリプトが動作を変えるようにしてあります。このような仕組みを入れておくとブックマークレット内に定数が入らず、コードの修正をしないで動きを変えることができるようになります。

firebaseに置いてあるconfigの設定内容
![image.png](https://qiita-image-store.s3.amazonaws.com/0/168315/e8256543-0f79-8057-27c7-6e49b8769aa8.png)

データ取得が終わったら、```showForm()```を実行します。


##formを作る処理

```javascript
function showForm(){
    form = 
    '<div>'+
    '<button id="_getitem">情報取得</button>'
    '</div>';
    $('body').append(form);

    $('#_getitem').on('click', function(){
        db.collection("configs").doc("rakuten").get()
        .then((doc)=>{
            config = doc.data();
        }).then(()=>{
            list = getItem();
            console.log(config.max_item, list.length);
            
            //ひとつずつ保存
            for(var i=0; i<config.max_item && i<list.length; i++){
                console.log(i);
                saveItemToFirestore(list[i]);
            }

            //まとめて保存
            saveItemsToFirestore(list);
        });

    });
}
```

jQueryを使ってボタンを1つ生成しています。作ったボタンにイベントハンドラを仕掛けておきます。
ボタンを押したときにやることは、
1. firebaseのconfigの最新値を読み直す
2. ```getItem()```で画面のHTMLから情報を抜きだす
3. ```saveItemToFirestore()```で保存する

クリックイベントには骨格だけを定義してあります。具体的な処理は、以降の関数に書いてあります。

##画面からデータを抜き出す部分
jQueryを駆使して取り出しています。firestoreに入れやすいように、```item={};```に格納しておきます。

consoleに出力するとこんな感じです。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/168315/e884587a-653f-fa01-7880-8702189027b8.png)


##firestoreに放り込む部分

```javascript
function saveItemToFirestore(item){
    db.collection('rakuten').add(item).then(function(docRef) {
        console.log("Document written with ID: ", docRef.id);
    });
}
```

これもチュートリアルレベルの使い方です。collectionに対して```add()```するだけです。javascriptのオブジェクト形式（連想配列形式）でデータをaddに渡せはOKです。firebaseのSDKのおかげでデータの保存は2行で済みます。楽ですね。document(collectionの下の要素)を指定しないで放り込んでいるので、ドキュメントIDは登録時にfirestore側に適当に振られます。

##firestoreに放り込む部分（バッチ）

```javascript
function saveItemsToFirestore(itemlist){
    var batch = db.batch();
    for(i=0; i<config.max_item && i<itemlist.length; i++){
        docid = itemlist[i].sid + '-' + itemlist[i].id;
        console.log('(%s) > %s', i, docid);
        var ref = db.collection("rakuten").doc(docid);
        batch.set(ref, itemlist[i]);
    }
    batch.commit().then(function () {
        console.log('success');
    });
}
```

こっちは複数のItemを同時に保存する方法です。```firestore```のインスタンスから```batch```というインスタンスを作って、そこにデータを預けておいて最後にまとめて```commit()```する使い方です。HTTP/Sの接続数が少ないので効率がよいです。単一の保存と違いdocument idを定義して保存するようにしています。

#実行してみる
firebase consoleでコレクションを削除しておきます。コレクションが無くても、外部から保存すると自動的に作られます。

javascriptのコードをdeployして、ブラウザバーにコードを入れてみます。
カリカリ動いて・・・

![image.png](https://qiita-image-store.s3.amazonaws.com/0/168315/4465981b-59d9-3345-32a0-3ced897b3c5d.png)

こんな感じでデータが保存されています。とっても簡単。

#まとめ
今回はブックマークレットからFirestoreを扱ってみました。
今までもブックマークレットからデータを保存する処理はやっていたのですが、PHPでRESTなAPIを作ってAPI経由でデータを保存していました。それに比べたらAPIを作る手間もなく、SDKが提供されているので保存する処理も簡単にできました。
ブックマークレットとFirebaseを組み合わせるというダレトクな記事ですが、何かのヒントになることを期待しましょう。