自分向けの単語勉強アプリを作った

起こったこと

応用情報の勉強しているが、単語がさっぱり覚えられない

試験日直前に一括で覚えるとして、どうやって勉強するかといろいろ考えたとき、PCで文章を登録できてスマホで勉強できるアプリが欲しいと思った。

また、既存のアプリは四択のものが多く、その選択肢から答えを推測できてしまう。応用情報技術者試験の午後問題では単語を記述させることが多いため、推測させるのではなく、初めから記述で回答するアプリが欲しいと思った。

作ったもの

DBを用意し、問題コンテンツを作成できるCMSを作成し、そのDBから問題を出力するアプリを作った。

問題コンテンツを作成できるCMS

問題アプリ。

おおやけ向けに作っていないのでいくつかUI的に怪しいが動くのでいったんはこれでよし。

作るのに使用したもの

PaasはHeroku

Postgresが今現在(2022/9/24)で無料であるこということとGithubアクションで自動デプロイしたかったから、というのが理由。

なお、Herokuは2022/11/28に無料版はなくなるみたい。

blog.heroku.com

Nodejs,Express,TypeOrm

本当は作成を思い立った時からMVCフレームワークを使いたかったのだがNodejsにMVCがなかった。正確にはいくつかあったのだけど情報収集が大変そうに感じた。結局Expressを使うことにしたのだけど、Expressはわからないことを検索すればいくつか記事が出てきてくれるのでこれは正解だった。一方でコントローラーに処理を少し書いてしまっている現状がある。リファクタリングしたいけどテストコードは書いていない。絶対よくないよね、これ。

DB操作関連のものはORMを使おうと思った。SequelizeかTypeOrmかで迷ったが、TypeOrmのほうがTypescriptの型を書きやすいという情報があったのでこちらを選択した。ORMそのものが今回が初めてだった。

正直TypeOrmは大変で、ネット検索をかけても古いバージョンの記述が多く、解決するのに時間がかかることが多かった。

UIはBulma

Bulmaにしたのは経験があるから。マテリアルUIでもよかったかもしれない。よく調べずに導入してしまった。導入しておいて書くのも悪いんだけど<button class="button"></button>と要素名と同じクラス名を書くのはなんだかなぁと思った。

VSCodeとかWSL2とかDockerとか使ったらPCが重い

VSCodeでコードを書き、WSL2でLinux環境で作成し、Dockerを使ってPostgresを構築した。DockerComposeを使っているので立ち上げも閉じるのも簡単だった。ただ、メモリが6GBのPCにはこれらの環境はとても辛かった。GPU向けの1.5GBを通常のメモリ使用に変更するなどで頑張ったが、やはり仮想環境マシマシには耐えられないようだった。なお、メモリ交換をしたらハードウェアエラーを起こした。保証も切れた。最初からメモリ交換できるタイプのPCにするか、高スペックのものを用意しましょう。

作るまで何日かかったか

3週間。ただ、平日は当然仕事をしているので実質的には6日間。応用情報の勉強と家族サービスもあるので、実際の工数はもうちょっと少ない。

作ったものは公開するか

いくつか課題があるので少なくとも直近は公開しない。前述の通り、UIがひどいというのもそうだけど、コンテンツがしょぼい。UIもコンテンツもしょぼいサービスは不安感しかない。不安感しかないから誰も使えない。どっちかの強化を図ってからことにあたりたい。

また、DBから直でデータを読みに行っている仕組み上、DBがボトルネックになることは必然だしセキュリティ的にも好ましくない。フロントで完結するような仕組みを考えるか、KVSにするかもしれない。

まずは

応用情報技術者試験に受かりたいなぁ。受かったら自慢できるしこのアプリの有用性を証明できるんだけどねぇ。

TypeormからHerokuのPostgressに繋ぐときに色々と詰まったところ

Version情報

おおよそ2022年9月ごろ時点のお話

"pg": "^8.8.0",
"typeorm": "^0.3.9"

heroku/7.63.0 wsl-x64 node-v14.19.0

error no pg_hba.conf entry for host

どういうタイミングで出力されていたかは定かじゃないが、error no pg_hba.conf entry for hostというエラーがでてアプリが立ち上がらなかった。

ssl: trueだけだと今度はself signed certificateと表示されエラーとなった。

