likes
comments
collection
share

Element Plus - Container布局容器组件源码解析学习

作者站长头像
站长
· 阅读数 15

前言

受历史的前后端不分离代码影响, 平时一直使用JQ和部分Vue2进行业务代码开发,很少接触到Ts和Vue3的开发内容,为了提高Ts和Vue3的熟练度和跳槽准备,故开坑了本专栏的内容(主要是学习组件的实现,每天进步一点点)

组件

git clone github.com/element-plu…

pnpm install

pnpm run dev --引入组件的调试环境

pnpm run docs:dev --组件文档,可以通过Vue-tools确定组件目录

入口

github.com/element-plu…

源码解析

typescript

github.com/element-plu…

import type { AppContext, Plugin } from 'vue'
export type SFCWithInstall<T> = T & Plugin
export type SFCInstallWithContext<T> = SFCWithInstall<T> & {
  _context: AppContext | null
}

withInstall

我之前写过的Message组件的时候有说过这个函数,用于组件本身的注册juejin.cn/post/718285…

github.com/element-plu… 这里有所有注册相关的函数

export const withInstall = <T, E extends Record<string, any>>(
  main: T,
  extra?: E
) => {
  ;(main as SFCWithInstall<T>).install = (app): void => {
    for (const comp of [main, ...Object.values(extra ?? {})]) {
      app.component(comp.name, comp)
    }
  }

  if (extra) {
    for (const [key, comp] of Object.entries(extra)) {
      ;(main as any)[key] = comp
    }
  }
  return main as SFCWithInstall<T> & E
}

传递两个参数,main类型为泛型T,extra是一个对象,通过Object.values 将 extra 中的属性值提取为一个数组, 并进行遍历进行 组件的注册. 如果extra不为空则通过 迭代器遍历 Object.entries 转换后的 二维数组, 将extra所有属性和值 挂载到 main 对象下

SFCWithInstall 通过泛型 将最后返回的 main 的类型 定义为 T & Plugin & E的交叉类型,为并且关系

withNoopInstall

export const withNoopInstall = <T>(component: T) => {
  ;(component as SFCWithInstall<T>).install = NOOP

  return component as SFCWithInstall<T>
}

withNoopInstall 很好理解, 它将使用这个函数调用的组件的install属性 重置为一个空函数了

布局组件Container

  <section :class="[ns.b(), ns.is('vertical', isVertical)]">
    <slot />
  </section>

一个section标签,内部接收插槽的使用

import { computed, useSlots } from 'vue'
import { useNamespace } from '@element-plus/hooks'

import type { Component, VNode } from 'vue'

defineOptions({
  name: 'ElContainer',
})
const props = defineProps({
  /**
   * @description layout direction for child elements
   */
  direction: {
    type: String,
  },
})
const slots = useSlots()

const ns = useNamespace('container')

const isVertical = computed(() => {
  if (props.direction === 'vertical') {
    return true
  } else if (props.direction === 'horizontal') {
    return false
  }
  if (slots && slots.default) {
    const vNodes: VNode[] = slots.default()
    return vNodes.some((vNode) => {
      const tag = (vNode.type as Component).name
      return tag === 'ElHeader' || tag === 'ElFooter'
    })
  } else {
    return false
  }
})

defineOptions 是由 unplugin-vue-macros 提供的对于Vue实验性特性的支持,这里用于组件的Name定义,如果是用Setup写法你可能会需要这样写. (写下文章的时候,Vue3.3已经出来了,并且对于defineOptions 这个API 已经得到了稳定支持 vuejs.org/api/sfc-scr…)

<script lang="ts“ setup></script>
<script>
export default{
    name:'ElContainer'
}
</script>

useNameSpace

packages/hooks/useNameSpace/index.ts

