background

Recently, I am making a trial of cross-platform framework, choosing UNI-app, and I plan to start the trial on H5 first.

Since the vuE-CLI-based scaffolding provided by Uni-App is slightly different from our internal scaffolding and there is a little bit of learning cost to use directly, we forked a little bit and made an internal version of the scaffolding (basically taking publicPath out of manifest.json, Dynamically configured), with the goal of making it work much like our own scaffolding.

The problem

After modifying the scaffolding, we found a problem when testing it locally, using this code:

<template>
  <view class="content">
    <image class="logo" src=".. /.. /static/logo.png"></image>.</view>
</template>
Copy the code

After publicPath was configured, we uploaded the dist/build/ H5 results to our static server and found that the images could not be displayed. Check the code on the console and the IMG tag looks like this:

That is, the configured publicPath does not take effect.

Look for a solution

I began to wonder if I had missed some configuration. I looked for the usage method of image component in UNI-app, and there seemed to be no special instructions in it. Well, Google around and find some solutions online that require the following code:

<template>
  <view class="content">
    <image class="logo" :src="require('.. /.. /static/logo.png')"></image>.</view>
</template>
Copy the code

Try it and, yes, the picture comes back:

But this solution is still slightly rough, change scaffolding can not stop here. So the search for a solution continued

Train of thought

As a quick thought, the problem is that the.vue file does not automatically require the SRC attribute value of the image component at compile time and therefore cannot be handled properly by file-loader or url-loader. It seems that the key to the problem is to compile the.vue file here.

So go to see the official documentation of VUe-Loader, vue-Loader documentation is obviously a special section to introduce this function:

There is a property transformAssetUrls that can be used to handle this problem. The default value for this property is:

{
  video: ['src'.'poster'].source: 'src'.img: 'src'.image: ['xlink:href'.'href'].use: ['xlink:href'.'href']}Copy the code

That is, vue-loader by default processes SRC attributes such as img tags and replaces them with require(…) if SRC is a relative path. The call. See how uni-App scaffolding works with vue-Loader. Node_modules = @dcloudio/vue-cli-plugin-uni/lib/h5/index

// @dcloudio/vue-cli-plugin-uni/lib/h5/index.js#L113
webpackConfig.module
  .rule('vue')
  .test([/\.vue$/, /\.nvue$/])
  .use('vue-loader')
  .tap(options= > Object.assign(options, {
      compiler: getPlatformCompiler(),
      compilerOptions: require('./compiler-options'),
      cacheDirectory: false.cacheIdentifier: false
  }))
  .end()
    // ...
Copy the code

As found in.tap, uni-app scaffolding does not have the property transformAssetUrls configured, probably just the default configuration. Node_modules /@dcloudio/vue-cli-plugin-uni/lib/h5/index

// @dcloudio/vue-cli-plugin-uni/lib/h5/index.js#L113
webpackConfig.module
  .rule('vue')
  .test([/\.vue$/, /\.nvue$/])
  .use('vue-loader')
  .tap(options= > Object.assign(options, {
      compiler: getPlatformCompiler(),
      compilerOptions: require('./compiler-options'),
      cacheDirectory: false.cacheIdentifier: false.// Add here
      transformAssetUrls: {
        'image': ['xlink:href'.'href'.'src']
      }
  }))
  .end()
    // ...
Copy the code

A closer look at the build source shows that the image component will actually be processed as a component created in the form of a rendering function:

// ...
return createElement("v-uni-image", {
  staticClass: "logo".attrs: {
    src: '.. /.. /static/logo.png'}})// ...
Copy the code

As you can see, the component name is changed to V-uni-image, which is why the above configuration does not take effect.

To solve

Continue with this:

// @dcloudio/vue-cli-plugin-uni/lib/h5/index.js#L113
webpackConfig.module
  .rule('vue')
  .test([/\.vue$/, /\.nvue$/])
  .use('vue-loader')
  .tap(options= > Object.assign(options, {
      compiler: getPlatformCompiler(),
      compilerOptions: require('./compiler-options'),
      cacheDirectory: false.cacheIdentifier: false.// Add here
      transformAssetUrls: { 
        'v-uni-image': 'src'
      }
  }))
  .end()
  // ...
Copy the code

The rebuild does work, and the generated code looks something like this:

return createElement("v-uni-image", { 
  staticClass: "logo".attrs: { 
    src: require(/ /...).}})Copy the code

abouttransformAssetUrls

How to handle the property transformAssetUrls:

/ / https://github.com/vuejs/vue-loader/blob/master/lib/loaders/templateLoader.js#L32

  const { compileTemplate } = require('@vue/component-compiler-utils')
// ...

