受欢迎的博客标签

NopCommerce源码架构详解--对seo友好Url的路由机制实现源码分析(Nopcommerce 4.3x .Net core 3.1)

Published

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

 

Nopcommerce 4.4x

.Net 5.x

 

 

1.插件路由的注册
 
路由注册
路由链接的生成
Url地址的解析

 

 

nopCommerce/src/Presentation/Nop.Web.Framework/Mvc/Routing/

 

 

下面是相关功能主要类:

1.路由注册

路由注册由2部分组成,一个功能是各程序集提供路由表;另一个功能是将分散在各处的程序集的路由表收集起来,然后统一进行注册到系统。

1.1 插件路由的注册-各程序集plugin提供路由表

Interface

using Microsoft.AspNetCore.Routing;

namespace Nop.Web.Framework.Mvc.Routing
{
    /// <summary>
    /// Route provider
    /// </summary>
    public interface IRouteProvider
    {
        /// <summary>
        /// Register routes
        /// </summary>
        /// <param name="endpointRouteBuilder">Route builder</param>
        void RegisterRoutes(IEndpointRouteBuilder endpointRouteBuilder);

        /// <summary>
        /// Gets a priority of route provider
        /// </summary>
        int Priority { get; }
    }
}

 

//come from:https://github.com/nopSolutions/nopCommerce/blob/12dd4825dfb56bbc64e192cd35d25e3205427646/src/Plugins/Nop.Plugin.Misc.Sendinblue/Infrastructure/RouteProvider.cs#L16

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Routing;
using Nop.Web.Framework.Mvc.Routing;

namespace Nop.Plugin.Misc.Sendinblue.Infrastructure
{
    /// <summary>
    /// Represents plugin route provider
    /// </summary>
    public class RouteProvider : IRouteProvider
    {
        /// <summary>
        /// Register routes
        /// </summary>
        /// <param name="endpointRouteBuilder">Route builder</param>
        public void RegisterRoutes(IEndpointRouteBuilder endpointRouteBuilder)
        {
            endpointRouteBuilder.MapControllerRoute(SendinblueDefaults.ImportContactsRoute, "Plugins/Sendinblue/ImportContacts",
                new { controller = "Sendinblue", action = "ImportContacts" });

            endpointRouteBuilder.MapControllerRoute(SendinblueDefaults.UnsubscribeContactRoute, "Plugins/Sendinblue/UnsubscribeWebHook",
                new { controller = "Sendinblue", action = "UnsubscribeWebHook" });
        }

        /// <summary>
        /// Gets a priority of route provider
        /// </summary>
        public int Priority => 0;
    }
}

 

//come from:https://github.com/nopSolutions/nopCommerce/blob/12dd4825dfb56bbc64e192cd35d25e3205427646/src/Presentation/Nop.Web.Framework/Mvc/Routing/RoutePublisher.cs

namespace Nop.Web.Framework.Mvc.Routing
{
    /// <summary>
    /// Represents implementation of route publisher
    /// </summary>
    public class RoutePublisher : IRoutePublisher
    {
        #region Fields

        /// <summary>
        /// Type finder
        /// </summary>
        protected readonly ITypeFinder _typeFinder;

        #endregion

        #region Ctor

        /// <summary>
        /// Ctor
        /// </summary>
        /// <param name="typeFinder">Type finder</param>
        public RoutePublisher(ITypeFinder typeFinder)
        {
            _typeFinder = typeFinder;
        }

        #endregion

        #region Methods

        /// <summary>
        /// Register routes
        /// </summary>
        /// <param name="endpointRouteBuilder">Route builder</param>
        public virtual void RegisterRoutes(IEndpointRouteBuilder endpointRouteBuilder)
        {
            //find route providers provided by other assemblies
            var routeProviders = _typeFinder.FindClassesOfType<IRouteProvider>();

            //create and sort instances of route providers
            var instances = routeProviders
                .Select(routeProvider => (IRouteProvider)Activator.CreateInstance(routeProvider))
                .OrderByDescending(routeProvider => routeProvider.Priority);

            //register all provided routes
            foreach (var routeProvider in instances)
                routeProvider.RegisterRoutes(endpointRouteBuilder);  //here
        }

        #endregion
    }
}

 

 

Step 1:定义路由规则

endpointRouteBuilder.MapControllerRoute("Topic", "{SeName}",
               new { controller = "Topic", action = "TopicDetails" });

Step 2:生成链接接

根据路由规则,前台会生成指定格式的链接,由Url.RouteUrl完成,如:

<li><a class="nav-link" href="@Url.RouteUrl("Topic", new {SeName = "topic.SeName"})">Name</a></li>

// come from:https://github.com/nopSolutions/nopCommerce/blob/4910cb1090d01e40d1ed7c80e8c471d1600d4fe8/src/Presentation/Nop.Web/Views/Blog/List.cshtml
 <a class="post-title" href="@Url.RouteUrl("BlogPost", new {SeName = item.SeName})">@item.Title</a>

 output

<a class="nav-link" href="/topic.SeName">Name</a>

 

1.2 统一注册路由

程序先通过反射方式获取实现了IRouteProvider接口的程序集,然后调用其中的void RegisterRoutes(IEndpointRouteBuilder endpointRouteBuilder)方法进行自动注册路由。

using System;
using System.Linq;
using Microsoft.AspNetCore.Routing;
using Nop.Core.Infrastructure;

namespace Nop.Web.Framework.Mvc.Routing
{
    /// <summary>
    /// Represents implementation of route publisher
    /// </summary>
    public class RoutePublisher : IRoutePublisher
    {
       

