低代码海报平台的组件库如何设计?

lxf2023-03-12 09:54:01

本文为稀土AdminJS技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!

前面两篇文章如何设计一款营销低代码可视化海报平台、低代码海报平台的编辑器难点剖析分别从海报平台的整体架构和编辑器难点两部分对乔巴海报搭建平台做了讲解,相信看完这两篇,大家对于平台的主体功能和实现也有了大概的了解。今天这一篇,我们会深入到组件库环节,相比传统的element-uiant-design,低代码平台的组件库往往受众很小(一般都是为自身的平台服务),设计时考虑的点也完全不同。

本篇文章会从组件库初始化、文字组件设计、图片组件设计、素材组件设计、组件库打包、组件库发布几个小节依次展开说明。

组件库初始化

首先是组件库的初始化,由于项目本身是基于vue的,所以这里直接使用vue create xxx来创建项目即可。

在我之前的文章从 Element UI 源码的构建流程来看前端 UI 库设计中有提过ElementUI在使用时有两种引入方式

  • 全局引入
import Vue from 'vue';
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
import App from './App.vue';

Vue.use(ElementUI);

new Vue({
  el: '#app',
  render: h => h(App)
});
  • 按需引入
import Vue from 'vue';
import { Pagination, Dropdown } from 'element-ui';

import App from './App.vue';

Vue.use(Pagination)
Vue.use(Dropdown)

new Vue({
  el: '#app',
  render: h => h(App)
});

这两种引入方式都用到了Vue.use,这便是Vue的插件系统。

来简单了解一下:

低代码海报平台的组件库如何设计?

通过vue官网,我们知道插件通常用来为 Vue 添加全局功能。插件的功能范围没有严格的限制——一般有下面几种:

  1. 添加全局方法或者 property。
  2. 添加全局资源:指令/过滤器/过渡等。
  3. 通过全局混入来添加一些组件选项。
  4. 添加 Vue 实例方法,通过把它们添加到 Vue.prototype 上实现。
  5. 一个库,提供自己的 API,同时提供上面提到的一个或多个功能。

Vue.js 的插件应该暴露一个 install 方法。这个方法的第一个参数是 Vue 构造器,第二个参数是一个可选的选项对象

了解了插件系统,结合我们组件库的两种引入方式,我们来对最开始的项目进行改造。

首先是按需引入,也就是单个组件导入并且作为插件使用:

import { CImage } from choba-lego-components
app.use(CImage)

这种需要每个组件新建一个文件夹,并且创建一个单独的index.ts文件。

import { App } from "vue";
import CImage from "./CImage.vue";

CImage.install = (app: App) => {
  app.component(CImage.name, CImage);
};

export default CImage;

组件设计为了插件,并拥有install方法。

最终还需要在全局入口文件导出:

import CImage from "./components/CImage";
export { CImage };

其次是全局引入这种方式,这种需要在全局入口文件index.ts中将所有组件导入,放到一个数组中,同样创建install方法,循环调用app.component方法,最后默认导出install函数:

import { App } from "vue";
import CText from "./components/CText";
import CImage from "./components/CImage";
import CShape from "./components/CShape";

const components = [CText, CImage, CShape];

const install = (app: App) => {
  components.forEach((component) => {
    app.component(component.name, component);
  });
};


export default {
  install,
};

改造完的项目目录结构为:

低代码海报平台的组件库如何设计?

编写组件代码

目前乐高平台的组件库还比较轻量,只有文字组件、图片组件和素材组件,相比一些成熟的组件库,如ant design,还会划分为通用组件、布局组件、导航组件、数据录入组件、数据展示组件、反馈型组件和其他。又或者像element ui,组件划分为基础组件、表单组件、数据呈现组件、通知类组件、导航类组件和其他。

就目前来说,乐高平台使用的配套组件库更偏向于纯展示类(这与海报的展现形式有关),会带一些轻交互。

所以组件设计维度其实是很简单的,更多处理是在样式维度。

