元件
元件介紹
為什麼要拆解成元件
- 增加程式碼的可複用性
- 避免單一檔案過大
- 易於管理及協作
- 元件功能獨立化
頁面元件結構
元件資料獨立 與 傳遞
接下來介紹到的 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>
{{ item.Opentime }}</p>
<p class="card-text"><i class="fas fa-map-marker-alt fa-map-gps"></i>
{{ item.Add }}</p>
<p class="card-text"><i class="fas fa-mobile-alt fa-mobile"></i>
{{ item.Tel }}
</p>
<p class="card-text"><i class="fas fa-tags text-warning"></i> {{ 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>