主頁 >  其他 > 用 JSX 建立組件 Parser(決議器)

用 JSX 建立組件 Parser(決議器)

2021-01-09 10:31:26 其他

這里我們一起從 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.jsentry 后面添加一個選項叫做 module

然后模塊中我們還可以加入一個 rules,這個就是我們構建的時候所使用的規則,而 rules 是一個陣列型別的配置,這里面的每一個規則是由一個 test 和一個 use 組成的,

  • test:
    • test 的值是一個正則運算式,用于匹配我們需要使用這個規則的檔案,這里我們需要把所有的 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 來試一試會是怎么樣的,與剛才一樣,我們只需要在命令列執行 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 方法,這里我們還需要給他加入 setAttributeappendChild 方法,因為在我們的 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 一樣去作業的話,我們就需要去實作我們的 setAttributeappendChild 邏輯,

接下來我們就一起來嘗試完成剩余的實作邏輯,在開始寫 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 是基本一致的,唯有兩個區別

  1. 在創建 DOM 節點的時候,可以通過傳當前元素名 type 到我們的建構式,并且用這個 type 去建立我們的 DOM 節點
  2. 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.createElementdocument.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站直播學習,歡迎過來《直播間》一起學習,

我們在這里互相監督,互相鼓勵,互相努力走上人生學習之路,讓學習改變我們生活!

學習的路上,很枯燥,很寂寞,但是希望這樣可以給我們彼此帶來多一點陪伴,多一點鼓勵,我們一起加油吧! (? ?????)?


我是來自《技術銀河》的三鉆,一位正在重塑知識的技術人,下期再見,

推薦專欄

小伙伴們可以查看或者訂閱相關的專欄,從而集中閱讀相關知識的文章哦,

  • ?? 《2021年總結》 — 一個一線戰場中的開發者,回歸到學習的學堂中,一開始這個程序確實遇到了挺多困擾的,一開始無法靜心下來學習,因為學習底層的知識確實需要靜下心來學,但是堅持了一段時間后,又會發現自己會愛上學習,愛上深挖這些知識,

  • 📖 《前端進階》 — 這里包含的文章學習內容需要我們擁有 1-2 年前端開發經驗后,選擇讓自己升級到高級前端工程師的學習內容(這里學習的內容是對應阿里 P6 級別的內容),

  • 📖 《資料結構與演算法》 — 到了如今,如果想成為一個高級開發工程師或者進入大廠,不論崗位是前端、后端還是AI,演算法都是重中之重,也無論我們需要進入的公司的崗位是否最后是做演算法工程師,前提面試就需要考演算法,

  • 📖 《FCC前端集訓營》 — 根據FreeCodeCamp的學習課程,一起深入淺出學習前端,穩固前端知識,一起在FreeCodeCamp獲得證書

  • 📖 《前端星球》 — 以實戰為線索,深入淺出前端多維度的知識點,內含有多方面的前端知識文章,帶領不懂前端的童鞋一起學習前端,在前端開發路上童鞋一起燃起心中那團火🔥

三鉆 CSDN認證博客專家 前端 Vue React
—— 起步于PHP,一入前端深似海,最后愛上了前端,Vue、React使用者,專于Web、移動端開發,特別關注產品和UI設計,專心、專注、專研,與同學們一起終身學習,關注我的微信公眾號《技術銀河》有更多最新知識文章與同學們分享,

轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/246521.html

標籤:其他

上一篇:梁勝:做開源專案的貢獻者沒有意義 | 人物志

