佐々木屋

技術的なことから趣味まで色々書きます

Node.jsによるモダンJavaScriptのIE11向けトランスパイル(ビルド編)

前回からの続き・・・。
sasaki816.hatenablog.com

前々回はこちら。
sasaki816.hatenablog.com


さて、実際このトランスパイルは手動か自動でnpmのスクリプトコマンドでビルドすればよいことになります。

npm run build

ただ、これだと色々厳しい(何言ってんだ)ので、VisualStudioからトランスパイルをできるように設定します。


●共通設定
GulpをPowershell経由で実行します。普通に書けば以下のようになります。
※package.jsonの"scripts"にbuild:ie11がある前提

<Gulp を直接呼び出す>

powershell -ExecutionPolicy Bypass -NoProfile -Command "npx gulp build:ie11"

<scripts.build:ie11 を実行して中で Gulpを 呼び出す>

powershell -ExecutionPolicy Bypass -NoProfile -Command "npm run build:ie11"

どちらでも問題ありませんが、前者はGulpを直接呼び出すことに対して、後者の方はpackage.jsonのscript経由で実行しますので、管理という面でいえば後者の方が汎用性は若干高いですね。

また、ワークフォルダがローカルに無く、プロジェクトフォルダに一任されている場合は、以下のように実行スクリプトを別ファイルに作成してプロジェクトフォルダへ置き、それを呼ぶ形にすると汎用性は格段にあがります(psファイルをコピーするだけで別のプロジェクトにも適用できる)。
build-ie11.ps1

# build-ie11.ps1
param (
    [string]$ProjectPath = "."
)

Push-Location $ProjectPath

npx gulp build:ie11

Pop-Location

但し、これはローカルの場合で、UNC(ネットワーク共有パス)の場合は安定しません(大抵エラーになる)。
その場合、一時的にドライブレターを設定してマウントする方法がおすすめです。以下は例えばx:ドライブにする方法です。

# build-ie11.ps1
$uncPath = $PSScriptRoot
$driveLetter = "x:"

net use $driveLetter /delete /y | Out-Null
net use $driveLetter $uncPath /persistent:no | Out-Null
Push-Location "$driveLetter\"

& "$driveLetter\node_modules\.bin\gulp.cmd" build:ie11

Pop-Location

net use $driveLetter /delete /y | Out-Null

呼び出し方は以下の通りとなります。

powershell -ExecutionPolicy Bypass -NoProfile -File "$(ProjectDir)build-ie11.ps1" -ProjectPath "$(ProjectDir)"


●ビルド時自動実行
ビルド時都度実行できるようにします。
色々やり方はありますが、ここでは一番簡単な.csprojにPowerShell実行を追加する方法を説明します。

プロジェクトフォルダにある.csprojの直前に以下を追加します。

<Target Name="RunGulpBeforeBuild" BeforeTargets="BeforeBuild">
  <Exec Command="powershell -ExecutionPolicy Bypass -NoProfile -File &quot;$(ProjectDir)start-build-ie11.ps1&quot;" />
</Target>

これでビルド時に自動で実行されるようになります。

ただし、プロジェクトファイルがUNCの場合はこの方法だと色々問題が発生します。
・ビルドが遅い(開発に影響)
・そもそもUNCがcmd.exeがカレントに対応できない
この場合は、次の外部ツールによる手動実行に頼った方が幸せになります。


●外部ツールによる手動実行
主に手動でビルドしたい、又はUNC対応したい場合はこちらが便利です。

以下のように設定します。

タイトル ※任意の文字列
コマンド powershell.exe
引数 -ExecutionPolicy Bypass -NoProfile -File "$(ProjectDir)start-build-ie11.ps1"
初期ディレクト (空白)又は $(ProjectDir)

Node.jsによるモダンJavaScriptのIE11向けトランスパイル(構成編)

前回からの続き・・・。
sasaki816.hatenablog.com

①フォルダの構成
ここで設定したgulpfile.jsファイル

const { src, dest } = require('gulp');
const babel = require('gulp-babel');
const newer = require('gulp-newer');
const cached = require('gulp-cached');
const plumber = require('gulp-plumber');
const path = require('path');

