从0到1搭建一个组件库-第7节课

 视频参考:

【我要做开源】华为大佬亲授,Vue DevUI开源指南07:大串讲,项目创建+配置+文档系统+组件开发+测试,mini-vue-devui欢迎star!_哔哩哔哩_bilibili

参考文档:

手把手带你从0到1搭建一个vue3组件库:mini-vue-devui - 掘金

【我要做开源】给 vue devui 组件库项目增加单元测试 - 掘金

我的gitee:

https://gitee.com/shaoxiaohao/component-library-construction


7.这节课把之前学到的都串联起来(我是在之前的文档上继续的)

1、搭建一个支持TypeScript/JSXVue3组件库工程(4)

2、增加能展开/收起的tree组件(2)

3、增加VitePress文档系统(5)

4、增加demo代码展开/收起功能(5)

5、搭建DevUI CLI快速创建组件模板(6)

6、单元测试(3)(这一步能力有限,后续更新)

由于Young老师和Kagol老师是纯纯的新建的项目,

我是在原来的项目基础上建的,所以这里重点更新在原来的项目上需要更新的地方

1 搭建一个支持TypeScript/JSXVue3组件库工程

参考:

2 增加能展开/收起的tree组件

这一步需要按照这个目录结构建一些文件和文件夹,主要是针对tree这个大的组件的基石的建立

├── devui
|  └── tree
|     ├── index.ts
|     └── src
|        ├── components
|        |  ├── icon-close.tsx
|        |  └── icon-open.tsx
|        ├── composables
|        |  └── use-toggle.ts
|        ├── tree-types.ts
|        ├── tree.scss
|        └── tree.tsx
复制代码

然后往文件里添加文件

tree/index.ts  tree的入口文件  

tree/index.ts

import type { App } from 'vue'
import Tree from './src/tree'

Tree.install = function(app: App): void {
  app.component(Tree.name, Tree)
}

export { Tree }

export default {
  title: 'Tree 树',
  category: '数据展示',
  status: '20%',
  install(app: App): void {
    app.use(Tree as any)
  }
}

复制代码

tree/src/tree.tsx  组件源文件

import { defineComponent, toRefs } from 'vue'
import { treeProps, TreeProps, TreeData, TreeItem } from './tree-types'
import IconOpen from './components/icon-open'
import IconClose from './components/icon-close'
import useToggle from './composables/use-toggle'
import './tree.scss'

export default defineComponent({
  name: 'DTree',
  props: treeProps,
  emits: [],
  setup(props: TreeProps, ctx) {
    const { data } = toRefs(props)
    const { openedData, toggle } = useToggle(data.value)

    // 增加缩进的展位元素
    const Indent = () => {
      return <span style="display: inline-block; width: 16px; height: 16px;"></span>
    }

    const renderNode = (item: TreeItem) => {
      return (
        <div
          class={['devui-tree-node', item.open && 'devui-tree-node__open']}
          style={{ paddingLeft: `${24 * (item.level - 1)}px` }}
        >
          <div class="devui-tree-node__content">
            <div class="devui-tree-node__content--value-wrapper">
              {
                item.children
                  ? item.open
                    ? <IconOpen class="mr-xs" onClick={() => toggle(item)} /> // 给节点绑定点击事件
                    : <IconClose class="mr-xs" onClick={() => toggle(item)} /> // 给节点绑定点击事件
                  : <Indent />
              }
              <span class="devui-tree-node__title">{ item.label }</span>
            </div>
          </div>
        </div>
      )
    }    

    const renderTree = (tree: TreeData): JSX.Element[] => {
      return tree.map(item => {
        if (!item.children) {
          return renderNode(item)
        } else {
          return (
            <>
              {renderNode(item)}
              {renderTree(item.children)}
            </>
          )
        }
      })
    }

    return () => {
      return (
        <div class="devui-tree">
          { openedData.value.map((item: TreeItem) => renderNode(item)) }
        </div>
      )
    }
  }
})

复制代码

tree-types.ts   组件props和类型文件

tree-types.ts

import type { PropType, ExtractPropTypes } from 'vue'

export interface TreeItem {
  label: string
  children: TreeData
  [key: string]: any
}

export type TreeData = Array<TreeItem>;

export const treeProps = {
  data: {
    type: Array as PropType<TreeData>,
    default: () => [],
  }
} as const

export type TreeProps = ExtractPropTypes<typeof treeProps>

复制代码

use-toggle.ts  展开/收起的hooks

use-toggle.ts

import { ref } from 'vue'
import { TreeData, TreeItem } from '../tree-types'

