20190302のJavaScriptに関する記事は30件です。

Canvasから動画を生成するWebサービスを試作してみた。

はじめに

3年ぶりの投稿です。
最近、Codepenなどを使ってCanvasで遊んでいるのですが、Canvasの画像・動きを動画として残せないかと考えるようになりました。
調べてみると、CCapture.jsなど、Canvasから動画を生成するライブラリがあるようなので、Herokuを使って、簡単なCanvas動画生成サービスを作ってみました。

作ったもの

成果物   :CapCanvas
ソースコード:Github

使い方

htmlヘッダーに以下の行を追加します。なお、idはcanvasのid、fpsはフレームレート、timeは録画時間(ms)を指定します。
'C'キーを推すとCanvasのキャプチャが開始され、録画終了後、動画(webm)がダウンロードされます。
<script type="text/javascript" src="https://capcanvas.herokuapp.com/capcanvas/id=app&fps=60&time=10000"></script>

実装

■JavaScript
Jsは、CCaputureのサンプルコードを参考に、以下のように実装しました。なお、$URL$ID$FPS$TIMEはサーバー側で文字列に置換します。

CapCanvas.js
window.onload = function(){
var script = document.createElement("script");
script.type = "text/javascript";
script.src = "$URL";
document.body.appendChild(script);
};
window.addEventListener('keydown', event => {
if(event.key=='c'||event.key=='C'){
let canvas=document.getElementById("$ID");
startCapture(canvas,$FPS,$TIME);
}
});
let startCapture=function(canvas,fps,time){
let cap=new CCapture({format:'webm',framerate: fps,verbose: true});
let render=function(){
requestAnimationFrame(render);
cap.capture( canvas );
};
requestAnimationFrame(render);
cap.start();
setTimeout(function(){
cap.stop();
cap.save();
},time);
};

■サーバー
サーバー側はリクエストパラメータを解析し、CapCanvas.jsを書き換えて送信します。
個人的にお気に入りのSpark Frameworkで作成しています。

Main.java
package net.termat.webapp.capcanvas;

import static spark.Spark.get;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

import spark.ModelAndView;
import spark.Spark;
import spark.template.mustache.MustacheTemplateEngine;

public class Main {
    private static String code;

    public static void main(String[] args) {
        Spark.staticFileLocation("/public");
        code=getJs();
        Optional<String> optionalPort = Optional.ofNullable(System.getenv("PORT"));
        optionalPort.ifPresent(p -> {
            int port = Integer.parseInt(p);
            Spark.port(port);
        });

        get("/", (request, response) -> {
            Map<String, Object> model = new HashMap<>();
             return new ModelAndView(model, "index.mustache");
        }, new MustacheTemplateEngine());

        get("/capcanvas/:param", (request, response) -> {
            try{
                String url="https://capcanvas.herokuapp.com/js/CCapture.all.min.js";
                Map<String,String> map=paramMap(request.params("param"));
                String id=map.get("id");
                String fps=map.get("fps");
                String time=map.get("time");
                response.status(200);
                String ret=new String(code);
                ret=ret.replace("$URL", url);
                ret=ret.replace("$ID", id);
                ret=ret.replace("$FPS", fps);
                ret=ret.replace("$TIME", time);
                return ret;
            }catch(Exception e){
                e.printStackTrace();
                response.status(400);
                response.type("application/json");
                return "";
            }
        });

    }

    private static String getJs(){
        try{
            URL url=Main.class.getResource("CapCanvas.js");
            BufferedReader br=new BufferedReader(new InputStreamReader(url.openStream()));
            StringBuffer buf=new StringBuffer();
            String line=null;
            while((line=br.readLine())!=null){
                buf.append(line);
            }
            return buf.toString();
        }catch(Exception e){
            return "";
        }
    }

    private static Map<String,String> paramMap(String param) throws Exception{
        Map<String,String> ret=new HashMap<String,String>();
        String[] p=param.split("&");
        for(int i=0;i<p.length;i++){
            String[] k=p[i].split("=");
            if(k.length<2)continue;
            ret.put(k[0], k[1]);
        }
        return ret;
    }
}

デプロイ

herokuへは、heroku-maven-pluginを使用してデプロイしました。
Port番号の解決に気が付かず、実際に動作させるまで2~3時間かかってしまいました。

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

firebaseとvueでソーシャルログインを実装

タイトルままですが、firebaseとvueでソーシャルログインを実装しました。

ソーシャルログイン

各ルーター側の.vueに影響がないようにしました。

firebaseなのでここでホスティングもしてます。

実際にログインもできます。

https://authentication-sample-001.firebaseapp.com/

プロジェクトはこちら

https://github.com/qoAopx/firebase_vue_authentication

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

FirebaseとVueでソーシャルログインを実装

タイトルままですが、FirebaseとVueでソーシャルログインを実装しました。

ソーシャルログイン

各ルーター側の.vueに影響がないようにしました。

Firebaseなのでここでホスティングもしてます。

実際にログインもできます。

https://authentication-sample-001.firebaseapp.com/

プロジェクトはこちら

https://github.com/qoAopx/firebase_vue_authentication

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

JavaScriptで今風の配列やオブジェクトのコピーのやり方

JSを勉強し始めた人向けの記事です。

今っぽい感じの配列やオブジェクトのコピーのやり方を記載します。(今=2019年です)

配列のコピー

配列
const array1 = [ 1, 2, 3 ]
const arrayCopy = [ ...array1 ]

スプレッド構文を使うことでconcatとかsliceとかforとかを使わずに書けます。

オブジェクトのコピー

オブジェクト
const obj1 = { a: 1, b: 2 }
const objCopy = { ...obj1 }

こちらもスプレッド構文を使ってコピーできます。Object.assignを使わずに書けます。

注意点

どちらもShallow copyです。

Deep copyではありません!

まとめ

自分はこの方法を知らなかったので、初めてこの書き方を知ったときはシンプルさに驚きました。

スプレッド構文はコピー以外にもいろいろと便利なので知らなかった場合は詳細を調べてみることをオススメします!

参考

スプレッド構文 - MDN

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

VBAでInternetExplore上のJavaScriptを無理やり動かすよ!

みんな大好きInternetExplore11のJavaScriptを、みんな大好きなExcelのVBAから動かします。

なにが嬉しいの?

・現在、表示されているIEのJavaScriptの保持している変数の中身を出力できるよ!
・無理やりJavaScriptで例外を発生させたときの挙動を確かめられるよ!
・サーバー側のソースコードを書き換えずにローカル環境でJavaScriptの関数をテスト用に置き換えられるよ!

ExcelVBAなので、手足を縛ってプログラムを開発するのが性癖なマゾい会社でも使えるよ!やったぜ!

環境

InternetExploere
Windows10
Office16 Excel 32bit

なお、64bitのExcelを使っている箇所はlongでなく、longPtrとかにしないと動かないと思うけど、ぶっちゃけ64bitマシン支給するようなブルジョワジーの組織だったらVisualStudio使わせてもらってC#で書いた方がいいと思います。

サンプル

IEAuto.bas
 Option Explicit

' Microsoft HTML Object Libraryを参照設定すること

Private Declare Function EnumWindows Lib "user32" (ByVal lpEnumFunc As Long, ByVal lParam As Long) As Long
Private Declare Function EnumChildWindows Lib "user32" (ByVal hWndParent As Long, ByVal lpEnumFunc As Long, ByVal lParam As Long) As Long

Private Declare Function GetClassName Lib "user32" Alias "GetClassNameA" (ByVal hwnd As Long, ByVal lpClassName As String, ByVal nMaxCount As Long) As Long
Private Declare Function RegisterWindowMessage Lib "user32" Alias "RegisterWindowMessageA" (ByVal lpString As String) As Long
Private Declare Function SendMessageTimeout Lib "user32" Alias "SendMessageTimeoutA" (ByVal hwnd As Long, ByVal msg As Long, ByVal wParam As Long, ByVal lParam As Long, ByVal fuFlags As Long, ByVal uTimeout As Long, lpdwResult As Long) As Long
Private Declare Function ObjectFromLresult Lib "oleacc" (ByVal lResult As Long, riid As Any, ByVal wParam As Long, ppvObject As Object) As Long

Private mIEWndList As Collection
Private Type UUID
  Data1 As Long
  Data2 As Integer
  Data3 As Integer
  Data4(0 To 7) As Byte
End Type

' MSHTML.IHTMLDocumentのキャッシュを検索する
Public Sub RefreshBrowserCache()
    Set mIEWndList = New Collection

    Call EnumWindows(AddressOf EnumIEWndProc, 0)
End Sub
' 指定の名称を含むタイトルのブラウザを検索する
Public Function FindBrowser(ByVal title As String) As MSHTML.IHTMLDocument2
    Dim r As MSHTML.IHTMLDocument2

    If mIEWndList Is Nothing Then
        Call RefreshBrowserCache
    End If

    Set r = FindBrowserInCache(title)
    If Not r Is Nothing Then
        Set FindBrowser = r
        Exit Function
    End If

    Call RefreshBrowserCache
    Set FindBrowser = FindBrowserInCache(title)

End Function

' キャッシュの中から指定の名称を含むタイトルのブラウザを検索する
Private Function FindBrowserInCache(ByVal title As String) As MSHTML.IHTMLDocument2
    Dim d As MSHTML.IHTMLDocument2
    For Each d In mIEWndList
        If InStr(d.title, title) > 0 Then
            Set FindBrowserInCache = d
            Exit Function
        End If
    Next

End Function



' コールバック関数
Private Function EnumIEWndProc(ByVal hwnd As Long, lParam As Long) As Boolean
    Const FRAME_NAME = "IEFrame"
    Const FRAME_NAME_EDGE = "TabWindowClass" 'Edgeの場合のクラス名

    Dim strClassName As String * 128
    Dim lngRet As Long

    strClassName = ""
    Call GetClassName(hwnd, strClassName, 128)
    Dim c As String
    c = Left(strClassName, Len(Trim(strClassName)) - 1)
    If c = FRAME_NAME Or c = FRAME_NAME_EDGE Then
        EnumChildWindows hwnd, AddressOf EnumIEServerWndProc, 0
    End If
    'Debug.Print "[" & Trim(strClassName) & "_]"
    EnumIEWndProc = True
End Function

' コールバック関数
Private Function EnumIEServerWndProc(ByVal hwnd As Long, ByVal lParam As Object) As Long
    Const SERVER_NAME = "Internet Explorer_Server"
    Dim strClassName As String * 128
    Dim lngRet As Long
    Dim doc As MSHTML.IHTMLDocument2

    strClassName = ""
    Call GetClassName(hwnd, strClassName, Len(strClassName))
    If Left(strClassName, Len(Trim(strClassName)) - 1) = SERVER_NAME Then
        Set doc = GetHTMLDocument(hwnd)
        If Not doc Is Nothing Then
            mIEWndList.Add doc
        End If
    End If
    EnumIEServerWndProc = 1
End Function

Private Function GetHTMLDocument(ByVal hwnd As Long) As MSHTML.IHTMLDocument2
    Dim doc As MSHTML.IHTMLDocument2
    Dim lMsg As Long
    Dim lRet As Long
    Dim hr As Long
    Dim IID_IHTMLDocument As UUID

    With IID_IHTMLDocument
        .Data1 = &H626FC520
        .Data2 = &HA41E
        .Data3 = &H11CF
        .Data4(0) = &HA7
        .Data4(1) = &H31
        .Data4(2) = &H0
        .Data4(3) = &HA0
        .Data4(4) = &HC9
        .Data4(5) = &H8
        .Data4(6) = &H26
        .Data4(7) = &H37
    End With
    lMsg = RegisterWindowMessage("WM_HTML_GETOBJECT")
    If lMsg <> 0 Then
        SendMessageTimeout hwnd, lMsg, 0, 0, &H2, 1000, lRet
        If lRet <> 0 Then
            If ObjectFromLresult(lRet, IID_IHTMLDocument, 0, doc) = 0 Then
                Set GetHTMLDocument = doc
            End If
        End If
    End If
End Function

サンプル
Public Sub test()
    Dim b As MSHTML.IHTMLDocument3
    Dim w As Object

    Set b = IEAuto.FindBrowser("Google")
    If b Is Nothing Then
        Debug.Print "見つからない"
        Exit Sub
    End If

    Dim r As Variant
    Dim elems As MSHTML.IHTMLElementCollection
    Dim elem As MSHTML.IHTMLElement
    Set elems = b.getElementsByName("btnK")
    Debug.Print elems.Item(0).getAttribute("value")

    ' 以下のような感じでJavaScriptにアクセスできるがEdgeの場合はアクセス違反になる
    ' 管理者権限も無駄
    Dim testId As Variant
    testId = Timer
    b.parentWindow.execScript ("function x() { result = 5+5+4; console.log('TEST'); var d = document.createElement('div'); d.id = 'test_elem" & testId & "'; d.innerHTML  = result; document.getElementsByTagName('body').item(0).appendChild(d); }; x();")

    Set elem = b.getElementById("test_elem" & testId)
    Debug.Print elem.innerHTML

End Sub

解説

IEのウィンドウハンドルからMSHTML.IHTMLDocumentを取得してあとはよくあるIEの自動操作をしています。
IHTMLDocumentに変換できるウィンドウのクラス名は「Internet Explorer_Server」です。
IE11の場合は「IEFrame」クラスの子ウィンドウになっています。
Edgeの場合は「TabWindowClass」クラスの子ウィンドウになっています。

プログラムとしては「IEAuto.FindBrowser("Google")」でタイトルにGoogleを含むものを検索してIHTMLDoucmentを取得します。
あとは、exeScriptでJavaScriptをたたくことが可能ですが、関数の仕様として戻り値を取得することはできません。

そのため、JavaScriptから値をVBAに返す場合は、テスト用の要素に書き込むといいでしょう。

なお、すくなくとも当方の環境だとexeScriptはEdgeだとアクセス違反になりました。

参考

Cant create HTML document from Hwnd using C#
https://stackoverflow.com/questions/20873885/cant-create-html-document-from-hwnd-using-c-sharp

【2017年1月版】Microsoft Edgeを操作するVBAマクロ(DOM編)
https://www.ka-net.org/blog/?p=7921

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

[ITP 2.0] Google広告でクロスドメインのコンバージョン計測をするために

はじめに:

・GTMコンバージョンリンカー
・gtagでset linker
いずれかの方法で、クロスドメインの設定が必要になります。

※Googleアナリティクスの_gacというクッキーも使える。この投稿では一旦省略します。

① Googleタグマネージャーで設定する場合

CV linker.PNG
・ドメイン間のリンクの有効可にチェック
・カンマつなぎで、複数ドメインを登録する
・ドメインまたぎの瞬間がフォームの送信(カートに入れた瞬間にドメインが変わる場合など)の時には、「装飾フォーム: true」にする必要あり
 (これが結構罠。デフォルトtrueではダメなんか...)
・トリガーはページビュー: All pages

② gtagでset linkerの設定する場合

・以下のコードでドメイン間のリンクを有効にしてあげます。
・が、かならず gtag('js', new Date());よりも早く呼ぶ必要があります。
(テストしましたが、クッキー引き継げなかったです。これも罠。)

...
gtag('set', 'linker', {
  'domains': ['exampleA.com', 'exampleB.com'],
  'decorate_forms': true // trueにしておきましょう。
});
gtag('js', new Date());
...

よって、最終的な形はこうなります。

<script async src="https://www.googletagmanager.com/gtag/js?id=AW-XXXXXXXX"></script>
<script>
  window.dataLayer = window.dataLayer || [];
  function gtag(){dataLayer.push(arguments);}  
  gtag('set', 'linker', {
    'domains': ['exampleA.com', 'exampleB.com'],
    'decorate_forms': true
  });   
  gtag('js', new Date());
  gtag('config', 'AW-XXXXXXXX');
</script>

③ 本当にクロスドメインでクッキーを引き継げているのか?の検証

Google広告にて、コンバージョン計測に活用できるファーストパーティークッキーは2つです。
・_gcl_aw (Google広告のクッキー)
・_gac  (※アナリティクスのクッキーです。アカウントのリンクが必要)

よって、「自動タグ設定にて付与された_gcl_aw / _gacを、ドメインAからドメインBに引き継げているかどうか」の検証が必要ですね。

④ 検証方法

手順1. gclid付与(広告クリックの状態を疑似的に再現)

「?gclid=xxxxx」とドメインAにてURLに付与

手順2. クッキー確認

デベロッパーツールにて、「_gcl_aw」というクッキーが生成されたか確認
※_gcl_aw=xxxxx(gclidで付与した値) という形式のクッキーです
クッキーの生成.PNG

手順3. ドメインを移動して、URLのクエリを確認

ユーザーと同じ導線でドメインBへ。URLに「?_gl=1*xxxxxx*_gcl_aw*xxxxx」というクエリが付くのを確認してください!(この中にクッキーの情報が入っているイメージ)

手順4. ドメインBでクッキーを確認

ドメインBにて、手順2でチェックしたクッキーと同じものが引き継がれていれば、ドメインBへのファーストパーティークッキーの引き継ぎ完了です。

プラグイン SUGOI!Cookies: gclid tester for Google Ads

SUGOI!Cookies: gclid tester for Google Ads
440-280.png

・手動での検証方法が面倒なので、プラグインを作ってみました。 気になる方は試してみてください。
・あくまで個人の趣味で開発してますが、バグがあれば直すので一報いただけると嬉しいです。随時改善中です。

⑤ 補足: コンバージョンデータ送信時のHTTP通信を見てみる 

 - もし本当の本当に「コンバージョン計測時にファーストパーティークッキーのデータを送信できているのかどうか」気になるのであれば、コンバージョン計測時のHTTPリクエストを確認してみましょう。

手順1 コンバージョンのHTTPリクエストを探す

デベロッパーツール=> Network
=> www.googleadservices.com/pagead/conversion/ で検索

手順2 ファーストパーティークッキーが送られているか確認

この通信にて、「_gcl_aw / _gac」が送信されていれば安心です。

クッキー.PNG

まとめ

・Google広告のコンバージョン計測で使えるファーストパーティーは「_gcl_aw」「_gac」の2つ。今回は「_gcl_aw」をクロスドメインで引き継ぐための話でした。

・クッキーを渡す側(ドメインA)、もらう側(ドメインB)のいずれもで、
 「GTM」 OR 「gtag」でのクロスドメイン設定の実装が必要になります。

以上です!

参考資料:
コンバージョン トラッキングの実装を検証する

Google アナリティクスによるウェブサイトでの Cookie の使用

Google 広告の自動タグ設定が正常に機能するか確認する

ドメインをまたぐカスタマー ジャーニーを測定する

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

[ITP 2.0] Google広告でクロスドメインのコンバージョン計測をするためのTODO

はじめに:

やりたいこと

Safariブラウザでもコンバージョン計測がしたい。
 日本のiPhone普及率を考えると、ここは無視できないところ。

そのために

・ファーストパーティークッキー(後述しますが、_gcl_aw/_gac)を生成する。
・広告をクリックした直後のランディングページと、コンバージョン計測したいページのドメインが異なる場合(クロスドメイン)、特別な方法でクッキーを渡してあげる必要がある。

しかし

・Googleの資料を見ても、クロスドメイントラッキングの話がなかなかない。
(これ解決しないと、ITP対応できないでしょうに!)
Google 広告のコンバージョンをトラッキングする方法
こんなん読んでも、ファーストパーティー、どころか、クッキーの「ク」の字もない。

そこで、

参考資料:
 ・コンバージョン トラッキングの実装を検証する
 ・Google アナリティクスによるウェブサイトでの Cookie の使用
 ・Google 広告の自動タグ設定が正常に機能するか確認する
 ・ドメインをまたぐカスタマー ジャーニーを測定する

ここらの資料をすべてまとめてみました。

ドメインをまたいでクッキーの引継ぎをするために

・GTMコンバージョンリンカー
・gtagでset linker
いずれかの方法で、クロスドメインの設定が必要になります。

① Googleタグマネージャーで設定する場合

CV linker.PNG
・ドメイン間のリンクの有効可にチェック
・カンマつなぎで、複数ドメインを登録する
・ドメインまたぎの瞬間がフォームの送信(カートに入れた瞬間にドメインが変わる場合など)の時には、「装飾フォーム: true」にする必要あり
 (これが結構罠。デフォルトtrueではダメなんか...)
・トリガーはページビュー: All pages

② gtagでset linkerの設定する場合

・以下のコードでドメイン間のリンクを有効にしてあげます。
・が、かならず gtag('js', new Date());よりも早く呼ぶ必要があります。
(テストしましたが、クッキー引き継げなかったです。これも罠。)

...
gtag('set', 'linker', {
  'domains': ['exampleA.com', 'exampleB.com'],
  'decorate_forms': true // trueにしておきましょう。
});
gtag('js', new Date());
...

よって、最終的な形はこうなります。

<script async src="https://www.googletagmanager.com/gtag/js?id=AW-XXXXXXXX"></script>
<script>
  window.dataLayer = window.dataLayer || [];
  function gtag(){dataLayer.push(arguments);}  
  gtag('set', 'linker', {
    'domains': ['exampleA.com', 'exampleB.com'],
    'decorate_forms': true
  });   
  gtag('js', new Date());
  gtag('config', 'AW-XXXXXXXX');
</script>

③ 本当にクロスドメインでクッキーを引き継げているのか?の検証

Google広告にて、コンバージョン計測に活用できるファーストパーティークッキーは2つです。
・_gcl_aw (Google広告のクッキー)
・_gac  (※アナリティクスのクッキーです。アカウントのリンクが必要)

よって、「自動タグ設定にて付与された_gcl_aw / _gacを、ドメインAからドメインBに引き継げているかどうか」の検証が必要ですね。

④ 検証方法

手順1. gclid付与(広告クリックの状態を疑似的に再現)

「?gclid=xxxxx」とドメインAにてURLに付与

手順2. クッキー確認

デベロッパーツールにて、「_gcl_aw」というクッキーが生成されたか確認
※_gcl_aw=xxxxx(gclidで付与した値) という形式のクッキーです
クッキーの生成.PNG

手順3. ドメインを移動して、URLのクエリを確認

ユーザーと同じ導線でドメインBへ。URLに「?_gl=1*xxxxxx*_gcl_aw*xxxxx」というクエリが付くのを確認してください!(この中にクッキーの情報が入っているイメージ)

手順4. ドメインBでクッキーを確認

ドメインBにて、手順2でチェックしたクッキーと同じものが引き継がれていれば、ドメインBへのファーストパーティークッキーの引き継ぎ完了です。

プラグイン SUGOI!Cookies: gclid tester for Google Ads

SUGOI!Cookies: gclid tester for Google Ads
440-280.png

・手動での検証方法が面倒なので、プラグインを作ってみました。 気になる方は試してみてください。
・あくまで個人の趣味で開発してますが、バグがあれば直すので一報いただけると嬉しいです。随時改善中です。

