MVC系列——MVC源碼學習:打造本身的MVC框架(四:瞭解神奇的視圖引擎)

前言:經過以前的三篇介紹,咱們基本上完成了從請求發出到路由匹配、再到控制器的激活,再到Action的執行這些個過程。今天仍是趁熱打鐵,將咱們的View也來完善下,也讓整個系列相對完整,博主不但願爛尾。對於這個系列,經過學習源碼,博主也學到了不少東西,在此仍是把博主知道的先發出來,供你們參考。html

本文原創地址:http://www.cnblogs.com/landeanfen/p/6019719.htmlgit

MVC源碼學習系列文章目錄:github

1、自定義ActionResult

經過前三篇的介紹,咱們已經實現了Action方法的執行,可是咱們執行Action方法的時候都是在方法裏面直接使用Response輸出結果,這樣寫至關不爽,而且html等文本編輯實在太不方便,因而萌生了本身去實現View的想法,這個過程並不容易,但沒辦法,凡事都要勇於邁出第一步。json

在MVC裏面,咱們最多見的寫法多是這樣:後端

//返回視圖頁面
public ActionResult Index()
{
      return View();  
}

//返回請求的數據
public JsonResult Index()
{
    return Json(new {}, JsonRequestBehavior.AllowGet);
}

將JsonResult轉到定義能夠看到,其實JsonResult也是繼承自ActionResult這個抽象類的,這麼神奇的ActioResult,到底是個什麼東西呢。咱們先仿照MVC裏面的也定義一個本身的ActionResult抽象類。緩存

namespace Swift.MVC.MyRazor
{
    public abstract class ActionResult
    {
        public abstract void ExecuteResult(SwiftRouteData routeData);
    }
}

這個類很簡單,就是一個抽象類,下面一個抽象方法,約束實現類必需要實現這個方法,究竟這個類有什麼用?且看博主怎麼一步一步去實現它。app

2、ContentResult和JsonResult的實現

查看MVC源碼可知,ActionResult的實現類有不少個,博主這裏就挑幾個最經常使用的來講說。框架

在MVC裏面,ContentResult經常使用來向當前請求輸出文本內容信息,JsonResult經常使用來返回序列化過的json對象。咱們分別來實現它們:ide

namespace Swift.MVC.MyRazor
{
    public class ContentResult:ActionResult
    {
        //頁面內容
        public string Content { get; set; }

        //編碼方式
        public Encoding ContentEncoding { get; set; }

        //response返回內容的格式
        public string ContentType { get; set; }

        public override void ExecuteResult(Routing.SwiftRouteData routeData)
        {
            HttpResponse response = HttpContext.Current.Response;

            if (!string.IsNullOrEmpty(ContentType))
            {
                response.ContentType = ContentType;
            }
            else
            {
                response.ContentType = "text/html";
            }
            if (ContentEncoding != null)
            {
                response.ContentEncoding = ContentEncoding;
            }
            if (Content != null)
            {
                response.Write(Content);
            }
        }
    }
}
ContentResult.cs
namespace Swift.MVC.MyRazor
{
    public class JsonResult:ActionResult
    {
        public JsonResult()
        {
            JsonRequestBehavior = JsonRequestBehavior.DenyGet;
        }

        public JsonRequestBehavior JsonRequestBehavior { get; set; }

        public Encoding ContentEncoding { get; set; }

        public string ContentType { get; set; }

        public object Data { get; set; }

        public override void ExecuteResult(Routing.SwiftRouteData routeData)
        {
            HttpResponse response = HttpContext.Current.Response;
            if (!String.IsNullOrEmpty(ContentType))
            {
                response.ContentType = ContentType;
            }
            else
            {
                response.ContentType = "application/json";
            }
            if (ContentEncoding != null)
            {
                response.ContentEncoding = ContentEncoding;
            }
            JavaScriptSerializer jss = new JavaScriptSerializer();
            var json = jss.Serialize(Data);
            response.Write(json);
        }
    }

    public enum JsonRequestBehavior
    {
        AllowGet,
        DenyGet,
    }
}
JsonResult.cs

