Featured image of post Vue.js 3 UI 组件库项目总结

Vue.js 3 UI 组件库项目总结

前言

这几天在学习 Vue 3,通过实操一个 UI 组件库项目来学习 Vue 3,使用了全新的 Vite 2 进行构建,使用 TypeScript 编写,用到了 Vue 3 的 script setup 语法糖、Composition APIWatchEffect 等等。

最后通过将组件库发布到 npm,顺便学习了发布和管理 npm 包的流程,目前项目阶段性完成,对过程中遇到的有些东西进行一个总结。

可以点击 Desn-UI 官网 进行预览。项目仓库

搭建项目

Vite 2 的速度

体验了 Vite 以提供原生 ESM 方式启动开发服务器,速度堪称闪电,怪不得 Logo 带了一个闪电图标。启动一个开发服务器从来没有超过过 2 秒,修改的视图更新也几乎同时响应在页面里,大大提高了开发体验。

搭建项目

使用 yarn create vite 项目名 --template vue-tsnpm create vite@latest 项目名 --template vue-ts,等待几秒就完成了模版下载。

这时候进入创建的目录,yarn install 安装依赖包,安装完成就可以运行 yarn dev 启动开发服务器了。

得到一个地址,浏览器打开它。会得到一个全新的欢迎页 Hello Vue 3 + TypeScript + Vite,这时候就可以进行开发了。

Vite 2 + Vue 3 项目结构

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
    项目目录
      ├╴node_modules
      │  └╴   #各种依赖包
      ├╴public
      │  └╴favicon.ico  #网站图标
      ├╴src
      │  ├╴assets
      │  │  └╴  #静态资源目录
      │  ├╴components
      │  │  └╴  #vue 组件目录
      │  ├╴main.ts  #项目主文件
      │  ├╴env.d.ts  #ts 声明文件
      │  └╴App.vue   #vue 主文件
      ├╴index.html   #html 模版
      ├╴tsconfig.json   #ts 配置文件
      ├╴vite.config.ts   #vite 配置文件
      ├╴package.json   #依赖包配置文件
      └╴README.md   #项目说明

相比 Vue 2,这个目录结构是简洁了太多了,对于我这种强迫症,目录清晰简洁真的太舒服了。这时候就可以开始开发自己的项目了。

script setup

不得不说,Vue 3 这个语法糖真的好用。写起来简洁,在模版使用不用 return,组件引入就能使用,大大节省了重复的 export defaultreturn。并且 <script setup> 中的代码会在 每次组件实例被创建的时候执行

简单示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
    <template>
      <Button @click="onClick">一个按钮</Button>
    </template>
    
    <script lang="ts" setup>
      import Button from './component/Button.vue'
      
      const onClick = () => {
        console.log('click')
      }
    </script>

不过这里使用 script setup 的话,有些东西的写法就会和写在 setup 函数不同了。

props 和 emits

不使用语法糖的话,props 是写在 export default 里面,而 emits 写在 setup 函数里,需要从 context 使用,并且最后需要 return 出来才可以在模版中使用。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
    export default {
      props:{
        color:{
          type: String,
          default: '#FFF',
          required: true
        }
      },
      setup(props,context){
        const onCLick = () => {
          context.emit('update:value', newValue)
        }
        
        return{
          onClick
        }
      }
    }

而使用 script setup 语法糖的话,就特别简单了。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
    const props = defineProps({
      color:{
          type: String,
          default: '#FFF',
          required: true
        }
    })
    const emits = defineEmits(['click'])
    const onClick = () => {
      emits('click', newValue)
    }

可以看到,代码行数少了不少,括号也少了不少,整体看起来会清晰很多。

ref

响应式状态需要明确使用响应式 APIs 来创建。和从 setup() 函数中返回值一样,ref 值在模板中使用的时候会自动解包,以下示例中模版会随着 count 的变化而更新。

1
2
3
4
5
6
7
8
9
  <template>
    <button @click="count++">{{ count }}</button>
  </template>
    
  <script setup>
    import { ref } from 'vue'
        
    const count = ref(0)
  </script>

