There is only one step between dream and reality!

preface

Recently the company’s project needs to achieve dynamic skin function, the brain inside the instant CSS VAR scheme, simple to use, easy to use, followed by the need to compatible WITH IE CSS VAR scheme declared bankruptcy!

Through the investigation found that the current community of mainstream programs are through the CSS preprocessor to achieve skin, for dynamic skin is CSS VAR, ε(┬┬﹏┬┬)3. So, I had a bold idea, their own wheel, is not adapted to IE, there is no difficult!

A link to the

  • Preview address: compatible with IE skin preview
  • Usage: The correct way to use Varx
  • Themex: a skin function solution.

TODO

  • Simple to use, withcss varTry not to conflict
  • iesupportcss varfunction
  • vue2,vue3,reactWait for the mainstream framework to break through

Preliminary investigations,

I immediately investigated the difficulties in implementing CSS VAR in IE:

  • rightcss varSyntax not supported, inline use will be filtered out when parsing
  • Exists in thestyleThe style inside the tag will exist, but will passstyleSheetFetching style rules will still be filtered
  • How do you update on demand without messing up your style

solution

Pollyfill exists in JS with low version compatibility, but CSS does not have a similar tool, so we decided to develop a POLlyfill tool that provides CSS var with JS.

Train of thought to realize

Storage center

Create a storage center to collect the CSS VAR variables used by using the provided encoding functions. Inspired by the dirty detection mechanism, it senses changes of CSS var in a fixed way. Based on the plug-in mechanism, it exposes some hook functions, similar to the component life cycle of VUE. That is, the storage center only stores variables and provides related functions.

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 (typeoffn ! = ='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 (typeoffn ! = ='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 (typeoffn ! = ='function' || this._onDestroyedCallbacks.includes(fn)) return

    this._onDestroyedCallbacks.push(fn)
  }

  /**
   * Remove before destroy callback
   *
   * @param fn callback function for destroyed
   */
  removeBeforeDestroyCallback(fn: HookFunc) {
    if (typeoffn ! = ='function') return

    remove(this._onBeforeDestroyCallbacks, fn)
  }

  /**
   * Remove destroyed callback
   *
   * @param fn callback function for destroyed
   */
  removeDestroyedCallback(fn: HookFunc) {
    if (typeoffn ! = ='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 (typeoffn ! = ='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]? .valueconst 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 (typeoffn ! = ='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 (typeoffn ! = ='function') return

    remove(this._onBeforeUpdateCallbacks, fn)
  }

  /**
   * Remove updated callback
   *
   * @param fn callback function for updated
   */
  removeUpdatedStateCallback(fn: HookFunc) {
    if (typeoffn ! = ='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 `--The ${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 = `--The ${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(! (kin 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 (typeofvalue.key ! = ='string') {
      value.key = isUndef(value.key) ? ' ' : String(value.key)
    }
    if ('onUpdate' in value && typeofvalue.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)
  }
}
Copy the code

Low browser plug-in (alternative scanner)

Provides the ability to perform scanning tasks when variables are accessed, updated, scan current style rules, and parse and replace invalid variables with a parser.

