likes
comments
collection

实现前端插件化架构设计,将需求开发「交给别人」

作者站长头像
站长
· 阅读数 3

前言

demo代码仓库: https://github.com/ericlee33/...

如果有帮助到的话,欢迎点个 star ⭐️ & follow https://github.com/ericlee33

背景

业务开发中遇到的问题

笔者现在在中台部门工作,平日业务开发时,经常遇到一个场景,业务方会提需求过来,需要中台方去协助定制开发业务插件,以此来补齐业务方需要的功能。

作为前端开发,如果不加以思考,很容易就会陷入到业务之中,实际上是业务方自己的需求,但是却需要我们来帮助开发。

在这种时候,我们需要思考一种方案,来把自己从需求黑洞中抽离出来。为什么不让业务方自己来作为插件开发者,自己开发呢?

有的同学可能会问,直接让需求方在中台项目仓库中去开发不就好了?

可不可以业务方自行开发呢?

听起来好像是一种办法,但是当项目结构很庞大,比如为多packages组成的monorepo项目,插件开发者就会遇到如下问题:

  • 很难快速理清中台项目的代码逻辑
  • 开发前,需要先安装项目依赖、本地启动项目,尤其是在项目庞大的情况下,本地首次编译项目,可能需要20分钟甚至更多,对业务方同学来说,简直是痛苦
  • 中台项目会有代码规范,插件开发者一上来也不一定能直接满足项目设立的代码规范,MR的时候需要中台前端同学帮忙做Code Review,也会消耗中台同学的精力
  • 插件开发者的代码逻辑如果有问题,可能会导致中台项目线上白屏

目标

整体流程

业务方自行开发过程中,实际上会存在一个很大的痛点,插件开发者不得不深入到我们的项目中,去了解内部实现细节。并且需要很长时间编译项目才能启动

⭐️ 如果,有一种方式,能提供给插件开发者插件模板,并且能用我们平台的线上环境去远程加载业务方同学本地开发的模板代码,让业务方进行本地开发插件,这样是不是就省去了让业务方理解我方平台代码的成本?

对流程进行拆解

让我们整理一下,如果需要实现上面这个流程,我们需要以下能力

React项目中,实现远程组件调用

有人可能会问,React不是可以通过import()语法去动态调用组件吗,这样不可以吗?

这种情况会遇到一个问题,像react是只允许单实例的,插件组件打包时候,需要配置externals,避免两次调用,这样的话就会出现依赖无法正常注入的问题。如果有同学感兴趣这块的话后续我会继续写一篇关于这块的文章。

线上环境调试本地调试代码

我们如果能做到通过某种方式,代理到线上环境接口,劫持接口返回值,来注入本地远程组件对应url地址,是不是就相当于实现了线上环境调试本地调试代码这种黑魔法

插件模板生成

这里为了让开发者体验更好,我们可以开发一个脚手架,根据开发者不同的要求,去生成不同的模板

插件产物打包上传

这里不细讲,如果感兴趣我后续可以再更一篇文章,思路是有以下两种:

  • 实现一个插件开发者后台,进行插件统一管理、版本控制、上传操作
  • 实现 VSCode 插件,让开发者在 VSCode 中,就可以将全流程闭环

技术方案

针对以上的想法,我进行了技术方案调研。

远程组件引入

在平台项目中,需要引入对打包好的远程组件进行动态加载

这里我们使用 remote-component 这个npm包来解决这个问题

github地址:https://github.com/Paciolan/r...

什么是远程组件

这里引用npm包文档的说明

远程组件在运行时从URL加载。它的使用方式与其他React组件相同

远程组件使用方法

const url =
  "https://raw.githubusercontent.com/Paciolan/remote-component/master/examples/remote-components/HelloWorld.js";

const HelloWorld = ({ name }) => <RemoteComponent url={url} name={name} />;

const Container = (
  <>
    <HelloWorld name="Remote" />
  </>
);

线上环境调试本地调试代码

为了达到这个目的,我们需要实现以下两点:

  • 搭建代理服务器
  • 劫持浏览器的请求,让浏览器的请求全部打到代理服务器上

