preface

As users have more and more demands, they have higher and higher requirements for App user experience. In order to better cope with various requirements, developers changed the App architecture from simple MVC to MVVM, VIPER and other complex architectures from the perspective of software engineering. Change the architecture suitable for the service for better maintenance projects in the future.

But users are still not satisfied and continue to demand more and more from developers, not only high-quality user experience, but also rapid iteration, preferably one new feature a day, and users want to experience new features without updating. In order to meet customer demand, the developers used H5, ReactNative, Weex and other technologies to retrofit existing projects. Project architecture has also become more complex, with vertical layers, such as network layer, UI layer and data persistence layer. Each horizontal layer is also componentized according to the business. While doing so would make development more efficient and maintainable, how do you decouple layers, interfaces and components, reduce coupling between components, and keep the system “high cohesion, low coupling” no matter how complex it is? This series of problems are in front of developers, urgently to solve. Today, let’s talk about some ideas for solving this problem.

directory

  • 1. The introduction
  • 2. What problems can App routing solve
  • 3. Realize the jump between apps
  • 4. Route design between components in App
  • 5. Advantages and disadvantages of each scheme
  • 6. Best plan

A tease.

Big front-end development for so many years, I believe will also encounter similar problems. React and Vue have been at the forefront of the rapid development of SPA in the past two years. Let’s take a look at how they deal with this problem.

In SPA single page application, routing plays a key role. The main function of routing is to ensure the synchronization of views and urls. From the front end’s perspective, a view is considered a representation of a resource. When users perform operations on the page, the application will switch between several interactive states. Routing can record some important states, such as the user viewing a website, whether the user logs in, and which page of the website the user visits. These changes are also recorded in the browser’s history, and users can switch status using the browser’s forward and back buttons. In general, the user can change the URL through manual input or interaction with the page, and then send a request to the server to obtain resources synchronously or asynchronously, and redraw the UI after success, as shown in the following figure:

The React-router uses the location passed in to render the new UI as follows:

Location comes from two sources, one is the browser back and forward, the other is a direct click on a link. After the new Location object, the matchRoutes method inside the Route matches a subset of the Route component tree that matches the current Location object and gets nextState, The Router component can be rerendered in this.setState(nextState).

The big front end goes something like this, and we can take those ideas and apply them to iOS. The Back/Forward in the figure above can be managed by the UINavgation in many cases on iOS. So the Router in iOS mainly deals with the green one.

2. What problems can App routing solve

Since the front end can solve the problem of URL and UI synchronization in SPA, what problems can this idea solve in App?

Consider the following questions and how we solve them gracefully in our development:

1.3 D-touch function or click push message, requiring the external to jump to a deep level interface inside the App.

For example, wechat’s 3D-Touch can directly jump to “my QR code”. “My QR code” interface in my inside the third level interface. Or even more extreme, the product needs to give a more abnormal demand, to jump to the App internal interface of the tenth layer, how to deal with it?

2. How to jump between a series of apps?

If you have several apps and want to jump to each other, what should you do?

3. How to remove the coupling between App components and App pages?

As the project becomes more and more complex, the jump logical relevance between each component and each page becomes more and more. How to gracefully remove the coupling between each component and the page?

4. How can I unify the page hopping logic on iOS and Android? How can we even unify the way the three ends request resources?

Some modules in the project will mix ReactNative, Weex, and H5 interfaces, which will also call Native interfaces and Native components. So, how can we unify the way of requesting resources on the Web end and Native end?

5. If dynamic delivery of configuration files is used to configure the jump logic of the App, how to achieve iOS and Android only need to share a set of configuration files?

6. If the App has a bug, how to do a simple hotfix function without using JSPatch?

For example, if the App is launched and suddenly encounters an urgent bug, can the dynamic page be degraded to H5, ReactNative or Weex? Or a native error screen?

7. How to make buried point statistics between calls and page jumps of each component? Every jump place handwritten code buried dot? Using Runtime AOP?

8. How to add logic check, token mechanism, risk control logic with gray scale in the process of calling between each component?

9. How to call the same interface or component in any interface of the App? Can only be implemented by registering a singleton in the AppDelegate?

For example, if there is a problem with the App, the user may be in any interface. How to force the user to log out at any time and anywhere? Or force them all to jump to the same local error screen? Or jump to the corresponding H5, ReactNative, Weex interface? How to let the user in any interface, anytime, anywhere pop up a View?

These problems can be solved by designing a path on the App side. So how do we design a route?

Iii. Realize the jump between apps

Before we talk about intra-app routing, let’s talk about how you can jump between iOS systems, between different apps.

1. In URL Scheme mode

IOS supports URL Scheme by default. For details, see the official documentation.

For example, entering the following command on the iPhone’s Safari browser will automatically open some apps:


// Open the mailbox
mailto://

// Call 110
tel:/ / 110Copy the code

Before iOS 9 just add URL types-URL Schemes in App info.plist as follows:

Add a com.ios.Qhomer Scheme here. To do this, type in the iPhone’s Safari browser:


com.ios.Qhomer://Copy the code

You can just open the App.

For other common apps, you can download its IPA file from iTunes, unzip it, and display the package contents to find the info.plist file. Open it, and you can find the corresponding URL Scheme inside.


/ / mobile phone QQ
mqq://

/ / WeChat
weixin://

// Sina Weibo
sinaweibo://

/ / hungry
eleme://Copy the code

Of course, some apps are sensitive to calling URL schemes, and they don’t want other apps to just call them.



- (BOOL)application:(UIApplication *)application
            openURL:(NSURL *)url
  sourceApplication:(NSString *)sourceApplication
         annotation:(id)annotation
{
    NSLog(@"sourceApplication: %@", sourceApplication);
    NSLog(@"URL scheme:%@", [url scheme]);
    NSLog(@"URL query: %@", [url query]);

    if ([sourceApplication isEqualToString:@"com.tencent.weixin"]) {// Allow open
        return YES;
    }else{
        return NO; }}Copy the code

If the App to be called is already running, its life cycle is as follows:

If the App to be called is in the background, its life cycle is as follows:

Understand the life cycle of the above, we can by calling application: openURL: sourceApplication: annotation: this method, to prevent some App call at random.

As shown in the figure above, the Ele. me App can be called through the URL Scheme, so we can call the Ele. me App in Safari. Mobile QQ is not allowed to be called, so we can’t jump to it in Safari.

For inter-app Communication, you can refer to the official document inter-App Communication.

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.

IOS 10 supports access to the system through the URL Scheme. Correct access to the system in iOS10 This section describes the URL summary list of iOS functions

