前回の投稿では、構築するための基礎を築きました。今こそ「本格的に」始めるときです。
私はVue.jsについてよく耳にしました。さらに、開発者からマネージャーに転向した友人が Vue について良いことを教えてくれたので、さらに興味が湧きました。私は Vue を見てみることにしました。これは、私が初心者の観点から学ぶ最初の「軽量」JavaScript フレームワークになります。
前回の投稿では、WebJars と Thymeleaf について説明しました。サーバー側とクライアント側のセットアップは次のとおりです。
両方を POM に統合する方法は次のとおりです。
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <!--1--> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> <!--2--> </dependency> <dependency> <groupId>org.webjars</groupId> <artifactId>webjars-locator</artifactId> <!--3--> <version>0.52</version> </dependency> <dependency> <groupId>org.webjars.npm</groupId> <artifactId>vue</artifactId> <!--4--> <version>3.4.34</version> </dependency> </dependencies>
Spring Boot 側では Kotlin Router と Bean DSL を使用しています。
fun vue(todos: List<Todo>) = router { //1 GET("/vue") { ok().render("vue", mapOf("title" to "Vue.js", "todos" to todos)) //2-3 } }
Todo
オブジェクトの静的リストを渡す
API の開発に慣れている方なら、 body()
関数に馴染みがあるでしょう。この関数は、おそらく JSON 形式でペイロードを直接返します。render render()
フローをビュー テクノロジ (この場合は Thymeleaf) に渡します。この関数は、次の 2 つのパラメータを受け入れます。
/templates
で、プレフィックスは.html
です。この場合、Thymeleaf は/templates/vue.html
のビューを想定しています。HTML 側のコードは次のとおりです。
<script th:src="@{/webjars/axios/dist/axios.js}" src="https://cdn.jsdelivr.net/npm/[email protected]/dist/axios.min.js"></script> <!--1--> <script th:src="@{/webjars/vue/dist/vue.global.js}" src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.js"></script> <!--2--> <script th:src="@{/vue.js}" src="../static/vue.js"></script> <!--3--> <script th:inline="javascript"> /*<![CDATA[*/ window.vueData = { <!--4--> title: /*[[${ title }]]*/ 'A Title', todos: /*[[${ todos }]]*/ [{ 'id': 1, 'label': 'Take out the trash', 'completed': false }] }; /*]]>*/ </script>
先週の記事で説明したように、Thymeleaf の利点の 1 つは、静的ファイルのレンダリングとサーバー側のレンダリングの両方が可能であることです。この魔法を機能させるには、クライアント側のパス(つまりsrc
) とサーバー側のパス(つまりth:src
を指定します。
それでは、Vue コードについて詳しく見ていきましょう。
いくつかの機能を実装したいと考えています:
Todo
項目が表示されます。Todo
完了チェックボックスをクリックすると、 completed
属性を設定/解除する必要があります。Todo
がすべて削除されます。Todo
Todo
リストに追加されます。id
: サーバー側で計算されたID。他のすべてのIDの最大値に1を加えたものlabel
: label
のラベルフィールドの値completed
: false
に設定最初のステップはフレームワークをブートストラップすることです。上記でカスタムvue.js
ファイルの参照をすでに設定しています。
document.addEventListener('DOMContentLoaded', () => { //1 // The next JavaScript code snippets will be inside the block }
次のステップは、Vue にページの一部を管理させることです。HTML 側では、Vue が管理するトップレベルの部分を決定する必要があります。任意の<div>
選択し、必要に応じて後で変更できます。
<div id="app"> </div>
JavaScript 側では、前の HTML <div>
の CSS セレクターを渡してappを作成します。
Vue.createApp({}).mount('#app');
この時点では、ページが読み込まれると Vue が起動しますが、目に見えるようなことは何も起こりません。
次のステップは、Vueテンプレートを作成することです。Vue テンプレートは、Vue によって管理される通常の HTML <template>
です。Vue は Javascript で定義できますが、HTML ページで行うことを好みます。
タイトルを表示できるルート テンプレートから始めましょう。
<template id="todos-app"> <!--1--> <h1>{{ title }}</h1> <!--2--> </template>
title
プロパティを使用します。設定が残っています
JavaScript 側では、管理コードを作成する必要があります。
const TodosApp = { props: ['title'], //1 template: document.getElementById('todos-app').innerHTML, }
title
プロパティを宣言します。
最後に、アプリを作成するときにこのオブジェクトを渡す必要があります。
Vue.createApp({ components: { TodosApp }, //1 render() { //2 return Vue.h(TodosApp, { //3 title: window.vueData.title, //4 }) } }).mount('#app');
render()
関数を必要としますh()
はオブジェクトとそのプロパティから仮想ノードを作成します。title
プロパティを初期化します
この時点で、Vue はタイトルを表示します。
この時点で、ユーザーがチェックボックスをクリックしたときのアクションを実装できます。これは、サーバー側の状態で更新する必要があります。
まず、 Todo
を表示するテーブル用に、新しいネストされた Vue テンプレートを追加しました。記事が長くなりすぎないように、詳細な説明は避けます。興味があれば、 ソース コードをご覧ください。
開始ライン テンプレートのコード (それぞれ JavaScript と HTML) は次のとおりです。
const TodoLine = { props: ['todo'], template: document.getElementById('todo-line').innerHTML }
<template id="todo-line"> <tr> <td>{{ todo.id }}</td> <!--1--> <td>{{ todo.label }}</td> <!--2--> <td> <label> <input type="checkbox" :checked="todo.completed" /> </label> </td> </tr> </template>
Todo
IDを表示するTodo
ラベルを表示するcompleted
属性がtrue
の場合はチェックボックスをオンにします
Vue では、 @
構文によるイベント処理が可能です。
<input type="checkbox" :checked="todo.completed" @click="check" />
ユーザーが行をクリックすると、Vue はテンプレートのcheck()
関数を呼び出します。この関数はsetup()
パラメータで定義します。
const TodoLine = { props: ['todo'], template: document.getElementById('todo-line').innerHTML, setup(props) { //1 const check = function (event) { //2 const { todo } = props axios.patch( //3 `/api/todo/${todo.id}`, //4 { checked: event.target.checked } //5 ) } return { check } //6 } }
props
配列を受け入れて、後でアクセスできるようにしますevent
を渡します前のセクションでは、2 つの間違いを犯しました。
次の機能である、完了したタスクのクリーンアップを実装することでこれを実現します。
これで、Vue 経由でイベントを処理する方法がわかりました。
<button class="btn btn-warning" @click="cleanup">Cleanup</button>
TodosApp
オブジェクトに、同じ名前の関数を追加します。
const TodosApp = { props: ['title', 'todos'], components: { TodoLine }, template: document.getElementById('todos-app').innerHTML, setup() { const cleanup = function() { //1 axios.delete('/api/todo:cleanup').then(response => { //1 state.value.todos = response.data //2-3 }) } return { cleanup } //1 } }
state
モデルを保存する場所です
Vue のセマンティクスでは、Vue モデルはリアクティブにしたいデータのラッパーです。リアクティブとは、ビューとモデル間の双方向バインディングを意味します。既存の値をref()
メソッドに渡すことで、リアクティブにすることができます。
Composition API では、リアクティブ状態を宣言するための推奨方法は、
ref()
関数を使用することです。
ref()
引数を受け取り、それを .value プロパティを持つ ref オブジェクト内にラップして返します。
コンポーネントのテンプレート内の参照にアクセスするには、コンポーネントの
setup()
関数から参照を宣言して返します。
やりましょう:
const state = ref({ title: window.vueData.title, //1-2 todos: window.vueData.todos, //1 }) createApp({ components: { TodosApp }, setup() { return { ...state.value } //3-4 }, render() { return h(TodosApp, { todos: state.value.todos, //5 title: state.value.title, //5 }) } }).mount('#app');
title
の設定方法を変更します。双方向バインディングがないので、これは必要ではありません。クライアント側でタイトルを更新しませんが、すべての値にわたって処理の一貫性を保つことを好みます。state
からオブジェクトの属性を設定する
この時点で、リアクティブなクライアント側モデルが完成しました。
HTML 側では、関連する Vue 属性を使用します。
<tbody> <tr is="vue:todo-line" v-for="todo in todos" :key="todo.id" :todo="todo"></tr> <!--1-2--> </tbody>
Todo
オブジェクトのリストをループするis
属性は、ブラウザがHTMLを解析する方法に対処するために重要です。詳細については、 Vueのドキュメントを参照してください。対応するテンプレートについては上記で説明しました。
これで、クライアントから新しいTodo
追加するという新しい機能を実装できるようになりました。 [追加]ボタンをクリックすると、ラベルフィールドの値が読み取られ、データが API に送信され、応答でモデルが更新されます。
更新されたコードは次のとおりです。
const TodosApp = { props: ['title', 'todos'], components: { TodoLine }, template: document.getElementById('todos-app').innerHTML, setup() { const label = ref('') //1 const create = function() { //2 axios.post('/api/todo', { label: label.value }).then(response => { state.value.todos.push(response.data) //3 }).then(() => { label.value = '' //4 }) } const cleanup = function() { axios.delete('/api/todo:cleanup').then(response => { state.value.todos = response.data //5 }) } return { label, create, cleanup } } }
create()
関数そのものTodo
リストに追加します。
HTML 側では、ボタンを追加してcreate()
関数にバインドします。同様に、 Labelフィールドを追加してモデルにバインドします。
<form> <div class="form-group row"> <label for="new-todo-label" class="col-auto col-form-label">New task</label> <div class="col-10"> <input type="text" id="new-todo-label" placeholder="Label" class="form-control" v-model="label" /> </div> <div class="col-auto"> <button type="button" class="btn btn-success" @click="create">Add</button> </div> </div> </form>
Vue は、 create()
関数を HTML ボタンにバインドします。非同期的に呼び出し、呼び出しによって返された新しい項目でリアクティブTodo
リストを更新します。Cleanup ボタンでも同じことを実行して、チェックされたTodo
オブジェクトを削除します。
コードが必要以上に複雑にならないように、意図的にエラー処理コードを実装しなかったことに注意してください。最初の経験としては十分な洞察が得られたので、ここで終わりにします。
この投稿では、Vue を使用して SSR アプリを拡張する最初のステップを踏みました。これは非常に簡単でした。私が遭遇した最大の問題は、Vue が行テンプレートを置き換えることでした。ドキュメントを詳しく読んでいなかったため、 is
属性を見逃していました。
しかし、HTTP 呼び出しを支援するために Axios を使用し、エラーを管理しなかったにもかかわらず、かなりの数の JavaScript 行を記述する必要がありました。
次の投稿では、同じ機能を Alpine.js で実装します。
この投稿の完全なソースコードは GitHub で見つかります:
さらに詳しく: