姚翔的部落格

如何部署一个Node.js项目

| Comments

工作中的项目需要一个简单的Web服务器,考虑到Node.js快捷方便的特性就选择它来搭建。在开发阶段,使用node命令就能很快速地启动服务器来调试,但是到实际部署阶段,就要考虑把它放到一个单独的后台进程去运行,同时一般还会需要Nginx来做反向代理,所以在Google上搜了一通后选择了Nginx + Upstart + Node的组合方式。这里我就大致记录下自己的配置步骤,以供像我一样初次部署的生手参考。(注:我们用的是ubuntu的部署系统,所以以下所有命令都是以ubuntu上的形式给出)

首先是安装各种必要的程序:Nginx, Node, Vi等,这里就不说明了。安装完毕后,我们先单独创建一个node用户来跑所有的node projects:

1
2
3
4
5
6
7
8
sudo adduser \\
    --system \\
    --shell /bin/bash \\
    --gecos 'user for running node.js projects' \\
    --group \\
    --disabled-password \\
    --home /home/node \\
    node

接下来便是配置一段Upstart的脚本来启动我们的Node应用,Ubuntu下Upstart的脚本默认是放在/etc/init下的,你可以创建一个和自己项目有关的myapp.conf文件放在下面,里面的内容如下(请把对应的文件目录和js文件替换成适合你自己项目的值):

1
2
3
4
5
6
7
8
9
10
11
12
description "XXX node server"
author  "Sherlock Yao"

start on (local-filesystems and net-device-up IFACE=eth0)
stop on shutdown

respawn

script
        cd /var/local/sites/myapp
        exec sudo -u node NODE_ENV=production /usr/local/bin/node /var/local/sites/myapp/express.js >> /var/log/myapp.log 2>&1
end script

然后便是把你要部署的代码放到服务器上,我们是用Git管理的,所以直接clone下来,这里要注意的就是一定要把文件的权限附给node用户,包括日志文件:

1
2
3
4
5
6
7
sudo mkdir -p /var/local/sites/myapp
sudo chown node /var/local/sites/myapp
cd /var/local/sites/myapp
sudo -u node git clone /path/to/myapp.git

sudo touch /var/log/myapp.log
sudo chown node /var/log/myapp.log

最后添加Nginx的反向代理,配置文件(/etc/nginx/sites-available/myapp)内容如下,记得把myapp替换成适合你自己情况的名字:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#添加完成后用"sudo service nginx restart"来重启nginx

upstream myapp {
    server 127.0.0.1:3000;
}

server {
    listen 80;
    server_name myapp.com; #这里写上你自己需要的域名或ip
    access_log /var/log/nginx/myapp.log;

    # pass the request to the node.js server with the correct headers and much more can be added, see nginx config options
    location / {
      proxy_set_header X-Real-IP $remote_addr;
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header Host $http_host;
      proxy_set_header X-NginX-Proxy true;

      proxy_pass http://myapp/;
      proxy_redirect off;
    }
 }

一切配置妥当后,就可以用下面的命令开启、查看、停止你的应用了:

1
2
3
sudo start myapp
sudo status myapp
sudo stop myapp

另外附上一篇我参考的英文博文,里面有更详尽的说明。

使用MagicalRecord时做Migration的注意事项

| Comments

MagicalRecord是一个非常好用的工具,让我们更方便地使用Core Data,同时代码上也能变得更简洁。但是简单易用的代价就是如果你不清楚它具体的实现机制时,当某些复杂情况出现后,产生一些“莫名其妙”的问题而导致手足无措。所以如果对Core Data本身不太了解的开发者,我建议还是不要用这个工具,等到对其机制有一定了解后再慢慢把它引入自己的代码。

今天要讲的是通过一个自己的实际经历来说明一些使用MagicalRecord(以下简称MR)的注意点。首先我遇到的我问题是在对已有数据结构做调整时,我需要使用Core Data支持的Light Migration功能,按照它的要求添加了新版本的model文件后,发现期待的数据顺利迁移没有出现,出错了!

