Natural sort order is what we usually see in most file managers when browsing a list of numbered files, that it sorts the numerical part of the file name as a number instead of comparing it character by character. For instance:

Lexicographical sort Natural sort
1.txt 1.txt
10.txt 2.txt
100.txt 3.txt
101.txt 4.txt
102.txt 5.txt
103.txt 10.txt
104.txt 11.txt
105.txt 12.txt
11.txt 13.txt
12.txt 14.txt
13.txt 15.txt
14.txt 100.txt
15.txt 101.txt
2.txt 102.txt
3.txt 103.txt
4.txt 104.txt
5.txt 105.txt

Lexicographical sort can be easily implemented using String::compareToIgnoreCase, but it is not very acceptable for end users. However for natural sort, things is actually a little more complicated.


The Definition

There is a small Wikipedia article on Natural Sort Order, which mentioned the following definition for natural sort order:

Natural sort order is an ordering of strings in alphabetical order, except that multi-digit numbers are ordered as a single character.

However, there’s still some undefined behavior about this sort order when case-insensitivity is involved alongside treating numbers as a whole. In an article discussing natural sort order in C, the author mentioned the following case:

Name
Test1
tesT1
test1
tEst2
test2
test3

These string is listed in a reasonable way, however if you simply define the natural sorting to be sorting chunks of characters and numbers individually, the Test string have to be equal to test so that the ordering won’t be deterministic.

And there’s another case involving leading zeros:

Name
test1
test01
test001
test01a

Intuitively, things that are shorter should come before those longer ones, so equal numbers with less leading zeros should come before those with more. Furthermore, if there’s more text following the number, it should come after those without, thus the aforementioned leading zero comparison has a lower priority.

I didn’t find any standard for these corner cases, so I’m merely deciding on the most reasonable approach.

Various Implementations

There’s an article on CodingHorror and a StackOverflow question that referenced some existing implementations.

And here are some additional implementations that I found inside some battlefield applications:

But there’s some common issues in existing implementations:

  • Use of regular expression: The comparator is frequently evaluated during the sort, so performance is our concern and splitting on regular expressions is not what we expect under such conditions.
  • Memory allocation: Also due to the frequent invocation, we should avoid memory allocation (e.g. actually building those “chunks”), and this can be done by iteration over the two strings.
  • Unicode awareness: Unicode code points are not necessarily single chars (think of surrogates), so those code point related methods should be used instead of simple increment to advance the index of current “character”.
  • Locale awareness: Instead of c >= '0' && c <= '9', one should use Character.isDigit() and Character.digit() for getting numbers out of a character.
  • Correctness: Some implementations cannot handle the two aforementioned corner cases properly.
  • Coding style: Some implementations are simply not so readable.

My Implementation

So I wrote my own implementation with those issues in mind, and it basically iterates over the two string and handles consecutive digits together. Most of the code is self-explanatory and the comments can also describe the algorithm:

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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
public class NaturalOrderComparator implements Comparator<String> {

private static final int DIGIT_RADIX = 10;

@Override
public int compare(String string1, String string2) {
int start1 = 0;
int start2 = 0;
int leadingZeroCompareResult = 0;
while (start1 < string1.length() && start2 < string2.length()) {
int codePoint1 = string1.codePointAt(start1);
int codePoint2 = string2.codePointAt(start2);
// Check if both code points are digits.
if (!Character.isDigit(codePoint1) || !Character.isDigit(codePoint2)) {
if (!codePointEqualsIgnoreCase(codePoint1, codePoint2)) {
return codePointCompareToIgnoreCase(codePoint1, codePoint2);
}
start1 = string1.offsetByCodePoints(start1, 1);
start2 = string2.offsetByCodePoints(start2, 1);
continue;
}
// Get end of current number.
int end1 = start1;
do {
end1 = string1.offsetByCodePoints(end1, 1);
} while (end1 < string1.length() && Character.isDigit(string1.codePointAt(end1)));
int end2 = start2;
do {
end2 = string2.offsetByCodePoints(end2, 1);
} while (end2 < string2.length() && Character.isDigit(string2.codePointAt(end2)));
// Get start of current number without leading zeros.
int noLeadingZeroStart1 = start1;
while (noLeadingZeroStart1 < end1 && Character.digit(string1.codePointAt(
noLeadingZeroStart1), DIGIT_RADIX) == 0) {
noLeadingZeroStart1 = string1.offsetByCodePoints(noLeadingZeroStart1, 1);
}
int noLeadingZeroStart2 = start2;
while (noLeadingZeroStart2 < end2 && Character.digit(string2.codePointAt(
noLeadingZeroStart2), DIGIT_RADIX) == 0) {
noLeadingZeroStart2 = string2.offsetByCodePoints(noLeadingZeroStart2, 1);
}
// If the two lengths of numbers (without leading zeros) differ, the shorter one comes
// first.
int noLeadingZeroLength1 = string1.codePointCount(noLeadingZeroStart1, end1);
int noLeadingZeroLength2 = string2.codePointCount(noLeadingZeroStart2, end2);
if (noLeadingZeroLength1 != noLeadingZeroLength2) {
return noLeadingZeroLength1 - noLeadingZeroLength2;
}
// If any two digits starting from the first non-zero ones differs, the less one comes
// first.
for (int digitIndex1 = noLeadingZeroStart1, digitIndex2 = noLeadingZeroStart2;
digitIndex1 < end1; digitIndex1 = string1.offsetByCodePoints(digitIndex1, 1),
digitIndex2 = string2.offsetByCodePoints(digitIndex2, 1)) {
int digit1 = Character.digit(string1.codePointAt(digitIndex1), DIGIT_RADIX);
int digit2 = Character.digit(string2.codePointAt(digitIndex2), DIGIT_RADIX);
if (digit1 != digit2) {
return digit1 - digit2;
}
}
// If the two numbers are the same, the one with less leading zeros (shorter) comes
// first.
int leadingZeroLength1 = string1.codePointCount(start1, noLeadingZeroStart1);
int leadingZeroLength2 = string2.codePointCount(start2, noLeadingZeroStart2);
if (leadingZeroLength1 != leadingZeroLength2) {
if (leadingZeroCompareResult == 0) {
leadingZeroCompareResult = leadingZeroLength1 - leadingZeroLength2;
}
}
start1 = end1;
start2 = end2;
}
// If one of the two strings is exhausted first, it comes first.
int remainingLength1 = string1.codePointCount(start1, string1.length());
int remainingLength2 = string2.codePointCount(start2, string2.length());
if (remainingLength1 != remainingLength2) {
return remainingLength1 - remainingLength2;
}
// The one with less leading zeros (shorter) comes first if others are the same.
if (leadingZeroCompareResult != 0) {
return leadingZeroCompareResult;
}
// Fall back to plain comparison.
return string1.compareTo(string2);
}

// @see String#regionMatches(boolean, int, String, int, int)
private static boolean codePointEqualsIgnoreCase(int codePoint1, int codePoint2) {
codePoint1 = Character.toUpperCase(codePoint1);
codePoint2 = Character.toUpperCase(codePoint2);
if (codePoint1 == codePoint2) {
return true;
}
codePoint1 = Character.toLowerCase(codePoint1);
codePoint2 = Character.toLowerCase(codePoint2);
return codePoint1 == codePoint2;
}

// @see String.CaseInsensitiveComparator#compare(String, String)
private static int codePointCompareToIgnoreCase(int codePoint1, int codePoint2) {
if (codePoint1 != codePoint2) {
codePoint1 = Character.toUpperCase(codePoint1);
codePoint2 = Character.toUpperCase(codePoint2);
if (codePoint1 != codePoint2) {
codePoint1 = Character.toUpperCase(codePoint1);
codePoint2 = Character.toUpperCase(codePoint2);
if (codePoint1 != codePoint2) {
return codePoint1 - codePoint2;
}
}
}
return 0;
}
}

The code is also available as a GitHub gist and is licensed under the Apache 2.0 license.

Further Considerations

In fact, to account for Unicode’s compatibility decomposition, canonical composition and case folding, an ICU Normalizer should be used with mode NFKC_Casefold. However, since the ICU package is not available on Android until Android Oreo, and the JDK implementation didn’t account for such level of correctness, I’m currently leaving it out and you can trivially add it by applying the normalization before comparison.

For years OpenWeather has been my favorite GNOME extension for quick and beautiful weather information. However today when I looked at my top bar again for weather, I had the impression as if something is missing:

And this is what it used to be like:

My degree Celsius symbol is missing!


Finding the culprit

The first thing I thought was, is this specific to OpenWeather? And I was surprised to find out that when I googled “degree Celsius symbol”, the °C as U+00B0 ° degree sign plus U+0043 C latin capital letter c is present, while ℃ as U+2103 ℃ degree celsius is simply empty. However a <span> with font-family: serif on Wikipedia showed the symbol correctly, so this must be a font/fontconfig issue.

