semigraphy

個人の意見です

Electron のクイックスタートで Hello, World! アプリを作成する

こんにちはsemiguraです。最近暖かくなってきたので嬉しいです。

Electron アプリを作ってみたいなと思い、ひとまず公式のチュートリアルを触って Hello, World! アプリを作成してみたのでそのやり方を備忘録として残しておきます。

TL;DR

  • yarn init && yarn add --dev electron でインストール
  • main.js にアプリの起動時と終了時の設定を記述する
  • preload.js にブラウザ上で動作する js を記述し、 main.js で読み込む
  • index.html に Electron アプリのウィンドウに表示されるページを記述する

環境

Electron 17.1.0

本題

パッケージインストール

mkdir my-electron-app && cd my-electron-app

yarn init && yarn add --dev electron

package.json の内容は以下になります。

{
  "name": "my-electron-app",
  "version": "1.0.0",
  "main": "main.js",
  "license": "MIT",
  "scripts": {
    "start": "electron ."
  },
  "devDependencies": {
    "electron": "^17.1.0"
  }
}

その後、以下をルートディレクトリに作成します。

  • アプリの設定を記述する main.js
  • ブラウザ上で動作する js を記述し、 main.js で読み込む preload.js
  • Electron アプリのウィンドウに表示されるページを記述する index.html

main.js にアプリの起動時の設定を記述する

const { app, BrowserWindow } = require('electron')
const path = require('path')

const createWindow = () => {
  // ブラウザウィンドウを作成する
  const mainWindow = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      // preload.js を読み込む
      preload: path.join(__dirname, 'preload.js')
    }
  })

  // mainWindow で index.html を読み込む
  mainWindow.loadFile('index.html')
}

index.html に Electron アプリのウィンドウに表示されるページを記述する

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Hello World!</title>
  </head>
  <body>
    <h1>Hello World!</h1>
    We are using Node.js <span id="node-version"></span>,
    Chromium <span id="chrome-version"></span>,
    and Electron <span id="electron-version"></span>.
  </body>
</html>

id が指定されている要素には preload.js で動的に文章を流し込みます。

preload.js にブラウザ上で動作する js を記述する

window.addEventListener('DOMContentLoaded', () => {
  const replaceText = (selector, text) => {
    const element = document.getElementById(selector)
    if (element) element.innerText = text
  }

  for (const dependency of ['chrome', 'node', 'electron']) {
    replaceText(`${dependency}-version`, process.versions[dependency])
  }
})

通常の Web ページと同様、 DOMContentLoaded でDOMが全て読み込まれた後に、 id が指定されている要素に chrome, node, electron の各バージョンが読み込まれます。

main.js にアプリの終了時の設定を記述する

app.whenReady().then(() => {
  createWindow()

  app.on('activate', () => {
    // macOS の場合
    // Dock アイコンのクリック時に他に開いているウインドウがない場合、アプリのウインドウを再作成する
    if (BrowserWindow.getAllWindows().length === 0) createWindow()
  })
})

// macOS 以外の場合
// 全ウインドウが閉じられたときに終了する
app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') app.quit()
})

yarn start で起動すると index.html に記述されたページがそのまま表示されます。

アプリ起動時のキャプチャ

また、ウィンドウが正常に閉じるのを確認できます。

参考ドキュメント

Amplify を初めて触ってみる (下調べ編)

本題

最近 Next.js を用いたWebアプリケーション開発の勉強をしており、その一環として Amplify を触ってみることにしました。 Amplify のようなサービスに関してほとんど知識が無かったのですが、いろいろ調べつつ以下のような理解をしました。

  • Amplify は、Web/モバイルアプリケーションをリリースするためのプラットフォーム
  • バックエンドとインフラは Amplify が AWS のサービスを使って自動的に構成してくれるため、フロントエンドの開発者がバックエンドを気にする必要がない

よりバックエンドとの密な連携が必要なWebアプリケーション向けの Netlify や Vercel のようなサービスかなという理解です。

Netlify 他はホスティングが主な用途でAPIなどとの連携は別途DBを用意して組み合わせる必要がありますが、Amplify はこれ一つで完結できそうな印象を持ちました。

よく使いそうなコマンド

amplify init

初期設定コマンド。Source Directory Path、Distribution Directory Path、Build Command、Start Command等をセットする。

amplify add hosting

ホスティングの設定を追加するコマンド。

amplify add auth

認証基盤を追加するコマンド。

