こんにちは、mito(@mito_works)です。
いつもは2023年3月現在、推奨されているVue3でVite環境で開発していますが、バージョンを下げ、Vue2のclass-styleを使い、Vue-CLI環境で開発をしたのでVue3との違いも含めてまとめます。
Contents
開発環境
以下、使用したパッケージのバージョン情報です。
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/)
@types/vueを入れるとよさそうなどいろいろ試しましたが、パッケージの開発が終了していたり…。色々勉強になりました!Vue-CLIでプロジェクトを作る
ターミナルで以下のコマンドを実行。
vue create sample-app
Presetを選んでいく
Manually select featuresを選び独自に設定していきます。


最後まで質問に答えるとインストールが始まります。
ローカルサーバーを立ち上げる
完了すると次の指示が表示されます。

ターミナルの指示通り進みます。
cd sample-app
npm run serve
無事、サイトが立ち上がりました!

Vue3で書いた場合
プロジェクト作成に時間がかかるのでその間にVue3で書いたコードを紹介します。
仕様はこちら↓
<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で書いたコードのポイント
ポイントは、以下2つだと思います。
・class構文が使えるような書き方をしていること
・TypeScript特有のデコレーターの書き方でclassを拡張していること
もう少し詳しく書きます。
import { Component, Vue } from "vue-property-decorator";
とvue-property-decoratorからComponentとVueをインポートしています。vue-property-decoratorはTypeScript固有のclass構文を書くためのパッケージです。
個人的に違うと感じたポイント
・classはvueを拡張したclassとして書く。(export default class Form extends Vue{})
・@Component,@Emit,@Propなどデコレーターの書き方をしている
・computedはget 関数名で表現する
Netlifyでサイトを公開する(おまけ)
今回は静的サイトを簡単に公開できるnetlifyを使って公開してみました。
下準備(本番環境用にビルドする)
npm run build
を行い、作ったファイルを本番環境用にビルドします。

netlifyにsign upする
 
distフォルダ一式をアップロードする
Sitesタブに移動し、下準備で作成したdistファイルをドラッグ&ドロップでアップロードします。
 
公開したサイトはこちら↓

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














