用 JSX 建立组件 Parser(解析器)_html

这里我们一起从 0 开始搭建一个组件系统。首先通过上一篇《​​前端组件化基础知识​​》中知道,一个组件可以通过 Markup 和 JavaScript 访问的一个环境。

所以我们的第一步就是建立一个可以使用 markup 的环境。这里我们会学习使用两种建立 markup 的风格。

第一种是基于与 React 一样的 JSX 去建立我们组件的风格。第二种则是我们去建立基于类似 Vue 的这种,基于标记语言的 Parser 的一种风格。

用 JSX 建立组件 Parser(解析器)_自定义_02

JSX 环境搭建


JSX 在大家一般认知里面,它是属于 React 的一部分。其实 Facebook 公司会把 JSX 定义为一种纯粹的语言扩展。而这个 JSX 也是可以被其他组件体系去使用的。


甚至我们可以把它单独作为一种,快捷创建 HTML 标签的方式去使用。



建立项目


那么我们就从最基础的开始,首先我们需要创建一个新的项目目录:

mkdir jsx-component


初始化 NPM


在你们喜欢的目录下创建这个项目文件夹。建立好文件夹之后,我们就可以进入到这个目录里面并且初始化 ​​npm​​。

npm init

执行以上命令之后,会出现一些项目配置的选项问题,如果有需要可以自行填写。不过我们也可以直接一直按回车,然后有需要的同学可以后面自己打开 ​​package.json​​ 自行修改。


安装 webpack


Wepack 很多同学应该都了解过,它可以帮助我们把一个普通的 JavaScript 文件变成一个能把不同的 import 和 require 的文件给打包到一起。

所以我们需要安装 ​​webpack​​ ,当然我们也可以直接使用 npx 直接使用 webpack,也可以全局安装 webpack-cli。

那么这里我们就使用全局安装 webpack-cli:

npm install -g webpack webpack-cli

安装完毕之后,我们可以通过输入下面的一条命令来检测一下安装好的 webpack 版本。如果执行后没有报错,并且出来了一个版本号,证明我们已经安装成功了。

webpack --version


安装 Babel


因为 JSX 它是一个 babel 的插件,所以我们需要依次安装 webpack,babel-loader, babel 和 babel 的 plugin。

这里使用 Babel 还有一个用处,它可以把一个新版本的 JavaScript 编译成一个老版本的 JavaScript,这样我们的代码就可以在更多老版本的浏览器中运行。

安装 Babel 我们只需要执行以下的命令即可。

npm install --save-dev webpack babel-loader

这里我们需要注意的是,我们需要加上 ​​--save-dev​​,这样我们就会把 babel 加入到我们的开发依赖中。

用 JSX 建立组件 Parser(解析器)_javascript_03

执行完毕后,我们应该会看到上面图中的消息。

为了验证我们是正确安装好了,我们可以打开我们项目目录下的 ​​package.json​​。

{
"name": "jsx-component",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"devDependencies": {
"babel-loader": "^8.1.0",
"webpack": "^5.4.0"
}
}

好,我们可以看到在 ​​devDependencies​​​ 下方,确实是有我们刚刚安装的两个包。还是担心的同学,可以再和 ​​package.json​​ 确认一下眼神哈。

用 JSX 建立组件 Parser(解析器)_html_04


配置 webpack


到这里我们就需要配置一下 webpack。配置 webpack 我们需要创建一个 ​​webpack.config.js​​ 配置文件。

在我们项目的根目录创建一个 ​​webpack.config.js​​ 文件。

首先 webpack config 它是一个 nodejs 的模块,所以我们需要用 module.exports 来写它的设置。而这个是早期 nodejs 工具常见的一种配置方法,它用一个 JavaScript 文件去做它的配置,这样它在这个配置里面就可以加入一些逻辑。

module.exports = {}

Webpack 最基本的一个东西,就是需要设置一个 entry (设置它的入口文件)。这里我们就设置一个 ​​main.js​​ 即可。

module.exports = {
entry: "./main.js"
}

这个时候,我们就可以先在我们的根目录下创建一个 ​​main.js​​​ 的文件了。在里面我们先加入一个简单的 ​​for​​ 循环。

// main.js 文件内容
for (let i of [1, 2, 3]) {
console.log(i);
}

这样 webpack 的基本配置就配置好了,我们在根目录下执行一下 webpack 来打包一下 ​​main.js​​ 的文件来看看。需要执行下面的这行命令进行打包:

webpack

用 JSX 建立组件 Parser(解析器)_javascript_05

执行完毕之后,我们就可以在命令行界面中看到上面这样的一段提示。


注意细节的同学,肯定要举手问到,同学同学!你的命令行中报错啦!黄色部分确实有给我们一个警告,但是不要紧,这个我们接下的配置会修复它的。


这个时候我们会发现,在我们的根目录中生成了一个新的文件夹 ​​dist​​。这个就是 webpack 打包默认生成的文件夹,我们所有打包好的 JavaScript 和资源都会被默认放入这个文件夹当中。

这里我们就会发现,这个 ​​dist​​​ 文件夹里面有一个打包好的 ​​main.js​​​ 的文件,这个就是我们写的 ​​main.js​​,通过 webpack 被打包好的版本。

然后我们打开它,就会看到它被 babel 编译过后的 JavaScript 代码。我们会发现我们短短的几行代码被加入了很多的东西,这些其实我们都不用管,那都是 Webpack 的 “喵喵力量”。

用 JSX 建立组件 Parser(解析器)_html_06

在代码的最后面,还是能看到我们编写的 ​​for​​ 循环的,只是被改造了一下,但是它的作用是一致的。


安装 Babel-loader


接下来我们来安装 babel-loader,其实 babel-loader 并没有直接依赖 babel 的,所以我们才需要另外安装 ​​@babel/core​​​ 和 ​​@babel/preset-env​​。我们只需要执行下面的命令行来安装:

npm install --save-dev @babel/core @babel/preset-env

用 JSX 建立组件 Parser(解析器)_自定义_07

最终的结果就如上图一样,证明安装成功了。这个时候我们就需要在 ​​webpack.config.js​​ 中配置上,让我们打包的时候用上 babel-loader。

在我们上面配置好的 ​​webpack.config.js​​​ 的 ​​entry​​​ 后面添加一个选项叫做 ​​module​​。

然后模块中我们还可以加入一个 ​​rules​​​,这个就是我们构建的时候所使用的规则。而 ​​rules​​​ 是一个数组类型的配置,这里面的每一个规则是由一个 ​​test​​​ 和一个 ​​use​​ 组成的。

  • test:
  • ​test​​​ 的值是一个正则表达式,用于匹配我们需要使用这个规则的文件。这里我们需要把所有的 JavaScript 文件给匹配上,所以我们使用​​/\.js/​​ 即可。
  • use:
  • loader:
  • 只需要加入我们的​​babel-loader​​ 的名字即可
  • options:
  • presets:
  • 这里是 loader 的选项,这里我们需要加入​​@babel/preset-env​

最后我们的配置文件就会是这个样子:

module.exports = {
entry: './main.js',
module: {
rules: [
{
test: /\.js$/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env'],
},
},
},
],
},
};

这样配置好之后,我们就可以来跑一下 babel 来试一试会是怎么样的。与刚才一样,我们只需要在命令行执行 ​​webpack​​ 即可。

用 JSX 建立组件 Parser(解析器)_自定义_08

如果我们的配置文件没有写错,我们就应该会看到上面图中的结果。

然后我们进入 ​​dist​​​ 文件夹,打开我们编译后的 ​​main.js​​,看一下我们这次使用了 babel-loader 之后的编译结果。

用 JSX 建立组件 Parser(解析器)_javascript_09

编译后的结果,我们会发现 ​​for of​​​ 的循环被编译成了一个普通的 ​​for​​ 循环。这个也可以证明我们的 babel-loader 起效了,正确把我们新版本的 JavaScript 语法转成能兼容旧版浏览器的 JavaScript 语法。

到了这里我们已经把 JSX 所需的环境给安装和搭建完毕了。


模式配置


最后我们还需要在 webpack.config.js 里面添加一个环境配置,不过这个是可加也可不加的,但是我们为了平时开发中的方便。

所以我们需要在 webpack.config.js 中添加一个 ​​mode​​​,这我们使用 ​​development​​。这个配置表示我们是开发者模式。

一般来说我们在代码仓库里面写的 webpack 配置都会默认加上这个 ​​mode: 'development'​​​ 的配置。当我们真正发布的时候,我们就会把它改成 ​​mode: 'production'​​。

module.exports = {
entry: './main.js',
mode: 'development',
module: {
rules: [
{
test: /\.js$/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env'],
},
},
},
],
},
};

