Vuetify 的组件分析

2,604 阅读5分钟

Vuetify

在使用一个组件之前,我们要先知道这个组件库做了什么事情,例如组件是如何封装,有哪些组价是我们可以使用的。然后大致了解之后,我们在分析有没有适合我们业务的组件。

Vuetify 目录结构分的也很清楚:

  • compnents 组件,实际的组件代码实现部分
  • directives 指令部分
  • locale 本地化部分
  • mixins 混入部分
  • presets 预设套餐
  • server 服务部分
  • styles 样式部分
  • utils 工具函数部分

Vuetify 目录结构

  • /vuetify/packages/vuetify/src/components

Vuetify 组件输入和引入全部在这个文件里面

export * from './VApp'
export * from './VAppBar'
export * from './VAlert'
// ...

指令

  • vuetify/packages/vuetify/src/directives

Vuetify 的全部自定指令

export { ClickOutside } from './click-outside'
export { Intersect } from './intersect'
export { Mutate } from './mutate'
export { Resize } from './resize'
export { Ripple } from './ripple'
export { Scroll } from './scroll'
export { Touch } from './touch'

国际化

Vuetify 实现的国际化

export { default as af } from './af'
export { default as ar } from './ar'
export { default as ca } from './ca'
//...

perset

  • /vuetify/packages/vuetify/src/presets

vuetify 的一些预设信息,类似于 Vuetify 的组件配置信息

// Styles
import '../../styles/main.sass'

// Locale
import { en } from '../../locale'

// Types
import { VuetifyPreset } from 'vuetify/types/services/presets'

export const preset: VuetifyPreset = {
  breakpoint: {
    // ...
  },
  icons: {
    // ...
  },
  lang: {
    // ...
  },
  rtl: false,
  theme: {
    // ...
    },
    themes: {
      light: {
        // ...
      },
      dark: {
        // ...
      },
    },
  },
}

server

export * from './application'
export * from './breakpoint'
export * from './goto'
export * from './icons'
export * from './lang'
export * from './presets'
export * from './theme'

Vuetify 采用了切面的思想,将一些配置形成切面,类似于 Ioc 的 Service 切面。管理所有的组件所需要的服务。

styles

  • /vuetify/packages/vuetify/src/styles

Vuetify 使用 sass 创建css样式,一下是css管理

@import './tools/_index'
@import './settings/_index'
@import './generic/_index'
@import './elements/_index'
@import './utilities/_index'

一些基本准备工作就是这些,下面我们具体的源码

主入口

  • /vuetify/packages/vuetify/src/index.ts
import * as components from './components'
import * as directives from './directives'
import Vuetify from './framework'

export default Vuetify

const install = Vuetify.install

Vuetify.install = (Vue, args) => {
  install.call(Vuetify, Vue, {
    components,
    directives,
    ...args,
  })
}

if (typeof window !== 'undefined' && window.Vue) {
  window.Vue.use(Vuetify)
}

我们看到 Vuetify 上具有 install 方法,得知使用是 Vue 插件方式实现的组件注册。所以我们在使用 Vuetify 组件时,可直接使用 use 方法全部注册。

  • vuetify/packages/vuetify/src/install.ts
import OurVue, { VueConstructor } from 'vue'
import { VuetifyUseOptions } from 'types'
import { consoleError } from './util/console'

export function install (Vue: VueConstructor, args: VuetifyUseOptions = {}) {
  if ((install as any).installed) return
  (install as any).installed = true

  const components = args.components || {}
  const directives = args.directives || {}

  // 全局注册 vuetify 组件
  for (const name in directives) {
    const directive = directives[name]

    Vue.directive(name, directive)
  }
  
  // 注册组件
  (function registerComponents (components: any) {
    if (components) {
      for (const key in components) {
        const component = components[key]
        if (component && !registerComponents(component.$_vuetify_subcomponents)) {
          Vue.component(key, component as typeof Vue)
        }
      }
      return true
    }
    return false
  })(components)

  if (Vue.$_vuetify_installed) return
  Vue.$_vuetify_installed = true

  // 混入生命周期 beforeCreate,在每个组件上挂载 $vuetify。
  Vue.mixin({
    beforeCreate () {
      const options = this.$options as any

      if (options.vuetify) {
        options.vuetify.init(this, options.ssrContext)
        this.$vuetify = Vue.observable(options.vuetify.framework)
      } else {
        this.$vuetify = (options.parent && options.parent.$vuetify) || this
      }
    },
  })
}
  • /vuetify/packages/vuetify/src/framework.ts

Vuetify 的框架,从下面我们可以看出 Vuetify 的服务就是框架的一个属性。

import { install } from './install'

// Types
import Vue from 'vue'
import {
  UserVuetifyPreset,
  VuetifyPreset,
} from 'vuetify/types/services/presets'
import {
  VuetifyService,
  VuetifyServiceContract,
} from 'vuetify/types/services'

// Services
import * as services from './services'

// 输出 Vuetify 类,在 Vuetify 上挂载属性方法
export default class Vuetify {
  static install = install

  static installed = false

  static version = __VUETIFY_VERSION__

  public framework: Dictionary<VuetifyServiceContract> = {}

  public installed: string[] = []

  public preset = {} as VuetifyPreset

  public userPreset: UserVuetifyPreset = {}

  constructor (userPreset: UserVuetifyPreset = {}) {
    this.userPreset = userPreset

    this.use(services.Presets)
    this.use(services.Application)
    this.use(services.Breakpoint)
    this.use(services.Goto)
    this.use(services.Icons)
    this.use(services.Lang)
    this.use(services.Theme)
  }

  init (root: Vue, ssrContext?: object) {
    this.installed.forEach(property => {
      const service = this.framework[property]

      service.framework = this.framework

      service.init(root, ssrContext)
    })

    this.framework.rtl = Boolean(this.preset.rtl) as any
  }

  use (Service: VuetifyService) {
    const property = Service.property

    if (this.installed.includes(property)) return
    
    this.framework[property] = new Service(this.preset, this as any)
    this.installed.push(property)
  }
}

组件 components

全局混入 mixins 创建组件

混入 mixins 是一个函数,有很多的函数重载,它的实现也很简单。

export default function mixins (...args: VueConstructor[]): VueConstructor {
  return Vue.extend({ mixins: args })
}

其实就是通过 Vue.extend 来传入 options 创建组件构造器,来创建组件构造器。经过尝试 Vue.extend 的 extend 方法是可以连续调用的。其实在 Vuetify 中,本质就是联系的调用 extend 方法来创建 Vue 组件,这样处理的好好处是,明确了我们需要哪些 mixins。 将 mixins 直接凸显出来。

组件结构

封装在一新的方法中进行。我们整体的看 Alert 组件,组件结构也是特别的清晰的:

  • Styles 组件需要的样式
  • Extensions 组件需要的扩展
  • Components 组件需要的依赖组件
  • Mixins 组件依赖的 mixins
  • Utilities 组件依赖的工具函数
  • Types 组件依赖的类型
  • Vue/Component vue 组件。

与 TS 结合

Vuetify 选择了 TypeScript 进行编写。没有使用 class TypeScript,而是使用 Vue.extend, Vue.extend 有良好的 Vue 的 TS 类型检测

组件: transition

首先要搞明白,Vuetify 中的组件没有使用 template,使用了 render 函数。如果你还没有书写 render 函数的形式来组件,快速的熟悉 render 函数写组件的方法。在 transition 是使用函数的方式创建 transition 组件的。源码中提供了创建 transition-group 和 transition 两个函数:

  • createSimpleTransition 创建简单的过渡,没有transitioin-group
  • createJavascriptTransition 创建过渡,可以是 transition-group

