【偷裤衩】Dan佬的Redux😎
👉 引言:这是一个源码共读的系列文章,我管它叫偷裤衩,顾名思义,非常形象,妙不可言,不可多言,回味无穷。
- 简单聊一下【偷裤衩】的价值:
- 促进深入理解: 通过集体讨论和分享经验,加深对源码的理解
- 提高编码技巧: 学习他人的开发思路和技巧,拓宽自己的思维方式,是真的可以学到很多骚操作
- 互相学习阅读源码技巧: 阅读源码本身也是需要一定技巧的,和经验的。
- 可能给开源社区贡献代码: 当你阅读完源码,或途中的一些问题,可以给开源社区提issue,甚至是PR,若被维护者Merged,那你便成为了开源社区贡献者。
- 简单聊一下【偷裤衩】的步骤:
- 选择源码: 选择一个对自己有价值或感兴趣的开源项目
- 分析源码结构: 理解项目的整体架构、模块划分及依赖关系
- 解读核心代码: 深入研究关键的核心代码实现,阅读和理解源码注释
组件库源码一般不会很深,所以并不会花时间去解读逻辑细节,若有兴趣可以自行观看,而本文只会关注以下的点。
- 各UI框架共性
- 组件开发思维图块(这里不讨论需遵循的开发规范或设计原则或设计模式,如BEM、S.O.L.I.D等)
开始🚀
- TDesign 腾讯开源UI库
组件目录结构如下图:
tsx
const Button = forwardRef((originProps: ButtonProps, ref: React.RefObject<HTMLElement>) => {
const props = useDefaultProps(originProps, buttonDefaultProps);
const {
type,
theme,
variant,
icon,
disabled,
loading,
size,
block,
ghost,
shape,
children,
content,
className,
suffix,
href,
tag,
onClick,
...buttonProps
} = props;
const { classPrefix } = useConfig();
const [btnDom, setRefCurrent] = useDomRefCallback();
useRipple(ref?.current || btnDom);
const renderChildren = content ?? children;
let iconNode = icon;
if (loading) iconNode = <Loading loading={loading} inheritColor={true} />;
const renderTheme = useMemo(() => {
if (!theme) {
if (variant === 'base') return 'primary';
return 'default';
}
return theme;
}, [theme, variant]);
const renderTag = useMemo(() => {
if (!tag && href && !disabled) return 'a';
if (!tag && disabled) return 'div';
return tag || 'button';
}, [tag, href, disabled]);
return React.createElement(
renderTag,
{
...buttonProps,
href,
type,
ref: ref || setRefCurrent,
disabled: disabled || loading,
className: classNames(
className,
[
`${classPrefix}-button`,
`${classPrefix}-button--theme-${renderTheme}`,
`${classPrefix}-button--variant-${variant}`,
],
{
[`${classPrefix}-button--shape-${shape}`]: shape !== 'rectangle',
[`${classPrefix}-button--ghost`]: ghost,
[`${classPrefix}-is-loading`]: loading,
[`${classPrefix}-is-disabled`]: disabled,
[`${classPrefix}-size-s`]: size === 'small',
[`${classPrefix}-size-l`]: size === 'large',
[`${classPrefix}-size-full-width`]: block,
},
),
onClick: !disabled && !loading ? onClick : undefined,
},
<>
{iconNode}
{renderChildren && <span className={`${classPrefix}-button__text`}>{renderChildren}</span>}
{suffix && <span className={`${classPrefix}-button__suffix`}>{parseTNode(suffix)}</span>}
</>,
);
});
Button.displayName = 'Button';
export default Button;
点我查看结构解读🔝
因为我们只看代码组织形式从而得出开发思维模型,所以不会探究更多逻辑细节。
- 从目录上我们可以看到一个组件开发需要涉及到的一些非代码层面的东西,测试用例,使用示例,样式脚本,默认参数,组件代码,说明文档,类型导出,组件代码。
- 从组件代码中我们可以看到一个组件需要包括的代码层面的东西,Props传值,国际化,默认图标及样式,主题处理,渲染逻辑处理,方便调试的处理,暴露内部属性或方法,事件处理,性能优化。
- Element-plus 饿了么团队开源UI库
组件目录如下:
vue
<template>
<component // [!code focus]
:is="tag" // [!code focus]
ref="_ref" // [!code focus]
v-bind="_props" // [!code focus]
:class="buttonKls" // [!code focus]
:style="buttonStyle" // [!code focus]
@click="handleClick" // [!code focus]
>
<template v-if="loading">
<slot v-if="$slots.loading" name="loading" />
<el-icon v-else :class="ns.is('loading')">
<component :is="loadingIcon" />
</el-icon>
</template>
<el-icon v-else-if="icon || $slots.icon">
<component :is="icon" v-if="icon" />
<slot v-else name="icon" />
</el-icon>
<span // [!code focus]
v-if="$slots.default" // [!code focus]
:class="{ [ns.em('text', 'expand')]: shouldAddSpace }" // [!code focus]
>
<slot />
</span>
</component>
</template>
<script lang="ts" setup>
import { computed } from 'vue'
import { ElIcon } from '@element-plus/components/icon'
import { useNamespace } from '@element-plus/hooks'
import { useButton } from './use-button'
import { buttonEmits, buttonProps } from './button'
import { useButtonCustomStyle } from './button-custom'
defineOptions({
name: 'ElButton',
})
const props = defineProps(buttonProps)
const emit = defineEmits(buttonEmits)
const buttonStyle = useButtonCustomStyle(props)
const ns = useNamespace('button')
const { _ref, _size, _type, _disabled, _props, shouldAddSpace, handleClick } =
useButton(props, emit)
const buttonKls = computed(() => [
ns.b(),
ns.m(_type.value),
ns.m(_size.value),
ns.is('disabled', _disabled.value),
ns.is('loading', props.loading),
ns.is('plain', props.plain),
ns.is('round', props.round),
ns.is('circle', props.circle),
ns.is('text', props.text),
ns.is('link', props.link),
ns.is('has-bg', props.bg),
])
defineExpose({
/** @description button html element */
ref: _ref,
/** @description button size */
size: _size,
/** @description button type */
type: _type,
/** @description button disabled */
disabled: _disabled,
/** @description whether adding space */
shouldAddSpace,
})
</script>
点我查看结构解读🔝
- 从目录上我们可以看到一个组件开发需要涉及到的一些非代码层面的东西,测试用例,样式脚本,默认参数,类型导出,组件代码。
- 从组件代码中我们可以看到一个组件需要包括的代码层面的东西,Props传值,国际化,默认图标,默认样式处理,主题处理,渲染逻辑处理,方便调试的处理,暴露内部属性或方法,命名空间,事件处理。
- radix-ui 国外开源社区UI库
组件目录结构如下:
vue
<script lang="ts">
import type { PrimitiveProps } from '@/Primitive'
import type { Ref } from 'vue'
import { useVModel } from '@vueuse/core'
import { createContext, useFormControl, useForwardExpose } from '@/shared'
import type { CheckedState } from './utils'
export interface CheckboxRootProps extends PrimitiveProps {
/** The checked state of the checkbox when it is initially rendered. Use when you do not need to control its checked state. */
defaultChecked?: boolean
/** The controlled checked state of the checkbox. Can be binded with v-model. */
checked?: boolean | 'indeterminate'
/** When `true`, prevents the user from interacting with the checkbox */
disabled?: boolean
/** When `true`, indicates that the user must check the checkbox before the owning form can be submitted. */
required?: boolean
/** The name of the checkbox. Submitted with its owning form as part of a name/value pair. */
name?: string
/** The value given as data when submitted with a `name`.
* @defaultValue "on"
*/
value?: string
/** Id of the element */
id?: string
}
export type CheckboxRootEmits = {
/** Event handler called when the checked state of the checkbox changes. */
'update:checked': [value: boolean]
}
interface CheckboxRootContext {
disabled: Ref<boolean>
state: Ref<CheckedState>
}
export const [injectCheckboxRootContext, provideCheckboxRootContext]
= createContext<CheckboxRootContext>('CheckboxRoot')
</script>
<script setup lang="ts">
import { computed, toRefs } from 'vue'
import { Primitive } from '@/Primitive'
import { getState, isIndeterminate } from './utils'
defineOptions({
inheritAttrs: false,
})
const props = withDefaults(defineProps<CheckboxRootProps>(), {
checked: undefined,
value: 'on',
as: 'button',
})
const emits = defineEmits<CheckboxRootEmits>()
const { disabled } = toRefs(props)
const checked = useVModel(props, 'checked', emits, {
defaultValue: props.defaultChecked,
passive: (props.checked === undefined) as false,
}) as Ref<CheckedState>
const { forwardRef, currentElement } = useForwardExpose()
const isFormControl = useFormControl(currentElement)
const ariaLabel = computed(() => props.id && currentElement.value
? (document.querySelector(`[for="${props.id}"]`) as HTMLLabelElement)?.innerText
: undefined)
provideCheckboxRootContext({
disabled,
state: checked,
})
</script>
<template>
<Primitive // [!code focus]
v-bind="$attrs" // [!code focus]
:id="id" // [!code focus]
:ref="forwardRef" // [!code focus]
role="checkbox" // [!code focus]
:as-child="props.asChild" // [!code focus]
:as="as" // [!code focus]
:type="as === 'button' ? 'button' : undefined" // [!code focus]
:aria-checked="isIndeterminate(checked) ? 'mixed' : checked" // [!code focus]
:aria-required="false" // [!code focus]
:aria-label="$attrs['aria-label'] || ariaLabel" // [!code focus]
:data-state="getState(checked)" // [!code focus]
:data-disabled="disabled ? '' : undefined" // [!code focus]
:disabled="disabled" // [!code focus]
@keydown.enter.prevent="() => {
// According to WAI ARIA, Checkboxes don't activate on enter keypress
}" // [!code focus]
@click="checked = isIndeterminate(checked) ? true : !checked" // [!code focus]
>
<slot />
</Primitive>
<input // [!code focus]
v-if="isFormControl" // [!code focus]
type="checkbox" // [!code focus]
tabindex="-1" // [!code focus]
aria-hidden // [!code focus]
:value="value" // [!code focus]
:checked="!!checked" // [!code focus]
:name="props.name" // [!code focus]
:disabled="props.disabled" // [!code focus]
:required="props.required" // [!code focus]
:style="{
transform: 'translateX(-100%)',
position: 'absolute',
pointerEvents: 'none',
opacity: 0,
margin: 0,
}" // [!code focus]
>
</template>
点我查看结构解读🔝
- 从目录上我们可以看到一个组件开发需要涉及到的一些非代码层面的东西,测试用例,样式脚本,默认参数,类型导出,组件代码,组件示例,组件文档。
- 从组件代码中我们可以看到一个组件需要包括的代码层面的东西,Props传值,国际化,默认图标,默认样式处理,主题处理,渲染逻辑处理,表单状态封装,事件处理,状态转换,双向绑定。
通过以上3个UI库,我们大致可以总结一个局部的组件思维模型:
注意,以上只是局部分析!
以上都是从组件目录开始的局部分析,本文旨在启发具体组件开发的思维,并非组件库架构,工程化建设,抑或是SSR等考量,所以请勿狭隘理解,本文旨在启发,抛砖引玉,关于组件库建设以及架构后续,我应该会单独开坑~
🖥️写在最后:
以上就是这期【偷裤衩】的全部内容了,阅读源码就像是读书,沿着各个源码作者的编码思路进行探索的过程,这有助于帮助自己偷师百家,成为仙道巅峰之人。