vant 4 暗黑主题源码阅读

前言

项目克隆和启动

1. 按照下面的命令启动项目

shell 复制代码
## 1. 克隆官网库
git clone git@github.com:vant-ui/vant.git
## 2. 安装依赖 
pnpm i
## 3. 启动
pnpm dev

2. 项目界面

源码分析

1. 点击头部主题切换按钮源码分析

  • 主题切换按钮模板代码
    绑定了一个toggleTheme事件
ini 复制代码
     <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>
  • JS逻辑
    获取默认的currentTheme和切换currentTheme的值
    根据currentTheme的值获取动态图标
    监听currentTheme的值变化,给html标签注入van-doc-theme-dark和van-doc-theme-light类, 这2个类中声明了各自的css变量,
javascript 复制代码
  data() {
    return {
      currentTheme: getDefaultTheme() // 读取默认值
    };
  },
  computed() {
    themeImg() {
      // 根据currentTheme 显示不同的图标
      if (this.currentTheme === 'light') {
        return 'https://b.yzcdn.cn/vant/dark-theme.svg';
      }
      return 'https://b.yzcdn.cn/vant/light-theme.svg';
    },
  },
  watch: {
    // 监听currentTheme的值
    currentTheme: {
      handler(newVal, oldVal) {
        // 将newVal存到本地localStorage里面
        window.localStorage.setItem('vantTheme', newVal);
        
        // html标签移除van-doc-theme-${oldVal}类
        document.documentElement.classList.remove(`van-doc-theme-${oldVal}`);
        
        // html标签注入van-doc-theme-${oldVal}类
        document.documentElement.classList.add(`van-doc-theme-${newVal}`);
        
        // 处理内嵌mobile端样式,详情见下面的2.syncThemeToChild源码分析
        syncThemeToChild(newVal);
      },
      immediate: true,
    },
  },
  methods: {
    // 按钮模板代码中绑定的切换按钮事件,更新currentTheme值
    toggleTheme() {
      this.currentTheme = this.currentTheme === 'light' ? 'dark' : 'light';
      }
   }
  • getDefaultTheme逻辑
    如果localStorage里面存在vantTheme值,则返回
    获取系统设置的主题色,如果是黑色系的,则返回dark
javascript 复制代码
export function getDefaultTheme() {
  // 如果localStorage里面存在vantTheme值,则返回
  const cache = window.localStorage.getItem('vantTheme');

  if (cache) {
    return cache;
  }
  // 获取系统设置的主题色,如果是黑色系的,则返回dark
  const useDark =
    window.matchMedia &&
    window.matchMedia('(prefers-color-scheme: dark)').matches;
  return useDark ? 'dark' : 'light';
}
  • css模块
    注入全局css变量
    van-doc-theme-light和van-doc-theme-dark样式内部注入各自的变量
    :root → 全局变量(普通页面用)
    :host → Web Components / Shadow DOM 内部变量 同时写 → 确保样式变量在所有场景里都可用(全局 + 组件)。
css 复制代码
:root,
:host {
  // colors
  --van-doc-black: #000;
  --van-doc-white: #fff;
  --van-doc-gray-1: #f7f8fa;
  --van-doc-gray-2: #f2f3f5;
  --van-doc-gray-3: #ebedf0;
  --van-doc-gray-4: #dcdee0;
  --van-doc-gray-5: #c8c9cc;
  --van-doc-gray-6: #969799;
  --van-doc-gray-7: #646566;
  --van-doc-gray-8: #323233;
  --van-doc-blue: #1989fa;
  --van-doc-green: #07c160;
  --van-doc-purple: #8e69d3;

  // sizes
  --van-doc-padding: 32px;
  --van-doc-row-max-width: 1680px;
  --van-doc-nav-width: 220px;
  --van-doc-border-radius: 20px;
  --van-doc-simulator-width: 360px;
  --van-doc-simulator-height: 620px;
  --van-doc-header-top-height: 64px;

  // fonts
  --van-doc-code-font-family: 'Menlo', 'Source Code Pro', 'Monaco',
    'Inconsolata', monospace;
}
.van-doc-theme-light {
  // text
  --van-doc-text-color-1: var(--van-doc-black);
  --van-doc-text-color-2: var(--van-doc-gray-8);
  --van-doc-text-color-3: #34495e;
  --van-doc-text-color-4: var(--van-doc-gray-6);
  --van-doc-link-color: var(--van-doc-blue);

  // background
  --van-doc-background: #eff2f5;
  --van-doc-background-2: var(--van-doc-white);
  --van-doc-background-3: var(--van-doc-white);
  --van-doc-header-background: #011f3c;
  --van-doc-border-color: var(--van-doc-gray-2);

  // code
  --van-doc-code-color: #58727e;
  --van-doc-code-comment-color: var(--van-doc-gray-6);
  --van-doc-code-background: var(--van-doc-gray-1);

  // blockquote
  --van-doc-blockquote-color: #2f85da;
  --van-doc-blockquote-background: #ecf9ff;
}

.van-doc-theme-dark {
  // text
  --van-doc-text-color-1: var(--van-doc-white);
  --van-doc-text-color-2: rgba(255, 255, 255, 0.9);
  --van-doc-text-color-3: rgba(255, 255, 255, 0.75);
  --van-doc-text-color-4: rgba(255, 255, 255, 0.6);
  --van-doc-link-color: #1bb5fe;

  // background
  --van-doc-background: #202124;
  --van-doc-background-2: rgba(255, 255, 255, 0.06);
  --van-doc-background-3: rgba(255, 255, 255, 0.1);
  --van-doc-header-background: rgba(1, 31, 60, 0.3);
  --van-doc-border-color: #3a3a3c;

  // code
  --van-doc-code-color: rgba(200, 200, 200, 0.85);
  --van-doc-code-comment-color: var(--van-doc-gray-7);
  --van-doc-code-background: rgba(0, 0, 0, 0.24);

  // blockquote
  --van-doc-blockquote-color: #bae6fd;
  --van-doc-blockquote-background: rgba(7, 89, 133, 0.25);
}

2.syncThemeToChild源码分析

Mobile UI是通过iframe内嵌的,外层PC端主题色更新了,如何通知内嵌的Mobile UI更新样式呢 ?具体需要看syncThemeToChild源码

  • syncThemeToChild源码解析

    获取iframe页面元素 把一个回调函数注入到iframeReady函数中去,回调函数中向iframe.contentWindow postMessage一个对象,要求更新主题对象

    外层页面的 window → 控制当前文档
    iframe.contentWindow → 控制 iframe 内部文档的 window

javascript 复制代码
export function syncThemeToChild(theme) {
  const iframe = document.querySelector('iframe');
  if (iframe) {
    iframeReady(() => {
     把一个消息对象发送给 iframe 内的页面,让它去处理
      iframe.contentWindow.postMessage(
        {
          type: 'updateTheme',
          value: theme,
        },
        '*',
      );
    });
  }
}
  • iframeReady源码
    如果isIframeReady变量为true, 则直接调用callback函数,否则将callback函数push到queue中
    在当前window中注入一个message事件,如果监听到iframeReady事件类型,则设置isIframeReady为true且执行queue里面的函数
ini 复制代码
let queue = [];
let isIframeReady = false;

function iframeReady(callback) {
  if (isIframeReady) {
    callback();
  } else {
    queue.push(callback);
  }
}

// 在当前window中注入一个message事件,如果监听到iframeReady事件,则设置isIframeReady为true且执行queue里面的函数
if (window.top === window) {
  window.addEventListener('message', (event) => {
    if (event.data.type === 'iframeReady') {
      isIframeReady = true;
      queue.forEach((callback) => callback());
      queue = [];
    }
  });
} else {
  如果这段代码在iframe内部执行的,则向父级window发送postMessage事件
  window.top.postMessage({ type: 'iframeReady' }, '*');
}
  • mobile端什么时候向父级window传递iframeReady
    也就是mobile端什么执行如下代码?
bash 复制代码
window.top.postMessage({ type: 'iframeReady' }, '*')

如图所示,mobile的router.js会导入iframe-sync.js函数,浏览器就会执行该文件内部的所有顶层代码,所以就实现了mobile发送postMessage事件

  • mobile端APP.vue代码
    mobile端切换主题相关逻辑和PC端类型,watch theme的值变化,然后更新对应的html
xml 复制代码
<template>
  <demo-nav />
  <router-view v-slot="{ Component }">
    <demo-section>
      <keep-alive>
        <component :is="Component" />
      </keep-alive>
    </demo-section>
  </router-view>
</template>

<script>
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 },
    );
  },
};
</script>

<style lang="less">
@import '../common/style/base';

body {
  min-width: 100vw;
  background-color: inherit;
}

.van-doc-theme-light {
  background-color: var(--van-doc-gray-1);
}

.van-doc-theme-dark {
  background-color: var(--van-doc-black);
}

::-webkit-scrollbar {
  width: 0;
  background: transparent;
}
</style>
  • mobile端App.vue获取theme函数useCurrentTheme分析
ini 复制代码
export function useCurrentTheme() {
  // 获取theme默认值并声明为响应式数据
  const theme = ref(getDefaultTheme());
  
  // 当前window监听一个message事件,当获取到data.type === 'updateTheme'时,更新theme的值
  window.addEventListener('message', (event) => {
    if (event.data?.type !== 'updateTheme') {
      return;
    }

    const newTheme = event.data?.value || '';
    theme.value = newTheme;
  });

  return theme;
}

