🦙

Nuxt TypeScript(Composition API)、Django REST Framework で、・・・

2021/07/12に公開

タイトル長すぎですね。途中で切れました。
本当は

Nuxt TypeScript(Composition API)、Django REST Framework で、Docker Compose上での開発基盤を作る

こいつの続きというか。
https://zenn.dev/jqinglong/scraps/168d1bb5dd4832

この悪戦苦闘の結果、まあまあ良い構成ができたと思っており、それを使って、さらにもう一つアプリを作ってます。スッキリいくと思うので、そのスッキリした記録を残そうと思ったのですが、そうは問屋はおろさず・・・それでも、上記よりは落ち着いてできたので、こちらに残しておきます。

現時点の様子はこんな感じ。
https://github.com/JQinglong/JQRally
この後も開発を進めますが、下記の状態は、この時点ですね。
https://github.com/JQinglong/JQRally/commit/9d2c1fd1f4271ecda09758eeeea8e5f8f798a775

作成画面

作成画面
あれ、タイトル間違っている・・・

フォルダ名は、作ろうとしているアプリの名前をそのまま使ってしまっていますが、お気にならさらず。

Docker環境

まず、下記フォルダ構成を作成。
以下、jq-rally としているところは、自身で作成するアプリの名前にしてください。

(作業フォルダ)
├── jq-rally-api
├── jq-rally-db
│   ├── mysql
├── jq-rally-web

次に、jq-rally-api/Dockerfile を作成

FROM python:3.8
ENV PYTHONUNBUFFERED 1
RUN mkdir /code
WORKDIR /code
ADD requirements.txt /code/
RUN pip install -r requirements.txt
RUN pip install mysqlclient
ADD . /code/
# CMD python3 manage.py runserver 0.0.0.0:8000
EXPOSE 8000

requirements.txt を作成

asgiref==3.3.4
certifi==2020.4.5.1
chardet==3.0.4
coreapi==2.3.3
coreschema==0.0.4
dj-database-url==0.5.0
dj-static==0.0.6
Django==3.2
django-cors-headers==3.7.0
django-filter==2.4.0
django-rest-swagger==2.2.0
django-toolbelt==0.0.1
djangorestframework==3.12.2
djangorestframework-jwt==1.11.0
drf-writable-nested==0.6.2
gunicorn==20.1.0
idna==2.9
itypes==1.1.0
Jinja2==2.11.2
MarkupSafe==1.1.1
mysqlclient==1.4.6
openapi-codec==1.3.2
psycopg2==2.8.6
PyJWT==1.7.1
pytz==2019.3
requests==2.23.0
simplejson==3.17.0
sqlparse==0.3.1
static3==0.7.0
typing-extensions==3.10.0.0
uritemplate==3.0.1
urllib3==1.25.9
whitenoise==5.2.0

そして、下記、docker-compose.yml 作成

docker-compose.yml
version: '3'

services:
  db:
    container_name: jq-rally-db
    image: mysql:8.0
    restart: always
    environment:
      MYSQL_DATABASE: rally_database
      # MYSQL_USER: root
      MYSQL_ROOT_PASSWORD: password
      MYSQL_ALLOW_EMPTY_PASSWORD: 'yes'
    volumes:
      - ./jq-rally-db/mysql:/var/lib/mysql
    ports:
      - 3306:3306
  api:
    container_name: jq-rally-api
    build: ./jq-rally-api
    command: python3 manage.py runserver 0.0.0.0:8000
    volumes:
      - ./jq-rally-api/:/code
    ports:
      - "8000:8000"
    depends_on:
      - db
  # web:
  #   container_name: jq-rally-web
  #   build: ./jq-rally-web
  #   ports:
  #     - 3000:3000
  #   volumes:
  #     - ./jq-rally-web:/app
  #     - /app/node_modules
  #   tty: true
  #   working_dir: /app
  #   command: sh -c "cd jq-rally/ && yarn install && yarn dev"

(MySQLのrootのパスワードの設定は8.0で変わりましたかね)

