網頁設計原則

按鈕尺寸

行動裝置的使用者增加,除了考量在電腦上顯示的網站內容,也須同時考量到觸控介面的操作可能性。因此,必須留意螢幕中的可觸控範圍,如果按鈕太小,很可能觸控的手指會遮擋住按鈕,也就無法確認按鈕位置。

參考 Google 跟 Apple 制訂的設計規範,建議觸控區域最小尺寸為 44 像素,銀幕上的 44 像素約略等同於紙張上的 10 mm。44 像素是指用於 CSS 編排的尺寸。若是使用圖片作為按鈕,則建議需 88 像素 以上。

除了要考量按鈕的尺寸大小,也要保持適當間距,避免過於相鄰的按鈕,減少誤觸的機率。

使用 RGB 色彩

銀幕是使用色彩光來表現色彩,因此在網頁設計中,指定顏色必須要用 RGB 設定。相較於印刷上常用的 CMYK,在螢幕上使用 RGB 的顏色會比較亮。

在網頁的世界可能會看到 「#FFFFFF」的色碼,這是把 RGB 轉換為 16 進位,使用了「0~9」、「A~E」共 16 個字來表示。字母越大、越接近 F,也就是 RGB 中月亮的顏色。

設計網站的建議尺寸

早前店腦銀幕史以稱為方形螢幕的 4:3 為主流;現在則是以 16:9 或 16:10 為主流,網頁寬度也越來越寬。知名網站的寬度通常都是設定在 960~1200 像素左右的寬度。因此建議以 1000 像素為基準,最大 1200 像素。

智慧型手機的尺寸多變,以主流的 Android 手機最寬頂多 480 像素,因此建議智慧型手機的銀幕畫面低於 480 像素。

螢幕解析度(畫素),與 CSS 設定的尺寸像素並不同。螢幕解析畫素跟終端裝置本身的「裝置像素比(device pixel ratio)」有關。以 iPhone5為例,裝置的像素比是 2 ,這是指「用 2 個畫素才能表現 CSS 的 1 像素」。因此,雖然 iPhone5 的實際解析度是 640,但在 CSS 版型中,會變成一半的 320 像素。同樣地,GALAXY S5 的裝置像素比是 3,相對於解析度 1080 像素的 CSS 版型是 1/3 的 360 像素。不過,圖片是以解析度為準來製作。

解析度

過去網頁設計的解析度都是以電腦為準的 72 dpi 為主,但是近幾年的行動裝置陸續出現解析度更高的裝置。若裝置使用筆72 dpi 高1.33~3倍的解析度,設計的圖片素材也需要準備對應的高解析度檔案。

舉例來說,Apple 旗下 Mac 跟 iPhone 等裝置使用了 Retina 高解析螢幕,就需要設定 2 倍以上的解析度素材。

要輸出 2 倍或 3 倍的圖片時,建議比照「image@2x.png」及「image@3x.png」的命名方式。

文字大小

太小的文字除了不易閱讀,有些瀏覽器也會限制最低字級大消,例如Chrome 瀏覽器中的最低字級為 10 像素,若設定小於 10 像素的文字,在畫面上依然會想是 10 像素的大小。

若有特殊狀況需要更小的字體,可以使用 CSS 的 transform 屬性強制縮小。

1
2
transform:scale(0.8);
transform:scale(0.6);

參考資料來源

  • 網頁設計鐵則&問題對策-北村崇・淺野 櫻 著

TypeScript-(一)基礎知識

解決 JavaScript 的問題點

JavaScript 本身的設計具有彈性,可是卻也成了開發的問題根源。

  1. 過於自由的型別

    多數其他的程式語言屬於「靜態型別」,也就是說,在撰寫程式碼、宣告變數的同時決定型別。

    JavaScript 採取「動態型別」,宣告變數時無須定義型別,不論存入什麼樣型別的值都不會受到限制,也沒有任何檢查機制。可是這就會導致,在實際執行到的時候,可能出現型態不符合才丟出錯誤的狀況。

    此外,由於 JavaScript 具有型別轉換(Cast)的功能,在型別稍有不符,但不至於差異太多的情況,會自動轉換變數型別讓程式碼能正常運行,可是這就會提高在撰寫程式碼時「不留意型別」造成的意外錯誤。

  2. 欠缺變數領域

    ES6之前的 JavaScript 變數,僅能指定為全域變數或、函式內的區域變數,無法更細緻的規範只能在某個物件內中作用。

  3. 特殊的物件導向

    多數程式語言的物件導向是「基於類別(Class Based)」。JavaScript 的物件導向相當特殊,屬於少見的「基於原型(Prototype Based)」。

