最近在学习vite的相关,简单实现了一下
项目文件
原始文件都在target文件夹中,.cache是后来生成的,实现的代码将放在src路径下,下面的就是react项目初始化的内容
请求的拦截处理
开启一个http服务,来拦截从浏览器过来的请求(知识点:type为module时,浏览器遇到import的时候会发一个请求去获取对应文件,请求地址为文件路径.同时,我们还要在index.html中塞入一个文件,这个文件后续将用于热更新的实现,作为websocket的客户端
const app = express()
const server = createServer(app)
const ws = createWebSocketServer(server);
app.get('/', (req, res) => {
res.set('Content-Type', 'text/html')
const htmlPath = join(__dirname, '../target', 'index.html')
let html = readFileSync(htmlPath, 'utf-8')
// 这里塞入一个客户端
html = html.replace('<head>', `<head>\n <script type="module" src="/@vite/client"> </script>`).trim()
res.send(html)
})
接下来我们将在dev.js中对浏览器的文件引入请求做拦截,主要是对.jsx文件和.css文件进行拦截
import express from 'express'
import { createServer } from 'http'
import { read, readFileSync } from 'fs'
import { transformCode, transformJSX, transformCss } from './transform'
app.get('/target/*', (req, res) => {
const filePath = join(__dirname, '..', req.path.slice(1))
// 这里是对静态资源的处理,后续会给.svg文件的请求的末尾加一个?import
if('import' in req.query){
res.set('Content-Type', 'application/javascript')
res.send(`export default "${req.path}"`)
return
}
switch(extname(req.path)){
case '.svg':
res.set('Content-Type', 'image/svg+xml')
res.send(
readFileSync(filePath, 'utf-8')
)
break
case '.css':
res.set('Content-Type', 'application/javascript')
res.send(
transformCss({
path: req.path,
code: readFileSync(filePath, 'utf-8')
})
)
break
default:
res.set('Content-Type', 'application/javascript')
res.send(
transformJSX({
appRoot: join(__dirname, '../target'),
path: req.path,
code: readFileSync(filePath, 'utf-8')
}).code
)
}
})
下面去实现transform系列方法,在transform.js中
import { transformSync } from 'esbuild'
import path, { join,extname, dirname } from 'path'
// 封装一个基于esbuild的代码处理函数
export function transformCode(opts){
return transformSync(opts.code, {
loader: opts.loader || 'js',
sourcemap: true,
format: 'esm'
})
}
// 如果是css,就要在页面浏览器页面上塞一个style标签
export function transformCss(opts){
return`
import { updateStyle } from '/@vite/client'
const id = "${opts.path}"
const css = "${opts.code.replace(/\n/g, '')}"
updateStyle(id, css)
export default css
`.trim()
}
// jsx文件esbuild解析后直接返回,但是要处理一下里面的代码引用,如果是node_modules的文件,从处理过的缓存里面那,如果是本地的,就直接拿
export function transformJSX(opts){
const ext = extname(opts.path).slice(1)
const ret = transformCode({
loader: ext,
code: opts.code
})
let { code } = ret
code = code.replace(
/\bimport(?!\s+type)(?:[\w*{}\n\r\t, ]+from\s*)?\s*("([^"]+)"|'([^']+)')/gm,
(a,b,c) => {
let from
if(c.charAt(0) === '.'){
from = join(dirname(opts.path), c)
from = JSON.parse(JSON.stringify(from).replace(/\\\\/g, "/"))
if(['svg'].includes(extname(from).slice(1))){
from = `${from}?import`
}
}else{
from = `/target/.cache/${c}/cjs/${c}.development.js`
}
return a.replace(b, `"${from}"`)
}
)
return {
...ret,
code
}
}
完善一下client.js中的相关函数
const sheetMap = new Map()
// 这是向页面塞style标签的实现
export function updateStyle(id,content){
let style = sheetMap.get(id)
if(!style){
style = document.createElement('style')
style.setAttribute('type', 'text/css')
style.innerHTML = content
document.head.appendChild(style)
}else{
style.innerHTML = content
}
sheetMap.set(id, style)
}
vite的依赖缓存
vite在项目启动的时候,会先把项目中的一些依赖缓存起来
// optmize.js中
import { build } from 'esbuild'
import { join } from 'path'
const appRoot = join(__dirname, '..')
// 缓存的路径为target/.cache文件夹中
const cache = join(appRoot, 'target', '.cache');
export async function optmize(pkgs = ['react', 'react-dom']){
const ep = pkgs.reduce((c, n) => {
c.push(join(appRoot, 'node_modules', n, `cjs/${n}.development.js`))
return c
}, [])
await build({
entryPoints: ep,
bundle: true,
format: 'esm',
logLevel: 'error',
splitting: true,
sourcemap: true,
outdir: cache,
treeShaking: 'ignore-annotations',
metafile: true,
define: {
"process.env.NODE_ENV": JSON.stringify("development")
}
})
}
热更新
热更新的实现是通过webSocket来实现的,dev.js中启动服务端,塞到浏览器中的client.js中有客户端,以此达到文件改变,通知浏览器的功能
// dev.js
import WebSocket from 'ws'
const targetRootPath = join(__dirname, '../target');
function createWebSocketServer(server){
const wss = new WebSocket.Server({ noServer: true})
server.on('upgrade', (req, socket, head) => {
if (req.headers['sec-websocket-protocol'] === 'vite-hmr') {
wss.handleUpgrade(req, socket, head, (ws) => {
wss.emit('connection', ws, req);
});
}
})
wss.on('connection', (socket) => {
socket.send(JSON.stringify({ type: 'connected'}))
})
wss.on('error', (e) => {
if(e.code !== 'EADDRINUSE'){
console.error(
chalk.red(`WebSocket server error:\n${e.stack || e.message}`)
)
}
})
return {
send(payload){
const stringified = JSON.stringify(payload)
wss.clients.forEach( client => {
if(client.readyState === WebSocket.OPEN){
client.send(stringified)
}
})
},
close() {
wss.close()
}
}
}
function watch () {
return chokidar.watch(targetRootPath, {
ignored: ['**/node_modeules/**', '**/.cache/**'],
ignoreInitial: true,
ignorePermissionErrors: true,
disableGlobbing: true
})
}
function getShortName(file, root){
return file.startsWith(root + '/') ? posix.relative(root, file) : file
}
function handleHMRUpdate(opts){
const {file, ws} = opts
const timestamp = Date.now()
// const shortFile = getShortName(file, targetRootPath)
const shortFile = '/App.jsx'
let updates
if(shortFile.endsWith('.css') || shortFile.endsWith('.jsx')){
updates = [
{
type: 'js-update',
timestamp,
path: `/${shortFile}`,
acceptedPath: `/${shortFile}`
}
]
}
ws.send({
type: 'update',
updates
})
}
// clent.js中
const host = location.host
const socket = new WebSocket(`ws://${host}`, `vite-hmr`)
socket.addEventListener('message', async ({data}) => {
handleMessage(JSON.parse(data)).catch(console.error);
})
async function handleMessage(payload){
switch(payload.type){
case 'connected':
console.log('[vite] connected.')
setInterval(() => {
socket.send('ping')
}, 3000)
break
case 'update':
console.log(payload)
payload.updates.forEach(async (update) => {
if(update.type === 'js-update'){
console.log('[vite] is update')
await import(`/target/${update.path}?t=${update.timestamp}`)
location.reload()
}
})
break;
}
}
启动的时候,先执行optmize文件,再执行dev.js 成功启动项目。