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 中移除文件名部分
                编译前:
                编译后:
                CS144-C++语法积累React Native开发杂记
                • Giscus