PWA 应用在多颜色主题站点下,顶部状态栏颜色自定义初探
技术frontend-dev, react-hooks, dom, safari, ui, pwa

PWA 应用在多颜色主题站点下,顶部状态栏颜色自定义初探

Published On
| 更新于
Updated On
15分钟阅读概述:本文简要探究了 PWA 应用顶部状态栏的颜色设置方案,以及给出了多颜色主题站点下如何配置的可行方案。

背景

吾辈有需求:将自己的独立站点,可以在 移动端浏览器 Safari 菜单中配置添加到主屏幕后,能够以伪桌面 App 的效果打开运行。

其中,状态栏的颜色需要匹配当前的颜色主题,给用户/阅读者更好的 UI 体验。 另外,该需求个人一开始就明确需要支持,不显突兀的状态栏颜色能更好的带来页面沉浸感。

这就需要涉及 PWA(渐进式 Web 应用) 的概念和一些简要配置。

以下是一些当中经历/细节体察/或搜索成果的简要记录:

移动端浏览器 Safari 默认效果

当没有设置 PWA 配置相关时,在 Safari (目前自用可测试版本为 Safari v17,该版本对 PWA 各方面相关都已经有了很好的支持),将自己的站点在手机上以最原始的网页形式打开,可以观察到,状态栏的颜色很好的跟随页面主题颜色作了自适应。(此时个人代码中并不涉及 <meta name="theme-color" content="???" /> 的显式设置)。

但是如果没有 theme-color 或者 apple-mobile-web-app-status-bar-style 的显式设置,则创立桌面 PWA 并打开,开发者会发现:状态栏的颜色不能再如同网页上的打开效果:自动更随页面主题颜色。(当然后续不排除得到官方更新的原生支持)

正题

服务于 Apple Web App 的配置

在头部标签 <head /> 下,当没有 meta 标签关于 PWA 应用的显式声明时,PWA 桌面打开效果主要以 Web app manifests (文件名常见site.webmanifest或其他同作用的配置文件如manifest.json)为参考。该文件下有如下核心字段:

{
  "name": "name",
  "short_name": "name",
  "description": "description",
  "icons": [
    {
      "src": "icons/32x32.png",
      "sizes": "32x32",
      "type": "image/png"
    },
    ...
  ],
  "theme_color": "#364151",
  "background_color": "#364151",
  "display": "standalone"
}

其中 theme_color 影响到 WebApp 状态栏的颜色。

而此时如果将该字段删除后的 site.webmanifest 文件添加到项目中,则伪桌面应用打开时,不能保留上面提到的 状态栏的颜色很好的跟随网页主题颜色作了自适应这样的效果。所以需要保留该字段。

以上的对于网页单主题,或者无主题颜色的情况,如上的配置就能够很好的支持了。

meta 标签语义配置同 site.webmanifest 优先级

前一节特别提到:“在头部标签 <head /> 下当没有 meta 标签关于 PWA 应用的显式声明时”,PWA 应用打开效果主要参考 Web app manifests 独立配置文件。

meta 标签语义配置优先级高过Web app manifests这样的独立配置文件,所以你可以如下的声明,服务于自己的 PWA 应用状态栏定义

<head>
  <meta name="theme-color" content="#000000" />
</head>

当然,进一步的,如果站点本身有一套简单的明暗主题切换情况的话,如下声明即可很好的支持该需求

<head>
  <meta
    name="theme-color"
    media="(prefers-color-scheme: dark)"
    content="#000000"
  />
  <meta
    name="theme-color"
    media="(prefers-color-scheme: light)"
    content="#ffffff"
  />
</head>

! 但是额外需要注意的是,我们依然需要必要的Web app manifests这样的独立配置文件作为 PWA 应用主题色指定的兜底措施。原因显而易见,来自于版本支持问题:Safari 浏览器从最近的 v15 版本才开始支持该类的 meta 标签语义配置兼容/支持表格可查

服务于 PWA / Apple WebApp 的过时、不推荐配置。

本节可不作阅读,只是对检索结果的必要记录

