Product Manager: "click here, I want to jump to any page I want to jump" -- decoupling efficiency artifact "unified jump routing"

Posted by rlafountain on Mon, 28 Feb 2022 07:48:33 +0100


1. Background

We know that the front-end domain locates the page according to the origin. To jump to the corresponding page, you only need to access the corresponding route, which is very convenient. However, there has been no concept of routing in iOS. To jump to a page, you need to create an instance of the target page, and then jump through the navigation controller, which is very cumbersome. There is such a requirement: when you click the push message (or click an area), you need to jump to any possible page. At first glance, I am resisting, but this demand seems reasonable. As a group of aspiring developers, we officially launched the 100 bottle unified hop routing project.

Unified hop: that is, jump to any page through the open() method of the unified hop routing SDK (the implementation method of the target page supports but is not limited to Native, fluent, HTML5, wechat applet, system application and other third-party applications).

2. What problems should be solved

As usual, when starting a project, we first sort out the pain points, sort out the problems to be solved, and then determine the needs.

2.1 page Jump

Page Jump only needs to provide the destination page route to the unified hop routing SDK. The unified hop routing SDK can jump to the destination page accurately, instead of creating a page instance and jumping through the navigation controller in the traditional way.

Reference carrying

When page Jump, you need to carry some parameters to the target page instance for the correct processing logic of the target page.

2.3 return value

Sometimes we need to return some values we are interested in when the target page is closed. For example: select the receiving address page. After selecting the address, you need to send the newly selected address information back to the previous page.

2.4 fallback to the specified route

Sometimes we need to go back to the specified page. Take publishing A short video as an example: first enter the video capture page (A) from the home page (M), enter the video editing page (B) after shooting, and enter the publishing page (C) after editing. After publishing the video, you should go back to the page (M) before entering the capture page (A) instead of the video editing page (B).

2.5 unified routing rules

Basic idea:

  • A route corresponds to a page
  • The route should be a meaningful string
  • Routing rules shall apply to each end
  • Each end shall bind the route to the page

Scheme:

From the above ideas, it is easy to think that our routing specification can follow the front-end routing specification, which is in line with Restful's requirements URI.