And I lessed my own fontconfig configuration with /etc/fonts/local.conf:

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
<?xml version="1.0"?>
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
<fontconfig>
<match>
<test name="family"><string>sans-serif</string></test>
<edit name="family" mode="prepend" binding="strong">
<string>WenQuanYi Micro Hei</string>
<string>WenQuanYi Zen Hei</string>
<string>WenQuanYi Bitmap Song</string>
<string>DejaVu Sans</string>
<string>Noto Color Emoji</string>
</edit>
</match>
<match>
<test name="family"><string>serif</string></test>
<edit name="family" mode="prepend" binding="strong">
<string>DejaVu Serif</string>
<string>WenQuanYi Bitmap Song</string>
<string>WenQuanYi Zen Hei Sharp</string>
<string>AR PL UMing CN</string>
<string>AR PL UMing TW</string>
<string>AR PL New Sung</string>
<string>Noto Color Emoji</string>
</edit>
</match>
<match>
<test name="family"><string>monospace</string></test>
<edit name="family" mode="prepend" binding="strong">
<string>WenQuanYi Micro Hei Mono</string>
<string>WenQuanYi Zen Hei Mono</string>
<string>WenQuanYi Bitmap Song</string>
<string>DejaVu Sans Mono</string>
<string>Noto Color Emoji</string>
</edit>
</match>
</fontconfig>

So the different order of font families in sans-serif and serif might be the issue. I googled a while for finding out the font selected by fontconfig for a glyph, and found an article suggesting the use of FC_DEBUG and pango-view:

1
FC_DEBUG=4 pango-view -q --font='<YOUR_FONT_FAMILY>' -t '<YOUR_CHARACTER>' 2>&1 | grep -o 'family:"[^"]+' | cut -c 10- | tail -n 1

The latter part basically filters out the last printed family name from the tedious debug output. And when I run this for my degree Celsius symbol (), I found:

What’s wrong with WenQuanYi Micro Hei? I’ve been using it for years, and now it’s giving me nothing for degree Celsius symbol? And why did fontconfig ever choose it seemed to have no such glyph?

Confused by this I opened my wqy-microhei.ttc with FontForge, and was again surprised to find out that my WenQuanYi Micro Hei is providing an empty glyph (instead of nothing) for certain codepoints!

This is crazy, and I’m left wondering how my degree Celsius symbol ever worked in the past few years.

Finding the way to fix it

There must be some configuration that allow me to blacklist certain glyphs in a font. And after another round of digging around, I found some interesting results.

The first one is a bug on fontconfig Bugzilla which requested such support back in 2006 and was marked fixed in 2011. Feeling excited, I scrolled down and found a single line of comment from the dev:

This is fixed in master with target=scan charset editing.

After all the hard work, why didn’t he or she celebrate the moment of closing this bug with a more detailed explanation, or at least a helpful pointer? Even after reading though man fonts-conf, I found nothing describing how I can edit the charset of a font family. After all, the <charset> element says it must contain one or more integer, however the whole charset normally contains a huge amount of hexadecimal integers.

So I searched harder and found another answer on StackOverflow. It suggested an undocumented way of configuration that’s mentioned somewhere in a RedHat bug thread, which was its only appearance on the whole Internet:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<match target="scan">
<test name="family" compare="not_eq">
<string>VL Gothic</string>
</test>
<edit name="charset" mode="assign">
<minus>
<name>charset</name>
<range>
<int>0x0021</int>
<int>0x00FF</int>
</range>
</minus>
</edit>
</match>

This is just what i wanted! So I happily applied this to my mischievous WenQuanYi Micro Hei, changing the compare to "eq" and setting the range to include 0x2103 (My precious degree Celsius symbol). Finally, sudo fc-cache -f && fc-cache -f! [sigh of relief]

Nope.

This could have worked, in some time of the history, but is not working for now, on my latest Arch Linux installation.

No.

But wait. I did saw something mentioning, we have a <charset> element and it must contain some integers? So I started to coin my own solution (You should also do the same for WenQuanYi Micro Hei Mono):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<match target="scan">
<test name="family">
<string>WenQuanYi Micro Hei</string>
</test>
<edit name="charset">
<minus>
<name>charset</name>
<charset>
<int>0x2103</int>
<int>0x2109</int>
<int>0x212B</int>
<int>0x2164</int>
<int>0x2169</int>
</charset>
</minus>
</edit>
</match>

sudo fc-cache -f && fc-cache -f, pango-view again to confirm, and Alt+F2 r to restart my GNOME shell. It worked! After all these hours, my degree Celsius symbol is back and alive again!

A final note

The above solution is only a workaround, and to actually fix this, The Wenquanyi Micro Hei font itself should be patched. In fact this was what I first tried when I surprisingly found out those empty glyphs.

However, when I went to wenq.org, there latest update was posted in late 2011 (We are in 2018 now), and their forum is giving me a blank page with certain resources returning 404. Their SourceForge issue tracker is also unattended for years with that notorious hangul advance bug still open. Sadly, the whole project seems dead. So I decided to work around instead of fixing this.

And one sentence I encountered during my search kept lingering in my mind:

When you gaze long into fontconfig, fontconfig also gazes into you.

Update: With another link on their official website, I was able to find their real bug tracker and this bug, and a similar bug. The dev said it’s fixed, but maybe some recent fontconfig or freetype changes brought it back. Maybe some day I should fork it and try to actually fix these bugs.

And according to the dev’s comment, those glyphs existed in Droid Sans Fallback, so a proper fix to my configuration should also include adding Droid Sans Fallback to the sans-serif and monospace families, right below the WenQuanYi Micro Hei entry.

前言

JavaScript 是一门灵活的动态语言,在实现应用功能时十分易用。最近有写作图形界面程序的需要,又比较喜爱 GTK+ 3 的设计,因此想到可以尝试使用 JavaScript 来完成任务。

选择运行时

首选的运行时当然是 node,因为它有着良好的 ES2015 特性支持,并且可以方便地使用 npm 管理的模块。在经过一番搜索之后,我找到了 WebReflection/node-gtk 这个项目,却发现它无法使用 node-gyp 在我的 Arch Linux 上进行编译。另一个 [creationix/node-gir] 提供了对 GObject Introspection 的绑定,但是在它的 README 中写明了有 bug 和内存管理等问题,看上去也难以令人满意。

因此,我选择了使用 GJS 作为运行时。虽然没有 node 的 ES2015 支持和使用不同模块的便利,但对于使用 GObject Introspection 的绑定而言足够可靠。

编写应用程序

GNOME 官方提供了 一个简单的 GJS 程序示例,可以按照它的架构进行编程。

文档

技巧

通过构造器设置属性

Gtk.Widget 的所有属性均可以通过向构造器传入的 Object 进行设置,而可用的属性可以通过运行 gtk3-widget-factory,然后启用 GtkInspector 来查看和实验。

布局

可以一直使用 Gtk.Grid 进行布局,它的功能类似于 Android 中的 LinearLayout。可以设置它的 border_widthrow_spacingcolumn_spacing 来控制留白,以及设置 expandhexpandvexpand 控制大小。通过调用(继承自 Gtk.Container 并经过覆盖)add() 方法即可按顺序添加子控件,不需要使用 attach() 那样复杂的功能。

设置 CSS Class

例如给 Gtk.Button 添加 suggested-action CSS 类使其变蓝:

1
button.get_style_context().add_class('suggested-action');

控件杂项

Gtk.Entry 相当于 Android 中的 EditText。可以通过设置 width_char 来修改 Gtk.Entry 的最小大小。

Gtk.Widigetsensitive 属性相当于 Android 中的 android:enabled

结语

Gjs 使得我们可以使用灵活的 JavaScript 编写美观的原生程序,总体来说体验是很好的。

我把自己编写的一个小程序放在了 GitHub Gist,可以作为参考。

Gjs 应用程序 OpticalSystem

最后,在编写这篇文章时,我找到了另一个 结合了 Gjs、NPM 和 Babel 的示例项目,读者也可以参考它进行编程。

前言

临近毕设,学院里提供了毕业设计报告的 LaTeX 模板,于是决定离开简单的 Markdown ,开始使用 LaTeX 进行写作。以下就是我使用过程中的一些记录。

样例项目

我对学院提供的旧 LaTeX 模板进行了较多的修改,使其符合了学院方面的最新格式要求,并且修复了设置字号时行间距不正确等等错误,可以直接使用或者用于参考学习:

浙江大学计算机科学与技术、软件工程专业本科毕业设计开题报告 LaTeX 模板

其中的 install-fonts.sh 可以用于从 Windows 分区获取需要的字体并进行安装。

安装 LaTeX

在 Arch Linux 上安装 LaTeX(TeXLive)较为简单,可以参考 Arch Wiki 安装对应的软件包。

也可以直接执行以下命令进行安装,这样基本不会在使用时遇到无法找到常用宏包的情况。

1
sudo pacman -S texlive-most texlive-langchinese

构建文档

xeCJK 是提供 LaTeX 中文支持的宏包,并且依赖于 XeLaTeX,因此,我们需要使用 xelatex 命令进行构建。

LaTeX 在构建交叉索引时需要多次运行,才能最终解析所有的引用,并且期间需要 BibTeX 对参考文献数据库进行处理。因此,一般的手动构建命令是:

1
2
3
4
xelatex main
bibtex main
xelatex main
xelatex main

不同于部分网页上给出的示例,这些命令都可以接受不带后缀名的参数,并且有时写出后缀名可能会阻碍多文件等情况下的正确构建。

对于子目录和多文件使用引用的情况,在主文件中应该使用 include 而非 input,否则会需要其他处理。

为了简化构建步骤,实际上应当使用 mklatex,它会根据需要自动调用各种命令。因此之前的手动构建步骤可以被以下命令替代:

1
mklatex -xelatex main

了解 LaTeX 和解决问题

LaTeX 有很多历史问题带来的不同,也有多重相似但不同的工具选择,因此网上提供的解决方案不一定可以工作,需要自己试验。

可以通过 ShareLaTeX 提供的文档 获得对 LaTeX 的初步了解。但是,这份文档中提到的引用等高级特性的使用可能与我们的方式不同。

