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

ElectronでToDoアプリを開発する

$
0
0

背景

Electronを用いたアプリ開発の記事を探していたところ、Mediumで@codedrakenさんのBuild a Todo App with Electronという記事に出会いました。
この記事ではToDoアプリ(メモ帳)の実装方法がチュートリアル形式で掲載されており、とても勉強になったので、ぜひこの記事を紹介(和訳)したいと思います。
自分が実装する上でつまづいた点や、気づいた点などを補足しながら、実装の流れをみていきたいと思います。

環境

機種: Mac Book Air 2017
OS : MacOS Mojave ver 10.14.5
node.js : v12.13.0
npm : 6.13.4
electron : 8.1.1

開発する上での前提条件

PCにNode.jsとnpmがインストール済み

読者に要求する前提知識

javascriptに関する基礎知識
Electronに関する基礎知識
(公式サイトのサンプルコードを実行できれば十分です)

作るもの

@codedrakenさんによるgithubのコードはこちら
1_mv-mAe1RuqgdfzuE-fGH2g.gif

アプリはメインウィンドウ(Todos)とサブウィンドウ(Add Todo)の2窓構成で、Todoリストを作成・追加および削除することができます。また、Todoリストはjson形式のファイルとして保存されるため、アプリを終了後も情報が保持されます。

<操作内容>
メインウィンドウで「Create a new Todo」ボタンを押すと、サブウィンドウが開き、ToDoリストを入力できるようになります。
サブウィンドウのフォームから文字列を入力後、Enterキーを押下あるいは「Add Todo」ボタンを押すと、メインウィンドウのリストに入力内容が付加されます。
なお、メインウィンドウのリストの内容は、クリックすることで削除されます。

内部構成

Electronのプロセスは、MainプロセスとRendererプロセスの2つに大別されます。Rendererプロセスは個々の「画面」を制御するもので、今回の場合は「Todos」と「Add Todo」の2つの画面がそれぞれRendererプロセスにより制御されています。MainプロセスはRendererプロセスの上位に存在し、個々のRendererプロセスのライフサイクルを制御します。
image.png

また、ElectronではRendererプロセスどうしの通信ができないため、Rendererプロセス間の通信は、必ずMainプロセスを介する必要があります(プロセスどうしが干渉したり、デッドロックを引き起こしたりするのを防ぐため)。
今回の例では、メインウィンドウ「Todos」からサブウィンドウ「Add Todos」を呼び出したり、サブウィンドウのフォーム入力によってメインウィンドウのリスト内容を変更したりします。これらのやりとりは、すべてMainプロセスを介することになります。

フォルダ構成

image.png

プロジェクトフォルダの直下(ルートディレクトリ)にメインプロセスのコード(main.js)をおき、rendererプロセスのコード一式はrendererフォルダ下にまとめます。なお、index.htmlとindex.jsでメインウィンドウを実装し、add.htmlとadd.jsでサブウィンドウを実装します。また、Window.jsとDataStore.jsで、それぞれメインプロセスで用いるクラスを記述します。

プロジェクトフォルダの作成

プロジェクトを進めるにあたって、プロジェクトフォルダを作成しましょう。場所は、desktopなど、好きなところで大丈夫です。フォルダの名前はelectron-todoにします。
Terminalを起動し、以下を実行します。

$ mkdir electron-todo
$ cd electron-todo

次に、作成したelectron-todoフォルダをnpmでイニシャライズし、package.jsonファイルを作成します。

$ npm init -y

npm initを実行したら、まずはelectron-todoフォルダ内にpackage.jsonファイルが作成されていることを確認します。

package.json
{"name":"electron-todo","version":"1.0.0","description":"","main":"index.js","scripts":{"test":"echo \"Error: no test specified\"&& exit 1"},"keywords":[],"author":"","license":"ISC"}

作成されたpackage.jsonファイルの"main"と"scripts"の項目について、以下のように修正しましょう。この修正により、terminal上からelectronを実行するとき、main.jsファイルがエントリーポイントとなります。

