使用JSX 建立组件 Parser(解析器)开发的示例

目录
  • JSX 环境搭建
  • 建立项目
  • 初始化 NPM
  • 安装 webpack
  • 安装 Babel
  • 配置 webpack
  • 安装 Babel-loader
  • 模式配置
  • 引入 JSX
  • JSX 基本用法
  • JSX 基础原理
  • 实现 createElement 函数
  • 实现自定义标签

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

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

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

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 加入到我们的开发依赖中。

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

为了验证我们是正确安装好了,我们可以打开我们项目目录下的 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 确认一下眼神哈。

配置 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

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

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

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

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

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

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

安装 Babel-loader

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

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

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

在我们上面配置好的 webpack.config.jsentry 后面添加一个选项叫做 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 即可。

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

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

编译后的结果,我们会发现 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 有什么区别。

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

引入 JSX

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

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

var a = <div/>

然后大胆地执行 webpack 看看!

好家伙!果然报错了。这里的报错告诉我们,在 = 后面不能使用 “小于号”,但是在正常的 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 语法了。

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

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

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

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

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

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

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

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

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

function createElement() {
 return;
}

let a = <div />;

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

实现 createElement 函数

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

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

我们就会发现第二个参数变成了一个以 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 打包看看效果。

在控制台中,我们可以看到最后编译出来的结果,是递归的调用了 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 打包,看一下效果。

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

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

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

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

首先我们可以看到,在 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 的操作的。

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

实现自定义标签

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

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

果不其然,就是会报错的。不过我们找到了问题根源的关键,这里我们发现当我们把 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 方法,这里我们还需要给他加入 setAttributeappendChild 方法,因为在我们的 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 打包看一下效果:

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

接下来我们就一起来尝试完成剩余的实现逻辑。在开始写 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 打包一下看看效果:

我们可以看到,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.createElementdocument.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 打包一下看看。

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

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

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



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

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



