vite原理及插件分析

2,157 阅读9分钟

一种新的、更快的web开发工具 官方文档

vite 简介

  1. Vite 是一个面向现代浏览器的更轻, 更快的 web 应用开发工具
  2. 它基于 ECMAScript 标准原生模块系统 (ES Modules) 实现
  3. 它的出现是为了解决 webpack 在开发阶段使用 webpack-dev-server 冷启动时间过长, 另外, webpack-hmr 热更新反应速度慢的问题(webpack项目越大越慢)

为什么选择vite

  1. 解决 webpack 在开发阶段使用 webpack-dev-server 冷启动时间过长, 另外, webpack-hmr 热更新反应速度慢的问题
  2. 使用,上手简单
  3. 便于扩展(虽然是新的工具,但是可直接使用rollup的生态,rollup已经相对成熟,并且在某些方面打包比webpack有优势)

为什么vite会那么快

  1. 底层实现上,vite是基于esbuild 预构建依赖的。

    esbuild 使用 go 编写,并且比以 js 编写的打包器预构建依赖, 快 10 - 100 倍。

    因为 js 跟 go 相比实在是太慢了,js 的一般操作都是毫秒计,go 则是纳秒。

  1. 和webpack相比,启动方式也是有区别的

    webpack 原理图:

image-20200929144416064

vite原理图:

image-20200929144957808

  • webpack会先打包,然后启动开发服务器,请求服务器时直接给予打包结果
  • 由于vite在启动的时候不需要打包,也就意味着不需要分析模块的依赖、不需要编译,因此启动速度非常快。当浏览器请求某个模块时,再根据需要对模块内容进行编译。这种按需动态编译的方式,极大的缩减了编译时间,项目越复杂、模块越多,vite的优势越明显
  • 由于现代浏览器本身就支持ES Module,会自动向依赖的Module发出请求。vite充分利用这一点,将开发环境下的模块文件,就作为浏览器要执行的文件,而不是像webpack那样进行打包合并。
  • vite是按需加载,webpack是全部加载:在HMR(热更新)方面,当改动了一个模块后,vite仅需让浏览器重新请求该模块即可,不像webpack那样需要把该模块的相关依赖模块全部编译一次,效率更高。
  • vite的优势在开发环境:当需要打包到生产环境时,vite使用传统的rollup(也可以自己手动安装webpack来)进行打包,因此,vite的主要优势在开发阶段。另外,由于vite利用的是ES Module,因此在代码中(除了vite.config.js里面,这里是node的执行环境)不可以使用CommonJS

vite原理

Vite利用了浏览器native ES module imports特性,使用ES方式组织代码,浏览器自动请求需要的文件,并在服务端按需编译返回,完全跳过了打包过程。关键变化是index.html中的入口文件导入方式

image.png

这样main.js中就可以使用ES6 Module方式组织代码:

image.png

vite需要根据请求资源类型做不同解析工作,比如App.vue,返回给用户的内容如下(返回的是js代码,这里是经过sfc解析返回的): image.png

手写简易vite实现:

手撸Vite,揭开Vite神秘面纱基于这篇文章自己尝试写了下简易版vite

vite基础应用

  1. 创建vite项目

    使用NPM:

    npm init vite
    

    使用YARN:

    yarn create vite
    
  2. 选择生成的框架
    • vanilla
    • vue
    • react
    • preact
    • lit
    • svelte

    注意,这里选择的vue是指vue3.0,暂不支持直接生成vue2项目,但是有现成的模板可以下载

    github.com/lstoeferle/…

  3. index.html 与项目根目录

    在一个 Vite 项目中,index.html 在项目最外层而不是在 public 文件夹内。这是有意而为之的:在开发期间 Vite 是一个服务器,而 index.html 是该 Vite 项目的入口文件。

  4. 配置路径别名
    resolve: {
         alias: {
           "@": "/src",
         },
       },
    
    
  5. vite中使用css的各种功能

    @import 内联和变基#

    Vite 通过 postcss-import 预配置支持了 CSS @import 内联,Vite 的路径别名也遵从 CSS @import。换句话说,所有 CSS url() 引用,即使导入的文件在不同的目录中,也总是自动变基,以确保正确性。

    @import url('/src/styles/test.scss');
    

    关于在项目中配置全局scss

    css:{
        preprocessorOptions: { 
          scss: {
            // additionalData: `./src/styles/var.scss";`,
            additionalData: [
              '@import "@/styles/var.scss";',
            ]
          }
        }
      }
    
  6. vite 中使用typescript

    Vite 天然支持引入 .ts 文件

    Vite 仅执行 .ts 文件的转译工作,并 执行任何类型检查。并假设类型检查已经被你的 IDE 或构建过程接管了(你可以在构建脚本中运行 tsc --noEmit 或者安装 vue-tsc 然后运行 vue-tsc --noEmit 来对你的 *.vue 文件做类型检查)。

    yarn add vue-tsc
    

    在package.json

    "build": "vue-tsc --noEmit && vite build",
    
  7. 项目中使用tsx,jsx

    安装插件yarn add @vitejs/plugin-vue-jsx -D vite.config.ts中引入即可

    import vueJsx from "@vitejs/plugin-vue-jsx";
    export default defineConfig({
      plugins: [vueJsx()]
     })
    

vite高级应用

  1. 热更新功能(如何实现热更新)

    官方文档

    Vite 提供了一套原生 ESM 的 HMR API。 具有 HMR 功能的框架可以利用该 API 提供即时、准确的更新,而无需重新加载页面或清除应用程序状态。

    注意,你不需要手动设置这些 —— 当你通过 create-vite 创建应用程序时,所选模板已经为你预先配置了这些。

    Vite 通过特殊的 import.meta.hot 对象暴露手动 HMR API。来实现热更新

    首先,请确保用一个条件语句守护所有 HMR API 的使用,这样代码就可以在生产环境中被 tree-shaking 优化:

    if (import.meta.hot) {
      // HMR 代码
    }
    

    hot.accept(cb)

    要接收模块自身,应使用 import.meta.hot.accept,参数为接收已更新模块的回调函数:

    export const count = 1if (import.meta.hot) {
      import.meta.hot.accept((newModule) => {
        console.log('updated: count is now ', newModule.count)
      })
    }
    

    Vite 的 HMR 实际上并不替换最初导入的模块:如果 HMR 边界模块从某个依赖重新导出其导入,则它应负责更新这些重新导出的模块(这些导出必须使用 let)。此外,从边界模块向上的导入者将不会收到更新。

    示例

    先生成一个纯净的vite项目(纯净的vite项目并不含有热更新功能)

    修改main.js文件如下

    import './style.css'
    ​
    ​
    export function rander() {
      document.querySelector('#app').innerHTML = `
        <h1>Hello Vite!</h1>
        <a href="https://vitejs.dev/guide/features.html" target="_blank">sssss</a>
      `
    }
    rander()
    // Vite 通过特殊的 import.meta.hot 对象暴露手动 HMR API。
    if(import.meta.hot){
      import.meta.hot.accept((newModule) => {
        newModule.rander()
      })
    }
    

    打开浏览器观察

    通过上面的了解,大概知道了热更新的逻辑与原理。

    Vite实现热更新,主要是通过创建WebSocket建立浏览器与服务器建立通信,通过监听文件的改变像客户端发出消息,客户端对应不同的文件进行不同的操作的更新

  2. glob-import 批量导入功能

    require context 是 webpack 提供的特有的模块方法,并不是语言标准,所以在 vite 中不再能使用 require context。但如果完全改为开发者手动 import 模块,一来是对已有代码改动容易产生模块导入的遗漏;二来是放弃了这种「灵活」的机制,对后续的开发模式也会有一定改变。但好在 vite2.0 提供了 glob 模式的模块导入。该功能可以实现上述目标。当然,会需要做一定的代码改动:

    webpack中批量引入:

    // 举例,批量导入全局组件
    const globJs = require.context('./globle/', true, /.js$/)
    console.log(globJs);
    ​
    globJs.keys().forEach(key => {
      console.log(key)
      console.log(globJs(key).default)
      const component = globJs(key).default
       app.component(component.name, component)
    })
    ​
    

    vite中批量

    使用glob

    const globModules = import.meta.glob('./glob/*') // 参数可为正则表达式 例如 './glob/*-[0-9].ts'
    console.log(globModules);
    Object.entries(globModules).forEach(([k,v])=>{
      console.log(k+':', v);
      // 使用导入模块的方法
      v().then(m=>{
        console.log(k+':'+ m.default);
        m.test()
      })
      
    })
    

    使用globEager

    const globModules = import.meta.globEager('./glob/*') // 参数可为正则表达式 例如 './glob/*-[0-9].ts'
    console.log(globModules);
    Object.entries(globModules).forEach(([k,v])=>{
      console.log(k+':', v);
      v.test()
     
    })
    

