平常小程序写的多一些,简单总结一下原理。但因为小程序也没开源,只能参考相关文档以及开发者工具慢慢理解了。
理解小程序原理的突破口就是开发者工具了,开发者工具是基于 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
整理一下:
来一段一段看一下:
设备信息
js
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 转换
js
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
了,关于浮点数可以看 一直迷糊的浮点数。
接着往下看:
js
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
js
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
函数:
js
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
中了。
js
...
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;
}
注入的全部代码
这里贴一下注入的全部代码:
js
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" }
)();
编译
剩下一个问题,我们写的代码是:
css
.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
的数组是哪里来的:
js
[
".",
[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
我们把这个 wcsc
文件拷贝到 index.wxss
的所在目录,然后将我们的 wxss
手动编译一下:
js
./wcsc -js ./index.wxss >> wxss.js
此时会发现生成的 wxss.js
就是我们上边分析的全部代码了:
总
因此对于代码 wxss
到显示到页面中就是三步了,第一步是编译为 js
,第二步将 js
通过 eval
注入到页面,第三步就是 js
执行过程中把 rpx
转为 px
,并且把 css
注入到 style
标签中。