likes
comments
collection
share

【源码共读】第11期 | 从vue-dev-server中学到了vue文件到html页面渲染过程中发生了什么

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

前言

清单

  • 学会使用 VSCode 调试源码
  • 学会如何编译 Vue 单文件组件
  • 学会如何使用 recast 生成 ast 转换文件
  • 如何加载包文件

入口

俗话说,“万事开头难”,那是因为大部分人不知道是如何开头的,不知道从哪里开始。 读了好几期源码后,我悟道了如何开头,即先从项目目录开始,从package.json开始了解项目,找到入口文件所在

了解项目目录

【源码共读】第11期 | 从vue-dev-server中学到了vue文件到html页面渲染过程中发生了什么

  • 从目录中看有这几个文件需要了解:
    • README.md文件中了解该库的使用方法和场景
    • package.json文件中找到入口文件和项目启动方式
    • test目录一般是测试文件所在地
    • bin目录在nodejs中一般是作为入口处(这里猜测是和node一样),如何验证呢

看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指向./bin/vue-dev-server.js这个文件夹,说明bin目录下的vue-dev-server.js即这个项目的入口文件;

然后在scripts配置项中可知npm run test即项目的启动方式,启动的过程中先进入到test文件目录,然后通过node命令启动了./bin/vue-dev-server.js这个文件

流程

上面通过查看项目目录package.json了解到了项目的入口文件和启动方式,接下来就看下主流程都做了什么

// bin/vue-dev-server.js
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启动了一个服务 端口3000 
 */

既然启动了一个本地服务器http://localhost:3000,那么打开这个网址看看都发生了什么?

【源码共读】第11期 | 从vue-dev-server中学到了vue文件到html页面渲染过程中发生了什么 输入网址后,看到当前网址请求了四个资源分别是localhostmain.jsvuetest.vue,那么为什么会请求这四个资源呢?从下图断点处可以看到:

【源码共读】第11期 | 从vue-dev-server中学到了vue文件到html页面渲染过程中发生了什么 express的静态目录指向了test目录,网址默认优先加载index.html

【源码共读】第11期 | 从vue-dev-server中学到了vue文件到html页面渲染过程中发生了什么index.html的script标签中引入了main.js文件资源,那么再看main.js中内容都是什么?

【源码共读】第11期 | 从vue-dev-server中学到了vue文件到html页面渲染过程中发生了什么 通过上图可知,main.js中使用了vue的写法,引入Vue资源和test.vue文件,并把test.vue资源挂载到#app节点上;

到这里为止,index.html所需是所有资源和页面请求中请求到的资源一一对应上了,但是点开请求的资源,发现并不是和代码中写法完全一致,比如:

【源码共读】第11期 | 从vue-dev-server中学到了vue文件到html页面渲染过程中发生了什么

【源码共读】第11期 | 从vue-dev-server中学到了vue文件到html页面渲染过程中发生了什么 这?发生了什么?在什么时候被改变了呢?

再回头看请求到资源之前发生了什么? app.use(vueMiddleware()) 看到这里使用了一个vueMiddleware函数,作为express的中间件,接着去看vueMiddleware函数中做了什么?

【源码共读】第11期 | 从vue-dev-server中学到了vue文件到html页面渲染过程中发生了什么 通过对app.use(vueMiddleware()) 打断点,一步步执行,发现走到了这里

【源码共读】第11期 | 从vue-dev-server中学到了vue文件到html页面渲染过程中发生了什么 这里通过对请求资源路径的判断,对资源加以处理并通过send函数返回处理后的结果,这样以来似乎资源中途改变找到了原因!至此主流程的分析结束,接下来对细节进行分析~

细节

中间件

中间件是什么?其实就是服务端接收到请求资源的信息后,在返回信息给客户端之前,中间拦截了资源后做了某些资源处理的过程就是中间件的作用

中间件在express中就是一个fucntion(req,res,next){},其中req就是请求的资源相关信息,res可以用来返回资源,next就是这个函数通过后,把控制权交给下一个环节

请求main.js的时候发生了什么?

if (req.path.endsWith(".js")) { // 处理js后缀的文件
      const key = parseUrl(req).pathname; // /main.js
      let out = await tryCache(key); //从缓存中拿数据结果

      if (!out) { // 缓存中没数据的时候,重新生成数据,并存入缓存中
        // transform import statements
        const result = await readSource(req);
        out = transformModuleImports(result.source); // 做的最主要的作用是整个js,把main.js中的vue替换成import Vue from "/__modules/vue"
        cacheData(key, out, result.updateTime); // 存储数据 把文件名为key  ast整合后的文件内容为value,文件的最后更新时间为最新更新时间
      } 
      send(res, out, "application/javascript"); // js资源发送给客户端
    } 
  • js后缀的文件,获取路径名main.js作为key去缓存中拿数据,如果缓存中没有数据,则重新读取main.js文件资源
  • 读取到文件资源后,交给transformModuleImports函数处理,拿到处理结果后,先根据key缓存一份,然后发送资源给客户端

接下来看看tryCache做了什么?

  // 功能:拿缓存中的数据
  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;
  }

看完了tryCache再看下readSource函数做了什么?

