vue2中組件互相調用實例methods中的方法實現詳解
前言:
大家都知道在vue2中相互調用組件的方法是很麻煩的一件事。比如說要調用子孫組件的的方法,需要refs,refs一層一層往下面找;調用上層組件的可以在組件上掛載,一層可以用v-on 和 emit解決,多層可以用provide和inject,要是多個兄弟組件間呢? 唯一方便點的就是eventBus,bus每次on事件后,都要記得在beforeDestory里面on事件后,都要記得在beforeDestory里面on事件后,都要記得在beforeDestory里面off事件,不然會多次on事件,在on事件,在on事件,在emit觸發(fā)的時候就會執(zhí)行多次,導致bug,另外在項目里面bus使用的多了,$emit時具體調用的是在調用的哪一個,在出現重名事件時就會讓人非常頭疼了,于是我就試著自己實現解決這個問題。
開始前:
我打算用全局mixin來做這個功能。本來打算在每個組件里面定義name來綁定methods的,考慮到這樣做每個vue組件里面都要自己手動定義name,而且也容易存在重名的情況,于是我就打算用vue組件所在的路徑來做,我發(fā)現vue組件實例上$options的prototype下有個__file屬性記錄了當前文件的路徑,當時生產環(huán)境下就沒有了,于是我想到了寫個weboack插件來實現,另外吐槽下webpack的鉤子真的多,示例不清晰。vue2項目大多數都是使用的js,代碼提示用jsconfig.json結合types, js代碼里面用注釋jsdoc語法添加代碼提示。
使用
直接在組件里面調用globalDispatch方法,有代碼提示的哦,考慮到一個組件可能同時調用了多次,所有可以多傳一個eKey 進行精確emit。在組件上可以進行eKey綁定(也可以寫e-key)。




第一步、定義全局mixin
import Vue from "vue";
const DEFAULT_E_KEY = "__default";
/**
* 方法合集
* @type {Record<string, {eKey: string; handler: function}[]>}
*/
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);
}
};
/**
* 監(jiān)聽方法
* @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組件上使用。
第二步添加代碼提示d.ts聲明文件
在項目下新建jsconfig.json
我用的時vue2.7版本寫的,主要時include把types文件夾的文件加進來
{
"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"]
}添加shims-vue.d.ts聲明文件
在types文件夾下新建shims-vue.d.ts, 因為globalDispatch需要支持兩種傳參形式,所以使用重載
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;
}
}添加event-keys.d.ts聲明文件
在types文件夾下新建event-keys.d.ts, 這個文件是用來給globalDispatch的第一個參數做代碼提示的,手動寫可以,寫個webpack插件自動讀取vue文件的路徑和方法自動生成更好,下面會貼出來。
export type EventKeys = "src/App.vue:onClick" | "src/views/IndexView.vue:test";
第三步編寫webpack插件
在項目根目錄下新建plugins文件夾
新建global-dispatch.js 自動生成event-keys.d.ts
開發(fā)者模式下才需要生成event-keys.d.ts,先遞歸找出所有的vue文件的路徑,然后讀取文件,用acorn庫解析,找出文件的methods里的所有方法名,用prettier格式化后寫入到event-keys.d.ts,在項目啟動和文件變化后都會執(zhí)行,在添加methos里新方法或刪除后,會執(zhí)行寫入。
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(/<script/g);
if (!scriptMatch) return;
const startIndex = content.indexOf("export default");
if (startIndex < 0) return;
const endIndex = content.indexOf("</script>", 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-path-loader.js webpack loader文件
這個文件是用來在vue實例上添加__filePath屬性的,本來是想寫在上面的插件一起的,無奈沒有在webpack文檔等地方找到在plugins里添加loader的方法,在vue-loader源碼里也沒有好的體現。 在開發(fā)者環(huán)境下vue的$options下有__file可以用,所以只需要生產環(huán)境啟用
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;
};配置vue.config.js
添加configureWebpack里的即可
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")
}
]
}
]
}
}
};后記
- 后續(xù)有時間可以弄成npm包,npm install 使用
- 找下weboack loader 寫進webpack plugin 的方法
- 目前還沒找不破壞vue源碼的情況下自定組件自定義props的類型,讓在組件上 有eKey的單詞提示,在vue/type/jsx.d.ts的ReservedProps下添加eKey?: string;才能實現功能。
倉庫地址
ywenhao/vue2-global-dispatch (github.com)
總結
到此這篇關于vue2中組件互相調用實例methods中的方法實現的文章就介紹到這了,更多相關vue2組件互相調用methods中方法內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
vue 中 elment-ui table合并上下兩行相同數據單元格
這篇文章主要介紹了vue 中 elment-ui table合并上下兩行相同數據單元格,本文通過實例代碼給大家介紹的非常詳細,具有一定的參考借鑒價值,需要的朋友可以參考下2019-12-12
VUE element-ui 寫個復用Table組件的示例代碼
本篇文章主要介紹了VUE element-ui 寫個復用Table組件的示例代碼,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-11-11
Vue.js每天必學之計算屬性computed與$watch
Vue.js每天必學之計算屬性computed與$watch,為大家詳細講解計算屬性computed與$watch,感興趣的小伙伴們可以參考一下2016-09-09
vue-image-crop基于Vue的移動端圖片裁剪組件示例
這篇文章主要介紹了vue-image-crop基于Vue的移動端圖片裁剪組件示例,小編覺得挺不錯的,現在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-08-08

