uni-app腳手架踩坑記

背景

最近在作跨平臺框架的試點,選擇了uni-app,並打算先在h5上開始試點。html

因爲uni-app提供的基於vue-cli的腳手架與咱們內部的腳手架稍有些不一樣,直接使用稍微有點學習成本,因此fork了一下,稍做修改,作了一個內部版本的腳手架(主要就是將publicPath從manifest.json中拿出來,實現動態配置),目的就是讓其用起來和咱們本身的腳手架差很少。vue

問題

修改完腳手架,在本地測試時,發現了一個問題,當時使用這樣一段代碼:node

<template>
  <view class="content">
    <image class="logo" src="../../static/logo.png"></image>
    ...
  </view>
</template>
複製代碼

在配置好publicPath後,將dist/build/h5的結果上傳到咱們的靜態服務器上後,發現圖片顯示不出來。在控制檯檢查一下代碼,發現此處的img標籤是這樣的:webpack

錯誤的src地址

也就是說配置的publicPath並未生效。git

尋找解決辦法

開始懷疑是否是本身漏掉了什麼配置,在uni-app找了關於image組件的使用方法,裏面看起來並無什麼特殊的說明。好吧,Google一下,發現網上有一些解決方案,須要把代碼改爲下面這樣:github

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

試了一下,確實,圖片回來了:web

正常

可是這樣解決仍是略有些簡陋,改腳手架不能止步於此。因而繼續尋找解決辦法vue-cli

思路

迅速的捋一下思路,出現這個問題的緣由是,.vue文件在編譯階段並無爲這個image組件的src屬性的值自動加上require,從而沒法被file-loader或url-loader來正確的處理。看來問題的關鍵就出在編譯.vue文件這裏。json

因此去看了一下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 
  },
}

複製代碼

嗯,不是辦法的辦法。

你若是有更好的解決方法歡迎評論區留言,討論。

關注咱們

相關文章
相關標籤/搜索