代碼不難理解,就是定義了當前Response的返回類型和編碼方式等等。接下來看看如何使用他們,爲了更加接近MVC的寫法,咱們在Controller基類裏面也定義一系列的「快捷方法」,何爲「快捷方法」,就是可以快速返回某個對象的方法,好比咱們在Controller.cs裏面增長以下幾個方法:函數

        protected virtual ContentResult Content(string content)
        {
            return Content(content, null);
        }

        protected virtual ContentResult Content(string content, string contentType)
        {
            return Content(content, contentType, null);
        }

        protected virtual ContentResult Content(string content, string contentType, Encoding contentEncoding)
        {
            return new ContentResult()
            {
                Content = content,
                ContentType = contentType,
                ContentEncoding = contentEncoding
            };
        }

        protected virtual JsonResult Json(object data, JsonRequestBehavior jsonBehavior)
        {
            return new JsonResult()
            {
                Data = data,
                JsonRequestBehavior = jsonBehavior
            };
        }

 咱們也按照MVC裏面的寫法,首先咱們新建一個控制器MyViewController.cs,裏面新增兩個方法:

namespace MyTestMVC.Controllers
{
    public class MyViewController:Controller
    {
        public ActionResult ContentIndex()
        {
            return Content("Hello", "text/html", System.Text.Encoding.Default);
        }

        public ActionResult JsonIndex()
        {
            var lstUser = new List<User>();
            lstUser.Add(new User() { Id = 1, UserName = "Admin", Age = 20, Address = "北京", Remark = "超級管理員" });
            lstUser.Add(new User() { Id = 2, UserName = "張三", Age = 37, Address = "湖南", Remark = "呵呵" });
            lstUser.Add(new User() { Id = 3, UserName = "王五", Age = 32, Address = "廣西", Remark = "呵呵" });
            lstUser.Add(new User() { Id = 4, UserName = "韓梅梅", Age = 26, Address = "上海", Remark = "呵呵" });
            lstUser.Add(new User() { Id = 5, UserName = "呵呵", Age = 18, Address = "廣東", Remark = "呵呵" });
            return Json(lstUser, JsonRequestBehavior.AllowGet);
        }
    }
}

看到這種用法,是否是似曾相識?注意,這裏的ActionResult並非MVC裏面的,而是上文咱們自定義的!沒錯,在原生的MVC裏面,這些方法也是這般定義的,由於以上封裝方法自己就是參考MVC的原理來實現的。

看着以上代碼,貌似大功告成,能夠直接測試運行了。是否是這樣呢?總感受少點東西呢。。。調試發現,咱們的Content()方法僅僅是返回了一個ContentResult對象,並無作其餘操做啊!按照上述定義思路,貌似應該調用ContentResult對象的ExecuteResult()方法纔對,由於這個方法裏面纔是真正的向當前的響應流裏面寫入返回信息。那麼這個ExecuteResult()方法究竟在哪裏調用呢?這裏,博主這樣調用了一下!在Controller.cs這個控制器的基類裏面,咱們修改了Execute()方法的邏輯:

  public abstract class Controller:ControllerBase,IDisposable
    {
        public override void Execute(SwiftRouteData routeData)
        {
            //1.獲得當前控制器的類型
            Type type = this.GetType();

            //2.從路由表中取到當前請求的action名稱
            string actionName = routeData.RouteValue["action"].ToString();

            //3.從路由表中取到當前請求的Url參數
            object parameter = null;
            if (routeData.RouteValue.ContainsKey("parameters"))
            {
                parameter = routeData.RouteValue["parameters"];
            }
            var paramTypes = new List<Type>();
            List<object> parameters = new List<object>();
            if (parameter != null)
            {
                var dicParam = (Dictionary<string, string>)parameter;
                foreach (var pair in dicParam)
                {
                    parameters.Add(pair.Value);
                    paramTypes.Add(pair.Value.GetType());
                }
            }

            //4.經過action名稱和對應的參數反射對應方法。
            //這裏第二個參數能夠不理會action字符串的大小寫,第四個參數決定了當前請求的action的重載參數類型
            System.Reflection.MethodInfo mi = type.GetMethod(actionName,
                BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase, null, paramTypes.ToArray(), null);

            //5.執行該Action方法
            var actionResult = mi.Invoke(this, parameters.ToArray()) as ActionResult;

            //6.獲得action方法的返回值,並執行具體ActionResult的ExecuteResult()方法。
            actionResult.ExecuteResult(routeData);
        }
    }

