偷会闲, 看看博客园, 有筒子写了篇: ASP.NET的路由
我翻了翻两前的一份邮件, 是我当时在项目之余的时间研究的,那时还没用MVC,所有项目都是 WebForm 的.
该方案我觉得可行,但是某同志一句: 不是基于底层的路径映射,效率不高, 还是 URLRewrite 好. 我笑尿了. 算鸟,都是往事. 现在想想, 这位同志还是不错的, 和另外一些人一比, 还是很伟岸的(我真心这样认为).
正文
优化地址无非就两个选择 URLRewrite 和 MVC 里的路由(Route)
关于 URLRewrtie 和 Route 的区别,可参考:
http://www.infoq.com/cn/news/2008/11/urlrewriting
从 .NET 3.5 SP1 起, 微软把 MVC 路由单独抽出来,放到 System.Web.Routing 下, WebForm 程序从此可以用上路由了.
.NET 4 对路由做了改进, 使用起来很简单.. 我们的项目都是 .NET 4 的, SEO 以后肯定是要做的, 所以我试着对 Hotel.Online 做了一下路由.
现在把使用过程中的注意事项说一下.
1, Route 使URL 的层次目录改变了, 原来是 HotelInfo.aspx , 路由后,很可能是 HotelInfo/45956/2011-06-21 这一小小的改变,却带来了大的影响.
A, PostBack : 原来的 Form action = “HotelInfo.aspx” , 路由后变成 action=”2011-06-21” 了, PostBack 当然是不对的. 要避免这个问题,请在代码里加上这句:
this.Form.Action = Page.ResolveUrl("~/HotelInfo.aspx");
B, script 标签的问题 : 在页面里 link 标签的地址(href)会自动转换, 但是 script 标签的地址(src)却不会自动转换, 解决这个问题,有两个办法
l A, base , <base href=”/xxx” /> 如 <base href=”/xxx” /><script src=”main.js”></script> 就会自动指向 /xxx/main.js , 这个需要注意先后顺序, 如果 script 出现在 base 前, 则没有这个效果.
l 使用绝对地址. 为了简化, 和 ResourceSite 的处理方式一样, 我在 BasePage 里将 ~ 符号替换成了 http://xxx.xxx.xxxx/xxx , 所以, 只要继承 BasePage 的页面, 都可以直接写 <script src=”~/main.js”></script>
C, A标签的问题, 我看HTML代码里有很多 <a href=”###” onclick=”…” 的写法, 在不加 base 之前, ### 指向当前页, 但是加了 base 后, ### 就指向 base 设定的地址了. 为了避免这种情况, 应使用 href=”javascript:void()” onclick=”…” 或 href=”javascript:doSomething();return false;” 或干脆就不用 A 标签.
2, 使用Route 后访问地址从 HotelInfo.aspx?hid=45956&ds=2011-06-21&de=2011-06-25 变成了 HotelInfo/45956/2011-06-21/2011-06-25 , 使用 Request.QueryString 取不出 hid , ds, de 这些URL 参数了, 因为 hid, ds, de 就不是以 url 参数形式出现的. 要获取这些值, 就要使用另外一个东西: Page.RouteData.Values[key] 了, 这个 key 不区分大小写,和 QueryString 一样.
3, 脚本里的 POST / GET , 原来的页在都在同一个目录层次, 所以直接
var form = document.createElement("FORM");
form.action = "HotelList.aspx";
是没有问题的, 能找到 HotelList.aspx 这个地址, 但是现在目录层次改变了, 在这样写就会找不到地址, 就如第一点的 A 里所描述的. 要解决这个问题, 要使用 base 或绝对地址.
以上是我在对 Hotel.Online 做路由遇到的问题.
下面说说使用.
1, WebApplication 或 网站要是 .NET Framework 4 (也可以用 3.5 , 但是具体有什么不同,我没有去了解, 下面的示例代码是针对 4 的)
2, 引用 System.Web.Routing 这个命名空间.
3, 在网站启动的时候,注册路由. 一般是放到 Global 的 Application_Start 里. .NET 4 里,允许用另外的方法: PreApplicationStartMethod , 这个东西我以应用到 VMaster 里, 可参考 SharedMaster.Offline. VMasterInit
4, 注册路由是通过 RouteTable.Routes. MapPageRoute 或 Add 方法. MapPageRoute 是对 Add 方法的简化操作.
我的实现:
RouteItemBase.cs
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Web.Routing; 6 7 namespace XXX.Frameworks.Route { 8 /// <summary> 9 /// 10 /// </summary> 11 public abstract class RouteItemBase { 12 13 /// <summary> 14 /// 路由名称,必须唯一 15 /// </summary> 16 public abstract string RouteName { 17 get; 18 } 19 20 /// <summary> 21 /// 路由地址 22 /// </summary> 23 public abstract string RouteUrl { 24 get; 25 } 26 27 /// <summary> 28 /// 物理地址 29 /// </summary> 30 public abstract string PhyicalUrl { 31 get; 32 } 33 34 /// <summary> 35 /// 路由的默认值 36 /// </summary> 37 public abstract RouteValueDictionary Default { 38 get; 39 } 40 41 /// <summary> 42 /// 约束 43 /// </summary> 44 public abstract RouteValueDictionary Constraint { 45 get; 46 } 47 48 /// <summary> 49 /// 50 /// </summary> 51 /// <param name="routes"></param> 52 /// <param name="item"></param> 53 public static void Map(RouteCollection routes, RouteItemBase item) { 54 routes.MapPageRoute(item.RouteName , item.RouteUrl, item.PhyicalUrl, false, item.Default, item.Constraint); 55 } 56 57 /// <summary> 58 /// 59 /// </summary> 60 /// <returns></returns> 61 public abstract object Format(); 62 } 63 }
HotelInfoRoute
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Web.Routing; 6 using XXX.Frameworks.Const; 7 using XXX.Frameworks.Route; 8 9 namespace Hotel.Online.Route { 10 11 /// <summary> 12 /// 13 /// </summary> 14 public class HotelInfoRoute : RouteItemBase { 15 16 public object Hid { 17 get; 18 set; 19 } 20 21 public DateTime Ds { 22 get; 23 set; 24 } 25 26 public DateTime De { 27 get; 28 set; 29 } 30 31 32 33 #region RouteItemBase 34 public override string RouteName { 35 get { 36 return RouteNames.HotelInfo.ToString(); 37 } 38 } 39 40 41 public override string RouteUrl { 42 get { 43 return "HotelInfo/{hid}/{ds}/{de}"; 44 } 45 } 46 47 public override string PhyicalUrl { 48 get { 49 return "~/HotelInfo.aspx"; 50 } 51 } 52 53 public override RouteValueDictionary Default { 54 get { 55 return new RouteValueDictionary(new { 56 Ds = DateTime.Now.AddDays(1).ToString(DateFormat.Date), 57 De = DateTime.Now.AddDays(2).ToString(DateFormat.Date) 58 }); 59 } 60 } 61 62 public override RouteValueDictionary Constraint { 63 get { 64 return new RouteValueDictionary(new { 65 Hid = @"\d+", 66 Ds = @"\d{4}-\d{2}-\d{2}", 67 De = @"\d{4}-\d{2}-\d{2}" 68 }); 69 } 70 } 71 72 public override object Format() { 73 return new { 74 Hid = this.Hid, 75 Ds = this.Ds.ToString(DateFormat.Date), 76 De = this.De.ToString(DateFormat.Date) 77 }; 78 } 79 #endregion 80 } 81 }
注意 HotelInfoRoute 的 RouteUrl , HotelInfo/{hid}/{ds}/{de} Hid, ds ,de 即路由的参数, 不区分大小写.
为了编程方便( 强类型 ), 我同时在这个类里定义了 Hid, Ds , De 几个属性(注意:和路由参数完全一样, 不区分大小写), 这几个属性会在页面获取 Route Url 和获取路由参数里发挥作用,会面会说.
Global 里
RouteHelper.AutoLoadRoutes 是用来自动发现某个 Assembly 下所有继承自 RouteItemBase 的类, 并自动注册到当前的路由表中, 这样,就不用每加个路由,都要手动修改 Global了.
1 void Map(RouteCollection routes) { 2 routes.RouteExistingFiles = true; 3 routes.Ignore("{resource}.axd/{*pathInfo}"); 4 routes.Ignore("{*AllRes}", new { 5 AllRes = @".*?\.(?!aspx)(.*)" 6 }); 7 8 //RouteItemBase.Map(routes, new HotelInfoRoute()); 9 RouteHelper.AutoLoadRoutes(routes, typeof(HotelInfoRoute).Assembly); 10 } 11 12 void Application_Start(object sender, EventArgs e) { 13 Map(RouteTable.Routes); 14 …. 15 16 ….
页面里
var routeData = this.Page.RouteData.Values;
var HotelID = (routeData["hid"] ?? "").ToString().ToInt(-110),
这样写,有些隐患, 假如路由参数 {hid} 变成了 {hotelID} 了, 而你在页面里还在傻傻的用 hid , 当然是取不到值的, 怎么办呢? 用强类型可以很好的避免这个问题. 怎么用?
Page.RouteData.Values 是一个 RouteValueDictionary 类型, 继承自 IDictionary<string, object> , 我针对 IDctionary<string, object> 做了一个扩展方法.
1 public static T ToEntity<T>(this IDictionary<string, object> dict) where T : new() { 2 var t = typeof(T); 3 var ps = t.GetProperties(); 4 T tmp = new T(); 5 6 foreach (var p in ps) { 7 if (!p.CanWrite) 8 continue; 9 //var v = dict.Get(p.Name); 10 if (dict.ContainsKey(p.Name)) { 11 p.SetValue(tmp, Convert.ChangeType(dict[p.Name], p.PropertyType), null); 12 } 13 } 14 return tmp; 15 }
然后直接用:
this.RouteData = this.Page.RouteData.Values.ToEntity<HotelInfoRoute>(); 就把该页面的路由参数提取出来了
var HotelID 直接等于 RouteData.Hid 就是了.
获取路由后的地址, 被我简化成如下了,
1 public static string GetRoutedUrl<T>(T routeData) 2 where T : RouteItemBase { 3 return RouteTable.Routes.GetVirtualPath(HttpContextHelper.Current.Request.RequestContext, routeData.RouteName.ToString(), new RouteValueDictionary( 4 routeData.Format() 5 )).VirtualPath; 6 }
HttpContextHelper 的定义在 XXX.Frameworks.Extends.HttpContextHelper 中定义, 为什么不用 HttpContext? 因为单元测试的时候会有问题.
return RouteHelper.GetRoutedUrl<HotelInfoRoute>(new HotelInfoRoute() {
Hid = hotelID.ToString(),
Ds = this.BeginDate,
De = this.EndDate
});
还是强类型, 不怕改.
将以有地址重定向到新地址
HotelInfo.aspx 这个地址肯定以经被搜索引擎收录, 现在如果弃用这样的地址, 对SEO和以有用户来说, 是最不希望出现的.
如果把老地址301 到新地址,对SEO和用户不会有任何影响. 为了达到这个效果,我加了一个方法:
1 public static void RedirectToRouted(this Page page, string routeName) { 2 if (page.RouteData.Route == null) { 3 var context = HttpContextHelper.Current; 4 var querys = context.Request.QueryString; 5 var keys = querys.Cast<string>().ToList(); 6 7 RouteValueDictionary datas = new RouteValueDictionary(); 8 keys.ForEach((k) => { 9 datas.Add(k, querys[k]); 10 }); 11 12 var vp = RouteTable.Routes.GetVirtualPath(context.Request.RequestContext, routeName, new RouteValueDictionary(datas)); 13 context.Response.Status = "301 Moved Permanently"; 14 context.Response.RedirectLocation = vp.VirtualPath; 15 context.Response.End(); 16 } 17 }
在需要将老地址转成新地址的页面里,加上:
1 protected override void OnPreInit(EventArgs e) { 2 this.Page.RedirectToRouted( RouteNames.HotelInfo.ToString() ); 3 4 base.OnPreInit(e); 5 }