amplify add api

バックエンドのAPIを追加するコマンド。

amplify mock api

先に追加したAPIのmockサーバーを生成するコマンド。

amplify push

ローカルで追加した設定を Amplify に反映するコマンド。

Next.js 製の Web アプリを PWA 化する

こんにちはsemiguraです。

TL;DR

  • yarn add next-pwa で PWA 対応用プラグインを導入
  • manifest.json と各種 favicon を生成し public/ 以下に置く
  • _document.jsx で呼び出す
  • 確認は Chrome DevTools の Application タブで行う

環境

Next.js 12.0.10 + next-pwa 5.4.4

本題

next-pwa 導入

yarn add next-pwa

next.config.js を書き換え

next.config.js

const withPWA = require("next-pwa");
const runtimeCaching = require("next-pwa/cache");

const config = {
  pwa: {
    dest: "public",
    runtimeCaching,
  },
};

module.exports = withPWA(config);
注意点

module.exports が複数あると動かない

e.g.

const withPWA = require("next-pwa");

module.exports = {
  webpack: (config, { isServer }) => {
    if (!isServer) {
      config.resolve.fallback.fs = false;
      config.resolve.fallback.child_process = false;
      config.resolve.fallback.net = false;
      config.resolve.fallback.dns = false;
      config.resolve.fallback.tls = false;
    }
    return config;
  },
};

module.exports = withPWA({
  pwa: {
    dest: "public",
    runtimeCaching: []
  },
});

manifest.json 他を生成

以下2つを使う

manifest.json 他を追加

public/manifest.json

