iis服务器助手广告广告
返回顶部
首页 > 资讯 > 精选 >vue组件库如何开发使用
  • 338
分享到

vue组件库如何开发使用

2023-07-04 21:07:19 338人浏览 安东尼
摘要

本文小编为大家详细介绍“Vue组件库如何开发使用”,内容详细,步骤清晰,细节处理妥当,希望这篇“vue组件库如何开发使用”文章能帮助大家解决疑惑,下面跟着小编的思路慢慢深入,一起来学习新知识吧。yarn workspace + lerna:

本文小编为大家详细介绍“Vue组件库如何开发使用”,内容详细,步骤清晰,细节处理妥当,希望这篇“vue组件库如何开发使用”文章能帮助大家解决疑惑,下面跟着小编的思路慢慢深入,一起来学习新知识吧。

yarn workspace + lerna: 管理组件库及其生态项目

考虑到组件库整体需要有多边资源支持,比如组件源码,库文档站点,color-gen等类库工具,代码规范配置,vite插件,脚手架,storybook等等,需要分出很多packages,package之间存在彼此联系,因此考虑使用monorepo的管理方式,同时使用yarn作为包管理工具,lerna作为包发布工具。

在monorepo之前,根目录就是一个workspace,我们直接通过yarn add/remove/run等就可以对包进行管理。但在monorepo项目中,根目录下存在多个子包,yarn 命令无法直接操作子包,比如根目录下无法通过yarn run dev启动子包package-a中的dev命令,这时我们就需要开启yarn的workspaces功能,每个子包对应一个workspace,之后我们就可以通过yarn workspace package-a run dev启动package-a中的dev命令了。

你可能会想,我们直接cd到package-a下运行就可以了,不错,但yarn workspaces的用武之地并不只此,像auto link,依赖提升,单.lock等才是它在monorepo中的价值所在。

启用yarn workspaces

我们在根目录packge.JSON中启用yarn workspaces:

{  "private": true,  "workspaces": [    "packages*.html"],      customSyntax: "postCSS-html"    },    {      files: ["***.vue": [      "eslint --fix",      "stylelint --fix",      "prettier --write"    ]  }}

在monorepo中,lint-staged运行时,将始终向上查找并应用最接近暂存文件的配置,因此我们可以在根目录下的package.json中配置lint-staged。值得注意的是,每个glob匹配的数组中的命令是从左至右依次运行,和webpack的loder应用机制不同!

最后,我们在.husky文件夹中找到pre-commit,并将yarn lint修改为npx --no-install lint-staged

#!/usr/bin/env sh. "$(dirname -- "$0")/_/husky.sh"npx --no-install lint-staged

至此,当我们执行git commit -m "xxx"时,lint-staged会如期运行帮我们校验staged(暂存区)中的代码,避免了对工作区的全量检查。

集成commitlint: 规范化commit message

除了代码规范检查之后,Git 提交信息的规范也是不容忽视的一个环节,规范精准的 commit 信息能够方便自己和他人追踪项目和把控进度。这里,我们使用大名鼎鼎的angular团队提交规范

commit message格式规范

commit message 由 HeaderBodyFooter 组成。其中Herder时必需的,Body和Footer可选。

Header

Header 部分包括三个字段 typescopesubject

<type>(<scope>): <subject>

type

其中type 用于说明 commit 的提交类型(必须是以下几种之一)。

描述
featFeature) 新增一个功能
fixBug修复
docsDocumentation) 文档相关
style代码格式(不影响功能,例如空格、分号等格式修正),并非css样式更改
refactor代码重构
perfPerfORMent) 性能优化
test测试相关
build构建相关(例如 scopes: WEBpack、gulp、npm 等)
ci更改持续集成软件的配置文件和 package 中的 scripts 命令,例如 scopes: Travis, Circle 等
chore变更构建流程或辅助工具,日常事务
revertgit revert
scope

scope 用于指定本次 commit 影响的范围。

subject

subject 是本次 commit 的简洁描述,通常遵循以下几个规范:

  • 用动词开头,第一人称现在时表述,例如:change 代替 changed 或 changes

  • 第一个字母小写

  • 结尾不加句号.

Body(可选)

body 是对本次 commit 的详细描述,可以分成多行。跟 subject 类似。

Footer(可选)

如果本次提交的代码是突破性的变更或关闭Issue,则 Footer 必需,否则可以省略。

集成commitizen(可选)

我们可以借助工具帮我们生成规范的message。

1. 安装

yarn add commitizen -D -W

2. 使用

安装适配器

yarn add cz-conventional-changelog -D -W

这行命令做了两件事:

  • 安装cz-conventional-changelog到开发依赖

  • 在根目录下的package.json中增加了:

"config": {  "commitizen": {    "path": "./node_modules/cz-conventional-changelog"  }}

添加npm scriptscm

"scripts": {  "cm": "cz"},

至此,执行yarn cm,就能看到交互界面了!跟着交互一步步操作就能自动生成规范的message了。

vue组件库如何开发使用

集成commitlint: 对最终提交的message进行校验

1. 安装

首先在根目录安装依赖:

yarn add commitlint @commitlint/cli @commitlint/config-conventional -D -W

2. 使用

接着新建.commitlintrc.js:

module.exports = {  extends: ["@commitlint/config-conventional"]};

最后向husky中添加commit-msg钩子,终端执行:

npx husky add .husky/commit-msg "npx --no-install commitlint -e $HUSKY_GIT_PARAMS"

执行成功之后就会在.husky文件夹中看到commit-msg文件了:

#!/usr/bin/env sh. "$(dirname -- "$0")/_/husky.sh"npx --no-install commitlint -e

至此,当你提交代码时,如果pre-commit钩子运行成功,紧接着在commit-msg钩子中,commitlint会如期运行对我们提交的message进行校验。

