0%

toRef

  • 將…變成 ref
    從 reactive 物件抽出包裝成一個 ref 的值,與原本物件 是連接的,所以修改時,原本的值也會跟著變動。
  • 只能處理一個屬性,須告知要輸入的屬性\物件
  • 注意: 更改原始物件內的值,所響應會跟著變動

在 prop 的應用

  • 來自的 props 是 物件資料,可以使用toRef
    1
    2
    3
    4
    5
    6
    7
    8
    9
    props: {
    apiUser: {
    required: true,
    type: Object
    }
    },
    setup(props) {
    const userCopy = toRef(props, 'apiUser')
    }
  • 範例二
1
2
3
4
5
6
7
8
9
10
<template>

<h2>姓名: {{person.name}}</h2>
<h2>年齡: {{person.age}}</h2>
<h2>職業描述: {{person.job.front.dec}}</h2>
<button @click='person.name += ~'>修改姓名</button>
<button @click='person.age ++ '>增加年齡</button>

</template>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<script>
import {reactive} from 'vue';

export default {
name: 'Demo',
setup(){
const person = reactive({
name: 'Eva',
age: 25,
job:{
front:{
dec:'前端工程師'
}
}
})

return{
person
}
}
}
</script>
  • 要將以上的模板寫的更容易
1
2
3
4
5
6
7
8
9
10
<template>

<h2>姓名: {{name}}</h2>
<h2>年齡: {{age}}</h2>
<h2>職業描述: {{dec}}</h2>
<button @click='name += ~'>修改姓名</button>
<button @click='age ++ '>增加年齡</button>

</template>

1
2
3
4
5
6
//就是返回普通的物件內容
return{
name: person.name,
age: person.age,
dec: person.job.front.dec
}
  • 但此方式要注意,字串模板會無法響應更新的問題

  • 使用 toRef 來達到響應

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<script>
import {reactive,toRef} from 'vue';

export default {
name: 'Demo',
setup(){
const person = reactive({
name: 'Eva',
age: 25,
job:{
front:{
dec:'前端工程師'
}
}
})

//單一轉換
const name2 = toRefs(person,'name')

return{
name: toRef(person,'name'),
age: toRef(person,'age'),
dec: toRef(person.job.front,'dec')
}
}
}
</script>

toRefs

toRefs用來把響應式物件轉換成普通物件,把物件中的每一個屬性,包裹成ref物件
* toRefs就是toRef的升級版,只是toRefs是把響應式物件進行轉換

延續上方,toRefs

  • 可以批量處理,一個物件裡的屬性
    • 只會將第一層展開,所以以下在 job 的內層
1
2
3
4
5
6
7
8
9
10
<template>

<h2>姓名: {{name}}</h2>
<h2>年齡: {{age}}</h2>
<h2>職業描述: {{job.front.dec}}</h2>
<button @click='name += ~'>修改姓名</button>
<button @click='age ++ '>增加年齡</button>

</template>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<script>
import {reactive,toRefs} from 'vue';

export default {
name: 'Demo',
setup(){
const person = reactive({
name: 'Eva',
age: 25,
job:{
front:{
dec:'前端工程師'
}
}
})

//可以批量處理,所以只需要傳入個物件,
const new = toRefs(person)
console.log(new)

//在物件中再放toRefs(person) ,會變成物件包物件,所以要展開
return{
...toRefs(person)
// name: toRef(person,'name'),
// age: toRef(person,'age'),
// dec: toRef(person.job.front,'dec')
}
}
}
</script>

從開始學習前端之後,大多的學習資源都是來自線上課程、youtube 影片為主,鮮少透過閱讀書籍來學習相關知識,但自從進入公司之後,因為公司內有些程式書籍能夠借閱,先是從 JavaScript 大全開始,一開始閱讀起來真的滿痛苦,不知道該如何消化內容,直接順著讀過去又怕忘記,但目前還是順順的讀到了第8章~後來,找到的方式就是在筆記軟體上做些註記,工作上遇到相關的再回過頭看一次,加入筆記。

而在此次,發現 TypeScript 即便已經看過介紹型別、基本使用基礎的影片後,仍然覺得不太足夠,所以就開始翻閱書籍 「讓 TypeScript 成為你全端開發的 ACE」,文中介紹滿多之前沒有注意到觀念、也有帶到不少 JavaScript 的觀念來呼應。所以本篇,多是來自於書中資訊,透過整理呈現。

註記 Annotation V.S. 斷言 Assertion

  • 型別註記:指在告訴 TS 編譯器:「任何被註記到的變數、函式的參數等,都必須遵照被註記過後的變數型別」。
  • 所以編譯器會隨時隨地的監測該變數有沒有出現型別衝突的可能—關鍵字為遵照。
  • 而,斷言型別則是無視 TS 編譯器分析整個程式碼的型別推論過程,果斷的告訴 TS 編譯器:「被斷言過後的表達式之運算結果就是某某型別」—關鍵意象是「覆蓋」該表達式的型別推論結果。

註記與斷言

  • 帶有冒號的相關語法
1
2
let randomNumber:number = Math.random();
const subscribed:boolean = true;
  • 如果遇到函式,參數(Argument)部分除了可以有類似註記方式標明輸入的參數型別外,在參數宣告結尾也可以註記該函式輸出之型別。
1
2
3
function isPositive(input: number):boolean{
return input > 0
}
  • 但JS裡,宣告函式的方法有很多種,其中一個是將函式最為值指派到變數。
1
2
3
const isPositive:(input:number) => boolean = function(input){
return input > 0
}
  • 以上,也能把變數指派的函式,改成ES6箭頭函式
    • 指派運算子(Assignment Operator ,就是指程式裡的等號)左方是函式的型別註記表示法,右方則是普通函式宣告
1
const isPositive:(input:number) => boolean = input => input >0;
  • 採用邊宣告函式、邊註記型別的方式定義函式
1
2
3
const isPositive = function (input:number):boolean{
return input>0
}
1
2
const isPositive =  (input:number):boolean =>  input>0

型別斷言語法

  • 斷言(Assertion)的語法很簡單,看到有使用關鍵字 as 或者 <T>(...) 的格式就是斷言的用法。
  • 通常會使用斷言的情境,程式沒辦法推論某表達式的確切運算結果之型別,我們才會用選擇使出斷言來處理這種情境。
  • 程式沒辦法推論?
    • 使用第三方的資源(Third-party resources),如使用外來JSON API 獲得的內容之型別格式、讀取檔案轉成 JSON 物件的結果、使用套件提供的功能、呼叫會回傳未知結果的函式。
1
const aNumber = returnsUnknow() as number;

或是這樣:

1
const aNumber = <number>(returnsUnkonw());
  • 請注意:斷言的語法部分,沒有人斷言在變數的名稱宣告部分—也就是是說,如果你這樣寫是錯的:
1
const aNumber as number = returnsUnknow(); //這是錯誤的
  • 概念有點像,決斷地告訴程式,某表達式的運算結果之型別。
  • 變數本身不是被運算,而是被指派某個東西,而斷言應該是斷在被指派的值或表達式的運算結果上。

複雜一點

  • 函式宣告表達式有可以被斷言(畢竟函式作為表達式也會被當成值)
1
const isPositive =(input => input > 0) as (input: number) => boolean

或者是:

1
const isPositive = <(input: number) => boolean>(input => input >0);
  • 上面斷言的寫法效果跟之前示範過基礎註記手法:
1
const isPositive :(input: number) => boolean = input => input >0;

或者是

1
const isPositive =(input: number):boolean => input >0;

說下,敘述式與表達式的定義與差別

  1. 敘述式:程式運行的流程,例如:JS 裡的判斷敘述式(if…else)以及迴圈敘述式(for 或者是 while 迴圈)。
  2. 表達式代表的則是程式碼運算的流程,並且會將運算結果回傳。其中兩者最關鍵差異為:敘述式不會回傳值,表達式則會。
  • 以下為常見的表達式範例:
1
2
3
4
5
6
7
8
9
10
 //運算表達式 Arithmetic expression
1+2*3 ; //回傳解果為7
//邏輯表達式 Logical Expression
true && (something === null || myAge < 18) //回傳結果為 true 或 false

