NopCommerce源码架构详解-对seo友好Url的路由机制实现源码分析

seo友好Url的路由机制实现源码分析

NopCommerce源码架构详解-对seo友好Url的路由机制实现源码分析。

内容

可能你刚开始看nop源码不太清楚一个Url对应具体的Controller是哪一个,因为Nop自身用了对seo友好的Url,它对路由进行了一些重写。我希望同学们通过我的这个文章对Nop路由有更深入的了解,以后也可以通过借鉴Nop的思路自己实现一个对SEO友好的Url路由。

相关类的类图如下:

2020-04-23-18-03-53

下面是相关功能主要类:

1、Nop.Web.Framework.mvc.Routes.IRoutePublisher和IRouteProvider,注册路由的共用接口。

IRoutePublisher:

public interface IRoutePublisher
{
/// <summary>
/// Register routes
/// </summary>
/// <param name="routes">Routes</param>
void RegisterRoutes(RouteCollection routeCollection);
}

IRouteProvider:

public interface IRouteProvider
{
void RegisterRoutes(RouteCollection routes);
 
int Priority { get; }
}

2、RouteProvider,实现接口IRouteProvider,注册一些核心路由规则,如首页、登录、注册、购物车等等。

using System.Web.Mvc;
using System.Web.Routing;
using Nop.Web.Framework.Localization;
using Nop.Web.Framework.Mvc.Routes;
 
namespace Nop.Web.Infrastructure
{
public partial class RouteProvider : IRouteProvider
{
public void RegisterRoutes(RouteCollection routes)
{
//We reordered our routes so the most used ones are on top. It can improve performance.
 
//home page
routes.MapLocalizedRoute("HomePage",
"",
new { controller = "Home", action = "Index" },
new[] { "Nop.Web.Controllers" });
 
//widgets
//we have this route for performance optimization because named routes are MUCH faster than usual Html.Action(...)
//and this route is highly used
routes.MapRoute("WidgetsByZone",
"widgetsbyzone/",
new { controller = "Widget", action = "WidgetsByZone" },
new[] { "Nop.Web.Controllers" });
 
//login
routes.MapLocalizedRoute("Login",
"login/",
new { controller = "Customer", action = "Login" },
new[] { "Nop.Web.Controllers" });
//register
routes.MapLocalizedRoute("Register",
"register/",
new { controller = "Customer", action = "Register" },
new[] { "Nop.Web.Controllers" });
//logout
routes.MapLocalizedRoute("Logout",
"logout/",
new { controller = "Customer", action = "Logout" },
new[] { "Nop.Web.Controllers" });
 
//shopping cart
routes.MapLocalizedRoute("ShoppingCart",
"cart/",
new { controller = "ShoppingCart", action = "Cart" },
new[] { "Nop.Web.Controllers" });
//wishlist
routes.MapLocalizedRoute("Wishlist",
"wishlist/{customerGuid}",
new { controller = "ShoppingCart", action = "Wishlist", customerGuid = UrlParameter.Optional },
new[] { "Nop.Web.Controllers" });
 
//customer
routes.MapLocalizedRoute("CustomerInfo",
"customer/info",
new { controller = "Customer", action = "Info" },
new[] { "Nop.Web.Controllers" });
routes.MapLocalizedRoute("CustomerAddresses",
"customer/addresses",
new { controller = "Customer", action = "Addresses" },
new[] { "Nop.Web.Controllers" });
routes.MapLocalizedRoute("CustomerOrders",
"customer/orders",
new { controller = "Customer", action = "Orders" },
new[] { "Nop.Web.Controllers" });
routes.MapLocalizedRoute("CustomerReturnRequests",
"customer/returnrequests",
new { controller = "Customer", action = "ReturnRequests" },
new[] { "Nop.Web.Controllers" });
routes.MapLocalizedRoute("CustomerDownloadableProducts",
"customer/downloadableproducts",
new { controller = "Customer", action = "DownloadableProducts" },
new[] { "Nop.Web.Controllers" });
 
//省略其它路由注册....
 
//page not found
routes.MapLocalizedRoute("PageNotFound",
"page-not-found",
new { controller = "Common", action = "PageNotFound" },
new[] { "Nop.Web.Controllers" });
}
 
public int Priority
{
get
{
return 0;
}
}
}
}

