深入小程序系列之一:小程序核心原理及模擬
- 2020 年 4 月 20 日
- 筆記
什麼是小程序
小程序是一種新的移動應用程序格式,是一種依賴 Web 技術,但也集成了原生應用程序功能的混合解決方案。
目前市面上小程序平台微信、支付寶、百度、頭條、京東、凡泰等;小程序一些特性有助於填補 Web 和原生平台之間的鴻溝,因此小程序受到了一些超級應用程序的歡迎。
-
它不需要安裝,支持熱更新。
-
具備多個 Web 視圖以提高性能。
-
它提供了一些通過原生路徑訪問操作系統功能(原生接口)或數據的機制。
-
它的內容通常更值得信賴,因為應用程序需要由平台驗證。
-
小程序可以分發到多個小程序平台(Web、原生應用,甚至是 OS)。這些平台還為小程序提供了入口,幫助用戶輕鬆找到所需的應用。
小程序核心功能
1、分離視圖層與邏輯層
在小程序中,視圖層通常與邏輯層分離。
-
視圖層 View 負責渲染小程序頁面,包括 Web 組件和原生組件渲染,可以將其視為混合渲染。例如,Web 組件渲染可以由 WebView 處理,但 WebView 不支持某些 Web 組件渲染,或者是性能受限;小程序還依賴於某些原生組件,例如地圖、視頻等。
-
邏輯層 Service 是用主要用於執行小程序的 JS 邏輯。主要負責小程序的事件處理、API 調用和生命周期管理。擴展的原生功能通常來自宿主原生應用程序或操作系統,這些功能包括拍照、位置、藍牙、網絡狀態、文件處理、掃描、電話等。它們通過某些 API 調用。當小程序調用原生 API 時,它會將 API 調用傳遞給擴展的原生功能,以便通過 JSBridge 進一步處理,並通過 JSBridge 從擴展的原生功能獲取結果。Service 為每個 Render 建立連接,傳輸需要渲染的數據以進一步處理。
-
如果事件由小程序頁面中的組件觸發,則此頁面將向 Service 發送事件以進一步處理。同時,頁面將等待 Service 發送的數據來重新渲染小程序頁面。
-
渲染過程可被視為無狀態,並且所有狀態都將存儲在 Service 中。
視圖層和邏輯層分離有很多好處:
-
方便多個小程序頁面之間的數據共享和交互。
-
在小程序的生命周期中具有相同的上下文可以為具備原生應用程序開發背景的開發人員提供熟悉的編碼體驗。
-
Service 和 View 的分離和並行實現可以防止 JS 執行影響或減慢頁面渲染,這有助於提高渲染性能。
-
因為 JS 在 Service 層執行,所以 JS 裏面操作的 DOM 將不會對 View 層產生影響,所以小程序是不能操作 DOM 結構的,這也就使得小程序的性能比傳統的 H5 更好。
小程序雙線程模型模擬
先看一下運行結果
接下來我們將用 iOS 代碼來模擬上述的雙線程模型。首先我們來實現視圖層與邏輯層的數據通訊
如上圖所示,視圖層與邏輯層都分別通過 JS Bridge 的 publish 和 subscribe 來實現數據的收發。
模擬實現
1、視圖層調用JSBridge.publish把事件傳遞給原生;參數: {eventName: 『』, data: {}}
//點擊按鈕,通知JS執行業務邏輯
function onTest() {
console.log('aaa')
FinChatJSBridge.subscribe('PAGE_EVENT', function (params) {
document.getElementById('testId').innerHTML = params.data.title })
FinChatJSBridge.publish('PAGE_EVENT', {
eventName: 'onTest',data: {}
})
}
2、原生 view 層收到 page 的事件,把事件傳遞轉發給 service 層處理
if ([message.name isEqualToString:@"publishHandler"]) {
NSString *e = message.body[@"event"];
[self.service callSubscribeHandlerWithEvent:e param:message.body[@"paramsString"]];
}
3、原生 service 層收到原生 view 層的事件,通過 jsbridge 把事件及參數傳遞給視圖 ervice 層執行 js 邏輯
NSString *js = [NSString stringWithFormat:@"ServiceJSBridge.subscribeHandler('%@',%@)",eventName,jsonParam];
[self evaluateJavaScript:js completionHandler:nil];
4、視圖 service,收到事件後,執行 JS 業務代碼
var Page = {
setData: function(data) {
//向原生視圖層發送更新數據信息
ServiceJSBridge.publish('PAGE_EVENT', {
eventName: 'onPageDataChange',
data: data
})
},
methods: {
onTest: function() {
// 執行JS方法,模擬小程序的setData,把數據更新到視圖層
Page.setData({
title: '我來自JS代碼更新'
})
console.log('my on Test')
}
}
}
var onWebviewEvent = function(fn) {
ServiceJSBridge.subscribe('PAGE_EVENT', function(params) {
console.log('FinChatJSBridge.subscribe')
var data = params.data,
eventName = params.eventName
fn({
data: data,
eventName: eventName
})
})
}
var doWebviewEvent = function(pEvent, params) {
// do dom ready
if (Page.methods.hasOwnProperty(pEvent)) {
// 收到視圖層的事件,執行JS對應的方法
Page.methods[pEvent].call(params)
}
}
5、執行業務 JS 代碼後,把數據更新傳遞給視圖層去更新 UI 界面展示數據
ServiceJSBridge.publish('PAGE_EVENT',{
eventName:'onPageDataChange',
data: data
})
6、原生 service 層收到視圖 service 層的事件,把事件傳遞給原生視圖層
if ([message.name isEqualToString:@"publishHandler"]) {
NSString *e = message.body[@"event"];
[self.controller callSubscribeHandlerWithEvent:e param:message.body[@"paramsString"]]; }
7、原生視圖層把收到的事件,傳遞給視圖 view 層
NSString *js = [NSString stringWithFormat:@"FinChatJSBridge.subscribeHandler('%@',%@)",eventName,jsonParam];
[self evaluateJavaScript:js completionHandler:nil];
8、視圖 view 層,收到事件後,更新界面
FinChatJSBridge.subscribe('PAGE_EVENT',function(params){
document.getElementById('testId').innerHTML = params.data.title
})
訂閱數據回調
訂閱數據回調
// 首先訂閱數據回調
JSBridge.subscribe('PAGE_EVENT', function(params) {
// ... 這裡對返回的數據進行處理
})
// 向JS Bridge發佈數據
// eventName: 用於標識事件名
// data: 為傳遞的數據
JSBridge.publish('PAGE_EVENT', { eventName: 'onTest', data: {} })
WKWebView 初始化
WKWebView 初始化
WKUserContentController *userContentController = [WKUserContentController new];
NSString *souce = @"window.__fcjs_environment='miniprogram'";
WKUserScript *script = [[WKUserScript alloc] initWithSource:souce injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:true];
[userContentController addUserScript:script];
[userContentController addScriptMessageHandler:self name:@"publishHandler"];
WKWebViewConfiguration *wkWebViewConfiguration = [WKWebViewConfiguration new];
wkWebViewConfiguration.allowsInlineMediaPlayback = YES;
wkWebViewConfiguration.userContentController = userContentController;
if (@available(iOS 9.0, *)) {
[wkWebViewConfiguration.preferences setValue:@(true) forKey:@"allowFileAccessFromFileURLs"];
}
WKPreferences *preferences = [WKPreferences new];
preferences.javaScriptCanOpenWindowsAutomatically = YES;
wkWebViewConfiguration.preferences = preferences;
self.webView = [[WKWebView alloc] initWithFrame:self.view.bounds configuration:wkWebViewConfiguration];
self.webView.clipsToBounds = YES;
self.webView.allowsBackForwardNavigationGestures = YES;
[self.view addSubview:self.webView];
NSString *urlStr = [[NSBundle mainBundle] pathForResource:@"view.html" ofType:nil];
NSURL *fileURL = [NSURL fileURLWithPath:urlStr];
[self.webView loadFileURL:fileURL allowingReadAccessToURL:fileURL];
WKWebView 事件回調處理
// 執行視圖層事件回調
- (void)callSubscribeHandlerWithEvent:(NSString *)eventName param:(NSString *)jsonParam
{
NSString *js = [NSString stringWithFormat:@"FinChatJSBridge.subscribeHandler('%@',%@)",eventName,jsonParam];
[self evaluateJavaScript:js completionHandler:nil];
}
- (void)evaluateJavaScript:(NSString *)javaScriptString completionHandler:(void(^)(id result,NSError *error))completionHandle
{
[self.webView evaluateJavaScript:javaScriptString completionHandler:completionHandler];
}
#pragma mark - WKScriptMessageHandle
// 視圖層JSBridge請求接收處理
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message
{
if ([message.name isEqualToString:@"publishHandler"]) {
NSString *e = message.body[@"event"];
[self.service callSubscribeHandlerWithEvent:e param:message.body[@"paramsString"]];
}
}
視圖層代碼
function onTest() {
console.log('aaa')
FinChatJSBridge.subscribe('PAGE_EVENT', function(params) {
document.getElementById('testId').innerHTML = params.data.title
})
FinChatJSBridge.publish('PAGE_EVENT', {
eventName: 'onTest',
data: {}
})
}
<div id="testId">我來自視圖層!</div>
<input type="button" value="調用JS邏輯層setData" style="border-radius:15px;background:#ed0c50;border: #EDD70C;color: white;font-size: 14px; width: 80%;" onclick="onTest();" />
邏輯層代碼
// page 對像模擬
var Page = {
setData: function(data) {
ServiceJSBridge.publish('PAGE_EVENT', {
eventName: 'onPageDataChange',
data: data
})
},
methods: {
onTest: function() {
Page.setData({
title: '我來自JS代碼更新'
})
console.log('my on Test')
}
}
}
var onWebviewEvent = function(fn) {
ServiceJSBridge.subscribe('PAGE_EVENT', function(params) {
var data = params.data,
eventName = params.eventName
fn({
data: data,
eventName: eventName
})
})
}
var doWebviewEvent = function(pEvent, params) {
// do dom ready
if (Page.methods.hasOwnProperty(pEvent)) {
Page.methods[pEvent].call(params)
}
}
onWebviewEvent(function(params) {
var eventName = params.eventName
var data = params.data
return doWebviewEvent(eventName, data)
})