Quantcast
Channel: Node.jsタグが付けられた新着記事 - Qiita
Viewing all 8833 articles
Browse latest View live

フロント・バックエンドサービスをコンテナ化してもGitコミット時にLefthookでテストやLint実行

$
0
0

TL;DR

  • フロント・バックエンドサービスをそれぞれコンテナ化、docker-composeで全てのコンテナを管理する
  • monorepoで管理した際に1リポジトリとなるので気軽にGit Hookの処理ができない
  • Lefthookを導入してpre-commit時にすべてのコンテナに対してLintツールを動作させるようにした

サンプルコード

https://github.com/MegaBlackLabel/lefthook-docker-node-go-dev-sample

1リポジトリで開発環境を管理したい

渋川さんの記事

マイクロサービスほどじゃないけどウェブサービスを分割開発したい人向けDocker設定を集めるスレ
https://qiita.com/shibukawa/items/fd49f98736045789ffc3

を読んでフロントエンドとAPIがごっちゃになっている開発環境ヨクナイ!ってことでサービス単位でコンテナ化してvs codeのリモートコンテナ機能を使って開発環境を再構築をしていたらGitとGItHooksの扱いで躓く。

Git Hooksの扱い

1リポジトリでサービスをコンテナ化してmonorepoを構築した際に.gitはルートディレクトリのみに存在する。
何が起こるかというと、フロントエンド開発時に「Husky+lint-stagedでコミット前にeslintやprettierを実行してコミット前にソースをチェックする」ができなくなります。これは治安が悪くなるってことで調査を進めた結果、試してみたのがLefthookです。

Lefthookを使ってみる

Lefthookとは?ということで公式の説明を引用

The fastest polyglot Git hooks manager out there

Fast and powerful Git hooks manager for Node.js, Ruby or any other type of projects.

  • Fast. It is written in Go. Can run commands in parallel.
  • Powerful. With a few lines in the config you can check only the changed files on pre-push hook.
  • Simple. It is single dependency-free binary which can work in any environment.
  • GO製。コマンドを並列実行できる
  • かんたんな設定ファイルでpre-pushのhookが使えるようになる
  • (GOによる)シングルバイナリなので、どのOSでも実行可能

今回は以下のことを実装しました。

  • 起動時にdocker-composeでインスタンス起動
  • 起動したインスタンスに対してコマンドを実施
  • 更新対象のファイルの拡張子をgrepして対象の拡張子がstageにあるときのみ実行する

※、余談ですが日本語での紹介記事は2つのみ。1つは公式の翻訳ともう一つはRubyでのHusky置き換え記事です。

Lefthook: 多機能GItフックマネージャ
https://techracho.bpsinc.jp/hachi8833/2019_10_16/79052

Git HooksマネージャーのLefthookを試してHusky(+lint-staged)と比較した結果、乗りかえました
https://blog.solunita.net/posts/change-lefthook-instead-of-lintstaged-with-husky/

Lefthookインストール

Lefthookインストールですが、公式サイトのInstallationか、リリースページから直接ダウンロードします。自分はWindows環境なのでプロジェクト内にlefthook.exeをそのまま置いて使っています。
インストール後に対象のリポジトリで以下のコマンドを実行

lefthook install #Windowsで直下に置いている場合は lefthook.exe install

インストールが完了するとリポジトリのルートに「lefthook.yml」が作成されますので、こちらに設定を書きます。

Lefthook設定ファイル解説

lefthook.yml
# EXAMPLE USAGE# Refer for explanation to following link:# https://github.com/Arkweid/lefthook/blob/master/docs/full_guide.md## pre-push:#   commands:#     packages-audit:#       tags: frontend security#       run: yarn audit#     gems-audit:#       tags: backend security#       run: bundle audit## pre-commit:#   parallel: true#   commands:#     eslint:#       glob: "*.{js,ts}"#       run: yarn eslint {staged_files}#     rubocop:#       tags: backend style#       glob: "*.rb"#       exclude: "application.rb|routes.rb"#       run: bundle exec rubocop --force-exclusion {all_files}#     govet:#       tags: backend style#       files: git ls-files -m#       glob: "*.go"#       run: go vet {files}#   scripts:#     "hello.js":#       runner: node#     "any.go":#       runner: go runpre-commit:piped:truecommands:1_docker-compose:root:.run:docker-compose up -d2_eslint:root:"containers/frontend/"glob:"*.{js,jsx,ts}"run:docker exec -it frontend-container yarn eslint-check3_frontend-test:root:"containers/frontend/"glob:"*.{js,jsx,ts}"run:docker exec -it frontend-container yarn test4_api-test:root:"containers/api/"run:docker exec -it api-container go test

サンプルとインデントの数が違うのはご愛嬌。今回使っている処理は以下の通り。

  • pre-commit: コミット実施前に実行してほしい処理
  • piped:commandsを名前順に実施する。そのため頭文字に数字をつける
  • commands:実行するコマンド
  • root:どの階層でコマンドを実施するかを記載
  • glob:stagedのファイルのうち、コマンド実行対象とするファイルを選別する
  • run:実行するコマンド

コマンドの内容ですが以下の処理を実施しています。

  • docker-composeでコンテナを起動
  • docker execを使用してフロントエンドコンテナでeslint+Prettierを実施
  • docker execを使用してフロントエンドコンテナでテストを実施
  • docker execを使用してAPIコンテナでテストを実施

フォルダ・ファイル構成

以下の想定でファイル構成を行っています。
- フロントエンドとAPIサーバをそれぞれコンテナ化して管理
- docker-composeでコンテナを一元管理
- フロントエンドではeslint+Prettierでのソース整形とテストを実施
- APIサーバではテストを実施
- それぞれ正常に実行時のみコミットを実施する

ファイル構成
lefthook-docker-node-go-dev-sample
│  .gitignore
│  docker-compose.yml
│  lefthook.exe
│  lefthook.yml
│  LICENSE
│  README.md
│
└─containers
    ├─api
    │      docker-entrypoint.sh
    │      Dockerfile
    │      go.mod
    │      go.sum
    │      main.go
    │      main_test.go
    │
    └─frontend
        │  .eslintrc.json
        │  docker-entrypoint.sh
        │  Dockerfile
        │  package.json
        │  README.md
        │  yarn.lock
        │
        ├─node_modules
        ├─public
        │      favicon.ico
        │      index.html
        │      logo192.png
        │      logo512.png
        │      manifest.json
        │      robots.txt
        │
        └─src
                App.css
                App.js
                App.test.js
                index.css
                index.js
                logo.svg
                serviceWorker.js
                setupTests.js

Lefthookでpre-commit時にコンテナに対してコマンド実行

pre-commitをrunしてみる。

実行結果
.\lefthook.exe run pre-commit
RUNNING HOOKS GROUP: pre-commit

  EXECUTE > 1_docker-compose
api-container is up-to-date
frontend-container is up-to-date

  EXECUTE > 2_eslint
yarn run v1.22.4
$ eslint --print-config .eslintrc.json | eslint-config-prettier-check
No rules that are unnecessary or conflict with Prettier were found.
Done in 0.69s.

  EXECUTE > 3_frontend-test
