Miox框架简介:51信用卡的SPA技术演进和解决方案

1,242 阅读11分钟
原文链接: mp.weixin.qq.com
  • Miox框架简介:51的SPA解决方案

    • Miox核心

    • Miox 引擎

    • render函数详解

    • 缓存的好处

    • 注册事件驱动

    • SSR初始化渲染

    • Web初始化渲染

    • 生命周期的设计

    • 记录History队列

    • 添加记录

    • 何时删除记录

    • 绑定监听

    • 移动端History行为难点突破

    • 虚拟Web Service的建立

    • 原生中间件的设计

    • 缓存模型的建立

    • 何时创建完成?

    • 何时渲染完成?

    • 销毁时候我们该做什么?

    • 当局部刷新的时候,我们做什么?

    • 页面激活映射?

    • 页面进入后台映射?

    • Vue@2x版本引擎详解

    • React引擎

    • Soyie

    • Webservice

    • Simplize

    • Miox简介

    • Miox演进历程

    • Miox的SPA

    • Miox的技术架构

    • 总结

Miox简介

Miox全称 Modern infrastructure of complex SPA,是51信用卡公司内部研发的一套SPA解决方案。它实现了生命周期和路由管理的最佳实践,避免了不统一的开发方式可能造成的性能下降和错误,并且可以平滑接入 SSR 和  Weex 这样业界最先进的混合开发技术,达到开发效率和接近原生体验两者之间的最佳平衡。

Miox 可以自动帮你处理路由切换、webview 生命周期管理等各种单页应用会面临的问题,让你专注于 webview 内的业务开发。同时,Miox 并不依赖任何框架,这意味着你的业务开发无论是基于 ReactVue还是其他框架,都可以完美的接入到 Miox 中来。

Miox演进历程

51信用卡管家(杭州恩牛网络技术有限公司)从2015年6月开始,不断摸索适合公司内部的SPA模式。在考虑了业界其他的SPA模式基础上,我们觉得都有各种利弊,解决方案都不能完全解决目前公司内部的需要。

Soyie

2015年7月到10月,当时非常盛行MVVM思路的架构,所以我们考虑了一套属于自己的双向绑定架构,名字叫 Soyie。对其解释:

soyie是一套针对移动端开发的高性能MVVM前端框架,类似 angular vue  reactjs avalon 。soyie集合来这些框架的优势,具有全组件,高性能的优点。特别是组件的制作(component)非常方便,您可以通过JSON化配置组件,也可以使用 class extends来继承基本组件类扩展您的组件。

除此之外,它兼容最新的IOS9。非常方便的如同搭积木一样拼装您的项目。

演示地址:http://cevio.github.io/soyie/test/?page=home

设想非常美好,就是做一套这样的数据架构用来解决内部出现复杂的数据变化问题。第一个项目就是当时的人品分项目。第一次,我们体会到了双向的魅力,但是远远不够,我们需要更加全面而系统的解决方案,而 soyie仅仅是一套数据架构。

Webservice

webservice 是一套单页面级的路由框架,包含了soyie soyie-http-router等框架或组件,能够实现复杂路由功能,类似于nodejs的express框架。它的优点在于我们可以处理复杂逻辑的页面路由,通过自定义路由回调,轻松实现页面逻辑。

GitHub:https://github.com/cevio/webservice

我们第一次考虑了使用路由对应回调的模式来动态处理页面渲染,这是一个非常具有标志性意义的时间点,为之后Miox的核心路由机制埋下了种子。

虽然在业务上基本能够胜任复杂的项目要求,但是在性能上有很大的缺陷,表现在数据量大的时候,页面非常卡顿。分析原因之后,发现是我们使用的Soyie,在核心逻辑上我们使用了类似 angular的脏检查机制。

为了解决这个性能问题,我们参考了VueJS的源码,发现了更好的模式。在此,我们没有兴致勃勃地直接去修改我们的代码,而是我们考虑了是否放弃掉 Soyie,选用业界正在兴起的VueJS@1.x

答案是肯定的。

Simplize