3、Nop.Web.Infrastructure.GenericUrlRouteProvider,同样的实现接口IRoutePublisher。这个RouteProvider定义了一些对SEO友好的路由。

using System.Web.Routing;
using Nop.Web.Framework.Localization;
using Nop.Web.Framework.Mvc.Routes;
using Nop.Web.Framework.Seo;
 
namespace Nop.Web.Infrastructure
{
public partial class GenericUrlRouteProvider : IRouteProvider
{
public void RegisterRoutes(RouteCollection routes)
{
//generic URLs
routes.MapGenericPathRoute("GenericUrl",
"{generic_se_name}",
new {controller = "Common", action = "GenericUrl"},
new[] {"Nop.Web.Controllers"});
 
//define this routes to use in UI views (in case if you want to customize some of them later)
routes.MapLocalizedRoute("Product",
"{SeName}",
new { controller = "Product", action = "ProductDetails" },
new[] {"Nop.Web.Controllers"});
 
routes.MapLocalizedRoute("Category",
"{SeName}",
new { controller = "Catalog", action = "Category" },
new[] { "Nop.Web.Controllers" });
 
routes.MapLocalizedRoute("Manufacturer",
"{SeName}",
new { controller = "Catalog", action = "Manufacturer" },
new[] { "Nop.Web.Controllers" });
 
routes.MapLocalizedRoute("Vendor",
"{SeName}",
new { controller = "Catalog", action = "Vendor" },
new[] { "Nop.Web.Controllers" });
 
routes.MapLocalizedRoute("NewsItem",
"{SeName}",
new { controller = "News", action = "NewsItem" },
new[] { "Nop.Web.Controllers" });
 
routes.MapLocalizedRoute("BlogPost",
"{SeName}",
new { controller = "Blog", action = "BlogPost" },
new[] { "Nop.Web.Controllers" });
 
routes.MapLocalizedRoute("Topic",
"{SeName}",
new { controller = "Topic", action = "TopicDetails" },
new[] { "Nop.Web.Controllers" });
}
 
public int Priority
{
get
{
//it should be the last route
//we do not set it to -int.MaxValue so it could be overriden (if required)
return -1000000;
}
}
}
}

4、Nop.Web.Framework.Localization.LocalizedRoute,它采用基类System.Web.Routing.Route,为了实现路由本地化。它提供一些属性和方法为获取到真正的路由做准备。它重写了基类Route两方法,GetRouteData和GetVirtualPath。

public override RouteData GetRouteData(HttpContextBase httpContext)
{
if (DataSettingsHelper.DatabaseIsInstalled() && this.SeoFriendlyUrlsForLanguagesEnabled)
{
string virtualPath = httpContext.Request.AppRelativeCurrentExecutionFilePath;
string applicationPath = httpContext.Request.ApplicationPath;
if (virtualPath.IsLocalizedUrl(applicationPath, false))
{
string rawUrl = httpContext.Request.RawUrl;
var newVirtualPath = rawUrl.RemoveLanguageSeoCodeFromRawUrl(applicationPath);
if (string.IsNullOrEmpty(newVirtualPath))
newVirtualPath = "/";
newVirtualPath = newVirtualPath.RemoveApplicationPathFromRawUrl(applicationPath);
newVirtualPath = "~" + newVirtualPath;
httpContext.RewritePath(newVirtualPath, true);
}
}
RouteData data = base.GetRouteData(httpContext);
return data;
}
 
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
VirtualPathData data = base.GetVirtualPath(requestContext, values);
 