yarn run v1.22.4
$ CI=true react-scripts test
PASS src/App.test.js
  ✓ renders learn react link(39ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        2.081s
Ran all test suites.
Done in 3.23s.

  EXECUTE > 4_api-test
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.

[GIN-debug] [WARNING] Running in"debug" mode. Switch to "release" mode in production.
 - using env:   export GIN_MODE=release
 - using code:  gin.SetMode(gin.ReleaseMode)[GIN-debug] GET    /ping                     --> github.com/MegaBlackLabel/lefthook-docker-node-go-dev-sample.setupRouter.func1 (3 handlers)[GIN] 2020/03/15 - 03:43:28 | 200 |      44.742µs |                 | GET      /ping
PASS
ok      github.com/MegaBlackLabel/lefthook-docker-node-go-dev-sample    0.014s

SUMMARY: (done in 7.78 seconds)✔️  1_docker-compose
✔️  2_eslint
✔️  3_frontend-test
✔️  4_api-test

コマンドが順番に実行されそれぞれの実行結果が表示。最後にサマリーとしてOK・NGが出力されます。

まとめ

  • サービス単位でコンテナ化して開発するのは便利だね。でもGit Hooksの処理ができない
  • Lefthook使えばできるよ。シングルバイナリだから導入もかんたんだよ
  • コンテナ化してもGit Hooksが使えるので複数の開発者がいても治安が維持できそう

以上


@kintone/rest-api-client をGitHubからインストールする

$
0
0

はじめに

@kintone/rest-api-clientの開発中の機能を、GitHubからインストールして早めに使ってみました。
一般的にNPMパッケージをGitHubからインストールするときに比べて、特殊な方法が必要だったのでメモ。

注意

  • Webpackビルド環境がある前提です
  • 開発中のを勝手に使う場合、深刻なバグがある可能性もあるので自己責任で!

ディレクトリ構造

rest-api-clientは、Gitリポジトリとしては特殊な形をしています。

https://github.com/kintone/js-sdk
この@kintone/js-sdkというリポジトリ内に、サブディレクトリとしてrest-api-clientが存在します。
https://github.com/kintone/js-sdk/tree/master/packages/rest-api-client

NPMの世界では @kintone/rest-api-clientという単独パッケージ扱いですが、Gitの世界では単なるサブディレクトリ。
(なんでこんな変な構成なのかは謎。こういうNPMデザインパターンもあるのか?誰か知ってたら教えてください)

なので、GitHubからこんな風にインストールしようとしてもエラーになります。

yarn add https://github.com/kintone/js-sdk/tree/master/packages/rest-api-client

方法その1(Gitのsubmoduleとして使用)

NPMではなく、Gitのsubmoduleとしてインストールします。

mkdir vendor
cd vendor
git submodule add https://github.com/kintone/js-sdk

自分でビルド

cd js-sdk/packages/rest-api-client/
yarn install
yarn build

こんな風にlibフォルダが出来ていたらビルド成功。

$ ls lib
KintoneAllRecordsError.d.ts       KintoneRestAPIClient.js   __tests__/  url.d.ts
KintoneAllRecordsError.js         KintoneRestAPIError.d.ts  client/     url.js
KintoneRequestConfigBuilder.d.ts  KintoneRestAPIError.js    http/
KintoneRequestConfigBuilder.js    KintoneTypes.d.ts         index.d.ts
KintoneRestAPIClient.d.ts         KintoneTypes.js           index.js

使うときは、こんな風に相対パスでjs-sdk/packages/rest-api-clientimportします。

import{KintoneRestAPIClient}from'../vendor/js-sdk/packages/rest-api-client'

これで、2020/3/16時点で未リリースのaddAllRecords関数だって使えちゃいます :smile:
image.png

方法その2(NPMモジュールとしてGitHubからインストール)

最初こっちのやり方考えてたんですが、方法1の方がいいと思ってやめました。

まず、Gitリポジトリ単位でjs-sdkごとインストールしてしまう。

yarn add https://github.com/kintone/js-sdk

そのままでは使えないので、該当ディレクトリに移動して自分でビルド

cd node_modules/@kintone/js-sdk/packages/rest-api-client/
yarn install
yarn build

使うときは、こんな風に@kintone/js-sdk配下のディレクトリをたどってimportします。

import{KintoneRestAPIClient}from'@kintone/js-sdk/packages/rest-api-client/lib'

rest-api-clientまで指定でOKかと思ったら、
rest-api-client/libまで指定しないとうまく動いてくれませんでした。

問題点

ビルド直後はうまく動くんですが、そのあと他のNPMモジュールを追加インストールしたら、せっかくビルドしたrest-api-client/libがきれいさっぱり無くなっちゃいました。インストール毎にクリーンにしてくれるんですね。。

たとえば.gitignoreで該当フォルダを除外しないようにして、libをコミットしたりすればいけますが、node_modulesの中の一部を除外するのがかなり大変だったりするので、やめといた方がよさそう・・・

一応やり方書いておきますが、こんなギャグみたいな.gitignore書いて、

.gitignore
node_modules/*
!/node_modules/@kintone
/node_modules/@kintone/*
!/node_modules/@kintone/js-sdk/
/node_modules/@kintone/js-sdk/*
!/node_modules/@kintone/js-sdk/packages/
/node_modules/@kintone/js-sdk/packages/*
!/node_modules/@kintone/js-sdk/packages/rest-api-client/
/node_modules/@kintone/js-sdk/packages/rest-api-client/*
!/node_modules/@kintone/js-sdk/packages/rest-api-client/.gitignore
!/node_modules/@kintone/js-sdk/packages/rest-api-client/lib/

さらに、こっちの.gitignoreからlibを消しておく。

node_modules/@kintone/js-sdk/packages/rest-api-client/.gitignore
 node_modules/
-lib/
 esm/
 umd/

まぁ、方法1の方が無難ですなw

おわりに

くれぐれも自己責任でね!
僕も正式リリースまでは、プロダクトコードには使いませんから!

Nodeインストール時に困った件

$
0
0

エラー内容

v12.16.1 is not found Can not fetch: https://nodejs.org/dist/v12.16.1/node-v12.16.1-darwin-x64.tar.gz

nodebrewの安定版をインストールしようとした際に
上記のようなエラーが出た。

解決策

どうやら別の方法で再インストール。

curl -L git.io/nodebrew | perl - setup
export PATH=$HOME/.nodebrew/current/bin:$PATH

出てきたPATHをbash.profileに追加。

open ~/.bash_profile

保存して閉じ、再読み込みを行う。

source ~/.bash_profile

参考にしました:
Can not fetch: とか 言われて nodebrew で node のインストールが失敗する

以上、ありがとうございました。

Browser/Node.js両対応、シンプルなHTTPクライアント"bent"

$
0
0

Node.jsでHTTPリクエストしたいなーと思って一番メジャーそうな request - npmを覗いたら deprecatedになってるじゃないですか!

代わりに、シンプルなHTTPクライアントで良いの無いかなーと調べてたら、requestのissuesコメントにあったbentというクライアントに辿り着きました。

bent

使い方はとても簡単。

localhost:3000GETしてレスポンスボディを文字列で受けたい場合...

constbent=require("bent");consthttpGet=bent("http://localhost:3000","GET","string");constresponseBody=httpGet("/");

これだけ。

Node.jsではhttpを、Browserではfetchというように内部で使い分けてるので、コードを統一しつつNode.jsで実行時にはCORS問題が発生しません。

Lambdaプロキシ統合でmultipartのフォームデータをパースする

$
0
0

ファイル添付とかmultipartで送信されてくることがあるので、そのパース方法。
busboyというライブラリを次のように使うことで実現可能。

eventはlambda関数の入力。

    if (event.headers['content-type'] && (event.headers['content-type'] as string).includes('multipart/form-data')) {
        console.log('IT IS MULTIPART');
        const input: CreateMeProfileInput = {};
        const busboy = new Busboy({
            headers: event.headers,
            defCharset: 'utf8'
        });
        return new Promise((resolve, reject) => {
            busboy.on('file', (fieldname: any, file: any, filename: any, encoding: any, mimetype: any) => {
                console.log('File [' + fieldname + ']: filename: ' + filename + ', encoding: ' + encoding + ', mimetype: ' + mimetype);
                file.on('data', function (data: any) {
                    util.inspect(data);
                    console.log(typeof data);
                    console.log('File [' + fieldname + '] got ' + data.length + ' bytes');
                });
                file.on('end', function () {
                    console.log('File [' + fieldname + '] Finished');
                });
            });
            busboy.on('field', function (fieldname: string, value: any, fieldnameTruncated: any, valTruncated: any, encoding: any, mimetype: any) {
                console.log(util.inspect(value));
                console.log(value.toString('utf8'));
                console.log('Field [' + fieldname + ']: value: ' + util.inspect(value) + ` encoding:${encoding}, valTruncated:${valTruncated}, mimetype:${mimetype}`);
            });
            busboy.on('finish',  () => {
                console.log('Done parsing form!');
                console.log(input);
                resolve(input);
            });
            // @ts-ignore
            busboy.on('error',  (err) => {
                console.log('busboy parse error');
                console.log(input);
                reject(err);
            });
            busboy.write(event.body); // 第二引数いらない
            busboy.end();
        });
    } 

入門の次のステップに進めないVue.js学習履歴(随時更新)

$
0
0

概要

ドットインストールの「Vue.js入門」を実施してある程度Vue.jsをわかった気になった。
https://dotinstall.com/lessons/basic_vuejs_v2

しかし、実際にリリースされているVue.jsのソースを見るとさっぱりわからなかった。
このため、疑問点と調査経緯を自分のメモ目的でこの記事に残していく。

疑問点

yarn run xxx

package.jsonのscriptsで定義されたxxxを実行する。

"scripts": {
    "xxx": "~~~~~~~~", ←これを実行
    "yyy": "~~~~~~~~",
    :
    :
  },

■yarn runのドキュメントはこちら。
https://classic.yarnpkg.com/ja/docs/cli/run

app.use(nuxt.render)

expressのミドルウェアとしてNuxt.jsを使う。

 const express = require('express')
 const app = express()
  :
 app.use(nuxt.render)
  :

Node.jsの「ミドルウェア」という概念がいまいちわかっていない・・・。

■API: nuxt.render(req, res)
https://ja.nuxtjs.org/api/nuxt-render/

■ExpressのミドルでNuxt.jsを利用
https://www.wakuwakubank.com/posts/666-nuxtjs-express-middle/

module.exports={}

外部(別ファイル)から参照できるようにする。

module.exports = {
  mode: 'xxx',
  router: {
    base: '/yyy/'
  },

上記の定義をしたjsファイルをrequireすると、jsファイルの中身を参照できる。

■module.exportsとは何か、どうもわからなかったので実験してみた〜Node.jsにて外部moduleをrequireする〜
http://karoten512.hatenablog.com/entry/2018/01/28/191928

this.$store.dispatch(~~~)

「$store」はどこにも宣言されていない。
Vuex(ビューックス?)のステートオブジェクトとのこと。
セッションみないたもの??セッションよりは奥が深そう。

■Vuexステート
https://vuex.vuejs.org/ja/guide/state.html

■Vue.js + Vuexでデータが循環する全体像を図解してみた
https://qiita.com/m_mitsuhide/items/f16d988ec491b7800ace

mockyでリクエストの内容をレスポンスに反映しようとしてハマったメモ。

$
0
0

やりたいこと

node.jsで動くWebAPIモックサーバーのmockyを使って、

{ "name": "John Smith" }

をPOSTリクエストしたら

{
  "id": 1,
  "name": "John Smith"
}

が返却され、

{ "name": "Taro Yamada" }

をPOSTリクエストしたら

{
  "id": 1,
  "name": "Taro Yamada"
}

が返却されるように、リクエストの内容をレスポンスに反映したかったのだが、つまづいたのでメモを残す。
mockyのインストール方法、基本の使い方はGithubのREADMEを参考にしてください。

結論

基本の使い方は公式のREADMEといいつつ、mocky自体かなりメンテされていないみたいなので少し書き方は今風にしてる箇所もあるが基本的に一緒。

mock.js
constmocky=require('mocky');mocky.createServer([{url:'/users',method:'POST',res:(req,res,callback)=>{constparams=JSON.parse(req.body);callback(null,{status:201,body:JSON.stringify({"id":1,"name":params.name})});}}]).listen(4321);

JSON.parseでリクエストのjsonをパースしてあげないとうまく変数を抽出できなかったり、JSON.stringifyでjson形式で書いたレスポンスをstring型に変換しないとうまく動作しない。(ここでつまづいた...😢)
特にJSON.stringifyを忘れたために、

_http_outgoing.js:670
    throw new ERR_INVALID_ARG_TYPE('first argument',
    ^
TypeError [ERR_INVALID_ARG_TYPE]: The first argument must be of type string or an instance of Buffer. Received an instance of Object
    at write_ (_http_outgoing.js:670:11)
    at ServerResponse.write (_http_outgoing.js:638:15)
    at sendRes (/app/node_modules/mocky/lib/mocky.js:166:9)
    at /app/node_modules/mocky/lib/mocky.js:74:10
    at Timeout._onTimeout (/app/mocky.js:16:9)
    at listOnTimeout (internal/timers.js:549:17)
    at processTimers (internal/timers.js:492:7) {
  code: 'ERR_INVALID_ARG_TYPE'
}

というエラーが出続けて泣いた😿

動作確認

これでcurlで確認すればうまくいくはず。

$ curl -X POST 'http://localhost:4321/users' -d '{ "name": "John Smith" }'
{"id":1,"name":"John Smith"}

みにくいのでpython -m json.toolをパイプで渡すと見やすくなる。

$ curl -X POST 'http://localhost:4321/users' -d '{ "name": "John Smith" }' | python -m json.tool
{
    "id": 1,
    "name": "John Smith"
}

Reference

2020年から始めるAzure Cosmos DB - JavaScript SDK (SQL API)を見てみる (Part.1)

$
0
0

th.jpeg

この記事について

本記事は、2020年3月6日 (米国時間) にて、Azure Cosmos DB に新しく Free Tier (無償利用枠) が登場したことに伴い、改めて Azure Cosmos DB を色々と触っていく試みの 3 回目です。
今回は、前回記事にて作成した CRUD アプリ内で使用している Microsoft Azure Cosmos JavaScript SDKについて見ていきたいと思います。

対象読者

  • Azure Cosmos DB について学習したい方
  • Node.js で Azure Cosmos DB への CRUD 操作を行いたい方
  • Microsoft Azure Cosmos JavaScript SDK の動作について理解したい方

Microsoft Azure Cosmos JavaScript SDK

実際に、Microsoft Docsの内容を元に、JavaScript SDK (SQL API) の中身を見ていきます。
今回は Azure Cosmos DB に接続する際に生成する、CosmosClient について確認します。

CosmosClient

TypeDoc の記載は、以下の通りです。

Provides a client-side logical representation of the Azure Cosmos DB database account.
This client is used to configure and execute requests in the Azure Cosmos DB database service.
Azure Cosmos DBデータベースアカウントのクライアント側の論理表現を提供します。
このクライアントは、Azure Cosmos DBデータベースサービスで要求を構成および実行するために使用されます。

CosmosClient.ts
constclient:CosmosClient=newCosmosClient({endpoint,key});

上にある通り、endpointkeyを使用してインスタンスを作成しています。

  • endpoint: Azure Cosmos アカウント URI、
  • key: Azure Cosmos アカウントのプライマリキー or セカンダリキー

どのコンストラクタが動いているのかを確認すると

(alias)newCosmosClient(options:CosmosClientOptions):CosmosClient(+1overload)importCosmosClient

とあり、CosmosClient(options: CosmosClientOptions)が動いているようです。
中身を確認してみます。

Azure/azure-sdk-for-js/sdk/cosmosdb/cosmos/src/CosmosClient.ts
/**
 * Creates a new {@link CosmosClient} object. See {@link CosmosClientOptions} for more details on what options you can use.
 * @param options bag of options - require at least endpoint and auth to be configured
 */constructor(options:CosmosClientOptions);// tslint:disable-line:unified-signatures

あれ、1行しかない?と思ったらすぐその下にconstructor(optionsOrConnectionString: string | CosmosClientOptions)があったので焦りました。。
このコンストラクタは長いので、部分ごとに見ていきます。

コンストラクタ処理 (1)

if(typeofoptionsOrConnectionString==="string"){optionsOrConnectionString=parseConnectionString(optionsOrConnectionString);}

これは単純に、コンストラクタの引数がstringCosmosClientOptionsかを判別しています。
引数が string の場合は、if 文の中で接続文字列を使って CosmosClientOptions を生成し、 optionsOrConnectionString に代入しています。
コンストラクタの引数が string と CosmosClientOptions と異なっているので、お決まりな感じの処理です。
コンストラクタの引数の型が違うからと言い、内容がほとんど重複するようなコンストラクタをしっかり分けて書く人をたまに見かけます。このコードは「そんな無駄なことしなくていいよ」と教えてくれるいい例ですね。

コンストラクタ処理 (2)

optionsOrConnectionString.connectionPolicy=Object.assign({},defaultConnectionPolicy,optionsOrConnectionString.connectionPolicy);

ここでは、CosmosClientOptions の connectionPolicyに値を設定しています。
その前に、CosmosClientOptions って何者だ?、という疑問があるので、先にこちらを見てみます。

Azure/azure-sdk-for-js/sdk/cosmosdb/cosmos/src/CosmosClientOptions.ts
exportinterfaceCosmosClientOptions{endpoint:string;key?:string;resourceTokens?:{[resourcePath:string]:string};tokenProvider?:TokenProvider;permissionFeed?:PermissionDefinition[];connectionPolicy?:ConnectionPolicy;consistencyLevel?:keyoftypeofConsistencyLevel;defaultHeaders?:CosmosHeaders;agent?:Agent;userAgentSuffix?:string;plugins?:PluginConfig[];}

CosmosClientOptions には 11 個のプロパティが定義されています。どうも CosmosClientOptions の中に endpoint や key をはじめとした Azure Cosmos DB を利用するための各種設定値が格納されるようです。
ちなみに TypeScript では、他の言語ではメンバー変数やフィールドと呼ばれる、名前を持ち、指定された型のデータを保持するものをプロパティといいます。

この CosmosClientOptions のプロパティの 1 つに ConnectionPolicyがありますので、先ほどの処理はこのプロパティの値を設定している部分と理解しました。
ConnectionPolicy は何かというのを確認するために、さらに中身をみてみます。

Azure/azure-sdk-for-js/sdk/cosmosdb/cosmos/src/documents/ConnectionPolicy.ts
exportinterfaceConnectionPolicy{connectionMode?:ConnectionMode;requestTimeout?:number;enableEndpointDiscovery?:boolean;preferredLocations?:string[];retryOptions?:RetryOptions;useMultipleWriteLocations?:boolean;

ConnectionPolicy の中には、connectionModerequestTimeoutなどの接続に関するポリシー設定があるようです。(これ以上、クラスを深くみると大変なので、一旦ここまでにします。)

一旦元に戻って、

(再掲)
optionsOrConnectionString.connectionPolicy=Object.assign({},defaultConnectionPolicy,optionsOrConnectionString.connectionPolicy);

をみると、Object.assignを使用しています。
Object.assign() が何かについては、こちらを参照してください。
つまり、CosmosClientOptions の ConnectionPolicy プロパティ内で未定義となっているものについて、デフォルト値を代入している感じです。

コンストラクタ処理 (3)

optionsOrConnectionString.defaultHeaders=optionsOrConnectionString.defaultHeaders||{};optionsOrConnectionString.defaultHeaders[Constants.HttpHeaders.CacheControl]="no-cache";optionsOrConnectionString.defaultHeaders[Constants.HttpHeaders.Version]=Constants.CurrentVersion;if(optionsOrConnectionString.consistencyLevel!==undefined){optionsOrConnectionString.defaultHeaders[Constants.HttpHeaders.ConsistencyLevel]=optionsOrConnectionString.consistencyLevel;}optionsOrConnectionString.defaultHeaders[Constants.HttpHeaders.UserAgent]=getUserAgent(optionsOrConnectionString.userAgentSuffix);

このoptionsOrConnectionString.defaultHeadersは、CosmosHeadersクラスです。
中身を見てみます。

Azure/azure-sdk-for-js/sdk/cosmosdb/cosmos/src/queryExecutionContext/CosmosHeaders.ts
exportinterfaceCosmosHeaders{[key:string]:string|boolean|number;}

キー(key) と キーに紐づく値(value) を格納できるようにしています。いわゆる連想配列の部分です。
連想配列をインタフェースで定義するという使い方もあるんですね。勉強になります。

つまり、ここの処理は、HTTP リクエストの各種ヘッダー情報を CosmosHeaders クラス (連想配列) を使って設定しているという感じです。

コンストラクタ処理 (4)

constglobalEndpointManager=newGlobalEndpointManager(optionsOrConnectionString,async(opts:RequestOptions)=>this.getDatabaseAccount(opts));

新しいGlobalEndpointManagerというヤツが出てきました。
GlobalEndpointManage にある、どのコンストラクタが動いているのかを確認すると

(alias)newGlobalEndpointManager(options:CosmosClientOptions,readDatabaseAccount:(opts:RequestOptions)=>Promise<ResourceResponse<DatabaseAccount>>):GlobalEndpointManagerimportGlobalEndpointManager

が動いているようです。中身を確認します。

Azure/azure-sdk-for-js/sdk/cosmosdb/cosmos/src/globalEndpointManager.ts
exportclassGlobalEndpointManager{privatedefaultEndpoint:string;publicenableEndpointDiscovery:boolean;privateisRefreshing:boolean;privateoptions:CosmosClientOptions;privatepreferredLocations:string[];constructor(options:CosmosClientOptions,privatereadDatabaseAccount:(opts:RequestOptions)=>Promise<ResourceResponse<DatabaseAccount>>){this.options=options;this.defaultEndpoint=options.endpoint;this.enableEndpointDiscovery=options.connectionPolicy.enableEndpointDiscovery;this.isRefreshing=false;this.preferredLocations=this.options.connectionPolicy.preferredLocations;}}

ナンダコレ、、、
となったので、TypeDoc を見てみます。

This internal class implements the logic for endpoint management for geo-replicated database accounts.
この内部クラスは、地理的に複製されたデータベースアカウントのエンドポイント管理のロジックを実装します。

ああ、そういうことか!とこの絵を思い出しました。
Azure Cosmos DB で、これ忘れたらダメなヤツやん。。

azure-cosmos-db.png

Microsoft Docs にある Azure Cosmos DB の概要にも一番最初に

Azure Cosmos DB は、Microsoft によってグローバルに配布されるマルチモデル データベース サービスです。

とあります。
Cosmos DB では、Cosmos アカウントに複数リージョンを関連付けさせることができ、関連付けられたすべてのリージョンにデータがシームレスにレプリケートされるようになっています。これはそれに関連する設定まわりの処理と認識しました。

コンストラクタ処理 (5)

this.clientContext=newClientContext(optionsOrConnectionString,globalEndpointManager);

クライアントコンテキストがやっと出てきました。
どのコンストラクタが動いているのかを確認すると、

(alias)newClientContext(cosmosClientOptions:CosmosClientOptions,globalEndpointManager:GlobalEndpointManager):ClientContextimportClientContext

が動いているようです。

Azure/azure-sdk-for-js/sdk/cosmosdb/cosmos/src/ClientContext.ts
exportclassClientContext{privatereadonlysessionContainer:SessionContainer;privateconnectionPolicy:ConnectionPolicy;publicpartitionKeyDefinitionCache:{[containerUrl:string]:any};publicconstructor(privatecosmosClientOptions:CosmosClientOptions,privateglobalEndpointManager:GlobalEndpointManager){this.connectionPolicy=cosmosClientOptions.connectionPolicy;this.sessionContainer=newSessionContainer();this.partitionKeyDefinitionCache={};}}

(クライアントコンテキストについては、説明するまでもないと思いますが) いわゆる、コンテキストの伝播とか言われているモノです。
ざっくり言うと、クライアントに関連する情報を保持している場所と私は思っています。メソッドの引数にクライアント情報をいちいち与えずに、全部ここを見ましょうよ、的なやつです。(適当)
クライアントコンテキストについて、良い説明となる資料を見つけられなかったので、見つけたら追記したいです。

コンストラクタ処理 (6)

this.databases=newDatabases(this,this.clientContext);this.offers=newOffers(this,this.clientContext);

やっと最後の処理です。
Databasesは、新しいデータベースの作成、およびすべてのデータベースの読み取り/クエリの操作を行うクラスです。
Offersについては、聞きなれない単語だと思います。(筆者もちゃんと理解しているわけではありませんが) REST経由で、SQL APIを使用する際に登場するもののようです。
Offer resource というものがあるんですね。別途、学習を進めたいと思います。

さいごに

今回は、Azure Cosmos DB に JavaScript/Node.js で接続する際に最初に生成される、CosmosClientについて、中身を確認してみました。
今回の内容は、前回、実際に CRUD アプリを作成 (前回記事) した時はたったの 1 行で終わってしまった内容です。

普段のアプリ開発では、あまり意識しない世界なのかもしれませんが、実際にコンストラクタの中で何が行われているのかを確認することは、Azure Cosmos DB の仕組みや使用するライブラリへの深い知見を得るためには必要な事かな、と思いました。

次回は、Databaseクラスについて見ていこうと思います。

関連リンク

前回記事

参考情報

npm

GitHub

Microsoft Docs

developer.mozilla.org


NFCシールを活用して自動打刻ツールを(個人的に)作ってみた話

$
0
0

はじめに

今回はNFCシールを使用して会社の自動打刻システムを、完全に自分用で作ります!

要件定義

なぜつくるか?

現在、弊社の勤怠は、エクセルで管理されています。
実際の打刻フローとしては、

  1. 出社したら出社時刻をエクセル開いて手動で打刻
  2. 保存
  3. 退社するときに退社時刻をエクセル開いて手動で打刻
  4. 保存

。。。
毎日エクセルポチポチするの面倒すぎる!!!!!!!!!

作業自体も面倒なのに、何日か打刻を忘れるとまあ面倒臭いことになります。

せっかくIT企業にいるんだからいろいろスマートにやりたい...
ということで、今回の自動打刻システムの開発を決意しました。

どう作るか?

今回開発する自動打刻システムでは、NFCシールを活用していきます。

処理の流れとしては、

  1. NFCシールにスマホをかざして専用のWEBサイトを表示する
  2. WEBサイトから自動打刻システムにリクエストを投げる
  3. 打刻する

といった感じにしようかと思います。

NFCシールにスマホかざすのとリクエストを投げるところにWEBサイト表示をはさんでいるのは、
意図しない打刻を防ぐためです。

クライアント側で打刻される時刻の確認、出社なのか退社なのかの選択できた方が、
手順は増えますが確実かなあということでワンクッションはさみました。

また、現状では個人用なのでユーザーの識別は行いません。

どう使うか?

想定される使用フローは下記のとおりです。

  1. 出社したらデスクのどこかしらに貼ったNFCシールにスマホをかざす
  2. 表示されるWEBサイトで打刻時間を確認、出社ボタンを押す
  3. (退社時も同じ)

かなりスマート...!(な気がする)

使用技術

インフラはAWSの各サービスを利用します。

メイン処理の部分にはLambda、エクセルファイル(勤怠管理)はS3に保存します。

サーバーサイドにnode.js、WEBフロントエンドにはVue.jsを使用しつつ、HTTP通信はaxiosを使用します。

なぜNFCシールを使うか?

ただ使ってみたかった。

実は今回の開発、個人的にNFCシールを使ったアプリを作ってみたかったので、NFCシールありきで考えていました。

スマホかざすだけで打刻できるのステキじゃん...

なぜnode.jsか?

一番の理由は、Lambdaがnode.jsで書けるからです笑

他にも
java, ruby, pythonなどなど、いろんな言語で書くことができます。

フロントエンド開発に興味があり、日頃からJavaScriptを勉強しているので、
サーバーサイドもJavaScriptで書こう!ということでnode.jsを選びました。

なぜLambdaか?

Lambdaとは、AWSが提供するサーバレスアーキテクチャを構築するためのサービスです。

通常は、EC2インスタンスは常時存在し、アプリケーションも常時起動されているのですが、Lambdaはリクエストが送られてきたときのみインスタンスを生成→アプリケーションを実行→インスタンスを破棄という挙動をします。

今回作成するアプリは常時起動している必要もないので、コストを抑える意味でもLambdaが適しているのではないかと考えました。

なぜVue.jsか?

個人的に使い慣れているのでフロントはVue.jsで書きます。

と言っても、現在時刻を表示するのと、ボタンを二つ配置するだけなので全く難しいことはしません笑

強いて言えば、ローディングのアニメーションを作り込むくらいでしょうか...。

HTTP通信はaxiosを使用します。

設計

アーキテクチャ図

アーキテクチャの全体像としては、下記の通りです。

Image from Gyazo

構成は至ってシンプルで、
HTTPリクエストをAPI Gatewayで受け付け、Lambdaに投げます。

勤怠を管理しているエクセルはS3においておき、Lambdaからそのエクセルファイルに書き込みをしていく感じです。

処理が完了すると、処理結果をSuccessかFailでクライアントに通知します。

アプリケーションの実装

自動打刻システム(node.js)実装

コードの全貌は下記のGitHubリポジトリを御覧ください。

GitHub リポジトリ

エクセルファイルの操作には、「xlsx-populate」というライブラリを使用しました。

最初は「xlsx」というライブラリを使って実装していましたが、このライブラリだと処理をして、保存するとマクロや書式が無効化された状態になってしまうのでつかえず...。

個人的にはドキュメントも「xlsx-populate」のほうが読みやすかったです!

処理としてはファイルを読み込んで、シートを指定して、セルを指定して値を書き込み、保存しているだけです。

弊社の勤怠表は月ごとにシートが分かれているので、処理の頭でDateオブジェクトを生成して、得られた各値でシートや記入するセルを判定しています。

また、弊社は30分ごとに勤務時間として打刻できるので、打刻する時刻を30分単位に変換する関数を用意しています。

今後もっと本格的に運用していくことになったら、このあたりで拡張の余地がありますね。

実装で苦労したのは非同期処理とAWS S3からファイルを取得して、書き込んだものをアップロードし直す処理のところ。

constparams={Bucket:'バケット名',Key:'キー'}s3.getObject(params,(err,data)=>{}

上記のように記述すれば、指定されたバケットのオブジェクト(ファイル)を取得できて、data変数に格納されます。

また、アップロードするときは、

constparams={Bucket:'バケット名',Key:'キー',Body:'アップロードしたいファイル'}s3.putObject(params,(err,data)=>{})

でアップロードできます!

ここがnode.jsの情報がなかなか転がってなくて苦労しました。

取得も書き込みも注意点としては、取ってきたり送信するためには、データ形式に気をつけなければなりません。

今回僕は、これらの処理の前後にエクセルファイルをバッファーに変換する処理をはさみ、変換したものをparams変数のBodyとしています。

クライアントサイド(vue.js)実装

クライアントサイド(WEB)はVue.jsで作りました。

最終的には静的サイトとしてビルドして、Netlifyでホスティングします。

こちらは特に難しいことはしていません。

UIはVuetifyを使ったので適当に作った割には整っています。

スクショですが、下記のようになりました。

Image from Gyazo

打刻すると、vue-loading-templateを使用したアニメーションが流れて、レスポンスが帰ってくるとアラートが表示されます。

Image from Gyazo

(若干左によってるのはスクショが下手だからです...笑)

インフラ環境構築

構築したもの


いよいよインフラの構築に入ります。

今回は、メインのAPIをLambdaで動かします。

勤怠表(エクセルファイル)はS3にアップロードしておき、Lambdaから読み取り、書き込みを行います。

HTTPリクエストの受け口として、APIGatewayを配置します。

ここに想定されるリクエストがとんできたら、それをトリガーにLambda関数が動く仕組みにしていきます。

また、クライアントサイド(WEBアプリ)はNetlifyという静的サイトのホスティングサービスを利用します。

Netlifyに関しては後日別記事で言及します。

ハマったポイント


私はインフラ超初心者なので、インフラ構築でかなりつまづきました...。

LambdaからS3のファイルをとってこれない

作成したLambdaに正しくロールを付与していなかったため、アクセス権限 is 何の状態が1時間くらい続きました...。


Lambda関数(メインAPI)が非同期処理になっていて肝心の処理を行う前にLambdaが終了してしまっていた

今回使用した「xlsx-populate」は非同期処理をすることが前提のライブラリです。

恥ずかしながら、node.jsだけでなく非同期処理の知識も乏しく、Lambdaはエラーなく終了するのに肝心の処理が実行できてない...。

という状態で約5日間潰しました。

エラー箇所の切り出しが下手だったなあと反省しています。

いろんな記事や書籍を読み漁りつつ、async awaitを駆使してなんとか解決しました。

CROS

実際にクライアントサイドからリクエストを投げる時にはまりました。

今までなーんとなくしか理解してなかったですが、これを機にしっかり学べました。

CROSに関しては別記事でまとめます。

実際につかってみた


実際の動画がこちら

明日から出社と退社が楽しみになりそう。

(ちょっと処理は遅いですが)個人的にほぼノンストレスに打刻できるようになったので満足です。

ただ、本当にきちんと勤怠をつけるためには小難しい会社のルールがあるみたいなので、そのうちきちんとしたものも作りたいです。

今のところはほぼ毎日きっちり定時に退社しているので細かい調整はそんなに必要なさそう...だと思ってます笑

おわりに

NFCさいこう!!!!!たのしい!!!!!

コード共有サイト作成 docker react node.js mongodb

$
0
0

成果物

https://code.itsumen.com/

リポジトリ

https://github.com/yuzuru2/code_site

開発環境

  • ubuntu 18.04
  • docker
  • docker-compose

使用ライブラリ周り

フロントエンド

  • parcel
  • bootstrap
  • highlight.js
  • react
  • nginx(静的ファイルを配信)

バックエンド

  • typescript
  • nodejs
  • pm2

データベース

  • mongodb

成果物を使うケース

  • ちょろっと書いたコードを誰かに見せたい時

UI

ホーム画面

無題.png

コード閲覧画面

無題.png

Node.jsでDeprecationWarningを出さずにencodeとdecode

$
0
0

環境

$ node --version
v12.16.1

経緯

Bufferを使って、encodeしたかった時にDeprecationWarningが出てしまったため対処
encode自体は問題なくできるが、nodejsのドキュメント的には対応した方が良さそう

test.js
constbefore='hogehoge'// これをbase64でencodeしたいconstbuffer=newBuffor(before)constafter=buffer.toString('base64')console.log(after)

$ node test.js
aG9nZWhvZ2U=(node:631)[DEP0005] DeprecationWarning: Buffer() is deprecated due to security and usability issues. Please use the Buffer.alloc(), Buffer.allocUnsafe(), or Buffer.from() methods instead.

やったこと

new Bufferの書き方を修正した

test.js
constbefore='hogehoge'constafter=Buffer.from(before).toString("base64");console.log(after)

↓ 無事エラーが消えた!

$ node test.js
aG9nZWhvZ2U=

ちなみに、decodeはこちら

test.js
constafter='aG9nZWhvZ2U'constbefore=Buffer.from(after,'base64').toString()console.log(before)

$ node test.js
hogehoge

参考

https://stackoverflow.com/questions/23097928/node-js-throws-btoa-is-not-defined-error

JavascriptのPromiseを解説します

$
0
0
User.query({where:{name:nm},andWhere:{password:pw}}).fetch()

これの戻り値が Promise オブジェクトが返ります。

(newPromise()).then(成功時の関数).catch(失敗時の関数)

みたいな

Docker-composeを使ってExpressの環境構築

$
0
0

Expressの環境構築

git clone https://github.com/Old-rever-brave/Express-Docker

cd Express-Docker

docker-compose up --build

npm install

npm start

http://localhost:3000/

Node.js/Expressのボディパーサーの仕様確認(エコーサーバー(echo-server))

$
0
0

背景/目的

クライアント側からのリクエストを各種パーサーがどのようなオブジェクトや配列にしてくれるのを確認することを目的にリクエストの情報をまとめてレスポンスにそのまま返却するようなエコーサーバーを作ってみました。
その他、HTTPクライアントのリクエストが想定通りになっているかを確認するのにも使えると思うので記録として残しておきます。

仕様

リクエストの以下の情報をレスポンスJSONにまとめて返す。

  • ヘッダー情報
  • パス
  • HTTPメソッド(Verb)
  • クエリパラメータ
  • リクエストボディ(ある場合のみ)

準備

npmとかnode.jsはインストールされている前提。

mkdir echo-server
cd echo-server
npm init -f
npm install -y express
touch index.js

ソースコード

↑で作ったindex.js

index.js
'use strict';constexpress=require('express');constapp=express();constbodyParser=require('body-parser');// Set body parserapp.use(bodyParser.urlencoded({extended:true,type:'application/x-www-form-urlencoded'}));app.use(bodyParser.json({type:'application/json'}));app.use(bodyParser.raw({type:'*/*'}));// All request process.app.use((req,res,next)=>{res.json({path:req.originalUrl.indexOf('?')===-1?req.originalUrl:req.originalUrl.substring(0,req.originalUrl.indexOf('?')),method:req.method,query:req.query,headers:req.headers,body:req.body?(Object.keys(req.body).length?req.body:undefined):undefined});});// Create HTTP server.consthttp=require('http');constserver=http.createServer(app);constport=process.env.PORT?parseInt(process.env.PORT,10):3000;server.listen(port);server.on('error',(err)=>{console.error(err);});server.on('listening',()=>{console.log('listening on '+port);});

実行

node index.js

実行結果確認

GETリクエスト

以下のようなクエリパラメータ名にすると配列にしてくれるみたいです。

request
$curl -s-X GET -H"Authorization: Bearer dummmy""http://localhost:3000/foo/bar?page=1&size=10&qarr[]=q1&qarr[]=q2&qarr[]=q3"  | jq .
response
{"path":"/foo/bar","method":"GET","query":{"page":"1","size":"10","qarr":["q1","q2","q3"]},"headers":{"host":"localhost:3000","user-agent":"curl/7.68.0","accept":"*/*","authorization":"Bearer dummmy"}}

POST(Json)

これはJSONなのでそのまま出る感じなのでさらっと。

request
$curl -s-X POST -H"Authorization: Bearer dummmy"-H"Content-Type: application/json"-d'{"foo":"foo-value", "bar": "bar-value", "arr": [{"sub": "sub1"},{"sub": "
sub2"}]}' "http://localhost:3000/hogehoge"  | jq .
response
{"path":"/hogehoge","method":"POST","query":{},"headers":{"host":"localhost:3000","user-agent":"curl/7.68.0","accept":"*/*","authorization":"Bearer dummmy","content-type":"application/json","content-length":"81"},"body":{"foo":"foo-value","bar":"bar-value","arr":[{"sub":"sub1"},{"sub":"sub2"}]}}

POST(フォーム)

Web画面(HTML)のフォーム送信(Content-Type: application/x-www-form-urlencoded)のケースです。
これは、 obj[sub]=obj-sub-valueという名前の付け方をすると、以下のようなjsonにしてくれます。

"obj":{"sub":"sub-value"}

ネストしたオブジェクトの場合は nested[sub1][sub2]=sub1-sub2という名前の付け方をすると

"nested":{"sub1":{"sub2":"sub1-sub2"}}

つまり、各フォーム要素のname属性を意識してつければ、例えばBackendのデータベースがMongoDBのようなオブジェクト形式だった場合にサーバー側でFormをオブジェクトの形に整形(組み立てる)処理を書かなくても済むわけです。もちろんJSonスキーマだったり、バリデーションのチェックは必要ですが。。

request
$curl -s-X POST -H"Authorization: Bearer dummmy"-H"Content-Type: application/x-www-form-urlencoded"-d'foo=foo-valoue&bar=bar-value&arr=arr1&arr=arr2&arr=arr3&
obj[sub]=sub-value&nested[sub1][sub2]=sub1-sub2' "http://localhost:3000/foo/bar" | jq .
response
{"path":"/foo/bar","method":"POST","query":{},"headers":{"host":"localhost:3000","user-agent":"curl/7.68.0","accept":"*/*","authorization":"Bearer dummmy","content-type":"application/x-www-form-urlencoded","content-length":"103"},"body":{"foo":"foo-valoue","bar":"bar-value","arr":["arr1","arr2","arr3"],"obj":{"sub":"sub-value"},"nested":{"sub1":{"sub2":"sub1-sub2"}}}}

Node.jsのworker_threadsに何を渡すべきか

$
0
0

久々にScalaの世界からJSの世界に帰ってきました。

1. 本日の課題

本来Node.jsは非同期処理をストイックに突き詰めることでマルチスレッドやマルチプロセスのようなオーバーヘッドを伴う方法よりも高効率に並列処理を実現しているわけです。
ただし、それが有効なのは頻繁に「待ち」≒「I/O処理」が発生する場合に限られます。
ひたすらI/OなしでCPUをぶん回す処理、を複数同時にやれって言われたらシングルスレッドなNodeはマルチスレッドに勝てません。

ですがですが、Nodeにだってマルチスレッド/マルチプロセスの仕組みはあります。

さて今回は、

  • 数百MB~数GB程度のデータ構造(変数として持っている)に対して
  • 秒オーダーの時間をかけておこなう検索処理を
  • 複数回おこなう
  • 元のデータ構造は更新しない(Read Only)

という要件で、この「複数回おこなう」というところをマルチスレッドかマルチプロセスで並列化して時間を短縮したい、というお題になります。1

2.候補

child_process/cluster

どちらもマルチプロセスなアプローチです。
マルチプロセスなので、メモリは各自が持ちます2。GB単位のメモリをそれぞれのプロセスが持ってたらちょっともったいないですね3。ということで早々に候補から排除。

worker_threads

今回の本命です。
本家にはこのように書いてあります。

child_processやclusterとは異なり、worker_threadsはメモリを共有できます。

ふむふむ、スレッドですからね。そりゃそうですよね。ですが続けてこうも書いてあります。

これは、ArrayBufferインスタンスを転送するか、SharedArrayBufferインスタンスを共有することにより行われます。

What's?
つまり、何も考えなくてもメモリが共有されるわけじゃないようですよ。
それが今回の本題というわけです。

3. Bufferを使うということ

ArrayBuffer/SharedArrayBufferというのはつまり中身はバイナリですから、

{"こんな":1,"好き勝手な":"aaa","形をした":["JSONから","作った","object/arrayなんか",],"面倒みないよ":true}

てなもんですよ。最後のtrueがなんだかムカつきますね。
なんとかBufferの基本的な使い方は、Uint32ArrayみたいなTypedArrayをViewとして使うことが多いんではないかと思います。4

メモリは大事にしたい(共有したい)、JSON-likeな構造も扱いたい、そんなわがままに答えてくれるものはないでしょうか。

JSON

メインスレッドでJSON.stringifyしてBufferに載せて、ワーカースレッドでJSON.parseする・・・えーと、parseしてしまったらメモリの共有になってません。ダメです。

JSON以外のシリアライザ

messagePackとかありますね。これもデコードせねばならないのでメモリの共有にはなりません。messagePackならJSONよりエンコード状態のサイズが若干小さいという利点はありますが、デコードしてしまうのでささいな差ですね。5

4. ないものは自分で作ればいいじゃない

そう、それ!
やっと本題だ。
要するに、

  • SharedArrayBufferに載せられて
  • デコードせずとも中身にObjectやArrayのようにアクセスできる

何かを作ってしまえばいいじゃないか、という話。

「デコードせずとも中身にObjectやArrayのようにアクセスできる」?は?

と思ったあなた!
あるんですよ、JSにはいにしえより伝わる黒魔術、その名もProxyが。
つまり、「Object(あるいはArray)に見える」Proxyが動的にBufferをデコードしてあげればいいんじゃないか、と思ったわけです。

この用途だとJSONやmessagePackなどのシリアライザでエンコードしておいて、SharedArrayBufferに載せて、ワーカー側では全体のデコードは「せず」に読み出しの時だけ必要部分をエンコードするようにすればいいわけですが、ここでもう一つ問題が。
JSONは、全体をスキャンしないと、構造が分からない。keyがあるのかどうか、あった場合に何byte目に入っているのかは、先頭から順に追っていかないとわからない。メモリが節約できても、それではあまりに遅すぎる。messagePackも「ほぼバイナリ形式のJSON」であり同じこと。

そういうわけですから、

  • Objectならkeyの有無、Arrayなら配列のサイズがすぐに分かって、
  • 値の格納場所にダイレクトにたどり着ける

そんなデータ構造のシリアライザが欲しい!
ということで作ってみましたよ。

roJson(Gistに飛びます)

使い方(sample.js)もGistに載せてますが一応再掲。
一般的なシリアライザ(JSON/messagePack等)と同じようにごくシンプル。

sample.js
constroJson=require('./roJson');constbuffer=roJson.encode({a:"Hello roJson.",b:1,c:[0,100,200]});constproxiedObject=roJson.getProxy(buffer);console.log(proxiedObject.a);// "Hello roJson."console.log(proxiedObject.c[1]);// 100

長くなったのでいったんここまで。
工夫ポイント、ベンチマークなどは次回!


  1. そんなヘビーな用途にNode.jsを使うなんて、なんてツッコミはなしの方向で。 

  2. OSの機能でプロセス間共有メモリってのがありますからそういうので共有できないことはないです。Node.jsから使う猛者がいるかどうかは知りませんが。 

  3. 昔の知識なので最新は違うかもしれないしOSにもよるんでしょうが、プロセスをforkするとすぐにはメモリはコピーされなくて(つまり共有されていて)、書き込んだ時点でページ単位でコピーしてそれぞれの道を歩むようになってました。大部分のメモリに書き込みが発生しないならそれに頼るのも一つの見識ですね。 

  4. そういうデータを使うのって機械学習とかCG系とかのイメージ。そういう用途Node.js使いますかね・・・。 

  5. あと、JSON.stringify/parseはJSエンジンネイティブ実装だし、最適化されまくってるので速さで勝てません。憎いです。 


Firebaseのアプリケーションで複数のプロジェクトを使用する場合、Firebase app named "xxx" already exists.エラーが出る

$
0
0

はじめに

一つのアプリケーションで複数のFirebaseプロジェクトを利用する際に、最初にServiceAccountを使って初期化するのですが、次に別のファイルとかで使用する際に、同じように初期化しようとすると、すでに同一名で初期化されているとエラーが出ます。
再利用の仕方をドキュメントで見つけられなかったので、備忘録で...

参考

環境

  • Node v13.9.0
  • npm 6.14.1

現象

'Firebase app named "dest" already exists. This means you called initializeApp() more than once with the same app name as the second argument. Make sure you provide a unique name every time you call initializeApp().'
とエラーが出る。

コード公式(2つ目の別プロジェクトの設定読み込み)(databaseをfirestoreに変更)

var admin = require("firebase-admin");

var serviceAccount = require("path/to/serviceAccountKey.json");

var secondaryAppConfig = {
  credential: admin.credential.cert(serviceAccount),
  databaseURL: "https://<DATABASE_NAME>.firebaseio.com"
};
// Initialize another app with a different config
var secondary = firebase.initializeApp(secondaryAppConfig, "secondary");

// Retrieve the database.
var secondaryDatabase = secondary.firestore();

上記のsecondaryDatabaseを別のファイルで利用する

var admin = require('firebase-admin');
var secondaryDatabase = admin.app("secondary").firestore();

以上

Node.jsの設計をつらつらと概観する

$
0
0

株式会社Global Mobility ServiceでソフトウェアエンジニアのインターンをさせてもらっているShirubaです。グローバルな環境で利用されている社会的サービスの開発の一端を担いたい志ある方は、ぜひ緩くお話ししましょう〜。バックエンドはNode.jsを使っています。🙋‍♂️→ 採用ページ


Node.jsについて色々資料を読んでメモをとったりしていたので、一度まとめておきたくて、この記事を書くことにしました。V8やLibuvなど低レイヤ技術の設計をベースにNode.jsを概観していきます。

Node.jsとは

1180px-Node.js_logo.svg.png

Node.js公式によるNode.jsの定義は以下です。

Node.js はスケーラブルなネットワークアプリケーションを構築するために設計された非同期型のイベント駆動の JavaScript 環境です。
https://nodejs.org/ja/about/

Node.jsを理解する上で重要な特徴を定義から抽出すると、以下の3つです。

  • スケーラブル
  • 非同期型
  • イベント駆動

この3つの特徴については後で触れていきます。

Node.jsの内部構造

1*-0Sa0i_g-gcL9sJqvecKEw.png
画像引用:https://blog.insiderattack.net/event-loop-and-the-big-picture-nodejs-event-loop-part-1-1cb67a182810

Node.jsは、いくつかのモジュールを組み合わせて構成されています。Node.jsを理解する上で重要なのは「V8」と「Libuv」です。この2つが、サーバーサイドでのJavascript実行環境を作っています。(クライアントサイドでは、chrome組み込みのv8とhtml5(イベントループ等を提供)でJavascript実行環境が実現されているそう。)

V8

1024px-V8_JavaScript_engine_logo_2.svg.png

どうでもいいですが、V8の読み方は「ヴィーエイト」です。謎に「ブイハチ」って読んでた自分を恥じたい。

V8の定義を公式から引用します。

V8 is Google’s open source high-performance JavaScript and WebAssembly engine, written in C++. It is used in Chrome and in Node.js, among others. It implements ECMAScript and WebAssembly, and runs on Windows 7 or later, macOS 10.12+, and Linux systems that use x64, IA-32, ARM, or MIPS processors. V8 can run standalone, or can be embedded into any C++ application.
https://v8.dev

  • V8っていうのは、Javascript Engineを指します。要するに、Javascriptで書かれているソースコードを受け取って、機械語に変換してOS上で実行してくれるのがV8です。
  • chromeとnode.jsはJavascript EngineとしてV8を採用していますが、それ以外は違います。例えばSafariではV8ではなくJavascriptCoreを採用しています。

ちなみに、EngineだとかRuntimeだとか単語がややこしいのですが、Javascript Engine、 Javascript Runtime、A compiler、Virtual Machineは全てV8を指すと考えて良いそうです。(参考:https://www.youtube.com/watch?v=PsDqH_RKvyc)

また、V8の定義に「ECMAScript」という単語が入っているので定義を引用しておきます。

ECMAScript(エクマスクリプト)は、JavaScriptの標準であり、Ecma Internationalのもとで標準化手続きなどが行われている。
引用:https://ja.wikipedia.org/wiki/ECMAScript

要するに、Javascirptの文法の標準がECMAScriptです。「(Javacsriptで書かれている)ソースコードが何を意味しているのか」を表します。V8が受け取る、Javascriptで書かれているソースコードは極論ただのテキストの塊です。V8は、Javascriptで書かれたソースコードをECMAScriptを用いて解析しています。

V8を理解していなくてもNode.jsのアーキテクチャは理解できるので、V8は後回しにして、この記事の最後で見ていきます。

Libuv

20190111205332.png

Libuvの定義を引用します。

libuv is a multi-platform support library with a focus on asynchronous I/O. It was primarily developed for use by Node.js, but it’s also used by Luvit, Julia, pyuv, and others.
引用:http://docs.libuv.org/en/v1.x/#overview

非同期I/Oは、OSごとに実現方法が異なります。epollを使うOSがあったり、kqueueを使うOSがあったり。(非同期I/Oについては後述。)そこでepollやkqueueなど低レイヤの技術を抽象化したインタフェースを作って、OSを気にすることなく非同期I/Oを使えるようにしようとして作られたのがLibuvです。

Libuvの内部は以下のようにデザインされています。

architecture-2.png
画像引用:http://docs.libuv.org/en/v1.x/design.html#design-overview

ちなみにNode.jsで使われているイベントループを提供してくれているのもLibuvです。

Node.js Bindings

これは、概念的なものです。

v8やlibuvはc++で書かれている一方で、Node.jsを使ってapplicationを作るときに私たちはjavascriptを用います。これがNode.jsの旨みでもあるのですが、私たちはJavascriptで開発しているのに、内部的にはc++で記述されているv8とかlibuvを利用できるのです。

このJavascriptと他のプログラミング言語の橋渡しをしているのがNode.js Bindingsです。

ちなみにNode.js Bindingsは、「Language Bindings」のことを指しています。ということで、「Language Bindings」の定義をwikipediaから引用します。

In computing, a binding is an application programming interface (API) that provides glue code specifically made to allow a programming language to use a foreign library or operating systemservice (one that is not native to that language).
Binding generally refers to a mapping of one thing to another. In the context of software libraries, bindings are wrapper libraries that bridge two programming languages, so that a library written for one language can be used in another language.[1] Many software libraries are written in system programming languages such as C or C++. To use such libraries from another language, usually of higher-level, such as Java, Common Lisp, Scheme, Python, or Lua, a binding to the library must be created in that language, possibly requiring recompiling the language's code, depending on the amount of modification needed.[2] However, most languages offer a foreign function interface, such as Python's and OCaml's ctypes, and Embeddable Common Lisp's cffi and uffi.[3][4][5]
https://en.wikipedia.org/wiki/Language_binding

Node.js Bindingsについて詳しくは触れませんが、Internals of Node- Advance node ✌️が面白かったです。

コアモジュール

Node.jsには組み込みのコアモジュールというものが存在します。コアモジュールは沢山あるので、それぞれの重要度とかはNode.js徹底攻略 ─ ヤフーのノウハウに学ぶ、パフォーマンス劣化やコールバック地獄との戦い方を参考にされたし。

サーバのアーキテクチャ

Node.jsの内部を雑に見渡したところで、Node.jsの設計を見ていきます。
Node.jsで特徴的なのが、採用しているサーバアーキテクチャです。

サーバーのアーキテクチャには、一般的に「Thread Based」と「Event Driven」があります。Node.jsの採用しているサーバアーキテクチャは「Event Driven」、つまり「イベント駆動型」です。(参考:Server Architectures

Thread-based

Thread Basedの場合のサーバの典型的なコードは以下のようになる。

nodes-event-loop-from-the-inside-out-sam-roberts-ibm-5-1024.jpg
[画像引用:https://www.slideshare.net/NodejsFoundation/nodes-event-loop-from-the-inside-out-sam-roberts-ibm]

acceptというシステムコールを通して接続されたコネクションをpthread_createで別のスレッドに渡して、別のスレッドでそのコネクションを処理させます。メインスレッドは、acceptでのブロッキング状態にすぐに戻り、ユーザーからの新しい接続に備えるという流れです。

つまり、ユーザーからのコネクション1つにつきスレッドを1つ作成して、そのスレッドでコネクションに対応しているという訳です。これだとスレッドの無駄使いだし、コンテキストスイッチも発生してしまいます。

このサーバアーキテクチャを図で表すと以下のようになります。

スクリーンショット 2020-03-13 10.08.44.png
[画像引用:Node.jsデザインパターン第2版]

Idle timeも多くなってしまっていることが分かります。このサーバアーキテクチャで出現した問題が「c10k問題」。c10k問題はThe c10k Problemを参考されたし。

wikipediaからc10k問題の定義を引用しときます。

C10K問題(英語: C10K problem)とは、Apache HTTP ServerなどのWebサーバソフトウェアとクライアントの通信において、クライアントが約1万台に達すると、Webサーバーのハードウェア性能に余裕があるにも関わらず、レスポンス性能が大きく下がる問題である。
引用:https://ja.wikipedia.org/wiki/C10K問題

またまた引用します。

preforkモデルのApatchでは、クライアントの接続要求から始まる一連の処理を各プロセスで1接続ずつ処理します。そのため大量の接続を同時に処理するにはその分だけプロセス(またはスレッド)を起動しなければなりません。これでも複数の接続を並行して処理することはできますが、あまり大量のプロセスを起動するとプロセス間コンテキストスイッチのオーバーヘッドが大きくなって性能が劣化します。これがC10K問題の本質です。
引用: nginx実践入門

このc10k問題を解決するのが、非同期I/Oであり、非同期I/Oを用いたサーバアーキテクチャである「Event-Driven」(イベント駆動型)です。

Event-Driven

イベント駆動型のサーバアーキテクチャを理解するためには、まず「非同期I/O」を理解する必要があります。

非同期I/O

Unixには、以下の5種類のI/Oモデルが存在します。

  1. ブロッキングI/O
  2. 非ブロッキングI/O
  3. I/Oの多重化(selectとpoll)
  4. シグナル駆動I/O(SIGIO)
  5. 非同期I/O(Posix.1のaio_関数群)

Node.jsで使われているのは「非同期I/O」です。

スクリーンショット 2020-02-16 9.51.18.png
画像引用:Unix Network Programming

処理をカーネルに任せ、処理が完了したらカーネルが元のスレッドに通知をよこすというI/Oモデルです。ちなみによく聞く「ノンブロッキングI/O」は以下のようなI/Oモデルです。

スクリーンショット 2020-02-16 9.50.52.png
画像引用:Unix Network Programming

図から分かるように、アプリケーション側からカーネルに「データの準備が完了したか」を尋ねる作業をループで繰り返す必要があり、リソースが勿体無いので、イベント駆動型では非同期I/Oモデルが採用されています。

この非同期I/Oモデルを用いることで実現されるのが「イベントループ」です。通知を発生させるイベントを常にループ文で監視していることから「イベントループ」です。また、このおかげでユーザーからのコネクションをシングルスレッドで処理することが可能になります。

スクリーンショット 2020-03-17 21.49.45.png
画像引用:Node.jsデザインパターン第2版

リアクタパターン

このイベントループを用いたイベント駆動型モデルは、リアクタパターンと呼ばれます。(非同期I/Oを用いたイベント駆動型モデルなので、プロアクタパターンと呼ぶのだろうか。「Node.jsデザインパターン第2版」に沿って、ここではリアクタパターンと呼ぶことにします。)

リアクタパターンの定義は以下。

リアクタパターンではI/Oの処理はいったんブロックされる。監視対象のリソース(群)で新しいイベントが発生することでブロックが解消され、この時、イベントに結びつけられたハンドラ(コールバック関数)に制御を渡すことで呼び出し側に反応(react)する。
引用:Node.jsデザインパターン第2版

Node.jsでは、非同期処理を使う場合、イベントにコールバックを持たせて、イベントが終了したものからコールバックを実行しています。ちなみに、Javascriptの関数は第1級オブジェクトなので、関数にコールバック関数を持たせるのが非常に容易です。

リアクタパターンを図で表すと以下のようになる。

スクリーンショット 2020-03-13 10.33.21.png
画像引用:Node.jsデザインパターン第2版

Node.jsでは、ここで説明した「イベント駆動型」モデルが採用されています。ただ、注意したいのは、Node.jsで用いられているイベントループのデザインはこれとは少し異なるということです。

まずNode.jsでは、非同期I/Oを使っている処理もありますが、内部的にスレッドプールを使っている処理もあります。そして2つにNode.jsではイベントキューが複数存在するということです。全てのイベントのハンドラが同一のイベントキューに入れられていくのではなく、イベントの種類に応じて積まれていくイベントキューが異なります。

Libuvが提供する非同期処理のアーキテクチャ

Node.jsで用いられる「イベントループ」を提供しているのがLibuvです。ここではLibuvが提供する以下の概念について見ていきます。

  • Event Loop
  • Handles
  • Requests
  • Thread Pool

イベントループ

イベントループの定義を公式から引用します。

The event loop is what allows Node.js to perform non-blocking I/O operations — despite the fact that JavaScript is single-threaded — by offloading operations to the system kernel whenever possible.
Since most modern kernels are multi-threaded, they can handle multiple operations executing in the background. When one of these operations completes, the kernel tells Node.js so that the appropriate callback may be added to the poll queue to eventually be executed.
引用:The Node.js Event Loop, Timers, and process.nextTick

先ほど紹介したように、「非同期I/O」を可能にするのが「イベントループ」です。ちなみに、イベントループはNode.jsのメインスレッドで、ひたすらクルクル回っています。(ループ文)

メインスレッドを止めてしまうようなタスク(I/Oに関するタスクなど)を入れてしまうと、その処理に時間を食ってしまい、そこでイベントループが止まってしまい、他の処理ができなくなります。そのため、そういった処理に関しては、カーネル内のマルチスレッドを使った非同期I/Oモデルに処理を依頼する訳です。そして依頼したI/O処理が完了したら、登録しておいたハンドラ(コールバック関数)を実行する訳ですが、このハンドラはqueueに入って、メインスレッド(イベントループが回っているスレッド)で順次実行されていきます。この挙動によって、Node.jsの非同期I/Oでは、「競合状態」を気にせずに開発することができます。

Node.jsのイベントループは、いくつかのフェーズから構成されています。このフェーズごとの挙動は、ここでは省略させてもらいます。イベントループに関する分かりやすかった図を載せておきます。

スクリーンショット 2020-03-11 21.49.08.png
[画像引用:https://drive.google.com/file/d/0B1ENiZwmJ_J2a09DUmZROV9oSGc/view]

この図内の「黄色いJSの箱」の部分を詳細に見ると以下のようなループになっています。

スクリーンショット 2020-03-11 21.47.21.png
[画像引用:https://drive.google.com/file/d/0B1ENiZwmJ_J2a09DUmZROV9oSGc/view]

Node.jsのサーバを開始する際にも、イベントループが利用されています。公式ドキュメントの、Node.jsを使ったサーバーを作るためのコードを引用します。

consthttp=require('http');consthostname='127.0.0.1';constport=3000;constserver=http.createServer((req,res)=>{res.statusCode=200;res.setHeader('Content-Type','text/plain');res.end('Hello World\n');});server.listen(port,hostname,()=>{console.log(`Server running at http://${hostname}:${port}/`);});

server.listenで内部的にepollなど非同期I/Oが用いられています。ハンドラは、arrow関数の部分ですね。tcp connectionをacceptした時のコールバックとしてアプリケーションが非同期に実行されるようにコードが書かれています。

HandleとRequest

イベントループ内で処理されるタスクはHandleオブジェクトとRequestオブジェクトの2種類存在します。

Handleは長期間存在することができるオブジェクトで、I/Oが発生していない時でもイベントループを維持します。Requestは短期間存在するオブジェクトで、I/Oが発生している時のみイベントループを維持します。

イベントループは、アクティブなHandlesもしくはRequestsがなければ止まります。

スレッドプール

Node.jsはイベント駆動型のサーバアーキテクチャを採用していることからも、よく「シングルスレッド」だと表現されます。しかしここで注意しておきたいのですが、Node.jsは処理によって、内部的にスレッドプールを使った並行処理を行なっています。

ここから動画「The Node.js Event Loop: Not So Single Threaded」から画像を大量拝借しています。(すごく分かりやすかった。)

例えばCPU intensiveな処理であるcryptモジュールを使ったコードを見てみます。

the-nodejs-event-loop-not-so-single-threaded-15-638.jpg
[画像引用:https://www.slideshare.net/nebrius/the-nodejs-event-loop-not-so-single-threaded]

ここでcrypt.pdkdf2は非同期に実行されています。(引数の最後に、非同期処理特有のcallbackであるarrow関数が見られます。これが無ければcrypt.pdkdf2は同期処理で実行されます。)for文でループさせてcrypt.pdkdf2を2回使用していることに注意して下さい。これをマルチコアで実行すると、実行にかかる時間は以下のようになります。

the-nodejs-event-loop-not-so-single-threaded-16-638.jpg
[画像引用:https://www.slideshare.net/nebrius/the-nodejs-event-loop-not-so-single-threaded]

同じくマルチコア環境で、今度は繰り返し回数を4回にしてみると以下のようになります。

the-nodejs-event-loop-not-so-single-threaded-17-638.jpg
[画像引用:https://www.slideshare.net/nebrius/the-nodejs-event-loop-not-so-single-threaded]

マルチコアで実行しているため前の場合と比べて2倍の時間がかかってしまっていることに注意です。また、preemptiveなマルチタスクとして処理されている(それぞれのタスクを割り当てられたtime sliceごとに実行していく)ので、4回全て同じくらいの処理時間で終了しています。

今度は繰り返し回数を6回にすると実行時間は以下のようになります。

the-nodejs-event-loop-not-so-single-threaded-18-638.jpg
[画像引用:https://www.slideshare.net/nebrius/the-nodejs-event-loop-not-so-single-threaded]

なぜこのようになるかというと、Node.jsが内部的に4つのthread poolをデフォルトで持っているからです。4つのタスクは4つのスレッドを用いてマルチタスクで処理され同時に終わっていますが、後の2つはスレッドが空いてから実行されます。(このthread poolはLibuvによって提供されているもので、環境変数UV_THREADPOOL_SIZEをいじることでthread poolの個数を変えることができます。)

これでNode.jsでは内部的にスレッドプールが用いられていることが分かりました。一方で、先に紹介した通り非同期I/Oも用いられています。非同期I/Oを示すためにhttpsモジュールを使った例も動画で紹介されていたので、見ていきます。

the-nodejs-event-loop-not-so-single-threaded-22-638.jpg
[画像引用:https://www.slideshare.net/nebrius/the-nodejs-event-loop-not-so-single-threaded]

(上のスライドではfor文を2回繰り返していますが、)for文を6回繰り返した場合の実行時間は以下です。

the-nodejs-event-loop-not-so-single-threaded-25-638.jpg
[画像引用:https://www.slideshare.net/nebrius/the-nodejs-event-loop-not-so-single-threaded]

httpモジュールは、thread poolを使わず、OSに依頼してepollなどを使っているため、6 Requestsの場合でもほぼ同時にタスクが終了しています。

では、どの処理がthread poolを使って、どの処理がepollやkqueueなど非同期I/Oを使うのかっていう話になりますが、以下の画像を参照してください。

the-nodejs-event-loop-not-so-single-threaded-29-638.jpg
[画像引用:https://www.slideshare.net/nebrius/the-nodejs-event-loop-not-so-single-threaded]

基本的にはthread poolではなく、OSが提供する非同期I/Oが使われます。じゃあ何故全てOSの非同期I/Oを使わずにthread poolを使う必要があるのか。それは設計上の難しさがあったからだそうです。詳しくはasynchronous disk I/Oを参考にしてください。

繰り返しますが、このthread poolはLibuvが提供しています。Libuvのデザインの画像をよく見ると右にthread poolと載っています。

architecture-2.png

V8

Libuvを一通り見たところで、次はV8を見ていきます。V8は、Javascirptで書かれているソースコードを受け取って、それを解析しcompileして実行します。

V8の機能群

1*QG6GNe2ag-4puxpjc5Y2iw.png
[画像引用:JavaScript V8 Engine Explained]

  • コードを実行する環境として、call stackとheapという概念があります。Javascriptはシングルスレッドで実行される言語であり、call stackは1つだけです。また、オブジェクトをheapに割り当てたりするわけですが、オブジェクトを使い終わったのに割り当てたメモリを解放しないとメモリリークが起きちゃいます。そこでOrinocoというGarbage Collectorの出番となります。
  • V8はJavascriptで書かれたソースコードを受け取って、それを機械語にする必要があります。その際に使われるのがIgnitionというインタプリタとTurboFanという最適化コンパイラです。

(Liftoffについてはこの記事では触れていません。)

V8の処理の流れ

1*ZIH_wjqDfZn6NRKsDi9mvA.png
[画像引用:Understanding V8’s Bytecode]

  • ソースコードをV8に渡す
  • Perserを使ってソースコードを解析。そしてASTという抽象構文木を作る。
  • IgnitionというInterpreterを使ってASTをBytecodeに変換する。(変換されたBytecodeは同じくIgnitionによって実行される。)
  • IgnitionはASTをBytecodeに変換しつつ、その時の変換情報を蓄えている。(Profiilng)
  • 特定の条件下でコードを最適化するために、Ignitionは、蓄えた情報とBytecodeをTurboFanに渡す。
  • TurboFanは、そのコードを最適化された機械語に変換して実行

IgnitionとTurboFanの部分が少しややこしいので、別の画像でも確認しておきましょう。

スクリーンショット 2019-12-12 16.44.07.png
[画像引用:Parsing JavaScript - better lazy than eager? ]

ASTがBytecode generatorによってBytecodeになり、それがIgnitionによって実行されます。(ASTをBytecodeに変換するBytecode generatorと、Bytecodeを実行するBytecode Handlerを合わせてIgnitionと総称しているぽいです。)
TurboFanは、Bytecodeを受け取り、機械語を生成し、それをそのまま実行しています。

ScannerとParserとAST

Scanner

スクリーンショット 2020-03-16 9.55.54.png
[画像引用:Blazingly fast parsing, part 1: optimizing the scanner]

Scannerは、V8に渡されたJavascriptソースコードを字句解析(tokenizer/ lexical analysis )して、tokenに分解します。tokenの定義は以下。

Tokens are blocks of one or more characters that have a single semantic meaning: a string, an identifier, an operator like ++.
引用:https://v8.dev/blog/scanner

パフォーマンス向上のためにScannerで使われている仕組みなど、詳細はBlazingly fast parsing, part 1: optimizing the scannerを参照して下さい。

Parser

字句解析を終えてparserに流れてきたtokenを、ECMAScriptで決められている構文に沿ってabstract syntax tree(AST)にします。この作業をparseといいます。

AST(Abstract Syntax Tree)っていうのは、抽象構文木のことで、プログラムの構造を示すオブジェクトです。

このASTは、Engineのみでなく、transpilerや静的解析にも使われるものです。V8では、このASTを基にBytecodeがIginitionによって作成されます。

ASTの例を1つ見ておきます。(参考:How JavaScript works: Parsing, Abstract Syntax Trees (ASTs) + 5 tips on how to minimize parse time)

以下のJavascriptコードをASTにします。

functionfoo(x){if(x>10){vara=2;returna*x;}returnx+10;}

ASTは以下。

0*mSOIiWpkctkD0Gfg..png
[画像引用:
How JavaScript works: Parsing, Abstract Syntax Trees (ASTs) + 5 tips on how to minimize parse time]

ちなみに以下のサイトで、JavascriptコードをASTに変換して見ることができます。

https://astexplorer.net

ASTに変換するこのParse作業っていうのは結構時間を食うものらしくて、最適化が重要になってきます。以下の画像は、どれだけParseに時間がかかっているかを示す画像。

スクリーンショット 2020-03-10 17.18.37.png
[画像引用:https://docs.google.com/presentation/d/1b-ALt6W01nIxutFVFmXMOyd_6ou_6qqP6S0Prmb1iDs/present?%20slide=id.p&slide=id.g2220ee5730_1_7]

そこでParseを最適化するために「Preparser」と「Parser」の2つのParserがV8では使われています。

スクリーンショット 2019-12-13 0.56.17.png
[画像引用:https://docs.google.com/presentation/d/1b-ALt6W01nIxutFVFmXMOyd_6ou_6qqP6S0Prmb1iDs/present?%20slide=id.p&slide=id.g1d5daf2403_0_153]

関数を除く、トップレベルに記述されているコードは、全て実行されるのでparse作業をしてASTに変換します。一方で関数にはトップレベルに書かれていたとしても結局呼ばれない関数も存在します。その結局実行されない関数に対して、フルでparse作業をすると、parseにかかる時間もメモリも無駄なのです。そこで「Preparse」の出番です。

Preparseでは、ASTを作ったりせず、とりあえず最低限必要な情報だけ作っておき、あとで関数が実際に呼び出されたらフルでParseします。

ちなみに、以下のようなJavascriptソースコードは、Parseという観点では非効率なコードです。

functionsayHi(name){varmessage="Hi "+name+"!"print(message)}sayHi("Sparkle")

このようなコードでは、関数をPreparseした後、その関数をすぐ呼び出すことになるのでフルでParseされます。つまりすぐにParseするのに一度Preparseさせてしまっているのです。詳しくは、How JavaScript works: Parsing, Abstract Syntax Trees (ASTs) + 5 tips on how to minimize parse timeを参照して下さい。

また、Preparserについて詳しく知りたい方は、公式のBlazingly fast parsing, part 2: lazy parsingを参照してください。

IgnitionとTurbofan

CranksahftとFull-codegen

V8ではInterpreterとCompilerとしてCrankshaftとFull-codegenが使われてきたけど、それらがIgnitionとTurbofanに変わりました。

20170803092506.jpg
[画像引用:Node8.3.0でデフォルトになるTF/Iに関わるベンチマークについて]

Crankshaftはtryスコープ、ES2015の最適化(e.g. scope, class literal, for of, etc…)などができなかったことや、低レイヤと高レイヤとの分離がうまくできておらずV8チームがアーキテクチャ依存のコードを大量生成しなければいけなかったことなどから、TurboFanに代わったそうです。

またFull-codegenは機械語にコンパイルするため、メモリを食うこと、またIgnitionが生成するBytecodeが最適化に利用できてそのスピードがFull-codegenよりも早かったことからIgnitionに代わったそうです。

処理の流れ

  • まずASTがIgnitionに渡され、そしてBytecode(機械語を抽象化したコード)が生成され、実行が開始されます。最適化はされていないにしてもIgnitionが素早くBytecodeを生成するため、アプリケーションは素早く動作し始めることができます。
  • Profilerがコードを監視していて、何度も繰り返しコンパイルされている部分、つまり最適化できると推測される部分を見つけます。ここではinline cachesという最適化手法が利用されています。
  • 最適化できる場合は、Ignitionもその情報を活用し最適化しますが、Turbofanも活用します。TurboFanにBytecodeが渡され、speculative optimizationという最適化手法を用いて、最適化された機械語が生成されます。最適化できるという推測が間違っていた場合は、deoptimizeされます。
  • profilerとcompilerのおかげで、徐々にJavascriptの実行が改善されていきます。

interpreter-optimizing-compiler-20180614.png
[画像引用:JavaScript engine fundamentals: Shapes and Inline Caches]

Bytecodeっていうのは、機械語を抽象化したものです。

1*aal_1sevnb-4UaX8AvUQCg.png
[画像引用:Understanding V8’s Bytecode]

BytecodeについてV8の人の説明を引用しておきます。

Bytecode is an abstraction of machine code. Compiling bytecode to machine code is easier if the bytecode was designed with the same computational model as the physical CPU. This is why interpreters are often register or stack machines. Ignition is a register machine with an accumulator register.
引用:Understanding V8’s Bytecode

まとめると、Ignitionは、Bytecode(抽象化された機械語)を素早く生成できるけど、最適化はされていません。Bytecodeは、メモリをあまり食わないという特徴もあります。一方でTurboFanは、最適化に少し時間はかかるけど、最適化された機械語を生成できます。生成されるのが機械語なので、抽象化された機械語であるBytecodeよりもメモリを多く使います。

またIgnitionとTurbofanには様々な最適化テクが使われていますが、ここでは省略します。

  • Speculative Optimization
  • Hidden Classes
  • inline caches

Call stack と Heap

Call stackの定義をwikipediaから引用します。

コールスタック (Call Stack)は、プログラムで実行中のサブルーチンに関する情報を格納するスタックである。実行中のサブルーチンとは、呼び出されたが処理を完了していないサブルーチンを意味する。
引用:https://ja.wikipedia.org/wiki/コールスタック

Call Stackは、コードが実行されていくにつれ、Stack Frameが積み重なっていきます。Call Stackを見ることで、プログラムが今どこにいるのか分かります。一方でHeapは、オブジェクトなどStack Frameのスコープを超えて保持すべきデータに対してメモリが割り当てられる場所です。

img2.jpeg
[画像引用:Confused about Stack and Heap?]

Javascriptをブラウザで実行する際に、以下のようなエラー画面を見たことがあると思います。

1*T-W_ihvl-9rG4dn18kP3Qw.png
(画像引用:How JavaScript works: an overview of the engine, the runtime, and the call stack

これはCall Stackを表していて、この場合だと以下のようなコードが順次Stack Frameとして積み重なっていったということになります。

functionfoo(){thrownewError('SessionStack will help you resolve crashes :)');}functionbar(){foo();}functionstart(){bar();}start();

最後に

4月からNode.jsを離れることになるので、ここまで読んだ記事とか知ったこととかをまとめてみました。間違っている部分とかあれば、指摘くださるとありがたいです。

参考

Node.js(Libuv含む)

Event Loop and the Big Picture — NodeJS Event Loop Part1 https://blog.insiderattack.net/event-loop-and-the-big-picture-nodejs-event-loop-part-1-1cb67a182810?

Don't Block the Event Loop (or the Worker Pool)
https://nodejs.org/ja/docs/guides/dont-block-the-event-loop/

Node.js徹底攻略 ─ ヤフーのノウハウに学ぶ、パフォーマンス劣化やコールバック地獄との戦い方
https://employment.en-japan.com/engineerhub/entry/2019/08/08/103000

The Node.js Event Loop, Timers, and process.nextTick()
https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/#event-loop-explained

そうだったのか! よくわかる process.nextTick() node.jsのイベントループを理解する
https://www.slideshare.net/shigeki_ohtsu/processnext-tick-nodejs

asynchronous disk I/O
https://blog.libtorrent.org/2012/10/asynchronous-disk-io/

http://nikhilm.github.io/uvbook/

Node.jsでのイベントループの仕組みとタイマーについて
https://blog.hiroppy.me/entry/nodejs-event-loop#Poll-Phase

Node.js event loop architecture
https://medium.com/preezma/node-js-event-loop-architecture-go-deeper-node-core-c96b4cec7aa4

今日から始めるNode.jsコードリーディング - libuv / V8 JavaScriptエンジン / Node.jsによるスクリプトの実行
https://blog.otakumode.com/2014/08/14/nodejs-code-reading-startup-script/

Nonblocking I/O
https://medium.com/@copyconstruct/nonblocking-i-o-99948ad7c957

process.nextTick()
https://www.slideshare.net/shigeki_ohtsu/processnext-tick-nodejs

Node.jsでのイベントループの仕組みとタイマーについて
https://blog.hiroppy.me/entry/nodejs-event-loop

ループの中で
https://www.youtube.com/watch?v=cCOL7MC4Pl0&t=1011s

libuv
https://www.youtube.com/watch?v=nGn60vDSxQ4

Node's Event Loop From the Inside Out by Sam Roberts, IBM
https://www.youtube.com/watch?v=P9csgxBgaZ8

イベントループとは一体何ですか? | Philip Roberts | JSConf EU
https://www.youtube.com/watch?v=8aGhZQkoFbQ

V8

JavaScript V8 Engine Explained
https://hackernoon.com/javascript-v8-engine-explained-3f940148d4ef

Understanding V8’s Bytecode
https://medium.com/dailyjs/understanding-v8s-bytecode-317d46c94775

Explaining JavaScript VMs in JavaScript - Inline Caches
https://mrale.ph/blog/2012/06/03/explaining-js-vms-in-js-inline-caches.html

An Introduction to Speculative Optimization in V8
https://benediktmeurer.de/2017/12/13/an-introduction-to-speculative-optimization-in-v8/

How JavaScript works: an overview of the engine, the runtime, and the call stack
https://blog.sessionstack.com/how-does-javascript-actually-work-part-1-b0bacc073cf

How JavaScript works: Parsing, Abstract Syntax Trees (ASTs) + 5 tips on how to minimize parse time
https://blog.sessionstack.com/how-javascript-works-parsing-abstract-syntax-trees-asts-5-tips-on-how-to-minimize-parse-time-abfcf7e8a0c8

Confused about Stack and Heap?
https://fhinkel.rocks/2017/10/30/Confused-about-Stack-and-Heap/

JavaScript Internals: Under The Hood of a Browser
https://medium.com/better-programming/javascript-internals-under-the-hood-of-a-browser-f357378cc922

JavaScript Internals: Execution Context
https://medium.com/better-programming/javascript-internals-execution-context-bdeee6986b3b

How Does JavaScript Really Work? (Part 1)
https://blog.bitsrc.io/how-does-javascript-really-work-part-1-7681dd54a36d

How JavaScript works: Optimizing the V8 compiler for efficiency
https://blog.logrocket.com/how-javascript-works-optimizing-the-v8-compiler-for-efficiency/

V8のIgnition Interpreterについて
https://speakerdeck.com/brn/v8falseignition-interpreternituite?slide=14

V8 javascript engine for フロントエンドデベロッパー
https://www.slideshare.net/ssuser6f246f/v8-javascript-engine-for

JavaScript engines - how do they even? | JSConf EU
https://www.youtube.com/watch?v=p-iiEDtpy6I&feature=youtu.be&t=722

V8: an open source JavaScript engine
https://www.youtube.com/watch?v=hWhMKalEicY&feature=emb_title

Marja Hölttä: Parsing JavaScript - better lazy than eager? | JSConf EU 2017
https://www.youtube.com/watch?v=Fg7niTmNNLg

V8公式
https://v8.dev/blog

nodebrewでNode.jsをインストール(デザイナー向け)

$
0
0

nodebrewとは

Node.js のバージョンを管理するツールです。

なぜNode.js のバージョンを管理するのか

それは Node.js が現在も開発中のプログラミング言語だからです。
日々様々な改善が行われ、新バージョンで機能が変更される、ということもあります。
今まで利用していた機能が、次のバージョンで使えなくなるということもあるのです。
そのようなことを防止するために、どのバージョンの Node.js を利用しているのかを明確にし、そして必要に応じてバージョンを切り替えたりできると便利なのです。

nodebrewのインストール

…の前に豆知識(?)

ターミナルでコマンドを実行したけどきちんと処理されているの?
やたら時間が掛かっているけどいつ処理が終わるの?
…という方。
処理の後に、以下が出たら処理が終わった合図(次のコマンドを打っても良い)です。

自分のPCの名前 ~ % 

それではnodebrewのインストールを始めましょう。

1.ターミナルで以下を実行

curl -L git.io/nodebrew | perl - setup

2.以下が表示されます

  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0
  0     0    0     0    0     0      0      0 --:--:--  0:00:01 --:--:--     0
  0     0    0     0    0     0      0      0 --:--:--  0:00:01 --:--:--     0
100 24634  100 24634    0     0  10473      0  0:00:02  0:00:02 --:--:--  174k
Fetching nodebrew...
Installed nodebrew in $HOME/.nodebrew

========================================
Export a path to nodebrew:

export PATH=$HOME/.nodebrew/current/bin:$PATH
========================================

3.以下を実行します

echo 'export PATH=$HOME/.nodebrew/current/bin:$PATH' >> ~/.profile

4.以下を実行します

source ~/.profile

5.以下を実行します

nodebrew

6.以下のような情報(バージョン情報)が表示されたらnodebrewのインストール成功です

nodebrew 1.0.1

Usage:
    nodebrew help                         Show this message
    nodebrew install <version>            Download and install <version> (from binary)
...

nodebrewでNode.jsをローカルインストール

…の前に豆知識(?)

  • グローバルインストール
    自動的に実行ファイルをパスが通るのでパスを指定しなくて実行できる
    ただし、バージョンアップ等で、動作しなくなるプロジェクトが発生する場合があるので注意
  • ローカルインストール
    プロジェクト単位でバージョンを管理できる
    プロジェクトごとなため、 容量がとられることがあるが、基本的にはローカルインストールを利用すべき

それではNode.jsのローカルインストールを始めましょう。

1.以下を実行します

安定版(Node.js公式サイトに「推奨版」と記載があるバージョン)をインストールするコマンドです。
nodebrew installでインストールすると時間が掛かるので以下を実行してください。

nodebrew install-binary stable

2.以下を実行します

nodebrew ls

3.以下のような情報(バージョン情報)が表示されたらNode.jsのローカルインストール成功です

v12.16.1

current: none

4.以下を実行します(どのバージョンを使用するかの指定:必須)

nodebrew use v12.16.1

5.完了

次回はgulpとgulp-sassの設定を行います。

参考サイト

Vue.js セットアップメモ(Mac)

$
0
0

この記事の目的

Vue.jsの学習を始める際に実施したセットアップ手順のメモです。

開発環境、必要なもの

  • Mac OS Catalina 10.15.2 ←自分の場合(参考程度)

セットアップ

ndenvのインストール

  • 複数バージョンのNodeを切り替えて使用することになると思いますので、ndenvをインストールします。
$ brew install ndenv
  • PATHを追加します
echo 'export PATH="$HOME/.ndenv/bin:$PATH"'
echo 'eval "$(ndenv init -)"'
  • Nodeをインストールするために node-buildをインストールします。
$ git clone https://github.com/riywo/node-build.git $(ndenv root)/plugins/node-build

Node.jsのインストール

  • インストールできるNode.jsバージョンを確認
$ ndenv install -l
  • Node.jsのインストール
$ ndenv install v12.16.1  #LTS版がおすすめ
  • 使用するNode.jsの設定
$ ndenv versions # インストールしているNode.jsのバージョンを確認する
$ ndenv global v12.16.1 # デフォルトバージョンを設定するとき
$ ndenv local v12.16.1 # プロジェクトごとに変更するとき
  • Node.jsのバージョン確認
$ node --version

Vue.jsのインストール

  • Vue CLIのインストール
$ npm install -g @vue/cli
  • Vue cli-service-globalのインストール
$ npm install -g @vue/cli-service-global

動作確認

  • vueファイルを作成します。
$ cat app.vue
<template>
<div id="app">
    <h1>Hello World</h1>
</div>
</template>
  • 実行する
$ vue serve
  • ブラウザで確認する。
    • http://localhost:8080にアクセスしてみてください。
    • 期待通りにブラウザに表示されていたら、セットアップ完了です!

終わりに

  • 最低限のセットアップになりますが、上記の手順で一通りの実行環境は整います。
  • editor用のプラグインの設定等の設定も追記予定です。

補足

  • Google Chrome用にvue-devtoolsをインストールしておくのも必須のようです。

レジの店員を呼ぶアプリをつくた android cordova ハイブリッドアプリ

$
0
0

成果物

https://play.google.com/store/apps/details?id=com.itsumen.regi&hl=ja

リポジトリ

https://github.com/yuzuru2/regi_apuri_sozai

環境

  • node.js 12.14.0
  • cordova 9.0.0
  • java 1.8.0

※javaとandroidのsdkは各自インスコしてパスを通しておいてください。

コマンド

$ npm i -g cordova
$ cordova create sample com.example.sample sample
$ cd sample
$ cordova platform add android
$ cordova plugin add cordova-plugin-volume-control
$ rm-rf www/*$ cd www
$ git clone https://github.com/yuzuru2/regi_apuri_sozai.git .$ cd ..
$ cordova run android
Viewing all 8833 articles
Browse latest View live