状态:spec 046 落地的第一版。目标形态是微信小程序 Skyline 渲染 + glass-easel 组件框架(
renderer: skyline、componentFramework: glass-easel、lazyCodeLoading: requiredComponents)。
fjs 的第三条产物路径。同一份 Vue SFC 源码:
| 路径 | 产物 | 模板去向 | 运行时 |
|---|---|---|---|
fjs build |
Flutter 宿主 bundle | render 函数(custom renderer) | element API + op 协议 |
fjs build --web |
静态站点 | render 函数(DOM 适配层) | vue-router + DOM 组件 |
fjs build --mp |
微信小程序四件套 | WXML(编译期直译) | @ufjs/runtime/wx 薄壳 |
与另外两端最大的不同:不引入 Vue 运行时。模板编译为 WXML 而非
render 函数,不打包 vdom;import { ref } from 'vue' 被别名到
@ufjs/runtime/wx,那只是 @vue/reactivity(ref/computed/watch,无
DOM、无 vdom)加一层 setup→setData 的胶水。分层思路对齐 wevu:
编译期(weapp-vite)管转换,运行期(wevu)管响应式与最小 setData。
cd examples/hello-fjs
pnpm build:mp # = fjs build --mp
pnpm dev:mp # = fjs dev --mp:watch src/,增量重建 dist/mp
# 用微信开发者工具打开 dist/mp/(appid 默认 touristappid,可在
# package.json fjs.mp.appid 配置)dev 模式没有 HTTP 服务:开发者工具自己监听 dist/mp 的文件变化并热编译,
fjs dev --mp 只负责 watch 源码 → 重建(全量发射,~100ms 级)。
产物是 TypeScript 源码(对齐官方 TS 快速启动模板):每个 SFC 发射
为一个可读的 .ts 模块,import 已重写到发射位置,TS 编译交给开发者
工具(useCompilerPlugins: ["typescript"])——fjs 不对应用代码做
esbuild 打包。跨页单实例不再靠注册表 hack:所有本地 TS/JS 依赖发射到
fjs/shared/<源相对路径>,微信的模块缓存保证全局单份。
package.json 的 fjs.mp 字段:
appid 的来源优先级:app.config.ts wxmp.appid(app 级配置,与 android/
ios 并列)> package.json fjs.mp.appid > touristappid。
wxmp.setting 写入 project.config.json 的 setting,与 fjs 的默认值按键
浅合并、用户优先:
wxmp: {
appid: 'wx55831603b568aa90',
renderer: 'skyline',
setting: { minified: true, minifyWXSS: true, minifyWXML: true },
}默认值里 es6 / enhance 为 true(产物是 TS 源码,typescript 插件只剥类型,
??、?. 等语法要靠它们降级,否则预览/上传报 Unexpected token ?),
useCompilerPlugins: ["typescript"] 必须保留——覆盖这几项要清楚后果。
dist/mp/
project.config.json miniprogramRoot + useCompilerPlugins: typescript
miniprogram/
app.{ts,json,wxss} skyline + glass-easel 全局配置
sitemap.json
fjs/runtime.ts vendor 包:wx 运行时 + @vue/reactivity(CJS)
fjs/rich-text.js rich-text 管线(仅 skyline 且有页面用到 <rich-text> 时产出)
fjs/routes.ts 生成路由表('fjs/pages' 的 wx 形态)
fjs/shared/<rel> 发射的本地 TS 模块(theme.ts、catalog.ts…)
fjs/modules/<module>/<tag>/ 模块包提供的组件四件套(如 iconmind)
fjs/{fjs-modal,…}/ runtime 提供的组件四件套(fjs-rich-text / fjs-rich-node 按需)
components/<name>/* 编译后的本地组件(Shell、Panel…)
pages/<route>/* **页面本体**:页面 SFC 即 page
assets/ 静态图片(import 被改写成根绝对路径常量)
<sub>/… 分包(fjs.mp.subpackages):页面、专属 vendor、
专属 shared、搬入的 public 目录,见下节
页面就是页面:路由 SFC 直接以 Component()(isPage: true)注册为
小程序页面(skyline 模板推荐写法),它自己的 wxml 就是
<shell route="{{ route }}">页面元素</shell>——shell 的包裹方式与
createFjsApp 在另外两端做的一致,route(path/query/meta)由编译器
注入 setup 并在 onLoad 同步 query。virtualHost 让组件不产生宿主
节点,flex 链与另外两端一致。emit 的 import 重写规则:
vue/fjs/fjs/router/@ufjs/runtime/wx/@ufjs/webgl → fjs/runtime;fjs/pages
→ fjs/routes;@/x.png 类资源 → 根绝对路径常量;其余本地模块 →
fjs/shared/<rel>(递归发射,源结构保持不变);import type 整条擦除;
裸导入按 app 的 package.json 处理:包名在 dependencies / devDependencies
里的,→ fjs/npm/<spec>.js(CJS shim),所有用到的 npm 入口由 esbuild 打进
一个 fjs/npm/vendor.js(es2018、minify,共用内部模块只有一份实例,如
echarts/zrender、three 及其 addons);不在 package.json 里的包名直接报错。产物
根目录不写 package.json:构建不使用「构建 npm」,该文件在 miniprogramRoot
之外也不参与上传,留着只会让开发者工具弹无关的 npm 构建提示。没用
开发者工具的「构建 npm」:它只打包每个包的 main 入口,子路径导入
(echarts/core、three/examples/jsm/...)和纯 ESM 包过不去,而且要求产物
目录里有 node_modules。注意主包 2MB 上限:大库(three、echarts 全量)真机
预览 / 上传可能超限——配分包(下一节)把这些库挪出主包。
package.json 的 fjs.mp.subpackages 声明微信 subPackages,页面源码零改动:
"fjs": {
"mp": {
"subpackages": [
{ "root": "game", "pages": ["example/game/"], "public": ["wm", "fb"] },
{ "root": "canvas", "pages": ["example/canvas/", "example/animation/"], "public": ["spine"] }
]
}
}-
pages片段的匹配语义与mp.exclude一致(path 全等 / includes / 页面名)。 页面命中两个分包、tab 页被配进分包、root 用保留名(pages/components/images/assets/workers/fjs/html)或互为前缀、主包一个页面不剩——都是 构建期报错。 -
npm vendor 按归属拆包:只被一个分包可达的库进
<root>/fjs/npm/vendor.js; 被主包或多个包引用的进主包 vendor(子包 require 主包合法,反之禁止;共享库 因此只有一份实例)。每包仍有一份fjs/npm/<spec>.jsshim,指向 spec 实际 所在的 vendor。本地模块同样按可达性归属:只被一个分包可达的进<root>/fjs/shared/,否则主包——否则一个只有分包页面用的 adapter 会把它 的大库依赖拽回主包 vendor。 -
public 目录跟分包走(
public字段,目录名单个名字):图片复制到<root>/<dir>/,产物里该包模块的字符串字面量与 wxml 里的/dir/…前缀 统一改写成/<root>/dir/…(模板串头部也改写,`/wm/${name}.png`这类 动态拼路径因此无需改页面)。数据文件(atlas/skel…)的 base64 模块仍留主包 (public-data 注册表在主包 app.ts 注册,主包不能 require 子包文件),注册表 key 改写成新 URL。守卫:包外任何产物文件若仍引用已搬目录的前缀,构建 报错并列出文件——宁可失败也不发一个真机断图的包。 -
已知限制:URL 拼在更长字符串中段(
'go /wm/x')不改写也不报错;wxss 的url()不支持(fjs 样式引擎本就不支持背景图,产物里不会出现)。 -
components/、runtime 组件、workers/恒在主包(子包页面跨包 require/引用 主包组件均合法)。没配subpackages时产物与不分包版本逐字节同形。 -
分包预下载:
mp.preloadRule按 fjs 路由路径(可省开头的/)声明, 编译期翻译成 app.json 的真实页面路径 key;packages引用subpackages的 root("__APP__"表示主包),network可选"all"/"wifi"(默认微信的 wifi)。key 不是已知路由、packages 引用未声明的 root、network 非法都是构建 报错。同组页面共享 2MB 预下载限额(微信侧校验)。hello-fjs 的配置:进"示例" tab 即在任意网络下预下载 game 与 canvas:"preloadRule": { "/example": { "network": "all", "packages": ["game", "canvas"] } }
-
hello-fjs 是参考配置:主包从 3.1MB 降到 ~1.2MB(源码口径),game/canvas 两个 分包各 ~0.8/1.1MB。
| fjs 标签 | 小程序端 |
|---|---|
| view/text/image/scroll-view/swiper/swiper-item/button/input/textarea/switch/slider/form/picker(-view/-column)/web-view | 同名直出(下面几行是直出时要补的语义/外观) |
| text | 打 .fjs-text;被 align-items: center/flex-end 的 column 父级(本 SFC 的 class)居中/尾对齐、且自身没有背景/边框/宽度的 text 再打 .fjs-text--center/--end(stretch + text-align)——skyline 不给交叉轴居中的 text 传宽度约束,长文本不换行 |
| button | 打 .fjs-button + 由静态 type/plain/size 推出的变体 class,数值同 base-css(重置 wx 按钮的 184px 宽、粗体、灰底);默认 hover-class="fjs-button--pressed":按住时 box-shadow: inset 0 0 0 999px rgba(0,0,0,.1) 整体压暗 10%(WeUI 按压模型,数值同 base-css 的 :active::after)。不用 ::after:webview 内置按钮自己占用 ::after(细边框),遮罩画不出来;内阴影两种渲染器都生效且跟随圆角。页面自带 hover-class 时不覆盖。disabled(静态或绑定)打 .fjs-button--disabled:保持变体配色、整体 50% 透明(wx 自带的灰色禁用外观被覆盖,同 base-css :disabled);loading 不交给 wx(其图标是深色小图、skyline 下还叠在文字上方),编译器在文字前画 web 同款 14px 转圈(2px 描边、四分之三圆,用两块裁剪拼出——skyline 四边颜色不同会丢圆角;样式随用到它的组件 wxss 输出,因为 skyline 不执行 app.wxss 里声明的 @keyframes),按钮打 .fjs-button--loading 不可点、不变淡 |
| input | secure→password,keyboard→type,默认补 maxlength="-1"(wx 默认 140);multiline(静态或 :multiline="true")编译为 textarea |
| switch | value→checked,默认 color="#34c759" |
| slider | 默认 active-color/block-color #007aff、block-size 16,去掉 wx 左右 18px 外边距 |
| swiper | 直接子节点必须是 swiper-item(<template> 透明、<slot> 放行),否则编译报错——与 Flutter / Web 共用 src/template/swiper-children.ts(specs/051);swiper-item 的直接子元素打 .fjs-fill 撑满 |
| scroll-view | 子节点统一包进 .fjs-scroll-inner,本 SFC class 里的 flex 布局声明(gap/flex-direction/align-items…)内联到包装上——skyline type="list" 不对直接子节点做 flex;direction: horizontal(class/style/属性)→ scroll-x。例外:静态 type="custom" 的 scroll-view 是 sticky 组件的宿主(sticky-header / sticky-section 必须是它的直接子节点),不注入 type/enable-flex、也不包内层;绑定的 :type 无法静态判断,按普通 list 滚动容器编译 |
list-view(:items + #default="{ item, index }") |
scroll-view type="list" + wx:for,行是直接子节点(skyline 按需构建,等价虚拟化,不包内层) |
| defer(specs/118) | 透明 <block>,内容与页面一起渲染;placeholder-height(静态 / 绑定、两种拼写)编译期丢弃 |
| checkbox / radio / checkbox-group / radio-group / label | runtime 组件 fjs-*(wx 原生语义不同:状态在 checked、change 只在 group 上触发)。value 布尔、change 载荷 "1"/"0",group 载荷同 ui-api.md;label 点整行转发给 for 指向或第一个控件。宿主 class 上的 flex 布局经 layout 属性内联到组件根节点(skyline 不支持 inherit) |
| progress | runtime 组件 fjs-progress:value 0-1、缺省为不定进度、type="circular" 转圈 |
| rich-text | 按渲染器分流(spec 050)。webview:原生 rich-text,nodes / space / user-select / bindtap 原样透传,只补 fjs-rich-text-host class(块级盒子),不打包管线、不拷组件。skyline:runtime 组件 fjs-rich-text(原生实现行内元素各占一行、无 ol 编号、表格挤成一行、img 宽度与 pre 空白失效)。在 wx 运行时跑另两端同一条管线(rich-text/* → wx/rich-text.ts 转成渲染数据),管线不在 fjs/runtime.ts 里:有页面用到时才单独打成 fjs/rich-text.js,由组件 require('../rich-text');由 fjs-rich-node 画出:块是 view、一个段落是一个 text 加一层行内 run、含图片的段落是可换行的横排、hr 是 16px 高中间 1px 线。组件实例只在块级嵌套处递归。编译器补 scope(页面 data-v class),并把用到 rich-text 的 SFC 的 wxss 汇总到 fjs/fjs-rich-node/page-styles.wxss 由组件 @import——skyline 下页面样式进不了组件模板。没有页面用 rich-text 时两个组件与 fjs/rich-text.js 都不产出 |
| picker-view | 打 .fjs-picker-view(220px 高)、picker-view-column 的直接子元素打 .fjs-picker-item(44px 行,居中 16px #333333),indicator-style 默认 height: 44px;静态 item-height 换算成内联高度(非数字字面量告警后按 44)。:value 编译为 value="{{ __fjs.pickerValue(v, __fjsPvN) }}":skyline 丢掉创建时的 value、列选项被整体替换时(联动列)再丢一次,但事后交相同下标就生效且不派 change,所以运行时 pickerSync 在首帧渲染完、以及 value / 各列 v-for 列表变化那次 setData 渲染完(setData 回调)后把 tick +1,wxs 重算出数组副本。v-for 里的 picker-view 只有首帧那次 |
| form | 原生 form;@submit 载荷 JSON.stringify(detail.value),键序即文档序(与 web 对拍一致)。fjs-checkbox / fjs-radio / 两个 group 挂 wx://form-field:交互把当前态写回 value;group 是字段(checkbox-group 值为选中名字数组、radio-group 为选中名字或 ''),成员挂到 group 下时把标识名挪到内部 key、清空 name——原生 form 跳过空 name 的字段,组内成员因此不单独出现 |
| inner-canvas | canvas type="2d" |
| 页面根节点的 scroll-view | 页面在 shell 的滚动主体里(有 shell 且路由未声明 scroll: false)时编译为普通 view:web / Flutter 上内层滚到头会交给外层、且内层本就没有有界高度,滚动全在外层;skyline 不做滚动接力,内层必须写死高度,超出主体视口的部分永远划不到(tab 页最后一截被挡)。自己管滚动的页面声明 scroll: false。例外:skyline 下静态 type="custom" 的根 scroll-view 不降级(skyline 没有页面级滚动,降级会连 sticky 子组件一起作废),高度检查照常强制 |
| scroll-view | 追加 type="list"(skyline 必需,webview 兼容)与 enable-flex——.fjs-box 基线让 scroll-view 成为 flex 容器,webview 下不加这个属性会告警;而去掉 flex 的话 webview 的滚动区不计入内容(scrollHeight 等于盒子高度,滚不动)。页面自己写了 type 时让位,见上面的 custom 例外 |
| sticky-header / sticky-section | 按渲染器分流(specs/052)。skyline:原生组件原样直出,offset-top / push-pinned-header / bindstickontopchange 都交给 skyline;必须是 type="custom" scroll-view 的直接子节点。注意 skyline 原生组件在首帧会对每个 header 先派一次 isStickOnTop:false(web / Flutter 端首帧静默),页面若以「收到事件」为吸顶依据需自行忽略首次。webview:编译为 runtime 自定义组件 fjs-sticky-header / fjs-sticky-section(specs/053,skyline 构建不拷贝这两个四件套)。组件用 virtualHost——sticky 必须长在页面流真实节点上,真实宿主会把吸顶边界缩到自身高度;事件由 IntersectionObserver(relativeToViewport 按 offset-top 平移)测量,offset-top 支持绑定值,allow-overlapping / padding / push-pinned-header 接受但不生效 |
| stack / divider / safe-area / position | view + 内置 class(fjs-stack 等,取值同 web 的 base-css) |
| modal | fjs-modal 自定义组件(runtime 提供,@modal-closed 同名;底部 sheet,数值同 base-css) |
| page-container | 同名透传 wx 原生标签(基础库 ≥ 2.16.0,specs/065):不进任何映射表,show / duration / round / close-on-slide-down 等属性原样直出,虚线事件走兜底命名(@before-enter → bindbeforeenter,与 wx 的 bind:beforeenter 等价)。web / Flutter 端是各自的实现(见 ui-api.md 三端差异表),返回手势关闭在小程序端由 wx 原生提供 |
| 模块 widget 标签(如 icon-mind) | 由模块包提供:fjs.widgets.<tag>.mp 指向包内四件套,构建拷贝到 fjs/modules/<包名>/<tag>/ 并写入 usingComponents。构建同样跑模块的 prepare 钩子,生成的每个 .json 转成 fjs/modules/<包名>/data/<file>.js(CommonJS,组件 require('../data/icons.json.js'))。编译器给 widget 补 fjs-color:从本 SFC 静态 class / style 推出的继承色(自身优先,再找祖先),可能是 var(--x) |
模板 @x 编译为 bindx="__fjsCall" + data-* 属性,运行时
adaptEvent(tag, event) 把 wx 事件对象归一成 fjs 语义载荷后再调
handler(载荷约定与另外两端一致,如 switch @change 收 "1"/"0"、
input 收 e.detail.value、tap 无载荷)。内联箭头函数被提取成生成代码,
v-for 作用域变量经 data-args 传递。已对账的 tag×event 组合见
fjs-runtime/src/wx/events.ts,未列出的走 e.detail 透传。数组/对象载荷
一律转成另两端同样的字符串:picker-view 与 picker @change 是下标数组 JSON 串
(单列 picker 是下标串),picker @columnchange 是 {"column":0,"value":2},
form @submit 是 {name: value} JSON 串。
| Vue 写法 | 小程序端 |
|---|---|
| 插值、三元、算术、成员访问 | {{ }} 直译 |
| v-if / else-if / else | wx:if / wx:elif / wx:else |
| v-for + :key | wx:for + wx:for-item/index + wx:key;嵌套循环未命名的下标自动取 __i1… |
n in 3 / n in rows(数字) |
字面量展开成 [1, 2, 3];脚本里初值为数字的绑定走 fjs/fjs.wxs 的 list()——Vue 从 1 数,wx:for 从 0 数 |
v-for 内依赖 item 的函数调用(picked.includes(item.id)) |
生成按下标取值的 computed 表 __dN[index](WXML 不能调用函数,实例级 computed 看不到循环变量) |
| v-show | hidden |
| :class 对象/数组/模板字符串 | 编译期内联展开(保留 v-for 作用域) |
| :style 对象/标识符 | 内联展开(数字值经 wxs unit() 补 px);含展开运算符/函数调用的走 stringifyStyle computed,v-for 内为按下标取值的 computed 表 |
| 内联事件 handler | 提取为 setup 内生成函数,经 __fjsCall 分发;v-for 内经 data-args 传各层下标,handler 里从响应式列表取回原对象(按值传的是 setData 快照副本,改它不会更新) |
requestAnimationFrame / cancelAnimationFrame |
小程序模块包装里这两个名字是 undefined 的遮蔽绑定,编译器给用到的模块补 import,运行时以 16ms 定时器实现 |
setImmediate / clearImmediate |
宿主里根本没有:库用 typeof window !== 'undefined' 探测浏览器时会走 Node 分支读这个裸全局(Anime.js 在模块求值期就这么挑主循环)。与上一行同一套注入,映射到同一个帧定时器——不用「尽快执行」的宏任务,那样的自我重排循环会把线程占满 |
模板中的函数调用 {{ f(x) }} |
提取为 computed |
| v-model(input/textarea) | value + bindinput |
| 默认/具名 slot | slot 直译 |
| 作用域插槽 | ❌ 告警并丢弃 |
let 绑定的模板更新 |
❌ 用 ref(与 Vue 行为一致) |
app.wxss 承载全局布局基线(容器 = column flex + border-box,同 web 的
base-css)。两个 skyline 硬约束决定了它的形态:
- skyline 只支持 class 选择器——
view {}这类标签选择器被静默忽略。 基线因此写在.fjs-box上,编译器给每个容器标签(view/scroll-view/ swiper 等,见 wxml.ts CONTAINER_TAGS)统一打这个 class。 - 百分比高度链在 skyline 不可靠——
page用100vh显式定高(官方 skyline quickstart 同款),页面:host用flex: 1 1 0%从 page 接链。
另外组件 JSON 一律 "styleIsolation": "apply-shared"——否则 app.wxss 进
不了组件(默认 isolated),基线同样失效。这三个坑任何一个漏掉,症状都
是"内容全部叠在页面顶部"。
- 样式:WXSS 是真 CSS。scoped 靠「属性选择器→class」改写(skyline
不匹配属性选择器);
position: fixed受 skyline 限制。 - 无单位数字 / flex-grow:与 web 共用
css-compat.ts的长度规则(height: 28→28px,@media条件同理);flex-grow: n补flex-basis: 0%(Expanded 语义,百分比基数在不定高容器里回退为内容尺寸)。 :active:改写为.fjs-pressed,模板里带该 class 的元素加hover-class="fjs-pressed" hover-stay-time="60";:hover/:focus触屏无对应,丢弃并告警。@media:skyline 不求值条件(所有块都生效)。编译期把块 N 的规则主体补.fjs-mq-N,带主体 class 的元素 class 上加{{ __fjsMq[N] }};运行时用 App 端 CSS 引擎同一个求值器(css/parser.tsmediaMatches)按窗口尺寸填充,wx.onWindowResize时重算。代价:这些规则比同名基础规则多一个 class 的优先级。横竖屏切换需要 app.json 的pageOrientation(当前未设)。- 页面路由:页面在
onLoad里挂载(此时才有 query),route由 setup 最开头的__fjsRoute提供并立即设为当前路由,页面自己useRoute()读到的就是本页和本页 query;路由字面量同时作为页面初始 data,先于页面挂载的 shell 首帧即可读到。onPageSettled以首帧渲染(onMounted)后一个宏任务近似——小程序没有转场结束事件。 - touch 事件:载荷与另外两端同形(
FjsTouchEvent:touches / changedTouches / identifier / offsetX 等),原点取 wx 的currentTarget.offsetLeft/Top(skyline 下查节点 rect 补上);canvas 的触点自带相对画布的x/y,offsetX/Y 直接取它,不用 clientY 减原点(两者参照系在 skyline 真机上可能不一致,纵向命中会偏);targetTouches近似为 touches;touch-action(class 或静态内联 style 里的 none / pan-x / pan-y)由编译器落地:webview 下none把 touchmove 编译成catchtouchmove(无处理函数时绑空方法);skyline 下节点外包同轴的horizontal-/vertical-drag-gesture-handler(none 两个都包,pan-y 只包横向,pan-x 只包纵向),内层同类型手势先识别,外层 scroll-view 不再滚动。手势组件是虚拟节点,不影响布局;v-if挪到最外层。 - 布局基线:全局默认 flex column + border-box(对齐 Flutter/web 两
端,因此 app.json 故意不设
defaultDisplayBlock/defaultContentBox)。 - public/:
public/下的图片拷到小程序根目录,/images/x.png这类根绝对路径两端一致。非图片文件(数据文件)也进包,但不以文件形式:真机上 FileSystemManager 读不到代码包(DevTools 读的是源码目录,只在真机上失败),白名单外的后缀(.atlas、.skel、.txt)上传时还会被过滤。所以每个数据文件编译成fjs/public/<路径>.js(导出 base64),fjs/public-data.js以字面量require懒加载登记(「过滤无依赖文件」会保留),app.ts启动时交给运行时;wx 的fetch('/spine/x.atlas')这类根相对 GET 不走wx.request(会报 invalid url),先查这张表,查不到再用 FileSystemManager,都没有返回 404。按路径被 wx API 使用的媒体类文件(mp3 / mp4 / wasm 等)照旧原样拷贝。npm 依赖里import '@ufjs/runtime'/'fjs'在 vendor 打包时映射到 wx 运行时(fjs/runtime),不会再打进一份 App 运行时。小程序模块包装会把requestAnimationFrame/cancelAnimationFrame遮成 undefined:SFC 由编译器补 import,vendor.js 顶部则注入转发到 wx 运行时的同名函数(spine-core 的AssetManager.loadAll靠它轮询)。 - safe-area:
edges(与 web / Flutter 同义)限定补哪几边。顶边只在盒子顶边位于状态栏下方之外时补(测量顶边,它不随自身 padding 移动);未写edges且顶边已在状态栏下方的是嵌套,全为 0(等价 Flutter SafeArea 对 MediaQuery 的消耗)。底边补 Home 指示条,tab 页不补(原生 tabBar 已占据,沿 owner 链查路由表meta.tab);按窗口底边(screenTop + windowHeight)而非屏幕底边算——Android 微信的页面窗口止于系统导航栏上方、那一条由微信自己涂色,内容无法穿过它,也不应再补一次。底边不测量:webview 渲染器排版过程中报告的盒子会随自身 padding 与内容增长,测量值曾变成约 600px 的底部内边距,把 shell 的 scroll-view 挤成零高——整页无法点击、:active无效。组件自身不 grow(与另外两端一致),宿主与根节点min-height: 0:webview 遵循 CSS 的 flex 最小内容尺寸,高度链断开时 scroll-view 会长到整页内容高,变成整个窗口滚动。 - 页面根:shell 模板(无 shell 时为页面模板)的根元素由编译器打
.fjs-page-root,撑满页面——对应 web 的fjs-page-host > *与 Flutter 根节点的 growChildren。hello-fjs 的 NavBar 自带safe-area edges="top"(状态栏区域与导航栏同底色、固定在滚动区外),TabBar 自带edges="bottom",二级页的底部安全区放在 shell 滚动区内容末尾(内容可滚到指示条下;scroll: false的页面仍在外面留一条)。 - skyline 限制(已确认,未绕过):input 不认
line-height,单行输入框比 web 矮约 2px;DevTools 模拟器里 textarea 的placeholder-style/placeholder-class不生效;text-transform不支持;四边颜色不同的 border 会让border-radius失效(fjs-progress 的圆环因此用裁剪实现)。 - icon-mind:skyline 没有内联 SVG,组件把与 web 替身相同的形状(描边粗细、duotone 规则一致)拼成 SVG data URI 交给
<image>。image 不继承color、skyline 又读不到计算样式(SelectorQuery 的 computedStyle 为空,mask-image/filter也不支持),颜色按color属性 >fjs-color>#333333取;var()由 wx 运行时解析——:style绑定里出现过的 CSS 自定义属性全部登记在一张全局表(style.tsresolveCssColor,主题切换时通知重绘)。局限:继承色只看模板静态 class,经:class动态切换或跨组件继承的颜色拿不到。原生 tabBar 不支持 SVG 图标,tab 仍只有文字。 - canvas:映射为
type="2d",wx/canvas.ts桥接 2d:<canvas ref>编译成id="fjs-cv-<ref>",首次渲染后经 SelectorQuery 拿节点赋给 setup ref(getContext('2d')/width/height/devicePixelRatio/toDataURL);@resize在测量到尺寸及窗口尺寸变化时触发,载荷同另两端;backing store = 布局尺寸 × pixelRatio,页面坐标是逻辑像素。loadCanvasImage等第一块 canvas 出现后用createImage()加载,drawImage / createPattern 里按节点换成该节点自己的图片(尚未解码完的那帧跳过);加载失败会 console.warn 路径和 errMsg。真机createImage不认 webp(安卓实测失败,模拟器正常),canvas 素材用 png / jpg。限制:ref 只认静态标识符、不能与id并用,v-if / v-for 里的 canvas 拿不到;DevTools 不支持 skyline canvas 调试,需真机。WebGL:import 了@ufjs/webgl的 SFC,canvas 编译为type="webgl"(wx 节点的 type 决定 context 种类,显式写type优先;@ufjs/webgl在小程序端映射到运行时,无需注册),getContext('webgl' / 'webgl2')直接返回 wx 的 context,texImage2D/texSubImage2D接受loadCanvasImage的句柄;同一块 canvas 只能拿 2d 或 webgl 其一。基础库没有 WebGL2 时getContext('webgl2')返回 null(基础库会打一条Invalid context type [webgl2]的 error),页面按?? getContext('webgl')回落。 - defer:App 与 web 上折叠线下的内容等转场结束才挂,小程序上随页面一起出——
<defer>要省的是 JS 挂载那一帧,skyline 按需构建节点,没有这笔账,拆开反而多一次 setData。placeholder-height因此没有意义,编译期丢弃。 - rich-text(skyline):嵌套 text 不支持
vertical-align(sub/sup只变小不抬升)与position/top;含图片的段落是「文字段 + 图片」的换行横排,图片旁的长文字在自己的盒子里换行、不绕排;非 scoped 的页面样式经 page-styles.wxss 会作用到所有页的 rich-text 内部节点(scoped 的带 data-v class,只命中来源页);<img>不给宽高时 load 后按原图宽度、不超过容器。 - rich-text(webview,原生):默认样式(标题字号、段落边距、列表缩进)来自原生 / 浏览器 UA,而不是
rich-text/defaults.ts,数值接近但不保证一致;页面<style scoped>规则命不中内部节点(原生节点不带 data-v class),要样式化内部节点请用非 scoped 样式;白名单外标签(script / iframe 等)被原生静默丢弃、没有控制台告警(加告警需要在 wx 侧解析 HTML,与「webview 不打包管线」冲突,spec 050 Q2)。 - picker-view:上下渐隐用原生遮罩(不是 web 的 mask-image);v-for 内的 picker-view 选项列表整体替换后不会重交 value。
- hello-fjs 示例页的开放情况:组件页开放 rich-text、picker-view、form、position(spec 048),仍排除 canvas、web-view、refresh;示例页开放 percent-spacing、pseudo、responsive、transition、page-settled、drag、dnd、2048;排除 echarts / f2 / shooter / three-gltf / gltf-viewer / webgl / webgl-instanced(npm 渲染库或 WebGL)、motion / anime(依赖 @vueuse/motion、animejs)、theme(Flutter 管线压测:styleEngine / op sink)、gomoku / tetris(canvas 桥);animation 组(anime / motion)自 spec 061 起开放。
- v-motion / @vueuse/motion(specs/061):小程序端没有元素可写,指令的
el.style[key] = v落不下去。编译器把v-motion元素的:initial/:enter/:variants(外加:delay/:duration)收进 setup 里的一次motion()/motionEach()调用,运行时交给useMotion一个DOM 形状的替身(一个带 reactivestyle的普通对象——motion 只碰el.style[key]与el.style.transform),再把替身的 style 串回绑到元素上(style="{{ __m0 }}",v-for 里是__m0[index]);元素自己的:style/ 静态style仍在前面,motion 写在后面生效。@vueuse/motion不进运行时包:useMotion由编译出的页面模块 import 后传进去。元素上的:ref拿到的就是这个替身,el.motionInstance.apply('right')照常可用。帧循环与 App 端同路:没有window,framesync 退到 16.7ms 的 setTimeout。 不支持:hovered/tapped/focused(要 DOM 事件)、visible/visibleOnce(要 IntersectionObserver)、leave(要 vdom 卸载钩子)——编译期告警后丢弃;嵌套 v-for 里的v-motion同样丢弃并告警。:key变化在另外两端是重挂载重播入场,这里由运行时重建实例等效实现(key 表达式编译进motionEach)。 - setData 的时机与内容(specs/062,来龙去脉见 performance.md 的「一页 Anime.js 为什么卡」):同一批响应式写入合并成一次 setData,排在一个微任务上(
nextTick()仍在它之后)——@vue/reactivity自己没有任务队列(队列在 runtime-core,这个目标不装),没有 scheduler 的watch是同步的,一帧写 25 个对象 × 3 个属性会过桥 75 次。另外v-for的列表按模板真正读到的字段投影后再下发(__fjsProject):wx:for="{{ dots }}"只为走一遍列表,模板读dot.id,那么scale每帧在变也不会让整个数组重发;整项被读({{ chip }}、dot[key]、事件data-args)、wx:key="*this"、嵌套 v-for 的内层列表、运行时才知道是否为数字的列表都原样放过。__fjsData因此就是「模板读到的名字」集合。 - fetch:
@ufjs/runtime/wx安装基于wx.request的 polyfill,文本/ JSON 响应可用;流式与 blob 不可用。 - toast / invokeHostAsync:
fjs模块在 wx 端的toast走wx.showToast。 - Worker(specs/049):
new Worker('/workers/x.js')→wx.createWorker('workers/x.js')。构建把src/workers/*打包后写到miniprogram/workers/,外包一层适配:函数内声明局部onmessage、postMessage(string)转worker.postMessage({ d }),worker 里抛出的错以{ e }回到页面onerror;app.json 加"workers": "workers"。 wx 同时只允许一个 worker:建新的会先终止旧的并console.warn一次。worker 里没有wxAPI。 - 路由:push/replace/back 映射
wx.navigateTo/redirectTo/navigateBack;栈深受小程序 10 层限制。
| 文件 | 职责 |
|---|---|
packages/fjs/src/mp/wxml.ts |
模板 AST → WXML + 生成代码提取 |
packages/fjs/src/mp/script.ts |
compileScript 产物包装与注入 |
packages/fjs/src/mp/css.ts |
WXSS + scoped class 改写 |
packages/fjs/src/mp/project.ts |
app.json / project.config 等工程文件 |
packages/fjs/src/mp/build.ts |
--mp 编排(编译闭包、.ts 发射、模块组件) |
packages/fjs-runtime/src/wx/ |
vue shim、instance(setData diff)、events、router、fetch、worker |
packages/fjs/src/project/workers.ts |
src/workers 扫描、打包、wx worker 适配(三端构建共用) |
{ "fjs": { "mp": { // 不进 app.json 的页面(路径或路径片段) "exclude": ["example/", "comp/canvas"], // 应用壳(每页包一层 NavBar/TabBar),默认 src/Shell.vue "shell": "@/Shell.vue", // project.config.json 的 appid 兜底(优先级低于 app.config.ts 的 wxmp.appid) "appid": "wx1234567890" } } }