了解css-tree

CSSTree is a tool set for CSS: fast detailed parser (CSS → AST), walker (AST traversal), generator (AST → CSS) and lexer (validation and matching) based on specs and browser implementations. The main goal is to be efficient and W3C spec compliant, with focus on CSS analyzing and source-to-source transforming tasks.

为什么使用它?

最近项目上有个需求:提供一个界面化的css代码编辑器,管理员可以输入css代码配置修改表单/组件的一些默认样式,从而针对不同业务需求实现不同的界面呈现效果。

为了避免样式的全局污染,需要对配置的样式添加默认前置导航,通过属性选择器约束样式的生效范围。如果通过string的一些方法粗暴的对样式进行替换or正则匹配,有很大的局限性,同时也不符合规范。

思考再三,找到了css -> ast -> css.方案,并通过css-treeAPI实现。

根据产品业务的区别,如果只是想实现样式自定义,不需要通过代码配置达到更强的交互,那么可以内置class,编写默认的一些style,用户通过在界面选择主题来实现该效果。参考百度amis

源码实现

给组件唯一标识

可以给组件的wrapper-dom添加具有唯一标志性的属性,用于约束自定义样式生效的区间范围.

<!-- 给组件 wrapper 绑定具有唯一标识的 id -->
<template>
  <div class="form-item_container" :id="field.__vModel__">
    <RenderWrapper v-bind="$attrs" v-on="$listeners" />
  </div>
</template>

helper实现

编写用于操作css-code的工具函数,给自定义的css-code添加内置的属性选择器前缀。

import { clone, generate, parse, walk } from 'css-tree';

export const styleHelper = {
  addClassPrefix(prefix: string, context: string) {
    const orig = parse(context); // css -> ast
    const ast = clone(orig); // clone ast node.

    walk(ast, (node, item) => {
      // 仅对选择器的首个 selector 节点添加前缀  eg.  .form .item {} -> .prefix .form .item {}
      if (!item?.prev) {
        if (node.type === 'TypeSelector') {
          // eg. input div li...
          node.name = `.${prefix} ${node.name}`;
        }

        if (node.type === 'ClassSelector') {
          // eg. .el-input .customCls...
          node.name = `${prefix} .${node.name}`;
        }
        // other process...
      }
    });

    return generate(ast); // ast -> css
  },

  loadCss(id: string, cssContent: string) {
    const styleEle = document.getElementById(`style_${id}`);

    styleEle?.remove(); // 热更新,避免重复性追加 style 节点.
    // or if (styleEle) return;

    const style = document.createElement('style');
    const head = document.getElementsByTagName('head')[0];

    style.id = `style_${id}`;

    const styleCode = styleHelper.addClassPrefix(`form-item_container[id=${id}]`, cssContent);

    style.appendChild(document.createTextNode(styleCode));
    head.appendChild(style);
  },
};

config-editor实现

用于配置css的编辑器组件.偷了个懒,如果你需要更友好的实现,可以添加css语法提示

<template>
  <el-dialog
    title="自定义样式"
    class="codemirror_dialog"
    :destroy-on-close="true"
    :close-on-click-modal="false"
    :visible.sync="customStyleVisible">
    <VueCodemirror v-model="innerValue" :options="options" />
    <template v-slot:footer>
      <el-button @click="customStyleVisible = false">取 消</el-button>
      <el-button type="primary" @click="onConfirmHandle">确 定</el-button>
    </template>
  </el-dialog>
</template>

<script lang="ts">
  import { Component, ModelSync, PropSync, Vue, Watch } from 'vue-property-decorator';

  @Component({
    name: 'StyleConfig',
    components: {
      VueCodemirror: async () => {
        const { codemirror } = await import(/* webpackChunkName: "codeMirror" */ 'vue-codemirror');
        return codemirror;
      },
    },
  })
  export default class StyleConfig extends Vue {
    @PropSync('visible')
    public customStyleVisible!: boolean;

    @ModelSync('value', 'input')
    public customStyle!: string;

    public innerValue = '';

    public options = {
      tabSize: 2,
      mode: 'text/css',
      theme: 'default',
      styleActiveLine: true,
      matchBrackets: true,
      placeholder:
        '请输入组件自定义css样式... eg.\n\n.el-input { color: #f00; } \n\n input { border: none; }',
    };

    @Watch('value', { immediate: true })
    public setContentValue(v: string) {
      this.innerValue = v;
    }

    public async created() {
      await this.initCodemirror();
    }

    public async initCodemirror() {
      await import(/* webpackChunkName: "codeMirror" */ 'codemirror/mode/css/css.js');
      await import(/* webpackChunkName: "codeMirror" */ 'codemirror/addon/display/placeholder.js');
      await import(
        /* webpackChunkName: "codeMirror" */ 'codemirror/addon/selection/active-line.js'
      );
      await import(/* webpackChunkName: "codeMirror" */ 'codemirror/addon/edit/matchbrackets.js');
      await import(/* webpackChunkName: "codeMirror" */ 'codemirror/lib/codemirror.css');
    }

    public onConfirmHandle() {
      this.customStyleVisible = false;
      this.customStyle = this.innerValue;
    }
  }
</script>

parser实现

在表单parser组件created钩子loadCss,将自定义的样式代码加载到html.

import { styleHelper } from './util/index';

public created() {
  const { code, css } = field.__config__;

  styleHelper.loadCss(code, css);
}