什么是插件?
插件(Plugin)是指一种可以向已有软件中添加特定功能的模块,它本身不能独立运行,必须依赖于宿主应用程序来执行。插件的设计目的是为了扩展软件的功能,而不需要修改原有的软件代码。通过插件机制,用户或开发者可以灵活地根据需求添加或移除功能,提升软件的可扩展性和灵活性。
举个栗子🌰
假如你有一条汽车生产流水线:
- 不同的工位 (Workstation)做不同的事情
- 当汽车到达某一个工位时,工人便开始加工
- 所有的工序都完成后,也就完成了汽车🚗的生产
- 采用固定的流水线,生产的汽车是完全一样的
众所周知汽车能够选配一些功能:座椅加热、定制轮毂、车衣、氛围灯、音响等。定制化可以使汽车销量更好,但这些功能需要更专业的工人参与,但当前流水线无法办到。于是工厂高薪聘请了专业的师傅,把他们带到了专门的工位。然后,轮胎工位的师傅说:“给我车子和工具,我来搞定轮胎!”,内饰工位的师傅说:“车子给我,我给你搞点氛围灯!”...
在整个过程中参与的角色:
- 流水线:主应用
- 不同功能的工位:特定的生命周期/事件
- 原工位上的工人:主应用代码
- 高薪聘请的工人:插件代码
- 待加工的汽车:传递给主应用或插件的参数
- 配套工具:应用上下文或者应用实例
假如我们不给插件传参会怎样?
只请了工人安排在特定的工位,不给他汽车和配套工具,那他就只能在这里做一些与工作无关的事情(摸鱼🐟),没有起到扩展原程序功能的作用。
“就好像我到餐厅里去吃饭,你不给我筷子,我怎么吃呢? 我去洗手间解决问题,你不给我手纸,我怎么解决呢? 我买了辆车,没有方向盘,我怎么开呢?”
插件系统的应用
插件的本质就是加入到主程序的执行生命周期中,在一些关键的节点做一些特定的事情,从而扩展主程序的功能。
Webpack插件系统
注:此图片来自:图解Webpack——实现Plugin
插件的初始化位于这个位置:
if (Array.isArray(options.plugins)) {
for (const plugin of options.plugins) {
if (typeof plugin === "function") {
/** @type {WebpackPluginFunction} */
(plugin).call(compiler, compiler);
} else if (plugin) {
plugin.apply(compiler);
}
}
}
初始化插件后,就开始 webpack 的编译构建流程,然后在关键的时机抛出对应的事件,以供插件订阅:
更加详细的webpack插件信息:hooks
这里值得注意的是,webpack是基于Tapable来构建插件系统的,其中事件的派发是通过new Function的形式来实现的,如:
function anonymous(param1) {
"use strict";
var _context;
var _x = this._x;
var _fn0 = _x[0];
_fn0(param1);
var _fn1 = _x[1];
_fn1(param1);
var _fn2 = _x[2];
_fn2(param1);
}
anonymous({})
那他为什么不直接通过循环调用回调函数来执行时呢?大佬给出了解答:github.com/webpack/tap…
JavaScript 引擎的编译优化:
new Function 方法: 当你使用 new Function 创建一个函数时,这个函数通常会被 JavaScript 引擎视为一个独立的脚本。这意味着它可以进行更彻底的优化。由于该函数是在运行时创建的,引擎有机会根据当前上下文优化它,这可以包括内联优化、避免不必要的变量查找等。
forEach 循环: 相比之下,使用 forEach 循环直接调用数组中的函数则涉及到多次函数调用的开销。每次循环都会调用一个函数,这可能会导致更多的上下文切换和较少的优化机会。每次函数调用都涉及到创建新的调用栈、传递参数等开销。
函数调用的开销:在 forEach 循环中,每次迭代都会进行一次函数调用。这些调用涉及到创建调用上下文、传递参数、以及在调用栈上进出的开销。
而使用 new Function 方法创建的函数,在它的内部直接编码了所有的函数调用。这种方式减少了函数调用的次数,因为所有的调用都被内联到一个单独的函数体内。这就减少了调用栈的变化,提高了执行效率。
总之,使用 new Function 创建的函数,由于更优的编译优化和减少的函数调用开销,往往在性能上优于简单的 forEach 循环调用。然而,这种优化的程度可能会因 JavaScript 引擎的实现细节而有所不同,而且它也带来了代码的复杂性和可维护性的挑战。在实际应用中,选择哪种方法取决于具体场景和性能需求。
Vue插件系统
Vue通过Vue.use来注册插件,从而集成router、store、自定义组件等功能。
插件通常用来为 Vue 添加全局功能。插件的功能范围没有严格的限制——一般有下面几种:
- 添加全局方法或者 property。如:vue-custom-element
- 添加全局资源:指令/过滤器/过渡等。如 vue-touch
- 通过全局混入来添加一些组件选项。如 vue-router
- 添加 Vue 实例方法,通过把它们添加到
Vue.prototype上实现。- 一个库,提供自己的 API,同时提供上面提到的一个或多个功能。如 vue-router
不同于webpack,Vue传递给插件的参数是Vue自身,从而可以在Vue原型链上定义很多自定义的属性和方法,也可以通过mixin来增强组件实例的功能,比如在组件的beforeCreate、destroyed等生命周期里做额外的操作。
通过全局方法 Vue.use() 使用插件。它需要在你调用 new Vue() 启动应用之前完成:
// 调用 `MyPlugin.install(Vue)`
Vue.use(MyPlugin)
new Vue({
// ...组件选项
})
对于Vue插件来说,“在一些关键的节点做一些特定的事情”中的“关键的节点”指的就是在 new Vue() 启动之前。
chrome插件系统
chrome浏览器也可以添加插件来扩展浏览器的功能,比如常见的:Vue Devtools、React Devtools、掘金浏览器插件等。
Chrome for Developers提供了开发指南:developer.chrome.com/docs/extens…
Google插件的运行原理可以总结为以下几个步骤:
- 插件加载:当用户打开浏览器时,浏览器会加载已安装的插件。插件的清单文件将被读取,并解析出插件的基本信息和配置。
- 背景页面启动:插件的背景页面将在插件加载后启动。背景页面是插件的核心,它会执行初始化操作、注册事件监听器,并提供插件的后台功能和API。
- 内容脚本注入:当用户访问特定网页时,内容脚本将被注入到页面中。内容脚本可以修改页面的DOM结构、监听页面事件,并与页面进行交互。
- 消息传递与通信:插件的各个组件可以通过消息传递机制进行通信。背景页面可以发送消息给内容脚本,或者接收来自内容脚本的消息,以实现数据交换和功能协调。
- 用户界面展示:插件可以在浏览器界面上展示自定义的用户界面,如工具栏按钮、弹出窗口等。用户可以通过与插件界面交互来使用插件提供的功能和选项。
- 定期更新和升级:插件开发者可以定期发布更新和升级版本,用户的浏览器将自动检查并安装新版本的插件。
如果你也想开发chrome插件,可以跟随这位大佬的教程:juejin.cn/post/723086… ,一共有18节。
总的来说
插件的优点
- 扩展性:插件允许开发者在不修改软件本体的情况下,为软件增加新的功能或特性。
- 模块化:每个插件通常是一个独立的模块,具有特定的功能或特性,便于管理和更新。
- 解耦性:插件机制使得插件和宿主应用解耦,插件可以单独开发、测试和更新。
- 灵活性:用户可以根据自己的需求选择加载哪些插件,而不必使用整个软件的全部功能。
插件的缺点
- 可信性:主程序几乎无法把控插件对其的更改,插件代码是否可信?
- 可读性:与主程序关联不强,当插件越来越多的时候,可能对理解源代码的运行方式产生障碍。
在阅读webpack源码的时候,从webpack启动初始化compiler对象开始,一直捋到compilation初始化后发现运行过程就结束了...what fxxx?以下为webpack启动主流程:
当时读到this.hooks.make.callAsync发现后续就没代码了,并且 Compilation 模块里还定义了一堆方法,似乎都没有用到?
实际上webpack是通过插件订阅compiler的make事件来添加entry,从而触发之后的buildModule、构建module graph等一系列动作。make.callAsync实际上就是在执行插件的逻辑了,比如SingleEntryPlugin就订阅了make事件:
compiler.hooks.make.tapAsync(
"SingleEntryPlugin",
(compilation, callback) => {
const { entry, name, context } = this;
const dep = SingleEntryPlugin.createDependency(entry, name);
compilation.addEntry(context, dep, name, callback);
}
);
从而通过compilation执行后续的构建流程。但是这种松散耦合的关系,当插件特别多的时候,使我们在阅读源码时,常常需要一个关键字一个关键字的去搜索,看看到底哪里订阅了这个事件,又是在哪里进行的触发的。但总体来说是瑕不掩瑜的。
何时使用
基于场景选择编程范式,当我们需要运行时动态的扩展程序功能而不想改变源码,那么就可以考虑使用插件模式。
如何设计
要实现插件模式有很多种方法,比如每到一个关键节点就把注册的插件循环执行一遍,或者通过发布订阅实现。而在当前,从扩展性、维护性、插件通信等方面,大多都采用了事件订阅的方式来实现这一设计。
大型程序集成插件模式可以参考webpack,集成Tapable来解耦插件与主程序的关联,提供统一的事件订阅系统。
小型程序集成插件模式则可以参考Vue.use,集成EventEmitter,如:
interface PluginFn {
(ctx: Ctx, ...args: any): void
}
interface PluginObject {
install: PluginFn
}
interface Event {
InstanceCreated: 'instance:created'
InstanceUpdate: 'instance:updated'
InstanceDestroy: 'instance:destroy'
}
interface Ctx {
plugins: (PluginFn | PluginObject)[]
Event: Event
events: Record<string, Function>
on(name: string, handler: Function): void
once(name: string, handler: Function): void
emit(name: string, ...args: any): void
off(name: string, handler: Function): void
}
interface App {
use(plugin: (PluginFn | PluginObject), ...args: [Ctx, ...any] | []): Ctx
}
其中App提供了use方法来注册插件,插件可以是一个函数或者带有install方法的对象,我们可以在App初始化的同时来注册插件,然后在App运行期间的恰当时机触发一些事件,当插件订阅了这个事件后,就能做一些特定的事情来扩展App的功能。
善于模仿
相信大家都看过黑客帝国、异次元骇客等科幻片,人类世界就是一个巨大的程序,而我们只是其中的npc。(悲观T_T)
抛开npc不谈,我认为现实世界是最好的程序,我们应该多观察这个世界,如果能在现实世界中找到相似的运作方式,那么就是咱们在写代码时的最优解。比如仿生学等等,都是咱对大自然的参考。
宇树科技是一家非常年轻的公司,和大疆一样,是咱们的排面!他们不管是制作机器人还是机器狗,都可以在其中看到对真正的人和狗的仿生模仿。大自然的经验告诉我们,亿万年的进化留下来的都是精华!(考虑购买一下机器人板块的股票,感觉势头很猛hhh)