Code for final

ふぁいなる向けのコード置き場です。すでにコードじゃないこともいっぱい。

最低限抑えておくレベルでElectronを使ったアプリ開発のプロセス間通信を簡単に理解する

Electronを使ってアプリ開発をしていましたが、Electronの仕組みを理解できていなかったので開発に必要なレベルで簡単にまとめました。
コードの説明はvue.js + typescriptを使った例になりますが、ほかの場合も考え方はほぼ同じだと思います。

メインプロセスとレンダラープロセスはサーバーとクライアントのような関係

Electronはメインプロセス(1つ)とレンダラープロセス(複数)で動作します。
メインから画面を立ち上げるごとにレンダラープロセスが生成されます。
ここでいうプロセスはexeファイルのことだと思ってください。
メインプロセスでOSとのやり取りを行い、レンダラープロセスではユーザーとやり取りを行います。
メインプロセスをサーバーと考えれば、レンダラープロセスはクライアント(ブラウザ)というイメージですかね。

クライアント・サーバー間の通信はHTTP、メイン・レンダラープロセス間の通信はプロセス間通信

クライアントとサーバー間はHTTPを使って通信するようにメインとレンダラー間はプロセス間通信(IPC)で通信します。
プロセス間通信というと難しく聞こえるかもですが、HTTPと同じでリクエストを送って、レスポンスを受けるのと変わりません。

プロセス間通信のイメージ

私が理解したプロセス間通信をイメージを絵にしてみました。個人的なイメージなので間違ってたらすみません。

f:id:finalstream:20200609144100j:plain

OSとやり取り(ファイル操作とか)するのはメインプロセスで、レンダラープロセスからは依頼(リクエスト)して結果(レスポンス)を受け取るイメージです。

レンダラープロセスの実装(リクエスト送信)

Node.jsにプロセス間通信を行うためのapiが用意されておりそれを使うことで簡単に実装できます。
レンダラープロセス内(vueファイル)でリクエストしたいタイミングでipcRenderer.invoke()を実行します。
以下の例はディレクトリパスを渡して配下のディレクトリを返却する検索処理だと思ってください。
"searchDirectory"が呼び出すAPIの名前で、this.directoryPathはAPIに渡すパラメタです。

import electron from "electron";

export default class App extends Vue {

  private ipcRenderer = electron.ipcRenderer;

