20190415のJavaScriptに関する記事は22件です。

ブラウザのタブのタイトルが動くサイトが面白い

こんにちは、ブログ「学生ブロックチェーンエンジニアのブログ」を運営しているアカネヤ(@ToshioAkaneya)です。

ブラウザのタブのタイトルが動くサイトが面白い

こちらのサイトにアクセスして、タブに表示されるタイトルに注目してみて下さい。
https://patrykadas.com/browser.html

...なんと、タイトルが横に流れています。
タイトルが長すぎてタブに入りきらないというのは日常茶飯事ですので、これは素晴らしいです。

どうやっているのか調べてみると、<title>タグの中身をJavaScriptで書き換えていました。

便利で面白いギミックですので、ぜひご自身のサイトで実装してみてはいかがでしょうか。

はてなブックマーク・Pocketはこちらから

はてなブックマークに追加
Pocketに追加

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

FirebaseのRealtime Databaseを触ってみる

FirebaseのRealtime Databaseをテスト的に使用した際のログ

Realtime Databaseとは?

NoSQL クラウド データベースでデータの保管と同期を行うことができます。データはすべてのクライアントにわたってリアルタイムで同期され、アプリがオフラインになっても、利用可能な状態を保ちます。

とのこと
ドキュメント : https://firebase.google.com/docs/database/

準備

先にプロジェクトを作成し、Hostingを済ませて置くと楽。
https://qiita.com/rakuraku0615/items/2e2f77437d3557842022

プロジェクトの構成

$ tree
.
├── firebase.json
└── public
    └── index.html

1 directory, 2 files

ローカルではこんなプロジェクトを用意

この資料のゴール

Realtime Databaseを利用したテストwebページを作成する。
- テキストボックスにテキストを入力すると、別タブで開いている同ページにて非同期で反映される。

作業

コンソールより、Firebase接続用のスニペットを獲得する

<script src="https://www.gstatic.com/firebasejs/5.9.4/firebase.js"></script>
<script>
  // Initialize Firebase
  var config = {
    apiKey: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    authDomain: "xxx-xxxx-x-xxxxx.firebaseapp.com",
    databaseURL: "https://xxx-xxxx-x-xxxxx.firebaseio.com",
    projectId: "xxx-xxxx-x-xxxxx",
    storageBucket: "xxx-xxxx-x-xxxxx.appspot.com",
    messagingSenderId: "xxxxxxxxx"
  };
  firebase.initializeApp(config);
</script>

テスト用HTMLを準備

<html>                                                                                                                                                                                   
    <head>                                                                                                                                                                               
        <title>Firebase test 1</title>                                                                                                                                                           <script src="https://www.gstatic.com/firebasejs/5.8.6/firebase.js"></script>                                                                                                     
        <script>                                                                                                                                                                         
            // Initialize Firebase                                                                                                                                                       
            var config = {                                                                                                                                                               
                apiKey: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",                                                                                                                       
                authDomain: "xxx-xxxx-x-xxxxx.firebaseapp.com",                                                                                                                          
                databaseURL: "https://xxx-xxxx-x-xxxxx.firebaseio.com",                                                                                                                  
                projectId: "xxx-xxxx-x-xxxxx",                                                                                                                                           
                storageBucket: "xxx-xxxx-x-xxxxx.appspot.com",                                                                                                                           
                messagingSenderId: "xxxxxxxxx"                                                                                                                                      
            };                                                                                                                                                                           
            firebase.initializeApp(config);                                                                                                                                              
        </script>                                                                                                                                                                        
        <script>                                                                                                                                                                         
            var db = firebase.database();                                                                                                                                                
            var chatAll = db.ref("/chat/all");                                                                                                                                                                                                                                                                                      
            chatAll.on("value", function(snapshot) {                                                                                                                                     
                document.getElementById("textMessage").innerText = snapshot.val().message;                                                                                               
            });                                                                                                                                                                                                                                                                                                                                 
            var changeData = function(){                                                                                                                                                 
                var message = document.getElementById("message").value;                                                                                                                  
                chatAll.set({message:message});                                                                                                                                          
            }                                                                                                                               
            window.onload = function() {                                                                                                                                                 
                document.getElementById("btnChangeData").onclick = changeData;                                                                                                           
            };                                                                                                                                                                           
        </script>                                                                                                                                                                        
    </head>                                                                                                                                                                              
    <body>                                                                                                                                                                               
        <p>Realtime Database</p>                                                                                                                                                  
        <ul>                                                                                                                                                                             
            <li id="textMessage"></li>                                                                                                                                                   
            <li><input type="text" name="" id="message"></li>                                                                                                                            
            <li><input type="button" value="Update" id="btnChangeData"></li>                                                                                                               
        </ul>                                                                                                                                                                            
    </body>                                                                                                                                                                              
</html>

Databaseを開始する

Database => Realtime と遷移し、Databaseを開始する

セキュリティの制限を解放

以下の値にセキュリティルールを更新する。

{
  "rules": {
    ".read": true,
    ".write": true
  }
}

*こちらは全書き込み、読み込みを解放してしまうため、テストが終わったらfalseへ直すこと。

スクリーンショット 2019-04-15 23.28.52.png

deployする

作業プロジェクトをdeployする

 firebase deploy --token "1/hO3xxxxxxxxxxxxxxxxxxxxxxxx........."                                                                

=== Deploying to 'xxx-xxxx-1-xxxx'...

i  deploying hosting
i  hosting[xxx-xxxx-1-xxxx]: beginning deploy...
i  hosting[xxx-xxxx-1-xxxx]: found 2 files in public
✔  hosting[xxx-xxxx-1-xxxx]: file upload complete
i  hosting[xxx-xxxx-1-xxxx]: finalizing version...
✔  hosting[xxx-xxxx-1-xxxx]: version finalized
i  hosting[xxx-xxxx-1-xxxx]: releasing new version...
✔  hosting[xxx-xxxx-1-xxxx]: release complete

✔  Deploy complete!

Project Console: https://console.firebase.google.com/project/xxx-xxxx-1-xxxx/overview
Hosting URL: https://xxx-xxxx-1-xxxx.firebaseapp.com

テスト

タブAで "hoo!" と入力すると
スクリーンショット 2019-04-15 23.37.43.png

タブBに
非同期で反映される
スクリーンショット 2019-04-15 23.37.55.png

ちなみにコンソールを確認すると、このようなデータ構造でストアされている
スクリーンショット 2019-04-15 23.39.41.png

参考にさせて頂いた記事

https://qiita.com/t_furu/items/665bea8b657c78ab2a22

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

本格JavaScript記号プログラミング(1) 6種類の記号だけでJavaScriptを書こう

JavaScriptが記号だけで書ける言語であることはよく知られています。

たとえば、以下のようなプログラムを実行すればHello, World!とコンソールに表示されますね。

