标签 框架 下的文章

我的上一篇文章中,我试图解释为什么我认为 Hyperapp 是一个 ReactVue 的可用替代品,原因是,我发现它易于起步。许多人批评这篇文章,认为它自以为是,并没有给其它框架一个展示自己的机会。因此,在这篇文章中,我将尽可能客观的通过提供一些最小化的例子来比较这三个框架,以展示它们的能力。

耳熟能详的计时器例子

计时器可能是响应式编程中最常用的例子之一,极其易于理解:

  • 你需要一个变量 count 保持对计数器的追踪。
  • 你需要两个方法来增加或减少 count 变量的值。
  • 你需要一种方法来渲染 count 变量,并将其呈现给用户。
  • 你需要挂载到这两个方法上的两个按钮,以便在用户和它们产生交互时变更 count 变量。

下述代码是上述所有三个框架的实现:

使用 React、Vue 和 Hyperapp 实现的计数器

这里或许会有很多要做的事情,特别是当你并不熟悉其中的一个或多个步骤的时候,因此,我们来一步一步解构这些代码:

  • 这三个框架的顶部都有一些 import 语句
  • React 更推崇面向对象的范式,就是创建一个 Counter 组件的 class。Vue 遵循类似的范式,通过创建一个新的 Vue 类的实例并将信息传递给它来实现。最后,Hyperapp 坚持函数范式,同时完全彼此分离 viewstateaction
  • count 变量而言, React 在组件的构造函数内对其进行实例化,而 Vue 和 Hyperapp 则分别是在它们的 datastate 中设置这些属性。
  • 继续看,你可能注意到 React 和 Vue 有相同的方法来与 count 变量进行交互。 React 使用继承自 React.ComponentsetState 方法来修改它的状态,而 Vue 直接修改 this.count。 Hyperapp 使用 ES6 的双箭头语法来实现这个方法,而据我所知,这是唯一一个推荐使用这种语法的框架,React 和 Vue 需要在它们的方法内使用 this。另一方面,Hyperapp 的方法需要将状态作为参数,这意味着可以在不同的上下文中重用它们。
  • 这三个框架的渲染部分实际上是相同的。唯一的细微差别是 Vue 需要一个函数 h 作为参数传递给渲染器,事实上 Hyperapp 使用 onclick 替代 onClick ,以及基于每个框架中实现状态的方式引用 count 变量。
  • 最后,所有的三个框架都被挂载到了 #app 元素上。每个框架都有稍微不同的语法,Vue 则使用了最直接的语法,通过使用元素选择器而不是使用元素来提供最大的通用性。

计数器案例对比意见

同时比较所有的三个框架,Hyperapp 需要最少的代码来实现计数器,并且它是唯一一个使用函数范式的框架。然而,Vue 的代码在绝对长度上似乎更短一些,元素选择器的挂载方式是一个很好的增强。React 的代码看起来最多,但是并不意味着代码不好理解。

使用异步代码

偶尔你可能需要处理异步代码。最常见的异步操作之一是发送请求给一个 API。为了这个例子的目的,我将使用一个[占位 API] 以及一些假数据来渲染一个文章列表。必须做的事情如下:

  • 在状态里保存一个 posts 的数组
  • 使用一个方法和正确的 URL 来调用 fetch() ,等待返回数据,转化为 JSON,并最终使用接收到的数据更新 posts 变量。
  • 渲染一个按钮,这个按钮将调用抓取文章的方法。
  • 渲染有主键的 posts 列表。

从一个 RESTFul API 抓取数据

让我们分解上面的代码,并比较三个框架:

  • 与上面的技术里例子类似,这三个框架之间的存储状态、渲染视图和挂载非常相似。这些差异与上面的讨论相同。
  • 在三个框架中使用 fetch() 抓取数据都非常简单,并且可以像预期一样工作。然而其中的关键在于, Hyperapp 处理异步操作和其它两种框架有些不同。当数据被接收到并转换为 JSON 时,该操作将调用不同的同步动作以取代直接在异步操作中修改状态。
  • 就代码长度而言,Hyperapp 依然只用最少的代码行数实现了相同的结果,但是 Vue 的代码看起来不那么的冗长,同时拥有最少的绝对字符长度。

异步代码对比意见

无论你选择哪种框架,异步操作都非常简单。在应用异步操作时, Hyperapp 可能会迫使你去遵循编写更加函数化和模块化的代码的方式。但是另外两个框架也确实可以做到这一点,并且在这一方面给你提供更多的选择。

To-Do 列表组件案例

在响应式编程中,最出名的例子可能是使用每一个框架里来实现 To-Do 列表。我不打算在这里实现整个部分,我只实现一个无状态的组件,来展示三个框架如何创建更小的可复用的块来协助构建应用程序。

示例 TodoItem 实现

上面的图片展示了每一个框架一个例子,并为 React 提供了一个额外的例子。接下来是我们从它们四个中看到的:

  • React 在编程范式上最为灵活。它支持函数组件,也支持类组件。它还支持你在右下角看到的 Hyperapp 组件,无需任何修改。
  • Hyperapp 还支持 React 的函数组件实现,这意味着两个框架之间还有很多的实验空间。
  • 最后出现的 Vue 有着其合理而又奇怪的语法,即使是对另外两个框架很有经验的人,也不能马上理解其含义。
  • 在长度方面,所有的案例代码长度非常相似,在 React 的一些方法中稍微冗长一些。

To-Do 列表项目对比意见

Vue 需要花费一些时间来熟悉,因为它的模板和其它两个框架有一些不同。React 非常的灵活,支持多种不同的方法来创建组件,而 HyperApp 保持一切简单,并提供与 React 的兼容性,以免你希望在某些时刻进行切换。

生命周期方法比较

另一个关键对比是组件的生命周期事件,每一个框架允许你根据你的需要来订阅和处理事件。下面是我根据各框架的 API 参考手册创建的表格:

生命周期方式比较

  • Vue 提供了最多的生命周期钩子,提供了处理生命周期事件之前或之后发生的任何事件的机会。这能有效帮助管理复杂的组件。
  • React 和 Hyperapp 的生命周期钩子非常类似,React 将 unmountdestory 绑定在了一起,而 Hyperapp 则将 createmount 绑定在了一起。两者在处理生命周期事件方面都提供了相当多的控制。
  • Vue 根本没有处理 unmount (据我所理解),而是依赖于 destroy 事件在组件稍后的生命周期进行处理。 React 不处理 destory 事件,而是选择只处理 unmount 事件。最终,HyperApp 不处理 create 事件,取而代之的是只依赖 mount 事件。

