Vite
,一个基于浏览器原生 ES Module imports
的 Web
开发构建工具
浏览器解析 type='module'
的 script
时,例如:
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
会发送 http
请求:http://localhost:4000/src/main.js
,vite
会拦截此请求,将需要的模块编译返回
- 不用打包,直接编译返回,速度快
- 真正的按需编译
- 支持 HMR
用 koa
启一个服务,将首页返回
const Koa = require('koa')
const Router = require('koa-router')
const fs = require('fs')
const path = require('path')
const app = new Koa()
const router = new Router()
// 返回index.html
router.get('/', async (ctx, next) => {
let content = fs.readFileSync('./index.html', 'utf-8')
ctx.type = 'text/html'
ctx.body = content
})
app.use(router.routes())
app.listen(4000, () => {
console.log('toy-vite server listen at 4000')
})
找不到请求,需要在服务端添加请求处理
// index.html中,会通过ES Module引入main.js(<script type="module" src="/src/main.js"></script>)
// ESM会发起http请求,去获取需要引入的模块
router.get(/\.js$/, async (ctx, next) => {
const {
request: { url },
} = ctx
// 此处是读取main.js,并返回
let content = fs.readFileSync(path.resolve(__dirname, url.slice(1)), 'utf-8')
ctx.type = 'application/javascript'
ctx.body = content // rewriteImport(content)
})
再次请求
错误的原因是因为在 main.js
中,引入了 vue
import { createApp } from 'vue'
vue
模块在 node_modules
中,浏览器并不不知道,需要手动转换,将模块内容读取出来,返回浏览器
对于非 "/"
、"./"
、"../"
方式的模块引用,统一转换成 "/@modules/xxx"
的形式,比如:
// 这句代码将转换成下面的形式
// import { createApp } from 'vue'
import { createApp } from '/@modules/vue'
// 转换方法:
// Uncaught TypeError: Failed to resolve module specifier "vue".
// Relative references must start with either "/", "./", or "../".
// 将 from 'vue' 转成 from '/@modules/vue'
function rewriteImport(content) {
return content.replace(/ from ['|"]([^'"]+)['|"]/g, function (s0, s1) {
if (s1[0] !== '.' && s1[1] !== '/') {
return ` from '/@modules/${s1}'`
} else {
return s0
}
})
}
转换后,再次请求,依旧报错
现在需要处理 3 种类型的请求:
- 模块:
/@modules/xxx
- vue 文件:
/xxx/xxx.vue
- 样式:
/xxx/xxx.css
处理 /@modules/xxx
请求
// /@modules的请求,会去node_modules里面查找模块
router.get(/^\/@modules/, async (ctx, next) => {
const {
request: { url },
} = ctx
// 模块:xxx/toy-vite/node_modules/vue
const modulePath = path.resolve(__dirname, 'node_modules', url.replace('/@modules/', ''))
// 模块文件位置,package文件中的module字段:dist/vue.runtime.esm-bundler.js
const module = require(`${modulePath}/package.json`).module
// 模块最终位置
const moduleFilePath = path.resolve(modulePath, module)
const content = fs.readFileSync(moduleFilePath, 'utf-8')
ctx.type = 'application/javascript'
ctx.body = rewriteImport(content)
})
处理 vue
文件,vue
文件中,包含 template
、js
、style
,需要分别处理
// 单文件组件解析,处理:import xx from 'xx.vue'
router.get(/\.vue$/, async (ctx, next) => {
const {
request: { url, query },
} = ctx
const vueFilePath = path.resolve(__dirname, url.split('?')[0].slice(1))
// 使用官方库@vue/compiler-sfc解析
const { descriptor } = compilerSfc.parse(fs.readFileSync(vueFilePath, 'utf-8'))
console.log('content:', descriptor.script.content)
if (!query.type) {
// 处理js内容
ctx.type = 'application/javascript'
ctx.body = `
${rewriteImport(descriptor.script.content.replace('export default ', 'const __script = '))}
import {render as __render} from "${url}?type=template"
__script.render = __render
export default __script
`
} else if (query.type === 'template') {
// 解析template,生成render函数
const template = descriptor.template
const render = compilerDom.compile(template.content, { mode: 'module' }).code
ctx.type = 'application/javascript'
ctx.body = rewriteImport(render)
}
})
处理 css
文件,生成 style
标签,插入到 head
中
// 处理css请求
router.get(/css$/, async (ctx, next) => {
const {
request: { url },
} = ctx
const p = path.resolve(__dirname, url.slice(1))
const file = fs.readFileSync(p, 'utf-8')
const content = `
// import {updateStyle} from '/vite/client'
const css = "${file.replace(/\n/g, '')}"
const link = document.createElement('style')
link.setAttribute('type', 'text/css')
document.head.appendChild(link)
link.innerHTML = css
export default css
`
ctx.type = 'application/javascript'
ctx.body = content
})
以上~