搭建代理服务器

代理服务器我们使用Anyproxy使用文档

浏览器代理插件

我们使用SwitchOmega,它可以对浏览器进行proxy代理

插件地址:https://chrome.google.com/web...实现前端插件化架构设计,将需求开发「交给别人」

插件模板脚手架

我们可以使用commander/inquirer/chalknpm包,设计开发脚手架工具

插件产物打包上传

插件开发者后台

如果是这样去实现的话,那我们需要在页面中提供以下功能:

  • 新增插件
  • 删除插件
  • 插件产物上传
  • 插件发布
  • 插件版本回退

VSCode插件

VSCode插件开发实现的功能类似于后台,但是可以免去在后台登录,以及本地打包,再拖拽产物到后台再上传的繁琐流程

插件开发API文档: https://code.visualstudio.com...

流程图

开发者视角

<div align="center"><img width="200" src="https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/98d682e2306346bdb5df6386a9f13f28~tplv-k3u1fbpfcp-watermark.image?" /></div>

平台维护者视角

<div align="center"><img width="400" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/6368c09e1f8f4094b023db1a8f2d05fe~tplv-k3u1fbpfcp-watermark.image?" /></div>

具体实现

项目结构

这里我们用pnpm先搭一个最简骨架,包含我们实现黑魔法所需的各个子项目

.
├── README.md
├── package.json
├── packages
│   ├── anyproxy-server  // 代理服务器,端口11111,劫持浏览器请求
│   ├── egg-server       // 后端服务,这里是为demo提供一个模拟的接口,供代理服务器进行代理,注入本地模板项目打包产物js对应的url
│   ├── platform         // 平台项目
│   └── plugin-page      // 插件项目
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
└── tsconfig.json

创建一个后台服务

为了展示demo效果,我们启动一个egg的小后台,只提供给平台项目一个/api/plugin_detail接口,用于获取远程组件url,我们暂时把返回的data写死为{},后续远程组件的url会通过代理服务器劫持提供,

这样就相当于是去模拟实现,代理线上接口调试本地代码了
import { Service } from 'egg';

/**
 * Test Service
 */
export default class Test extends Service {
  /**
   * sayHi to you
   * @param name - your name
   */
  public async sayHi() {
    return {
      code: 0,
      data: {},
      msg: 'ok',
    };
  }
}

开发代理服务器包

这里我直接提供代理服务器核心代码,我们设置代理服务器运行在本地11111端口,这里我们去代理后台提供的/api/plugin_detail接口,在返回时,增加注入本地模板项目打包好的产物url,这里我们的urlhttp://localhost:9001/main.bundle.js,后续会讲解这个url是怎么来的

const AnyProxy = require('anyproxy');
const rule = require('./rule/index');

const DEFAULT_PORT = 11111;

const proxyServer = new AnyProxy.ProxyServer({
  rule: {
    beforeSendResponse: (requestDetail, responseDetail) => {
      if (requestDetail.url.includes('/api/plugin_detail')) {
        const data = JSON.parse(responseDetail.response.body.toString());
        data.data.url = 'http://localhost:9001/main.bundle.js';
        responseDetail.response.body = JSON.stringify(data);
        return responseDetail;
      } else {
        return responseDetail;
      }
    },
  },
  port: DEFAULT_PORT,
  throttle: 10000,
  forceProxyHttps: true,
  wsIntercept: false, // 不开启websocket代理
  silent: false,
});

proxyServer.start();

配置浏览器代理插件

安装好浏览器代理插件之后,我们启动插件,点击新增new profile,我们随便起个名字,就叫test_local设置代理到本地代理服务器的端口,这里我们使用上面设置的11111端口。进行完这一步之后,我们就能将浏览器的请求打到我们本地代理服务器的上了

注意,要在Bypass List中配置<-loopback>,不然代理不到本地接口

实现前端插件化架构设计,将需求开发「交给别人」

到这里,前置工作就准备好了,下面我们开始开发平台项目,和插件页面

FAQ

  1. 如果代理网站时,网站不被信任,需要信任anyproxy的证书,不同系统证书位置不同,例如MacOS在如下目录 ./.anyproxy/certificates/rootCA.crt

