/issuetracker

Primary LanguageJavaScript

使用Iron工具快速搭建Meteor程序

采用Meteor框架搭建现代的web应用很快很方便,不过如果你使用iron scaffolding脚手架工具和autoforms工具包的话,这个过程会更加快捷,这个例子就展示了如何一步一步地新建一个程序。

##1. 安装Meteor和Iron工具

使用如下两个指令分别安装meteor和iron

$curl http://install.meteor.com/ |sh
$npm install -g iron-meteor

##2. 新建程序

使用如下指令创建一个叫做issuetracker的程序

$iron create issuetracker

这个脚手架工具将创建一个新的issuetracker目录,这时可以进入如图1所示目录看一下目录结构,meteor程序在app子目录里。

图1 新建程序的目录结构

##3. 安装新的package

可以通过下述指令添加一些常规的meteor package,我们将在项目中使用

$iron add twbs:bootstrap
$iron add aldeed:collection2
$iron add aldeed:autoform
$iron add aldeed:delete-button
$iron add momentjs:moment
$iron add accounts-password
$iron add ian:accounts-ui-bootstrap-3
$iron add natestrauser:font-awesome

然后再通过下属指令删除2个不使用的package

$iron remove autopublish
$iron remove insecure

这是如果打开app/.meteor/packages文件的话,应该可以看到如下package

meteor-platform
iron:router
twbs:bootstrap
aldeed:collection2
aldeed:autoform
aldeed:delete-button
momentjs:moment
accounts-password
ian:accounts-ui-bootstrap-3
natestrauser:font-awesome

##4. 添加数据模型

本项目管理issue的跟踪,只涉及到一个数据模型issue。在meteor中,数据被表述称collections,下述iron指令可以用于添加一个名叫issue的collection

$iron g:collection issues

这条指令实际上生成一个文件

app/lib/collections/issues.js

打开这个文件,修改成如下内容

Issues = new Mongo.Collection('issues');

Issues.attachSchema(new SimpleSchema({
  title:{
    type: String,
    label: "Title",
    max: 100
  },
  description:{
    type: String,
    label: "Description",
    max: 1024
  },
  dueDate:
  {
    type: Date,
    label: "Due Date",
    optional: true
  },
  priority:
  {
    type: String,
    label: "Priority",
    allowedValues: ['High', 'Medium', 'Low'],
    optional: true
  },
  createdBy: {
    type: String,
    autoValue: function() {
       return this.userId
    }
  }
}));

if (Meteor.isServer) {
  Issues.allow({
    insert: function (userId, doc) {
      return userId;
    },

    update: function (userId, doc, fieldNames, modifier) {
      return userId;
    },

    remove: function (userId, doc) {
      return userId;
    }
  });
}

在上述代码中,首先创建了一个Mongo的collection,并将他的引用保存在Issues里。然后,一个collection的schema被attach到这个对象上,用来描述数据库结构。 在这里使用一个SimpleSchema对象,这个对象包含4个字段title, description, dueDate and priority。 对于每一个字段,都定义了一个type和label,最后title和description都定义了max字段来限制字符串的长度。 而priority字段则设置了几个allowedValues作为其值域。

另外,在publish.js文件里面添加如下代码,以便客户端访问特定的数据集。

Meteor.publish('issues', function (userId) {
  return Issues.find({createdBy: userId});
});

这里的publish函数接受一个参数:userId。 这个参数用以限制被发布的对象仅限于创建它的user。

##5. 初始化用户界面

使用bootstrap来初始化用户界面,用户界面整体放在MasterLayout这个模板里,他的代码在如下文件里

app/client/templates/layouts/master_layout/master_layout.html

其代码如下

