8
The overall level of Ali's front-end can be said to be at the domestic top . Related open source component libraries, especially ant-design ( react version), have a high usage rate at home and abroad. With the continuous improvement of the front-end technology stack, the corresponding matching component library is also accompanied by the iteration of the version. The iterative process of these component libraries and the implementation of internal components are worth learning for every front-end developer. Especially some very good component libraries.

ant-design-vue component library directory structure

By github be the component library open source project on clone or download after the project directory structure and directory structure of our daily development of the project are different. The structure of several cores is as follows:

  1. antd-tools : This directory structure contains tools that combine the configuration of popular build tools such as webpack , gulp ant-design-vue for packaging, building, and publishing.
  2. components : This directory is ant-design-vue library components, and the packages of all public components are under this folder
  3. examples : A package for testing the packaged component library during the development process (Demo)
  4. scripts : Script collection, used to process some packaged and built configuration scripts of the library
  5. typings : A collection of some declaration files written for the library

Component source code analysis

  • Button component: button component, which is also one of the most commonly used components in our daily development

Button component source code analysis

The packaging of a component, from a basic point of view. Pay attention to the output and input of the component itself. The input and output here refer to its own attributes and behaviors or the attributes and behaviors passed from the parent.

We can see button components/button directory, the structure is as follows:

  • __tests__ : used to unit test the component
  • style less style files that need to be written to encapsulate the component
  • .tsx : the corresponding component file

button component in our daily project development process, read the source code from the following questions:

  1. How are the attributes type (such as default , primary , dashed etc.) of the component handled internally?
  2. How is icon attribute of this component processed internally?
  3. loading attribute of this component processed internally to achieve a similar anti-shake effect?
type attribute of the Button component

From ant-design-vue can see the official website, Button component has 'default', 'primary', 'ghost', 'dashed', 'link', 'text' these six types. When we set a different type , Button component will also change accordingly.

Open the button.tsx file, you can see the following code:

import buttonTypes from './buttonTypes';
const props = buttonTypes();
export default defineComponent({
  name: 'AButton',
  inheritAttrs: false,
  __ANT_BUTTON: true,
  props,
  ...
})
  • Imported the buttonTypes module
  • Define a props variable to receive the return value of the function exported by the buttonTypes
  • props variable to the props attribute of the Button

Next, look at the relevant code of the buttonTypes.ts

first line of code : import { tuple } from '../_util/type'; from _tuil of type introduced file tuple function, the function declaration is defined as follows:

export const tuple = <T extends string[]>(...args: T) => args; uses the ts in 06173e43cbea5c to constrain that T can only be a string array (that is, the elements in the array can only be string types), and the return word of the function is a specific string element.

first 7 line : const ButtonTypes = tuple('default', 'primary', 'ghost', 'dashed', 'link', 'text'); , defines a tuple type of a variable ButtonTypes , the tuple contains six elements.

first 8 line : export type ButtonType = typeof ButtonTypes[number]; , defined here ButtonType this type and for export;

The 23rd line of code: defines a buttonProps function, which is the Button of the props attributes of the entire 06173e43cbeab5 component. The function code is as follows:

const buttonProps = () => ({
  prefixCls: PropTypes.string,
  type: PropTypes.oneOf(ButtonTypes),
  loading: {
    type: [Boolean, Object],
    default: (): boolean | { delay?: number } => false,
  },
  disabled: PropTypes.looseBool,
  ghost: PropTypes.looseBool,
  block: PropTypes.looseBool,
  danger: PropTypes.looseBool,
  icon: PropTypes.VNodeChild,
});

It can be seen that type is vue-types which can only be one of the 6 mentioned above. From the above analysis, we can conclude that the origin of the type Button component, let's see how it is associated with the style of the Button For example, set type: danger , then Button component is red and the text is white. How is this relationship handled internally?

Return to the code Button.tsx Line 75 defines a calculated attribute classes . code show as below:

    const classes = computed(() => {
      const { type, shape, size, ghost, block, danger } = props;
      const pre = prefixCls.value;
      return {
        [`${pre}`]: true,
        [`${pre}-${type}`]: type,
        [`${pre}-${shape}`]: shape,
        [`${pre}-${sizeCls}`]: sizeCls,
        [`${pre}-loading`]: innerLoading.value,
        [`${pre}-background-ghost`]: ghost && !isUnborderedButtonType(type),
        [`${pre}-two-chinese-chars`]: hasTwoCNChar.value && autoInsertSpace.value,
        [`${pre}-block`]: block,
        [`${pre}-dangerous`]: !!danger,
        [`${pre}-rtl`]: direction.value === 'rtl',
      };
    });

Here you can see type , and the difference in button style performance is mainly in the two variables pre and type pre represents the prefix of the class class name of the corresponding component, such as the prefix ant-btn button component.

Finally look Button assembly return returned a method, as follows:

    return () => {
      const buttonProps = {
        class: [
          classes.value,
         attrs.class,
        ],
        onClick: handleClick,
      };

      const buttonNode = (
        <button {...buttonProps} ref={buttonNodeRef} type={htmlType}>
          {iconNode}
          {kids}
        </button>
      );

      if (isUnborderedButtonType(type)) {
        return buttonNode;
      }
      return <Wave ref="wave">{buttonNode}</Wave>;
    };