开发插件

写一个demo插件页面

我们首先写一个最简插件模板,这里我们直接写死,实际可以用脚手架生成一个插件项目模板出来

import React from 'react';
import './test.css';

const Plugin: React.FC<{}> = () => {
  return (
    <div
      style={{
        color: 'red',
        fontSize: '32px',
      }}
    >
      这个是远程加载的插件组件
    </div>
  );
};

export default Plugin;

webpack配置

这里我抽离几行核心配置,主要要注意libraryTarget需为commonjs格式,以及我们需要配置externals,避免打包react到插件产物中。

为了demo能跑起来,我们给devServer配置跨域头为*,并且运行到9001端口,这样本地插件产物js文件的地址就会是http://localhost:9001/main.bundle.js

  output: {
    filename: '[name].bundle.js',
    libraryTarget: 'commonjs',
  },
  externals: {
    react: 'react',
  },
  devServer: {
    hot: true,
    port: 9001,
    headers: {
      'Access-Control-Allow-Origin': '*',
    },
  },

在平台项目中,配置远程组件

注入远程组件所需的依赖

由于远程组件我们设置了externals属性,避免打包react,我们在平台项目中,需要提供react依赖给插件模板,这样插件模板才可以引用到react包的依赖

/**
 * Dependencies for Remote Components
 */
module.exports = {
  resolve: {
    react: require("react")
  }
};

webpack配置

这里我粘贴部分代码,主要核心点是我们需要在webpack.config.js中添加一个Webpack别名,这样RemoteComponent就可以加载这个文件。

module.exports = {
  resolve: {
    alias: {
      "remote-component.config.js": __dirname + "/remote-component.config.js"
    }
  }
};

平台调用远程组件方式

这里我调用我们小后台提供的/api/plugin_detail接口,本身接口没有返回url地址,但是当我们开启代理服务器时,就可以获取到本地插件组件打包产物的地址了

import React, { useEffect, useState } from 'react';
import { RemoteComponent } from '@paciolan/remote-component';
import { Card } from 'antd';

interface IPlatFormEntryProps {}

const PlatFormEntry: React.FC<IPlatFormEntryProps> = () => {
  const [url, setUrl] = useState('');
  const [loading, setLoading] = useState(true);
  useEffect(() => {
    setTimeout(async () => {
      const rawRes = await fetch('http://localhost:7001/api/plugin_detail', {
        method: 'GET',
      });

      const res = await rawRes.json();

      const url = res.data.url;

      setLoading(false);
      setUrl(url);
    }, 1000);
  }, []);

  const HelloWorld = (props) => <RemoteComponent url={url} {...props} />;

  return (
    <Card title={'主应用启动完毕'}>
      {loading ? <div>Loading,正在加载远程组件.....</div> : <HelloWorld />}
    </Card>
  );
};

export default PlatFormEntry;

整体流程联调以及效果展示

具体实现可以参考项目源码,我已经将源码贴在文章最下方

启动浏览器代理插件

我们选择我们刚刚配置好的test_local配置项实现前端插件化架构设计,将需求开发「交给别人」

启动我们monorepo内的4个子项目

进行平台项目目录,启动平台项目

cd packages/platform
pnpm dev

进行插件项目目录,启动插件项目

cd packages/plugin-page
pnpm dev

进行代理服务器项目目录,启动代理服务

cd packages/anyproxy-server
pnpm dev

进行后端项目目录,启动后端项目

cd packages/egg-server
pnpm dev

查看效果

访问平台项目首页,即可看到效果http://localhost:9000

实现前端插件化架构设计,将需求开发「交给别人」

等待接口返回后,即可加载出远程组件内容

实现前端插件化架构设计,将需求开发「交给别人」

实现前端插件化架构设计,将需求开发「交给别人」

demo代码仓库: https://github.com/ericlee33/...

如果有帮助到的话,欢迎点个 Star ⭐️ & follow https://github.com/ericlee33

欢迎关注前端江湖行公众号,我会分享很多有趣的技术文章,希望帮助更多的人深耕前端技术,紧跟技术潮流。