  async search() {
    const response: IpcResponse<string[]> = await this.ipcRenderer.invoke(
      "searchDirectory",
      this.directoryPath
    );
    if (response.error) {
      // エラーのときの処理
      this.showMessage(MessageLevel.Warning, "ディレクトリーが存在しません");
      return;
    }
    // 正常のときの処理
    this.updateDirectories(response.data);
  }

ipcRendererにはsend()というメソッドがありますが、async/awaitで書けるinvoke()を使ったほうがシンプルに書けるのでおすすめです。
TypeScriptで書いているのでIpcResponseというレスポンスオブジェクトを定義しています。これは自作クラスです。標準で用意されているかと思い、探してなかったので作りました。

メインプロセスの実装(レスポンス送信)

レンダラープロセス同様にこちらもAPIが用意されています。
メインプロセス(background.ts)にipcMain.handle()の処理を実装しておきます。
以下の例は指定パスの配下のディレクトリを取得して返却する処理になります。
"searchDirectory"は待ち受けるAPI名で、directoryPathは受信したパラメタです。

import { ipcMain } from "electron";

ipcMain.handle("searchDirectory", async (event, directoryPath) => {
  try {
    const dirents = await fs.promises.readdir(dirpath, { withFileTypes: true });
    return new IpcResponse(directoryPath);
  } catch (e) {
    return new IpcResponse(e);
  }
});

ipcMainにもon()というメソッドがありますが、async/awaitで書けるhandle()を使ったほうがシンプルに書けるのでおすすめです。

プロセス間通信を行うための設定

デフォルトの設定でプロセス間通信を行おうとすると、アプリを起動しただけで以下のようなエラーに苛まれます。

index.js:4 Uncaught ReferenceError: __dirname is not defined
    at Object../node_modules/electron/index.js (index.js:4)
    at __webpack_require__ (bootstrap:853)
    at fn (bootstrap:150)
    at Module../node_modules/cache-loader/dist/cjs.js?!./node_modules/babel-loader/lib/index.js!./node_modules/ts-loader/index.js?!./node_modules/cache-loader/dist/cjs.js?!./node_modules/vue-loader/lib/index.js?!./src/views/AppMain.vue?vue&type=script&lang=ts& (app.js:1056)
    at __webpack_require__ (bootstrap:853)
    at fn (bootstrap:150)
    at Module../src/views/AppMain.vue?vue&type=script&lang=ts& (AppMain.vue?679e:1)
    at __webpack_require__ (bootstrap:853)
    at fn (bootstrap:150)
    at Module../src/views/AppMain.vue (AppMain.vue?8622:1)

これはだいぶハマったのですが、どうやらデフォルトではセキュリティを高めるため、プロセス間通信できないようになっています。
background.tsの以下の箇所にある"nodeIntegration"をtrueにすればOKです。

// Create the browser window.
  win = new BrowserWindow({
    x: AppStore.instance.get("window.x"),
    y: AppStore.instance.get("window.y"),
    width: 800,
    height: 600,
    webPreferences: {
      // Use pluginOptions.nodeIntegration, leave this alone
      // See nklayman.github.io/vue-cli-plugin-electron-builder/guide/security.html#node-integration for more info
      nodeIntegration: true,  //  ★ここをtrueにする
    },
    title: "Empty Directory Cleaner",
  });

もしくはvue.config.jsでも指定できます。どちらかお好きな方で指定してください。

module.exports = {
  pluginOptions: {
    electronBuilder: {
      nodeIntegration: true,
    },
  },
};

ただ、このnodeIntegrationをtrueにするのは推奨されない行為らしいです。
これをfalseのままプロセス間通信できるか調べて見ました。
以下でも議論されていますが、やり方はあるっぽいですが、Node.jsの便利なAPIは使えないっぽいです。

github.com

まとめ

Electronのプロセス間通信はwebと同じで、サーバー(メインプロセス)とクライアント(レンダラープロセス)間で行われるHTTP通信のようなもの。

はてなブログで最終更新日を表示するときに投稿日と同じ場合は表示しないようにするカスタマイズ

はてなブログで最終更新日を表示したくなり、やり方を調べて実装したことをまとめておきます。

やり方は以下の記事を参考にさせてもらいました。
ありがとうございます!

www.tomomore.com

上記のとおりで実装して表示できたのですが、投稿日と最終更新日が同じ場合、以下のように表示されてしまいます。

f:id:finalstream:20200606134657p:plain

ちょっとだけカスタマイズさせてもらって、投稿日と最終更新日が同じ場合は、表示されないようにしました。
記事上に表示したかったので記事上ところに貼り付けました。
sitemap.xmlは各自のサイトに合わせて修正してください。
ちなみにプレビューには反映されませんので注意。確認は変更を保存してから実際のページで確認してください。

<!-- 更新日時表示 -->
<!--[if lt IE 9]>
  <script src="https://ajax.aspnetcdn.com/ajax/jQuery/jquery-1.12.3.min.js"></script>
<![endif]-->
<!--[if gte IE 9]><!-->
<script src="https://ajax.aspnetcdn.com/ajax/jQuery/jquery-2.2.3.min.js"></script>
<!--<![endif]-->
<script>
  (function($) {
    "use strict";
    var urls = [],
      opts = { cache: false, dataType: "xml" },
      p,
      url = "https://final.hateblo.jp/sitemap.xml";
    function parseSitemapXML(url) {
      var d = new $.Deferred();
      $.ajax(
        $.extend(opts, {
          url: url,
        })
      )
        .done(function(xml) {
          $(xml)
            .find("sitemap")
            .each(function() {
              urls.push(
                $(this)
                  .find("loc")
                  .text()
              );
            });
          d.resolve();
        })
        .fail(function() {
          d.reject();
        });
      return d.promise();
    }
    function findURL(url) {
      $.ajax(
        $.extend(opts, {
          url: url,
        })
      )
        .done(function(xml) {
          var isMatched = false;
          $(xml)
            .find("url")
            .each(function() {
              var $this = $(this);
              if ($this.find("loc").text() === location.href) {
                isMatched = true;
                appendLastmod($this.find("lastmod").text());
                return false;
              }
            });
          if (!isMatched) nextURL();
        })
        .fail(function() {});
    }
    function nextURL() {
      urls.shift();
      if (urls.length) findURL(urls[0]);
    }
    function appendLastmod(lastmod) {
      // 投稿日と更新日が同じ場合は表示しないようにする
      var pubdate = $("time[pubdate]").first().attr("datetime");
      pubdate = pubdate == undefined ? "" : pubdate;
      if (lastmod.substr(0, 10) == pubdate.substr(0, 10)) return;

      var $container = $("<div></div>", { class: "lastmod" }).text(lastmod.replace(/T.*0/, ""));
      if (
        $(".entry-header > .date")
          .get(0)
          .tagName.toLowerCase() === "span"
      ) {
        $(".entry-title").before($container);
      } else {
        $(".entry-date").append($container);
      }
    }
    p = parseSitemapXML(url);
    p.done(function() {
      findURL(urls[0]);
    });
    p.fail(function(error) {});
  })(jQuery);
</script>

追加したところはコメントいれてますが、以下のところだけです。
あとSSLサイトなのでjqueryのurlをhttpsに直しました。