同样的,Vuejs当时仅仅是一个数据驱动的架构,对于业务层的架构没有优势。所以我们考虑了非常久,将 webservice进行非常大的改造,从而出产了我们的Miox前身Simplize框架。

此框架的产生也是非常具有标志性意义的。确定了之后Miox解决方案的方向,我们一直都是坚定不移地朝着确定的方案不断努力。

自此,时间已经过去了一年左右。

它的第一个应用是在人品宝的红包项目中。当我们做完,拿出来给大家看的时候,都觉得这个项目靠谱了,很棒,我们得到非常大的鼓励。我们的大老板,孙总当时也是非常开心的(孙总当时还是很关注我们的内部技术的)。

既然这么好,为什么之后又改成了Miox呢?

原因非常简单,就是它的架构体系非常凌乱,而且当时我们的nav-bar等设计都是直接内嵌在 Simplize之内的,没有很好的拆分出来,模块化力度不够,在做非常复杂的业务的时候,很难完全满足。之后我们反思了这种模式,觉得应该进行再一次的改造,来更加合理化的设计整个架构,于是Miox应运而生。

Miox的SPA

单页应用无非就是解决一个问题,当URL变化的时候,我们如何处理页面渲染。传统的SPA模式,大部分会将URL绑定到一个页面,这对于简单的应用是完全正确的。如果我们遇到一种情况下,一个URl链接的改变,会引起渲染不同的页面的场景,那么我们该怎么办呢?其实这也是后端路由概念中最基本的部分,但是在前端路由弱化了这层逻辑。Miox考虑了后端路由的架构体系,提出了新的概念,那就是我们的路由与页面并非是绑定关系,我们需要非常灵活的模式去渲染不同的页面。

基于灵活渲染页面的前提下,我们对于页面渲染是否需要考虑下渲染性能呢?答案是肯定的。Miox对于渲染过程进行了缓存式处理,使得系统能够尽可能复用已经渲染过的页面webview。缓存模式也是Miox的一大亮点。

对比过各种前端路由框架,几乎都没有对当前行为操作自动进行方向性处理的。其实在SPA中,难点在于当前用户所操作的行为的方向性。一般情况下,都会去考虑history的队列,但是history根本不提供查询队列的方法。那么通过什么样方式,我们才能在页面进入后记录下当前用户行为的历史队列呢?答案只有一种方法,那就是sessionStorage。它随着页面的创建而创建,随着页面的消失而消失,正好符合我们的预期。我们需要写入一种算法,来记录、获取和删除这些队列。Miox做了这个工作,能够自动识别出当前用户行为的方向性。

Miox的技术架构

Miox主要分为核心、引擎和路由三部分组成。它是一种插拔式设计,也就是说可以更换任意一部分来实现不同的页面渲染目的。

  • 核心:可选模块 miox-core  miox-simple

  • 引擎:可选模块 miox-vue  miox-vue2x miox-react等。不同模块,页面的编写方式不同。

  • 路由:目前可选模块只有miox-router,如果需要其他路由模块,可以自行开发。

Miox核心

核心问题,主要讲述history行为、基础路由中间件、渲染引擎接入以及整体web端虚拟service的建立。

移动端History行为难点突破

History因浏览器无法提供查询队列,以至于我们需要自己来记录浏览队列。我们采用sessionStorage来记录,从用户进来第一个页面开始,之后无论发生什么行为都记录。但是这种模式有一个比较头疼的问题,那就是使用 <a href=""></a>或者global.location.href跳转的页面会使 history队列和sessionStorage不同步。所以,我们约定,不允许使用这两种方式跳转,取而代之的我们需要使用以下内置方法来跳转:

  • push 跳转添加记录。

  • go 自动匹配记录中的地址,不存在则使用 push行为跳转。

  • replace 替换当前路由。

  • redirect 静默跳转模式。

例如:

ctx.push('/a/b/c?a=1&b=2#test');
记录History队列

为了实现记录,我们创建一个类,同时必须与history记录一致,并且记录下当前游标索引,考虑到性能,我们还需要给这个类加上最大缓存个数的限制。

