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

1. 前言

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

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

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

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

上一篇是:vant 4 即将正式发布,支撑暗黑主题,那么是怎么完成的呢

这次咱们来学习 loading 组件。

学完本文,你将学到:

1. 学会怎么用 vue3 + ts 开发一个 loading 组件
2. 学会运用 vue-devtools 翻开组件文件,并能够学会其原理
3. 学会运用 @vue/babel-plugin-jsx 编写 jsx 组件
4. 等等

2. 准备工作

看一个开源项目,榜首步应该是先看 README.md 再看奉献文档 github/CONTRIBUTING.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
# 装置依靠,会运转一切 packages 下库房的 pnpm i 钩子 pnpm prepare 和 pnpm i
pnpm i
# Start development
pnpm dev

咱们先来看 pnpm dev 终究履行的什么指令。

vant 项目运用的是 monorepo 结构。检查根途径下的 package.json

vant/package.json => "dev": "pnpm --dir ./packages/vant dev" vant/packages/vant/package.json => “dev”: “vant-cli dev”`

pnpm dev 终究履行的是:vant-cli dev 启动了一个服务。本文首要是学习 loading 组件 的完成,所以咱们就不深入 vant-cli dev 指令了。

3. 从官方文档下手找到 demo 文件

有些小伙伴喜欢在新的项目中去装置组件,再去自行新建 demo 文件去调试预览。一般来说,其实没有必要。开源项目中一般就有 demo 类文件了。直接学习即可,省去新建的费事。

咱们凭着前端人直觉,应该很简单在找到 loading 文件夹的途径:vant/packages/vant/src/loading/demo/index.vue。 但真的是这个途径吗?或许说这个途径是怎么烘托到官方文档中页面上的呢。带着这个疑问,咱们从文档网站下手。

咱们很简单咱们经过 http://localhost:5173/#/zh-CN/loading。 接下来咱们来看,这个页面的对应的文件是在哪里,为啥路由 loading 就能抵达这个页面

3.1 mobile 项目中的 DemoBlock 组件

咱们经过 vue-devtools 能够找到和翻开 DemoBlock 组件文件。也能够经过极简插件下载装置。我从前写过文章剖析原理《据说 99% 的人不知道 vue-devtools 还能直接翻开对应组件文件?本文原理揭秘》

跟着 vant4 源码学习怎么用 vue3+ts 开发一个 loading 组件,仅88行代码

翻开后的文件是这样的,首要传入插槽,还有 cardtitle 特点。

// vant/packages/vant-cli/site/mobile/components/DemoBlock.vue
<template>
  <div class="van-doc-demo-block">
    <h2 v-if="title" class="van-doc-demo-block__title">{{ title }}</h2>
    <div v-if="card" class="van-doc-demo-block__card">
      <slot />
    </div>
    <slot v-else />
  </div>
</template>
<script>
export default {
  name: 'DemoBlock',
  props: {
    card: Boolean,
    title: String,
  },
};
</script>
// 省掉 less

在编辑器左边,咱们能够看到 vant-cli/site 目录结构。

跟着 vant4 源码学习怎么用 vue3+ts 开发一个 loading 组件,仅88行代码

3.2 mobile 项目中的 main.js 主进口文件

咱们很简单看出这便是一个 vue 的项目。咱们翻开 main.js 检查。

// vant/packages/vant-cli/site/mobile/main.js
import { createApp } from 'vue';
import DemoBlock from './components/DemoBlock.vue';
import DemoSection from './components/DemoSection.vue';
import { router } from './router';
import App from './App.vue';
// 模拟 touch
import '@vant/touch-emulator';
// 大局注册 DemoBlock 和 DemoSection 组件,在 demo 文件组件中会用到
window.app = createApp(App)
  .use(router)
  .component(DemoBlock.name, DemoBlock)
  .component(DemoSection.name, DemoSection);
setTimeout(() => {
  window.app.mount('#app');
}, 0);
// https://stackoverflow.com/questions/3885018/active-pseudo-class-doesnt-work-in-mobile-safari/33681490#33681490
document.addEventListener('touchstart', () => {}, {
  passive: true,
});

3.3 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>

检查vue-router 文档,能够发现: <router-view> 暴露了一个 v-slot API,首要运用 <transition><keep-alive> 组件来包裹你的路由组件。 Component: VNodes, 传递给 <component>is prop。 route: 解分出的标准化路由地址。

3.4 mobile 项目中的 rotuer.js 路由文件

// vant/packages/vant-cli/site/mobile/router.js
// 代码有省掉
import { decamelize } from '../common';
import { demos, config } from 'site-mobile-shared';
import { createRouter, createWebHashHistory } from 'vue-router';
const { locales, defaultLang } = config.site;
function getRoutes() {
  // 能够加上 debugger 自行打断点调试
  debugger;
  const routes = [];
  const names = Object.keys(demos);
  const langs = locales ? Object.keys(locales) : [];
  names.forEach((name) => {
      // time-picker timePicker
      const component = decamelize(name);
      if (langs.length) {
        langs.forEach((lang) => {
          routes.push({
            name: `${lang}/${component}`,
            // http://localhost:5173/#/zh-CN/loading
            path: `/${lang}/${component}`,
            // () => import('xxxx/vant/packages/vant/src/loading/demo/index.vue')
            component: demos[name],
            meta: {
              name,
              lang,
            },
          });
        });
      }
      // 代码有省掉
  }
}
export const router = createRouter({
  history: createWebHashHistory(),
  routes: getRoutes(),
  scrollBehavior: (to, from, savedPosition) => savedPosition || { x: 0, y: 0 },
});

跟着 vant4 源码学习怎么用 vue3+ts 开发一个 loading 组件,仅88行代码

按调试点进函数看到截图,能够看到具体代码和途径。

跟着 vant4 源码学习怎么用 vue3+ts 开发一个 loading 组件,仅88行代码

// http://localhost:5173/@id/vant-cli:site-mobile-shared?t=1667051150300
// vant/packages/vant-cli/dist/package-entry.js
// 省掉若干代码...
const Loading = () => import("/@fs/Users/ruochuan/git-source/github-ruochuan12/vant-analysis/vant/packages/vant/src/loading/demo/index.vue?t=1667051150300")

公然,确实是咱们在前文猜测途径 vant/packages/vant/src/loading/demo/index.vue

小结:经过路由 /loading 匹配组件 demos 中的组件 Loading component: () => import('xxx/loading/demo/index.vue')<router-view> 传递给 v-slot Component <component :is="Component" /> 特点烘托,不得不说秒啊。

接着咱们继续来看,loadingdemo 文件。

4. loading demo 文件 loading/demo/index.vue

// vant/packages/vant/src/loading/demo/index.vue
// 代码有省掉
<script setup lang="ts">
import VanIcon from '../../icon';
import VanLoading from '..';
import { useTranslate } from '../../../docs/site';
const t = useTranslate({
  'zh-CN': {
    type: '加载类型',
  },
  'en-US': {
    type: 'Type',
  },
});
</script>
<template>
  <demo-block :title="t('type')">
    <van-loading />
    <van-loading type="spinner" />
  </demo-block>
</template>

vue 文档 component is假如将组件自身传递给 is 而不是其称号,则不需求注册,例如在 <script setup> 中。 也便是为什么不需求注册 VanLoading

demo-block 是在 mobile main.js 进口文件注册的。

window.app = createApp(App)
  .use(router)
  .component(DemoBlock.name, DemoBlock)
  .component(DemoSection.name, DemoSection)

5. loading 进口文件 loading/index.ts

组件源码中的 TS 代码我不会过多解说。没学过TS的小伙伴,引荐学这个TypeScript 入门教程。

// vant/packages/vant/src/loading/index.ts
import { withInstall } from '../utils';
import _Loading from './Loading';
export const Loading = withInstall(_Loading);
export default Loading;
export { loadingProps } from './Loading';
export type { LoadingType, LoadingProps } from './Loading';
export type { LoadingThemeVars } from './types';
declare module 'vue' {
  export interface GlobalComponents {
    VanLoading: typeof Loading;
  }
}

5.1 withInstall 给组件目标增加 install 方法

import { camelize } from './format';
import type { App, Component } from 'vue';
// https://github.com/vant-ui/vant/issues/8302
type EventShim = {
  new (...args: any[]): {
    $props: {
      onClick?: (...args: any[]) => void;
    };
  };
};
export type WithInstall<T> = T & {
  install(app: App): void;
} & EventShim;
// 给传入的options目标增加 install 特点,在 app.use() 时运用。
// 有传参中有 name 特点,则注册 `van-loading` 和 `VanLoading` 两个称号的组件
export function withInstall<T extends Component>(options: T) {
  (options as Record<string, unknown>).install = (app: App) => {
    const { name } = options;
    if (name) {
      // van-loading
      app.component(name, options);
      // VanLoading
      app.component(camelize(`-${name}`), options);
    }
  };
  return options as WithInstall<T>;
}

关于 app.component 的用法,更多能够参考 vue 文档 app-component

咱们来讲讲装置。

5.2 装置 vant

# Vue 3 项目,装置最新版 Vant
npm i vant
# Vue 2 项目,装置 Vant 2
npm i vant@latest-v2
npm dist-tag vant
# alpha: 4.0.0-alpha.4
# beta: 4.0.0-beta.1
# latest-v1: 1.6.28
# latest-v2: 2.12.50
# latest: 3.6.4
# next: 3.4.3
# rc: 4.0.0-rc.6

跟着 vant4 源码学习怎么用 vue3+ts 开发一个 loading 组件,仅88行代码

对应的则是:npmjs.com 上 vant 介绍 的版别。

为啥讲这么具体,由于之前源码共读群里有小伙伴问这些版别装置的问题。

我猜测许多人尽管经常运用 npm,但很少有完好看过 npm 文档的。docs npmjs npm 也是脚手架,假如想看 npm i, npm dist-tag 等源码完成,能够检查它的github 源码库房。

import { createApp } from 'vue';
import { Loading } from 'vant';
const app = createApp();
app.use(Loading);
<van-loading />
<van-loading type="spinner" />
// 也能够
<VanLoading />

关于 app.use 的用法更多能够参考vue 文档 app.use

咱们继续看主文件。

6. 主文件 loading/Loading.tsx

主结构最终便是烘托图标和烘托文字。你或许会感慨本来便是这么简单。

// vant/packages/vant/src/loading/Loading.tsx
// 代码有删减
export default defineComponent({
  name,
  props: loadingProps,
  setup(props, { slots }) {
    // 咱们完全能够在这里打 debugger 调试源码
    // debugger;
    const spinnerStyle = computed(() =>
      extend({ color: props.color }, getSizeStyle(props.size))
    );
    // 烘托图标
    const renderIcon = () => {
      const DefaultIcon = props.type === 'spinner' ? SpinIcon : CircularIcon;
      return (
        <span class={bem('spinner', props.type)} style={spinnerStyle.value}>
          {slots.icon ? slots.icon() : DefaultIcon}
        </span>
      );
    };
    // 烘托文字
    const renderText = () => {
      if (slots.default) {
        return (
          <span
            class={bem('text')}
            style={{
              fontSize: addUnit(props.textSize),
              color: props.textColor ?? props.color,
            }}
          >
            {slots.default()}
          </span>
        );
      }
    };
    return () => {
      const { type, vertical } = props;
      return (
        <div
          class={bem([type, { vertical }])}
          aria-live="polite"
          aria-busy={true}
        >
          {renderIcon()}
          {renderText()}
        </div>
      );
    };
  },
});

有小伙伴或许注意到了,这感觉便是和 react 类似啊。其实 vue 也是支撑 jsx。不过需求装备插件 @vue/babel-plugin-jsx。大局查找这个插件,能够查找到在 vant-cli 中装备了这个插件。

咱们再来看前面完好的,介绍一些东西函数等。

// vant/packages/vant/src/loading/Loading.tsx
import { computed, defineComponent, type ExtractPropTypes } from 'vue';
import {
  extend,
  addUnit,
  numericProp,
  getSizeStyle,
  makeStringProp,
  createNamespace,
} from '../utils';
// [van-loading, ]
// bem 是个函数 circular
const [name, bem] = createNamespace('loading');
// fill(null) 填充是由于 new Array (能够省掉 new 操作符) 生成的数组是空位数组,不填充无法遍历。
const SpinIcon: JSX.Element[] = Array(12)
  .fill(null)
  .map((_, index) => <i class={bem('line', String(index + 1))} />);
const CircularIcon = (
  <svg class={bem('circular')} viewBox="25 25 50 50">
    <circle cx="50" cy="50" r="20" fill="none" />
  </svg>
);
// 导出 LoadingType
export type LoadingType = 'circular' | 'spinner';
// loading 特点
export const loadingProps = {
  size: numericProp,
  type: makeStringProp<LoadingType>('circular'),
  color: String,
  vertical: Boolean,
  textSize: numericProp,
  textColor: String,
};
// 导出类型
export type LoadingProps = ExtractPropTypes<typeof loadingProps>;

咱们来看从 utils 文件引入的函数。

6.1 东西函数 extend

export const extend = Object.assign;

便是取了个 Object.assign 别号。值得提醒的一点是:

Object.assign 是浅复制。参数也能够是 undefined,不会报错。

const props = {
  name: undefined,
  mp: '若川视野',
  desc: '加我微信 ruochuan12 参加每周一起参加源码共读'
};
const person = Object.assign({name: '若川'}, {age: '18'}, props.name);
console.log(person);
// {name: '若川', age: '18'}

6.2 东西函数 addUnit 增加单位

// vant/packages/vant/src/utils/format.ts
import { isDef, isNumeric } from './validate';
export const isDef = <T>(val: T): val is NonNullable<T> =>
  val !== undefined && val !== null;
// '1.11' 字符串算是数字 
export const isNumeric = (val: Numeric): val is string =>
  typeof val === 'number' || /^d+(.d+)?$/.test(val);
// 增加单位,也便是说 20px 省掉单位能够写成 20
export function addUnit(value?: Numeric): string | undefined {
  if (isDef(value)) {
    return isNumeric(value) ? `${value}px` : String(value);
  }
  return undefined;
}

6.3 东西函数 numericProp 数组或字符串特点

export const numericProp = [Number, String];

6.4 东西函数 getSizeStyle 获取款式

export type Numeric = number | string;
import type { CSSProperties } from 'vue';
import { type Numeric } from './basic';
export function getSizeStyle(
  originSize?: Numeric | Numeric[]
): CSSProperties | undefined {
  if (isDef(originSize)) {
    // 假如数组 [10, 20] 前面是 width, 后边是 height
    if (Array.isArray(originSize)) {
      return {
        width: addUnit(originSize[0]),
        height: addUnit(originSize[1]),
      };
    }
    const size = addUnit(originSize);
    return {
      width: size,
      height: size,
    };
  }
}

6.5 东西函数 createNamespace 创立域名空间

// vant/packages/vant/src/utils/create.ts
export function createNamespace(name: string) {
  const prefixedName = `van-${name}`;
  return [
    prefixedName,
    createBEM(prefixedName),
    createTranslate(prefixedName),
  ] as const;
}

剖析完东西函数,咱们就基本剖析完了 loading 组件,代码函数不多,主文件就 88 行。

7. 总结

咱们从剖析找到 demo 文件的方位 loading/demo/index.vue,找到进口文件 loading/index.ts,扼要描绘了装置 vant 对应 npm 版别和 loading 主文件 loading/Loading.tsx 东西函数。

其中 demo 文件方位原理设计的很巧妙。经过路由 /loading 匹配组件 demos 中的组件 Loading component: () => import('xxx/loading/demo/index.vue')<router-view> 传递给 v-slot Component <component :is="Component" /> 特点烘托。

源码也不是咱们幻想中的那么难,耐心学下来一定会有许多收成。

比较于原生 JS 等源码。咱们或许更应该学习,正在运用的组件库的源码,由于有助于协助咱们写事务和写自己的组件。开源项目通常有许多优雅完成、最佳实践、前沿技能等都能够值得咱们借鉴。

假如是自己写开源项目相对耗时耗力,而且短时间很难有很大收益,很简单放弃。而刚开始或许也无法参加到开源项目中,这时咱们能够先从看懂开源项目的源码做起。对于写源码来说,看懂源码相对简单。看懂源码后能够写文章共享回馈给社区,也算是对开源做出一种奉献。重要的是行动起来,学着学着就会发现许多都已经学会,训练了自己看源码的能力。


假如看完有收成,欢迎点赞、评论、共享支撑。你的支撑和必定,是我写作的动力

最终能够继续重视我@若川。这是 vant 第二篇文章。 上一篇是:vant 4 即将正式发布,支撑暗黑主题,那么是怎么完成的呢。我会写一个组件库源码系列专栏,欢迎我们重视。

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

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