在这里插一句,如果你不关心用户以前存在数据库的数据是否丢失(就是这么拽),只想确保任何时候都不会出错,那可以在配置MR前加上下面这句代码,它会确保在数据库文件和model文件不匹配的情况下直接删除数据:

1
[MagicalRecord setShouldDeleteStoreOnModelMismatch:YES];

本着用户至上的想法,我还是想找方法解决这个问题的,通过一番搜索后了解到在创建store的时候会用到一个option参数,MagicalRecord的代码是默认是如下的:

1
2
NSMutableDictionary *sqliteOptions = [NSMutableDictionary dictionary];
[sqliteOptions setObject:@"WAL" forKey:@"journal_mode"];

这里的journal_mode似乎指的是数据库记录下了操作日志文件,而这个文件在迁移的时候也会被同时迁移,但是这种模式下会报错(笔者没有深入看这部分的资料,所以阐述有可能有问题,请仅做参考),总之一些帖子里面的意见就是改成DELETE值:

1
[sqliteOptions setObject:@"DELETE" forKey:@"journal_mode"];

我试过之后没起作用,说明问题不在这里(笔者下文说明了当时这个出错场景的原因,但是并没有回头去验证journal mode这个参数是否会造成migration失败,所以希望有兴趣的读者自己去深入研究),那问题在哪里呢?在仔细读了MR的源代码后发现了一些端倪,我们一般使用MR时初始化只会写类似以下的这样一句代码:

1
[MagicalRecord setupCoreDataStackWithAutoMigratingSqliteStoreNamed:@"XXX.sqlite"];

它帮我们封装了所以初始化Core Data的方法,所以很多地方它使用了一些默认值,我们来看其中一个方法:

1
2
3
4
+ (NSManagedObjectModel *) MR_mergedObjectModelFromMainBundle;
{
    return [self mergedModelFromBundles:nil];
}

在没有OjbectModel的时候,MR会默认调用这个方法来创建,问题就出在这里啦,原来我的项目当中有用到XMPP的一个库,它本身自己就带了好几个data model的定义文件,而MR在创建时使用的merge的方法把所有model都合并到了一起,这就造成以前的数据库文件里的model hash code永远都无法和新的匹配,就算我提供了所有的model版本文件。那如何解决呢?好在MR设计地很灵活,我们可以通过一下方法来配置它:

1
2
3
4
[MagicalRecord setShouldAutoCreateManagedObjectModel:NO];
NSURL *modelURL = [[NSBundle mainBundle] URLForResource:@"XXX" withExtension:@"momd"];
NSManagedObjectModel *objectModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL];
[NSManagedObjectModel MR_setDefaultManagedObjectModel:objectModel];

可是就算这样我还是无法解决问题,因为用户手机里的老数据用的还是merge了的model hash值,所以就算我这样做老数据还是无法迁移了,只能保证这个版本以后新版本就可以支持迁移了。这是一次惨痛的教训啊!所以强烈建议所有使用MR的工程师们,在正式的产品级项目里,初始化MR的时候就用以上这段繁琐的代码吧,不要用一句初始化的方法了。

VIPER实践(下)

| Comments

上一篇文章我们设计好了View和Interactor,现在我们来做Presenter。

Presenter在这个用例里面的主要作用就是接受View提交过来的“提问”请求,然后向Interactor获取“答案”,最后把结果更新到View上,以下是实现部分的代码片段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
    @interface HumanComputerCommunicatePresenter : NSObject <AIRobotInteractorDelegate>

    @property (nonatomic, weak) id<ProgressViewInterface> progressView;
    @property (nonatomic, weak) id<MessageBoardViewInterface> messageBoard;
    @property (nonatomic, strong) AIRobotInteractor *aiRobotInteractor;

    - (void)wantSendMessage:(NSString *)message;

    @end


    @implementation HumanComputerCommunicatePresenter

    - (void)wantSendMessage:(NSString *)message {
      [self.progressView beginProgress];
      [self.aiRobotInteractor askQuestion:message]
    }

    - (void)aiRobotInteractor:(AIRobotInteractor *)aiRobotInteractor didResponseToQuestion:(NSString *)question response:(NSString *)response {
      [self.progressView endProgress];
      [self.messageBoard showMessage:response];
    }

    @end

