使用现代工具为 Web 应用构建基架
简介
Allo 的 Allo。任何编写 Web 应用的用户都可以知道保持高效工作有多么重要。当您必须要费心去寻找合适的样板、设置开发和测试工作流以及缩减和压缩所有源代码等繁琐任务时,这并非易事。
幸运的是,现代化的前端工具可以帮助实现大部分的自动化,让你可以专注于编写快速应用。本文将介绍如何使用 Yeoman 工作流,让 Web 应用能够简化使用 Polymer 创建应用的工作流。Polymer 是一个 polyfill 库,也可用于开发 Web 组件的应用。
悠悠、古朗和鲍尔登场
Yeoman 是个头戴帽子的人,他有三种能够提高工作效率的工具:
- “yo”是一种基架工具,可提供针对特定框架的基架的生态系统,这些基架称为生成器,可用于执行我前面提到的一些繁琐任务。
- grunt 用于构建、预览和测试您的项目,这要归功于 Yeoman 团队和 grunt-contrib 所挑选的任务的帮助。
- bower 用于依赖项管理,这样您便不再需要手动下载和管理脚本。
只需一两个命令,Yeoman 就可以为您的应用(或模型等单个部分)编写样板代码,编译您的 Sass,最小化和串联您的 CSS、JS、HTML 和图片,并在当前目录中启动一个简单的网络服务器。它还可以运行单元测试等。
您可以通过节点打包模块 (npm) 安装生成器。目前,220 多种生成器可供使用,其中许多生成器是由开源社区编写的。热门生成器包括 generator-Angular、generator-backbone 和 generator-ember。
安装最新版本的 Node.js 后,转到最近的终端并运行以下命令:
$ npm install -g yo
大功告成!现在,您已经有了 Yo、Grunt 和 Bower,并且可以直接从命令行运行它们。以下是运行 yo
的输出:
聚合物生成器
如前所述,Polymer 是一个包含 polyfill 和 sugar 的库,支持在现代浏览器中使用 Web 组件。通过该项目,开发者可以使用未来的平台来构建应用,并告知 W3C 哪些地方可以进一步改进动态规范。
generator-polymer 是一种新的生成器,可帮助您使用 Yeoman 构建 Polymer 应用的基架,从而让您通过命令行轻松创建和自定义 Polymer(自定义)元素,并使用 HTML Imports 导入这些元素。这样可以为您编写样板代码,从而节省您的时间。
接下来,运行以下命令来安装 Polymer 的生成器:
$ npm install generator-polymer -g
大功告成。现在,您的应用具备了 Web 组件的强大功能!
新安装的生成器包含一些您将可以访问的特定代码段:
polymer:element
用于为各个新的 Polymer 元素搭建基础。例如yo polymer:element carousel
polymer:app
用于构建您的初始 index.html,这是一个 Gruntfile.js,其中包含项目的构建时配置、Grunt 任务和为项目推荐的文件夹结构。它还能让您选择将 Sass Bootstrap 用于项目的样式。
我们构建一个 Polymer 应用
我们将使用一些自定义 Polymer 元素和我们的新生成器来构建一个简单的博客。
首先,请转到终端,创建一个新目录,然后使用 mkdir my-new-project && cd $_
通过 cd 命令进入该目录。现在,您可以通过运行以下命令启动 Polymer 应用:
$ yo polymer
它从 Bower 获取了最新版本的 Polymer,并为您的工作流构建了 index.html、目录结构和 Grunt 任务。我们一边等待应用完成准备,一边喝杯咖啡。
好的,接下来我们可以运行 grunt server
来预览应用:
服务器支持 LiveRefresh,这意味着您可以启动文本编辑器、编辑自定义元素,然后浏览器将在保存时重新加载。这提供了一个很好的实时视图,让您可以清楚了解应用的当前状态。
接下来,让我们创建一个新的 Polymer 元素来代表博文。
$ yo polymer:element post
Yeoman 提出了几个问题,例如我们是要添加构造函数,还是要使用 HTML 导入功能将 post 元素包含在 index.html
中。我们暂且对前两个选项说“否”,并将第三个选项留空。
$ yo polymer:element post
[?] Would you like to include constructor=''? No
[?] Import to your index.html using HTML imports? No
[?] Import other elements into this one? (e.g 'another_element.html' or leave blank)
create app/elements/post.html
这将在 /elements
目录中创建一个名为 post.html 的新 Polymer 元素:
<polymer-element name="post-element" attributes="">
<template>
<style>
@host { :scope {display: block;} }
</style>
<span>I'm <b>post-element</b>. This is my Shadow DOM.</span>
</template>
<script>
Polymer('post-element', {
//applyAuthorStyles: true,
//resetStyleInheritance: true,
created: function() { },
enteredView: function() { },
leftView: function() { },
attributeChanged: function(attrName, oldVal, newVal) { }
});
</script>
</polymer-element>
其中包含:
- 自定义元素的样板代码,允许您在网页中使用自定义 DOM 元素类型(例如
<post-element>
) - “原生”客户端模板的模板标记,以及用于封装元素样式的限定了范围的样式示例
- 元素注册样板和生命周期事件。
使用真实的数据源
我们的博客需要一个地方来撰写和阅读新帖子。为了演示如何使用真实的数据服务,我们将使用 Google Apps 电子表格 API。这使我们能够轻松地阅读使用 Google 文档创建的任何电子表格的内容。
开始设置:
在浏览器中(建议使用 Chrome)打开此 Google 文档电子表格(对于这些步骤,建议使用 Chrome)。它在以下字段下包含示例帖子数据:
- ID
- 标题
- 作家
- 内容
- 日期
- 关键字
- (作者的)电子邮件地址
- Slug(用于帖子的 Slug 网址)
转到文件菜单,然后选择复制即可创建您自己的电子表格副本。您可以在闲暇时修改内容,添加或移除帖子。
再次转到文件菜单,然后选择发布到网络。
点击开始发布
在“获取已发布数据的链接”下,从最后一个文本框中复制所提供网址的“键”部分。示例如下:https://docs.google.com/spreadsheet/ccc?key=0AhcraNy3sgspdDhuQ2pvN21JVW9NeVA0M1h4eGo3RGc#gid=0
将 key 粘贴到 your-key-goes-here 所在的网址中:https://spreadsheets.google.com/feeds/list/your-key-goes-here/od6/public/values?alt=json-in-script&callback=。使用上述键的示例如下:https://spreadsheets.google.com/feeds/list/0AhcraNy3sgspdDhuQ2pvN21JVW9NeVA0M1h4eGo3RGc/od6/public/values?alt=json-in-script
您可以将该网址粘贴到浏览器中,然后导航到该网址以查看博客内容的 JSON 版本。请记下网址,然后花点时间查看一下这些数据的格式,因为您需要反复检查该网址,以便稍后将其显示在屏幕上。
浏览器中的 JSON 输出可能有点令人望而生畏,但不必担心!我们只关注您的帖子的数据。
Google 电子表格 API 会使用特殊前缀 post.gsx$
输出您博客电子表格中的每个字段。例如:post.gsx$title.$t
、post.gsx$author.$t
、post.gsx$content.$t
等。当我们迭代 JSON 输出中的每一“行”时,我们将引用这些字段以获取每篇博文的相关值。
现在,您可以修改新基架的 post 元素,以将标记的某些部分与电子表格中的数据绑定bind。为此,我们引入了 post
属性,用于读取帖子标题、作者、内容以及我们之前创建的其他字段。selected
属性(稍后将填充)用于仅在用户导航到帖子的正确 Slug 时显示帖子。
<polymer-element name="post-element" attributes="post selected">
<template>
<style>
@host { :scope {display: block;} }
</style>
<div class="col-lg-4">
<template if="[[post.gsx$slug.$t === selected]]">
<h2>
<a href="#[[post.gsx$slug.$t]]">
[[post.gsx$title.$t ]]
</a>
</h2>
<p>By [[post.gsx$author.$t]]</p>
<p>[[post.gsx$content.$t]]</p>
<p>Published on: [[post.gsx$date.$t]]</p>
<small>Keywords: [[post.gsx$keywords.$t]]</small>
</template>
</div>
</template>
<script>
Polymer('post-element', {
created: function() { },
enteredView: function() { },
leftView: function() { },
attributeChanged: function(attrName, oldVal, newVal) { }
});
</script>
</polymer-element>
接下来,运行 yo polymer:element blog
,创建一个同时包含博文集合和博客布局的博客元素。
$ yo polymer:element blog
[?] Would you like to include constructor=''? No
[?] Import to your index.html using HTML imports? Yes
[?] Import other elements into this one? (e.g 'another_element.html' or leave blank) post.html
create app/elements/blog.html
这次,我们使用 HTML 导入功能将博客导入 index.html,让它显示在页面中。具体而言,对于第三个提示,我们将 post.html
指定为想要包含的元素。
和之前一样,系统会创建新的元素文件 (blog.html) 并将其添加到 /elements 中,这次会导入 post.html,并在模板标记中添加 <post-element>
:
<link rel="import" href="post.html">
<polymer-element name="blog-element" attributes="">
<template>
<style>
@host { :scope {display: block;} }
</style>
<span>I'm <b>blog-element</b>. This is my Shadow DOM.</span>
<post-element></post-element>
</template>
<script>
Polymer('blog-element', {
//applyAuthorStyles: true,
//resetStyleInheritance: true,
created: function() { },
enteredView: function() { },
leftView: function() { },
attributeChanged: function(attrName, oldVal, newVal) { }
});
</script>
</polymer-element>
由于我们要求通过 HTML 导入(在其他 HTML 文档中包含和重复使用 HTML 文档的一种方式)将博客元素导入我们的索引,因此还可以验证该博客元素是否已正确添加到文档 <head>
中:
<!doctype html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title></title>
<meta name="description" content="">
<meta name="viewport" content="width=device-width">
<link rel="stylesheet" href="styles/main.css">
<!-- build:js scripts/vendor/modernizr.js -->
<script src="bower_components/modernizr/modernizr.js"></script>
<!-- endbuild -->
<!-- Place your HTML imports here -->
<link rel="import" href="elements/blog.html">
</head>
<body>
<div class="container">
<div class="hero-unit" style="width:90%">
<blog-element></blog-element>
</div>
</div>
<script>
document.addEventListener('WebComponentsReady', function() {
// Perform some behaviour
});
</script>
<!-- build:js scripts/vendor.js -->
<script src="bower_components/polymer/polymer.min.js"></script>
<!-- endbuild -->
</body>
</html>
太棒了
使用 Bower 添加依赖项
接下来,我们来修改元素,以使用 Polymer JSONP 实用程序元素在 posts.json 中读取内容。您可以通过以下方法获取适配器:通过 Git 克隆代码库,或通过运行 bower install polymer-elements
通过 Bower 安装 polymer-elements
。
获得该实用程序后,您需要将其导入您的 blog.html 元素中,其中包含:
<link rel="import" href="../bower_components/polymer-jsonp/polymer-jsonp.html">
接下来,添加其标记,并将 url
提供给之前的博文电子表格,并在末尾添加 &callback=
:
<polymer-jsonp auto url="https://spreadsheets.google.com/feeds/list/your-key-value/od6/public/values?alt=json-in-script&callback=" response="[[posts]]"></polymer-jsonp>
有了这项功能,我们现在可以添加模板,以便在读完电子表格后对电子表格进行迭代。第一个请求用于输出目录,并为帖子提供指向 Slug 的链接标题。
<!-- Table of contents -->
<ul>
<template repeat="[[post in posts.feed.entry]]">
<li><a href="#[[post.gsx$slug.$t]]">[[post.gsx$title.$t]]</a></li>
</template>
</ul>
第二个函数会为找到的每个条目渲染一个 post-element
实例,并相应地将博文内容传递给该实例。请注意,我们将传递一个表示单个电子表格行帖子内容的 post
属性,以及一个将使用路线填充的 selected
属性。
<!-- Post content -->
<template repeat="[[post in posts.feed.entry]]">
<post-element post="[[post]]" selected="[[route]]"></post-element>
</template>
您看到的模板中使用了 repeat
属性,当提供了 [[ bindings ]] 时,它会为帖子数组集合中的每个元素创建并维护一个实例。
现在,为了填充当前的 [[route]],我们将进行欺诈,使用一个名为 Flatiron Director 的库;每当网址哈希值发生变化时,该库都会绑定到 [[route]]。
幸运的是,我们可以抓取 Polymer 元素(more-elements 软件包的一部分)。复制到 /elements 目录后,我们可以使用 <flatiron-director route="[[route]]" autoHash></flatiron-director>
引用它,将 route
指定为要绑定的属性,并指示它自动读取任何哈希更改的值 (autoHash)。
综上,现在我们得到以下结果:
<link rel="import" href="post.html">
<link rel="import" href="polymer-jsonp/polymer-jsonp.html">
<link rel="import" href="flatiron-director/flatiron-director.html">
<polymer-element name="blog-element" attributes="">
<template>
<style>
@host { :scope {display: block;} }
</style>
<div class="row">
<h1><a href="/#">My Polymer Blog</a></h1>
<flatiron-director route="[[route]]" autoHash></flatiron-director>
<h2>Posts</h2>
<!-- Table of contents -->
<ul>
<template repeat="[[post in posts.feed.entry]]">
<li><a href="#[[post.gsx$slug.$t]]">[[post.gsx$title.$t]]</a></li>
</template>
</ul>
<!-- Post content -->
<template repeat="[[post in posts.feed.entry]]">
<post-element post="[[post]]" selected="[[route]]"></post-element>
</template>
</div>
<polymer-jsonp auto url="https://spreadsheets.google.com/feeds/list/0AhcraNy3sgspdHVQUGd2M2Q0MEZnRms3c3dDQWQ3V1E/od6/public/values?alt=json-in-script&callback=" response="[[posts]]"></polymer-jsonp>
</template>
<script>
Polymer('blog-element', {
created: function() {},
enteredView: function() { },
leftView: function() { },
attributeChanged: function(attrName, oldVal, newVal) { }
});
</script>
</polymer-element>
哇!现在,我们有了一个简单的博客,它从 JSON 读取数据并使用两个通过 Yeoman 构建的 Polymer 元素。
使用第三方元素
最近,围绕 Web 组件的元素生态系统不断发展,例如 customelements.io 等组件库网站开始出现。在浏览社区创建的元素后,我找到了一张用于获取 gravatar 配置文件的元素,我们实际上也可以抓取它并将其添加到我们的博客网站中。
将 gravatar 元素源复制到 /elements
目录,通过 post.html 中的 HTML 导入功能将其包含在内,然后将
<link rel="import" href="gravatar-element/src/gravatar.html">
<polymer-element name="post-element" attributes="post selected">
<template>
<style>
@host { :scope {display: block;} }
</style>
<div class="col-lg-4">
<template if="[[post.gsx$slug.$t === selected]]">
<h2><a href="#[[post.gsx$slug.$t]]">[[post.gsx$title.$t]]</a></h2>
<p>By [[post.gsx$author.$t]]</p>
<gravatar-element username="[[post.gsx$email.$t]]" size="100"></gravatar-element>
<p>[[post.gsx$content.$t]]</p>
<p>[[post.gsx$date.$t]]</p>
<small>Keywords: [[post.gsx$keywords.$t]]</small>
</template>
</div>
</template>
<script>
Polymer('post-element', {
created: function() { },
enteredView: function() { },
leftView: function() { },
attributeChanged: function(attrName, oldVal, newVal) { }
});
</script>
</polymer-element>
让我们来看看这会给我们带来以下便利:
太美了!
在相对较短的时间内,我们构建了由多个网络组件组成的简单应用,而无需费心编写样板代码、手动下载依赖项、设置本地服务器或构建工作流。
优化应用
Yeoman 工作流包括另一个名为 Grunt 的开源项目,这是一个任务运行程序,可以运行许多特定于构建的任务(在 Gruntfile 中定义)来生成经过优化的应用版本。如果单独运行 grunt
,则会执行生成器为执行 lint 请求、测试和构建而设置的 default
任务:
grunt.registerTask('default', [
'jshint',
'test',
'build'
]);
上面的 jshint
任务将检查您的 .jshintrc
文件以了解您的偏好设置,然后对项目中的所有 JavaScript 文件运行该任务。如需使用 JSHint 的完整选项,请参阅相关文档。
test
任务大致如下所示,它可为我们建议开箱即用的测试框架 Mocha 创建和提供应用。此外,它还会为您执行测试:
grunt.registerTask('test', [
'clean:server',
'createDefaultTemplate',
'jst',
'compass',
'connect:test',
'mocha'
]);
因为在本例中我们的应用相当简单,所以我们把编写测试作为单独的练习留给您。构建流程还需要处理一些其他操作,因此我们来看看 Gruntfile.js
中定义的 grunt build
任务将做什么:
grunt.registerTask('build', [
'clean:dist', // Clears out your .tmp/ and dist/ folders
'compass:dist', // Compiles your Sassiness
'useminPrepare', // Looks for <!-- special blocks --> in your HTML
'imagemin', // Optimizes your images!
'htmlmin', // Minifies your HTML files
'concat', // Task used to concatenate your JS and CSS
'cssmin', // Minifies your CSS files
'uglify', // Task used to minify your JS
'copy', // Copies files from .tmp/ and app/ into dist/
'usemin' // Updates the references in your HTML with the new files
]);
运行 grunt build
,应用的正式版应已构建完毕,随时可供您发布。我们来试试看。
成功!
如果遇到问题,您可以访问 https://github.com/addyosmani/polymer-blog,查看预构建版本的 polymer-blog。
我们店内还有什么呢?
Web 组件仍处于发展状态,因此围绕它们的工具也处于演变状态。
目前,我们正在研究如何才能通过 Vulcanize(Polymer 项目的一款工具)等项目来串联其 HTML 导入文件以提高加载性能,以及组件生态系统如何与 Bower 这样的软件包管理器协同工作。
对于这些问题,如果有更好的答案,我们会立即通知您,但未来还有很多激动人心的时刻即将开始。
通过 Bower 独立安装 Polymer
如果您希望使用更轻的 Polymer,可以直接从 Bower 独立安装,方法是运行以下命令:
bower install polymer
这会将其添加到您的 bower_components 目录中。然后,您可以在应用索引中手动引用它,为将来做好准备。
您有何看法?
现在,您已经了解了如何通过 Yeoman 使用 Web Components 构建 Polymer 应用。如果您有关于生成器的反馈,请通过评论告诉我们,或者提交 bug 或发布到 Yeoman 问题跟踪器中。如果您有任何其他问题,希望该生成器能更好地满足您的需求,欢迎与我们联系,因为只有您的使用和反馈需要我们不断改进 :)