到此这篇关于使用JSX 建立组件 Parser(解析器)开发的示例的文章就介绍到这了,更多相关JSX建立组件Parser内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 使用JSX 建立 Markup 组件风格开发的示例(前端组件化)

    目录 JSX 环境搭建 初始化 NPM 安装 webpack 安装 Babel 配置 webpack 安装 Babel-loader 模式配置 引入 JSX JSX 基本用法 JSX 基础原理 实现 createElement 函数 实现自定义标签 它会包含以下方法: 这里我们一起从 0 开始搭建一个组件系统.首先通过上一篇<前端组件化基础知识>和<用 JSX 建立组件 Parser(解析器)>中知道,一个组件可以通过 Markup 和 JavaScript 访问的一个环境. 所以

  • 使用JSX实现Carousel轮播组件的方法(前端组件化)

    在我们用 JSX 建立组件系统之前,我们先来用一个例子学习一下组件的实现原理和逻辑.这里我们就用一个轮播图的组件作为例子进行学习.轮播图的英文叫做 Carousel,它有一个旋转木马的意思. 上一篇文章<使用 JSX 建立 Markup 组件风格>中我们实现的代码,其实还不能称为一个组件系统,顶多是可以充当 DOM 的一个简单封装,让我们有能力定制 DOM. 要做这个轮播图的组件,我们应该先从一个最简单的 DOM 操作入手.使用 DOM 操作把整个轮播图的功能先实现出来,然后在一步一步去考虑怎

  • vue组件jsx语法的具体使用

    如果使用render函数来写比较复杂的vue组件,对于可读性和可维护性都很不友好,而使用jsx就会让我们回到更接近于模板的语法.babel转译器会将jsx转译为render函数渲染. 配置 需要用到babel插件 安装 npm install\ babel-plugin-syntax-jsx\ babel-plugin-transform-vue-jsx\ babel-helper-vue-jsx-merge-props\ babel-preset-env\ --save-dev .babelr

  • 详解利用jsx写vue组件的方法示例

    前言 本文主要给大家介绍的是关于利用jsx写vue组件,下面话不多说,来一起看看详细的介绍吧. 我们平常写vue的组件时,一般都是用的是模版,这种方式看起来比较简洁,而且vue作者也推荐使用这个方式,但是这种方式也有一些它的弊端,例如模版调试麻烦,或者在一些场景下模版描述可能没那么简单和方便. 下面我们要讲的是如何在vue里面写jsx,知道react的人应该都知道jsx,jsx的一个特性就是非常灵活,虽然有的人觉得jsx很丑陋,把逻辑都写到模版的感觉,但萝卜青菜各有所爱,适合自己适合团队的就是最

  • 使用JSX 建立组件 Parser(解析器)开发的示例

    目录 JSX 环境搭建 建立项目 初始化 NPM 安装 webpack 安装 Babel 配置 webpack 安装 Babel-loader 模式配置 引入 JSX JSX 基本用法 JSX 基础原理 实现 createElement 函数 实现自定义标签 这里我们一起从 0 开始搭建一个组件系统.首先通过上一篇<前端组件化基础知识>中知道,一个组件可以通过 Markup 和 JavaScript 访问的一个环境. 所以我们的第一步就是建立一个可以使用 markup 的环境.这里我们会学习使

  • sql字段解析器的实现示例

    目录 1. 解题思路 2. 具体解析实现 3. 单元测试 用例:有一段sql语句,我们需要从中截取出所有字段部分,以便进行后续的类型推断或者别名字段抽取定义,请给出此解析方法. 想来很简单吧,因为 sql 中的字段列表,使用方式有限,比如 a as b, a, a b... 1. 解题思路 如果不想做复杂处理,最容易想到的,就是直接用某个特征做分割即可.比如,先截取出 字段列表部分,然后再用逗号',' 分割,就可以得到一个个的字段了.然后再要细分,其实只需要用 as 进行分割就可以了. 看起来好

  • jQuery EasyUI API 中文文档 - Parser 解析器

    用法 复制代码 代码如下: $.parser.parse(); // 解析整个页面 $.parser.parse('#cc'); // 解析某个具体节点 特性 名称 类型 说明 默认值 $.parser.auto boolean 定义是否自动解析easyui组件. true 事件 名称 参数 说明 $.parser.onComplete context 当解析器完成解析动作的时候触发. 方法 名称 参数 说明 $.parser.parse context 解析easyui组件.

  • Java Class 解析器实现方法示例

    最近在写一个私人项目,名字叫做ClassAnalyzer,ClassAnalyzer的目的是能让我们对Java Class文件的设计与结构能够有一个深入的理解.主体框架与基本功能已经完成,还有一些细节功能日后再增加.实际上JDK已经提供了命令行工具javap来反编译Class文件,但本篇文章将阐明我实现解析器的思路. Class文件 作为类或者接口信息的载体,每个Class文件都完整的定义了一个类.为了使Java程序可以"编写一次,处处运行",Java虚拟机规范对Class文件进行了严

  • python中parser.add_argument()用法实例(命令行选项、参数和子命令解析器)

    目录 一.argparse介绍 二.argparse使用——代码示例 1.创建一个解析器——创建 ArgumentParser() 对象 2.添加参数——调用 add_argument() 方法添加参数 3.解析参数——使用 parse_args() 解析添加的参数 四.python args parse_args() 报错解决 1.error: the following arguments are required: xxx 五.其他问题汇总(评论小伙伴问的) 1.下划线_和横线-的区别 2

  • unified如何处理markdown解析器详解

    目录 unified是什么 unified生态简介 工作原理 Parse Transform Stringify 牛刀小试 环境搭建 处理ESM类型包 最简用法 加载文档meta 一个实际使用例子: unified是什么 unified是用于文档处理的生态系统,核心包提供了文档处理的流程控制,具体功能由生态系统中各个插件提供.例如我们如果需要处理markdown,就需要使用markdown处理相关的插件.当然除了markdwon以外,还提供了处理HTML.JSX等的插件.其良好的扩展能力能让我们

  • 仅用500行Python代码实现一个英文解析器的教程

    语法分析器描述了一个句子的语法结构,用来帮助其他的应用进行推理.自然语言引入了很多意外的歧义,以我们对世界的了解可以迅速地发现这些歧义.举一个我很喜欢的例子: 正确的解析是连接"with"和"pizza",而错误的解析将"with"和"eat"联系在了一起: 过去的一些年,自然语言处理(NLP)社区在语法分析方面取得了很大的进展.现在,小小的 Python 实现可能比广泛应用的 Stanford 解析器表现得更出色. 文章剩下

  • C#实现WebSocket协议客户端和服务器websocket sharp组件实例解析

    看到这篇文章的题目,估计很多人都会问,这个组件是不是有些显的无聊了,说到web通信,很多人都会想到ASP.NET SignalR,或者Nodejs等等,实现web的网络实时通讯.有关于web实时通信的相关概念问题,在这里就不再做具体的介绍了,有兴趣的可以自行百度. 下面我们介绍一款WebSocket组件websocket-sharp的相关内容. 一.websocket-sharp组件概述 websocket-sharp是一个C#实现websocket协议客户端和服务端,websocket-sha

  • 简单了解SpringMVC常用组件作用解析

    spring mvc的核心在于其流程,这是使用springmvc框架的基础,springmvc是一种基于servlet的技术,它提供了核心控制器dispatcherservlet和相关的组件,并制订了松散的结构,一使用各种灵活的需求. 首先spring mvc框架是围绕DispatcherServlet而工作的,所以这个类是其最为重要的类.从他的名字来看,他是一个servlet,那么根据javaEE基础的学习,我们知道可以拦截http发送过来的请求,在servlet初始化时,spring mvc

随机推荐