在以上步驟5裏面,執行對應的Action方法以後,取到返回值,這個返回值就是一個ActionResult實現類的實例,好比上文的ContentResult和JsonResult等,而後在步驟6裏面調用具體的ExecuteResult()方法。這樣應該就能解決咱們上述問題!咱們來看測試結果:

 

這樣,咱們這個簡單的ContentResult和JsonResult就大功告成了,縱觀整個過程,ContentResult和JsonResult的實現思路是相對比較簡單的,能夠說就是對當前響應流的輸出作了一些封裝而已。

3、解析視圖引擎原理

上面實現了ContentResult和JsonResult,都是針對具體的返回值類型來定義的。除此以外,在MVC裏面咱們還有一個使用最多的就是和頁面html打交道的ViewResult。

一、視圖引擎原理解析

1.一、若是沒有視圖引擎,當咱們但願經過一個url去請求一個頁面內容的時候,咱們的實現思路首先應該就是直接在後臺拼Html,而後將拼好的Html交給響應流輸出。上面說過,這種作法太古老,開發效率低,不易排錯,而且使得先後端不能分離。這一系列的問題也反映出視圖引擎的重要性。

1.二、最簡單視圖引擎的原理:根據博主的理解,用戶經過一個Url去請求一個頁面內容的時候,咱們首先定義一個靜態的html頁面,html裏面佈局和邏輯先寫好,而後經過請求的url去找到這個靜態的html,讀取靜態html裏面的文本,最後將讀取到的文本交由Response輸出給客戶端。固然,這只是一個最基礎的原理,沒有涉及模板以及模板語法,咱們一步一步來,先將基礎原理搞懂,再說其餘的。

1.三、在開始接觸.net裏面視圖引擎以前,博主但願根據本身的理解先自定義一個視圖引擎。說作咱就作,下面就着手來試試。

二、自定義視圖引擎

有了上面的原理作支撐,博主就來動手本身寫一個最基礎的視圖引擎試試了。

2.一、首先定義一個ViewResult去實現ActionResult

namespace Swift.MVC.MyRazor
{
    public class MyViewResult : ActionResult
    {
        public object Data { get; set; }

        public override void ExecuteResult(Routing.SwiftRouteData routeData)
        {
            HttpResponse response = HttpContext.Current.Response;

            response.ContentType = "text/html";

            //取當前view頁面的物理路徑
            var path = AppDomain.CurrentDomain.BaseDirectory + "Views/" + routeData.RouteValue["controller"] + "/" + routeData.RouteValue["action"] + ".html";
            var templateData = string.Empty;
            using (var fsRead = new FileStream(path, FileMode.Open))
            {
                int fsLen = (int)fsRead.Length;
                byte[] heByte = new byte[fsLen];
                int r = fsRead.Read(heByte, 0, heByte.Length);
                templateData = System.Text.Encoding.UTF8.GetString(heByte);
            }
            response.Write(templateData);
        }
    }
}

2.二、在Controller.cs裏面定義「快捷方法」

     protected virtual MyViewResult View()
        {
            return new MyViewResult();
        }

        protected virtual MyViewResult View(object data)
        {
            return new MyViewResult()
            { 
                Data = data 
            };
        }

而後在MyViewController.cs裏面添加Action

        public ActionResult ViewIndex()
        {
            return View();
        }

2.三、添加視圖ViewIndex

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
    <title></title>
</head>
<body>
    My First View
</body>
</html>

測試結果:

固然,這只是爲了理解視圖引擎的工做原理而寫的一個例子,實際中的視圖引擎確定要比這個複雜得多,至少得有模板吧,而後模板語法裏面還得定義後端數據映射到html裏面的規則,又得定義一套模板語法規則,其工做量不亞於一個開源項目,博主以爲就沒有必要再深刻研究這個自定義模板了,畢竟.net裏面有許多現成而且還比較好用的模板。

關於.net下面的視圖模板引擎,博主接觸過的主要有RazorEngine、NVelocity、VTemplate等,下面博主依次介紹下他們各自的用法。

4、RazorEngine實現視圖引擎