この状態

(作業フォルダ)
├── jq-rally-api
│   ├── Dockerfile
│   ├── requirements.txt
├── jq-rally-db
│   ├── mysql
├── jq-rally-web
├── docker-compose.yml 

では、起動

docker compose up --build

注意:webのところをコメントアウトしていないと

ERROR: Service 'web' failed to build : Build failed

というエラー。

また、現時点では、Django プロジェクトを作っていないので、

jq-rally-api  | python3: can't open file 'manage.py': [Errno 2] No such file or directory
jq-rally-api exited with code 2

となっている。

起動確認してみると下記状態。

$ docker compose ps
NAME                SERVICE             STATUS              PORTS
jq-rally-db         db                  restarting          

このペースで行くと、どんだけ長くなるんだという感じなので、飛ばし飛ばし・・・

Django プロジェクト生成

下記を参照させていただいています。

https://qiita.com/mykysyk@github/items/fef6fb298393a029a5d4

conifg アプリケーション

この辺は飛ばしていきます。

作成された settings.py を編集(またあとで変更しますが、いったん)

jq-rally-api/config/settings.py
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'rally_database',
        'USER': 'root',
        'HOST': 'db',
        'PORT': 3306,
        'OPTIONS': {
            'charset': 'utf8mb4',
            'sql_mode': 'TRADITIONAL,NO_AUTO_VALUE_ON_ZERO,ONLY_FULL_GROUP_BY',
        },
    }
}

起動しているが、
django.db.utils.OperationalError: (2061, 'RSA Encryption not supported - caching_sha2_password plugin was built with GnuTLS support')
のエラー

対応としては、下記参照
MySQLでcaching_sha2_passwordのエラーが出る
https://qiita.com/miltood/items/9c266a283cee718b2f0b

MySQL8.0新機能 (caching_sha2_password 認証プラグイン)を回避したい。
https://qiita.com/RayDoe/items/baf53818ec8e44d4d148

$ docker exec -it jq-rally-db sh
# mysql -u root -p
mysql> show variables like 'default_authentication_plugin';
+-------------------------------+-----------------------+
| Variable_name                 | Value                 |
+-------------------------------+-----------------------+
| default_authentication_plugin | caching_sha2_password |
+-------------------------------+-----------------------+
mysql> exit
# cat /etc/mysql/my.cnf

注意:従来はパスワードなしで入れていたが、mysql 8.0 で変わっている。

/etc/mysql/my.cnf は編集できない(viはインストールされていない)ので、
これが参照している
/etc/mysql/conf.d/mysql.cnf
に追記してあげる。
ついでに、文字コードもセットしておく。
下記で、ファイル全上書きします。

# cat - > /etc/mysql/conf.d/mysql.cnf
[mysql]
default_authentication_plugin=mysql_native_password
default-character-set=utf8mb4

[mysqld]
character-set-server = utf8mb4
skip-character-set-client-handshake
collation-server = utf8mb4_general_ci
init-connect = SET NAMES utf8mb4

Ctrl+Cで抜けます。

しかし、これはだめでした。

# mysql -u root -p
mysql: [ERROR] unknown variable 'default_authentication_plugin=mysql_native_password'.

いったん戻して(この行だけ消しました)、こちらを参照。

PythonからMySQLへの接続でcaching_sha2_passwordのエラーが出た場合
https://qiita.com/satoryosato@github/items/611809ebfb28c689ae8e

作成するユーザjqは、自身のアプリに応じて名前を決めてください。

# mysql -u root -p
mysql> use mysql;
mysql> create user 'jq'@'%' identified WITH mysql_native_password by 'password';
mysql> grant all on rally_database.* to 'jq'@'%' with grant option;
mysql> flush privileges;

