在 ASP.NET Core 里扩展 Razor 查找视图目录不是什么新鲜和困难的事情,但 _ViewStart
和 _ViewImports
这2个视图比较特殊,如果想让 Razor 在我们指定的目录中查找它们,则需要耗费一点额外的精力。本文将提供一种方法做到这一点。注意,文本仅适用于 ASP.NET Core 2.0+, 因为 Razor 在 2.0 版本里的内部实现有较大重构,因此这里提供的方法并不适用于 ASP.NET Core 1.x
为了全面描述 ASP.NET Core 2.0 中扩展 Razor 查找视图目录的能力,我们还是由浅入深,从最简单的扩展方式着手吧。
首先,我们可以创建一个新的 ASP.NET Core 项目用于演示。
mkdir CustomizedViewLocation
cd CustomizedViewLocation
dotnet new web # 创建一个空的 ASP.NET Core 应用
接下来稍微调整下 Startup.cs
文件的内容,引入 MVC:
// Startup.cs
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
namespace CustomizedViewLocation
{
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseMvcWithDefaultRoute();
}
}
}
好了我们的演示项目已经搭好了架子。
在我们的示例项目中,我们希望我们的目录组织方式是按照功能模块组织的,即同一个功能模块的所有 Controller
和 View
都放在同一个目录下。对于多个功能模块共享、通用的内容,比如 _Layout
, _Footer
, _ViewStart
和 _ViewImports
则单独放在根目录下的一个叫 Shared
的子目录中。
ViewLocationFormats
假设我们现在有2个功能模块 Home
和 About
,分别需要 HomeController
和它的 Index
view,以及 AboutMeController
和它的 Index
view. 因为一个 Controller
可能会包含多个 view
,因此我选择为每一个功能模块目录下再增加一个 Views
目录,集中这个功能模块下的所有 View
. 整个目录结构看起来是这样的:
从目录结构中我们可以发现我们的视图目录为 /{controller}/Views/{viewName}.cshtml
, 比如 HomeController
的 Index
视图所在的位置就是 /Home/Views/Index.cshtml
,这跟 MVC 默认的视图位置 /Views/{Controller}/{viewName}.cshtml
很相似(/Views/Home/Index.cshtml
),共同的特点是路径中的 Controller
部分和 View
部分是动态的,其它的都是固定不变的。其实 MVC 默认的寻找视图位置的方式一点都不高端,类似于这样:
string controllerName = "Home"; // “我”知道当前 Controller 是 Home
string viewName = "Index"; // "我“知道当前需要解析的 View 的名字
// 把 viewName 和 controllerName 带入一个代表视图路径的格式化字符串得到最终的视图路径。
string viewPath = string.Format("/Views/{1}/{0}.cshtml", viewName, controllerName);
// 根据 viewPath 找到视图文件做后续处理
如果我们可以构建另一个格式字符串,其中 {0}
代表 View
名称, {1}
代表 Controller
名称,然后替换掉默认的 /Views/{1}/{0}.cshtml
,那我们就可以让 Razor
到我们设定的路径去检索视图。而要做到这点非常容易,利用 ViewLocationFormats
,代码如下:
// Startup.cs
public void ConfigureServices(IServiceCollection services)
{
IMvcBuilder mvcBuilder = services.AddMvc();
mvcBuilder.AddRazorOptions(options => options.ViewLocationFormats.Add("/{1}/Views/{0}.cshtml"));
}
收工,就这么简单。顺便说一句,还有一个参数 {2}
,代表 Area
名称。
这种做法是不是已经很完美了呢?No, No, No. 谁能看出来这种做法有什么缺点?
这种做法有2个缺点。
所有的功能模块目录必须在根目录下创建,无法建立层级目录关系。且看下面的目录结构截图:
注意 Reports
目录,因为我们有种类繁多的报表,因此我们希望可以把各种报表分门别类放入各自的目录。但是这么做之后,我们之前设置的 ViewLocationFormats
就无效了。例如我们访问 URL /EmployeeReport/Index
, Razor
会试图寻找 /EmployeeReport/Views/Index.cshtml
,但其真正的位置是 /Reports/AdHocReports/EmployeeReport/Views/Index.cshtml
。前面还有好几层目录呢~
因为所有的 View
文件不再位于同一个父级目录之下,因此 _ViewStart.cshtml
和 _ViewImports.cshtml
的作用将受到极大限制。原因后面细表。
下面我们来分别解决这2个问题。
最灵活的方式: IViewLocationExpander
有时候,我们的视图目录除了 controller
名称 和 view
名称2个变量外,还涉及到别的动态部分,比如上面的 Reports
相关 Controller
,视图路径有更深的目录结构,而 controller
名称仅代表末级的目录。此时,我们需要一种更灵活的方式来处理: IViewLocationExpander
,通过实现 IViewLocationExpander
,我们可以得到一个 ViewLocationExpanderContext
,然后据此更灵活地创建 view location formats
。
对于我们要解决的目录层次问题,我们首先需要观察,然后会发现目录层次结构和 Controller
类型的命名空间是有对应关系的。例如如下定义:
using Microsoft.AspNetCore.Mvc;
namespace CustomizedViewLocation.Reports.AdHocReports.EmployeeReport
{
public class EmployeeReportController : Controller
{
public IActionResult Index() => View();
}
}
观察 EmployeeReportController
的命名空间 CustomizedViewLocation.Reports.AdHocReports.EmployeeReport
以及 Index
视图对应的目录 /Reports/AdHocReports/EmployeeReport/Views/Index.cshtml
可以发现如下对应关系:
命名空间 | 视图路径 | ViewLocationFormat |
---|---|---|
CustomizedViewLocation | 项目根路径 | / |
Reports.AdHocReports | Reports/AdHocReports | 把整个命名空间以“.”为分割点掐头去尾,然后把“.”替换为“/” |
EmployeeReport | EmployeeReport | Controller 名称 |
Views | 固定目录 | |
Index.cshtml | 视图名称.cshtml |
所以我们 IViewLocationExpander
的实现类型主要是获取和处理 Controller
的命名空间。且看下面的代码。
// NamespaceViewLocationExpander.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.IO;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Controllers;
namespace CustomizedViewLocation
{
public class NamespaceViewLocationExpander : IViewLocationExpander
{
private const string VIEWS_FOLDER_NAME = "Views";
public IEnumerable<string> ExpandViewLocations(ViewLocationExpanderContext context, IEnumerable<string> viewLocations)
{
ControllerActionDescriptor cad = context.ActionContext.ActionDescriptor as ControllerActionDescriptor;
string controllerNamespace = cad.ControllerTypeInfo.Namespace;
int firstDotIndex = controllerNamespace.IndexOf('.');
int lastDotIndex = controllerNamespace.LastIndexOf('.');
if (firstDotIndex < 0)
return viewLocations;
string viewLocation;
if (firstDotIndex == lastDotIndex)
{
// controller folder is the first level sub folder of root folder
viewLocation = "/{1}/Views/{0}.cshtml";
}
else
{
string viewPath = controllerNamespace.Substring(firstDotIndex + 1, lastDotIndex - firstDotIndex - 1).Replace(".", "/");
viewLocation = $"/{viewPath}/{{1}}/Views/{{0}}.cshtml";
}
if (viewLocations.Any(l => l.Equals(viewLocation, StringComparison.InvariantCultureIgnoreCase)))
return viewLocations;
if (viewLocations is List<string> locations)
{
locations.Add(viewLocation);
return locations;
}
// it turns out the viewLocations from ASP.NET Core is List, so the code path should not go here.
List<string> newViewLocations = viewLocations.ToList();
newViewLocations.Add(viewLocation);
return newViewLocations;
}
public void PopulateValues(ViewLocationExpanderContext context)
{
}
}
}
上面对命名空间的处理略显繁琐。其实你可以不用管,重点是我们可以得到 ViewLocationExpanderContext
,并据此构建新的 view location format
然后与现有的 viewLocations
合并并返回给 ASP.NET Core
。
细心的同学可能还注意到一个空的方法 PopulateValues
,这玩意儿有什么用?具体作用可以参照这个 StackOverflow
的问题,基本上来说,一旦某个 Controller
及其某个 View
找到视图位置之后,这个对应关系就会缓存下来,以后就不会再调用 ExpandViewLocations
方法了。但是,如果你有这种情况,就是同一个 Controller
, 同一个视图名称但是还应该依据某些特别条件去找不同的视图位置,那么就可以利用 PopulateValues
方法填充一些特定的 Value
, 这些 Value
会参与到缓存键的创建, 从而控制到视图位置缓存的创建。
下一步,把我们的 NamespaceViewLocationExpander
注册一下:
// Startup.cs
public void ConfigureServices(IServiceCollection services)
{
IMvcBuilder mvcBuilder = services.AddMvc();
mvcBuilder.AddRazorOptions(options =>
{
// options.ViewLocationFormats.Add("/{1}/Views/{0}.cshtml"); we don't need this any more if we make use of NamespaceViewLocationExpander
options.ViewLocationExpanders.Add(new NamespaceViewLocationExpander());
});
}
另外,有了 NamespaceViewLocationExpander
, 我们就不需要前面对 ViewLocationFormats
的追加了,因为那种情况作为一种特例已经在 NamespaceViewLocationExpander
中处理了。
至此,目录分层的问题解决了。
_ViewStart.cshtml
和 _ViewImports
的起效机制与调整对这2个特别的视图,我们并不陌生,通常在 _ViewStart.cshtml
里面设置 Layout
视图,然后每个视图就自动地启用了那个 Layout
视图,在 _ViewImports.cshtml
里引入的命名空间和 TagHelper
也会自动包含在所有视图里。它们为什么会起作用呢?
_ViewImports
的秘密藏在 RazorTemplateEngine
类 和 MvcRazorTemplateEngine
类中。
MvcRazorTemplateEngine
类指明了 “_ViewImports.cshtml
” 作为默认的名字。
// MvcRazorTemplateEngine.cs 部分代码
// 完整代码: https://github.com/aspnet/Razor/blob/rel/2.0.0/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/MvcRazorTemplateEngine.cs
public class MvcRazorTemplateEngine : RazorTemplateEngine
{
public MvcRazorTemplateEngine(RazorEngine engine, RazorProject project)
: base(engine, project)
{
Options.ImportsFileName = "_ViewImports.cshtml";
Options.DefaultImports = GetDefaultImports();
}
}
RazorTemplateEngine
类则表明了 Razor
是如何去寻找 _ViewImports.cshtml
文件的。
// RazorTemplateEngine.cs 部分代码
// 完整代码:https://github.com/aspnet/Razor/blob/rel/2.0.0/src/Microsoft.AspNetCore.Razor.Language/RazorTemplateEngine.cs
public class RazorTemplateEngine
{
public virtual IEnumerable<RazorProjectItem> GetImportItems(RazorProjectItem projectItem)
{
var importsFileName = Options.ImportsFileName;
if (!string.IsNullOrEmpty(importsFileName))
{
return Project.FindHierarchicalItems(projectItem.FilePath, importsFileName);
}
return Enumerable.Empty<RazorProjectItem>();
}
}
FindHierarchicalItems
方法会返回一个路径集合,其中包括从视图当前目录一路到根目录的每一级目录下的_ViewImports.cshtml
路径。换句话说,如果从根目录开始,到视图所在目录的每一层目录都有 _ViewImports.cshtml
文件的话,那么它们都会起作用。这也是为什么通常我们在 根目录下的 Views
目录里放一个 _ViewImports.cshtml
文件就会被所有视图文件所引用,因为 Views
目录是是所有视图文件的父/
祖父目录。那么如果我们的 _ViewImports.cshtml
文件不在视图的目录层次结构中呢?
_ViewImports
文件的位置
在这个 DI
为王的 ASP.NET Core 世界里,RazorTemplateEngine
也被注册为 DI
里的服务,因此我目前的做法继承 MvcRazorTemplateEngine
类,微调 GetImportItems
方法的逻辑,加入我们的特定路径,然后注册到 DI
取代原来的实现类型。代码如下:
// ModuleRazorTemplateEngine.cs
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Mvc.Razor.Extensions;
using Microsoft.AspNetCore.Razor.Language;
namespace CustomizedViewLocation
{
public class ModuleRazorTemplateEngine : MvcRazorTemplateEngine
{
public ModuleRazorTemplateEngine(RazorEngine engine, RazorProject project) : base(engine, project)
{
}
public override IEnumerable<RazorProjectItem> GetImportItems(RazorProjectItem projectItem)
{
IEnumerable<RazorProjectItem> importItems = base.GetImportItems(projectItem);
return importItems.Append(Project.GetItem($"/Shared/Views/{Options.ImportsFileName}"));
}
}
}
然后在 Startup
类里把它注册到 DI
取代默认的实现类型。
// Startup.cs
// using Microsoft.AspNetCore.Razor.Language;
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<RazorTemplateEngine, ModuleRazorTemplateEngine>();
IMvcBuilder mvcBuilder = services.AddMvc();
// 其它代码省略
}
下面是 _ViewStart.cshtml
的问题了。不幸的是,Razor
对 _ViewStart.cshtml
的处理并没有那么“灵活”,看代码就知道了。
// RazorViewEngine.cs 部分代码
// 完整代码:https://github.com/aspnet/Mvc/blob/rel/2.0.0/src/Microsoft.AspNetCore.Mvc.Razor/RazorViewEngine.cs
public class RazorViewEngine : IRazorViewEngine
{
private const string ViewStartFileName = "_ViewStart.cshtml";
internal ViewLocationCacheResult CreateCacheResult(
HashSet<IChangeToken> expirationTokens,
string relativePath,
bool isMainPage)
{
var factoryResult = _pageFactory.CreateFactory(relativePath);
var viewDescriptor = factoryResult.ViewDescriptor;
if (viewDescriptor?.ExpirationTokens != null)
{
for (var i = 0; i < viewDescriptor.ExpirationTokens.Count; i++)
{
expirationTokens.Add(viewDescriptor.ExpirationTokens[i]);
}
}
if (factoryResult.Success)
{
// Only need to lookup _ViewStarts for the main page.
var viewStartPages = isMainPage ?
GetViewStartPages(viewDescriptor.RelativePath, expirationTokens) :
Array.Empty<ViewLocationCacheItem>();
if (viewDescriptor.IsPrecompiled)
{
_logger.PrecompiledViewFound(relativePath);
}
return new ViewLocationCacheResult(
new ViewLocationCacheItem(factoryResult.RazorPageFactory, relativePath),
viewStartPages);
}
return null;
}
private IReadOnlyList<ViewLocationCacheItem> GetViewStartPages(
string path,
HashSet<IChangeToken> expirationTokens)
{
var viewStartPages = new List<ViewLocationCacheItem>();
foreach (var viewStartProjectItem in _razorProject.FindHierarchicalItems(path, ViewStartFileName))
{
var result = _pageFactory.CreateFactory(viewStartProjectItem.FilePath);
var viewDescriptor = result.ViewDescriptor;
if (viewDescriptor?.ExpirationTokens != null)
{
for (var i = 0; i < viewDescriptor.ExpirationTokens.Count; i++)
{
expirationTokens.Add(viewDescriptor.ExpirationTokens[i]);
}
}
if (result.Success)
{
// Populate the viewStartPages list so that _ViewStarts appear in the order the need to be
// executed (closest last, furthest first). This is the reverse order in which
// ViewHierarchyUtility.GetViewStartLocations returns _ViewStarts.
viewStartPages.Insert(0, new ViewLocationCacheItem(result.RazorPageFactory, viewStartProjectItem.FilePath));
}
}
return viewStartPages;
}
}
上面的代码里 GetViewStartPages
方法是个 private
,没有什么机会让我们加入自己的逻辑。看了又看,好像只能从 _razorProject.FindHierarchicalItems(path, ViewStartFileName)
这里着手。这个方法同样在处理 _ViewImports.cshtml
时用到过,因此和 _ViewImports.cshtml
一样,从根目录到视图当前目录之间的每一层目录的 _ViewStarts.cshtml
都会被引入。如果我们可以调整一下 FindHierarchicalItems
方法,除了完成它原本的逻辑之外,再加入我们对我们 /Shared/Views
目录的引用就好了。而 FindHierarchicalItems
这个方法是在 Microsoft.AspNetCore.Razor.Language.RazorProject
类型里定义的,而且是个 virtual
方法,而且它是注册在 DI
里的,不过在 DI
中的实现类型是 Microsoft.AspNetCore.Mvc.Razor.Internal.FileProviderRazorProject
。我们所要做的就是创建一个继承自 FileProviderRazorProject
的类型,然后调整 FindHierarchicalItems
方法。
using System.Linq;
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc.Razor.Internal;
using Microsoft.AspNetCore.Razor.Language;
namespace CustomizedViewLocation
{
public class ModuleBasedRazorProject : FileProviderRazorProject
{
public ModuleBasedRazorProject(IRazorViewEngineFileProviderAccessor accessor)
: base(accessor)
{
}
public override IEnumerable<RazorProjectItem> FindHierarchicalItems(string basePath, string path, string fileName)
{
IEnumerable<RazorProjectItem> items = base.FindHierarchicalItems(basePath, path, fileName);
// the items are in the order of closest first, furthest last, therefore we append our item to be the last item.
return items.Append(GetItem("/Shared/Views/" + fileName));
}
}
}
完成之后再注册到 DI
。
// Startup.cs
// using Microsoft.AspNetCore.Razor.Language;
public void ConfigureServices(IServiceCollection services)
{
// services.AddSingleton(); // we don't need this any more if we make use of ModuleBasedRazorProject
services.AddSingleton<RazorProject, ModuleBasedRazorProject>();
IMvcBuilder mvcBuilder = services.AddMvc();
// 其它代码省略
}
有了 ModuleBasedRazorProject
我们甚至可以去掉之前我们写的 ModuleRazorTemplateEngine
类型了,因为 Razor
采用相同的逻辑 —— 使用 RazorProject
的 FindHierarchicalItems
方法 —— 来构建应用 _ViewImports.cshtml
和 _ViewStart.cshtml
的目录层次结构。所以最终,我们只需要一个类型来解决问题 —— ModuleBasedRazorProject
。
回顾这整个思考和尝试的过程,很有意思,最终解决方案是自定义一个 RazorProject
。是啊,毕竟我们的需求只是一个不同目录结构的 Razor Project
,所以去实现一个我们自己的 RazorProject
类型真是再自然不过的了。
1.添加HttpRequest
扩展方法
public static class RequestExtensions
{
//regex from http://detectmobilebrowsers.com/
private static readonly Regex b = new Regex(@"(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino", RegexOptions.IgnoreCase | RegexOptions.Multiline);
private static readonly Regex v = new Regex(@"1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-", RegexOptions.IgnoreCase | RegexOptions.Multiline);
public static bool IsMobileBrowser(this HttpRequest request)
{
var userAgent = request.UserAgent();
if ((b.IsMatch(userAgent) || v.IsMatch(userAgent.Substring(0, 4))))
{
return true;
}
return false;
}
public static string UserAgent(this HttpRequest request)
{
return request.Headers["User-Agent"];
}
}
2.新建CustomViewLocationExpander
继承IViewLocationExpander
public class CustomViewLocationExpander : IViewLocationExpander
{
public IEnumerable<string> ExpandViewLocations(ViewLocationExpanderContext context,
IEnumerable<string> viewLocations)
{
string name = context.Values["Theme"];
if (name != "Views")
{
viewLocations = viewLocations.Select(s => s.Replace("Views", name));
}
return viewLocations;
}
public void PopulateValues(ViewLocationExpanderContext context)
{
if (context.ActionContext.HttpContext.Request.IsMobileBrowser())
{
context.Values["Theme"] = "WapViews";
}
else
{
context.Values["Theme"] = "Views";
}
}
}
3.配置Startup
中的ConfigureServices
services.Configure<RazorViewEngineOptions>(o => {
o.ViewLocationExpanders.Add(new CustomViewLocationExpander());
});
4.在网站根目录创建WapViews
文件夹,里面内容和Views
的一样
有时候,在特定的情况下,虽然项目的PC端和移动端是同数据库,功能相同,但是页面的设计可能不尽相同,这就需要我们判断当前用户使用的是PC端还是移动端,以便我们准确的渲染页面。或者我们想要停用某一端就需要判断访问源是PC还是移动端
实例:
主要就是通过客户端传递的User-agent
来判断访问网站的客户端是PC还是手机,.NET
中就是
Request.ServerVariables[“HTTP_USER_AGENT”]。
比如正常pc是: Mozilla/5.0 (Windows NT 6.1; rv:27.0) Gecko/20100101 Firefox/27.0
常用手机的是:
Nokia5320的是: Nokia 5320/UCWEB7.0.1.34/28/999
HTC的安卓手机:Mozilla/5.0 (Linux; U; Android 2.2; zh-cn; HTC Desire Build/FRF91) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1;
iPhone的:Mozilla/5.0 (iPhone; U; CPU iPhone OS 3_1_2 like Mac OS X; zh-cn) AppleWebKit/528.18 (KHTML, like Gecko) Mobile/7D11
方法:通过正则表达式去匹配判断,代码如下:
ps:net core
获取http
信息与framwork
略有差异 https://blog.csdn.net/qq_39569480/article/details/105831712
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Abp.Authorization;
using Abp.Authorization.Users;
using Abp.MultiTenancy;
using Abp.Runtime.Security;
using Abp.UI;
using Hepu.Authentication.External;
using Hepu.Authentication.JwtBearer;
using Hepu.Authorization;
using Hepu.Authorization.Users;
using Hepu.Models.TokenAuth;
using Hepu.MultiTenancy;
using Abp.Runtime.Caching;
using Microsoft.AspNetCore.Http;
using System.Text.RegularExpressions;
namespace test.Controllers
{
[Route("api/[controller]/[action]")]
public class TokenAuthController : HepuControllerBase
{
private readonly IHttpContextAccessor _httpContextAccessor;
//注入 IHttpContextAccessor 获取http相关信息
public TokenAuthController(
IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
[HttpPost]
public async Task<AuthenticateResultModel> Authenticate([FromBody] AuthenticateModel model)
{
var headers = _httpContextAccessor.HttpContext.Request.Headers;
string str_u = headers["User-Agent"].FirstOrDefault();
//Regex b = new Regex(@"android.+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino", RegexOptions.IgnoreCase | RegexOptions.Multiline);
//Regex v = new Regex(@"1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(di|rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-", RegexOptions.IgnoreCase | RegexOptions.Multiline);
//if (!(b.IsMatch(str_u) || v.IsMatch(str_u.Substring(0, 4))))
if (str_u.Contains("Android")|| str_u.Contains("android"))
{
//
访问来源 = "Android";
Console.WriteLine(headers);
}
else
{
// Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.61 Safari/537.36
访问来源 = "PC";
Console.WriteLine(headers);
}
}
}
}