3

奇葩说框架之SFC编译原理

 2 years ago
source link: https://zhuanlan.zhihu.com/p/423603187
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

1.从vue打包说起

vue打包版本说明

v2-3a4d883d0058d3faf3dcb807f27206f1_720w.jpg

vue源码编译文件从功能角度主要分为两个类型:Full 版本和 Runtime-only 版本。

Full 版本包括编译器和运行时代码,其中编译器是具有将模板字符串转换成为js render函数功能的代码,运行时是具有生成VUE实例、渲染插入虚拟DOM等功能的代码。

其中,Runtime-only版本相比于Full版本,体积减少了30% 。同时将编译时和运行时分开,也提高了性能。

因此Runtime-only版本是性能方面的首选。要使用Runtime-only版本,最佳实践就是通过单文件组件的方式开发,提前完成编译工作。

2.单文件组件

Vue中单文件组件(single-file components,SFC )为扩展名为 .vue 的文件,示例如下:

<template>
  <div>
    <p>{{msg}},world</p>
  </div>
</template>

<script>
export default {
  data(){
    return {
      msg: 'hello'
    }
  }
}
</script>

<style lang="scss" scoped>
p {
  color: red;
}
</style>

我们平常在项目中开发使用的一般都是这种单文件组件。一个单文件组件可以包含四个部分:template、script、styles、customBlocks。

那么一个SFC是如何变成可以在浏览器上执行的代码呢?这就涉及到对于单文件组件的编译问题了。

3.单文件组件的编译

在通常的项目中,我们一般会使用webpack+vue-loader[1] 的方式编译单文件组件。先看简单的webpack配置文件:

const VueLoaderPlugin = require("vue-loader/lib/plugin");

module.exports = {
  module: {
    rules: [
      {
        test: /.vue$/,
        use: [{ loader: "vue-loader" }],
      }
    ],
  },
  plugins: [
    new VueLoaderPlugin()
  ],
};

可以看到,配置vue-loader的同时,还需要配置一个plugin。说到这里为了后面的内容能合理的展开,需要先了解一些Webpack方面的前置知识:

1、Webpack的loader和plugin有何区别?
loader是一个转换器,可以将A文件编译成B文件,主要用于将非js模块转换成为js模块,例如,将less通过less-loader转换成为css,通过css-loader进行css引用处理等,最后通过style-loader将css转换为脚本加载的js文件。
而plugin是一个扩展器,监听webpack打包过程中的某些节点,执行更为广泛的任务。从性能优化到代码压缩,从定义环境变量到重写html文件,功能强大到可以用来处理各种各样的任务。


2、Webpack中loader的执行顺序是怎么样的?
在 Webpack 中,loader 可以被分为 4 类:pre 前置、post 后置、normal 普通和 inline 行内,四种loader调用先后顺序为:pre > normal > inline > post 。在相同种类loader的情况下,调用的优先级为,自下而上,自右向左,pitch情况下,则反过来。

前置知识预览完后,我们先了解一下整体SFC编译过程。整个过程可以划分为两个阶段:

1.plugin执行阶段
该阶段其实就是前文中webpack配置文件中的VueLoaderPlugin代码执行过程。VueLoaderPlugin的核心任务只有一个就是重组rules,包括将pitcher loader和已有规则的clone处理后加入webpack配置信息module.rules中。可以理解为vue-loader有些复杂的东西不想让用户费脑筋,所以他自己在plugin阶段自动实现了。
2.loader执行阶段
由于在plugin阶段动态注入了pitcher loader、clone rules,因此loader阶段主要包括:
a、SFC文件调用vue-loader生成中间结果webpack module;
b、新生成的webpack module命中resourceQuery规则,调用pitcher loader,根据不同的处理逻辑,继续生成新的中间结果webpack module;
c、再次生成的新webpack module命中plugin阶段clone处理的具体loader,直接调用具体loader做处理。

下面具体看看每个过程。

先看vue-loader。对于一个loader来说,输入参数是上一个loader产生的结果或者资源文件,这里其实就是 .vue 文件。

拿到了文件内容,第一步会进行解析。

开始解析SFC,其实就是根据不同的block来拆解对应的内容,解析功能由 @vue/component-compiler-utilsvue-template-compiler提供。

上文例子中的单文件组件将会被解析为如下对象:

  {template:
    { type: 'template',
      content: '\n<div>\n<p>{{a}},world</p>\n</div>\n',
      start: 21,
      end: 62 },
   script:
    { type: 'script',
      content:
       '//\n//\n//\n//\n//\n\nexport default {\n  data () {\n    return {\n      msg: \'hello\'\n    }\n  }\n}\n',
      start: 83,
      attrs: {},
      end: 158,
      map:
       { version: 3,
         sources: [Array],
         names: [],
         mappings: ';;;;;;AAMA;AACA;AACA;AACA;AACA;AACA;AACA',
         file: 'source.vue',
         sourceRoot: 'example',
         sourcesContent: [Array] } },
   styles:
    [ { type: 'style',
        content: '\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\np {\n  color: red;\n}\n',
        start: 183,
        attrs: [Object],
        module: true,
        end: 207,
        map: [Object] } ],
   errors: [] }