class SessionStorage {
    constructor() {                this.stacks = {}; // 记录集
        this.current = -1; // 游标
        this.historyListRegExp = /^\@MIOX\:HISTORY\:INDEX\:/;                this.historyCurrentRegExp = /^\@MIOX\:HISTORY\:CURRENT/;                this.max = max; // 缓存的最大记录数
    }
}

在页面初始化时,收集到当前sessionStorage中的数据,因为有可能是用户刷新了页面,我们需要把记录同步。

class SessionStorage中添加一个init方法。

注意:之后所有window对象都使用 global替换,为了兼容SSR渲染。

每条记录模式为@MIOX:HISTORY:INDEX:${Index} = ${WebviewKey},${URIKey}

  • Index 每条记录在history中的索引

  • WebviewKey 自动分配的每个webview的随机值

  • URIKey 每个路由对应的唯一值,也是身份标识,这里采用MD5编码产生唯一值。

init() {        const stacks = {};        let current = -1;        const session = global.sessionStorage;        let i = session.length;        while (i--) {              const key = session.key(i);              let value = session.getItem(key);              if (this.historyListRegExp.test(key)) {                      const val = value.split(',');
            stacks[Number(key.replace(this.historyListRegExp, ''))] = {
              webviewKey: val[0],
              uriKey: val[1]
            };
        } else if (this.historyCurrentRegExp.test(key)) {
          current = Number(value);
        }
    }        this.stacks = stacks;        this.current = current;
}

constructor中运行 init()方法用来初始化。这样我们就得到了与history同步的数据队列,理论上也是与 History相同的队列。

添加记录

添加记录不仅仅进行跳转,同时必须同步到sessionStorage中。

add(index, webviewKey, md5) {       this.stacks[index] = {
       webviewKey: webviewKey,
       uriKey: md5
   };
   global.sessionStorage.setItem(               this.name(index), // 用来生成名字的
       `${this.stacks[index].webviewKey},${this.stacks[index].uriKey}`
   );
}

添加完记录,当前游标必定指向此记录,我们需要设定:

moveto(index) {       this.current = index;
    global.sessionStorage.setItem(`@MIOX:HISTORY:CURRENT`, index);
}
何时删除记录

删除记录是一个非常复杂的问题,需要理解浏览器push行为规则。比如我们的行为如下:

  • 当前位于 /

  • 跳转至:/a,当前位于 /a

  • 跳转至:/b,当前位于 /b

  • 回退到:/a,当前位于 /a

  • 回退到:/,当前位于 /

  • 跳转至:/c,当前位于 /c

我们来分析下,如果按照这样的逻辑跳转,浏览器会自动删除掉/a  /b路由,这个行为是浏览器决定的,所以对于这种情况下,我们必须删除掉这些路由对应的webview。但是如何决定呢?

经过我们的检测与分析,当有push行为发生的时候,我们都需要去检测比当前索引大的记录,然后根据记录删除掉对应的 webview,这样才能确保我们的sessionStorage中的记录是与history中的一致的。

del(index) {       delete this.stacks[index];
    global.sessionStorage.removeItem(this.name(index));
}

但是我们又考虑到性能上的问题,所以我们提出只能缓存this.max个webview。虽然这些webview对应会被删除,但是相应的记录不会被删除,当下次需要渲染这条记录的时候,我们可以重新创建出来,然后把相应的匹配关系绑定上。

removes(index) {       let keys = [],         indexs = [],         exists = [];               for (const i in this.stacks) {               if (Number(i) > index) {
           keys.push(this.stacks[i].webviewKey);
           indexs.push(i);
        } else {
           exists.push(i);
        }
    }         if (this.max < exists.length) {
        exists = exists.sort();               const remind = exists.slice(exists.length - this.max, exists.length);
        indexs = indexs.concat(remind);
        keys = keys.concat(remind.map(i => this.stacks[i].webviewKey));
    }               for (let i = 0; i < indexs.length; i++) {               this.del(Number(indexs[i]));
    }           return keys;
}