function lowBrowser(config? : LowBrowserConfig) :VarxPlugin {
  return (varx) = > {
    const lowBrowserMeta: LowBrowserMeta = {
      coverStyles: [].scanLink: config? .scanLink ??false.parser: null.onlyLowBrowser: config? .onlyLowBrowser ??false
    }

    // Bind the current plug-in meta information to the varx instance object
    varx.lowBrowserMeta = lowBrowserMeta

    // Handle the CSS link's overwrite style tag
    function flushCoverStyles() {
      // Reverse loop to avoid index corruption caused by removing elements
      for (let i = lowBrowserMeta.coverStyles.length - 1; i >= 0; i--) {
        const element = lowBrowserMeta.coverStyles[i]
        const originElement = element.__coverInfo.origin
        
        // If the parent element of the source CSS link does not exist, the label is considered to have been removed, and the overwrite style label is removed synchronously
        // Otherwise verify that the overwrite style label is sibling to the source label. If verification fails, move the label to the next level of the source label
        // Used to avoid overwriting the same name style in different style tags
        if (originElement.parentElement) {
          if(originElement.nextElementSibling ! == element) { insertAfter(originElement, element) } }else {
          lowBrowserMeta.coverStyles.splice(i, 1) element.parentElement? .removeChild(element) } } }// Patch the CSS link label
    function patchCssLink(el: HTMLLinkElement) {
      // Skip processing if an override style already exists for the element
      if (lowBrowserMeta.coverStyles.findIndex((style) = > style.__coverInfo.origin === el) > -1)
        return

      const parser = lowBrowserMeta.parser!

      // The parser parses the style content and generates a new style tag to append
      parser.parseValidStyleText(el).then((text) = > {
        // Check whether CSS var is useful
        if(! parser.hasValidCssVar(text))return

        const styleElement = document.createElement('style') as CoverStyleElement

        // Record override information for the style tag
        styleElement.__coverInfo = {
          origin: el,
          rawText: text
        }
        styleElement.textContent = parser.replaceValidCssVar(text, varx.state)

        insertAfter(el, styleElement)
        lowBrowserMeta.coverStyles.push(styleElement)
      })
    }

    // Patch the style tag
    function patchStyle(el: HTMLStyleElement & Partial<CoverInfo>) {
      const parser = lowBrowserMeta.parser!

      // Use the parser to parse the style tag style and replace the 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 the new style is identical to the old style, the assignment is skipped
        if(newText ! == text) { el.textContent = newText } }) }// Perform a scan task
    function scan() {
      // If it is used only in Internet Explorer, the system checks whether the current browser supports CSS var. If it does, the task ends. Otherwise, the system continues
      if (lowBrowserMeta.onlyLowBrowser && checkSupportNativeCssVar()) return

      // Create a parser
      if(! lowBrowserMeta.parser) { lowBrowserMeta.parser =newParser(varx.processNamespace, config? .parserHttpFn) }// Start with the old CSS link overlay style
      flushCoverStyles()

      // Scan for valid style tags (style, link)
      const styleElements = scanValidStyleElements()

      // Enable microtasks to prevent blocking the main thread
      Promise.resolve().then(() = > {
        styleElements.forEach((element) = > {
          // Patch corresponding labels
          isCssLink(element) ? patchCssLink(element) : patchStyle(element)
        })
      })
    }

    // Scan for valid style tags
    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
    }

    // Register event callback for VARx
    varx.onAccessState(() = > {
      scan()
    })
    varx.onUpdatedState(() = > {
      scan()
    })
    varx.onDestroyed(() = > {
      lowBrowserMeta.coverStyles.splice(0).forEach((element) = > {
        removeElement(element, element.parentElement ?? document.head)
      })

      delete varx.lowBrowserMeta
    })
  }
}
Copy the code

Parser (low browser plug-in attachment)

Create a parser to parse the content of style tags (style, link) scanned by the scanner, extract the style rules using CSS VAR, and replace them according to the style information of the storage center.

class Parser {
  private static _baseHttpFn: HttpFnParameter = null
  /** * The asynchronous function that gets the content of the CSS link file **@description* The default is the XMLHttpRequest instance object. ** *** If domain name or token authentication is required, you are advised to set this function to receive the href address and return the file content. *** ** *** Priority: httpFn > baseHttpFn > XHR *** **@default null
   *
   * @example
   * ```js
   * Parser.setBaseHttpFn((href) => data)
   * ```
   */
  static get baseHttpFn() {
    return this._baseHttpFn
  }

  private static _linkMap: LinkMap = {}
  /** * CSS link cache collection **@description CSS Link href is used as the cache key to retain the parsed result to avoid secondary parsing. * *@default {}
   *
   * @example
   * ```js
   * Parser.setLinkMap('./xxx.css', '.xxx { xxx }')
   * ```
   */
  static get linkMap() {
    return Object.freeze(deepCopy(this._linkMap))
  }

  private _namespace = ' '
  /** * the namespace of varx instance **@description Help with parsing capabilities. * /
  get namespace() {
    return this._namespace
  }

  private _httpFn: HttpFnParameter = null
  /** * The asynchronous function that gets the content of the CSS link file **@description* The default is the XMLHttpRequest instance object or baseHttpFn (if present). ** *** If domain name or token authentication is required, you are advised to set this function to receive the href address and return the file content. *** ** *** Priority: httpFn > baseHttpFn > XHR *** **@default null
   *
   * @example
   * ```js
   * Parser.setHttpFn((href) => data)
   * ```
   */
  get httpFn() {
    return this._httpFn ?? Parser.baseHttpFn
  }

