まくろぐ

webpack と TypeScript を組み合わせて使用する

更新:
作成:

はじめに

TypeScript は JavaScript コードに型付けすることができる優れたトランスパイラですが、変換後の .js ファイル群をまとめる(バンドルする)機能は備えていません。 また、モダンな Web サイトを構築するときは、CSS Modules や Sass/Less/Stylus といった仕組みを使用するのが常套手段となっています。

そのため、Web サイト用の .js ファイルを TypeScript を使って作成する場合、webpack などのバンドルツールを組み合わせて使用する必要があります。

  • TypeScript … .ts ファイルから .js ファイルへの変換
  • webpack … Web サイト用の各種リソースをバンドルする

バンドルツールには様々なものがありますが、大きなシェアを占めているのは webpack なので(2020年現在)、ここでは TypeScript と webpack を組み合わせて使用する方法を説明します。

☝️ webpack は必要なくなる? ES Module の仕組みにより、Web ブラウザからモジュール化された .js ファイルをインポートすることが可能になりつつあります。 しかし、Web サイトの最終的なデプロイ時には、各種リソースを最適化(minify など)する必要があるため、まだまだ webpack などのバンドルツールが必要です。

関連パッケージのインストール

TypeScript のインストール

プロジェクト用のディレクトリと package.json を作成し、TypeScript をインストールします。

$ mkdir myapp && cd $_
$ npm init -y
$ npm install --save-dev typescript

webpack のインストール

webpack 関連のパッケージをインストールします。

$ npm install --save-dev webpack webpack-cli ts-loader html-webpack-plugin
  • webpack … webpack 本体
  • webpack-cli … webpack のコマンドラインツール
  • ts-loader … webpack 経由で .ts ファイルをトランスパイルする
  • html-webpack-plugin.html ファイルを dist ディレクトリに出力する

実装

ここでは、下記のようなディレクトリ構成をゴールとします。 Web サーバーにデプロイすべきファイル群(HTML ファイルや JS ファイル)は、dist ディレクトリ以下に出力されることを想定しています。

ディレクトリ構成

myapp/
  +-- package.json       # Node プロジェクトの設定
  +-- tsconfig.json      # TypeScript の設定
  +-- webpack.config.js  # webpack の設定
  +-- dist/              # 出力先
  +-- src/
        +-- index.html
        +-- index.ts

TypeScript の設定 (tsconfig.json)

tsconfig.json
{
  "compilerOptions": {
    "target": "ES2015",
    "module": "commonjs",
    "lib": ["esnext", "dom"],
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,

    // 出力先などは webpack 側で指定するので本質的には必要なし
    "sourceMap": true,
    "outDir": "./dist",
    "sourceRoot": "./src",
  }
}

トランスパイル時の出力先などは webpack 側で制御するので、outDir などの指定は本質的には必要ありませんが、間違えて tsc コマンドで直接変換してしまった場合に、変な場所に .js ファイルが出力されないように念のため指定しておきます。

webpack の設定 (webpack.config.js)

webpack によるバンドル後の結果を dist ディレクトリに出力することや、TypeScript 関連のコード(.ts ファイル)を認識させるための設定を行います。

webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  // 開発用の設定
  mode: 'development',

  // エントリポイントとなるコード
  entry: './src/index.ts',

  // バンドル後の js ファイルの出力先
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'index.js'
  },

  // ソースマップファイルの出力設定
  devtool: 'source-map',

  module: {
    rules: [
      // TypeScript ファイルの処理方法
      {
        test: /\.ts$/,
        use: "ts-loader",
        include: path.resolve(__dirname, 'src'),
        exclude: /node_modules/
      }
    ]
  },

  plugins: [
    // HTML ファイルの出力設定
    new HtmlWebpackPlugin({
      template: './src/index.html'
    })
  ]
};

index.html の作成

Web サイトのトップページとなる index.html を作成します。 このファイルに script 要素を記述しておく必要はありません。 HtmlWebpackPluginsrc/index.htmldist/index.html にコピーするときに、script 要素を自動挿入してくれます。

src/index.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>MyApp</title>
    <meta name="viewport" content="width=device-width, initial-scale=1"></head>
  <body>
    <div id="root"></div>
  </body>
</html>

index.ts の作成

トップページから読み込まれる index.js ファイルの TypeScript 版を作成します。 ここでは、div 要素のテキストを Hello に変更しています。

src/index.ts
const root = document.getElementById('root') as HTMLDivElement;
root.innerText = 'Hello';

Web サイトのビルド

webpack と TypeScript の設定ができたら、次のようにビルドを実行します。

$ npx webpack

