溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

vue組件庫的在線主題編輯器的實(shí)現(xiàn)思路

發(fā)布時(shí)間:2020-10-25 01:22:24 來源:腳本之家 閱讀:375 作者:街角小林 欄目:web開發(fā)

一般而言一個(gè)組件庫都會(huì)設(shè)計(jì)一套相對(duì)來說符合大眾審美或產(chǎn)品需求的主題,但是主題定制需求永遠(yuǎn)都存在,所以組件庫一般都會(huì)允許使用者自定義主題,我司的vue組件庫hui的定制主題簡(jiǎn)單來說是通過修改預(yù)定義的scss變量的值來做到的,新體系下還做到了動(dòng)態(tài)換膚,因?yàn)槠つw本質(zhì)上是一種靜態(tài)資源(CSS文件和字體文件),所以只需要約定一種方式來每次動(dòng)態(tài)請(qǐng)求加載不同的文件就可以了,為了方便這一需求,還配套開發(fā)了一個(gè)Vessel腳手架的插件,只需要以配置文件的方式列出你需要修改的變量和值,一個(gè)命令就可以幫你生成對(duì)應(yīng)的皮膚。

但是目前的換膚還存在幾個(gè)問題, 一是不直觀,無法方便實(shí)時(shí)的看到修改后的組件效果,二是建議修改的變量比較少,這很大原因也是因?yàn)閱栴}一,因?yàn)椴恢庇^所以盲目修改后的效果可能達(dá)不到預(yù)期。

針對(duì)這幾個(gè)問題,所以實(shí)現(xiàn)一個(gè)在線主題編輯器是一個(gè)有意義的事情,目前最流行的組件庫之一的Element就支持主題在線編輯,地址:element.eleme.cn/#/zh-CN/the… ,本項(xiàng)目是在參考了Element的設(shè)計(jì)思想和界面效果后開發(fā)完成的,本文將開發(fā)思路分享出來,如果有一些不合理地方或有一些更好的實(shí)現(xiàn)方式,歡迎指出來一起討論。

實(shí)現(xiàn)思路

主題在線編輯的核心其實(shí)就是以一種可視化的方式來修改主題對(duì)應(yīng)scss變量的值。

項(xiàng)目總體分為前端和后端兩個(gè)部分,前端主要負(fù)責(zé)管理主題列表、編輯主題和預(yù)覽主題,后端主要負(fù)責(zé)返回變量列表和編譯主題。

后端返回主題可修改的變量信息,前端生成對(duì)應(yīng)的控件,用戶可進(jìn)行修改,修改后立即將修改的變量和修改后的值發(fā)送給后端,后端進(jìn)行合并編譯,生成css返回給前端,前端動(dòng)態(tài)替換style標(biāo)簽的內(nèi)容達(dá)到實(shí)時(shí)預(yù)覽的效果。

主題列表頁面

主題列表頁面的主要功能是顯示官方主題列表和顯示自定義主題列表。

官方主題可進(jìn)行的操作有預(yù)覽和復(fù)制,不能修改,修改的話會(huì)自動(dòng)生成新主題。自定義主題可以編輯和下載,及進(jìn)行修改名稱、復(fù)制、刪除操作。

官方主題列表后端返回,數(shù)據(jù)結(jié)構(gòu)如下:

{
 name: '官方主題-1', // 主題名稱
 by: 'by hui', // 來源
 description: '默認(rèn)主題', // 描述
 theme: {
 // 主題改動(dòng)點(diǎn)列表
 common: {
 '$--color-brand': '#e72528'
 }
 }
}

自定義主題保存在localstorage里,數(shù)據(jù)結(jié)構(gòu)如下:

{
 name: name, // 主題名稱
 update: Date.now(), // 最后一次修改時(shí)間
 theme: { // 主題改動(dòng)點(diǎn)列表
 common: {
 //...
 }
 }
}

復(fù)制主題即把要復(fù)制的主題的theme.common數(shù)據(jù)復(fù)制到新主題上即可。