const SRC_BASE = 'Scripts';
const SRC_PATTERN = `${SRC_BASE}/**/*.js`;
const DEST_DIR = 'ScriptsDist';

function buildIE11() {
  return src(SRC_PATTERN, { base: SRC_BASE })
    .pipe(plumber())
    .pipe(cached('ie11'))  // 最初にキャッシュ確認
    .pipe(newer(DEST_DIR)) // 変更があったファイルのみ
    .pipe(babel({
      presets: [['@babel/preset-env', {
        targets: { ie: '11' },
        useBuiltIns: 'usage',
        corejs: 3
      }]]
    }))
    .pipe(dest(DEST_DIR));
}

exports['build:ie11'] = buildIE11;

この場合、プロジェクトのフォルダ構成は以下の通りです。


②useBuiltIns設定のあれこれ
●useBuiltIns: 'usage'の場合
この場合はJSファイルごとにIE11向けポリフィルが埋め込まれますので、特に何も追加で用意、設定することはありません。
各JSファイルを個別に読み込む構成や、コードサイズを小さくしたい人むけですね。


●useBuiltIns: 'entry'の場合
この場合は以下のポリフィルが必要です。

import 'core-js/stable';
import 'regenerator-runtime/runtime';

これが無いと、IE11でモダンJavaScript機能が使えるようになりません。
原則各JSファイルの先頭に書く必要があります。

もしくは、例えばpolyfills.jsのように専用のファイルを作成し、そのトランスパイル後のJSファイルを先頭に読み込ませれることも可能です。マスターページなどを設定しているのであれば、マスターページにpolyfills.jsを読み込ませて、コンテンツページにそれ以外のJSファイルを読み込ませれば実現できます。


③コンテンツページへの設置と読み込み
コンテンツページに以下のように各JSファイルを読ませます。例えば、abc.jsの場合は以下です。

   <script type="module" src="<%= this.ResolveClientUrl("/Scripts/abc.js") %>"></script>
   <script nomodule src="<%= this.ResolveClientUrl("/ScriptsDist/abc.js") %>"></script>

※srcの相対パス問題は以下をどうぞ
sasaki816.hatenablog.com

type="module"」で設定すると、Chromiumブラウザでは正常に処理されますが、IE11では認識できませんので無視されます。
逆に「nomodule」で設定した場合は、Chromiumブラウザでは無視され、IE11でのみ処理される形となります。

これで両方読み込んでもそれぞれのブラウザ環境で片方だけ読まれますので、名前の衝突は発生しません。


次回はこちら。
sasaki816.hatenablog.com

Node.jsによるモダンJavascriptのIE11向けトランスパイル(導入編)

そろそろES6+に手を出そうと思ったけど、結局現場ではまだまだIEモードが手放せていない現実があります。
技術者としても新しい手順、作法に触れたい気持ちもあり、ChromiumとIE11と両方両立できるようトランスパイルしようと思います。

①Node.jsのインストール
公式サイトからインストーラを取得できます:
https://nodejs.org/ja

LTS(推奨版) を選んでください。
インストール後に以下コマンドでバージョンが表示されればOKです。

node -v
npm -v


②npm必要パッケージのインストール
●npmの初期化

npm init -y

●必要なパッケージのインストール
以下はあくまで例です。gulpを実行するjsに応じて適当に増減してください。

npm install --save-dev gulp gulp-babel @babel/core @babel/preset-env gulp-newer gulp-cached gulp-plumber core-js


③設定ファイルを作成
これらの設定ファイルは、すべて対象プロジェクトフォルダに入れて下さい。

●package.json
.babelrcを作成してもいいですが、package.jsonに含めることができて、その方が作成すべき総ファイル数が少なくて済みますのでおすすめです。

{
  "scripts": {
    "build:ie11": "gulp build:ie11"
  },
  "babel": {
    "presets": [
      [
        "@babel/preset-env",
        {
          "targets": {
            "ie": "11"
          },
          "useBuiltIns": "entry",
          "corejs": 3
        }
      ]
    ]
  }
}