可以发现,Presenter就是承担了一个中间者的角色,它才是传统意义上MVC机构中的C(Controller)。其中有一点要注意的是,对应ViewInterface的reference,我们采用了weak的定义来防止Strong reference cycle ,在我自己的设计当中,我偏向于如下的reference关系:

  • View –strong-> Presenter
  • Presenter –strong-> Interactor
  • Presenter –weak-> View

最后我们来看一下Routing,objc.io那片文章中的Routing做的很散,通过多个wireframe来实现,我个人的设计中偏向用一个大的wireframe结合Assembling Factory来实现Routing,废话不多说看代码比较直观,以下只是我个人目前的一种实现方案,仅供参考:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
@implementation AssemblingFactory

+ (UIViewController *)assembleChatroomView {
  ChatroomViewController *viewController = [[UIStoryboard genericStoryboard] instantiateViewControllerWithIdentifier:ChatroomViewIdentifier];
  HumanComputerCommunicatePresenter *presenter = [HumanComputerCommunicatePresenter new];
  AIRobotInteractor *interactor = [AIRobotInteractor new];

  viewController.presenter = presenter;
  presenter.progressView = viewController;
  presneter.messageBoard = viewController
  presenter.interactor = interactor;
  interactor.delegate = presenter;

  return viewController;
}

@end


@implementation Wireframe

+ (void)moveToNextPageOfViewController:(UIViewController *)viewController messenger:(PageMessenger *)messenger {
  SEL selector = [self selectorOfClass:[viewController class] messengerName:[messenger name]];
  IMP imp = [[self class] methodForSelector:selector];
  void (*func)(id, SEL, UIViewController*, NSDictionary*) = (void *)imp;
  func([self class], selector, viewController, [messenger params]);
}

+ (SEL)selectorOfClass:(Class)class messengerName:(NSString *)messengerName {
  static NSDictionary *selectorMap = nil;
  if (!selectorMap) {

    selectorMap = @{
                    @"SplashViewControllerDefault" : [NSValue valueWithPointer:@selector(moveToChatroomViewController:params:)]
                    // add more nav configuration here...
                    };
  }
  NSValue *value = [selectorMap valueForKey:[[class description] conj:messengerName]];
  return value ? [value pointerValue] : @selector(emptyMove:params:);
}

+ (void)moveToChatroomViewController:(UIViewController *)viewController params:(NSDictionary *)params {
  UIViewController *viewController = [AssemblingFactory assembleChatroomView];
  [(BaseViewController *)viewController setParams:params]; // I defined a base view controller to allow pass params
  [viewController.navigationController pushViewController:viewController animated:YES];
}

@end

以上大致就是我个人目前实践VIPER的一些心得,肯定非常不成熟,有很多地方可以改进,只想抛砖引玉让大家广思集益共同改进我们代码的架构方案,从而最终帮助提高开发效率。

VIPER实践(上)

| Comments

好的代码质量对于开发效率的提升可谓是指数级别的,好的代码架构则像是地基一样,是一切的前提。在iOS代码架构中讨论最多的对象就是ViewController了,传统的开发很容易产生其臃肿冗长的结果,我们很多人都为如何给它减肥伤透脑筋,无数前辈们探索出了各种不同的方案都值得我们学习参考。今天要说的VIPER架构就是较新的一次尝试,我是通过objc.io的文章第一次知道到它,具体链接这里,虽然文章没有非常深入这个主题,给出的示例代码个人认为还存在些缺陷,但它整个理念正好吻合我之前一直在构思的方案,而且给了我一个非常系统的思路,于是我直接把它应用到了一个新的项目中去,在实践中去深入体会其优缺点,同时不断根据自己的理解改进,下面就通过一个假想的App作为示例来说明一下我自己的理解。

