使用 Yeoman 和 Polymer 构建 Web 应用

使用现代工具为 Web 应用搭建框架

Addy Osmani
Addy Osmani

简介

Allo’ Allo’。任何编写 Web 应用的人都知道,保持高效工作非常重要。如果您必须担心繁琐的任务(例如查找合适的样板、设置开发和测试工作流,以及缩减和压缩所有源代码),这将是一项挑战。

幸运的是,现代前端工具可以帮助自动完成其中的大部分工作,让您可以专注于编写出色的应用。本文将向您介绍如何使用 Yeoman,这是一个 Web 应用工具工作流,可帮助您使用 Polymer 简化应用创建流程,Polymer 是一个用于使用 Web 组件开发应用的 polyfill 和 sugar 库。

Yeoman

认识 Yo、Grunt 和 Bower

Yeoman 是一个戴着帽子的男人,他提供了三种有助于提高工作效率的工具:

  • yo 是一个框架搭建工具,提供框架专用框架生态系统(称为生成器),可用于执行我之前提到的一些繁琐任务。
  • Grunt 可用于构建、预览和测试项目,这得益于 Yeoman 团队精心挑选的任务和 grunt-contrib
  • bower 用于依赖项管理,这样您就不必再手动下载和管理脚本。

只需一两个命令,Yeoman 即可为您的应用(或模型等各个部分)编写样板代码、编译 Sass、缩减和串联 CSS、JS、HTML 和图片,并在当前目录中启动一个简单的 Web 服务器。它还可以运行单元测试等。

您可以通过 Node 打包模块 (npm) 安装生成器,目前有超过 220 个生成器可供使用,其中许多都是由开源社区编写的。热门生成器包括 generator-angulargenerator-backbonegenerator-ember

Yeoman 首页

安装最新版本的 Node.js 后,前往最近的终端并运行以下命令:

$ npm install -g yo

大功告成!现在,您已经安装了 Yo、Grunt 和 Bower,并且可以直接从命令行运行它们。以下是运行 yo 的输出:

Yeoman 安装

Polymer 生成器

如前所述,Polymer 是一个包含 polyfill 和 sugar 的库,可让您在现代浏览器中使用 Web 组件。该项目让开发者能够使用未来的平台构建应用,并告知 W3C 可以进一步改进哪些正在传输的规范。

Polymer 生成器首页

generator-polymer 是一个新的生成器,可帮助您使用 Yeoman 构建 Polymer 应用的框架,让您能够通过命令行轻松创建和自定义 Polymer(自定义)元素,并使用 HTML 导入功能导入这些元素。这样可以为您编写样板代码,从而节省时间。

接下来,运行以下命令安装 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 元素和我们的新生成器构建一个简单的博客。

Polymer 应用

首先,前往终端,创建一个新目录,然后使用 mkdir my-new-project && cd $_ 进入该目录。现在,您可以通过运行以下命令来启动 Polymer 应用:

$ yo polymer
Polymer 应用构建

这会从 Bower 获取最新版本的 Polymer,并为您的工作流架构出 index.html、目录结构和 Grunt 任务。不妨在等待应用准备就绪时喝杯咖啡。

好的,接下来我们可以运行 grunt server 来预览应用的外观:

Grunt 服务器

该服务器支持 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>

其中包含:

使用真实的数据源

我们的博客需要一个用于撰写和阅读新博文的位置。为了演示如何使用真实的数据服务,我们将使用 Google Apps 表格 API。这样,我们就可以轻松读取使用 Google 文档创建的任何电子表格的内容。

