Closed11

Vue 3 を vite で立ち上げる

yukiyuki

Web+DB Press の雑誌の記事を写経しています。

example-vue-todo-list という名前で vite アプリ(って言えばいいのかな?)を立ち上げる。

~/github/others on ☁️  (ap-northeast-1) took 2s
❯ npm init vite-app example-vue-todo-list
Need to install the following packages:
  create-vite-app
Ok to proceed? (y) y
npm WARN deprecated create-vite-app@1.21.0: create-vite-app has been deprecated. run `npm init @vitejs/app` or `yarn create @vitejs/app` instead.
Scaffolding project in ~/github/others/example-vue-todo-list...

Done. Now run:

  cd example-vue-todo-list
  npm install (or `yarn`)
  npm run dev (or `yarn dev`)
yukiyuki

警告メッセージを見て気づいたので、こっちをやり直す。

npm init @vitejs/app

いろいろ埋めていくとプロジェクトが一旦立ち上がる。

~/github/others on ☁️  (ap-northeast-1)
❯ npm init @vitejs/app
Need to install the following packages:
  @vitejs/create-app
Ok to proceed? (y) y
✔ Project name: · example-vue-todo-app
✔ Select a framework: · vue
✔ Select a variant: · TypeScript

Scaffolding project in ~/github/others/example-vue-todo-app...

Done. Now run:

  cd example-vue-todo-app
  npm install
  npm run dev
yukiyuki

残りは、指示通り npm installnpm run dev するとブラウザで立ち上がる。

yukiyuki

生成されるコードを見てみる。Composition API を利用したコンポーネントが生成されている模様。無事に思ったとおり、Vue 3 で TypeScript に対応したテンプレートが作成された。

<template>
  <h1>{{ msg }}</h1>

  <p>
    Recommended IDE setup:
    <a href="https://code.visualstudio.com/" target="_blank">VSCode</a>
    +
    <a
      href="https://marketplace.visualstudio.com/items?itemName=octref.vetur"
      target="_blank"
    >
      Vetur
    </a>
    or
    <a href="https://github.com/johnsoncodehk/volar" target="_blank">Volar</a>
    (if using
    <code>&lt;script setup&gt;</code>)
  </p>

  <p>See <code>README.md</code> for more information.</p>

  <p>
    <a href="https://vitejs.dev/guide/features.html" target="_blank">
      Vite Docs
    </a>
    |
    <a href="https://v3.vuejs.org/" target="_blank">Vue 3 Docs</a>
  </p>

  <button @click="count++">count is: {{ count }}</button>
  <p>
    Edit
    <code>components/HelloWorld.vue</code> to test hot module replacement.
  </p>
</template>

<script lang="ts">
import { ref, defineComponent } from 'vue'
export default defineComponent({
  name: 'HelloWorld',
  props: {
    msg: {
      type: String,
      required: true
    }
  },
  setup: () => {
    const count = ref(0)
    return { count }
  }
})
</script>

<style scoped>
a {
  color: #42b983;
}

label {
  margin: 0 0.5em;
  font-weight: bold;
}

code {
  background-color: #eee;
  padding: 2px 4px;
  border-radius: 4px;
  color: #304455;
}
</style>
yukiyuki

data に定義した todo を単純に取り出すだけの実装を書く。

<template>
  <ul>
    <li v-for="todo in todoItems" v-bind:key="todo.id">{{ todo.text }}</li>
  </ul>
</template>

<script lang="ts">
export default {
  data() {
    return {
      todoItems: [
        { id: 1, text: 'Go out to the sea' },
        { id: 2, text: 'Invite a first member' }
      ]
    }
  }
}
</script>
yukiyuki

Vue 2 と違うポイントだと思うけど、ルートに複数コンポーネントを設置できるようになっている?div とかでわざわざ加工必要がなくなったと思う。便利。

<template>
  <input v-model="inputValue">
  <button v-on:click="handleClick">
    Todo を追加する
  </button>
  <ul>
    <li v-for="todo in todoItems" v-bind:key="todo.id">{{ todo.text }}</li>
  </ul>
</template>

<script lang="ts">
export default {
  data() {
    return {
      inputValue: '',
      todoItems: [
        { id: 1, text: 'Go out to the sea' },
        { id: 2, text: 'Invite a first member' }
      ]
    }
  },
  methods: {
    handleClick() {
      this.todoItems.push({
        id: this.todoItems.length + 1,
        text: this.inputValue,
      })
      this.inputValue = ''
    }
  }
}
</script>
yukiyuki

このあたりも Vue 2 とあまり使い心地は変わらなそう。リストをクリックすると、チェックマークがついて完了したという意味になる。

