Vite使用及原理实现

539 阅读8分钟

1.Vite介绍

Webpack是目前整个前端使用最多的构建工具

但是除了webpack之后也有其他的一些构建工具,比如rollup、parcel、gulp、vite等等

我们知道在实际开发中,我们编写的比如TypeScript、Vue文件是不能被浏览器直接识别的

所以我们必须通过构建工具来对代码进行转换、编译

但是随着项目代码越来越多,处理的越多,构建工作需要很久才能开启服务器,HMR也需要几秒才能反应

所以也有这样的说法:天下苦webpack久矣;

那什么是Vite呢?官方的定位:下一代前端开发与构建工具

Vite (法语意为 "快速的",发音 /vit/) 是一种新型前端构建工具,能够显著提升前端开发体验

优势:开发环境下使用ES6 Module无需打包,启动快,而且会对安装的依赖预打包

生产环境使用rollup ,并不会快很多

image-20220127160800732

2.Vite的构造和特点

它主要由两部分组成

  • 一个开发服务器,它基于原生ES模块提供了丰富的内建功能,HMR的速度非常快速,会立即编译当前所修改的文件
  • 一套构建指令,它使用rollup打开我们的代码,并且它是预配置的,可以输出生成环境的优化过的静态资源

实际上浏览器原生就支持模块化,可以使用<script src="./src/main.js" type="module"></script>

但是如果不借助其他工具,原生模块化也有一些弊端,比如对于浏览器请求频繁以及不能直接识别TypeScript、less、vue等代码

这也就是Vite想帮我们解决的事情

Vite serve

在执行vite serve的时候不需要打包,直接开启一个web服务器,当浏览器请求服务器,比如请求一个单文件组件,这个时候在服务器端编译单文件组件,然后把编译的结果返回给浏览器,注意这里的编译是在服务器端,另外模块的处理是在请求到服务器端处理的。

image-20220127142607613

vue-cli-service serve

当运行vue-cli-service serve的时候,它内部会使用webpack,首先去打包所有的模块,如果模块数量比较多的话,打包速度会非常的慢,把打包的结果存储到内存中,然后才会开启开发的web服务器,浏览器请求web服务器,把内存中打包的结果直接返回给浏览器,像webpack这种工具,它的做法是将所有的模块提前编译打包进bundle里,也就是不管模块是否被执行,是否使用到,都要被编译和打包到bundle。随着项目越来越大,打包后的bundle也越来越大,打包的速度自然也就越来越慢。

image-20220127142641667

Vite利用现代浏览器原生支持的ESModule这个模块化的特性省略了对模块的打包,对于需要编译的文件,比如单文件组件、样式模块等,vite采用的另一种模式即时编译,也就是说只有具体去请求某个文件的时候,才会在服务端编译这个文件,所以这种即时编译的好处主要体现在按需编译,速度会更快。

Vite特点

  • 快速冷启动
  • 模块热更新
  • 按需编译
  • 开箱即用
    • TypeScript - 内置支持
    • less/sass/stylus/postcss - 内置支持(需要单独安装)
    • JSX
    • Web Assembly

Vite未来个人很看好,但是目前支持不够完善

3.Vite的安装和使用

安装vite工具:

npm install vite –g # 全局安装
npm install vite –D # 局部安装

通过vite来启动项目:

npx vite

注意:Vite本身也是依赖Node的,所以也需要安装好Node环境,并且要求Node版本是大于12版本

4.Vite对css的支持

vite可以直接支持css的处理

  • vite可以直接支持css的处理

vite可以直接支持css预处理器,比如less

  • 直接导入less
  • 之后安装less编译器 npm install less -D

vite直接支持postcss的转换

  • 只需要安装postcss,并且配置 postcss.config.js 的配置文件即可
  • npm install postcss postcss-preset-env -D

image-20220127004558274

5.Vite对TypeScript的支持

vite对TypeScript是原生支持的,直接导入即可,它会直接使用ESBuild来完成编译

如果我们查看浏览器中的请求,会发现请求的依然是ts的代码

  • 这是因为vite中的服务器Connect会对我们的请求进行转发
  • 获取ts编译后的代码,返回给浏览器,浏览器就可以直接进行解析

注意:在vite2中,已经不再使用Koa了,而是使用Connect来搭建的服务器

image-20220127004734429

6.Vite对vue的支持

vite对vue提供第一优先级支持:

安装Vue:

npm install vue@next -D

安装支持vue的插件:

npm install @vitejs/plugin-vue -D

npm install @vue/compiler-sfc -D

vite.config.js中配置插件:

image-20220127005018569

7.Vite打包项

我们可以直接通过vite build来完成对当前项目的打包工具:npx vite build

image-20220127005054170

我们可以通过preview的方式,开启一个本地服务来预览打包后的效果:npx vite preview

可以配置对应npm命令

image-20220127013701403

8.ESBuild解析

ESBuild的特点

  • 超快的构建速度,并且不需要缓存
  • 支持ES6和CommonJS的模块化
  • 支持ES6的Tree Shaking
  • 支持Go、JavaScript的API
  • 支持TypeScript、JSX等语法编译
  • 支持SourceMap
  • 支持代码压缩
  • 支持扩展其他插件

ESBuild的构建速度

ESBuild的构建速度和其他构建工具速度对比:

image-20220127005244302

ESBuild为什么这么快呢?

  • ESBuild所有内容使用Go语言从零开始编写的,而不是使用第三方,可以直接转换成机器代码,而无需经过字节码
  • ESBuild可以充分利用CPU的多内核,尽可能让它们饱和运行

9.Vite脚手架工具

在开发中,我们不可能所有的项目都使用vite从零去搭建,比如一个react项目、Vue项目

这个时候vite还给我们提供了对应的脚手架工具

所以Vite实际上是有两个工具的:

  1. vite:相当于是一个构建工具,类似于webpack、rollup
  2. @vitejs/create-app:类似vue-cli、create-react-app

使用脚手架工具

## 创建工程
npm init vite-app <project-name>
## 进入工程目录
cd <project-name>
## 安装依赖
npm install
## 运行
npm run dev

npm init vite-app <project-name>的做法相当于省略了安装脚手架的过程

npm install @vitejs/create-app -g
create-app projectname

10.Vite原理实现

Vite核心功能

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

思路

  1. 修改第三方模块的路径

首先加载第三方模块中的import中的路径改变,改成加载@modules/模块文件名

  1. 加载第三方模块

当请求过来之后,判断请求路径中是否以@modules开头,如果是的话,去node_modules加载对应的模块

  1. 编译单文件组件

发送两次请求,第一次请求是把单文件组件编译成一个对象,第二次请求是编译单文件组件的模板,返回一个render函数,并且把render函数挂载到对象的render方法上。

最终代码:

#!/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 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)
})

// 将字符串转化成流
const stringToStream = text => {
  const stream = new Readable()
  stream.push(text)
  stream.push(null)
  return stream
}

// 3. 加载第三方模块。判断请求路径中是否以`@modules`开头,如果是的话,去node_modules加载对应的模块
app.use(async (ctx, next) => {
  // ctx.path --> /@modules/vue
  if (ctx.path.startsWith('/@modules/')) {
    const moduleName = ctx.path.substr(10)
    const pkgPath = path.join(process.cwd(), 'node_modules', moduleName, 'package.json')
    const pkg = require(pkgPath)
    ctx.path = path.join('/node_modules', moduleName, pkg.module)
  }
  await next()
})

// 1. 开启静态文件服务器
app.use(async (ctx, next) => {
  await send(ctx, ctx.path, {
    root: process.cwd(),
    index: 'index.html'
  })
  await next()
})

// 4. 处理单文件组件
app.use(async (ctx, next) => {
  if(ctx.path.endsWith('.vue')) {
    const contents = await streamToString(ctx.body)
    const { descriptor } = compilerSFC.parse(contents) // 返回一个对象,成员descriptor、errors
    let code
    if (!ctx.query.type) { // 第一次请求,把单文件组件编译成一个对象
      code = descriptor.script.content
      // console.log('code', 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()
})

// 2. 修改第三方模块的路径
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/') // 分组匹配,第一个分组中,from原样匹配form,\s+匹配一至多个空格,['"]匹配单引号或双引号。第二个分组中,?!标识不匹配这个分组的结果,也就是排除点开头或者\开头的情况
    .replace(/process\.env\.NODE_ENV/g, '"development"')// 替换process对象
  }
})


app.listen(4000)
console.log('Server running @ http://localhost:4000')

使用时先将cli项目link到全局,npm link

然后在vue3项目中执行my-vite-cli运行项目。将vue3中的图片和样式模块导入代码注释掉。