        #region Methods

        /// <summary>
        /// Register routes
        /// </summary>
        /// <param name="endpointRouteBuilder">Route builder</param>
        public virtual void RegisterRoutes(IEndpointRouteBuilder endpointRouteBuilder)
        {
            //find route providers provided by other assemblies
            var routeProviders = _typeFinder.FindClassesOfType<IRouteProvider>();

            //create and sort instances of route providers
            var instances = routeProviders
                .Select(routeProvider => (IRouteProvider)Activator.CreateInstance(routeProvider))
                .OrderByDescending(routeProvider => routeProvider.Priority);

            //register all provided routes
            foreach (var routeProvider in instances)
                routeProvider.RegisterRoutes(endpointRouteBuilder);
        }

        #endregion
    }
}

2.路由链接的生成

路由链接的生成数据来源分为两大类,一类是可以确定的路由,另一类是需要从数据库Urlrecord集合中读取地址信息,然后进行组合。

2.1 可以确定的路由

 

2.2 要从数据库Urlrecord集合中读取的路由

如About-us,首先从Urlrecord中找到seName="About-us"的记录

  //get URL records
            var urlRecords = await _urlRecordService.GetAllUrlRecordsAsync(slug: searchModel.SeName,
                languageId: languageId, isActive: isActive,
                pageIndex: searchModel.Page - 1, pageSize: searchModel.PageSize);

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

 

3、SlugRouteTransformer : DynamicRouteValueTransformer,继承DynamicRouteValueTransformer。这个定义了blogpost news对SEO友好的路由。

come from:https://github.com/nopSolutions/nopCommerce/blob/12dd4825dfb56bbc64e192cd35d25e3205427646/src/Presentation/Nop.Web.Framework/Mvc/Routing/SlugRouteTransformer.cs

namespace Nop.Web.Framework.Mvc.Routing
{
    /// <summary>
    /// Represents slug route transformer
    /// </summary>
    public class SlugRouteTransformer : DynamicRouteValueTransformer
    {
      //since we are here, all is ok with the slug, so process URL
            switch (urlRecord.EntityName.ToLowerInvariant())
            {
                case "product":
                    values[NopPathRouteDefaults.ControllerFieldKey] = "Product";
                    values[NopPathRouteDefaults.ActionFieldKey] = "ProductDetails";
                    values[NopPathRouteDefaults.ProductIdFieldKey] = urlRecord.EntityId;
                    values[NopPathRouteDefaults.SeNameFieldKey] = urlRecord.Slug;
                    break;

                case "producttag":
                    values[NopPathRouteDefaults.ControllerFieldKey] = "Catalog";
                    values[NopPathRouteDefaults.ActionFieldKey] = "ProductsByTag";
                    values[NopPathRouteDefaults.ProducttagIdFieldKey] = urlRecord.EntityId;
                    values[NopPathRouteDefaults.SeNameFieldKey] = urlRecord.Slug;
                    break;

                case "category":
                    values[NopPathRouteDefaults.ControllerFieldKey] = "Catalog";
                    values[NopPathRouteDefaults.ActionFieldKey] = "Category";
                    values[NopPathRouteDefaults.CategoryIdFieldKey] = urlRecord.EntityId;
                    values[NopPathRouteDefaults.SeNameFieldKey] = urlRecord.Slug;
                    break;

                case "manufacturer":
                    values[NopPathRouteDefaults.ControllerFieldKey] = "Catalog";
                    values[NopPathRouteDefaults.ActionFieldKey] = "Manufacturer";
                    values[NopPathRouteDefaults.ManufacturerIdFieldKey] = urlRecord.EntityId;
                    values[NopPathRouteDefaults.SeNameFieldKey] = urlRecord.Slug;
                    break;

                case "vendor":
                    values[NopPathRouteDefaults.ControllerFieldKey] = "Catalog";
                    values[NopPathRouteDefaults.ActionFieldKey] = "Vendor";
                    values[NopPathRouteDefaults.VendorIdFieldKey] = urlRecord.EntityId;
                    values[NopPathRouteDefaults.SeNameFieldKey] = urlRecord.Slug;
                    break;

                case "newsitem":
                    values[NopPathRouteDefaults.ControllerFieldKey] = "News";
                    values[NopPathRouteDefaults.ActionFieldKey] = "NewsItem";
                    values[NopPathRouteDefaults.NewsItemIdFieldKey] = urlRecord.EntityId;
                    values[NopPathRouteDefaults.SeNameFieldKey] = urlRecord.Slug;
                    break;

                case "blogpost":
                    values[NopPathRouteDefaults.ControllerFieldKey] = "Blog";
                    values[NopPathRouteDefaults.ActionFieldKey] = "BlogPost";
                    values[NopPathRouteDefaults.BlogPostIdFieldKey] = urlRecord.EntityId;
                    values[NopPathRouteDefaults.SeNameFieldKey] = urlRecord.Slug;
                    break;

                case "topic":
                    values[NopPathRouteDefaults.ControllerFieldKey] = "Topic";
                    values[NopPathRouteDefaults.ActionFieldKey] = "TopicDetails";
                    values[NopPathRouteDefaults.TopicIdFieldKey] = urlRecord.EntityId;
                    values[NopPathRouteDefaults.SeNameFieldKey] = urlRecord.Slug;
                    break;

                default:
                    //no record found, thus generate an event this way developers could insert their own types
                    await _eventPublisher.PublishAsync(new GenericRoutingEvent(values, urlRecord));
                    break;
            }

 

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);