wordpress_blog

This is a dynamic to static website.

Vue3 複習05

元件

元件介紹

為什麼要拆解成元件

  • 增加程式碼的可複用性
  • 避免單一檔案過大
  • 易於管理及協作
  • 元件功能獨立化

頁面元件結構

元件資料獨立 與 傳遞

接下來介紹到的 SPA 亦是使用元件

註冊元件的手法

// HTML
<div id="app">
  <h3>元件基本範例及結構</h3>
  <p>元件使用的基本要點</p>
  <ul>
    <li>元件需要在 createApp 後,mount 前進行定義</li>
    <li>元件需指定一個名稱</li>
    <li>元件結構與最外層的根元件結構無異(除了增加 Template 的片段)</li>
    <li>元件另有 prop, emits 等資料傳遞及事件傳遞</li>
  </ul>

  <h3>不同元件的註冊方式</h3>

  <h4>全域註冊</h4>
  <p>此 createApp 下,任何子元件都可運用,在中小型專案、一般頁面開發很方便</p>
  <alert1></alert1>
  <alert2></alert2>

  <h4>區域註冊</h4>
  <p>限制在特定元件下才可使用,在 Vue Cli 中很常使用此方法(便於管理)</p>
  <alert3></alert3>
  <alert4></alert4>

  <h4>模組化</h4>
  <p>同屬於區域註冊,Vue Cli 中的實戰運用技巧</p>
  <alert5></alert5>
</div>
// JS
<script type="module">
  // 注意,這段起手式與先前不同
  import alert5 from "./component-alert.js";

  // 區域註冊
  const alert3 = {
    data() {
      return {
        text: "內部文字 - 元件 3",
      };
    },
    template: `<div class="alert alert-primary" role="alert">
                {{ text }}
              </div>`,
  };

  const app = Vue.createApp({
    data() {
      return {
        text: "外部元件文字",
      };
    },
    // 區域註冊 - 掛載在根元件
    components: {
      alert3,
      alert5,
    },
    // 全域註冊 - 寫法 1
  }).component("alert1", {
    data() {
      return {
        text: "內部文字 - 元件 1",
      };
    },
    template: `<div class="alert alert-primary" role="alert">
                {{ text }}
              </div>`,
  });

  // 全域註冊 - 寫法 2
  app.component("alert2", {
    data() {
      return {
        text: "內部文字 - 元件 2",
      };
    },
    template: `<div class="alert alert-primary" role="alert">
                {{ text }}
              </div>`,
  });

  // 全域註冊
  app.component("alert4", {
    data() {
      return {
        text: "內部文字 - 元件 4",
      };
    },
    // 區域註冊 - 掛載在子元件
    components: {
      alert3,
    },
    template: `<div class="alert alert-primary" role="alert">
                {{ text }}
                <alert3></alert3>
              </div>`,
  });

  app.mount("#app");
</script>
// component-alert.js
// 模組化
export default {
  data() {
    return {
      text: "外部匯入的元件 - 元件 5",
    };
  },
  template: `<div class="alert alert-primary" role="alert">
    {{ text }}
  </div>`,
};

元件樣板製作

// HTML
<div id="app">
  <h3>樣板建立方式</h3>

  <h4>template</h4>
  <alert1></alert1>

  <h4>x-template</h4>
  <alert2></alert2>

  <h4>單文件元件(單一檔案包含 HTML, JS, CSS)</h4>
  <p>
    本章節不介紹,在 Vue Cli 課程中將會實作(較為簡單,使用與 x-template 接近)
  </p>

  <hr />
  <h3>元件運用</h3>
  <h4>直接使用 標籤 名稱</h4>
  <alert1></alert1>

  <h4>搭配 v-for 也是沒問題的</h4>
  <alert1 v-for="i in array" :key="i"></alert1>

  <h4>使用 v-is 綁定</h4>
  <!-- 要加入單引號 -->
  <div v-is="'alert1'"></div>
  <div is="vue:alert1"></div>

  <h4>動態屬性</h4>
  <input type="text" v-model="componentName" />

  <p>任何標籤均可搭配 v-is 進行動態切換</p>
  <!-- This rule reports deprecated v-is directive in Vue.js v3.1.0+. -->
  <div v-is="componentName"></div>

  <p>
    在 <code>component</code> 標籤中,可以使用 is 縮寫(由 v2 版轉移過來的功能)
  </p>
  <component v-bind:is="componentName"></component>
  <component :is="componentName"></component>

  <h2>動態標籤實戰技巧</h2>
  <table>
    <thead>
      <tr>
        <th>標題</th>
        <th>內文</th>
      </tr>
    </thead>
    <tbody>
      <!-- 會出現渲染顯示在不對的地方 -->
      <!-- <table-row></table-row> -->
      <!-- This rule reports deprecated v-is directive in Vue.js v3.1.0+. -->
      <tr v-is="'table-row'"></tr>
      <tr is="vue:table-row"></tr>
    </tbody>
  </table>
