设计一个node.js插件式系统

1,631 阅读6分钟

「本文正在参与技术专题征文Node.js进阶之路,点击查看详情

如果你用过 vscode或者Eclipse亦或者是jetbrains的开发工具 (甚至 steam平台 游戏的DLC也可以理解为插件), 都会发现这些工具可以在使用过程中下载丰富的插件. 插件带给IDE更多的可能性, 既拥有了更人性化的功能又增添了可玩性.

那么插件式开发是怎么做到的呢?

本文提供了使用node模拟一个简易的插件式软件. (node v14.17.1). 本文灵感来自于css-tricks.com/designing-a… 803d16a7b7d149ca0020ddfef24b892e.gif

定义

首先向还没听说过插件式架构或者OSGI规范的同学解释一下插件是什么?

所谓插件化, 就是在开发时将全部功能同时写进主程序中, 可以把方法逻辑取出放入插件模块中. 当主程序需要功能方法时从插件模块中按需调用.

这样可以使的主程序小而精悍, 大大减少了安装、加载体积. 插件可以进行热插拔, 安装即用的同时即使没有插件也不影响主程序运行.

优点
  1. 减少了主包体积.
  2. 更多的开发者可以加入补充生态.
  3. 便于修改Bug.

代码实现

说了这么多插件式开发的好处, 那么就让我们从下面简易的程序来进一步.

我们需要实现一个计算器程序, 已插件的方式给计算器提供 按钮, 让计算器实现能够实现加减乘除等功能.

1. 初始化node环境

npm init

生成node目录结构

calculator
   |
   | -- node_modules
   | -- plugins              // 插件目录
   |      | -- mult.js       // 乘法插件
   | -- calculator.js        // 计算器主程序
   | -- index.js             // 程序入口
   | -- package.json   

2. calculator.js

1. 首先我们需要定义一个值用来存放我们显示在屏幕上的数字.

module.exports = {
    // 显示数值
    currentValue: 0,
    
    ....
}

2. 这里我们定义一个方法专门用来修改显示的数值.

module.exports = {
    ...
    
    /*
    * 因为每次进行计算后, 结果数值基本都会改变, 
    * 所以我们这里遵循面向对象封装的基本思想将方法封装.
    */
        
    // 设置值
    setValue: function(value) {
        this.currentValue = value
        console.log(this.currentValue)
    },

    
    ....
}

3. 我们给计算器制作两个基础系统功能 加法减法.

module.exports = {
    // 显示数值
    currentValue: 0,
    
    ...
        
    // 加法函数
    plus(addend) { 
        this.setValue(this.currentValue + addend); 
    },
    
    // 减法函数
    minus(subtrahend) { 
        this.setValue(this.currentValue - subtrahend); 
    }

    ....
}

5. 插件注册机制

module.exports = {
    // 插件注册入口
    register(plugin) {
        const { name, exec } = plugin;
        this[name] = exec;
    }
}

4. 使用计算器 (index.js)

// 引入计算器模块
const myCal = require('./calculator.js');

// 使用计算器
myCal.setValue(5);  // => 5
myCal.plus(5);      // => 10
myCal.minus(2);     // => 8

5. 自定义插件并使用

// 引入计算器模块
const myCal = require('./calculator.js');

...

// 定义除法
const divisionPlugin = {
    name: 'division',
    exec: function(dividend) {
        myCal.setValue(myCal.currentValue / dividend)
    }
}

// 注册插件
myCal.register(divisionPlugin)
// 使用插件
myCal.divisionPlugin(2);  // => 4

引入问题

这样的插件安全吗?

通过代码可以发现, 这样的设计我们可以轻松获取到系统核心代码(及加法&减法), 这样的做法显然是不会被接受的, 所以下面我们需要将修改一下代码, 将系统核心代码与插件相分离, 插件开发这只能获取到 plugins(插件), 从而使得系统更加安全.

所以我们将修改思路, 将核心代码与插件代码相分离, 加入新的函数 button() 帮助我们调用计算器的各种方法.

// calculator.js
module.exports = {
    // 当前值
    currentValue: 0,
    // 显示数值
    setValue: function(value) {
        this.currentValue = value
        console.log(this.currentValue)
    },
    
    
    // ------------------------代码更新 开始-----------------------------
    // 系统核心代码
    core: {
        // 加法
        'plus': (currentValue, addend) => {
            return currentValue + addend
        },
        // 减法
        'minus': (currentValue, subtrahend) => {
            return currentValue - subtrahend
        }
    },
    
    // 已安装插件
    plugins: {},
   
    // 新增 button() 函数用于调用方法
    button: function(apiName, parameter) {
        let func = this.core[apiName] || this.plugins[apiName];
        this.setValue(func(this.currentValue, parameter))
    },
    // ------------------------代码更新 结束-----------------------------
    
    // 插件注册入口
    register: function(plugin) {
        const { name, exec } = plugin;
        // ------------注意更新-----------
        // 插件注册
        this.plugins[name] = exec;
    }

}

