Meteor学习笔记之二——TODO example

按照官网的步骤一步一步做的,记录其中的一些重要的地方并进行解读来加深一下印象吧,我列出来的代码变动是不完全的,如果想复现效果请参照教程,绿色高亮的代码就是改动的部分


1. Creating an app

meteor create simple-todos

这个命令会创建一个名为simple-todos的文件夹,里面包括

client/main.js        # a JavaScript entry point loaded on the client
client/main.html      # an HTML file that defines view templates
client/main.css       # a CSS file to define your app's styles
server/main.js        # a JavaScript entry point loaded on the server
package.json          # a control file for installing NPM packages
package-lock.json     # Describes the NPM dependency tree
.meteor               # internal Meteor files
.gitignore            # a control file for git

要运行我们刚创建的项目也很简单

cd simple-todos
meteor

然后我们在http://localhost:3000就可以看到运行的app了


2. Component

这里用的是React,是个方便高效的UI设计库
首先在相同目录新开一个终端,安装一些npm依赖包

meteor npm install --save react react-dom

修改代码的部分就直接参照官网吧,这里要注意的一点是新建一个imports文件夹
imports文件夹外的文件在meteorserver启动时就会自动导入,而文件夹内的文件只在有import声明时导入

export default class App extends Component {
  getTasks() {
    return [
      { _id: 1, text: 'This is task 1' },
      { _id: 2, text: 'This is task 2' },
      { _id: 3, text: 'This is task 3' },
    ];
  }

  renderTasks() {
    return this.getTasks().map((task) => (
      <Task key={task._id} task={task} />
    ));
  }

  render() {
    return (
      <div className="container">
        <header>
          <h1>Todo List</h1>
        </header>

        <ul>
          {this.renderTasks()}
        </ul>
      </div>
    );
  }
}

这段代码的逻辑是这样的:getTasks函数返回了一个数组,每个元素是一个对象,包括_idtext属性;renderTask函数将返回的结果以key=task._id进行映射,得到三个<Task/>对象;最后render函数再将Task对象进行渲染(也就是把数据以特定方式呈现给用户),渲染的方法定义在Task.js中,也就是这一段

export default class Task extends Component {
  render() {
    return (
      <li>{this.props.task.text}</li>
    );
  }
}

最后再加上人家给的CSS文件,网站就变得好看多了


3. Collection

CollectionMeteor用来存储数据的一种方式,比较特别的就是clientserver均可以访问它,省去许多写server代码的麻烦,同时它们会自动同步更新,所以网页上展示的一直是最新的数据。
新建一个Collection也非常简单

import { Mongo } from 'meteor/mongo';

export const Tasks = new Mongo.Collection('tasks');

记得像教程里说的一样将它放在imports/api文件夹下,这里一些细节和导入就省略了。

那么如何访问Collection中的数据呢,我们需要用到react-meteor-data这个包,它可以帮助我们建立一种“数据容器”来将Meteor的数据装载进React Component结构。使用它我们需要将我们的Component装载进withTracker这个高级Component容器内

