花十分鐘來了解下Vite原理吧

Vite是一個面向現代瀏覽器的一個更輕,更快的web應用開發工具,他基於ECMAScript標準原生模塊系統ES Module實現。javascript

他的出現是爲了解決webpack冷啓動時間過長,另外Webpack HMR熱更新反應速度慢的問題。css

使用Vite建立的項目就是一個普通的Vue3應用,相比基於Vue-cli建立的應用少了不少配置文件和依賴。Vite建立的項目開發依賴很是簡單,只有Vite和@vue/compiler-sfc, Vite是一個運行工具,compiler-sfc是爲了編譯.vue結尾的單文件組件。html

Vite目前默認僅支持Vue3.0的版本,在建立項目的時候經過制定不一樣的模板也支持使用其餘框架好比React,Vite提供了兩個子命令。vue

# 開啓服務器
vite serve
# 打包
vite build
複製代碼

開啓服務的時候不須要打包,因此啓動速度特別快。在生產環境打包和webpack相似會將全部文件進行編譯打包到一塊兒。對於代碼切割的需求Vite採用的是原生的動態導入來實現的,因此打包結果只能支持現代瀏覽器,若是老版本瀏覽器須要使用能夠引入Polyfill。java

以前咱們使用Webpack打包是由於瀏覽器環境並不支持模塊化,還有就是模塊文件會產生大量的http請求。在現代瀏覽器模塊化已經被支持了,http2也解決了多文件請求的問題。固然若是你的應用須要支持IE瀏覽器,那麼仍是須要打包的。由於IE並不支持ES Module。node

Vite建立的項目幾乎不須要額外的配置,默認支持TS、Less, Sass,Stylus,postcss等,可是須要單獨安裝對應的編譯器。同時還支持jsx和web assembly。webpack

Vite帶來的好處是提高開發者在開發過程當中的體驗,web開發服務器不須要等待能夠當即啓動,模塊熱更新幾乎是實時的,所需的文件按需編譯,避免編譯用不到的文件,開箱即用,避免loader及plugins的配置。web

Vite的核心功能包括開啓一個靜態的web服務器,而且可以編譯單文件組件,而且提供HMR功能。npm

當啓動vite的時候首先會將當前項目目錄做爲靜態服務器的根目錄,靜態服務器會攔截部分請求,當請求單文件的時候會實時編譯,以及處理其餘瀏覽器不能識別的模塊,經過websocket實現hmr。json

咱們本身來實現一下這個功能從而來學習其實現原理。

搭建靜態測試服務器

咱們首先實現一個可以開啓靜態web服務器的命令行工具。vite內部使用的是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靜態服務器的工具。

處理第三方模塊

咱們的作法是當代碼中使用了第三方模塊,咱們能夠經過修改第三方模塊的路徑,給他一個標識,而後再服務器中拿到這個標識來處理這個模塊。

首先咱們須要修改第三方模塊的路徑,這裏咱們須要一個新的中間件來實現。

須要判斷一下當前返回給瀏覽器的文件是不是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(/(from\s+['"])(?![\.\/])/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.rener = __render
__script.__hmrId = '/src/App.vue'
export default __script
複製代碼

也就是說vite會發送兩次請求,第一次請求會編譯單文件文件,第二次請求是編譯單文件模板返回一個render函數。

  1. 編譯單文件選項

咱們首先來實現一下第一次請求單文件的狀況。須要把單文件組件編譯成一個選項,這裏一樣用一箇中間件來實現。這個功能要在處理靜態服務器只有,處理第三方模塊路徑以前。

咱們首先須要對單文件組件進行編譯。這裏須要藉助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(/export\s+default\s+/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(/(from\s+['"])(?![\.\/])/g, '$1/@modules/').replace(/process\.env\.NODE_ENV/g, '"development"');
    }
})

複製代碼

至此咱們就實現了一個簡版的vite,固然這裏咱們只演示了.vue文件,對於css,less,其餘資源都沒有處理,不過方法都是相似的,感興趣的同窗能夠自行實現。HRM也沒有實現。

#!/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(/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函數 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(/(from\s+['"])(?![\.\/])/g, '$1/@modules/').replace(/process\.env\.NODE_ENV/g, '"development"'); } }) app.listen(5000) console.log('服務器已經啓動 http://localhost:5000') 複製代碼
相關文章
相關標籤/搜索