[][[[][[[[]==[]]+[]][+[]][+[]]+[[+[]==[]]+[+[]]+[][+[]]+[]][+[]][++[+[]][+[]]+[+[]]]+[[+[]==[]]+[][+[]]][+[]][++[+[]][+[]]+[+[]]]+[[][+[]]+[]][+[]][++[++[+[]][+[]]][+[]]]][[]]=[[][[[[]==[]]+[]][+[]][+[]]+[[+[]==[]]+[+[]]+[][+[]]+[]][+[]][++[+[]][+[]]+[+[]]]+[[+[]==[]]+[][+[]]][+[]][++[+[]][+[]]+[+[]]]+[[][+[]]+[]][+[]][++[++[+[]][+[]]][+[]]]]+[]][+[]][++[++[++[+[]][+[]]][+[]]][+[]]]+[[+[]==[]]+[][[[[]==[]]+[]][+[]][+[]]+[[+[]==[]]+[+[]]+[][+[]]+[]][+[]][++[+[]][+[]]+[+[]]]+[[+[]==[]]+[][+[]]][+[]][++[+[]][+[]]+[+[]]]+[[][+[]]+[]][+[]][++[++[+[]][+[]]][+[]]]]][+[]][++[+[]][+[]]+[+[]]]+[[+[]==[]]+[][+[]]][+[]][++[+[]][+[]]+[+[]]]+[[[]==[]]+[]][+[]][++[++[++[+[]][+[]]][+[]]][+[]]]+[[+[]==[]]+[]][+[]][+[]]+[[+[]==[]]+[]][+[]][++[+[]][+[]]]+[[+[]==[]]+[]][+[]][++[++[+[]][+[]]][+[]]]+[[][[[[]==[]]+[]][+[]][+[]]+[[+[]==[]]+[+[]]+[][+[]]+[]][+[]][++[+[]][+[]]+[+[]]]+[[+[]==[]]+[][+[]]][+[]][++[+[]][+[]]+[+[]]]+[[][+[]]+[]][+[]][++[++[+[]][+[]]][+[]]]]+[]][+[]][++[++[++[+[]][+[]]][+[]]][+[]]]+[[+[]==[]]+[]][+[]][+[]]+[[+[]==[]]+[][[[[]==[]]+[]][+[]][+[]]+[[+[]==[]]+[+[]]+[][+[]]+[]][+[]][++[+[]][+[]]+[+[]]]+[[+[]==[]]+[][+[]]][+[]][++[+[]][+[]]+[+[]]]+[[][+[]]+[]][+[]][++[++[+[]][+[]]][+[]]]]][+[]][++[+[]][+[]]+[+[]]]+[[+[]==[]]+[]][+[]][++[+[]][+[]]]]+[[][[[[]==[]]+[]][+[]][+[]]+[[+[]==[]]+[+[]]+[][+[]]+[]][+[]][++[+[]][+[]]+[+[]]]+[[+[]==[]]+[][+[]]][+[]][++[+[]][+[]]+[+[]]]+[[][+[]]+[]][+[]][++[++[+[]][+[]]][+[]]]][+[]]=[][[[[]==[]]+[]][+[]][+[]]+[[+[]==[]]+[+[]]+[][+[]]+[]][+[]][++[+[]][+[]]+[+[]]]+[[+[]==[]]+[][+[]]][+[]][++[+[]][+[]]+[+[]]]+[[][+[]]+[]][+[]][++[++[+[]][+[]]][+[]]]][[][[[[]==[]]+[]][+[]][+[]]+[[+[]==[]]+[+[]]+[][+[]]+[]][+[]][++[+[]][+[]]+[+[]]]+[[+[]==[]]+[][+[]]][+[]][++[+[]][+[]]+[+[]]]+[[][+[]]+[]][+[]][++[++[+[]][+[]]][+[]]]][[]]]]+[[][[[[]==[]]+[]][+[]][+[]]+[[+[]==[]]+[+[]]+[][+[]]+[]][+[]][++[+[]][+[]]+[+[]]]+[[+[]==[]]+[][+[]]][+[]][++[+[]][+[]]+[+[]]]+[[][+[]]+[]][+[]][++[++[+[]][+[]]][+[]]]][++[+[]][+[]]]=[[+[]==[]]+[]][+[]][+[]]+[[+[]==[]]+[][[[[]==[]]+[]][+[]][+[]]+[[+[]==[]]+[+[]]+[][+[]]+[]][+[]][++[+[]][+[]]+[+[]]]+[[+[]==[]]+[][+[]]][+[]][++[+[]][+[]]+[+[]]]+[[][+[]]+[]][+[]][++[++[+[]][+[]]][+[]]]]][+[]][++[+[]][+[]]+[+[]]]+[[]+[]][+[]][[][[[[]==[]]+[]][+[]][+[]]+[[+[]==[]]+[+[]]+[][+[]]+[]][+[]][++[+[]][+[]]+[+[]]]+[[+[]==[]]+[][+[]]][+[]][++[+[]][+[]]+[+[]]]+[[][+[]]+[]][+[]][++[++[+[]][+[]]][+[]]]][[]]][[[+[]==[]]+[][+[]]][+[]][++[+[]][+[]]+[+[]]]+[[[]==[]]+[]][+[]][++[+[]][+[]]]+[[+[]][+[]][[][[[[]==[]]+[]][+[]][+[]]+[[+[]==[]]+[+[]]+[][+[]]+[]][+[]][++[+[]][+[]]+[+[]]]+[[+[]==[]]+[][+[]]][+[]][++[+[]][+[]]+[+[]]]+[[][+[]]+[]][+[]][++[++[+[]][+[]]][+[]]]][[]]]+[]][+[]][++[+[]][+[]]+[++[+[]][+[]]]]+[[+[]==[]]+[]][+[]][++[++[++[+[]][+[]]][+[]]][+[]]]]]+[[][[[[]==[]]+[]][+[]][+[]]+[[+[]==[]]+[+[]]+[][+[]]+[]][+[]][++[+[]][+[]]+[+[]]]+[[+[]==[]]+[][+[]]][+[]][++[+[]][+[]]+[+[]]]+[[][+[]]+[]][+[]][++[++[+[]][+[]]][+[]]]][[]==[]]=[+[++[++[+[]][+[]]][+[]]+[++[++[++[++[++[+[]][+[]]][+[]]][+[]]][+[]]][+[]]]]][+[]][[][[[[]==[]]+[]][+[]][+[]]+[[+[]==[]]+[+[]]+[][+[]]+[]][+[]][++[+[]][+[]]+[+[]]]+[[+[]==[]]+[][+[]]][+[]][++[+[]][+[]]+[+[]]]+[[][+[]]+[]][+[]][++[++[+[]][+[]]][+[]]]][++[+[]][+[]]]](++[++[++[+[]][+[]]][+[]]][+[]]+[+[]])]+[[][[[[]==[]]+[]][+[]][+[]]+[[+[]==[]]+[+[]]+[][+[]]+[]][+[]][++[+[]][+[]]+[+[]]]+[[+[]==[]]+[][+[]]][+[]][++[+[]][+[]]+[+[]]]+[[][+[]]+[]][+[]][++[++[+[]][+[]]][+[]]]][+[]==[]]=[][[[[]==[]]+[]][+[]][+[]]+[[+[]==[]]+[+[]]+[][+[]]+[]][+[]][++[+[]][+[]]+[+[]]]+[[+[]==[]]+[][+[]]][+[]][++[+[]][+[]]+[+[]]]+[[][+[]]+[]][+[]][++[++[+[]][+[]]][+[]]]][[]==[]]+[[+[]==[]]+[]][+[]][++[+[]][+[]]]+[[+[]==[]]+[][[[[]==[]]+[]][+[]][+[]]+[[+[]==[]]+[+[]]+[][+[]]+[]][+[]][++[+[]][+[]]+[+[]]]+[[+[]==[]]+[][+[]]][+[]][++[+[]][+[]]+[+[]]]+[[][+[]]+[]][+[]][++[++[+[]][+[]]][+[]]]]][+[]][++[+[]][+[]]+[+[]]]+[[+[]==[]]+[]][+[]][+[]]+[[+[]==[]]+[][[[[]==[]]+[]][+[]][+[]]+[[+[]==[]]+[+[]]+[][+[]]+[]][+[]][++[+[]][+[]]+[+[]]]+[[+[]==[]]+[][+[]]][+[]][++[+[]][+[]]+[+[]]]+[[][+[]]+[]][+[]][++[++[+[]][+[]]][+[]]]]][+[]][++[+[]][+[]]+[+[]]]+[[+[]==[]]+[]][+[]][+[]]+[[][[[[]==[]]+[]][+[]][+[]]+[[+[]==[]]+[+[]]+[][+[]]+[]][+[]][++[+[]][+[]]+[+[]]]+[[+[]==[]]+[][+[]]][+[]][++[+[]][+[]]+[+[]]]+[[][+[]]+[]][+[]][++[++[+[]][+[]]][+[]]]][+[]]()+[]][+[]][++[+[]][+[]]+[++[++[++[+[]][+[]]][+[]]][+[]]]]+[][[[[]==[]]+[]][+[]][+[]]+[[+[]==[]]+[+[]]+[][+[]]+[]][+[]][++[+[]][+[]]+[+[]]]+[[+[]==[]]+[][+[]]][+[]][++[+[]][+[]]+[+[]]]+[[][+[]]+[]][+[]][++[++[+[]][+[]]][+[]]]][[]==[]]+[[+[]==[]]+[]][+[]][++[++[++[+[]][+[]]][+[]]][+[]]]]+[[][[][[[[]==[]]+[]][+[]][+[]]+[[+[]==[]]+[+[]]+[][+[]]+[]][+[]][++[+[]][+[]]+[+[]]]+[[+[]==[]]+[][+[]]][+[]][++[+[]][+[]]+[+[]]]+[[][+[]]+[]][+[]][++[++[+[]][+[]]][+[]]]][[]]][[][[[[]==[]]+[]][+[]][+[]]+[[+[]==[]]+[+[]]+[][+[]]+[]][+[]][++[+[]][+[]]+[+[]]]+[[+[]==[]]+[][+[]]][+[]][++[+[]][+[]]+[+[]]]+[[][+[]]+[]][+[]][++[++[+[]][+[]]][+[]]]][+[]==[]]][[]]=[][[][[[[]==[]]+[]][+[]][+[]]+[[+[]==[]]+[+[]]+[][+[]]+[]][+[]][++[+[]][+[]]+[+[]]]+[[+[]==[]]+[][+[]]][+[]][++[+[]][+[]]+[+[]]]+[[][+[]]+[]][+[]][++[++[+[]][+[]]][+[]]]][[]]][[][[[[]==[]]+[]][+[]][+[]]+[[+[]==[]]+[+[]]+[][+[]]+[]][+[]][++[+[]][+[]]+[+[]]]+[[+[]==[]]+[][+[]]][+[]][++[+[]][+[]]+[+[]]]+[[][+[]]+[]][+[]][++[++[+[]][+[]]][+[]]]][+[]==[]]]]+[[][[]][++[+[]][+[]]]=[][[[[]==[]]+[]][+[]][+[]]+[[+[]==[]]+[+[]]+[][+[]]+[]][+[]][++[+[]][+[]]+[+[]]]+[[+[]==[]]+[][+[]]][+[]][++[+[]][+[]]+[+[]]]+[[][+[]]+[]][+[]][++[++[+[]][+[]]][+[]]]][[]]]+[[][[]][++[++[+[]][+[]]][+[]]]=[][[[[]==[]]+[]][+[]][+[]]+[[+[]==[]]+[+[]]+[][+[]]+[]][+[]][++[+[]][+[]]+[+[]]]+[[+[]==[]]+[][+[]]][+[]][++[+[]][+[]]+[+[]]]+[[][+[]]+[]][+[]][++[++[+[]][+[]]][+[]]]][+[]]]+[[][[]][++[+[]][+[]]+[+[]]]=[][[[[]==[]]+[]][+[]][+[]]+[[+[]==[]]+[+[]]+[][+[]]+[]][+[]][++[+[]][+[]]+[+[]]]+[[+[]==[]]+[][+[]]][+[]][++[+[]][+[]]+[+[]]]+[[][+[]]+[]][+[]][++[++[+[]][+[]]][+[]]]][++[+[]][+[]]]]+[[][[]][++[++[+[]][+[]]][+[]]+[+[]]]=[[]+[]][+[]][[][[[[]==[]]+[]][+[]][+[]]+[[+[]==[]]+[+[]]+[][+[]]+[]][+[]][++[+[]][+[]]+[+[]]]+[[+[]==[]]+[][+[]]][+[]][++[+[]][+[]]+[+[]]]+[[][+[]]+[]][+[]][++[++[+[]][+[]]][+[]]]][[]]]]+[[][[]][[][++[++[+[]][+[]]][+[]]]]=[][++[++[+[]][+[]]][+[]]]([[+[]==[]]+[]][+[]][++[+[]][+[]]]+[[+[]==[]]+[]][+[]][++[++[++[+[]][+[]]][+[]]][+[]]]+[[+[]==[]]+[]][+[]][+[]]+[[+[]==[]]+[]][+[]][++[++[+[]][+[]]][+[]]]+[[+[]==[]]+[]][+[]][++[+[]][+[]]]+[[+[]==[]]+[][+[]]][+[]][++[+[]][+[]]+[+[]]]+[[][[[[]==[]]+[]][+[]][+[]]+[[+[]==[]]+[+[]]+[][+[]]+[]][+[]][++[+[]][+[]]+[+[]]]+[[+[]==[]]+[][+[]]][+[]][++[+[]][+[]]+[+[]]]+[[][+[]]+[]][+[]][++[++[+[]][+[]]][+[]]]]+[]][+[]][++[++[++[+[]][+[]]][+[]]][+[]]+[++[+[]][+[]]]]+[[+[]==[]]+[]][+[]][++[++[++[+[]][+[]]][+[]]][+[]]]+[[][[[[]==[]]+[]][+[]][+[]]+[[+[]==[]]+[+[]]+[][+[]]+[]][+[]][++[+[]][+[]]+[+[]]]+[[+[]==[]]+[][+[]]][+[]][++[+[]][+[]]+[+[]]]+[[][+[]]+[]][+[]][++[++[+[]][+[]]][+[]]]]+[]][+[]][++[++[+[]][+[]]][+[]]+[++[++[++[+[]][+[]]][+[]]][+[]]]]+[[[]==[]]+[]][+[]][++[+[]][+[]]]+[[[]==[]]+[]][+[]][++[++[+[]][+[]]][+[]]])()]+[[][[]][[][[][++[++[+[]][+[]]][+[]]]]]=[][++[++[+[]][+[]]][+[]]]([[+[]==[]]+[]][+[]][++[+[]][+[]]]+[[+[]==[]]+[]][+[]][++[++[++[+[]][+[]]][+[]]][+[]]]+[[+[]==[]]+[]][+[]][+[]]+[[+[]==[]]+[]][+[]][++[++[+[]][+[]]][+[]]]+[[+[]==[]]+[]][+[]][++[+[]][+[]]]+[[+[]==[]]+[][+[]]][+[]][++[+[]][+[]]+[+[]]]+[[][[[[]==[]]+[]][+[]][+[]]+[[+[]==[]]+[+[]]+[][+[]]+[]][+[]][++[+[]][+[]]+[+[]]]+[[+[]==[]]+[][+[]]][+[]][++[+[]][+[]]+[+[]]]+[[][+[]]+[]][+[]][++[++[+[]][+[]]][+[]]]]+[]][+[]][++[++[++[+[]][+[]]][+[]]][+[]]+[++[+[]][+[]]]]+[[[]==[]]+[]][+[]][++[+[]][+[]]]+[[[]==[]]+[]][+[]][++[++[++[+[]][+[]]][+[]]][+[]]]+[[][[[[]==[]]+[]][+[]][+[]]+[[+[]==[]]+[+[]]+[][+[]]+[]][+[]][++[+[]][+[]]+[+[]]]+[[+[]==[]]+[][+[]]][+[]][++[+[]][+[]]+[+[]]]+[[][+[]]+[]][+[]][++[++[+[]][+[]]][+[]]]][+[]]()+[]][+[]][++[+[]][+[]]+[++[++[++[+[]][+[]]][+[]]][+[]]]]+[[+[]==[]]+[][+[]]][+[]][++[+[]][+[]]+[+[]]]+[[][[[[]==[]]+[]][+[]][+[]]+[[+[]==[]]+[+[]]+[][+[]]+[]][+[]][++[+[]][+[]]+[+[]]]+[[+[]==[]]+[][+[]]][+[]][++[+[]][+[]]+[+[]]]+[[][+[]]+[]][+[]][++[++[+[]][+[]]][+[]]]]+[]][+[]][++[++[++[+[]][+[]]][+[]]][+[]]]+[[][[[[]==[]]+[]][+[]][+[]]+[[+[]==[]]+[+[]]+[][+[]]+[]][+[]][++[+[]][+[]]+[+[]]]+[[+[]==[]]+[][+[]]][+[]][++[+[]][+[]]+[+[]]]+[[][+[]]+[]][+[]][++[++[+[]][+[]]][+[]]]]+[]][+[]][++[++[++[+[]][+[]]][+[]]][+[]]+[++[+[]][+[]]]]+[][++[++[+[]][+[]]][+[]]]())()]+[[][[]][++[+[]][+[]]+[++[+[]][+[]]]]=[][[[+[]==[]]+[]][+[]][++[++[++[+[]][+[]]][+[]]][+[]]]+[[+[]==[]]+[][+[]]][+[]][++[+[]][+[]]+[+[]]]+[[+[]==[]]+[]][+[]][+[]]+[[+[]==[]]+[]][+[]][++[+[]][+[]]]+[[+[]==[]]+[+[]]+[][+[]]+[]][+[]][++[+[]][+[]]+[+[]]]+[[+[]==[]]+[]][+[]][++[++[++[+[]][+[]]][+[]]][+[]]]+[[[]==[]]+[]][+[]][++[++[++[+[]][+[]]][+[]]][+[]]]]()[[][[]][++[+[]][+[]]]]]+[[][[]][[][[][[][++[++[+[]][+[]]][+[]]]]]]=[[[]+[]][+[]][[][[[[]==[]]+[]][+[]][+[]]+[[+[]==[]]+[+[]]+[][+[]]+[]][+[]][++[+[]][+[]]+[+[]]]+[[+[]==[]]+[][+[]]][+[]][++[+[]][+[]]+[+[]]]+[[][+[]]+[]][+[]][++[++[+[]][+[]]][+[]]]][[]]]+[]][+[]][++[+[]][+[]]+[++[++[++[++[+[]][+[]]][+[]]][+[]]][+[]]]]+[[+[]==[]]+[]][+[]][++[++[++[+[]][+[]]][+[]]][+[]]]+[[+[]==[]]+[]][+[]][+[]]+[+[]+[][++[+[]][+[]]+[++[+[]][+[]]]]][+[]][++[+[]][+[]]+[+[]]]+[+[++[++[++[+[]][+[]]][+[]]][+[]]+[++[++[+[]][+[]]][+[]]]]][+[]][[][++[+[]][+[]]+[+[]]]](++[++[++[+[]][+[]]][+[]]][+[]]+[++[++[++[+[]][+[]]][+[]]][+[]]])+[[+[]==[]]+[][+[]]][+[]][++[+[]][+[]]+[+[]]]+[+[]+[][[][[][++[++[+[]][+[]]][+[]]]]]()[[][[]][++[+[]][+[]]]]][+[]][++[+[]][+[]]+[+[]]]+[[+[]==[]]+[]][+[]][++[+[]][+[]]]+[[+[]==[]]+[][[[[]==[]]+[]][+[]][+[]]+[[+[]==[]]+[+[]]+[][+[]]+[]][+[]][++[+[]][+[]]+[+[]]]+[[+[]==[]]+[][+[]]][+[]][++[+[]][+[]]+[+[]]]+[[][+[]]+[]][+[]][++[++[+[]][+[]]][+[]]]]][+[]][++[+[]][+[]]+[+[]]]+[][[[[]==[]]+[]][+[]][+[]]+[[+[]==[]]+[+[]]+[][+[]]+[]][+[]][++[+[]][+[]]+[+[]]]+[[+[]==[]]+[][+[]]][+[]][++[+[]][+[]]+[+[]]]+[[][+[]]+[]][+[]][++[++[+[]][+[]]][+[]]]][[]==[]]+[[+[]==[]]+[]][+[]][++[++[++[+[]][+[]]][+[]]][+[]]]+[[+[]==[]]+[]][+[]][++[+[]][+[]]]+[[+[]==[]]+[]][+[]][+[]]+[[][[[[]==[]]+[]][+[]][+[]]+[[+[]==[]]+[+[]]+[][+[]]+[]][+[]][++[+[]][+[]]+[+[]]]+[[+[]==[]]+[][+[]]][+[]][++[+[]][+[]]+[+[]]]+[[][+[]]+[]][+[]][++[++[+[]][+[]]][+[]]]][+[]]()+[]][+[]][++[+[]][+[]]+[++[++[++[+[]][+[]]][+[]]][+[]]]]+[+[]+[+[]][+[]][[][[[[]==[]]+[]][+[]][+[]]+[[+[]==[]]+[+[]]+[][+[]]+[]][+[]][++[+[]][+[]]+[+[]]]+[[+[]==[]]+[][+[]]][+[]][++[+[]][+[]]+[+[]]]+[[][+[]]+[]][+[]][++[++[+[]][+[]]][+[]]]][[]]]][+[]][++[+[]][+[]]+[+[]]]+[[[]==[]]+[]][+[]][++[+[]][+[]]]+[[+[]][+[]][[][[[[]==[]]+[]][+[]][+[]]+[[+[]==[]]+[+[]]+[][+[]]+[]][+[]][++[+[]][+[]]+[+[]]]+[[+[]==[]]+[][+[]]][+[]][++[+[]][+[]]+[+[]]]+[[][+[]]+[]][+[]][++[++[+[]][+[]]][+[]]]][[]]]+[]][+[]][++[+[]][+[]]+[++[+[]][+[]]]]+[[+[]==[]]+[]][+[]][++[++[++[+[]][+[]]][+[]]][+[]]]+[[[]==[]]+[]][+[]][++[++[++[+[]][+[]]][+[]]][+[]]]]+[[][[]][[][[][[][[][++[++[+[]][+[]]][+[]]]]]]]=[][++[++[+[]][+[]]][+[]]+[+[]]][[][++[+[]][+[]]+[++[+[]][+[]]]][[][[][[][[][++[++[+[]][+[]]][+[]]]]]]]([][++[++[+[]][+[]]][+[]]+[+[]]])[[[[]==[]]+[]][+[]][++[++[++[+[]][+[]]][+[]]][+[]]]+[[+[]==[]]+[][[[[]==[]]+[]][+[]][+[]]+[[+[]==[]]+[+[]]+[][+[]]+[]][+[]][++[+[]][+[]]+[+[]]]+[[+[]==[]]+[][+[]]][+[]][++[+[]][+[]]+[+[]]]+[[][+[]]+[]][+[]][++[++[+[]][+[]]][+[]]]]][+[]][++[+[]][+[]]+[+[]]]+[[+[]==[]]+[]][+[]][++[+[]][+[]]]+[[+[]==[]]+[]][+[]][+[]]]()[+[]]]]+[][[][++[++[+[]][+[]]][+[]]]]([[][[[[]==[]]+[]][+[]][+[]]+[[+[]==[]]+[+[]]+[][+[]]+[]][+[]][++[+[]][+[]]+[+[]]]+[[+[]==[]]+[][+[]]][+[]][++[+[]][+[]]+[+[]]]+[[][+[]]+[]][+[]][++[++[+[]][+[]]][+[]]]]+[]][+[]][++[++[++[+[]][+[]]][+[]]][+[]]]+[[+[]==[]]+[][[[[]==[]]+[]][+[]][+[]]+[[+[]==[]]+[+[]]+[][+[]]+[]][+[]][++[+[]][+[]]+[+[]]]+[[+[]==[]]+[][+[]]][+[]][++[+[]][+[]]+[+[]]]+[[][+[]]+[]][+[]][++[++[+[]][+[]]][+[]]]]][+[]][++[+[]][+[]]+[+[]]]+[[+[]==[]]+[][+[]]][+[]][++[+[]][+[]]+[+[]]]+[[[]==[]]+[]][+[]][++[++[++[+[]][+[]]][+[]]][+[]]]+[[+[]==[]]+[][[[[]==[]]+[]][+[]][+[]]+[[+[]==[]]+[+[]]+[][+[]]+[]][+[]][++[+[]][+[]]+[+[]]]+[[+[]==[]]+[][+[]]][+[]][++[+[]][+[]]+[+[]]]+[[][+[]]+[]][+[]][++[++[+[]][+[]]][+[]]]]][+[]][++[+[]][+[]]+[+[]]]+[[[]==[]]+[]][+[]][++[++[+[]][+[]]][+[]]]+[[+[]==[]]+[]][+[]][++[++[++[+[]][+[]]][+[]]][+[]]]+[][[][[][[][[][++[++[+[]][+[]]][+[]]]]]]](++[++[++[++[+[]][+[]]][+[]]][+[]]][+[]]+[++[++[++[++[++[++[+[]][+[]]][+[]]][+[]]][+[]]][+[]]][+[]]])+[[[]==[]]+[]][+[]][++[++[+[]][+[]]][+[]]]+[[+[]==[]]+[][[[[]==[]]+[]][+[]][+[]]+[[+[]==[]]+[+[]]+[][+[]]+[]][+[]][++[+[]][+[]]+[+[]]]+[[+[]==[]]+[][+[]]][+[]][++[+[]][+[]]+[+[]]]+[[][+[]]+[]][+[]][++[++[+[]][+[]]][+[]]]]][+[]][++[+[]][+[]]+[+[]]]+[[[]+[]][+[]][[][[[[]==[]]+[]][+[]][+[]]+[[+[]==[]]+[+[]]+[][+[]]+[]][+[]][++[+[]][+[]]+[+[]]]+[[+[]==[]]+[][+[]]][+[]][++[+[]][+[]]+[+[]]]+[[][+[]]+[]][+[]][++[++[+[]][+[]]][+[]]]][[]]]+[]][+[]][++[+[]][+[]]+[++[++[++[++[+[]][+[]]][+[]]][+[]]][+[]]]])([][[][[][[][[][++[++[+[]][+[]]][+[]]]]]]](++[++[++[++[++[++[++[+[]][+[]]][+[]]][+[]]][+[]]][+[]]][+[]]][+[]]+[++[++[+[]][+[]]][+[]]])+[[+[]==[]]+[]][+[]][++[++[++[+[]][+[]]][+[]]][+[]]]+[[[]==[]]+[]][+[]][++[++[+[]][+[]]][+[]]]+[[[]==[]]+[]][+[]][++[++[+[]][+[]]][+[]]]+[[+[]==[]]+[][[[[]==[]]+[]][+[]][+[]]+[[+[]==[]]+[+[]]+[][+[]]+[]][+[]][++[+[]][+[]]+[+[]]]+[[+[]==[]]+[][+[]]][+[]][++[+[]][+[]]+[+[]]]+[[][+[]]+[]][+[]][++[++[+[]][+[]]][+[]]]]][+[]][++[+[]][+[]]+[+[]]]+[][[][[][[][[][++[++[+[]][+[]]][+[]]]]]]](++[++[++[++[+[]][+[]]][+[]]][+[]]][+[]]+[++[++[++[++[+[]][+[]]][+[]]][+[]]][+[]]])+[[][[[[]==[]]+[]][+[]][+[]]+[[+[]==[]]+[+[]]+[][+[]]+[]][+[]][++[+[]][+[]]+[+[]]]+[[+[]==[]]+[][+[]]][+[]][++[+[]][+[]]+[+[]]]+[[][+[]]+[]][+[]][++[++[+[]][+[]]][+[]]]]+[]][+[]][++[++[++[+[]][+[]]][+[]]][+[]]+[++[+[]][+[]]]]+[][[][[][[][[][++[++[+[]][+[]]][+[]]]]]]](++[++[++[++[++[++[++[++[+[]][+[]]][+[]]][+[]]][+[]]][+[]]][+[]]][+[]]][+[]]+[++[++[++[++[++[++[++[+[]][+[]]][+[]]][+[]]][+[]]][+[]]][+[]]][+[]]])+[[+[]==[]]+[][[[[]==[]]+[]][+[]][+[]]+[[+[]==[]]+[+[]]+[][+[]]+[]][+[]][++[+[]][+[]]+[+[]]]+[[+[]==[]]+[][+[]]][+[]][++[+[]][+[]]+[+[]]]+[[][+[]]+[]][+[]][++[++[+[]][+[]]][+[]]]]][+[]][++[+[]][+[]]+[+[]]]+[[+[]==[]]+[]][+[]][++[+[]][+[]]]+[[[]==[]]+[]][+[]][++[++[+[]][+[]]][+[]]]+[[][+[]]+[]][+[]][++[++[+[]][+[]]][+[]]]+[][[][[][[][[][++[++[+[]][+[]]][+[]]]]]]](++[++[++[+[]][+[]]][+[]]][+[]]+[++[++[++[+[]][+[]]][+[]]][+[]]]))]
// Hello, World!

このことに関する解説記事も、Qiita上でいくつか執筆されています。

一方で、従来のQiita記事では「記号のみを使って」いればそれでよいとする制約が多く、使用される記号の種類に関する制約を課している例は見つけることができませんでした。
本記事では、記号のみを使ってJavaScriptを書くだけでなく、使用する記号の種類を極力少なくすることも考慮します。
その上で、JavaScriptプログラムを記号のみを用いて表現するための小手先のテクニックについて解説をしていきたいと思います。

それではやってみましょう。

レギュレーション

古来より、JavaScriptで任意のプログラムを書くために必要な最小の記号の数は6種類であるとされてきました。1
今回はそれに倣い、ルールを以下のように決めます。

  • プログラム中に使用してよいのは、[]()+=の6種類の記号のみ。
  • ブラウザとnode.jsの両方で動作しなければならない。
    • よって、明示的なwindowglobalへのアクセスは禁止。globalThis2もまだ存在しないということにしておきます。
    • 実行環境は記事執筆時点でのnode.jsとChromeの最新安定版とします。

記号プログラミングことはじめ

基本的な考え方

JavaScriptでは、aというオブジェクトのプロパティをa["x"]のようにして取得することができます。
つまり、console.log()console["log"]()として呼び出すことができます。
これは、 任意の文字列[]()の記号があればメソッドの実行が可能になることを意味します。

また、JavaScriptでは至る所で暗黙の型変換3が行われ、気がついたら文字列が数値になっていたりオブジェクトが文字列になっていたりします。
たとえば、

+[]      // 0
[]+[]    // ""
true+""  // "true"
false+"" // "false
(function f(){})+"" // "function f(){}"

