理解Vite原理 – vue-dev-server源码浅析

本文参加了由公众号@若川视野 发起的每周源码共读活动,      点击了解详情一起参与。

这是源码共读的第11期 | 尤雨溪几年前写的100多行的玩具 vite

前言

在Vue官方Github组织下有一个仓库: vue-dev-server, 总计代码不到200行,展示了通过ES Module导入的方式是如何解析Vue单文件组件的,也算Vite最初的雏形

github.com/vuejs/vue-d…

Vite官方文档:Vite 以 原生 ESM 方式提供源码。这实际上是让浏览器接管了打包程序的部分工作:Vite 只需要在浏览器请求源码时进行转换并按需提供源码。根据情景动态导入代码,即只在当前屏幕上实际使用时才会被处理

源码

入口文件

#!/usr/bin/env node
const express = require('express')
const { vueMiddleware } = require('../middleware')
const app = express()
const root = process.cwd();
app.use(vueMiddleware())
app.use(express.static(root))
app.listen(3000, () => {
console.log('server running at http://localhost:3000')
})
#!/usr/bin/env node

const express = require('express')
const { vueMiddleware } = require('../middleware')

const app = express()
const root = process.cwd();

app.use(vueMiddleware())

app.use(express.static(root))


app.listen(3000, () => {
  console.log('server running at http://localhost:3000')
})
#!/usr/bin/env node const express = require('express') const { vueMiddleware } = require('../middleware') const app = express() const root = process.cwd(); app.use(vueMiddleware()) app.use(express.static(root)) app.listen(3000, () => { console.log('server running at http://localhost:3000') })

使用了Express作为服务端,Use了一个中间件, 这个中间件就是解析Vue单文件组件的核心

promisify

const stat = require('util').promisify(fs.stat)
const stat = require('util').promisify(fs.stat)
const stat = require('util').promisify(fs.stat)

在14期有说过 promisify的用法,juejin.cn/post/713974…, 这里就是将 fs.stat这个方法转为Promise的形式调用

loadPkg

const fs = require('fs')
const path = require('path')
const readFile = require('util').promisify(fs.readFile)
async function loadPkg(pkg) {
//只有传入的值为 vue字符 才处理
if (pkg === 'vue') {
const dir = path.dirname(require.resolve('vue'))
//dir 取vue包路径, 拼接vue.esm.browser.js 拼接浏览器能识别的esm格式产物文件
const filepath = path.join(dir, 'vue.esm.browser.js')
//返回 读取这个文件的promise
return readFile(filepath)
}
else {
// TODO
// check if the package has a browser es module that can be used
// otherwise bundle it with rollup on the fly?
throw new Error('npm imports support are not ready yet.')
}
}
exports.loadPkg = loadPkg
const fs = require('fs')
const path = require('path')
const readFile = require('util').promisify(fs.readFile)




async function loadPkg(pkg) {
  //只有传入的值为 vue字符 才处理
  if (pkg === 'vue') {
    const dir = path.dirname(require.resolve('vue'))
    //dir 取vue包路径, 拼接vue.esm.browser.js  拼接浏览器能识别的esm格式产物文件
    const filepath = path.join(dir, 'vue.esm.browser.js')
    //返回 读取这个文件的promise
    return readFile(filepath)
  }
  else {
    // TODO
    // check if the package has a browser es module that can be used
    // otherwise bundle it with rollup on the fly?
    throw new Error('npm imports support are not ready yet.')
  }
}