</div>
// JS
<!-- x-template -->
<script type="text/x-template" id="alert-template">
  <div class="alert alert-primary" role="alert">
    x-template 所建立的元件
  </div>
</script>

<script type="module">
  // 注意,這段起手式與先前不同
  const app = Vue.createApp({
    data() {
      return {
        array: [1, 2, 3],
        componentName: "alert1",
      };
    },
  });

  app.component("alert1", {
    template: `<div class="alert alert-primary" role="alert">
                範例ㄧ
              </div>`,
  });

  // x-template
  app.component("alert2", {
    template: "#alert-template",
  });

  app.component("table-row", {
    template: `<tr>
                <td>$</td>
                <td>這是一個 tr 項目</td>
              </tr>`,
  });

  app.mount("#app");
</script>

Props 向內層元件傳遞資料狀態

// HTML
<div id="app">
  <h3>Props 靜態資料</h3>
  <p>由外部傳入資料至內部</p>
  外部資源:https://images.unsplash.com/photo-1605784401368-5af1d9d6c4dc?ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=600&q=80
  <photo
    url="https://images.unsplash.com/photo-1605784401368-5af1d9d6c4dc?ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=600&q=80"
  ></photo>

  <h3>動態資源</h3>
  <p>技巧:前內、後外</p>
  <!-- v-bind:url="imgUrl" -->
  <photo :url="imgUrl"></photo>

  <h3>單向數據流</h3>
  <photo2 :url="imgUrl"></photo2>

  <h3>命名限制</h3>
  <!-- props 屬性使用小駝峰命名時 -->
  <!-- :superUrl 要改寫成 :super-url -->
  <photo3 :super-url="imgUrl"></photo3>
</div>
// JS
<script type="module">
  const app = Vue.createApp({
    data() {
      return {
        imgUrl:
          "https://images.unsplash.com/photo-1605784401368-5af1d9d6c4dc?ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=600&q=80",
      };
    },
  });

  app.component("photo", {
    props: ["url"],
    template: `<img :src="url" class="img-thumbnail" alt>`,
  });

  app.component("photo2", {
    props: ["url"],
    template: `<img :src="url" class="img-thumbnail" alt><br>
    <input type="text" v-model="url"> {{ url }}`,
  });

  app.component("photo3", {
    // 小駝峰命名
    props: ["superUrl"],
    template: `<img :src="superUrl" class="img-thumbnail" alt>`,
  });
  app.mount("#app");
</script>

元件型別驗證

// HTML
<div id="app">
  <h3>Props 型別技巧</h3>
  <input type="number" v-model="money" />

  <props-type money="300"></props-type>
  <props-type money="true"></props-type>
  <props-type :money="300"></props-type>
  <props-type :money="true"></props-type>
  <props-type :money="{}"></props-type>
  <props-type :money="money"></props-type>
  <props-type :money="boo"></props-type>

  <h3>定義 Props 型別驗證</h3>
  <p>實戰中不太會用到全部技巧,常用的有:</p>
  <ul>
    <li>型別驗證</li>
    <li>預設值、是否必填</li>
  </ul>
  <props-validation :prop-a="fun" prop-c="required" :prop-f="10000">
  </props-validation>