推荐使用 TeX – LaTeX Stack Exchange 作为解决问题时的主要信息来源。

可以通过 texdef 命令查看 LaTeX 中命令的定义。例如:

1
texdef -t xelatex -s -c csbachelor thebibliography

零碎知识

  • \:换行,不新建段落。可选参数可以控制空白长度。
  • par:创建段落。parskipparindent 将生效。部分命令需要对段落才能生效,因此可能需要在最后一段文字最后加上 par
  • :一个空格。
  • &:一个 NBSP,不会被分行打断,常用于类似 图~1 这样的场景。
  • hspace{1em}:一个 1 em 长度的空格。
  • renewcommand:修改命令定义。
  • setlength:修改长度变量。
  • setcounter:修改计数器。

结语

LaTeX 十分强大,并且作为纯文本格式依然比 Word 文档等更加清晰、明确、可靠。但是,整个 LaTeX 环境也正因为它的高自由度和复杂,在许多方面缺乏一致性,这给使用者带来了不少困难。

总体来说,在经过大量的学习和尝试之后,我依然认为 LaTeX 是一个优秀的排版工具。它可以让使用者在一次配置之后获得强大的功能,并且在写作过程中始终保持工作在纯文本层面上。

关于各种命令和功能的常见用法,读者可以继续参考我在文章开头提到过的 报告模板,我也会在写作毕业设计论文时继续更新。

前言

因为计算机图形学课程作业的需要,我在使用过 OpenGL 的基础上学习了使用 WebGL 进行二维图形渲染。

WebGL 拥有与 OpenGL ES 相似的 API(前者基于后者),但与 OpenGL 相比,两者缺少基础图形的渲染管线,而是需要手写 shader 进行渲染。

基础

请阅读 WebGL Fundamentals 以获得对于 WebGL 的大体了解。

Shader

在 WebGL 中需要两种 shader 进行渲染。第一种是 vertex shader,负责对顶点进行变换,例如将像素坐标映射到 clip space。而第二种是 fragment shader,负责对像素点进行着色。

Shader 可以有两种参数。第一种是 attribute,在每次调用中不同,例如当前顶点的位置;第二种是 uniform,在渲染过程中共享,例如渲染的变换矩阵。

以下是两个实用的简单 2D shader:

Vertex shader:

1
2
3
4
5
6
7
attribute vec2 a_position;
uniform mat3 u_transformation;
void main() {
gl_Position = vec4((u_transformation * vec3(a_position, 1)).xy, 0, 1);
}

Fragment shader:

1
2
3
4
5
6
7
precision mediump float;
uniform vec4 u_color;
void main() {
gl_FragColor = u_color;
}

初始化

以下是我的一些工具函数,参考了 WebGL Boilerplate 和一部分 WebGL – Less Code, More Fun

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
function createAndCompileShader(gl, id) {
var element = document.getElementById(id);
var type = null;
switch (element.type) {
case 'x-shader/x-vertex':
type = gl.VERTEX_SHADER;
break;
case 'x-shader/x-fragment':
type = gl.FRAGMENT_SHADER;
break;
default:
throw 'Unknown shader type:' + element.type;
}
var source = element.textContent;
var shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
throw 'Error compiling shader:' + gl.getShaderInfoLog(shader);
}
return shader;
}
function createAndLinkProgram(gl, vertexShaderId, fragmentShaderId) {
var program = gl.createProgram();
gl.attachShader(program, createAndCompileShader(gl, vertexShaderId));
gl.attachShader(program, createAndCompileShader(gl, fragmentShaderId));
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
throw 'Error linking program:' + gl.getProgramInfoLog(program);
}
return program;
}

提供数据

以下是我自己写作的一些工具函数:

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
/**
* @param {[float]} value
*/
function setUniformFv(gl, program, name, value) {
gl['uniform' + value.length + 'fv'](gl.getUniformLocation(program, name), value);
}
/**
* @param {string} color '#RRGGBB'
* @param {float=} alpha in [0, 1]
*/
function setUniformColor(gl, program, name, color, alpha) {
if (!/^#[0-9A-Fa-f]{6}$/.test(color)) {
throw 'Invalid color:' + color;
}
setUniformFv(gl, program, name, [
parseInt(color.substr(1, 2), 16) / 0xff,
parseInt(color.substr(3, 2), 16) / 0xff,
parseInt(color.substr(5, 2), 16) / 0xff,
alpha || 1
]);
}
function setUniformMatrix(gl, program, name, value) {
gl['uniformMatrix' + Math.sqrt(value.length) + 'fv'](gl.getUniformLocation(program, name), false, value);
}
/**
* @param {[[float]]} value
*/
function setAttributeArrayFva(gl, program, name, value) {
var buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
var data = new Float32Array(Array.prototype.concat.apply([], value));
gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);
var location = gl.getAttribLocation(program, name);
gl.enableVertexAttribArray(location);
var size = value[0].length;
gl.vertexAttribPointer(location, size, gl.FLOAT, false, 0, 0);
}

变换矩阵

以下代码参考了 WebGL 2D TranslationWebGL Orthographic 3D。需要注意的是,原教程的代码中有部分矩阵错误地以行优先而非列有限的方式进行表示或运算。

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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
function makeIdentityMatrix() {
return [
1, 0, 0,
0, 1, 0,
0, 0, 1
];
}
function makeTranslationMatrix(tx, ty) {
return [
1, 0, 0,
0, 1, 0,
tx, ty, 1
];
}
function makeScalingMatrix(sx, sy) {
return [
sx, 0, 0,
0, sy, 0,
0, 0, 1
];
}
function degreeToRadian(angle) {
return angle / 180 * Math.PI;
}
function makeRotationMatrix(angle) {
angle = degreeToRadian(angle);
var cosa = Math.cos(angle);
var sina = Math.sin(angle);
return [
cosa, sina, 0,
-sina, cosa, 0,
0, 0, 1
];
}
function makeSkewXMatrix(angle) {
angle = degreeToRadian(angle);
var tana = Math.tan(angle);
return [
1, 0, 0,
tana, 1, 0,
0, 0, 1
];
}
function makeSkewYMatrix(angle) {
angle = degreeToRadian(angle);
var tana = Math.tan(angle);
return [
1, tana, 0,
0, 1, 0,
0, 0, 1
];
}
function makeProjectionMatrix(width, height) {
return [
2 / width, 0, 0,
0, -2 / height, 0,
-1, 1, 1
];
}
function multiplyMatrix(a, b) {
// A cdot B = (B^T cdot A^T)^T
var a00 = a[0], a01 = a[1], a02 = a[2];
var a10 = a[3], a11 = a[4], a12 = a[5];
var a20 = a[6], a21 = a[7], a22 = a[8];
var b00 = b[0], b01 = b[1], b02 = b[2];
var b10 = b[3], b11 = b[4], b12 = b[5];
var b20 = b[6], b21 = b[7], b22 = b[8];
return [
b00 * a00 + b01 * a10 + b02 * a20,
b00 * a01 + b01 * a11 + b02 * a21,
b00 * a02 + b01 * a12 + b02 * a22,
b10 * a00 + b11 * a10 + b12 * a20,
b10 * a01 + b11 * a11 + b12 * a21,
b10 * a02 + b11 * a12 + b12 * a22,
b20 * a00 + b21 * a10 + b22 * a20,
b20 * a01 + b21 * a11 + b22 * a21,
b20 * a02 + b21 * a12 + b22 * a22
];
}

近似于 OpenGL 的 API 封装

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
49
50
51
52
53
54
55
56
57
var _gl = null;
var _program = null;
function initialize(gl, program) {
_gl = gl;
_program = program;
}
function setColor(color, alpha) {
setUniformColor(_gl, _program, 'u_color', color, alpha);
}
var _transformations = [];
function getTransformation() {
return _transformations[_transformations.length - 1];
}
function _setTransformation(transformation) {
setUniformMatrix(_gl, _program, 'u_transformation', transformation);
}
function setTransformation(transformation) {
_setTransformation(transformation);
_transformations = [transformation];
}
function pushTransformation(transformation) {
transformation = multiplyMatrix(getTransformation(), transformation);
_setTransformation(transformation);
_transformations.push(transformation);
}
function popTransformation() {
_transformations.pop();
_setTransformation(getTransformation());
}
function drawTriangles(positions) {
setAttributeArrayFva(_gl, _program, 'a_position', positions);
_gl.drawArrays(_gl.TRIANGLES, 0, positions.length);
}
function drawRectangle(x, y, width, height) {
var x1 = x;
var y1= y;
var x2 = x + width;
var y2 = y + height;
drawTriangles([
[x1, y1],
[x2, y1],
[x1, y2],
[x1, y2],
[x2, y1],
[x2, y2]
]);
}

绘制和应对大小改变

以下代码参考了 WebGL Resizing the CanvasWebGL Anti-Patterns

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
function draw(gl) {
gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
gl.clearColor(0xde / 0xff, 0x29 / 0xff, 0x10 / 0xff, 0xff / 0xff);
gl.clear(gl.COLOR_BUFFER_BIT);
var program = createAndLinkProgram(gl, 'vertex-shader', 'fragment-shader');
gl.useProgram(program);
initialize(gl, program);
setColor('#ffdd00');
setTransformation(makeProjectionMatrix(1000, 800));
// Draw here.
gl.flush();
}
function main() {
var canvas = document.getElementById('canvas');
var webgl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
if (!webgl) {
alert("Error initializing WebGL. Your browser may not support it.");
return;
}
var needToDraw = true;
function resizeAndDraw() {
var resized = canvas.width !== canvas.clientWidth || canvas.height !== canvas.clientWidth * 0.8;
if (resized) {
canvas.width = canvas.clientWidth;
canvas.height = canvas.clientWidth * 0.8;
}
if (resized || needToDraw) {
needToDraw = false;
draw(webgl);
}
requestAnimationFrame(resizeAndDraw);
}
requestAnimationFrame(resizeAndDraw);
}
main();

