NopCommerce源码架构详解-对seo友好Url的路由机制实现源码分析
seo友好Url的路由机制实现源码分析
NopCommerce源码架构详解-对seo友好Url的路由机制实现源码分析。
内容
可能你刚开始看nop源码不太清楚一个Url对应具体的Controller是哪一个,因为Nop自身用了对seo友好的Url,它对路由进行了一些重写。我希望同学们通过我的这个文章对Nop路由有更深入的了解,以后也可以通过借鉴Nop的思路自己实现一个对SEO友好的Url路由。
相关类的类图如下:
下面是相关功能主要类:
1、Nop.Web.Framework.mvc.Routes.IRoutePublisher和IRouteProvider,注册路由的共用接口。
IRoutePublisher:
public interface IRoutePublisher { ///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里面,如下图:
比如,我们在前台访问: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);