在非MVC项目中使用Razor引擎生成html

Razor模板引擎是ASP.NET MVC项目中的核心模块,用于模板文件渲染工作。它完美接管了传统项目中的Web Forms的职能,并且在新的跨平台框架.NET Core中延续。在官方提供的项目模板中,它都是配合MVC作为Views层工作的。那么,能不能把它单独提取出来为其它场景服务呢?

答案当然是肯定的!


需求背景

前段时间接手了一个旧项目改造,由于预算有限,老板并不打算另起炉灶开新项目。针对旧项目做了些评估后,有一项优化涉及到模板引擎改的改进,原项目使用的是NVelocity引擎,这是一个非常古老的asp.net模板引擎,功能改进中受到的限制有两个:

一个是它不支持向模板中传递实例化类对象(可以加静态类,但加动态类后调用是失败的)。

另一个是在方法调用时不支持可选参数(提示的错误是匹配不到对应的方法)。

因为本次调整涉及到网址格式优化,如果这两个特性支持不到,修改时会非常费时费力,并且可维护性很差(旧的网址格式是直接在模板中拼装的)。要解决这个问题,首先想到的是看下这个模板引擎有没有改进过的新版,查了下资料,官方版本已经没更新了。github上有一个项目,最后更新也是停留在3年前,看记录,也没改到什么实质性的内容。把项目源码下载来大概看了下,结构比较复杂,改造起来也没太大把握,那么下一步方案,就是看看有没有替代品了。


解决方案

首先想到的也是razor,但是没有单独使用过它,之前都是在mvc项目中直接用的。于是先在网上查了些相关的文章,当时找到过两篇,有一篇还是国外的,但实现代码看起来比较复杂,总觉得有些不必要的内容。不过得到了一个关键信息,razor模板引擎有一个独立项目,名叫 RazorEngine ,于是找到官方文档 https://antaris.github.io/RazorEngine/ 里面有不少示例代码,跟着摸索下来,要点功能一一测试通过,核心实现上其实很简单。

下面帖出本人实现代码和调用方式。由于是从旧项目改造,这个通用类的方法延用了原接口。

思路有几条

  1. 创建模板引擎相关的几个静态属性,初始化时根据视图目录创建公用的模板引擎服务

  2. 创建一个动态模型,另外创建一个Dictionary 关联到这个动态模型上,以便动态绑定键值

    // ITemplateDriver 是原模板引擎提取出来的接口文件
    public class RazorTemplate : ITemplateDriver
    {

        protected static ITemplateServiceConfiguration config = null;
        protected static bool isEngineCreated = false;
        protected static IRazorEngineService service;

        private dynamic model;
        private IDictionary<String, Object> modelDict;

        public RazorTemplate(string viewPath, bool isDebug = false)
        {
            if (!isEngineCreated)
            {
                viewPath = viewPath.TrimEnd('/', '\\') + "\\";
                ITemplateManager manager = null;
                if (isDebug)
                {
                    manager = new WatchingResolvePathTemplateManager(new List<string> { viewPath }, new InvalidatingCachingProvider());
                }
                else
                {
                    manager = new ResolvePathTemplateManager(new List<string> { viewPath });
                }
                config = new FluentTemplateServiceConfiguration(
                        c => c.WithEncoding(RazorEngine.Encoding.Html)
                        .IncludeNamespaces("MyCMS.Common") // 这里引需要在模板中调用的模块
                        .ManageUsing(manager)
                        .UseDefaultCompilerServiceFactory());
                service = RazorEngineService.Create(config);
                isEngineCreated = true;
            }
            model = new ExpandoObject();
            modelDict = model;
        }


        public void Put(string key, object value)
        {
            if (modelDict.ContainsKey(key))
            {
                modelDict[key] = value;
            }
            else
            {
                modelDict.Add(key, value);
            }
        }

        /// <summary>
        /// 构建模板
        /// </summary>
        /// <param name="templateFile"></param>
        /// <param name="isPartial"></param>
        /// <param name="masterName"></param>
        /// <returns></returns>
        public string BuildString(string templateFile)
        {
            var sw = new StringWriter();
            DateTime startTime = DateTime.Now;
            service.RunCompile(config.TemplateManager.GetKey(templateFile,ResolveType.Global,null), sw, null, model);
            ILog log = log4net.LogManager.GetLogger("log");
            if (log.IsDebugEnabled)
            {
                log.Debug("模板("+ templateFile + ")编译耗时 " + (DateTime.Now - startTime));
            }
            modelDict.Clear();
            return sw.ToString();
        }

        /// <summary>
        /// 输出到Response
        /// </summary>
        /// <param name="templateFile"></param>
        public void Display(string templateFile)
        {

            //从文件中读取模板
            string html = BuildString(templateFile);

            //输出
            HttpContext.Current.Response.Clear();
            HttpContext.Current.Response.Write(html);
            HttpContext.Current.Response.Flush();
            HttpContext.Current.Response.End();
        }



        /// <summary>
        /// 输出到文件
        /// </summary>
        /// <param name="templateFile"></param>
        /// <param name="filename"></param>
        /// <param name="htmlpath"></param>
        /// <returns></returns>
        public string saveFile(string templateFile, string filename, string htmlpath)
        {
            //从文件中读取模板
            string html = BuildString(templateFile);

            //输出到文件
            FileOperate.FolderCreate(htmlpath);
            FileOperate.WriteFile(htmlpath + filename, html, false);
                
            return html;
        }
    }

调用方法如下:

var razor = new RazorTemplate(Server.MapPath("~/views/"));
razor.Put("title","网站标题");
razor.Put("dataList",new List<Article>(){
new Article(){ ... },
new Article(){ ... },
new Article(){ ... },
});

// 直接输出
razor.Display("article_list");

// 保存html到文件
razor.saveFile("article_list", "article_list_1.html","~/cache/");

模板文件按照mvc中的写法一样,可以使用Layout,Include等

本机测试首次编译模板文件耗时1s左右 ,后续渲染都是在毫秒级的,这个跟模板渲染的内容有关