如何理解 meteor react ui所提出的 Optimistic UI

meteor学习笔记 · IT叫兽meteor学习笔记
我之前的主要工作是偏向于后端的 PHP+MySQL 开发,对“前端”的理解比较肤浅,以为仅仅是“组织和表现数据,并兼容不同的设备界面”。在熟练使用 jQuery、BootStrap 以及简单尝试过 NodeJS、AngularJS 等后,飘飘然以为自己是“全栈工程师”了。殊不知随着前端技术的发展,“全栈”这个词,应该是指能灵活运用各种前后端技术,组合各种优秀组件,高效开发跨设备的应用。
最近因缘际会需要学习 Meteor 这个全栈开发框架,因为和之前熟悉的“前后端分离、数据分层分步传输展示”的开发习惯完全不同,在学习和实践的过程中一方面是不断克服不适,踩坑,另一方面又像探险一样不断发出“哇——”的惊叹。
Tony兄建议我根据 Tutorial 来实践学习 todos 案例,可能是由于英文词汇和新的开发思维两方面的障碍,在粗略看第一遍的时候效率不是很高,于是在等待安装和下载的过程中,我把 v2ex 和知乎上一些围绕 meteor 的其他技术概念有关的图文说明、对比分析看了一遍(如 react、Virtual DOM、Flux、flow-router 等),搞明白了“响应式编程”“数据热更新”“前后端同时存取mongodb数据库”“部署代码自动刷新保持session状态”等,终于知道它的特性、适用范围和与其他有关技术方案相比的异同。
正如官方所说 Full stack JavaScript for amazing apps,meteor是一个融合了各种前后端技术来开发跨设备应用的开发框架。(按我不成熟的理解,有些类似 ionic 为提高开发 hybird app 效率所做的工具/库整合,但 meteor 适用范围更广也更灵活)
安装 node按官方的下面的方法一直失败,可能是 14.10 对高版本(&=4)的 node 支持有问题
12curl -sL /setup_4.x | sudo -E bash -sudo apt-get install -y nodejs
最后用下面的方法成功(安装的是 0.10.xx 的 node 版本,一切正常)
1234567sudo apt-get purge node nodejssudo curl -sL /setup | sudo bash -sudo apt-get install -y nodejsnodejs -vnpm -v# 因为默认命令是 nodejs ,所以创建一个链接为 node 命令ln /usr/bin/nodejs /usr/sbin/node
一定要换淘宝的 npm 源123456npm config set registry https://registry.npm.taobao.org// 配置后可通过下面方式来验证是否成功npm config get registry// 或npm info express
下载与安装执行下面这个 shell 安装脚本的时候,需要下载一些被“墙”的资源(可以将这个安装脚本另存为 install.sh,然后 vi 看到),所以先要全局翻墙,或者对终端进行 HTTP 代理(查看我的另一篇文章)
1234567$ cd ~$ curl / | sh# 安装完毕后,会有一个 .meteor 的文件夹,体积好大啊……# 为 meteor 命令创建一个链接(如果已经存在则不需要再创建了,只要更新下环境变量即可)$ ln -s ~/.meteor/meteor /usr/local/bin/$ source .bash_profile
一个小问题
在前面为了让终端使用代理而配置 privoxy 时,如果通过 export 命令设置了 https_proxy 这个环境变量,会导致后面安装官方 todos 案例失败,报下面的错误
tunneling socket could not be established …… openssl……
通过运行 unset https_proxy 将这个环境变量去掉就可以了。
官方的 todos 案例跑起来看看眼见为实,我们先看看 meteor 的一个官方案例,它实现了注册登录、多人编辑、权限、界面跨平台等特性。在动手从零开始写代码前,大致看看我们最后会做出一个什么样的东西来。
1234# 安装 todos 案例$ meteor create --example todos$ meteor# 上面的命令输入后,会loading、building 一堆文件,可见 meteor 有“后端”特性,不是单纯的在html里加载前端 js 库
提示如下,就表示安装运行成功了。在浏览器打开 http://localhost:3000/ 访问看看。通过开启 safari 的 响应式开发工具或 chrome 的 设备模拟器,一套代码同时适应各种不同的设备尺寸。
meteor[[[[[ ~/Downloads/todos ]]]]]=& Started proxy.=& Started MongoDB.=& Started your app.=& App running at:
从头开始开发 todos1. 删掉前面的官方例子,创建一个空项目1234$ rm -rf todos$ meteor create simple-todos$ cd simple-todos$ meteor
2. 感受一下修改文件,自动刷新,不用手动刷新浏览器了。打开目录下的 simple-todos.html ,随便修改一下里面的 html 代码,保存后再回到浏览器,发现它已经自动刷新了。这和之前开发 PHP 的时候,修改完文件,要手动刷新浏览器产生新的请求才会更新页面的流程不一样。
同时,在终端工具里,会打印出一行信息,提示刚刚进行了一次刷新
Client modified – refreshing
十分类似之前做 ionic 开发的时候,使用过的 gulp + gulp-connect ——监听某个目录下的文件变化,结合浏览器的 livereload 插件实现自动刷新(估计内部的实现方式可能大同小异)。
3. 对 meteor 的第一印象,让我想起 angularJS 的“绑定”和 smarty 模板语言官方教程是直接删除实例代码,写一个循环显示数据。
我还没来得急删除,就发现这代码挺熟悉的。和 AngularJS 里的“数据双向绑定”和“模板嵌入”有些类似。
“工欲善其事,必先利其器”,先把 Sublime Text 的 meteor 自动完成插件装上。cmd+shift+p,输入 install package。
4. 然后删除默认代码,继续沿着教程进行,循环显示一个 tasks 数组分别在两个文件里输入以下内容(省略了部分不重要的代码)。
这里我并未删除 simple-todos.js 文件里原来那几段带有’hello’关键词的Template.hello.helpers 代码,页面虽然会报一个 error,但正常显示出了 tasks 列表,猜测也是用到了“作用域”的概念?一处错误不致影响整体?
12345678910&
{{
#each tasks
}}
{{ & task
}}
{{
}}& name="task"&
&{{
}}&&
Template.body.helpers({
{text: 'this is task1'},
{text: 'this is task2'},
{text: 'this is task3'},
文档上有段话很重要,大概说清楚了 meteor 是怎么处理html模板(和它的独特“模板语法”的)
meteor 会把app里面的html文件全部进行解析,就像普通html标签一样显示;而位于&template name=&模板名&&里面的内容,会编译到 meteor template 里面去,一种方式是就和上面的代码一样嵌入到 html 代码中,另一种方式就是用javascript的方法 Template.templateName
啊,听起来好像php的模板引擎里的“公共调用”和AngularJS里的基本directive,一种是 模板引擎的标签语法,一种是在后台进行$this-&display()加载模板,循环、变量显示什么的,类似如此
所以 { { # each } } 和{ { # if } },以及后面可能出现的这些“标签名”也就没什么好说的了,只是记忆多一套语法规则而已。
然后这里出现的新概念是 helper,在 Template.body里定义了一个叫 tasks的 helper,并返回了一个数组,然后在循环里显示了出来每个数组元素的 text属性
5. CSS文件略,不废话
6. 更真实的数据,以及“collection”“documents”的概念刚刚我们是手动填充的一个测试数组,没什么意思。
collection是 meteor 保存持久化数据的方式,能同时被 server 和 client 两端访问。
按PHP和MySQL的思维,处理数据都是: 前端发出数据操作请求——php操作数据库——MySQL将数据处理结果返回给PHP——PHP再渲染 html ,前后端同时操作数据库这怎么可能?
当然如果能实现:前端一查数据,发现有变化自己就更新界面,而不用辗转发送到后端处理,再由后端刷新页面,那该多好啊。工作中有时候遇到查 MySQL 却因为 php-fpm 出现 502 问题导致失败确实是令人焦头烂额。
好吧,带着疑问接着看……
Creating a new collection is as easy as calling MyCollection = new Mongo.Collection(“my-collection”); in your JavaScript. On the server, this sets up a MongoDB collection called my- on the client, this creates a cache connected to the server collection. We’ll learn more about the client/server divide in step 12, but for now we can write our code with the assumption that the entire database is present on the client.
教程这时候说了一通 server/client 使用 MyCollection = new Mongo.Collection(&my-collection&);会产生什么不同返回结果的废话,结果说要到 12 节才详细解释,有种“欲知后事如何……”,就是说接下来都是以 client 端为准
修改sample-todos.js,把前面手动设置的那个数组删除,改成从 collection 获取
1234567891011Tasks = new Mongo.Collection("tasks");if (Meteor.isClient) {
Template.body.helpers({
tasks: function () {
return Tasks.find({});
});}
当然了,界面里的那个列表已经消失了,因为 tasks 还是空的。
手动从 server 端插入新的测试数据……collection里的项目叫document
好吧,document 不是文档的意思么?这奇怪的命名。可以类比传统的关系型数据库: collection 是表,document 是一行记录吧?
注意这里是 server端……,新开一个命令行窗口,进入项目目录运行
123456789101112131415161718192021222324252627$ meteor mongo # 咦,居然自带 mongodb 了 下面是提示/**MongoDB shell version: 2.6.7connecting to: 127.0.0.1:3001/meteorWelcome to the MongoDB shell.For interactive help, type &help&.For more comprehensive documentation, see
http://docs.mongodb.org/Questions? Try the support group
/group/mongodb-usermeteor:PRIMARY&*/# 插入新数据,多插入几条$ db.tasks.insert({ text: &Hello world!&, createdAt: new Date() });# 操作成功会返回/**meteor:PRIMARY& db.tasks.insert({text: &hello world!&, createAt: new Date()});WriteResult({ &nInserted& : 1 })meteor:PRIMARY& db.tasks.insert({text: &hello java!&, createAt: new Date()});WriteResult({ &nInserted& : 1 })meteor:PRIMARY& db.tasks.insert({text: &hello python!&, createAt: new Date()});WriteResult({ &nInserted& : 1 })meteor:PRIMARY& db.tasks.insert({text: &hello php!&, createAt: new Date()});WriteResult({ &nInserted& : 1 })*/
这时候看浏览器,自动更新了列表。我们没有写任何 SELECT * FROM xxxx LIMIT xxx,甚至没有任何类似连接数据库的操作 mysql_connect(host, port, user, password),它!自!己!自!动!更!新!了!
7. 命令行太麻烦容易出错,通过 UI 界面插入数据(这才是现实操作)在html文件里添加一个表单
123 class="new-task"&
type="text" name="text" placeholder="Type to add new tasks" /&
关键问题来了,这个问题之前在默认实例代码里也发现了,就是不管是按钮还是表单,都没有一个类似 jQuery 里的 $(&#id&).click() 这样的明显的某个元素的事件绑定?
然而原来只是语法的不一样而已……,在 meteor 里,顺序是 动作 元素筛选器,例如下面的 submit .new-task,和 jQuery 貌似正好相反。名词、动词前后不一样,可能也体现了两者的设计思路的不同,一个注重“事件”,一个注重“元素”
123456789101112131415161718Template.body.events({
"submit .new-task": function (event) {
event.preventDefault();
var text = event.target.text.
Tasks.insert({
text: text,
createdAt: new Date()
event.target.text.value = "";
至此,终于发现使用 javascript 同时进行前后端开发的好处之一了吧——那就是“数据格式的统一”。想当初,PHP要保存一个使用了时间控件(类似 BootStrap 的 date 插件)的输入框时间值,得从 javascript 里进行一次转换,保存的时候PHP进行一次转换,才能放到 MySQL 里以时间戳形式存储……
马上插入一条数据看看:
教程 Attaching events to templates 这段啰啰嗦嗦意思就是怎么给模板绑定事件,没啥好说的,记住这段代码 Template.templateName.events(...)就可以了,至于里面具体的 event 类别,只能工作中阅读详细文档了。
关于插入数据
因为 NoSQL 的特性,我们可以朝 Tasks 这个 collection 里插入任意的数据,即使在定义 Scheme 的时候,不存在相关的信息也可以!这在传统 MySQL 里是很难的,一张表定义了哪些字段,就只能插入这些字段,不存在的字段名及数据会被丢弃或报错。而在 MongoDB 里,以上面为例,我们可以插入新数据的时候增加一个字段,如 {text: 'javascript 大法好', user: 'root', createAt: new Date()},其中 user 就是之前并未定义的数据字段。
因此这就导致了“安全性”,我们肯定不能让用户随便在 client 端随便插入数据。
文档又来了一招“预知后事如何,下回分解……”
关于这点,可以读一下 LeanCloud 的文档,并在线测试一下就明白了。
将排序改成最新的todo在前面,这个和 MySQL 对比来看也是有不同的,MySQL 是先在数据库里排好序,再直接显示;这里是先查,再对结果排序
1return Tasks.find({}, {sort: {createdAt: -1 }});
8. 把todo设为已完成,删除todo实际上总结这一步我们要学习的就是三点
如何 update 数据
如何删除数据
如何判断数据不同的状态,并显示不同的样式
编辑 simple-todos.html 文件,注意 li 的 class=&{ { # if checked } } checked { { /if } }&属性,这里和 AngularJS 的 ng-if有点类似,如果task数据的checked为true,则使用样式名checked
123456789 name="task"&
class="{{ #if checked }}checked{{ /if }}"&
class="delete"&&&
type="checkbox" checked="{{ checked }}" class="toggle-checked" /&
class="text"&{{ text }}&
同样要给删除按钮和完成checkbox绑定动作,注意Template.task,是为模板task绑定事件,而不是之前的 Template.body。这里使用了collection 的另外两个方法 update 和 remove
1234567891011Template.task.events({
"click .toggle-checked": function () {
Tasks.update(this._id, {
$set: {checked: ! this.checked}
"click .delete": function () {
Tasks.remove(this._id);
注意到this._id
this指向了一个单独的 task 对象,在 collection 里每个 document 都有一个唯一的 _id 字段用于指向这个特定的 document。有点类似于 MySQL 里数据值唯一的“自增字段”。
因为_id 字段可以指向这个特定的 document,所以可以使用 update 和 remove 来修改关联的 task 数据。
第一个参数是个标记这组 collectioin 里某个子集的 selector(“选择器”?),第二个参数是一个“update parameter”(不知如何翻译……),用 $set 来修改 checked 字段的值。
只有一个参数,就是一个 selector
9. 部署应用,在移动设备上安装(安卓和 iOS)我觉得这些和 ionic 十分类似……,只是命令名变了一下而已……
发布到免费的web测试环境上
应该就是把文件上传到
上,并分配一个二级域名,以便可以在网络上通过各种设备来访问。Uploading 非常慢……所以建议还是用你自己的服务器吧。
12$ meteor deploy my_app_# 输入你的邮箱等信息,如果名字已经被别人用了,那就换一个吧……
发布到 iOS设备上
1234567meteor install-sdk iosmeteor add-platform iosmeteor run ios# 需要开发者账号meteor run ios-devicemeteor run ios-device --mobile-server my_app_
发布到安卓设备(模拟器和真实设备)
1234567meteor install-sdk androidmeteor add-platform androidmeteor run android# 开启 Debugmeteor run android-devicemeteor run android-device --mobile-server my_app_
10. 使用 Session 存储临时 UI 状态(实现“隐藏已完成task”功能)首先把这个checkbox添加到页面中,通过改变选中状态实现 显示/隐藏 已完成task列表
123456 class="hide-completed"&
type="checkbox" checked="{{ hideCompleted }}" /&
Hide Completed Tasks
绑定动作,写入 Session 值,如果勾选,则为 true,没有勾选则为 false
123"change .hide-completed input": function (event) {
Session.set("hideCompleted", event.target.checked);
最后根据 hideCompleted 这个session值,进行分支处理,来对需要显示的数据进行过滤,修改前面的 helpers。注意里面的checked: { $ne: true }。
1234567891011121314Template.body.helpers({
tasks: function () {
if (Session.get("hideCompleted")) {
return Tasks.find({checked: {$ne: true }}, {sort: {createdAt: -1 }});
} else {
return Tasks.find({}, {sort: {createdAt: -1 }});
hideCompleted: function () {
return Session.get("hideCompleted");
Session与Mongo.Collecton的相似之处和不同之处
他们都可以认为是一种“reactive data”,意思就是当数据变化了,页面在检测到变化时会马上更新显示,而不需要去手动修改界面。而区别在于,Mongo.Collection 有时候会和 server 端关联,server 端触发数据变化会造成前端界面改变;而 Session 的存取则都是发生在 client 端,所以特别适合保存临时的UI状态。(我可以认为它有些像 LocalStorage 的功能吗?)
扩展功能,计数
添加一个helper
123incompleteCount: function () {
return Tasks.find({checked: {$ne: true }}).count();
在html里用 读取。
11. 添加用户账号系统真是太贴心了!!居然还自带了一套用户登录UI
1meteor add accounts-ui accounts-password
放入登录组件(是一个下拉按钮,点击弹出登录窗口)
123{{ & loginButtons }}
class="new-task"& 略
配置 account-ui,使用 username 的形式(默认是email)
123Accounts.ui.config({
passwordSignupFields: "USERNAME_ONLY"
这样就可以使用登录、注册功能了(完全不用写一行代码……),马上注册一个账号吧。
有了账户系统,就需要扩展一下我们app的权限控制了
只有登录后才可以添加 task
显示每个 task 的添加者
正如前面所说,collecton 的 document 可以在后期扩展更多的 field。这里添加两个:
owner(就是建立这个 task 的 user 的 _id)
username(简历这个 task 的 user 的 username,直接保存在 task 的 object,就不用再去查一次 user 了)
扩展之前写的 insert 方法,owner 和 username 都可以通过 Meteor.xxxx 来获取(有点像 PHP 框架里封装的用户 session 读取方法)
123456Tasks.insert({
text: text,
createdAt: new Date(),
owner: Meteor.userId(),
username: Meteor.user().username
因为需求是“只有登录了的用户才可以增加新的的 task”,所以未登录的时候,表单是不显示的,这里加上 { { # if 条件 } } 判断。还有要把添加这条 task 的 username 显示出来
12345678{{ #if currentUser }}
&form class="new-task"&
&input type="text" name="text" placeholder="Type to add new tasks" /&
{{ /if }}
&template……略&span class="text"&&strong&{{ username }}&/strong& - {{ text }}&/span&
如下图,登录后再创建 task,前面就会显示创建者的用户名了
Meteor 的教程习惯是“先摆实例,再讲道理”,我喜欢。文档接着开始解释这套用户系统的概念了。
它称 accounts-ui 叫 package,既然是“包”,就是说里面包括了 template、逻辑代码等整套处理方案。默认提供的是通过密码登录,也可以安装 accounts-facebook实现使用facebook账号登录(当然这在天朝就是个笑话了),这个package可以通过简单的 currentUser 在html里显示用户信息,在javascript可以用 Meteor.User() 来获取整个用户document数据
12. 提高系统的安全性,用“methods”来替代client端对数据库的操作截止目前,我们可以直接在 client 端对数据进行增删改查,没有任何对数据进行权限方面的保护。(回忆下看看,用户 A 新建的 task,用户 B 登录后也可以删除它,甚至不登录都可以——这么多可怕)
对于一个测试app或者个人自用的app来说不是什么问题,但多人共同访问一份数据,就必须加入各种权限控制策略了。
首先卸载 insecure 包,它允许我们在 client 端访问数据库,产品一旦过了“原型设计”阶段,就要卸载掉这个不安全的东西。
1$ meteor remove insecure
卸载完毕后,发现所有的按钮、表单组件都不可用了(具体表现如下图,点checkbox和删除按钮,界面闪一下却不会发生任何效果),那是因为client端操作数据库的权限被移除了。
定义 methods
官方文档对methods的解释有点拗口。大概意思是,如果我们要在 client 端对数据库进行任何一个操作,都要定义一个对应的 method,不能像以前那样直接在客户端进行 insert、remove 等操作了。并且 method 可以同时被 server 和 client 端执行。
可以大概把 method 想象成以前开发时服务器端定义的一个个“API”,专门用于实现一个小的功能。在这个 API 内部进行权限验证、数据处理等操作,客户端只需要带着“符合规定”的参数请求这个 API 地址就可以了,API 会返回结果。
只不过这个 API 不是以 url 的形式出现,因为“前后端”统一了嘛,所以 url 可以省略了。
在 if (Meteor.isClient) 的外面定义这些 methods,包括 addTask,deleteTask,setChecked,和 client 端代码差不多,只不过把操作都包裹到 method里面了。最后把之前的 client 端数据操作代码都改成调用 method 的方式
12345678910111213141516171819202122232425262728293031Meteor.methods({
addTask: function (text) {
if (! Meteor.userId()) {
throw new Meteor.Error("not-authorized");
Tasks.insert({
text: text,
createdAt: new Date(),
owner: Meteor.userId(),
username: Meteor.user().username
deleteTask: function (taskId) {
Tasks.remove(taskId);
setChecked: function (taskId, setChecked) {
Tasks.update(taskId, { $set: { checked: setChecked} });
}});Meteor.call("addTask", text);Meteor.call("setChecked", this._id, ! this.checked);Meteor.call("deleteTask", this._id);
在 addTask 这个 method 里判断用户是否登录,如果没有则抛出错误。后面我们再对 setChecked和deleteTask添加代码设置权限,让用户只能修改自己的 task。
Optimistic UI 的概念
为什么我们要在 client 和 server 上定义 method 呢?
在使用 Meteor.call的时候,有两件事平行进行着
就和传统的ajax请求一样,client发起一个请求到server端运行 method(这时候环境是安全的)
(牛逼之处来了)method 的一个“模拟器”在 client 端直接运行(不去 server 端请求),通过一些信息来 预测server 可能返回的结果
第一点好理解,第二点是干嘛的?以“新建一个task”为例
在服务器端返回结果之前,客户端首先已经在界面上生成并显示了这个task数据;等到服务器端结果返回结果,如果数据没有变化,那就这样保持客户端的数据,如果和客户端数据不一样就更新下客户端的数据
牛啊!真牛啊!以前写PHP的时候,一定要在表单提交完成,服务器端处理完毕后,再捕获返回值来显示数据,是一个先操作再看结果的过程。
现在,直接提交数据,马上就在界面上显示最新的数据,至于服务器端的处理,让它慢慢来吧,大不了有变化再更新一点嘛。是先看结果再等待操作过程。难怪前端发展日新月异,轮子不断涌现,这种颠覆传统的思想确实让人high起来!
13. 使用 Publish 和 Subscribe 来过滤数据在默认情况下,使用 Task.find() 会把所有数据都读取出来,这对于多用户的系统来说肯定是不行的。我们要让用户只能读取属于自己的数据。
和前面删除 insecure package 一样,需要删除 autopublish这个 package
1$ meteor remove autopublish
我们发现列表中的数据都消失了。此后从 server 读取数据,我们都要明确指出读取符合什么条件的数据。
publish运行在 server 端,而 Subscribe运行在 client 端,正如字面意思,一个“发布”,一个“订阅”
123456789if (Meteor.isServer) {
Meteor.publish("tasks", function(){
return Tasks.find();
});}if(Meteor.isClient) {
Meteor.subscribe("tasks");}
处理 task 的“私有”属性,以及判断 task 的 owner
在 task 的模板里添加如下代码,在当前用户是该 task 的 owner 的时候,允许将这个 task 设为私有或公开。同时,如果这条 task 是私有,也有显示对应的样式
123456789101112{{ #if isOwner }}
class="toggle-private"&
{{ #if private }}
{{ else }}
{{ /if }}
{{ /if }}
class="{{ #if checked }}checked{{ /if }} {{ #if private }}private{{ /if }}"&
注意上面模板中的 isOwner 变量,这个在 Template.task的 helpers 里获取(而不是 Template.body.helpers)
然后添加上 setPrivate 这个 method(稍后再在 client 里调用)
123456789101112131415161718192021222324Template.task.helpers({
isOwner: function () {
return this.owner === Meteor.userId();
Meteor.methods({
setPrivate: function (taskId, setToPrivate) {
var task = Tasks.findOne(taskId);
if (task.owner !== Meteor.userId()) {
throw new Meteor.Error("not-authorized");
Tasks.update(taskId, { $set: { private: setToPrivate } });
}"click .toggle-private": function () {
Meteor.call("setPrivate", this._id, ! this.private);
至此,我们可以对某一个 task 设置 private 属性值了。那么,怎么在 client 读取的时候自动进行过滤呢?
修改 server 端的 publish 规则,为 find 方法添加过滤规则,一是 tasks 的 owner 必须是当前用户
12345678Meteor.publish("tasks", function () {
return Tasks.find({
{ private: {$ne: true} },
{ owner: this.userId }
测试“private”是否生效
按文档说明,使用浏览器的“隐身模式”对比两个登录用户,用户 A 对某个 task 设置了 private,那么用户 B 是看不到这个 task 的。
最后,需要对 deleteTask 和 setChecked 进行权限的处理,在正式操作数据前添加代码
123456var task = Tasks.findOne(taskId);
if (task.private && task.owner !== Meteor.userId()) {
throw new Meteor.Error("not-authorized");
14. 晋级打怪最后,就是看看官方推荐的书, 下载官方的两个较大型案例研究,以及看看有关的工具、资源、设计,还有完整手册了~
后面是关于分别将 Angular 和 React 与 Meteor 集成的资料了。这个其实就属于萝卜青菜各有所爱了,我以前捣鼓 ionic 的时候是学习的 AngularJS,但是现在 React 很火嘛,于是就以学习 React 为主了。
使用 React 和 Meteor1. 一些枯燥的理论关于 reactreact 的官方说明乍看比较难懂,还不如直接去 facebook 看原版的。看了一些零散的资料,重要术语就是 “JSX”,“在javascript代码里写界面”,“界面和数据混合”,“Virtual DOM”,“组件化”等等。react 给我印象最深刻的就是“零零散散的组件拼装在一起”。
在 React 里,不再和 Meteor 一样用 或Template.templateName来容纳 HTML。而是使用 view component。
view component是通过 React.createClass定义的类,你可以在里面实现任何想要的方法。甚至包括生成视图(如 render 方法),获取数据(如通过 props属性从父元素中得到)。这和以前把 HTML 和 数据 分离的思路是完全不同的。
举个可能不是很恰当的例子,以前 HTML 和 数据 分离的模式,就像老式的诺基亚功能机,各种电话短信等等构成一个封闭的诺基亚手机操作系统,但它们只能用在同型号手机上,没法移植到摩托罗拉等功能机上,并且也很难把某个功能从操作系统里删掉,删掉就会导致功能不完整或者系统崩溃。
而 React 的组件就像现在手机里的 APP,各种 APP 是独立的,各自实现一个功能,不依赖其他的 APP 就能运行,可以任意删除而不影响操作系统,也可以在别的手机上安装不存在兼容问题。
例如,可以制作各种 component,如滚动列表、一个表单、一个漂亮的按钮等等,然后把它们用到各种各样的页面中去。
JSX直接用javascript来写HTML并通过 render()渲染出来
这里思维需要从原来直接写HTML里切换出来,例如 &div class='style_class'&,而因为 JSX 是用原生的 Javascript 生成 HTML,所以写法变成了className。
2. 新建项目明白了上面的概念,再结合代码就容易明白了,先创建一个新的项目叫 simple-todos-react
12# 首先要安装react支持$meteor add react
有下面的提示就表示安装成功了
Changes to your project’s package version selections:
coffeescript
added, version 1.0.11cosmos:browserify
added, version 0.9.3jsx
added, version 0.2.3react
added, version 0.14.3react-meteor-data
added, version 0.2.4react-runtime
added, version 0.14.4react-runtime-dev
added, version 0.14.4react-runtime-prod
added, version 0.14.4
1234567# 创建新项目$ meteor create simple-todos-react$ cd simple-todos-react# 因为以后都是用 React 的 JSX 了,所以现在可以删掉 `simple-todos-react.js`文件了。$ rm simple-todos-react.js$ meteor
安装好 sublime text 的插件
3. 找到simple-todos-react.html,删掉里面的 body 和 tempalte 代码段,改成一个“容器”**123&
id="render-target"&&&
4. 新建 simple-todos-react.jsx**注意 React.render(&App /&, document.getElementById(&render-target&));里的 &App /&,以及获取到的 id=render-target这个元素
123456if (Meteor.isClient) {
Meteor.startup(function () {
React.render( /&, document.getElementById("render-target"));
});}
5. 前面的代码里要render出这个 component,现在就来实现它**新建 App.jsx
里面定义的 getTask方法用于获取一组测试数据
注意看它的render方法,在里面 return 一段 HTML,而这段 HTML 里又用 {this.renderTasks()}调用了 renderTasks方法
最后 renderTasks 方法里,又调用了一个 component,叫&Task /&,这是需要后面再去定义的
我们现在不需要再在 simple-todos-react.html写 HTML代码了,全部在 JSX 文件里实现。component 有点像极大扩展特性的 template ,除了能被任意调用,还能够自己生成HTML、数据。
注意到this.getTasks().map里面有个奇怪的语法 (arg)=&{ },这是最新的
ES2015特性,更多的资料可阅读文档里附上的一些链接。
1234567891011121314151617181920212223242526App = React.createClass({
getTasks() {
{_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)=&{
key={task._id} task={task} /&;
render() {
className="container"&
&&Todo List&&
&{this.renderTasks()}&
}});
6. 最后来实现Task组件**新建 Task.jsx 文件。如前面所说,Task组件通过它的父级元素的props属性来获取数据。
1234567891011Task = React.createClass({
propTypes: {
task: React.PropTypes.object.isRequired
render() {
&{this.props.task.text}&
}});
7. 样式文件这个没什么新的,和前面原版的案例一模一样,复制一下就可以了
8. 保存数据关于操作 mongo 并插入测试数据也和前面一样,就不重复了。
12$ meteor mongo$ db.tasks.insert({ text: &Hello world!&, createdAt: new Date() });
mixin是什么
因为 react 的组件都是独立的,但这些组件可能拥有某些共同功能,为了能共享一部分代码,就使用 mixin 来定义写方法以实现这些共同的功能,使用了这个 mixin 的组件能自由地使用这些方法。(有点像PHP里的第三方工具类?)
在React component 内使用 ReactMeteorData 这个 mixin 从 collection 获取数据
当使用了这个 mixin 后,就可以定义一个 method getMeteorData来跟踪数据变化,这个 method 的返回值可以在 render() 里使用 this.data来访问。
首先在 simple-todos-react.jsx定义 collection
1Tasks = new Mongo.Collection("tasks");
删除之前 App.jsx 里的 getTasks 方法,改成 mixin 的 getMeteorData 方法,然后修改 renderTasks 里的数据获取方式为 this.data.tasks
1234567mixins: [ReactMeteorData],getMeteorData() {
return {
tasks: Tasks.find({}).fetch()
一个容易出错的地方
我在 tasks: Tasks.find({}).fetch() 后面写了个分号“;”, 结果报错如下。后来仔细对比文档删除后就好了。特别注意这里看上去像一条js语句,其实好像只是一个对象……
While processing files with jsx (for target web.browser):App.jsx:14:35: App.jsx: Unexpected token (14:35)
9. 表单在 App.jsx 的 render() 里添加一个表单,唯一注意的是表单有个onSubmit={this.handleSubmit}属性绑定了提交时候的动作。
然后写 handleSubmit 的实现,使用了 React 特有的一些方法如 React.findDOMNode
123456789101112131415161718192021 handleSubmit(event) {
event.preventDefault();
var text = React.findDOMNode(this.refs.textInput).value.trim();
Tasks.insert({
text: text,
createdAt: new Date() // current time
React.findDOMNode(this.refs.textInput).value = "";
render() {略&h1&Todo List&/h1&
&form className="new-task" onSubmit={this.handleSubmit} &
type="text"
ref="textInput"
placeholder="Type to add new tasks" /&
如上面例子可以看到,在 React 里,捕获事件是通过直接在组件上面引用一个方法(这里就是 onSubmit)。在事件捕获器内部,你可以使用 ref 属性和findDOMNode方法来引用组件的元素。
修改 getMeteorData 方法,增加sort
1tasks: Tasks.find({}, {sort: {createdAt: -1 }}).fetch()
10. task的删除和check进入 Task.jsx,为 Task 组件添加两个方法,然后修改 render 方法,添加按钮和checkbox,并绑定事件,代码很好懂就不详细介绍了。发现都是一直在“翻译”meteor 的语法到 react 的表达方式啊……要注意 render 里面 HTML 的属性名要换用“javascript”的方式(例如 className)
其实还是蛮喜欢 react 的表达方式的,主要是花括号不用打两个,哈哈!!
12345678910111213141516171819202122232425262728Task = React.createClass({
propTypes: {
task: React.PropTypes.object.isRequired
toggleChecked() {
Tasks.update(this.props.task._id, {
$set: {checked: !this.props.task.checked}
deleteThisTask() {
Tasks.remove(this.props.task._id);
render() {
const taskClassName = this.props.task.checked ? "checked" : "";
className={taskClassName}&
className="delete" onClick={this.deleteThisTask}&&&
type="checkbox" readOnly={true} checked={this.props.task.checked} onClick={this.toggleChecked} /&
className="text"&{this.props.task.text}&
}});
11. 保存临时 UI 状态数据前面的 deploy、发布到手机都已经说过了,略
话说接下来都不想写了,直接贴代码吧(基本上都是从 metor 自带的前端语法翻译成 react 的表达方式)
App.jsx 文件,这里新的知识是 state,回忆一下 meteor 自带的 Session吧,类似的功能。
getInitialState方法用于初始化 state 数据。
修改 getMeteorData,对返回的 tasks 数据增加一个过滤条件(判断是否勾选了“只看未完成”)。最后,增加一个返回值 incompleteCount,显示未完成task的总数,在 render 里用 ({this.data.incompleteCount})读取
123456789101112131415161718192021222324252627282930313233343536373839mixins: [ReactMeteorData],getMeteorData() {
let query = {};
if (this.state.hideCompleted) {
query = {checked: {$ne: true }};
return {
tasks: Tasks.find(query, {sort: {createdAt: -1 }}).fetch(),
incompleteCount: Tasks.find({checked: {$ne: true }}).count()
getInitialState() {
return {
hideCompleted: false
},略toggleHideCompleted() {
this.setState({
hideCompleted: ! this.state.hideCompleted
},render(){ 略&label className="hide-completed"&
type="checkbox"
readOnly={true}
checked={this.state.hideCompleted}
onClick={this.toggleHideCompleted} /&
Hide Completed Tasks
12. 把用户登录包 accounts-ui 封装成 react 的组件!还记得原版的 meteor 只要在模板里放一个 { { & loginButtons } } 就可以实现一个登陆框吗?在 react 里其实也可以用,只不过步骤多了一点,要先将他包裹成一个 react 的组件才能被使用。
命令行安装 accounts-ui 和 accounts-password 两个包(略)
创建一个 AccountsUIWrapper.jsx文件(也就是一个组件)
123456789101112131415AccountsUIWrapper = React.createClass({
componentDidMount() {
this.view = Blaze.render(Template.loginButtons,
React.findDOMNode(this.refs.container));
componentWillUnmount() {
Blaze.remove(this.view);
render() {
ref="container" /&;
}});
然后就可以直接在 App.jsx的 render 方法里直接调用这个组件
12 /& className="new-task" onSubmit={this.handleSubmit} &
Accounts.ui.config 的配置在 simple-todo-react.jsx里,和前面一样,略
Tasks.insert 里增加插入 owner 和 username ,和前面一样,略
这里要注意的是,使用 meteor 原版的前端库,可以直接调用 currentUser ,而被封装成 react 组件后,要手动把它作为返回值。修改 getMeteorData,添加一行获取 currentUser 值。
12345return {
tasks: Tasks.find(query, {sort: {createdAt: -1 }}).fetch(),
incompleteCount: Tasks.find({checked: {$ne: true }}).count(),
currentUser: Meteor.user()
然后才能被“模板”识别出来,修改 App.jsx,这一段比较长,但实际上是个三元运算符 condition ? true_switch : false_switch
12345678{ this.data.currentUser ?
&form className=&new-task& onSubmit={this.handleSubmit} &
type=&text&
ref=&textInput&
placeholder=&Type to add new tasks& /&
&/form& : &&
然后 Task.jsx 组件就可以使用 {this.props.task.username}把它的用户名显示出来了
123&span className="text"&
&{this.props.task.username}&: {this.props.task.text}
13. 使用 method 提高安全性和 meteor 的差不多,也是先删除 insecure 包,然后添加 Meteor.methods,最后把组件里操作数据库的方式改成 Meteor.call()的形式 。
例如 Task 组件
12345Meteor.call("setChecked", this.props.task._id, ! this.props.task.checked);Meteor.call("removeTask", this.props.task._id);
在 simple-todos-react.jsx里定义这些 methods,注意它们的写法和前面有点不一样,不是methodName: function(params){},而是更加简写的 methodName(params){}
14. 使用 server 端的 Publish 和 client 端的 Subscribe,以及将 task 设为私有的处理先删除 autopublish 包。然后添加 Public 和 Subscribe ,这个和之前的代码一样的,略
然后添加一个 method,和之前的 setPrivate一样,代码略(但是这里把 var task 改成了 const task)
传一个新的属性showPrivateButton给 Task ,只有当这条数据的 owner 等于当前登录用户的时候,才显示“设为私有”按钮。这里修改 App.jsx 的 renderTasks,注意两句的写法。
123456789return this.data.tasks.map((task) =& {
const currentUserId = this.data.currentUser && this.data.currentUser._
const showPrivateButton = task.owner === currentUserId;
key={task._id}
task={task}
showPrivateButton={showPrivateButton} /&;
然后在 Task.jsx 的propTypes属性对象里增加一个成员showPrivateButton(来自前面的传入,指定是 bool 类型的)。然后在 render 方法里根据 showPrivateButton 的值生成这个按钮。最后实现这个按钮点击绑定的事件 togglePrivate(调用 setPrivate 这个 method)。
还有最后,如果这个task是private的,它的样式也要和普通的task通过 taskClassName 有所区分(前面我们只设置了 checked 这个class,现在还要加上 private)
123456789101112131415161718192021222324Task = React.createClass({
propTypes: {
task: React.PropTypes.object.isRequired,
showPrivateButton: React.PropTypes.bool.isRequired
},略togglePrivate() {
Meteor.call("setPrivate", this.props.task._id, ! this.props.task.private);
},略{ this.props.showPrivateButton ? (
className="toggle-private" onClick={this.togglePrivate}&
{ this.props.task.private ? "Private" : "Public" }
) : ''} 略 render() {
const taskClassName = (this.props.task.checked ? "checked" : "") + " " +
(this.props.task.private ? "private" : "");
className={taskClassName}&
好了,现在仍然是所有用户都可以看到别人的包括私有的 task。和之前一样,在 server 端的 publish 方法里加上数据的过滤,只输出当前用户的 Public 属性的 task。
123456789101112131415161718192021if (Meteor.isServer) {
Meteor.publish("tasks", function () {
return Tasks.find({
{ private: {$ne: true} },
{ owner: this.userId }
});}``` 最后,在`removeTask`和`setChecked`两个 method 里添加权限处理。```javascriptconst task = Tasks.findOne(taskId);
if (task.private && task.owner !== Meteor.userId()) {
throw new Meteor.Error("not-authorized");
降级123456789#这个命令是安装的最新版本,如果你的app是老版本的,会报`Meteor x.x.x
is not installed and could not be downloaded`$curl / | sh#因此需要手动下载和修改安装脚本$wget $mv index.html install_meteor.sh$vi install_meteor.sh#把 RELEASE = x.x.x 改成旧版本号$ sh install_meteor.sh
meteor Package version not in catalog: npm-container
While selecting package versions:error: Package version not in catalog: npm-container 1.2.0
While refreshing package catalog to resolve previous errors:error: Network error: wss:///websocket: getaddrinfo ENOTFOUND
找到 .meteor\package 文件中的 npm-container 删除掉这行或者 卸载本地的 npm-container 重新安装
做完操作后,运行一次 meteor 工程后,再次做其他操作即可
mongodbmac 下安装与启动 mongo(这个是系统安装的独立的 mongo,而非 meteor 库自带的meteor,要区分开来)12345$ brew install mongodb$ brew services start mongodb$ mongo&db# 列出 test 库,表示安装成功
备份和还原 meteor 的数据库12345$ cd meteor-project$ meteor$ ps aux|grep &mongo&# 看到类似下面一样提示,这就是 meteor 的 mongo 运行的当前数据库,端口 3001,库名 meteor# /Users/zzy/.meteor/packages/meteor-tool/.1.1.10.1b51q9m++os.osx.x86_64+web.browser+web.cordova/mt-os.osx.x86_64/dev_bundle/mongodb/bin/mongo 127.0.0.1:3001/meteor
切换到“系统”的 mongo,使用 mongodump 命令来备份和还原
123456789#备份$ cd /usr/local/Cellar/mongodb/3.2.0/bin/$ mongodump -h 127.0.0.1 --port 3001 -d database_name [-c 表] -o ~/mongo_bak/#还原操作就是反过来(bson文件)$ mongorestore -h 127.0.0.1 --port 3001 -d database_name -c table_name --drop /mongo_bak/table_name.bson# 如果是 json 文件,只能一个个集合来处理$mongoimport --host 127.0.0.1 --port 3001 --db meteor -c collection_name --drop --file ./Downloads/mongo-txd-erp-033.js
是时候看完整的文档了学完了 meteor todos 和 todos with react ,我们可以边工作边查阅完整的文档。对所有组件、方法和属性、包的说明及用法,都可以在这里找到。简直是包罗万象,什么发邮件、用户权限控制、组件封装、网络请求、静态资源管理等等……
建议先粗略看一遍,了解目录和关键点,然后在遇到问题的时候就可以快速找到资料。
更完整的学习案例官方还有两个更完善的案例 todos 和 local market,可以clone代码下来研究学习,是非常好的资料。
工具、资源、包工具
meteor CLI
IDE与编辑器:常用的如 Sublime Text 3(我爱死它了),VS Code,Atom,WebStorm(重量级选手)
,用于调试、性能分析等
书啊、视频教程、博客、论坛等等,深入学习必备
好多的有意思的包,一个优秀的框架一定有繁荣发展的第三方资源
多说一句:在写这篇blog的时候,hexo解析行内代码块中有特殊符号的问题因为 meteor 里有不少语法是两个花括号,然后带个 &或者#号,而hexo有个bug,就是如果用 行内代码块 包裹,在生成的时候仍然会报错,例如 { { & xxxxxx } } 和 { { # xxxxxx } },在 hexo s的时候报错类似:
FATAL Template render error: unexpected token: #at Error.exports.TemplateError (/Users/zzy/my_blog/node_modules/nunjucks/src/lib.js:51:19)
网上查了一些资料,有的说在两个花括号和中间代码之间添加空格,有的说用反斜杠转义,但都没有解决问题。后来看了这篇
,使用 Raw 插件,发现它除了添加个 { % ,貌似没解决我的问题。后来测试下,只需要在每个花括号和特殊符号之间统统插入一个空格就可以了。

我要回帖

更多关于 对ui设计的理解 的文章

 

随机推荐