电子书阅读器(Epubjs)

202 阅读3分钟

主要介绍了 Epub.js 库的基本使用,渲染 epub 格式的电子书,展示了元数据、目录、章节切换、分页切换等功能。

Epub.js:基于 JavaScript 的电子书渲染库,专为在浏览器中解析和渲染 EPUB 格式文档设计。

提供开箱即用的电子书功能(文档渲染、持久化存储和分页显示),无需依赖原生应用或插件。

Github 地址:github.com/futurepress…

Demo 地址:github.com/futurepress…

  1. EPUB 解析
    • 解压 EPUB 文件(本质为 ZIP 压缩包)
    • 解析 OPF(内容清单)、NCX(目录结构)、HTML/CSS/图片资源等元数据。
  2. DOM 动态构建
    • 将 EPUB 章节内容(XHTML)转换为标准 HTML 元素
    • 按书籍结构生成层级化 DOM 树,支持复杂排版(如诗歌、表格)
  3. 样式渲染
    • 注入 EPUB 内嵌 CSS 样式,保留原书视觉设计(字体、布局、响应式适配)
    • 支持覆盖默认样式以实现自定义主题
  4. 交互层实现:通过 API 提供功能,如翻页、章节跳转、全文搜索、书签/高亮持久化等等

初始化 epub(滚动模式)

import Epub, { type NavItem, type Book, type Rendition } from "epubjs";

export default function EpubReader({ url = "/epub/moby-dick.epub" }: iProps) {
  const viewerRef = useRef<HTMLDivElement>(null);
  const [book, setBook] = useState<Book | null>(null);
  const [rendition, setRendition] = useState<Rendition | null>(null);
  const [metaData, setMetaData] = useState<any>(null);
  const [menu, setMenu] = useState<NavItem[] | null>(null);

  useEffect(() => {
    if (!url || !viewerRef.current) return;

    // 1. 创建EPUB实例
    const newBook = Epub(url, {
      openAs: "epub", // 明确指定为EPUB格式
    });
    // 2. 配置渲染参数
    const newRendition = newBook.renderTo(viewerRef.current, {
      width: "100%",
      height: "100%", // 高度自适应容器
      spread: "none", // none-禁用跨页视图;always-启用跨页时图
      flow: "scrolled", // 启用滚动模式
    });
    // 3. 初始化渲染
    newRendition.display();

    // 4. 加载元数据
    newBook.loaded.metadata.then((meta) => {
      console.log("metadata", meta);
      setMetaData(meta);
    });
    // 5. 加载目录导航
    newBook.loaded.navigation.then((nav) => {
      console.log("navigation", nav);
      setMenu(nav.toc);
    });

    setBook(newBook);
    setRendition(newRendition);
    return () => {
      newRendition?.destroy();
      newBook?.destroy();
    };
  }, [url]);

  return (
    <Container className="epub-page">
      <div className="epub-left">
        {/* 基础信息 */}
        <div className="info">
          <div>标题:{metaData?.title}</div>
          <div>作者:{metaData?.creator}</div>
          <div>出版社:{metaData?.publisher}</div>
          <div>出版时间:{metaData?.pubdate}</div>
        </div>
        {/* 目录 */}
        {/* 翻页控制按钮 */}
      </div>

      <div className="epub-right">
        <div ref={viewerRef} className="epub-viewer" />
      </div>
    </Container>
  );
}

整体效果如下图所示:

epub-1.png

元数据信息通过newBook.loaded.metadata获取,具体如下图所示:

epub-2.png

创建目录

目录信息通过newBook.loaded.navigation获取

如下图所示,为目录信息:

epub-3.png

epub-4.png

跳转章节,使用rendition.display()

// 当前章节索引
const [curIdx, setCurIdx] = useState(0);
// 切换章节事件
const handleChapter = (value: string) => {
  if (!menu?.length) return;

  // 查找选中章节
  const chapter = menu.find((item) => item.id === value);
  if (!chapter || !rendition) return;

  // 更新索引并跳转
  const index = menu.findIndex((item) => item.id === value);
  setCurIdx(index);
  rendition.display(chapter.href);
};

/* 目录 */
{
  menu && (
    <Select
      options={menu.map((o) => ({ value: o.id, label: o.label }))}
      onChange={handleChapter}
    />
  );
}

章节控制器

// 章节控制器(上一章/下一章)
const handlePageTurn = (direction: number) => {
  // 无数据处理
  if (!menu?.length) return;

  // 边界处理
  const newIndex = curIdx + direction;
  if (newIndex < 0 || newIndex >= menu.length) return;

  // 跳转处理
  const chapter = menu[newIndex];
  setCurIdx(newIndex);
  rendition?.display(chapter.href);
};

/* 章节控制按钮 */
<div className="tools">
  <Button
    type="primary"
    disabled={curIdx === 0}
    onClick={() => handlePageTurn(-1)}
  >
    上一章
  </Button>
  <Button
    type="primary"
    disabled={!menu || curIdx === menu.length - 1}
    onClick={() => handlePageTurn(1)}
  >
    下一章
  </Button>
</div>;

翻页控制器(分页模式)

上面实现的是一次展示一章的内容,滚动渲染(spread: "none", flow: "scrolled"),切换时,也是章节切换。

现在是双页并排显示内容(spread: "always"),使用分页模式(flow: "paginated"),通过上一页/下一页实现翻页。

修改渲染配置:使用分页模式,启用双页布局:

const newRendition = newBook.renderTo(viewerRef.current, {
  width: "100%",
  height: "100%",
  spread: "always", // 启用双页布局
  flow: "paginated", // 使用分页模式
});

增加翻页控制代码:

// 单页翻页功能
const handlePage = (direction: "prev" | "next") => {
  if (!rendition) return;

  if (direction === "prev") {
    // 上一页
    rendition.prev();
  } else {
    // 下一页
    rendition.next();
  }
};

<div className="tools">
  <Button type="primary" onClick={() => handlePage("prev")}>
    上一页
  </Button>
  <Button type="primary" onClick={() => handlePage("next")}>
    下一页
  </Button>
</div>;

epub-5.png