使用现代工具搭建 Web 应用
简介
大家好!编写 Web 应用的人都知道,保持高效工作有多重要。如果您必须担心一些繁琐的任务,例如寻找合适的样板、设置开发和测试工作流以及缩减和压缩所有源代码,那么这就是一个挑战。
幸运的是,现代前端工具可以帮助您自动完成大部分工作,让您专注于编写出色的应用。本文将向您展示如何使用 Yeoman(一种用于 Web 应用的工具工作流),通过 Polymer(一个用于使用 Web 组件开发应用的 polyfill 和 sugar 库)简化应用创建过程。
认识 Yo、Grunt 和 Bower
Yeoman 是一个戴着帽子的男人,他有三种工具可以提高您的工作效率:
- yo 是一种脚手架工具,提供特定于框架的脚手架生态系统(称为生成器),可用于执行我之前提到的一些繁琐任务。
- grunt 用于构建、预览和测试项目,这得益于 Yeoman 团队和 grunt-contrib 精心设计的任务。
- bower 用于依赖项管理,因此您不再需要手动下载和管理脚本。
只需一两个命令,Yeoman 就能为您的应用(或模型等单个部分)编写样板代码、编译 Sass、缩减和串联 CSS、JS、HTML 和图片,并在当前目录中启动一个简单的 Web 服务器。它还可以运行单元测试等。
您可以从 Node Packaged Modules (npm) 安装生成器,目前有超过 220 个生成器现已上架,其中许多生成器是由开源社区编写的。热门生成器包括 generator-angular、generator-backbone 和 generator-ember。
安装最新版本的 Node.js 后,打开终端并运行以下命令:
$ npm install -g yo
大功告成!现在,您已安装 Yo、Grunt 和 Bower,可以直接从命令行运行它们。以下是运行 yo 的输出:
Polymer 生成器
如前所述,Polymer 是一个 polyfill 和语法糖库,可让您在现代浏览器中使用 Web 组件。该项目允许开发者使用未来的平台构建应用,并向 W3C 告知可以进一步改进正在制定的规范的地方。
generator-polymer 是一个新的生成器,可帮助您使用 Yeoman 搭建 Polymer 应用,让您可以通过命令行轻松创建和自定义 Polymer(自定义)元素,并使用 HTML 导入功能导入这些元素。这可以为您编写样板代码,从而节省您的时间。
接下来,运行以下命令来安装 Polymer 的生成器:
$ npm install generator-polymer -g
就是这么简单。现在,您的应用已具备 Web 组件的超能力!
我们新安装的发电机有几个特定的部分,您可以访问:
polymer:element用于搭建新的单个 Polymer 元素。例如yo polymer:element carouselpolymer:app用于搭建初始 index.html、包含项目构建时配置的 Gruntfile.js 以及 Grunt 任务和为项目推荐的文件夹结构。它还提供了使用 Sass Bootstrap 为项目设置样式的选项。
让我们构建一个 Polymer 应用
我们将使用一些自定义 Polymer 元素和我们的新生成器构建一个简单的博客。
首先,前往终端,创建一个新目录,然后使用 mkdir my-new-project && cd $_ 进入该目录。现在,您可以通过运行以下命令来启动 Polymer 应用:
$ yo polymer
这将从 Bower 获取最新版本的 Polymer,并为您的工作流搭建 index.html、目录结构和 Grunt 任务。在等待应用完成准备工作期间,何不喝杯咖啡?
接下来,我们可以运行 grunt server 来预览应用的效果:
该服务器支持 LiveReload,这意味着您可以启动文本编辑器,修改自定义元素,浏览器会在保存时重新加载。这样,您就可以实时查看应用的当前状态。
接下来,我们创建一个新的 Polymer 元素来表示博文。
$ yo polymer:element post
Yeoman 会向我们提出一些问题,例如我们是否要添加构造函数,或者是否要使用 HTML 导入在 index.html 中添加 post 元素。我们先对前两个选项选择“否”,并将第三个选项留空。
$ 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 Spreadsheets API。这样,我们就可以轻松读取使用 Google 文档创建的任何电子表格的内容。
让我们来完成设置:
在浏览器(建议使用 Chrome)中打开此 Google 文档电子表格。它包含以下字段下的示例帖子数据:
- ID
- 标题
- 作者
- 内容
- 日期
- 关键字
- 电子邮件(作者)
- Slug(用于帖子的 Slug 网址)
前往文件菜单,然后选择制作副本,创建您自己的电子表格副本。您可以随时编辑内容,添加或移除帖子。
再次前往文件菜单,然后选择发布到网络。
点击开始发布
在获取已发布数据的链接下,从最后一个文本框中复制所提供网址的密钥部分。它看起来像这样:https://docs.google.com/spreadsheet/ccc?key=0AhcraNy3sgspdDhuQ2pvN21JVW9NeVA0M1h4eGo3RGc#gid=0
将密钥粘贴到以下网址中 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 Spreadsheets API 会输出博客电子表格中的每个字段,并带有特殊前缀 post.gsx$。例如:post.gsx$title.$t、post.gsx$author.$t、post.gsx$content.$t 等。在遍历 JSON 输出中的每一“行”时,我们将引用这些字段来获取每篇帖子的相关值。
现在,您可以修改新搭建的帖子元素,将部分标记绑定到电子表格中的数据。为此,我们引入了一个属性 post,它将读取我们之前创建的帖子标题、作者、内容和其他字段。selected 属性(我们稍后会填充该属性)用于仅在用户导航到正确的帖子别名时显示帖子。
<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>
我们来看看这会带来什么:
太棒了!
在相对较短的时间内,我们创建了一个由多个 Web 组件组成的简单应用,而无需担心编写样板代码、手动下载依赖项或设置本地服务器或构建工作流。
优化应用
Yeoman 工作流包含另一个名为 Grunt 的开源项目,这是一个任务运行器,可以运行许多特定于 build 的任务(在 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,系统应该会构建一个可用于生产的应用版本,供您发布。我们来试试。
成功!
如果您遇到困难,可以查看预构建版本的 polymer-blog,网址为 https://github.com/addyosmani/polymer-blog。
我们还有哪些精彩内容?
Web 组件仍处于不断演进的状态,其相关工具也是如此。
我们目前正在研究如何通过 Vulcanize(Polymer 项目中的一个工具)等项目来串联 HTML 导入,以提高加载性能,以及组件生态系统如何与 Bower 等软件包管理器配合使用。
当我们对这些问题有更好的答案时,会及时通知您,但未来还有很多激动人心的时刻。
使用 Bower 安装 Polymer 独立版
如果您希望以更轻量的方式开始使用 Polymer,可以直接通过 Bower 运行以下命令来单独安装它:
bower install polymer
这会将其添加到您的 bower_components 目录中。然后,您可以在应用索引中手动引用它,并开启未来。
您有何看法?
现在,您已经知道如何使用 Yeoman 搭建一个使用 Web 组件的 Polymer 应用。如果您对生成器有任何反馈,请在评论中告诉我们,或者提交 bug 或在 Yeoman 问题跟踪器中发帖。我们很想知道您是否希望生成器在其他方面做得更好,因为只有通过您的使用和反馈,我们才能不断改进 :)