(主要关于方案的设计与思考,如果能对有需要的同学有帮助就最好了)

前言:

客户端如果肆意妄为的, 添加web界面, 必定会带来CPU和内存的上升, 这个问题, 短期不解决没什么,但是随着产品的迭代, 因为项目改造成本就上去了,这个定时炸弹引爆之后就越棘手。

客户端

c++端解决方案,简单的一句话就是,只要管好web实例即可,但是不能,一刀切,要分类: 因为有些web界面(交互多,推送多的项目)并不适合单进程的方案,原因是:web做资源回收的成本上去之后,而且界面业务周期短,没有做资源缓存的必要;(tip:因为存在可能会在初始方案中一刀切,这样来,潜在的会严重影响web端开发效率)

先简单介绍下,单进程的客户端方案:

首先如果是采用cef方案的基本会有一个CefViewWidget, 而在这个之上都会做一层封装, 主要拿到cef实例后做监听事务,推送服务,等操作:构造函数中初始化的有两个关注点:

m_cefInstance,
m_key

WebWindows::WebWindows(QWidget* parent, const QString uri, const QString key, QMap<QString, std::shared_ptr<CefViewWidget>>* cefInstance)
	: QMainWindow(parent),
	m_cefInstance(cefInstance),
	m_key(key)
{
	setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
	setContentsMargins(0, 0, 0, 0);
	// 防止出现预加载的容器边框
	setStyleSheet("background: transparent;");
	setWindowFlags(Qt::FramelessWindowHint);
	createCefView(uri);
}

而在webwindow中申明了QMap<QString, std::shared_ptr<CefViewWidget>>* m_cefInstance;
这个专门管理web实例;

确定好管理的对象后就是创建过程

// 创建浏览器弹窗对象
void WebWindows::createCefView(const QString uri)
{

	qDebug() << "this create ing" << m_pCefViewWidget;
	if (m_pCefViewWidget) {
		m_pCefViewWidget->deleteLater();
		m_pCefViewWidget = nullptr;
	}
	QCefSetting setting;
	// 设置OSR帧率
	setting.setWindowlessFrameRate(60);
	int goToFlag =  0;

	// 如果存在实例, 直接获取即可
	
	if (m_key == "") {
		qDebug() << QStringLiteral("不需要共享实例");
		// 无key, 代表web实例不需要共享
		m_pCefViewWidget = new CefViewWidget(uri, &setting, this);
	}
	else 
	{
		if (m_cefInstance == nullptr) {
			initCefWidget(uri, &setting);
		}
		else 
		{
			if (m_cefInstance->contains(m_key)) {
				// 获取实例
				m_pCefViewWidget = (*m_cefInstance)[m_key].get();
				goToFlag = 1;
			}
			else
			{
				/*m_pCefViewWidget = new CefViewWidget(uri, &setting, this);*/
				initCefWidget(uri, &setting);
			}
			
		}
	}

	if (goToFlag == 0) {
		m_pCefViewWidget->setContextMenuPolicy(Qt::DefaultContextMenu);


		m_pCefViewWidget->setContextMenuPolicy(Qt::DefaultContextMenu);
		setCentralWidget(m_pCefViewWidget);

		//  生成页面就发送用户基本信息
		QJsonObject userInfoJson;//构建json对象json
		userInfoJson.insert("userName", UserInfo::instance().getUserName());//将dataobj中的数据插入到json对象中
		userInfoJson.insert("token", UserInfo::instance().getAccessToken());

		QJsonDocument document;
		document.setObject(userInfoJson);
		QByteArray byte_array = document.toJson(QJsonDocument::Compact);
		m_userInfo = QString(byte_array);
	}
	else 
	{
		qDebug() << "this is exist";
		m_pCefViewWidget->navigateToUrl(uri);
		setCentralWidget(m_pCefViewWidget);
	}
}

解释下上面的

    QString m_key;// 实例的唯一识别码

是的, 用来标记web界面的, 这个可以和web端开发者协商, 可以存储在ini文件中本地读取,但是更推荐的是初始客户端的时候直接从服务端先加载到本地,这样,客户端一开始就知道哪些web界面需要共享进程

有些会好奇m_cefInstance 是干什么的, 这个是单进程需要的也就是说多进程中并不需要

关键点在于:
 

if (m_cefInstance->contains(m_key)) {
	// 获取实例
	m_pCefViewWidget = (*m_cefInstance)[m_key].get();
	goToFlag = 1;
}

这里会拿到匹配到的key相关的web实例对象

这里是初始化的过程:

void WebWindows::initCefWidget(const QString& uri, const QCefSetting* setting)
{
	m_cefInstance->insert(m_key, std::make_shared<CefViewWidget>(uri, setting, this));
	m_pCefViewWidget = (*m_cefInstance)[m_key].get();
}

然后就是关键的怎么单例公用一个web对象 这里还涉及两个分层,展示下:(只贴关键代码)

AssetsWindow: 入口界面(可以这么理解)

class AssetsWindow : public BasicWindow
{
	Q_OBJECT

public:
	AssetsWindow(QWidget *parent = Q_NULLPTR);
	~AssetsWindow();
public:

	QMap<QString, std::shared_ptr<CefViewWidget>> m_webInstance; // 针对tab界面中的web界面做单例,同类型tab只存在一个实例:所以如果需要不同实例,那就开启不同类型tab
};

