标签:domain div basedir odi load 语法 item 获得 using
一. 背景:
在开发过程中,我们有时候想要独立出一块业务模块,进行模块化开发。但是微软自带的dotnet core mvc 框架秉承了传统 dotnet framework mvc 框架,再UI 层面层面,难以进行业务区分,这就回导致整个项目想当庞大。在这种背景下,我开始思考,能不能使用模块化开发方案进,从而降低单个项目的业务复杂度。
二. 搜集:
在以往的开发过程中,我所了解到的模块化开发框架有 微软的 Orchard CMS 框架,最近这款框架也支持了.net core 版本。以及业界鼎鼎大名的Abp .net 框架,这款框架现在也支持了.net core 版本。
Orchard CMS
优势:Orchard CMS 功能完善,支持热插拔
劣势:学习成本较高,难以将单独的模块式开发功能拿到自己的项目中来。
Abp .Net
优势:功能完善,原生支持模块化开发
劣势:引入模块化开发需要引入成套的Apb .Net 框架,难以与现有项目进行结合。
通过搜集现有的框架,发现已有的框架引太过庞大,难以直接应用到现有项目中。在这个基础上,我们开始思考能不能通过dotnet core 现有的middleware功能来实现以个模块化开发框架。
三. 思路
要自定义Request Middleware 首先要处理的就是路由问题。我们的方法是,单独维护一个Module 内部的路由表,然后在Module 定义时将路由表保存起来,然后当请求到达时去查询路由表,然后执行对应的方法。
注:值得注意的是,我们这里的路由表支持参数路由。
1. 路由表
路由表的设计需要注意以下几点:
a) 路由的添加
b) 路由表通过依赖注入设定为单例模式
c) 路由的解析,需要区别 get post , ... 等不同类型,需要支持参数表达式 /path/{id} 这样的方式
在这里的路由表设计,以及定义路由表的方式参考了nodejs express 的写法,在定Module方法的时候同时将路由添加到路由表:
a) 路由的添加:
首先我们要解决的是路由添加的问题,我参考了nodejs express的定义方法通过以下代码完成路由的添加:
public class InstanceBDModule : OryxWebModule
{
public InstanceBDModule()
{
Get("/user/info", async ctx =>
{
});
}
}
注:Get 在这里为快捷方法,Get方法的含义是 通过get 请求 /user/info 这个路由并执行,在内部通过处理将路由值处理为 /user/info_get 并保存在路由表,同时还要将Get请求的处理方法委托进行保存。通过这种方式我们可以处理get,post ,websocket 等类型的请求。
1 public void Request(string route, string method, Func func)
2 {
3 RouteTable.Add(route + "_" + method, new RouteTableItem
4 {
5 Func = func,
6 Path = route,
7 Method = method
8 });
9 }
b) 路由表类的定义:
1 public class RouteTable:Dictionarystring, RouteTableItem>
2 {
3
4 }
c) 路由的解析:
1 public static IApplicationBuilder UseOryxWeb(this IApplicationBuilder applicationBuilder)
2 {
3
4 //通过applicationBuilder.Use 完成中间件处理
5 return applicationBuilder.Use(async (ctx, next) =>
6 {
7 var routeTable = ctx.RequestServices.GetService();
8
9 var serverRouteTable = ctx;10 //匹配路由表中的值,支持参数匹配
11 var targetRoute = matchRoute(ctx, routeTable);
12 //如果为websocket 请求,则单独处理websocket
13 if (targetRoute.Method == "ws" && ctx.WebSockets.IsWebSocketRequest)
14 {
15 //非关键代码,在此注释
16 }
17
18 if (targetRoute != null)
19 {
20 var routeTableItem = targetRoute;
21 var func = routeTableItem.Func;
22 if (func != null && ctx.Request.Method.ToLower() == routeTableItem.Method)
23 {
24 //包装HttpContext , 然后交由路由表中保存的委托方法处理
25 var webContext = new OryxWebContext(ctx);
26 webContext.RouteValue(targetRoute.RouteValue);
27 using (webContext.HttpContext.RequestServices.CreateScope())
28 {
29 webContext.Body = await webContext.HttpContext.Request.Body.GetString();
30 if (webContext.Body.IsTrue())
31 {
32 webContext.JsonObj = JObject.Parse(webContext.Body);
33 }
34 await func(webContext);
35 }
36 }
37 else
38 {
39 await next();
40 }
41 }
42 else
43 {
44 await next();
45 }
46 });
47 }
至此,我们完成了路由的请求、解析并处理的过程。处理过程中我们用到了MatchRoute 方法来解析路由模板中的参数,这个方法采用了 Microsoft.AspNetCore.Routing.Template.TemplateParser.Parse 的方法来解析路由模板,通过TemplateMatcher 来获取模板路由中的值,对此方法的使用我会在另一片文章中单独介绍。具体的使用方法,大家可以查看完整的源代码来学习。
2. 处理器
完成了路由表的设计,还要有配套的处理器设计(类似mvc中的action 的设计)。
Request请求:在以往的开发过程中,我很羡慕其他阵营中框架帮忙处理好请求内容,我们直接使用即可。但是dotnet core mvc在处理的时候经常会遇到一些莫名其妙的问题,主要是由于前段content-type配置与后端 不匹配导致了发送过来的body没有数据,调试起来很头疼。所以在设计处理器框架这部分,我直接将请求的数据进行预处理,将常见的一些数据 例如json, querystring 进行处理,然后将数据通过内置的body方法传递给处理器。
Response相应:dotnetcore mvc 内置了Razor engine ,他本身很不错,但是cshtml 是预编译的,而我想使用的是动态处理的模板引擎,这样可以实时修改模板内容,实时更改页面。尝试了很多方式,razor engine 单独使用并不理想,所以在此框架内部我选择使用了scriban 模板引擎(dotnet liquid 也是一款非常不错的模板引擎,并且由于使用广泛,还可以兼容nodejs php使用同一种模板语法的模板引擎 )
在这里我们最终将Request和Response包装成OryxWebContext,代码如下:
Request 包含: get post websocket 处理
Reponse包含: Ajax WriteString RenderTemplate Send(websocket)方法
1 public class OryxWebContext
2 {
3 public HttpContext HttpContext { get; }
4
5 public WebSocket WebSocket { get; set; }
6
7 public Dictionarystring, string> ParamDictionary { get; }
8
9 public string Body { get; set; }
10
11 public dynamic JsonObj { get; set; }
12
13 public T Json()
14 {
15 var setting = new JsonSerializerSettings();
16 return JsonConvert.DeserializeObject(Body, setting);
17 }
18
19 public async Task Send(string content)
20 {
21 var buffer = new byte[1024 * 4];
22 var arrByte = new ArraySegmentbyte>(Encoding.UTF8.GetBytes(content));
23 await WebSocket.SendAsync(arrByte, WebSocketMessageType.Text, true, CancellationToken.None);
24 }
25
26 public Stream Stream { get; set; }
27
28 public OryxWebContext(HttpContext httpContext)
29 {
30 HttpContext = httpContext;
31 ParamDictionary = new Dictionarystring, string>();
32 HttpContext.Request.Query.ToList().ForEach(item =>
33 {
34 ParamDictionary.Add(item.Key, item.Value);
35 });
36 }
37
38 public async Task Write(string content)
39 {
40 await HttpContext.Response.WriteAsync(content);
41 }
42 public async Task Write(Stream stream)
43 {
44 HttpContext.Response.ContentType = "application/octet-stream";
45 await stream.CopyToAsync(HttpContext.Response.Body);
46 }
47
48 public async Task Ajax(object jsonObj)
49 {
50 HttpContext.Response.ContentType = "application/json";
51 var jsonSetting = new JsonSerializerSettings();
52 jsonSetting.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
53 var jsonStr = JsonConvert.SerializeObject(jsonObj, jsonSetting);
54 await HttpContext.Response.WriteAsync(jsonStr);
55 }
56
57 public async Task Render(string tempPath, object dataObj)
58 {
59 var tmpStr = ReadTemplateStr(tempPath);
60
61 TemplateContext ctx = new TemplateContext();
62 ctx.TemplateLoader = new OryxTemplateLoader(@"\OryxWeb\Shared");
63
64 var scriptObject = new ScriptObject();
65 scriptObject.Import(dataObj);
66 ctx.PushGlobal(scriptObject);
67
68 var template = Template.Parse(tmpStr);
69 var result = await template.RenderAsync(ctx);
70 HttpContext.Response.ContentType = "text/html";
71 await HttpContext.Response.WriteAsync(result);
72 }
73
74 public async Task Render(string tempPath)
75 {
76 var tmpStr = ReadTemplateStr(tempPath);
77 TemplateContext ctx = new TemplateContext();
78 ctx.TemplateLoader = new OryxTemplateLoader(@"OryxWeb\Shared");
79
80 var baseDir = AppContext.BaseDirectory;
81 var dir = baseDir + Path.GetDirectoryName(tempPath);
82
83 //var ll = ctx.TemplateLoader.GetPath(ctx, callerSpan, "index.html");
84
85 var template = Template.Parse(tmpStr, dir,
86 lexerOptions: new LexerOptions()
87 {
88 EnableIncludeImplicitString = true
89 ,
90 Mode = ScriptMode.Default
91 });
92
93 var result = await template.RenderAsync(ctx);
94 //var eva = Template.Evaluate(tmpStr, ctx);
95
96 HttpContext.Response.ContentType = "text/html";
97 await HttpContext.Response.WriteAsync(result);
98 }
99
100 public async Task RenderWithLayout(string tmepPath, string LayoutPath)
101 {
102 TemplateContext ctxsub = new TemplateContext();
103 //获得子页面
104 var subTemplate = GetTemplate(tmepPath, ctxsub);
105 var tempResult = subTemplate.RenderAsync();
106
107 //将父页面作为参数传入Layout
108 TemplateContext ctxLayout = new TemplateContext();
109 var scriptObject = new ScriptObject();
110 scriptObject.Import(new { renderbody = tempResult });
111 ctxLayout.PushGlobal(scriptObject);
112 var layoutTemplate = GetTemplate(LayoutPath, ctxLayout);
113 var result = await layoutTemplate.RenderAsync(ctxLayout);
114
115 HttpContext.Response.ContentType = "text/html";
116 await HttpContext.Response.WriteAsync(result);
117 }
118
119 public Template GetTemplate(string tempPath, TemplateContext ctx)
120 {
121 var tmpStr = ReadTemplateStr(tempPath);
122 ctx.TemplateLoader = new OryxTemplateLoader(@"OryxWeb\Shared");
123
124 var callerSpan = new SourceSpan();
125 var baseDir = AppContext.BaseDirectory;
126 var dir = baseDir + Path.GetDirectoryName(tempPath);
127
128 callerSpan.FileName = dir + "\\index.html";
129
130 //var ll = ctx.TemplateLoader.GetPath(ctx, callerSpan, "index.html");
131
132 var template = Template.Parse(tmpStr, dir,
133 lexerOptions: new LexerOptions()
134 {
135 EnableIncludeImplicitString = true
136 ,
137 Mode = ScriptMode.Default
138 });
139 return template;
140 }
141
142 private string ReadTemplateStr(string path)
143 {
144 var absolutePath = MapPath(path);
145 return File.ReadAllText(absolutePath);
146 }
147
148 private string MapPath(string path)
149 {
150 return Path.Combine(AppDomain.CurrentDomain.BaseDirectory, path.TrimStart(‘/‘).Replace("/", "\\"));
151 }
152
153 public void RouteValue(RouteValueDictionary routeValue)
154 {
155 if (routeValue != null)
156 {
157 routeValue.ToList().ForEach(item =>
158 {
159 ParamDictionary.Add(item.Key, item.Value.ToString());
160 });
161 }
162 }
163
164 public string this[string key]
165 {
166 get
167 {
168 if (!ParamDictionary.ContainsKey(key))
169 {
170 return string.Empty;
171 }
172 return ParamDictionary[key];
173 }
174 }
175
176 public T Service()
177 {
178 return HttpContext.RequestServices.GetService();
179 }
180 }
3. Startup
dotnet core startup 类主要有两个入口,一个用来管理类依赖注入的ConfigureServices ,一个用来配置请求Middleware 的Configure 方法。我们主要通过自定义处理 Request Middleare 来完成我们的请求处理,同时通过依赖注入将宿主的类实例引入到我们的模块中使用,Startup.cs :
1 public class Startup
2 {
3 public Startup(IConfiguration configuration)
4 {
5 Configuration = configuration;
6 }
7
8 public IConfiguration Configuration { get; }
9
10 // This method gets called by the runtime. Use this method to add services to the container.
11 public void ConfigureServices(IServiceCollection services)
12 {
13 services.AddOryxWeb();
14 }
15
16 // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
17 public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IServiceProvider serviceProvider)
18 {
19 app.UseOryxWeb();
20 }
21 }
完整的代码地址:https://github.com/OryxLib/Oryx.FastAdmin/tree/master/Libs/Oryx.Web.Core
[.Net Core] 基于.netcore middleware 机制, 实现模块化开发框架
标签:domain div basedir odi load 语法 item 获得 using
原文地址:https://www.cnblogs.com/blackcatpolice/p/13903299.html