1.vue-loader如何实现.vue的处理(vue3.0版本)

首先基于webpack的打包构建,所以暂不考虑parcel。
webpack处理.vue文件配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
module: {
    rules: [
      {
        test: /\.vue$/,
        loader: "vue-loader",
        options: vueLoaderConfig,
      },
      ....
  ],
  plugins: [
    new VueLoaderPlugin(), //千万不要忘了这个呦
    ...
  ],
}

在探究vue-loader之前,请先阅读该文章:深入Webpack-编写Loader

好了,通过上面配置我们知道,当webpack识别到.vue的文件后,会交给vue-loader处理。

compiler-sfc是如何处理.vue文件的

在这里声明以下,我先将compiler-sfc的提到前面来,方便按照处理顺序理解,其实它是在loader中最先调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
export function parse(
  source: string,
  {
    sourceMap = true,
    filename = 'component.vue',
    sourceRoot = '',
    pad = false,
    compiler = require('@vue/compiler-dom')
  }: SFCParseOptions = {}
): SFCParseResult {
  //生成sourceKey
  const sourceKey =
    source + sourceMap + filename + sourceRoot + pad + compiler.parse
  const cache = sourceToSFC.get(sourceKey)
  if (cache) {
    return cache
  }

  const descriptor: SFCDescriptor = {
    filename,
    template: null,
    script: null,
    styles: [],
    customBlocks: []
  }

  const errors: CompilerError[] = []
  //通过@vue/compiler-dom的compiler进行包装,此处对template的内容并未做处理
  const ast = compiler.parse(source, {
    // there are no components at SFC parsing level
    isNativeTag: () => true,
    // preserve all whitespaces
    isPreTag: () => true,
    getTextMode: (tag, _ns, parent) => {
      // 对于除去template的顶级标签保留为文本。
     // 简单来说就是对scrpit和style标签不做处理。
      if (!parent && tag !== 'template') {
        return TextModes.RAWTEXT
      } else {
        return TextModes.DATA
      }
    },
    onError: e => {
      errors.push(e)
    }
  })
  // 接下来就是将ast的`template`,`script`,`style`转换为Block,最终传递出去给webpack处理
  ast.children.forEach(node => {
    if (node.type !== NodeTypes.ELEMENT) {
      return
    }
    if (!node.children.length && !hasSrc(node)) {
      return
    }
    switch (node.tag) {
      case 'template':
        if (!descriptor.template) {
          descriptor.template = createBlock(
            node,
            source,
            pad
          ) as SFCTemplateBlock
        } else {
          warnDuplicateBlock(source, filename, node)
        }
        break
      case 'script':
        if (!descriptor.script) {
          descriptor.script = createBlock(node, source, pad) as SFCScriptBlock
        } else {
          warnDuplicateBlock(source, filename, node)
        }
        break
      case 'style':
        descriptor.styles.push(createBlock(node, source, pad) as SFCStyleBlock)
        break
      default:
        descriptor.customBlocks.push(createBlock(node, source, pad))
        break
    }
  })
  //以下是生成sourceMap逻辑
  if (sourceMap) {
    const genMap = (block: SFCBlock | null) => {
      if (block && !block.src) {
        block.map = generateSourceMap(
          filename,
          source,
          block.content,
          sourceRoot,
          pad ? 0 : block.loc.start.line - 1
        )
      }
    }
    genMap(descriptor.template)
    genMap(descriptor.script)
    descriptor.styles.forEach(genMap)
  }

  const result = {
    descriptor,
    errors
  }
  sourceToSFC.set(sourceKey, result)
  return result
}

