自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

Webpack - 手把手教你寫一個(gè) loader / plugin

開發(fā) 前端
webpack 只能理解 JavaScript 和 JSON 文件,這是 webpack 開箱可用的自帶能力。**loader **讓 webpack 能夠去處理其他類型的文件,并將它們轉(zhuǎn)換為有效模塊,以供應(yīng)用程序使用,以及被添加到依賴圖中。

[[406788]]

一、Loader

1.1 loader 干啥的?

webpack 只能理解 JavaScript 和 JSON 文件,這是 webpack 開箱可用的自帶能力。**loader **讓 webpack 能夠去處理其他類型的文件,并將它們轉(zhuǎn)換為有效模塊,以供應(yīng)用程序使用,以及被添加到依賴圖中。

也就是說(shuō),webpack 把任何文件都看做模塊,loader 能 import 任何類型的模塊,但是 webpack 原生不支持譬如 css 文件等的解析,這時(shí)候就需要用到我們的 loader 機(jī)制了。 我們的 loader 主要通過(guò)兩個(gè)屬性來(lái)讓我們的 webpack 進(jìn)行聯(lián)動(dòng)識(shí)別:

  1. test 屬性,識(shí)別出哪些文件會(huì)被轉(zhuǎn)換。
  2. use 屬性,定義出在進(jìn)行轉(zhuǎn)換時(shí),應(yīng)該使用哪個(gè) loader。

那么問(wèn)題來(lái)了,大家一定想知道自己要定制一個(gè) loader 的話需要怎么做呢?

1.2 開發(fā)準(zhǔn)則

俗話說(shuō)的好,沒(méi)有規(guī)矩不成方圓,編寫我們的 loader 時(shí),官方也給了我們一套用法準(zhǔn)則(Guidelines),在編寫的時(shí)候應(yīng)該按照這套準(zhǔn)則來(lái)使我們的 loader 標(biāo)準(zhǔn)化:

  • 簡(jiǎn)單易用。
  • 使用鏈?zhǔn)絺鬟f。(由于 loader 是可以被鏈?zhǔn)秸{(diào)用的,所以請(qǐng)保證每一個(gè) loader 的單一職責(zé))
  • 模塊化的輸出。
  • 確保無(wú)狀態(tài)。(不要讓 loader 的轉(zhuǎn)化中保留之前的狀態(tài),每次運(yùn)行都應(yīng)該獨(dú)立于其他編譯模塊以及相同模塊之前的編譯結(jié)果)
  • 充分使用官方提供的 loader utilities。
  • 記錄 loader 的依賴。
  • 解析模塊依賴關(guān)系。

根據(jù)模塊類型,可能會(huì)有不同的模式指定依賴關(guān)系。例如在 CSS 中,使用@import 和 url(...)語(yǔ)句來(lái)聲明依賴。這些依賴關(guān)系應(yīng)該由模塊系統(tǒng)解析。 可以通過(guò)以下兩種方式中的一種來(lái)實(shí)現(xiàn):

  • 通過(guò)把它們轉(zhuǎn)化成 require 語(yǔ)句。
  • 使用 this.resolve 函數(shù)解析路徑。
  • 提取通用代碼。
  • 避免絕對(duì)路徑。
  • 使用 peer dependencies。如果你的 loader 簡(jiǎn)單包裹另外一個(gè)包,你應(yīng)該把這個(gè)包作為一個(gè) peerDependency 引入。

1.3 上手

一個(gè) loader 就是一個(gè) nodejs 模塊,他導(dǎo)出的是一個(gè)函數(shù),這個(gè)函數(shù)只有一個(gè)入?yún)?,這個(gè)參數(shù)就是一個(gè)包含資源文件內(nèi)容的字符串,而函數(shù)的返回值就是處理后的內(nèi)容。也就是說(shuō),一個(gè)最簡(jiǎn)單的 loader 長(zhǎng)這樣:

  1. module.exports = function (content) { 
  2.  // content 就是傳入的源內(nèi)容字符串 
  3.   return content 

當(dāng)一個(gè) loader 被使用的時(shí)候,他只可以接收一個(gè)入?yún)?,這個(gè)參數(shù)是一個(gè)包含包含資源文件內(nèi)容的字符串。 是的,到這里為止,一個(gè)最簡(jiǎn)單 loader 就已經(jīng)完成了!接下來(lái)我們來(lái)看看怎么給他加上豐富的功能。