需要注意的就是新建主題時(shí)要判斷主題名稱是否重復(fù),因?yàn)閿?shù)據(jù)結(jié)構(gòu)里并沒有類似id的字段。另外還有一個(gè)小問題是當(dāng)預(yù)覽官方主題時(shí)修改的話會(huì)自動(dòng)生成新主題,所以還需要自動(dòng)生成可用的主題名,實(shí)現(xiàn)如下:

const USER_THEME_NAME_PREFIX = '自定義主題-';
function getNextUserThemeName() {
 let index = 1
 // 獲取已經(jīng)存在的自定義主題列表
 let list = getUserThemesFromStore()
 let name = USER_THEME_NAME_PREFIX + index
 let exist = () => {
 return list.some((item) => {
 return item.name === name
 })
 }
 // 循環(huán)檢測(cè)主題名稱是否重復(fù)
 while (exist()) {
 index++
 name = USER_THEME_NAME_PREFIX + index
 }
 return name
}

界面效果如下:

vue組件庫的在線主題編輯器的實(shí)現(xiàn)思路

因?yàn)樯婕暗綆讉€(gè)頁面及不同組件間的互相通信,所以vuex是必須要使用的,vuex的state要存儲(chǔ)的內(nèi)容如下:

const state = {
 // 官方主題列表
 officialThemeList: [],
 // 自定義主題列表
 themeList: [],
 // 當(dāng)前編輯中的主題id
 editingTheme: null,
 // 當(dāng)前編輯的變量類型
 editingActionType: 'Color',
 // 可編輯的變量列表數(shù)據(jù)
 variableList: [],
 // 操作歷史數(shù)據(jù)
 historyIndex: 0,
 themeHistoryList: [],
 variableHistoryList: []
}

editingTheme是代表當(dāng)前正在編輯的名字,主題編輯時(shí)依靠這個(gè)值來修改對(duì)應(yīng)主題的數(shù)據(jù),這個(gè)值也會(huì)在localstorage里存一份。

editingActionType是代表當(dāng)前正在編輯中的變量所屬組件類型,主要作用是在切換要修改的組件類型后預(yù)覽列表滾動(dòng)到對(duì)應(yīng)的組件位置及用來渲染對(duì)應(yīng)主題變量對(duì)應(yīng)的編輯控件,如下:

vue組件庫的在線主題編輯器的實(shí)現(xiàn)思路

頁面在vue實(shí)例化前先獲取官方主題、自定義主題、最后一次編輯的主題名稱,設(shè)置到vuex的store里。

編輯預(yù)覽頁面

編輯預(yù)覽頁面主要分兩部分,左側(cè)是組件列表,右側(cè)是編輯區(qū)域,界面效果如下:

vue組件庫的在線主題編輯器的實(shí)現(xiàn)思路

組件預(yù)覽區(qū)域

組件預(yù)覽區(qū)域很簡(jiǎn)單,無腦羅列出所有組件庫里的組件,就像這樣:

<div class="list">
 <Color></Color>
 <Button></Button>
 <Radio></Radio>
 <Checkbox></Checkbox>
 <Inputer></Inputer>
 <Autocomplete></Autocomplete>
 <InputNumber></InputNumber>
 //...
</div>

同時(shí)需要監(jiān)聽一下editingActionType值的變化來滾動(dòng)到對(duì)應(yīng)組件的位置:

<script>
{
 watch: {
 '$store.state.editingActionType'(newVal) {
 this.scrollTo(newVal)
 }
 },
 methods:{
 scrollTo(id) {
 switch (id) {
 case 'Input':
  id = 'Inputer'
  break;
 default:
  break;
 }
 let component = this.$children.find((item) =>{
 return item.$options._componentTag === id
 })
 if (component) {
 let el = component._vnode.elm
 let top = el.getBoundingClientRect().top + document.documentElement.scrollTop
 document.documentElement.scrollTop = top - 20
 }
 }
 }
}
</script>

編輯區(qū)域

編輯區(qū)域主要分為三部分,工具欄、選擇欄、控件區(qū)。這部分是本項(xiàng)目的核心也是最復(fù)雜的一部分。

