从0到1搭建兼容vue2和vue3的前端组件库(文末附脚手架)

前言

随着业务的不断成熟、完善,大多数情况下我们会发现页面中的功能大同小异,翻来覆去可能就是那几套界面交互与逻辑,反复开发就很浪费成本。

虽然目前市面上包括公司内已经有很多功能强大且完善的组件库供我们使用,但是对于我们目前复杂的产品的形态结构、各个页面设计时也可能会有一个功能在不同迭代出了多套设计的情况,PM画的原型也风格各异,同时也需要对多平台、多业务的支持。导致很多场景下样式也不能统一,沟通成本增大,效率低下,影响产品的可用性效果,因此搭建符合自身B端产品平台的组件库的诉求迫在眉睫。

回顾组件库搭建的整个项目过程,存在很多值得记录、反思的内容,文章接下来会分享和阐述项目中一些核心思路作为对整个项目流程的复盘,同时也旨在帮助大家学习组件库的构建思路。

ps:但是开发组件库还是需要投入时间和成本的,毕竟不是所有的业务都适合搞一套组件库,一定要体现价值(提效!提效!提效!)。说了这么多,接下来我们就来分析和实现一个团队内部的组件库吧。

通过本文档可以得到什么

  • 从0到1搭建前端组件库

  • 前端组件库的系统设计

  • 同时兼容vue2 vue3的解决方案

  • 文档站部署:如何接入kfx与流水线部署文档站

  • 组件部署:组件发布到npm上

  • 一个基于vite的轻量版组件库脚手架(附仓库地址)

一、组件系统设计

1、层级设计

上图是简单描述了组件在页面的作用方式及层级,我们可以看到一个页面可以由基础组件+业务组件+区块构成。对于一个复杂的前端系统,想要组件能够被频繁使用,且能满足大部分业务场景,就需要设计一个好的架构,对页面中的内容进行合理拆分。

2、组件拆分思路

成熟的大项目中有很多复杂的业务功能,但是不同模块或者子系统之间很多业务往往是相通的或者相似的,一个功能往往需要写很多次,其实完全没必要重复劳动,重复写多次不仅浪费人力成本对于可维护性来说也是一种灾难,所以基于这种场景我们的 业务组件 就很有必要出场了。

我们可以把系统中常用的功能和需求进行梳理,把功能或者需求类似的有机体封装成一个业务组件,并对外暴露接口来实现灵活的可定制性,这样的话我们就可以再不同页面不同子系统中复用同样的逻辑和功能了。

同理,不同页面中往往有可能出现视觉或者交互完全相同或者类似的区块(可以理解为大而全的业务组件),为了提高可复用性和提高开发效率,我们往往会基于基础组件和业务组件再进行一次封装,让其成为一个独立的区块以便直接复用。

在需求收集上我们遵循两个原则:

1、自上而下,首先需要根据对所有业务进行系统梳理,提取出高优组件;

2、由内而外,这个过程可以卷起产品同学征集需求,避免闭门造车。

将组件划分好后,就需要有足够细的粒度对组件进行拆分,这样才能最大程度复用组件。

通过这样一层层封装,我们就逐渐搭建了一套完整的组件化系统。但要注意一点就是高层次的组件会依赖低层次的组件,但是低层次的组件不可以包含高层次的组件。他们的关系就类似下图:

image

二、组件库搭建

先抛几个问题:

  • 老项目都是vue2写的,上了vue3,之前vue2的代码还能兼容吗?

  • 老项目vue2太大了,暂时没法全部升级,怎么平滑过渡?

  • 新开工程用的vue3新技术研发出来的,老项目也想引用这个模块,代码是不是要降级?

1、怎么兼容vue2 vue3

常规思路:

开发一套vue2的组件库,项目升级vue3时再同步升级一套vue3的组件库。

优点:前期开发快,项目中要么是vue2要么是vue3,所见即所得,没有兼容的心智负担

缺点:重构成本高,且需要维护两套代码,维护成本高。(一个需求搞两次,一个bug修两遍,工作量加倍,属实顶不住)

懒癌思路:就想开发一套代码,构建好可以同时支持vue2和vue3。

可行吗?可行!使用vue-demi,打穿vue2-vue3的壁垒,上面的问题就不复存在了。

根据创建者 Anthony Fu 的说法,Vue Demi 是一个开发实用程序,它允许用户为 Vue 2 和 Vue 3 编写通用的 Vue 库,而无需担心用户安装的版本。

