文章内容输出来源:拉勾教育前端高薪训练营
- Vite是一个面向现代浏览器的一个更轻、更快的Web应用开发工具
- 它基于ECMAScript标准原生模块系统(ES Moudule)实现,解决webpack-dev-server冷启动时间过长,HMR热更新反应速度慢
Vite和Vue-CLI
- Vite使用浏览器原生支持的ES Module加载模块,在开发模式下不需要打包可以直接运行
- Vite在生产环境下使用Rollup打包(基于ES Module的方式打包,不需要基于babel将import转换成require),打包体积比Webpack小
- Vue-CLI开发模式下必须对项目打包才可以运行
- Vue-CLI使用Webpack打包
Vite的特点
- 快速冷启动
- 代码按需编译,只有代码当前需要加载时才会编译
- 模块热更新
- 开箱即用
核心功能
- 静态Web服务器
- 编译单文件组件,拦截浏览器不识别的模块并处理
- HMR
与webpack的HMR区别
- Vite HMR:立即编译当前所修改的文件
- Webpack HMR:会自动以这个文件为入口重写build一次,所有的涉及到的依赖也都会被加载一遍
注:使用Webpack打包的两个原因,浏览器环境并不支持模块化和零散的模块文件会产生大量的HTTP请求
Vite项目依赖
- Vite
- @vue/compiler-sfc
Vite创建项目
- 基于Vue3的项目
$ npm init vite-app <project-name>
$ cd <project-name>
$ npm install
$ npm run dev
- 基于模板创建项目
$ npm init vite-app --template react
$ npm init vite-app --template preact
关于Vite是如何让浏览器识别.vue文件
Vite开启的web服务器会劫持.vue请求,把.vue文件解析成js文件并把响应头中的content-type设置为application/javascript
模拟Vite实现
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
package.json
{
"name": "vite-cli",
"version": "1.0.0",
"main": "index.js",
"bin": "index.js",
"license": "MIT",
"dependencies": {
"koa": "^2.13.4",
"koa-send": "^5.0.1"
}
}
index.js
#!/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 at http://localhost:3000')
在vite-cli目录下执行将命令链接到全局
$ npm link
在vue3项目中执行
$ vite-cli
修改第三方模块的路径
上面的代码因为main.js不是相对路径引入,会爆出
Uncaught TypeError: Failed to resolve module specifier “vue”. Relative references must start with either “/”, “./”, or “…/”.
在index.js处理
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)
})
// 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/')
.replace(/process\.env\.NODE_ENV/g, '"development"')
}
})
加载第三方模块
经过上方的路径处理,浏览器会去 http://localhost:3000/@modules/vue 下寻找文件并加载但此时并不存在文件,此时需要匹配@modules并将其修改为node_modules中对应的路径进行加载
index.js
const path = require('path')
// 3. 加载第三方模块
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()
})
编译单文件组件
$ yarn add @vue/compiler-sfc
index.js
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
}
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()
})
完整代码
#!/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. 加载第三方模块
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)
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()
})
// 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/')
.replace(/process\.env\.NODE_ENV/g, '"development"')
}
})
app.listen(3000)
console.log('Server running @ http://localhost:3000')