UI Library

分析下我见过的组件库

Element Plus

Vue3组件库 pnpm monorepo

image.png

  • components: 组件目录
  • constants: 常量目录
  • directives: 自定义指令目录
  • element-plus:
  • hooks: 抽取出能通用的hooks
  • locale: 国际化
  • test-utils: 一些测试方法的封装
  • theme-chalk: 组件的样式文件
  • utils: 通用方法封装

文档系统

使用vitepress作为文档系统,大部分页面样式都重新写组件

处理一

通过markdown-it-container,注入:::demo方式

image.png

在md文档中就能直接展示组件的demo

image.png

image.png

处理二

自定义md插件,识别标题后是否有() ,用于做额外展示

export default (md: MarkdownIt): void => {
  /**
   * To enable the plugin to be parsed in the demo description, the content is rendered as span instead of ElTag.
   */
  md.renderer.rules.tag = (tokens, idx) => {
    const token = tokens[idx]
    const value = token.content
    /**
     * Add styles for some special tags
     * vitepress/styles/content/tag-mark-content.scss
     */
    const tagClass = ['beta', 'deprecated', 'a11y'].includes(value) ? value : ''
    return `<span class="vp-tag ${tagClass}">${value}</span>`
  }

  md.inline.ruler.before('emphasis', 'tag', (state, silent) => {
    const tagRegExp = /^\^\(([^)]*)\)/
    const str = state.src.slice(state.pos, state.posMax)

    if (!tagRegExp.test(str)) return false
    if (silent) return true

    const result = str.match(tagRegExp)

    if (!result) return false

    const token = state.push('tag', 'tag', 0)
    token.content = result[1].trim()
    token.level = state.level
    state.pos += result[0].length

    return true
  })
}

可以看到,先走到before,识别标题后是否有(),如果有,拿出里面的值,存到token里,执行下一步

接着执行上面的tag方法,拿出括号的值,换成一个span标签

## Tag ^(2.3.4)

## Custom Color ^(beta)

image.png

处理三

给table包一层div,定义table样式

export default (md: MarkdownIt): void => {
  md.renderer.rules.table_open = () => '<div class="vp-table"><table>'
  md.renderer.rules.table_close = () => '</table></div>'
}

处理四

定义vite插件MarkdownTransform,对于.md文件做一些处理,比如在每个md底部添加源码链接,贡献者组件

const transformComponentMarkdown = (
  id: string,
  componentId: string,
  code: string,
  append: Append
) => {
  const lang = getLang(id)
  const docUrl = `${GITHUB_BLOB_URL}/${docsDirName}/en-US/component/${componentId}.md`
  const componentUrl = `${GITHUB_TREE_URL}/packages/components/${componentId}`
  const componentPath = path.resolve(
    projRoot,
    `packages/components/${componentId}`
  )
  const isComponent = fs.existsSync(componentPath)

  const links = [[footerLocale[lang].docs, docUrl]]
  if (isComponent) links.unshift([footerLocale[lang].component, componentUrl])
  const linksText = links
    .filter((i) => i)
    .map(([text, link]) => `[${text}](${link})`)
    .join('')

  const sourceSection = `
## ${footerLocale[lang].source}

${linksText}
`

  const contributorsSection = `
## ${footerLocale[lang].contributors}

<Contributors id="${componentId}" />
`

  append.footers.push(sourceSection, isComponent ? contributorsSection : '')

  return code
}

打包系统

Element Plus用的是gulp执行build命令

gulp: A toolkit to automate & enhance your workflow 翻译过来就是一个控制工作流的工具

// 主命令
"build": "pnpm run -C internal/build start",

// 就是执行以下命令
"start": "gulp --require @esbuild-kit/cjs-loader -f gulpfile.ts",

开发就是用的vite,打开playground

"dev": "pnpm -C play dev",

"dev": "vite",

打包的gulp执行过程是

image.png

gulp作为控制流工具,只要执行了tasks目录里的任务,里面的主要任务是通过rollup进行打包

image.png

打包格式包括esmcjs

image.png

有两种打包产物,一种是全量打包,一种是按模块打包

image.png

全量打包的产物在dist目录下,包含整个elmp的代码,适合全量引入比较省事

全量打包还分full和full.min,.min通过rollup插件rollup-plugin-esbuild导出的的plugin minify实现

按模块打包可以将每个组件单独打包出一份产物,对按需引入减小代码产物大小很有帮助

elmp结合unplugin-vue-components实现按需引入,配上内置的resolver即可

规范的目录结构有利于按需引入的实现,比如当我使用了ElButton组件后,unplugin-vue-components插件的resolvers会返回

{
  type: 'component',
  resolve: {
    name: 'button',
    from: 'element-plus/es',
    sideEffects: [
      'element-plus/es/components/base/style/css',
      'element-plus/es/components/button/style/css',
    ]
  }
}

这样就能拿到button的内容和样式了,就能注入vue页面当中

TDesign

Updated on 5/11/2023