//函式(或方法的呼叫) Function / Method Invocation Expression
Math.pow(2,10) //回傳結果為 1024

//三元運算子 Ternary Operator
myAge < 18 ? 'Youngster' : 'Adult'; //回傳結果為 'Youngster' 因為 myAge < 18

而敘述式:

1
2
3
4
5
6
7
8
9
10
11
12
13
if(/*expression 1*/){
//若 expression1 為 true 則執行

}else if(/*expression 2*/){
//若 expression2 為 true 則執行
} else{
//若 expression1、expression2 都不為 true 則執行此
}

//迴圈敘述式 Looping Statement
while (/*expression */){
//若 expression 為 true, 則重複執行直到 expression 為 false 時跳脫
}
  • 按照上面敘述式的定義 — 由於敘述式是在敘述運行流程,而不會回傳值,所以你才不會在 JavaScript 裡面看到這樣的寫法:
1
2
3
4
5
6
// JS 並有提供這寫法
const status = if(Age <18 ){
return 'Young'
}else{
return 'Adult'
}
  • 注意:敘述式不一定是多行式(或區塊式)地呈現的最佳案例:變數宣告的指派敘述式(Variable Declaration Assignment Statement)
    1
    2
    //變數宣告的 指派敘述式
    const foo =123;
  • 請問上面指派式會回傳什麼結果 =>數字 123 或 udefined。正解為 udefined
    • 指派式的回傳結果為 undefined
  • 所以可以歸納出,javaScript 的變數宣告的指派式屬於敘述式,非表達式。

表達式什麼時候會以非單行的程式碼,也就是區塊的方式?

  • 立即呼叫函式表達式(IIFE)
1
2
3
4
const status = (funcrion(myAge){
if (myAge <18){ return 'Young'}
return 'Adult'
})(16) //假設填入參數為16,呼叫該函式的結果回傳 'Young'
  • 當可以分清楚表達式與敘述式差別後,回歸型別斷言
  • 斷言的基礎語法
    • 斷言的語法只能用在表達式上,因為表達式具備回傳值,敘述式則沒有
    • 因此,可以「斷言該表達式所運算結果之代表型別」

寫法如下:

1
2
3
<expression> as T assertion

<T assertion >(<expression>)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//運算表達式
(foo+bar*baz) as number;

// 邏輯表達式
(isPositive(num) && idEven(num)) as boolean;

//function
Math.pow(2, 10) as number

//
(myAge <18 ? 'Young' : 'Adult') as string;

//IIFE
(funcrion(myAge){
if (myAge <18){ return 'Young'}
return 'Adult'
})(16) as number

//只要是表達式,就算在其他敘述式、表達式內也能使用
somefunction(foo as number , bar as string) as boolean

換種寫法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<number>(foo+bar*baz);

// 邏輯表達式
<boolean> (isPositive(num) && idEven(num)) ;

//function
<number> Math.pow(2, 10) ;

//
<string>(myAge <18 ? 'Young' : 'Adult') ;

//IIFE
<number>(
funcrion(myAge){
if (myAge <18){ return 'Young'}
return 'Adult'
})(16)
);
//只要是表達式,就算在其他敘述式、表達式內也能使用
<boolean>somefunction(foo as number , bar as string)

以上針對註記 Annotation V.S. 斷言 Assertion 做個簡單整理~

最近遇到將表單資料存到後端的方式,過去一直以為是將資料整理為物件方式存取即可,後來發現使用FormData來建立物件傳送檔案即可!

  • FormData 是一個表單格式,利用它將資料轉成表單的格式,並且以表單的形式回傳給後端

建立 FormData 物件

  • 最直接方式,先取得form的資料
1
2
3
4
5
6
7
8
// 根據表單元素取得表單資料
var form = document.querySelector('form');
var formData = new FormData(form);

// 可以用 append 把欄位灌進去表單中
var formData = new FormData(); // 一開始表單的資料是空的
formData.append('username', Chris); // 增加欄位
formData.append('image', file); // 檔案也可以透過 append 放進來

以下介紹幾個基礎的使用方式:

  • formData.append(name, value) : 加入有 name 和 value 的表單資料(name:表單標籤的名稱,value: input 輸入的值)
  • formData.append(name, blob, fileName) : 加入字段,就像它是 <input type="file">,第三個參數 fileName 設置檔案名
  • formData.delete(name) : 移除帶有 name 的字段,
  • formData.get(name) : 取得帶有 name 的值,
  • formData.has(name) : 如果帶有給定 name 的字段,返回 true,否則返回 false。

使用axios 傳遞資料

1
2
3
4
5
6
7
8
9
var formData = new FormData();
//宣告要取得的值
var title = form.name
formData.append("formTitle", title);
axios.post('upload_file', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
})

補充 vue+Typescript 宣告型別

  • 以 stack overflow 內的資料為例:
    • 可以使用 ref來取得DOM元素,並宣告該變數為HTMLFormElement
      1
      2
      3
      4
      5
      6
      <el-form
      class="m-auto"
      ref="formRef"
      :model="form"
      :rules="rules"
      >
      1
      const formRef = ref<HTMLFormElement>();
  • 另是使用 element-UI 的表單,也可以使用
    import { ElForm } from "element-ui";

如何使用jQuery AJAX submit 傳送form表單方法
JavaScript / jQuery 前端開發入門實戰
JavaScript Info-formData
stackoverflow

useAsyncData

1
2
3
4
5
6
7
8
9
10
const {
data: Ref<DataT>,
pending: Ref<boolean>,
refresh: (force?: boolean) => Promise<void>,
error?: any
} = useAsyncData(
key: string,
fn: () => Object,
options?: { lazy: boolean, server: boolean }
)
  • 頁面、組件中都可以使用useAsyncData 取得資料
  • Key :寫一個唯一的id
    請求資料的函式
  • 使用 useAsyncData 會回傳物件
    data : 返回的資料
    pedding : 指出資料是否被取得
    refresh : 是否要刷新資料

    Option 介紹

  • Lazy 延遲載入,直到數據獲取時,才顯示於頁面
  • 默認會是 false
  • Server 頁面是在server端要載入,就可以設定為true
  • Pick 從回傳的資料中,選擇要使用的key值

useAsyncData撰寫方式的範例

  • 本地建立資料夾
    • server/api/count.ts
1
2
3
4
5
let counter = 0
export default () => {
counter++
return JSON.stringify(counter)
}
  • app.vue,使用 useAsyncData
1
2
3
4
5
6
7
<script setup>
const { data } = await useAsyncData('count', () => $fetch('/api/count'))
</script>
<template>
Page visits: {{ data }}
</template>

useFetch

  • 頁面、組件中可以使用useFetch獲取任意URL資源
  • useFetch包裝 了useAsyncData 和 $fetch
  • 會根據 URL 自動生成 Key,也會去推斷API 的類型a
    1
    2
    3
    4
    5
    6
    const {
    data: Ref<DataT>,
    pending: Ref<boolean>,
    refresh: (force?: boolean) => Promise<void>,
    error?: any
    } = useFetch(url: string, options?)
1
2
3
4
5
6
7
<script setup>
const { data } = await useFetch('/api/count')
</script>

<template>
Page visits: {{ data.count }}
</template>

補充:其他2個類似方式

  • useLazyAsyncData : 方法等於useAsyncData,僅設置了lazy選項為true
  • useLazyFetch : 方法等於useFetch,僅設置了lazy選項為true

實作範例:

  • /api/mountains/everest
    • API 會產生的物件資料
1
2
3
4
5
6
7
8
9
10
11
{
"title": "Mount Everest",
"description": "Mount Everest is Earth's highest mountain above sea level, located in the Mahalangur Himal sub-range of the Himalayas. The China–Nepal border runs across its summit point",
"height": "8,848 m",
"countries": [
"China",
"Nepal"
],
"continent": "Asia",
"image": "https://upload.wikimedia.org/wikipedia/commons/thumb/f/f6/Everest_kalapatthar.jpg/600px-Everest_kalapatthar.jpg"
}

