按照官网的步骤一步一步做的,记录其中的一些重要的地方并进行解读来加深一下印象吧,我列出来的代码变动是不完全的,如果想复现效果请参照教程,绿色高亮的代码就是改动的部分。
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文件夹外的文件在meteor的server启动时就会自动导入,而文件夹内的文件只在有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函数返回了一个数组,每个元素是一个对象,包括_id和text属性;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
Collection是Meteor用来存储数据的一种方式,比较特别的就是client和server均可以访问它,省去许多写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这样不友好的方式来增加嘛
在App的header中加入如下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中增加两个元素,checkbox和delete 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)}>
×
</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);
最后修改一下App的Todo 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" />;
}
}
后面一系列小的改动这里就省略了,现在我们已经可以创建用户并登录了,然而暂时还没有什么卵用,所以我们给用户加一些关联性
- 只对登录用户展示新建任务的输入栏
- 展示哪个用户创建了哪个任务
要完成这两个功能,我们可以给tasks Collection增加两个属性
owner-创建这项任务的用户的_idusername-创建这项任务的用户的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中调用insert,update和remove,而是调用会判断用户是否具有权限进行操作的方法
由于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),这样按钮又会重新开始工作了,这样做的好处有什么呢?
- 当我们增加任务到数据库时,可以确保日期、所有者、用户名等正确有效
- 可以增加额外的验证逻辑让用户后面可以将任务设为私密
client的代码与数据库逻辑更加分离,比起让handler处理一大堆事务,现在的method可以在任何地方被调用
9. Publish and subscribe
现在我们需要了解安全性的另一半——到目前我们的数据库一直全部对client开放,也就是说如果调用Tasks.find()我们将得到所有任务,这对想储存私密任务的用户来说非常糟糕,我们需要控制Meteor发送给client端的数据
与insecure类似,每个项目默认含有autopublish特性,也就是自动同步数据库内容到client端,首先还是移除它
meteor remove autopublish
刷新后会发现任务列表被清空了,现在我们需要显式指定发送到client的数据,需要用到Meteor.publish和Meteor.subscribe函数
先试试发布所有任务:
if (Meteor.isServer) {
// This code only runs on the server
Meteor.publish('tasks', function tasksPublication() {
return Tasks.find();
});
}
在export部分增加订阅:
Meteor.subscribe('tasks');
现在任务又重新出现了,调用Meteor.publish在server注册了一项名为"tasks"的发布,调用Meteor.subscribe时client就订阅了发布的内容,在这里也就是全部的任务。
后面的内容有兴趣的朋友可以自行在官网查看
本文介绍如何使用Meteor和React从零开始构建一个Todo应用,包括创建项目、组件化开发、使用Collection存储数据、表单与事件处理、更新与删除数据、临时UI状态管理、用户账号添加、安全性增强及数据发布订阅等内容。
256

被折叠的 条评论
为什么被折叠?