生命周期对比意见

总的来说,每个框架都提供了生命周期组件,它们帮助你处理组件生命周期中的许多事情。这三个框架都为它们的生命周期提供了钩子,其之间的细微差别,可能源自于实现和方案上的根本差异。通过提供更细粒度的时间处理,Vue 可以更进一步的允许你在开始或结束之后处理生命周期事件。

性能比较

除了易用性和编码技术以外,性能也是大多数开发人员考虑的关键因素,尤其是在进行更复杂的应用程序时。js-framework-benchmark 是一个很好的用于比较框架的工具,所以让我们看看每一组测评数据数组都说了些什么:

测评操作表

  • 与三个框架的有主键操作相比,无主键操作更快。
  • 无主键的 React 在所有六种对比中拥有最强的性能,它在所有测试上都有令人深刻的表现。
  • 有主键的 Vue 只比有主键的 React 性能稍强,而无主键的 Vue 要比无主键的 React 性能明显差。
  • Vue 和 Hyperapp 在进行局部更新的性能测试时遇见了一些问题,与此同时,React 似乎对该问题进行很好的优化。

启动测试

  • Hyperapp 是三个框架中最轻量的,而 React 和 Vue 有非常小的大小差异。
  • Hyperapp 具有最快的启动时间,这得益于它极小的大小和极简的 API
  • Vue 在启动上比 React 好一些,但是差异非常小。

内存分配测试

  • Hyperapp 是三者中对资源依赖最小的一个,与其它两者相比,任何一个操作都需要更少的内存。
  • 资源消耗不是非常高,三者都应该在现代硬件上进行类似的操作。

性能对比意见

如果性能是一个问题,你应该考虑你正在使用什么样的应用程序以及你的需求是什么。看起来 Vue 和 React 用于更复杂的应用程序更好,而 Hyperapp 更适合于更小的应用程序、更少的数据处理和需要快速启动的应用程序,以及需要在低端硬件上工作的应用程序。

但是,要记住,这些测试远不能代表一般场景,所以在现实场景中可能会看到不同的结果。

额外备注

比较 React、Vue 和 Hyperapp 可能像在许多方面比较苹果、橘子。关于这些框架还有一些其它的考虑,它们可以帮助你决定使用另一个框架。

  • React 通过引入片段,避免了相邻的 JSX 元素必须封装在父元素中的问题,这些元素允许你将子元素列表分组,而无需向 DOM 添加额外的节点。
  • React 还为你提供更高级别的组件,而 VUE 为你提供重用组件功能的 MIXIN
  • Vue 允许使用模板来分离结构和功能,从而更好的分离关注点。
  • 与其它两个相比,Hyperapp 感觉像是一个较低级别的 API,它的代码短得多,如果你愿意调整它并学习它的工作原理,那么它可以提供更多的通用性。

结论

我认为如果你已经阅读了这么多,你已经知道哪种工具更适合你的需求。毕竟,这不是讨论哪一个更好,而是讨论哪一个更适合每种情况。总而言之:

  • React 是一个非常强大的工具,围绕它有大规模的开发者社区,可能会帮助你找到一个工作。入门并不难,但是掌握它肯定需要很多时间。然而,这是非常值得去花费你的时间全面掌握的。
  • 如果你过去曾使用过另外的 JavaScript 框架,Vue 可能看起来有点奇怪,但它也是一个非常有趣的工具。如果 React 不是你所喜欢的,那么它可能是一个可行的、值得学习的选择。它有一些非常酷的内置功能,其社区也在增长中,甚至可能要比 React 增长还要快。
  • 最后,Hyperapp 是一个为小型项目而生的很酷的小框架,也是初学者入门的好地方。它提供比 React 或 Vue 更少的工具,但是它能帮助你快速构建原型并理解许多基本原理。你为它编写的许多代码和其它两个框架兼容,要么立即能用,或者是稍做更改就行,你可以在对它们中另外一个有信心时切换框架。

作者简介:

