<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
Vite是一個更輕、更快的web應用開發工具,面向現代瀏覽器。底層基於ECMAScript標準原生模組系統ES Module實現。他的出現是為了解決webpack冷啟動時間過長以及Webpack HMR
熱更新反應速度慢等問題。
預設情況下Vite建立的專案是一個普通的Vue3應用,相比基於Vue-cli建立的應用少了很多組態檔和依賴。
Vite
建立的專案所需要的開發依賴非常少,只有Vite
和@vue/compiler-sfc
。這裡面Vite是一個執行工具,compiler-sfc
則是為了編譯.vue結尾的單檔案元件。在建立專案的時候通過指定不同的模板也可以支援使用其他框架例如React。專案建立完成之後可以通過兩個命令啟動和打包。
# 開啟伺服器 vite serve # 打包 vite build
正是因為Vite
啟動的web服務不需要編譯打包,所以啟動的速度特別快,偵錯階段大部分執行的程式碼都是你在編輯器中書寫的程式碼,這相比於webpack的編譯後再呈現確實要快很多。當然生產環境還是需要打包的,畢竟很多時候我們使用的最新ES規範在瀏覽器中還沒有被支援,Vite的打包過程和webpack類似會將所有檔案進行編譯打包到一起。對於程式碼切割的需求Vite採用的是原生的動態匯入來實現的,所以打包結果只能支援現代瀏覽器,如果需要相容老版本瀏覽器可以引入Polyfill
。
使用Webpack打包除了因為瀏覽器環境並不支援模組化和新語法外,還有就是模組檔案會產生大量的http請求。如果你使用模組化的方式開發,一個頁面就會有十幾甚至幾十個模組,而且很多時候會出現幾kb的檔案,開啟一個頁面要載入幾十個js資源這顯然是不合理的。
首先實現一個能夠開啟靜態web伺服器的命令列工具。vite1.x內部使用的是Koa來實現靜態伺服器。(ps:node命令列工具可以檢視我之前的文章,這裡就不介紹了,直接貼程式碼)。
npm init npm install koa koa-send -D
工具bin的入口檔案設定為原生的index.js
#!/usr/bin/env node const Koa = require('koa') const send = require('koa-send') const app = new Koa() // 開啟靜態檔案伺服器 app.use(async (ctx, next) => { // 載入靜態檔案 await send(ctx, ctx.path, { root: process.cwd(), index: 'index.html'}) await next() }) app.listen(5000) console.log('伺服器已經啟動 http://localhost:5000')
這樣就編寫好了一個node
靜態伺服器的工具。
我們的做法是當程式碼中使用了第三方模組(node_modules
中的檔案),可以通過修改第三方模組的路徑給他一個標識,然後在伺服器中拿到這個標識來處理這個模組。
首先需要修改第三方模組的路徑,這裡需要一個新的中介軟體來實現。判斷一下當前返回給瀏覽器的檔案是否是javascript,只需要看響應頭中的content-type。如果是javascript
需要找到這個檔案中引入的模組路徑。ctx.body
就是返回給瀏覽器的內容檔案。這裡的資料是一個stream,需要轉換成字串來處理。
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(/(froms+['"])(?![./])/g, '$1/@modules/'); } })
接著開始載入第三方模組, 這裡同樣需要一箇中介軟體,判斷請求路徑是否是修改過的@module開頭,如果是的話就去node_modules裡面載入對應的模組返回給瀏覽器。這個中介軟體要放在靜態伺服器之前。
// 載入第三方模組 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賦值,需要重新設定一個存在的路徑,因為之前的路徑是不存在的 ctx.path = path.join('/node_modules', moduleName, pkg.module); // 執行下一個中介軟體 awiat next();
這樣瀏覽器請求進來的時候雖然是@modules路徑,但是在載入之前將path路徑修改為了node_modules
中的路徑,這樣在載入的時候就會去node_modules
中獲取檔案,將載入的內容響應給瀏覽器。
載入第三方模組:
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賦值,需要重新設定一個存在的路徑,因為之前的路徑是不存在的 ctx.path = path.join('/node_modules', moduleName, pkg.module); // 執行下一個中介軟體 awiat next(); } })
之前說過瀏覽器是沒辦法處理.vue資源的, 瀏覽器只能識別js、css等常用資源,所以其他型別的資源都需要在伺服器端處理。當請求單檔案元件的時候需要在伺服器將單檔案元件編譯成js模組返回給瀏覽器。
所以這裡當瀏覽器第一次請求App.vue的時候,伺服器會把單檔案元件編譯成一個物件,先載入這個元件,然後再建立一個物件。
import Hello from './src/components/Hello.vue' const __script = { name: "App", components: { Hello } }
接著再去載入入口檔案,這次會告訴伺服器編譯一下這個單檔案元件的模板,返回一個render函數。然後將render函數掛載到剛建立的元件選項物件上,最後匯出選項物件。
import { render as __render } from '/src/App.vue?type=template' __script.render = __render __script.__hmrId = '/src/App.vue' export default __script
也就是說vite
會傳送兩次請求,第一次請求會編譯單檔案檔案,第二次請求是編譯單檔案模板返回一個render
函數。
編譯單檔案選項:
首先來實現一下第一次請求單檔案的情況。需要把單檔案元件編譯成一個選項,這裡同樣用一箇中介軟體來實現。這個功能要在處理靜態伺服器之後,處理第三方模組路徑之前。
首先需要對單檔案元件進行編譯需要藉助compiler-sfc
。
// 處理單檔案元件 app.use(async (ctx, next) => { if (ctx.path.endsWith('.vue')) { // 獲取響應檔案內容,轉換成字串 const contents = await streamToString(ctx.body); // 編譯檔案內容 const { descriptor } = compilerSFC.parse(contents); // 定義狀態碼 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(/exports+defaults+/g, 'const __script = ') code += ` import { render as __render } from '${ctx.path}?type=template' __script.rener = __render export default __script ` } // 設定瀏覽器響應頭為js ctx.type = 'application/javascript' // 將字串轉換成資料流傳給下一個中介軟體。 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
引數,需要將單檔案元件模板編譯成render函數。
首先需要判斷當前請求中有沒有type=template
。
if (!ctx.query.type) { ... } else if (ctx.query.type === 'template') { // 獲取編譯後的物件 code就是render函數 const templateRender = compilerSFC.compileTemplate({ source: descriptor.template.content }) // 將render函數賦值給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(/(froms+['"])(?![./])/g, '$1/@modules/').replace(/process.env.NODE_ENV/g, '"development"'); } })
至此就實現了一個簡版的vite,當然這裡我們只演示了.vue檔案,對於css,less等其他資源都沒有處理,不過方法都是類似的,感興趣的同學可以自行實現。
#!/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賦值,需要重新設定一個存在的路徑,因為之前的路徑是不存在的 ctx.path = path.join('/node_modules', moduleName, pkg.module); // 執行下一個中介軟體 awiat next(); } }) // 開啟靜態檔案伺服器 app.use(async (ctx, next) => { // 載入靜態檔案 await send(ctx, ctx.path, { root: process.cwd(), index: 'index.html'}) await next() }) // 處理單檔案元件 app.use(async (ctx, next) => { if (ctx.path.endsWith('.vue')) { // 獲取響應檔案內容,轉換成字串 const contents = await streamToString(ctx.body); // 編譯檔案內容 const { descriptor } = compilerSFC.parse(contents); // 定義狀態碼 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(/exports+defaults+/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函數 const templateRender = compilerSFC.compileTemplate({ source: descriptor.template.content }) // 將render函數賦值給code返回給瀏覽器 code = templateRender.code } // 設定瀏覽器響應頭為js ctx.type = 'application/javascript' // 將字串轉換成資料流傳給下一個中介軟體。 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(/(froms+['"])(?![./])/g, '$1/@modules/').replace(/process.env.NODE_ENV/g, '"development"'); } }) app.listen(5000) console.log('伺服器已經啟動 http://localhost:5000')
到此這篇關於學習Vite的原理的文章就介紹到這了,更多相關Vite原理內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45