分类存档: Web

使用 ukraine 建设 node.js 私有云

源由

node.js 越来越流行,托管 node.js 应用的云服务也越来越多,例如 nodejitsuheroku 等。

但是这些云服务通常有这样那样的限制,又或者是要收费的。而有些时候我通常不需要跑很大的应用,或者是很稳定的应用,只是为了跑一些小的,或者是学习用的 node.js 应用,并且我也有自己的 VPS,想把这些应用托管在自己的服务器上。

于是我需要去找一个可以在自己的 VPS 上建设一个 node.js 私有云的软件。

比较

在看了 中的 DIY Platforms 后,尝试了一下其中介绍的平台:

  • nodester: 安装比较麻烦,不支持新版本的 nodejs,安装说明还是针对 node 0.4.11 的
  • CloudFoundry: 比较庞大,而且是以 vm 方式安装,不适合 VPS
  • OpenShift: 同 CloudFoundry,不只支持 node.js,安装复杂,不适合 VPS
  • Nodejitsu: Nodejitsu 开源了他们所用的 node.js 应用管理项目 haibu,haibu 安装比较简单,而且支持最新的 nodejs 0.8.16,不过 Nodejitsu 同样开源的命令行客户端 jitsu 并不支持 haibu
  • Stagecoach: 文档不够清晰,看了很久也没明白它的架构和怎么部署⋯⋯

这样看来似乎没有一个可以满足我的需要,不过 GitHub 是强大的,通过搜索找到了 ukraine 这个项目:

ukraine glues haibu and node-http-proxy adding a little helper, chernobyl, that deploys into this cloud. It is probably as stable as you think it is.

这就是我想要的。

修改

原始的 ukraine 虽然已经基本满足了我的需要,但是还有一些小的功能需要增加:

  1. 使用 nginx 作为前端,这样 node.js 应用可以部署在 nginx 后面,与 PHP 等项目并存
  2. 使用 SSL 保护 haibu 的服务端,防止 auth_token 因为不加密的 HTTP 通信而泄漏
  3. 因为使用 nginx 作为前端,所以 haibu 服务端和 node-http-proxy 都不需要监听 0.0.0.0,而只需要监听 127.0.0.1
  4. 防止 node.js 应用监听了常用端口而导致其他应用启动失败,因为使用了 nginx 作为前端,node.js 应用本身监听了什么端口就不重要了
  5. 防止 node.js 应用直接对外提供服务,同样因为已经有 nginx,node.js 应用只需要监听 127.0.0.1 就行了
  6. chernobyl 不支持配置每个不同的 ukraine 监听在哪个端口,以及有没有配置 SSL
  7. 我想 ukraine 作为一个服务存在,这样在 VPS 启动时可以自动启动
  8. node.js 应用需要支持绑定自定义域名,而不是只能绑定子域名

所以我 fork 了 radekstepan 的 ukraine 到 ,并做了一些自己需要的修改。

安装修改后的 ukraine

如果你和我一样,也需要一个这样简单的 node.js 私云,那么以下的内容可以帮助里部署 ukraine 到自己的 VPS 上。

注意:安装教程以在 Ubuntu/Debian 上为例,并且所有命令是以 root 用户执行。

1. 安装 node.js

haibu 需要 node.js 的版本大于 0.8,所以需要安装最新的 node.js 包,或者自行编译安装。

参考这篇文章:Installing Node.js via package manager

2. 安装 forever

forever 是用来维持 ukraine 一直在启动状态

npm install forever -g

3. 配置 nodejs 用户

为了使所有 node.js 应用不使用 root 权限运行,防止出现权限方便的风险,需要添加一个用户 nodejs 来运行 node.js 应用。

groupadd nodejs
useradd -g nodejs -m -s /bin/bash nodejs

4. 获取并安装 ukraine

为了管理方便,这里安装 ukraine 到 /srv/ukraine 中,如果你不安装在这个位置,那么相关的脚本和配置文件都需要修改。

cd /srv
git clone https://github.com/ohdarling/ukraine
cd ukraine
git checkout private-cloud
npm install
chown -R nodejs.nodejs /srv/ukraine

5. 配置 ukraine

cd /srv/ukraine
cp config.example.json config.json
vim config.json

为了安全起见,建议 auth_token 不要留空。

example.com 需要替换为你自己的域名,这样以后部署了 node.js 应用时,会自动分配一个 package-name.example.com 的子域名。

6. 安装服务脚本

注意:这个脚本只适用于 Ubuntu/Debian。

cd /srv/ukraine
cp server/init-script/ukraine /etc/init.d/
chmod +x /etc/init.d/ukraine

7. 使用 nginx 作为前端服务

为了使 node.js 应用与原有的 PHP 共存,使用 nginx 作为 ukraine 的前端服务。

注意:部署 node.js 应用到 ukraine 需要 nginx 启用 chunkin 模块,默认情况下 nginx 并没有安装此模块,可以自行编译安装(参考 ),或者直接使用包管理器安装 nginx-extras,这个包中包含的 nginx 已经编译了 chunkin 模块。

添加以下配置文件内容到 /etc/nginx/sites-available/ukraine,并且在 /etc/nginx/sites-enabled/ 中添加一个到配置文件的符号链接。注意,需要替换配置文件内容中的 haibu.example.com*.example.com 为你自己的域名。

server {
    listen   80;
    server_name  haibu.example.com;

    access_log  /var/log/nginx/localhost.access.log;

    chunkin on;

    error_page 411 = @my_411_error;
        location @my_411_error {
        chunkin_resume;
    }

    location / {
        proxy_pass http://localhost:9002;
        proxy_set_header  X-Real-IP  $remote_addr;
    }
}

