Vite的原理介紹及應用

背景

在當今Webpack橫行的時代,Webpack的影響力不可謂之不大。對于一個主流Web項目的開發(fā)而言,大多數(shù)時候我們都會采用現(xiàn)有的腳手架作為項目開發(fā),打包工具如:Vue-cli、create-react-app,而他們都基于Webpack。但是,在不斷的使用和日常項目的迭代中,我們慢慢會走入一個窘境,就會出現(xiàn)我們稍微改動一行代碼我們就需要等待十幾秒甚至是數(shù)十秒的情況,這對于我們?nèi)找嬖鲩L的業(yè)務開發(fā)來說是十分不友好的。
深入Webpack打包原理我們可以清晰的知道他的編譯過程是靜態(tài)的,也就是說他會把所有可能用到的代碼全部進行打包構(gòu)建,會借助膠水代碼用來組裝各模塊,這樣打包出來的代碼是十分龐大的,很多時候其實我們在開發(fā)過程中并不需要全部代碼的功能,而是一小部分,這個時候大量的構(gòu)建時間都是多余的,我們需要一個能夠真正意義上實現(xiàn)懶加載的開發(fā)工具。

Vite是什么?

定義

Vite(輕量,輕快)vite是一個基于vue3單文件組件的非打包開發(fā)服務器。它做到了本地快速開發(fā)啟動,實現(xiàn)按需編譯,不再等待整個應用編譯完成。
面向現(xiàn)代化瀏覽器,基于原生模塊系統(tǒng) ES moudle 的開發(fā)服務器,在服務器端按需編譯返回,完全跳過了打包這個概念,服務器隨起隨用。
瀏覽器端使用 export、import 的方式導入和導出模塊,在 script 標簽里設置 type="module",瀏覽器會識別所有添加了type='module'的script標簽,對于該標簽中的import關(guān)鍵字,瀏覽器會發(fā)起http請求獲取模塊內(nèi)容。

它主要具有以下特點:

  • 快速的冷啟動
  • 即時的模塊熱更新
  • 真正的按需編譯

基本架構(gòu)

啟動項目時,則是啟動一個koa服務器,服務器攔截瀏覽器的es module的請求,服務器直接將 ESM 模塊內(nèi)容處理后,通過 path 找到目錄下對應的文件做一定的處理最終以 ES Modules 格式返回給客戶端。接著,現(xiàn)代瀏覽器通過解析 script module,對每一個 import 到的模塊進行 HTTP 請求,服務器繼續(xù)對這些 HTTP 請求進行處理并響應。(熱更新)


image.png

圖中的目標項目即我們開發(fā)時的項目,vite服務在解析模塊路徑以及讀取文件內(nèi)容時需要訪問目標項目中的模塊內(nèi)容或者配置文件等。

Vite的原理介紹

  • 使用vite后項目分析
它的核心在于使用了es的語法,我們可以看到index.html文件,和以前不一樣的地方在于,它使用了:
  <script type="module" src="/src/main.js"></script>
// 這樣引入的話就表示它是一個模塊,那這個script里面就可以使用import
//默認引入的是src下的main.js
image.png

從其他請求中我們也可以看出每一個.vue文件都被拆分成了多個請求,并通過type來標識是template還是style。


image.png
  • 結(jié)論

vite在這里做了兩件事,第一是修改了模塊請求路徑,第二就是將.vue文件進行解析拆分。(只是本文會進行詳細講解的有關(guān)于Vite實現(xiàn)的部分,而不是說Vite只干了這兩件事 Vite的功能還是十分強大的)

深入源碼講原理

1、創(chuàng)建自己的vite工具 my-vite

項目目錄概述:

  • my-vite
    -bin
    。www.js
    -node_modules
    -plugins
    -index.js
    -package.json

在項目根目錄創(chuàng)建bin目錄,并在bin目錄下創(chuàng)建一個www.js文件,文件內(nèi)容如下:

! /usr/bin/env node
// 執(zhí)行的入口
require('../index');

2、創(chuàng)建服務

在創(chuàng)建www.js文件中引入了index.js文件,主要是使用了koa啟動了一個簡單服務以及實現(xiàn)vite的功能

const Koa = require('koa')
const { moduleRewritePlugin } = require('./plugins/serverPluginModuleRewrite')
const serveStaticPlugin = require('./plugins/serverPluginServeStatic')
const { moduleResolvePlugin } = require('./plugins/serverPluginModuleResolve')
const { vuePlugin } = require('./plugins/serverPluginVue')

function createServer() {
    let app = new Koa();
    const context = { //直接創(chuàng)建一個上下文,來給不同的插件共享功能
        app,
        root: process.cwd()     
     }
    const resolvePlugin = [
        moduleRewritePlugin, //2.重寫我們的請求路徑, 重寫后瀏覽器會再次發(fā)送請求
        moduleResolvePlugin,//解析模塊路徑,服務端來解析模塊真實位置
        vuePlugin, // 解析.vue文件
        serveStaticPlugin //1.靜態(tài)服務插件,處理所需要處理的靜態(tài)文件    
    ]
    resolvePlugin.forEach(plugin => plugin(context))
    return app;
}
createServer().listen(4000, () => {
    console.log('vite start 4000')
})

