封面

分享一些增强访客幸福感的一些技巧

我自己写的主题上线到现在,虽然未开源,但也收获了一些访客的赞美。从界面到性能,从内容到照片,看到自己的学习成果被大家赞美,证明自己没有白学习。

我在协议页面明确表示我不接受将本站源码公开或分享给任何一人,但思想还是可以分享的。本站的高性能和功能丰富性仅在页面中说明了,但一直没机会出个教程。接上篇文章的内容,我想给大家分享一些我自己编写主题时踩过的坑和总结出的最优方案,有些可能在网上都找不到相关教程。

UI 设计

我自己不是什么设计师,仅有的审美也在观看上,没有自己设计 UI 的能力,但我会参考。国内外有很多平台,有很多设计风格供我们参考,博客圈内部也有很多优秀设计的博主,可以参考他们的设计。

在设计属于自己网站的风格前,要思考自己真正地需要什么。我个人的要求是风格统一,界面简洁,没有多余元素,对不同宽度的终端都作界面适配,观感一致,体现在本站上。我的另一个要求就是卡片风格,苹果风格,圆角,动效,骨架屏,精美的图标点缀,颜色明亮的配色,体现在我的毕业设计上。

毕业设计首页
毕业设计首页

除此之外,还需要确认自己不需要什么,这里我还是阐述一下我个人不喜欢的 UI 特点。我最厌恶的就是满天飞的屏幕特效,我来你的博客是为了看你的文章,或者是看你自己,而不是满天飞的屏幕特效的。站主要知道,适当的点缀确实可以让网页不再单调,但过于花哨的网站会适得其反。举一个生活中的例子,我们第一次访问你的博客,就好像想和你认识一下,是初次见面。如果你本人穿着夸张,什么大金链子,浓妆,海绵宝宝短裤,那无论是谁,对你的第一印象都会大打折扣。还有些人为了不让卡片遮挡背景图片,将背景设置得过于透明,反而导致文字看不清。

然后呢,就是给网站选一个统一的配色,不要太少也不要太多。一般地说,网页的主要表示形式为文字和图片,而文字的配色都是黑白灰。过于单调的配色,给人的感觉是在看上世纪的黑白默片,所以要适当地添加一些配色。我最推荐的颜色就是蓝色,本博的色系也都是蓝色。我觉得蓝色最适合的原因就是 HTML 的超链接原本就是蓝色,蓝色也是看起来最舒服的颜色。对比度不至于太低,颜色不会太突兀,看起来没那么鲜艳,

不要让网页颜色过于单调
不要让网页颜色过于单调

字体的选择也很重要。本站的风格是简洁,文字占主导地位,留白很大,适合用衬线字体。这类字体的特点就是放大后,字体棱角分明,在擅长使用文字造成视觉冲击的简约版网页,没有字体比它更适合了。如果网页动画丰富,圆角、阴影、卡片占主导地位,那无衬线字体就更适合它。无衬线字体的特点就是放大后不美观,但加粗后还可以,字体缩小时更为美观,适合文字不特别大的网页里。

性能优化

本站采用了很多性能优化的技术,包括 Service Worker、CDN、浏览器缓存,基于 Typecho 接口的本地缓存。

总结一下可能引起网站性能特别差的因素,这里顺着上面图中的网页架构阐述。

服务器和 CDN 的位置 位置在国外,如果没有特殊的网络优化(如 Cloudflare 的优选节点),在大陆能够明显地感受到网络速度的差异。

第三方资源加载 这里的第三方资源代表不和站点处于同一网域下的资源,比如 css、js。一般来说,JavaScript 的初始化逻辑都是在 load 之后完成的(为防止布局偏移,防止 CSS 加载没完成),如果某些资源拖慢了加载进度,导致初始化迟迟不能执行,会降低访客进一步访问的意愿。