Although all Scheme will be prohibited to open web pages inside wechat, iOS 9.0 has added a new function called Universal Links, which enables our App to launch the App through HTTP Links. 1. If you have installed the App, you can open the App through HTTP links in wechat, Safari browser, or other third-party browsers. 2. If the App is not installed, the web page will be opened.

There are 3 steps for specific Settings:

1. The App needs to enable the Associated Domains service and set Domains. Note that the App must start with applinks:.

2. The domain name must support HTTPS.

3. Upload a file named apple-app-site-association in Json format to the root directory of your domain name or to the. Well-known directory. IOS will automatically read this file. For details, please refer to the official documents.

If the App supports Universal Links, we can directly jump to our own App from other apps. Click the link as shown in the picture below. Since the link will Matcher to the link we set, it will be displayed in the menu to open it with our App.

In the browser, the effect is the same, if you support Universal Links, access the corresponding URL, will have a different effect. The diagram below:

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

From the URL Scheme mode supported by iOS system, we can see that Apple also uses URI to access a resource.

A Uniform Resource Identifier (OR URI) is a string used to identify the name of an Internet Resource. This identifier allows users to interact with resources on the network (commonly referred to as the World Wide Web) over specific protocols. The most common form of URI is the Uniform resource Locator (URL).

Here’s an example:

This is a URI, and each paragraph represents a corresponding meaning. The other party receives such a string, parses it according to the rules, and gets all the useful information.

Can this give us some ideas for designing routing between App components? If we want to define a three-way (iOS, Android, H5) unified access to resources, can we do it in this way?

Iv. Route design between App components

In the previous chapter, we introduced how iOS handles App jump logic for us. In this chapter, we will focus on how to design routing between components within an App. As for the routing design inside App, there are two main problems to be solved:

1. Hops between pages and components. 2. Components invoke each other.

Let’s analyze these two questions first.

1. About page hopping

During iOS development, you will often encounter the following scenarios: click a button to jump Push to another screen, or click a cell to Present a new ViewController. In MVC mode, it is common to create a VC and Push/Present it to the next VC. But in MVVM, there are some inappropriate situations.

As we all know, MVVM has taken MVC apart to look like the illustration above. The data related code of the original View has been moved into the ViewModel, and the corresponding C has been slimmed down to the structure of M-VM-C-V. The code in C here can only be left with page-hopping logic. In code it looks like this:

Assume that the execution logic of a button is encapsulated as command.

    @weakify(self);
    [[[_viewModel.someCommand executionSignals] flatten] subscribeNext:^(id x) {
        @strongify(self);
        // Jump logic
        [self.navigationController pushViewController:targetViewController animated:YES];
  }];Copy the code

There is nothing wrong with the above code per se, but it may detract from an important function of the MVVM framework.

In addition to decoupling, the purpose of the MVVM framework has two important purposes:

  1. Code with high repetition rate
  2. Facilitate unit testing

If we need to test whether a business is correct, we simply unit test the ViewModel. This assumes that our UI binding with ReactiveCocoa is spot-on. Currently binding is correct. So we only need to unit test to ViewModel to complete the test of business logic.

Page jumps are also business logic, so they should be unit tested in the ViewModel to ensure coverage of business logic tests.

There are two ways to put a page jump into the ViewModel. The first way is to use routing to achieve the second way, because there is no relationship with routing, so I will not elaborate here. If you are interested, you can see the specific implementation of the lPD-MvVM-Kit library on the page jump.

The coupling between page jumps is also reflected:

1. Since either pushViewController or presentViewController requires a ViewController to operate on, this class must be introduced, and the import header introduces coupling. 2. Since the jump operation is written here, if there is a bug on the line, it is not under our control. 3. Push messages or 3D-touch requirements require direct jump to internal level 10 interface, so you need to write an entry to jump to the specified interface.

2. On inter-component invocation

With respect to calls between components, decoupling is also required. As the business becomes more and more complex, we package more and more components. If the granularity of packaging is not right, the problem of high coupling between a large number of components will occur. The granularity of components can adjust the division of component responsibilities as the business changes. But calls between components are still inevitable, calling each other’s exposed interfaces. It is the responsibility of a well-designed route to reduce coupling between components.

3. How to design a route

To design a route that perfectly solves these two problems, let’s take a look at GitHub’s excellent open source library design ideas. Here are some routing schemes I found on Github, in descending order of Star. Analyze their respective design ideas in turn.

(1)JLRoutes Star 3189

JLRoutes has the most Star on Github, so let’s analyze its specific design ideas from it.

First, JLRoutes are affected by the URL Scheme idea. It treats all requests for resources as a URI.

Start by familiarizing yourself with the fields of NSURLComponent:

Note

The URLs employed by the NSURL

class are described in RFC 1808, RFC 1738, and RFC 2732.

JLRoutes passes in each string and splits it as above, taking each NSURLComponent according to the standard RFC definition.

JLRoutes globally stores a Map with Scheme as the Key and JLRoutes as the Value. So each scheme is unique in routeControllerMap.

As for why there are so many routes, the author believes that if routes are divided according to service lines, each service line may have different logic. Even though the components in each service may have the same name, different service lines may have different routing rules.

For example, if Didi is componentized according to taxi services in each city, then each city corresponds to each scheme here. Every city’s taxi-hailing business has taxi-hailing, payment… However, due to the different local laws and regulations in each city, these components may have different functions even if they have the same name. So there are multiple routes, which can also be interpreted as different namespaces.

An array is stored in each JLRoutes. This array holds each routing rule. JLRRouteDefinition holds block closures passed in from the outside, pattern, and the pattern after splitting.

The array of each JLRoutes is sorted by route priority, with the highest priority placed first.



- (void)_registerRoute:(NSString *)routePattern priority:(NSUInteger)priority handler:(BOOL(^) (NSDictionary *parameters))handlerBlock
{
    JLRRouteDefinition *route = [[JLRRouteDefinition alloc] initWithScheme:self.scheme pattern:routePattern priority:priority handlerBlock:handlerBlock];

    if (priority == 0 || self.routes.count == 0) {[self.routes addObject:route];
    } else {
        NSUInteger index = 0;
        BOOL addedRoute = NO;

        // Find an existing route whose priority is lower than that of the route to be inserted
        for (JLRRouteDefinition *existingRoute in [self.routes copy]) {
            if (existingRoute.priority < priority) {
                // If found, insert array
                [self.routes insertObject:route atIndex:index];
                addedRoute = YES;
                break;
            }
            index++;
        }

        // If no route is found with a lower priority than the current route to be inserted, or the last route has the same priority as the current route, only the last route can be inserted.
        if(! addedRoute) { [self.routes addObject:route]; }}}Copy the code