mysql> select user, host, plugin from mysql.user;
+------------------+-----------+-----------------------+
| user             | host      | plugin                |
+------------------+-----------+-----------------------+
| jq               | %         | mysql_native_password |
| root             | %         | caching_sha2_password |
| jq               | localhost | mysql_native_password |
| mysql.infoschema | localhost | caching_sha2_password |
| mysql.session    | localhost | caching_sha2_password |
| mysql.sys        | localhost | caching_sha2_password |
| root             | localhost | caching_sha2_password |
+------------------+-----------+-----------------------+

# mysql -u jq -p
mysql> use rally_database;
Database changed

jqで接続すれば、mysql_native_password で接続できるはず。

こうした上で、作成したユーザで接続するよう、settings.py を修正。
docker再起動。

jq-rally-api/config/settings.py
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'rally_database',
        'USER': 'jq',
        'PASSWORD': 'password',
        'HOST': 'db',
        'PORT': 3306,
        'OPTIONS': {
            'charset': 'utf8mb4',
            'sql_mode': 'TRADITIONAL,NO_AUTO_VALUE_ON_ZERO,ONLY_FULL_GROUP_BY',
        },
    }
}

localhost:8000 を開くと
Django 3.2
The install worked successfully! Congratulations!

という画面が表示されます。

アプリケーション追加

これも、基本的に上述の下記サイトの通りです。

https://qiita.com/mykysyk@github/items/fef6fb298393a029a5d4#sample_app-アプリケーション作成

途中エラー
'staticfiles' is not a registered tag library. Must be one of:
admin_list
・・・

下記で、staticfiles ではなく、static にします。

$ docker exec -it jq-rally-api /bin/bash
cat /usr/local/lib/python3.8/site-packages/django/template/defaulttags.py
grep -A 3 -B 1 -n staticfiles /usr/local/lib/python3.8/site-packages/rest_framework_swagger/templates/rest_framework_swagger/index.html
sed -i -e "s/load staticfiles/load static/" /usr/local/lib/python3.8/site-packages/rest_framework_swagger/templates/rest_framework_swagger/index.html

localhost:8000/swagger が開くようになります。

jq-rally-api/config/settings.py
djangorestframework-jwt の有効化

以上で、Django REST framework のプロジェクト生成が一通り完了となります。

Nuxtアプリケーション

アプリケーション作成

Dockerfile 作成

jq-rally-web/Dockerfile
FROM node:14.17.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

続いて、docker-compose.yml の最後の1行以外を有効化

docker-compose.yml
  web:
    container_name: jq-rally-web
    build: ./jq-rally-web
    ports:
      - 3000:3000
    volumes:
      - ./jq-rally-web:/app
      - /app/node_modules
    tty: true
    working_dir: /app
  #   command: sh -c "cd jq-rally/ && yarn install && yarn dev"
docker compose up --build

途中に
jq-rally-web | Welcome to Node.js v14.17.0.
というメッセージが見えます。

別のターミナルで、create nuxt-app を実行します。
TypeScript にして、Vuetify.js を使います。

$ docker exec -it jq-rally-web sh
/app# yarn create nuxt-app jq-rally

create-nuxt-app v3.7.0
✨  Generating Nuxt.js project in jq-rally
? Project name: jq-rally
? 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

docker compose を一旦終了し、docker-compose.yml の最後の1行を有効化し、docker compose up

docker compose up --build

localhost:3000

vueのロゴが変わった・・・
https://zenn.dev/jqinglong/articles/c8843bce4c475e

composition-api追加

本構成の大きなポイントである composition-api 追加をやっておきます。

$ docker exec -it jq-rally-web sh
/app # cd jq-rally/
/app/jq-rally # yarn add @nuxtjs/composition-api

特に問題なく動いている。念の為再起動しても問題なし。

Typescript関連環境調整

nuxt.configをTSに変更

git mv jq-rally-web/jq-rally/nuxt.config.js jq-rally-web/jq-rally/nuxt.config.ts

shims-vue.d.ts ファイル追加

tsconfig.json にちょっと先の話も含めて追記
firebase、jestはまだコメントアウト

