likes
comments
collection
share

通过vant主题切换原理来了解vite插件的使用

作者站长头像
站长
· 阅读数 19

本文参加了由公众号@若川视野 发起的每周源码共读活动, 点击了解详情一起参与。这是源码共读的第41期,链接:【若川视野 x 源码共读】第41期 | vant 4 正式发布了,支持暗黑主题,那么是如何实现的呢

阅读本文前,建议先阅读若川的文章《vant 4 即将正式发布,支持暗黑主题,那么是如何实现的呢》进行学习,关于clone代码、运行项目、如何通过vue-devtools打开组件文件等知识本文不再赘述,本文重点分析主题切换的相关源码。

主题切换的原理

如下图所示,绿色框选部分为主题切换按钮,点击可以在浅色风格和深色风格主题之间进行切换:

通过vant主题切换原理来了解vite插件的使用

包括主题切换按钮的源文件为:packages\vant-cli\site\desktop\components\Header.vue,主要代码如下:

<li v-if="darkModeClass" class="van-doc-header__top-nav-item">
  <a
    class="van-doc-header__link"
    target="_blank"
    @click="toggleTheme"
  >
    <img :src="themeImg" />
  </a>
</li>

由template代码可见,切换主题按钮的显示取决于darkModeClass,切换主题的方法为toggleTheme,而不同主题下切换按钮显示不同的图片是由themeImg决定的。

toggleTheme方法用于切换主题(由currentTheme变量所定义),代码很简单易懂:

toggleTheme() {
  this.currentTheme = this.currentTheme === 'light' ? 'dark' : 'light';
},

currentTheme变量的初始值由getDefaultTheme方法调用结果决定的,如下代码所示:

data() {
  return {
    currentTheme: getDefaultTheme(),
  };
},

getDefaultTheme方法定义在文件packages\vant-cli\site\common\iframe-sync.js中,代码如下:

export function getDefaultTheme() {
  const cache = window.localStorage.getItem('vantTheme');

  if (cache) {
    return cache;
  }

  const useDark =
    window.matchMedia &&
    window.matchMedia('(prefers-color-scheme: dark)').matches;
  return useDark ? 'dark' : 'light';
}

getDefaultTheme方法首先从缓存中读取vantTheme, 如果缓存不存在则使用window对象的matchMedia方法,matchMedia() 返回一个新的 MediaQueryList 对象,表示指定的媒体查询字符串解析后的结果,使用方法如下:

if (window.matchMedia("(max-width: 700px)").matches) {
    /* 窗口小于或等于 700 像素 */
} else {
    /*窗口大于 700 像素 */
}

可以由下图知道,当切换模式为黑暗模式时,缓存的vantTheme值会变为dark:

通过vant主题切换原理来了解vite插件的使用

除了在切换模式时改变currentTheme的值之外,也对currentTheme的改变进行监听:

watch: {
  currentTheme: {
    handler(newVal, oldVal) {
      window.localStorage.setItem('vantTheme', newVal);
      document.documentElement.classList.remove(`van-doc-theme-${oldVal}`);
      document.documentElement.classList.add(`van-doc-theme-${newVal}`);
      syncThemeToChild(newVal);
    },
    immediate: true,
  },
},

在currentTheme变换时,首先要将其最新的值存入到缓存中,然后在html元素上移除旧样式类并增加新样式类,如下图所示:

通过vant主题切换原理来了解vite插件的使用

通过vant主题切换原理来了解vite插件的使用

除了在html上增加和移除样式外,在currentTheme变化时还调用syncThemeToChild通知子元素:

export function syncThemeToChild(theme) {
  const iframe = document.querySelector('iframe');
  if (iframe) {
    iframeReady(() => {
      iframe.contentWindow.postMessage(
        {
          type: 'updateTheme',
          value: theme,
        },
        '*'
      );
    });
  }
}

可以看到syncThemeToChild负责同步主题到iframe,通信手段是使用postMessage方法。为了实现完整的通信,接收程序有一个事件监听器,监听 "message" 事件,监听事件通过useCurrentTheme方法实现的:

通过vant主题切换原理来了解vite插件的使用

可以看到useCurrentTheme方法中会监听message事件,然后判断是不是updateTheme事件,如果是则将新的主题值赋值给响应式的theme变量。接下来看一下useCurrentTheme在哪里被使用:

通过vant主题切换原理来了解vite插件的使用

由上图可以看到useCurrentTheme在文件vant\packages\vant-cli\site\mobile\App.vue中被使用了:

import { watch } from 'vue';
import DemoNav from './components/DemoNav.vue';
import { useCurrentTheme } from '../common/iframe-sync';
import { config } from 'site-mobile-shared';

export default {
  components: { DemoNav },

  setup() {
    const theme = useCurrentTheme();

    watch(
      theme,
      (newVal, oldVal) => {
        document.documentElement.classList.remove(`van-doc-theme-${oldVal}`);
        document.documentElement.classList.add(`van-doc-theme-${newVal}`);

        const { darkModeClass, lightModeClass } = config.site;
        if (darkModeClass) {
          document.documentElement.classList.toggle(
            darkModeClass,
            newVal === 'dark'
          );
        }
        if (lightModeClass) {
          document.documentElement.classList.toggle(
            lightModeClass,
            newVal === 'light'
          );
        }
      },
      { immediate: true }
    );
  },
};