{
  "theme_color": "#f69435",
  "background_color": "#f69435",
  "display": "standalone",
  "scope": "/",
  "start_url": "/",
  "name": "example-app",
  "short_name": "example-app",
  "icons": [
    {
      "src": "/android-chrome-36x36.png",
      "sizes": "36x36",
      "type": "image/png"
    },
    {
      "src": "/android-chrome-48x48.png",
      "sizes": "48x48",
      "type": "image/png"
    },
    {
      "src": "/android-chrome-72x72.png",
      "sizes": "72x72",
      "type": "image/png"
    },
    {
      "src": "/android-chrome-96x96.png",
      "sizes": "96x96",
      "type": "image/png"
    },
    {
      "src": "/android-chrome-128x128.png",
      "sizes": "128x128",
      "type": "image/png"
    },
    {
      "src": "/android-chrome-144x144.png",
      "sizes": "144x144",
      "type": "image/png"
    },
    {
      "src": "/android-chrome-152x152.png",
      "sizes": "152x152",
      "type": "image/png"
    },
    {
      "src": "/android-chrome-192x192.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "/android-chrome-256x256.png",
      "sizes": "256x256",
      "type": "image/png"
    },
    {
      "src": "/android-chrome-384x384.png",
      "sizes": "384x384",
      "type": "image/png"
    },
    {
      "src": "/android-chrome-512x512.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ]
}
  • 生成された favicon と manifest.json を全て public/ に入れる

_document.jsx を作成

import Document, {
  Html,
  Head,
  Main,
  NextScript,
  DocumentContext,
  DocumentInitialProps,
} from "next/document";

class MyDocument extends Document {
  static async getInitialProps(
    ctx: DocumentContext
  ): Promise<DocumentInitialProps> {
    // eslint-disable-next-line no-return-await
    return await Document.getInitialProps(ctx);
  }

  render() {
    return (
      <Html lang="ja-JP" dir="ltr">
        <Head>
          <meta
            name="msapplication-square70x70logo"
            content="/site-tile-70x70.png"
          />
          <meta
            name="msapplication-square150x150logo"
            content="/site-tile-150x150.png"
          />
          <meta
            name="msapplication-wide310x150logo"
            content="/site-tile-310x150.png"
          />
          <meta
            name="msapplication-square310x310logo"
            content="/site-tile-310x310.png"
          />
          <meta name="msapplication-TileColor" content="#000" />
          <meta name="apple-mobile-web-app-capable" content="yes" />
          <meta name="apple-mobile-web-app-status-bar-style" content="#000" />
          <meta name="apple-mobile-web-app-title" content="myapp" />
          <link
            rel="apple-touch-icon"
            sizes="180x180"
            href="/apple-touch-icon-180x180.png"
          />
          <meta name="application-name" content="myapp" />
          <meta name="theme-color" content="#000" />
          <meta name="description" content="this is myapp" />
          <link rel="icon" sizes="192x192" href="/icon-192x192.png" />
          <link rel="icon" href="/favicon.ico" />
          <link rel="manifest" href="/manifest.json" />
        </Head>
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    );
  }
}

export default MyDocument;

確認

確認は Chrome DevTools の Application タブで行う。

参考ドキュメント

Next.jsのプロジェクトにCypressを導入してGitHub Actionsで走るようにする

こんにちはsemiguraです。

TL;DR

  • yarn add -D cypress
  • 生成されたintegration以下にテストを書く
  • GitHub Actionsで走らせるにはMarketplaceにある公式のcypress-io/github-actionを使う

環境

Next.js 12.0.8 + Cypress 9.3.1

本題

Cypress導入

yarn add -D cypress

  • これでプロジェクトのルートディレクトリにcypressディレクトリが作られる。
  • 中身は以下。   - fixtures/   - integration/   - plugins/   - support/   - デフォルトで入っているのは全てサンプルデータ。

テストを書く

integration/foo.spec.js

describe("index.tsx", () => {
  it("Successful access", () => {
    cy.visit("http://localhost:3000");
  });
});
  • integration/foo.spec.jsにテストを書く。
  • yarn cypress openでCypressを起動してテストを実行できる。

GitHub Actionsを追加する

.github/workflows/main.yml

name: End-to-end tests
on: push
jobs:
  cypress-run:
    runs-on: ubuntu-20.04
    steps:
      - name: Checkout
        uses: actions/checkout@v2
      - name: Cypress run
        uses: cypress-io/github-action@v2
        with:
          start: npm run dev
  • Marketplaceにある公式のcypress-io/github-actionを使う。
  • この時ローカルサーバーを立ち上げる必要があるのでwith: start: npm run devを書いておく。

参考ドキュメント

RecoilのAtomをlocalStorageに保存して読み込む

注意点

  • より良いやり方があると思うので教えてください
  • AtomEffectは使っていません(今後試す予定)

TL;DR

  • setIntervalで1000ms毎に状態を取得して保存
  • RecoilRootのinitializeState に渡して読み込む

環境

Next.js 12.0.7 + Recoil 0.5.2

本題

Atoms の用意

atoms/states.ts

import { atom } from "recoil";

export const activityListState = atom<{ id: string; date: number | Date }[]>({
  key: "activityListState",
  default: [],
});

export const taskListState = atom<string[]>({
  key: "taskListState",
  default: [],
});
  • activityListState, taskListState の2つを用意

保存

pages/sandbox.tsx

import { useEffect } from "react";

import { useRecoilState } from "recoil";

import { activityListState, taskListState } from "../atoms/states";

function Sandbox() {
  const [taskList] = useRecoilState(taskListState);
  const [activityList] = useRecoilState(activityListState);

  useEffect(() => {
    const interval = setInterval(() => {
      if (
        localStorage.getItem("activityListState") !==
        JSON.stringify(activityList)
      ) {
        localStorage.setItem("activityListState", JSON.stringify(activityList));
      }
      if (localStorage.getItem("taskListState") !== JSON.stringify(taskList)) {
        localStorage.setItem("taskListState", JSON.stringify(taskList));
      }
    }, 1000);
    return () => clearInterval(interval);
  }, [activityList, taskList]);

  return <div />;
}

export default Sandbox;
  • useEffect内でsetIntervalを使用してstateの変更を取得しsetItemしている

読み込み

_app.tsx

import { useEffect, useState } from "react";

import type { AppProps } from "next/app";
import { RecoilRoot } from "recoil";

import { activityListState, taskListState } from "../atoms/states";

function MyApp({ Component, pageProps }: AppProps) {
  const [taskList, setTaskList] = useState([]);
  const [activityList, setActivityList] = useState([]);

  useEffect(() => {
    setTaskList(JSON.parse(localStorage.getItem("taskListState") || "[]"));
    setActivityList(
      JSON.parse(localStorage.getItem("activityListState") || "[]")
    );
  }, []);

  return (
    <RecoilRoot
      initializeState={(mutableSnapshot) => {
        mutableSnapshot.set(activityListState, activityList);
        mutableSnapshot.set(taskListState, taskList);
      }}
    >
      <Component {...pageProps} />
    </RecoilRoot>
  );
}

export default MyApp;
  • initializeStateとmutableSnapshotというものを使ってlocalStorageの値をatomsの初期stateに入れる。

参考ドキュメント