动手实现一个简单的 JSX 渲染器
最近有哥们问我 JSX 到底有多少种用法,直接把我问到了
所以打算写篇文章记录一下自己对 JSX 的认识,顺便也巩固一下自己的知识。
这篇文章我将使用 esbuild 这个 js 打包器来处理 JSX 语法,当然你使用 babel 或者 tsc 也可以,或者其他更多的支持 JSX 语法转换的打包器。
JSX 的概念
在现代的 Web 开发框架当中,React 算的上是 JSX 使用的开山鼻祖了,这里我直接给出 React 官网对 JSX 的表述。
JSX 就是 javascript 的语法扩展,能够让开发者使用类似 HTML 标签的方式使用 js 编写 UI 界面。 一个最简单的 JSX 如下所示:
const element = <h1>Hello JSX</h1>
JSX 与我们使用的 Vue 模版或者其他模版不一样,它只是 JavaScript 的语法糖,也就是说在我们将 JSX 编译成 js 文件之后,原本的 JSX 语法都将被替换成纯 js。
使用 esbuild 打包 JSX
esbuild 是一个使用 go 语言写的 javascript 打包器,速度非常快,能够处理现代化 web 开发当中各种资源的打包操作,如:js/ts 语法转换、jsx/tsx 编译等等,还能够更多的其他特性,详细的学习文档可以直接阅读官网。
- esbuild: esbuild.github.io/
搭建项目
直接创建一个标准的前端项目,将 esbuild 安装到项目的依赖当中,项目大概目录结构如下所示。
esbuild 安装命令
pnpm add esbuild -D
将下列执行命令添加到 packages.json 的 scripts 当中
"scripts": {
"build": "esbuild src/index.jsx --jsx=transform --outfile=dist/index.js"
},
然后在 index.jsx 当中写入
const element = <h1>hello jsx</h1>
然后执行 pnpm build
获得的打包后的 js 文件如下所示:
const element = /* @__PURE__ */ React.createElement("h1", null, "hello jsx");
可以看到,esbuild
已经帮我们把 JSX 转换成了 React.createElement
函数调用的方式了,默认情况下使用 esbuild
的 --jsx=transform
,esbuild
就会把我们写的 JSX 语法都编译成 React.createElement
。
我们也可以在 jsx 的文件顶部加上下面的注视,也可以指定我们需要将 jsx 编译为什么函数调用。
/** @jsx 函数名 */
也可以设置 jsconfig.json
或者 tsconfig.json
当中的 compilerOptions.jsxFactory
,也可以指定。
{
"compilerOptions": {
"jsxFactory": "h",
}
}
我们将上面的 index.mjs 修改为如下内容:
/** @jsx h */
const element = <h1>hello jsx</h1>
得到的结果如下:
const element = /* @__PURE__ */ h("h1", null, "hello jsx");
OK,通过上面的知识点,我们可以让我们编写的 JSX 在编译之后走我们自定义的函数调用。
自定义 JSX 渲染函数
这里我们也把自定义的渲染函数名称为 h,其含义为 hyperscript("hypertext" + "javascript").
通过结果我们可以反推出来我们函数的定义以及根据我们使用 Vue 或者 React 的经验,h 函数一边都是用来返回虚拟 DOM(vnode)的,那么我们可以直接写出以下函数:
function h(nodeName, attributes, ...args) {
let children = args.length ? [].concat(...args) : null;
return { nodeName, attributes, children };
}
我们给 name 增加一下打印语句,然后编译一下项目,并直接使用 node 执行一下打包之后的结果。
可以看到,成功的把我们需要的 vnode 打印出来了,可以测试一下标签嵌套,当然得到的结果也和我们预期的一样。
有了 vnode 之后,我们还需要一个 render 函数将其渲染到界面上。 可以尝试自己写一下,加深自己的理解,很简单,这里我直接贴出代码。
function render(vnode) {
// 节点是字符串直接渲染
if (vnode.split) return document.createTextNode(vnode);
// 根据节点创建元素
let n = document.createElement(vnode.nodeName);
// 节点的属性,这里需要考虑递归处理样式,这里只简单的设置属性
let a = vnode.attributes || {};
Object.keys(a).forEach( k => n.setAttribute(k, a[k]) );
// 递归处理子节点
(vnode.children || []).forEach( c => n.appendChild(render(c)) );
return n;
}
有了上面的函数,我们就已经能够成功的将我们写的jsx渲染到界面上了。
完整代码:
/** @jsx h */
function h(nodeName, attributes, ...args) {
let children = args.length ? [].concat(...args) : null;
return { nodeName, attributes, children };
}
function render(vnode) {
if (vnode.split) return document.createTextNode(vnode);
let n = document.createElement(vnode.nodeName);
let a = vnode.attributes || {};
Object.keys(a).forEach( k => n.setAttribute(k, a[k]) );
(vnode.children || []).forEach( c => n.appendChild(render(c)) );
return n;
}
const element = <h1 class="test">hello
<span>JSX</span>
</h1>
document.body.appendChild(render(element));
可以看到,界面按照我们的预期渲染到界面了。
我们在使用它来测试一下列表渲染,直接使用,完整的代码如下所示:
/** @jsx h */
function h(nodeName, attributes, ...args) {
let children = args.length ? [].concat(...args) : null;
return { nodeName, attributes, children };
}
function render(vnode) {
if (vnode.split) return document.createTextNode(vnode);
let n = document.createElement(vnode.nodeName);
let a = vnode.attributes || {};
Object.keys(a).forEach( k => n.setAttribute(k, a[k]) );
(vnode.children || []).forEach( c => n.appendChild(render(c)) );
return n;
}
let items = ['foo', 'bar', 'baz'];
function item(text) {
return <li>{text}</li>;
}
let list = render(
<ul>
{ items.map(item) }
</ul>
);
document.body.appendChild(list);
允许的效果如下所示:
非常完美,如果你读到这里,那么你已经成功的掌握了自己写 jsx 渲染函数的能力,反正最终 jsx 会转换为函数调用,所以 javascript 能怎么用,jsx 就能怎么用,非常灵活多变。
转载自:https://juejin.cn/post/7367676494976221194