Day 8:Preload 是什么?——在别人的网页里打黑工还要合规

Day 8 讲 preload 三层架构:隔离世界里的 IPC 桥,让 webview 安全操作 DOM 并调主进程能力。


开场:webview 能打开 TG 了,然后呢?

Day 6 我们把 Telegram Web 塞进中间栏,Day 7 给每个账号分了独立 partition。老王扫码登录成功,转头问:翻译按钮放哪?消息怎么同步?客户备注怎么挂上去?

这些问题有一个共同答案:得在 webview 里的第三方网页动手脚 。但 Telegram Web 是别人的页面,你没法改它的源码,也不能在页面里直接 require('electron')------Electron 12+ 默认开了 contextIsolation,页面世界和 Node 世界是两套隔离的公寓,钥匙不通用。

所以今天不讲具体平台 DOM 选择器,只讲 preload 是什么、为什么 IM 聚合离不开它、IPC 桥怎么搭

四层架构:谁住在哪一层

AnchorChat 里和 IM 相关的进程/世界,可以画成四层:

text 复制代码
主进程(Main)
  ├── 调 HTTP API、读写本地 DB、管理 session
  └── IPC handle / on

宿主渲染进程(WorkspaceView / ChatHost)
  ├── Vue 三栏 UI、Pinia 状态
  └── 监听 webview 的 ipc-message

webview preload(隔离世界)
  ├── 在 Telegram Web 加载前注入
  ├── 操作 DOM、挂 Vue 小组件、监听消息
  └── invoke 主进程 / sendToHost 回宿主

页面主世界(Telegram Web 自身)
  └── 平台 JS、Service Worker、内部 API(preload 读得到 DOM,但碰不到它的闭包变量)

preload 的定位很尴尬也很关键:它住在 webview 里,但不属于页面。像派进别人公司做驻场开发的同事------能改装修(DOM),不能动人家核心系统(页面 JS 闭包),有事还得打电话回总部(IPC)。

为什么必须 contextIsolation

主窗口和 webview 我们都建议:

typescript 复制代码
webPreferences: {
  nodeIntegration: false,
  contextIsolation: true,
  sandbox: true,
  webviewTag: true,
  preload: path.join(__dirname, 'preload.mjs'),
}

如果把 nodeIntegration: true 打开,页面里的任意脚本(包括 Telegram 自己加载的第三方 JS)都能碰 Node API------相当于把服务器 root 密码贴在会议室玻璃上。IM 聚合客户端嵌入的是不可控的第三方 Web 版,安全底线必须守住。

contextIsolation: true 之后,页面里的 window 和 preload 里的 window 不是同一个对象。preload 想给页面留接口,得用 contextBridge.exposeInMainWorld;想调主进程能力,走 ipcRenderer.invoke / send

两种 preload,别搞混

AnchorChat 里其实有两套 preload,新手最容易在这里绕晕:

类型 挂在哪 干什么
主窗口 preload BrowserWindowwebPreferences.preload 给 Vue 宿主暴露 ipcRendererelectronStore、窗口控制
平台 preload <webview :preload="..."> 注入 Telegram / WhatsApp Web,操作 DOM、挂翻译条

Day 6 解析的 host-script:resolve-preload-path 返回的是平台 preload 路径(public/preload/telegram.js)。主窗口的 preload.mjs 是另一条线,别混在一个文件里。

主窗口 preload 典型写法:

typescript 复制代码
// electron/preload.mjs --- 宿主侧,简化示例
import { contextBridge, ipcRenderer } from 'electron'

contextBridge.exposeInMainWorld('ipcRenderer', {
  invoke: (channel: string, ...args: unknown[]) => ipcRenderer.invoke(channel, ...args),
  send: (channel: string, ...args: unknown[]) => ipcRenderer.send(channel, ...args),
  on: (channel: string, listener: (...args: unknown[]) => void) => {
    ipcRenderer.on(channel, (_event, ...args) => listener(...args))
  },
})

平台 preload 不走 contextBridge 给页面暴露 API------它自己就是「在隔离世界里干活的脚本」,直接改 DOM、挂组件,必要时用 sendToHost 跟外面的 ChatHost 说话。

平台 preload 最小骨架

下一篇才会讲怎么 build 出第一个 telegram.js,今天先看结构长什么样

javascript 复制代码
// platform-scripts/telegram.js --- 简化示例,IIFE 构建后挂到 webview :preload
const { ipcRenderer } = require('electron/renderer')

let hostAppId = ''

// ChatHost 在 did-finish-load 后发来当前账号实例 ID
ipcRenderer.on('host:app-id', (_event, id) => {
  hostAppId = id
})

// 通知宿主:用户在 webview 里点了聊天区域(用于收起右侧抽屉等)
function notifyHostClicked() {
  ipcRenderer.sendToHost('host:webview-clicked')
}

// 新消息到了,让宿主播提示音 / 托盘闪动
function notifyNewMessage() {
  ipcRenderer.sendToHost('host:new-message')
}

// DOM 就绪后启动监听------具体 MutationObserver 逻辑 Day 10 展开
function bootstrap() {
  document.addEventListener('click', notifyHostClicked, true)
  // observeMessages() ... Day 10
}

if (document.readyState === 'loading') {
  document.addEventListener('DOMContentLoaded', bootstrap)
} else {
  bootstrap()
}

