Vue 3 文件编译流程详解与 Babel 的使用

一、背景

最近正在研究 react jsx 转化为 js 的过程,在学习完成之后,突然想到既然 react 是用过 babel 完成 jsx 的转化,那 vue 是不是也是使用 babel 的呢,带着这些疑问我开始探索 vue 的编译过程,经过一些资料的查询发现好像和 react 不太一样,但是网上却很少有深入的文章,因此我开始对源码的探索。

二、结论

vue 3 组件在编译过程中使用了 babel/parse 处理 script 中 js 代码处理为普通 js 代码,其余解析均未使用;下面我们将从源码层面解读。

三、@vitejs/plugin-vue 插件

调试前物料准备

  • vue 3 + vite
  • vscode

添加入口断点:

  • 找到入口文件 vite.config.js
  • 找到对应解析插件 vitejs/plugin-vue 插件 并在对应使用处打上断点
  • 在控制台打开 js 调试终端 (调试方法和浏览器类似)
  • 在对应终端启动我们的项目

vuePlugin 入口

进入 vue() 方法调用中,会出现 vuePlugin 方法这个方法暴露了很多配置属性,因为我们目的关注编译打包过程,所以我们这次只需关系核心的 buildStart 和 transform 即可,因此我们在这两个方法中打上断点

js 复制代码
// src/index.ts
function vuePlugin(rawOptions = {}) {
  // xxxx 省略
  return {
    name: "vite:vue",
    handleHotUpdate(ctx) {
    },
    config(config) {
    },
    configResolved(config) {
    },
    configureServer(server) {
    },
    // 开始打打包时处理的内容
    buildStart() {
      options.compiler = options.compiler || resolveCompiler(options.root);
    },
    async resolveId(id) {
    },
    load(id, opt) {
    },
    // 转化代码时执行的内容
    transform(code, id, opt) {
      // xxxx 省略
    }
  };
}

buildStart 方法

buildStart 方法主要是在服务启动前拿到编译的配置

接下来我们在 build 处打上断点看之后的流程,在控制台输入 options 查看其中内容:

此图可以看出最初 compiler 为空,我们的代码执行了 resolveCompiler 方法,并把结果存入了 options.compiler 中,为了清楚该方法做了什么,我们 step into 看一下这个方法的实现。

这个代码可以看出其实就是加载了并且返回了 vue/compiler-sfc 这个核心包,那加载这个包是做什么呢,我们可以接着往下看,继续走下一个断点,然后会发现我们项目启动了,并没有走到下一个断点。

transform 方法

那么是什么时候怎么才能走到我们的 transform 中呢?,(vite 的特点:先启动再根据加载页面按需加载所需要编译的代码 ),明白这一点之后我们只需要在浏览器访问这个地址:我这里就是 http://localhost:3000/ 访问之后发现我们的断点进入到了 transform 中:

从这边也可以看出 vite 打包为什么比 webpack 快

继续看 transform 里面的代码,可以看到 return transformMain() 这个就是我们的核心转化方法,我们继续 step into 该方法:

接着我们看一下这个方法的入参:

  • code: 就是我们的源代码
  • filename:这个文件的路径
  • options:配置项
  • pluginContext:插件的上下文(this)
  • ssr:暂不考虑
  • asCustomElement:暂不考虑

然后我们其实核心要看的就是这个 code -> 原生 js 的转化,因此往下看这个 code 都在哪里使用,首先我们就可以看到 createDescriptor 这个方法,这个时候我们打印一下descriptor 得到如下图:

发现我们 code 被解析如下三个部分 templatestylesscript。接着看这里的代码发现如下图 3 行代码:

js 复制代码
// 解析 descriptor 中的 script 部分
const { code: scriptCode, map } = await genScriptCode(descriptor, options, pluginContext, ssr);

// 解析 descriptor 中的 style 部分
const stylesCode = await genStyleCode(descriptor, pluginContext, asCustomElement, attachedProps);

// 解析 descriptor 中的 template 部分
const ({ code: templateCode, map: templateMap } = await genTemplateCode(descriptor, options, pluginContext, ssr));

然后在这里其实就比较清晰了,createDescriptor 先对源代码进行了初步解析,然后返回了 descriptor,然后我们进入 genScriptCodegenStyleCodegenTemplateCode 这三个方法进行具体代码转化。

接下来我们看下这四个方法的具体实现(只保留核心代码):

js 复制代码
// 得到原始的 descriptor
function createDescriptor(filename, source, { root, isProduction, sourceMap, compiler }) {
  const { descriptor, errors } = compiler.parse(source, {
    filename,
    sourceMap
  });
  return { descriptor, errors };
}

// 处理 descriptor 中 script 部分
async function genScriptCode(descriptor, options, pluginContext, ssr) {
  // 只保留核心代码
  const script = resolveScript(descriptor, options, ssr);
  scriptCode = options.compiler.rewriteDefault(script.content, "_sfc_main", xxx);
  return {
    code: scriptCode,
    map
  };
}
// 处理 css 
async function genStyleCode(descriptor, pluginContext, asCustomElement, attachedProps) {
  //  没有使用 compiler
}
// 处理 template
async function genTemplateCode(descriptor, options, pluginContext, ssr) {
  const template = descriptor.template;
  if (!template.lang && !template.src) {
    return transformTemplateInMain(template.content, descriptor, options, pluginContext, ssr);
  }
}

