先简介一下 webapck 中 loader 中的工作原理。
webpack 是只识别 js 代码的。但是我们写的代码中有很多类型的文件,比如:.vue
、.yaml
、.json
、.css
、.tsx
等等这些后缀文件,这些文件都是不被 webpack 所识别的,因此需要在webapck 打包前进行“翻译”。loader 的作用就是“翻译”的作用,将非 js 文件进行转义成 js 代码,然后 webpack 才会识别。
废话不多说,下面进入正题。
下载 vue-loader 源码:
yarn add vue-loader vue-template-compiler --dev
在 node_modules
中找到 vue-loader 文件夹下 lib/index.js
可以找到这块代码
const descriptor = parse({
source,
compiler: options.compiler || loadTemplateCompiler(loaderContext),
filename,
sourceRoot,
needMap: sourceMap
})
通过上面代码的运行 parse 函数,传入文件的 source 源码就会将整个文件解析成
下面用一段测试代码:
<template>
<div>
这里是div标签
<a>这是a标签</a>
</div>
</template>
<script>
export default {
data() {
return {
};
},
};
</script>
输出descriptor:
descriptor {
template: {
type: 'template',
content: '\n<div>\n 这里是div标签\n <a>这是a标签</a>\n</div>\n',
start: 10,
attrs: {
},
end: 58
},
script: {
type: 'script',
content: '//\n' +
'//\n' +
'//\n' +
'//\n' +
'//\n' +
'//\n' +
'//\n' +
'\n' +
'export default {\n' +
' data() {\n' +
' return {};\n' +
' },\n' +
'};\n',
start: 79,
attrs: {
},
end: 131
},
styles: [],
customBlocks: [],
errors: []
}
已经将 vue 文件中的 html 标签和 script 标签和 style 标签区分出来了。
那么原理是什么呢?
上面 parse 函数中传如 compiler 参数作为编译函数,loadTemplateCompiler 如下
function loadTemplateCompiler (loaderContext) {
try {
return require('vue-template-compiler')
} catch (e) {
if (/version mismatch/.test(e.toString())) {
loaderContext.emitError(e)
} else {
loaderContext.emitError(new Error(
`[vue-loader] vue-template-compiler must be installed as a peer dependency, ` +
`or a compatible compiler implementation must be passed via options.`
))
}
}
}
可以看到引入了 vue-template-compiler 这个文件node_modules/vue-template-compiler/build.js
其中 parseHTML 这个函数起到了解析源码的作用
function parseHTML (html, options) {
...
while (html) {
if (!lastTag || !isPlainTextElement(lastTag)) {
var textEnd = html.indexOf('<');
if (textEnd === 0) {
var endTagMatch = html.match(endTag);
if (comment.test(html)) {
...
continue
}
if (endTagMatch) {
...
continue
}
if (startTagMatch) {
...
continue
}
}
}
}
}
原理是,html 是.vue
后缀文件的源码(string 类型),然后通过正则不断的去匹配标签,然后将不同标签类型都进行一次判断:
在parseComponent
这个函数中,进行判断每个标签是否是特殊标签:<template>
、 <script>
、<style>
,如果是的话就按照对应的 block 存起来。最后返回的结果就是上面的 descriptor 结果。
function parseComponent (
content,
options
) {
...
//这里是 start 函数,每次正则匹配到 < 开始的标签时就会判断是不是特殊标签,如果是的话就存到sfc 变量里。sfc是 single file component 单文件组件。
function start (
tag,
attrs,
unary,
start,
end
) {
if (depth === 0) {
currentBlock = {
type: tag,
content: '',
start: end,
attrs: attrs.reduce(function (cumulated, ref) {
var name = ref.name;
var value = ref.value;
cumulated[name] = value || true;
return cumulated
}, {
})
};
if (isSpecialTag(tag)) {
checkAttrs(currentBlock, attrs);
if (tag === 'style') {
sfc.styles.push(currentBlock);
} else {
sfc[tag] = currentBlock;
}
} else {
// custom blocks
sfc.customBlocks.push(currentBlock);
}
}
}
//这里是匹配到结尾时,把开头和结尾中的内容放在 sfc对应 block 的 content 里面
function end (tag, start) {
if (depth === 1 && currentBlock) {
currentBlock.end = start;
var text = content.slice(currentBlock.start, currentBlock.end);
if (options.deindent !== false) {
text = deindent(text);
}
// pad content so that linters and pre-processors can output correct
// line numbers in errors and warnings
if (currentBlock.type !== 'template' && options.pad) {
text = padContent(currentBlock, options.pad) + text;
}
currentBlock.content = text;
currentBlock = null;
}
depth--;
}
//这里调用了上面写的 parseHTML 函数
parseHTML(content, {
warn: warn,
start: start,
end: end,
outputSourceRange: options.outputSourceRange
});
return sfc
}