關於.net裏面的模板引擎,RazorEngine算是相對好用的,它是基於微軟的Razor之上包裝而成的一個能夠獨立使用的模板引擎。也就是說,保留了Razor的模板功能,可是使得Razor脫離於Asp.net MVC,可以在其它應用環境下使用,換句話說,你徹底能夠在你的控制檯程序上面使用模板語法。

RazorEngine是一個獨立的開源項目,項目的地址是https://github.com/Antaris/RazorEngine

關於RazorEngine的使用以及具體的語法,園子裏面也是一搜一大把,這裏博主就不展開細說,只是將一些用到的方法介紹下。

一、基礎用法

要使用RazorEngine,首先必需要安裝組件,咱們使用Nuget。

安裝完成以後就能夠在咱們的.net程序裏面調用了。

先來看一個最簡單的。

string template = "姓名: @Model.Name, 年齡:@Model.Age, 學校:@Model.School";
var result = Razor.Parse(template, new { Name = "小明", Age = 16, School = "育才高中" });

我猜你已經知道結果了吧,你猜的沒錯。

是否是和MVC裏面的cshtml頁面的使用方式很是像~~它使用@Model這種做爲佔位符,動態去匹配字符串裏面的位置,從而獲得以上結果。

除此以外,RazorEngine還提供了引擎的方式,以下。

string template = "姓名: @Model.Name, 年齡:@Model.Age, 學校:@Model.School";
var result = Engine.Razor.RunCompile(template, "templateKey", null, new { Name = "小明", Age = 16, School = "育才高中" });

結果相似:

博主調試發現,第一次獲得result的時候有點慢,查詢官方文檔才知道,第一次須要記錄緩存,緩存的key是該方法的第二個參數「templateKey」,當第二次加載的時候基本上就飛快了。

博主詳細瞭解了下RunCompile()這個方法,它是IRazorEngineService類型的一個擴展方法,這裏的四個參數都有它本身的做用:第一個是匹配的字符串;第二個是緩存的Key,上文已經說過;第三個是一個Type類型,表示第四個參數的類型,若是爲null,那麼第四個參數就爲dynamic類型;第四個參數固然就是具體的實體了。

綜合上述Razor.Parse()和Engine.Razor.RunCompile()兩種方式,按照官方的解釋,第一種是原來的用法,當你使用它的時候會發現它會提示方法已通過時,官方主推的是第二種方式。博主好奇心重試了下兩種方式的區別,發現第一次調用的時候二者耗時基本類似,刷新頁面發現,Razor.Parse()每次調用都會耗時那麼久,而RunCompile()方式進行了緩存,第一次耗時稍微多點,以後每次調用時間基本能夠忽略不計。或許這就是官方主推第二種方式的緣由吧。

看到這裏,有的小夥伴們就開始想了,既然這個模板這麼方便,那麼咱們定義一個html,裏面就按照上述template變量那麼寫,而後讀取html內容,再用模板匹配html內容是否是就能夠實現咱們的模板要求了呢?沒錯,思路確實是這樣,咱們來試一把。

var filepath = AppDomain.CurrentDomain.BaseDirectory + @"Views\" + routeData.RouteValue["controller"] + "\\" + routeData.RouteValue["action"] + ".html";
var fileContent = Engine.Razor.RunCompile(File.ReadAllText(filepath), "templateKey2", null, new { Name = "小明", Age = 16, School = "育才高中" });

而後對應的html內容以下:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
    <title></title>
</head>
<body>
    姓名: @Model.Name, 年齡:@Model.Age, 學校:@Model.School
</body>
</html>

獲得結果

 就是這麼簡單!

二、做爲視圖引擎的實現

 有了上面的一些嘗試做爲基礎,將RazorEngine做爲咱們框架的視圖引擎就簡單了。

2.一、先定義一個ActionResult的實現類。因爲緩存的key必須惟一,這裏使用filepath做爲緩存的Key,第一次加載緩存,以後訪問該頁面就很快了。

namespace Swift.MVC.MyRazor
{
    public class RazorEngineViewResult:ActionResult
    {
        public object Data { get; set; }

        public override void ExecuteResult(Routing.SwiftRouteData routeData)
        {
            var filepath = AppDomain.CurrentDomain.BaseDirectory + @"Views\" + routeData.RouteValue["controller"] + "\\" + routeData.RouteValue["action"] + ".html";
            var fileContent = Engine.Razor.RunCompile(File.ReadAllText(filepath), filepath, null, Data);
            HttpResponse response = HttpContext.Current.Response;

            response.ContentType = "text/html";
            response.Write(fileContent);
            
        }
    }
}

