Vue在JSX中使用全局安装的组件

我们知道在Vue中通过render函数可以自定义渲染,可以通过createElement创建VNode(Vue2.x开始支持了JSX生成VNode)。下面是通过createElement和JSX创建的一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 使用createElement创建
render: (h, {row}) => {
return h('p', {
style: {
fontSize: '14px',
color: '#f00'
}
}, [row.text]);
}
// 使用jsx创建
render: (h, {row}) => (
<p style={{ color: '#f00', fontSize: '14px' }}>{ row.text }</p>
)

习惯了使用XML方式画UI,显然通过createElement创建的方式没有JSX来的直观。Vue也有介绍JSX在Vue和React里的区别(事实上,vue通过babel-plugin-transform-vue-jsx实现了将JSX转化为VNode)。常规组件(即标准的HTML标签)和React差不多,自定义组件可以通过导入组件然后使用的方式来创建:

1
2
3
import CustomCmp from '@/src/components/CustomCmp';
<CustomCmp />

那么对于全局安装的组件,如何在JSX中使用呢?值得注意的是,使用createElement来创建VNode却可以直接使用全局组件,JSX中却不行。下面是_createElement的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
function _createElement (
context,
tag,
data,
children,
normalizationType
) {
if (isDef(data) && isDef((data).__ob__)) {
process.env.NODE_ENV !== 'production' && warn(
"Avoid using observed data object as vnode data: " + (JSON.stringify(data)) + "\n" +
'Always create fresh vnode data objects in each render!',
context
);
return createEmptyVNode()
}
// object syntax in v-bind
if (isDef(data) && isDef(data.is)) {
tag = data.is;
}
if (!tag) {
// in case of component :is set to falsy value
return createEmptyVNode()
}
// warn against non-primitive key
if (process.env.NODE_ENV !== 'production' &&
isDef(data) && isDef(data.key) && !isPrimitive(data.key)
) {
warn(
'Avoid using non-primitive value as key, ' +
'use string/number value instead.',
context
);
}
// support single function children as default scoped slot
if (Array.isArray(children) &&
typeof children[0] === 'function'
) {
data = data || {};
data.scopedSlots = { default: children[0] };
children.length = 0;
}
if (normalizationType === ALWAYS_NORMALIZE) {
children = normalizeChildren(children);
} else if (normalizationType === SIMPLE_NORMALIZE) {
children = simpleNormalizeChildren(children);
}
var vnode, ns;
if (typeof tag === 'string') {
var Ctor;
ns = config.getTagNamespace(tag);
if (config.isReservedTag(tag)) {
// platform built-in elements
vnode = new VNode(
config.parsePlatformTagName(tag), data, children,
undefined, undefined, context
);
} else if (isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
// 这里就是为什么通过createElement可以使用全局挂在的组件原因
vnode = createComponent(Ctor, data, context, children, tag);
} else {
vnode = new VNode(
tag, data, children,
undefined, undefined, context
);
}
} else {
// direct component options / constructor
vnode = createComponent(tag, data, context, children);
}
if (isDef(vnode)) {
if (ns) { applyNS(vnode, ns); }
return vnode
} else {
return createEmptyVNode()
}
}

可以看出通过createElement可以使用全局组件原因在于这个函数有携带上下文环境context,继续查找resolveAsset函数,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/**
* Resolve an asset.
* This function is used because child instances need access
* to assets defined in its ancestor chain.
*/
function resolveAsset (
options,
type,
id,
warnMissing
) {
/* istanbul ignore if */
if (typeof id !== 'string') {
return
}
// 这里通过createElement创建时type为componets,options为vm.$options
// 即assets = vm.$options.components
var assets = options[type];
// check local registration variations first
if (hasOwn(assets, id)) { return assets[id] }
var camelizedId = camelize(id);
if (hasOwn(assets, camelizedId)) { return assets[camelizedId] }
var PascalCaseId = capitalize(camelizedId);
if (hasOwn(assets, PascalCaseId)) { return assets[PascalCaseId] }
// fallback to prototype chain
var res = assets[id] || assets[camelizedId] || assets[PascalCaseId];
if (process.env.NODE_ENV !== 'production' && warnMissing && !res) {
warn(
'Failed to resolve ' + type.slice(0, -1) + ': ' + id,
options
);
}
return res
}

可以看出,它会在vm.$options.components里查找,不管传入的是驼峰命名也好,帕斯卡命名也好都会尝试进行查找,最后找不到才去原型链上查找。因此,想要在JSX中使用全局安装的自定义组件,可以按如下方式使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
render: (h) => {
let { GlobalCustomCmp1, GlobalCustomCmp2 } = this.$options.components;
return (
<div>
<GlobalCustomCmp1 />
<GlobalCustomCmp2 />
</div>
);
}
// 当然也可以使用with来注入全部全局组件,简化代码
render: (h) => {
with (this.$options.components) {
return (
<div>
<GlobalCustomCmp1 />
</div>
);
}
}

注意: with不能在严格模式(strict)下使用