上周跟大家分享了如何使用vue的自定义指令实现自定义浏览器右键菜单,大家都觉得挺有意思的,这次我把它做成了插件,上传到了npm仓库。
在做这个插件的过程中,踩了蛮多坑,本文就跟大家分享下我的实现思路以及过程,欢迎各位感兴趣的开发者阅本文。
一开始我是直接用的typescript的tsc命令进行打包的,但是我的插件里用到了vue、scss,发现要把这些文件打包进去需要自己去配webpack。
我记得好久之前,我用Vue CLI 2.x创建项目时,可以选择当前要创建的项目是插件还是web项目,现在用的是Vue ClI 4.x了,在创建项目时没看到有这个选项。
于是,我带着侥幸的心理,去Vue CLI 官网找了一波,还真就被我找到了,它的build
指令有个target
选项,可以选择将其打包成一个插件,它的具体使用方法:vue-cli-service build。
既然Vue CLI提供了现成的解决方案,那就用它提供的吧。
create
命令创建一个名为vue-right-click-menu-next
的项目vue create vue-right-click-menu-next
项目创建好后,我们删掉CLI初始化时创建的东西,然后修改package.json中的内容。
在package.json中,CLI默认是把vue
和core-js
放在dependencies
下的,我们开发的插件是要给其他开发者引用的,如果我们打包的产物中包含Vue包的话可能会引发各种问题,比如用户可能会在引入我们的包之后会在runtime时创建两个不用的Vue实例,所以vue插件的package.json里一定不能将其放在dependencies中,而是要放在peerDependencies
中,表明会从引用者的其他的包中引入相对应的包,而不会在这个包里直接引入。
"peerDependencies": {
"core-js": "^3.6.5",
"vue": "^3.0.0"
}
{
"@commitlint/cli": "^11.0.0",
"@commitlint/config-angular": "^11.0.0",
"commitizen": "^4.2.2",
"cz-conventional-changelog": "^3.3.0",
"husky": "^4.3.0",
}
{
"config": {
"commitizen": {
"path": "./node_modules/cz-conventional-changelog"
}
},
"husky": {
"hooks": {
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
}
}
}
{
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s",
"commit": "git-cz"
}
由文档可知,可以通过vue-cli-service build --target lib --name myLib [entry]
命令,将一个单独的入口构建为一个库。
那么,我们就可以在package.json的script标签里添加build命令用以执行插件的打包,代码如下。
{
"build": "vue-cli-service build --target lib --name vueRightMenuPlugin src/main.ts",
}
由于我们的插件启用了typescript,使用它的默认打包,不会帮我们生成ts声明文件,使用我们插件的开发者项目可能会启用typescript,在引用插件时就会报错声明文件不存在,因此我们需要额外做下述操作:
tsconfig.json
z中添加下述代码,打包时在项目的指定位置自动生成配置文件。{
"declaration": true,// 是否生成声明文件
"declarationDir": "dist/lib",// 声明文件打包的位置
}
vue.config.js
文件,关闭并行打包的一些相关配置。module.exports = {
chainWebpack: config => {
if (process.env.NODE_ENV === "production") {
config.module.rule("ts").uses.delete("cache-loader");
config.module
.rule("ts")
.use("ts-loader")
.loader("ts-loader")
.tap(opts => {
opts.transpileOnly = false;
opts.happyPackMode = false;
return opts;
});
}
},
parallel: false
};
做完上述操作后,我们运行打包命令时就能自动生成声明文件了。
当我把插件开发完,测试时发现我引用的组件样式丢了,找了好久问题,最后在CLI的文档中找到了问题所在,他有个css.extract属性,它使用来配置打包时是否将css样式提取到独立的文件中,Default: 生产环境下是 true
,开发环境下是 false
,我们打包时他默认是true
,用户需要单独引入这个样式文件文件。
我们可以通过手动将其设置为false
,让其在打包时使用内联样式,这样就能解决样式失效的问题了,我们在vue.config.js
中加入下述代码。
module.exports = {
// 强制css内联
css: { extract: false }
}
做完上述操作,我们跟打包有关的相关的配置就弄好了,接下来我们在package.json中添加库的相关描述,让npm可以正确识别我们的插件。
{
"name": "vue-right-click-menu-next",
"version": "1.0.0",
"description": "支持vue3的右键菜单插件",
"private": false,
"main": "dist/vueRightMenuPlugin.common.js",
"types": "dist/lib/main.d.ts",
"publisher": "magicalprogrammer@qq.com",
"repository": {
"type": "git",
"url": "git+https://github.com/likaia/vue-right-click-menu-next.git"
},
"keywords": [
"vuejs",
"vue3",
"vue",
"rightMenu",
"右键菜单",
"vueRightMenu"
],
"author": "likaia",
"license": "MIT",
"bugs": {
"url": "https://github.com/likaia/vue-right-click-menu-next/issues"
},
"homepage": "https://github.com/likaia/vue-right-click-menu-next#readme",
}
完整的配置文件请移步:package.json
上篇文章我们的实现思路是需要vuex来做全局状态管理,控制右键菜单的显隐,这次我们要把它做成插件,再使用vuex的话,使用我们插件的人就需要必须引入vuex才行,那就有点不合适了。
经过一番思考后,我有了下述思路:
props
向组件传值。createApp
来加载组件,向组件内部传值,创建一个组件容器完成上述操作后,我们就实现了让右键菜单显示到指定位置,但是要怎么隐藏它呢,经过一番思考后,我又想到了下述思路:
menuVM
,默认声明为nullmenuVM
去接收其返回值menuVM
不为null,我们就把这个元素移除menuVM
不为null,表示它上次点开的右键菜单没关,这样就会出问题,因此我们也需要将其从body中移除分析出实现思路后,接下来我们就着手将其实现吧。
在项目的src下创建components文件夹,在文件夹下创建right-menu.vue
文件,样式和组件内容此处我们就不贴了,这里贴一下组件需要传的参数,完整代码请移步:right-menu.vue)
<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
name: "right-menu",
props: {
rightMenuStatus: String,
rightMenuTop: String,
rightMenuLeft: String,
rightMenuList: Array
}
});
</script>
我们可以通过vue3的createApp
方法来加载一个组件,并给他传值,然后挂载到某个dom节点上,代码如下:
/**
* 将组件挂在到节点上
* @param comp 需要挂载的组件
* @param prop 向组件传的参数
*/
const creatComp = function(comp: Component, prop: rightMenuAttribute) {
// 创建组件
const app = createApp(comp, {
...prop
});
// 创建一个div元素
const divEle = document.createElement("div");
// 将创建的div元素挂载追加至body里
document.body.appendChild(divEle);
// 将组件挂载至刚才创建的div中
app.mount(divEle);
// 返回挂载的元素,便于操作
return divEle;
};
接下来,我们在插件的install方法中,注册一个vue指令rightClick
,拦截它的右键事件,获取组件传过来来的参数,挂载组件,渲染右键菜单。代码如下:
install(app: App): void {
// 创建指令
app.directive("rightClick", (el, binding): boolean | void => {
// 指令绑定元素元素是否存在判断
if (el == null) {
throw "右键指令错误:元素未绑定";
}
el.oncontextmenu = function(e: MouseEvent) {
if (menuVM != null) {
// 销毁上次触发的右键菜单DOM
document.body.removeChild(menuVM);
menuVM = null;
}
const textArray = binding.value.text;
const handlerObj = binding.value.handler;
// 菜单选项与事件处理函数是否存在
if (textArray == null || handlerObj == null) {
throw "右键菜单内容与事件处理函数为必传项";
}
// 事件处理数组
const handlerArray = [];
// 处理好的右键菜单
const menuList = [];
// 将事件处理函数放入数组中
for (const key in handlerObj) {
handlerArray.push(handlerObj[key]);
}
if (textArray.length !== handlerArray.length) {
// 文本数量与事件处理不对等
throw "右键菜单的每个选项,都必须有它的事件处理函数";
}
// 追加右键菜单数据
for (let i = 0; i < textArray.length; i++) {
// 右键菜单对象, 添加名称
const menuObj: rightMenuObjType = {
text: textArray[i],
handler: handlerArray[i],
id: i + 1
};
menuList.push(menuObj);
}
// 鼠标点的坐标
const oX = e.clientX;
const oY = e.clientY;
// 动态挂载组件,显示右键菜单
menuVM = creatComp(rightMenu, {
rightMenuStatus: "block",
rightMenuTop: oY + "px",
rightMenuLeft: oX + "px",
rightMenuList: menuList
});
return false;
};
});
}
当用户点击完右键菜单后,我们需要对组件进行销毁,让其隐藏,因此我们在插件的install创建一个对body的点击监听,然后移除我们挂载的组件,代码如下:
install(app: App): void {
// 监听全局点击,销毁右键菜单dom
document.body.addEventListener("click", () => {
if (menuVM != null) {
// 销毁右键菜单DOM
document.body.removeChild(menuVM);
menuVM = null;
}
});
}
完整代码请移步:main.ts)
做完上述操作后,我们的插件就开发完成了,可以打包然后发布到npm仓库了。
终端执行下述命令:
npm publish --access public
插件发布成功:vue-right-click-menu-next
插件是不兼容Vue2.x的,因为creatApp时Vue3新增的语法,一开始我本来想用Vue2.x的extend来实现组件挂载的,发现Vue3把这个语法舍弃了。
这就造成了我需要写两套插件,维护两个插件。
插件的逻辑层面没有啥区别,只有挂载组件写法的不同,Vue2.x中需要使用下述写法:
/**
* 将组件挂在到节点上
* @param comp 需要挂载的组件
* @param prop 向组件传的参数
*/
const creatComp = function(comp, prop) {
// 创建组件
const app = Vue.extend(comp);
// 创建一个div元素
const divEle = document.createElement("div");
// 将创建的div元素挂载追加至body里
document.body.appendChild(divEle);
// 将组件挂载至刚才创建的div中, 使用propsData进行传参
new app({
propsData: {
...prop
}
}).$mount(divEle);
// 返回挂载的元素,便于操作
return divEle;
};
插件地址:vue-right-click-menu
本文中开发的插件代码地址:vue-right-click-menu | vue-right-click-menu-next)
在线体验地址:chat-system