数据库查询与 CPU 密集型任务 对于前者,我本人觉得,如果一个页面重复做了 3 次及以上数据库查询,我就会觉得次数很多了,这时我的解决方式就是利用缓存。之前有个小伙伴说我的站点地图缓存太严重,已经过去一天了最后一篇文章还是坐缆车那个,当时设计这个的时候考虑到要进行很多次数据库查询,所以当时想着这个页面应该没有很多人去看,我就自己设计了一天缓存一次。后来我对比了一下不使用缓存和使用缓存两种差异,还真不大,于是我将缓存去掉了。但它内部还是使用了多次数据库查询,我还是要考虑优化一下的。

这里也介绍一下本站在用的性能优化方案。CDN、浏览器缓存等的我就不说了,这里介绍一下不太起眼的 Service Worker

Service Worker

这里我会讲解的很详细,目的是希望读者能够了解它的工作原理,让你真正会用它。

这个之前实际上在 PWA 的文章中讲过。

简单理解,Service Worker 是一个中间人,如果说浏览器缓存实现了基本的缓存策略,那么 Service Worker 就是可以通过 JS 控制浏览器缓存策略,配置缓存优先或是网络优先。

这里介绍一下如何通过 Service Worker 自定义缓存策略,和本站同款。

首先确保你的电脑安装了 nodejs。

然后我们新建一个文件夹,或者叫项目,假如叫 MyWorkbox。

然后我们打开终端,切换到这个文件夹,执行:

npm install --save-dev workbox-webpack-plugin workbox-core workbox-routing workbox-strategies workbox-cacheable-response workbox-expiration

这里又要涉及到 Service Worker 的生命周期:「注册(Register)→ 安装(Install)→ 等待(Waiting)→ 激活(Activate)→ 冗余(Redundant)」五个阶段。

首先,我们需要使用下面的代码注册 Service Worker:

if ('serviceWorker' in navigator) {
  window.addEventListener('load', () => {
    navigator.serviceWorker.register('path/to/sw.js')
      .then(reg => console.log('SW 注册成功,scope:', reg.scope))
      .catch(err => console.error('SW 注册失败:', err));
  });
}

上述代码放到你的页面主 JavaScript 文件中。

上述代码中,register 函数返回一个 Promise,你可以选择使用 thencatch 补充对注册结果的输出,但我自己的话是没有添加的。

然后我们开始编写 sw.js 文件,它是通过上面创建的 MyWorkbox 打包出来的,打包器是 Webpack。

在 MyWorkbox/index.js 中先导入:

import { CacheFirst, NetworkFirst } from "workbox-strategies";
import { CacheableResponsePlugin } from "workbox-cacheable-response";
import { ExpirationPlugin } from "workbox-expiration";
import { setDefaultHandler, setCatchHandler } from "workbox-routing";
import { clientsClaim } from "workbox-core";

简单介绍一下,CacheFirst 是优先从缓存中获取,若失败了再走网络,NetworkFirst 则是反过来。CacheableResponsePlugin 是缓存插件,可指定什么响应才被缓存。ExpirationPlugin 是缓存过期策略,setDefaultHandler 用于设置默认请求策略,setCatchHandler 用于设置请求失败的兜底处理方式。clientsClaim() 是一个钩子,调用后会立刻让 Service Worker 立即接管页面控制权,不需等到刷新,用于代替 self.addEventListener("activate") 的。

然后调用 clientClaim,让 Service Worker 立即接管页面控制权。

self.addEventListener("install", event => {
  self.skipWaiting();
});

clientsClaim();

接下来就是我们的主要逻辑了——创建缓存逻辑。其实主要就两个情况,一个是网络优先,一个是缓存优先。

如果是样式表、JavaScript、图片等静态资源,要用缓存优先,那么代码就这样写。

这里以图片为例。

registerRoute(
  new RegExp(".*.(?:svg|ico|png|webp)"), // 正则匹配所有以 svg、ico、png、webp 结尾的请求
  new CacheFirst({                       // 使用缓存优先策略(优先返回缓存,缓存没有则请求网络)
    cacheName: "image-cache",            // 缓存名称为 "image-cache"
    plugins: [
      new CacheableResponsePlugin({ statuses: [0, 200] }), // 只缓存状态码为 0 或 200 的响应(成功或 opaque)
      new ExpirationPlugin({
        maxEntries: 200,                  // 最多缓存 200 项
        maxAgeSeconds: 30 * 24 * 60 * 60, // 每项缓存最多保存 30 天(单位为秒)
      }),
    ],
  })
);