Namely: URI = scheme:[//authority]path[?query][#fragment]

See: RFC3986

We define corresponding scheme s for routing in different fields, i.e. Native: native, fluent: fluent, http/https: http/https, applet: wxmp, third-party application: tp(third party), etc.

2.6 cross module API call

Sometimes we need to access the methods provided by other modules in the same way as launching a GET request to WebServer. In the traditional mode, cross module method intermodulation needs to refer to the target module, which is very cumbersome, and sometimes circular reference occurs.

2.7 behavioral routing

Sometimes we need to configure click behavior (non page Jump Behavior) for an area of the page. For example, an HTML5 page needs to invoke Native sharing behavior when clicking on an area on the page.

2.8 route interceptor

Sometimes we need to authenticate the route, reorganize the parameters, break points, redirect and other requirements. Therefore, adding the interceptor function to the unified hop routing SDK is a good solution.

2.9 packet route interceptor

Sometimes we need to authenticate a group of routes, reorganize parameters, break points, anti addiction and other requirements. Therefore, we add the packet interceptor function to the unified hop routing SDK.

2.10 route redirection

With the continuous iteration and technology update, many pages will be rewritten by other more appropriate technologies. At this time, the historical code is still using the old route. If you want to change the old code to the new route, you should consider the version control, change cost and possible risks. With the redirection function, you can cut into a new route without cost.

2.11 different technology stacks do not interfere with each other (keep elegant)

When a route is sent to the unified hop routing SDK, the unified hop routing SDK shall be able to distinguish which route scheduler to distribute the current route for scheduling. For example, the pages implemented by Native, fluent and HTML5 should be handed over to their respective routing scheduler.

3. What API s are provided

The following is the design of BBRouter's key API s:

NS_ASSUME_NONNULL_BEGIN

@interface BBRouter : NSObject <BBNativeRouter, BBBlockRouter>

@property (nonatomic, strong, class, readonly) BBRouterConfig *routerConfig;

///Set unified callback when jumping to undefined route
///@ param undefined routehandle unified callback when no route is defined
+ (void)setUndefinedRouteHandle:(void (^)(BBRouterParameter *))undefinedRouteHandle;

///Sets the callback that will open the specified page
+ (void)setWillOpenBlock:(void (^)(BBRouterParameter *))willOpenBlock;

///Set the callback that has opened the specified page
+ (void)setDidOpenBlock:(void (^)(BBRouterParameter *))didOpenBlock;

#pragma mark - register the route scheduler Dispatcher

///Register routing scheduler
///@ param dispatcher scheduler
/// @param scheme scheme
+ (BOOL)registerRouterDispatcher:(id<BBRouterDispatcher>)dispatcher scheme:(NSString *)scheme;

#pragma mark - BBBlockRouter
///Implementation of registered routing block
///@ param path route
///@ param action implementation
+ (BOOL)registerTask:(NSString *)path action:(BBBlockDispatcherAction)action;

///Remove registered block s
///Route corresponding to @ param path
+ (BOOL)removeTask:(NSString *)path;

#pragma mark - route jump API
///Route to the specified page (this method is the underlying method and is not recommended to be used directly. Please use the following method 👇 (convenient method)
///@ parameter parameter parameter
+ (void)routeWithRouterParameter:(BBRouterParameter *)parameter;

#pragma mark - page Jump if URL already exists

///Determines whether the specified URI can be opened
///@ param url page URI
+ (BOOL)canOpen:(NSString *)url;

///Open page
///@ param url page URI
+ (void)open:(NSString *)url;

///Open the page and carry the parameters
///@ param url page URI
///Parameters carried by @ param urlParams json serializable data types (complex data structures can be passed when scheme is native)
+ (void)open:(NSString *)url urlParams:(NSDictionary * __nullable)urlParams;

///Open the page and carry parameters, and support data return
/// @param url url
///Parameters carried by @ param urlParams json serializable data types (complex data structures can be passed when scheme is native)
///@ param resultCallback when the callback result is needed, it can be realized through the callback block
+ (void)open:(NSString *)url urlParams:(NSDictionary * __nullable)urlParams onPageFinished:(void (^ __nullable)(NSDictionary *result))resultCallback;

///Open the page and carry parameters, and support data return
/// @param url url
///Parameters carried by @ param urlParams json serializable data types (complex data structures can be passed when scheme is native)
///@ param exts extra parameters
///@ param resultCallback when the callback result is needed, it can be realized through the callback block
+ (void)open:(NSString *)url urlParams:(NSDictionary * __nullable)urlParams exts:(NSDictionary * __nullable)exts onPageFinished:(void (^ __nullable)(NSDictionary *result))resultCallback;

///Open the page and carry parameters, and support data return
/// @param url url
///Parameters carried by @ param urlParams json serializable data types (complex data structures can be passed when scheme is native)
///@ param exts extra parameter animated: whether there is transition animation
///@ param routerStyle transition animation
///@ param resultCallback when the callback result is needed, it can be realized through the callback block
+ (void)open:(NSString *)url urlParams:(NSDictionary * __nullable)urlParams exts:(NSDictionary * __nullable)exts routerStyle:(KBBRouterStyle)routerStyle  onPageFinished:(void (^ __nullable)(NSDictionary *result))resultCallback;

#pragma - mark BBNativeRouter

///Register the view controller through the class with path as the ID
+ (BOOL)registerClass:(Class)cls withPath:(NSString *)path;

///Register the view controller SDK through the class name, with path as the ID
+ (BOOL)registerWithClassName:(NSString *)className andPath:(NSString *)path;

///Register the view controller through the class name, with path as the identification, and confirm whether the parameters match the route
+ (BOOL)registerWithClassName:(NSString *)className andPath:(NSString *)path verifyBlock:(BOOL(^ __nullable)(NSString *path ,BBRouterParameter *routerParameter))verifyBlock;

///Delete registered view controller
+ (BOOL)removeRegisteredPath:(NSString *)path;

#Implementation of pragma mark - BlockDispatcher

///Execute the registered block and return the synchronization
/// @param url url
///@ param urlParams input parameter
+ (id _Nullable)invokeTaskWithUrl:(NSString *)url urlParams:(NSDictionary *)urlParams;

///Execute the registered block and return the synchronization
/// @param url url
///@ param urlParams input parameter
///@ param error error error message
+ (id _Nullable)invokeTaskWithUrl:(NSString *)url urlParams:(NSDictionary *)urlParams error:(NSError **)error;

#pragma mark - grouping

///Add path to the specified group
///@ param path routing path
///@ param group group name
+ (BOOL)addPath:(NSString *)path toGroup:(NSString *)group;

///Adds a set of paths to the specified group
///@ param paths path grouping
///@ param group group name
+ (void)addPaths:(NSArray<NSString *> *)paths toGroup:(NSString *)group;

///Specify all routes under the packet
///@ param group group name
+ (NSArray<NSString *> *)pathsInGroup:(NSString *)group;

///Configure the callback function of the grouping to handle the grouping logic
///@ param group group name
///@ param verifyBlock callback closure
+ (void)configGroup:(NSString *)group verifyBlock:(BOOL(^ __nullable)(NSString *path ,BBRouterParameter *routerParameter))verifyBlock;

///Back off
+ (UIViewController * _Nullable)backwardCompletion:(void (^ __nullable)(void))completion;

///Back off
///@ param animated whether animated
+ (UIViewController * _Nullable)backwardAnimated:(BOOL)animated completion: (void (^ __nullable)(void))completion;

///Fallback, no animation
///@ param count fallback series
+ (void)backwardCount:(NSInteger)count completion: (void (^ __nullable)(void))completion;

///Open the specified new view controller
///@ param vc new view controller
+ (void)openVC:(UIViewController *)vc routerParameter:(BBRouterParameter *)parameter;

///Does the view controller instance specified by the route currently exist
///@ param path routing path
+ (UIViewController * _Nullable)containsRouteObjectByPath:(NSString *)path;

///Returns to the previous level of the specified page
///@ param vc specified view controller instance
///@ param animatedBlock need animation
///@ param completion completed
+ (UIViewController * _Nullable)backwardVC:(UIViewController *)vc animatedBlock:(BOOL(^)(NSString *toppath))animatedBlock completion: (void (^__nullable)(void))completion;
@end

NS_ASSUME_NONNULL_END

3.1 register / remove routing scheduler

The route scheduler is used to isolate routes in different fields to facilitate decoupling. When using the unified hop routing SDK, developers can easily define their own routing scheduler and realize their own routing logic.

The jump logic of Native page, fluent page and HTML5 page must be different. At this time, there should be a corresponding routing scheduler to realize the jump behavior. Of course, behavioral routing also has a corresponding routing scheduler.

In short, you can give full play to your imagination and give full play to the ability of the unified hop routing SDK. You just need to define a routing scheduler suitable for you.

3.2 register / remove the binding relationship between route and page

We need to register the binding relationship between the route and the page with the unified hop routing SDK to allow the unified hop routing SDK to dynamically analyze the route, dynamically generate page instances and realize automatic jump.

It should be noted that this registration should allow updates to achieve dynamic updates to the routing table.

The registration is not necessarily a page, but also a Service. For example, the target page is provided by a third party and can only be opened by calling a method of the corresponding SDK. You can't register the page directly. At this time, we can register a Service for transfer. When the Service is called, we can easily realize the above requirements by calling the corresponding method of SDK.

3.3 open a route and get the return value

The unified hop routing SDK shall provide a method to open a page (or call a method) and provide a callback to obtain the return value.

The following should be included:

  • uri: route
  • Parameters: input parameters to carry
  • exts: other parameters (non business parameters, such as specifying transition animation mode)
  • Callback: callback function pointer

3.4 back to previous page

The unified hop routing SDK shall provide a method to return to the previous page.

3.5 back to the specified page

The unified hop routing SDK shall provide a method of fallback to the specified route in the fallback stack and return an instance of the specified route.
The unified hop routing SDK shall provide a method of fallback layer N routing.

3.6 processing of route not found

When the message pushes a page unique to the new version, the old version should enter the unified exit not found in the route. You can redirect here or prompt the user to upgrade to the latest version.

4. Key ideas and specifications

We have straightened out the requirements, and then we design the architecture of unified hop routing SDK.

4.1 how to achieve high scalability

Reasonable abstraction and function splitting are the basis of realizing high scalability.

We design the abstract concept of routing scheduler, which is used to isolate routes in different fields.

Native route, fluent route, HTML5 route, applet route, etc. have corresponding route scheduler to realize scheduling. Behavioral routing is also scheduled by the corresponding routing scheduler.

4.2 how to avoid intrusion and coupling

When the unified hop routing SDK was established, our project had a certain scale. If we need to modify the existing business code to access the unified hop routing SDK, it will undoubtedly be a disaster. Therefore, we must be perfectly compatible with traditional development methods and avoid introducing additional workload and member learning costs.

As you think, we are almost perfectly compatible with the traditional development methods. See the unified hop routing SDK (iOS Implementation) for details.

4.3 scientific management routing table

  • Centralized routing table management
  • Version management (applied to dynamic routing tables)
  • The routing table shall indicate the routing name, purpose description, in parameter, out parameter and other additional restrictions (such as the permission required to enter the page)

4.3.1 optimization of user experience

Generate code at each end through script:

Avoid hard coding: the routing table is mapped to a structure, and each route is an attribute. In this way, hard coding is avoided.

Input parameter constructor: the input parameter is a dictionary. We can generate the constructor corresponding to the dictionary according to the input parameter during route definition.

Output parameter: the output parameter is a dictionary. We can automatically generate the association attributes of the dictionary according to the routing table.

Version management: after tag ging the routing table warehouse, the script is automatically executed to generate the code of each end (this article will not expand).

4.4 routing table dynamic distribution

The configuration center provides the ability to update the routing table, and each end updates the routing table according to the agreed strategy.

5. Unified hop routing SDK (implemented on iOS side)

5.1 compatible with native development mode

Taking the traditional development method of iOS as an example, the following steps are required to jump to a new page:

  1. Create target ViewController instance
  2. The input parameter is passed in the way of ViewController instance attribute assignment
  3. Obtain the appropriate instance of NavigationController (if the transition mode is modal, obtain the appropriate instance of ViewController)
  4. NavigationController instance jumps to a new page in push mode (or ViewController jumps to a new page in modal mode)
  5. Return the value in block or delegate mode

The above methods can meet most scenarios. Let's think about how to realize the above steps in an elegant way:

  1. The URI is bound to the ViewController class in the form of key value pairs, and the ViewController instance is dynamically generated with the help of Objective-C runtime.
  2. The URI carries the input parameter in the form of Query (the input parameter will be resolved into a Dictionary in the unified hop routing SDK), and the key is the name of the ViewController attribute (or instance variable). With the help of Objective-C runtime, judge whether the ViewController class contains the attribute or instance variable, and judge whether the data type is consistent, If yes, the attribute or instance variable is assigned by Objective-C KVC, so as to realize the transfer of input parameters.
  3. By traversing the route fallback stack on the main Window (not necessarily the keyWindow, depending on the actual situation), you can obtain the appropriate NavigationController instance (the ViewController instance at the top of the stack when present).
  4. All the above conditions are met. At this time, it is easy to realize page Jump.
  5. For data return, we can return it when the ViewController is removed (it must not be dealloc, because dealloc will not be called in case of memory leakage, and memory leakage will occur occasionally).

The above ideas are clear and executable, but if you want to be more flexible and easy to use, you need to skillfully connect the ViewController instance with the routing related parameters.

We encapsulate the routing related parameters into a class RouterParameter, with the following structure:

@interface RouterParameter : NSObject

///Route domain (which route scheduler schedules the route)
@property (nonatomic, copy) NSString *scheme;
///Routing path (excluding query and fragment parts)
@property (nonatomic, copy) NSString *fullPath;
///URI query part
@property (nonatomic, copy) NSString *query;
///URI fragment part
@property (nonatomic, copy) NSString *fragment;
///Page Jump mode (push/present)
@property (nonatomic, assign) KBBRouterStyle routerStyle;
///Complete URI (will spell addition into query)
@property (nonatomic, copy) NSString *url;
///Route in parameter
@property (nonatomic, strong, readonly) NSMutableDictionary *addition;
///Additional parameters (routing behavior parameters, such as whether to start transition animation)
@property (nonatomic, strong, readonly) NSMutableDictionary *exts;
///Callback value (code, message, data)
@property (nonatomic, strong) NSDictionary *response;
///Callback function used to return value
@property (nonatomic, copy) void (^__nullable callBackBlock)(NSDictionary *result);

Convert the relevant parameters carried by the unified hop + (void) open: (nsstring *) URL urlparameters: (nsdictionary * _nullable) urlparameters exts: (nsdictionary * _nullable) exts routerstyle: (kbbrauterstyle) routerstyle onpagefinished: (void (^_nullable) (nsdictionary * result)) ResultCallback method into routerParameter instances and pass them inside the unified hop routing SDK, Add the attribute routerParameter for UIViewController through UIViewController Category and Objective-C "associated object".

At this moment, we will find that the above "ideas" have been implemented, the ideas are clear and easy to understand, and are perfectly compatible with the native development mode. Thus, the traditional mode can be painlessly and gradually switched to the "routing mode".

5.2 architecture

6. How to use

A quick browse of the Demo can help you understand a framework more intuitively. Let's take a look at the general usage and use.

6.1 overall process

Initialization phase:

  • Load routing table
  • Register route interceptor
  • Native route registration
  • Non page routing registration
  • Packet interceptor registration

Ready phase: at this time, the unified hop routing SDK is ready.

6.2 page class routing call

Own Native page:

Route registration

// Register the ViewController implemented by Objective-C
BBRouter.register(withClassName: "MomentsViewController", andPath: BBRouterPaths.moments)

// Register the ViewController implemented by Swift (note the namespace)
BBRouter.register(withClassName: swiftClassFullName("MomentsViewController", "Community"), andPath: BBRouterPaths.moments)

The page implemented by fluent / HTML5 is not registered here, but managed by the fluent / HTML5 project itself

Route jump

// Route jump without return value
[BBRouter open:BBRouterPaths.moments urlParams:@{@"momentId":@"11223344"}];

// Route jump with return value (bbrouterpatterns. Selectalcohol page may be implemented by any technology, such as Native[Swift\Objective-C], fluent, HTML5, etc.)
[BBRouter open:BBRouterPaths.selectAlcohol urlParams:@{@"alcoholId":@"112233"} onPageFinished:^(NSDictionary * _Nonnull result) {
    // r_data is an attribute added to NSDictionary through Objective-C's Category and associated object method, so as to eliminate hard coding.
    DEBUGLog(@"%@", [NSString stringWithFormat:@"%@", result.r_data]);
}];

BBRouterPaths. Select alcohol: hard code the route in this way. Direct hard coding cannot be checked by compiler, and the maintenance cost is high. One of the design goals of unified hop routing SDK is to eliminate hard coding.

6.2 method / behavior routing call

// Registration behavior
[BBRouter registerTask:@"action://xxx.com/yyy/zzz" action:^id _Nullable(BBRouterParameter * _Nonnull routerParameter) {
    return routerParameter.addition;
}];

// Method asynchronous call (Unified hop unified method for routing, regardless of the field of routing)
[BBRouter open:@"action://xxx.com/yyy/zzz" urlParams:@{@"name":@"xiaoming"} onPageFinished:^(NSDictionary * _Nonnull result) {
    DEBUGLog(@"%@", [NSString stringWithFormat:@"%@", result]);
}];

// Method synchronous call (event specific method for routing)
NSError *error = nil;
id result = [BBRouter invokeTaskWithUrl:@"action://xxx.com/yyy/zzz" urlParams:@{@"name":@"xiaoming"} error:&error];
DEBUGLog(@"%@", [NSString stringWithFormat:@"%@", result]);

Third party application designated page:

Unpack Taobao and tmall ipa file, analyzed their routing table and call rules, and found that our unified hop routing SDK also perfectly supports it with a try attitude.

// Taobao product details page
[BBRouter open:BBRouterPaths.threeSides urlParams:@{@"i":@"taobao://item.taobao.com/item.htm?id=554418184878"}];

// Tmall product details page
[BBRouter open:BBRouterPaths.threeSides urlParams:@{@"i":@"tmall://page.tm/itemDetail?itemID=551101867384"}];

6.3 simple demonstration of route interceptor

Parameter reorganization: in Objective-C, id is a keyword, but other languages can be used normally. In order to be compatible with this scenario, you can rearrange the input parameters in the interceptor.

BBRouter.register(withClassName: "XXXViewController", andPath: BBRouterPaths.xxx, verifyBlock: { path, routerParameter in
    routerParameter.addition["ID"] = routerParameter.addition["id"]
    return true
})

Redirection: the page is reconstructed with new technology. The new version should jump to the new page. With the help of redirection ability, we don't have to modify the existing code. Even if the old code jumps the old route, the runtime will be redirected to the new page.

BBRouter.register(withClassName: "XXXViewController", andPath: BBRouterPaths.xxx, verifyBlock: { path, routerParameter in
    let newParameter = BBRouterParameter(byURI: BBRouterPaths.yyy, addition: routerParameter.addition.copy() as! [String : Any])
    newParameter.actionBlock = routerParameter.actionBlock
    newParameter.routerStyle = routerParameter.routerStyle
    newParameter.exts.addEntries(from: routerParameter.exts as! [String : Any])
    BBRouter.route(with: newParameter)

    return false
})

6.4 simple demonstration of routing packet interceptor function

Here, a group interceptor is used to realize the requirement that a group of pages can be accessed only after successful login, and the consistency of user operation is realized.

let isAuthed = "isAuthed"
BBRouter.addPaths(needAuthedPaths, toGroup: isAuthed);
BBRouter.configGroup(isAuthed) { (path, routerParameter) -> Bool in
    if (memberId.isEmpty) {
        BBRouter.open(BBRouterPaths.login, urlParams: Dictionary(), exts: Dictionary()) { (result) in
            if (!memberId.isEmpty) {// If you are logged in, continue with the previous operation
                BBRouter.route(with: routerParameter)
            }
        }
        return false;
    }
    return true
}

6.5 route unregistered processing

//You can give the unregistered routing information to the HTML5 landing page here. At this time, it is very flexible. You can redirect or prompt the user to upgrade.

BBRouter.setUndefinedRouteHandle { (parameter) in
    let url = parameter.url
    BBRouter.open(BBRouterPaths.routerNotFound, urlParams: ["url":url])
}

Summary

The 100 bottle unified hop routing SDK makes unified hop a reality and lays a foundation for page visualization. So far, it has been put into use for about one year, which plays an important role in promoting the process of componentization / modularization, and has well completed the goal of "decoupling and efficiency improvement" during project initiation. What is more gratifying is that the iOS terminal can painlessly and gradually switch from the traditional mode to the "routing mode", and the access process is almost zero cost.

Due to the limited space, many important implementation details are not mentioned, and many application scenarios are not mentioned. On the other hand, I don't want to explain the details too thoroughly, so as not to be preconceived and affect everyone's thinking.

Finally, I sincerely hope you can point out the shortcomings of the scheme and put forward new optimization suggestions. If you have any questions, please leave a message at the bottom of the article and we will reply as soon as possible. If this article can enlighten you a little, please also like it. Thank you even more if you can share it with your friends.

More attention, please pay attention to our official account of "100 bottles technology". There are occasional benefits!

Topics: Front-end iOS