欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

WebApi+Bootstrap+KnockoutJs打造單頁面程序

 更新時(shí)間:2016年05月16日 15:38:00   投稿:lijiao  
這篇文章主要介紹了WebApi+Bootstrap+KnockoutJs打造單頁面程序的相關(guān)資料,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下

一、前言

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

二、SPA(單頁面)好處
在介紹具體的實(shí)現(xiàn)之前,我覺得有必要詳細(xì)介紹了SPA。SPA,即Single Page Web Application的縮寫,是加載單個(gè)HTML 頁面并在用戶與應(yīng)用程序交互時(shí)動態(tài)更新該頁面的Web應(yīng)用程序。瀏覽器一開始會加載必需的HTML、CSS和JavaScript,所有的操作都在這張頁面上完成,都由JavaScript來控制。

單頁面程序的好處在于:

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

當(dāng)然單頁面程序除了上面列出的優(yōu)點(diǎn)外,也有其不足:

不利于SEO。這點(diǎn)如果是做管理系統(tǒng)的話是沒影響的
初次加載時(shí)間相對增加。因?yàn)樗械腏S、CSS資源會在第一次加載完成,從而使得后面的頁面流暢。對于這點(diǎn)可以使用Asp.net MVC中Bundle來進(jìn)行文件綁定。關(guān)于Bundle的詳細(xì)使用參考文章:http://www.dbjr.com.cn/article/84329.htm、http://www.dbjr.com.cn/article/84329.htmhttp://www.dbjr.com.cn/article/82174.htm。
導(dǎo)航不可用。如果一定要導(dǎo)航需自行實(shí)現(xiàn)前進(jìn)、后退。對于這點(diǎn),可以自行實(shí)現(xiàn)前進(jìn)、后退功能來彌補(bǔ)。其實(shí)現(xiàn)在手機(jī)端網(wǎng)頁就是這么干的,現(xiàn)在還要上面導(dǎo)航的。對于一些企業(yè)后臺管理系統(tǒng),也可以這么做。
對開發(fā)人員技能水平、開發(fā)成本高。對于這點(diǎn),也不是事,程序員嘛就需要不斷學(xué)習(xí)來充電,好在一些前端框架都非常好上手。
三、使用Asp.net MVC+WebAPI+Bootstrap+KnockoutJS實(shí)現(xiàn)SPA

前面詳細(xì)介紹了SPA的優(yōu)缺點(diǎn),接下來,就讓我們使用Asp.net MVC+WebAPI+BS+KO來實(shí)現(xiàn)一個(gè)單頁面程序,從而體驗(yàn)下SPA流暢和對原始Asp.net MVC +Razor做出來的頁面進(jìn)行效果對比。

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

2. 創(chuàng)建對應(yīng)的倉儲和模型。這里演示的是一個(gè)簡單任務(wù)管理系統(tǒng)。具體的模型和倉儲代碼如下:

任務(wù)實(shí)體類實(shí)現(xiàn):

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

 /// <summary>
 /// 任務(wù)實(shí)體
 /// </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;
 }
 }

任務(wù)倉儲類實(shí)現(xiàn):

/// <summary>
 /// 這里倉儲直接使用示例數(shù)據(jù)作為演示,真實(shí)項(xiàng)目中需要從數(shù)據(jù)庫中動態(tài)加載
 /// </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 = "創(chuàng)建一個(gè)SPA程序",
 Description = "SPA(single page web application),SPA的優(yōu)勢就是少量帶寬,平滑體驗(yàn)",
 Owner = "Learning hard",
 FinishTime = DateTime.Parse(DateTime.Now.AddDays(1).ToString(CultureInfo.InvariantCulture))
 },
 new Task
 {
 Id =2,
 Name = "學(xué)習(xí)KnockoutJs",
 Description = "KnockoutJs是一個(gè)MVVM類庫,支持雙向綁定",
 Owner = "Tommy Li",
 FinishTime = DateTime.Parse(DateTime.Now.AddDays(2).ToString(CultureInfo.InvariantCulture))
 },
 new Task
 {
 Id =3,
 Name = "學(xué)習(xí)AngularJS",
 Description = "AngularJs是MVVM框架,集MVVM和MVC與一體。",
 Owner = "李志",
 FinishTime = DateTime.Parse(DateTime.Now.AddDays(3).ToString(CultureInfo.InvariantCulture))
 },
 new Task
 {
 Id =4,
 Name = "學(xué)習(xí)ASP.NET MVC網(wǎng)站",
 Description = "Glimpse是一款.NET下的性能測試工具,支持asp.net 、asp.net mvc, EF等等,優(yōu)勢在于,不需要修改原項(xiàng)目任何代碼,且能輸出代碼執(zhí)行各個(gè)環(huán)節(jié)的執(zhí)行時(shí)間",
 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. 實(shí)現(xiàn)后端數(shù)據(jù)服務(wù)。這里后端服務(wù)使用Asp.net WebAPI實(shí)現(xiàn)的。具體的實(shí)現(xiàn)代碼如下:

 /// <summary>
 /// Task WebAPI,提供數(shù)據(jù)服務(wù)
 /// </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);
 }
 }

