質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.50%

Q&A

解決済

2回答

945閲覧

Electronのメインプロセスで非同期処理なallメソッドでデータを取得してレンダラープロセスに返す方法

BeatStar

総合スコア4958

0グッド

0クリップ

投稿2022/11/03 03:19

編集2022/11/05 20:53

前提

(問題自体に不備があったので質問の意図自体を変更します)

Electron (node.js)初挑戦です。sqlite3を使いたいと思い、npmを使ってインストールしました。
サンプルとしてそのsqlite3を使ってデータベースからselectをした結果を取得しようとしています。

実現したいこと

  • 前提にある問題(非同期であるため処理が前後していて、値がおかしいこと)の解決

 → どのようにして安全にデータをmain.jsからrenderer.jsを渡すか

発生している問題・エラーメッセージ

main.js内のipcMain.handleメソッド内でsqlite3を使う処理をして、renderer.jsでinvokeして起動します。
ただ、(sqliteからselectするために使う)sqlite3.Database#allメソッド等は非同期らしく、allメソッドの次の処理と前後が逆になることがあり、renderer.jsに戻り値として返されるデータが常に空か{}のような中身のないデータになっています。

「Electron node.js sqlite3」と調べてもよくて、Electronの古いバージョンでのサンプル(renderer.jsに直接requireするとか)か、単にdb.all("select * from users", (error, rows) =>{ console.log( rows ); });のように、「ラムダ式の中でそのまま出力するだけ」となっています。
データを取り出すだけならいいのですが、非同期処理なので値が入ったり入らなかったりするのでそこで頭を悩ませています。

該当のソースコード

JavaScript

1// main.js 2// アプリケーション作成用のモジュールを読み込み 3const { app, BrowserWindow, ipcMain } = require("electron"); 4const path = require("path"); 5 6const sqlite3 = require( "sqlite3" ); 7const db = new sqlite3.Database('test.db'); 8 9// メインウィンドウ 10let mainWindow; 11 12const createWindow = () => { 13 // メインウィンドウを作成します 14 mainWindow = new BrowserWindow({ 15 width: 800, 16 height: 600, 17 webPreferences: { 18 preload: path.join(__dirname, "preload.js"), 19 } 20 }); 21 22 // メインウィンドウに表示するURLを指定します 23 // (今回はmain.jsと同じディレクトリのindex.html) 24 mainWindow.loadFile("index.html"); 25 26 // デベロッパーツールの起動 27 mainWindow.webContents.openDevTools(); 28 29 // メインウィンドウが閉じられたときの処理 30 mainWindow.on("closed", () => { 31 mainWindow = null; 32 // ここで閉じておく 33 db.close(); 34 }); 35 36 ipcMain.handle( "create_table", (_evnet, _arg) => { 37 db.serialize( function(){ 38 // テーブル作成 39 db.run( "create table users (name text, email text, age int)" ); 40 }); 41 return true; 42 }); 43 44 ipcMain.handle( "insert_record", (_evnet, _arg) => { 45 db.serialize( function(){ 46 // INSERT INTO テーブル名 VALUES (値1, 値2,...); 47 db.run( "insert into users values ('太郎', '****', 19)" ); 48 }); 49 return true; 50 }); 51 52 ipcMain.handle( "insert_recordEx", (_evnet, _arg) => { 53 db.serialize( function(){ 54 var stmt = db.prepare("insert into users('name', 'email', 'age') values(?, ?, ?)"); 55 stmt.run( '次郎", '****', 20 ); 56 stmt.finalize(); 57 }); 58 return true; 59 }); 60 61 ipcMain.handle( "select_record", (_evnet, _arg) => { 62 db.serialize( function(){ 63 /* 64 // 一個ずつ取り出す場合 65 db.each("SELECT * FROM users", (err, row) => { 66 console.log( row.name ); 67 }); 68 */ 69 70 /* 71 // 一括で取り出す場合 72 db.all("select * from users", (error, rows) =>{ 73 rows.forEach(row => console.log(row.name)); 74 }); 75 */ 76 77 /* 78 db.all("select * from users", (error, rows) =>{ 79 console.log( rows ); 80 }); 81 */ 82 83 /* 84 // 最初にヒットしたものを取り出す 85 db.get("select * from users", (error, row) =>{ 86 console.log( row ); 87 }); 88 */ 89 90 // HERE! 91 let txt = ""; // ① 92 db.all("select * from users", (error, rows) =>{ 93 rows.forEach(row => { txt += "hello"; }); // ② 94 }); 95 console.log( "select = " + txt ); // ③ 96 }); 97 return false; 98 }); 99}; 100 101// 初期化が完了した時の処理 102app.whenReady().then(() => { 103 createWindow(); 104 105 // アプリケーションがアクティブになった時の処理(Macだと、Dockがクリックされた時) 106 app.on("activate", () => { 107 // メインウィンドウが消えている場合は再度メインウィンドウを作成する 108 if (BrowserWindow.getAllWindows().length === 0) { 109 createWindow(); 110 } 111 }); 112}); 113 114// 全てのウィンドウが閉じたときの処理 115app.on("window-all-closed", () => { 116 // macOSのとき以外はアプリケーションを終了させます 117 if (process.platform !== "darwin") { 118 app.quit(); 119 } 120});

