NopCommerce源码架构详解--对seo友好Url的路由机制实现源码分析
Nopcommerce 4.4x
.Net 5.x
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。
- 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);