关于lint工具的集成到此就告一段落了,在实际开发中,我们还会对lint配置进行一些小改动,比如ignore,相关rules等等。这些和具体项目有关,我们不会变更package里的配置。

千万别投机取巧拷贝别人的配置文件!复制一时爽,代码火葬场。

图标库

巧妇难为无米之炊。组件库通常依赖很多图标,因此我们先开发一个支持按需引入的图标库。

假设我们现在已经拿到了一些漂亮的svg图标,我们要做的就是将每一个图标转化生成.vue组件与一个组件入口index.ts文件。然后再生成汇总所有组件的入口文件。比如我们现在有foo.svg与bar.svg两个图标,最终生成的文件及结构如下:

vue组件库如何开发使用

相应的内容如下:

// bar.tsimport _Bar from "./bar.vue";const Bar = Object.assign(_Bar, {  install: (app) => {    app.component(_Bar.name, _Bar);  }});export default Bar;
// foo.tsimport _Foo from "./foo.vue";const Foo = Object.assign(_Foo, {  install: (app) => {    app.component(_Foo.name, _Foo);  }});export default Foo;
// arGoIcon.tsimport Foo from "./foo";import Bar from "./bar";const icons = [Foo, Bar];const install = (app) => {  for (const key of Object.keys(icons)) {    app.use(icons[key]);  }};const ArgoIcon = {  ...icons,  install};export default ArgoIcon;
// index.tsexport { default } from "./argoIcon";export { default as Foo } from "./foo";export { default as Bar } from "./bar";

之所以这么设计是由图标库最终如何使用决定的,除此之外argoIcon.ts也将会是打包umd的入口文件。

// 全量引入import ArgoIcon from "图标库";app.use(ArgoIcon); // 按需引入import { Foo } from "图标库";app.use(Foo);

图标库的整个构建流程大概分为以下3步:

vue组件库如何开发使用

1. svg图片转.vue文件

vue组件库如何开发使用

整个流程很简单,我们通过glob匹配到.svg拿到所有svg的路径,对于每一个路径,我们读取svg的原始文本信息交由第三方库svgo处理,期间包括删除无用代码,压缩,自定义属性等,其中最重要的是为svg标签注入我们想要的自定义属性,就像这样:

<svg   :class="cls"   :style="innerStyle"  :stroke-linecap="strokeLinecap"  :stroke-linejoin="strokeLinejoin"  :stroke-width="strokeWidth">  <path d="..."></path></svg>

之后这段svgHtml会传送给我们预先准备好的摸板字符串

const template = `<template>  ${svgHtml}</template><script setup>defineProps({    "stroke-linecap": String;    // ...  })  // 省略逻辑代码...</script>`

为摸板字符串填充数据后,通过fs模块的writeFile生成我们想要的.vue文件。

2. 打包vue组件

在打包构建方案上直接选择vite为我们提供的lib模式即可,开箱即用,插件扩展(后面会讲到),基于rollup,能帮助我们打包生成ESM这是按需引入的基础。当然,commonjsumd也是少不了的。整个过程我们通过Vite 的javascript api实现:

import { build } from "vite";import fs from "fs-extra";const CWD = process.cwd();const ES_DIR = resolve(CWD, "es");const LIB_DIR = resolve(CWD, "lib");interface compileOptions {  umd: boolean;  target: "component" | "icon";}async function compileComponent({  umd = false,  target = "component"}: compileOptions): Promise<void> {  await fs.emptyDir(ES_DIR);  await fs.emptyDir(LIB_DIR);  const config = getModuleConfig(target);  await build(config);  if (umd) {    await fs.emptyDir(DIST_DIR);    const umdConfig = getUmdConfig(target);    await build(umdConfig);  }}
import { InlineConfig } from "vite";import glob from "glob";const langFiles = glob.sync("components/locale/lang            preserveModulesRoot: "components"           },          {            format: "commonjs",            dir: "lib",            entryFileNames: "[name].js",            preserveModules: true,            preserveModulesRoot: "components",            exports: "named" // 导出模式          }        ]      },      // 开启lib模式      lib: {        entry,        formats: ["es", "cjs"]      }    },    plugins: [      // 自定义external忽略node_modules      external(),      // 打包声明文件      dts({        outputDir: "es",        entryRoot: C_DIR      })    ]  };};
export default function getUmdConfig(type: "component" | "icon"): InlineConfig {  const entry =    type === "component"      ? "components/argo-components.ts"      : "components/argo-icons.ts";  const entryFileName = type === "component" ? "argo" : "argo-icon";  const name = type === "component" ? "Argo" : "ArgoIcon";  return {    mode: "production",    build: {      target: "modules", // 支持原生 ES 模块的浏览器      outDir: "dist", // 打包产物存放路径      emptyOutDir: true, // 如果outDir在根目录下,则清空outDir      sourcemap: true, // 生成sourcemap       minify: false, // 是否压缩      brotliSize: false, // 禁用 brotli 压缩大小报告。      rollupOptions: { // rollup打包选项        external: "vue", // 匹配到的模块不会被打包到bundle        output: [          {            format: "umd", // umd格式            entryFileNames: `${entryFileName}.js`, // 即bundle名            globals: {                            vue: "Vue"            }          },          {            format: "umd",            entryFileNames: `${entryFileName}.min.js`,            globals: {              vue: "Vue"            },            plugins: [terser()] // terser压缩          },        ]      },      // 开启lib模式      lib: {        entry, // 打包入口        name // 全局变量名      }    },    plugins: [vue(), vueJsx()]  };};
export const CWD = process.cwd();export const C_DIR = resolve(CWD, "components");

可以看到,我们通过type区分组件库和图标库打包。实际上打包图标库和组件库都是差不多的,组件库需要额外打包国际化相关的语言包文件。图标样式内置在组件之中,因此也不需要额外打包。

3. 打包声明文件

我们直接通过第三方库 vite-plugin-dts 打包图标库的声明文件。

import dts from "vite-plugin-dts";plugins: [  dts({    outputDir: "es",    entryRoot: C_DIR  })]

关于打包原理可参考插件作者的这片文章。

lequ7.com/guan-yu-qia…

4. 实现按需引入

我们都知道实现tree-shaking的一种方式是基于ESM的静态性,即在编译的时候就能摸清依赖之间的关系,对于"孤儿"会残忍的移除。但是对于import "icon.css"这种没导入导出的模块,打包工具并不知道它是否具有副作用,索性移除,这样就导致页面缺少样式了。sideEffects就是npm与构建工具联合推出的一个字段,旨在帮助构建工具更好的为npm包进行tree-shaking。

使用上,sideEffects设置为false表示所有模块都没有副作用,也可以设置数组,每一项可以是具体的模块名或Glob匹配。因此,实现图标库的按需引入,只需要在argo-icons项目下的package.json里添加以下配置即可:

{  "sideEffects": false,}

这将告诉构建工具,图标库没有任何副作用,一切没有被引入的代码或模块都将被移除。前提是你使用的是ESM。

指定入口

Last but important!当图标库在被作为npm包导入时,我们需要在package.json为其配置相应的入口文件。

{  "main": "lib/index.js", // 以esm形式被引入时的入口  "module": "es/index.js", // 以commonjs形式被引入时的入口  "types": "es/index.d.ts" // 指定声明文件}

引入storybook:是时候预览我们的成果了!

顾名思义,storybook就是一本"书",讲了很多个"故事"。在这里,"书"就是argo-icons,我为它讲了3个故事:

  • 基本使用

  • 按需引入

  • 使用iconfont.cn项目

初始化storybook

新建@argo-design/ui-storybookpackage,并在该目录下运行:

npx storybook init -t vue3 -b webpack5

-t (即--type): 指定项目类型,storybook会根据项目依赖及配置文件等推算项目类型,但显然我们仅仅是通过npm init新创建的项目,storybook无法自动判断项目类型,故需要指定type为vue3,然后storybook会帮我们初始化storybook vue3 app。

-b (--builder): 指定构建工具,默认是webpack4,另外支持webpack5, vite。这里指定webpack5,否则后续会有类似报错:cannot read property of undefine(reading 'get')...因为storybook默认以webpack4构建,但是@storybook/vue3依赖webpack5,会冲突导致报错。这里是天坑!!

storybook默认使用yarn安装,如需指定npm请使用--use-npm。

这行命令主要帮我们做以下事情:

  • 注入必要的依赖到packages.json(如若没有指定-s,将帮我们自动安装依赖)。

  • 注入启动,打包项目的脚本。

  • 添加Storybook配置,详见.storybook目录。

  • 添加Story范例文件以帮助我们上手,详见stories目录。

其中1,2步具体代码如下:

{  "scripts": {    "storybook": "start-storybook -p 6006",    "build-storybook": "build-storybook"  },  "devDependencies": {    "@storybook/vue3": "^6.5.13",    "@storybook/addon-links": "^6.5.13",    "@storybook/addon-essentials": "^6.5.13",    "@storybook/addon-actions": "^6.5.13",    "@storybook/addon-interactions": "^6.5.13",    "@storybook/testing-library": "^0.0.13",    "vue-loader": "^16.8.3",    "@storybook/builder-webpack5": "^6.5.13",    "@storybook/manager-webpack5": "^6.5.13",    "@babel/core": "^7.19.6",    "babel-loader": "^8.2.5"  }}

接下来把目光放到.storybook下的main.js与preview.js

preview.js

preview.js可以具名导出parameters,decorators,argTypes,用于全局配置UI(stories,界面,控件等)的渲染行为。比如默认配置中的controls.matchers:

export const parameters = {  controls: {    matchers: {      color: /(background|color)$/i,      date: /Date$/    }  }};

它定义了如果属性值是以background或color结尾,那么将为其启用color控件,我们可以选择或输入颜色值,date同理。

vue组件库如何开发使用

除此之外你可以在这里引入全局样式,注册组件等等。更多详情见官网 Configure story rendering

main.js

最后来看看最重要的项目配置文件。

module.exports = {  stories: [    "../stories*.stories.mdx",    "../stories*.stories.@(js|jsx|ts|tsx)"  ],  addons: [    "@storybook/addon-links",    "@storybook/addon-essentials",    "@storybook/addon-interactions"  ],  framework: "@storybook/vue3",  core: {    builder: "@storybook/builder-webpack5"  },}

  • stories, 即查找stroy文件的Glob。

  • addons, 配置需要的扩展。庆幸的是,当前一些重要的扩展都已经集成到@storybook/addon-essentials。

  • framework和core即是我们初识化传递的-t vue3 -b webpack5

更多详情见官网 Configure your Storybook project

配置并启动storybook

less配置

由于项目使用到less因此我们需要配置一下less,安装less以及相关loader。来到.storybook/main.js

module.exports = {  webpackFinal: (config) => {    config.module.rules.push({      test: /.less$/,      use: [        {          loader: "style-loader"        },        {          loader: "css-loader"        },        {          loader: "less-loader",          options: {            lessOptions: {              javascriptEnabled: true            }          }        }      ]    });    return config;  },}

配置JSX

storybook默认支持解析jsx/tsx,但你如果需要使用jsx书写vue3的stories,仍需要安装相关插件。

在argo-ui-storybook下安装 @vue/babel-plugin-jsx

yarn add @vue/babel-plugin-jsx -D

新建.babelrc

{  "plugins": ["@vue/babel-plugin-jsx"]}

关于如何书写story,篇幅受限,请自行查阅范例文件或官网。

配置完后终端执行yarn storybook即可启动我们的项目,辛苦的成果也将跃然纸上。

vue组件库如何开发使用

vue组件库如何开发使用

vue组件库如何开发使用

对于UI,在我们的组件库逐渐丰富之后,将会自建一个独具组件库风格的文档站点,拭目以待。

组件库

组件通信

在Vue2时代,组件跨层级通信方式可谓“百花齐放”,provide/inject就是其中一种。时至今日,在composition,es6,ts加持下,provide/inject可以更加大展身手。

provide/inject原理

在创建组件实例时,会在自身挂载一个provides对象,默认指向父实例的provides。

const instance = {  provides: parent ? parent.provides : Object.create(appContext.provides)}

appContext.provides即createApp创建的app的provides属性,默认是null

在自身需要为子组件供数据时,即调用provide()时,会创建一个新对象,该对象的原型指向父实例的provides,同时将provide提供的选项添加到新对象上,这个新对象就是实例新的provides值。代码简化就是

function provide(key, value) {   const parentProvides = currentInstance.parent && currentInstance.parent.provides;   const newObj = Object.create(parentProvides);  currentInstance.provides = newObj;  newObj[key] = value;}

而inject的实现原理则时通过key去查找祖先provides对应的值:

function inject(key, defaultValue) {   const instance = currentInstance;   const provides = instance.parent == null    ? instance.vnode.appContent && instance.vnode.appContent.provides    :instance.parent.provides;  if(provides && key in provides) {    return provides[key]  }}

你可能会疑惑,为什么这里是直接去查父组件,而不是先查自身实例的provides呢?前面不是说实例的provides默认指向父实例的provides么。但是请注意,是“默认”。如果当前实例执行了provide()是不是把instance.provides“污染”了呢?这时再执行inject(key),如果provide(key)的key与你inject的key一致,就从当前实例provides取key对应的值了,而不是取父实例的provides!

最后,我画了2张图帮助大家理解

vue组件库如何开发使用

vue组件库如何开发使用

新增button组件并完成打包

篇幅有限,本文不会对组件的具体实现讲解哦,简单介绍下文件

vue组件库如何开发使用

  • __demo__组件使用事例

  • constants.ts定义的常量

  • context.ts上下文相关

  • interface.ts组件接口

  • TEMPLATE.md用于生成README.md的模版

  • button/style下存放组件样式

  • style下存放全局样式

打包esm与commonjs模块

关于打包组件的esmcommonjs模块在之前打包图标库章节已经做了介绍,这里不再赘述。

打包样式

相对于图标库,组件库的打包需要额外打包样式文件,大概流程如下:

  • 生成总入口components/index.less并编译成css。

  • 编译组件less。

  • 生成dist下的argo.css与argo.min.css。

  • 构建组件style/index.ts。

1. 生成总入口components/index.less

import path from "path";import { outputFileSync } from "fs-extra";import glob from "glob";export const CWD = process.cwd();export const C_DIR = path.resolve(CWD, "components");export const lessgen = async () => {  let lessContent = `@import "./style/index.less";\n`; // 全局样式文件  const lessFiles = glob.sync("**/style/index.less", {    cwd: C_DIR,    ignore: ["style/index.less"]  });  lessFiles.forEach((value) => {    lessContent += `@import "./${value}";\n`;  });  outputFileSync(path.resolve(C_DIR, "index.less"), lessContent);  log.success("genless", "generate index.less success!");};

代码很简单,值得一提就是为什么不将lessContent初始化为空,glob中将ignore移除,这不是更简洁吗。这是因为style/index.less作为全局样式,我希望它在引用的最顶部。最终将会在components目录下生成index.less内容如下:

@import "./style/index.less";@import "./button/style/index.less";

2. 打包组件样式

import path from "path";import { readFile, copySync } from "fs-extra"import { render } from "less";export const ES_DIR = path.resolve(CWD, "es");export const LIB_DIR = path.resolve(CWD, "lib");const less2css = (lessPath: string): string => {  const source = await readFile(lessPath, "utf-8");  const { css } = await render(source, { filename: lessPath });  return css;}const files = glob.sync("**stylestyle*.vue", "***.tsx"],    exclude = "node_modules

有了具体场景下的颜色梯度变量,我们就可以设计变量供给组件消费了:

@color-primary-1: @primary-1;@color-primary-2: @primary-2;@color-primary-3: @primary-3;
.argo-btn.arco-btn-primary {  color: #fff;    background-color: @color-primary-1;}

在使用组件库的项目中我们通过 Less 的 ·modifyVars 功能修改变量值:

Webpack配置

// webpack.config.jsmodule.exports = {  rules: [{    test: /.less$/,    use: [{      loader: 'style-loader',    }, {      loader: 'css-loader',    }, {      loader: 'less-loader',     options: {       lessOptions: {         modifyVars: {           'primary-6': '#f85959',         },         javascriptEnabled: true,       },     },    }],  }],}

vite配置

// vite.config.jsexport default {  css: {   preprocessorOptions: {     less: {       modifyVars: {         'primary-6': '#f85959',       },       javascriptEnabled: true,     }   }  },}

设计暗黑风格

首先,颜色梯度变量需要增加暗黑风格。也是基于@blue-6计算,只不过这里换成了dark-color-palette函数:

@dark-blue-1: dark-color-palette(@blue-6, 1);@dark-blue-2: dark-color-palette(@blue-6, 2);@dark-blue-3: dark-color-palette(@blue-6, 3);@dark-blue-4: dark-color-palette(@blue-6, 4);@dark-blue-5: dark-color-palette(@blue-6, 5);@dark-blue-6: dark-color-palette(@blue-6, 6);@dark-blue-7: dark-color-palette(@blue-6, 7);@dark-blue-8: dark-color-palette(@blue-6, 8);@dark-blue-9: dark-color-palette(@blue-6, 9);@dark-blue-10: dark-color-palette(@blue-6, 10);

然后,在相应节点下挂载css变量

body {  --color-bg: #fff;    --color-text: #000;    --primary-6: @primary-6; }body[argo-theme="dark"] {  --color-bg: #000;    --color-text: #fff;    --primary-6: @dark-primary-6; }

紧接着,组件消费的less变量更改为css变量:

.argo-btn.argo-btn-primary {  color: #fff;    background-color: var(--primary-6);}

此外,我们还设置了--color-bg,--color-text等用于设置body色调:

body {  color: var(--color-bg);    background-color: var(--color-text);}

最后,在消费组件库的项目中,通过编辑body的argo-theme属性即可切换亮暗模式:

// 设置为暗黑模式document.body.setAttribute('argo-theme', 'dark')// 恢复亮色模式document.body.removeAttribute('argo-theme');

在线动态换肤

前面介绍的是在项目打包时通过less配置修改less变量值达到换肤效果,有了css变量,我们可以实现在线动态换肤。默认的,打包过后样式如下:

body {  --primary-6: '#3491fa'}.argo-btn {    color: #fff;    background-color: var(--primary-6);}

在用户选择相应颜色后,我们只需要更改css变量--primary-6的值即可:

// 可计算selectedColor的10个颜色梯度值列表,并逐一替换document.body.style.setProperty('--primary-6', colorPalette(selectedColor, 6));// ....

文档站点

还记得每个组件目录下的TEMPLATE.md文件吗?

## zh-CN```yamlmeta:  type: 组件  category: 通用title: 按钮 Buttondescription: 按钮是一种命令组件,可发起一个即时操作。```---## en-US```yamlmeta:  type: Component  category: Commontitle: Buttondescription: Button is a command component that can initiate an instant operation.```---@import ./__demo__/basic.md@import ./__demo__/disabled.md## API%%API(button.vue)%%## TS%%TS(interface.ts)%%

它是如何一步步被渲染出我们想要的界面呢?

vue组件库如何开发使用

vue组件库如何开发使用

TEMPLATE.md的作用

vue组件库如何开发使用

TEMPLATE.md将被解析并生成中英文版READE.md(组件使用文档),之后在vue-router中被加载使用。

这时当我们访问路由/button,vite服务器将接管并调用一系列插件解析成浏览器识别的代码,最后由浏览器渲染出我们的文档界面。

1. 解析TEMPLATE 生成 README

简单起见,我们忽略国际化和使用例子部分。

%%API(button.vue)%%%%INTERFACE(interface.ts)%%

其中button.vue就是我们的组件,interface.ts就是定义组件的一些接口,比如ButtonProps,ButtonType等。

解析button.vue

大致流程如下:

  • 读取TEMPLATE.md,正则匹配出button.vue;

  • 使用vue-doc-api解析vue文件; let componentDocJson = VueDocApi.parse(path.resolve(__dirname, "button.vue"));

  • componentDocJson转换成md字符串,md字符串替换掉占位符%%API(button.vue)%%,写入README.md;

关于vue文件与解析出来的conponentDocJson结构见 vue-docgen-api

解析interface.ts

由于VueDocApi.parse无法直接解析.ts文件,因此借助ts-morph解析ts文件并转换成componentDocJson结构的JSON对象,再将componentDocJson转换成md字符串,替换掉占位符后最终写入README.md;

  • 读取TEMPLATE.md,正则匹配出interface.ts;

  • 使用ts-morph解析inerface.ts出interfaces;

  • interfaces转componentDocJson;

  • componentDocJson转换成md字符串,md字符串替换掉占位符%%API(button.vue)%%,写入README.md;

import { Project } from "ts-morph";const project = new Project();project.addSourceFileAtPath(filepath);const sourceFile = project.getSourceFile(filepath);const interfaces = sourceFile.getInterfaces();const componentDocList = [];interfaces.forEach((interfaceDeclaration) => {  const properties = interfaceDeclaration.getProperties();  const componentDocJson = {    displayName: interfaceDeclaration.getName(),    exportName: interfaceDeclaration.getName(),    props: formatterProps(properties),    tags: {}  };  if (componentDocJson.props.length) {    componentDocList.push(componentDocJson);  }});// genMd(componentDocList);

最终生成README.zh-CN.md如下

```yamlmeta:  type: 组件  category: 通用title: 按钮 Buttondescription: 按钮是一种命令组件,可发起一个即时操作。```@import ./__demo__/basic.md@import ./__demo__/disabled.md## API### `<button>` Props|参数名|描述|类型|默认值||---|---|---|:---:||type|按钮的类型,分为五种:次要按钮、主要按钮、虚框按钮、线性按钮、文字按钮。|`'secondary' | 'primary' | 'dashed' | 'outline' | 'text'`|`"secondary"`||shape|按钮的形状|`'square' | 'round' | 'circle'`|`"square"`||status|按钮的状态|`'normal' | 'warning' | 'success' | 'danger'`|`"normal"`||size|按钮的尺寸|`'mini' | 'small' | 'medium' | 'large'`|`"medium"`||long|按钮的宽度是否随容器自适应。|`boolean`|`false`||loading|按钮是否为加载中状态|`boolean`|`false`||disabled|按钮是否禁用|`boolean`|`false`||html-type|设置 `button` 的原生 `type` 属性,可选值参考 [HTML标准](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#attr-type "_blank")|`'button' | 'submit' | 'reset'`|`"button"`||href|设置跳转链接。设置此属性时,按钮渲染为a标签。|`string`|`-`|### `<button>` Events|事件名|描述|参数||---|---|---||click|点击按钮时触发|event: `Event`|### `<button>` Slots|插槽名|描述|参数||---|:---:|---||icon|图标|-|### `<button-group>` Props|参数名|描述|类型|默认值||---|---|---|:---:||disabled|是否禁用|`boolean`|`false`|## INTERFACE### ButtonProps|参数名|描述|类型|默认值||---|---|---|:---:||type|按钮类型|`ButtonTypes`|`-`|

2. 路由配置

const Button = () => import("@argo-design/argo-ui/components/button/README.zh-CN.md");const router = createRouter({  {    path: "/button",  component: Button  }});export default router;

3. README是如何被渲染成UI的

首先我们来看下README.md(为方便直接省略.zh-CN)以及其中的demos.md的样子与它们最终的UI。

vue组件库如何开发使用

可以看到,README就是一系列demo的集合,而每个demo都会被渲染成一个由代码示例与代码示例运行结果组成的代码块。

开发vite-plugin-vue-docs解析md

yarn create vite快速搭建一个package

// vite.config.tsimport { defineConfig } from "vite";import vue from "@vitejs/plugin-vue";import md from "./plugins/vite-plugin-md/index";export default defineConfig({  server: {    port: 8002,  },  plugins: [md(), vue()],});
// App.vue<template>  <ReadMe /></template><script setup>import ReadMe from "./readme.md";</script>
// readme.md@import ./__demo__/basic.md

开发之前我们先看看插件对README.md源码的解析转换流程。

vue组件库如何开发使用

1. 源码转换

首先我们来实现第一步: 源码转换。即将

@import "./__demo__/basic.md"

转换成

<template>  <basic-demo /></template><script>import { defineComponent } from "vue";import BasicDemo from "./__demo__/basic.md";export default defineComponent({  name: "ArgoMain",  components: { BasicDemo },});</script>

转换过程我们借助第三方markdown解析工具marked完成,一个高速,轻量,无阻塞,多平台的markdown解析器。

众所周知,md2html规范中,文本默认会被解析渲染成p标签。也就是说,README.md里的@import ./__demo__/basic.md会被解析渲染成<p>@import ./__demo__/basic.md</p>,这不是我想要的。所以需要对marked进行一下小小的扩展。

// marked.tsimport { marked } from "marked";import path from "path";const mdImport = {  name: "mdImport",  level: "block",  tokenizer(src: string) {    const rule = /^@import\s+(.+)(?:\n|$)/;    const match = rule.exec(src);    if (match) {      const filename = match[1].trim();      const basename = path.basename(filename, ".md");      return {        type: "mdImport",        raw: match[0],        filename,        basename,      };    }    return undefined;  },  renderer(token: any) {    return `<demo-${token.basename} />\n`;  },};marked.use({  extensions: [mdImport],});export default marked;

我们新建了一个mdImport的扩展,用来自定义解析我们的md。在tokenizer 中我们定义了解析规则并返回一系列自定义的tokens,其中raw就是@import "./__demo__/basic.md",filename就是./__demo__/basic.md,basename就是basic,我们可以通过marked.lexer(code)拿到这些tokens。在renderer中我们自定义了渲染的html,通过marked.parser(tokens)可以拿到html字符串了。因此,我们开始在插件中完成第一步。

// index.tsimport { Plugin } from "vite";import marked from "./marked";export default function vueMdPlugin(): Plugin {  return {    name: "vite:argo-vue-docs",    async transform(code: string, id: string) {      if (!id.endsWith(".md")) {        return null;      }      const tokens = marked.lexer(code);      const html = marked.parser(tokens);      const vueCode = transformMain({ html, tokens });    },  };}
// vue-template.tsimport changeCase from "change-case";import marked from "./marked";export const transformMain = ({  html,  tokens,}: {  html: string;  tokens: any[];}): string => {  const imports = [];  const components = [];  for (const token of tokens) {    const componentName = changeCase.pascalCase(`demo-${token.basename}`);    imports.push(`import ${componentName} from "${token.filename}";`);    components.push(componentName);  }  return `  <template>    ${html}  </template>  <script>import { defineComponent } from "vue";${imports.join("\n")};export default defineComponent({  name: "ArgoMain",  components: { ${components.join(",")} },});</script>`;};

其中change-case是一个名称格式转换的工具,比如basic-demo转BasicDemo等。

transformMain返回的vueCode就是我们的目标vue模版了。但浏览器可不认识vue模版语法,所以我们仍要将其交给官方插件@vitejs/plugin-vuetransform钩子函数转换一下。

import { getVueId } from "./utils";export default function vueMdPlugin(): Plugin {  let vuePlugin: Plugin | undefined;  return {    name: "vite:argo-vue-docs",    configResolved(resolvedConfig) {      vuePlugin = resolvedConfig.plugins.find((p) => p.name === "vite:vue");    },    async transform(code: string, id: string) {      if (!id.endsWith(".md")) {        return null;      }      if (!vuePlugin) {        return this.error("Not found plugin [vite:vue]");      }      const tokens = marked.lexer(code);      const html = marked.parser(tokens);      const vueCode = transformMain({ html, tokens });      return await vuePlugin.transform?.call(this, vueCode, getVueId(id));    },  };}
// utils.tsexport const getVueId = (id: string) => {  return id.replace(".md", ".vue");};

这里使用getVueId修改扩展名为.vue是因为vuePlugin.transform会对非vue文件进行拦截就像我们上面拦截非md文件一样。

configResolved钩子函数中,形参resolvedConfig是vite最终使用的配置对象。在该钩子中拿到其它插件并将其提供给其它钩子使用,是vite插件开发中的一种“惯用伎俩”了。

2. 处理basic.md

在经过vuePlugin.transform及后续处理过后,最终vite服务器对readme.md响应给浏览器的内容如下

vue组件库如何开发使用

对于basic.md?import响应如下

vue组件库如何开发使用

可以看到,这一坨字符串可没有有效的默认导出语句。因此对于解析语句import DemoBasic from "/src/__demo__/basic.md?import";浏览器会报错

Uncaught SyntaxError: The requested module '/src/__demo__/basic.md?import' does not provide an export named 'default' (at readme.vue:9:8)

在带有module属性的script标签中,每个import语句都会向vite服务器发起请求进而继续走到插件的transform钩子之中。下面我们继续,对/src/__demo__/basic.md?import进行拦截处理。

// index.tsasync transform(code: string, id: string) {  if (!id.endsWith(".md")) {    return null;  }  // 新增对demo文档的解析分支  if (isDemoMarkdown(id)) {    const tokens = marked.lexer(code);    const vueCode = transformDemo({ tokens, filename: id });    return await vuePlugin.transform?.call(this, vueCode, getVueId(id));  } else {    const tokens = marked.lexer(code);    const html = marked.parser(tokens);    const vueCode = transformMain({ html, tokens });    return await vuePlugin.transform?.call(this, vueCode, getVueId(id));  }},
// utils.tsexport const isDemoMarkdown = (id: string) => {  return //__demo__//.test(id);};
// vue-template.tsexport const transformDemo = ({  tokens,  filename,}: {  tokens: any[];  filename: string;}) => {  const data = {    html: "",  };  const vueCodeTokens = tokens.filter(token => {    return token.type === "code" && token.lang === "vue"  });  data.html = marked.parser(vueCodeTokens);  return `  <template>    <hr />    ${data.html}  </template>  <script>import { defineComponent } from "vue";export default defineComponent({  name: "ArgoDemo",});</script>`;};

现在已经可以在浏览器中看到结果了,水平线和示例代码。

vue组件库如何开发使用

3. 虚拟模块

那如何实现示例代码的运行结果呢?其实在对tokens遍历(filter)的时候,我们是可以拿到vue模版字符串的,我们可以将其缓存起来,同时手动构造一个import请求import Result from "${virtualPath}";这个请求用于返回运行结果。

export const transformDemo = ({  tokens,  filename,}: {  tokens: any[];  filename: string;}) => {  const data = {    html: "",  };  const virtualPath = `/@virtual${filename}`;  const vueCodeTokens = tokens.filter(token => {    const isValid = token.type === "code" && token.lang === "vue"    // 缓存vue模版代码    isValid && createDescriptor(virtualPath, token.text);    return isValid;  });  data.html = marked.parser(vueCodeTokens);  return `  <template>    <Result />    <hr />    ${data.html}  </template>  <script>import { defineComponent } from "vue";import Result from "${virtualPath}";export default defineComponent({  name: "ArgoDemo",  components: {    Result  }});</script>`;};
// utils.tsexport const isVirtualModule = (id: string) => {  return //@virtual/.test(id);};
export default function docPlugin(): Plugin {  let vuePlugin: Plugin | undefined;  return {    name: "vite:plugin-doc",    resolveId(id) {      if (isVirtualModule(id)) {        return id;      }      return null;    },    load(id) {      // 遇到虚拟md模块,直接返回缓存的内容      if (isVirtualModule(id)) {        return getDescriptor(id);      }      return null;    },    async transform(code, id) {      if (!id.endsWith(".md")) {        return null;      }      if (isVirtualModule(id)) {        return await vuePlugin.transform?.call(this, code, getVueId(id));      }      // 省略其它代码...    }  }}
// cache.tsconst cache = new Map();export const createDescriptor = (id: string, content: string) => {  cache.set(id, content);};export const getDescriptor = (id: string) => {  return cache.get(id);};

vue组件库如何开发使用

最后为示例代码加上样式。安装prismjs

yarn add prismjs
// marked.tsimport Prism from "prismjs";import loadLanguages from "prismjs/components/index.js";const languages = ["shell", "js", "ts", "jsx", "tsx", "less", "diff"];loadLanguages(languages);marked.setOptions({  highlight(    code: string,    lang: string,    callback?: (error: any, code?: string) => void  ): string | void {    if (languages.includes(lang)) {      return Prism.highlight(code, Prism.languages[lang], lang);    }    return Prism.highlight(code, Prism.languages.html, "html");  },});

项目入口引入css

// main.tsimport "prismjs/themes/prism.css";

重启预览,以上就是vite-plugin-vue-docs的核心部分了。

vue组件库如何开发使用

遗留问题

最后回到上文构建组件style/index.ts遗留的问题,index.ts的内容很简单,即引入组件样式。

import "../../style/index.less"; // 全局样式import "./index.less"; // 组件样式复制代码

index.ts在经过vite的lib模式构建后,我们增加css插件,在generateBundle钩子中,我们可以对最终的bundle进行新增,删除或修改。通过调用插件上下文中emitFile方法,为我们额外生成用于引入css样式的css.js。

import type { Plugin } from "vite";import { OutputChunk } from "rollup";export default function cssjsPlugin(): Plugin {  return {    name: "vite:cssjs",    async generateBundle(outputOptions, bundle) {      for (const filename of Object.keys(bundle)) {        const chunk = bundle[filename] as OutputChunk;        this.emitFile({          type: "asset",          fileName: filename.replace("index.js", "css.js"),          source: chunk.code.replace(/.less/g, ".css")        });      }    }  };}

读到这里,这篇“vue组件库如何开发使用”文章已经介绍完毕,想要掌握这篇文章的知识点还需要大家自己动手实践使用过才能领会,如果想了解更多相关内容的文章,欢迎关注编程网精选频道。

--结束END--

本文标题: vue组件库如何开发使用

本文链接: https://www.lsjlt.com/news/347578.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

本篇文章演示代码以及资料文档资料下载

下载Word文档到电脑,方便收藏和打印~

下载Word文档
猜你喜欢
  • vue组件库如何开发使用
    本文小编为大家详细介绍“vue组件库如何开发使用”,内容详细,步骤清晰,细节处理妥当,希望这篇“vue组件库如何开发使用”文章能帮助大家解决疑惑,下面跟着小编的思路慢慢深入,一起来学习新知识吧。yarn workspace + lerna:...
    99+
    2023-07-04
  • 如何使用React进行组件库开发
    小编给大家分享一下如何使用React进行组件库开发,相信大部分人都还不怎么了解,因此分享这篇文章给大家参考一下,希望大家阅读完这篇文章后大有收获,下面让我们一起去了解一下吧!概述我们都知道,组件化的开发模式...
    99+
    2024-04-02
  • 如何使用Flex组件开发
    这篇文章主要为大家展示了“如何使用Flex组件开发”,内容简而易懂,条理清晰,希望能够帮助大家解决疑惑,下面让小编带领大家一起研究并学习一下“如何使用Flex组件开发”这篇文章吧。使用Flex组件开发在Flex应用程序中ActionScri...
    99+
    2023-06-17
  • Vue 开发工具:如何创建和使用自定义组件?
    ...
    99+
    2024-04-02
  • Element一套优雅的Vue 2组件库是如何开发的
    这期内容当中小编将会给大家带来有关Element一套优雅的Vue 2组件库是如何开发的,文章内容丰富且以专业的角度为大家分析和叙述,阅读完这篇文章希望大家可以有所收获。初期也遇到一些棘手的问题,很庆幸都找到...
    99+
    2024-04-02
  • Vue高效UI组件库—iView开发实践
    iView是一个基于Vue.js的高效UI组件库,它提供了丰富的UI组件和工具,可以帮助开发者快速构建高质量的Web应用。下面是一些...
    99+
    2023-09-21
    Vue
  • vue如何开发一个加载Button组件
    本篇内容介绍了“vue如何开发一个加载Button组件”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!组件背景点击按钮时请求一些接口数据,而为...
    99+
    2023-06-30
  • VUE 组件库大PK:盘点热门组件库,助力快速开发
    随着前端技术的发展,组件库作为前端开发的利器,深受广大开发者的喜爱。组件库可以帮助开发者快速搭建出 UI 界面,降低开发成本,提高开发效率。市面上有很多优秀的 VUE 组件库,本文将盘点目前热门的 VUE 组件库,并对比它们的优缺点,帮...
    99+
    2024-02-25
    VUE 组件库 Element UI Ant Design Vue iview vuetify
  • vue组件库怎么使用
    今天小编给大家分享一下vue组件库怎么使用的相关知识点,内容详细,逻辑清晰,相信大部分人都还太了解这方面的知识,所以分享这篇文章给大家参考一下,希望大家阅读完这篇文章后有所收获,下面我们一起来了解一下吧。完整项目目录结构git clone到...
    99+
    2023-07-04
  • 如何使用Vite+React进行组件开发
    本篇内容介绍了“如何使用Vite+React进行组件开发”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成! ...
    99+
    2024-04-02
  • 如何使用单文件组件.vue
    小编给大家分享一下如何使用单文件组件.vue,相信大部分人都还不怎么了解,因此分享这篇文章给大家参考一下,希望大家阅读完这篇文章后大有收获,下面让我们一起去了解一下吧!vuejs 自定义了一种.vue文件,...
    99+
    2024-04-02
  • 如何开发一个封装iframe的vue组件
    这篇文章给大家分享的是有关如何开发一个封装iframe的vue组件的内容。小编觉得挺实用的,因此分享给大家做个参考,一起跟随小编过来看看吧。VUE的基本组成单元,我看应该是组件。用VUE开发前端项目,就是开发一个个组件,然后搭积木一样,将项...
    99+
    2023-06-14
  • Vue+Element switch组件如何使用
    这篇“Vue+Element switch组件如何使用”文章的知识点大部分人都不太理解,所以小编给大家总结了以下内容,内容详细,步骤清晰,具有一定的借鉴价值,希望大家阅读完这篇文章能有所收获,下面我们一起来看看这篇“Vue+Ele...
    99+
    2023-07-02
  • Vue组件模板如何使用
    本篇内容主要讲解“Vue组件模板如何使用”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“Vue组件模板如何使用”吧!1. vue组件都是由这三部分组成<template> &...
    99+
    2023-07-04
  • Vue组件如何创建使用
    这篇文章主要介绍“Vue组件如何创建使用”的相关知识,小编通过实际案例向大家展示操作过程,操作方法简单快捷,实用性强,希望这篇“Vue组件如何创建使用”文章能帮助大家解决问题。一、组件组件是vue的重要的特征之一,可以扩展html的功能,也...
    99+
    2023-07-05
  • Vue中如何使用header组件
    这期内容当中小编将会给大家带来有关Vue中如何使用header组件,文章内容丰富且以专业的角度为大家分析和叙述,阅读完这篇文章希望大家可以有所收获。一、 header 组件开发 之数据的传递1. App.v...
    99+
    2024-04-02
  • vue如何使用高阶组件
    这篇文章主要为大家展示了“vue如何使用高阶组件”,内容简而易懂,条理清晰,希望能够帮助大家解决疑惑,下面让小编带领大家一起研究并学习一下“vue如何使用高阶组件”这篇文章吧。高阶组件1 一般情况//&nb...
    99+
    2024-04-02
  • Vue中如何使用component组件
    Vue中如何使用component组件,很多新手对此不是很清楚,为了帮助大家解决这个难题,下面小编将为大家详细讲解,有这方面需求的人可以来学习下,希望你能有所收获。 内置的组件component场...
    99+
    2024-04-02
  • vue弹窗组件如何使用
    这篇文章给大家分享的是有关vue弹窗组件如何使用的内容。小编觉得挺实用的,因此分享给大家做个参考,一起跟随小编过来看看吧。示例<!DOCTYPE html> <html ...
    99+
    2024-04-02
  • vue动态组件如何使用
    这篇文章主要介绍“vue动态组件如何使用”,在日常操作中,相信很多人在vue动态组件如何使用问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”vue动态组件如何使用”的疑惑有所帮助!接下来,请跟着小编一起来学习吧...
    99+
    2023-06-29
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作