# 16. 單檔模組化

TIP

模組化的目的,在於將不同的功能、不同的流程分開,以便未來好管理。

# 摘要

關於 Component:

  • 也有人稱為組件、元件、模組。
  • 可以是按鈕、標題、輸入框...等網頁中所使用的物件。
  • 目的在於將不同的功能、不同的流程分開,以便未來好管理。
  • 包含了 HTML、CSS 和 JS。

使用 component 的方法:

  1. 建立檔案,寫一支 component 檔案。
    • 檔名首字要大寫。
    • 避免和 HTML 原有標籤相同。
    • 副檔名為 ".vue"。
  2. 撰寫內容,建立三大區塊。
    • <script>
      • 撰寫 JavaScript 程式碼。
      • 包含這個 componemt 名稱、管理要從父組件接收的資料,以及 computedmethods 等資料的處理。
    • <template>
      • 撰寫 HTML 程式碼。
    • <style>
      • 撰寫 CSS 程式碼。
      • 可使用 scoped 屬性限制這些 CSS 只能用於此 componemt。
      • 選擇器建議使用 idclass 而非 HTML 標籤,以免效能變差。
  3. 到 App.vue 載入並掛載組件
    • 將這個 component 用 import 匯入進來。
    • 把 component 掛載到 Vue 的 components 屬性。
  4. 在 HTML 加入組件的自定義標籤
    • 把 component 的自定義組件標籤放到 App.vue 的 <template> 中適當的位置。
  5. 透過 props 可將資料從外部的父組件 (例如 App.vue) 傳遞到內部的子組件。
    • 方式一:資料放在 component 的自訂標籤。
    • 方式二:資料放在 data,綁定到 component 的自訂標籤。
  6. 透過 computed 屬性,使用 set$emit ,可將子組件的值更新到父組件。
    • 方式一:子組件用 $emit("事件", 值) ,父組件在標籤上綁定事件,再用 methods 進行處理。
    • 方式二:子組件用 $emit("update:變數", 值) ,父組件在標籤上綁定的變數後方加上 .sync 來讓更新同步。

# Component 檔案內容

Vue CLI 檔案架構和畫面

使用 Vue CLI 建立一個新的專案資料夾,例如: component-test。

vue create component-test

打開裡面的 "App.vue" 或 "HelloWorld.vue" 等副檔名為 "vue" 的檔案,這些都是用於這個專案的 component,裡面包含三大部分:

  • template → HTML
  • script → JavaScript
  • style → CSS

以 "HelloWorld.vue" 為例:

# template

用於撰寫這個 component 所需要的 HTML 程式碼。

//- HelloWorld.vue
<template>
  <div class="hello">
    <h1>{{ msg }}</h1>
    // 略...
  </div>
</template>

# script

負責 JavaScript 程式碼,包含:

  • 使用 ES6 的 export 語法
  • 給予這個 componemt 名稱 (name: 'HelloWorld')
  • 管理要從父組件接收的資料 (props)
  • 以及 computedmethods 的處理
//- HelloWorld.vue
<script>
export default {
  name: 'HelloWorld',  // 這個 componemt 的名稱
  props: {             // 管理資料從父組件傳遞到這個子組件的屬性
    msg: String        // 資料的名稱為 msg,型別為字串 (String)
  }
}
</script>

# style

<style> 則為 CSS 程式碼撰寫的區域。可使用 scoped 屬性限制這些 CSS 只能用於此 componemt。

如果 CSS 選擇器使用像下方這些 HTML 的標籤 (h3, ul, ...),效能較差。因此建議配合 HTML 標籤的 idclass 來使用。

//- HelloWorld.vue
/* 使用 scope 屬性,限制樣式影響的範圍 */
<style scoped>
h3 {
  margin: 40px 0 0;
}
ul {
  list-style-type: none;
  padding: 0;
}
li {
  display: inline-block;
  margin: 0 10px;
}
a {
  color: #42b983;
}
</style>

# Component 與 App.vue 的關係

"App.vue" 是主要 component ,也是其他 component 最後匯集的地方。

當我們寫好 component 時,需要到 "App.vue" 進行這些動作:

  1. 載入 component:用 import 載入進來。
  2. 掛載 component:將載入的 component 掛載到 Vue 的 components 屬性。
//- App.vue
<script>
// 將 component 用 import 載入進來
import HelloWorld from './components/HelloWorld.vue'

export default {
  name: 'App',
  components: {
    HelloWorld     // 掛載 component
  }
}
</script>

然後,就可以用 <component></component> 或者 <component /> 格式的自定義組件標籤,來將組件放到 HTML 主頁中。(裡面的 component 字樣可替換成自定義的組件標籤名稱。)

要傳到子組件的資料 (例如 msg) 也會被放在子組件所屬的自定義標籤裡。

//- App.vue
<template>
  <div id="app">
    <img alt="Vue logo" src="./assets/logo.png">
    <HelloWorld msg="Welcome to Your Vue.js App"/>    // 自定義組件標籤
  </div>
</template>

以下則是用於 #app 的 CSS 樣式。

//- App.vue
<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

# 建立單檔 component

像 "HelloWorld.vue" 這樣把 HTML、CSS、JavaScript 這三大部分都寫在同一個檔案裡,就是單檔 component。

以下新增一個標題模組 (TitleComponent.vue):

TitleComponent.vue

# 1. 建立新檔

檔名為 "TitleComponent.vue",需注意:

  • 檔名首字要大寫。
  • 避免和 HTML 原有標籤相同。
  • 副檔名為 ".vue"。

# 2. 撰寫內容

先在新檔中建立好 <script><template><style> 三大區塊。然後分別在這些區塊內撰寫 "TitleComponent" 這個標題組件的內容。

//- TitleComponent.vue
<template>
  <h1 class="title">{{ title }}</h1>
</template>

<script>
export default {
  name: "titleComponent",
  // 使用函式回傳 data 中的資料
  data() {
    return {
      title: "This is title in TitleComponent"
    };
  }
};
</script>

<style scoped>
h1.title {
  color: deeppink;
}
</style>

:::info 使用 data 函式回傳資料 需要注意的是,如果在 component 本體會用到 data,要用函式 return 資料,而非用以往物件的形式來放資料。

這是因為 component 可以重複被利用、分別使用不同的資料。使用物件會讓所有的 component 都使用同樣的資料。

但以函式來回傳 data ,則每次註冊 component,就會回傳一個新的物件。 :::

# 3. 到 App.vue 載入並掛載組件

將 "TitleComponent" 載入到 "App.vue",並掛載到 Vue ,以使用這個組件。

//- App.vue
<script>
import HelloWorld from "./components/HelloWorld.vue";
import TitleComponent from "./components/TitleComponent.vue";

export default {
  name: "App",
  components: {
    HelloWorld,
    TitleComponent
  }
};
</script>

# 4. 在 HTML 加入組件的自定義標籤

把 component 的自定義標籤,像一般的 HTML 標籤一樣,放到 App.vue 的 <template> 中適當的位置。

//- App.vue
<template>
  <div id="app">
    <TitleComponent />
    <img alt="Vue logo" src="./assets/logo.png" />
    <HelloWorld msg="Welcome to Your Vue.js App" />
  </div>
</template>

# 5. 執行以觀看成果

終端機進入到專案資料夾,輸入 npm run serve 取得執行位置。將位置貼到瀏覽器開啟,就能看到執行結果。

如果沒有結束終端機的執行,則在檔案進行的任何變更,存檔後都會即時更新到瀏覽器畫面上。

# props 接收外部傳入資料

# 子組件 (內組件)

如果在網頁中有多個地方都會用到這個 component,但這些 component 的內容各不相同,顯示的資料希望由外部提供,則不會在 component 本身使用 data 屬性。

為了接收外部傳進來的資料,我們會使用 props 屬性來管理外部資料。

//- TitleComponent.vue
<script>
export default {
  name: "titleComponent",
  // data() {
  //   return {
  //     title: "This is title in TitleComponent"
  //   };
  // }
  props: {             // 管理外部傳入的資料
    title: {           // 資料的名稱
      type: String,    // 傳入的資料類型
      required: true   // 有資料才會顯示此組件
    }
  }
};
</script>

required 屬性 在 "HelloWorld.vue" 裡面,資料為 msg: String ,只指定了資料的型別。 在這個例子,則為資料 title ,多用了 required 屬性,表示有資料時,才會顯示這個 component。

# 父組件 (外組件)

至於外部的父組件,則要給予資料。資料的呈現方式有兩種:

  1. 直接放在自定義標籤。例如: <HelloWorld msg="Welcome to Your Vue.js App" />
  2. 將資料放在 data 函式回傳,並綁定到所需的自定義標籤中的屬性。

HTML

//- App.vue
<template>
  <div id="app">
    <TitleComponent :title="titleText" />           // 資料綁定到 title 屬性
    <img alt="Vue logo" src="./assets/logo.png" />
    <HelloWorld msg="Welcome to Your Vue.js App" /> // 資料直接放在自定義標籤中
  </div>
</template>

JS

//- App.vue
<script>
import HelloWorld from "./components/HelloWorld.vue";
import TitleComponent from "./components/TitleComponent.vue";

export default {
  name: "App",
  data() {                                     // 用來回傳資料
    return {
      titleText: "This is title from App.vue"  // 要放到 title 的內容
    };
  },
  components: {
    HelloWorld,
    TitleComponent
  }
};
</script>

# 建立模組化 component

除了建立單檔 component 之外,副檔名為 ".vue" 的組件檔案,也可以像平常撰寫時,將 HTML、CSS,以及 JavaScript 這三個部分拆成獨立的檔案,建立模組化 component。

# 1. 建立組件資料夾與檔案

在 "components" 資料夾底下建立一個新資料夾,名稱為新的組件名稱,規則與單檔建立名稱相同,例如:InputComponent。

然後在此資料夾裡面建立以下三個檔案:

  • index.vue
  • style.css
  • template.html
src
├─assets
├─components
│ ├─InputComponent      // 模組化 component
│ │ ├─index.vue         // JavaScript,主要檔案
│ │ ├─style.css         // CSS
│ │ └─template.html     // HTML
│ ├─HelloWorld.vue      // 單檔 component
│ └─TitleComponent.vue  // 單檔 component
└─App.vue

# 2. 將 HTML 和 CSS 匯入到此組件的 .vue 檔案

以往撰寫 HTML、CSS 和 JavaScript 檔案時,會將 CSS 和 JavaScript 檔案分別以 <link><script> 標籤匯入到主要的 HTML 檔案裡面。

而在 Vue CLI ,則是將作用於同一個 component 的 "template.html" 和 "style.css" 的檔案路徑,分別用 src 屬性透過 <template><style> 標籤匯入到 "index.vue" 裡面。

//- index.vue

<script>
export default {
  name: "inputComponent"
};
</script>

// 使用 src 匯入此組件的 template 和 style
<template src="./template.html"></template>
<style src="./style.css" scoped></style>

如此一來,就能專心在個別檔案寫程式。

# 3. 撰寫程式

假如我們要在 input 欄位裡面預設顯示 "TitleComponent.vue" 組件用到的資料 titleText

HTML 部分,在 input 標籤使用 v-model 並綁定新的變數 text 來連接資料。

//- InputComponent/template.html

<input type="text" v-model="text" />

由於要使用 text 接收 titleText ,而 titleText 又是從 "App.vue" 傳入的資料,所以需要用 props 來管理 text

//- InputComponent/index.vue

<script>
export default {
  name: "inputComponent",
  props: {
    text: {
      type: String,
      required: true
    }
  }
};
</script>

# 4. 到 App.vue 載入並掛載組件

同樣地,我們要將這個 component 載入到 "App.vue" ,才能夠顯示到畫面上。

之前的 component 只有單檔,所以匯入檔案就等於匯入組件。但現在 "InputComponent" 被模組化,所以我們要傳入 "InputComponent" 資料夾底下主要的 "index.vue" 來匯入這個 component。

//- App.vue
<script>
import HelloWorld from "./components/HelloWorld.vue";
import TitleComponent from "./components/TitleComponent.vue";
import InputComponent from "./components/InputComponent/index.vue";

export default {
  name: "App",
  data() {
    return {
      titleText: "This is title from App.vue"
    };
  },
  components: {
    HelloWorld,
    TitleComponent,
    InputComponent
  }
};
</script>

匯入之後,將這個 component 放到畫面,並且綁定資料。

//- App.vue
<template>
  <div id="app">
    <TitleComponent :title="titleText" />
    <img alt="Vue logo" src="./assets/logo.png" />
    <InputComponent :text="titleText" />             // 放上畫面並綁定資料
    <HelloWorld msg="Welcome to Your Vue.js App" />
  </div>
</template>

如此一來,就能在畫面上看到多了一個輸入框,並且裡面已填好 titleText 的內容。

# $emit 將資料傳送到外部

v-model 可以讓資料與畫面雙向互動,如果我們希望修改 input 的內容時,上面的 titleText 標題的內容也能跟著改變,則需要透過 $emit 來進行資料的傳遞。

這是因為 titleText 是屬於外組件 "App.vue" 的資料,而不是 "InputComponent" 本身的資料,因此不被允許跨組件任意改動。

這裡我們建立一個新的變數 inputText 以和原有變數 text 區別,並透過 computed 去跟原本的 text 進行連動。也因此,HTML 這裡,input 改成跟資料 inputText 連結。

//- InputComponent/template.html

<input type="text" v-model="inputText" />

由於 inputText 要「取值」 (從 "App.vue" 取得 titleText 的內容) 以及「給值」 (當 input 的內容被改變時,新的值要被賦予 "App.vue" 的 titleText)。所以 inputText 用物件而非函式的形式,使用 get(){}set(){} 來實現「取值」以及「給值」這兩件事。

# 方法一:傳入事件、新值作為參數

# InputComponent

使用 $emit 有兩種方法。第一個方法是傳入事件、新值作為參數。

//- InputComponent/index.vue

<script>
export default {
  name: "inputComponent",
  props: {
    text: {
      type: String,
      required: true
    }
  },
  computed: {
    inputText: {
      // 取值:回傳 text 的值 (來自 App.vue 的 titleText 變數)
      get() {
        return this.text;
      },
      // 賦值:當 textChange 事件發生時,將新值 val 給父組件
      set(val) {
        this.$emit("textChange", val);
      }
    }
  }
};
</script>

# App.vue

textChange 這個事件會發生在 "InputComponent" 這個組件的內容改變時,並且觸發 "App.vue" 的事件處理器 changeHandler

//- App.vue

<template>
  <div id="app">
    <TitleComponent :title="titleText" />
    <img alt="Vue logo" src="./assets/logo.png" />
    <InputComponent :text="titleText" @textChange="changeHandler" />  // 綁定事件
    <HelloWorld msg="Welcome to Your Vue.js App" />
  </div>
</template>

要處理事件,一樣用 methods 來將改變後的新值,賦予 titleText,使得讀取到 titleTextTitleComponent 標題內容也跟著改變。

//- App.vue

<script>
import HelloWorld from "./components/HelloWorld.vue";
import TitleComponent from "./components/TitleComponent.vue";
import InputComponent from "./components/InputComponent/index.vue";

export default {
  name: "App",
  data() {
    return {
      titleText: "This is title from App.vueText
    };
  },
  components: {
    HelloWorld,
    TitleComponent,
    InputComponent
  },
  // 使用 methods 來處理事件,改變 titleText 的值
  methods: {
    changeHandler(val) {
      this.titleText = val;
    }
  }
};
</script>

# 方法二:使用 update 和 sync

# InputComponent

使用 $emit 還有另一個更簡潔的寫法來傳值。第一個參數使用 update:變數名稱

<script>
export default {
  name: "inputComponent",
  props: {
    text: {
      type: String,
      required: true
    }
  },
  computed: {
    inputText: {
      get() {
        return this.text;
      },
      set(val) {
        this.$emit("update:text", val);
      }
    }
  }
};
</script>

# App.vue

然後在 "App.vue" 的 template 裡面,將那個要被改值的變數後面加上 .sync

<template>
  <div id="app">
    <TitleComponent :title="titleText" />
    <img alt="Vue logo" src="./assets/logo.png" />
    <InputComponent :text.sync="titleText" />        // 變數 text 後面加上 .sync
    <HelloWorld msg="Welcome to Your Vue.js App" />
  </div>
</template>

這樣一來,事件跟處理器都不用寫,當內部的值更新時,就能將值從內部傳到外部同步改寫。只不過,除了改值以外,這個方法無法對變數進行其他動作。