meteor框架学习心得

时间:2022-11-24 20:09:43

前言

了解到这个框架其实是还是因为看书,然后花了一个星期作用把翻译出来的官方文档看了一遍,代码也跟着走了一遍,也算是有了个大概的认识吧。这里写一下体会吧。

简介

首先什么是Meteor(中文翻译:流星),我觉得取这个名字就是有它的意义的,因为用这个框架开发真的很快,就好像流星一样。它是一个基于Node.js和Mongodb的框架,使用DDP(分布式数据协议)在客户端和服务器间传送数据。这个是什么意思呢,也就是,我的数据资源在我客户端有一份,在我的服务器也有一份,我在操作客户端的时候,就不需要等待服务器那边的,马上可以做出响应,那么如果这个操作是非法的呢?也就是服务器那边没有操作成功,那客服端这边知道了以后,就会恢复了,这个有什么好处呢,这样只有我们的逻辑写的足够强壮的话,就不会出现服务器那边操作失败的情况了,这样我们就可以让客户端这里马上得到效果,无需等待。

结构

关于安装和使用在这里就不详细说了,首先介绍一下它的目录结构吧
meteor框架学习心得
这个就是Meteor1.04版本的一个目录结构,newmyapp就是我的项目的名称,.meteor就是他的核心文件,这个我们一般不用去管它,剩下的就是一个client和一个server的文件夹和一个package.json和git的忽略文件(里面就是忽略掉node_modules的文件,这个是我在这里可能使用了npm来安转Node.js的包?),就那么简单,client文件夹放客服端的文件,server文件夹放服务器的文件。不过我们在实际项目中,是会不断的添加目录的。我的项目目录就是下面这样的
meteor框架学习心得

可以看到多了好几个文件夹,我们一个一个来说

meteor 加载文件步奏
在 /lib 中的文件优先加载
所有以 main.* 命名的都在其他文件载入后载入
其他文件以字母顺序载入

  • collection
    这个文件夹里面存放着 Meteor 集合,《集合是一个特殊的数据结构,它将你的数据存储到持久的、服务器端的 MongoDB 数据库中,并且与每一个连接的用户浏览器进行实时地同步》这一段是来自官方的介绍,看起来是不是不知道在说什么,其实简单理解就是,这里保存了一些数据库的资源合集和特定数据库操作的方法和数据库操作规则,上代码吧 ./collections/posts.js

//这里就是所谓的集合,我们在这里定义了这个Posts就是一个对于数据库posts表的资源链接,这样我们就可以在客服端和服务器那边操作这个posts表了
Posts = new Mongo.Collection('posts');

//这里就是对于操作数据库自定义方法的一些限制,我们总不可能让其为所欲为
Posts.allow({
    update: function(userId, post) {
        // 只允许修改自己的文章
        return ownsDocument(userId, post);
    },
    remove: function(userId, post) {
        // 只允许修改自己的文章
        return ownsDocument(userId, post);
    }
});
Posts.deny({
    update: function(userId, post, fieldNames, modifier) {
        // 需要完成修改的时候不允许修改为已经存在的url
        var rs = Posts.findOne({ url: modifier.$set.url, _id: { $ne: post._id } });
        if (rs) {
            return true;
        }
        return (_.without(fieldNames, 'url', 'title').length > 0);
    }
});
Posts.deny({
    update: function(userId, post, fieldNames, modifier) {
        var errors = validatePost(modifier.$set);
        return errors.title || errors.url;
    }
});

validatePost = function(post) {
    var errors = {};
    if (!post.title)
        errors.title = "请填写标题";
    if (!post.url)
        errors.url = "请填写URL";
    return errors;
}