exports.loadPkg = loadPkg
const fs = require('fs') const path = require('path') const readFile = require('util').promisify(fs.readFile) async function loadPkg(pkg) { //只有传入的值为 vue字符 才处理 if (pkg === 'vue') { const dir = path.dirname(require.resolve('vue')) //dir 取vue包路径, 拼接vue.esm.browser.js 拼接浏览器能识别的esm格式产物文件 const filepath = path.join(dir, 'vue.esm.browser.js') //返回 读取这个文件的promise return readFile(filepath) } else { // TODO // check if the package has a browser es module that can be used // otherwise bundle it with rollup on the fly? throw new Error('npm imports support are not ready yet.') } } exports.loadPkg = loadPkg

loadPkg 的作用就是读取了 node_modules下 vue的产物文件 并返回

readSource

const path = require('path')
const fs = require('fs')
const readFile = require('util').promisify(fs.readFile)
const stat = require('util').promisify(fs.stat)
const parseUrl = require('parseurl')
const root = process.cwd()
async function readSource(req) {
//得到文件名
const { pathname } = parseUrl(req)
//拼接命令执行目录 移除开头的 / 字符
const filepath = path.resolve(root, pathname.replace(/^\//, ''))
return {
filepath,
source: await readFile(filepath, 'utf-8'),
updateTime: (await stat(filepath)).mtime.getTime()
}
}
exports.readSource = readSource
const path = require('path')
const fs = require('fs')
const readFile = require('util').promisify(fs.readFile)

const stat = require('util').promisify(fs.stat)
const parseUrl = require('parseurl')
const root = process.cwd()

async function readSource(req) {
  //得到文件名
  const { pathname } = parseUrl(req)
  //拼接命令执行目录 移除开头的 / 字符
  const filepath = path.resolve(root, pathname.replace(/^\//, ''))
  return {
    filepath,
    source: await readFile(filepath, 'utf-8'),
    updateTime: (await stat(filepath)).mtime.getTime()
  }
}

exports.readSource = readSource
const path = require('path') const fs = require('fs') const readFile = require('util').promisify(fs.readFile) const stat = require('util').promisify(fs.stat) const parseUrl = require('parseurl') const root = process.cwd() async function readSource(req) { //得到文件名 const { pathname } = parseUrl(req) //拼接命令执行目录 移除开头的 / 字符 const filepath = path.resolve(root, pathname.replace(/^\//, '')) return { filepath, source: await readFile(filepath, 'utf-8'), updateTime: (await stat(filepath)).mtime.getTime() } } exports.readSource = readSource

readSource 根据传入的文件路径, 拼接执行命名的目录 得到完整路径进行文件的读取 并返回

transformModuleImports

const recast = require('recast')
const isPkg = require('validate-npm-package-name')
function transformModuleImports(code) {
const ast = recast.parse(code)
recast.types.visit(ast, {
visitImportDeclaration(path) {
const source = path.node.source.value
//将import 的路径 不包含. 并且符合 package name格式的 替换为 /_modules/前缀
if (!/^\.\/?/.test(source) && isPkg(source)) {
path.node.source = recast.types.builders.literal(`/__modules/${source}`)
}
this.traverse(path)
}
})
return recast.print(ast).code
}
exports.transformModuleImports = transformModuleImports
const recast = require('recast')
const isPkg = require('validate-npm-package-name')

function transformModuleImports(code) {
  const ast = recast.parse(code)
  recast.types.visit(ast, {
    visitImportDeclaration(path) {
      const source = path.node.source.value
      //将import 的路径 不包含. 并且符合 package name格式的 替换为 /_modules/前缀
      if (!/^\.\/?/.test(source) && isPkg(source)) {
        path.node.source = recast.types.builders.literal(`/__modules/${source}`)
      }

      this.traverse(path)
    }

  })
  return recast.print(ast).code
}

exports.transformModuleImports = transformModuleImports
const recast = require('recast') const isPkg = require('validate-npm-package-name') function transformModuleImports(code) { const ast = recast.parse(code) recast.types.visit(ast, { visitImportDeclaration(path) { const source = path.node.source.value //将import 的路径 不包含. 并且符合 package name格式的 替换为 /_modules/前缀 if (!/^\.\/?/.test(source) && isPkg(source)) { path.node.source = recast.types.builders.literal(`/__modules/${source}`) } this.traverse(path) } }) return recast.print(ast).code } exports.transformModuleImports = transformModuleImports

这里使用了recast 将 文件代码中的import 路径 进行了处理,比如 main.js 内部的import Vue from 'vue' 会替换为 import Vue from '/__modules/vue'

文件的处理

if (req.path.endsWith('.vue')) {
//得到文件名
const key = parseUrl(req).pathname
let out = await tryCache(key)
//从缓存取出 时间对比 不存在就 通过 complier 编译这个vue单文件
if (!out) {
// Bundle Single-File Component
const result = await bundleSFC(req)
out = result
cacheData(key, out, result.updateTime)
}
send(res, out.code, 'application/javascript')
// js文件的处理
}
if (req.path.endsWith('.vue')) {
      //得到文件名
      const key = parseUrl(req).pathname
      let out = await tryCache(key)
      //从缓存取出 时间对比 不存在就 通过 complier 编译这个vue单文件
      if (!out) {

        // Bundle Single-File Component
        const result = await bundleSFC(req)
        out = result
        cacheData(key, out, result.updateTime)
      }



      send(res, out.code, 'application/javascript')
      // js文件的处理
    }
if (req.path.endsWith('.vue')) { //得到文件名 const key = parseUrl(req).pathname let out = await tryCache(key) //从缓存取出 时间对比 不存在就 通过 complier 编译这个vue单文件 if (!out) { // Bundle Single-File Component const result = await bundleSFC(req) out = result cacheData(key, out, result.updateTime) } send(res, out.code, 'application/javascript') // js文件的处理 }

最后看到 vueMiddleware 会返回一个 Async函数, 内部会对不同类型下进行处理
如果是 Vue单文件, 会通过 bundleSFC 也就是Complier 进行代码编译,转为JS能处理的格式,最后Send 函数 将结果以及 文件类型 返回交给 Res, 浏览器就会拿到对应的代码内容

if (req.path.endsWith('.js')) {
const key = parseUrl(req).pathname
let out = await tryCache(key)
if (!out) {
// transform import statements
//得到js文件内容
const result = await readSource(req)
//通过recast 转换内部的 import 路径
out = transformModuleImports(result.source)
cacheData(key, out, result.updateTime)
}
//将处理后的结果返回Res
send(res, out, 'application/javascript')
}
 if (req.path.endsWith('.js')) {
      const key = parseUrl(req).pathname

      let out = await tryCache(key)



      if (!out) {
        // transform import statements
      //得到js文件内容
        const result = await readSource(req)
        //通过recast 转换内部的 import 路径
        out = transformModuleImports(result.source)
        cacheData(key, out, result.updateTime)
      }

      //将处理后的结果返回Res
      send(res, out, 'application/javascript')
    }
if (req.path.endsWith('.js')) { const key = parseUrl(req).pathname let out = await tryCache(key) if (!out) { // transform import statements //得到js文件内容 const result = await readSource(req) //通过recast 转换内部的 import 路径 out = transformModuleImports(result.source) cacheData(key, out, result.updateTime) } //将处理后的结果返回Res send(res, out, 'application/javascript') }

这里js 主要做的就是 将 import 的非.字符和符合package name的路径替换为_modules/ 开头的路径

if (req.path.startsWith('/__modules/')) {
const key = parseUrl(req).pathname
const pkg = req.path.replace(/^\/__modules\//, '')
let out = await tryCache(key, false) // Do not outdate modules
if (!out) {
//loadPkg 就是从node_modules 里面读取 vue的产物文件
out = (await loadPkg(pkg)).toString()
//不需要时间对比 默认缓存
cacheData(key, out, false) // Do not outdate modules
}
//将产物代码返回,识别为JS格式
send(res, out, 'application/javascript')
}
if (req.path.startsWith('/__modules/')) {
      const key = parseUrl(req).pathname

      const pkg = req.path.replace(/^\/__modules\//, '')



      let out = await tryCache(key, false) // Do not outdate modules
      if (!out) {

      //loadPkg 就是从node_modules 里面读取 vue的产物文件
        out = (await loadPkg(pkg)).toString()
        //不需要时间对比 默认缓存
        cacheData(key, out, false) // Do not outdate modules
      }

        //将产物代码返回,识别为JS格式
      send(res, out, 'application/javascript')
    }
if (req.path.startsWith('/__modules/')) { const key = parseUrl(req).pathname const pkg = req.path.replace(/^\/__modules\//, '') let out = await tryCache(key, false) // Do not outdate modules if (!out) { //loadPkg 就是从node_modules 里面读取 vue的产物文件 out = (await loadPkg(pkg)).toString() //不需要时间对比 默认缓存 cacheData(key, out, false) // Do not outdate modules } //将产物代码返回,识别为JS格式 send(res, out, 'application/javascript') }

经过前面JS文件的处理后,Express读取到前面JS文件内部替换的路径格式,再将内容返回给浏览器,至此就结束了,此时浏览器就能读取和识别Vue的单文件组件了

总结

  1. 浏览器访问 JS Vue文件时候,通过 recast处理 js 内部的 非文件路径的包引入 转为__modules开头的路径、通过Vue Complier处理 Vue文件的代码, 返回给浏览器
  2. 浏览器访问 modules路径的文件,则通过 loadPkg 读取Vue内的产物文件代码返回给浏览器使用,用于加载Vue
  3. 核心就是通过Exporess 的中间件 针对不同格式的文件进行了处理,使得客户端可以正确的加载到文件对应代码

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MYnwMei3' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片