Vite學習:2 完善css載入 和 熱更新
上一篇(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
span
import Child from ‘。/Child。vue’;
export default {
components: {
Child
},
data() {
return {
msg: ‘my-vite haah !!!’
}
},
}
span {
color: red;
}
div {
color: red;
}
// Child。vue
span
export default {
data() {
return {
msg: ‘my-vite child !!!’
}
},
}
@import ‘。/style。css’;
div {
color: green;
}
// style。css
span {
color: green;
}
理論上頁面應該長這樣:
css載入
發現問題
重啟 my-vite 工具,發現 style。css 檔案載入異常 且 child元件顏色不符合預期 :
這是因為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
=
‘