コンテンツにスキップ

Web関連

Android開発: -> Kotlin

asdf

asdf asdf自体のインストール

asdf グローバルなバージョン指定

1
asdf global <lang-name> <version>

asdf 自動でインストールするパッケージの設定

  • ~/.default-npm-packagesに書き込めばよい
  • Pythonの場合は~/.default-python-packagesなどとすればよい

asdf 導入できる言語を調べる

1
2
asdf plugin list all
asdf plugin add nodejs https://github.com/asdf-vm/asdf-nodejs.git

asdf ある言語のインストールできるバージョンを調べる

1
2
asdf list-all <lang-name>
asdf list-all nodejs

asdf 特定言語・特定バージョンのアンインストール

1
2
asdf uninstall <lang-name> <version>
asdf uninstall nodejs 22.8.0

asdf 特定言語のバージョンの一覧

1
2
asdf list <lang-name>
asdf list nodejs

asdf 特定言語のバージョンをインストール

1
2
3
4
asdf install <lang-name> <version>
asdf install nodejs latest
asdf install nodejs 16.17.0
asdf install nodejs 18.12.0

asdf 特定ディレクトリでバージョン選択

  • コマンドasdf local nodejs 20.7.0を実行する
  • そのディレクトリにファイル.tool-versionsが置かれ, バージョンが指定される.
  • 手動で.tool-versionsを作ってもよい.
1
2
3
asdf local <lang-name> <version>
asdf local nodejs 16.17.0
asdf local nodejs 20.7.0

Chrome

拡張機能が表示されない

  • 参考: 対処2
  • ブラウザ画面の右上にある「拡張機能(パズルのピースのアイコン)」をクリック
  • 表示された拡張機能の一覧から、表示したい拡張機能の「ピンマーク」をクリックして固定化

全体のスクリーンショットを撮る

  • 参考
    • 2022/05時点では少し違うUIになっているようなので注意する
  • 手順
    • デベロッパーツールを立ち上げる
    • WindowsではCtrl+Shift+P, MacではCmd+Shift+Pを押す
    • 入力欄でscreenshotと打つ
    • 「フルサイズのスクリーンショットをキャプチャ」的な項目を選ぶ

CSS

CSS CSS道場的な何か

CSS styleタグでHTMLに直接指定

1
2
3
4
5
    <style>
      body {
        background-color: #00ff00
      }
    </style>

CSS アプリのようにフッタを画面下部に張り付ける

1
2
3
4
5
6
7
#sp-fixed-menu{
   position: fixed;
   width: 100%;
   bottom: 0px;
   z-index: 99;
   backgroundColor: white;
}

CSS 外部ファイルの読み込み

1
    <link rel="stylesheet" href="https://cdn.simplecss.org/simple-v1.css">

CSS 長い文を途中で切る

1
2
3
4
5
6
.shortcut {
    width: 300px; /* 要素の横幅を指定 */
    white-space: nowrap; /* 横幅のMAXに達しても改行しない */
    overflow: hidden; /* ハミ出した部分を隠す */
    text-overflow: ellipsis; /* 「…」と省略 */
}

CSS ブロック要素を中心に置く

1
margin: auto

Dart/Flutter

インストール

Mac

まずは公式を見よう.

const, final

  • 参考
  • finalは変数の性質
    • 「final変数に代入できない」など
    • 代入の左辺になったときの変数としての扱いについて差が出る
    • しかしfinal変数の保持する値には影響しない
  • constは変数の性質かつconst変数が保持する値の性質も規定
    • つまりconst変数の値を他の変数に代入したり、関数の引数として値を渡すときにconstが付く・付かないで制約回避できる・できないといった差異が出る場合あり

final指定

  • プログラム開始後のある時点で一回だけ初期化される.
  • 初期化以降は代入などを通じて変更されない/できないことが保証される(再代入不可)
  • なお、finalな変数が「指す先」のメモリ領域の内容が変更されることについての制約はない
1
2
3
4
final int i = 0;
i = 2; // Error(再代入不可)
final List<int> a = [1,2,3];
a[0] = 4; // OK(指す先は変更可能)

const指定

  • 変数の値が「コンパイル時定数(compile-time constant)」
  • constな変数の値はプログラムの実行開始に先立って初期化されていてプログラムの実行を通じて不変
  • const変数は再代入も不可: finalの意味に加えてconstな変数が指す先のメモリ領域の内容も変更不可
1
2
3
4
5
const int i = 0;
i = 2; // Error(再代入不可)
const List<int> a = [1,2,3]; // OK ※1
const List<int> b = const [1,2,3]; // OK(const値)
b[0] = 4; // コンパイルエラー(指す先も変更不可)

JavaScript/TypeScript

axios CSRF token mismatch

  • 参考
  • 特にLaravelのサーバーが提供するAPIを叩いたときにエラーでこれが出たとき
  • Laravel側に設定を追記してみよう
  • app/Http/Middleware/VerifyCsrfToken.phpに次のように追記
1
2
3
protected $except = [
        '/api/*'
];

CORSエラー時の対処

  • URL
  • 基本的にはバックエンドの設定

expressの場合

  • yarn add corsを実行
  • バックエンド側に次のコードを実装
    • 実際にはきちんと許可設定をつけるべき
1
2
3
4
5
6
7
8
9
const express = require('express')
const cors = require('cors')
const app = express()

app.use(cors())

app.get('/user/:userId', function (req, res, next) {
  res.json({result: '任意のオリジンからすべてのAPIがアクセスOK'})
})

eslint 先頭に_を含む未使用の変数を許容する

  • URL
  • .eslint.jsonrulesオブジェクトに次の要素を追加する
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
"rules": {
    "@typescript-eslint/no-unused-vars": [
      "warn",
      {
        "argsIgnorePattern": "^_",
        "varsIgnorePattern": "^_",
        "caughtErrorsIgnorePattern": "^_",
        "destructuredArrayIgnorePattern": "^_"
      }
    ]
  },

express エラー処理

express 単純なexpress serverのデプロイ, TypeScript

  • 参考
  • yarn add -D typescript @type/node
  • 適当にserver.tsなどを作る: index.jsのコピペそのままで可
  • package.jsonscriptsに追記
1
2
3
4
5
6
7
{
  "scripts": {
    "build": "tsc --build",
    "clean": "tsc --build --clean",
    "start": "yarn build; node dist/server.js"
  }
}
  • tsconfig.jsonを作って次のように書く
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
{
  "compilerOptions": {
    "noImplicitAny": false,
    "noEmitOnError": true,
    "removeComments": false,
    "sourceMap": true,
    "target": "es5",
    "outDir": "dist"
  },
  "include": [
    "**/*"
  ]
}
  • Procfileweb: yarn startに修正
  • ローカルでyarn startしてからAPIにアクセスして正しく値が返ってくるか確認
  • デプロイ

express prismaと連携したREST API

  • prismaとexpressでつくるREST API
  • データベースはPostgresにしているものの, すぐMySQLに書き換えられる上, 十分参考になる
  • データベースは{docker}で構築している
  • dockerについてはdockerを参考にすること.
  • テストのbeforeEachで呼ぶ初期化はPrisma テストで実際にデータベースを読み書きするの項目を参考にすること

fastify, prisma

  • apps/fastifyに移動
  • npx prisma init
  • apps/fastify/prismaschema.prismaが生成される
  • schema.prismaを編集
  • npx prisma migrate dev --name initを実行
  • apps/fastify/.env.sampleをコピーして.envを作る
  • TODO: ルートディレクトリにまとめられないか確認
  • npx prisma migrate dev --name init
  • TODO: workspace.jsonにタスクとして登録したい
  • seedを入れたい場合は次のように進める
  • package.json"type": "module"を設定
  • NODE_OPTIONS="--loader ts-node/esm" node prisma/seed.tsを実行

fastify main.tsへの設定

  • portだけではなくhostも設定する
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
app.listen(
  {
    port: Number(process.env.PORT) || 3000,
    host: process.env.HOST || '0.0.0.0', // ここも入れる
  },
  (err) => {
    if (err) {
      console.error(err);
      process.exit(1);
    }
  }
);

Fastify+PrismaでJWT認証付きREST-APIサーバーを作る

Firebase Authentication

Firebase Cloud Functionsの環境変数設定

  • URL
  • firebase functions:config:set slack.apikey="THE API KEY"などのコマンドで設定する
    • サーバー側にも同時に設定される
    • 変数名は全て小文字
  • 【必須】ローカルで開発用にfirebase serveする前に次のコマンド実行する
    • functionsフォルダの下に置かないと認識してくれないらしい
