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

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