Closed29

webpack v4でIE11 (ES5) 向けバンドルを作る

kazuma1989kazuma1989

「なんで今さら?」
そういう要件があるのだ。MicrosoftによるIE11のサポートは終わっていないし(TeamsやOffice 365の対応は終わった&終わるけど)

kazuma1989kazuma1989

というより、サポートを終わらせるために告知のダイアログを作る。そしてそのダイアログは、アプリ本体がIE11に対応せずクラッシュするようになったあとも動かしたい。ダイアログ単体をIE11でも動くJSとして出力したい。

kazuma1989kazuma1989

ちなみにダイアログをちょろっと出すだけなので、大きなパッケージに依存したりポリフィルを入れたりはしないです。なので本当にシンプルなトランスパイル&バンドルだけになります。

kazuma1989kazuma1989

基本のwebpack.config

webpack.config.js
// @ts-check
const path = require('path');

/** @type {import("webpack").Configuration} */
const config = {
  entry: path.resolve(__dirname, 'src/index.ts'),
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'my-first-webpack.bundle.js',
  },
};

module.exports = config;
kazuma1989kazuma1989

// @ts-check@type などで少しでもVS Codeの補完の恩恵を得る。@types/webpack のインストールが必要。

kazuma1989kazuma1989

module.exports = { ... } と書いてしまうと、補完は効くけど、赤線は引いてくれなくなるので、分けて書いてます。

kazuma1989kazuma1989
src/index.ts
import { foo } from './common';

foo();
src/common.ts
export function foo() {
  const message = 'foo';

  console.info(message);
}
kazuma1989kazuma1989

このままだと './common' が解決できずに失敗する。

$ webpack --mode production
Hash: e8a52a7ba0cf55d60158
Version: webpack 4.46.0
Time: 56ms
Built at: 2021/05/13 17:41:48
 1 asset
Entrypoint main = my-first-webpack.bundle.js
[0] ./src/index.ts 40 bytes {0} [built]

ERROR in ./src/index.ts
Module not found: Error: Can't resolve './common' in '/Users/kazuma/work/src'
 @ ./src/index.ts 1:0-31 3:0-3
error Command failed with exit code 2.
kazuma1989kazuma1989

拡張子 .ts はデフォルトでは解決できないので。次の設定にする(.js が解決できなくなるが構わない)

webpack.config.js
 // @ts-check
 const path = require('path');
 
 /** @type {import("webpack").Configuration} */
 const config = {
   entry: path.resolve(__dirname, 'src/index.ts'),
   output: {
     path: path.resolve(__dirname, 'dist'),
     filename: 'my-first-webpack.bundle.js',
   },
+
+  resolve: {
+    extensions: ['.ts'],
+  },
 };
 
 module.exports = config;
kazuma1989kazuma1989

👏

$ webpack --mode production
Hash: b3ef52cfb0d7315fe3b5
Version: webpack 4.46.0
Time: 87ms
Built at: 2021/05/13 17:43:26
                     Asset       Size  Chunks             Chunk Names
my-first-webpack.bundle.js  971 bytes       0  [emitted]  main
Entrypoint main = my-first-webpack.bundle.js
[0] ./src/index.ts + 1 modules 117 bytes {0} [built]
    | ./src/index.ts 40 bytes [built]
    | ./src/common.ts 77 bytes [built]
✨  Done in 0.85s.
kazuma1989kazuma1989

ts-loaderがなくても動いているように見えるが、それは拡張子が .ts なだけでTS特有の文法がないから。

src/common.ts
 export function foo() {
-  const message = 'foo';
+  const message: string | null = 'foo';
 
   console.info(message);
 }
$ webpack --mode production
Hash: 3d3933751c042a4ea898
Version: webpack 4.46.0
Time: 192ms
Built at: 2021/05/13 17:54:40
 1 asset
Entrypoint main = my-first-webpack.bundle.js
[0] ./src/common.ts 330 bytes {0} [built] [failed] [1 error]
[1] ./src/index.ts 40 bytes {0} [built]