1
firebase functions:config:get > functions/.runtimeconfig.json
  • プログラム中で呼び出すときは次のように書く
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
const functions = require('firebase-functions');

exports.printenvSlack = functions.https.onRequest( (req, res) => {
  const config = functions.config();
  if( config.slack && config.slack.apikey ){
    res.send( config.slack.apikey );
  }
  else{
    res.send("Not Found Config");
  }
});

Firebase Cloud Functionsのデプロイエラーログを見る

  • URL
  • firebase functions:logコマンドでデプロイ時のエラーログが見られる

Firebase v9: 認証の永続化

  • URL
    • 2022/7/28時点でこのページの「サポートされている認証状態の永続性のタイプ」の列挙型欄はおかしい
    • ここの値参照: 公式ではどこにある?
  • 結論: v9では次のように書けばよい
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
setPersistence(auth, browserLocalPersistence).then(() => {
  return signInWithEmailAndPassword(auth, email, password)
    .then((res) => {
      const user = res.user;
      setLoginUser(user);
      setLoginErrorMessage("");
    }).catch((error) => {
      setLoginErrorMessage(`${error.code}: ${error.errorMessage}`);
    });
}).catch((error) => {
  setLoginErrorMessage(`${error.code}: ${error.errorMessage}`);
});

Firebase エミュレーターを起動

1
firebase emulators:start

Firebase 開発環境と本番環境をわける

  • URL
  • 環境ごとにプロジェクトを作る
  • firebase projects:listで環境を確認
  • firebase use --addで使う環境を追加
    • 同時にエイリアスを設定
  • firebase use <alias>で適切な環境を設定
  • firebase functions:secrets:setで環境ごとに必要な環境変数を設定

Firebase サンプルプロジェクト

Firebase プロジェクトで使うアカウントの設定

1
firebase login:use someuser@example.com

Firebase プロジェクトの初期化

1
yarn firebase init

Firebase プロジェクトの変更

1
2
3
firebase login            # 必要に応じてアカウント変更
firebase projects:list    # 変更したいプロジェクトIDの確認
firebase use <project-id> # 変更コマンド

Firebase ログイン情報の確認

1
firebase login:list

Firebase ログイン情報の追加

1
firebase login:add

husky 設定

  • URL
  • husky.sh.gitignoreに追加
1
2
npm install husky --save-dev
npx husky install
  • npm scriptに追加
1
2
3
  "scripts": {
    "prepare": "husky install"
  }
  • npx husky add .husky/pre-commit "npm run lint"でファイルに追記できる

JavaScript: sleep()は一行で書ける

1
(async function (ms) { await new Promise(s => setTimeout(s, ms)) })(3000);

Javascript: Nullish CoalescingOptional Chaining

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
const users = [
  {
    name: 'Patty Rabbit',
    address: {
      town: 'Maple Town',
    },
  },
  {
    name: 'Rolley Cocker',
    address: {},
  },
  null,
];
for (const u of users) {
  const user = u ?? { name: '(Somebody)' };
  const town = user?.address?.town ?? '(Somewhere)';
  console.log(`${user.name} lives in ${town}`);
}

// Patty Rabbit lives in Maple Town
// Rolley Cocker lives in (Somewhere)
// (Somebody) lives in (Somewhere)

Javascript: オブジェクトに対する反復処理

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const user = {
  id: 3,
  name: 'Bobby Kumanov', username: 'bobby',
  email: 'bobby@maple.town',
};

console.log(Object.keys(user));
// [ 'id', 'name', 'username', 'email' ]

console.log(Object.values(user));
// [ 3, 'Bobby Bear', 'bobby', 'bobby@maple.town' ]

console.log(Object.entries(user));
//[
// ['id',3],
// ['name','BobbyKumanov'],
// ['username','bobby'],
// ['email','bobby@maple.town']
//]

// キーと値のペアを反復処理の中で扱う
Object.keys(user).map((k) => { console.log(k, user[k]); });
Object.entries(user).map(([k, v]) => { console.log(k, v) });

JavaScript: オブジェクトのあるキーを上書きする

1
2
3
4
5
6
7
const obj1 = {key1: "value1", key2: "value2"};
const obj2 = {key3: "value3"};
const obj3 = {...obj1, key1:"converted"};

{...obj1, ...obj2} // => { key1: 'value1', key2: 'value2', key3: 'value3' }

obj3 // => { key1: 'converted', key2: 'value2' }

JavaScript: オブジェクトの一部の要素しかほしくないとき

  • URL
  • 即時関数を使って切り出す
1
const sns = ((obj) => (obj.width))(someBigObject);

JavaScript: オブジェクトに対してmapしたいとき

  • 参考
  • 対象のオブジェクトobjに対してObject.keys(obj).map(home => some)となどすればよい
  • キーだけを得たいならkeys, 値だけならvalues, 両方はentries

JavaScript: シャローコピーはスプレッド構文で

1
2
3
4
constoriginal={a:1,b:2,c:3};
const copy = { ...original };
console.log(copy);            // { a: 1, b: 2, c: 3 }
console.log(copy===original); // false

JavaScript: ディープコピー

  • プロパティにDateオブジェクトや関数, undefinedなどがない場合はJSON.stringify()で書ける
  • 一般にはLodashなどライブラリを使う
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
const patty = {
  name: 'Patty Rabbit',
  email: 'patty@maple.town',
  address: { town: 'Maple Town' },
};
const rolley = JSON.parse(JSON.stringify(patty));
rolley.name = 'Rolley Cocker';
rolley.email = 'rolley@palm.town';
rolley.address.town = 'Palm Town';
console.log(patty);

//{
// name:'PattyRabbit',
// email:'patty@maple.town',
// address:{town:'MapleTown'}
//}

JavaScript: テーブルの行の並べ替え

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<!DOCTYPE html>
<html>
  <head>
    <title>Drag-and-drop Sortable List Demo</title>
    <meta charset="utf-8">
  </head>
  <body>
    <table>
      <thead>
        <tr>
          <th>EMAIL</th>
        </tr>
      </thead>
      <tbody id="products">
        <tr id="product-0" draggable="true">
          <td>jhon@gmail.com</td>
        </tr>
        <tr id="product-1" draggable="true">
          <td>marygirl@yahoo.com</td>
        </tr>
        <tr id="product-2" draggable="true">
          <td>cha24@yahoo.com</td>
        </tr>
      </tbody>
    </table>

    <script>
      let row;
      for (let i = 0; i < 3; i++) {
        const id = "product-" + i.toString();
        const tr = document.getElementById(id);
        tr.addEventListener("dragstart", event => {
          row = event.target;
        });
        tr.addEventListener("dragover", event => {
          const e = event;
          e.preventDefault(event);
          let children= Array.from(e.target.parentNode.parentNode.children);
          if(children.indexOf(e.target.parentNode)>children.indexOf(row)) {
            e.target.parentNode.after(row);
          } else {
            e.target.parentNode.before(row);
          }
        });
      }
    </script>
  </body>
</html>

JavaScript: 配列に対するrange, 反復処理

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
> [...Array(3)]
[ undefined, undefined, undefined ]

> [...Array(3)].map((_, n) => { console.log(`${n + 1} times`); });
1 times
2 times
3 times

> [...Array(3).keys()]
[0,1,2]

> [...Array(3).keys()].map((n) => { console.log(`${n + 1} times`); });
1 times
2 times
3 times

JavaScript: リストの並べ替え

  • URL
  • 2023/5時点では次のコードで動く.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
