使用 JSX 建立 Markup 组件风格
这里我们一起从 0 开始搭建一个组件系统。首先通过上一篇《前端组件化基础知识》和《用 JSX 建立组件 Parser(解析器)》中知道,一个组件可以通过 Markup 和 JavaScript 访问的一个环境。
所以我们的第一步就是建立一个可以使用 markup 的环境。这里我们会学习使用 JSX 来建立 markup 的风格。这里我们基于与 React 一样的 JSX 去建立我们组件的风格。
JSX 环境搭建
JSX 在大家一般认知里面,它是属于 React 的一部分。其实 Facebook 公司会把 JSX 定义为一种纯粹的语言扩展。而这个 JSX 也是可以被其他组件体系去使用的。
甚至我们可以把它单独作为一种,快捷创建 HTML 标签的方式去使用。
建立项目
那么我们就从最基础的开始,首先我们需要创建一个新的项目目录:
初始化 NPM
当然在你们喜欢的目录下创建这个项目文件夹。建立好文件夹之后,我们就可以进入到这个目录里面并且初始化 npm
。
执行以上命令之后,会出现一些项目配置的选项问题,如果有需要可以自行填写。不过我们也可以直接一直按回车,然后有需要的同学可以后面自己打开 package.json
自行修改。
安装 webpack
Wepack 很多同学应该都了解过,它可以帮助我们把一个普通的 JavaScript 文件变成一个能把不同的 import 和 require 的文件给打包到一起。
所以我们需要安装 webpack
,当然我们也可以直接使用 npx 直接使用 webpack,也可以全局安装 webpack-cli。
那么这里我们就使用全局安装 webpack-cli:
安装完毕之后,我们可以通过输入下面的一条命令来检车一下安装好的 webpack 版本。如果执行后没有报错,并且出来了一个版本号,证明我们已经安装成功了。
安装 Babel
因为 JSX 它是一个 babel 的插件,所以我们需要依次安装 webpack,babel-loader, babel 和 babel 的 plugin。
这里使用 Babel 还有一个用处,它可以把一个新版本的 JavaScript 编译成一个老版本的 JavaScript,这样我们就可以支持在更多老版本的浏览器中运行。
安装 Babel 我们只需要执行以下的命令即可。
这里我们需要注意的是,我们需要加上 --save-dev
,这样我们就会把 babel 加入到我们的开发依赖中。
执行完毕后,我们应该会看到上面图中的消息。
为了验证我们是正确安装好了,我们可以打开我们项目目录下的 package.json
。
好,我们可以看到在 devDependencies
下方,确实是有我们刚刚安装的两个包。还是担心的同学,可以再和 package.json
确认一下眼神哈。
配置 webpack
到这里我们就需要配置一下 webpack 的配置。配置 webpack 我们需要创建一个 webpack.config.js
配置文件。
在我们项目的根目录创建一个 webpack.config.js
文件。
首先 webpack config 它是一个 nodejs 的模块,所以我们需要用 module.exports 来写它的设置。而这个是早期 nodejs 工具常见的一种配置方法,它用一个 JavaScript 文件去做它的配置,这样它在这个配置里面就可以加入一些逻辑。
Webpack 最基本的一个东西,就是需要设置一个 entry (设置它的入口文件)。这里我们就设置一个 main.js
即可。
这个时候,我们就可以先在我们的根目录下创建一下我们 main.js
的文件了。在里面我们先加入一个简单的 for
循环。
这样我们 webpack 的基本配置就配置好了,我们在根目录下执行一下 webpack 来打包一下我们 main.js
的文件看看。我们只需要执行下面的这行命令:
执行完毕之后,我们就可以在命令行界面中看到上面这样的一段提示。
注意细节的同学,肯定要举手问到,同学同学!你的命令行中报错啦!黄色部分确实有给我们一个警告,但是不要紧,这个我们接下的配置会修复它的。
这个时候我们会发现,在我们的根目录中生成了一个新的文件夹 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
。我们只需要执行下面的命令行来安装:
最终的结果就如上图一样,证明安装成功了。这个时候我们就需要在 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
最后我们的配置文件就会是这个样子:
这样配置好之后,我们就可以来跑一下 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'
。
改好之后,我们在使用 webpack
编译一下,看看我们的 main.js
有什么区别。
显然我们发现,编译后的代码没有被压缩成一行了。这样我们就可以调试 webpack 生成的代码了。这里我们可以注意到,我们在 main.js
中的代码被转成字符串,并且被放入一个 eval()
的函数里面。我们的代码被放入了 eval
里面,那么我们就可以在调试的时候就可以把它作为一个单独的文件去使用了。
引入 JSX
万事俱备,只欠东风了,最后我们需要如何引入 JSX 呢?在引入之前,我们来看看,如果就使用现在的配置在我们的 main.js
里面使用 JSX 语法会怎么样。作为程序员的我们,总得有点冒险精神!
所以我们在我们的 main.js
里面加入这段代码:
然后我们执行 webpack 看看!
好家伙!果然报错了。这里的报错告诉我们,在 =
后面不能使用 “小于号”,但是在正常的 JSX 语法中,这个其实是 HTML 标签的 “尖括号”,因为没有 JSX 语法的编译过程,所以 JavaScript 默认就会认为这个就是 “小于号”。
所以我们要怎么做让我们的 webpack 编译过程支持 JSX 语法呢?这里其实就是还需要我们加入一个最关键的一个包,而这个包名非常的长,叫做 @babel/plugin-transform-react-jsx
。好那么我们就执行一段命令来安装一下这个包:
安装好之后,我们还需要在 webpack 配置中给他加入进去。我们需要在 module
里面的 rules
里面的 use
里面加入一个 plugins
的配置中加入 ['@babel/plugin-transform-react-jsx']
。
然后最终我们的 webpack 配置文件就是这样的:
配置好之后,我们再去执行一下 webpack。这时候我们发现没有再报错了。这样也就证明我们的代码现在是支持使用 JSX 语法编写了。
最后我们来围观一下,最后编程的效果是怎么样的。
我们会发现,在 eval
里面我们加入的 <div/>
被翻译成一个 React.createElement("div", null)
的函数调用了。
所有接下来我们就一起来看一下,我们应该怎么实现这个 React.createElement
,以及我们能否把这个换成我们自己的函数名字。
JSX 基本用法
首先我们来尝试理解 JSX,JSX 其实它相当于一个纯粹在代码语法上的一种快捷方式。在上一部分的结尾我们看到,JSX 语法在被编译后会出现一个 React.createElement
的一个调用。
JSX 基础原理
那么这里我们就先修改在 webpack 中的 JSX 插件,给它一个自定义的创建元素函数名。我们打开 webpack.config.js,在 plugins 的位置,我们把它修改一下。
上面我们只是把原来的 ['@babel/plugin-transform-react-jsx']
参数改为了 [['@babel/plugin-transform-react-jsx', {pragma: 'createElement'}]]
。加入了这个 pragma
参数,我们就可以自定义我们创建元素的函数名。
这么一改,我们的 JSX 就与 React 的框架没有任何联系了。我们执行一下 webpack 看一下最终生成的效果,就会发现里面的 React.createElement
就会变成 createElement
。
接下来我们加入一个 HTML 文件来执行我们的 main.js 试试。首先在根目录创建一个 main.html
,然后输入一下代码:
然后我们执行在浏览器打开这个 HTML 文件。
这个时候我们控制台会给我们抛出一个错误,我们的 createElement
未定义。确实我们在 main.js
里面还没有定义这个函数,所以说它找不到。
所以我们就需要自己编写一个 createElement
这个函数。我们直接打开根目录下的 main.js
并且把之前的 for
循环给删除了,然后加上以下代码:
这里我们就直接返回空,先让这个函数可以被调用即可。我们用 webpack 重新编译一次,然后刷新我们的 main.html 页面。这个时候我们就会发现报错没有了,可以正常运行。
实现 createElement 函数
在我们的编译后的代码中,我们可以看到 JSX 的元素在调用 createElement 的时候是传了两个参数的。第一个参数是 div
, 第二个是一个 null
。
这里第二个参数为什么是 null
呢?其实第二个参数是用来传属性列表的。如果我们在 main.js 里面的 div 中加入一个 id="a"
,我们来看看最后编译出来会有什么变化。
我们就会发现第二个参数变成了一个 以 Key-Value 的方式存储的 JavaScript 对象。到这里如果我们想一下,其实 JSX 也没有那么神秘,它只是把我们平时写的 HTML 通过编译改写成了 JavaScript 对象,我们可以认为它是属于一种 “语法糖”。
但是 JSX 影响了代码的结构,所以我们一般也不会完全把它叫作语法糖。
接下来我们来写一些更复杂一些的 JSX,我们给我们原本的 div 加一些 children 元素。
最后我们执行一下 webpack 打包看看效果。
在控制台中,我们可以看到最后编译出来的结果,是递归的调用了 createElement
这个函数。这里其实已经形成了一个树形的结构。
父级就是第一层的 div 的元素,然后子级就是在后面当参数传入了第一个 createElement 函数之中。然后因为我们的 span 都是没有属性的,所以所有后面的 createElement 的第二个参数都是 null
。
根据我们这里看到的一个编译结果,我们就可以分析出我们的 createElement 函数应有的参数都是什么了。
第一个参数
type
—— 就是这个标签的类型第二个参数
attribute
—— 标签内的所有属性与值剩余的参数都是子属性
...children
—— 这里我们使用了 JavaScript 之中比较新的语法...children
表示把后面所有的参数 (不定个数) 都会变成一个数组赋予给 children 变量
那么我们 createElement
这个函数就可以写成这样了:
函数我们有了,但是这个函数可以做什么呢?其实这个函数可以用来做任何事情,因为这个看起来长的像 DOM API,所以我们完全可以把它做成一个跟 React 没有关系的实体 DOM。
比如说我们就可以在这个函数中返回这个 type
类型的 element
元素。这里我们把所有传进来的 attributes
给这个元素加上,并且我们可以给这个元素挂上它的子元素。
创建元素我们可以用 createElement(type)
,而加入属性我们可以使用 setAttribute()
,最后挂上子元素就可以使用 appendChild()
。
这里我们就实现了 createElement
函数的逻辑。最后我们还需要在页面上挂載上我们的 DOM 节点。所以我们可以直接挂載在 body 上面。
这里还需要注意的是,我们的 main.html 中没有加入 body 标签,没有的话我们是无法挂載到 body 之上的。所以这里我们就需要在 main.html 当中加入 body 标签。
好,这个时候我们就可以 webpack 打包,看一下效果。
Wonderful! 我们成功的把节点生成并且挂載到 body 之上了。但是如果我们的 div
里面加入一段文字,这个时候就会有一个文本节点被传入我们的 createElement
函数当中。毋庸置疑,我们的 createElement
函数以目前的逻辑是肯定无法处理文本节点的。
接下来我们就把处理文本节点的逻辑加上,但是在这之前我们先把 div 里面的 span 标签删除,换成一段文本 “hello world”。
在我们还没有加入文本节点的逻辑之前,我们先来 webpack 打包一下,在我们挂上子节点之前,判断看看具体会报什么错误。
首先我们可以看到,在 createElement
函数调用的地方,我们的文本被当成字符串传入,然后这个参数是接收子节点的,并且在我们的逻辑之中我们使用了 appendChild
,这个函数是接收 DOM 节点的。显然我们的 文本字符串不是一个节点,自然就会报错。
通过这种调试方式我们可以马上定位到,我们需要在哪里添加逻辑去实现我们这个功能。这种方式也可以算是一种捷径吧。
所以接下来我们就回到 main.js
,在我们挂上子节点之前,判断一下 child 的类型,如果它的类型是 “String” 字符串的话,就使用 createTextNode()
来创建一个文本节点,然后再挂載到父元素上。这样我们就完成了字符节点的处理了。
我们用这个最新的代码 webpack 打包之后,就可以在浏览器上看到我们的文字被显示出来了。
到了这里我们编写的 createElement
已经是一个比较有用的东西了,我们已经可以用它来做一定的 DOM 操作了。甚至它可以完全代替我们自己去写 document.createElement
的这种反复繁琐的操作了。
这里我们可以验证一下,我们在 div 当中重新加上我们之前的三个 span, 并且在每个 span 中加入文本。
然后我们重新 webpack 打包后,就可以看到确实是可以完整这种 DOM 的操作的。
现在的代码已经可以完成一定的组件化的基础能力。
实现自定义标签
之前我们都是在用一些,HTML 自带的标签。如果我们现在把 div 中的 d 改为大写 D 会怎么样呢?
果不其然,就是会报错的。不过这就是我们找到问题的根源的关键,这里我们发现当我们把 div 改为 Div 的时候,传入我们 createElement
的 div 从字符串 'div' 变成了一个 Div
类。
当然我们的 JavaScript 中并没有定义 Div 类,这里自然就会报 Div 未定义的错误。知道问题的所在,我们就可以去解决它,首先我们需要先解决未定义的问题,所以我们先建立一个 Div 的类。
然后我们就需要在 createElement
里面做类型判断,如果我们遇到的 type 是字符类型,就按原来的方式处理。如果我们遇到是其他情况,我们就实例化传过来的 type
。
这里我们还有一个问题,我们有什么办法可以让自定义标签像我们普通 HTML 标签一样操作呢?在最新版的 DOM 标准里面是有办法的,我们只需要去注册一下我们自定义标签的名称和类型。
但是我们现行比较安全的浏览版本里面,还是不太建议这样去做的。所以在使用我们的自定义 element 的时候,还是建议我们自己去写一个接口。
首先我们是需要建立标签类,这个类能让任何标签像我们之前普通 HTML 标签的元素一样最后挂載到我们的 DOM 树上。
它会包含以下方法:
mountTo()
—— 创建一个元素节点,用于后面挂載到parent
父级节点上setAttribute()
—— 给元素挂上所有它的属性appendChild()
—— 给元素挂上所有它的子元素
首先我们来简单实现我们 Div
类中的 mountTo
方法,这里我们还需要给他加入 setAttribute
和 appendChild
方法,因为在我们的 createElement
中有挂載属性子元素的逻辑,如果没有这两个方法就会报错。但是这个时候我们先不去实现这两个方法的逻辑,方法内容留空即可。
这里面其实很简单首先给类中的 root
属性创建成一个 div 元素节点,然后把这个节点挂載到这个元素的父级。这个 parent
是以参数传入进来的。
然后我们就可以把我们原来的 body.appendChild 的代码改为使用 mountTo
方法来挂載我们的自定义元素类。
用现在的代码,我们 webpack 打包看一下效果:
我们可以看到我们的 Div 自定义元素是有正确的被挂載到 body 之上。但是 Div 中的 span 标签都是没有被挂載上去的。如果我们想它与普通的 div 一样去工作的话,我们就需要去实现我们的 setAttribute
和 appendChild
逻辑。
接下来我们就一起来尝试完成剩余的实现逻辑。在开始写 setAttribute 和 appendChild 之前,我们需要先给我们的 Div 类加入一个构造函数 constructor
。在这里个里面我们就可以把元素创建好,并且代理到 root
上。
然后的 setAttribute
方法其实也很简单,就是直接使用 this.root
然后调用 DOM API 中的 setAttribute
就可以了。而 appendChild
也是同理。最后我们的代码就是如下:
我们 webpack 打包一下看看效果:
我们可以看到,div 和 span 都被成功挂載到 body 上。也证明我们自制的 div 也能正常工作了。
这里还有一个问题,因为我们最后调用的是 a.mountTo()
,如果我们的 变量 a
不是一个自定义的元素,而是我们普通的 HTML 元素,这个时候他们身上是不会有 mountTo
这个方法的。
所以这里我们还需要给普通的元素加上一个 Wrapper 类,让他们可以保持我们元素类的标准格式。也是所谓的标准接口。
我们先写一个 ElementWrapper
类,这个类的内容其实与我们的 Div 是基本一致的。唯有两个区别
在创建 DOM 节点的时候,可以通过传当前元素名
type
到我们的构造函数,并且用这个 type 去建立我们的 DOM 节点appendChild 就不能直接使用
this.root.appendChild
,因为所有普通的标签都被改为我们的自定义类,所以 appendChild 的逻辑需要改为child.mountTo(this.root)
这里我们还有一个问题,就是遇到文本节点的时候,是没有转换成我们的自定义类的。所以我们还需要写一个给文本节点,叫做 TextWrapper
。
有了这些元素类接口后,我们就可以改写我们 createElement
里面的逻辑。把我们原本的 document.createElement
和 document.createTextNode
都替换成实例化 new ElementWrapper(type)
和 new TextWrapper(content)
即可。
然后我们 webpack 打包一下看看。
没有任何意外,我们整个元素就正常的被挂載在 body 的上了。同理如果我们把我们的 Div 改回 div 也是一样可以正常运行的。
当然我们一般来说也不会写一个毫无意义的这种 Div 的元素。这里我们就会写一个我们组件的名字,比如说 Carousel
,一个轮播图的组件。
完整代码 —— 对你有用的话,就给我一个 ⭐️ 吧,谢谢!
我是来自《技术银河》的*三钻*,一位正在重塑知识的技术人。下期再见。
版权声明: 本文为 InfoQ 作者【三钻】的原创文章。
原文链接:【http://xie.infoq.cn/article/abb8974ac6615053c42fbbe0c】。文章转载请联系作者。
评论