改好之后,我们在使用 ​​webpack​​​ 编译一下,看看我们的 ​​main.js​​ 有什么区别。

用 JSX 建立组件 Parser(解析器)_html_10

显然我们发现,编译后的代码没有被压缩成一行了。这样我们就可以调试 webpack 生成的代码了。这里我们可以注意到,我们在 ​​main.js​​​ 中的代码被转成字符串,并且被放入一个 ​​eval()​​ 的函数里面。那么我们就可以在调试的时候把它作为一个单独的文件去使用了,并且可以进行断点调试。


引入 JSX


万事俱备,只欠东风了,最后我们需要如何引入 JSX呢?在引入之前,我们来看看,如果就使用现在的配置在我们的 ​​main.js​​ 里面使用 JSX 语法会怎么样。作为程序员的我们,总得有点冒险精神!

用 JSX 建立组件 Parser(解析器)_javascript_11

所以我们在 ​​main.js​​ 里面加入这段代码:

var a = <div/>

然后大胆地执行 webpack 看看!

用 JSX 建立组件 Parser(解析器)_html_12

好家伙!果然报错了。这里的报错告诉我们,在 ​​=​​ 后面不能使用 “小于号”,但是在正常的 JSX 语法中,这个其实是 HTML 标签的 “尖括号”,因为没有 JSX 语法的编译过程,所以 JavaScript 默认就会认为这个就是 “小于号”。

所以我们要怎么做让我们的 webpack 编译过程支持 JSX 语法呢?这里其实就是还需要我们加入一个最关键的一个包,而这个包名非常的长,叫做 ​​@babel/plugin-transform-react-jsx​​。执行以下命令来安装它:

npm install --save-dev @babel/plugin-transform-react-jsx

安装好之后,我们还需要在 webpack 配置中给他加入进去。我们需要在 ​​module​​​ 里面的 ​​rules​​​ 里面的 ​​use​​​ 里面加入一个 ​​plugins​​​ 的配置,然后在其中加入 ​​['@babel/plugin-transform-react-jsx']​​。

然后最终我们的 webpack 配置文件就是这样的:

module.exports = {
entry: './main.js',
mode: 'development',
module: {
rules: [
{
test: /\.js$/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env'],
plugins: ['@babel/plugin-transform-react-jsx'],
},
},
},
],
},
};

配置好之后,我们再去执行一下 webpack。这时候我们发现没有再报错了。这样也就证明我们的代码现在是支持使用 JSX 语法了。

最后我们来围观一下,最后编程的效果是怎么样的。

用 JSX 建立组件 Parser(解析器)_javascript_13

我们会发现,在 ​​eval​​​ 里面我们加入的 ​​<div/>​​​ 被翻译成一个 ​​React.createElement("div", null)​​ 的函数调用了。

所以接下来我们就一起来看一下,我们应该怎么实现这个 ​​React.createElement​​,以及我们能否把这个换成我们自己的函数名字。

用 JSX 建立组件 Parser(解析器)_自定义_02


JSX 基本用法


首先我们来尝试理解 JSX,JSX 其实它相当于一个纯粹在代码语法上的一种快捷方式。在上一部分的结尾我们看到,JSX语法在被编译后会出现一个 ​​React.createElement​​ 的调用。


JSX 基础原理


那么这里我们就先修改在 webpack 中的 JSX 插件,给它一个自定义的创建元素函数名。我们打开 webpack.config.js,在 plugins 的位置,我们把它修改一下。

module.exports = {
entry: './main.js',
mode: 'development',
module: {
rules: [
{
test: /\.js$/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env'],
plugins: [
[
'@babel/plugin-transform-react-jsx',
{ pragma: 'createElement' }
]
],
},
},
},
],
},
};

上面我们只是把原来的 ​​['@babel/plugin-transform-react-jsx']​​​ 参数改为了 ​​[['@babel/plugin-transform-react-jsx', {pragma: 'createElement'}]]​​​。加入了这个 ​​pragma​​ 参数,我们就可以自定义我们创建元素的函数名。

这么一改,我们的 JSX 就与 React 的框架没有任何联系了。我们执行一下 webpack 看一下最终生成的效果,就会发现里面的 ​​React.createElement​​​ 就会变成 ​​createElement​​。

用 JSX 建立组件 Parser(解析器)_javascript_15

