Vueをやることになったので、チュートリアルをやってみました。
どうせなので、Vue3で始めようと思ったのですが、Vue3のチュートリアルが見つからなかったので、「基礎から学ぶ Vue.js」にて公開されているToDoリストのチュートリアルをVue3で書いてみました。
最終的なコードはGitHubにおいてあります。
基本的には以下の手順でおこないました。
- vue3 + Typescript + vite で環境を構築
- 上記チュートリアルのコードをなるべくコピペして動作するようにする。
- composition APIで書き換える。
- Element-plusで見栄えを整える。
- vue-routerを入れる。
見栄えをお手軽に良くしたかったので、UIのフレームワークを入れる予定でしたが、
Vue3に対応しているElement-plusを入れてみました。
Element-plusを入れるとnpm run dev
では、問題なさそうでしたが、
ビルド時にvue-router
関係のエラーが出たので、
vue-router
も入れる予定だったので、入れて簡単なルーティングをつけてみました。
ファイル構成
最終版のファイル構成は、以下のような感じになっています。
src
├── App.vue
├── components
│ ├── HelloWorld.vue
│ └── ToDo.vue //ToDoリストのメインのコンポーネント
├── libs
│ ├── AddToDo.ts //AddTodoメソッドを別ファイルに書き出した。
│ └── local_storage.ts //ローカルストレージとのやり取りも別ファイルに書き出した。
├── main.ts
├── types
│ └── todos.ts //ToDoリストで使う型の宣言
└── views
└── Home.vue //vue-routerで使った仮のホームページ
環境構築
以下のコマンドでプロジェクトフォルダを作成しました。
npm init @vitejs/app
✔ Project name: … sample
✔ Select a framework: › vue
✔ Select a variant: › vue-ts
エディタをVSCodeを使用しましたが、Vue用の言語ライブラリはVolar
を使用します。VSCodeのおすすめ通りにインストールすると、Vetur
を入れてしまうので、機能を切り替えておく。
(参考:Zenn: Vue3 + TypeScript + Tailwindの環境構築)
以下にざっと各ファイルの説明をしてみます。
#App.vue
ソースコードはこちら(App.vue)
7-13行目のところで、ローカルストレージに初期値を登録しました。
let todos:ATodo[] = [
{ "id": 0, "comment": "新しいToDo1", "state": 0 },
{ "id": 1, "comment": "新しいToDo2", "state": 0 }
];
todoStorage.save(todos);
todoStorage.uid = todos.length;
<template>
はvue-router
ようにルーティングのサンプルがおいてあります。
libs/local_storage.ts
ソースコードはこちら(libs/local_storage.ts)
基本的にはTodoのページにある内容をそのままコピーしましたが、他の場所でuid
についてのエラーがでたので、
6行目にプロパティとして追加しました。
uid:0,
また、後述しますが、fetch()
の返り値については、reactiveな変数を返すように変更しました。
libs/AddToDo.ts
ソースコードはこちら(libs/AddToDo.ts)
これは、compositionAPIにする際に、コンポーネントから抜き出した関数になります。ToDo.vue
のsetup()
から呼び出しています。
抜き出す際に以下の2つを引数として設定しました。
- todoの配列となっている
todos
-
ToDo.vue
で使用している<form>
で扱うデータの型の変数form
export function doAddTodo(todos:ATodo[],form:Form){
#types/todos.ts
ソースコードはこちら(types/todos.ts)
このファイルは、型の定義を記述しました。
todoの型となるAToDo
型(実際のtodoリストの型はAToDoの配列AToDo[]
としています。)と、<form>
で使用されるデータ型です。
#components/ToDo.vue
ソースコードはこちら(components/ToDo.vue)
こちらがtodoリストのメインのcomponentになります。
- compositionAPI
compositionAPIはVue3から導入されたコンポーネントの書き方で従来よりも、一連の処理をまとまったところに記述することができるようで、setup()
にまとめることができます。例えばチュートリアルにあるcomputed
は以下のようになっていますが、
computed: {
// ★STEP12
computedTodos: function () {
return this.todos.filter(function (el) {
return this.current < 0 ? true : this.current === el.state
}, this)
},
// ★STEP13 作業中・完了のラベルを表示する
labels() {
return this.options.reduce(function (a, b) {
return Object.assign(a, { [b.value]: b.label })
}, {})
// キーから見つけやすいように、次のように加工したデータを作成
// {0: '作業中', 1: '完了', -1: 'すべて'}
}
},
以下のように書き換えられます。(57-62行目)
export default defineComponent({
setup(_, context){
....
const computedTodos = computed(()=> todos.filter((el)=>{
return current.value < 0 ? true : current.value === el.state
}));
const labels = computed(():{[key:number]:string}=> options.reduce((a,b)=>{
return Object.assign(a, {[b.value]:b.label})
},{}));
....
}
}
他にもチュートリアルのコードでは、doAdd()
、doChangeState()
、doRemove()
の3つのmethodが定義されていますが、これらもsetup()
の中に以下のように記述しました。(64-71行目)
export default defineComponent({
setup(_, context){
....
const doAdd = function() { doAddTodo(todos, form) };
const doChangeState = function(item:ATodo) { item.state = item.state ? 0 : 1 };
// 削除の処理
const doRemove = function(item:ATodo) {
let index = todos.indexOf(item)
todos.splice(index, 1)
};
....
}
}
(doAddは、前述のlibs/AddToDo.ts
ファイルにdoAddTodo()
関数に処理を抜き出しています。)
- Element-plus
実務の方でUIのフレームワークも使用する予定だったので、Vue3に対応しているElement-plusを導入してみました。
以下の5種類のコンポーネントを利用しました。
el-radio
el-form
el-button
el-input
el-table
例えば、el-radio
は以下のようなチュートリアルのコードから
<label v-for="label in options">
<input type="radio"
v-model="current"
v-bind:value="label.value">{{ label.label }}
</label>
以下のように書き換えました。
<label v-for="label in options">
<el-radio v-model="current" v-bind:label="label.value">{{ label.label }}</el-radio>
</label>
いくつかハマった点があるので、以下にまとめておきます。
reactiveな変数の扱い
Todoリストの内容を格納する変数をリアクティブな変数として宣言したが値の代入によってリアクティブではなくなってしまった。
具体的にはlocal_storageから値をfetchするときに発生した。以下のコードではだめで、
fetch() {
const todos = JSON.parse(localStorage.getItem(STORAGE_KEY) || "[]");
todos.forEach((todo:any, index:any) => {
todo.id = index;
});
todoStorage.uid = todos.length;
return todos;
},
以下のように、reactiveな状態として返すことでうまく動いているようです。
fetch() {
const jtodos = JSON.parse(localStorage.getItem(STORAGE_KEY) || "[]");
jtodos.forEach((todo:any, index:any) => {
todo.id = index;
});
todoStorage.uid = jtodos.length;
const todos:ATodo[] = reactive<ATodo[]>(jtodos);
return todos; //リアクティブな変数が返される
},
el-form
element-plusのel-form
はv-modelに変数を設定するような使い方のようだったので、専用の型(前述のtypes/todos.tsファイルにあるForm型)を作成して使用しています。
これによりlibs/AddToDo.ts
にあるdoAddTodo()
関数の引数にForm
型を渡しています。
export class Form{
constructor(comment:string){
this.comment = comment
}
comment:string;
}
el-table
element-plusのel-table
は:data
にデータをバインドして、el-table-column
のprop
属性でバインドしたデータのキーを指定するような使い方になっています。
少し変わった使い方をする場合は、<template>
を使って、el-table-column
コンポーネントに注入する必要があるようです。(これをslotと呼ぶみたいです。)
例えば以下のように「コメント」列は、prop
属性にキーを指定するだけですが、「状態」列は行番号に対応する値を引数にして関数を呼ぶボタンをつける必要があるので、scope.$index
の値を使って行の番号を使用しています。
<el-table-column label="コメント" prop="comment" />
<el-table-column label="状態">
<template #default="scope">
<el-button type="primary" @click="doChangeState(computedTodos[scope.$index])">
{{labels[computedTodos[scope.$index].state]}}
</el-button>
</template>
</el-table-column>
一応動いているように見えますが、
なにか問題点がありそうなら教えて下さい。