先看一下變量列表的數(shù)據(jù)結(jié)構(gòu):

{
 "name": "Color",// 組件類型/類別
 "config": [{// 配置列表
 "type": "color",// 變量類型,根據(jù)此字段渲染對(duì)應(yīng)類型的控件
 "key": "$--color-brand",// sass變量名
 "value": "#e72528",// sass變量對(duì)應(yīng)的值,可以是具體的值,也可以是sass變量名
 "category": "Brand Color"http:// 列表,用來分組進(jìn)行顯示
 }]
}

此列表是后端返回的,選擇器的選項(xiàng)是遍歷該列表取出所有的name字段的值而組成的。

vue組件庫的在線主題編輯器的實(shí)現(xiàn)思路

因?yàn)橛行┳兞康闹凳且蕾嚵硪粋€(gè)變量的,所依賴的變量也有可能還依賴另一個(gè)變量,所以需要對(duì)數(shù)據(jù)進(jìn)行處理,替換成變量最終的值,實(shí)現(xiàn)方式就是循環(huán)遍歷數(shù)據(jù),這就要求所有被依賴的變量也存在于這個(gè)列表中,否則就找不到了,只能顯示變量名,所以這個(gè)實(shí)現(xiàn)方式其實(shí)是有待商榷的,因?yàn)橛行┍灰蕾嚨淖兞克赡懿⒉恍枰虿荒芸删庉?,本?xiàng)目目前版本是存在此問題的。

此外還需要和當(dāng)前編輯中的主題變量的值進(jìn)行合并,處理如下:

// Editor組件
async getVariable() {
 try {
 // 獲取變量列表,res.data就是變量列表,數(shù)據(jù)結(jié)構(gòu)上面已經(jīng)提到了
 let res = await api.getVariable()
 // 和當(dāng)前主題變量進(jìn)行合并
 let curTheme = store.getUserThemeByNameFromStore(this.$store.state.editingTheme) || {}
 let list = []
 // 合并
 list = this.merge(res.data, curTheme.theme)

 // 變量進(jìn)行替換處理,因?yàn)槟壳按嬖谠撉闆r的只有顏色類型的變量,所以為了執(zhí)行效率加上該過濾條件
 list = store.replaceVariable(list, ['color'])

 // 排序
 list = this.sortVariable(list)

 this.variableList = list

 // 存儲(chǔ)到vuex
 this.$store.commit('updateVariableList', this.variableList)
 } catch (error) {
 console.log(error)
 }
}

merge方法就是遍歷合并對(duì)應(yīng)變量key的值,主要看replaceVariable方法:

function replaceVariable(data, types) {
 // 遍歷整體變量列表
 for(let i = 0; i < data.length; i++) {
 let arr = data[i].config
 // 遍歷某個(gè)類別下的變量列表
 for(let j = 0; j < arr.length; j++) {
 // 如果不在替換類型范圍內(nèi)的和值不是變量的話就跳過
 if (!types.includes(arr[j].type) || !checkVariable(arr[j].value)) {
 continue
 }
 // 替換處理
 arr[j].value = findVariableReplaceValue(data, arr[j].value) || arr[j].value
 }
 }
 return data
}

findVariableReplaceValue方法通過遞歸進(jìn)行查找:

function findVariableReplaceValue(data, value) {
 for(let i = 0; i < data.length; i++) {
 let arr = data[i].config
 for(let j = 0; j < arr.length; j++) {
 if (arr[j].key === value) {
 // 如果不是變量的話就是最終的值,返回就好了
 if (!checkVariable(arr[j].value)) {
 return arr[j].value
 } else {// 如果還是變量的話就遞歸查找
 return findVariableReplaceValue(data, arr[j].value)
 }
 }
 }
 }
}

接下來是具體的控件顯示邏輯,根據(jù)當(dāng)前編輯中的類型對(duì)應(yīng)的配置數(shù)據(jù)進(jìn)行渲染,模板如下:

// Editor組件
<template>
 <div class="editorContainer">
 <div class="editorBlock" v-for="items in data" :key="items.name">
 <div class="editorBlockTitle">{{items.name}}</div>
 <ul class="editorList">
 <li class="editorItem" v-for="item in items.list" :key="item.key">
 <div class="editorItemTitle">{{parseName(item.key)}}</div>
 <Control :data="item" @change="valueChange"></Control>
 </li>
 </ul>
 </div>
 </div>
</template>

data是對(duì)應(yīng)變量類型里的config數(shù)據(jù),是個(gè)計(jì)算屬性:

{
 computed: {
 data() {
 // 找出當(dāng)前編輯中的變量類別
 let _data = this.$store.state.variableList.find(item => {
 return item.name === this.$store.state.editingActionType
 })
 if (!_data) {
 return []
 }
 let config = _data.config
 // 進(jìn)行分組
 let categorys = []
 config.forEach(item => {
 let category = categorys.find(c => {
  return c.name === item.category
 })
 if (!category) {
  categorys.push({
  name: item.category,
  list: [item]
  })
  return false
 }
 category.list.push(item)
 })
 return categorys
 }
 }
}

Control是具體的控件顯示組件,某個(gè)變量具體是用輸入框還是下拉列表都在這個(gè)組件內(nèi)進(jìn)行判斷,核心是使用component動(dòng)態(tài)組件:

// Control組件
<template>
 <div class="controlContainer">
 <component :is="showComponent" :data="data" :value="data.value" @change="emitChange" :extraColorList="extraColors"></component>
 </div>
</template>
<script>
// 控件類型映射
const componentMap = {
 color: 'ColorPicker',
 select: 'Selecter',
 input: 'Inputer',
 shadow: 'Shadow',
 fontSize: 'Selecter',
 fontWeight: 'Selecter',
 fontLineHeight: 'Selecter',
 borderRadius: 'Selecter',
 height: 'Inputer',
 padding: 'Inputer',
 width: 'Inputer'
}
{
 computed: {
 showComponent() {
 // 根據(jù)變量類型來顯示對(duì)應(yīng)的控件
 return componentMap[this.data.type]
 }
 }
}
</script>

一共有顏色選擇組件、輸入框組件、選擇器組件、陰影編輯組件,具體實(shí)現(xiàn)很簡(jiǎn)單就不細(xì)說了,大概就是顯示初始傳入的變量,然后修改后觸發(fā)修改事件change,經(jīng)Control組件傳遞到Editor組件,在Editor組件上進(jìn)行變量修改及發(fā)送編譯請(qǐng)求,不過其中陰影組件的實(shí)現(xiàn)折磨了我半天,主要是如何解析陰影數(shù)據(jù),這里用的是很暴力的一種解析方法,如果有更好的解析方式的話可以留言進(jìn)行分享:

