深入小程式系列之一:小程式核心原理及模擬

linkstar發表於2020-04-15

什麼是小程式

 

小程式是一種新的移動應用程式格式,是一種依賴 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 更好。

 

 

1585624519742918.png

小程式雙執行緒模型模擬

 

先看一下執行結果

1585624621618660.gif

 

 

接下來我們將用 iOS 程式碼來模擬上述的雙執行緒模型。首先我們來實現檢視層與邏輯層的資料通訊

1585624664431756.png

如上圖所示,檢視層與邏輯層都分別通過 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)
})

 

 

 

文件中心: Document

 

關於凡泰極客:幫助金融機構乃至任何希望擁有類似技術的其他行業機構,建立“碎片”的集散地、降低管理成本、提高研發效能,形成自己的數字化生態、與客戶和夥伴建立真正的數字化連線。

 

相關文章