<!DOCTYPE html>
<html>
  <head>
    <title>Drag-and-drop Sortable List Demo</title>
    <meta charset="utf-8">
    <style type="text/css">
      .slist {
        list-style: none;
        padding: 0;
        margin: 0;
      }
      .slist li {
        margin: 10px;
        padding: 15px;
        border: 1px solid #dfdfdf;
        background: #f5f5f5;
      }
      .slist li.hint {
        border: 1px solid #ffc49a;
        background: #feffb4;
      }
      .slist li.active {
        border: 1px solid #ffa5a5;
        background: #ffe7e7;
      }
      * {
        font-family: Arial, Helvetica, sans-serif;
        box-sizing: border-box;
      }
    </style>
    <script src="sort-list.js"></script>
  </head>
  <body>
    <ul id="sortlist" class="slist">
      <li draggable="true" ondragstart="dragStart(event)" ondragenter="dragEnter(event)" ondragleave="dragLeave(event)" ondragend="dragEnd(event)" ondragover="dragOver(event)" ondrop="drop(event)">First</li>
      <li draggable="true" ondragstart="dragStart(event)" ondragenter="dragEnter(event)" ondragleave="dragLeave(event)" ondragend="dragEnd(event)" ondragover="dragOver(event)" ondrop="drop(event)">Second</li>
      <li draggable="true" ondragstart="dragStart(event)" ondragenter="dragEnter(event)" ondragleave="dragLeave(event)" ondragend="dragEnd(event)" ondragover="dragOver(event)" ondrop="drop(event)">Third</li>
      <li draggable="true" ondragstart="dragStart(event)" ondragenter="dragEnter(event)" ondragleave="dragLeave(event)" ondragend="dragEnd(event)" ondragover="dragOver(event)" ondrop="drop(event)">Forth</li>
      <li draggable="true" ondragstart="dragStart(event)" ondragenter="dragEnter(event)" ondragleave="dragLeave(event)" ondragend="dragEnd(event)" ondragover="dragOver(event)" ondrop="drop(event)">Fifth</li>
    </ul>

    <script>
      const target = document.getElementById("sortlist");
      let items = target.getElementsByTagName("li");
      let current = null;
      function dragStart(e) {
        current = e.target;
        for (let it of items) {
          if (it != current) { it.classList.add("hint"); }
        }
      }
      function dragEnter(e) {
        if (e.target != current) { e.target.classList.add("active"); }
      }
      function dragLeave(e) {
        e.target.classList.remove("active");
      }
      function dragEnd(){
        for (let it of items) {
          it.classList.remove("hint");
          it.classList.remove("active");
        }
      }
      function dragOver(e) { e.preventDefault(); }
      function drop(e) {
        e.preventDefault();
        if (e.target != current) {
          let currentpos = 0, droppedpos = 0;
          for (let it=0; it<items.length; it++) {
            if (current == items[it]) { currentpos = it; }
            if (e.target == items[it]) { droppedpos = it; }
          }
          if (currentpos < droppedpos) {
            e.target.parentNode.insertBefore(current, e.target.nextSibling);
          } else {
            e.target.parentNode.insertBefore(current, e.target);
          }
        }
      }
    </script>
  </body>
</html>

Jest: Macでテストが動かない, watchman warning

  • 参考
  • brew uninstall watchmanwatchmanをアンインストールする
  • React NativeのテストをJestで書く場合は対処が必要らしい

Jest: tsconfig.jsonpaths設定をテストでも通したい

  • URL
  • tsconfig.jest.jsonを作って次のように書く.
1
2
3
{
  "extends": "./tsconfig.json",
}
  • yarn add -D ts-jestを実行
  • jest.config.jsを次のように書く.
    • うまく動かない場合はprefix: "<rootDir>/src"/srcがあるかも確認
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
const { pathsToModuleNameMapper } = require("ts-jest"); // 追加
const { compilerOptions } = require("./tsconfig"); // 追加

module.exports = {
  roots: ["<rootDir>/src"],
  testMatch: ["**/__tests__/**/*.+(ts|tsx|js)", "**/?(*.)+(spec|test).+(ts|tsx|js)"],
  transform: {
    "^.+\\.(ts|tsx)$": ["ts-jest", { tsconfig: "tsconfig.jest.json" }]
  },
  moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, { prefix: "<rootDir>/src" }) // 追加
};

KaTeX: 導入

KaTeXへのマクロ導入

  • URL
  • \\gdefを使えばよい

LIFFアプリ初期化

1
npx @line/create-liff-app

LIFF: liff-inspector

1
npx @line/liff-inspector

LocalStorage

Vercelでエラーがときの対応

  • localStorage.getItem()はキーがないとnullを返す
  • 文字列が常に返ってくる前提でいるとエラーが出る
  • VercelだとApplication error: a client-side exception has occurred (see the browser console for more information).が出る
  • Firefoxだとまともなエラーメッセージが出ない可能性があるのでChromeで見てみよう

MISC

XAMPPでSSLを有効にする

(2021/9 時点で)httpsは標準的になっていて, 各所でhttpだといろいろな問題が起きる. localhostはいろいろと特殊という話もあり, 状況に応じて考えるべきだが, 現時点でここではWindowsでのいわゆる「オレオレ証明書」の発行とインストール法を書く. 特に次のページを参考にすればよい.

MUI HTML5 Validation

  • URL
  • 上記URLに沿って実装すればよい
  • 念のため下記に転載: const emailRef = useRef<HTMLInputElement>(null);の型づけが重要
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
import { Button, Container, TextField } from "@mui/material";
import React, { useRef, useState } from "react";

type OnChangeEvent = React.ChangeEvent<HTMLInputElement>;
const EmailVaildPattern =
  "^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:.[a-zA-Z0-9-]+)*$";

const App = () => {
  const emailRef = useRef<HTMLInputElement>(null);
  const passwordRef = useRef<HTMLInputElement>(null);
  const confirmPasswordRef = useRef<HTMLInputElement>(null);
  const [emailValue, setEmailValue] = useState("");
  const [passwordValue, setPasswordValue] = useState("");
  const [confirmPasswordValue, setConfirmPasswordValue] = useState("");
  const [emailError, setEmailError] = useState(false);
  const [passwordError, setPasswordError] = useState(false);
  const [confirmPasswordError, setConfirmPasswordError] = useState(false);

  const formValidation = (): boolean => {
    let valid = true;

    const e = emailRef?.current;
    if (e) {
      const ok = e.validity.valid;
      setEmailError(!ok);
      valid &&= ok;
    }
    const p = passwordRef?.current;
    if (p) {
      const ok = p.validity.valid;
      setPasswordError(!ok);
      valid &&= ok;
    }
    const c = confirmPasswordRef?.current;
    if (c) {
      if (confirmPasswordValue.length > 0 &&
          passwordValue !== confirmPasswordValue) {
        c.setCustomValidity("パスワードが一致しません");
      } else {
        c.setCustomValidity("");
      }

      const ok = c.validity.valid;
      setConfirmPasswordError(!ok);
      valid &&= ok;
    }

    return valid;
  };

  return (
    <Container component="main" maxWidth="xs">
      <TextField
        margin="normal"
        fullWidth
        required
        inputRef={emailRef}
        value={emailValue}
        error={emailError}
        helperText={emailError && emailRef?.current?.validationMessage}
        inputProps={ {required: true, pattern: EmailVaildPattern} }
        onChange={(e: OnChangeEvent) => setEmailValue(e.target.value)}
        label="Email"
      />

      <TextField
        margin="normal"
        fullWidth
        required
        type="password"
        inputRef={passwordRef}
        value={passwordValue}
        error={passwordError}
        helperText={passwordError && passwordRef?.current?.validationMessage}
        inputProps={ {required: true} }
        onChange={(e: OnChangeEvent) => setPasswordValue(e.target.value)}
        label="Password"
      />

      <TextField
        margin="normal"
        fullWidth
        required
        type="password"
        inputRef={confirmPasswordRef}
        value={confirmPasswordValue}
        error={confirmPasswordError}
        helperText={confirmPasswordError &&
                    confirmPasswordRef?.current?.validationMessage}
        inputProps={ {required: true} }
        onChange={(e: OnChangeEvent) => setConfirmPasswordValue(e.target.value)}
        label="Confirm password"
      />

      <Button
        variant="contained"
        fullWidth
        sx={ {mt: 3} }
        onClick={() => {              //  
          if (formValidation()) {
            alert("OK!");
          }
        }}
      >
        Register
      </Button>
    </Container>
  );
};

MUI ver5でstyledに引数を渡す

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
export default function Story({ image, profileSrc, title }) {
  console.log(image);
  console.log(`url(${image.imageurl ? image.imageurl : ""})`);
  return (
    <StoryWrapper imageUrl={`${image}`}>
      <Avatar src={profileSrc} className="story__avatar" />
      <h4>{title}</h4>
    </StoryWrapper>
  );
}

const StoryWrapper = styled("div")(({ imageUrl }) => ({
  backgroundImage: `url(${imageUrl})`
}));

Nest.js

  • 参考
  • うまく動かないのでfastifyではなく素のexpress利用
1
2
3
4
yarn prisma migrate dev --preview-feature
yarn prisma generate

nest g resource

Nest.js deploy

1
heroku addons:create heroku-postgresql:hobby-dev

Next.js node.jsなしでリリース, SSG

  • next exportに対応するコマンドでビルドすればよい: 特にSSG.
  • 参考: サブディレクトリにリリースするときはnext.config.jsに次の設定をつける
    • /subなどは適切なサブディレクトリを指定する
1
2
3
4
module.exports = {
  assetPrefix: "/sub",
  basePath: "/sub",
};

Next.js TypeScriptのプロジェクト生成

