[后端人員耍前端系列]KnockoutJs篇:使用WebApi+Bootstrap+KnockoutJs打造單頁面程序


一、前言

  在前一個專題快速介紹了KnockoutJs相關知識點,也寫了一些簡單例子,希望通過這些例子大家可以快速入門KnockoutJs。為了讓大家可以清楚地看到KnockoutJs在實際項目中的應用,本專題將介紹如何使用WebApi+Bootstrap+KnockoutJs+Asp.net MVC來打造一個單頁面Web程序。這種模式也是現在大多數公司實際項目中用到的。

二、SPA(單頁面)好處

  在介紹具體的實現之前,我覺得有必要詳細介紹了SPA。SPA,即Single Page Web Application的縮寫,是加載單個HTML 頁面並在用戶與應用程序交互時動態更新該頁面的Web應用程序。瀏覽器一開始會加載必需的HTML、CSS和JavaScript,所有的操作都在這張頁面上完成,都由JavaScript來控制。

  單頁面程序的好處在於:

  1. 更好的用戶體驗,讓用戶在Web app感受native app的速度和流暢。
  2. 分離前后端關注點,前端負責界面顯示,后端負責數據存儲和計算,各司其職,不會把前后端的邏輯混雜在一起。
  3. 減輕服務器壓力,服務器只用生成數據就可以,不用管展示邏輯和頁面邏輯,增加服務器吞吐量。MVC中Razor語法寫的前端是需要服務器完成頁面的合成再輸出的。
  4. 同一套后端程序,可以不用修改直接用於Web界面、手機、平板等多種客戶端。

  當然單頁面程序除了上面列出的優點外,也有其不足:

  1. 不利於SEO。這點如果是做管理系統的話是沒影響的
  2. 初次加載時間相對增加。因為所有的JS、CSS資源會在第一次加載完成,從而使得后面的頁面流暢。對於這點可以使用Asp.net MVC中Bundle來進行文件綁定。關於Bundle的詳細使用參考文章:http://www.cnblogs.com/xwgli/p/3296809.htmlhttp://www.cnblogs.com/wangiqngpei557/p/3309812.html
  3. 導航不可用。如果一定要導航需自行實現前進、后退。對於這點,可以自行實現前進、后退功能來彌補。其實現在手機端網頁就是這么干的,現在還要上面導航的。對於一些企業后台管理系統,也可以這么做。
  4. 對開發人員技能水平、開發成本高。對於這點,也不是事,程序員嘛就需要不斷學習來充電,好在一些前端框架都非常好上手。

三、使用Asp.net MVC+WebAPI+Bootstrap+KnockoutJS實現SPA

  前面詳細介紹了SPA的優缺點,接下來,就讓我們使用Asp.net MVC+WebAPI+BS+KO來實現一個單頁面程序,從而體驗下SPA流暢和對原始Asp.net MVC +Razor做出來的頁面進行效果對比。

  1. 使用VS2013創建Asp.net Web應用程序工程,勾選MVC和WebAPI類庫。具體見下圖:

  

  2. 創建對應的倉儲和模型。這里演示的是一個簡單任務管理系統。具體的模型和倉儲代碼如下:

  任務實體類實現:

public enum TaskState
    {
        Active = 1,
        Completed =2
    }

    /// <summary>
    /// 任務實體
    /// </summary>
    public class Task
    {
        public int Id { get; set; }

        public string Name { get; set; }
        public string Description { get; set; }

        public DateTime CreationTime { get; set; }

        public DateTime FinishTime { get; set; }

        public string Owner { get; set; }
        public TaskState State { get; set; }

        public Task()
        {
            CreationTime = DateTime.Parse(DateTime.Now.ToLongDateString());
            State = TaskState.Active;
        }
    }

 

  任務倉儲類實現:

