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

总结

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

相关推荐
i听风逝夜16 分钟前
Web 3D地球实时统计访问来源
前端·后端
iMonster20 分钟前
React 组件的组合模式之道 (Composition Pattern)
前端
呐呐呐呐呢28 分钟前
antd渐变色边框按钮
前端
元直数字电路验证1 小时前
Jakarta EE Web 聊天室技术梳理
前端
wadesir1 小时前
Nginx配置文件CPU优化(从零开始提升Web服务器性能)
服务器·前端·nginx
牧码岛1 小时前
Web前端之canvas实现图片融合与清晰度介绍、合并
前端·javascript·css·html·web·canvas·web前端
灵犀坠1 小时前
前端面试八股复习心得
开发语言·前端·javascript
9***Y481 小时前
前端动画性能优化
前端
网络点点滴1 小时前
Vue3嵌套路由
前端·javascript·vue.js
牧码岛1 小时前
Web前端之Vue+Element打印时输入值没有及时更新dom的问题
前端·javascript·html·web·web前端