1
npx create-next-app sample-next-ts --typescript

Next.js 初期化

1
2
yarn create nx-workspace
yarn add @mui/material @emotion/react @emotion/styled @mui/styled-engine-sc styled-components

Next.js ビルド(エクスポート)

  • 設定: next.config.jsに設定
1
2
3
4
module.exports = {
  reactStrictMode: true,
  trailingSlash: true,
}
1
nx export phys --prod

Next.js ビルド時にFirebaseのfunctionsディレクトリを除外する

  • tsconfig.jsonexcludefunctionsを指定すればよい

node.js asdfの導入

  • Mac版の導入
1
2
3
brew install asdf
echo -e "\n. $(brew --prefix asdf)/libexec/asdf.sh" >> ~/.zshrc
exec $SHELL -l

node.js asdfnodeのランタイムバージョン指定でインストール

  • バージョンを指定したい場合は次のように確認できる
1
2
3
asdf plugin add nodejs
asdf plugin list
asdf list all nodejs
  • インストール自体はasdf install nodejs <version>
  • 最新版を入れるなら次の通り
1
2
3
asdf plugin install nodejs
asdf install nodejs latest
asdf global nodejs latest

node.js asdfでのnodeのランタイムバージョンをグローバルに指定

1
2
asdf global nodejs <version>
node -v

node.js asdfで特定ディレクトリでのバージョン指定

  • URL
  • .tool-versionsに記録される
1
2
asdf local nodejs <version>
node -v

node.js CSS導入

pico.cssでの例

1
2
npx create-next-app picosample --use-npm --example "https://github.com/vercel/next-learn/tree/master/basics/learn-starter"
npm install @picocss/pico

pages/index.jsimport '@picocss/pico/css/pico.min.css'を書けば反映される.

node.js docker軽量化

node.js dotenvを使う

1
npm i dotenv

node.jsのソースコード中では次の通り.

1
2
import * as dotenv from "dotenv";
dotenv.config(); // これがないと動かない

ReactやNext.jsで使っている場合は変数設定が特殊なので注意すること.

1
2
3
4
REACT_APP_API_BASE_URL=http://localhost:9000


NEXT_PUBLIC_FOO=hogehoge

React

  • 参考
  • .envの変数には必ずREACT_APP_をつけて次のように書く
1
2
3
REACT_APP_API_BASE_URL=xxxxxxxxxxxx
REACT_APP_FIREBASE_API_KEY=xxxxxxxxxxxx
REACT_APP_FIREBASE_AUTH_DOMAIN=xxxxxxxxxxxxxxxx
  • プログラム中ではprocess.env.REACT_APP_API_BASE_URLと書けば値を取れる

node.js package.jsonにあるパッケージのバージョンアップ方法

1
2
npm i -g npm-check-updates
ncu -u

イベントの型

親から子にハンドラ関数を渡す

関数にコンポーネントを渡す

  • 引数設定はiconで, コンポーネントを渡すときはicon={<InboxIcon />}のように書けばよい
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
type ItemProp = {
  to: string;
  text: string;
  icon: any;
};

function Item({ to, text, icon }: ItemProp) {
  return (
    <Link to={to} key={to}>
      <ListItem>
        <ListItemButton>
          <ListItemIcon>{icon}</ListItemIcon>
          <ListItemText primary={text} />
        </ListItemButton>
      </ListItem>
    </Link>
  );
}

export default function UpperLeftMenu() {
  return (
    <>
      <List>
        <Link to="/tempmenu/1" key="/tempmenu/1">
          <ListItem>
            <ListItemButton>
              <ListItemIcon>
                <InboxIcon />
              </ListItemIcon>
              <ListItemText primary="Home" />
            </ListItemButton>
          </ListItem>
        </Link>
        <Item to="/tempmenu/2" text="tempmenu/2" icon={<InboxIcon />} />
        <Item to="/tempmenu/3" text="tempmenu/3" icon={<InboxIcon />} />
      </List>
    </>
  );
}

node.js https.request, API呼び出しサンプル

GET

  • 何でもいいが, とりあえず使ったことがあるFlickrのAPIを実行する前提
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
const API_KEY = "some value"; // https://www.flickr.com/services/apps/create/apply/ でFlickrで取ってこよう

// Flickr画像データのURLを返す
const getFlickrImageURL = (photo, size) => {
  let url = `https://farm${photo.farm}.staticflickr.com/${photo.server}/${photo.id}_${
    photo.secret
  }`;
  if (size) {
    // サイズ指定ありの場合
    url += `_${size}`;
  }
  url += '.jpg';
  return url;
};

// Flickr画像の元ページのURLを返す
const getFlickrPageURL = (photo) => `https://www.flickr.com/photos/${photo.owner}/${photo.id}`;

// Flickr画像のaltテキストを返す
const getFlickrText = (photo) => {
  let text = `"${photo.title}" by ${photo.ownername}`;
  if (photo.license === '4') {
    // Creative Commons AttributionCC BYライセンス
    text += ' / CC BY';
  }
  return text;
};

// リクエストパラメータを作る
const paramObj = {
  method: 'flickr.photos.search',
  api_key: API_KEY,
  text: 'cat', // 検索テキスト
  sort: 'interestingness-desc', // 興味深さ順
  per_page: 12, // 取得件数
  license: '4', // Creative Commons Attributionのみ
  extras: 'owner_name,license', // 追加で取得する情報
  format: 'json', // レスポンスをJSON形式に
  nojsoncallback: 1, // レスポンスの先頭に関数呼び出しを含めない
};
let params = "";
params = Object.keys(paramObj).map(k => `${k}=${paramObj[k]}`).join("&");
const flickrUrl = `https://api.flickr.com/services/rest/?${params}`;
console.log(flickrUrl);

https.request(flickrUrl, { method: "GET" }, res => {
  console.log('statusCode:', res.statusCode);

  res.on('data', (d) => {
    process.stdout.write(d); // ここでリクエストの値が取れる
  });

  res.on("end", () => {
    console.log("END!");
  });
}).on("error", (error) => {
  console.log(`Error: ${error}`);
}).end();

node.js httpサーバーを立ち上げる

1
2
npm install -g http-server
http-server

node.js 運用モード, productionモード

  • アプリを運用モードで実行するには環境変数NODE_ENV=productionをセットして実行

Nx Cannot find module

  • 結論: 次のように最後に.jsをつけた
    • これで常に解決できるかは不明
1
- import renderMathInElement from "katex/dist/contrib/auto-render.js";

補足

  • 具体的にはkatex/dist/contrib/auto-renderで問題が出た.
  • node_modules配下にきちんとファイルはあった.
  • 何となく思い立ったため拡張子までつけてみたらどうかと思ってやってみて通った

Nx docker利用

Nx express静的ファイルの呼び出し

  • 2022-08-09検証
  • 参考
  • まとめ: 各プロジェクトのassets配下にファイルを置いて次のコードを追加
1
app.use(express.static(__dirname + "/assets"));

詳細メモ

  • ルート直下のworkspace.jsonを見る
  • projects > <package-name> > targets > build > options > assetsを見ると静的ファイルを置くべき場所が指定されている
  • 静的ファイルをpublicに置きたいならpublicまでのパスを指定すればよいらしい(未検証)
    • デフォルトのassetsに置いたときの挙動は確認

Nx Firebaseとの連携・デプロイ設定

  • URL: これに沿って設定すればよい
  • 上記の設定のもとでnx deploy functionsまたはyarn nx deployを実行すればデプロイできる
    • firebaseのfunctionsディレクトリにnodeの適切なバージョンを使う設定を入れること
    • asdfを使っているなら.tool-versions
  • 以下, 念のため実際の作業を転記

作業メモ: Cloud Functions

  • 対応するアプリケーション名はfunctionsで, apps/functionsディレクトリだとする
  • インストール
1
npm i -g firebase-tools
  • Firebaseにログイン・初期化: 適切なアカウントかどうかも確認
    • ログインユーザーの変更はFirebase関連の項を確認すること
1
2
3
firebase login:list
firebase login
firebase init
  • .gitignoreに下記を追加
1
2
3
4
firebase
firebase-debug.log*
firebase-debug.*.log*
.firebase/
  • プロジェクトがなければ次のコードでプロジェクトを生成
1
2
yarn add -D @nrwl/node
nx g @nrwl/node:application functions
  • firebase initが生成したpackage.jsonに含まれているパッケージのうち足りない分をNxワークスペースに追加
1
2
yarn add firebase-admin firebase-functions
yarn add -D firebase-functions-test
  • 次の作業用にパッケージ追加