export default function useToggle(data: TreeData): any {
  const openedTree = (tree: any) => {
    return tree.reduce((acc: TreeItem, item: TreeItem) => (
      item.open
        ? acc.concat(item, openedTree(item.children))
        : acc.concat(item)
    ), [])
  }

  const openedData = ref(openedTree(data)) // 响应式对象

  const toggle = (item: TreeItem) => {
    if (!item.children) return
    item.open = !item.open
    openedData.value = openedTree(data)
  }

  return {
    openedData,
    toggle,
  }
}
复制代码

icon-close.tsx   图标关闭组件

const IconClose = (props: any) => {
  return (
    <svg
      width="16px"
      height="16px"
      viewBox="0 0 16 16"
      version="1.1"
      xmlns="http://www.w3.org/2000/svg"
      class={["svg-icon", props.class]}
    >
      <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
        <rect x="0.5" y="0.5" width="15" height="15" rx="2" stroke="#252b3a"></rect>
        <path
          fill="#252b3a"
          d="M8.75,4 L8.75,7.25 L12,7.25 L12,8.75 L8.749,8.75 L8.75,12 L7.25,12 L7.249,8.75 L4,8.75 L4,7.25 L7.25,7.25 L7.25,4 L8.75,4 Z"
        ></path>
      </g>
    </svg>
  )
}

export default IconClose
复制代码

icon-open.tsx   图标开启组件

const IconOpen = (props: any) => {
  return (
    <svg
      width="16px"
      height="16px"
      viewBox="0 0 16 16"
      version="1.1"
      xmlns="http://www.w3.org/2000/svg"
      class={["svg-icon svg-icon-close", props.class]}
    >
      <g stroke-width="1" fill="none" fill-rule="evenodd">
        <rect x="0.5" y="0.5" width="15" height="15" rx="2" stroke="#5e7ce0"></rect>
        <rect x="4" y="7" width="8" height="2" fill="#5e7ce0"></rect>
      </g>
    </svg>
  )
}

export default IconOpen
复制代码

文档

目录结构  docs里的md文档,相当于官方文档源文件的地方。

├── docs
|  ├── components
|  |  └── tree
|  |     └── index.md
复制代码

tree/index.md

<template>
  <d-tree :data="data"></d-tree>
</template>

<script lang="ts">
import { defineComponent, ref } from 'vue'

export default defineComponent({
  setup() {
    const data = ref([{
      label: '一级 1', level: 1,
      children: [{
        label: '二级 1-1', level: 2,
        children: [{
          label: '三级 1-1-1', level: 3,
        }]
      }]
    }, {
      label: '一级 2', level: 1,
      open: true, // 新增
      children: [{
        label: '二级 2-1', level: 2,
        children: [{
          label: '三级 2-1-1', level: 3,
        }]
      }, {
        label: '二级 2-2', level: 2,
        children: [{
          label: '三级 2-2-1', level: 3,
        }]
      }]
    }, {
      label: '一级 3', level: 1,
      open: true, // 新增
      children: [{
        label: '二级 3-1', level: 2,
        children: [{
          label: '三级 3-1-1', level: 3,
        }]
      }, {
        label: '二级 3-2', level: 2,
        open: true, // 新增
        children: [{
          label: '三级 3-2-1', level: 3,
        }]
      }]
    }, {
      label: '一级 4', level: 1,
    }])

    return {
      data,
    }
  }
})
</script>
复制代码

main.ts    引入tree组件

import { createApp } from 'vue'
import App from './App.vue'
import Tree from '../devui/tree'

createApp(App)
.use(Tree)
.mount('#app')

使用

docs/components/tree/index.md

# Tree 树

:::demo 渲染一棵基本树

```vue
<template>
  <d-tree :data="data"></d-tree>
</template>

<script lang="ts">
import { defineComponent, ref } from 'vue'

export default defineComponent({
  setup() {
    const data = ref([{
      label: '一级 1', level: 1,
      children: [{
        label: '二级 1-1', level: 2,
        children: [{
          label: '三级 1-1-1', level: 3,
        }]
      }]
    }, {
      label: '一级 2', level: 1,
      open: true, // 新增
      children: [{
        label: '二级 2-1', level: 2,
        children: [{
          label: '三级 2-1-1', level: 3,
        }]
      }, {
        label: '二级 2-2', level: 2,
        children: [{
          label: '三级 2-2-1', level: 3,
        }]
      }]
    }, {
      label: '一级 3', level: 1,
      open: true, // 新增
      children: [{
        label: '二级 3-1', level: 2,
        children: [{
          label: '三级 3-1-1', level: 3,
        }]
      }, {
        label: '二级 3-2', level: 2,
        open: true, // 新增
        children: [{
          label: '三级 3-2-1', level: 3,
        }]
      }]
    }, {
      label: '一级 4', level: 1,
    }])

    return {
      data
    }
  }
})
</script>
:::

​​​​3 增加VitePress文档系统

安装vitepress依赖

yarn add -D vitepress
复制代码

编写docs/index.md文档

docs/index.md

# Hello VitePress
复制代码

编写脚本命令

{
  "scripts": {
    "dev": "vite",
    "build": "vue-tsc --noEmit && vite build",
    "serve": "vite preview",
    "docs:dev": "vitepress dev docs", // 新增
    "docs:build": "vitepress build docs", // 新增
    "docs:serve": "vitepress serve docs" // 新增
  }
}
复制代码

配置JSX

docs/vite.config.ts

import { defineConfig } from 'vite'
import vueJsx from '@vitejs/plugin-vue-jsx'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [vueJsx()]
})
复制代码

配置左侧菜单sidebar

docs/.vitepress/config.ts

const sidebar = {
  '/': [
    { text: '快速开始', link: '/' },
    {
      text: '通用'
    },
    {
      text: '导航',
    },
    {
      text: '反馈',
    },
    {
      text: '数据录入',
    },
    {
      text: '数据展示',
      children: [
        { text: 'Tree 树', link: '/components/tree/' },
      ]
    },
    {
      text: '布局',
    },
  ]
}

const config = {
  themeConfig: {
    sidebar,
  }
}

export default config
复制代码

引入tree组件

docs/.vitepress/theme/index.ts

import Theme from 'vitepress/dist/client/theme-default'
import Tree from '../../../devui/tree'

export default {
  ...Theme,
  enhanceApp({ app }) {
    app.use(Tree)
  }
}
复制代码

编写tree组件的md文档

docs/components/tree/index.md

# Tree 树

<d-tree :data="data"></d-tree>

<script lang="ts">
import { defineComponent, ref } from 'vue'

export default defineComponent({
  setup() {
    const data = ref([{
      label: '一级 1', level: 1,
      children: [{
        label: '二级 1-1', level: 2,
        children: [{
          label: '三级 1-1-1', level: 3,
        }]
      }]
    }, {
      label: '一级 2', level: 1,
      open: true, // 新增
      children: [{
        label: '二级 2-1', level: 2,
        children: [{
          label: '三级 2-1-1', level: 3,
        }]
      }, {
        label: '二级 2-2', level: 2,
        children: [{
          label: '三级 2-2-1', level: 3,
        }]
      }]
    }, {
      label: '一级 3', level: 1,
      open: true, // 新增
      children: [{
        label: '二级 3-1', level: 2,
        children: [{
          label: '三级 3-1-1', level: 3,
        }]
      }, {
        label: '二级 3-2', level: 2,
        open: true, // 新增
        children: [{
          label: '三级 3-2-1', level: 3,
        }]
      }]
    }, {
      label: '一级 4', level: 1,
    }])

    return {
      data
    }
  }
})
</script>
复制代码

参考:

  1. 【我要做开源】Vue DevUI开源指南05:给Vue3组件库添加VitePress文档系统

4 增加demo代码展开/收起功能

安装vitepress-theme-demoblock依赖

yarn add -D vitepress-theme-demoblock
复制代码

配置 demoBlockPlugin

docs/.vitepress/config.ts

import { demoBlockPlugin } from 'vitepress-theme-demoblock'

const config = {
  themeConfig: {
    sidebar,
  },
  
  // 以下是新增的
  markdown: {
    config: (md) => {
      // 这里可以使用 markdown-it 插件,vitepress-theme-demoblock就是基于此开发的
      md.use(demoBlockPlugin)
    }
  }
}
复制代码

配置 vitepress-rc 脚本命令

自动生成docs/.vitepress/theme/register-components.js

"register:components": "vitepress-rc"
复制代码

注册Demo/DemoBlock组件

docs/.vitepress/theme/index.ts

import Theme from 'vitepress/dist/client/theme-default'
import Tree from '../../../devui/tree'

// 新增
// 主题样式
import 'vitepress-theme-demoblock/theme/styles/index.css'
// 插件的组件,主要是demo组件
import { registerComponents } from './register-components.js'

export default {
  ...Theme,
  enhanceApp({ app }) {
    app.use(Tree)
    
    // 新增
    registerComponents(app)
  }
}
复制代码

编写demo展开/收起的md文档

docs/components/tree/index.md

# Tree 树

:::demo 渲染一棵基本树

```vue
<template>
  <d-tree :data="data"></d-tree>
</template>

<script lang="ts">
import { defineComponent, ref } from 'vue'

export default defineComponent({
  setup() {
    const data = ref([{
      label: '一级 1', level: 1,
      children: [{
        label: '二级 1-1', level: 2,
        children: [{
          label: '三级 1-1-1', level: 3,
        }]
      }]
    }, {
      label: '一级 2', level: 1,
      open: true, // 新增
      children: [{
        label: '二级 2-1', level: 2,
        children: [{
          label: '三级 2-1-1', level: 3,
        }]
      }, {
        label: '二级 2-2', level: 2,
        children: [{
          label: '三级 2-2-1', level: 3,
        }]
      }]
    }, {
      label: '一级 3', level: 1,
      open: true, // 新增
      children: [{
        label: '二级 3-1', level: 2,
        children: [{
          label: '三级 3-1-1', level: 3,
        }]
      }, {
        label: '二级 3-2', level: 2,
        open: true, // 新增
        children: [{
          label: '三级 3-2-1', level: 3,
        }]
      }]
    }, {
      label: '一级 4', level: 1,
    }])

    return {
      data
    }
  }
})
</script>
//```
:::
复制代码

参考:

  1. 【我要做开源】Vue DevUI开源指南05:给Vue3组件库添加VitePress文档系统

5 搭建DevUI CLI快速创建组件模板

安装依赖

yarn add -D commander inquirer fs-extra kolorist esbuild
复制代码

开发命令脚本

devui-cli/index.js

#!/usr/bin/env node
import { Command } from 'commander'
import { onCreate } from './commands/create'

// 创建命令对象
const program = new Command()

// 注册命令、参数、回调
program
  // 注册 create 命令
  .command('create')
  // 添加命令描述
  .description('创建一个组件模板或配置文件')
  // 添加命令参数 -t | --type <type> ,<type> 表示该参数必填,[type] 表示选填
  .option('-t --type <type>', `创建类型,可选值:component, lib-entry`)
  // 注册命令回调
  .action(onCreate)

// 执行命令行参数解析
program.parse()
复制代码

devui-cli/commands/create.js

import inquirer from 'inquirer'
import { red } from 'kolorist'

// create type 支持项
const CREATE_TYPES = ['component', 'lib-entry']
// 文档分类
const DOCS_CATEGORIES = ['通用', '导航', '反馈', '数据录入', '数据展示', '布局']

export async function onCreate(cmd = {}) {
  let { type } = cmd

  // 如果没有在命令参数里带入 type 那么就询问一次
  if (!type) {
    const result = await inquirer.prompt([
      {
        // 用于获取后的属性名
        name: 'type',
        // 交互方式为列表单选
        type: 'list',
        // 提示信息
        message: '(必填)请选择创建类型:',
        // 选项列表
        choices: CREATE_TYPES,
        // 默认值,这里是索引下标
        default: 0
      }
    ])
    // 赋值 type
    type = result.type
  }

  // 如果获取的类型不在我们支持范围内,那么输出错误提示并重新选择
  if (CREATE_TYPES.every((t) => type !== t)) {
    console.log(
      red(`当前类型仅支持:${CREATE_TYPES.join(', ')},收到不在支持范围内的 "${type}",请重新选择!`)
    )
    return onCreate()
  }

  try {
    switch (type) {
      case 'component':
        // 如果是组件,我们还需要收集一些信息
        const info = await inquirer.prompt([
          {
            name: 'name',
            type: 'input',
            message: '(必填)请输入组件 name ,将用作目录及文件名:',
            validate: (value) => {
              if (value.trim() === '') {
                return '组件 name 是必填项!'
              }
              return true
            }
          },
          {
            name: 'title',
            type: 'input',
            message: '(必填)请输入组件中文名称,将用作文档列表显示:',
            validate: (value) => {
              if (value.trim() === '') {
                return '组件名称是必填项!'
              }
              return true
            }
          },
          {
            name: 'category',
            type: 'list',
            message: '(必填)请选择组件分类,将用作文档列表分类:',
            choices: DOCS_CATEGORIES,
            default: 0
          }
        ])

        createComponent(info)
        break
      case 'lib-entry':
        createLibEntry()
        break
      default:
        break
    }
  } catch (e) {
    console.log(red('✖') + e.toString())
    process.exit(1)
  }
}

function createComponent(info) {
  // 输出收集到的组件信息
  console.log(info)
}

function createLibEntry() {
  console.log('create lib-entry file.')
}
复制代码

添加脚本命令

package.json

{
    // --bundle 标识打包的入口文件
    // --format 转换为目标格式代码
    // --platform 目标平台,默认 browser
    // --outdir 输出目录
    // 开发时实时编译
    "dev": "esbuild --bundle ./src/index.js --format=cjs --platform=node --outdir=./lib --watch",
    // 打包命令
    "build": "esbuild --bundle ./src/index.js --format=cjs --platform=node --outdir=./lib",
    // 执行 create 命令,如果有多个命令,可以去掉 create ,使用时再传入
    "cli": "node ./lib/index.js create"
}
复制代码

交互模式执行:

yarn cli
复制代码

带参数直接执行:

yarn cli -t component // -t 是 --type 的别名

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值