全棧式JavaScript

jobbole發表於2013-12-09

  如今,在建立一個Web應用的過程中,你需要做出許多架構方面的決策。當然,你會希望做的每一個決定都是正確的:你想要使用能夠快速開發的技術,支援持續的迭代,最高的工作效率,迅速,健壯性強。你想要精益求精並且足夠敏捷。你希望你選擇的技術能夠在短期和長期上都讓你的專案取得成功。但這些技術都不是輕而易舉就能選出來的。

  我的經驗告訴我,全棧式JavaScript符合了這所有的要求。可能你已經發現了些許端倪,又或許你已經在考慮它的實用性,並且在和朋友討論爭論它的話題。但是你是否親自嘗試過呢?在這篇文章中,我會對於全棧式JavaScript給出一個比較全面的介紹,為什麼它會是正確的選擇,它又是如何施展它的魔法的。

  先給出一個概括預覽:

toptal-blog-500-opt

  接下來我會一項一項地介紹這些元件。但是在這之前,我們簡短地回顧一下,我們是如何發展到現在的這個階段的。

  我為什麼選擇用JavaScript

  從1998年開始,我就是一個Web開發者。當時,我們使用Perl進行大多數的伺服器端的開發;但是從那時候開始,我們就在客戶端使用JavaScript。Web伺服器端的技術已經發生了翻天覆地的變化:我們被一波又一波的技術潮流推著往前走,PHP,ASP,JSP,.NET,Ruby,Python,這裡只列出了幾個例子。開發人員們開始意識到,在伺服器端和客戶端使用不同的語言使得事情變得複雜化。

  在早期的PHP和ASP的時代,那個時候模板引擎還僅僅是個設想,開發人員們在HTML中嵌入他們的應用程式碼。我們經常可以看到下面這種指令碼嵌入的寫法:

<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。更為不爽的是,以這樣的方式來寫程式碼,使得伺服器端和客戶端很難以非常和諧的方式處理相同的資料結構,即使是今天也是如此(當然除非你的開發團隊有專職的前端和後端工程師 — 但即使他們之間能夠共享資訊,但仍然不能僅僅基於對方的程式碼進行合作)。

<?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>

  最初,對於統一使用一種程式語言的嘗試是使用後臺的語言編寫客戶端的元件,然後編譯成JavaScript。但這種方式並沒有如期望的一樣很好地工作,許多相關的專案都失敗了(比如被ASP MVC取代了的ASP.NET Web forms, 又比如正在逐步被Polymer取代的GWT)。當然這些想法都是偉大的,從本質上講,都是想在伺服器端和客戶端使用同一種語言,讓我們可以重用一些元件和資源(注意這裡的關鍵詞:資源)。

  最終得出的答案很簡單:將JavaScript放到服務端

  其實JavaScript誕生之初是在網景公司的企業及伺服器的服務端,只是當時它還沒有完全準備好。經過數年的磨鍊和錯失,最終Node.js出現了,它不僅將JavaScript放到了伺服器端,同時也推廣了非阻塞式程式設計(non-blocking programming)的思想,這種思想來自於nginx的世界。感謝Node的創始者們nginx的技術背景,並且繼續(聰明地)保持了它的簡單性,也感謝JavaScript天生的事件輪詢機制。

  (一句話概括,非阻塞式程式設計目的在於將消耗時間的任務放到一邊,通過指定在這些任務結束時需要做的操作,這樣可以在同一時刻讓處理器去處理其他的請求。)

  Node.js永久性地改變了我們處理I/O訪問的方式。作為Web開發者,我們過去一直使用如下的方式訪問資料庫(I/O):

var resultset = db.query("SELECT * FROM 'table'");
drawTable(resultset);

  這裡的第一行程式碼本質上已經阻塞了你的程式碼,因為你的程式碼停止下來等待資料庫驅動返回一個結果集(resultset)。而與此同時,你的平臺架構其實給你提供了併發的方法,通常是通過執行緒(threads)和派生(forks)。

  在Node.js和非阻塞式程式設計的幫助下,我們可以更多的控制我們程式的執行流。現在(儘管在資料庫I/O驅動器的背後可能已經有並行執行),你可以定義你的程式在I/O操作期間並行做的事情,以及在接收到結果集之後做的操作。

db.query("SELECT * FROM 'table'", function(resultset){
   drawTable(resultset);
});
doSomeThingElse();

  上面的程式碼片段中,我們定義了兩個程式流:第一個在我們發出資料庫查詢之後執行的操作,第二個是以回撥的方式在我們接收到結果集之後做的操作。這是一個非常優雅並且強大的處理併發的方式。正如他們所說的,“一切都在並行執行——除了你的程式碼。(Evetything runs in parallel — except your code.)”這樣,你的程式碼會更易寫,有更高的可讀性,容易理解,也便於維護,這些都基於你找回了對程式流的控制。

  這些觀點早就不是很新的觀點,那為什麼他們隨著Node.js變得如此流行起來。很簡單:非阻塞式程式設計可以有多重實現的方式。但可能最簡單的就是使用回撥和事件輪詢。在大多數於語言裡,做到這點並不是一個簡單的事情。回撥機制在其他的一些於語言裡是一個比較常見的功能,但是事件輪詢卻不是。你會經常發現自己還需要在一些擴充套件庫上做掙扎(比如,Python中使用Tornado)。

  但是在JavaScript中,回撥機制已經被內建在語言中, 事件輪詢也是如此。而對JavaScript稍有了解的程式設計師對它們也非常熟悉(或者至少使用過它們,即使他們有可能並不完全理解什麼是事件輪詢)。突然之間,地球上所有的創業公司都可以在客戶端和伺服器端重用開發人員(或者資源),解決了“需要Python大師(Python Guru Needed)”的招聘釋出問題

  因此,現在我們有了一個發展迅速的平臺(感謝於非阻塞式程式設計),和一個非常易於使用的語言(感謝JavaScript)。但是這就足夠了嗎?它是可持續的嗎?我確信,JavaScript在將來會有一個非常重要的地位。下面我來告訴你為什麼。

  函數語言程式設計

  JavaScript是第一個將函式式正規化帶給民眾的語言(當然,Lisp第一個出現,但是大多數的程式設計師都沒有使用它開發過一個可以作為產品的應用)。Lisp和Self,這兩個深深影響了JavaScript的語言,充滿了創新的理念,它們解放了我們的思想,去挖掘新的技術,模式和規範。這些都延續到了JavaScript上。看一下mondas, Church number, 或者甚至(作為更有實踐性的例子)UnderscoreCollections functions,這些可以節約你一行又一行的程式碼。

  動態物件以及原型繼承

  沒有類(Classes),也沒有無窮無盡的類層次結構的物件導向(Object-oriented)程式設計是提供了更快速的程式設計體驗——只要建立物件,新增方法然後使用他們。更重要的是,它大大減少了維護時重構的成本,因為它允許程式設計師直接修改物件的例項,而不需要修改類。這種速度和靈活的方式為快速開發鋪平了道路。

  JavaScript就是網際網路

  JavaScript是因網際網路而生的。它從一開始就出現了,並且伴隨到現在。任何想要摧毀它的嘗試都以失敗而告終,比如Java Applets的衰落,VBScript被微軟的TypeScript(它最終會被編譯成JavaScript)所取代,以及Flash在手機市場以及HTML5上的一敗塗地。如果想不破壞成千上萬個Web頁面而取代JavaScript是不可能的,所以我們接下來的目標應該是提高和完善它。這個工作,沒有誰比ECMA的Technical Committee 39更適合了。

  當然,JavaScript的替代者們每天都在誕生,比如CoffeeScriptTypeScript,以及成千上萬能被編譯成JavaScript的語言。這些替代者們在開發過程中也許是有用的(通過source maps),但是他們最終都不可能成功地代替JavaScript,兩個主要原因:他們的社群永遠不會比JavaScript更大,他們中的優秀特性會被ECMAScript(也就是JavaScript)所吸收。JavaScript不是組合語言,它是一個你能理解程式碼的高階程式語言——所以你應該理解它。

  端到端(End-to-End)JavaScript:Node.js和MongoDB

  我們已經介紹了為什麼要使用JavaScript。接著,我們來看看使用Node.js和MongoDB的理由。

  NODE.JS

  Node.js是一個搭建快速和可擴充套件的網路應用的平臺——正如Node.js網站上所說。但是Node.js遠不止這些:它是如今最火的JavaScript執行環境,被大量的應用和程式庫所使用——甚至是瀏覽器的庫程式碼也執行在Node.js上。更重要的是,這種伺服器端的快速執行讓程式設計師可以專注於更復雜的問題,比如做自然語言處理Natural。即使你並沒有計劃用Node.js來寫你的伺服器端應用,你也有可能使用基於Node.js的工具來改進你的開發流程。舉例來說:用Bower來做前端包依賴管理,Mocha做單元測試,Grunt做自動化打包,甚至用Brachets做全文程式碼編輯。

  因此,如果你正準備開發伺服器端活客戶端的JavaScript應用,你就需要對Node.js更加熟悉,因為你在日常工作中會需要他。有一些很有趣的代替的選擇,但是它們中的任何一個的社群都不及Node.js的10%。

  MONGODB

  MongoDB是一個基於文件(Document-based)NoSQL資料庫,它使用JavaScript作為它的查詢語言(但是它不是用JavaScript寫的),它完善了我們端到端的JavaScript平臺。但是這個並不是我們選擇MonoDB的主要原因。

  MongoDB 是無模式的(schema-less),允許你以非常靈活的方式把物件持久化,因此能夠迅速的應對需求變更。此外,它具有高度可擴充套件性,並且基於map-reduce,讓它非常適合於大資料的應用。MongoDB如此靈活,以至於它既可以用作無模式的文件資料庫,也可以用作關係資料儲存(儘管它缺少事務,只能通過模擬來實現),甚至是用來快取結果的鍵值對儲存,就像MemcachedRedis

  基於Express的伺服器端元件化

  伺服器端的元件化開發一直不是一件容易的是。但是 Express(和Connect)帶來了“中介軟體(middleware)的思想”。在我看來,中介軟體是伺服器端定義元件最好的方式。如果你想找個熟悉的模式來對比一下的話,那它非常接近於管道和過濾器(pipes and filters)。

  基本思想就是將你的元件作為管道的一部分。管道處理一個請求(也叫輸入),生成一個結果(也叫輸出),但是你的元件並不負責整個響應結果。相反,它只做它需要做的修改,然後將委派給下管道的下一節點。當管道的最後的節點處理完之後,這個結果再返回給客戶端。

  我們稱這些管道的節點為中介軟體。很明顯,我們可以建立兩種型別的中介軟體:

  • 中間型(Intermediates)
    一箇中間型節中介軟體理請求和響應,但是它不負全權責整個響應,而是繼續將它們分派給下一個中介軟體。
  • 終結型(Finals)
    一個結束型中介軟體負責最終的響應結果。它對請求和響應進行處理,之後不會分派給下一個中介軟體。但實踐中,繼續分派給一箇中介軟體可以給架構帶來更高的靈活性(比如,之後需要增加其他的中介軟體),即使下一個中介軟體並不存在(這種情況下,結果會直接被傳遞到客戶端)。