在搜查对比 PWA 状态栏主题色配置方案中,一些过时的检索结果不可避免的出现在视线内。比如要对站点多主题颜色的情况作支持,部分检索结果给出这样的配置方案(年代久远,可以理解为是当时在不支持 meta#themeColor 配置下的无奈之举)

主要依赖 apple-mobile-web-app-status-bar-style 的 meta 语义配置标签。(官方配置文档可见

那么如何支持多主题颜色的情况呢?如下配置:

<meta
  name="apple-mobile-web-app-status-bar-style"
  content="black-translucent"
/>

经本人测试,该配置有明显对 UI 减分的效果出现,black-translucent 该名称显然的,将状态栏设置为透字的透明效果,上机效果为:页面的文字会随滚动出现在状态栏底部,观感十分不好。

所以,如果你正打算配置字段如下:

<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<!-- or -->
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
<!-- or -->
<meta
  name="apple-mobile-web-app-status-bar-style"
  content="black-translucent"
/>

除去 content="default"(该配置即保留 ios 默认的状态栏主题风格,并能够跟随系统的深色模式切换),应该必要了解, meta#themeColor 配置是对 meta#apple-mobile-web-app-status-bar-style 的完美替代。且也能服务于其他可能需部署平台(Android#Chrome)

多颜色主题风格站点下,初始 meta#themeColor 声明的局限性

如上推荐的一种初始的配置声明中

<head>
  <meta
    name="theme-color"
    media="(prefers-color-scheme: dark)"
    content="#000000"
  />
  <meta
    name="theme-color"
    media="(prefers-color-scheme: light)"
    content="#ffffff"
  />
</head>

也只能简单的支持明暗两套主题的切换情况。但如果是对于多颜色主题风格站点,可能就不是那么适合,或者说,简单两种状态栏的颜色配置不能满足吾辈对页面沉浸感方面的要求。

📌 meta#themeColor 动态赋初始值的实践

首先的,我们依然需要保留头部标签 <head /> 下的 meta#themeColor 静态内容标签,便于之后的 DOM 查询操作document.querySelector("meta[name='theme-color']")

<head>
  <meta name="theme-color" content="cyan" />
</head>

如下代码以 React + TypeScript 为例,那么需要充分解决如下两点:

  1. 页面初始加载时,对 meta#themeColor 赋值,赋值匹配当前页面颜色主题关联的约定状态栏颜色值。
  2. 当用户切换主题时,需要额外的更新 meta#themeColor 赋值

先给出可以实现该期望效果但是并非最佳的实践方案:

(示例代码中一些类型或字段声明存在缺失,但不影响整体思路呈现,所以不作补充)

// import ...
export const themeCssValMap: Record<ThemeUniqKeyType, string> = {
  default: "#333333",
  dark: "hsl(231.43 14.29% 9.61%)",
  soft: "hsl(36 18.52% 94.71%)",
  loafer: "hsl(0 0% 100%)",
  system: "#364151",
} as const;

export function useHdlMetaThemeOnce() {
  const { theme: curTheme } = useTheme() as { theme: ThemeUniqKeyType };
  React.useEffect(() => {
    const metaT = document.querySelector("meta[name='theme-color']")!;
    if (!metaT) return;

    metaT.setAttribute("content", themeCssValMap[curTheme ?? "system"]);
  }, [curTheme]);
}

❌ 上述实践不可忽视的两个弊端:

虽然上面的 React-Hooks 代码在必要处一次性调用,就能很好的同时处理上面提到的需充分解决的两点需求,但是有不可忽视的两个弊端:

  1. meta#themeColor 初始赋值,人为可预见的,应该是和组件的生命周期、何时挂载到页面上无关的,而 React Hooks 的写法,则将对 meta#themeColor 赋初始值的行为,和该自定义 hook 调用处所在的函数组件作了强绑定,这样会带来的影响是:页面的状态栏的颜色更新总是有一定延迟,总能明显的观察到状态栏颜色有 head>meta#themeColor 中声明的 cyan 颜色显著的变化到约定的主题对应的状态栏颜色值。所以,我们需要将 meta#themeColor 初始赋值更提前/提到非组件的顶层上去执行

  2. meta#themeColor 更新赋值,违背了 React 的最佳实践,即在考虑通过 React.useEffect 执行一些额外的副作用时,应该先考虑,这些副作用是否应该转而由事件回调函数(onClick/onSelect/onCheck)来执行,如此的实践,是减少 React.useEffect 的不必要额外执行。显然的,我们对 meta#themeColor 更新赋值总是发生在用户手动切换主题时,我们可以明确的将该逻辑放置在事件响应回调中,而无需依赖 React.useEffect 对依赖项 theme 值的变更的响应执行。

✔ meta#themeColor 初始赋值的最终实践

这里也假定页面的主题名称有被缓存到 localStorage 中(这样也才能支持我们获取到当前主题名去适配状态栏颜色)

<body>
  <script data-tid="theme-meta-hack-script">
    (function () {
      const themeCssValMap = {
        // ...
      };
      const curTheme = localStorage.theme;
      const $metaT = document.querySelector("meta[name='theme-color']");
      if (!$metaT) return;
      $metaT.setAttribute("content", themeCssValMap[curTheme ?? "system"]);
    })();
  </script>
</body>

“Hack Script” React Component

这里有必要额外补充,如果你的工程项目下,没有提供 Xxx.html 这样的入口级别文件,那么如上的代码插入可能不易实现。就比如吾辈当前站点使用的 NextJS 13 App Router 开发模式下,对开发者提供的可见入口文件为唯一确定的 /app/layout.tsx 下,但也可以通过如下手段在 DOM 树指定处插入上面的 <script /> 标签,唯一带来的代价是可读性较差。

app/layout.tsx

显然的,在组件 TSX/JSX 代码中,我们可以如下插入一段 script 标签:

function SomeHackScript() {
  return (
    <script
      dangerouslySetInnerHTML={{
        __html: `
          (${function () {
            /* ... code here */
          }.toSring()})()
        `,
      }}
    />
  );
}

✔ meta#themeColor 更新赋值的最终实践

假定原本已存在这样一个 React-Hook : const {theme, setTheme} = useTheme(),

那么此刻在事件响应回调中我们调用 setTheme 时,需要额外的定位和操作 meta#themeColor 标签。吾辈最终将该操作和 setTheme 强耦合,导出一自定义 hook 如下:

useThemeEff2Meta.ts

以上。是对“PWA 应用在多颜色主题站点下,顶部状态栏颜色自定义初探”的吾辈简要分享。


更新说明了:

所以更正后:

useThemeEff2Meta.ts
// import ...
function useThemeEff2Meta() {
  const { setTheme, ...rest } = useTheme();
- const metaTDomRef = React.useRef<HTMLMetaElement>(null);

- React.useEffect(() => {
-   metaTDomRef.current = document.querySelector("meta[name='theme-color']")!;
- }, []);

  const setThemeUponMeta = React.useCallback(
    (t: ThemeUniqKeyType) => {
      setTheme(t);
-     if (!metaTDomRef.current) return;
+     const metaT = document.querySelector("meta[name='theme-color']")!;
+     if (!metaT) return;

      /* 映射当前主题的颜色值 */
-       metaTDomRef.current.setAttribute(
+       metaT.setAttribute(
        "content",
        themeCssValMap[t ?? "system"],
      );
    },
    [setTheme],
  );

  return {
    setThemeUponMeta,
    // setTheme,
    ...rest,
  };
}

PWA 应用在多颜色主题站点下,顶部状态栏颜色自定义初探

https://blog.ninoh.cc/blog/a5-meta-theme-color[Copy]
本文作者
ninohx96
创建/发布于
Published On
更新/发布于
Updated On
许可协议
CC BY-NC-SA 4.0

转载或引用本文时请遵守“署名-非商业性使用-相同方式共享 4.0 国际”许可协议,注明出处、不得用于商业用途!分发衍生作品时必须采用相同的许可协议。