Nuxt 3 為了提高效率會在取得資料後,緩存在當前頁面的payload
官方建議:如果只打算在組件中使用到 title 和 description ,你可以使用關鍵的屬性運用即可
選項中加入 pick,裡面的值就是取得資料中的key值

1
2
3
4
5
6
7
8
<script setup>
const { data: mountain } = await useFetch('/api/mountains/everest', { pick: ['title', 'description'] })
</script>

<template>
<h1>{{ mountain.title }}</h1>
<p>{{ mountain.description }}</p>
</template>

參考資料

要熟悉 TypeScript 的型別訂定,並不容易,而且撰寫時也會花費較多的時間,但好處是減少程式碼的錯誤,也讓協作者更容易解讀對方撰寫的程式碼。
萬事起頭難,不同的地方就是多讀幾次,或是尋找相關的範例來相呼應!

Class

傳統方法中,JavaScript 透過建構函式實現類別的概念,透過原型鏈實現繼承。而在 ES6 中,我們終於迎來了 class。

  • 定義一切事務的抽象特點

  • object :class 的實例 new Class 生成

  • 物件導向 OOP(Object Oriented Programming) :三大特性 封裝、繼承、多型

    • 封裝(Encapsulation):將對資料的操作細節隱藏起來,只暴露對外的介面。外界呼叫端不需要(也不可能)知道細節,就能透過對外提供的介面來訪問該物件,同時也保證了外界無法任意更改物件內部的資料
    • 繼承(Inheritance):子類別繼承父類別,子類別除了擁有父類別的所有特性外,還有一些更具體的特性
    • 多型(Polymorphism):由繼承而產生了相關的不同的類別,對同一個方法可以有不同的響應

      複習下 ES6 撰寫方式

  • 使用 class 定義類別,使用 constructor 定義建構函式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Animal{
constructor(name){
this.name = name;
}
run () {
return `${this.name} is running`
}
}
const snake = new Animal('lily')
console.log(snake.run()); // lily is running

// 繼承的特性(繼承父類的屬性及方法), 繼承 run 的方法
class Dog extends Animal {
bark() {
return `${this.name} is barking`
}
}

const bao = new Dog('bao')
console.log(bao.run()) // bao is running
console.log(bao.bark()) // bao is barking
  • 使用 extends 關鍵字實現繼承
  • 这里我们重寫構造函式,注意在子類的構造函式中,必須使用 super 調用父類的方法,否則會報錯。
1
2
3
4
5
6
7
8
9
10
11
12
13
// 
class Cat extends Animal {
constructor(name) {
super(name)
console.log(this.name) //maomao
}
run() {
// run 方法重寫
return 'Meow, ' + super.run()
}
}
const maomao = new Cat('maomao')
console.log(maomao.run()) //Meow, maomao is runing

TypeScript 中的 class

TypeScript 可以使用三種訪問修飾符(Access Modifiers)

  • public 修飾的屬性或方法是公有的,可以在任何地方被調用到,預設所有的屬性和方法都是 public 的
  • private 修飾的屬性或方法是私有的,不能在声明它的类的外部調用
  • protected 修飾的屬性或方法是受保护的,它和 private 類似,區别是它在子類中也是允许被訪問的
    例子說明:
  • name 被設定為了 public,所以直接訪問實例的 name 屬性是允許的。
1
2
3
4
5
6
7
8
9
10
11
class Animal {
public name;
public constructor(name) {
this.name = name;
}
}

let a = new Animal('Jack');
console.log(a.name); // Jack
a.name = 'Tom';
console.log(a.name); // Tom
  • 希望有的屬性是無法直接存取的,這時候就可以用 private 了:
  • name 設為 private
1
2
3
4
5
6
7
8
9
10
11
12
13
class Animal {
private name;
public constructor(name) {
this.name = name;
}
}

let a = new Animal('Jack');
console.log(a.name); // Jack
a.name = 'Tom';

// index.ts(9,13): error TS2341: Property 'name' is private and only accessible within class 'Animal'.
// index.ts(10,1): error TS2341: Property 'name' is private and only accessible within class 'Animal'.
  • 使用 private 修飾的屬性或方法,在子類別中也是不允許訪問的:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Animal {
private name;
public constructor(name) {
this.name = name;
}
}

class Cat extends Animal {
constructor(name) {
super(name);
console.log(this.name);
}
}

// index.ts(11,17): error TS2341: Property 'name' is private and only accessible within class 'Animal'.
  • 而如果是用 protected 修飾,則允許在子類別中訪問:
  • 將上述程式碼修改一下
1
2
3
4
5
6
class Animal {
protected name;
public constructor(name) {
this.name = name;
}
}

類別 class 的型別

給類別加上 TypeScript 的型別很簡單,與介面類似:

1
2
3
4
5
6
7
8
9
10
11
12
class Animal {
name: string;
constructor(name: string) {
this.name = name;
}
sayHi(): string {
return `My name is ${this.name}`;
}
}

let a: Animal = new Animal('Jack');
console.log(a.sayHi()); // My name is Jack

類別

class 與 interface

  • 介面(Interfaces):不同類別之間公有的屬性或方法,可以抽象成一個介面。介面可以被類別實現(implements)。一個類別只能繼承自另一個類別,但是可以實現多個介面
  • 在 物件導向 世界中,一個 class 只能繼承自另外一個class
  • 有時候不同 class 之前,可以有共同的特性,使用子類繼承父類的方法很難來完成
  • class 可以使用 implements 來實現 interface (提高 OOP 的靈活性)

範例一:

  • 有 車子和手機,都有打開radio的功能
  • 可以將兩個共有的抽取為一個 interface
1
2
3
4
5
6
7
8
9
10
11
class Car  {
switchRadio(trigger: boolean) {

}
}

class Cellphone {
switchRadio(trigger: boolean) {

}
}

可以將兩個共有的抽取為一個 interface

  • void => 代表什麼都不返回
  • 在class 後面放入 implements 讓類去實現它
1
2
3
4
5
6
7
8
9
10
11
12
13
interface Radio {
switchRadio(trigger: boolean): void;
}

class Car implements Radio {
switchRadio(trigger) {
return 123
}
}
class Cellphone implements Radio {
switchRadio() {
}
}

範例二

  • 新特性為 檢查電池的容量
  • 是手機有,但汽車沒有的
  • 所以新建立一個 interface (檢查battery)
    • 此功能只有手機有,所以放在手機的class
1
2
3
4
5
6
7
8
9
10
11
interface Battery {
checkBatteryStatus(): void;
}
// 要實現多个介面,我们只需要中間用 都好 隔开即可。
class Cellphone implements Radio, Battery {
switchRadio() {
}
checkBatteryStatus() {

}
}

此外 interface 之間有繼承關係

  • 建立 radioWithBattery 繼承 Radio ,裡面再放入檢查電量的設定
1
2
3
4
5
6
7
8
9
10
11
12
interface radioWithBattery extends Radio {
checkBatteryStatus(): void;
}


class Cellphone implements radioWithBattery {
switchRadio() {
}
checkBatteryStatus() {

}
}

類別與介面

enum 列舉

  • 常數指執行程序中不會被改變的值,在 JS 中我們一般會用 const 來宣告
  • 但有些取值是在一定範圍內的一系列常數。
    • 如:一周內七天、三原色(紅、黃、藍)、方向(上、下、左、右)

      範例一

  • 數字列舉
  • 列舉成員會被賦值為從0,開始遞增
1
2
3
4
5
6
7
8
9
10
11
// 數字列舉,一个數字列舉可以用 enum 这个關鍵詞來定義,我们定義一系列的方向,然后这里面的值,列舉成员会被赋值为從 0 开始遞增的數字,
enum Direction {
Up,
Down,
Left,
Right,
}
console.log(Direction.Up) //0
// 還有一个神奇的點是這個列舉还做了反向映射
console.log(Direction[0]) //up

  • 可以手動賦予值
  • 未手動賦值的列舉項會接著上一個列舉項遞增。
1
2
3
4
5
6
7
8
9
10

Up = 10,
Down,
Left,
Right,
}
console.log(Direction.Down) // 11 ,後面的項目數值會遞增

console.log(Direction[0]) //up

範例二

1
2
3
4
5
6
7
8
9
10
11
// 字符串列舉
enum Direction {
Up = 'UP',
Down = 'DOWN',
Left = 'LEFT',
Right = 'RIGHT',
}
const value = 'UP'
if (value === Direction.Up) {
console.log('go up!')
}

範例三

  • 常數列舉
    • 編譯後的邏輯變少了
    • 使用常數列舉會內連列舉的用法並且不會將設定的列舉編譯成 JS 程式碼
1
2
3
4
5
6
7
8
9
10
const  enum Direction {
Up = 'UP',
Down = 'DOWN',
Left = 'LEFT',
Right = 'RIGHT',
}
const value = 'UP'
if (value === Direction.Up) {
console.log('go up!')
}


泛型 Generics

  • 要解決什麼問題
    • 建立函式 echo 參數為 arg ,接著宣告變數傳入參數
    • 但其中的型別沒有設定
    • 傳入和返回的型別無法統一
1
2
3
4
5
function echo(arg) {
return arg
}
const result = echo(123)
// 這時候我們發現了一個問題,我们傳入了數字,但是返回了 any

  • 當在建構 function、internet及Class 時,你會希望這些component都能被重複運用的 Generic(泛型)提供了一個彈性的作法。
  • 語法是: <T>

範例一

  • 泛型(Generics)是指在定義function、internet及Class的时候,不预先指定具體的型別,而在使用的时候再指定型別的一種特性。
1
2
3
4
5
function echo<T>(arg: T): T {
return arg
}

const result = echo(123)

範例二 傳入多個

1
2
3
4
5
6
function swap<T, U>(tuple: [T, U]): [U, T] {
return [tuple[1], tuple[0]]
}

const result = swap(['string', 123])

泛型第二部分 - 约束泛型

  • 帶有「限制」的泛型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function echoWithArr<T>(arg: T): T {
console.log(arg.length)
return arg
}

// 上例中,泛型 T 不一定包含属性 length,我们可以给他傳入任意型別,當然有些不包括 length 属性,那样就會報錯

// 我們使用了 extends 約束了泛型 T 必須符合介面 IWithLength 的形狀,也就是必須包含 length 屬性。
interface IWithLength {
length: number;
}
function echoWithLength<T extends IWithLength>(arg: T): T {
console.log(arg.length)
return arg
}

echoWithLength('str')
const result3 = echoWithLength({length: 10})
const result4 = echoWithLength([1, 2, 3])

//此時如果呼叫 loggingIdentity 的時候,傳入的 arg 不包含 length,那麼在編譯階段就會報錯了
echoWithLength(7);
// index.ts(10,17): error TS2345: Argument of type '7' is not assignable to parameter of type 'Lengthwise'


泛型第三部分 - 泛型在 class 和 interface 中的使用

  1. 泛型在 class 的使用
  • 在第一個程式中存在一个问题,它允許你向 Queue 中添加任何型別的數據,當然,當數據被彈出隊列时,也可以是任意類型
  • 在下方的範例中,看起来可以向隊列中添加 string 型別的數據,但是那麼在使用的過程中,就會出現無法捕捉的錯誤
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Queue {
private data = [];
push(item) {
return this.data.push(item)
}
pop() {
return this.data.shift()
}
}

const queue = new Queue()
queue.push(1)
queue.push('str')
console.log(queue.pop().toFixed())
console.log(queue.pop().toFixed())


1
2
3
4
5
6
7
8
9
10
11
12
class Queue<T> {
private data = [];
push(item: T) {
return this.data.push(item)
}
pop(): T {
return this.data.shift()
}
}
const queue = new Queue<number>()


  1. 泛型在 interface 中的使用
  • 之前提過可以使用介面的方式來定義一個函式需要符合的形狀
  • 當然也可以使用含有泛型的介面來定義函式的形狀:
1
2
3
4
5
6
7
8
9
10
11
//泛型和 interface
interface KeyPair<T, U> {
key: T;
value: U;
}

let kp1: KeyPair<number, string> = { key: 1, value: "str"}
let kp2: KeyPair<string, number> = { key: "str", value: 123}

let arr:number[] = [1,2,3];
let arrTwo:Array<number> = [1,2,3]

interface 搭配泛型之后,可以靈活的返回不同的型別

  • 創建一个拥有特定型別的容器,class 和 泛型 仿佛给一个容器貼上標籤一样

  • 泛型就好像一个可變的參數,在用的时候傳入,生成这個不同型別的一个容器,

  • 上個部分的用它来靈活的约束参数的型別,不需要參數是一个特别死板的型別,不希望他是一个特定 string、number 型別,我要傳入的参數必须有某某屬性、某某方法,否則就會報錯。

  • 在函式使用的时候,函式的这个型別推斷,不會進入到函式中,所以使用表達式,没法明確建立型別的绑定,用泛型可以让我们打破這個鴻溝,這個时候就可以返回它傳入的類型。

    Type Aliases

  • 就是给型別起一個别名,讓它可以更方便的被重用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let sum: (x: number, y: number) => number
const result = sum(1,2)
type PlusType = (x: number, y: number) => number
let sum2: PlusType

// 支持聯合
type StrOrNumber = string | number
let result2: StrOrNumber = '123'
result2 = 123 //沒有錯

// 字符串字面量 ,類似在列舉的常數變量
type Directions = 'Up' | 'Down' | 'Left' | 'Right'
// 使toWhere 這個變量為 Direction 類型
let toWhere: Directions = 'Up'

  • 我們使用 type 定了一個字串字面量型別 EventNames,它只能取三種字串中的一種。
    注意,型別別名與字串字面量型別都是使用 type 進行定義。
1
2
3
4
5
6
7
8
9
type EventNames = 'click' | 'scroll' | 'mousemove';
function handleEvent(ele: Element, event: EventNames) {
// do something
}

handleEvent(document.getElementById('hello'), 'scroll'); // 沒問題
handleEvent(document.getElementById('world'), 'dbclick'); // 報錯,event 不能為 'dbclick'

// index.ts(7,47): error TS2345: Argument of type '"dbclick"' is not assignable to parameter of type 'EventNames'.

Intersection Types

  • 使用 & 符號
  • 經過上面的 type 使 IName 就有了 name 和 age兩個屬性在裡面
    • 和interface 的 extends 有點類似,都是為了實現物件形狀組合和擴展
1
2
3
4
5
6
interface IName  {
name: string
}
type IPerson = IName & { age: number }
// 經過上面的 type 使 IName 就有了 name 和 age兩個屬性在裡面
let person: IPerson = { name: 'hello', age: 12}

什麼时候用介面,什麼时候用 Type Aliases:

  • interface 是 docker typing 的實現方式,是一種獨特類型,和extends class implememts 有關的用interface ,
  • 和交叉,聯合型別的有關的用 Type Aliases;

內建物件

DOM 和 BOM 的內建物件

DOM 和 BOM 提供的內建物件有:
Document、HTMLElement、Event、NodeList 等。
TypeScript 中會經常用到這些型別:

1
2
3
4
5
let body: HTMLElement = document.body;
let allDiv: NodeList = document.querySelectorAll('div');
document.addEventListener('click', function(e: MouseEvent) {
// Do something
});

ECMAScript 的內建物件

  • Boolean、Error、Date、RegExp 等。
1
2
3
4
let b: Boolean = new Boolean(1);
let e: Error = new Error('Error occurred');
let d: Date = new Date();
let r: RegExp = /[a-z]/;

TypeScript 內置

Utility Types

官方
Typescript 還提供了一些功能性,帮助性的型別,這些型別,大家在 JS 的世界是看不到的,這些型別叫做 utility types,提供一些簡潔明快而且非常方便的功能。

  • 使用 Partial
  • Omit
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// partial,它可以把傳入的型別都變成可選
interface IPerson {
name: string
age: number
}

let viking: IPerson = { name: 'viking', age: 20 }
type IPartial = Partial<IPerson>
let viking2: IPartial = { } //可以都不傳,也不會報錯

// Omit,它返回的型別可以忽略傳入型別的某个屬性
// 以下範例 將 name 忽略掉
type IOmit = Omit<IPerson, 'name'>
let viking3: IOmit = { age: 20 }

資料來源:

  1. 線上課程:實戰 Vue3.0(正式版) + TS
  2. TypeScript 新手指南

因為工作需求,有需多缺乏的技能需要補齊,TypeScript 就是其中一項。
上班看不懂的時候邊查詢,空閒時間再來上課不足不懂的知識點,但還是得透過文章的整理來好好消化一下!
既然要開始介紹 TypeScript 就先來說說使用它的原因吧。

為什麼使用 TypeScript

  • 程式語言中,有區分強型別和弱型別兩種類型。

  • 而 JavaScript 是弱型別語言。

  • 在 JavaScript 中,他會自己判斷應該執行的型別。自行做[型別自動轉換]

  • 在快速開發時,很有可能引發一些預期之外的錯誤,相當不利於多人協同開發。

    • 優點:寫法彈性。
    • 缺點:無法在開發時期檢查型別。
  • 依據上述的問題,所以使用 TypeScript

  1. 使程式碼更容易理解 (如函式需傳入的參數可以明確設定型別)
  2. 減少程式碼撰寫的錯誤
  3. 可以完全兼容JavaScript,此外能與 ES6 完美結合,並提供更多支援寫法。

安裝介紹

  • 就是直接到官方網站查看,依需求安裝

npm install -g typescript

  • 檢查版本
    tsc -v

型別介紹

從 JavaScript 型別分類開始

字串 string

  • 和ES6一樣,可以使用[字串樣板 Template Literals]
    1
    2
    3
    let firstName:string = 'NaNa'
    let message: string = `Hello,${firstName}`

    布林值 boolean

布林值是最基礎的資料型別,在 TypeScript 中,使用 boolean 定義布林值型別:

1
let isDonw:boolean = false 

注意:使用建構函式 Boolean 建立的物件不是布林值:

  • 事實上 new Boolean() 返回的是一個 Boolean 物件
    1
    2
    3
    4
    let createdByNewBoolean: boolean = new Boolean(1);

    // Type 'Boolean' is not assignable to type 'boolean'.
    // 'boolean' is a primitive, but 'Boolean' is a wrapper object. Prefer using 'boolean' when possible.
    1
    在 TypeScript 中,boolean 是 JavaScript 中的基本型別,而 Boolean 是 JavaScript 中的建構函式。

數值 number

1
2
3
let age:number = 10
let notANumber: number = NaN;
let infinityNumber: number = Infinity;

Null 和 Undefined

  • 與 void 的區別是,undefined 和 null 是所有型別的子型別
    • unfefined 可以賦值給number類型的變數
    • 陣列被赋值為 undefined 或 null 不会报错
    • let num:number = undefined
1
2
3
4
5
6
let u: undefined = undefined;
let n: null = null;

// 這樣也不會報錯
let u: undefined;
let num: number = u;

void

  • 通常用在當函式沒有回傳值時。
  • 而 void 型別的變數不能賦值給 number 型別的變數:
    1
    2
    3
    4
    let u: void;
    let num: number = u;

    // Type 'void' is not assignable to type 'number'.

    any

  • 在任意值上任何属性都是允許的
    • 當有明確型別時,應避免使用 any
    • 因為 any 可以任意調用方法和屬性,很有可能出現錯誤(就喪失類型檢查的意義)
      如果是 any 型別,則允許被賦值為任意型別
1
2
let myFavoriteNumber: any = 'seven';
myFavoriteNumber = 7;

未宣告型別的變數

變數如果在宣告的時候,未指定其型別,那麼它會被識別為任意值型別:

1
2
3
4
5
let something;
something = 'seven';
something = 7;

something.setName('Tom');

陣列 Array 與 元組 Tuple

陣列

  1. 在想宣告的型別後面加上[],宣告為某個型別的Array
    1
    let idList:number[] = [1,2,3]
  2. 使用陣列泛型,Array <元素型別>
    1
    let list:Array<number> = [1,2,3]
  • 注意,當已經宣告這是個數字陣列
    • 要 push 字串進去,會出現錯誤
      1
      2
      let list:Array<number> = [1,2,3]
      list.push("4")

      元組 Tuple

    • 剛剛的陣列將同一類型的數據放在一起,但是,想加入不同型別的數據怎麼辦?
    • 表示方式與陣列相似,將型別寫在[]
    • 有固定長度和元素型別的陣列
      1
      2
      3
      4
      // 表示方式和陣列非常相似,只不过它将型別寫在了里面 這時會對每一項產生了限定的作用
      let user: [string, number] = ['viking', 20]
      //但是当我们少寫一項 就會報錯,同樣多寫也會報錯
      user = ['molly', 20, true]

元組

物件的型別——介面 (Interface)

  • Interface 可以用來定義物件,還有由物件所延伸的型別(例如,陣列、函式)
  • age?: number; 在該屬性後面加上 ? 表示為可選屬性 ,也就是在宣告新的物件時,可以彈性加入或不加入age (也不會報錯)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 我们定義了一個介面 Person
interface Person {
name: string;
age: number;
}
// 接着定义了一个變數 NaNa,它的型別是 Person。
//这样,我们就约束了 NaNa 的形狀必須和介面 Person 一致。
let NaNa: Person ={
name: 'NaNa',
age: 20
}

//有时我们希望不要完全匹配一个形狀,那么可以用可選屬性:
interface Person {
name: string;
age?: number;
}
let NaNa: Person = {
name: 'NaNa'
}


唯讀屬性

  • readonly 是用在屬性上面
  • 希望物件中的一些欄位只能在建立的時候被賦值,那麼可以用 readonly 定義唯讀屬性
1
2
3
4
5
6
7
8
9
10
11
12
13
interface Person {
readonly id: number;
name: string;
age?: number;
}
// 建立物件
let Vic: Person ={
id:1,
name: 'Vic',
age: 20
}
//不能去修改id
Vic.id = 9527

函式型別

  • 函式可以作為參數、可以存入陣列,可以被另外一個函式返回、可以被賦值另外一個變數
  • 函式是由兩部分構成,一個是輸入(通過不同參數來實現),二為輸出(就是函數的返回結果)

    例子一

  • 設定參數型別、返回的型別
  • 若參數設定了 x、y兩個,也就只能放入兩個
    1
    2
    3
    4
    // 来到我们的第一个例子,约定输入,约定输出
    function add(x: number, y: number): number {
    return x + y
    }

例子二

  • 也可以設定可選參數
  • 在 z? 表示 z 可有可無
1
2
3
4
5
6
7
8
// 可选参数
function add(x: number, y: number, z?: number): number {
if (typeof z === 'number') {
return x + y + z
} else {
return x + y
}
}
  • 而在可選參數後面不可以再添加確定參數

例外說明:引數預設值

  • 在 ES6 中,我們允許給函式的引數新增預設值,TypeScript 會將添加了預設值的引數識別為可選引數:
1
2
3
4
5
function buildName(firstName: string, lastName: string = 'Cat') {
return firstName + ' ' + lastName;
}
let tomcat = buildName('Tom', 'Cat');
let tom = buildName('Tom'); // 因為 lastName 添加了預設值,識別為可選引數

此時就不受「可選引數必須接在必需引數後面」的限制了:

1
2
3
4
5
function buildName(firstName: string = 'Tom', lastName: string) {
return firstName + ' ' + lastName;
}
let tomcat = buildName('Tom', 'Cat');
let cat = buildName(undefined, 'Cat');

例子三

  • 函式的表達式
1
2
3
4
5
6
7
8
9
10
11
const add = (x: number, y: number, z?: number): number => {
if (typeof z === 'number') {
return x + y + z
} else {
return x + y
}
}

// 函式本身的类型
const add2: (x: number, y: number, z?:number) => number = add

  • 函式不只輸入、輸出有類型,本身也有類型
  • 將 add2 賦予 string 會出錯誤
  • 所以須聲明一模一樣的 const add2: (x: number, y: number, z?:number) => number = add

例子四

  • 使用 interface (單純定義函式的 Interface)
