As one of the maintainers of the bottom API of wechat small program, I have experienced ups and downs and all kinds of ridicule. In order to let you can better write a small program, specially comb an article introduction. If you have any fun place, welcome to developers.weixin.qq.com/ developer community.

PS: The boss wants to find someone, for their own strength of the front er, you can directly send resume to my email: [email protected]

Describe the communication system of applets

In order to better develop some high quality, high performance small program, here with you to understand the small program on different end of the architecture system distinction, better let you understand small program some unique code writing way.

The whole applets development ecosystem can be divided into two parts:

  • Wechat Developer Tools for Desktop NWJS (PC)
  • The official running environment of the mobile APP

The initial consideration was to use a two-threaded model to solve security and controllability problems. However, as the development complexity increases, the original two-thread communication time becomes unacceptable for some high-performance small programs. That is, every time the UI is updated, the API is manually invoked by the WebView. For the original infrastructure, please refer to the official image:

However, the above image is somewhat misleading, because webView rendering is actually done by the kernel on the mobile side, and webView is just a DOM/BOM interface exposed by the kernel. So, a performance breakthrough here is, can JSCore access the kernel interface directly through the Native layer? The answer is yes, so the above diagram can be easily divided into the correlation, and the new one looks like this:

In simple terms, the kernel changes and then selects a portion of the standard WebView interface for JsCore calls. One limitation, however, is that Android is relatively free. V8 provides plugin mechanisms that allow this, whereas IOS doesn’t allow this unless you are using IOS native components, in which case the same layer rendering logic is involved. In fact, their underlying content is the same.

In order to better understand the specific process of small program development, mobile phone debugging and debugging in the developer tool roughly distinguish, let’s analyze the two respective execution logic.

tl; dr

  • Developer tools communication system (two-way communication only) that is, all instructions are through appService <=> NWJS middle layer <=> WebView
  • Communication system operated by Native terminal:
    • Applets basic communication: bidirectional communication — (Core <=> webView <=> Intermedia <=> AppService)
    • High-order component communication: unidirectional communication system (AppService <= Android /Swift => core)
  • JSCore executes the logic of appService

Communication mode for developer tools

The two-thread model is used initially for security and control reasons. Simply put, all your JS execution is done in JSCore, whether it is bound events, attributes, DOM operations, etc.

Developer tools, mainly run on the PC side, use NWJS internally, but for a better understanding, here, directly according to the general technology of NWJS. The architecture used by the developer tools is to manage a webviewPool based on NWJS, through which appService_webView and content_webView are implemented.

So some of the performance difficulties in applets are not a big problem in developer tools. For example, divs cannot be placed on canvas elements, and custom controls cannot be set on video elements. The whole architecture is shown as follows:

When you open the developer tools, the first thing you see is the Console content in appService_webView.

The content_webView doesn’t need to be exposed to the outside world, because the underlying library of the applet executing inside has little to do with the actual code the developer is writing. For your understanding, you can just assume that the WXML displayed is content_webView.

When you perform the logic on the actual preview page, you pass the triggered signaling event to the Service_webView via content_webView. Because it’s two-threaded communication, anything that involves DOM event handling or other data communication is asynchronous, which is really important when you’re writing code.

If at the time of development, what kind of difficulties, welcome contact: developers zone | WeChat open community

IOS/Android protocol analysis

Before a brief understanding of the developer tools, applets simulation architecture. Actually running on a mobile phone, the architecture inside may be different. The main reasons are:

  • IOS and Android have different rendering logic for WebViews
  • Performance bottlenecks on mobile phones, JS raw is not suitable for high performance computing
  • Special elements such as video cannot be overwritten by other divs

The two-threaded architecture of the initial applet is similar to that of the developer tools, with content_webView controlling page rendering and AppService executing on the phone using JSCore. The default schema is actually this:

However, as the number of users increases, so does the expectation of small programs:

  • Is the performance of applets eaten by the dog?
  • Can the small program open faster?
  • Why is the package size of applets so small?

We all know that, so it’s all slowly being optimized bit by bit. Considering the poor rendering performance of native WebViews, Rex proposed using the same layer rendering to solve the performance problem. This method not only eliminates the ability to overwrite other elements in the video, but also improves the performance of component rendering.

During the specific development on mobile phones, developers need to pay attention to the essential differences between the communication architecture of some high-level components, such as video and Canvas, and the above dual-thread communication. For performance purposes, native components are used for rendering underneath. The communication cost here actually comes back to the communication between native and AppService.

In order to better understand the relationship between AppService and Native, here is a brief introduction of JSCore implementation methods.

JSCore is easy to understand

On both IOS and Android, JSCore is provided for the purpose of running JS code independently, and it also provides an interface for JSCore and Native communication. This means that the daily change of Native logic code can be well realized by tuning a JSCore through Native, without excessively relying on the release version to solve the corresponding problems. In fact, if it is not particularly rigorous, it can be directly said as a “hot update” mechanism.