dist ディレクトリ以下に、index.htmlindex.js ファイルが出力されるので、出力された index.html ファイルを開いて、Hello と表示されることを確認します。

ビルド方法は、package.json でスクリプト定義しておきましょう。

package.json
{
  "name": "myapp",
  "version": "1.0.0",
  "scripts": {
    "build": "webpack"
  },
  "devDependencies": {
    "html-webpack-plugin": "^4.3.0",
    "ts-loader": "^7.0.5",
    "typescript": "^3.9.6",
    "webpack": "^4.43.0",
    "webpack-cli": "^3.3.12"
  }
}

これで、npm run build でビルドできるようになります。

関連記事

Electron で Hello World (3) React を使えるようにする

更新:
作成:
/p/6pybmv6/img-001.png

概要

ここでは、Electron アプリの開発に React を導入する手順を示します。 React を導入すると、HTML をフラットな形でゴリゴリ記述していくのではなく、独自コンポーネント(例: <MyButton> コンポーネント)を定義して、まとまりのある単位でコンテンツを構築していくことができます。

下記の手順により、Electron + TypeScript による開発環境が構築できていることを前提とします。

この記事の手順が完了すると、Electron + TypeScript + React による開発環境が整います。 一応 webpack などのバンドルツールを使わなくても開発を始められるので、Electron と React の開発環境としての相性はよさそうです。

React のセットアップ

React モジュールのインストール

React モジュールおよび、TypeScript 用の型定義ファイルをインストールします。

$ npm install --save react react-dom
$ npm install --save-dev @types/react @types/react-dom

package.json の内容は次のような感じになります。

package.json
{
  "name": "myapp",
  "version": "0.0.1",
  "main": "build/main.js",
  "scripts": {
    "build": "tsc",
    "start": "tsc && electron ."
  },
  "devDependencies": {
    "@types/node": "^14.0.14",
    "@types/react": "^16.9.41",
    "@types/react-dom": "^16.9.8",
    "electron": "^9.0.5",
    "typescript": "^3.9.6"
  },
  "dependencies": {
    "react": "^16.13.1",
    "react-dom": "^16.13.1"
  }
}

JSX コードの有効化

TypeScript の設定ファイル (tsconfig.json) を編集し、 .tsx ファイル内に記述した JSX コードを認識できるようにします。 JSX コードはトランスパイルによって、通常の JavaScript コードに変換されます。

tsconfig.json
{
  "compilerOptions": {
    "target": "es6",
    "module": "commonjs",
    "lib": ["esnext","dom"],
    "outDir": "build",
    "rootDir": "src",
    "jsx": "react",  // .tsx ファイル内の JSX を認識
    "sourceMap": true,
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  }
}

これだけで、Electron + TypeScript の開発環境上で React を使用できるようになります。

実装

React コンポーネント (Hello.tsx)

src/components ディレクトリ以下に、独自の React コンポーネントを定義するための .tsx ファイルを格納することにします。 ここでは、Hello コンポーネントを作成します。 Hello 要素は、オプショナルな name 属性(プロパティ)を指定できるようにしてみます。 JSX 形式のコードを含むので、拡張子は .tsx にすることに注意してください(.jsx の TypeScript 版です)。

src/components/Hello.tsx
import React from 'react';

// Hello コンポーネントの属性(プロパティ)
export interface HelloProps {
  name?: string;
}

// Hello コンポーネントの定義
export class Hello extends React.Component<HelloProps> {
  public render(): React.ReactNode {
    const name = this.props.name ?? 'Mr. Unknown';
    return (
      <h1>Hello {name} in Electron</h1>
    );
  }
}

レンダラープロセス (renderer.tsx)

上記で定義した Hello コンポーネントを表示するためのレンダラープロセスを実装します。 ここでも JSX コードを使うので、拡張子は .tsx を使います。

src/renderer.tsx
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { Hello } from './components/Hello';

ReactDOM.render(
  <Hello />,
  document.querySelector('#root')
);

このコードは、HTML 内の <div id="root"> 要素の内容を、Hello コンポーネントが出力する内容に置き換えます。 パッと見、React クラスのインポートは不要に見えますが、JSX コードが React クラスを使うコードに変換されるので、インポートしておかないとコンパイルエラーになります。

次の HTML ファイルから、上記のスクリプトを読み込みます。

public/index.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Hello Electron!</title>
    <!-- https://electronjs.org/docs/tutorial/security#csp-meta-tag -->
    <meta http-equiv="Content-Security-Policy"
          content="script-src 'self' 'unsafe-inline';" />
  </head>
  <body>
    <div id="root"></div>
    <script>require('../build/renderer.js');</script>
  </body>
