impress.js 源码分析
Write By CS逍遥剑仙
我的主页: csxiaoyao.com
GitHub: github.com/csxiaoyaojianxian
Email: sunjianfeng@csxiaoyao.com
QQ: 1724338257
之前做展示用幻灯片,我一直热衷于使用PPT,刚开始学习PPT时总是强行使用各种页面特效,越做越复杂。现在看来,学技术大概都要经历一个从简到繁再到简
的过程吧。后来,无意间接触到prezi,被它强大的展示逻辑所折服,但用了段时间,发现使用prezi破解版有诸多不便,最关键的是,除了很炫的转场特效,单页的设计感不及PPT,总感觉不尽如人意。
总结下,PPT的单页设计感强,普及率高;prezi的展示思路清晰,变现力强,跨平台,但制作流程稍显不便。
最近,学习H5+CSS3时我突发奇想,为何不用H5来做幻灯片展示呢?刚开始两天,纯靠自己手写页面和转场,不是一般累。我试图去抽取常用的公共方法,尝试做成框架组件的形式,发现工作量巨大,而且以自己目前水平,写出来的代码通用性很差。在Google上搜索时,我发现了impress.js的存在,与我的设想不谋而合,于是乎……不再自己造轮子,又花了两天时间熟悉使用impress.js来设计幻灯片,效果完全超越了我的预期。
impress.js简单来说仅仅是实现了幻灯片的转场特效的框架,虽说将单页限制在框架之内,但所有单页还是需要自己用代码设计。虽然花费的时间远远超过了PPT和prezi,但是能够完全使用代码来定制幻灯片,能够使用几乎每台电脑必备的浏览器放幻灯片,何尝不是一种幸福!
花了一天探索了下impress.js源码,其实并不复杂,个人感觉收获颇丰,以下阐述我的收获。
1. impress.js华丽效果实现方法概述
通过审查元素发现,页面间转场的实现全部是依赖于CSS3,准确说是translate3d属性完成的。impress的水平移动是改变了translateX坐标,垂直移动是改变translateY坐标,而变小变大的绚丽效果是改变translateZ的坐标实现,而这些转化样式的事件监听是通过js来实现的。
2. impress.js具体的技术实现
2.1 动效参数的获取
(line307):参数获取
官方提供了一个demo:index.html,首先从data-* 属性入手。这是html5的新增api,允许用户自定义数据,通过dataset方法取出数据
var data = el.dataset, //el是通过getElememtById()获得的元素 step = { //定义了一个step对象,里面有4个css样式属性 translate: { x: toNumber(data.x), y: toNumber(data.y), z: toNumber(data.z) }, //toNumber函数将参数转换成数字,如果无法转换返回默认值 rotate: { x: toNumber(data.rotateX), y: toNumber(data.rotateY), z: toNumber(data.rotateZ || data.rotate) }, scale: toNumber(data.scale, 1), el: el };
在浏览器的console调试代码,元素的dataset 得到的是一个数组,可以依次取出x,y,z值,这就是动效实现的核心。
2.2 通用函数定义
(line1-line174):通用函数定义
pfx() ----- 通过检测浏览器给css3属性加上当前浏览器可用的前缀 arrayify() ----- 将Array-Like对象转换成Array对象 css() ----- 将指定属性应用到指定元素上 toNumber() ----- 将参数转换成数字,如果无法转换返回默认值 byId() ----- 通过id获取元素 $() ----- 返回满足选择器的第一个元素 $$() ----- 返回满足选择器的所有元素 triggerEvent() ----- 在指定元素上触发指定事件 translate() ----- 将translate对象转换成css使用的字符串 rotate() ----- 将rotate对象转换成css使用的字符串 scale() ----- 将scale对象转换成css使用的字符串 perspective() ----- 将perspective对象转换成css使用的字符串 getElementFromHash() ----- 根据hash来获取元素(hash是URL中形如#step1的东西) computeWindowScale() ----- 根据当前窗口尺寸计算scale,用于放大和缩小
2.3 主函数与5大api
(223line~):主函数与5大api
API: goto(), init(), next(), prev(), initStep()
主函数: var impress = window.impress = function ( rootId ) {……}
在index.html中调用了init()函数
<script src="js/impress.js"></script> <script> impress().init(); </script>
源码中的init()函数,分析写在注释中
var init = function() { if (initialized) { return; } //初始值initialized=false; //使用viewport支持移动设备 var meta = $("meta[name='viewport']") || document.createElement("meta"); // 定义的$风格函数,类似于jQuery // var $ = function ( selector, context ) { // context = context || document; // return context.querySelector(selector); // }; meta.content = "width=device-width, minimum-scale=1, maximum-scale=1, user-scalable=no"; if (meta.parentNode !== document.head) { //判断meta的parentNode节点是不是<head> meta.name = 'viewport'; //如果不是head标签,就js添加一个meta标签 document.head.appendChild(meta); } //初始化配置root (243line & 269line) var rootData = root.dataset; //获取到初始化的root数据,即id=“impress”的div标签里的内容 config = { width: toNumber(rootData.width, defaults.width), height: toNumber(rootData.height, defaults.height), maxScale: toNumber(rootData.maxScale, defaults.maxScale), minScale: toNumber(rootData.minScale, defaults.minScale), perspective: toNumber(rootData.perspective, defaults.perspective), transitionDuration: toNumber(rootData.transitionDuration, defaults.transitionDuration) }; windowScale = computeWindowScale(config); // wrap steps with "canvas" element arrayify(root.childNodes).forEach(function(el) { canvas.appendChild(el); }); root.appendChild(canvas); // arraify函数把类数组对象转化为真正数组(69line) document.documentElement.style.height = "100%"; css(body, { height: "100%", overflow: "hidden" }); var rootStyles = { position: "absolute", transformOrigin: "top left", transition: "all 0s ease-in-out", transformStyle: "preserve-3d" }; css(root, rootStyles); css(root, { top: "50%", left: "50%", transform: perspective(config.perspective / windowScale) + scale(windowScale) }); css(canvas, rootStyles); body.classList.remove("impress-disabled"); body.classList.add("impress-enabled"); // get and init steps steps = $$(".step", root); steps.forEach(initStep); // 找到每一个class为”step“的元素,返回root(id=“impress”)的数组 // forEach遍历每一个数组,对每个div用initstep()函数初始化 //即我们一开始分析的那个函数。主要是把data-*自定义的数据获得,附上transtion样式。 // set a default initial state of the canvas currentState = { translate: { x: 0, y: 0, z: 0 }, rotate: { x: 0, y: 0, z: 0 }, scale: 1 }; //当前的状态。位移为0,旋转为0,缩放为1. initialized = true; //初始化为true,即完成初始化 //triggerEvent()自定义事件监听函数 //其中,第一个参数为要处理的事件名 //第二个参数为表明事件是否冒泡 //第三个参数为表明是否可以取消事件的默认行为 //第四个参数为细节参数 triggerEvent(root, "impress:init", { api: roots["impress-root-" + rootId] }); };
总结:初始化过程分为两个阶段,第一个阶段是运行init()函数,第二个阶段是运行绑定到impress:init上的函数。在init()函数的结尾触发impress:init事件,这样绑定上去的函数就会全部触发。
2.4 事件对象绑定与监听
root.addEventListener("impress:init", function() { // STEP CLASSES steps.forEach(function(step) { step.classList.add("future"); }); root.addEventListener("impress:stepenter", function(event) { event.target.classList.remove("past"); //利用html5 classList属性对class类增删改查 event.target.classList.remove("future"); event.target.classList.add("present"); }, false); root.addEventListener("impress:stepleave", function(event) { event.target.classList.remove("present"); event.target.classList.add("past"); }, false); }, false);
init是初始化事件,stepenter是进入下一步事件,stepleave是离开上一步事件
var onStepEnter = function(step) { if (lastEntered !== step) { triggerEvent(step, "impress:stepenter"); lastEntered = step; } }; var onStepLeave = function(step) { if (lastEntered === step) { triggerEvent(step, "impress:stepleave"); lastEntered = null; } };
一个step就是一个页面,按一次键盘上的left键或者right键就会切换一次step,同时绑定键盘事件
document.addEventListener("keyup", function ( event ) {...} document.addEventListener("keydown", function ( event ) {...} document.addEventListener("click", function ( event ) {...} window.addEventListener("resize", throttle(function () {...} document.addEventListener("touchstart", function ( event ) {...}
函数简单总结
impress 主函数,构造impress对象,为全局对象 onStepEnter 用于触发impress:stepenter事件 onStepLeave 用于触发impress:stepleave事件 initStep 初始化给定 step init 主初始化函数 getStep 获取指定 step goto 切换到指定step prev 切换到上一个 step next 切换到下一个step
3 总结
总体看来,impress.js源码简洁明了,并不复杂,作者的本意也是构建一个基础的框架,让使用者自由发挥,正合吾意!很多技术单独实现都很简单,综合运用起来,如何保证命名空间不污染,代码的重用复用,js和css代码的兼容性等细节都是值得学习的地方。