# 參考資源 純 Javascript 教學 https://mtache.com/javascript#section7 詳細 Javascript 教學 https://www.fooish.com/javascript/ Javascript / Vue 教學 https://developer.mozilla.org/zh-TW/docs/Learn/Getting_started_with_the_web nodejs 教學 https://summer10920.github.io/2020/12-30/article-nodejs/#%E5%85%A5%E9%96%80%E6%93%8D%E4%BD%9C React 詳細教學 https://israynotarray.com/categories/react/page/3/ React 學習路徑 https://codelove.tw/@tony/post/gqB053 React 框架 Next.js 教學 https://ithelp.ithome.com.tw/articles/10265138 Next.js 官方教學 https://nextjs.tw/learn/foundations/from-react-to-nextjs/getting-started-with-nextjs # 基於 nodejs 的 Javascript 語法教學 * 本教學是建立在 nodejs 平台上的 * 原生的 Javascript 需要透過瀏覽器間接執行,而 nodejs 則讓 Javascript 能像其他語言一樣,直接在本地執行並在終端機輸出結果 ## 輸出 * 安裝好 nodejs 的執行環境後,使用"node + Javascript 檔案路徑",便可執行該 Javascript 檔案 * 每個 statement 的後面記得加上分號 * console.log() 指的是將訊息發到控制台;在 nodejs 的平台上,則會顯示於終端機上 ```javascript console.log("Hello World!"); //輸出 Hello World! console.log("Jason!"); //輸出 Jason! ``` ![image](https://hackmd.io/_uploads/ByX6PTXKa.png) ## 變數 * 共有三種不同的宣告方式,可以分別使用 var、let、const 宣告 * var 是函式作用域 (宣告後,在宣告時所處的整個函式中有效),let / const 是**區塊作用域** (宣告後,只在宣告時所在的 {} 中有效) * var 可重複宣告,let / const **不可重複宣告** (迴圈中的重複宣告除外) * Javascript 的變數可以存放任意的資料型態,且變數**跟 python 一樣是動態型別**的,可以隨時轉換某個變數中存放的資料型態 * Javascript 的資料型態可以為 * number: 存放整數、小數等等 * 遵循 IEEE 754 標準 * 範圍同 C++ double * 安全範圍在 $±2^{53}$ 間 (此範圍的精度是絕對準確的) * string: 存放字串 * boolean: 存放 bool * null: 代表這個變數是空的 * undefined: 代表這個變數宣告了,但完全沒賦值 * symbol: 表示 unique 的值 (之後會詳細敘述) * array: 表示陣列 (複合資料型態) * object: (複合資料型態) ```javascript // 不同的宣告方式 // 使用 var 宣告變數(不推薦) var x = 10; // 使用 let 宣告變數 let y = 20; // 使用 const 宣告常數 const PI = 3.14; // 不同的資料型態 //number let a = 10; let b = 3.14; //string let c = "Hello"; //boolean let d = true; //null let e = null; //undefined let f; //symbol let g = Symbol(); //印出變數的資料型態 console.log(typeof a); //輸出 number ``` ## 字串 * Javascript 中的字串可以使用 "" 或 '' 來包住,型別都是string * 不可以像C++一樣直接改字串內的某個位置 ```javascript //可以用單引號或雙引號來表示字串 let s1 = "Hello World!"; let s2 = 'Jason'; console.log(s1 + s2); //字串相加 (輸出 Hello World!Jason) console.log(s1[1]); //可以存取特定位置的字元 (輸出 e) // s1[1] = 'a'; //字串是不可變的 (s1[1] 仍然是 e,修改無效) console.log(s1.length); //取得字串的長度 (輸出 12) //string 與 number 之間的轉換 let x = 10; let y = "20"; x= String(x); //數字轉字串 y= Number(y); //字串轉數字 console.log(typeof x); //輸出 string console.log(typeof y); //輸出 number //長字串在程式碼內的換行,要加上 \ 來表示 var longString = 'This is a very long string which needs \ to wrap across multiple lines because otherwise my code is unreadable.'; ``` ## 陣列 * Javascript 的 array 並不會發生 index out of range 的錯誤 * 當嘗試存取不存在的元素時,存取到的會是 undefined 型態 * 當直接在陣列中原本不存在的位置新增一個元素時 (ex: 2),會將該元素確實插入陣列中,並在中間插入 empty item ![image](https://hackmd.io/_uploads/r18TYFBta.png =50%x) * Javascript 內建的 sort 預設會將其中的元素轉成字串後,再根據 asc2 碼比對;若想針對數值排序,則需要自訂 compare function ```javascript let a=[] //宣告一個空 array let b=[1, 3, 5] //在宣告 array 的同時初始化部分元素 let c=[3.14, ["apple"]] //array 可以包含任意的資料型態 console.log(c[1]) //存取 array 中的元素 (0-index) (輸出 ["apple"]) c[1] = ["banana", "watermelon"] //可以直接修改 array 中的元素 console.log(c) //輸出 [3.14, ["banana", "watermelon"]] console.log(b.length) //取得 array 的長度 (輸出 3) b.push(7) //在 array 的尾端新增元素 // b[b.length] = 7 //與上面的寫法等價 console.log(b) //輸出 [1, 3, 5, 7] b.pop() //刪除一個在 array 尾端的元素 console.log(b) //輸出 [1, 3, 5] b.splice(2, 0, 10); //在位置 2 插入 10 (第二個參數為 "插入位置" 後刪除的元素數量) console.log(b) //輸出 [1, 3, 10, 5] b.sort(function(a, b) { return a - b; // 遞增排序 }); console.log(b) //輸出 [1, 3, 5, 10] let d = Array.from(b); //複製 array b 到 array d,修改 d 不會影響 b let e = [...b, "eArray", 3.14]; // "..." 為展開運算子 (Spread Operator),可以將 array 展開 console.log(e) //輸出 [1, 3, 5, 10, "eArray", 3.14] const [member1, member2, ...otherMember] = e; // 陣列解構 (Array Destructuring),將 e 中的元素一次依序指定給 member1, member2, otherMember console.log(member1, member2); //輸出 1 3 console.log(otherMember); //輸出 [5, 10, "eArray", 3.14] // array 的 map() method 用法 let f = [1, 2, 3, 4, 5]; console.log(f.map( // map() 函式可以對 array 中的每個元素做處理,"()" 裡放的是關於處理邏輯的函式 (輸出 [2, 2, 6, 4, 10]) function(x) // x 為 array 中的元素,像是 C++ 中 for(auto x: f) 中的 x { if(x%2==0) return x; else return x*2; } )); console.log(f.map( function(x, index) // 可以使用第二個參數來取得元素的 index { return "index: " + index + ", value: " + x; } )); console.log(f.map(x => x * 2)); //可以結合箭頭函式 (Arrow Function) 來進一步簡化寫法 (輸出 [2, 4, 6, 8, 10]) // array 的 filter() method 用法 console.log(f.filter( // filter() 函式可以對 array 中的每個元素做篩選,"()" 裡放的是關於篩選邏輯的函式 (輸出 [2, 4]) function(x) { return x%2==1; // 若回傳 true,則保留該元素,否則則捨棄 } )); //輸出 [2, 4] // array 的 foreach() method 用法 f.forEach( // forEach() 函式可以對 array 中的每個元素,都執行一次 function 中的內容,"()" 裡放的是關於處理邏輯的函式 (輸出 1 2 3 4 5) function(x) { console.log(x); } ); ``` ## Object (物件) * 在傳統的物件導向語言中,物件是經由事先定義好的 schema (如在 C++ 中定義 class) 所創建的實體,這種物件導向的方式稱為 class-Based * 但 Javascript 屬於 Prototype-Based,也就是走了與 C++ 截然不同的物件導向模型;在此模型下,不需要 schema 便可以創建物件 * 本篇主要介紹不透過 schema 產生的物件,其語法及用途 * 是一個可以存任意型態(包含 function) 的 "資料型態" * Javascript 中的其他資料型態可以視為特殊的 Object * 可以直接拿來作為 hash table 使用 ```javascript let emptyObject = {}; //宣告一個空 Object //let emptyObject = new Object(); // 等價寫法 // 宣告一個 Object,並在宣告的同時初始化 let Student = { // 初始化 Object 的 attribute id: "410410054", name: "Jason", age: 22, // 初始化 Object 的 method sayHello: function() { let studentID = this.id; //使用 this 關鍵字,表示 "本物件"的 attribute console.log(studentID, "Hello!"); } }; console.log(Student.id); //存取物件的 attribute (輸出 410410054) Student.sayHello(); //使用物件的 method (輸出 Hello!) console.log("id" in Student); //判斷物件是否有某個 attribute (輸出 true) let temp="like"; Student[temp] = "apple"; //如果該 attribute 原本不存在,則新增之,"[]" 內可以放變數 //Student.like = "apple"; //等價寫法 console.log(Student); //存取物件的 attribute (輸出 apple) delete Student.like; //刪除物件的 attribute let Student1 = Object.assign({}, Student); //複製物件,修改 Student1 不會影響 Student console.log(Student1); let obj1 = {a: 1, b: 2}; let obj2 = {...obj1, c: 3, d: 4}; // "..." 為展開運算子 (Spread Operator),可以將物件展開 console.log(obj2); //輸出 {a: 1, b: 2, c: 3, d: 4} // 物件解構 (Object Destructuring),將 obj2 中的元素一次依序指定給 a, b, other // 注意 a, b 的名稱要與 obj2 中的 attribute 名稱相同才能正確解構,展開運算子不再此限 const {a, b, ...elementOther} = obj2; console.log(a, b); //輸出 1 2 console.log(elementOther); //輸出 {c: 3, d: 4} ``` ## if-else / switch * 跟 C++ 用法完全一樣 ## Loop * 有 while 跟 for 兩種,用法跟 C++ 完全一樣 ## function * Javascript 的 function 本質上是物件 * 基本使用方法類似 C++ * 不需要指定傳入值的型態 * 不需要指定回傳值的型態 * function 可以做為另一個 function 的參數傳入,此時該 function 稱為 callback function ```javascript function f(x, y) { return x + y; } // const f = (x, y) => x + y; // 箭頭函式 (Arrow Function),與上面的 f 函式寫法等價 function g(name, callback) { // do something callback(name); } function h(...args) // ..." 用於函式參數中表示其餘運算子 (Rest Parameter),可以接收不定數量的參數 { // args 是一個陣列,可以用 for 迴圈來存取 let sum=0; for(let i=0; i<args.length; i++) sum += args[i]; return sum; } console.log(f(1, 2)); //輸出 3 // callback function 用法 // "function() {}" 表示 Anonymous Function (匿名函式) g("Jason", function(n) {console.log("hello world,", n)}); // 輸出 hello world, Jason console.log(h(1, 2, 3, 4, 5)); //輸出 15 console.log(h(1, 2, 3)); //輸出 6 ``` ## exception * 可以向 python 一樣有 try-catch 區塊 ```cpp try { f(1); // 呼叫一個不存在的函式 } catch (err) // 使用 catch 捕捉錯誤 { if(err instanceof ReferenceError) // 可以用 if 判斷錯誤類型 { console.log("This is a ReferenceError error"); console.log(err.name + ': ' + err.message); // 輸出 ReferenceError: f is not defined } else { console.log("This is other error"); } } finally // 一定會執行的區塊,通常用來釋放資源,可以不寫 { console.log("finally, must be executed"); } try { throw new Error("something wrong"); // 主動丟出一個錯誤物件,可以自訂錯誤訊息 } catch (err) { console.log(err.name + ': ' + err.message); // 輸出 Error: something wrong } ``` ## class basic * 儘管 Javascript 是 Prototype-based,仍引進了一種語法糖 (更便利的語法結構),使創建及使用物件的過程更接近於 Class-based (C++ 的那套) 物件導向模型 ```javascript class human // 使用 class 關鍵字來定義類別的 schema { constructor(name, age) // 透過 constructor 來宣告與初始化這個 class 的 attribute { this.name = name; // 透過 this 關鍵字來指名要操作的目標屬於這個 class,而非傳入的參數或暫時宣告的區域變數 this.age = age; this.height = 170; } say() // 定義這個 class 的 method,這個函式會被所有這個 class 的物件所共用 { console.log("My name is", this.name); } } let person1 = new human("Jason", 18); // 透過 new 關鍵字來建立一個物件 instance,並且呼叫 constructor 來初始化 console.log(person1.name); // 存取物件的 attribute person1.say(); // 呼叫物件的 method ``` ## class private attribute and method * 主要使用 "#" 關鍵字代表這個 attribute 或 method 是 private 的 ```javascript class human { #ID; // 宣告一個 private attribute,外部無法存取 constructor(name, age) { this.name = name; this.age = age; this.height = 170; this.#ID=0; // class 內部可以存取 private attribute } say() { this.#printName(); // 宣告一個 private method,外部無法呼叫 } #printName() // 宣告一個 private method,外部無法呼叫 { console.log("My name is", this.name); } } let person1 = new human("Jason", 18); person1.say(); // console.log(person1.#ID); // person1.#printName(); // 這行會出錯,因為 #printName() 是 private method ``` ## class 繼承 * 概念同 C++,利用 extands 關鍵字完成繼承的動作 ```javascript class human // 使用 class 關鍵字來定義類別的 schema { constructor(name, age) // 透過 constructor 來宣告與初始化這個 class 的 attribute { this.name = name; // 透過 this 關鍵字來指名要操作的目標屬於這個 class,而非傳入的參數或暫時宣告的區域變數 this.age = age; this.height = 170; } say() // 定義這個 class 的 method,這個函式會被所有這個 class 的物件所共用 { console.log("My name is", this.name); } } class student extends human // 使用 extends 關鍵字來繼承父類別 { constructor(name, age, id) { super(name, age); // 呼叫父類別的 constructor,以初始化繼承自父類別的 attribute this.studentID = id; // 新定義並初始化 child class 自己的 attribute } study() // 定義 child class 自己的 method { console.log("I am studying"); } say() // overwrite 父類別的 method { console.log("My name is", this.name, ", my id is", this.studentID); } } let student1 = new student("Jason", 18, 410410054); // 透過 new 關鍵字來建立一個物件 instance,並且呼叫 constructor 來初始化 console.log(student1.studentID); // 存取物件的 attribute student1.say(); // 呼叫物件的 method ``` ## 同步與非同步 * Javascript 在處理任務時,可以選擇使用同步或非同步執行 * 同步(sync): 一次只執行一件任務 * 非同步(async): 多件任務同時進行,平行處理 ### promise * promise 可以視作一個用於非同步事件處理的物件 (把事件處理這件事物件化),共分成三種狀態 * pending: 事件已經運行中,尚未取得結果 * resolved:事件已經執行完畢且成功執行,回傳 "resolve" 的結果 * rejected:事件已經執行完畢但執行失敗,回傳 "reject" 的結果 ![image](https://hackmd.io/_uploads/rJqTl_5ia.png =80%x) * 在創建一個 promise instance 時,需傳入一個 function 作為參數;該 function 須包含兩個參數 resolve、reject,分別代表這個 promise 執行成功跟失敗時要呼叫的 method;一個 promise instance 被創建出來後,便會即刻執行裡面的內容 * promise 本身屬於非同步的事件;**所有的同步原始碼運行完以後才會執行非同步的事件** * 使用 .then() 這個 method 來處理 promise 執行完畢且成功執行後要做的事;傳入的參數是一個 function,包含兩個參數,代表Promise 函式中 "resolve()" 所傳入的值 * 在 .then() 這個 method 後緊接著使用 .catch 來處理 promise 執行完畢但執行失敗後要做的事,傳入的參數類似 .then() 的定義 ```javascript const myPromise = new Promise((resolve, reject) => { // 建立一個 Promise 物件 const num = Math.random() > 0.5 ? 1 : 0; // 隨機取得 0 or 1 if (num) { // 若 num 為 1 resolve('成功'); } else { reject('失敗') } }); myPromise.then((success) => { // mypromise 執行結束時,若成功則執行 then console.log(success); }) .catch((fail) => { // 若失敗則執行 catch console.log(fail); }); ``` 執行流程: 1. 創建一個 promise instance,其中的程式碼視為非同步事件 2. 執行 promise 中的程式碼,假設產生出的隨機數大於 0.5,故會呼叫 resolve(),並傳入參數 "成功" 3. 由於 promise 執行完畢且成功執行,故會進入到 .then() 區域中 4. 此時 success 便是 resolve 中傳入的參數,故會輸出 "成功" ### promise advance * 有時候會需要確保某些非同步內容需要在執行完另一些非同步內容後才被執行,這種關係可以鍊式串接 * 使用 return 關鍵字進行串接 詳細邏輯 ![image](https://hackmd.io/_uploads/BJRmkRhsa.png =80%x) ```javascript function promiseCreator(input) { return new Promise((resolve, reject) => { // 建立一個 Promise 物件 if (input!=0) { // 若 input 不為 0 resolve(`成功, input is ${input}`); } else { reject(`失敗, input is 0`) } }); } promiseCreator(1).then((success) => { // promiseCreator(1) 執行結束時,若成功則執行 then console.log(success); return promiseCreator(3) }) .then((success) => { // promiseCreator(3) 執行結束時,若成功則執行 then console.log(success); }) .catch((fail) => { // 若失敗則執行 catch console.log(fail); }); // 輸出: // 成功, input is 1 // 成功, input is 3 ``` 執行流程: 1. 呼叫 promiseCreator(1),創建第一個 promise 2. 第一個 promise 執行成功,接著呼叫 promiseCreator(3),創建第二個 promise 3. 第二個 promise 執行成功 可以視作當呼叫 promiseCreator() 時,便是丟出一個非同步事件給系統執行;執行完後,再使用 .then() 決定接下來要做的事 ### async/await * async 是作用於函式的修飾字,可以讓該函式無論如何都會回傳 promise * await 則是可用於 async 函式中的修飾字,用於等待某個 promise 回傳後再繼續下一步 ```javascript async function promiseCreator(input) { if (input!=0) { // 若 input 不為 0 return `成功, input is ${input}`; } else { return Promise.reject(`失敗, input is 0`) } } async function main() { // await 關鍵字只能在 async 函式中使用 try { // 使用 try-catch 來處理 promiseCreator(1) 或 promiseCreator(3) 執行失敗的情況 let success = await promiseCreator(1) // 等待 promiseCreator(1) 執行結束,若成功則將結果指定給 success console.log(success); success = await promiseCreator(3) // 等待 promiseCreator(3) 執行結束,若成功則將結果指定給 success console.log(success); } catch (error) { // 若 promiseCreator(1) 或 promiseCreator(3) 執行失敗,則執行 catch 中的程式 console.log(error); } } main(); // 輸出: // 成功, input is 1 // 成功, input is 3 ``` # React 教學 ## 環境建置 1. 下載 react app 專案模板 (只須執行一次,之後便會裝到全域之中) ``` npm install -g create-react-app ``` 2. 使用指令創建一個簡單的 react app 所存放的資料夾,並初始化專案架構 (npx 用於執行包含在 npm package 中的可執行檔案) ``` npx create-react-app my_course0_use_template ``` 3. 切換至專案目錄中,啟動伺服器,即可在 http://localhost:3000/ 看到結果 ``` npm start ``` ## react 專案模板介紹 詳情請見 my_course0_use_template 這個 react 專案 ### 專案目錄 ![image](https://hackmd.io/_uploads/ByDcg1rc6.png) * public 資料夾: 用來存放靜態資料 * index.html: react 預設的入口 Html * src 資料夾: 用來存放程式 * App.css: css 檔案,可以被其他 .js 檔案引入,以修飾渲染的內容 * App.js: jsx 檔案,可以被其他 .js 檔案引入,以插入至特定位置 * index.css: css 檔案,可以被其他 .js 檔案引入,以修飾渲染的內容 * index.js: 整個專案的入口 JS 檔案 ### 工作原理 1. 在終端機中輸入 npm start 後,react 會以 index.html 作為入口網站,並以 index.js 來渲染 index.html 2. 在 index.js 中,指明了要以入口網站中 Id 為 "root" 的 element 作為渲染的根目錄,並使用 "App" 來渲染 3. "App" 的來源是同目錄下的 App.js 中的 App function ## react hello world * 在 my_course1_react_helloworld 這個 react 專案中,展示了最基本所需的工作檔案及目錄 ![image](https://hackmd.io/_uploads/r12DjxSca.png) 在本例中,創建了一個包含 "Hello, world!" 的網站 index.js ```javascript import React from 'react'; import ReactDOM from 'react-dom/client'; const root = ReactDOM.createRoot(document.getElementById('root')); // 以 index.html 中的 <div id="root"></div> 作為根目錄 // 這種直接在 JS 中使用 html 語法的技巧,被稱為 JavaScript XML (jsx),為一種 javascript 的語法擴展 root.render( <h1>Hello, world!</h1>, {/* 這裡的內容會被渲染到 <div id="root"></div> 中 */} ); ``` index.html ```html <div id="root"></div> ``` ## React Component & hooks 觀念 * 在 React 的世界中,React Component 是可重複使用的程式碼片段 * 若是利用 JavaScript 函式來定義與描述 Component,這個 Component 便稱為Function Component(函式型組件) * 而 React Hooks 的功能則是用於控制 Function Component 的狀態和生命週期 * 通常在使用 React Hooks 時,會使用陣列解構取出兩個參數 * 第一個參數是 state variable 本身,做為渲染顯示用 * 第二個參數是一個函式,目的是更新 state variable,並同時重新渲染這個 component * 常見的 Hooks 包含 * useState: 為 Component 添加狀態 * useEffect: 用於處理 Component 的副作用 在本例中,創建了一個計算點擊次數的按鈕,並在點擊時同時更新網站的 title index.js ```javascript import React from 'react'; import ReactDOM from 'react-dom/client'; // 定義一個名為 ButtonCounter 的 Component // 注意: React 的 Component 必須要大寫開頭 function ButtonCounter() { // 使用 React.useState() 來定義一個 state variable // React.useState() 中傳入的參數為 state variable 的初始值 // React.useState() 會回傳一個陣列,陣列中的第一個值為 state variable,第二個值為更新 state variable 的 function // const 關鍵字作用於陣列身上,陣列中的元素仍然可以被重新賦值 const [count, setCount] = React.useState(0); // 這是一個 hook // 使用 React.useEffect() 來定義一個渲染這個 Component 時的副作用 (side effect) // React.useEffect() 中傳入的參數為一個 function,這個 function 會在渲染這個 Component 時被執行 React.useEffect(() => { // 這是另一個 hook document.title = `You clicked ${count} times`; // 這個例子中,在重新渲染這個 Component 時,會將網頁標題設定為 `You clicked ${count} times` }); // 拿一個空陣列作為 React.useEffect() 的第二個參數,可以讓這個 function 只在第一次渲染時被執行 // React.useEffect(() => { // document.title = `You clicked ${count} times`; // }, []); // 也可以將欲監聽的元素放在陣列中,當陣列中的元素有變化時,這個 function 會被執行 // React.useEffect(() => { // document.title = `You clicked ${count} times`; // }, [count]); // cleanup function 的特殊寫法,要寫在第一個參數的 function 中的 return 裡面 // 效果是在這個 Component 被重新渲染前,才會執行這個 function // React.useEffect(() => { // return () => { // document.title = `You clicked ${count} times`; // } // }, [count]); // 定義按鈕被點擊時的 handling function function handleClick() { setCount(count + 1); // 使用 setCount 來將 count 這個 state variable 的值更新成 count + 1 } return ( // 定義這個 Component 要回傳的內容,本例會回傳一個按鈕 <div> <button type="button" onClick={ handleClick }> Count is: { count } {/* 使用 {} 嵌入 state variable */} </button> </div> ); } const root = ReactDOM.createRoot(document.getElementById('root')); root.render( <ButtonCounter /> // 將 ButtonCounter 這個 Component 轉換成 React element 後,渲染到 <div id="root"></div> 中 ); ``` index.html ```html <div id="root"></div> ``` ## React Element * React Element 是描述網站 UI 的最小單位,可以視作 HTML 元素在 javascript 中的的抽象體現 * 在 React 中,可以利用 Jsx 語法輕鬆的建立 React Element ```javascript // 等號右邊為 Jsx 語法 // 此時 example 即為 React Element 型態 const example = <h1>Hello React</h1>; ``` * Jsx 是 React 針對 JavaScript 所出的擴充語法,全名是 JavaScript & XML,是一種 HTML in JavaScript 的擴充語法 * 在 Jsx 中,可以使用 "{}" 任意嵌入表達式 (Expression);表達式指的是會回傳結果的 statement * 可以嵌入變數 ```javascript const myName = 'Jason'; const example = <h1> Hello { myName } </h1>; ``` * 可以嵌入函式 ```javascript const sayHi = () => { return "Hi" } const example = <h1> { sayHi() } </h1>; ``` * 還有嵌入陣列並自動展開的用法 ```javascript const myList = [ <p key="e1">example 1</p>, <p key="e2">example 2</p>, ] const element = <h1>{myList}</h1>; ``` 結果: ![image](https://hackmd.io/_uploads/HJTd2vHqT.png =20%x) * 在 Jsx 語法中,需要用特殊的方式進行註解 ```javascript const example = <h1> Hello { /* 這是一段註解 */ }</h1>; ``` * 若是想要在 Jsx 語法中指定某個 element 的 style,可以使用以下寫法 ```javascript // style 屬性是駝峰式命名法,有別於純 Html (例如 background-color) const customStyle = { backgroundColor: 'black', color: 'white' }; const example = <h1 style={ customStyle }>Hello React</h1>; ``` * 若是想要在 Jsx 語法中指定某個 element 的 class,可以使用以下寫法 ```javascript // 由於 class 是保留字,故替換成 className const example = <h1 className="box">Hello React</h1>; ``` * Jsx key 是一個重要概念 * 使用時機為動態生成 react element 陣列時 * 可以幫助 react 追蹤 element,並在適當的時機重新渲染 本小節以一個較為複雜的 Jsx key 應用作為收尾 index.js ```javascript import React from 'react'; import ReactDOM from 'react-dom/client'; function ListItem() { // 宣告一個名為 ListItem 的 function component const [array, setArray] = React.useState(["a", "b", "c"]); // 宣告一個state variable function reverse() { // 宣告一個function,當按下按鈕時便會呼叫 setArray([...array].reverse()); // 將array反轉後,再將結果設定回 state variable } // 值得注意的是 Jsx 中嵌入的表達式內容 // 利用 array.map() 遍歷 array,並在每次的迭代中回傳一個 <li> react element return ( <div> <ul> {array.map( (item, index) => <li key={index}>{item}</li> )} </ul> <button type="button" onClick={ reverse }>反轉</button> </div> ) } const root = ReactDOM.createRoot(document.getElementById('root')); root.render( <ListItem /> ); ``` index.html ```html <div id="root"></div> ``` ## react event * react 的事件處理可以在某個事件發生時,呼叫對應的函式 * 共分成五種事件 * onClick: 點擊時會觸發,通常作用於 \<a>、\<button> * onChange: 改變時會觸發,通常作用於 \<input>、\<textarea>、\<select> * onSubmit: 送出時會觸發,通常作用於 \<form> * onKeyDown: 按下按鍵時會觸發,通常作用於 \<input>、\<textarea> * onFocus: 關注這個元素時會觸發,通常作用於 \<input>、\<textarea> * onBlur: 取消關注這個元素時會觸發,通常作用於 \<input>、\<textarea> * 特殊用法: 在呼叫事件處理函式時,可以使用以下語法來避免預設行為的觸發 ```javascript function submitEvent(e) { e.preventDefault(); //特殊用法: 阻止預設行為 window.alert('you Submit the form'); } ``` index.js ```javascript import React from 'react'; import ReactDOM from 'react-dom/client'; const root = ReactDOM.createRoot(document.getElementById('root')); function clickEvent() { window.alert('you Click the button'); } function changeEvent() { window.alert('you Change the input'); } function submitEvent(e) { e.preventDefault(); //特殊用法: 阻止預設行為 window.alert('you Submit the form'); } function keyDownEvent(e) { window.alert(`you Press the key ${e.key}`); } function focusEvent() { window.alert('you Focus the input'); } function blurEvent() { window.alert('you Blur the input'); } function MyApp() { return ( <div> <p>onClick Event</p> <button type="button" onClick={ clickEvent }>onClick</button> <hr /> <p>onChange Event</p> <input type="text" onChange={ changeEvent } /> <hr /> <p>onSubmit Event</p> <form onSubmit={ submitEvent }> <input type="text" /> <button type="submit">submit</button> </form> <hr /> <p>onKeyDown Event</p> <input type="text" onKeyDown={ keyDownEvent }/> <hr /> <p>onFocus Event</p> <input type="text" onFocus={ focusEvent }/> <hr /> <p>onBlur Event</p> <input type="text" onBlur={ blurEvent }/> <hr /> </div> ); } root.render( <MyApp /> ); ``` index.html ```html <div id="root"></div> ``` ## react component advance * react component 可以巢狀嵌套 * props 是將資料傳遞給 react component 的重要技巧 index.js ```javascript import React from 'react'; import ReactDOM from 'react-dom/client'; function Header() { return <h1>this is Header</h1>; } function Boby(props) { // 在 function component 的語法中,使用函式參數接收傳入的資料 return ( <div> <h1>this is Boby</h1> <h2>props.name: { props.name }</h2> <h2>props.object: { props.object.myName }</h2> <h2>props.array: { props.array[0] }</h2> <h2>props.number: { props.number }</h2> { props.dom } {/* react element 可以直接被渲染*/} { props.bobyEnd } {/* react component 可以直接被渲染*/} </div> ); } function BobyEnd () { return <h2>this is BobyEnd</h2>; } function Footer() { return <h1>this is Footer</h1>; } function MainPage() { return ( <div> <Header /> {/* 巢狀 component */} {/* 使用 props 傳遞資料給 Body 這個 component */} {/* 可以傳字串、物件、陣列、數字、react element、另一個 react component 等等*/} <Boby name="Jason" object={ { myName: "Jason", age: "21" } } array={ [1, 2, 3] } number={ 1 } dom={ <h2>props.dom</h2> } bobyEnd={ <BobyEnd /> } /> <Footer /> </div> ); } const root = ReactDOM.createRoot(document.getElementById('root')); root.render( <MainPage />, ); ``` index.html ```html <div id="root"></div> ``` ## react useContext * probs 已經可以將資料傳遞給下一個 Component,然而,如果需要跨 Component 傳遞資料的話,使用 useContext 的方法較簡潔 * useContext 分成兩種對象 * provider: 負責提供資料 ```javascript <DataContext.Provider value={ passData }> ``` * comsumer: 負責接收資料 ```javascript const value = React.useContext(DataContext); ``` index.js ```javascript import React from 'react'; import ReactDOM from 'react-dom/client'; const DataContext = React.createContext(); // 宣告一個 Context,用來在 Component 間傳遞資料 function MainPage() { const [passData, setData] = React.useState("myData"); // 本例中,passData 是要傳給其他 Component 的資料 return ( <DataContext.Provider value={ passData }> {/* provider,提供資料給以下被包住的 Component 以及其子 Component */} <h1>Main Page</h1> <SecondaryPage /> </DataContext.Provider> ) } function SecondaryPage() { return ( <div> <h2>Secondary Page</h2> <Button /> </div> ) } function Button() { const value = React.useContext(DataContext); // consumer,取得 Context 中的資料 // 此時 value 就是 MainPage 中的 passData return ( <button>{ value }</button> ) } const root = ReactDOM.createRoot(document.getElementById('root')); root.render( <MainPage /> ); ``` index.html ```html <div id="root"></div> ``` ## react 環境變數的使用 1. 在專案目錄中安裝用於管理環境變數的套件 ``` npm i dotenv ``` 2. 在專案的根目錄中新增一個 .env 的檔案,並打上想要的環境變數名稱跟值,以下是舉例 ```env REACT_APP_MY_ENV_VARIABLE=本地 REACT_APP_HOST=13.32.64.128 REACT_APP_PASSWORD=123123 ``` 注意: react 要求這些環境變數的前綴是 "REACT_APP_",若非使用 react 框架,則沒有這項要求 3. 直接使用即可 ```javascript const myVariable = process.env.REACT_APP_MY_ENV_VARIABLE; ``` # Next.js 教學 * Next.js 是一個基於 react 的框架,擁有許多好處 * routing 系統 * 效能優化 * 完美契合 Vercel (免費的部屬網站平台) ## 環境建置 (windows 10) 1. 使用以下指令創建一個簡單的 next.js app 所存放的資料夾,並初始化專案架構 (npx 用於執行包含在 npm package 中的可執行檔案) ``` npx create-next-app@latest my_course7_nextjs_use_template ``` 2. 接著在終端機中選擇一些跟專案有關的初始設定 ![image](https://hackmd.io/_uploads/H1c-17K5T.png) * 不使用 typescript * 使用 ESLint (處理 javascript 的程式碼品質和程式碼風格問題) * 不使用 tailwind CSS * 使用 /src 目錄,專案核心程式會放在這裡 * 不使用 app router (新出的 routing 系統),使用 page router (舊的,較簡單) * 使用預設的 import 方式 3. 切換至專案目錄中,使用以下指令安裝 next.js 所需環境 * 使用 next.js 13 版本 * 使用最新版 react * 使用最新版 react-dom * 使用最新版 eslint-config-next (用於檢查 JS 錯誤) ``` npm i next@13 react@latest react-dom@latest eslint-config-next@13 ``` 4. 使用以下指令啟動伺服器,即可在 http://localhost:3000/ 看到結果 ``` npm run dev ``` ## next.js 專案模板介紹 ### 專案目錄 ![image](https://hackmd.io/_uploads/rJlbmQYqp.png) * public 資料夾: 存放靜態檔案,例如圖片、來自外部的 JavaScript 或 CSS (直接寫在 html 中以連結方式引入,而不使用 import 來引入的那種) * src 資料夾: 專案核心程式碼存放處 * pages 資料夾: routing 的核心 * api 資料夾: next.js 也可以做為 api server,這裡是官方的一個範例 * \_app.js: 整個專案的入口 (所有頁面共用的最上層 Component),通常不需特別修改 * 可以在此 import 一些整個專案都會用到的 CSS 檔案 * \_document.js: 整個專案的入口,通常不需特別修改 * 可以在此插入一些整個專案都會用到的 Component * index.js: 對應至 "/" 要回傳的頁面 * styles 資料夾: 存放 CSS 檔案 * 補充: 一些只有 next.js 才有的 element,進一步優化了原生 html 的 element * \<Html>\</Html>: 在 _document.js 中出現,是 next.js 的底層用法,用來包住 html 區塊 * <Main />: 在 _document.js 中出現,是 next.js 的底層用法,這個 Component 會被 next.js 底層置換成各個 Page 的程式碼 * <NextScript />: 在 _document.js 中出現,在 next.js 的底層中,用來插入相關 script * \<Image>: * \<Link>: 取代原生 html 中的 \<a> 功能,速度快上許多 (主要利用客戶端的頁面跳轉與預先載入實現),推薦使用 ## next.js 嵌入靜態變數 由於 Next.js 在 build 階段便會將靜態變數嵌入在程式碼中,故需要透過一個內建的函式將靜態資源傳入至 default function 中;以下是一個簡單的程式範例 專案目錄 ![image](https://hackmd.io/_uploads/HJTAqwf36.png =20%x) .env ```javascript REACT_APP_HOST=本地 REACT_APP_DISPLAY_MESSAGE=hello react ``` index.js ```javascript import Head from "next/head"; export default function Home(props) { return ( <> <Head> <title>Create Next App</title> </Head> <main> <h1>{ props.REACT_APP_HOST }</h1> <h1>{ props.REACT_APP_DISPLAY_MESSAGE }</h1> </main> </> ); } // getStaticProps 是一個 next.js 中內建的函式 // 目的是取得靜態變數,並且將資料傳遞給 default function export async function getStaticProps() { // 回傳一個 props 物件,這個物件中包含了兩個 key-value pair // 這個物件會被傳遞給 default function return { props: { REACT_APP_HOST: process.env.REACT_APP_HOST, REACT_APP_DISPLAY_MESSAGE: process.env.REACT_APP_DISPLAY_MESSAGE, } }; } ``` ## next.js 嵌入動態變數 ## next.js routing * 使用 next.js 作為前端的最大好處之一便是內建的 routing 系統 * next.js routing 系統最大的特點是 "根據 pages 資料夾中的目錄自動進行 routing",檔案架構即是 routing 架構,極大的簡化了原先繁瑣的 routing 流程 ![image](https://hackmd.io/_uploads/H1oXjpF5p.png) routing 範例 (以 pages 下的 index.js 為例) ```javascript import Head from "next/head"; import Link from "next/link"; export default function Home() { return ( <> <Head> <title>Create Next App</title> </Head> <main> <h1>This is the dashboard</h1> <Link href="/">Go to root</Link> <hr></hr> <Link href="/dashboard/setting">Go to dashboard/settings</Link> </main> </> ); } ``` ## next.js 載入靜態資源 ### 圖片 * 載入圖片可以使用 html 原生的 \<img> * 也可以使用 next.js 內建的 \<Image />。在使用 \<Image /> 時,必須傳入三個參數 * src: 圖片的路徑,從 public 資料夾中開始計算 * 例如某張圖片路徑是 "my_course10_nextjs_load_static/public/img/myprofile.jpg",則此參數要填 "/img/myprofile.jpg" * width: 圖片的寬 * height: 圖片的長,長寬比跟原圖一致才不會導致變形 ### CSS * 使用原生的 \<link> 在 \<Head> 中引入區域性 (只在這個 Component 中有效) CSS即可 * 本地 CSS 檔案的路徑從 public 資料夾中開始計算 * 外部 CSS 則直接使用連結引入即可 * 如果要載入全域 CSS,則在 \_app.js 中使用 import 的方式載入 (需使用從 \_app.js 出發的相對路徑) ### JS * 外部的 JS 使用原生的 \<script> 在 \<Head> 中用引入即可,也可以使用經過 next.js 優化後的方式引入 * 本地 JS 檔案的路徑需使用 import 的方式載入 (需使用從 \_app.js 出發的相對路徑) ### 程式範例 (以 pages 下的 index.js 為例) ```javascript import Head from "next/head"; import Image from "next/image"; import Script from "next/script"; import * as mainJS from "@/../public/js/clickHandler.js"; // 引入本地的 JS function,@ 表示 styles 這個目錄 export default function Home() { return ( <> <Head> <title>Create Next App</title> <link rel="stylesheet" href="/css/h1style.css"></link> {/* 引入本地的 CSS 樣式 */} <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/font-awesome/css/font-awesome.min.css"></link> {/* 引入外部的 CSS 樣式 */} {/* <Script src="https://cdn.jsdelivr.net/gh/stevenjoezhang/live2d-widget/autoload.js" strategy="lazyOnload"></Script> 引入外部的 JS 檔案 (優化板) */} <script src="https://cdn.jsdelivr.net/gh/stevenjoezhang/live2d-widget/autoload.js"></script> {/* 也可以用內建的方式引入本地的 JS script */} </Head> <main> <h1 className= "custom-heading">Welcome to my profile</h1> <Image src="/img/myprofile.jpg" width={200} height={200} /> {/* 引入本地的圖片 */} <button onClick={mainJS.handleButtonClick}>Click me!</button> {/* 呼叫本地的 JS function*/} {/* 嵌入 live2D*/} <canvas id="live2d" width="200" height="200"></canvas> </main> </> ); } ``` ## next.js 動態 routing * next.js 的 "檔案架構即 routing" 提供了一種非常值觀的方式進行 routing,然而缺乏彈性 * 動態 routing 則可以使整個系統更具備彈性 * 可使用 routing (2~4 為動態 routing 生成的 route) 1. http://localhost:3000 2. http://localhost:3000/article/1 3. http://localhost:3000/article/2 4. http://localhost:3000/article/3 * 以下的範例提供了一個 toy example,描述了一個部落格網站 * query 後端 api 以取得資料庫中有的文章 id,並以此動態生成頁面 * 以這個例子來說,資料庫中有 [1, 2, 3] 三個文章 id * query 後端 api 以取得資料庫中某篇文章的詳細資訊,並以此渲染頁面 * 以這個例子來說,資料庫中每篇文章有 ArticleId、ArticleName 兩個資訊 檔案架構 ![image](https://hackmd.io/_uploads/SJsJt1rja.png) [id].js (動態 routing 的核心檔案) ```javascript import { getAllArticleId, getArticleData} from '@/../public/js/article.js'; export default function Post({ postData }) { return ( <> {postData.ArticleId} <hr /> {postData.ArticleName} </> ) } // 是 next.js 的內建函式,動態路由的生成關鍵 // paths 是類似以下型態的"物件型態陣列",須以特定格式撰寫 // 注意 params 中的 id 要與檔名"[]"中的名稱相同 // [ // { params: { id: '1' } }, // { params: { id: '2' } }, // { params: { id: '3' } } // ] // next.js 底層透過這個函式的回傳值,來生成動態的路由 export async function getStaticPaths() { const paths = await getAllArticleId(); console.log(paths); return { paths, fallback: false, }; } // 是 next.js 的內建函式。透過這個內建的函式,next.js 便可以取得呼叫外部 JS 檔案以取得靜態資料,並將結果經由底層函式傳遞給 default function // params 來源於 getStaticPaths 的回傳值,也就是動態路由中定義的路徑參數 ({ params: { id: '1' } } 中的 params) // postData 是一個物件,例如 // { ArticleId: '1', ArticleName: 'next.js tutorial' } // 這個函式的回傳值會被傳遞給 default function export async function getStaticProps({ params }) { const postData = await getArticleData(params.id); console.log(postData); return { props: { postData, }, }; } ``` article.js (作為使用 api 向後端請求資料的 helper function) ```javascript export function getAllArticleId() { return fetch('http://localhost:8000/api/getAllArticleId') // 使用 fetch 方法向後端API發送GET請求,這是一個異步操作 .then(response => { // 定義獲得 response 之後的處理邏輯 if (!response.ok) { // 首先檢查 response 是否成功 throw new Error('獲取 response 失敗'); } return response.json(); // 將 response 解析成JSON格式 }) .then(data => { // 定義解析JSON 成功之後的處理邏輯 // 把 {"ArticleId": [1, 2, 3]} 轉換成 [{params: {id: '1'}}, {params: {id: '2'}}, {params: {id: '3'}}] // data.ArticleId 是 [1, 2, 3] // 使用 map 的方法對每個元素進行處理 // 將 1 轉換成 {params: {id: '1'}},以此類推 const processedData = data.ArticleId.map(id => ({ params: { id: id } })); return processedData; // 回傳處理後的數據 }) .catch(error => { console.error('錯誤:', error); }); } // 這個函式示範了如何使用 fetch 方法向後端API發送GET請求,同時附帶參數 export function getArticleData(id) { return fetch('http://localhost:8000/api/getArticleData?id=' + id) .then(response => { if (!response.ok) { throw new Error('獲取 response 失敗'); } return response.json(); }) .then(data => { return data; }) .catch(error => { console.error('錯誤:', error); }); } ``` main.go (做為後端 server) ```go package main import ( "encoding/json" "fmt" "net/http" "github.com/labstack/echo/v4" ) func getAllArticleId(c echo.Context) error { // ---------- 模擬 query 資料庫操作 ---------- articleId := []string{"1", "2", "3"} // ---------- 資料庫操作結束 ---------- returnValue := map[string]interface{}{ "ArticleId": articleId, } jsonData, err := json.Marshal(returnValue) if err != nil { fmt.Println(err) } return c.JSONBlob(http.StatusOK, jsonData) } func getArticleData(c echo.Context) error { ArticleID := c.QueryParam("id") // ---------- 模擬 query 資料庫操作 ---------- ArticleName := "next.js tutorial" // ---------- 資料庫操作結束 ---------- returnValue := map[string]interface{}{ "ArticleId": ArticleID, "ArticleName": ArticleName, } jsonData, err := json.Marshal(returnValue) if err != nil { fmt.Println(err) } return c.JSONBlob(http.StatusOK, jsonData) } func main() { e := echo.New() e.GET("/api/getAllArticleId", getAllArticleId) e.GET("/api/getArticleData", getArticleData) e.Start(":8000") } ``` ## 將next.js 專案部屬於 vercel * vercel 是一個用於網站部屬的平台, 1. 準備好在 githun 的 repo ![image](https://hackmd.io/_uploads/r11jkgpsp.png) 2. 來到 vercel 官網,選擇部屬 next.js app ![image](https://hackmd.io/_uploads/Sk0Egepj6.png) 3. 選擇專案來源是目前已有的 github repo ![image](https://hackmd.io/_uploads/B1XFllpj6.png) 4. 選擇對應的 repo ![image](https://hackmd.io/_uploads/Sknoegaip.png) 5. 點擊 Deplay ![image](https://hackmd.io/_uploads/BJryblTjT.png) 6. 回到主頁面,發現剛剛部屬成功的網站 ![image](https://hackmd.io/_uploads/BkB-Vx6sT.png) 7. 在 Project > Domains > Edit 中可以重新設定 vercal 提供的免費域名 ![image](https://hackmd.io/_uploads/B1YQ4lTia.png) ![image](https://hackmd.io/_uploads/Hy9PNxpia.png) ![image](https://hackmd.io/_uploads/BkV-Hgas6.png) 8. 內建 CI/CD,github 上的內容有變動時便會自動更新當前已上線的網站 ![image](https://hackmd.io/_uploads/BJCiLg6ia.png) (具體而言,當 master (預設) branch 有變動時,便會進行更新 可以在 Settings > Git > Production Branch 中進行設定) ![image](https://hackmd.io/_uploads/rJP-claop.png)