package.json
{"name":"electron-todo","version":"1.0.0","description":"todo app","main":"main.js","scripts":{"start":"electron .","test":"echo \"Error: no test specified\"&& exit 1"},"keywords":[],"author":"","license":"ISC",}

最後に、あとでcodingする予定の、html,css,jsファイルをそれぞれ作成しておきましょう。

$ touch main.js Window.js DataStore.js
$ mkdir renderer
$ touch renderer/index.html renderer/index.js
$ touch renderer/app.html renderer/app.js
$ touch renderer/style.css

必要なパッケージの追加

npmを用いて、必要なパッケージをインストールしていきます。
今回インストールするパッケージは以下のとおりです。
Electron
electron-reload
electron-store
spectre.css

electron-reloadは、renderer画面のホットリロードを実現します。また、electron-storeは内部データをjson形式で保持するために必要です。そして、spectre.cssは、htmlを簡単にイケてるデザインにしてくれるパッケージです

terminalで以下のコードを実行します。

$ npm i electron
$ npm i electron-reload
$ npm i electron-store
$ npm i spectre.css

これで下準備は完了しました。あとはガリガリコードを書いていきます。

メインウィンドウを表示する

まずはメインウィンドウが表示されるところまで実装します。
メインウィンドウを描画するために必要な、index.html、index.js、style.cssを次のように記述します。

index.html
<!DOCTYPE html><htmllang="en"><head><metacharset="UTF-8"/><title>Todo</title><linkrel="stylesheet"href="../node_modules/spectre.css/dist/spectre.min.css"/><linkrel="stylesheet"href="style.css"/></head><body><divclass="container"><divclass="columns"><divclass="column col-10"><h1class="text-center">Todos</h1><buttonid="createTodoBtn"type="button"class="btn">
                        Create a New Todo
                    </button><ulid="todoList"></ul></div></div></div><script src="./index.js"></script></body></html>
index.js
// 後で実装する機能// 1. createTodoBtnボタンが押されたら、そのことをMainプロセスに知らせる// 2. 「todoリストを更新せよ」という命令をMainプロセスから受け取り、実行する
style.css
body{font:caption;}.todo-item{background:none;padding:0.5rem;margin:0;cursor:pointer;font-size:1rem;}.todo-item:nth-child(even){background:#f4f4ff;}.todo-item:hover{background:#d8d8d8;}

次に、メインプロセスを制御するためのmain.jsを以下のように記述します。

main.js
const{app,BrowserWindow}=require('electron');letmainWindow=null;app.on('ready',()=>{mainWindow=newBrowserWindow({webPreferences:{nodeIntegration:true,},});mainWindow.loadFile('./renderer/index.html');mainWindow.on('closed',()=>{mainWindow=null;});});app.on('window-all-closed',()=>{app.quit();});

これでメインウィンドウを立ち上げることができます。
それでは、一度この状態でterminalからelectronを起動してみましょう。

$ npm start

現時点では、index.jsには何も記述していない上に、main.jsも必要最低限の記述しかしていないため、メインウィンドウのボタンを押しても何も反応がありません。
スクリーンショット 2020-03-12 16.29.55.png

main.jsの中で、webPreferencesとしてnodeIntegration: trueとしたことに注意してください。これは、rendererプロセス中でnode.jsの機能を使えるようにする設定であり、あとでindex.jsおよびadd.jsをコードするときに必要となります。ところが、Electronの公式ではこの設定を一般的には推奨していません。Local環境で用いるアプリの場合は問題ありませんが、webを介するアプリを作成する場合には、XSSなどのセキュリティリスクを生むためです。
もし今後Electronを用いたアプリを作成される場合は、公式ドキュメントに目を通すようにしてください。

Window.jsファイルを編集する

上の例では、画面を実装するにあたってBrowserWindowクラスをそのまま用いました。以降では複数の画面を設定することになるので、BrowserWindowクラスを継承したWindowクラスを新たに定義します。Window.jsには、先ほど述べたnodeIntegrationの設定や、ファイル読み込みの設定などを記述します。これにより、main.jsに記述するコード行を減らすことができます。

Window.js
const{BrowserWindow}=require('electron');// default window settingsconstdefaultProps={width:500,height:800,show:false,webPreferences:{nodeIntegration:true,},};classWindowextendsBrowserWindow{constructor({file,...windowSettings}){super({...defaultProps,...windowSettings});this.loadFile(file);this.once('ready-to-show',()=>{this.show();});}}module.exports=Window;

メインウィンドウからサブウィンドウを呼び出す

次のステップとして、main.jsとindex.htmlを編集し、メインウィンドウのボタンを押すとサブウィンドウが表示されるようにしましょう。まずは、サブウィンドウを描画するのに必要なadd.htmlとadd.jsファイルについてcodingします。

add.html
<!DOCTYPE html><htmllang="en"><head><metacharset="UTF-8"/><title>Add Todo</title><linkrel="stylesheet"href="../node_modules/spectre.css/dist/spectre.min.css"/><linkrel="stylesheet"href="style.css"/></head><body><divclass="container"><divclass="columns"><divclass="column col-10"><h1class="text-center">Add Todo</h1><formid="todoForm"><divclass="form-group"><labelclass="form-label"for="add-input">Todo</label><inputclass="form-input"type="text"name="add-input"placeholder="I have to..."/></div><buttonclass="btn">Add Todo</button></form></div></div></div><script src="./add.js"></script></body></html>
add.js
// 後で実装する機能// フォームに内容が入力されたらMainプロセスに通知する

サブウィンドウの設定が完了したので、メインウィンドウの設定を行います。メインウィンドウのボタンが押された場合、このことをMainプロセスに通知する内容をindex.jsに記述しましょう。
RendererプロセスからMainプロセスへの通信を行うときは、Electronに内蔵されているipcRendererというモジュールを利用します。

index.js
const{ipcRenderer}=require('electron');// createTodoボタンが押されたら、そのことをMainプロセスに伝えるdocument.getElementById('createTodoBtn').addEventListener('click',()=>{ipcRenderer.send('add-todo-window');});

ipcRendererによってMainプロセスに通知が送られるようになりました。この通知は'add-todo-window'というチャンネルに送られるようになります。
では次に、Mainプロセスがこの通知を受け取れるように、main.jsを修正しましょう。
MainプロセスからRendererプロセスへの通知を行うときや、Rendererプロセスからの通知をMainプロセスが受け取る際には、ipcMainというモジュールを利用します。

main.js
constpath=require('path');const{app,ipcMain}=require('electron');constWindow=require('./Window');app.on('ready',()=>{letmainWindow=newWindow({file:path.join('renderer','index.html'),});// add todo windowletaddTodoWin;// create add todo windowipcMain.on('add-todo-window',()=>{if(!addTodoWin){addTodoWin=newWindow({file:path.join('renderer','add.html'),width:400,height:400,parent:mainWindow,});addTodoWin.on('closed',()=>{addTodoWin=null;});}});});app.on('window-all-closed',()=>{app.quit();});

それでは、一度この状態でterminalからelectronを起動してみましょう。

$ npm start

「Create a New Todo」ボタンを押すと、あらたに「Add Todo」ウィンドウが表示されるようになりました。

スクリーンショット 2020-03-13 10.33.20.png

現時点ではAdd Todoウィンドウのボタンについては何も設定していないため、ボタンを押しても何も反応はありません。

DataStore.jsファイルを編集する

今回作成するアプリでは、Todoリストをjson形式のファイルとして保持できるようにします。そのために、electron-storeモジュールを利用します。electron-storeはデータを保存するためのクラスとしてStoreクラスを用意していますが、今回はStoreクラスを継承したDataStoreクラスをあらたに定義します。

DataStore.js
constStore=require('electron-store');classDataStoreextendsStore{constructor(settings){super(settings);// initialize with todos or empty arraythis.todos=this.get('todos')||[];}saveTodos(){// save todos to JSON filethis.set('todos',this.todos);// returning 'this' allows method chainingreturnthis;}getTodos(){// set object's todos to todos in JSON filethis.todos=this.get('todos')||[];returnthis;}addTodo(todo){// merge the existing todos with the new todothis.todos=[...this.todos,todo];returnthis.saveTodos();}deleteTodo(todo){// filter out the target todothis.todos=this.todos.filter(t=>t!==todo);returnthis.saveTodos();}}module.exports=DataStore;

公式にある通り、jsonファイルはapp.getPath('userData')に保存されます。app.getPath('userData')の具体的なパスについては、こちらを参照してください

サブウィンドウの操作をメインウィンドウに反映させる

最後のステップになりました。あと実装しなければならない機能は、サブウィンドウのフォームに入力された内容をMainプロセスに通知し、その内容をDataStoreに保存すること、および、DataStoreの内容をメインウィンドウに反映させることです。

まずは、サブウィンドウのフォームに入力した内容を、Mainプロセスに通知する機能を実装しましょう。今回も、ipcRendererモジュールを利用するように、add.jsファイルに記述します。また、この通知は、'add-todo'チャネルとします。

add.js
const{ipcRenderer}=require('electron');document.getElementById('todoForm').addEventListener('submit',evt=>{// prevent default refresh functionality of formsevt.preventDefault();// get input on the formconstinput=evt.target[0];// send input.value to main proecssipcRenderer.send('add-todo',input.value);// reset inputinput.value='';});

次に、main.jsを編集します。以下では、DataStoreインスタンスを作成して、その中にtodoリストを保存できるようにします。また、Rendererプロセスからの通知に対する応答に対しても記述しています。

main.js
constpath=require('path');const{app,ipcMain}=require('electron');constWindow=require('./Window');constDatastore=require('./DataStore');// ホットリロード機能を有効化require('electron-reload')(__dirname);// create a new todo store name "Todos Main"consttodosData=newDatastore({name:'Todos Main'});app.on('ready',()=>{letmainWindow=newWindow({file:path.join('renderer','index.html'),});// add todo windowletaddTodoWin;// initialize with todosmainWindow.once('show',()=>{mainWindow.webContents.send('todos',todosData.todos);});// create add todo windowipcMain.on('add-todo-window',()=>{if(!addTodoWin){addTodoWin=newWindow({file:path.join('renderer','add.html'),width:400,height:400,parent:mainWindow,});addTodoWin.on('closed',()=>{addTodoWin=null;});}});ipcMain.on('add-todo',(event,todo)=>{constupdatedTodos=todosData.addTodo(todo).todos;mainWindow.send('todos',updatedTodos);});ipcMain.on('delete-todo',(event,todo)=>{constupdatedTodos=todosData.deleteTodo(todo).todos;mainWindow.send('todos',updatedTodos);});});app.on('window-all-closed',()=>{app.quit();});

最後に、メインウィンドウにtodoリストの内容が反映されるように、index.jsファイルを編集します。

index.js
const{ipcRenderer}=require('electron');// create add todo window buttondocument.getElementById('createTodoBtn').addEventListener('click',()=>{ipcRenderer.send('add-todo-window');});// delete todo by its text valueconstdeleteTodo=e=>{ipcRenderer.send('delete-todo',e.target.textContent);};// on receive todosipcRenderer.on('todos',(event,todos)=>{// get todoListconsttodoList=document.getElementById('todoList');// create html stringconsttodoItems=todos.reduce((html,todo)=>{html+=`<li class="todo-item">${todo}</li>`;returnhtml;},'');// set list html to the todo itemstodoList.innerHTML=todoItems;// add click handlers to delete the clicked todotodoList.querySelectorAll('.todo-item').forEach(item=>{item.addEventListener('click',deleteTodo);});});

これで一通りのcodingが終了しました。terminalからアプリを起動してみます。

$ npm start

1_mv-mAe1RuqgdfzuE-fGH2g.gif
問題なく動作しました。Electronの基本的な機能を学ぶには、とても良いサンプルアプリだと思います。

参考文献など

元の記事:Build a Todo App with Electron
ElectronのnodeIntegrationについて:【Electron】nodeIntegration: falseのまま、RendererプロセスでElectronのモジュールを使用する
Electronのipc通信について:【Electron連載】第4回 基本編-メイン/レンダラープロセスの話

おすすめの書籍: Electron in Action


Viewing all articles
Browse latest Browse all 8883

Trending Articles