ionic前端与nodejs后端达成TODO实例,从输入网站到
分类:前端技术

理想的应用框架

2015/07/19 · CSS, HTML5, JavaScript · 应用框架

原文出处: 侯振宇(@侯振宇hzy)   

很多人在入门软件工程不久后,都会接触到到“全栈开发”这一词汇,而这里的栈,自然是指产品的分层系统结构。拥有全栈开发能力的开发者,我们称之为全栈工程师(Full Stack developer)。
全栈工程师是指熟悉一个软件结构的各个部分,掌握开发不同层次的技术,并有能力单独实现软件产品的人。
这么一听确实是很高大上,然而千里之行始于足下,虽然不是人人都能成为全栈开发者,但了解全栈技术、培养全栈思维,对我们今后的学习工作、个人成长都是帮助匪浅。

大学的时候,在所有不同系统的开发模式中,对我个人来说最困难的就是Web开发,现在想来可能是因为在Web开发中,默认就有多个运行时环境共同作用,才能完成Web应用的整个流程。这个过程中涉及的一些运行时视图,在当时的认知水平情况下,无法全面理解。

背景

在过去对框架的设计中,我收到过的最有用的建议是:“不要一开始就根据现有的技术去整合和改进。而是先搞清楚你觉得最理想的框架应该是怎样的,再根据现在的技术去评估,的确实现不了时再妥协。这样才能做出真正有意义的框架。”
在这篇文章里,就让我们按照这样一条建议来探索一下现在的 web 框架最终可以进化成的样子,你绝对会被惊艳到。

前端,还是从前端说起。前端目前的现状是,随着早期的 Backbone,近期的 Angular、React 等框架的兴起,前端在 模块化、组件化 两个方向上已经形成了一定的行业共识。在此基础上,React 的 FLUX、Relay 则是进一步的对前端应用架构的探索。这些技术在目前国内的大公司、大团队内部实际上都落地得非常好,因为很容易和公司内部已有的后端技术栈结合。而且这些纯前端框架的配套技术方案一般比较成熟,例如在支付宝确定使用 React,其实有一部分原因是它兼容 IE8,并且有服务器端渲染方案来加速首屏。

相比之下,像 Meteor 这类从前到后包办的框架就较难落地。虽然能极大地提高开发效率,整体架构非常先进,但架构的每一个层级往往不容易达到行业内的顶尖标准。特别是在服务器端,对大公司来说,通常都有适合自己业务的服务器集群、数据库方案,并且经受过考验。因此当一个团队一上手就要做面向十万级、甚至百万级用户的产品时,是不太愿意冒风险去尝试的。反而是个人开发者、创业型的团队会愿意去用,因为确实能在短时间内高效地开发出可用的产品出来。包括像 Leancloud 提出的这类型的服务,也是非常受欢迎的。

这种现状,就是理想和现实的一个争论。Meteor 的方式能满足我对开发效率的理想,而团队已有的技术方案能保障稳定。能否整合其中的优势,不妨让我们进一步来细化一下对框架的希望:

– 有强大的前后端一致的数据模型层
– 代码可以可以复用。例如我有一个 User 模型,当我创建一个新的 user 时,user 上的字段验证等方法是前后端通用的,由框架自动帮我区别前后端环境。
– 数据模型和前端框架没有耦合,但可以轻松结合。这样在前端渲染型的框架进一步升级时,不影响我的业务逻辑代码。
– 由数据模型层提供自动的数据更新机制。例如我在前端要获取 id 为 1 的用户,并且如果服务器端数据有更新的话,就自动帮我更新,不需要我自己去实现轮询。我希望的代码写法是:

JavaScript

var user = new User({id:1}); user.pull(); user.watch();

1
2
3
var user = new User({id:1});
user.pull();
user.watch();

实际上,Meteor已经能实现绝大部分上述功能。但这不是软文。我要强调两点我不希望的:

– 我不希望这个数据模型层去包含业务逻辑,也就是我创建的user对象,我不希望它提供 login、logout 等 api。
– 我也不希望数据模型层自动和任何ORM框架绑定,提供任何 SQL 或 NoSQL 的数据支持。

看到这两点你可能心中大打问号,这两点不正是高效的精髓吗?前后端逻辑复用,屏蔽数据库细节。别急,让我们重新用“理想的方式”来思考一下“逻辑”和“数据持久化”这两件事。

本周校内课程上,老板给介绍了全栈开发的流程,给出实例以及具体介绍。课后自己做了做,算是第一次接触到了全栈开发的流程,感慨良多,第一次有写下来分享的冲动希望大家多多包涵,多多交流

后来在实际的互联网项目中,通过LAMP、Django、nodejs和Spring MVC等一系列不同语言和框架的开发学习,从摸索到完全可以在大脑中构建清晰的运行时视图,这个时候就可以像《架构实践》中所描述的篮球教练一样,看到细节。在组建培训团队的过程中,我也发现,对于这个运行时的理解程度,几乎就决定的新人、中间水平一直到全栈工程师的表现能力,还有对架构的理解。在面试的时候也能够很好的考察开发人员的全局视野。

数据与逻辑

我们以这样一个问题开头:任何一个应用,我们的代码最少能少到什么程度?

这算半个哲学问题。任何人想一想都会得到同一个答案:最少也就少到和应用本身的描述一一对应而已了。什么是应用描述?或者说什么是应用?我们会这样描述一个博客:“用户可以登录、退出。用户登录后可以发表文章。发表文章时可以添加相应的标签。”

抽象一下描述,答案很简单:数据,和逻辑。

如果你在一个流程要求严格的公司,应用描述就是prd或系分文档。应用的数据就是数据字典,应用的逻辑就是流程图的总和:

图片 1

流程图

图片 2

那么代码最少能怎么写呢?数据很简单,参照数据字典,我们来用一种即使是产品经理都能掌握的伪码来写:

//描述字段 User : { name : string } Post : { title : string, content : text } Tag : { name : string } //描述关系 User -[created]-> Post Post -[has]-> Tag

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//描述字段
User : {
name : string
}
 
Post : {
title : string,
content : text
}
 
Tag : {
name : string
}
 
