Code Monkey home page Code Monkey logo

route-matcher's Introduction


Route Matcher

This library will match routes in routable structures consisting of nested routers and routes. By extracting this logic out of the routes and routers themselves, we gain more power and flexibility. While traversing the routers and routes we can maintain a context stack telling us where we are in the structure and how to treat each route and router with respect to its current context, not just on its own in isolation.

Design

See the [Design.md] document

This fork offers the following extra features:

Router and Route have been split up into several small grouped API objects, to be found in /src:

  • /routable : Base prototypes (classes)
  • /route : Route APIs and helpers
  • /router : Router APIs and helpers
  • /signal : Signal APIs and helpers
  • /util : Utility APIs

The idea is to not force you in to having to use all the "bells & whistles", but instead allow you to compose your Router and Route APIs from some "sane" building blocks. Feel free to come with ideas for improvements on the design.

Build

This project uses webpack as the package tool, in order to support:

  • CommonJS
  • RequireJS

Gulp used to build the distribution file in dist/crossroads.js

Build:

gulp webpack

Will use dev/src/crossroads.js as the entry point and follow the require paths to create a dependency graph, which will be resolved in order to find the best way to concatenate the files into one. Webpack then creates a bundled file which is output to dist/crossroads.js

Prototype design

Base functionality for Router and Route is defined in BaseRoute Any Route/Router which can add routes to itself is a CompositeRoute. Here is an illustration of the basic Prototype (class) hierarchy.

var MyNestedRoute < extend(Route, CompositeRoute, ChildRoute)

var FullRouter = Xtender.extend(RouteComposer, RequestParser, RouteMatcher, RouterPiper, RouterSignals);
Crossroads.prototype  = Xtender.extend(Crossroads.prototype, FullRouter, ErrorHandler);

Xtender.extend is used to extend an Object (uses xtend by default). You can override this function to provide your own extension mechanism.

Here an example of composing your own prototypes:

Xtender.extend(Route.prototype, BaseRoutable.prototype);
Xtender.extend(Route.prototype, RouteApi);

var RouteApi = {
  // Route specific methods...
}

Xtender.extend(CompositeRoute.prototype, BaseRoutable.prototype);
Xtender.extend(Route.prototype, CompositeRoute.prototype;
Xtender.extend(Router.prototype, RouterApi);

var RouterApi = {
  // Route specific methods...
}

Route nesting

The Goal is to use the Composite pattern. Both Router and Roue are Composites, since a Route can have nested routes mounted. A router can pipe to another route if no routes were activated on that router.

Adding or mounting multiple routes

The method addRoutes has been added to both the Router and the Route, to allow for an array of routes to be added or even adding/attaching/mounting all the routes of a Router.

Routes information

getRoutesBy can be used to retrieve information about the routes registered on a given Router or Route. Example: router.getRoutesBy('pattern', 'priority').

You can also have this info displayed as a string by chaining a display() call: router.getRoutesBy().display()

parentRoute() will get the parent route of a mounted/nested route. getRoutes() will get all the routes of a Router or all the child routes mounted on a Route. Note: It does not return all the routes in the nested sub-tree.

Custom request transformations

The Router contains a method _buildRequest(request) which is called by parse to allow you to transform the request before parsing it. This can be useful if you want to allow the Router to be routed from other data providers than the URL. An example could be to route using some user settings or some incoming data that affects what the user should see etc.

If you have a collaborative app, a particular user might be able to control what other users will see, and that even/action could be fed into the router. Your imagination is the only limit ;)

Pending activation

When a route has been successfully activated it can return a status indicating it is performing a "long-running operation". If this is the case the Router will call handlePendingActivation(route). You can override this function to provide custom handling of some sort, such as showing a loading status, progress bar etc.

To determine if the activation is pending, you can override the _isPending(activateResult) function on the router (by default it currently always returns false).

Signals

The Route has the following default Signal strategy:

_defaultSignalStrategy : function(signalName, request) {
  if (_hasActiveSignal(this[signalName])) {
    this[switchName](request);
    return true;
  }
  var args = this._defaultSignalArgs(request)
  if (this._parent) {
    _delegateSignal(signalName, this._parent, args);
  } else {
    _delegateSignal(signalName, this._router, args);
  }
},

It will pass any signal or delegate a hash with the route that activated and the incoming request. The strategy first checks if the route itself has a listener for that signal. If so it will use that signal and return. Otherwise it will delegate to the parent route if it is delegatable and finally fallback to calling the router itself to take care of handling the signal.

The signals are:

Activation:

  • couldActivate
  • wasActivated
  • couldntActivate

Switching:

  • couldSwitch
  • wasSwitched
  • couldntSwitch

Deactivation:

  • wasDeactivated

Methods you can override for custom functionality:

  • willSwitch (when switch is called)

  • canSwitch:boolean

  • cannotSwitch:void

  • didSwitch (after switching has been initiated)

  • willActivate

  • canActivate:boolean

  • cannotActivate:void

  • didActivate

  • deactivate (extend)

  • deactivated (extend)

You can set the Route constructor "class" on the Router via router._RouteClass = MyRoute This allows you to easily create a custom Route class where you extend the base Route and have the router use this custom class whenever you add a route via addRoute or addRoutes. Splendid!

Piped routers

A router can contain one or more piped routers. If no routes match for a given request, the router will try each of the piped routes in succession until one of them matches.

A piped router has access to its parent via getParent(). Should we should support a router having multiple parents? I think for now it is better to limit to one parent only.