结语

作为一个复杂度适中的示例,我的作业代码可以在 这里 看到。

String 类

String 类表示一个字符串。 在设计上, 字符串类存储的字符串是不可变的(immutable)。想要修改一个字符串的内容只能通过新建一个新的字符串实例来实现。 且对于两个相同的字符串字面量而言, 他们指向的是同一个字符串对象。

并且String类相对于其他类, 具有以下特殊性:

  • String类是Java中唯一的具有重载运算符的类。 String重载了+运算符, 使得对于一个加号左右两边的String能够被连接起来。

成员变量

String类具有以下的关键成员变量:

final char[] value

存储组成这个字符串的字符数组。 因为是final声明, 所以在类被创建之后是不可修改的。

final int count

存储在value[]数组中的字符个数, 在大多数情况下与value.length中的值相同。

final int offset

存储在value[]数组中字符的起始位置。 此变量的创建目的是为了substring()方法提供便利

私有类

private static final class CaseInsensitiveComparator

String类私有类, 其具有compare()方法, 用于提供对于字符串之间的大小写不敏感的比较。

构造器

public String ()

默认构造器, 创建一个空的字符串, 设置value为"",offset为0,count为0。

public String (String str)

拷贝构造器, 根据传入的参数str,新建一个新的字符串实例。但由于字符串是不可变的, 所以拷贝是以浅拷贝的方式执行的。

public String (char[] data)

根据传入的data字符数组, 初始化一个新的字符串实例。

public String (char[] data, int offset, int count)

根据传入的data字符数组, offset偏移量和count数据, 来初始化一个新的字符串实例。

public String (byte[] ascii, int hibyte, int offset, int count)

使用二进制的方式初始化一个字符串, 其中ascii数组中存放每一个字符的ascii码。 并且最终每一位的值c = (char)(((hibyte & 0xff) << 8) | (b & 0xff))

即用hibyte填充最终字符的最高八位,以及用ascii数组的值填充低八位。

public String (byte[] ascii, int hibyte)

使用String (byte[] ascii, int hibyte, int offset, int count) 作为 Delegate Constructor, 默认offset为0, count为ascii.length

public String (byte[] data, int offset, String encoding)

使用encoding参数所规定的方式来解码由data数组传入的字符, 并依此新建一个字符串。

public String (StringBuffer buffer)

使用StringBuffer中存储的字符序列构造一个新的字符串。StringBuffer中字符的改动并不会影响通过此构造器构建的字符串。

public String (StringBuilder buffer)

使用StringBuilder中的字符序列构造一个新的字符串, StringBuilder中字符的改动并不会影响通过此构造器构建的字符串。

StringBuffer 类

StringBuffer类表示一个 可变的(changeable) 字符串, 它提供了一系列成员方法来对StringBuffer实例进行修改, 包括insert, replace, delete, appendreverse等。

StringBuffer实例是变长的, 所以尽管在定义时, 可能是以固定长度定义的, 但它的长度都可以进行不断正常。

出于某种特定的原因, 编译器一般使用StringBuffer来重载+运算符对于String类型的运算。 即表达式str_a+str_b, 等价于
new StringBuffer().append(str_a).append(str_b).toString()

成员变量

StringBuffer类拥有以下的关键成员变量:

int count

下一个可用buffer位置的索引, 同时也是当前字符串内容的大小。 注意此处count是public权限, 即程序可以从外部访问该变量。

char[] value

存储StringBuffer中的字符串Buffer本身。

boolean shared

标记Buffer是否与其他对象共享, 如果为真, 则在对字符串进行写操作时, 需要先将其复制一份。

构造器

public StringBuffer()

默认构造器, 在默认情况下, 会构建一个以DEFAULT_CAPACITY为大小的StringBuffer,在JDK1.2中, 这个值为16.

public StringBuffer(int capacity)

这个构造器会新建一个大小为capacitychar数组, 并且将其作为成员变量value的当前值。

public StringBuffer(String str)

这个构造器接受一个String参数,并将其值赋予countvalue,需要注意的是,通过此构造器得到的字符串Buffer的大小是原字符串的大小加上DEFAULT_CAPACITY。但是字符串的大小(length)依然是原字符串

StringBuilder类

StringBuilder类表示一个 可变的(changeable) 字符串, 与StringBuffer类似, 它也提供了诸如insert,replace,delete, append, reverse之类的方法。 但是与StringBuilder不同的是,StringBuilder是不同步的, 只能在一个线程的情况下使用。

关键成员函数

int count

下一个可用buffer位置的索引, 同时也是当前字符串内容的大小。

char[] value

存储StringBuilder中的字符串Buffer本身。

构造器

public StringBuilder()

默认构造器, 创建构建一个以DEFAULT_CAPACITY为大小的StringBuilder

public StringBuilder(int capacity)

这个构造器会新建一个大小为capacity的char数组, 并且将其作为变量value的当前值。

public StringBuilder(String str)

这个构造器会根据传入的String参数,并将其值赋予countvalue。 StringBuilder通过此构造器得到的实例对象与StringBuffer一样, value数组的大小会空余一个DEFAULT_CAPACITY

public StringBuilder(CharSequence seq)

根据传入的CharSequence参数, 新建一个StringBuilder实例。 大小同样是有一个DEFAULT_CAPACITY的空余。

比较异同

String与(StringBuffer及StringBuilder)进行比较

相同点:

  1. 都表示了一个字符串序列
  2. 都是final类, 都不允许被继承
  3. 这个字符串序列都是支持Unicode的
  4. 都支持对于字符串序列内容的部分读取
  5. 都重载了加号运算符, 能够直接使用”+”来连接两个字符串

不同点:

  1. String类型的字符串是 不可变(immutable) 的, 即一个String实例在被创建之后, 其字符串的大小, 内容不能再被改变。 如果需要进行改变的话, 实际上是新建了一个String实例。
  2. 而对于StringBuffer和StringBuilder而言, 其本质上实现了一个变长数组(VLA),所以其字符串大小,内容都是可以进行变化的。在进行修改的时候, StringBuffer和StringBuilder各自实现了一个public void ensureCapacity(int minimumCapacity)的方法, 以确保时刻Buffer数组的大小要大于minimumCapacity, 否则就将数组扩充成其原来size的两倍再加二。
  3. String类型对于字符串本身的操作更多,诸如toLowerCase, toUpperCase这类大小写转换的方法, 以及matches, split,replace这类正则表达式方法, 以及trimconcat这类操作字符串的方法,都仅在String类中实现,而在StringBuffer与StringBuilder类中并没有提供以上接口。

StringBuffer 与 StringBuilder进行比较

相同点:

  1. 前面与String类型共有的相同点不赘述
  2. StringBuffer类与StringBuilder类都实现了一个 可变的(changeable) 字符串对象, 可以通过一系列方法对字符串内容进行诸如增加,删减,插入,替换,翻转等操作。
  3. 实现的字符串buffer的本质都是一个变长数组(VLA), 在任何时刻都尽量保证数组不越界, 如果接近了其边界, 就将数组的大小扩充成原来大小的两倍加二。
  4. 默认的buffer大小DEFAULT_CAPACITY都为16

不同点:

  1. StringBuilder类相较StringBuffer, 多了一个基于CharSequence建立的构造器。
  2. StringBuffer比StringBuilder类多了一个shared指示,以标记buffer内容是否被多个对象所引用。如果被多个引用的话,在进行操作的时候就要将这个buffer再复制一份, 以保证其单引用的性质。 这一点其实是为了保证操作内容的同步化进行的。
  3. 正如之前所说, StringBuffer和StringBuilder的最大不同点在于其操作的同步性。 StringBuffer在其大多数成员方法前都加上了synchronized关键字, 以保证其线程安全。 也就是说, 在同一时间, 只有一个线程能够对同一个StringBuffer对象进行操作,这种限制避免了线程之间读写的冲突。 而对于StringBuilder来说, 就没有这一层限制。故其有可能会引起线程之间的冲突, 但从另一个角度而言, 也减少了诸如线程锁之类的可能的系统开销。 故相比StringBuffer而言, StringBuilder的速度可能会更快一些。 故两者需要权衡使用。

前言

Reveal.js 是一个美观实用 HTML 演示文稿框架,只需要你决定内容,就可以方便地产出外观优雅的演示文稿。你可以在线查看 Reveal.js 的 Demo

为了分享已经制作好的演示文稿,可以使用 GitHub Pages 进行部署,同时也能够不用安装依赖地开启演讲者视图。以下是我建立 slides.zhanghai.me 的过程。

基本

为了建立一个 GitHub Pages 站点,我们需要准备以下一些文件:

  • 404.html:GitHub Pages 将使用此页面作为默认 404 页面的替代,一般可以换成一个符合自己站点风格的页面。
  • CNAME:对于绑定自定义域名的 GitHub Pages,可以使用这个文件指定自己的自定义域名。
  • .nojekyll:我们的站点不需要 Jekyll 的特性干预,因此将它关闭来避免可能的问题。

