梦想和现实只有一步之遥!
前言
最近公司项目需要实现动态换肤功能,脑子里面瞬间出现 css var
方案,使用简单,易上手,紧接着需要兼容 ie
的需求出现 css var
方案宣告破产!
通过调研发现当前社区主流的方案都是通过 css
预处理器来实现换肤,对于动态换肤也是上 css var
的,ε(┬┬﹏┬┬)3。于是,我有了一个大胆的想法,自己造轮子,不就是适配 ie
吗,有何难!
相关链接
- 预览地址:兼容 IE 的换肤预览
- 使用方式:Varx 的正确使用方式
- 源码仓库:themex: 换肤功能的解决方案。
TODO
- 使用简单,与
css var
尽量不冲突 - 让
ie
支持css var
功能 - 与
vue2
、vue3
、react
等主流框架打通
初步调研
说干就干,当即调研了一下需要在 ie
实现 css var
的难点:
- 对
css var
语法不支持,行内使用会被解析时过滤掉 - 存在于
style
标签内的样式虽然会存在,但是通过styleSheet
获取样式规则仍然会被过滤 - 如何实现按需更新且保证样式不会错乱
解决思路
js
中存在低版本兼容的 pollyfill
,但是 css
并不存在类似的工具,所以决定开发一个用 js
来提供对 css var
的 pollyfill
工具。
思路实现
存储中心
创建存储中心,通过使用提供的编码函数来收集所用到的 css var
变量。灵感来自于脏检测机制,通过固定的使用方式来感知 css var
的变更情况,基于插件机制暴露部分钩子函数,类似于 vue
的组件生命周期,即存储中心只为存储变量及提供相关函数,内部不具备其他功能,仅支持通过插件扩展。
class Varx {
private static _instanceCount = 0
/**
* varx instance count
*/
static get instanceCount() {
return this._instanceCount
}
private _isVarx: boolean
/**
* Indicate whether it is currently a varx instance.
*/
get isVarx() {
return this._isVarx
}
private _id: number
/**
* varx instance id, it's only.
*/
get id() {
return this._id
}
private _namespace: string
/**
* namespace for varx
*/
get namespace() {
return this._namespace
}
private _strict: boolean
/**
* strict mode for varx
*/
get strict() {
return this._strict
}
private _keyFormatFn: KeyFormatFn | null
/**
* format function fro varx key
*/
get keyFormatFn() {
return this._keyFormatFn
}
/**
* namespace and key join string
*/
get joinStr() {
return this.namespace ? '__' : ''
}
/**
* namespace processed with join string
*/
get processNamespace() {
return this.namespace + this.joinStr
}
private _state: VarState
/**
* varx var storage center
*/
get state() {
return this._state
}
private _updateLocked: boolean
private _onBeforeUpdateCallbacks: HookFunc[]
private _onUpdatedCallbacks: HookFunc[]
private _destroyLocked: boolean
private _onBeforeDestroyCallbacks: HookFunc[]
private _onDestroyedCallbacks: HookFunc[]
private _accessLocked: boolean
private _onAccessStateCallbacks: HookFunc[]
constructor(options?: VarxInitOptions) {
const { plugins = [] } = options ?? {}
this._state = Object.create(null)
this._namespace = safeValue(options?.namespace, 'string', '')
this._strict = safeValue(options?.strict, 'boolean', false)
this._keyFormatFn = safeValue(options?.keyFormatFn, 'function', null)
this._updateLocked = false
this._onBeforeUpdateCallbacks = []
this._onUpdatedCallbacks = []
this._destroyLocked = false
this._onBeforeDestroyCallbacks = []
this._onDestroyedCallbacks = []
this._accessLocked = false
this._onAccessStateCallbacks = []
this._isVarx = true
this._id = Varx._instanceCount++
const { accessState, _onUpdateState, _onDestroy } = this
const that = this
// bind this
this.accessState = function (...args: Parameters<typeof accessState>) {
accessState.apply(that, args)
}
this._onUpdateState = function (...args: Parameters<typeof _onUpdateState>) {
_onUpdateState.apply(that, args)
}
this._onDestroy = function (...args: Parameters<typeof _onDestroy>) {
_onDestroy.apply(that, args)
}
Promise.resolve().then(() => plugins.forEach((plugin) => plugin(this)))
}
accessState() {
if (this._accessLocked) return
Promise.resolve().then(() => {
invokeCallbacks(this._onAccessStateCallbacks, this)
this._accessLocked = false
})
}
/**
* Trigger callback event when access state
*
* @param fn callback function for access state
*/
onAccessState(fn: HookFunc) {
if (typeof fn !== 'function' || this._onAccessStateCallbacks.includes(fn)) return
this._onAccessStateCallbacks.push(fn)
}
/**
* destroy resources
*/
destroy() {
// on destroy locked
if (this._destroyLocked) return
// lock destroy
this._destroyLocked = true
// Start a micro task execute update queues.
// Prevent block main process.
Promise.resolve().then(() => {
const beforeDestroyCallbacks = this._onBeforeDestroyCallbacks.splice(0)
const destroyedCallbacks = this._onDestroyedCallbacks.splice(0)
// before destroy
invokeCallbacks(beforeDestroyCallbacks, this)
// destroying
invokeCallbacks([this._onDestroy])
// after destroy
invokeCallbacks(destroyedCallbacks, this)
// unlock destroy
this._destroyLocked = false
})
}
/**
* Trigger callback event when before destroy
*
* @param fn callback function for before destroy
*/
onBeforeDestroy(fn: HookFunc) {
if (typeof fn !== 'function' || this._onBeforeDestroyCallbacks.includes(fn)) return
this._onBeforeDestroyCallbacks.push(fn)
}
private _onDestroy() {
this._namespace = ''
this._state = Object.create(null)
this._keyFormatFn = null
this._onBeforeUpdateCallbacks = []
this._onUpdatedCallbacks = []
this._onBeforeDestroyCallbacks = []
this._onDestroyedCallbacks = []
}
/**
* Trigger callback event when destroyed
*
* @param fn callback function for destroyed
*/
onDestroyed(fn: HookFunc) {
if (typeof fn !== 'function' || this._onDestroyedCallbacks.includes(fn)) return
this._onDestroyedCallbacks.push(fn)
}
/**
* Remove before destroy callback
*
* @param fn callback function for destroyed
*/
removeBeforeDestroyCallback(fn: HookFunc) {
if (typeof fn !== 'function') return
remove(this._onBeforeDestroyCallbacks, fn)
}
/**
* Remove destroyed callback
*
* @param fn callback function for destroyed
*/
removeDestroyedCallback(fn: HookFunc) {
if (typeof fn !== 'function') return
remove(this._onDestroyedCallbacks, fn)
}
/**
* update state
*
* @param state state for update
* @param eventType trigger update event type
*/
updateState(state: VarStateParameter, eventType: StateEventType = 'update') {
// On update locked, start a micro task execute updateState function.
if (this._updateLocked) return
logger.assert(
isObject(state),
'The state of parameter for updateState function must be an object.'
)
// lock updateState
this._updateLocked = true
// Start a micro task execute update queues
// Prevent block main process
Promise.resolve().then(() => {
// before update
invokeCallbacks(this._onBeforeUpdateCallbacks, this)
// updating
invokeCallbacks([this._onUpdateState], state, eventType)
// after update
invokeCallbacks(this._onUpdatedCallbacks, this)
// unlock updateState
this._updateLocked = false
})
}
/**
* Trigger callback event when before update
*
* @param fn callback function for before update
*/
onBeforeUpdateState(fn: HookFunc) {
if (typeof fn !== 'function' || this._onBeforeUpdateCallbacks.includes(fn)) return
this._onBeforeUpdateCallbacks.push(fn)
}
private _onUpdateState(updateState: VarStateParameter, eventType: StateEventType = 'update') {
objectEach(updateState, (v, k) => {
// get current state value
const oldValue = this._state[k]?.value
const value = this.safeValue(v, k, this._state[k])
// only on update set to state
if (eventType === 'update') {
this._state[k] = value
}
// Only on key not in state or value not equal oldValue emit onUpdate.
if (value.value !== oldValue) {
value.onUpdate?.(value.value, k, this, eventType)
}
})
}
/**
* Trigger callback event when updated
*
* @param fn callback function for updated
*/
onUpdatedState(fn: HookFunc) {
if (typeof fn !== 'function' || this._onUpdatedCallbacks.includes(fn)) return
this._onUpdatedCallbacks.push(fn)
}
/**
* Remove before update callback
*
* @param fn callback function for before update
*/
removeBeforeUpdateStateCallback(fn: HookFunc) {
if (typeof fn !== 'function') return
remove(this._onBeforeUpdateCallbacks, fn)
}
/**
* Remove updated callback
*
* @param fn callback function for updated
*/
removeUpdatedStateCallback(fn: HookFunc) {
if (typeof fn !== 'function') return
remove(this._onUpdatedCallbacks, fn)
}
/**
* encode key
*
* @param key will be encoded key
*
* @returns encoded key
*
* @example
* ```js
* const varx = new Varx({ namespace: 'demo' })
* varx.encodeKey('bg') // demo__bg
* ```
*/
encodeKey(key: string = '') {
logger.assert(
typeof key === 'string',
'The key of parameter for encodeKey function must be a string.'
)
const _key = this._strict ? key : this.decodeKey(key)
return this.processNamespace + (this._keyFormatFn?.(_key) ?? _key)
}
/**
* decode key
*
* @param key will be decoded key
*
* @returns decoded key
*
* @example
* ```js
* const varx = new Varx({ namespace: 'demo' })
* varx.decodeKey('demo__bg') // bg
* ```
*/
decodeKey(key: string = '') {
logger.assert(
typeof key === 'string',
'The key of parameter for decodeKey function must be a string.'
)
return key.startsWith(this.processNamespace) ? key.slice(this.processNamespace.length) : key
}
/**
* encode var key
*
* @param key will be encoded var key
*
* @returns encoded var key
*
* @example
* ```js
* const varx = new Varx({ namespace: 'demo' })
* varx.encodeVarKey('bg') // --demo__bg
* ```
*/
encodeVarKey(key: string = '') {
logger.assert(
typeof key === 'string',
'The key of parameter for encodeVarKey function must be a string.'
)
return `--${this.encodeKey(this.decodeVarKey(key))}`
}
/**
* decode var key
*
* @param key will be decoded var key
*
* @returns decoded var key
*
* @example
* ```js
* const varx = new Varx({ namespace: 'demo' })
* varx.decodeVarKey('--demo__bg') // bg
* ```
*/
decodeVarKey(key: string = '') {
logger.assert(
typeof key === 'string',
'The key of parameter for decodeVarKey function must be a string.'
)
const _namespace = `--${this.processNamespace}`
return key.startsWith(_namespace) ? key.slice(_namespace.length) : key
}
/**
* encode var obj
*
* @param key will be encoded var obj
* @param emitUpdate emit update event when encoded var obj
*
* @returns encoded var obj
*
* @example
* ```js
* const varx = new Varx({ namespace: 'demo' })
* varx.encodeVarObj({ bg: 'red' }) // { '--demo__bg': 'red' }
* ```
*/
encodeVarObj(obj: VarStateParameter, emitUpdate = true) {
logger.assert(
isObject(obj),
'The obj of parameter for encodeVarObj function must be an object.'
)
const varObj: Record<string, VarStateValueType> = {}
const tempVarState: VarState = {}
objectEach(obj, (v, k) => {
const key = this.encodeVarKey(k)
const value = this.safeValue(v, key)
varObj[key] = value.value
tempVarState[key] = value
})
if (emitUpdate) {
this.updateState(tempVarState)
} else {
this._onUpdateState(tempVarState, 'update')
}
return varObj
}
/**
* decode var obj
*
* @param key will be decoded var obj
*
* @returns decoded var obj
*
* @example
* ```js
* const varx = new Varx({ namespace: 'demo' })
* varx.decodeVarObj({ '--demo__bg': 'red' }) // { bg: 'red' }
* ```
*/
decodeVarObj(varObj: VarStateParameter) {
logger.assert(
isObject(varObj),
'The varObj of parameter for decodeVarObj function must be an object.'
)
const obj: Record<string, VarStateValueType> = {}
objectEach(varObj, (v, k) => {
obj[this.decodeVarKey(k)] = this.safeValue(v, k).value
})
return obj
}
/**
* encode var tuple
*
* @param key will be encoded var tuple
* @param emitUpdate emit update event when encoded var tuple
*
* @returns encoded var tuple
*
* @example
* ```js
* const varx = new Varx({ namespace: 'demo' })
* varx.encodeVarTuple('bg', 'red') // ['--demo__bg', 'red', 'var(--demo__bg)']
* ```
*/
encodeVarTuple(key: string, value: VarStateParameter, emitUpdate = true): VarTuple {
const _key = this.encodeVarKey(key)
const _value = this.safeValue(value, _key)
const tempState = { [_key]: _value }
if (emitUpdate) {
this.updateState(tempState)
} else {
this._onUpdateState(tempState)
}
return [_key, _value.value, `var(${_key})`]
}
/**
* delete var for varx state
*
* @param key needs delete var key
* @param emitUpdate whether emit update event when deleted
*/
deleteVar(key: string | string[], emitUpdate = true) {
if (typeof key === 'string') key = [key]
logger.assert(
Array.isArray(key),
'The key of parameter for deleteVar function must be a string or an array.'
)
const state: VarState = {}
for (let k of key) {
const _var = this.getVar(k)
if (isUndef(_var)) continue
if (!(k in this._state)) k = this.encodeVarKey(k)
delete this._state[k]
state[k] = _var!
}
if (emitUpdate) {
this.updateState(state, 'delete')
} else {
this._onUpdateState(state, 'delete')
}
}
/**
* get state var by key
*
* ***Calling the function will trigger the access event.***
*
* @param key key for varx state
*
* @returns varx state var
*/
getVar(key: string = '') {
logger.assert(
typeof key === 'string',
'The key of parameter for getVar function must be a string.'
)
const _var = this.state[this._strict ? key : this.encodeVarKey(key)]
if (isUndef(_var)) return
this.accessState()
return deepCopy(_var)
}
/**
* get state var value by key
*
* ***Calling the function will trigger the access event.***
*
* @param key key for varx state
*
* @returns varx state var value
*/
getVarValue(key: string = '') {
logger.assert(
typeof key === 'string',
'The key of parameter for getVarValue function must be a string.'
)
return this.getVar(key)?.value ?? ''
}
/**
* simple function for get var
*/
g(...args: Parameters<Varx['getVar']>) {
return this.getVar.apply(this, args)
}
/**
* simple function for getVarValue
*/
gv(...args: Parameters<Varx['getVarValue']>) {
return this.getVarValue.apply(this, args)
}
/**
* get safe var value
*
* @param value var value
* @param oldValue var old value
*
* @returns a safe var value
*/
safeValue(value: VarStateValueParameter, key?: string, oldValue?: VarStateValue): VarStateValue {
if (!isObject(value)) {
value = { value, key }
} else if (value !== null && typeof key === 'string' && key.length > 0) {
value.key = key
}
if (isUndef(value.value)) {
value.value = ''
} else if (isObject(value.value)) {
value.value = String(value.value)
}
if (typeof value.key !== 'string') {
value.key = isUndef(value.key) ? '' : String(value.key)
}
if ('onUpdate' in value && typeof value.onUpdate !== 'function') {
logger.warn(
'The onUpdate of parameter for safeValue must be a function. Otherwise it will be deleted.'
)
delete value.onUpdate
}
return Object.assign({}, oldValue, value)
}
}
低浏览器插件(替代扫描器)
提供在变量访问、更新时执行扫描任务,扫描当前样式规则,通过解析器解析并替换无效变量。
function lowBrowser(config?: LowBrowserConfig): VarxPlugin {
return (varx) => {
const lowBrowserMeta: LowBrowserMeta = {
coverStyles: [],
scanLink: config?.scanLink ?? false,
parser: null,
onlyLowBrowser: config?.onlyLowBrowser ?? false
}
// 为 varx 实例对象绑定当前插件元信息
varx.lowBrowserMeta = lowBrowserMeta
// 处理 css link 的覆盖样式标签
function flushCoverStyles() {
// 倒序循环避免移除元素导致索引错乱
for (let i = lowBrowserMeta.coverStyles.length - 1; i >= 0; i--) {
const element = lowBrowserMeta.coverStyles[i]
const originElement = element.__coverInfo.origin
// 如果来源 css link 的父级元素不存在,则认为标签已被移除,同步删除覆盖样式标签
// 否则验证覆盖样式标签是否与来源标签为兄弟关系,验证失败则移动标签至来源标签的下一级
// 用于避免不同样式标签内的同类名样式覆盖不生效
if (originElement.parentElement) {
if (originElement.nextElementSibling !== element) {
insertAfter(originElement, element)
}
} else {
lowBrowserMeta.coverStyles.splice(i, 1)
element.parentElement?.removeChild(element)
}
}
}
// 为 css link 标签打补丁
function patchCssLink(el: HTMLLinkElement) {
// 如果已经存在元素对应的覆盖样式则跳过处理
if (lowBrowserMeta.coverStyles.findIndex((style) => style.__coverInfo.origin === el) > -1)
return
const parser = lowBrowserMeta.parser!
// 通过解析器解析样式内容并生成新的 style 标签进行追加
parser.parseValidStyleText(el).then((text) => {
// 判断是否有用到 css var
if (!parser.hasValidCssVar(text)) return
const styleElement = document.createElement('style') as CoverStyleElement
// 为 style 标签记录覆盖信息
styleElement.__coverInfo = {
origin: el,
rawText: text
}
styleElement.textContent = parser.replaceValidCssVar(text, varx.state)
insertAfter(el, styleElement)
lowBrowserMeta.coverStyles.push(styleElement)
})
}
// 为 style 标签打补丁
function patchStyle(el: HTMLStyleElement & Partial<CoverInfo>) {
const parser = lowBrowserMeta.parser!
// 通过解析器解析 style 标签样式并替换 css var
parser.parseValidStyleText(el).then((text) => {
const rawText = el.__coverInfo ? el.__coverInfo.rawText : text
if (!parser.hasValidCssVar(rawText)) return
if (!el.__coverInfo) {
el.__coverInfo = {
origin: el,
rawText
}
}
const newText = parser.replaceValidCssVar(rawText, varx.state)
// 如果新的样式与旧的样式相同时直接跳过赋值
if (newText !== text) {
el.textContent = newText
}
})
}
// 执行扫描任务
function scan() {
// 如果仅在低版本浏览器(ie)中使用时,则会检测当前浏览器是否支持 css var ,若支持则结束任务,否则继续
if (lowBrowserMeta.onlyLowBrowser && checkSupportNativeCssVar()) return
// 创建解析器
if (!lowBrowserMeta.parser) {
lowBrowserMeta.parser = new Parser(varx.processNamespace, config?.parserHttpFn)
}
// 先处理旧的 css link 的覆盖样式
flushCoverStyles()
// 扫描获取有效的样式标签(style、link)
const styleElements = scanValidStyleElements()
// 开启微任务防止阻塞主线程
Promise.resolve().then(() => {
styleElements.forEach((element) => {
// 为对应的标签打补丁
isCssLink(element) ? patchCssLink(element) : patchStyle(element)
})
})
}
// 扫描有效的样式标签
function scanValidStyleElements() {
const elements: (HTMLStyleElement | HTMLLinkElement)[] = [].concat(
Array.from(document.getElementsByTagName('style') as any),
lowBrowserMeta.scanLink ? Array.from(document.getElementsByTagName('link') as any) : []
)
return elements
}
// 为 varx 注册事件回调
varx.onAccessState(() => {
scan()
})
varx.onUpdatedState(() => {
scan()
})
varx.onDestroyed(() => {
lowBrowserMeta.coverStyles.splice(0).forEach((element) => {
removeElement(element, element.parentElement ?? document.head)
})
delete varx.lowBrowserMeta
})
}
}
解析器(低浏览器插件附属)
创建解析器,对扫描器扫描到的样式标签(style
、link
)内容进行解析,提取出使用了 css var
的样式规则,并根据存储中心的样式信息进行替换。
class Parser {
private static _baseHttpFn: HttpFnParameter = null
/**
* 获取 css link 文件内容的异步函数
*
* @description
* 默认会采用 XMLHttpRequest 实例对象进行获取。
*
* ***若存在需要认证域名或 token 的情况则推荐设置该函数,接收 href 地址,并返回文件内容。***
*
* ***优先级:httpFn > baseHttpFn > xhr***
*
* @default null
*
* @example
* ```js
* Parser.setBaseHttpFn((href) => data)
* ```
*/
static get baseHttpFn() {
return this._baseHttpFn
}
private static _linkMap: LinkMap = {}
/**
* css link 缓存集合
*
* @description 以 css link href 为缓存 key ,对解析到的结果进行缓存保留,避免二次解析。
*
* @default {}
*
* @example
* ```js
* Parser.setLinkMap('./xxx.css', '.xxx { xxx }')
* ```
*/
static get linkMap() {
return Object.freeze(deepCopy(this._linkMap))
}
private _namespace = ''
/**
* varx 实例的命名空间
*
* @description 对解析功能提供帮助。
*/
get namespace() {
return this._namespace
}
private _httpFn: HttpFnParameter = null
/**
* 获取 css link 文件内容的异步函数
*
* @description
* 默认会采用 XMLHttpRequest 实例对象或 baseHttpFn (若存在)进行获取。
*
* ***若存在需要认证域名或 token 的情况则推荐设置该函数,接收 href 地址,并返回文件内容。***
*
* ***优先级:httpFn > baseHttpFn > xhr***
*
* @default null
*
* @example
* ```js
* Parser.setHttpFn((href) => data)
* ```
*/
get httpFn() {
return this._httpFn ?? Parser.baseHttpFn
}
/**
* @param namespace varx 命名空间
* @param httpFn 获取 css link 文件内容的异步函数
*/
constructor(namespace: string, httpFn?: HttpFnParameter) {
this._namespace = safeValue(namespace, 'string', '')
this.setHttpFn(httpFn)
}
/**
* 设置缓存 link href 数据
*
* @param key 需要缓存的 link href
* @param value 需要缓存的 link href 数据
*/
static setLinkMap(key: string, value: string) {
if ([key, value].some((v) => typeof v !== 'string')) return
this._linkMap[key] = value
}
/**
* 设置解析器的基异步函数
*
* @param fn 获取 css link 文件内容的异步函数
*/
static setBaseHttpFn(fn: HttpFnParameter = null) {
if (typeof fn !== 'function') {
this._baseHttpFn = null
} else {
this._baseHttpFn = (href) => Promise.resolve().then(() => fn(href))
}
}
/**
* 设置解析器的异步函数
*
* @param fn 获取 css link 文件内容的异步函数
*/
setHttpFn(fn: HttpFnParameter = null) {
if (typeof fn !== 'function') {
this._httpFn = null
} else {
this._httpFn = (href) => Promise.resolve().then(() => fn(href))
}
}
/**
* 解析 css var key
*
* @param cssVar 需要解析的 css var
*
* @returns 解析后的 css var key
*
* @example
* ```js
* const parser = new Parser('demo')
* parser.parseVarKey('var(--demo__bg)') // --demo__bg
* parser.parseVarKey('var(--demo__bg, red)') // --demo__bg
* ```
*/
parseVarKey(cssVar = '') {
const frontVar = cssVar.split(')')[0] ?? ''
return frontVar.replace(/(var\(|\s|\r)/g, '').split(',')[0] ?? ''
}
/**
* 解析有效的样式
*
* @param el 需要解析的样式标签
*
* @returns 解析后的样式
*
* @example
* ```js
* (async function() {
* const parser = new Parser()
* const styleText = await parser.parseValidStyleText(document.getElementsByTagName('style')[0])
* const linkText = await parser.parseValidStyleText(document.getElementsByTagName('link')[0])
*
* console.log(styleText) // .xxx { xxx } ...
* console.log(linkText) // .xxx { xxx } ...
* })()
* ```
*/
async parseValidStyleText(el: HTMLStyleElement | HTMLLinkElement) {
let styleText = ''
if (isCssLink(el)) {
if (el.href in Parser.linkMap) {
styleText = Parser.linkMap[el.href]
} else {
const { error, data, message } = await this.getFile(el.href)
if (error) {
// 若当前接口调用失败,但其他接口成功后尝试从缓存中重新获取
styleText = await Promise.resolve(Parser.linkMap[el.href] ?? '')
!styleText && logger.error(`href 地址:${el.href} 解析错误!原因:${message}`)
} else {
styleText = data
Parser.setLinkMap(el.href, data)
}
}
} else if (isStyle(el)) {
styleText = el.textContent ?? ''
}
return styleText
}
/**
* 获取 css link 文件内容
*
* @description 异步优先级:httpFn > baseHttpFn > xhr
*
* @param href css link href
*
* @returns 获取 css link 的文件内容
*
* @example
* ```js
* (async function() {
* const parser = new Parser()
* await parser.getFile('./xxx.css') // .xxx { xxx } ...
* })()
* ```
*/
async getFile(href: string) {
return new Promise<{ error: boolean; data: any; message: string }>((resolve) => {
if (!href) return resolve({ error: true, data: '', message: '参数 href 必填!' })
// 如果设置了 http 函数,则采用设置函数获取文件内容
if (typeof this.httpFn === 'function') {
return this.httpFn(href)
.then((data) =>
resolve({
error: false,
data: typeof data === 'string' ? data : '',
message: ''
})
)
.catch((e) =>
resolve({
error: true,
data: null,
message: e?.message ?? e.toString()
})
)
}
const xhr = new XMLHttpRequest()
xhr.onreadystatechange = () => {
if (xhr.readyState === 4) {
const error = xhr.status !== 200
resolve({
error,
data: error ? null : xhr.response,
message: error ? xhr.statusText : ''
})
}
}
xhr.open('GET', href)
xhr.send()
})
}
/**
* 替换有效的 css var 样式
*
* @param cssText css 样式
* @param varMap varx 的 varMap 集合
*
* @returns 替换 css var 后的样式
*/
replaceValidCssVar(cssText: string = '', varState: VarState = {}) {
return cssText.replace(
new RegExp(`var\\(--${this._namespace}[^)]+\\)`, 'g'),
(cssVar) => String(varState[this.parseVarKey(cssVar)]?.value) ?? ''
)
}
/**
* 判断是否包含有效的 css var
*
* @param text 需要验证的样式
*
* @returns 当前样式是否包含有效的 css var
*
* @example
* ```js
* const parser = new Parser('demo')
* parser.hasValidCssVar('.xxx { background: var(--background); }') // false
* parser.hasValidCssVar('.xxx { background: var(--demo__background); }') // true
* ```
*/
hasValidCssVar(text: string = '') {
return text.includes(`var(--${this._namespace}`)
}
}
InVue 插件(Vue 集成)
由于作者主要使用 vue
相关技术栈开发,所以优先考虑了 vue
的功能集成。
通过 vue-demi
抹平 vue2
和 vue3
之间的差异,提供使用一致性,为存储中心实例增加响应式属性及获取响应式变量的函数,解决了行内不能使用的问题,应尽可能的采用函数来获取响应式的变量,避免造成对原存储中心数据的冲击以及提升代码的维护性。
function createRState(): RVarx['_rState'] {
return isVue2 ? Vue2.observable({ value: {} }) : ref({})
}
function setRState(rState: RVarx['_rState'], state: VarState = {}) {
return isVue2 ? Vue2.set(rState, 'value', state) : (rState.value = state)
}
function inVue(varx: VarxCtor | RVarx): RVarx {
logger.assert(varx?.isVarx, 'The varx must be a varx instance.')
if (varx._rState !== undefined) {
return varx as RVarx
}
const rState = createRState()
varx._rState = rState
varx.gr = gr.bind(varx)
varx.grv = grv.bind(varx)
varx.onUpdatedState(() => {
setRState(rState, deepCopy(varx.state))
})
varx.onDestroyed(() => {
delete varx._rState
delete varx.gr
delete varx.grv
})
function gr(key: string) {
return rState.value[varx.g(key)?.key!]
}
function grv(key: string) {
return rState.value[varx.g(key)?.key!]?.value ?? ''
}
return varx as RVarx
}
生成根样式插件
当存储变量被改变时自动生成新的 :root
样式并挂载在 head
底部,为支持 css var
的浏览器提供样式注入。
function generateRootStyle(): VarxPlugin {
return (varx) => {
if (!checkSupportNativeCssVar()) return
const rootStyleMeta: RootStyleMeta = {
id: `varx-root-style__${varx.id}`,
el: null
}
varx.rootStyleMeta = rootStyleMeta
rootStyleMeta.el = getElement(rootStyleMeta.id, 'style') as HTMLStyleElement
function genRootStyle() {
const vars: string[] = []
objectEach(varx.state, (v, k) => {
vars.push(`${k}:${v.value};`)
})
setStyleText(rootStyleMeta.id, `:root{${vars.join('')}}`)
}
genRootStyle()
varx.onUpdatedState(() => {
genRootStyle()
})
varx.onDestroyed(() => {
removeElement(rootStyleMeta.id)
rootStyleMeta.el = null
delete varx.rootStyleMeta
})
}
}
目前该插件已发布与 npm
,可以通过 npm
安装,原创不易,希望能留下各位看官的痕迹 ❥,Thanks♪(・ω・)ノ!