//还可以写一些自定义的操作
Meteor.methods({
    postInsert: function(postAttributes) {
        check(this.userId, String);
        check(postAttributes, {
            title: String,
            url: String
        });

        var errors = validatePost(postAttributes);
        if (errors.title || errors.url)
            throw new Meteor.Error('invalid-post', "你必须为你的帖子填写标题和 URL");


        var postWithSameLink = Posts.findOne({ url: postAttributes.url });
        if (postWithSameLink) {
            return {
                postExists: true,
                _id: postWithSameLink._id
            }
        }

        var user = Meteor.user();
        var post = _.extend(postAttributes, {
            userId: user._id,
            author: user.username,
            submitted: new Date(),
            commentsCount: 0,
            upvoters: [],
            votes: 0
        });

        var postId = Posts.insert(post);

        return {
            _id: postId
        };
    },
    upvote: function(postId) {
        // 检查数据类型
        check(this.userId, String);
        check(postId, String);

        var affected = Posts.update({
            _id: postId,
            upvoters: {$ne: this.userId}
        },{
            $addToSet: {upvoters: this.userId},
            $inc: {votes: 1}
        });
        if (!affected) 
            throw new Meteor.Error('invalid', "You weren't able to upvote that post");
    }
});

整个代码看下来,如果之前你有了解过MVC的话,你应该会有感觉,这个和M层太像了,是的,我就简单理解为M层.

  • server 服务器文件夹
    在这里我们先要理解两个个概念,发布(Publication)和订阅(Subscription).发布是什么呢,一个 App 的数据库可能用上万条数据,其中一些还可能是私用和保密敏感数据。显而易见我们不能简单地把数据库镜像到客户端去,无论是安全原因还是扩展性原因。
    所以我们需要告诉 Meteor 那些数据子集是需要送到客户端,我们将用发布功能来做这个事儿。
    在这个文件夹里面,我们就做了发布这件事,代码
    ./server/publications.js
//这是一个标准的发布,需要哪些内容,和订阅的时候需要遵守的规定
Meteor.publish('posts', function(options) {
    check(options, {
    //必须为什么什么结构
        sort: Object,
        limit: Number
    });
    return Posts.find({}, options);
});

Meteor.publish('singlePost', function(id) {
    check(id, String)
    //单条
    return Posts.find(id);
});

Meteor.publish('comments', function(postId) {
    check(postId, String);
    //需要的不是全部,而是postId
    return Comments.find({ postId: postId });
});

Meteor.publish('notifications', function() {
    return Notifications.find({ userId: this.userId, read: false });
})

在这个文件夹里面还在一个main.js文件,项目初始化生成的,里面是这样的,其实就是服务器的需要用的一些包的引入
./server/main.js

import { Meteor } from 'meteor/meteor';

Meteor.startup(() => { // code to run on server at startup }); 

还有一个比较特别的文件,是用来做数据初始化的,也就是写一些数据来测试
./server/publications.js

if (Posts.find().count() === 0) {
    var now = new Date().getTime();
    // create two users
    var tomId = Meteor.users.insert({
        profile: { name: 'Tom Coleman' }
    });
    var tom = Meteor.users.findOne(tomId);
    var sachaId = Meteor.users.insert({
        profile: { name: 'Sacha Greif' }
    });
    var sacha = Meteor.users.findOne(sachaId);
    var telescopeId = Posts.insert({
        title: 'Introducing Telescope',
        userId: sacha._id,
        author: sacha.profile.name,
        url: 'http://sachagreif.com/introducing-telescope/',
        submitted: new Date(now - 7 * 3600 * 1000),
        commentsCount: 2,
        upvoters: [],
        votes: 0
    });
    Comments.insert({
        postId: telescopeId,
        userId: tom._id,
        author: tom.profile.name,
        submitted: new Date(now - 5 * 3600 * 1000),
        body: 'Interesting project Sacha, can I get involved?'
    });
    Comments.insert({
        postId: telescopeId,
        userId: sacha._id,
        author: sacha.profile.name,
        submitted: new Date(now - 3 * 3600 * 1000),
        body: 'You sure can Tom!'
    });
    Posts.insert({
        title: 'Meteor',
        userId: tom._id,
        author: tom.profile.name,
        url: 'http://meteor.com',
        submitted: new Date(now - 10 * 3600 * 1000),
        commentsCount: 0,
        upvoters: [],
        votes: 0
    });
    Posts.insert({
        title: 'The Meteor Book',
        userId: tom._id,
        author: tom.profile.name,
        url: 'http://themeteorbook.com',
        submitted: new Date(now - 12 * 3600 * 1000),
        commentsCount: 0,
        upvoters: [],
        votes: 0
    });
    for (var i = 0; i < 10; i++) {
        Posts.insert({
            title: 'Test post #' + i,
            author: sacha.profile.name,
            userId: sacha._id,
            url: 'http://google.com/?q=test-' + i,
            submitted: new Date(now - i * 3600 * 1000 + 1),
            commentsCount: 0,
            upvoters: [],
            votes: 0
        });
    }
}

这个完全是不需要的,项目正常以后可以删除掉.
这里需要说明一下,其实我们项目在一开始的时候,是可以自动发布的,就是只要是集合里面的资源,全部自动发布。这样是不是比较好呢,是方便了,但是安全性不好,什么数据客户端那边都可以操作了,而且也浪费资源,我不是所有数据都需要的,那么要怎么做呢,就是删除这个自动发布的包
autopublish

//在命令行
meteor remove autopublish
  • client 文件夹
    上面提到了发布和订阅,但是只说了发布没有说订阅,因为订阅是在客户端做的,但是客户端做了更多事情
    meteor框架学习心得
    里面有很多子文件夹,其实,meteor是不会去管你client的文件夹的,你文件随便怎么放都是可以的,他有一套机制会全部拿过来。自动识别和生成,但是我们为了自己可以更具清晰和开发方便而建了那么多文件夹
    例如helper这个文件夹,我们就放一些例如服务器的配置,这里说的配置是例如某个包是怎么配置
    ./client/helpers/config.js
//用户登录的包。只需要帐号
Accounts.ui.config({
  passwordSignupFields: 'USERNAME_ONLY' }); 

./client/spacebars.js

这里可以定义一些公共的方法,用于给模块里面使用

Template.registerHelper('pluralize', function(n, thing) {
    if (n === 1) {
        return '1 ' + thing;
    } else {
        return n + ' ' + thing + 's';
    }
})

使用

<p> //传两个参数 {{pluralize votes "Vote"}}, submitted by {{author}}, <a href="{{pathFor 'postPage'}}">{{pluralize commentsCount "comment"}}</a> {{#if ownPost}} <a href="{{pathFor 'postEdit'}}">Edit</a> {{/if}} </p>

stylesheets文件夹里面可以放一些css的资源
templates这个文件夹是比较重要的,我们会在这边做订阅和写html,其实真正的客户端内容都是在这里写,不过这里要先介绍一下lib的文件夹

  • lib 文件夹

    在项目载入的时候,这个文件夹会先载入,里面可以做路由和一些预定义的方法,在这里放我自己的路由文件

./lib/router.js


//路由配置
Router.configure({
    //默认模板
    layoutTemplate: 'layout',
    //载入等待的时候显示的模板
    loadingTemplate: 'loading',
    //404模板
    notFoundTemplate: 'notFound',
    //等待载入订阅这个以后再展示
    waitOn: function() {
        return [Meteor.subscribe('notifications')];

    }
});


//定义了一个controller
PostsListController = RouteController.extend({
    //默认模板
    template: 'postsList',
    increment: 5,
    // 需要获取的post
    postsLimit: function() {
        return parseInt(this.params.postsLimit) || this.increment;
    },
    findOptions: function() {
        return { sort: this.sort, limit: this.postsLimit() };
    },
    subscriptions: function() {
        this.postsSub = Meteor.subscribe('posts', this.findOptions());
    },
    posts: function() {
        // 当前页面所有的post
        return Posts.find({}, this.findOptions());
    },
    data: function() {
        // 这里有问题吧
        var hasMore = this.posts().count() === this.postsLimit();
        // 下一个路由的URL为当前页面的postsLimit参数+5
        // var nextPath = this.route.path({ postsLimit: this.postsLimit() + this.increment });
        return {
            posts: this.posts(),
            ready: this.postsSub.ready,
            nextPath: hasMore ? this.nextPath() : null
        };
    }
});

// NewPost的控制器,继承PostsList,然后定义一些特有的属性
NewPostsController =  PostsListController.extend({
    sort: {submitted: -1,  _id: -1},
    nextPath: function() {
        return Router.routes.newPosts.path({postsLimit: this.postsLimit() + this.increment})
    }
});
// BestPost的控制器,继承PostsList,然后定义一些特有的属性
BestPostsController = PostsListController.extend({
    sort: {votes: -1,submitted: -1,  _id: -1},
    nextPath: function() {
        return Router.routes.bestPosts.path({postsLimit: this.postsLimit() + this.increment})
    }
});

//默认的路由,关于路由,如果我们的路由没有指定controller(定义一个或者继承一个),那么里面的name属性就是去找的模板名了,必须有对于的模板
Router.route('/', {name: 'home', controller: NewPostsController});
Router.route('/new/:postsLimit?', {name: 'newPosts'});
Router.route('/best/:postsLimit?', {name: 'bestPosts'});

Router.route('/posts/:_id', {
    name: 'postPage',
    waitOn: function() {
        return [
            Meteor.subscribe('singlePost', this.params._id),
            Meteor.subscribe('comments', this.params._id),
        ];
    },
    data: function() {
        return Posts.findOne(this.params._id);
    }
});

Router.route('/posts/:_id/edit', {
    name: 'postEdit',
    waitOn: function() {
        return Meteor.subscribe('singlePost', this.params._id);
    },
    data: function() {
        return Posts.findOne(this.params._id); }
});



Router.route('/submit', { name: 'postSubmit' });

// 也可以在这里定义一些方法
var requireLogin = function() {
    if (!Meteor.user()) {
        if (Meteor.loggingIn()) {
            this.render(this.loadingTemplate);
        } else {
            this.render('accessDenied');
        }
    } else {
        this.next();
    }
}

Router.onBeforeAction('dataNotFound', { only: 'postPage' });

Router.onBeforeAction(requireLogin, { only: 'postSubmit' });

接下来就可以继续看server里面的文件了,我们其实只需要拿其中一个来解释就差不多了
./clinet/notifications.html

<template name="notifications">
    <a href="#" class="dropdown-toggle" data-toggle="dropdown">
        Notifications
        {{#if notificationCount}}
            <span class="badge badge-inverse">{{notificationCount}}</span>
        {{/if}}
        <b class="caret"></b>
    </a> {{#if notificationCount}}
    <ul class="notification dropdown-menu">
        {{#each notifications}} {{> notificationItem}} {{/each}}
    </ul>
    {{else}} {{/if}}
</template>
<template name="notificationItem">
    <li>
        <a href="{{notificationPostPath}}">
            <strong>{{commenterName}}</strong> commented on your post
        </a>
    </li>
</template>

首先,文件名字什么的不重要,meteor会根据这个templat的name属性来找.也就是前面路由的名字和这里的name是对应的,里面用的是Spacebars的模板语言,也就是{{}},两个括号里面的就是一些属性或者一些方法,甚至是引入的其他模块文件,那么这些属性是怎么来的呢,从同名的js文件来和路由里面的controller来的,还可以在一个文件里面定义两个template
./clinet/notifications.js

Template.notifications.helpers({
    notifications: function() {
        //还没有被读取的
        return Notifications.find({ userId: Meteor.userId(), read: false });
    },
    notificationCount: function() {
        return Notifications.find({ userId: Meteor.userId(), read: false }).count();
    }
});

Template.notificationItem.helpers({
    notificationPostPath: function() {
        return Router.routes.postPage.path({ _id: this.postId });
    }
});

Template.notificationItem.events({
    'click a': function() {
        Notifications.update(this._id, { $set: { read: true } });
    }
})

到这里,其实就可以写一个简单的meteor的应用了,不过其实meteor还有很多特性,没有在这篇blog里面提及。
怎么提示错误,怎么操作会话(Session),响应式,延时补偿等等
以后如果有动力了就再写吧。不过这个框架给我的感觉就是很超前,可以做的事情是很多的,建站是很快

项目github地址