JavaScript

1// renderer.js 2const { contextBridge, ipcRenderer } = require('electron') 3 4contextBridge.exposeInMainWorld( 5 'electronAPI', { 6 setTitle: (title) => ipcRenderer.send('set-title', title), 7 createTable : () => { 8 ipcRenderer.invoke( "create_table", "ok" ).then( (result) => { 9 console.log( result ); 10 }); 11 }, 12 insertRecord : () => { 13 ipcRenderer.invoke( "insert_record", "ok" ).then( (result) => { 14 console.log( result ); 15 }); 16 }, 17 insertRecordEx : () => { 18 ipcRenderer.invoke( "insert_recordEx", "ok" ).then( (result) => { 19 console.log( result ); 20 }); 21 }, 22 selectRecord : () => { 23 ipcRenderer.invoke( "select_record", "ok" ).then( (result) => { 24 console.log( result ); 25 }); 26 } 27})

html

1<!-- index.html --> 2<!DOCTYPE html> 3<html> 4<head> 5 <meta charset="UTF-8" /> 6 <!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP --> 7 <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'" /> 8 <meta http-equiv="X-Content-Security-Policy" content="default-src 'self'; script-src 'self'" /> 9 <title>Hello from Electron renderer!</title> 10</head> 11<body> 12 13 <form> 14 <button type="button" id="createTableButton">create table</button> 15 <button type="button" id="insertRecordButton1">insert record 1</button> 16 <button type="button" id="insertRecordButton2">insert record 2</button> 17 <button type="button" id="selectRecordButton">select</button> 18 </form> 19 20 <script src="./lib1.js"></script> 21</body> 22</html>

JavaScript

1// lib1.js 2 3document.getElementById("createTableButton").addEventListener('click', () => { 4 window.electronAPI.createTable(); 5}); 6 7document.getElementById("insertRecordButton1").addEventListener('click', () => { 8 window.electronAPI.insertRecord(); 9}); 10 11document.getElementById("insertRecordButton2").addEventListener('click', () => { 12 window.electronAPI.insertRecordEx(); 13}); 14 15document.getElementById("selectRecordButton").addEventListener('click', () => { 16 window.electronAPI.selectRecord(); 17});

試したこと

[やったこと1]
(最初はラムダ式の外にあるlet txtに入っていないのかと思い、質問して)txtの値を確認した

JavaScript

1db.all("select * from users", (error, rows) =>{ 2 console.log( "all" ); 3 rows.forEach(row => { txt += "hello"; }); 4 console.log( txt ); 5}); 6console.log( "out: " + txt );

[結果1]
renderer.js内のselectRecordメソッドでのconsole.log( result );ではfalseが表示され、コンソールには

out: all hellohello

と空になっていた。(それとラムダ式の関数が後から動いているらしい。つまり順番が違う。)

[やったこと2]
やったこと1のラムダ式を通常の関数にして渡す。

[結果2]
結果1と変わらず。

[やったこと3]
やったこと1のラムダ式の引数に _txtを追加。

db.all("select * from users", (error, rows, _txt) =>{ console.log( "all" ); rows.forEach(row => { _txt += "hello"; }); });

[結果3]
結果1,2と同じ。

[やったこと4]
ラムダ式内のtxtからするとipcMain.handle内のtxtは別の変数(グローバル変数のような感じ)と解釈されたのだと思い、main.jsの先頭に"use strict";を付けてみた。

[結果4]
変わらず。

[やったこと5]
公式ドキュメントからall系の一括読み込みメソッドを調べた。

[結果5]
探した範囲では存在しなかった。(コールバック関数を受け取って非同期的に行うものしかなかった。)