TypeScript 問世

為了修補上述缺點,而針對 JavaScript 進行擴充,這便誕生出新的語言稱作 AltJS(Alternative JavaScript),其中最知名的就是 TypeScript。

它的使用方式是,需要先撰寫 TypeScript 的程式碼,然後再編譯成 JavaScript 的程式供瀏覽器解讀、運作。

TypeScript 的特點

  1. 採用靜態型別

    在撰寫程式碼、宣告變數時,就必須要指定變數的型態,限制變數可以接受值的型別。藉由型別導入,也擴展相關功能,如:泛型。

  2. 導入基於類別(Class Based)的物件導向

    TypeScript 可以宣告物件的類別,隨著類別的導入,也能使用類別的繼承或介面(Interface)等正規物件導向的功能。

如何安裝導入 TypeScript

撰寫完的 TypeScript 原始碼,必須先經過編譯,因此需要使用「編譯程式(Compiler)」對原始碼進行編譯工作,並取得 JavaScript 程式碼。

使用 npm 指令進行安裝

1
npm install -g typescript

TypeScript 的編譯程式準備了名為「tcp」的命令指令, 可以簡單輸入指令,並針對欲編譯的檔案名稱進行編譯

1
tcp 檔案名稱.ts

執行編譯命令之後,就會在相同的位置建立相同檔案名稱的 js 檔案,這就是 JavaScipt 的程式碼檔案了。

TypeScript-(二)基本語法

值與變數

變數的型別

所有的值與變數都需要決定其型別,以下是 TypeScript 的幾種基本資料型別

  • 布林值(boolean):跟 JavaScript 一樣,是 true 或 false 這兩者之中期一的值。
  • 數值(number):所有的數值都被歸類為 number 的型別,並沒有分整數或浮點數。
  • 字串(string):前後以引號圈住的文字內容,都是為 string 型別。

變數的宣告

在變數的後方使用「冒號」,接著再輸入型別,這樣的變數就只能存入指定型別的值。

1
2
var 變數:型別;
var 變數:零別 = 值;

而實際上,這個「指定型別」的宣告動作並非必須,如果還是按照 JavaScript 的寫法「var 變數 = 值;」也不會出錯影響程式碼運作。可是既然都已經導入 TypeScript,不使用它「指定型別」的優點,就失去了原有的用意。

偶爾可能會需要「可以接受任何型別的變數」,可以使用「any」型別來宣告變數。

1
2
var 變數:any;
var 變數:零別 = 值;

型別的運作

在 TypeScript 原始碼中,在宣告變數時無論有沒有指定型別,在編譯後的 JavaScript 程式碼基本市長一樣的。那麼「指定型別的意義」是什麼?

最主要是,如果有指定型別,那在編譯過程中會去檢查變數的值是否符合型別,如果型別不符就會出現錯誤無法完成編譯動作。而通過編譯的程式碼,就可以確保最後產生的程式碼不會發生「存入不同型別」造成的問題。

var、let 與 const

一般變數宣吿 var

以 var 進行宣告,除了在函式內進行宣告以外,皆被視為全域變數,函式內的變數則只能在函式範圍內使用。

另外,var 可以針對同一個變數名稱進行重複宣告,如下:

1
2
var a:any = 1;
var a:any = "hello";

可是要注意一點,當 var 變數有指定型別,再次宣告時就不能改變為其他型別,如下案例會發生錯誤:

1
2
var a:number = 1;
var a:string = "hello"; //錯誤

區域變數宣吿 let

以 let 進行宣告,只能在一個 block 中被使用,脫離該範圍就無法使用這個變數。

且,不能重複宣告,如下:

1
2
let a:any = 1;
let a:any = "hello";//錯誤

如果要重新賦值變數,只需要用等號指派新的值,如下:

1
2
let a:any = 1;
any = "hello";

常數宣告 const

以 const 進行宣告,後續將無法改變其儲存的內容值。

1
2
const a:any = 1;
any = "hello";//錯誤

另外,const 跟 let 一樣具備 block 的性質,如果脫離的範圍就無法使用這個變數。

陣列的宣告

在零別後面加上中括號,即代表這個變數是陣列。但是要注意的是,TypeScript 跟 JavaScript的陣列不同,在TypeScript中的陣列「不可以存入不同型別的內容值」。

1
var 變數:型別[] = [...陣列內容值...];

型別的別名

如果變數命名很明確,就很容易理解當中儲存的型別,如下:

1
2
let email:string;
let phone:string;

