バイクとプログラミング

Category: プログラミング

ExperimentalでもESModulesをNodeで使いたい

TypeScriptで慣れたES ModulesをJavaScriptでも使ってみる

Node.jsをv13.8以上にアップグレードする。

sudo n latest && node -v => v13.8.0

package.jsonに追記する。typeのキー名は変わるかもって話があったようだけど、ほぼこれで確定のよう。

{ "type": "module" }

Rename configuration options to avoid “type” term · Issue #312 · nodejs/modules

使う。拡張子の省略は不可能。TypeScriptではできるので気をつける。

// export.js
export const HELLO_WORLD = 'Hello from module!'

// index.js
import { HELLO_WORLD } from './export.js';
console.log(HELLO_WROLD); // => 'Hello from module!' 

実行時にワーニングが出るが、–no-warningで消すことができる。

screenshot

GitHub Actions + SSH で自動デプロイ

忘備録です。

Azureなんかを利用していると特定ブランチの自動デプロイ(Continuous Delivery)は割と簡単ですが、自宅鯖を使っていると何らかの方法でデプロイしなければいけません。

そんなとき、私はいつもCircleCIを使っていましたが、GitHub Actionsの方がリポジトリだけですべて完結できるのでよりかんたんかと思い試してみました。

今回は下記のリポジトリで試しました。
kznrluk/hayaoshi-button-online: URL共有で早押しクイズ大会ができるWebアプリ

YMLの書き方はこんなかんじ。それほど難しいことはやっていません。

- name: deploy
  env:
    SECRET_KEY: ${{ secrets.SECRET_KEY }}
    SSH_USER: ${{ secrets.SSH_USER }}
    SSH_ADDR: ${{ secrets.SSH_ADDR }}
    SSH_COMMAND: ${{ secrets.SSH_COMMAND }}
  run: |
    echo "$SECRET_KEY" > secret_key
    chmod 600 secret_key
    ssh -oStrictHostKeyChecking=no ${SSH_USER}@${SSH_ADDR} -i secret_key "$SSH_COMMAND"

GitHub ActionsはデフォルトでSSHが利用できるので、CircleCIのようにaptでインストールする必要はありません。

ポイントは-oStrictHostKeyChecking=noを指定している点。これがないと普段初回接続で訊かれるようなプロンプトを出そうとしてしまい、エラーとなります。

ssh-agentを使えば秘密鍵をファイルとして保存せずに扱うこともできます。が、今回はとりあえずデプロイすることが目的だったのでパス。

Using a SSH deploy key in GitHub Actions to access private repositories — webfactory GmbH

SSH_COMMANDの内部は以下のような感じ。PM2でサービス化しているのと、Gitから直接Pullしてきているのでシンプルな内容です。このあたりはリポジトリごとに設定変更が必要そう。

sudo pm2 stop app_name && cd ./repo && git pull && npm install && sudo pm2 start app_name

ちなみに、設定の際に何度もサーバへの接続を失敗させてしまい下のようなエラーが発生しました。

ssh_exchange_identification: Connection closed by remote host

エラー内容からは想像しにくいのですが、試行回数が多すぎたときにエラーにする設定だそう。気がついたら直っていたので詳細は不明。

ssh ができない ssh_exchange_identification: Connection closed by remote host のエラー – II

以上です。

10分でできるテスト自動化 JS / TS + GitHub Actions + Morse Unit Testで快適CI

Twitterで紹介しました自作のテストランナー、Morse-Unit-Test(モールス ユニットテスト)ですが、これとGitHub Actionsでうまいことテストが自動化できたので紹介したいと思います。

Morseのテスト結果出力

Morseは簡単シンプルなテストランナーです。テストの定義に複雑なドキュメントを読む必要がなく、必要最小限のテスト環境を提供しているため、小規模なプロジェクトに最適な仕様になっています。

さらに詳しく知りたい方はREADMEを参照してみてください。
morse-unit-test/README-JA.md at develop · kznrluk/morse-unit-test

