@jsakamoto:Web系一般
2024-02-17T18:13:52+09:00
developer-adjust
C#、ASP.NET、TypeScript、Angular を中心にプログラミングに関した話題を諸々。
Excite Blog
構文ハイライトライブラリ PrismJS を ES モジュールとして使いたかったので雑に書き換えてみた話
http://devadjust.exblog.jp/29885234/
2024-02-17T16:44:00+09:00
2024-02-17T18:13:52+09:00
2024-02-17T18:12:19+09:00
developer-adjust
Web系一般
Web ブラウザ上で、HTML や各種プログラミング言語のコードを記したコンテンツに、構文ハイライトを適用する JavaScript ライブラリとして、著名なもののひとつに Prism というライブラリがある。
この Prism を使う簡単な方法としては、公式が用意しているダウンロードページ上で、構文ハイライトを効かせたい言語やプラグインなどを指定の上、その指定のとおりカスタマイズされた "prsim.js" をダウンロードして使う。
そうしてダウンロードした prism.js を HTML ページ上で script タグで読み込んでおくと、読み込み時に自動で構文ハイライトすべき要素を探し出して適用するほか、"Prism" というオブジェクトがグローバル名前空間に設置されるので、この Prism オブジェクトの "highlightElement" などのメソッドを使って、個別に構文ハイライトを適用したりすることができるようになる。
ES モジュール形式で使いたい
しかし今回は、この Prism を ES モジュール形式として他の JavaScript モジュールから import して使いたい需要が発生した。
ちなみに、公式からダウンロードしてきた prism.js を、他の JavaScript モジュールで import すれば (下記コード例)、HTML ページ上の script タグで読み込んだのと同様の振る舞い・効果となる。すなわち、読み込み時にその時点の HTLM コンテンツに対し構文ハイライトすべき要素を自動で探し出して適用し、かつ、グローバル名前空間に Prism オブジェクトが設置される。
// この JavaScript コードは <script type="module"> で読み込む
await import('./prism.js');
しかし今回やりたいのはそういうことではなくて、グローバル名前空間は汚染させずに、import した prism.js モジュールの export しているメンバーを呼び出すように使いたい (下記コード例)。
// こうやって import して、
const { Prism } = await import('./prism.js');
// こんな感じで使いたい!
Prism.highlightElement(element);
だが当然のことながら、公式からダウンロードした prism.js は ES モジュール形式ではないので、上記コードのようなことはできない。
いっぽうで、Prism は npm パッケージとしても配付されている。
この npm パッケージ版と何かしらのバンドラーとで頑張れば、Prism の ES モジュール版をビルドできそうにも思えた。しかし構文ハイライト対象の言語をカスタマイズするためには、Babel やそのプラグインなども構成する必要があるようで、なかなかに面倒臭そうで気持が萎えてしまった。今どきは ChatGPT に聞きながらやればできちゃうのかもだが。
とまぁ、そういうわけで、今回は、雑に、直接に (公式のダウンロードサイトでカスタマイズしてダウンロードした) prism.js を直接書き換えて、なんちゃって ES モジュール化して使うことにした。
雑に prism.js を書き換えて ESM 化
さてそのやり方だが、まず、prism.js がやっていることを突き詰めると、究極的には window オブジェクトに "Prism" というオブジェクトを生やしているだけである。
window.Prism = ...;// (※prism.js の実装を簡略化した擬似コード)
そこで、この既存の prism.js のコードを、まるまる、"window" という名前の引数を持つアロー関数で囲う。そうすると、Prism オブジェクトがこのアロー関数のスコープ内に閉じ込められ、グローバル名前空間に露出しなくなる。
// "window" という名前の引数を持つアロー関数で囲む。
((window) => {
// すると本物の window オブジェクトではなく、この関数の引数として
// 渡されたオブジェクト (以下のとおり空のオブジェクト) に Prism
// オブジェクトを生やすことになり、グローバル名前空間に漏れ出なくなる。
window.Prism = ...; // (※prism.js の実装を簡略化した擬似コード)
})({}); // 引数に空のオブジェクトを渡して即時実行。
そして、このアロー関数から明示的に Prism オブジェクトを戻り値で返すようにし、その戻り値 (= Prism オブジェクト) を ES モジュール形式でエクスポートする。
// このアロー関数の戻り値 (=Prism オブジェクト) をエクスポートする。
export const Prism = ((window) => {
window.Prism = ...; // (※prism.js の実装を簡略化した擬似コード)
return window.Prism})({}); // この関数内の window.Prism を返す。
これで以上のように書き換えた prism.js を、他の JavaScript モジュールからインポートして、グローバル名前空間を汚染することなく、以下のように利用することができるようになった。
// こうやって import して、
const { Prism } = await import('./prism.js');
// こんな感じで使える! 且つ、グローバル名前空間に Prism が漏れない!
Prism.highlightElement(element);
まとめ
今回は、構文ハイライトライブラリ Prism について、公式のダウンロードサイトでカスタマイズしてダウンロードした prism.js ファイルを、雑に直接書き換えて、なんちゃって ES モジュール形式にしてみた。
理想的には、ちゃんとしかるべきビルドパイプラインを組んでカスタマイズされた prism.js を ES モジュール形式でビルドするのが良いのだろう。
いっぽうで、今回行なった Hack は、「 export const Prism = ((window) => { 」と「return window.Prism})({}); 」という、各1行のコードでオリジナルの JavaScript コードを挟むだけなので、雑ではあるけれども、極めて迅速に ES モジュール形式にすることができたので、妥協案のひとつとして、そんなに悪くない着地点ではないか、と思っている。
とはいえ、実はすごく間抜けなことをしているのかもしれないので、「いやいや、そんなことしなくても、ES モジュール形式にできる / ES モジュール形式で入手できるよ!」というツッコミがあれば、ぜひぜひ、SNS 上で言及してもらうか、コメント欄にてお知らせ頂きたい。
]]>
"React での ASP.NET Core" テンプレートで生成されるプロジェクトの仕組みを調べてみた
http://devadjust.exblog.jp/29304896/
2022-08-20T16:39:00+09:00
2022-08-20T16:46:24+09:00
2022-08-20T16:39:32+09:00
developer-adjust
Web系一般
最近の Visula Studio または .NET SDK のプロジェクトテンプレートから上記構成のプロジェクトを新規作成すると、
React 側の開発環境構成で提供される開発用サーバーでコンテンツをサーブし、React 開発のツールチェインにてホットリロードなどを実現しつつサーバー側実装への API 呼び出しは、同開発用サーバーによる HTTP プロクシ機能で ASP.NET Core サーバープロセスへ要求を転送
という構成でできあがる。
ざっくり言うと上記のとおりであるわけだが、もう少し詳しく、上記構成がどのように構築されているか、調べてみた。
ASP.NET Core サーバー側
React + ASP.NET Core 構成のプロジェクト (.csproj) と、単独の ASP.NET Core サーバープロジェクトとの違いとして大きく目に付くのは、まずは Microsoft.AspNetCore.SpaProxy NuGet パッケージを参照していることだ。
<!-- .csproj -->
...
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.SpaProxy" Version="6.0.8" />
</ItemGroup>
...
Visual Studio 等でこのプロジェクトを開き、F5 ないしは Ctrl + F5 などでプロジェクトを実行すると、順番としてはまずは ASP.NET Core サーバープログラムがビルド、起動される。
そしてこのとき、Microsoft.AspNetCore.SpaProxy NuGet パッケージに収録されているアセンブリ (.dll) に棲息している各種実装が、React 側のビルドと開発サーバーの起動を行なうようだ。
実は、Microsoft.AspNetCore.SpaProxy NuGet パッケージをパッケージ参照でプロジェクトに追加するだけでは、この仕掛けは発動しない。
Microsoft.AspNetCore.SpaProxy をパッケージ追加しただけでプロジェクトを実行しても、あくまでも開発者が実装した (おそらくは Program.cs から始まるであろう) ASP.NET Core サーバープロセスが稼働するだけである。
Microsoft.AspNetCore.SpaProxy パッケージ内のアセンブリに収録されているコードが、ASP.NET Core サーバープログラム起動時に同時に実行されるのには、もうひとつ仕掛けがあって、それは、環境変数 "ASPNETCORE_HOSTINGSTARTUPASSEMBLIES" の指定だ。"./Properties/launchSettings.json" による起動プロファイルを確認するとわかるのだが、この起動プロファイルによって、Visual Studio や .NET CLI からのプロジェクト起動時に、環境変数 "ASPNETCORE_HOSTINGSTARTUPASSEMBLIES" に "Microsoft.AspNetCore.SpaProxy" というアセンブリ名を設定してからプログラムが起動するように指定されている。
// ./Properties/launchSettings.json
...
"profiles": {
"...": {
"environmentVariables": {
...,
"ASPNETCORE_HOSTINGSTARTUPASSEMBLIES": "Microsoft.AspNetCore.SpaProxy"
},
...
環境変数 "ASPNETCORE_HOSTINGSTARTUPASSEMBLIES" による効能について詳細はマイクロソフトの公式ドキュメントサイトを参照頂きたいが、
とにかく、起動プロファイルによって、環境変数 ASPNETCORE_HOSTINGSTARTUPASSEMBLIES に "Microsoft.AspNetCore.SpaProxy" が設定されていることで、ASP.NET Core サーバープログラムの起動時に、Microsoft.AspNetCore.SpaProxy パッケージ内に実装されているスタートアップ処理も読み込まれるらしい。
なお、Microsoft.AspNetCore.SpaProxy が実行する React 側開発サーバーの起動コマンドや、作業フォルダ、React 側開発サーバーのリッスン URL は、下記例のようにプロジェクトファイル (.csproj) 内に MSBuild プロパティ値として記載されている。
<!-- .csproj -->
...
<PropertyGroup>
...
<SpaRoot>ClientApp\</SpaRoot>
<SpaProxyServerUrl>https://localhost:44474</SpaProxyServerUrl>
<SpaProxyLaunchCommand>npm start</SpaProxyLaunchCommand>
...
Microsoft.AspNetCore.SpaProxy はこのプロジェクトファイル (.csproj) 内の指定を参照して動作するというわけである。
さてところで、起動プロファイル ./Properties/launchSettings.json を改めて確認すると、プロジェクト起動時にブラウザで開く URL は明示的に指定されていない。明示的指定がない場合は、Visual Studio ないしは .NET CLI は、ASP.NET Core サーバーのリッスン URL をブラウザで開くように動作する。しかし冒頭で説明したとおり、このプロジェクト構成では、ブラウザで開くべきは、React 側の開発サーバーのリッスン URL である。
実はここでも Microsoft.AspNetCore.SpaProxy が一役買っている。このプロジェクトの起動時、たしかにいったんはブラウザは ASP.NET Core サーバーのリッスン URL を開いて起動する。しかしこのブラウザからの要求を、Microsoft.AspNetCore.SpaProxy が捕捉し、ひとまず「待機中」の Web コンテンツを返すのだ。いっぽうで、Microsoft.AspNetCore.SpaProxy は、先に説明したとおり React 側のリッスン URL をプロジェクトファイル (.csproj) による指定にて把握しており、その React 側のリッスン URL でサーバーが開始しているかどうかをポーリングして確認する。その結果、Microsoft.AspNetCore.SpaProxy が、React 側開発サーバーが起動していることが確認できると、先ほどまずはいったんブラウザに返した「待機中」Web コンテンツを介して、ブラウザを React 側の開発サーバーのリッスン URL にページ遷移させるのだ。
こうした起動プロセスを経て、フロント Rect + サーバー ASP.NET Core の構成の開発環境でブラウザ起動までが実行されるというわけである。
React フロント側
さて、ここまでの説明でおわかりのとおり、React 開発サーバーは ASP.NET Core サーバープログラム (に注入された Microsoft.AspNetCore.SpaProxy) から起動され、その起動コマンドは前述のとおり "npm start" と設定されていた。
ところで、このプロジェクト構成における React 開発サーバー、素で "npx create-react-app" で作成した場合と異なり、(http ではなく) https プロトコルでリッスンしていたり、ASP.NET Core サーバー側で処理すべき URL への要求をプロクシ機能で中継したりするようになっている。その仕掛けがどうなっているのか見てみよう。
まずは React 側の packages.json を見てみる。"npm start" で実行される、"scripts" ノードの "start" スクリプトは、"react-scripts start" と設定されている。これは素で "npx create-react-app" で作成した場合と同じである。しかしそれとは別に、素で作った場合と比べて、"prestart" スクリプトが追加されているのがわかる。
// packages.json
...
"scripts": {
"prestart": "node aspnetcore-https && node aspnetcore-react",
"start": "react-scripts start",
...
この "prestart" スクリプトは、npm の動作仕様上、"npm start" コマンドを実行の際に、"start" スクリプトの先に実行されるスクリプトだ。
そしてその "prestart" スクリプトには、"aspnetcore-~" というファイル名で始まる、何やら ASP.NET Core と関係してそうな名前の JavaScript ファイルを Node.js で実行するスクリプトが指定されている。
つまり、React 開発サーバー起動の前に、これら "aspnetcore-~" という JavaScript プログラムが Node.js 上で実行されるわけだ。ではこれら JavaScript ファイルが何をやっているのか見てみよう。
aspnetcore-https.js
まずは "aspnetcore-https.js" から。この JavaScript コードが行なっているのは、React 開発サーバーが https でリッスンするようにするための、開発サーバー用サーバー証明書の発行だ。実装としては、この JavaScript プログラム内から "dotnet dev-certs" コマンドを実行することで、.NET SDK によって自己署名証明書を発行するようになっている。
ちなみに "aspnetcore-https.js" JavaScript プログラムの実行によって生成されるファイルは、PEM 形式の証明書である .pem ファイルと、秘密鍵である .key ファイルの 2つのファイルで、そのファイル名は既定では React 側の "packages.json" で指定されるパッケージ名 ("name" ノードの値) となっている。
これらファイルの保存先フォルダは、Windows OS 環境だと、"%USERPROFILE%\AppData\Roaming\ASP.NET\https" フォルダであるようだ。
aspnetcore-react.js
次に "aspnetcore-react.js"。こちらは、先に説明した "aspnetcore-https.js" の実行によって生成された自己署名証明書を、React 開発サーバーが使うように環境設定する JavaScript プログラムとなっている。
具体的には、React 側における ".env.development.local" 環境変数設定ファイルの自動編集という形で実装されている。Rect 開発サーバーは、環境変数 "SSL_CRT_FILE" と "SSL_KEY_FILE" のそれぞれに、証明書ファイルと秘密鍵ファイルのパスを設定しておくことで、https でリッスンできるようになるらしい。ということで、"aspnetcore-react.js" は、最終的に、下記のような内容が記載されているよう、React 側の ".env.development.local" 環境変数設定ファイルを生成・更新するようになっている。
# .env.development.local
SSL_CRT_FILE=C:\Users\...\AppData\Roaming\ASP.NET\https\....pem
SSL_KEY_FILE=C:\Users\...\AppData\Roaming\ASP.NET\https\....key
なお、証明書と秘密鍵を与えただけでは、React 開発サーバーは https でリッスン "できるようになる" だけで、そのままでは https でリッスンするわけではない。React 開発サーバーを https でリッスンさせるには、環境変数 "HTTPS" に "true" を設定しておく必要がある。そして実際、このプロジェクト構成では React 開発サーバーは https でリッスンしてくれるわけだが、ではどこで環境変数 "HTTPS" を設定しているのか。
.env.development
その設定場所は、React 側の環境変数設定ファイルのひとつである ".env.development" である。この環境変数設定ファイルは、プロジェクトテンプレートからのプロジェクト新規作成時に同時に生成されており、その内容は以下のようになっている。
# .env.development
PORT=44474
HTTPS=true
BROWSER=none
上記のとおり ".env.development" に環境変数 "HTTPS" に "true" を設定する指定が含まれている。この設定により、React 開発サーバーは、".env.development.local" で環境変数に設定されたパスの証明書及び秘密鍵を用いて、https でリッスンするようになる。
その他、リッスンするポート番号も、上記例だと 44474 に設定されている。ここのポート番号は、ASP.NET Core 側のプロジェクトファイル (.csproj) 内で指定した、React 側開発サーバーのリッスン URL の指定と一致している必要がある。そうでないと、ASP.NET Core サーバープログラムが、React 開発サーバーが起動しているかどうかの検知ができないからだ。
<!-- .csproj (再掲) -->
...
<PropertyGroup>
...
<SpaProxyServerUrl>https://localhost:44474</SpaProxyServerUrl>
...
および、".evn.development" には、環境変数 "BROWSER" に "none" の指定も追加されている。この指定がない場合は、"react-scripts start" による React 開発サーバー起動時に、ブラウザもいっしょに起動してくれる。しかしこの React + ASP.NET Core 構成の場合、Visual Studio ないしは .NET CLI によるプロジェクト起動によっても、./Properties/launchSettings.json の指定に従ってブラウザが起動する。つまりこのままだと、Visual Studio 上で Ctrl + F5 とかでプロジェクト起動したときに、ブラウザウィンドウが 2 つ開いてしまうわけだ。そこで、環境変数 "BROWSER" に "none" を設定することで React 側のブラウザ起動を抑止することで、プロジェクト起動時にブラウザウィンドウが 2つ開いてしまわないように構成しているわけである。
なお、React 側の環境変数設定ファイル ".env.development.local" であるが、ファイル名が "~.local" で終わる環境変数設定ファイルは、開発者個々人の環境に依存する設定を記述するファイルであるため、ソースコード管理には登録しない (してはならない) ファイルである。そのため、プロジェクトテンプレートからプロジェクト新規作成したときに、下記内容を含む ".gitignore" ファイルも同時に生成され、Git リポジトリにソース管理登録されないように構成されている。
# .gitignore
...
.env.local
.env.development.local
.env.test.local
.env.production.local
...
プロクシ
さてここまでで、React 開発サーバーを https でリッスンさせて起動するところまではわかった。残るは、ASP.NET Core サーバーへのプロクシによる中継方法である。これはどのようにして実現されているのだろうか。
プロジェクトテンプレートにて生成される React 側のファイルのひとつに、"./src/setupProxy.js" という JavaScript ファイルがある。もうファイル名から自明のように、この JavaScript プログラムが React 開発サーバー内のプロクシ機能を担っている。
この、プロジェクトテンプレートで作成された "./src/setupProxy.js" であるが、内容を見てみると、ASP.NET Core サーバーのリッスン URL は、環境変数 "ASPNETCORE_HTTPS_PORT" や "ASPNETCORE_URLS" を見て、実行時に動的によしなに判断してくれている。
いっぽうで、「どんな URL パスに対する要求は、ASP.NET Core サーバーへ中継するのか?」という判断は、どうも、この "./src/setupProxy.js" JavaScript プログラム内にハードコードされているように見える。
下記はその該当箇所である。
// ./src/setupProxy.js
...
const context = [
"/weatherforecast",
];
...
ASP.NET Core サーバー側で提供する Web API エンドポイントの URL パスが、すべて "/api/~" で始まる、などの規則性があれば、"./src/setupProxy.js" は下記のように書き換えておけばよいだろう。
// ./src/setupProxy.js
...
const context = [
"/api/",
];
...
そのような規則性がない場合は、対象となる URL パスを、"./src/setupProxy.js" にちまちまと書き記していくしかなさそうである。
// ./src/setupProxy.js
...
const context = [
"/weatherforecast",
"/userprofile",
"/draft",
"/articles",
"/tags",
...
];
...
まとめ
以上、React + ASP.NET Core プロジェクトの開発環境は、上記のようになかなかに凝った仕掛けで実現されていることがわかった。
だがひとたび上記のように理解できると、応用力が格段に上昇する。
例えば、Visual Studio ないしは .NET SDK のプロジェクトテンプレートから作られる React + ASP.NET Core プロジェクトだと、React 側は TypeScript に対応されていないのだが「やっぱり TypeScript で書きたい!」という場合も、
いったん ClientApp フォルダ以下はバッサリ削除して、改めて "npx create-react-app --template typescript" で TypeScript 構成込みで React 側コードを生成しなおし、あとは、"aspnetcore-https.js"、"aspnetcore-react.js"、".env.development"、"./src/setupProxy.js" を復元、"packages.json" の "prestart" スクリプトを書き足し直し
といった手順で対応可能であることがわかるだろう。
まぁ、この例だと、プロジェクトテンプレートが生成した結果に TypeScript 対応するよういじったほうが早いかもしれない。
しかしまぁ、とにかく、仕掛けがわかればできることも増えるはずだ。
]]>
node のバージョンあげたら node-sass の npm instal でこけた話 - windows-build-tools 入れずに解決させる
http://devadjust.exblog.jp/27615182/
2019-05-24T19:43:00+09:00
2019-05-24T19:43:36+09:00
2019-05-24T19:43:36+09:00
developer-adjust
Web系一般
先日、しばらく前に構築した、とある Web アプリ (SPA) プロジェクトを保守する必要が出てきた。
久しぶりに当該プロジェクトをリモートリポジトリからローカル開発環境に git clone し、とりあえず npm install を実行したところ、いきなりエラーになってしまった。
npm ERR! node-sass@4.7.2 postinstall: `node scripts/build.js`
npm ERR! Exit status 1
npm ERR!
npm ERR! Failed at the node-sass@4.7.2 postinstall script.
どうやら、SaSS を CSS へ変換する Node.js 用ライブラリ node-sass のパッケージインストールでこけたっぽい。
使用している node-sass のバージョンは v.4.7.2。
開発当時はちゃんとクリーンビルド成功してたのに、なんで?
Node.js のバージョンが当時と今とで違う当該プロジェクトの開発当時と、今回とで環境で変っていたのは Node.js のバージョンだった。
開発当時、インストールされていたのは、たしか Node.js の v.8 系。
現在は Node.js を v.10.15.3 にアップデートしたあとの環境である。
どうも、この Node.js のバージョン違いが、開発当時は問題なくて、現在だと npm install できなくなる原因らしい。
さて、この npm install できなくなる問題をどうにかしないと、本題の保守作業に入ることもままならない。
どうしたものか。
Python や C/C++ の環境が必要?インターネット検索した感じだと、どうやら、node-sass のバイナリビルドのために、Python 2.x や C/C++ の環境が必要らしい。
で、macOS や Linux 系列だと、だいたい Python や gcc 入ってたりするので、すんなり node-sass のバイナリビルドが済んでしまう模様 (あくまでもネットで聞きかじった情報であるが)。
ところが Windows OS ではそのあたりでひっかかって、node-sass のバイナリビルドができずに npm install がコケて終わり、となるらしい。
たしかに自分の場合も、それっぽいログが表示されていた。
gyp ERR! stack Error: Can't find Python executable "python", you can set the PYTHON env variable.
gyp ERR! stack at PythonFinder.failNoPython (C:\Work\app1\node_modules\node-gyp\lib\configure.js:484:19)
gyp ERR! stack at PythonFinder.<anonymous> (C:\Work\app1\node_modules\node-gyp\lib\configure.js:509:16)
gyp ERR! stack at C:\Work\app1\node_modules\graceful-fs\polyfills.js:282:31
このような場合の定番の解決方法は、「npm install --global windows-build-tools」を実行して、Windows OS 上でもそれらビルドに必要な環境を npm でインストールしてしまえばよいようだ。
でもちょっと待て欲しい。
開発当時は、そのようなビルドツールが未インストールでも、すんなり npm install できていたのだ。
なんでだろう?
ビルド済みバイナリが GitHub 上にある!いろいろ調べていくうちにだんだんとわかってきた。
まず、node-sass の Node パッケージスクリプトは、常にはバイナリビルドを実行しようとはしない。
まずは node-sass の GitHub リポジトリに登録されている、ビルド済みバイナリのダウンロードを試みるようだ。
そして、ダウンロードしようとしている node-sass のビルド済みバイナリのファイル名の末尾には、対応している Node.js 実行環境の "モジュールバージョン" が含まれていた。
具体的には、Windows OS 用バイナリの場合、
「win32-x64-{ここにモジュールバージョン}_binding.node」
というファイル名になる。
node-sass の GitHub リポジトリで調べてみると、node-sass v.4.7.2 のビルド済みバイナリは、モジュールバージョン 57 まで登録がある。
しかし今回の開発環境 Node.js v.10.15.3 のモジュールバージョンは 64 であった。
ということで、node-sass の GitHub リポジトリには登録のない、"v.4.7.2 で、且つモジュールバージョンが 64" であるビルド済みバイナリのダウンロードを試みて失敗 (HTTP 404 Not Found) となっていたようだ。
(下記はその証拠の npm install ログ表示。)
> node-sass@4.7.2 install C:\Work\app1\node_modules\node-sass
> node scripts/install.js
Downloading binary from https://github.com/sass/node-sass/releases/download/v4.7.2/win32-x64-64_binding.node
Cannot download "https://github.com/sass/node-sass/releases/download/v4.7.2/win32-x64-64_binding.node":
HTTP error 404 Not Found
なお、node-sass の Node パッケージスクリプトは次なる手段としてローカル環境でのバイナリビルドを開始する。
だがしかし自分の環境ではビルド用のツールの整備がなっていないので、これもコケてしまい結局エラーで終了、ということだったらしい。
さて、改めて node-sass の GitHub リポジトリの Release ページを見てみると、ちゃんとビルド済みバイナリが用意済みの、対応されている Node.js バージョンが明記されている。
これを頼りに今回は node-sass v.4.9.0 以降を使用するよう packages.json を更新した。
その結果、自分のローカル環境にビルドツール類をインストールすることなく、無事 npm install が成功するようになった。
まとめということで、Windows OS 上で、必ずしも windows-build-tools Node パッケージの利用をはじめビルドツールの整備をしなくても、node-sass のバージョンを適切にアップデートしていけば、すんなり npm install を通すことはできる、という話。
なお、Node.js のいつのバージョンからかよく覚えていないが、最近の Node.js Windows 版のインストーラーでは、ビルドツールも一緒にインストールするかどうかの選択肢が出てきてるっぽい。
この選択肢を有効にして Node.js をインストールしてある環境ならば、このような node-sass を npm install できない問題は起きないのだろうか。
検証したことはないが、気になるところである。
]]>
.NET HTTP REPL を使ってみた
http://devadjust.exblog.jp/27565785/
2019-04-23T00:19:00+09:00
2019-04-23T09:39:41+09:00
2019-04-23T00:19:08+09:00
developer-adjust
Web系一般
既存のコマンドラインツールで類似なものを挙げるとすれば、curl や wget が近いだろうか。
すなわち、コマンドプロンプト (ターミナル) 上から指定した HTTP 要求を送信し、返信されてきた応答を表示する類いの CUI (コマンドライン ユーザー インターフェース) プログラムである。
しかしながら、このツールの名前に "REPL" と付いているように、基本的に対話形式で使用する形態となっているのが .NET HTP REPL の特徴である。
REPL (Read-Eval-Print Loop) とは、入力・評価・出力のループのこと。主にインタプリタ言語において、ユーザーとインタプリタが対話的にコードを実行する。
出典: フリー百科事典『ウィキペディア(Wikipedia)』
ちなみに、.NET HTTP REPL のソースコードは GitHub 上の AspLabs というリポジトリでホスティングされており、Apache 2.0 ライセンスで公開されている。
このリポジトリの README には、"Repo for ASP.NET experiments that are not ready for a production release." と書かれており、この先どうなるか、心配すべきかわくわくすべきか迷うようなプロダクトが詰め込まれているように感じた。
ということで、実は .NET HTTP REPL は本記事を投稿の時点では公式リリース版ではなくプレビュー扱いである。
適宜注意されたし。
.NET HTTP REPL のインストールさて、.NET HTTP REPL を使用するには、まずは .NET Core SDK のインストールが必要である。
.NET Core SDK がインストールされ、コマンドプロンプト (ターミナル) 上から dotnet コマンドが実行可能になっていれば、.NET HTTP REPL のインストールは以下のコマンドを実行すればよい。
> dotnet tool install --global dotnet-httprepl --version 0.1.0-preview.19119.4このようにすることで、dotent Global Tools として nuget.org 経由で .NET HTTP REPL 本体がダウンロード、インストールされる。
これで .NET HTTP REPL が使えるようになる。
.NET HTTP REPL の起動・使い方.NET HTTP REPL は dotnet コマンドのサブコマンドという形態で実装されている。
そのため、コマンドプロンプト (ターミナル) から以下のコマンドを実行することで起動され、.NET HTTP REPL の対話モードに入る。
> dotnet httprepl最初に書いたとおり、.NET HTTP REPL は対話形式で使用する、HTTP 要求を送信するツールである。
まずは送信先 HTTP サーバーのホスト、プロトコル、ポートを、"base" という HTTP 要求送信の基点として設定しておく必要がある。
これは、.NET HTTP REPL の対話モードのコンソールにて、以下のように "set base" コマンドで設定する。
~ set base http://localhost:50893/base 設定が完了すると、.NET HTTP REPL のプロンプトも "(Disconnected)~" から "http://localhost:50893/~" というように設定された base に変る。
こうして base 設定が済んだら、あとは、HTTP 動詞と同じ名前で用意されているコマンドを入力すればよい。
例えば、URL = api/SampleData/WeatherForecasts に HTTP GET 要求を送信する場合は下記のような感じで、割と直感的に使える。
~ GET api/SampleData/WeatherForecastsHTTP サーバーからの返信結果は、例えば JSON 形式で返された場合はよしなに色づけ・書式化されて表示される。
当然 POST をはじめ、PUT, DELETE などの HTTP 動詞を送信するも可能で、また、POST や PUT で JSON コンテンツを送信したい場合は、-c スイッチに続けて送信したいコンテンツを記述すればよい。(以下は「ちょまど問題サーバー」に回答を送信して正答数を得る例)
POST https://chomado-problem-server.azurewebsites.net/answer -c [1,2,4,4,1,2,3,4,1,2]対話モードを抜けて .NET HTTP REPL を終了するには exit コマンドを実行すればよい。
~ exit.NET HTTP REPL には cd コマンドがあるさて .NET HTTP REPL のおもしろいところとして、対話形式のツールであることから状態が持てるので、"cd コマンド" が用意されていることが挙げられる。
つまり、HTTP 要求送信先 URL をスラッシュ区切りとしてディレクトリに見立てて、"現在ディレクトリ" を移動できる、という仕組みである。
例えば以下のように api/SampleData "ディレクトリ" まで、cd コマンドで "降りて" いれば、
~ cd api/SampleDataGET コマンドで api/SampleData/WeatherForecasts に要求送信するには、前半の api/SampleData を省略した、WeatherForecasts だけを送信先 URL として記述すればよい。
/api/SampleData~ GET WeatherForecasts"現在ディレクトリ" は .NET HTTP REPL のプロンプトに表示されているほか、引数なしの cd コマンドを実行することでも表示される。
特定のリソースを指す REST 風な Web API に対して読み書き操作を色々試したいときなど、cd コマンドで URL の共通部分まで、あらかじめ "降りて" おけば、入力の手間が省けて良さそうな感じである。
※2019/04/22現在、cd コマンドが例外を発生させることがある (当方の環境。.NET Core 3.0 Preview 4)。但しとりあえず .NET HTTP REPL は動作継続し、cd コマンドによる "現在ディレクトリ" の変更は行なわれているようである。
Swagger にも対応!?.NET HTTP REPL の対話モードで help コマンドを入力すると、どんなコマンドがあるのかヘルプ表示される。
これを見ると、まだ試せていないのだが、"set swagger" というコマンドもあるようで、どうやら Swagger 形式の Web API 定義 JSON を読み込んで便利に使えるようだ。
また、run コマンドによって "スクリプト" を実行できる、ともある。
これもまだ試せていないので、具体的にどのように使えるものなのか現時点ではわかっていないのだが、興味を引かれるところである。
感想.NET HTP REPL は、REPL と名前についているとおり対話形式で使用するものであり、起動時のコマンドライン引数で何か実行するということはどうやらできない模様だ(もしかしたら、スクリプト機能などで可能なのかもしれないが、まだよくわかっていない)。
そういう用途では、やはり curl などのほうが使えるということになりそうだ。
また、最近の Windows 10 OS には curl コマンドも標準で入るようになったので、自分がわかっている範囲だけでも、macOS/Linux各種ディストリ/Windows などなど、幅広い OS で使えるのも curl の良いところだろう。
他方、.NET HTTP REPL であるが、サーバーから返信された JSON コンテンツを、インデント付きで書式化・色づけして表示してくれるので、サーバー応答の JSON が読みやすいと感じた。
しかしながら、curl コマンドと jq コマンドの組み合わせ技のように、サーバー応答の JSON をフィルタしたり射影したりなどの凝った加工は、.NET HTTP REPL にはできないように見受けられる。
個人的には .NET HTTP REPL が直感的に使えるところが気に入った。
"GET /api/..." のように、自分が Web ブラウザになった気分で HTTP 要求を発行できるのが心地よい。
また、cd コマンドによる "現在ディレクトリ" の概念もおもしろいところだと思う。
さらに今後、Swagger 対応の機能の使い方がわかれば、もっと強力に使えるツールとなるかもしれないと期待もしている。
各種自動化やシェルスクリプトの記述などにおいては、相変わらず curl の出番となると思う。
そのいっぽうで、Web アプリ開発中に臨時的に Web API 呼び出しを試してみたいときなど、対話形式での使用のほうがよいケースでは、積極的に .NET HTTP REPL を使いそうな気がしている。
PowerShell の Invoke-WebRequest や Invoke-RestMethod は... うーん、自分が書く自動化系のスクリプトは PowerShell が俄然多いのでそういう PowerShell スクリプトファイル内ではよく登場するものの、今回の .NET HTTP REPL 的な、臨時的・対話的用途には、自分の手にはなじまなかったです、はい。
]]>
Angular + SignalR な Web アプリで、HubConnection.invoke() 時の変更検知発動を抑止する
http://devadjust.exblog.jp/27418443/
2019-01-28T22:06:00+09:00
2019-01-28T22:08:14+09:00
2019-01-28T22:06:20+09:00
developer-adjust
Web系一般
SignalR は、Node.js で言うところの Socket.IO に相当するようなライブラリ。
ASP.NET Core Web アプリに、ブラウザを含めたクライアントへのプッシュ通信など、リアルタイム Web 機能を実現できるようにするものだ。
クライアント(ブラウザ)側では、サーバー側からのプッシュ通信を待ち受け(ハンドラ関数を登録しておく)するほか、SignalR の通信チャンネルを通じてサーバー側を呼び出すこともできる。
具体的には、クライアント(ブラウザ)側の JavaScript コードにて、SignalR の通信チャンネルを表す HubConnection オブジェクトの invoke メソッドを呼び出すことで実装する。
HubConnection.invoke() で発生する変更検知を debounce したい!さて、SignalR を使用して、クライアント/サーバー間通信を頻繁に行なう、クライアント側は Angular を採用した SPA 実装を行なっていると、ちょっと処理速度性能的に問題となることがある。
あまりに頻繁に SignalR の HubConnection.invoke() を呼び出す必要があると、Angular の変更検知処理が都度都度走ってしまい、アプリが重くなってしまうのだ。
そのため、Angular の NgZone の機能を使い、HubConnection.invoke() 呼び出し時の Angular の変更検知発生を明示的に抑止し、デバウンス処理などを自前で組み込むことにしていた。
前提知識: Angular の変更検知を走らせないようにする方法下記記事が参考になる。
記事タイトルは「Angularの外部でイベントが発生した時の変更検知の方法」だが、「逆に Angular に変更検知をさせたくない場合」の節に変更検知を抑止する方法が記載されている。
具体的には、NgZone オブジェクトを DI 機構で受け取っておき、その上で、変更検知を走らせたくない処理を、NgZone.runOutsideAngular() メソッドのコールバック内で実行すればよい。
HubConnection.invoke() での変更検知を抑止さて、SignalR の HubConnection.invoke() 呼び出し時、Angular の変更検知を走らせないようにするには、結論としては以下の3点。
SignalR の接続開始処理 (HubConnection.start()) を、NgZone.runOutsideAngular() 内で実行する
HubConnection.invoke() を NgZone.runOutsideAngular() 内で実行する
HubConnection.invoke() の呼び出し結果を、async/await ではなく .then() による Promise メソッドチェーンで処理する
なぜ async/await してはいけないか?上記 3 の「async/await 使うな」についてはちょっと説明が要るかもしれない。
上記 3 を記載したのは、NgZone.runOutsideAngular() を使って変更検知抑止を実施したとしても、その処理を内包している async 関数の Promise が解決されるときに、結局変更検知が走るからである。
もっとも、その async 関数自体をさらに NgZone.runOutsideAngular() 内で実行すればこれまた変更検知は抑止できるので、絶対に async/await を使ってはいけない、と単純に言えるものではない。
また、もしかしたら、「とある async 関数 foo 内での HubConnection.invoke() 呼び出し時は、変更検知は抑止したいが、それら処理がひととおり終わって async 関数 foo から抜け出したら、変更検知が走って欲しい」というケースのほうが多いかも知れない。
そのような要件・シナリオの場合は、むしろ async/await で実装したほうが、コードも読みやすいし、期待した動作結果・挙動となることと思う。
start() と invoke()、片方だけ runOutsideAngular しても効果無しちなみに、HubConnection.start() と、HubConnection.invoke()、いずれか片方だけ NgZone.runOutsideAngular() 内で実行しても変更検知は走ってしまう。
特に HubConnection.invoke() だけ NgZone.runOutsideAngular() 内で実行した場合、変更検知は抑止されず、しかし変更検知のタイミングが invoke() の結果処理後にずれこみ、レンダリング内容に反映されないなど面倒なことになるようだ。
いっぽう、 HubConnection.start() を NgZone.runOutsideAngular() 内で実行しておいた場合は、
HubConnection.invoke() をそのまま実行した場合は従来どおり変更検知が実施され、
HubConnection.invoke() を NgZone.runOutsideAngular() 内で実行した場合は変更検知が抑止される、
という具合に呼び分けられるようになる。
つまり、SignalR 通信に関して Angular 変更検知を抑止するつもりなら、HubConnection.start() を NgZone.runOutsideAngular() 内で実行しておくのが肝要なようだ。
注意: HubConnection.on() で登録したハンドラ関数実行時の変更検知も抑止されてしまうしかしながら、前述の変更検知抑止処置 ― HubConnection.start() をNgZone.runOutsideAngular() 内で実行 ― をしてしまうと、サーバー側からのプッシュ通信を受け取るハンドラ関数呼び出し (HubConnection.on() で登録したコールバック関数の実行時) についても、一切、変更検知が走らなくなってしまう。
より正確に言えば、HubConnection.on() だけではなく、HubConnection.onclose() コールバックでも変更検知は発生しなかった。
これらコールバックでも変更検知を発動させるには、面倒だが、ハンドラ関数内で個別に NgZone.run() 呼び出しを使って明示的に変更検知を走らせるほかないようだ。
もっとも、「SignalR 通信のサーバー側プッシュのハンドラでは、 Angular 変更検知を抑止したい」ということであれば、HubConnection.start() をNgZone.runOutsideAngular() 内で実行すればよい、ということでもある。
コード例実際のコード例を GitHub にあげておく。
とくにクライアント側コード例の抜粋を以下に貼っておく。
]]>
module import できない古典的な jQuery 拡張を、import できるようにする
http://devadjust.exblog.jp/26236373/
2017-12-15T20:02:00+09:00
2017-12-15T20:05:27+09:00
2017-12-15T20:01:59+09:00
developer-adjust
Web系一般
// こうすることで jQuery に拡張機能 xxxx を括り付ける
$.fn.xxxx = ...
})(jQuery);これを webpack + TypeScript の開発環境にてどうやって使うか、その方法について投稿した。
前回の作戦は、「jQuery をバンドル対象外」とするよう webpack を構成することで、jQuery オブジェクトをグローバル名前空間に晒すこととし、その結果、旧来どおり script タグで jQeury を読み込むようになったので、レガシーな jQuery 拡張も使えるようになるよ、というものであった。
いっぽうで、このレガシーな jQuery 拡張の実装のほうに手を入れることで、その jQuery 拡張を module import 可能に対応させることで先の命題を解決することもできる。
今回はその方法について説明する。
レガシー jQuery 拡張をモジュールバンドルすると実行時エラーが起きる仕組みさて、レガシー jQuery 拡張ライブラリが、実行時に「ReferenceError: jQuery is not defined (jQuery というシンボルが見つからない)」エラーを引き起こすのは、レガシー jQuery 拡張ライブラリが、グローバル名前空間に "jQuery" というシンボルが存在することを前提としているためだ (下記参照)。(function ($) {
$.fn.xxxx = ...
})(jQuery); // <- ここいっぽうで、webpack を用いて CommonJS 方式でモジュールバンドルする際は、jQuery 本体もモジュール機能で名前空間が隔離されている。
結果、グローバル名前空間に "jQuery" という名前で jQuery オブジェクトをさらすことはない。
この食い違いが原因で、レガシー jQuery 拡張は実行時に "jQuery というシンボルが見つからない" エラーを引き起こすわけである。
CommonJS の仕組みにおいては、jQuery オブジェクトに用事があるモジュールは、そのモジュール内で "require('jquery')" を実行してその戻り値としての jQuery オブジェクトを使用する必要がある。解決方法ここまでわかれば、レガシー jQuery 拡張を CommonJS モジュール対応させるのは簡単だ。
グローバル名前空間にある jQuery シンボルを参照しようとするのをやめ、代わりに require('jquery') で jQuery モジュールを正規にインポートしたものを参照すればよい。(function ($) {
$.fn.xxxx = ...
})(require('jquery')); // グローバル変数 jQuery ではなく、
// require() の結果を引数に渡す!これで webpack + TypeScript な構成でも、この改造後の jQeuery 拡張を import しモジュールバンドルできるようになる。
下位互換も維持するにはなお、このような改造を施した jQuery 拡張ライブラリは、今度は、旧来どおり素で script タグでブラウザに読み込ませるような構成だと動作しないことになる。
script タグで個々の JavaScript ライブラリを読み込ませるような構成だと、ブラウザの JavaScript エンジン上では require などという CommonJS モジュール機構が存在しないので、ここで実行時エラーになるわけだ。
最初の話と逆転しているわけである。
ただし、jQuery 本体それ自体が、CommonJS 方式のモジュール機構にも、素の script タグで読み込ませるケースにも、両方に対応できていることからわかるように、今回話題にしてるようなレガシー jQuery 拡張も、両方のケースに対応させることは難しくない。
アルゴリズムとしては、下記のとおりだ。
まず、(jQuery拡張) 自身が読み込まれた環境が CommonJS モジュール機構内かどうかを判定し、モジュール機構内であれば、require('jquery') を実行して戻り値で返ってきたオブジェクト ( = jQuery オブジェクト) を、そうでなければグローバル名前空間の jQuery というシンボルを使って、jQuery 拡張機能を括り付ける
実際にコードに書き表したのが下記である。// 下記赤文字の jQuery 拡張機能を括り付ける関数を引数 factory として受け取り...
(function (factory) {
// 先述のアルゴリズムで jQuery オブジェクトの参照方法を if 文で仕分けて、
// 引数 factory で受け取った jQuery 拡張機能括り付け関数を実行する。
// ...という処理を無名関数として実装、即時実行する。
// CommonJS の場合...
if (typeof exports == 'object' && typeof module == 'object') {
// require('..') で取得したオブジェクト (= jQuery オブジェクト) を渡す
factory(require('jquery'));
}
// ブラウザの場合...
else {
// グローバル変数 jQeury を渡す
factory(jQuery);
}
})(function ($) { // jQuery 拡張を括り付ける本体
// ちなみに、この関数内は改造の必要はない。
$.fn.xxxx = ...
})CommonJS を知らないレガシーな jQuery 拡張機能であっても、このような改造を施せば、旧来通りの使い方もできる下位互換を維持しつつ、webpack による CommonJS モジュールバンドリングにも使用できるようになる。
補足ちなみにこの実装方法は、カラーピッカーとして (多分) 有名な jQuery 拡張である Spectrum のソースコードを参照してて知った。
Spectrum のソースコードでは、JavaScript のモジュール機構として、CommonJS のみならず、AMD 方式にも対応しており、ブラウザう + CommonJS + AMD の 3方式すべてに互換となっている。詳しくは Spectrum のソースコードを参照されたし。
]]>
型定義もなく module import もできないレガシー jQuery 拡張を、webpack + module import な TypeScript で使う
http://devadjust.exblog.jp/26233640/
2017-12-14T23:10:00+09:00
2017-12-14T23:10:35+09:00
2017-12-14T23:10:35+09:00
developer-adjust
Web系一般
自前のコードは TypeScript で記述し、これをモジュールバンドラーである webpack を使用して、JavScript へのトランスパイルからモジュールバンドルまで実施する構成である。
プロジェクト立ち上げの様子はこうだ。
まず npm (yarn でも可) でプロジェクトを初期化。
> npm init -f -y続けて必要な Node パッケージをインストールする。> npm install --save-dev webpack typescript ts-loader jquery @types/jquerypackage.json はこんな感じ。{
"name": "sample-webapp",
"version": "1.0.0",
"scripts": {
"build": "webpack",
"watch": "webpack -w"
},
"devDependencies": {
"@types/jquery": "^3.2.17",
"jquery": "^3.2.1",
"ts-loader": "^3.2.0",
"typescript": "^2.6.2",
"webpack": "^3.10.0"
}
}次に、TypeScript コンパイラを使用して、既定の tsconfig.json も作成しておく。>.\node_modules\.bin\tsc --initwebpack.cconfig.js も下記のとおり記述する。// webpack.config.js
module.exports = {
entry: ['./src/app.ts'],
output: {
filename: './wwwroot/js/bundle.js'
},
resolve: { extensions: ['.js', '.ts'] },
module: {
loaders: [
{
test: /\.ts$/, use: [
{ loader: 'ts-loader' }
]
}
]
},
devtool: 'source-map'
};そして、アプリケーションコードとなる ./src/app.ts を作成。// ./src/app.ts
import * as $ from 'jqeury';
$(() => {
let randomNum = Math.round(100 * Math.random());
$('#p1').text(randomNum);
});以上で、「npm run build」を実行すれば、app.ts が JavScript に変換されつつ、jQuery の JavaScript コードをモジュールバンドルした、"./wwwroot/js/bundle.js" ができあがる。
この bundle.js を HTML コンテンツ中から script タグで参照・読み込むよう記述するわけだ。
(例えば下記 ./wwwroot/index.html のように。)<html>
<body>
<p id="p1"></p>
<p id="p2"></p>
<script src="/js/bundle.js"></script>
</body>
</html>命題さてここで、下記のような実装がされた、クラシカルな jQuery 拡張ライブラリ ( 架空の例として ./wwwroot/js/jquery.fizzbuzz.js ) を使う必要に迫られたとする。// ./wwwroot/js/jquery.fizzbuzz.js
(function($) {
$.fn.fizbuzz = function(n) {
...
}
})(jQuery);先に述べた構成・開発環境において、どうやってこの古典的な jQuery 拡張を使用するのか、というのが本稿の命題だ。
まずは externals 化このクラシック実装な jQuery 拡張は、上記のとおり、JavaScript のモジュール機構など考慮していない実装なので、app.ts で下記のように// ./src/app.ts
import * as $ from 'jqeury';
import '../wwwroot/js/jquery.fizz.buzz';
$(() => {
let randomNum = Math.round(100 * Math.random());
$('#p1').text(randomNum);
});と import 文を書き足して参照しても、これは機能しない。
webpack によるビルドは成功するものの、いざブラウザで index.html を開くと「ReferenceError: jQuery is not defined (jQuery というシンボルが見つからない)」という実行時エラーになる。
そこで、まずは webpack の構成を変更し、jQuery が webpack によるモジュールバンドルに取り込まれないようにする。
そして代わりに、jQuery およびクラシック jQuery 拡張ライブラリ ( この例では jquery.fizzbuzz.js ) を、HTML 中から各々 script タグで参照するようにするのだ。
まず webpack.config.js に以下のように externals プロパティを記述する。// webpack.config.js
module.exports = {
entry: ['./src/app.ts'],
output: {
filename: './wwwroot/js/bundle.js'
},
resolve: { extensions: ['.js', '.ts'] },
module: {
loaders: [
{
test: /\.ts$/, use: [
{ loader: 'ts-loader' }
]
}
]
},
devtool: 'source-map',
externals: {
"jquery": '$'
}
};そして index.html には以下のとおりに script タグを追加する。<html>
<body>
<p id="p1"></p>
<p id="p2"></p>
<!-- 別途、下記 src 属性で指定されたパスに
当該 JavaScript ファイルを配置しておくこと -->
<script src="/js/jquery.min.js"></script>
<script src="/js/jquery.fizzbuzz.js"></script>
<script src="/js/bundle.js"></script>
</body>
</html>この構成は、今回のようなケースに限らず、JavaScript ライブラリを CDN 経由で参照するのでバンドルに含めたくない、といった場合にも採用する構成だ。
これで実行時に jquery.fizzbuzz.js が初期化失敗することはなくなった。
TypeScript における型定義の解決アプリケーションコードをTypeScript ではなく素の JavaScript で記述していたのであれば、以上、webpack の externals 指定を構成するだけで対応完了となる。
しかし今回の命題では、アプリケーションコードは TypeScript で記述することとしている。
そのため、何らかの手段で拡張された jQuery メソッドの情報を TypeScript に教えてやらないといけない。
今回の架空の例である jquery.fizzbuzz.js は、$("セレクタ").fizzbuzz(数値) という、fizzbuzz メソッドが jQuery に追加になるという代物である。
だが、ここまでの段階ではまだ、app.ts に下記のとおり記述しても、TypeScript のコンパイルが「TS2339: Property 'fizzbuzz' does not exist on type 'JQuery<HTMLElement>'.」というエラーで失敗する。// ./src/app.ts
import * as $ from 'jqeury';
$(() => {
let randomNum = Math.round(100 * Math.random());
$('#p1').text(randomNum);
$('#p2').fizzbuzz(randomNum); // ← この行がコンパイルエラー
});TypeScript コンパイラには、jquery.fizzbuzz.js によって fizzbuzz メソッドが増えるということがわからない・知らされていないためだ。
型定義情報を記述「jQuery に fizzbuzz メソッドが増えている」ことを TypeScript コンパイラに知らしめるためには、型定義を自前で書けばよい。
ということで、./src/jquery.fizzbuzz.d.ts を作成。
単純に jQuery の型定義で提供されている JQuery インターフェース定義に対し、追加の定義を書き足せばよい。// ./src/jquery.fizzbuzz.d.ts
interface JQuery {
fizzbuzz(n:number) : jQuery;
}以上で、TypeScript 型定義もなく、JavaScript モジュール機構にも対応していない古典的な jQuery 拡張ライブラリを、webpack + module import な TypeScript + jQuery の構成でも使用できるようになる。
参考までに、プロジェクト全体を GitHub にサンプルコードとして載せておいた。
こんなクラシックな jQuery 拡張を相手にしないで済むのであれば、それがいちばんよいのだろう。
とはいえ、なかなか現実にはそうもいかないこともあろうかと思う。
そんな場面に遭遇した時には、本稿の手順で対応することを検討するのも選択肢のひとつである。
]]>
Application Insights で Web アプリのクライアント JavaScript 例外を記録したら、URL は記録されていない?
http://devadjust.exblog.jp/25486730/
2017-08-30T23:15:00+09:00
2017-08-30T23:19:51+09:00
2017-08-30T21:49:12+09:00
developer-adjust
Web系一般
Application Insights の適用できるプラットフォームは多岐に渡る。
今回は Web ページの監視、クライアント側スクリプトにおける Application Insights の試用を開始した。
実装ガイドに従って手続きを進め、下記のような JavaScript コードをページに埋め込んだ。
<script type="text/javascript">
var appInsights = window.appInsights || function(config){
... 省略 ...
}({
instrumentationKey: "a79dec8d...c3a427"
});
window.appInsights = appInsights;
appInsights.trackPageView();
</script>
これで、JavaScript コード上で捕捉されなかった例外が Application Insights サービスにインターネット経由で送信され、Visual Studio 上や、Web ブラウザからの Azure ポータルサイト上で確認することができるようになる。
試用を始めて早速に、捕捉されないクライアントスクリプト例外が発生。
それではと、Azure ポータルで発生した例外のログを確認してみた。
なるほどなるほど、例外のメッセージはもちろん、スタックトレースも当然のごとく閲覧可能。
...と、見ているうちに気が付いた。
例外発生時のページ URL はどこ?
ページ URL の記載がない?このクライアントスクリプト例外が発生したときの、ページ URL がわからない。
「http://host/#/foo/edit/123」みたいに URL がわかれば、「foo の ID = 123 を編集中に、この例外が発生したんだな!」と、現象の再現に大変役立つ。
しかしその URL がわからないとなると、かなりきつい。
"url" という項目はあるにはある。
しかしこれは例外発生個所の JavaScript ファイルの URL だった。
そういう URL は無用である (スタックトレースみればわかるし)。
何か見落としているのかもしれないが、現時点でもなお、にクライアント側スクリプト例外の発生時のページ URL の記録は見つけられていない。
いちおう、ブラウザの開発者ツールで、Application Insights への送信内容も傍受して内容の JSON を見てみたが、そこにもページ URL らしきものは、自分には見つけられなかった。
自前で配線致し方ないので、捕捉されなかったクライアントスクリプト例外の記録を、Application Insights の既定のスクリプトに任せるのをやめ (下記)、
<script type="text/javascript">
var appInsights = window.appInsights || function(config){
... 省略 ...
}({
instrumentationKey:"a79dec8d...c3a427",
disableExceptionTracking: true // このオプション指定を追加
});
代わりに自前で、捕捉されなかった例外の Application Insights への送信を実装し、そこで window.location.href も盛り込むようにした。
window.onerror = function (msg, file, line, column, err) {
appInsights.trackException(err, null, {
url: file,
pageUrl: window.location.href
});
};
</script>
これで Azure ポータル上での例外ログを閲覧したときに、例外発生時の URL もわかるようになった。
でも本当にこれでいいの?...という感じで、とりあえずは自己解決したのだけれど、今になって落ち着いて考えるに、皆さん、クライアントスクリプト例外の記録に、例外発生時のページの URL が併記されてないまま Application Insights を利用されているのだろうか??
それとも自分が何か壮大な勘違い・間違いを犯しているのではないだろうか?
もし何か情報があればコメントや Twitter 上のツイートなどで教えていただけると大変ありがたい。
]]>
「お前の IP アドレスを echo する」Web アプリを作る (手順はあるけど実装コードそのものは6行!)
http://devadjust.exblog.jp/25075830/
2017-07-27T12:47:00+09:00
2017-07-27T12:53:10+09:00
2017-07-27T12:47:58+09:00
developer-adjust
Web系一般
Azure CLI 2.0 ("az" コマンド) を使って Azure SQL Database のファイヤーウォールを開閉する
http://devadjust.exblog.jp/23804079/
この記事の中で、「補足: 自 PC からのアクセスでのグローバル IP を知るには?」ということで、http://inet-ip.info の利用を紹介していた。
最近、inet-ip.info 落ちてる?しかしながら、ここ数日、http://inet-ip.info がたまーに落ちてることがあった。
https://t.co/2p2r8mmMZi が落ちてしまってるようだけど、heroku で動いてたのね。 pic.twitter.com/HD6Xp2W0jS— jsakamoto (@jsakamoto) July 26, 2017ほどなく復活はするのだが、自分は運が悪いのか、ここ最近は立て続けに利用不能の憂き目に逢っている。
どうやら Heroku 上に立てられているようなのだが、利用量の制約にでも触れてしまっているのだろうか。
それはさておき、まぁ、HTTP 要求に対し、要求もとのリモートアドレスを応答に返すような Web アプリの作成なんぞ大した手間ではない。
ということで、他人様がたててくれた Web アプリだけに頼るのではなく、ささっと自分専用の「お前の IP アドレスを echo する」Web アプリを作って立てることにした。
とはいえ、たかだか「お前の IP アドレスを echo する」のためだけなので、
Web アプリすら大仰
である。
ということで、Microsoft Azure の機能のひとつ、「Azure Function Apps」として構築することにした。
「お前の IP アドレスを echo する」Azure Function App今回はプログラミング言語としては JavaScript を採用。
ということで、言語 = JavaScript で、HTTP Trigger な Function を1つ作り、下記内容で実装すれば完了である。
// index.js
module.exports = function (context, req) {
var remoteAddress = req.headers['x-forwarded-for'];
context.res = {body: remoteAddress.split(':').shift()};
context.done();
};
あとは、この Function を起動する URL に、HTTP GET 要求でも送れば、応答コンテンツに、アクセス元 = 自身の IP アドレスが echo されて返ってくる次第。
以下、Azure Portal での操作手順の説明となるが、もう少し詳しく手順を説明してみよう。
手順をもうすこし詳細に説明してみるまずは Web ブラウザで Azure Portal サイトを開いてサインイン。
続けて、「New (追加)」から「Compute」>「Function App」とカテゴリをたどる。
最低限、アプリ名を指定し(アプリ名は URL の一部というか先頭部分となる)、適宜、ホスティングされる場所を選んで「Create」をクリック。
これでしばらくすると新規 Function App の枠ができあがる。
続けてこの枠の中に個別の Function を作っいく(今回は IP アドレスを echo する Function x 1つだけだが)。
Azure Portal 上で今作った Function App のブレードを開き、下図のように Functions の追加ボタンをクリック。するとどのような Function を追加するのかの画面になる。
この画面では、よく使われるシナリオ用の Function をさくっと追加できるようになっているのだが、あいにくと HTTP トリガーが選択肢にない。
そこで、「create your own custom function」をクリック。 これで、細かく選べる画面になる。
「Language (プログラミング言語)」は「JavaScript」を選択、シナリオは「HttpTrigger」を選択して、最後に Function の名前(これが URL パスの末尾になる)を決めて入力、「Create」をクリックする。 これで、HTTP 要求によって起動される Function がひとつできあがる。
実装内容を下図のように、今回案件の「お前の IP アドレスを echo する」内容に書き換えて「Save」をクリックして保存。 これで完成である。
あとは、この Function を起動する URL が画面右上のリンクから知らされるので、この URL を手に入れておく。 ちなみに、ここまで、既定の設定で作ってきたので、この Function は、いわゆる "パスワード" というか、API キーによる認証必須となっている。
つまり、API キーを添えて HTTP 要求を送らないと、Azure Function App のインフラストラクチャによって「401 Not Authorized」エラーになる。
なお、上図で見えている当該 Function を起動する URL には、クエリ文字列として、その API キーが込みで掲載されている。なので、この URL で HTTP 要求を送れば... ご覧のとおり、ちゃんとアクセス元のグローバル IP アドレスが応答として返ってくる。上図は PowerShell で Invoke-RestMethod による実行例だが、もちろん curl でも同じだ。 ちなみに API キーの指定方法だが、上記のように URL 中のクエリ文字列で "code=..." に指定するほか、HTTP 要求ヘッダに「x-functions-key」を追加してそこに API キーを指定してもよい。
下記など参照されたし。Azure Functions の API Key を扱ってみる
http://tech.guitarrapc.com/entry/2016/04/22/042331
まとめ以上、Azure Function App を使えば、「お前の IP アドレスを echo する」程度の Web アプリは、さくっと立てられる。
なお、Azure Function の料金については下記を参照されたし。
ベースの無料枠もあるので、但しストレージの使用料金はかかるのだがそれもたかがしれていると思うのでかなり廉価に運営できると思われる。
]]>
せっかく Gulp に乗り換えたので、Grunt でやってた単体テスト実行も乗り換えちゃう
http://devadjust.exblog.jp/23471488/
2016-10-27T23:03:55+09:00
2016-10-27T23:03:41+09:00
2016-09-06T22:28:48+09:00
developer-adjust
Web系一般
AngularJS を使った Web アプリクライアント側実装について、以前の投稿にて、Gulp の採用によって TypeScript ソースに対応するソースマップ付きでの Bundle & Minify を達成した。
さらに前回の投稿にて AngularJS における依存性注釈の自動生成を達成した。
このように、今更感あるものの、Gulp 採用に伴う開発環境改善を加速中の今日この頃なのだが、さて、単体テストの話。
過去の投稿にて紹介したとおり、クライアント側スクリプトの単体テストを実現するにあたって、テストランナー環境として Grunt とそのプラグイン ( grunt-contrib-jasmine ) を使用していた。
しかしここへ来て、前述のとおり、Bundle & Minify, 依存性注釈の自動生成のために Gulp とそのプラグインを使用するようになった。
となると、すでに Gulp でいろいろタスク実装してるのに、単体テストのためだけに Grunt を併用するのもなんだかなぁ、と思うようになってきた。
お前は Gulp 派なのか Grunt 派なのか、どっちかはっきりしろよ、みたいな。
ということで、その程度の理由・動機ではあったのだけれども、とにかく、Gulp に乗り換えに伴う Grunt 脱却を図ることにした。
Gulp で JavaScript 単体テスト実行を実現するプラグインは?乗り換えということで、grunt-contrib-jasmine で実現していたのと同じ環境を Gulp 上で再構築するには、どんな Gulp プラグインを採用するのがいいのかな、とネットでいろいろ検索。
そうやって情報収集しているうちに、Gulp のプラグインじゃなくて、
"karma" を使えばいいじゃん
ということがわかってきた。
"karma" とは、コマンドラインツールの名前。
実は "karma" の存在も以前から知っていた。
AngularJS を学習しはじめたときに、某書籍で「AngularJS 界隈では karma 使ってる事例あるよ」的なことが書かれてて、そこで "karma" なるものの存在を知った。
しかしその書籍からは "karma" とはなんなのかその本質がよく掴めなかった。
結果、自分の中では 「karma とは E2E テストの自動化に使う何からしい」という誤った認識で留まってしまっていた。
ところがこうして改めて、JavaScript 単体テストの実行環境を求めているうちに、ようやく見えてきたのだが、
karma って要するにテストランナーだったのですね。
※ほんっと、今更ですみません。
つまり、GUI 操作を伴う E2E テストに限らず、単体テストの実行にも普通に karma 使えるのだということがわかった。
ということで、karma を使った JavaScript 単体テストの実行環境を整えてみた。
karmaによるテスト実行環境の構築npm パッケージのインストールまずは必要となる下記 npm パッケージをプロジェクトにインストール。 karma の本体(コア機能)と、ヘッドレスブラウザ (GUI を持たない、このようなテストや各種自動化を目的とした特殊な Web ブラウザ) である PhantomJS を karma から起動できるようにするためのランチャー、および、単体テストフレームワークの Jasmine を krama 上で扱うためのアドインそして、単体テストフレームワーク Jasmine それ自体 > npm install --save-dev karma
> npm install --save-dev karma-phantomjs-launcher
> npm install --save-dev karma-jasmine
> npm-install --save-dev jasmine-coreなお、ヘッドレスブラウザ PhantomJS も、karma-phantomjs-launcher の依存関係解決により、プロジェクトにインストールされる。
ここでちょっと嬉しかったのは、Grunt のプラグイン (grunt-contrib-jasmine) では PhantomJS v.1.x でしか動作しなかったのだが、上記 karma-phantomjs-launcher では PhantomJS v.2.0 がインストールされること。
karma の構成ファイル「karma.conf.js」の作成さて必要な npm パッケージのインストールが済んだら、karma に対し、どのようにテストを実行したらよいのかを示す設定ファイル「karma.conf.js」を新規作成して記述する。
下記のとおり karma コマンドを実行することで、コンソール上の対話形式で「karma.conf.js」を初期作成することができる。> .\node_modules\.bin\karma init上記を実行すると、karma コマンドから、どのような構成にするかコンソール上での対話が始まる。
下図の要領でこれに応えて、karma.conf.js の初回生成を完遂させる。
こうして karma.conf.js ができたら、続けて下記要領で内容を編集して完成させる。
karma.conf.js は設定項目が多く見た目で怯みがちだが、特記すべき点に絞ると下記の要領となる。module.exports = function(config) {
config.set({
// テストフレームワークに Jasmine を使う指定。
frameworks: ['jasmine'],
// テスト実行環境に読み込む JavaScript ファイルの指定。
// 下記は自分のプロジェクトのフォルダ構成に基づく例。
files: [
// vendor
'Scripts/jquery-2.2.4.js',
'Scripts/angular.js',
'Scripts/angular-mocks.js',
// src
'bundle.js',
// spec
'_spec/**/*Spec.js'
],
// Visual Studio の出力ウィンドウで表示するため、
// テスト進捗状況の表示は progress ではなく dots を指定。
reporters: ['dots'],
// Visual Studio の出力ウィンドウで表示するため、
// コンソール出力はモノクロを指定。
colors: false,
// テスト実行に使うブラウザは PhantomJS を指定。
browsers: ['PhantomJS'],
// テストを実行するごとに処理終了させる指定。
singleRun: true
})
}karma によるテストの実行以上でお膳立てが整ったら、いよいよ実行である。
コマンドプロンプトにて> .\node_modules\.bin\karma start karma.conf.jsというように、start コマンドと構成ファイル名 (= karma.conf.js) を引数に指定して karma コマンドを実行する。
するとkarma コマンドが動き出して karma.conf.js を読み込み、これに記述されているとおり、PhantomJS を起動、単体テストを構成するコンテンツを読み込ませるPhangtmJS は読み込まされたコンテンツ、すなわち jasmine ベースのテストコードを実行
( 但し PhantomJS はヘッドレスブラウザということもあり、とくに GUI/ウィンドウが現れたりすることはない )、最後に karma が結果をすくい上げてレポート表示 された。
バッチリである。
これで grunt-contrib-jasmine による単体テスト実行環境から karma ベースの環境に完全移行できた。
もう grunt は使っていないので、「npm uninstall grunt --save-dev」して大丈夫である。
なお、単体テストフレームワークは jasmine のまま変更しなかったので、既存の単体テストコードは一切変更せずに済んでいる。
さらにソースマップ読み込みも!さらに、これは予想・期待していたものではなかったのだが、karma であれば、テスト失敗時のレポートなどで、
コンパイル元の TypeScript ソース行で該当箇所をレポートする
ように構成できることがわかった。
まず必要となるのは "karma-sourcemap-loader" という npm パッケージ。
これをプロジェクト構成に追加。> npm install --save-dev karma-sourcemap-loader次に、karma.conf.js 中にて、"プリプロセッサ" の指定に karma-sourcemap-loader を追加する。...
preprocessors: {
'**/*.js': ['sourcemap']
},
...こうすることで、TypeScript のコンパイル時に同時生成したソースマップファイルが読み込まれ、テスト実行後のレポート表示において、TypeScript ソースコード上でのファイル名と行番号とで表示されるようになった。
テスト失敗が報告された JavaScript の該当行から、コンパイル元の TypeScript ソースコード上での該当行を人力作業で特定するのは、不可能ではないとはいえまずまずの労力を要する。
そんな非生産的な、機械で解決できるはずの課題が、実際のところ今日まで grunt-contrib-jasmine 環境では解決できていなかったのだから、こうしてこの課題が解決され、大変晴れ晴れしい気分になった。
改めて、karma をはじめ各種ソフトウェアプロダクトに貢献されている方々に感謝である。
おまけこうして構築した karma による単体テスト実行環境だが、この単体テストを実行するにあたり、自分の場合、コマンドプロンプトから直接の karma コマンド実行をしていない。
代わりに、npm を一段階挟んでいる。
つまり、packages.json 内に、karma 実行コマンドを記載しておいてある (下記)。...
"scripts":{
"test":"karma --silent --no-color start karma.conf.js"
}
...こうしておくことで、「npm test」を実行することで単体テストが実行されるようになっている。
直接 karma を実行すればよいものを、わざわざ npm を経由しているのなぜか?
それは、自分の主たる開発環境が Visual Studio であることに由来している。
Visual Studio 上の設定で、Ctrl+R, Ctrl+J のキーコンビネーションが打鍵されると、「npm test」を実行して、結果を Visual Studio 内のウィンドウ (出力ウィンドウ) に表示するようにしてあるのだ。
そのため、npm 側で実際のテスト実行コマンドを隠蔽しておくと、従来の gurunt-contrib-jasmine で構築されたプロジェクトでも、今回新規構築した karma によるプロジェクトでも、どちらでも共通の「npm test」でテストを実行できる。
すなわち、どのプロジェクトを Visual Studio で開いても、共通のキーボード操作 Ctrl+R, Ctrl+J でテストが実行される、という仕掛けである。
なお補足として、上記 package.json に記述した karma コマンドに記載のオプションスイッチだが、「--silent」はテスト結果の表示上、よぶんなバナー表示などを抑止するもので、「--no-color」は、Visual Studio の出力ウィンドウにテスト結果表示をリダイレクトしてる関係上、コンソールの色指定コードは表示を乱してしまうことから、カラー表示コードの出力を抑止するスイッチである。
Gist にサンプルコードありますこれまでの内容と、実際に動作している構成フィル類とを、下記 Gist に置いてある。
Jasmine による TypeScript/JavaScript 単体テストを実行する環境を作る - karma 編
https://gist.github.com/jsakamoto/e4a957821216c38cdb7e9e20ba17e569
こちらも適宜参照されるとよいかもしれない。
以上、ご参考までに。
]]>
せっかく Gulp に乗り換えたので、AngularJS v.1.x の minify 対策自動化もやっちゃう
http://devadjust.exblog.jp/23462118/
2016-09-02T21:42:52+09:00
2016-09-02T21:42:25+09:00
2016-09-02T19:48:23+09:00
developer-adjust
Web系一般
すでに Angular v.2 もリリース候補を迎えている最中、ちょっと時代遅れになりつつあるが AngularJS v.1.x の話である。
AngularJS での DIAngularJS では独自の依存性注入機構 (Dependency Injection, "DI")が用いられている。
例えば次のようなコンストラクタ関数があるとする。function FooController($http){
...
}コンストラクタ関数の第一引数の引数名が「$http」になっている点に注目。
これを AngulaJS にコントローラーとして登録したとする。angular
.module('myApp',[])
.constroller('fooController', FooController);すると、この FooController が new されるときに、ちゃんと第一引数に AngularJS の HTTP サービスオブジェクトが引き渡されるのだ。
これは、引数の "名前" をもとに注入すべきオブジェクトを引き当てて渡してくれる、という AngularJS の DI の仕組みである。
Minify したらダメ!では、どうやって関数の引数の名前を持ってきてるのか?
それは JavaScript ではユーザー定義の関数は toString で文字列化するとその関数のソース文字列が返ってくる、という仕組みを使っている。
しかしこのように、関数定義のソースコード文字列から引数の名前を判別して、これに基づいて依存性注入しようとすると、お察しのとおり JavaScript コードの縮小化 (Minify) がされると破たんしてしまう。
さきの FooController コンストラクタ関数も、minify すると下記のように引数名が "$http" から "t" に短縮化されてしまうだろう。function FooController(t) {
...
}短縮化されたあとの FooController 関数を toString() してそのソース定義の文字列を入手したとしても、わかる引数名は "t" だ。
こうなっては FooController の第1引数に $http サービスオブジェクトを渡さなくてはいけないという情報は失われてしまっている。
Minify するなら依存性注釈かかなきゃダメ!AngularJS v.1.x ではこの事態を回避するため、コントローラー等として登録する際に、引数の名前を文字列で列記することができるようになっている。angular
.module('myApp',[])
.constroller('fooController', ['$http', FooController]);この、コンストラクタ引数を別途文字列配列で指定しているのを、"依存性注釈" というらしい。
当然のことながら、文字列リテラルは minify しても変更されない。
AngularJS は、FooController の定義を文字列で入手することなく、controller メソッドに渡された配列内、すなわち依存性注釈から、引数名を安全に割り出すことができる、という寸法だ。
ということで、AngularJS でコントローラーやサービス、フィルタなどなどを登録する際は、minify されることを想定して、前述のとおり依存性注釈を記述することが推奨される。
人がやる作業じゃない!しかしこれって、けっこう面倒で不毛な作業に感じてしまう。
実際のところ、アプリケーション本体側の TypeScript コードをいじりまわっている最中は、コンストラクタ関数の引数はどんどん変更することが多い。
「あ、やっぱり $q サービスいるじゃん」といった具合に。
そして、コンストラクタ引数の変更のたびに、依存性注釈も編集するのは、二度手間で本当に不毛である。
しかも、依存性注釈ので引数の順番を書き誤るバグとかやらかしてしまうと、本当にフラストレーションが溜まる。
コンストラクタ関数の Minify 前の引数名を、手作業で文字列配列で記述するなんて、
こんな退屈な作業こそ機械化できないのか!?
救世主 "ng-annotate"...ということで、実はそのような不毛で退屈な作業を自動化してくれるツールがある。
ng-annotate というツールがそれだ。
ng-annotate を使えば、angular
.module('myApp',[])
.constroller('fooController', FooController);というコードを入力すると、angular
.module('myApp',[])
.constroller('fooController', ['$http', FooController]);に相当する(※)、依存性注釈付きのコードが自動生成できるのだ。
こうして自動生成したあとの依存性注釈付きコードを minify すればバッチリである。
※ ... 実際に自動生成されるコードはこれとはちょっと違ってて、"$inject" プロパティというものを使ったりしているのだが、詳細は割愛。
自分もようやく ng-annotateを導入...という大変便利な ng-annotate、実はその存在は 2014年頃から耳にしていた。
しかしついつい「いずれ、そのうち...」などと採用を先送りしてしまっていた。
さてところで、前回の投稿で紹介したとおり、最近になって、タスク自動化ツール「Gulp」を使って TypeScript ソースコードに対応するソースマップを同時生成する Bundle & Minify 構成を構築した。
そして前述の ng-annotate は、Gulp のプラグインも提供されている。
ということで、せっかく Gulp に宗旨替えしたのだからこれを機会にとばかり、依存性注釈の自動生成を私の Gulp による Bundle & Minify 機構に取り込んでみた。
まずは ng-annotate の gulp プラグインである gulp-ng-annotate をプロジェクトにインストールしておく。> npm install -save-dev gulp-ng-annotate次に、Bundle & Minify 機構を実装している gulpfile.js を編集。
JavaScript ファイル群を Bundle (= concat, 連結) したあと、Minify (= uglify) するまえのタイミングに、依存性注釈の追加 (= ng-annotate) 処理を挿入する (下記太字の箇所)。var gulp = require('gulp');
...
var ngAnnotate = require('gulp-ng-annotate');
...
gulp.task('min:js', function () {
// bundleconfig.json の記述から1要素ごとに、
// Bunlde & Minify 化のタスクに射影する
var tasks = bundleconfig.map(function (bundle) {
var task = gulp.src(bundle.inputFiles, { base: "." })
.pipe(sourcemaps.init()) // ソースマップ生成機構を仕掛ける
.pipe(concat(bundle.outputFileName)) // concat で連結(Bundle)
.pipe(ngAnnotate()) // 依存性注釈を追加
.pipe(uglify()) // uglify で難読&縮小化(Minify)
.pipe(gulp.dest("."))
.pipe(sourcemaps.write("./")) // ソースマップを出力
.pipe(gulp.dest("."));
return task;
});
...※コード全体は こちらの gist に置いてある。
これで、コンストラクタ引数の記述に加えて、AngularJS v.1.x への登録記述でも引数を書かなくてはいけない不毛な作業から解放された。
ほんとに今更感満載だが、とにかく、やってよかった。
]]>
元の TypeScript ソースまでたどれるソースマップを伴う、JavaScript ファイルの Bundle & Minify 化の方法
http://devadjust.exblog.jp/23442678/
2016-08-29T20:33:00+09:00
2016-09-02T21:39:30+09:00
2016-08-29T20:00:55+09:00
developer-adjust
Web系一般
現在自分が抱えているプロダクトでは、TypeScript から JavaScript への変換は、1つの .ts ファイルにつき 1つの .js ファイルを生成する構成 ― プロジェクト内の .ts ファイル群からひとつの .js ファイルを生成するのではなく ― としている。
そしてこれら .ts から変換した .js ファイル群は、所定のグループごとに、ファイルサイズ削減を目的とした縮小化(Minify) を適用し、かつ、ひとつの .js ファイルへの単一化 (Bundle) を実施している。
Bundle & Minify を実施する具体的な手順としては、ASP.NET MVC が備える Bundle & Minify 機能 (Web アプリ実行時に動的に Bundle & Minify される)、ないしは、Visual Studio 拡張の「Bundle & Minifier」を使ってきた。
これまで実現できていなかったことだがしかし、この構成でひとつだけあきらめていたことがあった。
それは Bundle & Minify 化された JavaScript コードからの、
いちばん大元の TypeScript ファイルへたどることのできる
ソースマップファイル (.map) の生成
である。
TypeScript コンパイラは当然のことながら、.map ファイルを生成することができる。
実際、自分のプロダクトでも .map ファイルを生成するよう tsconfig.json を構成してある。
しかし前述の Bundle & Minify 化の手法だと、その Bundle & Minify 処理過程で新たに生成される .map ファイルは、Bundle & Minify 前の JavaScript コードにたどれるまでとなる。
TypeScript ソースコードにまで到達しないのだ。
なくてもどうにかならなくもないけど...もっとも TypeScript コンパイラが生成する JavaScript コードは、元の TypeScript コードと割と一対一で対応してたりする。
なので、Bundle & Minify 前の JavaScript コードにまで到達できれば、人力的探索で TypeScript ソースコードまでたどれなくもない。
また、IDE やブラウザが備えるデバッガ上で TypeScript ソースコード上でブレークポイントを張ったりしたいときも、実行時と開発時とで、HTML 上で読み込む JavaScript ファイルを違える工夫で回避できる。
つまりは、開発時は Bundle & Minify 前の JavaScript ファイル ― これには TypeScript コンパイラが生成した .map ファイルがついている ― を HTML で読み込むように、実行時とは構成を変更できるようにするのだ。
しかしそうはいっても、クラッシュ通知が来てるときに、そのスタックトレースが元の TypeScript コードにおけるソース行を直接指していないとなると、気分が焦っているのに余計な手間がかかって何とも煩わしい。
そこで重い腰を上げて、Bundle & Minify 化するときに TypeScript ソースコードまでたどれる .map ファイルを生成する方法はないものか、調べてみることにした。
Gulp 使えば楽勝だった!あれこれ検索したり試行錯誤を繰り返してる中で、Node.js 上で動作するタスク自動化ツールのひとつ、Gulp を使えばあっさり実現できることがわかった。
以下はその手順である。
必要なツールやパッケージのインストールNode.js のパッケージマネージャ、npm を使って、まずはプロジェクトフォルダ上で Gulp をインストール。
>npm init
...(package.json 新規作成のためにあれこれ問いに応答する)...
>npm install --save-dev gulpさらに、Gulp を使ってファイルを結合、つまり Bundle を実施するために gulp-concat、難読化を伴う JavaScript コードの縮小化のために gulp-uglify、いくつかの並行タスクを束ねるために merge-stream、そして今回の命題のキモとなる、ソースマップの生成のために gulp-sourcemapsを続けてインストールする。>npm install --save-dev gulp-concat
>npm install --save-dev gulp-uglify
>npm install --save-dev merge-stream
>npm install --save-dev gulp-sourcemapsBundle & Minify の設定ファイルそして話が前後するが、前述の Visual Studio 拡張にて Bundle & Minify 化するときに、どの JavaScript ファイルを Bundle & Minify するのかの設定ファイルとして、bundleconfig.json という JSON 形式のファイルで記述してある (内容は下記のような感じ)。[
{
"outputFileName": "lib.min.js",
"inputFiles":[
"lib/**/*.js
]
},
{
"outputFileName": "bundle.min.js",
"inputFiles":[
"src/**/*.js
]
}
]gulpfile.js の作成 - まずは最低限 Bundle & Minify するところまで前述の bundleconfig.json を読み込んで Bunlde & Minify するよう、gulpfile.js を組んでみる。
ソースマップ生成は抜きにしてまずは Bundle & Minify ができるところまでだと、こんな感じだ。
var gulp = require('gulp');
var concat = require('gulp-concat');
var uglify = require('gulp-uglify');
var merge = require('merge-stream');
// bundleconfig.json の内容を読み込む
var bundleconfig = require('./bundleconfig.json');
gulp.task('min:js', function () {
// bundleconfig.json の記述から1要素ごとに、
// Bunlde & Minify 化のタスクに射影する
var tasks = bundleconfig.map(function (bundle) {
var task = gulp.src(bundle.inputFiles, { base: "." })
.pipe(concat(bundle.outputFileName)) // concat で連結(Bundle)
.pipe(uglify()) // uglify で難読&縮小化(Minify)
.pipe(gulp.dest("."));
return task;
});
// それらタスクを束ねて返して実行
return merge(tasks);
});ここまでの状態で、プロジェクトフォルダをカレントディレクトリとしたコマンドプロンプト上で「.\node_modules\.bin\gulp min:js」などと実行すると、bundleconfig.json の指定に従って Bundle & Minify 化が実行されるはずだ。
ただしここまでの記述ではまだ、ソースマップは生成されない。
ソースマップ生成ではいよいよ、ソースマップの生成を組み込む。
単に Bundle & Minify 前のソースコードまでたどれるだけであれば、ネットで検索したときによく見かける下記の記述 (太字の箇所) を書き足せばよい。
var gulp = require('gulp');
...
var sourcemaps = require('gulp-sourcemaps');
...
gulp.task('min:js', function () {
// bundleconfig.json の記述から1要素ごとに、
// Bunlde & Minify 化のタスクに射影する
var tasks = bundleconfig.map(function (bundle) {
var task = gulp.src(bundle.inputFiles, { base: "." })
.pipe(sourcemaps.init()) // ソースマップ生成機構を仕掛ける
.pipe(concat(bundle.outputFileName)) // concat で連結(Bundle)
.pipe(uglify()) // uglify で難読&縮小化(Minify)
.pipe(gulp.dest("."))
.pipe(sourcemaps.write("./")) // ソースマップを出力
.pipe(gulp.dest("."));
return task;
});
...だが今回の最終目標は、Bundle & Minify 前のコードまでではなく、さらにその前の TypeScript コードにまでたどれるソースマップの生成だ。
そのために、gulp-sourcemaps に対して、以下の指示を書き足す。
...
var task = gulp.src(bundle.inputFiles, { base: "." })
.pipe(sourcemaps.init({loadMaps: true})) // ソースマップ生成機構を仕掛ける
...上記のとおり、gulp-sourcemaps の init メソッド呼び出しの際に、
「{loadMaps: true}」のオプション指定を書き足す
のだ。
こうすることにより、gulp-sourcemaps は、TypeScript コンパイラが生成したソースマップをまずは読み込んで、この情報に基づいて最終的に出力される Bundle & Minify 化された JavaScript コードに対するソースマップを生成するようだ。
つまりここまでの手順の要領で Gulp による Bundle & Minify タスクを実装すれば、Bundle & Minify 化された JavaScript コードを開始地点として、いちばんはじめの TypeScript ソースコードにまで到達できるソースマップも同時生成できるのだ。
最後に、watch と Visual Studio 統合も最後に、.ts ファイルを保存して JavaScript コードが生成・更新されたら、自動で Bundle & Minify も実行されるよう watch タスク (下記) も加えておくとよいだろう。gulp.task('watch', function () {
bundleconfig.forEach(function (bundle) {
gulp.watch(bundle.inputFiles, ["min:js"]);
});
});Visual Studio 上の作業と統合するのであれば、Visual Studio のタスクエクスプローラーから設定すればよい。
Visual Studio のタスクエクスプローラーを開けば、ここまでで gulpfile.js に記述した「min:js」タスクと「watch」タスクが自動検出されてツリーに見えているはずだ。
あとは、
-「min:js」タスクをビルド後に実行するタスクに、
-「watch」タスクをプロジェクト読み込み後に実行するタスクに、
割り当てるなどしておくとよいだろう。
以上で、クラッシュ時スタックトーレスやデバッガ上での実行において、Bundle & Minify 化した JavaScript コードが読み込まれている場合でも、大元の TypeScript ソースコードに基づいて取り扱うことができる、快適な環境を構築できた。
※上記に加えて、後日の投稿である "AngularJS の依存性注釈の自動生成" も組み込んだコード全体は こちらの gist に置いてある。
]]>
Visual Studio で開発中の Web アプリをスマートフォンから動作確認するのに ngrok を使う
http://devadjust.exblog.jp/23066969/
2016-04-14T20:05:30+09:00
2016-04-14T20:05:29+09:00
2016-04-14T19:55:50+09:00
developer-adjust
Web系一般
このような開発環境では、通常、Visual Studio から自動起動された IIS Express によってこの Web アプリがホストされる。
この開発環境内で PC ブラウザを起動し、開発中の Web アプリの URL ― 一般に "http://localhost:56789/" のような ― を開いて試験実行/動作確認をすることになる。
さらに昨今は、スマートフォン実機のブラウザからもこの開発中の Web アプリにアクセスして試験実行し、スマートフォン上でも正しく動作するか確認することが多い。
しかしスマホから見えるようにする手間が...さて、上記のようなローカル開発環境で開発中の Web アプリにスマートフォン実機からアクセスするには、まずは同一 LAN 内に立てた WiFi アクセスポイントにスマートフォンを接続する。
その上で、"http://(開発機のホスト名):56789/" といった URL をスマートフォンのブラウザで開くこととなる。
しかしセキュリティ上の観点からであろう、Visual Studio による Web アプリ開発においては、その Web アプリは localhost (127.0.0.1 及び ::1) にしかバインドされていない。
つまり既定では開発機の "外" からは接続できないように構成されている。
これを開発機の外からでも接続できるようにするには、微妙に面倒くさい。
概ね以下の手順を踏む必要がある。IIS Express に対する XML 形式の設定ファイルである applicationHost.config をエディタで編集し、開発機ホスト名でのバインドを追加開発機ホスト名での URL アクセスをユーザーに許可 (Visual Studio を管理者モードで起動するならば不要)Windows OS のファイヤーウォールで、開発中の Web アプリのポートでの受信を許可具体的な手順は下記などが詳しい。
しばやん雑記 - IIS Express で仮想サイトに複数のホスト名を割り当てる
http://blog.shibayan.jp/entry/20130306/1362572283
上記、各手順はそうたいした手間ではないけれども、どうにも微妙な面倒を感じる。
おまけに、URL アクセス制御やファイヤーウォールの許可を、ちゃんとこまめに都度元に戻すのであれば、この微妙な手間が倍増である。
そこで ngrokということで最近よく使うようになったのが "ngrok" というサービス。
ngrok
https://ngrok.com/
"ngrok" とは、ある種の "リバースプロキシ" サービスとでも言えばよいだろうか。
Web アプリ開発時などローカルループバックでリッスンしているような Web サーバーに対し、ngrok が提供する専用のプログラムを同じマシン上で起動すると、このローカル環境の Web サーバーを、ngrok 提供のインターネット上の URL に "中継" してくれるのだ。
ネットで検索すると、ngrok についての記事がわんさか見つかるので、詳しくはそれら記事も参照されたし。
https://www.google.co.jp/search?q=ngrok
使ってみるngrok は、ずばりサービス名と同じ名前の ngrok コマンドを提供している。
これをダウンロード・インストールし、PATH が通っている状態で、コマンドプロンプトから以下のように ngrok コマンドを実行する。> ngrok http 54090 -host-header=localhost第 1 引数と第 2 引数がローカルで実行中の Web アプリのプロトコルとポート番号。
第 3 引数は host 要求ヘッダ値の書き換え指定 (詳細後述) である。
実行すると下図のような画面が表示される。 上図のようにコンソール上に表示されているとおり、これで http://e1b0d393.ngrok.io/ にインターネット上からアクセスすると、そのアクセスを ngrok が中継して、現在開発中の Web アプリにアクセスできるのだ。
※この URL "http://~.ngrok.io" のサブドメイン部分は、ngrok コマンドを実行するたびに毎回、ランダムに変わる。
これでお手軽にスマホ (に限らないけど) から動作確認ができるというものだ。
終了させるには、このコンソール内で Ctrl + C を押せばよい。
ngrok コマンドは終了し、先の URL にアクセスしても無効となる。
-host-header オプションについて補足さて、先の ngrok コマンドの説明で第3引数に「-host-header=localhost」を追加した。
これについて説明を。
ブラウザから「http://e1b0d393.ngrok.io/」にアクセスした際、その HTTP 要求に含まれる Host 要求ヘッダには「e1b0d393.ngrok.io」と書かれていることになる。
ところが、Visual Studio で Web アプリ開発時の IIS Express の構成では、Host ヘッダが 「localhost」 でないと「おっと、これは "自分" 宛ての要求じゃないな」と判断してしまう。
その結果、HTTP 400 Bad Request「Invalid Hostname」を返してしまうのだ。
そこで ngrok 側にて、中継の際に Host 要求ヘッダ値を指定の値 (このケースでは "localhost" ) に書き換えるのが「-host-header=」オプションである。
まとめさて、毎回ランダムな URL になるとはいえ、LAN 内にとどまらずインターネット上に開発中の Web アプリを公開するということは、それなりのリスクはあるかもしれない。
とはいえ、必要になったらコンソールから ngrok コマンドを叩くだけで、スマホから容易にアクセスできるというのも魅力的だ。
ngrok は無償から利用可能。
毎回ランダムな URL ではなく固定したり、1度に複数の "中継" をしたり、といった使い方をしたくなったら有償プランに移行すればよい。
以上、インターネット上に公開されているという点はしっかり考慮しつつ、リスクとメリットを勘案して適宜利用するとよいと思う。
]]>
AngularJS での日付の書式化 - 自分の JavaScript コードから date フィルタを呼び出す
http://devadjust.exblog.jp/22943375/
2016-03-03T19:24:00+09:00
2016-03-03T22:22:34+09:00
2016-03-03T18:51:47+09:00
developer-adjust
Web系一般
AngulaJS での date フィルタによる日付の書式化JavaScript ネイティブには、日時型の値 (Date オブジェクト) を「2016/03/03」などのように書式化する仕組みが用意されていない。
幸い、AngulaJS を使用している場合は、AngularJS が備える "filter" の仕組みと、及び、AngularJS の Core に標準実装されている "date" フィルターのお蔭で、JavaScript の Date オブジェクトを、HTML のビューにバインドするときに書式化することは容易に可能だ。
一例としては、ビュー (HTML) 側に以下のように書くことができる。<span>{{ctrl.someDay | date:'yyyy/MM/dd'}}</span>...というのは、入門書にも載っている話。
date フィルタを自分の JavaScript コードから使うには?さてところで、諸事情により、Date オブジェクトを「2016/03/03」などのように書式化する処理を、AngulaJS のモデルバインダに任せるのではなく、自分が記述しているコントローラ等の JavaScript コード内で実行したい場合がある。
このような場合は、以下のようにコントローラ等のコンストラクタを記述することで、AngularJS に date フィルターを注入してもらうことができる(以下は生の JaavaScript コードではなく TypeScript によるコード)。class FooController {
constructor(dateFilter: any) {
}
}何のことはない、フィルタ名 + "Filter" という仮引数名でコンストラクタを作っておくと、その名前のフィルタを AngularJS が注入してくれるようにできているのだ。
こうして date フィルタを手に入れてしまえば、下記のようなコードで Date オブジェクトを自由に書式化できる。var now = new Date();
var text = dateFilter(now, 'yyyy/MM/dd');以上。
なお、上記コードでは折角の TypeScript なのに dateFilter の型を any としてサボっているが、ちゃんと型付すると下記となろうか。class FooController {
constructor(dateFilter: (value: Date, format?: string) => string) {
}
}あいにくと自分が入手した AngularJS の TypeScript 型定義ファイルでは date フィルタの型宣言を見つけられなかったので、インラインで型を記述した次第。
以上の内容を動作確認すべく、jsFiddle にサンプルを作成しておいた。
AngulaJS 1.5.0 での動作が確認できている。
jsFiddle - How to call date filter in controller's methods
https://jsfiddle.net/jsakamoto/phr09cxw/4/
補足1本投稿や上記 jsFiddle 上のサンプルでは、説明を簡略化するために "yyyy/MM/dd" といった固定の書式指定子を記載している。
しかし、多国語対応を考えると、本当はこのような固定の書式指定はたぶんよろしくないと思うので、一言ご注意まで。
補足2@KatsuYuzu からアドバイス頂戴したのでもうひとつ。@jsakamoto インジェクションのみにふぁい対策しましょ。コンテキストから外れてる気がしますが、サンプル的に罠かと。— しみみん (@KatsuYuzu) March 3, 2016
上記ツイートのとおり、先述の jsFiddle に掲載のコードは、そのまま愚直に minify すると機能しなくなる。
詳細は「angularjs minify」でググるとたくさん良記事が見つかるので割愛するが、AngularJS では minify にあたりちょっと配慮が必要なのだ。
下記記事を参考に ng-annotate などの "pre minify" を噛ますか、
Angularでminify対策を手動でやるのはもうやめよう
http://www.axlight.com/mt/sundayhacking/2015/01/angularminify.html
あるいは手作業で、コントローラの登録処理の記述を以下のように、「コントローラのコンストラクタに渡す引数名の文字列と、コントローラクラスを並べた配列」にしておくこと。angular
.module('app', [])
.controller('mainController', ['dateFilter', MainController]);
]]>
MaterializeCSS 付属の日付ピッカーで、日付がタップされたら日付ピッカーを閉じるようにする
http://devadjust.exblog.jp/22923821/
2016-02-26T22:47:33+09:00
2016-02-26T22:47:33+09:00
2016-02-26T22:35:21+09:00
developer-adjust
Web系一般
MaterializeCSS
http://materializecss.com/
Bootstrap のデザインに飽きた最近は、この CSS ライブラリを採用することが多くなってきた。
さてこの MaterializeCSS には、出典は pickadate.js の、MaterializeCSS 同梱用にカスタマイズされた日付ピッカーが同梱されている。 この日付ピッカー、見た目もなかなかよさげなので使うことが多い。
ところがつい先日、その扱いでちょっと躓いたところがあったのでメモしておく。
日付をタップしたらピッカーは閉じてほしいもっぱらスマホを対象とした Web アプリで、「日付をタップし、選択したら、即座に日付ピッカーは閉じてほしい」という要件が発生。
たしかにタッチデバイスではそのほうが軽快な操作感が得られる。 まぁ、たぶん簡単だろうと思って対応を試みたのだが、これがアテが外れて、四苦八苦する羽目に。
詳しい顛末は別の機会に譲るとして、紆余曲折の末、編み出した秘伝のソースコードはこんな感じ (下記コードは TypeScript)。
$(".datepicker").pickadate({
onSet: (ctx: any) => {
if (typeof(ctx) == 'number') {
setTimeout(() => {
$('.picker--opened .picker__close').click();
}, 0);
}
}
});解説MaterializeCSS にて日付ピッカーを有効にするには、上記のとおり、対象 DOM 要素をセレクタ指定した jQuery オブジェクトについて pickadate() メソッドを呼び出すことで適用する。
このとき、pickadate() メソッドの引数に各種オプション指定ができるのだが、ここで、onSet プロパティにコールバック関数を仕込んでおく。
こうすることで、日付ピッカー上で何かしら選択動作が行われると、この onSet プロパティに指定したコールバック関数が呼び出されるようになるのだ。
なので、方向性としては onSet コールバック関数が呼び出されたら、何か日付が選択されたということで、そのタイミングで日付ピッカーを閉じてやればよさそうだ。
ただしこの onSet コールバック関数は、日付の部分がタップ(選択)されたときのみならず、年や月が変更されたときにも発生する。 そこで onSet コールバック関数が呼び出されるときに引数に渡されるコンテキストオブジェクトも調べるようにする。
onSet コールバック関数の引数に渡されるコンテキストオブジェクトに、型が数値である select というプロパティが付いていたら、これが (年や月の変更ではなく) 日の部分がタップ、選択された意味となる。
こうした判断を経て、現在可視状態にある日付ピッカー要素の中の、close ボタンに対してクリック操作を JavaScript から起動することで、日付ピッカーが閉じられるようになる。
なお、UI 上のフィードバックを行うチャンスのために、setTimeout(...) で、いちど JavaScript 実行スレッドを抜けておくようにした。
以上の措置で、日付部分をタップ(選択)すれば日付ピッカーが自動で閉じるように作ることができた。]]>
https://www.excite.co.jp/
https://www.exblog.jp/
https://ssl2.excite.co.jp/