Doon - Markdown Notes
上記動画を参考にアプリを作成する。
使用技術
- Electron
- React
- CSS modules
- jotai
- TypeORM
- react-markdown
状態管理 (Jotai) とデータベース (TypeORM) の使い分け
UI状態: ユーザーインターフェースの現在の状態(例: モーダルの開閉、タブの選択状態)、ログイン状態や一時的なエラーメッセージなど、セッションを通じて持続するが、長期間の永続化を必要としない情報を格納する。
永続的なデータ管理: アプリケーションが再起動しても保持されるべきデータを管理する。
Electron + React + TypeScript の開発環境構築手順
Electron + React + TypeScript の開発環境構築
@quick-start/create-electron
npm: @quick-start/create-electron
electron reload
buildを高速化するライブラリ
tsconfig.jsonを理解する
tsconfig.jsonのオプションについて
オプションについての説明
@tsconfig/node16
が便利
TSConfig Basesの
{
"$schema": "https://json.schemastore.org/tsconfig",
"display": "Node 16",
"_version": "16.1.0",
"compilerOptions": {
"lib": ["es2021"],
"module": "node16",
"target": "es2021",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"moduleResolution": "node16"
}
}
Electronの場合、メインプロセス(main)とレンダラープロセス(renderer)で環境が異なるためそれぞれのプロセスのために異なるtsconfig.jsonファイルを設定する。
環境の違い
- メインプロセスはNode.js環境で動作し、ElectronのAPIやNode.jsのAPIにフルアクセス可能
- レンダラープロセスはChromium(Web)環境で動作し、WebAPIへのアクセスがメイン
mainプロセス
{
"compilerOptions": {
"module": "commonjs",
"target": "es6",
"outDir": "./dist/main",
"lib": ["esnext"],
"noImplicitAny": true,
"sourceMap": true
},
"include": ["src/main/**/*"],
"exclude": ["src/renderer/**/*"]
}
renderプロセス
{
"compilerOptions": {
"module": "esnext",
"target": "es6",
"outDir": "./dist/renderer",
"lib": ["dom", "esnext"],
"noImplicitAny": true,
"sourceMap": true,
"jsx": "react"
},
"include": ["src/renderer/**/*"],
"exclude": ["src/main/**/*"]
}
package.jsonで調整
"scripts": {
"build:main": "tsc -p tsconfig.main.json",
"build:renderer": "tsc -p tsconfig.renderer.json",
"build": "npm run build:main && npm run build:renderer"
}
tsconfig.node.json
とtsconfig.web.json
が存在する理由→ Electronはメインプロセス(Node.js環境)とレンダラープロセス(Webブラウザ環境)の両方のコードが含まれる為
aliasはelectron.vite.config.ts
で設定すればよかった模様
export default defineConfig({
main: {
plugins: [externalizeDepsPlugin()],
resolve: {
alias: {
'@/main': resolve('src/main'),
'@/lib': resolve('src/main/lib')
}
}
},
preload: {
plugins: [externalizeDepsPlugin()]
},
renderer: {
assetsInclude: 'src/renderer/assets/**',
resolve: {
alias: {
'@/renderer': path.resolve(__dirname, 'src/renderer'),
'@/components': resolve('src/renderer/src/components'),
'@/hooks': resolve('src/renderer/src/hooks'),
'@/assets': resolve('src/renderer/src/assets'),
'@/store': resolve('src/renderer/src/store'),
'@/mocks': resolve('src/renderer/src/mocks')
}
},
plugins: [react()]
}
})
コンテキストの分離について
コンテキスト分離は、Electronのセキュリティ機能の一つ。レンダラープロセスのグローバルスコープをメインプロセスや他のレンダラープロセスから分離することで、セキュリティ(クロスサイトスクリプティング(XSS)攻撃などから守る)を向上させる。
→ ウェブページのJavaScriptがElectronのAPIやNode.jsの機能に直接アクセスすることを防ぐ。
代わりに、contextBridge APIを使用して、メインプロセスから安全に公開された機能のみをレンダラープロセスで利用できる。
contextBridge.exposeInMainWorld(apiKey, api)
コンテキストが分離されているかどうかを確認する
if(process.contextIsolated) {
...
}
Electron vita
Scssで以下エラーが発生
[plugin:vite:css] [sass] Can't find stylesheet to import.
╷
2 │ @use '@/styles/libs/mixins/index' as *;
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
╵
src/renderer/src/styles/_base.scss 2:1 @use
src/renderer/src/styles/styles.scss 1:1 root stylesheet
ViteがScssのpathを解決できていないことが原因である模様
nagative merginの使い所
例えば、ヘッダー、フッターの左右と上下の位置は、ページにぴったり配置したいが、親の余白があるために子の配置がうまくいかない場合など。
mixinのtips
cssのテクニック
CSS入れ子チートシート
React.ComponentPropsについて
Props 型定義を自分で用意した場合、メンテナンスコストが高い為お勧めしない。実装が増えるたびに都度都度値を追加していく必要があるため。
type Props = {
value: string;
onChange?: React.ChangeEventHandler<HTMLInputElement>
onBlur?: React.FocusEventHandler<HTMLInputElement>
}
export const Input = ({ value, onChange, onBlur }: Props) => (
<input
value={value}
onChange={onChange}
onBlur={onBlur}
className={styles.input}
/>
)
React.ComponentProps 型を使う
type Props = React.ComponentProps<'input'>
export const Input = ({ className, ...props }: Props) => (
<input
{...props} // <- className 以外、全ての props を分割代入
className={clsx(className, styles.input)}
/>
)
React.ComponentPropsよりもComponentPropsWithoutRef
を使用することでrefを許容しているかを確認できる。
Component設計
Atomic Designについてあらためて評価する
Atomic Designの場合、コンポーネントの粒度を決定する基準が曖昧になることがある。例えば、所望のコンポーネントをOrganismsに作ったらいいのか、Moleculesに作ったらいいのか迷う場面がある。
ドラッグ可能な領域
electronでドラッグ可能な領域はcssの-webkit-app-region: drag
で設定する。
状態管理ライブラリ
▪️ 候補
- Redux
コードが膨大になりやすい。より簡単に記載できるRedux Toolkitは存在する。
DispatchとReducerが存在し、メンテナンスコストが大きい。
すべてのステートを一つのStoreで管理し、selectorにより必要なデータのみ抽出する。
ルールが厳密で大規模開発向き
- Recoil
AtomとSelector:Atomは一つの状態を保持する(Reduxとは異なりデータのソースとなるAtomが複数存在する)
ReduxのDispatchとReducerによる中間操作が不要
対象のコンポーネントを<RecoilRoot/>
で囲む。Atom を作成する際はkey
を指定する必要あり。
key
の管理をどのようにするか。
- Zustand
Redux に近い
- Jotai
コンポーネントで Atomの使用が可能で、プロバイダーでラップする必要なし。keyも不要
シンプルで直感的に操作できるJotaiを採用する。
Reactの歴史
以下の日付操作ライブラリを使用する。
最近出てきて使用してみたかったFromKitを使ってみる。
MDX Editor
マークダウンの生成にMDX Editorを使用する。
Toolbar
やCode blocks
が直感的に記述することができ良さげ
MDXEditorを使用したところ、以下のエラーが発生
Uncaught EvalError: Refused to evaluate a string as JavaScript
because 'unsafe-eval' is not an allowed source of script in the following Content Security Policy directive: "script-src 'self'".
at eval (<anonymous>)
at getGlobal (@mdxeditor_editor.js?v=1930ed33:65343:10)
at @mdxeditor_editor.js?v=1930ed33:65344:2
Content Security Policy(CSP)に関連したエラー:XSS攻撃などから保護するために、どのような外部リソースがWebページに読み込まれることを許可するかをブラウザに指示するためのもの
Electornではunsafe-eval を許可しないようにデフォルト設定されている。
MDXEditorや依存ライブラリがeval()関数を使用しているために、警告文がconsole.logに表示されている模様。
eval() 関数は文字列から JavaScript を実行する関数。
一応手段としては、'unsafe-eval' を CSP ルールに追加することは可能(セキュリティリスクは増加する)。
<meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-eval';">
代替案
react-markdown
, remark
など別のMDXを検討する。
- react-markdown
- SimpleMDE (react-simplemde-editor)
- Draft.js
- ProseMirror
react-markdown
を採用する