//描述关系
User -[created]-> Post
Post -[has]-> Tag

这里为了进一步帮助读者从已有的技术思维中跳出来,我想指出这段伪码和数据库字段描述有一个很大的区别,那就是:我不关心 User 和 Post 中间的关联关系到底是在两者的字段中都创建一个字段来保存对方的id,还是建立一个中间表。我只关心我描述它时的逻辑就够了。数据描述的代码,最简也就简单到这个程度了。

那么逻辑呢?我们先用按常规方式试试?

class User{ createPost( content, tags=[] ){ var post = new Post({content:content}) post.setTags( tags.map(tagName=>{ return new Tag(tagName)} ) ) return post } }

1
2
3
4
5
6
7
class User{
    createPost( content, tags=[] ){
        var post = new Post({content:content})    
        post.setTags( tags.map(tagName=>{ return new Tag(tagName)} ) )
        return post    
    }
}

好像还不错,如果今天产品经理说我们增加一个 @ 功能,如果文章里 @ 某个用户,那么我们就发个站内信给他。

class User{ createPost( content, tags=[] ){ var post = new Post({content:content}) post.setTags( tags.map(tagName=>{ return new Tag(tagName)} ) ) if( at.scan(content) ){ at.getUser(content).forEach( atUser =>{ system.mail( atUser ) }) } return post } }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class User{
    createPost( content, tags=[] ){
        var post = new Post({content:content})    
        post.setTags( tags.map(tagName=>{ return new Tag(tagName)} ) )
 
        if( at.scan(content) ){
            at.getUser(content).forEach( atUser =>{
                system.mail( atUser )
            })
        }
 
        return post    
    }
}

你应该意识到我要说什么了,像互联网这种可以快到一天一个迭代的开发速度,如果没有一个好的模式,可能用不了多久,新加的功能就把你的 createPost 搞成了800行。当然,我也并不是要讲设计模式。代码中的设计模式,完全依赖于程序员本人,我们要思考的是从框架层面提供最简单的写法。

让我们再回到哲学角度去分析一下业务逻辑。
我们所谓的逻辑,其实就是对一个 具体过程的描述 。在上面这个例子里,过程无非就是添加标签,全文扫描。描述一个过程,有两个必备点:

– 干什么
– 顺序

顺序为什么是必备的?某天上面发了文件说标题里带 XXX 的文章都不能发,于是你不得不在函数一开始时就进行检测,这时就必须指定顺序。

如果我们用左右表示会互相影响的顺序,从上下表示互不相干的顺序,把上面的最初的流程图重画一下:

图片 3

这是一棵树。如果我们再加个功能,添加的标签如果是某个热门标签,那么我们就把这篇文章放到网站的热门推荐里。这棵树会变成什么样子呢:

图片 4

是的,事实上人类思维中的任何过程,都可以画成一棵树。有条件的循环可以拆解成递归,最终也是一棵树。但重点并不是树本身,重点是上面这个例子演化的过程,从一开始最简单的需求,到加上一点新功能,再到加上一些恶心的特殊情况,这恰恰就是真实世界中 web 开发的缩影。真实世界中的变化更加频繁可怕。其中最可怕的是,很多时候我们的程序结构、用到的设计模式,都是适用于当前的业务模型的。而某天业务模型变化了,代码质量又不够好的话,就可能遇到牵一发动全身,大厦将倾的噩梦。几乎每个大公司都有一个“运行时间长,维护的工程师换了一批又一批”的项目。Amazon曾经有个工程师描述维护这种项目的感觉:“climb the shit mountain”。

回到之前的话题,在逻辑处理上,我们的理想是写出的代码即短,又具有极高的可维护性和可扩展性。

更具体一点,可维护性,就是代码和代码结构,能最大程度地反映业务逻辑。最好我的代码结构在某种程度上看来和我们流程图中的树一样。这样我读代码,就几乎能理解业务逻辑。而可扩展性,就是当出现变化时,我能在完成变化时,能尽量少地去修改之前的代码。同样的,如果我们能保障代码和代码结构能和流程图尽量一致,那么在修改时,图上怎么改,我们代码就怎么改。这也就是理论上能达到的最小修改度了。综上,我们用什么样的系统模型能把代码变得像树形结构一样?

很简单,事件系统就可以做到。我们把都一个业务逻辑当做事件来触发,而具体需要执行的操作单做监听器,那么上面的代码就可以写成:

JavaScript

// emitter 是事件中心 emitter.on("post.create", function savePost(){...}) emitter.on("post.create", function createTags(){...}, {before:"savePost"}) emitter.on("post.create", function scanSensitiveWords( post ){ if( system.scanSensitiveWords( post ) ){ return new Error("you have sensitive words in post.") } }, {block:all}) emitter.on("post.create", function scanPopTags(){...})

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// emitter 是事件中心
 
emitter.on("post.create", function savePost(){...})
 
emitter.on("post.create", function createTags(){...}, {before:"savePost"})
 
emitter.on("post.create", function scanSensitiveWords( post ){
 
    if( system.scanSensitiveWords( post ) ){
        return new Error("you have sensitive words in post.")
    }
 
}, {block:all})
 
emitter.on("post.create", function scanPopTags(){...})

JavaScript

//执行创建文章操作 emitter.fire("post.create", {...args})

1
2
//执行创建文章操作
emitter.fire("post.create", {...args})

这样看来,每个操作的代码变得职责单一,整体结构也非常工整。值得注意的是,在这段伪码里,我们用了 {before:"savePost"} 这样的参数来表示操作的顺序,看起来也和逻辑本身的描述一致。

让我们回到可维护性和可扩展性来检查这种写法。首先在可维护性上,代码职责变得很清晰,并且与流程描述一致。不过也有一个问题,就是操作的执行顺序已经无法给人宏观上的印象,必须把每个监听器的顺序参数拼起来,才能得到整体的顺序。

在可扩展性上,无路是新增还是删除操作,对应到代码上都是删除或新增相应的一段,不会影响到其他操作代码。我们甚至可以把这些代码拆分到不同的文件中,当做不同的模块。这样在增减功能时,就能通过增删文件来实现,这也为实现一个文件级的模块管理器提供了基础技术。

