本系列文章是分享我自己一步一步编写整个框架的过程,有兴趣的xdm可以参考源代码阅读。git仓库:github.com/sullay/art-…
渲染DOM元素
本文作为本系列的第一篇文章,也是我们编写前端框架的第一步,我们先完成框架中最重要也是最基础的功能也就是渲染DOM元素。
下面是我们经常会见到的一段html代码。
<div style="margin: 100px auto; text-align: center;">
<p style="color: red;">Hello World!</p>
</div>
复制代码
我们先考虑如何用js对象来抽象上述的dom结构,下面是我写给出的答案。
{
type: "div",
props: {style: "margin: 100px auto; text-align: center;"},
children:[{
type: "p",
props: {style: 'color:red;'},
children:["Hello World!"]
}]
}
复制代码
有了上面的js对象,我们可以开始反向思考如何把js对象渲染成页面需要dom元素。
我们需要编写一个render方法,这个方法接收两个参数,第一个参数element是我们上面构造的js对象,第二个参数parentDom是我们渲染好的元素对应的挂载点。
function render(element, parentDom) {
let dom;
if (typeof element === "string") {
dom = document.createTextNode(element)
} else {
const { type, props, children } = element;
// 根据元素类型创建对应的dom
dom = document.createElement(type);
// 设置属性
for (let key in props) dom[key] = props[key];
// 渲染子元素
for (let child of children) render(child, dom);
}
// 绑定到父元素
parentDom.appendChild(dom);
}
复制代码
此时我们已经编写好了render方法,在浏览器中测试一下,已经可以正常的将js对象渲染到页面上。
虚拟DOM与jsx
通过render方法,我们已经可以将js对象渲染成对应的dom元素了。但是通过html文本我们能够更清楚的了解页面的结构,但是仅仅通过js对象我们很难想象出页面该长什么样子,这时候我们就需要预编译将我们编写的类html文本处理成js对象。
jsx语法
考虑到esbuild、bable、ts都是支持jsx语法的,这里我们直接使用jsx语法,简化我们js对象的创建。 如果不清楚jsx语法的同学建议先了解一下,再阅读后续内容。
这里我们使用esbuild预处理jsx文件,命令如下:
![](/qrcode.jpg)
esbuild xxx.jsx --bundle --jsx-factory=h --jsx-fragment=Fragment --outfile=xxx.out.js
复制代码
我们使用esbuild对下面的代码进行处理
// 处理前代码
render(
<div style="margin: 100px auto; text-align: center;">
<p style="color:red;">Hello World!</p>
</div>,
document.getElementById('root')
)
// 处理后代码
(() => {
render(/* @__PURE__ */ h("div", {
style: "margin: 100px auto; text-align: center;"
}, /* @__PURE__ */ h("p", {
style: "color:red;"
}, "Hello World!")), document.getElementById("root"));
})();
复制代码
可以看到处理后的代码中多出了一个h方法,这个方法是jsx用于创建js对象的工厂函数,可以通过命令行终端jsx-factory参数修改函数名。
在开始编写我们的工厂函数h之前,我们可以重新整理一下我们的js对象。
function isEvent(key) {
return key.startsWith('on');
}
function getEventName(key) {
return key.toLowerCase().replace(/^on/, "");
}
// 普通元素
class vNode {
constructor(type = '', allProps = {}, children = []) {
this.type = type;
this.props = {};
this.events = {};
for (let prop in allProps) {
if (isEvent(prop)) {
this.events[getEventName(prop)] = allProps[prop];
} else {
this.props[prop] = allProps[prop];
}
}
// 处理子元素中的文字类元素
this.children = children.map(child => {
return vNode.isVNode(child) ? child : new vTextNode(child);
});
}
// 判断是否属于虚拟Dom元素
static isVNode(node) {
return node instanceof this;
}
}
// 文字元素
class vTextNode extends vNode {
constructor(text) {
super(vTextNode.type, { nodeValue: text })
}
static type = Symbol('TEXT_ELEMENT');
}
// 创建元素
export function h(type, props, ...children) {
return new vNode(type, props, children);
}
复制代码
这里我们编写了两个类分别表示我们的普通元素与文字元素,并且将事件与其他属性进行了区分,然后我们编写了h方法帮我们创建虚拟DOM对象。
此时更新一下我们的render方法,新增了事件监听。
function render(element, parentDom) {
if (!vNode.isVNode(element)) throw new Error("渲染元素类型有误");
const { type, props, events, children } = element;
// 根据元素类型创建对应的dom
const dom = vTextNode.isVNode(element) ? document.createTextNode('') : document.createElement(type);
// 设置属性
for (let key in props) dom[key] = props[key];
// 渲染子元素
for (let child of children) render(child, dom);
// 监听事件
for (let event in events) dom.addEventListener(event, events[event])
// 绑定到父元素
parentDom.appendChild(dom);
}
复制代码
到此为止,我们的框架已经基本完成了dom渲染功能。
下一边文章中将主要介绍自定义组件的实现,感兴趣的xdm可以关注我后续的更新。