乐者为王

Do one thing, and do it well.

逻辑题-共打了多少鱼

abcde五人打渔,打完睡觉,a先醒来,扔掉1条鱼,把剩下的分成5份,拿1份走了;b再醒来,也扔掉1条,把剩下的分成5份,拿1份走了;然后cde都按上面的方法取鱼。问他们最少打了多少条鱼?

渔民 醒来时鱼的总数 取走的鱼数
a x1 = x (x1 - 1) / 5
b x2 = 4 * (x1 - 1) / 5 (x2 - 1) / 5
c x3 = 4 * (x2 - 1) / 5 (x3 - 1) / 5
d x4 = 4 * (x3 - 1) / 5 (x4 - 1) / 5
e x5 = 4 * (x4 - 1) / 5 (x5 - 1) / 5

由于扔掉1条鱼后,还能被分成5份,设渔民醒来时鱼的总数为remain,那么(remain - 1) % 5的值为0,即remain % 5的值为1。

最简单的方法就是枚举,最小值从1开始:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fishmen = [0, 0, 0, 0, 0]  # 渔民取走的鱼数
total = 1

while true do
  remain = total  # 渔民a醒来时鱼的总数
  5.times do |i|  # 5个渔民轮流取鱼
    break if remain % 5 != 1  # 如果不符合扔掉1条鱼后还能分成5份的条件,就枚举下个值
    fishmen[i] = (remain - 1) / 5  # 渔民取走的鱼数
    remain = 4 * fishmen[i]  # 渔民取走鱼后剩下的鱼数
  end
  break if fishmen[4] != 0  # 如果渔民e也取到了鱼,那么就得到了鱼的总数
  total += 1
end

puts total  # 结果是3121条鱼

上面的代码总过做了3901次循环,下面来做进一步的优化。

从表格可以看出,因为(x5 - 1) % 5 == 0,推导出x5 >= 6;又x1 % 5 == 1,因此x1的个位数必须是1或者6。所以,枚举的最小值可以从11开始,每次步进为5。优化后的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fishmen = Array.new(n, 0)
total = 11

while true do
  remain = total
  5.times do |i|
    break if remain % 5 != 1
    fishmen[i] = (remain - 1) / 5
    remain = 4 * fishmen[i]
  end
  break if fishmen[4] != 0
  total += 5
end

puts total

总的循环次数减少到1401次,减少了整整64%的循环。

推而广之:n个渔民打渔,每个渔民依次扔掉1条鱼后,把鱼分成n份,然后拿走其中一份,求最少打了多少条鱼?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fishmen = Array.new(n, 0)
total = 2 * n + 1

while true do
  remain = total
  n.times do |i|
    break if remain % n != 1
    fishmen[i] = (remain - 1) / n
    remain = remain - 1 - fishmen[i]
  end
  break if fishmen[n - 1] != 0
  total += n
end

puts total

这下子无论多少个渔民打渔都可以用这段代码搞定了。我试了试9个渔民,发现竟然可以打近3.9亿条鱼,那得有多少鱼啊!另外,计算时间也明显开始变长了。不知道还能不能做更进一步的优化。如果你有更好的算法,请快点告诉我吧!

写出受欢迎的编程文章的10个技巧

英文原文:http://www.devx.com/blog/2009/01/10-tips-for-writing-consistent.html

对于作者,这是一个可悲的事实。当谈到受大众欢迎时,不是所有的文章都是平等的,一些文章被证明比其它的更受欢迎。事实是,不管你选的主题有多好,或者你写的有多好,你都无法保证每次都能打出本垒打。尽管如此,如果你已经在写编程的文章(或者你正在考虑写),这里列出的技巧被证明能帮助文章从沉闷乏味的堆里跃升至最多查看次数列表的顶部。

1. 选择一个简练的话题

挑选一个简练的话题是艰难的,因为很多有趣的话题一点也不简洁,它们是冗长的和复杂的。有时候你可以创建很好的独立文章作为一个较长的话题,但通常不要这样做。相反,你可能需要写一个系列或一本书。要么选择别的,要么把这个话题分成简练的块。

接下来的几个技巧可以帮助你满足技巧#1的要求:

2. 增强现有的文档

一些最受欢迎的文章本质上是增强文档。那并没有什么错,因为技术文档通常是匆忙产生的,是不完整的,不是由开发者编写的,或缺乏相关的例子。在几乎所有情况下,文档(或文档的缺乏)给作者提供了丰富的话题。

3. 比较事物

另一个屡屡受欢迎的策略是写比较两个或多个受欢迎的项目的文章。这些可以是语言、语言版本、APIs、数据库、操作系统、框架、编程方法、模式——在快速变化的开发者世界中,有着无数的机会。挑两个或多个开发者使用的技术,并且写那种最好能帮助开发者转型或选择它们的文章。

4. 列个清单

你可能会认为这是陈词滥调,但“10 for/about”类型的文章往往表现的很好。(如果这篇博文做的很好,我会认为它验证了这个观点,如果没有的话,或许我会减少成“9个技巧……”)。编辑们和出版商们喜欢这些文章,因为读者喜欢它们。我怀疑读者喜欢它们是因为包含很多内容的文章增加了概率,因为这些文章至少包含有一个需要的或有趣的材料,但也许人们只是喜欢列表。无论如何……

在你有一个坚实的话题后,你准备开始写作,请记住这些观点:

5. 忽视历史

是的,我知道你认为每个人在切入正题前都需要了解特定主题的历史背景,但事实是,他们很少这样做。比起你在高中学习代数时让你的父亲解释数学的历史,你的读者不再想思考你的历史评论。这里有个读者行为分析的内部技巧:大部分读者从来没有读完过第一页。所以,如果你没有回答他们的问题,或立即抓住他们的兴趣,不管你的文章的其余部分有多么正确,他们都不会看。链接到历史,切入正题。

6. 避免“HelloWorld”例子

在你读过的文章和书里你看到过一千个“HelloWorld”的代码例子,但这并不意味着它们是好的!它们不是。没有人喜欢它们。对于学习编程的任何事情它们是完全无用的。它们也不是娱乐。完全有可能写出清晰而简单的既可以教又不烦闷的例子。

7. 说明你的观点

开发者喜欢代码,的确,但你也可以通过包含插图和截图帮他们节省时间和精力。这是因为他们中的许多人可能不会运行你那迷人的示例代码,但如果他们正在阅读你的文章,他们很可能对结果感兴趣。显示输入或输出无论何时都很重要。