なお、他の開発者と環境を統一したい場合は以下も含めて書くようにします。

"devDependencies": {
  "@babel/core": "^7.27.1",
  "@babel/preset-env": "^7.27.2",
  "core-js": "^3.42.0",
  "gulp": "^4.0.2",
  "gulp-babel": "^8.0.0",
  "gulp-newer": "^1.4.0",
  "gulp-plumber": "^1.2.1"
  }


●gulpfile.js
これも一例です。高速に処理させるため、変更があったファイルのみ更新するようにしています。
Scriptsフォルダにある.jsファイルすべてが対象となり、ScriptsDistフォルダへIE11向けのファイルが出力されます。

const { src, dest } = require('gulp');
const babel = require('gulp-babel');
const newer = require('gulp-newer');
const cached = require('gulp-cached');
const plumber = require('gulp-plumber');
const path = require('path');

const SRC_BASE = 'Scripts';
const SRC_PATTERN = `${SRC_BASE}/**/*.js`;
const DEST_DIR = 'ScriptsDist';

function buildIE11() {
  return src(SRC_PATTERN, { base: SRC_BASE })
    .pipe(plumber())
    .pipe(cached('ie11'))  // 最初にキャッシュ確認
    .pipe(newer(DEST_DIR)) // 変更があったファイルのみ
    .pipe(babel({
      presets: [['@babel/preset-env', {
        targets: { ie: '11' },
        useBuiltIns: 'usage',
        corejs: 3
      }]]
    }))
    .pipe(dest(DEST_DIR));
}

exports['build:ie11'] = buildIE11;

また、Babelの useBuiltInsの設定は「entry」と「usage」の二つあり、以下の特性があります。
構成に応じて変更してください。

構成 useBuiltIns設定値
JSをバンドルしている entry
JSをバンドルしていない usage

次は構成編です。
sasaki816.hatenablog.com

JSONによるデータ連携

WEBアプリケーションでクライアントからサーバーへのデータ受け渡し方法は色々ありますが、属性が複数ある場合はカンマ区切りで渡すことがあります。
しかし、この場合要素の何番目が何の要素かの判断が必要で、属性が増えれば増えるほど面倒なことになります。

例えば、0番目が社員番号、1番目が氏名、2番目が年齢、などの場合です。大抵はhiddenなどのサーバーコントロールに値を入れて、サーバー側でpostbackした際に受け取ってsplitする形になります。

var syainCD = 12345;
var syainName = "佐々木";
var age = 40;
hidValue.value = syainCD  + "," + syainName  + "," + age; 
string[] values = hidValue.value.split(',')

こういった場合は、JSONでデータ受け渡しを行うとクラスへデータを渡せるので非常に便利です。
先にサーバー側でクラスを作っておきます。

public class JSonClass {
   public string SyainCD { get; set; }
   public string SyainName { get; set; }
   public string Age{ get; set; }
}

Javascript側でそれぞれの属性ごとに値を設定し、最後は「JSON.stringify」で変換します。

var json = { "SyainCD": syaiinCD, "SyainName": syainName,"Age": age};
hidValue.value = JSON.stringify(json);

サーバー側はPostbackした際にサーバーコントロールに格納されたJSON形式データをデシリアライズします。

var data = JsonSerializer.Deserialize<JSonClass >(hidSeq.Value);

こうすることで、サーバー側ではクラスのプロパティとして属性の値を取得することが可能です。


また、複数要素が絡む場合、つまり上記属性を持つデータを複数人扱うような場合は、配列にして渡します。

var json = [];
json.push({ "SyainCD": syaiinCD, "SyainName": syainName,"Age": age});
・・・
hidValue.value = JSON.stringify(json);

この場合サーバー側も配列にする必要があるので、Listなどジェネリックコレクションで受け取るとよいです。

var data = JsonSerializer.Deserialize<List<JSonClass> >(hidSeq.Value);

回復環境が見つかりません対処法 その2

以前Windows回復環境で以下の話をしました。
sasaki816.hatenablog.com