JSCore is provided on both Android and IOS platforms, and the engineering library running in the domestic environment is as follows:

  • Anroid: The domestic platform is relatively divided, but because it uses Google’s Android platform, most of them are based on The Chromium kernel, plus the middle layer to achieve. V8 JSCore is commonly used within Tencent.
  • IOS: on the IOS platform, because it is a whole ecological closed source, it can only be executed based on the webKit engine embedded in the system, and webKit-javascriptCore is provided to complete it.

Here we mainly with the official documentation of webKit -JavaScriptCore to explain.

JSCore Core foundation

JSCore execution architecture in general can be divided into three parts: JSVirtualMachine, JSContext and JSValue. These three make up the execution content of JSCore. Specific explanations are as follows:

  • JSVirtualMachine: It executes JS code by instantiating a VM environment. If you have more than one JS to execute, you need to instantiate multiple VMS. It is also important to note that these VMS cannot interact with each other because of GC problems.
  • JSContext: JSContext is the context object in which js code is executed. It is equivalent to a Window object in a WebView. Within the same VM, you can pass different contexts.
  • JSValue: Similar to WASM, JSValue is designed to map between JS data types and Swift data types. That is, anything mounted to jsContext is of type JSValue, and Swift automatically converts to and from JS internally.

For an overview, please refer to this architecture diagram:

Of course, in addition to the normal implementation logic of the above three architectures, there are also class architectures that provide interface protocols.

  • JSExport: a protocol used to expose native interfaces in JSCore. In simple terms, it will directly convert native properties and methods into prototype Object methods and properties.

Simply execute the JS script

JSCore allows you to execute JS code in a context. First you need to import JSCore:

Import JavaScriptCore // remember to import JavaScriptCoreCopy the code

It then executes using the evaluateScript method mounted by the Context, passing strings like new Function(XXX).

letContet :JSContext = JSContext() // instantiate JSContext context.evaluatescript ("function combine(firstName, lastName) { return firstName + lastName; }")

let name = context.evaluateScript("combine('villain', 'hr')")
print(name) //villainhr // Get the method defined in JS in swiftlet combine = context.objectForKeyedSubscript("combine") // Pass the argument to call: // becausefunctionThe argument passed in is simply an arguemnts[fake Array] that needs to be written as an Array in Swiftlet name2 = combine.callWithArguments(["jimmy"."tian"]).toString() 
print(name2)  // jimmytian
Copy the code

If you want to execute a locally typed JS file, you need to parse the JS file’s path in Swift and convert it to a String object. You can convert files directly using the system interfaces provided by Swift, bundles, and strings.

lazy var context: JSContext? = {
  let context = JSContext()
  
  // 1
  guard let
    commonJSPath = Bundle.main.path(forResource: "common", ofType: "js") else{// Use the Bundle to load the contents of the local JS fileprint("Unable to read resource files.")
      return nil
  }
  
  // 2
  do {
    letCommon = try String(contentsOfFile: commonJSPath, encoding: string.encoding.utf8) // Read file _ = context? .evaluatescript (common) // Use evaluate to execute JS files directly} catch (let error) {
    print("Error while processing script file: \(error)")}return context
}()
Copy the code

Exposure of the JSExport interface

JSExport is a protocol used to expose native interface in JSCore, which enables JS code to directly call native interface. In simple terms, it will directly convert native properties and methods into prototype Object methods and properties.

So how do you execute Swift code in JS code? The simplest way is to use JSExport directly to implement class passing. The class generated by JSExport is actually passing a global variable in JSContext (the variable name is the same as defined by Swift). This global variable is actually a prototype. And swift is just through context, right? .setobject (XXX) API to import a global Object interface Object into JSContext.

So how do you use the JSExport protocol?

First define the protocol that needs to be exported. For example, here we directly define a sharing protocol interface:

@objc protocol WXShareProtocol: JSExport {func wxShare(callback)->Void) //setShareInfo func wxSetShareMsg(dict: [String: AnyObject]) // Call alert content func showAlert(title: String, MSG :String)}Copy the code

Methods defined in protocol are public and need to be exposed to JS code for direct use. Methods not declared in Protocol are private. Next, let’s define the implementation of a specific WXShareInface:

@objc class WXShareInterface: NSObject, WXShareProtocol { weak var controller: UIViewController? weak var jsContext: JSContext? Var shareObj:[String:AnyObject] func wxShare(_ succ:)->{}) { // Successfully shared callback succ()} funcsetShareMsg(dict:[String:AnyObject]){
        self.shareObj = ["name":dict.name,"msg":dict.msg]
        // ...
    }

    func showAlert(title: String, message: String) {
        
        letalert = AlertController(title: title, message: message, preferredStyle: Alert. AddAction (AlertAction(title:"Sure", style:.default, handler: nil)) // Pop-up message self.controller? .presentViewController(alert, animated:true// When user content changes, the userInfoChange method in JS is triggered. // This method is private in Swift and will not be reserved for JSExport func userChange(userInfo:[String:AnyObject]) {letjsHandlerFunc = self.jsContext? .objectForKeyedSubscript("\(userInfoChange)")
        let dict = ["name": userInfo.name, "age": userInfo.age] jsHandlerFunc? .callWithArguments([dict]) } }Copy the code

The class is already defined, but we need to bind the current class to JSContext. This is done by converting the current Class to type Object and injecting it into JSContext.