function transformTemplateInMain(code, descriptor, options, pluginContext, ssr) {
  const result = compile(code, descriptor, options, pluginContext, ssr);
}

function compile(code, descriptor, options, pluginContext, ssr) {
  const result = options.compiler.compileTemplate(__spreadProps(__spreadValues({}, resolveTemplateCompilerOptions(descriptor, options, ssr)), {
    source: code
  }));
  return result;
}

function resolveScript(descriptor, options, ssr) {
  resolved = options.compiler.compileScript(descriptor, __spreadProps(__spreadValues({}, options.script), {
    // xxx
  }));
  return resolved;
}

genStyleCode 则处理为 import "/Users/zcy/Desktop/毕设/smart-port/src/App.vue?vue&type=style&index=0&lang.less" 后续文章中会介绍为什么这个东西是怎么解析的,本文不会过多讲解。

经过上面代码分析:可以看出核心处理方法为:

js 复制代码
options.compiler.compileTemplate、
options.compiler.compileScript、 
options.compiler.rewriteDefault、
options.compiler.parse

此时再看 options.compiler 这个对象是不是很眼熟呢?这个就是在 buildStart 中 获取到的 vue/compiler-sfc 核心包源码如下:

js 复制代码
options.compiler = options.compiler || resolveCompiler(options.root);

因此我们要看懂到底是怎么解析 vue 组件的,就需要深入这个包中。

四、@vue/compiler-sfc 核心包

parse 方法

我们在 options.compiler.parse 方法调用处打上断点,然后我们逐层进入方法:

我们进入到了 parse 方法这边可以看到有一个 ast 的转化,如下图调用了 compiler.parse 方法,经过分析发现这个方法源于,@vue/compiler-dom ,从这个包的依赖看出并没有使用 babel,略过这个核心包,因此得出结论 parse 方法中没有使用 babel。

compileScript、rewriteDefault 方法

继续刚才操作我们看 compileScript 方法:

顺腾摸瓜找到 parser$2 这个变量的源头

js 复制代码
var parser$2 = require('@babel/parser');

rewriteDefault 也是如此:

因此得出结论在 script 的解析中会使用 @babel/parse。下面为解析后的源码:

js 复制代码
import { ref } from 'vue';

const _sfc_main = {
  setup(__props, { expose }) {
  expose();

const state = ref(1)

const __returned__ = { state, ref }
Object.defineProperty(__returned__, '__isScriptSetup', { enumerable: false, value: true })
return __returned__
}
}

compileTemplate 方法

同样使用了 @vue/compiler-dom 进行转化,具体转化细节就不进行详细展开了, 结果会转化为一个 render 函数如下:

js 复制代码
import { resolveComponent as _resolveComponent, createVNode as _createVNode, toDisplayString as _toDisplayString, createElementVNode as _createElementVNode, Fragment as _Fragment, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"

const _hoisted_1 = { id: "nav" }

function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
  const _component_router_link = _resolveComponent("router-link")
  const _component_router_view = _resolveComponent("router-view")

  return (_openBlock(), _createElementBlock(_Fragment, null, [
    _createElementVNode("div", _hoisted_1, [
      _createVNode(_component_router_link, { to: "/login" }),
      _createVNode(_component_router_link, { to: "/" }),
      _createElementVNode("div", null, _toDisplayString($setup.state), 1 /* TEXT */)
    ]),
    _createVNode(_component_router_view)
  ], 64 /* STABLE_FRAGMENT */))
}ƒ

五、整体架构

六、总结

本文只是出于好奇浅浅研究下了一下 vue3 的编译过程,和其中涉及到 babel 使用的点,对于很多细节没有深入研究,因此只做了一个简单的分析,希望对大家有参考价值。

参考资料

相关推荐
沈梦研1 小时前
【Vscode】Vscode不能执行vue脚本的原因及解决方法
ide·vue.js·vscode
轻口味2 小时前
Vue.js 组件之间的通信模式
vue.js
fmdpenny5 小时前
Vue3初学之商品的增,删,改功能
开发语言·javascript·vue.js
涔溪5 小时前
有哪些常见的 Vue 错误?
前端·javascript·vue.js
亦黑迷失7 小时前
vue 项目优化之函数式组件
前端·vue.js·性能优化
计算机-秋大田8 小时前
基于SpringBoot的高校教师科研的设计与实现(源码+SQL脚本+LW+部署讲解等)
java·vue.js·spring boot·后端·课程设计
eason_fan8 小时前
分析vue3源码23(异步组件实现)
vue.js·前端框架·源码阅读
BigData-010 小时前
vue视频流播放,支持多种视频格式,如rmvb、mkv
前端·javascript·vue.js
工业互联网专业11 小时前
基于springboot+vue的高校社团管理系统的设计与实现
java·vue.js·spring boot·毕业设计·源码·课程设计
白宇横流学长12 小时前
基于SpringBoot+Vue的旅游管理系统【源码+文档+部署讲解】
vue.js·spring boot·旅游