下一篇:一文教你輕松搞定ANR例外捕獲與分析方法

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 網閘典型架構簡述

    網閘架構一般分為兩種:三主機的三系統架構網閘和雙主機的2+1架構網閘。 三主機架構分別為內端機、外端機和仲裁機。三機無論從軟體和硬體上均各自獨立。首先從硬體上來看,三機都用各自獨立的主板、記憶體及存盤設備。從軟體上來看,三機有各自獨立的作業系統。這樣能達到完全的三機獨立。對于“2+1”系統,“2”分為 ......

    uj5u.com 2020-09-10 02:00:44 more
  • 如何從xshell上傳檔案到centos linux虛擬機里

    如何從xshell上傳檔案到centos linux虛擬機里及:虛擬機CentOs下執行 yum -y install lrzsz命令,出現錯誤:鏡像無法找到軟體包 前言 一、安裝lrzsz步驟 二、上傳檔案 三、遇到的問題及解決方案 總結 前言 提示:其實很簡單,往虛擬機上安裝一個上傳檔案的工具 ......

    uj5u.com 2020-09-10 02:00:47 more
  • 一、SQLMAP入門

    一、SQLMAP入門 1、判斷是否存在注入 sqlmap.py -u 網址/id=1 id=1不可缺少。當注入點后面的引數大于兩個時。需要加雙引號, sqlmap.py -u "網址/id=1&uid=1" 2、判斷文本中的請求是否存在注入 從文本中加載http請求,SQLMAP可以從一個文本檔案中 ......

    uj5u.com 2020-09-10 02:00:50 more
  • Metasploit 簡單使用教程

    metasploit 簡單使用教程 浩先生, 2020-08-28 16:18:25 分類專欄: kail 網路安全 linux 文章標簽: linux資訊安全 編輯 著作權 metasploit 使用教程 前言 一、Metasploit是什么? 二、準備作業 三、具體步驟 前言 Msfconsole ......

    uj5u.com 2020-09-10 02:00:53 more
  • 游戲逆向之驅動層與用戶層通訊

    驅動層代碼: #pragma once #include <ntifs.h> #define add_code CTL_CODE(FILE_DEVICE_UNKNOWN,0x800,METHOD_BUFFERED,FILE_ANY_ACCESS) /* 更多游戲逆向視頻www.yxfzedu.com ......

    uj5u.com 2020-09-10 02:00:56 more
  • 北斗電力時鐘(北斗授時服務器)讓網路資料更精準

    北斗電力時鐘(北斗授時服務器)讓網路資料更精準 北斗電力時鐘(北斗授時服務器)讓網路資料更精準 京準電子科技官微——ahjzsz 近幾年,資訊技術的得了快速發展,互聯網在逐漸普及,其在人們生活和生產中都得到了廣泛應用,并且取得了不錯的應用效果。計算機網路資訊在電力系統中的應用,一方面使電力系統的運行 ......

    uj5u.com 2020-09-10 02:01:03 more
  • 【CTF】CTFHub 技能樹 彩蛋 writeup

    ?碎碎念 CTFHub:https://www.ctfhub.com/ 筆者入門CTF時時剛開始刷的是bugku的舊平臺,后來才有了CTFHub。 感覺不論是網頁UI設計,還是題目質量,賽事跟蹤,工具軟體都做得很不錯。 而且因為獨到的金幣制度的確讓人有一種想去刷題賺金幣的感覺。 個人還是非常喜歡這個 ......

    uj5u.com 2020-09-10 02:04:05 more
  • 02windows基礎操作

    我學到了一下幾點 Windows系統目錄結構與滲透的作用 常見Windows的服務詳解 Windows埠詳解 常用的Windows注冊表詳解 hacker DOS命令詳解(net user / type /md /rd/ dir /cd /net use copy、批處理 等) 利用dos命令制作 ......

    uj5u.com 2020-09-10 02:04:18 more
  • 03.Linux基礎操作

    我學到了以下幾點 01Linux系統介紹02系統安裝,密碼啊破解03Linux常用命令04LAMP 01LINUX windows: win03 8 12 16 19 配置不繁瑣 Linux:redhat,centos(紅帽社區版),Ubuntu server,suse unix:金融機構,證券,銀 ......

    uj5u.com 2020-09-10 02:04:30 more
  • 05HTML

    01HTML介紹 02頭部標簽講解03基礎標簽講解04表單標簽講解 HTML前段語言 js1.了解代碼2.根據代碼 懂得挖掘漏洞 (POST注入/XSS漏洞上傳)3.黑帽seo 白帽seo 客戶網站被黑帽植入劫持代碼如何處理4.熟悉html表單 <html><head><title>TDK標題,描述 ......

    uj5u.com 2020-09-10 02:04:36 more