1
2
3
4
5
6
7
8
// interface 描述函数类型
const sum = (x: number, y: number) => {
return x + y
}
interface ISum {
(x: number, y: number): number
}
const sum2: ISum = sum
  • 另一個範例
1
2
3
4
5
6
7
8
interface SearchFunc {
(source: string, subString: string): boolean;
}

let mySearch: SearchFunc;
mySearch = function(source: string, subString: string) {
return source.search(subString) !== -1;
}

型別推論(Type Inference)\ 聯合型別(Union Types) \ 型別斷言(Type Assertion)

型別推論(Type Inference)

  • 可以推論出我們賦值過程中,這個變數應該是什麼類型的
  • TS 會在沒有明確指定型別的時候,推測出型別

聯合型別(Union Types)

  • 用分隔符號
  • 當 TypeScript 不确定一个聯合型別的變數到底是哪個型別的时候,我们只能使用此聯合型別的所有型別裡共有的屬性或方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 允許 numberOrString 的型別是 string 或者 number,但是不能是其他型別。
let numberOrString: number | string
// 而在使用此聯合型別的所有型別里共有的屬性性或方法:
numberOrString.length //會報錯 (只有 string 適用)
numberOrString.toString() //string 和 number 的共同屬性是沒問題

//若放在函式定義型別
function getLength(something: string | number): number {
return something.length;
}

// index.ts(2,22): error TS2339: Property 'length' does not exist on type 'string | number'.
// Property 'length' does not exist on type 'number'.

  • 聯合型別的變數在被賦值的時候,會根據型別推論的規則推斷出一個型別:
1
2
3
4
5
6
let numberOrString: string | number;
numberOrString = 'seven';
console.log(numberOrString.length); // 5
numberOrString = 7;
console.log(numberOrString.length); // 編譯時報錯
// index.ts(5,30): error TS2339: Property 'length' does not exist on type 'number'.

上例中,第二行的 numberOrString 被推斷成了 string,存取它的 length 屬性不會報錯。
而第四行的 numberOrString 被推斷成了 number,存取它的 length 屬性時就報錯了。

  • type guard
  • 當遇到聯合類型,可以使用條件語句,自動幫你縮小型別範圍
1
2
3
4
5
6
7
8
// typescript 在不同的条件分支里面,智能的缩小了范围,这样我们代码出错的几率就大大的降低了。
function getLength2(input: string | number): number {
if (typeof input === 'string') {
return input.length
} else {
return input.toString().length
}
}

型別斷言(Type Assertion)

  • 開發者比 TS 更了解編寫的程式碼。因此,TS 允許開發者覆蓋它的推論,這樣的機制稱為「型別斷言」。
1
2
3
4
5
6
7
8
9
10
// 这里我们可以用 as 關鍵字,告诉typescript 这里我把它看作是一個 string,你可以给他用 string 的方法。
function getLength(input: string | number): number {
const str = input as string
if (str.length) {
return str.length
} else {
const number = input as number
return number.toString().length
}
}
  • 型別斷言不是型別轉換,斷言成一個聯合型別中不存在的型別是不允許的:
1
2
3
4
5
6
7
function toBoolean(something: string | number): boolean {
return <boolean>something;
}

// index.ts(2,10): error TS2352: Type 'string | number' cannot be converted to type 'boolean'.
// Type 'number' is not comparable to type 'boolean'.

基礎型別先介紹到這裡,下篇將介紹一些進階用法~

資料來源:

  1. 線上課程:實戰 Vue3.0(正式版) + TS
  2. TypeScript 新手指南

在使用 Nuxt.js 開啟專案後,會自動生成不少文件檔案,今天就來介紹幾樣基礎設定以及檔案的運用吧!

了解 Nuxt 路徑引用規則

  • ~以及@ :根目錄
    • (注意:nuxt.conf.js、非 nuxt 結構的 js 檔 除外)
  • 波浪或小老鼠 :從根目錄 搜尋assets 資料夾

    以下也一樣
    import test from "@/assets/js/test.js"
  1. ./:自己的目錄
  • nuxt.conf.js、以及非 nuxt 結構要這樣撰寫
  1. 放在 static 資料夾裡的引用方式(直接斜線)<img src="/demo.png" alt="">
  2. 補充:
    2.4 nuxt 圖片處理 - webpack 相關設定
  • 在asset 加入 img 資料夾(將圖片放在這裡)
    <img src="@/assets/img/demo.png" alt="">

  • 放在css作背景圖
    *background: url(~assets/img/demo.png); //nuxt css 引入圖片不能加斜線

    • 也不能將波浪改為@
      :::info
      原本波浪斜線方式:
      background: url(~/assets/img/demo.png);
      :::

