UNPKG

magix

Version:

view manager framewrok

1,872 lines (1,723 loc) 148 kB
//#snippet; //#uncheck = jsThis,jsLoop; //#exclude = loader,allProcessor; /*!3.8.16 Licensed MIT*/ /* author:kooboy_li@163.com loader:cmd enables:style,viewInit,service,ceach,router,resource,configIni,nodeAttachVframe,viewMerge,tipRouter,updater,viewProtoMixins,base,defaultView,autoEndUpdate,linkage,updateTitleRouter,urlRewriteRouter,state,updaterDOM,viewInitAsync optionals:updaterVDOM,updaterQuick,updaterAsync,updaterTouchAttr,serviceCombine,servicePush,tipLockUrlRouter,edgeRouter,forceEdgeRouter,cnum,vframeHost,layerVframe,collectView,share,keepHTML,naked,viewChildren,dispatcherRecast */ define('magix', ['$'], require => { if (typeof DEBUG == 'undefined') window.DEBUG = true; let $ = require('$'); let G_IsObject = $.isPlainObject; let G_IsArray = $.isArray; let G_COUNTER = 0; let G_EMPTY = ''; let G_EMPTY_ARRAY = []; let G_COMMA = ','; let G_NULL = null; let G_WINDOW = window; let G_Undefined = void G_COUNTER; let G_DOCUMENT = document; let GA = G_DOCUMENT.documentElement.getAttribute; let G_GetAttribute = (node, attr) => GA.call(node, attr); let G_DOC = $(G_DOCUMENT); let Timeout = G_WINDOW.setTimeout; let G_CHANGED = 'changed'; let G_CHANGE = 'change'; let G_PAGE_UNLOAD = 'pageunload'; let G_VALUE = 'value'; let G_Tag_Key = 'mxs'; let G_Tag_Attr_Key = 'mxa'; let G_Tag_View_Key = 'mxv'; let G_HashKey = '#'; function G_NOOP() { } let JSONStringify = JSON.stringify; let G_DOCBODY; //initilize at vframe_root /* 关于spliter 出于安全考虑,使用不可见字符\u0000,然而,window手机上ie11有这样的一个问题:'\u0000'+"abc",结果却是一个空字符串,好奇特。 */ let G_SPLITER = '\x1e'; let Magix_StrObject = 'object'; let G_PROTOTYPE = 'prototype'; let G_PARAMS = 'params'; let G_PATH = 'path'; let G_MX_VIEW = 'mx-view'; // let Magix_PathRelativeReg = /\/\.(?:\/|$)|\/[^\/]+?\/\.{2}(?:\/|$)|\/\/+|\.{2}\//; // ./|/x/../|(b)/// // let Magix_PathTrimFileReg = /\/[^\/]*$/; // let Magix_ProtocalReg = /^(?:https?:)?\/\//i; let Magix_PathTrimParamsReg = /[#?].*$/; let Magix_ParamsReg = /([^=&?\/#]+)=?([^&#?]*)/g; let Magix_IsParam = /(?!^)=|&/; let G_Id = prefix => (prefix || 'mx_') + G_COUNTER++; let MxGlobalView = G_Id(); let Magix_Cfg = { rootId: G_Id(), defaultView: MxGlobalView, error(e) { throw e; } }; let G_GetById = id => typeof id == Magix_StrObject ? id : G_DOCUMENT.getElementById(id); let G_IsPrimitive = args => !args || typeof args != Magix_StrObject; let G_Set = (newData, oldData, keys, unchanged) => { let changed = 0, now, old, p; for (p in newData) { now = newData[p]; old = oldData[p]; if ((!G_IsPrimitive(now) || old !== now) && !G_Has(unchanged, p)) { keys[p] = 1; changed = 1; } oldData[p] = now; } return changed; }; let G_NodeIn = (a, b, r) => { a = G_GetById(a); b = G_GetById(b); if (a && b) { r = a == b; if (!r) { try { r = (b.compareDocumentPosition(a) & 16) == 16; } catch (_magix) { } } } return r; }; let { assign: G_Assign, keys: G_Keys, hasOwnProperty: Magix_HasProp } = Object; let Header = $('head'); let View_ApplyStyle = (key, css) => { if (DEBUG && G_IsArray(key)) { for (let i = 0; i < key.length; i += 2) { View_ApplyStyle(key[i], key[i + 1]); } return; } if (css && !View_ApplyStyle[key]) { View_ApplyStyle[key] = 1; if (DEBUG) { if (key.indexOf('$throw_') === 0) { throw new Error(css); } Header.append(`<style id="${key}">${css}`); } else { Header.append(`<style>${css}`); } } }; let IdIt = n => G_GetAttribute(n, 'id') || (n['$a'] = 1, n.id = G_Id()); let G_ToTry = (fns, args, context, r, e) => { args = args || G_EMPTY_ARRAY; if (!G_IsArray(fns)) fns = [fns]; if (!G_IsArray(args)) args = [args]; for (e of fns) { try { r = e && e.apply(context, args); } catch (x) { Magix_Cfg.error(x); } } return r; }; let G_Has = (owner, prop) => owner && Magix_HasProp.call(owner, prop); //false 0 G_NULL '' undefined let G_TranslateData = (data, params) => { let p, val; if (G_IsPrimitive(params)) { p = params + G_EMPTY; if (p[0] == G_SPLITER && G_Has(data, p)) { params = data[p]; } } else { for (p in params) { val = params[p]; val = G_TranslateData(data, val); params[p] = val; } } return params; }; let Magix_CacheSort = (a, b) => b.f - a.f || b.t - a.t; /** * Magix.Cache 类 * @name Cache * @constructor * @param {Integer} [max] 缓存最大值,默认20 * @param {Integer} [buffer] 缓冲区大小,默认5 * @param {Function} [remove] 当缓存的元素被删除时调用 * @example * let c = new Magix.cache(5,2);//创建一个可缓存5个,且缓存区为2个的缓存对象 * c.set('key1',{});//缓存 * c.get('key1');//获取 * c.del('key1');//删除 * c.has('key1');//判断 * //注意:缓存通常配合其它方法使用,在Magix中,对路径的解析等使用了缓存。在使用缓存优化性能时,可以达到节省CPU和内存的双赢效果 */ function G_Cache(max, buffer, remove, me) { me = this; me.c = []; me.b = buffer || 5; //buffer先取整,如果为0则再默认5 me.x = me.b + (max || 20); me.r = remove; } G_Assign(G_Cache[G_PROTOTYPE], { /** * @lends Cache# */ /** * 获取缓存的值 * @param {String} key * @return {Object} 初始设置的缓存对象 */ get(key) { let me = this; let c = me.c; let r = c[G_SPLITER + key]; if (r) { r.f++; r.t = G_COUNTER++; //console.log(r.f); r = r.v; //console.log('hit cache:'+key); } return r; }, /** * 循环缓存 * @param {Function} cb 回调 * @param {Object} [ops] 回调时传递的额外参数 * @beta * @module ceach|service */ each(cb, ops, me, c, i) { me = this; c = me.c; for (i of c) { cb(i.v, ops, me); } }, /** * 设置缓存 * @param {String} key 缓存的key * @param {Object} value 缓存的对象 */ set(okey, value) { let me = this; let c = me.c; let key = G_SPLITER + okey; let r = c[key]; let t = me.b, f; if (!r) { if (c.length >= me.x) { c.sort(Magix_CacheSort); while (t--) { r = c.pop(); //为什么要判断r.f>0,考虑这样的情况:用户设置a,b,主动删除了a,重新设置a,数组中的a原来指向的对象残留在列表里,当排序删除时,如果不判断则会把新设置的删除,因为key都是a // if (r.f > 0) me.del(r.o); //如果没有引用,则删除 } } r = { o: okey }; c.push(r); c[key] = r; } r.v = value; r.f = 1; r.t = G_COUNTER++; }, /** * 删除缓存 * @param {String} key 缓存key */ del(k) { k = G_SPLITER + k; let c = this.c; let r = c[k], m = this.r; if (r) { r.f = -1; r.v = G_EMPTY; delete c[k]; if (m) { G_ToTry(m, r.o); } } }, /** * 检测缓存中是否有给定的key * @param {String} key 缓存key * @return {Boolean} */ has(k) { return G_Has(this.c, G_SPLITER + k); } }); let G_DefaultView; let G_Require = (name, fn) => { if (name) { let a = [], n; if (MxGlobalView == name) { if (!G_DefaultView) { G_DefaultView = View.extend(); } fn(G_DefaultView); } else if (G_WINDOW.seajs) { seajs.use(name, (...g) => { for (let m of g) { a.push(m && m.__esModule && m.default || m); } if (fn) { CallFunction(fn, a); } }); } else { if (!G_IsArray(name)) name = [name]; for (n of name) { n = require(n); a.push(n && n.__esModule && n.default || n); } if (fn) fn(...a); } } else { fn(); } }; function T() { } let G_Extend = (ctor, base, props, statics, cProto) => { //bProto.constructor = base; T[G_PROTOTYPE] = base[G_PROTOTYPE]; cProto = new T(); G_Assign(cProto, props); G_Assign(ctor, statics); cProto.constructor = ctor; ctor[G_PROTOTYPE] = cProto; return ctor; }; let G_SelectorEngine = $.find || $.zepto; let G_TargetMatchSelector = G_SelectorEngine.matchesSelector || G_SelectorEngine.matches; let G_DOMGlobalProcessor = (e, d) => { d = e.data; e.eventTarget = d.e; G_ToTry(d.f, e, d.v); }; let G_DOMEventLibBind = (node, type, cb, remove, scope) => { if (scope) { type += `.${scope.i}`; } if (remove) { $(node).off(type, cb); } else { $(node).on(type, scope, cb); } }; let Safeguard = data => data; if (DEBUG && window.Proxy) { let ProxiesPool = new Map(); Safeguard = (data, getter, setter, root) => { if (G_IsPrimitive(data)) { return data; } let build = (prefix, o) => { let key = getter + '\x01' + setter; let cached = ProxiesPool.get(o); if (cached && cached.key == key) { return cached.entity; } if (o['\x1e_sf_\x1e']) { return o; } let entity = new Proxy(o, { set(target, property, value) { if (!setter && !prefix) { throw new Error('avoid writeback,key: ' + prefix + property + ' value:' + value + ' more info: https://github.com/thx/magix/issues/38'); } target[property] = value; if (setter) { setter(prefix + property, value); } return true; }, get(target, property) { if (property == '\x1e_sf_\x1e') { return true; } let out = target[property]; if (!prefix && getter) { getter(property); } if (!root && G_Has(target, property) && (G_IsArray(out) || G_IsObject(out))) { return build(prefix + property + '.', out); } return out; } }); ProxiesPool.set(o, { key, entity }); return entity; }; return build('', data); }; } let Magix_PathToObjCache = new G_Cache(); let Magix_Booted = 0; //let Magix_PathCache = new G_Cache(); let Magix_ParamsObjectTemp; let Magix_ParamsFn = (match, name, value) => { try { value = decodeURIComponent(value); } catch (_magix) { } Magix_ParamsObjectTemp[name] = value; }; /** * 路径 * @param {String} url 参考地址 * @param {String} part 相对参考地址的片断 * @return {String} * @example * http://www.a.com/a/b.html?a=b#!/home?e=f / => http://www.a.com/ * http://www.a.com/a/b.html?a=b#!/home?e=f ./ =>http://www.a.com/a/ * http://www.a.com/a/b.html?a=b#!/home?e=f ../../ => http://www.a.com/ * http://www.a.com/a/b.html?a=b#!/home?e=f ./../ => http://www.a.com/ * //g.cn/a.html */ /*let G_Path = function(url, part) { let key = url + G_SPLITER + part; let result = Magix_PathCache.get(key), domain = G_EMPTY, idx; if (!Magix_PathCache.has(key)) { //有可能结果为空,url='' path=''; let m = url.match(Magix_ProtocalReg); if (m) { idx = url.indexOf(Magix_SLASH, m[0].length); if (idx < 0) idx = url.length; domain = url.slice(0, idx); url = url.slice(idx); } url = url.replace(Magix_PathTrimParamsReg, G_EMPTY).replace(Magix_PathTrimFileReg, Magix_SLASH); if (!part.indexOf(Magix_SLASH)) { url = G_EMPTY; } result = url + part; console.log('url', url, 'part', part, 'result', result); while (Magix_PathRelativeReg.test(result)) { result = result.replace(Magix_PathRelativeReg, Magix_SLASH); } Magix_PathCache.set(key, result = domain + result); } return result; };*/ /** * 把路径字符串转换成对象 * @param {String} path 路径字符串 * @return {Object} 解析后的对象 * @example * let obj = Magix.parseUri('/xxx/?a=b&c=d'); * // obj = {path:'/xxx/',params:{a:'b',c:'d'}} */ let G_ParseUri = path => { //把形如 /xxx/?a=b&c=d 转换成对象 {path:'/xxx/',params:{a:'b',c:'d'}} //1. /xxx/a.b.c.html?a=b&c=d path /xxx/a.b.c.html //2. /xxx/?a=b&c=d path /xxx/ //3. /xxx/#?a=b => path /xxx/ //4. /xxx/index.html# => path /xxx/index.html //5. /xxx/index.html => path /xxx/index.html //6. /xxx/# => path /xxx/ //7. a=b&c=d => path '' //8. /s?src=b# => path /s params:{src:'b'} //9. a=YT3O0sPH1No= => path '' params:{a:'YT3O0sPH1No='} //10.a=YT3O0sPH1No===&b=c => path '' params:{a:'YT3O0sPH1No===',b:'c'} //11. ab?a&b => path ab params:{a:'',b:''} //12. a=b&c => path '' params:{a:'b',c:''} //13. =abc => path '=abc' //14. ab= => path '' params:{ab:''} //15. a&b => path '' params:{a:'',b:''} let r = Magix_PathToObjCache.get(path), pathname; if (!r) { Magix_ParamsObjectTemp = {}; pathname = path.replace(Magix_PathTrimParamsReg, G_EMPTY); if (path == pathname && Magix_IsParam.test(pathname)) pathname = G_EMPTY; //考虑 YT3O0sPH1No= base64后的pathname path.replace(pathname, G_EMPTY).replace(Magix_ParamsReg, Magix_ParamsFn); Magix_PathToObjCache.set(path, r = { a: pathname, b: Magix_ParamsObjectTemp }); } return { path: r.a, params: { ...r.b } }; }; /** * 转换成字符串路径 * @param {String} path 路径 * @param {Object} params 参数对象 * @param {Object} [keo] 保留空白值的对象 * @return {String} 字符串路径 * @example * let str = Magix.toUri('/xxx/',{a:'b',c:'d'}); * // str == /xxx/?a=b&c=d * * let str = Magix.toUri('/xxx/',{a:'',c:2}); * * // str == /xxx/?a=&c=2 * * let str = Magix.toUri('/xxx/',{a:'',c:2},{c:1}); * * // str == /xxx/?c=2 * let str = Magix.toUri('/xxx/',{a:'',c:2},{a:1,c:1}); * * // str == /xxx/?a=&c=2 */ let G_ToUri = (path, params, keo) => { let arr = [], v, p, f; for (p in params) { v = params[p] + G_EMPTY; if (v || G_Has(keo, p)) { v = encodeURIComponent(v); arr.push(f = p + '=' + v); } } if (f) { path += (path && (~path.indexOf('?') ? '&' : '?')) + arr.join('&'); } return path; }; let G_ToMap = (list, key) => { let e, map = {}, l; if (list) { for (e of list) { map[(key && e) ? e[key] : e] = key ? e : (map[e] | 0) + 1; //对于简单数组,采用累加的方式,以方便知道有多少个相同的元素 } } return map; }; let G_ParseCache = new G_Cache(); let G_ParseExpr = (expr, data, result) => { if (G_ParseCache.has(expr)) { result = G_ParseCache.get(expr); } else { //jshint evil:true result = G_ToTry(Function(`return ${expr}`)); if (expr.indexOf(G_SPLITER) > -1) { G_TranslateData(data, result); } else { G_ParseCache.set(expr, result); } } if (DEBUG) { result = Safeguard(result); } return result; }; let CallIndex = 0; let CallList = []; let CallBreakTime = 48; let StartCall = () => { let last = G_Now(), next; while (1) { next = CallList[CallIndex - 1]; if (next) { next.apply(CallList[CallIndex], CallList[CallIndex + 1]); CallIndex += 3; if (G_Now() - last > CallBreakTime && CallList.length > CallIndex) { setTimeout(StartCall); console.log(`[CF] take a break of ${CallList.length} at ${CallIndex}`); break; } } else { CallList.length = CallIndex = 0; break; } } }; let CallFunction = (fn, args, context) => { CallList.push(fn, context, args); if (!CallIndex) { CallIndex = 1; setTimeout(StartCall); } }; let Mark = (host, key) => { let deletedKey = G_SPLITER + '$a'; let markObjectKey = G_SPLITER + '$b'; let sign; if (!host[deletedKey]) { let markHost = host[markObjectKey] || (host[markObjectKey] = {}); if (!markHost.hasOwnProperty(key)) { markHost[key] = 0; } sign = ++markHost[key]; } return () => { let temp = host[markObjectKey]; return temp && sign === temp[key]; } }; let Unmark = host => { host[G_SPLITER + '$b'] = 0; host[G_SPLITER + '$a'] = 1; }; let EventDefaultOptions = { bubbles: true, cancelable: true }; let DispatchEvent = (element, type, data) => { let e = new Event(type, EventDefaultOptions); G_Assign(e, data); element.dispatchEvent(e); }; /** * Magix对象,提供常用方法 * @name Magix * @namespace */ let Magix = { /** * @lends Magix */ mark: Mark, unmark: Unmark, dispatch: DispatchEvent, task: CallFunction, /** * 设置或获取配置信息 * @param {Object} cfg 初始化配置参数对象 * @param {String} cfg.defaultView 默认加载的view * @param {String} cfg.defaultPath 当无法从地址栏取到path时的默认值。比如使用hash保存路由信息,而初始进入时并没有hash,此时defaultPath会起作用 * @param {Object} cfg.routes path与view映射关系表 * @param {String} cfg.unmatchView 在routes里找不到匹配时使用的view,比如显示404 * @param {String} cfg.rootId 根view的id * @param {Array} cfg.exts 需要加载的扩展 * @param {Function} cfg.error 发布版以try catch执行一些用户重写的核心流程,当出错时,允许开发者通过该配置项进行捕获。注意:您不应该在该方法内再次抛出任何错误! * @example * Magix.config({ * rootId:'J_app_main', * defaultView:'app/views/layouts/default',//默认加载的view * defaultPath:'/home', * routes:{ * "/home":"app/views/layouts/default" * } * }); * * * let config = Magix.config(); * * console.log(config.rootId); * * // 可以多次调用该方法,除内置的配置项外,您也可以缓存一些数据,如 * Magix.config({ * user:'彳刂' * }); * * console.log(Magix.config('user')); */ config(cfg, r) { r = Magix_Cfg; if (cfg) { if (G_IsObject(cfg)) { r = G_Assign(r, cfg); } else { r = r[cfg]; } } return r; }, /** * 应用初始化入口 * @function * @param {Object} [cfg] 配置信息对象,更多信息请参考Magix.config方法 * @return {Object} 配置信息对象 * @example * Magix.boot({ * rootId:'J_app_main' * }); * */ boot(cfg) { G_Assign(Magix_Cfg, cfg); //先放到配置信息中,供ini文件中使用 G_Require(Magix_Cfg.ini, I => { G_Assign(Magix_Cfg, I, cfg); G_Require(Magix_Cfg.exts, () => { Router.on(G_CHANGED, Dispatcher_NotifyChange); State.on(G_CHANGED, Dispatcher_NotifyChange); Magix_Booted = 1; Router_Bind(); }); }); }, /** * 把列表转化成hash对象 * @param {Array} list 源数组 * @param {String} [key] 以数组中对象的哪个key的value做为hash的key * @return {Object} * @example * let map = Magix.toMap([1,2,3,5,6]); * //=> {1:1,2:1,3:1,4:1,5:1,6:1} * * let map = Magix.toMap([{id:20},{id:30},{id:40}],'id'); * //=>{20:{id:20},30:{id:30},40:{id:40}} * * console.log(map['30']);//=> {id:30} * //转成对象后不需要每次都遍历数组查询 */ toMap: G_ToMap, /** * 以try cache方式执行方法,忽略掉任何异常 * @function * @param {Array} fns 函数数组 * @param {Array} [args] 参数数组 * @param {Object} [context] 在待执行的方法内部,this的指向 * @return {Object} 返回执行的最后一个方法的返回值 * @example * let result = Magix.toTry(function(){ * return true * }); * * // result == true * * let result = Magix.toTry(function(){ * throw new Error('test'); * }); * * // result == undefined * * let result = Magix.toTry([function(){ * throw new Error('test'); * },function(){ * return true; * }]); * * // result == true * * //异常的方法执行时,可以通过Magix.config中的error来捕获,如 * * Magix.config({ * error:function(e){ * console.log(e);//在这里可以进行错误上报 * } * }); * * let result = Magix.toTry(function(a1,a2){ * return a1 + a2; * },[1,2]); * * // result == 3 * let o={ * title:'test' * }; * let result = Magix.toTry(function(){ * return this.title; * },null,o); * * // result == 'test' */ toTry: G_ToTry, /** * 转换成字符串路径 * @function * @param {String} path 路径 * @param {Object} params 参数对象 * @param {Object} [keo] 保留空白值的对象 * @return {String} 字符串路径 * @example * let str = Magix.toUrl('/xxx/',{a:'b',c:'d'}); * // str == /xxx/?a=b&c=d * * let str = Magix.toUrl('/xxx/',{a:'',c:2}); * * // str==/xxx/?a=&c=2 * * let str = Magix.toUrl('/xxx/',{a:'',c:2},{c:1}); * * // str == /xxx/?c=2 * let str = Magix.toUrl('/xxx/',{a:'',c:2},{a:1,c:1}); * * // str == /xxx/?a=&c=2 */ toUrl: G_ToUri, /** * 把路径字符串转换成对象 * @function * @param {String} path 路径字符串 * @return {Object} 解析后的对象 * @example * let obj = Magix.parseUrl('/xxx/?a=b&c=d'); * // obj = {path:'/xxx/',params:{a:'b',c:'d'}} */ parseUrl: G_ParseUri, /* * 路径 * @function * @param {String} url 参考地址 * @param {String} part 相对参考地址的片断 * @return {String} * @example * http://www.a.com/a/b.html?a=b#!/home?e=f / => http://www.a.com/ * http://www.a.com/a/b.html?a=b#!/home?e=f ./ =>http://www.a.com/a/ * http://www.a.com/a/b.html?a=b#!/home?e=f ../../ => http://www.a.com/ * http://www.a.com/a/b.html?a=b#!/home?e=f ./../ => http://www.a.com/ */ //path: G_Path, /** * 把src对象的值混入到aim对象上 * @function * @param {Object} aim 要mix的目标对象 * @param {Object} src mix的来源对象 * @example * let o1={ * a:10 * }; * let o2={ * b:20, * c:30 * }; * * Magix.mix(o1,o2);//{a:10,b:20,c:30} * * * @return {Object} */ mix: G_Assign, /** * 检测某个对象是否拥有某个属性 * @function * @param {Object} owner 检测对象 * @param {String} prop 属性 * @example * let obj={ * key1:undefined, * key2:0 * } * * Magix.has(obj,'key1');//true * Magix.has(obj,'key2');//true * Magix.has(obj,'key3');//false * * * @return {Boolean} 是否拥有prop属性 */ has: G_Has, /** * 获取对象的keys * @param {Object} object 获取key的对象 * @type {Array} * @beta * @module linkage|router * @example * let o = { * a:1, * b:2, * test:3 * }; * let keys = Magix.keys(o); * * // keys == ['a','b','test'] * @return {Array} */ keys: G_Keys, /** * 判断一个节点是否在另外一个节点内,如果比较的2个节点是同一个节点,也返回true * @function * @param {String|HTMLElement} node节点或节点id * @param {String|HTMLElement} container 容器 * @example * let root = $('html'); * let body = $('body'); * * let r = Magix.inside(body[0],root[0]); * * // r == true * * let r = Magix.inside(root[0],body[0]); * * // r == false * * let r = Magix.inside(root[0],root[0]); * * // r == true * * @return {Boolean} */ inside: G_NodeIn, /** * document.getElementById的简写 * @param {String} id * @return {HTMLElement|Null} * @example * // html * // <div id="root"></div> * * let node = Magix.node('root'); * * // node => div[id='root'] * * // node是document.getElementById的简写 */ node: G_GetById, /** * 应用样式 * @beta * @module style * @param {String} prefix 样式的名称前缀 * @param {String} css 样式字符串 * @example * // 该方法配合magix-combine工具使用 * // 更多信息可参考magix-combine工具:https://github.com/thx/magix-combine * // 样式问题可查阅这里:https://github.com/thx/magix-combine/issues/6 * */ applyStyle: View_ApplyStyle, /** * 返回全局唯一ID * @function * @param {String} [prefix] 前缀 * @return {String} * @example * * let id = Magix.guid('mx-'); * // id maybe mx-7 */ guid: G_Id, use: G_Require, Cache: G_Cache, nodeId: IdIt, use: G_Require, guard: Safeguard }; /** * 多播事件对象 * @name Event * @namespace */ let MEvent = { /** * @lends MEvent */ /** * 触发事件 * @param {String} name 事件名称 * @param {Object} data 事件对象 * @param {Boolean} [remove] 事件触发完成后是否移除这个事件的所有监听 * @param {Boolean} [lastToFirst] 是否从后向前触发事件的监听列表 */ fire(name, data, remove, lastToFirst) { let key = G_SPLITER + name, me = this, list = me[key], end, len, idx, t; if (!data) data = {}; data.type = name; if (list) { end = list.length; len = end - 1; while (end--) { idx = lastToFirst ? end : len - end; t = list[idx]; if (t.f) { t.x = 1; G_ToTry(t.f, data, me); t.x = G_EMPTY; } else if (!t.x) { list.splice(idx, 1); len--; } } } list = me[`on${name}`]; if (list) G_ToTry(list, data, me); if (remove) me.off(name); return me; }, /** * 绑定事件 * @param {String} name 事件名称 * @param {Function} fn 事件处理函数 * @example * let T = Magix.mix({},Magix.Event); * T.on('done',function(e){ * alert(1); * }); * T.on('done',function(e){ * alert(2); * T.off('done',arguments.callee); * }); * T.fire('done',{data:'test'}); * T.fire('done',{data:'test2'}); */ on(name, f) { let me = this; let key = G_SPLITER + name; let list = me[key] || (me[key] = []); list.push({ f }); return me; }, /** * 解除事件绑定 * @param {String} name 事件名称 * @param {Function} [fn] 事件处理函数 */ off(name, fn) { let key = G_SPLITER + name, me = this, list = me[key], t; if (fn) { if (list) { for (t of list) { if (t.f == fn) { t.f = G_EMPTY; break; } } } } else { delete me[key]; delete me[`on${name}`]; } return me; } }; Magix.Event = MEvent; let State_AppData = {}; let State_AppDataKeyRef = {}; let State_ChangedKeys = {}; let State_DataIsChanged = 0; let State_DataWhereSet = {}; let State_IsObserveChanged = (view, keys, r) => { let oKeys = view['$os'], ok; if (oKeys) { for (ok of oKeys) { r = G_Has(keys, ok); if (r) break; } } return r; }; let SetupKeysRef = keys => { keys = (keys + G_EMPTY).split(','); for (let key of keys) { if (G_Has(State_AppDataKeyRef, key)) { State_AppDataKeyRef[key]++; } else { State_AppDataKeyRef[key] = 1; } } return keys; }; let TeardownKeysRef = keys => { let key, v; for (key of keys) { if (G_Has(State_AppDataKeyRef, key)) { v = --State_AppDataKeyRef[key]; if (!v) { delete State_AppDataKeyRef[key]; delete State_AppData[key]; if (DEBUG) { delete State_DataWhereSet[key]; } } } } }; if (DEBUG) { setTimeout(() => { Router.on('changed', () => { setTimeout(() => { let keys = []; let cls = []; for (let p in State_DataWhereSet) { if (!State_AppDataKeyRef[p]) { cls.push(p); keys.push('key:"' + p + '" set by page:"' + State_DataWhereSet[p] + '"'); } } if (keys.length) { console.warn('beware! Remember to clean ' + keys + ' in {Magix.State} Clean use view.mixins like mixins:[Magix.State.clean("' + cls + '")]'); } }, 200); }); }, 0); } if (DEBUG) { let Started = 0; let NotifyList = []; let NotifyTimer = 0; let Notify = () => { let locker = {}; for (let n of NotifyList) { if (!locker[n.msg]) { console.warn(n.msg); locker[n.msg] = 1; } } NotifyList.length = 0; Started = 0; }; var ClearNotify = key => { for (let i = NotifyList.length; i--;) { let n = NotifyList[i]; if (n.key == key) { NotifyList.splice(i, 1); } } }; var DelayNotify = (key, msg) => { clearTimeout(NotifyTimer); Started = 0; NotifyList.push({ key, msg }); if (!Started) { Started = 1; NotifyTimer = setTimeout(Notify, 500); } }; } /** * 可观察的内存数据对象 * @name State * @namespace * @borrows Event.on as on * @borrows Event.fire as fire * @borrows Event.off as off * @beta * @module router */ let State = { /** * @lends State */ /** * 从Magix.State中获取数据 * @param {String} [key] 数据key * @return {Object} */ get(key) { let r = key ? State_AppData[key] : State_AppData; if (DEBUG) { if (key && Magix_Booted) { let loc = Router.parse(); if (G_Has(State_DataWhereSet, key) && State_DataWhereSet[key] != loc.path) { console.warn('beware! You get state:"{Magix.State}.' + key + '" where it set by page:' + State_DataWhereSet[key]); } } r = Safeguard(r, dataKey => { if (Magix_Booted) { let loc = Router.parse(); if (G_Has(State_DataWhereSet, dataKey) && State_DataWhereSet[dataKey] != loc.path) { console.warn('beware! You get state:"{Magix.State}.' + dataKey + '" where it set by page:' + State_DataWhereSet[dataKey]); } } }, (path, value) => { let sub = key ? key : path; DelayNotify(sub, 'beware! You direct modify "{Magix.State}.' + sub + '" You should call Magix.State.set() and Magix.State.digest() to notify other views {Magix.State} changed'); }); } return r; }, /** * 设置数据 * @param {Object} data 数据对象 */ set(data, unchanged) { State_DataIsChanged = G_Set(data, State_AppData, State_ChangedKeys, unchanged) || State_DataIsChanged; if (DEBUG && Magix_Booted) { let loc = Router.parse(); for (let p in data) { State_DataWhereSet[p] = loc.path; } } return this; }, /** * 检测数据变化,如果有变化则派发changed事件 * @param {Object} data 数据对象 */ digest(data, unchanged) { if (data) { State.set(data, unchanged); } if (State_DataIsChanged) { if (DEBUG) { for (let p in State_ChangedKeys) { ClearNotify(p); } } State_DataIsChanged = 0; //防止在change事件中再次digest造成的死循环 let keys = G_Assign({}, State_ChangedKeys); State_ChangedKeys = {}; this.fire(G_CHANGED, { keys }); } }, /** * 获取当前数据与上一次数据有哪些变化 * @return {Object} */ diff() { return State_ChangedKeys; }, setup(keys) { SetupKeysRef(keys); }, teardown(keys) { TeardownKeysRef(keys); }, /** * 清除数据,该方法需要与view绑定,写在view的mixins中,如mixins:[Magix.Sate.clean('user,permission')] * @param {String} keys 数据key */ clean(keys) { if (DEBUG) { let called = false; setTimeout(() => { if (!called) { throw new Error('Magix.State.clean only used in View.mixins like mixins:[Magix.State.clean("p1,p2,p3")]'); } }, 1000); return { '\x1e': keys, ctor() { let me = this; called = true; keys = SetupKeysRef(keys); me.on('destroy', () => { TeardownKeysRef(keys); }); } }; } return { ctor() { keys = SetupKeysRef(keys); this.on('destroy', () => TeardownKeysRef(keys)); } }; }, ...MEvent /** * 当State中的数据有改变化后触发 * @name State.changed * @event * @param {Object} e 事件对象 * @param {Object} e.keys 包含哪些数据变化的key集合 */ }; Magix.State = State; //let G_IsFunction = $.isFunction; let Router_VIEW = 'view'; let Router_HrefCache = new G_Cache(); let Router_ChgdCache = new G_Cache(); let Router_WinLoc = G_WINDOW.location; let Router_LastChanged; let Router_Silent = 0; let Router_LLoc = { query: {}, params: {}, href: G_EMPTY }; let Router_TrimHashReg = /(?:^.*\/\/[^\/]+|#.*$)/gi; let Router_TrimQueryReg = /^[^#]*#?!?/; function GetParam(key, defaultValue) { return this[G_PARAMS][key] || defaultValue !== G_Undefined && defaultValue || G_EMPTY; } let Router_Edge = 0; let Router_Hashbang = G_HashKey + '!'; let Router_UpdateHash = (path, replace) => { path = Router_Hashbang + path; if (replace) { Router_WinLoc.replace(path); } else { Router_WinLoc.hash = path; } }; let Router_Update = (path, params, loc, replace, silent, lQuery) => { path = G_ToUri(path, params, lQuery); if (path != loc.srcHash) { Router_Silent = silent; Router_UpdateHash(path, replace); } }; let Router_Bind = () => { let lastHash = Router_Parse().srcHash; let newHash, suspend; G_DOMEventLibBind(G_WINDOW, 'hashchange', (e, loc, resolve) => { if (suspend) { return; } loc = Router_Parse(); newHash = loc.srcHash; if (newHash != lastHash) { resolve = () => { e.p = 1; lastHash = newHash; suspend = G_EMPTY; Router_UpdateHash(newHash); Router_Diff(); }; e = { reject() { e.p = 1; suspend = G_EMPTY; Router_UpdateHash(lastHash); }, resolve, prevent() { suspend = 1; } }; Router.fire(G_CHANGE, e); if (!suspend && !e.p) { resolve(); } } }); G_DOMEventLibBind(G_WINDOW, 'beforeunload', (e, te, msg) => { e = e || G_WINDOW.event; te = {}; Router.fire(G_PAGE_UNLOAD, te); if ((msg = te.msg)) { //chrome use e.returnValue and ie use return value if (e) e.returnValue = msg; return msg; } }); Router_Diff(); }; let Router_PNR_Routers, Router_PNR_UnmatchView, /*Router_PNR_IsFun,*/ Router_PNR_DefaultView, Router_PNR_DefaultPath; let Router_PNR_Rewrite; let DefaultTitle = G_DOCUMENT.title; let Router_AttachViewAndPath = (loc, view) => { if (!Router_PNR_Routers) { Router_PNR_Routers = Magix_Cfg.routes || {}; Router_PNR_UnmatchView = Magix_Cfg.unmatchView; Router_PNR_DefaultView = Magix_Cfg.defaultView; Router_PNR_DefaultPath = Magix_Cfg.defaultPath || '/'; //Router_PNR_IsFun = G_IsFunction(Router_PNR_Routers); //if (!Router_PNR_IsFun && !Router_PNR_Routers[Router_PNR_DefaultPath]) { // Router_PNR_Routers[Router_PNR_DefaultPath] = Router_PNR_DefaultView; //} Router_PNR_Rewrite = Magix_Cfg.rewrite; //if (!G_IsFunction(Router_PNR_Rewrite)) { // Router_PNR_Rewrite = G_NULL; //} } if (!loc[Router_VIEW]) { let path = loc.hash[G_PATH] || (Router_Edge && loc.query[G_PATH]) || Router_PNR_DefaultPath; if (Router_PNR_Rewrite) { path = Router_PNR_Rewrite(path, loc[G_PARAMS], Router_PNR_Routers); } //if (Router_PNR_IsFun) { // view = Router_PNR_Routers.call(Magix_Cfg, path, loc); //} else { view = Router_PNR_Routers[path] || Router_PNR_UnmatchView || Router_PNR_DefaultView; //} loc[G_PATH] = path; loc[Router_VIEW] = view; if (G_IsObject(view)) { if (DEBUG) { if (!view.view) { console.error(path, ' config missing view!', view); } } G_Assign(loc, view); } } }; let Router_GetChged = (oldLocation, newLocation) => { let oKey = oldLocation.href; let nKey = newLocation.href; let tKey = oKey + G_SPLITER + nKey; let result = Router_ChgdCache.get(tKey); if (!result) { let hasChanged, rps; result = { params: rps = {}, //isParam: Router_IsParam, //location: newLocation, force: !oKey //是否强制触发的changed,对于首次加载会强制触发一次 }; let oldParams = oldLocation[G_PARAMS], newParams = newLocation[G_PARAMS], tArr = G_Keys(oldParams).concat(G_Keys(newParams)), key; let setDiff = key => { let from = oldParams[key], to = newParams[key]; if (from != to) { rps[key] = { from, to }; hasChanged = 1; } }; for (key of tArr) { setDiff(key); } oldParams = oldLocation; newParams = newLocation; rps = result; setDiff(G_PATH); setDiff(Router_VIEW); Router_ChgdCache.set(tKey, result = { a: hasChanged, b: result }); } return result; }; let Router_Parse = href => { href = href || Router_WinLoc.href; let result = Router_HrefCache.get(href), srcQuery, srcHash, query, hash, params; if (!result) { srcQuery = href.replace(Router_TrimHashReg, G_EMPTY); srcHash = href.replace(Router_TrimQueryReg, G_EMPTY); query = G_ParseUri(srcQuery); hash = G_ParseUri(srcHash); params = { ...query[G_PARAMS] , ...hash[G_PARAMS] }; result = { get: GetParam, href, srcQuery, srcHash, query, hash, params }; if (Magix_Booted) { Router_AttachViewAndPath(result); Router_HrefCache.set(href, result); } if (DEBUG) { result.params = Safeguard(result.params); result = Safeguard(result); } } return result; }; let Router_Diff = () => { let location = Router_Parse(); let changed = Router_GetChged(Router_LLoc, Router_LLoc = location); if (!Router_Silent && changed.a) { Router_LastChanged = changed.b; if (Router_LastChanged[G_PATH]) { G_DOCUMENT.title = location.title || DefaultTitle; } Router.fire(G_CHANGED, Router_LastChanged ); } Router_Silent = 0; if (DEBUG) { Router_LastChanged = Safeguard(Router_LastChanged); } return Router_LastChanged; }; //let PathTrimFileParamsReg=/(\/)?[^\/]*[=#]$/;//).replace(,'$1').replace(,EMPTY); //let PathTrimSearch=/\?.*$/; /** * 路由对象,操作URL * @name Router * @namespace * @borrows Event.on as on * @borrows Event.fire as fire * @borrows Event.off as off * @beta * @module router */ let Router = { /** * @lends Router */ /** * 解析href的query和hash,默认href为location.href * @param {String} [href] href * @return {Object} 解析的对象 */ parse: Router_Parse, /** * 根据location.href路由并派发相应的事件,同时返回当前href与上一个href差异对象 * @example * let diff = Magix.Router.diff(); * if(diff.params.page || diff.params.rows){ * console.log('page or rows changed'); * } */ diff: Router_Diff, /** * 导航到新的地址 * @param {Object|String} pn path或参数字符串或参数对象 * @param {String|Object} [params] 参数对象 * @param {Boolean} [replace] 是否替换当前历史记录 * @example * let R = Magix.Router; * R.to('/list?page=2&rows=20');//改变path和相关的参数,地址栏上的其它参数会进行丢弃,不会保留 * R.to('page=2&rows=20');//只修改参数,地址栏上的其它参数会保留 * R.to({//通过对象修改参数,地址栏上的其它参数会保留 * page:2, * rows:20 * }); * R.to('/list',{//改变path和相关参数,丢弃地址栏上原有的其它参数 * page:2, * rows:20 * }); * * //凡是带path的修改地址栏,都会把原来地址栏中的参数丢弃 * 传递对象,内部对value会进行encodeURIComponent操作,传递字符串需要开发者自己处理。 * R.to({ * page:2, * rows:20 * },null,true);//使用location.replace操作hash * R.to({ * page:2, * rows:20 * },null,null,true);//静默更新url但不派发事件 */ to(pn, params, replace, silent) { if (!params && G_IsObject(pn)) { params = pn; pn = G_EMPTY; } let temp = G_ParseUri(pn); let tParams = temp[G_PARAMS]; let tPath = temp[G_PATH]; let lPath = Router_LLoc[G_PATH]; //历史路径 let lParams = Router_LLoc[G_PARAMS]; let lQuery = Router_LLoc.query[G_PARAMS]; G_Assign(tParams, params); //把路径中解析出来的参数与用户传递的参数进行合并 if (tPath) { //设置路径带参数的形式,如:/abc?q=b&c=e或不带参数 /abc //tPath = G_Path(lPath, tPath); if (!Router_Edge) { //pushState不用处理 for (lPath in lQuery) { //未出现在query中的参数设置为空 if (!G_Has(tParams, lPath)) tParams[lPath] = G_EMPTY; } } } else if (lParams) { //只有参数,如:a=b&c=d tPath = lPath; //使用历史路径 tParams = { ...lParams, ...tParams }; //复制原来的参数,合并新的参数 } Router_Update(tPath, tParams, Router_LLoc, replace, silent, lQuery); }, ...MEvent /** * 当location.href有改变化后触发 * @name Router.changed * @event * @param {Object} e 事件对象 * @param {Object} e.path 如果path发生改变时,记录从(from)什么值变成(to)什么值的对象 * @param {Object} e.view 如果view发生改变时,记录从(from)什么值变成(to)什么值的对象 * @param {Object} e.params 如果参数发生改变时,记录从(from)什么值变成(to)什么值的对象 * @param {Boolean} e.force 标识是否是第一次强制触发的changed,对于首次加载完Magix,会强制触发一次changed */ }; Magix.Router = Router; let Dispatcher_UpdateTag = 0; /** * 通知当前vframe,地址栏发生变化 * @param {Vframe} vframe vframe对象 * @private */ let Dispatcher_Update = (vframe, stateKeys, view, isChanged, cs, c, promise) => { if (vframe && vframe['$a'] != Dispatcher_UpdateTag && (view = vframe['$v']) && view['$a'] > 1) { //存在view时才进行广播,对于加载中的可在加载完成后通过调用view.location拿到对应的G_WINDOW.location.href对象,对于销毁的也不需要广播 isChanged = stateKeys ? State_IsObserveChanged(view, stateKeys) : View_IsObserveChanged(view); /** * 事件对象 * @type {Object} * @ignore */ /*let args = { location: RefLoc, changed: RefG_LocationChanged,*/ /** * 阻止向所有的子view传递 * @ignore */ /* prevent: function() { args.cs = EmptyArr; },*/ /** * 向特定的子view传递 * @param {Array} c 子view数组 * @ignore */ /*to: function(c) { c = (c + EMPTY).split(COMMA); args.cs = c; } };*/ if (isChanged) { //检测view所关注的相应的参数是否发生了变化 promise = view['$b'](); } if (!promise || !promise.then) { promise = Vframe_Promise; } promise.then(() => { cs = vframe.children(); for (c of cs) { Dispatcher_Update(Vframe_Vframes[c], stateKeys ); } }); } }; /** * 向vframe通知地址栏发生变化 * @param {Object} e 事件对象 * @param {Object} e.location G_WINDOW.location.href解析出来的对象 * @private */ let Dispatcher_NotifyChange = (e, vf, view) => { vf = Vframe_Root(); if ((view = e[Router_VIEW])) { vf.mountView(view.to); } else { Dispatcher_UpdateTag = G_COUNTER++; Dispatcher_Update(vf , e.keys ); } }; let Vframe_RootVframe; let Vframe_GlobalAlter; let Vframe_Vframes = {}; let Vframe_Promise = { then: f => f() }; let Vframe_NotifyCreated = vframe => { if (!vframe['$b'] && !vframe['$d'] && vframe['$cc'] == vframe['$rc']) { //childrenCount === readyCount if (!vframe['$cr']) { //childrenCreated vframe['$cr'] = 1; //childrenCreated vframe['$ca'] = 0; //childrenAlter vframe.fire('created'); //不在view上派发事件,如果view需要绑定,则绑定到owner上,view一般不用该事件,如果需要这样处理:this.owner.oncreated=function(){};this.ondestroy=function(){this.owner.off('created')} } let { id, pId } = vframe, p = Vframe_Vframes[pId]; if (p && !G_Has(p['$e'], id)) { //readyChildren p['$e'][id] = 1; //readyChildren p['$rc']++; //readyCount Vframe_NotifyCreated(p); } } }; let Vframe_NotifyAlter = (vframe, e) => { if (!vframe['$ca'] && vframe['$cr']) { //childrenAlter childrenCreated 当前vframe触发过created才可以触发alter事件 vframe['$cr'] = 0; //childrenCreated vframe['$ca'] = 1; //childreAleter vframe.fire('alter', e); let { id, pId } = vframe, p = Vframe_Vframes[pId]; //let vom = vframe.owner; if (p && G_Has(p['$e'], id)) { //readyMap p['$rc']--; //readyCount delete p['$e'][id]; //readyMap Vframe_NotifyAlter(p, e); } } }; let Vframe_TranslateQuery = (pId, src, params, pVf) => { pVf = Vframe_Vframes[pId]; pVf = pVf && pVf['$v']; pVf = pVf ? pVf['$d']['$a'] : {}; if (src.indexOf(G_SPLITER) > 0) { G_TranslateData(pVf, params); } return pVf; }; /** * 获取根vframe; * @return {Vframe} * @private */ let Vframe_Root = (rootId, e) => { if (!Vframe_RootVframe) { /* 尽可能的延迟配置,防止被依赖时,配置信息不正确 */ G_DOCBODY = G_DOCUMENT.body; rootId = Magix_Cfg.rootId; e = G_GetById(rootId); if (!e) { G_DOCBODY.id = rootId; } Vframe_RootVframe = new Vframe(rootId); } return Vframe_RootVframe; }; let Vframe_AddVframe = (id, vframe) => { if (!G_Has(Vframe_Vframes, id)) { Vframe_Vframes[id] = vframe; Vframe.fire('add', { vframe }); id = G_GetById(id); if (id) id.vframe = vframe; } }; let Vframe_RunInvokes = (vf, list, o) => { list = vf['$f']; //invokeList while (list.length) { o = list.shift(); if (!o.r) { //remove vf.invoke(o.n, o.a); //name,arguments } delete list[o.k]; //key } }; let Vframe_Cache = []; let Vframe_RemoveVframe = (id, fcc, vframe) => { vframe = Vframe_Vframes[id]; if (vframe) { delete Vframe_Vframes[id]; Vframe.fire('remove', { vframe, fcc //fireChildrenCreated }); if (DEBUG) { let nodes = G_DOCUMENT.querySelectorAll('#' + id); if (nodes.length > 1) { Magix_Cfg.error(Error(`remove vframe error. dom id:"${id}" duplicate`)); } } id = G_GetById(id); if (id) { id['$b'] = 0; id.vframe = 0; id['$a'] = 0; } } }; /** * Vframe类 * @name Vframe * @class * @constructor * @borrows Event.on as on * @borrows Event.fire as fire * @borrows Event.off as off * @borrows Event.on as #on * @borrows