最新发布
  • 2023年最新微信小程式抓包教程

    01 開門見山 隔一個月發一篇文章,不過分。 首先回顧一下《微信系結手機號資料庫被脫庫事件》,我也是第一時間得知了這個訊息,然后跟蹤了整件事情的經過。下面是這起事件的相關截圖以及近日流出的一萬條資料樣本: 個人認為這件事也沒什么,還不如關注一下之前45億快遞資料查詢渠道疑似在近日復活的訊息。 訊息是 ......

    uj5u.com 2023-04-20 08:48:24 more
  • web3 產品介紹:metamask 錢包 使用最多的瀏覽器插件錢包

    Metamask錢包是一種基于區塊鏈技術的數字貨幣錢包,它允許用戶在安全、便捷的環境下管理自己的加密資產。Metamask錢包是以太坊生態系統中最流行的錢包之一,它具有易于使用、安全性高和功能強大等優點。 本文將詳細介紹Metamask錢包的功能和使用方法。 一、 Metamask錢包的功能 數字資 ......

    uj5u.com 2023-04-20 08:47:46 more
  • vulnhub_Earth

    前言 靶機地址->>>vulnhub_Earth 攻擊機ip:192.168.20.121 靶機ip:192.168.20.122 參考文章 https://www.cnblogs.com/Jing-X/archive/2022/04/03/16097695.html https://www.cnb ......

    uj5u.com 2023-04-20 07:46:20 more
  • 從4k到42k,軟體測驗工程師的漲薪史,給我看哭了

    清明節一過,盲猜大家已經無心上班,在數著日子準備過五一,但一想到銀行卡里的余額……瞬間心情就不美麗了。最近,2023年高校畢業生就業調查顯示,本科畢業月平均起薪為5825元。調查一出,便有很多同學表示自己又被平均了。看著這一資料,不免讓人想到前不久中國青年報的一項調查:近六成大學生認為畢業10年內會 ......

    uj5u.com 2023-04-20 07:44:00 more
  • 最新版本 Stable Diffusion 開源 AI 繪畫工具之中文自動提詞篇

    🎈 標簽生成器 由于輸入正向提示詞 prompt 和反向提示詞 negative prompt 都是使用英文,所以對學習母語的我們非常不友好 使用網址:https://tinygeeker.github.io/p/ai-prompt-generator 這個網址是為了讓大家在使用 AI 繪畫的時候 ......

    uj5u.com 2023-04-20 07:43:36 more
  • 漫談前端自動化測驗演進之路及測驗工具分析

    隨著前端技術的不斷發展和應用程式的日益復雜,前端自動化測驗也在不斷演進。隨著 Web 應用程式變得越來越復雜,自動化測驗的需求也越來越高。如今,自動化測驗已經成為 Web 應用程式開發程序中不可或缺的一部分,它們可以幫助開發人員更快地發現和修復錯誤,提高應用程式的性能和可靠性。 ......

    uj5u.com 2023-04-20 07:43:16 more
  • CANN開發實踐:4個DVPP記憶體問題的典型案例解讀

    摘要:由于DVPP媒體資料處理功能對存放輸入、輸出資料的記憶體有更高的要求(例如,記憶體首地址128位元組對齊),因此需呼叫專用的記憶體申請介面,那么本期就分享幾個關于DVPP記憶體問題的典型案例,并給出原因分析及解決方法。 本文分享自華為云社區《FAQ_DVPP記憶體問題案例》,作者:昇騰CANN。 DVPP ......

    uj5u.com 2023-04-20 07:43:03 more
  • msf學習

    msf學習 以kali自帶的msf為例 一、msf核心模塊與功能 msf模塊都放在/usr/share/metasploit-framework/modules目錄下 1、auxiliary 輔助模塊,輔助滲透(埠掃描、登錄密碼爆破、漏洞驗證等) 2、encoders 編碼器模塊,主要包含各種編碼 ......

    uj5u.com 2023-04-20 07:42:59 more
  • Halcon軟體安裝與界面簡介

    1. 下載Halcon17版本到到本地 2. 雙擊安裝包后 3. 步驟如下 1.2 Halcon軟體安裝 界面分為四大塊 1. Halcon的五個助手 1) 影像采集助手:與相機連接,設定相機引數,采集影像 2) 標定助手:九點標定或是其它的標定,生成標定檔案及內參外參,可以將像素單位轉換為長度單位 ......

    uj5u.com 2023-04-20 07:42:17 more
  • 在MacOS下使用Unity3D開發游戲

    第一次發博客,先發一下我的游戲開發環境吧。 去年2月份買了一臺MacBookPro2021 M1pro(以下簡稱mbp),這一年來一直在用mbp開發游戲。我大致分享一下我的開發工具以及使用體驗。 1、Unity 官網鏈接: https://unity.cn/releases 我一般使用的Apple ......

    uj5u.com 2023-04-20 07:40:19 more