Vite是什么以及解决了什么
Vite,一个基于浏览器原生 ES imports 的开发服务器。利用浏览器去解析 imports,在服务器端按需编译返回,完全跳过了打包这个概念,服务器随起随用。同时不仅有 Vue 文件支持,还搞定了热更新,而且热更新的速度不会随着模块增多而变慢。针对生产环境则可以把同一份代码用 rollup 打包。
如今前端的打包工具 webpack 一统江山。但是在开发一些大型项目或者多页面应用时,webpack的首次启动速度往往需要数分钟才能编译成功,大型的ts项目中,启动时间往往更长。同时webpack的是按需加载实际是假按需,一个文件的改动,需要从新编译所有文件,导致热更新的速度在大型项目中也会很慢。
而随着vue3发布的的工具vite就是为了解决此类问题,直接基于浏览器原生的esModle来实现,它的优点:
- 拥有更快的启动速度
- 及时的热更新
- 真正的按需加载
上图是 caniuse 上各个浏览器关于
import的支持情况。可见在开发环境使用是完全可以的。
vite的实现原理
vite的整体实现的流程如下:
- 拦截
import请求(无需编译 ES6) - 解析
.vue模版,编译成 js 和 css 文件,返回浏览器。
Talk is cheap. Show me the code。 下面我们来实现一个简易版的 simple-vite。
项目目录如下
├── client
│ ├── app.js
│ ├── bin
│ │ └── www
│ ├── compile
│ │ └── index.js
│ └── util
│ └── findPort.js
├── index.html
├── package.json
├── public
│ └── favicon.ico
├── src
│ ├── App.vue
│ ├── assets
│ │ └── logo.png
│ ├── components
│ │ └── HelloWorld.vue
│ ├── index.css
│ └── main.js
└── yarn.lock
client文件夹下为simple-vite的实现。src下的目录为 vite初始化项目时的文件。
1、建立本地服务器拦截
yarn add koa koa-static -D
// app.js
const Koa = require('koa')
const path = require('path')
const koaStatic = require('koa-static')
const app = new Koa();
// 根路径
global.$rootPath = path.resolve(__dirname, '../')
// 静态资源
app.use(koaStatic(global.$rootPath))
module.exports = app
// bin/www
#!/usr/bin/env node
const app = require('../app')
const findPort = require('../util/findPort')
findPort(3000).then(port=>{
app.listen(port);
})
// util/findPort
const net = require('net')
const findPort = function(port){
const server = net.createServer().listen(port)
return new Promise((resolve,reject)=>{
server.on('listening',function(){
console.log(`server is runing on port ${port}.........`)
server.close()
return resolve(port)
})
server.on('error',function(err){
if(err.code==='EADDRINUSE'){
return resolve(findPort(port+1))
}else{
console.log(err)
return reject(err)
}
})
})
}
module.exports = findPort
此时执行node bin/www 访问 http://localhost:3000/index.html 已经可以访问到页面。
2、拦截 html 请求
对于 html 页面的请求,首先注入一个函数,用来后期的更新 css 操作
// app.js
const { injectUpdateStyle } = require('./compile')
app.use(async (ctx, next) => {
await next();
const filePath = path.join(global.$rootPath, ctx.path)
/* 如果是返回的html 则注入js 的处理 */
if (ctx.response.is('html')) {
let str = fs.readFileSync(filePath, 'utf-8')
ctx.body = injectUpdateStyle(str)
}
})
// compile/index.js
module.exports.injectUpdateStyle = function injectUpdateStyle(content) {
const inject = ` <script>
function updateStyle(css){
const style = document.createElement('style')
style.innerHTML = css
document.head.appendChild(style)
}
</script>`
return content.replace(/<\/head>/, `$&${inject}`)
}
3、拦截 js 文件
拿 main.js 举例。main.js中的代码如下:
// main.js
import { createApp } from 'vue'
import App from './App.vue'
import './index.css'
createApp(App).mount('#app')
原生的esModule并不能解析import { createApp } from 'vue' 这样的语法,因此我们需要解析上面的from 'vue'这种语法,解析问浏览器可以识别的import请求。
此时需要用到两个库.
yarn add es-module-lexer magic-string @vue/compiler-sfc -D
同时拦截 js 的请求
// app.js
app.use(async (ctx, next) => {
await next();
const filePath = path.join(global.$rootPath, ctx.path)
// 增加一下代码
/* 如果是js */
if (ctx.response.is('js')) {
if (ctx.body instanceof Readable) { // 如果读取到 /@modules/ 时已经做了处理 不再是流
const source = fs.readFileSync(filePath, 'utf-8')
ctx.body = compileJs(source)
}
}
})
// compile/index.js
const { parse } = require('es-module-lexer')
const MagicString = require('magic-string')
/**
* 编译 js
* import { createApp } from 'vue' 转化为 import { createApp } from '@/modules/vue' 格式
*/
function compileJs(source) {
const [imports, exports] = parse(source, 'optional-sourcename');
const magicString = new MagicString(source)
const regx = /^[^\/\.@]/ // 开头不是 / . @
imports.forEach(({ s, e }) => {
const str = source.substring(s, e)
if (regx.test(str)) {
magicString.overwrite(s, e, `/@modules/${str}`)
}
})
return magicString.toString()
}
module.exports.compileJs = compileJs
此时用于将 import { createApp } from 'vue' 转化为 import { createApp } from '@/modules/vue' 因此需要拦截 /@modules/vue 并返回 vue.js。增加如下代码:
// app.js
app.use(async (ctx, next) => {
await next()
if (ctx.path.startsWith('/@modules/vue')) {
const vuePath = path.join(global.$rootPath, 'node_modules/vue/dist/vue.runtime.esm-browser.prod.js')
ctx.type = 'js'
const data = fs.readFileSync(vuePath, 'utf-8')
ctx.body = data
}
})
3、解析 css 请求
// app.js
app.use(async (ctx, next) => {
await next();
const filePath = path.join(global.$rootPath, ctx.path)
// 增加如下代码
/* 如果是返回css */
if (ctx.response.is('css')) {
const data = fs.readFileSync(filePath, 'utf-8').replace(/\n/g, '')
ctx.type = 'js'
ctx.body = `updateStyle('${compileCss(data)}')`
}
})
// compile/index.js
/* 编译 css */
const { compileStyle } = require('@vue/compiler-sfc')
module.exports.compileCss = function compileCss(content) {
const { code } = compileStyle({ source: content, trim: true }) // 此处的 compileStyle 是可以支持 是否 scoped postcss 等的
return code
}
4、解析 .vue 文件
const { parse: VueParse } = require('@vue/compiler-sfc')
// app.js
app.use(async (ctx, next) => {
await next();
const filePath = path.join(global.$rootPath, ctx.path)
// 增加如下代码
/* 如果是 vue 文件 */
if (ctx.path.endsWith('.vue')) {
const source = fs.readFileSync(filePath, 'utf-8')
if (!ctx.query.type) {
ctx.type = 'js'
ctx.body = compileVue(source, ctx.path)
}
}
})
// compile/index.js
const { parse: VueParse, compileScript, } = require('@vue/compiler-sfc')
/**
* 编译 vue
* 第一次编译 .vue 文件
*/
module.exports.compileVue = function compileVue(source, path) {
const { descriptor } = VueParse(source)
let { content } = compileScript(descriptor)
content = content.replace('export default', `const __script = `)
console.log(descriptor, 'descriptordescriptordescriptordescriptor')
let str = ''
if (descriptor.styles.length) {
str = descriptor.styles.reduce((total, item, index) => {
total += `import "${path}?type=style&index=${index}"\n`
return total
}, '')
}
let response = `${content}
${str}
import {render as __render} from "${path}?type=template"
__script.render = __render
export default __script
`
return response
}
此时一个 .vue 文件的请求编译成了如下的样子。
编译后的文件,会再次请求对应的
style 和 template。
5、解析 .vue 中的 template 和 style 返回
// app.js
app.use(async (ctx, next) => {
await next();
const filePath = path.join(global.$rootPath, ctx.path)
/* 如果是 vue 文件 */
if (ctx.path.endsWith('.vue')) {
const source = fs.readFileSync(filePath, 'utf-8')
const { descriptor } = VueParse(source)
if (!ctx.query.type) {
ctx.type = 'js'
ctx.body = compileVue(source, ctx.path)
} else if (ctx.query.type === 'template') { // 编译后的 .vue 文件发送的请求
ctx.type = 'js';
ctx.body = compileVueTemplate(descriptor.template.content)
} else if (ctx.query.type === 'style') { // 编译后的 .vue 文件发送的请求
const index = ctx.query.index
const code = compileCss(descriptor.styles[index].content)
ctx.body = `updateStyle(${JSON.stringify(code)})`
ctx.type = 'js'
}
}
})
const { compileTemplate } = require('@vue/compiler-sfc')
// compile/index.js
/* 编译 vue template */
module.exports.compileVueTemplate = function compileVueTemplate(content) {
const { code } = compileTemplate({ source: content, transformAssetUrls: false });
return compileJs(code)
}
此时vite初始化的项目已经可以运行。如下:
源代码
具体代码 simple-vite