Vite(Vue3.2)で始めるTodoアプリ開発
はじめに
今回は、Vueのビルド高速版、Viteでナビゲーション付きToDoアプリの開発を行っていきます。UIは一般的なBootstrap5.0を採用しています。
前回、Vue2+Vuetifyで公開しましたが、Vue3対応は先が長そうですし、今後はVue3.2(2021/8リリース)のscript setupを利用しようかと思い、こちらの記事を作成しました。
WSL2での開発環境構築
VSCodeの拡張機能をインストールします。
# update
sudo apt update
sudo apt upgrade
# npm
sudo apt-get install curl
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash
→ 時間がかかる。
source .bashrc
nvm install --lts
# Japanese Language Pack for Visual Studio
code --install-extension MS-CEINTL.vscode-language-pack-ja
# ESLint
code --install-extension dbaeumer.vscode-eslint
# Prettier
code --install-extension esbenp.prettier-vscode
# Vue Language Features (Volar) ※Veturは無効にすること
code --install-extension Vue.volar
もし、他にも手動で拡張機能をコマンドで追加した場合は、以下のコマンドで抽出できます。
code --list-extensions | xargs -L 1 echo code --install-extension
プロジェクト作成
Vueとは異なり、事前インストールがなく、セットアップもシンプルです。
> npm init vite@latest
√ Project name: ... todo
√ Select a framework: » vue
√ Select a variant: » vue-ts
> cd todo
> npm install
> npm run dev
→ 起動確認。http://localhost:5173/
環境設定
ESLint, Prettierをインストールし、設定ファイルを追加します。
ESLintとPretterの設定は、いろいろな記事があって混乱するのですが、結局これだけでも十分だと思います。他、必要なものがあれば教えてください!
> npm i -D eslint @vue/eslint-config-typescript
→ @vue/eslint-config-typescript ... typescript対応
> npm i -D prettier eslint-config-prettier
→ eslint-config-prettier ... eslintとprettierの競合を防ぐ
- フォーマッタをPrettierにする。jsonやtsは、vscode標準が適用されるため、上書き
- 保存、ペースト、改行時に整形する。ただし、Vueファイルは保存時のみ。
{
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"editor.formatOnPaste": true,
"editor.formatOnType": true
}
- printWidth ... 単純に行数が減るため。
- endOfLine ... 新規vue時の「Delete ␍」対策、要VSCode再起動
- htmlWhitespaceSensitivity ... タグ改行時の空白を許可する。
{
"printWidth": 150,
"endOfLine": "auto",
"htmlWhitespaceSensitivity": "ignore"
}
- 命名規則の追加、ソースの記述に統一感が出る。あとはお好みで。
/* eslint-disable @typescript-eslint/naming-convention */
module.exports = {
root: true,
env: {
// ブラウザ、node.jsを利用
browser: true,
node: true,
// es2021を利用
es2021: true,
},
// prettierを入れないと、eslintで競合してwarningになる。
extends: ["plugin:vue/vue3-recommended", "@vue/typescript/recommended", "prettier"],
parserOptions: {
ecmaVersion: 2021,
},
rules: {
// console.logを許可
"no-console": "off",
// ネーミングルールを追加
"@typescript-eslint/naming-convention": [
"error",
{
selector: "default",
format: ["camelCase"],
},
{
selector: ["property"],
format: ["camelCase", "PascalCase"],
},
{
selector: ["class", "enum", "interface", "typeAlias", "typeParameter"],
format: ["PascalCase"],
},
{
selector: "variable",
modifiers: ["const"],
format: ["camelCase", "UPPER_CASE"],
},
],
},
};
- eslintとpretterを一括実行できるようにする。
"scripts": {
...
"lint": "eslint --ext .ts,vue --ignore-path .gitignore .",
"lint-fix": "eslint --ext .ts,vue --ignore-path .gitignore . && prettier --write src/**/*.{ts,vue}"
},
Bootstrap 5 + SB Admin + Font Awesome
次にBootstrap 5で開発するための準備を行います。とはいっても、画面を1からデザインするのは大変なので、Start Bootstrapの中からダウンロード数が多いSB AdminをTemplateにして、作成していきます。
その際、Font Awesomeというアイコンフォントが使われているので、一緒にインストールします。Vue.jsで使うときは、vue-fontawesomeを使います。
> npm i startbootstrap-sb-admin
→ bootstrap5も含まれる。
> npm i @popperjs/core
→ linux上でビルドする際に使用。
> npm i --save @fortawesome/fontawesome-svg-core
> npm i --save @fortawesome/free-solid-svg-icons
> npm i --save @fortawesome/vue-fontawesome@prerelease
> npm i -D sass
→ sass/scssを利用ときは追加する。
次に、インストールしたファイルを使えるようにmain.tsを編集していきます。
import { createApp } from "vue";
import App from "./App.vue";
// Font Awesome
import { library } from "@fortawesome/fontawesome-svg-core";
import { fas } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { dom } from "@fortawesome/fontawesome-svg-core";
// Bootstrap 5 + SB Admin
import "startbootstrap-sb-admin/src/scss/styles.scss";
// アイコンをライブラリに追加して、DOM経由(class)で参照できるようにする。
library.add(fas);
dom.watch();
// componentを追加
createApp(App).component("font-awesome-icon", FontAwesomeIcon).mount("#app");
これで、SB Adminが利用できる準備ができました。試しに、dist/layout-static.htmlのbodyタグを、src/App.vueのtemplateタグ内にコピー(scriptタグは不要)し、他をすべて削除した状態で、画面を確認してみてください。ナビゲーションバーやスライドバーが表示されたと思います。
ToDo画面作成
今回、ナビゲーション付きのToDo画面を、以下の手順で作成していきます。
- vue-routerでルーティング
- ナビゲーションメニュー
- Todo画面作成
vue-routerでルーティング
以下のパッケージをインストールし、vue-routerを動かしていきます。
> npm i vue-router@4
- 一旦、HomeやToDo画面を空で作成し、App.vueをrouter-viewのみにします。今回は、404ページも、SB Adminを参考に作成してみます。
<template>
<h1 class="mt-4">Home</h1>
</template>
<template>
<h1 class="mt-4">TodoList</h1>
</template>
<template>
<div class="container">
<div class="row justify-content-center">
<div class="col-lg-6">
<div class="text-center mt-4">
<h1 class="display-1">404</h1>
<p class="lead">Page Not Found</p>
<p>ページが見つかりません。</p>
<a href="/">
<i class="fas fa-arrow-left me-1"></i>
Back
</a>
</div>
</div>
</div>
</div>
</template>
<template>
<router-view />
</template>
- routerを作成します。
import { createRouter, createWebHistory, RouteRecordRaw } from "vue-router";
import AppHome from "../views/AppHome.vue";
import TodoList from "../views/TodoList.vue";
import NotFound from "../components/NotFound.vue";
const routers: Array<RouteRecordRaw> = [
{
path: "/",
component: AppHome,
},
{
path: "/todo",
component: TodoList,
},
{
path: "/:pathMatch(.*)*",
component: NotFound,
},
];
const router = createRouter({
history: createWebHistory(),
routes: routers,
});
export default router;
- main.tsにrouterを追加する。
...
import router from "./router";
...
createApp(App).use(router).component("font-awesome-icon", FontAwesomeIcon).mount("#app");
- routerで設定したURLが表示されるか確認する。存在しないページは404が表示される。
ナビゲーションメニュー
SB Adminのdist/layout-static.htmlを参考に、ナビゲーションメニューを部品化していきます。
- ナビゲーションメニューを部品として作成する。
<script lang="ts">
// メニュー用のinterfaceを追加
export interface MenuItem {
type: "heading" | "menu";
title: string;
icon?: string;
url?: string;
}
</script>
<script setup lang="ts">
import { ref } from "vue";
import { useRouter } from "vue-router";
// タイトルとメニューアイテムを設定できるようにする。
defineProps<{ title: string; menuItems: MenuItem[] }>();
const router = useRouter();
const isToggle = ref(false);
const goToUrl = (url?: string) => {
if (url != undefined) {
router.push(url);
}
};
</script>
<template>
<!-- js/script.jsを参考にbodyにtoggleを追加 -->
<body class="sb-nav-fixed" :class="isToggle ? 'sb-sidenav-toggled' : ''">
<nav class="sb-topnav navbar navbar-expand navbar-dark bg-dark">
<!-- titleを可変にし、hrefをrouterで切り替えるように修正 -->
<a class="navbar-brand ps-3" @click="goToUrl('/')">{{ title }}</a>
<!-- isToggleでスライドバーの表示/非表示を切り替える -->
<button id="sidebarToggle" class="btn btn-link btn-sm order-1 order-lg-0 me-4 me-lg-0" @click="isToggle = !isToggle">
<i class="fas fa-bars"></i>
</button>
<!-- 不要な項目は削除 -->
</nav>
<div id="layoutSidenav">
<div id="layoutSidenav_nav">
<nav id="sidenavAccordion" class="sb-sidenav accordion sb-sidenav-dark">
<div class="sb-sidenav-menu">
<!-- menuItemsからメニューが生成されるように修正, サブメニューも改良すれば対応可能 -->
<div v-for="(item, index) in menuItems" :key="index" class="nav">
<div v-if="item.type == 'heading'" class="sb-sidenav-menu-heading">{{ item.title }}</div>
<a v-if="item.type == 'menu'" class="nav-link" @click="goToUrl(item.url)">
<div class="sb-nav-link-icon"><i class="fas fa-tachometer-alt" :class="item.icon"></i></div>
{{ item.title }}
</a>
</div>
</div>
</nav>
</div>
<div id="layoutSidenav_content">
<main>
<div class="container-fluid px-4">
<!-- コンテンツをrouter-viewに変更 -->
<router-view />
</div>
</main>
</div>
</div>
</body>
</template>
- App.vueからAppNavi.vueを呼び出す。アイコンはFont AwesomeのIconsから検索。
<script setup lang="ts">
import AppNavi, { MenuItem } from "./components/AppNavi.vue";
// メニューを設定する。
const menuItems: MenuItem[] = [
{
type: "heading",
title: "Main",
},
{
type: "menu",
title: "ToDo",
icon: "fa-list",
url: "/todo",
},
];
</script>
<template>
<AppNavi title="Todo App" :menu-items="menuItems"></AppNavi>
</template>
- ナビゲーションバーが表示され、メニューで画面が切り替わることを確認する。
Todo画面作成
ここからは、BootstrapのLayout, Content, Formsを見ながら、Todo画面の作成を行います。
ファイル構成としては、各部品はコンポーネント化し、ロジックはTodoList.vueにまとめています。
// interfaceは、別ファイルに分けないと重複エラーになる。
export interface Task {
id: number;
title: string;
done: boolean;
}
<script setup lang="ts">
import { Task } from "../models/Task";
defineProps<{ tasks: Task[] }>();
const emit = defineEmits<{
(eventName: "done", id: number): void;
(eventName: "delete", id: number): void;
}>();
</script>
<template>
<table class="table table-striped align-middle mt-4">
<thead>
<tr>
<th scope="col" width="50">#</th>
<th scope="col" width="600">Task</th>
<th scope="col">Action</th>
</tr>
</thead>
<tbody>
<template v-for="(task, index) in tasks" :key="index">
<tr :class="{ 'table-primary': task.done }">
<td>
<input class="form-check-input" type="checkbox" :checked="task.done" @click="emit('done', task.id)"/>
</td>
<td>
<label v-if="task.done" class="form-check-label">
<del>{{ task.title }}</del>
</label>
<label v-else class="form-check-label">
{{ task.title }}
</label>
</td>
<td>
<button type="button" class="btn btn-danger" @click="emit('delete', task.id)"><i class="fas fa-trash-alt"></i></button>
</td>
</tr>
</template>
</tbody>
</table>
</template>
<script setup lang="ts">
import { ref } from "vue";
const emit = defineEmits<{
(eventName: "add", newTaskTitle: string): void;
}>();
const newTaskTitle = ref("");
const addTask = () => {
// テキストが1文字以上のとき、イベントを起こす。
if (newTaskTitle.value.length > 0) {
emit("add", newTaskTitle.value);
newTaskTitle.value = "";
}
};
</script>
<template>
<form class="form-floating">
<input v-model="newTaskTitle" type="text" class="form-control mt-4" placeholder="Add Task" @keydown.enter.prevent="addTask()" />
<label>Add Task</label>
</form>
</template>
<script setup lang="ts">
import { reactive } from "vue";
import { Task } from "../models/Task";
import TaskAdd from "../components/TaskAdd.vue";
import TaskList from "../components/TaskList.vue";
const tasks: Task[] = reactive([
{
id: 1,
title: "起きる",
done: false,
},
{
id: 2,
title: "着替える",
done: false,
},
]);
// taskを検索し、フラグを更新する。
const addTask = (newTaskTitle: string) => {
let newTask: Task = {
id: Date.now(),
title: newTaskTitle,
done: false,
};
tasks.push(newTask);
};
// taskを検索し、フラグを更新する。
const doneTask = (id: number) => {
let task = tasks.find((t) => t.id === id);
if (task !== undefined) {
task.done = !task.done;
}
};
// taskを削除する。
const deleteTask = (id: number) => {
tasks.forEach((task, index) => {
if (task.id == id) tasks.splice(index, 1);
});
};
</script>
<template>
<h1 class="mt-4">Todo List</h1>
<div class="row">
<div class="col-xl-6 col-md-6">
<TaskAdd @add="(newTaskTitle) => addTask(newTaskTitle)"></TaskAdd>
<TaskList :tasks="tasks" @delete="(id) => deleteTask(id)" @done="(id) => doneTask(id)"></TaskList>
</div>
</div>
</template>
終わりに
Bootstrap 5.0からJQueryが取り除かれ、BootstrapVueを利用しなくても、Bootstrapのサイトを参考にしながら、Vue.jsで利用できるようになりました。
また、viteでビルドが高速になり、Vue3.2から導入されたscript setup構文で、CompostionAPIがシンプルな実装になりました。いくつか出てきた新しい記述(defineEmits、defineProps、useRouterなど)は、たぶんソースで理解できると思います。
他にも、ToDoアプリに関連する記事も掲載していますので、参考にして頂ければ幸いです。
Discussion