通过removes函数返回的是所有需要被删除的 webview的标记key,我们可以通过全局的cache模块找到对应的 webview,这里就不细讲cache模块了。如何删除,肯定也是引擎该决定的事情。

绑定监听

History的监听模式有两种,一种是hashchange,另一种是 popstate。对于SSR渲染,我们一般采用popstate,其他的一般使用 hashchange,但是也有例外配置,如果你设置了app.set('history mode', 'popstate'),即可强制使用 popstate

一般来说,不使用hashchange的,都认为这种路由页面比较丑!但是,个人认为这种模式还是比较稳定的,除非业务中不允许使用这种路由。相比 popstate,需要在404配置上做跳转,如果不做跳转,那么刷新是有很大的问题的。

listen() {
   global.addEventListener(               this.isPopState ? 'popstate' : 'hashchange',
        () => this.compare(this.uri, this.get())
   );
}

在Miox中,我们将监听结果可以分解为三部分:

  • pathchange 当路径发生变化时候触发

  • searchchange 当参数发生变化的时候触发

  • hashchange 当锚点发生变化的时候触发

以上listen函数中的 compare函数就是用来处理这三部分情况的。三部分触发也是具备优先级的:pathchange >  searchchange > hashchange

compare(prev, next) {       if (prev.pathname !== next.pathname) {               return this.emit('pathchange', next, prev);
    }    
    // sortQueryToString函数将对search参数进行签名算法计算后返回
   const prevSearch = this.sortQueryToString(prev.query || {});       const nextSearch = this.sortQueryToString(next.query || {});       if (prevSearch !== nextSearch) {               return this.emit('searchchange', next, prev);
    }           if (prev.hash !== next.hash) {               return this.emit('hashchange', next, prev);
    }
}

当URL的search部分的时候,其实我们需要用到签名算法来分析是否改变,而不是一味认为绝对是被改变了。我们会根据键值对统一进行排序后生成字符串进行比对,这样的好处在于无论数据前后顺序如何变化,都能判断出是否数据无任何变化,至于顺序,与对比无关。

sortQueryToString(obj) {       const result = [];       for (const i in obj) result.push(`${i}=${obj[i]}`);       return result.sort().join('&');
}

我们利用自定义事件触发来冒泡到全局app对象,只要全局app对象绑定了这三个事件,便能自己触发想要的行为。

但是这里需要注意的是,当使用popstate监听的时候,非 go方法都必须自己触发compare方法来保证流程稳定。

push(url) {       if (this.isPopState) {
        global.history.pushState(null, this.title, url);               this.compare(this.uri, this.get());
    } else {
        global.location.hash = url;
    }
}

replace(url) {       if (this.isPopState) {
       global.history.replaceState(null, this.title, url);              this.compare(this.uri, this.get());
    } else {              this.replaceHash(url);
    }
}

当使用hashchange监听的时候,如果用到replacce方法,那么我们需要做 locationreplace,代码如下:

replaceHash(path) {       const i = global.location.href.indexOf('#');
    global.location.replace(
       global.location.href.slice(0, i >= 0 ? i : 0) + '#' + path
    )
}

虚拟Web Service的建立

顾名思义,虚拟Web Service就是模拟后端服务的建立。它主要是在页面上启动一个服务,用来处理服务内部流程的。这也是我们的核心。不管miox-core还是 miox-simple都具有相同的服务体系。

它主要分为以下几个部分组成:

  • variables 自由变量集合

  • middleware 中间件类

  • cache 全局缓存字典,所有 webview对象都存于此,以键值对方式存储。

  • action 全局行为库

  • plugins 全局插件库

  • request 输入控制器

  • response 输出控制器

于是,我们便可建立如下类:

class ModernInfrastructureOfComplexSPA extends MiddleWare {
    constructor() {                super();                this.env = process.env.MIOX_ENV || 'web';                this.variables = new Dictionary();               this.cache = new Dictionary();                this.action = new Action(this);                this.plugins = new Plugin(this);                this.request = {};               this.response = {};
    }
}
注册事件驱动