⑤ 補足: コンバージョンデータ送信時のHTTP通信を見てみる 

 - もし本当の本当に「コンバージョン計測時にファーストパーティークッキーのデータを送信できているのかどうか」気になるのであれば、コンバージョン計測時のHTTPリクエストを確認してみましょう。

手順1 コンバージョンのHTTPリクエストを探す

デベロッパーツール=> Network
=> www.googleadservices.com/pagead/conversion/ で検索

手順2 ファーストパーティークッキーが送られているか確認

この通信にて、「_gcl_aw / _gac」が送信されていれば安心です。

クッキー.PNG

※Googleアナリティクスのクロスドメイン設定ができている、かつ、アカウントのリンクができている場合、_gacというクッキーも活用できます。

まとめ

・Google広告のコンバージョン計測で使えるファーストパーティーは「_gcl_aw」「_gac」の2つ。今回は「_gcl_aw」をクロスドメインで引き継ぐための話でした。

・クッキーを渡す側(ドメインA)、もらう側(ドメインB)のいずれもで、
 「GTM」 OR 「gtag」でのクロスドメイン設定の実装が必要になります。

以上です!

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

【個人開発】何が何でも魚拓を見つけるChrome拡張を作った

デモ

↓2019/03/02
キャプチャ4.PNG
↓1998/11/11
キャプチャ5.PNG

特徴

  • archive.is, archive.org, gyo.tc, googleのキャッシュ、と計四か所から魚拓のURLを探します!
  • vue.js製(やってみたかった)

なぜ作ったか

ハテブで魚拓コメがトップだったりして、需要を感じた。
後普通に自分が使いたかった。
そして、Qiitaユーザーの皆さんも必要性を感じてくださっている事と思います^^。

どう作ったか

Chorome拡張はHTML+CSS+javascriptの基本セットで作れます。
ファイル構成は以下のようになりました。

  • manifest.json(どの拡張でも必須)
  • popup.js
  • popup.html
  • popup.css
  • jquery
  • vue.js
  • icons(フォルダ。配下にicon16.png, icon48.png, icon128.png)

詳しく見ていきましょう。
manifest.jsonは、Chrome拡張のまとめ役みたいな感じ。

manifest.json
{
    "name": "FindGyotaku in Console",
    "version": "0.0.1",
    "manifest_version": 2,//2で固定する
    "permissions": ["tabs", "http://*/", "http://*/"],//tabsはアクティブなタブのURLを取得するため、http://*/,https://*/はChrome拡張からすべてのサイトへのアクセスを許可するために必要
    "description": "Show all Gyotaku",
    "icons": {
        "16": "icons/icon16.png",
        "48": "icons/icon48.png",
        "128": "icons/icon16.png"
    },
    "browser_action": {
        "default_popup": "popup.html"//ここでpopupのhtmlを指定
    }
}

そして、popup.jsが今回の大事ポイントです。

popup.js
let vue;
//https://gyo.tc/から
function findGyotakuByUrl(url) {
    return new Promise(resolve => {
        console.log("findGyotakuByUrl Started");
        $.get("https://gyo.tc/" + url, null,
            function (data) {
                console.log(data);
                let gyotakuList_ = new DOMParser()
                    .parseFromString(data, "text/html")
                    .querySelectorAll("[id^=fish]");
                resolve(gyotakuList_)
            },
            "html"
        );
    })
}
//https://archive.is/から
function findInternetArchiveByUrl(url) {
    return new Promise(resolve => {
        $.get(`https://archive.is/${url}`, function (data) {
            let rowsList =
                new DOMParser()
                    .parseFromString(data, "text/html")
                    .querySelectorAll("[id^=row]");
            let archivesList = [];
            rowsList.forEach(row => {
                archivesList.push(
                    Array.from(
                        row.querySelectorAll("[style^=text\\-decoration\\:none]")
                    )
                )
            });
            resolve(archivesList.flat());
            console.log(`flatten archivesList is ${archivesList.flat()}`);
        })
    })
}
//googleのwebcacheから
function findGoogleCache(url) {
    return new Promise(resolve => {
        $.get(`http://webcache.googleusercontent.com/search?q=cache:${url}`, function () {
            resolve(`http://webcache.googleusercontent.com/search?q=cache:${url}`);
        })
    })
}
//http://web.archive.orgから
function findWaybackMachineLatest(url) {
    return new Promise(resolve => {
        //limit=-1で最新
        $.get(`http://web.archive.org/cdx/search/cdx?limit=-1&fl=timestamp&url=${url}`, function (timestamp) {
            resolve(`https://web.archive.org/web/${timestamp}/${url}`);
            console.log(`waybackMachine response(latest) is ${timestamp}`);
        })
    })
}
//http://web.archive.orgから
function findWaybackMachineOldest(url) {
    return new Promise(resolve => {
        //limit=1で最古
        $.get(`http://web.archive.org/cdx/search/cdx?limit=1&fl=timestamp&url=${url}`, function (timestamp) {
            resolve(`https://web.archive.org/web/${timestamp}/${url}`);
            console.log(`waybackMachine response(latest) is ${timestamp}`);
        })
    })
}

console.log("popup.jsLoaded");

chrome.tabs.query({
    active: true,
    currentWindow: true
}, function (tabsArray) {
    console.log(`currentUrl is ${tabsArray[0].url}`);

    findGyotakuByUrl(tabsArray[0].url).then((gyotakuList) => {
        console.log(gyotakuList);
        gyotakuList.forEach(element => {
            vue.items.push(element)
        });
        vue.isLoadingGyotaku = false
    }).catch((err) => {
        console.log(err);
        vue.isLoadingGyotaku = false
    });
    findInternetArchiveByUrl(tabsArray[0].url).then((archivesList) => {
        console.log(archivesList);
        archivesList.forEach(element => {
            vue.items.push(element)
        });
        vue.isLoadingArchives = false
    }).catch((err) => {
        console.log(err);
        vue.isLoadingArchives = false
    });
    findGoogleCache(tabsArray[0].url).then((googleCacheUrl) => {
        console.log({
            href: googleCacheUrl
        });
        vue.items.push({
            href: googleCacheUrl
        });
        vue.isLoadingGoogleCache = false
    }).catch((err) => {
        console.log(err)
    });
    findWaybackMachineLatest(tabsArray[0].url).then((waybackMachineUrl) => {
        console.log({
            href: waybackMachineUrl
        });
        vue.items.push({
            href: waybackMachineUrl
        });
        vue.isLoadingWaybackMachine = false
    }).catch((err) => {
        console.log(err)
    });
    findWaybackMachineOldest(tabsArray[0].url).then((waybackMachineUrl) => {
        console.log({
            href: waybackMachineUrl
        });
        vue.items.push({
            href: waybackMachineUrl
        });
        vue.isLoadingWaybackMachine = false

    }).catch((err) => {
        console.log(err)
    });

});

$(function () {
    vue = new Vue({
        el: '#example-1',
        data: {
            items: [],
            isLoadingGyotaku: true,
            isLoadingArchives: true,
            isLoadingGoogleCache: true,
            isLoadingWaybackMachine: true,
        }
    })

});

分割して解説します。

function findGyotakuByUrl(url) {
    return new Promise(resolve => {
        $.get("https://gyo.tc/" + url, null,
            function (
                let gyotakuList_ = new DOMParser()
                    .parseFromString(data, "text/html")
                    .querySelectorAll("[id^=fish]");
                resolve(gyotakuList_)
            },
            "html"
        );
    })
}

↑要するにスクレイピングですね。javascriptではquerySelectorが便利です。
https://gyo.tc/ 以下にURLを投げると魚拓一覧が返るので、開発者ツールから頑張って解析しました。

function findInternetArchiveByUrl(url) {
    return new Promise(resolve => {
        $.get(`https://archive.is/${url}`, function (data) {
            let rowsList =
                new DOMParser()
                    .parseFromString(data, "text/html")
                    .querySelectorAll("[id^=row]");
            let archivesList = [];
            rowsList.forEach(row => {
                archivesList.push(
                    Array.from(
                        row.querySelectorAll("[style^=text\\-decoration\\:none]")
                    )
                )
            });
            resolve(archivesList.flat());
        })
    })
}

↑こちらもhttps://archive.is/ 以下にURLを投げると魚拓一覧が返るので、こちらも開発者ツールから頑張って解析しました。
Array.flat()はMDNで分かるように実験的なヤツで、配列を好きな次元に均せる関数です。デフォルトで一次元なのでそのまま使用しました。

function findGoogleCache(url) {
    return new Promise(resolve => {
        $.get(`http://webcache.googleusercontent.com/search?q=cache:${url}`, function () {
            resolve(`http://webcache.googleusercontent.com/search?q=cache:${url}`);
        })
    })
}

↑言うことなし。思ったより簡単。

function findWaybackMachineLatest(url) {
    return new Promise(resolve => {
        //limit=-1で最新
        $.get(`http://web.archive.org/cdx/search/cdx?limit=-1&fl=timestamp&url=${url}`, function (timestamp) {
            resolve(`https://web.archive.org/web/${timestamp}/${url}`);
        })
    })
}

function findWaybackMachineOldest(url) {
    return new Promise(resolve => {
        //limit=1で最古
        $.get(`http://web.archive.org/cdx/search/cdx?limit=1&fl=timestamp&url=${url}`, function (timestamp) {
            resolve(`https://web.archive.org/web/${timestamp}/${url}`);
            console.log(`waybackMachine response(latest) is ${timestamp}`);
        })
    })
}

↑apiが有って助かりました。
WaybackMachineのアーカイブは膨大なので最古&最新のみ取得しています。
limit=1で最古、limit=-1で最新を指定、fl=timestampでレスポンスをtimestampのみに限定します。
WaybackMachineはtimestampとURLを指定すると(珍しく)個別ページのURLが分かるので、timestampだけで十分なのです。

$(function () {
    vue = new Vue({
        el: '#example-1',
        data: {
            items: [],//以下デフォルト値を指定しています
            isLoadingGyotaku: true,
            isLoadingArchives: true,
            isLoadingGoogleCache: true,
            isLoadingWaybackMachine: true,
        }
    })

});
index.html
<p v-if="isLoadingGyotaku">
    魚拓から読み込んでいます
</p>

↑出ましたvue.js!読み込み中のメッセージを出しています。
vue.jsを使うと、HTMLとjs間でバインディングができます。
例えば、

HTMLのv-if属性にisLoadingGyotakuを指定

js側でvueインスタンス作成(vueのdata属性に、isLoadinGyotakuをメンバにしたオブジェクトを付けたもの)

vue.isLoadingGyotakuにアクセスできるように&
vue.isLoadingGyotakuがv-if="isLoadingGyotaku"とバインドされる

って感じ。
つまり、isLoadingGyotaku=trueならpタグが見えて、falseなら見えなくなる!
これは素晴らしいですね。本当に素晴らしい。
ほかにもいろいろな属性が有ります。だいたいv-が付く。

<ul>
    <li v-for="item in items">
        <a href="{{ item.href }}" target='_newtab'>
            {{ item.href }}
        </a>
    </li>
</ul>

↑v-forとか。見つかったURLを表示するために使っています。
{{}}はテンプレートリテラルみたいに使えます。
↓ラスト

chrome.tabs.query({
    active: true,
    currentWindow: true
}, function (tabsArray) {
    console.log(`currentUrl is ${tabsArray[0].url}`);

    findGyotakuByUrl(tabsArray[0].url).then((gyotakuList) => {
        console.log(gyotakuList);
        gyotakuList.forEach(element => {
            vue.items.push(element)

        });
        vue.isLoadingGyotaku = false
    }).catch((err) => {
        console.log(err);
        vue.isLoadingGyotaku = false
    });
    findInternetArchiveByUrl(tabsArray[0].url).then((archivesList) => {
        console.log(archivesList);
        archivesList.forEach(element => {
            vue.items.push(element)
        });
        vue.isLoadingArchives = false
    }).catch((err) => {
        console.log(err);
        vue.isLoadingArchives = false
    });
    findGoogleCache(tabsArray[0].url).then((googleCacheUrl) => {
        console.log({
            href: googleCacheUrl
        });
        vue.items.push({
            href: googleCacheUrl
        });
        vue.isLoadingGoogleCache = false
    }).catch((err) => {
        console.log(err);
        vue.isLoadingGoogleCache = false
    });
    findWaybackMachineLatest(tabsArray[0].url).then((waybackMachineUrl) => {
        console.log({
            href: waybackMachineUrl
        });
        vue.items.push({
            href: waybackMachineUrl
        });
        vue.isLoadingWaybackMachine = false
    }).catch((err) => {
        console.log(err)
        vue.isLoadingWaybackMachine = false
    });
    findWaybackMachineOldest(tabsArray[0].url).then((waybackMachineUrl) => {
        console.log({
            href: waybackMachineUrl
        });
        vue.items.push({
            href: waybackMachineUrl
        });
        vue.isLoadingWaybackMachine = false

    }).catch((err) => {
        console.log(err)
        vue.isLoadingWaybackMachine = false
    });

});

↑async/awaitで見やすくなるのは分かってるんだけどね……面倒なのでそのまま。
chrome.tabs.queryで今見てるタブのURLを取得後、非同期×5を回しています。
chrome.系はChrome拡張の時しか使えないapiで、ほかにも色々便利です。

はまったポイント

最初

let fuga = true
vue = new Vue({
        el: '#example-1',
        data: {
            isLoadingGyotaku: fuga,
        }
    })
fuga = false//バインドされてるはずなのに動かない!!!

って事が有って大変でしたね。本当は

vue = new Vue({
        el: '#example-1',
        data: {
            isLoadingGyotaku: true,
        }
    })
vue.isLoadingGyotaku = false//バインドされてて動く!!!

で良かったっていう。公式ドキュメントをちゃんと読みましょう。

後、動作確認にevent pagesを使ってしまって、popupのみで動くapiを動かせなかったりとか……
popupページで検証する方法を知らなかったとか……(左クリックでポップアップされたのを右クリ)

計何時間使ったでしょうかね。今はどうでも良いけど。

最後に

2020/04に大学生になる(浪人しなければ!)ので、良かったらインターンに誘ってください。
github:https://github.com/negitorogithub
gmail:bakeratta35@gmail.com

お読み頂きありがとうございました。 ~~旦⊂(・∀・ )

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

【備忘録】react-templatesについてまとめた

react-templates

install

https://www.npmjs.com/package/react-templates

npm i -g react-templates

gulp: https://www.npmjs.com/package/gulp-react-templates

npm i gulp-react-templates

gulp task code

サンプル

const gulp = require('gulp');
const rt = require('gulp-react-templates');
const rename = require("gulp-rename"); // 出力ファイル名を変更するため

const rt_path = './src/rt/*.rt';

gulp.task('rt', () => {
  return gulp.src(rt_path)
    .pipe(rt({ modules: 'es6' })) // 吐き出す際の出力フォーマットを選択
    .pipe(rename(name_info => {
      const new_basename = name_info.basename.replace('.rt', ''); // '.rt'を''に置換
      name_info.basename = new_basename;
    }))
    .pipe(gulp.dest('./src/templates'));
});

gulp.task("watch", () => {
  return gulp.watch(rt_path, gulp.series(['rt']));
});

Command line Interface

source: https://github.com/wix/react-templates/blob/gh-pages/docs/cli.md

先ほどの出力する際のoptionの例を以下に記載

// temp.rt
<h2>Hello React</h2>
// es6
// rt [file.rt|glob]* -m es6
import * as React from 'react';
import * as _ from 'lodash';
export default function () {
    return React.createElement('h2', {}, 'Hello React');
}
// commonJS
// rt [file.rt|glob]* -m commonJS
'use strict';
var React = require('react');
var _ = require('lodash');
module.exports = function () {
    return React.createElement('h2', {}, 'Hello React');
};
// none (default)
// rt [file.rt|glob]*
var tempRT = function () {
    return React.createElement('h2', {}, 'Hello React');
};

reactに組み込む

▼ こんなrtファイルを書いて

// temp.rt
<h2>Hello react-templates</h2>

▼ コンパイル(es6)してこんな感じにして

// temp.js
import * as React from 'react';
import * as _ from 'lodash';
export default function () {
    return React.createElement('h2', {}, 'Hello react-templates');
}

▼ importしてrenderのなかでreturnする

// App.js
import React, { Component } from 'react';
import Temp from './templates/temp.js';

class App extends Component {
  render() {
    return Temp();
  }
}

ReactDOM.render(<App />, document.getElementById('root'));

ブラウザに Hello react-templates が出力されればok

参考

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

TypeScript/JavaScriptの配列を一定の個数ごとに分割する

Ruby の each_slice、PHP の array_chunk みたいなやつです。
例えば [1, 2, 3, 4, 5, 6, 7] を3個ずつに分けて [[1, 2, 3], [4, 5, 6], [7]] を作りたい場合に使えます。

TypeScript

function chunk<T>(arr: T[], size: number) {
    return arr.reduce(
        (newarr, _, i) => (i % size === 0 ? [...newarr, arr.slice(i, i + size)] : newarr),
        [] as T[][]
    )
}

chunk([1, 2, 3, 4, 5, 6, 7], 3) // -> [[1, 2, 3], [4, 5, 6], [7]]

JavaScript

function chunk(arr, size) {
    return arr.reduce(
        (newarr, _, i) => (i % size === 0 ? [...newarr, arr.slice(i, i + size)] : newarr),
        []
    )
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

知識0でもプログラマーになれるたった1つの理由と3つの方法

理由

誰でも知識は0からスタート

方法

  1. 調べる
  2. 作る
  3. 発信する
  4. 1~3を継続する

※だいたいの職業において、上記が通じると思います

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

selectとoptionタグをjQueryを使って動的に追加する

デモ(動画)

Image from Gyazo

やりたいこと

上記のGIF通り、
カテゴリーが選択されたと同時に、親カテゴリーに紐づく子カテゴリーが出てくるようにしたい。
入れ子構造については以下を参照
【追記予定】awesome_nested_setを使った入れ子構造のDB設計

 実装概要

jQueryのajaxメソッドを使って、カテゴリーが選択されたら子カテゴリーがappendされるようにした。

 コード

routing

route.rb
Rails.application.routes.draw do
  root 'products#new'
  resources :products,  only: [:create] do
<!-- 今回はsearchアクションをajaxメソッドで叩きます-->
    collection do
      get 'search'
    end
  end
end

view

new.html.haml
= form_for @product do |f|
  = f.label :name
  = f.text_field :name, placeholder: "商品名を入力"

  #cat
    = f.select :category_id, Category.roots.map {|i| ["#{i.name}", i.id]}, { selected: @product.category_id, include_blank: true }, {id: "l_category"}

js

category.js
$(document).on('turbolinks:load', function() {

  // Mカテゴリーのselectを追加するHTML
  var cat_seach = $("#cat");

  function appendMselect() {
    var html =  
    `<select name="product[category_id]" id="m_category">
        <option value>---</option>
    </select>`
    cat_seach.append(html)
  }

  // Sカテゴリーのselectを追加するHTML
  function appendSselect() {
    var html =  
    `<select name="product[category_id]" id="s_category">
        <option value>---</option>
    </select>`
    cat_seach.append(html)
  }

  // Mカテゴリーのoptionを追加するHTML
  function appendMcat(m_cat) {
    $("#m_category").append(
      $("<option>")
        .val($(m_cat).attr('id'))
        .text($(m_cat).attr('name'))
    )
  }

  // Sカテゴリーのoptionを追加するHTML
  function appendScat(s_cat) {
    $("#s_category").append(
      $("<option>")
        .val($(s_cat).attr('id'))
        .text($(s_cat).attr('name'))
    )
  }

  // Lカテゴリーが選択された時のアクション
  $("#l_category").on('change', function() {
    l_cat = $(this).val()
    $("#m_category").remove()
    $("#s_category").remove()

    // ajaxでリクエストを送信
      $.ajax({
        type: "GET",
        url: "/products/search",
        data: {l_cat: l_cat},
        dataType: 'json'
      })
      // doneメソッドでappendする
      .done(function(m_cat) {
        appendMselect()
        m_cat.forEach(function(m_cat) {
          appendMcat(m_cat)
        })
      })
    })

// Mカテゴリーが選択された時のアクション
  $(document).on('change', "#m_category", function() {
    m_cat = $(this).val()
    $("#s_category").remove()

    $.ajax({
      type: "GET",
      url: "/products/search",
      data: {m_cat: m_cat},
      dataType: 'json'
    })
    .done(function(s_cat) {
      appendSselect()
      s_cat.forEach(function(s_cat) {
        appendScat(s_cat)
      })
    })
  })
})

controller

categories_controller.rb
<!--関係ないアクションは省略しています-->
  def search
    if params[:l_cat]
      @m_cat = Category.find(params[:l_cat]).children
    else
      @s_cat = Category.find(params[:m_cat]).children
    end
    respond_to do |format|
      format.html
      format.json
    end
  end

jbuilder

search.json.jbuilder
json.array! @m_cat do |m_cat|
  json.id   m_cat.id
  json.name m_cat.name
end

json.array! @s_cat do |s_cat|
  json.id   s_cat.id
  json.name s_cat.name
end

勉強になったところ

optionタグの追加の方法

optionの追加
$("#m_category").append(
      $("<option>")
        .val($(m_cat).attr('id'))
        .text($(m_cat).attr('name'))
    )
  }

こういう風に書くことで、selectタグのoptionが追加できることを知りました。
始めはselectを追加すればいいじゃん!と思ってましたが、配列形式で渡ってくるため、配列の中身分selectが増えてしまい、上手な方法が浮かばなかったので、こちらの方法にしました。

参考記事
・jQuery でセレクトボックスのプルダウン項目(option 要素)を追加/削除する方法
https://webllica.com/jquery-select-option-add-del/

まとめ

なんとか狙った機能は実装できましたが、MカテゴリーとSカテゴリーで二回書いているようなコードになっているので、リファクタリングしたいけどどうしていいのかわからん・・・

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

csv をパースする JavaScript

車輪の再発明です

CSV の仕様

RFC 4180

改行は CRLF でなければならないそうです。

実装例

function parseCSV(d) {
    let t = d.replace(/\r\n$/u, '')
    const r = [[]]
    let c = 0
    let l, m, n
    while ([l, m, n] = /^(|[^",](?:[^,\r]|\r[^,\n])*\r?|"(?:[^"]|"")*")(,|\r\n|$)/u.exec(t)) {
        if ('"' === m[0] && '"' === m[m.length - 1]) m = m.slice(1, -1)
        r[c].push(m.replace(/""/gu, '"'))
        if (!n) break
        if (',' !== n) r[++c] = []
        t = t.substring(l.length)
    }
    return r
}

実装例2

\n\r も改行とみなす方が便利かと思います。

function parseCSV(d) {
    let t = d.replace(/(\r?\n|\r)$/u, '')
    const r = [[]]
    let c = 0
    let l, m, n
    while ([l, m, n] = /^(|[^",][^,\r\n]*|"(?:[^"]|"")*")(,|\r?\n|\r|$)/u.exec(t)) {
        if ('"' === m[0] && '"' === m[m.length - 1]) m = m.slice(1, -1)
        r[c].push(m.replace(/""/gu, '"'))
        if (!n) break
        if (',' !== n) r[++c] = []
        t = t.substring(l.length)
    }
    return r
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

jQueryの書き方 〜初級〜

jQueryの超初歩を備忘録としてメモしておきます。
言語を複数使っていると、書き方がこんがらがってしまったりするので、
『どう書くんだったかな』となった時用です〜(^o^)

基本的な書き方

$('セレクタ'・ 'id' ・ 'class').メソッド;
htmlで使用しているセレクタ等を指定して、そのセレクタ等にメソッドを実行する。
以下のコードは、h1を徐々に隠せ!というコード。

$(function() {
  $('h1').fadeOut();
});

ちなみに書き出しのfunctionですが、HTMLの読み込みが完了してからjQueryの操作を開始するようにという指定です。

$(function() {
  //
});

下のような書き方もできますが、長いのでどちらかというと前者の方が使われますね。

$(document).ready(function(){
  //
});

イベントのタイミングを指定する

$('セレクタ'・'id'・'class').イベント(function(){ hogehoge });
htmlで使用しているセレクタ等を指定して、そのセレクタ等に実行するメソッドを、
発火タイミングを指定して設定する。
以下のコードは、id=hide-text要素がクリックされたら、id=textを下から上にアニメーションで表示させて!というコード。

$(function(){
  $('#hide-text').click(function(){
    $('#text').slideUp();
  });
});

自身を指定する

$(this)
発火イベントの時に自身を指定すると、イベントが起こった要素を取得することができる。
以下のコードは、クリックされたリストのcssを変更して!というコード。

$(function(){
  $('li').click(function(){
    $(this).css('color', 'red');
  });
});

変数を宣言する

同じjQueryオブジェクト(これまでで$()で宣言したもの)を複数回使う時は、変数にしましょう!
処理が高速化されます。
いちいち$()と宣言していると、そのオブジェクトを都度読みにいきますが、変数にしていればそのオブジェクトを読みにいかず、変数に格納されているデータを使うからです。
変数宣言にはvarを使います。
以下のコードは \$('div') を\$divに格納しています。
jQueryオブジェクトを格納する時は、わかりやすいように先頭に$をつける慣習があります。

var $div = $('div');

メソッドチェーン

同じfunction内でメソッドを何回も使いたい場合、メソッドチェーンを使えばスマートに書くことができます。
例えば以下のようなコードは、cssとfadeOutを同じthisに対して使っている場合です。

$(function(){
  $('li').click(function(){
    $(this).css('color', 'red');
    $(this).fadeOut();
  });
});

これは以下のようにメソッドチェーンを使って書くことができます。

$(function(){
  $('li').click(function(){
    $(this).css('color', 'red').fadeOut();
  });
});

冗長ではなく綺麗になっていますね!

下階層の要素を取得

find()とchildren()がある。
find()は、要素の下階層にある指定した要素を取得する。
children()は、要素の"1階層下"の指定した要素を取得する。

<div is="wrapper">
  <a href="#">リンク1</a>
  <p>
    <a href="#">リンク2</a>
  </p>
</div>
$(function(){
  $('#wrapper').find('a')
});
//リンク1とリンク2が取得できる

$(function(){
  $('#wrapper').find('a')
});
//リンク1だけ取得できる

初級編は以上です!
中級編もまとめています(^^)/

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

Puppeteerのセットアップから使い方まで〜ブラウザ操作の自動化〜

はじめに

Puppeteerを使ってみたら、とても簡単にブラウザ操作が自動化できたことに感動しました。
セットアップの方法とよく使うPuppeteerのAPIについてまとめましたので、これから使ってみようかなと思っている方の参考になれば嬉しいです。

Puppeteerとは

Puppeteer

読み方は「ぱぺてぃあ」。日本語だと人形使いという意味です。
Chromeブラウザを操作できるNodeのライブラリで、Chrome DevToolsのチームが開発を行っています。

Puppetterの特徴

ブラウザ操作ができるツールとしては、Selenium Webdriverが有名です。
Selenium Webdriverとの違いは、Puppeteerはヘッドレスブラウザを使うことができるので、高速に動作させることができます。
また、PuppeteerはChromeのブラウザしか操作ができません。

使ってみる

セットアップ

Node.jsが動く環境が必要です。

$ npm i puppeteer

サクッと試したいだけならPuppeteerがWebツールを提供してくれているので、そちらを使うと便利です。

Try Puppeteer

テストを書く

簡単なテストシナリオを書いてみます。
下記はGoogleのトップページに遷移して、画面のスクリーンショットを撮るテストシナリオです。

test.js
const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.goto('https://www.google.com');
  await page.screenshot({path: 'screenshot.png'});

  await browser.close();
})();

ベースはこんな感じです。
Browserのインスタンスを作成した後にPuppeteerのAPIを使って、ページ内のリンクをクリックして別ページに遷移したり、フォームを操作したり、ブラウザを操作していきます。

テストを実行する

下記コマンドを実行します。

$ node test.js

よく使うAPI

指定したURLにアクセスする

page.goto('https://www.google.com/');

クリック

page.click('input[type="submit"]');

クリックする要素の指定には、CSSセレクタを使用します。指定したセレクタが存在しない場合は、エラーになります。

$ node test.js
UnhandledPromiseRejectionWarning: Error: Node is either not visible or not an HTMLElement at ElementHandle._clickablePoint

フォーム操作

<form method="post" action="/update">
  <label for="name">名前</label>
  <input type="text" name="name" id="name">
  <label for="email">メールアドレス</label>
  <input type="text" name="email" id="email">
  <label for="gender">性別</label>
  <select id="gender" name="gender">
    <option value="m">男性</option>
    <option value="f">女性</option>
  </select>
  <label for="inquire">問い合わせの内容</label>
  <input type="radio" name="inquire" value="1" id="inquire_1"> 商品に関する問い合わせ
  <input type="radio" name="inquire" value="2" id="inquire_2"> その他
  <input type="submit" value="送信">
</form>

例えば上記のフォームを操作するAPIは以下のとおりです。

// テキストフィールドに値を設定する
page.type('input[name="name"]', 'kanoe');
page.type('input[name="email"]', 'kanoe@xxxx.xxx');
// プルダウンから選択する
page.select('#gender', 'f');
// ラジオボタンを選択する
page.click('#inquire_1');

デバイス切り替え

Puppeteerには既にデバイスのリストを定義しているので、そちらを使って切り替えると便利です。
定義されているデバイスは下記コードから確認できます。
https://github.com/GoogleChrome/puppeteer/blob/master/DeviceDescriptors.js

const puppeteer = require('puppeteer');
const devices = require('puppeteer/DeviceDescriptors');
// iPhone6を指定してみる
const iPhone = devices['iPhone 6'];

(async () => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.emulate(iPhone);
  await page.goto('https://www.ozmall.co.jp/');
  await page.screenshot({path: 'example.png'});
  await browser.close();
})();

デバイスを指定する方法以外にも、page.setUserAgentを使うことでUserAgentを直接指定することも可能です。

page.setUserAgent('Mozilla/5.0 (Linux; U; Android 4.0; en-us; GT-I9300 Build/IMM76D) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30');

待機

用途としては、JavaScriptのeventが起きるまで待機するために使うことが多いAPIです。

// imgタグが見つかるまで待機、見つかったらconsoleにログを出力する
page.waitFor('img')
    .then(() => console.log('gazou attayo!'));
// 1s待機
page.waitFor(1000)

画面のキャプチャ

// fullPageのオプションを指定すると、フルページでスクリーンショットが撮れる
page.screenshot({path: 'example.png', fullPage: true});

特定の要素に対して操作する

特定の1要素だけ取得して操作する場合は、page.$evalを使います。

// itemクラスを持つ最初の要素のテキストを取得する
const tab = await page.$eval('.item', el => el.textContent);

ページ内のセレクタを全て取得したい場合は、page.$$evalを使う必要があります。

// ページ内の複数のセレクタの内、特定の要素だけをクリックする
page.$$eval('#nav > .item', tabs => {
  tabs.filter(tab => tab.textContent === 'Contact')[0].click();
});

感想

UIテストを書くとき、Selenium Webdriver + 何かしらのテストフレームワークでテストをかくことが多いと思います。
わたしも、以前Selenium WebdriverとCodeceptionを使ってE2Eテストを構築したことがあるのですが、ブラウザのドライバの仕様に引っ張られて苦労することが多々ありました。
PuppeteerはChrome開発チームが開発していることもあって、ストレスなくブラウザ操作が実現できるのがとてもよかったです。あと環境構築もとても簡単なので、ちょっと試したい時にすぐ使えるのは便利だなーと思いました。

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

JavaScriptで動画再生を操作する

動画の設置

<video> 要素の記述

htmlの基本的な記述は以下の通り。
<video>要素のsrc属性で動画のパスを指定します。動画再生に非対応の古いブラウザ用に、代替テキストや画像を用意するとよいと言われています。その場合は、<video>要素内に記述します。

index.html
<video src="./hoge.mp4">
  <p>ご使用のブラウザでは動画再生に対応していません</p>
</video>

HTML5以降では、<video>要素内に<source>要素を記述し、複数のソースを指定することが可能です。その場合は、上から順に<source>をみていき、再生可能なものが利用されます。

index.html
<video>
  <source src="./hoge.mp4" type="video/mp4">
  <source src="./hoge.webm" type="video/webm">
 <p>ご使用のブラウザでは動画再生に対応していません</p>
</video>

<video> 要素に指定できる属性

<video> 要素のcontrols属性を使用することで、ブラウザに用意されたコントローラを使用することができます。インターフェースはブラウザに依存します。
コントローラーは使用せず、JavaScriptで操作する場合の記述は、次のセクションにまとめています。

index.html
<video src="./hoge.mp4" controls>
  <p>ご使用のブラウザでは動画再生に対応していません</p>
</video>

その他、 <video>要素には、以下の属性を指定できます。

属性 機能
autoplay ロードされたら自動的に再生を開始 なし
loop ループ再生する なし
muted デフォルトで音量をゼロにする なし
preload データのプリロードについての指定 "none"/"metadata"/"auto"
poster データが再生可能になるまでに表示させる画像 画像のパス
width 高さ 整数値
height 整数値

参考: https://developer.mozilla.org/ja/docs/Web/HTML/Element/video

JavaScriptでの動画再生

JavaScriptで動画を操作する場合の主要な記述を以下にまとめています。

動画の再生操作

index.html
<video id="video" src="./hoge.mp4">
  <p>ご使用のブラウザでは動画再生に対応していません</p>
</video>
main.js
var v = document.getElementById('video');
//再生
v.play();
//一時停止
v.pause();
//ロード
v.load();

play()

paused 属性をfalseに設定する、必要に応じリソースをロードする、再生を開始する

pause()

再生を中断する、paused属性をtrueに設定する、必要に応じリソースをロードする

load()

要素をリセットする、新たなリソースを選択しロードを開始する

JavaScriptでの再生位置の取得

main.js
//再生位置の取得
v.addEventListener('timeupdate', function() {
  if (v.currentTime !== 0) {
    console.log(timeConvert(v.currentTime));
  } else {
    console.log('0:00');
  }
})
//数値型から”00:00”表記への変換(秒、ミリ秒の場合)
function timeConvert(time) {
  var sec = Math.floor(time);
  var msec = ((time - sec) * 100).toFixed(0);
  return sec + ':' + msec;
}

currentTimeプロパティ

再生位置を数値型データ(秒単位)で返す

durationプロパティ

メディアの再生時間を数値型データ(秒単位)で返す

再生状態に関するプロパティ

  • played: 再生が完了した時間の長さを表す
  • paused: 再生が一時停止されているかどうかをBoolean値で表す
  • ended: 再生が終了しているかどうかをBoolean値で表す
  • error: 直近で発生したメディアのダウンロード中のエラーをerrorオブジェクトで返す

再生の設定に関するプロパティ

  • loop: 繰り返し再生の有効・無効をBoolean値で表す
  • controls: 再生をコントロールするユーザインタフェースの表示・非表示をBoolean値で表す
  • preload: メディアをプリロードすべきか指定する、nonemetadataautoのいずれかの値をとる
  • autoplay: htmlのautoplay属性を反映し、Boolean値で表す

再生状態に関するイベントハンドラ

  • play: 再生が開始したとき
  • timeupdate: 再生位置が変化したとき
  • pause: 再生が中断したとき
  • playing: 再生中断状態から、ふたたび再生可能になったとき
  • waiting: 次のフレームの受信を待っているとき
  • ended: 再生が完了したとき
  • error: 再生中にエラーが発生したとき
  • abort: エラー以外の原因で再生が停止したとき

メディアの読み込みステータスの取得

readyStateプロパティ

メディアリソースの読み込みの状態を取得する。戻り値は以下の通り。

  • 0 :HAVE_NOTHING
    リソースに関するいかなる情報も利用できない状態

  • 1: HAVE_METADATA
    リソースの情報(ビデオ要素の場合、ビデオの高さ・幅など)を取得済み、再生位置に関するデータは未取得

  • 2: HAVE_CURRENT_DATA
    再生位置に関するデータは取得済み、再生位置より先のフレームデータは未取得

  • 3: HAVE_FUTURE_DATA
    再生位置より先のフレームデータも取得済みで、早送りができる状態

  • 4: DONE(完了)
    3の状態で、なおかつ、十分なデータがロードできていて、このまま再生しても、再生位置が読み込みデータを追い越さないような状態

networkStateプロパティ

メディアリソースの読み込みのネットワークの状態を取得する。戻り値は以下の通り。

  • 0: NETWORK_EMPTY
    リソースに関するいかなる情報も利用できない状態
    redayStateプロパティのHAVE_NOTHINGと同じ状態)

  • 1: NETWORK_EMPTY
    要素がアクティブではあるが、ネットワークは使用されていない状態 (リソースが取得できている)

  • 2: NETWORK_LOADING
    リソースの読み込み中

  • 3: NETWORK_NO_SOURCE
    リソースが見つからない

ロード状態に関するイベントハンドラ

index.html
<video id="video">
  <source src="./hoge.mp4" type="video/mp4">
  <p>ご使用のブラウザでは動画再生に対応していません</p>
</video>
<p id="state"></p>
main.js
document.addEventListener('DOMContentLoaded', function() {
  var v = document.getElementById('video');
  var state = document.getElementById('state');
  //ロード開始
  v.addEventListener('loadedmetadata', function() {
    state.textContent = 'ロードを開始しました';
  })
  //読み込み完了
  v.addEventListener('loadeddata', function() {
    state.textContent = '読み込み完了しました';
  })
  //再生可能
  v.addEventListener('canplay', function() {
    state.textContent = '再生可能です';
  })
  //再生中
  v.addEventListener('playing', function() {
    state.textContent = '再生中です';
})

参考

https://developer.mozilla.org/ja/docs/Web/HTML/Element/video
https://developer.mozilla.org/ja/docs/Web/API/HTMLMediaElement

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

object

https://javascript.info/object

オブジェクトの左側と右側の読み方がわからなかったので、JSの基礎を調べる。
{a: hoge}みたいなやつ

結論

{key: value}でした。


ぐぐるワード
js object

検索結果一番目
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Working_with_Objects
オブジェクトだけど、このオブジェクトじゃない。次。

ぐぐるワード
js object keys

検索結果一番目
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/keys

The Object.keys() method returns an array of a given object's own property names, in the same order as we get with a normal loop.
うーん、ちょっと理解に苦しむ。次、

ぐぐるワード
js object key value pairs

検索結果2番目
https://javascript.info/object
これっぽい。

An object can be created with figure brackets {…} with an optional list of properties. A property is a “key: value” pair, where key is a string (also called a “property name”), and value can be anything.

keyとvalueのペアですっぽいこと言ってる。

これは5分で見つけた。いい感じ。

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

js {}

https://cointrader.co.jp/botdev/

勉強中です

疑問

const {trade} = require('cointrader')

はい、意味が分かりません


ググったワード
const {}

検索結果4番目
https://ushumpei.hatenablog.com/entry/2016/06/29/032906

複雑で理解できませんでした。が、
・分割代入である
・分割代入 - JavaScript | MDN<=このページをみればよい
ということがわかりました。


https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment

オブジェクトの分割代入という項目を発見
これこれ、こういうのが欲しいの

簡単な例

var o = {p: 42, q: true};
var {p, q} = o;
console.log(p); // 42
console.log(q); // true

完全に理解した。超簡単に書くと

{a} = {a: "hoge"}

で、右辺にオブジェクト、左辺にオブジェクトのキーを書けば、値を取得できる。
ここで、objectってkeyとvalueのペアであっているのかという疑問が生じる。。。
私の頭は容量が少なすぎて、簡単な言葉で説明できないと記憶してくれません。てことで後で調べる。

ということで試しにオンラインエディタで実行

https://playcode.io/online-javascript-editor

const {a} = {a: "hello"}
console.log(a);

---
hello

はい、OK
ここまで10分でできたOK、OK
てことで次はObjectについて。

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

js 分割代入 {}

https://cointrader.co.jp/botdev/

勉強中です

疑問

const {trade} = require('cointrader')

はい、意味が分かりません


ググったワード
const {}

検索結果4番目
https://ushumpei.hatenablog.com/entry/2016/06/29/032906

複雑で理解できませんでした。が、
・分割代入である
・分割代入 - JavaScript | MDN<=このページをみればよい
ということがわかりました。


https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment

オブジェクトの分割代入という項目を発見
これこれ、こういうのが欲しいの

簡単な例

var o = {p: 42, q: true};
var {p, q} = o;
console.log(p); // 42
console.log(q); // true

完全に理解した。超簡単に書くと

{a} = {a: "hoge"}

で、右辺にオブジェクト、左辺にオブジェクトのキーを書けば、値を取得できる。
ここで、objectってkeyとvalueのペアであっているのかという疑問が生じる。。。
私の頭は容量が少なすぎて、簡単な言葉で説明できないと記憶してくれません。てことで後で調べる。

ということで試しにオンラインエディタで実行

https://playcode.io/online-javascript-editor

const {a} = {a: "hello"}
console.log(a);

---
hello

はい、OK
ここまで10分でできたOK、OK
てことで次はObjectについて。

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

HTML canvas で画像を切り取る(crop) Javascript サンプルコード

HTML canvas & Javascript を使った画像の切り抜き「Crop」のサンプルコードです。

ブラウザ上で画像を加工しようとしたら、canvas しかないわけですが、
いちばんやりたいのは「切り抜き:Crop」なんだけど、まぁ、シンプルなコードがほしかったのに無いわけで。
無いときは自分で作るのがエンジニアの掟なわけです。

切り取る(crop) Demo

Mar-02-2019 15-57-23.gif
中央の枠の中を切り出すタイプ。先に切り出す枠を決めておいて、画像の方を確認しながら切り出す。
なんと言いますか、マウスで角から角へドラッグしたり、枠をつまんで移動するUIよりも、このほうが直感的でわかりやすいし、スマホとかタブレットにも向いているUIだと思うのです。

Crop してますか?

実際に触ってみてもらうのが早いでしょう。⬇︎

  • canvas 上で、マウスをドラッグすると切り抜く場所を変えられます。
  • canvas 上で、マウスのホイールをグリグリすると拡大率が変わります。
  • 「CROP」ボタンで、赤枠の部分が切り抜きされます。
  • 上部の「A」「B」「C」のボタンで画像が変わります。

See the Pen HTML crop canvas by yamazaki.3104 (@yamazaki3104) on CodePen.

実際のコードがこちら

Qiita を見ているひとたちに、細かい解説は不要だとは思うので、ざっと概要のみ書く

  • 主な canvas は編集用の id='cvs' と、ボタン「CROP」を押したあとに編集結果が入る id='out' の2つ

  • ボタン「A」「B」「C」は入力画像の変更を行う load_img() を呼ぶだけ。読み込まれた画像は img に入っていて、img は読み出すだけで加工はしない。

  • スライダーは id='scal' 画像の拡大縮小率を変えられるように用意したが、最終的にホイールイベントに対応したので、UIとしてはあまり役に立っていないかもしれない

  • 画像の切り抜き処理は drawImage() がやってます

特に難しいことは何もやっていない、平凡なコードだと思っています。マウスのドラッグで画像の位置を変えるコードがいちばん大変だった、、、とはいっても、HTMLも入れて90行程度なので、、、まぁ、半日くらいの作業量でしょうか。

<html>
<body>
<button onclick="load_img('https://lh3.ggpht.com/O0aW5qsyCkR2i7Bu-jUU1b5BWA_NygJ6ui4MgaAvL7gfqvVWqkOBscDaq4pn-vkwByUx=w300')">A</button>
<button onclick="load_img('https://www.mozilla.org/media/img/logos/firefox/logo-quantum-wordmark-white.bd1944395fb6.png')">B</button>
<button onclick="load_img('')">C</button><br>
<input id='scal' type='range' value='' min='10' max='400' oninput="scaling(value)" style='width: 300px;'><br>
<canvas id='cvs' width='300' height='400'></canvas><br>
<button onclick='crop_img()'>CROP</button><br>
<canvas id='out' width='200' height='200'></canvas>
<script>
    const cvs = document.getElementById( 'cvs' )
    const cw = cvs.width
    const ch = cvs.height
    const out = document.getElementById( 'out' )
    const oh = out.height
    const ow = out.width

    let ix = 0    // 中心座標
    let iy = 0
    let v = 1.0   // 拡大縮小率
    const img  = new Image()
    img.onload = function( _ev ){   // 画像が読み込まれた
        ix = img.width  / 2
        iy = img.height / 2
        let scl = parseInt( cw / img.width * 100 )
        document.getElementById( 'scal' ).value = scl
        scaling( scl )
    }
    function load_img( _url ){  // 画像の読み込み
        img.src = ( _url ? _url : 'https://1.bp.blogspot.com/-AoQB8vIvlcw/W8BOcXcEQ6I/AAAAAAABPZM/rXNbol90tXcxBZBlXsg__xix03b_F4nqwCLcBGAs/s800/pet_cat_omoi_sleep_man.png' )
    }
    load_img()
    function scaling( _v ) {        // スライダーが変った
        v = parseInt( _v ) * 0.01
        draw_canvas( ix, iy )       // 画像更新
    }

    function draw_canvas( _x, _y ){     // 画像更新
        const ctx = cvs.getContext( '2d' )
        ctx.fillStyle = 'rgb(200, 200, 200)'
        ctx.fillRect( 0, 0, cw, ch )    // 背景を塗る
        ctx.drawImage( img,
            0, 0, img.width, img.height,
            (cw/2)-_x*v, (ch/2)-_y*v, img.width*v, img.height*v,
        )
        ctx.strokeStyle = 'rgba(200, 0, 0, 0.8)'
        ctx.strokeRect( (cw-ow)/2, (ch-oh)/2, ow, oh ) // 赤い枠
    }
    function crop_img(){                // 画像切り取り
        const ctx = out.getContext( '2d' )
        ctx.fillStyle = 'rgb(200, 200, 200)'
        ctx.fillRect( 0, 0, ow, ow )    // 背景を塗る
        ctx.drawImage( img,
            0, 0, img.width, img.height,
            (ow/2)-ix*v, (oh/2)-iy*v, img.width*v, img.height*v,
        )
    }

    let mouse_down = false      // canvas ドラッグ中フラグ
    let sx = 0                  // canvas ドラッグ開始位置
    let sy = 0
    cvs.onmousedown = function ( _ev ){     // canvas ドラッグ開始位置
        mouse_down = true
        sx = _ev.offsetX
        sy = _ev.offsetY
    }
    cvs.onmouseout =
    cvs.onmouseup = function ( _ev ){       // canvas ドラッグ終了位置
        if ( mouse_down == false ) return
        mouse_down = false
        draw_canvas( ix += (sx-_ev.offsetX)/v, iy += (sy-_ev.offsetY)/v )
    }
    cvs.onmousemove = function ( _ev ){     // canvas ドラッグ中
        if ( mouse_down == false ) return
        draw_canvas( ix + (sx-_ev.offsetX)/v, iy + (sy-_ev.offsetY)/v )
    }
    cvs.onmousewheel = function ( _ev ){    // canvas ホイールで拡大縮小
        let scl = parseInt( document.getElementById( 'scal' ).value )
        document.getElementById( 'scal' ).value = scl + _ev.wheelDelta
        scaling( scl + _ev.wheelDelta )
        return false // イベントを伝搬しない
    }
</script>
</body>
</html>

動作確認環境

いまのところ

  • Chrome バージョン: 72.0.3626.109(Official Build) (64 ビット)
  • Safari バージョン 12.0.2 (14606.3.4)

でしか確認してません。ごめんなさい。
他の環境で確認できたら、追記します。

2019/03/03: 追記
Firefox 65.0.2 で確認したらホイールのイベントが取れてなかった、、、なぜ??
(原因不明の助)

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

takeWhile はメモリリーク (無駄なメモリ消費) の原因になり得る

takeWhile に参照透過でない式を渡すのは良くない。

上記が分かっていれば下記を読む必要はありません。

経緯

React開発ノウハウメモ(随時更新)
上記記事を読んで unsubscribecomponentWillUnmount に書かない方法を知ったので調べてみた。1

takeWhile はメモリリークの原因になり得る。

takeWhile は値が来たときのみ実行されるので値が来なければ unsubscribe されない。
無駄なネットワーク通信やDBへのアクセスを続けてしまう。
takeWhile は値に応じて処理を続けるべきか決まる場合で使うべき。

Observable を流れる値と関係なく処理を止めたいなら unsubscribe を呼ぶか takeUntil を使う。

テストしてみる

実際に試してみるためのコードを書いた。
それらで下記の 3 つの関数を使っている。

utils.ts
// s 秒待つ
const sleep = (s: number) => new Promise(r => setTimeout(r, s * 1000))
// タイムスタンプをつけてログを出す
const log = (...v: any[]) => {
    const ts = (performance.now() / 1000) | 0
    console.log(...v, `(${ts})`)
}
// キャンセルできる Observable を生成
const make = (name: string) => {
    return new Observable<number>(s => {
        let canceled = false
        const push = (v: number) => {
            log(name, 'sub1', v, '- canceled', canceled)
            s.next(v)
        }
        Promise.resolve().then(async () => {
            push(12) // すぐに 12 を渡す
            await sleep(10) // 10 秒待ってから
            push(13) // 13 を渡す
            push(14) // 14 を渡す
        })
        return () => {
            log(name, 'unsub1')
            canceled = true
        }
    })
}

テスト内容

上記 makeObservable を生成したのち 1 秒後に処理を止める。

unsubscribe する

一般的なやりかたであると思われる。
問題なく動いている。

test1.ts
const test1 = async () => {
    const read = make('test1').pipe(
        tap(v => log('test1', 'sub2', v)),
    ).subscribe()
    await sleep(1)
    read.unsubscribe()
}
test1() /*
test1 sub1 12 - canceled false (0)
test1 sub2 12 (0)
test1 unsub1 (1)
test1 sub1 13 - canceled true (10)
test1 sub1 14 - canceled true (10)
*/

takeWhile を使う

1 秒後に止めようとしたが、その後 9 秒経って次の値が来るまで止まっていない。

test2.ts
const test2 = async () => {
    let alive = true
    make('test2').pipe(
        takeWhile(() => alive),
        tap(v => log('test2', 'sub2', v)),
    ).subscribe()
    await sleep(1)
    alive = false
}
test2() /*
test2 sub1 12 - canceled false (0)
test2 sub2 12 (0)
test2 sub1 13 - canceled false (10)
test2 unsub1 (10)
test2 sub1 14 - canceled true (10)
*/

takeUntil を使う

問題なく動く。

test3.ts
const test3 = async () => {
    const unsub = new Subject<void>()
    make('test3').pipe(
        takeUntil(unsub),
        tap(v => log('test3', 'sub2', v)),
    ).subscribe()
    await sleep(1)
    unsub.next()
    unsub.complete()
}
test3() /*
test3 sub1 12 - canceled false (0)
test3 sub2 12 (0)
test3 unsub1 (1)
test3 sub1 13 - canceled true (10)
test3 sub1 14 - canceled true (10)
*/

unsubscribetakeUntil 、どちらを使うべきか?

こちらの記事 (英語)では takeUntil がオススメされている。

手続き的な unsubscribe よりも宣言的な takeUntil が良いという判断だろうか?

補足

ReactRxJS を組み合わせる場合

大規模なアプリなら redux-observable を、小規模なら rxjs-hooks を使うことを勧める。
古い React を使っているなら recompose も良い。


  1. useEffect のある今の ReactcomponentWillUnmount のような古い方法を使う必要はないと思う。 

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

書いて覚える ESLint ルールの作り方:キマリは通さない

はじめに

JavaScript の Linter である ESLint のルールを自作しそれを動かす方法を作りながら調べました。

この記事はその備忘録の第一回です。最初なのでまずは単純な ESLint ルールを作ります。そしてそれをローカルルールとして動かしてみます。

Linter とは

プログラムコードのクオリティを担保するため、開発チーム内でコーディングルールが決められます。

しかし、チームレビューなどの目視による確認方法では、見落としが発生することが容易に想像できます。そこでモダンな開発チームでは、静的コード解析ツール (Linter) を用いて自動的に機械にチェックさせています。そうすることで見落としを防ぐことができます。便利ですね。

例えるなら...

あなたのプロジェクトはガガゼト山です。ここに召喚士一行が通りかかりますが、あなたは次のルールを課しています:

召喚士は通す。 ガードも通す。 キマリは通さない。

しかし、24 時間 365 日、キマリが通るかどうかを一人で監視することは難しいです。立て看板を置いておきますか? キマリは無視して通ってしまうかもしれません。他のロンゾ族に伝えておきますか? もしかしたらそのロンゾ族は、反逆者である召喚士が己の身を捧げる覚悟を聞いてキマリを通してしまうかもしれません。

そこでビランとエンケに頼みます。ビランとエンケは絶対にキマリを通しません。召喚士を通しても、ガードを通しても、キマリは通しません。いつでもガガゼト山の道を一族の誇りをかけて監視しており、通りがかったキマリを見つけては過去の因縁を持ち出して弄ったり二対一で戦ったりして通せんぼします。二人はキマリを通さないことにかけてはエキスパートです。

しかし、二人はキマリの成長した強さに感服してキマリを通してしまいます。

というわけで

キマリを通さない ESLint ルールを作ります。

要件

すでに汎用的な ESLint のルールは広く公開されており、大抵の場合はそれらを組み合わせて適用すれば事足ります。ですがその中にキマリを通さない ESLint ルールはありません。なので自作しましょう。

ここではコード中の文字列リテラルに「キマリ」が含まれている場合にエラーにします。

ちなみに、文字列リテラル内で「キマリ」と連続していなければならないことにします。仮に「キ」と「マ」と「リ」を別個に宣言して後から結合しても、ガガゼト山を通ろうとしているのは「キマリ」ではなく「キ」と「マ」と「リ」に分割されたものだからです。

実装

まずは eslint を install しましょう。

コードは AST (抽象構文木)と呼ばれる、コードをパースした JSON で ESLint に入ってきます(AST のわかりやすい解説)。コードは抽象化され、例えば ""'' のような書き方の差異は構文木の構造自体には現れなくなります。それに JSON 形式なのでプログラムで簡単に扱うことができます。

今回の要件では Literal ノードを見て、その value が文字列かつ「キマリ」が含まれているかチェックするだけで OK です。エラー地点は Literal ノード、メッセージは「キマリは通さない」にします。

rules/kimahri-not-pass.js
"use strict";

module.exports = {
  create: function(context) {
    return {
      Literal: function(node) {
        if (
          typeof node.value == "string" &&
          node.value.indexOf("キマリ") !== -1
        ) {
          context.report({ node: node, message: "キマリは通さない" });
        }
      }
    };
  }
};

テスト

正しく動くかどうかテストを書きます。テストには mocha を利用し、次のような形式で簡単に書くことができます。

通すルールは valid に書きます。まずはvar a = 1;という簡単な式が通ることを書いておきましょう。後は召喚士は通す、ガードも通すことも書いておきます。

通さないルールである invalid でキマリを通さないことを記述します。

tests/kimahri-not-pass.js
"use strict";

const RuleTester = require("eslint").RuleTester;
const rule = require("../rules/kimahri-not-pass");

const tester = new RuleTester();

tester.run("kiomahri-not-pass", rule, {
  valid: [{ code: "var a = 1;" }, { code: '"召喚士"' }, { code: '"ガード"' }],
  invalid: [{ code: '"キマリ"', errors: ["キマリは通さない"] }]
});

テストスクリプトは mocha "tests/**/*.js" --reporter dot です。無事通過すれば作成完了です。

ルールを動かす

実際に開発しているような環境でルールを動かして確かめないと納得がいきません。実際に src ファイルを書いて、それに対して lint をかけてみます。

キマリを通さないのはガガゼト山だけでいいので、パッケージとしてではなくプロジェクト内のローカルルールとして動かします。ローカルルールを .eslintrc.js に書くために eslint-plugin-rulesdir を add します(--rulesdir オプションでもいいんですが後述の VSCode で動かすために plugin で書きます)。

.eslintrc.js は次のように書きます:

.eslintrc.js
const rulesDirPlugin = require("eslint-plugin-rulesdir");
rulesDirPlugin.RULES_DIR = "rules";

module.exports = {
  parserOptions: {
    ecmaVersion: 6
  },
  plugins: ["rulesdir"],
  rules: {
    "rulesdir/kimahri-not-pass": "error"
  }
};

src ファイルは次の通りです。

src/kimahri-not-pass.js
"use strict";

const a = "キマリは通さない";

eslint ./src を打てばちゃんとエラーとして検出されました。

VSCode でルールを動かす

さらに VSCode の ESLint 拡張を使って動作するようにします。

上で書いた通りコマンドオプションでなく eslint-plugin-rulesdir でルールの場所を教えてあげているので ESLint 拡張もローカルルールを読み込むことができます。

これが次のように表示されれば成功です。動かない時は VSCode を再起動するといいかと思います。
Screen Shot 2019-03-02 at 4.43.55.png

終わりに

今回、簡単な ESLint のルールを作って、それをローカルルールとして動作させる方法を学びました。

実際のプロダクトにおいてはキマリを通さないルールを課すことはないでしょうし、キマリは実力を示すことでガガゼト山を通過することを認められます。

これを足がかりに、次回はきちんとプロジェクトで使えそうなルールを書く予定です。

参考にしたサイト

・ルールの書き方
https://qiita.com/mysticatea/items/cc3f648e11368799e66c
https://techblog.yahoo.co.jp/javascript/how-to-create-eslint-rules

・AST のわかりやすい解説
https://efcl.info/2016/03/06/ast-first-step/

・実際にコードを書いて AST を見られるツール
https://astexplorer.net

・公式
https://eslint.org/docs/developer-guide/working-with-rules

あと実際に公開されているルールのコードを見ると勝てます。

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

Javascriptで、公開鍵ペア生成・暗号化・復号をしてみた

node.jsでRSA公開鍵を使った鍵ペア生成や暗号化/復号をする例はたくさんあったのですが、javascriptでの例はあまりなかったのでやってみました。
途中、はまってしまったので、備忘録として残しておきます。

とはいいつつも、なんてことはない、browserifyを使っただけです。。。
使わせてもらったnpmモジュールは「node-rsa」です。PureJavascriptという特徴に惹かれました。

rzcoder/node-rsa
 https://github.com/rzcoder/node-rsa

作成するモジュールの仕様

ブラウザ上のJavascriptから以下関数を呼び出せるモジュールとします。

  • RSA公開鍵ペアの生成
  • 公開鍵による暗号化
  • 秘密鍵による復号
  • PEM形式をDER形式に変換
  • DER形式をPEM形式に変換

ちなみに、暗号化時のパディング方式は「RSA_PKCS1_OAEP_PADDING」とします。PKCS#1 v2.0 with SHA-1のことです。

node.jsでモジュールを作成する

まずは、node.jsで期待する動作となることを確認しましょう。

npm init -y
npm install --save node-rsa
vi main.js

main.js
const NodeRSA = require('node-rsa');

function generateKeyPair(bits){
  const key = new NodeRSA({b: bits});

  var priv = key.exportKey('pkcs1-private-der');
  var pub = key.exportKey('pkcs1-public-der');

  return { public: pub, private: priv };
}

function pem2der(scheme, pem){
  const key = new NodeRSA(pem, scheme + '-pem');
  return key.exportKey(scheme + '-der');
}

function der2pem(scheme, key){
  var der = Buffer.from(key);
  const rsa = new NodeRSA(der, scheme + '-der');
  return rsa.exportKey(scheme + '-pem');
}

function publicEncrypt(key, buffer){
  var input = Buffer.from(buffer);
  var der = Buffer.from(key);
  const rsa = new NodeRSA(der, 'pkcs1-public-der', { encryptionScheme : 'pkcs1_oaep' });
  var enc = rsa.encrypt(input);
  return enc;
}

function privateDecrypt(key, buffer){
  var input = Buffer.from(buffer);
  var der = Buffer.from(key);
  const rsa = new NodeRSA(der, 'pkcs1-private-der', { encryptionScheme : 'pkcs1_oaep' });
  var dec = rsa.decrypt(input);
  return dec;
}

module.exports = {
  generateKeyPair: generateKeyPair,
  publicEncrypt: publicEncrypt,
  privateDecrypt: privateDecrypt,
  der2pem: der2pem,
  pem2der: pem2der,
};

ちなみに、はまった点を示しておきます。

publicEncryptやprivateDecryptでは、以下のような一見不要な処理をしています。ですが、なぜかこの処理を入れないと、ブラウザのJavascriptから呼び出したときにうまく復号してくれなかったんです。原因不明です。とりあえず動いているのでいいですが。。

var input = Buffer.from(buffer);

次は、それを呼び出すサンプルコードです。

index.js
var rsab = require('./main');

var key = rsab.generateKeyPair(2048);
console.log('private=', key.private);
console.log('public=', key.public);

var buffer = new Uint8Array(10);

var enc2 = rsab.publicEncrypt(key.public, buffer);
console.log('enc2=', enc2);
var dec2 = rsab.privateDecrypt(key.private, enc2);
console.log('OK? dec2=', dec2);

var pem = rsab.der2pem('pkcs1-private', key.private);
console.log(pem);
var der = rsab.pem2der('pkcs1-private', pem);
console.log(der);
var pem = rsab.der2pem('pkcs1-public', key.public);
console.log(pem);
var der = rsab.pem2der('pkcs1-public', pem);
console.log(der);

以下のような感じで出力されましたでしょうか?

private= <Buffer 30 82 02 5b 02 01 00 02 81 81 00 98 db 26 9b 37 a0 ec 95 d5 19 84 9e 51 64 96 0b 01 2a 90 aa ee b8 39 26 5d b0 b8 32 12 c1 dd 2d b8 cd 1b ea 69 1f 7c ... >
public= <Buffer 30 81 89 02 81 81 00 98 db 26 9b 37 a0 ec 95 d5 19 84 9e 51 64 96 0b 01 2a 90 aa ee b8 39 26 5d b0 b8 32 12 c1 dd 2d b8 cd 1b ea 69 1f 7c 4c 1e d3 9a ... >
enc2= <Buffer 1c fb e1 02 95 a6 4c b4 6f 4a 22 20 10 47 94 1d 0a ca d1 77 9a 4f 28 a8 8d 66 e7 cb 7c b7 cb 13 3e 07 3a a1 75 0d be 94 25 c7 ef fc 3e be de 6e f4 eb ... >
OK? dec2= <Buffer 00 00 00 00 00 00 00 00 00 00>
-----BEGIN RSA PRIVATE KEY-----
MIICWwIBAAKBgQCY2yabN6DsldUZhJ5RZJYLASqQqu64OSZdsLgyEsHdLbjNG+pp
H3xMHtOaJxrMOQh4hABGUiU5KS1gqT88c33ymvMw2hBm4ArdJ59n9y+PSIAKIdMO
9CNR5I4ONj59cbL3uaIZdPmmXZ2ApHU83nUVv1pqflULRMoEflGsMgfaRwIDAQAB
AoGATmllZYxk1diKx5tbZ590xNJlVm8JJKHUh41HABDVODPjL/yZBDItnhLWM6bJ
vWeoa8IzLd+nzqnL6GEJ2mXYflUiTTmeHr6Tgd+AtiMzmw9F9M6SDtKbLo9thHjz
ss31l6q18/o4lhOHxltcA1k0ns/ou2Ys5nre90/YtO65vdECQQDmbJdNw7OaLSyC
z1HRnfQ1wPTSsRtUzDPVVcirnssRgzPQXjwX+sJonG6Plt6Ymk3raAKYfj4/4eKp
wXSKgGWPAkEAqdJ+QealDvocx9n3L5WIcN1QKsQBbjW/lCZW9eBLY5J2eX2/KXCH
1UB4ycL4/sGJnnrKiEJcmMwAfJ7cuymTyQJAaN3O1kNUf7xDX5z+vYlHVRdl1GVp
7OOqS1LpKsHv5R5y/EeGzdr/kyBuaMbes1C2WYZSeBgsOGKTB3LVmRrqHQJAUrux
tvlewIUpMFQIEK6Q4itvuuXyrHaS9uIWy4KSN9hKz7VrRA0Gn+Xg8qqCE9rF6Od5
dBigc/Au7IeYkbYXwQJAcBdeq8OZvdQ93aZkBWntcYFV4N82XfulIYD3EbP+Fi9G
dUWWUtKzsJuby9vrb5yCiJgHO16GTH7RqGFJyd124A==
-----END RSA PRIVATE KEY-----
<Buffer 30 82 02 5b 02 01 00 02 81 81 00 98 db 26 9b 37 a0 ec 95 d5 19 84 9e 51 64 96 0b 01 2a 90 aa ee b8 39 26 5d b0 b8 32 12 c1 dd 2d b8 cd 1b ea 69 1f 7c ... >
-----BEGIN RSA PUBLIC KEY-----
MIGJAoGBAJjbJps3oOyV1RmEnlFklgsBKpCq7rg5Jl2wuDISwd0tuM0b6mkffEwe
05onGsw5CHiEAEZSJTkpLWCpPzxzffKa8zDaEGbgCt0nn2f3L49IgAoh0w70I1Hk
jg42Pn1xsve5ohl0+aZdnYCkdTzedRW/Wmp+VQtEygR+UawyB9pHAgMBAAE=
-----END RSA PUBLIC KEY-----
<Buffer 30 81 89 02 81 81 00 98 db 26 9b 37 a0 ec 95 d5 19 84 9e 51 64 96 0b 01 2a 90 aa ee b8 39 26 5d b0 b8 32 12 c1 dd 2d b8 cd 1b ea 69 1f 7c 4c 1e d3 9a ... >

当然ながら、実行ごとに鍵ペアの鍵値や暗号文の値は異なります。

browserify化する

まだbrowserifyをインストールしていない場合は、インストールしておきます。

npm install browserify -g

それではJavascriptのモジュールを作成します。

browserify -r ./main.js:node-rsa-js -o node-rsa-js.js

これで、node-rsa-js.jsというJavascriptファイルが出来上がりました。-rで指定した「node-rsa-js」がJavascriptからrequireする名前になります。

ブラウザから使ってみる

以下のようなサンプルHTMLファイルを作成します。

index.html
<html>
  <head>
  </head>
  <body>
    <script src="node-rsa-js.js"></script>
    <script>
      var rsab = require('node-rsa-js');
      var key = rsab.generateKeyPair(1024);
      console.log('private=', key.private);
      console.log('public=', key.public);

      var buffer = new Uint8Array(10);

      var enc2 = rsab.publicEncrypt(key.public, buffer);
      console.log('enc2=', enc2);
      var dec2 = rsab.privateDecrypt(key.private, enc2);
      console.log('OK? dec2=', dec2);

      var pem = rsab.der2pem('pkcs1-private', key.private);
      console.log(pem);
      var der = rsab.pem2der('pkcs1-private', pem);
      console.log(der);
      var pem = rsab.der2pem('pkcs1-public', key.public);
      console.log(pem);
      var der = rsab.pem2der('pkcs1-public', pem);
      console.log(der);
    </script>
  </body>
</html>

このindex.htmlと同じフォルダに先ほど作成したnode-rsa-js.jsを置きます。

早速、ブラウザから表示してみましょう。その時、各ブラウザのConsoleをのぞいてみてください。
以下のような感じで表示されれば成功です。

private= Uint8Array(607) [48, 130, 2, 91, 2, 1, 0, 2, 129, 129, 0, 135, 247, 194, 56, 19, 233, 127, 19, 147, 129, 65, 237, 175, 243, 152, 253, 56, 138, 53, 125, 92, 80, 238, 56, 162, 176, 145, 208, 171, 170, 67, 7, 180, 223, 138, 45, 41, 210, 92, 189, 81, 30, 52, 182, 180, 204, 22, 121, 80, 52, 11, 15, 134, 141, 167, 65, 149, 99, 59, 220, 57, 95, 87, 118, 213, 114, 177, 49, 174, 18, 88, 184, 11, 165, 220, 15, 137, 140, 159, 116, 207, 152, 39, 71, 161, 24, 217, 108, 192, …]
(index):10 public= Uint8Array(140) [48, 129, 137, 2, 129, 129, 0, 135, 247, 194, 56, 19, 233, 127, 19, 147, 129, 65, 237, 175, 243, 152, 253, 56, 138, 53, 125, 92, 80, 238, 56, 162, 176, 145, 208, 171, 170, 67, 7, 180, 223, 138, 45, 41, 210, 92, 189, 81, 30, 52, 182, 180, 204, 22, 121, 80, 52, 11, 15, 134, 141, 167, 65, 149, 99, 59, 220, 57, 95, 87, 118, 213, 114, 177, 49, 174, 18, 88, 184, 11, 165, 220, 15, 137, 140, 159, 116, 207, 152, 39, 71, 161, 24, 217, 108, 192, 225, 181, 95, 94, …]
(index):15 enc2= Uint8Array(128) [29, 169, 149, 166, 146, 167, 162, 135, 62, 94, 187, 65, 22, 38, 205, 152, 249, 187, 73, 173, 52, 92, 96, 143, 31, 10, 48, 169, 210, 168, 227, 253, 216, 79, 159, 110, 13, 66, 85, 133, 22, 120, 234, 216, 188, 133, 149, 29, 252, 221, 146, 158, 98, 149, 83, 28, 85, 97, 17, 46, 246, 216, 100, 119, 134, 119, 31, 152, 39, 23, 30, 125, 97, 189, 242, 22, 215, 106, 44, 189, 26, 110, 241, 140, 146, 235, 12, 116, 119, 120, 230, 12, 195, 135, 246, 164, 13, 163, 141, 115, …]
(index):17 OK? dec2= Uint8Array(10) [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
(index):20 -----BEGIN RSA PRIVATE KEY-----
MIICWwIBAAKBgQCH98I4E+l/E5OBQe2v85j9OIo1fVxQ7jiisJHQq6pDB7Tfii0p
0ly9UR40trTMFnlQNAsPho2nQZVjO9w5X1d21XKxMa4SWLgLpdwPiYyfdM+YJ0eh
GNlswOG1X14XtKAWQNJmSlu4toOmTMHwheoXK9r0H3V2oKhzVW7FBQBaEwIDAQAB
AoGACSWw6kbkqYjunn6J+BFiVH7ORYT2hfdQ1hkc5jX2UWYeU/+exDaDyLTBXHiC
N0y3wp6Ns/CwbPUrkK6BRVbVsm7NUAKVJqhjUEGlsSjoTqnvBNCBTe/hqbpfZNFr
VVUOkPHxOSDVttJtPJydExwDBq/dbibaTOqlAsmIRrfNMhECQQDEG19ngx1L/1V5
QAzxs5jylRV8AdAFHBZIIjhkfTv0jvY1AvzP3x/GTdTQfsgm0i79Q/dajvTgqWZL
0uXVDS8bAkEAsX5oJmuu/o5Jxdc+4sewgNp9H2RVCytj0viNmM0SxXj/9z8BT1rh
+TTjFlJi5Nzf08W6J4X2HI2SFiWVNeCYaQJAYsdy593oPOaRNDDqFDklulMT7lGv
aVN7ebwa8asPLyFvrMYjd8Vo6Oc9YgPqZ3uJhbLdfjTx7dQGHf1OHWG1rwJAF19j
65tGgfLOuCSt/UL6dR1QWK/nmv7tuDxOuQ5YKHTUkARV8dMv6pcSSEA1EZocR1DE
QKwdSolB8XSlf2DB8QJAX5IGTRZCz3qKQ9ghPfYKa45QT9M2uB0MX0cazgPUNrQf
0IGlzH1L7LSVGaGNkM3XYR2RxIVGn46Dv3g1REwAhQ==
-----END RSA PRIVATE KEY-----
(index):22 Uint8Array(607) [48, 130, 2, 91, 2, 1, 0, 2, 129, 129, 0, 135, 247, 194, 56, 19, 233, 127, 19, 147, 129, 65, 237, 175, 243, 152, 253, 56, 138, 53, 125, 92, 80, 238, 56, 162, 176, 145, 208, 171, 170, 67, 7, 180, 223, 138, 45, 41, 210, 92, 189, 81, 30, 52, 182, 180, 204, 22, 121, 80, 52, 11, 15, 134, 141, 167, 65, 149, 99, 59, 220, 57, 95, 87, 118, 213, 114, 177, 49, 174, 18, 88, 184, 11, 165, 220, 15, 137, 140, 159, 116, 207, 152, 39, 71, 161, 24, 217, 108, 192, …]
(index):24 -----BEGIN RSA PUBLIC KEY-----
MIGJAoGBAIf3wjgT6X8Tk4FB7a/zmP04ijV9XFDuOKKwkdCrqkMHtN+KLSnSXL1R
HjS2tMwWeVA0Cw+GjadBlWM73DlfV3bVcrExrhJYuAul3A+JjJ90z5gnR6EY2WzA
4bVfXhe0oBZA0mZKW7i2g6ZMwfCF6hcr2vQfdXagqHNVbsUFAFoTAgMBAAE=
-----END RSA PUBLIC KEY-----
(index):26 Uint8Array(140) [48, 129, 137, 2, 129, 129, 0, 135, 247, 194, 56, 19, 233, 127, 19, 147, 129, 65, 237, 175, 243, 152, 253, 56, 138, 53, 125, 92, 80, 238, 56, 162, 176, 145, 208, 171, 170, 67, 7, 180, 223, 138, 45, 41, 210, 92, 189, 81, 30, 52, 182, 180, 204, 22, 121, 80, 52, 11, 15, 134, 141, 167, 65, 149, 99, 59, 220, 57, 95, 87, 118, 213, 114, 177, 49, 174, 18, 88, 184, 11, 165, 220, 15, 137, 140, 159, 116, 207, 152, 39, 71, 161, 24, 217, 108, 192, 225, 181, 95, 94, …]

関数名を見れば、だいたい処理内容は理解いただけると思います。
1点だけ注意ですが、publicEncryptとprivateDecryptで指定する公開鍵はDER形式としています。
私がDER形式を使うことが多いためこのようにしていますが、PEM形式とすることは簡単だと思いますので、適宜修正してください。

以上

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

Javascriptで公開鍵ペア生成・暗号化・復号をしてみた

node.jsでRSA公開鍵を使った鍵ペア生成や暗号化/復号をする例はたくさんあったのですが、javascriptでの例はあまりなかったのでやってみました。
途中、はまってしまったので、備忘録として残しておきます。

とはいいつつも、なんてことはない、browserifyを使っただけです。。。
使わせてもらったnpmモジュールは「node-rsa」です。Pure Javascriptという特徴に惹かれました。

rzcoder/node-rsa
 https://github.com/rzcoder/node-rsa

(2019/3/2 追記)
せっかくなので、署名付与・署名検証も追加しておきました。

作成するモジュールの仕様

ブラウザ上のJavascriptから以下関数を呼び出せるモジュールとします。

  • RSA公開鍵ペアの生成
  • 公開鍵による暗号化
  • 秘密鍵による復号
  • 秘密鍵による署名付与
  • 公開鍵による署名検証
  • PEM形式をDER形式に変換
  • DER形式をPEM形式に変換

ちなみに、暗号化時のパディング方式は「RSA_PKCS1_OAEP_PADDING」とします。PKCS#1 v2.0 with SHA-1のことです。

node.jsでモジュールを作成する

まずは、node.jsで期待する動作となることを確認しましょう。

npm init -y
npm install --save node-rsa
vi main.js

main.js
const NodeRSA = require('node-rsa');

function generateKeyPair(bits){
  const key = new NodeRSA({b: bits});

  var priv = key.exportKey('pkcs1-private-der');
  var pub = key.exportKey('pkcs1-public-der');

  return { public: pub, private: priv };
}

function pem2der(scheme, pem){
  const key = new NodeRSA(pem, scheme + '-pem');
  return key.exportKey(scheme + '-der');
}

function der2pem(scheme, key){
  var der = Buffer.from(key);
  const rsa = new NodeRSA(der, scheme + '-der');
  return rsa.exportKey(scheme + '-pem');
}

function publicEncrypt(key, buffer){
  var input = Buffer.from(buffer);
  var der = Buffer.from(key);
  const rsa = new NodeRSA(der, 'pkcs1-public-der', { encryptionScheme : 'pkcs1_oaep' });
  var enc = rsa.encrypt(input);
  return enc;
}

function privateDecrypt(key, buffer){
  var input = Buffer.from(buffer);
  var der = Buffer.from(key);
  const rsa = new NodeRSA(der, 'pkcs1-private-der', { encryptionScheme : 'pkcs1_oaep' });
  var dec = rsa.decrypt(input);
  return dec;
}

function sign(key, buffer){
  var input = Buffer.from(buffer);
  var der = Buffer.from(key);
  const rsa = new NodeRSA(der, 'pkcs1-private-der');
  var sig = rsa.sign(input);
  return sig;
}

function verify(key, buffer, signature){
  var input = Buffer.from(buffer);
  var der = Buffer.from(key);
  var sig = Buffer.from(signature)
  const rsa = new NodeRSA(der, 'pkcs1-public-der');
  var result = rsa.verify(input, sig);
  return result;
}

module.exports = {
  generateKeyPair: generateKeyPair,
  publicEncrypt: publicEncrypt,
  privateDecrypt: privateDecrypt,
  sign: sign,
  verify: verify,
  der2pem: der2pem,
  pem2der: pem2der,
};

ちなみに、はまった点を示しておきます。

publicEncryptやprivateDecryptでは、以下のような一見不要な処理をしています。ですが、なぜかこの処理を入れないと、ブラウザのJavascriptから呼び出したときにうまく復号してくれなかったんです。原因不明です。とりあえず動いているのでいいですが。。

var input = Buffer.from(buffer);

次は、それを呼び出すサンプルコードです。

index.js
var rsab = require('./main');

var key = rsab.generateKeyPair(1024);
console.log('private=', key.private);
console.log('public=', key.public);

var buffer = new Uint8Array(10);

var enc2 = rsab.publicEncrypt(key.public, buffer);
console.log('enc2=', enc2);
var dec2 = rsab.privateDecrypt(key.private, enc2);
console.log('OK? dec2=', dec2);

var pem = rsab.der2pem('pkcs1-private', key.private);
console.log(pem);
var der = rsab.pem2der('pkcs1-private', pem);
console.log(der);
var pem = rsab.der2pem('pkcs1-public', key.public);
console.log(pem);
var der = rsab.pem2der('pkcs1-public', pem);
console.log(der);

var sig = rsab.sign(key.private, 'Hello World');
console.log('sig=', sig);

var result = rsab.verify(key.public, 'Hello World', sig);
console.log('result=', result);

以下のような感じで出力されましたでしょうか?

private= <Buffer 30 82 02 5c 02 01 00 02 81 81 00 86 d6 34 4f d8 d8 2d 33 75 1e 04 95 bc a8 a0 24 30 c8 a0 d3 c6 b7 73 fe c1 81 8c 05 25 75 f1 1e a9 c5 3e 75 c5 04 2a ... >
public= <Buffer 30 81 89 02 81 81 00 86 d6 34 4f d8 d8 2d 33 75 1e 04 95 bc a8 a0 24 30 c8 a0 d3 c6 b7 73 fe c1 81 8c 05 25 75 f1 1e a9 c5 3e 75 c5 04 2a d5 65 a3 b4 ... >
enc2= <Buffer 22 b0 af e7 8c 2e 02 eb 73 f1 f1 d0 28 c0 a3 4b c5 cf 46 d1 82 f2 3d 12 32 d0 bf 95 e4 13 61 46 f1 79 32 ce 10 b9 ce 5d 8d 4e e6 0c d8 5c 3c 67 e0 96 ... >
OK? dec2= <Buffer 00 00 00 00 00 00 00 00 00 00>
-----BEGIN RSA PRIVATE KEY-----
MIICXAIBAAKBgQCG1jRP2NgtM3UeBJW8qKAkMMig08a3c/7BgYwFJXXxHqnFPnXF
BCrVZaO0LtPLmeV3HeqtK9W67TyBe/FMgXLdZ6cpxOco4NLBWp/7QP2MwVesf9g/
fm4LZIxeN9GXM/HIQOvbObyKRmd30CKHxjWkbnMTi72SG+Rc2RqhPJJPvwIDAQAB
AoGAV9VDQFwt/cvOV95+t+VUZB7PIkyx3qEV63F7B4MugAIMbytPxiX/zQCnkeEL
IE7AtkZrr6ClWl3dky9ssPyGGJJIVr7byMU1VdJlse2DIfIXW4kl+jo/MipXzb4I
dr1D/AT043WS7hpyDKNDyZzmf4dwJsp9cpOm/AqUz+LfdIECQQDlh0+EuihKpaYs
y4vtlHHWjZJPoLQQAtk57NnMJaqmEYuQYCnYs7RAuGoCRSc6DdgukZp+wSZ0WBn3
u0sSHTb/AkEAlmMtDN8y6nzLZvGzxQDgNxyzkNrSwmFb7fAa0+o2Pea0ps1ByrpX
vOuebwze69kk5bldyVM+npwQZ1xiKrmnQQJBAKEXMmwI6zZYxCQ0R2TbBnp6qfFQ
7I9AMI1C+ikZVodvUPBnTXdVyHCT/XLSbhGEnfExJ6lGjmKhYrhHrwxrjKkCQF+6
T7Hy3eE/gOZNksYjUZYjUfYyJJiRCsiB30Hnw5FRqsrGu0uFpFXgkeBUjA4LEh6d
CSMfNywVYae5uc9CkEECQH1xO+qrkWeGrrX3JaKjRmo13mYMfO9wwNOtINrlb9so
1om826G3vYPX4qrcWYC4pr0t8/7hN+4P80UNEnHCiac=
-----END RSA PRIVATE KEY-----
<Buffer 30 82 02 5c 02 01 00 02 81 81 00 86 d6 34 4f d8 d8 2d 33 75 1e 04 95 bc a8 a0 24 30 c8 a0 d3 c6 b7 73 fe c1 81 8c 05 25 75 f1 1e a9 c5 3e 75 c5 04 2a ... >
-----BEGIN RSA PUBLIC KEY-----
MIGJAoGBAIbWNE/Y2C0zdR4ElbyooCQwyKDTxrdz/sGBjAUldfEeqcU+dcUEKtVl
o7Qu08uZ5Xcd6q0r1brtPIF78UyBct1npynE5yjg0sFan/tA/YzBV6x/2D9+bgtk
jF430Zcz8chA69s5vIpGZ3fQIofGNaRucxOLvZIb5FzZGqE8kk+/AgMBAAE=
-----END RSA PUBLIC KEY-----
<Buffer 30 81 89 02 81 81 00 86 d6 34 4f d8 d8 2d 33 75 1e 04 95 bc a8 a0 24 30 c8 a0 d3 c6 b7 73 fe c1 81 8c 05 25 75 f1 1e a9 c5 3e 75 c5 04 2a d5 65 a3 b4 ... >
sig= <Buffer 08 9f 2c 45 f2 e9 b4 8a f6 1e 88 e4 16 71 1a 07 18 fc 36 a5 8b d0 66 7d c6 b6 bc ff cf 51 b2 ca 5f d7 bb 7c 86 2e a0 be 97 15 4a 85 f6 f8 46 83 f2 db ... >
result= true

当然ながら、実行ごとに鍵ペアの鍵値や暗号文の値は異なります。

browserify化する

まだbrowserifyをインストールしていない場合は、インストールしておきます。

npm install browserify -g

それではJavascriptのモジュールを作成します。

browserify -r ./main.js:node-rsa-js -o node-rsa-js.js

これで、node-rsa-js.jsというJavascriptファイルが出来上がりました。-rで指定した「node-rsa-js」がJavascriptからrequireする名前になります。

ブラウザから使ってみる

以下のようなサンプルHTMLファイルを作成します。

index.html
<html>
  <head>
  </head>
  <body>
    <script src="node-rsa-js.js"></script>
    <script>
      var rsab = require('node-rsa-js');
      var key = rsab.generateKeyPair(1024);
      console.log('private=', key.private);
      console.log('public=', key.public);

      var buffer = new Uint8Array(10);

      var enc2 = rsab.publicEncrypt(key.public, buffer);
      console.log('enc2=', enc2);
      var dec2 = rsab.privateDecrypt(key.private, enc2);
      console.log('OK? dec2=', dec2);

      var pem = rsab.der2pem('pkcs1-private', key.private);
      console.log(pem);
      var der = rsab.pem2der('pkcs1-private', pem);
      console.log(der);
      var pem = rsab.der2pem('pkcs1-public', key.public);
      console.log(pem);
      var der = rsab.pem2der('pkcs1-public', pem);
      console.log(der);

      var sig = rsab.sign(key.private, 'Hello World');
      console.log('sig=', sig);

      var result = rsab.verify(key.public, 'Hello World', sig);
      console.log('result=', result);
    </script>
  </body>
</html>

このindex.htmlと同じフォルダに先ほど作成したnode-rsa-js.jsを置きます。

早速、ブラウザから表示してみましょう。その時、各ブラウザのConsoleをのぞいてみてください。
以下のような感じで表示されれば成功です。

private= Uint8Array(609) [48, 130, 2, 93, 2, 1, 0, 2, 129, 129, 0, 145, 2, 86, 33, 88, 50, 157, 251, 230, 51, 254, 9, 56, 64, 126, 169, 20, 2, 90, 3, 220, 32, 203, 53, 160, 62, 57, 209, 184, 183, 216, 203, 55, 131, 173, 205, 41, 174, 161, 16, 8, 17, 240, 238, 164, 117, 161, 187, 111, 10, 85, 116, 163, 25, 228, 14, 233, 56, 203, 16, 14, 234, 62, 49, 183, 171, 82, 140, 225, 101, 207, 84, 92, 47, 222, 212, 69, 102, 196, 226, 197, 38, 106, 237, 73, 6, 228, 221, 36, …]
(index):10 public= Uint8Array(140) [48, 129, 137, 2, 129, 129, 0, 145, 2, 86, 33, 88, 50, 157, 251, 230, 51, 254, 9, 56, 64, 126, 169, 20, 2, 90, 3, 220, 32, 203, 53, 160, 62, 57, 209, 184, 183, 216, 203, 55, 131, 173, 205, 41, 174, 161, 16, 8, 17, 240, 238, 164, 117, 161, 187, 111, 10, 85, 116, 163, 25, 228, 14, 233, 56, 203, 16, 14, 234, 62, 49, 183, 171, 82, 140, 225, 101, 207, 84, 92, 47, 222, 212, 69, 102, 196, 226, 197, 38, 106, 237, 73, 6, 228, 221, 36, 198, 25, 31, 221, …]
(index):15 enc2= Uint8Array(128) [8, 132, 229, 218, 1, 167, 51, 85, 250, 19, 215, 124, 177, 59, 76, 160, 142, 151, 103, 170, 200, 101, 222, 190, 148, 79, 70, 88, 63, 92, 227, 244, 220, 185, 117, 131, 98, 113, 73, 130, 247, 156, 254, 244, 198, 154, 109, 12, 135, 158, 125, 88, 186, 121, 244, 194, 18, 1, 180, 236, 159, 169, 51, 170, 195, 172, 166, 178, 122, 230, 209, 78, 45, 122, 16, 126, 2, 156, 65, 215, 100, 22, 63, 211, 127, 220, 51, 173, 106, 232, 80, 109, 228, 250, 245, 32, 137, 217, 236, 96, …]
(index):17 OK? dec2= Uint8Array(10) [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
(index):20 -----BEGIN RSA PRIVATE KEY-----
MIICXQIBAAKBgQCRAlYhWDKd++Yz/gk4QH6pFAJaA9wgyzWgPjnRuLfYyzeDrc0p
rqEQCBHw7qR1obtvClV0oxnkDuk4yxAO6j4xt6tSjOFlz1RcL97URWbE4sUmau1J
BuTdJMYZH91cLvedEwc5lc7SNHrmZytOaE4jxFACXdi9tHyu8fENhL4x1QIDAQAB
AoGAQassNDeL3K3J53vA0x+p/InaMseStasxIttrNcWQRHZrMo/P3HN/7xGohlKc
WcUfa77jSknenL//8D9Ni2ObPBstwhAIVu/n6UEOL19XSkTxGKExboI3zXXX8Uc5
5NTz6UQ12BPr4/cOmu19pAMuSRCA4QF/nfv3hpRGWNuWspUCQQDWcom+nT2DVzsx
B1mfPjV3d95049+p/WiWyRbxBh+G4MZs1PShT2cuchJNumrcgjZU6BFo9StSdSS8
DlVpa75rAkEArRtgpzBNLYqoJGBsq8bRZzgDlU72a7qYnxbmz/mBjQHIC4MUk3FD
rrrGfMBlQoK8poQBL9BZiPYNEB+N+JlgvwJBAJh8X3f8BUaEW6GUUWULbidiQ/uo
IV2VxK4blUWTjg1xfYbbsouVk5ASKvO8T8o2iP28+sxAMSr0A0f5hUBuDbsCQHbI
NHhEkpDPdjUP3UG5uXLkYsEPX9PoRFXV9yd6g8ToFgagOXw62kCJdS2hL1qGL0Dr
j4zpoKZ0f94yaM7PIC0CQQDL1VPh6oJ9MSDjoVGjVcjpxffXe/fNS/nnqRTn6lmZ
VT6KNQ+KKenp5RBqlYJjS2S9Jhnx1by3bdZOJlkIY34k
-----END RSA PRIVATE KEY-----
(index):22 Uint8Array(609) [48, 130, 2, 93, 2, 1, 0, 2, 129, 129, 0, 145, 2, 86, 33, 88, 50, 157, 251, 230, 51, 254, 9, 56, 64, 126, 169, 20, 2, 90, 3, 220, 32, 203, 53, 160, 62, 57, 209, 184, 183, 216, 203, 55, 131, 173, 205, 41, 174, 161, 16, 8, 17, 240, 238, 164, 117, 161, 187, 111, 10, 85, 116, 163, 25, 228, 14, 233, 56, 203, 16, 14, 234, 62, 49, 183, 171, 82, 140, 225, 101, 207, 84, 92, 47, 222, 212, 69, 102, 196, 226, 197, 38, 106, 237, 73, 6, 228, 221, 36, …]
(index):24 -----BEGIN RSA PUBLIC KEY-----
MIGJAoGBAJECViFYMp375jP+CThAfqkUAloD3CDLNaA+OdG4t9jLN4OtzSmuoRAI
EfDupHWhu28KVXSjGeQO6TjLEA7qPjG3q1KM4WXPVFwv3tRFZsTixSZq7UkG5N0k
xhkf3Vwu950TBzmVztI0euZnK05oTiPEUAJd2L20fK7x8Q2EvjHVAgMBAAE=
-----END RSA PUBLIC KEY-----
(index):26 Uint8Array(140) [48, 129, 137, 2, 129, 129, 0, 145, 2, 86, 33, 88, 50, 157, 251, 230, 51, 254, 9, 56, 64, 126, 169, 20, 2, 90, 3, 220, 32, 203, 53, 160, 62, 57, 209, 184, 183, 216, 203, 55, 131, 173, 205, 41, 174, 161, 16, 8, 17, 240, 238, 164, 117, 161, 187, 111, 10, 85, 116, 163, 25, 228, 14, 233, 56, 203, 16, 14, 234, 62, 49, 183, 171, 82, 140, 225, 101, 207, 84, 92, 47, 222, 212, 69, 102, 196, 226, 197, 38, 106, 237, 73, 6, 228, 221, 36, 198, 25, 31, 221, …]
(index):29 sig= Uint8Array(128) [80, 85, 255, 196, 23, 196, 40, 61, 23, 239, 13, 112, 221, 241, 216, 229, 45, 164, 241, 41, 212, 252, 55, 15, 86, 107, 42, 245, 86, 152, 155, 134, 37, 37, 255, 72, 93, 191, 63, 149, 144, 207, 72, 64, 80, 212, 92, 6, 74, 116, 62, 32, 25, 142, 155, 200, 42, 33, 31, 155, 163, 232, 219, 121, 99, 175, 164, 132, 140, 121, 145, 242, 147, 191, 90, 131, 195, 48, 193, 75, 3, 157, 98, 83, 161, 206, 221, 44, 132, 16, 144, 143, 103, 59, 233, 4, 87, 235, 15, 198, …]
(index):32 result= true

関数名を見れば、だいたい処理内容は理解いただけると思います。

1点だけ注意ですが、publicEncryptとprivateDecryptで指定する公開鍵はDER形式としています。
私がDER形式を使うことが多いためこのようにしていますが、PEM形式とすることは簡単だと思いますので、適宜修正してください。

(2019/3/2 追記)
言うのを忘れましたが、node-rsa-js.jsのファイルサイズは「でかい」です。768KBです。

以上

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

Javascriptで公開鍵ペア生成・暗号化/復号をしてみた

node.jsでRSA公開鍵を使った鍵ペア生成や暗号化/復号をする例はたくさんあったのですが、javascriptでの例はあまりなかったのでやってみました。
途中、はまってしまったので、備忘録として残しておきます。

とはいいつつも、なんてことはない、browserifyを使っただけです。。。
使わせてもらったnpmモジュールは「node-rsa」です。Pure Javascriptという特徴に惹かれました。

rzcoder/node-rsa
 https://github.com/rzcoder/node-rsa

(2019/3/2 追記)
せっかくなので、署名付与・署名検証も追加しておきました。

作成するモジュールの仕様

ブラウザ上のJavascriptから以下関数を呼び出せるモジュールとします。

  • RSA公開鍵ペアの生成
  • 公開鍵による暗号化
  • 秘密鍵による復号
  • 秘密鍵による署名付与
  • 公開鍵による署名検証
  • PEM形式をDER形式に変換
  • DER形式をPEM形式に変換

ちなみに、暗号化時のパディング方式は「RSA_PKCS1_OAEP_PADDING」とします。PKCS#1 v2.0 with SHA-1のことです。

node.jsでモジュールを作成する

まずは、node.jsで期待する動作となることを確認しましょう。

npm init -y
npm install --save node-rsa
vi main.js

main.js
const NodeRSA = require('node-rsa');

function generateKeyPair(bits){
  const key = new NodeRSA({b: bits});

  var priv = key.exportKey('pkcs1-private-der');
  var pub = key.exportKey('pkcs1-public-der');

  return { public: pub, private: priv };
}

function pem2der(scheme, pem){
  const key = new NodeRSA(pem, scheme + '-pem');
  return key.exportKey(scheme + '-der');
}

function der2pem(scheme, key){
  var der = Buffer.from(key);
  const rsa = new NodeRSA(der, scheme + '-der');
  return rsa.exportKey(scheme + '-pem');
}

function publicEncrypt(key, buffer){
  var input = Buffer.from(buffer);
  var der = Buffer.from(key);
  const rsa = new NodeRSA(der, 'pkcs1-public-der', { encryptionScheme : 'pkcs1_oaep' });
  var enc = rsa.encrypt(input);
  return enc;
}

function privateDecrypt(key, buffer){
  var input = Buffer.from(buffer);
  var der = Buffer.from(key);
  const rsa = new NodeRSA(der, 'pkcs1-private-der', { encryptionScheme : 'pkcs1_oaep' });
  var dec = rsa.decrypt(input);
  return dec;
}

function sign(key, buffer){
  var input = Buffer.from(buffer);
  var der = Buffer.from(key);
  const rsa = new NodeRSA(der, 'pkcs1-private-der');
  var sig = rsa.sign(input);
  return sig;
}

function verify(key, buffer, signature){
  var input = Buffer.from(buffer);
  var der = Buffer.from(key);
  var sig = Buffer.from(signature)
  const rsa = new NodeRSA(der, 'pkcs1-public-der');
  var result = rsa.verify(input, sig);
  return result;
}

module.exports = {
  generateKeyPair: generateKeyPair,
  publicEncrypt: publicEncrypt,
  privateDecrypt: privateDecrypt,
  sign: sign,
  verify: verify,
  der2pem: der2pem,
  pem2der: pem2der,
};

ちなみに、はまった点を示しておきます。

publicEncryptやprivateDecryptでは、以下のような一見不要な処理をしています。ですが、なぜかこの処理を入れないと、ブラウザのJavascriptから呼び出したときにうまく復号してくれなかったんです。原因不明です。とりあえず動いているのでいいですが。。

var input = Buffer.from(buffer);

次は、それを呼び出すサンプルコードです。

index.js
var rsab = require('./main');

var key = rsab.generateKeyPair(1024);
console.log('private=', key.private);
console.log('public=', key.public);

var buffer = new Uint8Array(10);

var enc2 = rsab.publicEncrypt(key.public, buffer);
console.log('enc2=', enc2);
var dec2 = rsab.privateDecrypt(key.private, enc2);
console.log('OK? dec2=', dec2);

var pem = rsab.der2pem('pkcs1-private', key.private);
console.log(pem);
var der = rsab.pem2der('pkcs1-private', pem);
console.log(der);
var pem = rsab.der2pem('pkcs1-public', key.public);
console.log(pem);
var der = rsab.pem2der('pkcs1-public', pem);
console.log(der);

var sig = rsab.sign(key.private, 'Hello World');
console.log('sig=', sig);

var result = rsab.verify(key.public, 'Hello World', sig);
console.log('result=', result);

以下のような感じで出力されましたでしょうか?

private= <Buffer 30 82 02 5c 02 01 00 02 81 81 00 86 d6 34 4f d8 d8 2d 33 75 1e 04 95 bc a8 a0 24 30 c8 a0 d3 c6 b7 73 fe c1 81 8c 05 25 75 f1 1e a9 c5 3e 75 c5 04 2a ... >
public= <Buffer 30 81 89 02 81 81 00 86 d6 34 4f d8 d8 2d 33 75 1e 04 95 bc a8 a0 24 30 c8 a0 d3 c6 b7 73 fe c1 81 8c 05 25 75 f1 1e a9 c5 3e 75 c5 04 2a d5 65 a3 b4 ... >
enc2= <Buffer 22 b0 af e7 8c 2e 02 eb 73 f1 f1 d0 28 c0 a3 4b c5 cf 46 d1 82 f2 3d 12 32 d0 bf 95 e4 13 61 46 f1 79 32 ce 10 b9 ce 5d 8d 4e e6 0c d8 5c 3c 67 e0 96 ... >
OK? dec2= <Buffer 00 00 00 00 00 00 00 00 00 00>
-----BEGIN RSA PRIVATE KEY-----
MIICXAIBAAKBgQCG1jRP2NgtM3UeBJW8qKAkMMig08a3c/7BgYwFJXXxHqnFPnXF
BCrVZaO0LtPLmeV3HeqtK9W67TyBe/FMgXLdZ6cpxOco4NLBWp/7QP2MwVesf9g/
fm4LZIxeN9GXM/HIQOvbObyKRmd30CKHxjWkbnMTi72SG+Rc2RqhPJJPvwIDAQAB
AoGAV9VDQFwt/cvOV95+t+VUZB7PIkyx3qEV63F7B4MugAIMbytPxiX/zQCnkeEL
IE7AtkZrr6ClWl3dky9ssPyGGJJIVr7byMU1VdJlse2DIfIXW4kl+jo/MipXzb4I
dr1D/AT043WS7hpyDKNDyZzmf4dwJsp9cpOm/AqUz+LfdIECQQDlh0+EuihKpaYs
y4vtlHHWjZJPoLQQAtk57NnMJaqmEYuQYCnYs7RAuGoCRSc6DdgukZp+wSZ0WBn3
u0sSHTb/AkEAlmMtDN8y6nzLZvGzxQDgNxyzkNrSwmFb7fAa0+o2Pea0ps1ByrpX
vOuebwze69kk5bldyVM+npwQZ1xiKrmnQQJBAKEXMmwI6zZYxCQ0R2TbBnp6qfFQ
7I9AMI1C+ikZVodvUPBnTXdVyHCT/XLSbhGEnfExJ6lGjmKhYrhHrwxrjKkCQF+6
T7Hy3eE/gOZNksYjUZYjUfYyJJiRCsiB30Hnw5FRqsrGu0uFpFXgkeBUjA4LEh6d
CSMfNywVYae5uc9CkEECQH1xO+qrkWeGrrX3JaKjRmo13mYMfO9wwNOtINrlb9so
1om826G3vYPX4qrcWYC4pr0t8/7hN+4P80UNEnHCiac=
-----END RSA PRIVATE KEY-----
<Buffer 30 82 02 5c 02 01 00 02 81 81 00 86 d6 34 4f d8 d8 2d 33 75 1e 04 95 bc a8 a0 24 30 c8 a0 d3 c6 b7 73 fe c1 81 8c 05 25 75 f1 1e a9 c5 3e 75 c5 04 2a ... >
-----BEGIN RSA PUBLIC KEY-----
MIGJAoGBAIbWNE/Y2C0zdR4ElbyooCQwyKDTxrdz/sGBjAUldfEeqcU+dcUEKtVl
o7Qu08uZ5Xcd6q0r1brtPIF78UyBct1npynE5yjg0sFan/tA/YzBV6x/2D9+bgtk
jF430Zcz8chA69s5vIpGZ3fQIofGNaRucxOLvZIb5FzZGqE8kk+/AgMBAAE=
-----END RSA PUBLIC KEY-----
<Buffer 30 81 89 02 81 81 00 86 d6 34 4f d8 d8 2d 33 75 1e 04 95 bc a8 a0 24 30 c8 a0 d3 c6 b7 73 fe c1 81 8c 05 25 75 f1 1e a9 c5 3e 75 c5 04 2a d5 65 a3 b4 ... >
sig= <Buffer 08 9f 2c 45 f2 e9 b4 8a f6 1e 88 e4 16 71 1a 07 18 fc 36 a5 8b d0 66 7d c6 b6 bc ff cf 51 b2 ca 5f d7 bb 7c 86 2e a0 be 97 15 4a 85 f6 f8 46 83 f2 db ... >
result= true

当然ながら、実行ごとに鍵ペアの鍵値や暗号文の値は異なります。

browserify化する

まだbrowserifyをインストールしていない場合は、インストールしておきます。

npm install browserify -g

それではJavascriptのモジュールを作成します。

browserify -r ./main.js:node-rsa-js -o node-rsa-js.js

これで、node-rsa-js.jsというJavascriptファイルが出来上がりました。-rで指定した「node-rsa-js」がJavascriptからrequireする名前になります。

ブラウザから使ってみる

以下のようなサンプルHTMLファイルを作成します。

index.html
<html>
  <head>
  </head>
  <body>
    <script src="node-rsa-js.js"></script>
    <script>
      var rsab = require('node-rsa-js');
      var key = rsab.generateKeyPair(1024);
      console.log('private=', key.private);
      console.log('public=', key.public);

      var buffer = new Uint8Array(10);

      var enc2 = rsab.publicEncrypt(key.public, buffer);
      console.log('enc2=', enc2);
      var dec2 = rsab.privateDecrypt(key.private, enc2);
      console.log('OK? dec2=', dec2);

      var pem = rsab.der2pem('pkcs1-private', key.private);
      console.log(pem);
      var der = rsab.pem2der('pkcs1-private', pem);
      console.log(der);
      var pem = rsab.der2pem('pkcs1-public', key.public);
      console.log(pem);
      var der = rsab.pem2der('pkcs1-public', pem);
      console.log(der);

      var sig = rsab.sign(key.private, 'Hello World');
      console.log('sig=', sig);

      var result = rsab.verify(key.public, 'Hello World', sig);
      console.log('result=', result);
    </script>
  </body>
</html>

このindex.htmlと同じフォルダに先ほど作成したnode-rsa-js.jsを置きます。

早速、ブラウザから表示してみましょう。その時、各ブラウザのConsoleをのぞいてみてください。
以下のような感じで表示されれば成功です。

private= Uint8Array(609) [48, 130, 2, 93, 2, 1, 0, 2, 129, 129, 0, 145, 2, 86, 33, 88, 50, 157, 251, 230, 51, 254, 9, 56, 64, 126, 169, 20, 2, 90, 3, 220, 32, 203, 53, 160, 62, 57, 209, 184, 183, 216, 203, 55, 131, 173, 205, 41, 174, 161, 16, 8, 17, 240, 238, 164, 117, 161, 187, 111, 10, 85, 116, 163, 25, 228, 14, 233, 56, 203, 16, 14, 234, 62, 49, 183, 171, 82, 140, 225, 101, 207, 84, 92, 47, 222, 212, 69, 102, 196, 226, 197, 38, 106, 237, 73, 6, 228, 221, 36, …]
(index):10 public= Uint8Array(140) [48, 129, 137, 2, 129, 129, 0, 145, 2, 86, 33, 88, 50, 157, 251, 230, 51, 254, 9, 56, 64, 126, 169, 20, 2, 90, 3, 220, 32, 203, 53, 160, 62, 57, 209, 184, 183, 216, 203, 55, 131, 173, 205, 41, 174, 161, 16, 8, 17, 240, 238, 164, 117, 161, 187, 111, 10, 85, 116, 163, 25, 228, 14, 233, 56, 203, 16, 14, 234, 62, 49, 183, 171, 82, 140, 225, 101, 207, 84, 92, 47, 222, 212, 69, 102, 196, 226, 197, 38, 106, 237, 73, 6, 228, 221, 36, 198, 25, 31, 221, …]
(index):15 enc2= Uint8Array(128) [8, 132, 229, 218, 1, 167, 51, 85, 250, 19, 215, 124, 177, 59, 76, 160, 142, 151, 103, 170, 200, 101, 222, 190, 148, 79, 70, 88, 63, 92, 227, 244, 220, 185, 117, 131, 98, 113, 73, 130, 247, 156, 254, 244, 198, 154, 109, 12, 135, 158, 125, 88, 186, 121, 244, 194, 18, 1, 180, 236, 159, 169, 51, 170, 195, 172, 166, 178, 122, 230, 209, 78, 45, 122, 16, 126, 2, 156, 65, 215, 100, 22, 63, 211, 127, 220, 51, 173, 106, 232, 80, 109, 228, 250, 245, 32, 137, 217, 236, 96, …]
(index):17 OK? dec2= Uint8Array(10) [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
(index):20 -----BEGIN RSA PRIVATE KEY-----
MIICXQIBAAKBgQCRAlYhWDKd++Yz/gk4QH6pFAJaA9wgyzWgPjnRuLfYyzeDrc0p
rqEQCBHw7qR1obtvClV0oxnkDuk4yxAO6j4xt6tSjOFlz1RcL97URWbE4sUmau1J
BuTdJMYZH91cLvedEwc5lc7SNHrmZytOaE4jxFACXdi9tHyu8fENhL4x1QIDAQAB
AoGAQassNDeL3K3J53vA0x+p/InaMseStasxIttrNcWQRHZrMo/P3HN/7xGohlKc
WcUfa77jSknenL//8D9Ni2ObPBstwhAIVu/n6UEOL19XSkTxGKExboI3zXXX8Uc5
5NTz6UQ12BPr4/cOmu19pAMuSRCA4QF/nfv3hpRGWNuWspUCQQDWcom+nT2DVzsx
B1mfPjV3d95049+p/WiWyRbxBh+G4MZs1PShT2cuchJNumrcgjZU6BFo9StSdSS8
DlVpa75rAkEArRtgpzBNLYqoJGBsq8bRZzgDlU72a7qYnxbmz/mBjQHIC4MUk3FD
rrrGfMBlQoK8poQBL9BZiPYNEB+N+JlgvwJBAJh8X3f8BUaEW6GUUWULbidiQ/uo
IV2VxK4blUWTjg1xfYbbsouVk5ASKvO8T8o2iP28+sxAMSr0A0f5hUBuDbsCQHbI
NHhEkpDPdjUP3UG5uXLkYsEPX9PoRFXV9yd6g8ToFgagOXw62kCJdS2hL1qGL0Dr
j4zpoKZ0f94yaM7PIC0CQQDL1VPh6oJ9MSDjoVGjVcjpxffXe/fNS/nnqRTn6lmZ
VT6KNQ+KKenp5RBqlYJjS2S9Jhnx1by3bdZOJlkIY34k
-----END RSA PRIVATE KEY-----
(index):22 Uint8Array(609) [48, 130, 2, 93, 2, 1, 0, 2, 129, 129, 0, 145, 2, 86, 33, 88, 50, 157, 251, 230, 51, 254, 9, 56, 64, 126, 169, 20, 2, 90, 3, 220, 32, 203, 53, 160, 62, 57, 209, 184, 183, 216, 203, 55, 131, 173, 205, 41, 174, 161, 16, 8, 17, 240, 238, 164, 117, 161, 187, 111, 10, 85, 116, 163, 25, 228, 14, 233, 56, 203, 16, 14, 234, 62, 49, 183, 171, 82, 140, 225, 101, 207, 84, 92, 47, 222, 212, 69, 102, 196, 226, 197, 38, 106, 237, 73, 6, 228, 221, 36, …]
(index):24 -----BEGIN RSA PUBLIC KEY-----
MIGJAoGBAJECViFYMp375jP+CThAfqkUAloD3CDLNaA+OdG4t9jLN4OtzSmuoRAI
EfDupHWhu28KVXSjGeQO6TjLEA7qPjG3q1KM4WXPVFwv3tRFZsTixSZq7UkG5N0k
xhkf3Vwu950TBzmVztI0euZnK05oTiPEUAJd2L20fK7x8Q2EvjHVAgMBAAE=
-----END RSA PUBLIC KEY-----
(index):26 Uint8Array(140) [48, 129, 137, 2, 129, 129, 0, 145, 2, 86, 33, 88, 50, 157, 251, 230, 51, 254, 9, 56, 64, 126, 169, 20, 2, 90, 3, 220, 32, 203, 53, 160, 62, 57, 209, 184, 183, 216, 203, 55, 131, 173, 205, 41, 174, 161, 16, 8, 17, 240, 238, 164, 117, 161, 187, 111, 10, 85, 116, 163, 25, 228, 14, 233, 56, 203, 16, 14, 234, 62, 49, 183, 171, 82, 140, 225, 101, 207, 84, 92, 47, 222, 212, 69, 102, 196, 226, 197, 38, 106, 237, 73, 6, 228, 221, 36, 198, 25, 31, 221, …]
(index):29 sig= Uint8Array(128) [80, 85, 255, 196, 23, 196, 40, 61, 23, 239, 13, 112, 221, 241, 216, 229, 45, 164, 241, 41, 212, 252, 55, 15, 86, 107, 42, 245, 86, 152, 155, 134, 37, 37, 255, 72, 93, 191, 63, 149, 144, 207, 72, 64, 80, 212, 92, 6, 74, 116, 62, 32, 25, 142, 155, 200, 42, 33, 31, 155, 163, 232, 219, 121, 99, 175, 164, 132, 140, 121, 145, 242, 147, 191, 90, 131, 195, 48, 193, 75, 3, 157, 98, 83, 161, 206, 221, 44, 132, 16, 144, 143, 103, 59, 233, 4, 87, 235, 15, 198, …]
(index):32 result= true

関数名を見れば、だいたい処理内容は理解いただけると思います。

1点だけ注意ですが、publicEncryptとprivateDecryptで指定する公開鍵はDER形式としています。
私がDER形式を使うことが多いためこのようにしていますが、PEM形式とすることは簡単だと思いますので、適宜修正してください。

(2019/3/2 追記)
言うのを忘れましたが、node-rsa-js.jsのファイルサイズは「でかい」です。768KBです。

以上

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

React Nativeでピアノを作る

はじめに

React Nativeでピアノを作ったのでソースコードの説明をします。
Githubのリポジトリはこちらです。

プロジェクトの立ち上げ

react-nativeのコマンドでプロジェクトを作ります。

react-native init piano
cd piano

ピアノ音源の準備

mp3形式で音源を準備します。
Githubのリポジトリにアップロードしているので、こちらをお使いください。

audio/
├── A.mp3
├── As.mp3
├── B.mp3
├── C.mp3
├── Cs.mp3
├── D.mp3
├── Ds.mp3
├── E.mp3
├── F.mp3
├── Fs.mp3
├── G.mp3
└── Gs.mp3

react-native-soundのインストール

React Nativeで音を鳴らすためのライブラリreact-native-soundをインストールします。

yarn add react-native-sound
react-native link react-native-sound

iOSではxcode上でプロジェクトファイルの中に音源をドラッグ&ドロップします。
Screen Shot 2019-02-27 at 18.29.57.png

Androidでは音源を android/app/src/main/res/raw のフォルダに入れます。

mkdir android/app/src/main/res/raw
cp audio/* android/app/src/main/res/raw/

ソースコード

ソースコードは以下になります。
順番に説明していきます。

App.jsx
import React from 'react';
import {
  StyleSheet,
  Text,
  TouchableOpacity,
  View,
} from 'react-native';

import Sound from 'react-native-sound';

export default class App extends React.Component {
  constructor( props ){
    super( props );

    this.state = {
      colorC : "white",
      colorCs: "black",
      colorD : "white",
      colorDs: "black",
      colorE : "white",
      colorF : "white",
      colorFs: "black",
      colorG : "white",
      colorGs: "black",
      colorA : "white",
      colorAs: "black",
      colorB : "white",
    }

    this.sound = {};
    const soundList = [ "C", "Cs", "D", "Ds", "E", "F", "Fs", "G", "Gs", "A", "As", "B" ]
    soundList.forEach(note => {
      this.sound[note] = new Sound( note + ".mp3", Sound.MAIN_BUNDLE, error => {
        if ( error ) {
          console.log("failed to load the sound.", error);
        }
      })
    });
  }
  stroke ( note ) {
    switch ( note ) {
      case "C":
        this.setState({ colorC: "rgba(1, 1, 1, 0.1)" })
        break;
      case "Cs":
        this.setState({ colorCs: "rgba(0, 0, 0, 0.5)" })
        break;
      case "D":
        this.setState({ colorD: "rgba(1, 1, 1, 0.1)" })
        break;
      case "Ds":
        this.setState({ colorDs: "rgba(0, 0, 0, 0.5)" })
        break;
      case "E":
        this.setState({ colorE: "rgba(1, 1, 1, 0.1)" })
        break;
      case "F":
        this.setState({ colorF: "rgba(1, 1, 1, 0.1)" })
        break;
      case "Fs":
        this.setState({ colorFs: "rgba(0, 0, 0, 0.5)" })
        break;
      case "G":
        this.setState({ colorG: "rgba(1, 1, 1, 0.1)" })
        break;
      case "Gs":
        this.setState({ colorGs: "rgba(0, 0, 0, 0.5)" })
        break;
      case "A":
        this.setState({ colorA: "rgba(1, 1, 1, 0.1)" })
        break;
      case "As":
        this.setState({ colorAs: "rgba(0, 0, 0, 0.5)" })
        break;
      case "B":
        this.setState({ colorB: "rgba(1, 1, 1, 0.1)" })
        break;
    }
    setTimeout( () => {
      this.sound[note].play(success => {
        if ( success ) {
          console.log("successfully finished playing.");
        } else {
          console.log("failed to play the sound.");
        }
      });
    }, 1);
  }
  stop( note ) {
    switch ( note ) {
      case "C":
        this.setState( { colorC: "white" } )
        break;
      case "Cs":
        this.setState( { colorCs: "black" } )
        break;
      case "D":
        this.setState( { colorD: "white" } )
        break;
      case "Ds":
        this.setState( { colorDs: "black" } )
        break;
      case "E":
        this.setState( { colorE: "white" } )
        break;
      case "F":
        this.setState( { colorF: "white" } )
        break;
      case "Fs":
        this.setState( { colorFs: "black" } )
        break;
      case "G":
        this.setState( { colorG: "white" } )
        break;
      case "Gs":
        this.setState( { colorGs: "black" } )
        break;
      case "A":
        this.setState( { colorA: "white" } )
        break;
      case "As":
        this.setState( { colorAs: "black" } )
        break;
      case "B":
        this.setState( { colorB: "white" } )
        break;
    }
    setTimeout( () => {
      for (let i=0; i<2000; i++) {
        this.sound[note].setVolume( 1.0-i/2000. );
      }
      this.sound[note].stop();
      this.sound[note].setVolume( 1.0 );
    }, 1 )
  }
  render () {
    return (
      <View style={styles.container}>
        <View style={{ flex: 1, flexDirection: "column", alignItems: "center" }}>
          <View style={{ flexDirection : "row", alignItems: "center", justifyContent: "center" }}>

            <View
              style={{ backgroundColor: "white", height: 100, width: 32, borderLeftWidth: 1, borderTopWidth: 1,}} >
            </View >
            <View
              onTouchStart={() => this.stroke("Cs")}
              onTouchEnd={() => this.stop("Cs")}
              style={{ backgroundColor: this.state.colorCs, height: 100, width: 32, borderTopWidth: 1, borderLeftWidth: 1,}} >
            </View >
            <View
              style={{ backgroundColor: "white", height: 100, width: 16, borderTopWidth: 1, }} >
            </View >
            <View
              onTouchStart={() => this.stroke("Ds")}
              onTouchEnd={() => this.stop("Ds")}
              style={{ backgroundColor: this.state.colorDs, height: 100, width: 32, borderTopWidth: 1, borderLeftWidth: 1,}} >
            </View >
            <View
              style={{ backgroundColor: "white", height: 100, width: 32, borderTopWidth: 1, }} >
            </View >
            <View
              style={{ backgroundColor: "white", height: 100, width: 32, borderTopWidth: 1, borderLeftWidth: 1, }} >
            </View >
            <View
              onTouchStart={() => this.stroke("Fs")}
              onTouchEnd={() => this.stop("Fs")}
              style={{ backgroundColor: this.state.colorFs, height: 100, width: 32, borderTopWidth: 1, }} >
            </View >
            <View
              style={{ backgroundColor: "white", height: 100, width: 16, borderTopWidth: 1, }} >
            </View >
            <View
              onTouchStart={() => this.stroke("Gs")}
              onTouchEnd={() => this.stop("Gs")}
              style={{ backgroundColor: this.state.colorGs, height: 100, width: 32, borderTopWidth: 1, }} >
            </View >
            <View
              style={{ backgroundColor: "white", height: 100, width: 16, borderTopWidth: 1, }} >
            </View >
            <View
              onTouchStart={() => this.stroke("As")}
              onTouchEnd={() => this.stop("As")}
              style={{ backgroundColor: this.state.colorAs, height: 100, width: 32, borderTopWidth: 1, }} >
            </View >
            <View
              style={{ backgroundColor: "white", height: 100, width: 32, borderRightWidth: 1, borderTopWidth: 1, }} >
            </View >

          </View>

          <View style={{ flexDirection : "row", alignItems: "center", justifyContent: "center" }}>

            <View
              onTouchStart={() => this.stroke("C")}
              onTouchEnd={() => this.stop("C")}
              style={{ backgroundColor: this.state.colorC, height: 100, width: 48, borderBottomWidth: 1, borderLeftWidth: 1 }} >
            </View >
            <View
              onTouchStart={() => this.stroke("D")}
              onTouchEnd={() => this.stop("D")}
              style={{ backgroundColor: this.state.colorD, height: 100, width: 48, borderBottomWidth: 1, borderLeftWidth: 1 }} >
            </View >
            <View
              onTouchStart={() => this.stroke("E")}
              onTouchEnd={() => this.stop("E")}
              style={{ backgroundColor: this.state.colorE, height: 100, width: 48, borderBottomWidth: 1, borderLeftWidth: 1 }} >
            </View >
            <View
              onTouchStart={() => this.stroke("F")}
              onTouchEnd={() => this.stop("F")}
              style={{ backgroundColor: this.state.colorF, height: 100, width: 48, borderBottomWidth: 1, borderLeftWidth: 1 }} >
            </View >
            <View
              onTouchStart={() => this.stroke("G")}
              onTouchEnd={() => this.stop("G")}
              style={{ backgroundColor: this.state.colorG, height: 100, width: 48, borderBottomWidth: 1, borderLeftWidth: 1 }} >
            </View >
            <View
              onTouchStart={() => this.stroke("A")}
              onTouchEnd={() => this.stop("A")}
              style={{ backgroundColor: this.state.colorA, height: 100, width: 48, borderBottomWidth: 1, borderLeftWidth: 1 }} >
            </View >
            <View
              onTouchStart={() => this.stroke("B")}
              onTouchEnd={() => this.stop("B")}
              style={{ backgroundColor: this.state.colorB, height: 100, width: 48, borderBottomWidth: 1, borderLeftWidth: 1, borderRightWidth: 1 }} >
            </View >

          </View>
        </View>
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#F5FCFF',
    flexDirection: "row",
  },
});

鍵盤の表示

Viewで鍵盤を作ってきます。
鍵盤に触れた時に音が鳴り、離した時に音が鳴り終わるように、OnTouchStartとOnTouchEndに処理を書いていきます。
また、鍵盤に触れている時にbackgroundColorを変えたいので、stateにしておきます。

  constructor( props ){
    super( props );

    this.state = {
      colorC : "white",
      colorCs : "black",

      ...

    }

    ...

  }

  render() {

        ...

        <View
          onTouchStart={() => this.stroke("Cs")}
          onTouchEnd={() => this.stop("Cs")}
          style={{ backgroundColor: this.state.colorCs, height: 100, width: 32, borderTopWidth: 1, borderLeftWidth: 1,}} >
        </View >

        ....

  }

音を鳴らす(鍵盤に触れる)

コンストラクタで音源を読み込んでおきます。
鍵盤に触れた時、鍵盤のbackgroundColorをstateで変更して、音を鳴らします。
音が鳴らないことがあるため、setTimeoutが必要です。

  constructor( props ){
    super( props );

    ...

    this.sound = {};
    const soundList = [ "C", "Cs", "D", "Ds", "E", "F", "Fs", "G", "Gs", "A", "As", "B" ]
    soundList.forEach(note => {
      this.sound[note] = new Sound( note + ".mp3", Sound.MAIN_BUNDLE, error => {
        if ( error ) {
          console.log("failed to load the sound.", error);
        }
      })
    });
  }

  stroke ( note ) {
    switch ( note ) {
      case "C":
        this.setState({ colorC: "rgba(1, 1, 1, 0.1)" })
        break;

      ...

    }

    setTimeout( () => {
      this.sound[note].play(success => {
        if ( success ) {
          console.log("successfully finished playing.");
        } else {
          console.log("failed to play the sound.");
        }
      });
    }, 1);
  }

音を止める(鍵盤から指を離す)

同様にstateで鍵盤の色を元に戻します。
音をいきなり止めてしまうとブツッと鳴ってしまうので、徐々にボリュームを下げてから止めるようにします。
止めた後は、次に音を鳴らす時のためにボリュームを元に戻しておきます。

  stop( note ) {
    switch ( note ) {
      case "C":
        this.setState( { colorC: "white" } )
        break;

      ...

    }

    setTimeout( () => {
      for (let i=0; i<2000; i++) {
        this.sound[note].setVolume( 1.0-i/2000. );
      }
      this.sound[note].stop();
      this.sound[note].setVolume( 1.0 );
    }, 1 )
  }

まとめ

React Nativeでピアノを実装しました。
パフォーマンスは良いとは言えませんが、アプリにちょっとした鍵盤を実装したい場合(?)に試してみてください!

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

Church数でfizzbuzz

「javascriptで難読fizzbuzz」っていうお題で書き始めました。

rnという関数でChurch数をjavascriptの整数に変換していますが、これは結果の表示用であって計算は全てChurch数のまま行ってます。

実行時の効率は考えてないので、結果が表示されるまで数秒かかります。

fizzbuzz.js
const z=f=>(x=>f(y=>x(x)(y)))(x=>f(y=>x(x)(y))) //Zコンビネーター
const id=x=>x //恒等関数
const sc=n=>f=>x=>f(n(f)(x)) //succ
const pr=n=>f=>x=>n(g=>h=>h(g(f)))(tr(x))(id) //pred
const mn=m=>n=>n(pr)(m) //引き算
const pl=m=>n=>n(sc)(m) //足し算
const ml=m=>n=>n(pl(m))(zr) //掛け算
const zr=f=>x=>x //0
const th=f=>x=>f(f(f(x))) //3
const fv=f=>x=>f(f(f(f(f(x))))) //5
const ft=ml(th)(fv) //15
const hd=ml(ml(fv)(fv))(sc(th)) //100
const tr=t=>f=>t //true
const fl=t=>f=>f //false
const iz=n=>n(tr(fl))(tr) //isZero
const nt=b=>b(fl)(tr) //not
const an=b=>c=>b(c(tr)(fl))(fl) //and
const or=b=>c=>nt(an(nt(b))(nt(c))) //or
const lt=z(f=>a=>b=>or(iz(a))(iz(b))(()=>nt(iz(b)))(()=>f(pr(a))(pr(b)))()) //less than
const md=z(f=>m=>n=>lt(m)(n)(()=>m)(()=>f(mn(m)(n))(n))()) //割り算のあまり
const cn=x=>y=>f=>f(x)(y) //cons

 //Church数からjavascriptの整数に変換(表示用)
const rn=z(f=>n=> iz(n)(()=>0)(()=>f(pr(n))+1)())

//Church数を受け取って、fizz, buzz, javascriptの整数のいずれかを返す関数
const fb=n=>iz(md(n)(ft))("FizzBuzz")(iz(md(n)(fv))("Buzz")(iz(md(n)(th))("Fizz")(rn(n)))) 

//100から0までの fizzbuzz の結果のリスト(降順)
const fbs=z(f=>n=>iz(n)(()=>cn(cn(zr)(zr))(zr))(()=>cn(cn(n)(fb(n)))(f(pr(n))))())(hd)

//結果のリストを逆順に出力
const ma=z(f=>ls=>iz(ls(tr)(tr))(()=>zr)(()=>(f(ls(fl)),console.log(ls(tr)(fl))))())(fbs)

以下はワンライナーver.

fizzbuzz2.js
(f=>(x=>f(y=>x(x)(y)))(x=>f(y=>x(x)(y))))(f=>ls=>(n=>n((t=>f=>t)(t=>f=>f))(t=>f=>t))(ls(t=>f=>t)(t=>f=>t))(()=>(f=>x=>x))(()=>(f(ls(t=>f=>f)),console.log(ls(t=>f=>t)(t=>f=>f))))())((f=>(x=>f(y=>x(x)(y)))(x=>f(y=>x(x)(y))))(f=>n=>(n=>n((t=>f=>t)(t=>f=>f))(t=>f=>t))(n)(()=>(x=>y=>f=>f(x)(y))((x=>y=>f=>f(x)(y))(f=>x=>x)(f=>x=>x))(f=>x=>x))(()=>(x=>y=>f=>f(x)(y))((x=>y=>f=>f(x)(y))(n)((n=>(n=>n((t=>f=>t)(t=>f=>f))(t=>f=>t))(((f=>(x=>f(y=>x(x)(y)))(x=>f(y=>x(x)(y))))(f=>m=>n=>((f=>(x=>f(y=>x(x)(y)))(x=>f(y=>x(x)(y))))(f=>a=>b=>(b=>c=>b(t=>f=>t)(c))((n=>n((t=>f=>t)(t=>f=>f))(t=>f=>t))(a))((n=>n((t=>f=>t)(t=>f=>f))(t=>f=>t))(b))(()=>(b=>b(t=>f=>f)(t=>f=>t))((n=>n((t=>f=>t)(t=>f=>f))(t=>f=>t))(b)))(()=>f((n=>f=>x=>n(g=>h=>h(g(f)))((t=>f=>t)(x))(x=>x))(a))((n=>f=>x=>n(g=>h=>h(g(f)))((t=>f=>t)(x))(x=>x))(b)))()))(m)(n)(()=>m)(()=>f((m=>n=>n(n=>f=>x=>n(g=>h=>h(g(f)))((t=>f=>t)(x))(x=>x))(m))(m)(n))(n))()))(n)((m=>n=>n((m=>n=>n(n=>f=>x=>f(n(f)(x)))(m))(m))(f=>x=>x))(f=>x=>f(f(f(x))))(f=>x=>f(f(f(f(f(x))))))))("FizzBuzz")((n=>n((t=>f=>t)(t=>f=>f))(t=>f=>t))(((f=>(x=>f(y=>x(x)(y)))(x=>f(y=>x(x)(y))))(f=>m=>n=>((f=>(x=>f(y=>x(x)(y)))(x=>f(y=>x(x)(y))))(f=>a=>b=>(b=>c=>b(t=>f=>t)(c))((n=>n((t=>f=>t)(t=>f=>f))(t=>f=>t))(a))((n=>n((t=>f=>t)(t=>f=>f))(t=>f=>t))(b))(()=>(b=>b(t=>f=>f)(t=>f=>t))((n=>n((t=>f=>t)(t=>f=>f))(t=>f=>t))(b)))(()=>f((n=>f=>x=>n(g=>h=>h(g(f)))((t=>f=>t)(x))(x=>x))(a))((n=>f=>x=>n(g=>h=>h(g(f)))((t=>f=>t)(x))(x=>x))(b)))()))(m)(n)(()=>m)(()=>f((m=>n=>n(n=>f=>x=>n(g=>h=>h(g(f)))((t=>f=>t)(x))(x=>x))(m))(m)(n))(n))()))(n)(f=>x=>f(f(f(f(f(x)))))))("Buzz")((n=>n((t=>f=>t)(t=>f=>f))(t=>f=>t))(((f=>(x=>f(y=>x(x)(y)))(x=>f(y=>x(x)(y))))(f=>m=>n=>((f=>(x=>f(y=>x(x)(y)))(x=>f(y=>x(x)(y))))(f=>a=>b=>(b=>c=>b(t=>f=>t)(c))((n=>n((t=>f=>t)(t=>f=>f))(t=>f=>t))(a))((n=>n((t=>f=>t)(t=>f=>f))(t=>f=>t))(b))(()=>(b=>b(t=>f=>f)(t=>f=>t))((n=>n((t=>f=>t)(t=>f=>f))(t=>f=>t))(b)))(()=>f((n=>f=>x=>n(g=>h=>h(g(f)))((t=>f=>t)(x))(x=>x))(a))((n=>f=>x=>n(g=>h=>h(g(f)))((t=>f=>t)(x))(x=>x))(b)))()))(m)(n)(()=>m)(()=>f((m=>n=>n(n=>f=>x=>n(g=>h=>h(g(f)))((t=>f=>t)(x))(x=>x))(m))(m)(n))(n))()))(n)(f=>x=>f(f(f(x)))))("Fizz")(((f=>(x=>f(y=>x(x)(y)))(x=>f(y=>x(x)(y))))(f=>n=> (n=>n((t=>f=>t)(t=>f=>f))(t=>f=>t))(n)(()=>0)(()=>f((n=>f=>x=>n(g=>h=>h(g(f)))((t=>f=>t)(x))(x=>x))(n))+1)()))(n)))))(n)))(f((n=>f=>x=>n(g=>h=>h(g(f)))((t=>f=>t)(x))(x=>x))(n))))())((m=>n=>n((m=>n=>n(n=>f=>x=>f(n(f)(x)))(m))(m))(f=>x=>x))((m=>n=>n((m=>n=>n(n=>f=>x=>f(n(f)(x)))(m))(m))(f=>x=>x))(f=>x=>f(f(f(f(f(x))))))(f=>x=>f(f(f(f(f(x)))))))((n=>f=>x=>f(n(f)(x)))(f=>x=>f(f(f(x)))))))

ワンライン(2571文字)

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

トラッキングの方法

Webのトラッキングは、GoogleAnalyticsなどのアクセス解析ツールや、アフィリエイトで使われていますが、その方法について分かっていることをつらつら書きます。

トラッキングとは

アクセス解析ツールのトラッキング、アフィリエイトのトラッキングはだいたい方法は同じ。

その前にまず、アフィリエイトとは、サイト運営者が自サイトに貼りだした広告をサイト訪問者がクリックし、商品購入や会員登録などに繋がった場合に報酬を支払うというもので、

  1. 広告クリック時にCookieを発行し(ex: Cookie発行ドメインは「*.アフィリエイトサービスプロバイダ.com」)
  2. 商品購入完了や会員登録完了ページでそのCookieを送信する(ex: https://アフィリエイトサービスプロバイダ.com/readCookie を実行)

という2点で広告が踏まれたこと、なにかしらの目標達成を検知している。

つまり 広告クリック〇〇完了ページでのCookie送信 の2点が大事でその途中のページ遷移はどうでもいい、異なるドメイン間で移動してようが関係なく、Cookieの性質を利用することでトラッキング(クロスドメインも)をしていたということになるが、そのトラッキング方法ができなくなることがあるのがAppleのITP対応であり、ASP(アフィリエイトサービスプロバイダ)各社はCookieに代わるトラッキングの方法を検討し導入しているという状況。

クロスドメインの問題

異なるドメイン間でデータをやり取りは基本できない。
ただ、それでは困るので解決するための手段はいくつかあり、その際に

「クロスドメインはどう対応する?」

などとよく使われている。と思う。

トラッキングでもクロスドメインはもちろん問題になり、例としては、ECサイトとショッピングカートでドメインが異なる場合に、アクセス解析ツールがドメインをまたいだ瞬間(ex:商品をカートにいれた瞬間)トラッキングができないことが問題になる。
トラッキングに使っていたCookieは、クロスドメインでデータをやり取りできない(※1)ため。

といってもアクセス解析ツールはどこもそんなことにはならないようにしてあるはずで、GoogleAnalyticsでは埋め込みコードにクロスドメインするドメインを列挙しておくことで対応できます。細かいやり方は本家の資料をみてください。

※1 Cookieの発行ドメインが、「*.example.com」で、クロスドメイン先のドメイン、上記の例だとショッピングカートのドメインが「cart.example.com」ならトラッキングできる。

トラッキングの方法一覧

サードパーティCookieで点と点のトラッキング

クリックと〇〇完了だけを特定すればいいアフィリエイトで用いられやすい、クロスドメインの問題も気にしない。(でもITPがね・・)

ファーストパーティCookieでユーザの動きを連続してトラッキング

GoogleAnalyticsはこの方法。クロスドメイントラッキングが必要な場合は、そのドメインを列挙する必要あり。
(しかしITP2.1(JavaScriptが扱えるcookieが7日間までしか使えなくなる)がね ・・噂によるとCookie+LocalStorageの合わせ技をすることになるのか?)

LocalStorage

あまり使ったことがないのでわからない..。

デバイス推定技術?

アフィリエイト大手のValueCommerceが発表している方法。具体的に発表されていない?実際にトラッキングされてみて試してみようかな?
https://www.valuecommerce.co.jp/news/c_press/4263/

最後に

ほかにもあると思いますし、勢いで書いたので間違っているところがありそうです。。
思いついたり、気づいたら追記・編集していきます。

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

【今日から携わる】Javascript入門(3)応用

Javascript入門(1)初級-中級
Javascript入門(2)上級
Javascript入門(3)応用←いまここ

Javascript入門のため、Progateをやってみました

学習コース JavaScript Ⅴ

モジュールを組み合わせよう

このレッスンでは、JavaScriptのコードを複数のファイルに分割し、
それらを組み合わせる方法を学びます。
複数ファイルに分割することで、維持・更新のしやすいコードを書くことができます。
また、パッケージと呼ばれる便利な機能を使うための方法も学びます。

最後の演習では、readline-syncを使って、コンソールでの質問に答えるプログラムを書きます。

ファイルの分割

コードの量が増えてくると1つのファイルで管理するのが大変になるため、複数のファイルでコードを管理することがあります。
今回は、メインのプログラムを実行する「script.js」とAnimalクラスを定義する「animal.js」、Dog クラスを定義する「dog.js」の3つのファイルにコードを分けてみましょう。

エラーをおこさないためにすること

ファイルを分割したときのエラーは、それぞれのファイルを関連づけし、必要な値を渡すことで解決できます。
今回の場合「dog.js」でAnimalクラスを、「script.js」でDogクラスを使用できるように設定する必要があります。

▼演習
・「animal.js」では、”Animal”クラスをエクスポートする
・「dog.js」では、”animal.js”を読み込み、”Dog”クラスをエクスポートする
・「script.js」では、”dog.js”を読み込む

animal.js
class Animal {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }

  greet() {
    console.log("こんにちは");
  }

  info() {
    this.greet();
    console.log(`名前は${this.name}です`);
    console.log(`${this.age}歳です`);
  }
}

// Animalクラスをエクスポートしてください
export default Animal;
dog.js
// Animalクラスをインポートしてください
import Animal from "./animal";

class Dog extends Animal {
  constructor(name, age, breed) {
    super(name, age);
    this.breed = breed;
  }

  info() {
    this.greet();
    console.log(`名前は${this.name}です`);
    console.log(`犬種は${this.breed}です`);
    console.log(`${this.age}歳です`);
    const humanAge = this.getHumanAge();
    console.log(`人間年齢で${humanAge}歳です`);
  }

  getHumanAge() {
    return this.age * 7;
  }
}

// Dogクラスをエクスポートしてください
export default Dog;
script.js
// Dogクラスをインポートしてください
import Dog from "./dog";

const dog = new Dog("レオ", 4, "チワワ");
dog.info();

値のエクスポート

クラスのエクスポートを行いましたが、エクスポート出来るのはクラスだけではありません。文字列や数値や関数など、どんな値でもエクスポートが可能です。
エクスポートする際は、下図のように「export default 定数名」とします。インポートする際は「import 定数名 from "./ファイル名"」とします。

▼演習
「dogData.js」という新しいファイルを作成しました。
「script.js」から「dogData.js」にdogインスタンスを定義する部分を移動させてみましょう。

script.js
// 定数dogをインポートしてください
import dog from "./dogData.js";

dog.info();
dogData.js
// Dogクラスのインポートと定数dogを以下に張り付けてください
import Dog from "./dog";

const dog = new Dog("レオ", 4, "チワワ");

// 定数dogをエクスポートしてください
export default dog;
dog.js
import Animal from "./animal";

class Dog extends Animal {
  constructor(name, age, breed) {
    super(name, age);
    this.breed = breed;
  }

  info() {
    this.greet();
    console.log(`名前は${this.name}です`);
    console.log(`犬種は${this.breed}です`);
    console.log(`${this.age}歳です`);
    const humanAge = this.getHumanAge();
    console.log(`人間年齢で${humanAge}歳です`);
  }

  getHumanAge() {
    return this.age * 7;
  }
}

export default Dog;
animal.js
class Animal {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }

  greet() {
    console.log("こんにちは");
  }

  info() {
    this.greet();
    console.log(`名前は${this.name}です`);
    console.log(`${this.age}歳です`);
  }
}

export default Animal;

名前付きエクスポート

export defaultはデフォルトエクスポートと呼ばれ、そのファイルがインポートされると自動的に「export default 値」の値がインポートされます。そのためエクスポート時の値の名前と、インポート時の値の名前は違っても問題ありません。

デフォルトエクスポートは1ファイル1つの値のみ使えます。このファイルをインポートする際には、デフォルトエクスポートの値を自動でインポートするため、値が1つのみとなっています。
複数の値をエクスポートしたい場合は、次のスライドで紹介する「名前付きエクスポート」を用います。

「import { 値の名前 } from "./ファイル名"」

名前付きエクスポートした値をインポートする場合は、エクスポート時と同じ名前で値を指定します。インポートする値は、エクスポート時と同様に、「import { 値の名前 } from "./ファイル名"」と{}で囲んで指定します。

「export { 名前1, 名前2 }」

名前付きエクスポートは、デフォルトエクスポートと違い、複数の定数やクラスを指定してエクスポートが出来ます。
また、左の図のように、「export { 名前1, 名前2 }」という形で書くことにより、1つのファイルから複数のエクスポートが出来ます。
インポートの際も、コンマで区切ることで複数のインポートができます。

▼演習
「dogData.js」で定数dogを、2つの定数dog1とdog2に書きかえておきました。
・「dogData.js」に、定義した定数dog1とdog2をエクスポートしていきましょう。
・「script.js」に、import dogではなく、dog1dog2をインポートするように書き換えましょう
dog1dog2の情報を出力するようにしましょう

dogData.js
import Dog from "./dog";

// 定数dog1, dog2を確認してください
const dog1 = new Dog("レオ", 4, "チワワ");
const dog2 = new Dog("ベン", 2, "プードル");

// 以下を書き換えて、定数dog1, dog2をエクスポートしてください
export {dog1, dog2};
script.js
// 以下を書き換えて、定数dog1, dog2をインポートしてください
import { dog1, dog2} from "./dogData";

// 定数dog1とdog2を出力するように左からコピーして書き換えてください
console.log("----------");
dog1.info();
console.log("----------");
dog2.info();

相対パス

これまでファイルの指定は「./ファイル名」としてきました。
これは相対パスと言い、記述されているファイルからみた位置関係を示しています。

▼ファイルを分ける

.src
├── class
│   ├── animal.js
│   └── dog.js
├── data
│   └── dogDate.js
└── script.js

パッケージ(1)

JavaScriptの世界では、誰かが作った便利なプログラムがパッケージという形で公開されています。また、JavaScriptの機能を使うことで、このパッケージを自分のプログラムの中に組み込んで使うことができます。

使い方

定義
import 定義名 from "パッケージ名";
使い方
import chalk from "chalk";
console.log(chalk.yellow("Hello World"));
console.log(chalk.bgCyan("Hello World"));

パッケージ(2)

コンソールから値を入力出来るようにするためのパッケージを使ってみましょう。

readline-sync

コンソールへの値の入力と、その入力された値をプログラムの中で使うことができるようになります。

▼演習
readline-syncを使って、コンソールでの質問に答えるプログラムを書いてみよう

dogDate.js
// readline-syncをインポートしてください
import readlineSync from "readline-sync";

import Dog from "../class/dog";

const dog1 = new Dog("レオ", 4, "チワワ");

// readlineSync.questionを使って書き換えてください
// const name = "ベン";
const name = readlineSync.question("名前を入力してください: ");

// readlineSync.questionIntを使って書き換えてください
// const age = 2;
const age = readlineSync.questionInt("年齢を入力してください: ");

// readlineSync.questionを使って書き換えてください
// const breed = "プードル";
const breed = readlineSync.question("犬種を入力してください: ");

const dog2 = new Dog(name, age, breed);

export { dog1, dog2 };

つぎの章

学習コース JavaScript Ⅵ

このレッスンでは「配列を操作するメソッド」について学習していきましょう。
配列の操作を学ぶと、データを柔軟に扱えるようになります。
これから学ぶ便利なメソッドはJavaScriptを実践的に使う場合に必須の知識です。

pushメソッド

▼演習
pushメソッドを使って、配列に新しい要素を追加しましょう。

演習
const characters = ["にんじゃわんこ", "ベイビーわんこ", "ひつじ仙人"];

console.log(characters);

// pushメソッドを使って配列charactersに、文字列「とりずきん」を追加してください
characters.push("とりずきん");

// 配列charactersを出力してください
console.log(characters);

forEachメソッド

forEachメソッドは配列の中の要素を1つずつ取り出して、全ての要素に繰り返し同じ処理を行うメソッドです。

forEachの仕組み

定義
配列 = [要素1,要素2,要素3];
配列.forEach((引数) => {処理});//アロー関数を使用
//配列の中の要素を1つずつ取り出して同じ処理をする

▼演習
forEachメソッドを使って、配列charactersの中身をすべて出力してください

演習
const characters = ["にんじゃわんこ", "ベイビーわんこ", "ひつじ仙人", "とりずきん"];

// forEachメソッドを使って、配列charactersの中身をすべて出力してください
characters.forEach((character) => {
  console.log(character);
});

findメソッド

findメソッドとは、アロー関数の処理部分に記述した条件式に合う1つ目の要素を配列の中から取り出すメソッドです。

とりあえず例文
const numbers = [1,3,5,7];

const foundNumber = numbers.find((number) => { //number:引数
  return number > 3; // number> 3:条件
});
console.log(foundNumber);//5
//条件に合う最初の要素が取り出されている

▼演習
1.findメソッドを使って配列numbersから3の倍数を見つけ、定数foundNumberに代入して出力してください
2.定数charactersからidが3のオブジェクトを見つけ、定数foundCharacterに代入して出力してください

演習
const numbers = [1, 3, 5, 7, 9];

// findメソッドを使って配列numbersから3の倍数を見つけ、定数foundNumberに代入してください
const foundNumber = numbers.find((number) => {
  return number % 3 === 0;
});

// foundNumberを出力してください
console.log(foundNumber);


const characters = [
  {id: 1, name: "にんじゃわんこ", age: 6},
  {id: 2, name: "ベイビーわんこ", age: 2},
  {id: 3, name: "ひつじ仙人", age: 100},
  {id: 4, name: "とりずきん", age: 21}
];

// 定数charactersからidが3のオブジェクトを見つけ、定数foundCharacterに代入してください
const foundCharacter = characters.find((character) => {
  return character.id === 3;
});

// foundCharacterを出力してください
console.log(foundCharacter);

filterメソッド

filterメソッドとは記述した条件に合う要素のみを取り出して新しい配列を作成するメソッドです。

▼演習
・filterメソッドを使ってnumbersから偶数を取り出し、定数evenNumbersに代入して出力してください
・charactersから20歳未満のキャラクターを取り出し、定数underTwentyに代入して出力してください

演習
const numbers = [1, 2, 3, 4];

// filterメソッドを使ってnumbersから偶数を取り出し、定数evenNumbersに代入してください
const evenNumbers = numbers.filter((number) => {
  return number % 2 === 0;
});

// evenNumbersを出力してください
console.log(evenNumbers);


const characters = [
  {id: 1, name:"にんじゃわんこ", age: 14},
  {id: 2, name:"ベイビーわんこ", age: 5},
  {id: 3, name:"ひつじ仙人", age: 100}
];

// charactersから20歳未満のキャラクターを取り出し、定数underTwentyに代入してください
const underTwenty = characters.filter((character) => {
  return character.age < 20;
});

// underTwentyを出力してください
console.log(underTwenty);

mapメソッド

mapメソッドとは、配列内のすべての要素に処理を行い、その戻り値から新しい配列を作成するメソッドです。

▼演習
・定数numbersにmapメソッドを使って配列を作り、定数doubledNumbersに代入して出力してください
・定数namesにmapメソッドを使って新しい配列を作り、定数fullNamesに代入して出力してください

演習
const numbers = [1, 2, 3, 4];

// 定数numbersにmapメソッドを使って配列を作り、定数doubledNumbersに代入してください
const doubledNumbers = numbers.map((number) => {
  return number * 2;
});

// 定数doubledNumbersを出力してください
console.log(doubledNumbers);


const names = [
  {firstName: "Kate", lastName: "Jones"},
    {firstName: "John", lastName: "Smith"},
    {firstName: "Denis", lastName: "Williams"},
    {firstName: "David", lastName: "Black"}
];

// 定数namesにmapメソッドを使って新しい配列を作り、定数fullNamesに代入してください
const fullNames = names.map((name) => {
  return name.firstName + name.lastName;
});

// 定数fullNamesを出力してください
console.log(fullNames);

Javascript入門(1)初級-中級
Javascript入門(2)上級
Javascript入門(3)応用←いまここ

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

【今日から携わる】Javascript入門のため、Progateをやってみた(3)

Javascript入門のため、Progateをやってみた(1)
Javascript入門のため、Progateをやってみた(2)
Javascript入門のため、Progateをやってみた(3)←いまここ

学習コース JavaScript Ⅴ

モジュールを組み合わせよう

このレッスンでは、JavaScriptのコードを複数のファイルに分割し、
それらを組み合わせる方法を学びます。
複数ファイルに分割することで、維持・更新のしやすいコードを書くことができます。
また、パッケージと呼ばれる便利な機能を使うための方法も学びます。

最後の演習では、readline-syncを使って、コンソールでの質問に答えるプログラムを書きます。

ファイルの分割

コードの量が増えてくると1つのファイルで管理するのが大変になるため、複数のファイルでコードを管理することがあります。
今回は、メインのプログラムを実行する「script.js」とAnimalクラスを定義する「animal.js」、Dog クラスを定義する「dog.js」の3つのファイルにコードを分けてみましょう。

エラーをおこさないためにすること

ファイルを分割したときのエラーは、それぞれのファイルを関連づけし、必要な値を渡すことで解決できます。
今回の場合「dog.js」でAnimalクラスを、「script.js」でDogクラスを使用できるように設定する必要があります。

▼演習
・「animal.js」では、”Animal”クラスをエクスポートする
・「dog.js」では、”animal.js”を読み込み、”Dog”クラスをエクスポートする
・「script.js」では、”dog.js”を読み込む

animal.js
class Animal {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }

  greet() {
    console.log("こんにちは");
  }

  info() {
    this.greet();
    console.log(`名前は${this.name}です`);
    console.log(`${this.age}歳です`);
  }
}

// Animalクラスをエクスポートしてください
export default Animal;
dog.js
// Animalクラスをインポートしてください
import Animal from "./animal";

class Dog extends Animal {
  constructor(name, age, breed) {
    super(name, age);
    this.breed = breed;
  }

  info() {
    this.greet();
    console.log(`名前は${this.name}です`);
    console.log(`犬種は${this.breed}です`);
    console.log(`${this.age}歳です`);
    const humanAge = this.getHumanAge();
    console.log(`人間年齢で${humanAge}歳です`);
  }

  getHumanAge() {
    return this.age * 7;
  }
}

// Dogクラスをエクスポートしてください
export default Dog;
script.js
// Dogクラスをインポートしてください
import Dog from "./dog";

const dog = new Dog("レオ", 4, "チワワ");
dog.info();

値のエクスポート

クラスのエクスポートを行いましたが、エクスポート出来るのはクラスだけではありません。文字列や数値や関数など、どんな値でもエクスポートが可能です。
エクスポートする際は、下図のように「export default 定数名」とします。インポートする際は「import 定数名 from "./ファイル名"」とします。

▼演習
「dogData.js」という新しいファイルを作成しました。
「script.js」から「dogData.js」にdogインスタンスを定義する部分を移動させてみましょう。

script.js
// 定数dogをインポートしてください
import dog from "./dogData.js";

dog.info();
dogData.js
// Dogクラスのインポートと定数dogを以下に張り付けてください
import Dog from "./dog";

const dog = new Dog("レオ", 4, "チワワ");

// 定数dogをエクスポートしてください
export default dog;
dog.js
import Animal from "./animal";

class Dog extends Animal {
  constructor(name, age, breed) {
    super(name, age);
    this.breed = breed;
  }

  info() {
    this.greet();
    console.log(`名前は${this.name}です`);
    console.log(`犬種は${this.breed}です`);
    console.log(`${this.age}歳です`);
    const humanAge = this.getHumanAge();
    console.log(`人間年齢で${humanAge}歳です`);
  }

  getHumanAge() {
    return this.age * 7;
  }
}

export default Dog;
animal.js
class Animal {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }

  greet() {
    console.log("こんにちは");
  }

  info() {
    this.greet();
    console.log(`名前は${this.name}です`);
    console.log(`${this.age}歳です`);
  }
}

export default Animal;

名前付きエクスポート

export defaultはデフォルトエクスポートと呼ばれ、そのファイルがインポートされると自動的に「export default 値」の値がインポートされます。そのためエクスポート時の値の名前と、インポート時の値の名前は違っても問題ありません。

デフォルトエクスポートは1ファイル1つの値のみ使えます。このファイルをインポートする際には、デフォルトエクスポートの値を自動でインポートするため、値が1つのみとなっています。
複数の値をエクスポートしたい場合は、次のスライドで紹介する「名前付きエクスポート」を用います。

「import { 値の名前 } from "./ファイル名"」

名前付きエクスポートした値をインポートする場合は、エクスポート時と同じ名前で値を指定します。インポートする値は、エクスポート時と同様に、「import { 値の名前 } from "./ファイル名"」と{}で囲んで指定します。

「export { 名前1, 名前2 }」

名前付きエクスポートは、デフォルトエクスポートと違い、複数の定数やクラスを指定してエクスポートが出来ます。
また、左の図のように、「export { 名前1, 名前2 }」という形で書くことにより、1つのファイルから複数のエクスポートが出来ます。
インポートの際も、コンマで区切ることで複数のインポートができます。

▼演習
「dogData.js」で定数dogを、2つの定数dog1とdog2に書きかえておきました。
・「dogData.js」に、定義した定数dog1とdog2をエクスポートしていきましょう。
・「script.js」に、import dogではなく、dog1dog2をインポートするように書き換えましょう
dog1dog2の情報を出力するようにしましょう

dogData.js
import Dog from "./dog";

// 定数dog1, dog2を確認してください
const dog1 = new Dog("レオ", 4, "チワワ");
const dog2 = new Dog("ベン", 2, "プードル");

// 以下を書き換えて、定数dog1, dog2をエクスポートしてください
export {dog1, dog2};
script.js
// 以下を書き換えて、定数dog1, dog2をインポートしてください
import { dog1, dog2} from "./dogData";

// 定数dog1とdog2を出力するように左からコピーして書き換えてください
console.log("----------");
dog1.info();
console.log("----------");
dog2.info();

相対パス

これまでファイルの指定は「./ファイル名」としてきました。
これは相対パスと言い、記述されているファイルからみた位置関係を示しています。

▼ファイルを分ける

.src
├── class
│   ├── animal.js
│   └── dog.js
├── data
│   └── dogDate.js
└── script.js

パッケージ(1)

JavaScriptの世界では、誰かが作った便利なプログラムがパッケージという形で公開されています。また、JavaScriptの機能を使うことで、このパッケージを自分のプログラムの中に組み込んで使うことができます。

使い方

定義
import 定義名 from "パッケージ名";
使い方
import chalk from "chalk";
console.log(chalk.yellow("Hello World"));
console.log(chalk.bgCyan("Hello World"));

パッケージ(2)

コンソールから値を入力出来るようにするためのパッケージを使ってみましょう。

readline-sync

コンソールへの値の入力と、その入力された値をプログラムの中で使うことができるようになります。

▼演習
readline-syncを使って、コンソールでの質問に答えるプログラムを書いてみよう

dogDate.js
// readline-syncをインポートしてください
import readlineSync from "readline-sync";

import Dog from "../class/dog";

const dog1 = new Dog("レオ", 4, "チワワ");

// readlineSync.questionを使って書き換えてください
// const name = "ベン";
const name = readlineSync.question("名前を入力してください: ");

// readlineSync.questionIntを使って書き換えてください
// const age = 2;
const age = readlineSync.questionInt("年齢を入力してください: ");

// readlineSync.questionを使って書き換えてください
// const breed = "プードル";
const breed = readlineSync.question("犬種を入力してください: ");

const dog2 = new Dog(name, age, breed);

export { dog1, dog2 };

つぎの章

学習コース JavaScript Ⅵ

このレッスンでは「配列を操作するメソッド」について学習していきましょう。
配列の操作を学ぶと、データを柔軟に扱えるようになります。
これから学ぶ便利なメソッドはJavaScriptを実践的に使う場合に必須の知識です。

pushメソッド

▼演習
pushメソッドを使って、配列に新しい要素を追加しましょう。

演習
const characters = ["にんじゃわんこ", "ベイビーわんこ", "ひつじ仙人"];

console.log(characters);

// pushメソッドを使って配列charactersに、文字列「とりずきん」を追加してください
characters.push("とりずきん");

// 配列charactersを出力してください
console.log(characters);

forEachメソッド

forEachメソッドは配列の中の要素を1つずつ取り出して、全ての要素に繰り返し同じ処理を行うメソッドです。

forEachの仕組み

定義
配列 = [要素1,要素2,要素3];
配列.forEach((引数) => {処理});//アロー関数を使用
//配列の中の要素を1つずつ取り出して同じ処理をする

▼演習
forEachメソッドを使って、配列charactersの中身をすべて出力してください

演習
const characters = ["にんじゃわんこ", "ベイビーわんこ", "ひつじ仙人", "とりずきん"];

// forEachメソッドを使って、配列charactersの中身をすべて出力してください
characters.forEach((character) => {
  console.log(character);
});

findメソッド

findメソッドとは、アロー関数の処理部分に記述した条件式に合う1つ目の要素を配列の中から取り出すメソッドです。

とりあえず例文
const numbers = [1,3,5,7];

const foundNumber = numbers.find((number) => { //number:引数
  return number > 3; // number> 3:条件
});
console.log(foundNumber);//5
//条件に合う最初の要素が取り出されている

▼演習
1.findメソッドを使って配列numbersから3の倍数を見つけ、定数foundNumberに代入して出力してください
2.定数charactersからidが3のオブジェクトを見つけ、定数foundCharacterに代入して出力してください

演習
const numbers = [1, 3, 5, 7, 9];

// findメソッドを使って配列numbersから3の倍数を見つけ、定数foundNumberに代入してください
const foundNumber = numbers.find((number) => {
  return number % 3 === 0;
});

// foundNumberを出力してください
console.log(foundNumber);


const characters = [
  {id: 1, name: "にんじゃわんこ", age: 6},
  {id: 2, name: "ベイビーわんこ", age: 2},
  {id: 3, name: "ひつじ仙人", age: 100},
  {id: 4, name: "とりずきん", age: 21}
];

// 定数charactersからidが3のオブジェクトを見つけ、定数foundCharacterに代入してください
const foundCharacter = characters.find((character) => {
  return character.id === 3;
});

// foundCharacterを出力してください
console.log(foundCharacter);

filterメソッド

filterメソッドとは記述した条件に合う要素のみを取り出して新しい配列を作成するメソッドです。

▼演習
・filterメソッドを使ってnumbersから偶数を取り出し、定数evenNumbersに代入して出力してください
・charactersから20歳未満のキャラクターを取り出し、定数underTwentyに代入して出力してください

演習
const numbers = [1, 2, 3, 4];

// filterメソッドを使ってnumbersから偶数を取り出し、定数evenNumbersに代入してください
const evenNumbers = numbers.filter((number) => {
  return number % 2 === 0;
});

// evenNumbersを出力してください
console.log(evenNumbers);


const characters = [
  {id: 1, name:"にんじゃわんこ", age: 14},
  {id: 2, name:"ベイビーわんこ", age: 5},
  {id: 3, name:"ひつじ仙人", age: 100}
];

// charactersから20歳未満のキャラクターを取り出し、定数underTwentyに代入してください
const underTwenty = characters.filter((character) => {
  return character.age < 20;
});

// underTwentyを出力してください
console.log(underTwenty);

mapメソッド

mapメソッドとは、配列内のすべての要素に処理を行い、その戻り値から新しい配列を作成するメソッドです。

▼演習
・定数numbersにmapメソッドを使って配列を作り、定数doubledNumbersに代入して出力してください
・定数namesにmapメソッドを使って新しい配列を作り、定数fullNamesに代入して出力してください

演習
const numbers = [1, 2, 3, 4];

// 定数numbersにmapメソッドを使って配列を作り、定数doubledNumbersに代入してください
const doubledNumbers = numbers.map((number) => {
  return number * 2;
});

// 定数doubledNumbersを出力してください
console.log(doubledNumbers);


const names = [
  {firstName: "Kate", lastName: "Jones"},
    {firstName: "John", lastName: "Smith"},
    {firstName: "Denis", lastName: "Williams"},
    {firstName: "David", lastName: "Black"}
];

// 定数namesにmapメソッドを使って新しい配列を作り、定数fullNamesに代入してください
const fullNames = names.map((name) => {
  return name.firstName + name.lastName;
});

// 定数fullNamesを出力してください
console.log(fullNames);

Javascript入門のため、Progateをやってみた(1)
Javascript入門のため、Progateをやってみた(2)
Javascript入門のため、Progateをやってみた(3)←いまここ

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