你是否想知道易企秀炫酷的H5是如何实现的,原理是什么,本文会为你揭秘并还原压缩过的源代码。
易企秀是一款h5页面制作工具,因方便易用成为业界标杆。后续一个项目会用到类似易企秀这样的自定义H5的功能,因此首先分析下易企秀的前端代码,看看他们是怎么实现的,再取其精华去其糟粕。
由于代码较多,且是压缩处理过的,阅读和还原起来较为困难,不过可以先大概分析下原理,然后有针对性的看主要代码,并借助VS Code等工具对变量、函数进行重命名,稍微耐心一点就能大概还原源代码。
分析数据模型
前端分析第一步,看看易企秀的数据模型:
dataList是页面配置信息,elemengts是页面元素的配置信息,obj是H5的配置信息,
加载流程分析
查看H5源代码,发现入口函数是:
eqShow.bootstrap();
顺藤摸瓜,大概分析下,主要流程如下:
主要的功能函数在eqxiu和window对象下面,其中的重点是parsePage、renderPage和app,下面一一来分析。
parsePage
先看主要代码(重命名后的),主要功能是为每一页生成一个section并appendTo(".nr"),另外如果页面有特效,加载相关js库并执行,最后再renderPage。
function parsePage(dataList, response) {
for (var pageIndex = 1; pageIndex <= dataList.length; pageIndex++) {
// 分页容器
$(' ').appendTo(".nr");
if (10 == pageMode) {
$("#page" + pageIndex).parent(".main-page").wrap(''),
$(".main-page").css({
width: $(".nr").width() + "px",
height: $(".nr").height() + "px"
});
}
if (dataList.length > 1 && 14 != pageMode && !response.obj.property.forbidHandFlip) {
if (0 == pageMode || 1 == pageMode || 2 == pageMode || 6 == pageMode || 7 == pageMode ||
8 == pageMode || 11 == pageMode || 12 == pageMode || 13 == pageMode || 14 == pageMode) {
$(' ')
.appendTo("#page" + pageIndex)
} else if (3 == pageMode || 4 == pageMode || 5 == pageMode || 9 == pageMode || 10 == pageMode) {
$(' ')
.appendTo("#page" + pageIndex);
}
}
....
renderPage(eqShow, pageIndex, dataList);
// 最后一页
if (pageIndex == dataList.length) {
eqxiu.app($(".nr"), response.obj.pageMode, dataList, response);
addEnabledClassToPageCtrl(response);
}
}
}
hasSymbols || addReportToLastPage(dataList, response);
}
渲染页面和组件
parsePage搭建了页面框架,renderPage实现页面渲染。
rendepage里,核心代码是:
eqShow.templateParser("jsonParser").parse({
def: dataList[pageIndex - 1],
appendTo: "#page" + pageIndex,
mode: "view",
disEvent: disEvent
});
templateParser负责将页面上的elements还原为组件,因此这里核心是要了解下templateParser,大致还原的代码如下:
var jsonTemplateParser = eqShow.templateParser("jsonParser", function () {
function createContainerFunction(container) {
return function (key, value) {
container[key] = value
}
}
function wrapComp(element, mode) {
try {
var comp = components[("" + element.type).charAt(0)](element, mode)
} catch (e) {
return
}
if (comp) {
var elementContainer = $(''),
elementType = ("" + element.type).charAt(0);
if ("3" !== elementType && "1" !== elementType) {
elementContainer.attr("comp-resize", "")
}
// 组件类型
/**
* 2 文本
* 3 背景
* 9 音乐
* v video
* 4 图片
* h shape形状
* p 图集
* 5 输入框
* r radio
* c checkbox
* z 多选按钮
* a 评分组件
* b 留言板
* 6 提交按钮
*/
switch (elementType) {
case "p":
elementContainer.removeAttr("comp-rotate");
break;
case "1":
elementContainer.removeAttr("comp-drag");
break;
case "2": // 文本
elementContainer.addClass("wsite-text");
break;
case "3":
// 背景
break;
case "x":
elementContainer.addClass("show-text");
break;
case "4":
// image
element.properties.imgStyle && $(comp).css(element.properties.imgStyle), elementContainer.addClass("wsite-image");
break;
case "n":
elementContainer.addClass("wsite-image");
break;
case "h":
elementContainer.addClass("wsite-shape")
break;
case "5":
elementContainer.removeAttr("comp-input");
break;
case "6":
case "8":
elementContainer.removeAttr("comp-button");
break;
case "v":
elementContainer.removeAttr("comp-video");
elementContainer.addClass("wsite-video");
if (element.properties && element.properties.lock) {
elementContainer.addClass("alock")
}
break;
case "b":
elementContainer.removeAttr("comp-boards");
elementContainer.attr("min-h", 60),
elementContainer.attr("min-w", 230);
break;
default:
break;
}
elementContainer.mouseenter(function () {
$(this).addClass("inside-hover")
}),
elementContainer.mouseleave(function () {
$(this).removeClass("inside-hover")
});
// edit或者非文本type,再套一层
if ("edit" === jsonTemplateParser.mode || "x" !== ("" + element.type).charAt(0)) {
var elementBoxContent = $(''),
elementBox = $('').append(elementBoxContent.append(comp));
elementContainer.append(elementBox),
"5" !== ("" + element.type).charAt(0) && "6" !== ("" + element.type).charAt(0) && "r" !== element.type && "c" !== element.type && "a" !== element.type && "8" !== element.type && "l" !== element.type && "s" !== element.type && "i" !== element.type && "h" !== element.type && "z" !== element.type || "edit" !== mode || $(comp).before($(''))
}
// 文本类型,处理font
var k, eleFonts = element.fonts || element.css.fontFamily || element.fontFamily;
if ("2" === elementType || "x" === elementType) {
for (var content = element.content, font_pattern = /font-family:(.*?);/g, matchResults = [], fonts = []; null !== (matchResults = font_pattern.exec(content));)
fonts.push(matchResults[1].trim());
if (1 !== fonts.length || "defaultFont" !== fonts[0] && "moren" !== fonts[0] || (eleFonts = null),
eleFonts) {
if ("view" === jsonTemplateParser.mode && element.css.fontFamily && window.scene && (window.scene.publishTime || !mobilecheck() && !tabletCheck() || (k = "@font-face{font-family:" + element.css.fontFamily + ';src: url("' + element.properties.localFontPath + '") format("truetype");}',
b(k))),
"object" == typeof eleFonts && eleFonts.constructor === Object) {
if (!jQuery.isEmptyObject(eleFonts))
for (var q in eleFonts)
u[q] || ("edit" === jsonTemplateParser.mode ? k = "@font-face{font-family:" + q + ";src: url(" + PREFIX_FILE_HOST + eleFonts[q] + ") format(woff);}" : window.scene && window.scene.publishTime && (k = "@font-face{font-family:" + q + ';src: url("' + PREFIX_S2_URL + "fc/" + q + "_" + element.sceneId + "_" + scene.publishTime + '.woff") format("woff");}'),
b(k),
u[q] = !0)
} else
u[eleFonts] || ("edit" === jsonTemplateParser.mode ? k = "@font-face{font-family:" + eleFonts + ";src: url(" + PREFIX_FILE_HOST + element.preWoffPath + ") format(woff);}" : window.scene && window.scene.publishTime && (k = "@font-face{font-family:" + eleFonts + ';src: url("' + PREFIX_S2_URL + "fc/" + eleFonts + "_" + element.sceneId + "_" + scene.publishTime + '.woff") format("woff");}'),
b(k),
u[eleFonts] = !0);
"edit" === jsonTemplateParser.mode && localStorage.setItem("shoppingFontFamily", JSON.stringify(u))
}
}
// 处理css
if (element.css) {
var elementWidth = 320 - parseInt(element.css.left, 10);
elementContainer.css({
width: elementWidth
});
elementContainer.css({
width: element.css.width,
height: element.css.height,
left: element.css.left,
top: element.css.top,
zIndex: element.css.zIndex,
bottom: element.css.bottom,
transform: element.css.transform
});
if (0 === element.css.boxShadowSize || "" + element.css.boxShadowSize == "0") {
element.css.boxShadow = "0px 0px 0px rgba(0,0,0,0.5)";
if ("edit" !== jsonTemplateParser.mode && "x" === ("" + element.type).charAt(0)) {
return elementContainer.append(comp),
elementContainer.find(".element-box").css({
borderStyle: element.css.borderStyle,
borderWidth: element.css.borderWidth,
borderColor: element.css.borderColor,
borderTopLeftRadius: element.css.borderTopLeftRadius,
borderTopRightRadius: element.css.borderTopRightRadius,
borderBottomRightRadius: element.css.borderBottomRightRadius,
borderBottomLeftRadius: element.css.borderBottomLeftRadius,
boxShadow: element.css.boxShadow,
backgroundColor: element.css.backgroundColor,
opacity: element.css.opacity,
width: "100%",
height: "100%",
overflow: "hidden"
}),
elementContainer.find("img").css({
width: "100%"
}),
elementContainer;
}
}
// Android 微信,图片,设置borderColor
isAndroid() &&
isWeixin() &&
"" + element.type == "4" &&
"0px" !== element.css.borderRadius &&
0 === element.css.borderWidth &&
element.properties.anim && (element.css.borderWidth = 1, element.css.borderColor = "rgba(0,0,0,0)");
var elementCss = $.extend(!0, {}, element.css);
delete elementCss.fontFamily,
elementBox.css(elementCss).css({
width: "100%",
height: "100%",
transform: "none"
}),
elementBox.children(".element-box-contents").css({
width: "100%",
height: "100%"
}),
// 设置宽高
"4" !== ("" + element.type).charAt(0) &&
"n" !== ("" + element.type).charAt(0) &&
"p" !== ("" + element.type).charAt(0) &&
"h" !== ("" + element.type).charAt(0) && "t" !== ("" + element.type).charAt(0) &&
"z" !== ("" + element.type).charAt(0) &&
$(comp).css({
width: element.css.width,
height: element.css.height
}),
// w01 w02 设置lineHeight
("w01" === element.type || "w02" === element.type) &&
$(comp).css({
lineHeight: element.css.height + "px"
}),
// shape 类型
"h" === ("" + element.type).charAt(0) &&
($(comp).find("g").length ?
$(comp).find("g").attr("fill", element.css.color) :
$(comp).children().attr("fill", element.css.color),
elementBox.children(".element-box-contents").css("position", "relative"))
}
return elementContainer
}
}
/**
* 将element按zindex排序
*/
function sortElementsByZindex(elements) {
for (var i = 0; i < elements.length - 1; i++)
for (var j = i + 1; j < elements.length; j++)
if (parseInt(elements[i].css.zIndex, 10) > parseInt(elements[j].css.zIndex, 10)) {
var element = elements[i];
elements[i] = elements[j],
elements[j] = element
}
for (var e = 0; e < elements.length; e++)
elements[e].css.zIndex = e + 1 + "";
return elements
}
function parseElements(pageDef, $edit_wrapper, mode) {
$edit_wrapper = $edit_wrapper.find(".edit_area");
var i, elements = pageDef.elements;
if (elements)
for (elements = sortElementsByZindex(elements),
i = 0; i < elements.length; i++)
if (elements[i].sceneId = pageDef.sceneId,
"" + elements[i].type == "3") {
// type == 3
var component = components[("" + elements[i].type).charAt(0)](elements[i], $edit_wrapper);
// if is edit mode, dispatch edit event
"edit" === mode
&&
editEvents[("" + elements[i].type).charAt(0)] &&
editEvents[("" + elements[i].type).charAt(0)](component, elements[i])
} else {
var comp = wrapComp(elements[i], mode);
if (!comp)
continue;
$edit_wrapper.append(comp);
// invoke interceptors
for (var n = 0; n < interceptors.length; n++)
interceptors[n](comp, elements[i], mode);
afterRenderEvents[("" + elements[i].type).charAt(0)] &&
(
afterRenderEvents[("" + elements[i].type).charAt(0)](comp, elements[i]),
"edit" !== mode && (
parseElementTrigger(comp, elements[i]),
r(comp, elements[i])
)
),
"edit" === mode &&
editEvents[("" + elements[i].type).charAt(0)] &&
editEvents[("" + elements[i].type).charAt(0)](comp, elements[i])
}
}
function getEventHandlers() {
return editEvents
}
function getComponents() {
return components
}
function addInterceptor(interceptor) {
interceptors.push(interceptor)
}
function getInterceptors() {
return interceptors
}
var components = {},
editEvents = {},
afterRenderEvents = {},
interceptors = [],
_width = containerWidth = 320,
_height = containerHeight = 486,
p = 1,
s = 1,
parser = {
getComponents: getComponents,
getEventHandlers: getEventHandlers,
addComponent: createContainerFunction(components),
bindEditEvent: createContainerFunction(editEvents),
bindAfterRenderEvent: createContainerFunction(afterRenderEvents),
addInterceptor: addInterceptor,
getInterceptors: getInterceptors,
wrapComp: wrapComp,
disEvent: !1,
mode: "view",
parse: function (parseInfo) {
var edit_wrapper = $('
'),
mode = this.mode = parseInfo.mode;
// page 定义
this.def = parseInfo.def,
parseInfo.disEvent && (this.disEvent = !0),
"view" === mode && tplCount++;
// 页面容器
var pageContainer = $(parseInfo.appendTo);
return containerWidth = pageContainer.width(),
containerHeight = pageContainer.height(),
p = _width / containerWidth,
s = _height / containerHeight,
parseElements(parseInfo.def, edit_wrapper.appendTo($(parseInfo.appendTo)), mode)
}
};
return parser
});
上面的重点是parseElements,先把elements按zindex排序,然后逐个渲染。
注意,渲染是根据elementType,从components找到对应的组件,然后创建一个实例,因此这里要单独说下组件是如何定义的。
先看下一个组件的配置信息大概是这样,有id,css,type和动画等配置信息:
{
"id": 29,
"css": {
"top": 124.93546211843,
"left": 62.967731059217,
"color": "#676767",
"width": 195,
"height": 195,
"zIndex": "1",
"opacity": 1,
"boxShadow": "0px 0px 0px rgba(0,0,0,0.5)",
"transform": "rotateZ(45deg)",
"lineHeight": 1,
"paddingTop": 0,
"borderColor": "rgba(255,255,255,1)",
"borderStyle": "double",
"borderWidth": 4,
"borderRadius": "0px",
"boxShadowSize": 0,
"paddingBottom": 0,
"backgroundColor": "rgba(252,230,238,0.16)",
"borderRadiusPerc": 0,
"boxShadowDirection": 0,
"textAlign": "left",
"borderBottomRightRadius": "0px",
"borderBottomLeftRadius": "0px",
"borderTopRightRadius": "0px",
"borderTopLeftRadius": "0px"
},
"type": "2",
"pageId": "24642",
"content": "
",
"sceneId": 8831293,
"properties": {
"anim": {
"type": 4,
"delay": 0.6,
"countNum": 1,
"duration": 1,
"direction": 0
},
"width": 195,
"height": 195
}
}
jsonParser里用一个components对象存储组件,通过addComponent添加组件,key就是组件的type:
addComponent: createContainerFunction(components)
function createContainerFunction(container) {
return function (key, value) {
container[key] = value
}
}
添加组件时,type 作为key,value为创建组件的函数:
// 添加组件1
jsonTemplateParser.addComponent("1", function (element, mode) {
var comp = document.createElement("div");
if (comp.id = element.id,
comp.setAttribute("class", "element comp_title"),
// 设置组件content
element.content && (comp.textContent = element.content),
element.css) {
var item, elementCss = element.css;
for (item in elementCss)
comp.style[item] = elementCss[item]
}
if (element.properties.labels)
for (var labels = element.properties.labels, f = 0; f < labels.length; f++)
$('')
.appendTo($(comp))
.html(labels[f].title)
.css(labels[f].color)
.css("width", 100 / labels.length + "%");
return comp
});
这样渲染组件时,根据element的类型就能找到createCompFunction,从而创建组件。
组件动画播放
H5之所以炫酷,很大一部分因为每个组件都有定制好的CSS3动画,我们这里来看看这些动画是如何执行的。
代码还是上一部分的代码,我们注意到组件渲染后,有一段代码;
// invoke interceptors
for (var n = 0; n < interceptors.length; n++)
interceptors[n](comp, elements[i], mode);
执行interceptors,这个interceptors可以通过addInterceptor注册拦截器,在组件渲染完成后会调用定义的拦截器,组件的动画就是这样来调用的。
// 添加拦截器执行动画
jsonTemplateParser.addInterceptor(function (comp, element, mode) {
eqxCommon.animation(comp, element, mode, jsonTemplateParser.def.properties)
});
我们发现,eqxiu通过addInterceptor注册了一个拦截器,该拦截器调用eqxCommon.animation执行组件动画,因此分析eqxCommon.animation就可以了解动画是如何实现的。
还是先看element里的定义:
"properties": {
"anim": {
"type": 4,
"delay": 0.6,
"countNum": 1,
"duration": 1,
"direction": 0
},
我们看到,anim里定义了type,delay,duration等配置信息,可以设想播放动画无非就是解析这个配置,然后执行,其中type应该是对应的各种动画类型,分析代码吧,下面给出破解后的代码:
// 动画播放序号
var animIndex = 0;
// 处理动画属性
if (element.properties && element.properties.anim) {
var anim = [];
element.properties.anim.length ? anim = element.properties.anim : anim.push(element.properties.anim);
var elementBox = $(".element-box", comp);
elementBox.attr("element-anim", "");
// 找出animations
for (var animType, animTypes = [], anims = [], index = 0, animLength = anim.length; animLength > index; index++)
if (null != anim[index].type &&
-1 != anim[index].type) {
animType = eqxCommon.convertType(anim[index]),
animTypes.push(animType),
anims.push(anim[index]);
}
if (properties && properties.scale)
return;
// 动画播放类型
element.properties.anim.trigger ?
comp.click(function () {
// 点击播放
playAnimation(elementBox, animType, element.properties.anim)
}) :
properties && properties.longPage ?
playAnimation(elementBox, animTypes, anims, !0, element.css) // longpage
:
playAnimation(elementBox, animTypes, anims)
}
上面的逻辑是先从element里找到anim,放入数组,然后再playAnimation。这里使用了convertType函数将数字type转换为真实的动画类型:
var convertType = function (a) {
var animType, c, d = a.type;
return "typer" === d && (animType = "typer"),
0 === d && (animType = "fadeIn"),
1 === d && (c = a.direction,
0 === c && (animType = "fadeInLeft"),
1 === c && (animType = "fadeInDown"),
2 === c && (animType = "fadeInRight"),
3 === c && (animType = "fadeInUp")),
6 === d && (animType = "wobble"),
5 === d && (animType = "rubberBand"),
7 === d && (animType = "rotateIn"),
8 === d && (animType = "flip"),
9 === d && (animType = "swing"),
2 === d && (c = a.direction,
0 === c && (animType = "bounceInLeft"),
1 === c && (animType = "bounceInDown"),
2 === c && (animType = "bounceInRight"),
3 === c && (animType = "bounceInUp")),
3 === d && (animType = "bounceIn"),
4 === d && (animType = "zoomIn"),
10 === d && (animType = "fadeOut"),
11 === d && (animType = "flipOutY"),
12 === d && (animType = "rollIn"),
13 === d && (animType = "lightSpeedIn"),
14 === d && (animType = "bounceOut"),
15 === d && (animType = "rollOut"),
16 === d && (animType = "lightSpeedOut"),
17 === d && (c = a.direction,
0 === c && (animType = "fadeOutRight"),
1 === c && (animType = "fadeOutDown"),
2 === c && (animType = "fadeOutLeft"),
3 === c && (animType = "fadeOutUp")),
18 === d && (animType = "zoomOut"),
19 === d && (c = a.direction,
0 === c && (animType = "bounceOutRight"),
1 === c && (animType = "bounceOutDown"),
2 === c && (animType = "bounceOutLeft"),
3 === c && (animType = "bounceOutUp")),
20 === d && (animType = "flipInY"),
21 === d && (animType = "tada"),
22 === d && (animType = "jello"),
23 === d && (animType = "flash"),
26 === d && (animType = "twisterInDown"),
27 === d && (animType = "puffIn"),
28 === d && (animType = "puffOut"),
29 === d && (animType = "slideDown"),
30 === d && (animType = "slideUp"),
24 === d && (animType = "flipInX"),
25 === d && (animType = "flipOutX"),
31 === d && (animType = "twisterInUp"),
32 == d && (animType = "vanishOut"),
33 == d && (animType = "vanishIn"),
animType
};
播放动画函数在playAnimation里:
- 大概是先判断类型是否是typer,typer的话是打字机特效,调用相关代码
- 设置element的的css animation属性,本质上是CSS3的animation动画,可以参见(http://www.w3school.com.cn/css3/css3_animation.asp)
elementBox.css("animation", "");
elementBox.css("animation", animTypes[animIndex] + " " + anims[animIndex].duration + "s ease " + anims[animIndex].delay + "s " +
(anims[animIndex].countNum ? anims[animIndex].countNum : ""));
anims[animIndex].count && animIndex == anims.length - 1 && elementBox.css("animation-iteration-count", "infinite");
elementBox.css("animation-fill-mode", "both");
最后,如果有多个动画,在播放完成后继续播放下一个:
// 动画播放结束,播放下一个动画(一个组件可能有多个动画)
elementBox.one("webkitAnimationEnd mozAnimationEnd MSAnimationEnd oanimationend animationend", function () {
animIndex++;
playAnimation(elementBox, animTypes, anims);
})
页面切换
由于是多页应用,因此涉及到页面切换,并且页面切换时还需要有对应的切换动画,改工作是由一个eqxiu对象来管理和实现的。
老套路,先看这块的配置吧,页面的配置在obj下面,其中pageMode定义了翻页效果:
"obj": {
"id": 8831293,
"name": "房产广告",
"createUser": "1",
"type": 103,
"pageMode": 4,
"image": {},
"property": "{\"triggerLoop\":true,\"slideNumber\":true,\"autoFlipTime\":4,\"shareDes\":\"\",\"eqAdType\":1,\"hideEqAd\":false,\"autoFlip\":true,\"lastPageId\":604964}",
"timeout": "",
"timeout_url": "",
"accessCode": null,
"cover": "syspic/pageimg/yq0KA1UrbkOAV_yiAAFuhyGx9LE397.jpg",
"bgAudio": "{\"url\":\"syspic/mp3/yq0KA1RHT3iAMXYOAAgPq1MjV9M930.mp3\",\"type\":\"3\"}",
"isTpl": 0,
"isPromotion": 0,
"status": 1,
"openLimit": 0,
"startDate": null,
"endDate": null,
"updateTime": 1426045746000,
"createTime": 1426572693000,
"publishTime": 1426572693000,
"applyTemplate": 0,
"applyPromotion": 0,
"sourceId": null,
"code": "U903078B74Q5",
"description": "房产广告",
"sort": 0,
"pageCount": 0,
"dataCount": 0,
"showCount": 44,
"eqcode": "",
"userLoginName": null,
"userName": null
},
pagemode是这样定义的:
pagemodes = [{
id: 0,
name: "上下翻页"
}, {
id: 4,
name: "左右翻页"
}, {
id: 1,
name: "上下惯性翻页"
}, {
id: 3,
name: "左右惯性翻页"
}, {
id: 11,
name: "上下连续翻页"
}, {
id: 5,
name: "左右连续翻页"
}, {
id: 6,
name: "立体翻页"
}, {
id: 7,
name: "卡片翻页"
}, {
id: 8,
name: "放大翻页"
}, {
id: 9,
name: "交换翻页"
}, {
id: 10,
name: "翻书翻页"
}, {
id: 12,
name: "掉落翻页"
}, {
id: 13,
name: "淡入翻页"
}];
在renderpage结束后,调用eqxiu.app:
// 最后一页
if (pageIndex == dataList.length) {
eqxiu.app($(".nr"), response.obj.pageMode, dataList, response);
addEnabledClassToPageCtrl(response);
}
来分析eqxiu.app代码,通过pagemode,我们可以看出翻页打开分为上下翻页、左右翻页两个大类:
if ("8" == pageMode || "9" == pageMode) {
transformTime = 0.7;
timeoutDelay = 800;
}
// 上下翻页 上下惯性翻页 立体翻页 卡片翻页 放大翻页 上下连续翻页 上下连续翻页
if (0 == pageMode || (1 == pageMode || (2 == pageMode || (6 == pageMode || (7 == pageMode || (8 == pageMode || (11 == pageMode || 12 == pageMode))))))) {
/** @type {boolean} */
upDownMode = true;
} else {
// 左右惯性翻页 左右翻页 左右连续翻页 翻书翻页
if (3 == pageMode || (4 == pageMode || (5 == pageMode || 10 == pageMode))) {
/** @type {boolean} */
leftRightMode = true;
}
}
然后配置里有一个autoFlip,代表是否自动翻页,通过setInterval设置定时翻页任务:
// 自动翻页
if (response.obj.property.autoFlip) {
// 自动翻页时间
autoFlipTimeMS = 1000 * response.obj.property.autoFlipTime;
setAndStartAutoFlip(autoFlipTimeMS);
}
/**
* 设置翻页时间间隔并启动翻页
* @param {number} textStatus
* @return {undefined}
*/
function setAndStartAutoFlip(autoFlipTime) {
autoFlipTime = autoFlipTime;
pauseAutoFlip();
startAutoFlip();
}
/**
* 启动自动翻页
* @return {undefined}
*/
function startAutoFlip() {
// 通过setInterval
autoFlipIntervalId = setInterval(function () {
if (!(10 === self._scrollMode)) {
if (!isTouching) {
nextPage();
}
}
}, autoFlipTimeMS);
}
默认情况下H5是支持touch滑动翻页的,这种滑动操作一般是监听相关事件,开始滑动、滑动中和滑动结束,为了同时支持移动端和PC端,还需要加上鼠标点击事件:
var isTouch = false;
self._$app.on("mousedown touchstart", function (e) {
if (!self._isforbidHandFlip) {
onTouchStart(e);
isTouch = true;
}
}).on("mousemove touchmove", function (e) {
if (!self._isforbidHandFlip) {
if (isTouch) {
onTouchMove(e);
}
}
}).on("mouseup touchend mouseleave", function (events) {
if (!self._isforbidHandFlip) {
onTouchEnd(events);
/** @type {boolean} */
isTouch = false;
}
});
翻页的核心无非就是判断位移是否超过特定的值,比如左右翻页X位移是否大于Y位移并且X的偏移量大于20。因此onTouchStart开始时,记录初始位置,onTouchMove时计算offset变化,按照pageMode执行对应的动画,onTouchEnd时判断位移是否足够,足够就切换页面,否则复位。
/**
* 开始滑动
* @param {Object} e
* @return {undefined}
*/
onTouchStart = function (e) {
/** @type {boolean} */
fa = false;
if (isMobile) {
if (e) {
e = event;
}
}
if (!self._isDisableFlipPage) {
self.$currentPage = self._$pages.filter(".z-current").get(0);
if (!C) {
/** @type {null} */
self.$activePage = null;
}
if (self.$currentPage) {
if (completeEffect($(self.$currentPage))) {
isTouching = true;
isCursorAtEnd = false;
ignoreEvent = true;
offsetX = 0;
offsetY = 0;
if (e && "mousedown" == e.type) {
currentPageX = e.pageX;
currentPageY = e.pageY;
} else if (e && "touchstart" == e.type) {
currentPageX = e.touches ? e.touches[0].pageX : e.originalEvent.touches[0].pageX;
currentPageY = e.touches ? e.touches[0].pageY : e.originalEvent.touches[0].pageY;
}
self.$currentPage.classList.add("z-move");
setAttribute(self.$currentPage.style, "Transition", "none");
if ("12" == self._scrollMode) {
/** @type {number} */
self.$currentPage.style.zIndex = 3;
}
}
}
}
};
/**
* 滑动处理
* @param {Object} e
* @return {undefined}
*/
onTouchMove = function (e) {
if (isMobile) {
if (e) {
e = event;
}
}
if (isTouching) {
if (self._$pages.length > 1) {
if (e && "mousemove" == e.type) {
offsetX = e.pageX - currentPageX;
offsetY = e.pageY - currentPageY;
} else {
if (e) {
if ("touchmove" == e.type) {
offsetX = (e.touches ? e.touches[0].pageX : e.originalEvent.touches[0].pageX) - currentPageX;
offsetY = (e.touches ? e.touches[0].pageY : e.originalEvent.touches[0].pageY) - currentPageY;
}
}
}
if (!fa) {
if (Math.abs(offsetX) > 20 || Math.abs(offsetY) > 20) {
/** @type {boolean} */
fa = true;
}
}
switch (self._scrollMode + "") {
case "0":
case "1":
case "2":
case "15":
//上下翻页
upDownFlip();
break;
case "3":
case "4":
// 左右翻页
leftRightFlip();
break;
case "5":
// 左右连续翻页
leftRightLoopFlip();
break;
case "7":
cardFlip();
break;
case "8":
scaleUpFlip();
break;
case "9":
switchFlip();
break;
case "11":
//上下连续翻页
upDownContinuousFlip();
break;
case "12":
//掉落翻页
dropFlip();
break;
case "13":
case "14":
//淡入翻页
fadeFlip();
break;
default:
break;
}
}
}
};
/**
* 滑动结束
* @param {?} e
* @return {undefined}
*/
onTouchEnd = function (e) {
if (isTouching && completeEffect($(self.$currentPage))) {
isTouching = false;
if (self.$activePage) {
self._isDisableFlipPage = true;
var ease;
ease = "6" == self._scrollMode || "7" == self._scrollMode ? "cubic-bezier(0,0,0.99,1)" : "12" == self._scrollMode ? "cubic-bezier(.17,.67,.87,.13)" : "linear";
self.$currentPage.style.webkitTransition = "-webkit-transform " + transformTime + "s " + ease;
self.$activePage.style.webkitTransition = "-webkit-transform " + transformTime + "s " + ease;
self.$currentPage.style.mozTransition = "-moz-transform " + transformTime + "s " + ease;
self.$activePage.style.mozTransition = "-moz-transform " + transformTime + "s " + ease;
self.$currentPage.style.transition = "transform " + transformTime + "s " + ease;
self.$activePage.style.transition = "transform " + transformTime + "s " + ease;
// 完成翻页
if ("0" == self._scrollMode || ("2" == self._scrollMode || ("1" == self._scrollMode || "15" == self._scrollMode))) {
endUpDownFlip();
} else if ("4" == self._scrollMode || "3" == self._scrollMode) {
// 左右翻页
endLeftRightFlip();
} else if ("5" == self._scrollMode) {
//左右连续翻页
endLeftRightContinueFlip();
} else if ("6" == self._scrollMode) {
//立体翻页
endCubeFlip();
} else if ("7" == self._scrollMode) {
//卡片翻页
endCardFlip();
} else if ("8" == self._scrollMode) {
//放大翻页
endScaleUpFlip();
} else if ("9" == self._scrollMode) {
//交换翻页
endSwitchFlip();
} else if ("11" == self._scrollMode) {
//上下连续翻页
endUpDownContinueFlip();
} else if ("12" == self._scrollMode) {
//掉落翻页
endDropFlip();
} else if ("13" == self._scrollMode || "14" == self._scrollMode) {
//淡入翻页
endFadeFlip();
}
/** @type {number} */
var pageIndex = $(self.$activePage).find(".m-img").attr("id").replace("page", "") - 1;
if (self._pageData[pageIndex].properties) {
if (self._pageData[pageIndex].properties.longPage) {
$(document).trigger("clearTouchPos");
}
}
$(self.$activePage).find("li.comp-resize").each(function (dataAndEvents) {
/** @type {number} */
var i = 0;
for (; i < self._pageData[pageIndex].elements.length; i++) {
if (self._pageData[pageIndex].elements[i].id == parseInt($(this).attr("id").substring(7), 10)) {
eqxCommon.animation($(this), self._pageData[pageIndex].elements[i], "view", self._pageData[pageIndex].properties);
var r20 = getComp(self._pageData[pageIndex].elements[i].id);
eqxCommon.bindTrigger(r20, self._pageData[pageIndex].elements[i]);
}
}
});
/** @type {number} */
var i = 0;
for (; i < self._pageData.length; i++) {
if (self._pageData[i].effObj) {
/** @type {boolean} */
self._pageData[i].effObj.pause = true;
}
}
if (self._pageData[pageIndex].effObj) {
self._pageData[pageIndex].effObj.startPlay();
}
eqShow.setPageHis(self._pageData[pageIndex].id);
} else {
self.$currentPage.classList.remove("z-move");
}
}
C = false;
};
然后再来看自动翻页nextPage
/**
* 启动自动翻页
* @return {undefined}
*/
function startAutoFlip() {
// 通过setInterval
autoFlipIntervalId = setInterval(function () {
if (!(10 === self._scrollMode)) {
if (!isTouching) {
nextPage();
}
}
}, autoFlipTimeMS);
}
自动翻页比较简单,模拟滑动操作,当位移足够时就可以自动翻页了:
/**
* 上一页
* @param {number} direction
* @return {undefined}
*/
function prePage(direction) {
if (!(leftRightMode && 2 == direction || upDownMode && 1 == direction)) {
if ("10" != self._scrollMode) {
var offset = 0;
// 开启滑动
onTouchStart();
// 定时器,增加offset,模拟滑动
var poll = setInterval(function () {
offset += 2;
if ("0" == self._scrollMode || ("1" == self._scrollMode || ("2" == self._scrollMode || ("6" == self._scrollMode || ("7" == self._scrollMode || ("8" == self._scrollMode || ("11" == self._scrollMode || ("12" == self._scrollMode || ("13" == self._scrollMode || ("14" == self._scrollMode || "15" == self._scrollMode)))))))))) {
// 纵向翻页,增加y
offsetY = offset;
} else {
if ("3" == self._scrollMode || ("4" == self._scrollMode || ("5" == self._scrollMode || "9" == self._scrollMode))) {
// 横向翻页,增加x
offsetX = offset;
}
}
// 触发move操作,模拟滑动
onTouchMove();
if (offset >= 21) {
// 位移超过20,
clearInterval(poll);
// 停止滑动,完成翻页
onTouchEnd();
}
}, 1);
} else {
// 翻书
$(document).trigger("bookFlipPre");
}
}
}
/**
* 下一页,逻辑和prePage类似
* @param {number} direction
* @return {undefined}
*/
function nextPage(direction) {
if (!(leftRightMode && 2 == direction || upDownMode && 1 == direction)) {
if ("10" != self._scrollMode) {
u = false;
var offset = 0;
if ("block" == $("body .boards-panel").css("display")) {
$("body .boards-panel").hide();
$("body .z-current").show();
}
onTouchStart();
var poll = setInterval(function () {
offset -= 2;
if ("0" == self._scrollMode || ("1" == self._scrollMode || ("2" == self._scrollMode || ("6" == self._scrollMode || ("7" == self._scrollMode || ("8" == self._scrollMode || ("11" == self._scrollMode || ("12" == self._scrollMode || ("13" == self._scrollMode || ("14" == self._scrollMode || "15" == self._scrollMode)))))))))) {
offsetY = offset;
} else {
if ("3" == self._scrollMode || ("4" == self._scrollMode || ("5" == self._scrollMode || "9" == self._scrollMode))) {
offsetX = offset;
}
}
onTouchMove();
if (-21 >= offset) {
clearInterval(poll);
onTouchEnd();
if (!triggerLoop) {
if (!self.$activePage) {
clearInterval(autoFlipIntervalId);
}
}
}
}, 1);
} else {
$(document).trigger("bookFlipNext");
}
}
}
总结
上面是花了大概一天多的时间阅读代码的成果,总结经验就是阅读代码先分析大的流程,再层层递进分析一些细节,就能一步一步接近真相。
另外,阅读压缩过的代码,可以借助VS Code,善用F2重命名,修改的越多,越接近本来的代码:)