server {
    listen   80;
    server_name  *.example.com;

    access_log  /var/log/nginx/localhost.access.log;

    location / {
        proxy_pass http://localhost:8000;
        proxy_set_header  X-Real-IP  $remote_addr;
        proxy_set_header Host $host;
    }
}

建议在 haibu.example.com 这个站点上启动 SSL 来保护 auth_token。

添加完配置文件后,使用以下命令让 nginx 重新载入配置:

nginx -s reload

8. 启动 ukraine

service ukraine start

9. 检查 ukraine 是否正常运行

打开浏览器,访问 http://haibu.example.com/version,将会看到以下内容:

{"version":"haibu 0.9.7"}

注意,如果在之前已经配置了 auth_token,将会看到:

{"message":"Wrong auth token"}

这说明 ukraine 已经正常启动。

部署自己的 node.js 应用

首先需要在本地安装 ukraine:

npm install -g git://github.com/ohdarling/ukraine\#private-cloud

如果之前配置了 auth_token:

chernobyl config haibu.example.com auth_token=xxxx

如果之前配置了 SSL:

chernobyl config haibu.example.com https=true
chernobyl config haibu.example.com haibu_port=443

现在可以部署 node.js 应用了,进入到 node.js 应用的根目录,运行以下命令:

chernobyl deploy haibu.example.com .

这就会部署这个 node.js 应用到 haibu.example.com 了。

给 node.js 应用绑定自定义域名

在给 node.js 绑定自定义域名,只需要在 package.json 中添加 domains 属性即可:

{
    "name": "example-app",
    "version": "0.0.2",
    "domains": [
        "custom-example.com"
    ]
    "dependencies": {
        "express": "2.5.x"
    },
    "scripts": {
        "start": "server.js"
    }
}

同样需要修改 nginx 的配置文件,把自定义域名加到 server_name 中:

server {
    listen   80;
    server_name  *.example.com, custom-example.com;

    access_log  /var/log/nginx/localhost.access.log;

    location / {
        proxy_pass http://localhost:8000;
        proxy_set_header  X-Real-IP  $remote_addr;
        proxy_set_header Host $host;
    }
}

注意事项

  1. 所有 node.js 应用会监听在一个随机端口,并且会监听在 127.0.0.1,也就意味着在外部没有办法直接访问这个应用
  2. package.json 中 scripts.start 属性,不需要带 node,只需要指定以哪个脚本启动即可,例如以下是错误的:
    {
        "name" : "example-app",
        "scripts" : {
            "start" : "node server.js"
        }
    }
  1. 如果需要 node.js 鉴定特定的端口,并能直接对外服务,可以在 package.json 的 env 属性中添加 “HAIBU_INDEPENDENT_SERVICE”: “true”,例如:
    {
        "name" : "somesocks",
        "scripts" : {
            "start" : "server.js"
        },
        "env" : {
            "HAIBU_INDEPENDENT_SERVICE" : "true"
        }
    }

问题及反馈

你可以在 GitHub 上 fork 这个仓库:

联系我

Twitter: @ohdarling88
Email: ohdarling88 at gmail.com

感谢

参考资料

  1. https://github.com/joyent/node/wiki/Node-Hosting
  2. https://github.com/ohdarling/ukraine
  3. https://github.com/radekstepan/ukraine
  4. https://github.com/nodejitsu/haibu
  5. https://github.com/nodejitsu/haibu-carapace
  6. Installing Node.js via package manager

— EOF —

推开窗户看世界 — Objective-C之外

引子

App Store 如今正风靡世界,许多人都想尝试去自己开发一个 iOS App,不过苹果官方推荐使用的是 Objective-C 这个语言。而 Objective-C 与 C、Java 之类的语言风格差异有些大,短时间并不容易掌握,又或者许多人只是想尝试开发一个可以运行在 iOS 上的程序而已,不想再去额外去学一门新的语言,那这个时候就可以考虑另外的一些技术来开发 iOS App。

在移动开发领域,得益于现在手机的性能越来越好,浏览器的功能越来越强大,JavaScript 也成为一门很流行的程序开发语言,而对应到 iOS 平台上,也有多种使用 JavaScript 来开发 iOS App 的技术。这次要介绍的三种技术之中,就有两种使用了 JavaScript。

另外,许多开发跨平台移动应用开发的解决方案,首选要支持的就是 iOS,因此在 iOS 这个平台上可以看到很多种不使用 Objective-C 去开发 iOS App 的技术,而且其中技术所编写出来的程序,除了可以在 iOS 上运行之外,还可以运行在 Android 以及其他系统平台上。能少写一份代码,何乐而不为呢~

不使用 Objective-C

这次要介绍的是三个不使用 Objective-C 来开发 iOS App 的解决方案:

这三个解决方案各有特点,它们的运行机制也有所不同,至于在开发 iOS App 时选用何种方案,可以根据每种方案不同的特点去选择适合自己的。

Titanium Mobile

Titanium Mobile 是 AppCelerator 推出的一个基于 JavaScript 的跨平台移动应用开发技术。通过 Titanium Mobile,可以直接使用 JavaScript 开发能运行于 iOS 和 Android 平台的应用程序,而代码只需要写一次。

Titanium Mobile 是通过将 JavaScript 代码映射到对应平台的 Native Code,在 Titanium Mobile 中操作某一个 UI 对象,或者是其他对象时,实时上是在操作对应平台中实际的 UI 对象,例如使用 Ti.UI.createView() 创建一个视图,它在 iOS 中运行时会实际对应到 UIView,使用 Ti.UI.createTabGroup() 就会对应到 iOS 中的 UITabBarController。这样做的好处就是,可以直接使用 JavaScript 来创建出原生的 UI,而不需要使用额外的代码来让程序 UI 更像系统的 UI。