至此,除了无法在执行顺序上有一个宏观印象这个问题,似乎我们得到了理想的描述逻辑的方式。那我们现在来攻克这最后一个问题。拿目前的这段伪码和之前的比较,不难发现,之前代码需要被执行一遍才能较好地得到其中函数的执行顺序,才能拿到一个调用栈。而现在的这段代码,我只要实现一个简单的 emitter,将代码执行一遍,就已经能得到所有的监听器信息了。这样我就能通过简单的工具来得到这个宏观的执行顺序,甚至以图形化的方式展现出来。得到的这张图,不就是我们一模一样的流程图吗?!

不知道你有没有意识到,我们已经打开了一扇之前不能打开的门!在之前的代码中,我们是通过函数间的调用来组织逻辑的,这和我们现在的方式有一个很大的区别,那就是:用来封装业务逻辑的函数,和系统本身提供的其他函数,没有任何可以很好利用的区别,即使我们能得到函数的调用栈,这个调用栈用图形化的方式打印出来也没有意义,因为其中会参杂太多的无用函数信息,特别是当我们还用了一些第三方类库时。打印的结果可能是这样:

图片 5

而现在,我们用来表述业务的某个逻辑,就是事件。而相应的操作,就是监听器。监听器无论是触发还是注册,都是通过 emitter 提供的函数,那么我们只需要利用 emitter,就能打印出只有监听器的调用栈。而监听器的调用栈,就是我们的流程图。

图片 6

代码结构可图形化,并且是有意义的可图形化,这扇大门一旦打开,门后的财富是取之不尽的。我们从 开发、测试、监控 三个方面来看我们能从中获得什么。

在开发阶段,我们可以通过调用栈生成图,那通过图来生成代码还会难吗?对于任何一份流程图,我们都能轻易地直接生成代码。然后填空就够了。在调试时、我们可以制作工具实时地打印出调用栈,甚至可以将调用时保存的传入传出值拿出来直接查看。这样一旦出现问题,你就可以直接根据当前保存的调用栈信息排查问题,而再无需去重现它。同理,繁琐的断点,四处打印的日志都可以告别了。

测试阶段,既然能生成代码,再自动生成测试用例也非常容易。我们可以通过工具直接检测调用栈是否正确,也可以更细致地给定输入值,然后检测各个监听器的传入传出值是否正确。

同样很容想到监控,我们可以默认将调用栈的数据建构作为日志保留,再用系统的工具去扫描、对边,就能自动实现对业务逻辑本身的监控。

总结一下上述,用事件系统去描述逻辑、流程,使得我们代码结构和逻辑,能达到一个非常理想的对应程度。这个对应程度使得代码里的调用栈信息就能表述逻辑。而这个调用栈所能产生的巨大价值,一方面在于可图形化,另一方面则在于能实现测试、监控等一系列工程领域的自动化。

到这里,我们已经得到了两种理想的表达方式来分别表述数据和逻辑。下面真正激动人心的时刻到了,我们来关注现实中的技术,看是否真的能够做出一个框架,让我们能用一种革命性的方式来写应用?

项目: TODO初始功能
前端:ionic跨平台app开发
后端:node.js开发

因此,我认为,理解从输入网址,到网页完整呈现出来的过程,对于Web应用开发,是至关重要的;具备了整个图景的思考和实战能力,就可以挑战真正意义上的全栈开发。本文将会试着简单描述这个过程,并且思考一下我对全栈开发的理解。

理想到现实

首先来看数据描述语言和和数据持久化。你可能早已一眼看出 User -[create]-> Post 这样的伪码是来自图数据库 Neo4j 的查询语言 cypher 。在这里我对不熟悉的读者科普一下。Neo4j 是用 java 写的开源图数据库。图数据本身是以图的方式去存储数据。

例如同样对于 User 这样一个模型,在 关系型数据库中就是一张表,每一行是一个 user 的数据。在图数据库中就是一堆节点,每个节点是一个 user。当我们又有了 Post 这个模型时,如果要表示用户创建了 Post 这样一个关系的话,在关系型数据库里通常会建立一个中间表,存上相应 user 和 post 的 id。也或者直接在 user 或 post 表里增加一个字段,存上相应的id。不同的方案适用于不同的场景。而 在图数据库中要表达 user 和 post 的关系,就只有一种方式,那就是创建一个 user 到 post 的名为 CREATED 的 关系。这个关系还可以有属性,比如 {createdAt:2016,client:”web”} 等。

你可以看出图数据和关系型数据库在使用上最大的区别是,它让你完全根据真实的逻辑去关联两个数据。而关系型数据库则通常在使用时就已经要根据使用场景、性能等因素做出不同的选择。

我们再看查询语言,在 SQL 中,我们是以SELECT ... FROM 这样一种命令式地方式告诉数据怎样给我我要的数据。语句的内容和存数据的表结构是耦合的。例如我要找出某个 user 创建的所有 post。表结构设计得不同,那么查询语句就不同。而在 Neo4js 的查询语句 cypher 中,是以 (User) -[CREATED] ->(Post) 这样的 模式匹配 的语句来进行查询的。这意味着,只要你能以人类语言描述自己想要的数据,你就能自己翻译成 cypher 进行查询。

除此之外,图数据当然还有很多高级特性。但对开发者来说,模式匹配式的查询语句,才是真正革命性的技术。熟悉数据库的读者肯定有这样的疑问:

其实很多 ORM 就能实现 cypher 现在这样的表达形式,但在很多大公司里,你会发现研发团队仍然坚持手写 SQL 语句,而坚决不用 ORM。理由是,手写 SQL 无论在排查问题还是优化性能时,都是最快速的。特别是对于大产品来说,一个 SQL 就有可能节约或者损失巨额资产。所以宁愿用 “多人力、低效率” 去换 “性能和稳定”,也不考虑 ORM。那么 cypher 如何面对这个问题?