user-manager-500-opt
(Large view)

  取一個具體的例子,假設伺服器端有一個“使用者管理”的元件。根據中介軟體的方式,我們最好能有終結型和中間型的中介軟體。對於終結節點,我們要有建立使用者和列出使用者的功能。但是在我們做這些操作之前,我們需要使用中間節點來做認證(因為我們不希望沒有認證過的請求能進來,甚至建立使用者)。一旦我們建立好了這些認證中介軟體,當我們想要把一個原先不需要認證的功能改變成認證功能的時候,我們只需要將這個中介軟體安插在相應的位置。

  單頁面(Single-Page)應用

  當你使用全棧式JavaScript的時候,多數情況下你會專注開發單頁面應用。大多數的Web開發者們都禁不住不止一次地嘗試著著手於單頁面應用。我已經建立了幾個(多數為個人的),我相信他們就是Web應用的未來。你是否在移動連結上對比過單頁面應用和通常的Web應用?他們在響應速度的差距有數十秒之多。

  (注意:有些人可能不同意我的觀點。比如Twitter,回滾了他們的單頁面途徑。與此同時,很多大的網站正在步入單頁面時代,比如Zendesk。我已經看到足夠的證據證明單頁面應用帶來的好處,並且對此深信不疑。但是具體還是因情況而異。)

  如果單頁面應用如此強大,那為什麼還是要選擇老土的方式來建立你的應用呢?我經常聽到的一種爭論就是他們擔心SEO(Search Engine Optimization)。但是如果你對此做了正確的處理,這將不是一個問題:你可以有多種解決方式,從使用無介面的瀏覽器(headless browser),比如PhantomJS,在檢測到網路爬蟲的時候渲染HTML,到使用一些現有框架執行伺服器端渲染

  基於Backbones.js,Marionette和Twitter Bootstrap的客戶端MV*模式

  關於使用MV*框架開發單頁面應用已經有太多的討論了。儘管很難選擇,但是我想說排名前三的是Backbone.js, EmberAngularJS

  這三個都是非常被推崇的,但哪個是最適合你的

  不幸的是,我必須得承認我在AngularJS上的經驗有限,所以我就把它放在討論範圍之外。那麼,Ember和Backbone.js代表瞭解決同一問題的兩種不同方式。

  Backbone.js很小,但是恰到好處的提供了建立一個簡單的單頁面應用所需要的功能。另一方面,Ember是一個建立單頁面應用的完整且專業的框架。它有更多的輔助工具,但是也有更加陡峭的學習曲線。(你可以閱讀更多關於Ember.js的內容。)

  基於你的應用的大小,可以簡單地通過比較“需要的功能”佔“可用的功能”的比例來做出決定,它會給你很大的提示。

  樣式設計也同樣是一個挑戰,但是再次,我們也可以列舉出一些可以助我們一臂之力的框架。對於CSS,Twitter Bootstrap是一個非常好的選擇,它提供了一套完整的樣式,它們可以立即使用,也非常便於自定義

  Bootstrap是使用LESS語言建立的,它是開源的,我們可以根據我們的需要來修改它。伴隨它的還有一大堆使用者友好的元件,它們也有非常完善的文件。此外,一個定製化模式讓你很方便地建立你自己的。毫無疑問,它正是這個工作所需要的正確的工具。

  最佳實踐:Grunt,Mocha,Chai,RequireJS 和 CoverJS

  最後,我們將定義一些最佳實踐,同時談談該如何實現和維護它們。具有代表性的,我的解決方案,最終聚焦到幾個工具上,他們本身都是基於Node.js。

  MOCHA 和 CHAI

  這些工具能幫助你使用測試驅動開發模式(test-driven development)或者行為驅動開發模式(behavior-driven development)來改進你的開發流程,建立一些基礎架構來管理你的單元測試,並且自動執行這些測試。

  現在有大量的JavaScript單元測試框架,為什麼要用Mocha?簡短的回答就是它即靈活又完善。我來解釋一下:

  • 使用者介面(Interfaces)
    也許你習慣於測試驅動的程式組和單元測試的概念,又或許傾向於行為驅動測試的使用describle和should來定義行為定義的理念。Mocha讓你可以同時使用這兩種方式。
  • 報表生成器(reporter)
    執行你的測試程式碼會生成測試結果的報表,你可以使用各式各樣的reporter來格式化這些結果。舉例來說,如果你需要提供一個持續整合伺服器資訊,你可以找到一個report來做這些。
  • 沒有指定斷言庫(Lack of an assertion library)
    這幾乎不是一個問題,Mocha決定讓你選擇自己要使用的斷言庫,從而給你更多的靈活性。你有很多的選擇,這正是Chai施展身手的地方。

  Chai 是一個非常靈活的斷言庫,它可以讓你使用如下三中主要斷言方式的任何一種:

  • assert
    這是來自老派測試驅動開發的經典的assert方式。比如:

     

    assert.equal(variable, "value");
  • expect
    這種鏈式的斷言風格在行為驅動開發中最為常見。比如:

     

    expect(variable).to.equal("value");
  • should
    這也是用在測試驅動開發中,但是我更推薦expect,因為should經常聽起來比較反覆(比如,定義一個行為規範,”it (should do something…)”)。舉例:

     

    variable.should.equal("value");

  Chai和Mocha可以無縫整合。使用這兩個程式庫,你可以使用測試驅動,行為驅動活任何想得到的方式來寫你的測試程式碼。

  GRUNT

  Grunt是你能夠自動化你的build任務,包含簡單的複製貼上和檔案拼接,模板預編譯,style語言(SASS和LESS)編譯,單元測試(使用Mocha),程式碼檢查,以及程式碼最小化(比如,使用UglifyJS或者Closure Compiler)。你可以新增你自己的自動化任務到Grunt中或者搜尋registry,那裡數百個外掛可供使用(再次提醒,選擇使用有良好的社群支援的工具)。Grunt也可以監控你的檔案,當發生更改時觸發一些操作。

  REQUIREJS

  RequireJS 聽起來是基於AMD API的另一種載入模組的方式,但是我敢保證地告訴你,它遠遠不止這個功能。使用RequireJS,你可以定義你的模組之間的依賴和層次結構,讓RequireJS庫幫你來載入他們。它還提供了一種非常簡便的方式來避免全域性變數汙染,通過在函式體中定義你的模組。這讓模組可以重用,不像名稱空間模組(namespaced modules)。試想一下:如果定義了一個類似於Demoapp.helloWorlModule的模組,你想把他改成Firstapp.helloWorldModule,那麼你需要把所有引用到Demoapp名稱空間的地方都做修改,才能讓它變得可移植。

  RequireJS還能讓你擁抱依賴注入模式。假設你有一個模組需要用到主應用物件(單例)的一個例項。通過使用RequireJS,你意識到你不需要使用全域性變數來儲存它,你也不能使用一個例項作為RequireJS的依賴。所以,你需要在你的模組構造器中載入這個依賴。讓我們看一個例子:

  在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的依賴,否則我們會建立出一個迴圈引用。

  COVERJS

  程式碼覆蓋率(Code coverage)是你測試的一個度量標準。正如它的名字所示,它能告訴你當前的測試集覆蓋了你程式碼的多少部分。CoverJS通過檢測你程式碼中的語句(而不是像JSCoverage那樣看程式碼行)並生成一個檢測過的版本的程式碼來測量你的測試程式碼的覆蓋率。它也可以支援對持續整合伺服器提供持續報表生成。

  總結

  全棧式JavaScript並不能解決所有的問題。但是它的社群和技術會帶領你走很長一段路。使用JavaScript,你可以建立基於統一的語言的可擴充套件的,可維護的應用。毫無疑問,這是絕對值得我們關注的。

  原文連結: smashingmagazine   翻譯: 伯樂線上 - Owen Chen

相關文章