大家都知道在vue2中相互调用组件的方法是很麻烦的一件事。比如说要调用子孙组件的的方法,需要refs,refs一层一层往下面找;调用上层组件的可以在组件上挂载,一层可以用v-on 和 emit解决,多层可以用provide和inject,要是多个兄弟组件间呢? 唯一方便点的就是eventBus,bus每次on事件后,都要记得在beforeDestory里面on事件后,都要记得在beforeDestory里面on事件后,都要记得在beforeDestory里面off事件,不然会多次on事件,在on事件,在on事件,在emit触发的时候就会执行多次,导致bug,另外在项目里面bus使用的多了,$emit时具体调用的是在调用的哪一个,在出现重名事件时就会让人非常头疼了,于是我就试着自己实现解决这个问题。
我打算用全局mixin来做这个功能。本来打算在每个组件里面定义name来绑定methods的,考虑到这样做每个vue组件里面都要自己手动定义name,而且也容易存在重名的情况,于是我就打算用vue组件所在的路径来做,我发现vue组件实例上$options的prototype下有个__file属性记录了当前文件的路径,当时生产环境下就没有了,于是我想到了写个weboack插件来实现,另外吐槽下webpack的钩子真的多,示例不清晰。vue2项目大多数都是使用的js,代码提示用jsconfig.json结合types, js代码里面用注释jsdoc语法添加代码提示。
直接在组件里面调用globalDispatch方法,有代码提示的哦,考虑到一个组件可能同时调用了多次,所有可以多传一个eKey 进行精确emit。在组件上可以进行eKey绑定(也可以写e-key)。
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 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 |
import Vue from "vue";
const DEFAULT_E_KEY = "__default"; /** * 方法合集 * @type {Record */ const events = {};
/** * 全局调用event的mixin * @type {Vue & import("vue").ComponentOptions} */ const globalDispatch = { created() { const attrs = this.$attrs; const eKey = attrs.eKey ?? attrs["e-key"]; const filePath = this.$options.__file ?? this.$options.__filePath; filePath && addEvents(filePath, this, eKey); }, destroyed() { const filePath = this.$options.__file ?? this.$options.__filePath; filePath && removeEvents(filePath, this); } };
/** * 监听方法 * @param {string} filePath 获取到的路径 * @param {Vue} vm vue组件实例 * @param {string=} eKey event key */ function addEvents(filePath, vm, eKey = DEFAULT_E_KEY) { const methods = vm.$options.methods; if (methods) { Object.entries(methods).forEach(([key, handler]) => { handler = handler.bind(vm); handler.vm = vm; const eventKey = `${filePath}:${key}`; const event = { eKey, handler };
if (events[eventKey] && events[eventKey].length) { events[eventKey].push(event); } else { events[eventKey] = [event]; } }); } }
/** * 移除方法 * @param {string} filePath 获取到的路径 * @param {Vue} vm vue组件实例 */ function removeEvents(filePath, vm) { Object.keys(events).forEach(key => { if (key.startsWith(filePath)) { events[key] = events[key].filter(v => v.handler.vm !== vm); } }); }
/** * * @param {import("../../types/event-keys").EventKeys | import("../../types/shims-vue").EventParams} params * @param {...any} args * @returns */ Vue.prototype.globalDispatch = function dispatch(params, ...args) { let eventKey, eKey = DEFAULT_E_KEY; if (typeof params === "string") { eventKey = params; } else if (typeof params === "object") { eventKey = params.target; eKey = params.eKey ?? DEFAULT_E_KEY; }
const eKeyMsg = eKey !== DEFAULT_E_KEY ? `eKey:${eKey},` : "";
if ( !eventKey || typeof eventKey !== "string" || !/^[^:]*:[^:](.*){1}$/.test(eventKey) ) { throw new Error(`${eKeyMsg}eventKey:${eventKey}, 参数不正确!`); }
const handlers = events[eventKey]?.filter(v => v.eKey === eKey); if (handlers && handlers.length) { const results = handlers.map(v => v.handler(...args)); if (results.length === 1) return results[0]; return results.map(result => ({ eKey, result })); }
const method = eventKey.split(":")[1]; throw new Error(`${eKeyMsg}method:${method},该方法未找到!`); };
export default globalDispatch; |
这个文件主要添加所有的组件的methods到events里面,在Vue.prototype上挂载globalDispatch 方法,方便在vue组件上使用。
我用的时vue2.7版本写的,主要时include把types文件夹的文件加进来
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
{ "compilerOptions": { "moduleResolution": "node", "target": "esnext", "baseUrl": ".", "allowJs": true, "sourceMap": false, "strict": true, "jsx": "preserve", "module": "ESNext", "paths": { "@/*": ["./src/*"] }, "lib": ["DOM", "ESNext"] }, "vueCompilerOptions": { "target": 2.7 }, "exclude": ["node_modules", "dist"], "include": ["src/**/*.js", "src/**/*.vue", "types/**/*.ts", "types/**/*.d.ts"] } |
在types文件夹下新建shims-vue.d.ts, 因为globalDispatch需要支持两种传参形式,所以使用重载
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
import Vue from "vue"; import { EventKeys } from "./event-keys";
export type EventParams = { target: EventKeys; eKey: string };
function globalDispatch(eventKey: EventKeys, ...args: any[]): any; function globalDispatch(eventParams: EventParams, ...args: any[]): any;
declare module "vue/types/vue" { interface Vue { /** * 全局互相调用event的dispatch */ globalDispatch: typeof globalDispatch; } } |
在types文件夹下新建event-keys.d.ts, 这个文件是用来给globalDispatch的第一个参数做代码提示的,手动写可以,写个webpack插件自动读取vue文件的路径和方法自动生成更好,下面会贴出来。
1 |
export type EventKeys = "src/App.vue:onClick" | "src/views/IndexView.vue:test"; |
在项目根目录下新建plugins文件夹
开发者模式下才需要生成event-keys.d.ts,先递归找出所有的vue文件的路径,然后读取文件,用acorn库解析,找出文件的methods里的所有方法名,用prettier格式化后写入到event-keys.d.ts,在项目启动和文件变化后都会执行,在添加methos里新方法或删除后,会执行写入。
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 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 |
const fs = require("fs"); const path = require("path"); const acorn = require("acorn"); const prettier = require("prettier"); const prettierConfig = require("../prettier.config");
/** * @typedef {import("webpack/lib/Compiler")} Compiler */
const PLUGIN_NAME = "global-dispatch"; const KEYS_PATH = path.resolve(__dirname, "../types/event-keys.d.ts");
class TransformFilePathPlugin { /** * @param {Compiler} compiler * @returns {void} */ apply(compiler) { compiler.hooks.done.tap(PLUGIN_NAME, () => { process.env.NODE_ENV === "development" && writeEventKeys(); }); } }
function writeEventKeys() { const vueFilePaths = getFilePath(); writeVueKeyPaths(vueFilePaths); }
/** * 缓存内容,防止重复写入 */ let keysContentCache = fs.readFileSync(KEYS_PATH, "utf-8");
/** * 写入__filePath到type Key文件 * @param {string[]} paths 路径集合 */ function writeVueKeyPaths(paths) { let keysContent = "export type EventKeys ="; const keys = [];
paths.forEach(p => { let content = fs.readFileSync(getSrcPath(p), "utf-8"); const scriptMatch = content.match(/", startIndex); content = content.substring(startIndex, endIndex);
const ast = acorn.parse(content, { sourceType: "module" }); const defaultExportAst = ast.body.find( v => v.type === "ExportDefaultDeclaration" );
let properties; if (defaultExportAst.declaration.type === "CallExpression") { properties = defaultExportAst.declaration.arguments[0].properties; } if ( defaultExportAst.declaration.type === "ObjectExpression" && Array.isArray(defaultExportAst.declaration.properties) ) { properties = defaultExportAst.declaration.properties; }
const methods = properties.find(v => v.key.name === "methods"); if (!methods) return;
if (methods.value.properties.length) { const methodNames = methods.value.properties.map( v => `${p}:${v.key.name}` ); keys.push(...methodNames); } });
keysContent += keys.map(v => `'${v}'`).join("|") || "string";
keysContent = prettier.format(keysContent, { ...prettierConfig, parser: "typescript" });
if (keysContentCache !== keysContent) { keysContentCache = keysContent; fs.writeFileSync(KEYS_PATH, keysContent); } }
/** * * @param {string=} p 路径 * @returns {string[]} 路径集合 */ function getFilePath(p = "src") { const paths = fs.readdirSync(getSrcPath(p), "utf-8"); const vueFiles = getVueFiles(paths, p); const dirs = getDirs(paths, p);
if (dirs.length) { dirs.forEach(dir => { vueFiles.push(...getFilePath(dir)); }); } return vueFiles; }
function getDirs(paths, path) { return paths .map(v => `${path}/${v}`) .filter(v => fs.statSync(v).isDirectory()); }
function getVueFiles(paths, path) { return paths.filter(v => v.endsWith(".vue")).map(v => `${path}/${v}`); }
function getSrcPath(p) { return path.resolve(__dirname, "../" + p); }
module.exports = { TransformFilePathPlugin }; |
这个文件是用来在vue实例上添加__filePath属性的,本来是想写在上面的插件一起的,无奈没有在webpack文档等地方找到在plugins里添加loader的方法,在vue-loader源码里也没有好的体现。 在开发者环境下vue的$options下有__file可以用,所以只需要生产环境启用
1 2 3 4 5 6 7 8 9 10 11 |
module.exports = function(content) { if (process.env.NODE_ENV === "development") return content;
const filePath = this.resourcePath .replace(/\/g, "/") .replace(/(.*)?src/, "src");
const reg = /export default.*?{/; content = content.replace(reg, $0 => `${$0} __filePath: "${filePath}",`); return content; }; |
添加configureWebpack里的即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
const path = require("path"); const { TransformFilePathPlugin } = require("./plugins/global-dispatch"); /** * @type {import('@vue/cli-service').ProjectOptions} */ module.exports = { lintOnSave: false, productionSourceMap: false, configureWebpack: { plugins: [new TransformFilePathPlugin()], module: { rules: [ { test: /.vue$/, use: [ { loader: path.resolve(__dirname, "./plugins/vue-path-loader.js") } ] } ] } } }; |
ywenhao/vue2-global-dispatch (github.com) https://github.com/ywenhao/vue2-global-dispatch