- Column address: Front-end compilation and engineering
- Series of articles: Babel stuff, a preview of the Vue file CLI tool, learning about the Babel plug-in through a “snazzy” example
- Jouryjc
Today we will take a look at the use of customBlocks in SFC and how it works.
Outline of this paper:
-
Understand customBlocks and basic configurations from < I18N > of VUE-I18N;
-
Understand vue-Loader’s handling of customBlocks from the source code level
vue-i18n
Vue-i18n is an internationalization plug-in for VUE. If you use SFC to write components, you can define the
block in the.vue file and write the corresponding entries within the block. This I18N tag is customBlocks. Here’s an example:
<template> <p>{{ $t('hello') }}</p> </template> <script> // App.vue export default { name: 'App' } </script> <i18n locale="en"> { "hello": "hello, world!!!!" } < / i18n > < i18n locale = "ja" > {" hello ":" こ ん に ち は, world!" } </i18n>Copy the code
// main.js
import Vue from 'vue'
import VueI18n from 'vue-i18n'
import App from './App.vue'
Vue.use(VueI18n)
const i18n = new VueI18n({
locale: 'ja'.messages: {}})new Vue({
i18n,
el: '#app'.render: h= > h(App)
})
Copy the code
This code defines both Japanese and English syntax, and can be used to switch languages by changing the locale value. In addition to the above usage, it is also supported to import files such as YAML or JSON:
<i18n src="./locales.json"></i18n>
Copy the code
// locales.json
{
"en": {
"hello": "hello world"
},
"ja": {
"hello": "Kohlinger: The world."}}Copy the code
For other uses, please refer to the usage documentation.
For customBlock to work, you need to specify the Loader for customBlock, otherwise the block is silently ignored. Webpack configuration at 🌰 :
const path = require('path')
const VueLoaderPlugin = require('vue-loader/lib/plugin')
module.exports = {
mode: 'development'.entry: path.resolve(__dirname, './main.js'),
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js'.publicPath: '/dist/'
},
devServer: {
stats: 'minimal'.contentBase: __dirname
},
module: {
rules: [{test: /\.vue$/,
loader: 'vue-loader'
},
{
test: /\.js$/,
loader: 'babel-loader'
},
// Rule corresponding to customBlocks
{
// Use resourceQuery to match a rule for a custom block that has no lang
// If a custom block matching rule is found, it will be processed, otherwise the custom block is silently ignored
resourceQuery: /blockType=i18n/.// rule-type Sets the type to match the module. It prevents defaultRules and their default import behavior from occurring
type: 'javascript/auto'.// This refers to vue-i18n-loader
use: [path.resolve(__dirname, '.. /lib/index.js')]}},plugins: [new VueLoaderPlugin()]
}
Copy the code
As you can see from the above code, if you want to use the customBlock functionality in the SFC, there are only two steps:
- Implement a process
customBlock
的loader
Functions; - configuration
webpack.module.rules
, specifyResourceQuery: /blockType= Your block name /
And then use step oneloader
To deal with it;
Source code analysis
In general, a loader is a conversion or loader for a specific resource, but vue-loader is not. It can handle each block defined in the SFC: By dismantling the block -> combining loader -> processing the block -> combining the result of each block into the final code workflow, complete the processing of the SFC. Let’s disassemble this assembly line in detail.
Dismantling block
We know that vue-loader-plugin must be introduced to use vue-loader, otherwise it will give you a big error:
`vue-loader was used without the corresponding plugin. Make sure to include VueLoaderPlugin in your webpack config.`
Copy the code
VueLoaderPlugin is defined in vue-loader\lib\plugin-webpack4.js:
const id = 'vue-loader-plugin'
const NS = 'vue-loader'
class VueLoaderPlugin {
apply (compiler) {
// add NS marker so that the loader can detect and report missing plugin
if (compiler.hooks) {
// webpack 4
compiler.hooks.compilation.tap(id, compilation= > {
const normalModuleLoader = compilation.hooks.normalModuleLoader // Synchronize hooks to manage all module loaders
normalModuleLoader.tap(id, loaderContext= > {
loaderContext[NS] = true})})}// use webpack's RuleSet utility to normalize user rules
const rawRules = compiler.options.module.rules
// https://webpack.js.org/configuration/module/#modulerules
const { rules } = new RuleSet(rawRules)
// Copy the loader you defined and apply it to the corresponding language block in the.vue file
const clonedRules = rules
.filter(r= >r ! == vueRule) .map(cloneRule)// ...
Pitcher is a pitcher who scores a goal, so it can be understood as giving the current block and loader rich features 😁
// Add template-loader to template block and stype-post-loader to style block
// Other features... Back to see
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
}
}
// Override the original rules configuration
compiler.options.module.rules = [
pitcher,
...clonedRules,
...rules
]
}
}
Copy the code
The VueLoaderPlugin adds the other loaders you define to the blocks of the SFC and modifies the module.rules in the configuration. Pitcher-loader was an important follow-up. There is a detailed explanation of Webpack Loader. If you don’t know Webpack Loader, you can first understand the role of the “pitcher”.
Having learned about the VueLoaderPlugin, we see vue-loader:
module.exports = function (source) {
const loaderContext = this
// ...
// Compile SFC -- parse.vue files to generate different blocks
const descriptor = parse({
source,
compiler: options.compiler || loadTemplateCompiler(loaderContext), // Vue-template-compiler is used by default
filename,
sourceRoot,
needMap: sourceMap
})
// ...
}
Copy the code
This parse method is at the heart of this section. Pass the SFC code to a custom compiler or the default @vue/component-compiler-utils for parsing. The specific execution process here is not carried out a detailed analysis, interested in children’s shoes can go to [cafe chat] “template compilation”. The result of the generated descriptor is as follows:
Next, generate the first code for each key in descriptor:
module.exports = function (source) {
const loaderContext = this
// ...
// Compile SFC -- parse.vue files to generate different blocks
const descriptor = parse({
source,
compiler: options.compiler || loadTemplateCompiler(loaderContext), // Vue-template-compiler is used by default
filename,
sourceRoot,
needMap: sourceMap
})
// ...
// 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,
isServer || isShadow // needs explicit injection?)}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`
Call genCustomBlocksCode to generate the code for the custom block
if (descriptor.customBlocks && descriptor.customBlocks.length) {
code += genCustomBlocksCode(
descriptor.customBlocks,
resourcePath,
resourceQuery,
stringifyRequest
)
}
/ /... Omit some heat change code
return code
}
// vue-loader\lib\codegen\customBlocks.js
module.exports = function genCustomBlocksCode (blocks, resourcePath, resourceQuery, stringifyRequest) {
return `\n/* custom blocks */\n` + blocks.map((block, i) = > {
// i18n can be used in many ways, including by importing other resources directly via SRC
// For demo, no external resources are defined.
const src = block.attrs.src || resourcePath
// Get other attributes, such as &locale=en and &locale=ja in demo
const attrsQuery = attrsToQuery(block.attrs)
// demo is ""
const issuerQuery = block.attrs.src ? `&issuerPath=${qs.escape(resourcePath)}` : ' '
// demo is ""
const inheritQuery = resourceQuery ? ` &${resourceQuery.slice(1)}` : ' '
const query = `? vue&type=custom&index=${i}&blockType=${qs.escape(block.type)}${issuerQuery}${attrsQuery}${inheritQuery}`
return (
`import block${i} from ${stringifyRequest(src + query)}\n` +
`if (typeof block${i} === 'function') block${i}(component)`
)
}).join(`\n`) + `\n`
}
Copy the code
We’ll skip the template, style, and script blocks and focus on customBlocks processing logic. The logic is simple: iterate over customBlocks to get some query variables and return the customBlocks code. Let’s look at the code that is finally returned from the first call to vue-loader:
/ * * / template block
import { render, staticRenderFns } from "./App.vue? vue&type=template&id=a9794c84&"
/ * * / script block
import script from "./App.vue? vue&type=script&lang=js&"
export * from "./App.vue? vue&type=script&lang=js&"
/* normalize component */
import normalizer from ! "" . /node_modules/vue-loader/lib/runtime/componentNormalizer.js"
var component = normalizer(
script,
render,
staticRenderFns,
false.null.null.null
)
/* Custom block, in this case the
block code */
import block0 from "./App.vue? vue&type=custom&index=0&blockType=i18n&locale=en"
if (typeof block0 === 'function') block0(component)
import block1 from "./App.vue? vue&type=custom&index=1&blockType=i18n&locale=ja"
if (typeof block1 === 'function') block1(component)
/* hot reload */
if (module.hot) {
var api = require("C:\\Jouryjc\\vue-i18n-loader\\node_modules\\vue-hot-reload-api\\dist\\index.js")
api.install(require('vue'))
if (api.compatible) {
module.hot.accept()
if(! api.isRecorded('a9794c84')) {
api.createRecord('a9794c84', component.options)
} else {
api.reload('a9794c84', component.options)
}
module.hot.accept("./App.vue? vue&type=template&id=a9794c84&".function () {
api.rerender('a9794c84', {
render: render,
staticRenderFns: staticRenderFns
})
})
}
}
component.options.__file = "example/App.vue"
export default component.exports
Copy the code
Continue with import:
/ * * / template block
import { render, staticRenderFns } from "./App.vue? vue&type=template&id=a9794c84&"
/ * * / script block
import script from "./App.vue? vue&type=script&lang=js&"
/* Custom block, in this case the
block code */
import block0 from "./App.vue? vue&type=custom&index=0&blockType=i18n&locale=en"
import block1 from "./App.vue? vue&type=custom&index=1&blockType=i18n&locale=ja"
Copy the code
Combination of loader
We can see that all of the above resources are present, right? Vue’s query parameter matches pitcher-loader, and the “pitcher” appears. Import block0 from “./ app.vue? Vue&type = custom&index = 0 & blockType = i18n & locale = en “handling:
module.exports.pitch = function (remainingRequest) {
const options = loaderUtils.getOptions(this)
const { cacheDirectory, cacheIdentifier } = options
const query = qs.parse(this.resourceQuery.slice(1))
let loaders = this.loaders
// if this is a language block request, eslint-loader may get matched
// multiple times
if (query.type) {
/ / remove eslint - loader
if (/\.vue$/.test(this.resourcePath)) {
loaders = loaders.filter(l= >! isESLintLoader(l)) }else {
// This is a src import. Just make sure there's not more than 1 instance
// of eslint present.
loaders = dedupeESLintLoader(loaders)
}
}
/ / to extract the pitcher - loader
loaders = loaders.filter(isPitcher)
// do not inject if user uses null-loader to void the type (#1239)
if (loaders.some(isNullLoader)) {
return
}
const genRequest = loaders= > {
// Important: dedupe since both the original rule
// and the cloned rule would match a source import request.
// also make sure to dedupe based on loader path.
// assumes you'd probably never want to apply the same loader on the same
// file twice.
// Exception: in Vue CLI we do need two instances of postcss-loader
// for user config and inline minification. So we need to dedupe baesd on
// path AND query to be safe.
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)
// loader.request contains both the resolved loader path and its options
// query (e.g. ?? ref-0)
loaderStrings.push(request)
}
})
return loaderUtils.stringifyRequest(this.'-! ' + [
...loaderStrings,
this.resourcePath + this.resourceQuery
].join('! '))}// script, template, style...
// if a custom block has no other matching loader other than vue-loader itself
// or cache-loader, we should ignore it
// If there is no other loader than vue-loader, ignore it
if (query.type === `custom` && shouldIgnoreCustomBlock(loaders)) {
return ` `
}
// When the user defines a rule that has only resourceQuery but no test,
// both that rule and the cloned rule will match, resulting in duplicated
// loaders. Therefore it is necessary to perform a dedupe here.
const request = genRequest(loaders)
return `import mod from ${request}; export default mod; export * from ${request}`
}
Copy the code
Pitcher-loader does three things:
- To eliminate
eslint-loader
, avoid repetitionlint
; - To eliminate
pitcher-loader
Itself; - According to different
query.type
And generate the correspondingrequest
And returns the result;
CustomBlocks in 🌰 returns the following result:
// en
import mod from "-! . /lib/index.js! . /node_modules/vue-loader/lib/index.js?? vue-loader-options! ./App.vue? vue&type=custom&index=0&blockType=i18n&locale=en";
export default mod;
export * from "-! . /lib/index.js! . /node_modules/vue-loader/lib/index.js?? vue-loader-options! ./App.vue? vue&type=custom&index=0&blockType=i18n&locale=en"
// ja
import mod from "-! . /lib/index.js! . /node_modules/vue-loader/lib/index.js?? vue-loader-options! ./App.vue? vue&type=custom&index=1&blockType=i18n&locale=ja";
export default mod;
export * from "-! . /lib/index.js! . /node_modules/vue-loader/lib/index.js?? vue-loader-options! ./App.vue? vue&type=custom&index=1&blockType=i18n&locale=ja"
Copy the code
Processing block,
Vue -i18n-loader = vue-i18n-loader = vue-i18n-loader = vue-i18n-loader = vue-i18n-loader = vue-i18n-loader = vue-i18n-loader = vue-i18n-loader Incomingquery.type has a value. For custom, this is custom:
// ...
// if the query has a type field, this is a language block request
// e.g. foo.vue? type=template&id=xxxxx
// and we will return early
if (incomingQuery.type) {
returnselectBlock( descriptor, loaderContext, incomingQuery, !! options.appendExtension ) }// ...
Copy the code
Executes to selectBlock:
module.exports = function selectBlock (descriptor, loaderContext, query, appendExtension) {
// template
// script
// style
// custom
if (query.type === 'custom'&& query.index ! =null) {
const block = descriptor.customBlocks[query.index]
loaderContext.callback(
null,
block.content,
block.map
)
return}}Copy the code
Vue -i18n-loader:
const loader: webpack.loader.Loader = function (
source: string | Buffer,
sourceMap: RawSourceMap | undefined
) :void {
if (this.version && Number(this.version) >= 2) {
try {
// Cache the result directly if the input and dependencies have not changed
this.cacheable && this.cacheable()
// Output the result
this.callback(
null.`module.exports = ${generateCode(source, parse(this.resourceQuery))}`,
sourceMap
)
} catch (err) {
this.emitError(err.message)
this.callback(err)
}
} else {
const message = 'support webpack 2 later'
this.emitError(message)
this.callback(new Error(message))
}
}
/** * Generate code for the i18n tag *@param {string | Buffer} source
* @param {ParsedUrlQuery} query
* @returns {string} code* /
function generateCode(source: string | Buffer, query: ParsedUrlQuery) :string {
const data = convert(source, query.lang as string)
let value = JSON.parse(data)
if (query.locale && typeof query.locale === 'string') {
value = Object.assign({}, { [query.locale]: value })
}
// Special character escape, \u2028 -> line delimiter, \u2029 -> paragraph delimiter, \\ backslash
value = JSON.stringify(value)
.replace(/\u2028/g.'\\u2028')
.replace(/\u2029/g.'\\u2029')
.replace(/\\/g.'\ \ \ \')
let code = ' '
code += `function (Component) {
Component.options.__i18n = Component.options.__i18n || []
Component.options.__i18n.push('${value.replace(/\u0027/g.'\\u0027')}')
delete Component.options._Ctor
}\n`
return code
}
/** * convert various usages to JSON string */
function convert(source: string | Buffer, lang: string) :string {
const value = Buffer.isBuffer(source) ? source.toString() : source
switch (lang) {
case 'yaml':
case 'yml':
const data = yaml.safeLoad(value)
return JSON.stringify(data, undefined.'\t')
case 'json5':
return JSON.stringify(JSON5.parse(value))
default:
return value
}
}
export default loader
Copy the code
Get the source and generate the value, which is pushed into component.options.__i18n. There are different ways to handle it for different situations (JSON, YAML, etc.).
At this point, the entire vue file is finished, and
finally builds the following code:
"./lib/index.js! ./node_modules/vue-loader/lib/index.js? ! ./example/App.vue? vue&type=custom&index=0&blockType=i18n&locale=en":
(function (module.exports) {
eval("module.exports = function (Component) {\n Component.options.__i18n = Component.options.__i18n || []\n Component.options.__i18n.push('{\"en\":{\"hello\":\"hello, world!!!! \"}}')\n delete Component.options._Ctor\n}\n\n\n//# sourceURL=webpack:///./example/App.vue? ./lib! ./node_modules/vue-loader/lib?? vue-loader-options");
})
Copy the code
Vue-i18n identifies component.options.__i18n as a component.options.
if (options.__i18n) {
try {
let localeMessages = options.i18n && options.i18n.messages ? options.i18n.messages : {};
options.__i18n.forEach(resource= > {
localeMessages = merge(localeMessages, JSON.parse(resource));
});
Object.keys(localeMessages).forEach((locale) = > {
options.i18n.mergeLocaleMessage(locale, localeMessages[locale]);
});
} catch (e) {
{
error(`Cannot parse locale messages via custom blocks.`, e); }}}Copy the code
conclusion
Starting with vuE-I18n’s tools, this article shares how to define a custom block in SFC. Then, the processing process of SFC is analyzed from vue-Loader source code, and the whole process is shown as follows:
- from
webpack
The build starts, the plug-in is called,VueLoaderPlugin
在normalModuleLoader
Hooks are executed; - The introduction of
SFC
Is matched for the first timevue-loader
, will pass@vue/component-compiler-utils
Parsing code into different blocks, for exampletemplate
,script
,style
,custom
; - The generated
code
, will continue to matchloader
.? vue
Will match the “pitcher.”pitcher-loader
; pitcher-loader
There are three main things to do: First, becausevue
The entire file has beenlint
Processed, so local code is filtered outeslint-loader
; Second, filter out yourselfpitcher-loader
; At last,query.type
To generate different onesrequest
和code
;- In the end
code
It will match againvue-loader
Is executed the second time,incomingQuery.type
Will specify the corresponding block, so will be based ontype
callselectBlock
Generate the final block code.