标签为“iT鐵人賽”的页面如下
你可能不知道在JS世界裡的特殊物件
特殊物件清單
JavaScript是一個有著龐大使用族群的程式語言,但是因為其歷史淵源和不同考量等因素下,其中有不少令人萬丈摸不著頭緒的設計。自連class
都只作為保留字而無實際作用的時候,就已經有在接觸,在後續越了解越多,想想應該是能來分享一些,其中一些我知道的特殊物件。
undefined
null
this
super
NaN
Infinity
new
new.target
Object.prototype
- 先有
Function
還是先有Object
- 先有
Symbol
Symbol.for()
、Symbol.keyFor()
document.all
typeof document.all
arguments
hashbang
- HTML comment
'use strict'
globalThis
window
document
其中有一些並不是真正的物件,但都是一些執行環境下支援特殊寫法。或許有一些並沒有實際作用,但可能很多人並不知道,畢竟平常大概也沒有人會這樣寫吧!所以其實也就是一些JavaScript裡無關緊要的有趣小地方。
當然…當中有一部分也有可能成為你日後會踩入的陷阱(抗)。那麼就先來說說undefined
和null
吧!
undefined
undefined
是一個屬於undefined
的物件。(但可能不是唯一)
typeof undefined; // -> "undefined"
In all non-legacy browsers, undefined is a non-configurable, non-writable property. (Even when this is not the case, avoid overriding it.) – from MDN
儘管在現今主流的瀏覽器都是不可改變全域的undefined
變數:
這些那些你可能不知道我不知道的web技術細節-目錄與完賽感想
終於、終於30天啦啦啦!!!
原本其實是有猶豫今年要不要報名的,因為去年結果讓我有點失落憔悴…
然後這次也沒有組團,沒拖人下水成功。雖然也還是有發現一些認識的朋友今年也有參加,但還是一度在猶豫要不要報名,所以我幾乎是拖到最後一天才報名的哈哈。
工作的這兩年,遇到的事情這樣看來應該不算少。我有把一些我有興趣的議題記錄下來的習慣,雖然回去看也就一些很零碎的關鍵字,甚至有些一度回想不起來是什麼玩意兒。 這些東西我是有可能另外寫出來做記錄發表的,所以這次抱著反正之後也還是想寫,那就參加寫吧的想法報名,沒完賽就算了。
不過其實原本是有兩個參賽主題的,但最後只選擇了一個。一方便是另外一個對現在我來說不太好組織,另外就是時間安排上,我是很後來才真正決定要報名的。
而且在開賽前幾天確診Orz
隔離了7天
「這些、那些、你可能不知道、我不知道的Web技術細節」記錄了一些受到工作同僚、朋友聊天討論啟發,進一步研究原本我不知道或沒那麼清楚的Web技術細節。儘管足有接近30篇,但其實與我原本記下的關鍵字還是少了不少。像是WebAssembly、WebRTC、WebGL、Mono Repo、Micro Frontend等等。有些東西我有一些接觸,也有不少是還需要花費大量時間學習的。
而且這個系列,每一篇都至少花 超過兩個小時 構思撰寫。並且我實在不是很像破壞每一篇的獨立和完整性。所以有些篇數對於一次要閱讀完怕是會有些吃力,但基本每一篇都可以獨立參考閱讀,而不需要在意閱讀順序性。
目錄(依發表時間序)
你可能不知道的Web API--Web Locks
前言
Web Locks相關的API目前還是實驗性質的,這意味著未來可能有所變動,會與本片內容提及用法、作用有差異。雖然是實驗性質,但目前主流瀏覽器都已經支援。
使用方式
最基本用法是透過navigator.locks.request()
取得一把鎖,如果無法取得就必須等待直到能夠取得。如果取得了,就可以執行後續callback的動作。通常callback是一個異步函式,舉例來說寫法會如下:
navigator.locks.request('lock-1', async (lock) => {
console.log('get lock-1');
console.log('do something');
console.log('release lock-1');
});
callback的執行區域,被稱作是 關鍵區域 (Critical section)。
如果設計的恰當,關鍵區域只會有一個在執行。把上面再改寫一下:
var lock_name = 'lock-1';
navigator.locks.request(lock_name, (lock) => {
console.log(`A: get lock ${lock.name}`);
return new Promise(res => {
/// 10秒後釋放鎖
setTimeout(() => {
console.log(`A: release lock ${lock.name}`);
res(); // release lock
}, 10000 /*ms*/);
})
})
navigator.locks.request(lock_name, (lock) => {
console.log(`B: get lock ${lock.name}`);
return new Promise(res => {
/// 5秒後釋放鎖
setTimeout(() => {
console.log(`B: release lock ${lock.name}`);
res(); // release lock
}, 5000 /*ms*/);
})
})
A: get lock lock-1 A: release lock lock-1 B: get lock lock-1 B: release lock lock-1
在上面範例,有兩個程式區塊A和B需要使用到lock-1
這把鎖。A需要消耗10秒,並優先取得了鎖;B必須等待10秒後,才會開始執行。
可以透過將Promise
的resolve()
或reject()
傳遞出來,來決定什麼時候要釋放鎖:
你可能不知道的(Web)API--FinalizationRegistry(GC)
你可能不知道的Web API–FinalizationRegistry(GC)
FinalizationRegistry是和WeakMap、WeakSet、WeakRef在ES12一同進入到語言規範裡的兩個API。其實後面幾個更容易使用到,但我今天偏偏就是要來聊聊前者–FinalizationRegistry
。
在說說為什麼這個API有點雞肋可能沒什麼人知道之前,還是先介紹介紹這個API的用法。這個API的作用是在變數物件在被記憶體回收以前,可以註冊一些清理動作。比如可以建立物件:
var registry = new FinalizationRegistry((heldValue) => {
console.log(`${heldValue} is cleaned`);
})
var obj1 = { toString() { return "<Object obj1>"} };
registry
的callback function將快被記憶體回收的訊息打印出來。如果我們希望了解obj1
和obj2
何時被回收,可以用.register()
註冊:
registry.register(obj1, obj1.toString());
那麼當obj1
或obj2
不再可以被存取的時候,就有可能被記憶體回收,進而列印出訊息出來:
obj1 = null;
至於為什麼
obj1
不能使用delete
,可以參考「你可能都不瞭解的JS變數祕密 - 一文了解無宣告、var、let、const變數細節」
(突然發現這也是你可能不知道系列呢XD)
你可能不知道的Web API--postMessage
前言
postMessage()
是少數可以讓兩個不同頁面交換訊息的方式。如其名,傳遞訊息,postMessage()
接收一段文字訊息,將這個文字訊息傳遞給通知的對象。通知的對象可以監聽message
事件獲取訊息。
關於postMessage()
實際上現在瀏覽器網頁API上,存在的postMessage()
API不只一個。有window.postMessage()
、Worker.postMessage()
、BroadcastChannel.postMessage()
和Client.postMessage()
,它們有著類似的使用方式。除了不同頁面溝通外,對於建立的Web Worker執行緒也有相似方式傳遞訊息,除Worker.postMessage()
外,在Web Worker環境下還可以建立BroadcastChannel
物件,使用BroadcastChannel.postMessage()
方法。至於Client.postMessage()
是在Service Worker使用的,有在寫PWA(Progressive Web Application,漸進式網頁應用程式)才比較會用到。
本節主要討論的是最基本的window.postMessage()
。
基本用法
基本上的用法可以傳遞一個字串作為訊息發送出去,像是window.postMessage("msg")
。然後監聽message
事件。所以我們可以做一個簡單的例子。
現在建立兩個頁面index.html
作為主畫面,sub.html
作為用iframe
嵌入在主畫面的子畫面。
主畫面內容如下,除了一個發送訊息的文字話框外,還有一個接收訊息的文字畫框,以及一個發送訊息的按鈕。
<!------ index.html ------>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<title>[DEMO] postMessage (Main)</title>
</head>
<body>
<form>
<textarea cols="30" id="msg" name="" rows="10"></textarea>
<br/>
<textarea cols="30" id="rev" name="" rows="10" disabled></textarea>
<br/>
<button>send</button>
</form>
<hr/>
<iframe frameborder="0" src="sub.html"></iframe>
<script type="text/javascript" src="main.js"></script>
</body>
</html>
相對來說子畫面就簡單一些,只有一個接受訊息的地方。它會將接受到的訊息原封不動的回傳回去。
你可能不知道的CSS Injection
前言
當你只是簡單地設置了一個CSP以後:
Content-Security-Policy: defalut-src 'self';
會發現誒~為什麼inline-script也不工作了?
<script type="text/javascript">console.log('Hello, World');</script>
要解決這個問題,同樣必須計算腳本雜湊值,然後設置新的內容安全政策:
Content-Security-Policy: defalut-src 'self';script-src 'sha256-2cq9aRSFdLqAC0FNx8cqcUjxA2Bmk5ZjlSvbIPQ1x/U=';
當然不止這種做法,還可以設置
nonce
等方式。
你可能不知道的內容安全策略(Content-Security-Policy, CSP)
前言
當我們知道了XSS,瞭解到對於外部資源的引用檢查是何等重要。那麼除了開發上需要注意意外,還可以怎麼做?
服務器設置CSP相關回應頭
CSP,全名Content Security Policy,也就是內容安全政策。這是為了告訴瀏覽器,這個頁面允許什麼行為,讓瀏覽器幫忙在檢查一次。與設個相關的主要有兩個Response Headers:Content-Security-Policy
和Content-Security-Policy-Report-Only
。
Content-Security-Policy
可以設定一些政策,當瀏覽器發現也面內容行為不符合這些政策的時候,就會被阻擋下來。
如果在一開始調整,有大量不確定受到影響的頁面,並不希望行為直接被阻擋下來,可以改先使用Content-Security-Policy-Report-Only
。當發現不符合政策的頁面內容時,先發送到後端的一個端點記錄下來。再所有被發現存在問題的頁面都已經修正後,再改成設置為Content-Security-Policy
。
繼續以前一個例子來用:A網站 http://a.127.0.0.1.nip.io:8000/index 有以下內容:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<title>CSP</title>
<link rel="stylesheet" href="http://b.127.0.0.1.nip.io:8000/xss.css" type="text/css" media="screen" />
</head>
<body>
<h1>Hello, World</h1>
</body>
<script type="text/javascript" src="http://b.127.0.0.1.nip.io:8000/xss.js"></script>
</html>
這次也真夠糟糕的,引用到兩份B網站 http://b.127.0.0.1.nip.io:8000 存在問題的內容–
/xss.css
和/xss.css
在部署的時候可以在伺服器回應的HTTP Response加入Header:
Content-Security-Policy: defalut-src 'self';
這次這兩個不安全的內容就會被瀏覽器阻擋下來。
如果確定外部資源內容是需要的話該怎麼辦?
你可能不知道的跨站腳本攻擊(Cross-Site Scripting,XSS)
前言
跨站腳本攻擊,英文Cross-Site Scripting,縮寫原本應該是CSS,但與階層樣式表–Cascading Style Sheets的縮寫相同,所以通常已X當做「交叉」的Cross,就變成是XSS。
在今天算是一個很嚴重的漏洞攻擊,因為有可能做到身份偽造,然後去進行資料竊取或破壞。但防禦跨站腳本攻擊,不單單只是前端開發工程師的責任,很大程度上也與服務如何部署有關係。
什麼是跨站腳本攻擊
跨站腳本攻擊,是在頁面上存在從其他來源引入的腳本,而這些腳本帶有惡意行為。要注意的是,腳本並不只是指JavaScript,HTML、CSS或其他資料內容也有可能是惡意注入的對象。
The malicious content sent to the web browser often takes the form of a segment of JavaScript, but may also include HTML, Flash, or any other type of code that the browser may execute.
惡意的JavaScript
比如說在A網站 http://a.127.0.0.1.nip.io:8000/index.html 有以下內容:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<title>XSS</title>
</head>
<body>
<h1>Hello, World</h1>
</body>
<script type="text/javascript" src="http://b.127.0.0.1.nip.io:8000/xss.js"></script>
</html>
在這個頁面中,使用了另一個網站B http://b.127.0.0.1.nip.io:8000 的JavaScript。但是其實xss.js
的檔案並不是自己可以掌控的,它的內容可能是:
你可能不知道的HTTP Header--If-Match和該怎麼設計Web API
前言
問個 假如我今天有個表單在前端要送,然後我想要先檢查這個表單有沒有被其他人更新
這是在一次和朋友的討論中有人提出來的問題。
恩…這是很典型資料競爭的問題,但並不是多個執行緒存取一份資料,而是多個瀏覽器客戶端(Browser Client)存取同份伺服器資源。
通常,我們在討論RESTful - 一種當下很常見的Web API設計方式的時候,只會提到建立(Create / POST)、讀取(Read / GET)、更新(Update / PUT)、補充(PATCH)和刪除(DELETE),也就是一般的CRUD。甚少會在深度討論一些細節,就如開頭提到的問題。並且通常在瀏覽網站,也很少去注意各個網站的API設計方式。
那麼這個問題要怎麼解決呢?
不管怎麼說,伺服器上的資料正確性最終需要由伺服器處理保護。 可以簡單一些在資料上添加版本(Version)或最後修改時間(Last Update time)的欄位。當收到更新或刪除訊息,該欄位應該保持一致。不過這就會出現一個很奇妙的現象:
> GET /data1
< 200 OK
< Content-Type: application/json
<
< {"name": "Bob", "age": 18, "version": 1}
當我們有一個資料data1
其版本為1
,如果我們需要更新的話,下面的訊息代表什麼意思?
> PUT /data1
> Content-Type: application/json
>
> {"name": "Alice", "age": 18, "version": 1}
< 204 No Content
這是一個更新,版本是不是應該跳到2
?但是按照前述這個版本應該要與記錄一致才會被修改,但是這樣似乎又與PUT
的意圖衝突?
或許有一些設計是檢查更新版本-1
是不是與當前記錄版本
相同作為檢查判斷條件。但是想想,其實版本號的決定,似乎不該由Client傳進的資料決定,而應該由後端資料管理行為決定。
那麼來介紹另一種設計方式,其實HTTP一般性規範都幫你準備好了,只是在我經驗觀察上,似乎並沒有那麼易用(也可能是因為了解的人真的不多),並不經常看到這樣的設計。這可能會包含一些你可能不知道的HTTP狀態碼(HTTP Status Code)和HTTP Headers。
你可能不知道的JS自動型別轉換
前言
如果要針對一個 物件陣列 取最大值該怎麼做?
一天,一位同事這麼問到。
首先需要先知道的是一般在JavaScript物件是無法比較大小的,所以這句話的意思是將物件特定屬性作為比較參考值,或是將物件數個屬性計算成一個可以比較的值後作為參考值,再進行比較尋找最大值。
起初,針對這個問題我第一想到的就是三種方式:
- 使用
Array.prototype.reduce()
- 使用
Array.prototype.sort()
- 使用
Math.max()
最後一個你可能會很好奇:物件無法比較,並且Math.max()
又不像Python的max()
可以輸入key
函式,這樣可以比較嗎?
沒錯,這個方式是存在問題的,但原因並不是因為「物件無法比較」,而是結果型別的問題,這個問題之後會提到。並且這也引發了今天的議題–「JS自動型別轉換」。
物件陣列怎麼取最大值?
這並不是今天主題的重點,也就不賣關子了,直接給出做法。
同樣拿Person類別為例子:
class Person {
name = "";
#birthday = new Date(); // 私有屬性
get birthday() { return this.#birthday }; // 調整處
addr = ""
get age() {
return new Date().getYear() - this.birthday.getYear();
}
constructor(name, birthday, addr = "") {
this.name = name;
this.#birthday = birthday; // 調整處
this.addr = addr;
}
hello() {
console.log(`Hello, ${this.name}.`);
}
}
現在有10個年幼不依的人:
var people = [
"Alice",
"Bob",
"Candy",
"Danel",
"Frank",
"Grant",
"Harry",
"Iris",
"Joe",
"Kevin"];
people = people.map(name => new Person(name, new Date(Math.floor(Math.random() * 100) + 1990, 1), ""))
現在如果希望找到年紀最大的,這裡提供幾種方式:
你可能不知道的JS物件私有屬性
前言
前幾年我曾經寫過「7天搞懂JS進階議題」系列文章,你可以在我的網站或是CoderBridge閱讀。其中在番外篇提到過「隱私成員」,在當時因為JavaScript並沒有隱私屬性的設計,所以想實現,當時使用了閉包和屬性描述器來處理。
時過境遷,在我寫完發表沒多久,ES11(2020)也正式推出了,其中就有關於私有屬性和私有方法的設計。根據Can I Use使用支援程度已經超過九成,也就是在今天主要瀏覽器除了一些舊版本和特別的瀏覽器外應該多數也都支援了。
原先JavaScript設計的問題
class Person {
name = "";
birthday = new Date();
addr = ""
get age() {
return new Date().getYear() - this.birthday.getYear();
}
constructor(name, birthday, addr = "") {
this.name = name;
this.birthday = birthday;
this.addr = addr;
}
hello() {
console.log(`Hello, ${this.name}.`);
}
}
var bob = new Person(/* name = */ "Bob",
/* birthday = */ new Date(2004/* year */,
1 /* mouth */,
1 /* day */),
/* addr = */ "臺灣")
console.log(`${bob.name}今年${bob.age}歲`); // Bob今年18歲
bob.hello();
bob.birthday = new Date(); // 變更生日
console.log(`${bob.name}今年${bob.age}歲`); // Bob今年0歲
如果設計了一個類別Person
有名字(name
)、生日(birthday
)、地址(addr
)等屬性,從嘗試性來說生日在出生以後就不能夠變了,但是上面程式碼片段當我們建立一個實例物件bob
後,依然可以變更生日。
你可能不知道的WebAuthN(FIDO)
名詞解釋
AuthN 和 AuthZ 分別是 Authentication 和 Authorization 的簡寫,也就是驗證和授權。
不光兩個英文字像…簡寫形式一樣容易讓人混淆🐷
不過比起全名或是 A12n 或 A11n ,這樣好分辨多了Orz…
那麼 WebAuthN 就是 Web 和 AuthN 的結合,也就是在 Web 上的身份驗證。這裡特別指的是由FIDO聯盟推出的FIDO2的其中一部分。特徵就是讓使用者在瀏覽器瀏覽網頁時,可以利用辨識、臉部辨識等方快速登入。至於FIDO是Fast IDentity Online的縮寫,也就是「快速線上身份識別」,也就不難理解聯盟成立的目的為何。
身份驗證的方式
在之前系列「用Keycloak學習身份驗證與授權」–淺談身份驗證與授權(1)和再談身份驗證與授權中,將整個身份驗證、授權到取得資源處理業務邏輯分成幾個部分看。
這次主要談的是WebAuthN,也就是身份驗證這一塊。能夠證明身份的方式通常又分成這麼幾種:
- 只有你知道。像是密碼、一次性密碼(OTP)、簡訊驗證碼等等。
- 只有你持有。像是汽機車鑰匙、硬體金鑰、手機手錶(手機或智慧手錶在附近自動解鎖)。
- 只有你天生是。指紋、虹膜、DNA。
你可能不知道cookie是怎被保存、保存在哪裡?
前言
再來是cookie最後,也是我認為最有意思的一部分–「cookie是怎被保存、保存在哪裡」 。
服務後端
對於開發後端服務的軟體開發人員,自己應當知道cookie存在哪裡,如何才可以將HTTP Request的狀態保留對應下來。而一般前端人員也不需要很清楚的知道cookie儲存在哪裡,只要認為儲存在瀏覽器內或記憶體內就好。
瀏覽器外存在哪裡
但是重開機後有一些cookie也存在,這表示必定有一個檔案儲存地方儲存這cookie。
以Google Chrome在Windows平臺為例,Cookie可能儲存在%LocalAppData%\Google\Chrome\User Data\Default\cookies
或%LocalAppData%\Google\Chrome\User Data\Default\Network\cookies
,這要看是什麼版本的若有在更新目前應該是後者。
這是以SQLite作為檔案型資料庫儲存的,因此可以使用對應的工具打開來看看:
你可能不知道cookie有些只能走特定道路(HTTP)
回信
當瀏覽器收到並儲存cookies後,在下一次的request就會將cookies跟著帶著送出去,回到Server。對於無狀態的HTTP,就可以使用這樣的方式讓Server回憶這個Request之前是誰,做到保留狀態。
同樣地接者調整前篇的程式碼片段。這次要將Server收到的Cookies直接作為Response的內容返回。先來添加需要的package:
from fastapi.requests import Request
然後添加一個endpoint–GET /cookies
,將Server收到的Cookies直接作為Response的內容返回:
@app.get('/cookies')
def cookies(request: Request):
return request.cookies
現在瀏覽 http://127.0.0.1.nip.io:8000/cookies 就可以收到傳給Server的Cookies作為JSON返回。
AJax with credition
你可能不知道cookie可以寄城市還可以分路段
預設收件區
Set-Cookie
就像寫信,除了內容當然還有收件區號。如果並沒有給Domain,就是當前的Domain。比如當瀏覽 http://localhost:8000/index1.html ,其記錄的Domain就是localhost
。
這個Domain就像是在寫信時填寫的區號,比如「桃園市楊梅區」的區號就是326
。
填寫收件區號
現在可以複製你可能不知道cookie是怎麼被製造出來的裡的DEMO程式碼內容,在此基礎上繼續修改,填寫收件區碼。
你可能不知道cookie是怎麼被製造出來的
前言
你是タコたち嗎?你喜歡吃cookies嗎?那麼你知道cookies是怎麼被製造出來的嗎?
cookie是怎麼被製造出來的
cookies是在瀏覽器儲存的小小資料片段,通常來說當瀏覽器發出request時,有可能同時將cookie發送出去。利用這個特性,可以將通常來說無狀態的HTTP保有記憶,做到登入功能、追蹤行爲等等。
雖然我們可以透過瀏覽器開發工具新增cookie。
你可能不知道的Function.prototype.bind()
前言
Function有三種用法,除了一般呼叫方式外,還可以使用Function.prototype.call()
或Function.prototype.apply()
方法。此外,Function還有一個很常見,偶爾會與後兩個用法混淆的方法–Function.prototype.bind()
。沒錯,這節就是要來說說Function.prototype.bind()
和另外兩者的差異,以及常見用法和你可能不知道的Function.prototype.bind()
。
<Fn>.call()
/<Fn>.apply()
和 <Fn>.bind()
的差異
由於過去其實我是寫過bind()
的相關內容的。所以我個人並不曾將三者搞混,蠻能區分用法上的不同的。不過在偶然幾次討論程式碼應該如何寫的過程中,發現偶爾會有人弄不清楚何時應該使用bind()
?何時使用其他兩者?
回頭看我過去所寫的,也蜻蜓點水的點到過call()
和apply()
。它們三者的參數形式確實有些像,特別是bind()
和call()
都接受一個thisArg
參數和多個參數展開。
所以Function.prototype.bind()
有甚麼不同之處嗎?
Function.prototype.call()
和Function.prototype.apply()
與一般函式呼叫寫在同一節裡,他們三著共同點是「會真的執行函示內容」。與他們不同的是Function.prototype.bind()
並不會真的執行函式,它會返回一個新的函式。
function helloWorld() {
console.log(`Hello World`)
}
helloWorld(); // 會印出 Hello World
helloWorld.call(); // 會印出 Hello World
helloWorld.apply(); // 會印出 Hello World
helloWorld.bind(); // 不會印出 Hello World。返回一個函式物件
新的函式物件與原本的可能沒有什麼差異:
var newFn1 = helloWorld.bind();
newFn1(); // 會印出 Hello World
雖然上面程式碼很像是直接賦值給變數,但還是有些差異。
var newFn2 = helloWorld;
newFn2 === helloWorld; // true。直接賦值的話是同一個函式物件
newFn1 === helloWorld; // false。使用bind()會產生一個新的函式物件,儘管它們用起來可能很像,但依然不同
直接賦值的話是同一個函式物件;相對來說,使用bind()
會產生一個新的函式物件。儘管它們用起來可能很像,但依然不同。
常見用法
在JavaScript裡面this
是一個特別的存在。它經常會有隱含綁定和隱含遺失的狀況。
為什麼你需要知道Function的三種用法
前言
在設計函式與呼叫函式前,或許得認識到一些限制,這些限制有可能造成需要使用不同的設計方式或呼叫方式。就來談一下一些在JavaScript語言裡的一些限制吧!
安全整數範圍
JavaScript裡關於「整數」是有範圍限制在的,按照規範這個值的範圍是(±2**53)
內,也就是-9007199254740991~9007199254740992
。這個值你可以透過Number.MIN_SAFE_INTEGER
和Number.MAX_SAFE_INTEGER
取得。
BigInt
在ES11後多加了一個基本類別BigInt
,儘管這個類型的使用方式和Number
並不相容1。但是在過去寫過的7天搞懂js進階議題中曾經使用過。如果你有需要超過-9007199254740991~9007199254740992
範圍的整數,可以考慮使用BigInt
。
陣列長度
Array
會需要留意:屬性.length
的最大值爲2**32-1
也就是4294967295
。
這意味著以下一些操作是會出問題的
var arr = Array(4294967296); // 超出最大範圍
{
let arr = Array(4294967295);
arr.push(0); // 超出最大範圍
}
此外,-1
的索引值並不是像Python會得到最後一個元素2。實際上經過以下操作:
var arr = [1,2,3,4];
arr[-1] = 5;
console.log(arr);
最後arr
的結果應該會像是:
[-1: 5, 1, 2, 3, 4]
另外.length
也不會算上-1
的索引值。
你可能不知道Function的三種用法
前言
原預計標題「你可能不知道的Math.max()
三種用法」。因為這是在調整Math.max()
時引發的話題。在這之後過了幾周,有另外一個同事詢問Function.prototype.call()
、Function.prototype.apply()
的差異。
因此,接下來將來看看「你可能不知道Function的三種用法」。除了一般的呼叫外,還有<Fn>.call()
和<Fn>.apply()
。試想已經有參數陣列args
:
var args = Array(15).fill(0);
args.forEach((arg, i, arr) => arr[i] = Math.floor(Math.random()*50));
如果要將args
傳遞給函式Math.max()
執行,通常可以這麼做:
Math.max(...args);
這相當於:
Math.max.call(null, ...args);
此外你還可以這麼做:
Math.max.apply(null, args);
這三種作法都可以得到相同結果:
Math.max(...args) === Math.max.call(null, ...args); //true
Math.max(...args) === Math.max.apply(null, args); // true
除此之外,因為Math.max()
的處理特性,恰好可以使用函式型開發方式中reduce
的概念,也確實可以使用args.reduce()
去得到與上面相同的結果:
Math.max(...args) === args.reduce((m, c) => Math.max(m, c))
接下來也會談到一些Array.prototype.reduce()
的事情。
你可能不知道Array.prototype.forEach()沒跟你說的事情
Array.prototype.forEach()
的用法
自知道Array
有forEach
的方法後,我自己是還蠻愛用的。
var names = ["World", "Bob", "Alice"]
names.forEach(name => console.log(`Hello, ${name}`))
並且與其他多數Array
支援的callback方法一樣,有多個很有效的參數:
var names = ["World", "Bob", "Alice"]
names.forEach((name, idx, arr) => {
console.log(`Hello, ${name}`)
arr[idx] += "."
})
console.log(names); // ["World.", "Bob.", "Alice."]
我們甚至可以用而外的thisArg
來處理某些事情:
var obj = {
"World": undefined,
"Bob": undefined
};
function checkHello(name) {
if (name in this)
return void (this[name] = "Yes");
return void (this[name] = "No");
}
var names = ["World", "Bob", "Alice"];
names.forEach(checkHello, obj);
console.table(obj);
結果:
name | result |
---|---|
World | Yes |
Bob | Yes |
Alice | No |
關於性能
在通常情況下,不會由瀏覽器處理大量的資料。通常而言forEach()
的需要時間基本沒有什麼差別:
你可能不知道的Call Stack
前言
Call Stack,中文「呼叫堆疊」,是一個很重要的概念。這並不是Web相關技術中特有的,不過為了解釋後續的內容,我決定安插一節說一下Call Stack的概念。
Call Stack
Stack 是一個先進後出(First-In-Last-Out / FILO)的資料結構。就像一本本書疊起來,然後只能一本本從最上面開始拿下來。
Call Stack 就是每次函式呼叫,都會將函數的環境狀態保存進Stack,函數的環境狀態通常叫做「Call Frame」,而儲存Call Frame的Stack,就是Call Stack。
最明顯的例子就是遞歸函式,比如:
function sum(accum, end) {
if (end === 0)
return accum;
return sum(accum + end, end -1);
}
sum(0, 5);
sum()
是將 0 到 end
之間的整數累加起來,且end
必須是大於等於0的整數。
當呼叫sum(0, 5)
的時候,Call Stack便會儲存這筆資訊
你可能不知道的即時更新方案:multipart/x-mixed-replace
multipart/x-mixed-replace
除了Polling、Long Polling、Server Send Event(SSE)和WebSocket以外,還可以透過multipart/x-mixed-replace
來更新資料。
multipart/x-mixed-replace
和Server Send Event(SSE)一樣,只能夠由Server單向傳送資料給瀏覽器。
不同的是它可能不能使用JavaScript處理更新的資料,但現在主流瀏覽器多數還是支援其中部分特性,這使得從前端部分實現非常簡單。
不再支持 XMLHttpRequest 中的 multipart 属性和 multipart/x-mixed-replace 响应。这是一个 Gecko 独有的特性,从来没被标准化过。你可以使用Server-Sent Events, Web Sockets (en-US)或者在 progress 事件中查看 responseText 属性的变化来实现同样的效果。1
Lab
資源
這次實驗會透過不斷讀取不同圖片,讓瀏覽器上不斷更新圖片內容。首先是圖片資源:
這些圖片資源名稱是: 1.jpg
、2.jpg
、3.jpg
。後續會輪流讀取回傳給瀏覽器。
前端畫面
<!-- index.html -->
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<title>即時更新內容 - multipart/x-mixed-replace</title>
</head>
<body>
<img alt="" src="/fake_video"/>
</body>
</html>
基本上這次連JavaScript都不需要寫,只要載入一張圖片資源即可。
你可能不知道的即時更新方案:WebSocket
WebSocket
WebSocket進一步解決了Long Polling會遇到的兩個問題:
- 取得Response後,需要在建立一次Reqeust。
- 僅能夠單向傳輸更新資訊。
不過WebSocket並不是超文本傳輸協定(HyperText Transfer Protocol,HTTP),但確實由HTTP開始的。因此首先是在瀏覽器發起Request之後,要進行協議的切換。Server會回傳切換資訊。
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: gx+UXBfX3qJcxachxkN/n8/3+WQ=
Sec-WebSocket-Extensions: permessage-deflate
Date: Sun, 04 Sep 2022 10:36:52 GMT
在這之後就可以進行雙向傳輸,當然以可以用於更新畫面資料。
優點
- 雙向傳輸
- 連線可以重複使用
缺點
實現複雜。不是所有瀏覽器都支援,不過現在主流瀏覽器基本支援。對於伺服器也有一定要求,在我經驗上許多免費服務器是無法使用相關技術的。
Lab
你可能不知道的即時更新方案:Server Send Event
Server Send Event
Server Send Event(SSE)解決了Long Polling會需要建立多次Request的問題。相比起Long Polling「取得Response後,需要在建立一次Reqeust」。Server Send Event在同一次HTTP連線中,由Server送出多次更新資料。
優點
連線可重複使用。
相比起Long Polling「取得Response後,需要在建立一次Reqeust」。Server Send Event在同一次HTTP連線中,由Server送出多次更新資料。
缺點
僅能夠由Server傳送訊息到瀏覽器的單向傳輸。
Lab
前端頁面
<!-- www-data/index.html -->
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<title>即時更新內容 - Sever Send Event</title>
</head>
<body>
<h1 id="content"></h1>
</body>
<script defer type="module">
const DEFAULT_TIMEOUT = 30000 /*ms*/;
const HEATBEAT_INTVAL = 5000 /*ms*/;
const contentEl = document.querySelector('#content');
let evtSource = new EventSource("/connect");
/* recive default message
evtSource.addEventListener('message', async (event) => {
console.log(`recive message: ${event.data}`);
})
*/
/* recive special event - update */
evtSource.addEventListener('update', (event) => {
contentEl.innerText = event.data;
});
</script>
</html>
前端頁面實現也算是簡單的,透過EventSource()
建立Server Send Event來源的連線。透過監聽message
或<event>
來取得更新資訊。
後端API
你可能不知道的即時更新方案:Long Polling
Long Polling
Polling 有兩個明顯的缺點:
- 就算資料沒有更新,也會有一次 Request / Response 的來回。
- 資料有了更新,也需要等待下一次的Request才會知道。
Long Polling解決了這兩個問題。
雖然行為概觀上與Polling一樣,是一次Request後得到Response,才再發出一個Request。但是Reponse的回傳可能會拉的老長,過了許久才回應,像彗星(Comet)一樣。也確實有一種方式就叫做彗星(Comet),Long Polling是他的變種1。
在Long Polling模式下,有一個長期連線,這個連線在資料一更新時,變會回傳Response,並結束此輪連線,然後再發起一次長連線請求,以做到即時更新的效果。
有一段時間在Facebook、Plurk可以見得此種方式。甚至現在還在Facebook、Plurk還是可以見得一些Request長時間沒有Response,不過我無法確定是否同為Long Polling。
下圖是Plurk的部份網路請求節圖。其中可以看到一個/connect
相關的請求,並過一段時間後才返回Response,隨後又建立一比連線:
你可能不知道的即時更新方案:Polling
靜態網頁 & 動態網頁
全球資訊網路(World Wide Web, WWW)最早用於學術研究機構,用於分享研究報告成果。起初常見型態為:資訊分享者自行建立Web伺服器,提供HTML靜態頁面,讓資訊受者透過瀏覽器取得資訊。
英國科學家提姆·柏內茲-李於1989年發明了全球資訊網。1990年他在瑞士CERN的工作期間編寫了第一個網頁瀏覽器。網頁瀏覽器於1991年1月向其他研究機構發行,並於同年8月向公眾開放。1
「資訊分享者自行建立Web伺服器,提供HTML靜態頁面,讓資訊受者透過瀏覽器取得資訊」這從現在來看,可能分成兩個不同維度的類型:
- Web 1.0: 資訊分享者自行建立Web伺服器。用戶只能單向被動的接受由權威內容服務提供商提供的內容,用戶大部分為內容消費者,而網站則由內容驅動2
- 靜態內容網頁。網頁內容並不會因為內容接收者的不同、時間地區的不同而有所不同。
隨後出現部落格平台、線上論壇,出現可以提供資訊內容的網站使用者。在此,網站內容不再簡單以HTMl方式儲存,更多的內容儲存於資料庫,由使用者提供,動態的提供給瀏覽器。這是 動態網頁 和 Web 2.0 。
此外伴隨者瀏覽器腳本語言與API的逐漸統一,確立了 ECMAScript / JavaScript 在瀏覽器的地位,結束了過往百家爭鳴的情況(不過現在還有一些原生支援特殊腳本語言的瀏覽器存在,但JavaScript已成主流)。 似動非動,吸引人眼球由JS、CSS建立動畫效果的網頁內容開始玲瑯滿目地出現。
1996年11月,網景正式向ECMA(歐洲電腦製造商協會)提交語言標準。1997年6月,ECMA以JavaScript語言為基礎制定了ECMAScript標準規範ECMA-262。JavaScript成為了ECMAScript最著名的實現之一。除此之外,ActionScript和JScript也都是ECMAScript規範的實作語言。3
其中也還有另一個令人興奮、今天廣泛使用的技術出現: Ajax(Asynchronous JavaScript and XML)。
在現今,Ajax技術主要以XMLHttpRequest
和fetch
兩個API呈現。也可能隱藏在jQuery4、Axios等比較高層次的套件函式庫之下。
這也使得由瀏覽器處理、在前端渲染生成不同內容的畫面成為可能。儘管這不是當前唯一的技術方式,但有許多別具特色的功能、前後分離的網站都依賴者這項能力。
接著就讓我們來探索幾個取得更新資訊的模式吧!
你可能不知道URL的路徑編碼Percent-encoding
前言
語言文字事件有趣的事情,英文由26個字母反覆組合,描述各種現象。中文永字八筆,構築千萬象形文字。在電腦的世界裡,一切的一切都是由無數個0和1所表示,你所從螢幕見到的任何文字、圖片、影片都是。在文字的表示法中,有一個最早誕生的編碼方式–ASCII Code。從某種角度來說,它也是在電腦世界中最安全的表示方式。
我們介紹過如何安全的儲存JavaScript程式碼而不受到編碼影響。如何在有限的字元中,在網頁HTML表達多國語言的文字。分享過DNS與電腦是如何認識IDN。這次,要來說說URL裡關於Path字段的編碼方式。
URL格式
當我們瀏覽https://bob:bobby@www.lunatech.com:8080/file;p=1?q=2#third
1,實際上可以將這個URL拆分成好幾個部分來看。
+-------------------+---------------------+
| Part | Data |
+-------------------+---------------------+
| Scheme | https |
| User | bob |
| Password | bobby |
| Host | www.lunatech.com |
| Port | 8080 |
| Path | /file;p=1 |
| Path parameter | p=1 |
| Query | q=2 |
| Fragment | third |
+-------------------+---------------------+
https://bob:bobby@www.lunatech.com:8080/file;p=1?q=2#third
\___/ \_/ \___/ \______________/ \__/\_______/ \_/ \___/
| | | | | | \_/ | |
Scheme User Password Host Port Path | | Fragment
\_____________________________/ | Query
| Path parameter
Authority
你可能不知道隱藏在Domain裡的編碼punycode
前言
2017年我有幸取得一年免費的「.台灣」域名。自此我也花了一點時間了解什麼是國際化域名(Internationalized Domain Name,IDN)。
在今天你可能也看過不少並非由英文、數字組成的域名,如果是中文或是你的母語,或許會感到一種貼切感,有些又比羅馬拼音更好記。但你知道的嗎?這可能是隱藏在網址列的假象。
國際化域名(Internationalized Domain Name,IDN)
國際化域名(英語:Internationalized Domain Name,縮寫:IDN)又稱特殊字元域名,是指部分或完全使用特殊的文字或非拉丁字母組成的網際網路域名1
早期的頂級域名只有.com
、.gov
、.org
、.net
、.int
、.edu
、.mil
。
這些頂級域名最初指的都是美國。後來才開始有了國家級域名如.tw
。加上作用後同樣視為頂級域名如.com.tw
。
隨後也出現了.jobs
、.info
、.tv
、.cc
等等為後綴的域名。
域名的功用性,個人感覺某種程度在降低。有些域名的申請越來越容易。
在「.台灣」後綴前,似乎就已經有非英文組成的域名了。譬如:https://中文.tw/ 。
現在,除了.tw
的後綴,還可以申請.台灣
(繁體)、.台湾
(簡體)的後綴。譬如: https://中文.台灣/ 。
國際化域名(IDN)可以提供一個好記、在地化的名字,有時候或許可以提升品牌形象。
國際化域名編碼(英語:Punycode)
不過,早期的DNS其實並不認得這麼多字。實際上這部分瀏覽器事先做了轉換了,像是 https://中文.tw/ ,在我使用的Mozilla Firefox Browser還可以短暫看到https://xn--fiq228c.tw/
。是的這就是 https://中文.tw/ 編碼後的結果。透過在瀏覽器網址列輸入xn--fiq228c.tw
,同樣可以進入 https://中文.tw/ 。
你可能不知道HTML Code Number
我們都知道的 <
和 >
如果需要在HTML裡面讓瀏覽器顯示<
和>
,可不能直接寫。你應該要這樣寫:
<p>
example: <input/>
</p>
來呈現內容
example: <input/>
其中lt
表示: LESS-THAN ;而gt
表示:GREATER-THAN。這是他們的符號名稱,同時還有號碼名稱。
可以使用10進制或16進制來表示這兩個符號,因此還可以寫成:
<p>
example: <input/>
</p>
<p>
example: <input/>
</p>
關於HTML編碼這件事情
與其他程式語言的原始碼儲存方式一樣,HTML文件是以純文字方式存在的。 這也就存在一個問題:如果不告訴電腦該用什麼編碼方式解讀它,就可能解讀成亂碼。
比如以下HTML內容使用Big5儲存:
<!doctype html>
<html lang="zh">
<head>
<meta charset="UTF-8"/>
<title>你好,世界</title>
</head>
<body>
<h1>你好,世界</h1>
<p>怎麼跟得上臺灣</p>
</body>
</html>
你可能得到這樣的結果:
你可能不知道在JavaScript裡的萬國碼
那時、天下人的口音言語、都是一樣。
他們往東邊遷移的時候、在示拿地遇見一片平原、就住在那裏。
他們彼此商量說、來罷、我們要作甎、把甎燒透了。他們就拿甎當石頭、又拿石漆當灰泥。
他們說、來罷、我們要建造一座城、和一座塔、塔頂通天、爲要傳揚我們的名、免得我們分散在全地上。
耶和華降臨要看看世人所建造的城和塔。
耶和華說、看哪、他們成爲一樣的人民、都是一樣的言語、如今旣作起這事來、以後他們所要作的事、就沒有不成就的了。
我們下去、在那裏變亂他們的口音、使他們的言語、彼此不通。
於是耶和華使他們從那裏分散在全地上.他們就停工、不造那城了。
因爲耶和華在那裏變亂天下人的言語、使衆人分散在全地上、所以那城名叫巴別。〈就是變亂的意思〉
– 創世記11
從一封亂碼郵件開始說起
自己個人很少書寫電子郵件,收到的郵件也多數是某種活動、告知事項或廣告類信件。 有些時候會收到一些需要回信或是主動發信件的情況。絕大多數文字語言上都不是什麼問題。但就是偶爾會有一兩封信件在眼前呈現的是我看不懂得符號。
最近,我就收到過一封信件…是亂碼。
會呈現亂碼的原因,有很大程度是顯示軟體錯誤的解讀了信件內容。這種情況可能可以透過取得原始信件資料,在使用其他編輯器和解碼方式打開顯示。
以信件來說,常見的分成兩種類型:
- 純文字
- HTML文件
特別是HTML文件,會有一段以<meta/>
標記內容使用的編碼方式(charset
)。
你可能不知道void也是一個運算子
void也是一個運算子
你可能不知道void
也是一個ECMAScript的保留關鍵字 (reserved keyword),也可能不知道它也是一個 運算子。
誒黑~!
因爲這張卡的效果就是:
什麼都不給
你可以像是一元運算子那樣使用它:
var a = 1;
a = -a; // -1
a = !a; // false
a = typeof a; // boolean
a = void a; // undefined
也可以將表達式用括號包起來,像是函式呼叫:
var a = 100
a = void(a) // undefined
實際上這個行爲更像是先針對表達式
(a)
求值,在使用運算子void
。過程中不存在一般意義上的函式呼叫。
這些那些你可能不知道我不知道的Web技術細節系列(前言)
前言
「這些、那些你可能不知道、我不知道的Web技術細節」,這是個我在這短短兩年實際工作上,遇到的一些原先並不清楚,甚至不知道的Web技術。
有一些或許是新的技術規範、有一些只是很冷門的技術知識、也有一些與實際實現有關。
你可能已經知道,也可能不清楚。邀請你與我一起探索這些冷門舊技術、鮮爲人知的新技術的技術細節喔~
【用Keycloak學習身份驗證與授權】系列目錄
本系列同樣發表於iThome體人賽 - 用Keycloak學習身份驗證與授權。
本頁後面還有一些小後記喔~
- 【用Keycloak學習身份驗證與授權01】Quick Start(1)
- 【用Keycloak學習身份驗證與授權02】Quick Start(2)
- 【用Keycloak學習身份驗證與授權03】淺談身份驗證與授權(1)
- 【用Keycloak學習身份驗證與授權04】淺談身份驗證與授權(2)
- 【用Keycloak學習身份驗證與授權05】什麼是Keycloak
- 【用Keycloak學習身份驗證與授權06】Keycloak的替代品
- 【用Keycloak學習身份驗證與授權07】什麼是OAuth
- 【用Keycloak學習身份驗證與授權08】OAuth 2
- 【用Keycloak學習身份驗證與授權09】再談身份驗證與授權
- 【用Keycloak學習身份驗證與授權10】深入OAuth 2
【用Keycloak學習身份驗證與授權33】Device Code(4)
這次應用使用PySide
來實現界面;qrcode
來產生需要的QR Code;並使用requests
來與身份驗證與授權伺服器的API溝通。現在透過pip
進行安裝需要的packages。
pip install PySide6 requests qrcode
其實本來可以考慮用electron.js,但是基於一些考量,最後決定使用PySide。
在昨天,透過Qt Designer建立了兩個需要的使用者界面,今天來實現邏輯部分。
建立Widget
在之前所設計的ui檔案分別是:example-device-code-app.ui
和login-dialog.ui
。這部分會分別將這兩份載入到類別內使用。所以同樣來建立兩個Widgets:ExampleDeviceCodeApp
和loginDialog
。
【用Keycloak學習身份驗證與授權32】Device Code(3)
本文接續device code(2)
現在已經知道了Device Code的登入流程了,那麼實際應用起來是怎麼樣的呢? 本片來實現一個可以使用Device Code Flow登入的應用。
使用者界面設計
首先,與「快速開始」應用相同,同樣需要一個顯式使用者資訊的地方,以及登入與登出的按鈕。
是的非常簡單。但悄悄先回到RCF8628,有一部分描述使用者界面的範本。該界面建議包含:操作說明、登入連接和user_code
。
【用Keycloak學習身份驗證與授權31】Open ID Connect & Social Login(2)
Keycloak Open-Id Connect
其實除了使用GitHub等社群帳號登入外,Keycloak也可以作爲Open-Id登入的提供者(Provider)。接著需要使用Keycloak本身來實現社群帳號登入,因爲這樣子可以看到更多細節。
建立新的Realm
現在,需要一個新的帳號系統。你可以在建立一個Keycloak伺服器,或是建立一個新的Realm。不同Realm的帳號系統是獨立不相干擾的,所以這裏就先建議一個新的Realm – G00gle
。
【用Keycloak學習身份驗證與授權30】Open ID Connect & Social Login(1)
因為略過了一些JWT格式細節分析。所以這部分也有部分不會好好提到
到目前爲止,爲何不同應用可以使用同一個帳號登入,已經在說明Client解釋過。這是在相關系統的應用下,那麼…沒直接相關的系統呢?譬如:使用Google登入。像這種使用不同帳號系統登入的方式,在Keycloak分成兩種。第三方系統登入,這篇僅會說明與 OAuth / Open-Id 相關的一種。如果你使用過Firebase、Auth0等服務,或是看過使用Google、Facebook、Microsoft、GitHub帳號登入的應用,對就是這類。這種社群帳號登入(Social Login)的方式,與前幾天提到的內容相關,而且可以在Keycloak實現。
鐵人賽只會實現而已,一些細節和更多的範例並不會提到。 (雖然原本就計劃寫)
Social Login 社群帳號登入
以GitHub帳號登入
【用Keycloak學習身份驗證與授權29】JWT權杖格式介紹(1)
總覺得…直接開始說明什麼是JWT格式來著。但感覺這樣會很無聊,不如我們從已經拿到的Token來看吧!
至今爲止,除了存取權杖(access_token
)、更新權杖(refresh_token
)外,還拿到過識別權杖(id_token
)。仔細看三者,都有兩個「.
」可以將權杖分成三個部份。
這些權杖都可以透過JWT.io去解析。總之先透過Password Grant Flow取得access_token
和refresh_token
,或是透過「快速開始」應用取得id_token
。
【用Keycloak學習身份驗證與授權28】Role
在帳號系統下,除了帳號本身與帳號群組外,通常還存在一個非常重要的部分–角色(Role),更有基於角色的存取授權方式(RBAC)。
寫到有點累了,沒意外的話之後是會提到RBAC
帳號如果代表一個人,這個人可能有多個角色身份。可能是個老師、主任、校長;可能是爸媽、叔姨;可能是員工、部長、處長、老闆,且可能有一群人擁有同一種角色。角色和帳號群組有點像,但在Keycloak是兩個概念。除此之外,在Keycloak還分成兩類型角色– Realm Roles 和 Client Roles 。
建立 Realm Roles
首先,你可以建立Realm共用的角色,像是員工、老闆等等較爲通用的角色。
點選在 Realm 選單下的 Roles ,然後再點選 Add Role :
【用Keycloak學習身份驗證與授權27】User & Claim & Profile
接著來看看爲什麼更新帳號資訊,在「快速開始」會有那些變化。
這與client scope和claim有關。關於後者之後會在詳細說說,而目前就先了解一下這個現象發生的原因。
首先,在我們取得token
的時候曾申明需要的scope
爲openid profile email
。其中profile
這個scope爲這次變化的主要原因。
來到Keycloak管理選單下的 Client Scopes ,然後找到 profile 。
接著將頁籤切換到 Mappers , 你會看見一堆與 User Attribute 有關的設定。
【用Keycloak學習身份驗證與授權26】User & Group
帳號(User)
基本訊息
接著來看看與帳號有關的設定。
在之前,已經建立過一帳號–bob
。過去學習實驗,也都以bob驗證身份。接著我們要來更新一下這個帳號。
首先看一下基本訊息:
來添加一些資訊:
- Email:
bob@fake.email
- First Name:
Bob
- Last Name:
Lee
- Email Verified:
ON
此外,可以要求使用者在必須做一些事情,譬如:驗證信箱、更新密碼、更新個人資訊等。
再次登入到應用–「快速開始」,可以看到有一些訊息也有些不同了。
【用Keycloak學習身份驗證與授權24】Clients
Client與一些安全相關的設定
在OAuth架構下的Client(客戶端)可以想象成是一個一個的應用程式。到目前爲止也已經建立過幾個Client:
這些Client有著自己的規則、資源、授權方式等。
可以複寫一些Realm的設定,包含產生存取權杖的方式。像是認爲RS256
簽名不夠,需要使用到RS512
:
【用Keycloak學習身份驗證與授權23】Realm
Realm,中文或許會翻作「域」,但基本很像是程式開發上,語言層面提供的包(package
)或是命名空間(namespace
)。或者可能可以更貼切的說是工作空間(workspace)。
你可以想象就像是一個企業、部門或是其他組織。有著相同的一些規範,同事們在同樣地工作空間生活、工作。但不同的企業、部門或是其他組織,可能會有類似的規範,但兩者不互相影響。
會特別有這個概念,是因爲Keycloak是可以建立多個Realm的。也就是,在同一間公司內,不同部門都可以有自己的Realm,制定部門自己的管理規範。或是特別爲外部客戶建立一個Realm,並制定特殊規範。
不同的Realm內,有著自己的帳號系統、密碼規範政策等。利用這個特性,之後也會用來更清楚的理解Open-Id。
你也可以同樣簡單視爲一個帳號資料庫、身份驗證伺服器。特別的是在會話成立期間,可以不需要再進行一次驗證,而這部分,會在提到Client時在多做說明。
如何建立一個Realm
要建立一個Realm是非常簡單。在之前也建立過「quick-start」這個Realm。也幾乎就只需要給個名字而已。
【用Keycloak學習身份驗證與授權22】Keycloak使用基本概念:前導
【用Keycloak學習身份驗證與授權21】在Flow這段小旅途外的風景
在這一小段路中介紹了Password Flow、Implicit Flow、Code Flow、Refresh Token Flow、Client Credentials Flow、PKCE、Device Code Flow。有些模式已經被發現可能有潛在風險,有些模式無法單獨使用。這或許還不是全部,至少到現在為止都還沒有提到過金融級應用Flow–CIBA。
Client Initiated Backchannel Authentication Profile(CIBA)
本小節也不會詳細介紹CIBA(Client Initiated Backchannel Authentication)。儘管CIBA現在階段還只是草案(Draft),但在Keycloak v15版本中已經可以使用。大概也已經確實有一些應用使用。
為什麼你不該繼續使用Implicit Flow?
在談到Implicit Flow時候,提到過:
將存取權杖暴露在使用者面前也不是非常好的做法
【用Keycloak學習身份驗證與授權20】Device Code(2)
光要完成這個範例就花了幾乎整整一天
做完後決定…來拆篇這第二部份,將有部份內容會在【實戰篇】展開。 今天就先來看看成果。
成果發表
【用Keycloak學習身份驗證與授權19】Device Code(1)
+----------+ +----------------+
| |>---(A)-- Client Identifier --->| |
| | | |
| |<---(B)-- Device Code, ---<| |
| | User Code, | |
| Device | & Verification URI | |
| Client | | |
| | [polling] | |
| |>---(E)-- Device Code --->| |
| | & Client Identifier | |
| | | Authorization |
| |<---(F)-- Access Token ---<| Server |
+----------+ (& Optional Refresh Token) | |
v | |
: | |
(C) User Code & Verification URI | |
: | |
v | |
+----------+ | |
| End User | | |
| at |<---(D)-- End user reviews --->| |
| Browser | authorization request | |
+----------+ +----------------+
Figure 1: Device Authorization Flow
The device authorization flow illustrated in Figure 1 includes the
following steps:
(A) The client requests access from the authorization server and
includes its client identifier in the request.
(B) The authorization server issues a device code and an end-user
code and provides the end-user verification URI.
(C) The client instructs the end user to use a user agent on another
device and visit the provided end-user verification URI. The
client provides the user with the end-user code to enter in
order to review the authorization request.
Device Code Flow這個與前面幾個特別不一樣。在之前,以往都是從登入開始,然後跳轉頁面回到App(Client)。也就是通常先有的是前端通訊,然後才是後端通信。
【用Keycloak學習身份驗證與授權18】PKCE
+-------------------+
| Authz Server |
+--------+ | +---------------+ |
| |--(A)- Authorization Request ---->| | |
| | + t(code_verifier), t_m | | Authorization | |
| | | | Endpoint | |
| |<-(B)---- Authorization Code -----| | |
| | | +---------------+ |
| Client | | |
| | | +---------------+ |
| |--(C)-- Access Token Request ---->| | |
| | + code_verifier | | Token | |
| | | | Endpoint | |
| |<-(D)------ Access Token ---------| | |
+--------+ | +---------------+ |
+-------------------+
Figure 2: Abstract Protocol Flow
PKCE模式
說穿了PKCE是基於Code flow的安全強化版。在整個過程前後添加了兩個動作–產生code_verifier
和code_challenge
,並在最後透過code_challenge
驗證code_verifier
。其目的有很大程度是為了建立前端通訊與後端通訊的關聯。
原先風險
那麼先來看看原本發生了什麼問題。
【用Keycloak學習身份驗證與授權17】Client Credentials
+---------+ +---------------+
| | | |
| |>--(A)- Client Authentication --->| Authorization |
| Client | | Server |
| |<--(B)---- Access Token ---------<| |
| | | |
+---------+ +---------------+
Figure 6: Client Credentials Flow
嘗試 Client Credentials flow
Client Credentials,這個模式有點特別。除了前面看到的它可能與其他模式並用以外,最特別的是,單純使用它,完全不需要資源擁有者參予。總之先來看看:
你可以使用RESTfer嘗試看看:
grant_type: client_credentials
client_id: oauth_tools
client_secret: <之前所產生的secret>
或是同樣可以透過OAuth.Tools嘗試看看。
【用Keycloak學習身份驗證與授權16】Refresh Token
+--------+ +---------------+
| |--(A)------- Authorization Grant --------->| |
| | | |
| |<-(B)----------- Access Token -------------| |
| | & Refresh Token | |
| | | |
| | +----------+ | |
| |--(C)---- Access Token ---->| | | |
| | | | | |
| |<-(D)- Protected Resource --| Resource | | Authorization |
| Client | | Server | | Server |
| |--(E)---- Access Token ---->| | | |
| | | | | |
| |<-(F)- Invalid Token Error -| | | |
| | +----------+ | |
| | | |
| |--(G)----------- Refresh Token ----------->| |
| | | |
| |<-(H)----------- Access Token -------------| |
+--------+ & Optional Refresh Token +---------------+
Figure 2: Refreshing an Expired Access Token
The flow illustrated in Figure 2 includes the following steps:
(A) The client requests an access token by authenticating with the
authorization server and presenting an authorization grant.
(B) The authorization server authenticates the client and validates
the authorization grant, and if valid, issues an access token
and a refresh token.
(C) The client makes a protected resource request to the resource
server by presenting the access token.
(D) The resource server validates the access token, and if valid,
serves the request.
(E) Steps (C) and (D) repeat until the access token expires. If the
client knows the access token expired, it skips to step (G);
otherwise, it makes another protected resource request.
(F) Since the access token is invalid, the resource server returns
an invalid token error.
使用refresh_token
取得access_token
接著是使用Refresh Token換取Access Token的流程。這大概是所有中最簡單的一個模式之一了。
但因爲先決條件是取得可用的 Refresh Token ,所以無法單獨存在。在RCF6749相關的流程圖中,關注的是G、H的部分。
至於一開始有什麼方式取得Refresh Token就非常的多。在已經介紹的密碼模式和code模式都有可能返回refresh_token
。
【用Keycloak學習身份驗證與授權15】Authorization Code
+----------+
| Resource |
| Owner |
| |
+----------+
^
|
(B)
+----|-----+ Client Identifier +---------------+
| -+----(A)-- & Redirection URI ---->| |
| User- | | Authorization |
| Agent -+----(B)-- User authenticates --->| Server |
| | | |
| -+----(C)-- Authorization Code ---<| |
+-|----|---+ +---------------+
| | ^ v
(A) (C) | |
| | | |
^ v | |
+---------+ | |
| |>---(D)-- Authorization Code ---------' |
| Client | & Redirection URI |
| | |
| |<---(E)----- Access Token -------------------'
+---------+ (w/ Optional Refresh Token)
Note: The lines illustrating steps (A), (B), and (C) are broken into
two parts as they pass through the user-agent.
Figure 3: Authorization Code Flow
Authorization Code是在 RFC6749第一個提到的流程,所以有時又被視爲 標準流程(Standard Flow) 。
它與前兩個流程很不一樣,分成 前端通訊(frontchannel) 和 後端通訊(Backchannel) 。不過,其實反倒是前兩個是所有模式裡的怪胎,在隱含模式下,後端通信並在前端通訊;在密碼模式下,根本不存在前端通信,資源擁有者需要高度信任客戶端(說穿了在前端通信下,資源擁有者也是高度信賴瀏覽器或代理(User-Agent))。
【用Keycloak學習身份驗證與授權14】Implicit (Legacy)
+----------+
| Resource |
| Owner |
| |
+----------+
^
|
(B)
+----|-----+ Client Identifier +---------------+
| -+----(A)-- & Redirection URI --->| |
| User- | | Authorization |
| Agent -|----(B)-- User authenticates -->| Server |
| | | |
| |<---(C)--- Redirection URI ----<| |
| | with Access Token +---------------+
| | in Fragment
| | +---------------+
| |----(D)--- Redirection URI ---->| Web-Hosted |
| | without Fragment | Client |
| | | Resource |
| (F) |<---(E)------- Script ---------<| |
| | +---------------+
+-|--------+
| |
(A) (G) Access Token
| |
^ v
+---------+
| |
| Client |
| |
+---------+
Note: The lines illustrating steps (A) and (B) are broken into two
parts as they pass through the user-agent.
Figure 4: Implicit Grant Flow
如果說password適用於原生應用環境(Native Application)下的話,接著就是適用於純前端環境。 在現在前後分離架構的情況,前端與後端連接並不緊密,甚至前端幾乎就可以視爲一個完整的應用。 因此將前端視爲授權框架下的「客戶端(Client)」也就不會太難理解。
【用Keycloak學習身份驗證與授權13】Password Grant (Legacy)
首先,先來看看直接使用帳號密碼授權的。
是的, OAuth 是有一個模式支援直接使用帳號密碼的。 與萬能鑰匙不太一樣的是,授權的結果仍然是由授權伺服器的權杖和資源伺服器決定。 儘管透過中央授權控制可以限制存取權杖可以做些什麼,但畢竟直接使用帳號密碼並不是特別好, 故在其他模式下都不適用時,才應該再考慮此模式。
+----------+
| Resource |
| Owner |
| |
+----------+
v
| Resource Owner
(A) Password Credentials
|
v
+---------+ +---------------+
| |>--(B)---- Resource Owner ------->| |
| | Password Credentials | Authorization |
| Client | | Server |
| |<--(C)---- Access Token ---------<| |
| | (w/ Optional Refresh Token) | |
+---------+ +---------------+
Figure 5: Resource Owner Password Credentials Flow
事前準備
安裝RESTer
【用Keycloak學習身份驗證與授權12】Flows這一小段路上路前注意事項
其實我原本是想要 RESTer 幹到底的哈😜。
今天有一點是插話的。考慮到接下來幾天的內容,所使用到的工具會有點多樣,所以行前做個提醒。
首先,你最好了解:
- HTTP Request / Response
- HTTP API (Web API)
- JSON
- BASE64
諾對於Postman這類工具有所熟悉再好不過。但接者幾天會使用:
- RESTer
- curl
有一些情況會直接使用。 - python
主要用於格式化JSON。
除此之外,如果熟悉Bash
的話同樣也有助於理解所有內容。此外還有可能會使用到 OAuth Tools 、 jwt.io 。(JWT的部分更有可能出現在之後關於Open-Id內容前後)
但其實,以上並非全部都是必須。最重要的是希望你能夠學習到OAuth本身的部分。
【用Keycloak學習身份驗證與授權11】OAuth 2
終於要來談談OAuth裡定義的細節了~
目前OAuth 2.0 一共定義了7種流程(flow)。在未來本系列可能稱之爲模式,不同模式適用於不同情況、不同環境。 就是因爲如此,OAuth才有高彈性的優勢。
OAuth 2.0 的可擴展性和模塊化是其最大的優勢之一,因為這使得該協議適用於各種環境。然而,正是這種靈活性導致不同的實現之間存在基本的相容性問題。當開發人員想在不同的系統上實現 OAuth 時,它提供的眾多自定義選項容易使人困惑。
本系列會介紹的模式包含:
- Password Grant (密碼模式)
- Implicit (隱含模式)
- Authorization Code (Code模式)
- Refresh Token
- Client Credentials (特殊密碼模式)
- PKCE
- Device Code
儘管 Implicit 和 Password Grant 被標記爲傳奇的(Legacy),但有時候仍然可能會使用到。重要的是你應該知道什麼情況應該使用什麼模式。同時記住,即使一個系統按照規範正確地實現了 OAuth,也不意味著該系統在實踐中就是安全的。
「OAuth 2.0 實戰」有一章決策圖可以幫助你決定使用什麼模式。但本系列應該不會提供。
【用Keycloak學習身份驗證與授權10】深入OAuth 2
喔不,其實今天還不會真正提到OAuth 2.0的深度內容。今天要來談談的是取得資源的細節。
使用帳號密碼,假裝自己是用戶
首先先試著想想看,如果你想要寫一支程式代替你處理某些事情。譬如:收信、發信。 更詳細的說,你寫了一個信件的客戶端(如:Thunderbird、Outlook)。 然後你會需要告訴這支程式你信箱的登入帳號密碼,由他去代替你收信、寄信。這個樣子就像是你把你所有的祕密都交給了它, 交給了它那把萬能鑰匙,而你完全信任這支程式。
其實這種狀況還真不少見。尤其在於你所申請的帳號,和使用的客戶端服務實際就是同一個時,這種行爲在正常不過。
但當它們是不同服務時,就可能出現問題了。你還能信任你提交的密碼不會被誤用嗎?不可能發生?
你可能有Gmail的帳號,你會很正常的使用Gmail的服務。但你知道Gmail除了自己本身外,它還可以幫你收其他信箱嗎?
比如說你還有ymail的帳號,但你更喜歡Gmail的界面,所以你希望使用Gmail來處理yamil的信件。這時候其實你就是告訴了Gmail 關於yamil的帳號密碼。相對的,也就是你應該是信任的Google的服務。
【用Keycloak學習身份驗證與授權09】再談身份驗證與授權
再談身份驗證與授權
現在,讓我們再一次把視線放到「身份驗證」和「存取控制」這些名詞身上。 在入門篇的「淺談身份驗證與授權」已經相當程度的解釋過各個名詞。 不過今天將要更關注在身份驗證與存取控制的細節上。
對於一個應用來說,最重要的是它的 業務邏輯 。 除了業務邏輯本身,為完成所需的工作,會需要取得必要之資源。這可能是一份檔案, 鏡頭、麥克風資源等不同種形式。
在 取得資源 過程中,也會有另外一層業務邏輯,也可能本身就是另一隻程式服務,對所需取得的資源,進行 存取控制 。
最後,爲了判斷是否具有存取該項資源的權限,有可能有必要進行 身份驗證或授權 。
【用Keycloak學習身份驗證與授權08】OAuth 2
這是入門篇的最後一天了,今天不會寫什麼內容,但來帶大家看個入門概念可用的工具 – OAuth 2.0 Playground。
OAuth 2.0定義了幾個flow,可用於不同情境下,由於後續會有更多詳細說明,所以今天只會帶大家初步認識,嚐鮮看看。
註冊帳號
點選 register a client and a user。別擔心,這是個隨時可以廢棄的帳號。你完全不用真的去記他,他也不會要求你提供什麼資訊。
然後你會得到一組帳號密碼。然後點選「open in new window」之後就可以按下「continue」。
【用Keycloak學習身份驗證與授權07】什麼是OAuth
先來回憶一下,何爲「授權」。試想像有一座宅邸,裏頭有無數房間。而你作爲這座宅邸的管家,擁有一把萬能鑰匙,可以開始宅邸內所有門扉。 此外,這把萬能鑰匙還有一個作用,就是產生出開啓特定門扉的鑰匙。 你可以產生出的鑰匙交給其他人,其他人就可以自由進出特定房間。這個動作就是「授權」。
OAuth 是一個開放標準的 授權協議 ,它允許 軟體應用 代表 資源擁有者 訪問資源擁有者的 資源 1。
OAuth是什麼?
【用Keycloak學習身份驗證與授權05】什麼是Keycloak
終於要來好好介紹一下甚麼是Keycloak了~
收先先來看一下Keycloak的基本資訊:
- 名稱: Keycloak
- 開發使用的程式語言: Java
- 公用: 單點登入驗證與授權工具
- 許可協議: Apache License 2.0
- 公開倉庫: https://github.com/keycloak/keycloak
- 官方網站: https://www.keycloak.org
- 撰寫當下最新版本: 15.0.2 (2021年8月20日)
在 快速開始 提到過起始畫面有一些細節:
【用Keycloak學習身份驗證與授權04】淺談身份驗證與授權(2)
實際上,在昨天已經將多數基礎都已經解釋過了,不過我想到還有一些東西可以再多做補充的。
對啦! 擔心彈藥不足,把一篇拆成兩篇來啦!👻
沒有身份識別的存取控制
在我們拆分的整個流程中分成:身份識別、身份驗證、授權、存取控制。但現在,你將Web App登出後再登入一次,你會發現「授權」的部分不見了! 但我們不會立刻來討論這個部分。先來說說身份識別。
不覺得,身份識別在整個流程之中非常雞肋嗎?也就只是將你這個「自然人」與系統中存在的「帳號」對應起來。 也確實如此,在這樣的拆分中,身份識別對於存取控制並不是必要的。在後來已MAC爲基礎發展的存取控制框架,也多不直接與帳號相關。
別擔心,之後會提到什麼是MAC(強制存取控制, Mandatory Access Control)。
不過還有一個更直接沒了這個流程的例子。在以「單人使用」作爲設計的系統之中,我們只需要拿到鑰匙就可以進行存取。
什麼?你說現在還有這種系統嗎?其實還真不少呢,加密上鎖過的壓縮檔案,上鎖的部落格文章。還有授權之後的流程,可能也不包含身份識別。
【用Keycloak學習身份驗證與授權03】淺談身份驗證與授權(1)
在「快速開始」的單元中,實際上已經完成了所有身份識別、身份驗證、授權和一些存取控制的流程。
在今天,將會來說說,這些看是相同,卻又有一些不同的名詞。
名詞認識
那麼首先,就先來認識各個名詞的英文吧!
名詞 | 英文 | 簡短說明 |
---|---|---|
身份識別 | Identification | 讓系統知道你是誰 |
身份驗證 | Authentication | 讓系統相信你是誰 |
授權 | Authorization | 允許他人存取某項資源 |
存取控制 | Access Control | 檢驗是否有資格存取某項資源 |
而我認爲軟體開發上,最重要的是 存取控制 。這直接關係到系統上的資源,但卻無法只存在存取控制,勢必需要身份驗證與授權的配合。這也是爲什麼我們很難將這些分開來看。
已「快速開始」爲例
現在,讓我們把視線重新放回剛弄好的「快速開始」。在這個範例中,已經多少碰觸到了每一塊的概念。首先是身份識別:
【用Keycloak學習身份驗證與授權02】Quick Start(2)
昨天,已經完成了一部分配置,且也已經可以建立帳號並登入了。
不過,這只能算是半套,而今天要在來完成另外半套。
你可以按照昨天的做法,重新建立一個新的Client。
只是注意在建立的時候,「Root URL」改爲: http://localhost:4200
。
今天,我們要自己實現一個前端網頁去的Web App,然後綁定Keycloak去做登入。
前置要求:
- 用Keycloak建立一個Client
- 網頁開發基礎知識(HTML/CSS/JavaScript)
- TypeScript的部分知識
- Angular知識(非必要)
調整Keycloak的Client配置
前面說過,Keycloak的Client實際上並不是真正的Client Application,只是做了一些關聯。 今天就要來 快速開始 個自己的Web App。而首先,需要先調整Client的關聯:
- 選擇「Clients」
- 找到昨天建立的「my-quick-start-app」,然後點選「Edit」
這此調整主要做兩個修改:
【用Keycloak學習身份驗證與授權01】Quick Start(1)
開始之前~2🎃。開完笑的~
但是想了許久,總覺的就這麼直接開始解釋各個名詞不太好。
想找個範例又有諸多擔心。
不如…先來快速開始做個範例!
快速開始將分成兩天。 今天會先跑過一次簡單的流程,明天才會寫一點程式。
這兩天看完後,依照需求,你甚至可以開始開發自己的應用。
那我們從Keycloak開始吧!
今天的前置需求:
- 只要裝好docker就好囉~
- 阿!對,你還要安裝個瀏覽器。
(不過你拿什麼在看本系列文章呢?)
透過Docker建立一個Keycloak應用
docker run -p 8080:8080 -e KEYCLOAK_USER=admin -e KEYCLOAK_PASSWORD=admin quay.io/keycloak/keycloak:15.0.2
這麼一條指令就可以開始這系列多數內容了(吧)。現在Keycloak會聆聽本機的8080 port。嘗試用瀏覽器開啓 http://localhost:8080 後,你應該會看到以下畫面:
【用Keycloak學習身份驗證與授權00】開始之前
這系列文章將帶大家探討軟體開發上,那些身份驗證與授權的相關議題。此外的話題還有身份識別、存取控制。 以目前諸多流行應用都以非單人使用的狀況之下,身份驗證與授權,幾乎是每位開發者都會遇到的題目。 不管你是串接OAuth、管理資源、寫後臺界面,甚至在最初應用的設計,幾乎都會扯上邊。 在業務邏輯之外,這或許會是相當重要的一部分。
關於身份驗證與授權,每一個部分都非常重要,也都可以分開來看,卻也非常難分開來看。
Because these three techniques are so closely related in most real applications, it is difficult to talk about them separate from one another. In particular, authentication and authorization are, in most actual implementations, inextricable. 1
就如同在Oracle上可查詢到的相關資料,這些部分儘管代表者不同概念,但彼此非常相關,實在很難分開來看。雖然如此,每一個部分都是非常龐大的內容,而本系列將會着重於授權控制與存取控制。在此之上會在探討近來已經非常普遍的OAuth 2.0、Open-Id、單點登入(SSO)和基於角色的存取控制(RBAC)
【30天Lua重拾筆記】系列目錄
本系列文章為 第12屆iT邦幫忙鐵人賽 參賽文章。
你可以同時於iT邦幫忙找到本系列目錄。
最全面的Lua入門學習…筆記草稿?No, No, No, No, No 在30天要所有東西提到貌似是不太可能了,但這將會是一個由淺入深的Lua參考筆記。會竟可能涵蓋所有Lua相關核心內容。
Lua非常小,有經驗的人甚至可以在幾小時內熟悉Lua核心基礎內容、幾周內使用進階功能。並且透過輕而小的Lua,或許可以從另一角度重視其他程式語言。是C、Lisp以外,我最為推薦學習的程式語言之一。
本系列包含內容:認識Lua、基礎型別、控制流程、進階概念、範例嵌入C/Java。從頭帶你了解Lua怎麼回事。
系列目錄
【30天Lua重拾筆記35】完賽感想與延伸閱讀
完賽感言
這系列文章在我3月當兵時就開始在規劃了,可是寫出來也還是和原本預計的差了蠻多的,看看我一開始預計撰寫的內容…
起初,我更是想說Lua這麼小,那應該可以非常完整的說明完所有部份吧!但越後面開始規劃每日的文章才發現…絕對會超過30篇。
而且更多時候是寫到一半,發現這邊不得不提到一下之後才會說明的部份。又或者是邊查資料邊寫,結果與預計寫的方向差了不少。又有些時候,因為對Lua已經有一些基礎了解,手打字追不上想寫的思路,或是大思路雖然有了,卻一直在某些地方腦袋打結…
這系列文章,有很多篇也比我預計寫的內容詳細了許多,比起最初可能只是各方面都提到一點點的筆記,儼然已經變成超過我預期內容的一些探討。自己撰寫收穫也頗多的,也還好目前並不算忙碌,才可以這樣做。若要給未來自己一些建議:
- 不管想到的是否完整、好壞,都可以先寫下來。
這次有許多已經先寫下來的小紀錄,最後都變成很常的文章或範例程式。有更多更是在其他文章撰寫下,也不停的修改。 - 不用多,先寫下小小的東西就好。當做種下一顆未來的種子,當未來有需要使用時,再來灌溉、發芽。
- 還是自己寫過、想過的東西,在未來更容以理解。
最後,感謝閱讀完本系列的文章的各位,本文章原本想要深入淺出,但中有諸多未能提及之處,只能在最後分享一些資源。
延伸閱讀
【30天Lua重拾筆記34】番外篇: Fengari - 一個JS實現的Lua,運行Lua在瀏覽器內吧!
幾年前關注過Moonshine和lua.vm.js,不過這兩個項目貌似沒什麼在更新了。Fengari這個這次到又是讓我為之一亮
Lua的實現真蠻多樣的,光是想讓Lua運行在瀏覽器就有不少,像是Moonshine、lua.vm.js、Starlight。有些使用JS;也有些利用了WASM、emscripten。Fengari是屬於前者的實現,是JS實現的版本。其除了可以在瀏覽器執行外,也提供了基於Node.js的執行器。這是一個蠻新興的項目,整體設計粗淺看來也相當不錯而且完整,今天會略微介紹一下,但建議可以先閱讀閱讀為什麼我們使用JS重寫了Lua?(英文)
Fengari是希臘文「月亮」的意思
於瀏覽器執行Lua
【30天Lua重拾筆記33】Java + Lua計算機
這是我前幾年作為學習/練習的例子。
看過與C交互後,接著來看看一個更實際應用的例子。不過不用C,來用Java。
為甚麼呢?Java自帶一個跨平台的視窗開發套組,本身也有豐富的函式庫可用,第三方函式庫也眾多,作為宿主語言是蠻好的標的。不過不直接使用原本的Lua,需要使用LuaJ。
其實最初只是因為Android App使用Java開發。而當時Android Studio編譯APK實在太慢,才會有為啥不能先用其他方式寫邏輯、開發,為啥不先嵌入一個腳本語言?才去做的一個練習,之後我使用的套件也確實有用於Android App開發上。
是的,如果你是正在開發Android,不管是以前主要用Java還是後來的Kotlin,今天應該都還是可以使用LuaJ。
LuaJ已經停滯好一段時間了,本次範例的為GitHub上另一個分支。基本上遵循Lua 5.2的規則,也就是本系列有部份是Lua 5.3、5.4的語法並不能使用於之。
目標
本次範例主要目標:一個含有GUI的簡易計算機。其包含:
- Java提供GUI界面,和部份功能。
- Lua處理邏輯。
- 一個擴展方法,能夠改變成品行為。
使用Lua撰寫邏輯
使用Lua的好處之一,在不確定要以和總平台開發前,是可以先使Lua撰寫邏輯,最後在嵌入到某個平台或框架。
【30天Lua重拾筆記32】進階議題: LuaRocks & LuaDist
LuaRocks
LuaRocks是類似npm、pip這樣的套件管理工具,你可以在上頭找到近4000個別人已經寫好的模組。
下載/安裝LuaRocks
在示例前,你需要先下載安裝好LuaRocks,若要下載其他版本,可以在這個頁面尋找看看。
在Linux上,你只需要將其解壓縮於可執行路徑底下即可。Windows可以參考官方說明。
使用LuaRocks
初始化環境
你可以不用做這一步。略過這一不的話,之後安裝得套件會存在於全局裡。有這樣的作法,主要是用於開發其他套件時使用。
mkdir example
cd example
luarocks init
搜尋套件
【30天Lua重拾筆記31】進階議題: 記憶體回收&弱表
TL;DR:
不要去修改預設值,除非你知道在做什麼
Lua會自己做記憶體回收,絕大多數時候不必為記憶體分配、管理而操心,而且通常它做的很好。但如果真的因為記憶體回收而影響到程式效率的執行,且你確定你有足夠的記憶體,你可以暫停讓Lua執行記憶體回收的操作:
停止收集記憶體垃圾
collectgarbage("stop")
collectgarbage("isrunning") --> false
或者,如果你是嵌入在C,可能會更傾向使用:
lua_gc(L, LUA_GCSTOP)
收集記憶體垃圾
如果要恢復,也只要:
collectgarbage("restart")
collectgarbage("isrunning") --> true
或是
【30天Lua重拾筆記30】進階議題: 與C交互(+Python)
Hello, Lua & C
現在,我們來嘗試從C去執行一個Lua程式,Lua程式就用最簡單的Hello,並命名為hello.lua
print "Hello"
然後來寫C程式 – hello_C.c
。
引入標頭檔
需要下載含有標頭檔和函式庫的版本
#include "lua.h"
#include "lauxlib.h"
建立Lua虛擬機
// new a lua VM
lua_State *L = luaL_newstate();
打開預設的所有函式庫
通常而言,不會全部開啟所有功能。
這只是範例,讓hello.lua
檔案擁有所有能力。
// open all libraries
luaL_openlibs(L);
執行Lua檔案
【30天Lua重拾筆記29】進階議題: 物件導向程式設計
與ECMAScript相同,採用原形設計的物件導向。你可以與7天搞懂JS進階議題相互服用,可能會有意想不到的效果。
模擬封裝
Lua並沒有直接保護內部資料的方法,你可能會需要使用 閉包 、 弱表 、 metatable 等來達成條件。
但今天只討論封裝裡最簡單的概念–集成,也是物件的基礎:將相關的物件、方法關連到一個結構裡面。沒錯,就是 table
,在本文中使用物件(object)一詞基本可視為同義。
Lua的
table
也確實很像是JS裡的Object
,但可能更像是Map
(一個ES6後出現的新類別)。
最基礎的資料結構概念是雜湊表。
建立物件1
建立物件(object
)就是建立表(table
),非常簡單:
coco = {
name = "桐生ココ",
age = 3501,
birth = {month = 6, day = 17},
slogan = "good morning mother f**ker",
say = function (self)
print(self.name .. ": " .. self.slogan)
end
}
coco:say() --> 桐生ココ: good morning mother f**ker
工廠模式
但我們總不能每次都這樣直接建立物件,或許可以由一個加工工廠(函數)來處理?
【30天Lua重拾筆記28】進階議題: Meta Programming
Meta Programming / 元程式設計
元程式設計(英語:Metaprogramming),又譯超程式設計,是指某類電腦程式的編寫,這類電腦程式編寫或者操縱其它程式(或者自身)作為它們的資料,或者在執行時完成部分本應在編譯時完成的工作。多數情況下,與手工編寫全部代碼相比,程式設計師可以獲得更高的工作效率,或者給與程式更大的靈活度去處理新的情形而無需重新編譯。 – 維基百科
簡單說,元程式設計,就是讓「程式能夠編寫程式」,改變程式運行的部份行為。Lua本身具有部份如此的能力,舉例來說,如果想要建立一組變數A-Z
,或許正常會這樣子寫:
A = 1
B = 2
C = 3
-- ....
Z = 26
但Lua可以有更聰明(hacking)的寫法:
for i=65, 65+25 do -- ASCII Code of A is 65
print(string.char(i))
_ENV[string.char(i)] = i - 64 -- 1 to 26
end
還記得_ENV的作用嗎?
【30天Lua重拾筆記27】進階議題: debug
Lua本身並沒有獨立的debugger相關工具,但他有一個強大的內置套件— debug。
打印調錯訊息traceback
debug = require "debug"
do
local ten = 0
function div10(n)
print(debug.traceback()) -- 打印調錯訊息
return n / ten
end
end
更新變數值
透過直接觀看函式內容,可以知道這個函式div10
並不符合預期。
do
local ten = 0
function div10(n)
return n / ten
end
end
所以需要改變ten
值,但這次不直接修改程式原始碼。我們得先了解怎麼取得閉包變數ten
:
【30天Lua重拾筆記26】進階議題: 錯誤處理
作為一個寄宿型的嵌入式語言,Lua設計更傾向由宿主語言(通常是C)處理錯誤。
但是可以在保護模式下,執行函式,並檢查函式是否執行成功。
很像是Go語言。這就是Lua的錯誤處理基本方法。
錯誤處理
作為一個寄宿型的嵌入式語言,Lua設計更傾向由宿主語言(通常是C)處理錯誤。
num = 10
str = "string"
print('Hello, Lua')
result = num / str -- Error: attempt to div a 'number' with a 'string'
print('here will not show, because error hapend before')
一般來說,並不傾向於Lua處理最後,因為數字與字串相除,所引發的錯誤。這個錯誤會持續引發至宿主語言,由宿主語言進行錯誤處裡。
【30天Lua重拾筆記25】進階議題: 模組化
Lua並沒有完整的模組系統,更多的是依賴模組開發者的設計。在Lua 5.1曾經有module()
的函數可用,但於Lua 5.2已經被移除。更多的需要使用_G
、_ENV
來使用,相關說明可以參考全局表與環境表。
module (name [, ...])
在require()
之前,需要先說一下如何載入程式檔案並執行。
載入檔案
對於模組化設計,最重要的一點是如何切割程式為不同檔案,接著就是如何串連不同檔案。在之前介紹過load
,Lua提供了另一個類似的方法loadfile
。
示例
首先先建立個 hello.lua 檔案,簡單就好:
print "Hello, World"
然後才是主程式 loadfile_hello_example.lua :
hello = loadfile("./hello.lua")
hello() --> Output: Hello, World
如果你分別於REPL環境打入這兩行指令,你會發現第一行指令並沒有任何輸出結果,只得到一個hello函數。只有在第二行執行函式時,才真正執行hello.lua程式檔案的內容。
執行檔案
【30天Lua重拾筆記24】中級議題: coroutine
coroutine
Lua提供coroutine
的函式庫,使其有能力編寫不同模式的程式。
thread create
你可以透過coroutine.create()
建立一個thread
。
t1 = coroutine.create(function() print("Hello, World") end)
print(type(t1)) -- Output: thread
Lua並不是多執行緒的,其thread
是輕量的。儘管Lua本身沒有異步(async)的寫法,但可以創造出異步的寫法。相對而言,值執行順序是明確許多的。
thread running
可以透過coroutine.resume()
去觸發一個thread
的執行:
coroutine.resume(t1) -- Output: Hello, World
傳入參數
可以對一個剛建立好的thread
傳入參數。
function hello(name)
while true do
coroutine.yield("Hello, " .. name)
end
end
t1 = coroutine.create(hello)
print(coroutine.status(t1)) -- suspended
print(coroutine.resume(t1, "Bob")) -- Output: Hello, Bob
print(coroutine.status(t1)) -- suspended
print(coroutine.resume(t1, "World")) -- Output: Hello, Bob
thread close
【30天Lua重拾筆記23】中級議題: 閉包
變數的查找
對於一個變數,Lua會先嘗試從當前詞法環境(Lexical)尋找,再從當前環境中尋找(
_ENV
)。
那的對於區塊變數呢??
function parentFunction()
local L1 = 100
local function childFunction()
print("here is child")
end
return childFunction
end
f1 = parentFunction(100)
f1() -- Output: here is child
上例中,區塊變數L1
沒有任何人可以存取的到,簡直消失在了時空夾縫之中。
詞法環境(Lexical)
詞法環境(Lexical)指的是程式編寫當下,執行區塊由內而外,可以見到的範圍。
隱藏區塊變數
在Lua變數預設值為nil
。透過查找規則,可以暫時隱藏一切區塊變數。
【30天Lua重拾筆記22】中級議題: 全局表(_G)、環境表(_ENV)
_G
和_ENV
在Lua有兩個特殊變量–_G
和_ENV
,其分別表示全局環境和當前環境。_G
在與C交互時,另有作用。但大致上你可以將兩者視為相同。實際上,在Lua環境建立之初,兩著也確實是相同的:
print(_G == _ENV) -- => Output: true
先前說過,Lua只有表(table
)這個複合結構。而_G
和_ENV
也是table
結構。_ENV
中包含一個_G
的key
,其值指向_G
,而起出_G
就是_ENV
。像是一個咬尾蛇(Ouroboros)
print(_ENV._G == _ENV) -- => Output: true
print(_G == _ENV) -- => Output: true
print(_G._G == _ENV) -- => Output: true
print(_G._G == _G) -- => Output: true
變數查找
對於一個變數,Lua會先嘗試從當前詞法環境(Lexical)尋找,在從當前環境中尋找(_ENV
)。這意味著你可以準備一個乾淨的執行環境執行函數。
【30天Lua重拾筆記21】基礎3: 再看pairs, ipairs
ipairs()
的行為
iparis會嘗試從索引1開始迭代表(陣列),直到其值為nil
。所以很像是:
arr = {1,2,3,4,5}
function my_ipairs(t --[[table]])
local i = 1
while t[i] ~= nil do
print(i, t[i])
i = i + 1
end
end
my_ipairs(arr)
1 1
2 2
3 3
4 4
5 5
也因此其如果有斷值,會在一半中止:
arr = {1,2,nil, 4, 5}
my_ipairs(arr)
1 1
2 2
pairs()
和next()
【30天Lua重拾筆記20】基礎3: 複合結構 - table
Lua只有一個原生的複合結構 – table
。實際上陣列是table
的特例。
陣列是table
的特例
arr = {1,2,3,4}
print(type(arr)) --> table
建立表(table
)
table
對應於:
程式語言 | 型別 |
---|---|
python | dict |
js | object or Map |
這個結構幾乎是現代高階語言必備的基礎。其又稱作hash-table
,是一個鍵值對(key/value)的結構。在Lua之中,其key可以是除了nil
和NaN
(Not a Number)以外的任何型別。
key值值域
obj = {} -- 建立一個空表
obj[1] = 1 -- 整數是合法的key值
obj[1.0] = 2 -- 浮點數是合法的key值
obj["string"] = 1 -- 字串是合法的key值
obj[math.huge] = 1 -- inf是合法的key值
--[[
要注意的是 obj[1] 和 obj[1.0]相同
其obj[1]和obj[1.0]最終值為2
--]]
----------------
print(obj[nil]) --> nil
obj[nil] = 1 --> Error: nil不是合法的key值,儘管取值不會出錯
print(obj[0/0]) --> nil
print(0/0) --> -nan
obj[0/0] = 1 --> Error: NaN不是合法的key值
value值值域
其value可以是nil
以外的值。
nil表示該key不存在,亦可視為刪除該key
obj[1] = nil -- 刪除`obj[1]`
print(obj[1]) --> nil --表示obj[1]不存在
【30天Lua重拾筆記19】基礎3: 陣列從1開始
建立陣列
關於陣列,其實也已經看過了。不過其實陣列還有兩個祕密,一個今天會揭露,另一個等等明天。
要建立一個陣列很簡單,很像C語言,只是把中括號[]
改成大括號{}
,像是:
programming_language = {
"C",
"C#",
"C++",
"Java",
"Swift",
"Python",
"Haskell",
}
陣列取值
陣列從1開始
與C語言一樣,使用下標運算取陣列之中的值, 要注意的是,Lua陣列從1開始
print(programming_language[1]) -- => Output: "C"
陣列長度
可以使用長度運算#
,取得陣列長度:
print(#programming_language) -- => Output: 7
迭代陣列
知道陣列取值方式,也知道陣列長度後,就可以來迭代陣列:
一般for迴圈方法
for i=1, #programming_language, 1 do
print(i, #programming_language[i], programming_language[i])
end
看我小巧思,每個值的長度,正好與其對應的索引值相同。
for-in + ipairs() 方法
可以寫的更簡單點:
【30天Lua重拾筆記18】基礎2: 應該知道的(總集+補充)
關於變數
- 值(value)有型別;變數(varible)沒有
- 基礎型別一共有8個
nil
boolean
number
string
function
userdata
thread
table
- table是唯一的複合型別(之後會提到)
- Lua 5.4之後,可以為變數標記屬性。Lua 5.4支援
const
和close
兩種屬性 - 變數預設是全局變數(實際上與特殊變數
_ENV
和_G
有關,之後會提到) - 變數值型別會自動轉型,但不該依賴
數字
- 整數和浮點數會自動轉換
- 整數有範圍,有溢位問題
- 浮點數也有精度丟失的問題
布林
- 只有
nil
和false
為假,其他為真 0
、空字串、空表亦為真
控制結構
- 有label &
goto
- label只屬於當前函式區塊
- 使用
elseif
,而非else if
(這點我覺得C就設計的比較漂亮)- 嵌套使用
if
(else if
),記得補上end
- 嵌套使用
- 非強求,但最好做好排版
【30天Lua重拾筆記17】基礎2: pcall, xpcall, load (eval, exec, apply)
eval / load
作為一個直譯的環境,幾乎一定會有一個與eval
等價的能力,不過在Lua叫做load
,與其他程式相同,這個功能是強大而應該小心使用的。下方程式會新建一個全局變數g1
,使這個宣告過程原本是一段字串。
load([[
g1 = 1 -- create a global variable g1
]])()
print(g1) -- => Output: 1
要注意的是,load
不回直接執行,其實其返回一個包裝函式:
f = load[[g2 = 2]]
print(type(f)) -- => function
print(g2) -- => Output: nil
f()
print(g2) -- => Output: 2
ECMAScript
類似的JS也有。
不過相較於JS,Lua其實有更安全的作法(以後會提到)。
eval('var g1 = 1')
console.log(g1) // => Output: 1
Python
Python有兩個函式做類似事情–eval
和exec
。不過eval
雖有返回值,但無法執行語句,只能執行表達式。所以本例中只能使用exec
。exec
永遠回傳None
,比起eval
更加強大。
【30天Lua重拾筆記16】基礎2: 多值返回&具名參數
回傳多值/多值返回
Lua函數可以返回多值。在我看來,這個特性是特殊的,只有少數語言真正做到多值返回。什麼意思?這表示在接收一個函數的返回值,可以輕易忽略掉主要值以外的結果。這些結果一般用於輔助,絕大多數時候,我們不關心、不需要,但有時候非常有作用。
先來看看Python裡的dict.get:
_d = {}
_v = _d.get("key", "value")
print(_v) # => Output: value
大多數時候,並不關心_v
的值是否真的從_d
來的,還是其實是個預設值。如果我們想要確定,那就要在多寫一次:
found = "key" in _d
我們可以替Lua寫一個相似的函數,但其還多返回一個用於解釋是否是有找到的值:
function get(dict, key, default)
if dict[key] ~= nil then
return dict[key], true
else
return default, false
end
end
其實可以在簡化一些:
function get(dict, key, default)
return (dict[key] or default), dict[key] ~= nil
end
這麼一來,平常可以這樣用:
【30天Lua重拾筆記15】基礎2: Label and Goto
Label & goto
這是一個強大的工具,要寫的漂亮並不容易,許多語言禁止了他。
Lua保有他。他很靈活,但你也應該慎重考慮是否真的應該使用。
而我認為,開發人員應該要是自由的,這才有創造的價值,這才能體現思考的藝術。就如同松本行弘說的:「語言體現思考的價值」
如果你的想法最初就是這樣想的,就先寫下來吧!「童子軍原則」別忘了逐漸改得更健壯。
Lua保有C/C++裡,goto
的能力,可以跳轉到函數區塊內任意可見的標籤(Label)。標籤的形式由兩個冒號夾成:
:: <Label Name> ::
跳出多層迴圈
BTW, Python 認為如果你需要使用到這個功能,表示你應該在拆分程式碼。
有了這強大的能力可以做啥?可以學像ES6那樣跳出外部迴圈:
九九乘法表只計算到6x4
(()=>{
outer:
for(let j = 2; j <= 9; j++){
let line_output = ""
for(let i = 1; i <= 9; i++){
line_output += `${j}x${i}=${j*i},\t`
if(i > 3 && j > 5){
console.log(line_output)
break outer
}
}
console.log(line_output)
}
})()
【30天Lua重拾筆記14】基礎2: 控制-while、repeat迴圈
print("鐵人賽開始")
for day=1,30,1 do
print("第" .. day .. "天: 每天寫文章")
end
print("鐵人賽結束")
for
迴圈適合用於次數明確的情況。例如鐵人賽30天發文不間斷,每天發一篇,發完30完賽。向這樣知道進步條件(每天一篇),中止條件(30篇),就非常適合用 for
迴圈。
Repeat/until迴圈
但有時候只知道中止條件,過程並不確定。像是在第n天,就寫n篇文章來囤稿,對我而言會是幾天完賽呢?
print("鐵人賽開始")
day = 1 -- 第幾天
completed = 0 -- 完成篇數
repeat
completed = completed + day
print("第" .. day .. "天: 每天寫文章,完成"..completed.."篇")
day = day + 1
until completed > 30
print("鐵人賽結束,完成篇數:"..completed) -- Output: 鐵人賽結束,完成篇數:36 -- (1+8)*8/2
鐵人賽開始
第1天: 每天寫文章,完成1篇
第2天: 每天寫文章,完成3篇
第3天: 每天寫文章,完成6篇
第4天: 每天寫文章,完成10篇
第5天: 每天寫文章,完成15篇
第6天: 每天寫文章,完成21篇
第7天: 每天寫文章,完成28篇
第8天: 每天寫文章,完成36篇
鐵人賽結束,完成篇數:36
需要每天寫文章,直到累積的文章超過30篇,也就完賽了。如果在第n天寫n篇,也只需要8天就完賽。
While迴圈
【30天Lua重拾筆記13】基礎2: 控制-For迴圈
相較於if
,Lua的for
迴圈有兩種,或說是三種。
進步的for迴圈
印出1-10:
for i = 1, 10, 1 do
print(i)
end
很類似C語言
#include <stdio.h>
int main(void){
int i = 0;
for(i = 1; i <= 10; i++){
printf("%d\n", i);
}
}
不同的是以逗點代替分號,中止條件相同時仍會在執行一次,進步(step)使用表達是,會自動相加並重新賦予值。
了解後也可來寫成10到1的程式:
for i = 10, 1, -1 do
print(i)
end
for-do
也是一個區塊,於一開始宣告的變數僅do-end
內部可見。這表示下方寫法中,進步條件指的是外部的i
,其值為1
。結果與輸出1-10相同。
i = 1
for i = 1, 10, i do
print(i)
end
pairs & ipairs初探 - for-in迴圈
for-in
迴圈用於迭代陣列(array
)或表格(table
)時。
Lua是沒有array型態的(還記得8種基礎型別嗎),關於這點之後會在說明。
至於什麼是table
?可以想像成是ES6裡的object
,或者更接近於Map
可以分別用ipairs
和pairs
來處理:
迭代陣列
【30天Lua重拾筆記12】基礎2: 控制 - 條件
分支條件控制 - if/elseif/else
Lua的分支控制條件就僅有這麼一組:if-then
/elseif-then
/else
/end
和其他語言一樣,elseif
可以出現多次。你想用else if
寫成巢狀也沒人會管你。他們兩個寫起來也真的蠻像的,除了你可能要多寫幾次end
:
if true then
print("if block")
elseif true then
print("elseif block")
else
print("else")
end
if true then
print("if block")
else if true then
print("elseif block")
else
print("else")
end
end
注意到最後有兩個end
。實際上我試故意將他這樣排版的,這樣兩個看起來比較像。
通常需要使用到巢狀分支,還是好好縮排比較好。
switch
雖然只有if
蠻簡單的,但有時候會想要使用switch
這類結構。沒事,可以模擬:
option = "one"
switch = {
["one"] = function () print "run one" end,
["tow"] = function () print "run two" end,
}
switch[option]() -- => run one
【30天Lua重拾筆記11】基礎1: 註釋
基礎2: 註釋
--[[
{
author = "lagagain",
date = 20200904,
title = "Comments of Lua code",
}
--]]
註釋是一段永遠不會被執行的區塊。你可以在註釋區塊隨意寫任何東西,而不必遵守任何Lua的語法規則。但更好的,註釋應該是作為程式碼的部份補充。至今為止的幾天,其實也過不少註釋。像是:
------------------
阿勒? 😲 這不是分隔線嗎?
單行註釋
是的,最基本的單行註釋以 --
開頭,直至行尾都會被視為註釋。也看過:
do
local a = 1
local b = 2
local sum = a + b -- sum = 1 + 2 => 3
end
或是
print("Hello, World") -- Output: Hello, World
本系列會部份以這樣方式去說明程式碼,程式碼的執行結果。不過在寫註釋時,最好是明確的補充程式碼的意義,像是:
【30天Lua重拾筆記10】基礎1: 類型 - 布林和nil
nil
nil
是Lua裡的一個特殊值,代表什麼也沒有。其型別也是nil
type(nil) -- => nil
布林
布林值只有true
和false
真值
只要不是nil
或是false
都為真, 包含0、空表、空字串 。
這與目前多數主流語言不同,需要特別注意!
【30天Lua重拾筆記09】基礎1: 類型 - 函數
函數 宣告
函數可以使用function
來做宣告,並以end
結束。
function hello()
print("Hello, World")
end
實際上這是Lua的語法糖,上面相當於:
hello = function()
print("Hello, World")
end
函數也是第一公民
在Lua,函數是第一公民,是基本型別,可以榜定於變數,也可以作為參數傳遞,或是回傳 值。
function genFun()
return function()
print("Hello, World")
end
end
function callFun(f)
f()
end
local _f = genFun() -- # get a function
callFun(_f) -- print("Hello, World")
函數、區域變數與閉包
函數同時也是一個區塊,可以保有區域環境變數,這樣的特性可以用來做閉包。
【30天Lua重拾筆記08】基礎1: 類型 - 字串
關於字串
與Python相同,字串是不可變得。但Lua字串於內部表示時,完全採用8-bits表示,包含0(\0
)。這也是為什麼在基礎1: 變數一篇,會特別說明\u{1F603}
的使用。
所以字串如果取其長度,可能和你想像的不一樣:
-- note: with UTF-8
print(string.len("我")) -- equal print(#"我") => 3
不過Lua是認得UTF-8的,其本身可以協助不支援UTF-8的程式語言(ex: C語言)。
-- note: with UTF-8
print(utf8.len("我")) -- => 1
C90包含<wchar.h>; C11有<uchar.h>。
儘管如此,不表示就是UTF-8寬字元(Wide character) 是電腦抽象術語(沒有規定具體實現細節),表示比8位元字元還寬的資料類型。不同於Unicode。 – 維基百科
因為Lua認得UTF-8,所以其原始碼最好以UTF-8儲存,否則可能執行會與想像不同。
字串的表示式
要表示字串在Lua有多種方式。
字面字串
簡單表示
【30天Lua重拾筆記07】基礎1: 類型 - 數字
整數與小數
數字(number)是Lua的基礎型別之一。Lua會自動判斷是整數還是小數,會自動轉換,無明確分界。
1.0 == 1
list = {"Bob", "Lua", "Luna", "Selene"}
list[1] == list[1.0]
type(1.0) -- => number
type(1) -- => number
雖然會自動轉換,不代表沒有區別
math.type(1.0) -- => float
math.type(1) -- => integer
溢位(overflow)
Lua 5.4使用64位元的整數和浮點數(雙精度浮點數double)。
math.type(9223372036854775807) -- => integer
math.type(9223372036854775808) -- => float
這表示Lua還是有可能溢位
9223372036854775807 + 1 -- => -9223372036854775808
精度丟失
浮點數的表示也有極限,可能會有精度丟失的問題
【30天Lua重拾筆記06】基礎1: 變數
變數名稱
Lua的變數名稱可以是底線(_
)或是任意字母([a-zA-Z]
)開頭,不能是數字或其他字元。之後的組成可以包含數字([0-9]
)、字母([a-zA-Z]
)或底線,並且是大小寫敏感,abc
、Abc
、ABC
可以作為三個不同的變數。
Lua作為嵌入型語言,使用最基本的ascii code。儘管其本帶有utf8函式庫,也可能有部份實現允許其他字元作為變數名稱,但不建議這樣做,並且在使用到非ascii字元時,最好以\u{1F603}
這樣方式來寫。像是:
print("\u{1F603}")
print('😃')
上面兩著等價,或是下面兩者亦相同。
print("\u{4f60}\u{597d}\u{ff0c}\u{4e16}\u{754c}\u{ff01}")
print("你好,世界!")
當然其實Lua是了解utf-8的,本系列也不會以上述方式撰寫,所以請確定儲存的程式檔案是以utf-8儲存,否則執行結果可能不如預期。
此外,Lua的底線開頭的變數,和Python一樣有時也代表一些意義,這會在說道metatable和物件導向程式設計時說明。
變數可見範圍
與多數程式語言一樣,分有全局變數和局部變數。不同的是,預設變數可見範圍是全局的(類似JS的var變數)。
g1 = 1
function f()
print(g1)
g2 = 2
end
f()
print(g2)
【30天Lua重拾筆記05】基礎1: 程式區塊(block、chunk)、排版
Lua的關鍵字
Lua的關鍵字並不多,就只有這麼幾個而已:
and | break | do | else | elseif | end |
false | for | function | goto | if | in |
local | nil | not | or | repeat | return |
then | true | until | while |
區塊(block)
有一些關鍵字會組合成程式區塊(block)。像是:
funciton
-end
do
-end
if-then
-end
while-do
-end
repeat
-until
夾於這些關鍵字中間的程式碼會成為一個環境區塊。
在區塊環境內,可以保有一些局部變數:
do
local name = "World"
print("Hello, "..name)
end
print(name)
Hello, World
nil
chunk
【30天Lua重拾筆記04】基礎1: Hello, Lua!
假設你已經選擇好並安裝 Lua的實現,且也準備好開發環境。使用過lua -v
沒問題後,就可以來試試看今天的入門示範程式。
你不必馬上了解今天的所有內容,將來都會說明到。今天只是快速的看看Lua程式長什麼樣子。
你可以執行lua
,在REPL交互式環境執行範例程式碼,也可以另存檔案,並用lua file.lua
執行看看。
第一個程式 - Hello, World
第一個程式?當然從打印出Hello, World開始。
Hello, World是指在電腦螢幕顯示「Hello, World!」(你好,世界!)字串的電腦程式。相關的程式通常都是每種電腦程式語言最基本、最簡單的程式,也會用作示範一個程式語言如何運作。同時它亦可以用來確認一個程式語言的編譯器、程式開發環境及運行環境是否已經安裝妥當。 – 維基百科
print "Hello, World"
Hello, World
不過這也太簡單了,來換個方式試試。
給個名字 - Hello, Lua
【30天Lua重拾筆記03】開發環境配置
開發環境配置
接著,來配置一下開發環境。主要會介紹三個開發環境,當然你想使用純文本編輯器也可以,我就是使用Emacs。
我會建議初學的人只使用代碼高亮的功能就好,最多…就使用到查找文件說明。 儘管今天介紹的配置都包含自動補全、格式美化、定義跳轉等等功能。但初學的人應該更關注在於其語法。(Lua語法也蠻自由的就是)
ZeroBrane
ZeroBrane是一個相當完整的Lua IDE,你幾乎可以直接下載下來使用。
其已經包含數個Lua版本,並可以輕易切換。
他也包含許多IDE應該要有的功能:
- 跳轉到函式定義
- 自動補全
- 除錯器
【30天Lua重拾筆記02】Lua的實現與選擇
Lua的實現與選擇
Lua的意思是葡萄牙文的「月亮」,其LOGO和其他相關也多與月亮有關。在開始使用學習Lua之前,比須先了解Lua的幾個版本與實際實現。
就像Python 2和Python 3有很大不同,Python 3各版本間又有些許不同。有些在Python 3.9能用的語法或功能,不一定可以在Python 3.5使用一樣。
Lua目前已經到了5.4版本,本系列內容也會以Lua 5.4為主。Lua 5.4於2020年釋出,所以還非常的新,有許多實現實際未達到這個標準。但Lua 5.1、5.3也已經使用多年,穩定度是可見的。就算是其餘版本的實現,也具有一定可用性。Lua設計極小,就某種程度上而言,甚至可以相對輕鬆的撰寫自己的實現。類似的於C語言,但是沒有C語言煩人的指標概念。所以Lua的實現也不少,其中著名的有:
極高效即時編譯的Lua實現。相容於Lua 5.1語法。
LuaJIT is compatible to the Lua 5.1 language standard. It doesn't support the implicit arg parameter for old-style vararg functions from Lua 5.0.
用於嵌入式設備開發的Lua,提供許多已經寫好的內容。(沒記錯的話也是使用Lua 5.1標準,沒用過)
Java版的實現,支援Lua 5.2的語法。在GitHub上有另一個Fork的版本,兩個有些差異。
JS實現的版本。其他JS實現的版本還有:Moonshine、lua.vm.js、Starlight。這是一個很新,也有點有趣的項目。我會在談談它。
C#的實現。
【30天Lua重拾筆記01】-認識Lua
認識Luna
盧娜(Luna,又寫作露娜或路娜)是羅馬神話中的月亮女神。「Luna」在法語和義大利語中也有月亮或月神的意思。在希臘神話中她的對應者為塞勒涅。盧娜也常常和黛安娜或赫卡忒混淆在一起。在羅馬的阿文提諾山上建有供奉她的神廟。 — 維基百科
錯棚了…
不是她 😅
再來一次 - 認識Lua
Lua是葡萄牙文的月亮,是一個輕量、快速、容易學習且容易嵌入的程式語言。其目標本就是成為一個很容易嵌入其它語言中使用的語言。其精簡的核心只包含一些最基本的功能,啟動速度非常之快。 儘管如此,透過最基本的功能,甚至可以實現多種編程範式。
可嵌入的程式語言
30天成爲Laravel萌新(目錄)
這是第二次參加「iT 邦幫忙鐵人賽」。上次報了兩個主題,只有一個完賽。這次報了三個主題……還好全都完賽了。雖然開賽前,對於一個主題先寫了近十天左右的文章,但在賽中,趕稿壓力還是頗大的。
30天成爲Laravel萌新(目錄)
「30天成爲Laravel萌新」是我最重要的一個系列,報名在Modern Web主題下,並同步發表於又LAG隨性筆記。下面列出每天文章連結:
- 30天成爲Laravel萌新(第0天) - 前言
- 30天成爲Laravel萌新(第1天) - 認識Laravel
- 30天成爲Laravel萌新(第2天) - 安装 Laravel
- 30天成爲Laravel萌新(第3天) - 使用laradock建立開發環境(上)
- 30天成爲Laravel萌新(第4天) - 使用laradock建立開發環境(下)
- 30天成爲Laravel萌新(第5天) - Laradock的工作空間容器
- 30天成爲Laravel萌新(第6天) - 配置專案
- 30天成爲Laravel萌新(第7天) - 認識artisan
- 30天成爲Laravel萌新(第8天) - 路由&頁面模板(1)
- 30天成爲Laravel萌新(第9天) - 路由&頁面模板(2)
- 30天成爲Laravel萌新(第10天) - 路由&頁面模板(3)
30天成爲Laravel萌新(第30-1天) - 總結
這是第二次參加鐵人賽。這個主題是我決定參賽出就已經定好,總算寫完了。從介紹、安裝、配置,使用laradock
、artisan
,路由、模板,Parsdown
(Markdown),再到控制器、資料庫,以及另我以些驚豔的Pagiantion
,然後多語系支援、紀錄檔,客製化錯誤頁面,檔案上傳與表單驗證,到最後登入驗證。
在這過程中,一學習到不少,很充實。(還有下次不要在一次報三個主題了…)
不過,Laravel官方文檔其實相當豐富完整,這30天的文章,頂多只能當作而外的參考而已,但願對想學習Laravel的人還是有幫助。
最後,你可以在這裡看到跟Laravel有關的文章。或是看看另外兩個系列文章( 又LAG的EOS.IO技術筆記 和 有點玩鬧性質的 又LAG的ML學習筆記 )。
30天成爲Laravel萌新(第30天) - 登入驗證
這部份在Django時,明明是最先學的,在Laravel卻放到了最後☺
要使用Laravel提供的會員系統,相當容易,只須要:
artisan make:auth
artisan migrate
然後瀏覽http://localhost/register 註冊帳號,或是http://localhost/login 登入帳號。仔細一看,會多了這些檔案:
new file: app/Http/Controllers/HomeController.php
new file: resources/views/auth/login.blade.php
new file: resources/views/auth/passwords/email.blade.php
new file: resources/views/auth/passwords/reset.blade.php
new file: resources/views/auth/register.blade.php
new file: resources/views/auth/verify.blade.php
new file: resources/views/home.blade.php
new file: resources/views/layouts/app.blade.php
modified: routes/web.php
其中routes/web.php
多了:
Auth::routes();
Route::get('/home', 'HomeController@index')->name('home');
現在瀏覽http://localhost/home 也有畫面了。
30天成爲Laravel萌新(第29天) - 表單驗證
昨天的程式碼有一些註解的內容,先取消註解試試。
resources/views/images/upload.blade.php部份內容
@if ($errors->any())
<div class="alert alert-danger">
<ul>
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
routes/web.php處理post('/images/upload')
請求的部份內容:
Validator::make($request->all(), [
'file' => 'required|image',
])->validate();
驗證表單資料
Validator::make($request->all(), [
'file' => 'required|image',
])->validate();
差不多等價於:
$validate = $request->validate([
'file' => 'required|image',
]);
$request
有validate()
的方法。還可以直接建立一個驗證器,繼承Request
:
php artisan make:request ImageUpload
上面執行完後,現在,在app/Http/Requests/
目錄下多了ImageUpload.php
,內容改成下面這樣:
30天成爲Laravel萌新(第28天) - 上傳檔案
Laravel要上傳檔案非常的簡單,今天就來簡單帶個範例吧!
建立檔案目錄連結
首先,現用Artisan建立目錄連結。
artisan storage:link
上面命令會建立storage/app/public
目錄,並將目錄同樣綁定到public/storage
。這讓於此目錄下的內容,可以透過http://localhost/storage/<FILE NAME>
存取。在public
目錄下的檔案,基本都可以直接透過瀏覽器存取。
上傳頁面
同樣以一個簡單的上傳頁面作為範例。先建立resources/views/images/upload.blade.php
:
@extends("base",['title'=>'上傳圖片'])
@section('title', '上傳圖片')
@section('body')
<form action="{{route('image.upload')}}" method="post" enctype="multipart/form-data">
@csrf
<!--
@if ($errors->any())
<div class="alert alert-danger">
<ul>
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
-->
<input name="file" type="file" accept="image/*" value=""/>
<input name="" type="submit" value="上傳"/>
</form>
@endsection
30天成爲Laravel萌新(第27天) - 本地化、多語言支援(Localization)
@extends("base",['title'=>'哎呀,找不著頁面'])
@section('title', '哎呀,找不著頁面')
@section('body')
<h1>{{__('404 error message')}}</h1>
<b>哎呀,找不著頁面</b>
<p>
<ul>
<li><a href="/">點擊我,回到首頁</a></li>
<li><a href="{{route('blog/post.index')}}">我的網誌</a></li>
</ul>
</p>
@endsection
開始
在昨天簡單建立的錯誤頁面中,又再一次偷渡了一個東西。對,就是那個有點怪怪的{{__('404 error message')}}
,實際上這等價於@lang('404 error message')
。透過lang()
函式的方式,也可以在控制器做變換。lang()
會根據給定的字串,以及設定(app/config.php
)中的locale
和faker_locale
轉換為對應的語言。預設會轉換為locale
所設定的,而備用語言可由faker_locale
設定。當兩者都不存在時,會直接輸出字串。還記得我們把app/config.php
部份內容改成這樣嗎?
'locale' => 'zh-TW',
'fallback_locale' => 'en',
不過,不管是locale
還是faker_locale
,都還找不著{{__('404 error message')}}
要轉換成的文字,所以現在會直接生成404 error message
。
30天成爲Laravel萌新(第26天) - 客製化404錯誤頁面
HTTP協議上存在許多狀態碼,其中400系列、500系列錯誤可能是最常見到。200、300很難被注意到。恐怕又以404錯誤、403錯誤、500錯誤、503錯誤最常見。一個好的錯誤提示頁面,可以帶來很好的使用體驗。你可以看看別人怎麼設計(以及2018年不可錯過的創意404報錯設計),甚至更有404 PAGE TEMPLATE (https://www.404pagefree.com/) 可以直接下載404錯誤頁面模板(這網址真有趣),裡頭模板多以CC-BY 3.0姓名標示授權,可用於商業用途。
那麼在Laravel要怎麼客製化這些錯誤提示頁面?以404錯誤頁面為例,可以建立resources/views/errors/404.blade.php
檔案,寫入以下內容:
@extends("base",['title'=>'哎呀,找不著頁面'])
@section('title', '哎呀,找不著頁面')
@section('body')
<h1>{{__('404 error message')}}</h1>
<b>哎呀,找不著頁面</b>
<p>
<ul>
<li><a href="/">點擊我,回到首頁</a></li>
<li><a href="{{route('blog/post.index')}}">我的網誌</a></li>
</ul>
</p>
@endsection
同樣的…CSS不是本系列重點……
30天成爲Laravel萌新(第25天) - 紀錄檔
使用Logging
現在,要再回到laradock來。不過同樣的,Lavavel儲存紀錄檔的地方也是在storage/logs
。這裡會儲存使用Log::info()
所寫下的紀錄。
docker-compose exec workspace bash -c "tail -f storage/logs/最新的log"
如果使用Linux的話,也可以直接:
tail -f storage/logs/最新的log
當然你也可以直接把紀錄檔,用文字編輯器開啟。不過上述會這麼做,是因為如次可以更簡單的在開發時除錯。有時候,Laravel錯誤訊息頁面提供的訊息並非一目瞭然,會需要用到printf
方式除錯(我假設…你不動gdb
,但用過最基本的除錯手法…)。在這裡,使用一系列Log::
的方法,其中,Log::debug()
就對應除錯資訊。這和我曾經寫過的瀏覽器console.log()外的一些其他用法 有些像。
30天成爲Laravel萌新(第24天) - 頁面清單 下篇(使用Pagnation和刪除按鈕)
就先來看看使用Laravel提供Pagination能夠多麼簡單。
首先,控制器只需要使用BlogPost::paginate($limit)
查詢就好,並將查詢結果直接傳遞給視圖。
public function index()
{
// 如果想要將每頁顯示數量變成可以調整的,
// index需要傳入 (Request $request),並由下方方式取得$limit傳入paginate
// $limit = $request->query('limit', 30);
$posts = BlogPost::paginate(30);
return view("blog/index",[
"posts"=>$posts,
]);
}
BlogPost::paginate($limit)
會自動根據Request的page
參數取得存取的頁面,自動計算offset
,並根據給予的每頁顯示數量限制來查詢。甚至連index()
都不需要傳入Request $request
。傳給視圖的也少了不少。說到視圖,立馬來看看變得多簡潔:
@section('body')
<div class="container">
<ul>
@foreach ($posts as $post)
<li>
<a href="{{route('blog/post.show',['id'=>$post->id])}}">{{ $post->title }}</a>
</li>
@endforeach
</ul>
</div>
{{ $posts->links() }}
@endsection
列出文章清單的部份,大致什麼太大變化。但是頁面頁數選項不再那麼複雜,沒錯,只需要$post->links()
就好。他會自動生成符合Bootstrap的Pagination。在瀏覽器接受到的HTML樣子可能如下:
呵呵,根本不需要寫這麼麻煩…也太簡單了吧!
30天成爲Laravel萌新(第23天) - 頁面清單 上篇
因為重點不在於CSS,不在於Bootstrap。 其實大可以將頁面弄的很美觀。在Laravel專案資料夾中,其實有一個給
npm
使用的package.json
檔案。 不過還是來把重點放在Laravel上。
使用Pagnation前
Laravel提供一個超級簡單的方式,來處理分頁問題。不過在使用前,來看看原本要怎麼實現index()
。不但要取得頁數,並計算檢索數量,然後從資料庫透過offset()
、linit()
的方式取得資料。
public function index(Request $request)
{
$offset = $request->query("offset", 0);
$limit = $request->query("limit", 30);
$page = $request->query("page");
if($page){
$offset = ($page - 1) * $limit;
}else{
$page = ($offset / $limit) + 1;
}
$request->merge([
"offset" => $offset,
"page" => $page,
]);
$posts = BlogPost::orderBy('activity_id','ASC')
->offset($offset)
->limit($limit)
->get();
return view("blog/index",[
"posts"=>$posts,
"page"=>$page,
"total_pages" => $total_pages,
]);
}
30天成爲Laravel萌新(第22天) - 資源控制器(Resource Controller) 下篇
最終,我決定將index()
和destroy()
另外寫。一個是寫完create()
和edit()
,destroy()
也就不怎麼難。但是index()
意外也能有豐富內容能寫…(加上有另一種用法我還不太會)
總體來說,這篇程式還是有些趕工粗糙….
重點觀念
@method
的使用 相當於隱藏欄位_method
。@csrf
的使用
建立頁面模板
同樣的,先建立一個編輯頁面的模板resource/views/blog/edit.blade.php
:
@extends("base",['title'=>'編輯文章'])
@section('title', '編輯文章')
@section('body')
<form method="post" action="{{($type=="edit") ?
route("blog/post.update", ["id"=>$id]) :
route("blog/post.store")}}">
@csrf
@method(($type=="edit")? "patch" : "post")
<label for="title">標題:</label>
<input name="title" type="text" value="{{$title}}" id="title" />
<br/>
<label for="content">內容:</label>
<textarea cols="30" id="content" name="content" rows="10">{{$content}}</textarea>
<br/>
<input name="" type="submit" value="儲存"/>
</form>
@endsection
30天成爲Laravel萌新(第21天) - 資源控制器(Resource Controller) 中篇
現在,要正式把控制器與資料庫連結起來。順便偷埋之後兩個主題☻。
引入所需使用的套件
首先,得先把之前建立好的Model引入。另外,我們在偷偷引入一個Illuminate\Support\Facades\Log
。
use Illuminate\Support\Facades\Log;
use App\BlogPost;
use Parsedown;
完成CRUD操作
Create / Store
public function store(Request $request)
{
$title = $request->input("titile", "未命名文章");
$content = $request->input("content");
$post = new BlogPost;
$post->title = $title;
$post->content = $content;
$post->save();
Log::info("Store New Blog Post: id = $post->id");
return redirect()->action(
'Blog\PostController@show', ['id' => $post->id]
);
}
可以透過Request
的input
方法,取得POST來的資料,還可以補上預設參數。這邊取得標題與內容後,建立一個新的PostBlog
實體存入資料庫。
Read / Show
public function show($id){
$post = BlogPost::find($id);
if(! $post){
abort(404);
}
$content = $post->content;
{
$Parsedown = new Parsedown();
$content = $Parsedown->text($content);
}
return view("blog.post", [
"title" => $post->titile,
"content" => $content,
]);
}
讀取與之前控制器的內容差不多。不同的是,需要先從資料庫尋找資料,如果找不到就回傳404找不到錯誤頁面。之後會對該頁面進行修改。現在http://localhost/blog/post/12 將會顯示404錯誤頁面,只有http://localhost/blog/post/1 才會出現之前填入的內容。
30天成爲Laravel萌新(第20天) - 資源控制器(Resource Controller) 上篇
CRUD & RESTful
所謂CRUD是Create、Read、Update、Delete。昨天已經從資料庫模型(Model)的角度看過基本操作了,今天要將些操作加入到控制器(Controller)。
C | Create
R | Read
U | Update
D | Delete
另外,還需要另外知道的一件事情是RESTful。RESTful並不是硬性規定,只是一種在HTTP請求上的一種慣例設計。通常HTTP請求方法有GET
、POST
、PUT
、DELETE
、PATCH
,以及HEAD
、CONNECT
、OPTIONS
、TRACE
。以下節錄自MDN:
GET
GET 方法請求展示指定資源。使用 GET 的請求只應用於取得資料。
HEAD
HEAD 方法請求與 GET 方法相同的回應,但它沒有回應主體(response body)。
POST
POST 方法用於提交指定資源的實體,通常會改變伺服器的狀態或副作用(side effect)。
PUT
PUT 方法會取代指定資源所酬載請求(request payload)的所有表現。
DELETE
DELETE 方法會刪除指定資源.
CONNECT
CONNECT 方法會和指定資源標明的伺服器之間,建立隧道(tunnel)。
OPTIONS
OPTIONS 方法描述指定資源的溝通方法(communication option)。
TRACE
TRACE 方法會與指定資源標明的伺服器之間,執行迴路返回測試(loop-back test)。
PATCH
PATCH 方法套用指定資源的部份修改。
其實也就差不多對應了CRUD的操作:
CRUD | RESTful |
---|---|
Create | POST / PUT |
Read | GET |
Update | PATCH / PUT |
Delete | Delete |
30天成爲Laravel萌新(第19天) - Model的基本操作
ORM是物件關聯對映(Object Relational Mapping)。在Larvel裡提供的 Eloquent ORM能讓開發者以一個簡單、美觀的方式操作關聯式資料庫。今天就來寫一點點基本的操作,更多可以查看文件,以及Laravel資料庫相關文件。
Select All
在SQL,可能會用SELECT * FROM <TABLE>;
來取得所有資料,儘管在資料量龐大時,這樣操作並不好,但是Laravel同樣有對應的操作:
use \App\BlogPost
$posts = App\BlogPost::all();
foreach ($posts as $post) {
echo $post->title;
}
條件查詢Where
條件查詢也非常長使用到:
$post = App\BlogPost::where('id', 1)->get();
// ->orderBy('name', 'desc') // 還可以加以設定以下參數
// ->take(10)
// ->get();
用where
自然很容易從SQL轉換了解。不過,Laravel有更簡單的方式針對主鍵做查詢:
$post = App\BlogPost::find(1);
// 等同於
$post = App\BlogPost::where('active', 1)->first();
30天成爲Laravel萌新(第18天) - 建立Model
前幾天建立了Blog Post在資料庫的Schema,並且遷移(Migration)進資料庫,建立相關的資料表(blog_post)。為了讓Larave提供的ORM系統,以一個美觀、簡易的方式操作資料庫,還得建立BlogPost Model類別。
當然也可以使用artisan建立一個樣板:
artisan make:model BlogPost
然後會有app/BlogPost.php
的檔案
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class BlogPost extends Model
{
//
}
這次,只需要簡單改成下面的樣子就好:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class BlogPost extends Model
{
protected $table = "blog_post";
// protected $primaryKey = 'id';
// public $timestamps = false;
}
30天成爲Laravel萌新(第17天) - 資料庫管理工具(下)‧使用DBeaver
今天要在介紹一個,我用過後就喜歡上的資料庫管理工具–DBeaver。
DBeaver的兩個版本
DBeaver有社區版和企業版兩種。社區版可以免費使用,並且跨平台,透過JDBC支援多種資料庫。此外還會自動檢測資料庫版本,自動尋找相對應的Drive。
我手動配置過phpMyAdmin過。同樣直接下載安裝的情況下,DBeaver簡單好多。
DBeaver的界面很漂亮,我還蠻喜歡的。其中還有一個功能測試時非常好用–產生假資料。這個和前幾天亂數短文很像,不過DBeaver會針對欄位型態產生資料。資料庫測試的模擬數據生成(Mock Data Generation)也有一些限制,有些資料類型還是無法產生。
連線資料庫並填入資料
說了這麼多,相信已經下載安裝好了吧?該像昨天一樣,來連接資料庫,並塞入文章吧!
30天成爲Laravel萌新(第16天) - 資料庫管理工具(上)‧使用phpMyAdmin
今天,要用 phpMyAdmin 來看看昨天建立的資料表,我會順面補充一些之前做的設定。
查看Docker容器狀態
我會假設使用的是laradock,如果你使用的是XAMPP可能會簡單一些。
那麼,首先先看看運行的容器:
docker-compose ps
我們之前有啟動phpMyAdmin的服務,那麼輸出應該包含下面內容:
Name Command State Ports
---------------------------------------------------------------------------------------------------------------
laradock_phpmyadmin_1 /docker-entrypoint.sh apac ... Up 0.0.0.0:8080->80/tcp
其中注意到 Ports 的部份,這意思是綁定(bind)電腦8080端口到容器的80端口。所以我們可以透過瀏覽 http://localhost:8080 來使用phpMyAdmin。
登入phpMyAdmin
這裡伺服器要輸入mariadb
,帳號和密碼分別是:default
和secret
。如果你是啟用MySQL的話,伺服器就改成mysql
。
30天成爲Laravel萌新(第15天) - 建立資料庫Migration
要把文章存入資料庫?那麼對於傳統關聯式資料庫,需要先建立資料表Schema。你可以透過明天要介紹的資料庫管理工具,也可以透過今天來來說的使用 Artisan 建立資料表。
使用Artisan建立資料表
Artisan可以快速的建立控制器基本模板,同樣也可以建立資料庫相關模板:
artisan make:migration create_blog_post_table
透過上面命令,會在database/migrations
新增相關檔案,檔名由<日期><時間>_create_blog_post_table
組成。前面時間相關的在某些情況下會很重要,後面介紹的 artiasn 命令會依序建立資料表,所以如果資料表有相互依賴就有影像。現在我們開啟該檔案看看:
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateBlogPostTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('blog_post', function (Blueprint $table) {
$table->bigIncrements('id');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('blog_post');
}
}
其中最重要兩行:
$table->bigIncrements('id');
$table->timestamps();
30天成爲Laravel萌新(第14天) - 控制器(Controller)
回頭看看,看,怎麼Route的內容變得這麼長阿!仔細一看,還做了路由以外的事情,像是修改變數內容等等。是不是該把Hangle Function獨立出來?有什麼辦法?Laravel的控制器(Controller)可以幫助。
新增Controller
我們可以透過Artisan來快速新增一個標準的控制器(Controller):
artisan make:controller Blog/ExamplePostController
在app/Http/Controllers/Blog
資料夾下會多一個ExamplePostController.php
的檔案,內容大致如下:
<?php
namespace App\Http\Controllers\Blog;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
class ExamplePostController extends Controller
{
//
}
Blog
這個新建立的資料夾,實際對應於昨日group
路由當中的'namespace' => 'Blog'
參數。並且在上面生成的程式中use App\Http\Controllers\Controller;
,也是因為這個新建立的模組是在App\Http\Controllers\Blog
底下的命名空間,因此要引入使用App\Http\Controllers\Controller
。關於PHP的模組系統,就不多做解釋了,只是相關的Blog
字串如不想使用,要做相對應的調整。
之後會改用資源控制器。這邊單純要做個對照,之後可以刪除不使用。
30天成爲Laravel萌新(第13天)- 簡易Blog頁面(下)
昨天利用亂數文章(Lorem ipsum)產生了@section('body')
的預設內容。今天,要在Route把變數$content
設成類似的亂數文章。
中文亂數文章
Lorem最多的資源大多還是英文,不過中文也有不少資源可以使用。此外,有些情況使用設計好的預設文字可能會更適合。今天,就只是想來用用而已XD。透過亂數假文產生器 Chinese Lorem Ipsum可以產生中文亂數文章,也可以用於產生標題。現在來簡單的把$title
設成日的及度加機子魚年
。$content
設成:
身主一?是字對一日國地包最中感行物評民活於力!進上城大、很會英我:的樣基覺型家海當期是著:算像系大心樣十因天特企以加想點以後許,念落是客我什在白實文種運明市下只能當作力美間要速包企以統推多,收什火投,顧外。
指的司之身鄉然說小人發,我雜人轉英臺區實說麼後男會友大,阿產效第續明造識竟自沒大上有。到全藝地港為精件連需,過聯排可然更友我後們時?一風了組麼兒比、方來出科!車卻讀力風人父資國個年愛成故作自功:原用中有、港看車我高心打!親北受活女微特利動果的無何片經物……象巴約得?色時值壓五……過點子得提圖陽可計,來麼室拉的合天!人觀難一手樹以防黃精也需被那部我冷不不連痛以哥多人事營件、目熱而夫加……紀味曾蘭不好空農後電題熱頭國:戲識者是相大覺光,雙命影心可那?前區孩增,物時。
產明物力區有王真?院見很原,經總萬官方,生回。
海在康代界積實兒變:她哥出邊。善快寶,死面大,紀著資小成去資戲面和的發行的止的接來,南生不能野:政生企性班們教求給學就得受再球也生克決的金長冷成。
標題、文章內容當然還無意義wwww
調整排版
恩~沒分段了?這是因為HTML會忽略空白,如過要換行還需要使用<br/>
。當然你可以在變數內容這麼加,然後在模板將顯示的部份改為{!! $content !!}
,或是也可以用<pre>
來做,像是把@section('body')
改成:
@section('body')
<h1>{{$title}}</h1>
<pre>{{$content}}</pre>
@endsection
30天成爲Laravel萌新(第12天)- 簡易Blog頁面(上)
今天要來建立一個部落格文章的極簡陋顯示頁面。
建立路由
在routes/web.php
檔案中添加以下內容:
Route::group(['prefix' => 'blog',
'as' => 'blog/',
'namespace' => 'Blog', ],
function(){
Route::get('/post/{post_id}', function($post_id){
$title = "Example Title";
$content = "Example Content";
return view('blog.post', [
"title" => $title,
'content' => $content,
]);
});
});
儘管其實完全不需要用到group
,不過未來可以像其他blog系統一樣,除了有文章頁面(post),還可以有其他獨立頁面(page)。另外,添加blog的前綴,可以將其他前綴用於其他作用,或是最後在做個轉址。總之,先選了個靈活的寫法。
其中,好好注意$title
和$content
這兩個參數,這兩個參數隨後會在多次變動修改,今天主要先把頁面顯示出來。
部落格文章頁面模板
接者建立resources/views/blog/post.blade.php
檔案(當然需要先建立resources/views/blog
資料夾)。並且上頭原本路由區用的view也可以寫為blog/post
。將這份檔案簡單填入以下內容:
@extends("base",['title'=>$title]) {{-- 第二個參數可以傳遞變數給父模板,但是父模板改用插槽方式,須改用以下方式 --}}
@section('title', $title)
@section('body')
<h1>{{$title}}</h1>
{{$content}}
@endsection
這個頁面模板繼承了基礎頁面(base
),並填入頁面內容。不過,還沒有基礎也面阿?現在來建立resources/views/base.blade.php
,並寫下以下內容:
30天成爲Laravel萌新(第11天) - 路由&頁面模板(4)
使用Artisan顯示目前路由狀態
首先,先來說說怎麼看目前路由狀態。透過artisan route:list
列出目前路由狀態:
+--------+----------------------------------------+--------------+------+---------+--------------+
| Domain | Method | URI | Name | Action | Middleware |
+--------+----------------------------------------+--------------+------+---------+--------------+
| | GET|HEAD | / | | Closure | web |
| | GET|HEAD | api/user | | Closure | api,auth:api |
| | GET|HEAD | hello | | Closure | web |
| | GET|HEAD|POST|PUT|PATCH|DELETE|OPTIONS | hello-world | | Closure | web |
| | GET|HEAD | hello/{name} | | Closure | web |
+--------+----------------------------------------+--------------+------+---------+--------------+
首先要注意到的是Method
和URI
,這也是在定義路由最基礎的部份,URL
在路由定義時就稱作PATH
。我們還可以將路由命名(Name
);至於Action
與以後會提到的控制器(Controller
)有關,這裡顯示Closure
表示路由由handler function
處理請求;最後Middleware
預計本系列文章不會提到,又興趣可以去看官方文件。
api/user
定義在routes/api.php
中。
基礎路由方法
基本路由方法有:
Route::get($uri, $callback);
Route::post($uri, $callback);
Route::put($uri, $callback);
Route::patch($uri, $callback);
Route::delete($uri, $callback);
Route::options($uri, $callback);
基本上就對應了REST的GET
、POST
、PUT
、PATCH
、DELETE
、OPTIONS
的請求方法。此外還可以透過Route::any($uri, $callback);
來直接處理請求,而不管請求方法;或是使用Route::match([$method,...], $uri, $callback);
來處理特定請求方法。
※ 請求方法 是HTTP請求裡頭的一個欄位:METHOD。
30天成爲Laravel萌新(第10天) - 路由&頁面模板(3)
回去看了一下文件,才發現之前有好多沒用到過 畢竟只學了2周的時間。不過有些設計挺有趣的。
今天來看一下Blade模板語言中的一些(我覺得)重要的功能。
印出變數
昨天已經看過怎麼顯示變數了,使用{{$variable}}
。今天要特提的是:怎麼給變數一個預設值。
{{isset($variable) ? $variable : "Default Value"}}
{{ $variable or 'Default' }}
不過在5.8、6.x以後似乎改成這樣:
{{$variable ?? 'Default'}}
我不清楚爲什麼把這麼方便的功能給去除掉了。 6.x之後的用法並沒有在官網的文件特別提,會發現單純是範例中有類似程式碼。
上面其實還相當於
@isset ($variable)
{{$variable}}
@else
Default
@endisset
另外還有@empty
@empty($variable)
Default
@else
{{$variable}}
@endempty
30天成爲Laravel萌新(第9天) - 路由&頁面模板(2)
繼續昨天
Route::get('/hello/{name}', function ($name) {
return '<h1>Hello, '.$name.'</h1>';
});
透過路徑接受了參數傳了進來,再加以加工輸出回去。不過這麼寫不怎麼美觀, 因為這違反了 「 只做一件事情 」的原則,不但處理了路由請求,還傳遞參數並渲染頁面。
今天透過模板系統,要來把路由請求處理與頁面設計分離。
首先,把上面程式修改為以下內容
Route::get('/hello/{name}', function ($name) {
return view("hello-name", [
"name" => $name,
]);
});
接著在新增一個模板檔案resources/views/hello-name.blade.php
<h1>Hello, {{$name}}</h1>
然後瀏覽http://localhost/hello/Daniel 看看,應該與之前畫面並無差異。
30天成爲Laravel萌新(第8天) - 路由&頁面模板(1)
接下來幾天,會交叉介紹路由(Route)和頁面模板(View/Blade)。幾經思考,這兩者關係十分密切,不太好單獨撰寫。
關於路由(Route),我們曾經在第六天短暫看到過。
<?php
Route::get('/', function () {
return '<h1>Hello</h1>';
});
現在我們看看routes
資料裡面的內容:
- api.php
- channels.php
- console.php
- web.php
我們修改的是web.php
的內容,這也是最主要訂立路由的檔案。api.php
的檔案實際上也不太有差別,不過api.php
底下定義的路由,預設會前置增加api/
的前綴,是設計用於提供HTTP API路由定義的檔案。
瀏覽 http://localhost/ 以後,就會顯示Hello World,如果你嘗試右鍵瀏覽網頁原始碼(view-source:http://localhost/)就會看到<h1>Hello</h1>
。
在回頭看看網址,除了localhost
外,後面的就是路徑/
,認知道這個斜線是很重要的,因為Apache2和Nginx對於結尾斜線的認知有些不同。接著再在wep.php
增加以下入由看看:
30天成爲Laravel萌新(第7天) - 認識artisan
Laravel有「 為網頁藝術家創造的框架 」的美譽,他的工具名字也很有意思 artisan ,意為 工匠 ,與藝術家(artist)一樣,是與藝術(art)有關的字。
artisan 可以用來顯示路由狀態、遷移資料庫、產生基本樣板程式碼、調整文件結構狀態等等。而且之前已經看過,就是我們用來產生專案文件密鑰(key)的artisan key:generate
。
不過,如果使用laradock
進入workspace
的docker容器的話,可能會找不著指令。artisan
詳細使用方式,會在未來有需要時在做說明,今天,就先來簡單看一下。
如果使用laradock
進入workspace
的docker容器的話,找不著指令嗎? 透過下面命令切換到/var/www
目錄下在試試。
cd /var/www
列出所有artisan子命令
不同版本的artisan
有可能存在使用差異。今天主要說明怎麼快速了解指令如何使用。
首先先學著者麼列出所有能使用的工具:
artisan
或是
artisan list
30天成爲Laravel萌新(第6天) - 配置專案
在前三天已經安裝好Laravel的基本環境。今天算是一個分水嶺,不管你採用哪種方式建立開發環境,都應該已經得到一個預設好的Laravel工作目錄。在此我不會解釋目錄結構,有興趣可以自行參閱文檔。不過是先留意一下幾個文件與目錄:
- artisan
- config/
- database/
- public/
- resources/
- routes/
- storage/
之後有用到會在加以說明。而今天,首先要進行專案的配置,也就是設定(config/)。沒錯,目錄 config 就是儲存相關配置的目錄。不過在此,我們還得先編輯 .env 檔案。你可能會找不著這份檔案,別擔心,目錄下有個 .env.example ,將其複製並重新命名即可。
接著我們找到以下內容:
APP_DEBUG=true
APP_URL=http://localhost
LOG_CHANNEL=stack
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=homestead
DB_USERNAME=homestead
DB_PASSWORD=secret
目前最爲重要的是關於APP和DB的設定。在發佈階段,APP_URL
會需要調整,不過我們之後在說說。而目前預設情況僅有自己的電腦能夠瀏覽。其次是APP_DEBG
,在發佈階段需要改為false
。 此外,還會注意到一個APP_KEY
的設定,如果其為空,請執行以下命令:
php artisan key:generate
接着是資料庫的設定,這裏需要改成資料庫的連結設定。如果使用MariaDB和MySQL,維持mysql
就好。(新版本的MariaDB可能有問題)
30天成爲Laravel萌新(第5天) - Laradock的工作空間容器
在開始配置Laravel的環境設定檔案之前(.env
),先來帶大伙看看laradocke最
重要的一個容器workspace
。
關於workspace容器
workspace是laradock連結各個容器的重要容器,還記得我們昨天這麼做嗎:
docker-compose exec workspace composer create-project --prefer-dist laravel/laravel tutorial_blog 5.8.*
docker-compose exec
的格式是docker-compose exec <CONTAINER> <COMMAND>
。可是,可沒有吧workspace
啟動(up
)起來呀!我們像下面啟動了nginx
、mariaDB
、phpmyadmin
而已阿。
docker-compose up -d nginx mariadb phpmyadmin
是的,laradock會自動啟動workspace
這個容器。並且注意到後面的COMMAND
實際就是在第2天用來初始話laravel專案的命令。在下完這的命令以後,會在原本建立的 laravel-tutorial 目錄下多一個 tutorial_blog 目錄。並且裡面有基本Laravel的環境。我們會在幾天後來設定這個環境,現在,來我們專注於 workspace 這個容器。
我們同用用docker-compose exec
來進到 workspace 容器裡面。
docker-compose exec -u laradock workspace /bin/bash
或是用docker exec
來進到裡頭。不過使用docker exec
你還會需要知道真正的容器名字。所以命令可能像是下面這樣:
docker exec -it -u laradock laradock_workspace_1 /bin/bash
我們使用 laradock 登入workspace容器(-u/--user
)。laradock 預設用於開發的使用者帳號,如果不加上這個選項,會使用最高管理權限登入(root)。透過使用這個帳號登入,未來可能可以省去一些不必要的麻煩(ex:宿主機和虛擬機掛載目錄的權限問題)。
30天成爲Laravel萌新(第4天) - 使用laradock建立開發環境(下)
建立專案目錄
本次專案目錄預計會有以下內容:
- laravel-tutorial
- laradock
- .laradock
- tutorial_blog
laradock 和 .laradock 先不管他們。先建立 laravel-tutorial 目錄,並在該目錄鍵入:
git clone https://github.com/Laradock/laradock.git
cd laradock
git checkout v7.15
以上會安裝laradock,並切換到7.15版本。
設定laradock
在建立服務容器前,要先做設定。
首先, 複製 env-example 為 .env,然後找到以下設定並變更:
APP_CODE_PATH_HOST=../tutorial_blog
...
...
...
DATA_PATH_HOST=../.laradock/data
APP_CODE_PATH_HOST
指定專案目錄(下一步驟建立),DATA_PATH_HOST
則是未來資料儲存的位置,包含資料庫儲存位置。
接着建立並啓動環境:
30天成爲Laravel萌新(第3天) - 使用laradock建立開發環境(上)
除了使用composer
以外,還可以使用 laradock 、Homestead、Valet、Laragon。Homestead是基於 Vagrant ,如果您已安裝VirtualBox,可以使用看看。而接下來說明laradock的使用方式。
Laradock 環境需求
laradock是基於docker的一個快速建立laravel的開發環境工具,理所當然的你會需要docker,此外你還會需要 docker-compose,以下列出範例使用的版本:
軟體 | 版本 |
---|---|
docker | 18.09 |
docker-compose | 1.25 |
git | 2.7.4 |
透過使用laradock,可以很快速的在Apache2、Nginx;MySQL、MariaDB;甚至是在PHP不同版本之間做切換。
30天成爲Laravel萌新(第2天) - 安装 Laravel
環境需求
這不是全部強制的,只是接下來一個月的時間,會以以下環境為範例:
軟體 | 版本 |
---|---|
Laravel | 5.8.18 |
Nginx | 1.14.0 |
MariaDB | 10.3.15 |
PHP | 7.2.19 |
如同前言所說,雖然Laravel已經釋出6.0版,但接下來將會以5.8為主。此外,也可以使用Apache網頁伺服器,儘管有些設定不同;至於資料庫也可以使用MySQL。並且,以上也都不是強制的,Laravel對於多個網頁伺服器、資料庫接受度良好,所以當然也可以使用PostgreSQL和其他支援PHP的網頁伺服器。
(雖然上面這樣列出,不過最後有可能會用Apache+MySQL再測試一次)
使用XAMPP
明天,我會介紹另外一個快速建置環境的方式,我會更推薦使用該方式。
如果你是Windows,可以直接安裝XAMPP,只是在之後如果遇到問題,請注意一下各個組件的版本。更多可以參考XAMPP的網站。
安裝Laravel
Composer是PHP的一個包管理器,儘管不是必要的,不過可以大量簡化Laravel的安裝程序,並且也可以加以安裝其他組件。因此,需要先確定Composer已經安裝好,並設定好環境。你可以透過 命令提示字元 或其他 Shell 的環境輸入composer -V
,正確安裝完會顯示版本資訊。
安裝Laravel:
composer global require laravel/installer
30天成爲Laravel萌新(第1天) - 認識Laravel
原本,我是想寫下Laravel的介紹,但是…可能有些單調。因為有些經驗實在是 不太多,就算是看別人寫的關於Laravel的特色,和過往PHP開發到底又怎樣差異, 也還是對我而言有些無感。因此決定從我自身角度來介紹Laravel。
Laravel又被人稱為 為網頁藝術家創造的框架 。那是因為,相較於以前PHP 將頁面資料與邏輯代碼混合寫在一起的 義大利麵寫法 , Laravel是類似 Django這樣廣義的MVC框架。也就是將頁面資料與邏輯代碼分開。
那摸到底為神ㄇ要用Laravel? 我簡單列出以下以點:
- 首先,PHP還沒有死亡。實際上PHP在許多地方還是可以看的到。著名的 WordPress、Drupal都是用PHP寫的。在許多與網頁相關應用方面,PHP成熟且 易用,這也讓使用PHP多了一個理由。過去寫過很短的PHP還活 著 可以去參考一下wwww。
- 相比WordPress的易用、易上手,Laravel提供更高度的彈性。
- 遵守Laravel一些基本的開發原則,原始碼更容易維護。(就是有些人一樣能 寫成義大利麵….)
- Laravel提供多個極為好用的基本可選用功能。包含身份驗證、資料驗證、資 料庫分離、ORM、資源控制器、上傳等等等。
30天成爲Laravel萌新(第0天) - 前言
今年有一段時間,短暫的1~2周,因為一些原因學習了Laravel,這次我事後的學習筆記。
Laravel是一個流行的PHP開發框架。不同於熱門的Drupal、WordPress.org, Laravel更像是Node.js的Express.js、Python的Django等等廣義的MVC框架。當然,他也可以快速的發展成CMS。
Laravel目前也發展到6.0,不過接下來幾天的內容會以5.8為主。Laravel也有完整的開發文檔。儘管有些翻譯仍然不完全,但也有多種語言的翻譯。首先,介紹一些學習資源:
- Laravel 官方網站
- Laravel.tw 中文站 文檔最新只到5.3版本
- Laravel中文學院
預計未來內容撰寫的方向:
- 關於Laravel
- 建立Laravel開發環境
- 使用Composer安装
- 使用laradock建立環境
- 認識artisan
- Route - MVC
- 會員系統
- Template
- Controller
- 文章資料庫 - Database Schema
- 商品資料庫 - 驗證資料
- 上傳檔案
- API