源宝导读:微软跨平台技术框架—.NET Core已经日趋成熟,已经具备了支撑大型系统稳定运行的条件。本文将介绍明源云ERP平台从.NET Framework向.NET Core迁移过程中的实践经验。
一、背景
随着ERP的产品线越来越多,业务关联也日益复杂,应用间依赖关系也变得错综复杂,单体架构的弱点日趋明显。19年初,由于平台底层支持了分应用部署模式,将ERP从应用子系统层面进行了切割分离,迈出了从单体架构向微服务架构转型的坚实一步。不久的将来,ERP会进一步将各业务拆分成众多的微服务,而微服务势必需要进行容器化部署和运行管理,这就要求ERP技术底层必须支持跨平台,所以将现有ERP系统从.NET Framework迁移到 .NET Core平台势在必行。
上一篇《【复杂系统迁移 .NET Core平台系列】之迁移项目工程》的文章中,我们分享了在改造.NET Core工程和类库过程中的经验,本文我们将分享在界面层改造过程中遇到的问题和解决思路。
ERP目前使用的是Asp.Net WebForm框架,在.NET Core中已经被废弃了,那么这部分就只有采用重写的方式。在Asp.Net Core Mvc中RazorPage的访问方式跟WebForm基于物理路径的访问提供了类似的机制;并且PageModel也跟Aspx的CodeBehind的方式类似。就是基于这样类似的方式,提供了结构和逻辑复用的可能性。基于这个起点,本篇将从如下步骤讲述界面层的改造过程:
页面路由:改造Url重定向的逻辑,定位到页面;
Aspx页面:改造Aspx页面为cshtml页面;
模板页面:改造Master为Layout页面;
服务端控件:改造服务端用户Ascx页面。
二、页面路由改造
ERP的页面有两种,一种是由建模配置出来的叫标准页面,由平台托管来提供功能;另一种是自定义页面,自己写Aspx实现所有业务逻辑。标准页面使用Url重写,而自定义页面直接用路径访问。在Asp.Net Core MVC中从Url映射到RazorPage是通过路由模块来实现的,为了支持以前的访问Url不变,我们需要对路由进行扩展。
区别于传统Asp.Net Mvc的页面通过路径加载,然后运行时编译不同,在.NET Core中默认是直接将*.cshtml页面编译到DLL中,然后通过AssemblyPart加载到MVC框架中,默认RazorPage的路径是DLL中相对于Page文件夹的路径。
例如下图所示的页面访问路径为“/PubPlatform/Nav/Home/Default”:
1、标准页面路由
举一个平台的标准页面Url的例子:“/std/00002212/822952a2-5091-e711-9bda-001583b5bd1a.aspx”。这个Url可以通过HttpModule重写为:
“/_frontend/templates/pages/Grid/template.aspx?FunctionCode=00002212&_pagename=822952a2-5091-e711-9bda-001583b5bd1a”。重写主要目的是找到“822952a2-5091-e711-9bda-001583b5bd1a”这个ID对应页面元数据内容,然后通过配置映射到物理路径的template.aspx页面。
RazorPage是通过IPageRouteModel Convention来提供路由的扩展的,提供了PageRouteModelConvention和FolderRoute ModelConvention类来实现扩展,这两种使用方式可以通过内置的AddPageRoute和AddFolderRouteModelConvention方法来实现,下面是平台使用文件夹路由转换的代码示例:
在上述代码中我们为所有/templates/pages路径下的页面,添加三个路由参数:
AppConst.FunctionCodeKey(FucntionCode):模块代码 对应上述Url中的00002212;
AppConst.PageNameKey(PageName):元数据标识 对应上述Url中的822952a2-5091-e711-9bda-001583b5bd1a;
AppConst.PageKey(Page):RazorPage页面的相对于Page文件夹的路径(框架自带)。
TemplateConstraint实现IActionConstraint用于在路由系统中决定URL是否匹配当前页面,还是仿照之前HttpModule中Url重写逻辑来做路由匹配即可实现Url路由到对应的RazorPage。
2、自定义页面路由
我们先看看改造前和改造后页面的组织结构对比:
在改造自定义页面路由的时候我们使用IPageRouteModelProvider扩展OnProviders Executing,扩展代码如下:
在标准页面中我们使用了MVC自带的扩展机制,自定义页面我们使用IPageRoute ModelProvider扩展OnProvidersExecuting,因为在标准页面中我们使用路径来做匹配,但是自定义页面中需要兼容产品页面路径和项目页面路径,使用了类似于黑名单和白名单的区别,所以分别采用了不同的策略。另外这里我们扩展了OnProvidersExecuted的方法,在所有路由加载完之后来进行路由替换,来实现项目页面替换产品页面。
三、页面改造
解决了路由访问的问题,接下来就要进行Aspx页面改造成cshtml了。首先我们简单介绍下RazorPage的一些基本特性:
前端通过cshmtl来渲染Html;
后端通过PageModel来处理业务逻辑;
通过OnGet或者OnPost来提供访问;
通过PageModel属性和BindProperty来实现参数到模型的绑定;
通过PageModel属性实现PageModel到cshtml页面的数据传递。
ERP所有的Aspx都没有*.cs的后台代码。这里简单介绍下ERP对Aspx页面的使用规范:
实现Html渲染逻辑;
OnLoad方法重写实现取数等简单后台操作;
页面回传请求通过Ajax处理 从功能特性可以发现我们不用处理传统Aspx的回传的请求。Aspx的OnLoad可以使用RazorPag的OnGet方法中来替换,然后赋值给PageModel的属性来传递给cshtml来绑定数据,页面中只需要将Aspx语法替换成Razor的语法即可。
语法转换工具可以从这里找到:https://github.com/telerik/razor-converter。
在之前ERP中由于缺少了继承自Page的基类所以在页面中会有重复代码,在新版本改造中我们将一些通用方法进行了抽取,做了如下的继承层级的改造:
有了这一层之后就能对一些例如权限、多语言、多时区等进行通用代码的封装。并且为以后通用逻辑等定义一个可扩展的地方。但是封装之后还有一个问题如果每个页面都需要去设置继承关系就会显得有些重复,并且容易犯错,在MVC中提供了如下两种简洁的方式:
_ViewStart中定义的逻辑全部页面生效;
_ViewImport按照目录优先级,路径靠近_ViewImport会重写上层的逻辑,对下层的页面生效。
四、模板页面改造
我们在做页面的时候一般都是需要使用Master页面来做布局封装的,首先我们来一张看看平台Aspx和cshtml的布局层级对比图 :
1、顶层Layout页面
原来的Master页面中有大量的重复代码,所以这里做了一层抽象。顶层Layout页面用来定义Html结构,通常html中将css部分放在header中,将script部分放在body的最下面。另外css和js分别可以分类为,第三方css/js,通用css/js,文件引用的css/js,系统自定义输出到界面的css/js和用户自定义的css和js,这些都是通过MVC的RenderSection方法,提供类似与类中virutal方法一样可以由页面重写。下面是示例代码:
2、模板页面
Nav模板页面定义Body结构,包括不同的页面布局:
_Nav.cshtml 用来定义带导航的页面;
_Json.cshtml 用来定义json页面;
_Content.cshtml 用来定义无导航的内容页面;
_Dialog.cshtml 用来定义弹框页面。
3、模板页面设置
在ERP中因为标准页面可以挂载到不同的模板中,例如常见的表单页面可以加载到带导航的页面中,也可以加载到弹出框中。而且指定模板的方式不是通过直接跟路径关联.由于使用了预编译的机制,并且页面的路径希望对其他引用者屏蔽,所以用名字代替页面路径来设置模板,从而让模板页面路径和使用者解耦。通过如下逻辑来替换:
在自定义页面中我们直接使用名字指定;
在标准建模页面中我们使用元数据中配置的名字来指定;
在url中通过_mp参数来指定模板的名字,兼容一些图书兼容场景,例如页面嵌套的业务场景;
最终为了兼容一套名称和路径对应关系,兼容Framework和Core我们使用了类似如下的映射方式。
五、服务端控件ascx改造
在Aspx的使用中为了提高代码的复用程度,一般都会使用Ascx的自定义控件,在MVC中有多种的替代方式,在比较单纯的数据绑定页面中我们使用了PartialView来替代,这部分主要在布局页面中使用,有如下模板:
Collapse.cshtml分类组件页面,默认收起左侧导航;
Footer.cshtml 底部组件,用于返回顶部;
Holder.cshtml 内容部件;
Navbar.cshtml 顶部导航组件,用于显示用户相关设置通知等信息;
Sidebar.cshtml 用于显示左边菜单。
为了将CSS,JS等资源加载更加集中化,我们也将布局页面中一些资源按照功能加载进行了加载。如下:
模块化sea.js;
默认的一些输出到界面的js变量,如用户名,时区,页面元数据等;
权限事件脚本等,(之前这部分可能会放在后台cs代码中,现在也放在部分页里面加载)。
除了这些简单逻辑之外,还有一些组件会直接使用后台的服务取数据,然后渲染页面这部分使用Core的Component(视图组件)来加载 例如tab页面的加载,这里需要取得上下文中用户信息,从而获取权限然后判断是否加载。
在标准页面中会用到列表,树列表等大控件,这类控件的逻辑在后台配合RazorEngine来渲染的,这部分对Asp.NET Core MVC正好兼容,在原来是通过自定义控件来提供访问,在Asp.NET MVC中是通过Html.XXXFor类似方法提供访问,在Core中为了让cshtml可读性更好,提供了TagHelper的方式,这里也将访问方式通过了TagHelper封装,代码示例:
六、总结
在Framework中有很多前端代码和后端代码糅合在一个Aspx页面中。有了RazorPage之后可以用cshtml来渲染界面,使用PageModel来处理数据,这样做到前端逻辑和后端逻辑的分离,并且根据职责将之前的不同功能的界面渲染需求,进行封装和职责的分离,使前端渲染逻辑更加清晰也更容易维护。在下一篇分享中,我们将介绍针对页面资源文件引用(例如CSS、JS文件)的迁移过程,大家敬请期待。
------ END ------
作者简介
熊同学: 研发工程师,目前负责ERP运行平台的设计与开发工作。
也许您还想看
.NET Core MVC扩展实践
分布式锁的实现与探索
ERP缓存实践经验分享