一応これで回復環境は復活するのですが、別の問題で回復環境が使えない場合があります。
WinRE を有効にするコマンド「reagentc /enable」をしても「REAGENTC.EXE: Windows RE イメージは見つかりませんでした。」というエラーが表示される場合が今回の現象です。

reagentc /enable
REAGENTC.EXE: Windows RE イメージは見つかりませんでした。

この原因はWinre.wim(Windows RE イメージ)が起動できない場合です。この場合はWindows10又は11のISOファイルからWinre.wimを取り出して入れてしまえば解決します。

Windows 11 のダウンロード
www.microsoft.com


リンクを開いたら、インストールメディアを作成するところまで行って「今すぐダウンロード」を選択します。


ダウンロードした実行ファイルを実行して、適当な外付けUSBにインストールメディアを作成してください。
* なお、作成対象にしたUSBは全てフォーマットされますので注意!


作成が終わったら、作成したインストールメディアを開いて、「sources」→「install.esd」を右クリック→「7_Zipで開く」で開きます(7_Zipは自力でインストールしてください・・・)。

こんな感じで格納されているので、

自分のパソコンのエディションを選択します。
1 = Windows 10 Home
3 = Windows 10 Pro


Windows」→「System32」→「Recovery」に「Winre.wim」があるので、これを回復環境を復活させたい対象パソコンの「C:\Windows\System32\Recovery」へコピーします。


次に対象パソコン側で、以下コマンドを発行します。

reagentc /setreimage /path C:\windows\system32\recovery

「REAGENTC.EXE: 操作は成功しました。」と表示されればOK。

もう一度、以下を実行します。

reagentc /enable

「REAGENTC.EXE: 操作は成功しました。」と表示されれば成功です。


これでめでたく回復環境が使えるようになります。

src属性のルート相対パス設定あれこれ

ASP.NETなどのHTMLでJavascriptや画像などの場所をsrc属性で指定する場合、普通のコンテンツページの場合は問題ありませんが、マスターページを利用していてそのコンテンツが異なる階層の場合、固定的に相対パスを記述すると読み込めなくなります。

例えば以下のような階層で作成した場合です。

WebForm1とWebForm2はどちらもSite1マスターページのコンテンツページです。
マスターページ側でJavascriptを読むように記述した場合、ScriptsフォルダはWebForm1から見た場合一つ上ですが、WebForm2から見た場合は二つ上になります。

このような場合はチルダを使ったルート相対パスを利用しますが、href属性と異なり、src属性はチルダを使っても勝手にルート相対パスへ変換してくれません。src属性を使ったチルダはResolveClientUrlメソッドを使ってインライン式(<%=・・・%>)を埋め込むことで変換されます。

<script src="<%= this.ResolveClientUrl("~/scripts/javascript.js") %>" type="text/javascript"></script>

※headタグ内に記載する場合はheadタグにrunat="server"を追加して下さい


大抵の場合はこれで解決するはずですが、クライアント側に記載したタグ内部に対してサーバー側イベント(C#側のPage_InitやPage_Loadなど)で変更を加えるような処理を行うと、「コントロールにコード ブロック (<% ... %>) が含まれているため、コントロールのコレクションを変更できません。」のエラーが発生してしまいます。


これを回避するにはいくつか手法があります(全てサーバー側で記述する、全てインライン式で記述する等)が、一番楽な方法は以下のように必要なインライン式部分をPlaceHolderタグで囲む方法です。

<asp:PlaceHolder ID="plhJS" runat="server">
   <script src="<%= this.ResolveClientUrl("~/scripts/javascript.js") %>" type="text/javascript"></script>
</asp:PlaceHolder>


これでWebForm1からだと「../scripts/javascript.js」、WebForm2だと「../../scripts/javascript.js」で変換してくれます。

ポストバック判断まとめ(備忘)

なんか同じこと何回も調べるので備忘。
初回実行時、フルポストバック、パーシャルポストバックのまとめ表。

※〇:True  ✕:False

項目 IsPostBack IsInAsyncPostBack
初回実行時
パーシャル
PostBackTrigger
submit