但是如果遇到變數命名曖昧不明的狀況,就比較難理解變數儲存的內容,如下:

1
2
let a:string;
let b:string;

因此 TypeScript 可以對型別設定其別名(Alias),設定方法如下:

1
type 別名 = 型別;

因此就可以在宣告變數之前,先設定不同的 string 別名,之後在宣告變數的型別時,以別名取代 string 型別做指定。如下方案例,就可以更加容易解讀程式碼。

1
2
3
4
type email = string;
type phone = string;
let a:email;
let b:phone;

列舉型別(enum)

TypeScript 增加了 JavaScript 中沒有的新型別,最有特色的就是「列舉型別」,只能預先存入固定數量內容值的型別,建立後無法再存入其他值,它能把變數在某些限制下進行存取並賦予其定義。

1
enum 型別名稱 {值1, 值2, ...};

先使用自訂名稱的列舉型別,之後就可以跟普通型別的使用方式一樣,如下範例:

1
2
3
4
5
6
7
8
9
10
enum Season {spring, summer, autumn, winter};

let s:Season = Season.summer;

if (s == Season.spring) {
console.log("It's spring!");
}

console.log(Season[0]);//印出 "spring"
console.log(Week["spring"]); //印出 0

「s:Season」的 Session 被視為一種型別,此 Season 型別的內容值需要以「型別名稱.值」的方式來表達。

Tuple 型別

Tuple 是多個值的集合,可以想像成「需要逐一指定每個內容值型別的陣列」。

1
2
let 變數:[型別1, 型別2, ...];
變數 = [值1, 值2, ...];

如下範例:

1
2
3
4
5
6
7
type name = string;
type phone = string;
enum gender {male,female};
type age = number;

let member:[name, phone, gender, age];
member = ["Amy", "09123456789",gender.female,20];

將 Tuple 型別跟陣列做結合,以下更進一步案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
type name = string;
type phone = string;
enum gender {male,female};
type age = number;

type member = [name, phone, gender, age];

let data:member[] = [];
data.push("Amy", "09123456789",gender.female,20]);
data.push("Peter", "0987654321",gender.male,18]);

console.log(data[0]);//印出 Amy, 09123456789,1,20
console.log(data[1]);//印出 Peter, 0987654321,0,18

TypeScript-(三)函數

函數的定義方式

指定型態

TypeScript 跟 JavaScript 的函數的宣告語法很相似,但是 TypeScript 可以指定「傳入參數的型別」以及「回傳值型別」。

1
2
3
function 函數名稱(參數名稱:型別,...):回傳值型別{
...處理內容...
}

在小括號中一樣可以傳入多個參數並以逗點分割,但是每個參數都可以使用分號指定型別;接著後面用分號指定回傳值型別。

以下範例:

1
2
3
function calc(price:number,count:number):string{
return "Total:"+price*count;
}

如果函數沒有回傳值,則可以指定回傳值為「void」,如下:

1
2
3
function click():void{
...
}

選擇性參數

有些情況,函數中的部分參數並非必傳值,而是開放性的 option,因此可以用「?」符號來代表它是可以忽略的參數,而當沒有傳入該參數時,在函數中其會以賦予「undefined」值。

1
2
3
4
5
6
7
8
9
10
11
12
13
function calc(price:number,tax?:number):number{
let tx = 0;
if(tax){
tx = tax;
}else{
tx = 0.05;
}

return Math.floor(price*(1+tx));
}

console.log(calc(100));//印出105
console.log(calc(100,0.1));//印出110

多載(Overload)

在撰寫一個函數功能時,可能會發生該函數在不同情況會有「相異型別的傳入參數」或「相異型別的回傳值」,可是在 JavaScript 中,「函數名稱」就不能重複宣告,如下方的情況:

1
2
3
4
5
6
function convert(name,index){
// ...
}
function convert(age,index){//merge重複宣告,覆蓋前一個merge函數
// ...
}

而透過 TypeScript,當參數或回傳值的型別不同時,則可以同時接受多個同名函數。

不過並非直接寫入多個同名函數,而是鮮血函數的宣告敘述,在彙整函數的處理內容,如下語法範例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function convert(info:number):boolean;
function convert(info:string):string;
function convert(info:string,note:string):string;
function convert(info:boolean):string;

function convert(info:any,note?:string):any{
switch(typeof info){
case number:
return info>18?true:false;
case string:
return info+(note?("-"+note):"");
case boolean:
return info?"yes":"no";
}
}

泛型(Generics)

當函數可以接受各種狀況的參數及回傳值時,我們可以使用any作為型別,不過有個問題是我們無法確認傳入與回傳的型別為何。

