您當前的位置:首頁 > 攝影

Vite學習:2 完善css載入 和 熱更新

作者:由 溜達魚 發表于 攝影時間:2022-01-11

上一篇(Vite 學習:1 從零實現一個 no-bundle 構建工具)已經實現了一個簡易 my-vite,毫無疑問它還有很多功能、細節需要完善。

本篇將完善 css載入 和 熱更新功能

改用 ts

為了可讀性和易維護,我們將現在 commonJS 規範的程式碼 改寫為 ts: 將所有 vite 檔案改為 ts 字尾,下載 ts-node 和 typescript,改用 ts-node 啟動專案。

這時會報一些錯誤,主要是型別補充 和 型別包載入問題,根據推斷補充 並 下載型別包即可:

//

package。json

“scripts”

{

“start”

“cd vite && ts-node 。/index。ts”

}

“devDependencies”

{

“@types/connect”

“^3。4。35”

“@types/debug”

“^4。1。7”

“@types/hash-sum”

“^1。0。0”

“@types/parseurl”

“^1。3。1”

“@types/ws”

“^8。2。2”

“ts-node”

“^10。4。0”

“typescript”

“^4。5。4”

}

完善“測試用例”

增加子元件,增加css相關程式碼:

// App。vue

// Child。vue

// style。css

span {

color: green;

}

理論上頁面應該長這樣:

Vite學習:2 完善css載入 和 熱更新

css載入

發現問題

重啟 my-vite 工具,發現 style。css 檔案載入異常 且 child元件顏色不符合預期 :

Vite學習:2 完善css載入 和 熱更新

Vite學習:2 完善css載入 和 熱更新

這是因為css中透過 import 載入了style。css 檔案,發起檔案請求但我們並沒有進行處理。

首先想到的解決方案是 新增 middleware/css,但 style。css 需要結合 vue style 的 scpoe、module 等屬性,如果單獨抽離到單獨的中介軟體需要處理很多和 vue 相關的細節。換個思路想,因為 css 檔案由 vue 元件引入,所以可以直接在 middleware/vue 處理 sfcStyle 的時候一起處理 @import 語句。

處理 @import css

思路為:判斷包含了 @import 時,將 postcss 處理後的css 傳給 @vue/compiler-sfc compileStyle

// middleware/vue

import postcss from ‘postcss’;

import atImport from ‘postcss-import’;

const compileSFCStyle = async (res: any, style: SFCStyleBlock, filepath: string, pathname: string, index: any) => {

let source: any = style。content

const needInlineImport = source。includes(‘@import’)

if (needInlineImport) {

const postRes = await postcss()

。use(atImport())

。process(source, {

from: filepath,

});

source = postRes。css;

}

// 。。。compileStyle

}

熱更新

熱更新功能在開發模式下給開發者帶來了很大的便利:在我們更改本地檔案時,可以自動重新渲染頁面對應模組 而無需手動重新整理,提升開發效率。

梳理成開發思路:監聽檔案變化 —— 獲取檔案變化資訊 —— 通知瀏覽器 —— 瀏覽器根據資訊替換節點/css檔案。

轉換成問題:如何監聽檔案變化 —— 如何對比檔案獲取變化資訊 —— 如何通知瀏覽器 —— 瀏覽器如何獲取資訊 如何替換節點/css。

接下來,我們按照上面的路徑 再把問題轉換成解決方案及程式碼:

監聽檔案變化

在 server 啟動時 使用工具包 chokidar 監聽 開發資料夾:

// vite/index。ts

import

chokidar

from

‘chokidar’

const

fileWatcher

=

chokidar

watch

cwd

{

ignored

/node_modules/

})

fileWatcher

on

‘change’

async

file

=>

{

// 判斷檔案更改型別

})

判斷檔案更改型別

(為了簡化,這裡只處理vue檔案)

判斷更改,就要知道檔案之前是什麼 現在是什麼,所以要做一層快取。那麼在什麼時候快取呢?當然是解析檔案的時候,也就是vue中介軟體中。在判斷檔案更改時,還要處理新的檔案,所以把vue檔案解析 抽成一個有快取功能的解析函式。

// vite/middleware/vue。ts

const

cache

=

new

Map

();

export

const

parseSFC

=

async

pathname

string

=>

{

const

{

filepath

source

}

=

await

readSource

pathname

const

{

descriptor

errors

}

=

parse

source

{

filename

filepath

sourceMap

true

})

const

prevDescriptor

=

cache

get

filepath

||

{};

cache

set

filepath

descriptor

return

{

filepath

descriptor

prevDescriptor

};

}

// vueMiddleware 函式內也有相應更改

有了快取功能,我們就能透過檔案路徑獲取新舊檔案內容了,然後透過逐個模組的判斷更改型別即可:

// vite/index。ts

if

file

endsWith

‘。vue’

))

{

const

resourcePath

=

‘/’

+

path

relative

cwd

file

const

{

descriptor

prevDescriptor

}

=

await

parseSFC

resourcePath

// 判斷檔案更改

}

function

isEqual

a

SFCBlock

|

null

b

SFCBlock

|

null

{

if

a

&&

b

return

true

if

a

||

b

return

false

if

a

src

&&

b

src

&&

a

src

===

b

src

return

true

if

a

content

!==

b

content

return

false

const

keysA

=

Object

keys

a

attrs

const

keysB

=

Object

keys

b

attrs

if

keysA

length

!==

keysB

length

{

return

false

}

return

keysA

every

((

key

=>

a

attrs

key

===

b

attrs

key

])

}

判斷 vue 檔案更改 可以分為3類:template script css,template script 都只有一塊 比較好處理

// vite/index。ts

if

isEqual

descriptor

script

prevDescriptor

script

))

{

send

({

type

‘reload’

path

resourcePath

})

return

}

if

isEqual

descriptor

template

prevDescriptor

template

))

{

send

({

type

‘rerender’

path

resourcePath

})

return

}

css是一個數組,且有scoped屬性,處理稍顯複雜:

const

prevStyles

=

prevDescriptor

styles

||

[]

const

nextStyles

=

descriptor

styles

||

[]

if

prevStyles

some

((

s

{

scoped

any

})

=>

s

scoped

!==

nextStyles

some

((

s

=>

s

scoped

{

send

({

type

‘reload’

path

resourcePath

})

}

nextStyles

forEach

((

_

i

=>

{

if

prevStyles

i

||

isEqual

prevStyles

i

],

nextStyles

i

]))

{

send

({

type

‘style-update’

path

resourcePath

index

i

})

}

})

prevStyles

slice

nextStyles

length

)。

forEach

((

_

any

i

number

=>

{

send

({

type

‘style-remove’

path

resourcePath

id

`

${

hash_sum

resourcePath

}

-

${

i

+

nextStyles

length

}

`

})

})

上面用到了 send 函式傳送通知,下面小節會講

通知 與 接收通知

傳送訊息:server端 監聽到檔案變動時,會透過websocket send訊息

接收訊息:瀏覽器 透過websocket 接收 server 發過來的訊息

// vite/index。ts

import { WebSocketServer, WebSocket } from ‘ws’;

const sockets = new Set()

wss。on(‘connection’, socket => {

// console。log(chalk。green(‘[wss connection]’))

sockets。add(socket)

})

// watcher 為 chokidar。watch 相關程式碼 抽象而成,第一個函式為監聽路徑,第二個函式為通知函式 send會呼叫該函式

watcher(root, (payload: any) => {

sockets。forEach(s => s。send(JSON。stringify(payload)))

})

到這裡 服務端已經能傳送訊息了,那瀏覽器如何接收訊息呢?這就需要向瀏覽器中注入接收 ws 資訊的相關程式碼了。問題又來了,在什麼時機 向哪個檔案注入?

這裡選擇在 index。html 中注入(預設 index。html 只有一個 且 比較好判斷,個人覺得也可以向 main。js 中注入):

新增一個hmr中介軟體,判斷為主html時,插入hmrClient 相關程式碼:

export

const

hmrMiddleware

=

()

=>

{

return

async

req

any

res

any

next

any

=>

{

if

req

url

===

‘/__hmrClient’

{

const

result

=

fs

readFileSync

path

resolve

__dirname

‘。。/client/client。js’

),

‘utf-8’

res

setHeader

‘Content-Type’

‘application/javascript’

res

end

result

}

else

if

req

url

===

‘/’

{

const

script

=

``

let

html

=

fs

readFileSync

path

resolve

__dirname

‘。。/。。/index。html’

),

‘utf-8’

const

tag

=

‘’

html

=

html

replace

tag

script

+

tag

);

res

setHeader

‘Content-Type’

‘text/html;charset=utf-8’

res

end

html

}