class App extends Component {
  renderTasks() {
    return this.props.tasks.map((task) => (
      <Task key={task._id} task={task} />
    ));
  }
  ...some lines skipped...
  export default withTracker(() => {
  return {
    tasks: Tasks.find({}).fetch(),
  };
})(App);

可以看到这里export的对象变成了withTracker,它返回Tasks中的所有数据作为tasks属性对应的值,所以前面不再需要getTasks函数,而是使用this.props.tasks进行映射。
想新加任务可以在终端中输入meteor mongo,再用mongo的语法进行插入,mongo数据库的语法可以参照这个教程


4. Forms and events

在这一部分我们为用户增加一个输入栏来新建任务,毕竟不能每次让使用者用db.tasks.insert这样不友好的方式来增加嘛
Appheader中加入如下form

<form className="new-task" onSubmit={this.handleSubmit.bind(this)} >
  <input
    type="text"
    ref="textInput"
    placeholder="Type to add new tasks"
  />
</form>

可以看到form具有onSubmit这样一个属性,表示进行上传操作,而执行的是后面的handleSubmit方法,后面会定义,这是React用来监听浏览器事件的常用方法;input含有一个ref属性,以便后面我们可以方便获取它。handleSubmit方法定义如下

handleSubmit(event) {
  event.preventDefault();

  // Find the text field via the React ref
  const text = ReactDOM.findDOMNode(this.refs.textInput).value.trim();

  Tasks.insert({
    text,
    createdAt: new Date(), // current time
  });

  // Clear form
  ReactDOM.findDOMNode(this.refs.textInput).value = '';
}

可以看到,在React中处理节点事件通过直接在Component上引用某个方法来实现,而在事件的handler内部则通过给Component一个ref属性然后用ReactDOM.findDOMNode来引用


5. Update and Remove

目前我们只能增加任务,现在我们将对它们进行更新和删除
task中增加两个元素,checkboxdelete button

render() {
  // Give tasks a different className when they are checked off,
  // so that we can style them nicely in CSS
  const taskClassName = this.props.task.checked ? 'checked' : '';

  return (
    <li className={taskClassName}>
      <button className="delete" onClick={this.deleteThisTask.bind(this)}>
        &times;
      </button>

      <input
        type="checkbox"
        readOnly
        checked={!!this.props.task.checked}
        onClick={this.toggleChecked.bind(this)}
      />

      <span className="text">{this.props.task.text}</span>
    </li>
  );
}

这里首先对是否check进行判断,是为了在CSS中呈现不同的效果
而checkbox和button和前面一样,在触发事件时就调用相应的方法:

toggleChecked() {
  // Set the checked property to the opposite of its current value
  Tasks.update(this.props.task._id, {
    $set: { checked: !this.props.task.checked },
  });
}

deleteThisTask() {
  Tasks.remove(this.props.task._id);
}

6. Temporary UI state

这一步增加了一个数据过滤特征,让用户可以选择隐藏已完成的任务
App中增加一个checkbox

<label className="hide-completed">
  <input
    type="checkbox"
    readOnly
    checked={this.state.hideCompleted}
    onClick={this.toggleHideCompleted.bind(this)}
  />
  Hide Completed Tasks
</label>

这里checked是从this.state.hideCompleted得到的,React Component有一个特殊的state用来保存各种封装的数据,当然我们需要在构造函数中对它进行初始化

constructor(props) {
  super(props);

  this.state = {
    hideCompleted: false,
  };
}

之后我们可以用this.setState函数对其进行更新

toggleHideCompleted() {
  this.setState({
    hideCompleted: !this.state.hideCompleted,
  });
}

renderTask函数中完成过滤的逻辑

renderTasks() {
  let filteredTasks = this.props.tasks;
  if (this.state.hideCompleted) {
    filteredTasks = filteredTasks.filter(task => !task.checked);
  }
  return filteredTasks.map((task) => (
    <Task key={task._id} task={task} />
  ));
}

与之前相比多的一步就是判断如果hideCompleted被勾选就先对任务进行过滤

还可以新加一个改动:显示未完成任务的统计,其实就是按条件计数。注意加到export的部分

export default withTracker(() => {
  return {
    tasks: Tasks.find({}, { sort: { createdAt: -1 } }).fetch(),
    incompleteCount: Tasks.find({ checked: { $ne: true } }).count(),
  };
})(App);

最后修改一下AppTodo List部分,完成

<h1>Todo List ({this.props.incompleteCount})</h1>

7. Adding user account

现在让我们试着添加用户,首先安装依赖包

meteor add accounts-ui accounts-password

接下来同样地,将要用到的Blaze UI包装进React Component,我们需要在imports/ui新建一个AccountsUIWrapper.js文件

import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { Template } from 'meteor/templating';
import { Blaze } from 'meteor/blaze';

export default class AccountsUIWrapper extends Component {
  componentDidMount() {
    // Use Meteor Blaze to render login buttons
    this.view = Blaze.render(Template.loginButtons,
      ReactDOM.findDOMNode(this.refs.container));
  }
  componentWillUnmount() {
    // Clean up Blaze view
    Blaze.remove(this.view);
  }
  render() {
    // Just render a placeholder container that will be filled in
    return <span ref="container" />;
  }
}

后面一系列小的改动这里就省略了,现在我们已经可以创建用户并登录了,然而暂时还没有什么卵用,所以我们给用户加一些关联性

