亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

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

 更新時(shí)間:2020年04月03日 08:52:04   作者:街角小林  
這篇文章主要介紹了vue組件庫(kù)的在線主題編輯器的實(shí)現(xiàn)思路,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下

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

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

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

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

主題在線編輯的核心其實(shí)就是以一種可視化的方式來(lái)修改主題對(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ù)覽的效果。

主題列表頁(yè)面

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

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

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

{
 name: '官方主題-1', // 主題名稱
 by: 'by hui', // 來(lái)源
 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)里并沒(méi)有類似id的字段。另外還有一個(gè)小問(wèn)題是當(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
}

界面效果如下:

因?yàn)樯婕暗綆讉€(gè)頁(yè)面及不同組件間的互相通信,所以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è)值來(lái)修改對(duì)應(yīng)主題的數(shù)據(jù),這個(gè)值也會(huì)在localstorage里存一份。

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

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

編輯預(yù)覽頁(yè)面

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

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

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

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

同時(shí)需要監(jiān)聽(tīng)一下editingActionType值的變化來(lái)滾動(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:// 列表,用來(lái)分組進(jìn)行顯示
 }]
}

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

因?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)目目前版本是存在此問(wèn)題的。

此外還需要和當(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í)行效率加上該過(guò)濾條件
 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)的和值不是變量的話就跳過(guò)
 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方法通過(guò)遞歸進(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)
 }
 }
 }
 }
}

接下來(lái)是具體的控件顯示邏輯,根據(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ù)變量類型來(lái)顯示對(duì)應(yīng)的控件
 return componentMap[this.data.type]
 }
 }
}
</script>

一共有顏色選擇組件、輸入框組件、選擇器組件、陰影編輯組件,具體實(shí)現(xiàn)很簡(jiǎn)單就不細(xì)說(shuō)了,大概就是顯示初始傳入的變量,然后修改后觸發(fā)修改事件change,經(jīng)Control組件傳遞到Editor組件,在Editor組件上進(jìn)行變量修改及發(fā)送編譯請(qǐng)求,不過(guò)其中陰影組件的實(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)來(lái)進(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()
}

接下來(lái)是發(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)單起見(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)跑通,接下來(lái)是一些提升體驗(yàn)的功能。

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

2.前進(jìn)回退功能:前進(jìn)回退功能說(shuō)白了就是把每一步操作的數(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ù)??梢酝ㄟ^(guò)對(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)還是回退來(lái)設(shè)置historyIndex的值,同時(shí)取出對(duì)應(yīng)位置的主題和變量數(shù)據(jù)替換當(dāng)前的數(shù)據(jù),然后請(qǐng)求編譯

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

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

因?yàn)轭伾A(yù)覽組件是需要顯示當(dāng)前顏色和顏色值的,那么就會(huì)有一個(gè)問(wèn)題,字體顏色不能寫死,否則如果字體寫死白色,那么如果這個(gè)變量的顏色值又修改成白色,那么將一片白色,啥也看不見(jiàn),所以需要?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包,前端傳過(guò)來(lái)的參數(shù)其實(shí)就一個(gè)json類型的對(duì)象,key是變量,value是值,但是這兩個(gè)包都不支持傳入額外的變量數(shù)據(jù)和本地的scss文件進(jìn)行合并編譯,但是提供了一個(gè)配置項(xiàng):importer,可以傳入函數(shù)數(shù)組,它會(huì)在編譯過(guò)程中遇到 @use or @import 語(yǔ)法時(shí)執(zhí)行這個(gè)函數(shù),入?yún)閡rl,可以返回一個(gè)對(duì)象:

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

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

但是實(shí)際使用過(guò)程中,不知為何sass包的這個(gè)配置項(xiàng)是無(wú)效的,所以只能使用node-sass,這兩個(gè)包的api基本是一樣的,但是node-sass安裝起來(lái)比較麻煩,尤其是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)變量的值替換為前端傳過(guò)來(lái)的變量,替換完成后通過(guò)importer函數(shù)返回進(jìn)行編譯,具體替換方式也有多種,我同事的方法是自己寫了個(gè)scss解析器,解析成對(duì)象,然后遍歷對(duì)象解析替換,而我,比較草率,直接用正則匹配解析修改,實(shí)現(xiàn)如下:

function(data) {
 // 前端傳遞過(guò)來(lái)的數(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)化,比如之前提到的變量依賴問(wèn)題,還有scss的解析合并方式,此外還有多語(yǔ)言、多版本的問(wèn)題需要考慮。

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

相關(guān)文章

  • vue使用html2canvas和jspdf將html轉(zhuǎn)成pdf

    vue使用html2canvas和jspdf將html轉(zhuǎn)成pdf

    在前端開發(fā)中, html轉(zhuǎn)pdf是最常見(jiàn)的需求,下面這篇文章主要給大家介紹了關(guān)于vue如何使用html2canvas和jspdf將html轉(zhuǎn)成pdf的相關(guān)資料,文中通過(guò)實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下
    2022-03-03
  • 詳解VUE響應(yīng)式原理

    詳解VUE響應(yīng)式原理

    這篇文章主要為大家介紹了vue組件通信的幾種方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下,希望能夠給你帶來(lái)幫助
    2021-11-11
  • Vue實(shí)現(xiàn)數(shù)據(jù)請(qǐng)求攔截

    Vue實(shí)現(xiàn)數(shù)據(jù)請(qǐng)求攔截

    這篇文章主要為大家詳細(xì)介紹了Vue實(shí)現(xiàn)數(shù)據(jù)請(qǐng)求攔截,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2019-10-10
  • ElementUI Tree 樹形控件的使用并給節(jié)點(diǎn)添加圖標(biāo)

    ElementUI Tree 樹形控件的使用并給節(jié)點(diǎn)添加圖標(biāo)

    這篇文章主要介紹了ElementUI Tree 樹形控件的使用并給節(jié)點(diǎn)添加圖標(biāo),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧
    2020-02-02
  • vue中elementUI表單循環(huán)驗(yàn)證方式

    vue中elementUI表單循環(huán)驗(yàn)證方式

    這篇文章主要介紹了vue中elementUI表單循環(huán)驗(yàn)證方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2023-10-10
  • vue中element-ui不能修改el-input框,或是不能修改某些值問(wèn)題

    vue中element-ui不能修改el-input框,或是不能修改某些值問(wèn)題

    這篇文章主要介紹了vue中element-ui不能修改el-input框,或是不能修改某些值問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2023-10-10
  • Vue3全局掛載使用Axios學(xué)習(xí)實(shí)戰(zhàn)

    Vue3全局掛載使用Axios學(xué)習(xí)實(shí)戰(zhàn)

    這篇文章主要為大家介紹了Vue3全局掛載使用Axios學(xué)習(xí)實(shí)戰(zhàn)示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2022-06-06
  • Vue el-table組件如何實(shí)現(xiàn)將日期格式化

    Vue el-table組件如何實(shí)現(xiàn)將日期格式化

    這篇文章主要介紹了Vue el-table組件如何實(shí)現(xiàn)將日期格式化問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2023-04-04
  • Vue3+TypeScript實(shí)現(xiàn)二維碼生成組件

    Vue3+TypeScript實(shí)現(xiàn)二維碼生成組件

    在?Web?應(yīng)用中,生成二維碼是常見(jiàn)的需求,本文介紹如何用?Vue3?和?TypeScript?開發(fā)一個(gè)二維碼生成組件,支持生成圖片或?canvas?形式的二維碼,并提供豐富的配置選項(xiàng),感興趣的小伙伴跟著小編一起來(lái)看看吧
    2024-04-04
  • vue中動(dòng)態(tài)修改img標(biāo)簽中src的方法實(shí)踐

    vue中動(dòng)態(tài)修改img標(biāo)簽中src的方法實(shí)踐

    本文主要介紹了vue中動(dòng)態(tài)修改img標(biāo)簽中src的方法實(shí)踐,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧
    2023-07-07

最新評(píng)論