const useNamespace = (block, namespaceOverrides) => {
    const namespace = useGetDerivedNamespace(namespaceOverrides); //得到默认的命名空间 没有声明的情况下默认是 el
    // 使用b函数 生成字符如block为container 生成字符el-container
    const b = (blockSuffix = "") =>
        _bem(namespace.value, block, blockSuffix, "", "");
    // 使用e函数 生成字符如block为container el-container-xxx
    const e = (element) =>
        element ? _bem(namespace.value, block, "", element, "") : "";
    const m = (modifier) =>
        modifier ? _bem(namespace.value, block, "", "", modifier) : "";
    const be = (blockSuffix, element) =>
        blockSuffix && element
            ? _bem(namespace.value, block, blockSuffix, element, "")
            : "";
    const em = (element, modifier) =>
        element && modifier
            ? _bem(namespace.value, block, "", element, modifier)
            : "";
    const bm = (blockSuffix, modifier) =>
        blockSuffix && modifier
            ? _bem(namespace.value, block, blockSuffix, "", modifier)
            : "";
    const bem = (blockSuffix, element, modifier) =>
        blockSuffix && element && modifier
            ? _bem(namespace.value, block, blockSuffix, element, modifier)
            : "";
    //state 判断传入的形参数量是否大于1个  statePrefix is- 比如传的是vertical 就是 is-vertical
    //如果 args有传入则默认则取一个值本身 没传直接就是true
    const is = (name, ...args) => {
        const state = args.length >= 1 ? args[0] : true;
        return name && state ? `${statePrefix}${name}` : "";
    };

    // for css var
    // --el-xxx: value;
    //通过对象遍历返回符合css变量格式的新对象
    const cssVar = (object) => {
        const styles = {};
        for (const key in object) {
            if (object[key]) {
                //--el-xxxx:object[key]
                styles[`--${namespace.value}-${key}`] = object[key];
            }
        }
        return styles;
    };
    // with block
    //通过对象遍历返回符合css变量格式的新对象
    const cssVarBlock = (object) => {
        const styles = {};
        for (const key in object) {
            if (object[key]) {
                //--el-container-xxxx:object[key]
                styles[`--${namespace.value}-${block}-${key}`] = object[key];
            }
        }
        return styles;
    };
    //非对象形式生成
    const cssVarName = (name) => `--${namespace.value}-${name}`;
    const cssVarBlockName = (name) => `--${namespace.value}-${block}-${name}`;

    return {
        namespace,
        b,
        e,
        m,
        be,
        em,
        bm,
        bem,
        is,
        // css
        cssVar,
        cssVarName,
        cssVarBlock,
        cssVarBlockName,
    };
};

const _bem = (
  namespace: string,
  block: string,
  blockSuffix: string,
  element: string,
  modifier: string
) => {
  let cls = `${namespace}-${block}`
  if (blockSuffix) {
    cls += `-${blockSuffix}`
  }
  if (element) {
    cls += `__${element}`
  }
  if (modifier) {
    cls += `--${modifier}`
  }
  return cls
}

useNameSpace 函数会返回一系列用于生成和BEM命名规范的方法,本质上就是对_bem函数的再调用, 如源码中的 ns.b() 根据一开始调用的 container,内部默认的defaultNamespace值为 el,最终会生成类名 el-container, 如果是 ns.be('harexs') 则是 el-container__harexs

ns.is

const statePrefix = 'is-'
const is: {
    (name: string, state: boolean | undefined): string
    (name: string): string
  } = (name: string, ...args: [boolean | undefined] | []) => {
    const state = args.length >= 1 ? args[0]! : true
    return name && state ? `${statePrefix}${name}` : ''
  }
  
  //将其简化一下
  const is = (name, ...args) => {
        const state = args.length >= 1 ? args[0] : true;
        return name && state ? `${statePrefix}${name}` : "";
    };

判断是否有传递第二个参数,有则 取值,没有默认True 然后判断条件成立,为真时 则返回类名is-name,否则空

isVertical

const isVertical = computed(() => {
  if (props.direction === 'vertical') {
    return true
  } else if (props.direction === 'horizontal') {
    return false
  }
  if (slots && slots.default) {
    const vNodes: VNode[] = slots.default()
    return vNodes.some((vNode) => {
      const tag = (vNode.type as Component).name
      return tag === 'ElHeader' || tag === 'ElFooter'
    })
  } else {
    return false
  }
})

用于判断是否要内容垂直排列展示即flex-direction: column;。如果没有显式声明排列方式时,会判断插槽中的子组件的组件名称是否有包含ElHeaderElFooter, 有则返回True

布局组件 Main

<template>
  <main :class="ns.b()">
    <slot />
  </main>
</template>
<script lang="ts" setup>
import { useNamespace } from '@element-plus/hooks'

defineOptions({
  name: 'ElMain',
})

const ns = useNamespace('main')
</script>

布局组件 Aside

<template>
  <aside :class="ns.b()" :style="style">
    <slot />
  </aside>
</template>
<script lang="ts" setup>
import { computed } from 'vue'
import { useNamespace } from '@element-plus/hooks'

import type { CSSProperties } from 'vue'

defineOptions({
  name: 'ElAside',
})
const props = defineProps({
  /**
   * @description width of the side section
   */
  width: {
    type: String,
    default: null,
  },
})

const ns = useNamespace('aside')
const style = computed(
  () =>
    (props.width ? ns.cssVarBlock({ width: props.width }) : {}) as CSSProperties
)
</script>
const cssVarBlock = (object: Record<string, string>) => {
    const styles: Record<string, string> = {}
    for (const key in object) {
      if (object[key]) {
        styles[`--${namespace.value}-${block}-${key}`] = object[key]
      }
    }
    return styles
  }