3、靜態(tài)服務插件

serverPluginServeStatic.js文件:使用koa-static中間件來托管靜態(tài)資源,同時我們需要拿到koa實例(app),其次需要獲取到目標項目的根目錄路徑(root),將目標項目進行整體托管,同時對于目標項目的 public目錄也進行托管,這樣,我們需要處理的靜態(tài)文件基本完成了。

const static = require('koa-static')
const path = require('path')

function serveStaticPlugin({ app, root }) {

    app.use(static(root)) // root指的是根目錄 訪問根目錄下的index.html
    app.use(static(path.resolve(root, 'public'))) //可以直接http://localhost:4000/文件名,訪問public下的文件,比如public文件下有aa.txt,則直接訪問http://localhost:4000/aa.txt
}

module.exports = serveStaticPlugin

4、重寫模塊路徑插件

為什么要重寫模塊路徑呢?
這是因為我們在使用import方式導入模塊的時候,瀏覽器只能識別./、../、/這種開頭的路徑,對于直接使用模塊名比如:import vue from 'vue',瀏覽器就會報錯,因為它無法識別這種路徑,這就是我們需要進行處理的地方了。

image.png

serverPluginModuleRewrite.js文件:

image.png

image.png

5、解析模塊路徑插件

在處理完所有的模塊路徑之后,我們就需要在服務端來解析模塊真實位置。

const reg = /^\/@modules\//
const fs = require('fs').promises
const path = require('path')
 
function moduleResolvePlugin({ app, root }) {
    app.use(async(ctx, next) => {
        // 如果沒有匹配到 /@modules就往下執(zhí)行即可
        if (!reg.test(ctx.path)) {
            return next();
        }
        const id = ctx.path.replace(reg, '');
        //console.log("id-->", id)  //vue
        let mapping = {
                vue: path.resolve(root, 'node_modules', '@vue/runtime-dom/dist/runtime-dom.esm-browser.js')
            }
            //如果是第三方模塊,則可以根據(jù)package.json進行查找
        const content = await fs.readFile(mapping[id], 'utf8')
        ctx.type = 'js' //返回的文件為js
        ctx.body = content
    })
}

exports.moduleResolvePlugin =  moduleResolvePlugin

6、解析.vue文件

當 Vite 遇到一個 .vue 后綴的文件時。由于 .vue 模板文件的特殊性,它被拆分成 template , css ,script 模塊三個模塊進行分別處理。最后會對 script ,template, css 發(fā)送多個請求獲取。


image.png

7、總結(jié)

?在 koa 中間件里獲取請求 body
?通過 es-module-lexer 解析資源 ast 拿到 import 的內(nèi)容
?判斷 import 的資源是否是 npm 模塊
?返回處理后的資源路徑:"vue" => "/@modules/vue"
?將處理的template,script,style等所需的依賴以http請求的形式,通過query參數(shù)形式區(qū)分并加載SFC文件各個模塊內(nèi)容。

Webpack & Vite 對比

  • 冷啟動對比


    image.png

    從左到右依次是: vue-cli3 + vue3 的demo, vite 1.0.0-rc + vue 3的demo, vue-cli3 + vue2的demo。在這個 gif 中已經(jīng)可以明顯感受到 vite 的優(yōu)勢了。vue-cli3 啟動Vue2大概需要5s左右,vue-cli3 啟動Vue3需要4s左右,而vite 只需要1s 左右的時間。

1.當我們對比使用 vue-cli-service serve 的時候,會有明顯感覺。因為 Webpack Dev Server 在啟動時,需要先 build—遍,而 build 的過程是需要耗費很多時間的。
2.而 Vite 則完全不同,當我們執(zhí)行 Vite serve 時(npm run dev),內(nèi)部直接啟動了 Web Server,并不會先編譯所有的代碼文件。那僅僅是啟動 Web Server,那么及時請求的編譯呢?關(guān)于支持 JSX, TSX,Typescript 編譯到原生 JS —— Vite 引入了EsBuild,是使用 Go 寫的,直接編譯為 Native 代碼,性能要比 TSC 好二三十倍, 當然也會用上緩存。

  • 即時的熱模塊更新

1.熱更新的時候,Vite 只需要立即編譯當前所修改的文件即可,所以 響應速度非???。
2.而 Webpack 修改某個文件過后,會自動以這個文件為入口重寫 build—次,所有的涉及到的依賴也都會被加載一遍,所以反應速度會慢很多。

  • 真正的按需編譯

1.Webpack 這類工具的做法是將所有模塊提前編譯、打包進 bundle 里,換句話說,不管模塊是否會被執(zhí)行,都要被編譯和打包到 bundle 里。隨著項目越來越大打包后的 bundle 也越來越大,打包的速度自然也就越來越慢。
2.Vite 利用現(xiàn)代瀏覽器原生支持 ESM 特性,省略了對模塊的打包。對于需要編譯的文件,Vite 采用的是另外一種模式:即時編譯。也就是說,只有具體去請求某個文件時才會編譯這個文件。所以,這種「即時編譯」的好處主要體現(xiàn)在:按需編譯。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容