jq-rally-web/jq-rally/tsconfig.json
{
  "compilerOptions": {
    "target": "ES2018",
    "module": "ESNext",
    "moduleResolution": "Node",
    "lib": [
      "ESNext",
      "ESNext.AsyncIterable",
      "DOM"
    ],
    "esModuleInterop": true,
    "allowJs": true,
    "sourceMap": true,
    "strict": true,
    "noEmit": true,
    "experimentalDecorators": true,
    "baseUrl": ".",
    "paths": {
      "~/*": [
        "./*"
      ],
      "@/*": [
        "./*"
      ]
    },
    "resolveJsonModule": true,
    "types": [
      "@nuxt/types",
      "@nuxtjs/axios",
      // "@nuxtjs/firebase",
      "@types/node",
      "vuetify",
      // "@types/jest",
      // "vue-scrollto"
    ]
  },
  "files": [
    "shims-vue.d.ts"
  ],
  "include": [
    "*.ts",
    "api/**/*.ts",
    "components/**/*.ts",
    "components/**/*.vue",
    "compositions/**/*.ts",
    "constants/**/*.ts",
    "layouts/**/*.ts",
    "layouts/**/*.vue",
    "store/**/*.ts",
    "plugins/**/*.ts",
    "utils/**/*.ts",
    "types/**/*.ts",
    "pages/**/*.ts",
    "pages/**/*.vue",
    "test/**/*.ts"
  ],
  "exclude": [
    "node_modules",
    ".nuxt",
    "dist"
  ]
}

そして、上階層にtsconfig.jsonの拡張用のファイルを置くことで、エディタ上のエラーを回避します。

tsconfig.json
{
  "extends": "./jq-rally-web/jq-rally/tsconfig.json",
  "compilerOptions": {
    "baseUrl": "./jq-rally-web/jq-rally",
  }
}

いったん
no such file or directory, open '/app/jq-rally/nuxt.config.js'
のエラーが発生しますが、再起動すれば問題なし

MemoデータのCRUDを作る(まずはREAD)

長くなりましたが、ここで画面作成。
API経由で、MemoデータのCRUDをおこないます。

まず、メニュー追加。修正箇所抜粋。追加の前のカンマも追加してますが。

jq-rally-web/jq-rally/layouts/default.vue
      items: [
        {
          icon: 'mdi-apps',
          title: 'Welcome',
          to: '/'
        },
        {
          icon: 'mdi-chart-bubble',
          title: 'Inspire',
          to: '/inspire'
        },
        // 追加
        {
          icon: 'mdi-pencil',
          title: 'Memo',
          to: '/memo'
        },
      ],

ページ追加。フォルダ作成して、index.vue を置く形にしておきます。

jq-rally-web/jq-rally/pages/memo/index.vue
<template>
  <div>
    <v-card dark>
      <v-card-title class="pa-2">
        <h3 class="title grow">メモ</h3>
      </v-card-title>
    </v-card>
    <!-- メモリスト -->
    <!-- <memo-list /> -->
  </div>
</template>

<script lang="ts">
// import {
//   defineComponent,
// } from '@nuxtjs/composition-api'
// import MemoList from '~/components/memo/MemoList.vue'

// export default defineComponent({
//   components: { MemoList },
//   setup() {
//     return {
//     }
//   }
// })
</script>

コンポーネントを呼び出すスクリプトはコメントアウトしておきます。
そういうことしなくて良いように、Storybook を使うんですよね、きっと。
そこはまだ導入してません。

で、コンポーネントを作りますが、その手順はこんな感じです。

jq-rally-web/jq-rally/types/index.ts
jq-rally-web/jq-rally/api/xxxRepository.ts
jq-rally-web/jq-rally/api/index.ts
jq-rally-web/jq-rally/api/createRepository.ts
jq-rally-web/jq-rally/compositions/useXxx.ts
jq-rally-web/jq-rally/compositions/index.ts

types/index.ts 最初なのでファイルを作りますが、以降何かコンポーネントを作る時は、追記していきます。