1
yarn add -D depcheck
  • 次の内容でtools/scripts/build-firebase-functions-package-json.tsを作成
    • ワークスペースルートにあるpackage.jsonを元にデプロイ用のpackage.jsonを生成するスクリプト
      • Functionsのソースコードが依存していないパッケージを除外
      • Node.jsランタイムのバージョン指定を追加
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
import * as depcheck from 'depcheck';
import * as fs from 'fs';
import * as path from 'path';

import * as packageJson from '../../package.json';

const PACKAGE_JSON_TEMPLATE = {
  engines: { node: '16' }, // 適切なバージョンを指定する
  main: 'main.js',
};

async function main(): Promise<void> {
  const args = process.argv.slice(2);
  if (!args?.length || !args[0]) {
    throw new Error('Application name must be provided.');
  }

  const APPLICATION_NAME = args[0];
  console.log(`Application name: ${APPLICATION_NAME}`);

  /*****************************************************************************
   * package.json
   * - Filter unused dependencies.
   * - Write custom package.json to the dist directory.
   ****************************************************************************/
  const ROOT_PATH = path.resolve(__dirname + '/../..');
  const DIST_PROJECT_PATH = `${ROOT_PATH}/dist/apps/${APPLICATION_NAME}`;

  console.log('Creating cloud functions package.json file...');

  // Get unused dependencies
  const { dependencies: unusedDependencies } = await depcheck(DIST_PROJECT_PATH, {
    package: {
      dependencies: packageJson.dependencies,
    },
  });

  // Filter dependencies
  const requiredDependencies = Object.entries(packageJson.dependencies as { [key: string]: string })
    ?.filter(([key, _value]) => !unusedDependencies?.includes(key))
    ?.reduce<{ [key: string]: string }>((previousValue, [key, value]) => {
      previousValue[key] = value;
      return previousValue;
    }, {});

  console.log(`Unused dependencies count: ${unusedDependencies?.length}`);
  console.log(`Required dependencies count: ${Object.values(requiredDependencies)?.length}`);

  // Write custom package.json to the dist directory
  await fs.promises.mkdir(path.dirname(DIST_PROJECT_PATH), { recursive: true });
  await fs.promises.writeFile(
    `${DIST_PROJECT_PATH}/package.json`,
    JSON.stringify(
      {
        ...PACKAGE_JSON_TEMPLATE,
        dependencies: requiredDependencies,
      },
      undefined,
      2
    )
  );

  console.log(`Written successfully: ${DIST_PROJECT_PATH}/package.json`);
}

main()
  .then(() => {
    // Nothing to do
  })
  .catch(error => {
    console.error(error);
  });
  • 次のコマンドを実行
