likes
comments
collection
share

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

作者站长头像
站长
· 阅读数 55

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

这是源码共读的第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')
})

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

promisify

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

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

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

这里使用了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文件的处理
    }

最后看到 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')
    }

这里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')
    }

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

总结

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