使用JSX 建立組件 Parser(解析器)開發(fā)的示例
這里我們一起從 0 開始搭建一個(gè)組件系統(tǒng)。首先通過上一篇《前端組件化基礎(chǔ)知識(shí)》中知道,一個(gè)組件可以通過 Markup 和 JavaScript 訪問的一個(gè)環(huán)境。
所以我們的第一步就是建立一個(gè)可以使用 markup 的環(huán)境。這里我們會(huì)學(xué)習(xí)使用兩種建立 markup 的風(fēng)格。
第一種是基于與 React 一樣的 JSX 去建立我們組件的風(fēng)格。第二種則是我們?nèi)ソ⒒陬愃?Vue 的這種,基于標(biāo)記語言的 Parser 的一種風(fēng)格。
JSX 環(huán)境搭建
JSX 在大家一般認(rèn)知里面,它是屬于 React 的一部分。其實(shí) Facebook 公司會(huì)把 JSX 定義為一種純粹的語言擴(kuò)展。而這個(gè) JSX 也是可以被其他組件體系去使用的。
甚至我們可以把它單獨(dú)作為一種,快捷創(chuàng)建 HTML 標(biāo)簽的方式去使用。
建立項(xiàng)目
那么我們就從最基礎(chǔ)的開始,首先我們需要?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)問題,如果有需要可以自行填寫。不過我們也可以直接一直按回車,然后有需要的同學(xué)可以后面自己打開 package.json
自行修改。
安裝 webpack
Wepack 很多同學(xué)應(yīng)該都了解過,它可以幫助我們把一個(gè)普通的 JavaScript 文件變成一個(gè)能把不同的 import 和 require 的文件給打包到一起。
所以我們需要安裝 webpack
,當(dāng)然我們也可以直接使用 npx 直接使用 webpack,也可以全局安裝 webpack-cli。
那么這里我們就使用全局安裝 webpack-cli:
npm install -g webpack webpack-cli
安裝完畢之后,我們可以通過輸入下面的一條命令來檢測(cè)一下安裝好的 webpack 版本。如果執(zhí)行后沒有報(bào)錯(cuò),并且出來了一個(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 加入到我們的開發(fā)依賴中。
執(zhí)行完畢后,我們應(yīng)該會(huì)看到上面圖中的消息。
為了驗(yàn)證我們是正確安裝好了,我們可以打開我們項(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 來寫它的設(shè)置。而這個(gè)是早期 nodejs 工具常見的一種配置方法,它用一個(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 來打包一下 main.js
的文件來看看。需要執(zhí)行下面的這行命令進(jìn)行打包:
webpack
執(zhí)行完畢之后,我們就可以在命令行界面中看到上面這樣的一段提示。
注意細(xì)節(jié)的同學(xué),肯定要舉手問到,同學(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
,通過 webpack 被打包好的版本。
然后我們打開它,就會(huì)看到它被 babel 編譯過后的 JavaScript 代碼。我們會(huì)發(fā)現(xiàn)我們短短的幾行代碼被加入了很多的東西,這些其實(shí)我們都不用管,那都是 Webpack 的 “喵喵力量”。
在代碼的最后面,還是能看到我們編寫的 for
循環(huán)的,只是被改造了一下,但是它的作用是一致的。
安裝 Babel-loader
接下來我們來安裝 babel-loader,其實(shí) babel-loader 并沒有直接依賴 babel 的,所以我們才需要另外安裝 @babel/core
和 @babel/preset-env
。我們只需要執(zhí)行下面的命令行來安裝:
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'], }, }, }, ], }, };
這樣配置好之后,我們就可以來跑一下 babel 來試一試會(huì)是怎么樣的。與剛才一樣,我們只需要在命令行執(zhí)行 webpack
即可。
如果我們的配置文件沒有寫錯(cuò),我們就應(yīng)該會(huì)看到上面圖中的結(jié)果。
然后我們進(jìn)入 dist
文件夾,打開我們編譯后的 main.js
,看一下我們這次使用了 babel-loader 之后的編譯結(jié)果。
編譯后的結(jié)果,我們會(huì)發(fā)現(xiàn) for of
的循環(huán)被編譯成了一個(gè)普通的 for
循環(huán)。這個(gè)也可以證明我們的 babel-loader 起效了,正確把我們新版本的 JavaScript 語法轉(zhuǎn)成能兼容舊版瀏覽器的 JavaScript 語法。
到了這里我們已經(jīng)把 JSX 所需的環(huán)境給安裝和搭建完畢了。
模式配置
最后我們還需要在 webpack.config.js 里面添加一個(gè)環(huán)境配置,不過這個(gè)是可加也可不加的,但是我們?yōu)榱似綍r(shí)開發(fā)中的方便。
所以我們需要在 webpack.config.js 中添加一個(gè) mode
,這我們使用 development
。這個(gè)配置表示我們是開發(fā)者模式。
一般來說我們?cè)诖a倉庫里面寫的 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),編譯后的代碼沒有被壓縮成一行了。這樣我們就可以調(diào)試 webpack 生成的代碼了。這里我們可以注意到,我們?cè)?main.js
中的代碼被轉(zhuǎn)成字符串,并且被放入一個(gè) eval()
的函數(shù)里面。那么我們就可以在調(diào)試的時(shí)候把它作為一個(gè)單獨(dú)的文件去使用了,并且可以進(jìn)行斷點(diǎn)調(diào)試。
引入 JSX
萬事俱備,只欠東風(fēng)了,最后我們需要如何引入 JSX呢?在引入之前,我們來看看,如果就使用現(xiàn)在的配置在我們的 main.js
里面使用 JSX 語法會(huì)怎么樣。作為程序員的我們,總得有點(diǎn)冒險(xiǎn)精神!
所以我們?cè)?main.js
里面加入這段代碼:
var a = <div/>
然后大膽地執(zhí)行 webpack 看看!
好家伙!果然報(bào)錯(cuò)了。這里的報(bào)錯(cuò)告訴我們,在 =
后面不能使用 “小于號(hào)”,但是在正常的 JSX 語法中,這個(gè)其實(shí)是 HTML 標(biāo)簽的 “尖括號(hào)”,因?yàn)闆]有 JSX 語法的編譯過程,所以 JavaScript 默認(rèn)就會(huì)認(rèn)為這個(gè)就是 “小于號(hào)”。
所以我們要怎么做讓我們的 webpack 編譯過程支持 JSX 語法呢?這里其實(shí)就是還需要我們加入一個(gè)最關(guān)鍵的一個(gè)包,而這個(gè)包名非常的長(zhǎng),叫做 @babel/plugin-transform-react-jsx
。執(zhí)行以下命令來安裝它:
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)沒有再報(bào)錯(cuò)了。這樣也就證明我們的代碼現(xiàn)在是支持使用 JSX 語法了。
最后我們來圍觀一下,最后編程的效果是怎么樣的。
我們會(huì)發(fā)現(xiàn),在 eval
里面我們加入的 <div/>
被翻譯成一個(gè) React.createElement("div", null)
的函數(shù)調(diào)用了。
所以接下來我們就一起來看一下,我們應(yīng)該怎么實(shí)現(xiàn)這個(gè) React.createElement
,以及我們能否把這個(gè)換成我們自己的函數(shù)名字。
JSX 基本用法
首先我們來嘗試?yán)斫?JSX,JSX 其實(shí)它相當(dāng)于一個(gè)純粹在代碼語法上的一種快捷方式。在上一部分的結(jié)尾我們看到,JSX語法在被編譯后會(huì)出現(xiàn)一個(gè) React.createElement
的調(diào)用。
JSX 基礎(chǔ)原理
那么這里我們就先修改在 webpack 中的 JSX 插件,給它一個(gè)自定義的創(chuàng)建元素函數(shù)名。我們打開 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']
參數(shù)改為了 [['@babel/plugin-transform-react-jsx', {pragma: 'createElement'}]]
。加入了這個(gè) pragma
參數(shù),我們就可以自定義我們創(chuàng)建元素的函數(shù)名。
這么一改,我們的 JSX 就與 React 的框架沒有任何聯(lián)系了。我們執(zhí)行一下 webpack 看一下最終生成的效果,就會(huì)發(fā)現(xiàn)里面的 React.createElement
就會(huì)變成 createElement
。
接下來我們加入一個(gè) HTML 文件來執(zhí)行我們的 main.js 試試。首先在根目錄創(chuàng)建一個(gè) main.html
,然后輸入一下代碼:
<script src="./main.js"></script>
然后我們執(zhí)行在瀏覽器打開這個(gè) HTML 文件。
這個(gè)時(shí)候我們控制臺(tái)會(huì)給我們拋出一個(gè)錯(cuò)誤,我們的 createElement
未定義。確實(shí)我們?cè)?main.js
里面還沒有定義這個(gè)函數(shù),所以說它找不到。
所以我們就需要自己編寫一個(gè) createElement
這個(gè)函數(shù)。我們直接打開根目錄下的 main.js
并且把之前的 for
循環(huán)給刪除了,然后加上這段代碼:
function createElement() { return; } let a = <div />;
這里我們就直接返回空,先讓這個(gè)函數(shù)可以被調(diào)用即可。我們用 webpack 重新編譯一次,然后刷新我們的 main.html 頁面。這個(gè)時(shí)候我們就會(huì)發(fā)現(xiàn)報(bào)錯(cuò)沒有了,可以正常運(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ù)是用來傳屬性列表的。如果我們?cè)?main.js 里面的 div 中加入一個(gè) id="a"
,我們來看看最后編譯出來會(huì)有什么變化。
我們就會(huì)發(fā)現(xiàn)第二個(gè)參數(shù)變成了一個(gè)以 Key-Value 的方式存儲(chǔ)的JavaScript 對(duì)象。到這里如果我們想一下,其實(shí) JSX 也沒有那么神秘,它只是把我們平時(shí)寫的 HTML 通過編譯改寫成了 JavaScript 對(duì)象,我們可以認(rèn)為它是屬于一種 “[[語法糖]]”。
但是 JSX 影響了代碼的結(jié)構(gòu),所以我們一般也不會(huì)完全把它叫作語法糖。
接下來我們來寫一些更復(fù)雜一些的 JSX,我們給原本的 div 加一些 children 元素。
function createElement() { return; } let a = ( <div id="a"> <span></span> <span></span> <span></span> </div> );
最后我們執(zhí)行以下 webpack 打包看看效果。
在控制臺(tái)中,我們可以看到最后編譯出來的結(jié)果,是遞歸的調(diào)用了 createElement
這個(gè)函數(shù)。這里其實(shí)已經(jīng)形成了一個(gè)樹形的結(jié)構(gòu)。
父級(jí)就是第一層的 div 的元素,然后子級(jí)就是在后面當(dāng)參數(shù)傳入了第一個(gè) createElement 函數(shù)之中。然后因?yàn)槲覀兊?span 都是沒有屬性的,所以所有后面的 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 之中比較新的語法...children
表示把后面所有的參數(shù) (不定個(gè)數(shù)) 都會(huì)變成一個(gè)數(shù)組賦予給 children 變量
那么我們 createElement
這個(gè)函數(shù)就可以寫成這樣了:
function createElement(type, attributes, ...children) { return; }
函數(shù)我們有了,但是這個(gè)函數(shù)可以做什么呢?其實(shí)這個(gè)函數(shù)可以用來做任何事情,因?yàn)檫@個(gè)看起來長(zhǎng)的像 DOM API,所以我們完全可以把它做成一個(gè)跟 React 沒有關(guān)系的實(shí)體 DOM。
比如說我們就可以在這個(gè)函數(shù)中返回這個(gè) type
類型的 element
元素。這里我們把所有傳進(jìn)來的 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ù)的邏輯。最后我們還需要在頁面上掛載上我們的 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 中沒有加入 body 標(biāo)簽,沒有 body 元素的話我們是無法掛載到 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ù)以目前的邏輯是肯定無法處理文本節(jié)點(diǎn)的。
接下來我們就把處理文本節(jié)點(diǎn)的邏輯加上,但是在這之前我們先把 div 里面的 span 標(biāo)簽刪除,換成一段文本 “hello world”。
let a = <div id="a">hello world</div>;
在我們還沒有加入文本節(jié)點(diǎn)的邏輯之前,我們先來 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ò)。
通過這種調(diào)試方式我們可以馬上定位到,我們需要在哪里添加邏輯去實(shí)現(xiàn)這個(gè)功能。這種方式也可以算是一種捷徑吧。
所以接下來我們就回到 main.js
,在我們掛上子節(jié)點(diǎn)之前,判斷以下 child 的類型,如果它的類型是 “String” 字符串的話,就使用 createTextNode()
來創(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 打包之后,就可以在瀏覽器上看到我們的文字被顯示出來了。
到了這里我們編寫的 createElement
已經(jīng)是一個(gè)比較有用的東西了,我們已經(jīng)可以用它來做一定的 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ān)鍵,這里我們發(fā)現(xiàn)當(dāng)我們把 div 改為 Div 的時(shí)候,傳入我們 createElement
的 div 從字符串 ‘div' 變成了一個(gè) Div
類。
當(dāng)然我們的 JavaScript 中并沒有定義 Div 類,這里自然就會(huì)報(bào) Div 未定義的錯(cuò)誤。知道問題的所在,我們就可以去解決它,首先我們需要先解決未定義的問題,所以我們先建立一個(gè) Div 的類。
// 在 createElment 函數(shù)之后加入 class Div {}
然后我們就需要在 createElement
里面做類型判斷,如果我們遇到的 type 是字符類型,就按原來的方式處理。如果我們遇到是其他情況,我們就實(shí)例化傳過來的 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è)問題,我們有什么辦法可以讓自定義標(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 樹上。
它會(huì)包含以下方法:
mountTo()
—— 創(chuàng)建一個(gè)元素節(jié)點(diǎn),用于后面掛載到parent
父級(jí)節(jié)點(diǎn)上setAttribute()
—— 給元素掛上所有它的屬性appendChild()
—— 給元素掛上所有它的子元素
首先我們來簡(jiǎn)單實(shí)現(xiàn)以下我們 Div
類中的 mountTo
方法,這里我們還需要給他加入 setAttribute
和 appendChild
方法,因?yàn)樵谖覀兊?createElement
中有掛載屬性子元素的邏輯,如果沒有這兩個(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)來的。
然后我們就可以把我們?cè)瓉淼?body.appendChild 的代碼改為使用 mountTo
方法來掛載我們的自定義元素類。
// document.body.appendChild(a); a.mountTo(document.body);
用現(xiàn)在的代碼,我們 webpack 打包看一下效果:
我們可以看到我們的 Div 自定義元素是有正確的被掛載到 body 之上。但是 Div 中的 span 標(biāo)簽都是沒有被掛載上去的。如果我們想它與普通的 div 一樣去工作的話,我們就需要去實(shí)現(xiàn)我們的 setAttribute
和 appendChild
邏輯。
接下來我們就一起來嘗試完成剩余的實(shí)現(xiàn)邏輯。在開始寫 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è)問題,因?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í)候,可以通過傳當(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è)問題,就是遇到文本節(jié)點(diǎn)的時(shí)候,是沒有轉(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 打包一下看看。
沒有任何意外,我們整個(gè)元素就正常的被掛載在 body 的上了。同理如果我們把我們的 Div 改回 div 也是一樣可以正常運(yùn)行的。
當(dāng)然我們一般來說也不會(huì)寫一個(gè)毫無意義的這種 Div 的元素。這里我們就會(huì)寫一個(gè)我們組件的名字,比如說 Carousel
,一個(gè)輪播圖的組件。
完整代碼 —— 對(duì)你有用的話,就給我一個(gè) ⭐️ 吧,謝謝!
我們?cè)谶@里互相監(jiān)督,互相鼓勵(lì),互相努力走上人生學(xué)習(xí)之路,讓學(xué)習(xí)改變我們生活!
學(xué)習(xí)的路上,很枯燥,很寂寞,但是希望這樣可以給我們彼此帶來多一點(diǎn)陪伴,多一點(diǎn)鼓勵(lì)。我們一起加油吧! (๑ •̀ㅂ•́)و
到此這篇關(guān)于使用JSX 建立組件 Parser(解析器)開發(fā)的示例的文章就介紹到這了,更多相關(guān)JSX建立組件Parser內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
基于JavaScript實(shí)現(xiàn)在新的tab頁打開url
這篇文章主要介紹了基于JavaScript實(shí)現(xiàn)在新的tab頁打開url 的相關(guān)資料,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下2016-08-08javascript createAdder函數(shù)功能與使用說明
createAdder(x)是一個(gè)函數(shù),返回一個(gè)函數(shù)。在JavaScript中,函數(shù)是第一類對(duì)象:另外它們可以被傳遞到其他函數(shù)作為參數(shù)和函數(shù)返回。在這種情況下,函數(shù)返回本身就是一個(gè)函數(shù)接受一個(gè)參數(shù),并增加了一些東西。2010-06-06原生js實(shí)現(xiàn)移動(dòng)小球(碰撞檢測(cè))
這篇文章主要介紹了原生js實(shí)現(xiàn)會(huì)動(dòng)的小球,碰撞檢測(cè)功能,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-12-12JavaScript實(shí)現(xiàn)精美個(gè)性導(dǎo)航欄筋斗云效果
這篇文章主要介紹了JavaScript實(shí)現(xiàn)精美個(gè)性導(dǎo)航欄筋斗云效果,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下2017-10-10javascript實(shí)現(xiàn)延時(shí)顯示提示框特效代碼
本文給大家分享的是javascript通過setTimeout實(shí)現(xiàn)延時(shí)顯示提示框的特效代碼,效果非常棒,這里推薦給大家2016-04-04BootStrap實(shí)現(xiàn)文件上傳并帶有進(jìn)度條效果
這篇文章主要介紹了BootStrap實(shí)現(xiàn)文件上傳并帶有進(jìn)度條效果,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下2017-09-09JS實(shí)現(xiàn)自動(dòng)固定頂部的懸浮菜單欄效果
這篇文章主要介紹了JS實(shí)現(xiàn)自動(dòng)固定頂部的懸浮菜單欄效果,涉及JavaScript針對(duì)window.onscroll事件的響應(yīng)及頁面樣式動(dòng)態(tài)變換的技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-09-09Three.js中網(wǎng)格對(duì)象MESH的屬性與方法詳解
三維開發(fā)渲染最多的對(duì)象大概是網(wǎng)格mesh了,Webgl開發(fā)三維也不例外,下面這篇文章主要給大家介紹了關(guān)于Three.js中網(wǎng)格對(duì)象MESH的屬性與方法,文中通過示例代碼介紹的非常詳細(xì),需要的朋友可以參考借鑒,下面來一起看看吧。2017-09-09對(duì)Layer彈窗使用及返回?cái)?shù)據(jù)接收的實(shí)例詳解
今天小編就為大家分享一篇對(duì)Layer彈窗使用及返回?cái)?shù)據(jù)接收的實(shí)例詳解,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2019-09-09