This method is a variable of the object type buttonProps class attribute to receive its own class attribute and the above-mentioned classes calculation attribute; then a node buttonNode , and the node node in the 06173e43cbebc6 node is the content of the html of button . And through jsx grammar to deconstruct buttonProps

<a-button type="primary">A</a-button>

For example, A above, we can know that its class name is: ant-btn ant-btn-primary .

Corresponding styles written in the folder style You can know that the type Button component corresponding to the different 06173e43cbec09 is different.

icon attributes of the Button component

When we use the Button component, for example, the component needs to display a combination of icons and text, we may write like this:

  <a-button type="primary">
    <template #icon><SearchOutlined /></template>
    查询
  </a-button>

So how does the component internally handle the code <template #icon><SearchOutlined /></template>

After analyzing the type attribute of the above-mentioned Button component, it is found that the definition processing of all attributes of the component is in buttonTypes.ts , so let's first look at the code related to icon

const buttonProps = () => ({
  prefixCls: PropTypes.string,
  type: PropTypes.oneOf(ButtonTypes),
  icon: PropTypes.VNodeChild,
  ...
});

You can see, the object returned by the function in a icon attribute whose type is PropTypes on the object VNodeChild type, then this VNodeChild particular is what is it? Finally, there is a type.ts file in the vue-types folder, which defines the type VueNode

export type VueNode = VNodeChild | JSX.Element;

So in the end we can know icon is Vuejs a virtual sub-node or JSX a element.

Next back to the button.tsx component itself.

export default defineComponent({
  name: 'AButton',
  slots: ['icon'],
  setup(props, { slots, attrs, emit }) {}
}
)

It can be seen that when the assembly is defined by vuejs its slots receives attribute icon this element.

Then look at the code in the method returned by return

const icon = getPropsSlot(slots, props, 'icon'); ?? Is it an object returned here or?

This method defines the icon variable. Next, getPropsSlot ’s look at the function of the 06173e43cbed4d method? In the _util/props-util file, you can see the implementation of this method, the code is as follows:

function getPropsSlot(slots, props, prop = 'default') {
  return props[prop] ?? slots[prop]?.();
}

It can be seen that the effect of this method is used for the processing slot If props is a corresponding prop , return directly, otherwise, get the corresponding prop slots to execute the method, and finally return an array. The array contains the corresponding virtual node elements. The attributes of the elements are roughly as follows:

anchor: null
appContext: null
children: "我是icon"
component: null
dirs: null
dynamicChildren: null
dynamicProps: null
el: text
key: null
patchFlag: 0
props: null
ref: null
scopeId: "data-v-c33314ea"
shapeFlag: 8
ssContent: null
ssFallback: null
staticCount: 0
suspense: null
target: null
targetAnchor: null
transition: null
type: Symbol(Text)
__v_isVNode: true
__v_skip: true

The next step is:

 const iconNode = innerLoading.value ? <LoadingOutlined /> : icon;
 const buttonNode = (
    <button {...buttonProps} ref={buttonNodeRef} type={htmlType}>
      {iconNode}
      {kids}
    </button>
  );