这个假想的App功能非常简单,类似一个简化版的Siri,用户在界面上输入一些文本后提交,应用就会返回一些回复的信息给你。首先在开始前我想说明一点,VIPER架构的核心思想就是把传统庞大的应用结构解构成View, interactor, Presenter, Entity, Routing五个部分,既然是做了解耦合,那各个部分直接应该做到相应的灵活和可重用,这点非常重要,我们在设计View,Presenter和Interactor时要时刻记住这一点。在objc.io原文章的示例代码中,这点做的就不够好,而在个人的实践中就更容易陷在这里,你会发现你给一个特定的View写了一个Presenter然后这个Presenter用到了一个特定的Interactor,这三个文件所做的就是应用中的一个特定的业务逻辑,所以也没办法被其他地方重用,其结果就是你只是简单的把原来属于一个ViewController的代码分散放到三个类中了,本质上却没有解耦合。请大家在看下面内容的时候时刻牢记这一点。同时示例我只提供了设计思路,并不提供实现的代码,因为示例的目的就是为了帮助理解VIPER的设计思路。

首先我们从View入手,界面上其实很简单:用户输入文本 -> 提交 -> 显示回应文本。我们想要的presenter对view的操作会有 1)显示/隐藏等待框 2)显示回应的文本两个主要的部分,接下来我们就要解耦合了。等待框和文本显示其实是不相关的两部分逻辑,所以我们的View Interface也应该要分开来,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 用于显示和隐藏等待框
@protocol ProgressViewInterface <NSObject>

- (void)beginProgress;
- (void)endProgress;

@end

// 用于显示回应文本
@protocol MessageBoardViewInterface <NSObject>

- (void)showMessage:(NSString *)message;

@end

Prsenter是“VIP”结构中的中间件,起着承上启下的作用,所以我们放到下一篇文章里写,接下来我们来设计Interactor。Interactor在这里就只有一个功能,就是一个智能AI的角色。一般来说,interactor是一部分具体业务逻辑的实现者,虽然我们提倡对接口编程,但是这些实现一般一个应用就只会有一份,同时像Kiwi这种测试框架已经提供了良好的mock和stub支持,所以我们就不再对interactor设计接口,而是直接实现,其.h文件大致如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@interface AIRobotInteractor : NSObject

@property (nonatomic, weak) id<CAIRobotInteractorDelegate> delegate;

- (void)askQuestion:(NSString *)question;

@end


@protocol AIRobotInteractorDelegate <NSObject>

@optional
- (void)aiRobotInteractor:(AIRobotInteractor *)aiRobotInteractor didResponseToQuestion:(NSString *)question response:(NSString *)response;

@end

注意,这里我们使用了Delegate作为和presenter的交互方式,但不是一定要用,具体可以通过实际需求来设计交互形式。同时在类的命名上我故意使用了一些广泛的名字,目的就是为了突出解耦这个概念,要知道这些view interface和interactor可以在任何需要的地方使用而不是只局限在当前这个view controller。

好了,下一篇我将具体说Presenter和Routing。

如何更新Storyboard的多国语言strings文件

| Comments

目前XCode已经提供了很好的国际化集成,使你很容易就可以让应用支持国际化,就连Storyboard里面的文字也可以自动提取出来让工程师配置对应的多国文字。今天要讲的是当一个Storyboard文件已经本地化了以后,又有了新的需求而进行了改动,这时该如何去更新多国语言文件。

首先,如果你还不清楚如何国际化storyboard文件,那请点击这里

当一个storyboard文件被localize了以后,在XCode右边的文件查看窗口就可以看到对应的语言列表,它们前面有一个checkbox,如果勾选的话就表示那种语言有对应的strings文件。因为XCode只支持一次性生成所有的storyboard的localize文件,所以当每次更新storyboard后想更新语言文件就变得非常麻烦。

File Inspector

我这里提供一个实践中学到的比较好的办法,就是首先你先备份一份你想要更新的语言文件,然后在右边的文件窗口把对应的那个勾选项取消掉,接着再选上,这个时候XCode就会重新生成一份新的文件。我们要做的就是把老文件里已经写好的对应关系复制到新文件上,再把新的元素的文字补上就可以了。