1
2
3
function identity(arg: any): any {
return arg;
}

假設我需要確保傳入跟傳出的型別相同,就可以使用泛型。使用<>符號自定義一個「型別變數」,作為取得的參數的型別,如下範例:

1
2
3
function identity<T>(arg:T):T{
return arg;
}

上述的 T 為型別變數名稱,用來儲存參數的型別。在函數執行時,辨識傳入參數 arg 的型別並賦予值給 T。

在呼叫使用函數時,可以如一般情況只傳入參數,編譯器會自動辨識傳入參數的型別用以定義型別變數 T ;或者也可以同時使用<>傳入型別變數。兩種情況如下範例:

1
2
3
4
5
//只傳入參數
console.log(identity(123));//印出 number 型別的123

//傳入參數、型別變數
console.log(identity<string>("hello"));//印出 string 型別的hello

可變數量參數

函數中的參數量若不固定,TypeScript 支援「可變量參數」,讓函數可以接受型別相同但數量不定的資料。

1
2
3
function 函數名稱(...參數:型別){
...
}

在參數前方加上「…」,則這個參數變數就會被視為可變量的,不論傳入多少個參數,只要是同型別,都能被函數處理。如下範例:

1
2
3
4
5
6
7
8
9
10
function sum(...item:number[]):number{
let total:number = 0;
for(let i:number=0 ; i<item.length ; i++){
total = total + item[i];
}
return total;
}

console.log(sum(1,2,3,4,5)); //印出 15
console.log(sum(1,2,3,4,5,6,7,8,9,10)); //印出 55

可以看到 ...item:number[],這邊就適當所有參數傳入函數中,會被當作一個number型別的陣列做處理,並且存為一個名為 item 的變數中。

箭頭函式(Arrow Function)

除了使用 function 關鍵語法定義函式,TypeScript 也提供更簡易的箭頭函式,目前也已經被ES6所採用。

1
2
3
(參數1,參數2,...)=>{
...執行內容...;
}

箭頭函式最主要的優點是「方便把整段函式當作一個資料值來使用」,而如果最終執行結果只有一行,且是一個回傳值,可以省去執行內容的大括號,以下舉例:

1
2
3
4
5
6
7
8
9
(n)=>{
return n*n;
}

//更簡化
(n)=>n*n;

//指定參數型別
(n:number)=>n*n;

如果想宣告一個變數,定且存入整段函數,同時去指定參數的型別及回傳值的型別,寫法如下:

1
let func:(number) => number = (n:number)=> n*n;

上方案例中,先宣告一個變數 func,冒號後方指定其型別為(number)=>number,意思是指定參數的型別為numebr、回傳值的型別指定為number;最後使等號存入值為 (n:number)=>n*n 這樣僅有一行的內容。

也因為箭頭函式簡略的特性,最常被使用的地方就是「將箭頭函式當作函式中的參數」,如下範例:

1
2
3
4
5
6
7
8
9
10
11
function print(n:number,func:(number)=>string){
let re:number = func(n);
return re;
}


let aFunc:(number)=>string = (n:number)=>"<面積>"+(n*n);
let bFunc:(number)=>string = (n:number)=>"<體積>"+(n*n*n);

console.log(print(5,aFunc));
console.log(print(5,bFunc));

TypeScript-(四)類別形式的物件導向

在 TypeScript 中採用了「類別形式的物件導向」,需要先定義「類別(Class)」,再根據類別產生實體(Instance),建立出實體的物件才能夠被操作使用。

1
2
3
4
5
6
7
//定義類別
class 類別名稱{
...類別內容...
}

//產生實體
var 變數名稱:類別名稱 = new 類別名稱(參數);

類別的內容

在定義類別的時候,就可以先規劃其包含的內容,主要分為三大項:

  1. 建構子(Constructor):用來做初始化的動作的特出方法,在建立實體物件時,會執行這個方法
  2. 屬性(Attribute):物件內用來儲存資料值的變數
  3. 方法(Method):物件中的函式功能

以下範例:

  • 首先定義一個名為 Person 的類別
  • Person 類別中有兩個屬性:name、age
  • Person 類別中有一個建構子(Constructor),當這個類別被 new 出來的時候,會傳入兩個參數:n、a,並執行建構子方法中的初始化的動作
  • Person 類別中有一個方法:say
  • 使用 「new」 關鍵字建立出實體,並且指派給 member 這個變數
  • member 可以被稱作實體物件,就可以去操作其中的屬性、方法。例如用 member.print() 呼叫 print 方法
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    class Person{
    //屬性
    name:string;
    age:numebr;

    //建構子
    constructor(n:string,a:number){
    this.name = n;
    this.age = a;
    }

    //方法
    say():string{
    let msg:string = "Hi! I'm " + this.name;
    return msg;
    }
    }

    //new 出實體
    let member:Person = new Person("Amy",20);
    console.log(member.print());//印出 Hi! I'm Amy