5. 使用Asp.net MVC Bundle對資源進(jìn)行打包。對應(yīng)的BundleConfig實(shí)現(xiàn)代碼如下:

/// <summary>
 /// 只需要補(bǔ)充一些缺少的CSS和JS文件。因?yàn)閯?chuàng)建模板的時(shí)候已經(jīng)添加了一些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"));
 }
 }

6. 因?yàn)槲覀冃枰陧撁嫔鲜沟妹杜e類型顯示為字符串。默認(rèn)序列化時(shí)會將枚舉轉(zhuǎn)換成數(shù)值類型。所以要對WebApiConfig類做如下改動:

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

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

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

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

注:如果上面沒有使用駝峰小寫風(fēng)格序列化的話,在頁面綁定數(shù)據(jù)的時(shí)候也要進(jìn)行調(diào)整。如綁定的Name屬性的時(shí)候直接使用Name大寫,如果使用name方式會提示這個(gè)屬性沒有定義錯(cuò)誤。由于JS是使用駝峰小寫風(fēng)格對變量命名的。所以建議大家加上使用駝峰小寫風(fēng)格進(jìn)行序列化,此時(shí)綁定的時(shí)候只能使用"name"這樣的形式進(jìn)行綁定。這樣也更符合JS代碼的規(guī)范。 

7. 修改對應(yīng)的Layout文件和Index文件內(nèi)容。

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">簡單任務(wù)管理系統(tǒng)</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>
  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>負(fù)責(zé)人</th>
 <th>創(chuàng)建時(shí)間</th>
 <th>完成時(shí)間</th>
 <th>狀態(tài)</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">添加任務(wù)</a>
</div>
</div>

<div id="create" style="visibility: hidden">
 <h2>添加任務(wù)</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">負(fù)責(zé)人 *</label>
 <div class="col-sm-10">
 <input class="form-control" id="taskOwner" name="taskOwner" data-bind="value: owner" placeholder="負(fù)責(zé)人">
 </div>
 </div>
 <div class="form-group">
 <label for="taskFinish" class="col-sm-2 control-label">預(yù)計(jì)完成時(shí)間 *</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">狀態(tài) *</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>

8. 創(chuàng)建對應(yīng)的前端腳本邏輯。用JS代碼來請求數(shù)據(jù),并創(chuàng)建對應(yīng)ViewModel對象來進(jìn)行前端綁定。具體JS實(shí)現(xiàn)代碼如下:

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進(jìn)行綁定
 ko.applyBindings(taskListViewModel, $('#list').get(0));
 ko.applyBindings(task, $('#create').get(0));
});

到此,我們的單頁面程序就開發(fā)完畢了,接下來我們來運(yùn)行看看其效果。

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

四、與Razor開發(fā)模式進(jìn)行對比
  相信大家從效果上已經(jīng)看出SPA優(yōu)勢了,接下來我覺得還是有必要與傳統(tǒng)實(shí)現(xiàn)Web頁面方式進(jìn)行一個(gè)對比。與Razor開發(fā)方式主要有以下2點(diǎn)不同:

1.頁面被渲染的時(shí)候,數(shù)據(jù)在瀏覽器端得到處理。而不是在服務(wù)器上。將渲染壓力分配到各個(gè)用戶的瀏覽器端,從而減少網(wǎng)站服務(wù)器的壓力。換做是Razor語法,前端頁面綁定語句應(yīng)該就是如下:

@Model IEnumerable<KnockoutJSSPA.Models.Task> 
@foreach (var item in Model)
{
 <tr>
 <td>@item.Name</td>
 <td>@item.Description</td>
 </tr>
}

這些都是在服務(wù)器端由Razor引擎渲染的。這也是使用Razor開發(fā)的頁面會看到頁面轉(zhuǎn)圈的情況的原因。因?yàn)槟忝壳袚Q一個(gè)頁面的時(shí)候,都需要請求服務(wù)端進(jìn)行渲染,服務(wù)器渲染完成之后再將html返回給客戶端進(jìn)行顯示。

2. 綁定的數(shù)據(jù)是動態(tài)的。意味著數(shù)據(jù)模型的改變會馬上反應(yīng)到頁面上。這效果歸功于KnockoutJs實(shí)現(xiàn)的雙向綁定機(jī)制。

采用這種方式,對于程序開發(fā)也簡單了,Web API只負(fù)責(zé)提供數(shù)據(jù),而前端頁面也減少了很多DOM操作。由于DOM操作比較繁瑣和容易出錯(cuò)。這樣也意味著減少了程序隱性的bug。并且,一個(gè)后端服務(wù),可以供手機(jī)、Web瀏覽器和平臺多個(gè)平臺使用,避免重復(fù)開發(fā)。

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

本文所有源碼下載:SPAWithKnockoutJs

如果大家還想深入學(xué)習(xí),可以點(diǎn)擊這里進(jìn)行學(xué)習(xí),再為大家附3個(gè)精彩的專題:

Bootstrap學(xué)習(xí)教程

Bootstrap實(shí)戰(zhàn)教程

Bootstrap插件使用教程

以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助。

相關(guān)文章

最新評論