• 开发无框架单页面应用


    基础对象

    首先是定义缺省的两个页面片段(缺省页面和出错页面,这两个页面是基础功能,所以放在库里)相关代码,对每个片段对应的url(例如home)定义一个同名的对象,里面存放了对应的 html 片段文件路径、初始化方法。

    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,初始化时未出现,在后面的方法中才会用到):

    var settings = {};               //global parameters
    settings.partialCache = {};      //cache for partial pages
    settings.divDemo = document.getElementById("demo");      //div for loading partials, defined in index.html

    主程序

    下面就是主程序了,所有的公用方法打包放到一个对象miniSPA中,这样可以避免污染命名空间:

    // Main Object here
    var miniSPA = {};

    然后是 changeUrl 方法,对应在index.html中有如下触发定义:

    <body onhashchange="miniSPA.changeUrl();">

    onhashchange是在location.hash发生改变的时候触发的事件,能够通过它获取局部 url 的改变。在index.html中定义了如下的链接:

    <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 片段嵌入的位置。

    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]找到对应的对象(类似于最上部定义的homenotfound),如对象不存在(无定义的路径)则转到404处理,否则通过ajaxRequest方法获取window[url].partial中定义的 html 片段并加载到局部刷新的div,并执行window[url].init初始化方法。

    ajaxRequest方法主要是和后端的服务进行交互,通过XMLHttpRequest发送请求(GETPOST),如果获取的是 html 片段就把它缓存到settings.partialCache[url]里,因为 html 片段是相对固定的,每次请求返回的内容不会变化。如果是其他请求(比如向 Github 的 markdown 服务 POST 一个字符串)就不能缓存了。

    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 片段。

    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.htmlhome.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(/
    /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>
  • 相关阅读:
    multilabel-multiclass classifier
    关于zabbix _get返回Could not attach to pid的问题
    python导出环境依赖到req,txt文件中
    inode满的解决方法
    搞定面试官:咱们从头到尾再说一次 Java 垃圾回收
    SpringBoot项目,如何优雅的把接口参数中的空白值替换为null值?
    万万没想到,JVM内存区域的面试题也可以问的这么难?
    万万没想到,面试中,连 ClassLoader类加载器 也能问出这么多问题…..
    npm私服verdaccio报sha错误的解决方案
    配置SQL Server 2016无域AlwaysOn(转)
  • 原文地址:https://www.cnblogs.com/jiangxiaobo/p/6340606.html
Copyright © 2020-2023  润新知