主题在线编辑能实现靠的就是scss的变量功能,编译scss可用使用sass包或者node-sass包,前端传过来的参数其实就一个json类型的对象,key是变量,value是值,但是这两个包都不支持传入额外的变量数据和本地的scss文件进行合并编译,但是提供了一个配置项:importer,可以传入函数数组,它会在编译过程中遇到 @use or @import 语法时执行这个函数,入参为url,可以返回一个对象:
{ contents: ` h1 { font-size: 40px; } ` }
contents的内容即会替代原本要引入的对应scss文件的内容,详情请看:…
但是实际使用过程中,不知为何sass包的这个配置项是无效的,所以只能使用node-sass,这两个包的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
因为主题的变量定义一般都在统一的一个或几个文件内,像hui,是定义在var-common.scss和var.scss两个文件内,所以可以读取这两个文件的内容然后将其中对应变量的值替换为前端传过来的变量,替换完成后通过importer函数返回进行编译,具体替换方式也有多种,我同事的方法是自己写了个scss解析器,解析成对象,然后遍历对象解析替换,而我,比较草率,直接用正则匹配解析修改,实现如下:
function(data) { // 前端传递过来的数据 let updates = data.common // 两个文件的路径 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') // 读取两个文件的内容 let commonScssContent = fs.readFileSync(commonScssPath, {encoding: 'utf8'}) let varScssContent = fs.readFileSync(varScssPath, {encoding: 'utf8'}) // 遍历要修改的变量数据 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`) }) // 修改路径为绝对路径,否则会报错 let mixinsPath = path.resolve(process.cwd(), 'node_modules/hui/packages/theme/mixins/_color-helpers.scss') mixinsPath = mixinsPath.split('\\').join('https://www.jb51.net/') 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() }
下载主题
下载的主题包里有两个数据,一个是配置源文件,另一个就是编译后的主题包,包括css文件和字体文件。创建压缩包使用的是jszip,可参考: github.com/Stuk/jszip 。
主题包的目录结构如下:
-theme --fonts --index.css -config.json
实现如下:
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 }
至此,前端和后端的核心实现都已介绍完毕。
总结