但一定很難想像最煩人的難關會卡在政府那裡,
而且居然是卡在「設立公司」這麼基本的節骨眼上。
前幾天在facebook 上 po 了一篇敘述這一切有多荒謬的文章,
我設立登記的是「閉鎖型股份有限公司」,
如果你還不知道什麼是閉鎖型,
可以參考 inside 上的這篇
不管是在股權架構還是從募資上來說,
都看的出來政府想要鼓勵創新的決心,
只是如果不是自己親身去辦,
還真的沒辦法瞭解踏出這個「創業的第零步」有多困難。
很多人常會抱怨台灣的政府制度跟不上,
但實際上這些困難點主要來自於公務體系的僵化,
這現象如果沒有被解決,
就算政府推動再先進的制度也毫無意義,
接著會照著下面的架構來敘述這個「意想不到的難題」。
試想如果你有一個覺得很棒的主意,
再加上研究後發現政府有許多鼓勵新創產業的政策,
一切都讓你迫不急待,
但當你開始辦「公司設立登記」
時,
你會發現我們的制度,或是代表制度的窗口,並沒有想像中這麼鼓勵我們,
甚至是排斥我們去做任何事情。
今天要講的是各種制度上的荒謬,以及申辦公司設立登記會遇到的狗屁倒灶,
期待以後要創業的人看完以後知怎樣避免這些鳥事;
但更期待當局者能讓制度更完善,
畢竟台灣不是一個資源豐富的地方,
我們完全沒有必要在這種沒有必要的地方讓做事情變得更艱難。
看到後來你真的會發現,制度的設計其實並沒有太大的問題,
有問題是公務人員裁量的部份。
或者該說公務人員長官裁量的部份。
e.g:
你會發現你做的事情可能第一次是對的、第二次又錯了。
最多人聽到設立公司這麼麻煩之後會問這句話,
而網路上教學大多也是準備好資料,
然後丟給會計事務所去幫你代辦。
舉例:
我比較想問的是:「為什麼這麼基本的東西要找會計師?」
儘管這筆開銷是小錢,
但老實說,除了資本額簽證之外,
其他應該都是創業者自己能搞定而且該了解的。
股東會、董事會、股權結構的設計、章程,
以及你所登記地址的營業產業別是否有限制,
或是你的公司業務算不算是特許行業,
我想我們都該同意會計師不會比你更了解你的公司
如果沒有親身經歷一遍是沒辦法體會的。
重點在我們根本不該習慣一個這麼基本的東西,
居然複雜到要請人來代辦才搞得定,
這樣還要公務員幹嘛?有會計師就好了啊!
只講最重要也最普遍的一點:因為資金,
其他諸如開發票、買東西報帳這些都算是小事情,
你一定會發現有很多事是沒有公司的話,連做的資格都沒有的。
創業需要申請一些補助案,
而不管還是民間或私人的補助案大多都需要先有公司,才能開始動,
或是拿到補助款;
更別說你如果需要募資,你也得要有間公司給人家當股東啊!
公司開張第一天就是錢在燒,所以錢當然要越快進來越好,
「設立公司」這件事情不該是一個瓶頸,
否則拖延了一兩個月後,公司產品的風頭可能就過了,
好不容易找到的 VC 也跑了。
對了,其實給會計師辦也不見得會比較快,
不過可以確認的是如果你不是自辦的話,公務員會對你比較友善一點 XD
接著就來談談我在過去兩個月的神奇旅程:
我不能說閉鎖型股份有限公司是個完美的制度,
但它確實對新創事業有很大的幫助,
前提是:政府的承辦人員也要這樣覺得。
當我準備好資料準備送件時,旁邊有個服務人員有空幫我檢查,
途中不停的問我:「閉鎖型真的有比較好嗎?」、「為什麼要辦這個?」
當然,基於好奇我也問了他閉鎖型的缺點是什麼,
結果居然被兇了:「反正不是比較新的就比較好啦!」
檢查完後,到了櫃台也再度被詢問:「為什麼要辦閉鎖型?」
後來我才理解,並不是因為閉鎖型有什麼麻煩,
而是我們去辦了他們不熟悉的業務,造成他們的麻煩。
如果只是被問幾下、酸幾句話就結束,
那我覺得也是人之常情,畢竟如果可以簡單,誰想要複雜?
但最麻煩的其實是在公文來回的時間消耗,
下面就來一一列舉:
可能有人會說一天要看幾千份公文,格式正確當然很重要。
你可能會覺得格式跑一點沒差,資料對比較重要。
但格式是否「正確」屬於公務人員可以裁量的部分,
大多都是只要一兩行跨頁或是稍微歪掉,
就算你上面的資料全部都是正確的也沒有用:
假如你用 google drive 來開,
或甚至你用的作業系統不是 windows,
這樣的事情其實是很常見的。
老實說我們到底審查的是那些框框格子,
還是框框和格子裡的字呢?
旁邊雖然有一台電腦可以印,但使用電腦的隊伍常常是大排長龍。
有個很直觀的做法是印一些空白表格,
讓民眾有錯直接手改來填就好了。
但這個意見此時是完全行不通的,
那個告訴我閉鎖型公司很麻煩的公務員說:
「上面有規定,申請表一定要全都電腦印刷。」
好一個上面有規定,一定要用電腦印出來紙筆寫的東西,
這就是我們數位化之後的政府嗎?
至於那個一站式填表單的系統有多難用我也不想再多說,
最白癡的地方是你並不是在線上把表格填完,
而是把表格下載下來,然後再上傳檔案上去。
有蓋章的部分還是要印出來再掃描才行。
假如格式終於沒問題了,
你要面臨的就是你資料不齊、不正確時需要做的事情。
在這個流程中,這件事叫做「補正」,
這一步才是惡夢的開始。
首先是你要看懂補正的內容,有些公文簡直不像是要寫給人看的。
有錯不會一次全檢查出來,會下次再給你驚喜。
舉例來說:
你第一次送的案件有兩個錯誤,承辦人員請你回去補正;
你把錯誤改正、送件後,他可能又會檢查出一個第一次送件就有的錯誤;
而有些錯誤其實公務人員是有裁量空間的,
有幾次需要補正案的原因是因為章程中要有股份轉讓限制,
實際上章程一直都有,只是我們的限制跟範本章程的不一樣,
處理的窗口還要我們改成跟範本一樣,
啊靠,都要照範本走的話幹嘛還自己定章程?
其實很多錯誤都是帶著印章去可以改完的,
只是公務員會用前一項的藉口來跟你說:「要用電腦印出來才行」,
所以你就得回家隔天再來辦一次。
同一個公務員,上一次和這一次說的話可能會完全不同。
以上這兩點看似有點太抽象,
我就直接舉這次遇到的例子吧!
這次遇到的事情是章程上有「錯誤」需要補正,
我當然是詢問能不能補上修改後的章程或是蓋章更改就行。
「不行喔!你們章程要改的話,要再開一次董事會來修章程才行。」
要改幾個字就可以搞定的東西,
要再重新開會、會議記錄、新舊章程比對⋯⋯
只是這些紙本上的東西的確麻煩,
但最要命的還是依公司法規定,
要在會議召開的十天前通知我的董事才能開董事會,
而董事最快也是在公司設立時才選出來,
這也就代表著送件的日期又要再度往後挪。
我:「一定要這麼麻煩嗎?這好像不太合理」
結果就是收到一個斬釘截鐵的答案,跟我說:「一定要照這樣走。」
大概過了一個禮拜多,等到時間可以讓我們再次送件時,
仍然是有錯誤,我和我的夥伴們這時候已經快崩潰了,
因為這代表著要再多等十天。(這時候已經一個月過去)
結果承辦人員想了一下說:「不然你們撤件重辦好了?」
這樣就可以直接送新的章程來,當作之前的都沒送過。
到這裡我已經開始發現人員的問題實在太大了,
一下可以、一下不行,如果一開始就可以的話為什麼不要說呢?
這時候我們還是忍下來了,
因為真的只想快點把這個狗屁倒灶給結束。
不過我也想到幫忙資本額簽證的會計師跟我說過:
「如果送件日離公司發起日超過一定時間要罰錢。」
去看了一下申請須知:
應於董事長就任或取得許可文件後15日內申請設立登記,逾期申請,將依公司法第387條第6項,處董事長新臺幣1萬元以上5萬元以下罰鍰。
於是我們當場詢問了承辦人員這樣會不會有問題,
他也是拍胸脯跟我們保證說他那邊有紀錄,這樣做不會罰錢。
再來就是不同的人,會給你不同的答案。
乍看之下蠻直觀,但跑這種公文時,你不會喜歡驚喜的。
接續上面的事情,這次將更改後的章程給在場的人員檢查。
:「我可以幫你送件,但是你距離董事長就任後已經超過十五天,這樣一位董事要罰一萬塊喔!你能接受嗎?」
接受你老師個教師節快樂啦!
於是我跟他說明:「上一次承辦人員跟我們說你們那邊有紀錄⋯⋯」
接著我的發言被打斷,他表示:「規定就是這樣,沒辦法。」
另一位公務員看著我們情緒有點激動,也過來打圓場:
「我想這是服務人員和民眾間常有的美麗的誤會。」
「他上次可能不是那個意思,應該是說你還是能送件,但是要快一點的話就要罰鍰。」
哇操勒,都給你講就好了,美麗誤會這種屁話都講得出來。
最後,我還是不信邪的抽了號碼牌去櫃檯問,
請上次的承辦人員出來面對,
沒想到他一口就說:「沒問題呀!這個不用罰錢,我們這邊有紀錄。」
千萬記得負責你案件的承辦人員是誰,
有事情問他最準。(相對最準)
辦完後,會有一個 QR code 讓你上網去查詢進度,
有寫一個回覆的期限是十天。
正常來講兩三天內就應該結束。
但我們最後一次就傻傻等了十天,案件進度仍然是審核中,
於是我們就打了電話去詢問。
得到的結果是:「很早就送去長官桌上等他看了,可是長官好像出去。」
⋯⋯所以我們又莫名其妙蒸發掉了十天。
如果以後有要辦這種東西的話,
最好是第二天就開始打給承辦人詢問。
所幸提出申訴之後,
很多東西真的是只要用筆改一改,
簽名蓋章就可以解決了。
也許我可能特別衰小,
但我相信在跑公司設立 RPG 的人中,
我一定不是被公務員刁難的特例。(包括會計師)
甚至,我也認為「公司設立」並不是一個最麻煩的東西。
有位現在也在公家機關服務的朋友告訴我會這樣的原因,
是因為怕送上去的東西被長官挑剔,
所以他們當然不會以民眾方便為主,而是看長官看不看得順眼。
這也就是為什麼他們那麼在乎格式和一些用字的原因。
另外,
許多的審查真的嚴格到像把想開公司的人當成罪犯一樣,
而這樣的防堵機制有堵住什麼嗎?
很巧的是我在第二次去送件時,
就遇到一位氣挫挫的老先生說他被人冒名開了一間公司,
記得很清楚他說:
「他來很多次都沒有人要幫他處理,一定要等事情真的要鬧大才有人理嗎?」
可見這個防堵機制既不夠安全,又麻煩,
同時也點出了這些基層公務人員的通病,
平時亂弄一通,一定要等到人真的要跟你翻臉或是投訴了,
才開始認真幫你做事情。
後來投訴之後才明白,
台北市的承辦人員其實也不太熟悉閉鎖型,連規費也是收錯的。
不過他全部推給櫃台人員就是了,
有一部份的人其實不是公務員,而是約聘,
老實說,整體而言給我一種把錯推給約聘人員的感覺,唉
這就是我們的公家機關,
因為會升官的不是認真做事的人,而是不會犯錯的人,
而不做事,就自然不會犯錯,
這也是為什麼一看到你是不熟悉的業務就一拖再拖的關鍵原因。
還有另一個問題是所謂的「鐵飯碗」,
基本上考上公務員以後,沒有淘汰制度,
也就是說劣質的公務員你也完全拿他沒辦法,
是個在根本裡就追求卡位的制度。
我知道連續三年考績丙等會免職,
但實際執行起來是個鄉愿的笑話,有興趣可以 google 一下,
考核官通常都會讓考績丙等輪流當,這樣就不會有人被免職。
我們這些公家機關服務的使用者,
遇到不合理的事情當然不能跟他摸摸鼻子算了,
這次經驗以後,讓我沒辦法輕易相信這些公務人員說的話,
我希望有一天我們政府機關的行政素質是能夠讓人民信任的,
而不是將公家機關與低效率、劣質聯想在一起。
世大運表示:唉
推薦報導者文章:誰讓台灣公僕變庸才,
總之,底下的執行人員如果是這個樣子,
那不管我們的制度和法規再怎麼先進都沒有用;
如果我們國家真的有要鼓勵創新的話,
先教會這些承辦人員如何讓設立公司變得簡單一點再來說吧!
希望當局者看到以後能有所改善,
不要再讓創業者消耗時間在這種莫名奇妙的地方了。
]]>來拋磚引玉地分享一下一個前端工程師找工作的心得,
主要想分享一下整個「找工作的流程」,
中間也會穿插一下對於徵才者的一些看法。
希望讓求職或徵才的人,都能更了解要怎麽對待前端工程師 XD
題外話是之前有寫過一篇關於前端工程師入門學習的:
PTT 文章代碼:#1MdHy0Kj
網路上還有很多其他優秀的前端學習資源,
只是到底要學習到怎樣的程度才能找到想要的工作,
這中間的隔閡,似乎還是很容易讓人迷惘,
所以今天這篇就自己的經驗來來分享,
身為一個普通新鮮人是怎樣準備「找工作」這件事情,
老實說我覺得重點永遠都不該是什麼研究履歷怎樣排版、怎麼猜 HR 或面試官看履歷的眼動,
甚至問學什麼比較有前途,
我不是說這些事情不重要,這些事情你當然還是得做,
只是如果你只是做了這些而已,往往都是治標不治本的方法。
做一件事情有分「術」跟「道」,
「術」是具體的做法;「道」則是做事的過程和背後的哲學。
而前面講的這些具體作法,我把它們歸類為求職「術」(方法),
這些「術」,不懂寫程式的獵頭可能都比你瞭解,
所以身為一個實事求是軟體開發者,
我們應該要找到自己為了什麼工作,
以及想做什麼工作的本質,
也就是探詢自己的求職之「道」,
懂了之後再搭配方法(術),找工作這件事自然也就水到渠成,
就算有問題,也會比較知道問題出在哪裡。
也許我工作經驗還不足,有想地不夠的地方,
但希望能幫助到還在迷惘的大家,
當然,也希望能省掉一點面試者和被面試者的時間(XD。
註:
這裡的前端工程師指的是不負責出圖,
而是要跟設計師協作、寫 html、CSS、JavaScript 的 web 前端工程師。
整篇文章的結構大概是這樣子:
我的背景介紹
找工作之前
開始找工作
評估自己的能力
找到想去的地方及準備
面試完以後
開價
自己的經驗分享
結論
非四大的學士,不是本科系,
2014 年中開始學習寫程式,
之前完全沒有寫程式的經驗,到現在差不多寫了兩年。
學習的路上碰到蠻多困難,也常常卡關,
覺得自己資質在眾工程師間絕對是屬於中下的,
當年要學寫程式時,我的朋友告訴我:
「寫程式太吃天份了啦!現在才起步太晚了。」
那時候的我看完了 vgod 的「神乎其技的程式設計之道」,
儘管滿腔熱血,
但也清楚這世界上有像 vgod 這種天份好、夠努力,
更別說對 CS 充滿熱情的人在;
只是後來仔細想想,哪個領域不是這樣子?
再者,開始工作和學習也發現:
其實許多人的努力根本就還沒達到要談天份的程度,
就算不跟一些怪物比,這世界還是充滿機會的 XD
而且連我都能做到的事情,相信大概八成以上的人都能輕鬆做到。
這篇文章寫的很粗淺,
主要的目的還是讓新手能省掉一堆繞遠路的時間。
首先要問自己的問題是:
「為什麼要當前端工程師?」
為什麼你要來寫程式?為什麼你不去做後端?
為什麼你不想做 Data Engineer?
這不是一個答錯就找不到工作的題目,
但是從來沒有思考過這個問題,那其實就是在思考上懶惰,
現實做了再多努力也很容易只是徒勞。
而且這個問題的答案也是當你遇到挫折時,
真正有辦法支持你繼續走下去,而不是得過且過的根。
在這裡也分享我對這個問題的想法,
對我來說初期選擇網頁前端的原因是:
後來也發現前端雖然很好入門,
但是前端仍然那麼缺人的原因,
是勞力的供需雙方都出了問題。
首先是「勞力的供給者(求職者本身)」的問題:
正因為網頁前端算是個「好入門」的領域,
所以許多人不把自己當作真的「軟體工作者」,
以為我們只要照著需求拉拉頁面就好,
有的也為了薪水所以跟風跳了過來卻發現薪水不如預期,
這時候就不免得問自己如果這個工作門檻這麼低,
被取代性這麼高,
到底哪來的自信覺得這樣有辦法坐領高薪?
許多「前端工程師」忽略了瀏覽器本身是多麽複雜的軟體,
也忽略了你將來可能要協作的會是一個複雜的系統,
少了一些電腦科學和軟體工程的知識當基石,
面對問題時你會少了很多想像力(這裡指對於解決問題的方法)。
蠻常聽到一些人在抱怨說學校裡教的理論脫節,
老實說,技術的東西會隨著時間迭代演進,
也許教授沒有告訴你這個跟你未來要做的事情有什麼關係,
捫心自問一下,
其實我們是在根本不夠熟悉理論前就嫌他不實用而已。
Computer Science 一直都是一門實用的科學,
但如果只是為了考試而念,那當然就不實用啦!
前端的生態大概每幾個月就會跑出一大堆新東西,
(像是最近很紅的:Vue.js
)
當追隨各種 framework 或是工具時,
很容易顯得浮躁,會學的不好甚至盲從,
更重要的是看一下這個「工具」能解決什麼問題,
細細探究下去會發現「工具」為什麼要這樣設計的哲學,
也會發現其中有許多原本以為很「理論」的東西被實作在裡面,
而這些「研究」就是在鍛鍊選擇和學習工具時的品味,
長期來看,這讓你自己更快速地建立自己的知識體系,
也更快速地提升自己的價值。
只是許多人忽略了這點,
忙著去追逐一些絢麗的畫面、火熱的框架,
這就是對自己的職業定位不夠清楚的緣故。
再來是「徵才者」的問題:
這裡會比較單純一點,
因為大多都是徵才方不知道怎樣面試一個前端,
或者不了解前端的重要性。
有些人心裡覺得:
「啊不就畫面拉一拉、改一改,有什麼難的?」
或是:
「有必要搞那麼多 Framework 或是 pattern 嗎?什麼 MVC、MVVM 的。」
但實際上 UI 實做起來的程式碼很容易就不合邏輯,
要寫出能夠維護、好擴充的程式碼更是需要實務和一定的「品味」,
舉例來說可以看看下面這個Flux challenge,
這是 Cyclejs 的作者對於 Flux 這個單向資料流框架發起的挑戰:
不用下去實作,光是看一下就會發現要良好地控制資料流、畫面的 state ,
以及設計一個精巧的 template 有多困難。
正因為對求職者期待錯誤,薪水自然也錯誤,
到最後如果好運把東西做出來,那可能也跟屎有七八分相像。
當你真的開始重視前端的複雜度和專業時,
自然面試的水平以及來應試者的水平就會上升,
好的企業跟求職者是相輔相成的。
講完雙方,言歸正傳一下,
對於我來說,
後端 API 的邏輯是理性的,
設計師圖稿的美感是感性的,
而前端就是站在理性跟感性中間的介面設計者,
處於這個位置,不管你面向哪邊都能夠看到許多有趣的東西,
而前端本身又一直是一個「生機蓬勃」的領域,
這是我為什麼選擇這個職業的原因,
但這也是我認為前端工程師容易浮燥、忘記如何追求知識本質的原因,
這個之後再提。
這裡不空談什麼人格特質,
只談兩件事情,一為學歷,二為技術力。
學歷:
首先是大家最愛討論的學歷,
老實說,如果不是大公司或是比較傳統的企業,
前端對於這一塊需求還好。
儘管學歷可能不會是一個好的評斷標準,
但在履歷數量大的時候直接用學歷篩,
對於想偷懶的徵才者來說蠻方便的。
另外,學歷是最現實的東西,
假如你跟我一樣學歷不好看,卻又非得進擺明需要四大碩本科學歷的大公司不可,
儘管上網問完有些人會告訴你有作品集比較重要 blabla 之類的,
但八成的情況是這些意見比較多是屬於安慰性質而已,
還是務實一點努力去考碩士或是換個目標吧 XD
把時間花在猶豫和求安慰真的是最可惜的一件事情。
如果你只是單純想要學習的話,你並不需要成為學生,
可以先看看這篇文章
技術力:
前端的東西太多太雜,我不會一一細列,
但對於你每個想火力展示的技術,
我認為都可以分為以下三個等級:
|
|
|
|
|
|
至於到底要放哪些在履歷表上,或是該怎麼展現,
如果你還是沒辦法靠自己完成,
相信我,對自己有了上述的理解,
你也更容易讓別人願意幫助你。
假如只說個我會 jQuery、React、Angular,
除非通靈,不然沒人知道你的「會」是到哪個程度,
身為一個工程師,連自己的規格都描述不清楚那是非常不專業的事情。
假如你已經很有經驗,獵人們自然會排隊幫你找工作;
反之你就得自己做打算,
但公司種類簡直跟前端框架一樣也是百百種,
如果有任何文章提出一個應付所有公司的解法,那一定是在扯淡。
這階段真正要解決的問題是:
「你到底想要在哪裡工作?」
很多人會抱怨把 104 打開或者是投履歷都沒人理或者沒回,
其中一個很大的問題是因為這是個「海投」的策略。
將心比心的問題,身為一個求職者,
你會希望自己被好好對待,
可是「海投」卻是一個讓你沒辦法好好對待每個機會的策略,
這不見得對於每個人都成立,但我認為對方如果要付出面試時間,
自己也應該準備足夠,讓對方沒有在浪費時間的感覺,
正因為每個選擇都要付出很多時間,
所以有沒有想清楚是不是非這間公司不可就顯得非常重要。
接著繼續解開「到底想在哪工作」這個問題,
前端工程師目前在台灣是一個有點尷尬的職位,
光是看許多人套 bootstrap 就稱自己 fullstack,
就知道蠻多公司不重視前端這個工作,
所以挑選時,除了各方上網搜尋意見之外,
也別忘記去公司的網頁 inspect element 看看 XD
不要想著進去一間公司就要待到老死,這樣對你和公司都是壞事,
(想待到老死去考國營就好了,不用在這塵世間浮沈)
先設定一個目標和時間,比如說待兩年,
然後問問自己這兩年你要完成什麼事情,待在這間公司有辦法幫你達到嗎?
很多職涯相關的文章已經講說面試要準備問題問面試官講到爛掉,
但每次提供的問題都蠻垃圾的,有時候會有點分不清楚內容農場跟媒體的差別。
總之「要問面試官什麼」的根本就是上述那個問題,
所有你想要的,都應該 contribute 到你想完成的那件事情上,
而有了這個問題當作基礎,就很好延伸思考了。
當你現在準備和了解的越少,
你未來可能要付出的代價越大。
談薪水其實已經偏向「術」,只是這個階段實在太重要 XD,
所以特別講一下。
一定要開一個讓自己覺得有尊嚴的薪水,
不要因為很怕自己不值得這個錢而開一個自己無法接受的低價錢,
不然你進去才一直覺得公司欠你,心態會漸漸扭曲。
另外這裡沒辦法寫的很具體,
是因為我實在無法評斷會某技能,就值多少錢,
一來,會到什麼程度只有自己最清楚,也關係到面試怎麼展現;
二來,地區、產業、公司的不同,都會大大影響。
如果前面的準備有做好,
這時候應該對自己的喊價應該要充滿信心才對。
一面試完,
要先思考的問題就是:
「你有多想跟面試你的那些(個)人一起工作?」。
就算未來不是你真正 co-work 的人,
我相信待在同一間公司的人都會有某些相同的特質。
面試要考倒一個人是相當容易的,
所以要自己過濾一下哪些明顯是在刁難。
(履歷上完全沒提到自己會,卻又故意問非常深入的部分。
比如說應徵前端結果問你熟不熟 CSP 和 Actor model的差別)
再來就是檢討自己有哪些不足的,簡單分可以分成兩種:
一,短期內學習就有效果的。(e.q:某個東西的 API 不熟悉,或是某個名詞你不知道是啥)
二,需要長期養成基礎的,要再擬個計畫來補強。(e.q:演算法不熟)
接著再更改履歷,
強烈建議用 git 來管理自己的履歷,
你也更容易從中看到每個版本的自己有什麼進步,
而針對不同的公司只要新開一個 branch 就好了。
每次面試完以後,我也會回到再度「評估自己的能力」一次,
如果你準備的夠足夠,
面試完時通常都能獲得更多,
因為你去的都是你認可的公司,同時你每次也是全力以赴。
這裡首先來分享自己曾經忽略的幾個點。
什麼資料結構、演算法,或是作業系統就不再提。
網路:
瀏覽器:
然後是一些迷思:
我的經驗是如果單純為了找工作才刻意去做這些事情,
那很可能會無法持之以恆,
我寫的所有技術筆記都是為了讓自己以後忘記時有東西可以參考,
同時我也認為我們平常工作上靠 google 得到那麼多資訊,
自己應該有一些回饋的責任,
而且如果搜尋問題的前幾個結果能是繁體中文的文章,
那會是一件很莫名令人開心的事情。(XD)
總之如果這些事情是你真心認為想做再去做才有意義。
人脈真的很重要,而分享和吸收新知也相當有意義。
我記得第一次參加大型技術 conference 時,
看著世界上許多人技術和商業都如此強勁,
真的會起到相當大的激勵作用。
不過就這陣子的觀察,有些活動很近似於大拜拜的行為,
好像參拜完大神會得到靈魂上的昇華一樣,
這不是一個很健康的現象。
參與社群時不應該時時想著從中得到什麼,
假如得到了什麼,更要思考著該怎麼樣去做有效地回饋才是。
我沒有上過任何上述的課程,這裡也不想談論自學和上課哪個好。
但這些課程中,大多都是在業界實戰過的老師,
出來的學生素質會參差不齊的原因很有可能是因為:
他們沒真正想過「為什麼想成為前端工程師」,
去上課可能是一個更有效率建立知識系統的選擇,
但把上課當成一個速成脫離低薪的「速解法」,
就很容易高不成低不就,甚至怪這些課程沒有用。
最後想要分享一下之前在 twitter 上看到的這段話:
「
你使用的語言被人嘲笑
職業被藐視
解決問題的努力被視為噱頭
自詡不弄髒手的人輕視你面對的問題
執行引擎到處是實作者從未重視過的 Bug
所有跨平台的承諾都在增加你要解決的問題
UX認為要的效果很簡單
QA給你的回報難對上問題
最後,報酬通常低於平均水準
你說,你想成為前端工程師
」
網頁前端的迭代速度跟特別的技能樹,
讓這個職業既有吸引力又讓人容易感到挫折,
希望透過這個分享讓大家更了解前端工程師,
也讓對前端有興趣卻不敢踏出那一步的人可以試試看。
主要在探討 text mining 和 分析,
經過我整理之後應該會變得好懂很多。
這是第二個禮拜課程的筆記,
假如你還沒看過第一個禮拜,
可以先看這裡:
這一週裡面會包含三個主題:
Syntagmatic relation discovery
Topic Mining and analysis
Probabilistic Topic Models
在開始之前先快速複習一下上禮拜學習的東西,
試著去解釋和回答以下幾個問題,
如果還會遲疑的話可能要回到第一篇稍微看一下:
deep techniques 和 shallow techniques 的 trade-off 是什麼
了解為什麼 NLP 是困難的
Paradigmatic relation 以及 Syntagmatic relation
Paradigmatic 以及 BM25 加上 tf-idf
這一章節可能會有比較多特別的名詞或公式,
不過拆開來一步步理解,
他們其實都是蠻直觀的概念,
我以前也是因為這堆符號嚇到被老師當掉過,
實際去學才發現,
嗯老師考卷還是出太難馬的幹
其實背後的本質就是簡單的數學而已。
要找到 Syntagmatic relation,
要觀察的就是 co-occurence,(同時出現的機率)。
為了培養一下對於挖掘組合性關係(Syntagmatic relation)的直覺,
直接來看下面這個問題,
有以下三個字,
你覺得哪個字最好去預測它在文本中會不會出現呢?
這裡的文本指的可能是 pseudo document、句子、段落
$$ W_1 = “meat”$$
$$ W_2 = “the”$$
$$ W_3 = “unicorn”$$
答案會是 “the”,因為它到處都是嘛!
再來可能會是 unicorn,因為這不是個常用的單字,出現機率可能很低。
矽谷表示: __
最後則是 meat,因為他出現的機率最為「難預測」,
你很難精確說明怎樣的情形「一定會」出現 meat,
也很難解釋什麼條件下「一定不會」出現 meat。
上述的情境其實可以當作一個 function 來看,
輸入是某個字(meat, the, unicorn),
輸出則是它會不會出現在文本中,
會就是 1,不會就是 0。
而上述的這個概念就是「隨機變數(Random Variable)」,
沒錯,我一開始也很疑惑,
但隨機變數就是一個函數,
數學家才不管什麼可維護性,naming 這種東西就是他們說了算,懂?
將一個機率空間中的值(子集合)對應到一個實數上(這裡是 0 或 1),
這就是 random variable。
$$
X_w = 0 \, or \, 1
$$
如果 w 存在文本中就等於 1,反之則為 0。
所以 X_w
如果越隨機,就越難預測,
問題來了,該怎麼去量化誰「比較隨機」呢?
要靠的是 Entropy(熵),不要被這個名詞嚇到了,
其實它也只是一個值,算出來越高,
代表這個 random variable 越隨機,這也就代表越難預測。
來看它的公式怎麼寫:
$$
\sum_{v = 1 \, or \, 0} -p(X_w=v)log_2 p(X_w=v)
$$
這裡為了方便計算,要假設 log_2(0) 為 0。
接著拿執硬幣的機率來舉例,
假設有個公正的硬幣,兩面出現的機會相等,都是 0.5;
另一個只會擲出反面的硬幣,
哪一個會算出較高的熵?
首先把公式列出來看:
$$
entropy = -p(x=正面)log_2(p(x=正面))-p(x=反面)log_2(p(x=反面))
$$
再來先帶入公正的硬幣:
|
|
$$
H(X) = -\frac{1}{2} log_2(\frac{1}{2})-\frac{1}{2} log_2(\frac{1}{2}) = 1
$$
而帶入
|
|
$$
H(X) = -0 \times log_2(0)- 1 \times log_2(1) = 0
$$
看的出來公平硬幣的熵較高,
而在數學上證明了這個直覺是對的,
公平硬幣比起只會出現一面的硬幣來的隨機多了!
很快地會發現,前面講的只是單一個字會不會出現的隨機性,
換言之這裡只計算了 occurence;
但我們真正想要知道的是跟其他字同時出現的 co-occurence 啊!
這時候就是 conditional entropy 上場的時候。
當知道字句中有 “eats” 這個字存在時,
出現 “meat” 的機率可以表示為下面這樣
$$
P(X_m = 1 \mid X_e = 1)
$$
所以直接把 entropy 公式中的P(X_m)
換成 P(X_m | X_e)
即可算出 conditional entropy:
$$
H(X_m \mid X_e = 1) = -p(X_m \mid X_e =1 )log_2(p(X_m \mid X_e =1 ))
$$
而 H(X_m)
會是 H(X_m | X_e=1)
的上界
因為
H(X_m)
=H(X_m | X_e=1)
+H(X_m | X_e=0)
這裡隱含著的道理就是「做的任何事情都是要降低不確定性」,
conditional entropy 不會超過原本的 entropy,
所謂的文本探勘,就是一個降低不確定性的過程,
而我們也發相資料間要彼此相關,才能夠有效的降低不確定性。
有了這個數學上的直覺、清楚我們目標在哪後再繼續。
上述知道使用 conditional entropy 可能會降低不可預測性後,
就要來問:要怎麼量化減少的程度為多少?
這時候就會用到 mutual information,
它代表的意義就是當我們知道其中一個詞時,
到底降低了多少的不確定性,
而兩個詞之間,相關的程度到底是多少?
這時候就要來介紹 Mutual Information,
Mutual information 用數學符號表示:
$$
I(X;Y) = H(X) - H(X|Y) = H(Y) - H(Y|X)
$$
圖大概是長這個樣子:
而 I(X;Y)
有三個特性:
大於等於 0
對稱性:
$$
I(X;Y) = I(Y;X)
$$
I(X;Y)
會等於 0(完全相關時,則為 1)這樣講可能還是不太能理解,所以我舉數學之美中的例子。
Bush,是翻作布希總統還是灌木叢總統呢?
要讓機器分辨這件事情,
如果有人沒理解過 NLP,會說如果後面是接總統或職稱,那就是總統,
不是的話,那就是灌木叢,
事實上我們都知道這樣的做法會讓語法的計算多到無法進行計算,或計算過於緩慢;
因此我們應該要利用的就是 Mutual Information,
我們先找到文本中提到布希是指總統時的文本,
並計算其中 Mutual information 最高的字:總統、美國、國會⋯⋯等 (a)
,
而灌木叢也是同理:森林、野生、土壤⋯⋯ (b)
。
最後在計算時,只要看上下文中的詞,是 a
還是 b
比較多就可以了。
前面是從熵的角度去切入,
更 practical 一點的做法是用機率的角度去看,
這裡介紹 KL-divergence 這個公式,
(或稱作 relative entropy 相對熵)
格式請見諒,mathjax 在同一行要使用 X_{..}系列時會出錯
如果你想知道這個公式的數學意義下面會有解釋,
但其實你只要記住:
相對熵越大,兩個函數差異越大,反之亦然。
假如今天輸出的是兩個分配,只要取值都大於 0 ,那麼相對熵也可以表示這兩個分配的差異。
kl-divergence 最值得注意的是 log2 裡面的這一項:
$$\frac{p(X_w1 = u,\, X_w2 = v)}{p(X_w1 = u) p(X_w2 = v)}$$
如果X_w1
跟 X_w2
是相互獨立的話,
分子分母會相同,
因此這一項會趨近於 1,
再取 log 後就會變成 0,
這樣在計算 divergence 時就會把這一項相互獨立的給去掉。
算 mutual information 的意義在於:
「因為我們想了解如果知道其中一個字,
到底能下降多少的不確定性?」
而兩個字出現的機率如果是完全獨立,(意思即 w2 出不出現都跟 w1沒差)
那 mutual information 自然要等於零,
因為知道其中一個字完全不會影響另一個出現的機率,
自然也不會降低任何不確定性了。
這裡是對獨立事件的小小補充,
講的並不嚴謹,
如果你對於 joint probability 和獨立事件沒有問題的話,可以跳過這一段
或是指正一下我哪裡的說明有誤 XD
如果你沒有修過機率論的話可能會覺得:
「欸? p(X_w1 = u, X_w2 = v)
不就等於p(X_w1 = u) * p(X_w2 = v)
嗎?」
但很可惜事實並非如此,用符號來表示並不直觀,
幸運的是機率這種東西用舉例的來觀察就會很直覺,
我們舉下面這個例子來說:
我們有四個 document,
X 則是 document 中有出現 w1或 w2 的隨機變數,
有出現 = 1,沒出現 = 0。
document | X_w1 | X_w2 |
---|---|---|
d1 | 1 | 0 |
d2 | 1 | 1 |
d3 | 1 | 1 |
d4 | 0 | 1 |
p(X_w1 = 1)
= 0.75p(X_w1 = 0)
= 0.25p(X_w2 = 1)
= 0.75p(X_w2 = 1)
= 0.25
而 p(X_w1 = 1, X_w2 = 1)
則為 0.5,
但 p(X_w1 = 1)
與 p(X_w2 = 1)
直接相乘卻是 0.1875,
由此可見這兩個在有相依性存在的情況下,
可能會不相等 XD。
但假設今天是擲一枚公正硬幣兩次的話:
p(x1=正面)
* p(x2=正面)
= 0.25
p(x1=正面, x2= 正面)
= 0.25
因為投擲一枚硬幣兩次是獨立事件,
而在更上面的例子中,
p(X_w1)
與 p(X_w2)
並沒有完全獨立。
基本上有這個概念就已經夠了,
如果你還是很有興趣可以去翻翻機率論,
我大學時候修過,覺得真的⋯⋯很快樂 ^_^。
前面對 Mutual information 有了概念以後,
下來就是真的去計算它,
並且利用它來找出 syntagmatic relation 了。
我們要用的方法是 Maximum Likelihood Estimation(最大概似估計)。
這名字聽起來一樣很炫砲,
但它的想法其實超簡單!
只要不是出現在數理統計裡的話
舉一個常見的例子:擲硬幣。
擲硬幣可以表示成一個 random variable: x
,
假設這個 x
的機率分佈是 p(x)
。
但其實我們不知道這個機率是不是公正的,
先進行了一次觀察,
而在這次觀察中擲了一萬次,然後就媽媽手
結果發現很剛好的有 5000次正面、5000次反面,
因此觀察後得到了一個 p'(x)
,
假設觀察後的 p'(x)
跟真正的 p(x)
分佈一致,
那 p(x)
產生 p'(x)
的「可能性」就是「最大」的。
用「觀察出來的機率分佈」去推論「真正的機率分佈」,
並最大化觀察結果等於真實機率分布的可能性,
這就是「Maximum Likelihood Estimation」的精神啦!
有了上述直覺後,再看看更嚴謹的數學定義,
我們會寫出一個 likelihood function:
$$
arg \, max_p p(x) = argmax \prod_i p1(x_i)
$$
我們要找可以最大化這個 function 的機率分佈: p。
argmax
:這個符號的意思就是要找出輸入後能夠最大化後方這個算式值的 arguments,
有時候很佩服數學家想得到這麼奇葩的符號表示法 QQ
再來看最後一個簡單例子,是從陳鍾誠老師的網站上引用的。
可以表現如何套用這個公式並且指出 Maximum likelihood estimation 的問題。
假設我們擲了一枚硬幣十次,有六次為正面,四次為反面。(這個擲十次硬幣其實就是一個 random variable: X)
我們看看下面三個機率模型可能產生 X 的機率有多少。
|
|
把上面的機率模型帶進去 likelihood function:
|
|
會發現 p3
這個得到的值最高,
可是對於投硬幣來說,其實 p1
才是更正確的模型才對,
這也就是 MLE(maximum likelihood estimation)的缺點,
如果樣本數太小或是有很嚴重偏誤的話,我們很容易見樹不見林 XD。
總而言之,簡單地去推論 P(X_w1=1)
、P(X_w2=1)
、P(X_w1=1, X_w2=1)
出現的機率:
為什麼找這三個是因為找出來後,
其他都可以從這三個機率去推導
我們就從這些觀察值中去假設機率是這樣,
就是用到了前面敘述的「最大概似估計方法」。
不過這樣的做法有個顯而易見的小缺點,
當我們觀察的樣本中,有些字可能從來沒出現過時,
它的機率會被估計成 0,
但我們的樣本可能相對很小,
直接就這樣斷定這個字不可能出現絕非好事。
所以要來介紹一下一個小技巧:Smoothing
smoothing 的做法很簡單,
就是對於每個情況發生的次數,
都加上一個小小的 constant。
如此就算我們觀察的樣本中完全沒有出現過某個情況,
這個情況也會有一個小小的次數在,
而不是直接斷定其機率為 0。
雖然機率為 0 也不代表不可能發生,
但這就不在這篇文章討論範圍內了。
複習一下三個從訊息論來的概念
Entropy(H(X)
):量化隨機變數 X 的不確定性
Conditional Entropy(H(X|Y)
):在給定 Y 提件下隨機變數 X 的不確定性
Mutual Information(I(X;Y)
):描述 知道 Y 或 X 其中一個後,會降低多少預測的不確定性
在這階段得到的 word association 是相當 general 的,
不管後續要做什麼應用,
都有辦法跟其他算法結合。
掌握了這些基石後,
就可以來應用在稍微 deep 一點的技巧上了。
前面一個禮拜多有了對詞的基本了解後,
要來做進階一點的分析和挖掘:Topic mining。
topic 其實就是一堆文字中的「主旨」(main idea)。
隨著資料顆粒度不同,會有所改變。
句子、文章、書籍的 topic 萃取會大大不同
先找到有哪些 topics
將文本資料(document)涵蓋各個 topic 的機率找出來。
有了這個直覺後來更嚴謹的定義一下要做什麼,
Input:
text documents 的集合 C={d_1, ..., d_N}
Number of topics
Output
k topics: {theta_1, ..., theta_k}
Coverage of topcis in each d_i
: {pi_i1, ..., pi_ik}
$$
j = 1, 2 … k
$$
$$
i = 1, 2 … N
$$
$$
\sum \pi_{ij} = 1
$$
再來的問題就是怎麼找出 theta 了
最直觀的方法就是從所有文本中,
挑出可以當作 Topic 的「詞」(word = term)。
Parse 所有的 text 拿到 candidate terms
設計一個 scoring function 來判定各個 candidate term 是不是適合當作 topic
選擇 k 個有最高分的 terms,但盡可能最小化冗餘
接著就直接來看看 term as topic 的做法。
最簡單的方式就是直接計算次數,觀察其佔的比例:
簡單的說 pi
就是每個 term 在每個 document(d_i
) 中所佔的比率,
而 theta
就是各個 term 了。
最後算出來的 pi_ij
就會是theta_j
/(document_i
中所有 term 出現的次數),
其實看的就是選的這個字在某份文件中跟其他 topics(這裡是 terms)所佔的比率,
最高的話,就判定它可能會是屬於 topic。
但是直接拿實際例子來檢驗,
馬上會發現一個大問題:
以下節錄自一則 NBA 球賽的報導:
Cavaliers vs. Golden State Warriors: NBA playoff finals … basketbal …
travel
to Cleveland …star
…
$$
\theta_1 = “sports”
$$
$$
\theta_2 = “travel”
$$
$$
\theta_3 = “science”
$$
出現 “sports”的次數為 0,
而出現 “travel” 的次數為 1。
所以這個運動新聞的 topic 在上述方法中反而會被歸類到 sports 去。
很明顯的,不能只看 “Sports” 這個字,
應該要把相關的字也加進來才對。
但如果把 related word 也加進來,
那 star
這個字就會有點 tricky 了,
因為 star
有模糊的空間可以被解釋,
除了表示明星之外,它也可能是指真正的星星,
會與 science
產生聯繫(天文物理學)。
總結一下 term as topic 會遇到的問題
Lack of expressive power
Incompleteness in vocabulary coverage
Word sense ambiguity
star
問題)假如 term as topic 行不通就兩手一攤說沒辦法那也太遜,
所以馬上要來優化前面的 topic mining 方法:
回顧一下上面提到 term as topic 會遇到的問題,
以及概念上應該要如何解決:
Lack of expressive power
解決方法:
Topic: {Multiple Words}
一個字當 topic 不夠,你有試過好多個一起嗎?
Incompleteness in vocabulary coverage
解決方法: 對於 word 加上權重
Word sense ambiguity
star
問題)解決方法: Split an ambiguous word
首先要做的事情就是小小的改變一下選取的 term: theta
,
它不再是一個個的單詞,
而是一個詞的機率分配,這絕對是概念上的一個大進步,
先記著 theta
代表的是機率分配這件事,
直接看下方這張圖:
舉 “Sports” 為例子,這代表 “Sports”這個分配底下,
裡面會出現詞的機率就是長這個樣子。
這裡的機率分配已經做過 smoothing 的處理,所以不會有 0
$$
V = set(w_1, w_2, …)
$$
這其實就是字典檔,裡面裝滿了各種 words(w
)。
而令人好奇的是 p(w | theta)
代表的意義是什麼,
舉例子可能會清楚一點,
theta_1
= topic 為 “Sports” 的機率分配,
p("game" | theta_1)
的意思就是在知道屬於 theta_1
的條件下,
“game” 這個字出現的機率。
也因此把每個 topic 當條件下出現的機率加總起來就會是 1。
上面那張圖中也把跟 topic 較不相關的字標成黑色,
可以看到它們在所屬 topic 下出現的機率明顯是較低的。
看一下這種把 theta
當分配來看待的做法有沒有解決上述問題:
解決方法: 一個 topic 底下已經可以有多個字,這些字組合起來可以變成一個相當複雜的 topic
解決方法: 現在每個字在所屬 topic 中都有其自己的權重
解決方法:現在在不同 topic 中出現的同樣字會有不同的機率
其實我也蠻驚訝只是換個表現的方式,
就能夠帶來這樣的成效,
下面就正式用機率模型的方式來表現 topics:
注意:theta 是一個 term 的「分布」
而 pij
就是該 topic 在這個 document_j
中的「覆蓋率」,
假如 pij
的覆蓋率越高,代表它越可能是屬於 theta_i
這個 topic。
雖然前面已經提到用機率模型的方式來呈現 topics,
但是要被分析的原始文本資料(text data),本身是沒有機率分布的,
所以要自己「生成」一個有機率分佈的 model 給它們,
這也就是我待會要介紹的概念: Generative model。
概念就是如上圖這個樣子,有以下幾個步驟:
Modeling of Data Generation: P(Data |Model, lambda)
lambda 是所有會產生 text data 的參數:
包括{theta_1,...theta_k}, {pi_11, ..., pi_1k}... {pi_N1, ... , pi_Nk}
要求得的答案是:知道哪些參數後,能夠讓我們在知道這些參數的條件下,讓產生出 Data
的機率最大化。
如果是上面圖來說的話,就是 lambda*
這個 parameter
上述資料的 lambda 是一維的,只是為了方便圖像化,
事實上的 model 不太可能長這麼簡單 XD
其實就是一連串字的機率分配
p("Today is Wednesday")
~= 0.001
p("Today Wednesday is ")
~= 0.00000000001
上下文相關( context dependent)
就是一種用機率方法來生成文字的 model,所以也可以稱作是 “Generative model”
介紹完概念,就直接來看有哪些 model 可以用。
這是一個最簡單的 model,
它假設產生的每個 word 都是相互獨立的。
假如你學過 naive Bayes 就會發現這比 naive 還要更 naive
例子:
$$
p(“today \, is \, Wed”) = p(“today”)p(“is”)p(“Wed”)
$$
再來看關於 Unigram LM 更實際的例子:
topic 1 是 text mining,
下方的 model 就是已知在這個 topic 的條件下,
各個字出現的機率分配。
從這個分配中去取樣本組成一個 data,
那很自然就會是屬於 text mining 的 document。
(Topic 2 就不再重複敘述)
但反過來從看到 data ,去判定它屬於哪個 topic,
才是我們真正想做的事情。
最直觀的方法就是前面提到的最大概似估計(Mamimum Likelihood Estimation):
但最大概似估計有它的限制,
在上面這張圖中的下方是我們去做分析和探勘時,
要一再問自己的兩個問題:
這就是最好的方法了嗎?
何謂「最好」?
接著繼續探討怎麼去更優化這個估計方式。
Maximum Likelihood 的問題:
舉例:
投擲硬幣兩次都正面,就估計這硬幣出現正面的機率為 1,
可見當觀察的資料過少時,很容易得到有極大偏誤的結果
在這裡要介紹另一個簡單,但是相當高效的方法:
Bayes Rule,也就是大家常在說的貝氏定理。
那 Bayes Rule 的核心概念是什麼呢?
直接舉個例子來說明:
假設現在有一個袋子裡面有 5 個球,
其中 4 顆黑球,1 顆白球,
很明顯地,拿出黑球的機率就是 4/5 = 0.8。
注意,這是在「知道所有情況下」才能這樣簡單的就拿到機率,
但是在現實生活的分析中,
常常會不知道所有的情況,
而貝氏機率的哲學就是從結果逆著去推論現實生活中的情況。
舉上述例子就是從袋子裡拿球三次,其中出現一次黑球、兩次白球,
再從這些觀察的結果去推論拿出球顏色的機率分布是什麼,
這就是 Bayes Rule。
這聽起來很像 Maximum likelihood estimation,
不過他們本來就不是相斥的存在,
先耐著性子繼續看下去要怎樣應用它。
假如你不懂條件機率和貝氏定理的話,強烈建議你看一下這篇文章:
看完這篇補充資料前面的例子之後,一定就懂貝氏的概念是什麼了。
這裡不花篇幅去解釋太多,專注在 Bayes 在統計語言模型上的應用
從最大化 P( X | theta)
變成最大化P( theta | X)
或是 P(X | theta) * P(theta)
,
P( theta| X)
的意思是:在符合 X 條件下,是屬於
theta
這個 topic的機率。而它也不會跟
P(X | theta) * P(theta)
相等,而是成正比的關係。
所以關鍵問題就在於如何定義 prior ( p(theta)
)。
看完例子後,
我想直接拿課堂簡報中的圖來說明比起直接用最大概似估計,
使用 Bayesian estimation 有什麼樣子的進步:
你會發現最右邊的那個分配就是 MLE 得到的結果,
我們要找的那個是介於 prior 跟 MLE 中間的 posterior mode,
所以這個 estimator 也叫做 MAP(Maximum posteriori estimate)。
除了比直接用 MLE 考慮得更多一些之外,
也更加 general。
前面也有提到貝氏和 MLE 的關係,
這裡其實也有個很直覺得解釋,
那就是如果 P(theta)
是個 uniform 分配(也就是說每個出現的機率都一樣時),
那基本上就等於是沒辦法提供任何訊息,
因此等於就是在求 P(X|theta)
,
即原先使用 MLE 的方法。
接著來運用上述 Bayes rule 的模型,
不過為了專注在概念的解釋上,先用了個簡化的版本,
Mining one topic 的意思是假設只有一個 topic,
所以 input 從原先的: C={d}, V, k
,
變成 C={d}, V
(k 固定為 1),
因此也不會有 pi
這個 output,
畢竟在這個例子裡不用去算在每個 document 中的 coverage 。
只有一個 topic 的話,那代表不管在哪個 d 裡面,
coverage 都會是 100%。
首先要來定義建立 Language Model 前該有的設定:
比較需要說明的是 Likelihood function,
後續會相當頻繁的遇到這個 function ,
所以理解它背後的含義是相當重要的,更別說它其實是個很直觀的函數 XD
這只是數學符號比較多,但沒有多出什麼新的東西來。
前面有說到我們用的模型是 Unigram LM,
所以假設每個詞出現在 text 中的機率都是相互獨立,
因此才能直接用相乘的來取得。
接著看到 p(x_1 | theta)
.. => p(w_1|theta)^c(w_1, d)...
的轉化,
看起來很嚇人,實際上只是從 random variable 轉化成一般機率的形式而已:
x_i
其實是指 w_i
出現在 document: d
中的隨機變數,
是「機率」。
在右上角的 c(w_i, d)
指的是 w_i
真正出現在 data d
中的次數,
因為在 Unigram LM 中,每個字出現的機率是相互獨立的,
也就是說同一個字重複出現的機率也是獨立的,
把 p(w_i | theta)
相乘起來就會是這個字出現的機率,
也就成功從 random variable 轉化成一般機率的形式。
弄懂 likelihood 後,
就能直接來找哪些參數 (theta_1,... theta_M)
可以最大化 p(d| theta)
。
$$
\prod_{i=1}^M \, \theta_i^{c(w_i, \, d)}
$$
為了方便計算,將連乘的部分取對數,
這樣做的好處就是直接化成相加的形式:
$$
\sum_{i=1}^M c(w_i, d) log \theta_i
$$
這樣做的意義完全就是為了數學上好算 XD
記得前面 theta
滿足這個限制後:
$$
\sum_{i=1}^M \theta_i = 1
$$
再回到原本要解決的問題,
怎麼找到有辦法最大化下面這個函數輸出值的 theta
呢?
$$
\sum_{i=1}^M c(w_i, d) log \theta_i
$$
答案是—— Lagrange Multiplier Method。
想瞭解更多關於 Lagrange 的原理可以看這邊
我大學最討厭的就是天才少年一號尤拉,以及天才少年二號 Lagrange。
你可以點上方的參考資料去深入瞭解,
不過你也可以在不知道原理的情況下繼續直接套用公式,
只要懂得簡單的微積分就好 XD
要找參數有辦法使一個凹函數出現最大值,
只要取個微分等於 0,再去解開參數等於多少就可以。
Lagrange 就是將原本的 function 轉化成以下這個形式:
$$
\sum_{i=1}^M c(w_i, d) log \theta_i + \lambda( sum \, of \, (\theta_i) - 1)
$$
前面有提到:
$$
\sum_{i=1}^M \theta_i = 1
$$
所以加上 lambda 那一項是不會影響 function 本身的,
但是加上 lambda 之後,我們在微分後可以多做許多事情:
這時候可能你會有個感覺:這只是個數學解題而已嘛!
最後可以得到 theta
與 lambda
間的關係,
再度回到上面 theta 加總起來是 1 的等式,
然後把
$$
\theta_i = - \frac{c(w_i, \, d)}{\lambda}
$$
代入
$$
\sum_{i=1}^M \theta_i = 1
$$
吃我 der lagrange 喇
最後再將得到的結果
$$
\lambda = -\sum_{i=1}^N c(w_i, \,d)
$$
代入
$$
\hat{\theta_i} = \frac{c(w_i,d)}{\lambda}
$$
上面加了一個小帽子唸作 hat,在統計學裡面這樣標記,
通常表示它是代表「估計」出來的機率,
而非是真正的機率。(真實的機率其實不得而知)
這個結果相當相當的直覺,
得到的結果其實就是個 normalized count。
直接拿 word 在 document 中出現的次數除以 document 的總字數。
前面敘述的模型有著我們從第一個禮拜就在討論的問題,
就是它沒辦法過濾掉 “the”, “a” 這種每個文章都有的常見字,
這些常見字其實沒有跟 topic 有很高的相關性,
但在 p(w|theta)
時卻會有著不低的機率。
不過下一個禮拜的課程才會進入要怎麼解決這個問題。
這一週蠻像以前在學機率論的,不過是簡化版的。
而且相較於以前學習的純理論,到考試再被證明題屌虐一番,
這一次有種 divide and conquer 的感覺,
我們有個很明確的目標和問題要解決,
至少我自己在經過這禮拜課程後,
對於使用統計語言模型以及 topic mining 有了初步的理解,
雖然看到 Lagrange 的時候罵了幾句髒話,
但從基石開始慢慢學起的感覺就是這個樣子,
多了點思考和研究,少了很多很多的浮躁。
也推薦看完後可以去 Coursera 上做做練習題,
看看自己是不是真的懂了。
最後補充一下為什麼有些東西都重複解釋,
其實訊息的冗余對於保存訊息是很有意義的,
當初我們有辦法解密古埃及文,
就是因為羅賽塔石碑上用了三種語言重複了一樣的訊息,
語言學家才能把上面的資訊解密,
這裡的訊息就是對於 text mining 的知識,
我相信這也是相同的道理。
數學之美:《第 6 章:信息的度量和作用》
所以開始上一門 coursera 上的課程,
主要在探討 text mining 和 分析,
會停留在比較 general purpose 的理論和演算法上,
乍看之下其實有點無聊,
但我覺得原因是課堂的教授預設我們有許多預備知識和對數學的靈敏度,
實際上它的內容非常的扎實,而且講解的也很深入,
經過我整理之後應該會變得好懂很多,
因為我不像那些教授一樣是天才 XDD
大學也沒修過這類型的課程,只是因為興趣所以研究這個。
可能就如同每個在網路上看到這篇文章的人一樣。
如果是跟我一樣的 NLP 新手,這一系列筆記應該會對你很有幫助,
並且成為你往更深層分析研究的基石。
這裡推薦一下大家去看吳軍教授的「數學之美」,
裡面有提到相當多關於「語言」以及電腦科學在自然語言處理上的介紹。
語言其實就是承載知識的載體,
仔細想一想我們其實能從大量的文本中整合出各式各樣的主題(topic),
甚至能從中汲取出作者的觀點來,
但是該怎麼做呢?
原本的自然語言處理領域認為,電腦要學會看文字,
應該要跟人類一樣從文法和字母開始學起,
但後來發現地球上有這麼多語言,每種語言又有特殊的文法,
而文法更是會隨著時代改變,
漸漸的,大家發現這條路好像不是這麼行得通。
更別說有 context dependancy 的語法分析的時間複雜度高達 O(n^6)
經過數十年在學術上的努力後,
終於發現用統計模型來讓電腦理解語言才是正途,
這也是現在在搜尋引擎中所用的技術之一,
而且無關什麼語言或是什麼樣的句法,
都能夠用統計語言模型的方法來表示。
我想如果大學教授有跟我提這件事的話,
我現在可能就不會是個寫程式的人,會是個研究統計學的狂熱者了 XD
這堂課主要就是做初步的去探討怎麼樣把「文本資料」,
轉換成我們能夠做運算的模型,
進而讓電腦理解。
有了這些基礎後,才能更簡單的運用電腦擅於運算的特性,
讓我們更快速的運用這些從文本上挖掘到的知識。
這是一門專注在 shallow techniques 上面的課,
但內容可一點都不膚淺,後面會再提到何謂 shallow,何謂 deep。
流程:
Natural language processing & text representation
Word association mining & analysis
Topic mining & anaylysis
Opinioon mining & sentiment analysis
Text-based prediction
來看一下流程圖:
上述的流程中,
你會發現越前面的步驟其實需要越少人工介入,但沒辦法挖掘更深層的資訊,
越趨近於 shallow techniques。(不深嘛)
越後面則是越趨近於 deep techniques。(不淺嘛)
Shallow techniques ,雖然得到的知識量比較少,但比較 general,不會因為領域不同被侷限,而且通常不需要或只需要相當少的人工介入(這門課裡面講的就是 shallow techniques)。
Deep techniques 需要人工介入,而且有更多局限性,但能獲取到更多的知識。
結合前兩者加上機器學習才能得到 actionable information(知道這些知識後,我們可以採取某些行動或決策)
目前還沒有什麼兩全其美的方法,
在開始文本挖掘之前,建立好這個 trade-off 的概念是相當重要的。
|
|
就是一連串的字元組合起來的字串,但對分析並沒有意義,
因為它沒有提供一個電腦能夠解析的結構。
這是這一門課主要探討的範圍,其他可以稍微看看就好 XD
這一層是所有 text mining 的基石,後續的分析和 mining 都能與此有所關連
為了得到更結構化的資料,我們可以試看看 POS(Part Of Speech) tags,
來為每個詞貼上一個 tag 看看,這個 tag 就是這個詞的詞性:
|
|
POS tag 的優劣分析
Pros:
word 是人們溝通的基本單位
有了這個結構之後我們能統計每個字出現的次數
能夠被應用到 topic analysis 上
Cons:
不夠 general,像是中文就不是這樣認字。(中文字和中文字中間不會用空格分開)
解法:中文會需要自己的斷詞(e.q: 結巴)
拆出這個句子裡有哪些「實體」與「關係」
e.q:我吃香蕉
這裡的實體就是我
、香蕉
,
而我們的關係是 吃
與被吃
。
|
|
瞭解這句話的「意圖」(intent)是什麼,光用想的就非常困難
e.q:
越往下是越深層的分析和挖掘
需要更多人工判斷
精確度其實是降低的
但是也越接近人類知識的表達
繼續研究下去之前我們得了解會有這樣的 trade-off
所以要想辦法在人的參與和機器學習間做優化
想想要怎樣讓人工的部分更簡單、省事
利用上述的結果來得到更精確的學習結果、讓人工參與部分減少
這堂課主要就是以 shallow techniques 為主,
Shallow techniques 有以下幾個特點:
General and robust
No/little manual effort
“Surprisingly” powerful
Can be combined with more sophisticated representations
有了基本的文字探勘概念以後,
要先了解的就是詞和詞之間的關係,
並且把它們用數學的方式來表達。
什麼是 word association
為什麼要挖掘 word association
如何挖掘 word association
簡單說就是詞跟詞之間的關係,
有以下兩種:
Paradigmatic Relation(聚合關係)
A
,B
兩個詞可以互相替換,那兩者就有 paradigmatic relation
從 similar context 去找: high context similarity => high paradigmatic relation
e.q: “cat” and “dog”
Syntagmatic Relation(組合關係)
A
,B
兩個詞可以互相結合,那兩者就有 Syntagmatic relation
從 correlated occurence 去找: high co-occurences but relatively low individual occurences => high syntagmatic relation
e.q: “cat” nad “sit”, “car” and “drive”
才能了解各個 document 間的關係,
容易去做 topic analysis,
要做更深層的分析前都得先做這一步。
前面我們稍微理解了 Paradigmatic 以及 Syntagmatic 兩者的定義,
現在要來更深入的了解 Paradigmatic Relation。
首先要了解的概念是 Pseudo document,
讓我們用不同的方式來理解 context(上下文)。
Pseudo document,其實就是各式各樣的 bag of words,
裡面裝著各種「字」。
|
|
Pseudo document 的表示方式就是像下面敘述的這樣:
|
|
第一項 Left1("cat")
就是 cat 左邊的一個字能放什麼,
Right1
則依此類推,
甚至我們也可以用 Window10("cat")
來表示 10 個字裡面出現 cat 的 pseudo document,
沒錯,window 10
所以 pseudo document 裡的字,
跟我們的目標字(這裡是 cat)相鄰或不相鄰都是可以的。
有了這些裝在袋子裡的字,我們就有了更堅實的基礎去比較 context 間的相似度。
以下用 Sim 這個 function 來表示 “Cat” 跟 “Dog” 的 context 相似度:
|
|
context 有著越高的相似度,代表 “cat” 跟 “dog” 兩個字越有聚合關係。
這裡並不是說一定要全部都算進去,我們也可以拿其中幾個袋子就好
而要選擇用哪些袋子來分析相似度,更是大大影響了我們分析的結果,
假如我們使用的是 Window10("cat")
,這個相對較寬鬆的條件,
我們得到的是更 general 的訊息,可能能有更廣泛的應用;
假如我們使用的是 Left1("cat")
這樣的 context 時,
只會知道 “cat” 左邊會出現什麼詞
得到的可能會是更趨近語法分析上的訊息,相較更侷限一些。
數學的美妙在於能把複雜的東西簡單化,
這裡我們很驚訝地發現能夠用 vector space 的方式來表現 context similarity
但問題來了,我們要怎樣去計算 d1
跟 d2
這兩個向量呢?
其中一個相當直觀的方法是 Expected Overlap of Words in Context (EOWC)
我實在不會把這個翻譯成中文 XD,以下簡稱它為 EOWC,
名字中有著 expected,假如你還記得高三的統計學,
就可以猜的到它可能隱含著期望值的概念在裡面
|
|
$$
d1=(x_1, …x_N), \quad x_i = \frac{c(w_i ,d1)}{|d1|}
$$
$$
d2=(y_1, …y_N), \quad y_i = \frac{c(w_i ,d2)}{|d2|}
$$
$$
sim(d1,d2) = d1.d2 = \sum_{i=1}^{N}(x_i \times y_i)
$$
x_i 會是每個字(w_i)在各自的 pseudo document 裡面出的次數(count),
再去除以 document 裡面的總次數。
我們得到的值就會是 w_i 這個字,出現在這個 pseudo document 中的頻率。
這是個 normalize 過的數值,所以將 x_1 加到 x_N 會是 1
兩個向量的 dot product 就會是 similarity。
dot product 的算法是這樣子, a = (1,2,1), b = (3, 3, 3)
a.b = 13+23+1*3 = 12
聽起來是蠻直觀的,
這代表兩個 document 中出現的詞相同頻率越高,
兩個 context 就越相近。
儘管 EOWC 給了我們對相似度相當直觀的感受,
還是要小心這裡面有兩個問題:
假如有其中一個詞的頻率在各個 document 出現次數超級高,那就會判定這兩個 context 有很高的相似度,就算其他詞重複頻率都很低也是一樣,這可能不會是我們想要的結果。
對所有字都一視同仁。
鑑於以上原因,我們再來看看有沒有什麼更好的方法。
兩個問題的解法:
It favors matching one frequent term very well over matching more distinct terms.
It treats every word equally (overlap on “the” isn’t as
so meaningful as overlap on “eats”).
先來看怎麼解決單一個詞擁有超高重複次數的問題,
TF transformation 其實就是把原本對應到的次數,轉換成另一個更合理的次數而已。
舉例來說:
我們可以把超過 1 的都 mapping 到 1 去, 0 的就是 0。
光用想的都知道上面這一個會丟失掉許多資料給我們的訊息,
所以接下來我們找到一個更合理的方式來做 TF transformation ,
叫做: BM25 Transformation。
小小科普一下,雖然 tf-idf 應該是大一就會的東西 XD
BM25 通常指的是 Okapi BM25,是一個搜尋引擎中的 ranking function,
BM 指的是 best match,它是由 BM11 以及 BM15 結合起來的,
不過起始點由 0 移至 1,所以從 26 變成了 25,
這就是 BM25 的由來。
還想更瞭解 BM11 和 BM15 的話可以看下方的補充資料:
假設輸入為 x,輸出為 y:
$$
y = BM25(x)
$$
更進一步看 BM25 這個算法的話:
$$
y = \frac{(k+1)x}{x+k}
$$
k 是我們可以自訂的數字,
可以簡單的看出來這個 y 最多無法超過 k+1,
如此便有效的將太多次數的詞都降到 k+1 了,
那問題就來了,k 要訂多少才合理呢?
暫且擱置這個問題,先來看原本 EOWC 的第二個問題:
It treats every word equally (overlap on “the” isn’t as
so meaningful as overlap on “eats”).
太常在所有文章中都出現的字,它的重要性應該要降低,
而鮮少出現的字應該要被提高,我們用的方法則是 IDF。
|
|
舉個小例子:
假設全部有 A, B, C 三個 documents,
而 W 出現在 A, B 兩個 documents,
情況會是 :
|
|
再來看 IDF 的公式:
$$
IDF(W) = log(\frac{M+1}{k})
$$
IDf = Inverse Document Frequency
原本是 字在所有 document 中出現的次數/ document 的總數
IDF 是將其反過來看,所以稱為 inverse document frequency
可以看到,當 k 越大時, IDF 就會越小,
當 k 到達最大值(M),代表每個 document 都有出現 W ,
IDF(W)會趨近於零,讓 “the” “的” 這種每個 document 都可能會出現的字權重降低,
對於第二個問題而言,是一個相當簡單卻有效的方法。
最後,我們要將這兩個方法加進我們的 EOWC 中來優化它。
一口氣看這些公式會有點嚇人,
我偏好一步一步來理解它。
首先要來定義幾個符號:
$$d1=(x_1, …x_N)$$
d1 就是 pseudo document,
裡面的 x 是一個個的 word
$$b \in [0,1]$$
$$k \in [0,+\infty)$$
$$|d1| = length\,of\,d1$$
有了上述的定義後,先不要去深究他們有什麼意義,
因為要放在下面這個公式中,他們的定義才有用:
$$
BM25(w_i, d1) = \frac{(k+1)c(w_i, d1)}{c(w_i,d1)+k(1-b+b\times\frac{|d1|}{average(|d|)})}
$$
看似好像多了很多奇怪的符號,
但只要把上面那個 BM25 拿來對照一下:
$$
y = BM25() = \frac{(k+1)x}{x+k}
$$
就會發現其實它只是把 c(w_i, d1)
帶入 x
中,
k
仍然是拿來控制整個 tf 轉換過後的上界;
你唯一需要注意的新東西是 b
,
$$
k(1-b+b\times\frac{|d1|}{average(|d|)})
$$
average(|d|)
是為了 normalize 過長的 document,
我們希望不要因為不同長度的 document ,
得到差異過大的結果,
我的經驗是理解到他在做 normalize 之後,
就不要在糾結在上面太多,
就像很多時候我們取 log 就是真的讓圖比較平滑或好看而已(或者是在輸入是 1 的時候能得到 0 值)。
再來就看我們怎麼對 doument 裡面的每個 word 做 normalize 以及轉化:
$$
SUM =\sum^{N}_{j=1}(BM25(w_j, d1))
$$
$$
x_i = \frac{BM25(w_i, d1)}{SUM}
$$
$$
\sum^{N}_{i=1}x_i = 1
$$
分母是每個字的得分,分子則是該字的得分,
這樣做的好處是相加起來會等於 1,
裡面隱含著就是機率模型的概念。
d2 的話就把 x 代換成 y,這裡不再重複寫一次
最後就可以來算 similarity 了:
$$
sim(d1,d2) = \sum_{i=1}^{N}(IDF(w_i) \times x_i \times y_i)
$$
新的算法中在一定程度上矯正了前面敘述到的兩個問題:
tf => 將在單一個 doucment 中重複次數過高的字給 normalize
idf => 將在每個 document 中都出現的字的權重給降低
因為算是中的三個元素都介於 0 到 1,
所以結果也會落在 0 跟 1 之間。
這一小節會更加的抽象一點,最後來看個有趣的小知識,
BM25 其實也能應用在 syntagmatic relation 上面,
前面有說到組合關係能夠從 correlated occurence 去找:
$$
IDF-weighted \quad d_1=(x_1 \times IDF(w_1), …, x_N \times IDF(w_N))
$$
原本的 x_i 只能表現他在同一個 docoument 裡面出現的頻率有多高,
(這裡是指經過 normalized BM25 transformation 的值)
但是我們不能說這個 x_i 與其對應的 w_i 是相關的(correlated),
因為有許多 common words(e.q: “the”, “的”) 也被包含在裡面,
但是我們前面有學過 idf 這個轉換法,
假如我們對每個 x_i
去乘上對應的 idf(w_i)
,
就能得到去掉常用出現的字之後,真正與 w_i 相關的值了。
我第一時間看到這裡有點疑惑,
Syntagmatic relation 不是從 word 同時出現的機率去挖掘的嗎?
假設我們要看 a 出現的話,b 同時出現的機率是多少,
其實我們得到每個 word 在每個 document 中出現的機率後,
就能算出在 a 字出現的條件下,b 也同時有出現的機率了,
就只是要算個條件機率而已 XD,相當的直觀!
這裡也只是我自己的推論,有錯的話也請不吝告知了
總計六個禮拜的課程,目前是第一個禮拜。
其實寫筆記想要寫到其他人看得懂比上課來的吃力一些,
但同時帶給我的好處就是更深入了解自己所研究的內容,
雖然 tf-idf 是這樣簡單的東西,
但實際上了解它背後的概念,會發現它能運用的場合非常的廣泛。
Text mining 並不是一門太空人般的學問,
畢竟我們生活離不開文字的溝通、訊息的傳遞,
當你有能力去知識的載體上挖更多知識時,
何樂而不為呢?
第一步通常會是制定 style guide,
但 style guide 越定越複雜後,要靠人工去檢查就顯得有點不切實際。
這時候就需要靠程式自動來做語法上的檢查及 highlight,
更殘酷一點可以讓不符合 coding style 的 code 無法被 commit
這就是 linter 的功用。
而這篇會以我最近在實務上以 eslint + webpack + githook 來做舉例。
我知道網路上有許多充滿獨到經驗的 javascript linter,最有名應該就是 airbnb 的),
直接拿來用當然也 ok,不過,我們固然要學工具,
工具背後的想法才是我們更該了解的。
這一篇並不是什麼懶人教學,複製貼上就能用的 eslint extends,
而是一步一步地去理解 eslint 到底能做到什麼事情,
也許你看完以後還是會選擇直接使用 airbnb 或是其他人寫好的 linter,
但這時候的你,
已經完全有能力參考前人經驗並制定出一套符合你們團隊需求的 linter,
甚至去看他們的設定時,能夠對他們為什麼這樣做更有想法。
希望你看完之後,學會的並不是 eslint 這個工具而已,
而是未來你要做類似東西時內心已經有一個架構在。
雖然站在巨人肩膀上能夠看的更遠,
但能夠自己造出一個鋼彈再站上去,那他媽完全是不一樣帥氣的事情。
我得承認在寫這篇文章前,
我並沒有使用 linter 的習慣,
因為說真的,在專案長到一定大小前,
linter 更像是 nice to have 而不是 must have 的東西,
儘管我們知道越早用它越好⋯⋯
大家可能在國文課本都看過方孝儒的指喻,
其實髒髒的 code 這件事就像指喻一樣「始以為不足治,而終至於不可為。」。
之前不去使用 eslint
的藉口都是沒空仔細研究,
的確,現實生活中的時程可能不允許你直接花大把時間在 linter 上,
所以這篇文章是從幾個禮拜的零碎時間中擠出來的。
首先要理解的是:linter 做的事情其實是「靜態的語法分析」。
這意味著我們不需要去執行 script,就能標記出不符合 coding styles 的地方。
另外,
eslint
的所有規則(rule)都是 pluggable
的,
沒有什麼東西是「太重要」而不能把它關掉,
包括你去下載別人的 eslint 設定,
你也可以把不適合你團隊的 rule 給關掉。
最後,
eslint
的 rules 是 “agenda free”,
官方並沒有提倡哪種 coding style 是好的,
你想怎麼樣組合你的 rules 就怎麼樣做。
官方文件中有介紹許多種 config 的方式,
可以從 command line、package.json 裡面設定,
但其實最常看到的還是從 .eslintrc
去設定,
所以這篇也會以 .eslintrc
,並且以 json 格式為主。
在寫 config 之前,你要先了解你可以對什麼東西設定 config,
其實只有 3 + 1 個東西而已:
Environments
Globals
前面有提到過,linter 做的是「靜態」的語法分析,所以它對你程式的運行環境是一無所知的,你必須自己把一些全域變數給加上去。
e.q: 開發 chrome extension 時,你要 call chrome 的 API 必須從 chrome
這個 global variable。
Rules
有點像是違反這條規則的嚴重程度,
有些比較輕微的你可以設定噴 warning 嚇嚇他就好,
但有一些你覺得寫出這些 code 來真是天理難容,你可以直接拿 error 噴死他。
Parser
這裡我把它放在多出來的 1,因為我們通常指定完 parser 之後就不會再其上面更改太多設定,甚至根本不需要指定 XD
的確,你有可能這輩子都不會寫 parser,但我相信探究技術的本質是一個技術人該有的初心,軟體工程師對於知識不該有太浮躁的心 :)
總之知其然而知其所以然是相當重要的,不管你有沒有修過編譯器(compiler),接下來會馬上科普地介紹 parser 是什麼,以及我們知道這些之後可以做什麼
這一小節會解釋如果你要啟用 jsx, es6 或 es7 語法你該做些什麼。
但要先解釋一下 Parser 是什麼?
如果你已經知道 Parser 在做什麼,
可以直接跳過分隔線中間的這一小段科普文
我們都知道電腦看不懂我們寫的 code:
|
|
Parser 會把我們的程式碼 parse 成 AST(Abstract Syntax Tree),
讓我們的程式碼能夠簡單的去操作這個 tree,
最後才會編譯成 binary 的形式。
延續上面的例子,
這段程式碼經過 Espree 這個 parser,「最終」可能會變成:
|
|
為了簡單一點,我把它在檔案的位置標記給移掉了,
但整體而言你可以感受一下,比起直接操作純文字,
轉成 AST 後能用更結構化的方式來取用程式碼。
科普就到此為止了,
想對 Parser 有更深入了解,可以參考一下我之前寫的筆記:
下面參考文章部分也有放一些我當初學習時讀的文章。
一言以蔽之,
Parser 就是將我們對語法的理解給「程式化」成一個樹狀的結構。
前面有說過, Parser 會將純文本的 code 轉成 AST,
eslint
中的 parser 只有預設支援 es5 語法,
所以其他額外的語法:es6, es7, jsx,都必須要另外設定。
Note:
支援 jsx 的 parser 不代表支援 React 的語法。
如果你想要直接使用 React 語法的話可以安裝
eslint-plugin-react
接著就直接來看要怎樣設置 Parser 的 options,
在 .eslintrc.json
中的 parserOptions
去設置:
|
|
ecmaVersion
: 顧名思義就是 ECMA script 的 version,有 3, 5, 6, 7 可以供你挑選
ecmaFeatures
啟用 jsx、strict mode 等 feature,預設都是關閉,可以用 boolean 的方式來開啟。
詳情可以到官網文件看
sourceType
:
預設是 script,但如果你的 code 是被包在 ECMA script 的 module 中,就要設成 module
白話文:你用 webpack 的話就把它設成 module
最後,雖然 eslint 預設的 parser 是 Espree
,但你還是可以換成其他的 parser:
parserOptions 是共通的,在以上的 parser 裡面不用擔心要寫不一樣的 parserOptions
可以在不同 environment 去 predefine 所需的 global variables。
注意:
這裡並不是去 assign global variables,
而是把不同 environments 的選項給打開,
接著你就會取到在這個 env 底下 predefine 的 global variables 了
一樣用 .eslintrc
舉例:
|
|
可以到官方文件看有哪些 environments 可以用:
假如你想要自訂或是用別人 plugin 中的 env 也很簡單:
|
|
定義你需要的 global variables,
這裡並不是像平常寫 code 時在 assign 值給 variable 一樣,
因為我們並不會去執行程式碼,只會進行靜態的分析,
所以我們做的事情只是確認這個 global variable 是有被 define 的,
舉例來說:
|
|
上述的 globalB
就是未被 define 的 global variable。
假如我們定義了不能接受未 define variable 的規則(rule),
linter 就會把這個視為語法檢查不通過。
所以我們必須要讓 eslint 知道這個全域變數是有被 define 過的:
|
|
如此一來 parser 在看到 globalB
時就會知道:
「啊!這不就是寫在 globals 裡面的 globalB 嗎?沒事兒沒事兒」
eslint 能夠很靈活的安裝第三方的 plugin。
這裡就是我們平常在使用 airbnb 的 config 時會用到的地方 XD,
通常名字會長這樣:eslint-plugin-*
,你可以省略掉前面這一段 prefix,
比如說eslint-plugin-demo
,在 .eslintrc
可以這樣引入:
|
|
待會提到 rule 時,再來解釋要怎樣引用 plugin 裡的東西。
rule 就是我們在 style guide 中定義的規則,
可以針對嚴重程度設定 error level:
分別是 off
, warn
, error
,
它們分別對應到 0, 1, 2 三個數字,
也就是說
{"curly": "error"}
和{"curly": 2}
是一樣的意思。
看例子可能會清楚一點:
|
|
這裡的 rule 都是 eslint 事先定義好的規則,
想看有哪些的話一樣可以到官方文件去看,
不過我想應該是不會有人一條條看完就是:eslint: rules
同樣的,我們可以藉由 <pluginName>/rule
來獲取 plugin 底下定義的 rule。
補充一下,你也可以在一些特別的時候 enable 或 disable rule。
舉例來說有一條規則是 code 裡面不要有任何 console.log
,
但有些地方一定要存在 console.log
該怎麼辦呢?
你可以讓 eslint ignore 掉這整個 file,不過這不是個好解法。
更好的做法應該是在那幾行 code 前面加上 “inline disable” 的 comment:
|
|
/* eslint-disable no-console */
底下的 code 都會關閉 no-console
這個規則,
但/* eslint-enable no-console */
會把 no-console
這個規則再次打開。
兩個搭配起來的結果就是在這兩段 comment 中間的 code 不會啟用 no-console
這個規則。
總結上述幾點,其實 extends 這個 array 裡面放的其實是個完整的 config,
你也可以直接 extend 別人 export 出來的 config。
這也是為什麼當你安裝別人的 extends 時,
會以 eslint-config-*
來當作 package 名稱,
而不是用 extension 的原因。
同樣的,在寫 config 時,可以忽略
eslint-config
這個 prefix
|
|
跟 git 一樣,可以忽略掉某些檔案:.eslintignore
前面的 rules 看起來都是別人幫你建好的,你也可以參照 working with rules 來定義你自己的 rule。
對我而言現有的 rules 幾乎已經把我能想到的模組都開發完,
理解 linter 對我來說最重要的是知道「哪些東西是我要的」,
最後再將其組裝起來,而不是從頭造一遍輪子。
同理,你也可以自己開發 plugin。
你可以 extend 其他人的 config,但你也可以在最外層去把 rule 給覆寫掉。
如果寫完 eslint 設定,卻還要每次寫完 code 都自己跑一次:
|
|
這是相當反人性的事情,所以在有了前述的背景知識後,
我們來看看日常開發中是如何使用的,
以下就兩個比較常見的方法來介紹,沒有誰好誰壞,
全看怎樣比較適合你的團隊。
我盡量不為讀的人預設什麼預備知識,
但懂一些 git 以及 webpack 的話,
設定起來會相當的 trivial:
Webpack
git hook
其實編輯器也有許多可以搭配 linter 的東西,
但鑑於所有開發者使用的編輯器種類太多,
而且編輯器的設置相對簡單,
所以這裡不會再贅述編輯器上面的設定。
首先要先安裝 eslint-loader
|
|
接著到 webpack.config.js
,只要看 loaders 這個屬性就好了:
|
|
loaders array 中的順序是相當重要的,因為它的順序是從最後面開始往前執行。
也就是說在讀取到 js 或是 jsx 檔案時,
會先經過 eslint 檢查,再進去 babel 轉譯。
反之的話,一定會噴錯噴得滿天飛 XD
而且這樣 eslint 去檢查的就是轉譯過後的程式碼了
但更好的方式是把 eslint-loader 放在 preloaders
中:
馬的我也是第一次知道有這東西,webpack 的 config 就是這麼令人驚喜
|
|
如此在開發途中,只要語法有違反規則,
就是視同為 error。
現在你已經有一個自動化的 linter 了。
補充一下,有些人可能會用 webpack 的 provide plugin,
去省掉一些 ../../../../actions/doSomething.js
的程式碼,
這時候你可能會需要這個 plugin: eslint-import-resolver-webpack,
然後你要在 .eslintrc
的 settings 裡面加上:
|
|
這樣子當你使用 no-unresolved
這條規則時,
eslint 會先去檢查你在 provide 裡的 alias,
再來看有沒有辦法 resolve 這個 path。
如果你很熟悉 git 的話甚至可以不用安裝這套件 XD
git hook 能做的事情就是讓開發者在執行某些 git 操作前,
自動得去執行某些 scripts,
不過首先,我們要先將 lint 的 script 給寫出來才行
接著在 package.json
裡面:
|
|
再來只要執行:
|
|
就會自動的去做 lint,簡單的 script 完成了,
接下來就是跟 git hook 結合的時候,
會需要用到 husky
這個套件:
|
|
會用它的原因是因為它用起來簡單粗暴,
不管你要用哪個 hook,只要把名字放在 npm script 中:
|
|
你的 git hook 就設定完成了
precommit 就是在 commit 之前我們會去執行
npm run lint
這個 script,這裡有個對應的 hooks 表格
你可以挑一個在適當的時機執行語法的 lint
不過如果每次 Commit 都要跑一次 lint,讓你很煩的話,
實務上的做法也可以只整合在 IDE 以及 CI server 上就足夠。
假如你還是要保留原本的流程,
但在某些整理 commit 時並不需要重新執行 lint的話,
也可以在 commit 時加上 --no-verify
或-n
。
還有一個做法是使用 lint-staged
這個套件,
簡單的說,它會讓 linter 只檢查新放上 stage 的 code,
在 git 中執行 add 之後,會把 file 放到 stage 上,
這就是為什麼他要命名為
lint-staged
。
這樣每次的 Commit 就不用重新檢查一次全部的程式碼了。
快速的想過一遍之後,
lint-staged
可能會有個小 gotcha,可能在
no-unresolved
這條 rule 上面犯錯。舉例來說 a 檔案會 require
./b
,然後我把 b 刪掉了,這次 a 檔案並不會上 stage,
所以這次檢查並不會把這個錯誤給檢查出來,
但整體而言其實還是為我們省了不少時間,
就看個人怎麼選擇啦!
在學習 eslint 的過程中,
對於這種自動化、提升團隊程式碼品質的東西又有了一些心得,
像是 eslint-loader 以及 git hook 的使用時機都是。
同時也理解到這種 pluggable 的特性在開發者的世界裡能帶來極大的成功,
看看 webpack、babel,以及 eslint 都是因為其容易製作 plugin 的特性,
讓開發者自主的開發出優質的插件來讓整個生態系更蓬勃。
也許工具會一代代推陳出新,
但是這種我為人人、人人為我的系統思維是不會變的。
最後,
在前面有說過 coding style 之於指喻,
最近實務上有個算小型的 project 從一開始開發時並沒有去使用 eslint,
到了今天把簡單的 style guide 訂出來之後,(大概就是四條 rules 而已,沒有直接引用 airbnb 那一套)
結果是這個樣子:
嗯⋯⋯
晚了一點以後更新成 airbnb 的 config 然後自己修改一下 rule,
變成:
….
這一定是假的。
]]>感謝
@ctwu
、李俊緯對開發流程中整合 eslint 的建議感謝 Amobiz Chen 提供
lint-staged
這個工具感謝陳威霖提醒我要加上 inline-disable 的用法 XD
這篇可能不會講到太多直接跟技術相關的東西,
如果你對這個東西怎麼寫出來比較有興趣的話,
可以看這篇:Clairvoyance 是怎麼開發的
主要是希望這次從開發到現在較多人使用,
中間受到許多幫助和指點的經驗,能夠被記錄下來,
假如以後有人遇到一樣的事情能從中借鏡。
當然,這也是對自己的一個反省。
首先還是得先講一下求職天~眼通是什麼。
它其實就是個 chrome 的 extension,
裝了它以後,能在人力銀行的職缺下方給評論,以及看到其他人給的評論,
載點在這裡:
其他說明的話 ptt 上的文章會清楚得多。
Jean Grey and Cyclops from Entertainment Weekly
求職天~眼通要做的事情很單純,
就是像前面說的把留言功能加上去而已,
不過其實就像我們平常在做一件事情一樣,用想的都很簡單。
但總歸其實只會遇到三個問題:
真正做的時候會遇到問題
做出來之後有沒有人用會是一個問題
有太多人用之後又會是一個問題
下面來說一下從端午連假到今天為止的這一段故事,
寫程式是這個故事很重要的一部份,
不過其實還有很多其他眉眉角角可以跟大家分享。
在開發前,首要的認知就是知道:
自己擅長什麼、手上有什麼資源,如果前兩者還不夠還要再準備什麼。
其實有這個發想是在端午節之前,
我平常是一個網頁前端工程師。
雖然也寫寫後端以及對系統感興趣,
但我知道 full stack 是一個被濫用的職稱。
這年頭多的是 Database 操作只會 CRUD 的前端工程師稱自己為 full stack,
或是只會套 bootstrap 的後端工程師稱自己為 full stack。
最常用的語言剛好就是 JavaScript ,可以直接拿來寫 Chrome 的插件,
但這還不夠,我還需要一個存放資料的 back-end。
下面這個基本上就是我畫在紙上的草圖:
本來打算直接在 AWS 上開一個 EC2+RDS 放著,
後來發現只是單純留言,也沒有要真正 render 一個網頁。
這個 back-end 需要能達到兩件事情:
計算的能力
資料的持久性
首先是單純的運算能力,最終看上了 AWS 的另一個服務 AWS lambda,
它是以 function 為單位,不會需要我去維護整台機器(serverless),
而且當運算量變大時,我大 amazon 會自己幫我 scale-out。
於是稍微研究了一下 serverless 這套 framework,
也寫了一份筆記在這裡:
再來則是資料的持久性,我選擇了 DynamoDB,
是個跟 lambda 搭配很常見的選擇。
儘管它看起來就是簡單易用,
但為了這個選擇其實下了不少功夫,
一開始是因為對「最終一致性」有疑慮,所以去看了 CAP 理論:
後來再看了這本簡介分散式運算的書:
總歸對系統的架構有個理解後,才開始安心使用,
儘管現在回頭看這兩份文本都可以跳過,
但要做能 scale-out 的系統,
對分散式運算如果一無所知的話,會沒有那個 sense,
身為一個軟體人就不該對未知的東西姑息或害怕去學它。
接著一切就緒後,我突然發現我少了一位設計師夥伴,
基於不想在假日麻煩人,我上了 codepen 去看他們的 license:
codepen 是一個讓前端工程師放作品的地方,上面有蠻多好玩的設計以及如何實做的原始碼。
有人會問跟 pinterest 有什麼差別?
簡言之 pinterest 是比較偏向純設計師的。
簡單說一句話,就是 public pen 都是 MIT license 的,
要使用的話,只要包含了他們原本的 license,就可以自由使用。
雖然最後幾乎都只是參考概念,並沒有真正援用哪個 pen 上的東西,
但身為一個軟體開發者,就應該遵守這些基本的規定,
畢竟當真的有人要找你麻煩時,沒有不知者不罪這種事情。
如果對於這些法規方面以及技術有興趣的人,
可以去 follow Muzik Online 首席工程師 Ant 的臉書,
上面有許多能讓技術人有一些法規 sense 的文章,
而且對於 Database 和系統架構,上面也很多東西可以看。
好了,現在我有:
前端開發能力
back-end
codepen 上參考的 UI
再來只是需要時間就能把東西做出來了。
大家看到的版本,其實是有滿滿 bug 的 beta 版本,
我只有讓身邊幾個朋友測試過,就先 po 在 soft_job 板上。
讓開發者以外的人先測過這一步至關重要,
因為自己開發的東西一定會有盲點。
會這樣做的原因是因為我想知道這東西是不是真的有需求,
當時 po 完文章就去睡覺,早上看到有三十推就覺得蠻開心,
結果幾個小時候 TonyQ 將它轉到八卦版去,直接一個爆衝。
但結局讓我又開心又害怕:
開心的是這東西真的有需求,而且很有需求。
害怕的是我覺得這東西還不夠成熟,怕一開始太難用就直接被拋棄。
令我意外的是, aws lambda 和 DynamoDB 完全撐住了流量,
當初把這些東西交到雲端託管有了成效,
後來去看 log 發現沒辦法留言,都是原本 code 裡面有 bug,
跟 AWS 一點關係都沒有 XD
不過後續就一直修到昨天晚上為止,留言功能才算正式穩定下來。
接著除了大量的 bug 回報之外,
也收到許多跟功能上有關的回饋。
其實大部分的回饋我都認為做了功能絕對會更完善,
但時間並不允許這樣做,
所以問題並不在於「現在要做什麼」,而是「現在不做什麼」。
這裡感謝 TonyQ 以及榮尼王給我的許多建議。
因為背負著很多人的期待,我並不能想做什麼就亂做什麼,
必須訂下一個明確開發的方向,
就現階段而言,讓這個 extension 活下去是至關緊要的事情,
因為已經有了第一批用戶(已註冊目前大概約四千多人),
剩下的只是繼續累積。
太激進、會讓人力銀行對這個 extension 採取行動的事情,
都不該去做,因為目前還玩不起這個槓桿。
不過 github 上面有許多人提了一些有辦法解決的方法,
總之,沒有「絕對不做」的事情,只有「現階段不做」而已,
有興趣的人也可以去看看,集思廣益:
有蠻多人在提到這件事情要商業化,
也有人覺得只要「不商業化」就先把你貼上「傻傻、不懂事」的標籤。
但其實我一點都不排斥商業化,我只是單純的覺得這件事不適合,
或者現在沒想到適合的方式。
像是如果要在上面硬是建立一個什麼商業模式,(像是廣告什麼的)
這東西最後看起來只會是一個擾民的垃圾。
而且當以獲利角度來做這些事情,
我就不能單純站在勞工的角度去思考了。
至於要永續經營,後續等真正穩定下來後,
會放上小額捐款的連結,
這件事會在擬定如何公布經費的使用以及規劃後才做,
不在現在就先急著募錢的原因很簡單,
因為我想讓捐錢的人真正弄清楚他們的錢為何所用,
畢竟群眾募資不是大乞討,懂?
這裡不談功能,最終的希望就是所有的勞方都會是天眼通的用戶。
因為大家並不是一年四季都在找工作,
但大家卻是一年四季都能上去做評論,
有時候並不是說一定要面試過或怎樣才能做評論,
短期內,可以揭露一些根本沒必要去的職缺,
長期下來,經驗的分享才是這個 extension 最難發揮價值的地方,
「老馬識途」這種事情,在職場上也是適用的,
總之,如何吸引大家去做這件事,就會是接下來的主要課題。
終於寫到這裡了,前幾天看到了 Codetengu 上分享了這篇文章:
我也想到前陣子 Alpha Go 很夯時,阮一峰所寫的文章:
身為一個軟體開發者,能了解電腦能做到的就是大量自動化、去中介化,
去取代掉那些機器可以取代的員工,
企業為了達成這件事情,自然要雇用一堆軟體工程師來幫忙,
所以軟體工作者也變成一個搶手的職業。
所謂技術帶來的平等,是指「資訊上」的平等,
我們的資訊流通因為網路和軟體越來越快,
舉例像是:歐巴馬總統和我們一樣都能用 google 快速查東西。
這年頭不知道還有沒有人記得百科全書這東西
但在財富上卻不盡然,我們拿到越來越多薪水時,也讓越來越多的人失業,
當此同時,除了繳了多一點點的稅,
我們大多數人並沒有負起什麼社會責任。
儘管軟體開發者理應是最有辦法讓想法付諸實現的人才對,
畢竟軟體能夠運行在電腦這個已經稱霸全球的載體上,
更別說我們還有了 Internet 這樣鋪天蓋地的通路,
寫程式這件事雖然有時候我也會因為智商不夠用覺得好難,
但是比起動不動要砸大錢的製造業,寫程式真的容易實現多了。
寫程式是世界的潮流沒錯,
只是許多台灣創業家提到寫程式就很喜歡強調矽谷如何、如何,
忽略了許多在本質上就有顯著差異的事情。
統計學裡面告訴我們:有顯著差異是要拒絕虛無假設的,這句話現在看來蠻有哲理。
我不會說面對國際市場是一件錯誤的事情,
在商言商總是有許多額外的考量,
畢竟連話說的不好聽,要怎麼讓人掏錢投資勒?
只是很多問題,其實台灣有其因應的解決方式,
而工程師本來就該是提出 solution 的人,而不是負責說空話的人,
所以更應該要虛心學習用一個台灣人的角度來看向世界以及台灣,
才能真正解決台灣的問題。
舉例來說:這個插件就是解決台灣特有的問題 XD,
因為國外的求職平台沒有像台灣這樣被壟斷。
中國那邊的招募平台也幾乎都有開放留言討論這個功能,
資方跟勞方是積極在爭論對話的。
所以這插件只有在這樣子的台灣才會有需求XD
題外話是其實台灣也有蠻多新的求職平台,
像是 sudo,
或是 yourator,
都相當不錯,而且在資訊上也相對傳統的人力銀行透明很多,
不過都是比較以新創或工程師為主。
特別講到 sudo 是因為他們的留言功能更完整,
(正因為以前就在那裡工作才更了解這些事情)
裡面的就職顧問雖然是講話很愛中英交雜的 AIESECer,
但絕對是真心要幫助工程師求職的:D
再來雖然現代人生活離不開電腦,
但其實對於軟體相關的事務都是有疏離和懼怕感的,
很多時候是因為身為人與機器的 Proxy 的我們沒有做好事情讓其他人有感。
身為一個在軟體產業工作的人,
這件事可能會蠻常見的,就是你有時候很難跟不寫程式的人敘述你到底完成了一些什麼 XD
舉例:
把什麼東西做了 cache 讓它更快
或是用了什麼 Design Pattern 提高了維護性
別人聽一聽常常是:「喔⋯⋯這樣啊⋯⋯」。
但生活中到處都是我們能夠付出專業能力去改變的地方,
工作之餘,還要有生活,生活之餘,
我們還能改善其他人的生活啊 :D
當認為有正確的事情該做,
就該運用系統化的角度去設計和解決,
因為假如做出來不小心規模化,你的系統又扛得住的話,
那可能就不小心改變世界了。
題外話是天眼通本來是個我跟別人講,
別人只會說:「喔~聽起來還不錯啊」的 Project。XDD
再說我們身在這個年代,
有各種雲端服務幫你搞定基礎建設(IaaS、SaaS、PaaS),
還有各種框架幫你搞定 UI。
基本上你只要有想法、計畫,再加上一段時間穩紮穩打的學習、練習,
幾乎就能解開各種 Issue 了,
不覺得很讚嗎?
讚讚讚!
也期許自己未來是真正的 RD 工程師,
而不是出現 bug 只會 XD 的 XD 工程師:
能夠讓求職者在人力銀行的職缺下面留言討論。
聽起來是很平常的需求,不過各大人力銀行就是不做這個功能,
所以我想看看假如有這個功能會不會對求職有正向的幫助。
下載的連結在這裡: Clairvoyance - 求職天眼通
目前還在 beta 階段,可能會有些 bug,
可以到粉絲頁留言,
或是在 github 上直接發 issue。
下面就來筆記一下為什麼要做這件事,以及怎麼做到的。
用的技術就是以下列的這些
front-end: reactjs、redux、redux-saga
back-end: aws-lambda, dynamodb, serverless-framework
目前只支援 104 和 1111,至於 yes123,後面會再提到為什麼暫時沒做。
下面來簡介一下是怎麼做出來、以及為什麼要做。
為什麼要做這件事情?
其實我比較想問的問題是:為什麼不要做這件事情呢?
我們買商品的時候,在拍賣網站上就可以看到買家對店家的評價、對商品的評價,
而求職的時候,卻一定要到其他討論區、其他網站,
才能看到其他人對於該職缺或公司的評價,
這其實是一件很不自然的事情,
再說種種擔心對手黑函還是求職者亂抹黑什麼的,
嗯⋯⋯電商其實也會遇到這樣的事情,
總之想不到一個很合理的解釋,
唯一能想得到的解釋就是「盈利模式」。
目前我們在人力銀行上找工作,其實是不用付錢的,
但是企業卻是要付費用才能張貼職缺。
合理的推斷,
其實我們這些求職方就是人力銀行的商品,
真正的使用者是那些企業用戶(資方),
而讓使用者能夠留言討論的功能,
可能會讓部分企業用戶不想使用。
看看各大人力銀行上,許多職缺都喜歡「面議」
就知道資訊不對稱對於資方來說是一件多麼正常的事情
我不會認為敘薪是簡單的,
但給個底價,避免浪費彼此時間這件事,
真心不應該難道哪裡去。
否則騙人去面試的行為,其實跟詐騙集團一樣可恥
無論背後的動機是什麼,
既然人力銀行有其考量不做這件事、我又認為有需要的話,
那與其動嘴巴抱怨台灣的求職平台不好用,
不如自己來做做看,看能不能為台灣險峻的就業環境帶來一些幫助。
在做這個 side project 之前,
其實我自己找工作從來都沒有用過各大人力銀行,
這次還花蠻多時間在探究自己到底為什麼不用這些平台,
以及他們到底缺少了什麼。
為什麼是這個名字?
命名一直是蠻困難的一件事情,
本來有想過要叫什麼 job-bar in in der。
不過後來還是靈光一現跑出這個單字:
Clairvoyance。
為什麼要取這個名字有兩個版本的故事:
Clairvoyance,可以翻作洞察力或是透視,
主要是希望透過求職者彼此分享經驗,
來透視一個職缺的好壞,或是否適合他。
其實就是 google 天眼通,
翻譯的第一個單字就是 Clairvoyance,
然後我蠻喜歡周星馳的賭聖,所以就這樣命名了。
其實會做這個 project ,
有一部分是因為自己最近開始接觸分散式運算,
開始了解去中心化的想法,
我認為與其把所有對平台上職缺的評論給「集中」起來,
不如將它分散到各自原本的職缺下方,
然後再將相同職缺的評論同步。(Consistency)
這樣的做法是更合理,而且使用起來更有效率的。
假如說一開始我知道會這麼搞剛的話,
應該就會放棄了⋯⋯
整個 project 主要分成三塊:
gui:Chrome extension 的 UI
問題:chrome 的插件是明碼的,假如要在上面做認證,就要在 chrome 上面直接放 secret key,這樣做一點都不 secret
解法:開了一個 serverless 的 api 專門來做這件事情,在 repo 的 README 裡面蠻詳細的紀錄如何做到,所以這篇裡面不會贅述這一點。
這裡畫了個很粗略的圖,看一下會比較有概念:
clv = clairvoyance
其實 backend 就是處理留言、工作、使用者,
而我並不想自己維護一台機器做這些事情,
所以我用了 serverless 的方式去解決,
想要瞭解更多關於 serverless 基礎的人,
可以看一下這篇舊文:淺析 serverless 架構
是我剛學習 serverless 時做的筆記,
同時也是繁體中文裡面最詳細的新手教學。
就算簡體中文其實也是啦
前端的話,有一些比較討厭的部分就是非同步的處理,
但這裡 saga 很簡單的幫我 handle 處理好了,
而且還給了相當好的測試性,
這點非常非常重要,
測試省掉了我不少 debug 的時間。
中間大概重構了一兩次
首先就是要先訂好 schema,以及各個資料相互的關聯性,
這裡有用到 GSI 來建立查詢的 index,
雖然我們會用公司名稱以及職缺名稱來查詢,
但這兩樣東西都不適合拿來當作 Primary Key,
我覺得看完這篇官方的最佳實踐,
就已經差不多能掌握怎樣去設計一個拿 dynamodb 當作資料庫服務的心法,
剩下的只是把資料長怎樣想清楚而已。
使用 DyanmoDB 時,會考慮到資料一致性的問題。
但畢竟這不是一個非常要求即時性的服務,
所以我對於最終一致性這件事情是有相當高的容忍度的 :D
什麼是最終一致性呢?
就是我們不保證每個節點讀取資料時,資料都會是相同的,(強一致性)
但隨著時間過去,每個節點上的數據會回歸一致。
這只是很粗略的說法,接下來幾個禮拜可能會寫一些和分散式有關的,
就會提到這一點。因為一致性對於分散式運算來說一直是一個很頭痛的問題。
我選擇使用 React 及 Redux 的原因蠻單純的,
因為我最近常在工作上用到它們。
Reactjs+ CSS Module
CSS 的命名一直是一個很難解的問題,這裡我的想法是無論再怎麼有效的規範,都是軟性的,CSS 的特性讓全域污染這件事情變得難以避免。但 CSS Module 卻可以讓所有的 class 都變成 local 的,
React 以 component 為主的開發模式,跟 CSS Module 搭配起來相當不錯
Redux Saga
處理非同步的資料流(像是從 backend fetch 資料)
有些 UI 上的 transaction 都可以在 saga 處理
使用 saga 的重點是「測試」,effect 的概念讓測試變得簡單很多,少了各種 mock
其實這裡本來想用 Rx 搞定,但工作上真的用了太多 Saga,現在有點回不去了⋯⋯
假如你未曾瞭解過 saga,可以看一下我的這篇文章 Saga Pattern 在前端的應用
這裡不是要說有著多精美的 UI,
是自己開發時,總覺得我開發的東西,真他媽怎麼用怎麼順手啊!
實際上別人一看到時,卻常常完全不是這麼一回事。
最好的方法就是請朋友幫忙用一下,
然後什麼都不要跟他說,也不要有任何預先的假設。
沒錯,就算你有說明書,User 就是死都不會看(我也是)
很常發生的事情就是 User 完全不知道你想幹什麼,
留言區塊那邊一開始就是這麼一回事,
所以如果有人說你做的東西「太工程師」、「太 geek」,
大概就是這個樣子。
感謝我的幾個被我巴著幫忙測試的朋友。
m (_ _) m
目前只支援 1111 以及 104,
yes123 的 url,有那麼一點難以預測⋯⋯
不過也是因為這個 Project ,可以感覺到各個求職平台是否用心,
未來要加入的功能應該有以下幾項:
個人留言職缺的追蹤
Facebook 粉絲頁的機器人
重構
其實我知道這個 beta 版本還有許多可以更好的地方,
不過我更想瞭解這個插件是不是真的能解決一些問題,
所以就先釋出這個 beta 版了!
假如有什麼想問的問題也可以留言、發 issue 或直接跟問我,
對我來說,不只是想 build 一個小小的插件,
我想造出一個對求職者來說真正透明友善的環境,
我知道一定會有蠻多人覺得這真是 too young, too naive 的想法,
不過不試試看,怎麼會知道結果怎樣勒?
我想再舉個更接近實際應用的例子,
儘管並不是所有的應用都適合 serverless 的架構,
但聊天機器人(chat bot)是一個相當好的例子,
且讓我稍後再說明為什麼。
今天就結合一下很實用的粉絲頁回覆機器人以及 serverless 。
你可以把聊天機器人想成是你粉絲頁自動回覆的員工
或是進行一些簡單的操作
而聊天機器人流行起來的原因正是因為 mobile 裝置上的介面,
並不能滿足於現代人操作的所有需求,
聊天的介面解放了我們在小框框裡做事的限制。
或者是你是小小公司的開發者,需要一個助理來幫你做很無腦或繁瑣的事情,
再講下去可能要一篇了,如果你對這個主題有興趣,
可以看看 灣區日報是如何運作的
微軟、line、slack 都出了,
臉書當然也要 bot 來幫我們處理一些事情。
相當不建議直接照著貼,可以先看看我的前一篇文章,
至少現在敢大膽的說是目前最詳盡的 serverless 繁體中文入門教學:
這一篇筆記裡面會介紹如何把一個 facebook 粉絲專頁的 bot,
用 serverless 的方式架起來。
這個 bot 能夠:
處理粉絲專頁接收到訊息的 events
執行對應的動作或回傳訊息
為了保持簡單,並且專注在 messenger bot 本身,
我不會用到其他服務的 events,像是 DynamoDB 或是 S3 之類的,
但其實只要能掌握收訊息,以及對應訊息做出動作,
基本上就掌握了搭配其他功能的 interface 了 :D
假如你是個懶得看文章的人,我一樣把 code 放在 github 上面了:
有幫助到你的話給星星打賞,有問題的話也歡迎提 issue 或直接告訴我。
為什麼我認為 chat bot 是一個非常適合 serverless 架構的運用?
想想我們平常聊天,訊息也都不會馬上回嘛!
所以我們其實不需要那麼真正的「real time」,
而且只有在有人丟訊息時,lambda 才會幫我們運算,
省下了不少機器閒置在那的費用。
對於延遲時間的容忍度高:
有使用才收費:
簡單的運算
AWS lambda 運算時間不能超過五分鐘,否則會被強制結束,但這種簡單的文字回覆,通常處理不會超過五分鐘…吧
當然如果你要跑什麼類神經網路,那我會建議那些運算邏輯可以放在真正的 server 上
題外話是這篇 面試遇到 用 deep learning 解 fizzbuzz
看到後面超好笑 XD
https
: facebook 的 bot 會需要有 https ,通常可以透過 CloudFlare 免費申請一個,但假如你使用 lambda 的話,原生給你的連結就是 https 的。同樣的,因為我認為介面隨時會改變,
所以我不做截圖的 step by step 。
申請的類型有 ios、android 什麼的,
先選網頁,然後網址可以亂打一通,這對之後沒有影響
到 facebook 的 app 控制台
在控制列選擇新增產品
選 Messenger Expression
會看到一個新的 Messenger 跑出來了,選它
接著可以選擇你要把你的 bot 安置的粉絲頁,選擇後會得到一個權杖。
我覺得權杖是一個一聽會覺得「啥?」的命名,
不過它的意思就是你能夠讓 bot 藉由這個「權杖」,
取得在你粉絲頁發文或是發訊息的「權利」
什麼是 Webhook?
你可以把它看成是一種 back-end 到 back-end 之間的通知,
最常見的例子就是 CI 了
e.q:今天在 github 上送了一個 commit,
webhook 就會把這邊更新的訊息帶去給 CI server,
CI server 收到後就會開始跑後續的流程
hook,就是鉤子,在網路上把訊息以及收到訊息要執行的行為鉤住,
帶到別的地方(callback url)去的就是 webhook
為了驗證我們的 callback 是不是正確的,
facebook 這邊會去做驗證,
確認它送來的hub.verify_token
跟你粉絲專頁的權杖一樣時,
就會把 request 中的hub.challenge
送回來。
這裡有個小雷是我們要送回來的值是 integer,不是 string
官方的例子大概長這樣:
|
|
接著就一如往常的開一個 serverless 專案,
建立一個 handler function。
|
|
然後在來看程式的進入點:
|
|
operation
這個屬性是為了後續的動作,不管對這個 callback url 呼叫東西,
都會進入這個
handler.js
但是我們必須有不同的動作,我認為這裡都是屬於在 bot 執行動作的邏輯之下,
所以將它們放在同一個 handler.js 中,你完全可以有不同的編排方式 :)
event
裡的東西哪裡來呢?
event
其實就是 request,serverless 是個 event-driven 的架構,
我們可以在 s-templates
裡面去設置 template,
這裡有個 tricky 的問題,就是要怎麼處理權杖?
有兩種方法,
一種是在本地端用 module export 的方式解決,
另一種則是用 aws 的 env variable。
首先先看用 aws 的 variable 怎麼解決
|
|
然後我們就可以在 template 中使用 ${KEY} 的語法來拿到 variable,
這裡要注意你是不是在每個不一樣的 stage 以及 region 都設置了 variable。
要檢查的話可以進去自動生成的 _meta
資料夾看。
_meta
是自動被 git 給忽略的
接下來到 s-function.json
裡面設定 request 的 template,
把 callback
|
|
再來看 template
長什麼樣子:
|
|
假如你還不太熟悉 serverless,
這裡就是在描述剛剛 handler
中event
的長相:
|
|
你可能會覺得對方如果知道你的 callback url 那不就顯示出你的 secret 了嗎?
其一是這個連結不會對外,而你也可以限制 request 的來源,
而這也是為什麼要加上這一段的原因:
|
|
如果沒有 secret 跟 verifyToken 沒有相等的話,
會直接結束,並且返回 error。
假如你不熟悉 aws 也不想接受這樣的做法的話,
你可以在本地新建一個secret.js
|
|
然後把這支檔案 .gitignore
就行了,
不過這其實算是一種 hack 的方式,並不是一個很漂亮的做法。
假如你要自己 host 一個服務來放 bot 的話,
還要去額外申請 https,但如果你用 serverless,
搭配 api gateway 就直接幫你避免掉了這個問題
|
|
function - callback
跟 endpoint - callback
都選起來,
部署上去之後會返回一個網址,
當我們對這個網址送一個帶有 http method 為 GET 的 Requst 時,
就會進入我們剛剛看到的 handler.js
中執行東西。
最後就是把返回的那個網址貼在 callback url 那裡,
再把權杖給貼上去:
(下面的欄位我都會全勾起來 XD)
正確的方式應該是在 back-end 上放上 secret(這裡指權杖),
facebook 會送個 request 到你的 callbakc url 去,
並且看看在 params 中的 hub.verify_token
是不是等於你放上去的 secret,
如果是的話,再把 params 中的 hub.challenge
當作 response 丟回來,
facebook 就會判定你這個 webhook 通過認證,
後續才能繼續進行下去。
有兩種方法可以去「監聽」粉絲專頁收到訊息的 event。
假如你寫過 rx,會知道 subscribe 可以監聽 event 是否進來,
接著我們會去做對應的動作。
假如你沒寫過 rx,那你應該去學一下。
簡單說就是當我們監聽的粉絲專頁收到訊息時,
剛剛設定的 webhook 會送一個 post method 的 request,
而我們可以做出對應的行為,這裡通常就是返回一些訊息,
facebook 的 messenger 還可以回傳附件之類的。
官方給的 demo code 長這個樣子,先只要大略掃過一遍就好,
後面會更詳細解說這裡在幹什麼,
畢竟第一次看到的時候我也不知道這到底在幹嘛:
|
|
唯一知道的是我們送訊息時,會丟一個 POST reqeust 給 webhook,
雖然最後得到了一個 sender
(訊息的發送者),以及傳送的text
訊息,
還是有點搞不懂到底在做什麼,像遇到這種情形時,
把東西 log 出來就對了。
所以第一個目標就是來觀察一下 facebook 到底會送一些什麼東西過來。
先把 post method 的 template 建出來
|
|
handler.js
中其實 succeed 傳回的結果是什麼都沒差,
重要的是我們能看到傳過來的 request,要把它 log 出來
這是我們在寫 code 時常做的 debug 方法,
就算 serverless 其實也沒有不同 XD
|
|
接著我們到 facebook 上丟給我們剛剛創的粉絲專頁一些訊息,
假設我們密他然後說個:「Hello bot 」
到 AWS Cloud Watch 上面就可以看到返回的 body 長這個樣子,
可以快速的掃過一次(大寫的是是代表一些 id,你懂的):
|
|
看到這個之後,比較能知道 facebook 的 sample code 在幹嘛,
而不是單純的 copy and paste。
再上一次 sample code 來對照一下
|
|
看起來 facebook 的工程師為了保留開發上的彈性,
所以加上了一些目前看起來有點冗的東西,
我們可以選擇一開始就把 messaging_events
在 template 裡面拿出來,
或者是一樣拿回整個 body,不過為了說明方便,
還是照它原本的格式走。
總之,理解後就能開始試著把它改成 serverless 的模式了:
(真的是幾乎長得一模一樣)
|
|
為什麼要拿 sender
以及 text
呢?
原因就是待會回覆訊息會需要用到。
回覆訊息要用到我們之前的能登入粉絲頁的「密碼權杖」,
假如你是用 variable 解決的話,這部分會簡單很多。
只要把剛剛在 callback url 的 fb_secret_key
copy 過去就好了:
|
|
一樣先來看一下 sample code 是怎麼做的:
|
|
沒錯,這裡根本就可以直接拿來用了,
我們先求有再求好:
|
|
在執行 sendTextMessage
時,
裡面的 request
會是非同步的,
也就是說在後續的流程裡如果你讓整個 function 提早結束的話,
訊息將不會被傳送。
不過 user 一進來,其實不會知道 bot 有哪些功能,
我們可以設定對話剛開始的開場白,只要在執行這行:
|
|
FB_SECRET_KEY
就是前面提到的密碼權杖,PAGE_ID
是你粉絲頁對應的 id,
出來結果大概就是這樣子
facebook 也提供一些更 fancy 的訊息格式
針對特定的訊息去做動作
比起一般的小編回覆訊息,這裡能夠藉由 messenger platform 提供的 API,
回覆一個更像 app 的訊息模板、提供更棒的 UX,
啊!這樣講好抽象,直接看一下成果的話大概是這樣子:
沒錯,就是做了一個自己 blog 的 feeds
剛剛在 sendTextMessage
裡面會把 text
再額外包一層處理,
可見這裡是保留了其他彈性,
往後翻一下文件就會看到我們可以自訂訊息的模板。
|
|
在 genMessageData
裡面:
不要被長度嚇到了,你可以對照圖片中的字,
跟下面程式碼做對照,其實都只是在處理 elements 裡面一個個 object 而已
|
|
截至目前為止,我們已經理解了怎麼接收和傳送訊息,
對我來說這是一個比 slack 更輕量的小助理,
其實搭配 DynamoDB 或是其他 backend 就可以做到 schedule 的效果。
同時我認為 bot 並不是拿來取代小編的,
可以將一些常問的問題和解答建在 bot 裡面,
讓小編不用再去回一些重複的問題,專注在寫出更好的文案,
以及更急迫需要回應的客戶上面。
可以選擇搭配 hubot 來處理各種訊息,
以及對應的動作。
不過仍然要強調一下,這篇筆記著重在如何建立一個這樣的 interface:
收訊息 => 執行動作
另外,把程式邏輯全部都放在
handelr.js
,只是為了說明方便,你可以選擇自己喜歡的方式來建構 bot。
最後,額外提醒一下 XD
目前完成的 bot 只能夠跟你個人通話而已,
假如你想讓其他人也看到的話,
必須到 facebook app 的控制台通過 facebook 的審核後才行,
希望大家能做出許多好玩的粉絲專頁應用 XD
不管是在部署還是開發,都是以一個個 function 為單位,
這帶來了程式碼上的高度 decoupling,但同時也因為過大的彈性,
常常搞的我們無所適從,就像這張圖一樣:
serverless 更考驗著我們對系統設計的思維,
這是一篇非常粗淺的文章,
目的在帶領對 serverless 有興趣的人無痛的入門,
不管是在概念上,還是在實務的使用上。
假如你是懶得看文章的人,可以直接到我的 github repo 上面看
有哪裡寫錯的話可以提個 issue,覺得讚讚讚的話也可以給星星以茲鼓勵。
試想當你是一個單槍匹馬的開發者時,你絕對會希望能真正專心在開發,
而不是一天到晚擔心機器有沒有死掉,或者配置環境就花了大半時間。
我只是一個前端工程師,對於後端的知識甚是淺薄,
serverless 對我而言是個很合理的選擇,
但這不代表我不在乎任何後端的專業性,
更不代表著後端工程師使用 serverless 架構就是代表實力不夠。
相反的,我認為後端工程師如果能從管理機器中解放,
設計出更好的 serverless 架構以及更專注在程式本身的邏輯上,
那從 serverless 上能獲得的增益一定也是相當驚人的。
看著我們虛擬化的趨勢 => VM => Container => Docker 的興起
儘管做法略有不同,但方向是一致的,
都是想讓程式開發者更能專注在程式本身,而不是管理機器上
話說回來,前端後端的分界點一直都是個有爭議的問題,
不過就不在這裡去討論了
這篇會需要用到數個 aws 的服務,不過為了讓事情更單純,
我只會用到 IAM, DynamoDB, API Gateway, CloudWatch 以及 Lambda,
都不熟悉這些也沒有關係,因為我在寫完這一段之前,
也只是大略的把文件掃過去,也不用擔心縮寫令人看不懂,
因為我最討厭的就是這種縮來縮去的東西,
所以接下來都會在提到的地方解釋我們正在處理的是什麼。
以往都是直接用 EC2 開一台機器,
要用什麼直接當自己家的在上面裝就是了。
(當然可以學一些東西自動化這流程: chef,不過這不是這篇的重點)
這篇會著重在比較抽象化的概念上,
而不是去針對特定的功能作 serverless 的實現,
但不要誤會了,後面還是有一個簡易 restful api 的實作
我認為能掌握以下幾個點,才是針對特定功能實現的基礎:
Project 的架構
對於設計一套 serverless architecture 的抽象概念
各個功能與 api 間對應的關係
資料的處理
要能永久被儲存
CRUD 操作
Schedule:定時或是 routine 的去做一些事情(這一篇文章裡面不會提到)
部署
Log
至於使用的語言會是 nodejs。
不需要自己管機器,以及近乎無限能力的 scale-out(你的財力夠的話)
相對便宜。因為我們是有執行 function 才收費
高度的解耦及靈活的配置
有人說過,當你手上只有錘子時,那你看到的所有東西都會是釘子。
不過對於 function
這麼 general purpose 的東西來說,
它的確能拿來解決一切計算相關的問題,端看你組合的方式對不對而已。
講了這麼多好處,現在當然要來講它的限制。
有限的記憶體
timeout
高度的解耦
Latency
因為我們是需要計算時,才會去要資源來運算,每次都算是一個 cold start,所以對 latency 完全無法容忍的服務,可能不適合。
實際上透過 schedule 可以一定程度的解決這問題
風險
Scale-out
API 更換
服務被停用
我說一個字大家就懂了:Parse
當事情走到這一步的時候,基本上就沒啥救了,這就是我們冒著最大的風險
但就如同前面所言,我認為 serverless 是未來大勢所趨,也許不會所有的 project 都如此,不過大多數的中小型專案都會轉向朝這一架構邁進。
過度的自由,失控的 decoupling
Config 的設置以及部署 function 簡化
文件和 plugins
社群或公司支持
Serverless 的官網上有說到,現在是由一群工程師全職在維護這個 framework
gitter 上問問題也幾乎馬上就能得到回答
Apex?
TJ 的產品,目前還在觀望中,但 serverless 看起來相對較穩定、成熟
不過光是 TJ 這個名字,就很值得一試
就像我前面說的,因為高度解耦的關係,其實要遷移過來「理論上」不是太難的事
我不認為一個環境的建置,是在把東西裝一裝之後就結束了,
因為東西裝一裝之後,通常後續只會有更多的問題,
而且一個 project 本來就需要在一開始就做好 deploy 的準備了。
不部署的話幹嘛要用 aws 啊?囧
完整一點的 setup 應該要包含了從 建置基本設定 => 部署
才算是真的結束,
所以這一小節會從配置到部署都走過一次。
AWS 的介面可能會因為時間的關係,與下方略有不同,
但估計變動不會太大,知道要使用什麼功能比較重要,
故我不會把操作介面的圖片放上來。
跟以往一樣,我認為建環境是最困難的部分
首先要建一個 IAM
role
IAM(Identity and Access Management)
IAM
的功用就是讓你能夠管理使用者對於服務和資源所擁有的「權限」可以針對不同的使用者,制定不同的角色,
舉例來說,如果你今天的 api 只想讓 user 從 s3 的 bucket 裡面讀一些靜態資源
你就不會想要讓他擁有 access DynamoDB 的權限,懂?
IAM 是免費的。
到 aws 選取 services,在拉下來一狗票的服務中,
選擇 IAM
。
建立一個新的 User,名字就輸入:serverless-admin
。
建立好之後,
把拿到的 Access Key Id
跟 Secret Access Key
給記下來,
待會會用到。
接著選擇剛剛建立的那個 user:serverless-admin
,
在 permissions 的地方加上新的 policy,
這裡 aws 相當貼心的提供我們超大一坨的 policies 可供選擇,
為了方便,我們直接選擇 AdministratorAccess
。
當在 production 環境時,這樣處理 permissions 不會是一個好主意 XD
坦白說我覺得 permissions 會是一個令人頭痛的點
我們選擇了 serverless-framework
這一套 serverless framework。
|
|
會要你輸入名字以及剛剛的 access key id 跟 secret access key。
接著還要選擇你想要你的 project 運行服務在的地區。
再來稍後三分鐘之後, project 就會建好了。
會生成一大堆東西,下面列出簡易版的解釋,
看不懂也沒關係,之後在實作中就會碰到很多次了:
|
|
先讓我們 focus 在 function
上,這些 config 真的都可以先放著沒關係。
這不代表他們不重要,只是晚點再回來看他們是在做什麼
如果你真的現在就等不及,也可以到 serverless 的官方文件看
|
|
選擇 nodejs => Create Endpoint
接著就可以看到多了一個 functions
資料夾,
並且裡面跟著一個 posts
以及一些東西了。
一樣我們只要知道自己現在建立了一些基礎建設,稍後再來回頭看這是什麼。
|
|
|
|
這兩個都記得要選才會把東西部署上去 aws-lambda。
選擇 deploy 之後稍待幾秒鐘,就可以看到回傳一個網址給你。
這就是能夠執行我們剛剛部屬上去的 posts
的地方。
如果你沒做任何更改,點進去後應該能看到
|
|
到這裡為止,我們才能不心虛的說:環境建完,可以繼續了。
前面一直說到 serverless 架構是以 function 為單位去部署和開發,
現在來對「lambda function」有個具體的抽象概念。(欸?
先來個大略的概觀,你可以跟剛剛 create 的 project 對照著看:
每個 function 可以有許多個 endpoint(進入點)
每個 endpoint 可以有許多個 method( GET, POST…)
Handler 則是 aws lambda 執行的進入點(就是 handler.js
)
來看一下 handler.js
|
|
實際上我們運行的 function 就是長下面這個樣子,
在開始討論其他配置,和 aws 要怎麼運行到這裡之前,
先搞清楚到底在談論什麼東西:
|
|
可以有第三個參數 cabllback,
不過其實只要這兩項就可以運作的很好了,
而且 callback 實在不是一個好事
source event,可以是 push 或 pull model。
假設 S3 上面資料新增,lambda function 會接收到 event 去做事情,
那這就是一個 push model。
假設今天是 lamda function 去掃了一遍 DynamoDB ,
發現有事情要根據上面的資料去做,
這就是一個 pull model。
而 source event 也可以很單純的來自 http request。
context
是一個 object,
裡面包含了當前 lambda 運行環境的訊息,
以及一些 method。
有三個 methods 是一定要知道的:
這裡的參數是可選的,我們可以只讓 function 做事,
沒有一定要強制回傳結果。
context.succeed(Object result)
可以在執行成功時回傳東西: context.succeed(someObject)
注意這裡的 result
必須要能夠被 JSON.stringifyu 轉成字串
context.fail(Error error)
context.done(Error error, Object result)
這個就有點奇葩了,有了成功和失敗為什麼還要存在個 done 呢?
如果 error 不為 null,這次的 lamda function 就會被認定為執行失敗
再來是可以看到目前執行剩餘時間:
context.getRemainingTimeInMillis()
這裡所謂的看到當然是指在 function 執行時我們能利用啦!
不過要注意的是如果歸零,
AWS lambda 就會強制終止我們的 lambda function 了。
handler.js
前面有提到過這裡就是 aws 運行的進入點,
要在 s-function.json
裡面設定,
這裡看到我們只在 handler
那個屬性打上 : handler.handler
,
這有兩件事情值得注意:
handler.js
這個 module 底下的 handler
|
|
第二件事就是這個 hanlder 屬性還隱含著我們目前能作用的 scope,
假如我們是:function1/handler.handler
,
就把上層的 parent folder 給包含進去,
所以他就吃得到我們在根目錄安裝的 npm 套件。
比如說你安裝了 react,那你就可以:
require('react')
理解到這樣的程度,就已經足夠進行下去了,
直接來實作吧!
直接看文件時,總會有種霧裡看花的感覺,
不過等到實際開始做之後,你會發現其實概念只要 mapping 過去,
並沒有想像中的困難。
這個是完成後的 github repo,
如果你中途發現有什麼錯誤的話,可以在上面查看是否有哪裡不一樣。
底下會包含基本的 CRUD 以及 list,
大多數的應用程式都不脫這五種操作,
就算需要更特殊的操作,
也總是要熟悉這些基礎後才能繼續前進,
包含著如何儲存資料以及 debug 的概念。
至於資料夾的結構或是 workflow 的順序,
你都可以依照個人的喜好去調整,不一定要照我寫的走。
沒錯,我們先來看看要怎麼找出錯誤,從犯錯中學習,是新手成長最快的方式
來修改一下functions/posts/hanlder.js
context
和 event
是我們在 lambda 中要好好處理的東西沒錯,
不過這裡先專注在出 bug 時要怎麼解決:
|
|
這裡的程式碼有個明顯的錯誤,待會我們會除錯並且學習如何看 log
稍做一些更改之後我們就可以再次部署了:
|
|
再到剛剛的網址,會發現出現錯誤了!
幸好這裡加上了許多 console.log
,
假如你曾經寫過 JavaScript 對這樣的除錯技巧一定不陌生,
但,這裡的 log 不會在 console 印出來,會到哪裡呢?
這裡就要使用 aws 上的另個服務:CloudWatch 了。
到 services 點 CloudWatch,選取 logs,
就會看到這裡有個 log groups 就是我們剛剛建立的 functions。
選進去後會很神奇地發現我們之前 call 的紀錄都在這裡。
在 log 中我們可以看到:
|
|
我們出了一個 typo 的錯誤,改正過來以後就成功啦!
|
|
要存資料庫前,必須先在 DynamoDB
建一張 Table。
DynamoDB 是一個 no sql 的資料庫
為了 scale-out ,它在使用上有一些限制,
但在這個簡單的示例中,並不會需要考量到這些,
假如有興趣深入的話,可以看補充資料的地方
到 aws 上選擇 DynamoDB
。
Create table
table name 輸入 posts
primary key 名稱設定為 id
下面的 default setting 取消勾選,然後將 Read capacity units 以及 Write capacity units 都調成 1
我們就有一個很陽春的 table 了
接著是在 handler
裡面的更動,
首先要安裝兩個 package
|
|
前面有說過 lambda function 其實就是根據 source event,
去執行對應的動作:
|
|
其實蠻像我們平常在
redux
中處理對應的 action type 的reducer
這裡建立了一個 DynamoDB
的 client,簡單的來說,我們會把 event.payload
這個 object,
新增成 Table 裡的一個新 item,並且給它一個唯一的 id
,
畢竟是 Primary key 嘛!
如果你不熟悉 Database 的基礎理論,Primary key。
Primary key 就是我們拿來識別這個 item 在這個表中是唯一的「身分證」,
在這裡我們是用
id
來作為我們的 Primary key。
那這個 event
又是怎麼來的呢?
首先我們要了解的是 Create 這個動作對應到的 http method 是 POST
,
所以當我們在對同一個 url 執行 GET
跟 POST
時,
雖然 call 的是同個 function(或者更精確地說,是同一個 Endpoint)。
在 posts
資料夾底下,可以看到一個 s-function.json
,
這個檔案中放著的是關於我們在進入 handler.js
時相關的 config。
當然也包括了前面說到的 event
。
先直接看到 endpoints
這個 attribute,裡面有許多個物件,
預設的是這個:
|
|
這裡有好多東西,
假如我們要在裡面定義我們對每個 endpoint 的長相,誰不發瘋呢?
眼尖的你應該看到了有 template
這個字眼,
而剛剛送進來的 event
正是一個 http request,
所以我們要做的事情已經呼之欲出了,就是在requestTemplates
加上我們指定的 template 名稱,
就能根據這個 template 生出我們想要的 event 。
在 endpoints
中加上了這個新的 object:
|
|
當進入這個 api 時(path 沒有改變),使用 POST method時,
我們的 request 會照著requestCreatePostTemplate
這個 template 走
$${requestCreatePostTemplate} 是特殊的語法,
讓 serverless 知道這是個 template 名字,而不是一般的 string。
所以我說,那個 tempalte 呢?
這裡要在 posts
底下新增 s-templates.json
,
所有的關於 lambda function 的 template 都會放在這裡。
接下來我們就可以設計我們的 request(event)的長相了:
|
|
這裡比較讓人疑惑的是 $input.json('$')
是什麼,
這其實是跟 API Gateway 比較有關係的 template 語法,
而不是 serverless 這個框架底下的。
This function evaluates a JSONPath expression and returns the results as a JSON string.
For example, $input.json(‘$.pets’) will return a JSON string representing the pets structure.
簡單的說,他會將 input 轉成一個 json-like string,
更棒的地方是他可以像我們平常 access 底下的 attribut 那樣去找底下的東西:
(就是所謂的 json path)
像是 $.pets
就是將我們吃到的 input object底下pets
對應到的東西,
轉成 string。
Amazon API Gateway: Mapping template reference
想瞭解更多關於 Template 的話可以參考 serverless framework 的文件:
接著回到一開始的 handler.js
,
就可以把跟 event
有關的東西與我們前面 template 裡面所做的 config 連接起來了:
|
|
這時候可以部署了!
部署完成之後我們需要試試有沒有成功,必須要打開 API Gateway,
一進去就可以看到對應 project 名稱的 api,
點進去能看到我們現在有哪幾個 api 可以用(url)。
可以把 API Gateway 想像成我們平常使用的 router
,
Gateway 會把要執行的 endpoint 接到對應的 url 上。
點擊 /posts
底下 POST
method 的 integration request ,
在 Body Mapping Templates 可以看到對應的 template:
|
|
那,要怎麼測試呢?
我習慣用 postman,算是一個測 api 相當好用的工具,
找到serverless-demo
這 project 底下對應的 stages
,
選擇當前對應的 stage(預設應該是 dev),
然後選擇Export as Swagger + Postman Extensions
這個選項,
會下載一個 json ,裡面把你所有建立的 request 都包好好的。
接著就能在 postman 中 import ,就能直接使用了。
首先當然是先測試原先的 GET
method,理論上來說應該要丟出 error,
因為送進來的 request(event),它的 operation
是 undefined
:
|
|
非常的好。
接著是POST
:
|
|
居然噴錯了,所以我們要再度到 CloudWatch 去看一下 log,
看起來 event
的樣子是對的,但往下一看就找到了這個錯誤:
|
|
我們在根目錄雖然有package.json
,
但是目前對於底下的 handler.js
而言,
它對根目錄是完全一無所知的,那該怎麼做呢?
在s-function.json
中的 handler
改成 functions/posts/handler.handler
,
我們能在這裡決定 function 要對整個 project 的權限到哪裡,
像這裡就會一直延伸到根目錄,所以我們在根目錄所安裝的 package,
自然到了posts
底下也吃得到了。
假如仍然沒有辦法動到 dynamodb 的話,
就要到 s-resources-cf.json
更改設定
在IamPolicyLambda.Properties.PolicyDocument.Statement
底下加上:
|
|
再去 Postman 執行一次,
DynamoDB 的 Table 裡面就會出現新一筆的資料了(一個新的 Item)。
第一步一樣是從 handler.js
裡面直接去做更改:
為什麼每次都從
handler.js
開始是因為這邊是最符合邏輯的地方,其他都比較特定的 config 問題
|
|
接著要到 s-function.json
裡面去加上對於 parameter 的設定,
以及加上 template:
在 GET method 的底下
|
|
最後則是 template:
|
|
假如你好奇為什麼要用
Key
的話,可以參考 DynamoDB js sdk 的 github
與 mongodb 的 query 非常相似
因為我們在 handler 中用了 context.done
,
這裡其實是個 callback function,等到 getItem
結束後,
才會執行 context.done
,
並且會依序傳入 error
、data
兩個 object,
所以回傳的 response 會是像這樣的一整個 item:
|
|
有時候我們並不想讓使用者知道這麼多,
所以可以使用 response template,
這裡就能看到前面說的 json path 的用處:
|
|
|
|
Update 跟 Read 的做法其實已經大同小異,
一樣是把查詢用的 Key 放在 params
中,
這裡我們一樣把整包 payload 都丟進來。
|
|
看起來只是改成使用 putItem
而已,
但其實這邊的 template 有點小小的改變。
|
|
這樣子的好處就是在更新時,只要在 params 輸入指定的 id
,
其餘要更新的部分就是放在 body
裡面。
這裡的
PUT
並不是 partial 的更新,而是整個會替換掉,符合它原本 HTTP method 對應的行為
至於s-function.json
裡面要怎麼改,這有點太 trivial ,
就不放上來了。
刪除一個 item,要做的事情比 update 單純多了,
基本上只要指定好 Key,一切就已經結束了:
|
|
|
|
除了以上的 CRUD 之外,
列出一定數量的 items 也是一個相當常見的需求。
|
|
|
|
最後的 Response template 會用到 foreach
語法,
坦白說這裡我壓根不想去理解這裡的意義是什麼,
我寧願在需要的時候再去查文件就好,
因為我相信這種夭壽的語法遲早會被改掉的:
|
|
現在大概知道,
為什麼當初開始學的時候網路上沒什麼好的教學文了,
因為 config 的設置真的是挺複雜的,
不過我想這一篇這樣記錄下來,應該能讓許多人省下走冤枉路的時間。
對於一個程式開發者來說,學習東西的時間就是最大的成本,
我想 serverless 不管對於前後端來說,
都是一項很超值的投資。
因為大部分時候,我們都不需要開一整台機器來完成你想做的事情。
在完成這篇之後,可以做什麼練習呢?
你可以試著把你原本在 EC2 上 host 的服務,
轉移成 serverless 架構。
光想就覺得超難的
或者是把一些 routine 的工作,用 serverless 的方式去做,
當你越過前面那些雞巴毛 config 後,
你會發現開發和部署上帶來的效率令你吃驚。
這是篇長文,你可以直接跳到你想看的地方就好
或是直接在 github 上面看我 step by step 的教學
redux-thunk-to-saga-tutorial
先把結論講在一開始,這並不只是一個 library 的使用方法介紹而已,
因為學習 saga pattern 對於前端工程師是有幫助的,
主要不出以下三個概念:
好的 UI/UX 該是一個畫面的 transaction
User 隨時能夠取消 transaction
滿足上述條件實作出來的資料流是要容易被測試的
那redux-saga
到底是在解決什麼問題呢?
答案:
讓我們的非同步 action 能夠更好被開發、維護、測試。
讓我們用不同的方式來思考非同步的前端資料流
saga 的中文翻譯是冒險故事
這裡來舉個例子:我們要登入
|
|
你會怎樣去設計這個資料流呢?
畫面要有什麼 state ?
假如登入要可以取消,你要怎樣改變畫面的 state 呢?
這個流程看似簡單,
但要處理的乾淨、又好測試,
是不是事情就沒有那麼直覺了?
目前看起來好像很抽象,但瞭解後,
redux-saga
並沒有什麼神奇的黑魔法。
我不認為 redux-saga
的只是拿來取代 redux-thunk
的工具,
重要的應該是 saga 這個 pattern 背後的概念,
給了你新的方式去思考前端資料流。
送出資料 => loading 動畫 => 完成
其實前端的畫面也隱含著 transaction 的概念在裡面。
我認為如果有出現以下幾個現象,
那 redux-saga
值得你一試:
學會 generator function 卻無處可應用
處理非同步的 action 時,總覺得哪裡怪怪的 => 回傳 promise 時要怎麼測試
純粹好奇 redux-saga
能幫助你什麼
有些人會說 redux-saga
的學習曲線比較陡峭,
其實並不盡然。
會覺得 redux-saga
太過困難,
通常就是因為一次就想直接學會、並應用,
忽略有些預先知識必須要一步一步學習,
而且有些情況,必須拉高一點視角會比較好看清楚,
從概念的角度去看,而不是只關注在前端的實作。
我認為這裡只有三件事情要掌握
什麼是 saga?
saga 跟前端開發有什麼關係?
redux-saga 的基礎用法
要學一個東西,把名詞搞懂是很重要的。
像 router 就是個很直覺又常見的名詞,
saga 是什麼呢?
redux-saga
有提供一些資源供參考,
包括了最原始提出 saga 這個 pattern 的論文。
一共 11 頁,不過扣掉 acknowledgment 跟 References ,
就只有 9 頁半啦!
不過論文中是從 Database 的角度看,
另一個影片,是從應用在分散式系統的角度去解釋,
提高了不少複雜度。
基於以前端的角度,這篇講解 saga 主要會以 paper 上為主。
saga 其實是個很簡單的概念,
要應用它也並不困難,
這篇論文在 DBMS 上實作的原因,
主要只是要闡明如何實做一個簡潔、有效率的 sagas,
所以不要擔心接下來講的例子看起來跟 redux 或前端開發沒有關係,
稍後會提到要怎樣在前端開發中應用 saga 這個 pattern。
所以看個幾分鐘之後,腦袋裡會冒出許多的問號:「所以 saga 是⋯⋯?」。
這裡我試著用最簡單的語言解釋 saga 是什麼。
Saga,就是個滿足特殊條件的 LLT(Long lived transaction)。
待會會說是什麼特殊條件。
如果你不知道什麼是 Transaction:
是 Database 上常會用到(但不僅止侷限於 Database)的名詞,
即是「交易」。
「交易」聽起來很抽象,
其實他要敘述的就是銀貨兩訖後,
一個交易才算是完成,
假如銀貨不兩訖的話,那要退回最一開始的時候,
買賣雙方的狀態會退回交易前的狀態,不會有任何改變。
Long lived transaction 是什麼呢?
而 LLT 就是一個長時間的 transaction,
就算沒有受到其他影響,
整個完成可能也需要數小時或數天。
聽起來,似乎是很糟糕的概念對吧?
因為為了實現 transaction,我們通常會把正在 transaction 中的 object lock 住,
讓其他人沒辦法更動它。
(維持資料的 consistency)
所以這麼長時間的 transaction,
會造成兩個問題:
較高的失敗率
dead lock 造成的長時間 delay
舉個很實際的例子,就是江蕙演唱會的訂票。
購票的時間可能會是某一段時間,
而我們最終要確認訂票的數,這就會是一個 LLT。
為解決這個問題,
我們這裡可以假設這個 LLT:T
可以被拆成許多相互獨立的 subtransaction的集合:t_1
~t_n
。
但如果我們不會希望t_1
~t_n
分別被送進 DB 並且記錄下來。
以上述江蕙演唱會的例子,
每個小t
就會是每筆訂票紀錄
如下圖:
假如每個 transaction 都一次就成功,
而且沒有人退票的話,那個 transaction 就會正常的被執行:
因為假如有一個失敗的話,
那 T
就不算是完成的 transaction。
儘管如此,這樣做也比一般的 transaction 帶來了一些彈性,
我們可以隨意的插入 subtransaction。
接著就來解釋 saga 運用什麼樣的設計方式來解決這些問題。
第一件要注意到的事就是 saga 仍然是個 LLT。
saga
: LLT that can be broken up into a collection of subtransactions that can be iterleaved in any way with other transactlons
作為一個 LLT,
假如任何一個 saga 中的 subtransaction: t_i
單獨執行了,
我們應該要有一個 compensating transaction c_i
可以將它 undo。
這裡的 compensating transaction,
指的是從語意上的觀點來看,
而不是整個系統都得還原到 t_i
發生的那個時間點。
再看一次上面這段話,魔鬼就藏在細節裡,
這正是 saga 為什麼可以解決 LLT 問題的關鍵。
你可能會覺得這兩件事不是差不多嗎?
舉個例子:
如果有個 LLT :
T
是要記住所有買江蕙票的座位數,底下每個訂票都是一個 subtransaction:
t
。假設
t_i
要被買票的人取消,我們執行
c_i
時,只是把買的座位數從 database 裡面減掉
而不是讓 database 回到
t_i
發生前的時間點
所以我們可以得到一個簡單的公式,
Saga’s gurantee:
如果全部都執行成功(Successful saga):
t_1
, t_2
…., t_n
示意圖:
失敗的話(Unsuccessful saga):
t_1
, t_2
…., t_n
, c_n
…, c_1
這裡可以注意到其實
c4
是沒有做任何事情的,在實作時候如果是最後一個 transaction failed 掉的話,可以忽略
c4
不過就算執行了也不應該會出錯
因為每個執行應該都是 idempotent(冪等)的
如此一來我們就掌握了對 saga 的基本知識了!
在進入redux-saga
前,先來看看我們會遇到什麼問題
講了這麼多抽象概念的事情,
讓我們回到實務上來看,
來看最開始的這個例子:
|
|
畫面出來大概是這樣:
以下部分你可能必須要熟悉
redux
,或是任何單向資料流的架構,
我盡量不預設讀者有任何預備知識來寫以下的文章 XD
不過真的不行的時候,會放上參考資料
在 redux 中,如果要改變畫面的狀態(state),
我們必須 dispatch 一個 action 到 store 去,
而對應的 reducer 會根據 action 幫我們生出下一個 state,
並且將 store 中的 state 更新成對應的新 state。
reducer(state , action) => nextState
假如還是很模糊的話,可以看看 redux 優秀的文件:
來看一下 login
的 reducer 會長什麼樣子:
這裡為了簡化,有刪去一些東西
|
|
歸類成以下幾個結果:
LOGIN_REQUEST
:當我們送出LOGIN_REQUEST
這個 action 時,會進入 loading 狀態
LOGIN_SUCCESS
:登入成功,會拿到 username
以及對應的 token
LOGIN_ERROR
:登入失敗,會拿到錯誤訊息
那真正執行的時候該如何執行呢?
Thunk?Is it good to drink?
來看一下維基百科的解釋:
In computer programming, a thunk is a subroutine that is created, often automatically, to assist a call to another subroutine.
只截錄一小段,剩下的多看也只是搞混。
簡單說就是我們為了把一個 subroutine A 的工作,
帶到另一個 subroutine B 做完,
中間需要一個橋樑:subroutine C,
這個 C 就是 thunk 啦!
在 redux 中,我們如果要讓一個 action 能夠更新,
必須要 dispatch 它。
所以上述的login
流程大概會長這個樣子:
|
|
loginRequest
是一個 action creator,
會回傳{type: LOGIN_REQUEST}
這個 object。
這裡回傳的就是一個 thunk,
因為我們在這個 action 裡面同時得完成:
送request
收到 response data
處理錯誤
所以我們必須把 dispatch 給傳進來,
完成原本只靠單個 subroutine(一般的 action creator) 無法做到的事情。
這裡有什麼問題呢?
你要如何去測試這個一連串的動作?
這裡回傳的是一個 promise,它無法被 abort,如果我們今天想加上取消按鈕呢?
loginCancel
這個 action 呢?當然, login 是一個相對簡易的流程,
假如遇到有更多 state 要處理,
無法寫出測試以及不那麼直覺的語法,
將會為我們的開發帶來一些問題。
這裡的一整個 loginFlow
,其實就是一個 LLT(長時間的 transaction),
可以看完這一段再回到這裡 XD
底下的 subtransaction 就是各個 action(request, success, error)。
有了這樣的概念之後,剩下來的事就簡單多了。
而且 saga 就是底下每個 transaction 都附帶 compensating transaction 的 LLT,
也就是說上述的 abort ,在 saga pattern 之下是內建的。
redux-saga
這裡跟概念比較沒關係,
但環境設定絕對是許多人卡關的第一步。
首先要建立一個 sagas 資料夾,
底下有一個 rootSaga,它會是一個 generator function:
|
|
接著在 middleware 中將它跑起來。
|
|
這裡的基本設定,其實每次都大同小異,
所以就不再多著墨底下發生什麼事情。
前面有提到的 subtransaction,可以很粗略的對應到這裡的 effect
。
saga 不出以下幾種情形:
監聽 action 發生 -> take, takeEvery
執行 transaction -> put
取消 transaction -> cancel
右邊的就是我們在 redux-saga 中對應到的 helper function,
他們就是 action creactor 一樣,會回傳一個物件,
不過這一次是回傳一個 effect ,而不是 action,
e.q: take({type: LOGIN_REQUEST})
就是產生一個拿到 loginRequest 的 effect。
接著就來把 code 改寫吧!
|
|
值得注意的是這裡都是 generator function,
假如你完全對 generator function 沒有概念的話,
推薦你看這篇文章。
是我寫的 XD
這裡的 code 還蠻語義化的,
就是當我們遇到一個 LOGIN_REQUEST
的 action ,
就會執行 loginFlow
這個 function。
接著是前面提到的好測試,
我們來測試這個 saga 吧!
|
|
這裡比較 tricky 是我們測試的是 effect 的名字,
為什麼不是直接 deepEqual 兩個 effect?
我們回傳的 effect 其實就是個 object,長相是下面這樣:
|
|
只要 name 是對的,我們就知道他在對應的 LOGIN_REQUEST
進來時,
會執行loginFlow
這個 function。
而且在JavaScript中會判斷這兩個 next 是不同 function XD
直接測試名字,是我現在想到比較直觀的方法
Talk is cheap:
|
|
call 跟我們熟悉的 Function.prototype.call
很像!
不一樣的是,這裡的 call 會回傳的是一個 effect
,
這代表什麼?代表我們能夠很好的測試它,
而不是真的去 call loginAPI,帶來了無止盡的 mock。
我們把 loginFlow 的 test 拆成四個部分來看
Initialize
Call loginAPI
Handle login success
Handle login error
前面的 watch function 會把 request 這個 action 丟進來這裡,
所以我們要先製造出一個待會會用到的 iterator:
執行 Generator function 會返回一個 iterator,
然後我們去對這個 iterator 呼叫next
function
感謝 CT 的指正。
|
|
再來則是 call API,注意我們測試的是 call effect,
而不是真的去呼叫這個 API:
|
|
|
|
這裡我們可以運用 generator 的特性來把假 error 丟進去XD
裡面的 catch 接到 error 之後,就會執行 login error 的流程了。
|
|
首先要把 login 的 saga 接到 root saga 去
接著我們要來把原本 dispatch 的 loginFlow action 換成 loginFlowSaga 了。
|
|
再來我們只要把原本放 loginFlow action 的地方,
換成 loginRequest
這個相對簡單的 action creator 就行了。
這樣也更符合實際在運作的方式,
他按下這個按鈕做的 action 就只是送出 request 而已,
剩下的部分就是讓 saga 中的 generator 去管理,
而且經由這樣的拆分,我們發現接下來能夠實作 cancel
。
就是 saga 中的 compensating
這裡的 code 就請到 github 上面去看了 XD
總之我們得到了一樣的效果,但是更容易測試以及維護:
前面有說到要實作取消這個功能,
在 promise 中是很困難的,因為 promise 沒有辦法 abort。
不過我們活用 generator 的,就有辦法很直觀的實作出這個功能來。
首先當然是先做出 cancel 這個 action,
以及讓 reducer 根據這個 action 作出對應的改變。
完成了之後,接下來就是 saga 的重頭戲了。
fork
and cancel
首先我們要將原本的 loginFlow 拆分成兩部分,
第一部分是原本的 login 流程:
|
|
第二部分則是取消 login:
|
|
這裡我們看到兩個新的 effect,第一個是 fork,
語法基本上跟 call 相同,
不同的部分是 fork 跟我們在 git 上面的 fork 一樣會開一支 branch出來處理,
當 yield fork effect 之後,
就會自動開一條 branch 執行下去,這裡有個 @kuy 做的圖:
而如果我們在上述 task 完成之前,就接收到了 loginCancel
這個 action,
那所有在 task
裡面的動作就會被 abort 掉!
是不是覺得有 race condition 的概念在裡面,
沒錯,redux-saga
也提供了race
這個 effect
這裡一樣也測試以下幾件事情
是否有 fork 一個新的 task
是否能處理 cancel 這個 function
拆分出來的 authorize 是否正常運作
首先當然是先看進入 loginFlow 之後有沒有 fork :
|
|
接下來是是否能處裡 cancel,
這裡我們就需要用到 mock 了,
在最外層的地方從 redux-saga/utils
引用 createMockTask
:
|
|
這裡仍然是運用了 generator 的特性來做 mock,
因為我們再隔一個動作才能取消 task,
所以在這之前我們要先把 mock 起來的 task 丟進去。
最後則是確認原本的 authorize 流程還是能正常運作,
基本上只是把原本的 test case 丟進另一個 describe 的 block 而已,
詳情可以去看 repo 裡的 code。
其實這裡蠻簡單的,
只是新增一個按鈕,按了會 dispatchcancelLogin
這個action,
一切就結束了。
像是底下這個樣子:
結論就是我們現在終於將 saga pattern 應用在前端了,
每一個好的 UX 都會是一個 transaction,
而且比起原本的論文中,我們多了一些彈性,
可以選擇要不要加上 compensating transiction。
如此一來我們的非同步 action 變得更好測試,
而且也不用擔心在每次處理過度複雜的資料流時,
沒有依據可找了,因為我們都是在組合各種 effect 而已XD
假如熟稔其他語言的人,
可能都知道 generator function 是什麼,
不過對於一位平常都在寫原生 JavaScript 的人,這就很新鮮了。
當然,generator 就算在 es 裡面也不算是什麼太新鮮的東西。
畢竟跟 JavaScript 有關的東西大概超過一個月就算舊的了
使用 Generator function 並不是一件求新求潮的一件事情,
活用 Generator function 能讓測試以及開發非同步的程式碼都變得更直觀。
這篇文章就來淺淺的介紹一下 Generator function 究竟是什麼。
之所以是淺淺的介紹是因為,
我認為深入介紹太多不同的特性,
沒有搭配實際的應用,
那其實只是一篇寫的比較詳細的 document,
所以這篇只打算介紹到「可以用」的程度而已。
這篇文章的圖片就是在提醒你這是一篇淺淺的文章,
讓我們慢慢跳進去,才不會一開始就把頭撞爛。
其實本來是要寫關於
redux-saga
的,
只是不先介紹 generator 真的講不下去 Q_Q
這篇會包含以下幾個主題:
先來講講我們熟悉的 function:
|
|
是個 run-to-completion 的 function,
一旦進去了,就會一直執行到結束,
看上述的 code 就知道這個東西會執行的非常非常久,
因為它一旦進入,就要執行到被完成為止。
generator function 特別的地方就是它可以被暫停,
等到下次進來時再繼續呼叫它。
先看下方這個改寫過後的小例子:
|
|
可能會有點不熟悉這樣的語法,
不過可以先感受一下一次拿一個值出來,
以及可以被暫停的 function 是長什麼樣子。
下面來更清楚地敘述一下 generator function 的語法。
|
|
function
後面或多個 *
。
有人會爭論到底是要放在 function 關鍵字後面,
還是直接放在 function 名字前面
e.q:function *generatorFoo
兩個都是合格的語法,
不過我習慣放在function
關鍵字後面,
我認為這是個不同的function
,
而且 function name 本身並不該包含*
至於參考資料裡面有附上 MDN 中對於 generator function 的語法介紹,
也將*
放在緊接著function
關鍵字的後方。
這一部份還沒做過更全面的研究,
畢竟最近也才在實作中加入 generator function 而已
yield
這個關鍵字估計就是 generator 中最特別的概念了,
|
|
當我們呼叫 generatorFoo
時,
會得到一個 iterator,
當我們每次呼叫這個 iterator 的 next
方法時,
就會執行 generatorFoo
,一直到出現 yield
關鍵字的地方,
接下來會暫停,直到下次呼叫 next
。
我知道還有
yield*
,不過這個概念等後面再說
next
|
|
像以上的例子,第一次呼叫 next
時,
就會執行到 yield i
這個位置,接著暫停這個函數,
直到下次執行next
。
next()
返回什麼?next
function 會返回一個物件,裡面包含著兩個 properties,
分別是 value
和 done
:
value
,就是我們在前一段中從 yield
那個位置,
接到的「值」。
done
是個 boolean 值,
假如這個 generator function 完全被執行完的話,
done
就會變成 true
,反之亦然。
這裡要注意的是當執行到最後一個 yield
時,
done
仍然會是 false
,
再執行一次才會得到 done
為 true
的結果。
而 generator function 仍然是一個 function,
我們可以在裡面 return
東西,
如此在執行到 return
這一行時,
next
就會返回 value
為 return
的東西,
並且 done
為 true
。
提醒:
如果你真的需要 return,那你很可能只需要普通的 function 就足夠
在 generator function 裡面 return 東西,
容易令人感到困惑,簡言之,沒事別這樣做。
next
中傳入參數我們可以這樣做:next(x)
,
這樣做的結果就是將x
塞入前一個 yield
產生的地方。
直接看例子會更有感覺
|
|
第一個 next
不傳入參數是因為在這之前,
不會有前面一個 yield
。
而第二個 next
中傳入的 12
完全替代掉了前面 x+1
的值,
所以後面的 z
會等於 12*2/3
,也就是 8
。
最後一個傳入 13
,是替代掉第二個 yield
所產生的值,
這裡已經可以完全忽略 y/3
是什麼,直接替代成 13
了。
將上述的值全部替代進去會長成下面這樣:
|
|
看起來很蠢沒錯,
不過這樣替代值的方式也許比直接文字描述來的更直觀一些
for...of
|
|
我們一樣可以使用 for
來遍歷整個 iterator,
不過要注意的是,我們只會拿出 done
為 false
的值,
也就是說上述的 2
並不會在 for...of
中被拿到。
雖然說是 optional,
不過為了在實戰中時寫出更 robust 的程式碼,
瞭解 error 要如何處理是很重要的,
畢竟你連丟出來都沒辦法, unit-test 就測不了啦!
在 generator 中可以用我們熟悉的 try…catch 技法來做到 error handling:
|
|
不一樣的是我們能在外面直接把 error 丟進去:
|
|
這裡有個比較 tricky 的地方是我們把 error 給丟進去後,
如果在 generator 內部沒有catch 到,
這個 error 就會丟出來外面被 catch 住:
|
|
我其實不喜歡用 delegate 這個字來解釋,
總覺得有點在賣弄的感覺 XD。
簡言之就是將遍歷 generator 的控制權交(delegate=委託)給內層的 generator。
Talk is cheap, show me the code:
|
|
執行到 yield* foo()
時,
就會把控制權交到 foo()
所產生的 iterator 上,
所以最後那個 for...of
就會印出 1~5 。
基本上就是這樣而已,我認為了解到這點就已足夠,
在這裡敘述太多語法卻沒加上實際應用,
真的只會搞混而已,
所以請容我到之後應用篇時再繼續說明 delegating 的好處。
如果現在就想再鑽下去,可以看下方的參考資料。
假如你是使用 webpack 來做前端資源的打包,
恭喜你,這是一件再簡單不過的事情;
假如不是的話,你可以:
學會使用 webpack
或是想辦法跟 Babel 搭起來
我們這裡會運用 babel 來幫我們非常簡便的在專案中啟用 generator function。
當然,如果要連 webpack 一起介紹會太囉唆,
以下都假設你已經會實際使用 webpack 的 babel-loader:
es2015 這個 preset 已經包含了 generator function。
preset 只幫我們做到 transform 的功能,
真正要在實際環境中動起來還需要 polyfill 的幫忙:
所以我們必須要安裝:
|
|
接著在 .babelrc
中
|
|
最後在你要用到 generator function 的地方加上 import 'babel-polyfill'
。
其實這裡不一定需要用到整包 polyfil,
只要有regeneratorRuntime
被定義好就行了
有興趣的人可以參考一下 facebook 的 regenerator
babel-polyfill 裡面也是用到這個 project
其實這篇文章主要是系統性的介紹 generator 到底是什麼,
下篇文章會介紹我們實際在應用時,
generator 能幫助我們做到什麼。
如果要一言以蔽之的話,
那就是:
「能將非同步的程式碼,用同步的語法來呈現。」
乍看之下很神奇,
但在了解 generator function 不過就是個能夠暫停、繼續的 function 後,
就大概能對他能做到的事有最初步的想像了。
沒有組語,只有 JavaScript。
之前看到 react-motion 的作者說了這句話:
Lisp: everything's data
— Cheng Lou (@_chenglou) 2016年4月20日
Smalltalk: everything's an object
Haskell: everything's computation
JavaScript: everything's a library
老實說,不知道從什麼時候開始,
我們前端工程師大多都變成工具的使用者,而不是設計者了,
真正基礎和有價值的事物其實一直都隱藏在我們常使用的工具底下,
像是我們為了使用 es6 語法的 babel 就是一個很好的例子,
好好靜下心來寫一個小小的 lisp compiler,瞭解底下發生了什麼事情,
以及弄懂 compiler 究竟是多麽偉大的想法,
對於一個前端工程師的身心健康都頗有幫助。
至少對我來說是這樣啦!
提到 Compiler 總是讓人望而生卻,
在 ember conf 上, James Kyle講的這個 talk,
讓我覺得該記錄一下這篇文章,
好好推廣一下 Babel 底下發生了什麼事情。
首先先來談談 Compiler 是在做什麼的,
Compiler 的工作是將「來源代碼」轉成「目標語言」。
除了我們熟知的 gcc 之外,還有 Babel,
沒錯,我們的生活周遭充滿了 compiler,
就算是寫 JavaScript,
如果要使用 es6 以上的語法,
你就必須得用到 Babel 這個 compiler。
將你寫的 code(來源代碼)轉成現在瀏覽器上跑得動的 JavaScript(目標代碼)。
關於這樣子使用是否合法,已經在 twitter 問過作者,
相關的 license 也放在最下方了。
懂 JavaScript(老實說不懂也沒什麼差)
不需要懂 Lisp(沒錯,雖然我很喜歡 Lisp)
有人要一起開 SICP 的讀書會嗎?XD
懂得寫出簡單的遞回函數
Regular Expression
這裡不會使用 ES 6 的語法,
主要原因只是想讓環境配置盡量單純簡單,
只要有辦法使用 JavaScript 的人就可以讓這個小 compiler 跑起來。
這裡要 compile 的不是 babel 或 js,
而是 lisp,原因是因為語法單純簡單得多,
能夠讓我們更專注在 compiler 的概念和抽象化上。
影片中有提到,要做出一個 compiler,
基本上只需要三步就完成了:
Parsing:將 code 轉成抽象化的樹狀格式,方便轉化。
Trasformation:將 Abastract syntax tree(之後會講到)轉化成好生成 code 的形式
Code Generation:產生目標的程式碼,這裡是 JavaScript
坦白說現在看起來是蠻直觀的想法,
但這種事情,都碼是你想出來之後就覺得很簡單,
所以做完一遍之後,反而更能感受到設計 compiler 是一件多麽偉大的事情。
想當年臉書紅的時候,多少人說過自己當年也想要做一個社群網站呢 XD
開始之前,先定義一下我們要完成什麼事情。
我們要將:
|
|
Compile 成:
|
|
就是這樣而已。應該沒有很難吧?
綜合一下前面講的,我們要寫的 compiler 會長這個樣子:
|
|
Parser(或稱 lexer),
會將 raw code 先切成一塊一塊後,
再根據這些小塊的語義來建立一個 Abstract Syntax Tree(以下簡稱 AST)。
這裡很明顯的分成兩個步驟:
Lexical Analysis:就是分詞啦!把 code 切成一塊塊的 tokens。
Syntatic Analysis:將上一個步驟的 tokens 轉成 AST
Lisp 的語法相當簡單,而且我們沒有要實作所有的語法 XD
目前看到的就是分成三種:Letters、Numbers,跟 Paranthesis。
會將多餘的空白忽略,因為空白的區隔是為了讓開發者好讀
就以這三個去分,先寫出三個 regular expression 來:
|
|
接著要 iterate 輸入的 raw code:
|
|
首先第一個就是先略過空白
|
|
再來是括號
|
|
最後兩個有點像,分別是 NUMBERS
和 LETTERS
:
|
|
以數字為例,只要碰到了第一個數字,
就會接著把剩下遇到連續的數字一起推進去。
所以這一階段我們會得到一個 tokens:
|
|
最後,如果沒有對應的 type,
會丟出一個 Type Error:
|
|
完整的 lexer
在這裡可以看到
為了方便寫成文章,所以做了一些改寫 XD
順序也有所調動。
畢竟原本是在半小時內要講完的事情,
原作者 James Kyle 也樂見有更多人對 Compiler 有興趣,
詳細情形見最下方 License,請不要擔心。
這一階段的任務就是把 tokens 轉成 AST。
這裡應該是最容易卡關的部分 XD,
不過讓我們慢慢來,並感受一下為什麼要這樣做。
這裡會運用到遞迴,減少各種迴圈,大大減低了 code 的數量,
也提高了可讀性,而且看起來還很帥。
有句話說得好:
「嫩嫩迴圈,大大遞迴。」
只要能遞迴,就一定要遞迴一下。
來看一下 parser 的結構是怎麼樣:
|
|
可以看到我們這裡還是會移動 current 來遍歷每個 token,
只是改成呼叫 walk
函數,利用 JavaScript closure 的特性,
呼叫並且更改 current。
如果對 closure 和 funcitonal programming with js有興趣,
可以參考一下這篇文章:
The Two Pillars of JavaScript — Pt 2: Functional Programming
來看看 walk
:
首先當然是先拿到 token:
|
|
先看一下比較單純的遇到數字該怎麼辦(以下幾個 type 的確認都在 walk 裡完成):
|
|
再來是如果遇到 (
該做什麼事,
直接看一整段太長了,所以我將它拆成兩半:
|
|
往後移一個,拿到下一個 token ,按照 lisp 的語法,
這裡會是一個 expression 的名字(可以想成 function name)。
建立一個 node object,params 裡面放的就是這個 expression 吃的參數。
再來繼續往下看下個 token,
這裡會比較困難一點點:
|
|
假如不是 )
的話,就會繼續往下走,
因為expression 中可能還是會有 expression:
|
|
所以假如遇到 (
,就會在執行一次 walk,
前面已經知道 walk 的功用就是解析一個expression,
要解析一個 expression 中的 expression 的方法,
那就在 walk
裡面再 call 一次 walk
就好了,
我想可以人體 compile 一下上面的那行 lisp,
會更理解這個概念。
最後在 walk 函數的最後,一樣加上 type error 的 handling
|
|
實作完 walk
函數以後,要建出 AST 就簡單多了
|
|
語法盡量寫的非常淺顯易懂,除了講解容易之外
在很難設中斷點的 js 裡,寫太聰明的 code 只是在搞自己而已。
這裡一樣附上完整的原碼: parser
這一階段的目標是要將 AST 轉成專為生成 JavaScript 而生的 nextAST
,
我認為稍微抽象一點的應該就是 Traverser 的部份,
不過如果你有遍歷各種樹的概念,那以下應該會是非常簡單的事情。
這裡要分成兩個函數來實作:
Traverser:去遍歷我們前面造出來的 AST,並執行我們想要執行在每個節點上的 function
Transformer:利用前面做出來的 Traverser 轉化成專為 JavaScript 而生的 nextAst
為了更清楚知道我們在做什麼,先看一下 transform 的結果:
|
|
為了要遍歷我們的 AST,
我們要先寫一個 helper function 來 traverse 每一個 token 的節點。
以下一樣把 traverser 分成兩部分來看,
現在先只要專注在最上方的 traverseArray
就行了:
traverseArray會做的事情就是對每個子節點執行 traverseNode。
|
|
traverser 裡面的
visitor
,面放著我們「拜訪」每個節點時要執行的方法,
Transform 的工作就是由 visitor 完成的,這裡先不要急,
到
transform
這個函數時就會看到 visitor 是如何作用的。
首先我們根據子節點的 type 呼叫對應執行的 method,
找到的話執行它,待會一再對子節點要執行的就是這一部份:
|
|
接著再根據子節點的 type,去執行 traverseArray
,
Program 的子節點是 body
,
CallExpression 的事 params
,
而單純的 NumberLiteral 則沒有子節點需要被遍歷。
|
|
再來則是重頭戲: transformer
,
transformer
是個相當 powerful 的概念,
至少在麥考貝拍爛它之前都是。
將我們一路 parse 過來的東西,轉成跟目標語言非常相近的 AST。
首先先造出一個新的 nextAst
:
|
|
再來這裡是有點 tricky 的部份,
我們對 ast 底下增加了一個隱藏的屬性:_context
,
下面我們對子節點的操作也會常常用到這個非常 naive 的方法。
其實只是在名字前面加上底線,並不是真正的隱藏
再來則是前面有提的的 visitor
,
這裡就能夠看出為什麼選擇 Lisp 了,
語法非常的簡單且直觀,
但 transformer
仍然是一個相較之下較為複雜的函數:
|
|
NumberLiteral
這個 method,做的事情並不難,
只是 push 一個節點到父節點的 _context
中而已。
假如今天我們的程式什麼都沒有,只有一個單純的數字:
|
|
那 transfomer 會造出來的nextAst
就是這樣
|
|
跟前面的 ast 幾乎是沒有差的。
接著看如果遇到 function call 時要怎麼做,
為了簡潔我省略了其他部分的 code。
我們同樣先造出一個 expression 的 object,
包含一個 callee 屬性。
callee 就是被呼叫的 function
(add 2 3)
中,被呼叫的就是add
這個 expression
再來同樣在這個節點建立一個 _context
,
並將其指到我們剛剛剛創造的 expression
,
以下是 CallExpression
這個 method 的 上半部:
|
|
再來則是跟 JavaScript 比較相關的部份,
因為 JavaScript 最上層的 Call Expression 其實是 statement,
所以在確定該個 expression 的父節點 type 不是 CallExpression
時,
要再多加一層 ExpressionStatement
。
Statement 就是敘述句,像是
var i = 0
;Expression 則是會產生值的,像是
yo()
。但你知道的,有些 Statement 的地方我們仍然可以產生值,
因此也就有了
Expression Statement
的存在。如此概括是有點草率,不過這裡還是將重點放在我們的 Compiler 上。
關於 Expression 和 Statement 在下方補充資料有放上一篇我覺得既短小又很不錯的文章!
|
|
於是我們就完成了 transformer
了!
下一階段就是根據這個 nextAst
來生成 code 了。
一樣附上 transformer
的 code
終於到最後啦!
其實有了前一段專為 JavaScript 生成的 nextAst
之後,
要生成 JavaScript 真的是毫不費力。
直接來看 codeGenerator
這個函數,
Program
: 用換行來區分各個小 Program。
Expression
:再來是在每個 Expression 後面加上 ;
。
CallExpression
:
callee
就是被呼叫的函數,arguments
則會被逗號分開來,如果 argument 是 expression 的話會繼續遞迴的呼叫 codeGenerator
Identifier
: expression (函數)的的名稱。
NumberLiteral
: 毫無反應,就是個數字。
最後則是不包含以上 type 的 node,就會丟出 type error。真的是非常 robust。
|
|
將以上的 function 組合起來,
就是一個 compiler 了:
|
|
很多時候我們都會覺得有好用的工具,
幹麻要自己造輪子呢?
但在造輪子的過程中,我們獲得的往往更多,
畢竟盲目的 call api 和使用 library 並不能體現一個軟體工作者的價值,
懂得何時該使用,甚至創造工具才是我們的天職所在。
能掌握更多知識,就能設計出更有創意的東西,
因為我們更了解所謂的「極限」在哪裡。
雖然只是一個「簡單的」from lisp to js compiler,
但相較於過度困難的屠龍本而言,
我想這是一個能相對友善了解 compiler 的起點。
可以到我的 repo 去看:Super tiny compiler practice,也可以看下方原作的XD 有些微妙的不同
The Two Pillars of JavaScript — Pt 2: Functional Programming
但這次把 travis CI 加進來以後,改部落格變得方便多了!
這樣就沒有理由阻止自己偷懶不寫文章了吧 XD
跟蠻多 static page generators 一樣,
可以跟 github page 做很好的搭配,
只是這一次,把 travis CI 也整進來了。
原因很簡單,個人的部落格,內容是公開的,
而 travis CI 對於 open source 的專案則是永久免費的。
實在沒有不用的理由 XD
不過部署的時候,會需要有寫入 repository 的權限,
我參考了以下這篇文章:
寫的很清楚 XD
我自己是另外 gen 了一個 public key 跟 private key,
所以會需要注意一下命名,
卡了半小時在那邊處理字打錯的白痴錯誤。
基本設定這樣就足以,接下來再來把 ga 和 留言功能加進來了。
]]>我們會希望寫出來的 code 能夠做成被複用的 Component,
不過首先要來拆解一下越來越肥大的 main function。
而 main 就可以被拆成 Model、View 、Intent。
先看一下上次 BMI example 的 main function
|
|
這麼大一包看起來絕對不是好事。
所以我們會把 main 分成三塊,
分別是 Model, Intent, View
Intent: to listen to the user
Model: to process information
View: to output back to the user
第一塊是「Intent」,
簡單說就是 User 想對 UI 做什麼事情的 Intent,
在這裡當然就是指雙方互動的部分:
|
|
|
|
model 則是處理資料流的部分:
|
|
這裏則是依照 Model 中的資料去建 Virtual DOM tree
我們不會把最後要 return 給 Driver 的東西也放在這
僅放跟 UI 生成相關的而已
|
|
然後我們的 main 變得簡潔許多,
看起來只是 function 組合起來而已:
|
|
那我們該如何減少重複的 Code 呢?
當 UI 的操作越變越複雜以後,
我們不會希望所有事情都能在一個 main 裡面解決,
這時候我們可以把重複的部分抽出來變成 component。
egghead 課程裡面有更精簡的怎麼把 main 提煉成 component 的過程,
不過核心精神蠻簡單的,就是 props 也是 stream。
因為 props 是會跟著傳下來的「資料」,
所以很自然的我們就會選擇處理資料的 model 下手。
而 model 收到的 sources 是從 Drivers 來的,
第一步就是先更動 drivers
|
|
再來就是把 props 傳進去:
|
|
記住: props 也是 Observable
|
|
initial value 的 stream concat 新進來 value 的 stream,
取代原本的 startWith
下一步就是把 label 的名字和單位給 return 出來,
變成一條 UI component 可以吃到的 state stream,
再把對應的值塞進 view 裡面,就能得到我們想要的 vtree$ 了。
我們現在每個 component 中都會有個 main function,
事實上我們能把 main 改成這個 component 的名字,
並且在更上層的 main 中去使用它,
因為事實上他就是一個 function,在 functional programming 中,
“composable” 可以說是最重要的概念之一。
|
|
而事實上,我們可以把 props 這件事移到 main 中去做
|
|
如果只有ㄧ個 component 的話,那 cycle.js 也太慘,
我們當然是可以組合多個 components,
只是該怎麼做呢?
很簡單,先把 sinks 個別抽出來:
|
|
這裏會發現一個問題,就是當我們移動其中一個 slider 時,
另一個也會被影響 ,使用者的互動 => intent
因為兩個的 class 都是 slider,
而 intent 中監聽的又是 “.slider” 底下的 input。
其實我們在 LabelSlider 裡就可以讓兩條 stream 分流,
因為我們傳進去的 sources.DOM
,是可以只要選取 weight 或 height 就好:
|
|
這裏做的事情就等於在 intent 裡面這樣:
|
|
我們 pre-select 了在 DOM 上面 class name 為 ‘.weight’的 stream。
要隔離開每個 Component 如果都像上面那樣做應該會瘋掉,
所以 Cyclejs 其實提供給我們一個 helper function: isolate
使用方法是傳入一個 Component function 當作 argument
再來會回傳一個 scoped 的 component function,
同樣吃 sources 進去,吐 sinks 出來
isolate(dataflowComponent, scope)
:第二個參數是 optional 的,如同看到的一樣
可能會有人覺得沒什麼差別,但如果單純使用
isolate(dataflowComponent)
,那會是一個不純的 function ,因為每次呼叫都會 return 一個不一樣的 scoped component function
但如果我們指定了 scope,那每次回來的就是同一個 scope 下的 component function
真正的濃醇香!
|
|
如此一來又減少了一些 boiler plate
目前缺的就是把 bmi 給算出來了,
首先我們知道這個運算會放在 main 裡面,
因為這就是這個簡單小 App 的主要邏輯。
|
|
現在問題來了:我們要怎樣得到 weightValue$ 以及 heightValue$ 呢?
從 sources 拿啊!
概念很簡單,我們從 main 中拿到的 source,
其實就是從前一層 component 中吐出來的 sinks,
所以我們自然從前一層 component 中回傳的 sinks 下手:
|
|
實作起來也是這麼簡單。
最後我們回到 main 中,
把 bmi$ 也加進去就成啦!
|
|
總計 21 回的課程算不上太長,很推薦有興趣的人去把它看完,
儘管實際上要弄懂 Cycle.js 的概念的確需要花點時間,
但學習 FRP 是值得的,畢竟我們就是在處理 dataflow + UI,
再加上 pure function 好測試、composable 的特性,
不由得感慨 Rx 寫起來真是爽。
相較於 React,Cycle.js 當然更接近 functinoal programming,
不論這個東西將來會不會用到產品上,
純函數式的東西總會莫名的吸引我。
如果要追求 fp,更應該要感受一下 elm
這一堂課的影片幾乎都在 jsfiddle 上完成,
(不曉得作者為啥要這樣XD)
我中間練習的程式碼有放在 github 上面,
筆記等年假再來好好整理一番。
還沒看過上一篇的可以先去看上一篇了解 Cycle.js,
這一篇會從 driver 開始講。
drivers 是在控制畫面的 render,
但是我們目前的 driver 都是只能回傳字串,
這一章節我們要真的來認真的操作 DOM,
並且實作幾個小例子來看看 Cycle.js 這個框架是怎樣改變我們思考資料流的方式。
這裏要來認真處理一下如何去從 object 去表示一個 DOM,
假如你之前實作過一個 Virtual DOM 的話,
我想會相當有幫助。
|
|
目前還只是沒加上 props 的簡化版。
|
|
這裏使用了 appendChild,所以如果不每次都清空的話,
等於每次都會 append 東西上來。
回頭看一下我們的 Main,
發現我們唯一能從 DOM 拿到的 event stream,
居然只有 click$,這並不符合我們日常的開發情境,
現在就來解決這個問題。
解法很簡單,就是在 return DOMSource 的時候,
給個能夠選取 tag 和 event type 的 interface。
|
|
這裏當然還是不夠 general 的版本,
不過這樣我們在 main function 裡面就能夠簡單的選取另一個 event 了。
一開始我也很疑惑 h 是啥?
答案很簡單, “h” stands for html
|
|
讓我們在 main 中要建造 elements 時省去不少力氣。
而 h1、h2、span⋯⋯等等你想得到的 tag,
都能藉由 function 來表示,
並且語法看起來也很簡單,
連我到後來都不禁思考:「我們真的需要 jsx 嗎?」
目前只是比較簡單的語法,還沒考慮到 properties,
在 main 中的長相大概會像這樣:
|
|
處理完語法後,我們來看看怎樣寫出一個更 serious 一點的 driver。
第一個發現的問題就是我們又把整個 Component 要 mount 的地方寫死了,
|
|
這樣的寫法讓我們必須要在 DOM 上一定要有 id 為 app 的 element,
才能夠啟用 DOMDriver。
DOMDriver 是一個 function,
所以我們只要能回傳一個「客製化」的 function,
這件事情不就解決了嗎?
這裏運用到了 JavaScript 中「閉包(Closure)」的概念,
|
|
下一個問題則是:
|
|
假如要 bind 到 DOM 上面是一個很大的 object,
那我們會遭遇到效能的問題。
再來則是 selectEvents
這個 function:
|
|
它只能指定 tagName,
不能用更方便的 selector 來選取想要的 element,
我們應該要提供一個更聰明一點的 API 來做這件事情。
關於這兩個問題點該怎麼重構,
作者並沒有詳細說明,但我們可以直接去看 source code,
這也是我們要將 CycleDOM import 進來的時候。
小記一下,
假如我們繼續用舊有版本的 run,
那
selectEvents
會沒有被綁進去 source 裡面。蠻好玩的,可以想一想要怎麼解這一個問題。
接下來的正式引進 cycle-dom 中的 makeDOMDriver,
而原本的程式碼也要跟著做變動。
沒有意外的,首先需要更動的就是 selectEvents
|
|
這底下有一個 virtual dom來 handle 重繪,
不會像我們先前一樣,每次一有更動,
就重新 flush 整個畫面。
而 h1, h 也變得更加強大,可以試試看在第一個參數傳入物件,
可以自訂 attributes,以及調整 style。
啊!終於要開始 Hello world 了,
跟以往不一樣的是我們已經跑了一次的底下大概會發生什麼事情,
才跟世界說 hello。
|
|
現在我們用剛剛學到的 select跟 events 來處理一下 input 的 events。
注意到我們在 input function 那裏的第一個參數寫下 .filed
,
會自動變成帶有 field class 的 input 。
(準確一點來說應該是 return 一個 virtual dom 的 element)
長這樣:
|
|
再來則是把 input event 以及 值給拿出來:
|
|
再來要做的事情很直觀,
就是把 name$ 裏的值給 map 到 DOM 上面去……嗎?
|
|
實際上這樣的作法會讓畫面上什麼都沒有,
因為 name$ 是 inputEv$ map 過後的結果,
而一開始 inputEv$ 是空的,自然沒有任何東西會 return 啦!
但要解決這個問題也很簡單,只需要startWith
這個好用的 operator 即可。
|
|
Hello world 完成啦!
在開始之前得提醒一下,
跟 Redux 在開發之前得先想好 StateTree 的道理有點像,
在 Cycle 中,我們會體會到要怎樣設計一個 Stream 的流向,
而 UI 只要跟著這個 Flow 去變化就行了
(狀態顯示為 Reactive 狂粉)
來個經典的 Counter example 。
廢話不多說,
就先把頁面和 increment 以及 decrement 的 click stream 弄出來:
|
|
拿到 Stream 之後呢?
|
|
這裏並沒有得到我們想要的東西,
來看一下 merge stream 是怎樣運作的,
|
|
我們必須有個東西把 Stream 上所有的值給加總,
想到 array 的 reduce 了嗎?
其實 Rx 有提供一個 Operator 給我們做類似的操作:
它叫做 scan
。
|
|
Cycle 強迫我們在一開始就想好資料的流向,
以及事件的處理,如此我們在開發的時候能夠更深思熟慮一點,
不會讓整個 Project 變得很 crazy。
在簡單的 Counter 下這好處還不明顯,我目前也沒用 Cycle 寫過大型的產品,
所以且讓我們繼續看下去。
開發 web,我們當然會需要送 http request,
所以我們就需要 http driver。
這裏我們要從 github 的 api 來拿 users 資料。
一樣先把基本的頁面弄出來
|
|
我們想讓使用者點下 get_first 的按鈕後,
就拿到 user 的資料。
前面有提到什麼是 read effect 跟 write effect,
effect 會因應 logics 規則的變化,真正影響到外在世界。
實際講起來太抽象了,我們現在把這個 App 中會發生的 effect 以及分類列出來,
會清楚很多:
|
|
|
|
|
|
在處理 Stream 時,往 Collection 的方向想會舒服很多,
因為我們處理 Array 也是如此,
最後一篇我們將會來看看 Cycle.js 怎樣提高我們程式碼的複用性,
學習用另一種方式去思考該怎樣拆解每個 Component。
其實軟體開發裡面沒有銀彈,
不過這種爭辯也更能夠激發出我們寫出更棒的軟體,
並且去反思現行流行的東西真的是「好」的嗎?
Cycle.js 的作者也寫下一篇他認為為什麼 Redux + React 不那麼好的原因:
同時間在 egghead.io 上也 release 了一個 Cycle.js 的課程:
(等等,這時機推出課程,真的不是在打廣告嗎?)
我認為這個課程還蠻推薦的原因有底下兩點:
作者會告訴我們 Cycle.js 這樣設計的理念
對我來說在學習一個框架時,
如果你不能理解為什麼要這樣設計,
那你就是用硬背的,這樣很容易忘記;
但如果你知道為什麼要命名成這樣、為什麼要這樣設計,
你等於進入了框架本身去使用它,
而不是被它框住。
Observable 給我們不一樣的方式來思考如何 Handle events
可以看看 Netflix 的例子
週末在家拉肚子之餘,順便把課程課完並做了一些筆記。
先來看一下 Cycle.js 的 Get started code,
|
|
現在看起來很不習慣,但這篇會從無到有的建一個簡單版的 Cycle.js 出來,
第一篇預計會實作很 primitve 的 drivers 以及 main,
接著會把 run 給重構到幾乎跟現在 Cycle 核心中的寫法一樣。
(當然還是只重概念說明的簡化版)
不過這都只是個人的學習筆記,
還是在大大推一下 egghead.io 上的課程
了解如何操作 collection
沒錯,Observable 和 array(或list)都是 collection
可以試試這個互動的課程,再來看這系列會更有感覺:
http://reactivex.io/learnrx/
對於 Rx 已經有基礎的認識
要來分清楚這兩個東西是什麼就要先來看一下程式碼了:
|
|
這是一個從 0 開始每一秒一數的計數器,
詳情請見 Timer。
上半部的部分是「Logic」,
而 subscribe 那一行開始,就是他怎樣呈現的 「Effect」。
這裏有個很巧妙的概念,
就是 Effect 才是真正影響到外面世界的地方(DOM),
正如他的名字一樣;
而 Logic 裏的東西只是單純的 Event stream,
我們不去 Subscribe 他們,就不會有任何事情發生。
Cycle.js 的原則就是將這兩大部分分開,
Effect
的部分是 Imperative 的,讓 Framework 幫你完成,
身為開發者我們只要關心 Logic
的部分就夠了,
而 Logic 的部分是 functional 的。
前面提到我們會將 logic 和 effect 分開,
在 Cycle 中我們習慣會將 Logic 放到 main 裡面。
|
|
我們在 Main 裡面建了兩條不同的 stream,
看起來已經將邏輯集中起來放,
但是最下方從 sink 開始,
似乎還是太 imperative 地去做這些事情。
我們 Hard Coding 的去指定 consoleLogEffect 這個函數,
一旦我們今天把 main 中的 log 拔掉,
那整個程式就會報錯了,
Cycle.js 中不希望我們每次更動 Logic 時需要注意一大堆 effect
再來就要介紹一下 run
這個 function。
run
|
|
run
會吃兩個參數,第一個就是我們管邏輯的 main,
第二個則是 effect,
我們如果不想要他在畫面上做事情,
把在 effectFunctions 中的那個 key 給註解掉就行了,
因為我們並沒有很 hard coding 的去呼叫每個 effectFunction。
但是這裡要重新命名一下,將 effectFunctions 改成 drivers,
一來是因為 effectFunctions 聽起來並不是個好命名方式XD
二來是 drivers 即是我們熟悉的驅動程式,建立了硬體和軟體中間溝通的介面;
而這裡的 driver 可以想成我們的程式(logic),和畫面(effect)中間溝通的介面;
還是很抽象嗎?
那就從字面上的意思來看, driver 就是駕駛員,
現在有一個駕駛員負責開著一台小車車,
嘟嘟嘟的把我們寫的邏輯運送到畫面上,
我們只要寫好邏輯、還有要送去的地方跟方式,
剩下的就交給 driver 幫我們處理啦!
|
|
這是我們手刻出來的簡單版本,
而 Cycle.js 首頁的 get started 例子中,
輪廓的確就是這樣子,
只是在 driver 的部分,
Cycle.js 幫我們做了更多事情。
|
|
前面有提過 Netflix 解決複雜電影選單的方式,
就是透過 Observable 來重新思考處理 Events 的方式,
但到目前為止,我們都還沒有用到最精髓的部分,
而是把內部 Logic 寫好,沒有接收任何外來的 event stream。
奠基於 Rx 上面的 Cycle.js 最精華的也正是這一段處理 event 的方式,
同時這也是 Cycle 這名字的由來。
首先先看前面寫的程式碼:
|
|
可以看到它只有 input,沒有 output。
而 main function 則反之,
我們想從外部 read something ,就代表我們的 main 必須要有 input。
這裏的前提是你照著 cycle.js 的單向資料流架構走
所以我們先在 main 和 driver 各加上 input 和 output。
接下來在 run
中會改回 hard code 的方式,
這是為了更容易去理解,接著就會遇到最奇妙的地方:
|
|
我們看到 run 中間,DOMSource 需要 sinks 才能建立,
但 sinks 也需要 DOMSource 才能被建立,
形成一個很微妙的循環,是一個雞生蛋蛋生雞的問題。
在更抽象化一點就是:
|
|
想要解決這件事其實沒那麼難,
想法上是這樣:
|
|
這裏要靠 rx 裡面的 subject 來建立我們的 proxy。
瞭解更多關於 Subject:
from rx-book
簡言之它同時繼承了 Observable 跟 Observer,
所以我們既可以 subscribe 它,(Observable)
又能夠對他呼叫 onNext、onError,以及 onCompleted(這就是 Observer 在做的事情)
這裏就比較困難要分段看了,
先到 run 裡面看看我們要怎麼按照上方的 pattern 來加入 proxy。
|
|
DOMDriver 回傳了一個 click-event 的 stream(Observable),
所以我們 subscribe 它,並且每一次呼叫 click 的 stream,
跟我們前面創造的 proxy 整合在一起,
下來再來看 proxy 傳進 main 發生了什麼事情。
|
|
簡單說就是我們每次在螢幕上按一下(click),
就會重啟整個 timer。
歸功於 flapMapLatest 這個 operator,
假如這裡改用 flapMap 的話,會發現舊的 stream 還在繼續跑,
整個 timer 會被搗亂,假如還不熟 flatMap 該怎麼用
請至 prerequisite 玩一下 learn-rx
而 startWith(null)
則是製造一次「假的」 event,
來觸發第一次還沒 click 之前的 effect。
現在的 code 看起來很糟糕,尤其是在 main 中 hard code DOMSource 這一點。
首先先從 run 中下手:
|
|
如此一來我們就不用去 hard code 的指定每個 proxySource,
而在 main 中簡單多了,只要把 click$ 的來源變成 sources.DOM 就好了,
但在這裡我們可能會對一個 undefined 呼叫 subscribe。
consoleLogDriver 並沒有 return 任何東西(nothing to be read)
要避免這點只要加個判斷式就能夠解決,
不過截至目前為止,我們其實已經把 Cycle core 中的 run 給實作的差不多了!
當然還是有些差異在,像是 error-handling,
以及在 Cycle core 的 proxy 中是用
ReplaySubject
而不是Subject
Hi 大家,先簡介一下自己背景:
其實我覺得第一點不是很重要、我也很討厭強調,
但偏偏就是很常被問,
要不然就是別人聽到你大學是XX系就會:「蛤?」
索性就把它列出來了。
興趣使然的前端工程師,擅長一鍵跑版
沒上過資策會、巨匠,或任何各種職訓班,但買過 tree house 的課程和幾本書
學習時間:一年半(從碼盲到現在終於可以改一些 code)
稍微熟一點的技能: JavaScript, CSS, html, React.js
預計未來要學的東西:Haskell, golang, Angular(2.0), Rx.js
這是之前相關的專訪,但我一直都想自己寫一篇,也不是 William 寫得不好,但總覺得哪裡不對勁,也許自幹就是一種工程師的浪漫吧!
後來想想,也許是整篇文章太強調「本科」影響不大
事實上我想說的是:「非本科不該是阻擋你寫程式的理由。」
總之這篇就是來介紹一下我是怎麼慢慢上手這個職業,中間會提到一些我覺得很棒的學習資源,和吸收新知的方式。
說到吸收新知,目前首推碼天狗,它讓我禮拜一早上都會很焦慮的重新整理,大家可以感受一下。
這是我簡介自己做過 projects 的slide
會放這個是因為我本來要買月費專案($7/month),卻手滑買到年費專案($70/year),gan,只好多多利用它了。
其實我本來立志成為一個 Data Scientist ,
只是不小心被擺到前端的位置上去……
回顧這一年半的旅程,前端的東西真的太多太雜了,
更容易完全只知其然而不知其所以然的就開始用某個新框架、library,
所以對我來說,「學什麼」是副課題,「不學什麼」才是真正的關鍵。
因為我前端工程師的路還沒走完,
所以應該在我退休或換職業(去賣雞排)之前,
都會繼續寫下去。
目前寫完三點:
非本科系 v.s 本科系
從哪裡開始學習?
前端工程師該懂後端嗎?
就來說說「本科系」來到底有沒有差。
首先,我們都知道學校裡的課程,
很少是真的專注在所謂「前端工程」上;
這是可以理解的,一來因為前端變化太快,
學期初才在說好棒棒的東西,
到了學期末可能就變 deprecated了。
所以這就代表非本科系跟本科系的人站在相同的學習立足點上了嗎?
No,你得面對現實,本科生就是有他的優勢在。
這裏要講個實習的故事。
我第一間去實習的新創公司,應徵的是行銷,
CEO 是個自己學習 JavaScript 並且把產品做出來的人,
更重要的是他是個很願意教的人,
在我表示我想朝這方向前進的意願時,
他很大方的說:「如果你對 JavaScript 有興趣可以教你。」
當時還有另一位是資管系的同學也一起,
第一次的作業是用 Angular 做表單的驗證,
怎麼讓使用者不能繼續輸入資料呢?
(當時的我連 JavaScript、html 都不會寫)
我的做法是非常土炮的將 input 換成 div 然後加上紅色的邊框,
另一位實習生則是使用了 disabled 這個 property,就搞定了。
講起來也沒什麼了不起的技巧,但不知道就是不知道。
我問他怎麼會知道有 disabled 這個特性,
他的回答也很簡單:「查文件啊!」
也是這次教訓,我知道要先查文件。
講起來蠻白癡,
不過會上 stackoverflow 和 google 找答案和看官方文件,
都是最基本的能力。
為什麼他會知道?
很簡單,因為平常他們在寫作業或作專題就需要這個能力。
既然我們遇到不會的字會查字典,
那為什麼我們寫軟體遇到問題時,不需要讀 doc 呢?
而對於整個電腦的理解,非本科系的人絕對也是被甩在幾條街之外,
因為我們不需要修資料結構、演算法,
更別說對於資料庫,
作業系統、計算機結構、計算機組織、編譯器理解的淺薄,
一定要掌握上述這些知識才能寫前端嗎?
這是一個很大的疑問;
但一個了解底下發生什麼事情的人,才會更知道極限在哪裏,
這個絕對是肯定的。
有時候你寫程式時會卡在一個小小的點,想出來之後覺得沒什麼,
而本科系的人能從以前上述課程中的經驗去延伸,
(不管是演算法或是系統相關的事情)
比你更快速得到答案。
畢竟,人家花了那麼多時間了解電腦,
你如果不是天縱英才,要比他們理解電腦就得更努力跟上才行。
這裏推薦一個很棒的課程,nand2tetris,
上面有很詳細的指示,如果你需要影片和評分系統的話,
coursera 上也有開課了:https://www.coursera.org/course/nand2tetris1。
這門課會從最基本的 nand(not and) 邏輯閘開始講起,
用模擬器組合出自己的 CPU、記憶體,定義自己的組合語言,
用習慣的程式語言寫出組譯器,
再寫出一個超簡易版的 JVM,最後用一個簡化過後的 Java 語言(真的超簡化),
寫出一個俄羅斯方塊來。
整台電腦、軟體,都是由你一手寫出來的,不覺得很熱血嗎?
而且你終於也能夠看懂這張圖的笑點在哪了:
當然,如果你在學習途中發現你對系統的東西很有興趣,
那也恭喜你發現新天地啦!
想當初為了所謂堅實的基礎,還跑去圖書館借白算盤來啃,
那又是另一個故事了。
總結一下這一段,
前端工程師也是軟體工程師,
對電腦一無所知的人寫出來的軟體,你敢用嗎?
我認為至少玩過一輪 nand2tetris 對於非本科系的人會相當有幫助,
本科系的人來寫前端確實是有一點優勢在,
但這不是認輸的藉口,
而是你必須比別人更努力找方法變強的原因。
另外,
千萬不要以為念研究所的人是只會讀書的書呆子,
比你聰明、比你努力,又比你勇敢的人永遠都多的是。
先來說說「單純」的前端從哪裡開始,
主要分成兩塊:
第一塊是 html 和 CSS:
我以前學習 html 和 CSS 的方法就是把 w3schools看完,
不能說有什麼不好,不過真的是看完大部分都忘記,
畢竟很多東西都馬是要用到的時候再去查。
但現在我會推薦 codecademy,
邊寫點東西邊學絕對是很有效的學習方式。
而學會基礎後,
要怎麼設計出好維護又乾淨的 html and CSS 那又是另一個很長的故事。
第二塊則是 JavaScript:
坦白說一年半過去,我仍然認為自己在 JavaScript 的知識上很貧瘠。
這裏有篇 10 個面試時應該要知道的問題,
可以探一下自己到底對 JavaScript 理解多少。
這裏如果把教學全部列出來,真的是完全列不完,
但學習的流程是這樣子:
掌握了基礎的語法和原則
實作練習
回頭研究基礎再繼續實作
重複以上循環不斷的把你的武器磨的更亮
至於掌握基礎的語法,你可以到以下任一網站,
挑一個你喜歡的,上完基礎 JavaScript 課程:
練習一段時間後,你會發現又有好多新工具冒出來了,
這時候你可以先辦個 github 帳號,
首先 watch awesome 這個 repo: https://github.com/sindresorhus/awesome ,
看一下你喜歡的領域有沒有什麼好東西,
再挑幾個你最有興趣的 repo 按下 watch,
最後再開始訂閱各大框架或社群的 weekly,
接著就是準備被源源不絕的資訊轟炸、不斷的學習和升級。
而值得一提的是, JavaScript 有很多工具可以用,
不管是 library 還是 framework,
學習之前,真的必須想一想:
「你真的需要用它嗎?」
舉例來說:
React 的確相當的好用,
但是你的畫面真的有那麼多 state 要處理嗎?
有些人簡單的認為 SPA(Single Page Application)就要用 React,
我得說不一定,假如根本沒有那麼複雜,
也許你只是需要一個 template engine 而已,
而把 React 當作 html 的 template 來用,
實在是有點太小看它了。
什麼時候該用 React 或是 React 到底好在哪裏,
這個議題其實已經超出了本篇文章的範圍,
有興趣的可以看這篇:React Components, Elements, and Instances by Dan Abramov(Redux 作者)
這也是為什麼我一直遲遲沒有碰 Angular 的原因,
(因為我還沒遇過複雜到需要用到它的情境)
但我認為在選擇前端的框架時,這篇文章很值得一看再看:
裡面並沒有太多的程式碼,只有比較 high level 的概念,
但看完你會比較理解別人說 MVC、MVP、MVVM、Model 2 是在說些什麼,
前端主要工作之一就是處理使用者介面(UI),
我認為理解這些模式是一個前端工程師必備的 common sense,
這些概念比起淘汰迅速的工具們,是比較能夠保值的,
並且也會漸漸影響你挑選工具的眼光。
而 medium 上也有許多好文章可以看,
twitter 上面也有很多大神可以讓你追蹤,
不要把這些事情當作是在大拜拜,
覺得追蹤越多人自己越屌,
重要的是你看他們生產的內容時得到了什麼。
另外臉書上的前端社團也很值得加入,台灣人的軟體能力是很強悍的:
重要的是在上面發問,也會有人很熱心的回答你。
假如這樣都還是讓你資訊焦慮,可以開始訂閱一些技術週刊,
像是碼天狗、TechBridge,
讓 curators 來幫你整理一些技術上的新知。
已經盡量精簡了資訊來源,希望能讓新手們不要太無所適從。
後端跟前端是完全不一樣的專業,
有人說 Node.js 能讓前端工程師跨足到後端去。
(Isomorphic?)
事實上前端工程師想往後端走還是有許多需要學習的,
不管是資料庫或是系統面,都不是平常前端會碰觸到的領域,
認為自己會寫 JavaScript 就硬上的下場通常是:
效能有問題
資安有問題
整個 server-side 的 code 都他媽很有問題
聽起來是很糟糕的事情,所以請千萬尊重專業,
讓我們前端歸前端、政治歸政治(欸?)。
那前端到底要理解後端到怎樣的程度呢?
這是一個很 tricky 的問題,
大部份人會說:「至少要會接資料啦!」
但要學到會接資料揪竟是需要怎樣的能力呢?
真的有人學到剛剛好就喊停的嗎?
最好的方法其實就是自己去玩一套網頁框架,
後端前端都寫一遍。
Rails, Laravel, Django 都是我認為不錯的選擇,
(Koa 也很不錯啦……)
重點是去感受一下自己要怎樣設計 DB 的 Schema,
怎樣做正規化、怎樣避免 N+1 Query,
以及整個框架的架構為什麼要這樣設計,
最後再跟自己拉的頁面整合在一起,然後部署上去,
(用 heroku 是有點偷懶,不過如果你對 server 真的沒興趣,還是可以考慮這樣做沒差)
等做到這一步,「至少要會接資料」這一點,
早就迎刃而解了。
對了,
記得也不要因為自己寫過後端的 code 就說自己是 full-stack,
這就跟你會收發 email 就說自己懂電腦一樣會被笑。
(IT crowd 真的是個不錯的影集)
有興趣可以看看這篇:
可以略懂 Async 在 server 端和 client 端的差異。
目前大概走到這裡,還有很多沒說到,
但學個基礎開始實作後就能體會到許多了。
至於實作,
可以選擇自己寫個身體健康、參與 open source,
或是去實習都是非常好的選擇
不管是 RWD、mobile web、跨瀏覽器的處理、SEO,
動畫該用 CSS3 或是 JavaScript 還是 SVG?
每天都有新的問題可以鑽研,
目前就先寫到這裡啦!
希望可以改變一些覺得前端工程師只是在切切版的想法,
也希望能幫助到想往前端工程師邁進的人。
]]>這兩個東西有一點很一致:
不管我們再怎麼討厭它,
都還是得面對它、處理它。
如果討厭寫 CSS,就更應該用這種方式來寫
我們應該用 module 化的方式來思考每個畫面上的東西
讓需要「工人智慧」的地方減到最少
「如果你覺得 CSS 很亂的話,那代表你心中沒有架構。」
會使用 webpack(幾乎只要會改 config 就行了)
把 CSS 當一回事的人
曾幾何時,我也覺得 CSS 是一個他媽有夠亂七八糟的東西,
直到不小心開始寫前端,我才發現前端不只是 JavaScript,
從 CSS 到 html 的設計,都需要仔細去思考「架構」這件事,
否則很容易讓技術債債台高築,到最後一發不可收拾。
使用起來合邏輯的東西,不代表能夠用很「邏輯化」的方式寫出來,
這正是 CSS 為什麼很容易亂七八糟的原因,
因為我們常常需要去指定很多畫面上的細節(imperative):
「欸欸,你這邊 width 要 300px,然後 margin 要設成 0 auto 才能置中」
而不是直觀的用程式碼來宣告我們想要畫面長怎樣(declarative):
「我們要一個看起來不錯的畫面」
處理太多細節很容易出錯,像是螢幕或視窗大小不一樣 300px 就不一定 ok 了,
而第二個 declarative way 似乎又太過理想化。
而我認為折衷的方式就是 module 化 CSS,
雖然也需要去實作 module 內的細節(imperative),
但完成之後,就可以將這些 module 組裝起來,
重複使用時就不需要去實做那麼多的細節,
沒錯,我們又往 declarative programming更進一步了。
現在看起來還是比較 high level 的概念,
但我認為知道為什麼要這樣做很重要,
稍後會在例子裏看到這樣做的好處是什麼。
在開始之前先講解一下兩個會推薦使用的工具,
(你也可以依自己喜歡的配置啦!)
分別是 Autoprefixer 以及 PostCSS。
假如熟悉 postcss 和 autoprefixer 在幹嘛的人可以直接跳下一段了。
其實我們平常在寫 CSS 的時候,為了處理跨瀏覽器的問題,
常常需要寫很噁心的 prefix,
就算有 SASS 的 include 語法,prefix 還是很噁心。
看到 autoprefixer 出現真是讓人痛哭流涕的一件事,
因為這代表以後有人會幫我們處理好 prefix,
同時還會把太舊的 prefix 給移除掉。(像是 border-radius
)
這裏就直接來安裝進專案吧!
webpack.config.js
|
|
唯一需要說明一下的就是可以指定我們要 support 到多老舊的 browser啦!
就這樣,恭喜你!
PostCSS 是一個可以用 JavaScript plugins 將 style 轉成我們想要樣子的工具。
(包括 lint, variables, mixins,以及好多東西……)
確切一點來說, PostCSS 是一個 node.js 的 package,
它可以將我們原本的 CSS 檔案轉成 AST(Abstraction Syntax Tree),
接著我們就可以藉由這個 API 來對 CSS 做事情,
做完後再將它轉成 String,輸出成我們想要的 CSS,
如果你懶得自己寫 plugin 來處理也不用擔心,
現在已經有兩百多個 plugins 在那裡等你愛智求真了。
我知道一定有人這時候在想:「那 SASS 呢?」
沒錯,這兩者看起來似乎有點像,不過可以先看一下這篇文章:
這裏則是值得一看的補充資料,其實官方的 readme 裏也都有寫:
簡言之,PostCSS 跟 SASS 或 LESS 最不一樣的點是:
「我們可以只採用我們想要的部分,並將其組裝起來。」
這不就是 Compoasable 和模組化嗎?
接著就來看看如何在 webpack 中設定 postcss,
和使用各種 plugins。
(坦白說這裏才是最頭痛的部分)
使用 webpack 雖然簡單,但 config 的寫法太雜亂了,
完成同樣一件事可以有好幾種方法,
目前連官方文件上也沒有一個一致的 best practice。
而阮義峰的這篇教學是我目前看過寫的最清楚易懂的,
從 entry 到跟 react 一起使用都有說到。
假如你直接跳過前兩個工具,其實也是 ok 啦!
因為 webpack 的 css-loader 本身就內建 module 功能:
|
|
現在終於要來講一下 CSS modules 可以做到什麼事情。
我們能夠將 selector 組合在一起
|
|
這裏要注意的是 composes 必須寫在其他 properties 的前面。
而我們也可以 compose 多個 className:
composes: classNameA classNameB;
乍看之下跟 SASS 的 extend 有點像,
但讓我們繼續看下去。
假設我們現在有另一個檔案: style.css
|
|
|
|
這給了我們很大的彈性,但小心不要 override properties,
我覺得官方文件的這一句話寫得很棒:
Best if classes do a single thing and dependencies are hierarchic.
這的確是我們在設計 CSS module 時,要常存心中的一句話。
這裏主要是說要如何運用 preprocessor ,
因為我們有時候還是需要 global 的 class。
|
|
如果你是打從專案一開始就使用 css module ,
那恭喜你!
但「通常」現有的專案上都是用 SASS 來解決,
這裡就以我工作上的專案來做例子。
這裏要提一下我們後端用的是 Rails,
Rails 有個邪惡的好東西叫做 Asset Pipeline,
它會將靜態資源壓成一個檔案,減少 request 數。
自動幫你做這件事聽起來很美好,
但實際上因為 css 有 global scope 的問題,
所以要怎麼確保每一頁只 load 到自己要的 style 呢?
我的做法是每一頁會有一個專屬的 id,
而命名的方式就是以 controller 加上 action 的名稱來命名。
像是 posts_controller 的首頁,
我就會給它專屬的一支檔案posts_index.scss
|
|
這樣做的第一個好處很明顯,
就是每個頁面裡的樣式就只會影響 id 裡的 scope。
那說好的 module 呢?
這裏就要用到 SASS 的 extend
,
假設 posts 和 show 都有一模一樣的 header,
這時候我就會把 header 抽出來像下面這樣:
|
|
|
|
|
|
看起來挺方便,
而且 Rails 的 routing 通常都是 restful 的,
所以理論上這樣 CSS 的名字也有一定的規則可循,
不會找不到檔案在哪裡。
(就算有自動搜尋,也要知道下哪些關鍵字吧!)
但,
如果今天根據 user 的身份不同,
會 render 不一樣的頁面呢?
#posts_index_super_user
?
沒錯,問題又變得開始複雜起來,
原因就出在它仍然是 global scope,
而我試圖想從命名來解決這件事情,
我常常在想:「啊!如果 CSS 是 local scope該有多好?」
A CSS Module is a CSS file in which all class names and animation names are scoped locally by default.
天啊!這解決了根本上的問題!
假如能夠用 component-based 的方式來思考,
讓 react component 從 css module 之間有對應的 name 來讀取樣式,
那不就更棒了嗎?
以後的資料夾結構會長這樣子:
|
|
一個資料夾底下就放著 component.js, component.css,
本身就是一個 micro-service,
而我們要做的正是把這些 micro-service 給組裝起來變成一個頁面,
最後再把這些頁面組裝起來變成 Application,相當舒服。
不過要如何從現有的專案改寫呢?
這裏就拿這個小小的部落格來舉例,
因為我一開始是用自己寫的 generator 生成專案,
(小打一下廣告,
平常開發前端 component 就是在這個生成的專案上開發,
弄好 react 和 hmr 之後,其實蠻方便的。)
順帶一提,這是開始改寫前的樣子:
|
|
到最後 stylesheets 裡面只會剩下 global 的 css 檔案,
像是 base.css 或是 theme.css 。
首先第一步當然就是處理 global 的 css,
思考的方向很簡單,就是哪些東西是每一個頁面都用得到的呢?
所以我們把 body, a, h1~h5之類的東西先拔出來:
|
|
接著來處理我們的 Nav bar,
從這裡開始,就要進入 module 化的思考方式,
一開始的時候你可能會覺得,欸?幹嘛這樣做?
但越到後面你會發現一旦你習慣這樣思考,
很多原本難解的問題都會迎刃而解,
尤其是用組裝的方式來思考畫面的元件,
能讓多狀態的呈現變得更簡單,
也更能明白哪個部分該抽象化出來變成 base。
先來看看這個 Nav 的例子。
預計會在以下幾個步驟循序漸進地去思考如何去寫 CSS Modules:
讀一下舊有的 js, css
最外層的 global selector
沒有狀態改變的 local selector
有狀態改變的 local selector
|
|
可以看到我們的 toggle_icon 會隨著 show 的值而改變樣式,
至於怎樣改變?就來看看原先架構下的 CSS 怎麼寫。
|
|
如果你有寫過 react native 的話,
就能體會到 style object 的好處,
假如沒有,那現在這是好好來玩玩看的時候。
我們從最外層開始拆解。
(其實由內而外、由外而內各有好壞,但這可能又要寫另外一篇了)
最外層的當然就是原生的 nav tag,
這裏其實大可直接給他 global
|
|
往下看到 logo :
|
|
要怎麼 import 它呢?
首先別忘記在 webpack 的 config 裡開啟 css modules 的功能。
再來只要這樣:
|
|
style.logo
讀到的就會是 webpack 幫我們生成的唯一字串,
不用擔心會跟其他 class 重複,不相信的話 console.log 看一下,
而跟以往相同,webpack 也會自動去幫我們寫入 style 到 head 裡面,
對應到的 class name 就是剛剛生成的唯一字串。
原理大概是這樣子。
再來則是為什麼我仍然使用 SASS 的原因: extend
來看看 toggle_icon,他就是我們平常看到手機版的選單,
按了之後會變形。
先直接看它原本的 CSS 長怎樣:
|
|
我知道有一些 PostCSS 的插件可以解決,
但這篇的重點在於模組化 CSS 的思考,所以就暫時先擱著啦!)
因為那個 icon 有三個橫條,每個橫條的設定都差不多,
所以我寫了一個 icon_bar 來被 extend。
|
|
接著則是重頭戲,
對於畫面來說,這個 toggle_icon 會有兩個狀態,
也就是說我們會有兩個 class 來處理它,
但這兩個狀態又有許多共同點,怎麼辦呢?
答案很簡單:
抽出來當 base,讓兩個狀態的 class 去 composes 這個 base 就好啦!
|
|
這裏抽出來的就是兩方都不會變的 properties,
把 transition 放在 base 裏的好處就是能看到狀態之間的變化,
這樣能實現一些簡單的動畫。
接著就是把我們寫好的 base 組裝起來而已,
toggle_icon!附身合體!
|
|
狀態的改變每個人都有自己喜好的方式,可以自行調整:
|
|
而 component 中該如何對應呢?
|
|
沒錯,就是這麼簡單而已。
回頭看看重構後的 CSS,
你會發現我們已經不是昔日把所有東西都丟在越來越多層的 class 裡面,
而是變成扁平且一塊一塊的了,
如果要重構的話我們也能夠將重複的部分抽出來。
再來更棒的是除了 global 的地方,
我們不用再擔心全域命名污染的問題,
畢竟沒有 import 到的 class 就永遠不會發生作用啊!
如果有寫錯的地方或是建議,很歡迎留言告訴我。
我真的最討厭寫 CSS 了。