解释一下。registerRoute() 传递两个参数,第一个就是怎么匹配 URL,可以是字符串,可以是一个正则表达式对象,也可以是一个返回布尔值的函数,可以定义更复杂的逻辑。

第二个参数是指定应用什么策略,这里使用的是缓存优先策略。它内部需要指定一个缓存名称,同时为更细致地定制缓存时长,我们可以通过 plugin 参数规定。

最后我们设置一个所有路由都不匹配时使用的处理策略。

// 默认路由处理
registerRoute(new RegExp("https://cdn.imqi1.com/.*"), args => {
  return fetchWithRetry(args.request); // 使用重试机制
});

// 设置默认策略,未匹配的走 fetchWithRetry
setDefaultHandler(({ request }) => fetchWithRetry(request));

// 如果 fetch 出错也返回兜底
setCatchHandler(({ request }) => {
  return new Response("请求失败,请检查网络", { status: 503 });
});

可以看到,这里有一个 fetchWithRetry 函数,这是我设置的重试策略。具体方式就是设置一个最大重试次数,当第一次请求时遇到错误,就进行第二次请求,以此类推。当超过最大次数后,代表服务不可用,非 Service Worker 的问题,返回一个 503 Service Unavailable 错误。

// 最大重试次数
const MAX_RETRIES = 3;

// 重试机制函数,针对单个文件的请求进行重试
async function fetchWithRetry(request, retries = 0, lastResponse = null) {
  let response;
  try {
    response = await fetch(request);
    return response;
  } catch (error) {
    lastResponse = response; // 记录失败时的响应(如果有)
    if (retries < MAX_RETRIES) {
      return fetchWithRetry(request, retries + 1, lastResponse); // 递归重试
    } else {
      console.error(`${MAX_RETRIES} 次尝试后请求失败:${request.url}`);
      // 返回最后一次获得的响应(如果有)
      return lastResponse || new Response("多次尝试后仍无法加载资源", { status: 503 });
    }
  }
}

这样我们的 Service Worker 就编写完成了,接下来我们用 Webpack 打包。

新建一个 webpack.config.js 文件,写入以下内容。

import path, { resolve as _resolve } from "path";
import { fileURLToPath } from "url";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

export default {
  mode: "production", // 或 development
  entry: "./src/workbox.js", // 你写的 service worker 源文件
  output: {
    filename: "sw.js", // 最终输出文件名
    path: path.resolve(__dirname, "dist"), // 输出目录
  },
  resolve: {
    extensions: [".js"],
  }
};

然后在终端运行下面的命令,打包成 sw.js:

webpack --config webpack.config.js

将生成的 sw.js 上传到和网站同源的目录下,不同域浏览器会忽略,然后用我介绍过的代码注册它就可以了。

进入网站,打开浏览器开发者控制台,前往应用 - Service Worker 查看它是否被注册。

查看 Service Worker 是否被注册
查看 Service Worker 是否被注册

在【网络】选项卡内,搜索 is:service-worker-intercepted,查看哪些请求是通过 Service Worker 发出的。

查看哪些请求是通过 Service Worker 发出的
查看哪些请求是通过 Service Worker 发出的

评论

评论即代表你已阅读并同意评论协议
  1. Teacher Du
    Teacher Du dusays.com

    Service Worker需要经过CF的节点吧?

    5小时前 北京联通
  2. 网友小宋
    网友小宋 xyzbz.cn

    只要不是花里胡哨的,简单点好一些。

    1天前 河南联通
  3. 小彦

    访问速度最重要,第二是内容好看。其他的,花里胡哨的都不重要~

    1天前 广东电信
  4. obaby

    不要太花哨,不要打开速度太墨迹,这样我觉得就可以看了,那些半天打开不开的实在是难绷,另外一个就是评论的验证码,看着那一堆验证码,如果不是特别想写点东西,实在不想评论。

    1天前 山东移动
内容加载中...