1303 lines
35 KiB
Markdown
1303 lines
35 KiB
Markdown
# CSS/JS/字体资源分析
|
||
|
||
本文档对微信读书 EPUB 阅读器的前端资源文件进行全面分析,涵盖 CSS 样式表、JavaScript 脚本和字体文件。
|
||
|
||
---
|
||
|
||
## 一、CSS 文件分析
|
||
|
||
### 1.1 样式层叠架构
|
||
|
||
微信读书的 CSS 采用分层架构:
|
||
|
||
```
|
||
default.css (基础HTML样式)
|
||
├── replace.css (EPUB替换样式)
|
||
├── replaceForLatinLanguageBook.css (拉丁语书籍替换样式)
|
||
├── replaceForMPChapter.css (公众号章节替换样式)
|
||
└── MediaPlatform.css (公众号文章基础样式)
|
||
└── MPExtra.css (公众号文章扩展样式)
|
||
|
||
dark.css (暗黑主题 - 覆盖层)
|
||
|
||
Style/style.css (富文本编辑器样式)
|
||
Style/normalize.css (标准化样式)
|
||
Style/preview.css (预览模式样式)
|
||
|
||
xcode.min.css (代码高亮主题)
|
||
WREpubMPVideo.css (视频播放器样式)
|
||
```
|
||
|
||
### 1.2 default.css - 基础HTML样式
|
||
|
||
**路径**: `output/resources/css/default.css` (222行)
|
||
|
||
Safari 派生的基础 HTML 默认样式,为 WebView 渲染提供标准化起点。
|
||
|
||
**核心特性**:
|
||
- HTML5 元素设置 `display:block`(article, aside, footer, header, nav, section 等)
|
||
- 隐藏非显示元素:`head, title, style, link, meta, script { display:none }`
|
||
- body 默认字体大小 `16px`
|
||
- 链接颜色 `#2262A3`
|
||
- 代码块样式:`background-color:rgba(0,0,0,.05)`, `border-radius:0.3em`, `padding:0.2em`
|
||
- 表格边框合并:`border-collapse:collapse`
|
||
|
||
### 1.3 replace.css - EPUB替换样式
|
||
|
||
**路径**: `output/resources/css/replace.css` (172行)
|
||
|
||
EPUB 内容的核心替换样式,定义了微信读书特有的排版规则。
|
||
|
||
**字体设置**:
|
||
- 标题和版权页使用 `"SourceHanSerifCN-Medium"` 字体
|
||
- 代码块使用 `Consolas, "Liberation Mono", Menlo, Courier, monospace`,大小 `0.7em!important`
|
||
|
||
**标题层级**:
|
||
| 类名 | 字号 | 字重 |
|
||
|------|------|------|
|
||
| `.firstTitle` | 1.5em | normal |
|
||
| `.secondTitle` | 1.3em | normal |
|
||
| `.thirdTitle` | 1.15em | normal |
|
||
| `.fourthTitle` | 1.1em | normal |
|
||
| `.fifthTitle` | 1.05em | normal |
|
||
| `.sixthTitle` | 1em | normal |
|
||
|
||
**微信读书自定义CSS属性**:
|
||
|
||
```css
|
||
/* 垂直居中样式 - 值2用于正文图片 */
|
||
.bodyPic {
|
||
wr-vertical-center-style: 2;
|
||
}
|
||
|
||
/* 垂直居中样式 - 值1用于强制垂直居中 */
|
||
.wr-vertical-center {
|
||
wr-vertical-center-style: 1 !important;
|
||
}
|
||
|
||
/* 页面关联标记 */
|
||
.weread-page-relate {
|
||
weread-page-relate: true;
|
||
}
|
||
```
|
||
|
||
**其他重要样式**:
|
||
- `.ftext` - 首字母放大效果(2.38em, float:left)
|
||
- `.book-chapter-tool` - 章节结束工具栏(78px高度)
|
||
- `p { text-align:justify; }` - 段落两端对齐
|
||
- `.eepub-single-image-title` - 单图片标题,使用 `"FZFSJW--GB1-0"` 字体
|
||
|
||
### 1.4 dark.css - 暗黑主题
|
||
|
||
**路径**: `output/resources/css/dark.css` (57行)
|
||
|
||
纯黑背景的暗黑主题样式。
|
||
|
||
**核心样式**:
|
||
```css
|
||
/* 所有元素强制黑色背景 */
|
||
* {
|
||
background-color: rgba(0, 0, 0, 1) !important;
|
||
}
|
||
|
||
/* 业务容器特殊背景 */
|
||
.business-container {
|
||
background-color: rgb(28, 28, 29) !important;
|
||
}
|
||
|
||
/* 文字颜色 */
|
||
body, p, div, span, a {
|
||
color: rgba(180, 180, 182, 1) !important;
|
||
}
|
||
|
||
/* 链接颜色 */
|
||
a {
|
||
color: rgba(84, 127, 176, 1) !important;
|
||
}
|
||
|
||
/* 引用块 */
|
||
blockquote {
|
||
color: rgb(218, 220, 224);
|
||
background-color: rgb(10, 14, 18);
|
||
}
|
||
```
|
||
|
||
**特殊处理**:
|
||
- 兼容搜狗/百度搜索页面的暗黑模式
|
||
|
||
### 1.5 replaceForLatinLanguageBook.css - 拉丁语书籍样式
|
||
|
||
**路径**: `output/resources/css/replaceForLatinLanguageBook.css` (113行)
|
||
|
||
拉丁语系书籍的特殊替换样式。
|
||
|
||
**与replace.css的关键差异**:
|
||
- 标题不设置 `font-family`(使用浏览器默认字体而非 SourceHanSerifCN-Medium)
|
||
- `.eepub-single-image-title` 使用 `"FZFSJW--GB1-0"` 字体
|
||
- 保留相同的 `wr-vertical-center-style` 属性
|
||
|
||
### 1.6 replaceForMPChapter.css - 公众号章节样式
|
||
|
||
**路径**: `output/resources/css/replaceForMPChapter.css` (137行)
|
||
|
||
微信公众号文章章节的特殊样式。
|
||
|
||
**字体设置**:
|
||
- 使用 `"Source Han Serif CN"` 字体族(带空格,与 replace.css 的 `"SourceHanSerifCN-Medium"` 不同)
|
||
- 标题 `font-weight: bold`(replace.css 中为 normal)
|
||
|
||
**特殊样式**:
|
||
```css
|
||
/* 打赏区域 */
|
||
.chapter-reward {
|
||
height: 120px;
|
||
}
|
||
|
||
/* 强制透明背景 - epub-custom-rule-21 */
|
||
p, strong, span {
|
||
background-color: transparent !important;
|
||
}
|
||
|
||
/* 强制透明背景 - epub-custom-rule-26 */
|
||
section, .rich_media_area_primary {
|
||
background-color: transparent !important;
|
||
}
|
||
```
|
||
|
||
### 1.7 MediaPlatform.css - 公众号文章基础样式
|
||
|
||
**路径**: `output/resources/css/MediaPlatform.css` (141行)
|
||
|
||
微信公众号文章的基础样式,包含标注系统。
|
||
|
||
**主要内容区域**:
|
||
```css
|
||
.rich_media_area_primary {
|
||
font-size: 18px;
|
||
line-height: 30px;
|
||
font-family: -apple-system;
|
||
}
|
||
```
|
||
|
||
**标注系统样式**:
|
||
| 类名 | 用途 | 样式 |
|
||
|------|------|------|
|
||
| `.highlight` | 高亮标注 | `background-color: rgba(255, 116, 0, .25)` |
|
||
| `.review` | 我的想法 | 红色虚线边框 `border: 1px dashed rgba(220, 53, 69, 1)` |
|
||
| `.friend-review` | 好友想法 | 灰色虚线边框 `border: 1px dashed rgba(130, 130, 130, 1)` |
|
||
| `.reference` | 引用标注 | `background-color: rgba(255, 214, 0, .25)` |
|
||
| `.tts` | TTS朗读高亮 | `background-color: rgba(27, 136, 238, .25)` |
|
||
|
||
**复合标注样式**:
|
||
```css
|
||
/* review 覆盖 highlight 的样式 */
|
||
.highlight.review {
|
||
background-color: transparent;
|
||
border: 1px dashed rgba(220, 53, 69, 1);
|
||
}
|
||
```
|
||
|
||
**其他重要样式**:
|
||
- `-webkit-tap-highlight-color: transparent` - 禁用所有标注类型的点击高亮
|
||
- `.h5_image_link { pointer-events: none; }` - 禁用图片链接点击
|
||
|
||
### 1.8 MPExtra.css - 公众号文章扩展样式
|
||
|
||
**路径**: `output/resources/css/MPExtra.css` (673行)
|
||
|
||
完整的微信公众号文章 CSS,从微信网页版复制。
|
||
|
||
**核心特性**:
|
||
```css
|
||
/* 内容区域最大宽度限制 */
|
||
.rich_media_content * {
|
||
max-width: 100% !important;
|
||
box-sizing: border-box !important;
|
||
}
|
||
```
|
||
|
||
**WeUI 对话框系统**:
|
||
- `.weui-mask` - 遮罩层
|
||
- `.weui-dialog` - 对话框
|
||
|
||
**音频播放器样式**:
|
||
- `.audio_container` - 音频容器
|
||
- `.audio_button` - 播放按钮
|
||
- `.audio_progress` - 进度条
|
||
|
||
**其他组件**:
|
||
- Loading spinner(base64 SVG 动画)
|
||
- GIF 图片处理(`.gif_img_wrp`)
|
||
- 设备特定媒体查询(iPhone6/6P)
|
||
- `.unsupported_iframe` 不支持元素样式
|
||
|
||
### 1.9 xcode.min.css - 代码高亮主题
|
||
|
||
**路径**: `output/resources/css/xcode.min.css` (1行,压缩)
|
||
|
||
highlight.js 的 xcode 主题,用于代码语法高亮。
|
||
|
||
- 白色背景,黑色文字
|
||
- 彩色语法标记(关键字、字符串、注释等)
|
||
|
||
### 1.10 WREpubMPVideo.css - 视频播放器样式
|
||
|
||
**路径**: `output/resources/css/WREpubMPVideo.css` (191行)
|
||
|
||
腾讯视频播放器的自定义样式覆盖。
|
||
|
||
**核心组件**:
|
||
- `.tvp_container` - 视频容器
|
||
- `.tvp_controlbar` - 控制栏
|
||
- `.tvp_playbtn` / `.tvp_pausebtn` - 播放/暂停按钮
|
||
- `.tvp_progress_slider` - 进度滑块
|
||
|
||
**资源引用**:
|
||
```css
|
||
/* 使用腾讯CDN的雪碧图 */
|
||
background-image: url(https://rescdn.qqmail.com/weread/cover/icon/WREpubMPVideo.png);
|
||
```
|
||
|
||
**广告隐藏**:
|
||
```css
|
||
/* 隐藏广告和应用下载提示 */
|
||
.tvp_ads_go, .tvp_app_download_onpause {
|
||
display: none !important;
|
||
}
|
||
```
|
||
|
||
### 1.11 Style/style.css - 富文本编辑器样式
|
||
|
||
**路径**: `output/resources/css/Style/style.css` (408行)
|
||
|
||
富文本编辑器的基础样式。
|
||
|
||
**编辑器模式**:
|
||
```css
|
||
/* 编辑模式 */
|
||
.re_Write {
|
||
/* 可编辑状态样式 */
|
||
}
|
||
|
||
/* 显示模式 */
|
||
.re_Display {
|
||
/* 只读状态样式 */
|
||
}
|
||
|
||
/* 夜间模式 */
|
||
.re_Night {
|
||
color: rgba(180, 180, 182, 1);
|
||
background-color: rgba(0, 0, 0, 1);
|
||
}
|
||
```
|
||
|
||
**书籍卡片组件**:
|
||
```css
|
||
.re_bookItem {
|
||
/* 书籍信息展示 */
|
||
}
|
||
.re_bookItem_cover {
|
||
/* 封面图片 */
|
||
}
|
||
.re_bookItem_title {
|
||
/* 书籍标题 */
|
||
}
|
||
.re_bookItem_author {
|
||
/* 作者信息 */
|
||
}
|
||
```
|
||
|
||
**图片处理**:
|
||
```css
|
||
.re_img {
|
||
/* 图片样式 */
|
||
}
|
||
.bodyPic {
|
||
/* 正文图片 */
|
||
}
|
||
```
|
||
|
||
**高DPI边框处理**:
|
||
```css
|
||
/* 使用transform:scale(0.5)实现0.5px边框 */
|
||
.retina-border {
|
||
border: 1px solid #ccc;
|
||
transform: scale(0.5);
|
||
}
|
||
```
|
||
|
||
### 1.12 Style/normalize.css - 标准化样式
|
||
|
||
**路径**: `output/resources/css/Style/normalize.css` (412行)
|
||
|
||
标准的 normalize.css v3.0.2,最小修改。
|
||
|
||
### 1.13 Style/preview.css - 预览模式样式
|
||
|
||
**路径**: `output/resources/css/Style/preview.css` (174行)
|
||
|
||
预览模式的特殊样式。
|
||
|
||
**核心样式**:
|
||
```css
|
||
.re {
|
||
font-size: 18px;
|
||
line-height: 30px;
|
||
font-family: -apple-system;
|
||
}
|
||
```
|
||
|
||
**书籍卡片**:
|
||
- 使用 0.5px 边框技术(retina 显示屏优化)
|
||
|
||
**占位符文本**:
|
||
```css
|
||
.re_placeholder:before {
|
||
content: attr(placeholder);
|
||
color: #999;
|
||
}
|
||
```
|
||
|
||
**引用块**:
|
||
```css
|
||
.blockquote {
|
||
border-left: 3px solid #ccc;
|
||
background-color: #f9f9f9;
|
||
padding: 10px;
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 二、JavaScript 文件分析
|
||
|
||
### 2.1 JS 架构概览
|
||
|
||
```
|
||
WeReadApi.js (JS Bridge 核心)
|
||
├── weread-highlighter.js (高亮/标注引擎)
|
||
│ ├── rangy-highlighter.js (Rangy高亮模块)
|
||
│ ├── rangy-classapplier.js (Rangy类应用器)
|
||
│ ├── rangy-textrange.js (Rangy文本范围)
|
||
│ └── rangy-core.js (Rangy核心库)
|
||
├── rich_editor.js (富文本编辑器)
|
||
├── rich_display.js (富文本显示控制器)
|
||
├── MediaPlatform.js (公众号工具函数)
|
||
├── mpForArticle.js (公众号内容调整)
|
||
├── mp_interceptor.js (点击事件拦截器)
|
||
├── Readability.js (文章内容提取)
|
||
├── HtmlGenerator.js (HTML生成器)
|
||
├── cssInjector.js (CSS注入器)
|
||
└── 其他工具脚本
|
||
```
|
||
|
||
### 2.2 WeReadApi.js - JS Bridge 核心
|
||
|
||
**路径**: `output/resources/js/Text/WeReadApi.js` (238行)
|
||
|
||
WebView 与原生应用之间的核心通信桥接。
|
||
|
||
**WeReadBridge 类**:
|
||
|
||
```javascript
|
||
WeReadBridge = function() {
|
||
this._sendMessageQueue = []; // 消息队列
|
||
this._callback_count = 1000; // 回调计数器
|
||
this._callback_map = {}; // 回调映射表
|
||
this.available_func = {}; // 可用函数
|
||
this._iframe = document.createElement("iframe");
|
||
this._resultIframe = document.createElement("iframe");
|
||
}
|
||
```
|
||
|
||
**通信协议**:
|
||
- 发送消息:`wereadapijs://dispatch_message/`
|
||
- 接收结果:`wereadapijs://private/setresult/`
|
||
|
||
**核心方法**:
|
||
|
||
| 方法 | 用途 |
|
||
|------|------|
|
||
| `handleWithRichEditor(apiName, params, successCallback, failCallback)` | 通用API调用 |
|
||
| `execMPReaderMethod(apiName, params, successCallback, failCallback)` | 公众号阅读器方法调用 |
|
||
| `fetchQueue()` | 获取队列中的消息(JSON字符串) |
|
||
| `_sendMessage(message)` | 将消息加入队列并触发原生调用 |
|
||
| `_call(func, params, callback)` | 创建带callbackId的消息并发送 |
|
||
| `handleMessage(message)` | 处理来自原生的回调响应 |
|
||
|
||
**消息格式**:
|
||
```javascript
|
||
{
|
||
"func": "apiName",
|
||
"params": {
|
||
"key": "value",
|
||
"callbackId": "1001"
|
||
}
|
||
}
|
||
```
|
||
|
||
**数据编码**:
|
||
- `_utf8Encode(str)` - UTF-8编码
|
||
- `_base64Encode(str)` - Base64编码
|
||
- 用于通过iframe URL传递数据
|
||
|
||
**Ready机制**:
|
||
```javascript
|
||
// 检查原生是否已注入信息
|
||
if (window["__WRB_INFO__"]) {
|
||
_onReady();
|
||
} else {
|
||
window["__WRB_INFO_CALL__"] = function() {
|
||
_onReady();
|
||
};
|
||
}
|
||
```
|
||
|
||
**全局实例**:
|
||
```javascript
|
||
window.wereadBridge = new WeReadBridge();
|
||
```
|
||
|
||
### 2.3 weread-highlighter.js - 高亮/标注引擎
|
||
|
||
**路径**: `output/resources/js/weread-highlighter.js` (1530行)
|
||
|
||
公众号文章的核心高亮和标注引擎,基于 Rangy 库构建。
|
||
|
||
**初始化**:
|
||
```javascript
|
||
// 初始化Rangy,使用TextRange模式
|
||
rangy.init();
|
||
highlighter = new rangy.Highlighter();
|
||
```
|
||
|
||
**标注类型**:
|
||
|
||
| 类名 | 用途 | 对应CSS |
|
||
|------|------|---------|
|
||
| `highlight` | 高亮标注 | `rgba(255, 116, 0, .25)` |
|
||
| `review` | 我的想法 | 红色虚线边框 |
|
||
| `friend-review` | 好友想法 | 灰色虚线边框 |
|
||
| `reference` | 引用标注 | `rgba(255, 214, 0, .25)` |
|
||
| `tts` | TTS朗读高亮 | `rgba(27, 136, 238, .25)` |
|
||
| `blue-clickable` | 可点击蓝色 | 用于书籍标题等 |
|
||
|
||
**核心函数**:
|
||
|
||
```javascript
|
||
// 初始化标注(从JSON加载)
|
||
initAnnotations(items)
|
||
|
||
// 创建高亮
|
||
didTapCreateHighlightButton()
|
||
|
||
// 创建想法
|
||
didTapCreateReviewButton(start, end)
|
||
|
||
// 删除标注
|
||
didTapDeleteAnnotationButton(start, end, type)
|
||
|
||
// 点击处理
|
||
click(el, event)
|
||
```
|
||
|
||
**TTS系统**:
|
||
|
||
```javascript
|
||
// TTS状态追踪
|
||
var lastTTSInfo = {
|
||
text: '',
|
||
index: 0,
|
||
startY: 0
|
||
};
|
||
|
||
// TTS相关类
|
||
class TTSRange {
|
||
constructor(start, end, text) { ... }
|
||
}
|
||
class TTSText {
|
||
constructor(text, index, startY) { ... }
|
||
}
|
||
|
||
// 获取TTS文本
|
||
getTTSText(startY)
|
||
|
||
// 高亮TTS文本
|
||
highlightTTSText(text, index, startY)
|
||
```
|
||
|
||
**书籍标题检测**:
|
||
|
||
```javascript
|
||
// 查找所有《》包围的书籍标题
|
||
findAllBookTitles()
|
||
|
||
// 高亮选中的书籍标题
|
||
highlightSelectedBookTitles(indexArray)
|
||
```
|
||
|
||
**链接处理**:
|
||
|
||
```javascript
|
||
// 获取所有前向链接
|
||
getAllForwardLinks()
|
||
|
||
// 转换H5链接为原生数据属性
|
||
transferH5Link()
|
||
|
||
// 解密微信读书加密链接
|
||
WRLink_decrypt(str)
|
||
// type '3' = 数字
|
||
// 其他 = 十六进制编码文本
|
||
```
|
||
|
||
**选择变化处理**:
|
||
```javascript
|
||
// 防抖处理(250ms)
|
||
document.addEventListener("selectionchange", debounce(function(e) {
|
||
// 发送选择变化事件到原生
|
||
wereadBridge.handleWithRichEditor("selectionChange", param, "", "");
|
||
}, 250));
|
||
```
|
||
|
||
**Bridge命令**:
|
||
- `selectionChange` - 选择变化
|
||
- `didTapHighlight` - 点击高亮
|
||
- `didTapReview` - 点击想法
|
||
- `didTapImage` - 点击图片
|
||
- `didTapBook` - 点击书籍
|
||
- `scrollToOffsetTop` - 滚动到偏移位置
|
||
- `didTapHighlightBookTitle` - 点击高亮书籍标题
|
||
|
||
### 2.4 Rangy 模块
|
||
|
||
#### 2.4.1 rangy-highlighter.js (711行)
|
||
|
||
Rangy 高亮器模块,带微信读书自定义修改。
|
||
|
||
**CharacterRange 类**:
|
||
```javascript
|
||
class CharacterRange {
|
||
constructor(start, end) { ... }
|
||
intersects(otherRange) { ... }
|
||
isContiguousWith(otherRange) { ... }
|
||
isContain(otherRange) { ... }
|
||
union(otherRange) { ... }
|
||
intersection(otherRange) { ... }
|
||
getComplements(orderedRanges) { ... }
|
||
}
|
||
```
|
||
|
||
**Highlighter 类**:
|
||
```javascript
|
||
class Highlighter {
|
||
highlightCharacterRanges(className, charRanges, ...) { ... }
|
||
highlightRanges(className, ranges, ...) { ... }
|
||
highlightSelection(selection, ...) { ... }
|
||
serialize() { ... }
|
||
deserialize(serialized) { ... }
|
||
}
|
||
```
|
||
|
||
**自定义方法**:
|
||
- `getMergedCharacterRange` - 合并重叠的高亮
|
||
- `highghtWhichContainSelection` - 查找包含当前选择的高亮
|
||
|
||
**序列化格式**:
|
||
```
|
||
type:TextRange|start$end$id$className[|$containerId]
|
||
```
|
||
|
||
#### 2.4.2 rangy-classapplier.js (1105行)
|
||
|
||
Rangy 类应用器模块。
|
||
|
||
**ClassApplier 类**:
|
||
```javascript
|
||
class ClassApplier {
|
||
constructor(className, options) { ... }
|
||
applyToRange(range) { ... }
|
||
undoToRange(range) { ... }
|
||
applyToSelection(selection) { ... }
|
||
undoToSelection(selection) { ... }
|
||
isAppliedToRange(range) { ... }
|
||
isAppliedToSelection(selection) { ... }
|
||
toggleRange(range) { ... }
|
||
toggleSelection(selection) { ... }
|
||
}
|
||
```
|
||
|
||
**暴露的工具方法**:
|
||
```javascript
|
||
rangy.dom.util.hasClass(node, className)
|
||
rangy.dom.util.addClass(node, className)
|
||
rangy.dom.util.removeClass(node, className)
|
||
rangy.dom.util.getClass(node)
|
||
rangy.dom.util.splitNodeAt(node, index, ...)
|
||
```
|
||
|
||
#### 2.4.3 rangy-textrange.js (1946行)
|
||
|
||
Rangy 文本范围模块,支持基于字符的范围操作。
|
||
|
||
**Position 类**:
|
||
```javascript
|
||
class Position {
|
||
constructor(node, offset) { ... }
|
||
// 字符类型检测
|
||
get characterType() { ... } // NON_SPACE, COLLAPSIBLE_SPACE, etc.
|
||
get isImage() { ... } // 图片文本支持
|
||
}
|
||
```
|
||
|
||
**字符类型常量**:
|
||
- `NON_SPACE` - 非空格字符
|
||
- `COLLAPSIBLE_SPACE` - 可折叠空格
|
||
- `NON_COLLAPSIBLE_SPACE` - 不可折叠空格
|
||
- `IMAGE_TEXT` - 图片文本
|
||
|
||
**Range 扩展方法**:
|
||
```javascript
|
||
range.selectCharacters(containerNode, startIndex, endIndex)
|
||
range.toCharacterRange(containerNode)
|
||
range.text // 获取文本
|
||
range.move(unit, count) // 移动范围
|
||
range.expand(unit, options) // 扩展范围
|
||
range.findText(text, options) // 查找文本
|
||
```
|
||
|
||
### 2.5 rich_editor.js - 富文本编辑器
|
||
|
||
**路径**: `output/resources/js/Text/rich_editor.js` (1710行)
|
||
|
||
用于编写想法/评论的完整富文本编辑器。
|
||
|
||
**RE 命名空间**:
|
||
|
||
```javascript
|
||
RE = {
|
||
editor: document.getElementById('editor'),
|
||
dataSeparator: 'r_e_ds',
|
||
// ... 其他属性和方法
|
||
}
|
||
```
|
||
|
||
**核心方法**:
|
||
|
||
| 方法 | 用途 |
|
||
|------|------|
|
||
| `RE.setHtml(contents)` | 设置HTML内容 |
|
||
| `RE.getHtml()` | 获取HTML内容 |
|
||
| `RE.getText()` | 获取纯文本 |
|
||
| `RE.setBold()` | 设置粗体 |
|
||
| `RE.setItalic()` | 设置斜体 |
|
||
| `RE.setHeading(level)` | 设置标题级别 |
|
||
| `RE.setBlockquote()` | 设置引用块 |
|
||
| `RE.setUnorderedList()` | 设置无序列表 |
|
||
| `RE.setOrderedList()` | 设置有序列表 |
|
||
| `RE.setJustifyLeft()` | 左对齐 |
|
||
| `RE.setJustifyCenter()` | 居中对齐 |
|
||
| `RE.setJustifyRight()` | 右对齐 |
|
||
| `RE.insertBook(id, title, author, cover)` | 插入书籍卡片 |
|
||
| `RE.insertImage(imgArray)` | 插入图片 |
|
||
| `RE.insertQQEmoticon(url, alt, w, h)` | 插入QQ表情 |
|
||
| `RE.insertLink(text, href, title)` | 插入链接 |
|
||
| `RE.unlink()` | 移除链接 |
|
||
| `RE.updateLink()` | 更新链接 |
|
||
| `RE.insertHTML(html)` | 插入HTML |
|
||
| `RE.insertText(text)` | 插入纯文本 |
|
||
| `RE.backuprange()` | 备份选择范围 |
|
||
| `RE.restorerange()` | 恢复选择范围 |
|
||
| `RE.focus()` | 获取焦点 |
|
||
| `RE.blurEditor()` | 失去焦点 |
|
||
|
||
**数据分隔符**:
|
||
```javascript
|
||
RE.dataSeparator = 'r_e_ds'; // 用于连接多个值
|
||
```
|
||
|
||
**内容变化通知**:
|
||
```javascript
|
||
RE.contentChanged = function(e) {
|
||
// 通知原生格式状态变化
|
||
wereadBridge.handleWithRichEditor("onSelectionChange", param, "", "");
|
||
}
|
||
```
|
||
|
||
**EPUB HTML导出**:
|
||
```javascript
|
||
RE.getHtmlForEpub = function() {
|
||
// 移除临时类并应用correctArticle
|
||
return correctedHtml;
|
||
}
|
||
```
|
||
|
||
**粘贴处理**:
|
||
```javascript
|
||
document.addEventListener('paste', function(e) {
|
||
// 拦截粘贴,发送到原生处理
|
||
wereadBridge.handleWithRichEditor("onPaste", {}, "", "");
|
||
});
|
||
```
|
||
|
||
### 2.6 rich_display.js - 富文本显示控制器
|
||
|
||
**路径**: `output/resources/js/Text/rich_display.js` (262行)
|
||
|
||
富文本内容的显示控制器,处理书籍和图片的交互。
|
||
|
||
**书籍交互**:
|
||
```javascript
|
||
RDisplay.onBookToucheStart = function(ele) {
|
||
ele.classList.add('re_bookItem_Touched');
|
||
}
|
||
RDisplay.onBookToucheEnd = function(ele) {
|
||
ele.classList.remove('re_bookItem_Touched');
|
||
}
|
||
RDisplay.onBookClick = function(ele) {
|
||
var id = ele.getAttribute('data-id');
|
||
wereadBridge.handleWithRichEditor("onGotoBookDetail", {"data-id": id}, "", "");
|
||
}
|
||
```
|
||
|
||
**图片交互**:
|
||
```javascript
|
||
RDisplay.onImageClick = function(ele) {
|
||
var src = ele.getAttribute('src');
|
||
var index = ele.getAttribute('index');
|
||
var position = RDisplay.getCoords(ele);
|
||
wereadBridge.handleWithRichEditor("onGotoImageDetail", {
|
||
"src": src,
|
||
"index": index,
|
||
"top": position.top,
|
||
"height": position.height,
|
||
"width": position.width,
|
||
"left": position.left
|
||
}, "", "");
|
||
}
|
||
```
|
||
|
||
**文本提取**:
|
||
```javascript
|
||
RDisplay.getText = function() {
|
||
// 过滤零宽连字符
|
||
// 将书籍替换为[书籍]
|
||
// 将图片替换为[图片]
|
||
return tempDom.innerText;
|
||
}
|
||
```
|
||
|
||
**选择变化追踪**:
|
||
```javascript
|
||
document.addEventListener("selectionchange", function(e) {
|
||
RDisplay.contentChanged(e);
|
||
});
|
||
|
||
RDisplay.contentChanged = function(e) {
|
||
// 检测链接点击
|
||
if (nodeName == 'a') {
|
||
param['link:'] = href;
|
||
param['link-title'] = title;
|
||
param['link-text'] = node.innerText;
|
||
}
|
||
wereadBridge.handleWithRichEditor("onSelectionChange", param, "", "");
|
||
}
|
||
```
|
||
|
||
**DOMContentLoaded通知**:
|
||
```javascript
|
||
document.addEventListener("DOMContentLoaded", function(event) {
|
||
wereadBridge.handleWithRichEditor("onDOMContentLoaded", "", "", "");
|
||
});
|
||
```
|
||
|
||
### 2.7 MediaPlatform.js - 公众号工具函数
|
||
|
||
**路径**: `output/resources/js/MediaPlatform.js` (126行)
|
||
|
||
公众号文章的实用工具函数。
|
||
|
||
**初始化**:
|
||
```javascript
|
||
mediaPlatform.init = function() {
|
||
// 更新空白高度
|
||
// 处理图片空白
|
||
// 保护懒加载
|
||
}
|
||
```
|
||
|
||
**信息提取**:
|
||
```javascript
|
||
mediaPlatform.getAuthor = function() {
|
||
// 为作者名称添加点击处理器
|
||
document.getElementById('js_name').addEventListener('click', ...);
|
||
}
|
||
|
||
mediaPlatform.getMPInfo = function() {
|
||
// 提取标题、缩略图URL、公众号名称
|
||
return {
|
||
title: title,
|
||
thumbnailUrl: thumbnailUrl,
|
||
accountName: accountName
|
||
};
|
||
}
|
||
```
|
||
|
||
**媒体控制**:
|
||
```javascript
|
||
pauseMedia()
|
||
pauseCurrentAudio()
|
||
pauseCurrentVideo()
|
||
```
|
||
|
||
### 2.8 mpForArticle.js - 公众号内容调整
|
||
|
||
**路径**: `output/resources/js/mpForArticle.js` (349行)
|
||
|
||
公众号文章内容的调整和优化。
|
||
|
||
**DOMContentLoaded处理**:
|
||
```javascript
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
adjustMPContent();
|
||
});
|
||
```
|
||
|
||
**内容调整**:
|
||
```javascript
|
||
adjustMPContent = function() {
|
||
// 1. 修复懒加载图片 (data-src → src)
|
||
// 2. 转换OAuth2授权链接为重定向URI
|
||
// 3. 设置视频iframe正确尺寸
|
||
// 4. 移除投票区域、小程序、小应用链接
|
||
// 5. 为<mpvoice>和<qqmusic>创建音频播放器
|
||
}
|
||
```
|
||
|
||
**音频播放器模板**:
|
||
```javascript
|
||
WRAUDIO_TEMPLATE = `
|
||
<div class="audio_container" onclick="...">
|
||
<div class="audio_button"></div>
|
||
<div class="audio_info">
|
||
<div class="audio_title">${title}</div>
|
||
<div class="audio_duration">${duration}</div>
|
||
</div>
|
||
<div class="audio_progress"></div>
|
||
</div>
|
||
`;
|
||
|
||
createWRAudio = function(title, src, duration) {
|
||
// 创建音频播放器DOM
|
||
}
|
||
```
|
||
|
||
### 2.9 mp_interceptor.js - 点击事件拦截器
|
||
|
||
**路径**: `output/resources/js/mp_interceptor.js` (74行)
|
||
|
||
公众号文章的点击事件拦截器。
|
||
|
||
**拦截目标**:
|
||
```javascript
|
||
// 作者点击
|
||
document.getElementById('js_name').addEventListener('click', function(e) {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
wereadBridge.handleWithRichEditor("clickMpInterceptor", {
|
||
contentType: "profile",
|
||
data: authorData
|
||
}, "", "");
|
||
});
|
||
|
||
// 版权信息点击
|
||
document.getElementById('copyright_info').addEventListener('click', function(e) {
|
||
// ...
|
||
});
|
||
|
||
// 公众号资料卡点击
|
||
document.querySelector('MP-COMMON-PROFILE').addEventListener('click', function(e) {
|
||
// ...
|
||
});
|
||
|
||
// 微信链接点击
|
||
document.querySelectorAll('a[href*="weixin.qq.com"]').forEach(function(el) {
|
||
el.addEventListener('click', function(e) {
|
||
wereadBridge.handleWithRichEditor("clickMpInterceptor", {
|
||
contentType: "link",
|
||
data: { href: el.href }
|
||
}, "", "");
|
||
});
|
||
});
|
||
```
|
||
|
||
### 2.10 Readability.js - 文章内容提取
|
||
|
||
**路径**: `output/resources/js/Readability.js` (1775行)
|
||
|
||
Arc90 Readability v1.7.1,用于从网页中提取可读内容。
|
||
|
||
**核心算法**:
|
||
- 基于段落内容的评分系统
|
||
- 类名权重(article, story, content 等正面词 vs comment, meta 等负面词)
|
||
- 链接密度分析
|
||
- 多页支持与自动翻页
|
||
|
||
**使用场景**:
|
||
```javascript
|
||
// 从微信公众号文章中提取正文
|
||
var readable = new Readability(document);
|
||
var article = readable.parse();
|
||
// article.content - 提取的HTML
|
||
// article.title - 文章标题
|
||
// article.author - 作者
|
||
```
|
||
|
||
### 2.11 HtmlGenerator.js - HTML生成器
|
||
|
||
**路径**: `output/resources/js/HtmlGenerator.js` (27行)
|
||
|
||
HTML生成工具,支持nonce注入。
|
||
|
||
```javascript
|
||
generator.generateHtml = function(content, js, css) {
|
||
// 移除headTrap/tailTrap注释
|
||
// 提取nonce: window.__nonce_str
|
||
// 注入JS和CSS
|
||
return html;
|
||
}
|
||
|
||
generator.generateHtmlForMPReview = function(content) {
|
||
// 特殊处理评论版权
|
||
}
|
||
```
|
||
|
||
### 2.12 cssInjector.js - CSS注入器
|
||
|
||
**路径**: `output/resources/js/cssInjector.js` (25行)
|
||
|
||
动态CSS样式管理工具。
|
||
|
||
```javascript
|
||
window.cssInjector = {
|
||
inject: function(json) {
|
||
// 创建/更新<style>元素(按ID)
|
||
var style = document.getElementById(json.id) || document.createElement('style');
|
||
style.id = json.id;
|
||
style.textContent = json.css;
|
||
document.head.appendChild(style);
|
||
},
|
||
remove: function(identifier) {
|
||
// 移除<style>元素(按ID)
|
||
var style = document.getElementById(identifier);
|
||
if (style) {
|
||
style.parentNode.removeChild(style);
|
||
}
|
||
}
|
||
};
|
||
```
|
||
|
||
### 2.13 其他工具脚本
|
||
|
||
#### patch_6.2.1.4.js (7行)
|
||
补丁脚本,修复版权文本大小写:
|
||
```javascript
|
||
// 将 "rights" 和 "reserved" 首字母大写
|
||
WRAboutViewController.viewDidLoad = function() {
|
||
// "All rights reserved" → "All Rights Reserved"
|
||
}
|
||
```
|
||
|
||
#### tencent-news-weread-custom.js (15行)
|
||
腾讯新闻自定义脚本:
|
||
```javascript
|
||
newsInjector.removeUnnecessary = function() {
|
||
// 隐藏"查看全文"提示
|
||
document.querySelectorAll('.detail-link').forEach(function(el) {
|
||
el.style.display = 'none';
|
||
});
|
||
}
|
||
```
|
||
|
||
#### InnerMP.js (27行)
|
||
内部公众号处理:
|
||
```javascript
|
||
(function() {
|
||
// 添加viewport meta标签
|
||
var meta = document.createElement('meta');
|
||
meta.name = 'viewport';
|
||
meta.content = 'width=device-width, initial-scale=1.0';
|
||
document.head.appendChild(meta);
|
||
|
||
// 隐藏标题前的空段落
|
||
// 隐藏第一个h1/h2/h3标题元素
|
||
})();
|
||
```
|
||
|
||
---
|
||
|
||
## 三、字体文件分析
|
||
|
||
### 3.1 字体清单
|
||
|
||
**路径**: `output/resources/fonts/`
|
||
|
||
| 字体文件 | 用途 |
|
||
|----------|------|
|
||
| `SourceHanSerifCN-Medium.ttf` | 默认正文字体(思源宋体) |
|
||
| `Lora-Regular.ttf` | Lora 衬线字体(常规) |
|
||
| `Lora-Italic.ttf` | Lora 衬线字体(斜体) |
|
||
| `PlayfairDisplay-Regular.ttf` | Playfair Display 字体 |
|
||
| `WeReadLS-Regular.otf` | WeRead LS 系列(常规) |
|
||
| `WeReadLS-Medium.otf` | WeRead LS 系列(中等) |
|
||
| `WeReadLS-Bold.otf` | WeRead LS 系列(粗体) |
|
||
| `FZJuZhenXinFang.ttf` | 方正聚珍新仿字体 |
|
||
| `OpenDyslexic-Regular.otf` | 阅读障碍友好字体 |
|
||
| `WeRead-Icon.ttf` | WeRead 图标字体 |
|
||
| `WeRead-Rating-Icon.ttf` | WeRead 评分图标字体 |
|
||
| `WeReadRN-Regular.ttf` | WeRead RN 字体 |
|
||
| `WeChatNumber.ttf` | 微信数字字体 |
|
||
| `SharpGroteskTRIAL*.ttf` | Sharp Grotesk 系列 |
|
||
| `icon_font.ttf` | 通用图标字体 |
|
||
| `weread_iconfont.ttf` | WeRead 图标字体 |
|
||
|
||
### 3.2 字体使用模式
|
||
|
||
**EPUB内容字体栈**:
|
||
```css
|
||
/* 标题和版权页 */
|
||
font-family: "SourceHanSerifCN-Medium";
|
||
|
||
/* 代码块 */
|
||
font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace;
|
||
|
||
/* 拉丁语书籍标题 */
|
||
/* 不设置font-family,使用浏览器默认 */
|
||
|
||
/* 单图片标题 */
|
||
font-family: "FZFSJW--GB1-0";
|
||
```
|
||
|
||
**公众号文章字体栈**:
|
||
```css
|
||
/* 主要内容 */
|
||
font-family: -apple-system;
|
||
|
||
/* 公众号章节 */
|
||
font-family: "Source Han Serif CN";
|
||
```
|
||
|
||
**编辑器字体栈**:
|
||
```css
|
||
/* 预览模式 */
|
||
font-family: -apple-system;
|
||
font-size: 18px;
|
||
line-height: 30px;
|
||
```
|
||
|
||
---
|
||
|
||
## 四、关键发现与实现细节
|
||
|
||
### 4.1 微信读书自定义CSS属性
|
||
|
||
**wr-vertical-center-style**:
|
||
- 值 `1`:`.wr-vertical-center` 类,强制垂直居中
|
||
- 值 `2`:`.bodyPic` 类,正文图片垂直居中样式
|
||
- 原生端通过解析此属性决定垂直对齐方式
|
||
|
||
**weread-page-relate**:
|
||
- 值 `true`:标记元素与页面相关
|
||
- 用于分页计算时的元素识别
|
||
|
||
### 4.2 JS Bridge 通信机制
|
||
|
||
**iframe URL方案**:
|
||
```javascript
|
||
// 发送消息
|
||
iframe.src = 'wereadapijs://dispatch_message/';
|
||
|
||
// 接收结果
|
||
resultIframe.src = 'wereadapijs://private/setresult/' + scene + '&' + encodedData;
|
||
```
|
||
|
||
**消息队列**:
|
||
- 所有消息先进入 `_sendMessageQueue`
|
||
- 原生端通过 `fetchQueue()` 批量获取
|
||
- 每个消息带有唯一 `callbackId` 用于回调匹配
|
||
|
||
**数据编码**:
|
||
- UTF-8编码 → Base64编码 → URL拼接
|
||
- 确保中文等特殊字符正确传输
|
||
|
||
### 4.3 高亮系统架构
|
||
|
||
**基于Rangy的扩展**:
|
||
1. `rangy-core.js` - 核心范围操作
|
||
2. `rangy-textrange.js` - 字符级范围操作(支持图片文本)
|
||
3. `rangy-classapplier.js` - CSS类应用
|
||
4. `rangy-highlighter.js` - 高亮管理(序列化/反序列化)
|
||
5. `weread-highlighter.js` - 微信读书业务逻辑
|
||
|
||
**标注持久化**:
|
||
```javascript
|
||
// 序列化格式
|
||
"type:TextRange|start$end$id$className[|$containerId]"
|
||
|
||
// 反序列化
|
||
highlighter.deserialize(serializedString);
|
||
```
|
||
|
||
### 4.4 TTS实现
|
||
|
||
**文本提取**:
|
||
```javascript
|
||
getTTSText = function(startY) {
|
||
// 从Y坐标位置开始遍历DOM
|
||
// 提取文本节点
|
||
// 返回TTS文本和位置信息
|
||
}
|
||
```
|
||
|
||
**渐进高亮**:
|
||
```javascript
|
||
highlightTTSText = function(text, index, startY) {
|
||
// 移除之前的TTS高亮
|
||
// 根据index计算新的高亮位置
|
||
// 应用tts类
|
||
}
|
||
```
|
||
|
||
### 4.5 富文本编辑器设计
|
||
|
||
**基于contentEditable**:
|
||
- 使用 `document.execCommand` 执行格式化命令
|
||
- 通过 `backuprange`/`restorerange` 保持选择状态
|
||
- 粘贴拦截 → 发送到原生处理
|
||
|
||
**书籍/图片插入**:
|
||
```javascript
|
||
// 插入书籍卡片
|
||
RE.insertBook = function(id, title, author, cover) {
|
||
var html = `<div class="re_bookItem" data-id="${id}">
|
||
<img class="re_bookItem_cover" src="${cover}"/>
|
||
<div class="re_bookItem_title">${title}</div>
|
||
<div class="re_bookItem_author">${author}</div>
|
||
</div>`;
|
||
RE.insertHTML(html);
|
||
}
|
||
```
|
||
|
||
### 4.6 暗黑主题实现
|
||
|
||
**全局覆盖策略**:
|
||
```css
|
||
/* 所有元素强制黑色背景 */
|
||
* {
|
||
background-color: rgba(0, 0, 0, 1) !important;
|
||
}
|
||
```
|
||
|
||
**特殊元素处理**:
|
||
- 业务容器使用 `rgb(28, 28, 29)` 背景
|
||
- 文字颜色统一为 `rgba(180, 180, 182, 1)`
|
||
- 链接颜色为 `rgba(84, 127, 176, 1)`
|
||
|
||
### 4.7 公众号文章处理流程
|
||
|
||
```
|
||
1. 加载HTML内容
|
||
2. MediaPlatform.init() - 初始化
|
||
3. adjustMPContent() - 内容调整
|
||
├── 修复懒加载图片
|
||
├── 转换OAuth链接
|
||
├── 设置视频尺寸
|
||
├── 移除不需要的元素
|
||
└── 创建音频播放器
|
||
4. mp_interceptor.js - 设置点击拦截
|
||
5. weread-highlighter.js - 初始化标注系统
|
||
6. Readability.js - 可选的内容提取
|
||
```
|
||
|
||
### 4.8 代码高亮
|
||
|
||
使用 highlight.js 的 xcode 主题:
|
||
- 白色背景
|
||
- 彩色语法标记
|
||
- 支持多种编程语言
|
||
|
||
### 4.9 视频播放器定制
|
||
|
||
**腾讯视频覆盖**:
|
||
- 自定义控制栏样式
|
||
- 隐藏广告和应用下载提示
|
||
- 使用腾讯CDN雪碧图
|
||
- 设备特定适配(iPhone6/6P)
|
||
|
||
---
|
||
|
||
## 五、依赖关系图
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ Native App (iOS) │
|
||
│ │
|
||
│ ┌───────────────────────────────────────────────────────┐ │
|
||
│ │ WKWebView │ │
|
||
│ │ │ │
|
||
│ │ ┌─────────────────────────────────────────────────┐ │ │
|
||
│ │ │ WeReadApi.js (Bridge) │ │ │
|
||
│ │ │ wereadapijs:// protocol │ │ │
|
||
│ │ └─────────────────────────────────────────────────┘ │ │
|
||
│ │ │ │ │
|
||
│ │ ┌───────────────┼───────────────┐ │ │
|
||
│ │ │ │ │ │ │
|
||
│ │ ▼ ▼ ▼ │ │
|
||
│ │ ┌────────────┐ ┌────────────┐ ┌────────────┐ │ │
|
||
│ │ │ EPUB渲染 │ │ 公众号渲染 │ │ 富文本编辑 │ │ │
|
||
│ │ │ │ │ │ │ │ │ │
|
||
│ │ │ replace.css│ │ MediaPlat │ │ rich_editor│ │ │
|
||
│ │ │ default.css│ │ form.css │ │ .js │ │ │
|
||
│ │ │ dark.css │ │ MPExtra.css│ │ rich_dis │ │ │
|
||
│ │ │ │ │ │ │ play.js │ │ │
|
||
│ │ │ │ │ highlighter│ │ │ │ │
|
||
│ │ │ │ │ .js │ │ │ │ │
|
||
│ │ └────────────┘ └────────────┘ └────────────┘ │ │
|
||
│ │ │ │
|
||
│ │ ┌─────────────────────────────────────────────────┐ │ │
|
||
│ │ │ Rangy Library │ │ │
|
||
│ │ │ core → textrange → classapplier → highlighter │ │ │
|
||
│ │ └─────────────────────────────────────────────────┘ │ │
|
||
│ │ │ │
|
||
│ └───────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
---
|
||
|
||
## 六、总结
|
||
|
||
微信读书的前端资源架构体现了以下设计特点:
|
||
|
||
1. **分层样式系统**:通过 default.css → replace.css/MediaPlatform.css → dark.css 的层叠,实现内容类型和主题的灵活切换
|
||
|
||
2. **原生桥接机制**:WeReadApi.js 通过 iframe URL scheme 实现 JS 与原生的高效通信,支持异步回调
|
||
|
||
3. **扩展的Rangy库**:在标准 Rangy 基础上添加了图片文本支持、高亮合并、序列化等微信读书特有功能
|
||
|
||
4. **完整的标注系统**:支持高亮、想法、好友想法、引用、TTS五种标注类型,每种有独立的样式和交互
|
||
|
||
5. **富文本编辑能力**:rich_editor.js 提供了完整的编辑功能,包括书籍/图片插入、格式化、链接管理等
|
||
|
||
6. **公众号文章优化**:通过 Readability.js 提取内容、mpForArticle.js 调整布局、mp_interceptor.js 拦截交互,实现公众号文章的原生体验
|
||
|
||
7. **多字体支持**:提供思源宋体、Lora、Playfair Display等多种字体选择,支持阅读障碍友好字体
|
||
|
||
8. **性能优化**:选择变化防抖(250ms)、懒加载图片修复、iframe消息队列批量处理等
|
||
|
||
---
|
||
|
||
*分析完成时间: 2026年5月18日*
|
||
*分析文件数量: 25个(23个完整分析,2个因过大无法读取)*
|