</div>
// JS
<script type="module">
  const app = Vue.createApp({
    data() {
      return {
        money: 300,
        big: 100n,
        boo: true,
        fun: () => {
          return "a";
        },
      };
    },
  });

  app.component("props-type", {
    props: ["money"],
    template: `<div>value: {{money}}, typeof:{{ typeof money }}</div>`,
  });

  app.component("props-validation", {
    props: {
      // 單一型別檢查,可接受的型別 String, Number, Object, Boolean, Function(在 Vue 中可使用 Function 驗證型別)
      // null, undefined 會直接通過驗證
      propA: Function,
      // propA: String,

      // 多個型別檢查
      propB: [String, Number],

      // 必要值
      propC: {
        type: String,
        required: true,
      },

      // 預設值
      propD: {
        type: Number,
        default: 300,
      },

      // 自訂函式
      propE: {
        type: Object,
        default() {
          return {
            money: 300,
          };
        },
      },

      // 自訂驗證
      propF: {
        validator(value) {
          return value > 1000;
        },
      },
    },
    template: `
      <p>propA: {{ propA }}</p>
      <p>propC: {{ propC }}</p>
      <p>propD: {{ propD }}</p>
      <p>propE: {{ propE }}</p>
      <p>propF: {{ propF }}</p>
      `,
  });

  app.mount("#app");
</script>

Emit 向外層傳遞事件

// HTML
<!-- 外層元件 -->
<div id="app">
  <h3>Emit 觸發外部事件</h3>
  <ul>
    <li>先定義外層接收的方法</li>
    <li>定義內層的 $emit 觸發方法</li>
    <li>使用 v-on 的方式觸發外層方法(口訣:前內、後外)</li>
  </ul>
  <p>{{ num }}</p>
  <!-- 內層元件 -->
  <!-- 3. 使用 v-on 的方式觸發外層方法 (口訣: 前內、後外) -->
  <button-counter v-on:emit-num="addNum"></button-counter>

  <h3>傳遞資料狀態</h3>
  內部傳來的資料:{{ text }}<br />
  <!-- 內層元件 -->
  <button-text @emit-text="getData"></button-text>

  <h3>命名注意</h3>
  <p>駝峰的大寫文字,可以改為 `-` 進行串接</p>
  內部傳來的文字:{{ text }}<br />
  <!-- 內層元件 -->
  <!-- 命名注意 @emitText 要改寫成 @emit-text -->
  <button-named @emit-text="getData"></button-named>
</div>
// JS
<script type="module">
  // 外層元件
  // 根元件
  const app = Vue.createApp({
    data() {
      return {
        num: 0,
        text: "",
      };
    },
    methods: {
      // 1. 先定義外層接收的方法
      addNum() {
        console.log("addNum", "先定義外層元件接收方法");
        this.num++;
      },
      // 1. 先定義外層接收的方法
      // 1-1. 透過參數的方式來接收內層傳來的資料
      getData(text) {
        console.log("getData", "先定義外層元件接收方法");
        this.text = text;
      },
    },
  });

  // 內層元件
  app.component("button-counter", {
    methods: {
      // 2. 定義內層的 $emit 觸發方法
      click() {
        console.log("click", "定義內層的 $emit 觸發方法");
        // 2-1. 自定義名稱
        this.$emit("emit-num");
      },
    },
    // 2-2. 事件觸發 v-on click
    template: `<button type="button" @click="click">add</button>`,
  });

  // 內層元件
  app.component("button-text", {
    data() {
      return {
        text: "內部資料",
      };
    },
    methods: {
      // 2. 定義內層的 $emit 觸發方法
      emit() {
        console.log("emit", "定義內層的 $emit 觸發方法");
        // 2-1. 自定義名稱, 要傳遞的參數
        this.$emit("emit-text", this.text);
      },
    },
    // 2-2. 事件觸發 v-on emit
    template: `<button type="button" @click="emit">emit data</button>`,
  });

  // 內層元件
  app.component("button-named", {
    methods: {
      // 2. 定義內層的 $emit 觸發方法
      emit() {
        console.log("emit", "定義內層的 $emit 觸發方法");
        // 2-1. 自定義名稱, 要傳遞的文字
        // 小駝峰命名
        this.$emit("emitText", "內部文字");
      },
    },
    // 2-2. 事件觸發 v-on emit
    template: `<button type="button" @click="emit">emit data</button>`,
  });

  app.mount("#app");
