首先是定义缺省的两个页面片断(缺省页面和出错页面,这两个页面是基础功能,因此放在库里)相关代码,对每一个片断对应的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>