• 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:

  1. Implement a processcustomBlockloaderFunctions;
  2. configurationwebpack.module.rules, specifyResourceQuery: /blockType= Your block name /And then use step oneloaderTo 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 eliminateeslint-loader, avoid repetitionlint;
  • To eliminatepitcher-loaderItself;
  • According to differentquery.typeAnd generate the correspondingrequestAnd 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:

  1. fromwebpackThe build starts, the plug-in is called,VueLoaderPluginnormalModuleLoaderHooks are executed;
  2. The introduction ofSFCIs matched for the first timevue-loader, will pass@vue/component-compiler-utilsParsing code into different blocks, for exampletemplate,script,style,custom;
  3. The generatedcode, will continue to matchloader.? vueWill match the “pitcher.”pitcher-loader;
  4. pitcher-loaderThere are three main things to do: First, becausevueThe entire file has beenlintProcessed, so local code is filtered outeslint-loader; Second, filter out yourselfpitcher-loader; At last,query.typeTo generate different onesrequestcode;
  5. In the endcodeIt will match againvue-loaderIs executed the second time,incomingQuery.typeWill specify the corresponding block, so will be based ontypecallselectBlockGenerate the final block code.