确实,cypher 可以在某种程度上理解成数据库自带的 ORM。它很难通过优化查询语句来提升性能,但可以通过其他方式。例如对耗时长的大查询做数据缓存。或者把存储分层,图数据库变成最底层,中间针对某些应用场景来使用其他的数据库做中间层。对有实力的团队来说,这个中间层甚至可以用类似于智能数据库的方式来对线上查询自动分析,自动实现中间层。事实上,这些中间技术早就已经成熟,结合上图数据库和cypher,是可以把传统的“人力密集型开发”转变为“技术密集型开发”的。

扯得略远了,我们重新回到模式匹配型的查询语句上,为什么说它是革命性的,因为它刚好满足了我们之前对数据描述的需求。任何一个开发者,只要把数据字典做出来。关于数据的工作就已经完成了。或者换个角度来说,在任何一个已有数据的系统中,只要我能在前端或者移动端中描述我想要的数据,就能开发出应用,不再需要写任何服务器端数据接口。Facebook 在 React Conf 上放出的前端 Relay 框架和 GraphQL 几乎就已经是这样的实现。

再来看逻辑部分,无论在浏览器端还是服务器端,用什么语言,实现一个事件系统都再简单不过。这里我们倒是可以进一步探索,除了之前所说的图形界面调试,测试、监控自动化,我们还能做什么?对前端来说,如果前后端事件系统可以直接打通,并且出错时通过图形化的调试工具能无需回滚直接排查,那就最好了。
例如:在创建 post 的前端组件中

JavaScript

//触发前端的 post.create 事件 var post = {title: "test", content: "test"} emitter.fire("post.create").then(function(){ alert("创建成功") }).catch(function(){ alert("创建失败") })

1
2
3
4
5
6
7
//触发前端的 post.create 事件
var post = {title: "test", content: "test"}
emitter.fire("post.create").then(function(){
    alert("创建成功")
}).catch(function(){
    alert("创建失败")
})

在处理逻辑的文件中:

JavaScript

//可以增加前端专属的逻辑 emitter.on("post.create", function checkTest(post){ if( post.title === "test"){ console.log("this is a test blog.") } }) //通过 server: 这样的命名空间来触发服务器端的事件 emitter.on("post.create", function communicateWithServer(post){ console.log("communicating with server") return emitter.fire("server:post.create", post) })

1
2
3
4
5
6
7
8
9
10
11
12
//可以增加前端专属的逻辑
emitter.on("post.create", function checkTest(post){
    if( post.title === "test"){
        console.log("this is a test blog.")
    }
})
 
//通过 server: 这样的命名空间来触发服务器端的事件
emitter.on("post.create", function communicateWithServer(post){
    console.log("communicating with server")
    return emitter.fire("server:post.create", post)
})

得到的事件栈

图片 7

在浏览器端可以打通和服务器端的事件系统,那么在服务器端呢?刚刚提到我们我们其实可以用任何自己熟悉的语言去实现事件系统,那是不是也意味着,只要事件调用栈的数据格式一致,我们就可以做一个跨语言的架构?

例如我们可以用nodejs的web框架作为服务器端入口,然后用python,用go去写子系统。只要约定好系统间通信机制,以及事件调用栈的数据格式,那么就能实现跨语言的事件系统融合。这意味你未来看到的调用栈图可能是:

图片 8

跨语言的实现,本身也是一笔巨大财富。例如当我们未来想要找人一起协同完成某一个web应用时,再也不必局限于某一种语言的实现。甚至利用docker等容器技术,执行环境也不再是限制。再例如,当系统负载增大,逐渐出现瓶颈时。我们可以轻松地使用更高效的语言或者执行环境去替换掉某个业务逻辑的监听器实现。

更多的例子,举再多也举不完。当你真正自己想清楚这套架构之后,你会发现未来已经在你眼前。

到这里,对“理想”的想象和对实现技术的思考终于可以划上句号了。对熟悉架构的人来说,其实已经圆满了。但我也不想放弃来“求干货”的观众们。下面演示的,就是在框架原型下开发的简单应用。这是一个多人的todo应用。

图片 9

前端基于react,后端基于koa。

目录结构

图片 10

前端数据(todo 列表) /public/data/todos.js

图片 11

前端逻辑(todo 基本逻辑) /public/events/todo.js

图片 12  

前端逻辑(输入@时展示用户列表) /public/events/mention.js
图片 13

后端逻辑(通知被@用户) /modules/mention.js

图片 14

通过调试工具得到的创建时的调用栈和输入@符号时的调用栈

图片 15

这只是一个引子,目的是为了让你宏观的感受将应用拆解为“数据 逻辑”以后能有多简单。目前这套框架已完成 50% ,实现了数据部分的设计、前后端事件融合,还有跨语言等方案正在开发中。未来将开源,期待读者关注。

TODO实例

运行时组件

首先我会从运行时的视角开始,在整个过程中从多个粒度,解析所有发生的事情的运行时。

最大粒度的划分,由两个运行时,本地的浏览器和云端的服务器组成;两个实体逻辑上是运行在不同机器上的完全隔离的进程里,因此,也可以把http协议理解为一种进程间通信。

在浏览器这个运行时里,至少需要三个必要的组件,才能完成工作,分别是进行http远程通讯的http客户端、javascript脚本解释器还有html css样式的渲染;简单的来看,就是这三个组件,加上浏览器的运行时控制器,组成了完整的Web浏览器。

在云端,也有必要的细分,可能物理上不一定完全分离,但是逻辑上必须由对应的单元组成。http服务器负责处理http协议的通讯,以文本或文件形式将url指向的资源返回给客户端;应用服务器执行特定语言的处理程序,进行计算;数据管理软件,负责提供和存储数据。根据不同架构和应用场景,还可以有很多细分的组件,通过各自的接口相互协作,完成数据计算和资源处理,对资源进行处理,并且返回给请求的客户端。

基本的运行时组件:

  • javascript解释器,如Google V8
  • html和css渲染
  • http客户端
  • http服务器,如Apache httpd、tomcat和nginx
  • 应用容器,如Tomcat、PHP解释器和Python运行时
  • 数据库,例如:MySQL、MongoDB、Redis等
  • 其他各种中间件,Kafka、ElasticSearch、ZooKeeper等

上面这些运行时环境,参与并实现了从浏览器请求一直到完整网页信息和交互效果的呈现。

简单过程描述

后记

