从 0 到 1 实现一个框架BootstrapVue: Lesson 2
如何实现一个<BLink />
1.首先看最核心的 render function
render 里面的代码, 用于控制最终渲染出来的效果
render() {
const { active, disabled, computedTag, isRouterLink, bvAttrs } = this
return h(
isRouterLink ? resolveComponent(computedTag) : computedTag,
{
class: [{ active, disabled }, bvAttrs.class],
style: bvAttrs.style,
attrs: this.computedAttrs,
props: this.computedProps,
// We must use `nativeOn` for `<router-link>`/`<nuxt-link>` instead of `on`
[isRouterLink ? 'nativeOn' : 'on']: this.computedListeners
},
this.normalizeSlot()
)
}
h 相当于 createElement, 是构造 VNode 的一个方法. 你所看到的视图是由很多个 VNode 构成的.
h的第一个参数, 表示 VNode 的类型. 这里用 isRouterLink 做了一个判断, 如果这个 BLink 代表一个 Vue 的 Router Link, 则使用resolveComponent(computedTag), 否则使用computedTag.
这里使用了一个 helper function. 判断 tag 存在且不为'a'的, 就是 routerLink.
export const isRouterLink = (tag) => !!(tag && !isTag(tag, "a"));
如果是 RouterLink, 则使用 resolveComponent(computedTag) 作为 VNode 的 type. 此时 type 是组件类型. 否则用 computedTag, 比如 div.
h的第二个参数, 表示 VNode 的属性.
这里控制样式的有 class 和 style, class 中使用了 array 和 object 的混合形式. 只有当 BLink active 和 disabled 为 true 时, 才会添加对应的 active 和 disabled class.
bvAttrs.class 和 bvAttrs.style 是统一表示其它的 class 和 style.
computedAttrs 和 computedProps 也是表示处理过后的 attrs 和 props.
最后加上 listeners 用于处理各种交互事件.
h的第三个参数, 表示这个 VNode 的子节点.
这里使用了 slot.
2.过一遍组件的参数
({
"prop": "href",
"description": "Denotes the target URL of the link for standard a links"
},
{
"prop": "rel",
"description": "Sets the 'rel' attribute on the rendered link"
},
{
"prop": "target",
"description": "Sets the 'target' attribute on the rendered link"
})
上面这些是BLink基本的 props 参数. 还有部分 props 是属于nuxt-link 和 router-link里的.
router-link 部分. 这部分和 vue-router 里面定义的部分基本一致.
// <router-link> specific props
export const routerLinkProps = {
to: {
type: [String, Object],
default: null,
},
append: {
type: Boolean,
default: false,
},
replace: {
type: Boolean,
default: false,
},
event: {
type: [String, Array],
default: "click",
},
activeClass: {
type: String,
// default: undefined
},
exact: {
type: Boolean,
default: false,
},
exactActiveClass: {
type: String,
// default: undefined
},
routerTag: {
type: String,
default: "a",
},
};
nuxt-link 部分. 与 nuxt 里描述的一致.
// <nuxt-link> specific props
export const nuxtLinkProps = {
prefetch: {
type: Boolean,
// Must be `null` to fall back to the value defined in the
// `nuxt.config.js` configuration file for `router.prefetchLinks`
// We convert `null` to `undefined`, so that Nuxt.js will use the
// compiled default. Vue treats `undefined` as default of `false`
// for Boolean props, so we must set it as `null` here to be a
// true tri-state prop
default: null,
},
noPrefetch: {
type: Boolean,
default: false,
},
};
3.看一下参数是如何实现可配置的
这里有一个makePropsConfigurable, 第一项参数是 props options, 第二项参数是一个 String, 用来表示配置的这个 props 属于谁.
export const props = makePropsConfigurable(
{
href: {
type: String,
default: null,
},
rel: {
type: String,
// Must be `null` if no value provided
default: null,
},
target: {
type: String,
default: "_self",
},
active: {
type: Boolean,
default: false,
},
disabled: {
type: Boolean,
default: false,
},
...routerLinkProps,
...nuxtLinkProps,
// To support 3rd party router links based on `<router-link>` (i.e. `g-link` for Gridsome)
// Default is to auto choose between `<router-link>` and `<nuxt-link>`
// Gridsome doesn't provide a mechanism to auto detect and has caveats
// such as not supporting FQDN URLs or hash only URLs
routerComponentName: {
type: String,
// default: undefined
},
},
NAME_LINK
);
makePropsConfigurable里面用了一个 reduce, reduce 的形式是
arr.reduce(callback( accumulator, currentValue[, index[, array]] ) {
// return result from executing something for accumulator or currentValue
}[, initialValue])
这里将传进去的 props 的每一项, 即每个单独的 prop, 都用 getComponentConfig根据 componentKey 去获取配置的值, 如果没有, 则 falllback 到当前的默认值
// Make a props object configurable by global configuration
// Replaces the current `default` key of each prop with a `getComponentConfig()`
// call that falls back to the current default value of the prop
export const makePropsConfigurable = (props, componentKey) =>
keys(props).reduce((result, prop) => {
const currentProp = props[prop];
const defaultValue = currentProp.default;
result[prop] = {
...cloneDeep(currentProp),
default() {
return getComponentConfig(
componentKey,
prop,
isFunction(defaultValue) ? defaultValue() : defaultValue
);
},
};
return result;
}, {});
4.看一下 Mixins
这里使用了 3 个 mixins, 分别是 attrsMixin, listenersMixin, normalizeSlotMixin
// @vue/component
export const BLink = /*#__PURE__*/ defineComponent({
name: NAME_LINK,
// Mixin order is important!
mixins: [attrsMixin, listenersMixin, normalizeSlotMixin],
inheritAttrs: false,
props,
...
})
先看到 attrsMixin, 这个 mixin 的作用主要是把$attrs代理到bvAttrs
import { makePropCacheMixin } from "../utils/cache";
export default makePropCacheMixin("$attrs", "bvAttrs");
将$attrs的属性深拷贝到bvAttrs
export const makePropCacheMixin = (
propName,
proxyPropName,
normalizePropFn = null
) =>
defineComponent({
data() {
return {
[proxyPropName]: cloneDeep(
isFunction(normalizePropFn)
? normalizePropFn(this[propName])
: this[propName]
),
};
},
watch: {
// Work around unwanted re-renders: https://github.com/vuejs/vue/issues/10115
[propName]: makePropWatcher(proxyPropName, normalizePropFn),
},
});
再看 listenersMixin, 这个 mixin 的作用主要是把$listeners或$attrs代理到bvListeners
// --- Mixin ---
export default makePropCacheMixin(
isVue2 ? "$listeners" : "$attrs",
"bvListeners",
isVue2 ? null : normalizeProp
);
normalizeSlotMixin, 这个 mixin 的作用主要是提供判断 slot 是否存在, 以及获取 slot 对应的 VNode 的方法.
export default defineComponent({
methods: {
hasNormalizedSlot(name = SLOT_NAME_DEFAULT) {
// Returns `true` if the either a `$scopedSlot` or `$slot` exists with the specified name
// `name` can be a string name or an array of names
return hasNormalizedSlot(
name,
isVue2 ? this.$scopedSlots : {},
this.$slots
);
},
normalizeSlot(name = SLOT_NAME_DEFAULT, scope = {}) {
// Returns an array of rendered VNodes if slot found
// Returns `undefined` if not found
// `name` can be a string name or an array of names
const vNodes = normalizeSlot(
name,
scope,
isVue2 ? this.$scopedSlots : {},
this.$slots
);
return vNodes ? concat(vNodes) : vNodes;
},
},
});
5.看一下事件处理
首先会对 evt 进行校验, 看是不是真的事件对象, 以及当前状态是否为 disabled
const { isRouterLink } = this
const evtIsEvent = isEvent(evt)
const suppliedHandler = this.bvListeners.click
if (evtIsEvent && this.disabled) {
// Stop event from bubbling up
// Kill the event loop attached to this specific `EventTarget`
// Needed to prevent `vue-router` for doing its thing
stopEvent(evt, { immediatePropagation: true })
}
...
然后判断是不是 RouterLink, 如果是, 则触发对应的点击事件.
...
else {
// TODO: Check if this is relevant for Vue 3 / Vue Router 4
/* istanbul ignore next */
if (isRouterLink && evt.currentTarget.__vue__) {
// Router links do not emit instance `click` events, so we
// add in an `$emit('click', evt)` on its Vue instance
evt.currentTarget.__vue__.$emit(EVENT_NAME_CLICK, evt)
}
接着对于该点击事件, 触发对应的 handler. 最后在$root 上 emit 对应的合成事件.这里是'bv::link::click'.
如果 href 是'#', 则阻止页面滚动
// Call the suppliedHandler(s), if any provided
concat(suppliedHandler)
.filter(h => isFunction(h))
.forEach(handler => {
handler(...arguments)
})
// Emit the global `$root` click event
this.$root.$emit(ROOT_EVENT_NAME_LINK_CLICKED, evt)
}
// Stop scroll-to-top behavior or navigation on
// regular links when href is just '#'
if (evtIsEvent && !isRouterLink && this.computedHref === '#') {
stopEvent(evt, { propagation: false })
}
6.技术总结
- 使用构造函数, 构造出节点
通过 isRouterLink 进行判断, 有选择的构造出 VNode
- 使用 Mixins, 减少重复代码
mixins: [attrsMixin, listenersMixin, normalizeSlotMixin]
分别提供了代理, 和 slot 的 helper function.
- 事件处理
disabled 的时候不处理, 正常状态下该 emit 到上层组件.