<template>
  <input v-model="inputValue">
  <button v-on:click="handleClick">
    Todo を追加する
  </button>
  <ul>
    <li v-for="todo in todoItems" v-bind:key="todo.id" v-on:click="todo.done = !todo.done">
      {{ todo.text }}
      <span v-if="todo.done">✔</span>
    </li>
  </ul>
</template>

<script lang="ts">
export default {
  data() {
    return {
      inputValue: '',
      todoItems: [
        { id: 1, text: 'Go out to the sea', done: false },
        { id: 2, text: 'Invite a first member', done: false }
      ]
    }
  },
  methods: {
    handleClick() {
      this.todoItems.push({
        id: this.todoItems.length + 1,
        text: this.inputValue,
        done: false
      })
      this.inputValue = ''
    }
  }
}
</script>

カーソルをポインタに変えておいて、いかにもクリックできるようにしてみるのはありかも。CSS の書き方も以前と変わらず。

<style scoped>
li {
  cursor: pointer;
}
</style>
yukiyuki

本は ECMA だったと思うけど、今回は TypeScript で実装しているので若干工夫が必要そう。Todo という type alias を追加しておいて、computed の関数の戻り値に型情報を付ける必要があった。

<template>
  <input v-model="inputValue">
  <button v-on:click="handleClick">
    Todo を追加する
  </button>

  <input v-model="filterValue" placeholder="フィルタテキスト">
  <ul>
    <li v-for="todo in filteredTodoItems" v-bind:key="todo.id" v-on:click="todo.done = !todo.done">
      {{ todo.text }}
      <span v-if="todo.done">✔</span>
    </li>
  </ul>
</template>

<script lang="ts">
type Todo = {
  id: number,
  text: string,
  done: boolean
}

export default {
  data() {
    return {
      inputValue: '',
      todoItems: [
        { id: 1, text: 'Go out to the sea', done: false },
        { id: 2, text: 'Invite a first member', done: false }
      ],
      filterValue: ''
    }
  },
  methods: {
    handleClick() {
      this.todoItems.push({
        id: this.todoItems.length + 1,
        text: this.inputValue,
        done: false
      })
      this.inputValue = ''
    }
  },
  computed: {
    filteredTodoItems(): Todo[] {
      if (!this.filterValue) {
        return this.todoItems;
      }
      return this.todoItems.filter((todo) => {
        return todo.text.includes(this.filterValue)
      })
    }
  }
}
</script>

<style scoped>
li {
  cursor: pointer;
}
</style>
yukiyuki

Vuetify も Chakura UI も Vue 3 に対応していないようなので、Tailwind CSS を使うことにする。

いつもどおり。

npm install -D tailwindcss@latest postcss@latest autoprefixer@latest
yukiyuki

Tailwind CSS をちょっと適用して多少見やすくしてみた。

<template>
  <div>
    <input v-model="inputValue" class="mx-4 p-2 rounded border-2 border-gray-300 focus:outline-none">
    <button v-on:click="handleClick" class="py-2 px-4 rounded bg-green-500 text-white focus:ring focus:outline-none focus:ring-green-500 focus:ring-opacity-50">
      Todo を追加する
    </button>
  </div>

  <div class="my-4 drop-shadow">
    <input v-model="filterValue" placeholder="フィルタテキスト" class="mx-4 p-2 rounded border-2 border-gray-300 focus:outline-none">
  </div>
  <ul class="mx-8 my-8 list-inside list-disc">
    <li v-for="todo in filteredTodoItems" v-bind:key="todo.id" v-on:click="todo.done = !todo.done">
      {{ todo.text }}
      <span v-if="todo.done">✔</span>
    </li>
  </ul>
</template>

<script lang="ts">
type Todo = {
  id: number,
  text: string,
  done: boolean
}

export default {
  data() {
    return {
      inputValue: '',
      todoItems: [
        { id: 1, text: 'Go out to the sea', done: false },
        { id: 2, text: 'Invite a first member', done: false }
      ],
      filterValue: ''
    }
  },
  methods: {
    handleClick() {
      this.todoItems.push({
        id: this.todoItems.length + 1,
        text: this.inputValue,
        done: false
      })
      this.inputValue = ''
    }
  },
  computed: {
    filteredTodoItems(): Todo[] {
      if (!this.filterValue) {
        return this.todoItems;
      }
      return this.todoItems.filter((todo) => {
        return todo.text.includes(this.filterValue)
      })
    }
  }
}
</script>

<style scoped>
li {
  cursor: pointer;
}
</style>
このスクラップは2021/05/09にクローズされました