2

插件化设计模式在前端领域的应用

 2 years ago
source link: https://webfe.kujiale.com/cha-jian-hua-she-ji-mo-shi-zai-qian-duan-ling-yu-de-ying-yong/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

软件开发中,随着系统功能变多,复杂度成指数级上升,而复杂度的增高多来源于模块间的耦合过于严重,插件化的设计模式能一定程度解决模块耦合的问题。抽象出系统的核心流程节点,基于这些节点与多个插件进行交互,最终实现整个系统。当然,前端领域的一些场景也有插件化应用的案例,本篇文章我们基于这些案例,一览其中的设计原理与插件核心执行流程。

Babel

Babel 是 JavaScript 编译器,更确切地说是源码到源码的编译器,通常也叫做“转换编译器(transpiler)”。 意思是说你为 Babel 提供一些 JavaScript 代码,Babel 更改这些代码,然后返回给你新生成的代码。

babel 不可能把所有 js 新特性都囊括进去,例如一些未进入标准的,或还在草案中的。所以使用插件化架构,用户需要哪些特性,自行增加 babel plugin 来使用,甚至自定义特定场景插件。

Babel 的三个主要处理步骤分别是: 解析(parse),转换(transform),生成(generate)。.这些步骤具体处理细节本篇文章不会扩展讲述,有兴趣可以查看 babel plugin handbook

1047a9fccf6e4b13861aad515b7f39c7~tplv-k3u1fbpfcp-zoom-1.image

这里最复杂的步骤是 转换,同时也是 插件 的工作阶段。babel 插件通过访问者模式定位具体 AST 节点,并进行节点路径的各种操作。节点的操作类似 DOM ,同样有节点树,与基于整个节点树的增删改查。

babel 插件设计本身并不复杂,插件间完全互相隔离,且无互相拦截阻断通信、异步等互相依赖性、执行时序等问题,是比较纯粹的 AST 转换。假设我们有这么一段代码:

function square(n) {
  return n * n;
}

它的树结构如下:

- FunctionDeclaration
  - Identifier (id)
  - Identifier (params[0])
  - BlockStatement (body)
    - ReturnStatement (body)
      - BinaryExpression (argument)
        - Identifier (left)
        - Identifier (right)

当我们向下遍历这颗树的每一个分支时我们最终会走到尽头,于是我们需要往上遍历回去从而获取到下一个节点。 向下遍历这棵树我们进入每个节点,向上遍历回去时我们退出每个节点。

配置的插件数组依次执行,类似这样:

01fade5b03b042c69e9e68fccb34a107~tplv-k3u1fbpfcp-zoom-1.image

babel 在遍历前后会对应执行 pre hook & post hook 函数,针对所有插件配置的 hook 执行一遍,传递当前文件信息(BabelFile),BabelFile 实例会包含 ast、code、path 等内部信息。遍历 AST 的过程中,遍历到某个节点都会依次执行所有插件对应配置的 visitor type callback,并且提供 enter、exit 更加细节的调用行为,给予插件定义更多的执行时机。

0ff1fe5edc4540eea3abedd93a399b07~tplv-k3u1fbpfcp-zoom-1.image

示例代码:

const babel = require('@babel/core');

/**
 * @returns {import('@babel/core').PluginItem}
 */
const createPlugin = (name) => {
  const log = (...args) => console.log(`[${name}]:`, ...args);
  return {
    name,
    pre(file) {
      log('pre');
    },
    visitor: {
      FunctionDeclaration(path, state) {
        log('visit FunctionDeclaration');
      },
      ReturnStatement(path, state) {
        log('visit ReturnStatement');
      },
    },
    post(file) {
      log('post');
    },
  };
};

babel.transform(
  `
    function a(m) {
        return m*m;
    }
`,
  {
    plugins: [createPlugin('A'), createPlugin('B')],
  },
);
[A]: pre
[B]: pre
[A]: visit FunctionDeclaration
[B]: visit FunctionDeclaration
[A]: visit ReturnStatement
[B]: visit ReturnStatement
[A]: post
[B]: post

Koa 应用程序是一个包含一组中间件函数的对象,它是按照类似堆栈的方式组织和执行的。

f377e913a8e04718af249fe79b33a780~tplv-k3u1fbpfcp-zoom-1.image

koa-compose 是经典的洋葱模型实现,图中的每一层洋葱圈在 koa 中叫 中间件(middleware)。中间件即函数,其核心包含 context、next 概念。

  • context: 即函数共享上下文对象,所有中间件都有完全控制权限;
  • next 即调用内层函数(图中的被包裹洋葱圈),类似调用堆栈, next 即当前栈的上一层栈函数,由当前中间件决定何时调用。

