vue-loader 15で、vue テンプレート内の任意の属性(data-testなど)を除外する
概要
<template>
<div class="hoge" data-test="Hoge">
</template>
上記のdata-test
のような、テストでは使いたいけどプロダクションコードには反映させたくないような属性を、vue-loader
の設定でプロダクションビルド時には除外し、以下のようにします。
<template>
<div class="hoge">
</template>
内容はVueコンポーネントのビルド時に不要な属性をtemplateから取り除く にかかれていることを、vue-loader
15系に合わせて書き直しつつ深堀りしただけになります。
バージョン情報
version | |
---|---|
node | 12.16.3 |
vue | 2.6.11 |
vue-loader | 15.9.1 |
webpack | 4.43.0 |
対象コンポーネント
以下のような、要素それぞれに入れ子があってdata-test
属性以外にもちょいちょい属性が付与されているようなサンプルコードをビルド対象として、data-test
属性のみ、テスト専用のメタデータなので除外したいと思います。
<template>
<div class="root">
<p data-test="Hoge">hoge</p>
<div data-test="Fuga">
<h1 id="foo" data-test="Foo">foo</h1>
<h2 id="bar" data-test="Bar">bar</h2>
<p :data-test="value">Hello</p>
</div>
</div>
</template>
<script>
export default {
data() {
return {
value: "Hello"
}
}
}
</script>
vue-loaderの構成について
本記事では便宜上、webpack
で vue-loader
を使うための設定を、以下のようなモジュールに切り出していることを前提とします。
module.exports = {
test: /\.vue$/,
use: [
{
loader: 'vue-loader',
options: {}
}
]
}
テンプレートのコンパイルに干渉する
vue-loader
は内部的に、 vue-template-compiler
を使って、Vueファイルの <template>
を、 createElement
を使ったピュアな Javascript に変換します。
変換の際に一度ASTを経由しますが、vue-template-compiler
では、その変換過程に任意の関数をフックすることができます。
vue-loader
の設定からvue-template-compiler
の設定を指定する場合、 compilerOptions を指定します。
干渉できるタイミングはいくつかありますが、今回は <template>
がASTに変換された直後のタイミングを使いたいので、 preTransformNode
を使用します。
module.exports = {
test: /\.vue$/,
use: [
{
loader: 'vue-loader',
options: {
compilerOptions: {
modules: [
{
preTransformNode(astEl) {
console.log(astEl)
}
}
]
}
}
}
]
}
この関数は、 <template>
をコンパイルする際の要素1個につき、その要素を表すASTを引数にして呼ばれます。
<template>
<div class="root">
<p data-test="Hoge">hoge</p>
<div data-test="Fuga">
<h1 id="foo" data-test="Foo">foo</h1>
<h2 id="bar" data-test="Bar">bar</h2>
<p :data-test="value">Hello</p>
</div>
</div>
</template>
の場合、親子合わせて全部で以下の6要素あるので、6回関数が呼ばれるわけですね。
- div.root
- p[data-test=Hoge]
- div[data-test=Fuga]
- h1#foo
- h2#bar
- p[data-test=value]
これでWebpackビルドをすると以下のように、対象コンポーネントのASTオブジェクト(6種)がドバっと出てきます。
{
type: 1,
tag: 'div',
attrsList: [ { name: 'class', value: 'root', start: 5, end: 17 } ],
attrsMap: { class: 'root' },
rawAttrsMap: { class: { name: 'class', value: 'root', start: 5, end: 17 } },
parent: undefined,
children: [],
start: 0,
end: 18
}
{
type: 1,
tag: 'p',
attrsList: [ { name: 'data-test', value: 'Hoge', start: 24, end: 40 } ],
attrsMap: { 'data-test': 'Hoge' },
rawAttrsMap: {
'data-test': { name: 'data-test', value: 'Hoge', start: 24, end: 40 }
},
parent: {
type: 1,
tag: 'div',
attrsList: [ [Object] ],
attrsMap: { class: 'root' },
rawAttrsMap: { class: [Object] },
parent: undefined,
children: [],
start: 0,
end: 18
},
children: [],
start: 21,
end: 41
}
{
type: 1,
tag: 'div',
attrsList: [ { name: 'data-test', value: 'Fuga', start: 57, end: 73 } ],
attrsMap: { 'data-test': 'Fuga' },
rawAttrsMap: {
'data-test': { name: 'data-test', value: 'Fuga', start: 57, end: 73 }
},
parent: {
type: 1,
tag: 'div',
attrsList: [ [Object] ],
attrsMap: { class: 'root' },
rawAttrsMap: { class: [Object] },
parent: undefined,
children: [ [Object], [Object] ],
start: 0,
end: 18
},
children: [],
start: 52,
end: 74
}
{
type: 1,
tag: 'h1',
attrsList: [
{ name: 'id', value: 'foo', start: 83, end: 91 },
{ name: 'data-test', value: 'Foo', start: 92, end: 107 }
],
attrsMap: { id: 'foo', 'data-test': 'Foo' },
rawAttrsMap: {
id: { name: 'id', value: 'foo', start: 83, end: 91 },
'data-test': { name: 'data-test', value: 'Foo', start: 92, end: 107 }
},
parent: {
type: 1,
tag: 'div',
attrsList: [ [Object] ],
attrsMap: { 'data-test': 'Fuga' },
rawAttrsMap: { 'data-test': [Object] },
parent: {
type: 1,
tag: 'div',
attrsList: [Array],
attrsMap: [Object],
rawAttrsMap: [Object],
parent: undefined,
children: [Array],
start: 0,
end: 18
},
children: [],
start: 52,
end: 74
},
children: [],
start: 79,
end: 108
}
{
type: 1,
tag: 'h2',
attrsList: [
{ name: 'id', value: 'bar', start: 125, end: 133 },
{ name: 'data-test', value: 'Bar', start: 134, end: 149 }
],
attrsMap: { id: 'bar', 'data-test': 'Bar' },
rawAttrsMap: {
id: { name: 'id', value: 'bar', start: 125, end: 133 },
'data-test': { name: 'data-test', value: 'Bar', start: 134, end: 149 }
},
parent: {
type: 1,
tag: 'div',
attrsList: [ [Object] ],
attrsMap: { 'data-test': 'Fuga' },
rawAttrsMap: { 'data-test': [Object] },
parent: {
type: 1,
tag: 'div',
attrsList: [Array],
attrsMap: [Object],
rawAttrsMap: [Object],
parent: undefined,
children: [Array],
start: 0,
end: 18
},
children: [ [Object], [Object] ],
start: 52,
end: 74
},
children: [],
start: 121,
end: 150
}
{
type: 1,
tag: 'p',
attrsList: [ { name: ':data-test', value: 'value', start: 166, end: 184 } ],
attrsMap: { ':data-test': 'value' },
rawAttrsMap: {
':data-test': { name: ':data-test', value: 'value', start: 166, end: 184 }
},
parent: {
type: 1,
tag: 'div',
attrsList: [ [Object] ],
attrsMap: { 'data-test': 'Fuga' },
rawAttrsMap: { 'data-test': [Object] },
parent: {
type: 1,
tag: 'div',
attrsList: [Array],
attrsMap: [Object],
rawAttrsMap: [Object],
parent: undefined,
children: [Array],
start: 0,
end: 18
},
children: [ [Object], [Object], [Object], [Object] ],
start: 52,
end: 74
},
children: [],
start: 163,
end: 185
}
今回は各要素の属性(attr)情報だけを知りたいので、 astEl
のうち、必要なプロパティだけ抜き出してもう一度見てみましょう。
preTransformNode(astEl) {
const { attrsList, attrsMap } = astEl
console.log({ attrsList, attrsMap })
console.log('------')
}
{
attrsList: [ { name: 'class', value: 'root', start: 5, end: 17 } ],
attrsMap: { class: 'root' }
}
------
{
attrsList: [ { name: 'data-test', value: 'Hoge', start: 24, end: 40 } ],
attrsMap: { 'data-test': 'Hoge' }
}
------
{
attrsList: [ { name: 'data-test', value: 'Fuga', start: 57, end: 73 } ],
attrsMap: { 'data-test': 'Fuga' }
}
------
{
attrsList: [
{ name: 'id', value: 'foo', start: 83, end: 91 },
{ name: 'data-test', value: 'Foo', start: 92, end: 107 }
],
attrsMap: { id: 'foo', 'data-test': 'Foo' }
}
------
{
attrsList: [
{ name: 'id', value: 'bar', start: 125, end: 133 },
{ name: 'data-test', value: 'Bar', start: 134, end: 149 }
],
attrsMap: { id: 'bar', 'data-test': 'Bar' }
}
------
{
attrsList: [ { name: ':data-test', value: 'value', start: 166, end: 184 } ],
attrsMap: { ':data-test': 'value' }
}
------
わかりやすくなってきました。
attrsList
は対象要素に設定されている属性の一覧で、 attrsMap
は属性名に対する設定値のマッピングですね。
ビルド結果を確認する①
webpack
でのビルドが完了すると、 output
で指定したパスにアセットファイルが生成されます。
それを覗いてみると、先程出力したASTをベースに、ピュアなJavascriptでVueオブジェクトを生成しているコードが見つかります。
var render = function () {
var _vm = this
var _h = _vm.$createElement
var _c = _vm._self._c || _h
return _c("div", { staticClass: "root" }, [
_c("p", { attrs: { "data-test": "Hoge" } }, [_vm._v("hoge")]),
_vm._v(" "),
_c("div", { attrs: { "data-test": "Fuga" } }, [
_c("h1", { attrs: { id: "foo", "data-test": "Foo" } }, [_vm._v("foo")]),
_vm._v(" "),
_c("h2", { attrs: { id: "bar", "data-test": "Bar" } }, [_vm._v("bar")]),
_vm._v(" "),
_c("p", { attrs: { "data-test": _vm.value } }, [_vm._v("Hello")]),
]),
])
}
この状態だと、プロダクションコードにも data-test
属性が入ってしまうので、それをASTの時点で削除しましょう。
data-test
属性を削除する
preTransformNode
のような関数は、ASTオブジェクトを受け取ってASTオブジェクトを戻します。その際、戻したほうのASTオブジェクトを使用してコンパイルが継続されるので、この関数内で、 data-test
属性を削除したASTオブジェクトを戻してあげればよいわけです。
と言っても非常に簡単で、先程の attrsList
attrsMap
から該当のデータだけ削除してあげればOKです。
preTransformNode(astEl) {
const { attrsList, attrsMap } = astEl
if (attrsMap['data-test']) {
delete attrsMap['data-test']
const index = attrsList.findIndex(x => x.name === 'data-test')
attrsList.splice(index, 1)
}
return astEl
}
これによって、もし対象要素が data-test
属性を持っていた場合、attrsMap
及び attrsList
から削除し、編集後のASTオブジェクトを戻すようになります。
これでビルドしてみましょう。
{
attrsList: [ { name: 'class', value: 'root', start: 5, end: 17 } ],
attrsMap: { class: 'root' }
}
-----
{ attrsList: [], attrsMap: {} }
-----
{ attrsList: [], attrsMap: {} }
-----
{
attrsList: [ { name: 'id', value: 'foo', start: 83, end: 91 } ],
attrsMap: { id: 'foo' }
}
-----
{
attrsList: [ { name: 'id', value: 'bar', start: 125, end: 133 } ],
attrsMap: { id: 'bar' }
}
-----
{
attrsList: [ { name: ':data-test', value: 'value', start: 166, end: 184 } ],
attrsMap: { ':data-test': 'value' }
}
-----
惜しい! data-test 属性は見事に消えていますが、動的バインディングの :data-test 属性が残ってしまいました。
これらはVueコンポーネント描画後のDOM上では同じ属性になりますが、この段階のASTでは全く異なる属性として扱われます。
やや冗長になっちゃうので、汎用的なコードを抜き出しましょう。
function removeAttr(astEl, name) {
const { attrsList, attrsMap } = astEl
if (attrsMap[name]) {
delete attrsMap[name]
const index = attrsList.findIndex(x => x.name === name)
attrsList.splice(index, 1)
}
return astEl
}
preTransformNode
からは、二つの属性を指定して関数を呼び出すだけにしてあげます。
preTransformNode(astEl) {
const { attrsList, attrsMap } = astEl
removeAttr(astEl, 'data-test')
removeAttr(astEl, ':data-test')
console.log({ attrsList, attrsMap })
console.log('-----')
return astEl
}
{
attrsList: [ { name: 'class', value: 'root', start: 5, end: 17 } ],
attrsMap: { class: 'root' }
}
-----
{ attrsList: [], attrsMap: {} }
-----
{ attrsList: [], attrsMap: {} }
-----
{
attrsList: [ { name: 'id', value: 'foo', start: 83, end: 91 } ],
attrsMap: { id: 'foo' }
}
-----
{
attrsList: [ { name: 'id', value: 'bar', start: 125, end: 133 } ],
attrsMap: { id: 'bar' }
}
-----
{ attrsList: [], attrsMap: {} }
-----
data-test
:data-test
ともに削除できました。完璧そうですね。
しかもこの構成なら test
以外に消しておきたい属性が出てきても汎用的に対応できます。
ビルド結果を確認する①
ではこの状態でWebpackのビルド結果をもう一度見てみましょう。
var staticRenderFns = [
function () {
var _vm = this
var _h = _vm.$createElement
var _c = _vm._self._c || _h
return _c("div", { staticClass: "root" }, [
_c("p", [_vm._v("hoge")]),
_vm._v(" "),
_c("div", [
_c("h1", { attrs: { id: "foo" } }, [_vm._v("foo")]),
_vm._v(" "),
_c("h2", { attrs: { id: "bar" } }, [_vm._v("bar")]),
_vm._v(" "),
_c("p", [_vm._v("Hello")]),
]),
])
},
]
しっかり data-test
:data-test
属性だけが除外され、 id
class
といった属性は残っていることが確認できました!
プロダクション環境でのみ除外する
ここまでで data-test
:data-test
を除外できましたが、普通にテストを実行する際は残っていてほしいので、本番用にビルドするときのみ除外するようにしたいですね。
process.env.NODE_ENV
に環境情報が入ってるとすると、以下のように整理すればプロダクションのみで除外できそうです。
/**
* ASTオブジェクトから任意の属性を除外して戻す
* @param {Object} astEl vue-loaderが単一要素をAST化したオブジェクト
* @param {string} name ASTから除外する属性名
*/
function removeAttr(astEl, name) {
const { attrsList, attrsMap } = astEl
if (attrsMap[name]) {
delete attrsMap[name]
const index = attrsList.findIndex(x => x.name === name)
attrsList.splice(index, 1)
}
return astEl
}
/**
* ASTオブジェクトからE2Eテスト用のカスタム属性を除外する
* @param {Object} astEl vue-loaderが単一要素をAST化したオブジェクト
*/
function removeTestAttr(astEl) {
removeAttr(astEl, 'data-test')
removeAttr(astEl, ':data-test')
return astEl
}
// プロダクション環境の場合のみ、data-test属性は抹消する
const compileModules = process.env.NODE_ENV === 'production' ? [{ preTransformNode: removeTestAttr }] : []
module.exports = {
test: /\.vue$/,
use: [
{
loader: 'vue-loader',
options: {
extractCSS: true,
compilerOptions: {
modules: compileModules
}
}
}
]
}
Discussion