Hatena::ブログ(Diary)

Ko-Taのバ・ー・ルのようなもの

2017-08-23

ChakaraCore + TypeScript

ChakraCoreに手を出したのは5割ぐらいはTypeScriptしてみたかったからです。

アプリ組み込みでどんな感じに楽できるのかやってみましょう。

TypeScriptインストール

TypeScriptは簡単に言ってしまえば動的言語を静的言語にして、ヒューマンエラーをコンパイラで出来るだけ検出させる仕組みです。

function型も引数による定義付けが(違う引数のfunction型を弾く)可能です。

  • TypeScript

https://www.typescriptlang.org/

nodeJSのバージョン管理ツール上で提供されていますので、npm経由で取得します。詳細は省略します。オフィシャルに書いてありますし。

nodeJsがまだの場合はインストールが必要です。

Editor

  • VSCode

https://code.visualstudio.com/

対応エディタはいろいろありますが、とりあえずVSCodeでも使ってみましょう。デフォルトで入れた瞬間から対応していますし、リアルタイムにコード補間とエラーチェックまでしてくれます。どのあたりまで補完&エラー補助してくれるのかも試す意味で使ってみましょう。

コンパイル

まず最小構成を作ってコンパイルしてみます。

  • main.ts
function main(s:string):void{
    console.log(s);
}
main();

VSCodeのターミナルからTypeScriptコンパイラである「tsc」を使用し、JSを出力させます。

tsc main.ts
  • main.js
function main(s) {
    console.log(s);
}
main("hallo world :)");

ここまでが基本です。

日本語も使うと思うので保存の際は一応bomをつけておきましょう。

VSCodeだと右下の文字コードをクリックすると保存形式の案内が出ます。

組み込み関数の定義

プログラム(アプリ)側から関数を組み込んだ場合、そのような関数は定義されていませんよ?とエラーが発生します。

どういった関数を組み込んだかを定義する .d.ts ファイルの作成が必要になります。

専用の定義構文が必要なのですが、ts からコンパイラで自動生成してくれる機能があるので、それを使うと簡単です。

  • app.ts
// void imp_print(string s);
function imp_print(s:string){ }

ターミナルから -d オプションでコンパイル。

tsc -d app.ts
  • app.d.ts
// void imp_print(string s);
declare function imp_print(s: string): void;

これを先ほどの main.ts に読み込ませます。

  • main.ts
/// <reference path="./app.d.ts" />
function main():void{
  imp_print("hello world :)");
}
main();

これで組み込み関数 imp_print が使用可能&インテリセンスが利きます。

VSCodeの場合は、なぜかもう一度コンソールで .d.ts を生成しなおさなくても、.ts があればそちらから情報をとってくるみたいです。

とはいえ、作業中は恩恵にあずかり、最後はちゃんと生成したほうがいいでしょう。

jsdoc(ドキュメントコメント)

jsdocコメントもつけちゃうと、.d.ts でもインテリセンスの際に説明文が出ます。

  • app.ts
/**
 * 組み込み。コンソール出力。
 * @param {string} s 文字列
 */
function imp_print(s:string):void{ }

ファイル分割と結合

早速難物がやってきました。

開発においてはモジュールは分割して表記、管理します。ただし、JavaScriptは今のところまだまだモジュール組み込みの環境が整っていないのもあり最終的に1つのファイルにまとめる必要があります。これはChakraCoreにおいても同様です。


単に結合すれば良いかというと大きく分けて二種類が存在します。

TypeScriptコンパイラが持つ単純な結合方法、commonJS形式(AMD形式など)によるrequire命令を使用する方法。

この2つの手法をミックスできれば問題では無いのですが、TypeScriptコンパイラはそれを許していません。また、2つそれぞれの方法で最終的に同じようなJSを出力しようとすると、ソースの表記が若干変わってくる点も問題です。コンパイラマクロでもあれば良かったのですが…。


というわけで、JSとしてもライブラリを出力したい!webとnodeJS両対応にしたいなども考え出すと、非常に頭が痛い問題です。

commonJS方式、require使用

この方式は最終的に require によるファイルのインポート命令に展開され、ファイルは結合されません。なので出力されるJSが元に近いので(この段階では)可読性があります。

また今後もファイルインポート周りで手が入る手はずなので将来性は保証されていると思います。

最大の欠点は、このままではChakraCoreが読み込んでくれないということです……。


まず、sub.ts を main.ts にインポートする構成を作成。

  • sub.ts
/// <reference path="./app.d.ts" />
export function sub_test():void{
    mp_print("sub test :)");
}
  • main.ts
// main.ts
/// <reference path="./app.d.ts" />
import * as sub from "./sub";
function main(s:string):void{
    imp_print(s);
    sub.sub_test();     //sub.ts
}
main("hallo world :)");

sub.ts は export を忘れずに。export が1つも無いと main.ts 側でエラーが出ます。最初なんだこれ?って思いました。

ではコンソールでコンパイルします。main.ts を指定するだけで関連するソースをすべてコンパイルしてくれます。ありがたい。

tsc main.ts -module commonjs
  • main.js
"use strict";
exports.__esModule = true;
/// <reference path="./app.d.ts" />
var sub = require("./sub");
function main(s) {
    imp_print(s);
    sub.sub_test(); //sub.ts
}
main("hallo world :)");

吐き出された main.js には require が挿入されています。が sub.js はこの段階では組み込まれていません。

また exports というのが追加されていますがこの子はモジュール単位の子でグローバルな情報としては使えません。


nodeJSならここで終わり。

JSエンジンにぶちこんで動かすには、これらを1つのファイルに結合しないといけません。

結合にはweb開発で主流な webpack とか browserify があります。

browserifyで結合

  • browserify

http://browserify.org/

何回書いても覚えられないつづりの browserify を試してみます。インストールはnpm任せです。

インストールの後、コンソールから結合します。

browserify main.js -o out.js
  • out.js
(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
"use strict";
exports.__esModule = true;
/// <reference path="./app.d.ts" />
var sub = require("./sub");
function main(s) {
    imp_print(s);
    sub.sub_test(); //sub.ts
}
main("hallo world :)");

},{"./sub":2}],2:[function(require,module,exports){
"use strict";
/// <reference path="./app.d.ts" />
exports.__esModule = true;
function sub_test() {
    imp_print("sub test :)");
}
exports.sub_test = sub_test;

},{}]},{},[1]);

ごちゃごちゃっとしてますが、即時実行関数になって、独自 require 関数を定義の後、中のコードを実行してる感じです。ちゃんと動きます。

さて、これの問題は、main()などの位置がtsで書いたつもりの位置と微妙に異なることです。グローバルオブジェクト直下じゃありません。なので、プログラムから引いてくるのが困難なことです。

よく見ると「exports」というのが存在しますがこれもモジュール単位なので使えません。


結局なにも手を加えない状態ではアプリからは完全に孤立した空間になるので、アプリとの連携が絶望的です。

グローバルで値をやりとりするなら、JS側でグローバルを取得、そしてそこへ外に出したいものを手動で書くほかないかなと思います。

ただし、グローバルの取得はちょっと細工しないと環境依存の元なので、npmから取得するのが良いみたいです。

  • get-global

https://www.npmjs.com/package/get-global


もう一つは、グローバルを一切介さない手法にするのが、モダンかも知れません。

ユーザー専用の受け渡しテーブルを作ってやり取りするか(アプリ側が作成、またはJSで作成した場合はアプリ側へ通達)、またはアプリ側が呼ぶ取得用関数をJS側から通達するのが良いのかも知れません。

// entry call function from app
imp_entryappevent(app_oninitialize,"initialize");
imp_entryappevent(app_onupdate,"update");
...

webではクリーンな感じですが組み込みで使うには面倒な感じですね。

TypeScriptコンパイラで結合

次はTypeScriptコンパイラによる結合です。と言っても、もう使ってますが「/// <reference path="" />」になります。

require と違ってそのまま追加するだけなので、import のように指定の名前空間に変更することができません。

なので最終的に同じようなJSに展開させようとすると、ソース側に namespace{} を追記する必要があります。

  • sub.ts
/// <reference path="./app.d.ts" />
namespace sub {
    export function sub_test():void{
        imp_print("sub test :)");
    }
}
  • main.ts