1
2
3
cp apps/functions/*.json tools/scripts
cp apps/functions/.eslintrc.json tools/scripts
cp apps/functions/jest.config.ts tools/scripts
  • tools/scripts/tsconfig.jsonに以下を追加
1
  "compilerOptions": { "resolveJsonModule": true, "module": "commonjs" },
  • workspace.jsonまたはプロジェクトのproject.jsonに追記
    • 大事なのはビルド設定
    • ビルドとfirebaseコマンドの組み合わせ, firebaseコマンドを実行するだけ
    • さらに念のためfirebase initが生成したpackage.jsonscriptsにある項目ものを移植
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
    // 最初の注意通り`apps/functions`にしている前提
    "functions": {
      // 
      "architect": {
        // build の項目を build-node に変更: build 時に他にも実行したことがあるのでサブコマンド扱いにする
        "build-node": {
          // 
        },
        // buildの項目を新しく追加: build-nodeを実行してTypeScriptビルドし, package.jsonを用意
        "build": {
          "builder": "@nrwl/workspace:run-commands",
          "options": {
            "commands": [
              {
                "command": "nx run functions:build-node"
              },
              {
                "command": "ts-node --project ./tools/scripts/tsconfig.json tools/scripts/build-firebase-functions-package-json.ts functions"
              },
              {
                "command": "cd dist/apps/functions && npm install --package-lock-only"
              }
            ],
            "parallel": false
          },
          "configurations": {
            "production": {
              "prod": true
            }
          }
        },
        // ビルドしてFirebase Emulatorを使って実行するよう修正
        "serve": {
          "builder": "@nrwl/workspace:run-commands",
          "options": {
            "command": "nx run functions:build && firebase emulators:start --only functions --inspect-functions"
          }
        },
        // ビルドしてfunctions:shellを使う設定を追加
        "shell": {
          "builder": "@nrwl/workspace:run-commands",
          "options": {
            "command": "nx run functions:build && firebase functions:shell --inspect-functions"
          }
        },
        // shell と同じ
        "start": {
          "builder": "@nrwl/workspace:run-commands",
          "options": {
            "command": "nx run functions:shell"
          }
        },
        // デプロイ設定を追加
        "deploy": {
          "builder": "@nrwl/workspace:run-commands",
          "options": {
            "command": "firebase deploy --only functions"
          }
        },
        // lint  test はそのまま
      }
    }
  • firebase initが生成したディレクトリ(ここではfunctions)を削除
  • firebase.jsonを編集
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
  // 作ったアプリケーション名で設定する
  "functions": {
    "source": "dist/apps/functions",
    "ignore": [
      "node_modules",
      ".git",
      "firebase-debug.log",
      "firebase-debug.*.log",
      ".eslintrc.json",
      "jest.config.ts",
      "tsconfig.app.json",
      "tsconfig.json",
      "tsconfig.spec.json"
    ],
    "predeploy": [
      "nx lint functions",
      "nx build functions"
   ]
  },
  • nx serve functionsで動作確認
  • nx deploy functionsでデプロイ・動作確認

Nx React+Fastify

  • 参考
  • 結論: 採用しない
  • fastifyのルートにReactを連携させる
  • フロントエンドとバックエンドを分けずに一体でリリースする
  • Nextだと使えなさそう

Nx storybookアプリケーション作成

1
nx g @nrwl/react:storybook-configuration --name=ui

Nx UIライブラリ追加

1
2
3
nx g @nrwl/react:lib ui

nx g @nrwl/react:component button --project ui

Nx アプリケーション削除

  • appsの下にあるモノがアプリケーション
  • 参考
  • URL
1
2
3
nx generate remove <project-name>

nx g rm <project-name>

Nx アプリケーション作成

  • next.jsアプリケーションを作る場合は次の通り.
1
nx generate @nrwl/next:application --name=society --dry-run --no-interactive

Nx アプリケーションのリネーム

1
2
nx g @nrwl/workspace:move --project oldNG newNG
nx g mv --project oldNG newN

Nx アプリケーションへのポート指定

  • URL
  • project.jsontargets.serve.option.portに指定する

Nx ビルド生成物に依存関係を抜き出したpackage.jsonを作成

  • URL
  • Nx自体が機能を持つ
  • workspace.jsonの各プロジェクトのconfigurations中, assetsの下に"generatePackageJson": trueを追記
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
             "outputPath": "dist/apps/api",
             "main": "apps/api/src/main.ts",
             "tsConfig": "apps/api/tsconfig.app.json",
-            "assets": ["apps/api/src/assets"]
+            "assets": ["apps/api/src/assets"],
+            "generatePackageJson": true
           },
           "configurations": {
             "production": {
             "outputPath": "dist/apps/html",
             "main": "apps/html/src/main.ts",
             "tsConfig": "apps/html/tsconfig.app.json",
-            "assets": ["apps/html/src/assets"]
+            "assets": ["apps/html/src/assets"],
+            "generatePackageJson": true
           },

Nx コマンドメモ・チュートリアルのメモ

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
npx create-nx-workspace my-project

yarn nx list # we can confirm options
yarn add @nrwl/react
yarn nx list @nrwl/react # check schematics
yarn nx g @nrwl/react:application --help # confirm options
yarn nx g @nrwl/react:application store --dry-run
yarn nx g @nrwl/react:application store



yarn nx run <proj>:<target>
yarn nx run store:serve
yarn nx run store:serve --port 3000 # we can set port in workspace.json
yarn nx run store:lint
nx serve store

yarn nx run store:serve # run react in tutorial
yarn add @mui/material

yarn nx g @nrwl/react:lib ui-shared --directory=store --dry-run
yarn nx g @nrwl/react:lib ui-shared --directory=store
yarn nx g @nrwl/react:component header --project=store-ui-shared

yarn nx g @nrwl/workspace:lib util-formatters --directory=store --dry-run
yarn nx g @nrwl/workspace:lib util-formatters --directory=store

yarn nx graph

yarn nx @nrwl/react:lib feature-game-detail --directory=store --appProject=store

yarn add -D @nrwl/express # 公式ページから調べて適切なコマンドを実行
yarn nx g @nrwl/express:application api --frontendProject=store --dry-run
yarn nx g @nrwl/express:application api --frontendProject=store
yarn nx run api:serve
yarn nx dep

yarn nx run-many --target=serve --projects=api,store --parallel=true

yarn nx run store:serveAppAndApi

yarn nx dep

yarn nx g @nrwl/workspace:lib util-interfaces --directory=api --dry-run
yarn nx g @nrwl/workspace:lib util-interfaces --directory=api

yarn add @nrwl/storybook -D
yarn nx list @nrwl/storybook
yarn nx g @nrwl/react:storybook-configuration store-ui-shared --configureCypress --generateStories --dry-run
yarn nx g @nrwl/react:storybook-configuration store-ui-shared --configureCypress --generateStories
yarn nx run store-ui-shared:storybook

yarn nx run store-ui-shared-e2e:e2e --watch
yarn nx run store-ui-shared-e2e:e2e --headless

yarn nx run store:test
yarn nx run store:test --watch

yarn nx run store:build --configuration=production

yarn nx run store:lint

yarn nx affected:dep-graph --base=<branch-name>
yarn nx affected:test --base=<branch-name>
yarn nx affected:lint --base=<branch-name>

yarn nx affected:test --all


yarn nx migrate latest

nx run my-js-app:build
nx build my-js-app
nx run-many --target=build --projects=app1,app2
nx run-many --target=test --all # Runs all projects that have a test target, use this sparingly.
nx affected --target=build# 毎回すべてのプロジェクトを実行するよりも効率的
nx generate workspace-generator my-generator # ワークスペース用のカスタムジェネレーター
nx migrate latest # Updates the version of Nx in `package.json` and schedules migrations to be run
nx migrate --run-migrations # Runs the migrations scheduled by the previous command.
nx graph
nx graph --watch # Updates the browser as code is changed
nx affected:graph # Highlights projects which may have changed in behavior
nx list
nx list @nrwl/react # Lists capabilities in the @nrwl/react plugin

Nx プロジェクト・ライブラリ内でのコマンド実行

  • cf: URL
  • トップのworkspace.jsonまたはプロジェクト・ライブラリのproject.jsonで次のように書く
    • 下記記述はworkspace.jsonの場合
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
"frontend": {
    "targets": {
        //...
        "ls-project-root": {
            "executor": "nx:run-commands",
            "options": {
                "command": "ls apps/frontend/src"
            }
        }
    }
}
  • 実行は次の通り
1
nx run frontend:ls-project-root

Nx プロジェクト作成

1
2
yarn create nx-workspace@latest
npx create-nx-workspace@latest --package-manager=yarn

ついでにmuiをインストールすると便利.

1
yarn add @mui/material @emotion/react @emotion/styled @mui/styled-engine-sc styled-components

Prisma

Prisma API setting

1
2
3
4
5
heroku create -a ys-nx-express-prisma
heroku config:set -a ys-nx-express-prisma PROJECT_NAME=express
heroku config:set -a ys-nx-express-prisma PORT=80

heroku buildpacks:add -a ys-nx-express-prisma heroku/nodejs

Prisma cf. command test

1
nx run prisma:ls

Prisma yarn

  • 次のコマンドで初期化する
1
2
3
yarn init
yarn add @prisma/client
yarn add -D @types/node prisma ts-node typescript
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
{
  "compilerOptions": {
    "sourceMap": true,
    "outDir": "dist",
    "strict": true,
    "lib": [
      "esnext"
    ],
    "esModuleInterop": true
  }
}
  • index.tsを作る
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import { PrismaClient } from "@prisma/client";

const prisma = new PrismaClient();

const userData = {
  data: {
    name: "Alice",
    email: "alice@prisma.io",
    posts: { create: { title: "Hello World" } },
    profile: { create: { bio: "I like turtles" } },
  },
};

async function main() {
  // await prisma.user.create(userData);
  const allUsers = await prisma.user.findMany({
    include: { posts: true, profile: true },
  });
  console.log(allUsers, { depth: null });

  const post = await prisma.post.update({
    where: { id: 1 },
    data: { published: true },
  });
  console.log(post);
}

main()
  .catch((e) => {
    throw e;
  })
  .finally(async () => {
    await prisma.$disconnect();
  });
1
2
3
4
  "scripts": {
    "dev": "npx ts-node index.ts",
    "start": "npx ts-node index.ts"
  }
1
web: yarn start
  • 次のコマンドでprismaを初期化
1
npx prisma init
  • prisma/schema.prismaを次のように設定する
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

model Post {
  id        Int      @id @default(autoincrement())
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
  title     String   @db.VarChar(255)
  content   String?
  published Boolean  @default(false)
  author    User     @relation(fields: [authorId], references: [id])
  authorId  Int
}

model Profile {
  id     Int     @id @default(autoincrement())
  bio    String?
  user   User    @relation(fields: [userId], references: [id])
  userId Int     @unique
}

model User {
  id      Int      @id @default(autoincrement())
  email   String   @unique
  name    String?
  posts   Post[]
  profile Profile?
}
  • 次のコマンドを発行する
1
npx prisma migrate dev --name init

Prisma カラムへの制約

  • 次のように書く
    • 注意: @mydbと書いた部分はdatasource mydbで指定したmydbにする
    • ネット上のサンプルだとよく@dbになっている
    • datasource dbを前提にしているのだろう
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
generator client {
  provider = "prisma-client-js"
}

// `mydb`はmysqlに作ったデータベース名を指定すること
datasource mydb {
  provider = "mysql"
  url      = env("DATABASE_URL")
}

model User {
  id       Int    @id @default(autoincrement())
  name     String @mydb.VarChar(255)
  email    String @unique @mydb.VarChar(255)
  password String @mydb.VarChar(255)
}

Prisma 初期化

1
2
3
4
5
nx run prisma:init


nx run prisma:migrate
nx run prisma:seed
  • TODO: 上記seed(または適切なdeploy?)への注意
    • 次の直接実行で無理やり実行できた
    • heroku run bash
    • libs/prismanpx prisma db seed実行
    • 当面は最悪これで実行
  • コマンドラインでnode --loader ts-node/esm libs/prisma/prisma/seed.tsを打つと通る
  • expressのmain.jsでポートの変数をprocess.env.portからprocess.env.PORTと大文字に変更する
  • Procfilerelease: npx prisma migrate deployを追記する
  • package.jsonに次の内容を追記する
1
2
3
4
5
{
  "prisma": {
    "schema": "libs/prisma/prisma/schema.prisma"
  }
}
  • 次のコマンドを実行する
1
heroku addons:create heroku-postgresql:hobby-dev

Prisma テストで使うデータベースを変える

  • 参考
  • テストする時に環境変数として接続文字列を指定する
1
2
3
  "scripts": {
    "test": "DATABASE_URL='mysql://root:root@localhost:3306/mydb_test' NODE_ENV=test jest"
  },

Prisma テストで実際にデータベースを読み書きする

  • 参考
  • 上記リンク先はPostgreSQL前提のようなのでMySQLなら次の通り
  • 下記の関数をbeforeEach()内で呼ぶ
    • resetTable(["User"])のように初期化したいテーブルを配列で指定する
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import { PrismaClient } from "@prisma/client";
import { Prisma } from ".prisma/client";
const prisma = new PrismaClient();

export const resetTable = async (modelNames: Prisma.ModelName[]): Promise<void> => {
  const tablenames = modelNames.map((modelName) => ({ tablename: modelName }));

  for (const { tablename } of tablenames) {
    try {
      await prisma.$executeRawUnsafe(`TRUNCATE TABLE ${tablename};`);
    } catch (error) {
      console.log({ error });
    }
  }
};

Prisma prismaを読み込むとts-node-devのリロードが走らない

Processing p5.js P5Wrapper Vectorを使う方法

  • URL
  • p5.constructor.[something]constructorを挟む

React create-react-appでTypeScriptプロジェクト生成

1
npx create-react-app {プロジェクト名} --template typescript

React+TypeScript useRefをTypeScriptで使うとObject is possibly 'null'

1
2
3
4
5
const refSample = useRef(null);                 # もとのコード
const refSample = useRef<HTMLDivElement>(null); # HTMLDivElementとは限らないのできちんと調べる

const resetScroll = (): void => refSample.current.scrollTo(0, 0); # もとのコード
const resetScroll = (): void => refSample.current?.scrollTo(0,0); # 修正版

React ID/PASSの自動入力を取れるinputの実装

  • きちんとvalueを使う
  • URL
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");

  return (
    <>
      <Box component="section" sx={{ display: "flex", flexDirection: "column", width: "400px", gap: "10px" }}>
        <TextField
          variant="outlined" label="Email"
          value={email} {/* ここが大事 */}
          onChange={ev => setEmail(ev.target.value)} />
        <TextField
          variant="outlined" label="Password" type="password"
          value={password} {/* ここが大事 */}
          onChange={ev => setPassword(ev.target.value)} />
      </Box>
    </>);

