声明:本文为稀土掘金技术社区首发签约文章,30天内禁止转载,30天后未获授权禁止转载,侵权必究!
插件到底是什么?
说起插件,很多人其实不太理解,甚至都不知道插件到底是个什么东西,他们只是人云亦云的,听别人说起某某某插件。 他们以为所谓的插件代指的其实是具体的某些开源或者不开源的库
所以,渐渐的,编写一个插件仿佛成了一个非常高深和专业
的东西,既需要有扎实的技术功底,还需要有很好的抽象 能力。
于是,大多数人就开始望而却步
, 他们总认为自己作为一名都市奴人
不配,只需要浅浅的写好业务就可以了。
其实,我想说,单单的写好业务真的可以吗?
答案显而易见,不可以
原因很简单,你写的越好,你就越具有可替代性,而你写的高深,别人就越替代不了你
你想想,在一个熟睡的深夜,老板总是偏爱的给你打电话解决问题?
这是因为你很优秀吗?
当然不是的
,是因为他没有办法!
找别人搞不定!
所以,要想更多的让别人搞不定,而只有你搞得定,除了适当的给代码写的鬼都看不懂之外(注意:这里的鬼都看不懂,是用别人看不懂的招数,实现功能),就得让你写的东西,给更多的人用,而插件刚好符合可以更多人用这个特质。代码不用很体面,能用就行
从这个角度讲,编写插件其实不是那些大佬的特权,而是你在工作中的提炼!
讲到这,你还觉得他有那么遥不可及吗!
额,跑题了,我们言归正传
插件到底是什么?
插件 主要是对当前的应用程序做拓展并且可以从源码代码中剥离出来,注意 这里我们的重点是拓展
和剥离
于是我们顺着这两个关键词,就可以很简单的在自己的心里形成一个插件的印象
首先他得是独立的
,其次他得是通用的
, 基于当前印象,我们其实大家可以发现,所谓插件,在我们自己的业务代码中无处不在,只不过,我们没有将它变成独立的而已!
不信?
你还记不记得,你有多少次,在自己的代码中写入防抖函数
和节流函数
,如果我猜的不错的话,你的每个项目里都少不了这俩货吧!(别问我是怎么知道的,问就是我就是这么干的)
而当你将这俩货提取出来放到npm 之后,严格意义上来说,他俩就可以叫做防抖截流插件
!
当然,你都想到了,这个世界上这么多聪明人,怎么会想不到呢?
于是就诞生了很多包含防抖节流
的库,比如:大名鼎鼎的Lodash
,恰巧包含了这俩货
我们只需要引入使用即可
代码如下:
// 引入一个节流函数
import throttle from 'lodash/throttle';
throttle(()=>{console.log('这是一个节流函数')},200)
// 引入一个防抖函数
import throttle from 'lodash/debounce';
debounce(()=>{console.log('这是一个防抖函数')},200)
有了这个插件,我们就不用在重复的引入这俩货了
,早点下班指日可待!
这就是插件的魅力, 好像其实也不那么难,我们只需要给我们的代码存到一个别人能找到的地方,仅此而已!
放下了对于插件恐惧,我们来开始正题,如何编写一个vue
插件
如何编写一个vue 的插件
说起vue
插件,相信很多jym
都能背出来我们用的很多插件,如数家珍,比如element-ui
、vant
、vuex
、vue-router
、pinia
等等
他们的内部的实现虽然很复杂,但是我想说的是,千里之行始于足下
就拿element-ui
、vant
,举例
本质上,这两个库其实是插件的集合!
他们的源码,其实是由一个个插件组成
合起来,虽然很唬人很高端的样子,但是单独放到每一个独立插件身上,却也是平平无奇
这种感觉怎么形容呢?
就好像,每一个字我都认识,但合起来,我却不知道什么意思
其实大家千万不要有这种心理包袱,所谓的明星插件
,他强的地方,不是因为技术深奥,而是因为做的早,用的人多
摊开来看,他的每一个插件,其实真的是平平无奇
就拿明星ui库element-ui
举例,他的button
插件代码如下:
<template>
//用原生的button按钮,更改了样式
<button
class="el-button"
@click="handleClick"
:disabled="buttonDisabled || loading"
:autofocus="autofocus"
:type="nativeType"
:class="[
type ? 'el-button--' + type : '',
buttonSize ? 'el-button--' + buttonSize : '',
{
'is-disabled': buttonDisabled,
'is-loading': loading,
'is-plain': plain,
'is-round': round,
'is-circle': circle
}
]"
>
<i class="el-icon-loading" v-if="loading"></i>
<i :class="icon" v-if="icon && !loading"></i>
<span v-if="$slots.default"><slot></slot></span>
</button>
</template>
<script>
export default {
name: 'ElButton',
inject: {
elForm: {
default: ''
},
elFormItem: {
default: ''
}
},
// 根据外部传递的参数,改变当前button按钮的形态和功能
props: {
type: {
type: String,
default: 'default'
},
size: String,
icon: {
type: String,
default: ''
},
nativeType: {
type: String,
default: 'button'
},
loading: Boolean,
disabled: Boolean,
plain: Boolean,
autofocus: Boolean,
round: Boolean,
circle: Boolean
},
computed: {
_elFormItemSize() {
return (this.elFormItem || {}).elFormItemSize;
},
buttonSize() {
return this.size || this._elFormItemSize || (this.$ELEMENT || {}).size;
},
buttonDisabled() {
return this.$options.propsData.hasOwnProperty('disabled') ? this.disabled : (this.elForm || {}).disabled;
}
},
methods: {
// 给外部的回调,能让外部直接能用上@click 方法而不是@click.native
handleClick(evt) {
this.$emit('click', evt);
}
}
};
</script>
其实大家会发现,所谓element-ui
源码就是这么平平无奇,应该还不如你写的业务组件吧!
不瞒大家说,这俩库的源码,我骥某人都浅浅的读过一些
总结来说,我认为他们的插件形式就分为那么几种(当然有更多的各位jym可以补充)
指令插件
所谓指令插件
其实就是利用vue自定义指令的能力
,绑定模板中,从而操作改变dom
实现功能
根据国际惯例,我们在开始正式举例指令插件之前,我们要简单的温习一下定义指令
底怎样使用
所谓自定义指令,本质上,就是一个包含特定生命周期函数的对象
,
代码如下:
const myDirective = {
// 在绑定元素的 attribute 前
// 或事件监听器应用前调用
created(el, binding, vnode, prevVnode) {
// 下面会介绍各个参数的细节
},
// 在元素被插入到 DOM 前调用
beforeMount(el, binding, vnode, prevVnode) {},
// 在绑定元素的父组件
// 及他自己的所有子节点都挂载完成后调用
mounted(el, binding, vnode, prevVnode) {},
// 绑定元素的父组件更新前调用
beforeUpdate(el, binding, vnode, prevVnode) {},
// 在绑定元素的父组件
// 及他自己的所有子节点都更新后调用
updated(el, binding, vnode, prevVnode) {},
// 绑定元素的父组件卸载前调用
beforeUnmount(el, binding, vnode, prevVnode) {},
// 绑定元素的父组件卸载后调用
unmounted(el, binding, vnode, prevVnode) {}
}
// 在模板中使用
<div v-my-directive></div>
以上代码中,我们发现,只需要在模板中用v+当前指令对象名字
我们即可使用指令,并且在不同的时机调用不同的生命周期函数,从而实现我们的功能
好了,片汤话讲完了,用我们继续用明星库element-ui
举例,在element-ui
中,经典的指令插件v-loading
v-loading 源码解析
v-loading
顾名思义,就是加载中。。。
废话少说,我们看代码,
import { Loading } from './service'
// 创建加载中遮罩层实例,并且挂在在body 中
const createInstance = (
el: ElementLoading,
binding: DirectiveBinding<LoadingBinding>
) => {
const vm = binding.instance
const getBindingProp = <K extends keyof LoadingOptions>(
key: K
): LoadingOptions[K] =>
isObject(binding.value) ? binding.value[key] : undefined
const resolveExpression = (key: any) => {
const data = (isString(key) && vm?.[key]) || key
if (data) return ref(data)
else return data
}
const getProp = <K extends keyof LoadingOptions>(name: K) =>
resolveExpression(
getBindingProp(name) ||
el.getAttribute(`element-loading-${hyphenate(name)}`)
)
const fullscreen =
getBindingProp('fullscreen') ?? binding.modifiers.fullscreen
const options: LoadingOptions = {
text: getProp('text'),
svg: getProp('svg'),
svgViewBox: getProp('svgViewBox'),
spinner: getProp('spinner'),
background: getProp('background'),
customClass: getProp('customClass'),
fullscreen,
target: getBindingProp('target') ?? (fullscreen ? undefined : el),
body: getBindingProp('body') ?? binding.modifiers.body,
lock: getBindingProp('lock') ?? binding.modifiers.lock,
}
// 挂在实例到dom节点中,以便实例被长久保存
el[INSTANCE_KEY] = {
options,
instance: Loading(options),
}
}
// 指令对象
export const vLoading: Directive<ElementLoading, LoadingBinding> = {
mounted(el, binding) {
if (binding.value) {
// 指令初始化
createInstance(el, binding)
}
},
// 指令更新重新初始化
updated(el, binding) {
const instance = el[INSTANCE_KEY]
if (binding.oldValue !== binding.value) {
if (binding.value && !binding.oldValue) {
createInstance(el, binding)
} else if (binding.value && binding.oldValue) {
if (isObject(binding.value))
updateOptions(binding.value, instance!.options)
} else {
instance?.instance.close()
}
}
},
// 指令卸载也要卸载当前实例
unmounted(el) {
el[INSTANCE_KEY]?.instance.close()
el[INSTANCE_KEY] = null
},
}
以上的v-loading
的示例代码之所以是优秀的插件, 原因是因为他是优秀的人写的
而之所以他能成为优秀的人,是因为,做到了一件事,代码分层
用人话说,就是一个函数只干一件事
。
于是以上代码中,我们可以发现两个优秀的技巧
- 1、利用
dom
存储全局实例,解决指令中,无法保存全局实例的缺陷 - 2、代码严格分层,将视图层和控制层,以及指令层分开,具有很好的可维护性
有了以上代码,我们就能很简单的在代码中使用即可
<el-button
v-loading="fullscreenLoading"
type="primary"
>
As a directive
</el-button>
我写的指令插件一个匹配原生体验的 vue 抽屉指令
组件插件
组件插件,dddd(懂得都懂)
,就是给我们普通的组件变成插件,组件这个东西出现的的目的,就是能让我们像搭建积木一样,组成我们的页面,并且这个积木我们可以复用。
于是听到了复用
俩字,我相信你一定是两眼放光了,因为就可以提炼代码成为插件了, 然而怎么提炼,这就考验各位jym
的的内功
了,就需要大家多看别人的代码,多思考,多总结
这里,给只能大家两个建议:
掌握好组件通信的方式灵活运用
相信很多人,在提炼组件的时候,只会用props
、 emit
两种,然而其实你不知道的是组件通信其实有9种(vue3版本)
- props
- $emit
- defineExpose
- $attrs
- v-model
- provide / inject
- pinia(Vuex)
- mitt(发布订阅))
- slot
那些常用的方式,我们不说了,省得各位jym
嫌我啰嗦,这里我们简单的讲讲其中的一种defineExpose
,你会发现有很多妙用
defineExpose
defineExpose
本质上就是父组件调用子组件的方法,使用方式如下:
// Child.vue
<script setup>
defineExpose({
childName: "这是子组件的属性",
someMethod(){
console.log("这是子组件的方法")
}
})
</script>
// Parent.vue
<template>
<child ref="comp"></child>
<button @click="handlerClick">按钮</button>
</template>
<script setup>
import child from "./child.vue"
import { ref } from "vue"
const comp = ref(null)
const handlerClick = () => {
console.log(comp.value.childName) // 获取子组件对外暴露的属性
comp.value.someMethod() // 调用子组件对外暴露的方法
}
</script>
以上代码中,就是简单的使用方法,它的妙用就是比如你要二次封装element-ui的Dialog 对话框
,我们知道对话框使用v-model
绑定,但好死不死的 这个Dialog
被封装在组件中,你在外部要改变状态,就要传值进去,但内部想改vue又不让
,此时此刻,defineExpose 就派上了用场,我们不需要传入变量去更新状态,只需要在组件外层调用即可,从而避免了复杂的状态变更
代码如下:
// Child.vue
<template>
<el-dialog
v-model="dialogVisible"
>
<span>This is a message</span>
</el-dialog>
</template>
<script setup>
import { ref } from "vue"
const dialogVisible = ref(false)
defineExpose({
dialogVisible
})
</script>
// Parent.vue
<template>
<child ref="comp"></child>
<button @click="handlerClick">按钮</button>
</template>
<script setup>
import child from "./child.vue"
import { ref } from "vue"
const comp = ref(null)
const handlerClick = () => {
comp.value.dialogVisible = true
}
</script>
注重代码分层,注重单向数据流
所谓代码分层,我之前说过了,就是一个函数只干一件事
, 要保证逻辑的解耦,从而实现更好的维护性。
有人问,我不应该把代码写的特别烂,鬼都看不懂吗? 这样才能具有不可替代性,
其实我想说,你代码写的高端,别人一样也看不懂。
之所以要写的具有可维护性,并不是为了别人,是为了让你下次能看得懂
我们来看单项数据流,之所以推崇单项数据流,同样的,也是为了可维护性,避免诡异的bug ,我们应该将数据状态的修改,放在父组件中统一更改。
于是有jym
说了, 你小子别忽悠我,这不就是普通的组件吗?跟插件有什么关系呢?
答案很简单,变成npm包多项目通用,不就变成插件了吗?
接下来,我就用我当年写的一个组件类型的插件举例,完整的教大家写一个插件
既然是通用的插件,就必须有脚手架!因为我们需要将插件打包变成npm包
既然我们做的是vue系列,那么脚手架就要选用vue
相关的工具链vite
vite
vite 作为下一代的前端工具链,除了能初始化我们的项目,当然的也能初始化插件项目
npm create vite@latest my-vue-app -- --template vue
接下来就要开始编写插件了,我说过,所谓插件就是独立的组件,
结合我以上给大家建议的两点,按照规矩办事
首先我当前组件是高仿抖音
的插件
然后利用分层原则,我们仔细分析以上结构(注意不要看盯着明星看)。
我们发现,当前结构可以归纳为两个组件文件,
- 1、 视频组件
- 2、滑动组件
视频组件
所谓视频组件,在前端领域,其实就是将video
做一层包装,重写样式,以及交互
代码如下:
<template>
<div class="item-box" @click="pause">
// 视频标签
<video
v-if="index === activeIndex"
id="td-video"
ref="video"
class="item-box--video"
playsinline="true"
webkit-playsinline="true"
mediatype="video"
:poster="poster"
@progress="progress"
@durationchange="durationchange"
@loadeddata="loadeddata"
@playing="playing"
@waiting="waiting"
@timeupdate="timeupdate"
@canplay="playing"
@ended="ended"
>
<source :src="src" type="video/mp4" />
</video>
// 重写封面图逻辑
<img
:src="poster"
alt=""
class="item-box--video"
v-if="index === activeIndex + 1 || index === activeIndex - 1 || showImg"
/>
// 利用插槽,解决动态传值问题
<slot></slot>
// 重写的样式
<div class="item-box--play" v-if="paused" @click.stop="play">
<img src="../assets/icon_4.png" alt="" />
</div>
<div class="template-loading" v-if="loading"></div>
<div class="item-box__progress" @click.stop="">
<div class="progress-time" v-if="isPress">
<span class="start">{{ startTime }}</span
>/<span class="end">{{ endTime }}</span>
</div>
<div class="progress" @click="handleProgress">
<div
class="progress-buffer"
:style="`width:${percentageBuffer}%`"
></div>
<div
ref="progressSpeed"
class="progress-speed"
@touchstart="touchstart"
@touchend="touchend"
@touchmove="touchmove"
:style="`left:${percentage}%`"
@click.stop=""
>
<div class="progress-speed--btn"></div>
</div>
</div>
</div>
</div>
</template>
<script lang="ts">
import { ref, defineComponent, onMounted } from "vue";
import { second } from "@/utils";
export default defineComponent({
props: {
poster: {
type: String,
default: "",
},
src: {
type: String,
default: "",
},
index: {
type: Number,
default: 0,
},
activeIndex: {
type: Number,
default: 0,
},
autoplay: {
type: Boolean,
default: false, // 浏览器在没有交互的情况下无法播放
},
},
setup({ autoplay }) {
// 是否是暂停状态
const paused = ref(true);
// 视频总时间
const endTime = ref(second(0));
//播放的时间
const startTime = ref(second(0));
// 是否是按下状态
const isPress = ref(false);
//缓冲进度
const percentageBuffer = ref(0);
// 播放进度
const percentage = ref(0);
// 保存计算后的播放时间
const calculationTime = ref(0);
// 拿到video 实例
const video = ref(null);
// 是否展示封面图
const showImg = ref(true);
// 是否处于缓冲中
const loading = ref(false);
// 播放
function play() {
video.value.play();
paused.value = false;
}
// 暂停
function pause() {
if (paused.value) return;
video.value.pause();
paused.value = true;
loading.value = false;
}
// 获取缓冲进度
function progress() {
if (!video.value) return;
percentageBuffer.value = Math.floor(
(video.value.buffered.length
? video.value.buffered.end(video.value.buffered.length - 1) /
video.value.duration
: 0) * 100
);
}
// 时间改变
function durationchange() {
endTime.value = second(video.value.duration);
console.log("时间改变触发");
}
// 首帧加载触发,为了获取视频时长
function loadeddata() {
console.log("首帧渲染触发");
showImg.value = false;
autoplay && play();
}
//当播放准备开始时(之前被暂停或者由于数据缺乏被暂缓)被触发
function playing() {
console.log("缓冲结束");
loading.value = false;
}
//缓冲的时候触发
function waiting() {
console.log("处于缓冲中");
loading.value = true;
}
// 时间改变触发
function timeupdate() {
// 如果是按下状态不能走进度,表示需要执行拖动
if (isPress.value || !video.value) return;
startTime.value = second(Math.floor(video.value.currentTime));
percentage.value = Math.floor(
(video.value.currentTime / video.value.duration) * 100
);
}
// 按下开始触发
function touchstart() {
isPress.value = true;
}
//松开按钮触发
function touchend() {
isPress.value = false;
video.value.currentTime = calculationTime.value;
}
// 拖动的时候触发
function touchmove(e) {
const width = window.screen.width;
const tx = e.clientX || e.changedTouches[0].clientX;
if (tx < 0 || tx > width) {
return;
}
calculationTime.value = video.value.duration * (tx / width);
startTime.value = second(Math.floor(calculationTime.value));
percentage.value = Math.floor((tx / width) * 100);
}
//点击进度条触发
function handleProgress(e) {
touchmove(e);
touchend();
}
// 播放结束时触发
function ended() {
play();
}
onMounted(() => {});
return {
video,
paused,
pause,
play,
progress,
durationchange,
loadeddata,
endTime,
startTime,
playing,
percentage,
waiting,
timeupdate,
percentageBuffer,
touchstart,
touchend,
touchmove,
isPress,
ended,
handleProgress,
loading,
showImg,
};
},
});
</script>
<style lang="scss">
.item-box {
height: 100%;
width: 100%;
position: relative;
background-color: #3e3a3a;
z-index: 1;
&--video {
width: 100%;
object-fit: cover;
cursor: pointer;
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
}
&--play {
position: absolute;
width: 53px;
height: 53px;
left: 50%;
top: 50%;
margin-left: -26.25px;
margin-top: -26.25px;
img {
width: 100%;
}
}
.template-loading {
position: absolute;
left: 50%;
margin-left: -36px;
width: 10px;
height: 10px;
border-radius: 50%;
box-shadow: 28px 0 0 #ff552e, 50px 0 0 #3e80f8, 72px 0 0 #53e7ae,
94px 0 0 #ffce5a;
animation: spinner-dot 1.2s infinite linear;
transform: translateX(-28px);
top: 50%;
margin-top: -5;
}
&__progress {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 115px;
background: linear-gradient(
180deg,
rgba(0, 0, 0, 0) 0%,
rgba(0, 0, 0, 0.4) 100%
);
.progress-time {
padding: 5px 20px;
background-color: rgba(0, 0, 0, 0.7);
border-radius: 18.5px;
position: absolute;
left: 50%;
bottom: 67px;
transform: translateX(-50%);
color: rgba(255, 255, 255, 1);
.start {
font-size: 13px;
font-weight: 700;
line-height: 18.5px;
display: inline-block;
padding-right: 3px;
}
.end {
font-size: 13px;
font-weight: 700;
line-height: 18.5px;
color: rgba(255, 255, 255, 0.6);
display: inline-block;
padding-left: 3px;
}
}
.progress {
position: absolute;
width: 100%;
height: 2px;
background: rgba(255, 255, 255, 0.5);
bottom: 3px;
left: 0;
&-buffer {
position: absolute;
left: 0;
top: 0;
height: 2px;
background: rgba(255, 255, 255, 0.5);
transition: width 1s;
}
&-speed {
position: absolute;
left: 0;
top: 0;
width: 30px;
height: 30px;
margin-left: -15px;
margin-top: -15px;
&--btn {
position: absolute;
left: 50%;
top: 50%;
width: 6px;
height: 6px;
margin-left: -3px;
margin-top: -2px;
background: rgba(255, 255, 255, 1);
border-radius: 50%;
}
}
}
&::before {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 3px;
background: #000;
content: "";
}
}
}
@keyframes spinner-dot {
20% {
box-shadow: 28px -10px 0 #ff552e, 50px 0 0 #3e80f8, 72px 0 0 #53e7ae,
94px 0 0 #ffce5a;
}
40% {
box-shadow: 28px 0 0 #ff552e, 50px -10px 0 #3e80f8, 72px 0 0 #53e7ae,
94px 0 0 #ffce5a;
}
60% {
box-shadow: 28px 0 0 #ff552e, 50px 0 0 #3e80f8, 72px -10px 0 #53e7ae,
94px 0 0 #ffce5a;
}
80% {
box-shadow: 28px 0 0 #ff552e, 50px 0 0 #3e80f8, 72px 0 0 #53e7ae,
94px -10px 0 #ffce5a;
}
}
</style>
滑动组件
这里我们之所以将滑动组件和视频组件分开,遵循分层原则,其实原因很简单,既然是插件,就要通用,可拓展
举个例子,如果我滑动的不是视频呢?是图片呢?
此时你就会发现解耦是非常有必要的
注意:这里我们利用第三方的swiper
来解决滑动问题
代码如下:
<template>
// 封装swiper
<swiper
class="my-swiper"
direction="vertical"
@transitionStart="transitionStart"
>
<swiper-slide class="slide-box" v-for="(item, index) in list" :key="index">
// 利用插槽 来动态更改滑动内容
// 并且性能优化保证只有当前屏以及上一屏下一屏有内容显示,其他屏为空节点
<slot
:item="item"
:index="index"
:activeIndex="activeIndex"
v-if="activeIndex >= index - 1 && activeIndex <= index + 1"
></slot>
</swiper-slide>
</swiper>
</template>
<script>
import { defineComponent, ref } from "vue";
import { Swiper, SwiperSlide } from "swiper/vue";
import "swiper/css";
export default defineComponent({
props: {
list: {
type: Array,
default: () => [],
},
},
components: {
Swiper,
SwiperSlide,
},
setup({ list }, { emit }) {
const activeIndex = ref(0);
function transitionStart(swiper) {
//表示没有滑动,不做处理
if (activeIndex.value === swiper.activeIndex) {
// 表示是第一个轮播图
if (swiper.swipeDirection === "prev" && swiper.activeIndex === 0) {
emit("refresh");
} else if (
swiper.swipeDirection === "next" &&
swiper.activeIndex === list.length - 1
) {
emit("toBottom");
}
} else {
activeIndex.value = swiper.activeIndex;
// 为了预加载视频,提前load 数据
if (swiper.activeIndex === list.length - 1) {
emit("load");
}
}
}
return {
transitionStart,
activeIndex,
};
},
});
</script>
<style lang="scss" scoped>
.my-swiper {
height: 100%;
position: relative;
.slide-box {
width: 100%;
height: 100%;
}
}
</style>
好了,组件写完了,接下来,我们就要将它变成插件,其实很简单只需要三步走
导出打包
import Yvideo from './video.vue'
import Yslide from './slide.vue'
// 导出全局注册
function install(app) {
app.component('Yslide', Yslide)
app.component('Yvideo', Yvideo)
}
// 局部导出
export { Yvideo, Yslide }
export default {
install
}
打包配置如下:
import { defineConfig } from 'vite'
const path = require('path')
import vue from '@vitejs/plugin-vue'
import dts from 'vite-plugin-dts'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue(), dts()],
build: {
lib: {
entry: path.resolve(__dirname, 'src/components/index.ts'),
name: 'videoSlide',
fileName: (format) => `index.${format}.js`
},
rollupOptions: {
// 确保外部化处理那些你不想打包进库的依赖
external: ['vue'],
output: {
// 在 UMD 构建模式下为这些外部化的依赖提供一个全局变量
globals: {
vue: 'Vue'
}
}
}
},
resolve: {
alias: {
"@": "/src",
},
},
})
发布npm包
- 在 npm 官网 www.npmjs.com/ 注册并创建 npm 账号
- 登录npm
// 登陆
npm login
// 控制台会提示输入相关信息
Log in on https://registry.npmjs.org/
Username: // 用户名
Password: // 密码
Email: (this IS public) // 邮箱
Enter one-time password: // 如果之前做过 双因素身份验证 (2FA),需要生成一次性密钥
Logged in as xxx on https://registry.npmjs.org/.
3、配置package.json
npm 包的引入是有规则的,我们只需要 更改版本以及main
和module
的指向路径即可
代码如下:
{
"name": "video-slide",
"version": "0.0.0", // 注意每次发版都要更改版本
"main": "dist/index.umd.js",
"module": "dist/index.es.js",
"style": "dist/styles.css",
}
4、上传发布插件
这一步就很简单了
// 发布
npm publish
然后就能在npm 中查到你发布的插件了
使用组件插件
使用组件插件其实就很简单了,我们只需要
npm i video-slide
// 全局使用
import videoSlide from 'video-slide'
createApp.use(videoSlide)
// 局部引入
import {Yslide,Yvideo} from 'video-slide'
components: {
Yslide,
Yvideo,
},
<Yslide
:list="data"
v-slot="{ item, index, activeIndex }"
@refresh="refresh"
@toBottom="toBottom"
@load="load"
>
<Yvideo
:src=""
:poster=""
:index="index"
:activeIndex="activeIndex"
autoplay
>
</Yvideo>
</Yslide>
hooks插件
hooks
这个时髦的词是react
推出来的一个功能,主要为了解决代码复用的问题,而作为中庸
的vue
这么好的东西。
怎么能不借鉴呢?
于是,他发明了一个叫Compoosition Api
的东西,来组合成hooks
到底什么是hooks
其实官方也没有明确的定义,而我理解的hooks
就是一个存放Compoosition Api
的函数,从而实现代码复用
代码如下:
实现一个加法功能-Hook
import { ref, watch } from 'vue';
const useAdd= ({ num1, num2 }) =>{
const addNum = ref(0)
watch([num1, num2], ([num1, num2]) => {
addFn(num1, num2)
})
const addFn = (num1, num2) => {
addNum.value = num1 + num2
}
return {
addNum,
addFn
}
}
export default useAdd
// 在代码中使用
<script setup>
import { ref } from 'vue'
import useAdd from './useAdd.js' //引入自动hook
const num1 = ref(2)
const num2 = ref(1)
//加法功能-自定义Hook(将响应式变量或者方法形式暴露出来)
const { addNum, addFn } = useAdd({ num1, num2 })
subFn(num1.value, num2.value
console.log(addNum)
</script>
代码的基本使用结束了,但是这却不是hooks真正的威力,我们可以利用他做很多事情,比如多组件值的共享实现类似vuex
的功能
众所周知, 我们的Compoosition Api
必须要在setup 中导出,却让很多人形成了一个思维定式,我必须要在setup中声明
其实,我们在哪里声明都可以,如此一来,我们可以设想一下,在一个全局的地方声明, 在各个组件中引入声明后的值,这样就能在全组件实现状态以及值的同步,就再也不用使用vuex
等状态管理插件了,我们可以自己写个插件来定制自己的业务。
代码如下:
// 测试hooks
import { ref } from "vue";
const res = ref(1);
export const useTest = () => {
return { res };
};
// app.vue中修改
<script setup lang="ts">
import HelloWorld from "./components/HelloWorld.vue";
import { useTest } from "./components/test";
const { res } = useTest();
setTimeout(() => {
res.value = 333;
}, 1000);
</script>
// HelloWorld.vue中也能感受到变化,实现全局状态
<script setup lang="ts">
import { useTest } from "./test";
const { res } = useTest();
console.log(res);
</script>
<template>
<h1>{{ res }}</h1>
</template>
相信完成这个壮举之后,别人用着你的全局状态,你觉得你还能被替代吗?
函数插件
所谓函数插件,顾名思义,他是一个函数。
额,各位jym
先不要开骂,我还没说完,他是一个函数,却是一个,包含能和vue
结合的插件,并且能带来意想不到的功能
这个招数在各个ui插件中,屡见不鲜
之所以 屡见不鲜 ,原因很简单——好用
例如
ElMessage({
type: 'info',
message: '这是个弹窗',
})
以上代码中,是一个消息提示弹窗,我们只需要在使用的时候调用即可,而并不需要写个组件然后引入这等麻烦的操作。
那具体他是怎么实现的呢?
之前我们说过,函数插件要跟vue
结合
说人话,就是要使用vue的函数
。
在开始讲之前,我们先来说说vue的核心能力是什么?
一句话概括,隐藏底层的dom操作
帮助我们更好的画界面,仅此而已
所以,vue
一定提供了,渲染dom 的操作
reender/createVNode 函数
render 函数是讲虚拟dom 变成dom
createVNode 见名知意, 就是创建虚拟dom 的
如果有jy
问我什么叫虚拟dom ?
额,那这边建议你去学习react
使用方式如下
// 生成虚拟dom ,当然可以是组件生成的
const vnode = createVNode(
MessageBoxConstructor,
props,
)
// 保存实例
vnode.appContext = appContext
// 渲染
render(vnode, container)
以上代码中,就是我们函数插件的核心,利用vue
的能力,将组件变成dom
即可!
如此一来,我们只需要在函数中包装即可。
// 组件弹窗模板 app.vue
<template>
<div>这是一个弹窗</div>
</template>
<script setup>
</script>
// 函数
import MessageConstructor from './app.vue'
function createMessage(){
const container = document.createElement('div')
const vnode = createVNode(
MessageConstructor,
props,
)
// 保存实例
vnode.appContext = appContext
// 渲染
render(vnode, container)
// 挂载
appendTo.appendChild(container.firstElementChild!)
}
// 代码项目中使用
createMessage()
最后
本文简单的梳理了一下常见的插件开发方式, 而当我们开发完了以后,还需要最后且最重要的一步,给别人使用,于是我们就需要使用npm
这个node 内置的包管理工具
而npm
怎么上传包,我们就不再赘述,因为我们前面已经讲过了
有兴趣可以参考本渣写的几个插件的配置方式