定义乘法插件

目录请看 1.初始化node环境

// mult.js
module.exports = {
    name: 'mult',
    exec: function(currentValue, multiplicand) {
        return currentValue * multiplicand
    }
}

注册乘法插件

// index.js

// 引入计算器模块
const myCal = require('./calculator.js');

// 使用计算器
myCal.setValue(3);           // => 3 
myCal.button('add', 17);     // => 20

// 引入乘法插件
const multPlugin = require('./plugins/mult');
// 注册插件
myCal.register(multPlugin);
// 使用插件
myCal.button('mult', 5);     // => 100

远程下载安装插件

这里使用node.jshttp 模块, 模拟了一个插件市场服务器, 并从远程下载并安装插件.

模拟插件市场

目录格式

pluginServer
  |
  | -- plugins               // 插件库
  |      | -- squared.js     // 计算平方插件
  | 
  | -- index.js              // 程序主入口
  | -- package.json

index.js

这里不是本文重点不做详细注释

const http = require('http');
const server = http.createServer()
const fs = require("fs");
const path = require('path');
server.on('request', (request, response) => {
    let url = request.url;
    if (url == '/getSquaredPlugin') {
        var filePath = path.join(__dirname, './plugins/squared.js');
        var stats = fs.statSync(filePath);
        if (stats.isFile()) {
            response.writeHead(200, [
                ['Content-Type', 'application/octet-stream'],
                ['Content-Disposition', 'attachment; filename=squared.js'],
                ['Content-Length', stats.size]
            ]);
            fs.createReadStream(filePath).pipe(response);
        } else {
            response.end(404);
        }
    }
})
server.listen(3000, () => 
    console.log('server is listening at port 3000');
})

squared.js

// squared.js
module.exports = {
    name: 'squared',
    exec: function(currentValue) {
        return currentValue * currentValue
    }
}

回到刚刚的计算器程序

// index.js

// 引入计算器模块 
const myCal = require('./calculator.js');

// 从服务器下载插件
let fs = require("fs");
let path = require("path");
let request = require("request");
let fileName = 'squared.js';
let dirPath = path.join(__dirname, "plugins");
let url = "http://localhost:3000/getSquaredPlugin"
let stream = fs.createWriteStream(path.join(dirPath, fileName));
// 将插件文件保存在项目根目录下 plugins 文件夹中
request(url).pipe(stream).on("close", function(err) {
    console.log("插件[" + fileName + "]下载完毕");
});

// 使用计算器 
myCal.setValue(3);            // => 3

/*
* 这里我们延迟5秒等待文件下载以及保存在本地, 
* 在实战开发中应该设计更合理的方式.
*/ 
setTimeout(() => {
    // 加载插件
    const squaredPlugin = require('./plugins/squared')
    // 注册插件
    myCal.register(squaredPlugin)
    // 使用插件
    myCal.press('squared');   // => 9
}, 5000)

🎉🎉🎉 大工告成 🎉🎉🎉

此案例是我在做毕业设计的时候, 想把毕业设计制作成像vscode那样的插件式架构应用软件. 特点提前先从小案例试水, 研究插件式开发的奥秘.

这里做一个小预告

我的毕业设计是一个管理 dockerk8s (Kubernetes) 的一个多平台界面化管理软件, 专为学校教学打造, 屏蔽了大量且容易出错的配置. 而且还提供了自制的捆绑镜像(比如在一个Linux系统中事先准备好某些程序或者文件, 拉去捆绑镜像即可完成所有的工作), 系统快照如下: IMG_0546.PNG IMG_1773.JPG IMG_1775.JPG 17463_raw.gif

等答辩结束后, 项目中基于vue3.x的UI框架, chaosUI将在GitHub开源,本人事先也做过一期部分组件的讲解文章 vue3 实现仿苹果系统侧边消息提示 欢迎大家捧场.

如果你想继续了解 插件式架构开发 或者了解我的毕业设计先阶段情况, 点赞 + 关注仪表鼓励哦 🖖🖖🖖

如果你有疑问或者为本文提供更好的建议欢迎在评论区评论

本人也是刚刚接触插件式开发, 如果有大佬发现了本文的不合理或改进之处, 恳请大佬一定留言.