React Shop - 一个小型的电子商务






4.99/5 (40投票s)
使用 Asp.Net MVC, ReactJS, ReactJS.Net 和 React-Bootstrap 构建的电子商务应用程序
遵循以下 3 个步骤
- 下载 ReactShop.zip - 1.7 MB (使用 Visual Studio 2015 或更高版本打开)
- 转到 工具 > Nuget 包管理器 > 管理解决方案的 NuGet 包...
- 点击 "更新" 选项卡。选择所有包以解决所有依赖项。
引言
在本文中,您将能够学习如何将 React 技术应用于新的或现有的 ASP.Net MVC 应用程序。目前市面上关于 React 与 Asp.Net MVC 集成的文章不多,所以我希望这个项目能帮助到正在寻找相关信息的读者。
背景
上个月我失业成为一名高级软件开发人员,试图在市场中找到自己的位置,最近我花了大量时间作为求职过程的一部分来完成特定的软件项目。不幸的是,到目前为止,这些小项目都没有让我被聘用,所以写这篇文章的主要动机是展示我最近在申请这些工作时研究的一些代码。与其白白丢弃代码,我认为最好与社区分享代码,并帮助他人解决我遇到的问题。
软件要求
为了完成本文的开发,我使用了
- SQL Server Express (在此 处下载)
- Visual Studio Community 2015 Update 3 或更高版本 (在此 处下载)
- Asp.Net MVC 5 ASP.NET MVC 5 是一个用于构建可扩展的、基于标准的 Web 应用程序的框架,它使用成熟的设计模式和 ASP.NET 及 .NET Framework 的强大功能。
- Microsoft ASP.NET Web Optimization Framework (通过 nuget) ASP.NET Optimization 提供了一种打包和优化 CSS 和 JavaScript 文件的方式。
- Web Analyzer (https://visualstudiogallery.msdn.microsoft.com/6edc26d4-47d8-4987-82ee-7c820d79be1d) 在 Visual Studio 中直接为 JavaScript, TypeScript, JSX, CSS 等提供静态分析。
- Web Extension Pack (https://visualstudiogallery.msdn.microsoft.com/f3b504c6-0095-42f1-a989-51d5fc2a8459) 设置 Visual Studio 以获得最佳 Web 开发体验的最简单方法。
- ReactJS.NET Core (通过 nuget) .NET 的 React.js 和 Babel 工具。重要提示:此包本身功能不多;您可能还需要一个集成包(如 React.Web.Mvc4)。请参阅项目站点 (http://reactjs.net/) 以获取更多详细信息、用法示例和示例代码。
- ReactJS.NET (MVC 4 和 5) (通过 nuget)
- ReactJS.NET - ASP.NET Web Optimization Framework 的 Babel (通过 nuget) 允许您通过 ASP.NET Web Optimization Framework 中的 Babel 转译 JavaScript。
- showdown (通过 nuget) Showdown 是 Markdown(标记语言)的 JavaScript 移植版。
Code First - Entity Framework
对于这个小型项目,我选择了 Entity Framework 6 提供的 Code First 技术来创建一个 Context
,它将我们的实体(C# 类/属性/类型)映射到数据库对象(表/列/数据类型)。Code First 甚至提供了数据库创建功能,因此我们可以避免在此项目中使用任何 Transact-SQL 脚本。
public class Context : DbContext
{
public DbSet<CartItem> CartItem { get; set; }
public DbSet<Product> Product { get; set; }
}
列表 1。 Context.cs 文件显示了 Entity Framework Code-First 上下文类。
生成我们数据库的数据实体集相当简约,仅包含 CartItem
和 Product
实体。其他所有内容(客户信息、总计、小计、折扣等)要么硬编码在后端,要么从 CartItem
值自动计算。
[Table("Product")]
public class Product
{
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
public string SKU { get; set; }
public string Description { get; set; }
public string SmallImagePath { get; set; }
public string LargeImagePath { get; set; }
public decimal Price { get; set; }
}
[Table("CartItem")]
public class CartItem
{
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
[ForeignKey("Product")]
public int ProductId { get; set; }
[Required]
public virtual Product Product { get; set; }
public int Quantity { get; set; }
}
列表 2。 我们项目的两个实体:Product 和 CartItem。
应用程序启动时,它会检查连接字符串中定义的 SQL Server 数据库是否存在。如果不存在,它将使用上面描述的 Code First 创建一个包含 CartItem
和 Product
表的新数据库,并相应地填充产品。
public class Global : HttpApplication
{
void Application_Start(object sender, EventArgs e)
{
...
var checkoutManager = AutoFacHelper.Resolve();
checkoutManager.InitializeDB();
}
}
列表 3。 Global.asax.cs 中的数据库初始化
public Context InitializeDB()
{
var db = new Context();
if (!db.Database.Exists())
{
db.Database.CreateIfNotExists();
var products = new string[]
{
"10 Million Member CodeProject T-Shirt|3399",
"Women's T-Shirt|3399",
"CodeProject.com Body Suit|1399",
"CodeProject Mug Mugs|1099",
"RootAdmin Mug|1099",
"Drinking Glass|1099",
"Stein|1399",
"Mousepad|1099",
"Square Sticker|299",
};
var index = 1;
foreach (var p in products)
{
var description = p.Split('|')[0];
var price = decimal.Parse(p.Split('|')[1]) / 100M;
var product =
db.Product.Add(new Product
{
SKU = Guid.NewGuid().ToString(),
SmallImagePath = string.Format("Images/Products/small_{0}.jpg", index),
LargeImagePath = string.Format("Images/Products/large_{0}.jpg", index),
Description = description,
Price = price
});
var cartItem =
db.CartItem.Add(new CartItem
{
Product = product,
Quantity = 1
});
index++;
}
db.SaveChanges();
}
return db;
}
列表 4。 CheckoutManager.cs 中的数据库设置
React.js
React 是 Facebook 创建的一个用于构建用户界面的 JavaScript 库。虽然其他 JavaScript 框架/库实现了 MV* 模式(例如 Knockjout JS 中的 MVVM 或 Angular JS 中的 MVC),但 React 是一个只专注于 V 部分的库。与 Backbone 等功能齐全的 JavaScript 框架不同,React JS 的目标是只做一件事,并做好。
如果您问大多数技术达人 2016/2017 年编程领域的热门趋势(例如 ThoughtWorks 的技术雷达),他们很可能会提到 React.js。
引用在前端 JavaScript 框架的浪潮中,React.js 因其围绕响应式数据流的设计而脱颖而出。仅允许单向数据绑定极大地简化了渲染逻辑,并避免了许多在其他框架编写的应用程序中普遍存在的问题。我们在越来越多的项目(大小项目)中看到了 React.js 的优势,同时我们仍然关注 AngularJS 等其他流行框架的状态和未来。这使得 React.js 成为我们 JavaScript 框架的首选。
单元测试
应用程序中的单元测试旨在满足以下业务需求
- 购买金额在 $500.00 到 $599.99 之间应给予 5% 的折扣率
- 购买金额在 $600.00 到 $699.99 之间应给予 10% 的折扣率
- 购买金额超过 $700.00 应给予 15% 的折扣率
请注意,每个限制值都与上面的需求表进行测试
[TestClass]
public class DiscountManagerTest
{
public DiscountManager discountManager;
[TestInitialize]
public void Initialize()
{
discountManager = new DiscountManager();
}
[TestMethod]
public void GetDiscount_0_Should_Return_Rate_0_And_Value_0()
{
var rule = discountManager.GetDiscount(0);
Assert.AreEqual(0, rule.Rate);
Assert.AreEqual(0, rule.CalculatedDiscount);
}
[TestMethod]
public void GetDiscount_499_99_Should_Return_Rate_0_And_Value_0()
{
var rule = discountManager.GetDiscount(499.99M);
Assert.AreEqual(0, rule.Rate);
Assert.AreEqual(0, rule.CalculatedDiscount);
}
[TestMethod]
public void GetDiscount_500M_Should_Return_Rate_5_And_Value_25()
{
var rule = discountManager.GetDiscount(500M);
Assert.AreEqual(.05M, rule.Rate);
Assert.AreEqual(25, rule.CalculatedDiscount);
}
[TestMethod]
public void GetDiscount_599_99M_Should_Return_Rate_5_And_Value_25()
{
var rule = discountManager.GetDiscount(599.99M);
Assert.AreEqual(.05M, rule.Rate);
Assert.AreEqual(30M, rule.CalculatedDiscount);
}
[TestMethod]
public void GetDiscount_600M_Should_Return_Rate_10_And_Value_60()
{
var rule = discountManager.GetDiscount(600M);
Assert.AreEqual(.10M, rule.Rate);
Assert.AreEqual(60M, rule.CalculatedDiscount);
}
[TestMethod]
public void GetDiscount_699_99M_Should_Return_Rate_10_And_Value_70M()
{
var rule = discountManager.GetDiscount(699.99M);
Assert.AreEqual(.10M, rule.Rate);
Assert.AreEqual(70M, rule.CalculatedDiscount);
}
[TestMethod]
public void GetDiscount_700M_Should_Return_Rate_15_And_Value_105()
{
var rule = discountManager.GetDiscount(700M);
Assert.AreEqual(.15M, rule.Rate);
Assert.AreEqual(105M, rule.CalculatedDiscount);
}
[TestMethod]
public void GetDiscount_10000M_Should_Return_Rate_15_And_Value_1500()
{
var rule = discountManager.GetDiscount(10000M);
Assert.AreEqual(.15M, rule.Rate);
Assert.AreEqual(1500M, rule.CalculatedDiscount);
}
列表 5。 DiscountManagerTest.cs 的内容。
图 1。 DiscountManagerTest 类的单元测试运行情况。
产品目录
图 2。 产品目录视图。
产品目录视图具有一个简单的产品轮播控件。如果您使用 Bootstrap 搜索产品轮播,您会找到类似的解决方案,例如 这个。
产品轮播非常棒,因为它具有无限的动画效果,并且能够用一小部分页面空间显示许多产品。
产品轮播是通过 Razor 视图引擎在服务器端渲染的。轮播一次显示四个产品,因此 Index.cshtml 视图中的代码定义了一个 foreach
循环,该循环以每组 4 个产品为单位迭代“页面”。
@using ReactShop.Core;
@model List<ReactShop.Core.DTOs.ProductDTO>
@{
ViewBag.Title = "ReactShop";
}
<div class="container">
<div class="row">
<div class="row">
<div class="col-md-9">
<h3>
Product Catalog
</h3>
</div>
<div class="col-md-3">
<!-- Controls -->
<div class="controls pull-right hidden-xs">
<a class="left fa fa-chevron-left btn btn-success" href="#carousel-example"
data-slide="prev"></a><a class="right fa fa-chevron-right btn btn-success"
href="#carousel-example"
data-slide="next"></a>
</div>
</div>
</div>
<div id="carousel-example" class="carousel slide hidden-xs" data-ride="carousel">
<!-- Wrapper for slides -->
<div class="carousel-inner">
@foreach (var pageIndex in Enumerable.Range(0, (Model.Count() - 1) / 4))
{
<div class="item@(pageIndex == 0 ? " active" : "")">
<div class="row">
@using(Html.BeginForm("AddToCart", "Home"))
{
@Html.AntiForgeryToken()
foreach (var product in Model.Skip(pageIndex * 4).Take(4))
{
<div class="col-sm-3">
<div class="col-item">
<div class="photo">
<img src="~/@product.SmallImagePath" class="img-responsive"
alt="a" width="350" height="260" />
</div>
<div class="info">
<div class="row">
<div class="price col-md-6">
<h5 class="truncate">
@product.Description
</h5>
<h5 class="price-line">
$<span class="price-text-color">@product.Price</span>
</h5>
</div>
</div>
<div class="separator clear-left">
<p class="btn-add">
<button type="submit" class="btn btn-link" name="SKU"
value="@product.SKU">
<i class="fa fa-shopping-cart" aria-hidden="false"></i>
Add to Cart
</button>
</p>
<p class="btn-details">
<button type="button" class="btn btn-link" name="SKU"
value="@product.SKU">
<i class="fa fa-list"></i><a href=""
class="hidden-sm"></a>
Details
</button>
</p>
</div>
<div class="clearfix">
</div>
</div>
</div>
</div>
}
}
</div>
</div>
}
</div>
</div>
</div>
</div>
列表 6。 Index.cshtml 视图的内容。
添加到购物车
请注意 防伪令牌 的使用。这是 Asp.Net MVC 提供的一项安全措施,用于保护您的应用程序免受跨站请求伪造攻击 (CSRF)。防伪令牌会被传递到客户端页面,然后又会传回控制器接收的提交 post 操作,以确保请求是由合法客户端发出的。请记住,永远不要相信任何人,并在您的应用程序中始终使用 AntiForgeryToken
。
[AcceptVerbs(HttpVerbs.Post)]
[ValidateAntiForgeryToken]
public ActionResult AddToCart(string SKU)
{
checkoutManager.SaveCart(new Core.DTOs.CartItemDTO
{
SKU = SKU,
Quantity = 1
});
return RedirectToAction("Cart", "Home");
}
列表 5。 HomeController.cs 中的代码片段,显示 AddToCart
方法上的 ValidateAntiForgeryToken
属性。
然后,CartItemDTO
将被传递给 CheckoutManager
类的 SaveCart
方法,该方法将根据 CartItemDTO
对象中提供的数量执行数据库操作(更新或删除)。
public void SaveCart(CartItemDTO newOrEditItem)
{
try
{
if (newOrEditItem.Quantity < 0)
newOrEditItem.Quantity = 0;
using (var db = new Context())
{
var product = db.Product.Where(p => p.SKU == newOrEditItem.SKU).Single();
var cartItem =
(from ci in db.CartItem
join p in db.Product on ci.ProductId equals p.Id
where p.SKU == newOrEditItem.SKU
select ci)
.SingleOrDefault();
if (cartItem != null)
{
if (newOrEditItem.Quantity == 0)
db.CartItem.Remove(cartItem);
else
{
cartItem.Quantity = newOrEditItem.Quantity;
cartItem.Product = product;
}
}
else
{
db.CartItem.Add(new CartItem
{
Product = product,
Quantity = newOrEditItem.Quantity
});
}
db.SaveChanges();
}
}
catch (DbEntityValidationException dbEx)
{
foreach (var validationErrors in dbEx.EntityValidationErrors)
{
foreach (var validationError in validationErrors.ValidationErrors)
{
Trace.TraceInformation("Property: {0} Error: {1}",
validationError.PropertyName,
validationError.ErrorMessage);
}
}
}
}
列表 8。 显示 CheckoutManager
类 SaveCart
方法的代码片段。
购物车
图 3。 购物车视图。
与产品目录相比,购物车页面的渲染方式截然不同。首先,Razor 引擎不直接用于渲染视图。相反,Razor 调用 React.Web.Mvc.HtmlHelperExtensions 类的 React
方法并将模型传递给它。Razor 随后渲染声明为 React 组件的 CartView
。
var CartItem = React.createClass({
getInitialState: function () {
var item = this.props.model;
return {
SKU: item.SKU,
SmallImagePath: item.SmallImagePath,
LargeImagePath: item.LargeImagePath,
Description: item.Description,
SoldAndDeliveredBy: item.SoldAndDeliveredBy,
Price: item.Price,
Quantity: item.Quantity,
Subtotal: item.Subtotal
};
},
updateState: function (change) {
this.setState(Object.assign({}, this.state, change))
},
handleIncrement: function () {
this.postQuantity(this.state.Quantity + 1);
},
handleDecrement: function () {
this.postQuantity(this.state.Quantity - 1);
},
removeItem: function () {
this.postQuantity(0);
},
postQuantity: function (quantity, callback) {
$('.overlay').show();
$.post('/api/Cart',
{
SKU: this.props.model.SKU,
Quantity: quantity,
Price: this.props.model.Price
})
.done(function (data) {
for (var item of data.CartItems) {
if (item.SKU == this.props.model.SKU) {
this.updateState({ Quantity: item.Quantity, Subtotal: item.Subtotal });
this.props.handleCartChange(data, item);
return;
}
}
}.bind(this))
.always(function () {
$('.overlay').hide();
});;
},
handleQuantityChanged: function (event) {
var newQty = 1;
var val = event.target.value;
if (val && !isNaN(val))
newQty = parseInt(val);
this.postQuantity(newQty);
},
render: function () {
return (
<Row className="vertical-align">
<Column md={2} className="justify-left">
<Row className="fullwidth">
<Column md={3}>
<img src={'../' + this.state.SmallImagePath} width="80" height="80" />
</Column>
</Row>
</Column>
<Column md={4} className="justify-left">
<Row className="fullwidth">
<Column md={9}>
<span>{this.state.Description}</span>
</Column>
</Row>
</Column>
<Column md={2} className="green justify-center">
<Dollars val={this.state.Price } />
</Column>
<Column md={2} className="justify-center">
<div className="text-center">
<ButtonGroup>
<input type="button" className="btn btn-default" value="-" onClick={this.handleDecrement} />
<input type="text" className="btn" value={this.state.Quantity} onChange={this.handleQuantityChanged } />
<input type="button" className="btn btn-default" value="+" onClick={this.handleIncrement} />
</ButtonGroup>
<a onClick={this.removeItem} className="remove pointer">Remove</a>
</div>
</Column>
<Column md={2} className="green justify-right">
<Dollars val={this.state.Subtotal} />
</Column>
</Row>
);
}
})
class CartView extends React.Component {
constructor(props) {
super(props);
this.state = {};
var items = [];
for (var i = 0; i < this.props.model.CartItems.length; i++) {
var item = this.props.model.CartItems[i];
items.push({
SKU: item.SKU,
SmallImagePath: item.SmallImagePath,
LargeImagePath: item.LargeImagePath,
Description: item.Description,
SoldAndDeliveredBy: item.SoldAndDeliveredBy,
Price: item.Price,
Quantity: item.Quantity,
Subtotal: item.Subtotal
});
}
this.state = {
canFinishOrder: true,
items: items,
Subtotal: this.props.model.Subtotal,
DiscountRate: this.props.model.DiscountRate,
DiscountValue: this.props.model.DiscountValue,
Total: this.props.model.Total
};
}
handleCartChange(cart, cartItem) {
var newState = Object.assign({}, this.state, {
Subtotal: cart.Subtotal,
DiscountRate: cart.DiscountRate,
DiscountValue: cart.DiscountValue,
Total: cart.Total
});
if (cartItem.Quantity == 0) {
newState.items.splice(newState.items.findIndex(i =>
i.SKU == cartItem.SKU), 1);
}
this.setState(newState);
}
render() {
const header = (<Row className="vertical-align">
<Column md={6} className="justify-left">item(s)</Column>
<Column md={2} className="justify-center">unit price</Column>
<Column md={2} className="justify-center">quantity</Column>
<Column md={2} className="justify-right">subtotal</Column>
</Row>);
const body = (this.state.items.map(item => {
return <CartItem key={item.SKU} model={item}
handleCartChange={this.handleCartChange.bind(this)} />;
}
));
const footer = (<Row>
<Column md={7}></Column>
<Column md={5} className="my-children-have-dividers">
<Row className="vertical-align">
<Column md={8} className="justify-right">
Subtotal ({this.state.items.length} <Pluralize value={this.state.items.length} singular="item" plural="items" />):
</Column>
<Column md={4} className="green justify-right">
<span>
<Dollars val={this.state.Subtotal} />
</span>
</Column>
</Row>
{ this.state.DiscountRate
?
<Row className="vertical-align">
<Column md={8} className="justify-right">
Discount (<span>{this.state.DiscountRate}</span>%):
</Column>
<Column md={4} className="green justify-right">
<span>
<Dollars val={this.state.DiscountValue} />
</span>
</Column>
</Row>
: null
}
<Row className="vertical-align">
<Column md={12} className="justify-right">
<h3>
Total:
<span className="green">
<Dollars val={this.state.Total} />
</span>
</h3>
</Column>
</Row>
</Column>
</Row>);
return (
<div className="cart">
{
this.state.items.length == 0 ? null :
<div>
{/* TITLE */}
<h3>Your shopping cart ({ this.state.items.length} <Pluralize value={this.state.items.length} singular="item" plural="items" />)</h3>
{/* NAVIGATION BUTTONS */}
<Row>
<Column md={3}>
<a href={this.props.urlNewProduct}>
<button type="button" className="btn btn-success">Add new product</button>
</a>
</Column>
<Column md={3} className="pull-right">
<a href={this.props.urlCheckoutSuccess}>
<button type="button" className="btn btn-success pull-right">Proceed to checkout</button>
</a>
</Column>
</Row>
{/* NAVIGATION BUTTONS */}
<br />
{/* CART PANEL */}
<Panel header={header} footer={footer}>
{body}
</Panel>
{/* CART PANEL */}
{/* NAVIGATION BUTTONS */}
<Row>
<Column md={3}>
<a href={this.props.urlNewProduct}>
<button type="button" className="btn btn-success">Add new product</button>
</a>
</Column>
<Column md={3} className="pull-right">
<a href={this.props.urlCheckoutSuccess}>
<button type="button" className="btn btn-success pull-right">Proceed to checkout</button>
</a>
</Column>
</Row>
{/* NAVIGATION BUTTONS */}
</div>
}
{
this.state.items.length > 0
? null
:
<div>
<h1><br /><br />:(</h1>
<div>
<h1>
Oops! Your shopping cart is empty.
</h1>
<br />
<div className="empty-cart-content-message">
Enter more products and resume shopping.
</div>
<br />
<div>
{
this.state.canFinishOrder
?
<a href={this.props.urlNewProduct}>
<button type="button" className="btn btn-success">Enter new product</button>
</a>
: null
}
</div>
</div>
</div>
}
</div>
);
}
}
列表 9。 Cart.jsx 文件的内容。
JSX 文件
初次接触 JSX 文件时,如果您不熟悉它,会觉得有些奇怪。它看起来像是 JavaScript 和 HTML 的混合体。现在将其与 Angular JS 的工作方式进行比较。Angular 似乎将 JavaScript 带入 HTML,而 React 则似乎将 HTML 带入 JavaScript。
当您看到 JSX 文件时,您可能会认为这些 HTML 片段直接插入到 HTML 页面中。但实际上,情况并非如此。相反,这些 HTML 部分会被“转译”成真正的 JavaScript 代码。事实上,该 HTML 标记只是 JavaScript 的另一种表示形式。最终,JSX 代码以纯 JavaScript 代码的形式部署到客户端,而这些 HTML 标签被渲染为 React.createElement
方法的结构化链。想象一下手动创建所有这些元素所涉及的大量重复工作。这就是为什么最终,React 只是纯粹的 JavaScript。
图 4。 jsx 如何被“转译”成代码。最终,一切都是 JavaScript。
扩展 React.Component
React 允许您通过将视图分解为更小的组件来创建复杂的视图。
其中 CartItem 组件代表购物车中的每一行(显示产品描述、数量和价格),而 Cart 组件包含其余部分。
每个 React 组件都必须实现 render
函数,该函数必须返回一个 React 组件树。render 函数返回的组件树可能是
- 一个非常简单的组件,例如渲染单个 div HTML 标签的代码,
- 一个复杂的组件,例如整个页面,或
- 一个 null 值
在我们的例子中,购物车视图的唯一组件是
- Cart
- CartItem
ReactJS.Net 及其强大之处
我们 .Net 开发者很幸运,Facebook 对我们的平台表现出喜爱。 ReactJS.Net 是一个将 Asp.Net MVC 与 React 集成在后端,支持 服务器端渲染 以及 JavaScript 代码的捆绑和最小化。
ReactJS.Net 提供 即时 JSX 到 JavaScript 编译。这意味着任何扩展名为 .jsx 的文件都将被自动编译成 JavaScript 并在服务器端缓存,无需预编译。看看当您直接浏览 CheckoutSuccess.jsx 文件时会发生什么。
图 5。 感谢 ReactJS.NET,CheckoutSuccess.jsx 自动生成的已编译 JavaScript。
ReactJS.NET 还提供 服务器端组件渲染。在通常情况下,我们会为购物车视图创建 .jsx 组件,然后将其发送到浏览器。浏览器会使用一些预加载的 JSON 对象来表示购物车项目,或者通过 AJAX 调用获取购物车数据,然后调用 ReactDOM.render()
来渲染具有初始状态的购物车。但借助 ReactJS.NET,我们可以预先在服务器端渲染购物车的初始状态(即项目、金额、折扣和总计),因为 ReactJS.NET 理解 .jsx 文件中 React 组件的语法,并且知道如何处理 JavaScript 表达式(即花括号 {} 中的表达式),将 Model
注入并渲染组件的初始状态(在发送到浏览器之前)。这种方法非常方便,在许多情况下可以最大限度地减少使用 Razor 或 aspx 等视图引擎。
@Html.React("CheckoutSuccessView", new
{
title = "React JS.Net + React-Bootstrap",
model = Model
})
列表 10。模型被传递给 CheckoutSuccessView
组件,ReactJS.NET 在服务器端预渲染一切。这真是太棒了!
React-Bootstrap
为了进一步推广组件化和重用概念,幸运的是,我们拥有整个 Bootstrap 框架重构的 React 版本,这要归功于 Jimmy Jia 的杰出工作和他仍在 alpha 发布的 React-Bootstrap 项目,它包含了一套 完整的组件。
所有使用过 Bootstrap 的人都知道,虽然它是一个非常有用的网页设计工具箱,但有时它也变得难以阅读和编写,因为最终它变成了一个无尽的 div
标签树,带有许多难以记忆的 CSS 类。
这就是 React-Bootstrap 发挥作用的地方。只需看看下面从 JSX 中提取的 React-Bootstrap 代码片段……
<Row className="vertical-align">
<Column md={2} className="justify-left">
<Row className="fullwidth">
<Column md={3}>
<img src={'../' + this.state.SmallImagePath} width="80" height="80" />
</Column>
</Row>
</Column>
……与编写相同视图所需的常规 HTML Bootstrap 标记进行比较
<div class="row vertical-align">
<div class="col-md-2 justify-left">
<div class="row fullwidth">
<div class="col-md-3">
<img src="../Images/Products/small_7.jpg" width="80" height="80"></div>
</div>
</div>
Props vs State
关于 React 实现中最常被问到的问题之一是:“我如何知道何时使用 props 或 state?”
每个 React 组件都使用两个特殊的对象:“props”(来自“properties”)和“state”。它们都用作渲染页面上丰富 HTML 的原始数据。
我们可以在 此处 找到关于 props
和 state
之间区别的深入解释。
提问 | props | 状态 |
---|---|---|
能否从父组件获取初始值? | 是 | 是 |
能否由父组件更改? | 是 | 否 |
能否在组件内部设置默认值? | 是 | 是 |
能否在组件内部更改? | 否 | 是 |
能否为子组件设置初始值? | 是 | 是 |
能否在子组件中更改? | 是 | 否 |
来源: https://github.com/uberVU/react-guide/blob/master/props-vs-state.md
来自 React 文档
引用props 是不可变的:它们从父组件传递而来,并由父组件“拥有”。为了实现交互,我们为组件引入了可变状态。this.state 对组件是私有的,可以通过调用 this.setState() 来更改。当状态更新时,组件会重新渲染自身。
自定义组件
在此项目中,我们实现了两个小型组件,它们充当助手,本身功能不多,但可以避免我们的其他主要组件变得混乱且难以阅读。
更新数量
购物车项目的数量可以通过 4 种不同的方式更新
- 通过单击减号按钮
- 通过单击加号按钮
- 直接在文本框中输入数量
- 通过单击删除链接
所有这些操作都会触发 postQuantity
函数,该函数会调用 /api/Cart Web API 方法,该方法会更改数据库中的数量,并返回购物车状态的新快照。在 post ajax 方法的最后,postQuantity
函数通过调用 updateState
函数来更新视图的 state
,将新的 Cart 数据作为新状态传递。
在产品删除的情况下,项目通过将数量更改为零来删除。这将触发与上述相同的流程。唯一的区别是,在更新结束时,购物车项目将从组件状态中删除。
结账详情
图 4。 结账成功视图。
虽然 CheckoutSuccess
视图不包含任何交互(除了“返回产品目录”按钮),但它完全实现为单个 React 组件。原因是我们能够利用我们上面已经解释过的 React-Bootstrap 库的组件所提供的简单语法。所有绑定值都通过 props
传递,并且不需要使用 React 的 state
对象。
@using System.Web.Optimization
@using System.Collections.Generic
@model ReactShop.Core.DTOs.CheckoutSummaryDTO
@{
Layout = "~/Views/Shared/_Layout.cshtml";
}
@{
ViewBag.Title = "ReactShop";
}
@Html.React("CheckoutSuccessView", new
{
title = "React JS.Net + React-Bootstrap",
model = Model
})
@Scripts.Render(" ~/bundles/js")
列表 10。 CheckoutSuccess.cshtml 文件的内容。
class CheckoutSuccessView extends React.Component {
constructor(props) {
super(props);
}
render() {
return (
<div>
<br />
<Row>
<span className="fa fa-check-circle"></span>
</Row>
<h3 className="text-center">
<span>Your order has been received. Thank you for your purchase!</span>
</h3>
<h4 className="text-center">
<span>Your order # is:</span>
<span className="green">{this.props.model.OrderNumber}</span>
</h4>
<br />
<h3>
Order Information
</h3>
<Panel>
<Row>
<Column md={6}>
<h4>
<span>Order No:</span>
<span className="green">{this.props.model.OrderNumber}</span>
</h4>
<p>
You will receive a confirmation e-mail with the details of your order. Please verify your AntiSpam settings of your e-mail provider.
</p>
</Column>
<Column md={6}>
<h4>Payment term</h4>
<div className="boleto">
<p><i className="fa fa-paypal leading-icon" aria-hidden="true"></i> Paypal</p>
<p className="offset30"><Dollars val={this.props.model.Total} /></p>
</div>
</Column>
</Row>
<Row className="gray row-eq-height border-top border-bottom">
<Column md={3}>
<h4><span className="fa fa-user leading-icon"></span>Your info</h4>
<p className="offset30">{this.props.model.CustomerInfo.CustomerName}</p>
<p className="offset30">{this.props.model.CustomerInfo.PhoneNumber}</p>
</Column>
<Column md={3} className="border-right">
<br />
<br />
<p>{this.props.model.CustomerInfo.Email}</p>
</Column>
<Column md={6}>
<h4><span className="fa fa-home leading-icon"></span>Shipping address</h4>
<p className="offset30">{this.props.model.CustomerInfo.DeliveryAddress}</p>
</Column>
</Row>
<Row className="gray">
<Column md={6}>
<h4><span className="fa fa-gift leading-icon"></span>Delivery</h4>
</Column>
<Column md={6}>
<br />
<p className="float-right">
Delivery time is {this.props.model.DeliveryUpTo} days
</p>
</Column>
</Row>
<Row className="gray">
<Column md={6}>
<p className="offset30"><b>Product description</b></p>
</Column>
<Column md={6} className="pull-right">
<p><b className="float-right">Quantity</b></p>
</Column>
</Row>
{ this.props.model.CartItems.map(item =>
<Row className="gray">
<Column md={6}>
<div className="offset30 truncate">
<span>•</span>
<span>{item.Description}</span>
</div>
</Column>
<Column md={6} className="pull-right">
<p className="float-right">{item.Quantity}</p>
</Column>
</Row>
)
}
</Panel>
<Row>
<Column md={9}></Column>
<a href="/">
<Column md={2}>
<Button bsStyle="success">Back to product catalog</Button>
</Column>
</a>
</Row>
</div>
);
}
}
列表 11。 CheckoutSuccess.jsx 文件的内容。
最后考虑
如果您花时间和耐心读到这里,非常感谢您的阅读!如果您对代码或文章有任何抱怨或建议,请告诉我。不要忘记在下面的评论区留下您的意见。
历史
2016/08/31:第一个版本
2016/09/01:解释了 jsx 文件
2016/09/04:解释了服务器端渲染