node Express にまつわるあれやこれや

本番環境で使うなら

サーバサイド JavaScript が語られる時、大抵は node の話が出てきて

PC 上でも JavaScript が動く→サーバ開発用としても使える

みたいなストーリーになっているのだが、 現実的に言えば、そのままでは使えない

思い出して欲しいのだが、そもそも JavaScript は非同期のクセの強いスクリプト系言語だ。
これだけでも使い勝手が悪い上、エラーが出るとプログラム自体が停止してしまう、というおおよそサーバー開発用言語としては不向きな弱点を持っている。

この弱点を回避するためには、停止した際に復旧する仕組みなどが必要なのだが、最近では pm2 の評判がいいようだ。

ところで、pm2 をグローバルでインストールするとき、sudo つけないとできなくない?

実用的にも

sudo npm install -g pm2

でいいと思うんだが。(詳しい使い方はこの記事あたり参照)

ちなみに pm2 の p はプロセスの p でしょう。
(ここでいうことではないかもしれないが、特異な言葉の使い方は周囲から理解されにくくなるのでやめた方がいいと思う。何言ってるかわからん人はリンク記事読んでください。通常、「スレッド」というべきところを「プロセス」と言っちゃているので、内容にはそそられるところがあったが、結局、読むのをやめてしまったっていう話です)

SSL 化

本番で使うなら、SSL 化は必須。

これも(今となっては)使えない書き方している記事は多い。

Express 3 以降は以下のように書き方になるらしい。

var fs = require('fs');
var http = require('http');
var https = require('https');
var privateKey  = fs.readFileSync('sslcert/server.key', 'utf8');
var certificate = fs.readFileSync('sslcert/server.crt', 'utf8');

var credentials = {key: privateKey, cert: certificate};
var express = require('express');
var app = express();

// your express configuration here

var httpServer = http.createServer(app);
var httpsServer = https.createServer(credentials, app);

httpServer.listen(8080);
httpsServer.listen(8443);

StackOverFlow より。

秘密鍵 server.key とサーバー証明書 server.crt を sslcert というフォルダに配置して読み込むだけでいい。

こういうことができる node 環境は素晴らしいと思う。

しかし、こういう情報を海外筋から入手しないといけないというのは少々いただけない。

愚痴

少々、愚痴っぽいテイストが入っているので、この際だから言わせてもらうと、ある程度、この業界でちゃんと生きていきたければ

CUnix

に関しては、イロハ+α 程度のスキルは身につけておいてほしい。

なんで、突然、こんなことを言い出したかといえば、某会社経由で「C や Unix ? あー、彼なら、余裕っすよ。経験あります」という触れ込みで配属された人が、まるっきりできていなかったから(笑・・えない)。

 

 

(続く)

 

 

 

NativeMessaging -host アプリとブラウザ拡張のタイミングの取り方-

NativeMessaging の続き。

単純な host アプリ- chrome 拡張のプログラム系の連携は取れるようになってきたが、実用的には工夫が必要。

というのは、ブラウザから母体のアプリを使う必要があるのは、母体のハード資源を使いたいという時がほとんどで、大抵の場合、

ハード資源の応答速度は遅い

から。

店舗で PIN 打ち込んで決済する時、(おそらく操作端末に直結している)カードリーダー経由でも速度的には微妙な感じだが、あれをブラウザ経由でやろうとするわけだから、諸々の工夫が必要になってくるのはわかると思う。

工夫は、大雑把に2方向考えられる。

一つは、PC 側アプリで非同期処理などを実装する。(Mac の場合、ここなど参照)

もう一つは、chrome 拡張側の工夫。

chrome 拡張の工夫

Service Worker と host との通信には、二通りある。

 

(続く)

 

JavaScript の非同期処理のクセが強いんじゃば

われわれのグループ(ネタのみ提供者含む)は、どういうわけか JavaScript (つか node 案件)に関してガチ仕事レベルで取り組んだものは一人もいない。

