type
status
date
slug
summary
tags
category
icon
password
JS Bundle
大前端工程里 H5 动态化 & 容器化的一套体系:
- bundle 就是把前端代码(JS/TS + Vue/React + CSS 等等),通过打包工具(如 webpack、rollup、vite)编译成一个 纯 JS 文件(通常还会有 map、资源文件等)
- 这个 bundle 会被容器加载和运行
- 页面被编译成 bundle
- bundle 被上传到一个类似于 cdn 的平台
- app 里面一般有个动态容器,它会拉取并加载这个bundle
- bundle 运行时通过 MSI/KNB(Bridge)和 native 交互,所以即使是 H5 写的,也能调起原生能力
- 业务场景中的优势:
- 前端页面可以通过下发 bundle 来更新,而不用重新发版整个 app
- 容器里面提供统一的桥(Bridge)去调用原生 api,比如定位、支付、扫码
打包构建:
- 项目框架 max/mrn,使用 metro 作为 javascript 的打包器,将 js/ts 代码打包成单个 bundle 文件
- 页面注册,AppRegistry.registerComponent
JS Bridge
是一种 H5 和 App 原生应用之间通信的方法:
- 原生应用通过注入 JavaScript 接口到 WebView 中,让 H5 页面可以调用原生方法,从而实现双向通信「本质还是利用了 webview 提供的注入方法」
- 它允许 JavaScript 代码调用原生应用的方法,并且允许原生应用调用 JavaScript 中的方法,实现了两者之间的双向通信
webview:用于在手机系统中展示 html 页面
- 是手机客户端中内置了一款高性能基于 webkit 内核浏览器,在 SDK 中封装的一个组件
- 在电脑上只需要通过浏览器打开 html 文件就可以浏览渲染好的网页,但在手机系统层面如果没有 webview 支持就无法展示 html 页面
- 有时候页面交互需要调用原生方法,比如摄像头、系统文件等,原生端就负责维护 html 调用的接口,然后按需要返回,webview 与 native 的交互的核心就是两者的交互如何实现,作为两者之间的沟通桥梁,
JS Bridge
就应运而生了
实现 JS Bridge:
- 原生应用给 webview 注册接口,在加载 webview 的时候提供一些接口给 js 使用
- 有点像原生应用是服务端,H5 是客户端,给服务端发送请求:
- 当 JavaScript 层需要调用原生组件时,会通过 Bridge 将调用信息序列化为 JSON 数据包。这个数据包通过异步的方式发送到原生层
- 原生层接收到数据包后,反序列化并执行相应的原生操作,随后将结果(若有)再次通过 Bridge 发送回 JavaScript 层,然后 js 层执行响应的回调函数
- 例子:调用 android.webView 提供的 addJavascriptInterface 方法给 webview 注册一个接口,名字叫 NativeInterface
- NativeInterface 会被挂载到 js 的全局作用域,webview 中前端可以使用 window.NativeInterface 访问
- js 封装 JS Bridge 对象,通常包含「给原生应用发送消息」的方法和「接收原生应用消息」的方法
- 原生应用调用 JSBridge 里面的方法给 H5 传递数据:
webView.evaluateJavascript
也是 andorid webview 提供的原生方法,参数是我们 js 里面声明的 JSBridge 全局对象
跨端框架原理
以 uniapp 为例:uniapp 是 DCloud 推出的跨平台前端框架,使用 HTML5、CSS 和 JavaScript 来开发跨平台应用。uniapp 通过将代码编译成各平台的原生代码或 Web 代码,实现跨平台运行。
编译阶段:
- 使用 Weex 编译开发者编写的 Vue.js 模版,Weex 将开发者的 Vue.js 模板和 JavaScript 逻辑代码转换为 Weex 可执行的格式
- CSS 样式表也被转换为适用于 Weex 的样式描述
编译器的处理过程:
- 预处理
- 模板解析:uniapp 编译器首先解析 Vue.js 模板,将其转换为相应的虚拟 DOM 结构
- 样式解析:解析 CSS 样式,将其转换为适用于各个平台的样式表
- 脚本解析:解析 JavaScript 逻辑代码,确保代码符合目标平台的要求
- 平台适配
- 组件映射:uniapp 编译器会将 Vue.js 组件映射到各个平台的原生组件,比如将 <view> 组件在 android 端映射为 View
- API 映射:uniapp 编译器会将通用 api 映射为各个平台的原生 api,例如
uni.request
在微信小程序中映射为wx.request
,在 ios 和 web 上也映射为各自的网络请求 api
- 代码生成
- 对于小程序平台,uniapp 编译器将 Vue.js 模板、CSS 样式和 JavaScript 逻辑转换为小程序的 WXML、WXSS 和 JS 文件
- 对于 ios 和 android 平台,uniapp 编译器将 Vue.js 模版和 js 逻辑转换为 Weex 格式,生成可以运行在 Weex 容器中的代码包
- 代码打包
- uniapp 编译器将处理过的代码、样式和资源文件打包成一个完整的项目结构并进行压缩优化
运行阶段:
- 编译后的应用运行在 ios 和 android 上时,会包含一个内嵌的 Weex 容器
- iOS
- Weex 容器通过 Objective-C 或 Swift 嵌入应用
- Weex 使用 JavaScriptCore 作为 JavaScript 引擎,将 Weex 代码解析并执行
- Weex 容器实际上是一个 UIView 控件,负责渲染 Weex 页面
- android
- Weex 容器通过 Java 嵌入应用
- Weex 使用 V8 作为 JavaScript 引擎,将 Weex 代码解析并执行
- Weex 容器实际上是一个 View 控件,负责渲染 Weex 页面
其实就是两个层级:
- 逻辑层(JavaScript)
- 你的 Weex 页面,其实就是 JS 写的逻辑和描述 UI 的 JSON。
- 这部分在移动端需要一个 JS 引擎 来执行:
- iOS → 用 JavaScriptCore(Apple 系统自带的 JS 引擎)
- Android → 用 V8(Chrome 的 JS 引擎)
- JS 引擎不会渲染 UI,只是跑你的业务逻辑、生成虚拟 DOM 或指令
- 渲染层(Native 控件)
- JS 代码通过桥接层(Bridge)告诉原生:我要一个
div
,里面放一个text
- Native 这边就把这些映射成 iOS 的 UIView / UILabel / UIButton 或 Android 的 View / TextView / Button
- 这些就是系统自带的 原生控件,负责最终显示在屏幕上
flutter:flutter 的最大特点之一是 它不依赖于原生控件。相比 React Native 和 uniapp 这类框架,它不通过桥接(Bridge)和原生控件交互来渲染 UI,而是通过 自定义的渲染引擎 来直接绘制界面
- flutter 不使用平台原生的控件,而是通过 Skia 绘制整个 ui,这意味着 flutter 可以完全控制每一帧的渲染过程,确保 UI 在不同平台上具有一致性
- 渲染层:flutter 会将 ui 代码转换为 Skia 可以理解的绘制指令,直接绘制到屏幕上,不需要传统的原生控件
Dart 语言与虚拟机:
- flutter 通过 Dart 编写代码并执行
- 开发时,flutter 通过 Dart 虚拟机来执行代码
- 发布应用时,Dart 代码会被 AOT 编译成平台特定的原生机器码,确保 Flutter 应用在各个平台上具有原生级别的性能
LLM 前端应用
server-sent events(sse)接入的方案:
- EventSource 实现接入:
- 是一个 web api,是浏览器提供的原生对象,专门用于处理 sse
- 基于 http 协议实现,通过与服务器建立一个持续连接,实现了服务器向客户端推送事件数据的功能
- 在客户端,通过 EventSource 对象注册事件处理函数,以接收来自服务器的事件数据
- 不支持 post 请求
- sse 本身规定使用 HTTP GET 请求来建立连接,服务器返回
Content-Type: text/event-stream
的响应头,然后持续不断往这个连接里推送数据
AppRegistry 的作用
AppRegistry 是 rn 的应用注册中心:
- 注册根组件,将 rn 组件注册为可被原生端调用的入口
- 原生代码通过组件名来启动对应的 rn 页面
- 原生端可以通过 appParameters 向 rn 组件传递初始参数
轻资讯消费页
在视频评论区上方部署 agent,推送与当前内容强关联的信息,并且提供 ai 对话功能,根据用户发送的信息生成资讯卡片。
核心内容
包括资讯大卡、资讯小卡、热点视频:
- 大卡:
- 标题
- ai 生成的流式数据
- 相关视频
- 猜你喜欢:点击 title 再生成一张大卡
- 小卡:更多资讯
- 纯文字:标题 + 简介
- 视频:标题 + 简介 + 3张视频封面
- 猜你喜欢:3个 title
- 热点视频:
- 横向滑动的视频列表
交互方式
- 卡片式布局:垂直滑动的卡片展示方式
- 动画切换:每张新的大卡通过翻页动画定位到屏幕上方,下滑通过翻页动画加载上一张大卡
- 更多资讯:紧接在最后一张大卡下方,触底自动加载更多
- 热点视频:横向滑动,点击封面播放视频列表
技术难点
数据同步
多个组件需要共享和同步大量状态数据,传统的 props 传递会导致组件层级过深和性能问题
解决方案:使用自定义 hook 来同步数据
- 在 hook 中调用接口获取数据
- 双重状态管理机制:先更新 ref,再触发渲染
- 使用 useState 确保触发页面重新渲染
- 使用 useRef 提供同步访问最新数据的能力
- 返回 ref 对象确保所有使用者获得同一个引用
数据不立马更新:
- 触发
- 解决
- 也可以通过双重状态管理来解决
- 原因
- 当调用 setItems 时,React 并不会立即更新组件状态,而是:
- 标记状态需要更新:React 将这次状态更新加入到更新队列中
- 批处理优化:React 会将多个状态更新合并,在合适的时机一起处理
- 异步调度:状态更新会在下一个渲染周期执行
useState 的闭包陷阱:在函数组件中使用
useState
,如果在事件处理函数或其他函数内部引用了state
,可能会因为闭包的原因,导致这些函数访问到的state
值是其创建时的旧值,而不是最新的值。- 原因:闭包存储旧值
- 解决
- 函数式更新
- 使用 ref 获取最新值
一般性能优化
- 使用 useRef 存储不需要触发重新渲染的数据
- 使用 memo 包裹组件避免不必要的重新渲染
- 一般场景:避免在父组件重新渲染的时候重新渲染子组件
- 对于要返回或者要作为参数的函数,使用 useMemo 包裹,缓存函数
- 一般场景:有函数要传给子组件,并且子组件用 memo 包裹的时候才有效
长列表性能优化
使用 FlatList 渲染长列表的时候出现警告:VirtualizedList: You have a large list that is slow to update,表示一个大列表的更新速度太慢了
优化方案:
- 优化 renderItem,使用 useCallBack 包裹;renderItem 作为 FlatList 的 props,这样传入可以减少子组件的重新渲染
- 将列表样式项等常量提出去并用 useMemo 包裹,不添加依赖,避免每次 renderItem 都要创建样式对象
- 创建独立的列表项组件 MoreInfoListItem 并用 memo 包裹,作为一个子组件来使用,这样这个子组件就能被缓存
处理流式响应
需要拼接获取的流式数据,并按照一定速度输出,呈现出打字机的效果。
- 发起流式请求,并设置流式状态,将获取到的状态和数据交给 InfoCard 组件渲染
- 流式状态通过 StreamStatus 接口管理:
- isApiStreamDone: API流式是否完成
- isMockStreamDone: 前端模拟打字效果是否完成
- currentIndex: 当前正在流式显示的卡片索引
- InfoCard 组件又将数据交给 MemoMarkDown 组件进行渲染,而这个组件就是用来呈现打字机效果的
- useCardData 钩子:
- 负责获取流式响应拼接完整的字符串
- 将拼接好的 content 传入 MemoMarkDown 组件
- MemoMarkDown 组件:
- 设置 displayText:
- 如果当前组件是正在流式显示的组件且流式未完成,则初始显示为空字符串
- 否则直接显示完整内容
- 使用 useEffect 包裹实现打字机效果的函数:
- 速度控制: 使用 setTimeout 和 1ms 的延迟来控制显示速度
- 步长控制:每次显示增加 2 个字符
- useCardData 会让 displayText 改变,每次就会触发这个 useEffect,就可以以平均的步长和时长进行输出了
Rendered more hooks than during the previous render
意思是多次渲染中 hooks 调用的数量不一致。
出现在需要兼容 IOS 和 Android 的时候,有一个组件的部分属性希望在 Android 上设置而 IOS 不需要设置,最开始写成了这样:
- 使用了条件性的 props 展开,安卓端和 IOS 端的这个组件接收到的 props 不同
- 可能导致组件内部状态不一致
- 可能导致组件内部调用的 hooks 数量不一致
修复:
Babel 配置
babel.config.js 中:
- placeholder: '__filename':占位符名称
- root: 'src':以 src 目录作为根路径
- dropAllFilenames: true:在 __dirname 中移除文件名部分
编译前:
编译后:
- Author:orangec
- URL:orange’s blog | welcome to my blog (clovy.top)/article/256c107a-b41d-80f6-a743-c6fd18f464fc
- Copyright:All articles in this blog, except for special statements, adopt BY-NC-SA agreement. Please indicate the source!