目錄
- JSX 環(huán)境搭建
- 建立項(xiàng)目
- 初始化 NPM
- 安裝 webpack
- 安裝 Babel
- 配置 webpack
- 安裝 Babel-loader
- 模式配置
- 引入 JSX
- JSX 基本用法
- JSX 基礎(chǔ)原理
- 實(shí)現(xiàn) createElement 函數(shù)
- 實(shí)現(xiàn)自定義標(biāo)簽
這里我們一起從 0 開(kāi)始搭建一個(gè)組件系統(tǒng)。首先通過(guò)上一篇《前端組件化基礎(chǔ)知識(shí)》中知道,一個(gè)組件可以通過(guò) Markup 和 JavaScript 訪問(wèn)的一個(gè)環(huán)境。
所以我們的第一步就是建立一個(gè)可以使用 markup 的環(huán)境。這里我們會(huì)學(xué)習(xí)使用兩種建立 markup 的風(fēng)格。
第一種是基于與 React 一樣的 JSX 去建立我們組件的風(fēng)格。第二種則是我們?nèi)ソ⒒陬愃?Vue 的這種,基于標(biāo)記語(yǔ)言的 Parser 的一種風(fēng)格。
JSX 環(huán)境搭建
JSX 在大家一般認(rèn)知里面,它是屬于 React 的一部分。其實(shí) Facebook 公司會(huì)把 JSX 定義為一種純粹的語(yǔ)言擴(kuò)展。而這個(gè) JSX 也是可以被其他組件體系去使用的。
甚至我們可以把它單獨(dú)作為一種,快捷創(chuàng)建 HTML 標(biāo)簽的方式去使用。
建立項(xiàng)目
那么我們就從最基礎(chǔ)的開(kāi)始,首先我們需要?jiǎng)?chuàng)建一個(gè)新的項(xiàng)目目錄:
- mkdir jsx-component
初始化 NPM
在你們喜歡的目錄下創(chuàng)建這個(gè)項(xiàng)目文件夾。建立好文件夾之后,我們就可以進(jìn)入到這個(gè)目錄里面并且初始化 npm
。
- npm init
執(zhí)行以上命令之后,會(huì)出現(xiàn)一些項(xiàng)目配置的選項(xiàng)問(wèn)題,如果有需要可以自行填寫。不過(guò)我們也可以直接一直按回車,然后有需要的同學(xué)可以后面自己打開(kāi) package.json
自行修改。
安裝 webpack
Wepack 很多同學(xué)應(yīng)該都了解過(guò),它可以幫助我們把一個(gè)普通的 JavaScript 文件變成一個(gè)能把不同的 import 和 require 的文件給打包到一起。
所以我們需要安裝 webpack
,當(dāng)然我們也可以直接使用 npx 直接使用 webpack,也可以全局安裝 webpack-cli。
那么這里我們就使用全局安裝 webpack-cli:
- npm install -g webpack webpack-cli
安裝完畢之后,我們可以通過(guò)輸入下面的一條命令來(lái)檢測(cè)一下安裝好的 webpack 版本。如果執(zhí)行后沒(méi)有報(bào)錯(cuò),并且出來(lái)了一個(gè)版本號(hào),證明我們已經(jīng)安裝成功了。
- webpack --version
安裝 Babel
因?yàn)?JSX 它是一個(gè) babel 的插件,所以我們需要依次安裝 webpack,babel-loader, babel 和 babel 的 plugin。
這里使用 Babel 還有一個(gè)用處,它可以把一個(gè)新版本的 JavaScript 編譯成一個(gè)老版本的 JavaScript,這樣我們的代碼就可以在更多老版本的瀏覽器中運(yùn)行。
安裝 Babel 我們只需要執(zhí)行以下的命令即可。
- npm install --save-dev webpack babel-loader
這里我們需要注意的是,我們需要加上 --save-dev
,這樣我們就會(huì)把 babel 加入到我們的開(kāi)發(fā)依賴中。
執(zhí)行完畢后,我們應(yīng)該會(huì)看到上面圖中的消息。
為了驗(yàn)證我們是正確安裝好了,我們可以打開(kāi)我們項(xiàng)目目錄下的 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
下方,確實(shí)是有我們剛剛安裝的兩個(gè)包。還是擔(dān)心的同學(xué),可以再和 package.json
確認(rèn)一下眼神哈。
配置 webpack
到這里我們就需要配置一下 webpack。配置 webpack 我們需要?jiǎng)?chuàng)建一個(gè) webpack.config.js
配置文件。
在我們項(xiàng)目的根目錄創(chuàng)建一個(gè) webpack.config.js
文件。
首先 webpack config 它是一個(gè) nodejs 的模塊,所以我們需要用 module.exports 來(lái)寫它的設(shè)置。而這個(gè)是早期 nodejs 工具常見(jiàn)的一種配置方法,它用一個(gè) JavaScript 文件去做它的配置,這樣它在這個(gè)配置里面就可以加入一些邏輯。
- module.exports = {}
Webpack 最基本的一個(gè)東西,就是需要設(shè)置一個(gè) entry (設(shè)置它的入口文件)。這里我們就設(shè)置一個(gè) main.js
即可。
- module.exports = {
- entry: "./main.js"
- }
這個(gè)時(shí)候,我們就可以先在我們的根目錄下創(chuàng)建一個(gè) main.js
的文件了。在里面我們先加入一個(gè)簡(jiǎn)單的 for
循環(huán)。
- // main.js 文件內(nèi)容
- for (let i of [1, 2, 3]) {
- console.log(i);
- }
這樣 webpack 的基本配置就配置好了,我們?cè)诟夸浵聢?zhí)行一下 webpack 來(lái)打包一下 main.js
的文件來(lái)看看。需要執(zhí)行下面的這行命令進(jìn)行打包:
- webpack
執(zhí)行完畢之后,我們就可以在命令行界面中看到上面這樣的一段提示。
注意細(xì)節(jié)的同學(xué),肯定要舉手問(wèn)到,同學(xué)同學(xué)!你的命令行中報(bào)錯(cuò)啦!黃色部分確實(shí)有給我們一個(gè)警告,但是不要緊,這個(gè)我們接下的配置會(huì)修復(fù)它的。
這個(gè)時(shí)候我們會(huì)發(fā)現(xiàn),在我們的根目錄中生成了一個(gè)新的文件夾 dist
。這個(gè)就是 webpack 打包默認(rèn)生成的文件夾,我們所有打包好的 JavaScript 和資源都會(huì)被默認(rèn)放入這個(gè)文件夾當(dāng)中。
這里我們就會(huì)發(fā)現(xiàn),這個(gè) dist
文件夾里面有一個(gè)打包好的 main.js
的文件,這個(gè)就是我們寫的 main.js
,通過(guò) webpack 被打包好的版本。
然后我們打開(kāi)它,就會(huì)看到它被 babel 編譯過(guò)后的 JavaScript 代碼。我們會(huì)發(fā)現(xiàn)我們短短的幾行代碼被加入了很多的東西,這些其實(shí)我們都不用管,那都是 Webpack 的 “喵喵力量”。
在代碼的最后面,還是能看到我們編寫的 for
循環(huán)的,只是被改造了一下,但是它的作用是一致的。
安裝 Babel-loader
接下來(lái)我們來(lái)安裝 babel-loader,其實(shí) babel-loader 并沒(méi)有直接依賴 babel 的,所以我們才需要另外安裝 @babel/core
和 @babel/preset-env
。我們只需要執(zhí)行下面的命令行來(lái)安裝:
- npm install --save-dev @babel/core @babel/preset-env
最終的結(jié)果就如上圖一樣,證明安裝成功了。這個(gè)時(shí)候我們就需要在 webpack.config.js
中配置上,讓我們打包的時(shí)候用上 babel-loader。
在我們上面配置好的 webpack.config.js
的 entry
后面添加一個(gè)選項(xiàng)叫做 module
。
然后模塊中我們還可以加入一個(gè) rules
,這個(gè)就是我們構(gòu)建的時(shí)候所使用的規(guī)則。而 rules
是一個(gè)數(shù)組類型的配置,這里面的每一個(gè)規(guī)則是由一個(gè) test
和一個(gè) use
組成的。
test:
-
test
的值是一個(gè)正則表達(dá)式,用于匹配我們需要使用這個(gè)規(guī)則的文件。這里我們需要把所有的 JavaScript 文件給匹配上,所以我們使用/\.js/
即可。
use: loader:
-
只需要加入我們的
babel-loader
的名字即可
options:
presets:
-
這里是 loader 的選項(xiàng),這里我們需要加入
@babel/preset-env
最后我們的配置文件就會(huì)是這個(gè)樣子:
- module.exports = {
- entry: './main.js',
- module: {
- rules: [
- {
- test: /\.js$/,
- use: {
- loader: 'babel-loader',
- options: {
- presets: ['@babel/preset-env'],
- },
- },
- },
- ],
- },
- };
這樣配置好之后,我們就可以來(lái)跑一下 babel 來(lái)試一試會(huì)是怎么樣的。與剛才一樣,我們只需要在命令行執(zhí)行 webpack
即可。
如果我們的配置文件沒(méi)有寫錯(cuò),我們就應(yīng)該會(huì)看到上面圖中的結(jié)果。
然后我們進(jìn)入 dist
文件夾,打開(kāi)我們編譯后的 main.js
,看一下我們這次使用了 babel-loader 之后的編譯結(jié)果。
編譯后的結(jié)果,我們會(huì)發(fā)現(xiàn) for of
的循環(huán)被編譯成了一個(gè)普通的 for
循環(huán)。這個(gè)也可以證明我們的 babel-loader 起效了,正確把我們新版本的 JavaScript 語(yǔ)法轉(zhuǎn)成能兼容舊版瀏覽器的 JavaScript 語(yǔ)法。
到了這里我們已經(jīng)把 JSX 所需的環(huán)境給安裝和搭建完畢了。
模式配置
最后我們還需要在 webpack.config.js 里面添加一個(gè)環(huán)境配置,不過(guò)這個(gè)是可加也可不加的,但是我們?yōu)榱似綍r(shí)開(kāi)發(fā)中的方便。
所以我們需要在 webpack.config.js 中添加一個(gè) mode
,這我們使用 development
。這個(gè)配置表示我們是開(kāi)發(fā)者模式。
一般來(lái)說(shuō)我們?cè)诖a倉(cāng)庫(kù)里面寫的 webpack 配置都會(huì)默認(rèn)加上這個(gè) mode: 'development'
的配置。當(dāng)我們真正發(fā)布的時(shí)候,我們就會(huì)把它改成 mode: 'production'
。
- module.exports = {
- entry: './main.js',
- mode: 'development',
- module: {
- rules: [
- {
- test: /\.js$/,
- use: {
- loader: 'babel-loader',
- options: {
- presets: ['@babel/preset-env'],
- },
- },
- },
- ],
- },
- };
改好之后,我們?cè)谑褂?webpack
編譯一下,看看我們的 main.js
有什么區(qū)別。
顯然我們發(fā)現(xiàn),編譯后的代碼沒(méi)有被壓縮成一行了。這樣我們就可以調(diào)試 webpack 生成的代碼了。這里我們可以注意到,我們?cè)?main.js
中的代碼被轉(zhuǎn)成字符串,并且被放入一個(gè) eval()
的函數(shù)里面。那么我們就可以在調(diào)試的時(shí)候把它作為一個(gè)單獨(dú)的文件去使用了,并且可以進(jìn)行斷點(diǎn)調(diào)試。
引入 JSX
萬(wàn)事俱備,只欠東風(fēng)了,最后我們需要如何引入 JSX呢?在引入之前,我們來(lái)看看,如果就使用現(xiàn)在的配置在我們的 main.js
里面使用 JSX 語(yǔ)法會(huì)怎么樣。作為程序員的我們,總得有點(diǎn)冒險(xiǎn)精神!
所以我們?cè)?main.js
里面加入這段代碼:
- var a = <div/>
然后大膽地執(zhí)行 webpack 看看!
好家伙!果然報(bào)錯(cuò)了。這里的報(bào)錯(cuò)告訴我們,在 =
后面不能使用 “小于號(hào)”,但是在正常的 JSX 語(yǔ)法中,這個(gè)其實(shí)是 HTML 標(biāo)簽的 “尖括號(hào)”,因?yàn)闆](méi)有 JSX 語(yǔ)法的編譯過(guò)程,所以 JavaScript 默認(rèn)就會(huì)認(rèn)為這個(gè)就是 “小于號(hào)”。
所以我們要怎么做讓我們的 webpack 編譯過(guò)程支持 JSX 語(yǔ)法呢?這里其實(shí)就是還需要我們加入一個(gè)最關(guān)鍵的一個(gè)包,而這個(gè)包名非常的長(zhǎng),叫做 @babel/plugin-transform-react-jsx
。執(zhí)行以下命令來(lái)安裝它:
- npm install --save-dev @babel/plugin-transform-react-jsx
安裝好之后,我們還需要在 webpack 配置中給他加入進(jìn)去。我們需要在 module
里面的 rules
里面的 use
里面加入一個(gè) 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'],
- },
- },
- },
- ],
- },
- };
配置好之后,我們?cè)偃?zhí)行一下 webpack。這時(shí)候我們發(fā)現(xiàn)沒(méi)有再報(bào)錯(cuò)了。這樣也就證明我們的代碼現(xiàn)在是支持使用 JSX 語(yǔ)法了。
最后我們來(lái)圍觀一下,最后編程的效果是怎么樣的。
我們會(huì)發(fā)現(xiàn),在 eval
里面我們加入的 <div/>
被翻譯成一個(gè) React.createElement("div", null)
的函數(shù)調(diào)用了。
所以接下來(lái)我們就一起來(lái)看一下,我們應(yīng)該怎么實(shí)現(xiàn)這個(gè) React.createElement
,以及我們能否把這個(gè)換成我們自己的函數(shù)名字。
JSX 基本用法
首先我們來(lái)嘗試?yán)斫?JSX,JSX 其實(shí)它相當(dāng)于一個(gè)純粹在代碼語(yǔ)法上的一種快捷方式。在上一部分的結(jié)尾我們看到,JSX語(yǔ)法在被編譯后會(huì)出現(xiàn)一個(gè) React.createElement
的調(diào)用。
JSX 基礎(chǔ)原理
那么這里我們就先修改在 webpack 中的 JSX 插件,給它一個(gè)自定義的創(chuàng)建元素函數(shù)名。我們打開(kāi) 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' }
- ]
- ],
- },
- },
- },
- ],
- },
- };
上面我們只是把原來(lái)的 ['@babel/plugin-transform-react-jsx']
參數(shù)改為了 [['@babel/plugin-transform-react-jsx', {pragma: 'createElement'}]]
。加入了這個(gè) pragma
參數(shù),我們就可以自定義我們創(chuàng)建元素的函數(shù)名。
這么一改,我們的 JSX 就與 React 的框架沒(méi)有任何聯(lián)系了。我們執(zhí)行一下 webpack 看一下最終生成的效果,就會(huì)發(fā)現(xiàn)里面的 React.createElement
就會(huì)變成 createElement
。
接下來(lái)我們加入一個(gè) HTML 文件來(lái)執(zhí)行我們的 main.js 試試。首先在根目錄創(chuàng)建一個(gè) main.html
,然后輸入一下代碼:
- <script src="./main.js"></script>
然后我們執(zhí)行在瀏覽器打開(kāi)這個(gè) HTML 文件。
這個(gè)時(shí)候我們控制臺(tái)會(huì)給我們拋出一個(gè)錯(cuò)誤,我們的 createElement
未定義。確實(shí)我們?cè)?main.js
里面還沒(méi)有定義這個(gè)函數(shù),所以說(shuō)它找不到。
所以我們就需要自己編寫一個(gè) createElement
這個(gè)函數(shù)。我們直接打開(kāi)根目錄下的 main.js
并且把之前的 for
循環(huán)給刪除了,然后加上這段代碼:
- function createElement() {
- return;
- }
- let a = <div />;
這里我們就直接返回空,先讓這個(gè)函數(shù)可以被調(diào)用即可。我們用 webpack 重新編譯一次,然后刷新我們的 main.html 頁(yè)面。這個(gè)時(shí)候我們就會(huì)發(fā)現(xiàn)報(bào)錯(cuò)沒(méi)有了,可以正常運(yùn)行。
實(shí)現(xiàn) createElement 函數(shù)
在我們的編譯后的代碼中,我們可以看到 JSX 的元素在調(diào)用 createElement 的時(shí)候是傳了兩個(gè)參數(shù)的。第一個(gè)參數(shù)是 div
, 第二個(gè)是一個(gè) null
。
這里第二個(gè)參數(shù)為什么是 null
呢?其實(shí)第二個(gè)參數(shù)是用來(lái)傳屬性列表的。如果我們?cè)?main.js 里面的 div 中加入一個(gè) id="a"
,我們來(lái)看看最后編譯出來(lái)會(huì)有什么變化。
我們就會(huì)發(fā)現(xiàn)第二個(gè)參數(shù)變成了一個(gè)以 Key-Value 的方式存儲(chǔ)的JavaScript 對(duì)象。到這里如果我們想一下,其實(shí) JSX 也沒(méi)有那么神秘,它只是把我們平時(shí)寫的 HTML 通過(guò)編譯改寫成了 JavaScript 對(duì)象,我們可以認(rèn)為它是屬于一種 “[[語(yǔ)法糖]]”。
但是 JSX 影響了代碼的結(jié)構(gòu),所以我們一般也不會(huì)完全把它叫作語(yǔ)法糖。
接下來(lái)我們來(lái)寫一些更復(fù)雜一些的 JSX,我們給原本的 div 加一些 children 元素。
- function createElement() {
- return;
- }
- let a = (
- <div id="a">
- <span></span>
- <span></span>
- <span></span>
- </div>
- );
最后我們執(zhí)行以下 webpack 打包看看效果。
在控制臺(tái)中,我們可以看到最后編譯出來(lái)的結(jié)果,是遞歸的調(diào)用了 createElement
這個(gè)函數(shù)。這里其實(shí)已經(jīng)形成了一個(gè)樹(shù)形的結(jié)構(gòu)。
父級(jí)就是第一層的 div 的元素,然后子級(jí)就是在后面當(dāng)參數(shù)傳入了第一個(gè) createElement 函數(shù)之中。然后因?yàn)槲覀兊?span 都是沒(méi)有屬性的,所以所有后面的 createElement 的第二個(gè)參數(shù)都是 null
。
根據(jù)我們這里看到的一個(gè)編譯結(jié)果,我們就可以分析出我們的 createElement 函數(shù)應(yīng)有的參數(shù)都是什么了。
-
第一個(gè)參數(shù)
type
—— 就是這個(gè)標(biāo)簽的類型 -
第二個(gè)參數(shù)
attribute
—— 標(biāo)簽內(nèi)的所有屬性與值 -
剩余的參數(shù)都是子屬性
...children
—— 這里我們使用了 JavaScript 之中比較新的語(yǔ)法...children
表示把后面所有的參數(shù) (不定個(gè)數(shù)) 都會(huì)變成一個(gè)數(shù)組賦予給 children 變量
那么我們 createElement
這個(gè)函數(shù)就可以寫成這樣了:
- function createElement(type, attributes, ...children) {
- return;
- }
函數(shù)我們有了,但是這個(gè)函數(shù)可以做什么呢?其實(shí)這個(gè)函數(shù)可以用來(lái)做任何事情,因?yàn)檫@個(gè)看起來(lái)長(zhǎng)的像 DOM API,所以我們完全可以把它做成一個(gè)跟 React 沒(méi)有關(guān)系的實(shí)體 DOM。
比如說(shuō)我們就可以在這個(gè)函數(shù)中返回這個(gè) type
類型的 element
元素。這里我們把所有傳進(jìn)來(lái)的 attributes
給這個(gè)元素加上,并且我們可以給這個(gè)元素掛上它的子元素。
創(chuàng)建元素我們可以用 createElement(type)
,而加入屬性我們可以使用 setAttribute()
,最后掛上子元素就可以使用 appendChild()
。
- function createElement(type, attributes, ...children) {
- // 創(chuàng)建元素
- let element = document.createElement(type);
- // 掛上屬性
- for (let attribute in attributes) {
- element.setAttribute(attribute);
- }
- // 掛上所有子元素
- for (let child of children) {
- element.appendChild(child);
- }
- // 最后我們的 element 就是一個(gè)節(jié)點(diǎn)
- // 所以我們可以直接返回
- return element;
- }
這里我們就實(shí)現(xiàn)了 createElement
函數(shù)的邏輯。最后我們還需要在頁(yè)面上掛載上我們的 DOM 節(jié)點(diǎn)。所以我們可以直接掛載在 body 上面。
- // 在 main.js 最后加上這段代碼
- let a = (
- <div id="a">
- <span></span>
- <span></span>
- <span></span>
- </div>
- );
- document.body.appendChild(a);
這里還需要注意的是,我們的 main.html 中沒(méi)有加入 body 標(biāo)簽,沒(méi)有 body 元素的話我們是無(wú)法掛載到 body 之上的。所以這里我們就需要在 main.html 當(dāng)中加入 body 元素。
- <body></body>
- <script src="dist/main.js"></script>
好,這個(gè)時(shí)候我們就可以 webpack 打包,看一下效果。
Wonderful! 我們成功的把節(jié)點(diǎn)生成并且掛載到 body 之上了。但是如果我們的 div
里面加入一段文字,這個(gè)時(shí)候就會(huì)有一個(gè)文本節(jié)點(diǎn)被傳入我們的 createElement
函數(shù)當(dāng)中。毋庸置疑,我們的 createElement
函數(shù)以目前的邏輯是肯定無(wú)法處理文本節(jié)點(diǎn)的。
接下來(lái)我們就把處理文本節(jié)點(diǎn)的邏輯加上,但是在這之前我們先把 div 里面的 span 標(biāo)簽刪除,換成一段文本 “hello world”。
- let a = <div id="a">hello world</div>;
在我們還沒(méi)有加入文本節(jié)點(diǎn)的邏輯之前,我們先來(lái) webpack 打包一下,看看具體會(huì)報(bào)什么錯(cuò)誤。
首先我們可以看到,在 createElement
函數(shù)調(diào)用的地方,我們的文本被當(dāng)成字符串傳入,然后這個(gè)參數(shù)是接收子節(jié)點(diǎn)的,并且在我們的邏輯之中我們使用了 appendChild
,這個(gè)函數(shù)是接收 DOM 節(jié)點(diǎn)的。顯然我們的文本字符串不是一個(gè)節(jié)點(diǎn),自然就會(huì)報(bào)錯(cuò)。
通過(guò)這種調(diào)試方式我們可以馬上定位到,我們需要在哪里添加邏輯去實(shí)現(xiàn)這個(gè)功能。這種方式也可以算是一種捷徑吧。
所以接下來(lái)我們就回到 main.js
,在我們掛上子節(jié)點(diǎn)之前,判斷以下 child 的類型,如果它的類型是 “String” 字符串的話,就使用 createTextNode()
來(lái)創(chuàng)建一個(gè)文本節(jié)點(diǎn),然后再掛載到父元素上。這樣我們就完成了字符節(jié)點(diǎn)的處理了。
- function createElement(type, attributes, ...children) {
- // 創(chuàng)建元素
- 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 就是一個(gè)節(jié)點(diǎn)
- // 所以我們可以直接返回
- return element;
- }
- let a = <div id="a">hello world</div>;
- document.body.appendChild(a);
我們用這個(gè)最新的代碼 webpack 打包之后,就可以在瀏覽器上看到我們的文字被顯示出來(lái)了。
到了這里我們編寫的 createElement
已經(jīng)是一個(gè)比較有用的東西了,我們已經(jīng)可以用它來(lái)做一定的 DOM 操作。甚至它可以完全代替我們自己去寫 document.createElement
的這種反復(fù)繁瑣的操作了。
這里我們可以驗(yàn)證以下,我們?cè)?div 當(dāng)中重新加上我們之前的三個(gè) span, 并且在每個(gè) span 中加入文本。11
- let a = (
- <div id="a">
- hello world:
- <span>a</span>
- <span>b</span>
- <span>c</span>
- </div>
- );
然后我們重新 webpack 打包后,就可以看到確實(shí)是可以完整這種 DOM 的操作的。
現(xiàn)在的代碼已經(jīng)可以完成一定的組件化的基礎(chǔ)能力。
實(shí)現(xiàn)自定義標(biāo)簽
之前我們都是在用一些,HTML 自帶的標(biāo)簽。如果我們現(xiàn)在把 div 中的 d 改為大寫 D 會(huì)怎么樣呢?
- let a = (
- <Div id="a">
- hello world:
- <span>a</span>
- <span>b</span>
- <span>c</span>
- </Div>
- );
果不其然,就是會(huì)報(bào)錯(cuò)的。不過(guò)我們找到了問(wèn)題根源的關(guān)鍵,這里我們發(fā)現(xiàn)當(dāng)我們把 div 改為 Div 的時(shí)候,傳入我們 createElement
的 div 從字符串 ‘div' 變成了一個(gè) Div
類。
當(dāng)然我們的 JavaScript 中并沒(méi)有定義 Div 類,這里自然就會(huì)報(bào) Div 未定義的錯(cuò)誤。知道問(wèn)題的所在,我們就可以去解決它,首先我們需要先解決未定義的問(wèn)題,所以我們先建立一個(gè) Div 的類。
- // 在 createElment 函數(shù)之后加入
- class Div {}
然后我們就需要在 createElement
里面做類型判斷,如果我們遇到的 type 是字符類型,就按原來(lái)的方式處理。如果我們遇到是其他情況,我們就實(shí)例化傳過(guò)來(lái)的 type
。
- function createElement(type, attributes, ...children) {
- // 創(chuàng)建元素
- 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 就是一個(gè)節(jié)點(diǎn)
- // 所以我們可以直接返回
- return element;
- }
這里我們還有一個(gè)問(wèn)題,我們有什么辦法可以讓自定義標(biāo)簽像我們普通 HTML 標(biāo)簽一樣操作呢?在最新版的 DOM 標(biāo)準(zhǔn)里面是有辦法的,我們只需要去注冊(cè)一下我們自定義標(biāo)簽的名稱和類型。
但是我們現(xiàn)行比較安全的瀏覽版本里面,還是不太建議這樣去做的。所以在使用我們的自定義 element 的時(shí)候,還是建議我們自己去寫一個(gè)接口。
首先我們是需要建立標(biāo)簽類,這個(gè)類能讓任何標(biāo)簽像我們之前普通 HTML 標(biāo)簽的元素一樣最后掛載到我們的 DOM 樹(shù)上。
它會(huì)包含以下方法:
-
mountTo()
—— 創(chuàng)建一個(gè)元素節(jié)點(diǎn),用于后面掛載到parent
父級(jí)節(jié)點(diǎn)上 -
setAttribute()
—— 給元素掛上所有它的屬性 -
appendChild()
—— 給元素掛上所有它的子元素
首先我們來(lái)簡(jiǎn)單實(shí)現(xiàn)以下我們 Div
類中的 mountTo
方法,這里我們還需要給他加入 setAttribute
和 appendChild
方法,因?yàn)樵谖覀兊?createElement
中有掛載屬性子元素的邏輯,如果沒(méi)有這兩個(gè)方法就會(huì)報(bào)錯(cuò)。但是這個(gè)時(shí)候我們先不去實(shí)現(xiàn)這兩個(gè)方法的邏輯,方法內(nèi)容留空即可。
- class Div {
- setAttribute() {}
- appendChild() {}
- mountTo(parent) {
- this.root = document.createElement('div');
- parent.appendChild(this.root);
- }
- }
這里面其實(shí)很簡(jiǎn)單首先給類中的 root
屬性創(chuàng)建成一個(gè) div 元素節(jié)點(diǎn),然后把這個(gè)節(jié)點(diǎn)掛載到這個(gè)元素的父級(jí)。這個(gè) parent
是以參數(shù)傳入進(jìn)來(lái)的。
然后我們就可以把我們?cè)瓉?lái)的 body.appendChild 的代碼改為使用 mountTo
方法來(lái)掛載我們的自定義元素類。
- // document.body.appendChild(a);
- a.mountTo(document.body);
用現(xiàn)在的代碼,我們 webpack 打包看一下效果:
我們可以看到我們的 Div 自定義元素是有正確的被掛載到 body 之上。但是 Div 中的 span 標(biāo)簽都是沒(méi)有被掛載上去的。如果我們想它與普通的 div 一樣去工作的話,我們就需要去實(shí)現(xiàn)我們的 setAttribute
和 appendChild
邏輯。
接下來(lái)我們就一起來(lái)嘗試完成剩余的實(shí)現(xiàn)邏輯。在開(kāi)始寫 setAttribute 和 appendChild 之前,我們需要先給我們的 Div 類加入一個(gè)構(gòu)造函數(shù) constructor
。在這里個(gè)里面我們就可以把元素創(chuàng)建好,并且代理到 root
上。
- constructor() {
- this.root = document.createElement('div');
- }
然后的 setAttribute
方法其實(shí)也很簡(jiǎn)單,就是直接使用 this.root
然后調(diào)用 DOM API 中的 setAttribute
就可以了。而 appendChild
也是同理。最后我們的代碼就是如下:
- class Div {
- // 構(gòu)造函數(shù)
- // 創(chuàng)建 DOM 節(jié)點(diǎn)
- constructor() {
- this.root = document.createElement('div');
- }
- // 掛載元素的屬性
- setAttribute(name, attribute) {
- this.root.setAttribute(name, attribute);
- }
- // 掛載元素子元素
- appendChild(child) {
- this.root.appendChild(child);
- }
- // 掛載當(dāng)前元素
- mountTo(parent) {
- parent.appendChild(this.root);
- }
- }
我們 webpack 打包一下看看效果:
我們可以看到,div 和 span 都被成功掛載到 body 上。也證明我們自制的 div 也能正常工作了。
這里還有一個(gè)問(wèn)題,因?yàn)槲覀冏詈笳{(diào)用的是 a.mountTo()
,如果我們的變量 a
不是一個(gè)自定義的元素,而是我們普通的 HTML 元素,這個(gè)時(shí)候他們身上是不會(huì)有 mountTo
這個(gè)方法的。
所以這里我們還需要給普通的元素加上一個(gè) Wrapper 類,讓他們可以保持我們?cè)仡惖臉?biāo)準(zhǔn)格式。也是所謂的標(biāo)準(zhǔn)接口。
我們先寫一個(gè) ElementWrapper
類,這個(gè)類的內(nèi)容其實(shí)與我們的 Div 是基本一致的。唯有兩個(gè)區(qū)別
-
在創(chuàng)建 DOM 節(jié)點(diǎn)的時(shí)候,可以通過(guò)傳當(dāng)前元素名
type
到我們的構(gòu)造函數(shù),并且用這個(gè) type 去建立我們的 DOM 節(jié)點(diǎn) -
appendChild 就不能直接使用
this.root.appendChild
,因?yàn)樗衅胀ǖ臉?biāo)簽都被改為我們的自定義類,所以 appendChild 的邏輯需要改為child.mountTo(this.root)
- class ElementWrapper {
- // 構(gòu)造函數(shù)
- // 創(chuàng)建 DOM 節(jié)點(diǎn)
- constructor(type) {
- this.root = document.createElement(type);
- }
- // 掛載元素的屬性
- setAttribute(name, attribute) {
- this.root.setAttribute(name, attribute);
- }
- // 掛載元素子元素
- appendChild(child) {
- child.mountTo(this.root);
- }
- // 掛載當(dāng)前元素
- mountTo(parent) {
- parent.appendChild(this.root);
- }
- }
- class Div {
- // 構(gòu)造函數(shù)
- // 創(chuàng)建 DOM 節(jié)點(diǎn)
- constructor() {
- this.root = document.createElement('div');
- }
- // 掛載元素的屬性
- setAttribute(name, attribute) {
- this.root.setAttribute(name, attribute);
- }
- // 掛載元素子元素
- appendChild(child) {
- child.mountTo(this.root);
- }
- // 掛載當(dāng)前元素
- mountTo(parent) {
- parent.appendChild(this.root);
- }
- }
這里我們還有一個(gè)問(wèn)題,就是遇到文本節(jié)點(diǎn)的時(shí)候,是沒(méi)有轉(zhuǎn)換成我們的自定義類的。所以我們還需要寫一個(gè)給文本節(jié)點(diǎn),叫做 TextWrapper
。
- class TextWrapper {
- // 構(gòu)造函數(shù)
- // 創(chuàng)建 DOM 節(jié)點(diǎn)
- constructor(content) {
- this.root = document.createTextNode(content);
- }
- // 掛載元素的屬性
- setAttribute(name, attribute) {
- this.root.setAttribute(name, attribute);
- }
- // 掛載元素子元素
- appendChild(child) {
- child.mountTo(this.root);
- }
- // 掛載當(dāng)前元素
- mountTo(parent) {
- parent.appendChild(this.root);
- }
- }
有了這些元素類接口后,我們就可以改寫我們 createElement
里面的邏輯。把我們?cè)镜?document.createElement
和 document.createTextNode
都替換成實(shí)例化 new ElementWrapper(type)
和 new TextWrapper(content)
即可。
- function createElement(type, attributes, ...children) {
- // 創(chuàng)建元素
- 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 就是一個(gè)節(jié)點(diǎn)
- // 所以我們可以直接返回
- return element;
- }
然后我們 webpack 打包一下看看。
沒(méi)有任何意外,我們整個(gè)元素就正常的被掛載在 body 的上了。同理如果我們把我們的 Div 改回 div 也是一樣可以正常運(yùn)行的。
當(dāng)然我們一般來(lái)說(shuō)也不會(huì)寫一個(gè)毫無(wú)意義的這種 Div 的元素。這里我們就會(huì)寫一個(gè)我們組件的名字,比如說(shuō) Carousel
,一個(gè)輪播圖的組件。
完整代碼 —— 對(duì)你有用的話,就給我一個(gè) ?? 吧,謝謝!

我們?cè)谶@里互相監(jiān)督,互相鼓勵(lì),互相努力走上人生學(xué)習(xí)之路,讓學(xué)習(xí)改變我們生活!
學(xué)習(xí)的路上,很枯燥,很寂寞,但是希望這樣可以給我們彼此帶來(lái)多一點(diǎn)陪伴,多一點(diǎn)鼓勵(lì)。我們一起加油吧! (? •??•?)?
到此這篇關(guān)于使用JSX 建立組件 Parser(解析器)開(kāi)發(fā)的示例的文章就介紹到這了,更多相關(guān)JSX建立組件Parser內(nèi)容請(qǐng)搜索服務(wù)器之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持服務(wù)器之家!
原文鏈接:https://tridiamond.blog.csdn.net/article/details/112352745