  / * * *@param Namespace varx Specifies the namespace *@param HttpFn An asynchronous function that gets the content of a CSS link file */
  constructor(namespace: string, httpFn? : HttpFnParameter) {
    this._namespace = safeValue(namespace, 'string', ") this.sethttpfn (httpFn)} /** * set the link href data to cache ** @param key link href * @param value Link href data to cache */  static setLinkMap(key: string, value: string) {if ([key, value].some((v) = > typeofv ! = ='string')) return

    this._linkMap[key] = value
  }

  /** * Sets the parser's base asynchronous function **@param Fn The asynchronous function */ that gets the content of the CSS link file
  static setBaseHttpFn(fn: HttpFnParameter = null) {
    if (typeoffn ! = ='function') {
      this._baseHttpFn = null
    } else {
      this._baseHttpFn = (href) = > Promise.resolve().then(() = > fn(href))
    }
  }

  /** * sets the parser's asynchronous function **@param Fn The asynchronous function */ that gets the content of the CSS link file
  setHttpFn(fn: HttpFnParameter = null) {
    if (typeoffn ! = ='function') {
      this._httpFn = null
    } else {
      this._httpFn = (href) = > Promise.resolve().then(() = > fn(href))
    }
  }

  /** * Parse CSS var key **@param CssVar Specifies the CSS var * * to be parsed@returns Parsed 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]????' '
  }

  /** * parse the valid style **@param El needs to parse the style tag * *@returns Parsed style * *@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) {
          // If the current interface fails to be invoked, but other interfaces succeed, try to retrieve it from the cache again
          styleText = await Promise.resolve(Parser.linkMap[el.href] ?? ' ')
          !styleText && logger.error(` href address:${el.href}Parsing error! The reason:${message}`)}else {
          styleText = data
          Parser.setLinkMap(el.href, data)
        }
      }
    } else if (isStyle(el)) {
      styleText = el.textContent ?? ' '
    }

    return styleText
  }

  /** * Get the CSS link file content **@description Asynchronous priority: httpFn > baseHttpFn > XHR * *@param href css link href
   *
   * @returns Get the file content of the CSS link@example* ```js * (async function() { * const parser = new Parser() * await parser.getFile('./xxx.css') // .xxx { xxx } ... *})() * ' '*/
  async getFile(href: string) {
    return new PromiseThe < {error: boolean; data: any; message: string} > ((resolve) = > {
      if(! href)return resolve({ error: true.data: ' '.message: 'Href mandatory! ' })

      // If the HTTP function is set, the set function is used to get the file content
      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) {
          consterror = xhr.status ! = =200

          resolve({
            error,
            data: error ? null : xhr.response,
            message: error ? xhr.statusText : ' '
          })
        }
      }

      xhr.open('GET', href)
      xhr.send()
    })
  }

  /** * Replace valid CSS var styles **@param CssText CSS style *@param VarMap The varMap set of varx * *@returns Replace the style */ after CSS var
  replaceValidCssVar(cssText: string = ' ', varState: VarState = {}) {
    return cssText.replace(
      new RegExp(`var\\(--The ${this._namespace}` [^)] + \ \).'g'),
      (cssVar) = > String(varState[this.parseVarKey(cssVar)]? .value) ??' ')}/** * Check whether the CSS var ** is valid@param Text Specifies the style to validate@returns Whether the current style contains valid 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(--The ${this._namespace}`)}}Copy the code

InVue plug-in (Vue integration)

Since the author mainly uses VUE related technology stack, the functional integration of VUE is given priority.

Vue-demi smoothed the difference between VUe2 and VUe3, provided consistency of use, added responsive attributes and functions to get responsive variables for the storage center instance, solved the problem that the line could not be used, so functions should be used as far as possible to get responsive variables. Avoid data impact on the original storage center and improve code maintainability.

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) {
    returnrState.value[varx.g(key)?.key!] ? .value ??' '
  }

  return varx as RVarx
}
Copy the code

Generate the root style plug-in

A new :root style is automatically generated when stored variables are changed and mounted at the bottom of the head to provide style injection for browsers that support 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
    })
  }
}
Copy the code

At present, the plug-in has been published with NPM, can be installed through NPM, the original is not easy, hope to leave you see the trace, Thanks to (· ω ·) Blue!