整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

在Django中使用Markdown

为开发人员,我们依赖于静态分析工具来检查、lint(分析)和转换我们的代码。我们使用这些工具来帮助我们提高生产效率并生成更好的代码。然而,当我们使用markdown编写内容时,可用的工具就很少。

在本文中,我们将介绍如何开发一个Markdown扩展来解决在使用Markdown管理Django站点中的内容时遇到的挑战。

你认为他们有linter吗?

照片来自Pexels,由mali maeder拍摄

问题

像每个网站一样,我们在主页、FAQ部分和“关于”页面等地方都有不同类型的(大部分)静态内容。很长一段时间以来,我们都是在Django模板中直接管理这些内容的。

当我们最终决定是时候将这些内容从模板转移到数据库中时,我们认为最好使用Markdown。从Markdown生成HTML更安全,它提供了一定程度的控制和一致性,并且对于非技术用户来说更容易处理。随着我们转移过程的进展,我们注意到我们遗漏了一些东西:

内部链接

当URL更改时,链接到内部页面的链接可能会中断。在Django模板和视图中,我们使用了reverseand {% url %},但是这在普通的Markdown中是不可用的。

在不同环境之间进行复制

绝对内部连接不能在不同环境之间进行复制。这可以使用相对链接来解决,不过目前没有开箱即用的增强这一点的方法。

无效链接

无效链接会损害用户体验,并导致用户质疑整个内容的可靠性。这并不是Markdown独有的东西,只不过HTML模板是由对URL有一定了解的开发人员维护的。另一方面,Markdown文档是为非技术写作人员设计的。

前期工作

当我研究这个问题时,我搜索了Python linters、Markdown预处理器和扩展来帮助生成更好的Markdown。结果都不是很好。一个引人注目的方法是使用Django模板来生成Markdown文档。

使用Django模板预处理Markdown

使用Django模板,你可以使用诸如url之类的模板标记来反向查询URL名称,并配合使用条件、变量、日期格式和所有其他Django模板特性。这种方法本质上是使用Django模板作为Markdown文档的预处理程序。

我个人认为这可能不是非技术作家的最佳解决方案。另外,我担心提供对Django模板标记的访问可能是危险的。

使用 Markdown

对这个问题有了更好的理解之后,我们准备在Python中更深入地研究Markdown。

将Markdown转换为HTML

要在Python中开始使用Markdown,我们先安装markdown包:

接着,创建一个Markdown对象并使用其函数将一些Markdown转换成HTML:

你现在可以在你的模板中使用这个HTML代码片段。

使用Markdown扩展

基本的Markdown处理器提供了生成HTML内容的基本要素。对于更“新奇”的选项,Python markdown包包含了一些内置扩展。一个流行的扩展是“extra”扩展,除了其他东西之外,它增加了对隔离代码块的支持:

为了使用我们独特的Django功能扩展Markdown,我们将开发自己的扩展。

创建一个Markdown扩展来处理内联链接

如果你查看源代码,你将看到要将markdown转换为HTML, Markdown会使用多种不同的处理器。一种类型的处理器是内联处理器。内联处理器会匹配特定的内联模式,如链接、反引号、粗体文本和带下划线的文本,并将它们转换为HTML。

我们的Markdown扩展的主要目的是验证和转换链接。因此,我们最感兴趣的内联处理器是LinkInlineProcessor。这个处理器以[Haki的网站](https://hakibenito.com)的形式获取markdown ,解析它并返回一个包含链接和文本的元组。

为了扩展该功能,我们扩展了LinkInlineProcessor并创建了一个Markdown.Extension, 我们用它来处理链接:

我们来将这段代码分解一下::

  • DjangoUrlExtension扩展注册了一个名为DjangoLinkInlineProcessor的内联链接处理器。这个处理器将取代任何其他现有的链接处理器。

  • 内联处理器DjangoLinkInlineProcessor扩展了内置的LinkInlineProcessor,并在它处理的每个链接上调用clean_link函数。

  • clean_link函数接收一个链接和一个域名,并返回一个转换后的链接。这就是我们要插入我们的实现的地方。

如何获得网站域名


要识别到你自己网站的链接,你必须知道你的网站的域名。如果你正在使用Django的sites框架,那么你可以使用它来获取当前域名。


我没有把它包含在我的实现中,因为我们没有使用sites框架。相反,我们在Django设置中设置了一个变量。


获取当前域名的另一种方法是使用HttpRequest对象。如果内容只在你自己的站点中被编辑,你可以尝试从请求对象中插入站点域名。这可能需要对你的实现进行一些更改。

要使用该扩展,请在初始化一个新的Markdown实例时添加它:

太好了,这个扩展已经被使用了,我们准备进入有趣的部分了!

验证和转换Django链接

既然我们得到了在所有链接上调用clean_link的扩展,那我们可以来实现我们的验证和转换逻辑。

验证mailto链接

要开始工作,我们将从一个简单的验证开始。mailto链接对于使用预定义的收件人地址、主题甚至消息正文打开用户的电子邮件客户端非常有用。

一个常见的mailto链接是这样的:

这个链接将打开你的电子邮件客户端,并设置成撰写一封主题行为“我需要帮助!”的新电子邮件给“support@service.com”。

mailto链接不一定非要包含电子邮件地址。如果你看一看这篇文章底部的“分享”按钮,你会发现像这样的一个mailto链接:

这个mailto链接没有包含收件人,仅包含了主题行和消息正文。

既然我们已经很好地理解了mailto链接是什么样子的,我们就可以向clean_link函数添加第一个验证:

为了验证mailto链接,我们向clean_link中添加了以下代码:

  • 检查链接是否以mailto:开头,以识别相关链接。

  • 使用正则表达式将链接分割到它的组件。

  • 从mailto链接中删除实际的电子邮件地址,并使用Django的EmailValidator验证它。

注意,我们还添加了一种名为InvalidMarkdown的新异常类型。我们定义了自己的自定义异常类型,以将它与markdown本身所引发的其他错误区分开来。

自定义错误类

我曾经写过关于自定义错误类的文章,为什么它们是有用的,以及你什么时候应该使用它们。

在我们继续之前,让我们添加一些测试,看看它的实际效果:

太棒了!按预期的运行了。

处理内部和外部链接

既然我们已经了解了mailto链接,我们也可以处理其他类型的链接:

外部链接

  • 我们的Django应用程序外部的链接。

  • 必须包含一个页面跳转协议(scheme):http或https。

  • 理想情况下,我们还希望确保这些链接没有被破坏,但我们现在不会这样做。

内部链接

  • 到我们的Django应用程序中的页面的链接。

  • 链接必须是相对的:这将允许我们在不同环境之间移动内容。

  • 使用Django的URL名称而不是一个URL路径:这将允许我们安全地来回移动视图,而不必担心markdown内容中的失效链接。

  • 链接可能包含查询参数(?)和片段(#)。

SEO

从SEO的角度来看,公共URL不应该改变。当他们这样做的时候,你应该使用重定向正确地处理它,否则你可能会受到搜索引擎的惩罚。

有了这个需求列表,我们就可以开始工作了。

解析URL名称

要链接到内部页面,我们希望编写者提供一个URL名称,而不是URL路径。例如,假设我们有这个视图:

这个页面的URL路径是https://example.com/, URL名称是home。我们想要在我们的markdown链接中使用这个URL名称home,就像这样:

这将渲染到:

我们还想支持查询参数和散列:

这将渲染到以下HTML:

在使用URL名称时,如果我们更改了URL路径,内容中的链接将不会被破坏。要检查作者提供的href是否是一个有效的url_name,我们可以尝试reverse它:

URL名称“home”指向URL路径“/”。当没有匹配项时,将会引发一个异常:

在我们继续之前,当URL名称包含查询参数或散列时,会发生什么:

这是有意义的,因为查询参数和散列不是URL名称的一部分。

要使用reverse并支持查询参数和散列,我们首先需要清除值。然后,检查它是一个有效的URL名称,并返回包含查询参数和散列的URL路径,如果提供了的话:

这个代码段使用一个正则表达式来以?或#的出现对href进行分割,并返回各部分。

请确保它可以工作:

太了不起了!作者们现在可以在Markdown中使用URL名称了。它们还可以包括要添加到该URL的查询参数和片段。

处理外部链接

要正确处理外部链接,我们需要检查两件事:

1.外部链接总是提供一个跳转协议,http:或者https:。

2.阻止到我们自己网站的绝对链接。内部链接应该使用URL名称。

到目前为止,我们已经处理了URL名称和mailto链接。如果我们通过了这两个检查,这意味着href是一个URL。让我们从检查链接是否是链接到我们自己的网站开始:

函数urlparse会返回一个命名元组,该元组包含URL的不同部分。如果netloc属性等于site_domain,那么该链接就确实是一个内部链接。

如果URL实际上是内部的,我们就需要终止。但是,请记住,作者们不一定是技术人员,因此我们希望帮助他们,并提供一个有用的错误消息。我们要求该内部链接使用URL名称而不是URL路径,所以最好让作者们知道他们提供的路径的URL名称。

要获得一个URL路径的URL名称,Django为我们提供了一个名为resolve的函数:

当找到匹配项时,resolve会返回一个ResolverMatch对象,其中包含URL名称和其他信息。当没有找到匹配项时,它就会引发一个错误:

这实际上就是Django在底层所做的工作,用来确定在一个新请求到来时执行哪个视图函数。

为了给作者们提供更好的错误信息,我们可以使用来自ResolverMatch对象的URL名称:

当我们识别出内部链接时,我们要处理两种情况:

  • 我们没有识别出这个URL:这个URL很可能是不正确的。请作者检查该URL是否有错误。

  • 我们识别出了这个URL: 这个URL是正确的,所以就告诉作者应该使用什么URL名称。

我们来实际地看一下它:

漂亮!外部链接被接受,内部链接被拒绝,并带有一个有用的消息。

要求跳转协议

我们要做的最后一件事是确保外部链接包含一个跳转协议,要么是http:,要么是https:。让我们将这最后一部分添加到函数clean_link:

使用解析后的URL,我们可以很容易地检查跳转协议。让我们确保它正在工作:

我们向这个函数提供了一个没有跳转协议的链接,但是它运行失败了,并显示了一条有用的消息。太酷了!

整合代码

这是clean_link函数的全部代码:

要了解所有这些特性的一个实际用例是什么样子的,请看下面的内容:

这将产生以下HTML:

不错!

结论

我们现在有一个很不错的扩展,它可以验证和转换Markdown文档中的链接!现在,在不同环境之间移动文档和保持内容整洁要容易多了,最重要的是,可以保持正确和最新!

源码

你可以在这个gist中找到全部源代码。(地址:https://gist.github.com/hakib/73fccc340e855bb65f42197e298c0c7d )

题外话

本文中所描述的功能对我们很有用,但是你可能需要根据自己的需求对它进行调整。

如果你需要一些想法,那么除了这个扩展之外,我们还创建了一个markdown Preprocessor,它允许作者们在markdown中使用常量。例如,我们定义了一个名为SUPPORT_EMAIL的常量,我们像这样使用它:

该预处理程序将用我们定义的文本替换字符串$SUPPORT_EMAIL,然后才渲染Markdown。

英文原文:https://hakibenita.com/django-markdown
译者:Nothing

、选择题(1-18题各3分19-36题各2分,共92分)

1.在HTML的<TD>标签中,align 属性的取值是( C )

A. top ; B. middle ; C. center ; D. bottom

<table border="1">

<tr>

<td width="100px">姓名</td>

<td>性别</td>

<td>年龄</td>

</tr>

<tr>

<td>张三</td>

<td>男</td>

<td>20龄</td>

</tr>

</table>

2. CSS样式表根据所在网页的位置,可分为( B )

A.行内样式表、内嵌样式表、混合样式表; B.行内样式表、内嵌样式表、外部样式表;

C.外部样式表、内嵌样式表、导入样式表; D.外部样式表、混合样式表、导入样式表

行内样式:

<html>

<body>

<div style="width:100px;height:100px;background:red;"></div>>

</body>

</html>

-----------------------------------------------------------------

内嵌样式:

<html>

<head>

<style type="text/css">

#div{width:100px;height:100px;background:red;}

</style>

</head>

<body>

<div id="div"></div>

</body>

</html>

--------------------------------------------------------------

外部样式:

<html>

<head>

<link rel="stylesheet" type="text/css" href="ccss.css">

</head>

<body>

<div id="div"></div>>

</body>

</html>

---------------------

css文件

#div{width:100px;height:100px;background:red;}

#和.区别

.点是使用class引用的,多个控件可以同时使用一个class,一个控件上也可以使用多个class,比如

.tdRed{border:solid 1px red;}

.tdBKBlue{background-color:blue;}

<td class="tdRed" />

<td class="tdRed tdBKBule"/>

而ID是在一个页面中唯一的

总得来说class表示泛性的,id表示个性的

比如你所有的按钮都是一个颜色的

.normalButton{background-color:blue;border:solid 0px black;}

对于提交按钮会要做的大一点

#submit{width:100px;height:100px;}

那么你的按钮就是

<input type="button" id="submit" class="normalButton" value="提交" />

普通的按钮就是

<input type="button" id="abcdefg" class="normalButton" value="普通按钮" />

3. 在插入图片标签中,对插入的图片进行文字说明使用的属性是( D )

A.name; B.id; C.src; D. alt

4. 对于<FORM action=″URL″ method=*>标签,其中*代表GET或( C )

A.SET; B. PUT; C. POST ; D. INPUT

Get和post区别

安全性:POST比GET安全;

编码方式:POST方式提交时可以通过HTML文档中的<META>元素设置实体部分的编码方式,而GET方式提交时URI默认的编码方式为ISO-8859-1,不可以在页面中设置;

传输文件大小:POST方式提交文件放在实体部分传输,大小无上限,而GET方式提交文件内容放在URI部分传输,最大为2KB;

请求速度:GET比POST快。

数据传输方式:GET:查询字符串(名称/值对)是在 GET 请求的 URL 中发送的,如:/test/demo_form.asp?name1=value1&name2=value2;POST:查询字符串(名称/值对)是在 POST 请求的 HTTP 消息主体中发送的。

5. 下列标签可以不成对出现的是( B )

A.〈HTML〉〈/HTML〉 ; B.〈P〉 〈/P〉; C.〈TITLE〉〈/TITLE〉 ; D.〈BODY〉〈/BODY〉

<p>是段落标签。

在HTML4.01中某些标签(<p><br>,<hr>,<img>, <input>,<link>等)允许不成对出现,但是不推荐。在现在的浏览器里,都会“兼容”这些单标签。浏览器解释<p>标签后,碰到一个不对应的标签时,会自动填补</p>。所以<p>标签可以单标签使用,但不推荐。

在HTML5中规定了元素必须始终关闭,也就是标签必须成对出现。

6. 对于标签〈input type=*〉,如果希望实现密码框效果,*值是( C

A. hidden; B.text ; C. password ; D. submit

7. HTML代码<select name=“name”></select>表示?( D

A. 创建表格 ; <table>

B. 创建一个滚动菜单; <marquee>

C. 设置每个表单项的内容;

D.创建一个下拉菜单

8. BODY元素用于背景颜色的属性是( C )

A. alink ; B. vlink ; C. bgcolor; D. background

9. 在表单中包含性别选项,且默认状态为“男”被选中,下列正确的是( A )

A. <input type=radio name=sex checked> 男 ; B.<input type=radio name=sex enabled>

C.<input type=checkbox name=sex checked>男;

D.nput type=checkbox name=sex enabled>男

性别(单选框):<input type="radio" value="1" name="sex" checked="checked"/>男

<input type="radio" value="2" name="sex"/>女

角色(下拉框):<select name="role">

<option value="1" selected="selected">教师</option>

<option value="2">学生</option>

</select>

10. 在CSS中下面哪种方法表示超链接文字在鼠标经过时,超链接文字无下划线?( B )

A. A:link{TEXT-DECORATION: underline }; B. A:hover {TEXT-DECORATION: none};

C. A:active {TEXT-DECORATION: blink }; D. A:visited {TEXT-DECORATION: overline }

11. JavaScript代码: 'abcdefg'.indexOf('D') 结果是( B )

A:0 B:-1 C:3 D:4

Js常用方法

1.substr

substr(start,length)表示从start位置开始,截取length长度的字符串。

var src="images/off_1.png";

alert(src.substr(7,3));

弹出值为:off

2.substring

substring(start,end)表示从start到end之间的字符串,包括start位置的字符但是不包括end位置的字符。

var src="images/off_1.png";

alert(src.substring(7,10));

弹出值为:off

3.indexOF

indexOf() 方法返回某个指定的字符串值在字符串中首次出现的位置(从左向右)。没有匹配的则返回-1,否则返回首次出现位置的字符串的下标值。

var src="images/off_1.png";

alert(src.indexOf('t'));

alert(src.indexOf('i'));

alert(src.indexOf('g'));

弹出值依次为:-1,0,3

4.lastIndexOf

lastIndexOf()方法返回从右向左出现某个字符或字符串的首个字符索引值(与indexOf相反)

var src="images/off_1.png";

alert(src.lastIndexOf('/'));

alert(src.lastIndexOf('g'));

弹出值依次为:6,15

5.split

将一个字符串分割为子字符串,然后将结果作为字符串数组返回。

以空格分割返回一个子字符串返回

var s, ss;

var s = "1,2,3,4";

ss = s.split(",");

alert(ss[0]);

alert(ss[1]);

12. <img src="name">的意思是?( A )

A. 图像相对于周围的文本左对齐; B. 图像相对于周围的文本右对齐;

C. 图像相对于周围的文本底部对齐; D. 图像相对于周围的文本顶部对齐

13. 点击按钮,在ID为“Link”的DIV标签内显示东软实训超链接, 下面对该按钮的onClick事件函数描述正确的是:C

A. Link.innerText='<a href="http://www.jb-aptech.com.cn">东软实训</a>';

B. Link.outerText='<a href="http://www.jb-ptech.com.cn">东软实训</a>';

C. Link.innerHTML='<a href="http://www.jb-aptech.com.cn">东软实训</a>';

D. Link.outerHTML='<a href="http://www.jb-aptech.com.cn">东软实训</a>'

innerHTML 设置或获取位于对象起始和结束标签内的

HTML

outerHTML 设置或获取对象及其内容的 HTML 形式

innerText 设置或获取位于对象起始和结束标签内的文本

outerText 设置(包括标签)或获取(不包括标签)对象的文本

innerText和outerText在获取时是相同效果,但在设置时,innerText仅设置标签内的文本,而outerText设置包括标签在内的文本

14.(“24.7” + 2.3 ) 的计算结果是( C

A. 27 ; B. 24.7 2.3; C. 24.72.3; D. 26.7

15. ( B )事件处理程序可用于在用户单击按钮时执行函数

A. onSubmit; B. onClick; C. onChange; D. onExit

属性当以下情况发生时,出现此事件onabort图像加载被中断onblur元素失去焦点onchange用户改变域的内容onclick鼠标点击某个对象ondblclick鼠标双击某个对象onerror当加载文档或图像时发生某个错误onfocus元素获得焦点onkeydown某个键盘的键被按下onkeypress某个键盘的键被按下或按住onkeyup某个键盘的键被松开onload某个页面或图像被完成加载onmousedown某个鼠标按键被按下onmousemove鼠标被移动onmouseout鼠标从某元素移开onmouseover鼠标被移到某元素之上onmouseup某个鼠标按键被松开onreset重置按钮被点击onresize窗口或框架被调整尺寸onselect文本被选定onsubmit提交按钮被点击onunload用户退出页面

16. 用户更改表单元素 Select 中的值时,就会调用( D )事件处理程序

A. onClick; B. onFocus; C. onMouseOver; D. onChange

17.onMouseUp 事件处理程序表示( A

A. 鼠标被释放; B. 鼠标按下; C. 鼠标离开某个区域; D. 鼠标单击

18. 下列哪一项表示的不是按钮( C

A. type="submit"; B. type="reset"; C. type="image"; D. type="button"

<img src="/i/eg_tulip.jpg" alt="上海鲜花港 - 郁金香" />

19.下面哪一项是换行符标签?( C

A. <body>; B. <font>; C. <br>; D. <p>

font规定文本字体、大小和颜色:

<font size="3" color="red">This is some text!</font>

<font size="2" color="blue">This is some text!</font>

<font face="verdana" color="green">This is some text!</font>

20. 下列哪一项是在新窗口中打开网页文档。( B

A. _self; B. _blank; C. _top; D. _parent

_blank在新窗口中打开被链接文档;

_self是指在本身这个网页窗口来打开新的网页链接;

_top表示在顶层窗口打开网页链接,即在整个窗口中打开被链接文档;

_parent表示在父窗口打开网页链接;

<a href="http://www.w3school.com.cn/" target="_blank">Visit W3School!</a>onclick="javascript:window.open('Default.aspx','_blank');"

21. 下面说法错误的是( D )

A. CSS样式表可以将格式和结构分离;

B. CSS样式表可以控制页面的布局;

C. CSS样式表可以使许多网页同时更新;

D. CSS样式表不能制作体积更小下载更快的网页

CSS样式表能为我们实现些什么样的功能?

1、你可以将格式和结构分离。

2、你可以以前所未有的能力控制页面布局。

3、你可以制作体积更小下载更快的网页。

4、你可以将许多网页同时更新,比以前更快更容易。

5、浏览器将成为你更友好的界面

将格式和结构分离

HTML从来没打算控制网页的格式或外观。这种语言定义了网页的结构和各要素的功能,而让浏览器自己决定应该让各要素以何种模样显示。 但是网页设计者要求的更多。所以当 Netscape推出新的可以控制网页外观的HTML标签时,网页设计者无不欢呼雀跃。 我们可以用<FONT FACE>、<I>包在<P>外边控制文章主体的外观等等。然后我们将所有东西都放入表格,用隐式GIF空格 产生一个20象素的边距。一切都变得乱七八糟。编码变得越来越臃肿不堪,要想将什么内容迅速加到网页中变得越来越难。 串接样式表通过将定义结构的部分和定义格式的部分分离使我们能够对页面的布局施加更多的控制。HTML仍可以保持简单明了的初衷。CSS代码独立出来从另一角度控制页面外观。

以前所未有的能力控制页面的布局

<FONT SIZE>能使我们调整字号,表格标签帮助我们生成边距,这都没错。但是,我们对HTML总体上的控制却很有限。我们不可能精确地生成80象素的高度,不可能控制行间距或字间距,我们不能在屏幕上精确定位图象的位置。但是现在,样式表使这一切都成为可能。

可以制作出体积更小下载更快的网页还有更好的消息:

样式表只是简单的文本,就象HTML那样。它不需要图象,不需要执行程序,不需要插件,不需要流式。它就象HTML指令那样快。有了CSS之后,以前必须求助于GIF的事情现在通过CSS就可以实现。还有,正如我先前提到的,使用串接样式表可以减 少表格标签及其它加大HTML体积的代码, 减少图象用量从而减少文件尺寸。

可以更快更容易地维护及更新大量的网页

没有样式表时,如果我想更新整个站点中所有主体文本的字体,我必须一页一页地修改每张网页。即便站点用数据库提供服务,我仍然需要更新所有的模板, 而且更新每一模板中每一个实例实例的 <FONT FACE>。样式表的主旨就是将格式和结构分离。 利于样式表,我可以将站点上所有的网 页都指向单一的一个CSS文件,我只要 修改CSS文件中某一行,那么整个站点 都会随之发生变动。

浏览器将成为你更友好的界面

不象其它的的网络技术,样式表的代码 有很好的兼容性,也就是说,如果用户 丢失了某个插件时不会发生中断,或者 使用老版本的浏览器时代码不会出现杂 乱无章的情况。 只要是可以识别串接样式表的浏览器就 可以应用它。

22. 要使表格的边框不显示,应设置border的值是( B )

A. 1; B. 0; C. 2; D. 3

23. 如果要在表单里创建一个普通文本框,以下写法中正确的是( A )

A. <INPUT>; B. <INPUT type="password">;

C. <INPUT type="checkbox">; D. <INPUT type="radio">

24. 以下有关按钮的说法中,错误的是( B )

A. 可以用图像作为提交按钮; B. 可以用图像作为重置按钮;

C. 可以控制提交按钮上的显示文字; D. 可以控制重置按钮上的显示文字。

<input type="image" src="pic.jpg" onclick="fangfa();"/>

function fangfa(){

document.formname.submit();

document.formname.reset();

}

ttpServer 静态下载

先来看一段代码,启动一个静态的http服务

import CBHttp.code

function main(parm)
{
    var httpServer = new HttpServer();
    httpServer.startServer();
	
    while(1)  //主线程不能退出
    {
        Sleep(1000);
    }
}

服务启动好了,假设我们的项目根目录是E:\cbrotherwork\httpserver,那么我们的服务根路径就默认是E:\cbrotherwork\httpserver\webroot

我们新建txt文件E:\cbrotherwork\httpserver\webroot1.txt,在文本里输入内容Hello HttpServer!

然后我们打开本机浏览器输入:http://127.0.0.1:8000/111.txt,你发现你在网页上访问到了111.txt

主流的文件格式都支持下载

MIME控制

静态下载的过程中,有时需要控制可下载的文件格式,那么就要手动调配MIME

import CBHttp.code

function main(parm)
{
    var httpServer = new HttpServer();
	
    httpServer.addMIME("mystyle","text/html");  //添加后缀名为.mystyle,mime为text/html
    httpServer.addMIME("c");  			//添加后缀名为.c,mime类型让系统自动识别
    httpServer.removeMIME("gif");  		//删除gif
	
    var mimiemap = httpServer.getMIME();  	//获取目前支持的mime类型
	
    mimiemap.begin();
    do
    {
        print mimiemap.getKey() + ":" + mimiemap.getValue();
    }
    while(mimiemap.next());		
	
    httpServer.startServer();
	
    while(1)
    {
        Sleep(1000);
    }
}

运行结果:

rar:application/octet-stream
bmp:image/bmp
xml:application/xml
html:text/html
c:text/plain
js:application/x-javascript
txt:text/plain
ico:image/x-icon
zip:application/zip
jpg:image/jpeg
mystyle:text/html
7z:application/octet-stream
json:application/json
swf:application/x-shockwave-flash
css:text/css
png:image/png

HttpServer 动态接口

我们通常需要在用户调用一个接口时通过一些条件来返回不同的内容,下面我们再通过一段代码看一下

import CBHttp.code

class HelloAction
{
    function DoAction(request,respon)				//这个函数写法是固定的,request表示客户端请求信息,respon是回复给客户机的信息
    {
        var myTime = new Time();
        respon.write("now time is:" + myTime.strftime("%Y/%m/%d %H:%M:%S"));
        respon.flush();
    }	
}

function main(parm)
{
    var httpServer = new HttpServer();
    httpServer.addAction("hello.cb",new HelloAction());		//我们注册接口hello.cb,响应类是HelloAction
    httpServer.startServer();
	
    while(1)	//主线程不能退出
    {
        Sleep(1000);
    }
}

然后我们浏览器输入:http://127.0.0.1:8000/hello.cb,每次访问返回的都是当前时间。

接口名字可以是任意字符串,官方建议用统一的后缀,方便跟其他web服务器协作。

HttpServer 接口

函数

描述

用法

addAction(name,actobj)

添加http响应接口,name为接口名字,actobj为响应的Action对象

httpServer.addAction("hello.cb",actobj)

startServer(port,listen_ip)

启动服务
port: 监听端口,默认8000
listen_ip:监听IP,不传值默认为0.0.0.0表示监听IPV4所有IP
传值为字符串"::"表示监听IPV6全部IP
传值为字符串"*"表示监听IPV6IPV4的全部IP

httpServer.startServer(port,listen_ip)

stopServer()

停止服务

httpServer.stopServer()

setRoot(path)

设置服务器跟目录,path: 根目录绝对路径

httpServer.setRoot(path)

setThreadCount(cnt)

设置响应线程数量,默认10个
设置大了并发量大但是消耗资源多

httpServer.setThreadCount(50)

setNormalAction(actName)

设置默认响应接口
访问
http://x.x.x.x/后面不带路径时默认执行到的界面

httpServer.setNormalAction("hello.cb")
httpServer.setNormalAction("hello.html")

set404Action(actName)

设置错误响应接口,不设置CBrother有默认页面

httpServer.set404Action("404.cb")

setOutTime(t)

设置请求超时时间,默认10秒,单位为秒

httpServer.setOutTime(3)

setMaxReqDataLen(len)

设置客户机发送的最大请求数据长度,默认500K,参数单位为(字节)

httpServer.setMaxReqDataLen(1024 * 1024)

openLog()

打开日志,在webroot平级建立log目录默认关闭,建议打开

httpServer.openLog()

closeFileService()

关闭文件下载服务录

httpServer.closeFileService()

setHttpsCertFile(CRT_PATH,KEY_PATH)

设置https证书
如果设置了证书,服务将为https协议,
CRT_PATH: crt证书路径
key: 证书路径证书与Nginx所需证书相同

httpServer.setHttpsCertFile("e:/a.crt","e:/a.key")

setWebSocketModule(name,wsmodule)

设置websocket模块来支持websocket协议
name: 接口名字
wsmodule:websocket模块对象

httpServer.setWebSocketModule(name,wsmodule)

getMIME()

获取服务目前支持的mime格式,返回一个map对象,key为扩展,value为类型

var map = httpServer.getMIME()

addMIME(ext,type)

添加mime类型,ext: 扩展名
type:类型,type不传值系统会自动识别

httpServer.addMIME("txt");
httpServer.addMIME("mystyle","text/plain");

removeMIME(ext)

移除mime类型,ext为扩展名

httpServer.removeMIME("txt");

除了stopServer,其他接口都应该在startServer之前调用。

HttpServer 响应Action类

响应类必须要有function DoAction(request,respon)函数,如果没有会注册失败。

这个方法会被多线程调用,如果要访问公共资源需要加锁。

·request记录了客户端请求的信息

函数

描述

用法

getUrl()

获取请求的路径

var url = request.getUrl()

getProperty(name)

获取HTTP头里的参数

var v = request.getProperty("Content-Type")

getAllProperty()

获取HTTP头里的所有的参数,返回一个Map对象

var proMap = request.getAllProperty()

getParm(name)

获取请求链接?后面的参数,127.0.0.1?k=v&k1=1

var v = request.getParm("k")

getParmInt(name)

获取请求链接?后面的参数,强转整形

var v = request.getParmInt("k1")

getAllParm()

获取请求链接?后面的所有参数,返回一个Map对象

var parmMap = request.getAllParm()

getData()

获取请求post的数据

var data = request.getData()

getRemoteIP()

获取客户机IP

var ip = request.getRemoteIP()

getMethod()

获取请求类型,"GET""POST"

var method = request.getMethod()

getCookieCount()

获取cookie数量

var cnt = request.getCookieCount()

getCookie(index)

根据索引获取cookie,返回cookie对象

var cookie = request.getCookie(0)

getCookie(cookieName)

根据名字获取cookie,如果有重名的cookie返回第一个

var cookie = request.getCookie("userName")

getFormData()

获取form表单数据,当客户端以form表单格式提交才返回FormData对象,否则返回null

var formdata = request.getFormData()

isKeepAlive()

客户端请求是否为保持连接状态,true为保持连接,false为立即关闭

var isKeep = request.isKeepAlive()

isHttps()

客户端请求是否为httpstruehttps,falsehttp

var isKeep = request.isHttps()

·respon是返回给客户端的信息

函数

描述

用法

addProperty(key,value)

添加http头数据,必须为字符串

respon.addProperty("Access-Control-Allow-Origin","*")

addCookie(cookie)

添加cookie

respon.addCookie(new Cookie("aaa","bbb"))

setStatus(status)

设置返回http状态

respon.setStatus(200)

write(data)

写入要返回数据

respon.write("hello")

flush()

发送数据

respon.flush()

Cookie 类

在日常开发中,服务器需要通过cookie来判别客户机身份,CBrother提供了Cookie类来描述http头中的Set-CookieCookie字段

函数

描述

用法

setName(name)

设置cookie的名称

myCookie.setName("aaa")

setValue(v,pwd)

设置cookie的字段,pwd不传为明文,传入密码会增加一个签名

myCookie.setValue("bbb")
myCookie.setValue("bbb","pwdstr")

getName()

获取cookie的名称

var name = myCookie.getName()

getValue(pwd)

获取cookie的字段,pwd不传返回原文,传入密码会检查签名

var cookie = myCookie.getValue()
var cookie = myCookie.setValue("pwdstr")

setPath(path)

设置cookie的有效路径,不设置默认为'/'

myCookie.setPath("/aa/cc/")

setDomain(domain)

设置cookie的域,默认为空

myCookie.setDomain("cbrother.net")

setExpires(timeSeconds)

设置cookie有效时间,单位为秒,设置为0表示删除cookie,默认浏览器关闭失效

myCookie.setExpires(300)

setHttpOnly(boolValue)

cookieHttpOnly标签,默认false

myCookie.setHttpOnly(true)

getPath()

获取cookie的有效路径

var path = myCookie.getPath()

getDomain()

获取cookie的域

var domain = myCookie.getDomain()

getExpires()

获取cookie有效时间

var t = myCookie.getExpires()

getHttpOnly()

获取cookieHttpOnly标签

var httponly = myCookie.getHttpOnly()

其中带密码的setValue方法会用密码和原始明文算一个md5值作为cookie签名,这样保证了cookie不可被伪造,一般可以用于登陆状态验证。当然,在一些安全性要求比较高的场景,开发者也可以自己实现安全级别更高的cookie加密验证方式。

通过一个段代码来学习一下Cookie的用法:

class CookieAction
{
    function DoAction(request,respon)
    {
        var cookieCnt = request.getCookieCount();  //获取客户机Cookie数量
        if(cookieCnt > 0)
        {
            for(var i = 0 ; i < cookieCnt ; i++)  //遍历的方式可以获取所有的Cookie,包括名字相同的Cookie
            {
                //客户机已经有cookie
                var cookie = request.getCookie(i);
                if(cookie.getName() == "pwdCookie")
                {
                    print cookie.getValue();
                    print cookie.getValue("pwd222");  //带密码的cookie
                }
                else
                {
                    print cookie.getValue();  //明文cookie
                }
            }
			
            var pwdCookie request.getCookie("pwdCookie");  //根据名字获取Cookie,如果有同名Cookie则只返回第一个
            print pwdCookie.getValue("pwd222");			
        }
        else
        {
            //客户机没有cookie
            var cookie = new Cookie("normalCookie","3333");
            respon.addCookie(cookie);  //添加明文cookie
			
            cookie = new Cookie();
            cookie.setName("pwdCookie");
            cookie.setValue("3333","pwd222");
            respon.addCookie(cookie);  //添加带密码的cookie
        }
		
        var myTime = new Time();
        respon.write("now time is:" + myTime.strftime("%Y/%m/%d %H:%M:%S"));
        respon.flush();
    }	
}

表单FormData 类

CBrother提供了FormData类来描述form表单数据

函数

描述

用法

getType()

获取表单enctype类型,FORM_WWW_URLENCODED表示application/x-www-form-urlencoded,FORM_MULTI_PART表示multipart/form-data。在lib/htppdef里定义

var t = form.getType()

getTextCnt()

获取type不为"file"input数量

var cnt = form.getTextCnt()

getText(index)

根据坐标获取inputvaluetypefile获取不到

var value = form.getText(0)

getText(name)

根据name获取inputvaluetypefile获取不到

var value = form.getText("txt1")

getTextName(index)

根据坐标获取inputnametypefile获取不到

var name = form.getTextName(0)

getMultiPartCnt()

formenctypeFORM_MULTI_PART时候,获取input数量

var cnt = form.getMultiPartCnt()

getMultiPart(index)

根据坐标获取input数据,返回Multipart对象

var data = form.getMultiPart(0)

getMultiPart(name)

根据name获取input数据,返回Multipart对象

var data = form.getMultiPart("file")

addText(name,str)

添加一个文本类型的input数据,name为名称,str为内容.根HttpClientRequest向其他服务器发起请求的时候使用

form.addText("111","111")

addFile(name,filename,data,mime)

添加一个文件类型的input数据,name为名称,filename为文件名,data为文件内容stringByteArray对象,mime为文件mime类型,不传值会自动识别

form.addFile("file1","222.txt","222")

比如web前端提交的form表单结构如下:

<form method="post" action="form.cb" >
	<input type="text" name="txt1">
	<input type="text" name="txt2">
	<input type="password" name="userpass"
	<input type="radio" name="sex" value="man" checked>man
	<input type="radio" name="sex" value="woman">woman
	<input type="file" name="file" value="hiddenvalue" multiple="multiple"/>
	<input type="submit" value="提交(提交按钮)">
</form>

网页展示效果如下:

因为这个表单没有指定enctype,所以Content-Typeapplication/x-www-form-urlencoded,我们日常使用到的大部分表单都是这种格式。当前端post提交后服务器使用如下方式获取表单数据

class FormAction
{
    function DoAction(request,respon)
    {
        print request.getData();  //打印发送的数据
        print "**********************分割线*******************************";
        
        var formdata = request.getFormData();
        if(formdata == null)
        {
            return;	//只有Content-Type为application/x-www-form-urlencoded或者multipart/form-data时候才会返回FormData对象,否则返回null
        }
		
        var type = formdata.getType();
        if(type == FORM_WWW_URLENCODED)	//Content-Type为application/x-www-form-urlencoded类型的表单
        {
            var textCnt = formdata.getTextCnt();
            for(var i = 0 ; i < textCnt ; i++)
            {
                print formdata.getTextName(i) + ":" + formdata.getText(i);
            }
        }
    }	
}

服务器输出结果:

txt1=111&txt2=222&userpass=333&sex=man&file=111.txt
**********************分割线*******************************
txt1:111
txt2:222
userpass:333
sex:man
file:111.txt

Multipart 类

这里需要学习一下form添加enctype=multipart/form-data时提交的报文格式

Multipart类来描述form表单在multipart/form-data数据类型下的一个input数据,这个类型的表单一般用在上传文件

函数

描述

用法

getParm(name)

获取Multipart段落内的参数

var parm = mulpart.getParm("name")

getData(isByte)

获取Multipart的数据,isByte不传值,默认返回字符串,传true返回ByteArray对象

var data = mulpart.getData()
var bytearry = mulpart.getData(true)

getDataLen()

获取数据长度

var len = mulpart.getDataLen()

isFile()

如果是文件类型,返回true

var isfile = mulpart.isFile()

getFileCount()

获取这个input上传了几个文件

var cnt = mulpart.getFileCount()

getFile(index)

获取文件对应数据,返回一个Multipart对象

var filedata = mulpart.getFile(0)

比如web前端提交的form表单结构如下:(只是添加了一个enctype="multipart/form-data")

<form method="post" action="form.cb"  enctype="multipart/form-data">
	<input type="text" name="txt1">
	<input type="text" name="txt2">
	<input type="password" name="userpass"
	<input type="radio" name="sex" value="man" checked>man
	<input type="radio" name="sex" value="woman">woman
	<input type="file" name="file" value="hiddenvalue" multiple="multiple"/>
	<input type="submit" value="提交(提交按钮)">
</form>

网页展示效果没有变化如下:

添加了enctypeContent-Type变成multipart/form-data,服务器使用如下方式获取表单数据

class FormAction
{
    function DoAction(request,respon)
    {
        print request.getData();  //打印发送的数据
        print "**********************分割线*******************************";
        
        var formdata = request.getFormData();
        if(formdata == null)
        {
            return;	//只有Content-Type为application/x-www-form-urlencoded或者multipart/form-data时候才会返回FormData对象,否则返回null
        }
		
        var type = formdata.getType();
        if(type == FORM_MULTI_PART)	//Content-Type为multipart/form-data类型的表单
        {
            var formcnt = formdata.getMultiPartCnt();
            for(var i = 0 ; i < formcnt ; i++)
            {
                var muldata = formdata.getMultiPart(i);				
                if(muldata.isFile())  //如果是上传的文件
                {
                    for(var j = 0 ; j < muldata.getFileCount() ; j++)
                    {
                        //上传的文件可以在这里通过File对象把数据保存到文件里,CBrother目录下sample/http/httpserver_form.cb里面有例子,这里只是把数据打印出来
                        var filedata = muldata.getFile(j);
                        print filedata.getParm("name") + " " + filedata.getParm("filename") + ": size=" + filedata.getDataLen() + " value=" + filedata.getData();
                    }
                }
                else
                {
                    print muldata.getParm("name") + ":" + muldata.getData();
                }
            }
        }
    }	
}

服务器运行结果如下:

------WebKitFormBoundaryw7KfcBx8f23zh8d4
Content-Disposition: form-data; name="txt1"

111
------WebKitFormBoundaryw7KfcBx8f23zh8d4
Content-Disposition: form-data; name="txt2"

222
------WebKitFormBoundaryw7KfcBx8f23zh8d4
Content-Disposition: form-data; name="userpass"


------WebKitFormBoundaryw7KfcBx8f23zh8d4
Content-Disposition: form-data; name="sex"

man
------WebKitFormBoundaryw7KfcBx8f23zh8d4
Content-Disposition: form-data; name="file"; filename="111.txt"
Content-Type: text/plain

i'am 111.txt file value!
------WebKitFormBoundaryw7KfcBx8f23zh8d4--
**********************分割线*******************************
txt1:111
txt2:222
userpass:
sex:man
file 111.txt: size=24 value=i'am 111.txt file value!

HttpClientRequest 主动发起请求

在开发过程中,我们经常会遇到与第三方服务器通信,这时候就需要主动请求第三方服务器接口,CBrother提供了HttpClientRequest来主动访问别人提供的httphttps接口

var myReq = new HttpClientRequest();

函数

描述

用法

setMethod(method)

设置请求方式,"GET"或者"POST",类型字符串

myReq.setMethod("POST")

setUrl(url)

设置请求地址,参数地址全路径

myReq.setUrl("http://127.0.0.1/hello.cb")
myReq.setUrl("https://www.baidu.com")

addProperty(key,value)

添加http头,必须都为字符串

myReq.addProperty("HOST","127.0.0.1")

getProperty(key)

获取HTTP头里的参数,返回值为字符串

var v = myReq.getProperty("Content-Type")

getAllProperty()

获取HTTP头里的所有的参数,返回一个Map对象

var proMap = myReq.getAllProperty()

addParm(key,value)

在链接上添加参数,必须都为字符串。会在链接地址后面追加?k1=v1&k2=v2

myReq.addParm("11","121")

getParm(key)

获取已经添加的参数

var v = myReq.getParm("k")

getAllParm()

获取已经添加的参数,返回一个Map对象

var parmMap = myReq.getAllParm()

addData(data)

添加要提交的数据,data为要提交的数据,必须都为字符串

myReq.addData("11221122")

flush()

发起请求,返回HttpClientResponse对象

var response = myReq.flush()

setOutTime(seconds)

设置超时时间,单位为秒

myReq.setOutTime(5)

setKeepAlive(isKeep)

设置是否保持与服务器连接,不设置默认false不保持

myReq.setKeepAlive(true)

reset(clearHead = true)

还原成一个新的请求对象,仅保留网络连接。开启keepalive以此来复用网络,clearHead传值为false则不清理已经添加的http

myReq.reset()

close()

如果开启了keepalive,则主动关闭与服务器链接,不开启没意义

myReq.close()

addFormData(enctype)

本次请求添加form表单数据
enctypeFORM_WWW_URLENCODED或者FORM_MULTI_PART,定义在lib/httpdef里,
返回FormDatad对象

var formdata = myReq.addFormData(FORM_WWW_URLENCODED)
var formdata = myReq.addFormData(FORM_MULTI_PART)

addCookie(cookie)

添加一个cookie

myReq.addCookie(new Cookie("aaa","bbb"))

getCookieCount()

获取添加cookie数量

var cnt = myReq.getCookieCount()

getCookie(index)

根据索引获取cookie

var cookie = myReq.getCookie(index)

getCookie(cookieName)

根据名字获取cookie

var cookie = myReq.getCookie("cookieName")

setProxy(proxyType,ip,port,acc,pwd)

设置代理,proxyType为"http","https","socket5"中的一种。ip,port为代理服务器IP,如果服务器需要验证用户名,最后两个参数需要写帐号密码

myReq.setProxy("socket5","111.111.111.111",4646)
myReq.setProxy("https","111.111.111.111",4646,"user","123")

HttpClientResponse为主动请求后返回的结果,HttpClientRequest的flush方法会返回一个HttpClientResponse对象

var myReq = new HttpClientRequest();
myReq.setMethod("GET");
myReq.setUrl("https://www.baidu.com");
var myResponse = myReq.flush();

函数

描述

用法

getStatus()

获取状态码

var staus = myResponse.getStatus()

getProperty(name)

获取HTTP头里的参数,返回值为字符串

var v = myResponse.getProperty("Content-Type")

getAllProperty()

获取HTTP头里的所有的参数,返回一个Map对象

var proMap = myResponse.getAllProperty()

getData()

获取返回的数据

var data = myResponse.getData()

getCookieCount()

获取cookie数量

var cnt = myResponse.getCookieCount()

getCookie(index)

根据索引获取cookie对象

var cookie = myResponse.getCookie(0)

getCookie(cookieName)

根据名字获取cookie对象

var cookie = myResponse.getCookie("cookieName")

例子:

import CBHttp.code

function main(parm)
{
    var myReq = new HttpClientRequest();
    myReq.setMethod("GET");
    myReq.setUrl("https://www.baidu.com");
    var myRes = myReq.flush();
    print myRes.getStatus();
    print myRes.getData();
}

结果:

200
<html>
<head>
        <script>
                location.replace(location.href.replace("https://","http://"));
        </script>
</head>
<body>
        <noscript><meta http-equiv="refresh" content="0;url=http://www.baidu.com/"></noscript>
</body>
</html>

在开发过程中,为了降低服务器压力,经常需要用到keepalive来保持连接多次请求,看下面的例子:

import CBHttp.code
import lib/httpdef

function main(parm)
{
    var myReq = new HttpClientRequest();
    myReq.setMethod(HTTP_GET);
    myReq.setUrl("http://www.cbrother.net");
    myReq.setKeepAlive(true);		//开启keepalive,如果没有这一句默认是请求完毕后自动关闭连接
    var myRes = myReq.flush();
    print myRes.getStatus();
    print myRes.getData();
	
    myReq.reset();		//重置请求对象,仅保留网络连接,如果因为超时连接被服务器主动关闭,那么第二次请求会重新创建连接
	
    myReq.setMethod(HTTP_GET);
    myReq.setUrl("http://www.cbrother.net/doc.html");
    var myRes = myReq.flush();			//第二次请求没有开启keepalive,所以这句执行完后连接就会关闭了
    print myRes.getStatus();
    print myRes.getData();		
}

客户端使用Cookie例子:

function main(parm)
{
    var myReq = new HttpClientRequest();
    myReq.setMethod("GET");
    myReq.setUrl("https://www.baidu.com");
    myReq.addCookie(new Cookie("aaa","bbb"));  //本次请求添加cookie
    var myRes = myReq.flush();
	
    var cookieCnt = myRes.getCookieCount();  //服务器返回的cookie信息
    for(var i = 0 ; i < cookieCnt ; i++)
    {
        var cookie = request.getCookie(i);
		
        print cookie.getName();
        print cookie.getValue();
    }
}

客户端模拟form表单的例子:

import lib/httpdef

function main(parm)
{
    var clientReq = new HttpClientRequest();
    clientReq.setMethod(HTTP_POST);
    clientReq.setUrl("http://127.0.0.1:8003/form.cb");
    var formdata = clientReq.addFormData(FORM_MULTI_PART); //也可以用FORM_WWW_URLENCODED,FORM_WWW_URLENCODED类型不支持上传文件
    formdata.addText("111","111");
    formdata.addText("222","222");
    formdata.addFile("file","111.txt","222");
    formdata.addFile("file","222.txt","34567");
    clientReq.flush();	
}

HttpServer同一个端口支持多个不同内容的网站

如果需要在一台服务上同时布置多个网站,则需要在调用HttpServer下面几个接口时第二个参数传入网站的域名即可

函数

描述

用法

setRoot(path,domain)

设置域名跟目录

httpServer.setRoot(path,"www.cbrother.net")

setNormalAction(actName,domain)

设置域名默认响应接口

httpServer.setNormalAction("hello.cb","www.cbrother.net")

set404Action(actName,domain)

设置域名错误界面接口,不设置有默认页面

httpServer.set404Action("404.cb","www.cbrother.net")

setHttpsCertFile(CRT_PATH,KEY_PATH,domain)

设置https证书,如果设置了证书,该域名才可支持https协议