使用webpack在開發中,只改動一句代碼,也需要數秒的熱更新,這是因為webpack需要將所有的模塊打包成一個一個或者多個模塊,然后啟動開發服務器,請求服務器時直接給予打包結果。這個過程隨着項目的擴大,速度會變慢。然后vite來了。
描述:針對Vue單頁面組件的無打包開發服務器,可以直接在瀏覽器運行請求的vue文件
特點:
- 冷服務啟動-使用ES6 import預覽的時候不打包
- 開發中熱更新
- 按需進行編譯,不會刷新全部DOM
vite
使用vite創建vue3項目
1、Npm 創建 vite項目
npm init vite-app projectName
2、Yarn 創建vite項目
yarn create vite-app projectName
3、vite創建react項目
-
新建文件夾。
-
進入文件夾中命令npm init vite-app --template react
-
安裝依賴 yarn
-
運行 yarn dev
問題1:既然去掉了webpack打包步驟,那么vite是如何處理這些模塊的呢?
當聲明一個 script 標簽類型為 module 時,
<script type="module" src="/src/main.js"></script>
瀏覽器就會像服務器發起一個GET http://localhost:3000/src/main.js請求main.js文件,
瀏覽器請求到了main.js文件,檢測到內部含有import引入的包,又會對其內部的 import引用發起 HTTP 請求獲取模塊的內容文件!如: GET http://localhost:3000/@modules/vue.js 如: GET http://localhost:3000/src/App.vue 其Vite 的主要功能就是通過劫持瀏覽器的這些請求,並在后端進行相應的處理將項目中使用的文件通過簡單的分解與整合,然后再返回給瀏覽器渲染頁面,vite整個過程中沒有對文件進行打包編譯,所以其運行速度比原始的webpack開發編譯速度快出許多!
- 在瀏覽器中啟動了一個socket服務,實時的接受一系列的指令,根據指令再處理相應的邏輯。
// src/node/serve/index.ts
export async function createServer(
inlineConfig: InlineConfig = {}
): Promise<ViteDevServer> {
// 創建serve服務
const app = connect() as Connect.Server
const ws = createWebSocketServer(httpServer, logger)
const watchOptions = serverConfig.watch || {}
const watcher = chokidar.watch(root, {
ignored: [
'**/node_modules/**',
'**/.git/**',
...(watchOptions.ignored || [])
],
...
}) as FSWatcher
}
createWebSocketServer處理
// src/node/serve/ws.ts
export function createWebSocketServer(
server: Server,
logger: Logger
): WebSocketServer {
// 啟動一個webSocket服務
const wss = new WebSocket.Server({ noServer: true })
server.on('upgrade', (req, socket, head) => {
...
}
})
// 通知客戶端鏈接成功,需要請求文件
wss.on('connection', (socket) => {
socket.send(JSON.stringify({ type: 'connected' }))
...
})
}
- server端負責在執行的各個階段向客戶端發送指令
// src/linet/client.ts
async function handleMessage(payload: HMRPayload) {
switch (payload.type) {
case 'connected': // scoket鏈接成功
console.log(`[vite] connected.`)
setInterval(() => socket.send('ping'), __HMR_TIMEOUT__)
break
case 'update':// js文件更新
...
...
break
case 'custom'://自定義
...
break
case 'full-reload': //網頁重刷新
...
break
case 'prune': //移除模塊
...
break
case 'error':
...
break
default:
const check: never = payload
return check
}
}
當vite文件監聽系統監聽到.vue組件發生變化之后,就會去解析編譯.vue組件,並向client發送一條對應指令,並把編譯后的代碼也發送給client
問題2:為什么給vue的模塊加一個前綴@modules ?
import { createApp } from 'vue'
編譯器能夠自動從node_modules尋找vue這個模塊,是因為npm install時,編譯器存儲了vue別名,因此可直接去node_modules中讀取。
但瀏覽器環境並沒有執行這個過程,因此依然會從當前文件的同級路徑下尋找vue這個文件,如果文件不存在,則報404錯誤,因此我們要把 node_modules 變成瀏覽器環境可識別的位置,即 /@modules/
vue模塊安裝在node_modules中,瀏覽器ES Module是無法直接獲取到項目下node_modules目錄中的文件。所以vite對import都做了一層處理,重寫了前綴使其帶有@modules
問題3:既然瀏覽器直接請求了.vue 文件,那么文件內容是如何做出解析的呢?
唯一編譯.vue文件,被解析成render函數返回給瀏覽器渲染頁面。當Vite遇到一個.vue后綴的文件時。由於.vue模板文件的特殊性,它被分割成template,css,腳本模塊三個模塊進行分別處理。最后放入script,template,css發送多個請求獲取。
單頁面文件的請求都是以*.vue作為請求路徑結尾,當服務器接收到這種特點的http請求,主要處理
- 根據
ctx.path確定請求具體的vue文件 - 使用
parseSFC解析該文件,獲得descriptor,一個descriptor包含了這個組件的基本信息,包括template、script和styles等屬性
然后根據descriptor和ctx.query.type選擇對應類型的方法,處理后返回
// plugin-vue/src/index.ts
export default function vuePlugin(rawOptions: Options = {}): Plugin {
...
transform(code, id) {
const { filename, query } = parseVueRequest(id)
...
if (!query.vue) {
...
} else {
// 使用parseSFC解析該文件
const descriptor = getDescriptor(filename)!
// 根據`descriptor`和`ctx.query.type`選擇對應類型解析的方法
if (query.type === 'template') {
return transformTemplateAsModule(code, descriptor, options, this)
} else if (query.type === 'style') {
...
}
}
}
}
// plugin-vue/src/template.ts
export function transformTemplateAsModule(
...
) {
...
if (options.devServer && !options.isProduction) {
returnCode += `\nimport.meta.hot.accept(({ render }) => {
__VUE_HMR_RUNTIME__.rerender(${JSON.stringify(descriptor.id)}, render)
})`
}
return {
code: returnCode,
map: result.map as any
}
}
總結:
1、默認采用ES 6原生模塊
2、默認會給vue的模塊加一個前綴@modules import { createApp } from '/@modules/vue.js'
3、解析.vue文件
vite的優雅之處就在於需要某個模塊時動態引入,而不是提前打包,自然而然提高了開發體驗
