vant 4 is about to be officially released and supports the dark theme, so how is it achieved?

This article is the first signed article of the rare earth nuggets technology community. Reprinting is prohibited within 14 days, and reprinting is prohibited without authorization after 14 days. Infringement must be investigated!

1 Introduction

Hello, I'm Wakagawa . We often use component libraries when developing our business. Generally speaking, we don't need to care about internal implementation in many cases. But if we want to learn and delve into the principles inside, we can analyze the implementation of the component library we use. What are the elegant implementations, best practices, cutting-edge technologies, etc. that we can learn from.

Compared to native and JSother source code. Maybe we should learn the source code of the component library we are using, because it helps us to write our business and write our own components.

If it is a Vuetechnology stack, most of the mobile terminal projects will use the vantcomponent library, currently (2022-10-24) staras many as 20.3k. We can choose the vantcomponent library to learn, I will write a series column of component library source code, everyone is welcome to pay attention.

This time we will analyze how the vant4new dark theme is implemented. vant4The version in the article is 4.0.0-rc.6. vantThe core developer is @chenjiahan , which is being updated all the time vant. The vant4official .

The dark theme is shown in the picture:

dark theme

You can also open the official documentation link and experience it yourself.

After reading this article, you will learn:

1. 学会暗黑主题的原理和实现
2. 学会使用 vue-devtools 打开组件文件,并可以学会其原理
3. 学会 iframe postMessage 和 addEventListener 通信
4. 学会 ConfigProvider 组件 CSS 变量实现对主题的深度定制原理
5. 学会使用 @vue/babel-plugin-jsx 编写 jsx 组件
6. 等等
复制代码

2. Preparations

When looking at an open source project, the first step should be to read README.md and then the contribution document github/CONTRIBUTING.md .

不知道大家有没有发现,很多开源项目都是英文的 README.md,即使刚开始明显是为面向中国开发者。再给定一个中文的 README.md。主要原因是因为英文是世界通用的语言。想要非中文用户参与进来,英文是必备。也就是说你开源的项目能提供英文版就提供。

2.1 克隆源码

贡献文档中有要求:You will need Node.js >= 14 and pnpm.

# 推荐克隆我的项目
git clone https://github.com/lxchuan12/vant-analysis
cd vant-analysis/vant

# 或者克隆官方仓库
git clone [email protected]:vant-ui/vant.git
cd vant

# Install dependencies
pnpm i

# Start development
pnpm dev
复制代码

我们先来看 pnpm dev 最终执行的什么命令。

vant 项目使用的是 monorepo 结构。查看根路径下的 package.json

2.2 pnpm dev

// vant/package.json
{
    "private": true,
    "scripts": {
        "prepare": "husky install",
        "dev": "pnpm --dir ./packages/vant dev",
  },
}
复制代码

再看 packages/vant/package.json

// vant/packages/vant/package.json
{
  "name": "vant",
  "version": "4.0.0-rc.6",
  "scripts": {
    "dev": "vant-cli dev",
  },
}
复制代码

pnpm dev 最终执行的是:vant-cli dev 启动了一个服务。本文主要是讲主题切换的实现,所以我们就不深入 vant-cli dev 命令了。

执行 pnpm dev 后,命令终端输入如图所示,可以发现是使用的是目前最新版本的 vite 3.1.8

pnpm-dev-vite

这时我们打开 http://localhost:5173/#/zh-CN/config-provider

3. 文档网站

打开后,我们可以按 F12vue-devtools 来查看vant 官方文档的结构。如果没有安装,我们可以访问vue-devtools 官网通过谷歌应用商店去安装。如果无法打开谷歌应用商店,可以通过这个极简插件链接 下载安装。

VanDocHeader component

mobile 端

VanDocSimulator component

3.1 通过 vue-devtools 打开组件文件

Open the VanDocSimulator component file

如图所示,我们通过 vue-devtools 打开 VanDocSimulator 组件文件。

曾经在我的公众号@若川视野 发起投票 发现有很多人不知道这个功能。我也曾经写过文章《据说 99% 的人不知道 vue-devtools 还能直接打开对应组件文件?本文原理揭秘》分析这个功能的原理。感兴趣的小伙伴可以查看。

