Erlo

深入解析 Vue 3 组件库 element-plus 架构源码

2021-01-08 10:00:13 发布   1121 浏览  
页面报错/反馈
收藏 2 赞

原文:https://juejin.cn/user/184373683164589

作者:Jenniferyingni

转载需经过作者本人授权。

基于 Vue3 的组件库 element-plus[1] 正式发布,element-plus 是一个使用 TypeScript + Composition API 重构的全新项目。官方列出了下面几项主要更新,本文会阅读 element-plus 的源码,从以下几个方面在整体和细节上来分析重构之后的源码,建议阅读本文前先 clone 组件代码。

  • 使用 TypeScript 开发
  • 使用 Vue 3.0 Composition API 降低耦合,简化逻辑
  • 使用 Vue 3.0 Teleport 新特性重构挂载类组件
  • Vue 2.0 全局 API 切换为 Vue 3.0 实例 API
  • 国际化处理
  • 官方文档网站打包
  • 组件库和样式打包
  • 使用 Lerna 维护和管理项目

Typescript 相关

element-plus 引入了 typescript, 除了配置对应的 eslint 校验规则、插件,定义 tsconfig.json 之外,打包 es-module 格式组件库的时候的时候使用到了一些 rollup 插件。

  • @rollup/plugin-node-resolve
  • rollup-plugin-terser
  • rollup-plugin-typescript2
  • rollup-plugin-vue
// build/rollup.config.bundle.js
import { nodeResolve } from "@rollup/plugin-node-resolve";
import { terser } from "rollup-plugin-terser";
import typescript from "rollup-plugin-typescript2";
const vue = require("rollup-plugin-vue");
export default [
  {
    // ... 省略前面部分内容
    plugins: [
      terser(),
      nodeResolve(),
      vue({
        target"browser",
        cssfalse,
        exposeFilenamefalse,
      }),
      typescript({
        tsconfigOverride: {
          include: ["packages/**/*""typings/vue-shim.d.ts"],
          exclude: ["node_modules""packages/**/__tests__/*"],
        },
      }),
    ],
  },
];
复制代码;

@rollup/plugin-node-resolve 打包依赖的 npm 包。

rollup-plugin-terser 压缩代码rollup-plugin-vue[2] 打包 vue 文件, css 样式交给了后续会提到的 gulp 来处理。

rollup-plugin-typescript2 是用了编译 typescript 的, 配置中排除了 node-modules 和测试相关文件, include 除了包含组件实现,还包含了 typings/vue-shim.d.ts 文件。

插件中使用到的 typings/vue-shim.d.ts 类型声明文件( 以 .d.ts 结尾的文件会被自动解析 ),定义了一些全局的类型声明,可以直接在 ts 或者 vue 文件中使用这些类型约束变量。还使用扩展模板对 import XX from XX.vue 的引入变量给出类型提示。

// typings/vue-shim.d.ts
declare module '*.vue' {
  import { defineComponent } from 'vue'
  const component: ReturnType<typeof defineComponent>
  export default component
}
declare type Nullable<T> = T | null;
declare type CustomizedHTMLElement<T> = HTMLElement & T
declare type Indexable<T> = {
  [key: string]: T
}
declare type Hash<T> = Indexable<T>
declare type TimeoutHandle = ReturnType<typeof global.setTimeout>
declare type ComponentSize = 'large' | 'medium' | 'small' | 'mini'
复制代码

除了 d.ts 文件之外,element-plus 中对于 props 的类型声明使用了 vue3 的 propType。以 下面的 Alert 为例, 使用了 PropType 的 props 类型会执行符合我们自定义的规则的构造函数,然后结合 typescript 做类型校验。其他非 props 中的类型声明则是使用了 interface

import { PropType } from 'vue'
export default defineComponent({
  name'ElAlert',
  props: {
    type: {
      typeString as PropType<'success' | 'info' | 'error' | 'warning'>,
      default'info',
    }
  }
})
复制代码

更多 vue3 的 typescript 支持可以查看官方文档[3]

Composition API

官方说明使用了 Vue 3.0 Composition API 降低耦合,简化逻辑。Composition API 的使用和 hooks 的复用 vue-3-playground[4] 中通过一个购物车 demo 的实现提供了一个直观和简洁的示例。

关于常用的 Composition API 的用法,可以查看这篇总结得比较好的文章:快速使用 Vue3 最新的 15 个常用 API[5]

除了使用新的 Composition API 来改写组件之外,element-plus 中 packages/hooks 目录下抽取了几个可复用的 hooks 文件:

以 autocomplete, input 等控件使用到的 use-attrs 为例, 主要做的事情是继承绑定的属性和事件,类似于 $attrs$listener 功能,但是做了一些筛选,去掉了一些不需要继承的属性和事件绑定。

watchEffect(() => {
  const res = entries(instance.attrs).reduce((acm, [key, val]) => {
    if (
      !allExcludeKeys.includes(key) &&
      !(excludeListeners && LISTENER_PREFIX.test(key))
    ) {
      acm[key] = val;
    }
    return acm;
  }, {});
  attrs.value = res;
});
复制代码;

Vue3 中仍然保留了 mixin,我们可以在特定组件或者是全局使用 mixin 来复用逻辑,同时也引入了 hooks 来改善 mixin 存在的一些问题[6]

  1. 渲染上下文中公开的属性的来源不清楚。例如,当使用多个 mixin 读取组件的模板时,可能很难确定从哪个 mixin 注入了特定的属性。
  2. 命名空间冲突。Mixins 可能会在属性和方法名称上发生冲突