/// <reference path="./app.d.ts" />
/// <reference path="./sub.ts" />
function main(s:string):void{
    imp_print(s);
    sub.sub_test();     //sub.ts
}
main("hallo world :)");

コンソールから--outFile を使って結合します。この時--module commonjsなどは使用できません。

import 構文などを使っているとエラーが発生します。

tsc main.ts -outFile out.js
  • out.js
/// <reference path="./app.d.ts" />
var sub;
(function (sub) {
    function sub_test() {
        imp_print("sub test :)");
    }
    sub.sub_test = sub_test;
})(sub || (sub = {}));
/// <reference path="./app.d.ts" />
/// <reference path="./sub.ts" />
function main(s) {
    imp_print(s);
    sub.sub_test(); //sub.ts
}
main("hallo world :)");

JSとして見るならかなりシンプルな展開です。ちゃんとグローバルオブジェクトに展開しています。逆に exports がなくなりました。

この時点で1つのファイルに結合されているので、ちょっとでも規模が大きくなると可読性は最低ですし、各ファイルのJSも出力されません。

プログラムから値引いてくる場合はこちらのほうがとても素直な印象です。


分割したい

手動でぶつ切り、後でプログラムでテキスト的に結合という手段がありますが、browserifyだともうちょっと賢い手法が使えます。

パラメータ -x で目的のモジュールを抜くことで分離が可能です。

browserify main.js -o out.js -x sub.js
  • main.js
(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
"use strict";
//  application implement function 
/// <reference path="./imp.d.ts" />
exports.__esModule = true;
/// <reference path="./sub.ts" />
var sub = require("./sub");
var main;
(function (main_1) {
    main_1.g_value = "test code";
    function app_initialize() {
        imp_print("initialize");
    }
    function app_finalize() {
        imp_print("finalize");
    }
    function app_update() {
        imp_print("update");
    }
    function main() {
        sub.test("aaaaa");
        return "script run";
    }
    imp_function(app_initialize, "initialize");
    imp_function(app_finalize, "finalize");
    imp_function(app_update, "update");
    main();
})(main = exports.main || (exports.main = {}));

},{"./sub":"/sub.js"}]},{},[1]);

sub.ja 相当のコードが抜かれても、requireなどはちゃんと残ってます。

で、sub.jsを突っ込めば動くかというと動かなくて、require が独自定義というわけなのでsub.jsを準じた感じに変換しないといけないわけです。公開モジュールというわけだ。

browserify -r sub.js -o sub_r.js

エラー。ファイルがありません。うん????

browserify -r ./sub.js -o sub_r.js

ハメですかねこれ。ハメですね。

webであれば

<script src="sub_r.s" />
<script src="out.js" />

みたいな感じで動くようになるので、要するに out.js の最初にこのコードを付け加えれば動きます。


たまにエラーが出ました。

原因は不明ですが公開モジュールの名前が変になったことがあります。これだと sub_r.s の1行目の453列あたりに表記されていますので確認すると良いでしょう。

正解のケースでは「./sub.js」という名前になっていますが、エラーが出た場合は違う書式になっているかも知れません。

filename : out.js
error : ScriptError
description : Cannot find module '/sub.js'
message : Cannot find module '/sub.js'

とエラーに要求名が書かれるのでそれに合わせれば解決できるでしょう。

「:」で強引にモジュール名を変えることも可能です。

browserify -r ./sub.js:./hogehoge.js -o sub_r.js

整合性がとれなくなるので、使うことはあんまり無いとは思いますけど。

所感

TypeScriptを使えばJSの面倒なところも全部見てくれるぜ!って訳でもなく、良くも悪くもしっかりJSの上に乗っかってる感があります。

個人的にはどうにかcommonJS型(モジュール形式でファイル分割)で開発したいのですが、nodeJS以外のマルチプラットフォームも考慮するのであればTypeScritコンパイルで結合して1つのファイルにする方が無難かな……というところで落ち着きました。

散々browseなんとかの説明までしておいて申し訳ないです。いつか役に立ちますって…たぶん:Q

スパム対策のためのダミーです。もし見えても何も入力しないでください
ゲスト


画像認証