As mentioned in the previous article, JSX is not a mystery, it exists only to relieve developers from writing lengthy createElement or H function calls. And because JSX looks so much like HTML, there is almost no learning curve for front-end developers. But the downside of JSX is that it’s a “dialect” that browsers don’t know. Only ordinary function calls compiled by tools such as Babel can be executed by the browser.
JSX is not the only solution for avoiding writing createElment calls. Vue, for example, supports the use of templates to describe the UI:
const App = defineComponent({
template: `
`.setup() {
const count = ref(0);
const inc = () = > {
count.value = count.value + 1;
};
return{ count, inc, }; }});Copy the code
Note that to use template Option, you must use the VUE with the Compiler version. The compile step has been moved from build time to Runtime. This saves us the build process, but increases the number of run-time steps. The size of the imported VUE file also increases. However, I personally don’t like Template very much because it has a dark magic feel to it compared to JSX. For example, the count variable is used in the code above. However, where this variable comes from and why it is accessible can be a bit of a mystery to those new to Vue.
Similarly, Preact is pushing an alternative to JSX called HTM. HTM is based on the Tagged Templates feature of JavaScript and can also be done without building. And its syntax is very similar to JSX. Examples from the official website:
<script type="module">
import { h, Component, render } from "https://unpkg.com/preact?module";
import htm from "https://unpkg.com/htm?module";
// Initialize htm with Preact
const html = htm.bind(h);
function App(props) {
return html`<h1>Hello ${props.name}!</h1>`;
}
render(html`<${App} name="World" />`.document.body);
</script>
Copy the code
As you can see, the above code can be executed directly in the browser due to the progressive improvement of modern browsers to the ECMAScript standard. It feels like a throwback to the days when the
Of course, none of these grammars is superior to each other, only the context in which they are used. JSX certainly has some advantages over the two solutions described above. JSX, for example, has good TypeScript support. The JSX experience should still be optimal in terms of code intelligence hints.
More importantly, many technologies are generic, not limited to a particular framework or library. As mentioned earlier, we can use JSX to develop Vue projects. The same is true of HTM. Since it can and any form of h(type, props,… The children function binding, then theoretically it can also be used with Vue. Let’s see if this is true.
HTM binding Vue
According to the documentation, we need to bind HTM to a signature h(type, props,… A function of children. At its simplest, we can write a function like this:
import * as vue from "vue";
function h(type, props, ... children) {
if (children.length === 0) {
return vue.h(type, props);
}
return vue.h(type, props, children);
}
Copy the code
Bind HTM to this function to get the HTML function:
import htm from "htm";
const html = htm.bind(h);
Copy the code
Let’s try it out:
test("html should return vnode".() = > {
const vnode = html`<div>Hello World</div>`;
expect(isVNode(vnode)).toBe(true);
expect(vnode.type).toBe("div");
expect(vnode.children).toEqual(["Hello World"]);
});
Copy the code
These matchers all passed without a hitch, that is, writing Vue code in HTM is completely feasible. But h is so crude that it doesn’t take into account the case of slots in HTML. Slots is written in Vue h like this:
render() {
// `<div><child v-slot="props"><span>{{ props.text }}</span></child></div>`
return h('div', [
h(
resolveComponent('child'),
null.// pass `slots` as the children object
// in the form of { name: props => VNode | Array<VNode> }
{
default: (props) = > h('span', props.text)
}
)
])
}
Copy the code
Written in HTML, it looks like this:
html`<${resolveComponent("child")}
>The ${{default: (props) => h("span", props.text) }}< / a > `;
Copy the code
But that’s actually the wrong way to write it. Because ultimately, we pass vue.h [{default: (props) => h(‘span’, props. Text)}], and that’s an array. Obviously, this is not the same as the official version code given to use vue.h. So we needed to do some processing for HTM to our children, and we also added support for V-slots as vuE-JSX did. In the end, we should achieve something like this:
html`<${resolveComponent("child")}
v-slots=The ${{default: (props) => h("span", props.text) }}> < / a > `;
/ / or
html`<${resolveComponent("child")}>${(props) => h("span", props.text)}< / a > `;
Copy the code
For this reason, we modify H:
function h(type, props, ... children) {
letslots = props? ."v-slots"];
if (children.length === 1) {
if (typeof children[0= = ="function") {
slots = slots ?? {};
if(! slots.hasOwnProperty("default")) {
slots.default = children[0]; }}}else if (children.length === 0) {
return vue.h(type, props);
}
return vue.h(type, props, slots ?? children);
}
Copy the code
We simply assume that if children contains only one function, that function should be used as the default slot. And we think the default in V-slots should have a higher priority than the default we infer. Of course, the code above doesn’t take into account some edge cases. However, as you can see, it is very easy to handle the parameters passed to us in HTM. We can modify H to suit our needs to support more advanced syntax. For example, we want to add support for Custom Directives. Currently, if we want to use Custom Directives, we write:
import { vShow } from "vue";
withDirectives(html`<div>Hello World</div>`, [[vShow, false]]);
Copy the code
Obviously, this method is very inconvenient to use. What we would like to see is:
html`<div v-show=The ${false}>Hello World</div>`;
Copy the code
So let’s make some changes to the props passed in:
function h(type, props, ... children) {
const newProps = {};
const directives = [];
for (const key in props) {
if (props.hasOwnProperty(key)) {
const result = /^v-(?
[a-zA-Z_][0-9a-zA-Z_]*)$/
.exec(key);
if(! result) { newProps[key] = props[key]; }else {
const { directiveName } = result.groups;
const directive =
directiveName === "show"
? vue.vShow
: vue.resolveDirective(directiveName);
if(directive) { directives.push([directive, props[key]]); }}}}letslots = props? ."v-slots"];
if (children.length === 1) {
if (typeof children[0= = ="function") {
slots = slots ?? {};
if(! slots.hasOwnProperty("default")) {
slots.default = children[0]; }}}else if (children.length === 0) {
return vue.withDirectives(vue.h(type, newProps), directives);
}
return vue.withDirectives(
vue.h(type, newProps, slots ? slots : children),
directives
);
}
export default h;
Copy the code
Operation effect:
test("html supports v-show".() = > {
const wrapper = mount(() = > html`<div v-show=The ${false}>Hello World</div>`);
expect(wrapper.html()).toMatchSnapshot();
});
// snapshot: exports[`html supports v-show 1`] = `"
Hello World
"`;
test("suports custom directives".() = > {
/ * * *@type Directive<any, string>
*/
const directive = {
mounted(_, { value }) {
console.log(`Hello ${value}`)}}const spy = jest.spyOn(global.console, "log")
const wrapper = mount(() = > html`<div v-greeting="World">Hello World</div>`, {
global: {
directives: { 'greeting': directive }
}
});
expect(spy).toHaveBeenCalledWith("Hello World")
expect(wrapper.html()).toMatchSnapshot();
spy.mockRestore();
});
// snapshot: exports[`html suports custom directives 1`] = `"<div>Hello World</div>"`;
Copy the code
The full code is on GitHub.
As you can see, we can easily enhance the syntax to make it easier to use, and there is much more we can do, such as adding support for global registered components, v-model and V-IF support, and so on. As with previous JSX enhancements, we were limited only by our imagination.