ConfigProvider组件解读

介绍

Vant Weapp 组件通过丰富的 CSS 变量 来组织样式,通过覆盖这些 CSS 变量,可以实现定制主题、动态切换主题等效果。

typescript 复制代码
// 返回一个cssVars变量
function mapThemeVarsToCSSVars(themeVars: Record<string, Numeric>) {
  const cssVars: Record<string, Numeric> = {};
  Object.keys(themeVars).forEach((key) => {
    const formattedKey = insertDash(kebabCase(key));
    cssVars[`--van-${formattedKey}`] = themeVars[key];
  });
  return cssVars;
}


function syncThemeVarsOnRoot(
  newStyle: Record<string, Numeric> = {},
  oldStyle: Record<string, Numeric> = {},
) {
  
  // 在documentElement的style属性中添加prop
  Object.keys(newStyle).forEach((key) => {
    if (newStyle[key] !== oldStyle[key]) {
      document.documentElement.style.setProperty(key, newStyle[key] as string);
    }
  });
  // 移除documentElement的style属性中历史prop
  Object.keys(oldStyle).forEach((key) => {
    if (!newStyle[key]) {
      document.documentElement.style.removeProperty(key);
    }
  });
}



export default defineComponent({
  name,

  props: configProviderProps,

  setup(props, { slots }) {
  
    // style对象
    const style = computed<CSSProperties | undefined>(() =>
      mapThemeVarsToCSSVars(
        extend(
          {},
          props.themeVars,
          props.theme === 'dark' ? props.themeVarsDark : props.themeVarsLight,
        ),
      ),
    );

    if (inBrowser) {
      const addTheme = () => {
        document.documentElement.classList.add(`van-theme-${props.theme}`);
      };
      const removeTheme = (theme = props.theme) => {
        document.documentElement.classList.remove(`van-theme-${theme}`);
      };

      watch(
        () => props.theme,
        (newVal, oldVal) => {
          if (oldVal) {
            removeTheme(oldVal);
          }
          addTheme();
        },
        { immediate: true },
      );
      // 组件激活时添加van-theme-light 或者 van-theme-dark
      onActivated(addTheme);
      // 组件激活时去除van-theme-light 或者 van-theme-dark
      onDeactivated(removeTheme);
      onBeforeUnmount(removeTheme);
        
      // 监听style属性的变化,然后添加到documentElement上面
      watch(style, (newStyle, oldStyle) => {
        if (props.themeVarsScope === 'global') {
          syncThemeVarsOnRoot(
            newStyle as Record<string, Numeric>,
            oldStyle as Record<string, Numeric>,
          );
        }
      });

      watch(
        () => props.themeVarsScope,
        (newScope, oldScope) => {
          if (oldScope === 'global') {
            // remove on Root
            syncThemeVarsOnRoot({}, style.value as Record<string, Numeric>);
          }
          if (newScope === 'global') {
            // add on root
            syncThemeVarsOnRoot(style.value as Record<string, Numeric>, {});
          }
        },
      );

      if (props.themeVarsScope === 'global') {
        // add on root
        syncThemeVarsOnRoot(style.value as Record<string, Numeric>, {});
      }
    }

    provide(CONFIG_PROVIDER_KEY, props);

    watchEffect(() => {
      if (props.zIndex !== undefined) {
        setGlobalZIndex(props.zIndex);
      }
    });

    return () => (
      <props.tag
        class={bem()}
        style={props.themeVarsScope === 'local' ? style.value : undefined}
      >
       // 默认显示default
        {slots.default?.()}
      </props.tag>
    );
  },
});

总结

对主题定制知识点更加清晰,之前对于这方面知识一直是模糊和一知半解的状态,加油拉~

相关推荐
匆叔3 小时前
JavaScript 性能优化实战技术
前端·javascript
子兮曰3 小时前
🚀前端环境变量配置:10个让你少加班的实战技巧
前端·node.js·前端工程化
用户51681661458413 小时前
Uncaught ReferenceError: __VUE_PROD_HYDRATION_MISMATCH_DETAILS__ is not defined
前端·vue.js
huabuyu3 小时前
构建极致流畅的亿级数据列表
前端
小枫学幽默3 小时前
2GB文件传一半就失败?前端大神教你实现大文件秒传+断点续传
前端
熊猫片沃子3 小时前
Vue 条件与循环渲染:v-if/v-else 与 v-for 的语法简介
前端·vue.js
ai产品老杨3 小时前
打破技术壁垒,推动餐饮食安标准化进程的明厨亮灶开源了
前端·javascript·算法·开源·音视频
文心快码BaiduComate4 小时前
来WAVE SUMMIT,文心快码升级亮点抢先看!
前端·后端·程序员
布列瑟农的星空4 小时前
html中获取容器部署的环境变量
运维·前端·后端