一個 JS 框架需要做什麼
學習再多,也是杯水車薪。
為什麼這麼說?不知道各位有沒有發現,雖然前端發展快,但一些有名的框架至少會火熱很長時間,比如 Backbone、React、Ember 。如果有心要學,肯定有足夠的時間把它學會,畢竟事實擺在面前,很多公司的上線產品就是用 React 來寫的,比如 Teambition 的簡聊,貌似它是從 Backbone 重構過來的。然而,很多同學在接手新專案時,常常會不知所措,不知道用什麼技術去做,或者說,只依賴於擅長的技術,就算在一些場景中它可能並不是最適合的。
因為這些同學平時不夠努力嗎?不是吧。他們可能會看書到很晚,瀏覽很多部落格,就是為了去了解 CORS 的應用,或者是想知道為什麼 Angular 中的 scope 在某些時候不能雙向繫結了。對,時間是花了,但遇到問題還是一頭霧水。可能前端就是這麼一份工作吧,慫恿你去學游泳,蛙泳、自由泳、蝶泳,海嘯來了照樣被沖走……
那這篇文章要說的是什麼呢?就是假設你現在什麼都沒學,就靠基本功,去完成一個靜態頁面,當然也有業務邏輯,包括資料的 CRUD、動畫,怎麼做?有個關於 VanillaJS 的梗不知道大家看過沒,你一定會會心一笑的。
沒有 jQuery 了,沒有 Bootstrap 了,扔掉所有你引以為傲的武器,但大惡魔 IE 6 還在。具體的需求不給了,反正給了你們也不會照著去實現,真有心要做的話,可以做一個 todo app 吧。
DOM 查詢
在沒有第三方框架可以用的時候,如果真的按照功能列表,從第一條實現到最後一條,每個模組用自執行匿名函式包起來,所有程式碼寫在一個檔案中,看上去十分合理,但真這麼做的話,恐怕你會瘋掉吧。哦,好處是你可以跟別人吹噓今天寫了三四百行程式碼,產量很高呢!
所以,不使用第三方框架,我們可以自己寫,它的功能只要符合應用場景就可以了,不用去考慮各種不會發生的奇葩情況。
好,開始。我們最依賴的功能是通過 CSS 選擇器獲取相應的 DOM 元素,這裡只使用相容性最高的方式,就是 id 和元素名選擇器。
var idRegex = /^#[\w\-]+/i, tagRegex = /^[a-z]+/i; function query(selector, context) { context = context || document; if (idRegex.test(selector)) { return document.getElementById(selector.substring(1)); } else if (tagRegex.test(selector)) { return context.getElementsByTagName(selector); } return null; }
對了,我把所有 DOM 操作放在了 F.DOM 名稱空間下,所以是這樣使用 query 方法的:
F.DOM.query('#id');
的確比 jQuery 的 $('#id') 方式麻煩很多,但“子不嫌母醜,狗不嫌家貧”,自己寫的程式碼,再爛也要用下去。
另外一些必須的操作就不把程式碼貼出來了,比如說 addClass、removeClass、hasClass 等。
DOM 事件
如果有同學參加過面試的話,我想“怎麼去監聽一個 DOM 事件?請儘可能考慮瀏覽器相容性”這個問題是經常會問到吧。這兒寫一個可行方案吧。
// 監聽 DOM 事件 function addEventListener(el, event, handler, useCapture) { if (el.addEventListener) { el.addEventListener(event, handler, useCapture); } else if (el.attachEvent) { el.attachEvent('on' + event, handler); } else { // not support } } // 取消 DOM 事件 function removeEventListener(el, event, handler, useCapture) { if (el.removeEventListener) { el.removeEventListener(event, handler, useCapture); } else if (el.detachEvent) { el.detachEvent('on' + event, handler); } else { // not support } }
我知道大家可能有更好的,或者更完善的方案,但抱歉這裡討論的重點不是它。
關於 DOM 事件方面,還有一些有用的方法,比如 preventDefault 和 stopPropagation 也可以自己去封裝一下。然後這兒想討論一下 DOM 載入完成的事件。jQuery 中我們會這麼用:
$(function() { // ready });
如果我們也想封裝一個類似的方法,可能會這麼寫:
addEventListener('window', 'load', callback);
可是 load 事件是在什麼情況下觸發的呢?當頁面上的所有資源,包括圖片,載入完之後才觸發!也就是說,如果圖片很多,網速很慢,那觸發 load 要花很長時間。在本地除錯時不會有這種延遲的問題,所以往往會被忽略。
那怎麼改正呢?第一,可以把 <script> 放到 <body> 中所有元素的下方,就不需要監聽任何“載入完成”的事件了。第二,監聽 DOMContentLoaded 事件,IE 9+ 支援。至於如何相容低版本瀏覽器,可以看這篇文章 (addDOMLoadEvent)。
元件式開發
“元件”這個詞其實來源於很多框架,比如 Backbone 中的 View,React 就更不用說了,它為了元件化專門規定了 JSX(當然它有更巨集偉的 goal) 。我們這裡討論的元件也是差不多的意思,就是按照功能,把頁面上分成一個個獨立的模組,模組之間通過訊息(事件)進行溝通。關於模組耦合,JSX 是通過類似於 HTML 標籤巢狀的方式來表現的,而我們自然沒這麼高階,就直接把依賴的模組注入到其他模組中,比如:
/** * 應用頂層,構造一些頁面中用到的元件 */ function App() { F.Component.call(this); } App.prototype = new F.Component(); F.extend(App.prototype, { constructor: App, init: function() { this._blogPost = new BlogPost('#blog-post'); this._blogList = new BlogList('#blog-list', this); this._newsList = new NewsList('#news-wrapper'); } }); new App();
其中,BlogPost 是釋出日誌的元件,BlogList 是日誌列表。釋出日誌後必然會顯示到列表中,所以在構造日誌列表時,會把 BlogPost 注入到 BlogList 中。
每個元件可以提供一個 id 選擇器,表示該元件需要繪製在哪個元素內。
訊息傳播機制
關於 BlogPost 和 BlogList,大家可以想象微博的主頁,它上面是一個釋出框,下面是微博列表,就是這樣一個介面。
當微博釋出之後,列表中需要增加新發布的內容,這個過程是誰給誰發訊息?按照物件導向的思想,應該是類似於這樣:
// 在 釋出框元件 中呼叫 列表元件 的方法 blogList.add(item);
顯然是 BlogPost 依賴於 BlogList 對嗎?但貌似我們上面的程式碼不是這個邏輯,而是反過來。那麼實際情況就成了這樣:
// 釋出框:BlogPost 中觸發事件 this.emit('add', item); // 列表:BlogList 中監聽事件 this.listenTo(blogPost, 'add', handler); // 由 handler 處理髮布事件
嗯,程式碼變多了,看來得強行圓回來。
為什麼我強烈建議使用後者?假設過了一段時間,某個充滿創意的策劃突然告訴你,當釋出微博之後,可以顯示到朋友圈(假設有這麼個東西)。那麼前者的方式會怎麼做?是不是首先給這個釋出框多注入一個依賴,即朋友圈,然後呼叫朋友圈的某個方法?
如果再過段時間,又有新創意了,是不是又得給釋出框加依賴了?最後搞得釋出框依賴於微博列表、依賴於朋友圈、依賴於其他 8 個元件,真不想用水性楊花來形容它。
這個問題很常見吧?如果用訊息機制的方式就會好很多,只需要在新增加的元件中監聽釋出框的 'add' 事件就可以了。
如果你能接受這個方式,可能想知道怎麼去簡單地實現它。
var Event = F.Event = function Event() { // 該元件相關的所有的事件都儲存在 _events 物件中 // 格式 - {'eventName': [{handler, context}*]} this._events = {}; }; F.extend(Event.prototype, { // 監聽事件 on: function(event, handler, context) { if (!this._events[event]) { this._events[event] = []; } this._events[event].push({ handler: handler, context: context || this }); }, // 觸發事件 emit: function(event) { var events = this._events[event] || [], args = []; // 第一個引數為事件名,後面的引數需要傳給處理該事件的方法,記錄到 args 中 if (arguments.length > 1) { args = slice.call(arguments, 1); } // 回撥時需要傳入引數 events.forEach(function(v) { v.handler.apply(v.context, args); }); } }
把重點部分貼了一下。第一,這個 Event 是所有元件的基類,所以每個元件都有 on 和 emit 方法。第二,F.extend 的作用就是把後面物件的方法和屬性直接賦值給第一個,extend 的意思是“擴充套件”而不是“繼承”,這點別混淆了。第三,通過改變上下文(就是 this),當一個元件的事件觸發時,由另一個元件處理。
由於上面省略了很多程式碼,一般還要考慮的情況有,怎麼取消監聽,怎麼實現例子中的 listenTo 等。
元件繼承
關於繼承,這篇文章略有提到(4.2 通過 prototype 實現繼承)。
這裡就寫個 F.extend 技巧好了。一般來說,會在繼承之後修改 prototype 的 constructor 屬性,並在它上面定義很多方法,就變成了:
A.prototype = new B(); A.prototype.constructor = A; A.prototype.f1 = function() {}; A.prototype.f2 = function() {};
大家不妨去實現一個 extend 方法,讓程式碼變成:
A.prototype = new B(); extend(A.prototype, { /* 原型上的方法和屬性 */ });
DOM 事件代理
一個元件往往會對應一個頁面區域,那在這個區域上會有單擊按鈕等一些 DOM 事件。由於在初始化元件時,這些元素還沒有追加到 DOM 上去,所以就不能使用 addEventListener 這個方法來監聽單擊事件。那要怎麼監聽呢?
兩種方法。一,在生成 HTML 片段時,設定元素的 onclick 屬性,比如:
container.innerHTML = '<a href="#" onclick="delegate(' + id + ')">click</a>';
技巧在於,這個 delegate 方法是全域性的,並且它能通過元件的 id 來找到對應的元件物件,再呼叫該元件的回撥函式。
二,在子元素新增到 DOM 之前,父容器是存在了的,所以可以對父容器監聽 click 事件,然後對 event.target 判斷。
addEventListener(container, 'click', delegate)
無論是哪種方法,具體實現時肯定會碰到問題,這些都是預期範圍內的,所以不用沮喪。
封裝 AJAX
同樣地,面試官極有可能問你“請用原生 JS 封裝 AJAX 的 GET 請求”。你應該已經熟稔於心,或者至少有筆記記錄了怎麼寫。
現在要討論的是,如何利用“訊息機制”去避免回撥。jQuery 中的 ajax 方法需要一個 success 的回撥,加上配置 url 等資訊,導致完成一次請求所用到的程式碼非常複雜,很難閱讀。ES 6 推出了 Promise,使得我們可以用同步的語法去做非同步的事,閱讀性得到了提升。
由於我們不能用 Promise,所以就發訊息吧,也很優雅。
F.extend(Request.prototype, { constructor: Request, get: function() { var xhr = createXHR(), self = this; xhr.open('GET', this._api, true); xhr.onreadystatechange = function() { if (xhr.readyState === 4 && xhr.status === 200) { self.emit('success', xhr.responseText); } }; xhr.send(); } });
程式碼並不全,說明一下,Request 繼承自 Event,構造時需要傳入一個 url,表示請求的地址。用法類似於:
// 在某個元件中,this 指向該物件的例項 var r = new Request('http://www.example.com/blogs'); r.get(); r.on('success', callback, this);
貌似有點像 Angular 中 new Resource(url); 的用法。
功能性相容 (Polyfill)
這部分主要是為了相容比如說 IE 6 不支援 HTML5 元素的樣式、陣列中的高階用法(forEach 和 map 等)、字串的高階用法(trim)、Function 的 bind 等。
因為是臨時編寫的框架,所以業務邏輯的程式碼中需要什麼,就補什麼。
相容 HTML5 元素你可以這麼做,很簡單:
document.createElement('header');
把所有用到的元素都 createElement 一遍就行了,這段程式碼必須放在 <head> 中。
至於相容 forEach、map、bind 這一些,網上應該有一大堆吧,這兒只是為了提醒各位去考慮這些方面。然後,網上的相容策略可能很複雜,沒必要,大家完全可以嘗試自己去寫,“過早的優化是萬惡之源”(這是個人最喜歡的名言了)。
淺談模板語言
這個雖然不是必須的,並且在我目前寫的程式碼中也沒有考慮到,但經過一位高人提醒,就覺得,咦,很多聽上去高大上的技術,從原理來講都是柴米油鹽這些基礎知識。
如果各位之前對 underscore 中的 _.template 方法並不瞭解,看完這節應該會幫助你一些。
假設要生成一個使用者名稱的連結,用模板可以這麼寫:
<a href="#">{{ name }}</a>
而用現在的方式是這麼做的:
var html = '<a href="#">' + model.name + '</a>';
那麼怎麼通過模板的方式去做,不用費勁地拼接字串呢?答案是正則。
function parse(template, model) { return template.replace(/\{\{\s*(.+?)\s*\}\}/g, function(match, p1) { return model[p1] || match; }); }
替換時,match 表示由正則匹配到的字串,這裡是 '{{ name }}',p1 表示匹配到的字串中第一個組的值,這裡是 'name',問號 ? 是阻止貪婪匹配,最後由返回值替換 match,這裡是 model.name 。
小結
文章中的程式碼可能只展示了一小部分,因為我主要是想說明一些值得考慮的點,並不是教程,至少大家可以用這些作為草稿去開始。
繞來繞去,JS 中的語法也屈指可數,那為什麼在學習新技術的時候會很焦灼呢?基礎是一個原因,沒有基礎就造不了任何建築;知識面是另一個,解決問題時最怕的是不知道有某個答案存在,使用 API 時最討厭的就是不知道它已經提供這個功能了。所以平時應該多看一些文章,有想法就記下來,無論是筆記的方式還是部落格的方式都行,寫部落格可以強迫你把想法表達出來,這跟“看懂”是不一樣的。
至於學習的價效比,我只能說,不要停!
你可能覺得,唉,React 是很好,但眼下又用不到,就算學了也沒用,還不如把時間花在績效上。自己寫程式碼永遠是個封閉的空間,包括因為遇到什麼問題被動地去 google 也好,如果不是主動去看新鮮事物,能力的增長是十分緩慢的。為什麼會不斷有新技術產生?這個事情本身就在告訴我們,需要用新的角度去解決新的問題(或者舊的)。舉兩個例子,IE 在 Windows 系統上是不會自動更新的,現在它死了(Windows 10 Edge);Adobe Flash 適應不了移動平臺,而安全漏洞又頻出,現在它馬上要死了(Adobe 的高層表示並不 care,因為 Flash 只佔很少一部分營收)。
網際網路它不跟你講人情的,適者生存。
相關文章
- 一個生產庫的DBA,你每天要做什麼?
- 對於小白,建立一個網站你需要做什麼?網站
- 我為什麼要做IT
- 你真的需要做一個App麼?APP
- 為什麼為什麼為什麼為什麼為什麼你要做一名程式設計師?程式設計師
- 何時需要做urlEncode,以及為什麼要做
- 為什麼你應該使用一個PHP框架PHP框架
- 從達摩院想什麼,到阿里要做什麼阿里
- 框架是個什麼東西?框架
- 為什麼要做聚合支付代理?
- 為什麼要做Redis分割槽?Redis
- 一家公司為什麼要做資料庫和AI兩個賽道?資料庫AI
- AngularJS - 下一個大框架AngularJS框架
- 為什麼要做程式碼審計?
- 為什麼要做介面測試?怎麼做?
- SpringBoot-2.3映象方案為什麼要做多個layerSpring Boot
- Koala Framework是什麼?我為什麼要寫這個框架?Framework框架
- 網站為什麼要做“等保”?怎麼做?網站
- 企業為什麼要做資料整合?
- 為什麼要做資料視覺化視覺化
- 伺服器到手後需要做什麼伺服器
- 我們為什麼要做一名系統管理員?
- 什麼是框架?為什麼說 Angular 是框架?框架Angular
- 要做一個 “有思路” 的猿!
- UI設計要做什麼,UI設計培訓都要學什麼UI
- 為什麼 Web 開發人員需要學習一個 JavaScript 框架?WebJavaScript框架
- 帶大家來看一下為什麼要做企業簽名
- 為什麼要做網路安全評估?多久進行一次?
- 什麼是模式? 什麼是框架?模式框架
- 企業為什麼要做應用多活?
- 為什麼企業要做大規模敏捷?敏捷
- 網站被挾持了需要做什麼網站
- 什麼是框架框架
- 前端工程師要做什麼?前端工程師需要什麼根本技能?前端工程師
- 雲端計算都要學什麼?學好Linux需要做些什麼?Linux
- 日誌脫敏是什麼意思?為什麼要做日誌脫敏?用什麼工具好?
- 跨境電商為什麼一定要做好電子郵件營銷
- 曾一度熱門的品牌APP在今天需要做對什麼APP