本文为稀土技能社区首发签约文章,14天内制止转载,14天后未获授权制止转载,侵权必究!

1. 前言

咱们好,我是若川。我倾力继续组织了一年每周咱们一同学习200行左右的源码共读活动,感兴趣的能够点此扫码加我微信 ruochuan12 参加。别的,想学源码,极力引荐重视我写的专栏《学习源码全体架构系列》,现在是重视人数(4.1k+人)榜首的专栏,写有20余篇源码文章。

咱们开发事务时经常会运用到组件库,一般来说,许多时候咱们不需要关怀内部完成。但是假如希望学习和深究里面的原理,这时咱们能够剖析自己运用的组件库完成。有哪些优雅完成、最佳实践、前沿技能等都能够值得咱们学习。

比较于原生 JS 等源码。咱们或许更应该学习,正在运用的组件库的源码,由于有助于协助咱们写事务和写自己的组件。

假如是 Vue 技能栈,开发移动端的项目,大多会选用 vant 组件库,现在(2022-10-24) star 多达 20.3k。咱们能够选择 vant 组件库学习,我会写一个组件库源码系列专栏,欢迎咱们重视。

这次咱们来剖析 vant4 新增的暗黑主题是怎么完成的。文章中的 vant4 的版别是 4.0.0-rc.6vant 的核心开发者是@chenjiahan,一直在更新vant 。估计不久后就会发布 vant4 正式版。

暗黑主题如图所示:

vant 4 行将正式发布,支撑暗黑主题,那么是怎么完成的呢

也能够翻开官方文档链接,自行体会。

学完本文,你将学到:

1. 学会暗黑主题的原理和完成
2. 学会运用 vue-devtools 翻开组件文件,并能够学会其原理
3. 学会 iframe postMessage 和 addEventListener 通讯
4. 学会 ConfigProvider 组件 CSS 变量完成对主题的深度定制原理
5. 学会运用 @vue/babel-plugin-jsx 编写 jsx 组件
6. 等等

2. 准备工作

看一个开源项目,榜首步应该是先看 README.md 再看奉献文档 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 git@github.com: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

vant 4 行将正式发布,支撑暗黑主题,那么是怎么完成的呢

这时咱们翻开 http://localhost:5173/#/zh-CN/config-provider

3. 文档网站

翻开后,咱们能够按 F12vue-devtools 来检查vant 官方文档的结构。假如没有装置,咱们能够访问vue-devtools 官网经过谷歌运用商铺去装置。假如无法翻开谷歌运用商铺,能够经过这个极简插件链接 下载装置。

vant 4 行将正式发布,支撑暗黑主题,那么是怎么完成的呢

mobile 端

vant 4 行将正式发布,支撑暗黑主题,那么是怎么完成的呢

3.1 经过 vue-devtools 翻开组件文件

vant 4 行将正式发布,支撑暗黑主题,那么是怎么完成的呢

如图所示,咱们经过 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 榜首篇文章。我会写一个组件库源码系列专栏,欢迎咱们重视。

我倾力继续组织了一年每周咱们一同学习200行左右的源码共读活动,感兴趣的能够点此扫码加我微信 ruochuan12 参加。

别的,想学源码,极力引荐重视我写的专栏《学习源码全体架构系列》,现在是重视人数(4.1k+人)榜首的专栏,写有20余篇源码文章。包括jQueryunderscorelodashvuexsentryaxiosreduxkoavue-devtoolsvuex4koa-composevue 3.2 发布vue-thiscreate-vue玩具vitecreate-vite 等20余篇源码文章。