洋葱模型扩展了一次性行为的拦截,预设数据等场景。很方便的对主链路进行数据更新,流程管控,流程校验等操作。对于 http request ,page bootstrap 这种一次执行场景非常适用。

中间件可将独立的逻辑做单独封装,解耦,降低系统复杂度,常用的中间件如:

  • 错误拦截,将友好错误信息返回给前台;
  • 缓存,可做到接口级别的缓存控制;
  • Session 数据预置,用户信息之类的基础数据。

Axios

Axios 是跨平台(node browser)的 http request 库。Axios 也有插件的概念,Axios 中叫 interceptor 拦截器,针对请求相应进行拦截、操作、更新等逻辑。reques interceptor 可拿到 request config,并进行修改;response interceptor 可拿到 reqeust config 与 http response 等信息。

在 Axios 中,拦截器就是普通 js 函数,请求、相应拦截独立。

// Add a request interceptor
axios.interceptors.request.use(function (config) {
  // Do something before request is sent
  return config;
}, function (error) {
  // Do something with request error
  return Promise.reject(error);
});

执行流程为(需要特别注意 request interceptor 的执行顺序),rejected 为 fullfilled 执行失败 or 上一个 interceptor 抛异常时执行,核心就是 Promise.then 的链式执行逻辑。如下图:

075a5b6843904c93998d65fe3d06a9e5~tplv-k3u1fbpfcp-zoom-1.image

Tapable(Webpack)

The tapable package expose many Hook classes, which can be used to create hooks for plugins.

Tapable 可以为插件提供钩子,webpack 的插件化架构就是基于此实现的,不同的执行流程产出不同的 hook 类型,webpack 打包流程中 hooks 点位非常多,并且根据需要,每个 hook 的类型会不同。相比以上案例,Tapable 实现相对会复杂很多,它包含了同步、异步并行/串行、可阻断、瀑布流等执行流程相关的概念,是集大成者。

Tapable 的四种同步流程:

01631ac6b1744677ab5855dfb634c668~tplv-k3u1fbpfcp-zoom-1.image

除了标准的流程外,其他流程都是基于 插件返回值 做文章:

  • Bail 可阻断流程,返回非 undefined 是执行阻断逻辑;
  • Waterfall 瀑布流,插件输入为前一插件的输出;

Loop 循环执行,所有插件返回值都会 undefined 时,才结束,否则继续从头开始执行。(目前 webpack 暂时没用到)

Tapable 异步流程类型共有 5 种:

  • AsyncParallelHook
  • AsyncParallelBailHook
  • AsyncSeriesHook
  • AsyncSeriesBailHook
  • AsyncSeriesWaterfallHook

增加了异步特有的并行、串行等逻辑,去除了 Loop。流程图与同步流程大致类似,就不重复绘制了。

我司登录注册是一个相当复杂的功能,其本身核心功能:

  • 手机号验证码登录
  • 手机号验证码注册

所有登录中涉及到:多账号绑定、登录风控、图片验证码、滑动验证码、协议弹窗;

所有注册流程涉及:手机绑定、身份选择、行业选择、协议弹窗;

基于此模块衍生出的扩展功能有:

  • 多账号切换;
  • 登录弹窗、登录页面;
  • 多个站点公用统一套核心登录注册逻辑;
  • 某站点可能会限制一些账号类型,权限等(例如仅商家账号可登陆);
  • 某注册来源送 xxx 奖励,注册来源传递问题;
  • 单点登录;
  • 手动埋点;
  • 某站点自定义可跳过某些注册流程,直接注册(拦截);

基于此,我们只保留核心流程,将其他流程作为插件形式,作为登录注册模块的插件,去修改、增加一些逻辑,去影响核心流程的走向 或单纯 获取事件点等。基于此衍生模块都成为了插件,整个模块复杂度被平摊到了不同插件内部中。

我司主站,收敛启动逻辑,主流程仅提供一些启动所需的 hook,例如:应用初始化、挂载、更新、卸载等钩子。基于此我们封装出了:

  • 用户信息插件
  • 导航信息注入插件;
  • 样式隔离插件;
  • 组件库主题包预置插件;
  • 权限判断插件;

以上实战,可以给予大家一些参考价值,具体实现细节包含敏感信息,暂不细说。

插件式的设计模式在软件开发当中应用相当广泛,插件大多数由不同开发人员、甚至不同团队去开发。它不仅能降低系统模块间的耦合度,更能给予软件全局的约束和规范,这对于大型软件开发相当重要。我们纵览了前端开发领域的一些实战,对插件设计思路有了大致的了解,并且深入了下这些插件的执行流程。最后对我司的一些复杂场景的实战经验做了分享。

希望大家看完有所收获,完。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK