实现一个简易的vite-处理node_modules

366 阅读2分钟

一、ESmodule

在说vite之前要先讲一下ESmodule,vite核心的原理就是利用了目前浏览器支持ESmodule模块化,新建四个文件,index.html、main.js、module_a.js、module_b.js,内容如下

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <script type="module" src="./main.js"></script>
</body>
</html>
import a from './module_a.js'
console.log('main')
import b from './module_b.js'
console.log('a')
export default 'a'
console.log('b')
export default 'b'

通过livesever打开,浏览器环境表现如下

image.png

image.png

image.png

image.png

二、搭建静态服务器

vite内部使用的koa搭建静态服务器,koa中文网www.koajs.com.cn/

安装koa和koa-send

image.png

#! /usr/bin/env node
const Koa = require('koa')
const app = new Koa()
app.use(async (ctx, next) => {
  ctx.body = 'Hello world'
})
app.listen(3000)
console.log('http://localhost:3000/')

ctx中的内容就是request和response中的内容,大体如下

image.png

ctx在调用的时候有一些语法糖

image.png

至此,我们的已经用koa搭起一个简单的服务器了,现在我们借助koa-send向浏览器推送文件

#! /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('Sever running @ http://localhost:3000/')

用npm link本地绑定一下我们的my-vite,运行npm create vue创建一个vue3项目,运行my-vite启动服务器,可以看到我们已经能正常拿到资源了,但是控制台报错

image.png

image.png

这是因为我们的vue项目的main.js中的代码中引用了第三方模块

image.png 浏览器中的ESmodule要求我们在使用的时候需要相对路径以/或./或../开头,但是我们在引用第三方模块时省略了前面的内容

三、修改第三方模块的路径

我们新增一个中间件,用于处理第三方的模块路径,我们首先要去获取文件的内容,然后通过改写文件内容的方式去处理第三方的模块路径,读取文件内容的时候是流式读取的,所以我们创建一个方法streamToString用于读取文件内容,在匹配from ‘后没有./和../开头的内容进行处理转译

app.use(async (ctx, next) => {
  if (ctx.type === 'application/javascript') {
    const context = await streamToString(ctx.body)
    ctx.body = context.replace(/(from\s+['"])(?!./)/g,'$1/@modules/')
  }
  await next()
})
const streamToString = (stream) => new Promise((resolve, reject) => {
  const buffers = []
  stream.on('data', (buffer) => { buffers.push(buffer) })
  stream.on('end', () => { resolve(Buffer.concat(buffers).toString('utf-8')) })
  stream.on('error', reject)
})

至此我们浏览器请求可以看到main.js中的文件内容已经改变,但是因为/@modules还是虚假的,所以暂时还请求不到第三方模块的文件

image.png

四、加载第三方模块

所以我们在给路径分配文件之前再新增一个中间件来重改这个请求路径,通过截取拿到我们要导入包,获取它的package.json去拿到module,ESmodule模块化指定的入口文件

app.use(async (ctx, next) => {
  if (ctx.path.startsWith('/@modules/')) {
    const moduleName = ctx.path.slice(10)
    const packPath = path.join(process.cwd(), 'node_modules', moduleName, 'package.json')
    const pkg = require(packPath)
    ctx.path = path.join('/node_modules', moduleName, pkg.module)
  }
  await next()
})

至此整个文件如下

#! /usr/bin/env node
const Koa = require('koa')
const send = require('koa-send')
const path = require('path')
const app = new Koa()

// readStream 流式数据转化为字符串
const streamToString = (stream) => new Promise((resolve, reject) => {
  const buffers = []
  stream.on('data', (buffer) => { buffers.push(buffer) })
  stream.on('end', () => { resolve(Buffer.concat(buffers).toString('utf-8')) })
  stream.on('error', reject)
})

// 3.加载第三方模块
app.use(async (ctx, next) => {
  if (ctx.path.startsWith('/@modules/')) {
    const moduleName = ctx.path.slice(10)
    const packPath = path.join(process.cwd(), 'node_modules', moduleName, 'package.json')
    const pkg = require(packPath)
    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()
})
// 2.处理文件前缀
app.use(async (ctx, next) => {
  if (ctx.type === 'application/javascript') {
    const context = await streamToString(ctx.body)
    ctx.body = context.replace(/(from\s+['"])(?!./)/g,'$1/@modules/')
  }
  await next()
})
app.listen(3000)
console.log('Sever running @ http://localhost:3000/')

现在我们可以拿到对应的文件了

image.png

但是我们打开控制台可以发现一个报错,一个是无法识别css,还有一个是无法识别vue文件,这个就是单文件组建和css文件的问题了,本文不做详解。

image.png