Docker・Nuxt・Typescript・Django REST Frameworkで開発
開発メモとして
何か作るときに、調べたことをメモしながら作っていきます。それが、ある程度まとまった内容になったら、ブログなり、Qiitaなり、Zennなりに書く(書こうという気持ちはある)というスタイルです。
けど、後から振り返ってまとめるのは、その時点では熱が冷めていたりすることもあります(それが大半だ)。
で、メモのスタイルは、こちらのスクラップブックと一致しているなと気づき、また、晒すことでホットな情報が得られるかもしれないし、ということで、これを書いていくことにしたいと思います。
タイトルすら更新していくと思います。
(気づき)
URLはそのまま貼り付けた方が良いですね。
作っているもの
構成の概要はこちらにも書きました。
そのうちGithubのソースも公開するつもりだし、加工するのも面倒なので、ソースのパス等もそのまま書いていきます。
技術選定
フロント:Nuxt
テンプレートが書きやすく、かつ、Vuetify が素晴らしいと思っているので、最近は Nuxt を使うようにしています。ただ、Typescript からは逃げていましたが、今回は Typescript でやります。
API
google spreadsheet api
直接使用は依存しすぎるのでダメ
GoogleSpread →HerokuDBは良いと思う
HerokuDB用APIを作ることにする
APIを作成するにあたっての参考情報
Django REST Framework
- Javaは捨て難いけど
- 言語としてのトレンド
- DB作るのはしんどいけど
python 環境
パッケージのインストールならpip,仮想環境の構築ならvirtualenv(venv)を使えば対応できますが,pipenvはそれらをまとめてより簡単に扱えるようにサポートしてくれます.
APIもherokuにデプロイしよう
DB
herokuは本来はPostgreSQL
チャレンジしてみる→断念してMySQL
マイグレーションもsqliteではなくpostgresにすることを考慮
参考情報
(必要なら)
Docker構成
参考情報
Django + REST framework + Swagger + JWT + docker-compose で開発環境構築
基本はこれに従うが、これはPythonだけ
DBも作成するので、大きな構成は下記
PoliLink
├── PolyLink.md
├── docker-compose.yml
├── .git
├── .gitignore
├── README.md
├── poli-link-api
│ ├── config
│ ├── __init__.py
│ ├── __pycache__
│ ├── ...
├── poli-link-db
│ ├── (postgres)
│ ├── ...
├── poli-link-web
│ ├── (nuxt)
│ ├── ...
pipenv
参考情報
brew update
# エラー対処
sudo rm /usr/local/share/Library/Caches/Yarn/v6/npm-micromatch-3.1.10-70859bc95c9840952f359a068a3fc49f9ecfac23-integrity/node_modules/micromatch/lib/.DS_Store
sudo rm /usr/local/share/Library/Caches/Yarn/v6/npm-ws-3.3.3-f1cf84fe2d5e901ebce94efaece785f187a228f2-integrity/node_modules/ws/lib/.DS_Store
sudo find /usr/local/share/Library/Caches/Yarn/v6 -type f -name ".DS_Store" -print | xargs sudo rm -f
brew install pyenv
pyenv install --list
# 3.8.1が出てこないので
brew upgrade pyenv
pyenv install --list
sudo pyenv install 3.8.1
それでもエラー
error: implicit declaration of function 'sendfile' is invalid
参考
sudo rm -rf /Library/Developer/CommandLineTools
xcode-select --install
brew reinstall zlib bzip2
==> Downloading https://homebrew.bintray.com/bottles/zlib-1.2.11.big_sur.bottle.tar.gz
(途中省略)
==> Summary
(途中省略)
==> bzip2
(途中省略)
For compilers to find bzip2 you may need to set:
export LDFLAGS="-L/usr/local/opt/bzip2/lib"
export CPPFLAGS="-I/usr/local/opt/bzip2/include"
メッセージに従って
export LDFLAGS="-L/usr/local/opt/zlib/lib"
export CPPFLAGS="-I/usr/local/opt/zlib/include"
export PKG_CONFIG_PATH="/usr/local/opt/zlib/lib/pkgconfig"
echo 'export PATH="/usr/local/opt/bzip2/bin:$PATH"' >> /Users/konnokiyotaka/.bash_profile
export LDFLAGS="-L/usr/local/opt/bzip2/lib"
export CPPFLAGS="-I/usr/local/opt/bzip2/include"
#さらに
echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.bash_profile
echo 'export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.bash_profile
echo 'eval "$(pyenv init -)"' >> ~/.bash_profile
source ~/.bash_profile
#https://teratail.com/questions/309549
brew install pyenv zlib bzip2 readline
CPPFLAGS="-I$(brew --prefix openssl)/include -I$(brew --prefix bzip2)/include -I$(brew --prefix readline)/include -I$(brew --prefix zlib)/include" LDFLAGS="-L$(brew --prefix openssl)/lib -L$(brew --prefix readline)/lib -L$(brew --prefix zlib)/lib -L$(brew --prefix bzip2)/lib" pyenv install 3.8.6
これでようやくOKで、本題
$ pyenv versions
system
* 3.7.2 (set by ...
3.8.6
$ pyenv global 3.8.6
$ pyenv versions
system
3.7.2
* 3.8.6 (set by ...
$ python -V
Python 3.8.6
$ pip install --upgrade pip
$ pip install pipenv
$ pipenv --version
pipenv, version 2020.11.15
$ echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.bash_profile
$ echo 'export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.bash_profile
$ echo 'eval "$(pyenv init -)"' >> ~/.bash_profile
Django環境・プロジェクト作成
参考情報
$ mkdir poli-link-api && cd poli-link-api
$ pipenv --python 3.8
gunicornは使わない
psycopg2-binaryもはPostgresように必要
$ pipenv shell
(poli-link-api)$ pipenv install django psycopg2-binary
✘ Locking Failed!
ERROR:pip.subprocessor:Command errored out with exit status 1:
(途中省略)
結局このエラーは解消できなかったので、mysqlにした
version: '3'
services:
db:
container_name: poli-link-db
image: mysql:5.7
restart: always
environment:
MYSQL_DATABASE: polilink_database
MYSQL_USER: root
MYSQL_PASSWORD: password
MYSQL_ALLOW_EMPTY_PASSWORD: 'yes'
volumes:
- ./poli-link-db/mysql:/var/lib/mysql
ports:
- 33306:3306
api:
container_name: poli-link-api
build: ./poli-link-api
command: python3 manage.py runserver 0.0.0.0:8000
volumes:
- ./poli-link-api/:/code
ports:
- "8000:8000"
depends_on:
- db
Django プロジェクトの生成
docker-compose run api django-admin.py startproject config .
Djangoアプリ作成
先ほどはdjango-admin.py startprojectでconfigというプロジェクトを作った
次は、python manage.py startapp sample_appでアプリを作る
$ docker exec -it poli-link-api /bin/bash
code# python manage.py startapp polilink_api
作成された
以下、先程のサイトの手順に従って、
テストビューの作成
アプリ用URLConfの新規作成
ルート用URLConfの編集
ここまでで、http://localhost:8000/ が「Hello, world.」に切り替わった
さらに
$ docker exec -it poli-link-api /bin/bash
code# pip install djangorestframework django-filter
Djangoモデル
poli-link-api/polilink_api/models.py
でモデル定義して、
$ docker exec -it poli-link-api /bin/bash
code# python manage.py makemigrations
code# python manage.py migrate
接続できない
Access denied for user 'root'@'192.168.32.1' (using password: YES)
しかし、もう少し続けてみると
エラーになるけど管理画面は表示される
Memosテーブルもできている
なので、続けていく
Djagno REST Framework のアプリケーション登録
Serializerの定義
ViewSetの定義
REST frameworkの設定をアプリ用URLConfに組み込む
REST frameworkの設定をルート用URLConfに
これにより、Api Root の画面が表示された
Django REST Swagger のインストール
$ docker exec -it poli-link-api /bin/bash
code# pip install django-rest-swagger
swaggerの有効化
swaggerの設定をルート用URLConfに組み込む
$ docker exec -it poli-link-api /bin/bash
code# python manage.py runserver
これでOK
djangorestframework-jwt のインストール
code# pip install djangorestframework-jwt
JWTの有効化
JWTの設定をルート用URLConfに組み込む
nuxt-typescript用のdocker
FROM node:14.16.0-alpine
ENV LANG=C.UTF-8 TZ=Asia/Tokyo
WORKDIR /app
RUN apk update
COPY ./package*.json ./
RUN npm install
COPY ./ .
ENV HOST 0.0.0.0
EXPOSE 3000
version: '3'
services:
db:
container_name: poli-link-db
image: mysql:5.7
restart: always
environment:
MYSQL_DATABASE: polilink_database
MYSQL_USER: root
MYSQL_PASSWORD: password
MYSQL_ALLOW_EMPTY_PASSWORD: 'yes'
volumes:
- ./poli-link-db/mysql:/var/lib/mysql
ports:
- 3306:3306
api:
container_name: poli-link-api
build: ./poli-link-api
command: python3 manage.py runserver 0.0.0.0:8000
volumes:
- ./poli-link-api/:/code
ports:
- "8000:8000"
depends_on:
- db
web:
container_name: poli-link-web
build: ./poli-link-web
ports:
- 3000:3000
volumes:
- ./poli-link-web:/app
- /app/node_modules
tty: true
working_dir: /app
# command: sh -c "cd poli-link/ && yarn dev"
最初は
COPY ./package*.json ./
でエラーになったが、一度外してビルドして、2回目からはOK
最後のコマンドはコメントアウトしておいて、構築
$ docker exec -it poli-link-web sh
/app# yarn create nuxt-app poli-link
? Project name: poli-link
? Programming language: TypeScript
? Package manager: Yarn
? UI framework: Vuetify.js
? Nuxt.js modules: Axios - Promise based HTTP client
? Linting tools: ESLint
? Testing framework: Jest
? Rendering mode: Single Page App
? Deployment target: Static (Static/Jamstack hosting)
? Development tools: (Press <space> to select, <a> to toggle all, <i> to invert selection)
? Continuous integration: None
? Version control system: Git
/app # cd poli-link/
/app/poli-link # yarn dev
起動したけど、エラー
webpack-internal:///./.nuxt/client.js:230 TypeError: Cannot read property 'call' of undefined
at __webpack_require__ (runtime.js:854)
at fn (runtime.js:151)
at eval (webpack-internal:///./node_modules/babel-loader/lib/index.js?!./node_modules/ts-loader/index.js?!./node_modules/vue-loader/lib/index.js?!./components/Keyboard.vue?vue&type=script&lang=ts&:29)
at Module../node_modules/babel-loader/lib/index.js?!./node_modules/ts-loader/index.js?!./node_modules/vue-loader/lib/index.js?!./components/Keyboard.vue?vue&type=script&lang=ts& (:3000/_nuxt/0.js:59)
at __webpack_require__ (runtime.js:854)
at fn (runtime.js:151)
at eval (webpack-internal:///./components/Keyboard.vue?vue&type=script&lang=ts&:2)
at Module../components/Keyboard.vue?vue&type=script&lang=ts& (:3000/_nuxt/0.js:23)
at __webpack_require__ (runtime.js:854)
at fn (runtime.js:151)
再起動したらOK
一旦ストップし、docker-composeもdown
docker-compose.yml
最後の行のコメントを戻して再度up
それぞれ参照可能
Composition API 導入
参考情報
一旦docker-compose.yml の最終行コメントアウトしてupした状態で
$ docker exec -it poli-link-web sh
/app # cd poli-link/
/app/poli-link # yarn add @nuxtjs/composition-api
tsconfig.json階層化
axios対応
上記参考情報のやり方は、pagesで、await
RepositoryFactory
ここで改めて、API呼び出しの構成を検討する
serviceではなくapi/repostry
そこでcrudを定義するという意味で、やろうとしていることは似ていると思われる
こんな構成か
│ ├ repositories
│ │ ├ repository.ts
(対象ごとに作るのではなく) ※baseUrlの考え方は欲しい
│ ├ store
│ │ └ xxx.ts
│ └ plugins
│ └ epository-factory.ts
│
└ nuxt.config.js
($repositories: Repositories // 追加)
nuxt.config.js から nuxt.config.ts への変更
参考情報
ただし、package.json の scriptsで、nuxt から nuxt-ts にするというのはエラーになるのでやらない
→エラーになるというよりも、
poli-link-web | WARN You're using Nuxt 2.15.3, which includes built-in TypeScript runtime support
poli-link-web |
poli-link-web |
poli-link-web | WARN You can safely use nuxt instead of nuxt-ts and remove @nuxt/typescript-runtime package
と出てる。これは不要と考えて戻す。
下記を参考に、layoutをtypescriptに変更
ただし、createComponent は現状は defineComponent
ちょっと古いか
下記追加
poli-link-web/poli-link/repositories/repository.ts
poli-link-web/poli-link/plugins/repository-factory.ts
そして、nuxt.config.ts に
import { Repositories } from "@/plugins/repository-factory";
を追加しようとするところで、
{
"resource": "/・・・/workspace/PoliLink/poli-link-web/poli-link/nuxt.config.ts",
"owner": "typescript",
"code": "2307",
"severity": 8,
"message": "Cannot find module '@/plugins/repository-factory' or its corresponding type declarations.",
"source": "ts",
"startLineNumber": 4,
"startColumn": 30,
"endLineNumber": 4,
"endColumn": 60
}
となっている。
json:tsconfig.json
"include": [
"*.ts",
"components/**/*.ts",
"components/**/*.vue",
"layouts/**/*.ts",
"layouts/**/*.vue",
"store/**/*.ts",
"plugins/**/*.ts",
"utils/**/*.ts",
"pages/**/*.ts",
"pages/**/*.vue"
],
この1行目を追加することで解消するが、今度は、
Could not find a declaration file for module 'vuetify/es5/util/colors'. '/・・・/workspace/PoliLink/poli-link-web/poli-link/node_modules/vuetify/es5/util/colors.js' implicitly has an 'any' type.
Try `npm i --save-dev @types/vuetify` if it exists or add a new declaration (.d.ts) file containing `declare module 'vuetify/es5/util/colors';`
こちらを参考に
tsconfig.jsonのcompilerOptions.typesに"vuetify"を追加
store を追加し、 pages から使用する
ところが、この部分のソースがちょっと理解できない
import { ClientResponse } from '@/types/model/client'
この、@/types/model がどこから来ているかわわからない・・・
こちらがよいか
tsではないこちらがより近い
そのものズバリの情報がないので自力で頑張る
公式のいつもの図
ComponentからはActionを呼び出し、ActionがMutationをコミットする。
Actionは非同期処理ができる
しかし、このやり方
もう少しべたに、個別のrepositoryを作る方法で試してみる
と
を見ながら
vueのinterfaceを拡張
というのはどこでやるのだろう・・・
まずは、下の方をそのままやってみる
いい感じっぽいが、
Access to XMLHttpRequest at 'http://localhost:8000/api/memo' from origin 'http://localhost:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
となるので、先にAPI側のCORS対応
こちらの通り
$ docker exec -it poli-link-api /bin/bash
code# pip install django-cors-headers
でインストールして、poli-link-api/config/settings.py に追記
すると下記エラー
ModuleNotFoundError: No module named 'corsheaders'
pip install するだけではダメで、
django-cors-headers==3.7.0
を追加
今度は次のエラー
(corsheaders.E013) Origin 'localhost:3000' in CORS_ORIGIN_WHITELIST is missing scheme or netloc
正しくは下記
CORS_ORIGIN_WHITELIST = (
'http://localhost:3000',
)
※最後に/を付けるとエラーになるのでこれだけ
ところが、やはりCORSエラーになる
MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware',
という感じで、CorsMiddleware は、CommonMiddleware より前に入れる。
すでに、CommonMiddleware は入っていたので、注意
これでCORSエラーは消えた
意外と大変だった
ところが今度は、
GET http://localhost:8000/api/ 401 (Unauthorized)
解決は
REST_FRAMEWORK = {
'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.coreapi.AutoSchema',
# 'DEFAULT_PERMISSION_CLASSES': ('rest_framework.permissions.IsAuthenticated',),
'DEFAULT_PERMISSION_CLASSES': ('rest_framework.permissions.AllowAny',),
これで、無事(?)Vue側のエラーになったので、API側は一旦終了
vueのエラーも、一旦使っていないファイルだったので、コメントアウトして、無事データ取得はできました。
さらに、別のリスト用componentを作っているようですが、そこも省略。
最低限の動くもの、ということで、こんな感じです。
<template>
<div class="container">
<p>{{ memos }}</p>
</div>
</template>
<script>
import { RepositoryFactory } from "~/repositories/RepositoryFactory";
const MemoRepository = RepositoryFactory.get('memo')
export default {
name: "name",
// components: { }
data() {
return {
isLoading: false,
memos: [],
}
},
created () {
this.fetch()
},
methods: {
async fetch () {
this.isLoading = true
const { data } = await MemoRepository.get()
this.isLoading = false
this.memos = data;
}
}
}
</script>
<style>
</style>
これをTypescriptに変更
参考情報
<template>
<div class="container">
<input v-model="state.memo" placeholder="メモを入力してください">
<br>
<li v-for="(memo, index) in state.memos" :key="index">
{{ memo }}
</li>
</div>
</template>
<script lang="ts">
import { defineComponent, reactive, onMounted } from "@nuxtjs/composition-api"
import { RepositoryFactory } from "~/repositories/RepositoryFactory";
const MemoRepository = RepositoryFactory.get('memo')
interface StateData {
id: number
title: string
memo: string
isLoading: boolean,
memos: []
}
export default defineComponent({
head: {
title: "Memo"
},
setup(_props, context) {
const state = reactive<StateData>({
id: 0,
title: 'title0',
memo: 'memo0',
isLoading: false,
memos: []
})
const fetch = async () => {
state.isLoading = true
console.log('state.isLoading', state.isLoading)
const { data } = await MemoRepository.get()
state.isLoading = false
console.log('state.isLoading', state.isLoading)
console.log('data', data)
state.memos = data
}
onMounted(fetch)
return {state}
}
})
</script>
<style>
</style>
その後、jsになっていた部分をtsにしようとしていましたが、悪戦苦闘の結果断念。
悩みながら調べていたら、これに出会いました。
store使わないのか、と思いましたが、こちら
の「Vuexとの使い分けについて」も参考にさせていただき、nuxt-realworld 方式で行くことにしました。動くサンプルがあるのはやはり助かります。
とはいえ、もうしばらく悪戦苦闘は続きました。
axios: {
baseURL: 'http://localhost:8000/api/',
},
このbaseURLがないと、「Failed to execute 'fetch' on 'Window': 1 argument required, but only 0 present.」エラー。
その後、apiからデータは取れてきているのに、戻り値として返せない、というエラーにずっとハマっていましたが、そもそも、APIの返し方が違うというオチでした。
Promise {<pending>}
__proto__: Promise
[[PromiseState]]: "fulfilled"
[[PromiseResult]]: Object
articles: (20) [{…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}]
articlesCount: 500
__proto__: Object
というAPIからの戻りを前提に、
type ArticleListResponse = ResponseTypes<{
articles: Article[]
articlesCount: number
}>
という型定義がされていたわけですが、
今回のAPIの戻りは
Promise {<pending>}
__proto__: Promise
[[PromiseState]]: "fulfilled"
[[PromiseResult]]: Array(2)
0: {id: 1, title: "a", memo: "a"}
1: {id: 2, title: "b", memo: "bmemo"}
length: 2
__proto__: Array(0)
なので、
type MemoListResponse = ResponseTypes<Memo[]>
としました。
これにより、Repositoryパターン的な形で、APIからデータを取得できるようになりました。
スクラップにも目次がついて欲しいですね・・・
まだ、色々挫折の残骸が残っていますが、とりあえず、データ取得ができたということで、今度は、データの作成、削除をやりたいと思います。
その記録を残していきます。
その前に、
データ取得の処理の流れを整理
components を使うとして、今回参考にしている「devJang/nuxt-realworld」では、データ表示側はデータをpropsで受け取るようにしていますが、データもcomponents で取得するとして、
const {
state: memoListState,
getMemoList
} = useMemoList()
const fetchData = async (offset = 0) => {
await getMemoList({ offset })
console.log('fetchData')
}
const getMemoList = async(payload: MemoListRequest = {}) => {
const memos= await $repository.memo.getMemoList(payload)
getMemoList({
limit = LIMIT_LIST_ITEM,
offset = 0,
}: MemoListRequest = {}): MemoListResponse {
const defaultParam = {
}
return axios.$get('/memo/', {
params: {...defaultParam, limit, offset},
})
},
という遡りになります。
データ更新処理の作成手順
先程の逆に行くことになるので、まずは、memoRepository.ts に、更新系API呼び出しを追加。
Request用の型定義が追加と合わせて。
※その際に、type Pick というものを使いますが、これは、
型 T の中から K に当てはまるプロパティのみを抜き取った新しい型を返します。
とのこと。なるほどねぇ
そして、最初に書いたものからちょっと訂正
RuntimeError: You called this URL via POST, but the URL doesn't end in a slash and you have APPEND_SLASH set. Django can't redirect to the slash URL while maintaining POST data. Change your form to point to localhost:8000/api/memo/ (note the trailing slash), or set APPEND_SLASH=False in your Django settings.
というエラーが出るので、スラッシュを忘れないように。
export type CreateMemoRequest = Pick<
Memo,
'title' | 'memo'
>
export type UpdateMemoPayload = Partial<CreateMemoRequest>
export type UpdateMemoRequest = {
payload: UpdateMemoPayload
slug: Memoid
}
・・・
export const memoRepository = (axios: NuxtAxiosInstance) => ({
・・・
createMemo(payload: CreateMemoRequest): MemoResponse | CustomErrors {
console.log('createMemo', payload)
return axios.$post('/memo/', payload)
},
updateMemo(request: UpdateMemoRequest): MemoResponse | CustomErrors {
return axios.$put(`/memo/${request.id}`, { article: request.payload })
},
deleteMemo(memoid: Memoid) {
return axios.$delete(`/memo/${memoid}`)
},
useMemoを追加
追加したら、
import useMemoList from "./useMemoList";
import useMemo from "./useMemo";
export {
useMemoList,
useMemo,
}
vueから呼び出し
vuetifyの話が挟まるので一旦終了
画面は、こんな感じを作ります。
まずは、データ表示部分。
Vuetify の v-data-table を使います。
<v-data-table
:headers="headers"
:items="memoList"
dense
>
<template v-slot:[`item.actions`]="{ item }">
<v-icon
small
class="mr-2"
@click="editItem(item)"
>
mdi-pencil
</v-icon>
<v-icon
small
@click="deleteItem(item)"
>
mdi-delete
</v-icon>
</template>
</v-data-table>
上に書いたように、useMemoListからstate型のmemoListStateを取得して、
...toRefs(memoListState),
として、returnすることにより、stateに入っている、memoListが使える、という感じですね。
headersも必要なので、こんな感じで足しておきます。
setup() {
const headers = [
{text: 'タイトル', value: 'title'},
{text: 'メモ', value: 'memo'},
{text: '', value: 'actions', sortable: false},
]
ここまでくるのも苦労しましたけど、データ追加部分はもっと大変でした。
途中の悪戦苦闘はかなり省略
<v-form lazy-validation>
<v-row>
<v-col
cols="12"
sm="4"
>
<v-text-field
label="タイトル"
dense
outlined
clearable
v-model="form.title"
>
</v-text-field>
</v-col>
<v-col
cols="12"
sm="4"
>
<v-text-field
label="メモ内容"
dense
outlined
clearable
v-model="form.memo"
>
</v-text-field>
</v-col>
<v-col
cols="12"
sm="4"
>
<v-btn color="primary" @click="handleCreateMemo">新規</v-btn>
</v-col>
</v-row>
</v-form>
refの参照のしかたとか色々調べましたが、一旦省略。
v-model="form.title"
の部分に集中します。
クリックのイベントの部分は、こうならないといけない。
const handleCreateMemo = async() => {
try {
const newMemo = await createMemo(createMemoState) //こことformを結びつける
if (!newMemo) {
return
}
console.log('Success')
} catch (error) {
console.log('error')
// setError(error)
}
}
createMemo はuseMemoにあるので
const { createMemo } = useMemo()
この引数は、CreateMemoRequest型
それと一致するのが、useMemo で定義している、CreateState型
そして、そちらで
const createState = reactive<CreateState>(initCreateState)
がreturnされている。ここがreactiveになっているので、scriptと、templateが連動する。
なので、
const {
createState: createMemoState,
} = useMemo()
として、createMemoStateをapi呼び出し処理への引数とする。
これをreturnするのも忘れず。
最終的にはこんな感じ。
setup() {
const headers = [
{text: 'タイトル', value: 'title'},
{text: 'メモ', value: 'memo'},
{text: '', value: 'actions', sortable: false},
]
const {
state: memoListState,
getMemoList,
} = useMemoList()
const {
createState: createMemoState,
} = useMemo()
const { createMemo } = useMemo()
const handleCreateMemo = async() => {
try {
console.log('createMemoState', createMemoState)
const newMemo = await createMemo(createMemoState) //こことformを結びつける
if (!newMemo) {
return
}
console.log('Success')
} catch (error) {
console.log('error')
// setError(error)
}
}
const fetchData = async (offset = 0) => {
await getMemoList({ offset })
console.log('fetchData')
}
const { fetchState } = useFetch(() => fetchData())
// console.log('fetchState', fetchState)
return {
headers,
// state,
fetchState,
fetchData,
...toRefs(memoListState),
form: createMemoState,
handleCreateMemo,
}
}
まずは、削除ダイアログを出す。
<v-dialog v-model="dialogDelete" max-width="500px">
<v-card>
<v-card-title class="headline">削除しますか?</v-card-title>
<v-card-actions>
<v-spacer></v-spacer>
<v-spacer></v-spacer>
</v-card-actions>
</v-card>
</v-dialog>
dialogDelete は元々Dataとして公開していた部分なので、reactiveなstateの中に定義して、return する。
削除アイコンから呼び出しているdeleteItemも。
setup() {
・・・
const state = reactive<Data>({
dialogDelete: false,
editedIndex: -1,
editedItem: {
title: '',
meeo: '',
},
})
・・・
const deleteItem = (item: Memo) =>{
state.editedIndex = item.id
state.editedItem = item
console.log(item)
state.dialogDelete = true
}
・・・
return {
headers,
...toRefs(state),
・・・
deleteItem,
}
閉じるボタン
<v-btn color="blue darken-1" text @click="closeDelete">Cancel</v-btn>
このcloseDelete
closeDelete () {
this.dialogDelete = false
this.$nextTick(() => {
this.editedItem = Object.assign({}, this.defaultItem)
this.editedIndex = -1
})
},
を、Composition API でどう書くか。
参考
import { nextTick } from 'vue'
は
TypeError: Object(...) is not a function
となった。
方法1はOK。
setup(_, { root }) {
・・・
const closeDelete = () =>{
state.dialogDelete = false
root.$nextTick(() => {
state.editedItem = Object.assign({}, state.defaultItem)
state.editedIndex = -1
})
}
削除のAPI呼び出し
<v-dialog v-model="dialogDelete" max-width="500px">
<v-card>
<v-card-title class="headline">削除しますか?</v-card-title>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="blue darken-1" text @click="closeDelete">Cancel</v-btn>
<v-btn color="blue darken-1" text @click="deleteItemConfirm">OK</v-btn>
<v-spacer></v-spacer>
</v-card-actions>
</v-card>
</v-dialog>
const state = reactive<Data>({
dialogDelete: false,
editedIndex: -1,
editedItem: {
id: -1,
title: '',
meeo: '',
},
defaultItem: {
id: -1,
title: '',
meeo: '',
},
})
・・・
const {
createState: createMemoState,
createMemo,
deleteMemo,
} = useMemo()
・・・
const deleteItemConfirm = async() =>{
console.log('deleteItemConfirm', state.editedItem)
const memoid:number = state.editedItem.id
deleteMemo(memoid)
closeDelete()
}
これでreturnに追加することで、削除はできた。
ただ、画面描画が更新されない。
別のページに行って戻れば更新されてる。
お手本である、nuxt-realworld 方式で、compositions/useArticleList.ts と compositions/useArticleSlug.ts が分かれているのでそれを真似して、poli-link-web/poli-link/compositions/useMemo.ts と、poli-link-web/poli-link/compositions/useMemoList.ts を作っていたのですが、情報はuseMemoListの中で定義されている memoList: Memo[] を参照しているわけなので、useMemoで更新した結果をuseMemoListのmemoListに伝えないといけないということになる。
nuxt-realworldでも、コメントについてはcompositions/useComment.ts一つで、
const deleteComment = async ({ slug, id }: DeleteCommentRequest) => {
await $repository.comment.deleteComment({ slug, id })
await getCommentList(slug)
}
としているので、それを参考に、useMemo.ts にまとめて、useMemoList.ts は廃止しました。
現時点の全体像
import { reactive, useContext } from '@nuxtjs/composition-api'
import { MemoListRequest, CreateMemoRequest, UpdateMemoRequest } from "@/api/memoRepository";
import { Memo } from "@/types";
import MemoKey from '~/store/memo-key';
type MemoPayload = Required<CreateMemoRequest>
type CreateState = MemoPayload
type State = {
memo: Memo
memoList: Memo[]
memoCount: number
}
const initCreateState = {
title: '',
memo: '',
}
const initState = {
memo: {
id: 0,
title: '',
memo: '',
},
}
export default function useMemo() {
const { $repository } = useContext()
const createState = reactive<CreateState>(initCreateState)
// const state = reactive<State>(initState)
const state = reactive<State>({
memo: {
id: 0,
title: '',
memo: '',
},
memoList: [],
memoCount: 0,
})
const getMemo = async (memoid: Memo['id']) => {
const { memo } = await $repository.memo.getMemo(memoid)
state.memo = memo
}
const getMemoList = async(payload: MemoListRequest = {}) => {
const memos= await $repository.memo.getMemoList(payload)
console.log('memos', memos)
state.memoList = memos
state.memoCount = memos.length
}
const createMemo = async (payload: CreateMemoRequest) => {
const response = await $repository.memo.createMemo(payload)
if ('memo' in response) {
await getMemoList()
return response.memo
}
return false
}
const updateMemo = async (payload: UpdateMemoRequest) => {
const response = await $repository.memo.updateMemo(payload)
if ('memo' in response) {
return response.memo
}
return false
}
const deleteMemo = async (memoid: Memo['id']) => {
await $repository.memo.deleteMemo(memoid)
await getMemoList()
}
return {
createState,
state,
getMemo,
getMemoList,
createMemo,
updateMemo,
deleteMemo,
}
}
<template>
<v-card>
<v-card-title> メモ の管理 </v-card-title>
<v-card-subtitle>
<v-form lazy-validation>
<v-row>
<v-col
cols="12"
sm="4"
>
<v-text-field
label="タイトル"
dense
outlined
clearable
v-model="form.title"
>
</v-text-field>
</v-col>
<v-col
cols="12"
sm="4"
>
<v-text-field
label="メモ内容"
dense
outlined
clearable
v-model="form.memo"
>
</v-text-field>
</v-col>
<v-col
cols="12"
sm="4"
>
<v-btn color="primary" @click="handleCreateMemo">新規</v-btn>
</v-col>
</v-row>
</v-form>
</v-card-subtitle>
<v-card-text>
<v-data-table
:headers="headers"
:items="memoList"
dense
>
<template v-slot:[`item.actions`]="{ item }">
<v-icon
small
class="mr-2"
@click="editItem(item)"
>
mdi-pencil
</v-icon>
<v-icon
small
@click="deleteItem(item)"
>
mdi-delete
</v-icon>
</template>
</v-data-table>
<v-dialog v-model="dialogDelete" max-width="500px">
<v-card>
<v-card-title class="headline">削除しますか?</v-card-title>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="blue darken-1" text @click="closeDelete">Cancel</v-btn>
<v-btn color="blue darken-1" text @click="deleteItemConfirm">OK</v-btn>
<v-spacer></v-spacer>
</v-card-actions>
</v-card>
</v-dialog>
</v-card-text>
</v-card>
</template>
<script lang="ts">
import { PropType } from 'vue'
import {
Data,
defineComponent,
reactive,
ref,
toRef,
toRefs,
useFetch,
onMounted,
} from "@nuxtjs/composition-api"
import { RepositoryFactory } from "~/repositories/RepositoryFactory";
import { useMemo } from "@/compositions";
const MemoRepository = RepositoryFactory.get('memo')
interface StateData {
id: number
title: string
memo: string
isLoading: boolean,
memos: []
}
import { Memo } from '@/types'
export default defineComponent({
name: 'MemoEdit',
setup(_, { root }) {
const headers = [
{text: 'タイトル', value: 'title'},
{text: 'メモ', value: 'memo'},
{text: '', value: 'actions', sortable: false},
]
const state = reactive<Data>({
dialogDelete: false,
editedIndex: -1,
editedItem: {
id: -1,
title: '',
meeo: '',
},
defaultItem: {
id: -1,
title: '',
meeo: '',
},
})
// const {
// state: memoListState,
// getMemoList,
// } = useMemoList()
const {
state: memoState,
createState: createMemoState,
createMemo,
deleteMemo,
getMemoList,
} = useMemo()
const handleCreateMemo = async() => {
try {
console.log('createMemoState', createMemoState)
const newMemo = await createMemo(createMemoState) //こことformを結びつける
if (!newMemo) {
return
}
console.log('Success')
} catch (error) {
console.log('error')
// setError(error)
}
}
const deleteItem = (item: Memo) =>{
state.editedIndex = item.id
state.editedItem = item
console.log('item', state.editedItem)
state.dialogDelete = true
}
const closeDelete = () =>{
state.dialogDelete = false
root.$nextTick(() => {
state.editedItem = Object.assign({}, state.defaultItem)
state.editedIndex = -1
})
}
const deleteItemConfirm = async() =>{
console.log('deleteItemConfirm', state.editedItem)
const memoid:number = state.editedItem.id
deleteMemo(memoid)
// fetchData()
closeDelete()
}
const fetchData = async (offset = 0) => {
await getMemoList({ offset })
console.log('fetchData')
}
const { fetchState } = useFetch(() => fetchData())
// console.log('fetchState', fetchState)
return {
headers,
...toRefs(state),
fetchState,
fetchData,
...toRefs(memoState),
form: createMemoState,
handleCreateMemo,
deleteItem,
closeDelete,
deleteItemConfirm,
}
}
// props: {
// memoList: {
// type: Array as () => PropType<Memo[]>,
// required: true,
// },
// },
})
</script>
<style>
</style>
では、最後Update
今参考にしているのは、Vuetifyのデータテーブルのサンプル
これだと、Update処理も、ダイアログを出してその中でやることにしていますが、より汎用的なのは、更新画面に遷移する形だと思うので、そちらにチャレンジ。
それに伴い、pages構成も変更。
git mv poli-link-web/poli-link/pages/memo.vue poli-link-web/poli-link/pages/memo/index.vue
編集用ページ作成
MemoEditFormというcomponentsを使うことにしておく
<template>
<div>
memo edit
<memo-edit-form />
</div>
</template>
<script>
import MemoList from '~/components/memo/MemoList.vue';
import MemoEditForm from '~/components/memo/MemoEditForm.vue';
export default {
components: { MemoList, MemoEditForm },
}
</script>
<style>
</style>
componentsはまずは、vuetifyのサンプルをそのまま貼り付けておく
<template>
<v-form
ref="form"
v-model="valid"
lazy-validation
>
<v-text-field
v-model="name"
:counter="10"
:rules="nameRules"
label="Name"
required
></v-text-field>
<v-text-field
v-model="email"
:rules="emailRules"
label="E-mail"
required
></v-text-field>
<v-select
v-model="select"
:items="items"
:rules="[v => !!v || 'Item is required']"
label="Item"
required
></v-select>
<v-checkbox
v-model="checkbox"
:rules="[v => !!v || 'You must agree to continue!']"
label="Do you agree?"
required
></v-checkbox>
<v-btn
:disabled="!valid"
color="success"
class="mr-4"
@click="validate"
>
Validate
</v-btn>
<v-btn
color="error"
class="mr-4"
@click="reset"
>
Reset Form
</v-btn>
<v-btn
color="warning"
@click="resetValidation"
>
Reset Validation
</v-btn>
</v-form>
</template>
<script>
export default {
data: () => ({
valid: true,
name: '',
nameRules: [
v => !!v || 'Name is required',
v => (v && v.length <= 10) || 'Name must be less than 10 characters',
],
email: '',
emailRules: [
v => !!v || 'E-mail is required',
v => /.+@.+\..+/.test(v) || 'E-mail must be valid',
],
select: null,
items: [
'Item 1',
'Item 2',
'Item 3',
'Item 4',
],
checkbox: false,
}),
methods: {
validate () {
this.$refs.form.validate()
},
reset () {
this.$refs.form.reset()
},
resetValidation () {
this.$refs.form.resetValidation()
},
},
}
</script>
<style>
</style>
そして、編集ボタンを押したときに、遷移するようにする
<v-icon
small
@click="$router.push(`/memo/${item.id}/edit`)"
>
mdi-pencil
</v-icon>
これでとりあえず遷移の動きができたので、poli-link-web/poli-link/components/memo/MemoEditForm.vue を作っていく
slugの取得は、useContextのparams
import {
defineComponent,
useContext,
} from '@nuxtjs/composition-api'
import MemoList from '~/components/memo/MemoList.vue';
import MemoEditForm from '~/components/memo/MemoEditForm.vue';
export default defineComponent({
setup() {
const { app, params, query } = useContext()
const { slug } = params.value
console.log('slug', slug)
return {
}
},
})
データ参照して更新は同じようにできるだろうと思ったら甘かった
データ参照はまあ良いです
const {
state: memoState,
updateMemo,
getMemo,
} = useMemo()
const fetchData = async () => {
await getMemo(props.memoId)
}
const { fetchState } = useFetch(() => fetchData())
return {
form: memoState,
}
ただ、ここで取得されている型が混乱していて、更新の処理を書く際も大変でした。
ちゃんと解消できていないので、動作はするけど、エラーが表示されている状態です。
const state = reactive<State>({
memoData: {
id: 0,
title: '',
memo: '',
},
memoList: [],
memoCount: 0,
})
const getMemo = async (memoid: Memo['id']) => {
const response = await $repository.memo.getMemo(memoid)
console.log('getMemo', response)
state.memoData = response //型が合っていないと言われてしまうがこれで返る
}
ここでresponseに返ってきているのが
{id: 24, title: "title", memo: "memoa"}
これでいいじゃんと思うのですが、state.memoData に入れるところで、型が合っていないと言われています。
ちょっとこれはそのままにして、更新処理へ
template
<template>
<v-form
lazy-validation
>
{{ form.memoData.id }}
<v-text-field
v-model="form.memoData.title"
label="タイトル"
required
></v-text-field>
<v-text-field
v-model="form.memoData.memo"
label="メモ"
required
></v-text-field>
<v-btn
color="success"
class="mr-4"
@click="handleUpdateMemo"
>
更新
</v-btn>
<v-btn @click="$router.push('/memo/')">戻る</v-btn>
</v-form>
</template>
これに向けて、formを返す
import {
Data,
defineComponent,
reactive,
ref,
toRef,
toRefs,
useFetch,
onMounted,
} from "@nuxtjs/composition-api"
import { RepositoryFactory } from "~/repositories/RepositoryFactory";
import { useMemo } from "@/compositions";
import { Memo } from '@/types'
const MemoRepository = RepositoryFactory.get('memo')
export default defineComponent({
name: 'MemoEditForm',
props: {
memoId: {
type: Number,
required: true,
},
},
setup(props, { root }) {
const {
state: memoState,
updateMemo,
getMemo,
} = useMemo()
const handleUpdateMemo = async() => {
try {
console.log('createMemoState', memoState)
await updateMemo({id: memoState.memoData.id, payload: {"title": memoState.memoData.title, "memo": memoState.memoData.memo}})
return
} catch (error) {
console.log('error')
// setError(error)
}
}
const fetchData = async () => {
await getMemo(props.memoId)
}
const { fetchState } = useFetch(() => fetchData())
return {
form: memoState,
handleUpdateMemo,
}
}
})
ちょっと時間開けたら忘れてしまったので駆け足
updateMemoのところ
const updateMemo = async (payload: UpdateMemoRequest) => {
const response = await $repository.memo.updateMemo(payload)
if ('memoData' in response) {
return response.memoData
}
return false
}
ここからmemoRepositoryのupdateMemoが呼ばれる
updateMemo(request: UpdateMemoRequest): MemoResponse | CustomErrors {
return axios.$put(`/memo/${request.id}/`, request.payload )
},
最初、APIから
{
"detail": "JSON parse error - Expecting property name enclosed in double quotes: line 1 column 2 (char 1)"
}
というエラーが返ってきていて、そうか、pythonは厳しいのか・・・、createは大丈夫なのに・・・
とか思いながら、JSON.stringify(request.payload) とかやってみましたが、そういう問題ではなく
やはり、APIの構造が違う
return axios.$put(`/memo/${request.id}/`, { data: request.payload })
から、上記のように、dataに入れない形にしたらOK。
これで、無事CRUDができました!
ここから、本来作りたい部分の画面デザインを考えようか、テストを書こうか、と悩みましたが、さすがに長すぎるので、一旦終了。
次の編に進む、ということにします。