喜欢编码的 Web 开发者,“30 秒编码” ( https://30secondsofcode.org/)和mini.css框架(http://minicss.org ) 的创建者。


via: https://hackernoon.com/javascript-framework-comparison-with-examples-react-vue-hyperapp-97f064fb468d

作者:Angelos Chalaris
译者:Bestony
校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

值得好好学习的 6 个 JavaScript 框架

常言道,条条大路通罗马,可是那一条适合我呢? 由于用于构建前端页面等现代技术的出现,JavaScript 在 Web 开发社区早已是如雷贯耳。通过在网页上编写几个函数并提供执行逻辑,可以很好的支持 HTML (主要是用于页面的 表现 或者 布局)。如果没有 JavaScript,那页面将没有任何 交互特性 可言。

现在的框架和库已经从蛮荒时代崛起了,很多老旧的技术纷纷开始将功能分离成模块。现在不再需要在整个核心语言中支持所有特性了,开发者允许所有用户创建库和框架来增强核心语言的功能。这样,语言的灵活性获得了显著提高。

如果在已经在使用 JavaScript (以及 JQuery) 来支持 HTML,那么你肯定知道开发和维护一个大型应用需要付出多大的努力以及编写多么复杂的代码,而 JavaScript 框架可以帮助你快速的构建交互式 Web 应用 (包含单页面应用或者多页面应用)。

当一个新手开发者想要学习 JavaScript 时,他常常会被各种 JavaScript 框架所吸引,也幸亏有为数众多的社区,任何开发者都可以轻易地通过在线教程或者其他资源来学习。

但是,唉!多数的程序员都很难决定学习和使用哪一个框架。因此在本文中,我将为大家推荐 6 个值得好好学习的 JavaScript 框架。让我们开始吧。

1、AngularJS

Angular

(注:这是我个人最喜欢的框架)

无论你是何时听说的 JavaScript,很可能你早就听过 AngularJS,因为这是在 JavaScript 社区中最为广泛使用的框架了。它发布于 2009 年,由 Google 开发 (这够有说服力让你使用了吧) ,它是一个开源项目,这意味着你可以阅读、编辑和修改其源代码以便更加符合自身的需求,并且不用向其开发者支付一分钱 (这不是很酷吗?)。

如果说你觉得通过纯粹的 JavaScript 代码编写一个复杂的 Web 应用比较困难的话,那么你肯定会兴奋的跳起来,因为它将显著地减轻你的编码负担。它符合支持双向数据绑定的 MVC ( 模型-视图-控制 Model–view–controller ) 设计典范。假如你不熟悉 MVC,你只需要知道它代表着无论何时探测到某些变化,它将自动更新前端 (比如,用户界面端) 和后端 (代码或者服务器端) 数据。

MVC 可以大大减少构建复杂应用程序所需的时间和精力,所以你只需要集中精力于一处即可 (DOM 编程接口会自动同步更新视图和模型)。由于 视图组件模型组件 是分离的,你可以很容易的创建一个可复用的组件,使得用户界面的效果非常好看。

如果因为某些原因,你已经使用了 TypeScript (一种与 JavaScript 非常相似的语言),那么你可以很容易就上手 AngularJS,因为这两者的语法高度相似。与 TypeScript 相似这一特点在一定程度上提升了 AngularJS 的受欢迎程度。

目前,Angular 2.0 已经发布,并且提升了移动端的性能,这也足以向一个新的开发者证明,该框架的开发活跃度够高并且定期更新。

AngularJS 有着大量的用户,包括 (但不限于) Udemy、Forbes、GoDaddy、Ford、NBA 和 Oscars。

对于那些想要一个高效的 MVC 框架,用来开发面面俱到、包含健壮且现代化的基础架构的单页应用的用户来说,我极力的推荐这个框架。这是为无经验 JavaScript 开发者设计的首选框架。

2、React

React

与 AngularJS 相似,React 也是一个 MVC ( 模型-视图-控制 Model–view–controller ) 类型的框架,但不同的是,它完全针对于 视图组件 (因为它是为 UI 特别定制的) ,并且可与任何架构进行无缝衔接。这意味着你可以马上将它运用到你的网站中去。

它从核心功能中抽象出 DOM 编程接口 (并且因此使用了虚拟 DOM),所以你可以快速渲染 UI,这使得你能够通过 node.js 将它作为一个客户端框架来使用。它是由 Facebook 开发的开源项目,还有其它的开发者为它贡献了代码。

假如说你见到过并喜欢 Facebook 和 Instagram 的界面,那么你将会爱上 React。通过 React,你可以给你的应用的每个状态设计一个简单的视图,当数据改变的时候,视图也自动随之改变。只要你想的话,可以创建各种的复杂 UI,也可以在任何应用中复用它。在服务器端,React 同样支持通过 node.js 来进行渲染。对于其他的接口,React 也一样表现得足够灵活。

除 Facebook 和 Instagram 外,还有好多公司也在使用 React,包括 Whatsapp、BBC、PayPal、Netflix 和 Dropbox 等。

如果你只需要一个前端开发框架来构建一个非常复杂且界面极好的强大视图层,那我极力向你推荐这个框架,但你需要有足够的经验来处理各种类型的 JavaScript 代码,而且你再也不需要其他的组件了 (因为你可以自己集成它们)。

3、Ember

Ember

这个 JavaScript 框架在 2011 年正式发布,是由 Yehuda Katz 开发的开源项目。它有一个庞大且活跃的在线社区,所以在有任何问题时,你都可以在社区中提问。该框架吸收融合了非常多的通用的 JavaScript 风格和经验,以便确保开发者能最快地做到开箱即用。

它使用了 MVVM ( 模型-视图-视图模型 Model–view–viewmodel ) 的设计模式,这使得它与 MVC 有些不一样,因为它由一个 连接器 (binder) 帮助视图和数据连接器进行通信。

对于 DOM 编程接口的快速服务端渲染,它借助了 Fastboot.js,这能够让那些复杂 UI 的性能得到极大提高。

它的现代化路由模式和模型引擎还支持 RESTful API,这确保你可以使用到这种最新的技术。它支持 句柄集成模板 Handlebars integrated template ,用来自动更新数据。

早在 2015 年间,它的风头曾一度盖过 AngularJS 和 React,被称为最好的 JavaScript 框架,对于它在 JavaScript 社区中的可用性和吸引力,这样的说服力该是足够了的。

对于不追求高灵活性和大型架构的用户,并且仅仅只是为了赶赴工期、完成任务的话,我个人非常推荐这个 JavaScript 框架。

4、Adonis

Adonis

如果你曾使用过 LaravelNodeJS,那么你在使用这一个框架之时会觉得相当顺手,因为它是集合了这两个平台的优点而形成的一个框架,对于任何种类的现代应用来说,它都显得非常专业、圆润和精致。

它使用了 NodeJS,所以是一个很好的后端框架,同时还附带有一些前端特性 (与前面提到那些更多地注重前端的框架不同),所以想要进入后端开发的新手开发者会发觉这个框架相当迷人。

相比于 NoSQL,很多的开发者都比较喜欢使用 SQL 数据库 (因为他们需要增强和数据以及其它特性的交互性),这一现象在这个框架中得到了很好的体现,这使得它更接近标准,开发者也更容易使用。

如果你混迹于各类 PHP 社区,那你一定很熟悉 服务提供者 Service Providers ,也由于 Adonis 其中包含相应的 PHP 风格,所以在使用它的时候,你会觉得似曾相识。

在它所有的特性中,最好的便是那个极为强大的路由引擎,支持使用函数来组织和管理应用的所有状态、支持错误处理机制、支持通过 SQL ORM 来进行数据库查询、支持生成器、支持 箭头函数 arrow functions 、支持代理等等。

如果喜欢使用无状态 REST API 来构建服务器端应用,我比较推荐它,你会爱上这个框架的。

5、Vue.js

Vue.js

这是一个开源的 JavaScript 框架,发布于 2014 年,它有个极为简单的 API,用来为 现代 Web 界面 Modern Web Interface 开发 交互式组件 Reactive components 。其设计着重于简单易用。与 Ember 相似,它使用的是 MVVM ( 模型-视图-视图模型 Model–view–viewmodel ) 设计范例,这样简化了设计。

这个框架最有吸引力的一点是,你可以根据自身需求来选择使用的模块。比如,你需要编写简单的 HTML 代码,抓取 JSON,然后创建一个 Vue 实例来完成可以复用的小特效。

与之前的那些 JavaScript 框架相似,它使用双路数据绑定来更新模型和视图,同时也使用连接器来完成视图和数据连接器的通信。这是一个还未完全成熟的框架,因为它全部的关注点都在视图层,所以你需要自己处理其它组件。

如果你熟悉 AngularJS,那你会感觉很顺手,因为它大量嵌入了 AngularJS 的架构,如果你懂得 JavaScript 的基础用法,那你的许多项目都可以轻易地迁移到该框架之下。

假如你只想把任务完成,或者想提升你自身的 JavaScript 编程经验,又或者你需要学习不同的 JavAScript 框架的本质,我极力推荐这个。

6、Backbone.js

Backbone.JS

这个框架可以很容易的集成到任何第三方的模板引擎中,默认使用的是 Underscore 模板引擎,而且该框架仅有一个依赖项 (JQuery),因此它以轻量而闻名。它支持带有 RESTful JSON 接口的 MVC ( 模型-视图-控制 Model–view–controller ) (可以自动更新前端和后端) 设计范例。

假如你曾经使用过著名的社交新闻网络服务 reddit,那么你肯定听说过它在几个单页面应用中使用了 Backbone.jsBackbone.js 的原作者为之建立了与 CoffeScript 旗鼓相当的 Underscore 模板引擎,所以你可以放心,开发者知道该做什么。

该框架在一个软件包中提供了 键值对 key-value 模型、视图以及几个打包的模块,所以你不需要额外下载其他的外部包,这样可以节省不少时间。框架的源码可以在 GitHub 进行查看,你可以根据需求进行深度定制。

如果你在寻找一个入门级框架来快速构建一个单页面应用,那么这个框架非常适合你。

总而言之

至此,我已经在本文着重说明了 6 个值得好好学习的 JavaScript 框架,希望你读完本文后能够决定使用哪个框架来完成自己的任务。

如果说对于选择框架,你还是不知所措,请记住,这个世界是实践出真知而非教条主义的。最好就是从列表中挑选一个来使用,看看最后是否满足你的需求和兴趣,如果还是不行,接着试试另一个。你也尽管放心好了,列表中的框架肯定是足够了的。


译者简介:

GHLandy —— 生活中所有欢乐与苦闷都应藏在心中,有些事儿注定无人知晓,自己也无从说起。


via: http://www.discoversdk.com/blog/6-best-javascript-frameworks-to-learn-in-2016

作者:Danyal Zia 译者:GHLandy 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

为什么你想要自己构建一个 web 框架呢?我想,原因有以下几点:

  • 你有一个新奇的想法,觉得将会取代其他的框架
  • 你想要获得一些名气
  • 你遇到的问题很独特,以至于现有的框架不太合适
  • 你对 web 框架是如何工作的很感兴趣,因为你想要成为一位更好的 web 开发者。

接下来的笔墨将着重于最后一点。这篇文章旨在通过对设计和实现过程一步一步的阐述告诉读者,我在完成一个小型的服务器和框架之后学到了什么。你可以在这个代码仓库中找到这个项目的完整代码。

我希望这篇文章可以鼓励更多的人来尝试,因为这确实很有趣。它让我知道了 web 应用是如何工作的,而且这比我想的要容易的多!

范围

框架可以处理请求-响应周期、身份认证、数据库访问、模板生成等部分工作。Web 开发者使用框架是因为,大多数的 web 应用拥有大量相同的功能,而对每个项目都重新实现同样的功能意义不大。

比较大的的框架如 Rails 和 Django 实现了高层次的抽象,或者说“自备电池”(“batteries-included”,这是 Python 的口号之一,意即所有功能都自足。)。而实现所有的这些功能可能要花费数千小时,因此在这个项目上,我们重点完成其中的一小部分。在开始写代码前,我先列举一下所需的功能以及限制。

功能:

  • 处理 HTTP 的 GET 和 POST 请求。你可以在这篇 wiki 中对 HTTP 有个大致的了解。
  • 实现异步操作(我喜欢 Python 3 的 asyncio 模块)。
  • 简单的路由逻辑以及参数撷取。
  • 像其他微型框架一样,提供一个简单的用户级 API 。
  • 支持身份认证,因为学会这个很酷啊(微笑)。

限制:

  • 将只支持 HTTP 1.1 的一个小子集,不支持 传输编码 transfer-encoding HTTP 认证 http-auth 内容编码 content-encoding (如 gzip)以及持久化连接等功能。
  • 不支持对响应内容的 MIME 判断 - 用户需要手动指定。
  • 不支持 WSGI - 仅能处理简单的 TCP 连接。
  • 不支持数据库。

我觉得一个小的用例可以让上述内容更加具体,也可以用来演示这个框架的 API:

from diy_framework import App, Router
from diy_framework.http_utils import Response


# GET simple route
async def home(r):
    rsp = Response()
    rsp.set_header('Content-Type', 'text/html')
    rsp.body = '<html><body><b>test</b></body></html>'
    return rsp


# GET route + params
async def welcome(r, name):
    return "Welcome {}".format(name)

# POST route + body param
async def parse_form(r):
    if r.method == 'GET':
        return 'form'
    else:
        name = r.body.get('name', '')[0]
        password = r.body.get('password', '')[0]

       return "{0}:{1}".format(name, password)

# application = router + http server
router = Router()
router.add_routes({
    r'/welcome/{name}': welcome,
    r'/': home,
    r'/login': parse_form,})

app = App(router)
app.start_server()

' 用户需要定义一些能够返回字符串或 Response 对象的异步函数,然后将这些函数与表示路由的字符串配对,最后通过一个函数调用(start_server)开始处理请求。

完成设计之后,我将它抽象为几个我需要编码的部分:

  • 接受 TCP 连接以及调度一个异步函数来处理这些连接的部分
  • 将原始文本解析成某种抽象容器的部分
  • 对于每个请求,用来决定调用哪个函数的部分
  • 将上述部分集中到一起,并为开发者提供一个简单接口的部分

我先编写一些测试,这些测试被用来描述每个部分的功能。几次重构后,整个设计被分成若干部分,每个部分之间是相对解耦的。这样就非常好,因为每个部分可以被独立地研究学习。以下是我上文列出的抽象的具体体现:

  • 一个 HTTPServer 对象,需要一个 Router 对象和一个 http\_parser 模块,并使用它们来初始化。
  • HTTPConnection 对象,每一个对象表示一个单独的客户端 HTTP 连接,并且处理其请求-响应周期:使用 http\_parser 模块将收到的字节流解析为一个 Request 对象;使用一个 Router 实例寻找并调用正确的函数来生成一个响应;最后将这个响应发送回客户端。
  • 一对 Request 和 Response 对象为用户提供了一种友好的方式,来处理实质上是字节流的字符串。用户不需要知道正确的消息格式和分隔符是怎样的。
  • 一个包含“路由:函数”对应关系的 Router 对象。它提供一个添加配对的方法,可以根据 URL 路径查找到相应的函数。
  • 最后,一个 App 对象。它包含配置信息,并使用它们实例化一个 HTTPServer 实例。

让我们从 HTTPConnection 开始来讲解各个部分。

模拟异步连接

为了满足上述约束条件,每一个 HTTP 请求都是一个单独的 TCP 连接。这使得处理请求的速度变慢了,因为建立多个 TCP 连接需要相对高的花销(DNS 查询,TCP 三次握手,慢启动等等的花销),不过这样更加容易模拟。对于这一任务,我选择相对高级的 asyncio-stream 模块,它建立在 asyncio 的传输和协议的基础之上。我强烈推荐你读一读标准库中的相应代码,很有意思!

一个 HTTPConnection 的实例能够处理多个任务。首先,它使用 asyncio.StreamReader 对象以增量的方式从 TCP 连接中读取数据,并存储在缓存中。每一个读取操作完成后,它会尝试解析缓存中的数据,并生成一个 Request 对象。一旦收到了这个完整的请求,它就生成一个回复,并通过 asyncio.StreamWriter 对象发送回客户端。当然,它还有两个任务:超时连接以及错误处理。

你可以在这里浏览这个类的完整代码。我将分别介绍代码的每一部分。为了简单起见,我移除了代码文档。

class HTTPConnection(object):
    def init(self, http_server, reader, writer):
        self.router = http_server.router
        self.http_parser = http_server.http_parser
        self.loop = http_server.loop

        self._reader = reader
        self._writer = writer
        self._buffer = bytearray()
        self._conn_timeout = None
        self.request = Request()

这个 init 方法没啥意思,它仅仅是收集了一些对象以供后面使用。它存储了一个 router 对象、一个 http_parser 对象以及 loop 对象,分别用来生成响应、解析请求以及在事件循环中调度任务。

然后,它存储了代表一个 TCP 连接的读写对,和一个充当原始字节缓冲区的空字节数组_conn_timeout 存储了一个 asyncio.Handle 的实例,用来管理超时逻辑。最后,它还存储了 Request 对象的一个单一实例。

下面的代码是用来接受和发送数据的核心功能:

async def handle_request(self):
    try:
        while not self.request.finished and not self._reader.at_eof():
            data = await self._reader.read(1024)
            if data:
                self._reset_conn_timeout()
                await self.process_data(data)
        if self.request.finished:
            await self.reply()
        elif self._reader.at_eof():
            raise BadRequestException()
    except (NotFoundException,
            BadRequestException) as e:
        self.error_reply(e.code, body=Response.reason_phrases[e.code])
    except Exception as e:
        self.error_reply(500, body=Response.reason_phrases[500])

    self.close_connection()

所有内容被包含在 try-except 代码块中,这样在解析请求或响应期间抛出的异常可以被捕获到,然后一个错误响应会发送回客户端。

while 循环中不断读取请求,直到解析器将 self.request.finished 设置为 True ,或者客户端关闭连接所触发的信号使得 self._reader_at_eof() 函数返回值为 True 为止。这段代码尝试在每次循环迭代中从 StreamReader 中读取数据,并通过调用 self.process_data(data) 函数以增量方式生成 self.request。每次循环读取数据时,连接超时计数器被重置。

这儿有个错误,你发现了吗?稍后我们会再讨论这个。需要注意的是,这个循环可能会耗尽 CPU 资源,因为如果没有读取到东西 self._reader.read() 函数将会返回一个空的字节对象 b''。这就意味着循环将会不断运行,却什么也不做。一个可能的解决方法是,用非阻塞的方式等待一小段时间:await asyncio.sleep(0.1)。我们暂且不对它做优化。

还记得上一段我提到的那个错误吗?只有从 StreamReader 读取数据时,self._reset_conn_timeout() 函数才会被调用。这就意味着,直到第一个字节到达时timeout 才被初始化。如果有一个客户端建立了与服务器的连接却不发送任何数据,那就永远不会超时。这可能被用来消耗系统资源,从而导致拒绝服务式攻击(DoS)。修复方法就是在 init 函数中调用 self._reset_conn_timeout() 函数。

当请求接受完成或连接中断时,程序将运行到 if-else 代码块。这部分代码会判断解析器收到完整的数据后是否完成了解析。如果是,好,生成一个回复并发送回客户端。如果不是,那么请求信息可能有错误,抛出一个异常!最后,我们调用 self.close_connection 执行清理工作。

解析请求的部分在 self.process_data 方法中。这个方法非常简短,也易于测试:

async def process_data(self, data):
    self._buffer.extend(data)

    self._buffer = self.http_parser.parse_into(
        self.request, self._buffer)

每一次调用都将数据累积到 self._buffer 中,然后试着用 self.http_parser 来解析已经收集的数据。这里需要指出的是,这段代码展示了一种称为依赖注入(Dependency Injection)的模式。如果你还记得 init 函数的话,应该知道我们传入了一个包含 http_parser 对象的 http_server 对象。在这个例子里,http_parser 对象是 diy_framework 包中的一个模块。不过它也可以是任何含有 parse_into 函数的类,这个 parse_into 函数接受一个 Request 对象以及字节数组作为参数。这很有用,原因有二:一是,这意味着这段代码更易扩展。如果有人想通过一个不同的解析器来使用 HTTPConnection,没问题,只需将它作为参数传入即可。二是,这使得测试更加容易,因为 http_parser 不是硬编码的,所以使用虚假数据或者 mock 对象来替代是很容易的。

下一段有趣的部分就是 reply 方法了:

async def reply(self):
    request = self.request
    handler = self.router.get_handler(request.path)

    response = await handler.handle(request)

    if not isinstance(response, Response):
        response = Response(code=200, body=response)

    self._writer.write(response.to_bytes())
    await self._writer.drain()

这里,一个 HTTPConnection 的实例使用了 HTTPServer 中的 router 对象来得到一个生成响应的对象。一个路由可以是任何一个拥有 get_handler 方法的对象,这个方法接收一个字符串作为参数,返回一个可调用的对象或者抛出 NotFoundException 异常。而这个可调用的对象被用来处理请求以及生成响应。处理程序由框架的使用者编写,如上文所说的那样,应该返回字符串或者 Response 对象。Response 对象提供了一个友好的接口,因此这个简单的 if 语句保证了无论处理程序返回什么,代码最终都得到一个统一的 Response 对象。

接下来,被赋值给 self._writerStreamWriter 实例被调用,将字节字符串发送回客户端。函数返回前,程序在 await self._writer.drain() 处等待,以确保所有的数据被发送给客户端。只要缓存中还有未发送的数据,self._writer.close() 方法就不会执行。

HTTPConnection 类还有两个更加有趣的部分:一个用于关闭连接的方法,以及一组用来处理超时机制的方法。首先,关闭一条连接由下面这个小函数完成:

def close_connection(self):
    self._cancel_conn_timeout()
    self._writer.close()

每当一条连接将被关闭时,这段代码首先取消超时,然后把连接从事件循环中清除。

超时机制由三个相关的函数组成:第一个函数在超时后给客户端发送错误消息并关闭连接;第二个函数用于取消当前的超时;第三个函数调度超时功能。前两个函数比较简单,我将详细解释第三个函数 _reset_cpmm_timeout()

def _conn_timeout_close(self):
    self.error_reply(500, 'timeout')
    self.close_connection()

def _cancel_conn_timeout(self):
    if self._conn_timeout:
        self._conn_timeout.cancel()

def _reset_conn_timeout(self, timeout=TIMEOUT):
    self._cancel_conn_timeout()
    self._conn_timeout = self.loop.call_later(
        timeout, self._conn_timeout_close)

每当 _reset_conn_timeout 函数被调用时,它会先取消之前所有赋值给 self._conn_timeoutasyncio.Handle 对象。然后,使用 BaseEventLoop.call\_later 函数让 _conn_timeout_close 函数在超时数秒(timeout)后执行。如果你还记得 handle_request 函数的内容,就知道每当接收到数据时,这个函数就会被调用。这就取消了当前的超时并且重新安排 _conn_timeout_close 函数在超时数秒(timeout)后执行。只要接收到数据,这个循环就会不断地重置超时回调。如果在超时时间内没有接收到数据,最后函数 _conn_timeout_close 就会被调用。

创建连接

我们需要创建 HTTPConnection 对象,并且正确地使用它们。这一任务由 HTTPServer 类完成。HTTPServer 类是一个简单的容器,可以存储着一些配置信息(解析器,路由和事件循环实例),并使用这些配置来创建 HTTPConnection 实例:

class HTTPServer(object):
    def init(self, router, http_parser, loop):
        self.router = router
        self.http_parser = http_parser
        self.loop = loop

    async def handle_connection(self, reader, writer):
        connection = HTTPConnection(self, reader, writer)
        asyncio.ensure_future(connection.handle_request(), loop=self.loop)

HTTPServer 的每一个实例能够监听一个端口。它有一个 handle_connection 的异步方法来创建 HTTPConnection 的实例,并安排它们在事件循环中运行。这个方法被传递给 asyncio.start\_server 作为一个回调函数。也就是说,每当一个 TCP 连接初始化时(以 StreamReaderStreamWriter 为参数),它就会被调用。

   self._server = HTTPServer(self.router, self.http_parser, self.loop)
   self._connection_handler = asyncio.start_server(
        self._server.handle_connection,
        host=self.host,
        port=self.port,
        reuse_address=True,
        reuse_port=True,
        loop=self.loop)

这就是构成整个应用程序工作原理的核心:asyncio.start_server 接受 TCP 连接,然后在一个预配置的 HTTPServer 对象上调用一个方法。这个方法将处理一条 TCP 连接的所有逻辑:读取、解析、生成响应并发送回客户端、以及关闭连接。它的重点是 IO 逻辑、解析和生成响应。

讲解了核心的 IO 部分,让我们继续。

解析请求

这个微型框架的使用者被宠坏了,不愿意和字节打交道。它们想要一个更高层次的抽象 —— 一种更加简单的方法来处理请求。这个微型框架就包含了一个简单的 HTTP 解析器,能够将字节流转化为 Request 对象。

这些 Request 对象是像这样的容器:

class Request(object):
    def init(self):
        self.method = None
        self.path = None
        self.query_params = {}
        self.path_params = {}
        self.headers = {}
        self.body = None
        self.body_raw = None
        self.finished = False

它包含了所有需要的数据,可以用一种容易理解的方法从客户端接受数据。哦,不包括 cookie ,它对身份认证是非常重要的,我会将它留在第二部分。

每一个 HTTP 请求都包含了一些必需的内容,如请求路径和请求方法。它们也包含了一些可选的内容,如请求体、请求头,或是 URL 参数。随着 REST 的流行,除了 URL 参数,URL 本身会包含一些信息。比如,"/user/1/edit" 包含了用户的 id 。

一个请求的每个部分都必须被识别、解析,并正确地赋值给 Request 对象的对应属性。HTTP/1.1 是一个文本协议,事实上这简化了很多东西。(HTTP/2 是一个二进制协议,这又是另一种乐趣了)

解析器不需要跟踪状态,因此 http_parser 模块其实就是一组函数。调用函数需要用到 Request 对象,并将它连同一个包含原始请求信息的字节数组传递给 parse_into 函数。然后解析器会修改 Request 对象以及充当缓存的字节数组。字节数组的信息被逐渐地解析到 request 对象中。

http_parser 模块的核心功能就是下面这个 parse_into 函数:

def parse_into(request, buffer):
    _buffer = buffer[:]
    if not request.method and can_parse_request_line(_buffer):
        (request.method, request.path,
         request.query_params) = parse_request_line(_buffer)
        remove_request_line(_buffer)

    if not request.headers and can_parse_headers(_buffer):
        request.headers = parse_headers(_buffer)
        if not has_body(request.headers):
            request.finished = True

        remove_intro(_buffer)

    if not request.finished and can_parse_body(request.headers, _buffer):
        request.body_raw, request.body = parse_body(request.headers, _buffer)
        clear_buffer(_buffer)
        request.finished = True
    return _buffer

从上面的代码中可以看到,我把解析的过程分为三个部分:解析请求行(这行像这样:GET /resource HTTP/1.1),解析请求头以及解析请求体。

请求行包含了 HTTP 请求方法以及 URL 地址。而 URL 地址则包含了更多的信息:路径、url 参数和开发者自定义的 url 参数。解析请求方法和 URL 还是很容易的 - 合适地分割字符串就好了。函数 urlparse.parse 可以用来解析 URL 参数。开发者自定义的 URL 参数可以通过正则表达式来解析。

接下来是 HTTP 头部。它们是一行行由键值对组成的简单文本。问题在于,可能有多个 HTTP 头有相同的名字,却有不同的值。一个值得关注的 HTTP 头部是 Content-Length,它描述了请求体的字节长度(不是整个请求,仅仅是请求体)。这对于决定是否解析请求体有很重要的作用。

最后,解析器根据 HTTP 方法和头部来决定是否解析请求体。

路由!

在某种意义上,路由就像是连接框架和用户的桥梁,用户用合适的方法创建 Router 对象并为其设置路径/函数对,然后将它赋值给 App 对象。而 App 对象依次调用 get_handler 函数生成相应的回调函数。简单来说,路由就负责两件事,一是存储路径/函数对,二是返回需要的路径/函数对

Router 类中有两个允许最终开发者添加路由的方法,分别是 add_routesadd_route。因为 add_routes 就是 add_route 函数的一层封装,我们将主要讲解 add_route 函数:

def add_route(self, path, handler):
    compiled_route = self.class.build_route_regexp(path)
    if compiled_route not in self.routes:
        self.routes[compiled_route] = handler
    else:
        raise DuplicateRoute

首先,这个函数使用 Router.build_router_regexp 的类方法,将一条路由规则(如 '/cars/{id}' 这样的字符串),“编译”到一个已编译的正则表达式对象。这些已编译的正则表达式用来匹配请求路径,以及解析开发者自定义的 URL 参数。如果已经存在一个相同的路由,程序就会抛出一个异常。最后,这个路由/处理程序对被添加到一个简单的字典self.routes中。

下面展示 Router 是如何“编译”路由的:

@classmethod
def build_route_regexp(cls, regexp_str):
    """
    Turns a string into a compiled regular expression. Parses '{}' into
    named groups ie. '/path/{variable}' is turned into
    '/path/(?P<variable>[a-zA-Z0-9_-]+)'.

    :param regexp_str: a string representing a URL path.
    :return: a compiled regular expression.
    """
    def named_groups(matchobj):
        return '(?P<{0}>[a-zA-Z0-9_-]+)'.format(matchobj.group(1))

    re_str = re.sub(r'{([a-zA-Z0-9_-]+)}', named_groups, regexp_str)
    re_str = ''.join(('^', re_str, '$',))
    return re.compile(re_str)

这个方法使用正则表达式将所有出现的 {variable} 替换为 (?P<variable>)。然后在字符串头尾分别添加 ^$ 标记,最后编译正则表达式对象。

完成了路由存储仅成功了一半,下面是如何得到路由对应的函数:

def get_handler(self, path):
    logger.debug('Getting handler for: {0}'.format(path))
    for route, handler in self.routes.items():
        path_params = self.class.match_path(route, path)
        if path_params is not None:
            logger.debug('Got handler for: {0}'.format(path))
            wrapped_handler = HandlerWrapper(handler, path_params)
            return wrapped_handler

    raise NotFoundException()

一旦 App 对象获得一个 Request 对象,也就获得了 URL 的路径部分(如 /users/15/edit)。然后,我们需要匹配函数来生成一个响应或者 404 错误。get_handler 函数将路径作为参数,循环遍历路由,对每条路由调用 Router.match_path 类方法检查是否有已编译的正则对象与这个请求路径匹配。如果存在,我们就调用 HandleWrapper 来包装路由对应的函数。path_params 字典包含了路径变量(如 '/users/15/edit' 中的 '15'),若路由没有指定变量,字典就为空。最后,我们将包装好的函数返回给 App 对象。

如果遍历了所有的路由都找不到与路径匹配的,函数就会抛出 NotFoundException 异常。

这个 Route.match 类方法挺简单:

def match_path(cls, route, path):
    match = route.match(path)
    try:
        return match.groupdict()
    except AttributeError:
        return None

它使用正则对象的 match 方法来检查路由是否与路径匹配。若果不匹配,则返回 None 。

最后,我们有 HandleWraapper 类。它的唯一任务就是封装一个异步函数,存储 path_params 字典,并通过 handle 方法对外提供一个统一的接口。

class HandlerWrapper(object):
    def init(self, handler, path_params):
        self.handler = handler
        self.path_params = path_params
        self.request = None

    async def handle(self, request):
        return await self.handler(request, **self.path_params)

组合到一起

框架的最后部分就是用 App 类把所有的部分联系起来。

App 类用于集中所有的配置细节。一个 App 对象通过其 start_server 方法,使用一些配置数据创建一个 HTTPServer 的实例,然后将它传递给 asyncio.start\_server 函数asyncio.start_server 函数会对每一个 TCP 连接调用 HTTPServer 对象的 handle_connection 方法。

def start_server(self):
    if not self._server:
        self.loop = asyncio.get_event_loop()
        self._server = HTTPServer(self.router, self.http_parser, self.loop)
        self._connection_handler = asyncio.start_server(
            self._server.handle_connection,
            host=self.host,
            port=self.port,
            reuse_address=True,
            reuse_port=True,
            loop=self.loop)

        logger.info('Starting server on {0}:{1}'.format(
            self.host, self.port))
        self.loop.run_until_complete(self._connection_handler)

        try:
            self.loop.run_forever()
        except KeyboardInterrupt:
            logger.info('Got signal, killing server')
        except DiyFrameworkException as e:
            logger.error('Critical framework failure:')
            logger.error(e.traceback)
        finally:
            self.loop.close()
    else:
        logger.info('Server already started - {0}'.format(self))

总结

如果你查看源码,就会发现所有的代码仅 320 余行(包括测试代码的话共 540 余行)。这么少的代码实现了这么多的功能,让我有点惊讶。这个框架没有提供模板、身份认证以及数据库访问等功能(这些内容也很有趣哦)。这也让我知道,像 Django 和 Tornado 这样的框架是如何工作的,而且我能够快速地调试它们了。

这也是我按照测试驱动开发完成的第一个项目,整个过程有趣而有意义。先编写测试用例迫使我思考设计和架构,而不仅仅是把代码放到一起,让它们可以运行。不要误解我的意思,有很多时候,后者的方式更好。不过如果你想给确保这些不怎么维护的代码在之后的几周甚至几个月依然工作,那么测试驱动开发正是你需要的。

我研究了下整洁架构以及依赖注入模式,这些充分体现在 Router 类是如何作为一个更高层次的抽象的(实体?)。Router 类是比较接近核心的,像 http_parserApp 的内容比较边缘化,因为它们只是完成了极小的字符串和字节流、或是中层 IO 的工作。测试驱动开发(TDD)迫使我独立思考每个小部分,这使我问自己这样的问题:方法调用的组合是否易于理解?类名是否准确地反映了我正在解决的问题?我的代码中是否很容易区分出不同的抽象层?

来吧,写个小框架,真的很有趣:)


