前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >小程序原理系列一之wxss

小程序原理系列一之wxss

作者头像
windliang
发布2023-11-27 15:54:57
1660
发布2023-11-27 15:54:57
举报
文章被收录于专栏:windliang的博客windliang的博客

平常小程序写的多一些,简单总结一下原理。但因为小程序也没开源,只能参考相关文档以及开发者工具慢慢理解了。

理解小程序原理的突破口就是开发者工具了,开发者工具是基于 NW.js,一个基于 Chromiumnode.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 整理一下:

来一段一段看一下:

设备信息

代码语言:javascript
复制
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();

主要更新了几个变量,deviceWidthdeviceDPR ,像素相关的知识很久很久以前写过一篇文章 分辨率是什么?。

https://zhuanlan.zhihu.com/p/55819582

这里再补充一下,这里的 deviceWidth 是设备独立像素(逻辑像素),是操作系统为了方便开发者而提供的一种抽象。看一下开发者工具预设的设备:

如上图,以 iphone6 为例,宽度是 375 ,事实上 iphone6 宽度的物理像素是 750

所以就有了 Dpr 的含义, iphone6dpr21px 相当于渲染在两个物理像素上。

rpx 转换

代码语言:javascript
复制
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_WIDTH750 ,也就是微信把屏幕宽度先强行规定为了 750 ,先用用户设定的 rpx 值除以 750 算出一个比例,最后乘上设备的逻辑像素。

如果设备是 iphone6 ,那么这里设备的逻辑像素就是 350,所以如果是 2rpx2/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 了,关于浮点数可以看 一直迷糊的浮点数

接着往下看:

代码语言:javascript
复制
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

代码语言:javascript
复制
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 函数:

代码语言:javascript
复制
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 其实就是用户写的 50rpx50 ,因此需要调用 transformRPX50 转为 px 再相加 res = transformRPX(content[1], opt.deviceWidth) + 'px' + res;

通过 makeup 函数,生成 css 字符串后,剩下的工作就是生成一个 style 标签插入到 head 中了。

代码语言:javascript
复制
...
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;
}

注入的全部代码

这里贴一下注入的全部代码:

代码语言:javascript
复制
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" }
)();

编译

剩下一个问题,我们写的代码是:

代码语言:javascript
复制
.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 的数组是哪里来的:

代码语言:javascript
复制
[
    ".",
    [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 手动编译一下:

代码语言:javascript
复制
./wcsc -js ./index.wxss >> wxss.js

image-20231125124432358

此时会发现生成的 wxss.js 就是我们上边分析的全部代码了:

因此对于代码 wxss 到显示到页面中就是三步了,第一步是编译为 js,第二步将 js 通过 eval 注入到页面,第三步就是 js 执行过程中把 rpx 转为 px,并且把 css 注入到 style 标签中。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2023-11-26,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 windliang 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 设备信息
  • rpx 转换
  • 生成 css
  • 注入的全部代码
  • 编译
相关产品与服务
云开发 CLI 工具
云开发 CLI 工具(Cloudbase CLI Devtools,CCLID)是云开发官方指定的 CLI 工具,可以帮助开发者快速构建 Serverless 应用。CLI 工具提供能力包括文件储存的管理、云函数的部署、模板项目的创建、HTTP Service、静态网站托管等,您可以专注于编码,无需在平台中切换各类配置。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档