MFC嵌入网页方案选型与实战:WebBrowser、WebView2、CEF3对比

发布时间:2026/9/29 17:26:47
MFC嵌入网页方案选型与实战:WebBrowser、WebView2、CEF3对比
早上刚坐下产品经理就甩过来一张截图旧的C MFC桌面程序里原来用CStatic画出来的一块报表区域现在要换成HTML图表还要能自适应窗口大小。我盯着屏幕沉默了十秒——不是不会是这个问题年代跨度太大方案太多坑更多。MFC嵌入web网页控件这件事主流路线其实就三条老派的WebBrowser、现代的WebView2、重型的CEF3。选错一次返工一周后面测试还会追着你改三遍。这篇文章就把三条路都摊开讲明白把你在MFC工程里真正会遇到的细节和坑一次说清包括HTML怎么铺满界面、WebView2 Runtime找不到怎么办、CEF3怎么和MFC消息循环和平共处这些平时文档里不写清楚的东西。1. 三条技术路线为什么MFC迟迟没有一个官方标准答案1.1 问题的本质桌面壳网页心MFC这套框架说到底就是Win32消息循环加句柄封装它自己是不带现代浏览器内核的。要在MFC窗口里显示网页本质上是解决三件事谁来渲染页面、渲染出来的窗口怎么嵌进MFC的窗口树、网页里的JavaScript怎么和C代码互相调用。这三点决定了你没法直接抄一套固定代码就跑。WebBrowser是COM组件WebView2是异步创建控制器CEF3更是直接拉起一整条Chromium子进程链。三者底层模型完全不同MFC这边能做的只有一件事——提供一个HWND让对方的渲染输出落在你的窗口区域里。谁来做这个宿主、怎么做就是选型的核心。很多刚接触的人会先去搜“MFC嵌入网页控件”然后找到一大把WebBrowser的帖子复制过来发现页面确实显示出来了但一跑现代前端项目就白屏。原因很简单WebBrowser内核是旧版IEES6语法和现代CSS特性支持残缺不是代码写错了是内核老了。而WebView2和CEF3都是Chromium内核同一套HTML代码在不同方案下的表现差距极大。所以第一步不是写代码是搞清楚你到底需要哪个内核。1.2 三条路线的横向对比维度WebBrowserWebView2CEF3内核IEMSHTMLChromiumEdge RuntimeChromium自带目标机依赖Windows自带IE可用即可需要WebView2 Runtime无全部随应用分发发布体积0运行时约100~200MB可离线安装包资源文件通常30~150MB现代Web支持差ES6兼容性差好紧跟Chromium版本好完全可控与MFC交互COM事件和IDocHostUIHandlerWebMessage异步通信CefV8Handler/消息路由多进程隔离无进程内ActiveX有独立渲染进程有独立渲染进程维护状态微软已停止新特性开发官方主推文档齐全开源社区维护依赖自己升级学习成本低中高内核版本可控性不可控跟随系统IE跟随已安装Runtime完全可控锁版本这个表基本能回答我第一次接触这问题时最纠结的部分。WebBrowser不是不能用而是你得先确认你的HTML内容有没有超出IE的兼容范围。WebView2是现在大多数新项目的合理起点官方在更新文档全部署虽然有运行时依赖但一次性装好就稳定。CEF3看起来和WebView2类似但它最大的区别是“内核完全由你打包分发”不依赖用户机器上装了什么。1.3 选型决策简表我的习惯是拿三个问题过滤一遍网页是简单静态页、本机帮助文档或老内网系统选WebBrowser半天搞定零依赖。要跑现代前端框架、要跟最新Web技术同步、目标机可以安装Edge Runtime选WebView2。目标机是Windows 7且不能用新运行时、需要锁定某个Chromium版本、需要自定义浏览器协议处理或深度定制内核行为选CEF3。注意最后一条Windows 7场景很关键。WebView2 Runtime的官方支持版本到109之后就不再支持Windows 7了如果你还要维护Win7机器就得用固定版本方式把旧版Runtime一起分发或者干脆上CEF3直接带一个老版本的Chromium走。这个决策在选型阶段就要做不要等到客户现场装一半才改那时候工单已经飞到老板桌子上去了。2. WebBrowser控件最快上手的兼容方案2.1 它到底是什么WebBrowser控件本质上是一个COM ActiveX组件封装的是MSHTML引擎也就是IE浏览器的排版引擎。在MFC里使用它系统会把它当做一个普通窗口控件嵌入对话框你通过它的COM接口调Navigate2、ExecWB这些方法控制行为。它的最大价值在于“零部署”。只要目标是Windows系统且IE组件存在控件的功能就可用不需要额外安装任何运行时。很多老MFC系统内部用的都是它比如把一个帮助手册页面嵌到用户手册弹窗里。如果HTML是你们自己控制的内容又比较老派它的性价比其实非常高。但它的缺点也很扎心微软已经不再为IE内核增加新特性HTML5支持停留在很早期的状态。现代前端框架构建出来的页面默认产物它基本跑不起来。如果你在WebBrowser里打开一个Vue或React打包后的页面最常见的现象是白屏或者报语法错误按F12都看不到调试信息排查成本很高。所以用它之前一定先确认你的页面不是现代SPA。2.2 在MFC对话框中快速放一个WebBrowser第一步在对话框资源编辑器里右键工具箱空白处选择“插入ActiveX控件”找到“Microsoft Web Browser”拖到对话框上。这个操作会自动生成一个CWnd派生封装类通常叫CWebBrowser2。第二步在对话框头文件里包含对应头文件并在InitInstance里调用AfxEnableControlContainer();这行代码很重要。MFC对话框要承载ActiveX控件必须启用控件容器支持不调用的话WebBrowser控件根本创建不出来运行时会直接断言失败或者控件区域一片空白。第三步在OnInitDialog里导航到目标地址BOOL CMyDialog::OnInitDialog() { CDialogEx::OnInitDialog(); // m_webBrowser是资源编辑器自动生成的控件变量 m_webBrowser.Navigate2(_T(https://example.com/report.html), nullptr, nullptr, nullptr, nullptr); return TRUE; }如果你不希望网页上有导航、右键菜单这些浏览器痕迹可以调用// 关闭脚本错误提示 m_webBrowser.ExecWB(OLECMDID_HIDETOOLBARS, OLECMDEXECOPT_DONTPROMPTUSER, nullptr, nullptr);这种写法优势是简单直接但有一个隐患Navigate2返回的是异步状态如果URL不可达控件区域就是空白不会给MFC这边任何有效回执。所以生产环境里一般还会挂一个DocumentComplete事件用来提示页面加载结果。2.3 网页怎么回调MFC代码WebBrowser的JS和C通信不像WebView2那样有现成的异步消息接口主流做法是拦截DISPID_NAVIGATECOMPLETE2事件然后通过IDispatch注入一个外部对象给页面。要用好这一套你得在CWebBrowser2的派生类里映射事件接收器然后处理页面里约定的特殊URL协议比如BEGIN_EVENTSINK_MAP(CMyDialog, CDialogEx) ON_EVENT(CMyDialog, IDC_WEBBROWSER1, DISPID_NAVIGATECOMPLETE2, OnNavigateComplete2, VTS_DISPATCH VTS_VARIANT) END_EVENTSINK_MAP()OnNavigateComplete2里可以解析URL参数。如果页面跳转到类似“myapp://getcpu?id123”这样的自定义协议你截获后解析参数再通过Navigate2往页面回填结果。这种“伪协议”方式虽然土但是老项目中非常稳定。如果你想正经一点让页面直接调用C方法就需要实现IDocHostUIHandler和IDispatch把外部接口挂到window.external上。这部分代码量不小还要处理COM引用计数稍有不慎就会内存泄漏。我的建议是WebBrowser方案里能用伪协议搞定就不要去撸IDocHostUIHandler省下来的时间够你多测两轮回归。2.4 WebBrowser的常见坑默认文档模式太低。IE默认可能用IE7兼容模式渲染哪怕你本机IE11也一样。需要改注册表给进程名设置FEATURE_BROWSER_EMULATION让它用高版本文档模式。网页弹窗和MFC对话框抢焦点。JS里的alert在嵌入控件里有时会弹到窗口后面用户以为程序卡死了。解决办法是尽量不用alert页面内部自己画提示。COM释放顺序。程序退出时要确保WebBrowser控件先释放再调用CoUninitialize否则退出时可能崩溃。建议在OnDestroy里先销毁控件。WebBrowser最让我头疼的其实是调试。页面出问题你不能像浏览器里按F12那样直接看Console只能靠错误弹窗和协议日志。所以我在实际项目中给它的定位很明确只放内部系统页面不放面向用户的复杂交互页面。3. WebView2我给新项目的默认处方3.1 为什么默认选它WebView2是微软现在的官方答案用于替代WebBrowser控件。它基于Chromium内核渲染能力跟Edge浏览器保持一致而且它给出的API设计明显更适合桌面程序异步初始化、事件驱动、网页和原生互相发消息。你在MFC里的宿主窗口只需要提供一个HWND它自己拉起独立的渲染进程网页崩溃不会把整个MFC程序带崩。相比CEF3WebView2最大的优势是你不用自己管理Chromium的生命周期。CEF3要你下载CEF包、配置CefSettings、处理浏览器进程和渲染进程的消息循环而WebView2只要系统装了Runtime你在代码里创建环境然后等回调就行。我经常跟同事说CEF3是让你开一辆手动挡车WebView2是自动挡日常通勤别折腾自己。当然它的代价是运行时依赖。目标机器上如果没装WebView2 Runtime程序启动或创建环境时会直接报错这是我们后面要重点处理的问题。3.2 在MFC对话框里初始化WebView2先通过NuGet安装Microsoft.Web.WebView2包。安装后包含头文件#include WebView2.h在OnInitDialog里写初始化逻辑BOOL CMyDialog::OnInitDialog() { CDialogEx::OnInitDialog(); // 获取宿主窗口句柄 HWND hWnd GetDlgItem(IDC_WEBVIEW_HOST)-GetSafeHwnd(); // 创建用户数据文件夹避免每次启动脏数据堆积 CString userDataFolder GetAppDataPath() _T(\\MyApp\\WebView2); CreateCoreWebView2EnvironmentWithOptions( nullptr, // 使用系统安装的Runtime userDataFolder, // 用户数据文件夹 nullptr, CallbackICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler( [hWnd](HRESULT result, ICoreWebView2Environment* env) - HRESULT { if (FAILED(result)) { return result; } env-CreateCoreWebView2Controller( hWnd, CallbackICoreWebView2CreateCoreWebView2ControllerCompletedHandler( [](HRESULT result, ICoreWebView2Controller* controller) - HRESULT { if (FAILED(result)) { return result; } // 保存controller后续调用都用它 return S_OK; }).Get()); return S_OK; }).Get()); return TRUE; }特别注意这个异步模型。CreateCoreWebView2EnvironmentWithOptions是异步的回调线程不一定是UI线程所以在回调里操作控件或成员变量时要用PostMessage把逻辑切回UI线程。如果你图省事在回调里直接访问对话框成员变量极大概率会遇到数据竞争导致的崩溃尤其是Release模式下特别隐蔽。创建完controller后获取它的CoreWebView2对象就可以导航wil::com_ptrICoreWebView2 webview; controller-get_CoreWebView2(webview.put()); webview-Navigate(Lhttps://example.com);3.3 让HTML铺满MFC界面的正确姿势这是我在社区里看到问得最多的一个问题“HTML全覆盖在MFC界面webview2”怎么实现。默认情况下WebView2创建出来的窗口大小跟你的宿主控件区域不一定完全一致尤其是对话框初始化时控件矩形还没最终确定回调执行时窗口尺寸往往是错的。所以要在得到controller后立即设置Bounds并且响应WM_SIZE随时调整。先封装一个内部函数void CMyDialog::UpdateWebViewBounds() { if (!m_controller) return; RECT rc; GetDlgItem(IDC_WEBVIEW_HOST)-GetClientRect(rc); m_controller-put_Bounds(rc); }在OnInitDialog创建成功后的回调里调用一次然后重写OnSizevoid CMyDialog::OnSize(UINT nType, int cx, int cy) { CDialogEx::OnSize(nType, cx, cy); UpdateWebViewBounds(); }关键是让宿主控件铺满对话框。建议把IDC_WEBVIEW_HOST这个占位控件设为对话框客户区的全部或大部分区域别把网页嵌在一个固定大小的小格子里否则用户拉伸窗口后四周全是灰色空白体验很糟糕。另外还有一个很隐蔽的坑如果宿主控件被别的控件遮挡哪怕只有1像素WebView2的渲染也可能出现黑边。因为WebView2本身是一个独立窗口MFC控件绘制并不参与它的合成你需要保证Z序里WebView2在最上层。如果实在有重叠UI需求要么把重叠内容也搬到HTML里实现要么调整窗口层次不要指望透明叠加。3.4 Could not find the WebView2 Runtime怎么破这个报错几乎是每个人第一次部署WebView2都会遇到的。它分两种情况一是开发机上没装Runtime二是目标机器上没装Runtime。你写程序时用NuGet包没问题因为开发机一般跟着Edge走但客户机器上不一定有Edge。最简单的解决方案是做一个检测逻辑// 检查Evergreen Runtime注册表 HKEY key; if (RegOpenKeyEx(HKEY_LOCAL_MACHINE, LSOFTWARE\\WOW6432Node\\Microsoft\\EdgeUpdate\\Clients\\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}, 0, KEY_READ, key) ERROR_SUCCESS) { // 已安装正常使用 RegCloseKey(key); } else { // 未安装引导用户安装或使用离线包 }离线部署的方案有三种直接下载“Evergreen Standalone”离线安装包在客户机器上静默安装。如果你要锁定版本下载对应版本的“Fixed Version”包通过registry注册到系统固定版本路径并在CreateCoreWebView2EnvironmentWithOptions的first参数里传入对应路径。如果你的程序是绿色软件不想动客户机器可以在安装程序里带上Bootstrapper启动时静默安装。另一个容易被忽略的点是CPU架构。32位进程会找32位Runtime64位进程找64位Runtime。如果客户环境只装了64位运行时你的程序却以x86编译还报找不到Runtime这时候不是你没装而是位数不匹配。建议新程序统一用x64编译省掉一堆兼容性麻烦。3.5 WebView2和MFC双向通信实战让网页拿本机CPU ID网页和原生通信是WebView2做得最顺手的地方。从C往网页里传值用ExecuteScriptwebview-ExecuteScript(Lwindow.receiveCpuId(Intel Core i7);, nullptr);从网页往C里传值用PostWebMessageAsJson配合WebMessageReceived事件webview-add_WebMessageReceived( CallbackICoreWebView2WebMessageReceivedEventHandler( [this](ICoreWebView2* sender, ICoreWebView2WebMessageReceivedEventArgs* args) - HRESULT { wil::unique_cotaskmem_string message; args-get_WebMessageAsJson(message); // 解析消息比如 {cmd:getCpuId} CString strMsg(message.get()); if (strMsg.Find(LgetCpuId) ! -1) { // 用__cpuid指令取CPU ID int cpuInfo[4] {0}; __cpuid(cpuInfo, 0); char id[64] {0}; sprintf_s(id, %08X-%08X, cpuInfo[2], cpuInfo[3]); // 异步给网页回值 CString js; js.Format(Lwindow.onNativeMessage(%s);, id); sender-ExecuteScript(js, nullptr); } return S_OK; }).Get());页面里这样发消息window.chrome.webview.postMessage({cmd: getCpuId});这套机制让人很舒服原生和网页内部各自维护状态消息一来一回都是异步的不会出现WebBrowser里改DOM要各种强转的窘境。产品经理再要什么“把本机信息拿到页面上展示”基本都能用这套模式解决。4. CEF3需要完全掌控内核时的重武器4.1 CEF3和WebView2的本质区别CEF3全称Chromium Embedded Framework是开源的Chromium嵌入框架你可以下载源码或预编译包把整个Chromium渲染能力变成你自己程序的一部分。它和WebView2最大的区别在于WebView2是“你依赖系统里有一个Edge Runtime”CEF3是“你把自己的Chromium带着走”。这种区别带了两个直接结果。第一CEF3的应用不依赖用户机器上的浏览器版本你锁定了某个Chromium版本那么你程序渲染的效果在任何机器上都是一样的。第二你能管的事更多拦截请求、自定义网络栈、注册自定义JavaScript扩展、控制缓存和Cookie、甚至深度定制CEF定制接口。如果你的产品需要做浏览器级功能比如一个内嵌浏览器组件且客户要求不能有Edge Runtime依赖CEF3是更合适的选择。代价是复杂度。你下载的CEF包是一整套工程里面包含了全部源码编译后的库文件和资源文件还要处理版本匹配、多进程模型、生命周期管理。在MFC里嵌入CEF3你面对的不是一个控件而是一个完整的浏览器架构。4.2 在MFC工程里接入CEF3的骨架以CEF 100系列为例接入手头工作分五步。第一步下载对应平台的CEF包解压后把libcef.dll、资源文件icudtl.dat、*.pak、子进程exe都放到输出目录并配置链接库路径和include路径。第二步写一个入口函数区分主进程和子进程。CEF是多进程架构你的exe会被反复拉起作为渲染子进程用必须在WinMain里判断int APIENTRY wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPWSTR lpCmdLine, int nCmdShow) { CefMainArgs main_args(hInstance); // 子进程走CEF自己的消息循环 CefExecuteProcess(main_args, nullptr, nullptr); // 主进程继续初始化 CefSettings settings; settings.multi_threaded_message_loop true; // 独立线程跑消息循环 CefInitialize(main_args, settings, nullptr, nullptr); // 启动MFC程序 // ... }CefExecuteProcess必须最先调用否则子进程会直接跑进MFC初始化界面乱套。第三步创建一个自己的CefApp继承类处理生命周期接口。第四步在MFC窗口里创建浏览器。用CefWindowInfo指定窗口句柄和初始矩形CefWindowInfo window_info; window_info.SetAsChild(hWnd, rect); CefBrowserSettings browser_settings; std::string url https://example.com; CefRefPtrCefClient client new MyCefClient(); CefBrowserHost::CreateBrowserSync(window_info, client.get(), url, browser_settings, nullptr, nullptr);第五步销毁时按顺序清理先关闭浏览器再CefShutdown顺序反了轻则内存泄漏重则直接崩溃。这套流程看起来不复杂但真正跑起来你会遇到各种细节资源文件路径不对导致启动失败子进程exe名称对不上导致无限拉起CefApp重载方法返回不正确导致V8交互失效。CEF的坑不是“能不能接入”而是“能不能稳定运行”。4.3 CEF3和MFC消息循环怎么协调这是MFC上CEF最大的坑。MFC自己有一套消息循环CEF也有自己的消息循环模型两者必须协调一致否则会出现界面卡顿、渲染不刷新、点击事件无响应。如果你的settings.multi_threaded_message_loop设为false那么CEF要求你把消息处理机会交给它BOOL CMyApp::OnIdle(LONG lCount) { CefDoMessageLoopWork(); return CWinApp::OnIdle(lCount); }这种模式下CEF的消息处理和MFC的在同一个线程里交错运行优点是简单缺点是如果MFC里某个操作阻塞了UI线程网页渲染也会跟着卡死。如果设为trueCEF会创建独立的消息循环线程渲染进程和主线程互不干扰。这时候你从MFC里调用CEF的接口要注意必须确保调用发生在UI线程上否则要用CefPostTask切到UI线程。这个模式我现在更推荐后期维护省心。还有一个常见错误是在MFC的OnTimer里频繁刷新页面或调用CEF接口。CEF的线程模型不允许你在任意线程直接操作浏览器对象记死这句话能少遭很多罪。4.4 CEF3的翻车点静态链接MFC下对话框创建失败。如果你项目设置为“在静态库中使用MFC”CEF初始化时可能因为运行库初始化顺序问题导致对话框创建失败。我发现的最快解决路径是改成“在共享DLL中使用MFC”或者用延迟加载DLL的方式规避。动态库版本匹配。libcef_dll_wrapper和libcef.dll版本必须严格匹配不要手动混搭。CefShutdown后不要访问任何CefRefPtr对象。引用计数对象内部状态已经被销毁访问会触发访问违规。子进程崩溃。如果render进程因为某些页面崩溃CEF默认会显示崩溃页面你可以通过CefRenderProcessHandler处理异常恢复。CEF3项目上线前我会专门在目标最低配置机器上做一次连续打开关闭页面的压力测试。因为这个框架的内存占用起步就是80MB以上如果页面负载高再叠加MFC自身的内存问题很容易出现内存泄漏被运维点名的情况。5. 常见问题与排查技巧实录5.1 高频故障速查表现象可能原因解决思路网页区域白屏控件在位WebView2初始化未完成 / CEF资源文件路径不对确认Environment初始化回调正常执行用GetAvailableCoreWebView2BrowserVersionString查版本提示Could not find the WebView2 Runtime目标机未装Runtime / 32位进程找64位运行时离线安装Evergreen或Fixed Version统一x64编译HTML没有铺满整个MFC窗口未响应WM_SIZE或Bounds设置时机不对封装UpdateWebViewBounds并在OnSize和初始化回调中调用MFC静态库中对话框创建失败运行库初始化顺序冲突改为共享DLL使用MFC或延迟加载CEF网页退出时出现Access Violation c0000005COM接口释放顺序错乱先销毁WebView2/CEF再CoUninitialize尤其检查WebBrowser控件CString字符串内存泄漏CString转BSTR时未释放SysAllocString使用CComBSTR或wil::unique_cotaskmem_string管理网页卡死主窗口也卡死MFC主线程被阻塞CEF未做独立消息循环开启multi_threaded_message_loop或用独立线程WebBrowser打开现代页面白屏IE文档模式过低设置FEATURE_BROWSER_EMULATION或在HTML里加meta标签指定Edge模式5.2 一个典型的排查案例有一次我同事在对话框里嵌了WebView2本地跑得好好的客户机器上怎么都是白屏。第一反应是Runtime没装一查装了后来用GetAvailableCoreWebView2BrowserVersionString查版本发现客户机器上是旧版Runtime而页面里用了一些新特性渲染不出来控制台也不报错。解决办法不是改代码是给客户补装新版Runtime或者在程序中检测到低版本时引导升级。这类问题看着是“网页显示不出来”实际上是“运行时版本太低”。排查WebView2问题时我建议第一步永远是验证运行时版本而不是查代码。版本正常再走后面流程能省一半排查时间。另一个常见问题是在MFC对话框里操作WebView2控件时偶尔会崩溃断点显示崩溃位置在系统代码里看不出原因。后来发现是初始化回调线程不是UI线程回调里直接操作了成员变量和界面线程产生了竞争。解决办法是把回调逻辑用PostMessage转到UI线程或者使用std::atomic保护状态。这类并发问题在Release模式下极其隐蔽加了日志也不一定复现最好在设计阶段就约定好WebView2回调一律不碰UI。5.3 字符串和内存管理细节MFC里的CString和WebView2的字符串类型之间频繁转换最容易踩内存泄漏。比如把CString传给WebView2时经常需要转成BSTR或LPWSTR。直接调用SysAllocString分配的内存必须手动释放用不好就是一个字节一个字节地往外漏。我习惯的做法是用CComBSTR或wil::unique_cotaskmem_string自动管理避免手写free逻辑。还有一点MFC程序退出时如果WebView2还持有网页引用而你已经释放了相关COM接口就会在关闭时偶发崩溃。稳妥的销毁顺序是先移除事件回调再关闭页面然后释放Controller最后释放Environment。顺序反了你看到的可能就是Access Violation c0000005。6. 我个人的选型建议与几个压箱底细节如果这个项目是我自己的我会按这个逻辑拍板页面自己写、功能简单直接WebBrowser图个零依赖要上现代前端技术栈、目标机Win10以上无脑WebView2客户明确要求不支持额外运行时、要锁定Chromium版本或者有深度定制需求再碰CEF3。我自己的MFC老项目里目前最满意的一套组合是WebView2加固定版本Runtime离线分发。安装包多带一个离线安装器静默安装完Runtime再启动主程序用户基本无感知页面效果和现代前端完全一致维护成本比CEF3低一个量级。每次我听到有同事说“MFC嵌入网页很痛苦”多半是选型阶段图省事选了WebBrowser结果被前端新特性卡死。工具本身没有绝对好坏关键是需求匹配。最后分享一个细节不管用哪条路线都不要在对话框模板里搞一个固定大小的网页区域就完事。网页内容是会变的分辨率是会变的用户拉伸窗口也是必然的。封装好一个“网页区域自适应”的函数并且在所有窗口尺寸变化路径上调用这件事应该在第一天就做。等你上线后给客户演示客户拉一下窗口网页区域跟着走得顺滑你对方案的信心也会完全不一样。