-
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 并不依赖任何框架,这意味着你的业务开发无论是基于 React、 Vue还是其他框架,都可以完美的接入到 Miox 中来。
Miox演进历程
51信用卡管家(杭州恩牛网络技术有限公司)从2015年6月开始,不断摸索适合公司内部的SPA模式。在考虑了业界其他的SPA模式基础上,我们觉得都有各种利弊,解决方案都不能完全解决目前公司内部的需要。
Soyie
2015年7月到10月,当时非常盛行MVVM思路的架构,所以我们考虑了一套属于自己的双向绑定架构,名字叫 Soyie。对其解释:
soyie是一套针对移动端开发的高性能MVVM前端框架,类似angularvuereactjsavalon。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-coremiox-simple -
引擎:可选模块
miox-vuemiox-vue2xmiox-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方法,那么我们需要做 location的replace,代码如下:
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);
}
ServerRenderer与 ClientRenderer函数将对首次加载起决定作用。
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);
}
当我们都取到prevWeview和 nextWebview的时候,我们就可以对这两个实例进行操作,但是需要注意的是,如果使用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的开源计划将在下半年进行,到时候文档会及时奉上,在这里先叩谢各位看官了!