A router can have a base route. when routes are added they should not know about the base route, but when evaluated as part of match, they should calculate their full route pattern by applying baseRoute() up the parent hierarchy. This way you can mount/dismount Routers easily without having to recalculate routes each time. I guess the full route name could be cached on first match attempt? Then clean cache when dismounted or remounted...

In order to achieve this, we need to "operate" on the route matching lv.

function Route(pattern, callback, priority, router, name) {
    this._router = router;
    this._name = name || 'unknown';
    this._pattern = pattern;
    this._priority = priority || 0;

    this._lexPattern();

_lexPattern: function() {
  var isRegexPattern = isRegExp(this._pattern),
      patternLexer = this._router.patternLexer,
      pattern = this._pattern,
      router = this.router;


  this._paramsIds = isRegexPattern? null : patternLexer.getParamIds(pattern);
  this._optionalParamsIds = isRegexPattern? null : patternLexer.getOptionalParamsIds(pattern);
  this._matchRegexp = isRegexPattern? pattern : patternLexer.compilePattern(pattern, router.ignoreCase);
  this._matchRegexpHead = isRegexPattern? pattern : patternLexer.compilePattern(pattern, router.ignoreCase, true);
},

It turns out that _matchRegexp are ilk are defined on route creation. If we want it to take into consideration the base route pattern where the route is mounted, we need to instead lazily evaluate on match. Here we assume we have a getPattern() method on the route which return this.baseRoute() + this._pattern;

However, going down this path, we would have to pass the router context around to all route methods. Not exactly elegant or maintainable.

isRegexPattern: function(router) {
  router = router || this._router;
  isRegExp(this.getPattern(router))
},

getMatchRegexp: function (router) {
  router = router || this._router;
  var pattern = this.getPattern(router);
  return isRegexPattern(router)? pattern : router.patternLexer.compilePattern(pattern, router.ignoreCase);
},

If we allow a route to be mounted on multiple different routers, we need to pass in the current context, ie. the current router instance we are matching for. The match function then becomes something like...

  // pass in router for which we are matching
  match : function (request, router) {
      request = request || '';
      //validate params even if regexp because of `request_` rule.
      return this.getMatchRegexp(router).test(request) && this._validateParams(request);
  },

Another, more sensible approach would be to have another class be the controller for the matching of routes. We could call it RoutingController. As it matches routes on a Router and calls route.match(), it should do it as a "transaction" such that route._activeRouter is set to the current router just before it starts and set back to null when done. In fact, to support these kinds of mechanics we need a thorough refactoring, which should be possible using the current fragmented design.

It could look sth like this. A much more flexible design than "inlining" the routing functionality inside the Router and Route themselves.

function RoutingController(router) {
  this.router = router;
}

RoutingController.prototype = {
  route: function(request) {
    this.requestParser(request).parse(this.router);
  },

  requestParser: function(request) {
    return new RequestParser(request, this.routeMatcher(request));
  },

  routeMatcher: function(request) {
    return new RouteMatcher(request);
  }
};

Another question with regards to mounting, is whether we should mount the original instance or mount a clone. If we always mount a clone, we would simplify it a lot and be able to finetune the route on each mounted note without affecting the other mounted instances. This makes a lot of sense! However in other cases there might be good reason to be able to have one re-configuration of a route have the same effect in multiple places it is used. So for good measure, we should allow both approaches. The default should be not to clone however!

router.mount(route, clone: true)

Authenticating and Authorizing routes

The route methods canActivate:boolean and canSwitch:boolean can be used to guard the route from activation and/or switching (redirect). Add any auth logic you like here. You can also centralize the logic at a higher level, such as a parent route or root route or even on the router using the same methods. Use the _defaultSignalStrategy for this.

You might consider using permit-authorize to provide the Authorization logic (guards) for your routes. We recommend using either a centralized approach (routes delegating to the router) or a decorator approach (decorating routes with individual auth logic).

Links

Dependencies

This library requires JS-Signals to work.

However it has been now been re-designed to make it super easy to decouple it from JS-Signals, to provide your own messaging mechanism.

License

MIT License

Distribution Files

Files inside dist folder.

  • crossroads.js : Uncompressed source code with comments.
  • crossroads.min.js : Compressed code.

You can install Crossroads on Node.js using NPM

npm install crossroads

Repository Structure

Folder Structure

dev       ->  development files
|- lib          ->  3rd-party libraries
|- src          ->  source files
|- tests        ->  unit tests
dist      ->  distribution files

Branches

master      ->  always contain code from the latest stable version
release-**  ->  code canditate for the next stable version (alpha/beta)
dev         ->  main development branch (nightly)
gh-pages    ->  project page
**other**   ->  features/hotfixes/experimental, probably non-stable code

Building your own

This project uses Node.js for the build process. If for some reason you need to build a custom version install Node.js and run:

node build

This will delete all JS files inside the dist folder, merge/update/compress source files and copy the output to the dist folder.

IMPORTANT: dist folder always contain the latest version, regular users should not need to run build task.

Running unit tests

On the browser

Open dev/tests/spec_runner-dist.html on your browser.

spec_runner-dist tests dist/crossroads.js and spec_runner-dev tests files inside dev/src - they all run the same specs.

On Node.js

Install npm and run:

npm install --dev
npm test

Each time you run npm test the files inside the dist folder will be updated (it executes node build as a pretest script).

route-matcher's People

Contributors

kristianmandrup avatar

Watchers

 avatar  avatar  avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.