某次小組內開週會,提到開發效率的問題,有個小夥伴提到寫新頁面的時候,template
大概佈局寫完後,對着 template
結構寫 scss
是件比較耗時耗力的事情,若是能做出一個自動依據 template
結構生成 scss
文件的 vscode
插件就行了css
我當時也沒在乎,後來週會結束後以爲這事情能夠作一下,因而抽空看了下 vscode
擴展的開發文檔,就上手 code
了,作出來後效果還不錯,最起碼不用再作人工對着 template
寫 scss
這種沒技術含量的事情了,寫好了一大堆 template
以後,一鍵轉換,仍是挺爽的 html
vscode
擴展市場,能夠在
vscode
插件市場查找安裝,或者
vscode
上直接搜索
AutoScssStruct4Vue 安裝
插件的源碼已經上傳到 Github ,須要的可自取,若是有問題,直接提 issue 便可vue
scss
文件的關鍵就是選擇器,能在模板上體現出來的選擇器屬性有 class
、id
和標籤名,因此必需要從模板中取得全部的選擇器git
模板的處理實際上就是字符串的處理,經過正則表達式從 <template>...</template>
中提取所需的選擇器名,既然是 vue
,我第一個就想到直接從 vue
源碼中將 vue
處理 template
的部分抄過來,不事後來看了一下源碼,想法可行,可是性價比不高github
vue
將 template
解析成 ast
的部分,與其餘的一些處理邏輯耦合在一塊兒了,並且處理了不少我並不須要的東西,好比指令、組件這些,我只是想取個選擇器屬性而已,因此這個想法就放棄了正則表達式
再仔細一想,拋開 vue
的那些東西來看(好比指令處理這些),其實處理 template
跟處理普通的 html
片斷沒太大區別,因而問題就變成了將html
片斷轉爲 ast
,這個就容易不少了api
不過仍是有些差異,畢竟 vue
的 template
和普通 html
片斷之間的處理方式仍是有點不一樣的,差別點在於 vue
的一些特性,例如 v-bind
語法,以及組件標籤數組
如下以 class
這個選擇器屬性爲例,id
同此less
由於考慮到 v-bind
語法,因此在 parse
模板時,正則表達式須要將這個考慮進去,屬性匹配正則爲:編輯器
const attrRE = /((:|v-bind:)?[\w$\-]+)|(?<word>["'"]){1}[\s\S]*?\k<word>/g;
複製代碼
這個正則會將標籤的屬性全都匹配,由於vue
的class
屬性有三種寫法:
因此轉換爲 ast
後,可能的結構有:
{
attrs: {
class: 'box1 box2'
},
bindAttrs: {
class: "['box1', name]"
},
// bindAttrs: {
// class: "{isActive: 'name'}"
// }
}
複製代碼
考慮到直觀性,在解析模板的時候,若是遇到是 v-bind
的選擇器屬性,我會將此屬性記錄在 bindAttrs
字段上,若是是普通的字符串尚需經,則記錄在 attrs
屬性上,主要是爲了方便後續提取屬性值
對於 attrs
上的選擇器屬性,這個沒什麼好說,直接字符串匹配便可:
// 獲取標籤的 選擇器名
seletorStr.split(/\s+/).filter(item => item)
複製代碼
至於 v-bind
屬性的處理,其實也很好辦,對於 data
字段,例以下面代碼段中的 name
,這個 name
變量究竟是什麼,只有在運行的時候動態獲取,字符串靜態處理的狀況不可能知道 name
是什麼東西的,因此直接跳過
bindAttrs: {
class: "['box1', name]"
}
複製代碼
那這就好辦了,對於 "['box1', name]"
或者 "{isActive: 'name'}"
字符串來講,我只須要取引號中間的字符串便可,即 box1
和 name
最後,若是一個標籤既沒有 class
也沒有 id
,那麼就取這個標籤的標籤名做爲選擇器
相比於普通 html
片斷,vue
是有組件的概念的,除了內置組件還有自定義組件,這些組件能夠加選擇器屬性也能夠不加,能夠自閉合也能夠不自閉合,因此要根據這兩種狀況分別處理下
這裏暫且認爲某個標籤只要不是標準的 html
標籤,那麼就是組件,組件能夠加 class
、id
這種選擇器屬性,也能夠不加,若是加了選擇器屬性,則提取,不然就將組件的子元素當成組件不爲組件的父元素的子元素進行處理
可能有點繞,看個例子就明白了:
<div class="box1">
<List>
<p id="box2"></p>
</List>
<List class="box3">
<p id="box4"></p>
</List>
</div>
複製代碼
對於 #box2
來講,其父元素是個組件,而且此組件沒有選擇器屬性,又不可能將組件的標籤名List
當成選擇器,因此跳過 <List>
,將 .box1
當成是 #box2
元素的父元素來進行處理
而對於 #box4
來講,雖然它的父元素也是個組件,但這個組件有選擇器屬性 class
,因此不跳過其父元素,最後生成的 scss
結構以下:
.box1 {
#box2 {}
.box3 {
#box4 {}
}
}
複製代碼
將 template
轉成 ast
,獲取到 template
結構及選擇器,接着按照層級轉爲 scss
字符串便可
然而這是一次性操做,若是你後續還對 template
進行修改,若是再按照前面來這麼一下,生成新的結構字符串,豈不是把你本身寫的 css
規則給清除了?
好比:
<List class="box3">
<p id="box4"></p>
</List>
複製代碼
對於上述 template
來講,生成的 scss
以下:
.box3 {
#box4 {}
}
複製代碼
這是個結構,那你確定要添加規則的,好比:
.box3 {
width: 100px;
height: 200px;
#box4 {
color: #fff;
}
}
複製代碼
而後你改了下 template
,變成:
<List class="box3">
<p id="box4"></p>
<span></span>
</List>
複製代碼
再次生成:
.box3 {
#box4 {}
span {}
}
複製代碼
因此你以前寫的 css
規則沒了
template
的修改是很常見的,你不可能一上來就把一個組件的 template
寫得明明白白,因此這是個常見的場景,須要避免問題的產生
先根據新 template
生成新 Scss Ast
,而後對比新舊 Scss Ast
,根據兩者之間的差別修正新 ast
,填補對應結構上的 css
規則,這是一條可行之路,但考慮到scss
文件就是由 template Ast
映射而來的,因此選擇器的結構確定能對的上,直接對比 template ast
和 Scss Ast
之間的差別,在舊 Scss Ast
上進行修正,保留原有 css
規則也是可行的
生成新的 template
以後,將之與舊 scss
結構進行對比,由於考慮到 css
規則寫法的放飛性,爲了不誤刪規則,我這裏在對比結構時,只會在舊版本的 scss
上進行增長操做,而不會刪減內容,刪除操做由你本身來作,畢竟相比於寫,刪是一件很容易的事情
不可能直接對比 scss
字符串的,還須要將 scss
字符串轉爲 ast
纔好
這比 template
的轉譯還簡單,無非是字符串遍歷遞歸罷了,只不過因爲 css
規則的寫法太爲所欲爲,因此須要注意的小點不少,好比由於同一級別下,一個選擇器規則可能會被同時應用在多個標籤上,可是這些標籤只須要一個 css
規則就夠了,即存在標籤與 css
規則之間的多對一關係,須要進行過濾
獲得新的 template ast
以後,與舊 scss
結構進行對比,對比同級別下的節點的差別,前面說了,對舊版 scss
文件實行只增不減操做,因此若是發現新 ast
相比於同級別舊 ast
新增了節點,則在 Scss Ast
的同級別上進行新增 Scss Ast
節點的操做,若是發現 template
刪除了某個節點,則跳過不作處理
if (matchIndex === -1) {
// 沒找到,說明 template 中新增了標籤
scssObj.children = scssObj.children.slice(0, childIndex + i).concat(
{
rule: '',
selectorNames: selector,
children: i == 0 ? trackChildren(templateObj) : []
},
scssObj.children.slice(childIndex)
)
}
複製代碼
我這裏嚴格映射了子節點之間的前後順序,若是一個選擇器屬性對應的 template
節點是其父元素的第 n
個子元素,則在 Scss Ast
中,此選擇器屬性也會是其父元素的第 n
個子元素(不考慮重複選擇器的狀況下),這種順序將會被保留
對比修正完畢以後,將會獲得新 Scss Ast
結構,而且結構上保留了已有的 css
規則
主要是 ast
結構的遍歷操做,額外須要處理下換行縮進等問題,比較簡單
上述核心邏輯完成了,轉爲 vscode
插件其實就是作交互了,對着 vscode插件開發文檔 找 API
便可
對於這個插件,我設置了兩個設置項
其一是插件運行時機,其二是 scss
文件保存的位置
對於這個設置項,提供了兩個選項:當文件保存時,和當右鍵點選菜單欄時
當文件保存時,顧名思義,就是當你寫好了 template
以後,保存當前文件,插件自動解析當前 vue
文件中的 template
,獲得 scss
字符串
當右鍵點選菜單欄時,即你的鼠標在編輯器文檔上右鍵,出現一個菜單欄,點擊插件命令便可,此爲默認選項
scss
文件保存的位置(scssFilePath)你可能會在一個 .vue
文件上同時寫 template
、script
以及 style
,也可能將 scss
寫到單獨的文件中,這也是很常見的場景,因此提供了兩種選擇,要麼默認將 scss
字符串寫到當前 .vue
文件的 style
標籤內(若是不存在,則自動建立),要麼你指定 .scss
文件的保存路徑(若是不存在,則自動建立)
插件的開發邏輯其實很清晰,主要是須要考慮的場景不少,正則規則須要兼容各類爲所欲爲的寫法,雖然我這裏只考慮常見場景(不常見場景,例如類名用中文,這種寫法雖然符合規範,但我這裏不考慮),但常見場景的寫法依舊不少,稍有哪一種狀況沒考慮到就出 bug
若是有問題,直接提 issue 便可
這個插件當初製做的時候是以 scss
爲基準製做的,但理論上對於 less
也是適用的,由於這個其實就是選擇器層級嵌套的問題,不涉及到具體某個預處理器的語法,並且由於 scss
與 less
之間寫法很類似,涉及到的正則其實都是同樣的, 因此若是你用的是 less
,應該也是可使用的,對這方面有需求的同窗能夠先試用下,若是發現有什麼問題,請提 issue ,若是後續對於 less
需求的人比較多,我會專門兼容一下 less