🔥

webpack+TypeScriptでNode.js用にバンドルする

2022/03/16に公開

概要

筆者は仕事でよくサーバーレス構成のアプリを作成します。コンピューティングな部分はAWS Lambdaを利用することが大変多いです。AWS Lambdaにおいては、Node.jsがランタイムとしてサポートされております、。当然ながら、TypeScriptはネイティブにはサポートされていないので、TypeScriptで作成されたソースコードをNode.js環境で動作するようにコンパイルしてやる必要があります。

また、AWS Lambdaといっても、多くはAPIGatewayやIoT Ruleなどのサービスのバックエンドで動作させることが多いと思います。その場合、例えばAPIGatewayの場合は、リソース*メソッドの数だけ別個のLambdaをデプロイすることになると思います。それぞれのAPI向けにバンドルされたコードをデプロイするのが理想的です。

これらの前提を踏まえると、tscでは複数のoutputが出来ないなど対応できない部分があるため、今回はwebpackでTypeScriptを変換し、バンドルを作成することにします。

用語について

ソースコード

われわれ開発者が編集するコードです。

バンドルとは

バンドルとは複数のファイルにまたがるソースコードを一つにまとめることです。モジュールごとの依存関係を解決して、一つにしておくことで容量が小さくなったり、読み込みの速度が上がったりします。

Webpackについて

Webpackとはモジュールバンドラーのことです。JavaScriptにおけるモジュールの歴史はなんとも複雑怪奇なものです。ESModule(import/export)を使うものがモダンブラウザでサポートされている一方、Node.jsにおいてはESModuleの策定前からモジュール機能の実装が進んでおりCommonjs(require)がデフォルトになっています。これらの実装の違いを何個かの設定をすることでバンドルにまとめ上げてくれるのがwebpackの役割です。

これからやること

複数のエントリーポイントを有するソースコードをwebpackにより依存関係を解決して、対応するoutputファイルを作成します。また、ts-loaderを用いて、TypeScriptのコンパイルもwebpackで行います。

$ node -v
v14.17.6

コード例

雛形プロジェクトの作成

$ mkdir typescript_sample && cd $_
$ yarn init -y
$ yarn add --dev webpack webpack-cli webpack-node-externals

ブラウザから使う予定はなく、公開する予定もないのでmainを削除し、privatetrueに設定します。また、buildのnpmスクリプト設定するのが慣習になっているので、設定します。

package.json
{
  "name": "typescript_sample",
  "version": "1.0.0",
-  "main": "index.js",
+  "private": true,
  "license": "MIT",
+  "scripts": {
+    "build": "webpack"
+  },
  "devDependencies": {
    "webpack": "^5.70.0",
    "webpack-cli": "^4.9.2",
    "webpack-node-externals": "^3.0.0"
  }
}

webpackの設定

まずはTypeScriptではなくJavaScriptで試してみます。

$ touch webpack.config.js
webpack.config.js
const path = require('path');

module.exports = {
  mode: 'development',
  target: 'node',
  externals: [
    nodeExternals({
      modulesFromFile: {
        exclude: ['dependencies'],
        include: ['devDependencies'],
      },
    }),
  ],
  entry: {
    'get-todo-handler': path.resolve(__dirname, './src/handlers/get-todo-handler.js')
  },
  output: {
    filename: '[name]/index.js', // [name]にはentry直下のキーが入る。ここでいう'get-todo-handler'
    path: path.resolve(__dirname, 'build'),
  }
};

以下のようにJavaScriptのファイルを追加します。

ここまでのフォルダ構成
.
├── package.json
+├── src
+│   ├── handlers
+│   │   └── get-todo-handler.js
+│   └── infrastructures
+│       └── todo-api.js
├── webpack.config.js
└── yarn.lock
src/handlers/get-todo-handler.js
import { getTodo } from '../infrastructures/todo-api'
export const handler = async () => {
  const todo = await getTodo()
  return todo
}
src/infrastructures/todo-api.js
export const getTodo = async () => {
  // 本当はここでaxiosなどで外部APIにアクセスしたりする
  return {
    "userId": 1,
    "id": 1,
    "title": "delectus aut autem",
    "completed": false
  }
}

ここまで出来たら、一度ビルドをし、ローカルでビルド後のファイルを実行してみます。返答が帰ってくれば成功です。

$ yarn build

TypeScriptの設定

ライブラリのインストール