8. 显示有趣的代码

许多技术作者似乎认为,在简短的解释之后提供大量的示例代码(或者更糟糕的是,只是显示代码而没有解释)将刺激他们的读者研究代码以获得启示。我向你保证那不是真的。最好的文章解释了话题,但只显示代码片断,然后解释或说明(或两者)代码做了什么,它如何与周围的代码或整体的话题适应,什么时候你应该使用它,什么时候不应该,也只有到那时——仅且当它是真正有用的时候——他们会向你打听更长的代码块。相反,只放有趣的代码在你的文章里,并将其余部分作为可运行的、完整的项目以供下载。

9. 化繁为简

避免冲动去告诉人们你的主题是多么地复杂。他们知道它是复杂的,或者他们很可能不会读你的文章。相反,想办法去让复杂的主题看起来更简单。

也许是所有技巧中最重要的一点:

10. 简明扼要

最受欢迎的技术文章只给读者他们需要的——没有更多。

最后,这是真的,有些文章很受欢迎,尽管它很少有甚至没有在这里列出的特性——但那并不是你可以控制的东西。尝试去写那种文章就像是博顺子,你会浪费大部分的时间。专注于基础,写大量的文章,然后其中的某些人将会成为赢家。

快乐写作。

在Application对象里存储数据的陷阱

看到Don't Store Data in the Application Object讲,在Application对象中存储共享数据会引起NullPointerException。顿时心里就咯噔了一下,用了四分之三秒,想起自己有个业余项目就干了这样的事。赶紧地测试看看。

打开应用,从MainActivity进入TxtViewerActivity界面(这里MainActivity主要是读取目录数据,然后保存在继承自Application的MainApp中,供TxtViewerActivity调用)。按手机Home键退出应用,这时你按菜单键可以看到该应用的缩图。然后在Eclipse中打开Window -> Show View -> Other -> Android -> Devices视图,双击窗口内的设备,然后点击设备下对应的进程,点击右上方红色的“Stop Process”图标。

重新按菜单键打开应用,然后……然后果然在LogCat中看到了有NullPointerException的大段红色警告文字。

为什么会Crash的?

根本原因在于:当应用被kill掉后,通过菜单键重新打开时,应用不会开始重新启动。Android系统会新建一个Application对象,然后启动上次离开时的TxtViewerActivity以造成这个应用从来没有被kill掉的假象。因为没有经过MainActivity的数据读取,所以在TxtViewerActivity中读取数据当然要抛出异常了。

有没有替代方法呢?

  • 直接将数据通过Intent传递给TxtViewerActivity?当然也会碰到上述同样的问题。
  • 使用SharedPreferences?可惜只能存储boolean、int、long、float和String五种数据类型,不支持List的存储;
  • 使用持久化存储?也不支持List的存储,而且太笨重了;
  • 使用Singleton对象保存共享数据,然后通过Intent传递呢?这个想法不错,还可以将读取assets资源等操作移到该对象中,做到单一职责原则,改善设计。不过这样一来Singleton对象会对MainActivity的context有长期引用,容易造成内存泄露。如果不把读取操作放进去……那根本就不可能,你能让一个追求完美的程序猿忍受糟糕的代码设计吗!

幸好早就有人总结出来经验了:使用Application的context代替Activity的context。

创建Singleton对象,在Application对该对象保持引用,把原来存储在Application中共享的数据全部移到Singleton对象中,将Activity中读取assets资源等操作也放入该对象,Activity中原来对Application对象的访问改成通过Application对象对Singleton对象的访问。

这样修改后,不光解决了应用的崩溃,还预防了内存泄漏,更改进代码的设计。

代码下载:https://github.com/dohkoos/txtReader

一开始就编写优质的OO代码

英文原文:https://weblogs.java.net/blog/2008/10/03/writing-great-oo-code-day-one

没有获取经验的捷径。编写良好的面向对象代码需要经验。尽管如此,这里有三个实践可以帮助你一开始就有个良好的开端:

  1. 使用测试驱动开发(TDD)编写你所有的代码
  2. 遵循简单的规则
  3. 命令代替询问(Tell Don't Ask)

使用TDD编写你所有的代码

测试先行编写的代码和测试后行编写的代码是非常非常不同的代码。测试先行编写的代码是松耦合和高内聚的。测试后行编写的代码往往会破坏封装,当一些属性或私有方法需要被暴露给测试的时候,因为这些类没有被设计成要被测试的。如果你编写的代码测试先行,代码的依赖性会更好,你的代码将是松耦合和高内聚的。稍后详细讨论测试如何帮助你设计更好的代码。

遵循简单的规则

代码是简洁的,当它:

  1. 通过所有的测试
  2. 不包含重复代码
  3. 表达了所有的意图
  4. 使用了最少的类和方法

重要的是注意到我使用了一个有序列表。顺序很重要。带有单一main()方法的单一GodClass并不简单。它可以通过所有的测试,但在比“Hello, world!”更复杂的任何程序里它一定会包含重复代码和没有表达所有的意图。

我与简单的规则的斗争重点围绕在If Bug 。我不明白遵循简单的规则如何阻止某人编写大量的if代码。有人会说,我试过了,大量的if代码不会表达意图。但是,当你读到这样的代码

1
2
3
if (mobile.getType() == MobileTypes.STANDARD) {
    alert();
}

它实在是太容易看出意图了。无论该代码是在哪个方法的上下文中,如果mobile是STANDARD类型,那么警报。你还需要多少意图?

然后我灵光小闪。如果有那样的代码,那么在代码的其它地方肯定还有更多。可能是这样的代码:

1
2
3
if (mobile.getType() == MobileTypes.GAS) {
    registerGasReading();
}

1
2
3
if (mobile.getType() == MobileTypes.TEXT) {
    sendTextMessage();
}

1
2
3
if (mobile.getType() == MobileTypes.LOCATION) {
    notifyLocation();
}

你看见了吗?我当然知道。违反规则2。许多许多违反规则2。并且是违反规则2的最糟糕的那种。重复代码在许多不同的代码片段中。重复代码将非常非常难被找到。所以为了帮助防止这个,我列出来了。

命令代替询问

命令代替询问意味着不要询问一个对象的状态然后做些什么。应该命令那个对象去做些什么。这意味着所有这些if例子变成了:

1
mobile.alert();

1
mobile.registerGasReading();

1
mobile.sendTextMessage();

1
mobile.notifyLocation();

现在假设有一些if子句散落在有重复实现的整个代码中。在那个大量if代码的版本中,它们将非常难被找到,但在命令代替询问版本中,所有的实现都在Mobile类中。所有的都在一个地方寻找和消除。

聆听你的测试也将帮助你保持代码简单。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public interface Alarm {
    void alert(Mobile mobile);
}

public class Siren implements Alarm {
    public void alert(Mobile mobile) {
    if (mobile.getType == MobileTypes.STANDARD) {
        soundSiren();
    }
  }
}

public class SirenTest extends TestCase {
    public void testAlert() {
        LocationMobile mobile = new LocationMobile();
        Siren siren = new Siren();
        siren.alert(mobile);
        assert(sirenSounded());
    }
}

如果你仔细聆听你的测试,它会问你,“你为什么需要LocationMobile去测试Siren?”是呀,为什么呢?似乎Siren甚至不应该知道LocationMobile。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class LocationMobile {
    private Alarm alarm;
    public LocationMobile(Alarm alarm) {
        this.alarm = alarm;
    }
    public void alert() {
        alarm.alert();    // alert on Alarm no longer needs a mobile
    }
}

public class LocationMobileTest extends TestCase {
    public void testAlert() {
        Alarm alarm = EasyMock.createMock(Alarm.class);
        alarm.alert();
        EasyMock.replay(alarm);
        Mobile mobile = new LocationMobile(alarm);
        mobile.alert();
        EasyMock.verify(alarm);
    }
}

看上去我仅仅互换了依赖。作为Alarm依赖Mobile的替换,现在有了Mobile依赖Alarm。如果你仔细看第一个测试,真正的依赖是Siren知道LocationMobile。一个具体类依赖于另一个具体类。这违反了依赖倒置原则 (DIP)。第二个例子是LocationMobile依赖接口Alarm。一个具体类依赖一个抽象。这满足了DIP。

如果你使用TDD编写你所有的代码,遵循简单的规则,以及命令代替询问,那么你会在那条成为一个更好的OO程序员的路上。良好的OO代码容易阅读和维护,但是可能难于编写。至少开始是这样。你写得越多,你将会变得更好,你将得到的经验也越多。与此同时,这些实践会让你在你的路上走得更好。

Ruby调试工具概览

调试Ruby代码最简单的方式就是使用puts或p方法。当有很多变量需要查看时,到处添加puts或p方法就可能变的不那么实际。幸好,Ruby社区提供了许多强大的调试工具。

Ruby 1.8+时代

调试Ruby代码使用ruby-debug。调试Rails代码则是pry-nav。不过在Ruby 1.9出来后ruby-debug就有问题了,于是就有了ruby-debug19,一个针对Ruby 1.9的ruby-debug移植版本。

Ruby 1.9.2+时代

等到Ruby 1.9.2发布,ruby-debug彻底歇菜,然后debugger就出现了。pry-nav也不好使了,还好有pry-debugger

Ruby 2+时代

新的Ruby调试工具byebug来了。虽然byebug也能调试Rails应用,但它不提供语法高亮,所以使用pry-byebug是个更好的选择。

Ruby 1.8+ Ruby 1.9 Ruby 1.9.2+ Ruby 2+
Ruby ruby-debug ruby-debug19 debugger byebug
Rails pry-nav pry-nav pry-debugger pry-byebug

其它

Pry其实不是纯粹的调试工具,它只是IRB的替代品,所以缺乏必要的调试指令。pry-nav、pry-debugger和pry-byebug做的只是分别把ruby-debug、debugger和byebug中的step、next、continue等指令添加到Pry中。

  • pry-nav = Pry + ruby-debug
  • pry-debugger = Pry + debugger
  • pry-byebug = Pry + byebug

如果要调试view怎么办?可以使用Web Console。在view里面加上<%= console %>,当view出现异常时,就会在异常界面下方,出现一个网页版的IRB,方便调试。Web Console默认只接受localhost的请求,假如需要让别的IP也能访问的话,可以这样做:

1
2
3
class Application < Rails::Application
  config.web_console.whitelisted_ips = '192.168.0.100'
end

如何使用VisualVM检测Java内存泄漏

Java的一个重要优点是通过垃圾收集器(Garbage Collection)自动管理内存的回收,程序员不需要关注它。程序员真的不需要关注内存管理吗?只要你碰到过OutOfMemoryError你就知道它不是真的。

这里我会展示如何使用VisualVM快速定位内存泄漏。先看下面这段代码:

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
import java.util.List;
import java.util.ArrayList;

public class MemoryLeakDemo {
    public static void main(String[] args) {
        new Thread(new MemoryLeak(), "MemoryLeak").start();
    }
}

class MemoryLeak implements Runnable {
    public static List<Integer> leakList = new ArrayList<Integer>();

    public void run() {
        int count = 0;
        while (true) {
            try {
                Thread.sleep(3);
            } catch (InterruptedException e) {
            }
            count++;
            Integer i = new Integer(count);
            leakList.add(i);
        }
    }
}

执行下列命令:

1
java -verbose:gc -XX:+PrintGCDetails -Xmx20m MemoryLeakDemo

等待一段时间后,你会看到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Exception in thread "MemoryLeak" java.lang.OutOfMemoryError: Java heap space
        at java.util.Arrays.copyOf(Arrays.java:3181)
        at java.util.ArrayList.grow(ArrayList.java:261)
        at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:235)
        at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:227)
        at java.util.ArrayList.add(ArrayList.java:458)
        at MemoryLeak.run(MemoryLeakDemo.java:22)
        at java.lang.Thread.run(Thread.java:745)
Heap
 PSYoungGen      total 3584K, used 298K [0x00000000ff980000, 0x00000000ffe80000, 0x0000000100000000)
  eden space 3072K, 9% used [0x00000000ff980000,0x00000000ff9ca908,0x00000000ffc80000)
  from space 512K, 0% used [0x00000000ffc80000,0x00000000ffc80000,0x00000000ffd00000)
  to   space 512K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000ffe80000)
 ParOldGen       total 13824K, used 12156K [0x00000000fec00000, 0x00000000ff980000, 0x00000000ff980000)
  object space 13824K, 87% used [0x00000000fec00000,0x00000000ff7df3e8,0x00000000ff980000)
 Metaspace       used 7993K, capacity 8164K, committed 8448K, reserved 1056768K
  class space    used 912K, capacity 954K, committed 1024K, reserved 1048576K