compiler-sfc做了什么?

  1. 当webpack调用vue-loader后,将.vue读取出来交给``compiler-sfc`解析。
  2. 调用@vue/compiler-dom的compiler.vue文件转换成AST(抽象语法树)
    1.1. 对于template的内容此时并没有做处理
    1.2 对于scriptstyle及自定义顶级标签不做处理,保留为文本格式。
  3. 对生成的AST转化为SFCDescriptor形式descriptor。(templatescript只允许存在一个)
  4. 对source进行处理map。
  5. 返回包含descriptor的结果。

通过源码分析vue-loader处理流程

由于代码太长我直接进行裁剪

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
const loader: webpack.loader.Loader = function(source: string) {
  const loaderContext = this
  ...
  // check if plugin is installed
  ...
  const {
    mode,
    target,
    sourceMap,
    rootContext,
    resourcePath,
    resourceQuery
  } = loaderContext
  ...
  // 调用`compiler-sfc`的parse生成SFCdescriptor
  const { descriptor, errors } = parse(source, {
    filename: resourcePath,
    sourceMap
  })
 ...
  // 重点来了
  // template
  let templateImport = ``
  let templateRequest
  if (descriptor.template) {
    const src = descriptor.template.src || resourcePath
    const idQuery = `&id=${id}`
    const scopedQuery = hasScoped ? `&scoped=true` : ``
    const attrsQuery = attrsToQuery(descriptor.template.attrs)
    const query = `?vue&type=template${idQuery}${scopedQuery}${attrsQuery}${resourceQuery}`
    templateRequest = stringifyRequest(src + query)
    // 在这里将经过`compiler-sfc`处理过的`template`对象
    // 转化成webpack的import形式,同时将所需要参数拼接在后面
    templateImport = `import { render } from ${templateRequest}`
    //在此处断点,可观察到
    //templateImport = ` import { render } from "./App.vue?vue&type=template&id=7ba5bd90&scoped=true"`
  }

  // script
  let scriptImport = `const script = {}`
  if (descriptor.script) {
    const src = descriptor.script.src || resourcePath
    const attrsQuery = attrsToQuery(descriptor.script.attrs, 'js')
    const query = `?vue&type=script${attrsQuery}${resourceQuery}`
    const scriptRequest = stringifyRequest(src + query)
    // 在这里将.vue里面的`script`转换成webpack的import形式
    scriptImport =
      `import script from ${scriptRequest}\n` + `export * from ${scriptRequest}`
  }

  // styles
  let stylesCode = ``
  let hasCSSModules = false
  if (descriptor.styles.length) {
    descriptor.styles.forEach((style: SFCStyleBlock, i: number) => {
      const src = style.src || resourcePath
      const attrsQuery = attrsToQuery(style.attrs, 'css')
      // make sure to only pass id when necessary so that we don't inject
      // duplicate tags when multiple components import the same css file
      const idQuery = style.scoped ? `&id=${id}` : ``
      const query = `?vue&type=style&index=${i}${idQuery}${attrsQuery}${resourceQuery}`
      const styleRequest = stringifyRequest(src + query)
       ...
      //对css module处理
      ...
      //此处转化成类似import "xxx.css/less/sass"形式
      stylesCode += `\nimport ${styleRequest}`
      // TODO SSR critical CSS collection
    })
  }
  // 将刚才上面的import语句转成通用的code,以便交给webpack的其他流程进行处理。
  let code = [
    templateImport,
    scriptImport,
    stylesCode,
    templateImport ? `script.render = render` : ``
  ]
    .filter(Boolean)
    .join('\n')

  // attach scope Id for runtime use
  //这里是个重点,style设置了scoped的话,会将id先暂存添加到,等运行时在做处理。
  if (hasScoped) {
    code += `\nscript.__scopeId = "data-v-${id}"`
  }
  ...
  // 对customBlocks处理
 ...
  // finalize
  code += `\n\nexport default script`
  return code
}

我们再看loader将.vue文件生成如下格式的js,

1
2
3
4
5
6
7
8
9
10
11
12
import { render } from "./App.vue?vue&type=template&id=7ba5bd90&scoped=true"
import script from "./App.vue?vue&type=script&lang=js"
export * from "./App.vue?vue&type=script&lang=js"

import "./App.vue?vue&type=style&index=0&id=7ba5bd90&scoped=true&lang=css"
script.render = render
script.__scopeId = "data-v-7ba5bd90"
/* hot reload */
...
script.__file = "src/App.vue"

export default script

最终输出了如下图形式的文件路径。具体内容,可以自己找个demo自行查看对应的文件内容。以便加深理解。

image.png

根据上面内容我们总结下vue-loader处理.vue的流程:
当webpack识别到有导入".vue"文件,则将.vue读取到,交给了vue的loader。

  1. vue-loader先调用compiler-sfc对文件内容转换为webpack模块化的import形式
  2. 这样.vue文件就成了标准的js形式,最后其他的loader对./App.vue?vue&type=(template|script|style)进行处理

不知道我有没有解释清楚。。。

现在我们再来了解VueLoaderPlugin做了什么?

VueLoaderPlugin做了什么

阅读之前请先阅读:Webpack原理-编写Plugin

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
class VueLoaderPlugin implements webpack.Plugin {
  static NS = NS

  apply(compiler: webpack.Compiler) {
     
    const rawRules = compiler.options.module!.rules
    // use webpack's RuleSet utility to normalize user rules
    const rules = new RuleSet(rawRules).rules as webpack.RuleSetRule[]

    // 查找到对`.vue`配置的rule
    let vueRuleIndex = rawRules.findIndex(createMatcher(`foo.vue`))
    if (vueRuleIndex < 0) {
      vueRuleIndex = rawRules.findIndex(createMatcher(`foo.vue.html`))
    }
    const vueRule = rules[vueRuleIndex]

    // get the normlized "use" for vue files
    const vueUse = vueRule.use as webpack.RuleSetLoader[]
    // 获取`vue-loader`的options
    const vueLoaderUseIndex = vueUse.findIndex(u => {
      return /^vue-loader|(\/|\\|@)vue-loader/.test(u.loader || '')
    })

    const vueLoaderUse = vueUse[vueLoaderUseIndex]
    const vueLoaderOptions = (vueLoaderUse.options =
      vueLoaderUse.options || {}) as VueLoaderOptions

    // 复制配置的rule
    const clonedRules = rules.filter(r => r !== vueRule).map(cloneRule)
    // 重点来了
    // 对于`.vue`文件的`template`的compiler
    const templateCompilerRule = {
      loader: require.resolve('./templateLoader'),
      test: /\.vue$/,
      resourceQuery: (query: string) => {
        const parsed = qs.parse(query.slice(1))
        return parsed.vue != null && parsed.type === 'template'
      },
      options: vueLoaderOptions
    }

    // for each rule that matches plain .js files, also create a clone and
    // match it against the compiled template code inside *.vue files, so that
    // compiled vue render functions receive the same treatment as user code
    // (mostly babel)
    const matchesJS = createMatcher(`test.js`)
    const jsRulesForRenderFn = rules
      .filter(r => r !== vueRule && matchesJS(r))
      .map(cloneRuleForRenderFn)

    // 此pitcher将会拦截到`.vue`,并处理`style`和`custom`等形式的
    // loaders matched for src imports)
    const pitcher = {
      loader: require.resolve('./pitcher'),
      resourceQuery: (query: string) => {
        const parsed = qs.parse(query.slice(1))
        return parsed.vue != null
      }
    }

    // 替换原始的rules
    compiler.options.module!.rules = [
      pitcher,
      ...jsRulesForRenderFn,
      templateCompilerRule,
      ...clonedRules,
      ...rules
    ]
  }
}

上面的代码比较长,不过我们了解到,VueLoaderPlugin做了什么:

  1. 找到.vue配置的rule,并获取到该rule配置的options。
  2. 通过option重新生成一份对于.vue?template的templateLoader,这里的templateLoader会对识别到的.vue的template,进行处理为render函数形式。
  3. 然后通过pitcher对style处理加入scope处理,具体参考下文./pitcher做了什么

./templateLoader做了什么

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import { compileTemplate } from '@vue/compiler-sfc'

const TemplateLoader: webpack.loader.Loader = function(source, inMap) {
  source = String(source)
  const loaderContext = this
  ...
  // 此处是将scopeId传入,最终处理style的scope
  const scopeId = query.scoped ? `data-v-${query.id}` : null
  //此loader才会将template转换成h形式的render方法,参考下图。
  const compiled = compileTemplate({
    source,
    inMap,
    filename: loaderContext.resourcePath,
    compiler: options.compiler,
    compilerOptions: {
      ...options.compilerOptions,
      scopeId
    },
    transformAssetUrls: options.transformAssetUrls || true
  })
  ...

  const { code, map } = compiled
  loaderContext.callback(null, code, map)
}

image.png

./pitcher 做了什么

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 负责拦截vue的 `style``script`或自定义标签
pitcher.pitch = function() {
  const context = this as webpack.loader.LoaderContext
  const rawLoaders = context.loaders.filter(isNotPitcher)
  let loaders = rawLoaders
  ...
  const query = qs.parse(context.resourceQuery.slice(1))
  ...
  // 在css-loader之前添加style-post-loader
  // 来处理style的scope
  if (query.type === `style`) {
    const cssLoaderIndex = loaders.findIndex(isCSSLoader)
    if (cssLoaderIndex > -1) {
      const afterLoaders = loaders.slice(0, cssLoaderIndex + 1)
      const beforeLoaders = loaders.slice(cssLoaderIndex + 1)
      返回一个处理`style`加入了`style-post-loader`的rules
      return genProxyModule(
        [...afterLoaders, stylePostLoaderPath, ...beforeLoaders],
        context
      )
    }
  }

  // 处理`.vue`的`custom`及cache
  if (query.type === `custom` && shouldIgnoreCustomBlock(loaders)) {
    return ``
  }
  return genProxyModule(loaders, context, query.type !== 'template')
}

./stylePostLoader做了什么

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { compileStyle } from '@vue/compiler-sfc'

const StylePostLoader: webpack.loader.Loader = function(source, inMap) {
  const query = qs.parse(this.resourceQuery.slice(1))
  const { code, map, errors } = compileStyle({
    source: source as string,
    filename: this.resourcePath,
    id: `data-v-${query.id}`,
    map: inMap,
    scoped: !!query.scoped,
    trim: true
  })

  if (errors.length) {
    this.callback(errors[0])
  } else {
    this.callback(null, code, map)
  }
}

image.png

好了,本文到此结束了。文章顺序比较乱,为此参阅以下流程图。


vue-loader处理流程