首页
Javascript
Html
Css
Node.js
Electron
移动开发
小程序
工具类
服务端
浏览器相关
前端收藏
其他
关于
公司注册

什么是vue的sfc?怎么解析成SFCDescriptor?

2019年01月29日 发布 阅读(8947) 作者:Jerman

我们平时写的 .vue 文件称为 SFC(Single File Components)。vue 会先对 .vue 文件进行解析,分成 template、script、styles、customBlocks 四个部分,称为 descriptor。之后,再对这四个部分分别进行编译最终得到可以在浏览器中执行的 .js 文件。本文介绍将 SFC 解析为 descriptor 这一过程。

SFCDescriptor,是表示 .vue 各个代码块的对象,为以下数据格式:

  1. // an object format describing a single-file component.
  2. declare type SFCDescriptor = {
  3. template: ?SFCBlock;
  4. script: ?SFCBlock;
  5. styles: Array<SFCBlock>;
  6. customBlocks: Array<SFCBlock>;
  7. };

vue 提供了一个 compiler.parseComponent(file, [options])方法,来将 .vue 文件解析成一个 SFCDescriptor。

文件入口

解析 sfc 文件的源码入口在 src/sfc/parser.js 中,编译后的产出在 /packages/vue-template-compiler 和 /packages/vue-server-renderer 下的 build.js 中。

build.js 文件中直接 export 出了parseComponent方法。

首先我们来看看parseComponent方法都做了哪些事情。

parseComponent方法

  1. /**
  2. * Parse a single-file component (*.vue) file into an SFC Descriptor Object.
  3. */
  4. export function parseComponent (
  5. content: string,
  6. options?: Object = {}
  7. ): SFCDescriptor {
  8. const sfc: SFCDescriptor = {
  9. template: null,
  10. script: null,
  11. styles: [],
  12. customBlocks: []
  13. }
  14. let depth = 0
  15. let currentBlock: ?SFCBlock = null
  16. function start (tag: string, attrs: Array<Attribute>, unary: boolean, start: number, end: number) {}
  17. // ...
  18. function end (tag: string, start: number, end: number) {}
  19. // ...
  20. parseHTML(content, {
  21. start,
  22. end
  23. })
  24. return sfc
  25. }

parseComponent方法中主要定义了startend两个函数,之后调用了parseHTML方法来对 .vue 文件内容践行编译。startend两个函数作为参数传给了parseHTML,我们等下再看。

先看下这个parseHTML方法是做啥的呢?

parseHTML方法

该方法看名字可以猜到是一个 html-parser。

parseHTML的代码细节较多,我们可以简单理解为:遍历解析查找文件中的各个标签,解析到每个起始标签时,调用 option 中的 start 方法进行处理;解析到每个结束标签时,调用 option 中的 end 方法进行处理。

对应到这里,就是分别调用parseComponent方法中定义的 startend 函数进行处理。

由于我们这里只是想要找到第一层标签,也就是 template、script这些。因此可以在parseComponent中维护一个 depth 变量,在start中将depth++,在enddepth--。那么,每个depth === 1的标签就是我们需要获取的信息,包含 template、script、style 以及一些自定义标签。

接下来我们来看startend中进行了哪些处理。

start

每当遇到一个起始标签时,执行start函数。

  1. function start (
  2. tag: string,
  3. attrs: Array<Attribute>,
  4. unary: boolean,
  5. start: number,
  6. end: number
  7. ) {
  8. if (depth === 0) {
  9. currentBlock = {
  10. type: tag,
  11. content: '',
  12. start: end,
  13. attrs: attrs.reduce((cumulated, { name, value }) => {
  14. cumulated[name] = value || true
  15. return cumulated
  16. }, {})
  17. }
  18. if (isSpecialTag(tag)) {
  19. checkAttrs(currentBlock, attrs)
  20. if (tag === 'style') {
  21. sfc.styles.push(currentBlock)
  22. } else {
  23. sfc[tag] = currentBlock
  24. }
  25. } else { // custom blocks
  26. sfc.customBlocks.push(currentBlock)
  27. }
  28. }
  29. if (!unary) {
  30. depth++
  31. }
  32. }

1、记录下 currentBlock。每个 currentBlock 包含以下内容:

  1. declare type SFCBlock = {
  2. type: string;
  3. content: string;
  4. start?: number;
  5. end?: number;
  6. lang?: string;
  7. src?: string;
  8. scoped?: boolean;
  9. module?: string | boolean;
  10. };

2、根据 tag 名称,将 currentBlock 对象保存在在返回结果对象中。

返回结果对象定义为 sfc,如果tag不是 script,style,template 中的任一个,就放在 sfc.customBlocks 中。如果是style,就放在 sfc.styles 中。script 和 template 则直接放在 sfc 下。

  1. if (isSpecialTag(tag)) {
  2. checkAttrs(currentBlock, attrs)
  3. if (tag === 'style') {
  4. sfc.styles.push(currentBlock)
  5. } else {
  6. sfc[tag] = currentBlock
  7. }
  8. } else { // custom blocks
  9. sfc.customBlocks.push(currentBlock)
  10. }

end

每当遇到一个结束标签时,执行end函数。

  1. function end (tag: string, start: number, end: number) {
  2. if (depth === 1 && currentBlock) {
  3. currentBlock.end = start
  4. let text = deindent(content.slice(currentBlock.start, currentBlock.end))
  5. // pad content so that linters and pre-processors can output correct
  6. // line numbers in errors and warnings
  7. if (currentBlock.type !== 'template' && options.pad) {
  8. text = padContent(currentBlock, options.pad) + text
  9. }
  10. currentBlock.content = text
  11. currentBlock = null
  12. }
  13. depth--
  14. }

如果当前是第一层标签(depth === 1),并且 currentBlock 变量存在,那么取出这部分text,放在 currentBlock.content 中。

  1. if (depth === 1 && currentBlock) {
  2. currentBlock.end = start
  3. let text = deindent(content.slice(currentBlock.start, currentBlock.end))
  4. // pad content so that linters and pre-processors can output correct
  5. // line numbers in errors and warnings
  6. if (currentBlock.type !== 'template' && options.pad) {
  7. text = padContent(currentBlock, options.pad) + text
  8. }
  9. currentBlock.content = text
  10. currentBlock = null
  11. }

depth--

在将 .vue 整个遍历一遍后,得到的 sfc 对象即为我们需要的 SFCDescriptor。

生成 .js

compiler.parseComponent(file, [options])得到的只是一个组件的 SFCDescriptor,最终编译成.js 文件是交给 vue-loader 等库来做的。


原文: https://meixg.cn/2018/04/23/vue-sfc-parser/

版权声明:本站文章除特别声明外,均采用署名-非商业性使用-禁止演绎 4.0 国际 许可协议,如需转载,请注明出处