对于转换生成的解析对象继续处理, template模块生成模块导入语句:

import { render, staticRenderFns } from "./source.vue?vue&type=template&id=27e4e96e&

script模块生成模块导入语句:

import script from "./source.vue?vue&type=script&lang=js&"
export * from "./source.vue?vue&type=script&lang=js&"

styles模块生成模块导入语句:

import style0 from "./source.vue?vue&type=style&index=0&module=true&lang=css&"

可以看到生成的代码里,又进行了新的一轮vue模块的导入,但每个导入都增加了参数部分,如template模块生成模块导入语句包含了 '?vue&type=template&id=27e4e96e' 参数。

导入过程中,根据前文前置知识中关于webpack loader的执行顺序,首先会执行具有pitch属性的pitcher loader。而pitcher loader依据resourceQuery中是否带有 'vue' ,恰好匹配到了这些新的vue模块的导入。

到这里就摊牌了,pitcher loader就是为了拦截这里生成的vue模块请求而生的。pitcher loader拦截了vue模块后,会找出templatestyle这两种模块,分别插入vue-loader自带的templateLoaderstylePostLoader这两种loader

templateLoader的主要作用就是将template模板文件编译成为render函数,模板编译过程请参考往期文章:你真的了解vue模版编译么?[2]

stylePostLoader主要用于处理scope css

经过pitcher loader处理之后,模块解析又会找到Vue Loader源码里去再走一遍逻辑。但是这次会走入selectBlock这个模块进行处理。源码在vue-loader的select.js文件中:

  if (query.type === `template`) {
    if (appendExtension) {
      loaderContext.resourcePath += '.' + (descriptor.template.lang || 'html')
    }
    loaderContext.callback(
      null,
      descriptor.template.content,
      descriptor.template.map
    )
    return
  }

  // script
  if (query.type === `script`) {
    if (appendExtension) {
      loaderContext.resourcePath += '.' + (descriptor.script.lang || 'js')
    }
    loaderContext.callback(
      null,
      descriptor.script.content,
      descriptor.script.map
    )
    return
  }

  // styles
  if (query.type === `style` && query.index != null) {
    const style = descriptor.styles[query.index]
    if (appendExtension) {
      loaderContext.resourcePath += '.' + (style.lang || 'css')
    }
    loaderContext.callback(
      null,
      style.content,
      style.map
    )
    return
  }

  // custom
  if (query.type === 'custom' && query.index != null) {
    const block = descriptor.customBlocks[query.index]
    loaderContext.callback(
      null,
      block.content,
      block.map
    )
    return
  }

可以看到,文件根据不同的template/script/style/custom模块对其相应的内容进行处理。最后使用webpack配置的loader对相应后缀进行规则匹配,并处理loaderContext.callback返回的相应内容。

至此vue loader的编译过程执行完毕。所以为什么打包都需要那么久,因为webpack在各种loader间导入导出转圈圈。

4.编译原理

高级语言必然涉及到语言处理器,需要将面向人的高级语言编译转换成为面向机器的运行语言。例如java语言需要通过编译器生成字节码然后通过虚拟机的解释器去执行。而上文讲述的SFC编译过程其实也是相同的,通过webpack将更适合开发的vue单文件组件编译成为浏览器可以运行的脚本语言,在这一过程的各个环节中或多或少都会运用到抽象语法树。

在前端编译的世界里,运用最广泛的莫过于抽象语法树。其规范来自于:ESTree项目[3] 。这个项目的初衷是通过社区的力量,保证和es规范的一致性,通过自定义的语法结构来表述JavaScriptAST,后来随着知名度越来越高,多位知名工程师的参与,使得变成了事实意义上的规范,目前这个库是Mozilla和社区一起维护的。
抽象语法树的应用包括不限于:

  • babel: 实现 JS 编译,转换过程是 AST 的转换
  • ESlint: 代码错误或风格的检查,发现一些潜在的错误
  • IDE 的错误提示、格式化、高亮、自动补全等
  • UglifyJS 压缩代码
  • 代码打包工具 webpack

随着前端工程化的脚步日益快速,前端编译也日趋成为一项必不可少技能。上面只是编译领域的冰山一角,更多的知识还有待大家去开发挖掘。

[1]vue-loader: https://github.com/vuejs/vue-loader

[2]你真的了解vue模版编译么?: https://mp.weixin.qq.com/s/Uvi2r3a2KwXrPdNAkexqLg

[3]ESTree项目: https://github.com/estree/estree


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK