Vite的原理介绍及应用

背景

在当今Webpack横行的时代,Webpack的影响力不可谓之不大。对于一个主流Web项目的开发而言,大多数时候我们都会采用现有的脚手架作为项目开发,打包工具如:Vue-cli、create-react-app,而他们都基于Webpack。但是,在不断的使用和日常项目的迭代中,我们慢慢会走入一个窘境,就会出现我们稍微改动一行代码我们就需要等待十几秒甚至是数十秒的情况,这对于我们日益增长的业务开发来说是十分不友好的。
深入Webpack打包原理我们可以清晰的知道他的编译过程是静态的,也就是说他会把所有可能用到的代码全部进行打包构建,会借助胶水代码用来组装各模块,这样打包出来的代码是十分庞大的,很多时候其实我们在开发过程中并不需要全部代码的功能,而是一小部分,这个时候大量的构建时间都是多余的,我们需要一个能够真正意义上实现懒加载的开发工具。

Vite是什么?

定义

Vite(轻量,轻快)vite是一个基于vue3单文件组件的非打包开发服务器。它做到了本地快速开发启动,实现按需编译,不再等待整个应用编译完成。
面向现代化浏览器,基于原生模块系统 ES moudle 的开发服务器,在服务器端按需编译返回,完全跳过了打包这个概念,服务器随起随用。
浏览器端使用 export、import 的方式导入和导出模块,在 script 标签里设置 type="module",浏览器会识别所有添加了type='module'的script标签,对于该标签中的import关键字,浏览器会发起http请求获取模块内容。

它主要具有以下特点:

  • 快速的冷启动
  • 即时的模块热更新
  • 真正的按需编译

基本架构

启动项目时,则是启动一个koa服务器,服务器拦截浏览器的es module的请求,服务器直接将 ESM 模块内容处理后,通过 path 找到目录下对应的文件做一定的处理最终以 ES Modules 格式返回给客户端。接着,现代浏览器通过解析 script module,对每一个 import 到的模块进行 HTTP 请求,服务器继续对这些 HTTP 请求进行处理并响应。(热更新)


image.png

图中的目标项目即我们开发时的项目,vite服务在解析模块路径以及读取文件内容时需要访问目标项目中的模块内容或者配置文件等。

Vite的原理介绍

  • 使用vite后项目分析
它的核心在于使用了es的语法,我们可以看到index.html文件,和以前不一样的地方在于,它使用了:
  
// 这样引入的话就表示它是一个模块,那这个script里面就可以使用import
//默认引入的是src下的main.js
image.png

从其他请求中我们也可以看出每一个.vue文件都被拆分成了多个请求,并通过type来标识是template还是style。


image.png
  • 结论

vite在这里做了两件事,第一是修改了模块请求路径,第二就是将.vue文件进行解析拆分。(只是本文会进行详细讲解的有关于Vite实现的部分,而不是说Vite只干了这两件事 Vite的功能还是十分强大的)

深入源码讲原理

1、创建自己的vite工具 my-vite

项目目录概述:

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

在项目根目录创建bin目录,并在bin目录下创建一个www.js文件,文件内容如下:

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

2、创建服务

在创建www.js文件中引入了index.js文件,主要是使用了koa启动了一个简单服务以及实现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 = { //直接创建一个上下文,来给不同的插件共享功能
        app,
        root: process.cwd()     
     }
    const resolvePlugin = [
        moduleRewritePlugin, //2.重写我们的请求路径, 重写后浏览器会再次发送请求
        moduleResolvePlugin,//解析模块路径,服务端来解析模块真实位置
        vuePlugin, // 解析.vue文件
        serveStaticPlugin //1.静态服务插件,处理所需要处理的静态文件    
    ]
    resolvePlugin.forEach(plugin => plugin(context))
    return app;
}
createServer().listen(4000, () => {
    console.log('vite start 4000')
})

3、静态服务插件

serverPluginServeStatic.js文件:使用koa-static中间件来托管静态资源,同时我们需要拿到koa实例(app),其次需要获取到目标项目的根目录路径(root),将目标项目进行整体托管,同时对于目标项目的 public目录也进行托管,这样,我们需要处理的静态文件基本完成了。

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就往下执行即可
        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')
            }
            //如果是第三方模块,则可以根据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 发送多个请求获取。


image.png

7、总结

在 koa 中间件里获取请求 body
通过 es-module-lexer 解析资源 ast 拿到 import 的内容
判断 import 的资源是否是 npm 模块
返回处理后的资源路径:"vue" => "/@modules/vue"
将处理的template,script,style等所需的依赖以http请求的形式,通过query参数形式区分并加载SFC文件各个模块内容。

Webpack & Vite 对比

  • 冷启动对比


    image.png

    从左到右依次是: vue-cli3 + vue3 的demo, vite 1.0.0-rc + vue 3的demo, vue-cli3 + vue2的demo。在这个 gif 中已经可以明显感受到 vite 的优势了。vue-cli3 启动Vue2大概需要5s左右,vue-cli3 启动Vue3需要4s左右,而vite 只需要1s 左右的时间。

1.当我们对比使用 vue-cli-service serve 的时候,会有明显感觉。因为 Webpack Dev Server 在启动时,需要先 build—遍,而 build 的过程是需要耗费很多时间的。
2.而 Vite 则完全不同,当我们执行 Vite serve 时(npm run dev),内部直接启动了 Web Server,并不会先编译所有的代码文件。那仅仅是启动 Web Server,那么及时请求的编译呢?关于支持 JSX, TSX,Typescript 编译到原生 JS —— Vite 引入了EsBuild,是使用 Go 写的,直接编译为 Native 代码,性能要比 TSC 好二三十倍, 当然也会用上缓存。

  • 即时的热模块更新

1.热更新的时候,Vite 只需要立即编译当前所修改的文件即可,所以 响应速度非常快。
2.而 Webpack 修改某个文件过后,会自动以这个文件为入口重写 build—次,所有的涉及到的依赖也都会被加载一遍,所以反应速度会慢很多。

  • 真正的按需编译

1.Webpack 这类工具的做法是将所有模块提前编译、打包进 bundle 里,换句话说,不管模块是否会被执行,都要被编译和打包到 bundle 里。随着项目越来越大打包后的 bundle 也越来越大,打包的速度自然也就越来越慢。
2.Vite 利用现代浏览器原生支持 ESM 特性,省略了对模块的打包。对于需要编译的文件,Vite 采用的是另外一种模式:即时编译。也就是说,只有具体去请求某个文件时才会编译这个文件。所以,这种「即时编译」的好处主要体现在:按需编译。

你可能感兴趣的:(Vite的原理介绍及应用)