Morseは人間にとって扱いやすいテストランナーを目指していますが、20KB以下のパッケージサイズ、依存ゼロ、テストの並列実行などなどの性能があり、CIと組み合わせてもストレスなく動作します。

設定レスなので、事前にテストだけ動かせる状態であれば、10分程度でGitHub Actionsと連携させることができるので、今回はその手順の紹介をします。

Step1: ローカルでテストを動かせるようにしておく

なにはともあれMorseをインストールします。

> npm install --save-dev morse-unit-test

インストールが完了したら早速テストを書きます。Morseは適当なファイルにテストを書き、その中でdoTest()関数を呼び出すだけで実行されます。この辺りは別途word-pokerのテストコードやドキュメントを参照してください。

word-poker/morse.ts at master · kznrluk/word-poker

テストが書き終わったらpackage.jsonを編集して、npm testでテストを実行できるようにしておきます。nodeでテストファイルを実行するだけでOKです。

"scripts": {
  "build": "tsc", // JSであれば不要
  "test": "npm run build && node ./build/tests/morse.js"
}

npm testでMorseが走ることを確認したらコミットしてリポジトリにpushします。

Step2: .github/workfrows/nodejs.yml の追加

この作業はブラウザで完結します。リポジトリのページを開いて、NodeJSをクリックします。

Node.jsの’Set up this workflow’を選択する
node-version以外は変える必要ありません

Morseは内部でArray#Flatを使っているので、テスト環境はNode12以上が必須になります。そのため、node-versionを12.xのみに編集してください。この仕様は後でなんとかします。

- node-version: [8.x, 10.x, 12.x]
+ node-version: [12.x]

編集したらコミットして保存します。

これで全ての設定が完了しました!あとは更新をpushするだけでGitHub Actionsで自動的にMorseがテストを行います。

Step3: テスト結果を見る

テスト結果はGitHub Actionsの画面から確認することができます。テストが通ると下のように表示されます。ちゃんとMorseのログも出ています。

テストが成功した場合

テストが失敗している場合は下のような表示になります。GitHub Actionsはコンソール色もそのまま出してくれるのでちゃんと見やすく表示できていますね。

テストが失敗した場合

もちろんプルリクエストの画面でもテスト結果が確認できます。

PR画面でテストに失敗していることがわかる

と、このような感じで簡単に導入することができました。

Morseはすぐ使えるテストランナーです。個人用リポジトリでも簡単に導入することができます。この機会にぜひMorseを使ってユニットテストを始めてみませんか?

TypeScriptで型安全なEventEmitterを実現する

Nodejsでイベント駆動プログラミングをするときに便利なのがevent.EventEmitterですが、これをTypeScriptで利用しようとするとうまく型安全にできません。

const event = new EventEmitter();
event.on('foo', arg => console.log(arg)); 
// argは必然的にanyとなる...
event.emit('foo', 123);
event.emit('foo', '123');
event.emit('foo', {}); // なんでもあり

この問題に対する解決策は以前からいくつか存在していましたが、どの方法もデメリットがある状態でした。

しかし、TypeScript 3.0から導入されたConditional Typesを利用することによりほぼ完全なイベントの型付けを実現できるようになりました。

今回は、それをライブラリとして利用しやすくしたstrict-event-emitter-typesを利用したので、忘備録として紹介します。

簡単な使用方法

大体README.mdに書かれている内容と同じです。

事前の準備として、イベントの型情報をまとめたインターフェースと、EventEmitterを取得します。


import StrictEventEmitter from 'strict-event-emitter-types';

// イベントの定義
interface Events {
  request: (request: Request, response: Response) => void;
  data: String; // (data: String) => void; のショートハンド
  done: void; // 引数なし
}

import { EventEmitter } from 'events';

// イベントエミッタとイベント定義を元にStrictEventEmitterを作成
const ee: StrictEventEmitter = new EventEmitter();

この方法で生成されたEventEmitter(サンプル内ee)は、全て型安全で利用することができます。

利用方法は普段のEventEmitterと同様です。


ee.on('request', (req, res) => ... );
// reqはRequest, resはResponseとして認識される。

ee.on('something', () => ... );
// イベントが存在しないのでコンパイルエラー

ee.on('done', hoge => ... );
// doneには引数がないのでコンパイルエラー

ee.emit('data', 'こんにちは!');
// OK

ee.emit('data', 12345);
// 型が合わないのでコンパイルエラー

簡単ですね。

ただし、TypeScriptの動作により、イベント定義インターフェースに不具合があった場合に不明瞭なエラーメッセージが出現します。

“not assignable to parameter of type ‘unique symbol'” error · Issue #9 · bterlson/strict-event-emitter-types

これはTypeScript側で修正予定だそうです。

Assignability of callback parameter defined with conditional tuple type · Issue #26013 · microsoft/TypeScript

strict-event-emitter-typesをラップする方法について

プルリクエストを送ってある件ですが、元ソース中のListenerTypeがExportされていないため、このライブラリのラッパを作成することができません。

Export ListenerType by kznrluk · Pull Request #13 · bterlson/strict-event-emitter-types

ローカルで編集してしまうか、私のPR元ブランチをクローンして使ってください。

kznrluk/strict-event-emitter-types: A type-only library for strongly typing any event emitter

JavaScript FetchAPIでのPOSTはContent-Typeを指定しない方が良い

TLDR: JSON以外はContent-Type不要。

以前作業したコードで、呼び出されたらAjaxを用いて通信し、レスポンスに対応した適当な処理を挟んでから呼び出し元にデータを渡す、いわゆるラッパクラスのようなものが存在する。

今まで、これらの処理はjQuery.ajax()を利用してきたが、今時jQueryというのもよくないのでFetchAPIへ移行する運びとなった。

新しいコードへの移行はかなり簡単に終了することができたが、FetchAPIのContent-Type指定で多少つまずきがあったのでメモ。

FetchAPIでは、POSTを行う際、bodyに指定されたデータの型が主要なものであればContent-Typeもそれに合わせて自動的に指定される。例えばStringであればtext/plain;charset=UTF-8, URLSearchParamsであればapplication/x-www-form-urlencoded;charset=UTF-8といった具合である。これは仕様に記載されている動作である。

Fetch Standard – https://fetch.spec.whatwg.org/#body-mixin

また、Content-Typeを手動で指定した場合、そちらが優先される。例えばJSON.Stringify()の返り値はStringであるが、Content-Typeを指定すれば勝手にtext/plainにされることはなくなる。

この仕様のため、JSONを除けば大概の送信手段はContent-Typeを指定する必要がない。指定してしまうと、データの型と相違があった場合にサーバがリクエストボディをうまく解釈できない場合がある。

特にFormDataの場合、手動でヘッダを指定するとboundaryを無視することになる。そのためデータがサーバに届いたとしても、それを解釈することができない。これのため一時間ほどハマった。

余談として、当時どうしてもうまく送信できなかったのでFetchのPolyfill実装を読んでみたが、BlobSearchParamsに関する記述はあるのにFormDataに関する記述が無く困った。XHRでFormDataを判別してContent-Typeに指定しているのか?そのうち調べてみたい。

fetch/fetch.js at master · github/fetch – https://github.com/github/fetch/blob/master/fetch.js#L233

以上です。

NodejsのTypeScriptでGlobalな変数定義

ゲームプログラミングにて、様々なステータスを引数で渡すことなくアクセスするためのFluxライクなライブラリを書いた。その際にグローバル変数を定義する必要があり、すこしハマったので調べたことをメモ。

グローバルな型定義を行うにはおなじみの.d.tsファイルを使用する。どこに設置しても良いが、今回は/types/index.d.tsに設置する。また、必ず@types/nodeを入れる必要がある。

index.d.tsファイルの内容は以下のような形。

import { State, Dispatcher } from '../src/xxx/State';

declare global {
    namespace NodeJS {
        interface Global {
            state: State;
            dispatcher: Dispatcher
        }
    }
}

declare global宣言でモジュールの型定義を拡張することができる。もちろんNodejsのglobalオブジェクトはNodeJS namespace内に定義されているため、それを拡張することになる。@types/nodeを必要とする理由はこのことから。

