vite 原理模拟实现

812 阅读3分钟

Vite 概念

  • Vite 是一个面向现代化浏览器的一个更轻、更快得Web应用开发工具
  • 基于ECAMAscript标准的原生模块系统EsModule 实现的
  • 它的出现是为了解决webpack冷启动和热更新慢的问题

基础使用

  • vite serve 开启一个用于开发的web 服务器,在启动的时候不需要编译所有代码文件,启动速度非常的快

  • vite build

Vite PK Webpack

从上图中可知, 使用vite的情况下,当浏览器请求过来时,由vite 在服务器端来对Vue单文件进行编译,最终将编译的结果返回给客户端。

再对比vue-cli-service 创建的服务,使用webpack打包:

首先要借助webpack对多有的文件进行build打包编译,然后要将结果存储在 内存中,当浏览器请求的时候,直接将内存中的返回给浏览器。随着项目变得庞大后,内存的消耗也会变得更多

HMR

  • Vite HMR 会立即编译更新的文件
  • Webpack HMR 会自动 以更新的文件为入口,重新build一次,这个文件涉及到的依赖也会被重新加载一遍

Vite 特性

  • 快速冷启动
  • 模块热更新
  • 按需编译
  • 开箱即用
    • 内置支持TypeScript
    • 内置支持 less/sass/stylus/postcss, 但需要安装对应的编译器
    • JSX
    • Web Assembly

Vite 实现原理

Vite 核心功能:

  • 静态Web服务器
  • 编译单文件组件
    • 拦截浏览器不识别的模块, 并处理
  • HMR

静态Web服务器

创建一个项目, 初始化package.json

npm init -y

安装koa相关依赖

npm install koa koa-send -D

packge.json 配置默认执行的文件

"bin": "index.js"

准备就绪后,开始编写vite-cli 脚本代码:

// 配置运行node 的位置
#!/usr/bin/env node
const Koa = require('koa')
const send = require('koa-send')
const app = new Koa()
// 1. 静态文件服务器
app.use(async (ctx, next) => {
  await send(ctx, ctx.path, { root: process.cwd(), index: 'index.html' })
  await next()
})
app.listen(3000)
console.log('Server running @ http://localhost:3000')

接着,我们要将vite-cli link 到全局,打开终端,输入

npm link

运行结果:

通过vite脚手架方式 创建一个Vue3 项目,参考链接

在终端中进入到这个项目的根目录下,输入vite-cli

服务正常开启。。。

打开浏览器,地址栏中输入http://localhost:3000/ ,目前的页面暂时是空白的,我们打开开发人员工具,可以看到有错误 错误的原因是:main.js 中在 导入vue 时,必须使用 "/","./", "../" webpack打包工具可以帮我们去解决这个问题,而此处我们需要使用接下来要介绍的方式来处理

修改第三方模块路径

实现思路:

  • 拦截响应报文头中 Content-Type: application/javascript; 的内容
  • 要将返回的流转换成字符串
  • 通过正则将 import vue from 'vue' 替换成 import { createApp } from '/@modules/vue'
const streamToString = stream => 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 streamToString(ctx.body)
    // import vue from 'vue'
    // import App from './App.vue'
    ctx.body = contents
      .replace(/(from\s+['"])(?![\.\/])/g, '$1/@modules/')
      // .replace(/process\.env\.NODE_ENV/g, '"development"')
  }
})

重启我们的vite-cli 服务,刷新页面,打开网络资源面板,如下图

此时,已经解决了上文中的问题,但是也新引入了问题

我们修改了第三方模块的加载路径,然而这个路径却是不存在的路径。

加载第三方模块

实现思路:

  • 拦截"/@modules"的请求
  • 通过path.join(),获取该模块的pakage.json路径
  • require 引入该模块的pakage.json,获取到该模块的入口js文件路径
  • 重写请求路径 具体实现代码:
const path = require('path')
// 3. 加载第三方模块
app.use(async (ctx, next) => {
  // ctx.path --> /@modules/vue
  // 拦截"/@modules"的请求
  if (ctx.path.startsWith('/@modules/')) {
    const moduleName = ctx.path.substr(10)
    // 通过path.join(),获取该模块的pakage.json路径
    const pkgPath = path.join(process.cwd(), 'node_modules', moduleName, 'package.json')
    // require 引入该模块的pakage.json,获取到该模块的入口js文件路径
    const pkg = require(pkgPath)
    // 重写请求路径
    ctx.path = path.join('/node_modules', moduleName, pkg.module)
    
  }
  await next()
})

重启vite-cli 服务,结果第三方模块 vue 正常引入了

打开控制台,发现还是打印了两条错误:

原因是浏览器是不识别App.vue 和 index.css 这两种模块,因此需要将这种浏览器不能识别的模块放在服务器端处理。

编译单文件组件

将上文中使用vite脚手架创建的项目运行起来,看一下Vue是如何实现的。

项目运行起来后,打开网络面板,我们观察上图中的App.vue 可以发现这个文件被请求了两次,我们来看下这两次都做了什么 首先我们来看第一次请求结果:

对比原文件:

可以发现原文件这部分被转换成了__script 对象,通过 import {render as __render} from "/src/App.vue?type=template" 发起了 第二次请求, 返回render 函数,挂载到 __script对象上

我们看第二次请求响应结果:此处返回了 render 函数

根据上文参照分析,我们来模拟实现一下这两次请求的实现。

这里我们要借助 @vue/compiler-sfc 这个文件来解析单文件,所以要先安装

npm install @vue/compiler-sfc -D

具体实现代码:

const { Readable } = require('stream')
const compilerSFC = require('@vue/compiler-sfc')
const stringToStream = text => {
  const stream = new Readable()
  stream.push(text)
  // 关闭流
  stream.push(null)
  return stream
}
// 4. 处理单文件组件
app.use(async (ctx, next) => {
  if (ctx.path.endsWith('.vue')) {
    const contents = await streamToString(ctx.body)
    const { descriptor } = compilerSFC.parse(contents)
    let code
    if (!ctx.query.type) {
      code = descriptor.script.content
      // console.log(code)
      code = code.replace(/export\s+default\s+/g, 'const __script = ')
      code += `
      import { render as __render } from "${ctx.path}?type=template"
      __script.render = __render
      export default __script
      `
    } else if (ctx.query.type === 'template') {
      const templateRender = compilerSFC.compileTemplate({ source: descriptor.template.content })
      code = templateRender.code
    }
    ctx.type = 'application/javascript'
    ctx.body = stringToStream(code)
  }
  await next()
})

点击下载完整代码