A, start
The last article looked at Webpack packaging, where loaders are used to transform non-JS files, and this time we’ll look at a specific and common loader, vue-Loader.
This article will start from the source code, detailed analysis of vue-loader’s working principle, and explain the implementation principle of Scoped CSS, CSS Modules.
Here’s a flow chart:
Second, pre-knowledge
1. Pitching Loader
Loader is used to convert the source code of a module. Since WebPack only recognizes JS files, loader generally converts other files to JS files.
The loader is called from right to left and from bottom to top. However, before the actual execution of loader, the pitch method of Loader will be lowered from left to right and from top to bottom.
Here is an example from the official website.
The Webpack configuration is as follows:
module.exports = {
// ...
module: {
rules: [{use: ['a-loader'.'b-loader'.'c-loader'],}]}}Copy the code
The steps that will take place are:
|- a-loader `pitch` |- b-loader `pitch` |- c-loader `pitch` |- requested module is picked up as a dependency |- c-loader normal execution |- b-loader normal execution |- a-loader normal executionCopy the code
A feature of the pitching loader is the ability to share data. The third argument to pitch, data, is exposed to loader’s this.data, as in the following example:
module.exports = function (content) {
return someSyncOperation(content, this.data.value);
};
module.exports.pitch = function (remainingRequest, precedingRequest, data) {
data.value = 42;
};
Copy the code
Another function is that if a loader’s pitch method returns non-undefined, it will interrupt the execution of subsequent loaders, such as the b-loader in the above example:
module.exports = function (content) {
return someSyncOperation(content)
}
module.exports.pitch = function () {
return 'export {}'
}
Copy the code
The execution sequence of loader is reduced to:
|- a-loader `pitch`
|- b-loader `pitch` returns a module
|- a-loader normal execution
Copy the code
2. Inline loader
Loader can be used in two modes: configuration mode and inline mode.
- Configuration mode is the way we usually use it, which is in
webpack.config.js
The use ofloader
. - The inline mode is in
import/require
Specified in the statementloader
.
The following is an example of using loader in inline mode:
import Styles from 'style-loader! css-loader? modules! ./styles.css';
Copy the code
! /style. CSS files are processed by csS-loader and style-loader respectively, and css-loader passes modules.
The webpack configuration equivalent to the above statement is as follows:
module.exports = {
module: {
rules: [{test: /\.css$/,
use: [
{ loader: 'style-loader' },
{
loader: 'css-loader'.options: {
modules: true}}, {loader: 'sass-loader'}]}};Copy the code
Inline import statements can be prefixed to override the loader, preLoader, and postLoader in the configuration.
- use
!
Prefix, will disable allnormal loader
(ordinary loader)import Styles from '! style-loader! css-loader? modules! ./styles.css';
- use
!!!!!
Prefix, will disable all configuredloader
(preLoader
,loader
,postLoader
)import Styles from '!! style-loader! css-loader? modules! ./styles.css';
- use
-!
Prefix, will disable all configuredpreLoader
andloader
But I can’t help itpostLoader
import Styles from '-! style-loader! css-loader? modules! ./styles.css';
3. resourceQuery
When configuring loader, the test field is configured to match files:
{
test: /\.vue$/,
loader: 'vue-loader'
}
// When importing a vue file, the file content is transferred to vue-loader for processing
import Foo from './source.vue'
Copy the code
ResourceQuery can match files according to the reference path parameters of files. If the imported file path with query parameter matches, the loader is also loaded
{
resourceQuery: /vue=true/,
loader: path.resolve(__dirname, './test-loader.js')}// The following two files will be processed by test-loader
import './test.js? vue=true'
import Foo from './source.vue? vue=true'
Copy the code
Iii. Vue-loader principle
The version for this analysis is V15.9.8.
The VueLoaderPlugin can be used as follows.
// webpack.config.js
const VueLoaderPlugin = require('vue-loader/lib/plugin')
module.exports = {
module: {
rules: [
// ...
{
test: /\.vue$/,
loader: 'vue-loader'}},plugins: [
new VueLoaderPlugin()
]
}
Copy the code
1. VueLoaderPlugin
Let’s take a look at what the VueLoaderPlugin does:
// plugin-webpack4.js
const RuleSet = require('webpack/lib/RuleSet')
class VueLoaderPlugin = {
apply(compiler) {
// use webpack's RuleSet utility to normalize user rules
const rawRules = compiler.options.module.rules
const { rules } = new RuleSet(rawRules)
// for each user rule (except the vue rule), create a cloned rule
// that targets the corresponding language blocks in *.vue files.
const clonedRules = rules
.filter(r= >r ! == vueRule) .map(cloneRule)const pitcher = {
loader: require.resolve('./loaders/pitcher'),
resourceQuery: query= > {
const parsed = qs.parse(query.slice(1))
returnparsed.vue ! =null
},
options: {
cacheDirectory: vueLoaderUse.options.cacheDirectory,
cacheIdentifier: vueLoaderUse.options.cacheIdentifier
}
}
// replace original rules
compiler.options.module.rules = [
pitcher,
...clonedRules,
...rules
]
}
}
module.exports = VueLoaderPlugin
Copy the code
The VueLoaderPlugin first gets the original rules of Webpack and then creates the pitcher rule, which uses the loader in./loaders/pitcher for the file containing vue in query, i.e. PitcherLoader.
And for carrying? Vue&lang =xx files with query parameters create the same rules as.xx files. For example, the query carries? Vue&lang =ts, copy and apply the rules defined by the user for. Ts, such as TS-loader. These duplicated rules are called clonedRules.
Then we have [pitcher,…clonedRules,…rules] as the new rules.
2. Stage 1
The entry file for vue-loader is lib/index.js, which exports a method. This method is called twice as WebPack processes the Vue file.
The first was to parse the. Vue file into blocks according to the teplate/script/style types using the parse method.
const { parse } = require('@vue/component-compiler-utils')
function loadTemplateCompiler() {
return require('vue-template-compiler')}module.exports = function (source) {
const {
resourceQuery = ' ',
resourcePath
} = loaderContext = this;
const descriptor = parse({
source,
compiler: loadTemplateCompiler(loaderContext),
})
// template
let templateImport = `var render, staticRenderFns`
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}${inheritQuery}`
const request = templateRequest = stringifyRequest(src + query)
templateImport = `import { render, staticRenderFns } from ${request}`
}
// script
let scriptImport = `var script = {}`
if (descriptor.script) {
const src = descriptor.script.src || resourcePath
const attrsQuery = attrsToQuery(descriptor.script.attrs, 'js')
const query = `? vue&type=script${attrsQuery}${inheritQuery}`
const request = stringifyRequest(src + query)
scriptImport = (
`import script from ${request}\n` +
`export * from ${request}` // support named exports)}// styles
let stylesCode = ` `
if (descriptor.styles.length) {
stylesCode = genStylesCode(
loaderContext,
descriptor.styles,
id,
resourcePath,
stringifyRequest,
needsHotReload,
)
}
let code = `
${templateImport}
${scriptImport}
${stylesCode}
/* normalize component */
import normalizer from ${stringifyRequest(`!${componentNormalizerPath}`)}
var component = normalizer(
script,
render,
staticRenderFns,
${hasFunctional ? `true` : `false`}.The ${/injectStyles/.test(stylesCode) ? `injectStyles` : `null`}.${hasScoped ? JSON.stringify(id) : `null`}.${isServer ? JSON.stringify(hash(request)) : `null`}
${isShadow ? `,true` : ` `}
)
`.trim() + `\n`
code += `\nexport default component.exports`
return code
}
Copy the code
Parse @vue/component-compiler-utils as follows:
function parse(options) {
const {
source,
filename = ' ',
compiler,
compilerParseOptions = { pad: 'line' },
sourceRoot = ' ',
needMap = true
} = options
const cacheKey = hash(
filename + source + JSON.stringify(compilerParseOptions)
)
let output = cache.get(cacheKey)
if (output) return output
output = compiler.parseComponent(source, compilerParseOptions)
if (needMap) {
if(output.script && ! output.script.src) { output.script.map = generateSourceMap() }if (output.styles) {
output.styles.forEach(style= > {
if(! style.src) { style.map = generateSourceMap() } }) } } cache.set(cacheKey, output)return output
}
Copy the code
ParseComponent (source) returns the output of the cache. If the output is not cached, the parse method calls Compiler.parsecomponent (source) to fetch the output. Assigns the map property to output.script and output.styles and returns output.
Compiler here is the compiler method derived from vue-template-compiler.
@vue/component-compiler-utils The name of this library is just a tool. The core logic is still in other libraries, such as vue-template-compiler here.
ParseComponent is in vue/ SRC/SFC /parser.js, which returns an object of the following structure:
{
template: null.script: null.styles: [].customBlocks: [].errors: []}Copy the code
Vue-loader determines the template/script/style of descriptor and concatenates the new reference path of each descriptor. The exported content is as follows:
import { render, staticRenderFns } from "./empty-state.vue? vue&type=template&id=619de588&scoped=true&"
import script from "./empty-state.vue? vue&type=script&lang=js&"
export * from "./empty-state.vue? vue&type=script&lang=js&"
import style0 from "./empty-state.vue? vue&type=style&index=0&id=619de588&scoped=true&lang=scss&"
/* normalize component */
import normalizer from ! "" . /.. /.. /.. /.. /.. /node_modules/vue-loader/lib/runtime/componentNormalizer.js"
var component = normalizer(
script,
render,
staticRenderFns,
false.null."619de588".null
)
export default component.exports
Copy the code
3. Stage 2
In the VueLoaderPlugin mentioned above, pitcherLoader is applied if the file query contains vue.
The first phase exports with vUE parameters that trigger the pitcherLoader call. pitcherLoader
const templateLoaderPath = require.resolve('./templateLoader')
const isPitcher = l= >l.path ! == __filenameconst isPreLoader = l= >! l.pitchExecutedconst isPostLoader = l= > l.pitchExecuted
module.exports = code= > code
module.exports.pitch = function () {
const query = qs.parse(this.resourceQuery.slice(1))
let loaders = this.loaders
// remove self
loaders = loaders.filter(isPitcher)
const genRequest = loaders= > {
const seen = new Map(a)const loaderStrings = []
loaders.forEach(loader= > {
const identifier = typeof loader === 'string'
? loader
: (loader.path + loader.query)
const request = typeof loader === 'string' ? loader : loader.request
if(! seen.has(identifier)) { seen.set(identifier,true)
loaderStrings.push(request)
}
})
return loaderUtils.stringifyRequest(this.'-! ' + [
...loaderStrings,
this.resourcePath + this.resourceQuery
].join('! '))}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)
const request = genRequest([
...afterLoaders,
stylePostLoaderPath,
...beforeLoaders
])
return query.module
? `export { default } from ${request}; export * from ${request}`
: `export * from ${request}`}}if (query.type === `template`) {
const preLoaders = loaders.filter(isPreLoader)
const postLoaders = loaders.filter(isPostLoader)
const request = genRequest([
...cacheLoader,
...postLoaders,
templateLoaderPath + `?? vue-loader-options`. preLoaders ])return `export * from ${request}`
}
const request = genRequest(loaders)
return `import mod from ${request}; export default mod; export * from ${request}`
}
Copy the code
The Noraml loader part of pitcherLoader does nothing and directly returns the previous code, so the core is on the pitch method.
Since the pitch method returns non-undefined and it is the first loader, subsequent loaders will be skipped.
The pitcherLoader replaces the query of the previous file with the corresponding query with the loader, as mentioned above.
For a reference to a type=template file, such as:
import { render, staticRenderFns } from "./create-team-dialog.vue? vue&type=template&id=127d8294&scoped=true&"
Copy the code
The pitcherLoader will replace the reference to this file with:
export * from "-! . /.. /.. /.. /.. /.. /node_modules/cache-loader/dist/cjs.js? {\"cacheDirectory\":\"node_modules/.cache/vue-loader\",\"cacheIdentifier\":\"2f391a00-vue-loader-template\"}! . /.. /.. /.. /.. /.. /node_modules/vue-loader/lib/loaders/templateLoader.js?? vue-loader-options! . /.. /.. /.. /.. /.. /node_modules/cache-loader/dist/cjs.js?? ref--0-0! . /.. /.. /.. /.. /.. /node_modules/vue-loader/lib/index.js?? vue-loader-options! ./create-team-dialog.vue? vue&type=template&id=127d8294&scoped=true&"
Copy the code
The intra-line loaders are executed from right to left, that is, vue-loader, cache-loader, templateLoader, and cache-loader are executed in sequence.
The following is the result of converting a file whose type is style:
import mod from "-! . /.. /.. /.. /.. /node_modules/vue-style-loader/index.js?? ref--6-oneOf-1-0! . /.. /.. /.. /.. /node_modules/css-loader/dist/cjs.js?? ref--6-oneOf-1-1! . /.. /.. /.. /.. /node_modules/vue-loader/lib/loaders/stylePostLoader.js! . /.. /.. /.. /.. /node_modules/postcss-loader/src/index.js?? ref--6-oneOf-1-2! . /.. /.. /.. /.. /node_modules/postcss-loader/src/index.js?? ref--6-oneOf-1-3! . /.. /.. /.. /.. /node_modules/cache-loader/dist/cjs.js?? ref--0-0! . /.. /.. /.. /.. /node_modules/vue-loader/lib/index.js?? vue-loader-options! ./gp.vue? vue&type=style&index=0&id=6e34f811&scoped=true&lang=css&";
export default mod;
export * from "/* reference address as above */"
Copy the code
For the preceding style files, the loader execution sequence is vue-loader, cache-loader, postCSS-loader * 2, vue-loader/stylePostLoader, CSS-loader, vue-style-loader.
For files of type=script, the last logic is used to convert the original loaders to the request parameter via genRequest, overwriting the previous file reference.
const request = genRequest(loaders)
return `import mod from ${request}; export default mod; export * from ${request}`
Copy the code
PitcherLoader replaces the file containing vUE in the query argument with the above reference, and then calls the inline Loader on the Query in turn. The first loader to be inlined is vue-loader, so vue-loader is called again.
Stage 3
This time vue-loader will simply execute the next loader according to Query. type.
module.exports = function (source) {
const incomingQuery = qs.parse(resourceQuery.slice(1))
if(! incomingQuery.type) {// Enter vue-loader for the second time
return selectBlock(
descriptor,
loaderContext,
incomingQuery,
)
}
}
function selectBlock() {
if (query.type === `template`) {
if (appendExtension) {
loaderContext.resourcePath += '. ' + (descriptor.template.lang || 'html')
}
loaderContext.callback(
null,
descriptor.template.content,
descriptor.template.map
)
return
}
if (query.type === `script`) {}if (query.type === `style`) {}if (query.type === `custom`) {}/ /...
}
Copy the code
For some type = template, insert a templateLoader pitcherLoader, the loader in the lib/loaders/templateLoader in js:
const { compileTemplate } = require('@vue/component-compiler-utils')
module.exports = function (source) {
// ...
const compiled = compileTemplate(finalOptions)
const { code } = compiled
return code + `\nexport { render, staticRenderFns }`
}
Copy the code
TemplateLoader calls the @vue/component-compiler-utils method compileTemplate and returns an object containing the render method, which is the core functionality of vue-Loader. Turn the component into a render function.
Here’s an example:
var render = function() {
var _vm = this;
var _h = _vm.$createElement;
var _c = _vm._self._c || _h;
return _c('div', {
staticClass: "wrap"
},
[_c('a', {
staticClass: "tip-toc-commbtn tip-btn-primary".on: {
"click": function($event) {
$event.stopPropagation();
return _vm.enterGame($event)
}
}
},
[_vm._v("Enter the game")]])}var staticRenderFns = []
export { render, staticRenderFns }
Copy the code
For some type = style, insert the stylePostLoader pitcherLoader, the loader in the lib/loaders/stylePostLoader in js:
const { compileStyle } = require('@vue/component-compiler-utils')
module.exports = function (source, inMap) {
const query = qs.parse(this.resourceQuery.slice(1))
const { code, map, errors } = compileStyle({
source,
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)
}
}
Copy the code
StylePostLoader calls compileStyle, which preends [data-v-hash] to the property selector containing the scoped style property.
The style section is later added to the head via style-loader, or extracted into a public CSS file via the miniCssExtractPlugin.
Get script, Render and staticRenderFns, call normalizeComponent at run time and return component, which includes options and exports.
Four,
The workflow of VUe-Loader is summarized as follows:
- will
.vue
File split intotemplate/script/styles
Three parts template
Part of the afterpitcherLoader
,templateLoader
“Will eventually passcompile
generaterender
andstaticRenderFns
- To obtain
script
Part, namedscript
In the backnormalizeComponent
Will be used in, and exportedscript
. styles
Part of the afterpitcherLoader
,stylePostLoader
“Will eventually passcss-loader
,vue-style-loader
Added to thehead
Medium, or passcss-loader
,miniCssExtractPlugin
Extract to a publiccss
File.- use
vue-loader
thenormalizeComponent
Method, mergescript
,Render and staticRenderFns
To return tocomponent
, which includes aoptions
andexports
.
Check out the Scoped CSS workflow:
vue-loader
In dealing with.vue
Of the filetemplate
Is generated based on the file path and file contenthash
Value.- if
.vue
In the filescoped
thestyle
Tag, generates onescopedId
likedata-v-hash
Here,hash
That’s up therehash
Value. - for
vue
In thestyle
Part,vue-loader
Will be incss-loader
Before adding their ownstylePostLoader
.stylePostLoader
Attributes are added to each selector[data-v-hash]
And then throughstyle-loader
thecss
Added to thehead
Medium, or passminiCssExtractPlugin
thecss
Extract into separate files. vue-loader
thenormalizeComponent
Method, determine ifvue
In the filescoped
thestyle
, then it returnsoptions._scopeId
For the abovescopedId
.- The above
_scopedId
invnode
When the DOM is generated for renderingdom
Increment and increment on the elementscopedId
That is, increasedata-v-hash
.
Through the above process, CSS modules are privatized.
In addition, a brief introduction to CSS Modules principle:
vue-loader
In dealing with.vue
File when encountered containsmodule
thestyle
Tag, will be generatedcode
In the injectioninjectStyles
Method, which executesthis["a"] = (style0.locals || style0)
orthis["$style"] = (style1.locals || style1)
So that it can be used in vue filesthis.$style.class0
The introduction of modular classes andid
.css-loader
rightvue
In the filestyle
Partial analysis, exportlocals
Property, and the original class nameid
Convert to a unique value.normalizeComponent
Determine if it containsinjectStyles
, it will berender
Method is packaged as containinginjectStyles
therenderWithStyleInjection
Methods.vue
When instantiated, it is executed firstinjectStyles
Method, and then execute the originalrender
Methods. suchvue
You can get it on the example$style
The name of the class andid
In this way, the modularity of CSS is realized.
V. Relevant materials
- Vue-loader in-depth learning
- Read the vue-loader principle
- Deeper into vue-loader principle
- Analyze the implementation of CSS Scoped from vue-loader source code
- Css-loader style-Loader Mechanism
- Implementation principles of less-loader, CSS-loader, and style-loader
- Webpack several common loader source analysis