// 解析css陰影數(shù)據(jù)
// 因?yàn)閞gb顏色值內(nèi)也存在逗號(hào),所以就不能簡(jiǎn)單的用逗號(hào)進(jìn)行切割解析
function parse() {
 if (!this.value) {
 return false
 }
 // 解析成復(fù)合值數(shù)組
 // let value = "0 0 2px 0 #666,0 0 2px 0 #666, 0 2px 4px 0 rgba(0, 0, 0, 0.12), 0 2px 4px 0 hlsa(0, 0, 0, 0.12),0 2px 4px 0 #sdf, 0 2px 4px 0 hlsa(0, 0, 0, 0.12), 0 2px 0 hlsa(0, 0, 0, 0.12), 0 2px hlsa(0, 0, 0, 0.12), 0 2px 4px 0 hlsa(0, 0, 0, 0.12)"
 // 根據(jù)右括號(hào)來進(jìn)行分割成數(shù)組
 let arr = this.value.split(/\)\s*,\s*/gim)
 arr = arr.map(item => {
 // 補(bǔ)上右括號(hào)
 if (item.includes('(') && !item.includes(')')) {
 return item + ')'
 } else {// 非rgb顏色值的直接返回
 return item
 }
 })
 let farr = []
 arr.forEach(item => {
 let quene = []
 let hasBrackets = false
 // 逐個(gè)字符進(jìn)行遍歷
 for (let i = 0; i < item.length; i++) {
 // 遇到非顏色值內(nèi)的逗號(hào)直接拼接目前隊(duì)列里的字符添加到數(shù)組
 if (item[i] === ',' && !hasBrackets) {
 farr.push(quene.join('').trim())
 quene = []
 } else if (item[i] === '(') {//遇到顏色值的左括號(hào)修改標(biāo)志位
 hasBrackets = true
 quene.push(item[i])
 } else if (item[i] === ')') {//遇到右括號(hào)重置標(biāo)志位
 hasBrackets = false
 quene.push(item[i])
 } else {// 其他字符直接添加到隊(duì)列里
 quene.push(item[i])
 }
 }
 // 添加隊(duì)列剩余的數(shù)據(jù)
 farr.push(quene.join('').trim())
 })
 // 解析出單個(gè)屬性
 let list = []
 farr.forEach(item => {
 let colorRegs = [/#[a-zA-Z0-9]{3,6}$/, /rgba?\([^()]+\)$/gim, /hlsa?\([^()]+\)$/gim, /\s+[a-zA-z]+$/]
 let last = ''
 let color = ''
 for (let i = 0; i < colorRegs.length; i++) {
 let reg = colorRegs[i]
 let result = reg.exec(item)
 if (result) {
 color = result[0]
 last = item.slice(0, result.index)
 break
 }
 }
 let props = last.split(/\s+/)
 list.push({
 xpx: parseInt(props[0]),
 ypx: parseInt(props[1]),
 spread: parseInt(props[2]) || 0,
 blur: parseInt(props[3]) || 0,
 color
 })
 })
 this.list = list
}

回到Editor組件,編輯控件觸發(fā)了修改事件后需要更新變量列表里面對(duì)應(yīng)的值及對(duì)應(yīng)主題列表里面的值,同時(shí)要發(fā)送編譯請(qǐng)求:

// data是變量里config數(shù)組里的一項(xiàng),value就是修改后的值
function valueChange(data, value) {
 // 更新當(dāng)前變量對(duì)應(yīng)key的值
 let cloneData = JSON.parse(JSON.stringify(this.$store.state.variableList))
 let tarData = cloneData.find((item) => {
 return item.name === this.$store.state.editingActionType
 })
 tarData.config.forEach((item) => {
 if (item.key === data.key) {
 item.value = value
 }
 })
 // 因?yàn)槭侵С诸伾敌薷臑槟承┳兞康?,所以要重新進(jìn)行變量替換處理
 cloneData = store.replaceVariable(cloneData, ['color'])
 this.$store.commit('updateVariableList', cloneData)
 // 更新當(dāng)前主題
 let curTheme = store.getUserThemeByNameFromStore(this.$store.state.editingTheme, true)
 if (!curTheme) {// 當(dāng)前是官方主題則創(chuàng)建新主題
 let theme = store.createNewUserTheme('', {
 [data.key]: value
 })
 this.$store.commit('updateEditingTheme', theme.name)
 } else {// 修改的是自定義主題
 curTheme.theme.common = {
 ...curTheme.theme.common,
 [data.key]: value
 }
 store.updateUserTheme(curTheme.name, {
 theme: curTheme.theme
 })
 }
 // 請(qǐng)求編譯
 this.updateVariable()
}

接下來是發(fā)送編譯請(qǐng)求:

async function updateVariable() {
 let curTheme = store.getUserThemeByNameFromStore(this.$store.state.editingTheme, true, true)
 try {
 let res = await api.updateVariable(curTheme.theme)
 this.replaceTheme(res.data)
 } catch (error) {
 console.log(error)
 }
}

參數(shù)為當(dāng)前主題修改的變量數(shù)據(jù),后端編譯完后返回css字符串,需要?jiǎng)討B(tài)插入到head標(biāo)簽里:

function replaceTheme(data) {
 let id = 'HUI_PREVIEW_THEME'
 let el = document.querySelector('#' + id)
 if (el) {
 el.innerHTML = data
 } else {
 el = document.createElement('style')
 el.innerHTML = data
 el.id = id
 document.head.appendChild(el)
 }
}

這樣就達(dá)到了修改變量后實(shí)時(shí)預(yù)覽的效果,下載主題也是類似,把當(dāng)前編輯的主題的數(shù)據(jù)發(fā)送給后端編譯完后生成壓縮包進(jìn)行下載。

下載:因?yàn)橐l(fā)送主題變量進(jìn)行編譯下載,所以不能使用get方法,但使用post方法進(jìn)行下載比較麻煩,所以為了簡(jiǎn)單起見,下載操作實(shí)際是在瀏覽器端做的。

function downloadTheme(data) {
 axios({
 url: '/api/v1/download',
 method: 'post',
 responseType: 'blob', // important
 data
 }).then((response) => {
 const url = window.URL.createObjectURL(new Blob([response.data]))
 const link = document.createElement('a')
 link.href = url
 link.setAttribute('download', 'theme.zip')
 link.click()
 })
}

至此,主流程已經(jīng)跑通,接下來是一些提升體驗(yàn)的功能。

1.重置功能:重置理應(yīng)是重置到某個(gè)主題復(fù)制來源的那個(gè)主題的,但是其實(shí)必要性也不是特別大,所以就簡(jiǎn)單做,直接把當(dāng)前主題的配置變量清空,即theme.common={},同時(shí)需要重新請(qǐng)求變量數(shù)據(jù)及請(qǐng)求編譯。

2.前進(jìn)回退功能:前進(jìn)回退功能說白了就是把每一步操作的數(shù)據(jù)都克隆一份并存到一個(gè)數(shù)組里,然后設(shè)置一個(gè)指針,比如index,指向當(dāng)前所在的位置,前進(jìn)就是index++,后退就是index--,然后取出對(duì)應(yīng)數(shù)組里的數(shù)據(jù)替換當(dāng)前的數(shù)據(jù)。對(duì)于本項(xiàng)目,需要存兩個(gè)東西,一個(gè)是主題數(shù)據(jù),一個(gè)是變量數(shù)據(jù)??梢酝ㄟ^對(duì)象形式存到一個(gè)數(shù)組里,也可以向本項(xiàng)目一樣搞兩個(gè)數(shù)組。

具體實(shí)現(xiàn):

1.先把初始的主題數(shù)據(jù)拷貝一份扔進(jìn)歷史數(shù)組themeHistoryList里,請(qǐng)求到變量數(shù)據(jù)后扔進(jìn)variableHistoryList數(shù)組里

2.每次修改后把修改后的變量數(shù)據(jù)和主題數(shù)據(jù)都復(fù)制一份扔進(jìn)去,同時(shí)指針historyIndex加1

3.根據(jù)前進(jìn)還是回退來設(shè)置historyIndex的值,同時(shí)取出對(duì)應(yīng)位置的主題和變量數(shù)據(jù)替換當(dāng)前的數(shù)據(jù),然后請(qǐng)求編譯

需要注意的是在重置和返回主題列表頁面時(shí)要復(fù)位themeHistoryList、variableHistoryList、historyIndex

3.顏色預(yù)覽組件優(yōu)化

vue組件庫的在線主題編輯器的實(shí)現(xiàn)思路

因?yàn)轭伾A(yù)覽組件是需要顯示當(dāng)前顏色和顏色值的,那么就會(huì)有一個(gè)問題,字體顏色不能寫死,否則如果字體寫死白色,那么如果這個(gè)變量的顏色值又修改成白色,那么將一片白色,啥也看不見,所以需要?jiǎng)討B(tài)判斷是用黑色還是白色,有興趣詳細(xì)了解判斷算法可閱讀:

