混合开发中:qt内嵌web,如何提升性能(随笔)
客户端如果肆意妄为的, 添加web界面, 必定会带来CPU和内存的上升, 这个问题, 短期不解决没什么,但是随着产品的迭代, 因为项目改造成本就上去了,这个定时炸弹引爆之后就越棘手。
(主要关于方案的设计与思考,如果能对有需要的同学有帮助就最好了)
前言:
客户端如果肆意妄为的, 添加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++,因为其实这个方案也很好,这样做的好处是,更加集中化的管理,和更持久的数据。
感谢阅读~~~~
更多推荐
所有评论(0)