受欢迎的博客标签

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

Published

http://www.lanhusoft.com/Article/373.html 

 

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

 

 

相关类的类图如下:

下面是相关功能主要类:

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

IRoutePublisher:

  1. public interface IRoutePublisher
  2. {
  3. /// <summary>
  4. /// Register routes
  5. /// </summary>
  6. /// <param name="routes">Routes</param>
  7. void RegisterRoutes(RouteCollection routeCollection);
  8. }

IRouteProvider:

 

  1. public interface IRouteProvider
  2. {
  3. void RegisterRoutes(RouteCollection routes);
  4.  
  5. int Priority { get; }
  6. }

 

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

  1. using System.Web.Mvc;
  2. using System.Web.Routing;
  3. using Nop.Web.Framework.Localization;
  4. using Nop.Web.Framework.Mvc.Routes;
  5.  
  6. namespace Nop.Web.Infrastructure
  7. {
  8. public partial class RouteProvider : IRouteProvider
  9. {
  10. public void RegisterRoutes(RouteCollection routes)
  11. {
  12. //We reordered our routes so the most used ones are on top. It can improve performance.
  13.  
  14. //home page
  15. routes.MapLocalizedRoute("HomePage",
  16. "",
  17. new { controller = "Home", action = "Index" },
  18. new[] { "Nop.Web.Controllers" });
  19.  
  20. //widgets
  21. //we have this route for performance optimization because named routes are MUCH faster than usual Html.Action(...)
  22. //and this route is highly used
  23. routes.MapRoute("WidgetsByZone",
  24. "widgetsbyzone/",
  25. new { controller = "Widget", action = "WidgetsByZone" },
  26. new[] { "Nop.Web.Controllers" });
  27.  
  28. //login
  29. routes.MapLocalizedRoute("Login",
  30. "login/",
  31. new { controller = "Customer", action = "Login" },
  32. new[] { "Nop.Web.Controllers" });
  33. //register
  34. routes.MapLocalizedRoute("Register",
  35. "register/",
  36. new { controller = "Customer", action = "Register" },
  37. new[] { "Nop.Web.Controllers" });
  38. //logout
  39. routes.MapLocalizedRoute("Logout",
  40. "logout/",
  41. new { controller = "Customer", action = "Logout" },
  42. new[] { "Nop.Web.Controllers" });
  43.  
  44. //shopping cart
  45. routes.MapLocalizedRoute("ShoppingCart",
  46. "cart/",
  47. new { controller = "ShoppingCart", action = "Cart" },
  48. new[] { "Nop.Web.Controllers" });
  49. //wishlist
  50. routes.MapLocalizedRoute("Wishlist",
  51. "wishlist/{customerGuid}",
  52. new { controller = "ShoppingCart", action = "Wishlist", customerGuid = UrlParameter.Optional },
  53. new[] { "Nop.Web.Controllers" });
  54.  
  55. //customer
  56. routes.MapLocalizedRoute("CustomerInfo",
  57. "customer/info",
  58. new { controller = "Customer", action = "Info" },
  59. new[] { "Nop.Web.Controllers" });
  60. routes.MapLocalizedRoute("CustomerAddresses",
  61. "customer/addresses",
  62. new { controller = "Customer", action = "Addresses" },
  63. new[] { "Nop.Web.Controllers" });
  64. routes.MapLocalizedRoute("CustomerOrders",
  65. "customer/orders",
  66. new { controller = "Customer", action = "Orders" },
  67. new[] { "Nop.Web.Controllers" });
  68. routes.MapLocalizedRoute("CustomerReturnRequests",
  69. "customer/returnrequests",
  70. new { controller = "Customer", action = "ReturnRequests" },
  71. new[] { "Nop.Web.Controllers" });
  72. routes.MapLocalizedRoute("CustomerDownloadableProducts",
  73. "customer/downloadableproducts",
  74. new { controller = "Customer", action = "DownloadableProducts" },
  75. new[] { "Nop.Web.Controllers" });
  76.  
  77. //省略其它路由注册....
  78.  
  79. //page not found
  80. routes.MapLocalizedRoute("PageNotFound",
  81. "page-not-found",
  82. new { controller = "Common", action = "PageNotFound" },
  83. new[] { "Nop.Web.Controllers" });
  84. }
  85.  
  86. public int Priority
  87. {
  88. get
  89. {
  90. return 0;
  91. }
  92. }
  93. }
  94. }

 

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

  1. using System.Web.Routing;
  2. using Nop.Web.Framework.Localization;
  3. using Nop.Web.Framework.Mvc.Routes;
  4. using Nop.Web.Framework.Seo;
  5.  
  6. namespace Nop.Web.Infrastructure
  7. {
  8. public partial class GenericUrlRouteProvider : IRouteProvider
  9. {
  10. public void RegisterRoutes(RouteCollection routes)
  11. {
  12. //generic URLs
  13. routes.MapGenericPathRoute("GenericUrl",
  14. "{generic_se_name}",
  15. new {controller = "Common", action = "GenericUrl"},
  16. new[] {"Nop.Web.Controllers"});
  17.  
  18. //define this routes to use in UI views (in case if you want to customize some of them later)
  19. routes.MapLocalizedRoute("Product",
  20. "{SeName}",
  21. new { controller = "Product", action = "ProductDetails" },
  22. new[] {"Nop.Web.Controllers"});
  23.  
  24. routes.MapLocalizedRoute("Category",
  25. "{SeName}",
  26. new { controller = "Catalog", action = "Category" },
  27. new[] { "Nop.Web.Controllers" });
  28.  
  29. routes.MapLocalizedRoute("Manufacturer",
  30. "{SeName}",
  31. new { controller = "Catalog", action = "Manufacturer" },
  32. new[] { "Nop.Web.Controllers" });
  33.  
  34. routes.MapLocalizedRoute("Vendor",
  35. "{SeName}",
  36. new { controller = "Catalog", action = "Vendor" },
  37. new[] { "Nop.Web.Controllers" });
  38.  
  39. routes.MapLocalizedRoute("NewsItem",
  40. "{SeName}",
  41. new { controller = "News", action = "NewsItem" },
  42. new[] { "Nop.Web.Controllers" });
  43.  
  44. routes.MapLocalizedRoute("BlogPost",
  45. "{SeName}",
  46. new { controller = "Blog", action = "BlogPost" },
  47. new[] { "Nop.Web.Controllers" });
  48.  
  49. routes.MapLocalizedRoute("Topic",
  50. "{SeName}",
  51. new { controller = "Topic", action = "TopicDetails" },
  52. new[] { "Nop.Web.Controllers" });
  53. }
  54.  
  55. public int Priority
  56. {
  57. get
  58. {
  59. //it should be the last route
  60. //we do not set it to -int.MaxValue so it could be overriden (if required)
  61. return -1000000;
  62. }
  63. }
  64. }
  65. }

 

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

  1. public override RouteData GetRouteData(HttpContextBase httpContext)
  2. {
  3. if (DataSettingsHelper.DatabaseIsInstalled() && this.SeoFriendlyUrlsForLanguagesEnabled)
  4. {
  5. string virtualPath = httpContext.Request.AppRelativeCurrentExecutionFilePath;
  6. string applicationPath = httpContext.Request.ApplicationPath;
  7. if (virtualPath.IsLocalizedUrl(applicationPath, false))
  8. {
  9. string rawUrl = httpContext.Request.RawUrl;
  10. var newVirtualPath = rawUrl.RemoveLanguageSeoCodeFromRawUrl(applicationPath);
  11. if (string.IsNullOrEmpty(newVirtualPath))
  12. newVirtualPath = "/";
  13. newVirtualPath = newVirtualPath.RemoveApplicationPathFromRawUrl(applicationPath);
  14. newVirtualPath = "~" + newVirtualPath;
  15. httpContext.RewritePath(newVirtualPath, true);
  16. }
  17. }
  18. RouteData data = base.GetRouteData(httpContext);
  19. return data;
  20. }
  21.  
  22.  
  23. public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
  24. {
  25. VirtualPathData data = base.GetVirtualPath(requestContext, values);
  26.  
  27. if (data != null && DataSettingsHelper.DatabaseIsInstalled() && this.SeoFriendlyUrlsForLanguagesEnabled)
  28. {
  29. string rawUrl = requestContext.HttpContext.Request.RawUrl;
  30. string applicationPath = requestContext.HttpContext.Request.ApplicationPath;
  31. if (rawUrl.IsLocalizedUrl(applicationPath, true))
  32. {
  33. data.VirtualPath = string.Concat(rawUrl.GetLanguageSeoCodeFromUrl(applicationPath, true), "/",
  34. data.VirtualPath);
  35. }
  36. }
  37. return data;
  38. }

 

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

