小程序原理系列一之 wxss
平常小程序寫的多一些,簡單總結一下原理。但因爲小程序也沒開源,只能參考相關文檔以及開發者工具慢慢理解了。
理解小程序原理的突破口就是開發者工具了,開發者工具是基於 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
整理一下:
來一段一段看一下:
設備信息
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
,像素相關的知識很久很久以前寫過一篇文章 分辨率是什麼?。
https://zhuanlan.zhihu.com/p/55819582
這裏再補充一下,這裏的 deviceWidth
是設備獨立像素 (邏輯像素),是操作系統爲了方便開發者而提供的一種抽象。看一下開發者工具預設的設備:
如上圖,以 iphone6
爲例,寬度是 375
,事實上 iphone6
寬度的物理像素是 750
。
所以就有了 Dpr
的含義, iphone6
的 dpr
是 2
, 1px
相當於渲染在兩個物理像素上。
rpx 轉換
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 問題及解決方案。
https://zhuanlan.zhihu.com/p/535456539
接下來一行 number = Math.floor(number + eps);
是爲了解決浮點數精度問題,比如除下來等於 3.9999999998
,實際上應該等於 4
,只是浮點數的問題導致沒有算出來 4
,加個 eps
,然後向下 floor
去整,就可以正常得到 4
了,關於浮點數可以看 一直迷糊的浮點數。
接着往下看:
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
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
函數:
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
中了。
...
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;
}
注入的全部代碼
這裏貼一下注入的全部代碼:
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" }
)();
編譯
剩下一個問題,我們寫的代碼是:
.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],
"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
手動編譯一下:
./wcsc -js ./index.wxss >> wxss.js
此時會發現生成的 wxss.js
就是我們上邊分析的全部代碼了:
總
因此對於代碼 wxss
到顯示到頁面中就是三步了,第一步是編譯爲 js
,第二步將 js
通過 eval
注入到頁面,第三步就是 js
執行過程中把 rpx
轉爲 px
,並且把 css
注入到 style
標籤中。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/Yvp6reXftZ9yAFkxPrZlcQ