lazy var context: JSContext? = {

  let context = JSContext()
  let shareModel = WXShareInterface()

  do{// Inject the WXShare Class object, then in JSContext you can call the swift object context directly through window.WXShare? .setObject(shareModel,forKeyedSubscript: "WXShare" as (NSCopying & NSObjectProtocol)!)
  } catch (let error) {
    print("Error while processing script file: \(error)")}return context
}()
Copy the code

This completes the step of injecting the Swift class into the JSContext; all that remains is the call. The main consideration here is where your JS is executing. For example, you can execute JS directly with JSCore, or bind JSContext directly to webView Context.

To directly execute JS locally, we need to load the local JS file first, and then execute. There is now a share.js file locally:

// share.js file wxshare.setsharemsg ({name:"villainhr",
    msg:"Learn how to interact with JS in swift"
});

WXShare.wxShare(()=>{
    console.log("the sharing action has done");
})
Copy the code

Then, we need to load it as before and execute:

// swift native code // swift funcinit(){
    guard 
    let shareJSPath = Bundle.main.path(forResource:"common",ofType:"js") else{
        return
    }
    
    do{// Load the current shareJS and parse it using JSCoreletshareJS = try String(contentsOfFile: shareJSPath, encoding: String.Encoding.utf8) self.context? .evaluateScript(shareJS) } catch(let error){
        print(error)
    }
    
}
Copy the code

If you want to bind the current WXShareInterface directly to the Webview Context, the previous instance Context needs to be changed directly to the Webview Context. UIWebview can get the current WebView Context directly, but WKWebview doesn’t have an interface to get the Context directly, Wkwebview prefers to use scriptMessageHandler to do jSBridge. Of course, there is a way to get the wKWebView context, you can use KVO trick to get it.

Func webViewDidFinishLoad(webView: func webViewDidFinishLoad) UIWebView) {// Load the current View's JSContext self.jsContext = webView.valueForkeypath ("documentView.webView.mainFrame.javaScriptContext") as! JSContext
    letModel = WXShareInterface() model.controller = self model.jsContext = self.jsContext Binding self. JsContext. SetObject (model,forKeyedSubscript: "WXShare"// Open the remote URL page // guardlet url = URL(string: "https://www.villainhr.com") else {
       // return// if the remote URL is not loaded, you can load // directlyletRequest = URLRequest(url: url) // webview.load (request) // Parse js code in HTML directly in jsContext //let url = NSBundle.mainBundle().URLForResource("demo", withExtension: "html") // self.jsContext.evaluateScript(try? String(contentsOfURL: url! Encoding: NSUTF8StringEncoding)) / / to monitor the current jsContext abnormal self. JsContext. ExceptionHandler = {(context, exception)in
        print("exception:", exception)
    }
}
Copy the code

We can then call native’s interface directly from share.js above.

Communication of native components

JSCore is actually executed in a native thread without DOM, BOM and other interfaces. Its execution is similar to the nodeJS environment. In a nutshell, it is an ECMAJavaScript parser that does not involve any environment.

In JSCore, communication with a native component is actually communication between two threads in native. For some high performance components, this communication delay has been reduced considerably.

So what is the communication between these two?

It’s events, DOM operations, things like that. In the same layer rendering, this information is actually managed by the kernel. So, the communication architecture here actually becomes:

In Native Layer, the proxy can be set in the kernel through some means, which can capture the events triggered by users on the UI interface well. Because of the deep Native knowledge involved here, I will not introduce more. Simply put, some touch events of the user can trigger corresponding events in the Native Layer directly through the interface exposed by the kernel. Here, we can roughly understand the relationship between the kernel and Native Layer, but what is the relationship between the actual rendered WebView and the kernel?

In the actual rendered WebView, the contents are actually the basic library JS and HTML/CSS files of the applet. By executing these files, the kernel internally maintains a render tree that corresponds to the HTML content in the WebView. As mentioned above, the Native Layer can also interact with the kernel, but there will be a thread unsafe phenomenon, two threads operating a kernel at the same time, it is likely to cause leakage. Therefore, the Native Layer also has some limitations, that is, it cannot directly operate the rendering tree of the page, and can only replace node types on the existing rendering tree.

The final summary

The main purpose of this article is to give you a better understanding of the applets architecture mode in the developer tools and mobile terminal difference, better develop some high performance, quality applets applications. That’s what the Small Program center has been doing. Finally, to summarize some important points from the previous section:

  • The developer tool has only two threads architecture, through the communication of appService_webview and content_webview, to achieve the simulation of small program mobile terminal.
  • On the mobile end, different communication architectures can not be optimized according to component performance requirements.
    • Normal div rendering, dual thread communication with JSCore and WebView
    • Higher-order components, such as video/map/ Canvas, usually use the kernel’s interface to achieve same-layer rendering. The communication mode is directly simplified to the kernel <=> Native <=> AppService. (Fast speed)

Due to my busy work, there are few meetings in the community. Here I recommend you to follow my wechat public account “Front-end Little Jimmy”, which will be updated in time

Reference:

Tutorial | “little application development guide”