はじめに
こんにちは、開発本部第2開発部、Webフロント第2グループでフロントエンド開発をしている伊原です。
私が開発を担当しているSafie Entrance2にて、認証に用いる顔認証端末のセットアップができるアプリケーションが欲しい、という要望が上がりました。
そこでVue3とElectronを利用して簡素なアプリケーションを作成しましたので、本記事ではVue3+Electronのアプリケーション構築方法のハンズオンを記載します。
当時は状態管理にPineを利用していましたが、今回のセットアップでは状態管理まではせず、アプリケーションの基本的な部分までをセットアップします。
アプリケーションの構成
本記事執筆時点での情報をもとに記載しているため、フレームワークのアップデートによりセットアップが異なる可能性があります。その場合は各技術で紹介している公式ドキュメントをご確認ください。
アプリケーションに採用した各技術について簡単に説明します。
Electron
次の説明は公式サイトより引用です。
Electron は、JavaScript、HTML、CSS によるデスクトップアプリケーションを構築するフレームワークです。 Electron は Chromium と Node.js をバイナリに組み込むことで、単一の JavaScript コードベースを維持しつつ、ネイテイブ開発経験無しでも Windows、macOS、Linux で動作するクロスプラットフォームアプリを作成できます。
https://www.electronjs.org/ja/docs/latest
Electronを利用することで、Webフロントエンドで利用されている技術をそのまま利用してデスクトップアプリケーションを作成する事ができます。
今回はVueフレームワーク+TypeScriptを利用します。
Electronの特徴として、以下2つのプロセスで動作しており、それぞれ役割が異なっています。
- メインプロセス
アプリケーション全体の挙動の制御をする。Node.jsのAPIを利用できる。
- レンダラープロセス
ウェブコンテンツをレンダリングする。
この2つのプロセスはお互いに独立しており、レンダラープロセスからNode.jsのAPIを直接利用してローカルにログを出力したりする事はできないようになっています。 これはセキュリティ上の理由からで、Webサイトの画面上からローカルのあらゆるものが操作可能、というのは問題がありますよね。
また、レンダラープロセスでAPIを呼び出す場合、API提供側がCORSを有効にしている場合、CORSエラーが発生します。 これを回避するため、メインプロセス側でAPI呼び出し後、結果をレンダラープロセスに渡してあげる必要があります。
メインプロセスでレンダラープロセスの通信結果を受け取りたい場合には、プロセス間通信と呼ばれる方式によってデータをやりとりします。 詳細は割愛しますが、後ほどのハンズオンで説明するpreloadスクリプトを利用して通信します。
各プロセスの詳細な説明は次の公式サイトをご確認ください。 https://www.electronjs.org/ja/docs/latest/tutorial/process-model https://www.electronjs.org/ja/docs/latest/tutorial/tutorial-preload
Vue.js
Web ユーザーインターフェース構築のための、親しみやすく、パフォーマンスと汎用性の高いフレームワークです。
今回は、公開されている最新バージョンのVue3を利用します。
TypeScript
有名な型付け可能なaltJSです。 Electron、 VueともにTypeScriptをサポートしているため、JavaScriptではなくTypeScriptを利用します。
新規アプリケーションのハンズオン
それでは、これまで紹介した技術を使ってアプリケーションの雛形を作ってみたいと思います。
事前準備
事前に以下の環境を構築しておきます。
- Node.js: v16.20
- npm: 8.19.4
Electron+Vueの開発では、セットアップを簡単にするVue CLI Plugin Electron Builderというプラグインが提供されています。そちらを利用してVue Cliにてセットアップを進めていきます。
Vue CLIの導入
アプリケーションのセットアップのため、Vue-CLIを導入します。 Vue CLI Plugin Electron Builderでは執筆時点では4が推奨されていますので、4.Xの最新版であるv4.5.19をインストールします。
$ npm install @vue/cli@4.5.19
Vueプロジェクトの作成
Vue-CLIでプロジェクトのテンプレートを作成します。
$ vue create my-electron-prj
作成時に表示されるオプション選択は、初めのvue3-tsを選択します。
Electronの組み込み
Vue CLI Plugin Electron Builderを利用してelectronをプロダクトに組み込みます。 先ほど作成したプロジェクトのディレクトリに移動し、electron-builderを追加します。
$ cd my-electron-prj $ vue add electron-builder
途中の質問には以下のように回答します。
ここで1つ問題があり、インストールされるElectron13.0.0はEOLとなっているため、Node.16に対応したサポート期間中のElectronにアップデートします
$ npm install electron@22.0.0
このままビルドするとvue-loaderのエラーで怒られるので、v16をインストールします。
$ npm install vue-loader-v16
アプリケーションの起動
ここまで環境構築が終わったら、実際にアプリケーションを立ち上げてみましょう。
$ npm run electron:serve
以下のような画面でアプリケーションが起動すれば成功です。 ここまででアプリケーションの大枠は作成できました。ひとまずお疲れ様でした。
アプリケーションの雛形は作成できましたが、現状ではメインプロセスとレンダラープロセス間で通信するための仕組みがまだ実装されておらず、メインプロセスでの処理結果を画面側に表示する、というような事はできません。 次以降でメインプロセスとレンダラープロセスを通信させるためのpreloadスクリプトを作成していきます。
preloadスクリプトの作成
メインプロセスとレンダラープロセスのプロセス間通信のため、preloadスクリプトを作成します。 今回は試しに、公開されている住所検索APIを利用して取得した結果を画面上に表示してみようと思います。
メインプロセスへのipcMainモジュールの追加
src/background.tsにメインプロセスからレンラダープロセスへ非同期で通信するためのモジュールであるipcMainモジュールをelectronからインポートします。
import { app, protocol, BrowserWindow, ipcMain } from "electron";
インポートと共に、住所取得APIの処理を追加します。 追加前に、APIリクエストのためにHTTPSリクエストライブラリとしてaxiosをインストールしておきます。
$ npm install axios
zipcode(郵便番号)を引数として住所情報を取得する処理を追加します。
ipcMain.handle( "searchZipcode", async (_event: IpcMainInvokeEvent, zipcode: string) => { const response = await axios.get( "https://zipcloud.ibsnet.co.jp/api/search", { params: { zipcode }, } ); return response.data; } );
preloadスクリプトの作成
レンダラープロセスからメインプロセスで追加したAPIを呼び出し、結果を受け取るためにipcRender, contextBridgeをインポートします。 ipcRenderはメインプロセスの指定のチャネルにイベント・引数を送信し、処理結果を受け取ります。contextBridgeはwindowにAPIを注入し、window[apiKey]にてAPIにアクセスできるようになります。 以下、preload.tsのファイルです。
import { contextBridge, ipcRenderer } from "electron"; contextBridge.exposeInMainWorld("api", { searchZipcode: (zipcode: string) => { return ipcRenderer.invoke("searchZipcode", zipcode); }, });
pathをインポートし、作成したpreload.tsのパスをbackground.tsのcreateWindow()関数のBrtowserWindow()をnewしているところのwebPreferencesに追加します。
import path from "path"; webPreferences: { nodeIntegration: process.env .ELECTRON_NODE_INTEGRATION as unknown as boolean, contextIsolation: !process.env.ELECTRON_NODE_INTEGRATION, preload: path.join(__dirname, "preload.js"), },
これでpreloadスクリプトの追加はOK…と言いたいところですが、このままだと1つ問題があり、ビルド時にpreload.jsがバンドルされません。 対策は次で紹介されている通りで、ルートディレクトリにvue.config.jsを作成し、preloadスクリプトのパスを記述します。
module.exports = { pluginOptions: { electronBuilder: { preload: "src/preload.ts", }, }, };
住所検索画面の作成
これでAPIを呼び出す準備はできたので、住所検索画面を作成していきます。 簡単な動作確認が目的なので、テンプレートを作成した際のviews/About.vueを差し替えて作成してしまいます。
<template> <div class="search-zipcode"> <div> <input v-model="zipcode" /> <button :disabled="isFetching" @click="onSearchZipcode">検索</button> </div> <div> <span v-if="isFetching">取得中</span> <span v-else>住所情報:{{ result }}</span> </div> </div> </template> <script lang="ts"> import { defineComponent, ref } from "vue"; export default defineComponent({ name: "SearchZipcode", setup() { const zipcode = ref(""); const result = ref(""); const isFetching = ref(false); const onSearchZipcode = async () => { try { isFetching.value = true; const response: { message: string | null; results: [ { address1: string; address2: string; address3: string; kana1: string; kana2: string; kana3: string; prefcode: string; zipcode: string; } ]; } = await (window as any).api.searchZipcode("4340016"); const { address1, address2, address3 } = response.results[0]; result.value = address1 + address2 + address3; } catch (error) { console.log(error); } finally { isFetching.value = false; } }; return { onSearchZipcode, zipcode, result, isFetching }; }, }); </script>
郵便番号の入力フォームと検索ボタン、それと検索結果表示用の欄のみがあるシンプルなページです。windowインターフェースに追加されたsearchZipcodeに入力した郵便番号を渡し、受け取った結果を結合して住所情報として表示します。
試しに、セーフィー株式会社の本社郵便番号「141-0033」を入力して検索してみます。
住所情報に想定通りの情報が表示されています。これで、メインプロセスのAPI呼び出しとプロセス間通信が問題なく動作している事が確認できました。
ここまでのハンズオンでアプリケーションの雛形作成、API呼び出しからのプロセス間通信が実現できました。後は同様に作っていただければ、Electronのアプリケーションが作成できるはずです。
終わりに
本記事ではVue3 + TypeScript + Electronでのアプリケーションの開発方法の基本をご説明しました。皆さんも本記事のハンズオンを活用して、Electronのアプリケーションを作ってみてください!