宿主 ChatHost 收到 sendToHost 的消息:

typescript 复制代码
// ChatHost.vue
webviewRef.value?.addEventListener('ipc-message', (event) => {
  if (event.channel === 'host:webview-clicked') {
    emit('chat-area-clicked')
  }
  if (event.channel === 'host:new-message') {
    window.ipcRenderer.send('tray:notify')
    playNotificationSound()
  }
})

// 页面加载完成后,把 appId 发给 preload
webviewRef.value?.send('host:app-id', props.account.id)

注意数据流方向:

  • preload → 宿主sendToHost(webview 内部事件要外壳配合)
  • preload → 主进程invoke / send(翻译 API、本地存储等)
  • 宿主 → preloadwebview.send(channel, payload)(下发 appId、采集指令等)

公共桥接模块:别把 IPC 散落一地

各平台脚本会共用「调翻译」「写输入框」「复制剪贴板」等能力。我们抽了一个公共模块(源码在独立 preload 仓库的 utils/app.js,连载里叫 hostBridge.js):

javascript 复制代码
// hostBridge.js
import { ipcRenderer } from './electron-shim.js'

export async function requestTranslate(text, channel, targetLang, accountId) {
  if (!text || !channel || !targetLang) {
    return { success: false, message: '参数不完整' }
  }
  try {
    return await ipcRenderer.invoke('host:translate', {
      text,
      channel,
      targetLang,
      accountId,
    })
  } catch (error) {
    return { success: false, message: '翻译调用失败' }
  }
}

export async function fillChatInput(element, text) {
  element.focus()
  // 用 ClipboardEvent 模拟粘贴,比直接改 innerText 更像用户输入
  // ... Day 17 发消息翻译时细讲
}

平台脚本只 import 这些封装好的函数,不直接散落 ipcRenderer.invoke('xxx')------否则 Telegram 改一版、WhatsApp 改一版,IPC 通道名能长成一片森林。

主进程注册对应 handle:

typescript 复制代码
// electron/main.ts --- 简化示例
ipcMain.handle('host:translate', async (_event, payload) => {
  const { text, channel, targetLang, accountId } = payload
  // 调后端翻译 API、走本地缓存 ... 后面展开
  return { success: true, translatedText: '...' }
})

隔离世界的边界:preload 碰不到什么

有一点很多教程不讲清楚:preload 能自由操作 DOM,但不代表能调用页面内部的 JS 对象

比如 Telegram Web 内部有 rootScopemanagers 这类闭包变量,preload 里 window.rootScope 可能是 undefined------它们在页面主世界,不在隔离世界。如果业务确实需要读页面内部状态,常见两条路:

  1. webview.executeJavaScript():由宿主在主世界执行一段代码,把结果通过 IPC 回传 preload(进阶场景,后面个别平台会用到)
  2. 读 DOM / IndexedDB:不碰闭包,只解析页面上可见的数据(更稳,但平台改版要跟着改选择器)

Day 8 记住原则就行:preload 的首要武器是 DOM,不是破解页面 JS

踩坑与思考

  • 两套 preload 别混 :主窗口 preload.mjs 给 Vue 用;<webview :preload> 给 IM 平台用,构建产物也分仓库
  • nodeIntegration: false 是底线:嵌入第三方 Web 版,不要为图省事打开 Node
  • IPC 通道集中管理 :用 host: 前缀区分宿主通信,主进程 handle 统一注册,方便审计
  • sendToHost vs invoke :前者找 ChatHost 爸爸,后者找主进程爷爷;别用 invoke 传点击事件,大材小用
  • 先骨架后平台 :公共 hostBridge.js + 平台入口 telegram.js 空壳跑通,再加 MutationObserver和 Vue 气泡组件

明日预告

Day 9:写第一个平台脚本 ------从空文件到 Vite lib 模式 IIFE 构建,把 telegram.js 打进 public/preload/,复制到客户端,刷新 webview 验证注入成功。终于要写「能跑的」代码了

相关推荐
陈随易1 天前
在 VSCode 里,把项目一键部署到服务器
前端·后端·程序员
Highcharts.js1 天前
教程:基于 React + Highcharts 构建一个单页应用程序、按需数据拉取与图表渲染
开发语言·前端·数据结构·react.js·前端框架·highcharts·页面应用
To_OC1 天前
我被 useState 坑了两次之后,终于把它的脾气摸透了
前端·javascript·react.js
鱼樱前端1 天前
我用 Claude Code 后,编码效率翻 3 倍。但更值钱的是别的。
前端·后端·ai编程
_lucas1 天前
给知识库网站接入AI问答
前端·ai编程·全栈
前端糕手1 天前
前端面试题大全:JavaScript + Vue3 + React + TypeScript + 工程化 + 性能优化
前端
我不叫武1 天前
一个用 Rust 写的离线编码查询桌面工具
前端
Web4Browser1 天前
指纹浏览器 API 自动化怎么接:启动 Profile、获取 CDP 端点并连接自动化框架
前端·网络·typescript·自动化
Patrick_Wilson1 天前
从 React 到 Flutter:写给前端的一张跨端知识地图
前端·flutter·react.js
颜酱1 天前
06 | 把 meta_config 同步进 MySQL(生成阶段)
前端·人工智能·后端