vuejs / vue-dev-server源码阅读

214 阅读4分钟

引言

vite简易版,vue-dev-server阅读源码记录

从代码里面摘出来几个重点,部分已经梳理完毕,还有一些陆续会梳理掉。主要介绍的点有:

  • vite冷启动流程
  • readSource函数
  • parseUrl依赖
  • recast依赖
  • recast实践
  • validate-npm-package-name依赖
  • transformModuleImports函数
  • cacheData函数
  • @vue/component-compiler依赖(单独拎出来讲吧)
  • esbuild生产环境用之后再说

重点介绍

vite冷启动流程

通过梳理vue-dev-server源码,理解了vite在本地开发时的流程。首先,启动开发服务器,然后利用新一代浏览器的ESM能力,直接可以识别import,让浏览器去分析模块之间的依赖关系(webpack本地启动会自己去分析import的依赖关系),无需打包,直接请求所需模块并实时编译(将.vue文件转成浏览器认识的js文件)

vite介绍及实现原理参考文章

readSource parseUrl

readSource.js源码,导出一个readSource方法,该方法接收express捕获的请求req对象作为入参,返回文件绝对路径和readFile后的文件内容,以及更新时间,应该是用来缓存用。这个文件比较简单

const path = require('path')
const fs = require('fs')
// promisify Node.js 内置的 util 模块有一个 promisify() 方法
// 该方法将基于回调的函数转换为基于 Promise 的函数。
// 这使您可以将 Promise 链和 async/await 与基于回调的 API 结合使用
const readFile = require('util').promisify(fs.readFile)
const stat = require('util').promisify(fs.stat)
// 解析url,返回url param
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

recast validate-npm-package-name

recast

这个内容比较多了,涉及到了ast,仓库链接github.com/benjamn/rec…

主要就是把js和ast之前转来转去,在转的时候做一些事情,具体使用可以看看参考文章里面的recast实际使用,方便理解,最好动手实践一下

validate-npm-package-name

判断是不是npm市场的依赖,字面意思就能理解

transformModuleImports

导出一个transformModuleImports方法,接收readFile获取的code,这个方法就一个作用,将main.js 中的 import 语句 import Vue from 'vue' 通过 recast 生成 ast 转换成 import Vue from "/__modules/vue" 。

这么做的目的就是最后会调用loadPkg方法将 vue-dev-server/node_modules/vue/dist/vue.esm.browser.js返回给浏览器

import Vue from 'vue' => import Vue from "/__modules/vue"
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
      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

cacheData、tryCache函数

举个例子,在核心文件middleware.js中有这么一段代码

  // 判断浏览器请求的文件是否为js
  if (req.path.endsWith('.js')) {
   // 取文件路径作为缓存的key
   const key = parseUrl(req).pathname
   // 读取缓存
   let out = await tryCache(key)
   // 没有找到的话就写入缓存
   if (!out) {
     // readSource上面介绍了,返回文件信息对象
     const result = await readSource(req)
     // transformModuleImports也不多说了
     out = transformModuleImports(result.source)
     // 写入缓存
     cacheData(key, out, result.updateTime)
   }
   // 请求成功
   send(res, out, 'application/javascript')
 } 

里面用到了两个重要的函数,tryCache读取缓存,cacheData写入缓存。缓存的实现原理是使用了lru-cache库(最近最少使用),听说面试会问这个,之后看看源码到底是怎么做的。

const vueMiddleware = (options = { cache = true }) => {
  let cache
  // 存放文件的更新时间,如果文件没有更新则不去读写缓存
  let time = {}
  if (options.cache) {
    // 核心库
    const LRU = require('lru-cache')

    cache = new LRU({
      // 缓存最大数量(应该是文件数量没仔细看解释)
      max: 500,
      // 不知道干啥的
      length: function (n, key) { return n * 2 + key.length }
    })
  }
  
  function cacheData (key, data, updateTime) {
    // peek与get类似,应该都是读取,不知道这里为什么还要再判断一下
    const old = cache.peek(key)
      
    if (old != data) {
      cache.set(key, data)
      if (updateTime) time[key] = updateTime
      return true
    } else return false
  }
  
  async function tryCache (key, checkUpdateTime = true) {
    // 读取缓存,没有的话返回undefined
    const data = cache.get(key)
    // 这里判断了一下如果文件的更新时间晚于缓存更新时间,那么就直接return出去不管有没有缓存,重新写入缓存
    if (checkUpdateTime) {
      const cacheUpdateTime = time[key]
      const fileUpdateTime = (await stat(path.resolve(root, key.replace(/^\//, '')))).mtime.getTime()
      if (cacheUpdateTime < fileUpdateTime) return null
    }

    return data
  }
  
}

参考文章