vue-dev-server 源码解析

82 阅读3分钟

vue-dev-server 源码解析

vue-dev-server 介绍

vue-dev-server是尤大编写的一个小工具,功能一句话介绍

This is a proof of concept.

Imagine you can import Vue single-file components natively in your browser... without a build step.

how it works

  • Imports are requested by the browser as native ES module imports - there's no bundling.
  • The server intercepts requests to *.vue files, compiles them on the fly, and sends them back as JavaScript.
  • For libraries that provide ES modules builds that work in browsers, just directly import them from a CDN.
  • Imports to npm packages inside .js files (package name only) are re-written on the fly to point to locally installed files. Currently, only vue is supported as a special case. Other packages will likely need to be transformed to be exposed as a native browser-targeting ES module.

我来简单翻译下就是,翻译的不好见谅

  • 没有打包,浏览器直接使用 ES module 导入
  • 服务拦截 *.vue 的文件请求,即时编译,然后返回浏览器对应的 js
  • 对于那些在浏览器提供 ES modules 构建的库来说,只需要从CDN导入
  • 对于vue文件内的 导入 会动态重写用来支持本地安装的那些库

老规矩,看库先看test,站在2022年这个角度,还是很熟悉的用法。

测试比较简单就三个文件: index.html , main.js , test.vue

接着找下 package.json ,看看命令

"bin": { "vue-dev-server": "./bin/vue-dev-server.js" },

"scripts": { "test": "cd test && node ../bin/vue-dev-server.js" }

也很简单,直接指向了 bin/vue-dev-server.js

#!/usr/bin/env nodeconst 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')
})
​

这里很好理解起了个服务

中间件 vueMiddleware

设置了 静态文件目录就当前目录

重点看下 vueMiddleware

const vueCompiler = require('@vue/component-compiler')
const fs = require('fs')
const stat = require('util').promisify(fs.stat)
const root = process.cwd()
const path = require('path')
const parseUrl = require('parseurl')
const { transformModuleImports } = require('./transformModuleImports')
const { loadPkg } = require('./loadPkg')
const { readSource } = require('./readSource')
​
const defaultOptions = {
  cache: true
}
​
const vueMiddleware = (options = defaultOptions) => {
  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 }
    })
  }
​
  const compiler = vueCompiler.createDefaultCompiler()
​
  function send(res, source, mime) {
    res.setHeader('Content-Type', mime)
    res.end(source)
  }
​
  function injectSourceMapToBlock (block, lang) {
    const map = Base64.toBase64(
      JSON.stringify(block.map)
    )
    let mapInject
​
    switch (lang) {
      case 'js': mapInject = `//# sourceMappingURL=data:application/json;base64,${map}\n`; break;
      case 'css': mapInject = `/*# sourceMappingURL=data:application/json;base64,${map}*/\n`; break;
      default: break;
    }
​
    return {
      ...block,
      code: mapInject + block.code
    }
  }
​
  function injectSourceMapToScript (script) {
    return injectSourceMapToBlock(script, 'js')
  }
​
  function injectSourceMapsToStyles (styles) {
    return styles.map(style => injectSourceMapToBlock(style, 'css'))
  }
  
  async function tryCache (key, checkUpdateTime = true) {
    const data = cache.get(key)
​
    if (checkUpdateTime) {
      const cacheUpdateTime = time[key]
      const fileUpdateTime = (await stat(path.resolve(root, key.replace(/^//, '')))).mtime.getTime()
      if (cacheUpdateTime < fileUpdateTime) return null
    }
​
    return data
  }
​
  function cacheData (key, data, updateTime) {
    const old = cache.peek(key)
​
    if (old != data) {
      cache.set(key, data)
      if (updateTime) time[key] = updateTime
      return true
    } else return false
  }
​
    // 解析单文件组件
  async function bundleSFC (req) {
      // 读取文件
    const { filepath, source, updateTime } = await readSource(req)
    const descriptorResult = compiler.compileToDescriptor(filepath, source)
    const assembledResult = vueCompiler.assemble(compiler, filepath, {
      ...descriptorResult,
      script: injectSourceMapToScript(descriptorResult.script),
      styles: injectSourceMapsToStyles(descriptorResult.styles)
    })
    return { ...assembledResult, updateTime }
  }
​
  return async (req, res, next) => {
    if (req.path.endsWith('.vue')) {      
        // 处理vue文件 bundle 后返回 javascript
      const key = parseUrl(req).pathname
      let out = await tryCache(key)
​
      if (!out) {
        // Bundle Single-File Component
        const result = await bundleSFC(req)
        out = result
        cacheData(key, out, result.updateTime)
      }
      
      send(res, out.code, 'application/javascript')
    } else if (req.path.endsWith('.js')) {
        // 处理javascript 文件 使用缓存 提高效率 返回  javascript
      const key = parseUrl(req).pathname
      let out = await tryCache(key)
​
      if (!out) {
        // transform import statements
        const result = await readSource(req)
        out = transformModuleImports(result.source)
        cacheData(key, out, result.updateTime)
      }
​
      send(res, out, 'application/javascript')
    } else if (req.path.startsWith('/__modules/')) {
        // 处理 modules 下的路径 尝试缓存  loadPkg 只对 vue 做了处理 返回 javascript
      const key = parseUrl(req).pathname
      const pkg = req.path.replace(/^/__modules//, '')
​
      let out = await tryCache(key, false) // Do not outdate modules
      if (!out) {
        out = (await loadPkg(pkg)).toString()
        cacheData(key, out, false) // Do not outdate modules
      }
​
      send(res, out, 'application/javascript')
    } else {
      next()
    }
  }
}
​
exports.vueMiddleware = vueMiddleware
​

到这里基本的解析就完成了,其实做的很简单,

  • 重复利用缓存,减少io
  • 充分的封装,保持主流程的清晰,与主流程无关的都封装出去,转移出去
  • 逻辑清晰,扩展容易,后续要扩展react 也很清楚要在哪里加