2.二、在Controller.cs裏面定義「快捷方法」

        protected virtual RazorEngineViewResult RazorEngineView()
        {
            return new RazorEngineViewResult();
        }

        protected virtual RazorEngineViewResult RazorEngineView(object data)
        {
            return new RazorEngineViewResult()
            {
                Data = data
            };
        }

2.三、在具體的控制器裏面調用

    public class MyViewController:Controller
    {
       public ActionResult ViewIndex()
        {
            return RazorEngineView(new { Name = "小明", Age = 16, School = "育才高中" });
        }
    }

2.四、對應的View頁面。咱們仍是用html代替,固然若是你想要用cshtml的文件,只須要改下上述文件路徑便可。

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
    <title></title>
</head>
<body>
    姓名: @Model.Name, 年齡:@Model.Age, 學校:@Model.School
</body>
</html>

獲得結果

5、NVelocity實現視圖引擎

關於NVelocity模板引擎,博主簡單從網上down了一個Helper文件。要使用它,首先仍是得安裝組件

首先給出VelocityHelper

 /// <summary>
    /// NVelocity模板工具類 VelocityHelper
    /// </summary>
    public class VelocityHelper
    {
        private VelocityEngine velocity = null;
        private IContext context = null;

        /// <summary>
        /// 構造函數
        /// </summary>
        /// <param name="templatDir">模板文件夾路徑</param>
        public VelocityHelper(string templatDir)
        {
            Init(templatDir);
        }

        /// <summary>
        /// 無參數構造函數
        /// </summary>
        public VelocityHelper() { }

        /// <summary>
        /// 初始話NVelocity模塊
        /// </summary>
        public void Init(string templatDir)
        {
            //建立VelocityEngine實例對象
            velocity = new VelocityEngine();

            //使用設置初始化VelocityEngine
            ExtendedProperties props = new ExtendedProperties();
            props.AddProperty(RuntimeConstants.RESOURCE_LOADER, "file");
            props.AddProperty(RuntimeConstants.FILE_RESOURCE_LOADER_PATH, HttpContext.Current.Server.MapPath(templatDir));
            //props.AddProperty(RuntimeConstants.FILE_RESOURCE_LOADER_PATH, Path.GetDirectoryName(HttpContext.Current.Request.PhysicalPath));
            props.AddProperty(RuntimeConstants.INPUT_ENCODING, "utf-8");
            props.AddProperty(RuntimeConstants.OUTPUT_ENCODING, "utf-8");

            //模板的緩存設置
            props.AddProperty(RuntimeConstants.FILE_RESOURCE_LOADER_CACHE, true);              //是否緩存
            props.AddProperty("file.resource.loader.modificationCheckInterval", (Int64)30);    //緩存時間(秒)

            velocity.Init(props);

            //爲模板變量賦值
            context = new VelocityContext();
        }

        /// <summary>
        /// 給模板變量賦值
        /// </summary>
        /// <param name="key">模板變量</param>
        /// <param name="value">模板變量值</param>
        public void Put(string key, object value)
        {
            if (context == null)
                context = new VelocityContext();
            context.Put(key, value);
        }

        /// <summary>
        /// 顯示模板
        /// </summary>
        /// <param name="templatFileName">模板文件名</param>
        public void Display(string templatFileName)
        {
            //從文件中讀取模板
            Template template = velocity.GetTemplate(templatFileName);
            //合併模板
            StringWriter writer = new StringWriter();
            template.Merge(context, writer);
            //輸出
            HttpContext.Current.Response.Clear();
            HttpContext.Current.Response.Write(writer.ToString());
            HttpContext.Current.Response.Flush();
            HttpContext.Current.Response.End();
        }

        /// <summary>
        /// 根據模板生成靜態頁面
        /// </summary>
        /// <param name="templatFileName"></param>
        /// <param name="htmlpath"></param>
        public void CreateHtml(string templatFileName, string htmlpath)
        {
            //從文件中讀取模板
            Template template = velocity.GetTemplate(templatFileName);
            //合併模板
            StringWriter writer = new StringWriter();
            template.Merge(context, writer);
            using (StreamWriter write2 = new StreamWriter(HttpContext.Current.Server.MapPath(htmlpath), false, Encoding.UTF8, 200))
            {
                write2.Write(writer);
                write2.Flush();
                write2.Close();
            }

        }

        /// <summary>
        /// 根據模板生成靜態頁面
        /// </summary>
        /// <param name="templatFileName"></param>
        /// <param name="htmlpath"></param>
        public void CreateJS(string templatFileName, string htmlpath)
        {
            //從文件中讀取模板
            Template template = velocity.GetTemplate(templatFileName);
            //合併模板
            StringWriter writer = new StringWriter();
            template.Merge(context, writer);
            using (StreamWriter write2 = new StreamWriter(HttpContext.Current.Server.MapPath(htmlpath), false, Encoding.UTF8, 200))
            {
                //write2.Write(YZControl.Strings.Html2Js(YZControl.Strings.ZipHtml(writer.ToString())));
                write2.Flush();
                write2.Close();
            }

        }
    }
VelocityHelper.cs

關於Velocity模板的語法,也沒啥好說的,直接在項目裏面將他們搭起來試試。

一、定義ActionResult的實現類VelocityViewResult

    public class VelocityViewResult:ActionResult
    {
        public object Data { get; set; }
        public override void ExecuteResult(Routing.SwiftRouteData routeData)
        {
        //這裏必須是虛擬路徑
            var velocity = new VelocityHelper(string.Format("~/Views/{0}/", routeData.RouteValue["controller"]));
            // 綁定實體model
            velocity.Put("model", Data);
            // 顯示具體html
            HttpResponse response = HttpContext.Current.Response;
            response.ContentType = "text/html";
            velocity.Display(string.Format("{0}.cshtml", routeData.RouteValue["action"].ToString()));
        }
    }

二、在Controller.cs裏面添加「快捷方法」

        protected virtual VelocityViewResult VelocityView()
        {
            return new VelocityViewResult();
        }

        protected virtual VelocityViewResult VelocityView(object data)
        {
            return new VelocityViewResult()
            {
                Data = data
            };
        }

三、在具體的控制器裏面調用

    public class MyViewController:Controller
    {       
        public ActionResult ViewIndex()
        {
            return VelocityView(new { Name = "小明", Age = 16, School = "育才高中" });
        }
    }

四、新建對應的視圖

上面咱們在測試RazorEngine引擎的時候,使用的是html代替,這裏咱們改用cshtml後綴的模板文件ViewIndex.cshtml

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title></title>
</head>
<body>
    <div>
        <h1>姓名: $model.Name</h1>
        <h1>年齡:$model.Age</h1>
        <h1>學校:$model.School</h1>
    </div>
</body>
</html>

這裏就是和RazorEngine不同的地方,不過很好理解,原來的@Model這裏用$model代替了而已。

測試結果

6、VTemplate實現視圖引擎

VTemplate模板引擎簡稱VT,好幾年前就據說過這麼一個東西,但一直沒研究它的語法,感受寫起來太麻煩,詳情能夠看看 這裏。這裏也不想展開說明了,由於以爲原理通了,實現起來就是模板語法的不一樣了,你們若是有興趣能夠本身去實現一套VT版本的視圖引擎。

7、總結

總結,又到了寫總結的時間了,好緊張~~這一篇主要解析了下MVC裏面View的原理以及使用模板引擎的實現,至此,咱們本身的MVC基本功能都已經有了,此係列暫時告一段落吧。仍是給出源碼地址:源碼下載

此係列文章,花了不少時間整理,可是也收到不少園友的打賞,在此感謝你們對博主的支持和厚愛。後續博主必定繼續努力,將更好的乾貨帶給你們!

若是你以爲本文可以幫助你,能夠右邊隨意 打賞 博主,也能夠 推薦 進行精神鼓勵。你的支持是博主繼續堅持的不懈動力。

本文原創出處:http://www.cnblogs.com/landeanfen/

歡迎各位轉載,可是未經做者本人贊成,轉載文章以後必須在文章頁面明顯位置給出做者和原文鏈接,不然保留追究法律責任的權利

相關文章
相關標籤/搜索