使用webpack在开发中,只改动一句代码,也需要数秒的热更新,这是因为webpack需要将所有的模块打包成一个一个或者多个模块,然后启动开发服务器,请求服务器时直接给予打包结果。这个过程随着项目的扩大,速度会变慢。然后vite来了。
描述:针对Vue单页面组件的无打包开发服务器,可以直接在浏览器运行请求的vue文件
特点:
- 冷服务启动-使用ES6 import预览的时候不打包
- 开发中热更新
- 按需进行编译,不会刷新全部DOM
vite
使用vite创建vue3项目
1、Npm 创建 vite项目
npm init vite-app projectName
2、Yarn 创建vite项目
yarn create vite-app projectName
3、vite创建react项目
-
新建文件夹。
-
进入文件夹中命令npm init vite-app --template react
-
安装依赖 yarn
-
运行 yarn dev
问题1:既然去掉了webpack打包步骤,那么vite是如何处理这些模块的呢?
当声明一个 script
标签类型为 module
时,
<script type="module" src="/src/main.js"></script>
浏览器就会像服务器发起一个GET
http://localhost:3000/src/main.js
请求main.js文件,
浏览器请求到了main.js
文件,检测到内部含有import
引入的包,又会对其内部的 import
引用发起 HTTP
请求获取模块的内容文件!如: GET
http://localhost:3000/@modules/vue.js
如: GET
http://localhost:3000/src/App.vue
其Vite
的主要功能就是通过劫持浏览器的这些请求,并在后端进行相应的处理将项目中使用的文件通过简单的分解与整合,然后再返回给浏览器渲染页面,vite
整个过程中没有对文件进行打包编译,所以其运行速度比原始的webpack
开发编译速度快出许多!
- 在浏览器中启动了一个socket服务,实时的接受一系列的指令,根据指令再处理相应的逻辑。
// src/node/serve/index.ts
export async function createServer(
inlineConfig: InlineConfig = {}
): Promise<ViteDevServer> {
// 创建serve服务
const app = connect() as Connect.Server
const ws = createWebSocketServer(httpServer, logger)
const watchOptions = serverConfig.watch || {}
const watcher = chokidar.watch(root, {
ignored: [
'**/node_modules/**',
'**/.git/**',
...(watchOptions.ignored || [])
],
...
}) as FSWatcher
}
createWebSocketServer处理
// src/node/serve/ws.ts
export function createWebSocketServer(
server: Server,
logger: Logger
): WebSocketServer {
// 启动一个webSocket服务
const wss = new WebSocket.Server({ noServer: true })
server.on('upgrade', (req, socket, head) => {
...
}
})
// 通知客户端链接成功,需要请求文件
wss.on('connection', (socket) => {
socket.send(JSON.stringify({ type: 'connected' }))
...
})
}
- server端负责在执行的各个阶段向客户端发送指令
// src/linet/client.ts
async function handleMessage(payload: HMRPayload) {
switch (payload.type) {
case 'connected': // scoket链接成功
console.log(`[vite] connected.`)
setInterval(() => socket.send('ping'), __HMR_TIMEOUT__)
break
case 'update':// js文件更新
...
...
break
case 'custom'://自定义
...
break
case 'full-reload': //网页重刷新
...
break
case 'prune': //移除模块
...
break
case 'error':
...
break
default:
const check: never = payload
return check
}
}
当vite文件监听系统监听到.vue组件发生变化之后,就会去解析编译.vue组件,并向client发送一条对应指令,并把编译后的代码也发送给client
问题2:为什么给vue的模块加一个前缀@modules ?
import { createApp } from 'vue'
编译器能够自动从node_modules寻找vue这个模块,是因为npm install时,编译器存储了vue别名,因此可直接去node_modules中读取。
但浏览器环境并没有执行这个过程,因此依然会从当前文件的同级路径下寻找vue这个文件,如果文件不存在,则报404错误,因此我们要把 node_modules 变成浏览器环境可识别的位置,即 /@modules/
vue模块安装在node_modules
中,浏览器ES Module
是无法直接获取到项目下node_modules目录中的文件。所以vite
对import
都做了一层处理,重写了前缀使其带有@modules
问题3:既然浏览器直接请求了.vue
文件,那么文件内容是如何做出解析的呢?
唯一编译.vue文件,被解析成render函数返回给浏览器渲染页面。当Vite遇到一个.vue后缀的文件时。由于.vue模板文件的特殊性,它被分割成template,css,脚本模块三个模块进行分别处理。最后放入script,template,css发送多个请求获取。
单页面文件的请求都是以*.vue
作为请求路径结尾,当服务器接收到这种特点的http请求,主要处理
- 根据
ctx.path
确定请求具体的vue文件 - 使用
parseSFC
解析该文件,获得descriptor
,一个descriptor
包含了这个组件的基本信息,包括template
、script
和styles
等属性
然后根据descriptor
和ctx.query.type
选择对应类型的方法,处理后返回
// plugin-vue/src/index.ts
export default function vuePlugin(rawOptions: Options = {}): Plugin {
...
transform(code, id) {
const { filename, query } = parseVueRequest(id)
...
if (!query.vue) {
...
} else {
// 使用parseSFC解析该文件
const descriptor = getDescriptor(filename)!
// 根据`descriptor`和`ctx.query.type`选择对应类型解析的方法
if (query.type === 'template') {
return transformTemplateAsModule(code, descriptor, options, this)
} else if (query.type === 'style') {
...
}
}
}
}
// plugin-vue/src/template.ts
export function transformTemplateAsModule(
...
) {
...
if (options.devServer && !options.isProduction) {
returnCode += `
import.meta.hot.accept(({ render }) => {
__VUE_HMR_RUNTIME__.rerender(${JSON.stringify(descriptor.id)}, render)
})`
}
return {
code: returnCode,
map: result.map as any
}
}
总结:
1、默认采用ES 6原生模块
2、默认会给vue的模块加一个前缀@modules import { createApp } from '/@modules/vue.js'
3、解析.vue文件
vite的优雅之处就在于需要某个模块时动态引入,而不是提前打包,自然而然提高了开发体验