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

此文转载自:https://tridiamond.blog.csdn.net/article/details/112352745

这里我们一起从 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,一个轮播图的组件。

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



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

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

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


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

推荐专栏

小伙伴们可以查看或者订阅相关的专栏,从而集中阅读相关知识的文章哦。

  • ⛳️ 《2021年总结》 — 一个一线战场中的开发者,回归到学习的学堂中。一开始这个过程确实遇到了挺多困扰的。一开始无法静心下来学习,因为学习底层的知识确实需要静下心来学。但是坚持了一段时间后,又会发现自己会爱上学习,爱上深挖这些知识。

  • 📖 《前端进阶》 — 这里包含的文章学习内容需要我们拥有 1-2 年前端开发经验后,选择让自己升级到高级前端工程师的学习内容(这里学习的内容是对应阿里 P6 级别的内容)。

  • 📖 《数据结构与算法》 — 到了如今,如果想成为一个高级开发工程师或者进入大厂,不论岗位是前端、后端还是AI,算法都是重中之重。也无论我们需要进入的公司的岗位是否最后是做算法工程师,前提面试就需要考算法。

  • 📖 《FCC前端集训营》 — 根据FreeCodeCamp的学习课程,一起深入浅出学习前端。稳固前端知识,一起在FreeCodeCamp获得证书

  • 📖 《前端星球》 — 以实战为线索,深入浅出前端多维度的知识点。内含有多方面的前端知识文章,带领不懂前端的童鞋一起学习前端,在前端开发路上童鞋一起燃起心中那团火🔥

已标记关键词 清除标记
相关推荐
©️2020 CSDN 皮肤主题: 博客之星2020 设计师:CY__ 返回首页