vite2 + vue3 开发组件库
使用vite2 + vue3开发一个组件库
完成后的效果图展示:
一、搭建一个 Vite 项目
参照vite官网
通过附加的命令行选项直接指定项目名称和你想要使用的模板。例如,要构建一个 Vite + Vue 项目
# yarn yarn create vite my-baseui --template vue
接下来执行
cd my-baseui yarn yarn dev
运行后打开页面长这样子:
在根目录下创建packages文件夹,目录结构如下:
描述:
packages/components
: 存放组件。packages/styles
: 存放全局样式和组件样式。packages/index.js
二、组件编写
一、components中,编写一个测试的button组件
二、components中的 button组件index.vue代码
<template> <button class="m-button" :class="styleClass" :disabled="disabled" :round="round"> <slot></slot> </button> </template> <script> import { computed } from 'vue'; export default { name: 'm-button', props: { type: { type: String, default: 'default', validator(value) { return ['default','primary', 'success', 'info', 'warning', 'danger','text'].indexOf(value) > -1; } }, disabled: { type: Boolean, default: false }, round: { type: Boolean, default: false } }, setup(props) { const styleClass = computed(() => { return { [`m-button--${props.type}`]: props.type, 'is-disabled': props.disabled, 'is-round': props.round } }) return { styleClass } } } </script> <style lang="scss" scoped> @import '../../styles/components/button.scss'; </style>
三、styles中的button样式代码
.m-button { display: inline-block; padding: 12px 20px; margin-right: 10px; border: 1px solid #d9d9d9; background: #fff; color: #333; cursor: pointer; } .is-disabled{ color: #c0c4cc; cursor: not-allowed; background-color: #fff; border-color: #ebeef5; } .m-button--primary{ color: #fff; background-color: #409eff; border-color: #409eff; } .m-button--success { color: #fff; background-color: #67c23a; border-color: #67c23a; } .m-button--info { color: #fff; background-color: #909399; border-color: #909399; } .m-button--warning { color: #fff; background-color: #e6a23c; border-color: #e6a23c; } .m-button--danger { color: #fff; background-color: #f56c6c; border-color: #f56c6c; } .m-button--text { border-color: transparent; color: #409eff; background: transparent; padding-left: 0; padding-right: 0; } // 圆角 .is-round{ border-radius: 20px; }
四、button组件中 index.js文件,用来把写的组件暴露出去
import mButton from './index.vue' mButton.install = app => { app.component(mButton.name, mButton) } export default mButton;
五、packages文件夹下新建index.js文件,用来管理所有的组件
import mButton from "./components/button/index"; import mInput from "./components/input/index"; // 组件列表 const components = [ mButton, mInput ] // 定义 install 方法,接收 Vue 作为参数。如果使用 use 注册插件,那么所有的组件都会被注册 const install = (Vue) => { // 判断是否安装 if (install.installed) return // 遍历注册全局组件 components.map(component => Vue.component(component.name, component)) } if (typeof window !== 'undefined' && window.Vue) { install(window.Vue); } export default { install, mButton, mInput }
三、组件引入
一、在src 的main.js
中引入
import { createApp } from 'vue' import App from './App.vue' import myBaseui from "../packages/index"; const app = createApp(App); app.use(myBaseui) app.mount('#app')
二、在src/App.vue中使用组件
改写 src/App.vue
,引入button
<template> <div> <m-button>默认按钮</m-button> <m-button type="primary">主要按钮</m-button> <m-button type="success">成功按钮</m-button> </div> </template>
浏览器上查看效果,可以看到已经成功了
一个组件库有很多个组件,每个组件都应该有它独立的文档。这个文档不仅有对组件各项功能的描述,更应该具有组件预览、组件代码查看等功能。同时为了良好的组件开发体验,我们希望这个文档是实时的,这边修改代码,那边就可以在文档里实时地看到最新的效果。接下来我们就来实现这么一个功能。
组件的文档一般是用 Markdown 来写。这里我们使用一个插件 vite-plugin-md
vite pulgin 将 md 文件转换成 vue 组件渲染的主要流程是:
- 配置 vue router 路由,指向 .md 文件
- 编写 vite 插件将 .md 文件解析成 vue 文件字符串
- 最后由 vite 的插件@vitejs/plugin-vue 将 vue 文件字符串编译成函数组件返回给前端
在src下,新建router/index.js文件
四、配置 vue router 路由,指向 .md 文件
import { createRouter, createWebHashHistory } from "vue-router"; const routes = [ { path: '/', name: '组件页面', component: () => import('@/views/home.vue'), children: [ { path: '/button', name: 'Button 按钮', component: () => import('/packages/components/button/doc/doc.md') }, { path: '/input', name: 'Input 输入框', component: () => import('/packages/components/input/doc/doc.md') } ] } ] const router = createRouter({ history: createWebHashHistory(), routes, }); export default router;
main.js中引入路由
import { createApp } from 'vue' import App from './App.vue' import router from "./router"; import myBaseui from "../packages/index"; const app = createApp(App); app.use(router) app.use(myBaseui) app.mount('#app')
这里路由引入的是Markdown 文件,这个在默认的 Vite 配置里是无效的,我们需要引入 vite-plugin-md
插件来解析 Markdown 文件并把它变成 Vue 文件。
五、编写 vite 插件将 .md 文件解析成 vue 文件
vite.config.js 如下:
import { defineConfig } from 'vite' import Vue from '@vitejs/plugin-vue'; import Markdown from 'vite-plugin-md'; import { resolve } from "path"; // https://vitejs.dev/config/ export default defineConfig({ base: '/my-baseui/', plugins: [ Vue({ include: [/\.vue$/, /\.md$/], }), Markdown(), ], resolve: { alias: { "@": resolve(__dirname, "src"), '~': resolve(__dirname, "packages") } }, })
这样配置以后,任意的 Markdown 文件都能像一个 Vue 文件一样被使用了
接下来,编写home.vue页面,增加侧边栏和主区域
<template> <div class="main-container"> <div class="sidebar"> <ul v-for="item in routes" :key="item"> <li v-for="(ele,index) in item.children" :key="ele" :class="{'active': mIndex == index}" @click="goPath(ele,index)"> {{ele.name}} </li> </ul> </div> <main class="app-main"> <router-view></router-view> </main> </div> </template> <script setup> import { computed, ref, reactive, onMounted } from 'vue'; import { useRouter } from 'vue-router'; const router = useRouter() const mIndex = ref(sessionStorage.getItem("mIndex") || '0'); const routes = computed(() => router.options.routes) const goPath = (ele,index) => { mIndex.value = index router.push({ path:ele.path }) sessionStorage.setItem("mIndex", index); } </script> <style lang="scss" scoped> .main-container{ display: flex; justify-content: space-between; overflow: hidden; .sidebar{ width: 200px; height: 100%; border-right: 1px solid #eee; text-align: center; ul{ li{ height: 50px; line-height: 50px; cursor: pointer; } .active{ color: #409eff; background-color: #ECF5FF; border-right: 1px solid #409eff; } } } .app-main{ flex: 1; padding: 20px 50px; overflow-y: auto; } } </style>
把App.vue改下
<template> <router-view></router-view> </template> <script setup> </script> <style> </style>
往 /packages/components/button/doc/doc.md
里面随便写点东西,
完成后,浏览器查看有效果了。
vite-plugin-md 是支持在 Markdown 里面写 setup 函数的!因此我们可以把需要执行 JS 逻辑的代码封装成一个组件,然后在 Markdown 里通过 setup 来引入。
在packages/components/button/doc 目录下新建一个 demo.vue
<template> <div> <m-button>默认按钮</m-button> <m-button type="primary">主要按钮</m-button> <m-button type="success">成功按钮</m-button> <m-button type="danger">危险按钮</m-button> </div> </template>
然后在md文件中引入
<script setup> import demo from './demo.vue'; </script> # Button 按钮 <demo/>
完成后,查看页面显示:
六、代码预览功能
接下来我们来实现代码预览功能,虽然说代码预览也很简单,可以直接在 Markdown 中贴代码,但是代码又写一遍就显得很繁琐了
有没有方法可以直接把demo.vue中的代码展示出来呢?
答案是可以的。
在 Vite 的开发文档里有记载到,它支持在资源的末尾加上一个后缀来控制所引入资源的类型。比如可以通过 import xx from 'xx?raw'
以字符串形式引入 xx 文件。基于这个能力,我们可以编写一个 <Preview />
组件来获取所需要展示的文件源码。
一、新建一个 Preview.vue
文件
<template> <div class="pre-code-box"> <transition name="slide-fade"> <pre class="language-html" v-if="showCode" > <code class="language-html">{{ sourceCode }}</code> </pre> </transition> <div class="showCode" @click="showOrhideCode"> <i :class="iconClass"></i> <span>{{ showCode ? "隐藏代码" : "显示代码" }}</span> </div> </div> </template> <script setup> import { computed, onMounted, ref } from "vue"; const props = defineProps({ compName: { type: String, default: "", require: true, }, demoName: { type: String, default: "", require: true, }, }); const showCode = ref(false); const sourceCode = ref(""); const iconClass = computed(() => { return [ 'iconfont', showCode.value ? 'icon-arrow-up-filling' : 'icon-arrow-down-filling' ] }) const showOrhideCode = () => { showCode.value = !showCode.value; } const getSourceCode = async () => { let msg = await import(/* @vite-ignore */ `/packages/components/${props.compName}/doc/${props.demoName}.vue?raw`) // console.log(msg.default); sourceCode.value = msg.default } onMounted(() => { getSourceCode() }) </script>
这里需要加 @vite-ignore
的注释是因为 Vite 基于 Rollup,在 Rollup 当中动态 import 是被要求传入确定的路径,不能是这种动态拼接的路径。此处加上该注释则会忽略 Rollup 的要求而直接支持该写法。
但是这样的写法,在开发模式下可行,build打包到线上后报错,拿不到资源,我们可以通过判断环境变量,在 build 模式下通过 fetch
请求文件的源码来绕过,我们后面再讲。
二、在md文件中引入preview
<script setup> import demo from './demo.vue'; import preview from '@/components/preview.vue'; </script> # Button 按钮 <demo/> <preview compName="button" demoName="demo"/>
查看页面效果:
我们发现,样式没有高亮效果,接下来下载插件 highlight.js 来实现代码高亮
三、main.js中引入 highlight.js ,样式有很多种,我们随便找个看看效果
import { createApp } from 'vue' import App from './App.vue' import router from "./router"; import myBaseui from "../packages/index"; import hljs from "highlight.js"; import "highlight.js/styles/color-brewer.css"; const app = createApp(App); app.use(router) app.use(myBaseui) app.directive("highlight", function (el) { const blocks = el.querySelectorAll("pre code"); blocks.forEach((block) => { hljs.highlightBlock(block); }); }); app.mount('#app')
preview.vue中加入 v-highlight 自定义指令
<pre class="language-html" v-if="showCode" v-highlight > <code class="language-html">{{ sourceCode }}</code> </pre>
调整后,页面高亮效果出来了
到这里,我们的组件库基本结构算是完成了,接下来,我们上传到github,部署静态站点。
七、部署站点
查看vite文档,在vite.config.js 中加入 base: '/my-baseui/',
执行npm run build 时,默认会打包成dist文件,我们把输出目录改成docs
vite.config.js加如下配置:
build: { outDir: 'docs' },
接下来,在你的项目中,创建一个 deploy.sh
脚本,包含以下内容(注意高亮的行,按需使用),运行脚本来部署站点
#!/usr/bin/env sh # 发生错误时终止 set -e # 构建 npm run build:docs # 进入构建文件夹 cd docs # 如果你要部署到自定义域名 # echo 'www.example.com' > CNAME git init git add -A git commit -m 'deploy' # 如果你要部署在 https://<USERNAME>.github.io # git push -f git@github.com:<USERNAME>/<USERNAME>.github.io.git main # 如果你要部署在 https://<USERNAME>.github.io/<REPO> git push -f git@github.com:wangibook/my-baseui.git master:gh-pages cd -
packages.json中的 scripts 修改下:
"scripts": { "dev": "vite", "build:docs": "vite build", "preview": "vite preview", "deploy": "bash deploy.sh" },
之后执行 npm run deploy 命令时,会把docs文件夹打包上传到github的 gh-pages 分支上
切换到gh-pages分支,可以看到看到内容就是我们打包的docs文件夹下的内容,
点击Settings--->Pages---->Source选择gh-pages分支然后点击链接https://wangibook.github.io/my-baseui/,
此时预览代码会报错,拿不到资源,其原因是由于 Rollup 无法进行静态分析,因此无法在构建阶段处理需要动态 import 的文件,导致会出现找不到对应资源。
我们可以通过判断环境变量,在 build 模式下通过 fetch
请求文件的源码来绕过。
把preview.vue做下修改:
const isDev = import.meta.env.MODE === 'development'; const getSourceCode = async () => { if(isDev) { let msg = await import(/* @vite-ignore */ `/packages/components/${props.compName}/doc/${props.demoName}.vue?raw`) // console.log(msg.default); sourceCode.value = msg.default } else { sourceCode.value = await fetch(`/my-baseui/components/${props.compName}/doc/${props.demoName}.vue`).then(res => res.text()); } }
打包后的文件夹里并没有components文件,我们得把packages下的components文件加入进去
每次复制粘贴这个components就会显得很麻烦,在这里可以用一个插件自动帮我们实现,下载插件 copy-dir
新建config/copyDocs.js
let copydir = require('copy-dir') copydir.sync( process.cwd() + '/packages/components', process.cwd() + '/docs/components', { utimes: true, mode: true, cover: true }, function (err) { if (err) throw err console.log('done') } )
再次执行构建命令 npm run deploy, 等一会儿查看部署的站点,代码预览可以拿到资源了。
八、分开文档和库的构建逻辑
我们目前打包出来的文件是交互文档docs,为了构建一个 my-baseui
组件库并发布到 npm,可供别人下载使用,我们需要将构建的逻辑分开
在根目录下添加一个 /build
目录,依次写入 base.config.js,dist.config.js,docs.config.js,分别为基础配置、库配置和文档配置
base.config.js:
基础配置,需要确定路径别名、配置 Vue 插件和 Markdown 插件用于对应文件的解析。
import { defineConfig } from 'vite'; import Vue from '@vitejs/plugin-vue'; import Markdown from 'vite-plugin-md'; import { resolve } from "path"; // https://vitejs.dev/config/ export default defineConfig({ plugins: [ Vue({ include: [/\.vue$/, /\.md$/], }), Markdown(), ], resolve: { alias: { "@": resolve(__dirname, "../src") } }, })
dist.config.js:
import baseConfig from './base.config'; import { defineConfig } from 'vite'; export default defineConfig({ ...baseConfig, build: { outDir: 'dist', rollupOptions: { // 请确保外部化那些你的库中不需要的依赖 external: ["vue"], output: { // 在 UMD 构建模式下为这些外部化的依赖提供一个全局变量 globals: { vue: "Vue", } }, }, lib: { entry: "./packages/index.js", name: "my-baseui", }, } });
docs.config.js:
import baseConfig from './base.config'; import { defineConfig } from 'vite'; export default defineConfig({ ...baseConfig, base: '/my-baseui/', build: { outDir: 'docs', }, });
完成了上面这些构建配置以后,修改一下packages.json
"scripts": { "dev": "vite --host", "build:docs": "vite build --config ./build/docs.config.js && node ./config/copyDocs.js", "build:dist": "vite build --config ./build/dist.config.js", "preview": "vite preview", "deploy": "bash deploy.sh" },
执行npm run build:docs 结果
执行npm run build:dist 结果
九、上传npm
一、在package.json中添加发布的一些信息
描述:
description:描述信息
main:入口
keywords:关键词
npm发布时,要将package.json中的private属性值改为false
README.md中写使用说明
# my-baseui ## 安装 使用npm 或 yarn 安装 ``` npm install my-baseui yarn add my-baseui ``` ## 引入 my-baseui ### 完整引入 #### 需要注意的是 css 样式文件需要单独引入。 在 main.js 中写入以下内容: ```js import { createApp } from 'vue' import App from './App.vue' // 导入组件库 import myBaseui from 'my-baseui' import 'my-baseui/dist/style.css'; const app = createApp(App) app.use(myBaseui) app.mount('#app') ``` <br/> ## 愉快开始 #### 至此 my-baseui 就引入完成并且可以使用了。 ```html <!-- html --> <m-button>默认按钮</m-button> <m-button type="primary">主要按钮</m-button> ``` <br/>
发布npm
执行npm login 命令,输入用户名和密码,输入密码时是看不到的
之后提示输入email,成功后你的邮箱会收到一个one-time password,填入这个一次性密码
登录之后,执行npm publish进行发布(每次进行发布的时候记得改下版本号)
发布成功后,到npm上,查看头像---->packages,就可以看到发布的包了。
至此,大体就算完成了,后面就是完善每个组件。
如果你感兴趣的话,请前往 GitHub 查看源码和完整文档
github地址: https://github.com/wangibook/my-baseui