开发无框架单页面应用 — 老码农的祖传秘方

什么是单页面应用(SPA)?

维基百科上的描述是这样的:

也就是说,单页面应用是仅包含单个网页的应用,目的是为了提供类似于本地应用的流畅用户体验。

需不需要框架?

要实现单页面应用,现在已经有很多现成的框架了,比如AngularJSEmber.jsBackbone.js等等。它们都是很全面的开发平台,为单页面应用开发提供了必需的页面模板、路径解析和处理、后台服务api访问、DOM操作等功能。

事实上,现代的web应用开发基本都离不开一个甚至多个框架,开发无框架应用的想法听起来蛮不靠谱的,对吧?

但是我总觉得现在是时候抛弃框架了。前两年我都在用AngularJS做开发,可以说已经比较熟悉它了,我的第一个单页面应用就是在AngularJS的启发下做出来的。框架曾经是我的挚爱。

但是现在每次看着它们那庞大臃肿的身躯和晦涩的语法,我都会想到诸葛亮的那句名言:“好累,感觉不会再爱了”。还有不同框架下各种工具、插件难以混用的现状,让我不得不经常需要自己写原生代码解决很多问题。时间长了,我自然冒出一个想法:“为啥不干脆抛弃框架,直接写原生代码呢?毕竟,框架也是原生代码写出来的嘛。”

怎么实现无框架SPA?

在微博里表达了这个想法之后,有不少朋友提出了各种意见和建议,我非常感谢。其中还有个小朋友评论道:“我看到了一个从大型机到web的大叔,在抠性能[偷笑]这是职业病嘛。”。看到这条评论,我含笑不语。

这种职业病在我们从90年代过来的老码农里还是比较普遍的,当年内存64K,磁盘360K,必须精打细算才能过日子。1个byte要掰成2个4位用,链表要自己实现,每一K内存里放了啥都门清。后来工作了,在ES/9000上做开发,系统资源也是非常金贵的。

记得有一次我们单位因为某个数据库应用系统吃内存太厉害,找IBM加了128K内存,一下子就花了60多万人民币,60多万哪!当时我的心在滴血:“把钱给我一半,我帮你们优化一下,省下这些内存行不?”。后来有机会瞻仰了一下那个系统的代码,我滴个妈呀,无数的join操作,当时骂娘的心都有了,但代码是我们部门一位元老写的,我一个新来的菜鸟惹不起…

总之,那时写代码是艺术,现在有的同学动不动就把一堆东西全load到内存里,反正内存不够了就加,这不是败家子么!哼!(老码农倚老卖老,不能算新闻)

好了,一不小心扯远了,还是说单页面应用的事情。

总之,无框架单页面应用看似可行,但难度有多大?我还是心里没底,需要一点理论依据给自己壮胆。所以我就在网上到处寻摸了一番,偶然找到了这篇 Google 工程师 Joe Gregorio 写的文章《别再用JS框架了》,里面的分析有一种与我心有戚戚的感觉,看完还给它翻译成中文了。

不过,他提出的方法是更超前的,例如 imports 和 Polymer,我曾经试过,印象中只有 Google 的 Chrome Canary 才有支持,而且要先在选项中打开一些试验功能,浏览器会变得不那么稳定。而 X-Tag 和 Bosonic 也要依赖于一个小的库。而我想做的是现在的浏览器就已经能支持的功能,用原生代码来实现。所以他这篇文章只能让我坚定方向,但是具体的做法还得靠自己去发现。

后来又看了几篇比较偏学术的文章,例如这篇 Mixu 写的《Single page apps in depth》,对我也不太适用。他的模板都需要先编译为JS对象存放,和 AngularJS 的方法类似,但我觉得在一个小规模应用里应该有更加优雅的实现方法。

找了好几天文档,我突然意识到自己浪费了不少时间。所谓理论依据应该是高层次的,解决可行性的问题,剩下的就是自己去想办法实现了。可行性不是明摆着的嘛,那么多框架不也是用原生代码实现的么?