合理的API设计会带来更多代码上的规范和方便,考虑到Miox中虚拟Web Service的运行,如果统一运行口,那会给程序带来非常大的方便。

这个入口提供的函数方法必须将我们的中间件运行,同时监听到运行的开始和结束,用来注入类似埋点时间收集等功能:

async createServerProcess(url, key) {       if (this.action.redirecting) return;       this.set('RENDERED-VIEW-KEY', key || RandomString.geneate());       this._createRequestAndResponseContext(url);       this.action.start();       this.emit('server:start');       await this.execute(this); // 运行中间件
    this.emit('server:end');       const webview = this.view;       if (webview && this.env !== 'server') {               this.history.notify(this.view);
    }       this.action.stop();       this.installed = true;
}

之后,我们只要调用这个方法,就会执行当前所在URI资源的匹配逻辑。很多时候,我们是否能够给程序分块,如何分块,将决定一个程序编写的规范与否与运行效率的高低与否。

之后我们需要向全局app对象注册事件,来将URL变化与这个函数关联起来。之前讲到history机制突破的时候,我们提出了三种事件模式,这里就可以用到了。

我们首先绑定这三种模式,当然这只是对于非服务端渲染而言的(服务端渲染又没有history - -#):

this.history = new History(this);this.history.listen();// pathchange事件注册this.history.on('pathchange', (next, prev) => {        this.history.set(next);        this.createServerProcess(this.history.url)
    .then(() => this._exchange())
    .catch(e => this.emit('error', e));
});// searchchange事件注册this.history.on('searchchange', (next, prev) => {        this.history.set(next);        this.createServerProcess(this.history.url)
    .then(() => this._exchange())
    .catch(e => this.emit('error', e));
});// hashchange事件注册this.history.on('hashchange', (next, prev) => {        this.history.set(next);        const cb = this.get('hashchange');        if (typeof cb === 'function' && global && global.document) {
        cb(this.webview);
    }
});

特别是hashchange事件,它不需要进入服务流程,只是页面上的锚点改变,那么我们就可以回调出去,让用户决定如何处理。

此处需要注意的是,这个事件注册,将对后续的操作起作用,对首次加载不会起作用。所以我们要分开首次加载与后续加载,用不同的方法去保证,请看下节。

SSR初始化渲染

我们先来讲服务端渲染,因为它与其它的不同,分为两端:

  • server 服务端

  • client 客户端

因为SSR需要返回的对象不同,在server端,需要给系统返回一个 Function来对接miox-vue2x-server-render(Miox对Vue的服务端渲染模块)的入口;而 client端,不需要返回任何。

于是我们就有了选择性返回:

switch (this.env) {      case 'server': return ServerRenderer(this);      case 'client': return ClientRenderer(this);      case 'web': return WebRenderer(this);
}

ServerRendererClientRenderer函数将对首次加载起决定作用。

ServerRenderer函数的决定作用:

当时,我们并未初始化,未调用createServerProcess函数,所以,页面不会进入中间件而得到匹配的webview。所以,我们需要运行这个函数。

但是,SSR引擎规定我们必须返回一个Function对象,之后这个对象会被带上参数调用,那么我们需要模拟下这个对象。

export default MX => {        return options => {        // SSR渲染引擎会给出这个三个变量
        const { url, app, ctx } = options;
        MX.$application = app;
        MX.$context = ctx;                if (!url) {                        return Promise.reject(new Error('Server render miss url'));
        }        // 这里注解
    }
}

当SSR引擎回调我们的方法的时候,需要提供一个Promise对象,用来确保webview顺利加载。所以,在注解处需要返回一个 Promise对象。

return new Promise((resolve, reject) => {
  MX.emit('server:render:start', options);
  MX.createServerProcess(url)
      .then(() => {                    const webview = MX.get('SERVER-RENDER-WEBVIEW');                    if (webview) {                              if (                        typeof webview.MioxInjectLocalRefresh === 'function'                ) {                                  // 兼容局部刷新方案
                    const cb = webview.MioxInjectLocalRefesh();                    if (typeof cb === 'function') {
                      cb(Url.parse(url, true), {});
                    }
              }
              setTimeout(() => {
                  MX.emit('server:render:end', options);
                  resolve(webview);
              }, 0);
          } else {                          const err = new Error('No webview matched');
              err.code = 404;
              reject(err);
              MX.emit('server:render:end', options);
          }
      })
      .catch(err => {
          err.code = 500;
          reject(err);
          MX.emit('server:render:end', options);
      });
});