ERROR in ./src/common.ts 2:15
Module parse failed: Unexpected token (2:15)
You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders
| export function foo() {
>   const message: string | null = 'foo';
| 
|   console.info(message);
 @ ./src/index.ts 1:0-31 3:0-3
error Command failed with exit code 2.
kazuma1989kazuma1989
webpack.config.js
 // @ts-check
 const path = require('path');
 
 /** @type {import("webpack").Configuration} */
 const config = {
   entry: path.resolve(__dirname, 'src/index.ts'),
   output: {
     path: path.resolve(__dirname, 'dist'),
     filename: 'my-first-webpack.bundle.js',
   },
 
+  module: {
+    rules: [
+      {
+        test: /\.ts$/,
+        use: 'ts-loader',
+        exclude: /node_modules/,
+      },
+    ],
+  },
   resolve: {
     extensions: ['.ts'],
   },
 };
 
 module.exports = config;
kazuma1989kazuma1989
$ webpack --mode production
Hash: c6dd3d494f3696851b6b
Version: webpack 4.46.0
Time: 2624ms
Built at: 2021/05/13 17:58:23
                     Asset       Size  Chunks             Chunk Names
my-first-webpack.bundle.js  971 bytes       0  [emitted]  main
Entrypoint main = my-first-webpack.bundle.js
[0] ./src/index.ts + 1 modules 117 bytes {0} [built]
    | ./src/index.ts 39 bytes [built]
    | ./src/common.ts 78 bytes [built]
✨  Done in 4.14s.
kazuma1989kazuma1989

あ、tsconfig.jsonを忘れていた。tsconfig.jsonでトランスパイルが通る状態にしつつ、noEmit: false(falseが
デフォルト値なので、あえて noEmit: true にしなければOK)にしておけばOK。

kazuma1989kazuma1989
tsconfig.json
// https://www.typescriptlang.org/ja/tsconfig
{
  // src フォルダーだけを対象にする。
  "include": ["./src/**/*"],

  "compilerOptions": {
    //
    // プロジェクト設定
    //
    // tsc 以外のツールが TypeScript をトランスパイルしやすいようにいくつかの記法を禁止する。
    "isolatedModules": true,

    // IE11 で使える API を許可する。
    "lib": ["ES5", "DOM"],

    // ES Module 記法で書くための設定。
    "module": "ESNext",

    // IE11 で使える文法を許可する。
    "target": "ES5",

    //
    // 厳密なチェック
    //
    // スクラッチで書くなら strict=true.
    "strict": true,

    //
    // モジュール解決
    //
    // `import * as React from "react"` の代わりに `import React from "react"` と書ける。
    "allowSyntheticDefaultImports": true,

    // allowSyntheticDefaultImports のランタイム版といったイメージ。
    "esModuleInterop": true,

    // import 対象の探し方を決める。後方互換を求める理由がなければ原則 Node.
    "moduleResolution": "Node",

    //
    // ソースマップ
    //

    //
    // Linter Checks
    //
    // インデックスアクセスは null チェックが必要。
    "noUncheckedIndexedAccess": true,

    //
    // 実験的な機能
    //

    //
    // Advanced
    //
    // モジュール名(=ファイル名)の大文字/小文字を厳密に区別する。
    // ファイル名の大文字/小文字が区別されない OS で役に立つ。
    "forceConsistentCasingInFileNames": true,

    // tsc コマンドは自分のソースコードの型チェックだけに用いる(パッケージ間の整合性まではチェックしない)。
    "skipLibCheck": true
  }
}
kazuma1989kazuma1989

module=ESNextにしたけど自信がない。import文はどうせwebpackがハンドリングするのでなんでもいいかなと思いESNextにしたが、ランタイムに違いが生まれるのだろうか。

kazuma1989kazuma1989

結構違いがありそう?やってることは一緒か?
- は module=CommonJS, + は module=ESNext)

dist/my-first-webpack.bundle.js
@@ -90,18 +90,16 @@
 /*!***********************!*\
   !*** ./src/common.ts ***!
   \***********************/
-/*! no static exports found */
-/***/ (function(module, exports, __webpack_require__) {
+/*! exports provided: foo */
+/***/ (function(module, __webpack_exports__, __webpack_require__) {
 
 "use strict";
-
-Object.defineProperty(exports, "__esModule", { value: true });
-exports.foo = void 0;
+__webpack_require__.r(__webpack_exports__);
+/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "foo", function() { return foo; });
 function foo() {
     var message = 'foo';
     console.info(message);
 }
-exports.foo = foo;
 
 
 /***/ }),
@@ -110,14 +108,14 @@ exports.foo = foo;
 /*!**********************!*\
   !*** ./src/index.ts ***!
   \**********************/
-/*! no static exports found */
-/***/ (function(module, exports, __webpack_require__) {
+/*! no exports provided */
+/***/ (function(module, __webpack_exports__, __webpack_require__) {
 
 "use strict";
+__webpack_require__.r(__webpack_exports__);
+/* harmony import */ var _common__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./common */ "./src/common.ts");
 
-Object.defineProperty(exports, "__esModule", { value: true });
-var common_1 = __webpack_require__(/*! ./common */ "./src/common.ts");
-common_1.foo();
+Object(_common__WEBPACK_IMPORTED_MODULE_0__["foo"])();
 
 
 /***/ })
kazuma1989kazuma1989

よくわからんからTreeShakeの有効な(いやこの規模でかつ目的的にそんなの必要なパッケージに依存はしないだろうが)ESNextにしておくか。実機で動作確認して動かなければCommonJS, AMDを試せばいい。

kazuma1989kazuma1989

module=ESNextでバンドルしたJSがIE11で動いた。これで設定はよし。

kazuma1989kazuma1989

あとはロジックを書いていくだけ。とはいってもinsertAdjacentHTMLでダイアログHTMLをねじ込むだけだが。string literalが使えるので書きやすいはず。

kazuma1989kazuma1989

画像やCSSが必要だった。css-loaderやfile-loaderの設定も追加する。

kazuma1989kazuma1989
webpack.config.js
@@ -16,6 +16,21 @@ const config = {
         use: 'ts-loader',
         exclude: /node_modules/,
       },
+
+      {
+        test: /\.css$/,
+        use: [
+          {
+            loader: 'style-loader',
+          },
+          {
+            loader: 'css-loader',
+            options: {
+              modules: true,
+            },
+          },
+        ],
+      },
     ],
   },
   resolve: {
kazuma1989kazuma1989

"css-loader": "3.6.0", と少し古めなので、*.module.css を自動で拾ってくれない。のでオプションを明示的に設定した。

kazuma1989kazuma1989

CSSファイルを眺めながら文字列でクラス名を揃える、ということはやりたくなかったので typed-css-modules を使った。とても便利。頻繁には実行しないのでnpx実行にしておいた。

kazuma1989kazuma1989

file-loaderではなくurl-laoderを加える。画像は小さめなのでできればONE JSにパックしたいため。

kazuma1989kazuma1989
webpack.config.js
@@ -31,6 +31,11 @@ const config = {
           },
         ],
       },
+
+      {
+        test: /\.png$/,
+        use: 'url-loader',
+      },
     ],
   },
   resolve: {
このスクラップは2021/05/28にクローズされました