参考になったサイト

github.com

sslの設定のほかにrejectUnauthorized: falseの設定が必要だったようだ(これで本当にいいのかはちょっとわからんが。。。)

ssl: trueにすると今度はローカルでのテスト時に影響があったので僕は以下のような形にしてみた。本番環境ではNODE_ENVをprodcutionにするということを前提があるので注意。

  const option: DataSourceOptions = {
    ...
-   ssl: false,
+   ssl: process.env.NODE_ENV === "production",
+   extra:
+     process.env.NODE_ENV === "production"
+       ? { ssl: { rejectUnauthorized: false } }
+       : undefined,
    ...

syntaxerror: cannot use import statement outside a module

migration:runを実施するタイミングでsyntaxerror: cannot use import statement outside a moduleというエラーが出た。エラー吐くタイミングで落ちるので当然アプリとして立ちあらがず。

参考になったサイト

dev.classmethod.jp

typescriptで開発していると起きうるケースのようだ。ビルド後のマイグレーションファイル(js)を読みにいくように設定する必要があるとのこと。ts-nodeの設定を変えて解決する方法もあるようだ。

qiita.com

syntaxerror: cannot use import statement outside a module typeormぐぐるとたくさん出てくるので参考にしてみてほしい。

www.google.com

僕はこういう風に変えた

  const option: DataSourceOptions = {
    ...
    migrations: [
      __dirname + "/migrations/**/*.ts",
+     __dirname + "/migrations/**/*.js",
    ],
    ...

サンダルで靴屋に行ったけど試着時に靴下を貸してくれた

起こったこと

妻がサンダルを履いて買い物に出かけた。その際に靴を買うことになったが、靴下を履いていない。直で試着するのは憚れるところであった。

靴下を借りれた

「靴下がないので試着はちょっと遠慮したいです」と言ったところ「でしたら、靴下お貸ししますか?」と答えてもらえた。靴下を借りて試着を成功することができた。

htmlでホバー時に文字サイズを大きくする際、下の要素がずれるのをやめたかった

起こったこと

ホバー時に文字を大きくする処理を以下のようにCSSで書いた。

<style>
    .hogehoge {}

    .hogehoge:hover {
      font-size: 130%;
    }
</style>
<div class="hogehoge">
  文字を大きく
</div>
<p>
  ここはそのまま
</p>

しかし、対象となる要素が増加した文字サイズ分広がっていて、下にある要素も文字サイズが増えた分下がる。かくかくしていて見栄えが良くなかった。

対処した方法

transform: scale()を使うことにした。transform-originの設定も色々と変えると大きくなる起点も変えられる。うまくいかないときはこれも変えてみること。

developer.mozilla.org

<style>
    .piyopiyo {}

    .piyopiyo:hover {
      transform: scale(1.3);
      transform-origin: left;
    }
</style>
<div class="piyopiyo">
  文字を大きく
</div>
<p>
  ここはそのまま
</p>

PlayWright+MonoRepoはどうするのがよいのだろう

monorepoとE2E

コンポーネントを共有するようなWebアプリケーションmonorepoがあるとして、これらはどのようにするのが良いのか悩んでいる(現在進行形)

こういうのにしてみたけど

monorepo/
├── common # コンポーネントなど、projectsが呼び出す共通のデータが格納されているディレクトリ
├── e2e # テストが入ったディレクトリ
└── projects # プロジェクトのモノレポ
    ├── hogehoge # hogehogeプロジェクト
    └── piypiyo # piyopiyoプロジェクト

hogehogeとpiyopiyoは似通ったシステムであり共通しているデータもあるが、それぞれ独自の機能も存在している。E2Eテストを書く場合はプロジェクトごとに書く必要がある。なお、すべてnode.js、またはtypescriptで記述されている。

e2eディレクトリには共通するテストを書き、それらは関数にて呼び出すようにする。

// e2e/toppage.test.ts
import { test, expect, Page } from "@playwright/test";
export const toppageTest = (specData: any) => {
  test("トップページテスト1", async ({ page }) => {
    await page.goto("/");
    // ...
  } );
}

projectはこれをimportして実行する。また、projectごとに行いたいテストはe2eディレクトリのnode_modules内のplaywrightをimportし、テストケースを記述する。

// projects/hogehoge/e2e/toppage.test.ts
import { toppageTest } from "../../../e2e/toppage.test;
import { test, expect, Page } from "../../../e2e/node_modules/@playwright/test";

const specData = {}; // hogehogeっぽいデータ

toppageTest(specData); // 共通のテスト

test("hogehogeプロジェクトのトップページ特有のテスト", async ({ page }) => {
  // hogehogeプロジェクトのトップページ特有のテスト
});

これらはtypescriptで記述してあるので、tsconfig.jsonのpathsで設定を変えてあげた。

{
  "compilerOptions": {
    "baseUrl": "./",
    "paths": {
      "@e2e": ["../../../e2e/*"],
    },
  }
}

とりあえずこういう形になった。

// projects/hogehoge/e2e/toppage.test.ts
import { toppageTest } from "@e2e/toppage.test;
import { test, expect, Page } from "@e2e/node_modules/@playwright/test";

const specData = {}; // hogehogeっぽいデータ

toppageTest(specData); // 共通のテスト

test("hogehogeプロジェクトのトップページ特有のテスト", async ({ page }) => {
  // hogehogeプロジェクトのトップページ特有のテスト
});

なんかやりかたに進展があったらまた書いてみます。

Playwrightで非同期でボタンを複数押そうとしたら安定しなかった

version情報

"@playwright/test": "1.22.2"

起こったこと

Webアプリケーション内にあるボタンを二つ押すと遷移できるページで、非同期で二つのボタンを押そうとしたけど出来なかった。

前提

例えば、ボタン1とボタン2を押すと下の要素に「二つのフラグが立ちました」と表示されるWebアプリケーションがあるとする。

(ごちゃごちゃして申し訳ない)

コードではこう書いた

test("ボタンを非同期で二つ押す", async ({ page }) => {
  await page.goto("/");
  const btn1 = page.locator(`text="ボタン1"`);
  const btn2 = page.locator(`text="ボタン2"`);
  const btns = [btn1, btn2];
  await Promise.all(btns.map(async (b) => await b.click()));
  await expect(page.locator(".flager")).toHaveText(/二つのフラグが立ちました/);
});

数回このコードで実行してみたが、Chromiumeだけ成功するパターン、firefoxだけ成功するパターンなど、いろいろと不思議なことが起きた。

こうした

test("ボタンを順繰りで二つ押す", async ({ page }) => {
  await page.goto("/");
  const btn1 = page.locator(`text="ボタン1"`);
  const btn2 = page.locator(`text="ボタン2"`);
  const btns = [btn1, btn2];
  for (let btn of btns) {
    await btn.click();
  }
  await expect(page.locator(".flager")).toHaveText(/二つのフラグが立ちました/);
});

こう書いたら安定した。

アプリ側の実装はこのような感じ

let flag1 = false;
let flag2 = false;
const btn1 = document.getElementById("button1");
const btn2 = document.getElementById("button2");

const flag1AndFlag2 = () => {
  if (flag1 && flag2) {
    const p = document.querySelector(".flager") as HTMLParagraphElement;
    p.innerText = "二つのフラグが立ちました";
  }
};

btn1?.addEventListener("click", () => {
  flag1 = true;
  flag1AndFlag2();
});
btn2?.addEventListener("click", () => {
  flag2 = true;
  flag1AndFlag2();
});

jszipで圧縮してもサイズ変わらない感じあった

version情報

    "typescript": "^4.7.4"
    "jszip": "^3.10.1"

起こったこと

jszipを使ってzipデータを作成しようとしたらあんまり圧縮されてる感じがなかった。 stuk.github.io

import fs from "fs";
import jszip from "jszip";
import { text } from "./text"; // 60,000文字ぐらいのテキスト

const zip = new jszip();
zip.file("test.txt", text);

zip.generateAsync({ type: "nodebuffer" }).then((blob) => {
  fs.writeFileSync("test.zip", blob);
});

zipデータ

というかオプション当てないとどうも無圧縮っぽい。

オプション当てた

{ compression: "DEFLATE"}というオプションを当てた

stuk.github.io

defaultがSTORE (no compression)って書いてあった

zip.generateAsync({ type: "nodebuffer", compression: "DEFLATE" }).then((blob) => {
  fs.writeFileSync("test.zip", blob);
});

およそ七割が圧縮された