😎

Nuxt Modulesを使ってNuxt3にElectronを導入しよう!

はじめに

この記事はNuxt3のアプリケーションに、Nuxt Modulesを利用してElectronを導入する方法をハンズオン形式でご紹介します。

残念なことに、Electron用のNuxt Modulesであるnuxt-electronv0.7.0において、Nuxtのバージョンがv3.6.5より大きい場合、正しく動作しません。
devモードで起動した際、Electronのウィンドウが無限リロードしてしまいます。
詳細は以下のIssuesをご覧ください。
https://github.com/caoxiemeihao/nuxt-electron/issues/74

そこで、今回は正常に動作するバージョンまでNuxtのバージョンをダウングレードさせることで対応します。

Nuxtの最新版で動作する暫定的な方法も存在します。
それについては以下の記事を参照してください。

https://zenn.dev/comm_vue_nuxt/articles/cbaf30c7054c40

Nuxt Modulesとは

Nuxtの提供するモジュールシステムです。
フレームワークを拡張する機能を簡単に導入できます。

https://nuxt.com/docs/guide/concepts/modules

Electronとは

JavaScript、HTML、CSSを利用してデスクトップアプリを作成できるフレームワークです。

https://www.electronjs.org/ja/docs/latest

環境

疎通確認を行った環境です。

Node: v20.13.1
Nuxt: v3.6.5
Electron: v31.0.1
nuxt-electron: v0.7.0
パッケージマネージャー: npm

Nuxt Electronの導入

前提として必要なものは以下になります。

  • Node.js v18.0.0以上
  • CLI(ターミナル、コマンドプロンプト、etc.)
  • テキストエディタ

1. Nuxtアプリケーションのセットアップ

  1. 直下にNuxtプロジェクトを作成したいフォルダでnuxi initを使ってスタータプロジェクトを作成

    npx nuxi@latest init <project-name> --no-install --package-manager=npm
    

    実行するとGitを初期化するかどうかを聞かれます。
    お好みのものを指定してください。
    ※Gitの設定に関しては以降の手順に関与しません。

    --no-installオプションについて

    Nuxtのバージョンをダウングレードする必要があるため、同時にinstallを行わないオプションを付与しています。
    Nuxt Docs > API > Commands > nuxi init --no-install

    --package-manager=npmオプションについて

    利用するパッケージマネージャーとしてnpmを指定しています。
    そのほかのパッケージマネージャーの指定も可能ですが、以降の手順はnpmを選択した場合のものです。
    Nuxt Docs > API > Commands > nuxi init --package-manager

  2. 作成したNuxtプロジェクトに移動

    cd <project-name>
    
  3. package.jsonでnuxtのバージョンを3.6.5に変更
    テキストエディタでpackage.jsonを開き、以下のように編集します。

    {
      "name": "nuxt-app",
      "private": true,
      "type": "module",
      "scripts": {
        "build": "nuxt build",
        "dev": "nuxt dev",
        "generate": "nuxt generate",
        "preview": "nuxt preview",
        "postinstall": "nuxt prepare"
      },
      "dependencies": {
    -   "nuxt": "^3.11.2",
    +   "nuxt": "3.6.5",
        "vue": "^3.4.27",
        "vue-router": "^4.3.2"
      }
    }
    
  4. nuxt.config.tsのdevtoolsオプションをfalseにする

    Nuxt DevToolsの導入にはNuxt v3.9.0以上が必要です。

    Nuxt DevToolsにはNuxt v3.9.0以上が必要です。
    Nuxt DevTools Docs > Guide > Getting Started

    devtoolsオプションを無効化しない場合、コマンドを実行するたびにNuxt DevToolsをインストールするかを尋ねられます。

    nuxt.config.ts
    // https://nuxt.com/docs/api/configuration/nuxt-config
    export default defineNuxtConfig({
    - devtools: { enabled: true },
    + devtools: { enabled: false },
    });
    
  5. パッケージをインストールする

    npm install
    
  6. devモードでNuxtアプリケーションが起動できることを確認

    npm run dev -- -o
    
    オプションの意味

    devモード起動時に渡した-oは自動的にブラウザのページを開くオプションです。
    nuxiに実装されています。
    Nuxt Docs > API > Commands > nuxi dev

    また、その直前についている--はnpmオプションの終了を示す区切り文字です。
    これ以降の引数はスクリプトに直接渡されます。
    -oオプションはnuxiに実装されているため、スクリプトに直接引数を渡さなければ動作しません。
    npm Docs > CLI > Commands -npm-run-script