然后在 GitHub 上建立仓库,执行以下的命令:

1
2
3
4
5
6
7
git init
# 将默认的分支名 master 改为 gh-pages
git symbolic-ref HEAD refs/heads/gh-pages
git commit -am 'Initial commit.'
# 将 your/repo/origin 替换为你的仓库地址
git remote add origin your/repo/origin
git push --set-upstream origin gh-pages

当然,使用自定义域名的话还要配置一下 DNS 记录。

演示文稿

为了部署 Reveal.js 的演示文稿,可以利用 Git 的 Submodule 来完成对 Reveal.js 项目本身的引用。如此以来,我们的站点项目的 Git 仓库就不用跟踪 Reveal.js 中各个文件的状态,又可以记住所引用的 Reveal.js 仓库当时的版本(Commit SHA1)。

比较幸运的是,GitHub Pages 的构建过程是可以支持 Submodule 的。需要注意的是,Submodule 的仓库地址必须采用 https 而非 ssh 协议(否则会收到构建失败的邮件通知)。以下是将 Reveal.js 作为 Submodule 引入的命令:

1
git submodule add https://github.com/hakimel/reveal.js.git

之后将你的 index.html 中对于 Reveal.js 文件的应用都加上 reveal.js/ 的目录前缀即可。

其他详情可以参考我的站点仓库 DreaminginCodeZH/slides.zhanghai.me

结语

Reveal.js 是一个易用而优雅的 HTML 演示文稿框架,借助于它,我已经多年没有用过 PowerPoint 了。至于利用 GitHub Pages 将它放置在网页端,则可以在免运维的情况下可靠地分享自己的演示文稿,同时也可以不用安装大量 npm 依赖来开启演讲者视图,算是一个简单又有用的小技巧。

因此,将我的配置过程和结果记录在此,希望能对他人有所帮助。

前言

Travis CI 是 GitHub 上开源项目采用持续集成的常见选择。为了给 豆芽 提供持续集成版本用于公开测试,我配置了 Travis CI,并自己编写了脚本将构建结果发布到另一个空项目的 Release 中,并将其间的过程和遇到的问题记录于此。

Travis CI 构建 Android 项目的时间较长,因此调试配置时十分耗时。希望我的经验能对他人有所帮助。

Travis CI

Travis CI 分为免费版(travis-ci.org)和付费版(travis-ci.com),两者之间没有相互的链接或说明,第一次配置时容易混淆。开源项目选择免费版即可。配置过程可以参考官方的 Getting StartedAndroid 项目配置

关于 Android 项目有一些较为微妙的配置问题,我自己调试并查阅了一些 Issue 方才解决。

  1. 为了能够找到并下载最新的 Build Tools,需要启用最新版本的 Tools(- tools)。
  2. Lint 过程中如果 Platform Tools 版本低于 SDK 版本则会报错,需要启用最新版本的 Platform Tools(-platform-tools)。
  3. 为了能够找到并下载最新的 Platform Tools,需要已有最新的 Tools,因此与官方给出的样例不同,需要将 - tools 放置在 - platform-tools 之前。

因此我的 Android 部分最终配置如下:

1
android:
  components:
    - tools
    - platform-tools
    - build-tools-24.0.1
    - android-24
    - extra-android-m2repository

详细配置可以参考我的 .travis.yml

启用构建缓存

Gradle 是一个为缓存优化过的工具,因此官方也提供了相应的 开启缓存的方法

1
before_cache:
  - rm -f $HOME/.gradle/caches/modules-2/modules-2.lock
cache:
  directories:
    - $HOME/.gradle/caches/
    - $HOME/.gradle/wrapper/

应用签名

为了给 CI 版本的 APK 签名,需要提供相应的 keystore 和密码。Travis CI 提供了 在设置中定义环境变量 的方式来传递低敏感级的信息。

基于以上文档和一些搜索结果,并参考过 Shadowsocks-Android 的配置方式,我将我的签名配置编写为了从 signing.properties、环境变量、终端输入三个层级进行获取的方式。

以下是我的 signing.gradle

1
2
3
4
5
6
7
8
9
10
11
12
android {
signingConfigs {
release {
Properties signingProperties = new Properties()
signingProperties.load(rootProject.file("signing.properties").newDataInputStream())
storeFile rootProject.file(signingProperties.get("storeFile"))
storePassword signingProperties.get("storePassword") ?: System.getenv("STORE_PASSWORD") ?: System.console()?.readLine("nStore password:")
keyAlias signingProperties.get("keyAlias")
keyPassword signingProperties.get("keyPassword") ?: System.getenv("KEY_PASSWORD") ?: System.console()?.readLine("nKey password:")
}
}
}

在 Android Studio 中进行 Gradle 同步时,System.console() 返回 null,因而密码均为 null,不会中断同步过程,也不影响调试版本的构建。

我创建了 signing.propertiessigning.properties.travis 两个文件,在前者中填写 keystore 的路径并加入 .gitignore,而将后者在 .travis.ymlbefore_script 中复制为 signing.properties

而在 Travis CI 的设置中,添加 STORE_PASSWORDKEY_PASSWORD 两个环境变量即可。建议在环境变量值的两端加上单引号以避免特殊字符被 shell 错误处理。

获取版本信息

直接在 .travis.yml 中调用 git describegit log 等命令是无法成功的,因为 Travis CI 采用了 git clone --depth=50 来进行 clone。相应地,需要先执行 git fetch --unshallow 来完成 clone。

我采用了 语义化版本(Semver)来命名版本。因此,我的版本名称采用了如下方式获取:

1
version="$(git describe --long --tags | sed's/^v//;s/-([0-9]+)-g([0-9a-f]+)/+1.2/')"

例如,在名为 1.0.0-alpha 的 tag 后第 227 次短哈希值为 886f8ce 的 commit,对应的版本名即为 1.0.0-alpha.1+227.886f8ce

然后使用 sed -i 's/versionName .*/versionName "'"${version}"'"/' app/build.gradle 即可更新 build.gradle 中的 versionName 字段。

上传至 Release

GitHub 提供了在 Release 中上传二进制构建输出的功能。然而,如果直接在项目仓库中为每次 commit 添加 Release(和相应的 tag)则未免过于杂乱,因此我选择了新建 一个只有 README 的仓库,并将所有持续集成版本的 Release 创建在此仓库中。

为了实现此功能,我选择了通过 curl 调用 GitHub API 的方式来完成。经过查阅文档和大量的调试,我的脚本最终是如下编写的:

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
#!/bin/bash

set -e

repo="$1"
shift
echo "Repo: ${repo}" >&2

version="$1"
shift
echo "Version: ${version}" >&2

tag="v${version}"
echo "Tag: ${tag}" >&2

body="$1"
shift
echo "Body: ${body}" >&2

# Get old release by tag
echo "Getting old release by tag..." >&2
response="$(curl -H"Authorization: token ${GITHUB_ACCESS_TOKEN}""https://api.github.com/repos/${repo}/releases/tags/${tag}")"
echo "${response}" >&2
old_release_id="$(echo "${response}" | jq -r '.id')"

if [["${old_release_id}" != "null" ]]; then

# Delete old release
echo "Deleting old release..." >&2
response="$(curl -X 'DELETE' -H "Authorization: token ${GITHUB_ACCESS_TOKEN}" "https://api.github.com/repos/${repo}/releases/${old_release_id}")"
echo "${response}" >&2
fi

# Create release
echo "Creating release..." >&2
response="$(curl -H "Authorization: token ${GITHUB_ACCESS_TOKEN}" -H 'Content-Type: application/json' --data "{ "tag_name": $(echo -n"${tag}"| jq -s -R -r @json), "name": $(echo -n"${tag}"| jq -s -R -r @json), "body": $(echo -n"${body}"| jq -s -R -r @json) }" "https://api.github.com/repos/${repo}/releases")"
echo"${response}">&2
upload_url="$(echo "${response}" | jq -r '.upload_url' | sed 's/{?name,label}$//g')"
echo"Upload url: ${upload_url}">&2

for file in"$@"; do
# Upload file
echo"Uploading file: ${file}">&2
name="$(basename "${file}")"
response="$(curl -H "Authorization: token ${GITHUB_ACCESS_TOKEN}" -H "Content-Type: $(file -b --mime-type"${file}")" --data-binary "@${file}" "${upload_url}?name=$(echo -n"${name}"| jq -s -R -r @uri)")"
echo"${response}">&2
done

而设置 GITHUB_ACCESS_TOKEN 环境变量后的用法则如:

1
./upload-to-releases.sh 'DreaminginCodeZH/DouyaCiBuilds' "${version}" "${description}" "douya-ci-${version}.apk"

其他

如果无法确定错误原因,就多加些 echo 或者 cat 吧。

如果希望在 Lint 失败时查看输出,可以在 after_failure 中加入 cat app/build/outputs/lint-results-*.html

我所采用的配置都可以在 豆芽 中找到。

结语

为 Android 项目使用 Travis CI 的过程还算简单,但是也有一些微妙的问题需要解决,这令我花费了不少时间。而将每次的构建输出上传至另一个仓库的 Release 则是我考虑了一段时间后得出的方案,之前没见到过这种方式,用 curl 调用 GitHub API 也是第一次,同时再次感受到了 bash 的得心应手,总体上是一次十分有趣的体验。

因此,将我的配置过程和结果记录在此,希望对其他开发者有所帮助。

这章教程承接第三章结束的地方, 我们将继续那个网络民调应用, 并且将关注与简单的表单处理与精简我们的代码。