cssVarBlock 函数就用于生成 对应 Scss中写好的变量 进行覆盖

需要注意的是最终style 会生成 如这样的值给到style属性中 --el-aside-width: 200px 如果你在组件库去看 会发现它的最终呈现是这样的:

Element Plus - Container布局容器组件源码解析学习 Element Plus - Container布局容器组件源码解析学习

由于行内样式权重大于类样式,这里的style 相当于是 把这个css 变量的值覆盖了, 原先是由el-aside 类样式中的定义的300px

布局组件 Header、Footer

<template>
  <header :class="ns.b()" :style="style">
    <slot />
  </header>
</template>
<script lang="ts" setup>
import { computed } from 'vue'
import { useNamespace } from '@element-plus/hooks'

import type { CSSProperties } from 'vue'

defineOptions({
  name: 'ElHeader',
})

const props = defineProps({
  /**
   * @description height of the header
   */
  height: {
    type: String,
    default: null,
  },
})

const ns = useNamespace('header')
const style = computed(() => {
  return props.height
    ? (ns.cssVarBlock({
        height: props.height,
      }) as CSSProperties)
    : {}
})
</script>

和aside组件类似, 也是相同的style属性定义对原本的css变量进行覆盖

样式

elementPlus是如何定义样式的?我们可以在theme-chalk这个包中看到所有的样式,这里以 footer 组件为例

/* footer.scss */
@use 'mixins/mixins' as *;
@use 'mixins/var' as *;
@use 'common/var' as *;

@include b(footer) {
  @include set-component-css-var('footer', $footer);

  padding: getCssVar('footer-padding');
  box-sizing: border-box;
  flex-shrink: 0;
  height: getCssVar('footer-height');
}

混入 bem命名的 b方法

@use 'mixins/mixins' as *;

/* 定义好的变量 */
$namespace: 'el' !default;
$common-separator: '-' !default;
$element-separator: '__' !default;
$modifier-separator: '--' !default;
$state-prefix: 'is-' !default;
@mixin b($block) {
/* el-footer */
  $B: $namespace + $common-separator + $block !global;
    /* @content标识符可以理解为插槽 外部使用这个混入时编写的样式规则会生成到这里 */
  .#{$B} {
    @content;
  }
}

混入set-component-css-var方法

@include set-component-css-var('footer', $footer); 在说这个方法前需要先知道在common/var.scss中 定义了这个变量

$footer: () !default;
$footer: map.merge(
  (
    'padding': 0 20px,
    'height': 60px,
  ),
  $footer
);

sass:map 是Sass的内置函数,merge用于合并两个样式规则。 变量footer 默认是空样式规则。()是多个样式规则的写法

然后我们再看混入本身的定义

@mixin set-component-css-var($name, $variables) {
  @each $attribute, $value in $variables {
    @if $attribute == 'default' {
      #{getCssVarName($name)}: #{$value};
    } @else {
      #{getCssVarName($name, $attribute)}: #{$value};
    }
  }
}

遍历 footer变量 , 将遍历的每一个样式规则分为key 和 value,把它当作对象看待即可, 然后通过 getCssVarName 生成对应的 css变量

/* mixins/function.scss */
@function joinVarName($list) {
  $name: '--' + config.$namespace;
  @each $item in $list {
    @if $item != '' {
      $name: $name + '-' + $item;
    }
  }
  @return $name;
}

// getCssVarName('button', 'text-color') => '--el-button-text-color'
@function getCssVarName($args...) {
  @return joinVarName($args);
}

@function getCssVar($args...) {
  @return var(#{joinVarName($args)});
}

这里也很好理解,将传入的默认参数当作可展开参数,然后通过循环就可以构造出比如这样的变量--el-footer-padding:60px, 而生成出来的变量就可以给到 footer.scss 中去使用 padding: getCssVar('footer-padding'); getCssVar 就是将结果当作变量返回,getCssVarName 就是返回的变量字符串

造个轮子

复现轮子只是为了加深印象和代码,先看,再改,最后再实现它。 github.com/Gu1st/eleme…

总结

useNamespace非常的巧妙实现了CSS类名的生成,以及在Scss中的许多处理都是我没见过,平时如果编写代码最多也只是用到 嵌套的写法, 或者变量定义, 混入、函数等等 很多技巧让我眼前一亮。 。ElementPlus中仅仅一个布局容器组件 通过插槽机制实现布局排列的判断,标签 section/aside/main/header/footer 结合flex布局 就可以完成如后台管理界面的基本布局

转载自:https://juejin.cn/post/7251501945273548860
评论
请登录