初めに
実用を目的とはしていません。Electron
の構造を理解するために、どれだけ骨格を抜き出せるかを目的にしています。
というわけでスクラッチから書いてみます。
html
中にstyle
とjavascript
を直書きしているので、以下のワーニングが出ます。
Electron Security Warning (Insecure Content-Security-Policy)
以下のレポジトリにワーニングの出ない、他の機能も入ったバージョンを置いておきます。
ワーニングの消し方や結果だけを知りたい方はこちらで!
https://github.com/Satachito/electron-quick-start-MOD
npm
,npx
は入っている前提です。(node
が入っていれば通常入っています)
準備
フォルダを作ってその中にpackage.json
を作ってnpm
でElectron
をインストールします。
$ mkdir TE
$ cd TE
{ "main": "main.js"
, "devDependencies": { "electron": "^18.0.0-alpha.4" }
}
$ npm i
preload.js
ipcRenderer
をレンダラプロセスが使えるようにすることによって、メインプロセスにメッセージを送れるようにします。
contextBridge
を使って、レンダラプロセスがメインプロセスからのdata
とmenu
メッセージを受け取れるようにしています。
-
data
メッセージはメインプロセスがファイルを開けたらその内容を送ってきます -
menu
メッセージはメインプロセスが選択されたメニューを送ってきます
const { contextBridge, ipcRenderer } = require( 'electron' )
contextBridge.exposeInMainWorld(
'ipcRenderer'
, ipcRenderer
)
contextBridge.exposeInMainWorld(
'onData'
, $ => ipcRenderer.on( 'data', $ )
)
contextBridge.exposeInMainWorld(
'onMenu'
, $ => ipcRenderer.on( 'menu', $ )
)
index.html
index.html
に編集用のtextarea
とレンダラプロセスを記述します。
preload.js
で見えるようになったonMenu
はSave
とSaveAs
メッセージに反応し、編集中のデータ(TextArea.value
)をメインプロセスに送ります。メインプロセスは保存したらファイル名を、しなかったらundefined
を戻します。
onData
はメインプロセスから送られてきたファイルの内容をtextarea
にセットし、ファイル名をドキュメントタイトルにセットします。
ウインドウが閉じられる時、元のデータと編集中のデータが違っていたら、ev.returnValue
に何かセットして、メインプロセスでBrowserWindow
のwebContents
がwill-prevent-unload
を検知できるようにします。
<textarea id=TextArea style="resize: none; width: 100%; height: 100%"></textarea>
<script type=module>
let
prevData = TextArea.value
onData(
( ev, $, file ) => (
prevData = $
, TextArea.value = $
, document.title = file
)
)
onMenu(
( ev, $ ) => {
const
Save = _ => ipcRenderer.invoke( _, TextArea.value ).then(
file => file && (
prevData = TextArea.value
, document.title = file
)
)
switch ( $ ) {
case 'Save':
Save( 'save' )
break
case 'SaveAs':
Save( 'saveAs' )
break
}
}
)
onbeforeunload = ev => TextArea.value != prevData
? ev.returnValue = '' // for Chrome/Electron
: undefined
</script>
main.js
メインプロセスです。
CreateWindow
で新しいBrowserWindow
を開きます。
file
名が引数で渡されたら、BrowserWindow
のwebContents
がdid-finish-load
を検知したらレンダラプロセスにファイルの内容を送ります。
利用者が編集済みのウインドウを閉じようとしたとき、webContents
はwill-prevent-unload
を検知します。編集を継続するか破棄するかを選ばせます。
ファイルメニューに New
,Open
,Save
,SaveAs
を付け加えます。
- 利用者が
New
を選んだらCreateWindow
を呼びます。 - 利用者が
Open
を選んだらopenDialog
を出してfile
が選ばれたらそのfile
名を引数にCreateWindow
を呼びます。 - 利用者が
Save
を選んだらレンダラプロセスにmenu
,save
と送ります。レンダラプロセスはonMenu
でこれを検知すると、メインプロセスにSave
でデータを送ってきます。無事保存したら保存したfile
名をレンダラプロセスに返します。保存しなかったらundefine
が返されます。 - 利用者が
SaveAs
を選んだらレンダラプロセスにmenu
,saveAs
と送ります。レンダラプロセスはonMenu
でこれを検知すると、メインプロセスにSaveAs
でデータを送ってきます。無事保存したら保存したfile
名をレンダラプロセスに返します。保存しなかったらundefine
が返されます。
最後に引数でファイルが渡された時の処理をしています。
const
{ app, BrowserWindow, Menu, MenuItem, ipcMain, dialog } = require( 'electron' )
const
Send = ( ...$ ) => {
const _ = BrowserWindow.getFocusedWindow()
_ && _.send( ...$ )
}
const
CreateWindow = file => {
const
$ = new BrowserWindow(
{ width : 1600
, height : 800
, webPreferences : {
preload : require( 'path' ).join( __dirname, 'preload.js' )
}
}
)
$.loadFile( 'index.html' )
file && (
$.webContents.on(
'did-finish-load'
, () => $.send( 'data', require( 'fs' ).readFileSync( file, 'utf8' ), file )
)
, $.webContents.file = file
)
$.webContents.on(
'will-prevent-unload'
, ev => dialog.showMessageBoxSync(
$
, { type: 'question'
, buttons: [ 'Discard change and close', 'No' ]
, message: 'Do you really want to close this window?\nChanges you made may not be saved.'
}
) === 0 && ev.preventDefault()
)
}
const
SaveAs = ( ev, $ ) => {
const file = dialog.showSaveDialogSync(
{ properties : [ 'openFile', 'openDirectory' ]
, defaultPath : ev.sender.file
}
)
file && (
require( 'fs' ).writeFileSync( file, $ )
, ev.sender.file = file
)
return file
}
ipcMain.handle( 'saveAs', SaveAs )
ipcMain.handle(
'save'
, ( ev, $ ) => {
const file = ev.sender.file
return file
? ( require( 'fs' ).writeFileSync( file, $ )
, file
)
: SaveAs( ev, $ )
}
)
const
isMac = process.platform === 'darwin'
app.on(
'window-all-closed'
, () => isMac || app.quit()
)
app.on(
'activate'
, ( event, hasVisibleWindows ) => hasVisibleWindows || CreateWindow()
)
app.on(
'open-file'
, ( ev, _ ) => (
ev.preventDefault()
, CreateWindow( _ )
)
)
app.whenReady().then(
() => {
const
menu = Menu.getApplicationMenu()
const
fileMenu = menu.items.find( $ => $.role === 'filemenu' ).submenu
fileMenu.insert(
0
, new MenuItem(
{ label : 'Save as...'
, click : ev => Send( 'menu', 'SaveAs' )
}
)
)
fileMenu.insert(
0
, new MenuItem(
{ label : 'Save'
, accelerator : 'CmdOrCtrl+S'
, click : ev => Send( 'menu', 'Save' )
}
)
)
fileMenu.insert( 0, new MenuItem( { type: 'separator' } ) )
fileMenu.insert(
0
, new MenuItem(
{ label : 'Open...'
, accelerator : 'CmdOrCtrl+O'
, click : ev => {
const _ = dialog.showOpenDialogSync( { properties: [ 'openFile', 'openDirectory' ] } )
_ && _.forEach( $ => CreateWindow( $ ) )
}
}
)
)
fileMenu.insert(
0
, new MenuItem(
{ label : 'New'
, accelerator : 'CmdOrCtrl+N'
, click : ev => CreateWindow()
}
)
)
Menu.setApplicationMenu( menu )
const _ = process.argv.slice(
isMac
? process.argv[ 0 ].split( '/' ).pop() === 'Electron' ? 2 : 1
: process.argv[ 0 ].split( '\\' ).pop() === 'electron.exe' ? 2 : 1
)
_.length
? _.forEach( _ => CreateWindow( _ ) )
: CreateWindow()
}
)
実行
$ npx electron .
最後に
パック前で以下の感じです。目的は果たせた気がします。
% wc *.js *.html package.json
136 446 2852 main.js
13 34 289 preload.js
34 102 640 index.html
3 10 77 package.json
186 592 3858 total