また、index.d.tsをtsconfig.jsonで明示的に読み込む必要がある。

{
  "compilerOptions": {
    ...
    "typeRoots": ["types"], // 型定義ファイルを設置したディレクトリ
    "types": ["node"],
    ...
  }
}

これでglobalオブジェクトを使用する準備ができた。あとはコードを書けばコンパイルできる。

global.dispatcher = new Dispatcher();
global.dispatcher.dispatch('your_event');

以上です。

CyberAgent TechJOBに参加しました

非常に有意義な夏休みになりました。

はじめに

インターンの環境や待遇・イベントについては一緒に参加した方が先に記事にされていますので、ここでは自分と直接関係のあることを書いていきたいと思います。

ぜひ他の方のブログも参照してみてください。一口にCyberAgentのインターンといっても、配属される先によって千差万別あることがわかると思います。

【CATechJob】BLADE XLOADで内定者アルバイトとして学んだこと
https://ameblo.jp/yui-unityengineer/entry-12514915180.html

CyberAgentのAbemaTVでインターンしてきた話
https://blog.p1ass.com/posts/abema-intern/

他の方のブログに書いてあるように、人事の方が企画のイベントやランチがとても楽しかったです。”人事”に持っていたイメージがだいぶ変わりましたし、何を相談しても良い雰囲気があってよかったと思います。

何も考えずにTwitterで「野菜不足だー」と呟いたら人事の宇都宮さんが野菜ジュースを買ってきていただいた時はびっくりしました。

美味しかったです!

ここから下は個人的なことになります。

インターン参加の目的

CyberAgentへのインターンに限らず、エントリーするときに考えていたことをまとめました。

ビジネス的な視点を持ちたい

私はここ一年半ほどフリーランスエンジニアとして活動してきました。直近は主にソーシャルゲームのサーバサイドエンジニアで、自由な働き方のできるフリーランスは学業との兼業に最適でしたが、新卒では正社員として入社できることを目標にしています。

例外もありますが、基本的にフリーランスとは顧客の求めるものを納品して、それの対価を得るものです。そのため、チームとしてプロダクトの価値を最大化することや、中長期に渡った経営・運用の知識を得にくく、どうしても個人戦になってしまうイメージがあります。

そこで、会社員としてエンジニアをするのであれば、技術力を伸ばすことだけではなく、どのようにプロダクトを成長させ、コストを減らし、集客を行うのか、そういったビジネス的な視点を得ていきたいと思うようになりました。

その布石として、参加するインターンでは、ビジネス視点もしくはそれに近いものを取り入れることができれば良いと考えていました。

フロントエンドエンジニアとして

上述した通り、実務経験があるのはほぼサーバサイドのみでした。フロントエンドの実務経験は社内向けツールのみで、jQuery+Ajaxのようなシンプルなものしかありません。

そのため、独学でDOMやUIUX、Reactのようなフロントエンドフレームワークを実践してきましたが、これらのスキルセットを次のステップに進めたいと感じ、教えてもらえる立場になれるインターンであれば、フロントエンドをやってみよう!という気持ちになっていました。

エントリーから参加まで

書類選考 → 一次面接 → 二次面接 → 参加という形でした。参加までは全てオンラインで行われ、Webカメラを使った面接も比較的自由に時間の設定が可能でした。

定型文のような質問はなく、事前に提出したエントリーシートやGitHubをベースに話を進めていただけるので、こちらとしても非常に話しやすかったです。

CAM – Fensiに参加

私はCyberAgentグループのCAMという企業に配属されました。CAMは創業2000年と、多数の子会社が存在するCAグループの中でも歴史の長い企業であり、主にファンビジネスなどのBtoCを扱っています。

その中で、私はFensiのフロントエンドチームに配属されました。Fensiは登録者自身のファンサイトを簡単に作成できるサービスで、リリースされてからまだ三ヶ月ほどしか経っておらず、まだまだ新規開発の勢いが残っている感じでした。

業務内容