  1. 只对登录用户展示新建任务的输入栏
  2. 展示哪个用户创建了哪个任务

要完成这两个功能,我们可以给tasks Collection增加两个属性

  1. owner-创建这项任务的用户的_id
  2. username-创建这项任务的用户的username,我们将直接把用户名存在task对象中,这样我们就不用每次展示task都去查询用户

修改handleSubmit如下:

Tasks.insert({
      text,
      createdAt: new Date(), // current time
      owner: Meteor.userId(),           // _id of logged in user
      username: Meteor.user().username,  // username of logged in user
    });

export部分增加返回当前用户:

  return {
    tasks: Tasks.find({}, { sort: { createdAt: -1 } }).fetch(),
    incompleteCount: Tasks.find({ checked: { $ne: true } }).count(),
    currentUser: Meteor.user(),
  };
})(App);

render方法中判断,只当有用户登录时显示输入栏

{ this.props.currentUser ?
  <form className="new-task" onSubmit={this.handleSubmit.bind(this)} >
    <input
      type="text"
      ref="textInput"
      placeholder="Type to add new tasks"
    />
  </form> : ''
}

最后在每个任务前显示创建它的用户:

<span className="text">
  <strong>{this.props.task.username}</strong>: {this.props.task.text}
</span>

8. Security with methods

到目前为止,所有人都可以对数据库进行操作,这其实是很不安全的,我们需要对权限进行控制
最好的方式是不再直接在client中调用insertupdateremove,而是调用会判断用户是否具有权限进行操作的方法
由于Meteor项目默认具有insecure特性(允许我们直接修改数据库),所以现在我们先移除它

meteor remove insecure

现在所有按钮和输入都不再生效,因为client端的数据权限被移除了,我们重新在tasks中定义每个操作对应的方法:

import { Meteor } from 'meteor/meteor';
import { Mongo } from 'meteor/mongo';
import { check } from 'meteor/check';

export const Tasks = new Mongo.Collection('tasks');

Meteor.methods({
  'tasks.insert'(text) {
    check(text, String);

    // Make sure the user is logged in before inserting a task
    if (! this.userId) {
      throw new Meteor.Error('not-authorized');
    }

    Tasks.insert({
      text,
      createdAt: new Date(),
      owner: this.userId,
      username: Meteor.users.findOne(this.userId).username,
    });
  },
  'tasks.remove'(taskId) {
    check(taskId, String);

    Tasks.remove(taskId);
  },
  'tasks.setChecked'(taskId, setChecked) {
    check(taskId, String);
    check(setChecked, Boolean);

    Tasks.update(taskId, { $set: { checked: setChecked } });
  },
});

然后把之前我们直接对数据库进行操作的地方全部修改,比如
Tasks.insert改成Meteor.call('tasks.insert', text),这样按钮又会重新开始工作了,这样做的好处有什么呢?

  1. 当我们增加任务到数据库时,可以确保日期、所有者、用户名等正确有效
  2. 可以增加额外的验证逻辑让用户后面可以将任务设为私密
  3. client的代码与数据库逻辑更加分离,比起让handler处理一大堆事务,现在的method可以在任何地方被调用

9. Publish and subscribe

现在我们需要了解安全性的另一半——到目前我们的数据库一直全部对client开放,也就是说如果调用Tasks.find()我们将得到所有任务,这对想储存私密任务的用户来说非常糟糕,我们需要控制Meteor发送给client端的数据
insecure类似,每个项目默认含有autopublish特性,也就是自动同步数据库内容到client端,首先还是移除它

meteor remove autopublish

刷新后会发现任务列表被清空了,现在我们需要显式指定发送到client的数据,需要用到Meteor.publishMeteor.subscribe函数
先试试发布所有任务:

if (Meteor.isServer) {
  // This code only runs on the server
  Meteor.publish('tasks', function tasksPublication() {
    return Tasks.find();
  });
}

export部分增加订阅:

Meteor.subscribe('tasks');

现在任务又重新出现了,调用Meteor.publishserver注册了一项名为"tasks"的发布,调用Meteor.subscribeclient就订阅了发布的内容,在这里也就是全部的任务。

后面的内容有兴趣的朋友可以自行在官网查看

猜你喜欢

转载自blog.csdn.net/github_39273626/article/details/80226187