React KaTeX連携

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import React from 'react';
import PropTypes from 'prop-types';

import Markdown from 'markdown-to-jsx';
import renderMathInElement from 'katex/dist/contrib/auto-render';

const MarkdownViewer = ({ content }) => {
  const ref = React.useRef(null);

  React.useEffect(() => {
    if (ref.current) {
      renderMathInElement(ref.current, {
        delimiters: [
          { left: '$$', right: '$$', display: true },
          { left: '\\[', right: '\\]', display: true },
          { left: '$', right: '$', display: false },
          { left: '\\(', right: '\\)', display: false },
        ],
      });
    }
  }, [ref.current]);

  return (
    <div ref={ref}>
      <Markdown>{content}</Markdown>
    </div>
  );
};

MarkdownViewer.propTypes = {
  content: PropTypes.string.isRequired,
};

MarkdownViewer.defaultProps = {};

React react-hook-form TypeScript連携

React useEffectの第二引数

  • URL
    • 第二引数を空にするとレンダリング毎に実行
      • 一般に第二引数を空にするのは危険
      • コンポーネントはstatepropsなどに変更がある度にレンダリングされる
      • 予期せずuseEffectが実行されてしまう可能性がある
    • 第二引数を指定
      • 指定するが配列の中身は空: 初回のレンダリング後だけ実行
      • 実際に値を設定: 指定した値に変化があった時に実行
  • URL
    • useEffect(effect, deps);
    • useEffectは第一引数に関数effectを取り, 第二引数に配列depsを取る.
    • effect: そのコンポーネントが返す仮想DOMの差分が実DOMに反映された直後に実行される
    • depseffectが依存する値を書き込む
    • Reactdepsに渡された値が前回のレンダリング時と比べて更新されていた場合だけeffectを実行

React useEffectで画面がちらつくとき -> useLayoutEffect

  • URL
  • コールバックされるタイミングが違う

React コンポーネント設計の参考

React 配列やオブジェクトの更新とともにUIも更新したいとき

  • 参考
  • 次の二つの方法がある.
    • 新しい値を変数で保持する
    • 関数型の更新を使う

新しい値を変数で保持する

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import {useState} from "react"

export default function Index () {
  const [count, setCount] = useState(0);
  const onButtonClick = () => {
    const new_count = count + 1;
    setCount(new_count);
    alert(`count:${new_count}`);
  }

  return (
    <div>
      <button onClick={onButtonClick}>押して</button>
    </div>
  )
}

関数型の更新を使う

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import {useState} from "react"

export default function Index () {
  const [count, setCount] = useState(0);
  const onButtonClick = () => {
    setCount((pre_count) => pre_count + 1)  //初回クリック時のpre_countは0
    setCount((pre_count) => {               //初回クリック時のpre_countは1
      alert(`count:${pre_count}`);
      return pre_count
    })
  }

  return (
    <div>
      <button onClick={onButtonClick}>押して</button>
    </div>
  )
}

React Native

iOS, for Mac

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
brew install node
brew install watchman
sudo gem install cocoapods


sudo arch -x86_64 gem install ffi
arch -x86_64 pod install

rbenv install 2.7.4

npx react-native init AwesomeProject
npx react-native init AwesomeTSProject --template react-native-template-typescript

npx react-native start # at the directory, AwesomeProject
cd ios
pod install
cd /path/to/AwesomeProject
npx react-native run-ios # in another terminal

RESTクライアント

  • VSCode REST Client
  • Emacs http
    • C-c C-cでそのときにいるブロックのリクエストを発動できる
  • 拡張子.httpのファイルを作ってその中に次のような形式で書けばよい
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
### はコメント
### GETリクエストは単純に1行でよい
###
### 自分のIPアドレス
GET https://httpbin.org/ip

### xmlを表示する
GET https://httpbin.org/xml

### ヘッダをつける
POST https://httpbin.org/post?foo=bar
User-Agent: Emacs24

### POSTリクエスト
### ヘッダとデータの間には空行を入れる
POST https://httpbin.org/post?val=key
User-Agent: Emacs24
Content-Type: application/json

{
  "foo": "bar"
}

styled-componentsでのReceived "true" for a non-boolean attributeのようなエラー対応

  • 参考
  • TODO: その他も追記
  • 結論: 次のように自分で追加したpropとそれ以外を分離すればよい
    • ({isOpen, ...props})の部分
1
2
3
4
5
6
7
const SidebarContainer = styled('aside')(({isOpen, ...props}) => {
  return {
    position: 'fixed',
    opacity: isOpen ? '90%' : '0',
    top: isOpen ? '0' : '-100%',
  };
});

TypeScript Eventの型

