初めに
実用を目的とはしていません。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
package.json
{ "main": "main.js"
, "devDependencies": { "electron": "^18.0.0-alpha.4" }
}
$ npm i
preload.js
ipcRendererをレンダラプロセスが使えるようにすることによって、メインプロセスにメッセージを送れるようにします。
contextBridgeを使って、レンダラプロセスがメインプロセスからのdataとmenuメッセージを受け取れるようにしています。
dataメッセージはメインプロセスがファイルを開けたらその内容を送ってきます
menuメッセージはメインプロセスが選択されたメニューを送ってきます
preload.js
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を検知できるようにします。
index.html
<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が返されます。
最後に引数でファイルが渡された時の処理をしています。
main.js
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
↧