终于写完了。框架只是架构的实现。这套架构几乎孕育了近两年,这其中已经开发出一款实现了部分功能,基于nodejs的服务器端原型框架。完整的框架开发目前也已经四个月了。虽然从它落地的这些前端技术、数据技术看起来,它其实是有技术基础的,应该是积累的产物。但实际上,最早的关于数据和逻辑的思路,却是在我读研时对一个“很虚”的问题的思考:什么样的系统是最灵活的系统?在很长一段时间内,对各种架构的学习中我都没有找到想要的答案,直到后来在学认知心理学和神经学的时候,我想到了人。人是目前可以理解的最具备适应性,最灵活的系统。人是怎么运作的?生理基础是什么?

认知心理学里提到曾经有一个学派认为人的任何行为都不过是对某种刺激的反射,这种刺激可以是来自内部也可以是外部。来自内部的刺激有两个重要来源,一是生理上,例如饥饿,疲惫。二则是记忆。例如,你每天起床要去工作,是因为你的过去的记忆告诉你你需要钱,或者你喜欢工作的内容。这对人来说也是一种刺激,所以你产生了去工作的动机。外部刺激就更简单,例如生理上的被火烫了,心理上被嘲讽、被表扬等等。而人的反应,就是对这些刺激而产生的多种反射的集合。例如早上起床,你的一部分反射是产生上班的动机,但是如果你生病了,你的身体和记忆就会刺激你去休息。最终你会在这两种刺激下达到一个平衡,做出反应。值得注意的是,大部分时候,人在不同时间面临相同的刺激,却做出不同的反应。并不是因为后来某些反射被删除了,而是因为后来形成了更强的反射区压制住了之前的反射。它的生理基础就是神经学中的神经递质可以互相压制。

如果我们把要打造的系统看做一个有机体,把迭代看做生长,把用户的使用看做不断的刺激。那我们是不是就能模拟人的反射过程来打造系统,从而期待系统得到像人一样的适应力?而恰恰你会发现科幻作品中的人工智能产品通常都以人的形态出现。因为我们希望我们所使用的产品,就像人一样通情达理,具有人一样的领悟能力。而要达到这样的效果,或许就是不断给给他添加人对刺激的反射规则。

思考到这一步的时候,我对应用架构的设计哲学已经基本定型。后来验证出来的,这样的系统能够极大地提高研发效率,都只是这段哲学的附加价值。其实提高研发效率的原理很简单,无论系统的需求再怎么扩展、再怎么变更,它也是遵循人本身的思维逻辑的。因此,你始终可以使用本身就模拟人类认知的系统去适应它。并且,它怎么变化,你就怎么变化。

架构这种东西,最终仍然关注在使用者身上的。所以与其和我讨论确定的技术问题,不如讨论这些更有意义。对思考架构的人来说,我认为眼界和哲学高度,最重要。

 


基于LAMP的简单描述

  1. 用户在浏览器中输入一个链接地址,
  2. 浏览器通过DNS服务,找到url指向的服务器。
  3. 浏览器通过TCP协议,向Apache Httpd服务器请求资源。
  4. Apache Httpd服务器根据配置,决定如何处理资源;并且处理请求及响应的header和body。
  5. 直接返回静态资源,如果是配置了其他应用服务器或者网关,那么apache将请求转发给网关或应用容器,例如Tomcat或者调用php解释器,在应用容器返回处理的结果之后,返回给客户端。
  6. php运行时或者Tomcat分析路由和请求头部,执行相应的服务端代码,调用服务器配置的资源,根据请求,进行计算。
  7. php程序调用MySQL数据库,通过mysql协议获取程序对应的数据。

讨论记录

尤小右:感觉其实就是 flux 啊,但是 string-based global event bus 规模大了还是会有点坑爹的。一个事件触发的后果遍及全栈,不好 track。

答:和flux的区别在于flux的数据对象本身和对数据的操作是合在store里的。事件系统规模的问题通过两个方式控制:一是命名空间。二是事件只应用在业务逻辑个程度就够了,像“存入数据库”这种操作就不要再用事件触发。这样系统就不会乱掉,因为它只反映业务逻辑。

玉伯也叫黑侠:认识心理学那段很有趣。很关注如何让业务代码随着时间流逝不会腐化而会趋良?比如事件fire点,怎么才能可控又够用,而不会随着业务复杂而爆发式增长?(简单如seajs, 随着插件的多样化事件点都经常不够用)。还有如何让事件间彼此解耦?经常一个需求要添加多个监听,做得不好还可能影响其他功能点。

答:用事件去反映业务逻辑,而不是技术实现的逻辑”不只是这套架构对于防止事件滥用的一个建议,更是它的哲学理论的重要部分。遵守它,这套框架就能把高可扩展性和高可维护性发挥到极致。我们用一个常见的例子来说明这一点。有时候面临需求变更,我们会觉得难搞,会对产品经理说:“你这个变更影响很大,因为我的代码中xxx不是这样设计的”。而产品经理有可能不理解,因为对他来说,变更的需求可能只是一个很简单的逻辑,加上一点特殊情况而已。产生这种矛盾的关键就在于,没有找到一种能准确描述业务逻辑的方式去组织代码。如果组织代码的方式和描述业务逻辑的方式一致,那么业务逻辑上觉得改动点很简单,代码上就也会很简单。这套架构中的事件系统、包括事件拥有的顺序控制等特性,都是为了提供一种尽可能合适的方式去描述业务逻辑。只有这样,才能实现代码最少、最可读、最可扩展。它本身是为描述业务逻辑而不是技术实现逻辑而生。所以只有遵守这个规则,才能得到它带来的财富。

玉伯也叫黑侠:嗯,看明白了。感觉是将代码阶段的复杂性,前移到了业务系分阶段,如果系分阶段做得好,那么代码就会很优雅。反之,则很难说。进一步提一个无耻要求:怎么保证系分阶段的良好性呢?不少时候,写代码的过程,就是梳理业务逻辑的过程,写完后,才明白某个需求真正该怎么实现。