我们可以看到 vant/packages/vant-cli/site/desktop/components/Simulator.vue 文件,主要是 iframe 实现的,渲染的链接是 /mobile.html#/zh-CN。我们也可以直接打开 mobile 官网 验证下。

// vant/packages/vant-cli/site/desktop/components/Simulator.vue
<template>
  <div :class="['van-doc-simulator', { 'van-doc-simulator-fixed': isFixed }]">
    <iframe ref="iframe" :src="src" :style="simulatorStyle" frameborder="0" />
  </div>
</template>

<script>
export default {
  name: 'VanDocSimulator',

  props: {
    src: String,
  },
  // 省略若干代码
}
复制代码

3.2 destop 端

和打开 VanDocSimulator 类似,我们通过 vue-devtools 打开 VanDocHeader 组件文件。

打开了文件后,我们也可以使用 Gitlens 插件。根据 git 提交记录 feat(@vant/cli): desktop site support dark mode,查看添加暗黑模式做了哪些改动。

接着我们来看 vant/packages/vant-cli/site/desktop/components/Header.vue 文件。找到切换主题的代码位置如下:

模板部分

// vant/packages/vant-cli/site/desktop/components/Header.vue

<template>
    <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>
复制代码

JS部分

// vant/packages/vant-cli/site/desktop/components/Header.vue

<script>

import { getDefaultTheme, syncThemeToChild } from '../../common/iframe-sync';

export default {
  name: 'VanDocHeader',
  data() {
    return {
      currentTheme: getDefaultTheme(),
    };
  },
    watch: {
        // 监听主题变化,移除和添加样式 class
        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}`);
                // 我们也可以在这里加上debugger自行调试。
                debugger;
                // 同步到 mobile 的组件中
                syncThemeToChild(newVal);
            },
            immediate: true,
        },
    },

    methods: {
        // 切换主题
        toggleTheme() {
          this.currentTheme = this.currentTheme === 'light' ? 'dark' : 'light';
        },
    }
}

</script>
复制代码

3.3 iframe 通信 iframe-sync

上文JS代码中,有 getDefaultTheme, syncThemeToChild 函数引自文件 vant/packages/vant-cli/site/common/iframe-sync.js

文件开头主要判断 iframe 渲染完成。

// vant/packages/vant-cli/site/common/iframe-sync.js

import { ref } from 'vue';
import { config } from 'site-desktop-shared';

let queue = [];
let isIframeReady = false;

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

if (window.top === window) {
  window.addEventListener('message', (event) => {
    if (event.data.type === 'iframeReady') {
      isIframeReady = true;
      queue.forEach((callback) => callback());
      queue = [];
    }
  });
} else {
  window.top.postMessage({ type: 'iframeReady' }, '*');
}
复制代码

后半部分主要是三个函数 getDefaultThemesyncThemeToChilduseCurrentTheme

// 获取默认的主题
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';
}

// 同步主题到 iframe 用 postMessage 通信
export function syncThemeToChild(theme) {
  const iframe = document.querySelector('iframe');
  if (iframe) {
    iframeReady(() => {
      iframe.contentWindow.postMessage(
        {
          type: 'updateTheme',
          value: theme,
        },
        '*'
      );
    });
  }
}

// 接收、使用主题色
export function useCurrentTheme() {
  const theme = ref(getDefaultTheme());

  // 接收到 updateTheme 值
  window.addEventListener('message', (event) => {
    if (event.data?.type !== 'updateTheme') {
      return;
    }

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

  return theme;
}
复制代码

在项目中,我们可以可以搜索 useCurrentTheme 看在哪里使用的。很容易我们可以发现 vant/packages/vant-cli/site/mobile/App.vue 文件中有使用。

3.4 mobile 端

// 模板部分
// vant/packages/vant-cli/site/mobile/App.vue

<template>
  <demo-nav />
  <router-view v-slot="{ Component }">
    <keep-alive>
      <demo-section>
        <component :is="Component" />
      </demo-section>
    </keep-alive>
  </router-view>
</template>
复制代码
// js 部分
// vant/packages/vant-cli/site/mobile/App.vue
<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>

复制代码

上文阐述了浅色主题和暗黑主题的实现原理,我们接着来看如何通过 ConfigProvider 组件实现主题的深度定制。

4. ConfigProvider 组件,深度定制主题

这个组件的文档有说明,主要就是利用 CSS 变量 来实现的,具体可以查看这个链接学习。这里举个简单的例子。

// html
<div id="app" style="--van-color: black;--van-background-color: pink;">hello world</div>
// css
#app {
  color: var(--van-color);
  background-color: var(--van-background-color);
}
复制代码

可以预设写好若干变量,然后在 style 中修改相关变量,就能得到相应的样式,从而达到深度定制修改主题的能力。

比如:如果把 --van-color: black;,改成 --van-color: red; 则字体颜色是红色。 如果把 --van-background-color: pink; 改成 --van-background-color: white; 则背景色是白色。

vant 中有一次提交把之前所有的 less 变量,改成了原生 cssvar 变量。breaking change: no longer support less vars

vantConfigProvider 组件其实就是利用了这个原理。

知晓了上面的原理,我们再来简单看下 ConfigProvider 具体实现。

// vant/packages/vant/src/config-provider/ConfigProvider.tsx
// 代码有省略
function mapThemeVarsToCSSVars(themeVars: Record<string, Numeric>) {
  const cssVars: Record<string, Numeric> = {};
  Object.keys(themeVars).forEach((key) => {
    cssVars[`--van-${kebabCase(key)}`] = themeVars[key];
  });
  // 把 backgroundColor 最终生成类似这样的属性
  // {--van-background-color: xxx}
  return cssVars;
}

export default defineComponent({
  name,

  props: configProviderProps,

  setup(props, { slots }) {
    // 完全可以在你需要的地方打上 debugger 断点
    debugger;
    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 }
      );

      onActivated(addTheme);
      onDeactivated(removeTheme);
      onBeforeUnmount(removeTheme);
    }

    // 插槽
    // 用于 style
    // 把 backgroundColor 最终生成类似这样的属性
    // {--van-background-color: xxx}

    return () => (
      <props.tag class={bem()} style={style.value}>
        {slots.default?.()}
      </props.tag>
    );
  },
});
复制代码

有小伙伴可能注意到了,这感觉就是和 react 类似啊。其实 vue 也是支持 jsx。不过需要配置插件 @vue/babel-plugin-jsx。全局搜索这个插件,可以搜索到在 vant-cli 中配置了这个插件。

5. 总结

我们通过查看 README.md 和贡献文档等,知道了项目使用的 monorepovite 等,pnpm i 安装依赖,pnpm dev 跑项目。

我们学会了利用 vue-devtools 快速找到我们不那么熟悉的项目中的文件,并打开相应的文件。

通过文档桌面端和移动端的主题切换,我们学到了原来是 iframe 渲染的移动(mobile)端,通过 iframe postMessageaddEventListener 通信切换主题。

学会了 ConfigProvider 组件是利用 CSS 变量 预设变量样式,来实现的定制主题。

也学会使用 @vue/babel-plugin-jsx 编写 jsx 组件,和写 react 类似。

相比于原生 JS 等源码。我们或许更应该学习,正在使用的组件库的源码,因为有助于帮助我们写业务和写自己的组件。开源项目通常有很多优雅实现、最佳实践、前沿技术等都可以值得我们借鉴。

如果是自己写开源项目相对耗时耗力,而且短时间很难有很大收益,很容易放弃。而刚开始可能也无法参与到开源项目中,这时我们可以先从看懂开源项目的源码做起。对于写源码来说,看懂源码相对容易。看懂源码后可以写文章分享回馈给社区,也算是对开源做出一种贡献。重要的是行动起来,学着学着就会发现很多都已经学会,锻炼了自己看源码的能力。

如果看完有收获,欢迎点赞、评论、分享支持。你的支持和肯定,是我写作的动力

最后可以持续关注我@若川。这是 vant 第一篇文章。我会写一个组件库源码系列专栏,欢迎大家关注。

In addition, if you want to learn the source code, I highly recommend paying attention to the column I wrote (currently the Nuggets column has the first number of followers, 4.1K+ people) "Learning the overall architecture of the source code" contains jQuery, underscore, lodash, vuex, sentry, axios, redux, koa, vue-devtools, vuex4, koa-compose, vue 3.2 发布, vue-this, create-vue, 玩具viteMore than 20 source code articles.

Guess you like

Origin juejin.im/post/7158239404484460574