最初はソースコードの把握を進めつつ簡単な不具合解消を手伝い、慣れてきたらチケットを消費する形で作業を進めました。

最後の二週間ほどは売り上げページや振り込み可能額表示など、新規追加をイチから作成、みたいなタスクもいただきました。

仕事に対してのレベル感はちょうどよく、得るものと提供できるもののバランスが取れている感じでした。技術的にそれほど難しいことを行なっているわけではありませんが、まともにCSSやWeb Componentsでフロントエンドを書くのは初めてだったので、知見を広げることができたと思います。

一日も暇になる日がなく、毎日がっつりと仕事をさせてもらいました。メンターさんも忙しく、近くにいない日が多かったですが、自分でうまく仕事を回せてたのではないかなぁと思います。

最終日にお酒の席でフィードバックをいただきましたが、質問の仕方や仕事の取り方など高評価をいただけて嬉しかったです。

得られたもの

技術については、フロントエンドで一ヶ月も実務経験を積めばその分だけ上がるのは当たり前なので、それ以外に経験したことをベースにまとめてみました。

他業種との調整

BtoCのプロダクトなので、チームの中にはデザイナさんがいます。仕事の流れとしては、デザイナさんがモックアップしたデザインをベースにコードへ落とし込んでいくような流れになっています。

趣味でUIUXを学んでいたので、チャンスだ!と思い自分のタスク範囲内にあるUIUXをチェックしていましたが、流石は本職のデザイナさん、ほとんど気になるところはありませんでした。

一部、おや?と思うところや細かなカラーコードの違いなど、直接お伺いして自分の考えを伝えたこともあり、その時のフィードバックなど、新鮮な気分を味わうことができました。このような業種間の調整ができたのは、自分にとってプラスになったのではないかなと思います。

余談ですが、UXエンジニアという業種が存在することも在籍中に知りました。せっかくBtoCのフロントエンドエンジニアになるのであれば、UI・UXにも気を配れるエンジニアになれたらいいなと思います。

UXエンジニアとアクセシビリティエバンジェリストによる品質と開発速度が担保されたチームのつくりかた | FEATUReS

共通化とお金

これは上述したビジネス視点を持ちたいという点に繋がる成果でした。

コンプラ的な問題から、詳しく書くことはできませんが、Fensiはサービス自体が社内で重要な立ち位置を持っています。簡単に書けば共通化という話になりますが、様々なサイトでシステムが共通化できるようによく考えて作られており、そこが興味深かったです。

メンターさんのお話では、CTO発案のシステムだそうです。共通化によって何割もコストを削減できているようなので、やはり上を目指すのであればこのようなお金周り、運用周りを考えることのできるエンジニアになる必要があるのだなと感じました。

新規開発のスピード感

Fensiの開発は全体的にとてもスピード感がありました。ソーシャルゲーム開発現場では見られない、出してから考えるスタンスで開発を行っていたため、ほぼ必須のように思える売り上げ系の画面の実装がリリース後だったりして面白かったです。

私が担当した振り込み可能額表示画面でも、ユーザに売り上げが発生したことを確認してから開発を行うことが決まるなど、要所要所で優先順位が変化する様子がわかりました。

Facebookの”Done is better than perfect”を思い出させるような現場だったと思います。

不満点

特にこれといった不満点はありませんでした。

強いて言うならモニタが27インチFHDとあまり品質の良いものではなく、コントラストが低めで見にくかったことくらいでしょうか。

人事さんとの面接でも不満点があればと聞かれましたが、本当にストレスレスな環境だったと思います。

今後について

インターン参加で始まった就活ですが、今後も様々な企業を視野に入れて活動していきたいと思います。特に今興味があるのはゲームのサーバサイド、もしくはWebサービスのフロントエンドですので、これらの知見を伸ばしていくことができればいいなと考えています。

一方で、この二つは分野的にあまり重なるものではないので、今後どちらか一つに絞っていく必要がありそうです。どちらも自分にとって興味のある分野なので、就活を進めつつ技術の選択もしていきたいと思っています。

おわりに

割と真面目な感じになってしまいました。余裕があれば、次はエンジョイ編と題してこの続きを雑多にまとめたやつを作れればと思っています。

