webpack+TypeScriptでNode.js用にバンドルする
概要
筆者は仕事でよくサーバーレス構成のアプリを作成します。コンピューティングな部分は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
を削除し、private
をtrue
に設定します。また、build
のnpmスクリプト設定するのが慣習になっているので、設定します。
{
"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
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
import { getTodo } from '../infrastructures/todo-api'
export const handler = async () => {
const todo = await getTodo()
return todo
}
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
コードも修正します。
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;
};
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/
に設定します。そのほかはドキュメント通りです。
{
"compilerOptions": {
"outDir": "./build/",
"noImplicitAny": true,
"module": "es6",
"target": "es5",
"jsx": "react",
"allowJs": true,
"moduleResolution": "node"
}
}
そして、webpack.config.jsも修正します。
ts-loader
はmodule.rules
で指定します。また、.ts
および.js
の拡張子のファイルを解決するようにresolve
を指定します。
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
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