在 Vuetify 中间过渡分为了三大类

  • Component specific transitions 组件特定的过渡
    • VCarouselTransition 轮播过渡
    • VCarouselReverseTransition 轮播反向过渡
    • VTabTransition Tab过渡
    • VTabReverseTransition Tab 反向过渡
    • VMenuTransition Menu 过渡
    • VFabTransition Fab 过渡
  • Generic transitions 通用过渡
    • VDialogTransition 对话过渡
    • VDialogBottomTransition 对话底部过渡
    • VFadeTransition 渐隐过渡
    • VScaleTransition 缩放过渡
    • VScrollXTransition X滚动过渡
    • VScrollXReverseTransition X滚动反向过渡
    • VScrollYTransition Y滚动过渡
    • VScrollYReverseTransition Y滚动反向过渡
    • VSlideXTransition X滑动滚动
    • VSlideXReverseTransition X滑动反向过渡
    • VSlideYTransition Y滑动过渡
    • VSlideYReverseTransition Y滑动方向过渡
  • Javascript transitions JavaScript 过渡
    • VExpandTransition 扩展过渡
    • VExpandXTransition 扩展X过渡

组件: VAlert

提示组件,提示组件提供了丰富的颜色配置, 能够配置出各种颜色丰富多彩的 alert 提示内容。

  • 缓存
    • __cachedBorder 缓存 border
    • __cachedDismissible 缓存 dismissible
    • __cachedIcon 缓存 icon
  • 混入
    • Toggleable
    • Themeable
    • Transitionable
    • VSheet
  • 依赖组件
    • VBtnVIcon
  • 生成元素方法
    • genWrapper
    • genContent
    • genAlert

组件: VApp

VApp 是 Vuetify 的入口组件,使用 Vuetify 组件的顶层,必须使用 VApp 组件进行包裹才能正常使用。

mixins: Themeable classs: BEM

{
	attrs: { 'data-app': true },
    domProps: { id: this.id },
}

本质上是渲染了一个带有 slot 的 div 元素。那么他是如何控制整个 Vuetify 的样式的呢?

  • themeClasses
class: {
  'v-application--is-rtl': this.$vuetify.rtl,
  'v-application--is-ltr': !this.$vuetify.rtl,
  ...this.themeClasses,
},
  • 检查 this.$vuetify, 是否进行了初始化。没有初始化,就抛出一个错误: Error: Vuetify is not properly initialized

组件: VAppBar

  • 混入
    • VToolbar
    • Applicationable
    • Scrollable
    • SSRBootable
    • Toggleable
  • 单位转换
    • convertToUnit
  • 指令
    • Scroll
  • 特殊的 render
const render = VToolbar.options.render.call(this, h)
render.data = render.data || {}

特殊的 render,不是直接的 render 而是调用了 VToolbar options 的 render 方法。VToolbar 组件是 Vue.extend 生成的一个 Vue 组件构造器。需要的注意的是 data 不是Vue 中的响应式数据 data。

这里其实给有一个很好的扩展方式,Vue.extend 方法暴露的构造器,在别的组件内扩展的时候使用 calll 方法调用,这个实例的扩展方法。例如,我们也可以用来扩展 classes, style 等内容。

VSheet

VSheet 旨在为 Vuetify 中的其他 paper 组件提供支持。 它旨在用作低级组件。

  • 混入
    • BindsAttrs
    • Colorable 包含了方法 setBackgroundColor
    • Elevatable
    • Measurable
    • Themeable

VToolbar

它通常是站点导航的主要来源。工具栏是一个灵活的容器,可以通过多种方式使用。

  • 扩展
    • VSheet 的基础上进行扩展, 相当于链式的调用 extends 方法进行 options 合并
  • 组件依赖
    • VImg
  • 工具
    • convertToUnit 单位
    • getSlot,获取插槽
    • breaking
export function getSlot (vm: Vue, name = 'default', data?: object | (() => object), optional = false) {
  if (vm.$scopedSlots[name]) {
    return vm.$scopedSlots[name]!(data instanceof Function ? data() : data)
  } else if (vm.$slots[name] && (!data || optional)) {
    return vm.$slots[name]
  }
  return undefined
}

VSheet

Todo

  • trantions
  • VApp
  • VAppBar
  • VSheet
  • VToolbar
  • VAutocomplete
  • VAvatar
  • VBadge
  • VBanner
  • VBottomNavigation
  • VBottomSheet

参考

...