<template name="MasterLayout">
  <nav class="navbar navbar-inverse navbar-fixed-top" role="navigation">
    <div class="container">
      <div class="navbar-header">
          <button type="button" class="navbar-toggle" data-toggle="collapse" data-target="#navigationbar">
            <span class="sr-only">Toggle navigation</span>
            <span class="icon-bar"></span>
            <span class="icon-bar"></span>
            <span class="icon-bar"></span>
          </button>
          <a class="navbar-brand" href="/"><i class="fa fa-bug"></i> IssueTracker</a>
      </div>
      <div class="collapse navbar-collapse" id="navigationbar">
        <ul class="nav navbar-nav navbar-right">
            <li>{{#linkTo route="home"}}<i class="fa fa-home"></i> Home{{/linkTo}}</li>
            {{#if currentUser}}
              <li>{{#linkTo route="issuesList"}}<i class="fa fa-align-justify"></i> Issues{{/linkTo}}</li>
              <li>{{#linkTo route="insertIssue"}}<i class="fa fa-plus"></i> New Issue{{/linkTo}}</li>
            {{/if}}
            <li><div class="nav navbar-nav navbar-right"> {{> loginButtons}}</div></li>
        </ul>
      </div>
    </div>
  </nav>
  <div class="container">
    {{> yield}}
  </div>
  <nav class="navbar navbar-default navbar-fixed-bottom">
    <div class="container">
      <ul class="nav navbar-nav navbar-right">
          <li>Built with Meteor</li>
      </ul>
    </div>
  </nav>
</template>

注意,也需要修改一下如下css文件

app/client/stylesheets/main.css

其代码可以修改为

.main {
}

body { padding-top: 70px; padding-bottom: 70px;}

然后运行程序,可以看到如图2所示效果

图2 初始化程序界面

##6. 创建Controller和Route

执行如下指令,创建一个controller

$iron g:controller Issues

生成文件

app/lib/controllers/issues_controller.js

再执行

$iron g:route 'insert_issue'
$iron g:route 'issues_list'
$iron g:route 'edit_issue'

效果如下

图3 通过iron指令生成route

然后删掉在app/lib/controllers下面多生成的3个文件

edit_issue_controller.js
insert_issue_controller.js
issues_list_controller.js

最后修改app/lib/controllers/issues_controller.js如下

IssuesController = RouteController.extend({
  subscriptions: function () {
    this.subscribe('issues', Meteor.userId());
  },

  data: function () {
    return Issues.findOne({_id: this.params._id});
  },

  insert: function () {
    this.render('InsertIssue', {});
  },

  list: function() {
    this.render('IssuesList', {});
  },

  edit: function() {
    this.render('EditIssue', {});
  }
});

最后我们修改routes.js如下

Router.configure({
  layoutTemplate: 'MasterLayout',
  loadingTemplate: 'Loading',
  notFoundTemplate: 'NotFound'
});

Router.route('/', {
  name: 'home',
  controller: 'HomeController',
  action: 'action',
  where: 'client'
});

Router.route('/insert_issue', {
  name: 'insertIssue',
  controller: 'IssuesController',
  action: 'insert',
  where: 'client'
});


Router.route('issues_list', {
  name: 'issuesList',
  controller: 'IssuesController',
  action: 'list',
  where: 'client'
});

Router.route('/issue/:_id', {
  name: 'editIssue',
  controller: 'IssuesController',
  action: 'edit',
  where: 'client'
});

这时访问http://localhost:3000/issues_list会有如下显示

图4 访问issues list网页

##7. 对routes进行安全控制

在routes.js加入以下代码以防止非授权的针对InsertIssue和IssuesList的访问

Router.onBeforeAction(function() {
  if (!Meteor.user()) {
    this.render('AccessDenied');
  } else
  {
     this.next();
  }
}, {only: ['issuesList', 'insertIssue']});

然后,我们用如下template指令AccessDenied生成页面

$iron g:template AccessDenied

其中,access_denied.html的内容修改如下

<template name="AccessDenied">
  <div class="alert alert-danger" role="alert">
    <h2>Access Denied</h2>
    <hr>
    <h4>Please log in to access this page!</h4>
  </div>
</template>

这时候在未登录时访问http://localhost:3000/issues_list会有如下显示

图5 访问issues list网页显示Access Denided

##8. 实现IssuesController

IssuesController在issues_controller.js里实现。首先,我们在subscriptions函数里通过subscribe使用2个参数来从服务器端订阅issues,第一个参数是我们需要订阅的collection的名字'issues',第二个参数是用户的id,这样限制只能获取该用户的issue。 然后,data属性用来获取特定id的数据项,在这里我们使用了findOne函数。 最后3个函数insert, list and edit则分别用来渲染不同的模板。该类代码如下

IssuesController = RouteController.extend({
  subscriptions: function () {
    this.subscribe('issues', Meteor.userId());
  },

  data: function () {
    return Issues.findOne({_id: this.params._id});
  },

  insert: function () {
    this.render('InsertIssue', {});
  },

  list: function() {
    this.render('IssuesList', {});
  },

  edit: function() {
    this.render('EditIssue', {});
  }
});

##9. 插入新Issue

插入新Issue的页面代码在insert_issue.html文件里,其内容修改如下

<template name="InsertIssue">
  <h1>Create New Issue</h1>
  {{> quickForm collection="Issues" id="insertIssueForm" type="insert" omitFields="createdBy" buttonContent="Create"}}
</template>

这里我们使用autoform里面的quickForm生成页面,最终页面显示如下

图6 Insert Issue页面显示

##10. 显示Issue列表

我们将issues_list.html的代码修改如下,以实现显示issue列表的功能。

<template name="IssuesList">
  <h1>Issues List</h1>
  <table class="table table-hover">
        <thead>
          <tr>
            <th>Title</th>
            <th>Description</th>
            <th>Due Date</th>
            <th>Priority</th>
            <th></th>
          </tr>
        </thead>
        <tbody>
          {{#each issues}}
            <tr>
              <td>{{title}}</td>
              <td>{{description}}</td>
              <td>{{dueDateFormatted}}</td>
              <td>
                {{#if priorityHigh}}
                  <span class="label label-danger">{{priority}}</span>
                {{/if}}

                {{#if priorityMedium}}
                  <span class="label label-warning">{{priority}}</span>
                {{/if}}

                {{#if priorityLow}}
                  <span class="label label-success">{{priority}}</span>
                {{/if}}
              </td>
              <td>
                {{#linkTo route='editIssue'}}
                  <i class="fa fa-pencil-square-o"></i>
                {{/linkTo}}
              </td>
            </tr>
          {{/each}}

        </tbody>
      </table>
</template>

在输出中,issue的优先级可以控制bootstrap的显示样式,具体有3种类型:label-danger, label-warning and label-success。为了决定每种类型显示成什么样子,可以通过修改 issues_list.js代码控制。

Template.IssuesList.events({
});

Template.IssuesList.helpers({
  issues: function () {
    return Issues.find();
  },
  dueDateFormatted: function () {
    return moment(this.dueDate).format("MMM Do YY");
  },
  priorityHigh: function() {
    if (this.priority === 'High')
      return true;
    else
      return false;
  },
  priorityMedium: function() {
    if (this.priority === 'Medium')
      return true;
    else
      return false;
  },
  priorityLow: function() {
    if (this.priority === 'Low')
      return true;
    else
      return false;
  }
});

最终显示效果如下

图6 最终Issue List页面显示

##11. 修改和删除Issue

在IssueList的模板里,列表的最后一列是一个修改issue的链接,如下

<td>
 {{#linkTo route='editIssue'}}
   <i class="fa fa-pencil-square-o"></i>
 {{/linkTo}}
</td>

这里指向一个EditIssue的模板,它指向route'/issue/:_id',这个route包含_id 路由参数。为了输出一个可以编辑的form,我们在edit_issue.html中插入以下代码

<template name="EditIssue">
  <h1>Edit Issue</h1>
  {{> quickForm collection="Issues" doc=this id="editIssueForm" type="update" omitFields="createdBy" buttonContent="Update"}}
  <hr>
  {{> quickRemoveButton collection="Issues" _id=this._id beforeRemove=beforeRemove class="btn btn-danger"}}
</template>

这里同样使用了autoform的机制,最后使用了一个quickRemoveButton模板来删除一个issue。另外,在Edit Issue里面我们使用了beforeRemove模板来让用户确认是否删除。当删除完成以后再显示IssuesList模板。

Template.EditIssue.events({
});

Template.EditIssue.helpers({
  beforeRemove: function () {
      return function (collection, id) {
        var doc = collection.findOne(id);
        if (confirm('Really delete issue: "' + doc.title + '"?')) {
          this.remove();
          Router.go('issuesList');
        }
      };
    }
});

AutoForm.addHooks(null, {
  onSuccess: function(operation, result, template) {
    Router.go('issuesList');
  }
});

最终修改界面如下

图7 修改Issue界面

参考资料

  1. 本文参考如下文章编写,并修正了其中的一些错误 How to Build Web Apps Ultra Fast with METEOR + Iron Scaffolding and Automatic Form Generation
  2. 可运行demo参见网址:http://wmzhai-issuetracker.meteor.com