类GenericPathRoute核心代码如下:

  1. public override RouteData GetRouteData(HttpContextBase httpContext)
  2. {
  3. RouteData data = base.GetRouteData(httpContext);
  4. if (data != null && DataSettingsHelper.DatabaseIsInstalled())
  5. {
  6. var urlRecordService = EngineContext.Current.Resolve<IUrlRecordService>();
  7. var slug = data.Values["generic_se_name"] as string;
  8. //performance optimization.
  9. //we load a cached verion here. it reduces number of SQL requests for each page load
  10. var urlRecord = urlRecordService.GetBySlugCached(slug);//查询url对应的路由规则
  11. //comment the line above and uncomment the line below in order to disable this performance "workaround"
  12. //var urlRecord = urlRecordService.GetBySlug(slug);
  13. if (urlRecord == null)
  14. {
  15. data.Values["controller"] = "Common";
  16. data.Values["action"] = "PageNotFound";
  17. return data;
  18. }
  19. //ensre that URL record is active
  20. if (!urlRecord.IsActive)
  21. {
  22. //URL record is not active. let's find the latest one
  23. var activeSlug = urlRecordService.GetActiveSlug(urlRecord.EntityId, urlRecord.EntityName, urlRecord.LanguageId);
  24. if (!string.IsNullOrWhiteSpace(activeSlug))
  25. {
  26. //the active one is found
  27. var webHelper = EngineContext.Current.Resolve<IWebHelper>();
  28. var response = httpContext.Response;
  29. response.Status = "301 Moved Permanently";
  30. response.RedirectLocation = string.Format("{0}{1}", webHelper.GetStoreLocation(false), activeSlug);
  31. response.End();
  32. return null;
  33. }
  34. else
  35. {
  36. data.Values["controller"] = "Common";
  37. data.Values["action"] = "PageNotFound";
  38. return data;
  39. }
  40. }
  41.  
  42. //ensure that the slug is the same for the current language
  43. //otherwise, it can cause some issues when customers choose a new language but a slug stays the same
  44. var workContext = EngineContext.Current.Resolve<IWorkContext>();
  45. var slugForCurrentLanguage = SeoExtensions.GetSeName(urlRecord.EntityId, urlRecord.EntityName, workContext.WorkingLanguage.Id);
  46. if (!String.IsNullOrEmpty(slugForCurrentLanguage) &&
  47. !slugForCurrentLanguage.Equals(slug, StringComparison.InvariantCultureIgnoreCase))
  48. {
  49. //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)
  50. var webHelper = EngineContext.Current.Resolve<IWebHelper>();
  51. var response = httpContext.Response;
  52. //response.Status = "302 Found";
  53. response.Status = "302 Moved Temporarily";
  54. response.RedirectLocation = string.Format("{0}{1}", webHelper.GetStoreLocation(false), slugForCurrentLanguage);
  55. response.End();
  56. return null;
  57. }
  58.  
  59. //处理URL并动态赋值真正的Controller的相关信息
  60. switch (urlRecord.EntityName.ToLowerInvariant())
  61. {
  62. case "product":
  63. {
  64. data.Values["controller"] = "Product";
  65. data.Values["action"] = "ProductDetails";
  66. data.Values["productid"] = urlRecord.EntityId;
  67. data.Values["SeName"] = urlRecord.Slug;
  68. }
  69. break;
  70. case "category":
  71. {
  72. data.Values["controller"] = "Catalog";
  73. data.Values["action"] = "Category";
  74. data.Values["categoryid"] = urlRecord.EntityId;
  75. data.Values["SeName"] = urlRecord.Slug;
  76. }
  77. break;
  78. case "manufacturer":
  79. {
  80. data.Values["controller"] = "Catalog";
  81. data.Values["action"] = "Manufacturer";
  82. data.Values["manufacturerid"] = urlRecord.EntityId;
  83. data.Values["SeName"] = urlRecord.Slug;
  84. }
  85. break;
  86. case "vendor":
  87. {
  88. data.Values["controller"] = "Catalog";
  89. data.Values["action"] = "Vendor";
  90. data.Values["vendorid"] = urlRecord.EntityId;
  91. data.Values["SeName"] = urlRecord.Slug;
  92. }
  93. break;
  94. case "newsitem":
  95. {
  96. data.Values["controller"] = "News";
  97. data.Values["action"] = "NewsItem";
  98. data.Values["newsItemId"] = urlRecord.EntityId;
  99. data.Values["SeName"] = urlRecord.Slug;
  100. }
  101. break;
  102. case "blogpost":
  103. {
  104. data.Values["controller"] = "Blog";
  105. data.Values["action"] = "BlogPost";
  106. data.Values["blogPostId"] = urlRecord.EntityId;
  107. data.Values["SeName"] = urlRecord.Slug;
  108. }
  109. break;
  110. case "topic":
  111. {
  112. data.Values["controller"] = "Topic";
  113. data.Values["action"] = "TopicDetails";
  114. data.Values["topicId"] = urlRecord.EntityId;
  115. data.Values["SeName"] = urlRecord.Slug;
  116. }
  117. break;
  118. default:
  119. {
  120. //no record found
  121.  
  122. //generate an event this way developers could insert their own types
  123. EngineContext.Current.Resolve<IEventPublisher>()
  124. .Publish(new CustomUrlRecordEntityNameRequested(data, urlRecord));
  125. }
  126. break;
  127. }
  128. }
  129. return data;
  130. }

 

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

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

  1. switch (urlRecord.EntityName.ToLowerInvariant())
  2. {
  3. //....省略其它代码
  4. case "category":
  5. {
  6. data.Values["controller"] = "Catalog";
  7. data.Values["action"] = "Category";
  8. data.Values["categoryid"] = urlRecord.EntityId;
  9. data.Values["SeName"] = urlRecord.Slug;
  10. }
  11. break;
  12. //....省略其它代码
  13. }

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

 

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

  1. public virtual void RegisterRoutes(RouteCollection routes)
  2. {
  3. var routeProviderTypes = typeFinder.FindClassesOfType<IRouteProvider>();
  4. var routeProviders = new List<IRouteProvider>();
  5. foreach (var providerType in routeProviderTypes)
  6. {
  7. //Ignore not installed plugins
  8. var plugin = FindPlugin(providerType);
  9. if (plugin != null && !plugin.Installed)
  10. continue;
  11.  
  12. var provider = Activator.CreateInstance(providerType) as IRouteProvider;
  13. routeProviders.Add(provider);
  14. }
  15. routeProviders = routeProviders.OrderByDescending(rp => rp.Priority).ToList();
  16. routeProviders.ForEach(rp => rp.RegisterRoutes(routes));
  17. }

 

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

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

 

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

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