概要
<template><divclass="hoge"data-test="Hoge"></template>
上記のdata-test
のような、テストでは使いたいけどプロダクションコードには反映させたくないような属性を、vue-loader
の設定でプロダクションビルド時には除外し、以下のようにします。
<template><divclass="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
属性だけはE2Eテスト専用の情報なので除外したいと思います。
<template><divclass="root"><pdata-test="Hoge">hoge</p><divdata-test="Fuga"><h1id="foo"data-test="Foo">foo</h1><h2id="bar"data-test="Bar">bar</h2><p:data-test="value">Hello</p></div></div></template><script>exportdefault{data(){return{value:"Hello"}}}</script>
vue-loaderの構成について
本記事では便宜上、webpack
で vue-loader
を使うための設定を、vue.js
に以下のように分離していると仮定します。
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を指定します。
干渉できるタイミングはいくつかありますが、今回はテンプレートをASTになったばかりのタイミングを使いたいので、 preTransformNode
を使用します。
module.exports={test:/\.vue$/,use:[{loader:'vue-loader',options:{compilerOptions:{modules:[{preTransformNode(astEl){console.log(astEl)}}]}}}]}
この関数は、テンプレートをコンパイルする際のElement1個に付き、そのElementのASTを引数にして一度呼ばれます。
<template><divclass="root"><pdata-test="Hoge">hoge</p><divdata-test="Fuga"><h1id="foo"data-test="Foo">foo</h1><h2id="bar"data-test="Bar">bar</h2><p:data-test="value">Hello</p></div></div></template>
の場合、親子合わせて全部で6要素あるので、6回関数が呼ばれるわけですね。
これで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}
今回は各Elementの属性情報だけが知りたいので、astEl
のうち、必要なプロパティだけ抜き出してもう一度見てみましょう。
preTransformNode(astEl){const{attrsList,attrsMap}=astElconsole.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
は対象Elementに設定されている属性の一覧で、attrsMap
は属性名に対する設定値のマッピングですね。
ビルド結果を確認する①
webpack
でのビルドが完了すると、 output
で指定したパスにアセットファイルが生成されます。
それを覗いてみると、先程出力したASTをベースに、ピュアなJavascriptでVueオブジェクトを生成しているコードが見つかります。
varrender=function(){var_vm=thisvar_h=_vm.$createElementvar_c=_vm._self._c||_hreturn_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}=astElif(attrsMap['data-test']){deleteattrsMap['data-test']constindex=attrsList.findIndex(x=>x.name==='data-test')attrsList.splice(index,1)}returnastEl}
これによって、もし対象Elementが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では全く異なる属性として扱われます。
やや冗長になっちゃうので、汎用的なコードを抜き出しましょう。
functionremoveAttr(astEl,name){const{attrsList,attrsMap}=astElif(attrsMap[name]){deleteattrsMap[name]constindex=attrsList.findIndex(x=>x.name===name)attrsList.splice(index,1)}returnastEl}
preTransformNode
からは、二つの属性を指定して関数を呼び出すだけにしてあげます。
preTransformNode(astEl){const{attrsList,attrsMap}=astElremoveAttr(astEl,'data-test')removeAttr(astEl,':data-test')console.log({attrsList,attrsMap})console.log('-----')returnastEl}
{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のビルド結果をもう一度見てみましょう。
varstaticRenderFns=[function(){var_vm=thisvar_h=_vm.$createElementvar_c=_vm._self._c||_hreturn_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
といった属性は残っていることが確認できました!