按照官网的步骤一步一步做的,记录其中的一些重要的地方并进行解读来加深一下印象吧,我列出来的代码变动是不完全的,如果想复现效果请参照教程,绿色高亮的代码就是改动的部分。
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
-创建这项任务的用户的_id
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
中调用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
就订阅了发布的内容,在这里也就是全部的任务。
后面的内容有兴趣的朋友可以自行在官网查看