nuxt.config.js 設定

  • vue cli3 設定vue 專案是透過是 main.js/ vue.config.js 這兩隻檔案。
    • main.js 為程式的進入點
  • nuxt 則統一用 nuxt.conf.js 做為設定。
    • 要編輯這份文件達到專案需求
    • 如 wedpack 是放在 build

  • 此外在此檔案可以做標題、引入 CDN 等基礎設置
    • head,meta 可以做表頭的設定
    • script 可以引入 JS 的檔案或 CDN 資源
  • css 全域設定,也同樣放在這裡
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
export default {
// Global page headers: https://go.nuxtjs.dev/config-head
head: {
title: 'myNuxt',
htmlAttrs: {
lang: 'en'
},
meta: [
{ charset: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
{ hid: 'description', name: 'description', content: '' },
{ name: 'format-detection', content: '網站前後端、網頁設計、程式語言網站' }
],
link: [
{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }
],
script:
[
{ src: "/jquery.min.js"},
{ src: "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.11.2/js/all.js" }
]

},

引用 static 靜態資源的檔案

  • static資料夾:靜態資源資料夾
    • 其中要連接該檔案路徑要用斜線
    • 這裡面的檔案不會被打包和優化

套件安裝!

  1. 最簡單方式是使用打包後的 CDN 或 js 檔案
    • 在head 的 script 放入 CDN
  2. 使用 nuxt plugins 自包 js 套件 或 vue 套件 → 較麻煩
  3. 使用 nuxt modules 來引入套件
    • 套件較少
  • nuxt module 主要是將 plugin 在封裝一層出來。(就是 nuxt 專有的套件),如果對安裝 plugins 沒把握,就找有 nuxt modules 的套件來安裝。

練習1包裝一般的 vue 套件

vuejs-datepicker

1、npm install vuejs-datepicker

  • 若直接依照官網來安裝,並在檔案引入會發生錯誤
  • 所以 install 完之後要再執行下面動作
    2、plugins 新增一個檔案:datepicker.js → 撰寫套件
  • 在 plugins 資料夾,新增檔案
    1
    2
    3
    4
    5
    import Vue from 'vue'
    import Datepicker from 'vuejs-datepicker';

    Vue.component('Datepicker', Datepicker)

3、nuxt.config.js 安裝 plugin → 可以設定該套件只在 客戶端 運作

  • 可以將插件安裝在客戶端或是 server 端
    1
    { src: '~/plugins/datepicker.js', mode: 'client' },
  • 要使用時,再放入頁面

    練習2 安裝一般的 js 套件

    GSAP
  1. npm install gsap
  2. plugins 新增一個檔案:gsap.js → 撰寫套件
    1
    2
    3
    4
    5
    import Vue from 'vue'
    import gsap from "gsap";

    Vue.prototype.$gsap = gsap

  3. nuxt.config.js 安裝 plugin
    1
    2
    3
    4
    plugins: [
    { src: '~/plugins/gsap.js', mode: 'client' },
    ],

  • create() 會在前端執行\後端執行

練習3 nuxt 安裝

axios modules

  1. npm install @nuxtjs/axios
  2. plugins 新增一個檔案:axios.js
  • 如同前面的練習,到 nuxt.config.js
    1
    2
    3
    4
    modules: [
    '@nuxtjs/axios',
    ],

製作網站,都會希望自己的網站可以被搜尋引擎找得到,並且可以在使用者輸入關鍵字後,可以在頭幾個項目中就能關注到自己的網站!
而在使用 Vue.js 框架,因為是使用 SPA 架構無法做到 SEO (search engine optimization) 的效果,別人無法輕易的找到我們所製作的網站。
今天要介紹的 Nuxt.js 是一個基於 Vue.js、用來處理伺服器渲染(Server-side rendering(SSR))的框架。透過 SSR 我們可以做到更好的 SEO 效果。

本文首要任務: 認識 SPA 與 SSR 的差異

SPA (Single Page Application) 架構

  • 讓一些後端的工作,分擔到前端 (如: router 設定)
  • 流程為:使用者開了瀏覽器 -> 向後端server要資料,因為改為SPA 架構,所以提供的會是SPA 首頁(會載入JS\編譯好的CSS\一頁空殼的HTML網頁),此空殼經過 Vue 編譯產出 DOM 物件,讓瀏覽器印出。
  • 讓回應速度更快,使用者在轉換頁面時得到更好的體驗。
    • SPA(Single Page Application)意思是僅有一個頁面的應用程式,也就是說網頁不需跳轉頁面就可以達到基本的建立、讀取、修改、刪除資料功能。
  • 後端負責調整API 邏輯\架構

    SPA 缺點

  • SEO差(SPA 一開始吐的是空殼),GOOGLE 爬不到頁面資訊
  • SMO差,FB爬不到頁面資訊
  • 首屏速度慢,一開始畫面會空白或閃爍
    • 發出請求資訊到實際上呈現完整頁面時間

SSR (Server Side Rendering)

流程為:使用者開了瀏覽器 -> 向後端server要資料,因為改為SSR,所以會提供一整個頁面(不會是畫面空白閃爍)

  • 希望頁面可以將網頁整個東西呈現,並讓搜尋引擎查得到
  • 處理 SEO(針對搜尋引擎)
  • title
  • meta tag
    • 全域 <head> 設定
    • 單頁 <head> 設定
  • description
  • 處理 SMO (針對 Social media)
    • twitter\youtube\Line…

      有更多的彈性設定

      Nuxt 有三種模式,可以幫你解決問題

  1. Universal: SSR + CSR
  • 全域設定 mode: ‘Universal’
  1. SPA: 不跑SSR
  • 全域設定 mode: ‘spa’ 或執行 nuxt-spa
  1. Static Generated (Pre Rendering): 產生靜態頁面
  • nuxt run generate
    • 預先將頁面轉成靜態頁面
    • 如要拉10樣商品,就先拉10個頁面

希望本篇的扼要介紹能夠讓讀者快速知道其中的差異。

參考資料:
SSR — Nuxt.js 超入門
Vue Nuxt 介紹與實作範例

之前的實作練習都沒有機會使用到 Vuex,總是覺得它的資料存取、取得或是管理,對於剛接觸Vue 的我來說還是相對複雜,而且剛開始接觸 Vue 總要先練習下 props \ emit 資料傳遞才行啊~
既然已經會使用基礎的資料傳遞後,那就要近一步實作 Vuex 狀態管理

實作功能說明

  • 資料的取得來自 JSONPlaceholder
    • 從Vuex來實作資料的取得、刪除、更新
  • 針對目前取得的資料進行筆數的篩選
  • 區分未完成事項、以及完成事項
  • 可透過點擊方塊改變色塊
  • 接下來~會製作 3個 component ,將元件放到 App.vue

成品

開始前別忘了先建立 module

Vuex 檔案的處理

  • 這裡開專案直接有建立 vuex
  • 進入 store > index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { createStore } from 'vuex';
import todos from './modules/todos';


export default createStore({
state: {
},
mutations: {
},
actions: {
},
modules: {
todos,
}
})

建立 modules 資料夾

  • store > modules > todo.js
    • 從這裡的資料取得並傳遞到 Todos.vue
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      import axios from 'axios';

      const state = {};
      const getters = {};
      const actions = {};
      const mutations = {};

      export default {
      state,
      getters,
      actions,
      mutations
      }

      接下來要嘗試來串取資料

  • 取得假資料
    jsonplaceholder-todo

    在 todo.js 來製作資料的串接

  • 首先state這裡要先建立空陣列,getters 則是建立資料取得的函式
  • 在action 來發出請求,取得回應,並傳給 mutations
    • mutations 是改變資料的地方
  • 引入axios,使用 Asyc await 使用 get 來串接資料
    • 將串到的資料 commit 到 mutations,改變原本todos資料內容
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import axios from 'axios';

const state = {
todos: []
};
const getters = {
getTodos: (state) => state.todos,
};
const actions = {
async fetchTodos({ commit }) {
const res = await axios.get("https://jsonplaceholder.typicode.com/todos");
// console.log(res.data);
commit('setTodos', res.data);
}
};
const mutations = {
setTodos: (state, todos) => {
state.todos = todos;
}
};

export default {
state,
getters,
actions,
mutations
}

建立 Todos.vue

  • 此元件主要是呈現所有的 Todo list

    取得 vuex 中的資料

  • 引入 mapGetters, mapActions

  • 將要從 getters 拿到的陣列,放到computed做資料的取得

    • 將該陣列getTodos 放到 v-for
  • 串接的 action 使用 mapActions,於 methods中執行,而此函式要在created階段運行

  • 如此一來就可以,從 Vuex 中得到資料

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<template>
<h3>Todos</h3>
<div class="todos">
<div class="todo" v-for="todo in getTodos" :key="todo.id">
{{ todo.title }}
</div>
</div>
</template>

<script>
import { mapGetters, mapActions } from "vuex";
export default {
name: "Todos",
methods:{
...mapActions(["fetchTodos"])
},
computed: mapGetters(["getTodos"]),
created() {
this.fetchTodos();
},
};
</script>

製作加入新 todo 功能

  • 建立一個新的 component => AddTodo.vue
  • 在 AddTodo 這個元件,製作 輸入框
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    <template>
    <div>
    <h3>Add Todo</h3>
    <div class="add">
    <form>
    <input type="text" placeholder="Add Todo..." v-model="title" />
    <input type="submit" value="Submit" />
    </form>
    </div>
    </div>
    </template>
    <script>
    export default {
    name: "AddTodo",
    };
    </script>

    到 module > todo.js

  • 使用 async 與 axios 傳入資料
    • 第二個參數為 title
    • post 的網址一樣,後面接的參數為title, complete: false (any new todo that it’s not going to completed)
1
2
3
4
5
6
7
8
9
10
11
12
const actions = {
async fetchTodos({ commit }) {
const res = await axios.get("https://jsonplaceholder.typicode.com/todos");
// console.log(res.data);
commit('setTodos', res.data);
},
async addTodo({ commit }, title) {
const res = await axios.post("https://jsonplaceholder.typicode.com/todos", { title, completed: false });
commit('addNewTodo', res.data);
}

};
  • 而在action 中使用axios 來新增資料,要將這必新資料 push 到 todo的陣列中
    • 存入的該資料要放在,陣列的最前面所以使用 unshift
      1
      2
      3
      4
      5
      6
      7
      8
      const mutations = {
      setTodos: (state, todos) => {
      state.todos = todos;
      },
      addNewTodo: (state, todo) => {
      state.todos.unshift(todo);
      }
      };

到 AddTodo.vue 來呼叫此 action

  • import mapActions
1
2
3
4
5
6
7
8
9
10
11
<template>
<div>
<h3>Add Todo</h3>
<div class="add">
<form @submit="onSubmit">
<input type="text" placeholder="Add Todo..." v-model="title" />
<input type="submit" value="Submit" />
</form>
</div>
</div>
</template>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

<script>
import { mapActions } from "vuex";
export default {
name: "AddTodo",
data() {
return {
title: "",
};
},
method: {
...mapActions(["addTodo"]),
onSubmit(e) {
e.preventDefault();
//點擊後呼叫action中的加入todo函式,並傳入title資料
this.addTodo(this.title);
},
},
};
</script>


這裡要特別提出說明

因為是使用線上的虛擬資料,他不允許我們可以存入新資料到他的API裡面
所以可以看到下方的錯誤顯示(重複 id)
而此在重新整理之後,存入的資料會消失


製作刪除功能

  • 以id作為參數,來依此刪除
  • 不用儲存response資料
    • 在 async deleteTodo ,製作 by id 刪除
    • 到 mutations => 使用 filter 存下資料
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import axios from 'axios';

const state = {
todos: []
};
const getters = {
getTodos: (state) => state.todos,
};
const actions = {
async fetchTodos({ commit }) {
const res = await axios.get("https://jsonplaceholder.typicode.com/todos");
// console.log(res.data);
commit('setTodos', res.data);
},
async addTodo({ commit }, title) {
const res = await axios.post("https://jsonplaceholder.typicode.com/todos", { title, completed: false });
commit('addNewTodo', res.data);
},
async deleteTodo({ commit }, id) {
await axios.delete(`https://jsonplaceholder.typicode.com/todos/${id}`);
commit('removeTodo', id)
}

};
const mutations = {
setTodos: (state, todos) => {
state.todos = todos;
},
addNewTodo: (state, todo) => {
state.todos.unshift(todo);
},
removeTodo: (state, id) => {
state.todos = state.todos.filter((todo) => todo.id !== id);
//使用filter 將,不是id的篩選出來
}
};

export default {
state,
getters,
actions,
mutations
}

  • 回到 Todos.vue
  • 在 template 放入垃圾桶的 icon
    • 執行刪除的功能,在 icon 綁定刪除事件deleteTodo()
    • deleteTodo()就是來自於 methods 取得的事件
      1
      2
      3
      4
      5
      6
      7
      8
      9
      <template>
      <h3>Todos</h3>
      <div class="todos">
      <div class="todo" v-for="todo in getTodos" :key="todo.id">
      {{ todo.title }}
      <i class="fas fa-trash-alt" @click="deleteTodo(todo.id)"></i>
      </div>
      </div>
      </template>
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      <script>
      import { mapGetters, mapActions } from "vuex";
      export default {
      name: "Todos",
      methods: {
      ...mapActions(["fetchTodos", "deleteTodo"]),
      },
      computed: mapGetters(["getTodos"]),
      created() {
      this.fetchTodos();
      },
      };
      </script>


製作篩選器功能

  • 當 FilterTodo.vue 這裡的篩選器執行的時候,會執行 action中的函式,進行篩選
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    <style scoped>
    select {
    margin-top: 20px;
    padding: 6px;
    border: #ef9a9a 1px solid;
    }
    </style>
    <template>
    <div>
    Filter todos:
    <select>
    <option value="200">200</option>
    <option value="100">100</option>
    <option value="50">50</option>
    <option value="30">30</option>
    <option value="20">20</option>
    <option value="10">10</option>
    </select>
    </div>
    </template>
    1
    2
    3
    4
    5
    <script>
    export default {
    name: "FilterTodos",
    };
    </script>

將該元件綁到 App.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<template>
<div id="app">
<div class="container">
<AddTodo />
<FilterTodos />
<Todos />
</div>
</div>
<!-- <router-view /> -->
</template>

<script>

import Todos from "@/components/Todos.vue";
import AddTodo from "@/components/AddTodo.vue";
import FilterTodos from "@/components/FilterTodos.vue";

export default {
name: "app",
components: {
Todos,
AddTodo,
FilterTodos,
},

};
</script>

到 module > todo.js 來製作 actions

  • 要傳入 option 的 value
    • 第二個參數為事件
    • 在 axios 的 get 放入 url,接上取得的參數
    • commit 直接傳到 setTodos ,將串接到的資料透過 mutations 存到 todos
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      import axios from 'axios';

      const state = {
      todos: []
      };
      const getters = {
      getTodos: (state) => state.todos,
      };
      const actions = {

      async filterTodos({ commit }, e) {
      //取得下拉選單的值
      //console.log(e.target.value);
      let selected = parseInt(e.target.value);
      const res = await axios.get(`https://jsonplaceholder.typicode.com/todos?_limit=${selected}`);
      commit('setTodos', res.data);
      }

      };
      const mutations = {
      setTodos: (state, todos) => {
      state.todos = todos;
      },
      addNewTodo: (state, todo) => {
      state.todos.unshift(todo);
      },
      removeTodo: (state, id) => {
      state.todos = state.todos.filter((todo) => todo.id !== id);
      //使用filter 將,不是id的篩選出來
      }
      };

      export default {
      state,
      getters,
      actions,
      mutations
      }

  • 將action 綁到 FilterTodos.vue
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25

    <template>
    <div>
    Filter todos:
    <select @change="filterTodos($event)">
    <option value="200">200</option>
    <option value="100">100</option>
    <option value="50">50</option>
    <option value="30">30</option>
    <option value="20">20</option>
    <option value="10">10</option>
    </select>
    </div>
    </template>

    <script>
    import { mapActions } from "vuex";
    export default {
    name: "FilterTodos",
    methods: {
    ...mapActions(["filterTodos"]),
    },
    };
    </script>


todos 完成與否,狀態的改變

  • 點擊兩下來更改狀態
    • 當要更新資料,要使用put,傳入updTodo物件資料
      • 將該資料傳到mutation
    • 接著到 mutation 製作更新的動作
    • 要取得目前的index
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import axios from 'axios';

const state = {
todos: []
};
const getters = {
getTodos: (state) => state.todos,
};
const actions = {

async updataTodo({ commit }, updTodo) {
const res = await axios.put(`https://jsonplaceholder.typicode.com/todos/${updTodo.id}`,updTodo);
commit('renewTodo', res.data);
},

};
const mutations = {

renewTodo: (state, updTodo) => {
console.log(updTodo);
//我們是要在同一筆資料上更新
const index = state.todos.findIndex(todo => todo.id === updTodo.id);
//以下簡單確認 indx 是否有存在
if (index !== -1) {
state.todos.splice(index, 1, updTodo);
}
}
};

export default {
state,
getters,
actions,
mutations
}

回到 Todos.vue

  • template 插入legend
  • 引入 updataTodo到 methods
  • onDblclick()建立一個變數,它包含的物件內容為id、title、completed,其中我只要要改變的值是 complete true\false
    • 這裡的 id,title 都一樣。complete 則設定狀態的改變
    • 最後在呼叫一次 updataTodo() 傳入新的物件
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      <template>
      <h3>Todos</h3>
      <div class="legend">
      <span>雙重點擊來標示已完成的事項</span>
      <span> <span class="incomplete-box"></span> = 未完成 </span>
      <span> <span class="complete-box"></span> = 已完成 </span>
      </div>
      <div class="todos">
      <div
      @dblclick="onDblclick(todo)"
      class="todo"
      v-for="todo in getTodos"
      :key="todo.id"
      :class="{ 'is-complete': todo.completed }"
      >
      {{ todo.title }}
      <i class="fas fa-trash-alt" @click="deleteTodo(todo.id)"></i>
      </div>
      </div>
      </template>

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25

      <script>
      import { mapGetters, mapActions } from "vuex";
      export default {
      name: "Todos",
      methods: {
      ...mapActions(["fetchTodos", "deleteTodo", "updataTodo"]),
      onDblclick(todo) {
      const updTodo = {
      id: todo.id,
      title: todo.title,
      completed: !todo.completed,
      };
      this.updataTodo(updTodo);
      },
      },
      computed: mapGetters(["getTodos"]),
      created() {
      this.fetchTodos();
      },
      };
      </script>



參考資料:
[Vue.js] Vuex 學習筆記 (5) - getters 的核心概念
Vuex
Using this.$store.dispatch vs using mapActions

試想今天有一個網頁,上方的導覽列有不同的分類,帶領你到該項目的分頁中
那我們該如何實現,將各個項目的內容一一呈現

簡單分別製作 component

首先:component的命名要用大寫

  • 在 component 中
  • 在 App.vue
    讓vue知道有個component可以被使用,透過ES6的解構放入

    將引入的component放到template

將其他組件引入

補充說明:

修改路徑:
用~@來指定圖片路徑:@在vuecli是代表特殊意義,就會直接從src這個資料夾底下開始查找;而波浪符號,代表現在在css之中使用這個功能

  • 也就是在專案中,即便路徑複雜,只要在路徑最前面加上@就會直接進去src