阅读视图

发现新文章,点击刷新页面。

📖 小说网站预导航技术全解:秒开下一章 + 浏览器前进/后退支持

一、前言

在小说网站中,点击「下一章」通常需要等待网络请求,体验不佳。为提升用户阅读体验,很多网站采用 预导航(Prefetch / Pre-navigation) 技术:提前获取下一章内容,点击时立即展示。

本文将从三个角度详细介绍实现方式:

  1. HTML <link> 预加载(最简单,零 JS 方案)
  2. JS fetch 主动预加载(可控性更高)
  3. fetch + History API(支持浏览器前进/后退,最佳体验)
  4. 如何通过 history.back() 控制浏览器箭头显示

二、方式一:<link rel="prefetch">

HTML5 提供了原生预取机制,写在 <head> 中:

<link rel="prefetch" href="/chapter2.html" as="document">
  • prefetch:浏览器空闲时预加载资源(下一章 HTML / 图片等)
  • prerender:在后台完整渲染页面,点击时几乎零延迟,但兼容性较差

优点:

  • 实现简单,一行代码搞定
  • 浏览器自动缓存资源

缺点:

  • 无法控制加载时机
  • 不会自动与浏览器前进/后退联动

适用场景: 简单优化,如博客或小说目录页


三、方式二:JS fetch 主动预加载

如果需要更精确的控制,可用 JS fetch

let nextUrl = "/chapter2.html";
let cache = null;

// 页面加载时预取下一章
fetch(nextUrl)
  .then(res => res.text())
  .then(html => cache = html);

// 点击下一章
document.getElementById("next").addEventListener("click", (e) => {
  e.preventDefault();
  if (cache) {
    document.getElementById("content").innerHTML = cache;
  } else {
    location.href = nextUrl;
  }
});

优点:

  • 精确控制加载时机
  • 可缓存多章内容

缺点:

  • 浏览器前进/后退按钮失效(未写入历史栈)

四、方式三:fetch + History API(推荐🔥)

为了让前进/后退按钮也能秒开,需要结合 pushStatepopstate

const contentEl = document.getElementById("content");
let preloadCache = {};

// 初始化历史栈
history.replaceState({ html: contentEl.innerHTML }, "", location.href);

// 预取下一章
function preload(url) {
  if (preloadCache[url]) return;
  fetch(url)
    .then(res => res.text())
    .then(html => {
      const doc = new DOMParser().parseFromString(html, "text/html");
      preloadCache[url] = doc.querySelector("#content").innerHTML;
    });
}

// 点击切换章节
document.querySelectorAll("a.nav").forEach(link => {
  link.addEventListener("click", async (e) => {
    e.preventDefault();
    const url = link.href;

    let newContent = preloadCache[url];
    if (!newContent) {
      const res = await fetch(url);
      const html = await res.text();
      const doc = new DOMParser().parseFromString(html, "text/html");
      newContent = doc.querySelector("#content").innerHTML;
    }

    contentEl.innerHTML = newContent;
    history.pushState({ html: newContent }, "", url);

    // 顺便预取下一章
    const next = link.nextElementSibling?.href;
    if (next) preload(next);
  });
});

// 浏览器前进/后退
window.addEventListener("popstate", (e) => {
  if (e.state && e.state.html) {
    contentEl.innerHTML = e.state.html;
  } else {
    location.reload();
  }
});

优点:

  • 点击下一章秒开
  • 前进/后退按钮秒开
  • 可扩展多章预取策略

缺点:

  • 需要额外 JS 逻辑
  • 刷新页面仍需服务端响应

五、控制浏览器箭头可用

  • 浏览器前进按钮只有在 有可前进历史 时才激活
  • pushState 不会自动激活前进按钮
  • 可行方法:先创建多条历史,然后调用 history.back()
// 初始化
history.replaceState({id:1},"","#1");
history.pushState({id:2},"","#2");

// 此时前进按钮仍灰色
history.back(); // 后退到第1章,前进按钮可用

JS 无法直接让浏览器箭头激活,只能通过创建历史并后退实现


六、方案对比