いや、簡単なウェブ案件はあるんすよ。

だが、chrome 拡張に手を出すにあたってそれじゃいかんだろうとようやくある程度本格的にプログラミングし始めた。

node 系の隆盛などは知っているし、「ブラウザ以外でも動作するようになって・・・」みたいな歴史的な知識もある。

でも、実際にプログラム組まないとその言語固有のクセなんてわからない。

 

けっこう見落とされがちなのは「JavaScript は非同期処理」というものだろう。

例えば、


処理1

処理2

処理3


というようなプログラム構造があった時、処理2が重いと先に処理3を実行することがある。
だから、処理2を処理3より前に実行したいときは promise を使いましょう、みたいな教科書にも出てくるよくある話に繋がるわけだが、知識として知っていても実務レベルで身についているかというとそんなことはなく、バグを作るときは作る(笑)。

この前イタい思いをしたのは次のようなケース。


コールバック関数1{

promise(

コールバック関数2

処理A

).then{

処理B

}

}


こう書くと「コールバック関数2が完了した後に処理Aが走ると思い込んで不具合起こしたんでしょ」と言われそうだが、実際、その通り。

もちろん落ち着いて考えれば、コールバック関数2の処理が重い場合、JavaScript はしれっと処理Aを先に実行してしまうってのはわかる。

大体においてコールバック関数を使うのは思い処理をやらせたいときだから、バグが出るのは当然なのだが、初学者のワイは「promise 使ってるじゃん!コールバック関数2と処理Aが何でこの順序でできないんだよ」と静かに怒りを覚えていた。

ちなみに何でこの不具合に気がついたかといえば、コールバック関数2で値を代入した変数が、処理Aの時点で何度実行しても undefined になっていたから。

慣れてくれば、undifined が出てきた時点で、「ああ、これは実行順序に起因するバグだ」と気が付くのだろうが、習いたての段階ではそうそう気が付かないのだ。

色々と学びの多いプログラミングでやんした。

 

(続く)

chrome 拡張で PC ネイティブアプリとお話する -Native Messaging-

ネットを彷徨っていて「へー」といたく感心した記事。

Native Messaging を使い、ブラウザ拡張でプロセス間通信を行う方法

Native Messaging を用いると、Web ブラウザとは別に、ユーザーのPCにインストールされたネイティブアプリケーションとブラウザ拡張の間でメッセージ交換を行うことができます。つまり、Web ブラウザに用意された API ではアクセスできないハードウェアなどのリソースに対して、ブラウザ拡張からアクセスできるようになります。

こういう技術があるのは知っていたが、詳しい知識はゼロ。

拡張機能を持っている各種ブラウザなら適用可能な技術なようだが、chorome ユーザーが多いと思うので今回は chrome 拡張で考えてみる。

ところで・・

なぜ、Native Messaging が必要か

と思う人もいるかもしれない。

ワイもブラウザ関連の技術はそれほど詳しくないので上手い説明はできないが、簡単にいうなら「サイトページなどと連動して動く JavaScript は、基本的に PC 本体のリソースを使うことが制限されているから」ということになる。

というか、なんの制限もなく操作できたら困る。

昨今では、ネットを介して家電の操作を行う、なんてことが当たり前にできるようになったが、これがブラウザ経由で簡単にできてしまったら、帰宅時に家中めちゃくちゃなんて事態が生じてしまう。

家電ぐらいなら可愛いもので、USB を介して産業システムを制御しているような PC が悪意あるページから操作されてしまったら、社会レベルでかなり大変なことになる。

だが、その一方で、これだけネットが普及してしまうと、各種ネットサービスを提供するにあたってブラウザから PC のリソースを操作したいという要求は強く存在する。

実際、WebUSB のような規格も制定されつつあるのだが、各種機能が消えたり復活したりと仕様がなかなか一定しない。