可以看到在建構子中或是方法中要拿取類別中的屬性,會使用「this.xxx」的格式來取用,例如:this.namethis.age

存取修飾詞(Access Modifier)

類別中的屬性或方法,基本上都可以從外部進行存取,而產生實體物件之後,也可以對於它的屬性跟方法進行改寫。

然而,如果沒有去限制存取權限,就可能會從不恰當的地方進行存取,導致錯誤而無法正常執行程式碼。

因此,TypeScript 中使用「存取修飾詞」,用來「限制屬性或方法可以接受從哪些範圍進行存、取的動作」。

簡單來說,public 修飾詞級即代表可以從外部取用該資源;而 private 只能接受內部的存取。

存取修飾詞的使用方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
class Person {
public name:string;
private age:number;

constructor(n:string,y:number){
// ...略
}

print():string{
// ...略
}
}

存取子(Accessor)

當類別中的屬性為敏感資料時,不適合隨意從外部直接存取,因此會將該屬性設定為 private,當我們需要去存取這個屬性時,就需要使用「存取子」的功能,透過它去控制屬性的存取動作。

存取子基本上是以方法的形式來進行定義,不過他跟一般的方法又有些不同M在開頭需要冠上「get」跟「set」關鍵字。

get:純粹用來「取得」屬性值,因此不能傳入參數
set:存粹用來「設定」屬性值,因此不能有回傳值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Person{
public name:string;
private age:number;

get ageGetter():number{
return this.age;
}

set ageSetter(year:number){
this.age = year<0 ? 0 : year;
}

constructor(n:string,year:number){
this.name = n;
this.age = year;
}

print():string{
// ...略
}
}

類別的繼承(extends)

繼承正如其名稱,讓新建立的類別可以直接去繼承既有類別中的所有屬性跟方法,透過「繼承」就可以不用重複賺些多個類似的類別功能。

1
2
3
class 新類別名稱 extends 既有類別名稱{
// ...略
}

透過這種「繼承」機制所創建出來的新類別會被稱作既有類別的「子類別(Sub-class)」,而反過來說,作為繼承來源的既有類別則會稱作新類別的「超類別(Super-class)或稱作父類別」。

在子類別中要呼叫執行父類別的建構子,可以使用 super 方法。

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
class Person{
public name:string;
private age:number;

constructor(n:string,year:numebr){
this.name = n;
this.age = year;
}

print():string{
let msg:string = "My name is "+this.name;
return msg;
}
}

class Student extends Person{
public grade:number;

constructor(n:string,year:number,g:number){
super(n,year);
this.grade = g;
}

printAll():string{
let msg:string = "My name is "+this.name+". And, I'm in "+this.grade+" grade.";
return msg;
}
}

方法覆寫(Override)

假設子類別中想要使用跟父類別中同名的方法,這稱作「覆寫(override)」,當子類別的實體呼叫該方法時,程式會優先執行子類別中的方法,而不會去執行父類別中的同名方法,因為父類別中的方法已經被子類別的方法所取代。

類別屬性與類別方法

上述內容中,都強調在類別中的屬性跟方法,都必須要「先宣告出實體物件,才能透過實體物件去呼叫」。不過,其實也可以寫成「非隸屬於實體物件的資源」。

想要指定這些資源並非放在個別的實體,而是隸屬於類別身,就需要用「static」修飾詞。如果在屬性或方法宣告時加上 static,那麼該屬性或方法就會被配置在類別的層級,這稱作為「類別屬性」跟「類別方法」,他們無法從個別實體中被呼叫,需要直接從類別別呼叫,也就是使用「類別名稱.屬性名稱」這樣的語法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Person{
public name:string;
public age:numebr;
public static checked = false;

constructor(n:string,y:number){
this.name = m;
this.age = y;
}
}

function doClick():void{
Person.checked = true;
}

介面(Interface)

有時候在分工開發時,首位開發者創造了多個類別,而後續接手的開發者無法確定各個類別中的必要屬性跟方法為何,因此可以透過 「介面」來提醒其他開發者,該個類別中必定具備哪些屬性跟方法。

介面可以預先彙整類別中所需要的屬性跟方法,但是不一定要先撰寫完畢其中的內容。

