npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@runnan/obvious

v1.1.0

Published

a front end micro service communication frame work

Downloads

4

Readme

obvious.js

轻量级的微前端框架

示例

  • 介绍

微前端架构,也常被叫做前端微服务架构,其实这并不算一个很新的概念,已经有许多公司做过相应的实践。要说前端微服务自然绕不开后端微服务,一个技术或概念的出现并不是凭空产生,而是为了解决开发中存在的痛点和问题。在后端没有微服务架构的时候,随着系统体量的增加,整个项目中大大小小的功能模块集中在一起,变得非常臃肿,难以维护。同时单体系统的各个功能模块依赖于同样的数据库、内存资源等,一旦某个模块对资源使用不当,整个系统都被拖垮。同时在对系统做集群扩展的时候,只能对整个系统进行水平扩展,而不能只针对性地对造成性能瓶颈的模块进行扩展。正是由于这样的原因,后端才出现了微服务的架构,即各个功能模块单独开发和部署,可以用不同的编程语言编写,占用独立的资源,每一个服务都跑在自己的进程中,微服务之间通过轻量级机制(http restful接口, rpc调用等)进行消息通信,最终多个微服务共同组成完整的系统。这样系统的灵活性大大增加,同时在团队组织和开发方式上,各个微服务组可以自由选择编程语言,相对独立地进行开发和维护,大大减少了管理成本。

说回前端微服务,React、Angular、Vue等前端框架出现后,DOM操作已经由框架帮你做了,另外页面渲染也几乎被javaScript完全cover, 在jQuery大行其道的时代,前端开发是先有页面结构,再用数据填充结构,而三大框架出现后,前端开发模式变成了先有数据,再根据数据生成页面结构。因此自然可以开发出愈发复杂的前端应用,而我认为真正让前端地位出现质的提升的一个标志是单页应用(SPA)的出现,在没有SPA的时候,页面之间的数据传递一定要经过后端,而单页应用时代,复杂的页面数据传递被前端cover之后,前端的体量一下子可以变得很大,特别是一些toB的应用,可能动辄有四五十页需要管理。这时跟后端同样的问题也就出现了,虽然React、Vue、Angular各自都有成熟的状态管理和单页框架,但是如果要做一个大的前端系统,就只能选择一套技术栈,当然了,在前端领域,一般一个公司都会用相对统一的技术栈,但是另一个问题是这种模式下所有人都得往同一个仓库推代码,对于一个四五十页的应用来说,这样的架构恐怕过于臃肿了。

因此就有了前端微服务的概念,我们希望有这样一种架构:前端应用被划分为相对独立的微服务,对前端来说,一个微服务可以认为是一段可执行的js代码。这些微服务可以被独立开发,独立伺服,最终以script标签的形式引入同一个html中,然后各自渲染自己负责的部分,最重要的是,这些微服务应该能够相互通信。后端微服务的通信可以通过rest接口等方式实现,但是对于前端来说,微服务之间的通信不可能走网络请求,这样的代价太大了,甚至可以说本末倒置。那么前端微服务之间应该怎么通信呢?似乎js在后端的存在形式——Node.js已经给出了答案,Node.js基于EventEmitter构筑了完整的事件通信机制,同样是javaScript, 或许我们也可以在前端做同样的事情——这就是Obvious.js

  • 概念

    • EventEmiter: 事件收发器,类似Node.js的EventEmitter, 提供事件通信能力,是Obvious的核心
    • Bus: 消息总线,每new一个Bus实例,实例内部都有一个EventEmitter,可以被传递给微服务, 同时可以配置微服务的伺服路径,由Bus拉取js代码执行
    • Socket: 消息接口,一个微服务与其他微服务通信的前端套接字。在EventEmitter提供的消息通信的基础上,封装了一层状态通信能力
    • App: 前端微服务,负责页面渲染或其他功能的前端代码,通过socket与其他app通信, app名与socket必须同名

    架构

  • 使用

  1. 下载依赖包:

    npm install @runnan/obvious // 请从1.0.3版本开始使用
  2. 在平台微服务中创建bus, 并配置bus所管理的微服务的资源:

    // 平台服务
    import { createBus } from '@runnan/obvious';
    
    // 调用完createBus后,window.__Bus__上将会挂载一个名为global的bus
    createBus('global', {
        service1:{
            js: ['/assets/service1/vendor.js', '/assets/service1/entry.js'],
            css: ['/assets/service1/entry.css']
        },
        service2: {
            js: ['/assets/service2/vendor.js', '/assets/service2/entry.js'],
            css: ['/assets/service2/entry.css']
        }
    });
  3. 微服务开发团队在约定好bus之后,分头开发微服务: 在微服务的入口文件中用 bus.createSocket创建socket, 并用socket读写状态和收发事件来与其他微服务通信

    // http://localhost/assets/service1/entry.js
    import { getBus } from '@runnan/obvious';
    
    const bus = getBus('global');
    
    bus.createSocket('service1', [], (socket, config) => {
        /**
         * callback函数里写微服务的业务逻辑: 与技术栈无关,可以用react、vue、angular
         * 渲染页面,也可以用jQuery或者原生js操作dom; 甚至也可以专门
         * 用来发ajax请求。因此微服务的本质只是一段可执行的javaScript
         * 代码, obvious提供的是用socket与其他微服务通信的能力
         */
    
        // 监听事件
        socket.on('service2BroadCast', (message) => {
            console.log(`get service2 broadCast: ${message}`);
        });
    
        // 操作dom
        document.getElementById('addCount').onclick = () => {
            const currentCount = socket.getState('count');
            socket.setState('count', currentCount + 1);
        };
    
        // 操作状态
        socket.initState('service1_ready', true);
    });
    // http://localhost/assets/service2/entry.js
    import { getBus } from '@runnan/obvious';
    
    const bus = getBus('global');
    
    bus.createSocket('service2', ['service1_ready'], (socket, config) => {
        // 读取配置信息
        socket.initState('count', config.initCount);
    
        // 触发事件
        socket.emit('service2Broadcast', 'hello, I am service2');
    });
  4. 在平台服务中通过bus.startApp()启动子微服务: 在创建bus时,已经配置过微服务的css和js资源有哪些,所谓启动微服务,就是先依次加载配置的css资源,再依次加载并执行配置的js资源。

    import { getBus } from '@runnan/obvious';
    
    const bus = getBus('global');
    
    bus.startApp('service2', {initCount: 1}).then(() => {
        console.log('成功拉起service2');
    });
    
    /**
     * 虽然拉起service1在拉起service2之后执行
     * 但是由于ervice2依赖状态service1_ready,
     * 所以实际是service1中执行完socket.initSta('service1_ready', true);
     * 之后,才执行service2中的回调
     */
    bus.startApp('service1').then(() => {
        console.log('成功拉起service1');
    })
    