1.4 四種 loader

我們基本可以把常見(jiàn)的 loader 分為四種:

  • 同步 loader
  • 異步 loader
  • "Raw" Loader
  • Pitching loader

① 同步 loader 與 異步 loader

一般的 loader 轉(zhuǎn)換都是同步的,我們可以采用上面說(shuō)的直接 return 結(jié)果的方式,返回我們的處理結(jié)果:

  1. module.exports = function (content) { 
  2.  // 對(duì) content 進(jìn)行一些處理 
  3.   const res = dosth(content) 
  4.   return res 

也可以直接使用 this.callback() 這個(gè) api,然后在最后直接 **return undefined **的方式告訴 webpack 去 this.callback() 尋找他要的結(jié)果,這個(gè) api 接受這些參數(shù):

  1. this.callback( 
  2.   err: Error | null, // 一個(gè)無(wú)法正常編譯時(shí)的 Error 或者 直接給個(gè) null 
  3.   content: string | Buffer,// 我們處理后返回的內(nèi)容 可以是 string 或者 Buffer() 
  4.   sourceMap?: SourceMap, // 可選 可以是一個(gè)被正常解析的 source map 
  5.   meta?: any // 可選 可以是任何東西,比如一個(gè)公用的 AST 語(yǔ)法樹 
  6. ); 

接下來(lái)舉個(gè)例子:

[[406789]]

這里注意[this.getOptions()](https://webpack.docschina.org/api/loaders/#thisgetoptionsschema) 可以用來(lái)獲取配置的參數(shù)

從 webpack 5 開始,this.getOptions 可以獲取到 loader 上下文對(duì)象。它用來(lái)替代來(lái)自loader-utils中的 getOptions 方法。

  1. module.exports = function (content) { 
  2.   // 獲取到用戶傳給當(dāng)前 loader 的參數(shù) 
  3.   const options = this.getOptions() 
  4.   const res = someSyncOperation(content, options) 
  5.   this.callback(null, res, sourceMaps); 
  6.   // 注意這里由于使用了 this.callback 直接 return 就行 
  7.   return 

這樣一個(gè)同步的 loader 就完成了!

再來(lái)說(shuō)說(shuō)異步: 同步與異步的區(qū)別很好理解,一般我們的轉(zhuǎn)換流程都是同步的,但是當(dāng)我們遇到譬如需要網(wǎng)絡(luò)請(qǐng)求等場(chǎng)景,那么為了避免阻塞構(gòu)建步驟,我們會(huì)采取異步構(gòu)建的方式,對(duì)于異步 loader 我們主要需要使用 this.async() 來(lái)告知 webpack 這次構(gòu)建操作是異步的,不多廢話,看代碼就懂了:

  1. module.exports = function (content) { 
  2.   var callback = this.async() 
  3.   someAsyncOperation(content, function (err, result) { 
  4.     if (err) return callback(err) 
  5.     callback(null, result, sourceMaps, meta) 
  6.   }) 

② "Raw" loader

默認(rèn)情況下,資源文件會(huì)被轉(zhuǎn)化為 UTF-8 字符串,然后傳給 loader。通過(guò)設(shè)置 raw 為 true,loader 可以接收原始的 Buffer。每一個(gè) loader 都可以用 String 或者 Buffer 的形式傳遞它的處理結(jié)果。complier 將會(huì)把它們?cè)? loader 之間相互轉(zhuǎn)換。大家熟悉的 file-loader 就是用了這個(gè)。簡(jiǎn)而言之:你加上 module.exports.raw = true; 傳給你的就是 Buffer 了,處理返回的類型也并非一定要是 Buffer,webpack 并沒(méi)有限制。

  1. module.exports = function (content) { 
  2.   console.log(content instanceof Buffer); // true 
  3.   return doSomeOperation(content) 
  4. // 劃重點(diǎn)↓ 
  5. module.exports.raw = true

③ Pitching loader

我們每一個(gè) loader 都可以有一個(gè) pitch 方法,大家都知道,loader 是按照從右往左的順序被調(diào)用的,但是實(shí)際上,在此之前會(huì)有一個(gè)按照從左往右執(zhí)行每一個(gè) loader 的 pitch 方法的過(guò)程。pitch 方法共有三個(gè)參數(shù):

  • remainingRequest:loader 鏈中排在自己后面的 loader 以及資源文件的絕對(duì)路徑以!作為連接符組成的字符串。
  • precedingRequest:loader 鏈中排在自己前面的 loader 的絕對(duì)路徑以!作為連接符組成的字符串。
  • data:每個(gè) loader 中存放在上下文中的固定字段,可用于 pitch 給 loader 傳遞數(shù)據(jù)。

在 pitch 中傳給 data 的數(shù)據(jù),在后續(xù)的調(diào)用執(zhí)行階段,是可以在 this.data 中獲取到的:

  1. module.exports = function (content) { 
  2.   return someSyncOperation(content, this.data.value);// 這里的 this.data.value === 42 
  3. }; 
  4.  
  5. module.exports.pitch = function (remainingRequest, precedingRequest, data) { 
  6.   data.value = 42; 
  7. }; 

注意! 如果某一個(gè) loader 的 pitch 方法中返回了值,那么他會(huì)直接“往回走”,跳過(guò)后續(xù)的步驟,來(lái)舉個(gè)例子:

[[406790]]

假設(shè)我們現(xiàn)在是這樣:use: ['a-loader', 'b-loader', 'c-loader'],那么正常的調(diào)用順序是這樣:

現(xiàn)在 b-loader 的 pitch 改為了有返回值:

  1. // b-loader.js 
  2. module.exports = function (content) { 
  3.   return someSyncOperation(content); 
  4. }; 
  5.  
  6. module.exports.pitch = function (remainingRequest, precedingRequest, data) { 
  7.   return "誒,我直接返回,就是玩兒~" 
  8. }; 

那么現(xiàn)在的調(diào)用就會(huì)變成這樣,直接“回頭”,跳過(guò)了原來(lái)的其他三個(gè)步驟:

1.5 其他 API

  • this.addDependency:加入一個(gè)文件進(jìn)行監(jiān)聽,一旦文件產(chǎn)生變化就會(huì)重新調(diào)用這個(gè) loader 進(jìn)行處理
  • this.cacheable:默認(rèn)情況下 loader 的處理結(jié)果會(huì)有緩存效果,給這個(gè)方法傳入 false 可以關(guān)閉這個(gè)效果
  • this.clearDependencies:清除 loader 的所有依賴
  • this.context:文件所在的目錄(不包含文件名)
  • this.data:pitch 階段和正常調(diào)用階段共享的對(duì)象
  • this.getOptions(schema):用來(lái)獲取配置的 loader 參數(shù)選項(xiàng)
  • this.resolve:像 require 表達(dá)式一樣解析一個(gè) request。resolve(context: string, request: string, callback: function(err, result: string))
  • this.loaders:所有 loader 組成的數(shù)組。它在 pitch 階段的時(shí)候是可以寫入的。
  • this.resource:獲取當(dāng)前請(qǐng)求路徑,包含參數(shù):'/abc/resource.js?rrr'
  • this.resourcePath:不包含參數(shù)的路徑:'/abc/resource.js'
  • this.sourceMap:bool 類型,是否應(yīng)該生成一個(gè) sourceMap

官方還提供了很多實(shí)用 Api ,這邊只列舉一些可能常用的,更多可以戳鏈接👇更多詳見(jiàn)官方鏈接

1.6 來(lái)個(gè)簡(jiǎn)單實(shí)踐

功能實(shí)現(xiàn)

接下來(lái)我們簡(jiǎn)單實(shí)踐制作兩個(gè) loader ,功能分別是在編譯出的代碼中加上 /** 公司@年份 */ 格式的注釋和簡(jiǎn)單做一下去除代碼中的 console.log ,并且我們鏈?zhǔn)秸{(diào)用他們:

company-loader.js

  1. module.exports = function (source) { 
  2.   const options = this.getOptions() // 獲取 webpack 配置中傳來(lái)的 option 
  3.   this.callback(null, addSign(source, options.sign)) 
  4.   return 
  5.  
  6. function addSign(content, sign) { 
  7.   return `/** ${sign} */\n${content}` 

console-loader.js

  1. module.exports = function (content) { 
  2.   return handleConsole(content) 
  3.  
  4. function handleConsole(content) { 
  5.   return content.replace(/console.log\(['|"](.*?)['|"]\)/, ''

調(diào)用測(cè)試方式

功能就簡(jiǎn)單的進(jìn)行了一下實(shí)現(xiàn),這里我們主要說(shuō)一下如何測(cè)試調(diào)用我們的本地的 loader,方式有兩種,一種是通過(guò) Npm link 的方式進(jìn)行測(cè)試,這個(gè)方式的具體使用就不細(xì)說(shuō)了,大家可以簡(jiǎn)單查閱一下。 另外一種就是直接在項(xiàng)目中通過(guò)路徑配置的方式,有兩種情況:

1.匹配(test)單個(gè) loader,你可以簡(jiǎn)單通過(guò)在 rule 對(duì)象設(shè)置 path.resolve 指向這個(gè)本地文件

webpack.config.js

  1.   test: /\.js$/ 
  2.   use: [ 
  3.     { 
  4.       loader: path.resolve('path/to/loader.js'), 
  5.       options: {/* ... */} 
  6.     } 
  7.   ] 

2.匹配(test)多個(gè) loaders,你可以使用 resolveLoader.modules 配置,webpack 將會(huì)從這些目錄中搜索這些 loaders。例如,如果你的項(xiàng)目中有一個(gè) /loaders 本地目錄:

webpack.config.js

  1. resolveLoader: { 
  2.   // 這里就是說(shuō)先去找 node_modules 目錄中,如果沒(méi)有的話再去 loaders 目錄查找 
  3.   modules: [ 
  4.     'node_modules'
  5.     path.resolve(__dirname, 'loaders'
  6.   ] 

配置使用

我們這里的 webpack 配置如下所示:

  1. module: { 
  2.     rules: [ 
  3.       { 
  4.         test: /\.js$/, 
  5.         use: [ 
  6.           'console-loader'
  7.           { 
  8.             loader: 'company-loader'
  9.             options: { 
  10.               sign: 'we-doctor@2021'
  11.             }, 
  12.           }, 
  13.         ], 
  14.       }, 
  15.     ], 
  16.   }, 

項(xiàng)目中的 index.js:

  1. function fn() { 
  2.   console.log("this is a message"
  3.   return "1234" 

執(zhí)行編譯后的 bundle.js: 可以看到,兩個(gè) loader 的功能都體現(xiàn)到了編譯后的文件內(nèi)。

  1. /******/ (() => { // webpackBootstrap 
  2. var __webpack_exports__ = {}; 
  3. /*!**********************!*\ 
  4.   !*** ./src/index.js ***! 
  5.   \**********************/ 
  6. /** we-doctor@2021 */ 
  7. function fn() { 
  8.    
  9.   return "1234" 
  10. /******/ })() 

二、Plugin

為什么要有 plugin

plugin 提供了很多比 loader 中更完備的功能,他使用階段式的構(gòu)建回調(diào),webpack 給我們提供了非常多的 hooks 用來(lái)在構(gòu)建的階段讓開發(fā)者自由的去引入自己的行為。

基本結(jié)構(gòu)

  • 一個(gè)最基本的 plugin 需要包含這些部分:
  • 一個(gè) JavaScript 類
  • 一個(gè) apply 方法,apply 方法在 webpack 裝載這個(gè)插件的時(shí)候被調(diào)用,并且會(huì)傳入 compiler 對(duì)象。
  • 使用不同的 hooks 來(lái)指定自己需要發(fā)生的處理行為
  • 在異步調(diào)用時(shí)最后需要調(diào)用 webpack 提供給我們的 callback 或者通過(guò) Promise 的方式(后續(xù)異步編譯部分會(huì)詳細(xì)說(shuō))

  1. class HelloPlugin{ 
  2.   apply(compiler){ 
  3.     compiler.hooks.<hookName>.tap(PluginName,(params)=>{ 
  4.       /** do some thing */ 
  5.     }) 
  6.   } 
  7. module.exports = HelloPlugin 

Compiler andCompilation

Compiler 和 Compilation 是整個(gè)編寫插件的過(guò)程中的**重!中!之!重!**因?yàn)槲覀儙缀跛械牟僮鞫紩?huì)圍繞他們。

compiler 對(duì)象可以理解為一個(gè)和 webpack 環(huán)境整體綁定的一個(gè)對(duì)象,它包含了所有的環(huán)境配置,包括 options,loader 和 plugin,當(dāng) webpack 啟動(dòng)時(shí),這個(gè)對(duì)象會(huì)被實(shí)例化,并且他是全局唯一的,上面我們說(shuō)到的 apply 方法傳入的參數(shù)就是它。

compilation 在每次構(gòu)建資源的過(guò)程中都會(huì)被創(chuàng)建出來(lái),一個(gè) compilation 對(duì)象表現(xiàn)了當(dāng)前的模塊資源、編譯生成資源、變化的文件、以及被跟蹤依賴的狀態(tài)信息。它同樣也提供了很多的 hook 。

Compiler 和 Compilation 提供了非常多的鉤子供我們使用,這些方法的組合可以讓我們?cè)跇?gòu)建過(guò)程的不同時(shí)間獲取不同的內(nèi)容,具體詳情可參見(jiàn)官網(wǎng)直達(dá)。

上面的鏈接中我們會(huì)發(fā)現(xiàn)鉤子會(huì)有不同的類型,比如 SyncHook、SyncBailHook、AsyncParallelHook、AsyncSeriesHook ,這些不同的鉤子類型都是由 tapable 提供給我們的,關(guān)于 tapable 的詳細(xì)用法與解析可以參考我們前端構(gòu)建工具系列專欄中的 tapable 專題講解。

基本的使用方式是:

  1. compiler/compilation.hooks.<hookName>.tap/tapAsync/tapPromise(pluginName,(xxx)=>{/**dosth*/}) 
  • Tip: 以前的寫法是 compiler.plugin ,但是在最新的 webpack@5 可能會(huì)引起問(wèn)題,參見(jiàn) webpack-4-migration-notes

同步與異步

plugin 的 hooks 是有同步和異步區(qū)分的,在同步的情況下,我們使用 .tap 的方式進(jìn)行調(diào)用,而在異步 hook 內(nèi)我們可以進(jìn)行一些異步操作,并且有異步操作的情況下,請(qǐng)使用 tapAsync 或者 tapPromise 方法來(lái)告知 webpack 這里的內(nèi)容是異步的,當(dāng)然,如果內(nèi)部沒(méi)有異步操作的話,你也可以正常使用 tap 。

tapAsync

使用 tapAsync 的時(shí)候,我們需要多傳入一個(gè) callback 回調(diào),并且在結(jié)束的時(shí)候一定要調(diào)用這個(gè)回調(diào)告知 webpack 這段異步操作結(jié)束了。👇 比如:

  1. class HelloPlugin { 
  2.   apply(compiler) { 
  3.     compiler.hooks.emit.tapAsync(HelloPlugin, (compilation, callback) => { 
  4.       setTimeout(() => { 
  5.         console.log('async'
  6.         callback() 
  7.       }, 1000) 
  8.     }) 
  9.   } 
  10. module.exports = HelloPlugin 

tapPromise

當(dāng)使用 tapPromise 來(lái)處理異步的時(shí)候,我們需要返回一個(gè) Promise 對(duì)象并且讓它在結(jié)束的時(shí)候 resolve 👇

  1. class HelloPlugin { 
  2.   apply(compiler) { 
  3.     compiler.hooks.emit.tapPromise(HelloPlugin, (compilation) => { 
  4.       return new Promise((resolve) => { 
  5.         setTimeout(() => { 
  6.           console.log('async'
  7.           resolve() 
  8.         }, 1000) 
  9.       }) 
  10.     }) 
  11.   } 
  12. module.exports = HelloPlugin 

做個(gè)實(shí)踐

接下來(lái)我們通過(guò)實(shí)際來(lái)做一個(gè)插件梳理一遍整體的流程和零散的功能點(diǎn),這個(gè)插件實(shí)現(xiàn)的功能是在打包后輸出的文件夾內(nèi)多增加一個(gè) markdown 文件,文件內(nèi)記錄打包的時(shí)間點(diǎn)、文件以及文件大小的輸出。

首先我們根據(jù)需求確定我們需要的 hook ,由于需要輸出文件,我們需要使用 compilation 的 emitAsset 方法。 其次由于需要對(duì) assets 進(jìn)行處理,所以我們使用 compilation.hooks.processAssets ,因?yàn)?processAssets 是負(fù)責(zé) asset 處理的鉤子。

這樣我們插件結(jié)構(gòu)就出來(lái)了👇OutLogPlugin.js

  1. class OutLogPlugin { 
  2.   constructor(options) { 
  3.     this.outFileName = options.outFileName 
  4.   } 
  5.   apply(compiler) { 
  6.     // 可以從編譯器對(duì)象訪問(wèn) webpack 模塊實(shí)例 
  7.     // 并且可以保證 webpack 版本正確 
  8.     const { webpack } = compiler 
  9.     // 獲取 Compilation 后續(xù)會(huì)用到 Compilation 提供的 stage 
  10.     const { Compilation } = webpack 
  11.     const { RawSource } = webpack.sources 
  12.     /** compiler.hooks.<hoonkName>.tap/tapAsync/tapPromise */ 
  13.     compiler.hooks.compilation.tap('OutLogPlugin', (compilation) => { 
  14.       compilation.hooks.processAssets.tap( 
  15.         { 
  16.           name'OutLogPlugin'
  17.           // 選擇適當(dāng)?shù)?nbsp;stage,具體參見(jiàn): 
  18.           // https://webpack.js.org/api/compilation-hooks/#list-of-asset-processing-stages 
  19.           stage: Compilation.PROCESS_ASSETS_STAGE_SUMMARIZE, 
  20.         }, 
  21.         (assets) => { 
  22.           let resOutput = `buildTime: ${new Date().toLocaleString()}\n\n` 
  23.           resOutput += `| fileName  | fileSize  |\n| --------- | --------- |\n` 
  24.           Object.entries(assets).forEach(([pathname, source]) => { 
  25.             resOutput += `| ${pathname} | ${source.size()} bytes |\n` 
  26.           }) 
  27.           compilation.emitAsset( 
  28.             `${this.outFileName}.md`, 
  29.             new RawSource(resOutput), 
  30.           ) 
  31.         }, 
  32.       ) 
  33.     }) 
  34.   } 
  35. module.exports = OutLogPlugin 

對(duì)插件進(jìn)行配置:webpack.config.js

  1. const OutLogPlugin = require('./plugins/OutLogPlugin'
  2.  
  3. module.exports = { 
  4.   plugins: [ 
  5.     new OutLogPlugin({outFileName:"buildInfo"}) 
  6.   ], 

打包后的目錄結(jié)構(gòu):

  1. dist 
  2. ├─ buildInfo.md 
  3. ├─ bundle.js 
  4. └─ bundle.js.map 

buildInfo.md

可以看到按照我們希望的格式準(zhǔn)確輸出了內(nèi)容,這樣一個(gè)簡(jiǎn)單的功能插件就完成了!

 

責(zé)任編輯:姜華 來(lái)源: 微醫(yī)大前端技術(shù)
相關(guān)推薦

2022-05-18 08:51:44

調(diào)用模板后端并行

2023-03-27 08:28:57

spring代碼,starter

2023-11-28 07:36:41

Shell腳本部署

2019-08-26 09:25:23

RedisJavaLinux

2022-06-28 15:29:56

Python編程語(yǔ)言計(jì)時(shí)器

2014-01-22 09:19:57

JavaScript引擎

2020-12-23 09:48:37

數(shù)據(jù)工具技術(shù)

2017-07-19 13:27:44

前端Javascript模板引擎

2022-08-26 08:01:38

DashWebJavaScrip

2022-09-22 12:38:46

antd form組件代碼

2016-11-01 09:46:04

2021-07-14 09:00:00

JavaFX開發(fā)應(yīng)用

2011-01-10 14:41:26

2011-05-03 15:59:00

黑盒打印機(jī)

2018-05-16 13:50:30

Python網(wǎng)絡(luò)爬蟲Scrapy

2018-05-16 15:46:06

Python網(wǎng)絡(luò)爬蟲PhantomJS

2020-05-09 09:59:52

Python數(shù)據(jù)土星

2023-03-22 09:00:38

2018-11-22 09:17:21

消息推送系統(tǒng)

2019-10-29 15:46:07

區(qū)塊鏈區(qū)塊鏈技術(shù)
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)