我们来完成设置:

  1. 在浏览器(建议使用 Chrome)中打开 Google 文档电子表格。其中包含以下字段下的示例帖子数据:

    • ID
    • 标题
    • 作者
    • 内容
    • 日期
    • 关键字
    • 电子邮件地址(作者)
    • 标记(用于帖子的标记网址)
  2. 前往文件菜单,然后选择复制以创建自己的电子表格副本。您可以随时自由修改内容,添加或移除帖子。

  3. 再次前往文件菜单,然后选择发布到网络

  4. 点击开始发布

  5. 获取指向已发布数据的链接下,从最后一个文本框中复制所提供网址的部分。示例如下:https://docs.google.com/spreadsheet/ccc?key=0AhcraNy3sgspdDhuQ2pvN21JVW9NeVA0M1h4eGo3RGc#gid=0

  6. 密钥粘贴到以下网址中 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

  7. 您可以将该网址粘贴到浏览器中并访问该网址,以查看博文内容的 JSON 版本。记下该网址,然后花些时间查看此数据的格式,因为您需要对其进行迭代,以便稍后在屏幕上显示。

浏览器中的 JSON 输出可能看起来有点令人生畏,但别担心!我们只对您的帖子数据感兴趣。

Google 表格 API 会使用特殊前缀 post.gsx$ 输出博客电子表格中的每个字段。例如:post.gsx$title.$tpost.gsx$author.$tpost.gsx$content.$t 等。在迭代 JSON 输出中的每个“行”时,我们将引用这些字段,以返回每篇帖子的相关值。

现在,您可以修改新建的帖子元素,将部分标记绑定到电子表格中的数据。为此,我们引入了属性 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

Bower 依赖项

创建该实用程序后,您需要在 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>

完成以上设置后,我们现在可以添加模板,以便在读入电子表格后对其进行迭代。第一个会输出目录,其中包含指向相应帖子泥巴标签的链接标题。

<!-- 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 属性会为我们帖子的数组集合中的每个元素(如果提供)创建并维护一个具有 [[ 绑定 ]] 的实例。

Polymer 应用

现在,为了填充当前的 [[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>
Polymer 应用

哇!现在,我们有一个简单的博客,它会从 JSON 读取数据,并使用 Yeoman 搭建的两个 Polymer 元素。

使用第三方元素

围绕 Web 组件的元素生态系统最近在不断发展,customelements.io 等组件库网站也开始出现。浏览社区创建的元素后,我发现了一个用于提取 gravatar 个人资料的元素,我们实际上也可以提取该元素并将其添加到我们的博客网站中。

“自定义元素”首页

将 Gravatar 元素来源复制到 /elements 目录,通过 HTML 导入在 post.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>

我们来看看这会带来什么好处:

包含自定义元素的 Polymer 应用

太棒了!

在相对较短的时间内,我们就创建了一个由多个 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,系统应该会构建一个可用于正式版的应用版本,供您发布。我们来试试。

Grunt build

成功!

如果您遇到困难,可以查看 https://github.com/addyosmani/polymer-blog,了解 polymer-blog 的预构建版本。

我们还有什么?

Web 组件仍处于不断演变的状态,因此围绕它们的工具也是如此。

我们目前正在研究如何通过 Vulcanize(Polymer 项目的工具)等项目串联 HTML 导入内容以提升加载性能,以及组件生态系统如何与 Bower 等软件包管理器搭配使用。

一旦我们有更好的答案来解答这些问题,便会立即通知您,但未来还有许多令人兴奋的事情。

使用 Bower 安装 Polymer 独立版

如果您希望以更轻量的方式开始使用 Polymer,可以直接从 Bower 中单独安装它,只需运行以下命令即可:

bower install polymer

这会将其添加到 bower_components 目录中。然后,您可以在应用索引中手动引用它,并在未来大放异彩。

您有何看法?

现在,您已经了解了如何使用 Web 组件和 Yeoman 为 Polymer 应用搭建框架。如果您对生成器有任何反馈,请在评论中告诉我们,或提交 bug 或在 Yeoman 问题跟踪器中发布帖子。我们非常乐意了解您希望生成器在哪些方面做得更好,因为只有通过您的使用和反馈,我们才能不断改进 :)