コーディングの勉強

TypeScriptでVue.js(Vue2のclass-style,Vue-CLI)

こんにちは、mito(@mito_works)です。

mito
mito
Vueを学びコードを書く面白さに目覚め始めました!

いつもは2023年3月現在、推奨されているVue3でVite環境で開発していますが、バージョンを下げ、Vue2のclass-styleを使い、Vue-CLI環境で開発をしたのでVue3との違いも含めてまとめます。

momo
momo
特にtypescriptのclass-styleの書き方が公式ドキュメントも1ページしかなく試行錯誤したのでもしお困りの方いらっしゃたら参考になればうれしいです!

 

開発環境

以下、使用したパッケージのバージョン情報です。

Vue-CLI 5.0.8

Vue.js 2.6.14

typescript 4.9.5

Node.js 16.13.2

npm 8.1.2

Vue-CLI公式ページ(https://cli.vuejs.org/)

mito
mito
Vue-CLIは古いバージョンを使用すると、ほかのパッケージとの互換性でエラーがでるので最新バージョンがよさそうです!
momo
momo
エラーを読み解き解消を試みたけど、結局は最新にバージョン上げたら解決したね!
mito
mito
そう!@types/vueを入れるとよさそうなどいろいろ試しましたが、パッケージの開発が終了していたり…。色々勉強になりました!

Vue-CLIでプロジェクトを作る

ターミナルで以下のコマンドを実行。

vue create sample-app

 

momo
momo
sample-appの部分はプロジェクト名になるよ(好きな名前でOK)

Presetを選んでいく

Manually select featuresを選び独自に設定していきます。

momo
momo
色々選択項目が出てくるけど大事なことは、typescriptでclass-styleの書き方がしたいのでUse class-style component syntax? Yesとすること。

最後まで質問に答えるとインストールが始まります。

ローカルサーバーを立ち上げる

完了すると次の指示が表示されます。

 

ターミナルの指示通り進みます。

cd sample-app
npm run serve
mito
mito
コンパイルが成功して問題もなし!という情報が出たら成功です!

無事、サイトが立ち上がりました!

Vue3で書いた場合

プロジェクト作成に時間がかかるのでその間にVue3で書いたコードを紹介します。

仕様はこちら↓

mito
mito
要約すると、ユーザーが入力したテキストがローカルストレージに登録される。登録したものを削除できる。といった簡単な投稿アプリです。
<script setup lang="ts">
import { isTemplateNode } from "@vue/compiler-core";
import { ref, computed } from "vue";
const newsList = ref([
  {
    id: 0,
    description: "新しい靴を買ったこと",
  },
  {
    id: 1,
    description: "好きなアニメの続編が公開された",
  },
]);
const disabled = ref<boolean>(true);
const limitInputCheck = computed(() => {
  if (inputDescriotion.value.length >= 30 || inputDescriotion.value.length <= 0) {
    disabled.value = true;
  } else {
    disabled.value = false;
  }
  return inputDescriotion.value.length >= 20 ? "20文字以内でお願いします😮" : "";
});
const inputDescriotion = ref<string>("");
const input = () => {
  const news = { id: Date.now(), description: inputDescriotion.value };
  newsList.value.push(news);
  inputDescriotion.value = "";
};
const deleteItem = (id: number) => {
  newsList.value = newsList.value.filter((value) => {
    return value.id != id;
  });
};
</script>

<template>
  <div>
    <h1>🎉Post Good News of the week🎉</h1>
    <div class="l-container">
      <div class="l-container__form">
        <label for="post">今週のうれしかったこと、楽しかったことを共有してください</label>
        <input type="text" id="post" v-model="inputDescriotion" />
        <p v-if="limitInputCheck.length > 0" class="p-alert">{{ limitInputCheck }}</p>
        <button type="button" class="p-btn-post" @click="input" :disabled="disabled">Post</button>
      </div>
      <div class="l-container__list">
        <p v-if="newsList.length <= 0">投稿お待ちしています~😉</p>
        <ul class="p-list">
          <li v-for="item in newsList" :ley="item.id">
            <p>{{ item.description }}</p>

            <button type="button" class="p-btn-delete" @click="deleteItem(item.id)">Delete</button>
          </li>
        </ul>
      </div>
    </div>
  </div>
</template>

<style scoped>
input[type="text"] {
  padding: 0.5rem;
  font-size: 1.5rem;
  width: 500px;
}
.l-container__form {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1rem;
}
.p-btn-post {
  background-color: #f70a8d;
  color: #fff;
}
.p-btn-delete {
  padding: 0 2rem;
  background-color: rgb(128, 194, 233);
  color: #fff;
}
.p-list {
  list-style: none;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
}
.p-list li {
  padding: 1rem 0.5rem;
  background-color: rgb(203, 240, 252);
  font-weight: bold;
  width: 500px;
  display: flex;
  justify-content: space-between;
}
.p-alert {
  color: #f70a8d;
  font-weight: bold;
}
</style>

vue-vite-goodnews/goodnews-step1/src/App.vue

Vue2のclass-styleで書いた場合

親子間のデータの受け渡しも実装してみたかったので、コンポーネントを分けました。

簡単なディレクトリは

APP.vue

components

|_Form.vue

|_List.vue

となっています。

vueファイルの中身

<template>
  <div id="app">
    <h1>🎉Post Good News of the week🎉</h1>
    <FormItem @input-item="inputParentsItem($event)" />
    <List :postList="postList" @delete-item="deletParentItem($event)" />
  </div>
</template>

<script lang="ts">
import { Component, Vue } from "vue-property-decorator";
import FormItem from "./components/Form.vue";
import List from "./components/List.vue";
import { Post } from "./newsItem";

@Component({
  components: {
    FormItem,
    List,
  },
})
export default class App extends Vue {
  postList = [
    {
      id: 0,
      description: "新し靴を買ったこと",
    },
    {
      id: 1,
      description: "早起きできた",
    },
  ];

  inputParentsItem($event: string): void {
    const post: Post = { id: Date.now(), description: $event };
    this.postList.push(post);
  }
  deletParentItem($event: number): void {
    this.postList = this.postList.filter((val) => {
      return val.id != $event;
    });
  }
}
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;

  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
button {
  border: none;
  border-radius: 8px;
  padding: 0.7rem 1.2rem;
  font-size: 1rem;
  font-weight: 500;
}
</style>

vue-cli-goodnews-/blob/main/src/App.vue

<template>
  <div class="l-container__form">
    <label for="post">今週のうれしかったことを共有してください</label>
    <input type="text" id="post" v-model="inputDescription" />
    <p v-if="limitInputCheck" class="p-alert">{{ limitInputCheck }}</p>
    <button type="button" class="p-btn-post" @click="inputItem" :disabled="disabled">Post</button>
  </div>
</template>

<script lang="ts">
import { Vue, Component, Emit } from "vue-property-decorator";
@Component
export default class Form extends Vue {
  inputDescription = "";
  disabled = true;
  //emit
  @Emit("input-item")
  inputItem(): string {
    return this.inputDescription;
  }
  //computed
  get limitInputCheck(): string | null {
    if (this.inputDescription.length >= 20 || this.inputDescription.length <= 0) {
      this.disabled = true;
    } else {
      this.disabled = false;
    }
    return this.inputDescription.length >= 20 ? "20文字以内でお願いします😮" : "";
  }
}
</script>

<style scoped>
input[type="text"] {
  padding: 0.5rem;
  font-size: 1.5rem;
  width: 500px;
}
button:disabled {
  border: none;
  filter: opacity(0.4);
  cursor: not-allowed;
}
.l-container__form {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1rem;
}
.p-btn-post {
  background-color: #f70a8d;
  color: #fff;
}
.p-alert {
  color: #f70a8d;
  font-weight: bold;
}
</style>

vue-cli-goodnews-/blob/main/src/components/Form.vue

<template>
  <div class="l-container__list">
    <!-- <ul>
      <li v-for="post in postList" :key="post.id">{{ post.id }}</li>
    </ul> -->
    <p v-if="postList.length <= 0">投稿お待ちしています~😉</p>
    <ul class="p-list">
      <li v-for="post in postList" :key="post.id">
        <p>{{ post.description }}</p>
        <button type="button" class="p-btn-delete" @click="deleteItem(post.id, post.description)">Delete</button>
      </li>
    </ul>
  </div>
</template>

<script lang="ts">
import { Vue, Component, Emit, Prop } from "vue-property-decorator";
import { Post } from "../newsItem";
@Component
export default class List extends Vue {
  @Prop() postList!: Post[];
  @Emit("delete-item")
  deleteItem(id: number, description: string): number | null {
    if (confirm(`${description}を削除してよいですか?`)) {
      return id;
    }
    return null;
  }
}
</script>

<style scoped>
.p-btn-delete {
  padding: 0 2rem;
  background-color: rgb(128, 194, 233);
  color: #fff;
}
.p-list {
  list-style: none;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
}
.p-list li {
  padding: 1rem 0.5rem;
  background-color: rgb(203, 240, 252);
  font-weight: bold;
  width: 500px;
  display: flex;
  justify-content: space-between;
}
</style>

vue-cli-goodnews-/blob/main/src/components/List.vue

型定義ファイルの中身

export type Item = { [key: string]: string | number };
export type Post = {
  id: number;
  description: string;
};

vue-cli-goodnews-/blob/main/src/newsItem.ts

Vue2のclass-styleで書いたコードのポイント

mito
mito
最初はvue3の書き方と全然違う!と戸惑いましたが、よく読んでいくと、理解できるようになりました。

ポイントは、以下2つだと思います。

・class構文が使えるような書き方をしていること

・TypeScript特有のデコレーターの書き方でclassを拡張していること

もう少し詳しく書きます。

import { Component, Vue } from "vue-property-decorator";

とvue-property-decoratorからComponentとVueをインポートしています。vue-property-decoratorはTypeScript固有のclass構文を書くためのパッケージです。

 

momo
momo
「Use class-style component syntax」をyesにすると使えるよ!
mito
mito
property-decoratorによってさまざまなデコレーターが使えるようになっています

個人的に違うと感じたポイント

・classはvueを拡張したclassとして書く。(export default class Form extends Vue{})

・@Component,@Emit,@Propなどデコレーターの書き方をしている

・computedはget 関数名で表現する

Netlifyでサイトを公開する(おまけ)

今回は静的サイトを簡単に公開できるnetlifyを使って公開してみました。

下準備(本番環境用にビルドする)

npm run build

を行い、作ったファイルを本番環境用にビルドします。

momo
momo
本番公開用にdistフォルダが出来上がるよ!

netlifyにsign upする

mito
mito
githubアカウントを使いました。

distフォルダ一式をアップロードする

Sitesタブに移動し、下準備で作成したdistファイルをドラッグ&ドロップでアップロードします。

mito
mito
なんとそれだけでURLが発行されサイトが公開されます!
momo
momo
URLを変更したい場合はSite settingsから変更できるよ!

公開したサイトはこちら↓

Special Thanks

以下サイトとUdemy教材をとても参考にさせていただきました。

 

 

ABOUT ME
mito
こんにちは!mitoです。 フロントエンド開発、デザイン全般をやっているクリエイターです。 学ぶことと教えることが好きです。 子育てをする中で、自分の生活に働き方を合わせたいと思うようになり、2020年4月からフリーランスになりました。 好奇心旺盛でやりたいこと多め、ワクワクすると止まれない性格です。 同じように一緒にチャレンジする人の背中そっと押せたり、励ましあえるようなブログになるといいなと思っています。