12.其它细节
上面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)
如果使用replaceState方法不会触发动画。
其中第三个参数不仅可以传数据,也可以传动画配置,并且在_getReplaceClass方法中进行切换类名,达到动画的效果。
_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,需要等它们全部加载完会耗费很长时间。因此我们可以对常见的资源进行统一定义加载。
可以将多个组件的js放在一个js文件中,因为获取组件的时候,判断是否存在已经加载过该组件, 需要手动关闭。在App.define, app.defineComponent, app.definePage, app.definePopup的第三个参数设为true,其中app为App的实例对象。
可以将引入的html当作字符串, 注入到Http.cache对象中 App.setHttpCache(url, str); 由于转换为字符串编入js中(以字符串形式编辑html比较困难,不建议全部放入js中)。
如果某个service服务仅仅与该方法有关,把引入代码从顶部移到方法内,。比如
clickHandler: function (ev) { App.require(["mapTool"], function (mapTool) { // dosomething }) }
持续化数据和异步操作
对于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的封装方式,跳转页面的时候让获取数据操作停止,存数据的回调中判断是否当前页面是有效显示页。
对于不做中断的异步操作,可以放在staticPage中执行,因为它是一直存在的,等执行完毕后通过触发app.currentPage的自定义事件,进行相关的更新操作。
浏览器缓存
浏览器缓存可以提高页面的加载速度,有时候却成了我们更新项目的一大阻碍,特别在测试公众号的时候。因此我们通过后缀名版本来解决问题。比如我们在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,在没有网的情况下要打开网站,这时候只能通过打开本地页面,虽然功能有点阉割,但是页面布局还是可以复用原来的,我们需要做一下的调整:
把绝对引用全部改为相对引用,这一点都是可以支持的,通过改写App.join方法统一更改js的加入;
无法使用按需加载(可以按需加载js),需要对所有的静态资源进行统一加入。这一点难度虽然不大,但是操作起来比较繁琐;
如果使用了strui框架,需要针对使用的组件进行资源统一加入。
虽然付出了一些努力,但是非常值得的,底层是支持本地文件打开的,以下功能会受到限制:
无法使用ajax功能,无法与后端进行交互;
无法使用history api。在本地打开,会将这些方法全部过滤掉。
支持SSR
SSR对于单页面相对多页面是一个缺陷,尽管努力去弥补,但总是无法尽善尽美。而且单纯在前端努力是无法完成的。这里我们通过以下手段来实现SSR:
如果是纯粹单页面,index.html的body元素应该只有引用script的。我们在body上加入data-preload属性,代表它使用了SSR, 然后加入{{{body}}}, 代表着服务生成的html代码;
接着在服务端,复制前端的renderHTML方法。根据浏览器访问地址,拼装填充{{{body}}}的html片段(这里后端使用nodejs,可以共享前端的js方法);
index.js中的声明App对象的时候,currentName需要根据pathname改变。如下代码
// location.pathname = "/detail"; if (location.pathname == "/detail") app = new App("hello str", "static", "detail"); else app = new App("hello str", "static", "home");
虽然通过上面拼接成的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的各种技术
在index.html中加入以下js字段
if ("serviceWorker" in navigator) { navigator.serviceWorker.register("./sw.js") .then(function (registration) { console.log("ServiceWorker registration successful with scope: ", registration.scope); }).catch(function (err) { console.log("ServiceWorker registration failed: ", err); }); }
然后在根目录中加入sw.js,里面的内容自定义,代码略。
总结
这一篇作为完结篇,主要对常见的开发问题进行了进一步的扩展。