普通视图
特斯拉:第三代特斯拉人形机器人即将亮相,预计年产百万台
半日主力资金加仓电力设备股,抛售电子股
Vue-从内置指令到自定义指令实战
前言
在 Vue 的开发世界里,“指令(Directives)”是连接模板与底层 DOM 的桥梁。除了官方提供的强大内置指令外,Vue 还允许我们根据业务需求自定义指令。本文将带你一次性梳理 Vue 指令体系,并手把手实现一个高频实用的“一键复制”指令。
一、 Vue 内置指令全家桶
在深入自定义指令之前,我们先复习一下这些每天都在用的“老朋友”。内置指令以 v- 开头,是 Vue 预设的特殊属性。
| 指令 | 作用描述 | 核心要点 | |
|---|---|---|---|
v-bind |
响应式地更新 HTML 属性 | 简写为 :,如 :src、:class
|
|
v-on |
绑定事件监听器 | 简写为 @,如 @click
|
|
v-model |
在表单及组件上创建双向绑定 | 它是 v-bind 与 v-on 的语法糖 |
|
v-if / v-else |
根据条件渲染/销毁元素 | 真正的条件渲染(销毁与重建) | |
v-show |
根据条件切换元素的显示 | 基于 CSS 的 display: none 切换 |
|
v-for |
基于源数据多次渲染元素 | 建议必须绑定唯一的 :key
|
|
v-html |
更新元素的 innerHTML
|
注意:易导致 XSS 攻击,慎用 | |
v-once |
只渲染元素和组件一次 | 随后的重新渲染将跳过该部分,用于优化性能 |
二、 自定义指令:像 v-model 一样强大
1. 核心概念
自定义指令主要用于提高代码复用性。当你发现自己在多个组件中都在操作同一个 DOM 逻辑时,就该考虑将其封装为指令了。
2. 生命周期(钩子函数)
Vue 3 重构了指令钩子,使其与组件生命周期完美对齐:
| Vue 3 钩子 | Vue 2 对应 | 执行时机 |
|---|---|---|
beforeMount |
bind |
指令第一次绑定到元素时调用 |
mounted |
inserted |
绑定元素插入父节点时调用 |
beforeUpdate |
update |
元素所在组件 VNode 更新前 |
updated |
componentUpdated |
组件及子组件全部更新后调用 |
unmounted |
unbind |
指令与元素解绑且元素已卸载 |
3. 钩子函数参数
指令对象的钩子函数中都带有如下参数:
-
el: 绑定的真实 DOM。 -
binding: 对象,包含-
name:指令名,不包括v-前缀。 -
value:指令的绑定值,例如:v-my-directive="1 + 1"中,绑定值为2。 -
oldValue:指令绑定的前一个值,仅在 ``update/beforeUpdate和componentUpdated/updated` 钩子中可用。无论值是否改变都可用。 -
expression:字符串形式的指令表达式。例如v-my-directive="1 + 1"中,表达式为"1 + 1"。 -
arg:传给指令的参数,可选。例如v-my-directive:foo中,参数为"foo"。 -
modifiers:一个包含修饰符的对象。例如:v-my-directive.foo.bar中,修饰符对象为{ foo: true, bar: true }
-
-
vnode:Vue编译生成的虚拟节点 -
oldVnode:上一个虚拟节点,仅在update/beforeUpdate和componentUpdated/updated钩子中可用
三、 实战:实现“一键复制”指令 v-copy
1. 指令逻辑实现 (/libs/directives/copy.ts)
import { Directive, DirectiveBinding } from 'vue';
export const copyDirective: Directive = {
mounted(el: HTMLElement, binding: DirectiveBinding) {
el.style.cursor = 'copy';
// 绑定点击事件
el.addEventListener('click', () => {
const textToCopy = binding.value;
if (!textToCopy) {
console.warn('v-copy: 无复制内容');
return;
}
// 现代浏览器 API
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(String(textToCopy))
.then(() => alert('复制成功!'))
.catch(() => alert('复制失败'));
} else {
// 兼容降级方案
const textarea = document.createElement('textarea');
textarea.value = String(textToCopy);
textarea.style.position = 'fixed';
textarea.style.left = '-9999px';
document.body.appendChild(textarea);
textarea.select();
try {
document.execCommand('copy');
alert('复制成功!');
} catch (err) {
console.error('复制失败', err);
}
document.body.removeChild(textarea);
}
});
}
};
2. 全局注册与使用
注册 (main.ts):
import { createApp } from 'vue';
import App from './App.vue';
import { copyDirective } from './libs/directives/copy';
const app = createApp(App);
app.directive('copy', copyDirective); // 全局注册
app.mount('#app');
使用:
<template>
<button v-copy="'这是要复制的内容'">点击复制</button>
</template>
四、 总结
-
内置指令覆盖了 90% 的开发场景,应熟练掌握其简写与区别(如
v-ifvsv-show)。 -
自定义指令是操作 DOM 的最后防线,通过
mounted和updated钩子可以实现极其灵活的逻辑。 -
注意规范:在 Vue 3 + TS 环境下,务必为指令和参数标记类型,以确保代码的健壮性。
现货白银价格小幅反弹后日内第二次下跌超10%
恒指午间休盘跌2.4%,恒生科技指数跌3.68%
九部门:持续完善银行卡、移动支付、现金服务等支付环境,便利春节期间入境游客支付
国家人工智能产业投资基金入股新芯航途
微信通话时,是如何判断“当前/对方网络不佳”的?以及我们自己怎么实现?
前阵子跟客户微信语音聊需求,说着说着突然没声了,屏幕立马弹出“对方网络不佳”的提示,或者自己这边提示"当前网络不佳",反复切WiFi、开流量都没用,最后只能换电话沟通。其实这件事我想了很久了,还是打算今天拿来好好唠唠,顺便也给自己涨涨姿势,看看到底是神不可及的技术!!还是最最最简单的网络延迟方法。
为什么需要“网络不佳”提示
在微信通话这种实时音视频场景里,用户对流畅有非常低的容忍度,一旦出现断续的声音、口型不同步、画面卡顿或通话直接掉线,用户就会迅速认为服务不可靠并中断通话或投诉。因此在界面上及时、准确地提示“当前/对方网络不佳”不仅是对用户体验的尊重,也是减少误判、引导用户采取补救措施(切换到语音、关视频、切换网络或靠近路由器)的关键。具体场景包括:地铁或电梯等移动过程中发生的小区切换导致丢包与抖动;多人群聊或屏幕共享时上行带宽被耗尽导致画面质量急剧下降等,自适应码流和重传策略提供触发条件,并提升用户对恢复机制的信任感——这些都是设计“网络不佳”提示的直接动因。
![]()
微信是如何做到的?(猜测)
从技术上看,“网络好不好”并不是一个主观判断,而是一组持续可观测、可量化的网络与媒体质量信号。在实时音视频(RTC)系统中,最基础的一层是网络层指标:丢包率(Packet Loss)反映数据在传输路径上的可靠性;抖动(Jitter)描述包到达时间的不稳定性,直接决定是否需要更大的播放缓冲;RTT(Round-Trip Time)则刻画端到端时延和链路拥塞程度。在其之上是媒体层指标:码率(Bitrate)是否能稳定达到目标值、帧率(FPS)是否持续下降、关键帧是否频繁请求;再往上是体验层的综合指标,如 MOS(Mean Opinion Score) ,通过对丢包、时延、抖动、音频 PLC 触发次数、视频卡顿时长等信号加权估算“用户主观感受”。这些指标的共同点在于:它们都来自客户端和传输层的实时统计
在微信以及主流 RTC 平台(WebRTC、Agora、Zoom、腾讯云 TRTC 等)的实现中,通常不会依赖单一指标来下结论,而是采用多信号融合 + 时间窗口判断的方式。典型做法包括:在信令层和媒体层同时采集统计数据(冗余信令),避免单一路径或单一模块失效;通过 上/下行探测包(Probe Packet) 或带宽估计算法(如基于延迟梯度、丢包反馈的 BWE)持续判断链路可用带宽;在弱网或移动场景下启用 多通路/备份链路(如 Wi-Fi + 蜂窝网络的快速切换或并行探测);在播放端使用 自适应缓冲区(Adaptive Jitter Buffer) ,根据抖动动态调整缓冲深度,以在“低延迟”和“不卡顿”之间取平衡。一旦检测到多个关键指标在一定时间窗口内持续恶化(例如丢包率超过阈值、RTT 快速上升、码率被迫下探),系统就会触发体验等级下降,并映射为“当前/对方网络不佳”的用户提示。
这种思路在公开资料中也有佐证。WebRTC 官方文档和 RFC 中详细描述了基于 RTCP 统计的带宽估计与拥塞控制模型;腾讯、字节、阿里等厂商在公开专利中多次提到 多维网络质量评估、弱网对抗与体验分级提示机制;学术与工业界关于 MOS 预测的技术文献也表明,将底层网络指标映射为用户可理解的体验标签,是大规模 RTC 系统的通用做法。
如何决策
在产品层面,“网络不佳”不是技术结论展示,而是不干扰用户体验,核心目标只有一个:在不打扰用户的前提下,帮他理解当前通话异常的原因。因此微信这类产品在设计上通常遵循以下取舍。
网络指标是实时波动的,但提示不能实时波动。
实际策略通常是:时间窗口 + 连续恶化判定,例如在 2~5 秒内持续丢包升高、RTT 上扬、码率被迫下探,才认为是“稳定性问题”,否则只是短暂抖动,直接忽略。
这也是为什么你在地铁刚进隧道那一瞬间,微信往往不会立刻弹“网络不佳”。
当然了哈~~ 也不排除微信确实没及时检测到,哈哈哈
技术方案
如果把“网络不佳”当成一个完整的技术功能来看,它并不是某个 if 判断,而是一条很清晰的过程:数据采集 → 指标聚合 → 质量评分 → 防抖与阈值 → 展示或策略处理。
一、数据采集(Data Collection)
第一步解决的不是判断,而是你到底能看到什么。在 RTC 客户端里,采集通常来自三层:
- 网络层:RTT、丢包率、抖动、发送/接收速率、重传次数
- 传输/协议层:RTCP 统计、NACK/PLI/FIR 次数、拥塞窗口变化
- 媒体层:编码码率、实际渲染帧率、卡顿时长、音频 PLC 触发次数
注意:这些数据不是按事件上报,而是以固定周期(如 200ms / 500ms / 1s)持续采样,形成时间序列。
二、指标聚合(Aggregation)
原始指标是噪声极大的,不能直接用。现实情况下我们系统一定要收集:
- 滑动时间窗(如最近 3s / 5s)
- 计算均值、P95、变化斜率
- 标记异常峰值(Spike)而不是立刻判坏
举个栗子:
一次 200ms 的 RTT 飙升,可能是 GC、系统调度或基站抖动;
但 RTT 连续 5 秒单调上升 + 丢包同步增加,才是链路拥塞的信号。
其实这个操作就是把瞬时的网络状态,转换成一个网络趋势,方便判断是否要提示用户!
三、质量评分(Quality Scoring)
接下来不是直接出网络好/网络坏,而是要有体验层映射。常见方式如下:
-
规则加权:
score = w1*丢包 + w2*RTT + w3*卡顿 + w4*帧率下降 -
分档映射:
优 / 良 / 可接受 / 差(对应 MOS 区间)
四、提示以及处理
这里的提示我们必须做防抖,不能反复频繁提示用户!
进入阈值:评分连续低于 X,持续 ≥ T 秒 退出阈值:评分连续高于 Y(Y > X),持续 ≥ T′ 秒 状态锁定:同一状态不重复触发提示
然后就是处理了, UI 层:展示「当前 / 对方网络不佳」。 要做的处理:
- 自动降码率 / 降分辨率
- 关闭视频保音频
- 切备用链路 / 重连
- 统计层:上报埋点,用于后续策略优化
也就是说, “网络不佳”往往是系统已经做了很多努力之后的结果告知 ,而不是直接哇啦哇啦告诉用户,你踏马网废了。
整体流程示意
flowchart LR
A[原始数据采集<br/>RTT / 丢包 / 帧率] --> B[时间窗口聚合<br/>均值 / 趋势]
B --> C[质量评分<br/>MOS / 等级]
C --> D[防抖 & 阈值判断<br/>状态机]
D --> E[UI 提示<br/>网络不佳]
D --> F[自适应策略<br/>降码率/切链路]
我们如何实现呢?(ReactNative)
前文拆解的这套网络检测逻辑,并非微信独有的技术壁垒,在工程实践中,我们完全可以自己完成一套方案。下面直接用React Native结合WebRTC的实操举例,别眨眼,我要写代码了。(可以眨眼)
技术选型与依赖
在RN项目中,基于WebRTC做数据采集是最稳妥的选择,第一步先安装核心依赖:
yarn add react-native-webrtc
这个库自带的getStats方法,是网络质量判断的核心入口,里面包含了所有关键数据维度:
- RTT(往返延迟)
- packetsLost / packetsSent(丢包数/发送数)
- jitter(抖动)
- bitrate(码率,通过bytesSent差分计算得出)
- frameRate(帧率,部分平台支持)
这里要明确一个核心认知:无需刻意计算网络状态,重点是精准读取传输过程中的原生统计数据。
![]()
数据采集(定时 + 时间序列)
const statsBuffer: StatSample[] = [];
setInterval(async () => {
const stats = await pc.getStats();
const parsed = parseStats(stats);
statsBuffer.push({
rtt: parsed.rtt,
packetLoss: parsed.packetLoss,
jitter: parsed.jitter,
bitrate: parsed.bitrate,
ts: Date.now(),
});
// 只保留最近5秒的数据
prune(statsBuffer, 5000);
}, 1000);
这里有两个至关重要的细节:切勿依赖单次数据快照,必须保留时间维度的连续数据。缺少这两点,后续的防抖处理和趋势判断都会沦为空谈。
指标聚合 + 质量评分(可解释优先)
function calcQuality(samples: StatSample[]) {
const avgLoss = mean(samples.map(s => s.packetLoss));
const avgRtt = mean(samples.map(s => s.rtt));
const avgJitter = mean(samples.map(s => s.jitter));
let score = 100;
if (avgLoss > 0.05) score -= 30;
if (avgRtt > 300) score -= 30;
if (avgJitter > 50) score -= 20;
return score;
}
这种规则加权的评分方式,在真实工程场景中应用极广。核心原因很简单:可调优、可回滚、可追溯,出现问题时能快速定位到具体异常指标。
阈值 + 防抖(用状态机思路,别堆if判断)
let badSince: number | null = null;
let state: 'GOOD' | 'BAD' = 'GOOD';
function updateState(score: number) {
const now = Date.now();
if (score < 60) {
if (!badSince) badSince = now;
if (now - badSince > 3000 && state !== 'BAD') {
state = 'BAD';
showNetworkBad();
}
} else {
badSince = null;
if (state === 'BAD' && score > 75) {
state = 'GOOD';
hideNetworkBad();
}
}
}
这段逻辑的核心要点很明确:评分低于60分时触发预警判定,持续3秒无改善才切换至异常状态;恢复时需评分超过75分才回切正常状态。这一步的设计直接决定提示功能的专业性,有效避免频繁误报影响用户体验。
举例方便所以使用打分制,也可以其他的
然后UI展示轻提示
{state === 'BAD' && (
<View style={styles.badNetwork}>
<Text>醒醒!!你踏马网废了</Text>
</View>
)}
采用轻量提示设计,不弹窗、不弹出 Toast、不抢占用户操作焦点,仅安静告知用户:当前网络存在异常,非设备故障或操作问题。
![]()
自动降级处理,这点很重要
在真实项目中,网络异常提示绝非仅展示一句文案,更重要的是触发对应的自适应应对策略:
例如当异常状态持续5秒:
- 自动降低视频码率
- 下调视频分辨率或帧率
当异常状态持续10秒:
- 提示用户关闭视频,优先保障音频通话通畅
当状态恢复正常时:
- 缓慢提升码率,避免一次性拉满导致再次卡顿
这里有个核心工程原则务必记牢:恢复要慢,降级要快。
总结
从技术角度来看,判断通话网络好坏,其实就是三件事:持续采集指标、观察趋势、连续判定。瞬时波动不算数,只有连续多秒丢包、抖动高、延迟大,才真正算网络不佳。再配合降码率、先保音频、延迟提示的策略,就能在用户几乎感觉不到的情况下保证体验。核心逻辑很朴素,但工程上最难的是防抖、聚合和兜底
交易员们称印度央行通过海外渠道抛售美元以支撑卢比
Vue-深度解析“组件”与“插件”的区别与底层实现
前言
在 Vue 的生态系统中,“组件(Component)”和“插件(Plugin)”是构建应用的两大基石。虽然它们都承载着逻辑复用的使命,但在设计模式、注册方式和职责边界上却截然不同。本文将带你从底层原理出发,理清二者的核心差异。
一、 核心概念对比
1. 组件 (Component)
组件是 Vue 应用的最小构建单元,通常是一个 .vue 后缀的文件。
- 本质:可复用的 UI 实例。
- 职责:封装 HTML 结构、CSS 样式和 TS 交互逻辑。
2. 插件 (Plugin)
插件是用于扩展 Vue 全局功能的工具库。
-
本质:一个包含
install方法的对象或函数。 -
职责:为 Vue 添加全局方法、全局指令、全局组件或注入全局属性(如
vue-router、pinia)。
二、 关键区别总结
| 特性 | 组件 (Component) | 插件 (Plugin) |
|---|---|---|
| 功能范围 | 局部的 UI 渲染与交互 | 全局的功能扩展 |
| 代码形式 |
.vue 文件(SFC)或渲染函数 |
暴露 install 方法的 JS/TS 对象 |
| 注册方式 |
app.component() 或局部引入 |
app.use() |
| 使用场景 | 按钮、弹窗、列表等 UI 单元 | 路由管理、状态管理、全局水印指令等 |
三、 编写形式
1. 编写一个组件
组件的编写我们非常熟悉,通常使用 DefineComponent 或 <script setup>。
<template>
<button class="my-btn"><slot /></button>
</template>
<script setup lang="ts">
// 组件内部逻辑
</script>
2. 编写一个插件 (Vue 3 写法)
在 Vue 3 中,插件的 install 方法第一个参数变为 app (应用实例) ,而不再是 Vue 构造函数。
// myPlugin.ts
import type { App, Plugin } from 'vue';
export const MyPlugin: Plugin = {
install(app: App, options: any) {
// 1. 添加全局方法或属性 (通过 config.globalProperties)
app.config.globalProperties.$myGlobalMethod = () => {
console.log('执行全局方法');
};
// 2. 注册全局指令
app.directive('my-highlight', {
mounted(el: HTMLElement, binding) {
el.style.backgroundColor = binding.value || 'yellow';
}
});
// 3. 全局混入 (慎用)
app.mixin({
created() {
// console.log('插件注入的生命周期');
}
});
// 4. 注册全局组件
// app.component('GlobalComp', MyComponent);
// 5. 提供全局数据 (Provide / Inject)
app.provide('plugin-config', options);
}
};
四、 注册方式的演进
1. 组件注册
-
全局注册:
app.component('MyBtn', MyButton) -
局部注册:在父组件中直接
import导入。
2. 插件注册
在 Vue 3 中,使用应用实例的 use 方法。
// main.ts
import { createApp } from 'vue';
import App from './App.vue';
import { MyPlugin } from './plugins/myPlugin';
const app = createApp(App);
// 安装插件,可以传入可选配置
app.use(MyPlugin, {
debug: true
});
app.mount('#app');
五、 总结与注意事项
-
Vue 3 的变化:Vue 3 移除了
Vue.prototype,改为使用app.config.globalProperties来挂载全局方法。 -
职责分离:如果你的代码是为了在页面上显示一段内容,请写成组件;如果你是为了给所有的组件提供某种“超能力”(如统一处理错误、多语言支持),请写成插件。
-
插件的 install 机制:
app.use内部会自动调用插件的install方法。如果插件本身就是一个函数,它也会被直接当做install函数执行。
九部门:鼓励金融机构与重点商户合作策划春节专属活动,推出消费红包、消费立减等优惠
Rspress 2.0 发布:面向体验与 AI 的全新升级
![]()
本文作者为 Rstack 团队 - SoonIter
我们很高兴地宣布 Rspress 2.0 的正式发布!
Rspress 是基于 Rsbuild 的静态站点生成器,专为开发者打造的文档站工具。自 2023 年正式发布以来,Rspress 1.x 累计迭代 144 个版本,共有 125 位贡献者 参与项目开发。越来越多的开发者选择 Rspress,借助其高效的编译性能、约定式路由和组件库预览等功能,搭建美观可靠的文档站点。
基于社区的反馈和建议,Rspress 2.0 在 主题美观度、AI-native、文档开发体验、与 Rslib 一起使用 等方面更进一步。
为什么是 Rspress 2.0
Rspress 1.x 已经解决了文档站框架编译性能的问题,但仍存在一些问题影响着作为一个文档开发工具的核心体验。2.0 版本将不止于对编译性能的追求,也聚焦于文档站体验的其他方面:
-
主题样式:一套更美观的默认主题,并提供了多种 自定义主题 方式,解决了 1.x 在主题定制上缺乏稳定 API 的问题。
-
AI-native:文档不仅服务于人类读者,也需要被 Agent 更好地理解和使用。Rspress 现在内置了 llms.txt 生成和从 SSG 衍生出的 SSG-MD 功能,生成高质量的 Markdown 渲染内容供 Agent 读取。
-
按需编译,瞬间启动:默认启用 lazyCompilation,配合链接 hover 时对资源的 preload 功能,仅在访问特定路由时构建所需文件,实现无论项目规模多大,dev 也可瞬间启动。
-
Shiki 代码高亮:默认集成 Shiki,在构建时完成语法高亮,支持主题切换、transformer 扩展,比如 @rspress/plugin-twoslash,带来更丰富的代码块展示效果。
-
文档开发体验:优化
_nav.json、_meta.json等文件的 HMR 并新增 json schema 用于 IDE 内的代码提示;默认开启死链检查功能;新增文件代码块语法,支持引用外部文件;@rspress/plugin-preview 和 @rspress/plugin-playground 支持同时使用等。 -
Rslib 集成:现在可以在使用
create-rslib创建组件库项目时,选择 Rspress 作为文档工具,快速搭建组件文档站点。
这是一次对现有架构的全面升级,下面将介绍 Rspress 2.0 和它的 全新主题、高质量 llms.txt 生成、集成 Shiki、按需编译等重要功能。
![]()
2.0 新特性
全新主题
2.0 默认主题迎来了一次系统性升级,它由团队设计师 @Zovn Wei 整体设计,在视觉效果和阅读体验上都有较大幅度提升,并且每个组件均可独立替换,拥有很高的可定制性。
![]()
主题定制
按照定制化程度从低到高,有 CSS 变量、BEM 类名、ESM 重导出覆盖、组件 eject 四种自定义主题方式。
- CSS 变量:新主题暴露了更多 CSS 变量,覆盖主题色、代码块、首页等样式。你可以在 CSS 变量 页面交互式地预览和调整所有 CSS 变量,找到满意的配置后直接复制到项目中使用。
:root {
/* 自定义主题色 */
--rp-c-brand: #3451b2;
--rp-c-brand-dark: #2e4599;
/* 自定义代码块样式 */
--rp-code-block-bg: #1e1e1e;
}
- BEM 类名:内置组件现在均采用 BEM 命名规范。这是一个十分 Old School 的选择,但也是我们深思熟虑的决定。用户可以通过 CSS 选择器精准调整样式,HTML 结构更加清晰;同时与 Rspress 用户自身使用的 CSS 框架解耦,用户可以任意选择 CSS 框架(Tailwind、Less、Sass 等),比如使用 Tailwind V4 或 V3 而不用担心版本,也不用担心与 Rspress 内置 CSS 产生冲突。
/* BEM 命名规范 */
.rp-[component-name]__[element-name]--[modifier-name] {
}
/* 根据 BEM 类名轻松覆盖组件样式 */
.rp-nav__title {
height: 32px;
}
.rp-nav-menu__item--active {
color: purple;
}
-
ESM 重导出覆盖:如果 CSS 上的修改无法满足定制需求,可以通过 JS 进行更深度的定制。在
theme/index.tsx中利用 ESM 重导出,可以覆盖任意一个 Rspress 的内置组件。
import { Layout as BasicLayout } from '@rspress/core/theme-original';
const Layout = () => <BasicLayout beforeNavTitle={<div>some content</div>} />;
export { Layout }; //[!code highlight]
export * from '@rspress/core/theme-original'; //[!code highlight]
-
组件 eject:你可以使用全新的
rspress eject [component]命令,这个命令会将指定组件的源代码复制到theme/components/目录下,你可以自由修改这些代码,甚至直接交给 AI 修改,来实现深度定制。
# 将 DocFooter 组件导出到 theme 目录
rspress eject DocFooter
导航栏、侧边栏 tag
Rspress 2.0 实现了 Tag 组件,现在可以使用 frontmatter 中的 tag 属性,在侧边栏或导航栏进行 UI 标注。
---
tag: new, experimental # 会在 H1 和 Sidebar 进行显示
---
import { Tag } from '@rspress/core/theme';
# Tag
## Common tags <Tag tag="new" /> {/* 会在右侧 outline 进行显示 */}
内置多语言支持
在 1.x 版本中,Rspress 仅内置了英文文本,如果使用其他语言例如 zh,必须对所有的文本都进行配置,使用起来较为繁琐。现在 2.0 主题内置了 zh、en、ja、ko、ru 等多种语言的翻译文本,系统会根据语言配置自动进行 "Tree Shaking",仅打包你使用到的文本及语言,未内置的语言会兜底到 en 文本。你也可以通过 i18nSource 配置项扩展或覆盖翻译文本。
Rspress 未来会支持更多内置语言,如果你有兴趣,请参考 这位贡献者的 Pull Request。
llms.txt 支持
Rspress 现在将 llms.txt 生成能力集成到 core 中,并实现了全新的 SSG-MD(Static Site Generation to Markdown,静态站点 Markdown 生成)能力。
在基于 React 动态渲染的前端框架中,往往存在静态信息难以提取的问题,Rspress 也面临同样的挑战。Rspress 允许用户通过 MDX 片段、React 组件、Hooks 以及 TSX 路由等动态特性来增强文档表现力。但这些动态内容在转换为 Markdown 文本时会面临以下问题:
- 直接将 MDX 输入给 AI 会包含大量代码语法噪音,并丢失 React 组件内容
- 将 HTML 转为 Markdown 往往效果不佳,信息质量难以保证
为了解决这个问题,Rspress 2.0 引入了 SSG-MD 特性。这是一个全新的功能,它类似于 静态站点生成(SSG),但不同之处在于它将你的页面渲染为 Markdown 文件,而非 HTML 文件,并生成 llms.txt 及 llms-full.txt 相关文件。
![]()
相比于将 HTML 转化为 Markdown 等传统方式,SSG-MD 在渲染期间拥有更优质的信息源,比如 React 虚拟 DOM,从而保证更高的静态信息质量和灵活性。
![]()
启用方式非常简单:
import { defineConfig } from '@rspress/core';
export default defineConfig({
llms: true,
});
构建后将生成如下结构:
doc_build
├── llms.txt
├── llms-full.txt
├── guide
│ └── start
│ └── introduction.md
└── ...
若想定制自定义组件的渲染内容,可通过环境变量控制:
export function Tab({ label }: { label: string }) {
if (import.meta.env.SSG_MD) {
// SSG-MD 模式下输出纯文本描述
return <>{`**Tab: ${label}**`}</>;
}
// 正常渲染交互式组件
return <div className="tab">{label}</div>;
}
这样既保证了文档的交互体验,也能帮助 AI 理解组件的语义信息。
详见 SSG-MD 使用指南
Shiki 编译时代码块高亮
Rspress 2.0 默认使用 Shiki 进行代码高亮。相比 1.x 的 prism 运行时高亮方案,Shiki 在编译时完成高亮处理。
- 支持多种主题样式,比如在 CSS 变量 页面可以交互式地切换和预览不同的 Shiki 主题。
- 同时 Shiki 也允许使用自定义的 transformer 进行扩展来丰富写作,例如 twoslash 等。
- 按需引入编程语言,不增加运行时开销和包体积。
- 基于 TextMate 语法实现与 VS Code 一致的准确语法高亮。
下面是一些 Shiki transformer 的示例,直观感受一下 Shiki 带来的文档创造力:
const hi = 'Hello';
const msg = `${hi}, world`;
// ^?
console.log('Not focused');
console.log('Focused'); // [!code focus]
console.log('Not focused');
详见 代码块
构建性能提升
Rspress 2.0 底层由 Rsbuild 和 Rspack 2.0 预览版本驱动,同时默认开启了 按需编译 和 持久化缓存。
按需编译
默认开启 dev.lazyCompilation,只有当你访问某个页面时,该页面才会被编译,大幅提升了开发启动速度,甚至实现毫秒级的冷启动。Rspress 同时实现了路由的 preload 策略,当鼠标悬停在链接上时会预先加载目标路由页面,搭配 lazyCompilation 实现无损的开发体验。
![]()
持久化缓存
2.0 同时默认开启了 持久化缓存,在热启动中复用上次编译的结果,提升 30%-60% 的构建速度。这意味着在首次运行 rspress dev 或 rspress build 后,后续启动速度都会明显提升。
文档开发体验
默认开启死链检查
Rspress 2.0 默认开启死链检查功能。在构建过程中,会自动检测文档中的无效链接,帮助你及时发现和修复。
import { defineConfig } from '@rspress/core';
export default defineConfig({
markdown: {
link: {
checkDeadLinks: true, // 默认开启,可通过 false 关闭
},
},
});
![]()
详见 链接
文件代码块
你可以使用 file="./path/to/file" 属性来引用外部文件作为代码块的内容,将示例代码放在单独的文件中维护。
```ts file="./_demo.ts"
```
```tsx file="<root>/src/components/Button.tsx"
```
详见 文件代码块
preview 更灵活的 meta 用法
@rspress/plugin-preview 现在基于 meta 属性使用,更加灵活,也可以配合文件代码块。
下面是一个使用 iframe 预览代码块的示例:
```tsx preview="iframe-follow" file="./_demo.ts"
```
它将会渲染为:
![]()
并且 @rspress/plugin-playground 现在支持和 plugin-preview 一起使用,通过 meta 属性切换即可,例如 ```tsx playground
支持若干配置文件的 HMR
基于 Rsbuild 重新设计的 虚拟模块插件,现在支持 i18n.json、_nav.json、_meta.json、文件代码块以及 @rspress/plugin-preview 中 iframe 相关的 HMR。修改这些配置文件后,页面会自动热更新,无需手动刷新。
Rslib & Rspress
在使用 create-rslib 创建项目时,你现在可以选择 Rspress 工具。这让你能够在开发组件库的同时,快速搭建配套的文档站点,用于编写组件的使用说明、展示 API 参考,或实时预览组件效果。
执行 npm create rslib@latest 并选中 Rspress,会生成下方的文件结构:
模版中内置了 rsbuild-plugin-workspace-dev 插件,可在启动 Rspress 开发服务器的同时自动运行 Rslib 的 watch 命令。
直接运行 npm run doc 启动 Rspress 的开发服务器对 Rslib 组件库进行预览:
{
"scripts": {
"dev": "rslib build --watch",
"doc": "rspress dev" // 执行该命令
}
}
更多 Rspress 官方插件
Rspress 2.0 新增了多个官方插件:
- @rspress/plugin-algolia:支持替换 Rspress 的内置搜索为 Algolia DocSearch(感谢 @algolia 团队的协助)。
- @rspress/plugin-twoslash:为 TypeScript 代码块添加类型提示。
- @rspress/plugin-llms:为不支持 SSG 和 SSG-MD 的项目提供 llms.txt 生成能力。
- @rspress/plugin-sitemap:自动生成 Sitemap 文件,用于优化 SEO。
其他 Breaking changes
从 Rspress 1.x 迁移
如果你是 1.x 项目的用户,我们准备了一份详尽的迁移文档,帮助你从 1.x 升级到 2.0。
你可以直接使用页面中的 "复制 Markdown" 功能,将其输入给你常用的编码 agent(如 Claude Code 等)来完成迁移。
请参考 迁移指南。
移除 mdxRs 配置
我们注意到很大一部分 1.x 用户为了使用 Shiki、组件库预览功能和自定义 remark/rehype 插件,而主动关闭 mdxRs,并且在开启按需编译和持久化缓存后,即使使用 JS 版本的 mdx 解析器,性能优化效果已经非常显著。
为了换取更好的扩展性和维护性,我们决定在 Markdown/MDX 编译流程中不再使用 Rust 版本的 MDX 解析器(@rspress/mdx-rs)。这使得 Rspress 能够更好地集成 Shiki 等 JavaScript 生态的工具。
Node.js 与上游依赖版本要求
Rspress 2.0 要求 Node.js 版本 20+,React 版本 18+。
| 依赖 | 允许范围 | 默认版本 | 说明 |
|---|---|---|---|
react |
^18.0.0 || ^19.0.0 |
19 | 不再支持 React 17,如项目已安装则使用项目版本 |
react-dom |
^18.0.0 || ^19.0.0 |
19 | 与 react 版本保持一致 |
react-router-dom |
^6.0.0 || ^7.0.0 |
7 | 如项目已安装则使用项目版本 |
unified |
^11.0.0 |
11 | 自定义 remark/rehype 插件需兼容 |
包名及导入路径变更
Rspress 将 rspress、@rspress/runtime、@rspress/shared、@rspress/theme-default 都整合进了 @rspress/core 中,项目和插件现在均只需安装一个 @rspress/core 包即可。
{
"dependencies": {
- "rspress": "1.x"
- "@rspress/shared": "1.x"
+ "@rspress/core": "^2.0.0"
}
}
- import { defineConfig } from 'rspress/config';
+ import { defineConfig } from '@rspress/core';
- import { useDark } from 'rspress/runtime'
- import { PackageManagerTabs } from 'rspress/theme';
+ import { useDark } from '@rspress/core/runtime'
+ import { PackageManagerTabs } from '@rspress/core/theme';
如果你开发了 Rspress 插件,请将插件的 peerDependencies 从 rspress 变更为 @rspress/core:
{
"peerDependencies": {
"@rspress/core": "^2.0.0"
}
}
下一步
Rspress 2.0 的发布只是一个新的起点。本次发布后,Rspress 将持续迭代:
- 推进生态集成:与 Rslib、Rstest 更深度地结合,提供前端项目和组件库项目的一体化开发体验。
- 探索 AI 与文档更深度集成:如智能问答、自动摘要等;完善 SSG-MD 使其稳定并更加易用。
感谢所有为 Rspress 做出贡献的开发者和用户!如果你在使用过程中遇到问题或有任何建议,欢迎在 GitHub Issues 中反馈。
立即使用或升级到 Rspress 2.0,体验全新的文档开发之旅!
npm create rspress@latest
A股三大指数午间休盘集体下跌,资源股走弱
一次 Agent Skill 的实战体验,以及 MCP 和 Skill 的区别
本周通过一个小需求尝试了下 Agent Skill,效果还不错。
比如你要做一个网站,以前没装技能的时候,AI 生成的代码又是那个熟悉的:
蓝紫渐变色 + 千篇一律的布局 + 明显的 AI 审美(不同的模型,产生的结果不同)
而通过 Agent Skill 的形式,可以提前配置好:
- 配色体系
- 字体
- 布局风格
当然,rules 和 prompt 也能做到这一点。
但 Agent Skill 的优势在于:把 Prompt 打包成一个文件夹,让 AI 按需读取和使用。
虽然本质上没啥区别,都是 prompt,但 Skill 的形态更工程化、更灵活。
![]()
MCP 和 Skill:经常一起出现,但不是一回事
现在用 AI Agent 工具(Claude Code、Cursor)时,经常会遇到两个概念:
- MCP
- Skill
我觉得有必要区分清楚:两者各有侧重,是互补关系,而不是替代关系。
Anthropic 官方的说法:
MCP connects Claude to external services and data sources.
Skills provide procedural knowledge—instructions for how to complete specific tasks or workflows.
翻成一句话就是:
MCP 让 AI 能拿到数据,Skill 教 AI 怎么处理数据。
MCP 在做什么?
MCP 的三个核心组成:
- Tools(工具)
- Resources(资源)
- Prompts(提示)
LLM(大语言模型)本身并不执行函数,在 Agent 架构中,通常是由规划层(Planner / System Prompt) 决定“要做什么”。
Function Calling 负责在推理过程中,表达模型想要调用某个工具的意图。MCP 构建在 Function Calling 之上,进一步规范工具的描述方式、发现机制与调用协议。可以理解为:
- 规划层:决定做什么
- Function Calling:表达要调用哪个工具
- MCP:规范这个工具从哪里来、如何被发现、如何被调用
MCP 更关注的是:AI 与外部世界的连接能力。
同时需要注意:
MCP 本身并不提供推理能力,
它解决的是连接与通信的标准化问题。
是否正确使用工具、如何组合工具,仍然取决于模型能力与上层 Agent 设计。
Skill 在做什么?
Skill 可以以文件夹(Prompt 资产)形式存在,里面包含:
- 指令
- 脚本
- 资源
但 Skill 的价值并不在于“文件夹本身”,而在于:
这些 Prompt 资产能够被 Agent 识别、发现、加载和组合使用。
在架构层级上:Skill 是「提示 / 知识层」、MCP 是「集成层」。
Skill 通常分三层加载:
- 元数据(始终加载)
- 核心指令(按需加载)
- 支持文件(按需加载)
它解决的是:如何把经验、规范、做事方法沉淀下来并复用。
同时需要明确:
从能力本质上看,Skill 并不是新的模型能力,
而是对 Prompt 的工程化封装与组织升级。
提升的是稳定性与可维护性,而不是智能本身的跃迁。
什么时候用 MCP?什么时候用 Skill?
-
用 MCP
- 获取外部数据
- 调接口
- 操作系统、文件、数据库
-
用 Skill
- 内部规范
- 标准化实践经验
- 固定工作方式
- 代码风格 / 设计风格约束
网上也有人提到 Skill 可以用于指定工作流程,这块我还没有深入实践,后面有时间会再尝试。
一个对照式实战示例:同一个需求,不同方式怎么做?
假设现在有一个需求:从接口获取用户数据,并生成一个用户列表页面。
只用 Prompt
你可能会这样写:
请从接口 api.xxx.com/users 获取用户数据,并使用 Vue3 生成一个简洁风格的用户列表页面。
特点:
- 每次都要重复写
- 输出风格不稳定
因此,这种方式更适用于一次性需求。
使用 Skill 固化“页面生成方式”
创建一个 Skill,例如:
/frontend-ui-skill
├── metadata.json
├── instructions.md
├── style-guide.md
instructions.md:
所有页面使用:
- 浅色背景
- 中性色配色
- 卡片式布局
- Vue3 + Composition API
之后你只需要说:
生成用户列表页面
AI 会自动套用该 Skill 的规则。适用于前端规范化输出的场景。
使用 MCP 获取真实数据
通过 MCP 暴露一个工具:
getUsers()
你对 AI 说:
调用 getUsers
AI 会通过 MCP 获取接口数据。
MCP + Skill 组合
流程:
- MCP:调用 getUsers()
- Skill:规定页面结构和风格
- AI:生成页面代码
你只需要说:“生成用户列表页面”,背后完成了:
- 拿数据
- 套规范
- 产出代码
一个更大的共性
不管是 MCP、Prompt 还是 Skill,本质目标都一致:
降低模型幻觉,提高稳定性,提高效率。
但也必须明确:
MCP、Prompt、Skill 都无法从根本上消除模型幻觉,
它们能做的是:降低出错概率、提高一致性、减少不确定性。
因此,完全脱离人工审核的流程化自动生成,在工程上仍然是不可靠的。
它们更合理的定位是:
放大工程师能力的工具,而不是替代工程师。
Vue-实例从 createApp 到真实 DOM 的挂载全历程
前言
无论是 Vue 2 的 new Vue() 还是 Vue 3 的 createApp(),将组件配置转化为页面上可见的真实 DOM,中间经历了一系列复杂的转换。理解这一过程,不仅能帮我们更好地掌握生命周期,更是理解响应式原理的基础。
一、 挂载过程总览
Vue 实例的挂载过程,本质上是将组件配置转化为虚拟 DOM,最终映射为真实 DOM,并建立响应式双向绑定的过程。
二、 核心挂载步骤详解
1. 初始化阶段 (Initialization)
在 Vue 3 中,通过 createApp 开始。
-
创建实例:根据传入的根组件配置创建一个应用上下文(vue实例),接着进行数据初始化。
-
初始化数据:这是最关键的一步。Vue 会依次初始化 Props、Methods、Setup (Vue 3)、Mixins、Data、Computed。
-
校验:Vue 会校验
props和data中的变量名是否重复。 -
响应式绑定:Vue 3 使用
Proxy(Vue 2 使用Object.defineProperty)对数据进行劫持,建立依赖收集机制。
-
校验:Vue 会校验
2. 模板编译阶段 (Template Compile)
这一步将“肉眼可见”的 HTML 模板转化为机器高效执行的 JavaScript 代码。
-
解析 (Parser) :将
template字符串解析为 抽象语法树 (AST) 。 - 转换 (Transformer) :对 AST 进行静态提升、补丁标记(Patch Flags)等优化。
- 生成 (Generator) :将 AST 转换成 render 渲染函数 字符串。
3. 生成虚拟 DOM (VNode)
- Vue 调用生成的
render函数。 -
render函数根据Template执行后会返回一个 虚拟 DOM 树 (Virtual DOM) 。它是对真实 DOM 的一种轻量级 JavaScript 对象描述。
4. 挂载与 Patch (Mounting & Patching)
- 调用 Mount:执行组件的挂载方法。
- 渲染真实 DOM:渲染器(Renderer)遍历虚拟 DOM 树,递归创建真实的 HTML 元素。
-
更新页面:将生成的真实 DOM 插入到指定的容器(如
#app)中,替换掉原本的内容。
5. 完成挂载
- 一旦真实 DOM 渲染完毕,Vue 会触发
mounted(组合式 API 为onMounted)生命周期钩子,此时开发者可以安全地访问 DOM 节点。
三、 Vue 3 挂载示例
在 Vue 3 项目中,挂载通常发生在 main.ts。
// main.ts
import { createApp } from 'vue'
import App from './App.vue'
// 1. 创建应用实例
const app = createApp(App)
// 2. 挂载到指定 DOM 容器
// 挂载过程中会执行编译、数据拦截、生成 VNode 并渲染
app.mount('#app')
四、 总结
-
AST 与 VNode 的区别:
- AST:是对 HTML 语法的描述,用于代码编译阶段。
- VNode:是对 DOM 节点的描述,用于运行时渲染和 Diff 算法。
-
双向绑定的建立时机:在
data初始化阶段,Vue 就已经通过响应式 API 拦截了数据。当render函数读取数据时,会自动触发依赖收集。 -
重新挂载:如果响应式数据发生变化,Vue 不会重新走一遍完整的挂载过程,而是通过 Diff 算法 对比新旧 VNode,仅更新发生变化的真实 DOM 部分。
支付宝集福明日开启:新增“健康福”,将由蚂蚁阿福发放
在 Cloudflare 平台上构建垂直微前端
想象一下,你正在开发一个大型Web应用。营销团队想要用Astro构建他们的页面以获得最佳的SEO效果,而产品团队却坚持要用React来构建功能丰富的后台管理系统。更糟糕的是,每次发布新版本时,十几个团队的代码都需要一起打包、一起测试、一起上线——只要其中一个团队引入了一个bug,整个发布就要回滚。这种"一荣俱荣、一损俱损"的耦合方式,是不是让你感到无比头疼?
或者,你的公司刚刚收购了一个创业公司,他们的产品是用Vue写的,而你们的主站是用React写的。你想把他们的功能整合进来,但又不希望把两个完全不同的代码库强行混在一起。
这些都是现代Web开发中真实存在的难题。传统的微前端架构通常是"水平"的——同一个页面上的不同组件来自不同的服务。但如果有一种方式,能让每个团队完全独立地开发、部署和维护自己的功能模块,而用户却感觉在使用一个无缝的、统一的应用呢?
这就是垂直微前端(Vertical Microfrontends)要解决的问题。现在,Cloudflare推出了一款全新的Worker模板,让这种架构变得前所未有的简单。
什么是垂直微前端?
垂直微前端是一种架构模式,单个独立团队拥有应用程序功能的完整切片,从用户界面一直到底层的CI/CD流水线。这些切片通过域名上的路径来定义,你可以将各个独立的Worker与特定路径关联起来:
/ = 营销网站
/docs = 文档
/blog = 博客
/dash = 仪表盘
我们还可以进一步细化,在更细粒度的子路径上关联不同的Worker。比如在仪表盘中,你可能通过各种功能或产品来划分URL路径的深度(例如 /dash/product-a),在两个产品之间导航可能意味着两个完全不同的代码库。
现在有了垂直微前端,我们还可以这样设计:
/dash/product-a = WorkerA
/dash/product-b = WorkerB
上面的每个路径都是独立的前端项目,它们之间没有任何共享代码。product-a 和 product-b 路由映射到分别部署的前端应用,它们有自己的框架、库、CI/CD流水线,由各自的团队定义和拥有。
你可以端到端地拥有自己的代码。但现在我们需要找到一种方法将这些独立的项目缝合在一起,更重要的是,让它们感觉像是一个统一的体验。
Cloudflare自己也在经历这个痛点,因为仪表盘有许多独立的团队负责各自的产品。团队必须面对一个事实:在他们控制范围之外所做的更改会影响用户对其产品的体验。
在内部,我们现在对自己的仪表盘也采用了类似的策略。当用户从核心仪表盘导航到我们的ZeroTrust产品时,实际上它们是两个完全独立的项目,用户只是通过路径 /:accountId/one 被路由到那个项目。
视觉上的统一体验
将这些独立项目缝合在一起,让它们感觉像一个统一的体验,并没有你想象的那么困难:只需要几行CSS魔法。我们绝对不希望发生的事情是将我们的实现细节和内部决策泄露给用户。如果我们无法让这个用户体验感觉像一个统一的前端,那我们就对用户犯下了严重的错误。
要实现这种巧妙的手法,让我们先了解一下视图过渡和文档预加载是如何发挥作用的。
视图过渡
当我们想要在两个不同页面之间无缝导航,同时让最终用户感觉流畅时,视图过渡非常有用。在页面上定义特定的DOM元素,让它们一直保留到下一页可见,并定义任何变化的处理方式,这成为了多页应用的强大缝合工具。
然而,在某些情况下,让各个垂直微前端感觉不同也是完全可以接受的。比如我们的营销网站、文档和仪表盘,它们各自都有独特的定义。用户不会期望这三者在导航时都感觉统一。但是……如果你决定在单个体验中引入垂直切片(例如 /dash/product-a 和 /dash/product-b),那么用户绝对不应该知道它们底层是两个不同的仓库/Worker/项目。
好了,说得够多了——让我们开始动手吧。我说过让两个独立的项目对用户来说感觉像是一个是低成本的,如果你还没有听说过CSS视图过渡,那么接下来我要让你大开眼界了。
如果我告诉你,你可以在单页应用(SPA)或多页应用(MPA)的不同视图之间创建动画过渡,让它们感觉像是一个整体?在添加任何视图过渡之前,如果我们导航属于两个不同Worker的页面,中间加载状态会是浏览器中的白色空白屏幕,持续几百毫秒,直到下一页开始渲染。页面不会感觉统一,当然也不会像单页应用。
![]()
如果希望元素保留,而不是看到白色空白页,我们可以通过定义CSS视图过渡来实现。通过下面的代码,我们告诉当前文档页面,当视图过渡事件即将发生时,将nav DOM元素保留在屏幕上,如果现有页面和目标页面之间存在任何外观差异,我们将使用ease-in-out过渡来动画展示。
突然之间,两个不同的Worker感觉就像一个了。
@supports (view-transition-name: none) {
::view-transition-old(root),
::view-transition-new(root) {
animation-duration: 0.3s;
animation-timing-function: ease-in-out;
}
nav { view-transition-name: navigation; }
}
![]()
预加载
在两个页面之间过渡让它"看起来"无缝——我们还希望它"感觉"像客户端SPA一样即时。虽然目前Firefox和Safari不支持Speculation Rules,但Chrome/Edge/Opera确实支持这个较新的API。Speculation Rules API旨在提高未来导航的性能,特别是对于文档URL,让多页应用感觉更像单页应用。
分解成代码,我们需要定义一个特定格式的脚本规则,告诉支持的浏览器如何预取与我们Web应用程序连接的其他垂直切片——可能通过某些共享导航链接。
<script type="speculationrules">
{
"prefetch": [
{
"urls": ["https://product-a.com", "https://product-b.com"],
"requires": ["anonymous-client-ip-when-cross-origin"],
"referrer_policy": "no-referrer"
}
]
}
</script>
有了这些,我们的应用程序会预取其他微前端并将它们保留在内存缓存中,所以如果我们导航到那些页面,会感觉几乎是即时的。
对于明显可区分的垂直切片(营销、文档、仪表盘),你可能不需要这样做,因为用户在它们之间导航时会预期有轻微的加载。然而,当垂直切片定义在特定可见体验内时(例如在仪表盘页面中),强烈建议使用。
通过视图过渡和推测规则,我们能够将完全不同的代码仓库联系在一起,感觉就像它们来自单页应用一样。如果你问我,这太神奇了。
零配置请求路由
现在我们需要一种机制来托管多个应用程序,以及一种在请求流入时将它们缝合在一起的方法。定义一个Cloudflare Worker作为"路由器",允许在边缘的单个逻辑点处理网络请求,然后将它们转发给负责该URL路径的垂直微前端。而且我们可以将单个域名映射到该路由器Worker,其余的就"正常工作"了。
服务绑定
如果你还没有探索过Cloudflare Worker服务绑定,那么值得花点时间了解一下。
服务绑定允许一个Worker调用另一个Worker,而无需经过公开可访问的URL。服务绑定允许Worker A调用Worker B上的方法,或将请求从Worker A转发到Worker。进一步分解,路由器Worker可以调用已定义的每个垂直微前端Worker(例如营销、文档、仪表盘),假设它们都是Cloudflare Workers。
这为什么重要?这正是将这些垂直切片"缝合"在一起的机制。我们将在下一节深入探讨请求路由如何处理流量分割。但要定义这些微前端中的每一个,我们需要更新路由器Worker的wrangler定义,这样它就知道允许调用哪些前端。
{
"$schema": "./node_modules/wrangler/config-schema.json",
"name": "router",
"main": "./src/router.js",
"services": [
{
"binding": "HOME",
"service": "worker_marketing"
},
{
"binding": "DOCS",
"service": "worker_docs"
},
{
"binding": "DASH",
"service": "worker_dash"
}
]
}
上面的示例定义在我们的路由器Worker中,然后告诉我们被允许向三个独立的额外Worker(营销、文档和仪表盘)发出请求。授予权限就这么简单,但让我们深入研究一些更复杂的逻辑,包括请求路由和HTML重写网络响应。
请求路由
了解了在需要时可以调用的各种其他Worker之后,现在我们需要一些逻辑来确定何时将网络请求定向到哪里。由于路由器Worker被分配到我们的自定义域名,所有传入的请求首先在网络边缘到达它。然后它确定哪个Worker应该处理请求,并管理结果响应。
第一步是将URL路径映射到关联的Worker。当收到某个请求URL时,我们需要知道它需要被转发到哪里。我们通过定义规则来实现这一点。虽然我们支持通配符路由、动态路径和参数约束,但我们将专注于基础——字面路径前缀——因为它更清楚地说明了要点。
在这个例子中,我们有三个微前端:
/ = 营销
/docs = 文档
/dash = 仪表盘
上面的每个路径都需要映射到一个实际的Worker(参见上面章节中的wrangler服务定义)。对于我们的路由器Worker,我们定义一个额外的变量,包含以下数据,这样我们就知道哪些路径应该映射到哪些服务绑定。现在我们知道当请求进来时应该将用户路由到哪里!定义一个名为ROUTES的wrangler变量,内容如下:
{
"routes": [
{"binding": "HOME", "path": "/"},
{"binding": "DOCS", "path": "/docs"},
{"binding": "DASH", "path": "/dash"}
]
}
让我们设想一个用户访问我们网站的路径 /docs/installation。在底层,发生的情况是请求首先到达我们的路由器Worker,它负责了解什么URL路径映射到哪个独立的Worker。它理解 /docs 路径前缀映射到我们的 DOCS 服务绑定,参照我们的wrangler文件指向我们的 worker_docs 项目。我们的路由器Worker知道 /docs 被定义为垂直微前端路由,从路径中移除 /docs 前缀,将请求转发给我们的 worker_docs Worker来处理请求,然后最终返回我们得到的任何响应。
为什么要删除 /docs 路径呢?这是一个实现细节的选择,目的是当Worker通过路由器Worker访问时,它可以清理URL来处理请求,就像它是从路由器Worker外部调用的一样。像任何Cloudflare Worker一样,我们的 worker_docs 服务可能有自己的独立URL可以访问。我们决定希望该服务URL继续独立工作。当它附加到我们的新路由器Worker时,它会自动处理移除前缀,这样服务就可以从自己定义的URL或通过我们的路由器Worker访问……任何地方都可以,无所谓。
HTMLRewriter
用URL路径分割我们的各种前端服务(例如 /docs 或 /dash)让我们很容易转发请求,但当我们的响应包含不知道它被通过路径组件反向代理的HTML时……嗯,这就会出问题。
假设我们的文档网站在响应中有一个图片标签 <img src="./logo.png" />。如果我们的用户正在访问页面 https://website.com/docs/,那么加载 logo.png 文件可能会失败,因为我们的 /docs 路径只是由我们的路由器Worker人为定义的。
只有当我们的服务通过路由器Worker访问时,我们才需要对一些绝对路径进行HTML重写,这样我们返回的浏览器响应才能引用有效的资源。实际上发生的是,当请求通过我们的路由器Worker时,我们将请求传递给正确的服务绑定,并从中接收响应。在将其传回客户端之前,我们有机会重写DOM——所以在看到绝对路径的地方,我们继续用代理路径预先填充它。以前我们的HTML返回的图片标签是 <img src="./logo.png" />,现在我们修改为在返回客户端浏览器之前 <img src="./docs/logo.png" />。
![]()
让我们回到CSS视图过渡和文档预加载的魔法。我们当然可以把那段代码手动放到我们的项目中并让它工作,但这个路由器Worker也会使用HTMLRewriter自动为我们处理这些逻辑。
在你的路由器Worker ROUTES 变量中,如果你在根级别设置 smoothTransitions 为 true,那么CSS过渡视图代码会自动添加。此外,如果你在路由中设置 preload 键为 true,那么该路由的推测规则脚本代码也会自动添加。
下面是两者结合使用的示例:
{
"smoothTransitions": true,
"routes": [
{"binding": "APP1", "path": "/app1", "preload": true},
{"binding": "APP2", "path": "/app2", "preload": true}
]
}
开始使用
你今天就可以开始使用垂直微前端模板构建了。
访问Cloudflare仪表盘的链接,或者进入"Workers & Pages"并点击"创建应用程序"按钮开始。从那里,点击"选择模板"然后"创建微前端",你就可以开始配置你的设置了。
![]()
更多使用指南,可以点击查看文档 ,如果您对各种云原生架构的内容感兴趣,也可以关注我的博客:程序猿DD,第一时间获得干货更新。