5.高阶应用:资源配置中间件: 微前端架构是为了适应大型前端系统,比如一个OA系统,一个运维监控系统,一个大型的在线IDE, 这些复杂系统的前端有可能将被划分成20至30个前端微服务,如果这些微服务的js和css资源路径只能像上面的示例中一样,在创建bus时通过硬编码进行配置的话,显然是非常不灵活的。为了适应这种更加复杂的场景,createBus预留了第三个参数,让开发者能通过实现中间件的方式,更优雅地实现微服务资源注册和加载。 比如,你作为X公司某产品的总架构师,要求所有前端微服务的js资源最终都只打包成一个文件,所有css资源最终也都只打包成一个文件,且文件名都是微服务名,部署到https://cdn.x.com,那么你在创建bus时,只需要传入这样一个中间件函数,就能方便地启动遵照这个规范部署的前端微服务了。 ```javaScript

// 一个最简单的资源加载中间件
const async XsimpleMiddleware = (name, loadJs, loadCss) => {
    await loadJs(`https://cdn.x.com/assets/js/${name}.js`);
    await loadCss(`https://cdn.x.com/assets/css/${name}.css`);
}

createBus('global', null, XsimpleMiddleWare);

bus.startApp('service1');
```
  • API

    • Socket:

      • socket.on(): 监听事件

        |参数名|是否必选|类型|描述| |:---:|:---:|:---:|:---:| |eventName|是|string|事件名| |callback|是|Function|回调函数|

      • socket.off(): 解绑回调函数

        | 参数名 | 是否必选 | 类型 | 描述 | |:---:|:---:|:---:|:---:| | eventName | 是 | string | 事件名 | | callback | 是 | Function | 回调函数 |

        同Node.js的EventEmiter一样,解绑时的回调函数必须和监听事件时绑定的回调函数是同一个

      • socket.emit(): 触发事件

        | 参数名 | 是否必选 | 类型 | 描述 | |:---:|:---:|:---:|:---:| | eventName | 是 | string | 事件名 | | ...args | 否 | 不定长参数 | 传递给事件回调函数的参数

      • socket.initState(): 初始化状态

        | 参数名 | 是否必选 | 类型 | 描述 | |:---:|:---:|:---:|:---:| | stateName | 是 | string | 状态名 | | value | 是 | any | 状态值 | | private | 否 | boolean | 是否是私有状态, 默认为false, 如果为true,则其他socket将不能修改该状态的值

      • socket.getState(): 获取状态

        | 参数名 | 是否必选 | 类型 | 描述 | |:---:|:---:|:---:|:---:| | stateName | 是 | string | 状态名 |

      • socket.setState(): 修改状态

        | 参数名 | 是否必选 | 类型 | 描述 | |:---:|:---:|:---:|:---:| | stateName | 是 | string | 状态名 | | value | 是 | any | 状态值 |

        一个状态必须在init之后才能被set,否则将报错,如果状态在init时被声明为私有状态,则只有init该状态的socket才可以修改它的值

      • socket.watchState(): 监听状态

        | 参数名 | 是否必选 | 类型 | 描述 | |:---:|:---:|:---:|:---:| | stateName | 是 | string | 状态名 | | callback | 是 | Function | 回调函数, 接收两个参数,分别是newValue和oldValue |

      • socket.unwatchState(): 取消监听状态

        | 参数名 | 是否必选 | 类型 | 描述 | |:---:|:---:|:---:|:---:| | stateName | 是 | string | 状态名 | | callback | 是 | Function | 回调函数, 接收两个参数,分别是newValue和oldValue |

        解绑时的回调函数必须和监听事件时绑定的回调函数是同一个

      • socket.name: socket的名字

    • Bus:

      • Bus():构造函数

        | 参数名 | 是否必选 | 类型 | 描述 | |:---:|:---:|:---:|:---:| | assets | 是 |{ [appName: string]: { js: string[], css: string[] } } | 配置要拉取的微服务的静态资源 | middleware | 否 | (appName: string, loadJs?: Function, loadCss?: Function) => Promise | 配置如何拉取js资源的中间件

        在Bus构造函数中, 可以通过assets手动配置静态资源,只需要配置资源路径即可 middleware是一个函数,接收三个参数,第一个参数是必选的app名, 插件开发者需要根据app名拉取对应的js和css资源, 插件可接收obvious提供的两个参数loadJS和loadCss, 这两个参数都是函数,入参是资源路径,loadJS(src)将创建script标签,加载src下的js代码并执行, loadCss(src)将加载src下的css资源并插入link标签, 插件最后需要返回一个Promise。 如果同时assets和middleware都配置了同一个微服务的资源,则assets的配置生效。 关联API: createBus

      • startApp():拉起app并启动(执行对应的js代码)

        | 参数名 | 是否必选 | 类型 | 描述 | |:---:|:---:|:---:|:---:| |appName| 是| string | app名,必须与app内声明的socket同名 | | config | 否 | any | app配置, 将在app对应的socket被create时被传给socket,如果多次start同一个app,则只有第一次传入的config生效 |

        startApp将返回一个Promise, 如果app是第一次被拉起,则bus会加载app对应的资源,等资源加载并执行成功后才进入promise的then回调, 但是如果app已经被start过一次,则执行startApp将直接进入then回调,且不会把config配置传递给对应的微服务。

      • createSocket(): 创建前端套接字

        | 参数名 | 是否必选 | 类型 | 描述 | |:---:|:---:|:---:|:---:| | socketName | 是 | string | socket名,必须与app同名 | | dependencies | 是 | string[] | app依赖的状态列表,如果不依赖任何状态则传入一个空数组即可(状态参见socket介绍) | callback | 是 | Function | 执行app逻辑的函数,例如用React将视图渲染进一个div中。接收两个参数, 第一个参数是app对应的socket实例,用于与其他app通信, 第二个参数是Bus在startApp时传入的config对象,用于初始化app | | timeout | 否 | number | 依赖状态的超时时间,默认为10*1000ms

        startApp和createSocket需要配合使用,createSocket通常是某个微服务代码的入口函数,在createSocket的callback内执行app具体逻辑,例如有一个printString微服务,作用是将字符串{{config.text}}渲染到id为{{config.container}}的div中, 其中config由微服务被拉起时初始化,假设该微服务的代码伺服在/printString/assets/js/index.js下, 则需要在平台服务中创建Bus并拉起微服务:

        window.globalBus = new Bus({
            printString: {
                js: ['/printString/assets/js/index.js']
            }
        });
        window.globalBus.startApp('printString', {
            text: 'hello world',
            container: 'container'
        }).then(() => {
            console.log('successfully start');
        });

        而/printString/assets/js/index.js中的逻辑则是:

        window.globalBus.createSocket('printString', [], (socket, config) => {
            ReactDOM.render(<div>{config.text}</div>, document.getElementById(config.container));
        });
      • getSocket(): 获取Bus管理下的对应名字的socket的实例

        | 参数名 | 是否必选 | 类型 | 描述 | |:---:|:---:|:---:|:---:| | socketName | 是 | string | socket名 |

      • state:Bus管理下的所有state。该值是总线状态的一个映射,是只读的,要修改状态必须通过socket.initState和setState进行修改,直接修改bus.state会抛出异常

    • createBus()

      | 参数名 | 是否必选 | 类型 | 描述 | |:---:|:---:|:---:|:---:| | name | 是 | string | bus的名字 | | assets | 是 |{ [appName: string]: { js: string[], css: string[] } } | 配置要拉取的微服务的静态资源 | middleware | 否 | (appName: string, loadJs?: Function, loadCss?: Function) => Promise | 配置如何拉取js资源的中间件

      正如createSocket中的样例代码所示,独立部署的两个微服务要进行通信,需要基于同一个bus实例,为了达到这个目的,在new Bus()创建出bus实例后,我们把这个实例手动挂载到window对象上,这样带来的一个问题是,当有多个团队分别基于多个bus进行通信时,有可能会不小心命名出重名bus,出现全局变量冲突。因此,obvious提供了createBus函数,它会创建一个Bus,并将其挂载到window.__Bus__上,例如,执行createBus('global')将会new一个Bus实例并挂载在window.__Bus__上,然后可以通过getBus('global')获取bus实例,执行其他操作。推荐使用createBus来创建bus, 因为不必添加额外的全局变量,且window.__Bus__做了属性保护,是只读的,挂载在window.__Bus__上的属性也是只读的,可以保证bus实例创建并挂载后不会被修改。用createBus创建同名bus,在运行时会抛出错误提示。

    • getBus()

      | 参数名 | 是否必选 | 类型 | 描述 | |:---:|:---:|:---:|:---:| | name | 是 | string | bus的名字 |

      获取bus实例,与createBus搭配使用

  • 预置状态

    • ${appName}: 表示名字是appName的微服务就绪。这个状态在用createSocket创建出app对应的socket,且回调函数执行完后被init,常用于声明微服务依赖 例子: 有两个微服务A和B,基于demoBus进行消息通信 微服务A在启动时监听printHelloWorld事件:
      import {getBus} from '@runnan/obvious';
      
      const bus = getBus('demoBus');
      bus.createSocket('A', [], (socket) => {
          socket.on('printHelloWorld', () => {
              console.log('Hello World');
          });
      });
      微服务B在启动时触发printHelloWorld事件,为了保证在触发事件时,微服务A已经监听了该事件,微服务B在创建socket时可以把$A作为状态依赖:
      import {getBus} from '@runnan/obvious';
      
      const bus = getBus('demoBus');
      bus.createSocket('B', ['$A'], (socket) => {
          socket.emit('printHelloWorld');
      });
      在平台服务中,由于微服务B已经声明了它依赖微服务A,因此即使demoBus先拉起微服务B,B的回调逻辑也会等待A的回调逻辑执行完后才执行
      import {createBus, getBus} from '@runnan/obvious';
      
      createBus('demoBus', {
          A: {
              js: ['http://{hosta}/assets/a.js'] // 先执行A
          },
          B: {
              js: ['http://{hostb}/assets/b.js'] // 后执行B
          }
      });
      
      const bus = getBus('demoBus');
      
      bus.startApp('B'); // 先拉起B
      bus.startApp('A'); // 后拉起A
  • Q&A

    • Q: 不同微服务定义的全局变量和样式如何避免互相影响? A: 对于要定义全局变量的场景,建议改为通过socket定义微服务私有状态, 或者使用Symbol把全局变量挂载到window对象上;为了避免样式污染,建议在构建时加入css module特性
    • Q: 事件收发和状态更改是同步的还是异步的? A: EventEmitter的所有操作都是同步的,obvious的状态机制也是基于同步的EventEmitter实现的
    • Q: 内置状态${appName}就绪可以保证app的所有代码都执行完了吗? A: ${appName}就绪只能保证app内的所有同步逻辑都执行完了,而不能保证异步逻辑也执行完了
  • 关联项目

    • react-obvious: 结合了obvious和react的一个类react-redux框架
    • feda: Front End Deploy Agent, 基于Node.js、Nginx、docker技术构建的一个前端静态资源管理应用,可以帮助你理解obvious资源配置中间件的使用场景
    • omicro-cli:适配Feda的微前端脚手架,支持生成代码模板和上传服务包到Feda
  • 扩展生态

    • 对Vue、Angular框架的适配
    • 微前端单页框架,实现react-router控制页面跳转,而页面渲染则不限制技术栈
  • 写在最后

    本代码仓的demo目录是一个简单的微前端示例工程,实现了把Vue页面嵌入React单页框架中的功能,为了方便开发者理解“不同微服务能独立部署”的含义,没有使用dev-server热更新,而是用express进行静态伺服。

    闭门造车,水平有限,现将代码开源,希望感兴趣的朋友能提出意见,一起交流,欢迎issue、fork和PR。你的star是我前进的动力