客户端渲染就有所不同了。它不需要提供回调,只要初始化时候直接运行createServerProcess即可。

export default MX => {
    MX.emit('client:render:polyfill');
    MX.emit('client:render:start');
    MX.history.onClient = true;
    MX.createServerProcess(MX.history.url, MX.history.session.local.webviewKey)
    .then(() => {                    const webview = MX.view;                    if (webview) {
                MX.emit('client:render:mount', webview);
                MX.emit('client:render:end');                            if (typeof webview.MioxInjectLocalRefresh === 'function') {                // 兼容局部刷新方案
                    const cb = webview.MioxInjectLocalRefresh();                    if (typeof cb === 'function') {
                        cb(MX.history.uri, {});
                    }
                }
                MX._exchange();                            return new Promise(resolve => setTimeout(() => resolve(webview), 0));
            ‍} else {                            const err = new Error('No webview matched');
                err.code = 404;
                MX.emit('client:render:end');                            return Promise.reject(err);
            }‍
    })
    .catch(err => {
        MX.emit('client:render:end');                return Promise.reject(err);
    })
}

如此一来不论客户端还是服务端,在渲染过程中,都已经将我们所要的Webview取到了。如果未取到,那么就是用户书写的问题了。

Web初始化渲染

它和SSR相比,再简单不过了。直接MX.createServerProcess即可取到我们想要的Webview。

export default MX => {
    MX.history.onClient = true;
    MX.emit('client:render:start');
    MX.createServerProcess(MX.history.url, MX.history.session.local.webviewKey)
    .then(() => {                const webview = MX.view;                if (webview) {
            MX.emit('client:render:end');                        if (typeof webview.MioxInjectLocalRefresh === 'function') {                // 兼容局部刷新方案
                const cb = webview.MioxInjectLocalRefresh();                if (typeof cb === 'function') {
                    cb(MX.history.uri, {});
                }
            }
            MX._exchange();                        return new Promise(resolve => setTimeout(() => resolve(webview), 0));
        } else {                        const err = new Error('No webview matched');
            err.code = 404;
            MX.emit('client:render:end');                        return Promise.reject(err);
        }
    })
    .catch(err => {
        MX.emit('client:render:end');                return Promise.reject(err);
    })
}
生命周期的设计

基于Hisotry对Webview的key的删除,其实我们可以总结出Miox页面是存在生命周期的。它们的产生与销毁,完全是由history行为和最大缓存个数决定的。

this.on('clear', keys => {        let i = keys.length;        while (i--) {                const key = keys[i];                const vm = this.app.cache.get(key);                this.app.__removeVMKey__(key, vm);
    }
});

原生中间件的设计

用此功能主要开发一些实用的插件,来辅助miox的运行。

中间件的设计,依赖于Middleware模块,这个模块很小很简单。通过堆栈数组的 compose行为组成一个回调函数来完成。

class MiddleWares extends EventEmitter {
    constructor() {                super();                this.middlewares = [];
    }

    use(...args) {                const result = [];                for (let i = 0; i < args.length; i++) {                        let cb = args[i];                        if (typeof cb !== 'function') {                                throw new Error(                                        'middleware must be a function ' +                                        'but got ' + typeof cb
                );
            }                        if (isGeneratorFunction(cb)) {
                cb = convert(cb);
            }

            result.push(cb);
        }                this.middlewares.push.apply(this.middlewares, flatten(result));                return this;
    }

    __defineProcessHandle__() {                this.__processer__ = compose(this.middlewares);
    }        async execute(context) {                return await this.__processer__(context || this);
    }
}

代码很简单,不用一一细说了。