(false+"")[1] // "a"

この愉快極まりない挙動を利用することで何もないところから文字列を拾ってきて、それを利用してプロパティにアクセスする、というのが記号プログラミングの最初の考え方になります。

はじめに+[]があった

数を得る

+[]0ですが、ここから任意の非負整数値を得ることができます。

++[+[]][+[]] // 1

これは実行すると1になります。
Ctrl + Shift + Iキーあたりを押すとJavaScriptの実行コンソールが開くかもしれないので、実際に実行して試してみましょう。

+[]0になるので、上のコードは以下のように読み替えることができます。

++[0][0]

左の[0]0が入った要素1の配列ですから、右の[0]で0番目の要素にアクセスすると0が得られますね。
前置の++によってこの0をインクリメントすることで1を得ています。4

2以降も同様に作れます。5

++[++[+[]][+[]]][+[]] // 2
++[++[++[+[]][+[]]][+[]]][+[]] // 3

以後簡単のため、++[+[]][+[]]のかわりに1などと書いたりすることがあります。

undefinedとNaNと文字列キャスト

  [][[]]  // undefined (存在しない配列要素へのアクセス)
 +[][[]] // NaN (undefinedを+で数値にキャスト)
  [][[]] +[] // "undefined" (undefinedを文字列に変換)
 +[][[]] +[] // "NaN"
[ [][[]] +[] ] [0][0] // "u"
[ [][[]] +[] ] [0][1] // "n"
[ [][[]] +[] ] [0][2] // "d"
[+[][[]] +[] ] [0][0] // "N"

存在しないプロパティにアクセスすることで、undefinedも得ることができます。
また、x+[]は、xを文字列に変換する力を持っており、undefinedは文字列に変換すると"undefined"となります。
私たちは今、0,1,2,...とundefined, NaN, そしていくつかの文字にアクセスすることができるようになりました。

はじめてのプロパティアクセス 6

ところで"undefined"の文字列から、"find"を取り出すことができます。
こうですね。

const find = "undefined"[4] + "undefined"[5] + "undefined"[6] + "undefined"[8] // "find"

このおかげで、Array.prototype.findにアクセスできます。

[]["find"]     // === Array.prototype.find
[]["find"] +[] // "function find() { [native code] }"

"function find() { [native code] }"からは"function"に含まれる文字をはじめとしていくつかの文字を取り出すことができますね。

ちなみにこれまでのコードを省略せず[]+の記号だけで書くと、以下のようになります。

// === Array.prototype.find
[][[[][[]]+[]][+[]][++[++[++[++[+[]][+[]]][+[]]][+[]]][+[]]]+[[][[]]+[]][+[]][++[++[++[++[++[+[]][+[]]][+[]]][+[]]][+[]]][+[]]]+[[][[]]+[]][+[]][++[++[++[++[++[++[+[]][+[]]][+[]]][+[]]][+[]]][+[]]][+[]]]+[[][[]]+[]][+[]][++[++[++[++[++[++[++[++[+[]][+[]]][+[]]][+[]]][+[]]][+[]]][+[]]][+[]]][+[]]]]

この程度なら、まだ人間でも手書きできますね。

結局のところ、[]+の三種類の記号によって、

0123456789
acdefinotuvy
IN
{}[]() +-.,