通过上一篇文章低代码海报平台的编辑器难点剖析我们知道文字、图片、素材组件有很多通用属性:actionTypeurlwidthheightpaddingpositionborderopacity等,同时文字组件也拥有本身的一些特有属性:fontcolortextAligntextDecoration,图片组件拥有:imageSrc,素材组件拥有:backgroundColor

可以看到每一个组件都被分为了两大类:

  • 样式属性
  • 其他属性

对于样式属性,设计时又有两种方案:

  • 直接在外部使用时传入一个css对象
  • 每一个样式属性分别传入

在平时的业务开发中,我其实用第一种方案更多一些,但如果是在现在的组件库场景,显得就不是那么的合适。

之所以这么说是因为在上面分析的props中,除了样式属性外还有一些是非样式属性(目前是点击相关),那么第一种方案的话就会多一层结构,而第二种分别传入的方式则是我们已经把纯样式属性筛选后的结果了。

这里拿其中的文字组件最终的代码来做进一步说明:

<template>
  <component
    :is="tag"
    :style="styleProps"
    class="c-text-component"
    @click.prevent="handleClick"
  >
    {{ text }}
  </component>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import handleStylePick from "../handleStylePick";
import handleComponentClick from "../handleComponentClick";
import {
  componentsDefaultProps,
  convertToComponentProps,
  isEditingProp,
} from "../../defaultProps";
const extraProps = {
  tag: {
    type: String,
    default: "p",
  },
  ...isEditingProp,
};
const defaultProps = convertToComponentProps(
  componentsDefaultProps["c-text"].props,
  extraProps
);

export default defineComponent({
  name: "c-text",
  props: {
    tag: {
      type: String,
      default: "div",
    },
    ...defaultProps,
  },
  setup(props) {
    const styleProps = handleStylePick(props);
    const handleClick = handleComponentClick(props);
    return {
      styleProps,
      handleClick,
    };
  },
});
</script>

<style scoped>
h2.c-text-component,
p.c-text-component {
  margin-bottom: 0;
}
button.c-text-component {
  padding: 5px 10px;
  cursor: pointer;
}
.c-text-component {
  box-sizing: border-box;
  white-space: pre-wrap;
}
</style>

首先通过handleStylePick拿到纯样式属性,然后通过handleComponentClick拿到上面提到的其他属性。这里的handleStylePickhandleComponentClick是三个组件都会用到的通用方法。

handleStylePick

import { pick, without } from "lodash-es";
import { computed } from "vue";
import { textDefaultProps } from "../defaultProps";

export const defaultStyles = without(
  Object.keys(textDefaultProps),
  "actionType",
  "url",
  "text"
);
const handleStylePick = (props: any, pickStyles = defaultStyles) => {
  return computed(() => pick(props, pickStyles));
};

export default handleStylePick;

handleComponentClick

const handleComponentClick = (props: any) => {
  const handleClick = () => {
    if (props.actionType && props.url && !props.isEditing) {
      window.location.href = props.url;
    }
  };
  return handleClick;
};

export default handleComponentClick;

至于本地组件库开发和调试可以使用npm link

组件添加测试用例

我们同样以文本组件为例来进行说明。单元测试的目的是为了尽可能发布前用test case的方式去测试组件功能的完整性和正确性,对于使用方来说也更具说服力。

针对文本组件,我写了三个case:

  • CText组件可以正常渲染,包含属性
  • 当有actionType和url属性时,点击CText组件可以正常跳转
  • 当正在编辑状态时,点击CText组件,即使拥有actionType和url属性也不应该触发跳转
import { shallowMount } from "@vue/test-utils";
import CText from "../../src/components/CText";
import { textDefaultProps } from "../../src/defaultProps";

describe("CText.vue", () => {
  const { location } = window;
  beforeEach(() => {
    Object.defineProperty(window, "location", {
      writable: true,
      value: { href: "" },
    });
  });
  afterEach(() => {
    window.location = location;
  });
  it("CText组件可以正常渲染,包含属性", () => {
    const msg = "test";
    const props = {
      ...textDefaultProps,
      text: msg,
    };
    const wrapper = shallowMount(CText, { props });
    expect(wrapper.text()).toBe(msg);
    expect(wrapper.element.tagName).toBe("P");
    const style = wrapper.attributes().style;
    expect(style.includes("font-size")).toBeTruthy();
    expect(style.includes("actionType")).toBeFalsy();
  });
  it("当有actionType和url属性时,点击CText组件可以正常跳转", async () => {
    const props = {
      ...textDefaultProps,
      actionType: "url",
      url: "http://cosen95.cn/",
      tag: "h2",
    };
    const wrapper = shallowMount(CText, { props });
    expect(wrapper.element.tagName).toBe("H2");
    await wrapper.trigger("click");
    expect(window.location.href).toBe("http://cosen95.cn/");
  });
  it("当正在编辑状态时,点击CText组件,即使拥有actionType和url属性也不应该触发跳转", async () => {
    const props = {
      ...textDefaultProps,
      actionType: "url",
      url: "http://cosen95.cn/",
      tag: "h2",
      isEditing: true,
    };
    const wrapper = shallowMount(CText, { props });
    await wrapper.trigger("click");
    expect(window.location.href).not.toBe("http://cosen95.cn/");
  });
});

执行测试用例:

低代码海报平台的组件库如何设计?

组件库打包

说到打包,必不可少的第一步就是打包工具的选择,以目前市面最为常用的两种打包工具来说:

低代码海报平台的组件库如何设计?

webpack我相信做前端的同学大家都用过,那么为什么有些场景还要使用rollup呢?这里我简单对webpackrollup做一个比较:

总体来说webpackrollup在不同场景下,都能发挥自身优势作用。webpack对于代码分割和静态资源导入有着“先天优势”,并且支持热模块替换(HMR),而rollup并不支持。

所以当开发应用时可以优先选择webpack,但是rollup对于代码的Tree-shakingES6模块有着算法优势上的支持,若你项目只需要打包出一个简单的bundle包,并是基于ES6模块开发的,可以考虑使用rollup

其实webpack2.0开始就已经支持Tree-shaking,并在使用babel-loader的情况下还可以支持es6 module的打包。实际上,rollup已经在渐渐地失去了当初的优势了。但是它并没有被抛弃,反而因其简单的API、使用方式被许多库开发者青睐,如ReactVue等,都是使用rollup作为构建工具的。

显然经过一番对比后,rollup作为组件库的打包方案是最合适不过的。

打包方案确定了,下一个要思考的问题就是应该打包什么类型的文件?

我们可以先去看下目前Element UIAnt Design分别都支持什么样的安装方式:

低代码海报平台的组件库如何设计?

低代码海报平台的组件库如何设计?

可以看到,基本都同时支持npm下载或者浏览器直接引入的方式,那么对应的最终打包文件就是ES ModuleUMD格式

到这里,我们就可以开始编写rollup配置文件了 - rollup.config.js

import vue from "rollup-plugin-vue";
import css from "rollup-plugin-css-only";
import typescript from "rollup-plugin-typescript2";
import { nodeResolve } from "@rollup/plugin-node-resolve";
import { name } from "../package.json";
const file = (type) => `dist/${name}.${type}.js`;
const overrides = {
  compilerOptions: { declaration: true },
  exclude: ["tests/**/*.ts", "tests/**/*.tsx"],
};
export { name, file };
export default {
  input: "src/index.ts",
  output: {
    name,
    file: file("esm"),
    format: "es",
  },
  plugins: [
    nodeResolve(),
    typescript({ tsconfigOverride: overrides }),
    vue(),
    css({ output: "choba-lego-components.css" }),
  ],
  external: ["vue", "lodash-es"],
};

这里面用到的几个插件的含义为:

  • rollup-plugin-vue:处理vue文件
  • rollup-plugin-css-only:单独打包css文件
  • rollup-plugin-typescript2:处理ts文件
  • @rollup/plugin-node-resolve:rollup 无法识别 node_modules 中的包,帮助 rollup 查找外部模块,然后导入

这里主要还是以介绍项目中的rollup plugin为主,关于rollup更具体的可参考我之前的文章: 一文带你快速上手Rollup

这里着重说一下rollup-plugin-typescript2,上面也提到了,组件库需要提供类型声明文件,也就是.d.ts,那么就需要在配置时添加tsconfigOverride

const overrides = {
  compilerOptions: { declaration: true },
  exclude: ["tests/**/*.ts", "tests/**/*.tsx"],
};

这样在打包结果中就会包含对应的类型声明文件了。

有些场景下,虽然我们使用了@rollup/plugin-node-resolve插件,但可能我们仍然想要某些库保持外部引用状态,这时我们就需要使用external属性,来告诉rollup.js哪些是外部的类库。

这里的vuelodash-es都可以放到external中。

有了这个基础的文件后,针对ES ModuleUMD还要有单独的配置文件: rollup.esm.config.js

import basicConfig, { name, file } from "./rollup.config";
export default {
  ...basicConfig,
  output: {
    name,
    file: file("esm"),
    format: "es",
  },
};

rollup.umd.config.js

import basicConfig, { file } from "./rollup.config";
export default {
  ...basicConfig,
  output: {
    name: "ChobaLegoComponents",
    file: file("umd"),
    format: "umd",
    globals: {
      vue: "Vue",
      "lodash-es": "_",
    },
    exports: "named",
  },
};

核心区别在于format,然后umd的还要配置一下globals(来提供全局变量名称)和exports

配置完成,来看下打包结果:

低代码海报平台的组件库如何设计?

组件库发布

最后一步就是将组件库发布到npm了。

第一步肯定是要调整package.json为符合npm publish的条件了。有以下几个字段要做相应调整:

  • name:包的唯一标识,不能和其他包重名
  • version:包版本
  • private:如果需要发布到npm的话,需要放开限制,也就是设置为false
  • main:指定程序的主入口文件(这里是dist/choba-lego-components.umd.js)
  • module:指向的应该是一个基于 ES6 模块规范使用ES5语法书写的模块
  • files:用于描述你 npm publish 后推送到 npm 服务器的文件列表,如果指定文件夹,则文件夹内的所有内容都会包含进来

除了上面这些外还有像keywordshomepage这些不是特别重要的我就不赘述了。

下面我要补充说一下上面没讲到的dependenciesdevDependenciespeerDependencies

dependencies 指定了项目运行所依赖的模块,开发环境和生产环境的依赖模块都可以配置到这里。

有一些包有可能你只是在开发环境中用到,例如用于检测代码规范的 eslint 或者是用于进行测试的 jest ,用户使用你的包时即使不安装这些依赖也可以正常运行,反而安装他们会耗费更多的时间和资源,所以你可以把这些依赖添加到 devDependencies 中,这些依赖照样会在你本地进行 npm install 时被安装和管理,但是不会被安装到生产环境。

peerDependencies 用于指定你正在开发的模块所依赖的版本以及用户安装的依赖包版本的兼容性。这个可能有点难理解。就以我们这个组件库为例,组件库依赖指定版本的vue,那么就要保证组件库使用方的环境也一定要有对应版本的vue依赖。这个时候,就可以把vue配置到peerDependencies中。

聊完这些,在命令行执行npm login进行npm的登录

低代码海报平台的组件库如何设计?

然后可以使用npm whoami验证是否已登录成功:

低代码海报平台的组件库如何设计?

登录成功后,修改package.json中的版本号,然后执行npm publish

低代码海报平台的组件库如何设计?

出现上图的效果,就表明已经发布成功了。

这里还有一个点在于,可以利用npm scripts的钩子(prepublishOnly),在publish前做一些操作,比如对组件做linttest,都通过后才真正执行publish操作。

去npm官网看下:

低代码海报平台的组件库如何设计?

1.0.5就是刚刚发上去的版本。

看完了这些,去组件库使用的地方看一下:

低代码海报平台的组件库如何设计?

已经正常渲染了。

到这里,组件库从初始化、开发、添加测试用例、打包、使用方引入这一系列流程就已经梳理完毕了~