打开VisualVM开始监测MemoryLeakDemo,在Monitor标签页我们可以看到实时的程序内存堆的使用情况:

波峰到波谷处是执行了GC的,明显可以看到执行GC后内存曲线仍旧呈上扬趋势,也就是说,内存占用是只升不降。到底是什么原因导致的呢?

打开Sampler标签页,点击Memory按钮启动一个内存分析会话,VisualVM会定期获取所有执行线程的转储,分析栈跟踪信息,实时显示成堆直方图。通过堆直方图,我们就可以知道哪个对象占用了较多的内存,以便做进一步的优化。

如上图所示,第1行的Integer对象占用内存最大,已经有41万多实例了,并且还在持续增加中。很显然,罪魁祸首就是它了!

对抗完美

英文原文:http://usabilitypost.com/2008/10/08/fighting-perfection/

如果你像我一样,你可能经常会发现自己从来没有完全满意你的工作——总是做出调整和修改,总是找到你不太喜欢的事情然后改进他们。这适用于很多创造性的努力——或许你正在为你的博客加工一篇文章,整理一份报告或者写一封重要的电子邮件。

问题是,即使在作出修改后,仍然有一些事情你可以调整,事情仍然不是相当完美。

这当然是很好的,你给自己设置了一个高标准。如果你不满意你的工作,那么为什么你的访客或客户就要满意呢?

追求完美是件好事

一个建筑师拥有的两个最重要的工具是绘图室的橡皮擦和工地的大铁锤。

Frank Lloyd Wright

Steve Jobs不满意iPhone的第一个版本。他做了一个艰难的决定去放弃最初的设计,因为他不喜欢它;他觉得这不是Apple能做到的最好的。这引起了很多问题给开发团队,因为他们必须在很短的时间范围内整理出一份全新的设计。

新版本获得了成功,要是他没有做出这个艰难的决定,iPhone将不会成为手机行业的大标志,这在一定程度上要归功于它的标志性设计。

但是……

完美是困难的和费时的

完美可以是危险的和误导的。什么时候足够好?什么时候你可以前进,释放你应用的新版本或发布你的新文章?完美是太高的一个目标,因为它简直太难和太费时去实现。

如果你成了完美的奴隶,你会发现你所有的时间被耗尽。你会不停地做修改调整调整修改——事情却没有按时做完。

如何对抗完美?

考虑优先级——什么是真正要紧的事?对于一个非常大的公司,类似iPhone的东西是一个关键的产品;如果你搞砸了,它能造成严重损失。把产品做正确是至关重要的。设定一个非常高的标准在这里将是一个不错的主意。

那么更小的事情像是博客的设计呢?最终,它通常并不重要,除非这个博客是你的主要业务。在这里简单是你的盟友。简单的东西很难被搞砸,因此创建简单的事情然后把工作做完。

你最宝贵的资源是你的时间。为了对抗完美你必须将时间排出优先次序,并专注于那些要紧的事情。如果你正在做的和改进的事情没有那么重要,那么这些事情就不应该去做。

把事情做完

做完。开始把它当作一个咒语。当你去做完它时意味着某些事情已经被完成。决定已经做出,你可以继续前进。做完意味着你正在累积动力。

37signals, Getting Real

执行比想法更重要。把足够好的东西释放出来比做完美但从未完成的东西更好。不要在你做的每件事情上寻求完美——除了那些真正要紧的事情。驯服完美——更快地做完其它的每件事情,把节省下来的时间用在你最重要的项目上。

你能真正地掌握多少编程技术?

英文原文:http://thecodist.com/article/how-many-programming-technologies-can-you-really-master

我总是看见公司或者他们的招聘人员广告他们正在寻找的人:“有从零开始开发iOS和Android应用的丰富经验,必须掌握现代移动和Web技术,包括Java、HTML5、CSS3、JavaScript、JSON和AJAX”。

没有这样的人。你可以掌握一门技术但在其它方面平平;你可以掌握一门技术然后转向另一门技术但是会忘记很多先前的技术;你可以简单地欺骗足够多的人让他们认为你能做到,然后期望你恰巧能够搞定它。

在今天,任何主要领域的编程都是高度复杂的,不断变化的,并且通常是带着很大时间压力完成的。所有这些都不允许你投入大量非编程时间去学习即使是最新的变化,更不用说从零开始掌握一切。你只能通过做真实的项目了解新环境,有多少人能够在同一时间同时编写所有大型的本地Android、本地iOS和响应式Web客户端呢?

在我作为程序员的34年里,我很少工作在超过一个主要领域。我的第一份工作是在一台supermini上,然后Apple上的6502汇编和在一台PC上的Pascal,我的两个创业公司都是用C为Mac开发,我更多地为其他人(包括Apple)工作,用C为Mac开发,一点点C++,接着从Objective-C/WebObjects转换到用Java开发Web客户端和服务端(尽管两者都很少),一些JavaScript,然后在Mac和Windows上进行C++游戏编程,最后是Objective-C和iOS。每次转换都是匆匆忙忙地大量学习,接着是年复一年的掌握所有新的东西。

如果由于某种原因,有人确实能做Android和iOS两者——更不用说Web——以一个真正的大师水平,他们应该能赚比大多数公司愿意支付的更多的钱。公司想要的是雇几个能做所有事情的人,并且以他们能够得到的最低的工资水平。然而我无法理解有人能够同时在这么多事情上成为一个专家,以及他们如何能够用多种技术编写多个应用并且坚持下去。我认识一些极其聪明的人,但我不记得有人棒到确实能在同一时间兼顾不相关的技术并且产出技艺精湛的应用。

也许会有例外。但我仍然认为大多数人做不到。人们当然可以掌握一件事情然后转向掌握另一件事情,但在这个过程中你不可避免地会忘记前者的细节。去年,我在等待裁员且没有什么事情要做的几个月里(我是最后无缘无故裁员中的一个,因为在品牌的最终出售之前我们所有的技术已经被换掉了),我花了一个月的时间在C++上,然后Node.js,最后Swift。今年继续这些语言(因为我的新工作做的是Objective-C)我发现我已经忘了大部分我所学过的。如果你不经常使用某样东西,记忆似乎丢到了脑后。在用PHP重写这个博客引擎的过程中我在我脑中把所有这些语言都搞混了。