方案 代码复杂度 可控性 前进/后退支持 适用场景
<link rel="prefetch"> 极低 简单优化
JS fetch 定制预取策略
fetch + History API 小说网站 / 阅读器

七、进一步优化思路

  • 条件预取:根据 navigator.connection.saveData 判断是否省流量
  • 多章节预取:提前缓存后两章,实现点击前进秒开
  • Service Worker:离线缓存,断网也能阅读
  • 无限滚动:直接拼接章节,去掉翻页操作

八、总结

  • 最简单方式<link rel="prefetch">,无需 JS
  • 更灵活方式:JS fetch,可缓存下一章
  • 最佳体验方式fetch + History API,前进/后退全支持

👉 对于小说网站,推荐第三种方案,配合预加载和缓存,可实现媲美本地阅读器的流畅体验

CSS 里的斜杠 /:你可能忽略的小细节

在日常写 CSS 时,很多人以为 / 只是数学除号,或者是可以随便用来分隔的符号。但实际上,它在 CSS 中有两种很重要的用法

  1. 作为语法分隔符,区分不同类型的值。
  2. 作为数学运算符,表示除法或比例。

我们一起来看看常见的场景。


1. 斜杠作为分隔符

有些 CSS 简写属性的语法,必须用 / 来区分不同部分,否则浏览器就分不清。

字体简写:字号 / 行高

p {
  font: 16px/1.5 "Arial", sans-serif;
}

这里的 16px/1.5 表示 字号是 16px,行高是 1.5。如果你把 / 去掉写成 16px 1.5,浏览器会直接报错。


圆角:椭圆半径

.card {
  border-radius: 50% / 30%;
}

这里 / 的意思是 横向半径 50%,纵向半径 30% ,结果就是一个椭圆角。
如果没有 /,写成 50% 30%,那就成了「左上=50%,右上=30%,右下=50%,左下=30%」,完全不同的效果。


Grid 布局:区域范围

.item {
  grid-area: 1 / 2 / 3 / 5;
}

这里的四个数字依次是:行起点 / 列起点 / 行终点 / 列终点
每个 / 把行和列的定义分隔开,否则就混淆了。


背景图:位置 / 尺寸

.hero {
  background: url(bg.jpg) center / cover no-repeat;
}

这里 center / cover 的意思是:位置居中,尺寸为 cover
如果没有 /,浏览器会把 cover 当成颜色或其他属性,结果完全不对。


边框图像:三段式写法

.box {
  border-image: url(border.png) 30 / 12 / 6 stretch;
}

这里的 / 分别把 slice / width / outset 分开。少一个斜杠就会解析出错。


2. 斜杠作为比例或除法

除了分隔符,/ 还可以真正做「数学」。

宽高比

.video {
  aspect-ratio: 16 / 9;
}

这是最常见的用法,16 / 9 就等于「16:9 的宽高比」。


calc() 计算

.col {
  width: calc(100% / 3);
}

这里 100% / 3 表示把父容器宽度平均分成三份。
要注意:只能用数字去除单位值,像 1rem / 2rem 是不合法的。


3. 小结

可以把 CSS 里的 / 记成两类:

  • 分隔符:用在简写属性里,把不同语义的值分开。

    • 代表场景:fontborder-radiusgrid-areabackgroundborder-imagemask
  • 比例/除法:用在需要算比例或分配空间的地方。

    • 代表场景:aspect-ratiocalc()

一句口诀:
👉 在简写里,/ 是语法规定的“隔板”;在计算里,/ 是数学里的“除法”。

Nuxt 3 微前端:模块导入导出与路由跳转实战

在 Nuxt 3 中使用微前端(Micro Frontend, MFE)可以实现模块化、按需加载和跨应用路由跳转。相比 Nuxt 2,Nuxt 3 提供了原生 Composition API、Vite 支持以及更灵活的模块系统,使得微前端集成更方便。


1. 概念

  • 主应用(Host) :负责加载和渲染子应用,管理全局路由与状态。
  • 子应用(Remote) :独立部署的模块化 Nuxt 3 应用,可通过 Module Federation 暴露组件或页面。
  • Module Federation:Webpack 5 的微前端实现方案,允许子应用暴露模块,主应用按需加载。
  • 路由跳转:微前端下,需处理主应用和子应用之间的路由通信。

2. 原理

2.1 子应用导出模块

Nuxt 3 配合 Webpack Module Federation,通过 exposes 导出组件:

// remoteApp/nuxt.config.ts
import { defineNuxtConfig } from 'nuxt/config'
import { ModuleFederationPlugin } from 'webpack'

export default defineNuxtConfig({
  build: {
    extend(config) {
      config.plugins?.push(
        new ModuleFederationPlugin({
          name: 'remoteApp',
          filename: 'remoteEntry.js',
          exposes: {
            './Widget': './components/Widget.vue'
          },
          shared: ['vue', 'vue-router']
        })
      )
    }
  }
})
  • name:子应用名称。
  • filename:远程入口文件。
  • exposes:导出的组件列表。
  • shared:共享依赖,避免重复打包。

2.2 主应用导入模块

主应用通过 remotes 引入子应用组件:

// hostApp/nuxt.config.ts
import { defineNuxtConfig } from 'nuxt/config'
import { ModuleFederationPlugin } from 'webpack'

export default defineNuxtConfig({
  build: {
    extend(config) {
      config.plugins?.push(
        new ModuleFederationPlugin({
          remotes: {
            remoteApp: 'remoteApp@http://localhost:3001/remoteEntry.js'
          },
          shared: ['vue', 'vue-router']
        })
      )
    }
  }
})

主应用可以直接使用子应用暴露的组件:

<script setup lang="ts">
import Widget from 'remoteApp/Widget'
</script>

<template>
  <div>
    <h1>Host App</h1>
    <Widget />
  </div>
</template>

2.3 路由跳转

Nuxt 3 使用 Composition API 的 useRouteruseRoute 进行路由操作。微前端中:

  1. 主应用内部跳转
const router = useRouter()
router.push('/dashboard')
  1. 子应用跳转到主应用或其他子应用
// 子应用中
function navigateToHost(path: string) {
  window.dispatchEvent(new CustomEvent('navigate', { detail: path }))
}

// 主应用中
window.addEventListener('navigate', (e: any) => {
  const router = useRouter()
  router.push(e.detail)
})

3. 对比

功能 Nuxt 3 单体 Nuxt 3 微前端
模块拆分 不支持 支持按子应用拆分
部署 单包 子应用独立部署
路由 全局 需协调主子应用路由
组件复用 受限 可跨应用导入

4. 实践示例

4.1 创建子应用(Remote)

npx nuxi init remoteApp
cd remoteApp
npm install
npm install -D webpack webpack-cli
  • 配置 nuxt.config.ts 添加 Module Federation。
  • 暴露组件 Widget.vue
  • 启动服务:npm run dev,生成 remoteEntry.js

4.2 创建主应用(Host)

npx nuxi init hostApp
cd hostApp
npm install
npm install -D webpack webpack-cli
  • 配置 nuxt.config.ts 添加 remotes
  • 在页面中引入 <Widget />
  • 添加全局事件监听处理子应用路由跳转。

5. 拓展功能

  1. 状态共享:通过 Pinia 或 Vue 3 的 provide/inject 实现主子应用状态共享。
  2. 权限控制:主应用统一管理路由权限,子应用仅渲染组件。
  3. 懒加载:使用动态 import 按需加载子应用,减少主应用首屏压力。
  4. 多子应用组合:支持多个微前端模块组合成复杂系统。

6. 潜在问题

  • CSS 冲突:子应用样式可能污染主应用,建议使用 Scoped 或 CSS Module。
  • 路由冲突:子应用路由与主应用冲突时,需要命名空间或前缀处理。
  • 依赖版本冲突:Vue/Nuxt 版本需保持兼容。
  • 性能开销:过多子应用增加网络请求和运行时开销。

7. 思路图示

+-------------------+      +-------------------+
|    Host App       |      |    Remote App     |
|                   |      |                   |
| +---------------+ |      | +---------------+ |
| | Nuxt Router   |<-----> | | Widget.vue    | |
| +---------------+ |      | +---------------+ |
|                   |      |                   |
| <Widget />        |      | remoteEntry.js    |
+-------------------+      +-------------------+
❌