useSlots 和 useAttrs

<script setup> 使用 slotsattrs,可以分别用 useSlotsuseAttrs 两个辅助函数:

1
2
3
4
    import { useSlots, useAttrs } from 'vue'
    
    const slots = useSlots()
    const attrs = useAttrs()

useSlotsuseAttrs 是真实的运行时函数,它会返回与 setupContext.slotssetupContext.attrs 等价的值,同样也能在普通的组合式 API 中使用。

生命周期钩子

<script setup> 中使用生命周期钩子,需要稍稍修改一下:

1
2
3
4
5
6
    <script setup>
      import { onMounted } fron 'vue'
      onMounted(() => {
        // 你的代码
      })
    </script>

除了 create 系列的钩子不能使用之外(下面会说),其余的钩子使用方法相同。需要先从 vue 引入,然后在前面添加 on 并且改为 Camel-Case 命名,传入一个 callback,就会在对应生命周期执行 callback

script setup 总结

  • setup 执行时尚未创建组件实例,所以在 setup 中不能使用 this
  • 由于 setup 是围绕 beforeCreatecreated 生命周期钩子运行的,因此无需显式定义它们。换句话说,应该在这些钩子中编写的任何代码都应直接在 setup 函数中编写。

Teleport

这可是个好东西,以前在做各种 DOM 操作,CSS 样式的时候,不可避免的会遇到某些冲突,查来查去发现需要改一大堆东西才行,但是由于别的原因可能不能去改。

那可得试试 Teleport 这个组件了。简单的来说他就是一个传送门,可以把它包裹的内容传送到指定的 DOM 节点下,看下面示例:

1
2
3
4
  <teleport to="body">
      <div class="Dialog">
      </div>
  </teleport>

这个 .Dialog 节点,最终渲染的时候会出现在 body 下面,也就是 to 属性指定的 DOM 节点内。

实用场景一般会是在弹窗组件中。或者有些嵌套的组件被外层影响,并且是临时展示之类的,就可以使用这个传送门来解决。

将 markdown 文件 .md 展示在页面中并且高亮

在搭建组件库官网的过程中,因为需要将 markdown 转换为 html,并且提供代码块高亮展示,这时候就遇到了一些问题。

  • 因为不能把每个想用 markdown 写的文档页面单独写为组件,那样太不程序员了。
  • 但如果只用一个模版展示,其他的用 slot 插槽呢?这又有一个问题,必须得用 html 来编写文档,这样也太不程序员了。

如果我可以用 markdown 来写文档,用单个 vue 组件来读取这些 .md 文件,那就太棒了!于是我就跳进了这个坑里。

期间尝试各种插件,各种高亮引入等等,失败了一天多,这个过程就不赘述了,大抵就是从早到晚搜搜搜,试试试,最后也没弄出来的难过心情。

然后在翻看 Vite 2 文档时,看到了这个:静态资源处理

这里 Vite 官方为静态资源提供了多种导入形式,其中有一项: 将资源引入为字符串,那这个就是我想要的了,于是就开始了尝试。

实现思路

在组件内,将 markdown.md 引入为字符串。打印出来会发现,它把 markdown.md 里面的 markdown 格式内容完全打印了出来。

1
2
    import markdown from 'markdown.md?raw'
    console.log(markdown)

拿到了 markdown 字符串之后就可以把它转换为 html 字符串进行渲染了,然后添加高亮等等都可以。

最终实现

尝试完了之后发现其实还是有点麻烦,最终我采用了动态组件的实现方式,用到了 vite-plugin-md 这个插件,并且通过 Vue Router 实现了单个展示组件展示任意 markdown 文档。

首先创建一个用来展示 markdown 的页面组件。这个组件里面有一个动态组件 <component :is="" />

is 什么呢?那就在 script 里面引入 .md 文件,下面是完整的实现。

  • router.ts
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import { createRouter, createWebHistory } from 'vue-router';
import Markdown from './components/Markdown.vue';

