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

可能你刚开始看nop源码不太清楚一个Url对应具体的Controller是哪一个,因为Nop自身用了对seo友好的Url,它对路由进行了一些重写。

NopCommerce源码架构详解概述

文章目录

概述

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

内容

可能你刚开始看nop源码不太清楚一个Url对应具体的Controller是哪一个,因为Nop自身用了对seo友好的Url,它对路由进行了一些重写。我希望同学们通过我的这个文章对Nop路由有更深入的了解,以后也可以通过借鉴Nop的思路自己实现一个对SEO友好的Url路由。

相关类的类图如下:

2020-04-23-18-03-53

下面是相关功能主要类:

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里面,如下图:

2020-04-23-18-04-01

比如,我们在前台访问: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);
原文出处:蓝狐软件工作室【蓝狐】

原文链接:http://m.lanhusoft.com/Article/349.html

本文观点不代表 .Net中文网 立场,转载请联系原作者。

发表评论

登录后才能评论