本地服务怎么编译vue模板文件

93 阅读6分钟

vue项目中有很多.vue文件,但是这个文件对于浏览器来说肯定是解析不了的,那么就用到了vue-loader对这个文件代码进行了处理,最后才成了浏览器上的.vue文件。以App.vue为例:

image.png

e539bb8ee6f0bcbd6bac3ca55987b0e.png

可以看到两个App.vue差别还是很大的,那么是怎么样变过来的呢,接下来一步步深入。

入口loader()

源码位置:node_modules/vue-loader/dist/index.js

function loader(source) {
   // 省略一些变量的定义
  
    const { descriptor, errors } = parse(source, {
        filename,
        sourceMap,
    });
 
   
    // script
    let scriptImport = `const script = {}`;
    let isTS = false;
    const { script, scriptSetup } = descriptor;

    if (script || scriptSetup) {
        const lang = (script === null || script === void 0 ? void 0 : script.lang) || (scriptSetup === null || scriptSetup === void 0 ? void 0 : scriptSetup.lang);
        isTS = !!(lang && /tsx?/.test(lang));
        const externalQuery = Boolean(script && !scriptSetup && script.src)
            ? `&external`
            : ``;
        const src = (script && !scriptSetup && script.src) || resourcePath;
        const attrsQuery = attrsToQuery((scriptSetup || script).attrs, 'js');
        const query = `?vue&type=script${attrsQuery}${resourceQuery}${externalQuery}`;
        let scriptRequest;
        if (enableInlineMatchResource) {
            scriptRequest = stringifyRequest((0, util_1.genMatchResource)(this, src, query, lang || 'js'));
        }
        else {
            scriptRequest = stringifyRequest(src + query);
        }
        scriptImport =
            `import script from ${scriptRequest}\n` +
                // support named exports
                `export * from ${scriptRequest}`;
    }
    // template
    let templateImport = ``;
    let templateRequest;
    const renderFnName = isServer ? `ssrRender` : `render`;
    const useInlineTemplate = (0, resolveScript_1.canInlineTemplate)(descriptor, isProduction);
    if (descriptor.template && !useInlineTemplate) {
        const src = descriptor.template.src || resourcePath;
        const externalQuery = Boolean(descriptor.template.src) ? `&external` : ``;
        const idQuery = `&id=${id}`;
        const scopedQuery = hasScoped ? `&scoped=true` : ``;
        const attrsQuery = attrsToQuery(descriptor.template.attrs);
        const tsQuery = options.enableTsInTemplate !== false && isTS ? `&ts=true` : ``;
        const query = `?vue&type=template${idQuery}${scopedQuery}${tsQuery}${attrsQuery}${resourceQuery}${externalQuery}`;
        if (enableInlineMatchResource) {
            templateRequest = stringifyRequest((0, util_1.genMatchResource)(this, src, query, options.enableTsInTemplate !== false && isTS ? 'ts' : 'js'));
        }
        else {
            templateRequest = stringifyRequest(src + query);
        }
        templateImport = `import { ${renderFnName} } from ${templateRequest}`;
        propsToAttach.push([renderFnName, renderFnName]);
    }
    // styles
    let stylesCode = ``;
    let hasCSSModules = false;
    const nonWhitespaceRE = /\S+/;
    if (descriptor.styles.length) {
        descriptor.styles
            .filter((style) => style.src || nonWhitespaceRE.test(style.content))
            .forEach((style, i) => {
            const src = style.src || resourcePath;
            const attrsQuery = attrsToQuery(style.attrs, 'css');
            const lang = String(style.attrs.lang || '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.src || style.scoped ? `&id=${id}` : ``;
            const inlineQuery = asCustomElement ? `&inline` : ``;
            const externalQuery = Boolean(style.src) ? `&external` : ``;
            const query = `?vue&type=style&index=${i}${idQuery}${inlineQuery}${attrsQuery}${resourceQuery}${externalQuery}`;
            let styleRequest;
            if (enableInlineMatchResource) {
                styleRequest = stringifyRequest((0, util_1.genMatchResource)(this, src, query, lang));
            }
            else {
                styleRequest = stringifyRequest(src + query);
            }
            if (style.module) {
                if (asCustomElement) {
                    loaderContext.emitError(new Error(`<style module> is not supported in custom element mode.`));
                }
                if (!hasCSSModules) {
                    stylesCode += `\nconst cssModules = {}`;
                    propsToAttach.push([`__cssModules`, `cssModules`]);
                    hasCSSModules = true;
                }
                stylesCode += (0, cssModules_1.genCSSModulesCode)(id, i, styleRequest, style.module, needsHotReload);
            }
            else {
                if (asCustomElement) {
                    stylesCode += `\nimport _style_${i} from ${styleRequest}`;
                }
                else {
                    stylesCode += `\nimport ${styleRequest}`;
                }
            }
            // TODO SSR critical CSS collection
        });
        if (asCustomElement) {
            propsToAttach.push([
                `styles`,
                `[${descriptor.styles.map((_, i) => `_style_${i}`)}]`,
            ]);
        }
    }
    let code = [templateImport, scriptImport, stylesCode]
        .filter(Boolean)
        .join('\n');
    // attach scope Id for runtime use
    if (hasScoped) {
        propsToAttach.push([`__scopeId`, `"data-v-${id}"`]);
    }
    // Expose filename. This is used by the devtools and Vue runtime warnings.
    if (!isProduction) {
        // Expose the file's full path in development, so that it can be opened
        // from the devtools.
        propsToAttach.push([
            `__file`,
            JSON.stringify(rawShortFilePath.replace(/\\/g, '/')),
        ]);
    }
    else if (options.exposeFilename) {
        // Libraries can opt-in to expose their components' filenames in production builds.
        // For security reasons, only expose the file's basename in production.
        propsToAttach.push([`__file`, JSON.stringify(path.basename(resourcePath))]);
    }
    // custom blocks
    if (descriptor.customBlocks && descriptor.customBlocks.length) {
        code += `\n/* custom blocks */\n`;
        code +=
            descriptor.customBlocks
                .map((block, i) => {
                const src = block.attrs.src || resourcePath;
                const attrsQuery = attrsToQuery(block.attrs);
                const blockTypeQuery = `&blockType=${qs.escape(block.type)}`;
                const issuerQuery = block.attrs.src
                    ? `&issuerPath=${qs.escape(resourcePath)}`
                    : '';
                const externalQuery = Boolean(block.attrs.src) ? `&external` : ``;
                const query = `?vue&type=custom&index=${i}${blockTypeQuery}${issuerQuery}${attrsQuery}${resourceQuery}${externalQuery}`;
                let customRequest;
                if (enableInlineMatchResource) {
                    customRequest = stringifyRequest((0, util_1.genMatchResource)(this, src, query, block.attrs.lang));
                }
                else {
                    customRequest = stringifyRequest(src + query);
                }
                return (`import block${i} from ${customRequest}\n` +
                    `if (typeof block${i} === 'function') block${i}(script)`);
            })
                .join(`\n`) + `\n`;
    }
    // finalize
    if (!propsToAttach.length) {
        code += `\n\nconst __exports__ = script;`;
    }
    else {
        code += `\n\nimport exportComponent from ${stringifyRequest(exportHelperPath)}`;
        code += `\nconst __exports__ = /*#__PURE__*/exportComponent(script, [${propsToAttach
            .map(([key, val]) => `['${key}',${val}]`)
            .join(',')}])`;
    }
    if (needsHotReload) {
        code += (0, hotReload_1.genHotReloadCode)(id, templateRequest);
    }
    code += `\n\nexport default __exports__`;
    return code;
}

核心代码就做了接下来这几件事:

  1. const { descriptor, errors } = parse(source, { filename, sourceMap, });通过parse函数获取descriptor对象。这个对象里面东西有很多很多,下面的一些代码也是围绕这个对象展开的,让我们先知道下descriptor具体是什么,parse

2.到这一步我们知道了descriptor里面有些什么了,接下来就是拼接import语句,以及所有code代码的拼接了。

代码import

image.png

下面分别是import代码实现。

image.png

image.png

image.png

其余代码拼接

image.png

   let code = [templateImport, scriptImport, stylesCode]
        .filter(Boolean)
        .join('\n');
    // attach scope Id for runtime use
    if (hasScoped) {
        propsToAttach.push([`__scopeId`, `"data-v-${id}"`]);
    }
    // Expose filename. This is used by the devtools and Vue runtime warnings.
    if (!isProduction) {
        // Expose the file's full path in development, so that it can be opened
        // from the devtools.
        propsToAttach.push([
            `__file`,
            JSON.stringify(rawShortFilePath.replace(/\\/g, '/')),
        ]);
    }
    else if (options.exposeFilename) {
        // Libraries can opt-in to expose their components' filenames in production builds.
        // For security reasons, only expose the file's basename in production.
        propsToAttach.push([`__file`, JSON.stringify(path.basename(resourcePath))]);
    }
    // custom blocks
    if (descriptor.customBlocks && descriptor.customBlocks.length) {
        code += `\n/* custom blocks */\n`;
        code +=
            descriptor.customBlocks
                .map((block, i) => {
                const src = block.attrs.src || resourcePath;
                const attrsQuery = attrsToQuery(block.attrs);
                const blockTypeQuery = `&blockType=${qs.escape(block.type)}`;
                const issuerQuery = block.attrs.src
                    ? `&issuerPath=${qs.escape(resourcePath)}`
                    : '';
                const externalQuery = Boolean(block.attrs.src) ? `&external` : ``;
                const query = `?vue&type=custom&index=${i}${blockTypeQuery}${issuerQuery}${attrsQuery}${resourceQuery}${externalQuery}`;
                let customRequest;
                if (enableInlineMatchResource) {
                    customRequest = stringifyRequest((0, util_1.genMatchResource)(this, src, query, block.attrs.lang));
                }
                else {
                    customRequest = stringifyRequest(src + query);
                }
                return (`import block${i} from ${customRequest}\n` +
                    `if (typeof block${i} === 'function') block${i}(script)`);
            })
                .join(`\n`) + `\n`;
    }
    // finalize
    if (!propsToAttach.length) {
        code += `\n\nconst __exports__ = script;`;
    }
    else {
        code += `\n\nimport exportComponent from ${stringifyRequest(exportHelperPath)}`;
        code += `\nconst __exports__ = /*#__PURE__*/exportComponent(script, [${propsToAttach
            .map(([key, val]) => `['${key}',${val}]`)
            .join(',')}])`;
    }
    if (needsHotReload) {
        code += (0, hotReload_1.genHotReloadCode)(id, templateRequest);
    }
    code += `\n\nexport default __exports__`;
    return code;

最后就是我们浏览器对应的文件了。

image.png

待解决问题:

现在知道了浏览器的.vue文件是怎么生成的了,这就是为什么我们打印导入的.vue文件返回的是一个对象

image.png, 但是找了找对应的render,script导入地址 这些个文件却发现在babel中。

image.png

由于一直到不到执行loader的上下文,目前只能猜想到生成这个文件其实是调用了callback函数,可以看到loader函数这一行if (incomingQuery.type) { return (0, select_1.selectBlock)(descriptor, id, options, loaderContext, incomingQuery, !!options.appendExtension); },找到对应代码

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.selectBlock = void 0;
const resolveScript_1 = require("./resolveScript");
function selectBlock(descriptor, scopeId, options, loaderContext, query, appendExtension) {
    // template
    if(descriptor.filename==='D:\\EXAM\\test\\fof\\src\\App.vue'){
        console.log('select',query);
    }
    if (query.type === `template`) {
        // if we are receiving a query with type it can only come from a *.vue file
        // that contains that block, so the block is guaranteed to exist.
        const template = descriptor.template;
        if (appendExtension) {
            loaderContext.resourcePath += '.' + (template.lang || 'html');
        }
        loaderContext.callback(null, template.content, template.map);
        return;
    }
    // script
    if (query.type === `script`) {
        const script = (0, resolveScript_1.resolveScript)(descriptor, scopeId, options, loaderContext);
        if (appendExtension) {
            loaderContext.resourcePath += '.' + (script.lang || 'js');
        }
        loaderContext.callback(null, script.content, script.map);
        return;
    }
    // styles
    if (query.type === `style` && query.index != null) {
        const style = descriptor.styles[Number(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[Number(query.index)];
        loaderContext.callback(null, block.content, block.map);
    }
}
exports.selectBlock = selectBlock;

我看了看loader执行过程。 if(descriptor.filename==='D:\\EXAM\\test\\fof\\src\\App.vue'){ console.log(1111111,incomingQuery.type,source,_resourceQuery); }用这行代码放在loader中运行,发现log了三次,也就是一个.vue字符串被当作source给loader执行,执行了三次,第一次incomingQuery.type是undefined所以没有执行上面的select_1.selectBlock函数,然后第二次incomingQuery.type是template,所以进入了select_1.selectBlock函数,并且执行了loaderContext.callback(null, template.content, template.map);这个语句,这个语句我推断就是生成babel/bin里面type=template文件,也就是导出xx.Vue文件的render()渲染函数的文件。type=script和type=style处理方法同理。

总之要解决这个问题最关键的地方是找到执行loader()函数的上下文。后面有空再来填坑,有知道的大佬麻烦私聊我下,蟹蟹~

parse

parse(source,options) 位置:\node_modules\@vue\compiler-sfc\dist\compiler-sfc.cjs.js

入参:

source : .vue模板字符串

options: 一些配置参数

返回值:{ descriptor, errors }对象,重点的是descriptor,下面是descriptor代码:

{
  filename: 'D:\\EXAM\\test\\fof\\src\\App.vue',  //文件名
  source: '<template>\r\n' +                      //同入参source
    '  <router-view></router-view>\r\n' +
    '</template>\r\n' +
    '\r\n' +
    '<script setup>\r\n' +
    "import './normalize.css'\r\n" +
    'import store from "@/store";\r\n' +
    '\r\n' +
    '\r\n' +
    "document.body.className = !store.state.themeValue ? 'dark' : 'light'\r\n" +
    '\r\n' +
    '\r\n' +
    '</script>\r\n' +
    '\r\n' +
    '<style scoped>\r\n' +
    '\r\n' +
    '</style>\r\n' +
    '\r\n' +
    '\r\n',
  template: {            
    type: 'template',
    content: '\r\n  <router-view></router-view>\r\n',            //temlatle标签内的元素
    loc: {
      source: '\r\n  <router-view></router-view>\r\n',           
      start: [Object],
      end: [Object]
    },
    attrs: {},
    ast: {                                                        //AST抽象语法树
      type: 1,
      ns: 0,
      tag: 'template',
      tagType: 0,
      props: [],
      isSelfClosing: false,
      children: [Array],
      loc: [Object],
      codegenNode: undefined
    },
    map: {                                                       //source-map相关?
      version: 3,
      sources: [Array],
      names: [],
      mappings: ';EACE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC',
      file: 'D:/EXAM/test/fof/src/App.vue',
      sourceRoot: '',
      sourcesContent: [Array]
    }
  }, 
  script: null,                                                  //script标签内元素
  scriptSetup: {                                                 //<script setup>内元素
    type: 'script',
    content: '\r\n' +
      "import './normalize.css'\r\n" +
      'import store from "@/store";\r\n' +
      '\r\n' +
      '\r\n' +
      "document.body.className = !store.state.themeValue ? 'dark' : 'light'\r\n" +
      '\r\n' +
      '\r\n',
    loc: {
      source: '\r\n' +
        "import './normalize.css'\r\n" +
        'import store from "@/store";\r\n' +
        '\r\n' +
        '\r\n' +
        "document.body.className = !store.state.themeValue ? 'dark' : 'light'\r\n" +
        '\r\n' +
        '\r\n',
      start: [Object],
      end: [Object]
    },
    attrs: { setup: true },
    setup: true
  },
  styles: [],                                                   //style标签内元素
  customBlocks: [],
  cssVars: [],
  slotted: false,
  shouldForceReload: [Function: shouldForceReload]

根据上面代码可以知道parse方法主要就是对descriptor对象实现然后给loader函数使用。

接下来一步步看看descriptor对象。

descriptor定义

 const descriptor = {
    filename,
    source,
    template: null,
    script: null,
    scriptSetup: null,
    styles: [],
    customBlocks: [],
    cssVars: [],
    slotted: false,
    shouldForceReload: (prevImports) => hmrShouldReload(prevImports, descriptor)
  };

上面代码定义了一些属性,其中filename,source是调用的时候传过来的。

ast结构

{
  type: 0,
  children: [
    {
      type: 1,
      ns: 0,
      tag: 'template',
      tagType: 0,
      props: [],
      isSelfClosing: false,
      children: [Array],
      loc: [Object],
      codegenNode: undefined
    },
    {
      type: 1,
      ns: 0,
      tag: 'script',
      tagType: 0,
      props: [Array],
      isSelfClosing: false,
      children: [Array],
      loc: [Object],
      codegenNode: undefined
    },
    {
      type: 1,
      ns: 0,
      tag: 'style',
      tagType: 0,
      props: [Array],
      isSelfClosing: false,
      children: [Array],
      loc: [Object],
      codegenNode: undefined
    }
  ],
  helpers: Set(0) {},
  components: [],
  directives: [],
  hoists: [],
  imports: [],
  cached: 0,
  temps: 0,
  codegenNode: undefined,
  loc: {
    start: { column: 1, line: 1, offset: 0 },
    end: { column: 1, line: 20, offset: 253 },
    source: '<template>\r\n' +
      '  <router-view></router-view>\r\n' +
      '</template>\r\n' +
      '\r\n' +
      '<script setup>\r\n' +
      "import './normalize.css'\r\n" +
      'import store from "@/store";\r\n' +
      '\r\n' +
      '\r\n' +
      "document.body.className = !store.state.themeValue ? 'dark' : 'light'\r\n" +
      '\r\n' +
      '\r\n' +
      '</script>\r\n' +
      '\r\n' +
      '<style scoped>\r\n' +
      '\r\n' +
      '</style>\r\n' +
      '\r\n' +
      '\r\n'
  }

抽象语法树,就是把DOM元素按照层级关系一层一层的抽象成一个对象。 知道了这个结构那接下来代码就可以分析了

  const ast = compiler.parse(source, {                              //调用parse方 ,生成抽象语法树
    // there are no components at SFC parsing level
    isNativeTag: () => true,
    // preserve all whitespaces
    isPreTag: () => true,
    getTextMode: ({ tag, props }, parent) => {
      if (!parent && tag !== "template" || // <template lang="xxx"> should also be treated as raw text
      tag === "template" && props.some(
        (p) => p.type === 6 && p.name === "lang" && p.value && p.value.content && p.value.content !== "html"
      )) {
        return 2;                                
      } else {                       
        return 0;
      }
    },
    onError: (e) => {
      errors.push(e);
    }
  });

  ast.children.forEach((node) => {          //将第一层children遍历    
    if (node.type !== 1) {
      return;
    }
    if (ignoreEmpty && node.tag !== "template" && isEmpty(node) && !hasSrc(node)) {
      return;
    }
    switch (node.tag) {
      case "template":                         //处理template标签
        if (!descriptor.template) {
          const templateBlock = descriptor.template = createBlock(      
            node,
            source,
            false
          );
          templateBlock.ast = node;
          if (templateBlock.attrs.functional) {
            const err = new SyntaxError(
              `<template functional> is no longer supported in Vue 3, since functional components no longer have significant performance difference from stateful ones. Just use a normal <template> instead.`
            );
            err.loc = node.props.find((p) => p.name === "functional").loc;
            errors.push(err);
          }
        } else {
          errors.push(createDuplicateBlockError(node));
        }
        break;
      case "script":                    //处理script标签
        const scriptBlock = createBlock(node, source, pad);
        const isSetup = !!scriptBlock.attrs.setup;
        if (isSetup && !descriptor.scriptSetup) {
          descriptor.scriptSetup = scriptBlock;
          break;
        }
        if (!isSetup && !descriptor.script) {
          descriptor.script = scriptBlock;
          break;
        }
        errors.push(createDuplicateBlockError(node, isSetup));
        break;
      case "style":                     //处理style标签
        const styleBlock = createBlock(node, source, pad);
        if (styleBlock.attrs.vars) {
          errors.push(
            new SyntaxError(
              `<style vars> has been replaced by a new proposal: https://github.com/vuejs/rfcs/pull/231`
            )
          );
        }
        descriptor.styles.push(styleBlock);
        break;
      default:
        descriptor.customBlocks.push(createBlock(node, source, pad));
        break;
    }
  });

上面代码通过parse方法生成了AST,如果想知道AST具体怎么生成的可以点击这儿:vue文件AST语法树是怎么生成的(还没创作)。

然后代码根据tag的类型分别将template标签,script标签以及style标签里面的内容赋值给了descriptor对象里的属性了。