const history = createWebHistory();

export default createRouter({
  history,
  routes: [
    {
      path: '/doc',
      component: Doc,
      children: [
        { path: '/doc/:name', component: Markdown },
      ]
    },
  ],
});
  • components/markdown.vue
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<template>
  <component :is = "content" :key="name" />
</template>

<script lang="ts" setup>
  import { shallowRef, watchEffect, computed, ComputedRef } from 'vue';
  import { useRoute } from 'vue-router';
  import router from '../router';
  
  const route = useRoute();
  const content: any = shallowRef(null);

  const currentName: ComputedRef = computed(() => {
    return route.params.name;
  });

  watchEffect(() => {
    if (route.path.startsWith('/doc/')) {
      import(/* @vite-ignore */'../markdown/' + currentName.value + '.md')
      .then(e => {
        content.value = e.default;
      }).catch(() => {
        router.replace('/404');
      });
    }
  });
</script>

这样就可以实现用户在访问 /doc/* 的路径时,自动展示 src/markdown 内名字为 *.md 的 markdown 文件内容。不需要一个文件一个组件,也不需要新建 markdown 就要去别的地方加一点东西了。

主要原理就是:

  • 在 router.ts 注册路由为 /doc/* 的所有路由都展示 Markdown 组件,这个路由添加了一个动态参数,可以将 /doc/ 后面的参数传给 Markdown 组件。
  • 在 Markdown 组件使用 watchEffect 对全局路由变化进行监听,当匹配到路由前缀为 /doc/ 的路由变化时,动态 import src/markdown 内的同名 .md 的内容。
  • 由于我安装了 vite-plugin-md 插件,它会自动把 .md 引入为 vue 组件,这时就可以使用 vue 的动态组件来动态展示内容了。

这里有几个点需要注意:

  • 组件内匹配到路由前缀后,如未找到对应文件,则需要跳转到 404 页面;
  • 动态组件必须有一个不会重复的 :key ,否则视图可能不会正确地更新;

代码高亮

至于代码部分高亮,我使用了 highlight.js , 在 main.ts 内注册了一个全局指令 highlight,之后在我需要高亮的代码容器添加一个 v-highlight 即可实现代码高亮。

  • highlight.js 这里我使用了单语言引入,你可以查看官方文档进行其他方式引入
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 引入 highlight 核心和语言
import hljs from 'highlight.js/lib/core';
import javascript from 'highlight.js/lib/languages/javascript';
import xml from 'highlight.js/lib/languages/xml';
import shell from 'highlight.js/lib/languages/shell';

// 注册 highlight 语言
hljs.registerLanguage('javascript', javascript);
hljs.registerLanguage('xml', xml);
hljs.registerLanguage('shell', shell);

hljs.configure({
  ignoreUnescapedHTML: true
});

export default {
  mounted(el) {
    const blocks = el.querySelectorAll('pre code');
    blocks.forEach((block: HTMLDivElement) => {
      hljs.highlightElement(block);
    });
  }
};
  • main.ts
1
2
3
4
5
6
// 引入自定义指令
import highlight from './directives/highlight';
import './assets/style/highlight.scss';

// 添加自定义指令
app.directive('highlight', highlight);
  • Markdown.vue 刚才的例子里,直接添加一个指令即可
1
2
3
<template>
  <component :is = "content" :key="name" v-highlight/>
</template>

总结

通过这次小项目实践,大概熟悉了 Vite + Vue 3 开发的基本流程,在写代码的过程中进行了多次封装,对于封装的时机和封装的思路、熟练度都有了提高,对于一份代码,优化也不会毫无头绪了。

实操使用了 Vite 2.7.2Vue 3.2.25、包括其他 typescript 等等依赖也都用了最新的,还有代码高亮和 markdown 展示的实现逻辑,包括 npm 发包等等过程中,自己研究、查资料填平了不少的坑,技能提升和成就感还是蛮高的。


可以点击 Desn-UI 官网 进行预览。项目仓库

继续加油!

(完)