學習Vite的原理
1. 概述
Vite是一個更輕、更快的web應(yīng)用開發(fā)工具,面向現(xiàn)代瀏覽器。底層基于ECMAScript標準原生模塊系統(tǒng)ES Module實現(xiàn)。他的出現(xiàn)是為了解決webpack冷啟動時間過長以及Webpack HMR
熱更新反應(yīng)速度慢等問題。
默認情況下Vite創(chuàng)建的項目是一個普通的Vue3應(yīng)用,相比基于Vue-cli創(chuàng)建的應(yīng)用少了很多配置文件和依賴。
Vite
創(chuàng)建的項目所需要的開發(fā)依賴非常少,只有Vite
和@vue/compiler-sfc
。這里面Vite是一個運行工具,compiler-sfc
則是為了編譯.vue結(jié)尾的單文件組件。在創(chuàng)建項目的時候通過指定不同的模板也可以支持使用其他框架例如React。項目創(chuàng)建完成之后可以通過兩個命令啟動和打包。
# 開啟服務(wù)器 vite serve # 打包 vite build
正是因為Vite
啟動的web服務(wù)不需要編譯打包,所以啟動的速度特別快,調(diào)試階段大部分運行的代碼都是你在編輯器中書寫的代碼,這相比于webpack的編譯后再呈現(xiàn)確實要快很多。當然生產(chǎn)環(huán)境還是需要打包的,畢竟很多時候我們使用的最新ES規(guī)范在瀏覽器中還沒有被支持,Vite的打包過程和webpack類似會將所有文件進行編譯打包到一起。對于代碼切割的需求Vite采用的是原生的動態(tài)導入來實現(xiàn)的,所以打包結(jié)果只能支持現(xiàn)代瀏覽器,如果需要兼容老版本瀏覽器可以引入Polyfill
。
使用Webpack打包除了因為瀏覽器環(huán)境并不支持模塊化和新語法外,還有就是模塊文件會產(chǎn)生大量的http請求。如果你使用模塊化的方式開發(fā),一個頁面就會有十幾甚至幾十個模塊,而且很多時候會出現(xiàn)幾kb的文件,打開一個頁面要加載幾十個js資源這顯然是不合理的。
- Vite創(chuàng)建的項目幾乎不需要額外的配置默認已經(jīng)支持TS、Less, Sass,Stylus,postcss了,但是需要單獨安裝對應(yīng)的編譯器,同時默認還支持jsx和Web Assembly。
- Vite帶來的好處是提升開發(fā)者在開發(fā)過程中的體驗,web開發(fā)服務(wù)器不需要等待即可立即啟動,模塊熱更新幾乎是實時的,所需的文件會按需編譯,避免編譯用不到的文件。并且開箱即用避免loader及plugins的配置。
- Vite的核心功能包括開啟一個靜態(tài)的web服務(wù)器,能夠編譯單文件組件并且提供HMR功能。當啟動vite的時候首先會將當前項目目錄作為靜態(tài)服務(wù)器的根目錄,靜態(tài)服務(wù)器會攔截部分請求,當請求單文件的時候會實時編譯,以及處理其他瀏覽器不能識別的模塊,通過websocket實現(xiàn)hmr。
2. 實現(xiàn)靜態(tài)測試服務(wù)器
首先實現(xiàn)一個能夠開啟靜態(tài)web服務(wù)器的命令行工具。vite1.x內(nèi)部使用的是Koa來實現(xiàn)靜態(tài)服務(wù)器。(ps:node命令行工具可以查看我之前的文章,這里就不介紹了,直接貼代碼)。
npm init npm install koa koa-send -D
工具bin的入口文件設(shè)置為本地的index.js
#!/usr/bin/env node const Koa = require('koa') const send = require('koa-send') const app = new Koa() // 開啟靜態(tài)文件服務(wù)器 app.use(async (ctx, next) => { ? ? // 加載靜態(tài)文件 ? ? await send(ctx, ctx.path, { root: process.cwd(), index: 'index.html'}) ? ? await next() }) app.listen(5000) console.log('服務(wù)器已經(jīng)啟動 http://localhost:5000')
這樣就編寫好了一個node
靜態(tài)服務(wù)器的工具。
3. 處理第三方模塊
我們的做法是當代碼中使用了第三方模塊(node_modules
中的文件),可以通過修改第三方模塊的路徑給他一個標識,然后在服務(wù)器中拿到這個標識來處理這個模塊。
首先需要修改第三方模塊的路徑,這里需要一個新的中間件來實現(xiàn)。判斷一下當前返回給瀏覽器的文件是否是javascript,只需要看響應(yīng)頭中的content-type。如果是javascript
需要找到這個文件中引入的模塊路徑。ctx.body
就是返回給瀏覽器的內(nèi)容文件。這里的數(shù)據(jù)是一個stream,需要轉(zhuǎn)換成字符串來處理。
const stream2string = (stream) => { ? ? return new Promise((resolve, reject) => { ? ? ? ? const chunks = []; ? ? ? ? stream.on('data', chunk => {chunks.push(chunk)}) ? ? ? ? stream.on('end', () => { resolve(Buffer.concat(chunks).toString('utf-8'))}) ? ? ? ? stream.on('error', reject) ? ? }) } // 修改第三方模塊路徑 app.use(async (ctx, next) => { ? ? if (ctx.type === 'application/javascript') { ? ? ? ? const contents = await stream2string(ctx.body); ? ? ? ? // 將body中導入的路徑修改一下,重新賦值給body返回給瀏覽器 ? ? ? ? // import vue from 'vue', 匹配到from '修改為from '@modules/ ? ? ? ? ctx.body = contents.replace(/(from\s+['"])(?![\.\/])/g, '$1/@modules/'); ? ? } })
接著開始加載第三方模塊, 這里同樣需要一個中間件,判斷請求路徑是否是修改過的@module開頭,如果是的話就去node_modules里面加載對應(yīng)的模塊返回給瀏覽器。這個中間件要放在靜態(tài)服務(wù)器之前。
// 加載第三方模塊 app.use(async (ctx, next) => { ? ? if (ctx.path.startsWith('/@modules/')) { ? ? ? ? // 截取模塊名稱 ? ? ? ? const moduleName = ctx.path.substr(10); ? ? } })
拿到模塊名稱之后需要獲取模塊的入口文件,這里要獲取的是ES Module模塊的入口文件,需要先找到這個模塊的package.json
然后再獲取這個package.json
中的module字段的值也就是入口文件。
// 找到模塊路徑 const pkgPath = path.join(process.pwd(), 'node_modules', moduleName, 'package.json'); const pkg = require(pkgPath); // 重新給ctx.path賦值,需要重新設(shè)置一個存在的路徑,因為之前的路徑是不存在的 ctx.path = path.join('/node_modules', moduleName, pkg.module); // 執(zhí)行下一個中間件 awiat next();
這樣瀏覽器請求進來的時候雖然是@modules路徑,但是在加載之前將path路徑修改為了node_modules
中的路徑,這樣在加載的時候就會去node_modules
中獲取文件,將加載的內(nèi)容響應(yīng)給瀏覽器。
加載第三方模塊:
app.use(async (ctx, next) => { ? ? if (ctx.path.startsWith('/@modules/')) { ? ? ? ? // 截取模塊名稱 ? ? ? ? const moduleName = ctx.path.substr(10); ? ? ? ? // 找到模塊路徑 ? ? ? ? const pkgPath = path.join(process.pwd(), 'node_modules', moduleName, 'package.json'); ? ? ? ? const pkg = require(pkgPath); ? ? ? ? // 重新給ctx.path賦值,需要重新設(shè)置一個存在的路徑,因為之前的路徑是不存在的 ? ? ? ? ctx.path = path.join('/node_modules', moduleName, pkg.module); ? ? ? ? // 執(zhí)行下一個中間件 ? ? ? ? awiat next(); ? ? } })
4. 單文件組件處理
之前說過瀏覽器是沒辦法處理.vue資源的, 瀏覽器只能識別js、css等常用資源,所以其他類型的資源都需要在服務(wù)端處理。當請求單文件組件的時候需要在服務(wù)器將單文件組件編譯成js模塊返回給瀏覽器。
所以這里當瀏覽器第一次請求App.vue的時候,服務(wù)器會把單文件組件編譯成一個對象,先加載這個組件,然后再創(chuàng)建一個對象。
import Hello from './src/components/Hello.vue' const __script = { ? ? name: "App", ? ? components: { ? ? ? ? Hello ? ? } }
接著再去加載入口文件,這次會告訴服務(wù)器編譯一下這個單文件組件的模板,返回一個render函數(shù)。然后將render函數(shù)掛載到剛創(chuàng)建的組件選項對象上,最后導出選項對象。
import { render as __render } from '/src/App.vue?type=template' __script.render = __render __script.__hmrId = '/src/App.vue' export default __script
也就是說vite
會發(fā)送兩次請求,第一次請求會編譯單文件文件,第二次請求是編譯單文件模板返回一個render
函數(shù)。
編譯單文件選項:
首先來實現(xiàn)一下第一次請求單文件的情況。需要把單文件組件編譯成一個選項,這里同樣用一個中間件來實現(xiàn)。這個功能要在處理靜態(tài)服務(wù)器之后,處理第三方模塊路徑之前。
首先需要對單文件組件進行編譯需要借助compiler-sfc
。
// 處理單文件組件 app.use(async (ctx, next) => { ? ? if (ctx.path.endsWith('.vue')) { ? ? ? ? // 獲取響應(yīng)文件內(nèi)容,轉(zhuǎn)換成字符串 ? ? ? ? const contents = await streamToString(ctx.body); ? ? ? ? // 編譯文件內(nèi)容 ? ? ? ? const { descriptor } = compilerSFC.parse(contents); ? ? ? ? // 定義狀態(tài)碼 ? ? ? ? let code; ? ? ? ? // 不存在type就是第一次請求 ? ? ? ? if (!ctx.query.type) { ? ? ? ? ? ? code = descriptor.script.content; ? ? ? ? ? ? // 這里的code格式是, 需要改造成我們前面貼出來的vite中的樣子 ? ? ? ? ? ? // import Hello from './components/Hello.vue' ? ? ? ? ? ? // export default { ? ? ? ? ? ? // ? ? ?name: 'App', ? ? ? ? ? ? // ? ? ?components: { ? ? ? ? ? ? // ? ? ? ? ?Hello ? ? ? ? ? ? // ? ? ?} ? ? ? ? ? ? // ?} ? ? ? ? ? ? // 改造code的格式,將export default 替換為const __script = ? ? ? ? ? ? code = code.relace(/export\s+default\s+/g, 'const __script = ') ? ? ? ? ? ? code += ` ? ? ? ? ? ? ? ? import { render as __render } from '${ctx.path}?type=template' ? ? ? ? ? ? ? ? __script.rener = __render ? ? ? ? ? ? ? ? export default __script ? ? ? ? ? ? ` ? ? ? ? } ? ? ? ? // 設(shè)置瀏覽器響應(yīng)頭為js ? ? ? ? ctx.type = 'application/javascript' ? ? ? ? // 將字符串轉(zhuǎn)換成數(shù)據(jù)流傳給下一個中間件。 ? ? ? ? ctx.body = stringToStream(code); ? ? } ? ? await next() }) const stringToStream = text => { ? ? const stream = new Readable(); ? ? stream.push(text); ? ? stream.push(null); ? ? return stream; }
npm install @vue/compiler-sfc -D
接著我們再來處理單文件組件的第二次請求,第二次請求url會帶上type=template
參數(shù),需要將單文件組件模板編譯成render函數(shù)。
首先需要判斷當前請求中有沒有type=template
。
if (!ctx.query.type) { ? ? ... } else if (ctx.query.type === 'template') { ? ? // 獲取編譯后的對象 code就是render函數(shù) ? ? const templateRender = compilerSFC.compileTemplate({ source: descriptor.template.content }) ? ? // 將render函數(shù)賦值給code返回給瀏覽器 ? ? code = templateRender.code }
這里還要處理一下工具中的process.env
,因為這些代碼會返回到瀏覽器中運行,如果不處理會默認為node導致運行失敗。可以在修改第三方模塊路徑的中間件中修改,修改完路徑之后再添加一條修改process.env。
// 修改第三方模塊路徑 app.use(async (ctx, next) => { ? ? if (ctx.type === 'application/javascript') { ? ? ? ? const contents = await stream2string(ctx.body); ? ? ? ? // 將body中導入的路徑修改一下,重新賦值給body返回給瀏覽器 ? ? ? ? // import vue from 'vue', 匹配到from '修改為from '@modules/ ? ? ? ? ctx.body = contents.replace(/(from\s+['"])(?![\.\/])/g, '$1/@modules/').replace(/process\.env\.NODE_ENV/g, '"development"'); ? ? } })
至此就實現(xiàn)了一個簡版的vite,當然這里我們只演示了.vue文件,對于css,less等其他資源都沒有處理,不過方法都是類似的,感興趣的同學可以自行實現(xiàn)。
#!/usr/bin/env node const path = require('path') const { Readable } = require('stream) const Koa = require('koa') const send = require('koa-send') const compilerSFC = require('@vue/compiler-sfc') const app = new Koa() const stream2string = (stream) => { ? ? return new Promise((resolve, reject) => { ? ? ? ? const chunks = []; ? ? ? ? stream.on('data', chunk => {chunks.push(chunk)}) ? ? ? ? stream.on('end', () => { resolve(Buffer.concat(chunks).toString('utf-8'))}) ? ? ? ? stream.on('error', reject) ? ? }) } const stringToStream = text => { ? ? const stream = new Readable(); ? ? stream.push(text); ? ? stream.push(null); ? ? return stream; } // 加載第三方模塊 app.use(async (ctx, next) => { ? ? if (ctx.path.startsWith('/@modules/')) { ? ? ? ? // 截取模塊名稱 ? ? ? ? const moduleName = ctx.path.substr(10); ? ? ? ? // 找到模塊路徑 ? ? ? ? const pkgPath = path.join(process.pwd(), 'node_modules', moduleName, 'package.json'); ? ? ? ? const pkg = require(pkgPath); ? ? ? ? // 重新給ctx.path賦值,需要重新設(shè)置一個存在的路徑,因為之前的路徑是不存在的 ? ? ? ? ctx.path = path.join('/node_modules', moduleName, pkg.module); ? ? ? ? // 執(zhí)行下一個中間件 ? ? ? ? awiat next(); ? ? } }) // 開啟靜態(tài)文件服務(wù)器 app.use(async (ctx, next) => { ? ? // 加載靜態(tài)文件 ? ? await send(ctx, ctx.path, { root: process.cwd(), index: 'index.html'}) ? ? await next() }) // 處理單文件組件 app.use(async (ctx, next) => { ? ? if (ctx.path.endsWith('.vue')) { ? ? ? ? // 獲取響應(yīng)文件內(nèi)容,轉(zhuǎn)換成字符串 ? ? ? ? const contents = await streamToString(ctx.body); ? ? ? ? // 編譯文件內(nèi)容 ? ? ? ? const { descriptor } = compilerSFC.parse(contents); ? ? ? ? // 定義狀態(tài)碼 ? ? ? ? let code; ? ? ? ? // 不存在type就是第一次請求 ? ? ? ? if (!ctx.query.type) { ? ? ? ? ? ? code = descriptor.script.content; ? ? ? ? ? ? // 這里的code格式是, 需要改造成我們前面貼出來的vite中的樣子 ? ? ? ? ? ? // import Hello from './components/Hello.vue' ? ? ? ? ? ? // export default { ? ? ? ? ? ? // ? ? ?name: 'App', ? ? ? ? ? ? // ? ? ?components: { ? ? ? ? ? ? // ? ? ? ? ?Hello ? ? ? ? ? ? // ? ? ?} ? ? ? ? ? ? // ?} ? ? ? ? ? ? // 改造code的格式,將export default 替換為const __script = ? ? ? ? ? ? code = code.relace(/export\s+default\s+/g, 'const __script = ') ? ? ? ? ? ? code += ` ? ? ? ? ? ? ? ? import { render as __render } from '${ctx.path}?type=template' ? ? ? ? ? ? ? ? __script.rener = __render ? ? ? ? ? ? ? ? export default __script ? ? ? ? ? ? ` ? ? ? ? } else if (ctx.query.type === 'template') { ? ? ? ? ? ? // 獲取編譯后的對象 code就是render函數(shù) ? ? ? ? ? ? const templateRender = compilerSFC.compileTemplate({ source: descriptor.template.content }) ? ? ? ? ? ? // 將render函數(shù)賦值給code返回給瀏覽器 ? ? ? ? ? ? code = templateRender.code ? ? ? ? } ? ? ? ? // 設(shè)置瀏覽器響應(yīng)頭為js ? ? ? ? ctx.type = 'application/javascript' ? ? ? ? // 將字符串轉(zhuǎn)換成數(shù)據(jù)流傳給下一個中間件。 ? ? ? ? ctx.body = stringToStream(code); ? ? } ? ? await next() }) // 修改第三方模塊路徑 app.use(async (ctx, next) => { ? ? if (ctx.type === 'application/javascript') { ? ? ? ? const contents = await stream2string(ctx.body); ? ? ? ? // 將body中導入的路徑修改一下,重新賦值給body返回給瀏覽器 ? ? ? ? // import vue from 'vue', 匹配到from '修改為from '@modules/ ? ? ? ? ctx.body = contents.replace(/(from\s+['"])(?![\.\/])/g, '$1/@modules/').replace(/process\.env\.NODE_ENV/g, '"development"'); ? ? } }) app.listen(5000) console.log('服務(wù)器已經(jīng)啟動 http://localhost:5000')
到此這篇關(guān)于學習Vite的原理的文章就介紹到這了,更多相關(guān)Vite原理內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
在小程序/mpvue中使用flyio發(fā)起網(wǎng)絡(luò)請求的方法
這篇文章主要介紹了在小程序/mpvue中使用flyio發(fā)起網(wǎng)絡(luò)請求的方法,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-09-09vue雙向數(shù)據(jù)綁定原理分析、vue2和vue3原理的不同點
這篇文章主要介紹了vue雙向數(shù)據(jù)綁定原理分析、vue2和vue3原理的不同點,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-12-12vue開發(fā)調(diào)試神器vue-devtools使用詳解
這篇文章主要為大家詳細介紹了vue開發(fā)調(diào)試神器vue-devtools的使用方法,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-07-07詳解Vue自定義指令如何實現(xiàn)處理圖片加載失敗的碎圖
這篇文章主要介紹了詳解Vue自定義指令如何實現(xiàn)處理圖片加載失敗的碎圖,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習吧2023-02-02Vue實現(xiàn)數(shù)字輸入框中分割手機號碼的示例
本篇文章主要介紹了Vue實現(xiàn)數(shù)字輸入框中分割手機號碼的示例,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-10-10