接下来我们加入一个 HTML 文件来执行我们的 main.js 试试。首先在根目录创建一个 ​​main.html​​,然后输入一下代码:

<script src="./main.js"></script>

然后我们执行在浏览器打开这个 HTML 文件。

用 JSX 建立组件 Parser(解析器)_html_16

这个时候我们控制台会给我们抛出一个错误,我们的 ​​createElement​​​ 未定义。确实我们在 ​​main.js​​ 里面还没有定义这个函数,所以说它找不到。

所以我们就需要自己编写一个 ​​createElement​​​ 这个函数。我们直接打开根目录下的 ​​main.js​​​ 并且把之前的 ​​for​​ 循环给删除了,然后加上这段代码:

function createElement() {
return;
}

let a = <div />;

这里我们就直接返回空,先让这个函数可以被调用即可。我们用 webpack 重新编译一次,然后刷新我们的 main.html 页面。这个时候我们就会发现报错没有了,可以正常运行。


实现 createElement 函数


用 JSX 建立组件 Parser(解析器)_自定义_17

在我们的编译后的代码中,我们可以看到 JSX 的元素在调用 createElement 的时候是传了两个参数的。第一个参数是 ​​div​​​, 第二个是一个 ​​null​​。

这里第二个参数为什么是 ​​null​​​ 呢?其实第二个参数是用来传属性列表的。如果我们在 main.js 里面的 div 中加入一个 ​​id="a"​​ ,我们来看看最后编译出来会有什么变化。

用 JSX 建立组件 Parser(解析器)_自定义_18

我们就会发现第二个参数变成了一个以 Key-Value 的方式存储的JavaScript 对象。到这里如果我们想一下,其实 JSX 也没有那么神秘,它只是把我们平时写的 HTML 通过编译改写成了 JavaScript 对象,我们可以认为它是属于一种 “[[语法糖]]”。


但是 JSX 影响了代码的结构,所以我们一般也不会完全把它叫作语法糖。


接下来我们来写一些更复杂一些的 JSX,我们给原本的 div 加一些 children 元素。

function createElement() {
return;
}

let a = (
<div id="a">
<span></span>
<span></span>
<span></span>
</div>
);

最后我们执行以下 webpack 打包看看效果。

用 JSX 建立组件 Parser(解析器)_自定义_19

在控制台中,我们可以看到最后编译出来的结果,是递归的调用了 ​​createElement​​ 这个函数。这里其实已经形成了一个树形的结构。

父级就是第一层的 div 的元素,然后子级就是在后面当参数传入了第一个 createElement 函数之中。然后因为我们的 span 都是没有属性的,所以所有后面的 createElement 的第二个参数都是 ​​null​​。

根据我们这里看到的一个编译结果,我们就可以分析出我们的 createElement 函数应有的参数都是什么了。

  • 第一个参数​type​​ —— 就是这个标签的类型
  • 第二个参数​attribute​​ —— 标签内的所有属性与值
  • 剩余的参数都是子属性​...children​​​ —— 这里我们使用了 JavaScript 之中比较新的语法​​...children​​ 表示把后面所有的参数 (不定个数) 都会变成一个数组赋予给 children 变量

那么我们 ​​createElement​​ 这个函数就可以写成这样了:

function createElement(type, attributes, ...children) {
return;
}

函数我们有了,但是这个函数可以做什么呢?其实这个函数可以用来做任何事情,因为这个看起来长的像 DOM API,所以我们完全可以把它做成一个跟 React 没有关系的实体 DOM。

比如说我们就可以在这个函数中返回这个 ​​type​​​ 类型的 ​​element​​​ 元素。这里我们把所有传进来的 ​​attributes​​ 给这个元素加上,并且我们可以给这个元素挂上它的子元素。

创建元素我们可以用 ​​createElement(type)​​​,而加入属性我们可以使用 ​​setAttribute()​​​,最后挂上子元素就可以使用 ​​appendChild()​​。

function createElement(type, attributes, ...children) {
// 创建元素
let element = document.createElement(type);
// 挂上属性
for (let attribute in attributes) {
element.setAttribute(attribute);
}
// 挂上所有子元素
for (let child of children) {
element.appendChild(child);
}
// 最后我们的 element 就是一个节点
// 所以我们可以直接返回
return element;
}

这里我们就实现了 ​​createElement​​ 函数的逻辑。最后我们还需要在页面上挂載上我们的 DOM 节点。所以我们可以直接挂載在 body 上面。

