通过vant主题切换原理来了解vite插件的使用
本文参加了由公众号@若川视野 发起的每周源码共读活动, 点击了解详情一起参与。这是源码共读的第41期,链接:【若川视野 x 源码共读】第41期 | vant 4 正式发布了,支持暗黑主题,那么是如何实现的呢。
阅读本文前,建议先阅读若川的文章《vant 4 即将正式发布,支持暗黑主题,那么是如何实现的呢》进行学习,关于clone代码、运行项目、如何通过vue-devtools打开组件文件等知识本文不再赘述,本文重点分析主题切换的相关源码。
主题切换的原理
如下图所示,绿色框选部分为主题切换按钮,点击可以在浅色风格和深色风格主题之间进行切换:
包括主题切换按钮的源文件为: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:
除了在切换模式时改变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元素上移除旧样式类并增加新样式类,如下图所示:
除了在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方法实现的:
可以看到useCurrentTheme方法中会监听message事件,然后判断是不是updateTheme事件,如果是则将新的主题值赋值给响应式的theme变量。接下来看一下useCurrentTheme在哪里被使用:
由上图可以看到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)传递过来的:
看一下index.vue文件(vant\packages\vant-cli\site\desktop\components\index.vue):
可见index.vue中用到的darkModeClass也是从父组件传来的(App.vue)。看一下App.vue文件(vant\packages\vant-cli\site\desktop\App.vue)的内容:
由上图可知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:
由上图可知,当编译时遇到site-desktop-shared时,会调用genSiteDesktopShared方法,看一下这个方法(vant\packages\vant-cli\src\compiler\gen-site-desktop-shared.ts):
由上图所示,代码中从SRC_DIR读取目录,然后解析这些文档,调用genImportDocuments等方法生成插件处理的代码。下面看一下SRC_DIR的定义,如下图所示:
由上图可知,SRC_DIR为getSrcDir方法的调用结果,而getSrcDir中又调用了getVantConfig,getVantConfig返回的是vantConfig对象,而vantConfig又是getVantConfigAsync的调用结果,getVantConfigAsync方法是导入VANT_CONFIG_FILE路径所指向的文件。VANT_CONFIG_FILE定义如下图所示:
可见VANT_CONFIG_FILE变量定义的是vant.config.mjs(vant\packages\vant\vant.config.mjs)文件的路径:
由上图可以看到vant.config.mjs中定义了darkModeClass和lightModeClass。
下面看一下插件vitePluginGenVantBaseCode被调用的时机:
vitePluginGenVantBaseCode插件的调用
vitePluginGenVantBaseCode 的调用者是getViteConfigForSiteDev(vant\packages\vant-cli\src\config\vite.site.ts):
而getViteConfigForSiteDev的调用者是compileSite(vant\packages\vant-cli\src\compiler\compile-site.ts):
搜索了一下compileSite,发现两个调用处,如下图所示:
以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):
由上图可知当运行命令run dev的时候会调用cli.ts。在运行vant项目时,会使用pnpm run dev命令,而这个命令实际运行的命令如下图:
也就是运行packages下vant的dev命令,而vant的dev命令如下:
也就是运行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