</html>

メインプロセス (main.ts)

最後に、Electron アプリのエントリポイントとなるメインプロセス (main.ts) の実装です。 このコードは特に変更はなく、単純に public/index.html を読み込んで表示します。

src/main.ts
import { app, BrowserWindow } from 'electron';

// メインウィンドウの表示
function createWindow () {
  const options: Electron.BrowserWindowConstructorOptions = {
    width: 500,
    height: 200,
    webPreferences: {
      nodeIntegration: true  // とりあえず import のため
    }
  }
  const win = new BrowserWindow(options);
  win.loadFile('public/index.html');
}

// Electron の初期化が完了したらウィンドウを作成
app.whenReady().then(createWindow);

関連記事

Electron のメインプロセスとレンダラープロセスの関係

更新:
作成:

メインプロセスとレンダラープロセス

Electron アプリを作成するときは、メインプロセスとレンダラープロセスを意識して使い分ける必要があります。

/p/mkisgqb/img-001.png

レンダラープロセス側で呼び出し可能な Electron モジュール(他の Node モジュールも含む)は制限されていて、実行できる JavaScript ライブラリは、Web ブラウザ上で実行可能な JavaScript に毛が生えたものくらいものと考えておくのがよいです。

ipcRenderer モジュールは例外的にレンダラープロセスから使ってもよいとされているモジュールのひとつで、これ経由でメインプロセスに対して要求(メッセージ)を送ることができます。 逆にメインプロセスからのメッセージをハンドルすることもできます。

通知先のレンダラープロセスの指定

メインプロセスに対して、レンダラープロセスは複数存在することがあるので、メインプロセスからメッセージを送るときは、どのレンダラープロセスへのメッセージなのかを意識する必要があります。

レンダラープロセスからのメッセージに応答する

レンダラープロセスからのイベントを ipcMain.on() でハンドルする場合、コールバック関数の第1パラメータとして渡される event: Electron.IpcMainEvent オブジェクトから送信元のレンダラープロセス (sender: Electron.WebContents) を参照することができます。

下記のメインプロセスは、my-add イベントとしてレンダラープロセスから 2 つの数値を受け取り、IpcMainEvent.reply() を使って送信元のレンダラープロセスに応答メッセージ(数値を足した結果)を返しています。

main.ts(メインプロセス)
// レンダラープロセスからメッセージを受信して、応答メッセージを返す
ipcMain.on('my-add', (evt: Electron.IpcMainEvent, num1: number, num2: number) => {
  evt.reply('my-add-reply', num1 + num2);
  //evt.sender.send('my-add-reply', num1 + num2);
});

レンダラープロセス側では、ipcRenderer.send() でメインプロセスにメッセージを送り、その応答を ipcRenderer.on() でハンドルするように実装します。

renderer.ts(レンダラープロセス)
import { ipcRenderer } from 'electron';

// メインプロセスへメッセージを送信
ipcRenderer.send('my-add', 100, 200);

// メインプロセスからメッセージを受信
ipcRenderer.on('my-add-reply', (evt: Electron.Event, result: number) => {
    alert(result);  //=> 300
});

ある BrowserWindow のレンダラープロセスにメッセージを送る

BrowserWindow のインスタンスがあれば、そのウィンドウのレンダラープロセスに対してメッセージを送ることができます。

main.ts(メインプロセス)
const win = new BrowserWindow(options);

win.webContents.once('did-finish-load', () => {
  win.webContents.send('message-from-main', 'Hello!');
});
win.loadFile('public/index.html');

メインプロセス側から能動的にメッセージを送る場合は、レンダラープロセス側のコンテンツが準備できてから送らないといけないことに注意してください。 上記の例では、レンダラープロセスから最初に did-finish-load イベント を受診したときに、メッセージを送信するようにしています。

別の方法として、win.loadFile() が返す Promise が resolve 状態になるのを待つという方法があります。 タイミングは did-finish-load イベントが発行されるタイミングと同じになります。 このあたりは、loadFile() の API ドキュメント に記述されています。

main.ts(別の方法)
win.loadFile('public/index.html').then(() => {
  win.webContents.send('message-from-main', 'Hello!');
});

レンダラープロセス側は、単純に ipcRenderer.on() で受診すれば OK です。

renderer.ts(レンダラープロセス)
import { ipcRenderer } from 'electron';

ipcRenderer.on('message-from-main', (evt: Electron.Event, msg: string) => {
  alert('Message from main: ' + msg);
});

関連記事

メニュー

まくろぐ
サイトマップまくへのメッセージ