Componentization is also a platitude topic, this article mainly says in componentization, the station is more important position routing design. In your project, you may directly rely on the routing components of the third party, or you may customize a set of components according to the actual needs of the project. Now I want to talk about the design and analysis of routing through several popular third-party components. Here do not recommend that we use which good which bad, just learn their design ideas. As we see tripartite library source code, should be learning the idea of programming and design.

preface

With more and more App needs and more and more complex businesses, there is an increasing need for a more efficient framework for more efficient iterations, improved user experience and reduced maintenance costs. Therefore, we have probably experienced the reconstruction and componentization of the project. According to the actual needs of the project, the new framework may need to be layered horizontally and vertically with different granularity, for more efficient development and maintenance in the future. Then comes a problem, how to maintain the characteristics of “high cohesion, low coupling”, the following to talk about some ideas to solve this problem.

What problems can routing solve

List a few common development problems, or requirements:

  1. Push message, or open a URL on the web page to jump to a page inside the App
  2. Jump between other apps or other apps of your own company
  3. Jumping between pages between different components
  4. How to unify the page hopping logic at both ends
  5. How can I demote a buggy online page to another H5 or error page
  6. Buried point of page redirect
  7. Logic checking during a jump

All of these problems can be solved by designing a route. Let’s continue to see how to implement the jump with these problems.

Realize the jump

Through the above questions, we hope to design a set of steps to realize the unified jump of App external and internal, so let’s talk about the realization of App external jump first.

URL Scheme

Add URL types-URL Schemes to info.plist

And then in Safari you can type in URL access to our App

- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation
{
    
}
Copy the code

Through the above method can listen to the call of the external App, you can do some interception or other operations as needed.

App can also jump directly to the system Settings. For example, some requirements require the detection of whether the user has enabled certain system permissions. If not, the pop-up box will prompt you. Click the button of the pop-up box to directly jump to the corresponding setting interface in the system Settings.

Universal links

Universal Links allows us to launch our App with an HTTP link.

  • If you have an App installed, whether it’s in Safari or some other third-party browser or whatever, you can open the App
  • If not, the page will be opened

Setting Mode:

applinks:

So those are the two ways to jump between apps in iOS.

Routing design

After finishing the jump logic between apps, we will enter the key point, the route design inside App. There are two main problems to be solved:

  • Components call each other, and as the business becomes more complex, if the granularity of componentization is not appropriate, the number of components will increase, and the dependencies between components will inevitably increase
  • Calls between a page and its component, such as pushing a VC, need to import this class, resulting in strong dependencies so that dead code cannot be demoted to another page if an online bug occurs

Combining the above two questions, how do we design a route? First, of course, to see the wheel someone else built. – below I will list a few wheels that I have used and referred to in my development. Some are directly used by others, and some are encapsulated by others’ ideas. All in all, they are worth learning.

The Route analysis

JLRoutes

JLRoutes is currently star5.3K on GitHub, which should be the most popular routing component on GitHub. Therefore, we first analyze its design idea.

  1. JLRoutes maintains a globalJLRGlobal_routeControllersMapThe map uses Scheme as the key and JLRoutes as the value. Therefore, each scheme is unique.
+ (instancetype)routesForScheme:(NSString *)scheme
{
    JLRoutes *routesController = nil;
    
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        JLRGlobal_routeControllersMap = [[NSMutableDictionary alloc] init];
    });
    
    if(! JLRGlobal_routeControllersMap[scheme]) { routesController = [[self alloc] init]; routesController.scheme = scheme; JLRGlobal_routeControllersMap[scheme] = routesController; } routesController = JLRGlobal_routeControllersMap[scheme];return routesController;
}
Copy the code
  1. Scheme can be thought of as a URI, and each registered string is shred
- (void)addRoute:(NSString *)routePattern priority:(NSUInteger)priority handler:(BOOL (^)(NSDictionary<NSString *, id> *parameters))handlerBlock
{
    NSArray <NSString *> *optionalRoutePatterns = [JLRParsingUtilities expandOptionalRoutePatternsForPattern:routePattern];
    JLRRouteDefinition *route = [[JLRGlobal_routeDefinitionClass alloc] initWithPattern:routePattern priority:priority handlerBlock:handlerBlock];
    
    if (optionalRoutePatterns.count > 0) {
        // there are optional params, parse and add them
        for (NSString *pattern in optionalRoutePatterns) {
            JLRRouteDefinition *optionalRoute = [[JLRGlobal_routeDefinitionClass alloc] initWithPattern:pattern priority:priority handlerBlock:handlerBlock];
            [self _registerRoute:optionalRoute];
            [self _verboseLog:@"Automatically created optional route: %@", optionalRoute];
        }
        return;
    }
    
    [self _registerRoute:route];
}
Copy the code
  1. Insert JLRoutes into the array by priority, with the highest priority placed first