[やったこと6]
(非同期処理であり、コールバック関数としてラムダ式が渡されていることから)通常の関数として切り出して、renderer.js側でコールバック関数として定義し、ipcRenderer.invokeの引数として渡して処理させる。

// renderer.jsから抜粋 function callback( err, rows ){ console.log( rows ); // 本来はここで何らかの処理をする } ... selectRecord : () => { ipcRenderer.invoke( "select_record", callback ).then( (result) => { console.log( result ); }); }
// main.jsから抜粋 ipcMain.handle( "select_record", (_evnet, f) => { db.serialize( function(){ let txt = ""; db.all("select * from users", (error, rows) =>{ f( error, rows ); }); //console.log( "select = " + txt ); }); return false; });

[結果6]
VM4 sandbox_bundle:29 Uncaught (in promise) Error: An object could not be cloned.というエラーメッセージがディベロッパーツール上に表示される。
→ 調べるとメインプロセスとレンダラープロセスは分断されているために起こるエラー。ただし仕様上のものなので対処できないらしい?

[やったこと7]
念のためdb.serializeに渡している無名関数の引数としてfを指定した。

[結果7]
結果6と同様。

後思いつく方法としては『メインプロセスでDB操作を行いながら、直接DOM操作も行う』という方法ですが、できればメインプロセスはウィンドウの処理をして、DOM操作を行うのはレンダラープロセスに任せたいのです。

補足情報(FW/ツールのバージョンなど)

言語: JavaScript
node.js : node v16.17.0
Electron: Electron v21.1.1

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

Zuishin

2022/11/03 05:19

キャプチャはできているので、質問が内容と合っていません。 app.whenReady で作成したデータを contextBridge.exposeInMainWorld で使用する方法を聞くべきでしょう。
Zuishin

2022/11/04 00:05

> 恐らくコールバック関数としてのラムダ式が呼ばれる時期が違うため いえ、txt はローカル変数で、コンソール以外どこにも出力されていないため、他の場所で使用することはできません。 return txt; で返した場合も、それは sqlite.Database.serialize に渡されたコールバック関数の戻り値でしかありません。 そのコールバック関数の戻り値がどう扱われるのかは調べていないのでわかりません。 他の場所で使いたいのであれば、何らかの方法で渡さなければならないでしょうから、前のコメントの繰り返しになりますが、とりあえずラムダ式からは離れて、その渡す方法を聞くべきでしょう。
BeatStar

2022/11/05 11:38 編集

通知が来ていなかったので気づきませんでした…。 一応やっと答えにたどり着きました。(自己解決にする予定ですが) コールバック関数でのreturnは私もそうだと解釈していましたが…。
guest

回答2

0

自己解決

コールバック関数として渡すことは質問にあることと同じですが方法が間違っていたようです。

  1. メインプロセス、レンダラープロセスの両方からアクセスできるようにコールバック関数をlib2.jsのような新しいjsファイルに記述する。その関数名をmodule.exportsにセットする。
  2. (1)で用意したコールバック関数を使うjsファイル(特にメインプロセス)にconst callbackSelect = require( "./lib2" );のようにしてrequireする。
  3. 実際にコールバック関数として渡す。

JavaScript

1// lib2.js内 2 3// 定義して 4const callbackSelect = (err, row) =>{ 5 console.log( row ); 6} 7 8// module.exportsにセット 9module.exports = callbackSelect;

JavaScript

1// main.js内 2... 3const path = require("path"); 4const callbackSelect = require( "./lib2" ); // ここで関数を取り出す 5... 6 7const createWindow = () => { 8 ... 9 ipcMain.handle( "select_record", (_evnet, _arg) => { 10 db.serialize( function(){ 11 db.each("select * from users", callbackSelect ); // コールバック関数として渡す 12 } 13 } 14}

これでeachメソッドでもallメソッドでも取り出せました。

投稿2022/11/05 11:53

BeatStar

総合スコア4958

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

0

console.log を以下のように移動すると結果は変わってくると思いますがどうでしょう?

diff

1 // HERE! 2 let txt = ""; 3 db.all("select * from users", (error, rows) =>{ 4 rows.forEach(row => { txt += "hello"; }); 5+ console.log( "select = " + txt ); 6 }); 7- console.log( "select = " + txt );

投稿2022/11/03 04:22

退会済みユーザー

退会済みユーザー

総合スコア0

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

BeatStar

2022/11/03 05:00

回答ありがとうございます。 本質問に書き忘れていましたが、やりたいことはlet txtの値を最終的に呼び出し側に相当するrenderer.jsに返したいのです。 (説明下手ですみません)
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.50%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問