既然如此,我们先看下vue-demi的原理:主要是利用compositionAPI在写法上和vue3的一致性进行兼容的过渡。

核心:通过postinstall这个钩子,对版本判断从而去更改lib文件下的文件,动态改变用户引用的版本。

image

v2 引入了compositionAPI支持vue3写法

image

v3 什么都不用做,我们写的就是vue3写法,只不过没有script setup,具体原因后面会讲。

image

总结一句,就是vue-demi会根据用户使用vue的版本号来判断,vue2时加入@vue/composition-api。

那好了,我们的写法搞定了,现在组件库可以一套代码兼容vue2和vue3了吗?

还是不行,我们的核心功能是用sfc的vue文件打包的,写的是template,并不是render函数,关于template的解析,v2和v3解析出来的不能通用的,因为v3之所以快,是因为对temlate的比对优化了,具体咋优化的大家可以查看vue3的源码。这种场景肯定不能只打包一次就同时支持vue2和vue3调用。

那我们可以参考vue-demi,从postinstall着手,也编译两个版本,在宿主系统中通过宿主系统的版本判断要加载哪套组件代码,不就ok了吗?

没错,目前就是这么干的。

image

此时就可以一套代码,使用vue2和vue3编译两次,达到支持vue2和vue3的目的。

我们来看下优缺点:

优点:一套代码,易于维护,开发成本低,同时支持vue2、vue3

缺点:写代码的时候,仍然有些写法是vue2和vue3有差异的,并不能完全抹平,但是情况很少。需要编译两次,

例如:组件上绑定v-model,vue3子组件的属性为 modelValue, vue2为 'value';

import { isVue2, isVue3 } from 'vue-demi' 
if (isVue2) { 
  // Vue 2 only 
} else { 
  // Vue 3 only 
}

问题:如果不用SFC,改用render的写法,能只build一次吗 ?

答:可以。

我们的组件render-demo.ts如下

import { defineComponent, h, ref } from 'vue-demi'

export default defineComponent({
  name: 'RenderDemo',
  props: {},
  setup() {
    const count = ref(0)
    return () => h('div', {
        on:{  // vue2 h函数底层为 createElement
            click(){
                console.log('update')
                count.value++
            }

        },
        onClick() {  // vue3  h函数
            console.log('update')
            count.value++
            }
        }, [
            h('div', `count: ${count.value}`),
            h('div', 'RenderDemo')
        ])
  }
})

通过编译后产物是一样的:即可认为不论在vue2还是vue3环境打包只build一次即可同时支持。但是有vue2和vue3写法上的区别(参照官方文档:v3v2),需要手动处理。PS:这样享受不到vue3模板编译静态提升的优化了。

image

问题:vue3支持optionApi吗?

答:支持。

问题:那能不能都用optionApi写,只build一次?

答:不能,因为vue2和vue3编译SFC的依赖插件不同,底层代码有差异。

image
image

好吧...还是build两次吧...

2、怎么测试组件是否能在vue2 和vue3环境下正常使用

分别发布vue2 和vue3的包,然后在两个环境引用进行测试验证。

但是由于目前,潜在的问题就是组件库依赖了其他的基础组件库,例如KwaiUi,这个在宿主系统中的版本是不确定的,所以这个地方可能有兼容性问题,暂时没有想到好的解决办法。

3、搭建流程

三、文档站搭建

1、文档站是拿什么写的

基于vitepress构建

官网文档:vitepress

2、vue的组件为什么能写到markdown里?

VitePress 使用markdown-it作为 Markdown 渲染器。上面的很多扩展都是通过自定义插件实现的。您可以使用以下选项进一步自定义markdown-it实例:markdown.vitepress/config.js

2、为什么用vitepress

横向对比

<colgroup><col width="0.2"><col width="0.2"><col width="0.2"><col width="0.2"><col width="0.2*"></colgroup>
|

框架

|

官网

|

优点

|

缺点

|

匹配度

|
|

vuePress

|

https://www.vuepress.cn/

|

相比于vitePress插件更多一点,快速

|

需要对vite进行进一步支持

|

⭐️⭐️⭐️

|
|

vitePress

|

https://vitepress.vuejs.org/

|

原生支持vite以及vue3,提供了比较简洁的文档编辑能力,快速

|

因为是新出的,所以插件不如vuePress丰富

|

⭐️⭐️⭐️⭐️⭐️ 开箱即用

|
|

MDocs

|

https://main--mdocs-template.jinx.corp.kuaishou.com/

|

公司内部,可以嵌入可在线演示的代码编辑器

|

react框架,不匹配

|

⭐️

|
|

通过markDown渲染器进行开发

|

市面上各种markDown解析器,webpack也有些loader插件。

例如:

import Markdown from 'vite-plugin-md'

export default defineConfig({
  // 默认的配置
  plugins: [
    vue({ include: [/\.vue$/, /\.md$/] }),
    Markdown(),
  ],
})

这样就可以把md当vue用了

|

自由度高

|

成本高,基础能力(例如路由,代码高亮,导航等)需要自己开发各种插件

|

⭐️

|

根据我们的需求情况,需要快速搭建,核心内容也是将组件大范围应用于业务,故而选择vitepress。后续如果需要自由度更高的情况,再换技术栈也是不迟的。

3、vue的组件为什么能写到markdown里?

image

底层大都是对markdown编译为相应的html再套一层<template></template>进行预览。具体如何做词法解析的这里暂不做解释。

VitePress 使用markdown-it作为 Markdown 渲染器。上面的很多扩展都是通过自定义插件实现的。vitepress也支持使用以下选项进一步自定义markdown-it实例:markdown.vitepress/config.js

const anchor = require('markdown-it-anchor')
module.exports = {
  markdown: {
    // options for markdown-it-anchor
    // https://github.com/valeriangalliat/markdown-it-anchor#permalinks
    anchor: {
      permalink: anchor.permalink.headerLink()
    },
    // options for markdown-it-toc-done-right
    toc: { level: [1, 2] },
    config: (md) => {
      // use more markdown-it plugins!
      md.use(require('markdown-it-xxx'))
    }
  }
}

4、搭建流程

目录架构如下(tips:使用mddir可以生成)

|-- docs
    |-- index.md // 首页 yml语法描述填空即可
    |-- package.json
    |-- vite.config.ts 
    |-- yarn.lock
    |-- .vitepress  // vitepress 配置
    |   |-- config.js  // 主配置
    |   |-- routes // 文档路径配置
    |   |   |-- guide.js
    |   |-- theme  // 主题配置
    |       |-- index.js
    |-- guide    // 文档在这个文件夹下
        |-- configuration.md
        |-- getting-started.md
        |-- what-is-commercial-ui.md
        |-- my-components  // 新建组件文档目录
        |   |-- my-components.md   // 组件文档
        |   |-- demo  // 示例
        |       |-- demo-1.vue  // 按序号写多个demo

安装vitepress

npm i vitepress -D 
全局安装 npm i vitepress -g

packages.json

{
  ...
  "scripts": {
    "docs:dev": "vitepress dev docs",
    "docs:build": "vitepress build docs",
    "docs:serve": "vitepress serve docs"
  },
  ...
}

docs目录下执行

$ yarn docs:dev

浏览器输入:http://localhost:3000/getting-started

具体的配置可以参考官网文档:vitepress

引入组件库与依赖组件

cd docs/.vitepress/theme/index.js

import DefaultTheme from 'vitepress/theme';
import KwaiUI from '@ks/kwai-ui';

/** 库导入 */
import AuditUI from '@ks-cqc/audit-ad-components';

/** 本地导入 */
// import AuditUI from '../../../src/index';

/** 依赖库样式导入 */
import '@ks/kwai-ui/lib/theme-new-era/index.css';
import '@ks-cqc/audit-ad-components/lib/v3/style.css';

export default {
    ...DefaultTheme,
    enhanceApp({ app }) {
        app.use(KwaiUI); // 依赖的基础组件库
        app.use(AuditUI); // 我们的业务组件库
    },
};

现在我们就可以愉快地开发组件库了,但是并不愉快,因为需要一直手动创建好多个文件,要是能一键创建就好了。

四、cli能力快速开发组件

到目前为止,我们的整个“实时可交互式文档”已经搭建完了,是不是意味着可以交付给其他同学进行真正的组件开发了呢?假设你是另一个开发同学,我跟你说:“你只要在这里,这里和这里新建这些文件,然后在这里和这里修改一下配置就可以新建一个组件了!”你会不会很想打人?我们先看看需要哪些步骤...

1、快速开发一个基础组件需要哪些文件?

首先需要的是组件文件,有了组件文件还需要文档文件,至少需要四个。

  • 创建四个文件如下:
image

image

创建了文件还不够,还需要对一些文件进行修改。

  • 为文档创建路由
image
  • 组件库引入组件,注册组件,暴露组件
image

好麻烦,组件还没开发,竟然先要改这么多地方,每次都这样搞一遍人都麻了,这种枯燥乏味的事情还是交给程序干吧。。。

2、如何通过命令行快速创建模板文件?

  • 首先就是需要解决文件创建

这个简单,我们创建一套模板文件,对应上述四个必须的基础文件

|-- component-template
  |-- comp   // 组件文件
  |   |-- component-template.vue // 组件代码
  |   |-- index.ts  // 暴露组件
  |-- doc   // 文档文件
    |-- component-template.md   // 组件文档
    |-- demo // 文档中的示例文件
    |-- demo-1.vue 

名称怎么改呢?不能都叫demoComponent吧

文件名在copy时替换即可,文件中的内容通过mastache语法进行模板替换即可。

代码如下:

// 替换文件内容
function replaceInfo(filePath, componentName, componentDesc) {
    fs.readFile(filePath, (err, data) => {
        if (err) {
            return err;
        }
        let str = data.toString();
        str = str.replace(/{{component-name}}/g, componentName).replace(/{{component-desc}}/g, componentDesc);
        fs.writeFile(filePath, str, function (err) {
            if (err) {return err;}
        });
    });
}
// 替换文件名
async function modifyInfo({ filesPath, componentName, componentDesc}) {
    filesPath.forEach(async ({ target }) => {
        walk(target, function (path, fileName) {
            const oldPath = path + '/' + fileName;
            const newPath = path + '/' + fileName.replace('component-template', componentName);
            renameFile(oldPath, newPath);
            replaceInfo(newPath, componentName, componentDesc);
        });
    });
}
  • 自动修改上述的路由和组件引入以及暴露的文件

这里我们虽然也可以直接修改文件,注入代码,但是试想一下,如果日后组件越来越多,这个时候路由和入口文件很可能这样,密密麻麻,还很乱,看着就头疼。

image

image

所以我们不能用这么蠢的方式。。。

需要简单修改一下

  • 对于组件引入和暴露可以这样

收拢到entry.ts中进行引入暴露

image

index.ts只需要对entry暴露出去的模块对象遍历注册即可,单个导出也更简洁了

// 引入公共样式
import './styles/base.less';

// 引入组件
import * as components from './entry';
import { installArgument } from './utils/install';
// 全局注册组件
const install = function (_vue: installArgument) {
    Object.values(components).forEach(comp => {
        if (!comp || !comp.name) {
            return;
        }
        comp.install(_vue);
    });
};

const plugin = {
    install,
};

// 单个导出
export * from './entry';

// 整体导出
export default plugin;
  • 文档路由同理,也收拢到一个入口文件,进行遍历
import components from '../../../components';

.....

compRouteConfig = [
        {
            text: '业务组件',
            collapsible: true,
            items: [
                ...Object.keys(components).map(comName => {
                    return { text: components[comName].zhName, link: `/guide/${components[comName].name}/${components[comName].name}` };
                }),
                // { text: 'demo', link: `/guide/demo/demo.md` }
            ],
        },
    ];

components.json中

{
    "demo-component": {
        "name": "demo-component",
        "zhName": "示例组件",
        "desc": "默认:这是一个新组件",
        "url": "./src/components/demo-component/index.ts"
    }
}

总结一下:

1、创建四个文件,替换其中关键信息

2、修改entry,components.json,添加新建的组件信息

需要的关键信息只有三个,组件名,组件描述,组件中文名,所以我们创建时要输入如下即可:

image

想实现命令行提问并收集输入的答案需要用到一个库 inquirer

代码很简单

inquirer.prompt([
        {
            type: 'input',
            name: 'componentName',
            message: 'Component name (kebab-case):',
            default: 'demo-component',
        },
        {
            type: 'input',
            name: 'componentZhName',
            message: '请输入你要新建的组件名(中文):',
            default: '示例组件',
        },
        {
            type: 'input',
            message: '请输入组件的功能描述:',
            name: 'componentDesc',
            default: '默认:这是一个新组件'
        }
    ]);

通过node运行后,会提出三个我们设计好的问题,保存在一个对象中我们导出使用即可。然后拿着这些信息按照上面的流程替换模板内容,拷贝文件到对应目录就可以快速开发了。

五、部署

自己丢到服务器上即可,也可以托管到github。

附:

组件库基础能力脚手架

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343

推荐阅读更多精彩内容