/// <summary>
    /// 這里倉儲直接使用示例數據作為演示,真實項目中需要從數據庫中動態加載
    /// </summary>
    public class TaskRepository
    {
        #region Static Filed
        private static Lazy<TaskRepository> _taskRepository = new Lazy<TaskRepository>(() => new TaskRepository());

        public static TaskRepository Current
        {
            get { return _taskRepository.Value; }
        }

        #endregion 

        #region Fields
        private readonly List<Task> _tasks = new List<Task>()
        {
            new Task
            {
                Id =1,
                Name = "創建一個SPA程序",
                Description = "SPA(single page web application),SPA的優勢就是少量帶寬,平滑體驗",
                Owner = "Learning hard",
                FinishTime = DateTime.Parse(DateTime.Now.AddDays(1).ToString(CultureInfo.InvariantCulture))
            },
            new Task
            {
                Id =2,
                Name = "學習KnockoutJs",
                Description = "KnockoutJs是一個MVVM類庫,支持雙向綁定",
                Owner = "Tommy Li",
                FinishTime = DateTime.Parse(DateTime.Now.AddDays(2).ToString(CultureInfo.InvariantCulture))
            },
            new Task
            {
                Id =3,
                Name = "學習AngularJS",
                Description = "AngularJs是MVVM框架,集MVVM和MVC與一體。",
                Owner = "李志",
                FinishTime = DateTime.Parse(DateTime.Now.AddDays(3).ToString(CultureInfo.InvariantCulture))
            },
            new Task
            {
                Id =4,
                Name = "學習ASP.NET MVC網站",
                Description = "Glimpse是一款.NET下的性能測試工具,支持asp.net 、asp.net mvc, EF等等,優勢在於,不需要修改原項目任何代碼,且能輸出代碼執行各個環節的執行時間",
                Owner = "Tonny Li",
                FinishTime = DateTime.Parse(DateTime.Now.AddDays(4).ToString(CultureInfo.InvariantCulture))
            },
        };

        #endregion 

        #region Public Methods
        public IEnumerable<Task> GetAll()
        {
            return _tasks;
        }

        public Task Get(int id)
        {
            return _tasks.Find(p => p.Id == id);
        }

        public Task Add(Task item)
        {
            if (item == null)
            {
                throw new ArgumentNullException("item");
            }

            item.Id = _tasks.Count + 1;
            _tasks.Add(item);
            return item;
        }

        public void Remove(int id)
        {
            _tasks.RemoveAll(p => p.Id == id);
        }

        public bool Update(Task item)
        {
            if (item == null)
            {
                throw new ArgumentNullException("item");
            }

            var taskItem = Get(item.Id);
            if (taskItem == null)
            {
                return false;
            }

            _tasks.Remove(taskItem);
            _tasks.Add(item);
            return true;
        }
        #endregion 
    }

  3. 通過Nuget添加Bootstrap和KnockoutJs庫。

  4. 實現后端數據服務。這里后端服務使用Asp.net WebAPI實現的。具體的實現代碼如下:

  /// <summary>
    /// Task WebAPI,提供數據服務
    /// </summary>
    public class TasksController : ApiController
    {
        private readonly TaskRepository _taskRepository = TaskRepository.Current;

        public IEnumerable<Task> GetAll()
        {
            return _taskRepository.GetAll().OrderBy(a => a.Id);
        }

        public Task Get(int id)
        {
            var item = _taskRepository.Get(id);
            if (item == null)
            {
                throw new HttpResponseException(HttpStatusCode.NotFound);
            }

            return item;
        }

        [Route("api/tasks/GetByState")]
        public IEnumerable<Task> GetByState(string state)
        {
            IEnumerable<Task> results = new List<Task>();
            switch (state.ToLower())
            {
                case "":
                case "all":
                    results = _taskRepository.GetAll();
                    break;
                case "active":
                    results = _taskRepository.GetAll().Where(t => t.State == TaskState.Active);
                    break;
                case "completed":
                    results = _taskRepository.GetAll().Where(t => t.State == TaskState.Completed);
                    break;
            }

            results = results.OrderBy(t => t.Id);
            return results;
        }

        [HttpPost]
        public Task Create(Task item)
        {
           return _taskRepository.Add(item);
        }

        [HttpPut]
        public void Put(Task item)
        {
            if (!_taskRepository.Update(item))
            {
                throw new HttpResponseException(HttpStatusCode.NotFound);
            }
        }

        public void Delete(int id)
        {
            _taskRepository.Remove(id);
        }
    }
View Code

  5. 使用Asp.net MVC Bundle對資源進行打包。對應的BundleConfig實現代碼如下:

/// <summary>
    /// 只需要補充一些缺少的CSS和JS文件。因為創建模板的時候已經添加了一些CSS和JS文件
    /// </summary>
    public class BundleConfig
    {
        // For more information on bundling, visit http://go.microsoft.com/fwlink/?LinkId=301862
        public static void RegisterBundles(BundleCollection bundles)
        {
            bundles.Add(new ScriptBundle("~/bundles/jquery").Include(
                        "~/Scripts/jquery-{version}.js"));

            bundles.Add(new ScriptBundle("~/bundles/jqueryval").Include(
                        "~/Scripts/jquery.validate*"));

            // Use the development version of Modernizr to develop with and learn from. Then, when you're
            // ready for production, use the build tool at http://modernizr.com to pick only the tests you need.
            bundles.Add(new ScriptBundle("~/bundles/modernizr").Include(
                        "~/Scripts/modernizr-*"));

            bundles.Add(new ScriptBundle("~/bundles/bootstrap").Include(
                      "~/Scripts/bootstrap.js",
                      "~/Scripts/bootstrap-datepicker.min.js"));

            bundles.Add(new StyleBundle("~/Content/css").Include(
                      "~/Content/bootstrap.css",
                      "~/Content/bootstrap-datepicker3.min.css",
                      "~/Content/site.css"));

            bundles.Add(new ScriptBundle("~/bundles/knockout").Include(
                      "~/Scripts/knockout-{version}.js",
                    "~/Scripts/knockout.validation.min.js",
                    "~/Scripts/knockout.mapping-latest.js"));

            bundles.Add(new ScriptBundle("~/bundles/app").Include(
                "~/Scripts/app/app.js"));
        }
    }
View Code

  6. 因為我們需要在頁面上使得枚舉類型顯示為字符串。默認序列化時會將枚舉轉換成數值類型。所以要對WebApiConfig類做如下改動:

public static class WebApiConfig
    {
        public static void Register(HttpConfiguration config)
        {
            // Web API 配置和服務

            // Web API 路由
            config.MapHttpAttributeRoutes();

            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );

            // 使得序列化使用駝峰式大小寫風格序列化屬性
            config.Formatters.JsonFormatter.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
            // 將枚舉類型在序列化時序列化字符串
            config.Formatters.JsonFormatter.SerializerSettings.Converters.Add(new StringEnumConverter());
        }
    }

  注:如果上面沒有使用駝峰小寫風格序列化的話,在頁面綁定數據的時候也要進行調整。如綁定的Name屬性的時候直接使用Name大寫,如果使用name方式會提示這個屬性沒有定義錯誤。由於JS是使用駝峰小寫風格對變量命名的。所以建議大家加上使用駝峰小寫風格進行序列化,此時綁定的時候只能使用"name"這樣的形式進行綁定。這樣也更符合JS代碼的規范。 

  7. 修改對應的Layout文件和Index文件內容。

  Layout文件具體代碼如下:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title> Learninghard SPA Application</title>
    @Styles.Render("~/Content/css")
    @Scripts.Render("~/bundles/modernizr")
</head>
    <body>
        <div class="navbar navbar-inverse navbar-fixed-top">
            <div class="container">
                <div class="navbar-header">
                    <p class="navbar-brand">簡單任務管理系統</p>
                </div>
                <div class="navbar-collapse collapse">
                    <ul class="nav navbar-nav">
                        <li class="active"><a href="/">主頁</a></li>
                    </ul>
                </div>
            </div>
        </div>

        <div class="container body-content" id="main">
            @RenderBody()
            <hr />
            <footer>
                <p>&copy; @DateTime.Now.Year - Learninghard SPA Application</p>
            </footer>
        </div>

        @Scripts.Render("~/bundles/jquery")
        @Scripts.Render("~/bundles/bootstrap")
        @Scripts.Render("~/bundles/knockout")
        @Scripts.Render("~/bundles/app")
    </body>
</html>
View Code

  Index頁面代碼如下:

@{
    ViewBag.Title = "Index";
    Layout = "~/Views/Shared/_Layout.cshtml";
}


<div id="list" data-bind="if:canCreate">
<h2>Tasks</h2>
<div class="table-responsive">
    <table class="table table-striped">
        <thead>
            <tr>
                <th>編號</th>
                <th>名稱</th>
                <th>描述</th>
                <th>負責人</th>
                <th>創建時間</th>
                <th>完成時間</th>
                <th>狀態</th>
                <th></th>
            </tr>
        </thead>
        <tbody data-bind="foreach:tasks">
            <tr>
                <td data-bind="text: id"></td>
                <td><a data-bind="text: name, click: handleCreateOrUpdate"></a></td>
                <td data-bind="text: description"></td>
                <td data-bind="text: owner"></td>
                <td data-bind="text: creationTime"></td>
                <td data-bind="text: finishTime"></td>
                <td data-bind="text: state"></td>
                <td><a class="btn btn-xs btn-primary" data-bind="click:remove" href="javascript:void(0)">Remove</a></td>
            </tr>
        </tbody>
    </table>
</div>
<div class="col-sm-4">
    <a href="javascript:void(0)" data-bind="click: function(data, event){ setTaskList('all') }">All </a> |
    <a href="javascript:void(0)" data-bind="click: function(data, event){ setTaskList('active') }"> Active</a> |
    <a href="javascript:void(0)" data-bind="click: function(data, event){ setTaskList('completed') }"> Completed</a>
</div>
<div class="col-sm-2 col-sm-offset-6">
    <a href="javascript:void(0)" data-bind="click: handleCreateOrUpdate">添加任務</a>
</div>
</div>

<div id="create" style="visibility: hidden">
    <h2>添加任務</h2>
    <br/>
    <div class="form-horizontal">
        <div class="form-group">
            <label for="taskName" class="col-sm-2 control-label">名稱 *</label>
            <div class="col-sm-10">
                <input type="text" data-bind="value: name" class="form-control" id="taskName" name="taskName" placeholder="名稱">
            </div>
        </div>
        <div class="form-group">
            <label for="taskDesc" class="col-sm-2 control-label">描述</label>
            <div class="col-sm-10">
                <textarea class="form-control" data-bind="value: description" rows="3" id="taskDesc" name="taskDesc" placeholder="描述"></textarea>
            </div>
        </div>
        <div class="form-group">
            <label for="taskOwner" class="col-sm-2 control-label">負責人 *</label>
            <div class="col-sm-10">
                <input class="form-control" id="taskOwner" name="taskOwner" data-bind="value: owner" placeholder="負責人">
            </div>
        </div>
        <div class="form-group">
            <label for="taskFinish" class="col-sm-2 control-label">預計完成時間 *</label>
            <div class="col-sm-10">
                <input class="form-control datepicker" id="taskFinish" data-bind="value: finishTime" name="taskFinish">
            </div>
        </div>
        <div class="form-group">
            <label for="taskOwner" class="col-sm-2 control-label">狀態 *</label>
            <div class="col-sm-10">
                <select id="taskState" class="form-control" data-bind="value: state">
                    <option>Active</option>
                    <option>Completed</option>
                </select>
                
            </div>
        </div>
        <div class="form-group">
            <div class="col-sm-offset-2 col-sm-10">
                <button class="btn btn-primary" data-bind="click:handleSaveClick">Save</button>
                <button data-bind="click: handleBackClick" class="btn btn-primary">Back</button>
            </div>
        </div>
    </div>
</div>
View Code

  8. 創建對應的前端腳本邏輯。用JS代碼來請求數據,並創建對應ViewModel對象來進行前端綁定。具體JS實現代碼如下:

var taskListViewModel = {
    tasks: ko.observableArray(),
    canCreate:ko.observable(true)
};

var taskModel = function () {
    this.id = 0;
    this.name = ko.observable();
    this.description = ko.observable();
    this.finishTime = ko.observable();
    this.owner = ko.observable();
    this.state = ko.observable();
    this.fromJS = function(data) {
        this.id = data.id;
        this.name(data.name);
        this.description(data.description);
        this.finishTime(data.finishTime);
        this.owner(data.owner);
        this.state(data.state);
    };
};

function getAllTasks() {
    sendAjaxRequest("GET", function (data) {
        taskListViewModel.tasks.removeAll();
        for (var i = 0; i < data.length; i++) {
            taskListViewModel.tasks.push(data[i]);
        }
    }, 'GetByState', { 'state': 'all' });
}

function setTaskList(state) {
    sendAjaxRequest("GET", function(data) {
        taskListViewModel.tasks.removeAll();
        for (var i = 0; i < data.length; i++) {
            taskListViewModel.tasks.push(data[i]);
        }},'GetByState',{ 'state': state });
}

function remove(item) {
    sendAjaxRequest("DELETE", function () {
        getAllTasks();
    }, item.id);
}

var task = new taskModel();

function handleCreateOrUpdate(item) {
    task.fromJS(item);
    initDatePicker();
    taskListViewModel.canCreate(false);
    $('#create').css('visibility', 'visible');
}

