前言
最近在系统学习 Vue 3 ,想做一些轮子来提高自己开发的效率,就萌生了做一个UI组件库官网的想法。目前拥有Input、Switch、Dialog、Button、Tabs组件
环境搭建和前期准备
安装
create-vite-app,可以直接使用yarn dev打开网页进行调试;vue-router,用于配置不同页面的路由;
- 其它有
github-markdown-css、marked、sass和prismjs。
具体的版本号可以参考 package.json:
{
"name": "sky-ui-dev",
"version": "0.0.0",
"scripts": {
"dev": "vite",
"build": "vite build"
},
"dependencies": {
"github-markdown-css": "4.0.0",
"prismjs": "^1.21.0",
"vue": "^3.1.3",
"vue-router": "4.0.0-beta.3"
},
"devDependencies": {
"@vue/compiler-sfc": "^3.1.3",
"marked": "^2.1.1",
"sass": "^1.32.11",
"vite": "^1.0.0-rc.1"
}
}
UI库的CSS最小影响原则
(1)不推荐使用scoped
HTML 标签中的 data-v-xxx 中的 xxx 每次运行都是随机数,我们需要输出稳定不变的 class 选择器,方便使用者覆盖。
(2)class前缀需要个性化
比如 .sky-theme-button,不太容易被使用者覆盖。.button和.theme-link这类前缀就比较通用化,很容易被使用者覆盖。
(3)存放UI库的通用样式
我们可以新建一个 sky.scss ,用来存放UI库的通用样式,同时写名前缀仅用于此UI库。
[class^="sky-"], [class*=" sky-"] {
margin: 0;
padding: 0;
box-sizing: border-box;
}
[class^="sky-"] 意味着包含 tree- 开头的 class,相当于 <div class="tree-button"> 这类标签。
[class*=" sky-"] 意味着包含 空格+tree- 开头的 class,相当于 <div class="xxx sky-button">这类标签。
项目目录结构
plugins:存放项目介绍 markdown 文档展示的插件;
assets:官网logo;
components:不同页面的 Vue 文件;
lib:组件文件;
markdown:官网介绍和开始文档;
views:官网的页面文件。
Vue Router 配置
在项目根目录下创建 router.ts 文件,我们这里使用 hash 模式来配置路由,不需要架设后台服务器。
// hash 模式
const history = createWebHashHistory();
export const router = createRouter({
history: history,
// 链接到不同的文件
routes: [
{path: '/', component: Home},
{
path: '/doc',
component: Doc,
children: [
{path: '', redirect: '/doc/intro'},
{path: 'intro', component: md(intro)},
{path: 'get-started', component: md(getStarted)},
{path: 'switch', component: SwitchDemo},
{path: 'button', component: ButtonDemo},
{path: 'dialog', component: DialogDemo},
{path: 'tabs', component: TabsDemo},
{path: 'radio', component: RadioDemo},
{path: 'input', component: InputDemo}
]
}
]
});
router.afterEach(() => {});
在 App.vue 加入 <router-view>
<template>
<router-view/>
</template>
<script lang="ts">
import {router} from './router';
export default {
name: 'App',
setup(){}
}
</script>
main.ts 中加入:
app.use(router)
到此,一个项目的前期工作已经基本准备完毕,接下来就开始我们的UI库开发。
组件开发
在开发过程中细节比较多,这里就挑几个有代表性的出来讲讲。
如何让组件的某一部分触发事件?
在 Button 组件中,外部有一个 div 红框,点击这个 div 会触发事件,但我们只需要点击 button 触发事件。如何才能实现呢?
第一步:让组件里的 div 不继承属性
设置 inheritAttrs: false 即可,这时点击div不会触发事件。inherit意为继承,attrs为attributes的缩写,意为属性。但是,这时的button点击也不会触发事件,因为这个语句默认会把所有继承的事件屏蔽掉。下一步我们需要进行另外的设置。
<script lang="ts">
export default {
inheritAttrs: false
}
</script>
第二步:让 div 中的 button 绑定 $attrs
$attrs 可以让某个元素 button 继承绑定在 Button 上的所有属性。只需要在 button 上加一句 v-bind="$attrs"即可。
<button v-bind="$attrs">
这时点击 button,就可以在控制台中看到 @click/@focus/@mouseover 事件结果出现。
如何按需加载不同组件的事件?
比如我一个组件想要 click 和 focus 两个事件,另一个组件只需要 click 事件,应该如何实现呢?主要思路是在组件内部声明setup,传入参数,并将获取到的参数 return 出来即可。
<template>
<div :size="size"> <!--第三步:在标签上定义获取到的事件或属性,这里是规定了尺寸大小-->
<button v-bind="$attrs">
<slot/>
</button>
</div>
</template>
<script lang="ts">
export default {
inheritAttrs: false,
setup(props, context){
//第一步:析构语法获取到外部的事件或属性
const {size, onClick, onMouseOver} = context.attrs;
//第二步:把获取到的size事件或属性return出来
return {size}
}
}
</script>
还可以使用 ES6 最新的剩余操作符来简化。获取到 ...rest 之后,在 template 中使用即可。
<script lang="ts">
export default {
inheritAttrs: false,
setup(props, context){
//使用...rest扩展操作符,获取到除size之外其他的属性或事件
const {size, ...rest} = context.attrs;
return {size, rest}
}
}
</script>
弹窗中的具名插槽(slot)
我们制作的弹窗(Dialog),希望可以让用户自定义其中的标题和内容,这时候可以使用具名插槽 v-slot。Vue 3 中的具名插槽的使用方式跟 Vue 2 有所不同。
第一步:使用 v-slot:xxx 定义好插槽的名字:
<template v-slot:content>
<div>hello</div>
</template>
第二步:在需要的地方引入插槽,使用 <slot name="xxx">
<main>
<slot name="content"/>
</main>
实现效果如下:
使用 Teleport “传送”组件
由于 CSS 层级上下文的原因,Dialog 组件虽然 z-index为10,但是其处于<div style="z-index: 1;">标签中,所以 Dialog 的层级肯定是没有外部 <div style="z-index: 2;"> 的层级高,从而被红色方框遮挡。这个组件的层级是由其所在的环境大小决定的,而不是由组件本身的层级来决定。 打个比方,一般学校里的尖子班,即便成绩排名中游,也会比普通班的中游成绩要高。
那我们应该如何防止 Dialog 被遮挡呢?这时候就需要使用 teleport 。teleport 的中文意思是“传送”,我们可以借用它来把 Dialog “传送”出去,这样就不会被其他元素影响了。
使用 <Teleport>把需要“传送”的组件包起来,加入 to="body" 来指定需要传送的目的地,这里直接移动到 body 标签下。
成功后,我们在开发者工具中就可以看到 Dialog 已经被成功“传送”了。
动态挂载组件
如果不想在组件中声明一个变量,又想改变这个变量的值,我们可以使用 createApp 的 h 来实现。这里就拿【一键打开Dialog组件】这个功能来举例。
第一步:创建一个 Button,添加 @click="showDialog" 事件,功能为点击打开 Dialog;
第二步:showDialog 函数中调用 openDialog,用户可以传入 title、content、ok、cancel 来自定义 Dialog 组件中的内容;
<script>
const showDialog = ()=>{
openDialog({
title:'标题',
content:'你好',
ok(){console.log('ok');},
cancel(){console.log('cancel');}
})
}
return {showDialog}
}
}
</script>
第三步:创建一个 openDialog.ts 组件,可以获取外部的属性,在 body 中直接创建一个 div 。这里需要使用h() 来构造新的 Dialog 。
import Dialog from './Dialog.vue';
import {createApp, h} from 'vue';
export const openDialog = (options) => {
const {title, content, ok, cancel} = options;
const div = document.createElement('div');
document.body.appendChild(div);
const close = () => {
app.unmount(div);
div.remove();
};
const app = createApp({
render() {
//使用h构造Dialog
return h(Dialog, {
visible: true, 'onUpdate:visible': (newVisible) => {
if (newVisible === false) {
close();
}
},
ok, cancel
}, {
title, content
});
}
});
//挂载新的div
app.mount(div);
};
使用 Template Refs 动态设置 div 宽度
在做好 Tab 的底部导航条提醒后,一开始把宽度规定为 100px,但宽度已经超过了文字的宽度。对于组件库来说,如何根据使用者的文字来动态设置这个导航条的宽度呢?我们需要使用 Vue3 中的 Template Refs 。
根据 Vue 3 文档中的用法,我们在导航文字 div 上绑定一个 ref ,挂载组件的时候使用 ref 里面的 value 获取到 div 宽度。
<template>
<div :ref="element => { if(element) navItems[index] = element }"></div>
<!--如果element存在,那么就让navItems里的元素等于element-->
<div class="tree-tabs-nav-indicator"></div>
</template>
<script lang="ts">
import {onMounted, ref} from 'vue';
export default {
setup(props, context) {
const navItems = ref<HTMLDivElement[]>([]);
const indicator = ref<HTMLDivElement>(null);
onMounted(() => {
const divs = navItems.value;
const result = divs.filter(div=>div.classList.contains('selected'))[0]
const {width} = result.getBoundingClientRect()
indicator.value.style.width = width + 'px'
});
return {navItems, indicator};
}
};
</script>
成功运行代码后,我们可以在控制台实时获取 indicator 宽度。
组件源代码美化
使用 custom block 展示源代码
我们想要把组件中的源代码展示到页面上,这时候需要用到custom block这个插件。
第一步:在 vite.config.ts 中加入以下代码,主要作用是解析组件中的代码。
import {md} from './plugins/md'
import fs from 'fs'
import {baseParse} from '@vue/compiler-core'
export default {
vueCustomBlockTransforms: {
demo: (options) => {
const { code, path } = options
const file = fs.readFileSync(path).toString()
const parsed = baseParse(file).children.find(n => n.tag === 'demo')
const title = parsed.children[0].content
const main = file.split(parsed.loc.source).join('').trim()
return `export default function (Component) {
Component.__sourceCode = ${
JSON.stringify(main)
}
Component.__sourceCodeTitle = ${JSON.stringify(title)}
}`.trim()
}
}
}
第二步:在需要展示代码的组件中,添加 <demo> 标签:
<demo>
常规用法
</demo>
<template>
<Switch v-model:value="bool"/>
</template>
第三步:父组件中展示代码:
<div class="demo-code">
<pre>{{Switch1Demo.__sourceCode}}</pre>
</div>
使用 prismjs 和 v-html 高亮源代码
prismjs 是一个用于高亮代码的库,只需引入即可使用,适用于我们UI库中进行代码展示。
安装prismjs:yarn add prismjs
在组件中引入,并使用 setup() 导出,方便使用:
import 'prismjs';
import 'prismjs/themes/prism-okaidia.css';
//themes文件中为不同的主题,大家可以多试几个,用自己比较喜欢的
const Prism = (window as any).Prism;
export default {
setup() {
return {Prism};
}
};
在 template 中进行展示:
<template>
<div class="demo-code">
<pre class="language-html" v-html="Prism.highlight(Switch1Demo.__sourceCode,
Prism.languages.html,'html')"/>
</div>
</template>
高亮代码效果如下:
兼容 TypeScript 和 markdown
有时候一些库没有TypeScript声明文件,可以在安装完库后运行如下代码,xxx为库名:
yarn add --dev @types/xxx
一般IDE都不会识别markdown类型文件,可以在 shims.d.ts 中声明一下,避免报错:
declare module '*.md'{
const str: string
export default str
}
项目自动化打包和部署
使用如下命令:
yarn build
运行成功后会在项目根目录下生成一个 dist 文件夹,只需要把这个文件夹上传到 github 即可。
一般从打包到部署 github 需要输入很多命令行,这时候我们可以使用一个 deploy.sh 来实现自动化部署。
在项目根目录下新建一个 deploy.sh,输入以下命令(加&&是需要确认命令的运行情况,如某条命令运行失败则步停止):
rm -rf dist &&
yarn build &&
cd dist &&
git init &&
git add . &&
git commit -m "update" &&
git branch -M main &&
//这里输入你的的github仓库地址
git remote add origin git@github.com:xxxxxx &&
git push -f -u origin main &&
cd ..
以后只需要在终端中输入 sh deploy.sh 即可实现一行代码部署。
如果这篇文章帮助到了你,欢迎关注,点赞,评论~