当我们调用execute函数的时候,会自动执行。Miox提供这样一个类,为了给用户使用方便,可以自主创建内置中间件,来完成复杂的功能逻辑。

缓存模型的建立

缓存模型只对移动端核心miox-core做讲解。我们只对移动端做缓存处理,而桌面端,无需处理。

模型分三部分组成:

  • [动态缓存] webviewKey与真实webview实例的绑定缓存

  • [动态缓存] webview构造体与路由缓存的绑定缓存

  • [静态缓存] 路由索引与webviewKey的绑定缓存

webviewKey 指每个webview的标识,是随机的不确定的,在没次被创建的时候就注定的标识。

webview构造体与webview实例的关系,就如同function() {}new (function() {this.a = 1})()的区别。

render函数详解

当路由变化,通过内部的createServerProcess方法,进入到中间件。匹配到的中间件中一定会有一个中间件使用了 render方法来渲染页面。那么这个时候就进入了缓存获取或者直接获取的方案进程。

如果当我们使用强制创建类型的方法,比如push或者 redirect等,那么我们会直接创建。

await engine.create(webview, args);

如果我们使用了静默创建的方法,那么优先会通过缓存来取对应的页面,如果找不到就走强制创建方案流程。

if (!cache) {
  nextWebview = await engine.create(webview, args);
} else {  const _key = key;
    key = webview[mark];      if (key) {
      nextWebview = this.app.cache.get(key);            if (nextWebview && typeof nextWebview.MioxInjectWebviewActive === 'function') {
          nextWebview.MioxInjectWebviewActive();
      }
  } else {
      key = _key;
  }
}

每次被渲染出来,webview构造体上都会写入一个基于URL的真实webview缓存,用来缓存式匹配到相应的webview。跳过创建等待的过程,加快渲染速度。

如果还是找不到,那么只能再创建

if (!nextWebview) {
  nextWebview = await engine.create(webview, args);
}

当我们都取到prevWeviewnextWebview的时候,我们就可以对这两个实例进行操作,但是需要注意的是,如果使用SSR渲染,那么有可能当前的nextWebview是不存在根节点的,因为根本还没有 mounted,那么你需要跳过节点动画操作,而直接返回这个nextWebview

如果都取到了,我们需要在动画结束的时候给2个节点分别移除或者添加class="active",用来最终呈现页面。

缓存的好处

我们尽可能使用缓存,可以达到运行性能的提升。这里的缓存逻辑,都会走中间件,但是会在渲染的构造体上做缓存,是因为,我们更加多变灵活的路由理念决定。比如说,当一个变量控制着一个路由的不同渲染。

import A from './a.vue';import B from './b.vue';const silent = false;

router.patch('/test', async ctx => {        if (silent) {                await ctx.render(A);
    } else {                await ctx.render(B);
    }
});

当前渲染了B页面。

当我们进入到了/test路由,跳转到了 /test2路由进行操作,在/test2路由上,我们对变量 silent=true进行了修改,然后又跳回到/test路由。此时,我们需要得到A页面。如果按照传统的路由设计,我们还是会得到B页面,因为,这是上一个缓存页面的结果。但是,实际情况,并非如此。我们理应得到A页面。

当我们返回到/test的时候,通过中间件,得到了webview的构造体,构造提上有存在此路由的具体webview实例的关联信息,那么直接使用缓存得到。

上述情况,也许传统的路由真的没法解决,只能通过一些hack的方式解决,比如说钩子什么的。但是我们的路由设计中,完全考虑了这种动态的真实情况,同时在这种情况上做了缓存,能够最大限度复用到已存在的webview对象。

Miox 引擎

引擎是Miox必不可少的一部分。如果没有了引擎,那么Miox无法渲染出页面。但是基于插拔式设计,引擎能够替换为各种想要的主流框架,无非建立一层驱动即可。

这里我们只对Vue引擎讲解,因为在51信用卡管家公司内部技术栈是Vue。

Vue@2x版本引擎详解

这里为也会介绍引擎的驱动设计模式。

首先引擎必须是一个class类型对象,在ES5中当然可以是一个可以实例化的 Function对象。

它必须提供一个async create() 方法。同时会获得两个参数:

  • webview 页面构造体

  • args 参数

此方法的设计模式跟传统的后台模板设计思路类似,都是由模板+数据构成。它的逻辑就是如何创建一个页面。

在服务端渲染下,我们无法知道节点,它是事后添加上去的,通过mounted事件能够知道。所以,在这种情况下,我们不需要指定节点是什么。除此之外,我们需要指定节点。

const Arguments = {}; if (['server', 'client'].indexOf(this.env) === -1) {
     Arguments.el = this.createWebviewRoot();
}

基于参数,我们创建出VM对象,然后返回出来,告诉引擎接收器创建出来的对象是什么。

Arguments.propsData = options || {};
Arguments.extends = Webview;new webview(Arguments).$on('webview:created', function(){
    resolve(this);
});

在这里,我们指定webview:created事件为方法结束事件,只有触发了这个事件,我们才认为渲染完成。

还有一个可选的方法是install,用户初始安装的时候自动执行代码,来设置一些东西。

install() {    // 将$miox注入到Vue对象上去
    Vue.prototype.$miox = this.ctx;    // 注册内部使用的一些指令
    directives(this.ctx);
}

这样引擎注册就完成了,一点都不难。无非搞清楚如何创建和初始化时候该做什么事情而已。

之后我们介绍下额外的webview设计模式。我们提供一个基础的webview标准化对象,用来兼容Miox内部逻辑。

何时创建完成?

我们需要使用created方法告诉系统。

@life created() {       this.$nextTick(
       () => this.$emit('webview:created')
    );
}
何时渲染完成?

我们使用mounted方法告诉系统。

@life mounted() {       this.__MioxInjectElement__ = this.$el.parentNode;       this.$nextTick(() => {               this.$emit('webview:mounted');               this.$miox.emit('webview:mounted', this);
    });
}
销毁时候我们该做什么?

因为在Miox中,我们预先对外部节点进行了包裹,所以在删除这个组件的时候告诉Miox为还需要删除哪些节点。

@life destroyed(){       if (this.__MioxInjectElement__ && this.__MioxInjectElement__.parentNode) {               this.__MioxInjectElement__.parentNode.removeChild(this.__MioxInjectElement__);
   }       this.$miox.emit('webview:destroyed', this);
}

销毁过程是什么样的?

MioxInjectDestroy(){ this.$destroy(); }
当局部刷新的时候,我们做什么?

什么是局部刷新?

局部刷新就是当URL改变的时候,系统不自动重新渲染页面,而是在当前页面上的某块区域改变视图结构。

我们需要通知这个webview一个refresh事件。

MioxInjectLocalRefresh() {       const refresher = !!this._events.refresh;       if (refresher) {               return (newValue, oldValue) => {                       this.$emit('refresh', newValue, oldValue);
        }
    }
}
页面激活映射?

所谓页面激活,无非就是页面在次进来的时候出发的事件。第一次进来不称为激活,而是在此从缓存中激活的时候才称为页面激活。

MioxInjectWebviewActive() {       this.$emit('webview:active');       this.$miox.emit('webview:active', this);
}
页面进入后台映射?

与页面激活理论相同,无法就是相反的结果。

MioxInjectWebviewUnActive() {       this.$emit('webview:unActive');       this.$miox.emit('webview:unActive', this);
}

React引擎

关于React引擎,这里不多做介绍,因为引擎的设计与VUE类似,可以参考 miox-react引擎设计的实现。

可能React引擎团队内部写的比较旧,已不兼容现在的Miox。在这里,我主要是解释一个问题,那就是我们只要写一个驱动,能够把所有渲染引擎接入到Miox中来。

总结

Miox实现了一套非常完备的路由体系,同时兼顾到页面的生命周期。这种解决方案已在51信用卡前端部门运作一年有余,我们团队对Miox的开发非常重视,希望各位看官多多关注!

Miox的开源计划将在下半年进行,到时候文档会及时奉上,在这里先叩谢各位看官了!