Taro + Skyline WXSS 守卫
改 Skyline 项目的样式前先过一遍这里的黑名单,不要靠猜——很多在 WebView 里人畜无害的写法在 Skyline 里会被静默丢弃(不报错、不警告,只是没效果)。
一、最容易踩的 5 个坑
1. 渐变 / 背景 ⚠️ 最容易中招,全是"静默丢弃"
// ❌ 一条声明里叠 5 张图 → 整条被丢弃,页面只剩纯色底
background-image: radial-gradient(...), radial-gradient(...), radial-gradient(...);
// ❌ 两个长度 = 椭圆 → Skyline 只支持 circle,整条被丢弃
background-image: radial-gradient(464px 392px at -8% 4%, #ffdccb 0%, rgba(255,220,203,0) 68%);
Skyline 的硬限制(官方文档只写"radial-gradient() ✓",细节在社区/官方 wxss skill 里):
| 项 | 限制 |
|---|---|
background-image / mask-image |
最多 2 个值(不是"支持多层") |
background-position |
最多 2 组值 |
background-repeat / background-size |
不支持多组值 |
radial-gradient 形状 |
只支持 circle,不支持 ellipse(写两个长度 = 椭圆 → 整条丢弃) |
radial-gradient 尺寸 |
只支持 px(写 rpx 不生效) |
radial-gradient 色标 |
只支持 % |
linear-gradient 色标位置 |
支持 % 和固定长度(px / rpx 等) |
conic-gradient |
完全支持,无额外限制 |
Taro 特有陷阱:pxtransform 会把 px 编译成 rpx,所以"尺寸只支持 px"这条在 Taro 项目里等于没法写尺寸。
解法:索性不写渐变尺寸,用默认的 farthest-corner,把范围交给节点自身的 width/height(布局属性用 rpx 没问题)。
<view class="glow-layer glow-1" />
<view class="glow-layer glow-2" />
// 只负责定位与穿透,具体位置/尺寸/渐变各页面自己给
.glow-layer { position: absolute; pointer-events: none; }
// 一团光晕 = 一个节点:sizeless circle + 节点自身定尺寸,中心用 % + translate 对齐
@mixin glow($rgb, $size, $x, $y) {
left: $x;
top: $y;
width: $size;
height: $size;
transform: translate(-50%, -50%);
background-image: radial-gradient(circle, rgba($rgb, 1) 0%, rgba($rgb, 0) 70%);
}
.glow-1 { @include glow(#ffdccb, 928px, -8%, 4%); }
父容器要 position: relative,内容要 position: relative; z-index: 1 压上去
(Skyline 的 z-index 只作用于兄弟节点,所以"光晕节点在前、内容在后"正好够用)。
正方形容器的 farthest-corner 半径 = 边长/√2 ≈ 0.707×边长,色标停在 70% 时可见半径≈边长/2,刚好等于设计稿的半径。
配套注意:渐变的透明端写 rgba(同一个颜色, 0),不要写 transparent——后者等价 rgba(0,0,0,0),收尾会泛灰边。
同一个坑的另一面:box-shadow 不支持多个叠加。
> 如果 radial-gradient 连 circle 形式都不生效(老版本客户端),退路是:① 用 linear-gradient 斜向近似角落染色(linear 支持度最好);② 预渲染一张 PNG 用 url() 贴上去。
2. filter / backdrop-filter 只支持单个 function
不支持 drop-shadow、不支持 url()。backdrop-filter 还有"与 opacity 混合表现异常""blur 某些情况不一致"的已知问题。
所以毛玻璃方案直接放弃,改用不透明底 + 上浮阴影模拟。
3. 布局类
inline/inline-block不支持(inline-block仅能在text组件内嵌套使用)position: sticky不支持 → 用sticky-header/sticky-section组件替代overflow: scroll不支持 → 只能用scroll-view- 通配选择器
* {}、属性选择器[attr] {}不支持 currentColor不支持- 伪元素只支持
::before/::after env()只支持safe-area-inset-*系列
3.1 scroll-view 必须显式给宽高 ⚠️
Skyline 下 scroll-view 默认不会按内容撑开,必须自己写 width + height,否则高度塌成 0、内容看不见。
(除非在 rendererOptions.skyline 里开 enableScrollViewAutoSize: true,这是全局开关,改了会影响所有 scroll-view,谨慎。)
所以这类"一行标签"的容器要把高度定死,别指望内容撑开:
.cat-bar { width: 100%; height: 64px; } /* scroll-view 本体 */
.cat-chip { box-sizing: border-box; height: 64px; } /* 子项也用同一个高度,避免被裁切/留白 */
> 注意 defaultContentBox: true 的项目里默认是 content-box,height 不含 padding/border,
> 想让 64px 就是最终高度得显式写 box-sizing: border-box。
3.2 横向滚动标签栏的正确写法 ⚠️
社区经典写法是 white-space: nowrap + 子项 display: inline-block。
Skyline 不支持 inline-block,照抄会把每个 chip 压成整行宽,表现为「点 A 命中 B」、
布局竖着堆——很多人以为是 scroll-view 的 bug,其实是 inline-block 没生效。
Skyline 正确写法:内层 flex 行 + 子项 flex-shrink: 0,靠子项溢出撑出可滚宽度。
<scroll-view class="cat-bar" :scroll-x="true" :show-scrollbar="false" :enhanced="true">
<view class="cat-row">
<view class="cat-chip" v-for="c in tabs" :key="c.value" @tap="pick(c.value)">
<text class="cat-chip-text">{{ c.label }}</text>
</view>
</view>
</scroll-view>
.cat-bar { width: 100%; height: 64px; }
.cat-row { display: flex; flex-direction: row; align-items: center; }
.cat-chip { flex-shrink: 0; box-sizing: border-box; height: 64px; display: flex; align-items: center; padding: 0 28px; }
横向 scroll-view 嵌在纵向 scroll-view 里是常规用法,没问题。
3.3 横向 scroll-view 的两个连带坑(都源于"内容一变就重置/裁剪")⚠️
① 滚动位置会被同一次 setData 冲掉:切换选中项时子项 class 变化、或 v-if 切换结构,横条会缩回最左边。用户的原话常是「点了靠后的标签就弹回首位」,注意是回弹——先滚过去又被弹回来。
⚠️ 别用 scroll-into-view 救(实测踩过):Skyline 的横滚 scroll-view 上它不生效,条子照旧缩回最左边(纵向的"回顶"能用它,横滚不行)。而且它是常驻生效属性——值保持非空时,之后任何 setData 都会重新滚到该节点。加上 v-for 出来的子项会被包在 Fragment 里,id 查不查得到还得看渲染器实现,又多一层不确定。
正确做法:受控 scroll-left 像素级定位。 它是普通属性,值一变就精确滚到该像素位置,不查 id、不看节点结构。
const scrollLeft = ref(0)
let liveLeft = 0, syncTimer = null, nudge = 0
/** 滚动中零 setData(会打断原生滚动),停下 220ms 再把真实位置写回绑定值 */
function onBarScroll(e) {
liveLeft = (e.detail && e.detail.scrollLeft) || 0
clearTimeout(syncTimer)
syncTimer = setTimeout(() => {
if (Math.abs(scrollLeft.value - liveLeft) > 1) scrollLeft.value = liveLeft
}, 220)
}
/** 让选中项可见;已经看得见就不滚 */
function keepVisible(value) {
const idx = tabs.findIndex((t) => t.value === value)
if (idx < 0) return
const q = Taro.createSelectorQuery()
q.select('.bar').boundingClientRect()
q.select('.bar').scrollOffset()
q.selectAll('.chip').boundingClientRect() // ⚠️ 用 selectAll + 下标,别用 #id
q.exec((res) => {
const bar = res && res[0], off = res && res[1], chips = res && res[2]
if (!bar || !off || !chips || !chips[idx]) return
const cur = off.scrollLeft || 0
const pad = 22 // 单位是 px:这两个 API 都返回真实 px,不是 rpx
const left = chips[idx].left - bar.left + cur // 子项在内容坐标系里的位置
const right = left + chips[idx].width
let target = cur
if (left < cur + pad) target = Math.max(0, left - pad)
else if (right > cur + bar.width - pad) target = right - bar.width + pad
else return // 已经看得见,不动
nudge = nudge ? 0 : 1 // ⚠️ 见下,不能省
scrollLeft.value = Math.round(target) + nudge
})
}
⚠️ 那个交替 1px 偏移不能省,否则就是"回弹":Skyline 只在 scroll-left 值发生了变化的那次渲染才应用滚动。而 loading=false 会把位置重置为 0,此时若重算出的目标值和绑定值恰好相同(都是 300),Vue 不触发更新 → 条子停在最左边,表现为「滚过去了又被弹回来」。每次写入交替加 0/1px 保证值一定在变;1px 在 335px 可视区里看不出来,用整数是为了避免被渲染器取整丢掉。
量坐标的时机也要留神:位置重置是异步的,补定位(如 loading 结束后)要隔一拍(setTimeout(..., 60))再量,否则量到的是重置前的坐标、算出来"不用滚"就真的不动了。左偏移 + 当前 scrollLeft 这个式子对"位置有没有被重置"两种情形都成立,可以放心用。
② 会裁掉"撑满它高度"的子项的 box-shadow:scroll-view 高 64px、子项也高 64px 时,向下偏移的投影整段落在裁剪区之外,一行都画不出来(实测:子项底边往下第 2 行就是纯背景色)。
要么别写这个阴影(写了是无效声明,还会让人误以为有),要么给 scroll-view 加高 / 留 padding-bottom 腾出裁剪空间。
判据:同一页里不贴裁剪边的元素(如网格卡片)阴影能正常渲染 → 说明不是"不支持 box-shadow",而是"被裁了"。
3.4 多列网格:不要用 gap + 百分比宽度 ⚠️
flex-wrap: wrap + gap 在 Skyline 下,首行会被多顶出一段 gap(实测截图几何:首行整体右移恰好一个 gap 的距离,并因此少放一张卡 → 表现为「第一行 2 张、第二行 3 张」,后面几行正常)。
即使算术上"刚好放得下"也不安全:3×31% + 2×22px = 667.1 ≤ 670 只余 2.9px,一点取整误差就换行。
稳妥写法——不用 gap,用 margin 撑间距,并留 ≥1% 余量:
.grid { display: flex; flex-wrap: wrap; }
.grid-item { width: 29.5%; margin: 0 11px 22px; } /* 3×(29.5% + 22px) = 658.9 ≤ 每屏 670px */
> 想做到"左右贴齐 + 间距精确"就要让每行最后一个不吃 margin(行容器切行、或 :nth-child(3n) 清零)——
> 但 :nth-child 要 Skyline 1.3.3 / 微信 8.0.50+,行容器/负 margin/calc 在 Skyline 上的支持也没背书。
> 多数情况下选"左右各内缩半格间距(如 11px)"的稳方案更划算。
4. 默认值和 web 不一样
| 属性 | Skyline 默认 | Web 默认 |
|---|---|---|
border-*-width |
3 |
medium |
border-*-color |
black |
currentcolor |
display |
flex |
inline/block |
box-sizing |
border-box |
content-box |
后两个项目里常通过 rendererOptions.skyline.defaultDisplayBlock / defaultContentBox 改回 web 语义,改样式前先看 app.json 有没有开。
5. wxss 前缀警告
Unsupported Property. Style Text: -webkit-box-sizing... 这类警告源码里往往没有前缀——是 Taro mini 构建默认开的 autoprefixer 注入的。关掉:
// config/index.js
mini: { postcss: { autoprefixer: { enable: false } } } // h5 端别动
二、改完必须做的编译校验(不依赖 devtools)
沙箱里 node_modules/.bin/sass 可能被执行策略拦(exit 66 + 乱码 stderr),但 sass 的 JS API 可用。
脚本必须写到项目目录内,否则 require('sass') 解析不到 node_modules。
// save as <project>/.scss_check.cjs
const sass = require('sass'), fs = require('fs'), path = require('path')
const ROOT = path.join(process.cwd(), 'src')
const strip = (c) => c.replace(/\/\*[\s\S]*?\*\//g, '') // 断言前必须去注释,否则注释里的示例代码会误报
function walk(d, o = []) { for (const e of fs.readdirSync(d, { withFileTypes: true })) { const p = path.join(d, e.name); e.isDirectory() ? walk(p, o) : ((e.name.endsWith('.vue') || e.name.endsWith('.scss')) && o.push(p)) } return o }
let fail = 0
for (const f of walk(ROOT)) {
const src = fs.readFileSync(f, 'utf8'), rel = path.relative(ROOT, f)
let blocks = []
if (f.endsWith('.vue')) { const re = /<style[^>]*lang=["']scss["'][^>]*>([\s\S]*?)<\/style>/g; let m; while ((m = re.exec(src))) blocks.push(m[1]) }
else blocks.push(src)
if (!blocks.length) continue
blocks.forEach((code, i) => {
const tag = rel + (blocks.length > 1 ? ' #' + (i + 1) : '')
try {
const raw = sass.compileString(code, {
loadPaths: [path.dirname(f), ROOT, path.join(ROOT, 'styles')],
style: 'expanded',
logger: { warn(msg) { throw new Error('WARN: ' + msg) } } // 把废弃警告当失败,@import 之类藏不住
})
const css = strip(raw.css)
console.log(`OK ${tag} -> ${raw.css.length} bytes (去注释后 ${css.length})`)
// 断言 1:background-image 最多 2 个值
for (const line of css.split('\n')) {
if (/background-image\s*:/.test(line)) {
const n = (line.match(/gradient\(/g) || []).length
if (n > 2) { fail++; console.log(` !! ${tag} 一条 background-image 塞了 ${n} 张图(Skyline 最多 2)`) }
}
}
// 断言 2:radial-gradient 只能是 circle + px 尺寸
for (const g of css.matchAll(/radial-gradient\(([^;]*)\)/g)) {
const head = g[1].split(',')[0]
if (/\d+(px|rpx)\s+\d+(px|rpx)/.test(head)) { fail++; console.log(` !! ${tag} 椭圆尺寸「${head.trim()}」,Skyline 只支持 circle`) }
if (/\d+rpx/.test(head)) { fail++; console.log(` !! ${tag} rpx 尺寸「${head.trim()}」,Skyline 只支持 px`) }
}
} catch (e) { fail++; console.log(`FAIL ${tag}\n ` + String(e.message).split('\n').slice(0, 5).join('\n ')) }
})
}
console.log('\n=== ' + (fail === 0 ? '全部通过' : fail + ' 处问题') + ' ===')
运行:node .scss_check.cjs(用 binary_context 里当前的 managed node 绝对路径),跑完删掉脚本。
顺手抓到的一个真问题:共享 tokens 文件的注释会被复制
被 @use 的 tokens 文件里如果用 /** ... */(CSS 注释),sass 会原样输出到每一个引用者的产物。
一个文件 @use 它就多一份注释字节。改成 //(SCSS 行注释)sass 会丢弃,产物立刻变干净:
某项目 tokens.scss 用 /** */ 时编译出 689 字节、改 // 后 0 字节;引用它的 nav-bar.vue 从 1201 → 511 字节。
结论:styles/tokens.scss 这类共享文件一律用 // 注释,且只放变量 + @mixin,不写选择器规则。
配套:类名对账(Skyline 下尤其值得做)
Skyline 静默丢弃不合规样式,所以"模板用了某个 class 但样式没定义"也不会报错,只是看起来没效果。
写个脚本对账,能提前抓到。三个坑:
- 提静态类名的正则要加负向后顾:
/(?<!:)class="([^"]*)"/g。
用/class="([^"]*)"/g会把:class="..."的内容也匹配进来(:class="本身含class="),
于是表达式里的变量名(如activeCat、showTopBtn)被当成类名,刷出一堆假阳性。 :class只取单引号字符串字面量:'chip-' + (tone || 'plain')会切成chip-和plain两个片段,
不是真实类名——把^(chip-|tint-|glow-)$这类已知前缀片段列进白名单排除,最终类是拼接后的chip-blue。- 要双向查:既查"模板用了但样式没定义",也查"色板/常量里可能出现、但样式没定义"。
后者抓到过真 bug:常量表新增了pink,但.chip-pink忘了写,那个分类就落回默认底色。
比"大括号配平"之类的静态检查可靠得多,而且能在没有微信开发者工具的情况下提前抓到废弃警告和语法错误。
三、长度换算(改数值前必须确认)
先看 config/index.js 的 pxtransform 配置确定比例:
- 本项目现状:
1px(code) = 1rpx(编译产物里464px → 464rpx),
即 code 数值 = 375 设计稿数值 × 2。 - 从 375 宽的设计稿搬数值进代码时,记得乘以 2,否则所有尺寸会小一半。
判断方法:改完看编译产物 wxss,464px 出来是 464rpx 就是 1:1,是 928rpx 就是 2:1。
四、其它 Skyline 行为陷阱
scroll-into-view是常驻生效属性:只要保持非空,之后任何setData都会重新滚到该节点,
表现是"往下滚一点就被弹回顶部"。用法:设值 →nextTick→ 定时器(约 400ms)后清空。
它只在纵向(页面/竖滚 scroll-view)上可用;横滚 scroll-view 上实测不生效,见 § 3.3 ①。scroll-left只在值变化时应用:想用受控scroll-left精确回滚位置(横条保持选中项可见、列表恢复位置),
每次写入都要让值真的变(交替 1px 偏移即可),否则"位置被重置为 0 + 目标值与旧值相同"就什么都不会发生。同 § 3.3 ①。- 滚动过程中
setData会打断原生滚动:滚动事件要跨线程回调逻辑层,所以
@scroll里不要直接 setData,用 ~120ms 防抖,等滚动停止再更新 UI(如"回到顶部"按钮)。 - 不支持的客户端会 fallback 到 WebView:所以两套渲染都要能看。
glassEaselWebview是 app.json/页面 json 的顶层字段(与componentFramework并列,不能放window里),WebView 下启用 glass-easel 运行时需基础库 ≥ 3.8.12。
五、图标怎么办
Skyline 不支持 <u-svg> 之类的 svg 组件。图标用这三条路:
1. CSS 画(圆点、色块、border 三角)
2. 单色字符(› ↑ ‹ 之类,注意字体渲染差异)
3. <image> 载入 png/svg 资源