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()
})