ElectronとNuxt.jsでテキストファイルの読み書きを行う
あらすじ
Electronでテキストファイルの読み書きを行う方法を調べたのでNuxt.jsでテキストエディタ的なものをさっくり作れるかと思いきやそうでもなかった🤔
環境
electron 11.0.2
nuxt 2.14.7
前に調べたときよりもElectronのメジャーバージョンが上がっているが、ひとまずスルー。
Nuxt.jsのセットアップ
普通にWebページを作るようにプロジェクトを作成する。
npx create-nuxt-app sugarnote
create-nuxt-appのログ。特記事項はSPAモードを選択しているぐらい。
create-nuxt-app v2.14.0
✨ Generating Nuxt.js project in sugarnote
? Project name sugarnote
? Project description My majestic Nuxt.js project
? Author name sato_tcs
? Choose programming language JavaScript
? Choose the package manager Yarn
? Choose UI framework Tailwind CSS
? Choose custom server framework None (Recommended)
? Choose Nuxt.js modules (Press <space> to select, <a> to toggle al
l, <i> to invert selection)
? Choose linting tools Prettier
? Choose test framework None
? Choose rendering mode Single Page App
? Choose development tools (Press <space> to select, <a> to toggle
all, <i> to invert selection)
コマンド実行後に画面に表示された方法でNuxt.jsのプロジェクトを実行する。
cd sugarnote
yarn dev
SPAモード選択の理由
productionとして実行するときにサーバを動かしたくなかったためSSRモードは除外。残りはSPAモードとSSGモードになるが、URLからパラメータを取得するとしたらSPAの方が都合が良さそうなのでそちらに(paramsじゃなくてqueryで済ますならSSGでも良い?🤔)。
Electronからページを表示
ElectronでNuxt.jsのページを見れるようにする。
メインプロセスのファイル
ElectronとExpressを追加する(Expressは開発中にメインプロセスからNuxt.jsのプロジェクトを動かすためのもの)。
yarn add --dev electron express
main.js
を作成する。
main.js(全コード)
const { loadNuxt, build } = require('nuxt')
const { app: electronApp, BrowserWindow } = require('electron')
const path = require('path')
const expressApp = require('express')()
const isDev = process.env.NODE_ENV !== 'production'
const port = process.env.PORT || 3000
function createWindow() {
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: true
}
})
const url = isDev
? `http://localhost:${port}/`
: `file://${path.resolve(__dirname, '..', 'dist', 'index.html')}`
win.loadURL(url)
win.webContents.openDevTools()
}
async function start() {
if (isDev) {
const nuxt = await loadNuxt(isDev ? 'dev' : 'start')
expressApp.use(nuxt.render)
build(nuxt)
expressApp.listen(port, '0.0.0.0')
console.log(`Server listening on \`localhost:${port}\``)
}
electronApp.whenReady().then(createWindow)
electronApp.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
electronApp.quit()
}
})
electronApp.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow()
}
})
}
start()
メインプロセスからNuxt.jsを実行する
開発中はlocalhostで動いているものを参照し、productionではビルド後のファイルを参照する。
developmentのときNuxt.jsを動かす
if (isDev) {
const nuxt = await loadNuxt(isDev ? 'dev' : 'start')
expressApp.use(nuxt.render)
build(nuxt)
expressApp.listen(port, '0.0.0.0')
console.log(`Server listening on \`localhost:${port}\``)
}
Electronから参照するURL
const url = isDev
? `http://localhost:${port}/`
: `file://${path.resolve(__dirname, '..', 'dist', 'index.html')}`
Nuxt.jsの設定ファイル
nuxt.config.js
を編集する。
nuxt.config.js(全コード)
export default {
mode: 'spa',
/*
** Headers of the page
*/
head: {
title: process.env.npm_package_name || '',
meta: [
{ charset: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
{
hid: 'description',
name: 'description',
content: process.env.npm_package_description || ''
}
],
link: [{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }]
},
/*
** Customize the progress-bar color
*/
loading: { color: '#fff' },
/*
** Global CSS
*/
css: [],
/*
** Plugins to load before mounting the App
*/
plugins: [],
/*
** Nuxt.js dev-modules
*/
buildModules: [
// Doc: https://github.com/nuxt-community/nuxt-tailwindcss
'@nuxtjs/tailwindcss'
],
/*
** Nuxt.js modules
*/
modules: [],
/*
** Build configuration
*/
build: {
/*
** You can extend webpack config here
*/
extend(config, ctx) {
config.output.publicPath = '_nuxt/'
}
},
/*
**
*/
router: {
mode: 'hash'
}
}
routerの設定
routerのmodeを指定しないと、おそらくhistoryモードになっている。今回はURLを見せにいかない想定なのでhashモードでSPAを動かす。
export default {
// ...
router: {
mode: 'hash'
}
}
publicPathの設定
file://
からソースを参照したときに絶対パスだとファイルが見つからなそうなので修正する。
export default {
// ...
build: {
extend(config, ctx) {
config.output.publicPath = '_nuxt/'
}
}
}
本来の用途としては、CDNを使用するときに書き換えるらしい🙄(参考)
動作確認
開発用のコマンド(ポート番号はあってもなくても可)。
env PORT=3333 yarn electron .
本番環境では、ビルドを行ってから環境を指定してelectron .
を実行する。
yarn build
env NODE_ENV=production yarn electron .
ファイルの読み書きの実装
前に調べたものを参考にファイルの読み書きを実装してみたところ、Electronの画面上にエラーが表示された。
ERROR in ./node_modules/electron/index.js
Module not found: Error: Can't resolve 'fs' in '/Users/hogehoge/sugarnote/node_modules/electron'
「まあそこ参照してたら見つからないよなー🤔」という内容のエラーが出る。
Webページを検索してみたところ、「fsがない」というエラーはWebpackの使用中に割と起こる模様。原因はWebpackで行うコンパイルがデフォルトでwebをターゲットにしているためであるためらしい(webがターゲットだと、nodeの機能であるfsモジュールが使えない)。
検索で見つかる対処法が直感的に理解できなかったので(empty
とか付けるやつ)、さらに調べるとcontextBridge
について書かれている記事を見つけた。
ざっくりいうと、
- レンダラープロセスではセキュリティ上の理由でnodeモジュールを使用しない方がよい
- レンダラープロセスでnodeモジュールを使用したい場合はレンダラープロセスからメインプロセスに要求して、メインプロセスから結果を返してもらう(レンダラープロセスで
require('electron').remote
を使用しない)
といった感じになる。
元々フロント側ででfsを読み込んでいるのに違和感があったので修正を始める。
main.js
(抜粋)
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
preload: path.resolve(__dirname, 'preload.js')
}
})
preload.js
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('electron', {
open: () => ipcRenderer.invoke('open'),
save: data => ipcRenderer.invoke('save', data)
})
pages/index.js
素のJavaScriptからVue.jsに書き換えた。
pages/index.js(全コード)
<template>
<div>
<div>
<button type="button" @click="onOpen">Open</button>
<button type="button" @click="onSave">Save</button>
</div>
<div>
<textarea v-model="text"></textarea>
</div>
</div>
</template>
<script>
export default {
data: () => ({
text: ''
}),
mounted() {
},
methods: {
async onOpen(event) {
const { canceled, data } = await electron.open()
if (canceled) return
this.text = data[0] || ''
},
async onSave(event) {
await electron.save(this.text)
}
}
}
</script>
<style scoped>
button {
@apply px-2 rounded bg-blue-600 text-white;
}
textarea {
@apply border border-gray-400 bg-white;
}
</style>
参考
- Context Isolation | Electron
- contextBridge | Electron
- Target | webpack
- Nuxt.js+Electronを試してみるv2 - Qiita
- Electron で nodeIntegration: false にする方法 - Qiita
- Electron Uncaught ReferenceError: require is not defined発生時の対処法 | mebee
- 【Electron】nodeIntegration: falseのまま、RendererプロセスでElectronのモジュールを使用する - Qiita
- ElectronでcontextBridgeによる安全なIPC通信 - Qiita
- [Vue.js] vue-routerのhashモードとhistoryモードの違いをざっくり理解する - Qiita
あとがき
fs見つからない問題と遭遇してから開発を優先したため、「あーこれどういう流れで解決したっけ?🤔」と思い出せなくて雰囲気で書いたので調査の過程が実際と若干異なるかも知れない🙄
Discussion