1
2
3
4
5
6
7
8
9
type Props = {
  onClick: (event: React.MouseEvent<HTMLInputElement>) => void
  onChange: (event: React.ChangeEvent<HTMLInputElement>) => void
  onkeypress: (event: React.KeyboardEvent<HTMLInputElement>) => void
  onBlur: (event: React.FocusEvent<HTMLInputElement>) => void
  onFocus: (event: React.FocusEvent<HTMLInputElement>) => void
  onSubmit: (event: React.FormEvent<HTMLFormElement>) => void
  onClickDiv: (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => void
}

TypeScript querySelectorを使うときの型引数指定, TS2339: Property 'style' does not exist on type 'Element'.

  • 参考
  • この要素は絶対存在するからnullabilityを除去したい: は後置演算子!を使う
1
2
3
4
5
6
7
8
9
const foo = document.querySelector<HTMLElement>('.foo');
if (foo) {
    foo.style.display = '';
}

const bars = document.querySelectorAll<HTMLInputElement>('input[name="bar"]');
bars.forEach((bar) => {
    console.log(bar.value);
});

TypeScript React開発時のprops.childrenの型

1
2
3
4
5
6
7
8
import { ReactNode } from 'react';

function Parent({ children }: {
  children: ReactNode
}) {
  return (
    <div>{ children }</div>
  );

TypeScript windowオブジェクト未定義になった場合の対処, Property 'gtag' does not exist on type 'Window & typeof globalThis'. TS2339

  • URL
  • 次のエラーが出たときの対処
1
Property 'gtag' does not exist on type 'Window & typeof globalThis'.  TS2339
  • まず@types/gtag.jsをインストールする
1
yarn add -D @types/gtag.js
  • tsconfig.jsontypesを確認する
    • 未設定なら多分大丈夫
    • どこかで設定しているなら"gtag.js"を追加する
  • もう一度ビルドや実行する

TypeScript tsconfig.jsonメモ

  • 適宜更新する予定
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
{
  "compilerOptions": {
    "target": "es2020",
    "module": "esnext",
    "lib": [
      "es2020",
      "dom",
      "dom.iterable"
    ],
    "jsx": "preserve",
    "sourceMap": true,
    "outDir": "./dist",
    "rootDir": "/",
    "strict": true,
    "moduleResolution": "node",
    "esModuleInterop": true,
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "allowJs": true,
    "noEmit": true,
    "incremental": true,
    "resolveJsonModule": true,
    "isolatedModules": true
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
  "exclude": ["dist", "node_modules"],
  "compileOnSave": false
}

TypeScript tsconfig.jsonで設定したパスをts-nodeで使う

  • URL
  • yarn add -D tsconfig-pathsでインストール
  • 実行コマンドをts-node -r tsconfig-paths/register src/index.tsにする
    • 特に-r tsconfig-paths/registerを追加

TypeScript イベントの型

  • 都度調べる必要がある: 例を一つ挙げておく
1
handleDelete: (e: React.MouseEvent<HTMLInputElement>) => void;

TypeScript インターフェースでのインデックスシグネチャ

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
interface Status {
  [parameter: string]: number;
}
const myStatus: Status = {
  level: 22,
  experience: 3058,
  maxHP: 156,
  maxMP: 174,
  attack: 39,
  defense: 25,
};

TypeScript 2021/12 Jestで型安全にモックを書く

TypeScript 型ガード

  • あるスコープ内での型を保証する式
1
2
3
4
5
const foo: unknown = '1,2,3,4';

if (typeof foo === 'string') { console.log(foo.split(',')); }

console.log(foo.split(',')); //コンパイルエラー

TypeScript 型定義の調査, TypeSearch

TypeScript 環境変数に型をつけつつ.envを脱却する

TypeScript 関数型

1
2
// type 型の名前 = (引数名: 引数の型) => 戻り値の型;
type Increment = (num: number) => number;

TypeScript 絶対パスでインポート

  • tsconfig.jsonで次のように指定すると@/path/toで指定できる
1
2
3
4
5
6
7
  "compilerOptions": {
    "rootDir": "./src", // 必要に応じて設定
    "baseUrl": "./src",
    "paths": {
      "@/*": ["*"]
    }
  }

yarn package-lock.jsonからyarn.lockに移行

  • URL
  • yarn importを実行すればよい

yarn typesync

  • yarn add -D typesyncでインストール
  • scripts"preinstall": "typesync || :"を追加

yarn クリーンインストール

1
2
rm -rf node_modules
yarn cache clean

yarn パッケージの更新: yarn upgrade <package-name>

  • 全部最新化するのはyarn upgrade --latest

yarn バージョン指定でインストール

1
yarn add <package-name>@<version>

yarn viteで初期化

1
yarn create vite --template react-ts

Heroku

Heroku デプロイの参考

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
yarn create nx-workspace --package-manager=yarn nx-fullstack
nx generate @nrwl/node:application api

yarn nx run-many --target=serve --projects=nx-fullstack,api --parallel=true




"build": "yarn nx run-many --target=build --projects=nx-fullstack,api --parallel=true"


"start": "node dist/apps/api/main.js"

heroku login
heroku create


web: yarn start


git push heroku master


heroku open

Heroku バックエンドの設定

1
2
3
heroku create -a ys-jssamples-api
heroku config:set -a ys-jssamples-api PORT=80
heroku buildpacks:add -a ys-jssamples-api heroku/nodejs

HTML

CSSをHTMLに埋め込むときのコード片

ふだん書かないので余計に覚えていられません.

直接HTMLに書くとき

1
2
3
<style type="text/css">
/* some code */
</style>

外部CSSを読み込むとき

1
<link rel="stylesheet" type="text/css" href="sample.css" />

PDFをHTMLに埋め込む方法

いろいろありますが Google Document の URL で PDF を埋め込むのが便利です.

1
2
3
4
<iframe
src="http://docs.google.com/gview?url=http://www.soumu.go.jp/main_sosiki/joho_tsusin/top/ninshou-law/pdf/law_1.pdf&embedded=true"
style="width:100%; height:80vh;" frameborder="0">
</iframe>

ここで ?url= の指定を適当な URL に変えると PDF が埋め込めます.

ngrok

ngrok CORS設定

  • 参考
  • フロント・バックエンドともに設定が必要.
  • フロントエンドで次のように設定する:
    • axios前提の設定.
    • 他のライブラリやAPIでも同じように設定すればよいはず.
1
2
3
4
5
import axios from "axios";
axios.defaults.baseURL = process.env.NEXT_PUBLIC_SERVER_URI;
axios.defaults.headers.post['Content-Type'] = 'application/json;charset=utf-8';
axios.defaults.headers.post['Access-Control-Allow-Origin'] = '*';
axios.defaults.withCredentials = true;
  • バックエンドで次のように設定する: 例はexpress.
1
2
3
4
5
6
7
import * as cors from "cors";
const app = express();
app.use(cors({
  origin: true,             // 許可するフロントエンド側のURL設定
  credentials: true,        // レスポンスヘッダーにAccess-Control-Allow-Credentials追加
  optionsSuccessStatus: 200 // レスポンスstatusを200に設定
}));

ngrok 開発中のローカルのサーバーをhttp/httpsで公開できる

1
ngrok http <port> --region ap

ngrok 設定ファイル置場確認

  • 次のコマンドを実行するとアップグレードとともに設定ファイルの場所がわかる
  • 2022/7時点でMacだとホーム直下には作られないので注意しよう
1
ngrok config upgrade

PWA

PWA サブディレクトリにリリース: Next.js前提

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
{
  "name": "Gallery for Physics",                                        //適宜修正
  "short_name": "gallery-phys",                                         //適宜修正
  "description": "物理学ギャラリー. 基礎方程式や物理学者名言集を収録.", //適宜修正
  "start_url": "/service/phys-gallery",                                 //適宜修正
  "display": "standalone",
  "orientation": "portrait-primary",
  "background_color": "#fff",
  "theme_color": "#fff",
  "dir": "itr",
  "icons": [
    {
      "src": "icon-192x192.png", //適宜修正, ルートディレクトリは`start_url`で指定しているのでそこからのパスを指定すればよい
      "sizes": "192x192",
      "type": "image/png"
    }
  ]
}
  • next.config.jsに次のように追記
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// eslint-disable-next-line @typescript-eslint/no-var-requires
const withNx = require('@nrwl/next/plugins/with-nx');
const withPWA = require("next-pwa");
const runtimeCaching = require("next-pwa/cache");

const SUB_DIRECTORY = "/service/phys-gallery";        // 適宜修正
const isProd = process.env.NODE_ENV === "production";

/**
 * @type {import('@nrwl/next/plugins/with-nx').WithNxOptions}
 * @type {import('next').NextConfig}
 **/
const nextConfig = {
  nx: {
    // Set this to true if you would like to to use SVGR
    // See: https://github.com/gregberge/svgr
    svgr: false
  },
  reactStrictMode: true,
  trailingSlash: true
};

module.exports = withNx(nextConfig);
module.exports = withPWA({
  pwa: {
    disable: process.env.NODE_ENV !== 'production',
    dest: "public",
    register: true,
    skipWaiting: true,
    runtimeCaching,
    fallbacks: {
      document: `${SUB_DIRECTORY}/_offline.html`
    }
  },
  assetPrefix: isProd ? SUB_DIRECTORY : "",
  basePath:    isProd ? SUB_DIRECTORY : ""
});
  • pages配下に_document.tsxを次のような内容で設置
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import Document, {Head, Html, Main, NextScript} from "next/document";

class MyDocument extends Document {
  render() {
    return (
      <Html>
        <Head>
          <title>物理学ギャラリー</title>
          <link rel="shortcut icon" type="image/vnd.microsoft.icon" href="favicon.ico"/>
          <link rel="icon" type="image/vnd.microsoft.icon" href="favicon.ico"/>
          <link rel="apple-touch-icon" sizes="180x180" href="apple-touch-icon-180x180.png"/>
          <link rel="icon" type="image/png" sizes="192x192" href="icon-192x192.png"/>
          <link rel="manifest" href="manifest.json"/>
        </Head>
        <body>
        <Main/>
        <NextScript/>
        </body>
      </Html>
    );
  }
}

export default MyDocument;
  • pages配下に_offline.tsxを次のような内容で配置
1
2
import Index from ".";
export default Index;

PWA ファビコン生成

PWA メタ情報設定

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<meta name='twitter:card' content='summary' />
<meta name='twitter:url' content='https://yourdomain.com' />
<meta name='twitter:title' content='PWA App' />
<meta name='twitter:description' content='Best PWA App in the world' />
<meta name='twitter:image' content='https://yourdomain.com/icons/android-chrome-192x192.png' />
<meta name='twitter:creator' content='@DavidWShadow' />
<meta property='og:type' content='website' />
<meta property='og:title' content='PWA App' />
<meta property='og:description' content='Best PWA App in the world' />
<meta property='og:site_name' content='PWA App' />
<meta property='og:url' content='https://yourdomain.com' />
<meta property='og:image' content='https://yourdomain.com/icons/apple-touch-icon.png' />

Vercel

Vercel フロントエンドNext.jsのデプロイ参考

Vercel リリース用の設定

  • cf. deploy
  • GitHub連携する
  • Settingsタブを開く
  • Build & Development Settingsを開く
  • BUILD COMMAND: yarn build --prod
    • package.jsonbuildを正しく設定している前提
  • OUTPUT DIRECTORY
    • yarn: dist/apps/line-minapp-sample/.next
      • appsの下のディレクトリは適宜適切な設定を選ぶ
    • nx: TODO

Vercel リリース時の環境変数設定

  • Settingsからの左メニューでEnvironmental Veriablesから設定

Zoom 他の人も画面共有できるようにする

  • cf. URL
  • ミーティング中はメニューの「セキュリティ」から「画面共有」にチェックを入れる

フォント

  • Windows 8.1以降とOS X Mavericks(10.9)以降: 「游ゴシック体」と「游明朝体」が共通で収録
  • Noto Sans(源ノ角ゴシック): 日中韓3か国語に対応したオープンソースフォント