// 作用:处理文件,并返回当前文件全路径和文件流 更新时间
async function readSource(req) {
  const { pathname } = parseUrl(req) // 拿路径
  const filepath = path.resolve(root, pathname.replace(/^\//, '')) // 文件路径组合 当前进程+路径 
  return {
    filepath, // D:\2023\vue-dev-server-analysis\vue-dev-server\test\main.js
    source: await readFile(filepath, 'utf-8'), // 读取的main.js的文件流
    updateTime: (await stat(filepath)).mtime.getTime() // 获取当前文件的更新时间,修改时间
  }
}

这个函数主要是返回文件全路径、main.js文件流、文件更新时间,接着核心地方来了transformModuleImports函数

// 作用:js 处理import并转换成js
function transformModuleImports(code) { 
  const ast = recast.parse(code) // ast 
  recast.types.visit(ast, {
    visitImportDeclaration(path) {
      const source = path.node.source.value 
      if (!/^\.\/?/.test(source) && isPkg(source)) { // 主要处理import xxx from xxx 并且不是相对路径的 并且在package.json文件中存在的依赖包
        path.node.source = recast.types.builders.literal(`/__modules/${source}`) // 主要处理package.json中第三方依赖的路径问题,重新定义新的路径
      }
      this.traverse(path)
    }
  })
  return recast.print(ast).code
}

exports.transformModuleImports = transformModuleImports
/**
 * recast:
 *  作用:用于操作js代码的AST抽象语法树,AST是一个表示代码结构的树形结构,通过解析js代码生成
 *  1.解析js代码并生成AST
 *  2.遍历和查询AST节点,进行静态代码分析
 *  3.修改AST节点,进行重构和转换操作,如添加、删除、重命名和移除节点等操作
 *  4.将修改后的AST重新生成js代码
 */

当js处理的时候发现了需要加载的新资源vuetest.vue,按照顺序,需要先处理vue资源

第三方库vue资源是如何处理的呢

 if (!/^\.\/?/.test(source) && isPkg(source)) { // 主要处理import xxx from xxx 并且不是相对路径的 并且在package.json文件中存在的依赖包
        path.node.source = recast.types.builders.literal(`/__modules/${source}`) // 主要处理package.json中第三方依赖的路径问题,重新定义新的路径
      }
  • 在处理js资源的时候,需要第三方库vue就处理成了/__modules/vue这样的,如main.js中import Vue from "/__modules/vue"
  • 而中间件拦截的地方有个关于/__modules/的判断,如下:
if (req.path.startsWith("/__modules/")) { // 处理/__modules/相关的文件逻辑
      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(); // 读取的是vue.esm.browser.js文件流
        cacheData(key, out, false); // Do not outdate modules 存放当前文件名和数据 不用设置日期
      }

      send(res, out, "application/javascript");
    }
  • 同js类似的步骤,先从缓存中拿数据,没拿到,就通过loadPkg函数加载数据,然后再次缓存数据,最后发给客户端资源
  • 其中如何加载vue数据的呢
// 作用:加载处理vue包
async function loadPkg(pkg) {
  if (pkg === 'vue') { 
  // require.resolve('vue') D:\2023\vue-dev-server-analysis\vue-dev-server\node_modules\vue\dist\vue.runtime.common.js
    const dir = path.dirname(require.resolve('vue'))
    // require.resolve():获取指定模块的绝对路径  path.dirname:获取该路径所在的目录路径
    const filepath = path.join(dir, 'vue.esm.browser.js') // 拼接成浏览器支持的文件路径
    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.')
  }
}

到这里就清楚的知道了为什么能加载到node_modulues中的vue资源了,接下来还有一个test.vue资源的加载处理,这个是如何做的呢?

.vue文件的处理

 if (req.path.endsWith(".vue")) { // 处理vue后缀的文件
      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");
    }
  • 同样代码四步走:拿缓存数据---没有则对.vue文件处理bundleSFC---处理后的数据,缓存----发送给客户端
  • 其他都不说了,就重点说下bundleSFC函数
  // 作用:编译成纯js相关的文件
  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 };
  }

通过断点可以看到@vue/component-compiler库加载了test.vue后,形成的数据解构和结果,然后通过vueCompiler.assemble方法拿到的结果

【源码共读】第11期 | 从vue-dev-server中学到了vue文件到html页面渲染过程中发生了什么 最后,assembledResult.code就是test.vue源文件转换成js之后的具体内容,如下

【源码共读】第11期 | 从vue-dev-server中学到了vue文件到html页面渲染过程中发生了什么 这就是关于test.vue文件所做的事情

LRU原理

/**
 * lru-cache:最近最少使用算法
 * 帮助缓存函数的结果,减少不必要的重复计算
 * 
 * 工作原理:
 * 当缓存空间满了之后,会先cleared最久未使用的缓存项;
 * 每当读取或写入缓存时,对应的缓存项就会被移动至最近使用链表的头部
 */

总结

通过对vue-dev-server仓库源码的学习,看到了express中间件对不同类型的文件的处理,就比如index.htmlmain.jsvuetest.vue四类资源的处理方式,也看到了对性能的考虑,使用到了cache缓存原理LRU的具体应用,其中从第三方库vue和vue组件的编译中学到了如何加载包文件和编译 Vue 单文件组件,进一步加深了我对Vue框架的理解,最后说一句,在vscode中断点调试的方式真方便~~~。

转载自:https://juejin.cn/post/7249650184215674938
评论
请登录