另外,AppCelerator 在收购了 Apatna 之后,推出了适用于 Titanium 的 IDE Titanium Studio,通过使用 Titanium Studio,可以很方便的创建项目、带智能提示的编辑器,以及很方便的调试项目。

在 Titanium Studio 中调试 iOS 项目时,可以做到单步调试,也就意味着,可以拥有不弱于使用 Xcode 开发项目的体验。断点、单步、变量查看功能一应具全。如果需要发布到 App Store,也可以很方便的通过图形化界面来打包。

当然,使用 Titanium Studio 也需要配合 Xcode 来使用,必须要在安装了 Xcode 的情况下,才可以使用 Titanium Studio 来调试和测试项目。

在 Titanium Studio 推出之前,Titanium Mobile 是靠一个 Developer Tool 来进行打包等操作的,相对比较繁琐,而有了 Titanium Studio 之后,让一切变得简单,我也是从那时开始关注 Titanium Mobile。

对于前端开发工程师来说,只需要理解了 iOS 开发中的一些概念,就可以使用 Titanium Mobile 来开发一个像模像样的 iOS App。并且 Titanium Mobile 实现了 CommonJS 规范,可以很方便来模块化程序代码。

PhoneGap

PhoneGap 是一个使用 HTML+CSS+JavaScript 来开发移动 App 的解决方案,使用它来开发 App 只需要有 Web 开发基础即可。它在今年10月份被 Adobe 收购了,然后加入到 Apache Software Foundation 进行孵化。

目前 PhoneGap 已经支持了市面上大多数的智能手机平台,其中就有 iOS。

在我看来,PhoneGap 其实只是提供了一个运行于各种智能手机平台的浏览器的壳,通过这个壳,PhoneGap 的 JavaScript 库可以和系统进行沟通,从而实现在 Web 页面中与系统交互的功能。

因为 PhoneGap 只提供了系统功能的 API 调用,而没有提供任何和界面相关的 API,那么界面就只能自己来折腾了。

幸好开源的世界是强大的,已经有了一堆这样的界面库来供我们选择,像 jQTouchjQuery MobileSencha Touch 之类,都提供了类似于 iOS 的界面组件,可以让我们省去许多界面上的编码工作。

使用浏览器来运行 App 的一个坏处就是:慢。因为整个 App 就是一个网页,如果编码不当,整个程序使用起来体验会比较差,和原生的应用会有很大区别。

在上面提到的几个 UI 框架之中,我比较喜欢的是 jQTouch,它只提供了基本 UI 框架和一些视图切换效果,做出来的程序是一个单页面 App,在整个程序的反应速度上会感觉比较好。不过 jQTouch 也缺少一些常用的组件像对话框,而这些在 jQuery Mobile 和 Sencha Touch 之中又有提供。选择使用哪个框架就要看个人喜好了,它们的网站都提供了在线预览功能

其实,如果用不到系统的 API,只是想把自己的程序包装成一个很像 App 的 App 的话,只要用一用 PhoneGap 提供的壳就行了,它的 API 几乎可以不用看,配合一些 UI 框架可以快速的产出一个 iOS App 来。

当然了,PhoneGap 最大的好处就是,几乎跨了几乎所有的主流智能手机平台,这对于初创团队来说,是迅速推出各个平台客户端的一个好方法。另外 PhoneGap 官方网站也提供了一系列配套服务,例如使用 PhoneGap Build 可以直接编译各个平台的程序安装包,而不需要开发者自己在本地配置每个平台的编译环境。

Mono Touch

Mono Touch 是一个使用 C# 来编写跨平台应用的框架,同样支持了 iOS 和 Android 两大平台。

Mono Touch 对于 .NET 程序员来说应该是一个好消息,除了调试,其他的都可以在 Windows 中搞定,调试的时候可以通过在虚拟机里运行 Mac OS X 来解决。Mono Touch 因为是基于 Mono 的,也有个配套的 IDE 可以用:Mono Develop。因为都是 .NET,当然也可以用 Vistual Studio 这个更强大的工具了。

与 Titanium Mobile 和 PhoneGap 不同的是,Mono Touch 如果要编译到设备,或者发布到 App Store 的话,是需要收费的。

另外,因为 Mono Touch 也是编译成可执行代码再部署到设备上的,因此运行速度相对于 PhoneGap 所制作的应用来说,应该会快上一些。

不过 Mono Touch 要收费,并且收费还不是很便宜,可能会影响到它的普及率。

小结

在上面讲的三个解决方案之外,还有其他好多大大小小的移动应用开发解决方案,但是用于 iOS 上的解决方案,主要也就是映射代码、或者是包装的形式了,对于应用而不是游戏类型来说,一般都是够用的,在具体选择的时候,也可以根据自己更为熟悉哪种语言,或者框架来挑选。

如果你发现了其他有意思的移动应用开发技术,也可以和我分享一下 :)

参考资料

  1. http://www.appcelerator.com/products/titanium-mobile-application-development
  2. http://developer.appcelerator.com/apidoc/mobile/latest
  3. http://phonegap.com/about
  4. https://build.phonegap.com
  5. http://www.mono-project.com/Main_Page
  6. http://xamarin.com/monotouch
  7. https://github.com/xamarin/monotouch-samples
  8. http://blog.zhaojie.me/2010/09/develop-ios-app-with-monotouch-in-visual-studio-1.html

— EOF —

使用 node.js + nginx 建设网站

