当创建一个会部署到浏览器环境中的应用程序时,必须解决我称之为 应用程序/视图状态同步 的问题:您需要控制前端视觉结果(DOM 的形状),还需要处理应用程序数据。DOM 与数据之间的交互会随着应用程序的增长而变得越来越复杂,所以自行管理该交互会变成一种容易出错的做法。理想情况下,您会将这些工作转交给第三方框架来负责。您希望投入尽可能少的精力来解决应用程序/视图状态同步问题。
在您可选择的几个 JavaScript 框架中,新一代 Vue.js 框架非常关注如何用极少的外部特性来解决应用程序/视图状态同步问题。Vue.js 是一个面向对象、数据驱动的 DOM 管理系统 — 或者更简洁地说,就是一个对象-DOM 绑定系统。Vue.js 负责在浏览器 DOM 中创建应用程序的数据清单。
这个由两部分组成的教程系列将介绍 Vue.js 的基本知识,然后介绍 Vue 的高级用法。在第 1 部分中,将会使用 Vue、 NPM 和 webpack 为一个模块化应用程序构建一个小巧但功能全面的 UI,以及一个符合生产级的构建版本和依赖关系管道。在第 2 部分 中,会通过 IBM Bluemix 平台即服务将该应用程序部署在云中。完成本教程的学习后,您就可以轻松地在自己的项目中应用 Vue。请参见部分,获取第 1 部分的完整示例代码。
尽管具有极低的复杂性,但 Vue.js 支持复杂的、大规模的需求。该框架为您提供了足够的工具来构建您所需的任何功能,无需堆叠其他大量特性。根据我的经验,与其他框架相比,Vue 在复杂性和特性频谱中找到了简单性与功能的平衡点:
首先,我将向您介绍 Vue.js 的基础知识,以便您能了解 Vue 的工作原理和如何最佳地使用它。然后,在此基础上,您将使用更高级的 Vue 技术,以及现代的、提供生产级环境支持的工具来构建应用程序和管道。
为新项目创建一个名为 recruiteranking 的目录。然后 下载 最新的 Vue 包,将其保存在一个单独的位置,在包含该 Vue 包的 recruiteranking 中创建一个 index.html 文件,如清单 1 所示。
清单 1. 基本的 index.html 文件
<html> <head> <meta charset="utf-8"> <script type="text/javascript" src="vue.js"></script> </head> <body> <div id="test"> <p>User: {{ username }}</p> </div> <script> test = new Vue({ // 1 — Instantiate a vue instance el: '#test', data: { username: "Luke Skywalker" } }); </script> </body> </html>
“ Vue 中的大部分操作都是通过 Vue 实例完成的,这正是我将 Vue 描述为面向对象的原因。 ”
在中,请注意您在 <script>
标记中创建 Vue
实例的位置(标为注释 1)。Vue 中的大部分操作都是通过 Vue
实例完成的,这正是我将 Vue 描述为面向对象的原因。这个 Vue
实例仅有两个字段,二者都是 Vue
类的关键属性。 el
属性告诉 Vue 它将绑定到哪个 DOM 元素;此属性可以是任何解析为单个元素(或实际元素)的查询。如果您熟悉 Backbone,可以认为此字段类似于该框架中的 el
属性。
第二个属性是 data
字段。此属性是 Vue
对象上的一个神奇的字段,它表示实例状态,可自动与 DOM 交互。 data
属性也是一个包含 username
字段的对象。 username
字段是我创建的一个字段。 data
属性可包含任何类型、任何名称的字段,包括数组和复杂的嵌套对象。现在来看看 HTML 的主体, <div>
中具有 id="test"
的段略元素包含 {{ username }}
标志。此标志是一个模板标志,与 mustache 标志非常相似(它们对于 ${}
格式的 JavaServer Pages Expression Language 标志的意图类似)。
Vue 将 test
<div>
与 test
实例相关联,并将 username
令牌替换为实例上的 username
字段的值。所以,如果加载该页面,它会显示 User:Luke Skywalker :
现在,在浏览器中打开一个 JavaScript 控制台。(如果使用的是 Chrome,可以按下 F12 打开开发人员控制台。)因为您为 Vue
实例提供了全局范围内的一个引用,所以您可以在控制台中访问 test
实例。可以通过更改 test Vue
实例上的 data
对象的值来更改页面上显示的用户名。如何获得 data
字段的访问权?您不会获得该权限。 data
字段可在实例上直接设置: test.username
。
所以,如果您将 test.username = "Han Solo";
输入控制台中,就会在 UI 中看到该变化的反映,其中 User:Han Solo 取代了 User:Luke Skywalker :
您刚执行的更改通过探出 Vue
实例的状态并直接修改它而破坏了封装。Vue 提供了一种更好的替代方法。清单 2 向 test
对象添加了一个 methods
属性。
清单 2. methods
属性
test = new Vue({ el: '#test', data: { username: "Luke Skywalker" }, methods: { changeName: function(name){ this.username=name; } } });
test Vue
实例现在有一个 methods
字段,其中有一个 changeName
方法。这个简单的方法接受一个参数,然后使用该参数设置您之前看到的 username
数据成员。现在,在 JavaScript 控制台中,您可以 直接 访问 changeName
方法,像 test
实例上的方法一样。打开 JavaScript 控制台并输入 test.changeName("Chewy");
。
现在,进一步完善这个简单示例,看看 Vue 如何支持绑定到用户输入字段。再次访问 test <div>
并添加一个 input
字段,将该字段的 v-model
属性设置为 username
:
<div id="test"> <p>User: {{ username }}</p> <input v-model="username"> </div>
Vue 从 Angular 借鉴了术语 指令 , v-model
属性也被视为指令。通过这次简单添加,UI 现在有一个绑定到 Vue 模型的表单输入字段。该绑定是 双向的 :如果 Vue 数据模型发生更改,表单字段也会发生更改,反之亦然。
v-model
指令使创建与表单输入的双向绑定变得非常简单。您可以在控制台中运行 test.changeName("Yoda")
来测试绑定的两个方向。您会看到,文本显示和表单输入值都会发生变化来反映更新。
我们要介绍的 Vue 的最后一个基本特性是用户事件处理。您通过指令处理事件。在本例中,格式为 v-on:events
,其中 events
是您想要监听的事件,比如 click
。
在该标记中,添加一个使用事件处理的按钮:
<div id="test"> <p>User: {{ username }}</p> <input v-model="username"> <button v-on:click="defaultUser">Default User</button> </div>
(使用 Vue 速记语法,您可以将事件指令缩短为 @
。所以您可以将该按钮处理函数重写为 <button @click="defaultUser">Default User</button>
。)
清单 3 包含对 JavaScript 的相应更改:添加一个 defaultUser
方法来将 username
设置回 Luke Skywalker
。
清单 3. 向 Vue
实例添加一个单击处理函数
test = new Vue({ el: '#test', data: { username: "Luke Skywalker" }, methods: { changeName: function(name){ this.username=name; }, defaultUser: function(){ this.changeName("Luke Skywalker"); } } });
单击该按钮,并观察绑定到 username
属性的所有元素是否都被更新为默认值,通过此操作验证 v-on:click
处理函数是否有效。
现在,您已经了解了 Vue 中的状态、 data
属性、行为和 methods
属性。将上述这些与 el
属性、事件处理和绑定结合使用,它们是 API 的核心元素。在继续阅读本文的时候,您会发现这些简单的基础元素为您提供了大量强大的功能。
本文的重点是演示应用程序的前端。我通过 Spark Framework 设置的一个简单的演示后端提供了简单的端点来显示前端 JavaScript。本文没有涉及到数据库:所有数据都在内存中。您在第 2 部分 中学习生产部署时,将会了解后端的内部原理。
您已准备好开始构建一个更复杂的演示应用程序:一个 RESTful 单页应用程序。(您需要继续在 recruiteranking 项目的文件中操作,所以不要删除它们。)Vue 是我们要介绍的主角,但要将各部分组合起来,还需要使用其他技术作为配角。具体来讲,将会使用 webpack 作为构建系统。webpack 是一个强大的、高度可配置的系统;我们仅使用它的少部分功能。我们还将使用 Foundation 实现响应式 CSS 布局,但 Foundation 仍然高度透明。此外还会使用 NPM。
使用此技术栈,构建一个应用程序来帮助对您接触过的招聘公司进行排名。该应用程序在顶部有一个徽标和菜单栏,还有一个包含招聘公司的主表格。表格的行按 1 到 5 排名,可扩展这些行来编辑公司的细节。
您需要使用 Node 和 NPM 来安装 webpack,然后包含依赖项。按照以下 说明 安装二者。
构建流程的目的是:除了 Node 和 NPM 外, 不 安装任何全局依赖项。此方法可保持工作区干净整洁。所有依赖项都在项目中明确列出,可通过一个命令部署在任何环境中。
要确认您的系统已准备好,可以打开命令行并运行 npm -v
。如果获得了一个版本作为响应,那么您的 NPM 安装已准备就绪。
如果之前没有为项目创建过目录,那么请创建一个:
mkdir recruiteranking
更改到 recruiteranking 并运行以下命令来获得一个默认的 package.json 文件:
npm init
稍后您会在 package.json 中定义开发依赖项,还会借助 webpack 的强大功能来指定部署依赖项,比如 jQuery。但继续后面的操作之前,需要在 Git 中设置跟踪,以便为更改建立一个保护网:
git init git add git commit -m "a single step"
或者不采用 "a single step"
,而是将它称为 "initial commit"
或适合您的风格的任何叫法。
现在,在一个前端 IDE 中打开 package.json。(我最喜欢的是 Sublime。)将您的依赖项添加到 package.json,然后,可将它用作客户端依赖的每个工具的记录列表(与 Apache Maven 对 Java™ 应用程序的做法相同)。请记住,您希望通过 NPM 来管理其他所有依赖项,而且没有隐藏的依赖项;然后,可以运行一个 NPM 命令来安装您需要的所有包。清单 4 给出了更新的依赖项列表。
清单 4. package.json 依赖项列表
{ "name": "recruiteranking", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { }, "author": "", "license": "ISC", "dependencies": { "jquery": "2.1.1", "foundation-sites": "5.5.3", "webpack": "~1.12.6", "webpack-dev-server": "~1.14.0", "style-loader": "~0.6", "script-loader": "~0.5", "css-loader": "~0.23.0", "node-sass-loader": "~0.1.7", "sass-loader": "~3.1.2", "vue": "1.0.10", "vue-loader": "7.2.0" } }
现在开始,我只会介绍要执行的更改,而不是介绍整个列表。
在中,重点在于添加的 dependencies
属性,它包含多个包:
*-loader
依赖项。这些都由 webpack 使用。webpack 可使用和组合各种各样的文件类型,高效地将它们打包在一起,将它们作为 JavaScript 文件包含在您的最终项目中。所以 “加载器” 就像是 webpack 的术语,您可以通过它们为 webpack 提供与 JavaScript、CSS 和 Vue 通信的能力。 在命令行上转到项目的根目录(package.json 文件位于这里)并键入 npm install
。此命令将会添加 node_modules 目录,并将指定的包下载到该目录。等待(可能时间有点长)所有包下载完成。
现在,您需要依赖的所有工具都已准备就绪,可以使用 webpack 将它们集中在一起。
在项目根目录中,添加一个 webpack.config.js 文件。此文件将会配置 webpack;它是真正的 JavaScript 而不是 JSON,为您提供了更高的灵活性。清单 5 显示了入门 webpack 配置。
清单 5. webpack.config.js
var path = require("path"); module.exports = { entry: { main: "./app/js/main.js" }, output: { path: __dirname, filename: "bundle.js" }, module: { loaders: [ { test: //.css$/, loader: "style!css" }, { test: //.scss$/, loaders: ["style", "css", "sass"] }, { test: //.vue$/, loader: 'vue' } ] }, sassLoader: { includePaths: [path.resolve(__dirname, "./node_modules/foundation-sites/scss/")] } };
我将快速略过这个配置文件,仅在每个需要注意的地方稍作介绍。
首先,要定义一个包含 path
,该模块提供了处理文件系统的功能。
接下来是对 module.exports
的调用,该名称在 CommonJS 中的意思是 “这是我的包中包含的内容”。webpack 构建项目时依赖于这个模块。如果您熟悉 Maven,可以将 module.exports
视为与 Maven 项目对象模型 (POM) 等效的 Webpack 模块。
entry
webpack 支持多个入口点,并将应用程序输出分解为多个 很方便,也可以方便地支持项目的增量加载。
一种常见的用例是定义一个与应用程序 bundle 独立的供应商 bundle。然后,应用程序可在必要时加载经常改变的应用程序代码,保持用户浏览器缓存中的供应商代码不变。
这里不需要使用这些特性,但您可以在 此处 找到它们的深入介绍。
entry
字段告诉 webpack 从何处开始该项目,被用作模块包含的根目录。可以在中看到,/app/js/main.js 定义了项目的所有运行时依赖项。
output
output
告诉 webpack 将最终结果放在何处。您的 index.html 在包含该构建版本时引用了这个文件。
module
配置您的 webpack 的加载器。加载器负责读取原始资产并将它们转换为最终的 bundle。加载器是可插拔和可自定义的。 module
上的详细信息会告诉加载器它们应该处理哪些文件类型(例如样式加载器处理 .css)。 test
字段表示 “如果该文件通过此测试,则应用此加载器。” module
之后是 sassLoader.includePaths
,它引导 Sass 加载器在 foundation-sites
依赖项在 NPM 内添加的 foundation 目录中查找 — 必须这么做,才能正确解析您应用程序中的 foundation SCSS includes。
您的最终构建资产是一个 JavaScript 文件。CSS/SCSS 加载器创建一个由 JavaScript 捆绑和注入的最终产品。不会输出任何最终的 CSS 文件。
现在,您已经拥有了生产级依赖项管理功能和一种生产级构建设置。您已准备好使用二者来构建 UI 了。
将包含以下内容的入口文件 main.js 添加到 /app/js/ 中(相对于您的项目根目录的路径):
$ = jQuery = require('jquery'); foundation = require('foundation-sites'); Vue = require("vue"); require('../css/app.scss');
在这个初始 main.js 文件中,您需要 jQuery 和 Foundation 包,以及您自己的本地 app.scss 文件,该文件是您的样式的入口点。请注意,您为 jQuery 提供了两个全局标识符 $
和 jQuery
,使用它们作为 Foundation JavaScript 中一些松散的引用的解决方案。这些标识符非常标准,不会导致任何问题。您还需要将 Vue
全局化,我们稍后将解决这个问题。
将包含以下内容的 app.scss 文件添加到 /app/css/app.scss 上:
@import "settings"; @import "foundation";
您在此文件中包含了 Foundation Sass 和您自己的所有全局样式。
要对全新的 JavaScript/Vue/Foundation 装备执行的第一个操作是切换您部署和测试该应用程序的方式。webpack 提供了一个基于 Node/Express 的开发服务器来托管该应用程序,这使得编码工作变得很轻松。该开发服务器还支持代理,这便于稍后与后端进行交互。要运行开发服务器,必须对您的 package.json 文件进行更改。因为您的所有依赖项都是本地托管的 NPM 包,所以需要对 webpack 运行 NPM。因此,在 package.json 中,要将此条目添加到 scripts
对象中来启用开发服务器:
"scripts": { "server": "webpack-dev-server" }
现在打开命令行并运行 npm run-scripts server
,以便让该应用程序在 localhost 上的 8080 端口上运行。 run-scripts
是一个运行指定脚本的 NPM 工具,会加载 package.json 中定义的所有本地依赖项。现在,只需一行代码即可加载所有依赖项,打包它们,然后在浏览器中测试它们。更好的地方是, webpack-dev-server
会实时地热部署您的代码更改。开发服务器将构建版本捆绑到内存中并从内存部署;它未将任何输出文件放在文件系统中,这使得代码更改时能快速地重新部署。
现在,在您的根目录中创建的 index.html 文件中(参见),将包含的 JavaScript 文件的名称从 vue.js
更改为 bundle.js
:
<script type="text/javascript" src="bundle.js"></script>
在浏览器中,打开 http://localhost:8080/index.html。您可以看到该应用程序已加载,且像之前一样正常运行,例外的是该按钮和输入现在已样式化 — 因为 Foundation 已成功加载,而且它的 CSS 已注入到页面中。外观变得更加美观了。该页面也是响应式的;调整浏览器窗口,您会看到输入字段也相应地进行了调整。
现在是时候消除全局 Vue
变量并将您的 JavaScript 转移到 main.js 中了。剪切 index.html 中的 <script>
标记中的代码并将它们粘贴到 main.js 中。将 bundle.js
include 转移到 index.html 的末尾处;需要在解析标记后调用它,以便代码可以在浏览器渲染和 DOM 激活后引用 DOM。现在您的 index.html 文件类似于清单 6。
清单 6. 更新后的 index.htm
<html> <head> <meta charset="utf-8"> </head> <body> <div id="test"> <p>User: {{ username }}</p> <input v-model="username"> <button v-on:click="defaultUser">Default User</button> </div> <script src="http://localhost:8080/webpack-dev-server.js"></script> <script type="text/javascript" src="bundle.js"></script> </body> </html>
您的 main.js 文件现在类似于清单 7。
清单 7. 更新后的 main.js
$ = jQuery = require('jquery'); foundation = require('foundation-sites'); var Vue = require("vue"); require('../css/app.scss'); test = new Vue({ el: '#test', data: { username: "Luke Skywalker" }, methods: { changeName: function(name){ this.username=name; }, defaultUser: function(){ this.changeName("Luke Skywalker"); } } });
将该页面重新加载到浏览器中,它会像之前一样正常运行。请注意中的另一点:您使用 var
限定了 Vue
引用的范围,所以现在不会将该变量泄漏到全局命名空间中。
“ 您将会看到如何将应用程序功能组合到组件中。组件是促进应用程序设计中的重用、删除重复和模块化的好方法。 ”
您的依赖项管理和构建系统都已就绪,您已装入了所需的包,这些包正在正常运行,而且脚本代码已组合到一个外部 JavaScript 文件中。现在看看如何将应用程序功能组合到组件中。组件是促进应用程序设计中的重用、删除重复和模块化的好方法。
Vue 应用程序有一个主要的父 Vue
实例,它包含并呈现所有其他实例。(可以拥有多个 Vue
实例,但这里不需要这么做。)您首先将登录特性改进为一个名为 user
的组件。
这是 user
组件的 index.html 的新的主体内容:
<div id="app"> <user></user> </div>
名为 app
的 <div>
仍是一个 Vue
实例,但您将自定义 <user>
标记创建为一个 Vue 组件。现在访问 main.js。保持 import 不变,删除剩余代码。为 user
组件添加一个组件定义,然后添加 App Vue
的声明。清单 8 显示了组件声明。
清单 8. 以编程方式定义一个 user
组件
Vue.component('user', { template: '<div>User: {{ username }}</div>', data: function(){ return { username: "Luke Skywalker" } } });
清单 8 使用了 Vue.component
方法,您可以使用它在一个名称(第一个参数)下注册一个组件,并跟随着一个 组件参数对象(第二个参数)。
在 Vue 组件中,使用一个函数返回数据字段,以避免跨实例的数据共享。
请注意,HTML 标记已转移到 template
属性中。(如果您熟悉 Dojo,就会注意到它与 Dojo 的小部件系统的模板属性具有类似的工作原理。)接下来是一个 data
属性 — 一个 返回 数据对象的函数, 而不是 直接返回数据对象。采用该方法的原因是,您创建了一个可在多个位置重用的组件,就像一种类型。您不希望所有类型实例共享 data
字段,如果它是一个直接对象,就会发生这种情况。
接下来将以下行(实例化 App
实例)添加到 main.js 中:
var App = new Vue({ el: "#app", data: {}, methods: {} });
App Vue
很简单 — 它只获取该元素。
如果重新加载该页面,就会看到各项功能都正常运行。 App
实例已成功将 user
组件注入到其标记中,而且呈现了 User:Luke Skywalker 。
现在,我们将在组件化上更进一步,将 User
组件转移到它自己的 .vue 文件中。从项目的根目录,创建一个 /app/vue 文件夹并添加一个 user.vue 文件。将清单 9 中的代码添加到该文件中。
清单 9. 在单独的文件 user.vue 中定义的 User 组件
<style> </style> <template> <p>User: {{ username }}</p> </template> <script> module.exports = { data:function () { return { username: "Luke Skywalker" } } } </script>
中的 3 个标记( <style>
、 <template>
和 <script>
)表示任何组件的 3 个元素:CSS、标记和代码。(针对 Sublime 用户的提示:单击 View > syntax > HTML 告诉 Sublime 显示正确的突出显示。)
目前为止,尚未对该组件使用任何自定义 CSS,所以 <style>
标记是空的。 <template>
元素包含您熟悉的标记, <script>
标记包含您熟悉的 data
元素,但以不同方式包装它。因为您位于一个捆绑为 CommonJS 组件的文件中,所以需要使用 module.exports
并像中那样定义您的组件。)请注意,您没有在这里声明组件名称。
返回到 main.js,如何使用该组件?您必须做两件事:包含该组件并在您的 App Vue
中引用它,如清单 10 所示。
清单 10. 在 App Vue
中使用模块化的 User
组件
var User = require("../vue/user.vue"); var App = new Vue({ el: "#app", data: {}, methods: {}, components: { user: User } });
在中,您请求 user.vue 文件并为它提供一个局部变量引用。接下来,在 App
实例声明中,添加一个 components
属性。 components
属性的职责是将您的 User
Vue 类型映射到 <user>
标记。该组件的键是在 Vue 标记中很有用的标记名称,组件值是这些标记引用的 Vue 组件。
现在,如果重新加载,就会再次看到 User:Luke Skywalker ,证明应用程序仍能运行,并按原样分解为可加载的组件。所有各部分都已准备就绪。您已准备好采取下一步操作,开始将这些部分集成到一个真实的应用程序中。
首先,使用 Foundation 添加一个菜单栏,并将您的 user
组件放入其中。清单 11 显示了已经更新了菜单栏代码的 index.html。
清单 11. 包含用户名顶栏的 index.html
<html> <head> <meta charset="utf-8"> </head> <body> <div id="app"> <div class="sticky"> <nav class="top-bar" data-topbar role="navigation" id="top-bar"> <ul class="title-area"> <li class="name"> <h1> <a href="#">Recruiteranking</a> </h1> </li> </ul> <section class="top-bar-section"> <ul class="right"> <li class="active"> <user></user> </li> </ul> </section> </nav> </div> </div> <script src="http://localhost:8080/webpack-dev-server.js"></script> <script type="text/javascript" src="bundle.js"></script> </body> </html>
现在,当重新加载时,该应用程序在顶栏的左侧显示了 Recruiteranking ,在右侧显示了 User:Luke Skywalker :
接下来,需要构建一个更好的数据流系统。首先让 username
字段摆脱 user
组件的控制,并将它提供给 App
。在这里,您会看到 Vue 组件分层结构的实际强大功能。
转到 user.vue 并执行清单 12 中所示的更改 — 添加 props
成员。
清单 12. 将 props
成员添加到用户中
<script> module.exports = { props: ["username"], data:function () { return { username: "Luke Skywalker" } } } </script>
props
成员会告诉该组件哪些属性可供其父组件访问。所以在这里,应该表明 username
可供父 Vue
实例访问。
接下来,将一个 data
字段添加到 App Vue
实例中。这个 data
字段可以是一个直接对象,而不是函数,因为您不会重用此实例。在 data
字段中,放入一个包含 username
字段的 User
对象。您放入了一个 User
对象,因为接下来您可能希望添加已登录用户的其他信息,比如 ID。 App
的新 data
字段为:
data: { user: { username: "Luke Skywalker" } }
这里要注意两点。首先,父 Vue
实例可与子组件交互。其次,Vue 可将 App
上的 data
字段作为复杂对象而无缝地处理。
现在,要将这些部分衔接在一起,可返回到 index.html 字段,使用 v-bind
指令告诉 user
组件在何处查找它的 username
数据。这是将父对象绑定到 User
组件的标记:
<user v-bind:username="user.username"></user>
Vue 会毫无怨言地导航 user
上的句点运算符,以便获得 username
属性。
v-bind
速写语法 您可以对 v-bind
使用速写语法,该语法类似于针对事件处理的速写语法。Vue 将冒号解释为 v-bind
指令。所以将父数据绑定到 User
组件的标记可重写为 <user :username="user.username"></user>
。
user
组件的 username
字段现在可以简单而又优雅地绑定到父 user.username
字段。默认情况下,数据绑定是 单向 流动的,从父字段流向子字段。Vue 支持双向通信,甚至拥有一个组件间事件系统。就现在而言,您只需要了解基本的父子通信,但在需要的时候知道您可以实现双向通信也很不错。
如何才能让用户进行登录?回想目前完成的所有工作,您可能会对这么快的进度感到惊讶。
首先,通过将 user.vue 文件中的 username
字段设置为 null
,删除默认用户 Luke Skywalker:
module.exports = { user: null };
user
组件现在仅关心数据的显示,而不是数据本身。
此刻,如果您重新加载并查看控制台,就会看到一个错误: [Vue warn]:Error when evaluating expression user.username.Turn on debug mode to see stack trace.
出现该错误是因为在 index.html 的 HTML 中,您引用了 user.username
,而且您的 user
对象为 null
。您只希望在一个人登录时显示用户名。可以使用 v-if
指令实现此有条件渲染:
<li class="active" v-if="user"> <user v-bind:username="user.username"></user> </li>
v-if
指令表示 “仅在 v-if
条件为 true 时渲染此元素”。在这种情况下,仅在用户数据对象不为 null
时渲染此列表项 — 这正是您想要的结果。现在用户名仅在用户登录时显示。
要在用户为 null
时显示一个 Log In 按钮,可在顶栏中添加一个 else
代码块:
<li class="active" v-if="user"> <user v-bind:username="user.username"></user> </li> <li class="active" v-else> <a href="#" v-on:click="showLogin">Log In</a> </li>
请注意,登录操作上有一个指向 showLogin()
的 v-on:click
处理函数。将该处理函数添加到您的 App Vue
中,如清单 13 所示。
清单 13. App Vue
上的 showLogin()
方法
App = new Vue({ el: "#app", data: { user: null, loggingIn: false }, methods: { showLogin: function(){ this.loggingIn=true; } } //...
在 logIn()
方法中执行的所有任务只是将 App.data
中的 loggingIn
标志设置为 true
。您可以使用该标志显示一个登录窗格。(Vue 也支持动画过渡。)您可以注意到,您没有直接执行任何 DOM 操作;设置该标志并让标记中的 Vue 指令来完成工作。
在 index.html 中,在紧挨 top-bar 标记后的地方添加一个登录表单,如清单 14 所示。
清单 14. 将一个登录表单添加到 index.html 中
<div id="app"> <div class="sticky"> <nav class="top-bar" data-topbar role="navigation" id="top-bar"> ... </nav> </div> <div class="small-12 columns" id="sign-in-on" v-show="loggingIn"> <div class="row"> <div class="large-12 columns"> <div class="signup-panel"> <form id="signup-form" data-abide="ajax"> <div class="row collapse"> <div class="small-10 columns"> <input type="text" placeholder="Username" name="username" required> </div> </div> <div class="row collapse"> <div class="small-10 columns "> <input placeholder="Password" name="password" required type="password"> </div> </div> <button type="submit" v-on:click="login">Log In</button> </form> </div> </div> </div> </div> </div>
登录页面会在 main.loggingIn
设置为 true
时显示。您可以注意到,您是使用 v-show
指令实现此结果的。 v-show
不同于 v-if
,因为 v-show
虽然渲染了此内容,但隐藏它,而 v-if
完全不会渲染该内容。在本例中,您选择了隐藏和显示 (hide-and-reveal),而不是按需渲染。您的 Log In 按钮也已附加到方法 login
(同样使用 v-on:click
)。该方法为您执行登录操作。
现在需要与后端进行通信了。出于本教程的目的,我提供了一个可在本地运行的后端(请参见)。如果您在工作站上同时开发前端和后端(换句话说,您是一位 “全堆栈” 开发人员),那么您可以在端口 4567 上运行开发后端,代理会请求它,就像我在这里使用演示应用程序所做的一样。执行 java -jar rr-backend-1.0.jar
来运行所下载的后端服务器。该服务器启动并开始通过嵌入式 Jetty(通过 Spark Framework)处理请求。
您需要采用某种方法来告诉客户端开发服务器将 API 请求转发给后端。webpack 在开发服务器代理中提供了该功能。将清单 15 中的代码添加到您的 webpack.config.js 文件的根级对象中。
清单 15. 将开发服务器代理添加到 webpack.config.js
module.exports = { ... devServer: { proxy: { '/api/*': { target: 'http://localhost:4567', secure: false } } } }
现在,保持后端应用程序一直运行,对 /api/*
路径的任何请求都会转到该服务器,并在端口 4567 上进行监听。
保持后端服务器一直运行,在浏览器中访问 http://localhost:8080/api/test 来验证服务器是否通过开发服务器推送回了它的响应。如果响应是 TEST OK
,则一切正常。
要继续采用模块化设计,可以将您的登录表单转变为组件。清单 16 显示了该组件。
清单 16. login
组件
<style> </style> <template> <form action="{{ action }}" method="{{ method }}" v-on:submit.stop.prevent="login"> <div class="row collapse"> <div class="small-10 columns"> <input type="text" placeholder="Username" name="username" v-model="payload.username" required> </div> </div> <div class="row collapse"> <div class="small-10 columns "> <input placeholder="Password" name="password" v-model="payload.password" required type="password"> </div> </div> <button type="submit">Log In</button> </form> </template> <script> module.exports = { props: { 'action': { type: String, required: true }, 'method': { type: String, default: "post" } }, data: function() { return { payload: { username: null, password: null } } }, methods: { login: function() { this.$http.post(this.action, this.payload, function (data, status, request) { this.$dispatch('onLoginSuccess', data); }).error(function (data, status, request) { console.error(status); }); } } } </script>
您应该对 login
组件最为熟悉,但有两个值得注意的新内容。您可以注意到,您有一个更加详细的 props
字段,您使用它从表单中调入 method
和 action
。 props
字段显示 Vue 为 required
、 type
和 default
参数提供了一定的支持。 props
字段甚至包含一个验证器,以防您需要它。
可以使用 v-on:submit.stop.prevent="login"
监听模板表单的提交,在这里的 v-on
指令上,可以看到两个修饰符: stop
和 prevent
。这些修饰符的用途是预防默认的浏览器操作和阻止事件传播。根据您的需要,可以像此处一样一起使用这些修饰符,也可以单独使用它们。
调用 login
方法时,发送了一个包含来自 data
对象的 payload
字段的 post
,它通过 v-bind
绑定到输入字段。 $http
对象来自 vue-resource
插件,我们稍后将安装该插件。但是首先请注意,您对一个字符串和从 post
返回的数据执行了 #dispatch
。通过使用 $dispatch
(Vue 组件事件系统的一部分),子组件可在组件树中向父组件发送事件。您将在 App Vue
中处理此事件。
转到 main.js 并添加 events
字段:
events: { onLoginSuccess: function(data) { this.user = data; this.loggingIn = false; } }
events.onLoginSuccess
捕获该事件。后端测试服务是一个存根,它返回添加了 UUID 的用户对象。您在 App.user
上设置 user
对象,并将 loggingIn
设置为 false
,这会隐藏登录表单。
在这些更改生效之前,需要包含 vue-resource
插件。您可以使用直接 XHR 或 jQuery 或您想要的 Ajax 支持,但 vue-resource
插件会让各项功能变得干净而又简单,就像您在中的 login
方法中的 this.$http
调用中看到的一样。要添加 vue-resource
支持,可以转到 package.json 并将 "vue-resource":"0.5.1"
添加到依赖项。接下来,进入 main.js 并将此行添加到 imports 中:
Vue.use(require('vue-resource'));
可以注意到,您调用了 Vue.use()
来将模块作为插件添加到 Vue 运行时中。
有两种选择来向表单添加验证(Foundation 和 Vue 都包含验证器支持),但我们跳过了验证,前进到与招聘公司相关的功能。您想要一个可供用户单击来查看详细信息的表格,而且还希望登录的用户能够编辑、删除和创建招聘公司条目。登录需求也就是应用程序的授权部分。(很容易通过推断创建用户角色或访问控制列表来作为授权的基础,但为了简便起见,这里只使用了登录检查。)
将 CRUD 操作映射到 RESTful 语义,如下所示: PUT
表示创建, GET
表示读取, POST
表示更新, DELETE
表示删除。所以您的服务器中用于 recruiter
的 API 类似于:
GET /api/recruiter POST /api/recruiter PUT /api/recruiter DELETE /api/recruiter
这里唯一有争议的地方是 POST
和 PUT
动词的含义,但这是一个很小的问题,不会让人夜不能寐。
测试服务器是全内存型的,没有验证或授权检查,因此,如果重新启动该服务器,就会丢失更改。
首先创建 grid 组件来显示招聘公司列表,使用来自 Vue 的 表格示例 作为起点。此组件将您目前学到的许多功能融合在一起,还引入了一些新功能。
通过将清单 17 中的代码放在 /app/js/vue/grid.vue 文件中来创建 grid
组件。
清单 17. grid
组件:/app/js/vue/grid.vue
<style> /* CSS truncated for brevity */ </style> <template> <table> <thead> <tr> <th v-for="key in columns" @click="sortBy(key)" :class="{active: sortKey == key}"> {{key | capitalize}} <span class="arrow" :class="sortOrders[key] > 0 ? 'asc' : 'dsc'"> </span> </th> </tr> </thead> <tbody> <tr v-for="entry in data | orderBy sortKey sortOrders[sortKey]"> <td v-for="key in columns" v-on:click="rowClick(entry)"> {{entry[key]}} </td> </tr> </tbody> </table> </template> <script> module.exports = { props: { data: Array, columns: Array }, data: function() { var sortOrders = {} this.columns.forEach(function(key) { sortOrders[key] = 1 }) return { sortKey: '', sortOrders: sortOrders } }, methods: { sortBy: function(key) { this.sortKey = key this.sortOrders[key] = this.sortOrders[key] * -1 }, rowClick: function(row){ this.$dispatch('onRecruiterDetail', row); } } } </script>
可以注意到,这个组件包含我为了保持简单性而在清单中省略的样式。您可以在可下载的应用程序中看到该样式 — 我们为您提供了表格外观的简单 CSS。
可以在中的 <template>
中注意到,您有一个基本的表格布局。头标记 <th>
包含一些新内容:
v-for
指令(表示 Vue 的迭代器支持):您可以通过它根据数组来发出指定的标记。在本例中,您使用了 v-for="key in columns"
,它类似于一个正式的 JavaScript for
循环,表示 “对于 columns
集合中的每一项,渲染该标记,将 key
变量公开为当前元素的引用。” sortBy
方法,并通过 @click="sortBy(key)"
传入当前键。 :class="{active: sortKey == key}"
。请记住,单独一个冒号是 v-bind
语法的一种速记,所以您看到的是与 Vue
实例的 data
成员的绑定 — 但在本例中,表示 class
属性。Vue 为 class
和 style
属性提供了一些特殊的处理方法。每个属性接受一个逗号分隔的名称列表,每个名称指向一个解析为布尔值的表达式。在本例中,如果 sortKey == key
,那么这个 <th>
元素就会收到 active
类: <th class="active">
。此表达式暗示 Vue 组件实例有一个类似于 key
变量的 sortKey
数据字段。 <th>
的主体的基本标志,包含一个您不熟悉的特性 — 过滤器 : {{key | capitalize}}
。该过滤器名为 capitalize
,它通过竖线字符添加到 key
的基值中。 capitalize
是一个内置的过滤器,其用途从字面上就可以看出。(借助 Vue,您还可以定义自己的自定义过滤器,但这里不会这么做。) 中下一个有趣的地方是表主体的 <td>
元素上的 v-for
指令: v-for="entry in data | orderBy sortKey sortOrders[sortKey]"
。此循环在 data
属性上执行并使用 orderBy
函数修改结果,它传入两个参数: sortKey
和 sortOrders[sortKey]
。 orderBy
使用参数来确定排序算法。第一个参数定义了要使用有序对象上的哪个字段,第二个参数告诉该算法按升序 ( true
) 还是降序 ( false
) 进行排序。
该组件的 JavaScript 部分负责支持标记的各个部分。 props
字段提供了 data
(表格主体所迭代的数组)和 columns
(标头所迭代的数组)。父组件设置 data 和 columns 数组;父组件负责将应用程序数据提供给子组件进行渲染。
data
字段证明您可以在返回 data
对象之前执行任意 JavaScript。在中, data
代码准备 sortKey
和 sortOrders
字段。
对于 columns 数组中的每个键, sortOrders
被设置为 1
,该值在表格主体上的 orderBy
操作中解析为 true
(升序)。
该组件有两个方法: sortBy
和 rowClick
。 sortBy
方法设置活动的软键,然后通过乘以 -1 来对该键的 sortOrders
值求反: this.sortOrders[key] = this.sortOrders[key] * -1
。最后, rowClick
方法接受传入的项(当前被迭代的对象)并发出一个 onRowClicked
事件供父 Vue
处理(它向上遍历父组件链并继续调用任何 onRowClicked
处理函数,直到返回 false
或父组件链结束。)
要遍历该表格,只需将它包含在 main.js 中并提供 columns
和 data
属性。在 main.js 中,可以添加 var grid = require("../vue/grid.vue");
来包含该组件。请记得在 components
属性中注册该 grid 组件:
components: { user: user, login: login, grid: grid }
另外,将 gridData
和 columns
字段添加到 data
属性中,并注意在这里设置表格的列标题:
gridColumns: ['name', 'rank'], gridData: []
现在,您需要通过 ready
方法填充 gridData
。您目前应该尚未看到过这个方法。 ready
是一个生命周期回调,它在渲染并完全准备好 Vue
实例后执行。清单 18 显示了 App.ready
方法。
清单 18. 使用 ready()
回调将数据加载到 App Vue
中
ready: function() { this.$http.get("/api/recruiter").then(function(response) { this.gridData = response.data; }, function(response) { console.error(response.status); }); }
在 ready
方法中,再次使用 vue-resource $http()
方法,像对 login
所做的一样。这一次,要将一个 GET
请求发送到 /api/recruiter
URL(还记得您的代理根据 /api/
路由将此请求转发到服务器吗?)使用 .then
提供一个处理函数。response 参数包含您处理响应所需的所有信息。在本例中,会将 data
字段设置为您的 gridData
。当 main Vue
准备好后,会从服务器填充您的表格。
最后一步是将 grid
组件引入到布局中,这很容易实现。在 index.html 中,将以下标记添加到 HTML 主体,放在紧挨顶栏代码的下方:
<div class="row panel" id="main"> <div class="large-6 columns"> <grid :data="gridData" :columns="gridColumns"></grid> </div>
在前面的代码中,将您需要的字段绑定到所提供的数据: data
绑定到 gridData
, columns
绑定到 gridColumns
。现在,如果重新加载该页面,您就会看到招聘公司表格,其中填充了一些初始行:
现在您希望能够单击一行来查看细节。完整的示例应用程序(参见)在 /js/app/vue/recruiter-detail.vue 文件中包含一个 recruiter-detail
组件。该组件不包含任何新的和不同的内容,所以我不会深入介绍它。将 recruiter-detail
作为组件添加到 main.js 中,然后将它包含在标记中,如清单 19 所示。
清单 19. 将 recruiter-detail
组件添加到 index.html 标记中
<div class="row panel" id="main"> <div class="large-6 columns"> <grid :data="gridData" :columns="gridColumns"></grid> </div> <div class="large-6 columns" v-show="detail"> <detail :payload="detail" :editable="user" action="/api/recruiter"> </detail> </div> </div>
这里值得注意的部分是 :payload
绑定,它被解析为 main Vue
上的 detail
。回想一下 grid
组件在单击一行时发出了一个事件,该事件被 main
上的 onRecuiterDetail
事件处理函数处理:
onRecruiterDetail: function(recruiter) { this.detail = recruiter; }
onRecuiterDetail
方法在 recruiter-detail
中设置表单的内容,它还会显示视图(通过 <div>
上的 v-show
指令)。
现在如果您单击一行,就会获得招聘公司的详细信息:
请注意,在将 detail 组件包含在 HTML 中时,您将 editable
字段绑定到了 main Vue
上的 user
对象上。因此,仅在用户登录后,编辑功能才能生效。如果您已经登录,就会看到详细信息部分看到一些按钮。请注意,无论是登录后打开详细信息,还是详细信息已经可见,然后您再登录,这里的所有功能都是相同的:
Save 和 Delete 按钮发出的事件都由 main Vue
处理 — 所有业务逻辑都不会在 detail 组件中发生。这些事件包括 onRecruiterSave
和 onRecruiterDelete
。它们都使用 vue-resource
支持向 API 发出恰当的 REST 调用,使用所提供的 recruiter
实例来填充数据。因为 detail 组件中的 recruiter
实例是从表格数组中获得的,所以表格会立即发生更改,以反映用户对细节表单中的值的更改。
您需要做的最后一项操作是,工具栏上需要一个创建招聘公司的按钮:
<li class="active" v-if="user"> <a href="#" v-on:click="createRecruiter">New Recruiter</a> </li>
同样地,仅在用户登录时才能启用该特性。 createRecruiter
函数很简单:
createRecruiter: function() { this.detail = {}; }
通过在 detail
字段上设置一个空对象, createRecruiter
可以显示招聘公司细节窗格。如果 id
字段已存在,该表格会被当作更新操作还处理,否则会被当作创建操作来处理。
现在,您已经拥有了完整的 CRUD 支持,包括一个可排序的表格以及基本的身份验证和授权。
您可以使用 Vue.js 的核心特性来构建您能想到的任何东西,同时保持项目干净而且容易管理。Vue 的广泛功能具有极低的开销。这些品质造就了 Vue 受欢迎程度的快速增长,而且社区创建的功能在不断增加。您可以利用适合 Vue 的工具来最大限度地改进您的开发体验。在本系列的第 2 部分 中,您会看到使用 IBM Bluemix 将 recruiter-ranking 应用程序部署到云中的两种方法。
描述 | 名字 | 大小 |
---|---|---|
Recruiteranking 前端代码 | recruiteranking.zip | 75KB |
独立的后端 WAR | rr-backend-war.zip | 321KB |