wxss 设计思路
WXSS 具有 CSS的大部分特性。同时为了更适合开发微信小程序,WXSS 对 CSS 进行了扩充以及修改。通俗的可以理解成基于CSS改了点东西,又加了点东西。
与 CSS 相比,WXSS 扩展的特性有:
- 尺寸单位
rpx(responsive pixel) : 可以根据屏幕宽度进行自适应。规定屏幕宽为750rpx。如在 iPhone6 上,屏幕宽度为375px,共有750个物理像素,则750rpx = 375px = 750物理像素,1rpx = 0.5px = 1物理像素。
- 样式导入
使用 @import语句可以导入外联样式表, @import后跟需要导入的外联样式表的相对路径,用;表示语句结束。
编译
/**index.wxss**/ .test{ height: calc(100rpx-2px); width: 200rpx; }
如上我们定义的index.wxss,会被编译成js,注入webview
我们把编译后的js分成三部分,展开分析。
第一部分用于获取一套基本设备信息,包含设备高度、设备宽度、物理像素与CSS像素比例、设备方向。
/*********//*第一部分*//*设备信息*//*********/varBASE_DEVICE_WIDTH=750;// 基础设备宽度750varisIOS=navigator.userAgent.match("iPhone"); // 是否ipheone 机型vardeviceWidth=window.screen.width||375; // 设备宽度 默认375vardeviceDPR=window.devicePixelRatio||2; // 获取物理像素与css像素比例 默认2varcheckDeviceWidth=window.__checkDeviceWidth__||function() { varnewDeviceWidth=window.screen.width||375// 初始化设备宽度varnewDeviceDPR=window.devicePixelRatio||2// 初始化设备 像素比例varnewDeviceHeight=window.screen.height||375// 初始化设备高度// 判断屏幕方向 landscape 为横向,如果是横向 高度值给宽度if (window.screen.orientation&&/^landscape/.test(window.screen.orientation.type||'')) newDeviceWidth=newDeviceHeight// 更新设备信息if (newDeviceWidth!==deviceWidth||newDeviceDPR!==deviceDPR) { deviceWidth=newDeviceWidthdeviceDPR=newDeviceDPR } } // 检查设备信息checkDeviceWidth()
第二部分:转化rpx
核心就是:下面两句,做了一个精度收拢
number = number /BASE_DEVICE_WIDTH *** (newDeviceWidth || deviceWidth);**
number =Math.floor(number + eps);
/*********//*第二部分*//*转化rpx*//*********/vareps=1e-4;//0.0001vartransformRPX=window.__transformRpx__||function(number, newDeviceWidth) { // 如果0 返回 0 0rpx = 0pxif ( number===0 ) return0; // px = rpx值 / 基础设备宽度750 * 设备宽度number=number/BASE_DEVICE_WIDTH* ( newDeviceWidth||deviceWidth ); // 返回小于等于 number + 0.0001的大整数,用户收拢精度number=Math.floor(number+eps); if (number===0) {// 如果number == 0,说明输入为1rpxif (deviceDPR===1||!isIOS) {// 非IOS 或者 像素比为1,返回1return1; } else { return0.5; } } returnnumber; }
第三部分主要是 setCssToHead 顾名思义
/*********//*第三部分*//*setCssToHead*//*********/window.__rpxRecalculatingFuncs__=window.__rpxRecalculatingFuncs__|| []; var__COMMON_STYLESHEETS__=__COMMON_STYLESHEETS__|| {} %svarsetCssToHead=function(file, _xcInvalid, info) { varCa= {}; varcss_id; varinfo=info|| {}; var_C=__COMMON_STYLESHEETS__functionmakeup(file, opt) { var_n=typeof(file) ==="string"; if (_n&&Ca.hasOwnProperty(file)) return""; if (_n) Ca[file] =1; varex=_n?_C[file] : file; varres=""; for (vari=ex.length-1; i>=0; i--) { varcontent=ex[i]; if (typeof(content) ==="object") { varop=content[0]; if (op==0) res=transformRPX(content[1], opt.deviceWidth) +"px"+res; elseif (op==1) res=opt.suffix+res; elseif (op==2) res=makeup(content[1], opt) +res; } elseres=content+res } returnres; } varstyleSheetManager=window.__styleSheetManager2__varrewritor=function(suffix, opt, style) { opt=opt|| {}; suffix=suffix||""; opt.suffix=suffix; if (opt.allowIllegalSelector!=undefined&&_xcInvalid!=undefined) { if (opt.allowIllegalSelector) console.warn("For developer:"+_xcInvalid); else { console.error(_xcInvalid); } } Ca= {}; css=makeup(file, opt); if (styleSheetManager) { varkey= (info.path||Math.random()) +':'+suffixif (!style) { styleSheetManager.addItem(key, info.path); window.__rpxRecalculatingFuncs__.push(function(size) { opt.deviceWidth=size.width; rewritor(suffix, opt, true); }); } styleSheetManager.setCss(key, css); return; } if (!style) { varhead=document.head||document.getElementsByTagName('head')[0]; style=document.createElement('style'); style.type='text/css'; style.setAttribute("wxss:path", info.path); head.appendChild(style); window.__rpxRecalculatingFuncs__.push(function(size) { opt.deviceWidth=size.width; rewritor(suffix, opt, style); }); } if (style.styleSheet) { style.styleSheet.cssText=css; } else { if (style.childNodes.length==0) style.appendChild(document.createTextNode(css)); elsestyle.childNodes[0].nodeValue=css; } } returnrewritor; } setCssToHead([".", [1], "test{ height: calc(", [0, 100], "-2px); width: ", [0, 200], "; }\n", ])(typeof__wxAppSuffixCode__=="undefined"?undefined: __wxAppSuffixCode__);
setCssToHead 传的参数 是我们定义的wxcss,变成了结构化数据,方便遍历处理
index.wxss中写rpx单位的属性都变成了区间的样子[0, 100]、[0, 200]。其他单位并没有转换。这样的话就可以方便的识别哪里写了rpx单位
[".", [1], "test{ height: calc(", [0, 100], "-2px); width: ", [0, 200], "; }\n", ]
注入
在渲染层的一个的标签中,有很长的一串字符串,并且用eval方法执行。如果你仔细看的话,还是可以勉强分辨出,这个字符串正是我们前面编译出来的js转换成的。
这样就可以得知,编译后的代码是通过eval方法注入执行的。这样的话完成了WXSS的一整套流程。
同时我们也可以看到,是在修改pageFrame 的路径之后,初始化小程序样式配置文件之后,才开始注入样式文件
Virtual Dom 渲染流程
微信开发者工具和微信客户端都无法直接运行小程序的源码,因此我们需要对小程序的源码进行编译。
代码编译过程包括本地预处理、本地编译和服务器编译。
为了快速预览,微信开发者工具模拟器运行的代码只经过本地预处理、本地编译,没有服务器编译过程,而微信客户端运行的代码是额外经过服务器编译的。
编译
<!--index.wxml--><viewclass="container">Weixin<textstyle="position:relative;">文本</text></view><buttonbindtap="test">按钮</button>
如上面这段简单的wxml文件,经过编译之后,被编译成了 1500 多行
全部代码都被包裹在$gwx函数中,编译后的WXML文件,以js的形式插入到了渲染层的
但是在这个script标签中插入了$gwx函数之后并没有立即执行这个函数。
在渲染层的一个的<script>
标签中,我们可以看到这段代码
var decodeName = decodeURI("./pages/index/index.wxml") var generateFunc = $gwx(decodeName)
我们在控制抬手动执行$gwx()的返回值 generateFunc()函数
返回的树形结构,就是该页面wxml对应的js对象形式表示的dom树
这是一个类似Virtual Dom的对象,交给了 WAWebview.js 来渲染成真实DOM
事件系统设计
核心在于,wxml和js文件在两个线程渲染,解析。事件如何绑定?
我们最开始在wxml文件中定义的事件绑定,其实转化成虚拟dom树结构之后,其实只是一个键值对,表明了某个dom上有绑定某个事件,并没有完成事件绑定。
WAWebview.js 处理虚拟dom树时,会去循环遍历attr属性,判断attr中的属性名是否为事件属性
if (n = e.match(/^(capture-)?(mut-)?(bind|catch):?(.+)$/))
如果是,通过addListener方法进行了事件绑定。
可以理解成,通过addListner方法监听tap事件,就相当于 window.addEventListener对mouseup方法的监听。
回调函数中对函数的event信息进行组装,并触发sendData方法。
sendData方法就是向逻辑线程发送event数据的方法。
下图是我们在逻辑层接收到的数据和准备发送的数据结构
可以看到数据结构是一样的,
目前在触发sendData方法之前这些逻辑的解析包括event参数的组装都是在渲染层的底层基础库WAWebview.js中完成的,也就是说还在渲染线程中。
事件
微信小程序中主要事件绑定:bind catch
bind /catch后可以紧跟一个冒号,其含义不变,如bind:tap catch:tap
。
catch 会阻止事件向上冒泡。
mut-bind 来绑定事件。一个 mut-bind 触发后,如果事件冒泡到其他节点上,其他节点上的 mut-bind 绑定函数不会被触发,但 bind 绑定函数和 catch 绑定函数依旧会被触发。
需要在捕获阶段监听事件时,可以采用capture-bind、capture-catch关键字,后者将中断捕获阶段和取消冒泡阶段。