昨天搞定了一个小网站的搭建,用了 node.js,另外为了能在一个 VPS 上搭建多个网站,用了 nginx 作为反向代理。

软件介绍

嗯,从维基上复制了一下~

node.js

Node.js是一个事件驱动I/O伺服端JavaScript环境,基于V8。目的是为了提供撰写可扩充网络程式,如web服务。第一个版本由Ryan Dahl于2009年释出,后来,Joyent雇用了Dahl,并协助发展Node.js。

nginx

nginx(发音同engine x)是一款由俄罗斯程序员Igor Sysoev所开发轻量级的网页服务器、反向代理服务器以及电子邮件(IMAP/POP3)代理服务器。

cluster

在 node.js 0.6.0 之前,有一个第三方的 node.js 模块 cluster,用来进行多核服务器上运行 node.js,以及提供扩展的支持。但是在 node.js 0.6.0 之后,node.js 本身就提供了 cluster 的支持,另外,第三方的 cluster 也与 node.js 0.6 有兼容性问题。目前 node.js 的稳定版本是 0.6.5,因此需要使用原生的 cluster 来代替第三方的 cluster。

幸好内置的 cluster 也足够简单,如果只是为了多核负载均衡,以及支持即时服务重启的话,只需要写一点的代码就可以完成这些功能了。

server.js

var path = require('path');
var http = require('http');
var cluster = require('cluster');

var NODE_ENV = process.env.NODE_ENV || 'production';
var appName = path.basename(__dirname);
var appPort = 9000;

var numCPUs = require('os').cpus().length;

if (cluster.isMaster) {
    process.title = appName + ' master';
    console.log(process.title, 'started');

    // 根据 CPU 个数来启动相应数量的 worker
    for (var i = 0; i < numCPUs; i++) {
        cluster.fork();
    }

    process.on('SIGHUP', function() {
        // master 进程忽略 SIGHUP 信号
    });

    cluster.on('death', function(worker) {
        console.log(appName, 'worker', '#' + worker.pid, 'died');
        cluster.fork();
    });

} else {
    process.title = appName + ' worker ' + process.env.NODE_WORKER_ID;
    console.log(process.title, '#' + process.pid, 'started');

    process.on('SIGHUP', function() {
        // 接收到 SIGHUP 信号时,关闭 worker
        process.exit(0);
    });

    http.Server(function(req, res) {
        res.writeHead(200);
        res.end('Worker ' + process.env.NODE_WORKER_ID);
    }).listen(8000);
}

运行服务 node server.js

nodejs master started
nodejs worker 1 #38928 started
nodejs worker 3 #38930 started
nodejs worker 2 #38929 started
nodejs worker 4 #38931 started

如果直接 kill 掉某一个 worker,kill 38928

nodejs worker #38931 died
nodejs worker 5 #38934 started

可以看到一个新的 worker 会马上启动,这就保证了服务的不间断性。

Virtual Host 支持

通常情况下,我们不会在一个 IP 上只部署一个网站。在使用 node.js 时,可以使用 connect 提供的 vhost 支持 Virtual Host,但是,这也限制了服务器只能用 node.js,而不能同时使用其他的服务,例如再安装一个 PHP 服务之类。

这时就可以使用 nginx 的反向代理来解决了,用户在访问网站时,请求先到 nginx 进行处理,如果是 node.js 站点的话,将请求转发到 node.js 的服务,然后再将 node.js 服务的结果返回给用户。

在 nginx 中设置反向代理很简单,一句 proxy_pass 就可以搞定:

server {
    listen 80;
    server_name example.com;

    location / {
        proxy_pass http://localhost:9000;
    }
}

在添加了 Virtual Host 之后,就可以把一些静态资源,例如 CSS、JavaScript 之类的文件,直接交给 nginx 来处理,而不是什么请求都需要到 node.js 这一层去处理,也省去反向代理这一关的消耗。

Session 支持

使用 express 这个 node.js web framework 来创建网站时,可以配合 connect 这个中间件来实现 session 支持。

默认情况下,connect 的 session 是使用内置的内存存储来存放 session 信息,这时如果 node.js 服务一旦重启,所有的 session 信息都会丢失,这对于用户来说不是个好体验,那么我们可以用外部的存储来存放 session 信息,例如 redis。

要让 connect 使用 redis 作为 session 存储的话也是很方便的:

var express = require('express');
var RedisStore = require('connect-redis')(express);

var app = express.createServer(
    express.session({ secret: 'keyboard cat', store : new RedisStore() })
);

监测文件改动

在调试的时候,经常需要重新启动 node.js 以便修改过的文件生效,原来第三方的 cluster 有一个配置项,可以很方便的配置监测时间间隔,文件改动后自动重新启动 worker,但是原生的 cluster 就没有这个功能了,需要自己来实现。

fs 模块提供了 watch 函数,可以方便的监测文件修改,使用这个就可以来实现文件修改后自动重启 woker 功能了。

if (cluster.isMaster) {
    process.title = appName + ' master';
    console.log(process.title, 'started');

    var workers = [];

    // 根据 CPU 个数来启动相应数量的 worker
    for (var i = 0; i < numCPUs; i++) {
        var worker = cluster.fork();
        workers.push(worker.pid);
    }

    process.on('SIGHUP', function() {
        // master 进程忽略 SIGHUP 信号
    });

    // 监测文件改动,如果有修改,就将所有的 worker kill 掉
    fs.watch(__dirname, function(event, filename) {
        workers.forEach(function(pid) {
            process.kill(pid);
        });
    });

    cluster.on('death', function(worker) {
        var index = workers.indexOf(worker.pid);
        if (index != -1) {
            workers.splice(index, 1);
        }
        console.log(appName, 'worker', '#' + worker.pid, 'died');
        worker = cluster.fork();
        workers.push(worker.pid);
    });

}

