上面11篇论述了主要的原理,作为最后一篇,我们主要论述单页面相比于多页面的灵活的部分,如何使用最原始的html,js,css发挥web的最大魅力。 动画,过场动画单页面要比多页面灵活,拥有过场动画是它最直观的表现,并且页面切换不会出现白屏的现象。 在底层ReplaceProto对象中,专门设置了两个dom,一个dom作为放置当前页面,另一个dom放置切换页面。在切换过程中,通过两个dom的过渡产生过场动画。动画方式在css3中定义,然后根据情况进行不同的动画切换, 同时完成后退和前进两个过场
它们共同组成了页面的动画效果 默认的动画由App对象里面定义,在附带的app.css中定义行为 this.options = { changeClass: "app-change", backClass: "app-back", area: "change-state", in: { // 进场动画 back: "page-out", change: "page-in" }, out: { // 出场动画 back: "page-in-reverse", change: "page-out-reverse" } }; 在切换页面的时候,也可以通过传入不同的类名,实现自定义动画, 详细见ReplaceProto的render方法。render: function (pagename, isReplace, option) _getReplaceClass: function (option) { var options = this.options; option = option || {}; return { backStaticClass: option.backClass || options.backClass, changeStaticClass: option.changeClass || options.changeClass, areaClass: option.area || options.area, backActiveClass: this.isRenderBack ? option.backClass || options.out.back : option.backClass || options.in.back, changeActiveClass: this.isRenderBack ? option.changeClass || options.out.change : option.changeClass || options.in.change } }, 类似的在PopUp对象中同样有页面切换动画,PopUp对象还要有个弹出弹窗和关闭的动画。请看动画设置: this.options = { className: "popup", changeClass: "popup-change", backClass: "popup-back", area: "popup-state", currentIn: { // 显示页面上的入场 backClass: "popup-active-out", changeClass: "popup-active-in" }, currentOut: { // 显示页面上的出场 backClass: "popup-active-in-reverse", changeClass: "popup-active-out-reverse" }, staticIn: "popup-static-out", // 弹窗进入页面 staticOut: "popup-static-in" // 弹窗隐藏 }; 可以在PopUp配置或在show和hidden方法中设置不同的动画效果。 show: function (dom, config, target, isDismisBeforeShow) // config.staticIn hidden: function (option, bk) // option.staticOut或show方法传入的config.staticOut 对于组件的页面切换动画和App的动画切换是一致的。 初始化页面历史缓存如果用户从首页进入网站,我们不用对history记录做任何更改,这是一种常规情况。然而网站的入口是url,如果url不是进入首页,而是从详情页或是付款页进入网站,或者通过其它手段(扫码等)。 当用户在该页面进行了操作(如果不做任何操作,点击后退应该是退出网站),为了让用户有一个浏览流程,在详情页点击后退应该是返回到列表页。 原理很简单,判断进入初始页,然后先pushState若干个页面。然后渲染页面。从App.initialize的方法来看,有一个_prevAttachHistory(prevHistory)操作,这就是为了该目的。这里的prevHistory是由开发初始化定义的App实例对象的getInitHistory来得到,这是一个url数组。 例如我们初始化定义 app.getInitHistory = function (pathname) { if (pathname === "/detail") return [ "/home" ]; // 也可以带参数的url } 通过这样配置后,页面直接打开详情页,在详情页操作后,点击后退键回退到首页,而不是退出该网站。注意,如果进入页面后没有任何操作,直接点击退出,是不会退到首页的,这是浏览器的一大特性,只有用户与该页面有交互,即使是touchstart,mousedown都能后退到首页。后退到前页面,因为历史记录中存的不是页面缓存,它是初始化一个新的Page对象,会走完Page的整个生命周期流程 加速加载优化,service延迟加载从上面篇章提到,整个资源获取都是按需加载的,即使组件中的小图标的svg片段也是如此。特别一个大的首页,里面包含着很多小图标,引入很多组件js,片段html,需要等它们全部加载完会耗费很长时间。因此我们可以对常见的资源进行统一定义加载。
持续化数据和异步操作对于Component,Page, PopUp,App对象中,都定义一个data,这是一个放置临时数据的对象。特别是Page,这个data格式有严格的要求,必须是可序列化的。 在Page执行restore的时候,我们是无法获取到在其他页面改变了的全局数据,我们可以把数据存放在App实例对象的data里面,然后切换页面的时候获取App对象中的data数据,进行有效的局部刷新操作。这要比重新去后端获取一次更合理。 如果在PopUp对象的Page对象,也可以用相同的方式放在PopUp的对象的data里面。统一放置方法可以用this.parent.data.key = value; 一个网站,很少使用大量的持久化数据,对于webapp,使用持久化数据却很常见。我们可以使用同步操作的localStorage或异步操作的IndexedDB数据库。在使用IndexedDB的时候要特别的注意,异步操作时页面突然切换导致回调函数执行错误,因此尽量在执行完后再执行跳转。如果不可避免,可以仿照Ajax的封装方式,跳转页面的时候让获取数据操作停止,存数据的回调中判断是否当前页面是有效显示页。 浏览器缓存浏览器缓存可以提高页面的加载速度,有时候却成了我们更新项目的一大阻碍,特别在测试公众号的时候。因此我们通过后缀名版本来解决问题。比如我们在index.html上header的新建script上加一句App.version = 2.0; 再把str.js和index.js版本号更改相同的版本号。接着框架内部会把所有的引入的js以及获取的静态文件都会加上?v=2.0重置所有的版本号 内存使用单页面对于内存的使用非常的苛刻。如果无限制的使用,会导致页面奔溃或让手机设备快速耗电。因此这里我们对每个模块的引用都做了严格的处理。对于dom和事件,在页面销毁的时候都会自动去销毁。而且引用外库的时候,我们建议在init初始化数据,在dispose方法中进行数据清理。对于引用没有数据回收操作的外库的时候要特别小心,不能无限制的新建对象,这样会导致页面堆积越多的内存而无法销毁。我们可以使用创建一个对象,然后进行无限制的使用(单例模式)。 通过异步按需加载的好处在于,能让内存使用量尽可能的变少。在加载首页的时候,我们的网页的内存使用量基本和纯使用静态页面的网站持平的。随着组件量以及页面的增加,我们缓存了大量的js,静态html,会让内存使用量增多,而且缓存在history的Page对象,也会提高内存使用量。尽管如此,我们的内存使用量也不会超过静态页面太多,在可以接受的范围之内。 本地文件打开有时候我们需要本地直接打开,虽然用的很少,但还是会遇到的。比如原生App嵌入webview,在没有网的情况下要打开网站,这时候只能通过打开本地页面,虽然功能有点阉割,但是页面布局还是可以复用原来的,我们需要做一下的调整:
虽然付出了一些努力,但是非常值得的,底层是支持本地文件打开的,以下功能会受到限制:
支持SSRSSR对于单页面相对多页面是一个缺陷,尽管努力去弥补,但总是无法尽善尽美。而且单纯在前端努力是无法完成的。这里我们通过以下手段来实现SSR:
虽然通过上面拼接成的html可以在浏览器上直接打开,然而浏览器毕竟没有直接渲染组件的功能, 因此渲染的结果不会太好。只能让搜索引擎获取到, 然后通过下面的方法进行分别渲染: if (preload === "true") { // 通过更改html,渲染组件 var activeHtml = pageContainer.innerHTML; pageContainer.innerHTML = ""; var staticHtml = document.body.innerHTML; if (staticPage.preload) staticPage.preload(); staticPage.initialize(body, staticHtml, {}, function () { body.removeAttribute("data-preload"); that._initCurrentPage(staticPage, currentPage, prevHistory); if (currentPage.preload) currentPage.preload(); currentPage.initialize(that.changeDom, activeHtml); }); } else { // 常规手段 staticPage.render(function (html) { staticPage.initialize(body, html, {}, function () { that._initCurrentPage(staticPage, currentPage, prevHistory) currentPage.render(function (html) { currentPage.initialize(that.changeDom, html); }); }); }); } 超越web,支持electron等方式现在web在通过electron打包成桌面App,因为electron使用了node技术,所以在获取文件或者资源的时候就不一样了。我们可以更改fetch方法: if (typeof __dirname === "string") { require("fs").readFile(url, "utf-8", function (error, result) { if (error) console.log(error); else Http.cache.dispatch(url, result); }) } else { var obj = createRequest(this, url, undefined, function (result) { Http.cache.dispatch(url, result); }, { onabort: function (ev) { Http.cache.remove(url); } }); this.http.ajax(obj); } 更改获取文件路径方法 App.join = function (url) { if (typeof __dirname === "string") return require("path").join(__dirname, url); return url; }; 还需要更改Page获取资源路径方法 function getBaseUrl(urlStr) { if (typeof __dirname === "string") { urlStr = require("path").join(__dirname, urlStr); return urlStr.split("\\").slice(0, -1).join("\\") + "\\"; } return urlStr.split("/").slice(0, -1).join("/") + "/"; } 这样子,就可以兼容electron的环境了。 使用pwa技术有幸于web的发展进程都是围绕了渐进增强的路线,所以很容易让webapp支持pwa的各种技术
总结这一篇作为完结篇,主要对常见的开发问题进行了进一步的扩展。 |
|