</script>

Emits 驗證

// HTML
<div id="app">
  <h3>Emits API</h3>
  {{ num }}
  <button-counter @add="addNum"></button-counter>

  <h3>驗證資料內容</h3>
  <button-counter2 @add="addNum"></button-counter2>
</div>
// JS
<script type="module">
  const app = Vue.createApp({
    data() {
      return {
        num: 0,
      };
    },
    methods: {
      addNum(num) {
        this.num = this.num + num;
      },
    },
  });

  app.component("button-counter", {
    data() {
      return {
        num: 1,
      };
    },
    // 主要會出現在該值是由 data 定義,但難以追蹤他的變化時會出現

    // 加上 emits 警告除錯
    emits: ["add"],
    template: `
      <button type="button" @click="num++">調整 num 的值</button>
      <button type="button" @click="$emit('add', num)">add</button>`,
  });

  app.component("button-counter2", {
    emits: {
      add: (num) => {
        if (typeof num !== "number") {
          console.warn("add 事件參數型別需為 number");
        }
        return typeof num === "number";
      },
    },
    template: `
      <button type="button" @click="$emit('add', '1')">Emit 驗證是否為數值</button>
    `,
  });

  app.mount("#app");
</script>

Slot 插槽

// HTML
<div id="app">
  <h3>Slot 插巢與插巢預設值</h3>
  <card>
    <p>這是由外層定義的</p>
  </card>
  <br />
  <card></card>

  <h3>具名插巢</h3>
  <card2>
    <template v-slot:header>我喜歡這張卡片</template>
    <!--  預設請加入 default  -->
    <template v-slot:default>這是卡片 2 號</template>
    <template v-slot:footer>這是卡片腳</template>
  </card2>

  <h3>具名插巢縮寫</h3>
  <card2>
    <template #header>我喜歡這張卡片</template>
    <!--  預設請加入 default  -->
    <template #default>這是卡片 2 號</template>
    <template #footer>這是卡片腳</template>
  </card2>
</div>
// JS
<script type="module">
  const app = Vue.createApp({});
  app.component("card", {
    template: `<div class="card" style="width: 18rem;">
      <div class="card-header">
        元件 Header
      </div>
      <div class="card-body">
        <slot>
          <p>這是預設值</p>
        </slot>
      </div>
      <div class="card-footer">
        元件 Footer
      </div>
    </div>`,
  });

  app.component("card2", {
    template: `<div class="card" style="width: 18rem;">
      <div class="card-header">
        <slot name="header">元件 Header</slot>
      </div>
      <div class="card-body">
        <slot>這段是預設的文字</slot>
      </div>
      <div class="card-footer">
        <slot name="footer">元件 Footer</slot>
      </div>
    </div>`,
  });

  app.mount("#app");
</script>

Slot Props 插槽傳遞資料狀態

// HTML
<div id="app">
  <h3>插巢 Prop</h3>
  <p>將元件內的變數取出使用,稱為 slot prop</p>
  <card>
    <template v-slot:default="slotProps">
      我想取出元件的值來使用 {{ slotProps.product.name }}
    </template>
  </card>
  <hr />
  <h2>多個(解構)</h2>
  {{ product }}
  <card2 :product="product">
    <template #header> 買早餐 </template>
    <template #default="{ product, veganName }">
      {{ product }}
      <br />
      {{ veganName }}
    </template>
  </card2>
  <br /><br />
  <card3 :product="product">
    <template #header> 買早餐 </template>
    <template #default="{ product, veganName='是素非素' }">
      {{ product }}
      <br />
      {{ veganName }}
    </template>
  </card3>