定義介面的語法:

1
2
3
4
interface 介面名稱{
// ...屬性...;
//...方法...;
}

使用介面的語法:

1
2
3
class 類別名 implements 介面{
// ...略
}

這樣參考某個介面的類別,就必須以覆寫的方式去宣告這些介面中的屬性跟方法,如果漏掉了某些項目,就無法被成功編譯。

簡而言之,介面的用意在於「強制要求類別中必須要實作介面中的屬性跟方法」。

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
interface Man{
name:string;
age:number;

print():string;
}

class Person inplements Man{
public name :string;
public age :number;

constructor(n:string,y:number){
this.name = n;
this.age = y;
}

print():string{
let msg = "My name is "+ this.name;
return msg;
}
}

class Student implements man{
public name :string;
public age :number;
public grade:number;

constructor(n:string,y:number,g:numebr){
this.name = n;
this.age = y;
this.grade = g;
}

print():string{
let msg:string = "My name is "+this.name+". And, I'm in "+this.grade+" grade.";
return msg;
}
}

var list:Man[] = [];
list.push(new Person("Amy",30));
list.push(new Student("Peter",20,2));

上述程式碼中,使用「list:Man[]」的方式指定此陣列中的型別需要是 Man 型別,而 Person 跟 Student 都是透過 Man 介面所時做出來的類別,因此在 list 陣列中放入 Person 跟 Student 時做出來的實體物件是被接受的。

React-(五-2)組件進階:組件的父子溝通

父叫子做事

  • 第 4 行,在父層組件中引入子組件,並且在第 27 行使用子組件
  • 第 7 行,使用 createRef() 來創建一個ref,並且在第 27 行給子組件使用
  • 第 20 行,點擊觸發第 12 行 addChild() 函式時,則去拿取該 ref 並觸發定義子組件中的 addCount()函式

    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
    //Parent.js

    import React,{Component,createRef} from 'react';
    import Child from './Child'

    class Parent extends Component{
    childRef = createRef();
    state = {
    count:0
    }
    addChild = ()=>{
    this.childRef.current.addCount();
    }
    render(){
    return (
    <div>
    <h3>Parent:{this.state.count}</h3>
    <button onClick={this.addChild}>+Child +1</button>

    <Child ref={this.childRef}/>
    </div>
    )
    }
    }

    export default Parent;
  • 第 10 行,子組件中的 addCount()函式會被觸發

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // Child.js

    import React,{Component} from 'react';

    class Child extends Component{
    state = {
    count:0
    }

    addCount = ()=>{
    this.setState({
    count:this.state.count + 1,
    })
    }
    }

    export default Child;

子叫父做事

  • 第 11 行,先定義好動作,也就是從子層觸發父層去執行的函式
  • 第 21 行,把函式當作一個 props 傳給子層。當子層觸發 addParent ,就會在父層執行 addCount

    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
    // Parent.js

    import React,{Component} from 'react';
    import Child from './Child'

    class Parent extends Component{
    state = {
    count:0
    }

    addCount = () => {
    this.setState({
    count:this.state.count + 1,
    })
    }
    render(){
    return (
    <div>
    <h3>Parent:{this.state.count}</h3>

    <Child addParent={this.addCount}/>
    </div>
    )
    }
    }

    export default Parent;
  • 第 14 行,用 props 綁定 addparent

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    // Child.js

    import React,{Component} from 'react';

    class Child extends Component{
    state = {
    count:0
    }

    render(){
    return (
    <div>
    <h3>Child:{this.state.count}</h3>
    <button onClick={this.props.addParent}>Parent +1</button>
    </div>
    )
    }
    }

    export default Child;

父叫子做事(方法二)

父對子溝通,除了用 ref 溝通,另一種狀況是子層沒有自己的 state(剝奪子層的自主權)。

  • 第 8 行,增加一個變數 childCount 作為子層的 count 資料
  • 第 27 行,透過 props 傳入屬性跟方法給子層

    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
    //Parents.js

    import Child from "./child";

    class parent extends Component{
    state = {
    count:"",
    childCount:""
    }
    addParent = () =>{
    this.setState({
    count:this.state.count + 1
    })
    }
    addChild = () => {
    this.setState({
    childCount:this.state.childCount + 1
    })
    }
    render(){
    return(
    <div>
    <h3>Parent:{this.state.count}</h3>
    </div>
    <button onClick={this.adParent}>+Parent</button>
    <button onCLick={this.addChild}>+Child</button>
    <Child count={this.state.childCount} addParent={this.addParent} addChild={this.addChild}></Child>
    )
    }
    }
  • 第 4 行,先解構上層傳來的 props,作為子層的屬性及方法

  • 第 10, 11 行,就可以把解構出來的屬性或方法給子層顯示、使用
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // Child.js

    class Child extends Component{
    const {count,addParent,addChild} = this.props;

    render(){

    return(
    <div>
    <h3>Child:{count}</h3>
    <button onClick={adParent}>+Parent</button>
    <button onCLick={addChild}>+Child</button>
    </div>
    )
    }
    }