答:不太认同写代码的过程是梳理业务逻辑的过程。可以说写代码的过程是梳理具体技术实现的过程。如果一开始写代码的人连业务逻辑都不清楚,再好的技术和框架也无法防止他写出烂代码。基于事件的架构其实不是对系分的要求提高了,反而是降低了。因为只要求你理清楚逻辑,具体的实现写得再烂,之后都可以依赖事件系统架构本身的灵活性去完善的。就例如“发表文章后给所有被@的人发站内信”这样的逻辑,你可能一开始没有考虑发站内信的时候最好用个队列,防止请求被卡住。但只要你做到了最基础的把“发送站内”这个监听器注册到“发表文章”的事件上。未来就能在不影响任何其他代码的情况下去优化。实际上没有任何框架能帮你写好代码,即使DDD社区也是强调不断重构,只可能“降低让你写好代码的门槛”。这套架构就是屏蔽很多技术上的概念,用事件的方式让你只关注逻辑。

玉伯也叫黑侠:有没有一种让代码趋良的架构?可能刚开始写得乱糟糟,但随着做的需求越多,写的代码越多,整体可维护性反而会变得越好?比如前后端分层,让后端专注业务模型,一般来说,业务模型会逐步趋于完善和稳定,前端代码也会逐步变好。用一些约束,推动代码的良性循环。这些约束,是否就是理想应用架构的精髓?这些约束是什么?可能是某种要求比如测试覆盖率,也可能是某种强制约束比如必须通过数据改动来更新界面。roof的约束是用事件去反映业务逻辑,但这个约束更多是「道德」层面,而不是「法律」,比如如何防止「大事件」(一个事件里,一坨技术实现的逻辑代码)?如何让人羞于去写出糟糕的代码?

答:即使前后端分离,业务模型趋于稳定,也是靠开发者自身不断重构去实现的,要不然怎么会“趋于”稳定呢。架构只可能让人站到更好地平台上,用更好地方式去写好代码,不可能主动帮人把代码变好。文中架构就是通过屏蔽技术细节,让你关注业务逻辑的方式,让代码易理解,也让你能不影响业务地去升级技术。这套架构因为有一个清晰的事件调用栈数据结构,所以能很容易地做出相应的测试、监控工具保障代码质量。但要实现“法律”是不可能的。即使是Java、即使是领域驱动编程,也可以在它好的架构下写出各种糟糕的代码。毕竟编程仍然是一件需要创造力的工作。这就像硬币的两面,如果要实现法律,那工作本身必须是无需创造,完全可以按照流程由机器人生产。如果要创造力,就必然会有因人而异的品质差异。

1 赞 3 收藏 评论

图片 16

前端部分

云端服务器

服务器存储了根据系统设计需要提供的各种资源,包括核心的MVC处理代码,这里MVC和分层架构是一种入门级的最佳实践,可以应付大多数的Web场景需要。整个MVC和三层架构,都是通过代码实现的一种逻辑结构,运行在PHP解释器或者其他语言的对应运行时中;理论上说,每个web应用,通过MVC就可以实现业务逻辑,其他的所有资源,都可以通过MVC代码的调用实现。

除了核心的业务逻辑以外,根据模块化,设计的需要,或者现实的限制,单个Web应用可能并不拥有所需的所有资源,就需要通过调用各种本地或者远程的资源来获取自身没有的数据。一般来说,服务端的语言运行环境,通过驱动可以做物理机能做的任何事情。外部资源无论数据库还是消息队列,rpc都是基于互联网通讯的,通过tcp或者http,以自己设计的一套协议提供对外服务。在mvc执行的运行时,可以自己实现对应服务的协议来调用远程资源,但是更一般的情况,我们直接调用远程资源在对应语言下提供的API来完成资源获取,这些API和我们的调用程序会实现某种设计模式,使应用和API以更有逻辑完整性的方式整合起来。

而浏览器端的主角javascript,除了nodejs之外,在服务端看来,跟另外两个主角html、css一样,从html诞生之初,就与普通文本无异,是一种超文本。虽然通过语法和语义需要,服务端也可以实现处理文本的API,更优雅的生成最终的三大主角,例如freemarker对应html,或者grunt、gulp的uglify、less等构建过程放在服务器端处理。

跨平台开发技术:

编写一份代码,自动生成iOS、Android等相应平台的应用

**Cordova **

  • 提供一组设备相关的API,移动应用通过对应的API访问不同的平台
  • 提供了一组统一的JavaScript类库,以及为这些类库所用的设备相关的原生后台代码
  • 免费开源

由于是混合手机应用开发,我们选用 ionic 框架。ionic专注于用WEB开发技术,优化html、css和js的性能,构建高效的应用程序,有以下特点:

  • 基于angular,绑定sass,提供很多手机应用UI组件
  • 使用cordova生成相应平台的应用
  • 强大的命令行工具
  • 简单易学(最重要,适合上手)

再简单点说,只要会写前端,就能够开发手机应用


开始创建
# npm install -g ionic cordova

用cordova对ionic的底层进行打包创建。
npm:Javascript的包管理工具,若尚未使用过,请自主安装一下
# ionic start todo blank
自动生成名为todo的空项目
# ionic serve

查看运行结果,该命令应该在todo目录下运行

todo初始项目

打包运行
# ionic platform add android/ios
在ionic平台安装SDK,android/ios任选
# ionic build android/ios
build项目
# ionic emulate android/ios
在SDK上模拟真机运行
# ionic serve —lab
在web浏览器中模拟两种机型

想要了解更多有关ionic的操作,请大家移步ionic官方文档自由学习哦~
由于本机是Mac OS 10.12系统,自带xcode以及iOS的Simulator,下面的操作均为mac os系统上的ios模拟。想要在Android上面尝试的小伙伴,可以下载 :Android SDK

功能实现

  • 列表显示tasks
  • 添加输入文本框和按钮来添加task
  • 给task添加checkbox,选中状态下的task,文本有划线

前端效果图

具体实现:Angular.js
打开www文件目录下的index.html文件,由于我们使用ionic自动创建项目,只需要在<body>部分实现功能主体即可.

1.列表显示task:

<body ng-app="starter" ng-controller="starterControl">
    <ion-pane>
      <ion-header-bar class="bar-stable">
        <h1 class="title">Ionic Blank Starter</h1>
      </ion-header-bar>
      <ion-content>
        <ion-list>
         <ion-checkbox  ng-checked="task.checked" ng-repeat="task in tasks" >
              {{task.title}}
          </ion-checkbox>
        </ion-list>
      </ion-content>
    </ion-pane>
  </body>

给名为”starter”的app绑定一个控制器,我们将通过controller控制app的view。在content部分创建一个list,list中的UI组件选择checkbox,通过angular中的数据绑定机制,每一个tasks数组中的task,都会创建一个checkbox对象,并用task的title属性进行初始化。

在/www/js目录下的app.js文件中,通过controller给html文件传递数据,即tasks数据集

.controller('starterControl', function($scope) {
  $scope.tasks = [
    {title: 'demo1', checked: false},
    {title: 'demo2', checked: true},
    {title: 'demo3', checked: false}
  ]
})

接下来,就可以运行程序看看效果

checkbox_list

OK,没有问题。

2.添加输入文本框和按钮来添加task:

先在view中添加相应的UI控件,代码如下:

 <form ng-submit="createTask(task)">
       <label class="item item-input">
            <input type="text" placeholder="What do you need to do?" ng-model="task.title">
            <button type="submit" class="button button-small button-positive">Create Task</button>
       </label>
 </form>

添加一个label,类型为输入item,label中添加输入框和创建按钮。将该组件通过 ng-submit 与 ** createTask(task)** 事件绑定,当点击button时,调用controller中的该事件,并传递 task.title 这个参数。

controller中的事件响应:

$scope.createTask = function (task) {
    $scope.tasks.push({
      title: task.title,
      checked:false
    })

3.选中状态下的task,文本有划线:

在ion-checkbox标签中添加如下属性,表示checkbox被点击时调用change(task)事件,ng-class在task.checked为true时,切换为'checkbox-true'

ng-click="change(task)" ng-class="{'checkbox-true':task.checked}"

在/www/css目录下的style.css文件中,添加自定义的class:checkbox-true

.checkbox-true {
  text-decoration: line-through;
}

同样,在controller中定义change事件,即点击checkbox时改变checked属性的值

 $scope.change=function(task) {
    task.checked = !task.checked;
  }

再次运行程序,即可呈现上图的TODO实例,我们的前端搭建就完成啦~
我们已经完成了TODO的基本功能,接下来,我们就应该才考虑如何持久化我们的数据
答案很简单:与服务器通信,数据存储


浏览器客户端

用户在浏览器里看到的一切,从输入url获取对应的html文本开始,在html解析阶段,script、link标签所指向的资源,会发起新的http请求来获取,这个跟img加载图片是一样的。css样式的解析也是并行发生的,从link到style各个优先级的css将会应用到浏览器建立的dom结构中,用户自定义的css样式加上浏览器为每种标准html标签内置的style结合起来,就是用户看到的页面效果。script标签加载执行javascript代码也是同步开始的,尤其是现代前端,单页面web时代,javascript扮演了更加重要的作用,包括在前端更像一个完整的客户端应用,前端通过mvc、mvvm、amd等技术具备了完整端系统的能力。通过浏览器的ajax支持,websocket实现异步通讯,就能够实现纯粹前后端分离的应用。

这样我们就可以去试着理解各种web应用如何构建,并且自己亲自权衡取舍,设计构建一个合适的web应用。

后端部分

对开发的意义

web全栈开发,对于我自己而言,我觉得来自于达芬奇更早的维特鲁威,在2000多年前的古罗马时代对建筑师所需要具备的能力和素质的描述,要构建永恒之道的系统,理所当然需要理解整个系统如何构建。

从系统架构的角度,软件系统,本身就是复杂的有机系统,任何构建的系统,应该是可以被一个人能够清晰完整的理解,在大脑中建立模型的。否则就需要将系统的复杂性分散到不同的子系统,对于一个逻辑完整边界清晰的系统应该能够理解。

从开发角度,在精益mvp模式开发中,团队总是处在能力不足的困扰之下,围绕系统的薄弱环节,应该有人既能看到整体,又有能力实现具体的细节。

1.服务器
  • nodejs编写后端:使用express框架,方便进行快速开发
  • 通过实现RESTful的接口存取数据:实现对应的Post、Get请求
  • postman查看
2.数据存储
mongodb
  • 具有传统关系型数据库的CURD操作
  • json格式进行数据存取,更符合前端数据
如何通过代码进行数据存取
  • 搭建nodejs服务器,与mongodb链接,通过mongoose实现方便的数据存取
    mongoose是什么?nodejs环境下对mongodb数据库操作的封装。

Express框架搭建
>$npm install express --save
安装完成后,新建express初始化的项目:backend
# express -e backend
如下图所示,则完成项目的建立

backend


mongoose进行数据存取
准备工作:mongodb的安装,mongoose的安装
在backend目录下,输入如下命令导入mongoose库
#npm install express jade mongoose -g
安装完成后,我们通过mongoose进行数据库操作:

1.建立数据模型Task
在项目根目录中新建models文件夹,在文件夹中新建task.js文件,并进行如下的编码

var mongoose = require('mongoose');

//申明一个mongoons对象
var TaskSchema = new mongoose.Schema({
    title: String,
    checked:Boolean,
    create_at: {
        type: Date,
        default: Date.now
    },
    updateAt: {
        type: Date,
        default: Date.now()
    }
});

//暴露出的方法
module.exports = mongoose.model('Task', TaskSchema);

TaskSchema作为数据模型,通过Task这一方法名,可被其他module获取、调用。

2.连接服务器与mongodb数据库

服务器与数据库连接涉及到跨域请求,express框架为解决跨域请求,提供了非常方便的库cors,我们首先在backend中安装cors.

npm install cors

在app.js文件中,我们首先调用mongoose与cors库

var mongoose = require('mongoose');
var cors = require('cors');

调用后,进行数据库连接

mongoose.connect('mongodb://localhost/task', function(err) {
    if (err) {
        console.log('connection error', err);
    } else {
        console.log('mongodb connection successful');
    }
});

我们连接的数据库为task
此时,我们在backend目录下,输入命令

npm start

成功连接

这样,我们已经成功连接了服务器与数据库~ 至于更多有关mongoose的操作,有兴趣的小伙伴自行阅读相关文档哦


RESTful接口实现数据存取
Router定义
我们看到backend项目目录下的routes文件夹,这是express框架用来保存相关通信方法的文件。创建文件 task.js,update.js,如下为task.js的实例

var express = require('express');
var router = express.Router();
var Task = require('../models/task');


router.post("/", function(req, res, next){
    var task = req.body;
    Task.create(task, function (err, task) {
        if (err) {
            return res.status(400).send("err in post /task");
        } else {
            return res.status(200).json(task);
        }
    });
});


router.get("/", function(req, res, next){
    Task.find({}, function(err, tasks){
        if(err){
            return res.status(400).send("err in get /task");
        }else{
            console.log(tasks);
            return res.status(200).json(tasks);
        }
    })
});

module.exports = router;

定义router获取express框架内的Router()方法库
定义Task获取我们之前建立的Task数据模型
post方法:获取请求中的body部分,由此新建task,添加到数据库中,数据库存储数据成功则返回数据,否则便报错
get方法:遍历数据库,取出数据存入tasks中并返回

数据交互方式定义OK后,我们在app.js中保存这些模块的路径

app.use('/task', task);
app.use('/update',myupdate);

当然,不要忘记使用跨域请求库

app.use(cors())

完成到现在,我们的服务器与数据库通信告一段落。在前后端交互之前,让我们先检验一下后端的功能是否实现。
这里,我们使用 postman 来进行调试:

Post功能

如图,我们对我们express服务器的http发出post请求,在body部分输入json格式的task对象,进行请求。我们得到了正确的返回值,说明该post功能的数据存取已经实现~

后端功能也实现啦~~~~!现在只需要交互前后端,进行通信,我们的todo实例就能完整搭建


前后端链接

在前端获取数据
  • angular通过http发送ajax请求
  • 使用post方法创建task,get方法获取所有的task
  • 分别对应后端的路由
后端路由接受请求,返回数据

前端获取数据
angular发送http请求:factory工厂
如果熟悉spring的朋友,应该对这个模式很熟悉。简单的说,就是angular把json格式的对象作为参数,封装到http请求中,再通过一定的方法,向对应的http请求的过程。
在app.js中进行如下编码:

.factory('Tasks', function($http) {
    var base = "http://localhost:3000";
    return {
      all: function() {
        return $http.get(base   "/task");
      },
      save: function(task) {
        return $http.post(base   "/task", {title: task.title, checked:task.checked});
      },
      update: function(task) {
        return $http.post(base   "/update", {title: task.title, checked:task.checked});
      }
    }
  })

创建一个名称为'Tasks'的工厂,对应的http目标为我们的express服务器的地址 http://localhost:3000
工厂中的三个方法all,save,update分别对应获取所有tasks,保存新建的task,改变checkbox状态并返回新状态三个http请求。其中后两个操作涉及具体的对象,所以请求中带有相应的对象参数。

相应的实现all、save、update函数,save请求在createTask函数中,update请求相应的在change函数中:

Tasks.all()
      .success(function(tasks){
        $scope.tasks = tasks;
      })
      .error(function(){
        $scope.tasks = [];
      })

 Tasks.save(task)
        .success(function(task){
          console.log(task);
        })
        .error(function(){
          console.log("request error");
        });
    }

