一個 JS 框架需要做什么
為什么這么說?不知道各位有沒有發(fā)現(xiàn),雖然前端發(fā)展快,但一些有名的框架至少會火熱很長時間,比如 Backbone、React、Ember 。如果有心要學(xué),肯定有足夠的時間把它學(xué)會,畢竟事實(shí)擺在面前,很多公司的上線產(chǎn)品就是用 React 來寫的,比如 Teambition 的簡聊,貌似它是從 Backbone 重構(gòu)過來的。然而,很多同學(xué)在接手新項(xiàng)目時,常常會不知所措,不知道用什么技術(shù)去做,或者說,只依賴于擅長的技術(shù),就算在一些場景中它可能并不是最適合 的。
因?yàn)檫@些同學(xué)平時不夠努力嗎?不是吧。他們可能會看書到很晚,瀏覽很多博客,就是為了去了解 CORS 的應(yīng)用,或者是想知道為什么 Angular 中的 scope 在某些時候不能雙向綁定了。對,時間是花了,但遇到問題還是一頭霧水??赡芮岸司褪沁@么一份工作吧,慫恿你去學(xué)游泳,蛙泳、自由泳、蝶泳,海嘯來了照樣被 沖走……
那這篇文章要說的是什么呢?就是假設(shè)你現(xiàn)在什么都沒學(xué),就靠基本功,去完成一個靜態(tài)頁面,當(dāng)然也有業(yè)務(wù)邏輯,包括數(shù)據(jù)的 CRUD、動畫,怎么做?有個關(guān)于 VanillaJS 的梗不知道大家看過沒,你一定會會心一笑的。
沒有 jQuery 了,沒有 Bootstrap 了,扔掉所有你引以為傲的武器,但大惡魔 IE 6 還在。具體的需求不給了,反正給了你們也不會照著去實(shí)現(xiàn),真有心要做的話,可以做一個 todo app 吧。
DOM 查詢
在沒有第三方框架可以用的時候,如果真的按照功能列表,從第一條實(shí)現(xiàn)到最后一條,每個模塊用自執(zhí)行匿名函數(shù)包起來,所有代碼寫在一個文件中,看上去十分合理,但真這么做的話,恐怕你會瘋掉吧。哦,好處是你可以跟別人吹噓今天寫了三四百行代碼,產(chǎn)量很高呢!
所以,不使用第三方框架,我們可以自己寫,它的功能只要符合應(yīng)用場景就可以了,不用去考慮各種不會發(fā)生的奇葩情況。
好,開始。我們最依賴的功能是通過 CSS 選擇器獲取相應(yīng)的 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 事件
如果有同學(xué)參加過面試的話,我想“怎么去監(jiān)聽一個 DOM 事件?請盡可能考慮瀏覽器兼容性”這個問題是經(jīng)常會問到吧。這兒寫一個可行方案吧。
- / 監(jiān)聽 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
- }
- }
我知道大家可能有更好的,或者更完善的方案,但抱歉這里討論的重點(diǎn)不是它。
關(guān)于 DOM 事件方面,還有一些有用的方法,比如 preventDefault 和 stopPropagation 也可以自己去封裝一下。然后這兒想討論一下 DOM 加載完成的事件。jQuery 中我們會這么用:
- $(function() {
- // ready
- });
如果我們也想封裝一個類似的方法,可能會這么寫:
addEventListener('window', 'load', callback);
可是 load 事件是在什么情況下觸發(fā)的呢?當(dāng)頁面上的所有資源,包括圖片,加載完之后才觸發(fā)!也就是說,如果圖片很多,網(wǎng)速很慢,那觸發(fā) load 要花很長時間。在本地調(diào)試時不會有這種延遲的問題,所以往往會被忽略。
那怎么改正呢?第一,可以把 <script> 放到 <body> 中所有元素的下方,就不需要監(jiān)聽任何“加載完成”的事件了。第二,監(jiān)聽 DOMContentLoaded 事件,IE 9+ 支持。至于如何兼容低版本瀏覽器,可以看這篇文章 (addDOMLoadEvent)。
#p#
組件式開發(fā)
“組件”這個詞其實(shí)來源于很多框架,比如 Backbone 中的 View,React 就更不用說了,它為了組件化專門規(guī)定了 JSX(當(dāng)然它有更宏偉的 goal) 。我們這里討論的組件也是差不多的意思,就是按照功能,把頁面上分成一個個獨(dú)立的模塊,模塊之間通過消息(事件)進(jìn)行溝通。關(guān)于模塊耦合,JSX 是通過類似于 HTML 標(biāo)簽嵌套的方式來表現(xiàn)的,而我們自然沒這么高級,就直接把依賴的模塊注入到其他模塊中,比如:
- /**
- * 應(yīng)用頂層,構(gòu)造一些頁面中用到的組件
- */
- 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 是發(fā)布日志的組件,BlogList 是日志列表。發(fā)布日志后必然會顯示到列表中,所以在構(gòu)造日志列表時,會把 BlogPost 注入到 BlogList 中。
每個組件可以提供一個 id 選擇器,表示該組件需要繪制在哪個元素內(nèi)。
消息傳播機(jī)制
關(guān)于 BlogPost 和 BlogList,大家可以想象微博的主頁,它上面是一個發(fā)布框,下面是微博列表,就是這樣一個界面。
當(dāng)微博發(fā)布之后,列表中需要增加新發(fā)布的內(nèi)容,這個過程是誰給誰發(fā)消息?按照面向?qū)ο蟮乃枷耄瑧?yīng)該是類似于這樣:
// 在 發(fā)布框組件 中調(diào)用 列表組件 的方法
blogList.add(item);
顯然是 BlogPost 依賴于 BlogList 對嗎?但貌似我們上面的代碼不是這個邏輯,而是反過來。那么實(shí)際情況就成了這樣:
// 發(fā)布框:BlogPost 中觸發(fā)事件
this.emit('add', item);
// 列表:BlogList 中監(jiān)聽事件
this.listenTo(blogPost, 'add', handler);
// 由 handler 處理發(fā)布事件
嗯,代碼變多了,看來得強(qiáng)行圓回來。
為什么我強(qiáng)烈建議使用后者?假設(shè)過了一段時間,某個充滿創(chuàng)意的策劃突然告訴你,當(dāng)發(fā)布微博之后,可以顯示到朋友圈(假設(shè)有這么個東西)。那么前者的方式會怎么做?是不是首先給這個發(fā)布框多注入一個依賴,即朋友圈,然后調(diào)用朋友圈的某個方法?
如果再過段時間,又有新創(chuàng)意了,是不是又得給發(fā)布框加依賴了?最后搞得發(fā)布框依賴于微博列表、依賴于朋友圈、依賴于其他 8 個組件,真不想用水性楊花來形容它。
這個問題很常見吧?如果用消息機(jī)制的方式就會好很多,只需要在新增加的組件中監(jiān)聽發(fā)布框的 'add' 事件就可以了。
如果你能接受這個方式,可能想知道怎么去簡單地實(shí)現(xiàn)它。
- var Event = F.Event = function Event() {
- // 該組件相關(guān)的所有的事件都保存在 _events 對象中
- // 格式 - {'eventName': [{handler, context}*]}
- this._events = {};
- };
- F.extend(Event.prototype, {
- // 監(jiān)聽事件
- on: function(event, handler, context) {
- if (!this._events[event]) {
- this._events[event] = [];
- }
- this._events[event].push({
- handler: handler,
- context: context || this
- });
- },
- // 觸發(fā)事件
- emit: function(event) {
- var events = this._events[event] || [],
- args = [];
- // 第一個參數(shù)為事件名,后面的參數(shù)需要傳給處理該事件的方法,記錄到 args 中
- if (arguments.length > 1) {
- args = slice.call(arguments, 1);
- }
- // 回調(diào)時需要傳入?yún)?shù)
- events.forEach(function(v) {
- v.handler.apply(v.context, args);
- });
- }
- }
把重點(diǎn)部分貼了一下。第一,這個 Event 是所有組件的基類,所以每個組件都有 on 和 emit 方法。第二,F(xiàn).extend 的作用就是把后面對象的方法和屬性直接賦值給第一個,extend 的意思是“擴(kuò)展”而不是“繼承”,這點(diǎn)別混淆了。第三,通過改變上下文(就是 this),當(dāng)一個組件的事件觸發(fā)時,由另一個組件處理。
由于上面省略了很多代碼,一般還要考慮的情況有,怎么取消監(jiān)聽,怎么實(shí)現(xiàn)例子中的 listenTo 等。
組件繼承
關(guān)于繼承,這篇文章略有提到(4.2 通過 prototype 實(shí)現(xiàn)繼承)。
這里就寫個 F.extend 技巧好了。一般來說,會在繼承之后修改 prototype 的 constructor 屬性,并在它上面定義很多方法,就變成了:
- A.prototype = new B();
- A.prototype.constructor = A;
- A.prototype.f1 = function() {};
- A.prototype.f2 = function() {};
大家不妨去實(shí)現(xiàn)一個 extend 方法,讓代碼變成:
- A.prototype = new B();
- extend(A.prototype, { /* 原型上的方法和屬性 */ });
DOM 事件代理
一個組件往往會對應(yīng)一個頁面區(qū)域,那在這個區(qū)域上會有單擊按鈕等一些 DOM 事件。由于在初始化組件時,這些元素還沒有追加到 DOM 上去,所以就不能使用 addEventListener 這個方法來監(jiān)聽單擊事件。那要怎么監(jiān)聽呢?
兩種方法。一,在生成 HTML 片段時,設(shè)置元素的 onclick 屬性,比如:
container.innerHTML = '<a href="#" onclick="delegate(' + id + ')">click</a>';
技巧在于,這個 delegate 方法是全局的,并且它能通過組件的 id 來找到對應(yīng)的組件對象,再調(diào)用該組件的回調(diào)函數(shù)。
二,在子元素添加到 DOM 之前,父容器是存在了的,所以可以對父容器監(jiān)聽 click 事件,然后對 event.target 判斷。
addEventListener(container, 'click', delegate)
無論是哪種方法,具體實(shí)現(xiàn)時肯定會碰到問題,這些都是預(yù)期范圍內(nèi)的,所以不用沮喪。
封裝 AJAX
同樣地,面試官極有可能問你“請用原生 JS 封裝 AJAX 的 GET 請求”。你應(yīng)該已經(jīng)熟稔于心,或者至少有筆記記錄了怎么寫。
現(xiàn)在要討論的是,如何利用“消息機(jī)制”去避免回調(diào)。jQuery 中的 ajax 方法需要一個 success 的回調(diào),加上配置 url 等信息,導(dǎo)致完成一次請求所用到的代碼非常復(fù)雜,很難閱讀。ES 6 推出了 Promise,使得我們可以用同步的語法去做異步的事,閱讀性得到了提升。
由于我們不能用 Promise,所以就發(fā)消息吧,也很優(yōu)雅。
- 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,構(gòu)造時需要傳入一個 url,表示請求的地址。用法類似于:
// 在某個組件中,this 指向該對象的實(shí)例
var r = new Request('http://www.example.com/blogs');
r.get();
r.on('success', callback, this);
貌似有點(diǎn)像 Angular 中 new Resource(url); 的用法。
功能性兼容 (Polyfill)
這部分主要是為了兼容比如說 IE 6 不支持 HTML5 元素的樣式、數(shù)組中的高級用法(forEach 和 map 等)、字符串的高級用法(trim)、Function 的 bind 等。
因?yàn)槭桥R時編寫的框架,所以業(yè)務(wù)邏輯的代碼中需要什么,就補(bǔ)什么。
兼容 HTML5 元素你可以這么做,很簡單:
document.createElement('header');
把所有用到的元素都 createElement 一遍就行了,這段代碼必須放在 <head> 中。
至于兼容 forEach、map、bind 這一些,網(wǎng)上應(yīng)該有一大堆吧,這兒只是為了提醒各位去考慮這些方面。然后,網(wǎng)上的兼容策略可能很復(fù)雜,沒必要,大家完全可以嘗試自己去寫,“過早的優(yōu)化是萬惡之源”(這是個人最喜歡的名言了)。
淺談模板語言
這個雖然不是必須的,并且在我目前寫的代碼中也沒有考慮到,但經(jīng)過一位高人提醒,就覺得,咦,很多聽上去高大上的技術(shù),從原理來講都是柴米油鹽這些基礎(chǔ)知識。
如果各位之前對 underscore 中的 _.template 方法并不了解,看完這節(jié)應(yīng)該會幫助你一些。
假設(shè)要生成一個用戶名的鏈接,用模板可以這么寫:
<a href="#">{{ name }}</a>
而用現(xiàn)在的方式是這么做的:
var html = '<a href="#">' + model.name + '</a>';
那么怎么通過模板的方式去做,不用費(fèi)勁地拼接字符串呢?答案是正則。
- function parse(template, model) {
- return template.replace(/\{\{\s*(.+?)\s*\}\}/g, function(match, p1) {
- return model[p1] || match;
- });
- }
替換時,match 表示由正則匹配到的字符串,這里是 '{{ name }}',p1 表示匹配到的字符串中第一個組的值,這里是 'name',問號 ? 是阻止貪婪匹配,最后由返回值替換 match,這里是 model.name 。
小結(jié)
文章中的代碼可能只展示了一小部分,因?yàn)槲抑饕窍胝f明一些值得考慮的點(diǎn),并不是教程,至少大家可以用這些作為草稿去開始。
繞來繞去,JS 中的語法也屈指可數(shù),那為什么在學(xué)習(xí)新技術(shù)的時候會很焦灼呢?基礎(chǔ)是一個原因,沒有基礎(chǔ)就造不了任何建筑;知識面是另一個,解決問題時最怕的是不知道有某 個答案存在,使用 API 時最討厭的就是不知道它已經(jīng)提供這個功能了。所以平時應(yīng)該多看一些文章,有想法就記下來,無論是筆記的方式還是博客的方式都行,寫博客可以強(qiáng)迫你把想法表 達(dá)出來,這跟“看懂”是不一樣的。
至于學(xué)習(xí)的性價比,我只能說,不要停!
你可能覺得,唉,React 是很好,但眼下又用不到,就算學(xué)了也沒用,還不如把時間花在績效上。自己寫代碼永遠(yuǎn)是個封閉的空間,包括因?yàn)橛龅绞裁磫栴}被動地去 google 也好,如果不是主動去看新鮮事物,能力的增長是十分緩慢的。為什么會不斷有新技術(shù)產(chǎn)生?這個事情本身就在告訴我們,需要用新的角度去解決新的問題(或者舊 的)。舉兩個例子,IE 在 Windows 系統(tǒng)上是不會自動更新的,現(xiàn)在它死了(Windows 10 Edge);Adobe Flash 適應(yīng)不了移動平臺,而安全漏洞又頻出,現(xiàn)在它馬上要死了(Adobe 的高層表示并不 care,因?yàn)?Flash 只占很少一部分營收)。
互聯(lián)網(wǎng)它不跟你講人情的,適者生存。