それで Native Messaging のような技術が必要とされている、ということなんだろう。

最近だとマイナ関係のサイトで、この技術は結構使われているようだ。

今は過渡期なんでしょう。

話を元に戻そう。

まずは環境の構築。

準備

ネイティブアプリを呼び出す chrome 拡張とネイティブアプリの開発環境、それとサーバー機能のあるテスト環境が必要そうなので、以下のようなフォルダをまず作成。

opj-c フォルダで呼び出されるネイティブアプリの開発、
browser フォルダで chrome 拡張の開発、
testserver にテスト環境を整備、

という意図。

こうすれば、初心者でも混乱しないだろう。

testserver に関しては、今回は『イマドキのフロントエンド開発』で紹介されている node – webpack のものを使った。
なお、このときのエントリポイント index.js は空で構わない。

環境はバッチリなんだけどさ

環境を整えた後、ネット上のサンプルなどを試した・・・

のだが、見事なまでに動かないね(笑)。

これには理由があって

・現在の manifest.json のバージョンアップ(2 → 3)が 2024/11 と間近に迫っていて、現行の chrome は 3 推奨

・3 にすると書き方はけっこう変わるらしく従来の書き方ではエラーが出る

という背景がある。

とりあえず、「動く」chrome 拡張の manifest.json の書き方を提示しておくと

{
  "name": "Sample",
  "version": "1.0.0",
  "manifest_version": 3,
  "description": "Sample Chrome Extension",
  "background": {
    "service_worker": "background.js"
  },
  "permissions": [ "nativeMessaging"],
  "content_scripts": [{
    "matches": ["http://localhost:3000/*"],
    "js": [
      "content.js"
    ]
  }]
}

これを、browser フォルダに入れておく。

chrome 拡張に登録する際には、browser フォルダを指定し、デバッグなどは testserver 内で立ち上げた node 由来のサーバの指定のページで行うことができる。

ちなみに、2 → 3 で大きく変わったのは、 service_worker という書き方。

これに関しては google 公式が『拡張機能 Service Worker について』という記事を公開しているので、目を通しておきましょう。
また、Service Worker を使う API も公式がドキュメントを公開してくれている。

host 側アプリ

操作するホスト PC 側のアプリだが、何でもいいというものではなく、入出力は標準入出力に限定されている。(ここら辺も上述の「ブラウザから PC の操作は制限されている」という影響でてますね)

だから、メッセージのやり取りは stdio(C の場合)で行う必要がある。
Java であれば System.in あたりを使う。

適当に作成してみる。

もっとも、アプリは C や Java で作成した「いかにも」なものでなくてもよく、シェルスクリプトでもいいらしいのだが。
参照:『Google ChromeでトレイからCDを排出できるブラウザ拡張をつくりました(Native messaging版)

ホスト側アプリは、細かいことを気にしなければ、こんなものでも動く。

#include <stdio.h>
#include <limits.h>

int main(){
    
    uint8_t *input;
    uint8_t *buffer;
    if((input = (uint8_t *)fgets((char *)buffer,INT_MAX,stdin)) != NULL){
        //確認用
        //for(int i=0;i<sizeof(input);i++){
        //    printf("input data is %x", input[i]);
        //}
    }
    fwrite(input, sizeof(input), INT_MAX, stdout);
}

stdin で入ってきたバイト列をそのまま stdout で返しているだけ。

これでも chrome 拡張側(以下のコード)は反応してくれた。

ただ、仕様には全くあっていない。JavaScript は見事なまでに response を undefined と認識している(笑)。

ホストアプリからのメッセージは、正しくは

32byte ヘッダ(以下のデータ長) + base64 でエンコードされた JSON データ

のバイト列で返さないといけないらしい。
→と書いていたが、以下のコードのネイティブアプリからのレスポンス {“message”:”sign”} を Service Worker は正しく読んでいたので、必ずしも base64 でエンコードする必要はなさそうだ。