function const getContrastYIQ = (hexcolor) => {
 hexcolor = colorToHEX(hexcolor).substring(1)
 let r = parseInt(hexcolor.substr(0, 2), 16)
 let g = parseInt(hexcolor.substr(2, 2), 16)
 let b = parseInt(hexcolor.substr(4, 2), 16)
 let yiq = ((r * 299) + (g * 587) + (b * 114)) / 1000
 return (yiq >= 128) ? 'black' : 'white'
}

colorToHEX是一個(gè)將各種類型的顏色值都轉(zhuǎn)為十六進(jìn)制顏色的函數(shù)。

4.一些小細(xì)節(jié)

logo、導(dǎo)航、返回按鈕、返回頂部等小控件隨當(dāng)前編輯中的主題色進(jìn)行變色。

到這里前端部分就結(jié)束了,讓我們喝口水繼續(xù)。

后端部分

后端用的是nodejs及eggjs框架,對(duì)eggjs不熟悉的話可先閱讀一下文檔: eggjs.org/zh-cn/ ,后端部分比較簡(jiǎn)單,先看路由:

module.exports = app => {
 const { router, controller } = app

 // 獲取官方主題列表
 router.get(`${BASE_URL}/getOfficialThemes`, controller.index.getOfficialThemes)

 // 返回變量數(shù)據(jù)
 router.get(`${BASE_URL}/getVariable`, controller.index.getVariable)

 // 編譯scss
 router.post(`${BASE_URL}/updateVariable`, controller.index.updateVariable)

 // 下載
 router.post(`${BASE_URL}/download`, controller.index.download)
}

目前官方主題列表和變量數(shù)據(jù)都是一個(gè)寫死的json文件。所以核心只有兩部分,編譯scss和下載,先看編譯。

編譯scss

主題在線編輯能實(shí)現(xiàn)靠的就是scss的變量功能,編譯scss可用使用sass包或者node-sass包,前端傳過來的參數(shù)其實(shí)就一個(gè)json類型的對(duì)象,key是變量,value是值,但是這兩個(gè)包都不支持傳入額外的變量數(shù)據(jù)和本地的scss文件進(jìn)行合并編譯,但是提供了一個(gè)配置項(xiàng):importer,可以傳入函數(shù)數(shù)組,它會(huì)在編譯過程中遇到 @use or @import 語法時(shí)執(zhí)行這個(gè)函數(shù),入?yún)閡rl,可以返回一個(gè)對(duì)象:

{
 contents: `
 h2 {
 font-size: 40px;
 }
 `
}

contents的內(nèi)容即會(huì)替代原本要引入的對(duì)應(yīng)scss文件的內(nèi)容,詳情請(qǐng)看:sass-lang.com/documentati…

但是實(shí)際使用過程中,不知為何sass包的這個(gè)配置項(xiàng)是無效的,所以只能使用node-sass,這兩個(gè)包的api基本是一樣的,但是node-sass安裝起來比較麻煩,尤其是windows上,安裝方法大致有兩種:

npm install -g node-gyp
npm install --global --production windows-build-tools
npm install node-sass --save-dev

npm install -g cnpm --registry=https://registry.npm.taobao.org
cnpm install node-sass

因?yàn)橹黝}的變量定義一般都在統(tǒng)一的一個(gè)或幾個(gè)文件內(nèi),像hui,是定義在var-common.scssvar.scss兩個(gè)文件內(nèi),所以可以讀取這兩個(gè)文件的內(nèi)容然后將其中對(duì)應(yīng)變量的值替換為前端傳過來的變量,替換完成后通過importer函數(shù)返回進(jìn)行編譯,具體替換方式也有多種,我同事的方法是自己寫了個(gè)scss解析器,解析成對(duì)象,然后遍歷對(duì)象解析替換,而我,比較草率,直接用正則匹配解析修改,實(shí)現(xiàn)如下:

