preface
The code address
JSBox is an APP created by Zhong Da that can be written in JavaScript. It provides a set of interface solutions, and then it also provides basically all the native capabilities. It’s sort of a simplified version of a small program. It also implements a simple code editor, so you can write code directly on the APP. If you are interested, I strongly suggest you go to the next JSBox to support the big one.
When I love an APP, I usually try to implement its functions. We tried to copy Cosmos (see little star ^_^). Recently, it took some time to implement the basic display function and basic code editing function of JSBox. There are also some problems in the process. There are two parts: one on the engine and the other on the code editor.
JavaScriptCore is introduced
The entire engine is built on JavaScriptCore, which is briefly introduced here. JavaScriptCore provides the ability to interact with JS and native. You can execute a piece of JAVASCRIPT code without using the browser, or you can inject a native object directly into js. One thing to notice is that JavaScriptCore doesn’t have a Dom Window or anything like that.
JSValue
JSValue is an object in the JS environment. It could be any type, it could be an array, it could be a string or it could be a JS method. Javascript and native data transfer have a set of basic cast tables. JavaScriptCore will do a basic layer of transformation for us.
Objective-C type | JavaScript type
--------------------+---------------------
nil | undefined
NSNull | null
NSString | string
NSNumber | number, boolean
NSDictionary | Object object
NSArray | Array object
NSDate | Date object
NSBlock (1) | Function object (1)
id (2) | Wrapper object (2)
Class (3) | Constructor object (3)
Copy the code
JSValue also has toXXX methods to convert JS data to native data.
JSContext
We’ll use JSContext a lot. Here’s a simple way to use it:
Use a
JSContext *context = [[JSContext alloc] init];
[context evaluateScript:@"var a = 'hello word';"];
NSLog(@"% @",[context[@"a"] toString]);
Copy the code
In this code, I declare an a variable in the JS environment, and then I get the object through the context and I print out the value of the object. This variable here can also be a JS method. If it is a method, call it directly with callWithArguments:.
Use two
native:
context[@"log"] = ^(JSValue *value) {
NSLog(@"% @",[value toString]);
};
Copy the code
js:
log('hello word');
Copy the code
In this code we first inject a log method into js that takes one argument. And then you can call this method in JS directly by the name log. The implementation of the method is the code logic in the block.
Use three
We can take the method defined in JS directly and call it directly in native. js
var sum = function(a,b) {
return a + b
}
Copy the code
native
[context[@"sum"] callWithArguments:@[@(1),@(2)]];
Copy the code
JSExport
Using JSExport we can pass native objects directly to JS. Js can call an object’s method directly from a property. The specific usage is as follows
@protocol studentExport <JSExport>
@property (nonatomic, strong) NSString *name;
@property (nonatomic, assign) NSInteger age;
- (void)study;
@end
@interface student : NSObject <studentExport>
@property (nonatomic, strong) NSString *name;
@property (nonatomic, assign) NSInteger age;
- (void)study;
@end
Copy the code
We created a protocol inherited from JSExport that implements two properties and a method. And then the object that we create inherits from that protocol and implements the methods and properties in that protocol. In this way, if we create a student object and pass it to JS, JS can get the name age property directly and call the study method directly. It’s amazing
JSBox basic usage introduction
As you go along, if you have any questions about something you can look at the JSBox documentation
$ui.render({
props: {
id: "label",
title: "Hello, World!"
},
views: [
{
type: "label",
porps: {
text : 'hello word'
}
layout: function(make, view) {
make.center.equalTo(view.super)
make.size.equalTo($size(100, 100))}}]})Copy the code
All this code does is pop up a controller and add a label to it. We passed in a JS object. Well, first of all, this object has a props property, which looks like some properties of the current controller, like title, which definitely sets the title. And then we have an array of views, which is obviously a data structure for views. Inside is a type property that corresponds to the type of the current view, and layout corresponds to the layout of the current view. Porps hold the properties of the view.
JSBox basic function implementation details
With the use of JavaScriptCore mentioned above, it’s easy to infer that JSContext must define a method that corresponds to the $ui.render method call. We pass in a JS object, and we have done a rough analysis of the data structure above. The next step is to analyze how to parse the object and display it. Jsvalue [@’ XXX ‘] can be passed to the native jsvalue object, we can get the specific data by jsvalue[@’ XXX ‘], the data can be js method or some basic data.
Control creation
Combined with the above analysis, control creation is to get the views parameter in jsValue and then parse out the views array of each view, through the view type attribute and the document of the native control to create a control one by one.
Control properties
Attribute assignment
The assignment is relatively easy to understand and if the name of the property corresponds to the name of the property that we want to assign to in the native world we just set it with KVC and it’s ok. If the name doesn’t match the property we just add a property with the current name in the category, and do the correct parameters in the set method and that’s ok.
Obtaining properties
To do this, we need to use JSExport. We need to add the properties that support fetching to a custom protocol that inherits from JSExport, and then create a category that inherits from that protocol. So when we pass the native object to JS, the JS side gets the properties.
@protocol ZHNJSBoxUILabelExport <JSExport>
@property (nonatomic, copy) NSString *text;
@property (nonatomic, strong) UIFont *font;
@property (nonatomic, strong) UIColor *textColor;
@property (nonatomic, strong) UIColor *shadowColor;
@property (nonatomic, assign) NSInteger align;
@property (nonatomic, assign) NSInteger lines;
@property (nonatomic, assign) BOOL autoFontSize;
@end
@interface UILabel (ZHNJSBoxUILabel) <ZHNJSBoxUILabelExport>
@property (nonatomic, assign) NSInteger align;
@property (nonatomic, assign) NSInteger lines;
@property (nonatomic, assign) BOOL autoFontSize;
@end
Copy the code
Control position
layout: function(make, view) {
make.center.equalTo(view.super)
make.size.equalTo($size(100, 100))}Copy the code
As iOS developers, we can see it at a glance. This is the way to write it. If we want to make. Center.equalto (view.super) to be called without error, we need to have a center in make and an equalTo property in center. There are two ways to do this
Methods a
Use JSExport to add the required properties to MASConstraintMaker and methods to MASConstraint. One point that needs to be noted is that equalTo method is used in JSBox. However, for the basic data types like height,width and so on in selector, mas_equalTo is required to package the value passed, so special processing is required. The parameter passed in by equalTo also needs a layer of conversion. The original one uses mas_xxx, while JSBox simply canceles mas_ for simplicity.
Way 2
The way a train of thought is clear, the code implementation is not what big difficulty. However, I finally found that it needed to change the details of the caching library. So I tried to see if there was any other way to do it, and finally I tried to use the JSPatch implementation method by matching the regular pattern and making the attributes go through a uniform method, and the methods go through a uniform method. Make.center.equalto (view.super) finalizes with the structure make.__lp(‘center’).__lr(‘equalTo’)(‘view.super’). We can use [make left] instead of [make left] for the navigation for which we will retrieve the information. We first add __lp and __lr methods to the js base class. Attributes are a unified method of the property name is passed to the native, native directly with [maker performSelector: NSSelectorFromString (property)] way calls with respect to ok. The methods are slightly different, on the JS side we need to synthesize __lr(‘equalTo’)(‘view.super’) into a method call.
var args = Array.prototype.slice.call(arguments);
return oc_LayoutRelation(slf,methodName,args[0]);
Copy the code
Get the method name and parameters and pass them to the native call. Native to use [maker performSelector: NSSelectorFromString (seletName)] get a block, and then in the direct call block return parameter with respect to ok.
Control
As mentioned earlier, JS can pass a method directly to native. Native takes this method directly and callWithArguments: it is directly ok. In other words, we just need to save this JS method. It’s ok to call this JS method in the native method logic. JavaScriptCore has its own memory management mechanism, and native has its own memory management mechanism. If we just set the jsValue that we passed in as a property, then when the JS side tries to free the JS object, it will find that its memory is natively managed, so there’s no permission to free it and it will just crash. Scrolling through the documentation, there is an object called JSManagedValue that is a weak reference to the internal JsValue, which seems to solve the reference problem. It has no effect on the life cycle of the JS object, which means we can’t get this method on native after the JS object is released.
When I was at a loss, I looked at the implementation of JSPatch. JSPatch uses a global dictionary for storage. JSPAtch’s JSContext is a singleton, which means that the release of its JSValue is bound to the entire app lifecycle. So there is no question of saying that. But our JSContext is obviously for each script, so it’s not quite the same. I was stuck for several days without finding the method. Then I tried to look at the code of WEEX. The whole project was a little large, but I didn’t look very carefully. Using the JSPatch code, IT occurred to me that when parsing into a JS method, I could add such a method attribute to the JS base class object. And then when you need to use it you just get this method by name. So the lifecycle of this method is bound to the JSContext, and when it’s freed then these methods are freed. Of course, it’s possible to do a global dictionary in JSContext.
Ideas spread
From what I can see now, the implementation of a similar framework foundation is similar. Weex mini programs like this just add a layer of compilation to this, you can write front-end code directly, and they will eventually compile that front-end code into JS code. If you have some dynamic needs, but you don’t want to introduce a heavy framework like WEEX, you can actually try to implement your own dynamic framework.
conclusion
The above outline introduces some basic implementation ideas and some problems. This article focuses on the basic engine of JSBox, which I’ve modeled at about 1/100. Below may also write an article to analyze how to implement a simple code editor, please look forward to!!