// for vue-component-compiler
// All options that will eventually be passed to the template compiler
  const finalOptions = {
    source,
    filename: this.resourcePath,
    compiler,
    compilerOptions,
    // allow customizing behavior of vue-template-es2015-compiler
    transpileOptions: options.transpileOptions,
    transformAssetUrls: options.transformAssetUrls || true.// Watch this!!
    isProduction,
    isFunctional,
    optimizeSSR: isServer && options.optimizeSSR ! = =false.prettify: options.prettify
  }

  const compiled = compileTemplate(finalOptions)  // Pass all options to the compileTemplate template compiler
  // ...
Copy the code

To continue:

  / / https://github.com/vuejs/component-compiler-utils/blob/master/lib/compileTemplate.ts#L113
  import assetUrlsModule from './templateCompilerModules/assetUrl'
  // ...
  let finalCompilerOptions = finalOptions
  
  if (transformAssetUrls) { // If a custom transformAssetUrls is passed in, merge it with the default
    const builtInModules = [
      transformAssetUrls === true
        ? assetUrlsModule()
        : assetUrlsModule(transformAssetUrls),
      srcsetModule()    
    ]
    finalCompilerOptions = Object.assign({}, compilerOptions, {
      modules: [...builtInModules, ...(compilerOptions.modules || [])] // Does it look familiar})}const { render, staticRenderFns, tips, errors } = compile(
    source,
    finalCompilerOptions
  )
Copy the code

Continue to chase:

/ / https://github.com/vuejs/component-compiler-utils/blob/master/lib/templateCompilerModules/assetUrl.ts

// Familiar faces
const defaultOptions: AssetURLOptions = {
  video: ['src'.'poster'].source: 'src'.img: 'src'.image: ['xlink:href'.'href'].use: ['xlink:href'.'href']}// These tags are processed by returning a postTransformNode
export default(userOptions? : AssetURLOptions) => {const options = userOptions
    ? Object.assign({}, defaultOptions, userOptions)
    : defaultOptions

  return {
    postTransformNode: (node: ASTNode) = > {
      transform(node, options)
    }
  }
}

function transform(node: ASTNode, options: AssetURLOptions) {
  for (const tag in options) {
    // ...
    attributes.forEach(item= > node.attrs.some(attr= > rewrite(attr, item)))
  }
}

function rewrite(attr: Attr, name: string) {
  if (attr.name === name) {
    / /... Something like that
    attr.value = `require(${attr.value}) `
  }
  return false
}
Copy the code

As you can see, the options in transformAssetUrls directly generate a hook called postTransformNode, which processes each element of the Template template to create a separate syntax tree node ASTNode. And to be executed after ASTNode has been further processed. The postTransformNode counterpart is the preTransformNode hook, which, as the name suggests, executes before the generated ASTNode is ready to be processed further. These two types of hooks can be put into a {modules: [hook]} object and passed into the final template compiler.

Similar code can be seen in the compiler option @dcloudio/vue-cli-plugin-uni/lib/h5/compiler-options.js’ of uni-app’s custom compiler:

// @dcloudio/vue-cli-plugin-uni/lib/h5/compiler-options.js#L113
module.exports = {
  isUnaryTag,
  preserveWhitespace: false.modules: [require('.. /format-text'), {
    preTransformNode (el, {
      warn
    }) {
        // ...
    },
    postTransformNode (el, {
      warn,
      filterModules
    }) {
        // ...
    }
Copy the code

The V-UNI – prefix for uni-App’s own components is added in this way. Therefore, the above problems can also be directly dealt with in a more violent way here:

// @dcloudio/vue-cli-plugin-uni/lib/h5/compiler-options.js#L113

// Import vue's own processing methods
const assetUrlsModule = require('@vue/component-compiler-utils/dist/templateCompilerModules/assetUrl').default
// Generate hooks for label handling
const builtInModules = assetUrlsModule({ 'v-uni-image': 'src' })

module.exports = {
  isUnaryTag,
  preserveWhitespace: false.modules: [require('.. /format-text'), {
    ...builtInModules,
  }, {
    preTransformNode (el, {
      warn
    }) {
        // ...
    },
    postTransformNode (el, {
      warn,
      filterModules
    }) {
        // ...
    }
Copy the code

For more information on modules, see: Arrays of compiler modules

Solutions directly in the project

If you want to use the official scaffolding directly to solve this problem, you can add the following code to vue.config.js:

module.exports = {
  chainWebpack(webpackConfig) {
    webpackConfig.module
    .rule('vue')
    .test([/\.vue$/, /\.nvue$/])
    .use('vue-loader')
    .tap(options= > Object.assign(options, {
      transformAssetUrls: {
        'v-uni-image': 'src'
      }
    }))
    .end()
  },
  
  configureWebpack (config) {
    / /... blablabla}},Copy the code

Well, not the way to do it.

If you have a better solution, please leave a comment in the comments section.

Pay attention to our