Dynamic Route Loading in Angular 2 Fails. (Beta)

2019-02-15 16:25发布

问题:

I want to load routes for the @RouteConfig Dynamically from a service which fetches in format JSON,

[
  { "path" : "/about" , "name" : "About" , "component" : "AboutComponent" },
  { "path" : "/contact" , "name" : "Contact" , "component" : "ContactComponent" }
]

Following is the code for pushing it into the RouteDefinition Array,

for (let i = 0; i < data.length; i++) {
  //console.log(data[i].path+" "+data[i].name+" "+data[i].component);
  this.routeConfigArray.push({ //routeConfigArray : RouteDefinition
    'path': data[i].path,
    'name': data[i].name,
    'component': data[i].component
  });
  this._router.config(this.routeConfigArray);   // THIS FAILS TO CONFIG PATHS ON ROUTER
}

The 'component':data[i].component requires Classname, where as it recieves a it via a String class. How do i convert the string containing classname into a class??

Also i have tried the Route class using :

this.routeConfigArray.push(new Route({path: data[i].path, name: data[i].name, component:data[i].component}));

Console Error:

Component for route "/about" is not defined, or is not a class. i have tried numerous ways like using eval("new "+data[i].component+"()); || new window[data[i].component] .

I am stuck at this and really confused as to how to resolve this.

回答1:

TypeScript compiles imports into javascript in a certain way, you can try something like this to be able to use eval() (cringe). This obviously won't work if typescript compiles differently, but it is fun to check out if this works any ways :)

for (let i = 0; i < data.length; i++) {
  this.routeConfigArray.push({ //routeConfigArray : RouteDefinition
    path: data[i].path,
    name: data[i].name,
    component: getComponent(data[i].component)
  });
  this._router.config(this.routeConfigArray);   
}

function getComponent(comp : string) : Function {
    //convert camelCase to underscore notation
    let component : string = comp;
    component = component[0].toLowerCase() + component.slice(1);
    component = component.replace(/([A-Z])/g, function(match) {
        return '_' + match.toLowerCase();
    });
    component += '_1';
    return eval(component[comp])
}

ADDENDUM

As addition to your own solution with using a AsyncRoute, I believe you actually got quite a good solution going on. Perhaps if you place all the pages in a certain way, you can extract the resource location from the path, but that is not necessary. (i mean to get from the path string /about to the resource string ./app/about/about.component shouldn't be hard with a small algorithm. But that might be something for an update.

Anyways, you can try something like this with the AsyncRoute

warning: untested code ahead

let routes : any[] = [
    { "path" : "/about" , "name" : "About" , "component" : "AboutComponent", "route": "/About" , "resource" : "./app/about/about.component" },
    { "path" : "/contact" , "name" : "Contact" , "component" : "ContactComponent", "route": "/Contact" , "resource" : "./app/contact/contact.component" }
];

routes.forEach((route : any) => {
    this.routeConfigArray.push(
        new AsyncRoute({
            path : route.path,
            loader : () => System.import(route.resource).then(m => m[route.component]),
            name : route.name
        })
    );
});

this._router.config(this.routeConfigArray);


回答2:

You are too good @PierreDuc , i was just looking at regex to build the same function, some edits i would like to point out to bring it in working state....

for (let i = 0; i < data.length; i++) {
  this.routeConfigArray.push({ //routeConfigArray : RouteDefinition
    'path': data[i].path,
    'name': data[i].name,
    'component': getComponent(data[i].component).constructor
  });
  this._router.config(this.routeConfigArray);   
}

function getComponent(comp : string) : Function {
    //convert camelCase to underscore notation
    let component : string = comp;
    component = component[0].toLowerCase() + component.slice(1);
    component = component.replace(/([A-Z])/g, function(match) {
        return '_' + match.toLowerCase();
    });
    component += '_1.';
    return eval("new "+component+comp+"()")
}

Thankyou once again dude, its now in a running mode!!! Phew!



回答3:

You could use the another approach that the one from @Pierre and @Pratik based on a method that returns the name of classes:

Object.prototype.getName = function() { 
  var funcNameRegex = /function (.{1,})\(/;
  var results = (funcNameRegex).exec((this).constructor.toString());
  return (results && results.length > 1) ? results[1] : "";
};

In your component you can then configure your routes dynamically like this:

ngOnInit() {
  this.routes = [
    {
      path: '/test', component: 'OtherComponent', name: 'Test'
    }
  ];
  this.configureRoutes(this.routes);
  this.router.config( this.routes);
}

configureRoutes(routes) {
  var potentialComponents = [ OtherComponent ];
  routes.forEach((route) => {
    route.component = potentialComponents.find((component) => {
      return component.name === route.component;
    });
  });
}

This requires to know by advance potential components that can be involved in routing.

See this plunkr for demo: https://plnkr.co/edit/KKVagp?p=preview.

See this question:

  • How do I get the name of an object's type in JavaScript?


回答4:

Update to the first above scenario : The above dynamically loads routes on the UI, but for that to happen i do have to mention it in my AppComponent

import {AboutComponent} from '/path';
import {ContactComponent} from '/path';

//and then either include it in a directive or mention The component Name some place in the code

But this defeats the purpose 'Dynamic Loading', as i have to know which components would be requested and also cannot lazy load them. Say i do that for 500 components, i have to load these 500 components and then just from a JSON mentioned above i pick which has to be loaded on the UI.

Solution(Completed And Tested) ==> I now do not have to mention the components i want to load in any import statement neither in any Directive. Thererby making it lazy loading and Completely Dynamic

How By Using AsyncRoute Class.

Here's my new JSON
//"route" is to map it to [routerLink]="['/Home']" & "resource" is the actual path of component

 [
  { "path" : "/about" , "name" : "About" , "component" : "AboutComponent", "route": "/About" , "resource" : "./app/about/about.component" },
  { "path" : "/contact" , "name" : "Contact" , "component" : "ContactComponent", "route": "/About" , "resource" : "./app/about/about.component" },
  { "path" : "/blog" , "name" : "Blog" , "component" : "AboutComponent", "route": "/About" , "resource" : "./app/about/about.component" },
  { "path" : "/news" , "name" : "News" , "component" : "AboutComponent", "route": "/About" , "resource" : "./app/about/about.component" }
]

Now to the code, here i fetch this JSON and add it to the routeConfigArray : RouteDefinition[] and call the Router's .config(routeConfigArray)

  let routes : any[] = data; // consist the json data in array


routes.forEach((route : any) => {
        this.routeConfigArray.push(
            new AsyncRoute({
                path : route.path,
                loader : () => System.import(route.resource).then(m => m[route.component]),
                name : route.name
            })
        );
    });

    this._router.config(this.routeConfigArray);

And thats how it works!!!