【Debug筆記】利用pointer-events:none解決點擊事件失效

在button 內層包了一個 svg 圖片,而在 button 上面綁定 click 事件,可是當點按的範圍在 svg 區域內時,click 事件無法被正常觸發。

原因是因為 svg 蓋在 button 上方時,但滑鼠點擊時,上方的 svg 層遮蓋住,導致無法點到下層 button 。

1
2
3
svg {
pointer-events: none;
}

解決方法是,透過 CSS3 的新屬性 pointer-events,讓點擊事件忽略上層 svg,如此一來就可點到下層 button 區塊。

React-(五-4)三種組件

(一) Class Component

透過繼承 React 的 Component,所創建出來的組件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Progress.js
import React,{Component} from "react";

class Progress extends Component{
render(){
const {value} = this.props;
return (
<div className="bar-outer">
<div
className="bar-inner"
style={{width:`${value}%`}}/>
<span>{value}%</span>
</div>
)
}
}

export default Progress;

(二) Functional Component

相較於 Class Component,在Functional Component 中,props 是當作一個參數傳進函式中

使用情境: 當這個 Component 中沒有使用到 state ,只有 render 函式而沒有其他自定義方法

因此,Functional Component 又稱作 Stateless Component

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import React from "react";

const ProgressBar = (props) =>{
const {value} = props;
return (
<div className="bar-outer">
<div
className="bar-inner"
style={{width:`${value}%`}}/>
<span>{value}%</span>
</div>
)
}

export default ProgressBar;

(三) Pure Component

Pure ComponentClass Component 很相似,最主要是差在效能上。

只要 props 裡面的值沒有改變,Pure Component 就不會重新 render

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//ProgressBar.js
//Class Component:
import React,{PureComponent} from "react";

class ProgressBar extends PureComponent{
state:{
count {value} = this.props;
};
render(){
const {value} = this.props;
console.log("Re-render Progress")
return (
...略
)
}
}

此外,Pure Component 也會去比對自己的 state,在每一次決定是否要重新 render,是採取 shallow compare 的方式是去比對,也就是僅針對 state 中的第一層值是否改變

所以在下方程式碼中,即使每次重複點按 button 觸發 add(),卻因為 count 沒有被改變值,且第一層的 people 也沒有被改變,所以並不會重新 render

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
//Progress.js
import React,{PureComponent} from "react";

class Progress extends PureComponent{
state:{
count:0,
people:{
age:10
},
};
add:()=>{
this.setState({
count:0,
people:this.state.people
});
};
render(){
const {value} = this.props;
console.log("Re-render Progress")
return (
<div>
<button onClick={this.add}></button>
</div>
)
}
}

即使把第一層的 people 拿出來,重新setState,也第9行的 people 跟第4行的 people 是同一個 people,所以也不會重新render

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// ...略
class Progress extends PureComponent{
state:{
people:{
age:10
},
};
add:()=>{
const {people} = this.state;
people.age += 1;
this.setState({
people:people,
});
};
render(){
const {value} = this.props;
console.log("Re-render Progress")
return (
<div>
<button onClick={this.add}></button>
</div>
)
}
}

但是如果把上述案例都改為繼承 Component 而非 PureComponent,不論值 props 跟 state 中的值是否改變,都會重新 render

React-(五-3)組建進階:動態控制樣式className跟style

直接不 render

假設想要讓圖片動態顯示、隱藏,第一種方法是設定一個型態為布林值的變數,依據值的 true 或 false,搭配三元運算式決定是否要 render 出這個元素。

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
//Main.js

import React,{Component} from "react";
import picture from "./image/test.png";

class MainPage extends Component{
state = {
visable:true,
};
toggle = () =>{
this.setState({
visable: !this.state.visable,
});
};
render(){
const {visable} = this.state;
return (
<div>
<button onClick={this.toggle}>Toggle</button>
<hr/>
{visable?<img src={picture}/>:null}
</div>
)
}
}

export default MainPage;

又或者,用稍微更簡潔的方式取代三元判斷式

1
2
3
4
5
//三元判斷式
{visable ? <img src={picture}> : null}