// 在 main.js 最后加上这段代码
let a = (
<div id="a">
<span></span>
<span></span>
<span></span>
</div>
);

document.body.appendChild(a);

这里还需要注意的是,我们的 main.html 中没有加入 body 标签,没有 body 元素的话我们是无法挂載到 body 之上的。所以这里我们就需要在 main.html 当中加入 body 元素。

<body></body>

<script src="dist/main.js"></script>

好,这个时候我们就可以 webpack 打包,看一下效果。

用 JSX 建立组件 Parser(解析器)_html_20

Wonderful! 我们成功的把节点生成并且挂載到 body 之上了。但是如果我们的 ​​div​​​ 里面加入一段文字,这个时候就会有一个文本节点被传入我们的 ​​createElement​​​ 函数当中。毋庸置疑,我们的 ​​createElement​​ 函数以目前的逻辑是肯定无法处理文本节点的。

接下来我们就把处理文本节点的逻辑加上,但是在这之前我们先把 div 里面的 span 标签删除,换成一段文本 “hello world”。

let a = <div id="a">hello world</div>;

在我们还没有加入文本节点的逻辑之前,我们先来 webpack 打包一下,看看具体会报什么错误。

用 JSX 建立组件 Parser(解析器)_自定义_21

用 JSX 建立组件 Parser(解析器)_html_22

首先我们可以看到,在 ​​createElement​​​ 函数调用的地方,我们的文本被当成字符串传入,然后这个参数是接收子节点的,并且在我们的逻辑之中我们使用了 ​​appendChild​​,这个函数是接收 DOM 节点的。显然我们的文本字符串不是一个节点,自然就会报错。

通过这种调试方式我们可以马上定位到,我们需要在哪里添加逻辑去实现这个功能。这种方式也可以算是一种捷径吧。

所以接下来我们就回到 ​​main.js​​​,在我们挂上子节点之前,判断以下 child 的类型,如果它的类型是 “String” 字符串的话,就使用 ​​createTextNode()​​ 来创建一个文本节点,然后再挂載到父元素上。这样我们就完成了字符节点的处理了。

function createElement(type, attributes, ...children) {
// 创建元素
let element = document.createElement(type);
// 挂上属性
for (let name in attributes) {
element.setAttribute(name, attributes[name]);
}
// 挂上所有子元素
for (let child of children) {
if (typeof child === 'string')
child = document.createTextNode(child);
element.appendChild(child);
}
// 最后我们的 element 就是一个节点
// 所以我们可以直接返回
return element;
}

let a = <div id="a">hello world</div>;

document.body.appendChild(a);

我们用这个最新的代码 webpack 打包之后,就可以在浏览器上看到我们的文字被显示出来了。

到了这里我们编写的 ​​createElement​​​ 已经是一个比较有用的东西了,我们已经可以用它来做一定的 DOM 操作。甚至它可以完全代替我们自己去写 ​​document.createElement​​ 的这种反复繁琐的操作了。

这里我们可以验证以下,我们在 div 当中重新加上我们之前的三个 span, 并且在每个 span 中加入文本。11

let a = (
<div id="a">
hello world:
<span>a</span>
<span>b</span>
<span>c</span>
</div>
);

然后我们重新 webpack 打包后,就可以看到确实是可以完整这种 DOM 的操作的。

用 JSX 建立组件 Parser(解析器)_自定义_23

现在的代码已经可以完成一定的组件化的基础能力。


实现自定义标签


之前我们都是在用一些,HTML 自带的标签。如果我们现在把 div 中的 d 改为大写 D 会怎么样呢?

let a = (
<Div id="a">
hello world:
<span>a</span>
<span>b</span>
<span>c</span>
</Div>
);

用 JSX 建立组件 Parser(解析器)_html_24

用 JSX 建立组件 Parser(解析器)_自定义_25

果不其然,就是会报错的。不过我们找到了问题根源的关键,这里我们发现当我们把 div 改为 Div 的时候,传入我们 ​​createElement​​​ 的 div 从字符串 ‘div’ 变成了一个 ​​Div​​ 类。

当然我们的 JavaScript 中并没有定义 Div 类,这里自然就会报 Div 未定义的错误。知道问题的所在,我们就可以去解决它,首先我们需要先解决未定义的问题,所以我们先建立一个 Div 的类。