function(data) {
 // 前端傳遞過來的數(shù)據(jù)
 let updates = data.common
 // 兩個(gè)文件的路徑
 let commonScssPath = path.join(process.cwd(), 'node_modules/hui/packages/theme/common/var-common.scss')
 let varScssPath = path.join(process.cwd(), 'node_modules/hui/packages/theme/common/var.scss')
 // 讀取兩個(gè)文件的內(nèi)容
 let commonScssContent = fs.readFileSync(commonScssPath, {encoding: 'utf8'})
 let varScssContent = fs.readFileSync(varScssPath, {encoding: 'utf8'})
 // 遍歷要修改的變量數(shù)據(jù)
 Object.keys(updates).forEach((key) => {
 let _key = key
 // 正則匹配及替換
 key = key.replace('$', '\\$')
 let reg = new RegExp('(' +key + '\\s*:\\s*)([^:]+)(;)', 'img')
 commonScssContent = commonScssContent.replace(reg, `$1${updates[_key]}$3`)
 varScssContent = varScssContent.replace(reg, `$1${updates[_key]}$3`)
 })
 // 修改路徑為絕對(duì)路徑,否則會(huì)報(bào)錯(cuò)
 let mixinsPath = path.resolve(process.cwd(), 'node_modules/hui/packages/theme/mixins/_color-helpers.scss')
 mixinsPath = mixinsPath.split('\\').join('/')
 commonScssContent = commonScssContent.replace(`@import '../mixins/_color-helpers'`, `@import '${mixinsPath}'`)
 let huiScssPath = path.join(process.cwd(), 'node_modules/hui/packages/theme/index.scss')
 // 編譯scss
 let result = sass.renderSync({
 file: huiScssPath,
 importer: [
 function (url) {
 if (url.includes('var-common')) {
  return {
  contents: commonScssContent
  }
 }else if (url.includes('var')) {
  return {
  contents: varScssContent
  }
 } else {
  return null
 }
 }
 ]
 })
 return result.css.toString()
}

下載主題

下載的主題包里有兩個(gè)數(shù)據(jù),一個(gè)是配置源文件,另一個(gè)就是編譯后的主題包,包括css文件和字體文件。創(chuàng)建壓縮包使用的是jszip,可參考: github.com/Stuk/jszip 。

主題包的目錄結(jié)構(gòu)如下:

-theme --fonts --index.css -config.json

實(shí)現(xiàn)如下:

async createThemeZip(data) {
 let zip = new JSZip()
 // 配置源文件
 zip.file('config.json', JSON.stringify(data.common, null, 2))
 // 編譯后的css主題包
 let theme = zip.folder('theme')
 let fontPath = 'node_modules/hui/packages/theme/fonts'
 let fontsFolder = theme.folder('fonts')
 // 遍歷添加字體文件
 let loopAdd = (_path, folder) => {
 fs.readdirSync(_path).forEach((file) => {
 let curPath = path.join(_path, file)
 if (fs.statSync(curPath).isDirectory()) {
 let newFolder = folder.folder(file)
 loopAdd(curPath, newFolder)
 } else {
 folder.file(file, fs.readFileSync(curPath))
 }
 })
 }
 loopAdd(fontPath, fontsFolder)
 // 編譯后的css
 let css = await huiComplier(data)
 theme.file('index.css', css)
 // 壓縮
 let result = await zip.generateAsync({
 type: 'nodebuffer'
 })
 // 保存到本地
 // fs.writeFileSync('theme.zip', result, (err) => {
 // if (err){
 // this.ctx.logger.warn('壓縮失敗', err)
 // }
 // this.ctx.logger.info('壓縮完成')
 // })
 return result
 }

至此,前端和后端的核心實(shí)現(xiàn)都已介紹完畢。

總結(jié)

本項(xiàng)目目前只是一個(gè)粗糙的實(shí)現(xiàn),旨在提供一個(gè)實(shí)現(xiàn)思路,還有很多細(xì)節(jié)需要優(yōu)化,比如之前提到的變量依賴問題,還有scss的解析合并方式,此外還有多語言、多版本的問題需要考慮。

到此這篇關(guān)于vue組件庫的在線主題編輯器的實(shí)現(xiàn)思路的文章就介紹到這了,更多相關(guān)vue在線主題編輯器內(nèi)容請(qǐng)搜索億速云以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持億速云!

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI