平常小程序写的多一些,简单总结一下原理。但因为小程序也没开源,只能参考相关文档以及开发者工具慢慢理解了。
理解小程序原理的突破口就是开发者工具了,开发者工具是基于 NW.js
,一个基于 Chromium
和 node.js
的应用运行时。同时暴漏了 debug
的入口。
点开后就是一个新的 devTools
的窗口,这里我们可以找到预览界面的 dom
。
小程序界面是一个独立的 webview
,也就是常说的视图层,可以在命令行执行 document.getElementsByTagName('webview')
,可以看到很多 webview
。
我这边第 0
个就是 pages/index/index
的视图层,再通过 document.getElementsByTagName('webview')[0].showDevTools(true)
命令单独打开这个 webview
。
熟悉的感觉回来了,其实就是普通的 html/css
,小程序的原理的突破口也就在这里了。
这篇文章简单看一下页面的样式是怎么来的,也就是 wxss
做了什么事情。
源码中 data1
的样式:
开发中工具中对应的样式:
rpx
的单位转成了 px
,同时保留网页不认识的属性名,大概就是为了方便的看到当前类本身的属性和一些文件信息。
这个样式是定义在 <style>
中,
让我们展开 <head>
找一下:
data1
确实在 <style>
中,继续搜索,可以看到这里 <style>
中的内容是通过在 <script>
执行 eval
插入进来的。
把这一段代码丢给 chatGPT
整理一下:
来一段一段看一下:
设备信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| var BASE_DEVICE_WIDTH = 750; var isIOS = navigator.userAgent.match("iPhone"); var deviceWidth = window.screen.width || 375; var deviceDPR = window.devicePixelRatio || 2; var checkDeviceWidth = window.__checkDeviceWidth__ || function() { var newDeviceWidth = window.screen.width || 375; var newDeviceDPR = window.devicePixelRatio || 2; var newDeviceHeight = window.screen.height || 375; if (window.screen.orientation && /^landscape/.test(window.screen.orientation.type || '')) { newDeviceWidth = newDeviceHeight; } if (newDeviceWidth !== deviceWidth || newDeviceDPR !== deviceDPR) { deviceWidth = newDeviceWidth; deviceDPR = newDeviceDPR; } }; checkDeviceWidth();
|
主要更新了几个变量,deviceWidth
、deviceDPR
,像素相关的知识很久很久以前写过一篇文章 分辨率是什么?。
这里再补充一下,这里的 deviceWidth
是设备独立像素(逻辑像素),是操作系统为了方便开发者而提供的一种抽象。看一下开发者工具预设的设备:
如上图,以 iphone6
为例,宽度是 375
,事实上 iphone6
宽度的物理像素是 750
。
所以就有了 Dpr
的含义, iphone6
的 dpr
是 2
, 1px
相当于渲染在两个物理像素上。
rpx 转换
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| var eps = 1e-4; var transformRPX = window.__transformRpx__ || function(number, newDeviceWidth) { if (number === 0) return 0; number = number / BASE_DEVICE_WIDTH * (newDeviceWidth || deviceWidth); number = Math.floor(number + eps); if (number === 0) { if (deviceDPR === 1 || !isIOS) { return 1; } else { return 0.5; } } return number; };
|
核心就是这一行 number = number / BASE_DEVICE_WIDTH * (newDeviceWidth || deviceWidth);
,其中 BASE_DEVICE_WIDTH
是 750
,也就是微信把屏幕宽度先强行规定为了 750
,先用用户设定的 rpx
值除以 750
算出一个比例,最后乘上设备的逻辑像素。
如果设备是 iphone6
,那么这里设备的逻辑像素就是 350
,所以如果是 2rpx
,2/750*375=1
最后算出来就是 1px
,实际上在 iphone6
渲染的是两个物理像素,也就是常常遇到的 1px
过粗的问题,解决方案可以参考这篇 前端移动端1px问题及解决方案。
接下来一行 number = Math.floor(number + eps);
是为了解决浮点数精度问题,比如除下来等于 3.9999999998
,实际上应该等于 4
,只是浮点数的问题导致没有算出来 4
,加个 eps
,然后向下 floor
去整,就可以正常得到 4
了,关于浮点数可以看 一直迷糊的浮点数。
接着往下看:
1 2 3 4 5 6 7
| if (number === 0) { if (deviceDPR === 1 || !isIOS) { return 1; } else { return 0.5; } }
|
在 transformRPX
函数整个代码里第一行 if (number === 0) return 0;
,number
等于 0
已经提前结束了,所以这里 number
得到 0
就是因为除的时候得到了一个小数。
如果 deviceDPR === 1
,说明逻辑像素和物理像素是一比一的,不可能展示半个像素,直接 return 1
。
如果不是 iOS
也直接返回 1
,这是因为安卓手机厂商众多,即使 deviceDPR
大于 1
,也不一定支持像素传小数,传小数可能导致变 0
或者变 1
,为了最大可能的保证兼容性,就直接返回 1
。
对于苹果手机,据说是从 iOS 8
开始支持 0.5px
的,但没找到当时的官方说明:
因此上边的代码中,对于 deviceDPR
大于 1
,并且是苹果手机的就直接返回 0.5
了。
生成 css
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| setCssToHead( [ ".", [1], "container { display: flex; align-items: center; justify-content: center; ;wxcs_originclass: .container;;wxcs_fileinfo: ./pages/index/index.wxss 1 1; }\n", ".", [1], "data1{ color: red; font-size: ", [0, 50], "; ;wxcs_style_font-size : 50rpx; ;wxcs_originclass: .data1;;wxcs_fileinfo: ./pages/index/index.wxss 6 1; }\n", ".", [1], "data2{ color: blue; font-size: ", [0, 100], "; ;wxcs_style_font-size : 100rpx; ;wxcs_originclass: .data2;;wxcs_fileinfo: ./pages/index/index.wxss 11 1; }\n", ".", [1], "data3{ color: blue; font-size: ", [0, 100], "; ;wxcs_style_font-size : 100rpx; ;wxcs_originclass: .data3;;wxcs_fileinfo: ./pages/index/index.wxss 17 1; }\n", ], undefined, { path: "./pages/index/index.wxss" } )();
|
通过调用 setCssToHead
把上边传的数组拼接为最终的 css
。
核心逻辑就是循环上边的数组,如果数组元素是字符串直接相加就好,如果是数组 [1]
、[0, 50]
这样,需要特殊处理下:
核心逻辑是 makeup
函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| function makeup(file, opt) { var _n = typeof(file) === 'string'; if (_n && Ca.hasOwnProperty(file)) return ''; if (_n) Ca[file] = 1; var ex = _n ? _C[file] : file; var res = ''; for (var i = ex.length - 1; i >= 0; i--) { var content = ex[i]; if (typeof(content) === 'object') { var op = content[0]; if (op === 0) res = transformRPX(content[1], opt.deviceWidth) + 'px' + res; else if (op === 1) res = opt.suffix + res; else if (op === 2) res = makeup(content[1], opt) + res; } else res = content + res; } return res; }
|
如果遇到 content
是 [1]
,也就是 op
等于 1
,添加一个前缀 res = opt.suffix + res;
。
如果遇到 content
是 [0, 50]
,也就是 op
等于 0
,这里的 50
其实就是用户写的 50rpx
的 50
,因此需要调用 transformRPX
将 50
转为 px
再相加 res = transformRPX(content[1], opt.deviceWidth) + 'px' + res;
。
通过 makeup
函数,生成 css
字符串后,剩下的工作就是生成一个 style
标签插入到 head
中了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| ... css = makeup(file, opt); if (!style) { var head = document.head || document.getElementsByTagName('head')[0]; style = document.createElement('style'); style.type = 'text/css'; style.setAttribute("wxss:path", info.path); head.appendChild(style); ... } if (style.styleSheet) { style.styleSheet.cssText = css; } else { if (style.childNodes.length === 0) style.appendChild(document.createTextNode(css)); else style.childNodes[0].nodeValue = css; }
|
注入的全部代码
这里贴一下注入的全部代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142
| var BASE_DEVICE_WIDTH = 750; var isIOS = navigator.userAgent.match("iPhone"); var deviceWidth = window.screen.width || 375; var deviceDPR = window.devicePixelRatio || 2; var checkDeviceWidth = window.__checkDeviceWidth__ || function() { var newDeviceWidth = window.screen.width || 375; var newDeviceDPR = window.devicePixelRatio || 2; var newDeviceHeight = window.screen.height || 375; if (window.screen.orientation && /^landscape/.test(window.screen.orientation.type || '')) { newDeviceWidth = newDeviceHeight; } if (newDeviceWidth !== deviceWidth || newDeviceDPR !== deviceDPR) { deviceWidth = newDeviceWidth; deviceDPR = newDeviceDPR; } }; checkDeviceWidth(); var eps = 1e-4; var transformRPX = window.__transformRpx__ || function(number, newDeviceWidth) { if (number === 0) return 0; number = number / BASE_DEVICE_WIDTH * (newDeviceWidth || deviceWidth); number = Math.floor(number + eps); if (number === 0) { if (deviceDPR === 1 || !isIOS) { return 1; } else { return 0.5; } } return number; }; window.__rpxRecalculatingFuncs__ = window.__rpxRecalculatingFuncs__ || []; var __COMMON_STYLESHEETS__ = __COMMON_STYLESHEETS__ || {};
var setCssToHead = function(file, _xcInvalid, info) { var Ca = {}; var css_id; var info = info || {}; var _C = __COMMON_STYLESHEETS__;
function makeup(file, opt) { var _n = typeof(file) === 'string'; if (_n && Ca.hasOwnProperty(file)) return ''; if (_n) Ca[file] = 1; var ex = _n ? _C[file] : file; var res = ''; for (var i = ex.length - 1; i >= 0; i--) { var content = ex[i]; if (typeof(content) === 'object') { var op = content[0]; if (op === 0) res = transformRPX(content[1], opt.deviceWidth) + 'px' + res; else if (op === 1) res = opt.suffix + res; else if (op === 2) res = makeup(content[1], opt) + res; } else res = content + res; } return res; }
var styleSheetManager = window.__styleSheetManager2__; var rewritor = 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) { var key = (info.path || Math.random()) + ':' + suffix; if (!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) { var head = 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)); else style.childNodes[0].nodeValue = css; } } return rewritor; }
setCssToHead([])(); setCssToHead( [ ".", [1], "container { height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: space-between; padding: ", [0, 200], " 0; ;wxcs_style_padding : 200rpx 0; box-sizing: border-box; ;wxcs_originclass: .container;;wxcs_fileinfo: ./app.wxss 2 1; }\n", ], undefined, { path: "./app.wxss" } )(); setCssToHead( [ ".", [1], "container { display: flex; align-items: center; justify-content: center; ;wxcs_originclass: .container;;wxcs_fileinfo: ./pages/index/index.wxss 1 1; }\n", ".", [1], "data1{ color: red; font-size: ", [0, 50], "; ;wxcs_style_font-size : 50rpx; ;wxcs_originclass: .data1;;wxcs_fileinfo: ./pages/index/index.wxss 6 1; }\n", ".", [1], "data2{ color: blue; font-size: ", [0, 100], "; ;wxcs_style_font-size : 100rpx; ;wxcs_originclass: .data2;;wxcs_fileinfo: ./pages/index/index.wxss 11 1; }\n", ".", [1], "data3{ color: blue; font-size: ", [0, 100], "; ;wxcs_style_font-size : 100rpx; ;wxcs_originclass: .data3;;wxcs_fileinfo: ./pages/index/index.wxss 17 1; }\n", ], undefined, { path: "./pages/index/index.wxss" } )();
|
编译
剩下一个问题,我们写的代码是:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| .container { display: flex; align-items: center; justify-content: center; } .data1{ color: red; font-size: 50rpx; }
.data2{ color: blue; font-size: 100rpx; }
.data3{ color: blue; font-size: 100rpx; }
|
但上边分析的 <script>
生成 css
的数组是哪里来的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| [ ".", [1], "container { display: flex; align-items: center; justify-content: center; ;wxcs_originclass: .container;;wxcs_fileinfo: ./pages/index/index.wxss 1 1; }\n", ".", [1], "data1{ color: red; font-size: ", [0, 50], "; ;wxcs_style_font-size : 50rpx; ;wxcs_originclass: .data1;;wxcs_fileinfo: ./pages/index/index.wxss 6 1; }\n", ".", [1], "data2{ color: blue; font-size: ", [0, 100], "; ;wxcs_style_font-size : 100rpx; ;wxcs_originclass: .data2;;wxcs_fileinfo: ./pages/index/index.wxss 11 1; }\n", ".", [1], "data3{ color: blue; font-size: ", [0, 100], "; ;wxcs_style_font-size : 100rpx; ;wxcs_originclass: .data3;;wxcs_fileinfo: ./pages/index/index.wxss 17 1; }\n", ],
|
是微信帮我们把 wxss
进行了编译,编译工具可以在微信开发者工具目录搜索 wcsc
,Library
是个隐藏目录。
我们把这个 wcsc
文件拷贝到 index.wxss
的所在目录,然后将我们的 wxss
手动编译一下:
1
| ./wcsc -js ./index.wxss >> wxss.js
|
此时会发现生成的 wxss.js
就是我们上边分析的全部代码了:
总
因此对于代码 wxss
到显示到页面中就是三步了,第一步是编译为 js
,第二步将 js
通过 eval
注入到页面,第三步就是 js
执行过程中把 rpx
转为 px
,并且把 css
注入到 style
标签中。