Since the routes in this array are a monotonic queue, finding the priority can only be traversed from the highest to the lowest.

The route search process is as follows:

First, initialize a JLRRouteRequest based on the external URL, and then use the JLRRouteRequest to request the current route array in turn. Each rule generates a response, but only the response that matches the conditions will match. Finally, fetch the matching JLRRouteResponse and fetch the corresponding parameters in its dictionary. The important code for lookup and matching is as follows:



- (BOOL)_routeURL:(NSURL *)URL withParameters:(NSDictionary *)parameters executeRouteBlock:(BOOL)executeRouteBlock
{
    if(! URL) {return NO;
    }

    [self _verboseLog:@"Trying to route URL %@", URL];

    BOOL didRoute = NO;
    JLRRouteRequest *request = [[JLRRouteRequest alloc] initWithURL:URL];

    for (JLRRouteDefinition *route in [self.routes copy]) {
        // Check each route to generate the corresponding response
        JLRRouteResponse *response = [route routeResponseForRequest:request decodePlusSymbols:shouldDecodePlusSymbols];
        if(! response.isMatch) {continue;
        }

        [self _verboseLog:@"Successfully matched %@", route];

        if(! executeRouteBlock) {// If we are asked to disallow execution, but find a matching route response.
            return YES;
        }

        // Set the final parameters
        NSMutableDictionary *finalParameters = [NSMutableDictionary dictionary];
        [finalParameters addEntriesFromDictionary:response.parameters];
        [finalParameters addEntriesFromDictionary:parameters];
        [self _verboseLog:@"Final parameters are %@", finalParameters];

        didRoute = [route callHandlerBlockWithParameters:finalParameters];

        if (didRoute) {
            // Calling Handler succeeded
            break; }}if(! didRoute) { [self _verboseLog:@"Could not find a matching route"];
    }

    // If no matching route is found in the current routing rule, the current route is not global and can be degraded to global, then we continue to look in the global routing rule.
    if(! didRoute &&self.shouldFallbackToGlobalRoutes && ! [self _isGlobalRoutesController]) {
        [self _verboseLog:@"Falling back to global routes..."];
        didRoute = [[JLRoutes globalRoutes] _routeURL:URL withParameters:parameters executeRouteBlock:executeRouteBlock];
    }

    // If no match is found, this closure will be called for final processing.

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

Here’s an example:

We will register a Router as follows:



[[JLRoutes globalRoutes] addRoute:@"/:object/:primaryKey" handler:^BOOL(NSDictionary *parameters) {
  NSString *object = parameters[@"object"];
  NSString *primaryKey = parameters[@"primaryKey"];
  // stuff
  return YES;
}];Copy the code

We pass in a URL and let the Router process it.


NSURL *editPost = [NSURL URLWithString:@"ele://post/halfrost? debug=true&foo=bar"];
[[UIApplication sharedApplication] openURL:editPost];Copy the code

After a match, we get a dictionary like this:


{
  "object": "post"."action": "halfrost"."debug": "true"."foo": "bar"."JLRouteURL": "ele://post/halfrost? debug=true&foo=bar"."JLRoutePattern": "/:object/:action"."JLRouteScheme": "JLRoutesGlobalRoutesScheme"
}Copy the code

To illustrate the above process, see the diagram below:

JLRoutes can also support Optional routing rules if you define a routing rule:


/the(/foo/:a)(/bar/:b)Copy the code

JLRoutes will help us register the following four routing rules by default:


/the/foo/:a/bar/:b
/the/foo/:a
/the/bar/:b
/theCopy the code

(2)routable-ios Star 1415

A Routable route is a URL router used in In-App native. It can be used on iOS or Android.

Two dictionaries are kept in UPRouter. The routes dictionary stores keys for routing rules and values for UPRouterOptions. CachedRoutes stores the Key as the final URL with passed parameters and the Value as the RouterParams. RouterParams will contain UPRouterOptions for routes matching, additional open parameters openParams and extra parameters extraParams.





- (RouterParams *)routerParamsForUrl:(NSString *)url extraParams: (NSDictionary *)extraParams {
    if(! url) {//if we wait, caching this as key would throw an exception
        if (_ignoresExceptions) {
            return nil;
        }
        @throw [NSException exceptionWithName:@"RouteNotFoundException"
                                       reason:[NSString stringWithFormat:ROUTE_NOT_FOUND_FORMAT, url]
                                     userInfo:nil];
    }

    if ([self.cachedRoutes objectForKey:url] && ! extraParams) {return [self.cachedRoutes objectForKey:url];
    }

   // Compare the number of parameters and the number of pathComponents after the URL is split by /
    NSArray *givenParts = url.pathComponents;
    NSArray *legacyParts = [url componentsSeparatedByString:@ "/"];
    if([legacyParts count] ! = [givenParts count]) {NSLog(@"Routable Warning - your URL %@ has empty path components - this will throw an error in an upcoming release", url);
        givenParts = legacyParts;
    }

    __block RouterParams *openParams = nil;
    [self.routes enumerateKeysAndObjectsUsingBlock:
     ^(NSString *routerUrl, UPRouterOptions *routerOptions, BOOL *stop) {

         NSArray *routerParts = [routerUrl pathComponents];
         if ([routerParts count] == [givenParts count]) {

             NSDictionary *givenParams = [self paramsForUrlComponents:givenParts routerUrlComponents:routerParts];
             if (givenParams) {
                 openParams = [[RouterParams alloc] initWithRouterOptions:routerOptions openParams:givenParams extraParams: extraParams];
                 *stop = YES; }}}];if(! openParams) {if (_ignoresExceptions) {
            return nil;
        }
        @throw [NSException exceptionWithName:@"RouteNotFoundException"
                                       reason:[NSString stringWithFormat:ROUTE_NOT_FOUND_FORMAT, url]
                                     userInfo:nil];
    }
    [self.cachedRoutes setObject:openParams forKey:url];
    return openParams;
}Copy the code

The main thing to do in this code is to iterate through the Routes dictionary and find a string that matches the parameters and return it as RouterParams.



- (NSDictionary *)paramsForUrlComponents:(NSArray *)givenUrlComponents routerUrlComponents:(NSArray *)routerUrlComponents {

    __block NSMutableDictionary *params = [NSMutableDictionary dictionary];
    [routerUrlComponents enumerateObjectsUsingBlock:
     ^(NSString *routerComponent, NSUInteger idx, BOOL *stop) {

         NSString *givenComponent = givenUrlComponents[idx];
         if ([routerComponent hasPrefix:@ ","]) {
             NSString *key = [routerComponent substringFromIndex:1];
             [params setObject:givenComponent forKey:key];
         }
         else if(! [routerComponent isEqualToString:givenComponent]) { params =nil;
             *stop = YES; }}];return params;
}Copy the code

The first argument to the above function is a split array of incoming URL arguments. The second parameter is an array of split routing rules. The first position in the routerComponent is the parameter name because the number follows the parameter name. In the params dictionary, the parameter is named Key and the parameter is Value.



 NSDictionary *givenParams = [self paramsForUrlComponents:givenParts routerUrlComponents:routerParts];
if (givenParams) {
       openParams = [[RouterParams alloc] initWithRouterOptions:routerOptions openParams:givenParams extraParams: extraParams];
       *stop = YES;
}Copy the code

UPRouterOptions, givenParams, and routerParamsForUrl: extraParams: The second input parameter to the method, which uses these three parameters as initialization parameters, generates a RouterParams.


[self.cachedRoutes setObject:openParams forKey:url];Copy the code

The last step, sell.cachedroutes, contains a dictionary where Key is a URL with an argument and Value is a RouterParams.

Finally, the matched RouterParams are converted into the corresponding Controller.



- (UIViewController *)controllerForRouterParams:(RouterParams *)params {
    SEL CONTROLLER_CLASS_SELECTOR = sel_registerName("allocWithRouterParams:");
    SEL CONTROLLER_SELECTOR = sel_registerName("initWithRouterParams:");
    UIViewController *controller = nil;
    Class controllerClass = params.routerOptions.openClass;
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
    if ([controllerClass respondsToSelector:CONTROLLER_CLASS_SELECTOR]) {
        controller = [controllerClass performSelector:CONTROLLER_CLASS_SELECTOR withObject:[params controllerParams]];
    }
    else if ([params.routerOptions.openClass instancesRespondToSelector:CONTROLLER_SELECTOR]) {
        controller = [[params.routerOptions.openClass alloc] performSelector:CONTROLLER_SELECTOR withObject:[params controllerParams]];
    }
#pragma clang diagnostic pop
    if(! controller) {if (_ignoresExceptions) {
            return controller;
        }
        @throw [NSException exceptionWithName:@"RoutableInitializerNotFound"
                                       reason:[NSString stringWithFormat:INVALID_CONTROLLER_FORMAT, NSStringFromClass(controllerClass), NSStringFromSelector(CONTROLLER_CLASS_SELECTOR),  NSStringFromSelector(CONTROLLER_SELECTOR)]
                                     userInfo:nil];
    }

    controller.modalTransitionStyle = params.routerOptions.transitionStyle;
    controller.modalPresentationStyle = params.routerOptions.presentationStyle;
    return controller;
}Copy the code

If Controller is a class, call the allocWithRouterParams: method to initialize it. If the Controller is already an instance, the initWithRouterParams: method is called to initialize it.

The general flow of Routable is illustrated as follows:

(3)HHRouter Star 1277

This is a Router for Pudding animation, inspired by ABRouter and Routable iOS.

Let’s take a look at the HHRouter Api. The method it provides is very clear.

The ViewController provides two methods. Map is used to set routing rules, matchController is used to match routing rules, and after matching, it returns the corresponding UIViewController.



- (void)map:(NSString *)route toControllerClass:(Class)controllerClass;
- (UIViewController *)matchController:(NSString *)route;Copy the code

The block closure provides three methods. Map also sets routing rules. MatchBlock: matches the route, finds the specified block, but does not call the block. CallBlock: finds the specified block and calls it immediately.



- (void)map:(NSString *)route toBlock:(HHRouterBlock)block;

- (HHRouterBlock)matchBlock:(NSString *)route;
- (id)callBlock:(NSString *)route;Copy the code

The difference between matchBlock: and callBlock: is that the former does not automatically invoke closures. So matchBlock: once the method finds the corresponding block, if it wants to call it, it needs to call it manually.

In addition to these methods, HHRouter provides a special method for us.


- (HHRouteType)canRoute:(NSString *)route;Copy the code

This method is used to find the RouteType of the routing rule. There are three routetypes:



typedef NS_ENUM (NSInteger, HHRouteType) {
    HHRouteTypeNone = 0,
    HHRouteTypeViewController = 1,
    HHRouteTypeBlock = 2
};Copy the code

Let’s see how HHRouter manages routing rules. The entire HHRouter is controlled by an NSMutableDictionary *routes.



@interface HHRouter(a)
@property (strong.nonatomic) NSMutableDictionary *routes;
@endCopy the code

The HHRouter is a very sophisticated route design, despite the seemingly simple dictionary data structure.



- (void)map:(NSString *)route toBlock:(HHRouterBlock)block
{
    NSMutableDictionary *subRoutes = [self subRoutesToRoute:route];
    subRoutes[@ "_"] = [block copy];
}

- (void)map:(NSString *)route toControllerClass:(Class)controllerClass
{
    NSMutableDictionary *subRoutes = [self subRoutesToRoute:route];
    subRoutes[@ "_"] = controllerClass;
}Copy the code

The above two methods are block closures and ViewController method entities that set routing rules to call. SubRoutesToRoute: method is called when setting rules for both ViewController and block closures.



- (NSMutableDictionary *)subRoutesToRoute:(NSString *)route
{
    NSArray *pathComponents = [self pathComponentsFromRoute:route];

    NSInteger index = 0;
    NSMutableDictionary *subRoutes = self.routes;

    while (index < pathComponents.count) {
        NSString *pathComponent = pathComponents[index];
        if(! [subRoutes objectForKey:pathComponent]) { subRoutes[pathComponent] = [[NSMutableDictionary alloc] init];
        }
        subRoutes = subRoutes[pathComponent];
        index++;
    }

    return subRoutes;
}Copy the code

The above function is a dictionary to construct routing matching rules.

Here’s an example:


[[HHRouter shared] map:@"/user/:userId/"
         toControllerClass:[UserViewController class]];
[[HHRouter shared] map:@"/story/:storyId/"
         toControllerClass:[StoryViewController class]];
[[HHRouter shared] map:@"/user/:userId/story/? a=0"
         toControllerClass:[StoryListViewController class]].Copy the code

After setting three rules, the route rule dictionary will look something like this:



{
    story =     {
        ":storyId" =         {
            "_" = StoryViewController;
        };
    };
    user =     {
        ":userId" =         {
            "_" = UserViewController;
            story =             {
                "_" = StoryListViewController;
            };
        };
    };
}Copy the code

After the routing rule dictionary is generated, the dictionary is traversed when a match is made.

Suppose there is a route coming:


  [[[HHRouter shared] matchController:@"hhrouter20://user/1/"] class].Copy the code

The HHRouter processes this route by matching the previous scheme first. If the scheme is incorrect, subsequent matching fails.

Then route matching is performed, and the parameter dictionary is generated as follows:



{
    "controller_class" = UserViewController;
    route = "/user/1/";
    userId = 1;
}Copy the code

The specific routing parameter matching function is in


- (NSDictionary *)paramsInRoute:(NSString *)routeCopy the code

It’s implemented in this method. This method is in accordance with the routing matching rules, the URL parameters are passed in one by one parsing out, with? All of them are parsed into dictionaries. This method is not difficult, I will not repeat.

The ViewController dictionary also adds two items by default:


"controller_class" = 
route =Copy the code

Route stores the full URL that was passed in.

What if an incoming route is followed by an access string? Let’s see:


[[HHRouter shared] matchController:@"/user/1/? a=b&c=d"]Copy the code

Parsing out all the arguments in the dictionary would look like this:


{
    a = b;
    c = d;
    "controller_class" = UserViewController;
    route = "/user/1/? a=b&c=d";
    userId = 1;
}Copy the code

Similarly, what if it’s a block closure?

Let’s add a routing rule for the block closure:



[[HHRouter shared] map:@"/user/add/"
                   toBlock:^id(NSDictionary* params) {
                   }];Copy the code

This rule generates a dictionary of routing rules.


{
    story =     {
        ":storyId" =         {
            "_" = StoryViewController;
        };
    };
    user =     {
        ":userId" =         {
            "_" = UserViewController;
            story =             {
                "_" = StoryListViewController;
            };
        };
        add =         {
            "_" = "<__NSMallocBlock__: 0x600000240480>";
        };
    };
}Copy the code

Notice that “_” is followed by a block.

There are two ways to match a block closure.


// 1. After the first method matches the corresponding block, you need to call the closure manually.
    HHRouterBlock block = [[HHRouter shared] matchBlock:@"/user/add/? a=1&b=2"];
    block(nil);


// 2. The second method automatically calls the closure after a block is matched.
    [[HHRouter shared] callBlock:@"/user/add/? a=1&b=2"];Copy the code

The matched parameter dictionary is as follows:


{
    a = 1;
    b = 2;
    block = "<__NSMallocBlock__: 0x600000056b90>";
    route = "/user/add/? a=1&b=2";
}Copy the code

By default, block’s dictionary adds the following two items:


block = 
route =Copy the code

Route stores the full URL that was passed in.

The generated parameter dictionary is eventually bound to the Associated Object of the ViewController.



- (void)setParams:(NSDictionary *)paramsDictionary
{
    objc_setAssociatedObject(self, &kAssociatedParamsObjectKey, paramsDictionary, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (NSDictionary *)params
{
    return objc_getAssociatedObject(self, &kAssociatedParamsObjectKey);
}Copy the code

The binding process takes place when the match match is complete.




- (UIViewController *)matchController:(NSString *)route
{
    NSDictionary *params = [self paramsInRoute:route];
    Class controllerClass = params[@"controller_class"];

    UIViewController *viewController = [[controllerClass alloc] init];

    if ([viewController respondsToSelector:@selector(setParams:)]) {
        [viewController performSelector:@selector(setParams:)
                             withObject:[params copy]];
    }
    return viewController;
}Copy the code

The ViewController we end up with is what we want. The corresponding parameters are in the dictionary of the params attribute it is bound to.

Diagram the above process as follows:

(4)MGJRouter Star 633

This is a routing method for Mushroom Street.

The origin of this library:

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.

In terms of data structure, MGJRouter is the same as HHRouter.


@interface MGJRouter(a)
@property (nonatomic) NSMutableDictionary *routes;
@endCopy the code

Let’s take a look at some of the improvements it makes to the HHRouter.

1. If the MGJRouter supports openURL, you can send userInfo to the MGJRouter

[MGJRouter openURL:@"mgj://category/travel" withUserInfo:@{@"user_id": @1900} completion:nil];Copy the code

The HHRouter does not support dictionary parameters, but the URL Query Parameter can be used after the URL.



    if (parameters) {
        MGJRouterHandler handler = parameters[@"block"];
        if (completion) {
            parameters[MGJRouterParameterCompletion] = completion;
        }
        if (userInfo) {
            parameters[MGJRouterParameterUserInfo] = userInfo;
        }
        if (handler) {
            [parameters removeObjectForKey:@"block"]; handler(parameters); }}Copy the code

MGJRouter’s handling of the userInfo is encapsulate it directly to the Key = MGJRouterParameterUserInfo corresponds to the Value.

2. Support Chinese urls.

    [parameters enumerateKeysAndObjectsUsingBlock:^(id key, NSString *obj, BOOL *stop) {
        if ([obj isKindOfClass:[NSString class]]) {
            parameters[key] = [obj stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; }}];Copy the code

This is where you have to pay attention to the coding.

3. Define a global URL Pattern as a Fallback.

This is modeled after the idea that the mismatch of JLRoutes will automatically degrade to Global.


    if (parameters) {
        MGJRouterHandler handler = parameters[@"block"];
        if (handler) {
            [parameters removeObjectForKey:@"block"]; handler(parameters); }}Copy the code

Parameters dictionary will first store the next routing rule, which is stored in the block closure. When matching, this handler will be taken out, and the matching will be degraded to this closure for final processing.

4. When the OpenURL ends, you can execute the Completion Block.

In MGJRouter, the author modified the structure of routing rules stored in the original HHRouter dictionary.


NSString *const MGJRouterParameterURL = @"MGJRouterParameterURL";
NSString *const MGJRouterParameterCompletion = @"MGJRouterParameterCompletion";
NSString *const MGJRouterParameterUserInfo = @"MGJRouterParameterUserInfo";Copy the code

Each of these keys holds some information:

MGJRouterParameterURL Specifies the complete URL information that is saved and transmitted. MGJRouterParameterCompletion saved is completion closures. MGJRouterParameterUserInfo save the UserInfo dictionary.

Here’s an example:



    [MGJRouter registerURLPattern:@"ele://name/:name" toHandler:^(NSDictionary *routerParameters) {
        void (^completion)(NSString *) = routerParameters[MGJRouterParameterCompletion];
        if (completion) {
            completion(@" Done"); }}]; [MGJRouter openURL:@"ele://name/halfrost/? age=20" withUserInfo:@{@"user_id": @1900} completion:^(id result) {
        NSLog(@"result = %@",result);
    }];Copy the code

The URL above matches successfully, and the resulting parameter dictionary has the following structure:


{
    MGJRouterParameterCompletion = "<__NSGlobalBlock__: 0x107ffe680>";
    MGJRouterParameterURL = "ele://name/halfrost/? age=20";
    MGJRouterParameterUserInfo =     {
        "user_id" = 1900;
    };
    age = 20;
    block = "<__NSMallocBlock__: 0x608000252120>";
    name = halfrost;
}Copy the code
5. You can manage urls in a unified manner

This feature is very useful.

URL processing is not careful, easy to scattered in all corners of the project, not easy to manage. For example, when registering, the pattern is MGJ ://beauty/: ID, and when opening, it is MGJ ://beauty/123. In this way, if the URL is changed, it will be very troublesome to deal with and difficult to manage uniformly.

So MGJRouter provides a class method to handle this problem.


#define TEMPLATE_URL @"qq://name/:name"

[MGJRouter registerURLPattern:TEMPLATE_URL  toHandler:^(NSDictionary *routerParameters) {
    NSLog(@"routerParameters[name]:%@", routerParameters[@"name"]); // halfrost
}];

[MGJRouter openURL:[MGJRouter generateURLWithPattern:TEMPLATE_URL parameters:@[@"halfrost"]]];
}Copy the code

GenerateURLWithPattern: replaces all of the: in the macro we defined with the following array of strings, assigning values in turn.

Diagram the above process as follows:

In order to distinguish between page to page calls and component to component calls, Mogulstreet came up with a new method. The Protocol method is used to make inter-component calls.

There is an Entry between each component. This Entry does three things:

  1. Register the URL that this component cares about
  2. Registers methods/properties that this component can call
  3. Respond differently at different stages of the App lifecycle

OpenURL calls between pages look like this:

Each component will register with MGJRouter, and components can call each other or other apps through openURL: method to open an interface or call a component.

Mogustreet uses Protocol for calls between components.

[ModuleManager registerClass:ClassA forProtocol:ProtocolA] adds a new mapping to the MM’s internal dict.

[ModuleManager classForProtocol: ProtocolA] before returning to result in MM internal dict protocol corresponding class, use the party don’t need to care about this class is what east east, It implements the ProtocolA anyway, so just use it.

There needs to be a public place to hold these public ProtoCls, which is publicProtocl.h in the diagram.

My guess is that the implementation might look something like this:



@interface ModuleProtocolManager : NSObject

+ (void)registServiceProvide:(id)provide forProtocol:(Protocol*)protocol;
+ (id)serviceProvideForProtocol:(Protocol *)protocol;

@endCopy the code

Then this is a singleton in which each protocol is registered:


@interface ModuleProtocolManager(a)

@property (nonatomic.strong) NSMutableDictionary *serviceProvideSource;
@end

@implementation ModuleProtocolManager

+ (ModuleProtocolManager *)sharedInstance
{
    static ModuleProtocolManager * instance;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        instance = [[self alloc] init];
    });
    return instance;
}

- (instancetype)init
{
    self = [super init];
    if (self) {
        _serviceProvideSource = [[NSMutableDictionary alloc] init];
    }
    return self;
}

+ (void)registServiceProvide:(id)provide forProtocol:(Protocol*)protocol
{
    if (provide == nil || protocol == nil)
        return;
    [[self sharedInstance].serviceProvideSource setObject:provide forKey:NSStringFromProtocol(protocol)];
}

+ (id)serviceProvideForProtocol:(Protocol *)protocol
{
    return [[self sharedInstance].serviceProvideSource objectForKey:NSStringFromProtocol(protocol)];
}Copy the code

A dictionary is used to store each registered protocol in the ModuleProtocolManager. Now guess the ModuleEntry implementation.



#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>

@protocol DetailModuleEntryProtocol <NSObject>

@required;
- (UIViewController *)detailViewControllerWithId:(NSString*)Id Name:(NSString *)name;
@endCopy the code

Each module then has a “connector” connected to the exposed protocol.



#import <Foundation/Foundation.h>

@interface DetailModuleEntry : NSObject
@endCopy the code

In its implementation, the need to introduce three external files, one is ModuleProtocolManager, one is DetailModuleEntryProtocol, the last is a component modules need to jump or call or page.



#import "DetailModuleEntry.h"

#import <DetailModuleEntryProtocol/DetailModuleEntryProtocol.h>
#import <ModuleProtocolManager/ModuleProtocolManager.h>
#import "DetailViewController.h"

@interface DetailModuleEntry()"DetailModuleEntryProtocol>

@end

@implementation DetailModuleEntry

+ (void)load
{
    [ModuleProtocolManager registServiceProvide:[[self alloc] init] forProtocol:@protocol(DetailModuleEntryProtocol)];
}

- (UIViewController *)detailViewControllerWithId:(NSString*)Id Name:(NSString *)name
{
    DetailViewController *detailVC = [[DetailViewController alloc] initWithId:id Name:name];
    return detailVC;
}

@endCopy the code

The Protocol-based scheme is now complete. If you need to invoke a component or jump to a page, find the DetailModuleEntry based on the ModuleEntryProtocol in the ModuleProtocolManager dictionary. Finding the DetailModuleEntry finds the component or page’s “entry”. Just pass in the parameters.




- (void)didClickDetailButton:(UIButton *)button
{
    id< DetailModuleEntryProtocol > DetailModuleEntry = [ModuleProtocolManager serviceProvideForProtocol:@protocol(DetailModuleEntryProtocol)];
    UIViewController* detailVC = [DetailModuleEntry detailViewControllerWithId: @ "details interface Name:" @ "my cart"]. [self.navigationController pushViewController:detailVC animated:YES];

}Copy the code

This allows you to call the component or interface.

If components have the same interfaces, you can further separate them out. These detached interfaces become “meta-interfaces” that are large enough to support the entire component layer.

(5)CTMediator Star 803

Then there is @Casatwy’s proposal, which is based on Mediator.

The traditional Mediator model looks like this:

In this mode, every page or component will depend on Mediator, and each component will no longer depend on each other. The call between components will only rely on Mediator, and Mediator will still depend on other components. So is this the final plan?

See how @Casatwy continues to optimize.

The main idea is to take advantage of the simple and crude idea of target-Action and use Runtime to solve the decoupling problem.


- (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params shouldCacheTarget:(BOOL)shouldCacheTarget
{

    NSString *targetClassString = [NSString stringWithFormat:@"Target_%@", targetName];
    NSString *actionString = [NSString stringWithFormat:@"Action_%@:", actionName];
    Class targetClass;

    NSObject *target = self.cachedTarget[targetClassString];
    if (target == nil) {
        targetClass = NSClassFromString(targetClassString);
        target = [[targetClass alloc] init];
    }

    SEL action = NSSelectorFromString(actionString);

    if (target == nil) {
        // This is one of the places to handle non-responsive requests. The demo is made simple, so if there is no target to respond to, just return. In practice, a fixed target could be assigned in advance to handle the request at this time
        return nil;
    }

    if (shouldCacheTarget) {
        self.cachedTarget[targetClassString] = target;
    }

    if ([target respondsToSelector:action]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
        return [target performSelector:action withObject:params];
#pragma clang diagnostic pop
    } else {
        // It is possible that target is a Swift object
        actionString = [NSString stringWithFormat:@"Action_%@WithParams:", actionName];
        action = NSSelectorFromString(actionString);
        if ([target respondsToSelector:action]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
            return [target performSelector:action withObject:params];
#pragma clang diagnostic pop
        } else {
            // This is where the non-response request is handled. If there is no response, try calling the notFound method corresponding to target
            SEL action = NSSelectorFromString(@"notFound:");
            if ([target respondsToSelector:action]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
                return [target performSelector:action withObject:params];
#pragma clang diagnostic pop
            } else {
                // This is also where unresponsive requests are handled. Without notFound, the demo returns directly. In practice, you can use the fixed target above mentioned.
                [self.cachedTarget removeObjectForKey:targetClassString];
                return nil; }}}}Copy the code

TargetName is the Object that calls the interface, actionName is the SEL that calls the method, params is the parameter, shouldCacheTarget is whether or not to cache, if it needs to cache, store the target. Key is targetClassString and Value is target.

Call performTarget: action: params: shouldCacheTarget:. The third parameter is a dictionary, which can pass many parameters, as long as the key-value is written. Methods for handling errors are unified in one place. If a target does not have one, or if it cannot respond to a method, it can handle errors at Mediator.

However, in the actual development process, no matter interface call or intercomponent call, many methods need to be defined in Mediator. Therefore, the author suggested that we use the Category method to split all methods of Mediator, so that the class of Mediator would not be too large.


- (UIViewController *)CTMediator_viewControllerForDetail
{
    UIViewController *viewController = [self performTarget:kCTMediatorTargetA
                                                    action:kCTMediatorActionNativFetchDetailViewController
                                                    params:@{@"key":@"value"}
                                         shouldCacheTarget:NO
                                        ];
    if ([viewController isKindOfClass:[UIViewController class]]) {
        // After the View Controller is delivered, it can be selected by the outside world as push or present
        return viewController;
    } else {
        // This is where exception scenarios are handled, depending on the product
        return [[UIViewControlleralloc] init]; }} - (void)CTMediator_presentImage: (UIImage *)image
{
    if (image) {
        [self performTarget:kCTMediatorTargetA
                     action:kCTMediatorActionNativePresentImage
                     params:@{@"image":image}
          shouldCacheTarget:NO];
    } else {
        // The scenario where image is nil is handled, depending on the product
        [self performTarget:kCTMediatorTargetA
                     action:kCTMediatorActionNativeNoImage
                     params:@{@"image": [UIImage imageNamed:@"noImage"]}
          shouldCacheTarget:NO]; }}Copy the code

I’m just going to put each of these methods in a Category, and they’re all called the same way, performTarget: Action: params: shouldCacheTarget:.

Finally, the dependence of mediators on components was removed, and each component no longer depended on each other. The call between components only depended on mediators, while mediators did not depend on any other components.

(6) Some solutions are not open source

In addition to the open source routing schemes above, there are some that are not as well designed as open source. Here you can analyze and communicate with us.

This solution is a solution of the Uber rider App.

Uber considered replacing the architecture with VIPER after finding some disadvantages of MVC, such as the huge fat VC with tens of thousands of lines and the inability to conduct unit testing. But VIPER also has certain disadvantages. Because of its ios-specific structure, iOS has to make some trade-offs for Android. View-driven application logic, which means that application state is view-driven and the entire application is locked in the view tree. Changes to the business logic associated with operating application state must go through Presenter. Thus exposing business logic. The result is a tight coupling between the view tree and the business tree. This makes it very difficult to implement a Node that only has business logic or only has view logic.

By improving VIPER architecture, absorbing its excellent features and improving its shortcomings, a new architecture of Uber rider App — Riblets(Ribs) was formed.

In this new architecture, even similar logic is divided into small, independent components that can be tested separately. Each component has a very specific purpose. Using these bits and pieces of Riblets, the entire App is eventually stitched together into a Riblets tree.

Through abstraction, a Riblets(ribs) is defined as the following six smaller components, each of which has its own responsibilities. Further abstracts the business logic and view logic through a Riblets(ribs).

A Riblets is designed like this, how is that different from VIPER and MVC before that? The biggest difference is in routing.

The Router inside the Riblets(ribs) is no longer view logic driven, but now business logic driven. As a result of this major change, the entire App is no longer presentation-driven, but now data-driven.

Each Riblet consists of a Router, an Interactor, a Builder, and their associated components. Hence its name (router-interactor-Builder, Rib). There are also optional presenters and views. Routers and interactors handle business logic, while presenters and views handle View logic.

Focus on the responsibilities of routing in Riblet.

1. Route responsibilities

Routing is responsible for associating and disassociating other child Riblets in the entire App structure tree. The decision is passed by the Interactor. In the process of state transition, routing will also affect the life cycle of the Interactor associator when associating and disassociating sub-riblets. The route contains only two business logic:

1. Provide methods for associating and disassociating other routes. 2. State transition logic that determines the final state between multiple children.

2. Assemble

Each Riblets has only a pair of Router routes and Interactor associators. But they can have multiple pairs of views. Riblets deals only with business logic, not view-specific parts. Riblets can have a single View (one Presenter Presenter and one View View), multiple views (one Presenter Presenter and multiple View views, or multiple Presenter Presenter and multiple View views), You can even have no View (no Presenter and no View). This design can be useful for business logic tree construction, but also good separation from the view tree.

For example, the rider Riblet is a Riblet without a view that checks if the current user has an active route. If the rider determines the route, the Riblet is associated with the Riblet of the route. The Riblet of the route shows the route diagram on the map. If no route is determined, the rider’s Riblet is associated with the requested Riblet. The requested Riblet is displayed on the screen waiting to be called. Riblets like Rider’s Riblet, which have no view logic at all, separate the business logic and play a major role in driving the App and underpinning the modular architecture.

3. How does Riblets work

Data flow in Riblet

In this new architecture, data flows are one-way. Data streams generate Model streams from service service streams to Model streams. The Model Stream flows from the Model Stream to the Interactor correlator. Interactor correlator, scheduler, and remote push can all trigger changes to a Service to cause changes to the Model Stream. Model Stream generates models that are immutable. This mandatory requirement causes the correlator to change the state of the App only through the Service layer.

Two examples:

  1. A state change of data from background to View View causes server background to trigger push to App. The data is pushed to the App, and an immutable stream of data is generated. Once the correlator receives the Model, it passes it to the Presenter. The Presenter converts the Model into a View model and passes it to the view.

  2. Data goes from the view to the server background when the user clicks a button, such as the login button. The View triggers UI events that are passed to the Presenter. Presenter invokes the associator Interactor login method. The Interactor, in turn, calls the actual login method of the Service Call. After requesting the network, the data is pulled to the backend server.

Data flow between riblets

When an Interactor needs to call the events of other riblets in a project dealing with business logic, the Interactor needs to be associated with its child Interactor. See the 5 steps above.

If the calling method calls a parent class from a child, the Interactor interface of the parent class is usually defined as a listener. If the calling method is called from the parent class to the child class, then the child class’s interface is usually a delegate that implements some Protocol from the parent class.

In Riblet’s scenario, the Router is only used to maintain a tree relationship, while the Interactor plays the role of deciding which logical jumps to trigger between components.

Five. Advantages and disadvantages of each scheme

URLRoute -> protocol-class -> target-action route ->Target-Action This is also a gradual process of getting into the essence.

1. Advantages and disadvantages of URLRoute registration scheme

First of all, URLRoute may be a method derived from the way of front-end Router and jump in system App. It requests resources through a URL. Whether H5, RN, Weex, iOS interfaces or components request resources in the same way. The URL will also take parameters, so that you can call any interface or component. So this is the easiest way and the first one you can think of.

URLRoute has many advantages. The biggest advantage of URLRoute is that the server can dynamically control page redirection, process errors after a page failure in a unified manner, and unify the request modes of the three terminals, iOS, Android, H5, RN, and Weex.

But this approach also depends on the needs of different companies. If the company has completed the scaffolding tool dynamically delivered by the server side and the front-end has completed the Native end, the same business interface can be replaced at any time if there is an error, then the probability of choosing URLRoute will be greater at this time.

But H5 developers feel burdened if the company doesn’t have an H5 interface that can be replaced if something goes wrong. If the company does not have a system that dynamically delivers routing rules to the server, then the company may not use URLRoute. Because of the small amount of dynamism that URLRoute brings, companies can do this with JSPatch. – Online bugs can be fixed immediately with JSPatch instead of using URLRoute.

Therefore, the choice of URLRoute scheme depends on the company’s development, personnel allocation and technology selection.

The URLRoute scheme also has some disadvantages. First, the URL map rules need to be registered, and they will be written in the load method. Writing it in the load method will affect the startup speed of the App.

The second is a lot of hard coding. The names of the components and pages in the URL links are hard-coded, as are the parameters. And each URL parameter field must be maintained by a document, which is also a burden for business developers. In addition, URL short links are scattered all over the App, which is really a bit troublesome to maintain. Although Mogujie thought of unified management of these links with macros, it still could not solve the problem of hard coding.

A really good route serves the whole App invisibly, which is a process without perception. From this point, it is a little missing.

The final disadvantage is that the URL is not friendly enough to pass NSObject parameters, which at most pass a dictionary.

2. Advantages and disadvantages of the protocol-class registration scheme

Advantages of the protocol-class scheme, which is not hard-coded.

The protocol-class scheme also has some disadvantages, as each Protocol must be registered with the ModuleManager.

In this scenario, the ModuleEntry needs to depend on both the ModuleManager and the pages inside the component or both. ModuleEntry can also depend on ModuleEntryProtocol, but this dependency can be removed, such as using the Runtime NSProtocolFromString method and hard coding to remove Protocol dependencies. However, considering that hard coding is not friendly to bugs and later maintenance, the dependency on Protocol should not be removed.

A final disadvantage is that component method calls are scattered and there is no uniform entry point, which makes it impossible to do the same thing if the component doesn’t exist or if an error occurs.

3. Advantages and disadvantages of target-Action

The advantage of target-Action is that it takes full advantage of Runtime features without registering this step. The target-Action solution only has a layer of dependency on Mediator. Classes for Mediators are maintained in mediators, with each Category corresponding to a Target. Methods in Categroy correspond to Action scenes. The target-Action scheme also unifies the call entry between all components.

The target-action scheme can also provide a certain security guarantee by verifying the Native prefix in the URL.

The disadvantage of target-action is that Target_Action packages regular parameters as dictionaries in a Category and unpacks dictionaries as regular parameters at Target, resulting in some hard coding.

4. How to split components?

This is a question that should have been considered before any attempt was made to implement componentization. Why put it here? Because the component split each company has its own split scheme, according to the business line split? According to the smallest business function module disassembly? Or is it split by a finished feature? This involves the resolution of the thickness of the problem. The degree of component split is directly related to the degree of decoupling needed in future routing.

Suppose you package all the logins into one component. Since logins involve multiple pages, they are all packaged into one component. When other modules need to call the login state, they need to use the interface exposed by the login component to obtain the login state. At this point you can consider writing these interfaces to the Protocol and exposing them to the outside world. Or target-action. The granularity is a bit coarser if you divide all of a function into login components.

If you just split the details of the login state into a meta-component, you can call this component directly if you want to get the login state. The granularity of this division is very fine. This results in a huge number of components.

So when you split the components, maybe when the business wasn’t complicated, you split the components, and there wasn’t much coupling. However, as the business changes, the coupling between the previously divided components becomes more and more, so it is considered to continue to separate the previously divided components. Some businesses may be cut, and some of the smaller components may still be put together. In short, the partitioning of components may continue until the business is fully anchored.

Six. The best plan

In terms of architecture, I don’t think it makes sense to talk about architecture without talking about business. Because architecture serves the business, talking about architecture is an ideal state. So there is no best plan, only the most suitable plan.

The best solution is the one that works best for your business. Divide and rule, choose different solutions for different businesses is the best solution. If you have to adopt a general plan, different businesses need the same plan, need to compromise too much is not good.

I hope this article can help you choose the most suitable routing scheme for your business. Of course, there must be more excellent scheme, I hope you can give me more advice.

References:

Implementing THE componentization scheme based on CTMediator in existing projects: iOS Application ARCHITECTURE discussion on THE componentization scheme RIDER APP