どうやら、ブラウザとホストアプリは Native Messaging を介してお話できているようですね。

もうちょっとちゃんと書くかと思っていたら、それ用のテストコードを書いている人がいたので、ちょっぴり改変して以下に掲げておく。(しかし、このコードはなかなか教育的ですね)

#include <stdio.h>
#include <limits.h>

#include <stdlib.h>
#include <stdint.h>
#include <unistd.h>

uint8_t* read_input()
{
    uint32_t length;
    size_t count;
    int err;
    count = fread(&length, sizeof(uint32_t), 1, stdin);
    if (count != 1)
    {
        if (feof(stdin))
        {
            fprintf(stderr, "Unexpectedly encountered EOF while reading file\n");
        }
        else if ((err = ferror(stdin)))
        {
            fprintf(stderr, "An error occured while reading file, err code: %d\n", err);
            clearerr(stdin);
        }
        return NULL;
    }
    uint8_t* value = malloc((length + 1) * sizeof(*value));
    if (value == NULL)
    {
        fprintf(stderr, "An error occured while allocating memory for value");
        return NULL;
    }
    count = fread(value, sizeof(*value), length, stdin);
    if (count != length)
    {
        if (feof(stdin))
        {
            fprintf(stderr, "Unexpectedly encountered EOF while reading file\n");
        }
        else if ((err = ferror(stdin)))
        {
            fprintf(stderr, "An error occured while reading file, err code: %d\n", err);
            clearerr(stdin);
        }
        free(value);
        return NULL;
    }
    return value;
}

size_t write_output(const uint8_t* const value, uint32_t length)
{
    size_t count;
    int err;
    if (length > (1024 * 1024)) {
        fprintf(stderr, "Message too large");
        return 0;
    }
    count = fwrite(&length, sizeof(uint32_t), 1, stdout);
    if (count != 1)
    {
        if (feof(stdout))
        {
            fprintf(stderr, "Unexpectedly encountered EOF while reading file\n");
        }
        else if ((err = ferror(stdout)))
        {
            fprintf(stderr, "An error occured while reading file, err code: %d\n", err);
            clearerr(stdout);
        }
        return 0;
    }
    count = fwrite(value, sizeof(char), length, stdout);
    if (count != length)
    {
        if (feof(stdout))
        {
            fprintf(stderr, "Unexpectedly encountered EOF while writing file\n");
        }
        else if ((err = ferror(stdout)))
        {
            fprintf(stderr, "An error occured while writing file, err code: %d\n", err);
            clearerr(stdout);
        }
        return 0;
    }
    fflush(stdout);
    return length + 4;
}

int main()
{
    size_t count;
    uint8_t* val = read_input();
    if (val == NULL)
    {
        exit(EXIT_FAILURE);
    }
    sleep(3);//add akiba-chan
    count = write_output((uint8_t*)"{\"msg\":\"sign\"}", 14);
    if (count != 18)
    {
        free(val);
        exit(EXIT_FAILURE);
    }
    free(val);
}

「ちょっぴり改変」というのは、 main 関数内の sleep(3) の追加。
PC ハードのリソースを使う場合、大抵の場合、ソフト的な処理に比べ時間がかかる。
そのためリクエストを送ってからレスポンスが返ってくるまで秒単位の時間を設定できるようにした。
(x 秒待ちたいときは sleep(x) とする)

実際、マイナ(PKI)カードの署名処理では数秒単位の時間がかかる。

 

(続く)

node.js に慣れてきた時に読む記事

node.js は JavaScript 実行環境と捉えた方がいい。

だから、(ブラウザではなく)コマンドラインから JavaScript コードを走らせるコマンドは

node XXX.js

になる。


npm は、Node Package Manager の略。

何かのパッケージをインストールするときは、グルーバルなのかローカルなのか意識しよう。

グローバルの場合(あるユーザーの node 環境すべてで使える)は、-g (–global) オプションを忘れずに。