vite插件开发

  1. vite插件是什么

    使用Vite插件可以扩展Vite能力,比如解析用户自定义的文件输入,在打包代码前转译代码,或者查找第三方模块。

    img

    插件调用顺序

    • 别名处理Alias
    • 用户插件设置enforce: 'pre'
    • Vite核心插件
    • 用户插件未设置enforce
    • Vite构建插件
    • 用户插件设置enforce: 'post'
    • Vite构建后置插件(minify, manifest, reporting)

    img 示例:新建test-plugin.js,写入测试代码

export default (enforce) => {
    return {
        name: 'test',
        enforce,
        buildStart() {
            console.log('buildStart', enforce);
        },
        resolveId() {
            console.log('resolveId', enforce);
        }
    }
}

在vite.config.js引入插件

​
import testPlugin from "./src/plugins/test-plugin";
​
// https://vitejs.dev/config/
export default defineConfig({
    plugins: [
        vue(),
        testPlugin('post'),
        testPlugin(),
        testPlugin('pre'),
    ]
})

执行结果

![image.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/28894cac327b46cfae8e87d3ff0c34eb~tplv-k3u1fbpfcp-zoom-1.image)

2. ##### vite插件的形式

`Vite`插件是一个**拥有名称**、**创建钩子**(build hook)或**生成钩子**(output generate hook)**的对象**。

```
export default {
  name: 'my-vite-plugin',
  resolveId(id) {},
  load(id) {},
  transform(code) {}
}
```

如何插件含有配置功能,他的形式应该是一个接受插件选项,**返回插件对象的函数**

```
// options为配置选项
export default function (options) {
  return {
    name: 'my-vite-plugin',
    resolveId(id){},
    load(id){},
    transform(code){},
  }
}
```

0. ##### 插件钩子(类似于vue生命周期)

开发时,`Vite dev server`创建一个插件容器按照`Rollup`调用创建钩子的规则请求各个钩子函数。

下面的钩子会在服务启动时调用一次(文件更新也不会调用)

-   options:替换或操纵`rollup`选项
-   buildStart:开始创建

vite特有的钩子

-   config: 修改Vite配置
-   configResolved:Vite配置确认
-   configureServer:用于配置dev server,可以进行中间件操作
-   transformIndexHtml:用于转换宿主页
-   handleHotUpdate:自定义HMR更新时调用

下面钩子每次有模块请求时都会被调用:(核心hook)

-   resolveId:创建自定义确认函数,常用语定位第三方依赖(找到对应的文件)
-   load:创建自定义加载函数,可用于返回自定义的内容(加载文件源码)
-   transform:可用于转换已加载的模块内容(转变源码为需要的代码)

下面钩子会在服务器关闭时调用一次:

-   buildEnd:
-   closeBundle

##### 范例:钩子调用顺序测试

```
export default function myExample () {
    // 返回的是插件对象
    return {
        name: 'hooks-order', 
        // 初始化hooks,只走一次
        options(opts) {
            console.log('options');
        },
        buildStart() {
            console.log('buildStart');
        },
        // vite特有钩子
        config(config) {
            console.log('config');
            return {}
        },
        configResolved(resolvedCofnig) {
            console.log('configResolved');
        },
        configureServer(server) {
            console.log('configureServer');
            // server.app.use((req, res, next) => {
            //   // custom handle request...
            // })
        },
        transformIndexHtml(html) {
            console.log('transformIndexHtml');
            return html
            // return html.replace(
            //   /<title>(.*?)</title>/,
            //   `<title>Title replaced!</title>`
            // )
        },
        // 通用钩子
        resolveId(source) {
            console.log(resolveId)
            if (source === 'virtual-module') {
                console.log('resolvedId');
                return source; 
            }
            return null; 
        },
        load(id) {
            console.log('load');
                
            if (id === 'virtual-module') {
                return 'export default "This is virtual!"';
            }
            return null;
        },
        transform(code, id) {
            console.log('transform');
            if (id === 'virtual-module') {
            }
            return code
        },
    };
  }
```

执行结果

```
config
configResolved
options
configureServer
buildStart
transformIndexHtml
load
load
transform
transform
```

实战部分敬请期待