如果你是在iOS上从Objective-C转到Swift,那至少还有些重叠。但Android和iOS不仅是不同的语言,一切都不同,从工具到如何布局去支持多个主要的OS发行和24,000多种不同的设备。仅仅是跟上所有这些年在六月WWDC的新变化就要花费大量的精力;雪上加霜的是Apple释放的示例代码在最新beta版的XCode里已经不能编译。就算你不写代码整天只是观看视频和阅读文档与示例代码,你怎么能一本正经的说你是一个专家呢?

给两个不同的移动OS环境添加复杂混乱的是现代Web开发,特别是那些某一天出现然后第二天消失的JavaScript框架。你需要三个脑袋才能跟上它。和我一起工作的JavaScript程序员也就够跟上一个(在这里是AngularJS)。

因此找一个能用JavaScript写iOS、Android和移动/桌面Web的人,使用现代的API并且仍然可以支持旧的OS版本,明白不同设计和UI方法的优缺点,特别是所有不同浏览器和Android设备中的微妙之处,并且在创纪录的时间内交付无bug的结果,是幻想。哦对,还要以低于市场的价格为你工作。

当我开始编程时,一切都极其原始,我只需要知道一门语言和一种OS,根本没有框架。甚至在我的两个创业公司里我只需要掌握C,通晓Macintosh和一些偶尔的68K汇编。今时不同以往。然而我们仍然只有一个大脑,并且大脑不服从摩尔定律,它们不能升级。

因此,如果你能(诚实地)同时做Android、iOS和移动Web,并且交付技艺精湛的结果,我向您致敬!但我真的希望你也能赚3倍的钱。

使用Rails 4.2+ 测试异步邮件

英文原文:https://blog.engineyard.com/2015/testing-async-emails-rails-42

如果你正在构建的应用需要发送邮件,我们都赞同绝不能阻塞控制器,因此异步发送必不可少。为了实现这一点,我们需要借助可以在后台处理任务的异步处理库将邮件发送代码从原始请求/响应周期中移出。

我们如何确信我们的代码在进行此更改时会如预期的那样行事?在这篇博文中,我们将研究测试它的一种方法。我们将使用MiniTest(因为Rails内置了这个框架),但是这里介绍的概念可以很容易地转换为RSpec。

好消息是从Rails 4.2开始,异步发送邮件比以前更容易。我们将在我们的示例中使用Sidekiq作为队列系统,但是由于ActionMailer#deliver_later构建在Active Job之上,该接口很干净,使得异步处理库的使用不可知。这意味着如果我刚才没有提到它,作为开发者或者用户的你将不会知情。设置队列系统是另一个话题,你可以在Getting Started With Active Job了解更多。

不要依赖具体

在我们的示例中,我们假设Sidekiq和它的依赖关系已经正确配置,因此特定于此场景的唯一一段代码是声明Active Job将使用哪个队列适配器:

1
2
3
4
5
6
7
8
# config/application.rb

module OurApp
  class Application < Rails::Application
    ...
    config.active_job.queue_adapter = :sidekiq
  end
end

Active Job在隐藏所有的实质的队列实现细节上做得很好,使得它对于Resque、Delayed Job或其它队列可以用同样的方式工作。所以如果我们要换用Sucker Punch,唯一要做的更改就是在引用相应的依赖包后,将队列适配器从:sidekiq切换到:sucker_punch。

基于Active Job

如果你刚开始使用Rails 4.2,或者对Active Job不太了解,Ben Lewis’ intro to Active Job是一个很好的起点。不过,这篇文章还留给我一个细节去盼望,即找到一个干净的、地道的方法来测试一切都如预期的那样工作。

根据本文的目标,我们假设你有:

  • Rails 4.2+
  • 已经设置好使用队列后端的Active Job(例如Sidekiq、Resque等)
  • 一个Mailer

任何Mailer都应该使用这里描述的概念,我们将用以下这封“欢迎邮件”来使我们的示例更实际:

1
2
3
4
5
6
7
8
9
10
11
12
#app/mailers/user_mailer.rb

class UserMailer < ActionMailer::Base
  default from: 'email@example.com'

  def welcome_email(user:)
    mail(
      to: user.email,
      subject: "Hi #{user.first_name}, and welcome!"
    )
  end
end

为了保持事情简单并专注于什么是重要的,我们想在用户注册后给他发送一封“欢迎邮件”。

这就像Rails指南邮件程序示例中的那样:

1
2
3
4
5
6
7
8
9
10
# app/controllers/users_controller.rb

class UsersController < ApplicationController
  ...
  def create
    ...
    # Yes, Ruby 2.0+ keyword arguments are preferred
    UserMailer.welcome_email(user: @user).deliver_later
  end
end

Mailer应该做最后的任务

接下来,我们要确保控制器内的任务能完成我们预期的。

在测试指南中,在Custom Assertions And Testing Jobs Inside Other Components上面的部分教给我们大约半打这样的自定义断言。

或许你的第一本能是立刻使用[assert_enqueued_jobs][assert-enqueued-jobs]来测试每次创建新用户时我们是否将邮件发送任务放入队列。

你可以这样做:

1
2
3
4
5
6
7
8
9
10
11
12
# test/controllers/users_controller_test.rb

require 'test_helper'

class UsersControllerTest < ActionController::TestCase
  ...
  test 'email is enqueued to be delivered later' do
    assert_enqueued_jobs 1 do
      post :create, {...}
    end
  end
end

即使你这样做,你也会惊讶于测试的失败,被告知assert_enqueued_jobs没有定义而无法使用。

这是因为我们的测试继承自ActionController::TestCase,它在编写时没有包含ActiveJob::TestHelper。

但是我们可以快速地修复这个问题:

1
2
3
4
5
6
# test/test_helper.rb

class ActionController::TestCase
  include ActiveJob::TestHelper
  ...
end

假设我们的代码能完成我们预期的,我们的测试现在应该是绿色的。

这是个好消息。我们可以继续重构代码,添加新功能,或者添加新测试。让我们跟随后者,测试我们的邮件是否被发送,以及它是否有预期的内容。

作为delivery_method选项被配置成:test的结果,ActionMailer能给我们提供一个所有发出邮件的数组。我们可以通过ActionMailer::Base.deliveries访问它。当发送的邮件串联时,测试邮件是否被真正发送就非常简单。我们要做的就是在行动完成后,检查交付数量增加1。用MiniTest来写的话,就像下面这样:

1
2
3
assert_difference 'ActionMailer::Base.deliveries.size', +1 do
  post :create, {...}
end

我们的测试是实时发生的,但正如我们在本文开头赞同的那样,不能阻塞控制器并在后台任务中发送邮件,所以我们现在需要协调一切,以确保我们的系统是确定的。因此,在我们的异步世界中,我们需要首先执行所有入队的任务,然后才能评估其结果。要执行待处理的Active Job任务,我们将使用perform_enqueued_jobs

1
2
3
4
5
6
7
8
9
test 'email is delivered with expected content' do
  perform_enqueued_jobs do
    post :create, {...}
    delivered_email = ActionMailer::Base.deliveries.last

    # assert our email has the expected content, e.g.
    assert_includes delivered_email.to, @user.email
  end
end

缩短反馈循环

迄今为止,我们都在进行功能性测试以确保我们的控制器如预期的那样行事。但是,当代码中的更改可能会破坏我们发送的邮件时,如何单元测试邮件程序才能缩短反馈循环并获得快速洞察?

Rails测试指南建议在这里使用fixtures,但我发现它们太过脆弱。特别是在开始的时候,当还在试验邮件的设计或内容时,变化会很快地使它们过时,导致测试变红。相反,我偏向使用assert_match来关注应该是邮件正文部分的关键元素。

为了这个目的和更多(比如抽象处理多部分邮件的逻辑),我们可以构建我们的自定义断言。这使我们能够扩展MiniTest标准断言Rails专用断言。这也是创建我们自己的领域专用语言(DSL)进行测试的一个很好的示例。

让我们在test目录中创建一个shared文件夹来托管我们的SharedMailerTests模块。我们的自定义断言看起来像这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# /test/shared/shared_mailer_tests.rb

module SharedMailerTests
  ...
  def assert_email_body_matches(matcher:, email:)
    if email.multipart?
      %w(text html).each do |part|
        assert_match matcher, email.send("#{part}_part").body.to_s
      end
    else
      assert_match matcher, email.body.to_s
    end
  end
end

接下来,我们要让邮件程序测试意识到这个自定义断言,所以我们把它混合在ActionMailer::TestCase中。我们可以以类似于我们在ActionController::TestCase中包含ActiveJob::TestHelper的方式来做到这点:

1
2
3
4
5
6
7
8
# test/test_helper.rb

require 'shared/shared_mailer_tests'
...
class ActionMailer::TestCase
  include SharedMailerTests
  ...
end

请注意,我们首先需要在test_helper中依赖我们的shared_mailer_tests。

这些就绪后,我们现在可以确保我们的邮件包含我们预期的关键元素。想象一下,我们想确保我们发送给用户的URL包含一些用于追踪的特定UTM参数。我们现在可以使用我们的自定义断言配合我们的老朋友perform_enqueued_jobs做到这些:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# test/mailers/user_mailer_test.rb

class ToolMailerTest < ActionMailer::TestCase
  ...
  test 'emailed URL contains expected UTM params' do
    UserMailer.welcome_email(user: @user).deliver_later

    perform_enqueued_jobs do
      refute ActionMailer::Base.deliveries.empty?

      delivered_email = ActionMailer::Base.deliveries.last
      %W(
        utm_campaign=#{@campaign}
        utm_content=#{@content}
        utm_medium=email
        utm_source=mandrill
      ).each do |utm_param|
        assert_email_body_matches utm_param, delivered_email
      end
    end
  end
end

结论

让ActionMailer基于Active Job之上,使得从同步发送邮件切换到通过队列发送邮件变得非常简单,就如同从deliver_now切换到deliver_later那样。

由于Active Job使得设置任务基础设施(不需要知道太多你正在使用的队列系统)如此容易,你的测试不再需要关心是否以后会切换到Sidekiq或Resque。

让你的测试设置正确以便于可以充分利用Active Job提供的新的自定义断言可能会有点棘手。希望本教程能使你对整个过程更加明白。

附:你有ActionMailer或Active Job的经验吗?有任何建议吗?有什么陷阱?我们很乐意听到你的经验。

保护你的Paperclip下载

英文原文:https://thewebfellas.com/blog/protecting-your-paperclip-downloads

去年11月份,当我首次在博客上谈到Paperclip时,我简要介绍了在控制器后面隐藏文件,而不是简单地将它们放在公共目录中展示给大家看。从那时起,我就注意到如何真正做到这点的问题定期地在Rails论坛上出现。几周前,我不得不弄清楚如何更新我们的某些代码来保护我们已经从本地文件系统迁移到Amazon S3的资产。所以我认为这可能是一个值得分享的技巧。

模型

我将使用来自我原先的Paperclip博文中的Getting clever with validations部分的Track模型的一个轻微更新版本。以下是代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Track < ActiveRecord::Base

  has_attached_file :mp3,
                    :url => '/:class/:id/:style.:extension',
                    :path => ':rails_root/assets/:class/:id_partition/:style.:extension'

  validates_attachment_presence :mp3
  validates_attachment_content_type :mp3, :content_type => ['application/mp3', 'application/x-mp3', 'audio/mpeg', 'audio/mp3']
  validates_attachment_size :mp3, :less_than => 20.megabytes

  def downloadable?(user)
    user != :guest
  end

end

这段代码执行以下操作:

  • 定义含有名为mp3的Paperclip附件的Track模型。
  • 配置用于访问mp3资源的URL,例如/track/1/original.mp3。在URL中留下样式属性可以使未来版本的代码去做像生成轨道的10秒预览这样的事情(使用自定义的Paperclip处理器),这些预览可以使用像/track/1/preview.mp3这样的URL访问。
  • 配置Paperclip存储上传文件的路径(例如RAILS_ROOT/assets/tracks/000/000/001/original.mp3,其中RAILS_ROOT是Rails应用程序根目录的路径)——这里重要的是,文件存储在/public目录之外。
  • 定义验证器以确保是mp3文件被上传,该验证器包含有效的内容类型并且限制文件不能太大。
  • 定义downloadable?方法,可用于实现对轨道的用户访问权限。为简单起见,它只允许所有登录的用户访问轨道,不过你可以用应用程序需要的任何逻辑来替换它。

路由和控制器

现在,如果你尝试在视图中提供像下面这样的链接来下载mp3:

1
link_to('Listen', track.mp3.url)

当你点击它的时候将得到这样的路由错误:

1
2
Routing Error
No route matches "/tracks/1/original.mp3" with {:method=>:get}

我通常在routes.rb文件中使用map.connect将Paperclip URL映射到控制器中的download动作,就像下面这样:

1
2
3
ActionController::Routing::Routes.draw do |map|
  map.connect 'tracks/:id/:style.:format', :controller => 'tracks', :action => 'download', :conditions => { :method => :get }
end

不需要使用命名路由,因为你不太可能需要通过名称引用路由。如果需要的话,通过映射到自定义的download动作,你还可以在控制器中提供标准的资源丰富的CRUD动作。清教徒式的REST粉丝可能会坚持将下载映射到单独的资源,并且使用POST请求创建一个新的下载资源:如果你打算做更多而不仅仅是将文件流传输到客户端(例如日志统计,更新账单信息),这可能值得考虑,否则为什么要复杂化事情?

然后,TracksController需要一个download动作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class TracksController < ApplicationController

  SEND_FILE_METHOD = :default

  def download
    head(:not_found) and return if (track = Track.find_by_id(params[:id])).nil?
    head(:forbidden) and return unless track.downloadable?(current_user)

    path = track.mp3.path(params[:style])
    head(:bad_request) and return unless File.exist?(path) && params[:format].to_s == File.extname(path).gsub(/^\.+/, '')

    send_file_options = { :type => File.mime_type?(path) }

    case SEND_FILE_METHOD
      when :apache then send_file_options[:x_sendfile] = true
      when :nginx then head(:x_accel_redirect => path.gsub(Rails.root, ''), :content_type => send_file_options[:type]) and return
    end

    send_file(path, send_file_options)
  end

end

控制器中有很多事情发生,这里是download动作的细节:

  1. 尝试使用:id参数找到Track模型,如果轨道不存在则返回404 Not Found响应。
  2. 把当前用户传递给downloadable?方法,决定是否允许用户下载轨道。
    • 这里假设current_user是应用程序的身份验证代码提供的方法,如果没有用户登录则返回:guest符号,否则返回User对象。这里以及downloadable?方法中使用的实际代码可能需要修改以适应你自己的身份验证代码。
    • 如果不允许用户下载轨道,控制器将返回403 Forbidden响应。
  3. 然后为传递的:style参数生成服务器上的mp3文件的路径。
  4. 然后,控制器确保文件存在(如果使用无效的:style参数则不会),并且请求的文件扩展名与文件的实际扩展名相匹配。如果需要,它返回400 Bad Request。
  5. 被用于send_file方法的散列选项将与mp3文件的MIME类型一起被初始化。
    • 我在这里使用mimetype-fu插件(使用ruby script/plugin install git://github.com/mattetti/mimetype-fu.git安装),而不是mp3附件的content_type属性,以允许将来不同的样式可能会使用不同的文件类型的功能。
  6. 控制器支持标准流传输,使用Lighttpd/Apache X-Sendfile或Nginx X-Accel-Redirect方法下载文件。
    • 为简单起见,这里使用SEND_FILE_METHOD常量来配置要使用的方法,但是在实际的应用程序中,应该将其存储在某种配置设置中(我推荐使用Luke Redpath的SimpleConfig插件进行此类操作)。
    • 取决于配置的流传输方法,控制器既可以使用send_file(用于标准和X-Sendfile流传输)也可以使用带报头的响应(用于X-Accel-Redirect流传输)。
    • 在Rails应用程序中我们通常使用Nginx,因此我们借助如下的Nginx配置以使用X-Accel-Redirect方法进行文件的流传输:
1
2
3
4
location /assets/ {
  root /path/to/rails_root;
  internal;
}

其中/path/to/rails_root包含我们的Rails应用程序根目录的完整路径。

视图

Track模型通过downloadable?方法提供访问控制逻辑,而在TracksController中的download动作处理mp3文件流传输,这两者使得使用带有Paperclip提供的url方法的link_to帮助器在视图中提供下载链接成为可能。例如,index视图可能如下所示:

1
2
3
4
5
<ul>
<% @tracks.each do |track| %>
  <li><%= link_to('Listen', track.mp3.url) %></li>
<% end %>
</ul>

扩展到Amazon S3

更新!Paperclip的最新版本包含到期的URL support

如果你的mp3存储在本地文件系统上,上面的代码可以正常地工作,但如果你的站点开始增长,并且你需要在存储空间和下载容量方面进行扩展,那么你可能需要迁移到S3。

Paperclip提供的S3存储模块目前使用RightAWS,而2.3.1+版已经开始使用AWS::S3作为替代。请注意,如果要使用位于欧洲的S3存储桶,则此更改确实会导致一些问题,因此如果这对你来说是个问题的话,你可能需要继续使用2.3.0版本。我将在下面的代码中覆盖这两个版本。

更改存储模块

首先要更改的是Track模型中的has_attached_file定义:

1
2
3
4
5
6
7
has_attached_file :mp3,
                  :url => ':s3_domain_url',
                  :path => 'assets/:class/:id/:style.:extension',
                  :storage => :s3,
                  :s3_credentials => File.join(Rails.root, 'config', 's3.yml'),
                  :s3_permissions => 'authenticated-read',
                  :s3_protocol => 'http'

这里最明显的更改分别依次为指定存储模块,凭据文件位置,上传文件权限和通信协议方面的:storage、:s3_credentials、:s3_permissions和:s3_protocol选项。

凭据YAML文件用于指定你的S3帐户的访问密钥、私有访问密钥和存储桶名称,应该如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
common: &common
  access_key_id: your_access_key_id_goes_here
  secret_access_key: your_secret_access_key_goes_here

development:
  <<: *common
  bucket: app-name-development

test:
  <<: *common
  bucket: app-name-test

production:
  <<: *common
  bucket: app-name-production

为每个环境配置单独的S3桶是个好主意,可以防止意外混淆测试文件与生产文件!

:s3_permissions选项允许你指定四个固定访问策略中的一个,在这种情况下,authenticated-read被用于设置读取访问仅限于对象所有者或经过身份验证的用户。

对:url和:path选项的更改略微不那么明显::url被设置为“:s3_domain_url”,它是使用虚拟托管风格的桶访问的Paperclip插补(如果使用欧洲桶这尤其重要,因为这是首选的访问方式);:path被Paperclip用于生成存储在S3上的对象的密钥名称。

通过这些更改,上传到Track模型的新文件将使用从:path选项生成的密钥存储在相应的S3桶中,例如http://app-name-development.s3.amazonaws.com/assets/tracks/1/original.mp3。

重定向不再有流传输和时间

迁移到S3的好处之一是,你的服务器不再需要自己涉及将数据流传输到客户端(使用X-SendFile和X-Accel-Redirect等技术可以帮助你给Rails进程解除负担,但服务器仍然必须做所有的工作)。作为将文件数据发送给客户端的替代,控制器中的download动作现在需要处理权限检查,然后将客户端重定向到S3上的文件以进行下载。

然而,这会有个问题,因为上传文件正在使用阻止公开访问它们的authenticated-read固定访问策略。幸运的是,S3提供一种方法,可以为仅在特定时间段内运行的私有内容生成已验证的URL:然后我们的控制器可以将客户端重定向到此临时URL以启动下载,但如果有人获取URL的详细信息并尝试在以后访问该文件,那么他们将被告知该URL已经过期。

更新的代码如下所示:

1
2
3
4
5
6
7
8
9
def download
  head(:not_found) and return if (track = Track.find_by_id(params[:id])).nil?
  head(:forbidden) and return unless track.downloadable?(current_user)

  path = track.mp3.path(params[:style])
  head(:bad_request) and return unless params[:format].to_s == File.extname(path).gsub(/^\.+/, '')

  redirect_to(AWS::S3::S3Object.url_for(path, track.mp3.bucket_name, :expires_in => 10.seconds))
end

在控制器中有两个主要区别:

  • 没有检查以确保对象存在,因为这将增加到S3的额外请求的开销——让S3去为不存在的对象返回适当的响应吧。
  • 作为流传输文件的替代,它重定向到使用url_for方法生成的临时URL。临时URL设置为在10秒后过期,该时间应足够长,以便重定向去启动下载:如果下载时间超过10秒,只要已经开始,它将继续下去,直到完成为止。

如果你正在使用Paperclip 2.3.0或更旧的版本,因为RightAWS的缘故,控制器重定向代码应如下所示:

1
redirect_to(track.mp3.s3.interface.get_link(track.mp3.s3_bucket.to_s, path, 10.seconds))

重新考虑视图

当使用本地文件系统存储时,Paperclip附件的url方法可被用于链接到TracksController的download动作,但现在文件托管在S3上,url方法返回的是S3桶的URL,这不是我们想要的。

这里有几个选择,路由可以被更改为命名路由,然后视图可以使用如下所示的代码:

1
link_to('Listen', download_track_path(track.id, 'original', 'mp3')

我认为更好的方法是在Track模型中添加一个新方法,使用Paperclip插补来生成下载URL。与此同时,还可以将验证S3 URL的代码从控制器中移到模型中。这里是完整的Track模型:

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
class Track < ActiveRecord::Base

  has_attached_file :mp3,
                    :url => ':s3_domain_url',
                    :path => 'assets/:class/:id/:style.:extension',
                    :storage => :s3,
                    :s3_credentials => File.join(Rails.root, 'config', 's3.yml'),
                    :s3_permissions => 'authenticated-read',
                    :s3_protocol => 'http'

  validates_attachment_presence :mp3
  validates_attachment_content_type :mp3, :content_type => ['application/mp3', 'application/x-mp3', 'audio/mpeg', 'audio/mp3']
  validates_attachment_size :mp3, :less_than => 20.megabytes

  def downloadable?(user)
    user != :guest
  end

  def download_url(style = nil, include_updated_timestamp = true)
    url = Paperclip::Interpolations.interpolate('/:class/:id/:style.:extension', mp3, style || mp3.default_style)
    include_updated_timestamp && mp3.updated_at ? [url, mp3.updated_at].compact.join(url.include?("?") ? "&" : "?") : url
  end

  def authenticated_url(style = nil, expires_in = 10.seconds)
    AWS::S3::S3Object.url_for(mp3.path(style || mp3.default_style), mp3.bucket_name, :expires_in => expires_in, :use_ssl => mp3.s3_protocol == 'https')
  end

end

对于RightAWS (Paperclip <= 2.3.0),验证URL的代码略有不同:

1
2
3
def authenticated_url(style = nil, expires_in = 10.seconds)
  mp3.s3.interface.get_link(mp3.s3_bucket.to_s, mp3.path(style || mp3.default_style), expires_in)
end

然后整理download动作,将重定向更改为:

1
redirect_to(track.authenticated_url(params[:style]))

现在视图可以链接到下载了:

1
link_to('Listen', track.download_url)

结束

希望这里我给了你足够的信息,以便你开始在自己的Rails应用程序中实施受保护的下载。但是请不要忘记,如果你使用的是欧洲S3桶,需要进行一些修补,否则会出现以下错误:

1
AWS::S3::PermanentRedirect: The bucket you are attempting to access must be addressed using the specified endpoint. Please send all future requests to this endpoint.

当你尝试并且上传时——彻底地修复将是另一天的工作!

FAQ

如何强制下载文件,而不是在浏览器中显示?

如果你使用的是文件系统存储模块,那么当使用send_file方法时,Rails会给attachment设置Content-Disposition报头,你不需要做任何事情。IE可能仍然会坚持在线显示文件,所以你可能需要用点蛮力,在控制器中给application/octet-stream设置Content-Type报头:

1
send_file_options = { :type => 'application/octet-stream' }

当然你也可以使用一些简单的浏览器嗅探来为智能浏览器返回正确的Content-Type,以及为IE返回application/octet-stream报头。

如果使用S3存储模块,那么你需要将s3_headers选项添加到模型的has_attached_file定义中:

1
2
3
4
5
6
7
8
has_attached_file :mp3,
            :url => ':s3_domain_url',
            :path => 'assets/:class/:id/:style.:extension',
            :storage => :s3,
            :s3_credentials => File.join(Rails.root, 'config', 's3.yml'),
            :s3_permissions => 'authenticated-read',
            :s3_protocol => 'http',
            :s3_headers => { :content_disposition => 'attachment' }

当控制器重定向到S3 URL时,Amazon将发送你在此处指定的报头,强制下载。就像使用文件系统存储一样,你也可以使用这种方法强制Content-Type报头,尽管你将无法基于用户浏览器使用任何类型的浏览器嗅探来选择内容类型:

1
2
3
4
5
6
7
8
has_attached_file :mp3,
            :url => ':s3_domain_url',
            :path => 'assets/:class/:id/:style.:extension',
            :storage => :s3,
            :s3_credentials => File.join(Rails.root, 'config', 's3.yml'),
            :s3_permissions => 'authenticated-read',
            :s3_protocol => 'http',
            :s3_headers => { :content_type => 'application/octet-stream', :content_disposition => 'attachment' }