via: http://mattscodecave.com/posts/simple-python-framework-from-scratch.html

作者:Matt 译者:Cathon 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

(题图来自:es-static.us

一个月前,我们就开始了一年一度SitePoint框架人气调查。这个月结束后, 我们需要花点时间来看看结果。 共收到了7800多份问卷,远远超过我们迄今为止做的任何调查,甚至在过滤掉无效的问卷后,我们最终得到的有效参与者仍然很多。

首先第一件事情,如我们所承诺的,你可以在此 下载 详细的报告。请随便使用它 – 如果你对一些图表感兴趣,请与我们分享你的想法!可以阅读原文中的“数据”部分了解详细信息。

2015年最流行的框架

框架的赢家

要查看下面图片或全屏幕版本,只需点击它们。 或在新的标签页打开他们。

正如预期的那样,Laravel再次远远胜出。

php_framework_popularity_at_work_-_sitepoint2c_2015

php_framework_popularity_in_personal_projects_-_sitepoint2c_2015

有一些人可能会担心,部分框架的分支版本可能影响Laravel的胜出,但我们可以看到,即使合并其他的框架的各个版本,Laravel也能获胜。

下面将以表格形式来呈现数据, 只是因为我没有时间做漂亮的图表。我会尽快更新图表。

按国家统计框架

下面我们来看看所有超过50票的国家,这些都是他们最喜欢使用框架:

国家总票数工作中最爱票数个人最爱票数
United States819Laravel219Laravel293
Czech Republic770Nette611Nette639
United Kingdom496Laravel138Laravel166
Germany428Symfony276Laravel100
France343Symfony2149Symfony2136
Brazil305Laravel100Laravel111
India287Laravel62Laravel77
Ukraine263PHPixie66PHPixie67
Indonesia242CodeIgniter77Laravel64
Russian Federation235Yii 253Yii 272
Poland216Symfony252Symfony246
Netherlands209Laravel64Laravel84
Romania183Symfony249Symfony248
Canada138Laravel40Laravel52
Spain131Symfony247Symfony243
Vietnam112Laravel34Laravel43
Iran101Laravel34Laravel35
Italy100Laravel20Laravel25
Australia99Laravel30Laravel39
Slovakia94Nette48Nette47
Belgium79Laravel26Laravel31
Serbia78Laravel20Laravel29
Hungary73Laravel17Laravel19
Turkey71Laravel26Laravel28
Mexico68Laravel22Laravel21
Bulgaria66Laravel13Laravel20
Lithuania65Symfony222Laravel26
Thailand58CodeIgniter14Laravel16
Pakistan57CodeIgniter14CodeIgniter13
Philippines54Laravel15Laravel16
Argentina52Laravel16Laravel21
Bangladesh51Laravel18Laravel16
Belarus51Symfony220Symfony219
Portugal50Laravel12Laravel17

这是一个有趣的趋势观察。大多数英语国家青睐Laravel,而法国则对Symfony忠诚 – 这是它们自己的产品。有趣的是,一个令人难以置信的是大部分捷克人(在本调查中第二活跃的国家!)青睐Nette – 这个框架在西方世界知之甚少,而乌克兰最喜欢的当地框架 – PHPixie。当你观察前五名的国家会觉得更加有趣 – 不只是赢家– 你可以自己看看!

按年龄分组框架

最后,如果我们看看各年龄组的前5名框架,我们得到这样的结果:

小于18岁票数:131
工作中最爱票数个人最爱票数
PHPixie73PHPixie73
Laravel24Laravel27
Nette8Nette9
No Framework6No Framework5
CodeIgniter4Symfony24
18 – 25 岁票数:2433
工作中最爱票数个人最爱票数
Laravel604Laravel720
Nette329Nette338
PHPixie259PHPixie259
Symfony2258Symfony2255
CodeIgniter178Yii 2194
26 – 35 岁票数:3870
工作中最爱票数个人最爱票数
Laravel788Laravel1049
Symfony2636Symfony2597
CodeIgniter292Yii 2323
Nette285Nette303
Yii 2258CodeIgniter235
36 – 45 岁票数:1044
工作中最爱票数个人最爱票数
Laravel191Laravel249
Symfony2146Symfony2134
CodeIgniter91Yii 279
Zend Framework 277Zend Framework 271
Company Internal Framework73CodeIgniter68
45 岁以上票数:252
工作中最爱票数个人最爱票数
Laravel52Laravel66
CodeIgniter31No Framework29
Symfony223CodeIgniter27
No Framework21Yii 222
Yii 219Zend Framework 214

Laravel再次领先所有框架,Symfony紧随其后,除了在最小年龄组的情况中 – PHPixie 也许是由于在学校中培训的原因?结果并不出乎意料,除了最小年龄组和最老年龄组似乎并不使用框架。最明显的是CodeIgniter,即便是现在,仍保持着很强的传统势力和忠实的用户群。

有趣的是,与去年同期相比Phalcon的人气急剧下降,甚至跌出了排行榜,这也许是由于样本量大增的原因?

不幸的是,由于去年一些抱怨,我们在本次调查没有包括性别数据。这本来是一个有趣的载体。

(注:本文是节译,完整报告请查看原文)。