这样,每次文件保存之后,node.js 都会自动重启,从而避免了每次保存文件要手动重启服务的麻烦。

当然,在使用监测文件自动重启的时候,最好加上 NODE_ENV 的判断,在 development 的时候才进行自动重启,而 production 的时候使用手动重启就够了。

小结

总的来说,使用 node.js 来构建网站还是很方便的,加上 nginx 反向代理之后,与使用 PHP 之前也没有很大的区别,又可以享受到 node.js 的高效。

嗯,就这样了,希望此文对你有所帮助。

参考资料

  1. https://zh.wikipedia.org/wiki/Node.js
  2. http://nodejs.org/docs/v0.6.5/api/cluster.html
  3. http://learnboost.github.com/cluster/
  4. http://expressjs.com/
  5. http://senchalabs.github.com/connect/
  6. http://redis.io/

— EOF —

慎用 script 节点的 src 属性来传递参数

在有些使用 javascript 来渲染数据的时候,为了能动态获取不同的数据,并且保持 javascript 代码的可扩展性,会将 javascript 代码中获取数据的部分需要的参数提取出来,做为参数放在 script 节点的外部。

一般来说,传递参数到 javascript 文件内部的方法有两种,一种是将参数写在一个 script 节点中,写成全局变量的方式的传递给紧接着这个 script 节点的外部 javascript 中,Google Analytics 就是使用这样的方式:

<script type="text/javascript">
var p1 = "v1", p2 = "v2";
</script>
<script type="text/javascript" src="foo.js"></script>

另外一种是将参数直接写在 script 节点的 src 属性中,相当于一个页面的查询字符串一样:

<script type="text/javascript" src="foo.js?p1=v1&p2=v2"></script>

不过,使用 script 节点的 src 属性来传递参数需要注意一个很重要的问题,那就是动态变化的 src 属性会导致缓存失效

现在,为了网站性能的需要,一般都会将 javascript 文件放在独立的服务器上,并设置一个较长的过期时间,这样客户端只会在第一次访问网站时需要去下载这个 javascript 文件。但是,如果使用 src 来传递参数,就可能会使这种缓存策略失效。特别是 src 中存在动态参数的情况,例如统计脚本中如果有一个 ip 参数,那么访客每次连上线时,可能 ip 都会不同,就会导致 javascript 缓存失效了。

解决这个问题的方法也很简单,简单地的将 src 属性中的参数放到 script 节点的一个自定义属性中就可以了,例如 data-args,而 src 属性只需要保留一个时间戳就可以了。因为使用 src 属性来传递参数本来就需要定位 script 节点,所以改由 data-args 自定义属性来传递参数并不会增加额外的代码。只不过页面会通不过 w3c 的验证罢了 :)

&lt;script type="text/javascript" src="foo.js" data-args="p1=v1&amp;p2=v2"&gt;&lt;/script&gt;

再次提醒,慎用 script 节点的 src 属性来传递参数 :)

在 AIR 中突破同源策略访问 iframe 中的内容

本篇所讲关于 AIR 的内容是基本 HTML+JS,而不是 Flex 或其他。

在 AIR 中,如果当前页面有一个 iframe,并且我们需要获取这个 iframe 的内容,但是如果 iframe 里包含的是远程页面,在默认情况下,浏览器策略并不允许这种情况,这就是同源策略的限制。

在 Titanium 这个类似于 AIR 的平台中,默认情况下是可以直接从应用的页面中去读取 iframe 中的内容而没有任何限制,但是在 AIR 中不可以。在网上搜索了一番,再看了好几遍 AIR 的文档之后,终于找到解决问题的方法了。

在 AIR 中,iframe 标签拥有额外的几个属性:sandboxRoot、documentRoot、allowCrossDomainXHR 及 ondominitialize。这里为了解决读取 iframe 内容所用到的属性就是 sandboxRoot 及 documentRoot。

通过 sandboxRoot 及 documentRoot,可以将本地的页面映射为远程页面,并在沙盒中运行,另外,在沙盒中运行的页面就会拥有映射域名的域。例如,我们可以将本地的 proxy.html 映射为 http://example.com/local/proxy.html,这样,在实际运行时,proxy.html 中如果用 document.domain 获取页面所在的域,就会得到 example.com,这个时候如果在 proxy.html 中添加一个 iframe,这个 iframe 指向我们实际需要获取内容的地址,因为域相同,就可以直接获取 iframe 的内容了。

例如,我们要在 AIR 中获取 http://example.com/member/login.html 的内容,或者操作这个页面的元素,就可以先用一个 proxy.html,用来实现将本地文件运行在沙盒中,再用一个 login.html 来对实际网页进行操作。

<!– proxy.html –>
<iframe src=”http://example.com/local/login.html” id=”frame” width=”100%”
sandboxRoot=”http://example.com/local/”
documentRoot=”app:/” name=”frame” height=”100%”></iframe>

    <!– login.html –>
    <iframe id=’f’ frameborder=”0″ scrolling=”no”
    src=”http://example.com/member/login.html”
    width=”100%” height=”100%”></iframe>
    <script type=”text/javascript”>
    alert(document.domain);
    var f = document.getElementById(‘f’);
    f.onload = function() {
    alert(f.contentWindow.document.documentElement.innerHTML);
    };
    </script>

    在 proxy.html 中,虽然 iframe 的 src 属性是 http://example.com/locale/login.html,但是由于这个 iframe 具有 sandboxRoot 属性,所以实际请求会被重定向到相对于 sandboxRoot,并且使用 documentRoot 进行定位的实际地址 app://login.html,但是这个时候 login.html 中具有  document.domain=”example.com” 这个属性,所有可以直接使用 iframe.contentWindow 来访问 iframe 中的内容。

    虽然解决方法有些麻烦,但终归可以实现突破同源策略的限制来读取 iframe 的内容了。

    当然,如果你有更好的方法,请不吝赐教:)

    使用 arguments.caller 实现自动回调

    这是一篇介绍类似于 js hack 的文章,只是说明了一种可行的途径,并且可能会增加代码复杂程度,具体项目中是否可以使用还请自辨:)

    在前端开发过程中,有许多业务流程可能是需要用户进行登录的,并且登录过程是放在弹出层中,这样就可以不用刷新页面,增强用户体验。在登录时,用户的操作就会被打断,为了进一步增强用户体验,我们可能需要在登录完成后自动继续进行用户在登录前想进行的操作。

    假设有这样一个场景,用户需要发表一个留言,但是发表留言是需要登录的,而发表留言的输入框是一直显示的,这也就要求在用户点击了发表按钮时对用户登录状态进行验证,传统的做法是将在用户登录状态检查封装在一个函数之中,这个函数接收一个回调参数,如果登录验证通过,则执行回调函数。这样的逻辑可以用以下代码表示:

    function doAction() {
    checkLogin(function() {
    // 处理业务逻辑
    });
    }
    function checkLogin(callback) {
    if (isLogin) {
    callback();
    } else {
    showLogin(callback);
    }
    }
    function showLogin(callback) {
    document.getElementById(“login-btn”).onclick = function() {
    isLogin = true;
    callback();
    };
    }

      总觉得这样的方式会将业务逻辑放到一个匿名函数中,而不是放在了按钮的事件响应函数中,感觉上不是那么好。这对整个事件响应函数的改动比较大,主要的业务逻辑都放在了登录验证函数的回调中。而希望可以是下面这样:

      function doAction() {
      // 直接在函数开始进行登录验证,将业务逻辑独立出来
      if (!doLogin()) {
      return;
      }
      // 处理业务逻辑
      }
      function doLogin() {
      if (isLogin) {
      return true;
      } else {
      var callback = function() {}; // 自动组装 callback
      showLogin(callback);
      return false;
      }
      }
      function showLogin(callback) {
      document.getElementById(“login-btn”).onclick = function() {
      isLogin = true;
      callback();
      };
      }

        这里的关键在于怎么“自动组装 callback”,很幸运的是,JavaScript 的 Function 提供了一个属性 caller 可以用来获取调用当前函数的函数是什么。并且还可以用 caller 的 arguments 属性来获取 caller 执行时的参数是什么,这也就使我们自动组装 callback 并恢复 caller 的原参数成为可能。

        <div id=”result”></div>
        <input type=”button” value=”Sumit” onclick=”sendPost()” id=”post” />
        <input type=”button” value=”Clear login” onclick=”isLogin=false” id=”reset” />
        <div id=’login’ style=”border:1px solid #000;padding:10px;margin:10px;display:none”>
        <strong>Login</strong><br />
        <input type=”button” id=’loginbtn’ value=”Login” />
        </div>
        <script type=”text/javascript”>
        // 是否登录
        var isLogin = false;
        // 检查登录状态
        var checkLogin = function(cfg) {
        if (isLogin) {
        return true;
        }
        cfg = cfg || {};
        var callback = null;
        // 自动执行回调
        if (cfg.autoCallback && arguments.callee.caller) {
        try {
        var caller  = arguments.callee.caller,
        args    = caller.arguments || [],
        scope   = cfg.callbackScope || {},
        newArgs = [];
        for (var i = 0; i < args.length; ++i) {
        newArgs.push(args[i]);
        }
        callback = caller ? function() {
        try {
        caller && caller.apply(scope, newArgs);
        } catch (e) {
        //alert(e.message);
        }
        } : null;
        } catch (e) {
        callback = null;
        }
        }
        showLogin(callback);
        return false;
        };
        function sendPost() {
        if (!checkLogin({autoCallback:true})) {
        return;
        }
        document.getElementById(‘result’).innerHTML += ‘Hello<br />’;
        }
        function showLogin(callback) {
        document.getElementById(‘login’).style.display = ‘;
        document.getElementById(‘loginbtn’).onclick = function() {
        document.getElementById(‘login’).style.display = ‘none’;
        isLogin = true;
        callback && callback();
        }
        }
        </script>

        其实弄出这么个方法来实现登录自动回调也是为了方便写代码,不用每次去将一堆业务逻辑放到 callback 中,直接在函数开始回一个 if (!checkLogin({autoCallback:true})) 就行。

        caller 属性在 MDC 中被标记为“非标准”的,但是在主流浏览器中,都是支持这个属性的,也就是说,在浏览器兼容性上使用 caller 是没有问题的。

        不过,使用奇技淫巧容易伤身体,慎用:)

        HTML5 Canvas 起步(3) – 颜色与渐变

        上一篇介绍了 HTML5 中 Canvas 的路径,这篇将要介绍一下 Canvas 中的颜色及渐变。

        Canvas 中的基本颜色系统

        在 Canvas 中,颜色主要用途就是在绘制路径时,用来指定填充颜色和边框颜色。

        Canvas 中的颜色参数值有两种格式:

        1. 如果透明度为 1.0,也就是不透明,颜色值的格式就与一般使用一样,为:#AABBCC,其中 AA、BB、CC 分别为 Red、Green、Blue 分量。

        2. 如果透明度不为 1.0,也就是带透明,颜色值格式可以使用 rgba(r, g, b, a),其中 r、g、b、a 分别为 Red、Green、Blue 分量和透明度。透明度的值为 0 至 1.0 之间的一个数值。

        3. 颜色值还可以为已知的颜色名称,例如 red、blue、green 等。

        总的说来,Canvas 中颜色值的格式与 CSS 中一致,因此颜色值没有特别需要注意的地方。

        图片附件

        <div id="_mcePaste">&lt;canvas id="canvas" width="600" height="500"&gt;&lt;/canvas&gt;
        &lt;script type="text/javascript"&gt;
            var canvas = document.getElementById("canvas");
            var ctx = canvas.getContext("2d");
            // fill a rectangle whit sepecified fill style
            function fillRect(color) {
                ctx.fillStyle = color;
                ctx.fillRect(0, 0, 150, 100);
                ctx.font = "10pt Arial";
                ctx.fillStyle = "white";
                ctx.fillText(color, 6, 20);
                ctx.translate(160, 0);
            }
            ctx.translate(0, 50);
            // fill rect with color name
            fillRect("green");
            // fill rect with 0xAABBCC
            fillRect("#AABBCC");
            // fill rect with rgba(50, 100, 150, 0.5)
            ctx.translate(0, -25);
            fillRect("rgba(50,100,150,0.5)");
         
            // fill another rect with rgba(150, 100, 50, 0.5)
            ctx.translate(-110, 50);
            fillRect("rgba(150, 100, 50, 0.5)");
        &lt;/script&gt;</div>

        注意,以上代码需要使用 Firefox 3.5 来查看,在 Firefox 3.0.x 中,Canvas 的 Context 对象不支持 fillText 方法,而我安装的 Chrome 2.0.174.0 对 translate 方法的实现有误。

        2. Canvas 中的渐变

        WHATWG 的 Canvas 规范中规划了两种渐变模式,一种是线性渐变,另一种是径向渐变。如果需要在 Canvas 中使用渐变,首先要根据你所要创建的渐变模式来调用 Context 的相应方法来创建一个渐变对象,这个对象就是用来控制渐变的效果。

        继续阅读 »

        HTML5 Canvas 起步(2) – 路径

        上一篇介绍了 HTML5 中 Canvas 的基本概念,这篇将要介绍一下 Canvas 中的基本图形。

        图形的基础 – 路径

        在 Canvas 中,所有基本图形都是以路径为基础的,也就是说,我们在调用 2dContext 的 lineTo、rect 等方法时,其实就是往已经的 context 路径集合中再添加一些路径点,在最后使用 fill 或 stroke 方法进行绘制时,都是依据这些路径点来进行填充或画线。

        在每次开始绘制路径前,都应该使用 context.beginPath() 方法来告诉 Context 对象开始绘制一个新的路径,否则接下来绘制的路径会与之前绘制的路径叠加,在填充或画边框时就会出现问题。在绘制完成路径后,可以直接使用 context.closePath() 方法来关闭路径,或者手动关闭路径。另外,如果在填充时路径没有关闭,那么 Context 会自动调用 closePath 方法将路径关闭。

        基本路径方法

        1. beginPath, closePath

        这两个方法在前面已经介绍过,分别用来通知 Context 开始一个新的路径和关闭当前的路径。

        在 Canvas 中使用路径时,应该要保持一个良好的习惯,每次开始绘制路径前都要调用一次 beginPath 方法,否则画出来的效果难看不说,还会严重影响性能。

        在下面这张图中,左边的图形在每次绘制矩形前都调用了一次 beginPath 来清除之前的路径并重新开始绘制新的路径,而后面的图形则就只在绘制所有图形前调用了一次 beginPath 来清除路径,因此,虽然这里是使用的边框色是 #666,但是右边的图形颜色比左边的深一些,因为每次使用 stroke 绘制边框时,会把之前的路径再次绘制一遍,叠加起来颜色就比原来深一些。

        图片附件

        <div id="_mcePaste">&lt;canvas id="canvas" width="500" height="500"&gt;&lt;/canvas&gt;
        &lt;script type="text/javascript"&gt;
            var canvas = document.getElementById("canvas");
            var ctx = canvas.getContext("2d");
            ctx.strokeStyle = "#666";
            function useBeginPath() {
                for (var i = 0; i &lt; 5; ++i) {
                    ctx.beginPath();
                    ctx.rect(10 + i*20, 10 + i*20, 210 - i*40, 210 - i*40);
                    ctx.stroke();
                }
            }
            function notUseBeginPath() {
                ctx.beginPath();
                for (var i = 0; i &lt; 5; ++i) {
                    ctx.rect(240 + i*20, 10 + i*20, 210 - i*40, 210 - i*40);
                    ctx.stroke();
                }
            }
            useBeginPath();
            notUseBeginPath();
        &lt;/script&gt;</div>

        在 Context 中路径数较少时,如果不考虑显示效果,性能上还可以接受,但是如果 Context 中的路径数很多时,在开始绘制新路径前不使用 beginPath 的话,因为每次绘制都要将之前的路径重新绘制一遍,这时性能会以指数下降。

        因此,除非有特殊需要,每次开始绘制路径前都要调用 beginPath 来开始新路径。 继续阅读 »

        HTML5 Canvas 起步(1) – 基本概念

        什么是Canvas

        <canvas> 是一个新的 HTML 元素,这个元素在 HTML5 中被定义。这个元素通常可以被用来在 HTML 页面中通过 JavaScript 进行绘制图形、合成图像等等操作,也可以用来做一些动画。当然,目前 HTML5 规范还在草稿阶段,正式发布也许要等到2010年,不过现在已经有不少浏览器已经支持了部分 HTML5 规范。目前支持 canvas 元素的浏览器有 Firefox 3+、Safari 4、Chrome 2.0+ 等,因此,在运行本页中的例子时,请确保你使用的是上述浏览器之一。

        尽管在 Mozilla 已经有不少关于 Canvas 的教程,我还是决定把自己的学习过程记录下来。如果觉得我写的不够明白,那么你可以在参考资料中找到 Mozilla 网站上 Canvas 教程的链接。

        另外,可以在这里找到一些有趣的 Canvas 示例。

        开始使用 Canvas

        使用 Canvas 很简单,与使用其他 HTML 元素一样,只需要在页面中添加一个 <canvas> 标签即可:

        <canvas id="screen" width="400" height="400"></canvas>

        当然,这样只是简单的创建了一个 Canvas 对象而已,并没有对它进行任何操作,这个时候的 canvas 元素看上去与 div 元素是没什么区别的,在页面上什么都看不出来:)

        另外,canvas 元素的大小可以通过 width 与 height 属性来指定,这与 img 元素有点相似。

        Canvas 的核心:Context

        前面说到可以通过 JavaScript 来操作 Canvas 对象来进行绘制图形、合成图像等操作,这些操作并不是通过 Canvas 对象本身来进行的,而是通过 Canvas 对象的一个方法 getContext 获取 Canvas 操作上下文来进行。也就是说,在后面我们使用 Canvas 对象的过程中,都是与 Canvas 对象的 Context 打交道,而 Canvas 对象本身可以用来获取 Canvas 对象的大小等信息。

        要获取 Canvas 对象的 Context 很简单,直接调用 canvas 元素的 getContext 方法即可,在调用的时候需要传递一个 Context 类型参数,目前可以用的并且是唯一可以用的类型值就是 2d:

        <script type="text/javascript"><!--mce:0--></script>

        Firefox 3.0.x 的尴尬

        Firefox 3.0.x 虽然支持了 canvas 元素,但是并没有完全按照规范来实现,规范中的 fillText、measureText 两个方法在 Firefox 3.0.x 中被几个 Firefox 特有的方法代替,因此在 Firefox 3.0.x 中使用 Canvas 时需要先 fix 这个几个方法在不同浏览器中的差别。

        继续阅读 »

        “云端的编辑器”,10 步安装 Bespin Development Server(Python版)

        Mozilla Labs 在今年情人节那天发布了一个叫 Bespin 的编辑器,这是一个基于网络的可扩展文本编辑器,按照现在流行的说法,就是“云编辑”了。并且,这个编辑器是开源的。

        图片附件

        Bespin 是基于 Canvas 的,因此目前它只支持少数浏览器,其中包括 Firefox 3+、Safari 4 以及 Chrome 2 开发版,因此,如果要使用 Bespin 的话,你必须使用这几款浏览器中的一种。

        要体验 Bespin,可以直接在 bespin.mozilla.com 注册一个帐号并且登录,不过目前 bespin.mozilla.com 所用的代码并不是最新版本的,线上使用的代码有许多不完善之处。因此,除了使用 Mozilla Labs 官方的 Bespin 站点,我们也可以从 Mozilla Labs 下载 Bespin 源代码,并在本地搭建 Bespin 服务,从而可以体验 Mozilla Labs 最新的开发成果。

        注意,这篇文章并不介绍怎么去使用 Bespin,而是介绍怎么样去配置一个可以在本地运行的 Bespin 服务器,因此,如果你需要了解怎么去使用这个编辑器,可以参阅 Mozilla Labs 上的文档,或者等我再写一篇使用 Bespin 的文章:)

        准备工作

        首先需要说明的是,这里介绍的配置 Bespin 本地服务器的环境是 Windows Vista(or WinXP) + Python。另外,如果是在 Vista 中安装 Bespin Server,你使用管理员权限来运行 cmd。

        第 1 步:在这里把 Bespin 的源代码下载下来,然后解压到任意目录,我这里解压到的是“D:\Source\Bespin\bespin-8b89188c5066”。

        第 2 步:这篇文章介绍的是 Python 版 Bespin 服务器的配置,因此还需要安装 Python 解释器,可以在这里下载到 Python 2.5.4,使用默认设置安装即可。

        第 3 步:在 bespin 源代码目录中新建一个目录 Scripts,这个目录与 backend 和 frontend 两个目录是平级的。然后到 Python 的安装目录下,将安装目录下的 msvcr71.dll、python.exe、python25.dll、pythonw.exe 四个文件拷贝到前面建立的 Script 目录中。

        第 4 步:将 Python 安装目录中的 libs 目录拷贝到 bespin 源代码目录中。

        第 5 步:Bespin Python 服务器所用的有些组件是使用 C 写的,在 Windows 上要编译与 Python 兼容的 C 扩展,需要使用 MinGW,下载以下这些压缩包,解压到 D:\Tools\MinGW,这里解压的位置可以自己选,但是后面要用到,所以请记住你解压的路径:

        http://nchc.dl.sourceforge.net/sourceforge/mingw/binutils-2.19.1-mingw32-bin.tar.gz
        http://nchc.dl.sourceforge.net/sourceforge/mingw/gcc-g++-3.4.5-20060117-3.tar.gz
        http://nchc.dl.sourceforge.net/sourceforge/mingw/gcc-core-3.4.5-20060117-3.tar.gz
        http://nchc.dl.sourceforge.net/sourceforge/mingw/w32api-3.13-mingw32-dev.tar.gz
        http://nchc.dl.sourceforge.net/sourceforge/mingw/mingwrt-3.15.2-mingw32-dev.tar.gz

        继续阅读 »