如上代码使用了useCurrentTheme方法返回的响应式变量theme,监听theme的变化。如果theme值发生变化时,则修改html元素上的样式类。除此之外,也读取配置文件中的配置config,并根据darkModeClass以及lightModeClass并结合theme的当前值用toggle方法来进行类名的切换。

下面具体研究一下darkModeClass的使用和定义:

darkModeClass的定义

上文中,我们看到Header中(packages\vant-cli\site\desktop\components\Header.vue)使用了darkModeClass来控制主题切换按钮的显示与隐藏。而这个darkModeClass是从父组件(index.vue)传递过来的:

通过vant主题切换原理来了解vite插件的使用

看一下index.vue文件(vant\packages\vant-cli\site\desktop\components\index.vue):

通过vant主题切换原理来了解vite插件的使用

可见index.vue中用到的darkModeClass也是从父组件传来的(App.vue)。看一下App.vue文件(vant\packages\vant-cli\site\desktop\App.vue)的内容:

通过vant主题切换原理来了解vite插件的使用

由上图可知App.vue中使用的darkModeClass是config.site对象的某个属性。而config是从'site-desktop-shared'中导入的。搜索了一下'site-desktop-shared'发现它也不是一个npm包,结合搜索结果,笔者认为这是通过vite的插件机制实现的:

下图中主要展示了vite.site.ts文件(vant\packages\vant-cli\src\config\vite.site.ts中定义的插件vitePluginGenVantBaseCode:

通过vant主题切换原理来了解vite插件的使用

由上图可知,当编译时遇到site-desktop-shared时,会调用genSiteDesktopShared方法,看一下这个方法(vant\packages\vant-cli\src\compiler\gen-site-desktop-shared.ts):

通过vant主题切换原理来了解vite插件的使用

由上图所示,代码中从SRC_DIR读取目录,然后解析这些文档,调用genImportDocuments等方法生成插件处理的代码。下面看一下SRC_DIR的定义,如下图所示:

通过vant主题切换原理来了解vite插件的使用

由上图可知,SRC_DIR为getSrcDir方法的调用结果,而getSrcDir中又调用了getVantConfig,getVantConfig返回的是vantConfig对象,而vantConfig又是getVantConfigAsync的调用结果,getVantConfigAsync方法是导入VANT_CONFIG_FILE路径所指向的文件。VANT_CONFIG_FILE定义如下图所示:

通过vant主题切换原理来了解vite插件的使用

可见VANT_CONFIG_FILE变量定义的是vant.config.mjs(vant\packages\vant\vant.config.mjs)文件的路径:

通过vant主题切换原理来了解vite插件的使用

由上图可以看到vant.config.mjs中定义了darkModeClass和lightModeClass。

下面看一下插件vitePluginGenVantBaseCode被调用的时机:

vitePluginGenVantBaseCode插件的调用

vitePluginGenVantBaseCode 的调用者是getViteConfigForSiteDev(vant\packages\vant-cli\src\config\vite.site.ts):

通过vant主题切换原理来了解vite插件的使用

而getViteConfigForSiteDev的调用者是compileSite(vant\packages\vant-cli\src\compiler\compile-site.ts):

通过vant主题切换原理来了解vite插件的使用

搜索了一下compileSite,发现两个调用处,如下图所示:

通过vant主题切换原理来了解vite插件的使用

以dev.ts(vant\packages\vant-cli\src\commands\dev.ts)为例,看一下其代码:

import { setNodeEnv } from '../common/index.js';
import { compileSite } from '../compiler/compile-site.js';

export async function dev() {
  setNodeEnv('development');
  await compileSite();
}

下面看一下dev.ts的调用者——cli.ts(vant\packages\vant-cli\src\cli.ts):

通过vant主题切换原理来了解vite插件的使用

由上图可知当运行命令run dev的时候会调用cli.ts。在运行vant项目时,会使用pnpm run dev命令,而这个命令实际运行的命令如下图:

通过vant主题切换原理来了解vite插件的使用

也就是运行packages下vant的dev命令,而vant的dev命令如下:

通过vant主题切换原理来了解vite插件的使用

也就是运行vant-cli的dev命令,实际上会执行cli.ts中定义的dev命令,最终就会执行插件vitePluginGenVantBaseCode的代码。

和切换主题有关的还有ConfigProvider,可以阅读文档学习,其原理是使用CSS变量来实现的,若川老师的文章里已经对其进行解读,本文就不做解释了。

总结

本文通过源码分析了vant主题切换的原理,知道了桌面端和移动端共享关于主题切换的信息是通过iframe 的postMessage 以及 addEventListener来实现的。我们也知道黑暗模式是通过配置文件中的darkModeClass字段定义的,而vant在使用这个字段的时候采用的是'import from site-desktop-shared'的方式,本文详细分析了这是通过编写vite插件和运行命令调用插件实现的。

转载自:https://juejin.cn/post/7230838101076852773
评论
请登录