全棧式JavaScript
如今,在創(chuàng)建一個Web應(yīng)用的過程中,你需要做出許多架構(gòu)方面的決策。當(dāng)然,你會希望做的每一個決定都是正確的:你想要使用能夠快速開發(fā)的技術(shù),支持持續(xù)的迭代,最高的工作效率,迅速,健壯性強(qiáng)。你想要精益求精并且足夠敏捷。你希望你選擇的技術(shù)能夠在短期和長期上都讓你的項(xiàng)目取得成功。但這些技術(shù)都不是輕而易舉就能選出來的。
我的經(jīng)驗(yàn)告訴我,全棧式JavaScript符合了這所有的要求??赡苣阋呀?jīng)發(fā)現(xiàn)了些許端倪,又或許你已經(jīng)在考慮它的實(shí)用性,并且在和朋友討論爭論它的話題。但是你是否親自嘗試過呢?在這篇文章中,我會對于全棧式JavaScript給出一個比較全面的介紹,為什么它會是正確的選擇,它又是如何施展它的魔法的。
先給出一個概括預(yù)覽:
接下來我會一項(xiàng)一項(xiàng)地介紹這些組件。但是在這之前,我們簡短地回顧一下,我們是如何發(fā)展到現(xiàn)在的這個階段的。
我為什么選擇用JavaScript
從1998年開始,我就是一個Web開發(fā)者。當(dāng)時,我們使用Perl進(jìn) 行大多數(shù)的服務(wù)器端的開發(fā);但是從那時候開始,我們就在客戶端使用JavaScript。Web服務(wù)器端的技術(shù)已經(jīng)發(fā)生了翻天覆地的變化:我們被一波又一 波的技術(shù)潮流推著往前走,PHP,ASP,JSP,.NET,Ruby,Python,這里只列出了幾個例子。開發(fā)人員們開始意識到,在服務(wù)器端和客戶端 使用不同的語言使得事情變得復(fù)雜化。
在早期的PHP和ASP的時代,那個時候模板引擎還僅僅是個設(shè)想,開發(fā)人員們在HTML中嵌入他們的應(yīng)用代碼。我們經(jīng)??梢钥吹较旅孢@種腳本嵌入的寫法:
- <script>
- <?php
- if ($login == true){
- ?>
- alert("Welcome");
- <?php
- }
- ?>
- </script>
或者更糟糕:
- <script>
- var users_deleted = [];
- <?php
- $arr_ids = array(1,2,3,4);
- foreach($arr_ids as $value){
- ?>
- users_deleted.push("<php>");
- <?php
- }
- ?>
- </script>
對于新手來說,很容易被不同語言之間的用法而混淆,犯下一些很典型的錯誤,比如for和foreach。更為不爽的是,以這樣的方式來寫代碼,使得服務(wù)器 端和客戶端很難以非常和諧的方式處理相同的數(shù)據(jù)結(jié)構(gòu),即使是今天也是如此(當(dāng)然除非你的開發(fā)團(tuán)隊(duì)有專職的前端和后端工程師 — 但即使他們之間能夠共享信息,但仍然不能僅僅基于對方的代碼進(jìn)行合作)。
- <?php
- $arr = array("apples", "bananas", "oranges", "strawberries"),
- $obj = array();
- $i = 10;
- foreach($arr as $fruit){
- $obj[$fruit] = $i;
- $i += 10;
- }
- echo json_encode(obj);
- ?>
- <script>
- $.ajax({
- url:"/json.php",
- success: function(data){
- var x;
- for(x in data){
- alert("fruit:" + x + " points:" + data[x]);
- }
- }
- });
- </script>
最初,對于統(tǒng)一使用一種編程語言的嘗試是使用后臺的語言編寫客戶端的組件,然后編譯成JavaScript。但這種方式并沒有如期望的一樣很好地工作,許多相關(guān)的項(xiàng)目都失敗了(比如被ASP MVC取代了的ASP.NET Web forms, 又比如正在逐步被Polymer取代的GWT)。當(dāng)然這些想法都是偉大的,從本質(zhì)上講,都是想在服務(wù)器端和客戶端使用同一種語言,讓我們可以重用一些組件和資源(注意這里的關(guān)鍵詞:資源)。
最終得出的答案很簡單:將JavaScript放到服務(wù)端
其實(shí)JavaScript誕生之初是在網(wǎng)景公司的企業(yè)及服務(wù)器的 服務(wù)端,只是當(dāng)時它還沒有完全準(zhǔn)備好。經(jīng)過數(shù)年的磨煉和錯失,最終Node.js出現(xiàn)了,它不僅將JavaScript放到了服務(wù)器端,同時也推廣了非阻 塞式編程(non-blocking programming)的思想,這種思想來自于nginx的世界。感謝Node的創(chuàng)始者們nginx的技術(shù)背景,并且繼續(xù)(聰明地)保持了它的簡單性, 也感謝JavaScript天生的事件輪詢機(jī)制。
#p#
(一句話概括,非阻塞式編程目的在于將消耗時間的任務(wù)放到一邊,通過指定在這些任務(wù)結(jié)束時需要做的操作,這樣可以在同一時刻讓處理器去處理其他的請求。)
Node.js永久性地改變了我們處理I/O訪問的方式。作為Web開發(fā)者,我們過去一直使用如下的方式訪問數(shù)據(jù)庫(I/O):
- var resultset = db.query("SELECT * FROM 'table'");
- drawTable(resultset);
這里的第一行代碼本質(zhì)上已經(jīng)阻塞了你的代碼,因?yàn)槟愕拇a停止下來等待數(shù)據(jù)庫驅(qū)動返回一個結(jié)果集(resultset)。而與此同時,你的平臺架構(gòu)其實(shí)給你提供了并發(fā)的方法,通常是通過線程(threads)和派生(forks)。
在Node.js和非阻塞式編程的幫助下,我們可以更多的控制我們程序的執(zhí)行流?,F(xiàn)在(盡管在數(shù)據(jù)庫I/O驅(qū)動器的背后可能已經(jīng)有并行執(zhí)行),你可以定義你的程序在I/O操作期間并行做的事情,以及在接收到結(jié)果集之后做的操作。
- db.query("SELECT * FROM 'table'", function(resultset){
- drawTable(resultset);
- });
- doSomeThingElse();
上面的代碼片段中,我們定義了兩個程序流:第一個在我們發(fā)出數(shù)據(jù)庫查詢之后執(zhí)行的操作,第二個是以回調(diào)的方式在我們接收到結(jié)果集之后做的操作。這是一個非常優(yōu)雅并且強(qiáng)大的處理并發(fā)的方式。正如他們所說的,“一切都在并行執(zhí)行——除了你的代碼。(Evetything runs in parallel — except your code.)”這樣,你的代碼會更易寫,有更高的可讀性,容易理解,也便于維護(hù),這些都基于你找回了對程序流的控制。
這些觀點(diǎn)早就不是很新的觀點(diǎn),那為什么他們隨著Node.js變得如此流行起來。很簡單:非阻塞式編程可以有多重實(shí)現(xiàn)的方式。但可能最簡單的就是使用回調(diào)和事件輪詢。在大多數(shù)于語言里,做到這點(diǎn)并不是一個簡單的事情。回調(diào)機(jī)制在其他的一些于語言里是一個比較常見的功能,但是事件輪詢卻不是。你會經(jīng)常發(fā)現(xiàn)自己還需要在一些擴(kuò)展庫上做掙扎(比如,Python中使用Tornado)。
但是在JavaScript中,回調(diào)機(jī)制已經(jīng)被內(nèi)建在語言中, 事件輪詢也是如此。而對JavaScript稍有了解的程序員對它們也非常熟悉(或者至少使用過它們,即使他們有可能并不完全理解什么是事件輪詢)。突然之間,地球上所有的創(chuàng)業(yè)公司都可以在客戶端和服務(wù)器端重用開發(fā)人員(或者資源),解決了“需要Python大師(Python Guru Needed)”的招聘發(fā)布問題。
因此,現(xiàn)在我們有了一個發(fā)展迅速的平臺(感謝于非阻塞式編程),和一個非常易于使用的語言(感謝JavaScript)。但是這就足夠了嗎?它是可持續(xù)的嗎?我確信,JavaScript在將來會有一個非常重要的地位。下面我來告訴你為什么。
函數(shù)式編程
JavaScript是第一個將函數(shù)式范式帶給民眾的語言(當(dāng)然,Lisp第一個出現(xiàn),但是大多數(shù)的程序員都沒有使用它開發(fā)過一個可以作為產(chǎn)品的應(yīng)用)。Lisp和Self,這兩個深深影響了JavaScript的語言,充滿了創(chuàng)新的理念,它們解放了我們的思想,去挖掘新的技術(shù),模式和規(guī)范。這些都延續(xù)到了JavaScript上??匆幌耺ondas, Church number, 或者甚至(作為更有實(shí)踐性的例子)Underscore的Collections functions,這些可以節(jié)約你一行又一行的代碼。
動態(tài)對象以及原型繼承
沒有類(Classes),也沒有無窮無盡的類層次結(jié)構(gòu)的面向?qū)ο螅∣bject-oriented)編程是提供了更快速的編程體驗(yàn)——只要創(chuàng)建對 象,添加方法然后使用他們。更重要的是,它大大減少了維護(hù)時重構(gòu)的成本,因?yàn)樗试S程序員直接修改對象的實(shí)例,而不需要修改類。這種速度和靈活的方式為快 速開發(fā)鋪平了道路。
JavaScript就是互聯(lián)網(wǎng)
JavaScript是因互聯(lián)網(wǎng)而生的。它從一開始就出現(xiàn)了,并且伴隨到現(xiàn)在。任何想要摧毀它的嘗試都以失敗而告終,比如Java Applets的衰落,VBScript被微軟的TypeScript(它最終會被編譯成JavaScript)所取代,以及Flash在手機(jī)市場以及HTML5上的一敗涂地。如果想不破壞成千上萬個Web頁面而取代JavaScript是不可能的,所以我們接下來的目標(biāo)應(yīng)該是提高和完善它。這個工作,沒有誰比ECMA的Technical Committee 39更適合了。
當(dāng)然,JavaScript的替代者們每天都在誕生,比如CoffeeScript,TypeScript,以及成千上萬能被編譯成JavaScript的語言。這些替代者們在開發(fā)過程中也許是有用的(通過source maps), 但是他們最終都不可能成功地代替JavaScript,兩個主要原因:他們的社區(qū)永遠(yuǎn)不會比JavaScript更大,他們中的優(yōu)秀特性會被 ECMAScript(也就是JavaScript)所吸收。JavaScript不是匯編語言,它是一個你能理解代碼的高級編程語言——所以你應(yīng)該理解 它。
端到端(End-to-End)JavaScript:Node.js和MongoDB
我們已經(jīng)介紹了為什么要使用JavaScript。接著,我們來看看使用Node.js和MongoDB的理由。
NODE.JS
Node.js是 一個搭建快速和可擴(kuò)展的網(wǎng)絡(luò)應(yīng)用的平臺——正如Node.js網(wǎng)站上所說。但是Node.js遠(yuǎn)不止這些:它是如今最火的JavaScript運(yùn)行環(huán)境, 被大量的應(yīng)用和程序庫所使用——甚至是瀏覽器的庫代碼也運(yùn)行在Node.js上。更重要的是,這種服務(wù)器端的快速執(zhí)行讓程序員可以專注于更復(fù)雜的問題,比 如做自然語言處理的Natural。即使你并沒有計(jì)劃用Node.js來寫你的服務(wù)器端應(yīng)用,你也有可能使用基于Node.js的工具來改進(jìn)你的開發(fā)流程。舉例來說:用Bower來做前端包依賴管理,Mocha做單元測試,Grunt做自動化打包,甚至用Brachets做全文代碼編輯。
因此,如果你正準(zhǔn)備開發(fā)服務(wù)器端活客戶端的JavaScript應(yīng)用,你就需要對Node.js更加熟悉,因?yàn)槟阍谌粘9ぷ髦袝枰S幸恍┖苡腥さ拇娴倪x擇,但是它們中的任何一個的社區(qū)都不及Node.js的10%。
MONGODB
MongoDB是一個基于文檔(Document-based)NoSQL數(shù)據(jù)庫,它使用JavaScript作為它的查詢語言(但是它不是用JavaScript寫的),它完善了我們端到端的JavaScript平臺。但是這個并不是我們選擇MonoDB的主要原因。
MongoDB 是無模式的(schema-less),允許你以非常靈活的方式把對象持久化,因此能夠迅速的應(yīng)對需求變更。此外,它具有高度可擴(kuò)展性,并且基于map-reduce,讓它非常適合于大數(shù)據(jù)的應(yīng)用。MongoDB如此靈活,以至于它既可以用作無模式的文檔數(shù)據(jù)庫,也可以用作關(guān)系數(shù)據(jù)存儲(盡管它缺少事務(wù),只能通過模擬來實(shí)現(xiàn)),甚至是用來緩存結(jié)果的鍵值對存儲,就像Memcached和Redis。
#p#
基于Express的服務(wù)器端組件化
服務(wù)器端的組件化開發(fā)一直不是一件容易的是。但是 Express(和Connect)帶來了“中間件(middleware)的思想”。在我看來,中間件是服務(wù)器端定義組件最好的方式。如果你想找個熟悉 的模式來對比一下的話,那它非常接近于管道和過濾器(pipes and filters)。
基本思想就是將你的組件作為管道的一部分。管道處理一個請求(也叫輸入),生成一個結(jié)果(也叫輸出),但是你的組件并不負(fù)責(zé)整個響應(yīng)結(jié)果。相反,它只做它需要做的修改,然后將委派給下管道的下一節(jié)點(diǎn)。當(dāng)管道的最后的節(jié)點(diǎn)處理完之后,這個結(jié)果再返回給客戶端。
我們稱這些管道的節(jié)點(diǎn)為中間件。很明顯,我們可以創(chuàng)建兩種類型的中間件:
- 中間型(Intermediates)
- 一個中間型節(jié)中間件理請求和響應(yīng),但是它不負(fù)全權(quán)責(zé)整個響應(yīng),而是繼續(xù)將它們分派給下一個中間件。
- 終結(jié)型(Finals)
- 一個結(jié)束型中間件負(fù)責(zé)最終的響應(yīng)結(jié)果。它對請求和響應(yīng)進(jìn)行處理,之后不會分派給下一個中間件。但實(shí)踐中,繼續(xù)分派給一個中間件可以給架構(gòu)帶來更高的靈活性(比如,之后需要增加其他的中間件),即使下一個中間件并不存在(這種情況下,結(jié)果會直接被傳遞到客戶端)。
取一個具體的例子,假設(shè)服務(wù)器端有一個“用戶管理”的組件。根據(jù)中間件的方式,我們最好能有終結(jié)型和中間型的中間件。對于終結(jié)節(jié)點(diǎn),我們要有創(chuàng)建用 戶和列出用戶的功能。但是在我們做這些操作之前,我們需要使用中間節(jié)點(diǎn)來做認(rèn)證(因?yàn)槲覀儾幌M麤]有認(rèn)證過的請求能進(jìn)來,甚至創(chuàng)建用戶)。一旦我們創(chuàng)建好 了這些認(rèn)證中間件,當(dāng)我們想要把一個原先不需要認(rèn)證的功能改變成認(rèn)證功能的時候,我們只需要將這個中間件安插在相應(yīng)的位置。
單頁面(Single-Page)應(yīng)用
當(dāng)你使用全棧式JavaScript的時候,多數(shù)情況下你會專注開發(fā)單頁面應(yīng)用。大多數(shù)的Web開發(fā)者們都禁不住不止一次地嘗試著著手于單頁面應(yīng)用。我已經(jīng)創(chuàng)建了幾個(多數(shù)為個人的),我相信他們就是Web應(yīng)用的未來。你是否在移動鏈接上對比過單頁面應(yīng)用和通常的Web應(yīng)用?他們在響應(yīng)速度的差距有數(shù)十秒之多。
(注意:有些人可能不同意我的觀點(diǎn)。比如Twitter,回滾了他們的單頁面途徑。與此同時,很多大的網(wǎng)站正在步入單頁面時代,比如Zendesk。我已經(jīng)看到足夠的證據(jù)證明單頁面應(yīng)用帶來的好處,并且對此深信不疑。但是具體還是因情況而異。)
如果單頁面應(yīng)用如此強(qiáng)大,那為什么還是要選擇老土的方式來創(chuàng)建你的應(yīng)用呢?我經(jīng)常聽到的一種爭論就是他們擔(dān)心SEO(Search Engine Optimization)。但是如果你對此做了正確的處理,這將不是一個問題:你可以有多種解決方式,從使用無界面的瀏覽器(headless browser),比如PhantomJS,在檢測到網(wǎng)絡(luò)爬蟲的時候渲染HTML,到使用一些現(xiàn)有框架執(zhí)行服務(wù)器端渲染。
基于Backbones.js,Marionette和Twitter Bootstrap的客戶端MV*模式
關(guān)于使用MV*框架開發(fā)單頁面應(yīng)用已經(jīng)有太多的討論了。盡管很難選擇,但是我想說排名前三的是Backbone.js, Ember和AngularJS。
這三個都是非常被推崇的,但哪個是最適合你的?
不幸的是,我必須得承認(rèn)我在AngularJS上的經(jīng)驗(yàn)有限,所以我就把它放在討論范圍之外。那么,Ember和Backbone.js代表了解決同一問題的兩種不同方式。
Backbone.js很小,但是恰到好處的提供了創(chuàng)建一個簡單的單頁面應(yīng)用所需要的功能。另一方面,Ember是一個創(chuàng)建單頁面應(yīng)用的完整且專業(yè)的框架。它有更多的輔助工具,但是也有更加陡峭的學(xué)習(xí)曲線。(你可以閱讀更多關(guān)于Ember.js的內(nèi)容。)
基于你的應(yīng)用的大小,可以簡單地通過比較“需要的功能”占“可用的功能”的比例來做出決定,它會給你很大的提示。
樣式設(shè)計(jì)也同樣是一個挑戰(zhàn),但是再次,我們也可以列舉出一些可以助我們一臂之力的框架。對于CSS,Twitter Bootstrap是一個非常好的選擇,它提供了一套完整的樣式,它們可以立即使用,也非常便于自定義。
Bootstrap是使用LESS語言創(chuàng)建的,它是開源的,我們可以根據(jù)我們的需要來修改它。伴隨它的還有一大堆用戶友好的組件,它們也有非常完善的文檔。此外,一個定制化模式讓你很方便地創(chuàng)建你自己的。毫無疑問,它正是這個工作所需要的正確的工具。
最佳實(shí)踐:Grunt,Mocha,Chai,RequireJS 和 CoverJS
最后,我們將定義一些最佳實(shí)踐,同時談?wù)勗撊绾螌?shí)現(xiàn)和維護(hù)它們。具有代表性的,我的解決方案,最終聚焦到幾個工具上,他們本身都是基于Node.js。
MOCHA 和 CHAI
這些工具能幫助你使用測試驅(qū)動開發(fā)模式(test-driven development)或者行為驅(qū)動開發(fā)模式(behavior-driven development)來改進(jìn)你的開發(fā)流程,創(chuàng)建一些基礎(chǔ)架構(gòu)來管理你的單元測試,并且自動運(yùn)行這些測試。
現(xiàn)在有大量的JavaScript單元測試框架,為什么要用Mocha?簡短的回答就是它即靈活又完善。我來解釋一下:
- 用戶界面(Interfaces)
- 也許你習(xí)慣于測試驅(qū)動的程序組和單元測試的概念,又或許傾向于行為驅(qū)動測試的使用describle和should來定義行為定義的理念。Mocha讓你可以同時使用這兩種方式。
- 報表生成器(reporter)
- 運(yùn)行你的測試代碼會生成測試結(jié)果的報表,你可以使用各式各樣的reporter來格式化這些結(jié)果。舉例來說,如果你需要提供一個持續(xù)集成服務(wù)器信息,你可以找到一個report來做這些。
- 沒有指定斷言庫(Lack of an assertion library)
- 這幾乎不是一個問題,Mocha決定讓你選擇自己要使用的斷言庫,從而給你更多的靈活性。你有很多的選擇,這正是Chai施展身手的地方。
Chai 是一個非常靈活的斷言庫,它可以讓你使用如下三中主要斷言方式的任何一種:
- assert
這是來自老派測試驅(qū)動開發(fā)的經(jīng)典的assert方式。比如:
- assert.equal(variable, "value");
- expect
這種鏈?zhǔn)降臄嘌燥L(fēng)格在行為驅(qū)動開發(fā)中最為常見。比如:
- expect(variable).to.equal("value");
should
這也是用在測試驅(qū)動開發(fā)中,但是我更推薦expect,因?yàn)閟hould經(jīng)常聽起來比較反復(fù)(比如,定義一個行為規(guī)范,”it (should do something…)”)。舉例:
- variable.should.equal("value");
Chai和Mocha可以無縫集成。使用這兩個程序庫,你可以使用測試驅(qū)動,行為驅(qū)動活任何想得到的方式來寫你的測試代碼。
#p#
GRUNT
Grunt是你能夠自動化你的build任務(wù),包含簡單的復(fù)制粘貼和文件拼接,模板預(yù)編譯,style語言(SASS和LESS)編譯,單元測試(使用Mocha),代碼檢查,以及代碼最小化(比如,使用UglifyJS或者Closure Compiler)。你可以添加你自己的自動化任務(wù)到Grunt中或者搜索registry,那里數(shù)百個插件可供使用(再次提醒,選擇使用有良好的社區(qū)支持的工具)。Grunt也可以監(jiān)控你的文件,當(dāng)發(fā)生更改時觸發(fā)一些操作。
REQUIREJS
RequireJS 聽起來是基于AMD API的另一種加載模塊的方式,但是我敢保證地告訴你,它遠(yuǎn)遠(yuǎn)不止這個功能。使用RequireJS,你可以定義你的模塊之間的依賴和層次結(jié)構(gòu),讓 RequireJS庫幫你來加載他們。它還提供了一種非常簡便的方式來避免全局變量污染,通過在函數(shù)體中定義你的模塊。這讓模塊可以重用,不像命名空間模塊(namespaced modules)。試想一下:如果定義了一個類似于Demoapp.helloWorlModule的模塊,你想把他改成Firstapp.helloWorldModule,那么你需要把所有引用到Demoapp命名空間的地方都做修改,才能讓它變得可移植。
RequireJS還能讓你擁抱依賴注入模式。假設(shè)你有一個模塊需要用到主應(yīng)用對象(單例)的一個實(shí)例。通過使用RequireJS,你意識到你不 需要使用全局變量來存儲它,你也不能使用一個實(shí)例作為RequireJS的依賴。所以,你需要在你的模塊構(gòu)造器中加載這個依賴。讓我們看一個例子:
在main.js:
- define(
- ["App","module"],
- function(App, Module){
- var app = new App();
- var module = new Module({
- app: app
- })
- return app;
- }
- );
在module.js:
- define([],
- function(){
- var module = function(options){
- this.app = options.app;
- };
- module.prototype.useApp = function(){
- this.app.performAction();
- };
- return module
- }
- );
注意,我們不能在module的定義中加入對main.js的依賴,否則我們會創(chuàng)建出一個循環(huán)引用。
COVERJS
代碼覆蓋率(Code coverage)是你測試的一個度量標(biāo)準(zhǔn)。正如它的名字所示,它能告訴你當(dāng)前的測試集覆蓋了你代碼的多少部分。CoverJS通過檢測你代碼中的語句 (而不是像JSCoverage那樣看代碼行)并生成一個檢測過的版本的代碼來測量你的測試代碼的覆蓋率。它也可以支持對持續(xù)集成服務(wù)器提供持續(xù)報表生 成。
總結(jié)
全棧式JavaScript并不能解決所有的問題。但是它的社區(qū)和技術(shù)會帶領(lǐng)你走很長一段路。使用JavaScript,你可以創(chuàng)建基于統(tǒng)一的語言的可擴(kuò)展的,可維護(hù)的應(yīng)用。毫無疑問,這是絕對值得我們關(guān)注的。