vocab with context アプリ開発記録

# アプリの概要
- 文章を読みながら外国語の語彙を増やす
- アプリに外国語のテキストを貼り付ける → 覚えたい単語をハイライト → 単語の意味が表示される (現在は仏→英に対応)
- Github: https://github.com/jesuissuyaa/tangochou

# スクリーンショット
![0914 vocab with context (仮).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/430849/e8460692-93f2-7292-9737-1397205939a0.png)
![0914 vocab with context (仮) (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/430849/18e3feaa-727a-8706-5f76-762bf77f7e6d.png)
![0914 vocab with context (仮) (2).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/430849/0629e708-44f8-57d5-4996-7d070b68b7c8.png)
![0914 vocab with context (仮) (3).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/430849/2d821e30-73ea-1f46-defd-8c1af1fb0f74.png)

# 仕様
## 構成図

![0914 vocab with context (仮) (4).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/430849/7ab445e6-bb5a-bf14-0e5a-faa07db6afb3.png)

- 画面
- データベース: 以下のデータを保存
    - ユーザの入力した単語
    - ユーザがハイライトした単語
    - 外部サイトから取得した単語の意味
- API: 外部の辞書サイトから単語の意味をスクレイピング→アプリに返す

## ライブラリ & フレームワーク
- React
- Next.js: `create-next-app`でブートストラップ (https://nextjs.org/docs#quick-start)
- Typescript
- [json-server] (https://www.npmjs.com/package/json-server): JSON形式でデータを読み書きできるデータベース
- [cheerio] (https://www.npmjs.com/package/cheerio): スクレイピング用のパッケージ; jQueryの書き方でスクレイピングできる点がわかりやすい
- [request] (https://www.npmjs.com/package/request): APIで外部サイトにリクエストするために使用
- [isomorphic-unfetch] (https://www.npmjs.com/package/isomorphic-unfetch): json-serverでデータを読み書きするときに`fetch`を使用


## アクティビティ図
![tangochou.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/430849/46864827-83b2-69fc-faa0-0ee45bb8deb1.png)

# 開発のポイント
## APIを自分で書きました
### 経緯
最初は翻訳のライブラリかAPIを使うことを考えていましたが、

- 一つの単語に対して複数の意味を返してくれるものが見つからない
- APIキーの取得が大変
- 有料の場合もある
- 単語単位でなく文章単位の翻訳が想定されていることが多い

ということから、自分でAPIを書くことにしました

### セットアップ
- 公式ドキュメンテーションを読む　(https://nextjs.org/docs#api-routes)
- API用のフォルダを作る

```bash
/ # ルートディレクトリ
 └ pages
    └ api
      └ # api用の.jsファイルを置く
```

`/pages/api`に置いたAPIは、`/api/*`にアクセスすることで叩くことができます
例: `/pages/api/dictionary.js`は`localhost:3000/dictionary`にアクセスするとGETやPOSTができます

### コード
下のサンプルコードでは、
`url`で設定したサイトにアクセス
→ cheerioでh1タグをスクレイピング
→ h1タグのテキストをレスポンスで返す
ということをしています

```javascript:my-api.js
import request from 'request';
import cheerio from 'cheerio';

export default ({ query: { id } }, res) => {
  request(url,
    (err, response, body) => {
      if (err) {
        console.error(err);
        res.status(500);
        res.end('server error');
      }

      const $ = cheerio.load(body);
      const foo = $('h1').text();
      res.status(200).json({ message: `scraped h1 tag and got ${foo} ` });
    },
  );
};
```

**クエリパラメータ**
APIのURLにはパラメータを設定することができます
サンプルコードの中では `{ query: { id } }` となっている部分で、URLの`?id=`の値を取得しています
例えば `localhost:3000/api/my-api?id=10` にアクセスすると、APIの中で`id`の値を使うことができます

**cheerioでスクレイピング**
サンプルコードの中では、以下の2行でcheerioを使っています

```javascript
const $ = cheerio.load(body);
const foo = $('h1').text();
```
`$ = cheerio.load(body)`でレスポンスのbodyを`$`に入れることで、`$('.myclass')`や`$('#myid')`のようにjQuery風の書き方でスクレイピングしたいサイトに要素を取得できます

**responseを返す**
エラーの場合は500を返します

```javascript
res.status(500);
res.end('server error');
```

成功した場合は200とJSON形式のデータを返します

```javascript
res.status(200).json({ message: `scraped h1 tag and got ${foo} ` });
```

## json-serverでデータベースを実装しました

こちらの記事が大変参考になりました
https://qiita.com/t12u/items/2be73956b788c745048f

### json-serverの使い方

- json-serverを`npm`でアプリに追加
- `db.json`をルートディレクトリに用意 → データを書き込む
- ターミナルからjson-serverを起動: `npm run json-server`
- アプリから`localhost:30001/<データベース名>`にHTTPリクエストを投げることで読み書きする

### json-serverでの読み書き

- 新規作成: GET
- 編集: PUT
- 削除: DELETE

でそれぞれデータを操作できます

### アプリのデータベース構成

```bash
texts
  + id: 1 (number) # データのID; json-serverによって自動で振られる
  + text: the quick brown fox jumps over the lazy dog (string) # ユーザの入力した文章
  + wordlist: ['fox', 'lazy'] (string[]) # 文章の中でハイライトされた単語

vocab
  + id: 1 (number)
  + word: lazy (string) # 単語
  + definitions: ['気だるい', '怠けている', 'ゆっくりとした'] (string[]) # 単語の意味の配列
```

### コード

アプリの中では`fetch`を使って`localhost:3001/text`や`localhost:3001/vocab`にHTTPリクエストを投げます
下の例ではvocabデータベースのすべてのエントリーのwordの値をとってきています

```react
export async function getWords () {
  const res = await fetch('http://localhost:3001/vocab')
  const data = await res.json();
  return (<ul>{data.map(entry => <li>entry.word</li>)}</ul>)
};
```

```html

<!-- getWords()の返り値の一例 -->
<ul>
  <li>lazy</li>
  <li>fox</li>
  <li>dog</li>
</ul>
```

新たに書き込みをするときは、このようなコードになります

```typescript
fetch('http://localhost:3001/vocab', {
  method: 'POST',
  headers: {
    Accept: 'application/json',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    word: 'hoge',
    definition: ['ほげ', 'ホゲホゲ']
  }),
}).then(getWords);
```

## アクサンの処理が大変でした

### 経緯

冒頭の概要で書いたように、今回はフランス語の文章を対象にしました
フランス語の単語の意味をAPIでとってくるときは、例えば https://www.collinsdictionary.com/dictionary/french-english/vouloir のように、URLに調べたい単語を入れます
ところが、フランス語にはアクサン記号つきの文字 (e.g. î, é) が含まれるため、単語をそのままURLのパラメータにするとエラーになります
そこで、文字列を処理する必要があります

### 処理の流れ

1. 単語がクリックされる → être
2. 単語の文字列を取得 → être
3. アクサン付きの文字をエンコーディング -> %EAtre
4. URLにアクセス -> https://www.collinsdictionary.com/dictionary/french-english/%EAtre

### コード

アクサンのエンコードをする関数は以下の通りです
何も考えずにアクサン文字を1つずつ`String.replace()`で置換します

文字列の処理は、アクサン以外にもユーザが入力した文章をデータベースに保存するとき、`'`や`&`をエスケープすることも行ったため、
関数を`utils/strUtils.tsx`の中にまとめて置きました
アプリ本体で使うときは `import { encodeAccents } from ../utils/strUtils` のようインポートします

```typescript:utils/strUtils.tsx
export const encodeAccents: (str: string) => string = (str: string) =>
  str
    .toLowerCase()
    .replace(/ç/g, '%E7')
    .replace(/é/g, '%E9')
    .replace(/â/g, '%E2')
    .replace(/ê/g, '%EA')
    .replace(/î/g, '%EE')
    .replace(/ô/g, '%F4')
    .replace(/û/g, '%FB')
    .replace(/à/g, '%E0')
    .replace(/è/g, '%E8')
    .replace(/ù/g, '%F9')
    .replace(/ë/g, '%EB')
    .replace(/ï/g, '%EF')
    .replace(/ü/g, '%FC');
```

### 備考

文字列の処理について、アポストロフィには`'`の他にも`’`が使われていることがあります

#まとめ

開発を通して、

- Next.jsを使ったReactアプリケーションの開発
- json-serverでのデータベース実装
- APIの書き方
- fetchの使い方

などを学びました

未解決な点としては、

- Next.jsで`.scss`ファイルを扱う方法
- `request`をネストする方法

などがあります

機能を実装していくにあたり、細かい点を決めるとき（単語リスト画面に単語の意味を表示するかどうかなど）にどうするべきかの判断基準がぶれていたので、次の開発では最初に想定ユーザ・想定シーンをより具体的に定めていきます
データベースも、とりあえずライブラリを入れてからデータ構造を設計しましたが、こちらはライブラリをある程度使わないと仕様がわからないことと、データ構造が比較的単純だったことがあり、今回のやり方でもあまり困りませんでした

今後の予定としては、しばらく自分でアプリを使ってみて、バグ取りや更にあったら便利な機能を実装していきます