A icon node is defined, and the buttonNode node is directly applied to the button label in the icon slot method (it becomes a child element of the button

Therefore, seen from the following analysis, Button assembly for icon treatment is mainly a combination vuejs of slots or props attribute corresponding to meet the conditions prop virtualization (virtual generate a child node) performed.

loading attribute of the Button component

In the process of project development, the Button component is very high, such as passing some data to the back-end through its click event and storing it in the database after processing. This is a very common business development point, but if we continuously click the button multiple times within 1 or 2 seconds , if no processing is done, a lot of duplicate data will eventually be stored in the database. To solve this problem, we have to use javascript to deal with, but the processing of ant-design-vue in button encapsulates throttling processing, as long as we use this component, add a loading attribute. It's okay. How is the encapsulation inside its components achieved?

Still look buttonTypes.ts this file loading related code, the code is as follows:

const buttonProps = () => ({
  ... // other code
  loading: {
    type: [Boolean, Object],
    default: (): boolean | { delay?: number } => false,
  },
  ...// other code
  onClick: {
    type: Function as PropType<(event: MouseEvent) => void>,
  },
});

You can see that in the object returned by the method buttonProps , there is a property of loading and a method of onClick

  • The value of the loading boolean type or an object type. Its default value is false
  • onClick method is an event object whose parameter is a mouse event, and the method has no return value

Next back to the button.tsx component itself.

In this document line 22 a type definition type Loading = boolean | number; defines Loading type of numerical or Boolean type

Then a variable is defined in the setup const innerLoading: Ref<Loading> = ref(false); innerLoading variable on the 46th line is a response variable with a value of Boolean or numeric value. So what is the purpose of defining this variable? Continue to look at the code related to it.

In line 52 is defines a loadingOrDelay calculated attribute. To receive loading this prop update

    const loadingOrDelay = computed(() =>
      typeof props.loading === 'object' && props.loading.delay
        ? props.loading.delay || true
        : !!props.loading,
    );

On line 58, monitor the value change of the calculated attribute loadingOrDelay watch , and perform related logic processing:

    watch(
      loadingOrDelay,
      val => {
        clearTimeout(delayTimeoutRef.value);
        if (typeof loadingOrDelay.value === 'number') {
          delayTimeoutRef.value = window.setTimeout(() => {
            innerLoading.value = val;
          }, loadingOrDelay.value);
        } else {
          innerLoading.value = val;
        }
      },
      {
        immediate: true,
      },
    );

If loadingOrDelay value is number type, set a definition, loadingOrDelay seconds after the loadingOrDelay assign a value to the latest innerLoading variables. Otherwise, directly loadingOrDelay the value of innerLoading to 06173e43cbefb9.

const delayTimeoutRef = ref(undefined);

Because the timer is set, when the component is about to be destroyed (unloaded), the timer needs to be cleared.

    onBeforeUnmount(() => {
      delayTimeoutRef.value && clearTimeout(delayTimeoutRef.value);
    });

Finally, the click event is logically processed on line 121:

    const handleClick = (event: Event) => {
      // https://github.com/ant-design/ant-design/issues/30207
      if (innerLoading.value || props.disabled) {
        event.preventDefault();
        return;
      }
      emit('click', event);
    };

It can be seen ant-design-vue for Button components loading realize is actually relatively simple and ingenious. By props received loading property, and not directly to a series of process by the attribute value change. Instead, a innerLoading variable and loadingOrDelay calculated attributes are internally defined to perform the corresponding logical processing. This is because loading is passed from an external component and cannot be modified directly.

Button component reference

Method 1: The Button.tsx component can be directly imported for use, but it can only be used inside the project. Another by vuejs to components provided install processing the component approach.

import type { App, Plugin } from 'vue';
import Button from './button';
/* istanbul ignore next */
Button.install = function (app: App) {
  app.component(Button.name, Button);
  return app;
};

export default Button as typeof Button & Plugin;
Related knowledge tips
  1. TypeScript in 06173e43cbf0e4

    Array combines objects of the same type, while Tuple combines objects of different types

    let tom: [string, number] = ['Tom', 25];

  2. Anti-shake and throttling

    Throttling: If you are a 7-year-old child, one day your mother is making chocolate cakes. But this cake is not for you but for the guests. At this time, you keep asking her for cakes. In the end she gave you a piece, but you continue to ask her for more cakes. She agrees to give you more cakes, provided that they will give you a piece in an hour. This is because you still continue to ask her for cakes, but she ignores you at this time. Finally an hour later, you get more cakes. If you want to get more cakes, no matter how many times it takes, you will get more cakes in an hour.

    对于节流,无论用户触发事件多少次,在给定的时间间隔内,附加的函数都只会执行一次。

Anti-shake: Consider the same cake example. This time you kept asking your mother for cakes. She was very angry and told you that she would give you cakes only if you remained silent for an hour. This means that if you keep asking her, you will not get the cake-you will only get the cake one hour after the last question.

对于防抖,无论用户触发事件多少次,一旦用户停止触发事件,附加函数将仅在指定时间后执行。
  1. Subcomponent prop check

    As you can see, in the entire ant-design-vue component, the verification of the sub-component props is processed by the vue-types plug-in. The first point is to reduce the amount of code, and the second point is to facilitate reading and expansion.

    vue-types plug-in provides the createTypes method, we can use this method to expand more types, for example, the method in ant-design-vue is as follows:

    import { createTypes } from 'vue-types';
    const PropTypes = createTypes({
      func: undefined,
      bool: undefined,
      string: undefined,
      number: undefined,
      array: undefined,
      object: undefined,
      integer: undefined,
    });
    
    PropTypes.extend([
      {
        name: 'looseBool',
        getter: true,
        type: Boolean,
        default: undefined,
      },
      {
        name: 'style',
        getter: true,
        type: [String, Object],
        default: undefined,
      },
      {
        name: 'VNodeChild',
        getter: true,
        type: null,
      },
    ]);
  2. Component packaging rules

    • Components are used: should start from the feelings of the user (programmer)
    • There is no "how to do it": the characteristics of the project need to be considered
    • Good components are not designed, but modified: often adjusted, sometimes refactored
    • The function of the component should be single and simple: don't try to squeeze many functions into one component. Embody the single responsibility principle
    • ...
  3. Encapsulated components for others to use

    For encapsulated public components, when encapsulating components, you must consider how to let others introduce and use them. At present, a more popular npm to manage the component library through 06173e43cbf3a4, and then users can introduce and use corresponding components on demand. This requires the "installation" and "export" operations of a certain component when the component is packaged.

    /* istanbul ignore next */
    Button.install = function (app: App) {
      app.component(Button.name, Button);
      return app;
    };
    export default Button

前端扫地僧
2.5k 声望1.2k 粉丝