TypeScriptと、webpackでTypeScriptを扱うためのts-loaderをインストールします。

$ yarn add --dev typescript ts-loader

ソースコードを修正

フォルダ構成
.
├── build
│   └── get-todo-handler
│       └── index.js
├── package.json
├── src
│   ├── handlers
-│   │   └── get-todo-handler.js
+│   │   └── get-todo-handler.ts
│   └── infrastructures
-│       └── todo-api.js
+│       └── todo-api.ts
+├── tsconfig.json
├── webpack.config.js
└── yarn.lock

コードも修正します。

src/handlers/get-todo-handler.js
import { getTodo } from "../infrastructures/todo-api";

interface Event {
  id: number;
}

interface Todo {
  userId: number;
  id: number;
  title: string;
  completed: boolean;
}

export const handler = async (event: Event): Promise<Todo> => {
  const id = event.id;
  const todo = await getTodo(id);
  return todo;
};
src/infrastructures/todo-api.ts
interface Todo {
  userId: number;
  id: number;
  title: string;
  completed: boolean;
}

export const getTodo = async (id: number): Promise<Todo> => {
  // 本当はここでaxiosなどで外部APIにアクセスしたりする
  return {
    userId: 1,
    id: 1,
    title: "delectus aut autem",
    completed: false,
  };
};

tsconfig.jsonはwebpackのドキュメントを参考にします。outDirはwebpackの方で指定するので関係ないのですが、tscコマンドで誤って実行した時のために、./build/に設定します。そのほかはドキュメント通りです。

tsconfig.json
{
  "compilerOptions": {
    "outDir": "./build/",
    "noImplicitAny": true,
    "module": "es6",
    "target": "es5",
    "jsx": "react",
    "allowJs": true,
    "moduleResolution": "node"
  }
}

そして、webpack.config.jsも修正します。
ts-loadermodule.rulesで指定します。また、.tsおよび.jsの拡張子のファイルを解決するようにresolveを指定します。

webpack.config.js
const path = require('path');

module.exports = {
  mode: 'development',
  target: 'node',
  externals: [
    nodeExternals({
      modulesFromFile: {
        exclude: ['dependencies'],
        include: ['devDependencies'],
      },
    }),
  ],
  entry: {
    'get-todo-handler': path.resolve(__dirname, './src/handlers/get-todo-handler.ts')
  },
  output: {
    filename: '[name]/index.js',
    path: path.resolve(__dirname, 'build'),
  },
+ module: {
+   rules: [
+     {
+       test: /\.ts$/,
+       use: [
+         {
+           loader: 'ts-loader',
+         },
+       ],
+     },
+   ],
+ },
+ resolve: {
+   extensions: ['.ts', '.js'],
+   modules: ["node_modules"]
+ },
};

以上で最低限の構成ができました。

handlerを増やす

フォルダ構成
.
├── build
│   └── app
│       └── index.js
├── package.json
├── src
│   ├── handlers
│   │   ├── get-todo-handler.ts
+│   │   └── put-todo-handler.ts
│   └── infrastructures
│       └── todo-api.ts
├── tsconfig.json
├── webpack.config.js
└── yarn.lock
webpack.config.js
const path = require('path');

module.exports = {
  ...(省略)...
  entry: {
    'get-todo-handler': path.resolve(__dirname, './src/handlers/get-todo-handler.ts'),
+    'update-todo-handler': path.resolve(__dirname, './src/handlers/update-todo-handler.ts'),
  },
  ...(省略)...
};

今感じで増やします。

build結果は以下のような感じになります。

フォルダ構成
├── build
│   └── get-todo-handler
│       └── index.js
+│   └── update-todo-handler
+│       └── index.js
├── package.json
├── src
│   ├── handlers
│   │   └── get-todo-handler.ts
│   └── infrastructures
│       └── todo-api.ts
├── tsconfig.json
├── webpack.config.js
└── yarn.lock

最後に

webpackによるTypeScriptのプロジェクトのバンドルする方法をまとめました。Node.js環境での実行を想定した非常にシンプルな例です。フロントでいろんなAssetsやStyleSheetなどをバンドルしたい場合は、もっと勉強する必要がありそうです。これらの学習を通して、JavaScriptのモジュールの奇々怪界な歴史に思いをはせたり、エコシステム全体として後方互換性を大事にしている姿勢に触れたりと大変勉強になりました。

Discussion