</div>
// JS
<script type="module">
  const app = Vue.createApp({
    data() {
      return {
        product: {
          name: "蛋餅",
          price: 30,
          vegan: false,
        },
      };
    },
  });

  app.component("card", {
    data() {
      return {
        product: {
          name: "蛋餅",
          price: 30,
          vegan: false,
        },
      };
    },
    template: `
    <div class="card" style="width: 18rem;">
      <div class="card-body" >
        <slot :product="product"></slot>
      </div>
    </div>
    `,
  });

  app.component("card2", {
    props: ["product"],
    data() {
      return {
        veganName: "",
      };
    },
    created() {
      console.log();
      this.veganName = this.product.vegan ? "素食" : "非素食";
    },
    template: `
    <div class="card" style="width: 18rem;">
      <div class="card-body" >
        <slot :product="product" :veganName="veganName"></slot>
      </div>
    </div>
    `,
  });

  app.component("card3", {
    props: ["product"],
    data() {
      return {
        veganName: "",
      };
    },
    created() {
      console.log();
      this.veganName = this.product.vegan ? "素食" : "非素食";
    },
    template: `
    <div class="card" style="width: 18rem;">
      <div class="card-body" >
        <slot :product="product"></slot>
      </div>
    </div>
    `,
  });

  app.mount("#app");
</script>

Mitt 跨元件資料傳遞

// HTML
<div id="app">
  <p>將元件內的變數取出使用,稱為 slot prop</p>
  <p>
    套件路徑:<a href="https://github.com/developit/mitt"
      >https://github.com/developit/mitt</a
    >
  </p>
  <card-on></card-on>

  <card-emit></card-emit>
</div>
// JS
<script type="module">
  import "https://unpkg.com/mitt/dist/mitt.umd.js"; // mitt
  const emitter = mitt();

  const app = Vue.createApp({});

  app.component("card-emit", {
    data() {
      return {
        product: {
          name: "蛋餅",
          price: 30,
          vegan: false,
        },
      };
    },
    methods: {
      sendData() {
        console.log("sendData");
        // 與 this.$emit 類似
        // 自定義的事件名稱, 參數
        emitter.emit("sendProduct", this.product);
      },
    },
    template: `
    <div class="card" style="width: 18rem;">
      <div class="card-body" >
        <button @click="sendData">送出</button>
      </div>
    </div>
    `,
  });

  app.component("card-on", {
    data() {
      return {
        item: {},
      };
    },
    created() {
      emitter.on("sendProduct", (data) => {
        console.log("card-on", data);
        this.item = data;
      });
    },
    template: `
    <div class="card" style="width: 18rem;">
      <div class="card-body">
        {{ item }}
      </div>
    </div>
    `,
  });

  app.mount("#app");
</script>

元件章節作業

API 站點說明API 連結位址備用位址作業範例

作業條件:

  • 每張卡片都需要使用元件製作
  • 分業需要製作成元件
  • 分頁需要開收資料 props
  • 分頁需要可以進行切換 emit
  • 分頁細節
  • Previous, Next 需要可以正確運作 (套用 disabled)
  • 當前頁面需要套用 .active 的視覺效果
// HTML
<!-- Bootstarp 5 CSS CDN -->
<link
  rel="stylesheet"
  href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css"
/>
<!-- Font Awesome -->
<link
  rel="stylesheet"
  href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.1/css/all.min.css"
/>
<div id="app">
  <div class="container py-5">
    <h2>元件章節作業</h2>
    <p>作業條件:</p>
    <ul>
      <li>每張卡片都需要使用元件製作</li>
      <li>分頁需要製作成元件</li>
      <li>分頁需要接收資料 props</li>
      <li>分頁需要可以進行切換 emit</li>
      <li>分頁細節</li>
      <li>Previous, Next 需要可以正確運作 (套用 disabled)</li>
      <li>當前頁面需要套用 .active 的視覺效果</li>
    </ul>
    <div v-if="this.totalPages">
      <div class="row" id="content">
        <card :card-url="displayData"></card>
      </div>
      <div class="d-flex justify-content-center mt-4">
        <pagination
          :current-page="currentPage"
          :total-pages="totalPages"
          @emit-pagination="changePage"
        ></pagination>
      </div>
    </div>
    <div v-else>等待資料抓取中...</div>
  </div>