想到这儿,我就开始自己尝试了。前后一共只花了两三天时间,写出来一共一百多行JS,就基本解决了问题。其实把代码写完了回顾一下,这些方法都算不上什么创新,都是标准的东西而已。肯定有别人也这么做了,只是我不知道而已吧。

可能有读者看到这儿不耐烦了:“Talk is cheap. Show me the code.”好吧,下面就是代码的描述。

老码农的实现方法

基础对象

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

随后是全局变量,包含了 html 片段代码的缓存、局部刷新所在 div 的 DOM 对象和向后端服务请求返回的根数据(rootScope,初始化时未出现,在后面的方法中才会用到):

主程序

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

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

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

每个 url 都以#号开头,这样就能被onhashchange事件抓取到。最后的 div 就是局部刷新的 html 片段嵌入的位置。

上面的代码先获取改变后的 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 一个字符串)就不能缓存了。

对于不支持XMLHttpRequest的浏览器(主要是 IE 老版本),本来是可以在 else 里加上xmlhttp = new ActiveXObject(‘Microsoft.XMLHTTP’);的,不过,我手头也没有那么多老版本 IE 用于测试,而且老版本 IE 本来就是我深恶痛绝的东西,凭什么要支持它啊?所以就干脆直接给个alert完事。

render方法一般在每个片段的初始化方法中调用,它会设定全局变量中的根对象,并通过refresh方法渲染 html 片段。

获取后端数据后,如何渲染 html 片段是个比较复杂的问题。这就是 DOM 操作了。总体思想就是从 html 片段的根部入手,遍历 DOM 树,逐个替换属性和文本中的占位变量(例如<img src="emojis.value"><p>{{emojis.key}}</p>),匹配和替换是在feedData方法中完成的。

这里最麻烦的是data-repeat属性,这是为了批量渲染格式相同的一组元素用的。比如从 Github 获取了全套的 emoji 表情,共计 888 个(也许下次升级到1000个),就需要渲染 888 个元素,把 888 个图片及其说明放到 html 片段中去。而 html 片段中对此只有一条定义:

等 888 个 emoji 表情来了之后,就要自动把<li>元素扩展到 888 个。这就需要先clone定义好的元素,然后根据后台返回的数据逐个替换元素中的占位变量。

从上面的代码可以看到,refresh方法是一个递归执行的函数,每次处理当前 node 之后,还会递归处理所有的孩子节点。通过这种方式,就能把模板中定义的所有元素的占位变量都替换为真实数据。

feedData用来替换文本节点中的占位变量。它通过正则表达式获取{{...}}中的内容,并把多级属性(例如data.map.value)切分开,逐级循环处理,直到最底层获得相应的数据。

initFunc方法的作用是解析片段对应的初始化方法,判断其类型是否为函数,并执行它。这个方法是在changeUrl方法里调用的,每次访问路径的变化都会触发相应的初始化方法。

最后是miniSPA库自身的初始化。很简单,就是先获取404.html片段并缓存到settings.partialCache.notfound中,以便在路径变化时使用。当路径不合法时,就会从缓存中取出404片段并显示在局部刷新的 div 中。

好了,核心的代码就是这么多。整个 js 文件才区区 155 行,比起那些动辄几万行的框架是不是简单得不能再简单了?

有了上面的miniSPA.js代码以及配套的404.htmlhome.html,并把它们打包放在lib目录下,下面就可以来看我的应用里有啥内容。

应用代码

说到应用那就更简单了,app.js一共30行,实现了一个GET和一个POST访问。

首先是getEmoji对象,定义了一个 html 片段文件路径和一个初始化方法。初始化方法中分别调用了miniSPA中的ajaxRequest方法(用于获取 Github API 提供的 emoji 表情数据, JSON格式)和render方法(用来渲染对应的 html 片段)。

然后是postMD对象,它除了 html 片段文件路径和初始化方法(因为初始化不需要获取外部数据,所以只需要调用render方法就可以了)之外,重点在于submit方法。submit会把用户提交的输入文本和其他两个选项打包 POST 给 Github 的 markdown API,并获取后台解析标记返回的 html。

