国产xxxx99真实实拍_久久不雅视频_高清韩国a级特黄毛片_嗯老师别我我受不了了小说

資訊專欄INFORMATION COLUMN

撰寫可測試的 JavaScript

Eminjannn / 2620人閱讀

摘要:我們已經(jīng)準備好經(jīng)歷一段痛苦的撰寫單元測試的過程了,但最終我們能夠撰寫可測試的。這種代碼是很容易進行集成測試的,但幾乎不可能針對功能單元進行多帶帶的測試。我們絕對可以寫出集成測試的代碼,但我們應該很難寫出單元測試了。

轉(zhuǎn)自 勾三股四 - 撰寫可測試的 JavaScript

這篇文章算是 A List Apart 系列文章中,包括滑動門在內(nèi),令我印象最深刻的文章之一。最近有時間翻譯了一下,分享給更多人,希望對大家有所幫助!

我們已經(jīng)面對到了這一窘境:一開始我們寫的 JavaScript 只有區(qū)區(qū)幾行代碼,但是它的代碼量一直在增長,我們不斷的加參數(shù)、加條件。最后,粗 bug 了…… 我們才不得不收拾這個爛攤子。

如上所述,今天的客戶端代碼確實承載了更多的責任,瀏覽器里的整個應用都越變越復雜。我們發(fā)現(xiàn)兩個明顯的趨勢:1、我們沒法通過單純的鼠標定位和點擊來檢驗代碼是否正常工作,自動化的測試才會真正讓我們放心;2、我們也許應該在撰寫代碼的時候就考慮到,讓它變得可測試。

神馬?我們需要改變自己的編碼方式?是的。因為即使我們意識到自動化測試的好,大部分人可能只是寫寫集成測試(integration tests)罷了。集成測試的側(cè)重點是讓整個系統(tǒng)的每一部分和諧共存,但是這并沒有告訴我們每個獨立的功能單元運轉(zhuǎn)起來是否都和我們預期的一樣。

這就是為什么我們要引入單元測試。我們已經(jīng)準備好經(jīng)歷一段痛苦的撰寫單元測試的過程了,但最終我們能夠撰寫可測試的 JavaScript

單元與集成:有什么不同?

撰寫集成測試通常是相當直接的:我們單純的撰寫代碼,描述用戶如何和這個應用進行交互、會得到怎樣的結(jié)果就好。Selenium 是這類瀏覽器自動化工具中的佼佼者。而 Capybara 可以便于 Ruby 和 Selenium 取得聯(lián)系。在其它語言中,這類工具也舉不勝舉。

下面就是搜索應用的一部分集成測試:

def test_search
    fill_in("q", :with => "cat")
    find(".btn").click
    assert( find("#results li").has_content?("cat"), "Search results are shown" )
    assert( page.has_no_selector?("#results li.no-results"), "No results is not shown" )
end

集成測試對用戶的交互行為感興趣,而單元測試往往僅專注于一小段代碼:

  

當我伴隨特定的輸入調(diào)用一個函數(shù)的時候,我是否收到了我預期中的結(jié)果?

我們按照傳統(tǒng)思路撰寫的程序是很難進行單元測試的,同時也很難維護、調(diào)試和擴展。但是如果我們在撰寫代碼的時候就考慮到我將來要做單元測試,那么這樣的思路不僅會讓我們發(fā)現(xiàn)測試代碼寫起來很直接,也會讓我們真正寫出更優(yōu)質(zhì)的代碼。

我們通過一個簡單的搜索應用的例子來做個示范:

當用戶搜索時,該應用會向服務器發(fā)送一個 XHR (Ajax 請求) 取得相應的搜索結(jié)果。并當服務器以 JSON 格式返回數(shù)據(jù)之后,通過前端模板把結(jié)果顯示在頁面中。用戶在搜索結(jié)果中點“贊”,這個人的名字就會出現(xiàn)在右側(cè)的點“贊”列表里。

一個“傳統(tǒng)”的 JavaScript 實現(xiàn)大概是這個樣子的:

// 模板緩存,緩存的內(nèi)容均為 jqXHR 對象
var tmplCache = {};

/**
 * 載入模板
 * 從 "/templates/{name}" 載入模板,存入 tmplCache
 * @param  {string} name 模板名稱
 * @return {object}      模板請求的 jqXHR 對象
 */
function loadTemplate (name) {
  if (!tmplCache[name]) {
    tmplCache[name] = $.get("/templates/" + name);
  }
  return tmplCache[name];
}

/**
 * 頁面主要邏輯
 * 1. 支持搜索行為并展示結(jié)果
 * 2. 支持點“贊”,被贊過的人會出現(xiàn)在點“贊”列表里
 */
$(function () {

  var resultsList = $("#results");
  var liked = $("#liked");
  var pending = false; // 用來標識之前的搜索是否尚未結(jié)束

  // 用戶搜索行為,表單提交事件
  $("#searchForm").on("submit", function (e) {
    // 屏蔽默認表單事件
    e.preventDefault();

    // 如果之前的搜索尚未結(jié)束,則不開始新的搜索
    if (pending) { return; }

    // 得到要搜索的關鍵字
    var form = $(this);
    var query = $.trim( form.find("input[name="q"]").val() );

    // 如果搜索關鍵字為空則不進行搜索
    if (!query) { return; }

    // 開始新的搜索
    pending = true;

    // 發(fā)送 XHR
    $.ajax("/data/search.json", {
      data : { q: query },
      dataType : "json",
      success : function (data) {
        // 得到 people-detailed 模板
        loadTemplate("people-detailed.tmpl").then(function (t) {
          var tmpl = _.template(t);

          // 通過模板渲染搜索結(jié)果
          resultsList.html( tmpl({ people : data.results }) );

          // 結(jié)束本次搜索
          pending = false;
        });
      }
    });

    // 在得到服務器響應之前,清空搜索結(jié)果,并出現(xiàn)等待提示
    $("
  • ", { "class" : "pending", html : "Searching …" }).appendTo( resultsList.empty() ); }); // 綁定點“贊”的行為,鼠標點擊事件 resultsList.on("click", ".like", function (e) { // 屏蔽默認點擊事件 e.preventDefault(); // 找到當前人的名字 var name = $(this).closest("li").find("h2").text(); // 清除點“贊”列表的占位元素 liked.find(".no-results").remove(); // 在點“贊”列表加入新的項目 $("
  • ", { text: name }).appendTo(liked); }); });
  • 我的朋友 Adam Sontag 稱之為“自己給自己挖坑”的代碼:展現(xiàn)、數(shù)據(jù)、用戶交互、應用狀態(tài)全部分散在了每一行代碼里。這種代碼是很容易進行集成測試的,但幾乎不可能針對功能單元進行多帶帶的測試。

    單元測試為什么這么難?有四大罪魁禍首:

    沒有清晰的結(jié)構(gòu)。幾乎所有的工作都是在 $(document).ready() 回調(diào)里進行的,而這一切在一個匿名函數(shù)里,它在測試中無法暴露出任何接口。

    函數(shù)太復雜。如果一個函數(shù)超過了 10 行,比如提交表單的那個函數(shù),估計大家都覺得它太忙了,一口氣做了很多事。

    隱藏狀態(tài)還是共享狀態(tài)。比如,因為 pending 在一個閉包里,所以我們沒有辦法測試在每個步驟中這個狀態(tài)是否正確。

    強耦合。比如這里 $.ajax 成功的回調(diào)函數(shù)不應該依賴 DOM 操作。

    組織我們的代碼

    首當其沖的是把我們代碼的邏輯縷一縷,根據(jù)職責的不同把整段代碼分為幾個方面:

    展現(xiàn)和交互

    數(shù)據(jù)管理和保存

    應用的狀態(tài)

    把上述代碼建立并串連起來

    在之前的“傳統(tǒng)”實現(xiàn)里,這四類代碼是混在一起的,前一行我們還在處理界面展現(xiàn),后兩行就在和服務器通信了。

    我們絕對可以寫出集成測試的代碼,但我們應該很難寫出單元測試了。在功能測試里,我們可以做出諸如“當用戶搜索東西的時候,他會看到相應的搜索結(jié)果”的斷言,但是無法再具體下去了。如果里面出了什么問題,我們還是得追蹤進去,找到確切的出錯位置。這樣的話功能測試其實也沒幫上什么忙。

    如果我們反思自己的代碼,那不妨從單元測試寫起,通過單元測試這個角度,更好的觀察,是哪里出了問題。這進而會幫助我們改進代碼,讓代碼變得更易于重用、易于維護、易于擴展。

    我們的新版代碼遵循下面幾個原則:

    根據(jù)上述四類職責,列出每個互不相干的行為,并分別用一個對象來表示。對象之前互不依賴,以避免不同的代碼混在一起。

    用可配置的內(nèi)容代替寫死的內(nèi)容,以避免我們?yōu)榱藴y試而復刻整個 HTML 環(huán)境。

    保持對象方法的簡單明了。這會把測試工作變得簡單易懂。

    通過構(gòu)造函數(shù)創(chuàng)建對象實例。這讓我們可以根據(jù)測試的需要復刻每一段代碼的內(nèi)容。

    作為起步,我們有必要搞清楚,該如何把應用分解成不同的部分。我們有三塊展現(xiàn)和交互的內(nèi)容:搜索框、搜索結(jié)果和點“贊”列表。

    我們還有一塊內(nèi)容是從服務器獲取數(shù)據(jù)的、一塊內(nèi)容是把所有的內(nèi)容粘合在一起的。

    我們從整個應用最簡單的一部分開始吧:點“贊”列表。在原版應用中,這部分代碼的職責就是更新點“贊”列表:

    var liked = $("#liked");
    var resultsList = $("#results");
    
    // ...
    
    resultsList.on("click", ".like", function (e) {
      e.preventDefault();
      var name = $(this).closest("li").find("h2").text();
      liked.find( ".no-results" ).remove();
      $("
  • ", { text: name }).appendTo(liked); });
  • 搜索結(jié)果這部分是完全和點“贊”列表攪在一起的,并且需要很多 DOM 處理。更好的易于測試的寫法是創(chuàng)建一個點“贊”列表的對象,它的職責就是封裝點“贊”列表的 DOM 操作。

    var Likes = function (el) {
      this.el = $(el);
      return this;
    };
    
    Likes.prototype.add = function (name) {
      this.el.find(".no-results").remove();
      $("
  • ", { text: name }).appendTo(this.el); };
  • 這段代碼提供了創(chuàng)建一個點“贊”列表對象的構(gòu)造函數(shù)。它有 .add() 方法,可以在產(chǎn)生新的贊的時候使用。這樣我們就可以寫很多測試代碼來保障它的正常工作了:

    var ul;
    
    // 設置測試的初始狀態(tài):生成一個搜索結(jié)果列表
    setup(function(){
      ul = $("
    
    *");
    });
    
    test("測試構(gòu)造函數(shù)", function () {
      var l = new Likes(ul);
      // 斷言對象存在
      assert(l);
    });
    
    test("點一個“贊”", function () {
      var l = new Likes(ul);
      l.add("Brendan Eich");
    
      // 斷言列表長度為1
      assert.equal(ul.find("li").length, 1);
      // 斷言列表第一個元素的 HTML 代碼是 "Brendan Eich"
      assert.equal(ul.find("li").first().html(), "Brendan Eich");
      // 斷言占位元素已經(jīng)不存在了
      assert.equal(ul.find("li.no-results").length, 0);
    });
    

    怎么樣?并不難吧 :-) 我們這里用到了名為 Mocha 的測試框架,以及名為 Chai 的斷言庫。Mocha 提供了 testsetup 函數(shù);而 Chai 提供了 assert。測試框架和斷言庫的選擇還有很多,我們出于介紹的目的給大家展示這兩款。你可以找到屬于適合自己的項目——除了 Mocha 之外,QUnit 也比較流行。另外 Intern 也是一個測試框架,它運用了大量的 promise 方式。

    我們的測試代碼是從點“贊”列表這一容器開始的。然后它運行了兩個測試:一個是確定點“贊”列表是存在的;另一個是確保 .add() 方法達到了我們預期的效果。有這些測試做后盾,我們就可以放心重構(gòu)點“贊”列表這部分的代碼了,即使代碼被破壞了,我們也有信心把它修復好。

    我們新應用的代碼現(xiàn)在看起來是這樣的:

    var liked = new Likes("#liked"); // 新的點“贊”列表對象
    var resultsList = $("#results");
    
    // ...
    
    resultsList.on("click", ".like", function (e) {
      e.preventDefault();
      var name = $(this).closest("li").find("h2").text();
      liked.add(name); // 新的點“贊”操作的封裝
    });
    

    搜索結(jié)果這部分比點“贊”列表更復雜一些,不過我們也該拿它開刀了。和我們?yōu)辄c“贊”列表創(chuàng)建一個 .add() 方法一樣,我們要創(chuàng)建一個與搜索結(jié)果有交互的方法。我們需要一個點“贊”的入口,向整個應用“廣播”自己發(fā)生了什么變化——比如有人點了個“贊”。

    // 為每一條搜索結(jié)果的點“贊”按鈕綁定點擊事件
    var SearchResults = function (el) {
      this.el = $(el);
      this.el.on( "click", ".btn.like", _.bind(this._handleClick, this) );
    };
    
    // 展示搜索結(jié)果,獲取模板,然后渲染
    SearchResults.prototype.setResults = function (results) {
      var templateRequest = $.get("people-detailed.tmpl");
      templateRequest.then( _.bind(this._populate, this, results) );
    };
    
    // 處理點“贊”
    SearchResults.prototype._handleClick = function (evt) {
      var name = $(evt.target).closest("li.result").attr("data-name");
      $(document).trigger("like", [ name ]);
    };
    
    // 對模板渲染數(shù)據(jù)的封裝
    SearchResults.prototype._populate = function (results, tmpl) {
      var html = _.template(tmpl, { people: results });
      this.el.html(html);
    };
    

    現(xiàn)在我們舊版應用中管理搜索結(jié)果和點“贊”列表之間交互的代碼如下:

    var liked = new Likes("#liked");
    var resultsList = new SearchResults("#results");
    
    // ...
    
    $(document).on("like", function (evt, name) {
      liked.add(name);
    })
    

    這就更簡單更清晰了,因為我們通過 document 在各個獨立的組件之間進行消息傳遞,而組件之間是互不依賴的。(值得注意的是,在真正的應用當中,我們會使用一些諸如 Backbone 或 RSVP 庫來管理事件。我們出于讓例子盡量簡單的考慮,使用了 document 來觸發(fā)事件) 我們同時隱藏了很多臟活累活:比如在搜索結(jié)果對象里尋找被點“贊”的人,要比放在整個應用的代碼里更好。更重要的是,我們現(xiàn)在可以寫出保障搜索結(jié)果對象正常工作的測試代碼了:

    var ul;
    var data = [ /* 填入假數(shù)據(jù) */ ];
    
    // 確保點“贊”列表存在
    setup(function () {
      ul = $("
    
    *");
    });
    
    test("測試構(gòu)造函數(shù)", function () {
      var sr = new SearchResults(ul);
      // 斷言對象存在
      assert(sr);
    });
    
    test("測試收到的搜索結(jié)果", function () {
      var sr = new SearchResults(ul);
      sr.setResults(data);
    
      // 斷言搜索結(jié)果占位元素已經(jīng)不存在
      assert.equal(ul.find(".no-results").length, 0);
      // 斷言搜索結(jié)果的子元素個數(shù)和搜索結(jié)果的個數(shù)相同
      assert.equal(ul.find("li.result").length, data.length);
      // 斷言搜索結(jié)果的第一個子元素的 "data-name" 的值和第一個搜索結(jié)果相同
      assert.equal(
        ul.find("li.result").first().attr("data-name"),
        data[0].name
      );
    });
    
    test("測試點“贊”按鈕", function() {
      var sr = new SearchResults(ul);
      var flag;
      var spy = function () {
        flag = [].slice.call(arguments);
      };
    
      sr.setResults(data);
      $(document).on("like", spy);
    
      ul.find("li").first().find(".like.btn").click();
    
      // 斷言 `document` 收到了點“贊”的消息
      assert(flag, "事件被收到了");
      // 斷言 `document` 收到的點“贊”消息,其中的名字是第一個搜索結(jié)果
      assert.equal(flag[1], data[0].name, "事件里的數(shù)據(jù)被收到了" );
    });
    

    和服務器直接的交互是另外一個有趣的話題。原版的代碼包括一個 $.ajax() 的請求,以及一個直接操作 DOM 的回調(diào)函數(shù):

    $.ajax("/data/search.json", {
      data : { q: query },
      dataType : "json",
      success : function( data ) {
        loadTemplate("people-detailed.tmpl").then(function(t) {
          var tmpl = _.template( t );
          resultsList.html( tmpl({ people : data.results }) );
          pending = false;
        });
      }
    });
    

    同樣,我們很難為這樣的代碼撰寫測試。因為很多不同的工作同時發(fā)生在這一小段代碼中。我們可以重新組織一下數(shù)據(jù)處理的部分:

    var SearchData = function () { };
    
    SearchData.prototype.fetch = function (query) {
      var dfd;
    
      // 如果搜索關鍵字為空,則不做任何事,立刻 `promise()`
      if (!query) {
        dfd = $.Deferred();
        dfd.resolve([]);
        return dfd.promise();
      }
    
      // 否則,向服務器請求搜索結(jié)果并把在得到結(jié)果之后對其數(shù)據(jù)進行包裝
      return $.ajax( "/data/search.json", {
        data : { q: query },
        dataType : "json"
      }).pipe(function( resp ) {
        return resp.results;
      });
    };
    

    現(xiàn)在我們改變了獲得搜索結(jié)果這部分的代碼:

    var resultList = new SearchResults("#results");
    var searchData = new SearchData();
    
    // ...
    
    searchData.fetch(query).then(resultList.setResults);
    

    我們再一次簡化了代碼,并通過 SearchData 對象拋棄了之前應用程序主函數(shù)里雜亂的代碼。同時我們已經(jīng)讓搜索接口變得可測試了,盡管現(xiàn)在和服務器通信這里還有事情要做。

    首先我們不是真的要跟服務器通信——不然這又變成集成測試了:諸如我們是有責任感的開發(fā)者,我們已經(jīng)確保服務器一定不會犯錯等等,是這樣嗎?為了替代這些東西,我們應該“mock”(偽造) 與服務器之間的通信。Sinon 這個庫就可以做這件事。第二個障礙是我們的測試應該覆蓋非理想環(huán)境,比如關鍵字為空。

    test("測試構(gòu)造函數(shù)", function () {
      var sd = new SearchData();
      assert(sd);
    });
    
    suite("取數(shù)據(jù)", function () {
      var xhr, requests;
    
      setup(function () {
        requests = [];
        xhr = sinon.useFakeXMLHttpRequest();
        xhr.onCreate = function (req) {
          requests.push(req);
        };
      });
    
      teardown(function () {
        xhr.restore();
      });
    
      test("通過正確的 URL 獲取數(shù)據(jù)", function () {
        var sd = new SearchData();
        sd.fetch("cat");
    
        assert.equal(requests[0].url, "/data/search.json?q=cat");
      });
    
      test("返回一個 promise", function () {
        var sd = new SearchData();
        var req = sd.fetch("cat");
    
        assert.isFunction(req.then);
      });
    
      test("如果關鍵字為空則不查詢", function () {
        var sd = new SearchData();
        var req = sd.fetch();
        assert.equal(requests.length, 0);
      });
    
      test("如果關鍵字為空也會有 promise", function () {
        var sd = new SearchData();
        var req = sd.fetch();
    
        assert.isFunction( req.then );
      });
    
      test("關鍵字為空的 promise 會返回一個空數(shù)組", function () {
        var sd = new SearchData();
        var req = sd.fetch();
        var spy = sinon.spy();
    
        req.then(spy);
    
        assert.deepEqual(spy.args[0][0], []);
      });
    
      test("返回與搜索結(jié)果相對應的對象", function () {
        var sd = new SearchData();
        var req = sd.fetch("cat");
        var spy = sinon.spy();
    
        requests[0].respond(
          200, { "Content-type": "text/json" },
          JSON.stringify({ results: [ 1, 2, 3 ] })
        );
    
        req.then(spy);
    
        assert.deepEqual(spy.args[0][0], [ 1, 2, 3 ]);
      });
    });
    

    出于篇幅的考慮,這里對搜索框的重構(gòu)及其相關的單元測試就不一一介紹了。完整的代碼可以移步至此查閱。

    當我們按照可測試的 JavaScript 的思路重構(gòu)代碼之后,我們最后用下面這段代碼開啟程序:

    $(function() {
      var pending = false;
    
      var searchForm = new SearchForm("#searchForm");
      var searchResults = new SearchResults("#results");
      var likes = new Likes("#liked");
      var searchData = new SearchData();
    
      $(document).on("search", function (event, query) {
        if (pending) { return; }
    
        pending = true;
    
        searchData.fetch(query).then(function (results) {
          searchResults.setResults(results);
          pending = false;
        });
    
        searchResults.pending();
      });
    
      $(document).on("like", function (evt, name) {
        likes.add(name);
      });
    });
    

    比干凈整潔的代碼更重要的,是我們的代碼擁有了更健壯的測試基礎作為后盾。這也意味著我們可以放心的重構(gòu)任意部分的代碼而不必擔心程序遭到破壞。我們還可以繼續(xù)為新功能撰寫新的測試代碼,并確保新的程序可以通過所有的測試。

    測試會在宏觀上讓你變輕松

    看完這些的長篇大論你一定會說:“納尼?我多寫了這么多代碼,結(jié)果還是做了這么一點事情?”

    關鍵在于,你做的東西早晚要放到網(wǎng)上的。同樣是花時間解決問題,你會選擇在瀏覽器里點來點去?還是自動化測試?還是直接在線上讓你的用戶做你的小白鼠?無論你寫了多少測試,你寫好代碼,別人一用,多少會發(fā)現(xiàn)點 bug。

    至于測試,它可能會花掉你一些額外的時間,但是它到最后真的是為你省下了時間。寫測試代碼測出一個問題,總比你發(fā)布到線上之后才發(fā)現(xiàn)有問題要好。如果有一個系統(tǒng)能讓你意識到它真的能避免一個 bug 的流出,你一定會心存感激。

    額外的資源

    這篇文章只能算是 JavaScript 測試的一點皮毛,但是如果你對此抱有興趣,那么可以繼續(xù)移步至:

    幻燈演示 2012 Full Frontal conference in Brighton, UK

    Grunt 一個可以進行自動化測試等諸多事情的工具

    測試驅(qū)動的 JavaScript 開發(fā) 及其 中文版

    文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

    轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/85502.html

    相關文章

    • 編寫可測試的Go代碼

      摘要:功能測試函數(shù)功能測試函數(shù)需要接收類型的單一參數(shù),類型用來管理測試狀態(tài)和支持格式化的測試日志。測試函數(shù)的相關說明,可以通過來查看幫助文檔。下面是一個例子被測試的進程退出函數(shù)測試進程退出函數(shù)的測試函數(shù)參考資料原文鏈接 原文鏈接:http://tabalt.net/blog/golang... Golang作為一門標榜工程化的語言,提供了非常簡便、實用的編寫單元測試的能力。本文通過Golan...

      khlbat 評論0 收藏0
    • MVC

      摘要:模式的目的是實現(xiàn)一種動態(tài)的程序設計,使后續(xù)對程序的修改和擴展簡化,并且使程序某一部分的重復利用成為可能。它處理事件并作出響應。事件包括用戶的行為和數(shù)據(jù)上的改變。此外,提高了應用程序的靈活性和可配置性。 我的博客地址 → MVC | The story of Captain,轉(zhuǎn)載請注明出處。 MVC模式 (Model–View–Controller)是軟件工程中的一種軟件架構(gòu)模式,把軟...

      luck 評論0 收藏0
    • 全棧工程師的武器——MEAN

      摘要:自年發(fā)布以來,走過了漫長的道路。一下子,工程師認為自己不只是前端開發(fā)者了。這種趨勢被稱為全棧的或純的解決方案。可以認為它是文檔結(jié)構(gòu)的數(shù)據(jù)庫,而不是由行列表組成的數(shù)據(jù)庫。也是高度可測試的,這是很重要的。 JavaScript自1995年發(fā)布以來,走過了漫長的道路。已經(jīng)有了幾個主要版本的ECMAScript規(guī)范,單頁Web應用程序也慢慢興起,還有支持客戶端的JavaScript框架。作為一...

      chanjarster 評論0 收藏0
    • [ 學習路線 ] 2015 前端(JS)工程師必知必會 (2)

      摘要:轉(zhuǎn)自前端外刊評論非常感謝,翻譯的很好,受益很多,轉(zhuǎn)到此處讓前端小伙伴們也驚呆下上次我寫前端工程師必知必會已經(jīng)是三年前了,那是我寫過最火的文章了。測試的第二大障礙是工具。 轉(zhuǎn)自:前端外刊評論 非常感謝,翻譯的很好,受益很多,轉(zhuǎn)到此處讓前端小伙伴們也驚呆下........ 上次我寫《前端工程師必知必會》已經(jīng)是三年前了,那是我寫過最火的文章了。三年了,我仍然會在Twitter上...

      stefan 評論0 收藏0
    • [ 學習路線 ] 2015 前端(JS)工程師必知必會 (2)

      摘要:轉(zhuǎn)自前端外刊評論非常感謝,翻譯的很好,受益很多,轉(zhuǎn)到此處讓前端小伙伴們也驚呆下上次我寫前端工程師必知必會已經(jīng)是三年前了,那是我寫過最火的文章了。測試的第二大障礙是工具。 轉(zhuǎn)自:前端外刊評論 非常感謝,翻譯的很好,受益很多,轉(zhuǎn)到此處讓前端小伙伴們也驚呆下........ 上次我寫《前端工程師必知必會》已經(jīng)是三年前了,那是我寫過最火的文章了。三年了,我仍然會在Twitter上...

      liaorio 評論0 收藏0

    發(fā)表評論

    0條評論

    最新活動
    閱讀需要支付1元查看
    <