function handleBackClick() {
    taskListViewModel.canCreate(true);
    $('#create').css('visibility', 'hidden');
}

function handleSaveClick(item) {
    if (item.id == undefined) {
        sendAjaxRequest("POST", function (newItem) { //newitem是返回的對象。
            taskListViewModel.tasks.push(newItem);
        }, null, {
            name: item.name,
            description: item.description,
            finishTime: item.finishTime,
            owner: item.owner,
            state: item.state
        });
    } else {
        sendAjaxRequest("PUT", function () {
            getAllTasks();
        }, null, {
            id:item.id,
            name: item.name,
            description: item.description,
            finishTime: item.finishTime,
            owner: item.owner,
            state: item.state
        });
    }
    
    taskListViewModel.canCreate(true);
    $('#create').css('visibility', 'hidden');
}
function sendAjaxRequest(httpMethod, callback, url, reqData) {
    $.ajax("/api/tasks" + (url ? "/" + url : ""), {
        type: httpMethod,
        success: callback,
        data: reqData
    });
}

var initDatePicker = function() {
    $('#create .datepicker').datepicker({
        autoclose: true
    });
};

$('.nav').on('click', 'li', function() {
    $('.nav li.active').removeClass('active');
    $(this).addClass('active');
});

$(document).ready(function () {
    getAllTasks();
    // 使用KnockoutJs進行綁定
    ko.applyBindings(taskListViewModel, $('#list').get(0));
    ko.applyBindings(task, $('#create').get(0));
});
View Code

  到此,我們的單頁面程序就開發完畢了,接下來我們來運行看看其效果。

   從上面運行結果演示圖可以看出,一旦頁面加載完之后,所有的操作都好像在一個頁面操作,完全感覺瀏覽器頁面轉圈的情況。對比於之前使用Asp.net MVC +Razor開發的頁面,你是否感覺了SPA的流暢呢?之前使用Asp.net MVC +Razor開發的頁面,你只要請求一個頁面,你就可以感受整個頁面刷新的情況,這樣用戶體驗非常不好。

四、與Razor開發模式進行對比

  相信大家從效果上已經看出SPA優勢了,接下來我覺得還是有必要與傳統實現Web頁面方式進行一個對比。與Razor開發方式主要有以下2點不同:

  1. 頁面被渲染的時候,數據在瀏覽器端得到處理。而不是在服務器上。將渲染壓力分配到各個用戶的瀏覽器端,從而減少網站服務器的壓力。換做是Razor語法,前端頁面綁定語句應該就是如下:
@Model IEnumerable<KnockoutJSSPA.Models.Task>  
@foreach (var item in Model)
{
    <tr>
        <td>@item.Name</td>
        <td>@item.Description</td>
    </tr>
}

 

  這些都是在服務器端由Razor引擎渲染的。這也是使用Razor開發的頁面會看到頁面轉圈的情況的原因。因為你每切換一個頁面的時候,都需要請求服務端進行渲染,服務器渲染完成之后再將html返回給客戶端進行顯示。

  2. 綁定的數據是動態的。意味着數據模型的改變會馬上反應到頁面上。這效果歸功於KnockoutJs實現的雙向綁定機制。

  采用這種方式,對於程序開發也簡單了,Web API只負責提供數據,而前端頁面也減少了很多DOM操作。由於DOM操作比較繁瑣和容易出錯。這樣也意味着減少了程序隱性的bug。並且,一個后端服務,可以供手機、Web瀏覽器和平台多個平台使用,避免重復開發。

五、總結

  到此,本文的介紹就介紹了。本篇主要介紹了使用KnockoutJs來完成一個SPA程序。其實在實際工作中,打造單頁面程序的模式更多的采用AngularJS。然后使用KnockoutJs也有很多,但是KnockoutJs只是一個MVVM框架,其路由機制需要借助其他一些類庫,如我們這里使用Asp.net MVC中的路由機制,你還可以使用director.js前端路由框架。相對於KnockoutJs而言,AngularJs是一個MVVM+MVC框架。所以在下一個專題將介紹使用如何使用AngularJs打造一個單頁面程序(SPA)。

  本文所有源碼下載:SPAWithKnockoutJs

  另外,如果覺得本文對你有幫助,請幫忙點下推薦或者掃描二維碼對我進行打賞,你們的支持也是我繼續為大家分享好文章的動力,希望大家可以對我支持。謝謝

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM