如何獲取 vue 單檔案自身原始碼路徑

CNine發表於2019-05-04

檢視原文

這個問題要從一個想法說起。

D2Admin 是一個開源的,前端中後臺整合方案,原先是基於 vue-cli2,大概是向 vue-cli3 過渡時, 作者老李,想在頁面右下角加個 Toggle 點選,跳到當前頁面原始碼對應的 github 頁面。

確實很實用的功能,D2Admin 的 Demo 頁面太多了,想看某個頁面的原始碼,對於不熟悉專案目錄結構的新手很不友好。

這些頁面統一為 .vue 元件,那麼轉換一下:如何獲取 vue 單檔案自身原始碼路徑?

目前經歷了三個方案,最終目標是把自身路徑賦值到 this.$options.__source 上。目前方案 3 是最新的。

方案 1 :node + __filename

直接使用 node 中的 __filename 變數:

<template>
  <h1>{{ $options.__source }}</h1>
</template>

<script>
export default {
  mounted() {
    this.$options.__source = __filename
  }
}
</script>
複製程式碼

因為 webpack 編譯時,會把原始碼檔案在內部轉為 node 模組,.vue 檔案中的 script 內容也被轉換了, 其中的 __filename 在編譯時被執行,直接得到當前檔案自身路徑。

使用這個變數還需要在 webpack 配置中啟用 node.__filename

/* vue.config.js */
module.exports = {
  // ...
  chainWebpack: config => {
    // ...
    config.node
      .set('__dirname', true) // 同理
      .set('__filename', true)
  }
};
複製程式碼

缺點

  • 要在每個元件裡手動賦值,還不能用 mixin
  • __filename 得到的路徑在部分 .vue 檔案下並不準確,路徑可能還會帶附帶 querystring

一開始,堅強的老李用這個方式,給上百個元件手動掛上了路徑,但總比手動寫死每個路徑要好

方案 2 :vue-loader + exposeFilename

在 loader 層面,其實 vue-loader 提供了一個 exposeFilename 選項,只要啟用, 就會給每個 .vue 元件帶上 this.$options.__file,上面有準確的路徑。

這樣只需要改 loader 配置:

/* vue.config.js */
module.exports = {
  // ...
  chainWebpack: config => {
    // ...
    config.module
      .rule('vue')
      .use('vue-loader')
        .loader('vue-loader')
        .tap(options => {
          options.exposeFilename = true
          return options
        })
  }
};
複製程式碼

開發環境預設是開啟 exposeFilename 的。

此時元件內應該直接取 this.$options.__file,內容大致為 src/foo/bar.vue

缺點

  • 為了安全vue-loader 在生產環境將 __file 賦值為檔名,非路徑名,詳見文件

後來得知這個方法,老李就第一時間改程式碼,刪了方案 1 中的所有附加程式碼,結果發現生產環境結果不一致,翻車了orz

方案 3 :loader + Custom Block

既然方案 2 不讓在生產環境用,那就自己寫 loader 去加上這個原始碼路徑,這裡採用了 Custom Block

這個方法是慢慢除錯自定義的 loader 摸索出來的,先在 vue-loader 之前加自定義的 loader A, 去注入 Custom Block 程式碼,再在全域性加入一個針對該 Custom Block 的 loader B。

這裡將方案封裝,在 chainWebpack 中呼叫即可:

/* vue.config.js */
const VueFilenameInjector = require('./path/to/vue-filename-injector')

module.exports = {
  chainWebpack: config => {
    VueFilenameInjector(config, {
      propName: '__source' // default
    })
  }
}
複製程式碼

原始碼參考:@d2-projects/d2-advance/tools/vue-filename-injector

.
└── vue-filename-injector
    ├── README.md
    ├── index.js
    └── src
        ├── index.js
        └── lib
            ├── config.js
            ├── injector.js
            └── loader.js
複製程式碼

vue-filename-injector/index.js

const { blockName } = require('./lib/config.js')

// for chainWebpack
module.exports = function(config, options) {
  // 注入
  config.module
    .rule('vue')
    .use('vue-filename-injector')
    .loader(require.resolve('./lib/injector.js'))
    .options(options)
    .after('vue-loader') // 不知為何 .before() 不是預期的結果,這裡就繞了一下
    .end()
  // 解析
  config.module
    .rule('')
    .resourceQuery(new RegExp(`blockType=${blockName}`))
    .use('vue-filename-injector-loader')
    .loader(require.resolve('./lib/loader.js'))
    .end()
}
複製程式碼

vue-filename-injector/lib/config.js

const defaultPropName = '__source'
const blockName = 'vue-filename-injector'

module.exports = {
  defaultPropName,
  blockName
}
複製程式碼

vue-filename-injector/lib/injector.js,原始碼部分來自 vue-loader

const path = require('path')
const loaderUtils = require('loader-utils')

const { blockName, defaultPropName } = require('./config.js')

module.exports = function (content /*, map, meta */) {
  const loaderContext = this

  const {
    rootContext,
    resourcePath
  } = loaderContext

  const context = rootContext || process.cwd()
  const options = loaderUtils.getOptions(loaderContext) || {}
  const rawShortFilePath = path
    .relative(context, resourcePath)
    .replace(/^(\.\.[\/\\])+/, '')

  const propName = options.propName || defaultPropName

  content += `
<${blockName}>
export default function (Component) {
  Component.options.${propName} = ${JSON.stringify(rawShortFilePath.replace(/\\/g, '/'))}
}
</${blockName}>
`
  return content
}
複製程式碼

vue-filename-injector/lib/loader.js

module.exports = function(source, map) {
  this.callback(null, source, map)
}
複製程式碼

相關倉庫

可進入預覽頁面檢視效果,在右下角有 Toggle

github.com/d2-projects… (可能還在翻車中)

github.com/d2-projects…

總結

目前看來,用自定義 loader 去注入程式碼是最便捷的方案,不用在每個元件中手寫重複的程式碼。 由於 vue-cli3 採用 chainWebpack,加上個人對 webpack 的理解更是不深,暫時採用方案 3。

相關文章