</div>
// JS
<!-- Vue 3 CDN -->
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<!-- Bootstrap 5 JS CDN -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
<script type="module">
  const app = Vue.createApp({
    // 資料 (函式)
    data() {
      return {
        spotsData: [],
        displayData: [],
        perPage: 20,
        currentPage: 1,
        totalPages: 0,
      };
    },
    // 生命週期 (函式)
    created() {
      console.log(this);
      this.getData();
      this.pagination();
      this.updateData();
    },
    // 方法 (物件)
    methods: {
      getData() {
        const jsonUrl =
          "https://api.kcg.gov.tw/api/service/Get/9c8e1450-e833-499c-8320-29b36b7ace5c";
        let jsonData = {};

        fetch(jsonUrl, { method: "get" })
          .then((res) => {
            return res.json();
          })
          .then((data) => {
            jsonData = data.data.XML_Head.Infos.Info;
            // console.log(jsonData);
            this.spotsData = jsonData;
            this.pagination();
            this.updateData();
          });
      },
      pagination() {
        const dataTotal = this.spotsData.length;
        console.log(dataTotal);
        this.totalPages = Math.ceil(dataTotal / this.perPage);
      },
      changePage(page) {
        this.currentPage = page;
        this.updateData();
      },
      updateData() {
        const start = (this.currentPage - 1) * this.perPage;
        const end = start + this.perPage;
        this.displayData = this.spotsData.slice(start, end);
      },
    },
  });

  // 卡片元件
  app.component("card", {
    props: ["cardUrl"],
    template: `
        <div class="col-md-6 py-2" v-for="item in cardUrl" :key="item.Name">
            <div class="card bg-dark text-white text-left">
              <img class="card-img-top img-cover" height="155px" :src="item.Picture1">
              <div class="card-img-overlay d-flex justify-content-between align-items-end p-0 px-3" style="background-color: rgba(0, 0, 0, .2)">
                <h5 class="card-img-title-lg">{{ item.Name }}</h5>
                <h5 class="card-img-title-sm">{{ item.Zone }}</h5>
              </div>
            </div>
            <div class="card-body text-left">
                <p class="card-text"><i class="far fa-clock fa-clock-time"></i>&nbsp;
                  {{ item.Opentime }}</p>
                <p class="card-text"><i class="fas fa-map-marker-alt fa-map-gps"></i>&nbsp;
                  {{ item.Add }}</p>
                <p class="card-text"><i class="fas fa-mobile-alt fa-mobile"></i>&nbsp;
                  {{ item.Tel }}
                </p>
                <p class="card-text"><i class="fas fa-tags text-warning"></i>&nbsp; {{ item.Ticketinfo ? item.Ticketinfo : '無' }}</p>
            </div>
        </div>
        `,
  });

  // 分頁元件
  app.component("pagination", {
    props: ["currentPage", "totalPages"],
    methods: {
      changePage(page) {
        this.$emit("emit-pagination", page);
      },
    },
    template: `
        <nav aria-label="Page navigation example" v-if="this.totalPages">
          <ul class="pagination" id="pageid">
            <li class="page-item">
              <a
                href="#"
                @click="changePage(currentPage - 1)"
                :class="{ disabled: currentPage === 1 }"
                class="page-link"
                >Previous</a
              >
            </li>
            <!--當被點擊時changePage會等於被點擊到的值-->
            <li class="page-item" v-for="i in totalPages" :key="i">
              <a
                href="#"
                class="page-link"
                :class="{ active: currentPage === i }"
                @click="changePage(i)"
                >{{i}}</a
              >
            </li>
            <!--當cardsPage小於totalPages時,被點擊可以將頁碼加1-->
            <li class="page-item">
              <a
                href="#"
                class="page-link"
                @click="changePage(currentPage + 1)"
                :class="{ disabled: currentPage === totalPages }"
                >Next</a
              >
            </li>
          </ul>
        </nav>
        `,
  });

  app.mount("#app");
</script>