Vite
- Vite是一个面向现代浏览器的一个更轻、更快的web开发应用
- 基于ECMAScript标准原生模块系统(ESM)实现 Vite项目依赖项
- Vite
- @vue/compiler-sfc
Vite的特点
- 快速冷启动
- 模块热更新
- 按需编译
- 开箱即用 vite serve
vue-cli-service serve
vite只有当文件请求时才会去编译相应的模块
- vite只有在请求时才会去编译文件,文件修改也只会编译当前文件
- webpack HMR会以这个文件为入口重新build一次,所有涉及的依赖都会被重新加载一遍
vite build
- Rollup 打包
- 动态导入
使用webpack的原因
- 浏览器环境不支持模块化
- 零散的模块文件会产生大量的http请求(http2可以解决通过复用链接)
开箱即用
- Typescript内置支持
- less/sass/stylus/postcss内置支持 需要单独安装依赖
- jsx
- Web Assembly
Vite的核心功能
vite在启动时,会把当前项目作为静态服务器的根目录,并拦截服务器的部分文件请求,当遇到浏览器不能识别的模块时会进行编译,通过websocked实现hmr
Vite的实现原理
静态web服务器
npm init -y
在package.json文件中设置name为vite-cli即脚手架的启动命令为vite-cli
首先启动一个web服务器
#!/usr/bin/env node
const Koa = require('koa')
const send = require('koa-send')
const app = new Koa()
app.use(async (ctx,next) => {
await send(ctx, ctx.path, {
root: process.cwd(),
index: 'index.html'
})
await next()
})
app.listen(3000, () => {
console.log('serve listen 3000')
})
使用npm link或yarn link 将这个脚手架工具安装到本地全局
npm link
# 启动
vite-cli
处理javascript请求
// stream转换成字符串
function streamToString(text) {
return new Promise((resolve, reject) => {
let chunks = []
text.on('data', chunk => chunks.push(chunk))
text.on('error', reject)
text.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')))
})
}
app.use(async (ctx, next) => {
if (ctx.type === 'application/javascript') {
// ctx.body 是stream类型,需转换成字符串
ctx.body = await streamToString(ctx.body)
}
await next()
})
加载第三方模块
首先需要将上面JavaScript代码中import的第三方包路径修改为'/@modules/',然后对第三方模块路径进行处理
// 处理第三方包路径 应该放在最前面
app.use(async (ctx, next) => {
if (ctx.path.startsWith('/@modules/')) {
let moduleName = ctx.path.substr(10)
let modulePath = path.resolve(process.cwd(), 'node_modules', moduleName)
let pkg = require(path.resolve(modulePath, 'package.json'))
ctx.path = path.join('/node_modules', moduleName, pkg.module)
}
await next()
})
...
app.use(async (ctx, next) => {
if (ctx.type === 'application/javascript') {
// ctx.body 是stream类型
let text = await streamToString(ctx.body)
// 修改第三方模块包的路径
ctx.body = text.replace(/(from\s+['"])(?!.\/)/g, '$1/@modules/')
}
await next()
})
编译单文件组件
首先需要修改vue文件格式为以下格式
import {render as __render} from "/aa.vue?type=template"
__script.render = __render
export default __script
第二次请求vue文件时,输出编译后的代码
// 编译文件
const build = 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
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') {
let template = compilerSfc.compileTemplate({
source: descriptor.template.content,
filename: descriptor.filename,
id: ctx.path
})
code = template.code
}
ctx.body = stringToStream(code)
ctx.type = 'application/javascript'
}
await next()
}
处理外部引入的css文件
首先需要将js代码中引入的css文件路径替换为xxx@import
app.use(async (ctx, next) => {
if (ctx.type === 'application/javascript') {
// ctx.body 是stream类型
let text = await streamToString(ctx.body)
// 修改第三方模块包的路径
ctx.body = text.replace(/(from\s+['"])(?![.\/])/g, '$1/@modules/')
.replace(/process\.env\.NODE_ENV/g, '"develop"')
// css
.replace(/(import\s+['"][\w\.\/]+\.css)(['"])/g,'$1?import$2')
}
await next()
})
然后把css代码添加到html样式中
...
if (ctx.path.endsWith('.css')) {
const css = await streamToString(ctx.body)
let code = `
import {updateStyle} from '/build/util/style.js'
const css = ${JSON.stringify(css)}
updateStyle(css)
export default css
`
ctx.body = stringToStream(code)
ctx.type = 'application/javascript'
}
...
export function updateStyle(content,id){
const style = document.createElement('style')
style.setAttribute('type', 'text/css')
style.innerHTML = content
document.head.appendChild(style)
}
完整代码
入口文件
#!/usr/bin/env node
const Koa = require('koa')
const send = require('koa-send')
const path = require('path')
const {
static,
npmHandler,
build
} = require('./middlewares/index.js')
const {
streamToString
} = require('./util')
const app = new Koa()
// 处理第三方依赖
app.use(npmHandler)
// 处理静态资源
app.use(static)
app.use(build)
// 修改第三方模块依赖
app.use(async (ctx, next) => {
if (ctx.type === 'application/javascript') {
// ctx.body 是stream类型
let text = await streamToString(ctx.body)
// 修改第三方模块包的路径
ctx.body = text.replace(/(from\s+['"])(?![.\/])/g, '$1/@modules/')
.replace(/process\.env\.NODE_ENV/g, '"develop"')
.replace(/(import\s+['"][\w\.\/]+\.css)(['"])/g,'$1?import$2')
}
await next()
})
app.listen(3000)
console.log('server listen http://localhost:3000')
middlewares
const send = require('koa-send')
const path = require('path')
const compilerSfc = require('@vue/compiler-sfc')
const {
streamToString,
stringToStream
} = require('../util')
// 静态资源
const static = async (ctx, next) => {
await send(ctx, ctx.path, {
root: process.cwd(),
index: 'index.html'
})
await next()
}
// 处理第三方依赖包
const npmHandler = async (ctx, next) => {
if (ctx.path.startsWith('/@modules/')) {
let moduleName = ctx.path.substr(10)
let modulePath = path.resolve(process.cwd(), 'node_modules', moduleName)
let pkg = require(path.resolve(modulePath, 'package.json'))
ctx.path = path.join('/node_modules', moduleName, pkg.module)
}
await next()
}
// 编译文件
const build = 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
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') {
let template = compilerSfc.compileTemplate({
source: descriptor.template.content,
filename: descriptor.filename,
id: ctx.path
})
code = template.code
console.log(descriptor.filename)
}
ctx.body = stringToStream(code)
ctx.type = 'application/javascript'
} else if (ctx.path.endsWith('.css')) {
const css = await streamToString(ctx.body)
let code = `
import {updateStyle} from '/build/util/style.js'
const css = ${JSON.stringify(css)}
updateStyle(css)
export default css
`
ctx.body = stringToStream(code)
ctx.type = 'application/javascript'
}
await next()
}
module.exports = {
static,
npmHandler,
build
}
utils
const {
Readable
} = require('stream')
// stream 转换成 string
function streamToString(text) {
return new Promise((resolve, reject) => {
let chunks = []
text.on('data', chunk => chunks.push(chunk))
text.on('error', reject)
text.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')))
})
}
function stringToStream(text) {
const stream = new Readable()
stream.push(text)
stream.push(null)
return stream
}
module.exports = {
streamToString,
stringToStream
}
style
export function updateStyle(content,id){
const style = document.createElement('style')
style.setAttribute('type', 'text/css')
style.innerHTML = content
document.head.appendChild(style)
}