// 在 createElment 函数之后加入
class Div {}

然后我们就需要在 ​​createElement​​​ 里面做类型判断,如果我们遇到的 type 是字符类型,就按原来的方式处理。如果我们遇到是其他情况,我们就实例化传过来的 ​​type​​。

function createElement(type, attributes, ...children) {
// 创建元素
let element;
if (typeof type === 'string') {
element = document.createElement(type);
} else {
element = new type();
}

// 挂上属性
for (let name in attributes) {
element.setAttribute(name, attributes[name]);
}
// 挂上所有子元素
for (let child of children) {
if (typeof child === 'string') child = document.createTextNode(child);
element.appendChild(child);
}
// 最后我们的 element 就是一个节点
// 所以我们可以直接返回
return element;
}

这里我们还有一个问题,我们有什么办法可以让自定义标签像我们普通 HTML 标签一样操作呢?在最新版的 DOM 标准里面是有办法的,我们只需要去注册一下我们自定义标签的名称和类型。

但是我们现行比较安全的浏览版本里面,还是不太建议这样去做的。所以在使用我们的自定义 element 的时候,还是建议我们自己去写一个接口。

首先我们是需要建立标签类,这个类能让任何标签像我们之前普通 HTML 标签的元素一样最后挂載到我们的 DOM 树上。

它会包含以下方法:

  • ​mountTo()​​​ —— 创建一个元素节点,用于后面挂載到​​parent​​ 父级节点上
  • ​setAttribute()​​ —— 给元素挂上所有它的属性
  • ​appendChild()​​ —— 给元素挂上所有它的子元素

首先我们来简单实现以下我们 ​​Div​​​ 类中的 ​​mountTo​​​ 方法,这里我们还需要给他加入 ​​setAttribute​​​ 和 ​​appendChild​​​ 方法,因为在我们的 ​​createElement​​ 中有挂載属性子元素的逻辑,如果没有这两个方法就会报错。但是这个时候我们先不去实现这两个方法的逻辑,方法内容留空即可。

class Div {
setAttribute() {}
appendChild() {}
mountTo(parent) {
this.root = document.createElement('div');
parent.appendChild(this.root);
}
}

这里面其实很简单首先给类中的 ​​root​​​ 属性创建成一个 div 元素节点,然后把这个节点挂載到这个元素的父级。这个 ​​parent​​ 是以参数传入进来的。

然后我们就可以把我们原来的 body.appendChild 的代码改为使用 ​​mountTo​​ 方法来挂載我们的自定义元素类。

// document.body.appendChild(a);
a.mountTo(document.body);

用现在的代码,我们 webpack 打包看一下效果:

用 JSX 建立组件 Parser(解析器)_自定义_26

我们可以看到我们的 Div 自定义元素是有正确的被挂載到 body 之上。但是 Div 中的 span 标签都是没有被挂載上去的。如果我们想它与普通的 div 一样去工作的话,我们就需要去实现我们的 ​​setAttribute​​​ 和 ​​appendChild​​ 逻辑。

接下来我们就一起来尝试完成剩余的实现逻辑。在开始写 setAttribute 和 appendChild 之前,我们需要先给我们的 Div 类加入一个构造函数 ​​constructor​​​。在这里个里面我们就可以把元素创建好,并且代理到 ​​root​​ 上。

constructor() {
this.root = document.createElement('div');
}

然后的 ​​setAttribute​​​ 方法其实也很简单,就是直接使用 ​​this.root​​​ 然后调用 DOM API 中的 ​​setAttribute​​​ 就可以了。而 ​​appendChild​​ 也是同理。最后我们的代码就是如下:

class Div {
// 构造函数
// 创建 DOM 节点
constructor() {
this.root = document.createElement('div');
}
// 挂載元素的属性
setAttribute(name, attribute) {
this.root.setAttribute(name, attribute);
}
// 挂載元素子元素
appendChild(child) {
this.root.appendChild(child);
}
// 挂載当前元素
mountTo(parent) {
parent.appendChild(this.root);
}
}

我们 webpack 打包一下看看效果:

用 JSX 建立组件 Parser(解析器)_html_27

我们可以看到,div 和 span 都被成功挂載到 body 上。也证明我们自制的 div 也能正常工作了。

这里还有一个问题,因为我们最后调用的是 ​​a.mountTo()​​​,如果我们的变量 ​​a​​​ 不是一个自定义的元素,而是我们普通的 HTML 元素,这个时候他们身上是不会有 ​​mountTo​​ 这个方法的。