创造一个简单的表单

让我们来升级一下上个教程中创建的问题详细内容模板(polls/detail.html), 让它包含一个HTML的<form>元素:

1
2
3
4
5
6
7
8
9
10
11
12
13
<!--polls/templates/polls/detail.html-->
<h1>{ { question.question_text } }</h1>

{ % if error_message % }<p><strong>{ { error_message } }</strong></p>{ % endif % }

<form action="{ % url 'polls:vote' question.id % }" method="post">
{ % csrf_token % }
{ % for choice in question.choice_set.all % }
<input type="radio" name="choice" id="choice{ { forloop.counter } }" value="{ { choice.id } }" />
<label for="choice{ { forloop.counter } }">{ { choice.choice_text } }</label><br />
{ % endfor % }
<input type="submit" value="Vote" />
</form>

快速浏览一下上面的代码:

  • 上面的这个模板给每一个选项显示了一个单选按钮。 每一个单选按钮的值与每一个选项的id相联系。 每一个单选按钮的名字都是choice. 这意味着, 当某人选择了这些单选按钮之一然后提交这个页面的时候, 系统会使用POST传递一个数据choice=#, 此处的#,代表选中选项的id。 这是HTML表单的基本概念。

  • 我们把这个表单的动作属性设置为{ %url 'polls:vote' question.id% }, 然后我们设置动作方法为post。 此处使用method='post'(与之相对的是method='get')十分重要, 因为此处提交表单的这个动作将会出发服务器端的数据操作。 只要当你创建一个需要服务器端数据操作的表单的时候, 使用post总是最佳选择。 这个建议并不仅仅真对于Django, 这是Web开发的实际经验。

  • forloop.counter记录了for标签内循环的次数。

  • 既然我们创建了一个POST表单(拥有着操作数据的功能),我们就需要担心一下跨站请求伪造(Cross Site Request Forgeries)的问题。 但是谢天谢地,你并不需要太过于担心这个, 因为Django创造了一个非常易用的系统来避免这个问题。 简而言之, 所有使用POST提交,并且目标是一个内链URL的表单, 都需要有一个{ % CSRF token % }标签。

现在, 让我们来创建一个能够处理提交的数据的Django视图吧。 别忘了, 在第三章教程中, 我们为这个民调应用创建了一个URL设置, 包含着如下代码:

url(r'^(?P<question_id>d+)/vote/$', views.vote, name='vote')

我们之前也为vote视图创造了一个简单版本, 现在让我们来写一个完整版的:

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
#polls/views.py

from django.shortcuts import get_object_or_404, render
from django.http import HttpResponseRedirect, HttpResponse
from django.core.urlresolvers import reverse

form polls.models import Choice, Question
#...
def vote(request, question_id):
p = get_object_or_404(Question, pk=question_id)
try:
selected_choice = p.choice_set.get(pk = request.POST['choice'])
except (KeyError, Choice.DoesNotExist):
#重新显示问题投票表单
return render(request, 'polls/detail.html', {
'question': p,
'error_message': "You didn't select a choice.",
})
else:
selected_choice.votes += 1
selected_choice.save()
#当成功提交一个POST数据之后, 要记得返回一个
#HttpResponseRedirect, 不然当用户点击后退
#按钮的时候, 同样的数据会被提交多次。
return HttpResponseRedirect(reverse('polls:results', args=(p.id, )))

以上的代码包含一些我们之前没有提过的东西:

  • request.POST是一个字典型对象。 它允许你通过键值对来访问POST传递的数据。 在上文的代码中, request.POST['choice']以字符串的形式,返回了选中的选项的ID。 request.POST的值总是以字符串表示的。

    Django同样提供request.GET来以同样的方式访问GET数据。 在我们的代码中,我们只使用request.POST, 来确保数据只由POST请求来操作。

  • choice没有提供一个POST数据的话,request.POST['choice']会引发一个KeyError错误。 上面的代码检查了KeyError错误发生的可能性, 并在choice没有提供POST数据的时候重新显示表单页面,并且返回一个错误信息。
  • 在添加了投票统计之后, 代码返回了一个HttpResponseRedirect而非一个普通的HttpResponseHttpResponseRedirect只需要一个参数: 被重定向的目标URL。

    正如代码中的注释提到的那样, 你应当总是返回一个HttpResponseRedirect, 这不仅仅是对于Django而言。 这是一个优秀的编程实践经验。

  • 我们在本例的HttpResponseRedirect构造器中使用了一个reverse()函数。 这个函数帮助我们避免了在视图函数中硬编码写入一个URL。 给定我们想要传递到的那个视图的名字与这个URL模式所需的参数,在这个例子中, 使用我们在第三章教程中创建的URL设置, 这个reverse()函数会返回如下字符串:

    '/polls/3/results/'
    

    这里的3是p.id的值。 这个重定向的URL会调用result视图来显示最终的页面。

正如我们在第三章教程中提到的那样, request是一个HttpRequest对象。

在某人为某个问题投票之后, vote()视图重定向到这个问题的最终视图中, 它的视图代码如下:

1
2
3
4
5
6
from django.shortcuts import get_object_or_404, render


def results(request, question_id):
question = get_object_or_404(Question, pk=question_id)
return render(request, 'polls/results.html',{'question':question})

它与我们在第三章写的detail视图几乎完全一样, 仅有的不同之处在于模板的名字。 我们将会在稍后修改这处冗余。

现在, 创建一个polls/results.html模板

1
2
3
4
5
6
7
8
9
10
<!--polls/templates/polls/results.html-->
<h1>{ { question.question_text } }</h1>

<ul>
{ % for choice in question.choice_set.all % }
<li>{ { choice.choice_text } } -- { { choice.votes } } vote{ { choice.votes|pluralize } }</li>
{ % endfor % }
</ul>

<a href="{ % url 'polls:detail' question.id % }">Vote again?</a>

现在在你的浏览器中访问/polls/1来为这个问题投票。 你将见到一个随着你每次投票都有变化的结果页面。 如果你没有选择一个选项就提交了的话, 你还将见到一个错误页面。

使用基础视图: 代码越少越好

之前的detail()result()视图都极其简单, 以及, 像之前提过的那样—— 冗赘。 而那个显示着问题列表的index()视图, 也是一样。

这些视图体现着网页开发的一个基本模式:根据URL中传递的一个参数来从数据库中获取数据, 读取一个模板然后返回渲染之后的页面。 因为这实在是太平常了, Django提供了一个快捷表示, 叫做基础视图(generic views)(或者叫泛型视图).

基础视图将程序抽象表示, 以至于你甚至不需要书写Python代码就能够创建一个应用。

然我们把民调应用转化成使用基础视图系统, 所以我们就可以删去一大堆无用的代码。 我们只需做一下这几步就可以完成转化:

  • 改变URL设置
  • 删除一些旧的,不需要的视图
  • 引入基于基础视图系统的新的视图

修改URL设置

首先, 打开polls/urls.py, 并作如下修改:

1
2
3
4
5
6
7
8
9
10
11
12
#polls/urls.py

from django.conf.urls import patterns, url

from polls import views

urlpatterns = patterns('',
url(r'^$',views.IndexView.as_view(), name ='index'),
url(r'^(?P<pk>d+)/$', views.DetailView.as_view(), name='detail'),
url(r'^(?P<pk>d+)/results/$', views.ResultsView.as_view(), name='results'),
url(r'^(?P<question_id>d+)/vote/$', views.vote, name='vote'),
)

注意第二个和第三个正则表达式匹配参数名已经从<question_id>变成了<pk>

修改视图

接下来, 我们要对旧的index, detailresult视图做一些修改, 并且使用之前提到的Django的基础视图。 打开polls/views.py, 并作如下修改。

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
#polls/views.py

from django.shortcuts import get_object_or_404, render
from django.http import HttpResponseRedirect
from django.core.urlresolvers import reverse
from django.views import generic

from polls.models import Choice, Question

class IndexView(generic.ListView):
template_name = 'polls/index.html'
context_object_name = 'latest_question_list'

def get_queryset(self):
return Question.objects.order_by('-pub_date')[:5]

class DetailView(generic.DetailView):
model = Question
template_name = 'polls/detail.html'

class ResultsView(generic.DetailView):
model = Question
template_name = 'polls/result.html'

def vote(request, question_id):
...#与之前相同

我们使用了两个基础视图: ListViewDetailView。 这两个视图分别抽象了“显示一列对象”和“显示某个特定对象的详细内容”两个概念。

  • 每一个抽象视图都需要直到它所作用的数据模型。 这个信息由model属性传递
  • DetailView基础视图需要由URL中匹配得到被称为pk的主键键值, 所以我们在URL设置中把question_id改名作pk

默认情况下, DetailView基础视图使用叫做<app name>/<model name>_detail.html的模板。 在本例中, 它使用的是polls/question_detail.html。 这里的template_name属性被用来告诉Django去使用一个特定的模板名称, 而非Django自动默认的模板。 我们还为result视图指定了一个模板名称 – 这确保了result视图和detail视图在渲染时会有不同的显示, 尽管他们都是基于DetailView

同样的, ListView使用默认的<app name>/<model name>_list.html 我们使用template_name来告诉ListView使用我们存在的polls/index.html模板

在之前的教程中, 我们将一个包含着questionlatest_question_list的内容对象传递到模板中。 对于DetailView来说, 这些信息都是自动传递的。 因为我们使用了一个Django的视图。 Django能够自己为这个内容决定一个合适的名字。 但是, 对于ListView,自动生成的内容变量是question_list。 为了覆盖这个, 我们提供了context_object_name属性, 特殊指定了我们想要替代性地使用latest_question_list。 作为一个可选的方法, 你应该更改你的模板来匹配新的默认内容变量 – 但是让Django使用你想要的变量会更简单。