这两个对象对应的 html 片段如下:

getEmoji.html :

postMD.html :

演示地址

以上代码的在线演示可以在我的 Github 项目页面看到。

以上演示代码已经在Chrome,Firefox,SafariOpera较新版本上测试过。IE 9以上版本估计也可以,不过没测过。

另外,这些代码还有不少值得优化的地方,不过时间有限,主要是为了达到演示目的,所以暂时就不去改它了。

打赏支持我写出更多好文章,谢谢!

打赏作者

打赏支持我写出更多好文章,谢谢!

任选一种支付方式

9 收藏 16 评论

关于作者:老码农

搞得定代码,罩得住娃;治得好跟腱,踢得了球。Hi,我是老码农,蜀黍有练过,小盆友们不要随便模仿喔。(新浪微博:@老码农的自留地) 个人主页 · 我的文章 · 122 ·    

相关文章

可能感兴趣的话题



直接登录
最新评论
  • 谢谢,很好用。miniSPA.refresh中remove the empty template node后childrenCount未更新导致children溢出bug。Google浏览器。

    • 老码农 其实,我是一个作家 2016/04/19

      谢谢指出你遇到的问题。

      这个问题应该是给template提供的数据为空或者格式不对,因为在前面的循环里会把template的样式clone成具体的数据,如果没有数据或者格式不匹配,那么clone就不会进行。template在里面只是起一个模板的作用,并不包含实际数据,所以在clone循环完成后就会删掉了。

      因为这篇文章只是一个方法的演示,为了突出核心逻辑,就没有加入很多边界条件的判断。具体应用的时候还是需要自己去定制的。

      再次感谢提供反馈。

  • 0.0 前端 2016/04/19

    不明觉厉

  • 公司也有一个这样的原生js单页面应用框架,只是没有使用占位符方式输出页面,现在碰到一个问题:公司用这个做了一个基于微信的wap站,有页面实现了摇一摇功能,输出别的页面摇一摇功能一直处在监听中,没有释放。也就是说填充容器之外的dom和bom对象监听随时可能根据填充内容变化而增加某些监听,【能有办法在每次填充容器之前释放掉除onhashchange】之外的所有监听吗?跪求!~

    • 老码农 其实,我是一个作家 2016/04/20

      不清楚你们的代码是什么样的,但是从原理上来说,事件监听不一定非要反复释放,而是可以通过参数的传递来判断。

  • 程序媛 前端开发 04/17

    觉得很吊,用在自己的项目中了。但是由于小白出身,只能实现单页的跳转,方法的事件的添加不知道使用什么来实现。希望能得到作者的指点,小女子感激不尽

    • 老码农 其实,我是一个作家 04/19

      事件的处理可以直接添加到元素上,不用改动现有代码。对于 data-repeat 元素,可以在事件响应方法的参数里传递它的 id 等唯一标识,例如在

      <img data-src='{{data.value}}’ width=’80’ height=’80’>

      元素里加上

      onclick=”getEmoji.clicked(‘{{data.key}}’);”

      miniSPA.refresh() 方法会自动把{{data.key}} 解析为实际的参数。

      然后在 app.js 里对应的 partial 对象里定义响应方法,例如:

      回头我再更新一下 github 上的演示,把这些代码加进去,然后在 emoji 示例中点击每个图标,就会出现上述代码产生的 alert 提示框。

      • 程序媛 前端开发 04/27

        非常感谢,我会好好在研究一下您的这个模式使用。希望还能跟您继续交流,我的qq是2549241079

        • 老码农 其实,我是一个作家 04/28

          我没有qq,不过你有问题可以随时在这里提,我会及时反馈的。:)

      • 程序媛 前端开发 04/27

        另外,我在此基础上做了一个简单的路由,整个下来觉得管理文件非常方便

  • Masir   07/25

    怎么带参数跳转

    • 老码农 其实,我是一个作家 08/23

      这只是一个框架性的演示,其他功能可以自己扩展

  • 腿短跑得快 web前端 08/23

    求大神github

跳到底部
返回顶部