用 JSX 建立元件 Parser(解析器)
這裡我們一起從 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.js
的 entry
後面新增一個選項叫做 module
。
然後模組中我們還可以加入一個 rules
,這個就是我們構建的時候所使用的規則。而 rules
是一個數組型別的配置,這裡面的每一個規則是由一個 test
和一個 use
組成的。
- test:
test
的值是一個正則表示式,用於匹配我們需要使用這個規則的檔案。這裡我們需要把所有的 JavaScript 檔案給匹配上,所以我們使用/\.js/
即可。
- use:
- loader:
- 只需要加入我們的
babel-loader
的名字即可
- 只需要加入我們的
- options:
- presets:
- 這裡是 loader 的選項,這裡我們需要加入
@babel/preset-env
- 這裡是 loader 的選項,這裡我們需要加入
- presets:
- loader:
最後我們的配置檔案就會是這個樣子:
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
方法,這裡我們還需要給他加入 setAttribute
和 appendChild
方法,因為在我們的 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 一樣去工作的話,我們就需要去實現我們的 setAttribute
和 appendChild
邏輯。
接下來我們就一起來嘗試完成剩餘的實現邏輯。在開始寫 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 是基本一致的。唯有兩個區別
- 在建立 DOM 節點的時候,可以通過傳當前元素名
type
到我們的建構函式,並且用這個 type 去建立我們的 DOM 節點 - 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.createElement
和 document.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站直播學習,歡迎過來《直播間》一起學習。
我們在這裡互相監督,互相鼓勵,互相努力走上人生學習之路,讓學習改變我們生活!
學習的路上,很枯燥,很寂寞,但是希望這樣可以給我們彼此帶來多一點陪伴,多一點鼓勵。我們一起加油吧! (๑ •̀ㅂ•́)و
我是來自《技術銀河》的三鑽,一位正在重塑知識的技術人。下期再見。
推薦專欄
小夥伴們可以檢視或者訂閱相關的專欄,從而集中閱讀相關知識的文章哦。