Tasks.update(task)
        .success(function(task){
          console.log(task);
        })
        .error(function(){
          console.log("request error");
        });

就这样,我们实现了从前端操作,生成http请求,服务器处理请求,与数据库交互,数据返回前端,改变view的完整过程。

就这样,我们的tode实例大功告成啦~


总结

第一次在简书上写文章,有些忐忑,有些疲惫,最终还是敲完了这些字贴完了这些图,很高兴能记录下自己觉得有意义的东西,并分享出来。

这个小小的项目,给我麻雀虽小五脏俱全之感,让我熟悉了全栈开发的流程,也解触了很多实际开发中正热的技术,拓宽了视野,收获良多。

不由感慨,c老板是真的厉害,课程设计的这么独到~~

本文仅供学习分享,转载请注明出处,严禁商业用途
项目源码请前往我的Github:https://github.com/NJUcong/ 如果喜欢请star~
我的微博:http://www.weibo.com/5401055058/profile?rightmod=1&wvr=6&mod=personinfo

本文由pc28.am发布于前端技术,转载请注明出处:ionic前端与nodejs后端达成TODO实例,从输入网站到

上一篇:疗养 CSS 的艺术 下一篇:没有了
猜你喜欢
热门排行
精彩图文
  • 疗养 CSS 的艺术
    疗养 CSS 的艺术
    调试 CSS 的方法 2016/09/06 · CSS ·调试 原稿出处: BenFrain   译文出处:众成翻译 -yanni4night    本人经验过不菲 CSS代码的调解专门的学业,有人家写的也是
  • 何以设置链接样式,CSS结商谈层叠
    何以设置链接样式,CSS结商谈层叠
    如何设置链接样式 2016/09/05 · CSS 本文作者: 伯乐在线 -赖祥燃。未经作者许可,禁止转载! 欢迎加入伯乐在线 专栏作者。 每个合法的文档都会生成一个文
  • CSS图像替换,文本缩进
    CSS图像替换,文本缩进
    CSS图像替换:文本缩进,负边距以及更多方法 2016/07/04 · CSS ·图像替换 原文出处: BaljeetRathi   译文出处:众成翻译    CSS中的图像替换有着广泛而多样
  • 用法教程
    用法教程
    CSS Modules 用法教程 2016/06/19 · CSS ·Modules 初藳出处:阮一峰    学过网页开拓就能知晓,CSS 无法算编程语言,只是网页样式的一种描述方法。 为了让 CSS也
  • 最基本的多少个概念,面试希图之CSS
    最基本的多少个概念,面试希图之CSS
    CSS 最核心的几个概念 2015/08/26 · CSS · 5评论 ·CSS 原文出处:GeekPlux    本文将讲述 CSS中最核心的几个概念,包括:盒模型、position、float等。这些是 CSS的基