生成页面

	defaultWiget = std::make_shared<EcospherePannel>(m_tabWidget, pageUri, tabKey, &m_webInstance);

为了防止被回收掉,所以我把web实例交给最外层管理,这里就是AssetsWindow;

然后这个是我的中间层: 拿到外层的webinstance之后,去初始化

class EcospherePannel : public BaisicTabPannelLayout
{
	Q_OBJECT

public:
	EcospherePannel(QWidget *parent = nullptr, const QString& pageUri = "", const QString& tabKey = "", QMap<QString, std::shared_ptr<CefViewWidget>>* cefInstance = nullptr);
	~EcospherePannel();
public:
	QMap<QString, std::shared_ptr<CefViewWidget>>* m_cefInstance; // web对象实例 需要透传
};

然后在需要加载页面的地方,再下放到webwindows中去让他添加,也就是这个引用,目前是传递到了webwindow

// 加载初始的路由页面: 详情界面
void EcospherePannel::loadInitialModule()
{
	// 列表界面也用web端 取消客户端二级界面
	QString detailUri = m_uri;
	if (detailUri == "") {
		detailUri = "http://xxxxxx/";
	}
	
	m_detailPage = std::make_shared<WebWindows>(parentWidget(), detailUri, m_tabKey, m_cefInstance);
	addRoute(m_detailPage);
}

因为仿造web端重新设计了c++的客户端所以在添加路由的地方写了实例化:

std::make_shared<WebWindows>(parentWidget(), detailUri, m_tabKey, m_cefInstance);

这一部实例化就解释了上面webwindows的代码中可能会有的疑惑: m_cefInstance哪来的。

最后就是切换界面这个就是直接拿webwindows零时获取到的m_pCefViewWidget 执行cef槽方法,去做跳转就行

到此c++端基本大致思路就结束了,整体设计思路简单的一点就是不要让实例的对象被回收即可;

只是提供一种解决方案的思路,比较简陋,但是关键点,不管哪个方案,还需要和web端沟通好,单进程和多进程的都需要给web端留个口子,接下来就是沟通中需要留哪些口子,继续说明下

web端

c++端单进程方案好了之后,剩下为了web端可以正常运行,有几点比较重要:

1: 不能寄希望,在切换页面做资源回收,所以要事先让qt端在切换或者加载界面的时候主动广播事件broadcastEvent(cefEvent);

是的: 加载界面的信号需要从c++端给到web端

2: 这里在实际操作中很快就会意识到,登录问题, 基本上之前有客户端内嵌web的场景的自然沉淀一套自己的登录机制,但是这里还是说一下:

客户端直接给个方法, 依托QCefView::invokeMethod qt的槽方法,返回登录所需要token等信息, 注意一点的是,一定要加参数,这个参数主要用来校验这个调用方是否合法的,校验方法可以很多种,双方约定即可

到这里大致方案的设计思路已经讲的差不多了,最后给一下js端的方法

import { useEffect ,useCallback } from "react";

export const  useCefSocket = (eventName="", fn) => {
    const resolveResp = useCallback((...args)=>{
        fn(...args);
    },[])
    useEffect(()=>{
        if(window.CallBridge && String(eventName).trim().length > 0){
            window.CallBridge.removeEventListener(eventName, resolveResp);
            window.CallBridge.addEventListener(eventName,  resolveResp);
        }
    },[])
}

export const  handleInvokeCClient = (eventName, ...arg) => {
    // invoke C++ code
    if(window.CallBridge && String(eventName).trim().length > 0){
        window.CallBridge.invokeMethod(eventName, ...arg);
    }
}

// 告诉客户端前端已经页面加载完成
export const  useHandleInvokeDidMounted = () => {
  useEffect(()=>{
    if(window.CallBridge){
      window.CallBridge.invokeMethod('readyReceiveMsg');
    }
  },[])
}

export const  onCallQuery = (request, callBack) =>  {
    let query = {
      // id: 1,
      request,// 可是是复杂的json数据
      onSuccess:  (response) =>  {
        // window.alert(response);
        console.log(response)
        callBack && typeof callBack == 'function' &&  callBack(response);
      },
      onFailure:  (error_code, error_message) => {
        console.log(error_code, error_message);
      }
    };
    window.CefViewQuery(query);
  }

这里之所以会多出一个useHandleInvokeDidMounted 的钩子, 是因为为了可以让web端初始化界面加载和普通的调用客户端方法做有效区分,如果不想区分也行,这个可以结合具体业务做改动

web端再追加一下:

关于web端设计逻辑:

说下思路:

外层套的微应用你可以选择乾坤, 如果感觉笨重了,可以选择无界,要是再简陋点,你甚至iframe都行,但是不建议,因为有效的外部生命周期和数据管理,成熟的为前端框架都可以做到有效的适配

然后就是数据存储,存储实际就是切换界面,这里在这里只要和客户端做好通信,一定要在切换前先把有效数据存储到缓存中,比如IndexedDB

以下就是所说的有效数据:

  • 当前页面展示的数据。
  • 用户已经完成的交互状态。
  • 页面滚动位置或特定视图状态。

web端在切换微应用展示的时候,直接读取缓存数据即可

多提一嘴的是:也可以再切换界面的时候,直接把数据交给c++,因为其实这个方案也很好,这样做的好处是,更加集中化的管理,和更持久的数据。

感谢阅读~~~~

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