参考

上記の手順はNuxtの公式ドキュメントを参考に構成しています。
https://nuxt.com/docs/getting-started/installation#new-project

2. Nuxt Electronをinstall

の5つのパッケージを追加します。

npm install --save-dev nuxt-electron vite-plugin-electron vite-plugin-electron-renderer electron electron-builder

3. nuxt.config.tsのmodulesにelectronを追加

nuxt.config.tsmodulesプロパティを追加し、nuxt-electronを要素に含む配列を値として設定します。

nuxt.config.ts
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
  devtools: { enabled: false },
+ modules: ["nuxt-electron"],
});

modulesプロパティの詳細は以下を参照してください。
https://nuxt.com/docs/api/nuxt-config#modules-1

4. electronの型情報をNuxtに追加

npx nuxi prepare

このコマンドを実行することで、nuxt.config.tsにElectronの設定を追加する際に型補完を利用できるようになり、型エラーも出なくなります。

prepareコマンドはアプリケーションに.nuxtディレクトリを作成し、型を生成します。
Nuxt Docs > API > Commands > nuxi prepare

※devモードで起動した際、buildを実行した際などにも型情報が更新されます。

5. nuxt.config.tsにelectronの設定を追加

electronプロパティに設定を追加することで、Nuxt ModulesのElectronに対してオプションを設定できます。

nuxt.config.ts
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
  devtools: { enabled: true },
  modules: ["nuxt-electron"],
+ electron: {
+   build: [
+     {
+       entry: "electron/main.ts",
+     },
+   ],
+ },
+ ssr: false,
});

この設定はnuxt-electronのREADMEを参考にしています。

詳しい説明

buildオプションには複数のエントリビルドを指定できます。
これにはメインプロセス、プリロードスクリプト、ワーカープロセスなどが含まれます。

https://github.com/caoxiemeihao/nuxt-electron/blob/d7cf10ddaf8e050564d46b0a906cc7a468573e60/src/index.ts#L17

また、ssrオプションをfalseに設定する理由については以下のIssuesに基づくものです。
クライアントサイドのスタイルシートが正しくビルドされない問題を回避するための措置です。

https://github.com/caoxiemeihao/nuxt-electron/issues/43

6. electron-env.d.tsを作成する

electronで利用する環境変数の型を定義します。
プロジェクトのトップレベルにelectron-env.d.tsのコードは以下になります。

electron-env.d.ts
/// <reference types="vite-plugin-electron/electron-env" />

declare namespace NodeJS {
  interface ProcessEnv {
    APP_ROOT: string
    VITE_PUBLIC: string
  }
}

7. electron/main.tsを作成する

アプリケーションの作成、その設定や挙動を記述します。
プロジェクトのトップレベルにelectronフォルダを作成し、その中にmain.tsを作成します。
これは5. nuxt.config.tsにelectronの設定を追加にて、buildのentryとしてパスを設定したファイルです。

main.ts
import { app, BrowserWindow } from "electron";

let win: BrowserWindow | null;
const createWindow = () => {
  win = new BrowserWindow();

  if (process.env.VITE_DEV_SERVER_URL) {
    win.loadURL(process.env.VITE_DEV_SERVER_URL);
  }
};

app.on("window-all-closed", () => {
  if (process.platform !== "darwin") {
    app.quit();
    win = null;
  }
});
app.on("activate", () => {
  if (BrowserWindow.getAllWindows().length === 0) {
    createWindow();
  }
});

app.whenReady().then(() => {
  createWindow();
});

Electronの準備が終わったことを検知し、起動したdevサーバーのURLをElectronで開きます。
window-all-closedactivateのイベント発火時、プロセスの削除、起動を制御しています。

詳しい説明
BrowserWindow

BrowserWindow

ブラウザウィンドウを作成、制御できるモジュールです。
アプリのウィンドウの見た目や動作などを変更するメソッドが用意されています。
https://www.electronjs.org/ja/docs/latest/api/browser-window

loadURL

URLを受け取り、ブラウザウィンドウでそれを読み込みます。
https://www.electronjs.org/ja/docs/latest/api/browser-window#winloadurlurl-options

process.env.VITE_DEV_SERVER_URL

process.env.VITE_DEV_SERVER_URL

DevモードでNuxtアプリケーションが起動している際のURLが格納されている環境変数です。
nuxt-electronで実装されているようです。
https://github.com/caoxiemeihao/nuxt-electron/blob/d7cf10ddaf8e050564d46b0a906cc7a468573e60/src/index.ts#L110

app

app

Electronアプリケーションのイベントライフサイクルを制御するモジュールです。
アプリケーションの状態に関するイベントが発生し、それらを制御するメソッドが用意されています。
https://www.electronjs.org/ja/docs/latest/api/app

window-all-closedイベント

全てのウィンドウが閉じられたときに発生するイベントです。
https://www.electronjs.org/ja/docs/latest/api/app#イベント-window-all-closed

今回の実装では、WindowsとLinuxの一般的なアプリケーションのライフサイクルと同等の挙動を実現するために利用されています。
この実装により、すべてのウィンドウが終了した場合にアプリケーションを完全に終了します。
https://www.electronjs.org/ja/docs/latest/tutorial/quick-start#全ウインドウを閉じた時にアプリを終了する-windows--linux

activateイベント

macOS環境でにおいて、アプリケーションがアクティブになったタイミングで発生するイベントです。
https://www.electronjs.org/ja/docs/latest/api/app#イベント-activate-macos

今回の実装では、macOSの一般的なアプリケーションのライフサイクルと同等の挙動を実現するために利用されています。
この実装により、ウィンドウを開いていなくても起動し続け、ウィンドウがないときにアプリをアクティブにすると新規ウィンドウが開きます。
https://www.electronjs.org/ja/docs/latest/tutorial/quick-start#開いたウインドウがない場合にウインドウを開く-macos

whenReady

Electronの初期化を検知し、そのタイミングで実行されるメソッドです。
これにより、Electronがアプリケーションをロードする準備が整ったあとにそれらを実行できます。
https://www.electronjs.org/ja/docs/latest/api/app#appwhenready

そのほかにもブラウザをカスタマイズする設定が存在します。
詳細は以下を参照してください。
https://www.electronjs.org/ja/docs/latest/api/browser-window#new-browserwindowoptions

8. package.jsonのmainフィールドを設定

package.json
{
  "name": "nuxt-app",
  "private": true,
  "type": "module",
+ "main": "dist-electron/main.js",
  "scripts": {
    "build": "nuxt build",
    "dev": "nuxt dev",
    "generate": "nuxt generate",
    "preview": "nuxt preview",
    "postinstall": "nuxt prepare"
  },
  "dependencies": {
    "nuxt": "3.6.5",
    "vue": "^3.4.27",
    "vue-router": "^4.3.2"
  },
  "devDependencies": {
    "electron": "^30.0.9",
    "electron-builder": "^25.0.0-alpha.9",
    "nuxt-electron": "^0.7.0",
    "vite-plugin-electron": "^0.28.7",
    "vite-plugin-electron-renderer": "^0.14.5"
  }
}

9. 起動を確認

npm run dev

上記のコマンドを実行すると、Electronのウィンドウが起動し、Nuxtのスタートページが表示されるはずです。

アプリケーションのビルド

ビルドしたアプリケーションを正しく動作させるためにはdevモードでの動作環境に追加で設定が必要になります。

elecrton/main.tsにコードを追加する

ビルド後のindex.htmlのパスを用意し、それをロードするようなコードを追加します。

electron/main.ts
import { app, BrowserWindow } from "electron";
+ import path from "node:path";

+ process.env.APP_ROOT = path.join(__dirname, "..");
+ export const RENDERER_DIST = path.join(process.env.APP_ROOT, ".output/public");
+ process.env.VITE_PUBLIC = process.env.VITE_DEV_SERVER_URL
+   ? path.join(process.env.APP_ROOT, "public")
+   : RENDERER_DIST;

let win: BrowserWindow | null;
const createWindow = () => {
  win = new BrowserWindow();

  if (process.env.VITE_DEV_SERVER_URL) {
    win.loadURL(process.env.VITE_DEV_SERVER_URL);
- }
+ } else {
+   win.loadFile(path.join(process.env.VITE_PUBLIC!, "index.html"));
+ }
};

app.on("window-all-closed", () => {
  if (process.platform !== "darwin") {
    app.quit();
    win = null;
  }
});

app.on("activate", () => {
  if (BrowserWindow.getAllWindows().length === 0) {
    createWindow();
  }
});

app.whenReady().then(() => {
  createWindow();
});

package.jsonを編集する

versionプロパティの追加typeプロパティの削除ビルド用コマンドを追加の3つを行います。
すべての変更を反映したpackage.jsonは以下になります。

package.json
{
  "name": "nuxt-app",
  "private": true,
- "type": "module",
  "main": "dist-electron/main.js",
+ "version": "0.0.0",
  "scripts": {
    "build": "nuxt build",
    "dev": "nuxt dev",
+   "build:win": "nuxt build --prerender && electron-builder --win",
+   "build:mac": "nuxt build --prerender && electron-builder --mac",
    "generate": "nuxt generate",
    "preview": "nuxt preview",
    "postinstall": "nuxt prepare"
  },
  "dependencies": {
    "nuxt": "3.6.5",
    "vue": "^3.4.27",
    "vue-router": "^4.3.2"
  },
  "devDependencies": {
    "electron": "^30.0.9",
    "electron-builder": "^25.0.0-alpha.9",
    "nuxt-electron": "^0.7.0",
    "vite-plugin-electron": "^0.28.7",
    "vite-plugin-electron-renderer": "^0.14.5"
  }
}
詳しい説明

1. versionプロパティの追加

バージョンを指定しないとelectron-builderでのビルド時にエラーが発生し、ビルドが完了できません。

⨯ Please specify 'version' in the package.json (D:\Zenn\playground\version-test\package.json)
package.json
{
  "name": "nuxt-app",
  "private": true,
  "type": "module",
  "main": "dist-electron/main.js",
+ "version": "0.0.0",
  "scripts": {
    "build": "nuxt build",
    "dev": "nuxt dev",
    "generate": "nuxt generate",
    "preview": "nuxt preview",
    "postinstall": "nuxt prepare"
  },
  "dependencies": {
    "nuxt": "3.6.5",
    "vue": "^3.4.27",
    "vue-router": "^4.3.2"
  },
  "devDependencies": {
    "electron": "^30.0.9",
    "electron-builder": "^25.0.0-alpha.9",
    "nuxt-electron": "^0.7.0",
    "vite-plugin-electron": "^0.28.7",
    "vite-plugin-electron-renderer": "^0.14.5"
  }
}

2. typeプロパティの削除

ES Moduleを利用したままビルドを行うと、ビルドしたアプリケーション実行時にエラーが発生してしまいます。

A JavaScript error occurred in the main process

Uncaught Exception:
ReferenceError: __dirname is not defined in ES module scope
This file is being treated as an ES module because it has a '.js' file extension and
'D:\Zenn\playground\module-test\release\0.0.1\win-unpacked\resources\app.asar...\package.json'
contains "type": "module". To treat it as a CommonJS script, rename it to use the '.cjs' file
extension.
  at file:///D:/Zenn/playground/module-test/release/0.0.1/win-unpacked/resources/app.asar...:31
  at ModuleJob.run (node:internal/modules/esm/module_job:222:25)
  at async ModuleLoader.import (node:internal/modules/esm/loader:316:24)
  at async node:electron/js2c/browser_init:2:120652
  at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:138:5)

回避するため、typeプロパティを削除します。

package.json
{
  "name": "nuxt-app",
  "private": true,
- "type": "module",
  "main": "dist-electron/main.js",
  "version": "0.0.0",
  "scripts": {
    "build": "nuxt build",
    "dev": "nuxt dev",
    "generate": "nuxt generate",
    "preview": "nuxt preview",
    "postinstall": "nuxt prepare"
  },
  "dependencies": {
    "nuxt": "3.6.5",
    "vue": "^3.4.27",
    "vue-router": "^4.3.2"
  },
  "devDependencies": {
    "electron": "^30.0.9",
    "electron-builder": "^25.0.0-alpha.9",
    "nuxt-electron": "^0.7.0",
    "vite-plugin-electron": "^0.28.7",
    "vite-plugin-electron-renderer": "^0.14.5"
  }
}

3. ビルド用コマンドを追加

Electronアプリケーションをビルドするためのスクリプトコマンドを追加します。
Nuxtアプリケーションをビルドした後、electron-builderを実行します。