これだけの文字が得られるということが知られています。 (http://slides.com/sylvainpv/xchars-js/#/16)
今回紹介していない要素として、三種類の記号からInfinity"-"、負の整数も得ることができます。
https://github.com/aemkei/jsfuck が(この記事が必要なくなるぐらいに)非常によくまとまっているので、興味があれば調べてみてください。

第4の記号、=の登場

そろそろ記号プログラミングの発想にも慣れてきたことと思います。
どんどん先に進んでいきましょう。そうしないと記事を書く私がもちません。

truefalseを得る

[]+の3種類の記号のみを用いて、真偽値型の値であるtruefalseを手に入れる方法は未だ発見されていません。
そのため、これらの値を手に入れるために新たな記号である=を導入します。

ここで新たに記号を導入する最大の目的はboolキャストの手段を得るためですので、!<>といった記号でも問題ありません。
!は使い勝手が良いので人気のある記号ですが、今回は[]+の3種類の記号で既に大半のプリミティブを得ているため、=を採用します。
=はなんといっても代入が解禁されるので魅力的な記号ですね。まあ記号だけでは変数名を書けないんですが。7

 []==[] // false
+[]==[] // true
// まわりくどい取り出し方をしているのは、
// []==[] (false) のあとに +[]を直接つなげると []==[]+[] (false) となって "false"が得られないため
[[[]==[]][0]+[]][0][3] // "s"

"constructor"

これで"true""false"に含まれる文字を手に入れることができました。
今までに得た文字を合わせると、"constructor"という魔法の文字列が得られます。

[]["constructor"]    // === Array
[]["constructor"]+[] // "function Array() { [native code] }"
""["constructor"]+[] // "function String() { [native code] }"
[]["find"]["constructor"] // Function ([]["constructor"]["constructor"]でもよい)

これでより多くの文字が得られ、さらにさまざまなコンストラクタにアクセスする方法も得ることができました。

関数呼び出しの壁と、あらゆるものを手に入れる力

()の導入

関数を実行することができなければ、私たちにはわずかな力しかありません。
これまでJavaScriptを6種類よりも少ない記号の数で書こうとする試みはすべて、()の記号なしには任意の関数を実行することができないというただ一点によって阻まれてきたのです。8
これまで6種類という限界を破ろうとした者の挑戦を悉く跳ね除け、そこに無数の屍の山を築き上げてきた()の2文字を導入する時が来ました。
これで使用する記号の種類は[]()+=の6種類となります。

Number.prototype.toString

()の力を得て、さらに多くの文字を手に入れましょう。

const array = [];
for(let i=10;i<36;i++){
    array.push(i.toString(36));
}
// array => ["a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z"]

数値型の値の.toString()メソッドに引数nを渡すと、その数値をn進数に変換した文字列が得られます。
たとえばx.toString(16)xの16進数表記を得ることができます。
これによって、"a"から"z"までのすべての英小文字が表現可能です。

[20][0]["toString"](36) // "k"

関数の作成と実行

[]["find"]["constructor"]Functionへの参照を指しているので、これを実行することで関数を作成することができます。

[]["find"]["constructor"]("return 10") // function anonymous() {return 10}
[]["find"]["constructor"]("return eval")() // === eval

そしてevalが手に入りました。
これで、文字列として表現したプログラムはすべて実行することができます。

遠すぎた"C"

とはいえ、まだすべての英大文字が手に入っているわけではなく、また様々な記号なども表現することができません。
我々に救いをもたらすと期待されている存在とは、即ちString.fromCharCodeです。
String.fromCharCodeは文字コードを数値で与えればそれに対応する文字を返すため、あらゆる文字が表現可能になります。9
しかしながら、現時点ではまだ"C"は手に入っておらず、これを呼び出すことができません。10

"P"の入手

const afunc = Function("return async "+Function())()

これを実行することでasync functionが得られます。
さらにasync functionの中身はPromiseですから、

afunc()+[] // "[object Promise]"

これで"P"が得られました。

Object.getOwnPropertyNamesへのアクセス

[]["entries"]()["constructor"] // === Object
Object["getOwnPropertyNames"](String)["sort"]()[0] // "fromCharCode"

"P"を得ることで何をするかというと、これで"getOwnPropertyNames"を構築するための文字がすべて揃います。
Objectには上に示したように、Array.prototype.entries()を実行して得られたイテレータの["constructor"]Objectと一致することを利用すればアクセスできます。
これによってStringが持っているメソッド名をすべて取得でき、当然"fromCharCode""C"の文字を得るより先に手に入ってしまいます。11

Theory of Everything

String["fromCharCode"](42) // "*"
String["fromCodePoint"](127843) // "?"

そういえばevalを既に手に入れていましたね。
今や任意の文字列を構築可能なため、あらゆるJavaScriptコードが実行可能になりました。

たとえば、記事冒頭のプログラムは、console.log("Hello, World!")と同じ処理を行っています。
これを記号化して実行するには、

eval('console.log("Hello, World!")')

少なくともこのようなプログラムが書ければ大丈夫ですね。
まあ実際には、consoleさえ得られれば、メソッド呼び出し自体にはevalを使用する必要がないので、

console.log("Hello, World!")
console["log"]("Hello, World!")
Function("return console")()["log"]("Hello, World!")
[]["find"]["constructor"]("return console")()["log"]("Hello, World!")

のように変形していけばよいです。evalも不要ですね。
"!"などの文字はString.fromCharCode()を用いて生成することができるので、"Hello, World!"の文字列は

String.fromCharCode(72) + // "H"
"e" + "l" + "l" + "o" +
String.fromCharCode(44) + // ","
" " +
String.fromCharCode(87) + // "W"
"o" + "r" + "l" + "d" +
String.fromCharCode(33) // "!"

のようにすれば構築できます。

同様にして[]["find"]["constructor"]("return console")()["log"]("Hello, World!")の文字列部分をすべて記号で表現してしまえば、プログラムの記号化を行うことができ、最終的に記事冒頭のようなコードになります。12

まとめ・次回予告

この記事では、JavaScriptコードを最小限の記号だけで表現するための考え方について解説しました。
使用する記号の種類を少なくするためには、JavaScriptの暗黙の型変換やprototypeなどについての仕様をうまく利用する必要があるので、記号プログラム化の手法について知ることはJavaScriptの言語仕様の理解をより深めることに繋がる……かもしれません。

今回は記号プログラムの基本的なアイデアや考え方に重点を置いているため、この記事で解説した事項は概ね既知のものです。13
https://github.com/aemkei/jsfuck 等で古くから行われている研究に対して、ここで改めて敬意を表します。

また、基本的な考え方は示しましたが、これだけで記号化したJavaScriptプログラムを書けるかと言われれば、まあ人間にとって無理があると考えざるを得ないでしょう。
やはりここは、JavaScriptコードを記号に変換するプログラムなどを作りたいところです。

この記事のタイトルには(1)というナンバリングがついていますので、もしかしたら次回があるかもしれません。14
現時点では何の見通しも進捗もありませんが、プログラムの記号化を行うプログラムの作成や、文字数節約のためのテクニックの紹介などは今回書けなかったので書きたいところです。
最終的なゴールとして、記号プログラムコンパイラを記号プログラムで書くとか、記号プログラムでクワインを書くといったあたりに到達できればと思っています。


  1. Xchars.js, aemkei/jsfuck また、提案段階の|>演算子が導入されれば5文字まで減らせることが明らかになっています (Xchars.js Subset: 5 characters

  2. https://github.com/tc39/proposal-global, ?globalThis?と?global?と?this? 

  3. JavaScriptのプリミティブへの変換を完全に理解する 

  4. 単に++0と書いても0が右辺値となるのでインクリメントはできませんが、[0]という配列の一要素に対してはインクリメントを行うことができます。a=[0];++a[0];を実行する場合を考えましょう。 

  5. ある値xに対して、[x][0]のように配列で包んで即座に中身を取り出すことで、()の代わりに優先順位を明示することもできます。 たとえば0+0をしようとして+[]++[]と書いてしまうと++の部分で構文エラーになりますが、+[]+[+[]][+[]]と書くことで回避できます。 

  6. はじめての「意味のある文字列を添え字とした」プロパティアクセス 

  7. 主に表現の使いまわしによる文字数の節約に使えます。将来的に役に立つときが来る……予定です。 

  8. https://github.com/aemkei/jsfuck#execute-functions toString,valueOf``などは、任意の引数を関数に与えることができないという点でうまくいかないとされています。 

  9. 実際にはemojiなどの取得のためにString.fromCodePointも必要ですが 

  10. 実はhttps://github.com/aemkei/jsfuck/blob/87d4d4e9112e9c9a3dd7db34afcbbdc063b18a5b/jsfuck.js では、この時点で"C"を入手できています。それはFunction("return escape")()(("")["italics"]())[2]を実行し、"<i></i>"escapeにかけるというものです。escape関数は非推奨とされているので、ここではほかの方法をとることにしています。 

  11. ここでは「Stringの持つメソッド名をsortすると先頭に"fromCharCode"が来る」という事実に依存しているため、将来的にメソッドが増えるなどして正しく取得できなくなる可能性があります。とはいえ、より良い取得方法に書き換えることは難しくありません。 

  12. 冒頭のコードは横着して生成したので、実際にこの通りに記号化してもコードが一致しないことがあります。 

  13. ただし、fromCharCodeを得るためのasync functiongetOwnPropertyNamesの利用については、本記事の独自研究であると考えています。 

  14. というかもっともっと長い記事を書くつもりだったんですが、今回分だけで十分長くなってしまったので一度投稿した形となります 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

UnityのWebGLで外部JavaScriptライブラリを使う

概要

UnityのWebGLプラットフォームで、FirebaseやStripeといった外部サービスのJavaScriptライブラリを使いたかったので、そのときに必要だった手順をまとめました。

WebGLTemplatesの用意

公式ページにあるように
Assets/WebGLTemplates/テンプレート名/
に以下のファイルを作成します。
使いたいライブラリも記述します。

index.html
<!DOCTYPE html>
<html lang="en-us">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <title>Unity WebGL Player | %UNITY_WEB_NAME%</title>
    <!--使いたいライブラリを追加-->
    <script src="%UNITY_WEBGL_LOADER_URL%"></script>
    <script>
    var gameInstance = UnityLoader.instantiate("gameContainer", "%UNITY_WEBGL_BUILD_URL%");
    </script>
  </head>

  <body>
    <div id="gameContainer" style="width: %UNITY_WIDTH%px; height: %UNITY_HEIGHT%px; margin: auto"></div>
  </body>  
</html>

C#←→JavaScriptの連携について

C#とブラウザのJavaScriptと連携する方法も公式ページにあります。
軽く手順説明します。

C#→JavaScript

Assets/Plugins/
以下にこのようなファイルを作成します。

test.jslib
mergeInto(LibraryManager.library, {
  Hello: function () {
    //外部ライブラリを使った処理
  },
});

そして以下のようなコードを書き、WebGLでビルドすると
C#からtest.jslibのHello関数を通して、外部ライブラリの処理呼ぶことができます。

NewBehaviourScript.cs
using UnityEngine;
using System.Runtime.InteropServices;

public class NewBehaviourScript : MonoBehaviour
{
    [DllImport("__Internal")]
    private static extern void Hello();

    void Start()
    {
        Hello();
    }
}

JavaScript→C#

話が脱線しますがこちらも
WebGLTemplatesのindex.htmlでこのように書きます。

index.html
<!DOCTYPE html>
<html lang="en-us">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <title>Unity WebGL Player | %UNITY_WEB_NAME%</title>
    <script src="%UNITY_WEBGL_LOADER_URL%"></script>
    <script>
    var gameInstance = UnityLoader.instantiate("gameContainer", "%UNITY_WEBGL_BUILD_URL%");
    gameInstance.SendMessage('MyGameObject', 'MyFunction', 5);
    </script>
  </head>

  <body>
    <div id="gameContainer" style="width: %UNITY_WIDTH%px; height: %UNITY_HEIGHT%px; margin: auto"></div>
  </body>  
</html>

公式ページに明記されてないと思いますが、以下のように書く必要がありました。

    gameInstance.SendMessage('MyGameObject', 'MyFunction', 5)

ちょっと工夫

jslibにコードを書いて動作確認をするには、毎回プロジェクトをビルドする必要があります。(よね?)
結構時間がかかって大変だったので、私はWebGLTemplatesにJavaScriptファイルを作成して
ビルド後に一緒に吐き出されるそのファイルを編集して確認していました。

また、index.htmlにボタンを追加し、jslibからは実行せずに、そのボタンによってJavaScriptの処理が実行されるようにしました

動作的には
  1. シーンに応じてC#→jslibでボタンを有効化無効化 (タイトル画面でログインボタンを有効にするとか)
  2. ボタンでJavaScriptを実行
  3. JavaScriptの処理結果をC#に渡す

という感じです。

記事を書いたときの環境

Unity 2018 3.8f1

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Nuxt.jsでどうしても直接DOMを操作したい

背景

大前提として、DOMを直接操作する前にまず打てる手を色々と考えていただきたいです:pray:

さて、Nuxt.jsを使っていて、どうしてもDOMを直接操作したい場合があります。
たとえば、ページ遷移直後に、遷移先の内部リンク( /hoge#id )に自動でスクロールしたい場合などが考えられます。
このような場合、ライフサイクルイベントと直接DOM操作を駆使して動かす以外に方法がないのでは?と思い、実際にやってみました。

tl;dr

mountedとnextTickを使います。

mountedはスクロールするためにDOMの操作が必要になるので使用します。
ただし、mountedは子コンポーネントのマウントを保証しないので、nextTickを使い、レンダリングが終わるまで(操作対象のDOMがあることを保証して)コールバック関数を実行しないようにすることが重要です。

もし内部リンクとなるidを指定したコンポーネント内からさらにコンポーネントを呼び出している場合は、子コンポーネントの中でmountedとnextTickを使ってください。

動かしてみる

記法はvue-class-componentに従っているのでよしなに脳内変換をお願いします :bow:

test.vue
<template>
 <div id="id">
  <p>Please scroll</p>
 </div>
</template>

<script lang="ts">
import { Component } from "vue-property-decorator";

@Component
export default class Child extends Vue {
  private mounted() {
    this.$nextTick(function() {
      const element = document.getElementById('id')
      if (!element) {
        return
      }
      // elementの位置座標を取得
      const rect = element.getBoundingClientRect()
      window.scrollTo(rect.left, rect.top)
    })
  }
}
</script>

<style>
</style>

内部リンクとなるidがない場合はスクロールはせず終了します。

さらに

上記のままだと、決まったidにしか反応できないので、以下のようにすれば内部リンクが増えたときも便利です。

test.vue
<template>
 <div id="id">
   <p>Please scroll</p>
  </div>
</template>

<script lang="ts">
import { Component } from "vue-property-decorator";

@Component
export default class Child extends Vue {
  private mounted() {
    this.$nextTick(function() {
      // url内のhash(#id)を取得する
      const hash = this.$route.hash
      if (hash.length === 0) {
        return
      }
      // hash.slice(1)でhashから「#」を取り除いておく
      const element = document.getElementById(hash.slice(1))
      if (!element) {
        return
      }
      const rect = element.getBoundingClientRect()
      window.scrollTo(rect.left, rect.top)
    })
  }
}
</script>

<style>
</style>

vue-routerの$route.hashを使えばurl内にあるhashを取得してくれるので、hashの動的な変更に合わせて内部リンクへのスクロールが可能です。

最後に

しつこいようですが、直接DOMを操作する前になにかできることはないか考えてみるのがベストだと思います。少しでもお役に立てれば幸いです :relaxed:

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

SPA で右クリックや shift + クリックなどによる別窓表示に対応させる

経緯

import React, { useCallback, useMemo } from 'react'
import useReactRouter from 'use-react-router'
import { myAction } from './myAction'

export const Link = ({ href, children }) => {
  const { history } = useReactRouter()

  const onClick = useCallback(e => {
    myAction(href)
    history.push(href)
    e.preventDefault()
    return false
  }, [href])

  const _href = useMemo(() => `#${href}`, [href])

  return (
    <a href={_href} onClick={onClick}>
      {children}
    </a>
  )
}

上記のようなソースコードを書くと Mac の Meta + click や windows の ctrl + click を押した際に
別のタブで表示されるのではなく現在のタブで history.push(href) が実行される。

各種ライブラリがどのように対応しているか

ライブラリによってチェックしている項目が多少異なるようです。
良い感じに組み合わせてキミの最強の onClick を作ろう!!!!!

gatsbyjs の対応

gatsby/packages/gatsby-link/src/index.js#L137

チェック項目 (一部

  • e.button !== 0
  • e.defaultPrevented
  • e.metaKey
  • e.altKey
  • e.ctrlKey
  • e.shiftKey

next.js の対応

next.js/packages/next/client/link.js#L60

チェック項目 (一部

  • nodeName === 'A'
  • e.nativeEvent.which === 2
  • !isLocal(href)

react router の対応

react-router/packages/react-router-dom/modules/Link.js#L15

チェック項目 (一部

  • (!this.props.target || this.props.target === "_self")
  • !isModifiedEvent(event)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Reactにおけるトランスパイラー【Babel】の役割

概要

前回の記事【ReactのJSXについて理解を深めよう!】においてちらっとトランスパイラーについて触れましたが、今回はさらにトランスパイラーについて深掘りしていきたいと思います!

続きはこちら
【webpack】をサクッとまとめる

トランスパイラーとは

スクリーンショット 2019-04-15 20.59.26.png

人間の職業で言うところの通訳者・翻訳者のような役割を果たしてくれます!

トランスコンパイラ(他にトランスパイラ、ソース・トゥ・ソースコンパイラ、などとも)は、あるプログラミング言語で書かれたプログラムのソースコードを入力として受け取り、別のプログラミング言語の同等のコードを目的コードとして生成する、ある種のコンパイラである。

wikipediaより

JSXはそのままでは実行できず、トランスパイラーを使って実行可能なJavaScriptに変換する必要があります。その変換作業のことをトランスパイリング、変換することをトランスパイルと言います!

JSXで書いたコードは私たち人間には可読性の高いシンタックスとなっておりますが、残念ながらJavaScriptのシンタックスとは認識されません。

したがって、ブラウザやノードではそのコードは実行されません...
実は、前回の記事までに紹介したJSXのコードは自動的にJavaScriptに変換されていたのです!

ではどのようにして自動変換されていたのでしょうか...

Babel

ReactではBabelというトランスパイラーがJSXを暗黙的にJavaScriptに変換してくれています。
では実際にJSXを書いてみて、BabelがどのようにJavaScriptに変換するのかを見てみましょう!

まずはBabel REPLにアクセスしましょう
スクリーンショット 2019-04-15 21.34.24.png

左側のスペースにJSXのコードを打つと、画像のように右側のスペースにリアルタイムでJavaScriptに変換されます。
スクリーンショット 2019-04-15 21.23.52.png

このように、今まで書いてきたJSXはBabelを通してJavaScriptにトランスパイルされブラウザ上で実行されていたということです。

Reactを用いて実際にアプリケーションを作る上では、いちいちJSXのコードがどのようにJavaScriptに変換されているのか気にする必要はありませんが、裏ではBabel君が頑張ってくれていたんだな〜って思いながらコードを書くとBabel君も喜んでくれるのではないのでしょうか。

さらに詳しく知りたい方へ

Learn ES2015にアクセスしてみましょう!

リファレンス

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

CSS in JS ライブラリ「emotion」で、Atomic Design風なコンポーネントを作ってみる

はじめに

emotion は、パフォーマンス良くスタイルを構築する事を念頭に設計された CSS-in-JSライブラリです。

emotion の使い方を少し体験すると同時に、コンポーネントごとに適用するにはどう書いていくかを探るために、 Atomic Design のカードの例を参考に、サンプルアプリケーションを作成しました。

この記事は、そのサンプルアプリケーション作成にあたって、現時点で気になったポイントや迷いがあるポイントなどを記載しています。

書き方は色々あると思いますが、 styled の書き方がしっくりきたので、それを使って書いています。

サンプルアプリケーション

コードも含めて以下に置いています。
Next.jsのSandboxをベースに書いています。

https://codesandbox.io/s/vnnl16yqny

Atoms

Atoms は、「もっとも基本的な要素」で、それ以上分解することができないものなので、 HTMLタグ1つで表現できるもの として考えます。

Atomsを利用するコンポーネントは props 経由でデータを Atoms のコンポーネントに渡します。

下記の card-image.js という Atoms のコンポーネントでは、画像のurlのみ props で渡してもらいます。(厳密にはdefault値は必要かもしれません)

atoms/card-image.js
import styled from "@emotion/styled";

const Img = styled.img({
  width: "64px",
  height: "64px",
  borderRadius: "32px",
  label: "a-card-image"
});

const CardImage = props => <Img src={props.imgSrc} />;

export default CardImage;

Atoms には marginを設定しない 方がよいのではないかと考えています。理由として、 marginは周りの要素と組み合わさって初めて発生するものなので、 Atoms 内で持つ事はないと思います。

一方で、周りの要素を組み合わさって発生するmarginについては、付与すべき対象はこの Atoms のコンポーネントになると思います。

そこで、 CSS props の機能を使って、以下のように実現します。

atoms/card-title.js
import styled from "@emotion/styled";

const Div = styled.div(
  {
    /* atoms は margin を持たない */
    fontSize: "14px",
    fontWeight: 600,
    color: "rgba(0, 0, 0, 0.7)",
    label: "a-card-title"
  },
  props => ({
    /* molecules として組み合わさる時に margin が付与される */
    margin: props.margin,
    color: props.color
  })
);

const CardTitle = props => <Div {...props.overrideStyle}>{props.title}</Div>;

export default CardTitle;

上記では、 props.overrideStyle からmarginのデータをもらい、上書きしています。
この機能を使えば、変更可能なプロパティを Atoms 側でコントロールできるので、上記の例だと、 「color は変えて良い」と定義できるかなと思います。

Molecules

Molecules は、 Atoms を組み合わせたもので、 独自の性質・機能を持つもの として考えます。

以下のように Atoms をimportしてきて、組み込みます。

molecules/card.js
import styled from "@emotion/styled";
import CardImage from "../atoms/card-image";
import CardTitle from "../atoms/card-title";
import CardDescription from "../atoms/card-description";

...

const Card = props => (
  <Div {...props.overrideStyle.Card}>
    <CardImage imgSrc={props.imgSrc} />
    <DivCardInfo>
      <CardTitle
        overrideStyle={combineStyle(overrideStyleOfCardTitle, {
          ...props.overrideStyle.CardTitle
        })}
        title={props.title}
      />
      <CardDescription
        overrideStyle={combineStyle(overrideStyleOfCardDescription, {
          ...props.overrideStyle.CardDescription
        })}
        description={props.description}
      />
    </DivCardInfo>
  </Div>
);

export default Card;

今回のサンプルでは、上位のまとまりである Organisms と同様に悩んでいる部分ではあるのですが、 style を親のコンポーネントから来るものと組み合わせて子のコンポーネントに渡す必要があります。

そのマージ処理は今回以下のようにして作りました。

molecules/card.js
...

// 子コンポーネントの書き換え可能なstyleを上書きする
const overrideStyleOfCardTitle = {
  margin: "4px 0 0 0"
};

// 子コンポーネントの書き換え可能なstyleを上書きする
const overrideStyleOfCardDescription = {
  margin: "8px 0 0 0"
};

/*
  親コンポーネントからのスタイル指定とmergeする方法
*/
const combineStyle = (thisStyle, parentStyle) => {
  return Object.assign(thisStyle, parentStyle);
};

const Card = props => (
  ...
      <CardTitle
        overrideStyle={combineStyle(overrideStyleOfCardTitle, {
          ...props.overrideStyle.CardTitle
        })}
        title={props.title}
      />
   ...
);

上記では、 combineStyle() で、マージをしています。
マージの仕方は色々あると思っていて、どれを基本とするか、他にいい案がないかはまだ自分の中では固まっていないです。

Organisms

Organisms は、「原子・分子を組み合わせた比較的複雑な構造」で、 MolculesAtoms を組み合わせて作るまとまりとして考えます。

このサンプルでは、シンプルなカードのリストをこのレイヤーに配置しています。
基本的には Organisms と考える事は同じなのですが、このサンプルで1つハマった点がありました。
cssでよくある :first-of-type といった擬似クラスのスタイルについてどうやって表現するかです。
この擬似クラスの受け口を子のコンポーネントに持たせると、他のプロパティと同じ方法で表現できますが、
それを子コンポーネントが持つのはどうなんだろうというのが、自分の中で疑問になりました。

今回のサンプルでは、苦肉の策で以下のように実現させましたが、何か他にいい方法がないかは考えたいです。

organisms/card-list.js
...

// XXX: first-of-type の表現をpropsで渡すのがわからず、苦肉の策
// 回数に応じて適用するstyleを分岐させる。
const overrideStyleOfCard = (i, style) => {
  const defaultStyle = {
    margin: "8px 0 0 0"
  };
  const firstChildStyle = {
    margin: "0"
  };

  return i == 0
    ? Object.assign(style.Card, firstChildStyle)
    : Object.assign(style.Card, defaultStyle);
};

const CardList = ({ cardInfoList }) => (
  <Div>
    {cardInfoList.map((data, i) => {
      overrideStyleOfCard(i, data.overrideStyle);
      return <Card {...data} key={i} />;
    })}
  </Div>
);

...

Templates

Templates は、「インタフェースの骨格」で、UIコンテンツ構造にフォーカスしたものです。

これまでの Atoms から Organisms の作り方であれば不要なのではないかな?と思い、今回はこれに当たるコンポーネントは作りませんでした。

Pages

Pages は、実際のコンテンツを適用したもの。 今回のコンポーネント設計では一番上位にあたるもの と考えます。
そのため、ページの基本構造のDOMはここに記載していきます。

pages/index.js
import CardImage from "../atoms/card-image";
import CardTitle from "../atoms/card-title";
import CardDescription from "../atoms/card-description";
import Card from "../molecules/card";
import CardList from "../organisms/card-list";
import { H1, H2, H3 } from "../global-style/h";

// カードの情報
const redCardInfo = {
  ...
}

const greenCardInfo = {
  ...
}

const blueCardInfo = {
  ...
}

// カード情報リスト
const sampleCardInfoList = [redCardInfo, greenCardInfo, blueCardInfo];

export default () => (
  <section>
    <H1>Atomic Design Practice</H1>
    <p>
      このサンプルは、
      <a href="https://uxdaystokyo.com/articles/glossary/atomic-design/">
        https://uxdaystokyo.com/articles/glossary/atomic-design/
      </a>
      のカードの例を参考に組み立てています。
    </p>
    <div>
      <H2>Atoms</H2>
      <H3>CardImage</H3>
      <CardImage imgSrc={blueCardInfo.imgSrc} />
      <H3>CardTitle</H3>
      <CardTitle title={blueCardInfo.title} />
      <H3>CardDescription</H3>
      <CardDescription description={blueCardInfo.description} />
    </div>
    <div>
      <H2>Molecules</H2>
      <H3>Card</H3>
      <Card {...blueCardInfo} />
    </div>
    <div>
      <H2>Organisms</H2>
      <H3>Card List</H3>
      <CardList cardInfoList={sampleCardInfoList} />
    </div>
  </section>
);

Next.js そのものをまだ勉強していないのですが、 pages ディレクトリ以下がルーティングの起点となっているようで、 偶然の一致かもしれませんが、相性はよさそうです。

共通スタイル

例えば、hXタグは共通したスタイルを持ちたいなというときは、 common 的な場所に書いておき、 import して使うのが良さそうかなと思っています。

global-style/h.js
import styled from "@emotion/styled";

// hX の共通 style
const HcommonStyle = {
  color: "#eee",
  padding: "8px",
  borderRadius: "4px"
};

// h1 の style
export const H1 = styled.h1(
  { ...HcommonStyle },
  {
    fontSize: "24px",
    background: "#222",
    label: "g-h1"
  }
);

// h2 の style
export const H2 = styled.h2(
  { ...HcommonStyle },
  {
    fontSize: "20px",
    background: "#555",
    label: "g-h2"
  }
);

// h3 の style
export const H3 = styled.h3(
  { ...HcommonStyle },
  {
    fontSize: "14px",
    fontWeight: "500",
    background: "#999",
    label: "g-h3"
  }
);

pages/index.js
...
import { H1, H2, H3 } from "../global-style/h";
...

export default () => (
  <section>
    <H1>Atomic Design Practice</H1>
...

ディレクトリ構造的にどこに置くかは少し悩みどころですが・・・。

おわりに

今回は触れてませんが、 emotion は記法がいくつか選択できるので、初めてCSS-in-JSを使ってみた自分でも、基本的な使い方はすぐに慣れました。

課題として、 Atomic Design 風にコンポーネント分割していく際に、style の上書きをどうするかや、擬似クラスの表現の仕方があるのと、 Next.js で実際にアプリケーションを作っていくと、どういった壁が出てくるかが未知数です。

一方で、これまではcss(scss)をFLOCSSで管理したりしていましたが、結局JS側のコンポーネントと乖離が出たりして、運用が長く続けば続くほど、辛い感じになってきている印象です。

これをCSS in JSで実装し、かつコンポーネントは Atomic Design の考え方をベースにしてルール化しておくと、チーム開発時に混乱が起きる確率が減るのではないかなと思っています。

まだまだ足りない観点やルールがありそうなので、これをベースにさらに作ってみて、新たな気づきがあれば、アップデートもしくは新規で書き起こそうと思います。

おまけ

実は上記サンプルは2つ目で、ボツにした1つ目は以下。
https://codesandbox.io/s/136z47n693

overrideするstyleは全て上部でDOMを使って吸収するというやり方が大きく違います。どちらが良いかは好みかもしれませんが、対応しきれないケースがでるかなと思い、ボツにしました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

WindowsでFirebase CloudFunctionsを使ってみる

FNCT創造工学演習、予備実験向けにハンズオンを作成しました。

Firebaseを準備する

GmailなどのGoogleアカウントを所有していることを前提として、記事を読みすすめてもらいたい。

Googleアカウントでログインした状態で、Firebase Consoleにアクセスしてみよう。
https://console.firebase.google.com/u/0/?hl=ja

「プロジェクトを追加」をクリックして、サンプルプロジェクトを作成する。
image.png

「sample」と名前を入力して、サンプルプロジェクトを作成。
image.png

ここまでで、Firebaseの前準備は完了だ。

開発環境を準備する

node.jsをインストールする

開発環境のベースとして、まずはnode.jsをインストールする。
こちらの公式サイトからインストーラをダウンロードする。
https://nodejs.org/en/

左側にある、「Recommended For Most Users」と書いてあるボタンから。
image.png

ダウンロードしたインストーラを起動して、node.js一式をインストールする。
選択肢はデフォルトのままでOK。

インストール後、コマンドプロンプトから下記コマンドを実行。バージョンが表示されればインストール成功だ。

> node --version
v10.15.3

Firebaseツールをインストールする

続いて、Firebaseの開発環境を準備していこう。
npmを使って、次のコマンドでFirebaseツールをインストールする。

> npm install -g firebase-tools

次に、Firebaseアカウントにログインする。

> firebase login

エラーレポートについて訊ねられるので、いちおうYesを選んでおこう。

? Allow Firebase to collect anonymous CLI usage and error reporting information? (Y/n)

Windowsファイアウォールのアクセス許可を求められるかもしれないが、その場合は「アクセスを許可する」を選んでおく。

image.png

ブラウザ画面上でGoogleログインを促されるので、適切なGoogleアカウントでログインする。

CloudFunctions開発環境をセットアップする

下記のコマンドで、Functionsセットアップに進もう。
適当なところに新しくフォルダを作成して、そこで下記コマンドを実行。

> firebase init functions

続けていいか聞かれるので、当然Yesを。

? Are you ready to proceed? (Y/n)

どのプロジェクトを使用するか選択を求められるので、先程Firebase上で作成したsampleプロジェクトを指定しよう。

=== Project Setup

First, let's associate this project directory with a Firebase project.
You can create multiple project aliases by running firebase use --add,
but for now we'll just set up a default project.

? Select a default Firebase project for this directory:
  [don't setup a default project]
> sample-00000 (sample)
  [create a new project]

使用する言語を聞かれるので、まずはJavaScriptを選択しておこう。

=== Functions Setup

A functions directory will be created in your project with a Node.js
package pre-configured. Functions can be deployed with firebase deploy.

? What language would you like to use to write Cloud Functions? (Use arrow keys)
> JavaScript
  TypeScript

文法・構文などをチェックしてくれるESLintというツールの使用を訊ねられるが、いったんNoを選んでおこう。

? Do you want to use ESLint to catch probable bugs and enforce style? (y/N)

必要な依存ライブラリ等をインストールするか聞かれるので、Yesを選んでおこう。

? Do you want to install dependencies with npm now? (Y/n)

ファイル・フォルダがいくつか自動で作成されるので、下記のように /functions/index.js ファイルの下半分がコメントアウトされている部分を、コメント除去する。
なおコード編集には、Visual Studio Codeの利用がオススメ。

/functions/index.js
const functions = require('firebase-functions');

// // Create and Deploy Your First Cloud Functions
// // https://firebase.google.com/docs/functions/write-firebase-functions
// 
// exports.helloWorld = functions.https.onRequest((request, response) => {
//  response.send("Hello from Firebase!");
// });

ローカル環境で実行を確認する

下記のコマンドを実行すると、ローカルのPC内に仮想サーバが起動し、Functionsのデバッグ実行が可能となる。

> firebase serve --only functions

すると下記のようにメッセージが表示され、下の行にアドレスが記載されているので、これをコピーしてブラウザでアクセスしてみる。
メッセージが表示されるのを確認しよう。

=== Serving from 'C:\xxxxxxxx\Functions'...

i  functions: Preparing to emulate functions.
Warning: You're using Node.js v10.15.3 but Google Cloud Functions only supports v6.11.5.
+  functions: helloWorld: http://localhost:5000/sample-00000/us-central1/helloWorld

次にパラメータを使った関数を作成してみる。
helloWorld 関数を、以下のように書き換えてみよう。

/functions/index.js
exports.helloWorld = functions.https.onRequest((request, response) => {
  const name = request.query.name;
  response.send(`Hello, ${name}!`);
});

書き換えて保存したら、ブラウザのURLを helloWorld?name=hoge のように変更してアクセスしてみる。表示されるメッセージが変わったはずだ。

ローカルデバッグを終了するときは、Ctrl+Cを押すこと。

Firebaseにデプロイする

ローカルでの動作が確認できたので、下記コマンドでFirebaseにこのプログラムを送信しよう。

> firebase deploy

しばらく待つとデプロイが完了する。

デプロイしたFunctionは、Firebase Consoleからアドレスが確認できる。
このアドレスをブラウザから開いて、動作を確認しよう。
image.png

以上で、Firebase CloudFunctionsの簡単なサンプルが実行できた。
他の動きに書きかえる、関数を増やすなどして、Functionsの使い方を続けて探っていってみよう。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

GoogleスプレッドシートのCONCATENATEの文字制限をGASを用いて突破する方法

問題

Google スプレッドシートの標準の関数CONCATENATE
https://support.google.com/docs/answer/3094123?hl=ja

文字列を結合してくれるものなのですが5万文字を超える場合はエラーがでて結合ができなくなる。
スクリーンショット 2019-04-15 19.30.12.png

解決方法

今回はこれをGAS(Google App Script)を用いて解決する
GASを書くには以下のようにツール→スクリプトエディタでエディタで開く
スクリーンショット 2019-04-15 19.32.37.png

コードはたったの5行!で今回はCONCATENATEを独自拡張したものなので関数名はCONCATENATE_EXTENSIONとしました。
スクリーンショット 2019-04-15 19.34.22.png
やってることは引数を取得してtextのsplit関数で結合しているのみです。

これを保存するとそのシートで CONCATENATE_EXTENSION(A1, A2) みたいにCONCATENATEと同じように使えて5万文字の制限がないものとして使える。

まとめ

今回のCONCATENATE関数のようにGoogleスプレッドシートで制限がかかっているような関数でもGASを使って独自に(5行くらい)書けば便利な関数が使えるのでオススメです!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

長押しボタンを作ってみた【円プログレス対応】

はじめに

前回作成した記事の続きです。
長押しボタンを作ってみた【初期実装】

今回やりたいこと

長押しボタンの進捗表現の方法として、ボタンの周りに進捗状況が表示されて、
ボタンの中心にカウントが表示されるようなやつを作りたいと思います。

スクリーンショット 2019-04-15 11.03.28.png
※Fortniteより参照

作る

進捗の表示

ボタンの周りに進捗をつけるために調べた結果、 conic-gradient というのが使えることがわかりました。
conic-gradient()

cssの関数らしく、特定の点を中心にグラデーションを表現できるようなものらしいです。

例えば、

conic-gradient.css
{
  width: 100px;
  height: 100px;
  border-radius: 50%;
  background: conic-gradient(red 90deg, green 90deg 180deg, yellow 180deg 270deg, blue 270deg 360deg);
}

こんな感じで指定すると、

スクリーンショット 2019-04-15 11.15.57.png

こんな感じの円が表現できます。
なので、このcssの値をjavascriptで動的に変化させることで進捗を表現します。

index.html
<script>
var LIMIT = 10000;
var progress = 0;
・・・
progress = progress + (360 / (LIMIT / 100));
var outerCircle = document.getElementById('outer-circle');
outerCircle.style.background = 'conic-gradient(red ' + progress + 'deg, white 0deg 360deg)';
</script>

javascript内で conic-gradient の値を動的に変化させてます。

残り時間の表示

円の中心に残り時間を表示させるために、カウントの処理を修正しました。

今まで setInterval を使ってカウントアップさせ、そのカウントを使っていましたが、
setInterval は、ブラウザ負荷などによって正確には指定した秒数毎に呼ばれないため、
時間を取得してそれを利用するように変更しました。

index.html
<script>
var LIMIT = 10000;
var INTERVAL = 100;
var button1 = document.getElementById('button1');
var sec = document.getElementById('sec');

・・・
var start = function () {
  startTime = new Date().getTime();
  if (!timer) {
    timer = setInterval(counter, INTERVAL);
  } 
}
・・・
var counter = function() {
  var now = new Date().getTime();
  var sec = document.getElementById('sec');
  sec.innerText = ((LIMIT - (now-startTime)) / 1000).toFixed(1);
  if (LIMIT < now-startTime) {
    sec.innerText = 'OK';
    clearInterval(timer);
    timer = 0;
    count = 0;
  }
}
</script>

完成

動画が貼れないので、リンクを置いておきます。
https://www.youtube.com/watch?v=nqjS81ull1k&feature=youtu.be

ソースコードは、以下にあります。
https://github.com/takuhou/smart-ui/blob/feature/circle-progress/index.html

結構ぬるぬる動きます。

今後の対応として、実は、 conic-gradient がchrome以外の他ブラウザでほぼ動かないので、ブラウザ対応をやります。
Can I use

今回の記事で書いたプログラミングの様子をYoutube上にアップロードしておりますので、是非ご覧ください。
【実況】長押しボタンをプログラミング【円ボタン対応】
えんじにぁ〜TV

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

雰囲気で使わない React hooks の useCallback/useMemo

React hooks にはメモ化のための useCallback と useMemo という関数があります。
hooks を使い始めて、この二つの関数を知った私はこう思いました。

「え?無条件でパフォーマンス上がるんなら全部これで書くべきやん!」

と。

というわけで、しばらくそのスタンスで書いてきたのですが、果たしてこの「無条件でパフォーマンスが上がる」という前提は本当に正しいのか、というかそもそも"パフォーマンス上がる"とは具体的に何をしてくれるのかを理解せずに使っていたとので、ここで「全て useCallback/useMemo で書く」という方針が正しいのか、それとも他の方針が存在するかを考えてみました。

大きく次の3つの観点で考えます。

  1. パフォーマンス
  2. 可読性
  3. バグの発生しやすさ

1.パフォーマンス

「そもそも useCallback/useMemo はパフォーマンス向上の用途なのに、パフォーマンス観点で気にすることあるんか?」

と思った方もいらっしゃるかもしれません。まあ私もそう思っていたんですが、昔も「PureComponentにするとむしろパフォーマンスが落ちるケースがある」みたいなこともあったので、無思考にそう信じるのもよろしくないと思うのでこれをしっかり理解してみようと思います。

useCallback/useMemo 自体の処理コストを考える

先ほどの前提が正しいかを確認するためには「useCallback、useMemoを使った時のコスト」と「使わなかった時のコスト」を比べる必要があります。

まず useCallback/useMemo の処理のコストというのは「メモ化した値(厳密言うと deps ですが)との比較」です。
hooks は基本的に dependencyList(この記事ではこれ以降 deps と呼びます) と呼ばれるものを配列とした引数に取り、その値が変わった時に値が更新されるようになっています。

例として一度 mount した後の useCallback の処理である updateCallback を見てみましょう

react/ReactFiberHooks.js at master · facebook/react · GitHub

function updateCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  const prevState = hook.memoizedState;
  if (prevState !== null) {
    if (nextDeps !== null) {
      const prevDeps: Array<mixed> | null = prevState[1];
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        return prevState[0];
      }
    }
  }
  hook.memoizedState = [callback, nextDeps];
  return callback;
}

この中の areHookInputsEqual というものが比較のための関数みたいですね。これも見てみましょう。

function areHookInputsEqual(
  nextDeps: Array<mixed>,
  prevDeps: Array<mixed> | null,
) {
  // いろいろ warning 割愛

  for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
    if (is(nextDeps[i], prevDeps[i])) {
      continue;
    }
    return false;
  }
  return true;
}

指定された deps 毎に is という関数で比較しているようです。さらにそれも見てみましょう。

react/objectIs.js at master · facebook/react · GitHub

function is(x: any, y: any) {
  return (
    (x === y && (x !== 0 || 1 / x === 1 / y)) || (x !== x && y !== y) // eslint-disable-line no-self-compare
  );
}

(全然話逸れるが、なぜこれが Object.is と同値なのか全く分からない。教えて強い人。)

なのでこの is関数 x deps に指定された要素の数が useCallback/useMemo 自体のコストと考えて良さそうです。次に使わない場合のコスト、言い換えると useCallback/useMemo がどんな状態の時にどんな最適化をもたらしているかを見ていきましょう。

useMemo の恩恵

これまで useCallback/useMemo とひっくるめて語っていましたが、ここからは分けて考えます。

useMemo は計算した結果を保持するための関数です。

const memoedValue = React.useMemo(() => /* 何かしらの複雑な計算 */, [])

なので useMemo を使わない場合の処理コストは、中身で行なっている計算によります。 その計算が is関数 x deps の数より重ければ useMemo を使った方がお得ということになります。ただいちいち「これは useMemo 使った方がパフォーマンスいいのか?」と考えるのもそれはそれで生産性低いので、ある程度自動的に使う使わないの判断ができるような軸が欲しい…と思いましたが、いい感じの軸が思いつきませんでした。

例えば次のようなものは useMemo 使うまでもないとパッと見で分かると思うんですが、

const checkedAll = checkedIds.length === items.length

もう少し複雑なものがどっちがお得かは実際に検証してみないと分からないでしょう。一々そんなことをするのも馬鹿らしいので、疑わしきものはとりあえず useMemo みたいな方針が濃厚なのかなという気がします。

useCallback の恩恵

まず useCallback による最適化は useMemo の純粋にメモ化することによって計算量を抑えるのとは意味合いが違います。

実際に検証してみたわけではないですが(めんどくさい)、関数インスタンスを作成するコストは先ほどの is関数の比較するコストよりおそらく低いでしょう。このコストだけ考えると「あれ?useCallbackってコスト増えるだけで意味なくね?」と思うかもしれませんが、そうではありません。

useCallback は不要に新しく関数インスタンスを作成することを抑制することによって不要な再描画を減らしてくれます。詳しい話はこちらの記事のアンチパターンとして挙がっている「アロー関数をpropsに即時関数で渡す」を読むと分かりやすいと思います。こちらの記事では bind を使うことによって新しく関数が作成されることを防いでいますが、 useCallback を使うことにより Class でなくとも同じことができるようになります。

逆に言うと、子供のコンポーネントに対して関数を渡すようなことがなければ、特に useCallback を使う意味はないということです。(厳密に言うと PureComponent のように Props の変化に対して shallow equal で再描画判定を行うような場合限定なのですが、親が子のコンポーネントがどうやって再描画判定しているのかを気にするのも不自然な話なので、子に関数を渡している場合は useCallback で包んでおけばいいと思います。)

まとめ

まとめるとパフォーマンス観点では次のようになります。

  • useMemo
    • 疑わしきものは大体useMemo使っておけばおk
  • useCallback
    • 子に関数の参照を渡す場合は useCallback 使っておく。そうでない場合は使っても意味ない

余談: メモリに関して

ちなみに何ですがメモ化と聞くと、単純な実行速度や計算量の話だけでなく「メモリをめっちゃ食ってしまうことはないの?」と疑問を抱く方がいらっしゃるかもしれませんが、こと hooks のメモ化に関しては特に問題がありません。

というのも一般的なプログラミングの関数のメモ化とは違い、 hooks のメモ化は直前の値しか記憶していないからです。なので useCallback/useMemo を使っている場合と使っていない場合でメモリの使用量は変わりません。

試しに先ほども登場した useCallback のアップデート処理を見てみましょう。

function updateCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  const prevState = hook.memoizedState;

  // deps が変わっていない場合は前回の State を返す
  if (prevState !== null) {
    if (nextDeps !== null) {
      const prevDeps: Array<mixed> | null = prevState[1];
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        return prevState[0];
      }
    }
  }

  // 変わっている場合は新たな State に上書きして返す
  hook.memoizedState = [callback, nextDeps];
  return callback;
}

ご覧の通り hook.memoizedState は一つだけです。これは deps が変わった時に「上書き」されます。このため、メモ化によるメモリの使い過ぎについては特に心配する必要はありません。

2.可読性

次に可読性です。当然ですが useCallback/useMemo を使うと1個包む関数が増えるので普通に記述するよりいささか冗長になります。

const hoge = () => console.log('fuga')
const hoge = React.useCallback(() => console.log('fuga'), [])

うん、微妙に読みづらいですね。

ただ、書いといてなんですが、個人的にはこの観点どうでもよいと思っています。他の観点の方がより本質的な課題なのでこれが決め手になるというのはあり得ないなと。ただ気まぐれに useCallback/useMemo 使っているところとそうでないところが散乱しても気持ち悪いので、方針はちゃんと揃えておきたいなという気持ちです。

3.バグの発生しやすさ

これは実体験からの感想ですが、useCallback/useMemo(特に useCallback の方)を使う方がバグが増えます。
理由としては useCallback を使った際に dependencyList をちゃんと書いていないと、古い値を保持してしまうためです。
ちなみにかの Dan 先生はこのような値の保持を stale closures と呼んでいます。直訳で古いクロージャという意味ですね。

もし全く useCallback/useMemo を使わない場合は、パフォーマンス改善はないもののとりあえず常に最新の値を参照してくれるため、また、多くの人はこの挙動を期待したメンタルモデルでプログラミングしていると思うので、意図しない挙動は減る印象です。

防ぐ方法

上記のようなバグを防ぐための基本的なプラクティスとしては「useCallback/useMemo の中で参照する要素は全て deps に書く」です。とりあえずそうしておけば必ず最新の値には保たれます。

また、このリスクを軽減する方法として eslint の exhaustive-deps があります。

[ESLint] Feedback for 'exhaustive-deps' lint rule · Issue #14920 · facebook/react · GitHub

この lint rule を入れれば、 deps が必要な useXXX の関数を使った時に、 deps に列挙されていない要素があれば warning を出してくれるようになります。

小ネタ: deps に書きたくない時もある

基本的には全部列挙した方がいいというのはそうなんですが、あえてそうしたくない場合もあります。
分かりやすいのが古くは componentDidMount でやっていたような「Mount時に一回だけ」やりたいような処理は useEffect を使いつつも deps には何も入れたくありません。それ以外にも小技として useCallback 内で useState の set系の関数を実行する時に、引数に関数を入れ、deps には何も入れないことによって関数の再生成を抑えることができます。

詳しくはこちらの記事にまとまっていますが、この記事でも軽く解説します。
React Hooks、useStateの更新関数引数には関数を - Qiita

まずは普通に deps に定義した例

function Hoge() {
  const [count, setCount] = React.useState(0)

  const increment = React.useCallback(() => {
    setCount(count + 1)
  }, [count])

  return (
    <div>
      <Button onClick={increment}>ふやす</Button>
      {count}
    </div>
  )
}

これでも動きはしますが、 count がアップデートされる度に関数が生成されてしまいます。
useCallback 内では以前の状態と相対的に状態を更新しているだけです。そして useState の更新関数では関数を引数に与えると、その関数の引数に現在の状態が入ってきます。これを利用することによって次のような書き方でも意図通り動くようになります。

function Hoge() {
  const [count, setCount] = React.useState(0)

  const increment = React.useCallback(() => {
    setCount(_count => _count + 1)
  }, [])

  return (
    <div>
      <Button onClick={increment}>ふやす</Button>
      {count}
    </div>
  )
}

こうすることによって、useCallback での関数生成はマウント時に1回きりだけでよくなります。

なにが言いたいかというと、「deps に全て列挙する」が常に正しいわけではなく、意図的に列挙しない指定しないケースもいくつかあるのでルールは柔軟にしていきたいね、ということです。この辺も型化して自動で判定していけるようにできればいいですね。

結論

長々と語ってきましたが、結論としては次のような感じで書いていくのがいいかなと思います。

  • useCallback/useMemo が効果を発生する状況をちゃんとチームで周知する
    • 自動化で判定するのは現状難しそうなので、知識共有して個々で適材適所使いどころを判断する
  • eslint の ehaustive-deps を可能であれば利用する
    • なんらかの形で「参照している要素を deps に指定する」を実現できればよい
    • ただしあえて指定したくないケースもあるので柔軟に対応する

"適材適所"というフワフワした結論にはなってしまいましたが、ご意見あればぜひコメントください!
お読みいただきありがとうございました?‍♂️

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Uncaught TypeError: $.ajax is not a function

jQueryのCDNちゃんと書いたのに$.ajaxが使えないと出る

Uncaught TypeError: $.ajax is not a function

なんで><

...ググる...

あった〜〜
TypeError: $.ajax is not a function エラーが出た時の対策(jQuery 3.x)

CDN、軽量版(slim)使ってた....
minifiedに変えたら普通にできた笑

ちゃんと説明読めよって話

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Gulp Version4.0で環境構築してみた

概要



今回 趣味で 動きのあるサイトを作りたいなと思って JavaScriptを使用しようかなと思っていました。そしてタスクランナーを使用した方が色々と便利であるということを知り gulpを使ってみることにしました。gulp version4は2018年の12月11日にリリースされ version3と比べると変更点が少しあり まだまだアップデートされて日が浅いのでなかなか 記事がそこまで多いとは言えず 結構 苦労しました。お役に立てれば幸いです。 なお 初めて記事を投稿するのでお手柔らかにしていただけるとありがたいです笑

手順 ( 簡潔に )


今回はnpmのローカルインストールでいきたいと思います。

npm install --save -dev gulp 
npm install --save -dev browser-sync



--save -devは 上記でインストールした情報を package.jsonのdevDependenciesに記入してくれます。


ディレクトリー構成

image.png

babelの設定


{
  "presets": ["es2015"]
}

ES2015のソースコードを使用するので それを自動コンパイルするために設定します。




gulpfile

gulpfile.babel.js
import gulp from "gulp";
import browserSync from "browser-sync";

gulp.task("browserSync", function() {
  browserSync({
    server: {
      baseDir: "./src/",
      index: "Home.html"
    }
  });
});

gulp.task("bs-reload", function(done) {
  browserSync.reload();
  done(); -- タスクの終了を明記する
});

gulp.task("browser-sync", function() {
  gulp.watch("./src/*.html", gulp.task("bs-reload"));
  gulp.watch("./src/*.css", gulp.task("bs-reload"));
  gulp.watch("./src/*.js", gulp.task("bs-reload"));
});

gulp.task("default", gulp.parallel("browserSync", "browser-sync"));


注意点は主に2つあります。


1つ目はbrowserSync.reload();した後にdone()という きちんとタスクをここで終了するということを明示してあげると良いかと思います。これをやらないと初めてファイルが変更された時にしかreloadされないといった現象が起こるようです。またfunction()にきちんとdoneを渡してあげてください。渡さないとReferenceErrorが出ます。

2つ目はversion4からgulp.watchの引数はgulp.taskやgulp.parallelといったものに変更になっているのでそのように書くと良いです。Version3のように記入するとReferenceErrorが出ます。

実行する時


cd プロジェクトの名前
npx gulp


こちらについては npx gulp のコマンドで実行するのが便利です。

参照記事

https://qiita.com/tonkotsuboy_com/items/8227f5993769c3df533d
https://mae.chab.in/archives/2547
https://github.com/mochajs/mocha/issues/232
https://hyper-text.org/archives/2018/12/gulp-4-0-release.shtml
https://teratail.com/questions/168814

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Javascript 「良いパーツ」によるベストプラクティス を読んで

はじめに

JavaScript 「良いパーツ」によるベストプラクティスを読んだ感想です。

amazon

間違い等ありましたら、指摘をお願いいたします。

感想

JavaScriptを題材にしていますが、すべての言語に対して共通の考え方ができる部分も多く、大変勉強になりました。
JavaScriptはJavaAppletの失敗により世に出てすぐに使われはじめ、研究機関などによるブラッシュアップを受けていない言語だということを初めて知りました。
というより、様々な言語も最初は完成度が低く、ブラッシュアップを経て、盤石な言語へと進化していくという当たり前のことを認識させられました。
これが、OSSへの貢献の重要性などにつながってくるのだと考えました。

また、JavaScriptの良くない部分は使わず、良い部分を選択して使用する、という考え方が非常に大事です。
この考え方は、JavaScriptだけに留まらず、様々な言語で活用できるかと思います。
良くないパーツ = 誤解を招く可能性があるパーツ。
これまでわかりにくい実装は、ユーティリティコードを自身で書いて対応してきましたが、そもそも使わないという選択があるのであればそれが最善であることがわかりました。

これらの気づきを実現するためには、柔軟にコーディングを変更するスキルが必要であり、悪魔的な書き方で1行で書けてしまうという甘い誘惑に打ち勝つ考えが必要だと感じました。

今後に生かすこと

コールバック、クロージャ、メモ化などの実例が順序立ててわかりやすく記載されています。
コールバックとクロージャの必要性と理解できたことは自身の考え方に大きな変化を生んでいると思います。
メモ化は、どんな言語でも適用できるキャッシュ技術だと思います。
今まで自身で作成してきたプログラムに思いをはせると、メモ化で高速化できるものがあったように思います。
アプリケーション側でキャッシュするという考え方、とても素晴らしいです。

簡単ですが、アウトプットします。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

jQueryからVue.jsへ乗り換えてみた

はじめに

 この記事でQiitaデビュー?ですが、文書書くの下手くそなので、読みにくかったらすみません。
JavaScriptとjQueryで個人的に作りたかったマネーカウンター(コインカウンター)を題材に、仕事と趣味半々で作ってました。でも複雑怪奇になったのでVue.jsを使えば多分楽になると思って、移植しました。
 なお、PWAとして まねかん と、 Cordovaでラッピングして Androidアプリ「まねかん」 がありますので、参考まで。 ソースは GitHub においてあります。

「まねかん」制作のアイデア

 個人的にとある集会の会計をやっていて、小銭勘定を常に行う必要があったので、Excelで計算してたのですが、必ず手元にExcelがあるわけがないので、スマホアプリを作ろうと考えました。大元のExcelはこんな感じ。
image.png
 ただし、スマホアプリを一から設計するだけの余裕と技量が無いので、Webアプリの作り方をググっていくと、PWAとか便利な技術があることに気づきました。これならできると早速作成を開始しました。

jQueryの頃

 考え方は単純なので、感覚としては

<!DOCTYPE html>
<html lang="jp">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
     <script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
</head>
<body>
<p>
<span>1円:</span>
<input class="count" type="number" value="0"><span class="subtotal s1">0円</span>
</p>
<p>
    <span>5円:</span>
    <input class="count" type="number" value="0">
    <span class="subtotal s5">0円</span>
</p>
<p>
    <span>10円:</span>
    <input class="count" type="number" value="0">
    <span class="subtotal s10">0円</span>
</p>
<p>
    <span>50円:</span>
    <input class="count" type="number" value="0">
    <span class="subtotal s50">0円</span>
</p>
<p>
    <span>100円:</span>
    <input class="count" type="number" value="0">
    <span class="subtotal s100">0円</span>
</p>
<p>
    <span>500円:</span>
    <input class="count" type="number" value="0">
    <span class="subtotal s500">0円</span>
</p>
<p>
    <span>千円:</span>
    <input class="count" type="number" value="0">
    <span class="subtotal s1000">0円</span>
</p>
<p>
    <span>弐千円:</span>
    <input class="count" type="number" value="0">
    <span class="subtotal s2000">0円</span>
</p>
<p>
    <span>五千円:</span>
    <input class="count" type="number" value="0">
    <span class="subtotal s5000">0円</span>
</p>
<p>
    <span>壱万円:</span>
    <input class="count" type="number" value="0">
    <span class="subtotal s10000">0円</span>
</p>
<p>
    <span class="total">0円</span>
</p>
<script>
    let items = [
        {type: 's1', callName: '1円', unit: 1},
        {type: 's5', callName: '5円', unit: 5},
        {type: 's10', callName: '10円', unit: 10},
        {type: 's50', callName: '50円', unit: 50},
        {type: 's100', callName: '100円', unit: 100},
        {type: 's500', callName: '500円', unit: 500},
        {type: 's1000', callName: '千円', unit: 1000},
        {type: 's2000', callName: '弐千円', unit: 2000},
        {type: 's5000', callName: '五千円', unit: 5000},
        {type: 's10000', callName: '壱万円', unit: 10000}
    ];
    $(function() {
        $('input').on('change', function () {
            let total = 0;
            for (let i = 0; i < $('.count').length; i++) {
                let c = Number($('input').eq(i).val());
                let u = items[i].unit;
                let subtotal = u * c;
                total += subtotal;
                $('.subtotal.' + items[i].type).text(`${Number(subtotal).toLocaleString()}円`);
            }
            $('.total').text(`合計:${Number(total).toLocaleString()}円`);
        });
    });
</script>
</body>
</html>

とデータを用意して、あとはinputタグだけで計算させてました。
あとはjQueryでイベント拾って計算させるだけという簡素な物でしたが、いろんなjQueryコンポーネントで装飾していくと、いろいろイベント処理とかが複雑になってきました。で、Vue.jsをつかうと楽になる的な情報を聞きました。

Vue.jsに移植

 Vue.jsは設計思想的には非常に合理的で、フォームの値が変わったら計算項目はすべて自動計算されるのが特徴です。jQueryだとDOMにイベントを割り当てて、関数を定義しなければいけませんが、基本の書き方をすれば、全部計算してくれます。

<!DOCTYPE html>
<html lang="jp">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script src="https://cdn.jsdelivr.net/npm/vue"></script>
</head>
<body>
<div id="app">
    <p v-for="(item, index) in items">
        <span>{{ item.callName }}</span><input type="number" v-model.number="item.counts"><span class="subtotal"> {{ subtotalMoney[index] }} 円</span>
    </p>
    <p>
        <span>合計: {{ totalMoney }} </span>
    </p>
</div>
<script>
    const app = new Vue({
        el: '#app',
        data: {
            items: [
                {callName: '1円', unit: 1, counts: 0},
                {callName: '5円', unit: 5, counts: 0},
                {callName: '10円', unit: 10, counts: 0},
                {callName: '50円', unit: 50, counts: 0},
                {callName: '100円', unit: 100, counts: 0},
                {callName: '500円', unit: 500, counts: 0},
                {callName: '千円', unit: 1000, counts: 0},
                {callName: '弐千円', unit: 2000, counts: 0},
                {callName: '五千円', unit: 5000, counts: 0},
                {callName: '壱万円', unit: 10000, counts: 0}
            ]
        },
        computed: {
            totalMoney: function () {
                let total = 0;
                for (let i = 0; i < this.items.length; i++) {
                    total += this.items[i].unit * this.items[i].counts;
                }
                return Number(total).toLocaleString();
            },
            subtotalMoney: function () {
                let subtotals = [];
                for (let i = 0; i < this.items.length; i++) {
                    subtotals.push(Number(this.items[i].unit * 
                        this.items[i].counts).toLocaleString());
                }
                return subtotals;
            }
        }
    });
</script>
</body>
</html>

 Vue.jsは見た目に難しいようですが意外と単純で、computed:節に計算する関数を書けば、いちいち関数をコールする必要もありません。
 jQueryだけだとイベントを一つ一つ書かなければいけませんが、すっきりします。

雑感

 何で先に Vue.js を覚えなかったのか?と思うほど動作を単純化できます。もちろん見栄えを良くするにはBootstrapとかで装飾したりでjQueryも必要ですけれども…
 そのうちVueコンポーネントの書き方でも書こうかなと思ってます。

参考

Vue.js 公式サイト(日本語) https://jp.vuejs.org/index.html
jQuery 公式サイト(英語) http://jquery.com/

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【JS】フォームでデータ送信後、サブウィンドウを開く方法

JS勉強中の初心者の覚書です。

サブウィンドウは、window.openで開くことができます。

サブウィンドウを開く
window.open("url", "window_name", "width=100,height=300,scrollbars=yes");

普通にサブウィンドウを開くだけなら、
上記のコードをonclickなりで指定してあげれば問題ありません。

私は、
フォームからデータをPOSTしつつサブウィンドウを開きたいなあ、と思いました。
上記のコードでは、指定したURLを開くことはできますが
当然、データを送信することはできません。

そういうことをするためには、
先にサブウィンドウを開いた後で、
そのサブウィンドウをtargetに指定してsubmitしてあげればOKです。

フォーム
<form action="/action" method="post" name="form_hoge" id="form_hoge">
  <input type="text" name="name" value="">
  <input type="button" name="button" value="submit" onclick="open()">
</form>
サブウィンドウを開く
function open() {
    window.open("about:blank","window_name","width=100,height=300,scrollbars=yes");
    document.form_hoge.target = "window_name";
    document.form_hoge.submit();
}

こうすることによって
あたかもデータ送信しながらサブウィンドウが開いたように見えるわけですね。
カンタン!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Payjpに登録したクレジットカードで商品購入を実装する(Rails)

この記事を読む前に

↓前回の投稿内容が実施されていることを前提に記載しています。
Payjpでクレジットカード登録と削除機能を実装する(Rails)

実装する機能と前提条件

Payjp(Pay.jp)から既に顧客IDとカードIDを取得済みでの購入を想定しています。
実装するものとしては下記のとおりです。

  • 自作のカード登録フォーム(前回)
  • カード情報とユーザーの紐づけ(前回)
  • ユーザーとカードの登録と削除(前回)
  • 商品の購入(←今回)

「購入する」ボタンを押して購入!
image.png

バージョン情報

ruby 2.3.1
Rails 5.0.7

前提条件

  • deviseが導入済みでログインができている(current_userを使うため)
  • hamlでの記載(gem 'haml-rails')
  • payjpのアカウントが既に取得できていて、ユーザーとカードの登録が完了している(前回実施)
  • cardテーブルに以下の情報が登録されている(前回実施)
    • user_id → UserテーブルのID
    • customer_id → payjpの顧客ID
    • card_id → payjpのデフォルトカードID

顧客IDとデフォルトカードIDは以下の画面で顧客ごとに確認できます。
もしDBを作成していなかったり可動しなかったりする場合はID直打ちでトライしてみましょう。
顧客詳細確認画面
image.png

1.コントローラーを作成しよう

app/controllers/purchase_controller.rb
class PurchaseController < ApplicationController

  require 'payjp'

  def index
    card = Card.where(user_id: current_user.id).first
    #Cardテーブルは前回記事で作成、テーブルからpayjpの顧客IDを検索
    if card.blank?
      #登録された情報がない場合にカード登録画面に移動
      redirect_to controller: "card", action: "new"
    else
      Payjp.api_key = ENV["PAYJP_PRIVATE_KEY"]
      #保管した顧客IDでpayjpから情報取得
      customer = Payjp::Customer.retrieve(card.customer_id)
      #保管したカードIDでpayjpから情報取得、カード情報表示のためインスタンス変数に代入
      @default_card_information = customer.cards.retrieve(card.card_id)
    end
  end

  def pay
    card = Card.where(user_id: current_user.id).first
    Payjp.api_key = ENV['PAYJP_PRIVATE_KEY']
    Payjp::Charge.create(
    :amount => 13500, #支払金額を入力(itemテーブル等に紐づけても良い)
    :customer => card.customer_id, #顧客ID
    :currency => 'jpy', #日本円
  )
  redirect_to action: 'done' #完了画面に移動
  end

end

2.購入画面と完了画面を作成しよう

購入画面
app/views/purchase/index.html.haml
%h2 購入を確定しますか?
%p Apple MacBook Pro 13インチ
%p ¥135,000(送料込み)
%br
%h3 支払い方法
- if @default_card_information.blank?
  %br /
- else
  -#以下カード情報を表示
  = "**** **** **** " + @default_card_information.last4 
  - exp_month = @default_card_information.exp_month.to_s
  - exp_year = @default_card_information.exp_year.to_s.slice(2,3)
  = exp_month + " / " + exp_year
%br
= form_tag(action: :pay, method: :post) do
  %button 購入する

image.png
ちなみにexp_monthはカードの期限月、exp_yearは期限年、last4はカードの下4桁を取得します。
https://pay.jp/docs/api/#顧客のカード情報を取得

完了画面
app/views/purchase/done.html.haml
%h2 購入が完了しました!
%p Apple MacBook Pro 13インチ
%p ¥135,000(送料込み)

image.png

3.ルートを設定しよう

config/routes.rb
  #今回設定分
  get 'purchase', to: 'purchase#index'
  post 'purchase/pay', to: 'purchase#pay'
  get 'purchase/done', to: 'purchase#done'

本当に購入されているのだろうか…

実装はこれで完了です!
画面変遷はできてても登録ができているか不安な場合は
売上一覧の画面で確認できます!
image.png

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Amazonプライムが値上げだそうですが、Prime Videoの見放題終了日を表示するブラウザ拡張にパッチを当てた

なんだか Amazonプライム が値上げだそうですね。
私もAmazonプライムを利用しているので値上げは少し残念ですが、これも良いサービス継続のためと思って受け入れようと思っています。

ところで Amazon Prime Video って、見放題の期間が終了しそうな作品の検索ページがあるのですが、
Amazon.co.jp: 30日以内にプライム会員特典ではなくなる作品

この作品検索ページに公開期限を表示してくれるChrome拡張/Safari拡張がありまして、それにパッチを当てましたのでご紹介します。

expiration.png
↑作品サムネイル画像をマウスオーバーすると、こんな感じで公開期限をポップアップ表示してくれます。

動機

本来のAmazonの作品検索ページでは肝心の「公開期限がいつか?」が分からず辛かったのですが、公開期限を表示してくれるChrome拡張をGithubで見つけた!
harawata/prime-video-expiration-date: Amazon ビデオの "30 日以内にプライム会員特典ではなくなる作品" のリストに見放題終了日を表示します。
作者は MyBatis で有名な harawata さんです。

しかしこのコードは2017年頃に書かれたようで、残念ながら2019年4月現在のAmazon検索ページではうまく動作しませんでした。。。

そこでパッチを当てて動作するようにしました。

インストール方法 for Chrome

本家のharawataさんにPull Requestしましたので、いずれ取り込まれるかもしれませんが、お急ぎの方は以下のリンクからパッチ版であるfix-for-amazon-updatesブランチのソース一式を[Clone or Download]ボタン等でzipファイルをダウンロードして、
htaketani/prime-video-expiration-date at fix-for-amazon-updates

早速、本家のharawataさんにマージしてもらいました!
以下の Releasesページから v1.1.0がDLできます。
Releases · harawata/prime-video-expiration-date

解凍した中身の PrimeVideoExpirationDate.safariextension ディレクトリをChromeの 拡張機能(chrome://extensions/) ページにドラッグ&ドロップしてインストールしてください。

Chromeの最新版(バージョン 73.0.3683.86)で動作確認しています。

インストール方法 for Safari

オリジナルのプログラムはChromeとSafariの両対応しているとのことですのでSafari用の Info.plist にも修正を入れています。

インストール方法は README を参考にしてください。

しかしながら、私の修正が悪いのかインストール方法が悪いのか、手元のSafariにインストールを試みたものの動作させることができませんでした。。。
[未署名の機能拡張を許可]オプションを有効にしてみたりしたのですが、よく分かりませんでした。
私は普段Chromeしか使っていないため、Safariの機能拡張について明るくなく、このあたりの方法分かる方がいらっしゃいましたら、アドバイスいただければと思います。

修正に際して出会った技術的ないくつか

普段、フロントエンドは全くやらないので苦労しました。
何かおかしな所あったらご指摘ください。

脱jQuery?

オリジナルはjQueryを利用して書かれていましたので、最初は「ついでにjQueryを使わずに書き直そうかな」とも思ったのですが、ちょっと面倒で結局jQueryをそのまま利用しています。

とりあえず、jQueryを最新版 v3.4.0にアップグレードしておきました。

AmazonのAPIが返すjsonの末尾にゴミ

https://www.amazon.co.jp/gp/video/hover/(ASINコード) で得られる json は以下のような形式ですが、なぜか末尾にゴミ?のような文字列「&&&」がくっついていました。

末尾にゴミのあるjson
["hover-body","<div id=\"av-hover-body\" >...</div>"]&&&

最初はこれに気付かずに、jQueryの$.getJSON()が働かないことに首をかしげました。
この「&&&」って何なんでしょう?

JSONパースできずにエラー
$.getJSON(url, opts, function(data){
  ...
});

APIのレスポンスを、正規表現で末尾のゴミを除去してからJSON.parse()するように修正しました。

末尾のゴミを除去してからJSONパース
$.get(url, opts, function(raw){
  // json末尾の"]"以外の文字を削除
  var json = raw.replace(/[^\]]+$/, "");
  var data = JSON.parse(json);
  ...
}, "text");

注意

本extensionではPrime Videoホーム画面のマウスホバー時の作品概要を得るAPIを利用しています。
元々のREADMEにも書かれていますが、ここで利用しているAPIでは公開期限が必ず提供されるわけでは無いようです。
公開期限が表示されていない作品はいつ期限切れを迎えるかは分かりませんのでお早めに。

逆に言えば、期限表示されている作品は安心して視聴を先延ばしできる、と考えることもできるかもしれませんが、いずれにしても過信は禁物です。

最後に

プライムビデオで「砂の器 デジタルリマスター版」を見ました。
加藤嘉の熱演にやられました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Payjpでクレジットカード登録と削除機能を実装する(Rails)

実装する機能と前提条件

今回、Payjp(Pay.jp)を利用して入力フォームを直接ページ内に設置します。
実装するものとしては下記のとおりです。

  • 自作のカード情報入力フォーム
  • カード情報とユーザーの紐づけ
  • ユーザーとカードの登録と削除
  • 商品の購入次回の記事参照

イメージとしてはこんな感じの入力フォームです。
image.png

もしSDKを使う場合は別記事をご覧いただくかPAY.JP API 利用ガイド | PAY.JPを触るとイメージをつかみやすいと思います。

バージョン情報
・ruby 2.3.1
・Rails 5.0.7

前提条件
・deviseが導入済みでログインができている(current_userを使うため)
・hamlでの記載(gem 'haml-rails')

1.Payjpのアカウントを作成しよう

Payjpのサイトでアカウントを作成します。

2.APIを確認しよう

ダッシュボードのAPIより確認ができます。
今回はテストモードでの実装なので、テスト秘密鍵テスト公開鍵を使用します。
スクリーンショット 2019-04-14 16.31.04.png

3.payjpのgemを設置しよう

下記をgemfileに記載しbandle installを実施します。

gem 'payjp'

4.payjp.jsを読み込めるようにしよう

下記の通りに追記します。

app/views/layouts/application.html.haml
%html
  %head
    %meta{:content => "text/html; charset=UTF-8", "http-equiv" => "Content-Type"}/
    %title payjptest
    %script{src: "https://js.pay.jp/", type: "text/javascript"}
    -# このscriptを記載
    = csrf_meta_tags
    = stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track': 'reload'
    = javascript_include_tag 'application', 'data-turbolinks-track': 'reload'
  %body
    = yield

5.データベースを作成しよう

下記内容でpayjpのデータを保管するデータベースを作成します。nullはお好みで。
尚、紐づけは以下のとおりです。
- user_id -> Userテーブルのid
- customer_id -> payjpの顧客id
- card_id -> payjpのデフォルトカードid
(デフォルトカードidはトークンとは違います。ここの理解で結構時間をとってしまいました…)

db/migrate/20190400000000_create_cards.rb
class CreateCards < ActiveRecord::Migration[5.0]
  def change
    create_table :cards do |t|
      t.integer :user_id, null: false
      t.string :customer_id, null: false
      t.string :card_id, null: false
      t.timestamps
    end
  end
end

ちなみにカード情報そのものを保存することは禁止されていますので、
payjpに保管されている情報を顧客idやカードidで呼び出すことで情報取得や支払いなどに対応します。
カード情報非通過化対応のお願い

6.コントローラーを作成しよう

下記内容でコントローラーを作成します。
ENV["PAYJP_PRIVATE_KEY"]は環境変数でテスト秘密鍵を設定し読み込みます。
私はdotenvを利用しています。

app/controllers/card_controller.rb
class CardController < ApplicationController

  require "payjp"

  def new
    card = Card.where(user_id: current_user.id)
    redirect_to action: "show" if card.exists?
  end

  def pay #payjpとCardのデータベース作成を実施します。
    Payjp.api_key = ENV["PAYJP_PRIVATE_KEY"]
    if params['payjp-token'].blank?
      redirect_to action: "new"
    else
      customer = Payjp::Customer.create(
      description: '登録テスト', #なくてもOK
      email: current_user.email, #なくてもOK
      card: params['payjp-token'],
      metadata: {user_id: current_user.id}
      ) #念の為metadataにuser_idを入れましたがなくてもOK
      @card = Card.new(user_id: current_user.id, customer_id: customer.id, card_id: customer.default_card)
      if @card.save
        redirect_to action: "show"
      else
        redirect_to action: "pay"
      end
    end
  end

  def delete #PayjpとCardデータベースを削除します
    card = Card.where(user_id: current_user.id).first
    if card.blank?
    else
      Payjp.api_key = ENV["PAYJP_PRIVATE_KEY"]
      customer = Payjp::Customer.retrieve(card.customer_id)
      customer.delete
      card.delete
    end
      redirect_to action: "new"
  end

  def show #Cardのデータpayjpに送り情報を取り出します
    card = Card.where(user_id: current_user.id).first
    if card.blank?
      redirect_to action: "new" 
    else
      Payjp.api_key = ENV["PAYJP_PRIVATE_KEY"]
      customer = Payjp::Customer.retrieve(card.customer_id)
      @default_card_information = customer.cards.retrieve(card.card_id)
    end
  end
end

7.カードの登録画面を作成しよう

今回は登録画面と確認兼削除画面の2つを作成します。デザインはアレンジしてください。

登録画面

app/view/card/new.html.haml
= form_tag(card_pay_path, method: :post, id: 'charge-form',  name: "inputForm") do
  %label カード番号
  = text_field_tag "number", "", class: "number", placeholder: "半角数字のみ" ,maxlength: "16", type: "text", id: "card_number"
  %br
  %label 有効期限
  %select#exp_month{name: "exp_month", type: "text"}
    %option{value: ""} --
    %option{value: "1"}01
    %option{value: "2"}02
    %option{value: "3"}03
    %option{value: "4"}04
    %option{value: "5"}05
    %option{value: "6"}06
    %option{value: "7"}07
    %option{value: "8"}08
    %option{value: "9"}09
    %option{value: "10"}10
    %option{value: "11"}11
    %option{value: "12"}12
  %span 月/
  %select#exp_year{name: "exp_year", type: "text"}
    %option{value: ""} --
    %option{value: "2019"}19
    %option{value: "2020"}20
    %option{value: "2021"}21
    %option{value: "2022"}22
    %option{value: "2023"}23
    %option{value: "2024"}24
    %option{value: "2025"}25
    %option{value: "2026"}26
    %option{value: "2027"}27
    %option{value: "2028"}28
    %option{value: "2029"}29
  %span%br
  %label セキュリティコード
  = text_field_tag "cvc", "", class: "cvc", placeholder: "カード背面3~4桁の番号", maxlength: "4", id: "cvc"
  #card_token
  = submit_tag "追加する", id: "token_submit"

image.png

最初、= sectionを使っていたのですがうまく行かなかったため
%optionが連続発生し駄長なコードになっているのはご了承ください…

確認兼削除画面

app/view/card/show.html.haml
%label 登録クレジットカード情報
%br
= "**** **** **** " + @default_card_information.last4
%br
- exp_month = @default_card_information.exp_month.to_s
- exp_year = @default_card_information.exp_year.to_s.slice(2,3)
= exp_month + " / " + exp_year
= form_tag(card_delete_path, method: :post, id: 'charge-form',  name: "inputForm") do
  %input{ type: "hidden", name: "card_id", value: "" }
  %button 削除する

image.png

8.Payjpにデータを送りトークンを取得しよう

提供されているpay.jpのサンプルを参照し一部アレンジしております。

app/assets/javascripts/payjp.js
document.addEventListener(
  "DOMContentLoaded", e => {
    Payjp.setPublicKey("pk_test_79ae2d2743199a76f3ebbbbb"); //ここに公開鍵を直書き
    let btn = document.getElementById("token_submit"); //IDがtoken_submitの場合に取得されます
    btn.addEventListener("click", e => { //ボタンが押されたときに作動します
      e.preventDefault(); //ボタンを一旦無効化します
      let card = {
        number: document.getElementById("card_number").value,
        cvc: document.getElementById("cvc").value,
        exp_month: document.getElementById("exp_month").value,
        exp_year: document.getElementById("exp_year").value
      }; //入力されたデータを取得します。
      Payjp.createToken(card, (status, respnse) => {
        if (status === 200) { //成功した場合
          $("#card_number").removeAttr("name");
          $("#cvc").removeAttr("name");
          $("#exp_month").removeAttr("name");
          $("#exp_year").removeAttr("name"); //データを自サーバにpostしないように削除
          $("#card_token").append(
            $('<input type="hidden" name="payjp-token">').val(response.id)
          ); //取得したトークンを送信できる状態にします
          document.inputForm.submit();
          alert("登録が完了しました"); //確認用
        } else {
          alert("カード情報が正しくありません。"); //確認用
        }
      });
    });
  },
  false
);

9.ルートを作成しよう

今回はshow,pay,new,deleteの4つのメゾットがあるので下記の通り追記します。
一部はresoursesで設定してもよいかと思います。

config/routes.rb
  post 'card/new', to: 'card#new'
  post 'card/show', to: 'card#show'
  post 'card/pay', to: 'card#pay'
  post 'card/delete', to: 'card#delete'

10.カードを登録してみよう

テストカードはこちらです。
それ以外を打ち込んだ場合はトークンが発行できずはねられます。
image.png

以上でカード登録から削除まで一通り実装できました。
次回はこれを使って商品支払いを実装します。

参考

https://pay.jp/docs/api/?ruby

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Express風の簡単WebSocketフレームワーク、Hexnutのドキュメントを読んで試してみた

注意

WebSocketを趣味で勉強しているものです、間違いなどありましたらご指摘お願いします :bow:

Hexnut とは

google翻訳
?Hexnutはミドルウェアベースの、Express / Koa風のWebソケット用フレームワーク

Installing HexNut

インストール

npm i hexnut

Creating a server

サーバー起動

server.js
const HexNut = require('hexnut');
const app = new HexNut({ port: 8080 });

app.start();
node server.js
index.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>

<script>
{
    const ws = new WebSocket("ws://127.0.0.1:8080");
    ws.addEventListener("open", e => console.log("socket open"));
}
</script>
</body>
</html>

これでWebSocketサーバーが簡単に起動でき、接続も確認できました

image.png

Examples

https://github.com/francisrstokes/hexnut#examples

簡単な例

server.js
const Hexnut = require('hexnut');
const app = new Hexnut({ port: 8080 });

app.onerror = async (err, ctx) => {
    ctx.send(`Error! ${err.message}`);
};

app.use(ctx => {
    if (ctx.isConnection) {
        // 接続時に送信
        ctx.state = { count: 0 };
        return ctx.send('Hello, and welcome to the socket!');
    }

    ctx.state.count++;
    ctx.send(`Message No. ${ctx.state.count}: ${ctx.message}`); // ctx.messageで受信したメッセージを取得できる
});

app.start();
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>

<input type="button" value="send">
<script>
{
    const ws = new WebSocket("ws://127.0.0.1:8080");
    ws.addEventListener("message", e => console.log(e.data));
    document.querySelector("[type=button]").addEventListener("click", e => {
        ws.send(new Date().toLocaleTimeString());
    });
}
</script>
</body>
</html>

簡単にメッセージの送受信をすることができました

LoeU6FxC9i.gif

ctx.send(data)だと送信してきたクライアントにしかメッセージを送らないため、
接続しているクライアント全てに送るときはctx.sendToAll(data)とするようです

JSONを自動的に解析する

server.js
const Hexnut = require('hexnut');
const bodyParser = require('hexnut-bodyparser');
const app = new Hexnut({ port: 8080 });

app.use(bodyParser.json());

app.use(ctx => {
    if (ctx.isConnection) {
        ctx.state = { count: 0 };
        return ctx.send('Hello, and welcome to the socket!');
    }

    const { type, date, msg } = ctx.message;
    if (type) {
        ctx.state.count++;
        ctx.send(`Message No. ${ctx.state.count}: ${type} ${date} ${msg}`);
    } else {
        ctx.send(`Invalid message format, expecting JSON with a "type" key`);
    }
});

app.start();
npm i hexnut-bodyparser
node server.js
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>

<input type="button" value="send">
<script>
{
    const ws = new WebSocket("ws://127.0.0.1:8080");
    ws.addEventListener("message", e => console.log(e.data));
    document.querySelector("[type=button]").addEventListener("click", e => {
        ws.send(JSON.stringify({
            type: "send",
            date: new Date().toLocaleTimeString(),
            msg: "\nabc\nあいう\n???",
        }));
    });
}
</script>
</body>
</html>

JSONを自動でオブジェクトに解析してくれたのが確認できました

OcRE3mIUDf.gif

hexnut-bodyparser を使わなくても、 JSON.parse(ctx.message); とすれば同じように扱ってくれます

  const Hexnut = require('hexnut');
  const bodyParser = require('hexnut-bodyparser');
  const app = new Hexnut({ port: 8080 });

- app.use(bodyParser.json());

  app.use(ctx => {
      if (ctx.isConnection) {
          ctx.state = { count: 0 };
          return ctx.send('Hello, and welcome to the socket!');
      }

-     const { type, date, msg } = ctx.message;
+     const { type, date, msg } = JSON.parse(ctx.message);
      if (type) {
          ctx.state.count++;
          ctx.send(`Message No. ${ctx.state.count}: ${type} ${date} ${msg}`);
      } else {
          ctx.send(`Invalid message format, expecting JSON with a "type" key`);
      } 
  });

  app.start();

メッセージの種類判別

server.js
const Hexnut = require('hexnut');
const handle = require('hexnut-handle');
const app = new Hexnut({ port: 8080 });

// 接続時に呼ばれるイベント
app.use(handle.connect(ctx => {
  ctx.count = 0;
}));

// msg === 'incCount' に一致する値が送られ的場合に実行される
app.use(handle.matchMessage(
  msg => msg === 'incCount',
  ctx => ctx.count++
));

// msg === 'decCount' に一致する値が送られ的場合に実行される
app.use(handle.matchMessage(
  msg => msg === 'decCount',
  ctx => ctx.count--
));

// msg === 'getCount' に一致する値が送られ的場合に実行される
app.use(handle.matchMessage(
  msg => msg === 'getCount',
  ctx => ctx.send(ctx.count)
));

// ↑に一致しなかった値が送られてきた場合にはここが実行される
app.use(handle.message(ctx => {
    ctx.send(`Any other kind of message will go here.`);
}));

app.start();
npm i hexnut-handle
node server.js
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>

<input type="button" value="incCount">
<input type="button" value="decCount">
<input type="button" value="getCount">
<script>
{
    const ws = new WebSocket("ws://127.0.0.1:8080");
    ws.addEventListener("message", e => console.log(e.data));

    Array.from(document.querySelectorAll("[type=button]")).forEach(elm => {
        elm.addEventListener("click", e => ws.send(e.target.value));
    });
}
</script>
</body>
</html>

このように、送信するメッセージによって処理を分岐させることができました

masO8QdxPi.gif

Sequencing Interactions

メッセージを受信する順番を保証できる機能のようです

const Hexnut = require('hexnut');
const bodyParser = require('hexnut-bodyparser');
const sequence = require('hexnut-sequence');
const app = new Hexnut({ port: 8080 });

app.use(bodyParser.json());

// ユーザーが接続したときに発生
app.use(sequence.onConnect(function* (ctx) {
  ctx.send(`Welcome, ${ctx.ip}`);
  const name = yield sequence.getMessage(); // 接続時、次のメッセージが来るまで待つ、メッセージを受信したらイテレータを次に進める
  ctx.clientName = name;
  return;
}));

app.use(sequence.interruptible(function* (ctx) {
    // clientNameが存在すればイテレータを次に進める
    yield sequence.assert(() => 'clientName' in ctx);

    // type == greeting を受信すると、イテレータを次に進める
    const greeting = yield sequence.matchMessage(msg => msg.type === 'greeting');

    // type == time を受信すると、イテレータを次に進める
    const time = yield sequence.matchMessage(msg => msg.type === 'time');

    // ↑の条件が全てが受信されると、↓の送信の部分に処理が到達する
    return ctx
        .send(`${greeting.value} ${ctx.clientName}です、 今の時間は${time.value}です`);
}));

app.start();
npm i hexnut-sequence
node server.js
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>

<input type="button" value="name" data-msg="tarou">
<input type="button" value="greeting" data-msg='{"type":"greeting","value":"おはようございます"}'>
<input type="button" value="time" data-msg='{"type":"time","value":"10:00"}'>
<script>
{
    const ws = new WebSocket("ws://127.0.0.1:8080");
    ws.addEventListener("message", e => console.log(e.data));

    Array.from(document.querySelectorAll("[type=button]")).forEach(elm => {
        elm.addEventListener("click", e => ws.send(e.target.dataset.msg));
    });}
</script>
</body>
</html>

sYeyKuFdPr.gif

Middlewareを自作

server.js
const Hexnut = require('hexnut');
const app = new Hexnut({ port: 8080 });

const myMiddleware = async (ctx, next) => {
    ctx.send("send myMiddleware message.");
    return await next();
}
app.use(myMiddleware);

app.use(ctx => {
    ctx.send("send message."); 
});

app.start();
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>

<input type="button" value="send">
<script>
{
    const ws = new WebSocket("ws://127.0.0.1:8080");
    ws.addEventListener("message", e => console.log(e.data));
    document.querySelector("[type=button]").addEventListener("click", e => {
        ws.send(new Date().toLocaleTimeString());
    });
}
</script>
</body>
</html>

ミドルウェアで、クライアントに送信前の処理がうまくいきました

ptYMjdxDJg.gif

HexNut Docs - API

https://github.com/francisrstokes/hexnut/blob/master/docs/api.md

引用部分は全て↑のページのgoogle翻訳です:bow:

HexNut Server

new HexNut(wsConfig)

新しいHexNutインスタンスを作成します

const Hexnut = require('hexnut');
const app = new Hexnut({ port: 8080 });

app.use(middleware)

HexNutインスタンスにミドルウェア機能を追加します

app.start()

HexNut Websocketサーバーを起動します。

const Hexnut = require('hexnut');
const app = new Hexnut({ port: 8080 });
app.start(); // サーバー起動

app.stop()

HexNut Websocketサーバーを停止します。

const Hexnut = require('hexnut');
const app = new Hexnut({ port: 8080 });
app.start();
app.stop(); // サーバー起動して即終了する

ctx

HexNut接続を表すコンテキストオブジェクト

ctx.message

受信したメッセージ

app.use(ctx => {
    if (ctx.isConnection) {
        // 接続時はnullになる
        console.log(ctx.message);
    } else {
        // 受信したメッセージ
        console.log(ctx.message);
    }
});

ctx.isConnection

app.use(ctx => {
    // 新しく接続されたのであればtrue、そうでなければfalse
    console.log(ctx.isConnection);
});

ctx.isMessage

app.use(ctx => {
    console.log(ctx.isMessage);
});
html
<script>
const ws = new WebSocket("ws://127.0.0.1:8080"); // 接続時は、ctx.isMessageはfalse

ws.send("hoge"); // メッセージ送信した時は、ctx.isMessageはtrue
</script>

ctx.isClosing

接続終了したらtrueになる

app.use(ctx => {
    console.log(ctx.isClosing);
});
html
<script>
const ws = new WebSocket("ws://127.0.0.1:8080"); // 接続時は、ctx.isClosingはfalse

ws.close(); // 接続終了した時は、ctx.isClosingはtrue
</script>

ctx.requestHeaders

この接続を開始したhttp(s)ヘッダを表すオブジェクト

app.use(ctx => {
    console.log(ctx.requestHeaders);
});
コンソール
{ host: '127.0.0.1:8080',
  connection: 'Upgrade',
  pragma: 'no-cache',
  'cache-control': 'no-cache',
  upgrade: 'websocket',
  origin: 'file://',
  'sec-websocket-version': '13',
  'user-agent':
   'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.103 Safari/537.36',
  'accept-encoding': 'gzip, deflate, br',
  'accept-language': 'ja-JP,ja;q=0.9,en-US;q=0.8,en;q=0.7',
  'sec-websocket-key': '+oE1f1UWmZ3dJ0k8Et23XQ==',
  'sec-websocket-extensions': 'permessage-deflate; client_max_window_bits' }

ctx.ip

クライアントのIPアドレス

app.use(ctx => {
    console.log(ctx.ip); // ::ffff:127.0.0.1
});

ctx.path

接続を開始した文字列URLパス

app.use(ctx => {
    console.log(ctx.path);
});
html
<script>
    const ws = new WebSocket("ws://127.0.0.1:8080/path"); // ctx.pathは 「/path」となる
</script>

ctx.method

接続を開始するためのHTTPメソッド

app.use(ctx => {
    console.log(ctx.method);
});
html
<script>
    const ws = new WebSocket("ws://127.0.0.1:8080"); // ctx.methodは 「GET」となる
</script>

ctx.send(data)

クライアントにメッセージを送る

app.use(ctx => {
    ctx.send("send message.");
});
html
<script>
    const ws = new WebSocket("ws://127.0.0.1:8080");
    ws.addEventListener("message", e => console.log(e.data));
</script>

image.png

ctx.sendToAll(data)

接続されているすべてのクライアントにメッセージを送信する

app.use(ctx => {
    ctx.sendToAll(new Date().toLocaleTimeString());
});

接続しているクライアントにメッセージを送ることができました

54pZQA4Rx7.gif

ctx.app

HexNutアプリへの参照

const Hexnut = require('hexnut');
const app = new Hexnut({ port: 8080 });

app.use(ctx => {
    console.log(app === ctx.app); // true
});
app.start();

確認バージョン

  • node v10.14.1
  • npm 6.9.0
  • hexnut 0.1.1
  • hexnut-bodyparser 0.1.0
  • hexnut-handle 1.0.0
  • hexnut-sequence 0.1.0

最後まで読んでいただいてありがとうございましたm(_ _)m

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Web Componentsをフロントエンドフレームワークっぽく使ってみた

注意

私はjavascriptを趣味で勉強しているものです。これが正しいやり方というわけではなく、私が趣味で勉強しながらこうやったら面白いんじゃないかと考えながら作ったものなので注意してください。
間違いなどありましたら、ご指摘お願いいたします :bow:

タイトルの フロントエンドフレームワークっぽく という部分ですが、コンポーネント単位で再利用可能イベント設定ができる、という感じのものを自分では思いながら作成しました。

この記事内のコードは全てchromeの最新で確認しました(version 73.0.3683.103)

参考

簡単なカウンターを作成してみました

コードはこちらにも用意しました

8uh9gDvkAh.gif

src/index.js
import AppElement from "./AppElement";

document.body.appendChild(AppElement);
src/AppElement.js
import "./CountElement";
import "./BtnElement";

window.customElements.define("app-element", class extends HTMLElement {
    constructor() {
        super();
        const shadowRoot = this.attachShadow({ mode: "open" });

        shadowRoot.innerHTML = `      
            <div class="container">
                <count-element></count-element>
                <btn-element class="blue">up</btn-element>
                <btn-element class="green">down</btn-element>
            </div>

            <style>        
            .container {
                width: 200px;
                text-align: center;
                font-family: Verdana, sans-serif;
            }
            </style>
        `;
    }

    connectedCallback() {
        this.addEventListener("count", e => {
            this.shadowRoot.querySelector("count-element")
              .count(e.detail.countType);
        });
    }
});

export default document.createElement("app-element");
src/BtnElement.js
window.customElements.define("btn-element", class extends HTMLElement {
    constructor() {
        super();
        const shadowRoot = this.attachShadow({mode: "open"});
        shadowRoot.innerHTML = `
            <button type="button" class="${this.className}" >
                <slot></slot>
            </button>

            <style>
            button {
                border-radius: 5px;
                cursor: pointer;
                font-size: 15px;
                height: 35px;
                width: 70px;
                color: #fff;
                transition: opacity .15s;
            }
            button:hover {
                opacity: .8;
            }

            button.blue {
                background-color: #007bff;
                border-color: #007bff;
            }
            button.green {
                background-color: #28a745;
                border-color: #28a745;
            }
            </style>
        `;
    }

    connectedCallback() {
        this.addEventListener("click", e => {
            this.count(e.target.textContent);
        });
    }

    count(countType) {
        this.dispatchEvent(new CustomEvent("count", { 
            bubbles: true,
            composed: true,
            detail: { countType },
        }));
    }
});
src/CountElement.js
window.customElements.define("count-element", class extends HTMLElement {
    constructor() {
        super();
        const shadowRoot = this.attachShadow({mode: "open"});
        shadowRoot.innerHTML = `
            <p class="count">0</p>

            <style>
            .count {
                font-size: 25px;
                margin: 10px;
            }
            </style>
        `;
    }

    count(countTyep) {
        const p = this.shadowRoot.querySelector("p");
        p.textContent = Number(p.textContent) + (countTyep === "up" ? 1 : -1);
    }
});
npm i webpack webpack-cli
npx webpack --mode development #./dist/main.jsが出力

私のやり方が悪いかもしれないですが、vueやreactを使ったほうが綺麗になりそうだと思いました。


ここからは、自分が参考サイトなどから調べたりしたことを書いていきます

Custom Elements

ユーザー定義のHTML要素

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
    <my-element></my-element>

<script>
class MyElement extends HTMLElement {
        constructor() {
            super();
            const shadowRoot = this.attachShadow({mode: "open"});
            shadowRoot.innerHTML = `      
                <div>hogefuga</div>    
                <style>        
                div {          
                    color: green;
                }      
                </style>      
            `;
        }
}
window.customElements.define("my-element", MyElement);
</script>
</body>
</html>

window.customElements.define("my-element", MyElement); と定義したので、htmlで<my-element></my-element>と書けば使うことができる ※タグ名の-は既存のタグ名と被らないようにつけたほうが良いようです

Screen Shot 2019-04-14 at 18.11.53.png


↑の例ではHTMLに直接<my-element></my-element>と書きましたが、javascriptからタグを生成することもできます

<body>
    <script>
    window.customElements.define("my-element", class extends HTMLElement {
        constructor() {
            super();
            const shadowRoot = this.attachShadow({mode: "open"});
            shadowRoot.innerHTML = "<div>hogefuga</div>";
        }
    });

    // customElements.get()を使う
    const el = customElements.get("my-element");
    const myElement = new el();
    document.body.appendChild(myElement);

    // または、
    document.body.appendChild(document.createElement("my-element"));
    </script>
</body>

Screen Shot 2019-04-14 at 18.26.52.png

ライフサイクルメソッド

  • connectedCallback
  • disconnectedCallback
  • attributeChangedCallback
  • adoptedCallback

connectedCallback()

<body>
    <my-element></my-element>
    <my-element></my-element>
    <my-element></my-element>

    <script>
    window.customElements.define("my-element", class extends HTMLElement {
        constructor() {
            super();
        }

        // 要素がDOMツリーに挿入されたときに呼び出される
        connectedCallback() {
            console.log(this) // ↑で3回my-elementタグを書いているので、3回実行される
        }
    });
    </script>
</body>

Screen Shot 2019-04-14 at 18.22.37.png

attributeChangedCallback()

observedAttributes()の配列に存在する属性が変更された時に実行される

<body>
    <my-element id="elm" foo="1" bar="a"></my-element>

    <script>
    window.customElements.define("my-element", class extends HTMLElement {
        constructor() {
            super();
            const shadowRoot = this.attachShadow({mode: "open"});
            shadowRoot.innerHTML = "<div>hogefuga</div>";
        }

        static get observedAttributes() {
            return ["foo", "bar"];
        }

        attributeChangedCallback(attr, oldVal, newVal) {
            switch(attr) {
                case "foo":
                    console.log("foo", oldVal, newVal);
                    return;
                case "bar":
                    console.log("bar", oldVal, newVal);
                    return;
            }
        }
    });
    </script>
</body>

63mnVXJaQp.gif

ちなみに実行順序は以下のようです

constructor -> attributeChangedCallback -> connectedCallback

APIを設定

関数を定義

<body>
    <my-element></my-element>

    <script>
    window.customElements.define("my-element", class extends HTMLElement {
        constructor() {
            super();
            const shadowRoot = this.attachShadow({mode: "open"});
            shadowRoot.innerHTML = "<div>hogefuga</div>";
        }

        doSomething() {
            console.log("do something");
        }
    });
    const element = document.querySelector('my-element');
    element.doSomething();
    </script>
</body>

Screen Shot 2019-04-14 at 18.58.06.png

プロパティの定義

setterを使うことで簡単にできる、disabledプロパティを設定してみました

<body>
    <my-element id="elm"></my-element>

    <script>
    window.customElements.define("my-element", class extends HTMLElement {
        constructor() {
            super();
            const shadowRoot = this.attachShadow({mode: "open"});
            shadowRoot.innerHTML = `
                <h1>hogefuga</h1>
                <input type="button" value="button">
            `;
        }

        set disabled(isDisabled) {
            if(isDisabled) {
                this.setAttribute("disabled", "");
                this.shadowRoot.querySelector("h1").style.opacity = 0.5;
                this.shadowRoot.querySelector("input").setAttribute("disabled", "");
            } else {
                this.removeAttribute("disabled");
                this.shadowRoot.querySelector("h1").style.opacity = 1;
                this.shadowRoot.querySelector("input").removeAttribute("disabled");
            }
        }

        get disabled() {
            return this.hasAttribute("disabled");
        }
    });
    </script>
</body>

mti0zNrttZ.gif

Shadow DOM

Shadow DOMを使うとHTMLとCSSはコンポーネント内にカプセル化され、他のスタイルシートの影響を受けず、Shadow DOM内のスタイルが外部に影響することもなくなります。

video要素とかでもShadow DOMが使われています。

<video controls></video>

chromeのデフォルトではデベロッパーツールでShadow DOMをみることはできません

image.png

なので、settingから設定を変えましょう

9ybbItHjXw.gif

デベロッパーツールでもShadow DOMが確認できるようになりました


shadow DOM の使い方

引用

以下に shadow DOM における用語を定義します。

  • Shadow host: shadow DOM が追加された、通常の DOM ノード
  • Shadow tree: shadow DOM の中にある DOM ツリー
  • Shadow boundary: shadow DOM と通常の DOM の境界
  • Shadow root: shadow ツリーの根ノード

attachShadow() でshadow root を任意の要素に追加することができます。
このメソッドではパラメータとして mode オプションを open または closed の値で取ります。

open の場合は shadow DOM の内部にメインページに書かれた javascript からアクセスできます

<div id="hoge"></div>
<div id="fuga"></div>
<video controls id="video"></video>

<script>
const hogeShadowRoot = hoge.attachShadow({mode: 'open'});
hogeShadowRoot.innerHTML = "<span>hogehoge</span>";
console.log(hoge.shadowRoot); // mode: 'open'と設定したので、shadow root を確認できる

const fugaShadowRoot = fuga.attachShadow({mode: 'closed'});
fugaShadowRoot.innerHTML = "<span>fugafuga</span>";
console.log(fuga.shadowRoot); // mode: 'closed'と設定したので、nullとなる

console.log(video.shadowRoot); // videoタグは mode:closed になっているので、null
</script>

Screen Shot 2019-04-14 at 19.45.54.png

:hostセレクタを使用してコンポーネント自体のスタイルを設定

shadow DOM が追加された、通常の DOMにスタイルを設定できる

:host {
  display: block;
}

disabled属性が付いている時だけスタイルを当てることもできました

<body>
<my-element></my-element>
<my-element disabled></my-element>

<script>
window.customElements.define("my-element", class extends HTMLElement {
    constructor() {
        super();
        const shadowRoot = this.attachShadow({mode: "open"});
        shadowRoot.innerHTML = `
            <h1>hogehoge</h1>
            <h2>fugafuga</h2>
            <h3>piyopiyo</h3>

            <style>
            /* disabled属性が付いている時だけstyleを適用 */
            :host([disabled]) {
                display: block;
                opacity: 0.5;
            }
            </style>
        `;
    }
});
</script>
</body>

Screen Shot 2019-04-14 at 20.06.12.png

Shadow DOMの外側からstyleを適用する

css変数を使うことでうまくいくようです

<my-element></my-element>
<style>
my-element {
    --background-color: tomato;
}
</style>

<script>
window.customElements.define("my-element", class extends HTMLElement {
    constructor() {
        super();
        const shadowRoot = this.attachShadow({mode: "open"});
        shadowRoot.innerHTML = `
            <h1>hogehoge</h1>
            <style>
            h1 {
                background-color: var(--background-color);
            }
            </style>
        `;
    }
});
</script>

Screen Shot 2019-04-14 at 20.33.01.png

slots

<slot>はShadow DOM内でプレースホルダーのように機能してくれます

ユーザー提供のマークアップとShadow DOMを合体させて新しいDOMツリーを作ってくれます

<body>
<image-gallery>
  <img src="http://placehold.jp/100x100.png">
  <img src="http://placehold.jp/100x100.png">
</image-gallery>

<script>
window.customElements.define("image-gallery", class extends HTMLElement {
    constructor() {
        super();
        const shadowRoot = this.attachShadow({mode: "open"});
        shadowRoot.innerHTML = `
            <h1>hoge gallery</h1>
            <slot></slot>
        `;
    }
});
</script>
</body>

↑の例ですと、<image-gallery>の子要素が、Shadow DOMの<slot></slot>の部分に適用されます

Screen Shot 2019-04-14 at 20.52.28.png


名前付きで<slot>を指定することもできます

<body>
<image-gallery>
  <img src="http://placehold.jp/100x100.png" slot="fuga">
  <img src="http://placehold.jp/100x100.png" slot="fuga">
  <img src="http://placehold.jp/150x150.png" slot="piyo">
</image-gallery>

<script>
window.customElements.define("image-gallery", class extends HTMLElement {
    constructor() {
        super();
        const shadowRoot = this.attachShadow({mode: "open"});
        shadowRoot.innerHTML = `
            <h1>hoge gallery</h1>
            <div class="skyblue">
                <slot name="fuga"></slot>
            </div>
            <div class="limegreen">
                <slot name="piyo"></slot>
            </div>

            <style>
            .skyblue { background: skyblue; }
            .limegreen { background: limegreen; }
            </style>
        `;
    }
});
</script>
</body>

Screen Shot 2019-04-14 at 20.56.50.png


slotchange イベントでslotが変更された時に、内容を取得することもできるようです

<body>
<image-gallery>
    aaa
    <img src="http://placehold.jp/100x100.png">
    bbb
    <img src="http://placehold.jp/100x100.png">
    ccc
</image-gallery>

<script>
window.customElements.define("image-gallery", class extends HTMLElement {
    constructor() {
        super();
        const shadowRoot = this.attachShadow({mode: "open"});
        shadowRoot.innerHTML = `
            <h1>hoge gallery</h1>
            <slot></slot>
        `;
    }
});

const slot = document.querySelector("image-gallery").shadowRoot.querySelector("slot");
slot.addEventListener('slotchange', e => {
  const changedSlot = e.target;
  console.log(changedSlot.assignedNodes());
});
</script>
</body>

Screen Shot 2019-04-14 at 21.03.37.png

template element

templateタグのなかのマークアップは非表示になります。
javascriptでDOMをcloneして表示したいDOMにappendChild()などすることで表示することができます。

なので画面上の表示を大幅に変更したい場合などに、複数templateタグを用意して切り替えたりすると便利です。

<body>
<my-element></my-element>

<script>
window.customElements.define("my-element", class extends HTMLElement {
    constructor() {
        super();
        const shadowRoot = this.attachShadow({mode: 'open'});

        this.shadowRoot.innerHTML = `
            <template id="view1">
                <p>This is view 1</p>
            </template>

            <template id="view1">
                <p>This is view 1</p>
            </template>

            <div id="container">
                <p>This is the container</p>
            </div>
        `;
    }

    connectedCallback() {
        const content = this.shadowRoot.querySelector('#view1').content.cloneNode(true);
        this.shadowRoot.querySelector('#container').appendChild(content);
    }
});
</script>
</body>

Screen Shot 2019-04-14 at 21.28.51.png

イベント設定

普通にイベント設定して動作してくれます

<body>
<btn-element></btn-element>

<script>
window.customElements.define("btn-element", class extends HTMLElement {
    constructor() {
        super();
        const shadowRoot = this.attachShadow({mode: "open"});
        shadowRoot.innerHTML = "<button>click</button>";
    }

    connectedCallback() {
        this.addEventListener("click", e => {
            console.log("clickされました")
        });
    }
});
</script>
</body>

Screen Shot 2019-04-14 at 21.42.24.png

Custom Elementからイベント発火して、別のCustom Elementのメソッドを実行する

<body>
<my-app>
    <run-element></run-element>
    <alert-element></alert-element>
</my-app>

<script>
window.customElements.define("run-element", class extends HTMLElement {
    constructor() {
        super();
        const shadowRoot = this.attachShadow({mode: "open"});
        shadowRoot.innerHTML = "<button>click</button>";
    }

    connectedCallback() {
        this.addEventListener("click", e => {
            this.handleAlert();
        });
    }

    handleAlert() {
        // カスタムイベントでev-alertイベントを設定し、発火させる
        // bubbles: trueとすることでBubblingさせる
        // ※Bubblingとは、ある要素でイベントが発生すると、まずその要素でハンドラが実行され、次にその親で、次にその上の先祖ですべての処理が実行される動作
        this.dispatchEvent(new Event("ev-alert", { bubbles: true }));
    }
});

window.customElements.define("alert-element", class extends HTMLElement {
    constructor() {
        super();
        const shadowRoot = this.attachShadow({mode: "open"});
        shadowRoot.innerHTML = "<h1>hogehoge</h1>";
    }

    alert() {
        alert("文字背景を黄色にします");
        this.shadowRoot.querySelector("h1").style.background = "yellow";
    }
});

window.customElements.define("my-app", class extends HTMLElement {
    constructor() {
        super();
        const shadowRoot = this.attachShadow({mode: "open"});
        shadowRoot.innerHTML = "<slot></slot>";
    }

    connectedCallback() {
        // run-elementでev-alertイベントが発火すると、親のタグなので受け取れる
        this.addEventListener("ev-alert", e => {
            // 別のCustom Elementのメソッドを実行することができる
            this.querySelector("alert-element").alert();
        });
    }
});
</script>
</body>

wQB5CXfS2W.gif


イベントに値を持たせたい場合

CustomEventを使い、detailを指定するとe.detailで受け取れる

        // イベント発火
        this.dispatchEvent(new CustomEvent("hoge", {
            bubbles: true, 
            detail: { msg: "hello" },
        }));

        // イベントリスナー
        this.addEventListener("hoge", e => {
            console.log(e.detail); // { msg: hello }
        });

最後まで読んでいただいてありがとうございましたm(_ _)m

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む