所以这里我们还需要给普通的元素加上一个 Wrapper 类,让他们可以保持我们元素类的标准格式。也是所谓的标准接口。

我们先写一个 ​​ElementWrapper​​ 类,这个类的内容其实与我们的 Div 是基本一致的。唯有两个区别

  1. 在创建 DOM 节点的时候,可以通过传当前元素名​​type​​ 到我们的构造函数,并且用这个 type 去建立我们的 DOM 节点
  2. appendChild 就不能直接使用​​this.root.appendChild​​​,因为所有普通的标签都被改为我们的自定义类,所以 appendChild 的逻辑需要改为​​child.mountTo(this.root)​
class ElementWrapper {
// 构造函数
// 创建 DOM 节点
constructor(type) {
this.root = document.createElement(type);
}
// 挂載元素的属性
setAttribute(name, attribute) {
this.root.setAttribute(name, attribute);
}
// 挂載元素子元素
appendChild(child) {
child.mountTo(this.root);
}
// 挂載当前元素
mountTo(parent) {
parent.appendChild(this.root);
}
}

class Div {
// 构造函数
// 创建 DOM 节点
constructor() {
this.root = document.createElement('div');
}
// 挂載元素的属性
setAttribute(name, attribute) {
this.root.setAttribute(name, attribute);
}
// 挂載元素子元素
appendChild(child) {
child.mountTo(this.root);
}
// 挂載当前元素
mountTo(parent) {
parent.appendChild(this.root);
}
}

这里我们还有一个问题,就是遇到文本节点的时候,是没有转换成我们的自定义类的。所以我们还需要写一个给文本节点,叫做 ​​TextWrapper​​。

class TextWrapper {
// 构造函数
// 创建 DOM 节点
constructor(content) {
this.root = document.createTextNode(content);
}
// 挂載元素的属性
setAttribute(name, attribute) {
this.root.setAttribute(name, attribute);
}
// 挂載元素子元素
appendChild(child) {
child.mountTo(this.root);
}
// 挂載当前元素
mountTo(parent) {
parent.appendChild(this.root);
}
}

有了这些元素类接口后,我们就可以改写我们 ​​createElement​​​ 里面的逻辑。把我们原本的 ​​document.createElement​​​ 和 ​​document.createTextNode​​​ 都替换成实例化 ​​new ElementWrapper(type)​​​ 和 ​​new TextWrapper(content)​​即可。

function createElement(type, attributes, ...children) {
// 创建元素
let element;
if (typeof type === 'string') {
element = new ElementWrapper(type);
} else {
element = new type();
}

// 挂上属性
for (let name in attributes) {
element.setAttribute(name, attributes[name]);
}
// 挂上所有子元素
for (let child of children) {
if (typeof child === 'string')
child = new TextWrapper(child);
element.appendChild(child);
}
// 最后我们的 element 就是一个节点
// 所以我们可以直接返回
return element;
}

然后我们 webpack 打包一下看看。

用 JSX 建立组件 Parser(解析器)_html_27

没有任何意外,我们整个元素就正常的被挂載在 body 的上了。同理如果我们把我们的 Div 改回 div 也是一样可以正常运行的。

当然我们一般来说也不会写一个毫无意义的这种 Div 的元素。这里我们就会写一个我们组件的名字,比如说 ​​Carousel​​,一个轮播图的组件。



​完整代码​​ —— 对你有用的话,就给我一个 ⭐️ 吧,谢谢!



用 JSX 建立组件 Parser(解析器)_javascript_29

博主开始在B站直播学习,欢迎过来《​​直播间​​》一起学习。

我们在这里互相监督,互相鼓励,互相努力走上人生学习之路,让学习改变我们生活!

学习的路上,很枯燥,很寂寞,但是希望这样可以给我们彼此带来多一点陪伴,多一点鼓励。我们一起加油吧! (๑ •̀ㅂ•́)و



我是来自《​技术银河​》的​三钻​,一位正在重塑知识的技术人。下期再见。


用 JSX 建立组件 Parser(解析器)_自定义_02

用 JSX 建立组件 Parser(解析器)_自定义_02

用 JSX 建立组件 Parser(解析器)_javascript_32


更多内容详见微信公众号:Python研究所

用 JSX 建立组件 Parser(解析器)_html_33