else

{

await

next

()

}

}

}

這裡需要注意的是 __hmrClient 載入需要在 main。js 檔案之後,保證 vue 相關程式碼已載入

watcher 函式 其實也屬於 hmr,也可以整理到該中介軟體中

模組熱替換

到這裡,已經實現了資訊通訊,所以根據資訊熱替換就可以了,需要在客戶端的注入檔案 __hmrClient 中補充

template script 更改需要配合 vue 的 __VUE_HMR_RUNTIME__ ,需要給節點新增 hmr 相關屬性:

// vite/middleware/vue。ts compileSFCMain 函式 export default 之前

out

+=

`

\

n__script。__hmrId =

${

JSON

stringify

pathname

}

`

__hmrClient(vite/client/client。js) 用來連線服務端 ws 以及處理 message 資訊:

var

__VUE_HMR_RUNTIME__

=

window

__VUE_HMR_RUNTIME__

const

socket

=

new

WebSocket

`ws://

${

location

host

}

`

socket

addEventListener

‘message’

({

data

})

=>

{

const

{

type

path

id

index

}

=

JSON

parse

data

switch

type

{

// 。。。 根據 type 處理

}

})

處理各種型別資訊:(path 和 id 規則和上一篇 middleware/vue 的內容是匹配的,可以對照看一下)

case

‘connected’

console

log

`[vds] connected。`

break

case

‘reload’

import

`

${

path

}

?t=

${

Date

now

()

}

`

)。

then

((

m

=>

{

__VUE_HMR_RUNTIME__

reload

path

m

default

console

log

`[vds]

${

path

}

reloaded。

${

JSON

stringify

m

default

}

`

})

break

case

‘rerender’

import

`

${

path

}

?type=template&t=

${

Date

now

()

}

`

)。

then

((

m

=>

{

__VUE_HMR_RUNTIME__

rerender

path

m

render

console

log

`[vds]

${

path

}

template updated。`

})

break

case

‘style-update’

console

log

`[vds]

${

path

}

style

${

index

>

0

`#

${

index

}

`

``

}

updated。`

import

`

${

path

}

?type=style&index=

${

index

}

&t=

${

Date

now

()

}

`

break

case

‘style-remove’

const

style

=

document

getElementById

`vue-style-

${

id

}

`

if

style

{

style

parentNode

removeChild

style

}

break

case

‘full-reload’

location

reload

()

小結

vue 中介軟體中 需要給script新增 __hmrId 標記,且在 parse 時根據檔案路徑進行快取

index 中宣告 服務端 ws,並透過 ws。on(‘connection’, socket => { // handleSocket }) 收集客戶端連線來的socket

hmr 中介軟體中,宣告 watcher 檔案監聽函式,當觸發 chokidar。watch(。。。dirpath)。on(‘change’, (file) => { // handleFile }) 時,根據第1步的快取 處理檔案更改型別 為 payload 資訊,觸發傳送函式

index 中呼叫 watcher,並在傳送函式內觸發 socket。send(payload)

hmr 中介軟體中,在 index。html 中注入 __hmrClient 檔案,使瀏覽器端 宣告socket,以觸發第2步的ws。connection,根據在第4步接收的 payload資訊 進行節點熱替換

發現問題

// todo

最佳化

// todo

node 工具包

包名

功能

備註

chokidar

監聽檔案變化

ws

websocket

postcss

透過js命令轉換css

postcss-import

postcss外掛:解析css中的@import語句

標簽: __  vue  CSS  index  Style