UIScrollView在Autolayout下的实践经验

| Comments

平常在我们使用UIScrollView的场景中,经常会有需求给它增加多个子元素,同时又需要有对应的布局。Autolayout对布局灵活性支持的提升是显而易见的,所以我们当然希望在Scroll View里也能使用约束(Constraint)来进行布局,不过在实践中Autolayout的引入往往会带来一些意外的问题,今天我就介绍一种比较好的实现方法来规避一些不必要的麻烦。

首先我们在Scroll View下面加一层额外的UIView(以下称为View A),同时确保这个UIView是ScrollView的唯一子View,所以我们需要的其他页面元素都应该放在A下面,这个A的作用就是更方便我们来控制ScrollView的内容长度。

接下来我们就要添加A和ScrollView之间的约束了:

  • 我们需要给A添加Width和Height的约束,用一个写死的值(或者用placeholder约束,然后在代码里动态添加约束,这样可以做到动态控制内容的大小),这样做的目的就是确定A的内容大小。

  • 同时我们也需要添加A到Scroll View的top,right,bottom和left的约束,这些约束的目的是为了能让Scroll View计算出它的content size,这些约束不会改变A的大小,其实Scroll View就是通过A的大小和这些周边的距离值来确定其内容大小的。

做完上面两步后,我们就可以像平常一样设计我们的布局,添加逻辑代码了。当然这不能规避所有问题,但是通过这个方案可以从概念上分离内容大小和显示区域大小,从而简化我们的工作。

想深入了解的话可以点这里参考文章

初识Docker

| Comments

几天前通过同事知道了Docker这个工具,今天在工作的空暇时间打开了它的官网想进一步了解一下它,在完成了一个快速的练习教程后,惊奇地发现其优秀的功能,但却混乱了我的思维。弄清以及理解其原理一直我学新东西的方法,当然也是一种束缚,以致我在不清楚其原理前始终都会不得使用的要领,所以这一次我又开始了漫漫Google之路,想弄明白这个工具的概念到底是什么。

结果不太理想,因为它的底层技术涉及到LXC和AUFS这些我完全不了解的东西,所以在我写这篇文章的时候,脑中还处于一片迷雾之中。但我略微看清了一些它的轮廓,所以我想用自己的语言来描述一下我所见到的景象,以供那些和我一样初次接触Docker的人一些参考。

首先让我把平时我们的开发工作比喻成一次表演(发布)的排练过程,有表演就涉及到舞台(运行环境),我们肯定希望最终的表演舞台同我们排练时使用的是相似的,这样才会达到最好的表演效果。但实际情况是,各个地点的场地都不一样,这样布置舞台(配置环境)就成为一个非常繁琐和痛苦的工作。

Docker为我们提供了什么呢?它带来一个大大的集装箱。它告诉演出者(开发人员),让他们在这个集装箱的空间里进行排练,同时保证到时候所有演出场地都会有一个一模一样的集装箱场地。Docker又跑到各个场地,告诉那些承办方它将要放一个集装箱在场地上作为舞台,尺寸是多长多宽多高,他们要做的就是到时候腾出一块能放这个集装箱的空间就可以了。

听起来似乎一切问题都解决了,但是演出者有些不满意,这个集装箱空空荡荡的,完全不像个舞台呀。没问题,Docker拿出了一本册子(Docker Hub),里面有许许多多舞台模板,都是根据这个集装箱设计的,而且还有其他乐队贡献的他们设计的模板,完全不用担心。演出者们挑了好久,终于选择了一款自己喜欢的,但是仍然觉得不满意。这也没问题,你们可以自己再二次改造啊,只要把你们需要装修的步骤(Dockerfile)写下来就可以了。于是Docker把模板的布置步骤再加上演出者后期改进的装修步骤混合成一份新的施工说明书交给了各地的承办方。

一个个集装箱被运到了各个场地,工人们根据施工说明造出了一个个一模一样的集装箱舞台,和演出者们排练时用的完全一样。这下就真的不用担心演出效果和排练时不一样了。