12.其它细节

上面11篇论述了主要的原理,作为最后一篇,我们主要论述单页面相比于多页面的灵活的部分,如何使用最原始的html,js,css发挥web的最大魅力。

动画,过场动画

单页面要比多页面灵活,拥有过场动画是它最直观的表现,并且页面切换不会出现白屏的现象。

在底层ReplaceProto对象中,专门设置了两个dom,一个dom作为放置当前页面,另一个dom放置切换页面。在切换过程中,通过两个dom的过渡产生过场动画。动画方式在css3中定义,然后根据情况进行不同的动画切换, 同时完成后退和前进两个过场

  1. 进场动画,当切换到另一个页面。另一个页面就会以入场动画显示到屏幕上;

  2. 出场动画,当切换到另一个页面,当前页面就会以出场动画退出屏幕中。

它们共同组成了页面的动画效果

默认的动画由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,需要等它们全部加载完会耗费很长时间。因此我们可以对常见的资源进行统一定义加载。

  1. 可以将多个组件的js放在一个js文件中,因为获取组件的时候,判断是否存在已经加载过该组件, 需要手动关闭。在App.define, app.defineComponent, app.definePage, app.definePopup的第三个参数设为true,其中app为App的实例对象。

  2. 可以将引入的html当作字符串, 注入到Http.cache对象中 App.setHttpCache(url, str); 由于转换为字符串编入js中(以字符串形式编辑html比较困难,不建议全部放入js中)。

  3. 如果某个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,在没有网的情况下要打开网站,这时候只能通过打开本地页面,虽然功能有点阉割,但是页面布局还是可以复用原来的,我们需要做一下的调整:

  1. 把绝对引用全部改为相对引用,这一点都是可以支持的,通过改写App.join方法统一更改js的加入;

  2. 无法使用按需加载(可以按需加载js),需要对所有的静态资源进行统一加入。这一点难度虽然不大,但是操作起来比较繁琐;

  3. 如果使用了strui框架,需要针对使用的组件进行资源统一加入。

虽然付出了一些努力,但是非常值得的,底层是支持本地文件打开的,以下功能会受到限制:

  1. 无法使用ajax功能,无法与后端进行交互;

  2. 无法使用history api。在本地打开,会将这些方法全部过滤掉。

支持SSR

SSR对于单页面相对多页面是一个缺陷,尽管努力去弥补,但总是无法尽善尽美。而且单纯在前端努力是无法完成的。这里我们通过以下手段来实现SSR:

  1. 如果是纯粹单页面,index.html的body元素应该只有引用script的。我们在body上加入data-preload属性,代表它使用了SSR, 然后加入{{{body}}}, 代表着服务生成的html代码;

  2. 接着在服务端,复制前端的renderHTML方法。根据浏览器访问地址,拼装填充{{{body}}}的html片段(这里后端使用nodejs,可以共享前端的js方法);

  3. 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的各种技术

  1. 在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);
             });
     }
  2. 然后在根目录中加入sw.js,里面的内容自定义,代码略。

总结

这一篇作为完结篇,主要对常见的开发问题进行了进一步的扩展。

(0)

相关推荐

  • 她家厨房人人夸,只因这12处细节称得上装修典范!

    你有没有过这样的经历:厨房设计不合理,每次做饭都像在打仗!别人40分钟做完一顿饭,你却花了2小时! 其实,想要厨房经久耐用.做饭轻松?装修前,Get到下面这些点就行了. 一.厨房动线布局 好用的厨房前 ...

  • 落款的12个细节,可看出一个人的书法水准

    中国画讲堂2021-03-27 19:08:15 书法落款,不单是尾部,是包含了作者在书法作品中除正文内容以外的所有书写内容.落款包括:正文内容的出处.赠送对象.创作缘由.创作时间.作者姓名.字号等. ...

  • 《无双》打造罕见犯罪题材,12个细节解读周润发郭富城如何造伪钞

    预告片看了很多,但还没见过这样的!电影<无双>官方最新发布了一版"天王造假"最新预告片,预告片中有一句台词"只要有心,假的也可以变成真的!",从预告 ...

  • 这12个细节让你看穿男人是否风流

            有人这么说过:女人一生,谁没遇过几个渣男!在爱情心理学中,渣男最典型的表现就是花心,那么怎么判断自己遇到的男人是否花心呢?今天小编就给大家讲解一下这12个细节让你看穿男人是否风流,一起 ...

  • 细胞总是养不好污染,这12个细节一定要注意!

    养细胞是一件戳心戳肺的事情,你要像照顾小孩一样仔细对待,爱护她,呵护她.如果你照顾的时候能注意这些问题,会让你的细胞养的更好.下面小麦就将养细胞的注意事项跟大家交流一下. 细胞培养前的准备 在您带上手 ...

  • 青年律师接待当事人,需要知道的12个细节

    接待当事人是青年律师的日常工作,通过聆听当事人诉说,了解案情,制定诉讼策略,书写起诉状或答辩状.怎样才能省时.高效地做好聆听工作呢?要注意以下12个细节. 一.不要过早下结论.当事人说话,大多从自己角 ...

  • 打借条不注意这12个细节,钱可能就要不回来了……

    今日法言 法律人的掌中宝 本文仅供交流学习 , 版权归属原作者,部分文章推送时未能及时与原作者取得联系,若来源标注错误或侵犯到您的权益烦请告知,我们将立即删除 现在的人,维权意识是有的,只要借钱出去, ...

  • 落款的12个细节

    书法落款,不单是尾部,是包含了作者在书法作品中除正文内容以外的所有书写内容.落款包括:正文内容的出处.赠送对象.创作缘由.创作时间.作者姓名.字号等. 落款的字体,一般是隶不用篆,楷不用隶,行草不用楷 ...

  • 你不知道的这12个细节,随时会毁掉你的电路...

    发现这些细节,拯救电路很多人都一样,我们很多工程师在完成一个项目后,发现整个项目大部分的时间都花在"调试检测电路整改电路"这个阶段,也正是这个阶段,很多项目没有办法进行下去,停滞在 ...