运行服务器,你就会看见基于基础视图的全新的民意调查应用。

这章教程将继续第二章结束的地方, 我们将继续编写我们的网络民调应用,并切将专注于创建一个界面 – 视图(view)

思想

视图, 是在你的Django应用中具有一个特定模板的特定的函数, 它生成某“种” 特定的Web页面。

举例来说, 在一个博客应用之中, 你可能会有如下的视图:

  • 博客主页, 显示最新的几篇博文
  • 博文的“详细” 页面, 完整展示某一篇博文的详细信息
  • 以年为基础的分类页面 ,以某一年的月份为基础分类博文
  • 以月为基础的分类页面, 以某一月的日子为基础分类博文
  • 以日为基础的分类界面, 显示某一天发布的所有博文
  • 评论动作, 处理对某一篇博文的评论功能

在我们的民意调查应用中, 我们有如下四个视图:

  • 问题“索引”视图: 显示最新的几个问题
  • 问题“详细”视图: 显示一个问题的文本, 以及一个投票的表单
  • 问题“结果”视图: 显示一个特定问题的投票结果
  • 投票动作: 处理对某个特定问题的特定选项的投票操作

在Django中, 网页和其它的内容是由视图所呈现的。 每个视图由一个单独的Python函数所表示(或者在以类为基础的视图中, 以方法来表示)。 Django会通过审视被要求访问的URL(准确地说, 域名之后的那部分URL)来选择一个特定的视图。

你可能经常会遇见像 “ME2/Sites/dirmod.asp?sid=&type=gen&mod=Core+Pages&gid=A6CD4967199A42D9B65B1B” 这样的URL, 你会十分欣慰Django有着一个比这要优雅的多的URL系统。

一个URL模式字符串其实就是URL的一般形式, 例如: /newsarchive/<year>/<month>

要从一个URL中获得一个视图, Django使用被称为URLconfs的方法。 URLconfs适配URL模式字符串(用正则表达式的形式描述),并选择相对应的视图。

编写你的第一个视图

让我们来着手编写第一个视图, 打开polls/views.py文件, 然后输入如下的Python代码:

1
2
3
4
5
6
7
#polls/views.py

from django.http import HttpResponse


def index(request):
return HttpResponse("Hello, world. You're at the polls index.")

这是Django能够提供的最基础的视图。 要访问这个视图, 我们需要将其映射到URL上 – 所以我们需要一个URL配置。

若想在应用目录下设置一个URL配置, 创建一个叫做urls.py的文件。 你的应用目录现在应该长这个样子:

1
2
3
4
5
6
7
polls/
__init__.py
admin.py
models.py
tests.py
urls.py
views.py

polls/urls.py文件中应当包含如下内容:

1
2
3
4
5
6
7
8
9
10
#polls/urls.py

from django.conf.urls import patterns, url

from polls import views


urlpatterns = patterns('',
url(r'^$',views.index, name='index'),
)

下一步是把URL设置的根节点指向polls.urls模块。 在mysite/urls中添加一个include()方法:

1
2
3
4
5
6
7
8
9
#mysite/urls.py

from django.conf.urls import patterns, include, url
from django.contrib import admin

urlpatterns = patterns('',
url(r'^polls/', include('polls.urls')),
url(r'^admin/', include(admin.site.urls)),
)

你现在就已经把一个index视图添加进了URL设置之中。 在你的浏览器中访问 http://localhost:8000/polls/ , 你就将见到你在index视图中定义的那行文字: Hello, world. You’re at the polls index

url函数传递四个参数, 两个是必须参数: regex正则表达式 和 view视图。 以及两个可选参数: kwargs关键字参数,name名称。

现在让我们一个一个介绍一下他们各自分别代表着什么:

url()参数: regex 正则表达式

术语’regex’ 是 正则表达式’regular expression’ 的缩写, 代表着一种对字符串中特定模式进行匹配的语法。 Django从第一个正则表达式开始, 并按照列表依次对要求访问的URL进行匹配, 直到找到所需的URL。

请注意, 这些URL的正则匹配并不会对POST和GET的传递参量进行匹配。 如在一个http://www.example.com/myapp的URL请求中, URL设置会匹配 myapp/。 而在一个http://www.example.com/myapp/?page=3的请求中, URL设置仍然只会匹配myapp/

如果在正则表达式方面需要任何帮助, 请访问Wikipedia的正则表达式相关页面,与Python的re模块的相关官方文档。 以及由O’Relly公司出版的由Jeffrey Friedl所著的《精通正则表达式》也是相当不错的选择。 但实际上, 你并不需要成为一名正则表达式的专家, 你仅仅需要知道怎么去对一些简单的模式进行匹配就好了。 事实上, 十分复杂的正则表达式有着很惨的使用表现,所以你最好还是不要太过依赖于正则表达式。

最终, 一个性能方面的提醒: 这些正则表达式在URL设置模块被加载的时候就被编译好了, 他们运行的速度超级快(如果这个匹配并非像前文所讲的那么的复杂的话)。


url()参数: view 视图

当Django找到了一个正则表达式的匹配, 它会调用相对应的视图函数, 并传递一个HttpRequest作为第一参数,被正则表达式匹配的其它内容作为剩余参数。 如果正则表达式仅作简单的匹配, 匹配值将作为普通参数(positional arguments)进行传递, 如果使用了名称匹配, 那么匹配值将作为键值对来传递。 我们将会在稍后提供一个小小的例子。


url()参数: kwargs 关键字

任意的关键字都可以被以字典的形式作为参数传递给目标视图。 在此教程中我们将不会使用这个功能。


url()参数: name 名称

为你的URL起一个名字, 使得你可以在Django项目的其他地方, 尤其是模板中明确地调用它。 这个强大的功能使得你可以在只用在一个文件中就可以对url模式匹配做一个全局的修改。


编写更多的视图

现在让我们对polls/views.py 文件添加更多的视图。 这些视图有着些许的差别, 因为他们都接受了一个参数:

1
2
3
4
5
6
7
8
9
10
11
#polls/views.py

def detail(request, question_id):
return HttpResponse("You're looking at question %s." %question_id)

def results(request, question_id):
response = "You're looking at the results of question %s."
return HttpResponse(response %question_id)

def vote(request, question_id):
return HttpResponse("You're voting on question %s." %question_id)

对这些新增的视图添加url链接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#polls/urls.py

from django.conf.urls import patterns, url
from polls import views

urlpatterns = patterns('',
#ex: /polls/
url(r'^$', views.index, name='index'),
#ex: /polls/5/
url(r'^(?P<question_id>d+)/$', views.detail, name='detail'),
#ex: /polls/5/results/
url(r'^(?P<question_id>d+)/results/$', views.results, name = 'results'),
#ex: /polls/5/vote/
url(r'^(?P<question_id>d+)/vote/$', views.vote, name='vote'),
)

现在在你的浏览器中实验一下: 如果输入/polls/34的话。 它将会运行detail()函数, 并且会显示你所输入的问题id所对应的那个问题。 试试/polls/34/results//polls/34/vote/也将出现相对应的结果

当某人对你的服务器请求一个网页: 比如说/polls/34的时候, Django会读取mysite.urlsPython模块, 因为ROOT_URLCONF设置指向的这里。 它寻找到名为urlpatterns的变量并且逐一匹配其中的正则表达式。 这里我们使用的include()函数是对其它的URL设置的简单引用。 注意这里的include()函数中的正则表达式并没有一个$(标记字符串末尾)字符,而是一个下划线。 当Django遇到include()的时候, 它会将当前部分已经被匹配的URL部分删去, 然后把剩余部分交予include的URL设置去做进一步处理。

include()函数背后的思想是把URL变得“即插即用”。 在这里我们的民调应用是被放在polls/urls.py, 他们还可以被放在polls/下, 或者在/fun_polls/下, 或者在/content/polls/下, 他们还可以被放在其它目录下, 而这个应用仍然可以工作。

当用户访问/polls/34/的时候,Django会做如下操作:

  • Django会找到与这个URL匹配的正则表达式^polls/
  • 然后Django会截去匹配的字符串polls/, 然后把剩下的文本34/传递到polls.urlsURL设置中, 来做进一步操作。
  • 此时这个字符串与r'^(?P<question_id>d+)/$'相匹配, 最终映射到一个对detail()函数的调用上:

    detail(request = <HttpRequest object>, question_id='34')
    

上面的question_id='34'这一部分来自于(?P<question_id>d+)。 这里使用一个括号来把一个模式“包裹”起来, 它“捕捉”其匹配的那个字符串, 然后把它作为一个参数传递给视图。 这里?P<question_id>定义了这个匹配内容的名字。 d+是正则表达式语法中对一个数字序列的匹配定义。

因为URL模式是一系列正则表达式, 实际上他们能做的事情是没有限制的。 并没有需要添加一个类似于.html的后缀 —— 除非你真的想这么做, 在这种情况下你可以写如下的代码:

(r'^polls/latest.html$','polls.views.index'),

但是千万别, 这样真的太傻了。

写真正能够做一些事的视图

每个视图都承担着一个或两个责任: 返回一个HttpResponse对象, 包含着这个被请求的页面的信息, 或者引发一个诸如Http404的异常——这都由你决定。