- (void)_registerRoute:(JLRRouteDefinition *)route
{
    if (route.priority == 0 || self.mutableRoutes.count == 0) {
        [self.mutableRoutes addObject:route];
    } else {
        NSUInteger index = 0;
        BOOL addedRoute = NO;
        
        // search through existing routes looking for a lower priority route than this one
        for (JLRRouteDefinition *existingRoute in [self.mutableRoutes copy]) {
            if (existingRoute.priority < route.priority) {
                // if found, add the route after it
                [self.mutableRoutes insertObject:route atIndex:index];
                addedRoute = YES;
                break; } index++; } / /if we weren't able to find a lower priority route, this is the new lowest priority route (or same priority as self.routes.lastObject) and should just be added if (! addedRoute) { [self.mutableRoutes addObject:route]; } } [route didBecomeRegisteredForScheme:self.scheme]; }Copy the code
  1. Initialize a JLRRouteRequest based on the URL, then search through the array of JLRoutes until a match is found. Then fetch parameters and execute Handler
- (BOOL)_routeURL:(NSURL *)URL withParameters:(NSDictionary *)parameters executeRouteBlock:(BOOL)executeRouteBlock
{
    if(! URL) {return NO;
    }
    
    [self _verboseLog:@"Trying to route URL %@", URL];
    
    BOOL didRoute = NO;
    
    JLRRouteRequestOptions options = [self _routeRequestOptions];
    JLRRouteRequest *request = [[JLRRouteRequest alloc] initWithURL:URL options:options additionalParameters:parameters];
    
    for (JLRRouteDefinition *route in [self.mutableRoutes copy]) {
        // check each route for a matching response
        JLRRouteResponse *response = [route routeResponseForRequest:request];
        if(! response.isMatch) {continue;
        }
        
        [self _verboseLog:@"Successfully matched %@", route];
        
        if(! executeRouteBlock) { //if we shouldn't execute but it was a match, we're done now
            return YES;
        }
        
        [self _verboseLog:@"Match parameters are %@", response.parameters];
        
        // Call the handler block
        didRoute = [route callHandlerBlockWithParameters:response.parameters];
        
        if (didRoute) {
            // if it was routed successfully, we're done - otherwise, continue trying to route break; } } if (! didRoute) { [self _verboseLog:@"Could not find a matching route"]; } // if we couldn't find a match and this routes controller specifies to fallback and its also not the global routes controller, then...
    if(! didRoute && self.shouldFallbackToGlobalRoutes && ! [self _isGlobalRoutesController]) { [self _verboseLog:@"Falling back to global routes..."]; didRoute = [[JLRoutes globalRoutes] _routeURL:URL withParameters:parameters executeRouteBlock:executeRouteBlock]; } / /if, after everything, we did not route anything and we have an unmatched URL handler, then call it
    if(! didRoute && executeRouteBlock && self.unmatchedURLHandler) { [self _verboseLog:@"Falling back to the unmatched URL handler"];
        self.unmatchedURLHandler(self, URL, parameters);
    }
    
    return didRoute;
}
Copy the code

CTMediator

CTMediator is currently a star 3.3K on Github. It is a lightweight library with only one class and one category and only a few lines of code in total. What’s more, the author added Chinese annotations and detailed examples in key codes. The main idea is to use target-Action and runtime for decoupling. In this pattern, each component does not depend on the other, but all rely on the middleware for scheduling. The header exposes two methods that handle calls to the remote App and local component respectively

- (id _Nullable)performActionWithUrl:(NSURL * _Nullable)url completion:(void(^_Nullable)(NSDictionary * _Nullable info))completion; - (id _Nullable) performTarget:(NSString * _Nullable)targetName action:(NSString * _Nullable)actionName params:(NSDictionary * _Nullable)params shouldCacheTarget:(BOOL)shouldCacheTarget;Copy the code

For remote App, also do a step of security processing, the final resolution is also called in the local component processing method

- (id)performActionWithUrl:(NSURL *)url completion:(void (^)(NSDictionary *))completion
{
    if (url == nil) {
        return nil;
    }
    
    NSMutableDictionary *params = [[NSMutableDictionary alloc] init];
    NSString *urlString = [url query];
    for (NSString *param in [urlString componentsSeparatedByString:@"&"]) {
        NSArray *elts = [param componentsSeparatedByString:@"="];
        if([elts count] < 2) continue;
        [params setObject:[elts lastObject] forKey:[elts firstObject]]; } // This is written mainly for security reasons, preventing hackers from remotely calling local modules. This is sufficient for most scenarios, but more complex security logic can be done if the requirements are more stringent. NSString *actionName = [url.path stringByReplacingOccurrencesOfString:@"/" withString:@""];
    if ([actionName hasPrefix:@"native"]) {
        return@(NO); } // The demo handles URL routing very simply, taking the corresponding target name and method name, but this is enough for most of the requirements. If you need to expand, Result = [self performTarget:url.host Action :actionName params:params shouldCacheTarget:NO];if (completion) {
        if (result) {
            completion(@{@"result":result});
        } else{ completion(nil); }}return result;
}
Copy the code

Requests that do not respond are processed uniformly

- (void)NoTargetActionResponseWithTargetString:(NSString *)targetString selectorString:(NSString *)selectorString originParams:(NSDictionary *)originParams
{
    SEL action = NSSelectorFromString(@"Action_response:");
    NSObject *target = [[NSClassFromString(@"Target_NoTargetAction") alloc] init];
    
    NSMutableDictionary *params = [[NSMutableDictionary alloc] init];
    params[@"originParams"] = originParams;
    params[@"targetString"] = targetString;
    params[@"selectorString"] = selectorString;
    
    [self safePerformAction:action target:target params:params];
}
Copy the code
use

Specific use requires the following steps:

  • For each line of business (or component) that needs to be scheduled by another component, create one for that componentTargetClass toTarget_Name prefixes

    This class will add all the methods that need to be scheduled by other componentsAction_Name the prefix.

  • Create a CTMediator category for each component

    This category is what the caller depends on to complete the schedule, so that all the methods in the category are called uniformly, all of themperformTarget: action: params: shouldCacheTarget:

  • The final dispatch logic is handed over to CTMediator
  • The caller only needs to rely on the category of the component that has scheduling requirements

If you are interested, you can also take a look at the author’s article, which introduces in detail the design idea of CTMediator and how to implement the componentization scheme based on CTMediator in existing projects by adding CTMediator iOS application architecture

MGJRouter

MGJRouter currently on Github star 2.2k

The problem with JLRoutes is that the implementation of finding urls is not efficient, traversing rather than matching. There is more function. HHRouter URL lookup is based on matching, so it is more efficient. MGJRouter also adopts this method, but it is too tightly bound to ViewController, which reduces flexibility to some extent. Hence the MGJRouter.

/** * saves all registered urls * structure like @{@"beauty": @ {@":id": {@"_", [block copy]}}}
 */
@property (nonatomic) NSMutableDictionary *routes;
Copy the code

The MGJRouter is a singleton object that maintains a registry in the format of “URL -> Block” internally, through which blocks registered by the server are saved. The caller can map the block through the URL and initiate the call to the server through the MGJRouter.

The general usage process is as follows:

  • The service component provides one externallyPublicHeaderIn thePublicHeaderDeclares a list of urls that can be called externally
#ifndef MMCUserUrlDefines_h
#define MMCUserUrlDefines_h/** description my personal center page @return MMCUserViewController
 */
#define MMCRouterGetUserViewController @"MMC://User/UserCenter"/** description my message list @return MMCMessageListViewController
 */
#define MMCRouterGetMessageVC @"MMC://User/MMCMessageListViewController"
Copy the code
  • A block is registered within a component, and the caller invokes the block through a URL
+ (void)registerGotoUserVC
{
    [MMCRouter registerURLPattern:MMCRouterGetUserViewController toHandler:^(NSDictionary *params) {
    
    }];
}
Copy the code
  • With an openURL call, you can concatenate parameters after the URL as a GET request, or you can pass in a dictionary via param
[MMCRouter openURL:MMCRouterGetUserViewController];
Copy the code
  • In addition to jumping,MGJRouterIt also provides methods that can return an object
+ (void)registerURLPattern:(NSString *)URLPattern toObjectHandler:(MGJRouterObjectHandler)handler
Copy the code

For example, the route returns a controller that can be handled by the caller.

+(void)registerSearchCarListVC{ [MMCRouter registerURLPattern:MMCRouterGetSearchCarListController toObjectHandler:^id(NSDictionary *params) { NSDictionary *userInfo = [params objectForKey:MMCRouterParameterUserInfo];  NSString *carType = [userInfo objectForKey:MMCRouterCarType];  MMCSearchCarListViewController *vc = [[MMCSearchCarListViewController alloc] init]; vc.strCarType = carType;return vc;
    }];
}
Copy the code
Protocol – class

According to the use of MGJRouter introduced above, it is not difficult to see the problems of URL hard coding and parameter limitations. In order to solve these problems, Mogujie also proposed the Protocol scheme. The Protocol scheme consists of the ModuleManager class for communication between components and the MGJComponentProtocol class.

Messages are called and forwarded through the middleware ModuleManager, and a mapping table is maintained inside the ModuleManager, which is changed from “URL -> block” to “Protocol -> Class”.

Because the current hand of the project did not use this, so the use of code will not paste, interested in their own hundred degrees.

The advantages and disadvantages

URL Registration scheme

Advantages:

  1. The easiest way to think about it
  2. It can unify the scheduling of three terminals
  3. Dynamic demotion of online bugs

Disadvantages:

  1. Hard coded, urls need to be managed specifically
  2. URL rules require advance registration
  3. There is resident memory. Memory problems may occur

Protocol – Class

Advantages:

  1. No hard coded
  2. Parameters are unlimited, and you can even pass models

Disadvantages:

  1. Added new middleware and a lot of protocol, call encoding complex
  2. Route’s jump logic is scattered among classes and difficult to maintain

Target – the Action plan

Advantages:

  1. Use Runtime to decouple calls without registration
  2. There is a certain amount of security
  3. Unify processing outside the App and between components

Disadvantages:

  1. You need to create a separate category for each component, and the authors recommend that category also be a separate POD
  2. The call is also hardcoded internally, requiringTarget_ ,Action_Naming rules

Finally, I want to say that there is no best route, only the most suitable route for your project. According to the actual situation of your project, analyze and decide which componentization scheme to use.