Quantcast
Channel: Node.jsタグが付けられた新着記事 - Qiita
Viewing all articles
Browse latest Browse all 9140

Electron 18 を使って200行以内でファイルの入出力ができるようなテキストエディタを作ってみる。

$
0
0
初めに 実用を目的とはしていません。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

Viewing all articles
Browse latest Browse all 9140

Trending Articles