http://www.lanhusoft.com/Article/373.html
NopCommerce源码架构详解--对seo友好Url的路由机制实现源码分析
相关类的类图如下:
下面是相关功能主要类:
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里面,如下图:
比如,我们在前台访问: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);