 function appendLastmod(lastmod) {
      // 投稿日と更新日が同じ場合は表示しないようにする
      var pubdate = $("time[pubdate]").first().attr("datetime");
      pubdate = pubdate == undefined ? "" : pubdate;
      if (lastmod.substr(0, 10) == pubdate.substr(0, 10)) return;

投稿日の取得ははてなブログの仕様が変わったらできなくなりそうな気がしますが、
そのときはそのときということで。
なんこかはてなブログのサイトを開いて試しましたが、とれたのでほかのサイトでも使えると思います。

Electron + Vue.js + TypeScriptでWindowsアプリ開発環境を構築する手順(2020年版)

アプリを開発したくなり、前から気になってたElectronで作ってみようと調べてたらVue.jsが使えることがわかり、 アプリの開発環境を構築ようとしたものの、思いのほかつまづいたので対処方法をメモっときます。
たぶん時が流れると状況は変わってくると思うので2020年現在の手順と理解ください。

Electronとは

web(HTML+CSS)の技術でデスクトップアプリを作成できるフレームワークです。
ちなみにVisual Studo CodeはElectronでできています。

www.electronjs.org

Node.jsをインストール

現時点で推奨版となっている12.18をダウンロードしてインストールします。
インストールは次へ次へでOKです。

nodejs.org

Visual Studio Codeをインストール

すでに入っているひとはスキップでOKです。

code.visualstudio.com

VueCLIをインストール

Vue CLIはVue.jsで開発するのにほぼ必須なツールです。 Visual Studio Codeを起動してターミナルを開いて以下のコマンドを実行する。

>npm install -g @vue/cli

vue uiを実行

vue uiはプロジェクトの作成やプラグインのインストール、ビルドなどをGUIで行えるようになる便利なツールです。
Vue CLIに同梱されていますので別途インストールは不要です。
Vue CLIインストールが終わったら以下のコマンドをたたいてvue uiを起動します。

>vue ui

ここで以下のエラーがでました。

PS C:\Users\final> vue ui
vue : このシステムではスクリプトの実行が無効になっているため、ファイル C:\Users\final\AppData\Roaming\npm\vue.ps1 を読み込むことができま
せん。詳細については、「about_Execution_Policies」(https://go.microsoft.com/fwlink/?LinkID=135170) を参照してください。
発生場所 行:1 文字:1
+ vue ui
+ ~~~
    + CategoryInfo          : セキュリティ エラー: (: ) []、PSSecurityException
    + FullyQualifiedErrorId : UnauthorizedAccess

1年くらい前まではこんなエラー出なかったんですが、任意のコードが実行されないようにOSのセキュリティが厳しくなったんだと思います。 対処としてPowerShellを管理者権限で別に起動して以下のコマンドを実行します。

>PowerShell Set-ExecutionPolicy RemoteSigned

実行後は"vue ui"が成功すると思います。

vue uiでプロジェクトを作成

プロジェクトの作成は以下の記載の手順と同じ内容でOKです。

final.hateblo.jp

今回はテストを除いて以下を選択しました。ElectronでもTypeScriptは強く推奨します。

  • Babel
  • TypeScript
  • Router
  • Linter / Formatter

ただ、そのまま進めると以下のようなエラーが出てプロジェクト作成に失敗しました。

エラーの内容は以下のような感じで原因がさっぱりです。
1年くらい前は同じ手順で問題なく動いていたんですけどね。

at makeError (C:\Users\final\AppData\Roaming\npm\node_modules\@vue\cli\node_modules\execa\index.js:174:9)
    at C:\Users\final\AppData\Roaming\npm\node_modules\@vue\cli\node_modules\execa\index.js:278:16
    at processTicksAndRejections (internal/process/task_queues.js:97:5)
    at async C:\Users\final\AppData\Roaming\npm\node_modules\@vue\cli\node_modules\@vue\cli-ui\apollo-server\connectors\projects.js:345:5    at async Object.wrap (C:\Users\final\AppData\Roaming\npm\node_modules\@vue\cli\node_modules\@vue\cli-ui\apollo-server\connectors\progress.js:39:14) {

海外の掲示板にyarnが入っていないのが悪いみたいなことが書いてあったので以下のコマンドを実行してから再度作成したらうまくいきました。
ちなみにyarnはnpmの後継みたいなコマンドです。

>npm install -g yarn

Electron用のプラグインを追加する

いよいよElectronの導入です。上記までの手順は通常のVue.jsの開発と同じです。
vue uiのプラグイン追加で、"electron-builder"を検索してプラグインをインストールします。
検索すると何個か出てきますが、一番上のダウンロード数が一番多いやつを選択します。 これもそのまま進めるとエラーになりました。(エラーメッセージは出ず何度やってもプラグインがインストールできない)

そこで一旦、vscodeを終了します。
vscodeを起動して、vueプロジェクトのフォルダを開きます。
ターミナルを開いてvue uiを起動する。
もっかいプラグインを追加する。
とうまくいきました。
Electronのバージョンはデフォルトのままで9.0を選択します。

アプリを起動

vue uiのタスクに”electron:serve”があるのでそれを実行するとアプリが起動します。

f:id:finalstream:20200603134224p:plain

途中、出力欄に赤いものが見えるかもしれませんが、起動はできました。

exeを作成

アプリを作ったらexeを配布する必要があります。
タスクの一覧にある"electron:build"を選択します。
パラメータで"Build for Windows"をONにします。
あとはタスクを実行します。

するとエラーになります。ビルドだとエラーがあると成功しないようになっており、アプリ起動するときに出てたエラーが邪魔をするわけです。
エラーはたくさん出てますが、以下のようなエラーがでていました。

ERROR in d:/finalstream/newdev/edc/node_modules/electron/electron.d.ts(1659,31):
1659:31 Cannot extend an interface 'NodeJS.EventEmitter'. Did you mean 'implements'?

これも日本のサイトに情報がなかったので公式のissueに確認すると、”@types/node”パッケージがv13以降だとエラーになるみたいなことが書いてありました。

github.com

”yarn list --depth=0”コマンドでインストールされているバージョンを確認すると"@types/node@14.0.9"となっていました。
どうやらVue CLIでインストールすると最新のバージョンが入るみたいです。
これでは現時点では動かないので以下のコマンドをvscodeのターミナルで実行してダウングレードします。
バージョンはv12系ならなんでもいいと思うので、現時点のv12系の最新版にします。

www.npmjs.com

 >yarn upgrade @types/node@12.*

それでもっかいビルドするとエラーがなくなると思いきや、1個残りました。

ERROR in d:/finalstream/newdev/edc/src/background.ts(27,7):
27:7 Type 'string' is not assignable to type 'boolean | undefined'.
    25 |       // Use pluginOptions.nodeIntegration, leave this alone
    26 |       // See nklayman.github.io/vue-cli-plugin-electron-builder/guide/security.html#node-integration for more info
  > 27 |       nodeIntegration: process.env.ELECTRON_NODE_INTEGRATION
       |       ^
    28 |     }
    29 |   });
    30 | 

エラーを見る限り、booleanに変換しろとのことなので、background.tsの該当箇所を以下のように修正します。"!!"つけてbooleanに変換します。

nodeIntegration: !!process.env.ELECTRON_NODE_INTEGRATION

これでビルドができました。
ビルドしたものはプロジェクトフォルダの"dist_electron"にできます。
デフォルトでインストーラも作ってくれるみたいですね。
インストーラが不要な方は"win-unpacked"フォルダの中を配布すればOKかと思います。
サンプルまんまのアプリですが、100MBくらいとサイズがでかいですね。
vscodeも確認しましたが、同じような感じだったのでElectronアプリはそんな感じなんだと思います。
いまの時代、100MBなんて数秒でダウンロードできるので問題ないですね。

最後に

以上でアプリの開発環境ができました。
これからアプリを作ってつまづきポイントがあればまた記録したいと思います。