2016 年 2 月
第 31 卷,第 2 期
此文章由机器翻译。
ASP.NET - 借助 ASP.NET 和 React 实现渐进增强
可靠地提供 Web 内容是游戏的机会。随机元素都是用户的网络速度和浏览器的功能。渐进式增强功能或 PE 中时,是涵盖此不可预测性的开发技术。PE 的基础是服务器端呈现的 html。它是唯一的方法,以最大限度地在游戏中的内容传输成功的可能性。对于使用现代的浏览器的用户,层上 JavaScript 来增强的体验。
类似 AngularJS 和 Knockout 数据绑定库随着单页面应用程序 (SPA) 来自到其自身。SPA 是 PE antithesis,因为客户端上呈现 HTML,通过它将忽略 Web 不可预测特性。在慢速网络上的访问者面临着加载时间长,而用户和搜索引擎的功能较少的浏览器可能无法接收任何内容根本。但是,即使这些问题尚未 blunted SPA 的吸引力。
SPA 终止渐进式增强功能。它是太难将服务器呈现应用程序转变为通过 JavaScript 增强 SPA。
幸运的是,事实证明,PE 并不真正死。它只已休眠,和一个名为 React JavaScript 库具有只出现并会将其唤醒它。响应提供这两个领域的最佳产品,因为它可以在服务器上和客户端上运行。您可以从服务器呈现应用程序开始,在瞬间,使其作为客户端呈现一个。
TodoMVC 项目 (todomvc.com) 提供了同一个 Todo SPA 与不同的 JavaScript 数据绑定库生成以帮助您决定可供选择。它是一个很好的项目,但实现从客户端-呈现只会受到影响。在本文中,我将此权限通过为逐渐增强 SPA 使用 React 和 ASP.NET 构建的精简版本。我将注意力集中在只读的功能,因此您将能够查看您的 todos 列表并进行筛选以显示活动或已完成的。
在服务器上的呈现
与 PE 的旧方法,我将构建使用 Razor 来呈现 todo 列表在服务器上的 ASP.NET MVC 应用程序。如果我决定对它进行增强转换 SPA,我将重新开始,我可能不得不重新实现在 JavaScript 中的呈现逻辑。使用我 PE 到新方法时,我将构建使用而不 Razor 的响应来呈现 todo 列表在服务器上的 ASP.NET MVC 应用程序。这样一来,它能兼用作客户端呈现代码。
我将首先创建一个名 TodoMVC 的新 ASP.NET MVC 项目。除了 View 层之外的代码是 unremarkable,因此 Models 文件夹包含返回一个 IEnumerable todos,TodoRepository 并 HomeController 中调入存储库中的索引方法。从那一刻起,事情开始看起来有点不同。而不是将 todo 列表传递给 Razor 视图中,我会将其传递到生成服务器上的 HTML 响应。
若要在服务器上运行 JavaScript 需要 Node.js,其中您可以从下载 nodejs.org。Node.js 附带了名为 npm 自己程序包管理器。我将安装使用 npm,就像我使用 NuGet 来安装的.NET 程序包的响应。我打开命令提示符,cd 插入我 TodoMVC 项目文件夹并运行"npm 安装做出响应"命令。
接下来,我将创建名为 app.jsx (稍后,我将介绍文件扩展名为.jsx) 脚本文件夹中的文件。此文件将保存发生在典型的 ASP.NET MVC 项目的 Razor 视图的响应呈现逻辑。Node.js 使用模块加载系统,因此,加载 React 模块中,我将添加需要的开始位置的 app.jsx 的语句:
var React = require('react');
做出反应的用户界面由多个组件组成。每个组件有一个输入的数据变为 HTML 的呈现器函数。输入的数据作为属性传入。内部 app.jsx,我将创建采用 todos 并将其输出作为未经排序的列表中,每个 todo 表示为一个列表项的标题的列表组件:
var List = React.createClass({
render: function () {
var todos = this.props.todos.map(function (todo) {
return <li key={todo.Id}>{todo.Title}</li>;
});
return <ul>{todos}</ul>;
}
});
文件的扩展名为.jsx 因为响应代码是 JavaScript 和 HTML 类似的语法调用 JSX 的组合。我想要在服务器上,运行此代码,但是 Node.js 不理解 JSX,因此我必须先将文件转换为 JavaScript。将 JSX 转换为 JavaScript 称为 transpiling,且示例 transpiler Babel。我无法将我 app.jsx 内容粘贴到联机 Babel transpiler (babeljs.io/repl),并创建 transpiled 输出从 app.js 文件。但是,会自动完成此步骤,因为 app.jsx 可能相当经常更改的更有意义。
我将使用 Gulp 来自动执行 app.jsx 转换 app.js。Gulp 是附带了多种插件来帮助您转换的源文件的 JavaScript 任务运行程序。更高版本起,我将编写 Gulp 任务向上浏览器的 JavaScript 该捆绑包。现在,我需要通过通过 Babel transpiler app.jsx,因此它可在 Node.js 在服务器上的任务。我将安装 Gulp 和插件 Babel 从 npm 通过运行:
npm install gulp gulp-babel babel-preset-react
如您所见,通过用空格隔开包名称我可以使用一条命令来安装多个包。我将创建 gulpfile.js TodoMVC 项目文件夹内的,并向其中添加 transpile 任务:
var babel = require('gulp-babel');
gulp.task('transpile', function(){
return gulp.src('Scripts/app.jsx')
.pipe(babel({ presets: ['react'] }))
.pipe(gulp.dest('Scripts/'))
});
该任务由三个步骤组成。首先,Gulp 接收 app.jsx 源文件。然后,将文件传送通过 Babel transpiler。最后,输出 app.js 文件将保存到脚本文件夹。若要使该任务可运行,我将使用记事本指着它的脚本项 TodoMVC 项目文件夹中创建 package.json 文件:
{
"scripts": {
"transpile": "gulp transpile"
}
}
我将从命令行运行 transpile 任务使用"npm 运行 transpile。" 这将生成可以在 Node.js 内运行,因为 JSX 已替换为 JavaScript 的 app.js 文件。
因为我使用了响应作为视图层,我想要将 todos 从控制器传递到列表组件,已返回的 HTML。在 Node.js,app.js 内部的代码是私有的并且仅可公共显式将其导出。我将执行 getList 函数从导出 app.jsx 列表组件才能创建外部,记住,以便更新 app.js 运行 transpile 任务:
function getList(todos) {
return <List todos={todos} />;
}
exports.getList = getList;
HomeController C# 中,执行 getList 函数是在 JavaScript 中。若要跨此边界的调用,我将使用 Edge.js (tjanczuk.github.io/edge),该书现已从 NuGet 通过运行安装软件包 Edge.js。Edge.js 期待您将其传递包含返回具有两个参数的函数的 Node.js 代码的 C# 字符串。第一个参数包含传递从 C# 的数据,第二个参数是用于将 JavaScript 数据返回给 C# 的回调。运行 npm 安装做出的 dom 时进行"",以便将在响应的服务器呈现功能之后, 我将使用 Edge.js 来创建一个函数,从传入的 todos 数组中返回列表组件的 HTML:
private static Func<object, Task<object>> render = Edge.Func(@"
var app = require('../../Scripts/app.js');
var ReactDOMServer = require('react-dom/server');
return function (todos, callback) {
var list = app.getList(todos);
callback(null, ReactDOMServer.renderToString(list));
}
");
从 Node.js 代码 Edge.js 创建一个 C# Func,我将其分配给名为"呈现"HomeController 中的变量。Todos 列表调用呈现器将返回 HTML。使用 async/await 模式,因为 Edge.js 到调用是异步的我将添加到索引方法中,此调用:
public async Task<ActionResult> Index()
{
var todos = new TodoRepository().Todos.ToList();
ViewBag.List = (string) await render(todos);
return View();
}
我添加了 HTML 返回到动态 ViewBag,以便我可以从 Razor 视图中访问它。即使 React 正在执行的所有工作,我仍需要 Razor 将 HTML 发送到浏览器并完成服务器呈现下面这行:
<div id="content">@Html.Raw(ViewBag.List)</div>
渐进式增强这种新方法可能看起来与旧方法相比的更多工作。但别忘了,这种新方法,与服务器呈现代码会变得客户端呈现代码。不会有重复的工作量将服务器呈现应用程序转变为 SPA 时所需的旧方法。
在服务器上筛选
Todos 必须是可筛选,以便可以显示是活动状态还是已完成的。筛选意味着超链接和超链接表示的路由。我只是已替换为响应,可在客户端和服务器运行的 JavaScript 呈现器的剃刀原则。接下来,我将同样的方法处理应用于路由。而不是使用 ASP.NET MVC 附带的路由解决方案,我要将其替换为导航路由器 (grahammendick.github.io/navigation),可在客户端和服务器运行的 JavaScript 路由器。
我需要运行"npm 安装导航"以便在路由器中。可以将导航路由器视为状态机,其中每种状态表示应用程序中的不同视图。在 app.jsx,我要将路由器配置与表示 todo"列表"视图状态。我将分配该状态与可选"filter"参数的路由,以便筛选 Url 查找 like"/ 活动"和"/ 已完成":
var Navigation = require('navigation');
var config = [
{ key: 'todoMVC', initial: 'list', states: [
{ key: 'list', route: '{filter?}' }]
}
];
Navigation.StateInfoConfig.build(config);
使用旧方法到 PE 时,会使控制器中的筛选逻辑。使用新方法时,筛选逻辑位于响应代码以便可以重用客户端上时我将其转换 SPA。列表组件将执行筛选器中,并检查它针对 todo 已完成状态,以确定要显示的列表项:
var filter = this.props.filter;
var todoFilter = function(todo){
return !filter || (filter === 'active' && !todo.Completed)
|| (filter === 'completed' && todo.Completed);
}
var todos = this.props.todos.filter(todoFilter).map(function(todo) {
return <li key={todo.Id}>{todo.Title}</li>;
});
我将更改从列表组件返回的 HTML,以包括筛选的 todo 列表下方的筛选器超链接:
<div>
<ul>{todos}</ul>
<ul>
<li><a href="/">All</a></li>
<li><a href="/active">Active</a></li>
<li><a href="/completed">Completed</a></li>
</ul>
</div>
导出"getList"函数需要一个附加参数,以便它可以将新的筛选器属性传递给列表组件。这是最后更改 app.jsx 若要支持筛选,因此是重新运行 Gulp transpile 任务来生成全新 app.js 的好时机。
function getList(todos, filter) {
return <List todos={todos} filter={filter} />;
}
必须从 URL 中提取所选筛选器。您可能想要注册的 ASP.NET MVC 路由,以便筛选器传递到控制器。但这会复制在导航路由器中已配置的路由。相反,我将使用导航路由器来提取该筛选器参数。首先,我将从 C# RouteConfig 类中删除所有提及路由参数。
routes.MapRoute(
name: "Default",
url: "{*url}",
defaults: new { controller = "Home", action = "Index" }
);
导航路由器有 navigateLink 函数以进行分析的 Url。将它提交 URL,它将提取的数据存储在 StateContext 对象。然后可以访问此数据使用路由参数的名称作为键:
Navigation.StateController.navigateLink('/completed');
var filter = Navigation.StateContext.data.filter;
我将向呈现 Edge.js 函数以便可从当前的 URL 检索筛选器,并传递到 getList 函数中插入此路由参数提取代码。但在服务器上的 JavaScript 无法访问当前请求的 URL,因此它必须从 C# 中,以及 todos,通过该函数的第一个参数中传递:
return function (data, callback) {
Navigation.StateController.navigateLink(data.Url);
var filter = Navigation.StateContext.data.filter;
var list = app.getList(data.Todos, filter);
callback(null, ReactDOMServer.renderToString(list));
}
对 HomeController 的 Index 方法的相应更改是将对象传递到保留的 URL 从服务器端请求和 todo 列表中的呈现器调用。
var data = new {
Url = Request.Url.PathAndQuery,
Todos = todos
};
ViewBag.List = (string) await render(data);
使用筛选到位,生成的服务器端阶段已完成。从服务器呈现保证开始,todo 列表是可查看的所有浏览器和搜索引擎。该计划是通过筛选 todo 列表客户端上的增强的现代浏览器的体验。导航路由器将管理浏览器历史记录,并确保客户端筛选 todo 列表将保持可存为书签。
客户端上呈现
如果我在此之前构建带有 Razor UI,我将是不接近现在 SPA 完成比开始时的行。无需复制在 JavaScript 中的呈现逻辑是老式 PE 掉落失去兴趣的原因。但是,与响应,其实恰恰相反因为我可以重复使用我的所有 app.js 代码在客户端。就像我用于响应呈现到 HTML 列表组件,在服务器上,我将使用它来对客户端上 DOM 呈现该相同的组件。
若要呈现在客户端上的列表组件我需要 todos 访问。我将向他们发送中的 JavaScript 变量作为服务器呈现器的一部分来进行 todos 可用。通过向 HomeController 中 ViewBag 添加 todo 列表,我可以将序列化到 JavaScript 数组内的 Razor 视图:
<script>
var todos =
@Html.Raw(new JavaScriptSerializer().Serialize(ViewBag.Todos));
</script>
我将创建脚本文件夹来存放客户端呈现逻辑的 client.js 文件。此代码将查找相同作为 Node.js 代码传递到 Edge.js 来处理服务器呈现,但调整以适应在环境中的差异。因此,URL 来源于浏览器的位置对象,而不是服务器端请求和响应呈现列表组件到内容的 div 中,而不是 HTML 字符串:
var app = require('./app.js');
var ReactDOM = require('react-dom');
var Navigation = require('navigation');
Navigation.StateController.navigateLink(location.pathname);
var filter = Navigation.StateContext.data.filter;
var list = app.getList(todos, filter);
ReactDOM.render(list, document.getElementById('content'));
我将向 app.jsx,告诉我使用 HTML5 历史记录,而不是哈希历史记录默认导航路由器添加一条线。如果我不这样做,navigateLink 函数会认为必须更改 URL,并更新浏览器哈希算法以匹配:
Navigation.settings.historyManager =
new Navigation.HTML5HistoryManager();
如果我可以添加直接对 Razor 视图中,将所做的更改所需的客户端呈现的末尾的 client.js 脚本引用。遗憾的是,它不是这么简单,因为需要 client.js 内的语句是 Node.js 模块加载系统的一部分和浏览器不能被识别。我将使用 Gulp 插件调用 browserify 到单个的 JavaScript 文件,那么我可以将其添加到 Razor 视图该捆绑 client.js 及其所有所需的模块创建的任务。我需要运行"npm 安装 browserify 乙烯源流"到导入该插件:
var browserify = require('browserify');
var source = require('vinyl-source-stream');
gulp.task('bundle', ['transpile'], function(){
return browserify('Scripts/client.js')
.bundle()
.pipe(source('bundle.js'))
.pipe(gulp.dest('Scripts/'))
});
我不想捆绑包任务运行,除非它包含对 app.jsx 的最新更改。若要确保 transpile 任务始终运行第一,我将其变为捆绑包任务的依赖项。您可以看到 Gulp 任务的第二个参数列出了其依赖关系。到 package.json 捆绑包任务的脚本部分,我将添加一个条目。运行命令"npm 运行捆绑"将创建 bundle.js 和我将添加到 Razor 视图的底部指着它的脚本引用:
<script src="~/Scripts/bundle.js"></script>
通过服务器呈现 HTML,我构建的应用程序启动速度快于在 todomvc.com 因为它们不能显示任何内容,直到其 JavaScript 加载并执行。同样,JavaScript 加载后在我的应用程序中,客户端呈现运行。与此相反,这不会在所有更新 DOM,但允许将附加到服务器呈现内容,以便可以在客户端上处理后续 todo 列表筛选的响应。
客户端上筛选
如果您正在做 PE 传统的方式,可能会实现客户端上筛选通过切换类名称,可以控制所有 todo 项的可见性。但如果没有使用 JavaScript 的路由器来帮助解决问题,就极易变得中断浏览器历史记录。如果你忘记将 URL 更新,则筛选出的列表将可存为书签。通过执行 PE 操作的最新方法,我已经导航路由器启动并运行在客户端要使浏览器历史记录保持原样。
若要更新的 URL,单击筛选器超链接时,我需要截取单击事件并将超链接的 href 传递到路由器的 navigateLink 函数。没有将处理这对我来说,在导航路由器插件的响应,提供我规定的方式生成超链接。例如,而不是编写 < href ="/ 活动"> 活动 </a >,我必须使用该插件提供了 RefreshLink 做出反应的组件:
var RefreshLink = require('navigation-react').RefreshLink;
<RefreshLink toData={{filter: 'active'}}>Active</RefreshLink>
在运行"npm 安装导航-响应",以使插件中,我将通过三个筛选器超链接替换为对应的 RefreshLink 更新 app.jsx 中的列表组件。
若要使用户界面和 URL 保持同步,我必须筛选 todo 列表,当 URL 发生更改时,不仅能在筛选器单击超链接还浏览器的后退按钮按下时。而不是添加单独的事件侦听器,我可以添加到导航路由器将调用任何时间导航单侦听器发生。此导航侦听器必须连接到我创建了作为路由器配置的一部分"list"状态。首先,我将从导航路由器使用的键从配置中访问此状态:
var todoMVC = Navigation.StateInfoConfig.dialogs.todoMVC;
var listState = todoMVC.states.list;
导航侦听器是分配给该状态的"导航"属性的函数。当 URL 发生更改时,导航路由器将调用此函数,并传入从 URL 中提取的数据。我将使用重新呈现列表组件为使用新的筛选器的"content"div 的导航侦听器替换 client.js 中的代码。响应将会负责其余部分,更新 DOM 以显示刚筛选的 todos 处理:
listState.navigated = function(data){
var list = app.getList(todos, data.filter);
ReactDOM.render(list, document.getElementById('content'));
}
在实现的筛选,我会意外地从触发初始客户端呈现的 client.js 删除代码。我将通过添加对"Navigation.start"的调用 client.js 底部恢复此功能。这有效地将当前的浏览器 URL 传递到路由器的 navigateLink 函数,它触发导航侦听器并执行客户端呈现中。我将重新运行包任务以使 app.js 和 bundle.js 最新的更改。
PE 到新的方法是当今 alchemy。它会转变为 SPA 金牌服务器呈现应用程序的基本金属。但它采用一种特殊的处理,该转换的基本金属一个 JavaScript 库中生成运行得同样好的服务器上并在浏览器中: React 和导航路由器 Razor 和 ASP.NET MVC 路由位置。这是 Web 新化学。
剪切 Mustard
PE 的目标是适用于所有浏览器,并且可以提供改进的体验的现代浏览器的应用程序。但是,在构建此改进的体验,我已停止使用旧版浏览器中的 todo 列表。SPA 转换依赖于 HTML5 History API,例如,不支持 Internet Explorer 9。
PE 不是指产品/服务在所有浏览器相同的体验。Todo 列表并不一定要在 Internet Explorer 9 SPA。在浏览器不支持 HTML5 历史记录,我可以回退到服务器呈现应用程序。我将更改 Razor 视图以动态方式加载 bundle.js,因此它将只发送到浏览器支持 HTML5 历史记录:
if (window.history && window.history.pushState) {
var script = document.createElement('script');
script.src = "/Scripts/bundle.js";
document.body.appendChild(script);
}
此检查称为"剪切 mustard",这是因为满足的要求这些浏览器被视为值得接收 JavaScript。最终结果是光学视觉效果的 Web 等效其中相同的图片也看起来像兔子或鸭子。看一看 todo 列表通过现代浏览器,并且 SPA 中,但是 squint 它使用的旧浏览器和它是一个传统的客户端 / 服务器应用程序。
Graham Mendick认为在站点中所有可以访问和渐进式增强功能,同构 JavaScript 开创了新的可能性非常高兴。他是导航 JavaScript 路由器,他希望将帮助人们无处藏身同构的作者。在 Twitter 上获取与他联系: @grahammendick。
感谢以下的微软技术专家对本文的审阅: Steve Sanderson
Steve Sanderson 是 Microsoft 的 ASP.NET 团队的 Web 开发人员。他当前的重点是让 ASP.NET 构建丰富的 JavaScript 应用程序的开发人员很好。