首先是定義缺省的兩個頁面片斷(缺省頁面和出錯頁面,這兩個頁面是基礎功能,因此放在庫裏)相關代碼,對每一個片斷對應的url(例如home
)定義一個同名的對象,裏面存放了對應的 html 片斷文件路徑、初始化方法。css
var home = {}; //default partial page, which will be loaded initially home.partial = "lib/home.html"; home.init = function(){ //bootstrap method //nothing but static content only to render } var notfound = {}; //404 page notfound.partial = "lib/404.html"; notfound.init = function(){ alert('URL does not exist. please check your code.'); }
隨後是全局變量,包含了 html 片斷代碼的緩存、局部刷新所在 div 的 DOM 對象和向後端服務請求返回的根數據(rootScope
,初始化時未出現,在後面的方法中才會用到):html
var settings = {}; //global parameters settings.partialCache = {}; //cache for partial pages settings.divDemo = document.getElementById("demo"); //div for loading partials, defined in index.html
下面就是主程序了,全部的公用方法打包放到一個對象miniSPA
中,這樣能夠避免污染命名空間:node
// Main Object here var miniSPA = {};
而後是 changeUrl 方法,對應在index.html
中有以下觸發定義:git
<body onhashchange="miniSPA.changeUrl();">
onhashchange
是在location.hash發生改變的時候觸發的事件,可以經過它獲取局部 url 的改變。在index.html
中定義了以下的連接:github
<h1> Demo Contents:</h1> <a href="#home">Home (Default)</a> <a href="#postMD">POST request</a> <a href="#getEmoji">GET request</a> <a href="#wrong">Invalid url</a> <div id="demo"></div>
每一個 url 都以#
號開頭,這樣就能被onhashchange
事件抓取到。最後的 div 就是局部刷新的 html 片斷嵌入的位置。ajax
miniSPA.changeUrl = function() { //handle url change var url = location.hash.replace('#',''); if(url === ''){ url = 'home'; //default page } if(! window[url]){ url = "notfound"; } miniSPA.ajaxRequest(window[url].partial, 'GET', '',function(status, page){ if(status == 404){ url = 'notfound'; //404 page miniSPA.ajaxRequest(window[url].partial,'GET','',function(status, page404){ settings.divDemo.innerHTML = page404; miniSPA.initFunc(url); //load 404 controller }); } else{ settings.divDemo.innerHTML = page; miniSPA.initFunc(url); //load url controller } }); }
上面的代碼先獲取改變後的 url,先經過window[url]
找到對應的對象(相似於最上部定義的home
和notfound
),如對象不存在(無定義的路徑)則轉到404
處理,不然經過ajaxRequest
方法獲取window[url].partial
中定義的 html 片斷並加載到局部刷新的div,並執行window[url].init
初始化方法。正則表達式
ajaxRequest
方法主要是和後端的服務進行交互,經過XMLHttpRequest
發送請求(GET
或POST
),若是獲取的是 html 片斷就把它緩存到settings.partialCache[url]
裏,由於 html 片斷是相對固定的,每次請求返回的內容不會變化。若是是其餘請求(好比向 Github 的 markdown 服務 POST 一個字符串)就不能緩存了。bootstrap
miniSPA.ajaxRequest = function(url, method, data, callback) { //load partial page if(settings.partialCache[url]){ callback(200, settings.partialCache[url]); } else { var xmlhttp; if(window.XMLHttpRequest){ xmlhttp = new XMLHttpRequest(); xmlhttp.open(method, url, true); if(method === 'POST'){ xmlhttp.setRequestHeader("Content-type","application/x-www-form-urlencoded"); } xmlhttp.send(data); xmlhttp.onreadystatechange = function(){ if(xmlhttp.readyState == 4){ switch(xmlhttp.status) { case 404: //if the url is invalid, show the 404 page url = 'notfound'; break; default: var parts = url.split('.'); if(parts.length>1 && parts[parts.length-1] == 'html'){ //only cache static html pages settings.partialCache[url] = xmlhttp.responseText; //cache partials to improve performance } } callback(xmlhttp.status, xmlhttp.responseText); } } } else{ alert('Sorry, your browser is too old to run this app.') callback(404, {}); } } }
對於不支持XMLHttpRequest
的瀏覽器(主要是 IE 老版本),原本是能夠在 else 里加上xmlhttp = new ActiveXObject(‘Microsoft.XMLHTTP’);的,不過,我手頭也沒有那麼多老版本 IE 用於測試,並且老版本 IE 原本就是我深惡痛絕的東西,憑什麼要支持它啊?因此就乾脆直接給個alert
完事。後端
render
方法通常在每一個片斷的初始化方法中調用,它會設定全局變量中的根對象,並經過refresh
方法渲染 html 片斷。api
miniSPA.render = function(url){ settings.rootScope = window[url]; miniSPA.refresh(settings.divDemo, settings.rootScope); }
獲取後端數據後,如何渲染 html 片斷是個比較複雜的問題。這就是 DOM 操做了。整體思想就是從 html 片斷的根部入手,遍歷 DOM 樹,逐個替換屬性和文本中的佔位變量(例如<img src="emojis.value">
和<p>{{emojis.key}}</p>
),匹配和替換是在feedData
方法中完成的。
這裏最麻煩的是data-repeat
屬性,這是爲了批量渲染格式相同的一組元素用的。好比從 Github 獲取了全套的 emoji 表情,共計 888 個(也許下次升級到1000個),就須要渲染 888 個元素,把 888 個圖片及其說明放到 html 片斷中去。而 html 片斷中對此只有一條定義:
<ul> <li data-repeat="emojis" data-item="data"> <figure> <img src='{{data.value}}' width='100' height='100'> <figcaption>{{data.key}}</figcaption> </figure> </li> </ul>
等 888 個 emoji 表情來了以後,就要自動把<li>
元素擴展到 888 個。這就須要先clone
定義好的元素,而後根據後臺返回的數據逐個替換元素中的佔位變量。
miniSPA.refresh = function(node, scope) { var children = node.childNodes; if(node.nodeType != 3){ //traverse child nodes, Node.TEXT_NODE == 3 for(var k=0; k<node.attributes.length; k++){ node.setAttribute(node.attributes[k].name, miniSPA.feedData(node.attributes[k].value, scope)); //replace variables defined in attributes } var childrenCount = children.length; for(var j=0; j<childrenCount; j++){ if(children[j].nodeType != 3 && children[j].hasAttribute('data-repeat')){ //handle repeat items var item = children[j].dataset.item; var repeat = children[j].dataset.repeat; children[j].removeAttribute('data-repeat'); var repeatNode = children[j]; for(var prop in scope[repeat]){ repeatNode = children[j].cloneNode(true); //clone sibling nodes for the repeated node node.appendChild(repeatNode); var repeatScope = scope; var obj = {}; obj.key = prop; obj.value = scope[repeat][prop]; //add the key/value pair to current scope repeatScope[item] = obj; miniSPA.refresh(repeatNode,repeatScope); //iterate over all the cloned nodes } node.removeChild(children[j]); //remove the empty template node } else{ miniSPA.refresh(children[j],scope); //not for repeating, just iterate the child node } } } else{ node.textContent = miniSPA.feedData(node.textContent, scope); //replace variables defined in the template } }
從上面的代碼能夠看到,refresh
方法是一個遞歸執行的函數,每次處理當前 node 以後,還會遞歸處理全部的孩子節點。經過這種方式,就能把模板中定義的全部元素的佔位變量都替換爲真實數據。
feedData
用來替換文本節點中的佔位變量。它經過正則表達式獲取{{...}}
中的內容,並把多級屬性(例如data.map.value
)切分開,逐級循環處理,直到最底層得到相應的數據。
miniSPA.feedData = function(template, scope){ //replace variables with data in current scope return template.replace(/\{\{([^}]+)\}\}/gmi, function(model){ var properties = model.substring(2,model.length-2).split('.'); //split all levels of properties var result = scope; for(var n in properties){ if(result){ switch(properties[n]){ //move down to the deserved value case 'key': result = result.key; break; case 'value': result = result.value; break; case 'length': //get length from the object var length = 0; for(var x in result) length ++; result = length; break; default: result = result[properties[n]]; } } } return result; }); }
initFunc
方法的做用是解析片斷對應的初始化方法,判斷其類型是否爲函數,並執行它。這個方法是在changeUrl
方法裏調用的,每次訪問路徑的變化都會觸發相應的初始化方法。
miniSPA.initFunc = function(partial) { //execute the controller function responsible for current template var fn = window[partial].init; if(typeof fn === 'function') { fn(); } }
最後是miniSPA
庫自身的初始化。很簡單,就是先獲取404.html
片斷並緩存到settings.partialCache.notfound
中,以便在路徑變化時使用。當路徑不合法時,就會從緩存中取出404片斷並顯示在局部刷新的 div 中。
miniSPA.ajaxRequest('lib/404.html', 'GET','',function(status, partial){ settings.partialCache.notfound = partial; }); //cache 404 page first
好了,核心的代碼就是這麼多。整個 js 文件才區區 155 行,比起那些動輒幾萬行的框架是否是簡單得不能再簡單了?
有了上面的miniSPA.js
代碼以及配套的404.html
和home.html
,並把它們打包放在lib
目錄下,下面就能夠來看個人應用裏有啥內容。
說到應用那就更簡單了,app.js
一共30行,實現了一個GET
和一個POST
訪問。
首先是getEmoji
對象,定義了一個 html 片斷文件路徑和一個初始化方法。初始化方法中分別調用了miniSPA
中的ajaxRequest
方法(用於獲取 Github API 提供的 emoji 表情數據, JSON格式)和render
方法(用來渲染對應的 html 片斷)。
var getEmoji = {}; getEmoji.partial = "getEmoji.html" getEmoji.init = function(){ document.getElementById('spinner').style.visibility = 'visible'; document.getElementById('content').style.visibility = 'hidden'; miniSPA.ajaxRequest('https://api.github.com/emojis','GET','',function(status, partial){ getEmoji.emojis = JSON.parse(partial); miniSPA.render('getEmoji'); //render related partial page with data returned from the server document.getElementById('content').style.visibility = 'visible'; document.getElementById('spinner').style.visibility = 'hidden'; }); }
而後是postMD
對象,它除了 html 片斷文件路徑和初始化方法(由於初始化不須要獲取外部數據,因此只須要調用render
方法就能夠了)以外,重點在於submit
方法。submit
會把用戶提交的輸入文本和其餘兩個選項打包 POST 給 Github 的 markdown API,並獲取後臺解析標記返回的 html。
var postMD = {}; postMD.partial = "postMD.html"; postMD.init = function(){ miniSPA.render('postMD'); //render related partial page } postMD.submit = function(){ document.getElementById('spinner').style.visibility = 'visible'; var mdText = document.getElementById('mdText'); var md = document.getElementById('md'); var data = '{"text":"'+mdText.value.replace(/\n/g, '<br>')+'","mode": "gfm","context": "github/gollum"}'; miniSPA.ajaxRequest('https://api.github.com/markdown', 'POST', data,function(status, page){ document.getElementById('spinner').style.visibility = 'hidden'; md.innerHTML = page; //render markdown partial returned from the server }); mdText.value = ''; } miniSPA.changeUrl(); //initialize
這兩個對象對應的 html 片斷以下:
getEmoji.html :
<h2>GET request: Fetch emojis from Github pulic API.</h2> <p> This is a list of emojis get from https://api.github.com/emojis: </p> <i id="spinner" class="csspinner duo"></i> <span id="content"> <h4>Get <strong class="highlight">{{emojis.length}}</strong> items totally.</h4> <hr> <ul> <li data-repeat="emojis" data-item="data"> <figure> <img src='{{data.value}}' width='100' height='100'> <figcaption>{{data.key}}</figcaption> </figure> </li> </ul> </span>
postMD.html :
<h2> POST request: send MD text and get rendered HTML</h2> <p> markdown text here (for example: <strong>Hello world github/linguist#1 **cool**, and #1! </strong>): </p> <textarea id="mdText" cols="80" rows="6"></textarea> <button onclick="postMD.submit();">submit</button> <hr> <h4>Rendered elements from Github API (https://api.github.com/markdown):</h4> <i id="spinner" class="csspinner duo"></i> <div id="md"></div>