今回のインターンでは様々な方にお世話になりました。おかげさまで非常に楽しく勉強になる一ヶ月を送ることができました。本当にありがとうございました。

裏には寄せ書きもいただきました

以上です。

ES Modulesではシビアな循環参照

タイトルの通りです。ES Modulesでどハマりしました。

DOMを所定の形式に変換して出力を行う処理を書いていましたが、その中で循環参照が生まれ二進も三進もいかなくなってしまったためメモを残します。

下記のようなファイル群を用意します。Chrome Version 74.0.3729.169で再現。

// script.js
import ModuleA from './ModuleA.js';

new ModuleA().createModuleB();
// ModuleA.js
import ModuleB from './ModuleB.js';

export default class ModuleA {
    createModuleB() {
        return new ModuleB();
    }
}
// ModuleB.js
import ModuleA from './ModuleA.js';

export default class ModuleB extends ModuleA {
}

上記のコードをHTMLから読み込むと下記のようなエラーが出ます。

ReferenceError: can't access lexical declaration `ModuleA' before initialization

まさか循環参照が悪さをしているとは知らず、直感的にはわかりにくいエラーで大変混乱しました。これは普段宣言していない変数を参照しようとすると出るエラーです。

これを解決するには、ModuleA.jsを読み込む前に、あらかじめModuleB.jsを読み込んでおく必要があります。

// script.js
import ModuleB from './ModuleB.js'; //  ここで読み込む
import ModuleA from './ModuleA.js';

new ModuleA().createModuleB();

これで問題なく読み込むことができました。

まだ理解が浅く、なぜこうなるのかを断言できないので詳しくは次回以降にメモできればいいかなと思います。

JavaFX CanvasでUndo/Redo

タイトルの通りです。大学の課題でJavaFXを使ったペインタを作成中です。

全体のソースはこちら。
kznrluk/4s-program-painter: ネットワークプログラミング応用演習 – 簡易ペインター

JavaFXのCanvasは他のCanvasと同じように、描写した内容を逐一保持しないため、Undo / Redoの処理には多少工夫が必要になります。

  • MOUSE_PRESSEDからMOUSE_RELEASEDまでの軌道を保持する。
  • Canvasの現時点の画像を取得して保持する。
  • Canvasのインスタンスをコピーする(実際にできるか不明)。

選択時点で軌道を保持して再描写する際にどれほどのコストがかかるのか不明だったため、Canvasの画像を取得する二番目を選びました。今考えてみると、画像で保持するのもそれはそれでコストがかかる気がしますが。

実装は比較的簡単です。履歴を保持するクラスを用意します。

JavaではArrayDequeという便利な配列があるので、それを使っています。

https://github.com/kznrluk/4s-program-painter/blob/master/src/Painter/Controller/ImageHistory.java

描写開始時に現在の描画内容を画像化して保存します。

this.canvas.addEventHandler(MouseEvent.MOUSE_PRESSED, event -> {
    WritableImage snapshot = new WritableImage(CANVAS_SIZE_X, CANVAS_SIZE_Y);
    this.canvas.snapshot(null, snapshot);
    this.history.add(snapshot);

    currentPen.mousePressed(event);
});

Undo / Redo時にスタックされている要素を取得し、キャンバスに描写します。

void undo() {
    WritableImage image = this.history.getLast(this.canvas.snapshot(null, null));
    GraphicsContext gtx = this.canvas.getGraphicsContext2D();
    gtx.drawImage(image, 0, 0);
}

void redo() {
    WritableImage image = this.history.redo(this.canvas.snapshot(null, null));
    GraphicsContext gtx = this.canvas.getGraphicsContext2D();
    gtx.drawImage(image, 0, 0);
}

描写終了時ではなく、描写開始時に履歴を作成することがポイントです。描写終了時に履歴を作成すると、Undoする場合に現在の描画内容が一度出現してしまいます。

そのうちしっかり書き直したいですが忘備録として。とりあえず以上です。

Powered by WordPress & Theme by Anders Norén