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つを使う
- 様々なファビコンを一括生成。favicon generator
- favicon を生成する
- PWA Manifest Generator | SimiCart
- manifest.json を生成する
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" } ] }
_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に入れる。