package.json
{
  "name": "nuxt-app",
  "private": true,
  "type": "module",
  "main": "dist-electron/main.js",
  "version": "0.0.0",
  "scripts": {
    "build": "nuxt build",
    "dev": "nuxt dev",
+   "build:win": "nuxt build --prerender && electron-builder --win",
+   "build:mac": "nuxt build --prerender && electron-builder --mac",
    "generate": "nuxt generate",
    "preview": "nuxt preview",
    "postinstall": "nuxt prepare"
  },
  "dependencies": {
    "nuxt": "3.6.5",
    "vue": "^3.4.27",
    "vue-router": "^4.3.2"
  },
  "devDependencies": {
    "electron": "^30.0.9",
    "electron-builder": "^25.0.0-alpha.9",
    "nuxt-electron": "^0.7.0",
    "vite-plugin-electron": "^0.28.7",
    "vite-plugin-electron-renderer": "^0.14.5"
  }
}

そのほかのビルドオプションは以下を参照してください。
https://www.electron.build/cli

electron-builder.json5を作成し、ビルド設定を記述する

electron-builder.json5を作成し、ビルド後のアプリケーションの状態についての設定を記述します。
nuxt-electronのquick-startを踏襲し、json5形式を採用しています。
そのほかに利用可能なフォーマットは、electron-builder > Common Configurationを参照してください。

electron-builder.json5
// @see - https://www.electron.build/configuration/configuration
{
  "$schema": "https://raw.githubusercontent.com/electron-userland/electron-builder/master/packages/app-builder-lib/scheme.json",
  "appId": "your.app.id",
  "directories": {
    "output": "release/${version}"
  },
  "files": [
    ".output/**/*",
    "dist-electron"
  ],
  "mac": {
    "target": [
      "dmg"
    ]
  },
  "win": {
    "target": [
      {
        "target": "nsis",
        "arch": [
          "x64"
        ]
      }
    ],
  },
}

そのほかの設定については以下のリファレンスを参照してください。
https://www.electron.build/configuration/configuration

もう少し易しいドキュメントとして、electron-viteのドキュメントも参考になります。
https://electron-vite.github.io/build/electron-builder.html

ビルドを実行する

npm run build:winまたはnpm run build:macを実行してビルドを行ってください。
release/{バージョン}フォルダにElectronのビルド成果物が格納されています。

Electronを一時的に無効化するには

ブラウザの画面で動作確認を行いたい場合、nuxt.config.tsのmodulesからnuxt-electronを削除することで無効化できます。
また、併せてelectronプロパティを削除することをお勧めします。
4. electronの型情報をNuxtに追加でも触れたとおり、nuxt-electronがmodulesから削除されることで型情報が変化し、electronプロパティに型エラーが発生するためです。

一時的な無効化の場合、削除に代わりコメントアウトで対応するのが一般的です。
これらを反映すると、nuxt.config.tsは以下のようなコードになります。

nuxt.config.ts
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
  devtools: { enabled: false },
  modules: [
    // "nuxt-electron"
  ],
  // electron: {
  //   build: [
  //     {
  //       entry: "electron/main.ts",
  //     },
  //   ],
  // },
});

modulesプロパティの配列に含まれる要素がnuxt-electronのみの場合はmodulesプロパティごと削除しても問題ありません。

Vue・Nuxt 情報が集まる広場 / Plaza for Vue・Nuxt.

Discussion

れいな@底なし沼の魔女れいな@底なし沼の魔女

nuxt-electronの記事、すごく参考になりました。

実際に試してみて、Windows環境での問題(おそらくWindows11依存)がありました。

electron-builderが最新の25.0.0-alpha.9だとビルド時にEBUSYでエラーになる

https://github.com/electron-userland/electron-builder/issues/8250
issueで上がってますが、globalにインストールすれば動くとコメントにありましたが動きませんでした。なのでバージョンをさせてビルドすると以下の問題発生。

electron-builderを24.13.3のlatestにしてビルドすると今度はdylibのシンボリックリンクが作成できずにエラーになる。

https://github.com/electron-userland/electron-builder/issues/8149

winCodeSignのdylibを解凍時に管理者権限がないとコピーできずにエラーになる問題のためエラーになります。解決方法としては、winCodeSign.7zwinCodeSign-2.6.0を別途ダウンロードして管理者権限で起動した7-zipで解凍してwinCodeSign-2.6.0フォルダを

C:\Users<YourUserName>\AppData\Local\electron-builder\Cache\winCodeSign\

electron-builderのキャッシュフォルダにコピーする方法で起動できました。
参照先

electron-builderがv25の正式版が出れば解消されると思うのですが、nuxt-electronと関係のないところで躓く罠だったので、参考になればと思いコメントさせていただきます。