本文接着上文讲
4、createElement 与 为什么要在Vue中使用jsx
createElement可能对于习惯于编写template的同学可能还比较陌生,这个方法用于编写render函数的场景,其中输入参数h便是createElement,请看下面的例子:
//已经省略无关代码
export default {
name: 'ElCollapseTransition',
functional: true,
render(h, { children }) {
const data = {
on: new Transition()
};
return h('transition', data, children);
}
};
这段代码位于element-ui/src/transitions/collapse-transition.js中。
在Vue的官方文档中有这样一节详细阐述了createElement的用法。
为什么我们要用createElement呢?有些时候我们需要动态的插入一个VNode,并且想要这个VNode跟我们正常写的模板一样具备事件响应的能力,比如说表格、表单等,每个表格列里面要涵盖不同的渲染内容的需求,我们是没办法事先就想好这个表格里面要展示的内容的,最容易想到的方式就是在运行的时候插入一段VNode。所以我们就可以对外提供一个函数,要求这个函数的入参是createElement,并且返回一个VNode。
ElementUI Tree自定义树节点的示例代码:
renderContent(h, { node, data, store }) {
return (
<span class="custom-tree-node">
<span>{node.label}</span>
<span>
<el-button size="mini" type="text" on-click={ () => this.append(data) }>Append</el-button>
<el-button size="mini" type="text" on-click={ () => this.remove(node, data) }>Delete</el-button>
</span>
</span>);
}
但是createElement有一个天生的不足,就是代码实在是太太太太太太冗余了。于是,为了改进直接编写createElement造成的代码冗余,便出现了jsx,所以jsx并不是React专属的啦。 上述ElementUI Tree自定义树节点的示例代码就是基于JSX编写的。我们把这段代码编写成原生的createElement函数的形式让大家来感受一下JSX的好处。
function renderContent(h, { node, data, store }) {
return h(
"span",
{
class: "ustom-tree-node",
},
h("span", {}, node.label),
h("span", {}, [
h(
"el-button",
{
props: {
size: "mini",
type: "text",
},
on: {
click: () => {
this.append(data);
},
},
},
"Append"
),
h(
"el-button",
{
props: {
size: "mini",
type: "text",
},
on: {
click: () => {
this.remove(node, data);
},
},
},
"Delete"
),
])
);
}
关于jsx的语法本文不做介绍。
最后再聊另外一个话题,来看一段代码,我们分别用template的语法和jsx的语法来编写。
使用template编写的源码:
<template>
<div class="hello" :class="{ world: a }">
<input type="text" v-model="val" />
</div>
</template>
<script>
export default {
data() {
return {
a: "",
val: ""
};
},
mounted() {
this.a = 1;
}
};
</script>
编译结果:
//以省略无关代码
var o = function() {
var e = this,
t = e.$createElement,
o = e._self._c || t;
return o("div", { staticClass: "hello", class: { world: e.a } }, [
o("input", {
directives: [
{
name: "model",
rawName: "v-model",
value: e.val,
expression: "val"
}
],
attrs: { type: "text" },
domProps: { value: e.val },
on: {
input: function(t) {
t.target.composing || (e.val = t.target.value);
}
}
})
]);
};
惊不惊喜,意不意外,template最终经过webpack编译之后,还是成了createElement函数的形式,哈哈哈。 然后我们再将其改写成jsx的形式: 源码如下:
<script>
export default {
data() {
return {
a: "",
val: ""
};
},
mounted() {
this.a = 1;
},
render(h) {
return (
<div class={{ hello: true, word: a }}>
<input v-model={this.val} type="text" />
</div>
);
}
};
</script>
编译结果如下:
//以省略无关代码
{
data: function() {
return { a: "", val: "" };
},
mounted: function() {
this.a = 1;
},
render: function(e) {
var r = this;
return e("div", { class: { hello: !0, word: a } }, [
e(
"input",
t()([
{
on: {
input: function(e) {
e.target.composing || (r.val = e.target.value);
}
},
attrs: { type: "text" },
domProps: { value: r.val }
},
{
directives: [
{ name: "model", value: r.val, modifiers: {} }
]
}
])
)
]);
}
}
所以现在大家应该明白为什么如果我们既没有写template并且也没有写render函数的时候,Vue会抛出一个提示说render函数和template必须要有一个呢。
需要注意一点的是JSX需要预编译,如果程序在运行的时候需要动态插入VNode的场景,仍然只能用createElement函数的形式。
但实际项目中仍然优先考虑template的形式编写代码,一方面render函数入门门槛高,对于新手及其不友好,另一方面使用template编写的代码会经过webpack编译优化,在某些场景下性能会好一些。再者render函数里面不能使用指令(唯一一个v-model指令都是社区支持的,😁),编程体验不太好。但是render函数具有一定的可编程性,因此,需要根据开发场景适当的切换(我在实际项目几乎不会用到<component />组件,哈哈哈)。
还有一些笔者觉得在使用体验上不是特别友好的地方,Vue2中props和attrs是分开的,比如ElementUI里面写的组件接收的属性:
当我们切换到他的源码的时候你会发现,卧槽,你这个操作简直把我整神!!!
因此你需要明确的将placeholder写到attrs属性上去,这个placeholder才会生效。道听途说没有得到求证的消息,Vue3里面统一了attrs和props
另外一个区别就是,使用render函数使用插槽不是特别方便,因为Vue把作用域插槽跟普通插槽分别进行了处理。关于这个插槽,接下来我们会讲到。
Vue还提供了$createElement这个属性,这个属性其实就是我们的render函数的h,这个函数我只有在render函数的形式下才成功使用过。因为这个h入参的关系,假设业务特别复杂的情况下,我想要拆分render函数,但是拆分出来的函数老是第一个参数必须是h,实在是让人难以接受,于是,在拆分的函数中直接使用$createElement代替,避免了h参数的传递。
5、$slots 和$scopedSlots
首先,如果对插槽不太熟悉的同学请查阅Vue的官方文档。
在上一节中我们聊到了Vue中createElement函数的使用场景,并且说到了Vue中的插槽问题。
什么时候使用插槽?什么时候使用createElement呢?
我的开发经验告诉我,如果你的API想要设计的反人类的话,就使用createElement函数吧,如果要设计的友好的话,就设计成插槽,哈哈哈。为什么这样说呢,上文说到了,当我们要在运行的时候动态的插入一段VNode的时候,我们可以借助Vue.component中组件的template的字段,然后再将其使用插槽插入,实际上我们把这个解析template得到VNode的过程交给Vue去处理了,否则我们将会面临写一大段的createElement生成VNode的问题。使用插槽的好处就是直观,可以方便的使用Vue的template的那些语法特性(最大的好处就是指令没有之一),代码简洁。
有些时候我们真的需要用一个JSON来表述表格的定义,从ElementUI-Table的使用体验上来说,感觉不是特别友好,
这儿我参考了AntDesign-Vue、iView、和ElementUI几个UI框架的在表格部分的设计,想要适应任何自定义表格列的组件。iview使用的是createElement形式实现自定义表格列的。ElementUI直接使用的是子组件(el-table-column)的形式配置表格列。AntDesign使用的是动态插槽(Vue3已经支持动态插槽)。回到ElementUI上来,我们先看看ElementUI-Table的slot-scope="xxx"(虽然这个写法已经废弃)这个语法特性是怎么实现的。
element-ui/packages/table/table-column.js第170行左右:
// 已经省略无关代码
column.renderCell = (h, data) => {
let children = null;
if (this.$scopedSlots.default) {
children = this.$scopedSlots.default(data);
} else {
children = originRenderCell(h, data);
}
const prefix = treeCellPrefix(h, data);
const props = {
class: 'cell',
style: {}
};
if (column.showOverflowTooltip) {
props.class += ' el-tooltip';
props.style = {width: (data.column.realWidth || data.column.width) - 1 + 'px'};
}
return (<div { ...props }>
{ prefix }
{ children }
</div>);
};
大家可以看到,其实$scopedSlots就是一个返回VNode的函数($slots是一个VNode)。 有了这个基础之后,我们就大有可为了,我们可以判断当前$scopedSlots是否具有某个变量,如果有,则执行。在这儿,我贴一段我自己封装ElementUI表格时候对动态插槽的实现:
//这是我编写的表格列的组件中的一段代码
setupSlots() {
var slotFunc = this.$scopedSlots[this.prop]
//如果当前表格列对应的prop存在这个具名作用于插槽的话,则使用插槽的内容进行渲染,否则使用table-column-wrapper组件进行渲染。
return {
default: ({ row, $index }) => {
return typeof slotFunc === 'function' ? (
slotFunc({ row, $index })
) : (
<table-column-wrapper
prop={this.prop}
row={row}
index={$index}
config={this.config}
/>
)
},
}
}
我们花了很大的篇幅介绍$scopedSlots,接下来该介绍$slots了,我最开始使用JSX编写基础组件的时候,因为深入数据对象那一节,想当然的以为使用JSX只有一个$scopeSlots,当我某一天用JSX写ElementUI的el-input的时候发现代码并没有生效。 我编写的代码如下:
//已省略非关键代码
render(h) {
const spanNode = h("span", "HelloWorld");
return (
<el-input
v-model={this.val}
type="text"
{...{
scopedSlots: {
prepend: spanNode
}
}}
/>
);
}
后来我回想到,我们正常写template的时候,写插槽,都是将需要插入的组件放到当前组件的内部,然后添加slot属性。于是对这段代码进行改正:
render(h) {
return (
<el-input v-model={this.val} type="text">
<span slot="append">HelloWorld</span>
</el-input>
);
}
这样就会生效了,但是这样又有了新问题,我们硬编码了slot,如何实现动态的slot呢,我是这样解决的:
methods: {
genSlots() {
return Object.entries(this.$slots).map(([prop, vnode]) => {
return <template slot={prop}>{vnode}</template>;
});
}
},
render(h) {
return (
<el-input v-model={this.val} type="text">
{/* 不管你生成多少个slot,只能是我当前这个组件支持的slot,我才会给你展示 */}
{this.genSlots()}
</el-input>
);
}
目前我对动态插槽的实现思路,还没有经过对程序的运行性能影响的检验,忘读者酌情参考,如有纰漏,敬请指正。 基于以上思路就可以完全在Vue2中实现动态插槽。
6 $attrs 和 $listeners
在element-ui/packages/image/src/main.vue中,我们可以看到如下代码:
<template>
<div class="el-image">
<img
v-else
class="el-image__inner"
v-bind="$attrs"
v-on="$listeners"
@click="clickHandler"
:src="src"
:style="imageStyle"
:class="{ 'el-image__inner--center': alignCenter, 'el-image__preview': preview }">
</div>
</template>
这两个属性对于封装基于第三方组件的组件的时候,是十分好用的,由于笔者在之前的文章有对这个知识点的讲解,感兴趣的朋友可以参考这里,此处将不再赘述。
7、aria-*
关于aria-*标签,我最开始使用ElementUI的时候,也比较好奇,为什么好端端的代码要多写一些“无意义”的标签呢,后来我通过查阅资料得知,这让网站的对无障碍访问支持有重要的意义。 下面这段话是我搬运的MDN的官方释义(我不光生产代码,还是代码的搬运工,哈哈哈哈):
Accessible Rich Internet Applications (ARIA) 是能够让残障人士更加便利的访问 Web 内容和使用 Web 应用(特别是那些由JavaScript 开发的)的一套机制。
ARIA 是一组特殊的易用性属性,可以添加到任意标签上,尤其适用于 HTML。role 属性定义了对象的通用类型(例如文章、警告,或幻灯片)。额外的 ARIA 属性提供了其他有用的特性,例如表单的描述或进度条的当前值。 因此,在后面的开发中,为了是我们的网站具备更好的无障碍访问特性,还得在合适的时机使用这些语法呢,害,知识又增加了呢。
8、i18n
ElementUI是支持多语言的一个UI库,但是通过研读它的代码发现,其实多语言的实现也并不复杂。 其核心代码位于element-ui/src/locale/index.js中。
//如果有应用了Vue其它对i18n支持的插件,则使用其它的多语言插件
let i18nHandler = function() {
const vuei18n = Object.getPrototypeOf(this || Vue).$t;
if (typeof vuei18n === 'function' && !!Vue.locale) {
if (!merged) {
merged = true;
Vue.locale(
Vue.config.lang,
deepmerge(lang, Vue.locale(Vue.config.lang) || {}, { clone: true })
);
}
return vuei18n.apply(this, arguments);
}
};
export const t = function(path, options) {
let value = i18nHandler.apply(this, arguments);
//如果返回的值满足条件,说明用户使用了其它的i18n插件
if (value !== null && value !== undefined) return value;
const array = path.split('.');
let current = lang;
//否则使用ElementUI自己内置的语言替换模式
for (let i = 0, j = array.length; i < j; i++) {
const property = array[i];
value = current[property];
if (i === j - 1) return format(value, options);
if (!value) return '';
current = value;
}
return '';
};
然后local保留解析方法(代码位于element-ui/src/mixins/locale.js) 代码如下:
import { t } from 'element-ui/src/locale';
export default {
methods: {
t(...args) {
return t.apply(this, args);
}
}
};
在任何需要的地方(我以DatePicker为例)引入locale混入, 代码示例如下:
<!--已省略非关键代码-->
<template>
<table
:class="{ 'is-week-mode': selectionMode === 'week' }">
<tbody>
<tr>
<th v-if="showWeekNumber">{{ t('el.datepicker.week') }}</th>
<th v-for="(week, key) in WEEKS" :key="key">{{ t('el.datepicker.weeks.' + week) }}</th>
</tr>
</tbody>
</table>
</template>
其既支持Vue主流的多语言插件,又提供了内置支持,这个手段对于我们在编写插件的时候还是有一定的借鉴意义的,毕竟其还是有可能面向外国用户使用的,如果预先不考虑这个问题,到时候又修改一堆的代码(忽然想到了邓爷爷的一句话,教育要面向现代化,面向世界,面向未来,哈哈哈😁)。
总结
这两篇文章通过ElementUI的源码分析,阐述了SCSS的一些高级用法以及CSS BEM和一些Vue高级API用法,很多内容都是来自于我在实际开发中的理解与心得体会,我可能阐述了的ElementUI所蕴含的编程技法的冰山一角都还算不上(有兴趣的读者可以查阅一下ElementUI下utils模块中的内容,也有一定的价值,可能会帮助你更好的使用ElementUI。),希望广大读者与我分享一些好的开发心得体会,毕竟只有交流才能碰撞出思维的火花,彼此都能进步,哈哈哈。
最后,在此感谢ElementUI团队为我们贡献出了如此优秀的开源项目,点赞三连。
由于笔者水平有限,写作过程中难免出现错误,若有纰漏,请各位读者指正,你们的意见将会帮助我更好的进步。本文乃笔者原创,若转载,请联系作者本人,邮箱404189928@qq.com🥰