jq-rally-web/jq-rally/types/index.ts
export type ResponseType<V> = Promise<V>
export type ResponseMapType<K extends string, V> = Promise<{ [P in K]: V }>

export type ResponseTypes<T> = Promise<T>

export type CustomErrors = {
  errors: {
    errorName: string[]
  }
}

export interface ListRequestType  {
  limit?: number
  offset?: number
}

export interface MemoType {
  id: number
  title: string
  memo: string
}

同じフォルダに、plugin-types.d.ts も置いておきます。

jq-rally-web/jq-rally/types/plugin-types.d.ts
import { Repository } from '@/api'

declare module 'vue/types/vue' {
  interface Vue {
    $repository: Repository
  }
}

declare module '@nuxt/types' {
  interface NuxtAppOptions {
    $repository: Repository
  }
  interface Context {
    $repository: Repository
  }
}

declare module 'vue/types/vue' {
  interface Vue {
    $scrollTo: any
  }
}

これは最初に作っておけば、以降は気にしなくて良いです。

jq-rally-web/jq-rally/constants/index.ts
export const ErrorType = {
  Unauthorized: 401,
  Forbidden: 403,
  NotFound: 404,
  Unprocessable: 422,
}

export const LIMIT_LIST_ITEM = 20
export const LIMIT_LIST_ITEM_LARGE = 60

これをデータごとに作っていきます。

jq-rally-web/jq-rally/api/memoRepository.ts
import { NuxtAxiosInstance } from '@nuxtjs/axios'
import {
  Memo,
  ResponseType,
  ResponseTypes,
  CustomErrors,
} from '@/types'
import { LIMIT_LIST_ITEM } from '@/constants'

type Memoid = Memo['id']

export interface MemoListRequest  {
  limit?: number
  offset?: number
}
export type CreateMemoRequest = Pick<
  Memo,
  'title' | 'memo'
>
export type UpdateMemoPayload = Partial<CreateMemoRequest>
export type UpdateMemoRequest = {
  payload: UpdateMemoPayload
  id: Memoid
}

// type MemoResponse = ResponseType<'memoData', Memo>
type MemoResponse = ResponseType<Memo>

// type MemoListResponse = ResponseTypes<{
//   memos: Memo[]
//   memosCount: number
// }>
type MemoListResponse = ResponseTypes<Memo[]>

export const memoRepository = (axios: NuxtAxiosInstance) => ({
  getMemo(memoid: Memoid): MemoResponse {
    return axios.$get(`/memo/${memoid}`)
  },

  getMemoList({
    limit = LIMIT_LIST_ITEM,
    offset = 0,
  }: MemoListRequest = {}): MemoListResponse {
    const defaultParam = {
    }

    console.log('getMemoList', axios.$get('/memo', {      params: {...defaultParam, limit, offset},    }))

    return axios.$get('/memo/', {
      params: {...defaultParam, limit, offset},
    })
  },
  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}/`, request.payload )
  },
  deleteMemo(memoid: Memoid) {
    return axios.$delete(`/memo/${memoid}/`)
  },

})

export type MemoRepository = ReturnType<typeof memoRepository>

apiを増やすごとに追加していきます。

jq-rally-web/jq-rally/api/index.ts
export * from './createRepository'
export * from './memoRepository'

これもapiを増やすごとに、最初と最後の部分を追加していきます。

jq-rally-web/jq-rally/api/createRepository.ts
import { Context } from '@nuxt/types'
import {
  memoRepository,
  MemoRepository,
} from '@/api'
import { ErrorType } from '@/constants'

export type Repository = {
  memo: MemoRepository
}

/**
 * @see https://axios.nuxtjs.org
 * @see https://github.com/gothinkster/realworld/tree/3155494efe68432772157de38a90c49b3698897f/api
 */
const createRepository = ({ app, $axios, redirect }: Context): Repository => {
  $axios.onError((error) => {
    console.log('createRepository error', error, error.response?.data)
    if (!error.response) {
      return
    }

    const code = error.response.status

    if (code === ErrorType.Unprocessable) {
      return Promise.reject(error.response.data.console.errors)
    }
    if (code === ErrorType.Unauthorized) {
      redirect('/login')

      return
    }

    if (code === ErrorType.Forbidden) {
      app?.router?.back()

      return
    }

    if (code === ErrorType.NotFound) {
      redirect('/')
    }
  })

  console.log('createRepository', $axios)

  return {
    memo: memoRepository($axios),
  }
}

export default createRepository

実際画面から呼び出す処理。

jq-rally-web/jq-rally/compositions/useMemo.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 = {
  memoData: 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>({
    memoData: {
      id: 0,
      title: '',
      memo: '',
    },
    memoList: [],
    memoCount: 0,
  })
  const getMemo = async (memoid: Memo['id']) => {
    const memoData = await $repository.memo.getMemo(memoid)
    console.log('memoid', memoid)
    console.log('getMemo', memoData)

    state.memoData = memoData
  }

  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)

    console.log('createMemo response', response)
    if (response) {
      await getMemoList()
      return response
    }

    return false
  }

  const updateMemo = async (payload: UpdateMemoRequest) => {
    const memoData = await $repository.memo.updateMemo(payload)

    if (memoData) {
      return memoData
    }

    return false
  }

  const deleteMemo = async (memoid: Memo['id']) => {
    await $repository.memo.deleteMemo(memoid)
    await getMemoList()
  }
  return {
    createState,
    state,
    getMemo,
    getMemoList,
    createMemo,
    updateMemo,
    deleteMemo,
  }
}

処理追加ごとに追記

jq-rally-web/jq-rally/compositions/index.ts
import useMemo from "./useMemo";

export {
  useMemo,
}

準備が整ったので、画面コンポーネントを作成

jq-rally-web/jq-rally/components/memo/MemoList.vue
<template>
  <div>
    <v-card dense>
      <v-data-table
        :headers="headers"
        :items="memoList"
        :search="search"
        hide-default-header
        dense>
        <template v-slot:item="{ item }">
          {{ item }}
        </template>
      </v-data-table>
    </v-card>
  </div>
</template>

<script lang="ts">
import { reactive, toRefs, useFetch, defineComponent } from '@nuxtjs/composition-api';
import { useMemo } from '@/compositions';

export default defineComponent({
  name: 'MemoList',
  setup() {
    const headers = [
      { text: 'タイトル', value: 'title' },
      { text: 'メモ', value: 'memo' },
      { text: '', value: 'actions', sortable: false },
    ];
    const state = reactive({
      search: '',
      filter: {},
      sortDesc: false,
    });
    const { state: memoState, getMemoList } = useMemo();

    const fetchData = async (offset = 0) => {
      await getMemoList({ offset });
    };

    const { fetchState } = useFetch(() => fetchData());

    return {
      headers,
      ...toRefs(state),
      ...toRefs(memoState),
      fetchState,
    };
  },
});
</script>

そして、pageのコメントを外す

jq-rally-web/jq-rally/pages/memo/index.vue
<template>
  <div>
    <v-card dark>
      <v-card-title class="pa-2">
        <h3 class="title grow">メモ</h3>
      </v-card-title>
    </v-card>
    <!-- メモリスト -->
    <memo-list />
  </div>
</template>

<script lang="ts">
import {
  defineComponent,
} from '@nuxtjs/composition-api'
import MemoList from '~/components/memo/MemoList.vue'

export default defineComponent({
  components: { MemoList },
  setup() {
    return {
    }
  }
})
</script>

ここまでやると、こんなエラー

./node_modules/@nuxtjs/composition-api/dist/runtime/index.mjs 276:47-55
Can't import the named export 'computed' from non EcmaScript module (only default export is available)
./node_modules/@nuxtjs/composition-api/dist/runtime/index.mjs 303:11-19
Can't import the named export 'computed' from non EcmaScript module (only default export is available)
./node_modules/@nuxtjs/composition-api/dist/runtime/index.mjs 304:11-19
Can't import the named export 'computed' from non EcmaScript module (only default export is available)
・・・ずっと続く

ここまで現時点の推奨版node:14.17でやっていましたが、16.3に変更

docker compose up --build

そういう問題でもなかった
問題は
./node_modules/@nuxtjs/composition-api/dist/runtime/index.mjs
なので、いちど削除
バージョン指定して再インストール

$ docker exec -it jq-rally-web sh
/app # cd jq-rally/
/app/jq-rally # yarn remove @nuxtjs/composition-api

/app/jq-rally # yarn add '@nuxtjs/composition-api@^0.22.4'

composition-api を入れた後は、
nuxt.config.ts への追記も忘れずに。

jq-rally-web/jq-rally/nuxt.config.ts
  plugins: [
    '@/plugins/composition-api',
    '@/plugins/repository',
    // '@/plugins/vue-scrollto'
  ],

  buildModules: [
    // https://go.nuxtjs.dev/typescript
    '@nuxt/typescript-build',
    // https://go.nuxtjs.dev/vuetify
    '@nuxtjs/vuetify',
    '@nuxtjs/composition-api'
  ],


  axios: {
    baseURL: 'http://localhost:8000/api/',
  },
jq-rally-web/jq-rally/plugins/composition-api.ts
import Vue from 'vue'
import VueCompositionApi from '@vue/composition-api'

Vue.use(VueCompositionApi)
jq-rally-web/jq-rally/plugins/repository.ts
// import { MemoRepository } from "@/repositories/memoRepository";

// export interface Repositories {
//   memo: MemoRepository
// }

// export default function( { $axios }: any, inject: any) {
//   const memo = new MemoRepository($axios)
//   const repositories: Repositories = {
//     memo
//   }
//   inject('repositories', repositories)
// }

import { Plugin } from '@nuxt/types'

import createRepository from '@/api/createRepository'

const repository: Plugin = (ctx, inject) => {
  const repositoryWithAxios = createRepository(ctx)

  inject('repository', repositoryWithAxios)
}

export default repository

API側でCORS対応もしておく

jq-rally-api/config/settings.py
MIDDLEWARE = [
    'corsheaders.middleware.CorsMiddleware',
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

CORS_ORIGIN_WHITELIST = (
    'http://localhost:3000',
    'https://jqrally-web.netlify.app',
)

    'corsheaders.middleware.CorsMiddleware',

    'django.middleware.common.CommonMiddleware',

より上に

これで、データ表示がされました。

MemoデータのCRUDを作る(CUD)

constを定義しておいて

jq-rally-web/jq-rally/compositions/util/const.ts
import { MemoType } from "@/types";

export const defaultMemoItem: MemoType = {
  id: 0,
  title: '',
  memo: '',
};

CRUD用のコンポーネント作成

jq-rally-web/jq-rally/components/memo/MemoAdmin.vue
<template>
  <v-card>
    <v-card-title> メモの管理 </v-card-title>
    <v-card-text>
      <!-- メモ追加 -->
      <v-card>
        <v-card-text>
          <v-form lazy-validation>
            <v-text-field
              label="タイトル"
              dense
              outlined
              clearable
              v-model="form.title"
            >
            </v-text-field>
            <v-text-field
              label="メモ"
              dense
              outlined
              clearable
              v-model="form.memo"
            >
            </v-text-field>
            <v-btn color="primary" @click="handleCreateMemo">新規メモ</v-btn>
          </v-form>
        </v-card-text>
        <v-snackbar v-model="snackbar">
          更新しました。
          <template v-slot:action="{ attrs }">
            <v-btn
              color="success"
              outlined
              text
              v-bind="attrs"
              @click="snackbar = false"
            >
              Close
            </v-btn>
          </template>
        </v-snackbar>
      </v-card>

      <!-- メモ一覧 -->
      <v-data-table :headers="headers" :items="memoList" :search="search" dense>
        <template v-slot:top>
          <v-text-field
            dense
            v-model="search"
            clearable
            flat
            solo-inverted
            hide-details
            prepend-inner-icon="mdi-magnify"
            label="Search"
          ></v-text-field>
        </template>

        <template v-slot:[`item.actions`]="{ item }">
          <v-icon small @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="dialogEdit" max-width="500px">
        <v-card>
          <v-card-title class="headline">構成員情報更新</v-card-title>
          <v-text-field
            label="タイトル"
            dense
            outlined
            clearable
            v-model="editedItem.title"
          >
          </v-text-field>
          <v-text-field
            label="メモ"
            dense
            outlined
            clearable
            v-model="editedItem.memo"
          >
          </v-text-field>
          <v-card-actions>
            <v-spacer></v-spacer>
            <v-btn color="blue darken-1" text @click="updateItem">更新</v-btn>
            <v-btn color="blue darken-1" text @click="close"
              >Cancel</v-btn
            >
            <v-spacer></v-spacer>
          </v-card-actions>
        </v-card>
      </v-dialog>

      <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="close"
              >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 { useMemo } from "@/compositions";
import { defaultMemoItem } from "@/compositions/util/const";

import { MemoType } from "@/types";

export default defineComponent({
  name: "MemoAdmin",
  setup(_, { root }) {
    const headers = [
      { text: "タイトル", value: "title" },
      { text: "メモ", value: "memo" },
      { text: "", value: "actions", sortable: false },
    ];

    const state = reactive({
      search: "",
      dialogDelete: false,
      dialogEdit: false,
      editedIndex: 0,
      editedItem: defaultMemoItem,
      snackbar: false,
    });

    const {
      state: memoState,
      createState: createMemoState,
      createMemo,
      updateMemo,
      getMemoList,
      deleteMemo,
    } = useMemo();

    const editItem = (item: MemoType) => {
      state.editedIndex = item.id;
      state.editedItem = item;
      console.log("item", state.editedItem);
      state.dialogEdit = true;
    };
    const updateItem = async () => {
      console.log("MemoAdmin updateItem", state.editedItem);
      await updateMemo({ id: state.editedItem.id, payload: state.editedItem });
      close();
    };
    const deleteItem = (item: MemoType) => {
      state.editedIndex = item.id;
      state.editedItem = item;
      state.dialogDelete = true;
    };
    const deleteItemConfirm = async () => {
      console.log("deleteItemConfirm", state.editedItem);
      const memoid: number = state.editedItem.id;
      deleteMemo(memoid);
      // fetchData()
      close();
    };
    const close = () => {
      state.dialogEdit = false;
      state.dialogDelete = false;
      root.$nextTick(() => {
        state.editedItem = Object.assign({}, defaultMemoItem);
        state.editedIndex = 0;
      });
    };

    const handleCreateMemo = async () => {
      try {
        console.log("createMemoState", createMemoState);
        const newMemo = await createMemo(createMemoState);

        if (!newMemo) {
          return;
        }
        state.snackbar = true;
      } catch (error) {
        console.log("error", error);
      }
    };

    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,
      editItem,
      updateItem,
      deleteItem,
      deleteItemConfirm,
      close,
    };
  },
});
</script>

<style>
</style>

コンポーネントをページに表示します。

jq-rally-web/jq-rally/pages/memo/index.vue
<template>
  <div>
    <!-- メモ編集 -->
    <memo-admin />
  </div>
</template>

<script lang="ts">
import {
  defineComponent,
} from '@nuxtjs/composition-api'
import MemoAdmin from '~/components/memo/MemoAdmin.vue'

export default defineComponent({
  components: { MemoAdmin },
  setup() {
    return {
    }
  }
})
</script>

git add .
git commit -m "CRUD"
git push

トップページデザイン

Carousels
https://vuetifyjs.com/ja/components/carousels/#cycle

Latest
Two lines and subheader
https://vuetifyjs.com/ja/components/lists/#two-lines-and-subheader

以上!
やっぱりまとまりないですよね・・・。

Discussion