if (data != null && DataSettingsHelper.DatabaseIsInstalled() && this.SeoFriendlyUrlsForLanguagesEnabled)
{
string rawUrl = requestContext.HttpContext.Request.RawUrl;
string applicationPath = requestContext.HttpContext.Request.ApplicationPath;
if (rawUrl.IsLocalizedUrl(applicationPath, true))
{
data.VirtualPath = string.Concat(rawUrl.GetLanguageSeoCodeFromUrl(applicationPath, true), "/",
data.VirtualPath);
}
}
return data;
}

5、Nop.Web.Framework.Seo.GenericPathRoute,这个类是真正把友好的Url解析到我们在RouteProvider配置好的友好的路由规则。它继承了类Nop.Web.Framework.Localization.LocalizedRoute。

类GenericPathRoute核心代码如下:

public override RouteData GetRouteData(HttpContextBase httpContext)
{
RouteData data = base.GetRouteData(httpContext);
if (data != null && DataSettingsHelper.DatabaseIsInstalled())
{
var urlRecordService = EngineContext.Current.Resolve<IUrlRecordService>();
var slug = data.Values["generic_se_name"] as string;
//performance optimization.
//we load a cached verion here. it reduces number of SQL requests for each page load
var urlRecord = urlRecordService.GetBySlugCached(slug);//查询url对应的路由规则
//comment the line above and uncomment the line below in order to disable this performance "workaround"
//var urlRecord = urlRecordService.GetBySlug(slug);
if (urlRecord == null)
{
data.Values["controller"] = "Common";
data.Values["action"] = "PageNotFound";
return data;
}
//ensre that URL record is active
if (!urlRecord.IsActive)
{
//URL record is not active. let's find the latest one
var activeSlug = urlRecordService.GetActiveSlug(urlRecord.EntityId, urlRecord.EntityName, urlRecord.LanguageId);
if (!string.IsNullOrWhiteSpace(activeSlug))
{
//the active one is found
var webHelper = EngineContext.Current.Resolve<IWebHelper>();
var response = httpContext.Response;
response.Status = "301 Moved Permanently";
response.RedirectLocation = string.Format("{0}{1}", webHelper.GetStoreLocation(false), activeSlug);
response.End();
return null;
}
else
{
data.Values["controller"] = "Common";
data.Values["action"] = "PageNotFound";
return data;
}
}
 
//ensure that the slug is the same for the current language
//otherwise, it can cause some issues when customers choose a new language but a slug stays the same
var workContext = EngineContext.Current.Resolve<IWorkContext>();
var slugForCurrentLanguage = SeoExtensions.GetSeName(urlRecord.EntityId, urlRecord.EntityName, workContext.WorkingLanguage.Id);
if (!String.IsNullOrEmpty(slugForCurrentLanguage) &&
!slugForCurrentLanguage.Equals(slug, StringComparison.InvariantCultureIgnoreCase))
{
//we should make not null or "" validation above because some entities does not have SeName for standard (ID=0) language (e.g. news, blog posts)
var webHelper = EngineContext.Current.Resolve<IWebHelper>();
var response = httpContext.Response;
//response.Status = "302 Found";
response.Status = "302 Moved Temporarily";
response.RedirectLocation = string.Format("{0}{1}", webHelper.GetStoreLocation(false), slugForCurrentLanguage);
response.End();
return null;
}
 
//处理URL并动态赋值真正的Controller的相关信息
switch (urlRecord.EntityName.ToLowerInvariant())
{
case "product":
{
data.Values["controller"] = "Product";
data.Values["action"] = "ProductDetails";
data.Values["productid"] = urlRecord.EntityId;
data.Values["SeName"] = urlRecord.Slug;
}
break;
case "category":
{
data.Values["controller"] = "Catalog";
data.Values["action"] = "Category";
data.Values["categoryid"] = urlRecord.EntityId;
data.Values["SeName"] = urlRecord.Slug;
}
break;
case "manufacturer":
{
data.Values["controller"] = "Catalog";
data.Values["action"] = "Manufacturer";
data.Values["manufacturerid"] = urlRecord.EntityId;
data.Values["SeName"] = urlRecord.Slug;
}
break;
case "vendor":
{
data.Values["controller"] = "Catalog";
data.Values["action"] = "Vendor";
data.Values["vendorid"] = urlRecord.EntityId;
data.Values["SeName"] = urlRecord.Slug;
}
break;
case "newsitem":
{
data.Values["controller"] = "News";
data.Values["action"] = "NewsItem";
data.Values["newsItemId"] = urlRecord.EntityId;
data.Values["SeName"] = urlRecord.Slug;
}
break;
case "blogpost":
{
data.Values["controller"] = "Blog";
data.Values["action"] = "BlogPost";
data.Values["blogPostId"] = urlRecord.EntityId;
data.Values["SeName"] = urlRecord.Slug;
}
break;
case "topic":
{
data.Values["controller"] = "Topic";
data.Values["action"] = "TopicDetails";
data.Values["topicId"] = urlRecord.EntityId;
data.Values["SeName"] = urlRecord.Slug;
}
break;
default:
{
//no record found
 
//generate an event this way developers could insert their own types
EngineContext.Current.Resolve<IEventPublisher>()
.Publish(new CustomUrlRecordEntityNameRequested(data, urlRecord));
}
break;
}
}
return data;
}