Hooks 带来的好处是

  1. 暴露给模板的属性具有明确的来源,因为它们是从 Hook 函数返回的值。
  2. Hook 函数返回的值可以任意命名,因此不会发生名称空间冲突。

Teleport 的使用

element-plus 对几个挂载类组件使用了 vue3 的新特性 Teleport,这个新特性可以帮我们把其包裹的元素移动到我们指定的节点下。

Teleport 提供了一种干净的方法,允许我们控制在 DOM 中哪个父节点下呈现 HTML,而不必求助于全局状态或将其拆分为两个组件。-- Vue 官方文档

查看官网我们会发现 Dialog,Drawer,以及使用了 Popper 的 Tooltip 和 Popover 都新增了一个 append-to-body 属性。

我们以 Dialog 为例:appendToBody 为 false, Teleport 会被 disabled, DOM 还是在当前位置渲染。

当 appendToBody 为 true 时, dialog 中的内容放到了 body 下面。

<template>
  <teleport to="body" :disabled="!appendToBody">
    <transition
      name="dialog-fade"
      @after-enter="afterEnter"
      @after-leave="afterLeave"
    >

        ...
     </transition>
  </teleport>

</tamplate>

在原来的 element-ui 中,Tooltip 和 Popover 也是直接放在了 body 中,原来是通过 vue-popper.js 来使用 document.body.appendChild 来添加元素到 body 下的,element-plus 使用 Teleport 来实现相关逻辑。

全局 API - 实例 API

当我们安装好组件库,use 方法会执行 install 方法去全局挂载组件。

我们先来看一下 Vue 2.x element-ui 中全局 API 的写法:

  • Vue.component 方法绑定全局组件
  • Vue.use 绑定全局自定义指令
  • Vue.prototype 绑定全局变量和全局方法
const install = function (Vue, opts = {}{
  locale.use(opts.locale);
  locale.i18n(opts.i18n);

  // Vue.component 方法绑定全局组件
  components.forEach((component) => {
    Vue.component(component.name, component);
  });

  // Vue.use 绑定全局自定义指令
  Vue.use(InfiniteScroll);
  Vue.use(Loading.directive);

  // Vue.prototype 绑定全局变量和全局方法
  Vue.prototype.$ELEMENT = {
    size: opts.size || "",
    zIndex: opts.zIndex || 2000,
  };
  Vue.prototype.$loading = Loading.service;
  Vue.prototype.$msgbox = MessageBox;
  Vue.prototype.$alert = MessageBox.alert;
  Vue.prototype.$confirm = MessageBox.confirm;
  Vue.prototype.$prompt = MessageBox.prompt;
  Vue.prototype.$notify = Notification;
  Vue.prototype.$message = Message;
};

但是在 vue 3.0 中,任何全局改变 Vue 行为的 API 现在都会移动到应用实例上,也就是 createApp 产生的 app 上了, 对应的 API 也做了相应的变化。再来看使用 Vue 3.0 的 element-plus 中,全局 API 改写成了实例 API。

import type { App } from 'vue'
const plugins = [
  ElInfiniteScroll,
  ElLoading,
  ElMessage,
  ElMessageBox,
  ElNotification,
]
const install = (app: App, opt: InstallOptions): void => {
  const option = Object.assign(defaultInstallOpt, opt)
  use(option.locale)
  app.config.globalProperties.$ELEMENT = option    // 全局设置默认的size属性和z-index属性
  // 全局注册所有除了plugins之外的组件
  components.forEach(component => {
    app.component(component.name, component)
  })
  plugins.forEach(plugin => {
    app.use(plugin as any)
  })
}
复制代码

除此之外写法上有一些不同的是,消息类组件添加 $ 全局方法在 element-plus 中被移动到了 index.ts 里面, 几个消息通知类型的组件都放到了 plugins,使用 app.use 会调用对应组件 index.ts 中的 install 方法,代码如下:

(Message as any).install = (app: App): void => {
  app.config.globalProperties.$message = Message
}
复制代码

国际化

packages 下有一个 locale 文件夹,控制语言切换 packages/locale/index.ts 中抛出了 2 个方法,方法 t 和方法 use, t 控制 vue 文件中文本的翻译替换,use 方法修改全局语言

// packages/locale/index.ts
export const t = (path: string, option?): string => {
  let value;
  const array = path.split(".");
  let current = lang;
  for (let i = 0, j = array.length; i < j; i++) {
    const property = array[i];
    value = current[property];
    if (i === j - 1return template(value, option);
    if (!value) return "";
    current = value;
  }
  return "";
};
复制代码;

会在 vue 文件中引入 locale 中的 t 方法

import { t } from "@element-plus/locale";
复制代码;

然后就可以在 template 使用多语言 key 值了,例如:label="t('el.datepicker.nextMonth')"t 方法会帮你找到对应的语言文件中的对应值。
再来看看 use 方法,抛出的 use 方法可以设置全局语言种类,也修改

登录查看全部

参与评论

评论留言

还没有评论留言,赶紧来抢楼吧~~

手机查看

返回顶部

给这篇文章打个标签吧~

棒极了 糟糕透顶 好文章 PHP JAVA JS 小程序 Python SEO MySql 确认