前言
大家好,在上次我们已经把webpack从基础使用到原理分析讲过了,今天我们来聊一聊vite,vite为什么快。
在上篇关于webpack中已经提到了,webpack会把我们的项目构建好然后交给浏览器,那么这个过程就包含了js、css等等资源的构建,例如需要请求某一份资源,这份资源中又要请求另一份资源,构建完成才交给浏览器。
那么vite是怎么做的呢?
vite
vite读取项目代码后,他就直接把这份唯一的html文件输出给浏览器了,让浏览器来加载,那么在浏览器加载html时就会碰到一系列的引入,那么他就向vite发请求需要这一份js,js又向项目中取,拿到这份js后再交给浏览器,如此往复。这么看,vite就像是一个后端的服务器(所以你能明白我们配置vite.config.js用来处理跨域做代理了吗),这也是为什么我们vue3+vite构建一个新项目后,f12打开就能看见明明还没开始写代码,就有很多个资源的请求了。
这就是webpack和vite在构建理念上的不同之处了,在交给浏览器之前webpac会让浏览器等着,将资源全部加载完毕后才交付(从头到尾将代码读明白,甚至还会把浏览器识别不了的语法帮你降低版本)
构建vite
那么接下来我们来看看vite是如何打造的,原理是什么样的,我们来自己做一个vite
项目结构:
js
npm i vue
index.html此处用module类型是因为如果不用这种方式引入,import语法是没办法识别的
js
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<div id="app">
</div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
main.js
js
import {createApp} from "vue";
import './style.css'
import App from './App.vue'
createApp(App).mount('#app')
我们通过标签引入这份js代码,浏览器会当成一个资源请求,但我们并没有这个接口,所以会报一个404错误,但是我们用vite+vue也是一样的,那么vite在这个过程中起到了什么作用?事实上就是想办法让这个接口存在
那么我们在myVite根目录下创建一个simple-vite.js来模拟一下 simple-vite.js
js
const http = require('http');
const fs = require('fs');
const path = require('path')
const server = http.createServer((req, res) => {
const {url,query} = req
if (url === '/'){
res.writeHead(200, {'Content-Type': 'text/html'})
let html = fs.readFileSync('./index.html', 'utf-8')
res.end(html)
}else if (url.endsWith('.js')) { // /src/main.js
const p = path.resolve(__dirname, url.slice(1))
console.log(p)
res.writeHead(200, {'Content-Type': 'application/javascript'})
let content = fs.readFileSync(p, 'utf-8')
res.end(content)
}
})
server.listen(5173, () => {
console.log('项目运行在5173')
})
js
npx nodemon simple-vite.js
然后访问localhost:5173就会自动请求index.html进入到index.html后又会要main.js这个资源,此时就不会再报404的错误了,因为我们有这个接口,并且拿到了main.js这份资源。
但是,错误又出现了。
这种错误通常是没有正确设置响应头导致返回的文件类型不正确或者是网页中引用的模块脚本也就是我们的import要来的文件路径不正确或者是文件不存在
确实如此,我们import 一份vue的以后是from"vue"这里不是一个路径,我们必须告诉浏览器这份资源的正确路径才能够找到,因此我们需要一个方法来重写import,将其更改为一个路径。例如去node_modules/vue中寻找资源
js
const http = require('http');
const fs = require('fs');
const path = require('path')
function rewriteImport(content){
return content.replace(/ from ['"](.*)['"]/g, function(s0,s1){
if(s1[0] !== '.' && s1[0] !== '/'){
return ` from '/@module/${s1}'`
}else {
return s0
}
})
}
const server = http.createServer((req, res) => {
const {url,query} = req
if (url === '/'){
res.writeHead(200, {'Content-Type': 'text/html'})
let html = fs.readFileSync('./index.html', 'utf-8')
res.end(html)
}else if (url.endsWith('.js')) { // /src/main.js
const p = path.resolve(__dirname, url.slice(1))
console.log(p)
res.writeHead(200, {'Content-Type': 'application/javascript'})
let content = fs.readFileSync(p, 'utf-8')
res.end(rewriteImport(content))
}
})
server.listen(5173, () => {
console.log('项目运行在5173')
})
此时,不仅请求了main.js,当读到main.js后又会看见import xx from vue,此时会去请求vue资源。
那么接下来的工作就简单起来了,其实就是不断的写接口,如果你的路径是我们改写的@modules/xxx那就去node_modules下面找。
js
const http = require('http');
const fs = require('fs');
const path = require('path')
function rewriteImport(content){
return content.replace(/ from ['"](.*)['"]/g, function(s0,s1){
if(s1[0] !== '.' && s1[0] !== '/'){
return ` from '/@module/${s1}'`
}else {
return s0
}
})
}
const server = http.createServer((req, res) => {
const {url,query} = req
if (url === '/'){
res.writeHead(200, {'Content-Type': 'text/html'})
let html = fs.readFileSync('./index.html', 'utf-8')
res.end(html)
}else if (url.endsWith('.js')) { // /src/main.js
const p = path.resolve(__dirname, url.slice(1))
console.log(p)
res.writeHead(200, {'Content-Type': 'application/javascript'})
let content = fs.readFileSync(p, 'utf-8')
res.end(rewriteImport(content))
}else if (url.startsWith('/@module/')){
const prefix = path.resolve(__dirname, 'node_modules',url.replace('/@module/', ''))
const module = require(prefix + '/package.json').module // vue源代码路径
const p = path.resolve(prefix, module) // vue完整路径
const content = fs.readFileSync(p, 'utf-8')
res.writeHead(200,{
'Content-Type': 'application/javascript'
})
res.end(rewriteImport(content))
}else if ()
})
server.listen(5173, () => {
console.log('项目运行在5173')
})
能够看见,main.js请求了,vue源代码也请求到了,剩下的就是css和vue的文件如何请求,接下来还是else if继续写接口对吧?但是写else if 拿vue后缀的请求,请求到了浏览器也没办法读懂,因此需要对vue代码进行编译,但是我们已经拿到vue的源码,vue源码中有编译器。
js
const http = require('http')
const fs = require('fs')
const path = require('path')
const compilerSfc = require('@vue/compiler-sfc')
const compilerDom = require('@vue/compiler-dom')
function rewriteImport(content) { // ++++新增
return content.replace(/ from ['|"]([^'"]+)['|"]/g, function(s0, s1) { // 找到 from 'vue' 中的 'vue'
if (s1[0] !== '.' && s1[1] !== '/') {
return ` from '/@modules/${s1}'`
} else {
return s0
}
})
}
const server = http.createServer((req, res) => {
const { url } = req
const query = new URL(req.url, `http://${req.headers.host}`).searchParams;
if (url === '/') {
res.writeHead(200, {
'content-type': 'text/html'
})
let content = fs.readFileSync('./index.html', 'utf-8')
res.end(content)
} else if (url.endsWith('.js')) { // '/src/main.js'
const p = path.resolve(__dirname, url.slice(1))
res.writeHead(200, {
'content-type': 'application/javascript'
})
let content = fs.readFileSync(p, 'utf-8')
res.end(rewriteImport(content))
} else if (url.startsWith('/@modules/')) { // '/@modules/vue'
const prefix = path.resolve(__dirname, 'node_modules', url.replace('/@modules/', ''))
const module = require(prefix + '/package.json').module
const p = path.resolve(prefix, module)
const content = fs.readFileSync(p, 'utf-8')
res.writeHead(200, {
'content-type': 'application/javascript'
})
res.end(rewriteImport(content))
} else if (url.indexOf('.vue') !== -1) {
const p = path.resolve(__dirname, url.split('?')[0].slice(1))
const { descriptor } = compilerSfc.parse(fs.readFileSync(p, 'utf8'))
// 问我要 App.vue 的 js 部分
if (!query.get('type')) {
res.writeHead(200, {'Content-Type': 'application/javascript'})
const content = `
${rewriteImport(descriptor.script.content.replace('export default', 'const __script = '))}
import { render as __render } from "${url}?type=template"
__script.render = __render
export default __script
`
res.end(content)
} else if (query.get('type') === 'template') { // 问我要 App.vue 的 template 部分
const template = descriptor.template
const render = compilerDom.compile(template.content, {mode: 'module'}).code
res.writeHead(200, {'Content-Type': 'application/javascript'})
res.end(rewriteImport(render))
}
} else if (url.endsWith('.css')) {
const p = path.resolve(__dirname, url.slice(1))
const file = fs.readFileSync(p, 'utf8')
const content = `
const css = "${file.replace(/\n/g, '')}"
let link = document.createElement('style')
link.setAttribute('type', 'text/css')
document.head.appendChild(link)
link.innerHTML = css
export default css
`
res.writeHead(200, {'Content-Type': 'application/javascript'})
res.end(content)
}
})
server.listen(5173, () => {
console.log('项目运行在 5173');
})
小结
所以,vite为什么快?
Vite相比于Webpack之所以构建快是因为,Vite借助新版本浏览器可以读懂模块化语法的特点,将项目中的模块化引入统一以一个又一个http请求的方式响应给浏览器,这样做的好处就是省去了网络包构建过程中递归做依赖收集的耗时步骤,又因为Vite是开发环境的工具,绝大多数情况下我们不用不考虑兼容性,不会有人开发时还用老版本的浏览器吧!
当然vite快,这并不是绝对的,如果一个项目有几千几万个资源文件,那发这么多个http请求能快到哪里呢?
宏观结论:vite更快