可以看到上面通过获取路由中变量generic_se_name的值,然后通过这个值查询这个url对应的路由规则。Nop把这个对应信息存在表UrlRecord里面,如下图:

2020-04-23-18-04-01

比如,我们在前台访问:http://localhost:15536/books,其实generic_se_name的值就为books,然后会找到字段Slug的值为books的记录。接着进行处理Url的Switch语句:

switch (urlRecord.EntityName.ToLowerInvariant())
{
//....省略其它代码
case "category":
{
data.Values["controller"] = "Catalog";
data.Values["action"] = "Category";
data.Values["categoryid"] = urlRecord.EntityId;
data.Values["SeName"] = urlRecord.Slug;
}
break;
//....省略其它代码
}

可以看到请求url:http://localhost:15536/books,真正执行的是Catalog中的Category方法。

6、RoutePublisher,实现接口IRoutePublisher,通过typeFinder.FindClassesOfType查找项目中所有实现了接口IRouteProvider的类,并依次注册其里面的路由。

public virtual void RegisterRoutes(RouteCollection routes)
{
var routeProviderTypes = typeFinder.FindClassesOfType<IRouteProvider>();
var routeProviders = new List<IRouteProvider>();
foreach (var providerType in routeProviderTypes)
{
//Ignore not installed plugins
var plugin = FindPlugin(providerType);
if (plugin != null && !plugin.Installed)
continue;
 
var provider = Activator.CreateInstance(providerType) as IRouteProvider;
routeProviders.Add(provider);
}
routeProviders = routeProviders.OrderByDescending(rp => rp.Priority).ToList();
routeProviders.ForEach(rp => rp.RegisterRoutes(routes));
}

在程序启动的时候就会注册路由,依赖注入在Nop.Web.Framework.DependencyRegistrar类中有下面的代码把接口IRoutePublisher用类RoutePublisher来注册:

builder.RegisterType<RoutePublisher>().As<IRoutePublisher>().SingleInstance();

最后在类MvcApplication中的会调用routePublisher注册所有路由规则到MVC框架中:

 
//register custom routes (plugins, etc)
var routePublisher = EngineContext.Current.Resolve<IRoutePublisher>();
routePublisher.RegisterRoutes(routes);

posted on 2022-04-02 14:46  大西瓜3721  阅读(76)  评论(0编辑  收藏  举报

导航