uni-app脚手架踩坑记

10,510 阅读4分钟

背景

最近在做跨平台框架的试点,选择了uni-app,并打算先在h5上开始试点。

由于uni-app提供的基于vue-cli的脚手架与我们内部的脚手架稍有些不同,直接使用稍微有点学习成本,所以fork了一下,稍作修改,做了一个内部版本的脚手架(主要就是将publicPath从manifest.json中拿出来,实现动态配置),目的就是让其用起来和我们自己的脚手架差不多。

问题

修改完脚手架,在本地测试时,发现了一个问题,当时使用这样一段代码:

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

在配置好publicPath后,将dist/build/h5的结果上传到我们的静态服务器上后,发现图片显示不出来。在控制台检查一下代码,发现此处的img标签是这样的:

错误的src地址

也就是说配置的publicPath并未生效。

寻找解决办法

开始怀疑是不是自己漏掉了什么配置,在uni-app找了关于image组件的使用方法,里面看起来并没有什么特殊的说明。好吧,Google一下,发现网上有一些解决方案,需要把代码改成下面这样:

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

试了一下,确实,图片回来了:

正常

但是这样解决还是略有些简陋,改脚手架不能止步于此。于是继续寻找解决办法

思路

迅速的捋一下思路,出现这个问题的原因是,.vue文件在编译阶段并没有为这个image组件的src属性的值自动加上require,从而无法被file-loader或url-loader来正确的处理。看来问题的关键就出在编译.vue文件这里。

所以去看了一下vue-loader的官方文档,vue-loader的文档很明显的有专门的一节来介绍这个功能:

大概意思就是说有一个transformAssetUrls的属性可以用来处理这种问题。这个属性的默认值是:

{
  video: ['src', 'poster'],
  source: 'src',
  img: 'src',
  image: ['xlink:href', 'href'],
  use: ['xlink:href', 'href']
}

也就是说,vue-loader默认会处理比如img标签的src属性,如果src的值为相对路径,就会将其替换为require(...)调用。去看一下uni-app的脚手架是怎么配vue-loader便知。直接去node_modules下看源码,找到配置vue-loader的地方在@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()
    // ...

.tap中发现,uni-app的脚手架并没有配置transformAssetUrls这个属性,可能只是走了默认的配置。直接在本地修改一下试试吧,直接修改node_modules/@dcloudio/vue-cli-plugin-uni/lib/h5/index,试着为image增加一个src属性:

// @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,
      // 新增的这里
      transformAssetUrls: {
        'image': ['xlink:href', 'href', 'src']
      }
  }))
  .end()
    // ...

发现并没有生效,仔细察看build后的源码,发现实际上image组件最终会被处理成以渲染函数的形式来创建的组件:

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

可以看到,组件名会被修改为v-uni-image,所以上面的配置才没有生效。

解决

继续改成这样:

// @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,
      // 新增的这里
      transformAssetUrls: { 
        'v-uni-image': 'src'
      }
  }))
  .end()
  // ...

重新build,确实生效了,看一下生成的代码大概是这样的:

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

关于transformAssetUrls

趁热打铁,又看一下vue-loader的源码,看看到底是如何处理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
// 最终将传递给模板编译器的所有选项
  const finalOptions = {
    source,
    filename: this.resourcePath,
    compiler,
    compilerOptions,
    // allow customizing behavior of vue-template-es2015-compiler
    transpileOptions: options.transpileOptions,
    transformAssetUrls: options.transformAssetUrls || true,  // 注意这里!!!
    isProduction,
    isFunctional,
    optimizeSSR: isServer && options.optimizeSSR !== false,
    prettify: options.prettify
  }

  const compiled = compileTemplate(finalOptions)  // 将所有的选项传递给compileTemplate模板编译器
  // ...

追下去:

  // 详见 https://github.com/vuejs/component-compiler-utils/blob/master/lib/compileTemplate.ts#L113
  import assetUrlsModule from './templateCompilerModules/assetUrl'
  // ...
  let finalCompilerOptions = finalOptions
  
  if (transformAssetUrls) { // 如果传入了自定义的transformAssetUrls,将其与默认的合并
    const builtInModules = [
      transformAssetUrls === true
        ? assetUrlsModule()
        : assetUrlsModule(transformAssetUrls),
      srcsetModule()    
    ]
    finalCompilerOptions = Object.assign({}, compilerOptions, {
      modules: [...builtInModules, ...(compilerOptions.modules || [])] // 是不是很眼熟
    })
  }

  const { render, staticRenderFns, tips, errors } = compile(
    source,
    finalCompilerOptions
  )

继续追:

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

// 熟悉的面孔
const defaultOptions: AssetURLOptions = {
  video: ['src', 'poster'],
  source: 'src',
  img: 'src',
  image: ['xlink:href', 'href'],
  use: ['xlink:href', 'href']
}

// 原来是通过返回一个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) {
    // ... 大概是这个意思
    attr.value = `require(${attr.value})`
  }
  return false
}

可以看到,原来transformAssetUrls里面的选项会直接生成一个叫的postTransformNode的钩子,他的作用就是用来处理模板template里面的每一个元素element,生成单独的语法树节点ASTNode,并在ASTNode被进一步处理之后要执行的。与postTransformNode对应的还有preTransformNode钩子,顾名思义,就是在生成的ASTNode即将被进一步处理之前要执行的钩子。这两类钩子可以放到一个 { modules: [ 钩子 ] } 的对象中,一并传入给最终的模板编译器。

而在uni-app的自定义编译器的编译器选项@dcloudio/vue-cli-plugin-uni/lib/h5/compiler-options.js`中,也可以看到类似的代码:

// @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
    }) {
        // ...
    }

uni-app自有组件的v-uni-前缀就是通过这种方式添加的。所以,上面的遇到问题也可以直接在这里比较暴力的方式处理:

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

// 将vue自带的处理方法引进来
const assetUrlsModule = require('@vue/component-compiler-utils/dist/templateCompilerModules/assetUrl').default
// 生成标签处理的钩子
const builtInModules = assetUrlsModule({ 'v-uni-image': 'src' })

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

更多关于modules的信息可参考: 编译器模块的数组

直接在项目中的解决办法

如果想直接使用官方的脚手架来解决这个问题,就可以在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  
  },
}

嗯,不是办法的办法。

你如果有更好的解决方法欢迎评论区留言,讨论。

关注我们