トップページから遷移する画面の作成
今まででトップページしかなかったので、今回はトップページから遷移し、DBのデータを検索するページを作成する
全体的には以下のような構成
トップ → データ検索するテーブル選択画面 → 各テーブルのデータを検索する画面
内容としてはだいぶシンプルなのでわかりやすいはず
DBサーバーの作成
DBサーバーとして今回はLubuntuをいれているPCをDBサーバーとして使用する
(仮想環境でもいいが、今回はちょうど使っていないPCがあるので試してみる)
MySQLサーバーをインストールして、Lubuntuのipアドレスをメモしておく
※今回の趣旨とずれるので細かいことは説明しない
DBサーバー側の準備ができたらWindows側でもDBにアクセスしてデータを取得する準備を行う
ここでは、セキュリティの観点(SQLインジェクションなど)からNuxt(ブラウザ)から直接MySQLには接続できないので、NuxtのサーバーAPI機能(Nitro)を使用する
ルートディレクトリに server/api フォルダを作成して、その中にDB接続してデータを取得するjsファイルを置いていく
↓こんな感じになるっぽい(例)
import mysql from 'mysql2/promise'
export default defineEventHandler(async (event) => {
// MySQL接続情報(Lubuntu側)
const connection = await mysql.createConnection({
host: '192.168.1.50', // ← LubuntuのIPアドレスを指定
user: 'your_user', // ← MySQLユーザー名
password: 'your_password', // ← MySQLパスワード
database: 'your_database', // ← DB名
})
const [rows] = await connection.execute('SELECT * FROM users')
await connection.end()
return rows
})
mysql2/promiseは非同期処理用
defineEventHandlerはNuxtのサーバーAPI用関数
準備ができたのでDBからデータがとれるか適当に試してみる
事前に用意した countriesテーブルとdisplayテーブル(ほぼ意味のあるデータはない)のうち、countriesテーブルの内容を見てみる
<div>
<table border="1" cellpadding="5">
<thead>
<tr>
<th>Name</th>
<th>Population</th>
<th>Language</th>
<th>Area (km²)</th>
</tr>
</thead>
<tbody>
<tr v-for="country in countries" :key="country.name">
<td>{{ country.name }}</td>
<td>{{ country.population }}</td>
<td>{{ country.language }}</td>
<td>{{ country.area }}</td>
</tr>
</tbody>
</table>
</div>
ちゃんとDBサーバーから全データを取得できていることが確認できた(つけた覚えがないのになぜかAreaの後ろにkm^2がついているが)
テーブル選択画面・検索画面の作成
テーブル選択画面は単純にテーブル一覧があり、ダブルクリックすると次画面へ遷移するようにしたい
コードは以下のようになった
<template>
<div>
<h1>テーブル選択</h1>
<p>データを検索・CSV出力したいテーブルを選択して下さい</p>
<table border="1">
<thead>
<tr><th>テーブル名</th></tr>
</thead>
<tbody>
<tr v-for="table in tables" :key="table" @dblclick="goTable(table)" style="cursor:pointer">
<td>{{ table }}</td>
</tr>
</tbody>
</table>
</div>
</template>
<script setup>
const { data: tables } = await useFetch('/api/tables') // サーバーAPIからテーブル名一覧を取得する → ["countries", "display"]
const router = useRouter()
function goTable(tableName) {
router.push(`/tables/${encodeURIComponent(tableName)}`)
}
</script>
useFetch()でAPIエンドポイントを指定(今回は'/api/tables')し、取得したデータをtablesという名前の変数に代入している
tablesはテーブル一覧を作成するときに使用する
また、上記でテーブル一覧を持ってくるためのAPIも作成する↓
import mysql from 'mysql2/promise'
export default defineEventHandler(async () => {
const connection = await mysql.createConnection({
host: '192.168.3.5',
user: 'remote_user',
password: 'secrert',
database: 'learn',
})
const [rows] = await connection.query(`SHOW TABLES`)
await connection.end()
// 結果からテーブル名だけ抽出
return rows.map(r => Object.values(r)[0])
})
MySQLの返り値は配列のオブジェクトなので、Object.values(r)[0] でテーブル名だけを取り出して返す
テーブル選択後に遷移する画面も用意しておかないといけないので作成する
<template>
<div>
<h1>{{ tableName }} 検索</h1>
<form @submit.prevent="searchData">
<div v-for="col in columns" :key="col">
<label>{{ col }}: </label>
<input v-model="filters[col]" type="text" />
</div>
<button type="submit">検索</button>
</form>
<table border="1" v-if="results.length">
<thead>
<tr>
<th v-for="col in columns" :key="col">{{ col }}</th>
</tr>
</thead>
<tbody>
<tr v-for="row in results" :key="row.id">
<td v-for="col in columns" :key="col">{{ row[col] }}</td>
</tr>
</tbody>
</table>
</div>
</template>
<script setup>
import { useRoute } from 'vue-router'
import { reactive, ref, onMounted } from 'vue'
const route = useRoute()
const tableName = route.params.tableName
// カラム
const columns = ref([])
// フィルター用データ
const filters = reactive({})
// ページ読み込み時にカラム情報を取得
onMounted(async () => {
const data = await $fetch(`/api/columns?table=${encodeURIComponent(tableName)}`)
columns.value = data
// filters をカラムごとに初期化
columns.value.forEach(col => filters[col] = '') })
// 検索結果
const results = ref([])
async function searchData() {
const data = await $fetch('/api/search', {
method: 'POST',
body: { table: tableName, filters: filters }
})
results.value = data
}
</script>
まずファイル名について、Nuxt3では[パラメータ名].vueのように[]を使うと動的ルートとして扱われるためこのファイル名になっている
ルート情報は route.params.tableName でURLからテーブル名を取得し、tableNameに代入される
onMounted()内でページ読み込み時の処理を記述していて、columns.value.forEach(col => filters[col] = '' でそれぞれの入力フィールドの初期値を空文字にする
また、filtersはv-modelを使用して自動的に値を更新している
このfiltersは検索する際の条件として利用する(nameがJapanなど)
searchData()関数では検索APIを呼び出してdataに代入する
その後、リアクティブなresultsに結果を代入することで自動的に検索結果が反映される
実際の画面描画を試してみる
テーブル選択画面は問題なさそう(見づらい以外は)
続いてcountriesをダブルクリックして遷移する
問題なく表示されている
続いて検索処理のテストを行うため、検索APIを作成する
import mysql from 'mysql2/promise'
export default defineEventHandler(async (event) => {
const body = await readBody(event)
const { table, filters } = body
const connection = await mysql.createConnection({
host: '192.168.3.5',
user: 'remote_user',
password: 'Daichan0404',
database: 'learn',
})
// WHERE句を作成
const whereClauses = []
const values = []
for (const [key, value] of Object.entries(filters)) {
if (value) {
whereClauses.push(`${key} LIKE ?`)
values.push(`%${value}%`)
}
}
const where = whereClauses.length ? `WHERE ${whereClauses.join(' AND ')}` : ''
const [rows] = await connection.execute(`SELECT * FROM ${table} ${where}`, values)
await connection.end()
return rows
})
動きを試してみる
countriesテーブル選択後、name入力フォームに「J」を入力後に検索ボタンを押下
部分一致検索なので、正常に検索が動いている
レイアウト修正
画面内の動きは問題なさそうだったので、幅や大きさなどの修正を行い見やすくする
修正後の各画面↓
もっと改善点はあると思うがとりあえずこんなもんで...
最後に
というわけでNuxt, Vueを使用して簡単なアプリケーションを作ることができた。(CSV出力は面倒になったので存在しないことになりました)
色々わからない点は多かったが、なんとか完成できてとりあえずよかった。
Nuxt,Vueは思ってたよりもシンプルで分かりやすいものが多かったので、興味がある人はぜひ。
参考
Nuxt.jsのサーバー機能
https://tech-education-nav.com/contents/educational-materials/nuxt-js/nuxtjs-server-overview-and-use-cases
リアクティブ
https://ja.vuejs.org/guide/essentials/reactivity-fundamentals