你的视图可以从一个数据库中读取记录。 它可以使用一个模板系统——Django默认的, 或者是第三方的Python模板——或者不使用。 它可以生成一个PDF文件, 输出XML文件, 实时生成zip文件…… 做一切你想的事情, 只要使用相对应的Python支持库就可以。

而Django所需要的仅仅是一个HttpResponse, 或者是一个异常。

出于方便起见, 让我们使用Django自带的数据库API。 以下是我们的新的index()视图, 它显示了数据库中最新的五个问题, 用逗号连接, 并根据发布时间排序。

1
2
3
4
5
6
7
8
9
#polls/views.py

from django.http import HttpResponse
from polls.models import Question

def index(request):
latest_question_list = Question.objects.order_by('-pub_date')[:5]
output = ','.join([p.question_text for p in latest_question_list])
return HttpResponse(output)

这里有一个问题: 这个页面的内容是被直接编码在视图源代码中的, 如果你想要修改的话。 需要修改这个视图的Python代码。 所以在这里我们使用Django的模板系统创建一个模板, 来把设计与视图分割开来。

首先, 在你的polls目录下创建一个templates文件夹。 Django将在这里寻找网页模板。

Django的TEMPLATE_LOADERS设置包含了一个指示从不同的源头调用模板的列表。 其中的一个默认选项是django.template.loaders.app_directories.Loader, 它自动在每一个INSTALLED_APPS中寻找一个’templates’文件夹。 这就是在第二章教程的时候,在我们没有修改 TEMPLATE_DIRS的情况下Django依然能够找到模板的原因。

安排模板
我们可以把我们所有的模板都放在同一个地方, 比如一个巨大的模板目录下, 此时它依然可以工作得十分完美。 但是,这个模板属于那个民调应用, 所以与我们创建的管理后台的页面模板不同, 我们把这个模板放在应用的模板目录下, 而不是放在项目的模板目录下。

在你刚刚创建的这个templates文件夹中, 再创建另一个叫做polls的文件夹。 并且在这个文件夹中, 创建一个叫做index.html的文件。 你的模板此时的目录应该是polls/templates/polls/index.html。 因为app_directories模板加载器如之前我们所描述的那样工作, 所以此时你可以简单地在Django中使用polls/index.html就可以调用这个模板。

模板命名空间
因为我们可能会直接把文件放在 polls/templates之中(而不是另外新建一个polls文件夹), 但是这确实是一个坏主意。 Django会选择与这个名字相匹配的第一个模板。 而如果此时你有一个与其同名而在不同应用下的模板的话。 Django将不能区分它们两个。 我们需要能够让Django准确无误地找到正确的那一个。 此时最简单的方式就是为其确定命名空间, 也就是把每个模板都放进用每个应用的名字命名的文件夹中

在这个模板中输入如下内容:

1
2
3
4
5
6
7
8
9
10
11
<!--polls/templates/polls/index.html -->

{ % if latest_question_list % }
<ul>
{ % for question in latest_question_list % }
<li><a href="/polls/{ { question.id } }/">{ { question.question_text } }</a></li>
{ % endfor % }
</ul>
{ % else % }
<p> No polls are available. </p>
{ % endif % }

然后升级我们的index视图, 来使用这个模板:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#polls/views.py

from django.http import HttpResponse
from django.template import RequestContext, loader

from polls.models import Qeustion


def index(request):
latest_question_list = Question.objects.order_by('-pub_date')[:5]
template = loader.get_template('polls/index.html')
context = Requestcontext(request, {
'latest_question_list': latest_question_list,
})
return HttpResponse(template.render(context))

这个代码从叫做polls/index.html的文档中读取了模板, 并且把它传递给一个内容对象。 内容对象是一个将模板变量名与Python对象一一映射的字典对象。

通过你的浏览器访问polls/来读取这个页面, 此时你将能够看见一个列表显示着我们在第一章中创建的”What’s up”问题。 这个链接指向这个问题的详细页面。

一个快捷表示: render()

读取一个模板, 填入内容, 再用HttpResponse返回处理后的模板, 这一系列操作经常被执行。 所以Django提供了一个快捷表示, 我们用它重写一下index()视图:

1
2
3
4
5
6
7
8
9
10
#polls/views.py
from django.shortcuts import render

from polls.models import Question


def index(request):
latest_question_list = Question.objects.order_by('-pub_date')[:5]
context = {'latest_question_list': latest_question_list}
return render(request, 'polls/index.html', context)

这里我们可以注意到, 如果我们使用了render的话, 我们就不再需要导入loader, RequestContextHttpResponse了。

render()方法把request对象作为第一参数, 然后把模板名字作为第二参数, 然后一个可选的字典变量作为第三参数。 它返回一个给定的模板通过给定的内容渲染之后得到的HttpResponse对象。

引发一个404错误

现在, 让我们改动一下detail视图 – 那个显示给定问题的详细内容的视图:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#polls/views.py

from django.http import Http404
from django.shortcuts import render

from polls.models import Question
#...

def detail(request, question_id):
try:
question = Question.objects.get(pk = question_id)
except Question.DoesNotExist:
raise Http404("Question does not exist")
return render(request, 'polls/detail.html', {'question': question})

这里有一个全新的概念: 如果所要求的ID不存在的话, 这个视图引发了一个Http404异常。

我们将在稍后讨论你应该在polls/detail.html 模板中放一些什么。 但如果你想尽快地完成这个教程的话, 一个包含如下内容的模板就够了:

1
2
<!--polls/templates/polls/detail.html-->
{ { question } }

一个快捷表示: get_object_or_404()

调用get()函数, 并在对象不存在的时候引发Http404异常,也是一个十分常见的操作。 Django提供了一个快捷表示。 根据其重写detail()视图,如下所示:

1
2
3
4
5
6
7
8
9
#polls/views.py

from django.shortcuts import get_object_or_404, render

from polls.models import Question
#...
def detail(request, question_id):
question = get_object_or_404(Question, pk=question_id)
return render(request, 'polls/detail.html',{'question': question})

这个get_object_or_404()函数把一个Django数据模型作为它的第一参数, 以及任选的键值对作为其它参数。 这个函数把参数传递给get()函数, 如果对象不存在的话, 引发404异常

思想
为什么我们使用一个函数get_object_or_404(), 而不是自动使用更高层面的ObjectDoesNotExist异常, 或者用一个API来引发Http404异常呢?

因为这样的话就会把数据模型与视图联系在一起。 而Django的一个哲学就是低耦合度。 一些系统自带的合作函数存放于django.shortcuts模型之中

同样也有一个get_list_or_404()函数, 使用方法和get_object_or_404()一样,只不过使用的是filter()函数, 如果列表是空的就返回404异常

使用模板系统

回到我们的民调应用的detail()视图中。 给定一个内容变量question, 我们的polls/detail.html应该长这个样子:

1
2
3
4
5
6
7
<!--polls/templates/polls/detail.html-->
<h1>{ { question.question_text } }</h1>
<ul>
{ % for choice in question.choice_set.all % }
<li>{ { choice.choice_text } }</li>
{ % endfor % }
</ul>

模板系统使用 点 来访问一个变量的属性。 在上面的{ { question.question_text } }的例子中, 首先Django在question对象中做一次字典查找。 如果没有找到, 他做一次属性查找——在这个例子中成功找到了—— 如果还是失败的话, 他就会做一次列表查找。

对方法的调用发生在{ % for % }循环之中: question.choice_set.all 被解析为Python的代码question.choice_set.all(), 将会返回一个可迭代的Choice对象并且适于在{ % for % }标签内使用。

删去在模板中硬编码的URL

别忘了, 在之前我们写那个polls/index.html模板中的链接的时候, 我们用了一些硬编码的风格:

<li><a href="/polls/{ {question.id} }/">{ {question.question_text} }</a></li>

在这里使用硬编码,紧耦合方法的弊端在于, 如果要修改链接的话, 会造成相当大的修改模板的工作量。 既然你在polls.urls模块中的url()方法里定义了正则表达式的名字参数, 你就可以使用{ %url% }标签来代替这里特定的url地址。

<li><a href = "{ % url 'detail' question.id % }">{ { question.question_text } }</a></li>

这个函数的工作原理是查询polls.urls模块中的URL定义。 你可以清除地看见名称’detail’被做了如下定义:

url(r'^(?P<question_id>d+)/$', views.detail, name='detail'),

如果你想要把问题的详细页面转换到别的URL上, 也只用修改polls/urls.py就好了。

为URL名称确定命名空间

这个教程项目只包含一个应用:polls。 在真实的Django应用环境中, 可能在同一个项目下有着五个, 十个, 乃至20个或更多不同的应用。 Django是怎样区别其中不同的URL的? 举例来说, polls应用有一个detail视图, 同样一个博客应用也可能有一个相同的视图。 Django使怎样确保在使用{ % url % }模板标签的时候不至于混乱呢?

这个问题的答案就是为你的根URL配置添加命名空间。 在mysite/urls.py文件中,添加命名空间的定义。

1
2
3
4
5
6
7
8
9
#mysite/urls.py

from django.conf.urls import patterns, include, url
from django.contrib import admin

urlpatterns = patterns('',
url(r'^polls/', include('polls.urls', namespace="polls")),
url(r'^admin/', include(admin.site.urls)),
)

然后把具体模板中的代码从:

<li><a href = "{ % url 'detail' question.id % }">{ { question.question_text } }</a></li>

改成:

<li><a href = "{ % url 'polls:detail' question.id % }">{ { question.question_text } }</a></li>