//更簡化一些
{visable && <img src={picture}>}

在上述兩個做法中,都是採取直接不 render 出 image 的方式,也就是說當 visable 為 false 時,在 DOM 元素中是直接沒有畫出這個 image。

如果情況是一定要 render 出這個 image,那就可以採用改變 scc 樣式來決定顯示、隱藏,因此就必須動態綁定 style。

動態綁定 style

補:在 JSX 中,style 是一個物件

在 image 上面綁定 style 屬性,而 style 屬性綁定一個物件,在這個物件中就可以定義 display 樣式,樣式一樣採用三元判斷是的做法。

特別注意,在第 10 行中的連續兩個大括號,第一個外面的大括號代表 JSX 綁定值的表示,第二個裡面的大括號代表它是一個物件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//Main.js
// ...略
render(){
const {visable} = this.state;
return (
<div>
<button onClick={this.toggle}>Toggle</button>
<hr/>
<img
style={{display:visable?'block':'none'}}
src={picture}/>
</div>
)
}

也可以直接將 style 屬性綁定的物件,直接在外面宣告成一個物件型態的變數 pictureObj

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//Main.js
// ...略
render(){
const {visable} = this.state;
const pictureObj = {
display:visable?'block':'none'
}
return (
<div>
<button onClick={this.toggle}>Toggle</button>
<hr/>
<img
style={pictureObj}
src={picture}/>
</div>
)
}

查看 DOM 元素的變化,可以看到顯示跟隱藏時,image 都有被 render 出來,只是他的樣式 display 改變了。

動態綁定 className

先在 css 檔案中定義好 class 的樣式

1
2
3
4
5
6
7
/* main.css */
.image{
display:block;
}
.hide{
display:none;
}

第 4 行,引入 css 檔案
第 17 行,宣告一個型態為字串的變數,並將該變數當作第 22 行 className 屬性綁定的值

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
//Main.js
import React,{Component} from "react";
import picture from "./image/test.png";
import "./style/main.css";

class MainPage extends Component{
state = {
visable:true,
};
toggle = () =>{
this.setState({
visable: !this.state.visable,
});
};
render(){
const {visable} = this.state;
const pictureClass = visable ?"image":"image hide"
return (
<div>
<button onClick={this.toggle}>Toggle</button>
<hr/>
<img className={pictureClass} src={picture}/>
</div>
)
}
}

export default MainPage;

因為有重複的 class ,也可以改用字串模板的方式

1
const pictureClass = `image ${visable?'':hide}`

React-(五-1)組件進階:ref屬性找到DOM

ref 三種用法

ㄧ.傳入字串(官方未來不支援)

補充:componentDidMount是第一次加載到這個組件時,會呼叫的生命週期函式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//Input.vue

// import... 略
class Inputer extends Component{
componentDidMount(){
this.refs.msgInput.focus();
}
render(){
return(
<h3>Input Your message</h3>
<input type="text" ref="msgInput"/>
)
}
}

執行結果

二. 傳入函式

  • 第 7 行,函式的參數 tag 就是呼叫該函式的 DOM 元素,也就是第 13 行的input
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//inputer.js
// import... 略
class Inputer extends Component{
componentDidMount(){
this.msgInput.focus();
}
setMsgInputRef = (tag)=>{
this.msgInput = tag;
}
render(){
return(
<h3>Input Your message</h3>
<input type="text" ref="this.setMsgInputRef"/>
)
}
}

也可以直接在函式中去執行 focus()

1
2
3
4
5
6
7
8
9
10
11
12
13
//inputer.js
// import... 略
class Inputer extends Component{
setMsgInputRef = (tag) => {
tag.focus();
}
render(){
return(
<h3>Input Your message</h3>
<input type="text" ref="this.setMsgInputRef"/>
)
}
}

三. 使用 createRef(官方推薦作法)

  • 第 3 行,引入 React 的 createRef 方法
  • 第 6 行,將 createRef 執行過後的結果回傳值指定給一個變數
  • 接著在生命週期函式中,第 9 行,記得要加上 .current 拿取到 ref
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    //inputer.js

    import React, { Component, createRef} from 'react';

    class Inputer extends Component {
    msgInput = createRef();

    componentDidMount(){
    this.msgInput.current.focus();
    }

    render(){
    return (
    <div>
    <h3>Enter your message</h3>
    <input type="text" ref={this.msgInput}/>
    </div>
    )
    }
    }

    export default Inputer;
© 2020 Leah's Blog All Rights Reserved. 本站访客数人次 本站总访问量
Theme by hiero