本文首发CSDN,如需转载请与CSDN联系。

作为一名产品经理,首先要知道产品对于所属公司来说意味着什么,要探寻这个问题,我们又得知道和公司息息相关的是什么,在我的理解来看,与公司状况相关的因素有以下这些:

  • 市场份额
  • 平均订单金额
  • 盈利能力
  • 资产回报率
  • 从订单转化为现金的周期
  • 应收账款
  • 借贷成本

从这些因素体现出来的最直接的就是公司收入,公司的财务状况,进而可以得出公司的经营状况,如果这些指标一塌糊涂,那么这个公司离倒闭也就不远了。那么现在我们再来看产品对公司意味着什么,应该不难发现上面这些指标都离不开产品,产品的市场份额、产品的平均订单金额、产品的盈利能力、产品的回报率、产品订单转化为现金的周期、产品的应收账款、投入产品时的借贷成本。好的产品就能创造出好的这些指标,反之亦然,所以往大了说,产品经理在某种程度上对公司的生存有着一定决定因素,尤其规模不是很大的公司。那么作为产品管理者,如何能帮公司打造出好的产品呢?

武装自己

首先我们需要能让我们打造出好产品的方法论来武装自己,所谓方法论,就是一门学问所采用的方法、规则与公理,放在软件工程中,它便指一系列编撰好的建议方法、训练方法及材料、使用的各种工具。在当今的IT领域,这种方法论莫过于DevOps了,我用它武装为自己的铠甲。

我不打算就DevOps做概念上的解释,我要说的是为什么我要选择DevOps,它能给公司和产品带来哪些好处,概括来说有以下五点:

  • 它能使产品更快的投入市场。
  • 它能提升客户满意度。
  • 它能提升产品的市场份额。
  • 它能提升员工生产力及工作成就感和幸福感。
  • 它能给公司在市场中创造出巨大的竞争优势。

打造利剑

产品的产出过程就是开发过程,在开发方法上我选择敏捷开发作为我的利剑,虽然这不是什么新鲜东西,但是它却是经过长期千锤百炼,经得起考验的开发方法,就像使用千锻、万锻后的精钢打造的利剑一般,首先在材质上就不会轻易损坏,只是你能否耍好剑的问题。

从理论上来说,敏捷开发在当今湍流的IT领域中好处不言而喻,积极甚至激进的个体互动、时刻有可交付的成果、紧密的客户合作、快速的响应变化都完胜传统瀑布式开发那套冗长的过程、冗余的事无巨细的文档、漫长的合同谈判和循规蹈矩的拖沓计划。

有了利剑,我门要学习剑术,敏捷开发有若干方法可供我们使用,比如Scrum、特性驱动开发(FDD)、测试驱动该开发(TDD)、行为驱动开发(BDD)、精益开发等,但是敏捷开发不存在官方的方法,没有完整的方法列表,也不存在最好的方法一说,只有最合适的方法。我选择了Scrum,理由很简单,它同样经历了多年的历练,已去其糟粕。

个体互动

我们先来看看个体互动,首先要明确的是你的组员,不论是开发人员、测试人员还是运维人员,他们绝对不是你的工具,不是你的枪,更不是你枪里的子弹,他们是你的伙伴,一起战斗的伙伴,只是分工不同而已。所以我们要了解他们,和他们建立互信互助的关系,建立团队沟通习惯,围绕斗志高昂的团队成员开展工作,这也是敏捷开发的原则和成功要素之一。那么我们该如何做到这些呢?这就需要使用Scrum剑术中的这几个技能:

  • 每日立会:每天不论早晚,抽15分钟时间,团队每人都要发言,汇报工作完成情况以及遇到的问题,让大家都彼此了解对方的工作内容和进度。
  • 每周代码评审会:团队成员之间互相对代码质量进行评审,提出意见和建议。但是要确保的是每个人都要放下自尊,虚心接受和诚恳的评价。
  • 每月畅谈会:团队成员之间勇于互相展现出自己脆弱的一面,可促使团队达成互相信任。每人发言,说说自己遇到的最困难的事,或觉得自己做的比较差劲的事,虽然这种方式一开始会有点残忍,但一旦团队成员彼此之间成为了倾诉对象,那么团队的团结互信将达到另一个境界。
  • 每月回顾会议:也称为总结会议,每月进行总结并讨论在各方面需要改进的地方。
  • 不定期技术交流会:我们鼓励团队中的每个人都向着专家的目标去努力,尽可能去专精自己擅长的技术,有一定积累后,无私的与大家分享交流,我很乐于看到团队成员彼此都称为对方老师。这有利于促进团队整体实力的提升。

时刻有可交付的成果

何为可交付的成果,这里的成果指的不仅是产品,每一个开发人员完成的功能模块甚至与一个功能都算是是成果。那么可交付的成果既对功能模块有用的功能、对产品有用的功能模块、对客户有用的产品。那么所谓有用又如何定义呢,它在这里指的不仅是代码逻辑无误并测试通过这么简单,而是让客户买账。那么我们要如何从源头就做到有用呢?这里需要用到Scrum剑术中的几个技能以及另外一个剑术,我们先来看看这个剑术。

区分“相关”与“无关”的工作

这个剑术用一句话来概括就是弄清楚与实现公司目标息息相关的是什么。具体的技能以下三个:

  • 把公司上层的评估指标作为前提条件与具体的业务部门和开发部门的任务关联起来。只要能说明IT风险会对业务绩效指标产生多大的影响,就能着手指定更好的业务决策。
  • 与各指标对应的业务流程负责人进行访谈,理解客户的需求与期望、产品系列、上市时间、销售渠道等分出项目优先级。
  • 冻结低优先级的产品,保留高优先级的产品,形成相对单一的工作流。

从这个剑术可以看出,它不仅适用于产品层面的管理,也适用于公司层面的运营。但不管是作为产品经理还是作为公司运营者,我们都得有一个列表,那就是公司的远期目标,也就是公司上层的评估指标:

  • 我们要创建什么?
  • 我们有正确的产品吗?
  • 我们能有效的创建产品吗?
  • 我们能尽快把产品推向市场占有一席之地吗?
  • 我的产品能带来感兴趣的潜在客户吗?
  • 我们遵守了对客户的承诺吗?
  • 我们是在获得客户还是在流失客户?
  • 我们的销售预测准确率靠谱吗?

与此对应的是:

  • 了解客户的需求和期望。
  • 根据市场和客户确定产品系列。
  • 提高研发效能。
  • 缩短产品交付周期。
  • 研究销售时机和销售渠道。
  • 保证按时交货。
  • 提高客户留存率。
  • 保证市场和销售报告数据的精准性。

以上八点其实是环环相扣的。当把这些问题都搞清楚后,自然而然就可以区分出哪些工作是“相关”的,哪些是“无关”的。

细分任务

当我们确定了一堆相关的工作后,需要使用Scrum中的另外几个技能将这些工作根据优先级进一步的细分:

  • 确定产品需求列表(Product Backlog)。
  • 开发团队根据产品需求列表作出整体工作量的预估。
  • 通过迭代计划会议(Sprint Planning Meeting)根据优先级及资源情况从产品需求列表中筛选出用户故事(User Story),作为本次迭代要完成的目标,一般周期在1-4个星期内。
  • 将用户故事再进行细化,形成迭代需求列表(Sprint Backlog),通过看板将其可视化。

我们通过确定出的“相关”工作,根据优先级进一步确定产品需求列表,这要注意的是,现在的产品需求列表中的内容已经是和公司的评估指标相关链的任务,所以都是极有价值的,现将这个需求列表评估出整体的大致工作量,然后通过迭代计划会议从中筛选出用户故事,也就是确定团队的短期目标,最后再将用户故事细化为更小的简单任务,一般周期保证在2天以内,分配给每个团队成员,在必要的时候还可以使用计划纸牌工具进行周期确认。Scrum中的这4个技能干的事就是能让整个团队清楚我们的最终目标和每一个短期目标,以及对整个目标的时间把控,在不断分解的过程中消除团队对庞大目标的恐惧感,并建立信心。

计划纸牌工具的作用是确认探讨最小任务的具体周期。比如A程序员开发一个功能要5个小时,而B程序员认为只需要2个小时,那么他们各自取出有相应时间的牌藏在手中,最后摊牌,如果时间差距很大,那么A和B就可以对这两个时间进行讨论,最后确定最合适的任务周期。

持续集成

持续集成也是Scrum剑术中的技能之一,持续集成也就是每日集成部署,保证每天都要有一个可以成功编译,并可以演示的版本,要做到这一点,传统的集成部署方式显然是无法实现的,所以我们需要使用自动化集成部署方案。

持续集成一般分为四个阶段,也是通过不断摸索实践,从历史长河演化而来,但这四个阶段的方式没有谁好谁坏,只有我们的现状适合哪个阶段。

  • 代码级别的集成:这个阶段不依赖独立的集成工具,一般使用IDE内置的编译工具。同时代码风格检查、单元测试、测试覆盖率都有开发人员在本机人工执行。接下来的交付准备环境、运行测试、备份旧版本、新版本打标签以及反馈机制等其他重复的事情都由手工完成。
  • 集成工作流:这个阶段整个开发流程的重心从代码级别的集成转移到了更自动化地编译和更完善的测试验证,致力于在最短时间内发现问题,缩短开发周期,提高软件质量。具体的形式是先进行代码编译,触发单元测试,集成测试,打包测试,自动部署到测试环境。循环往复,形成编译-构建-测试-集成-部署的工作流。
  • 持续交付与部署:在上个阶段,自动部署只是最终部署在测试环境,还需要手动部署到生产环境,因为产品从需求到部署的过程中会经历若干个不同的环境,如开发环境、QA环境、自动化测试环境、生产环境等。所以在这个阶段要建立标准化的环境部署顺序,在工作流中增加部署预生产环境,并执行灰度集成测试,做好线上环境部署后的回归测试。持续交付并不是指软件每一个改动都要尽快部署的产品环境中,而是指任何的代码修改都可以在任何时候实施部署。而持续部署,指的是自动部署到生产环境中。
  • 基于Docker的持续集成:这个阶段是上个阶段的进化,主要解决的问题是通过Docker统一部署环境。具体形式是开发者提交代码,触发单元测试,集成测试,打包测试,产品构建,触发Docker镜像构建,构建镜像上传至私有仓库,镜像下载至执行机器,镜像运行。

通过持续集成我们就可以大幅缩短成果的交付周期,从而达到不断交付有价值的成果以满足客户需求的先决条件。

至此,我们有互相高度信任的团队,有条不紊的做着正确的事,不过我们只完成了计划内工作流的第一步,既优化工作优先级。目前我们只是有了让产品更快的投入市场的先决条件,想要真正实现,那么还需要提高计划内工作流的流量吞吐率及流速。

寻觅坐骑

首先我们要明确在我们的所有工作中一共有四种类型的工作:

  • 业务工作:也就是我们需要完成的和产品相关的工作。
  • 内部工作:团队内部做的一些改进工作,比如搭建自动化部署框架等。
  • 变更工作:由上面两种工作引起的工作,比如开发向测试交接时引起的问题,业务工作的需求改变引起的问题,内部的改进工作引起的问题等。
  • 计划外工作:一般都由上面三种工作导致,尤其是变更工作引起的需要补救的工作,而且往往优先级都相对较高。

业务工作和内部工作我们又称之为计划内工作,变更工作往往也是我们无法避免的,而计划外工作是最为可怕的,如恶魔一般,我们要以牺牲计划内工作为代价去消灭它。

所以我们知道了影响计划内工作流流速的其中一个因素就是计划外工作,那么影响流量吞吐率的因素是什么呢?那就是约束点

我们将产品从需求到交付的过程想象为一个加工工厂的加工流水线过程,产品需求看作是加工原料,开发、测试、运维等看作是工厂流水线上每一环节的机器,原料从流水线起始位置流入,经过一个个加工机器,最终加工为一个成品。但是当其中的某个机器工作效率很低的时候,在该机器处就会堆积越来越多从上游传来的半成品,而下游的机器则闲置着,或者使用率极低,这种情况下这个工厂的生产效率可想而知。那么这个效率很低的机器就是整个工厂流水线的约束点,不但影响了流速也影响了吞吐率。那么这个机器相当于我们产品开发中的什么呢?是不同分工的个人还是不同分工的团队呢?

带着这个问题我们继续回到这个工厂,仔细观察可以我们可以看出加工流水线上的每个加工环节都有四个部分组成,那就是机器、人员、方法、测评。机器是工具,人员按照方法操作工具,然后根据测评细则检查加工的半成品在这一环节是否合格。这四部分组成的就叫工作中心,工作中心就是产品开发中不同分工的团队,所以某个团队的效率低下就会称为整个工作流的约束点。

那么团队为什么会成为约束点呢?因为团队里也有约束点。我们来继续看这个工厂,如果操作某个机器的人操作不熟练,或者一个人要兼顾好几个机器的话,那么这个人员就可能成为这个工作中心的约束点,甚至是多个工作中心的约束点。所以,解决约束点的问题是至关重要的,所有在非约束点所做的改进都是假象。

消除或保护约束点

有些约束点是因为自身能力问题导致的,这种情况下我们可以先调整他的任务,将优先级相对低的任务分配给他,同时通过技术交流会或者师带徒快速提升他的能力,从而消除约束点。另一种情况的约束点恰恰是因为这个人能力很强或者他的工作牵连着别的工作中心,从而参与了多个工作中心,反而使他自己的工作中心效率低下,这种情况我们就要采取保护约束点的措施:

  • 永远不要让这种约束点迁就别的工作中心,我们的做法应该完善每个工作中心的方法,使之标准化和自动化,我们的持续集成技能就能改善这一点。
  • 将这种约束点着力于完成优先级相对比较高的任务。

所以我们要善于识别约束点,然后消除或保护约束点,最后寻找下一个约束点,以此反复。

杜绝计划外工作

为什么计划外工作会影响工作流流速呢?因为它增大了工作流中的某个工作中心,或者工作中心里某个人员的等待执行计划内工作的时间。等待时间怎么算的呢?等待时间等于忙碌百分比除以空闲百分比。

等待时间 = 忙碌百分比 / 空闲百分比

因为计划内工作,在前期都指定好了合理的周期,所以团队成员的忙碌百分比一般不会超过50%,所以空闲百分比也是50%,那么等待时间就是1。如果有大量的计划外工作涌进来,那团队成员的忙碌百分比就有可能达到70%或80%,甚至更高,假如忙碌百分比达到了80%,那么空闲百分比为20%,等待时间将增加到4。所以从这个公式可以看到,超负荷的工作任务其实是产生约束点的罪魁祸首,而计划外工作又是超负荷工作的始作俑者。

那么我们该如何杜绝计划外工作呢?我们知道计划外工作一般都是有变更工作引起的补救工作,因为80%的计划外工作都是由20%的变更工作造成的。既然变更工作是不可避免的,那么就尽量做到不引起补救工作,也就是要干净利落的完成变更工作,防止因变更工作导致其他问题发生。所以要想有效的杜绝加化外工作,那就需要建立起变更管理系统

变更管理系统的主要作用是确保能正确实施变更确认、分析、评估、计划、实施、检查的过程。它的相关干系人可分为三个角色:

  • 客户:发起变更工作的源头,这个客户指的不仅是用我们产品的真实客户,在产品生命周期中,测试团队可能是开发团队的客户,开发团队可能是运维团队的客户,因为他们都有可能给上游或下游发起变更。
  • 变更委员会委员:既对变更进行分析、评估、计划及决定变更优先级和变更实施者的人。
  • 变更实施者:既具体实施变更的人或团队。

这三种角色的人参与整个变更流程,基本的变更流程如下:

  • 产生变更:客户有新的需求或者更改原有需求。产品生命周期中,上下游的工作中心发起变更,比如发现Bug,或者修改部署环境配置等。
  • 分析变更:变更委员会确定变更请求的技术可行性以及变更成本和变更收益,暂时过滤涉及约束点的变更。
  • 评估变更:评估变更影响范围,既实施了变更后会对产品的哪些地方产生什么样的影响,明确变更影响和确定防范措施。
  • 计划变更:根据分析变更和评估变更的结果,确定可实施的变更,既变更优先级,然后分配变更实施者。最后将所有状态的变更使用看板将其可视化。
  • 实施变更:变更实施者根据计划变更的结果,按计划执行变更、测试变更、完备变更文档、发布变更。
  • 检查和关闭变更:对已完成的变更进行检查,根据变更文档和测试变更的结果决定是否认定变更成果完成并关闭变更。

有了完善的变更管理系统,通过严格的变更流程,我们首先可以筛选变更,其次可以对变更了如指掌,可以通过计划内工作看板和变更工作看板分析出如何安排变更工作,安排给谁最为合适,最后我们可以确保更变工作在掌控中安全的完成,不会产生额外的需要补救的计划外工作。

所以消除或保护约束点以及变更管理系统可以帮助我们提高工作流吞吐量和流速,是提升我们速度的最好坐骑。

至此,我们就完成了计划内工作流的第二步识别保护约束点,并且基本实现了第一工作法,既帮助我们在工作到来时如何建立快速工作流,使需求-开发-测试-运维-客户整个自左向右的工作流流量最大化,不让缺陷流向下游工作中心,为了整体目标不断对工作流进行优化。在第一工作法的帮助下,我们似乎可以使产品更快的投入市场了。

装备盾牌

当拿着利剑乘骑着坐骑冲进战场后,其实战争才刚刚开始。根据我的经验,绝大多数产品在投入市场后依然会存在一些Bug,而且会被客户发现,哪怕之前已经经过了测试系统的测试,所以当产品快速投入市场后,客户的的各种新需求和Bug反馈会如猛兽一般砸向我们,我们要做的就是一边盾挡洪水般的新需求一边斩杀客户发现的Bug,但仍会让我们措手不及。然而客户的需求和要求是无穷无尽的,像填不满的沟壑,如果我们能及时进行预判,那么我们就能自如许多,逐渐和客户形成良性循环。

个体之间的反馈回路

我们在开发过程中,经常会遇到这种情况,A开发人员开发的某个功能流转到B开发人员使用,但是B开发人员发现这个功能开发的不完善,不能满足B的需求,于是B按照自己的需求修改了A开发的功能,而不会去考虑这个变更是否会影响到C的使用,于是这个功能在工作流上一直流转下去就有可能已经远悖与原始需求了,如果这个功能看作是工厂生产线中产品的一个零部件,那么这个四不像的零部件在组装成品时会带来什么呢?必然是零件不合规,于是20%的变更引起了80%的计划外工作,眼看交付日期降至,开发人员又开始奔命与修补工作,即使最后完成了成品的组装,或许还是会留下不可预知的隐患及Bug。解决这类问题的最好办法就是建立起个体之间的问题反馈回路,它能避免不必要的变更工作。

一般变更工作都是在事物相对成型的状态下动其内部细节的工作,这种变更工作往往能牵一发而动全身,一个不好就会像釜底抽薪一般,让整体摧枯拉朽的坍塌,所以我们要有变更系统来加以管理和约束。上面那个例子中,如果当B发现A开发的功能不合规时,能立即反馈给A,经商榷探讨后由A及时修正了这个功能,并且A的这个修正行为并不属于变更行为,那么也许后面一系列的问题都不会发生了。这就是个体与个体之间的反馈回路,这种反馈回路要尽可能的短,回路两头要能快速响应,工作流中的每个个体都应该要建立这种反馈回路,而且尽量不要跨个体建立。

工作中心之间的反馈回路

个体之间建立反馈回路,能有效保证单个工作中心能按照它的规格产出合格的成果,但也许这个成果与整个工作流对产品的规格来说还有差异,所以工作中心和工作中心之间也要建立反馈回路,开发团队和测试团队之间,开发团队和运维团队之间,每个工作中心要指定一个个人作为反馈信息接口人,这样从细节到整体都能有效降低变更工作的发生率,从而大大减少计划外工作的发生率。

然而,反馈回路反馈的不仅仅是各种问题,还有其他更重要的信息,那就是市场团队和产品团队之间的反馈回路承载的信息。我们能预判市场和客户的需求呢?市场团队通过反馈回路不断提供的各种销售统计和市场报告是良药,能使产品团队知道做哪些事能让公司利益最大化,做到对市场需求和客户需求的预判,从而能抢占先机的将迎合市场的新功能推向市场。

反馈机制就是我们的盾牌,有了这面盾牌,我们就能很好的完成计划内工作流的第三步按时高质量交付成果,做到不欠技术债务,减少变更工作,进一步杜绝计划外工作。同时这也是第二工作法的核心内容,那就是建立尽可能短的个体之间和工作中心之间的不间断反馈回路,在个体之间、工作中心之间建立共同的目标和共同的解决问题的机制,这使我们能在产品初始阶段就能筹划并保证产品质量问题,避免返工,并且能及时获取到市场数据,做到市场需求和客户需求的预判,从而提升客户满意度和提升产品在市场的份额。

营造环境

第三工作法的核心是在团队或公司建立鼓励探索、不断从失败中吸取教训、理解反复的实践是精通工作的先决条件的文化。让团队形成敢于创新、敢于冒险以及高度信任彼此的文化,同时要让所有人知道非功能性需求对于产品同等重要,合理安排功能性需求实现和非功能性需求实现的计划。第三工作法精髓在于不断尝试和理解重复练习是熟练掌握的前提。

总结

我们以DevOps方法论为基础,以三步工作法为指导思想,使用敏捷开发、区分“相关”与“无关”工作法、变更管理系统、保护约束点、建立反馈机制等具体方法,经过不断的实践和优化最后形成的工作流称之为价值流,它是从需求获取到代码签入再到产品投产整个工作流中的关键路径。我们要将价值流上的所有东西进行版本控制,使价值流中的每个个体都共享一种文化,这种文化不仅重视彼此的时间和贡献,而且为了实现整体的持续改进,要勇于不断向自己的工作注入压力,同时使每个个体都要像重视功能性需求一样重视非功能性需求,比如产品质量、可扩展性、可维护性、可操作性、安全性等。

如果我们能建立起这种价值流,那么就能提升员的工生产力以及工作成就感和幸福感,让公司重塑生产能力,从库存型生产转变为订单型生产,从给公司在市场中创造出巨大的竞争优势。

参考文献:

《凤凰项目》
不可错过的「持续集成」

推荐书单:

《凤凰项目》
《目标》
《持续交付》

本文首发CSDN,如需转载请与CSDN联系。

AngularJS是什么

我们知道HTML是一种创建网页的静态标记语言,它很基础,但不失灵活,然而它自身并不提供对灵活性的具体实现,所以我们要创建具有可用性及赏心悦目的Web应用就需要使用其他语言与之结合去操控它的DOM、标签、标签属性、标签层级结构实现样式变换、动态数据变换、动态元素变换等,比如结合CSS和JavaScript语言。

但是越多的结合就意味着功能实现越复杂,我们需要写大量的代码去实现类似数据绑定、动态展现、远程服务请求等功能,所以jQuery这类的JS库、ember这类的框架应用而生。前者让我们在完成某些单一功能时调用它封装好的方法,从而减少代码量。后者让我们可以遵循它的规范去填充它设计好的代码结构,从而逐步完成完整的应用功能。

而AngularJS是JS库和框架的结合,它诞生于2009年,由Misko Hevery 等人创建,后为Google所收购并发扬光大。AngularJS通过为开发者呈现一个更高层次的抽象来简化应用的开发,提供了构建一个CRUD应用可能用到的全部内容包括:自定义HTML标签、事件绑定、数据绑定、基本模板标识符、表单验证、路由、深度链接、组件重用、依赖注入等。

文章中的所有示例均可在这里下载。

AngularJS的特性

AngularJS有着诸多的特性,但最为核心的是其Scope概念、MVC模式、路由、模块化、依赖注入、指令系统、双向数据绑定、Service、Provider等。

模块化

我们先从AngularJS的模块化特性说起。什么是模块?既应用程序中不同功能的容器。在AngularJS中,我们可以按业务功能的不同将实现划分为不同的模块,这些模块可以在一个JS文件中,也可以将它们放在不同的JS文件中,既一个JS文件为一个模块。

模块化的优势

使用模块化在我们的编程阶段有着诸多的好处:

  • 可以使我们的代码结构非常清晰,有较好的可读性。
  • 可以使我们复用代码。
  • 在前端编程中,我们都知道JS的引用顺序是很重要的,但使用AngularJS的模块时我们不需要关系每个模块之间的顺序。
  • 可以很好的实现单元测试。

定义模块

定义一个模块很简单,在JS文件中申明一个模块:

var mianModule = angular.module("mianModule", []);

使用AngularJS的module方法申明一个模块,该方法有两个参数:

  • 第一个参数为模块名称。
  • 第二个参数为一个数组,该参数的含义为当前定义的模块所依赖的模块,如果有依赖模块则传入包含模块名称的数组,若无依赖则传入空数组。

使用模块

在了解如何使用定义好的模块之前,需要先清楚在AngularJS中,模块与模块之间可以是相互独立,老死不相往来的关系,也可以是依赖关系,并且可以是嵌套依赖关系:

// modules.js
// 互相独立的模块
var mainModule = angular.module("mainModule", []);
var anotherModule = angular.module("anotherModule", []);
// modules.js
// 有依赖关系的模块
var mainModule = angular.module("mainModule", ["secondModule"]);
var secondModule = angular.module("secondModule", ["thirdModule"]);
var thirdModule = angular.module("thirdModule", []);

在AngularJS中使用定义好的模块也有两种方式,对应两种不同的应用场景。

ngApp方式

这种方式是AngularJS团队比较推荐的使用方法,只需要在HTML标签中使用AngularJS的ng-app指令指定要使用的模块既可,一般指定的是应用的主模块,或者说是应用入口模块:

<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Demo for Module</title>
<script src="../angular-1.5.8.js"></script>
<script src="modules.js"></script>
</head>
<body ng-app="mainModule">

</body>
</html>

ng-app指令的含义类似Java中的main方法,是当前页面中应用的唯一主入口,所以一个页面中只能使用一次np-app指令。将ng-app指令定义在哪个HTML的标签中就表示该标签及它的所有子标签就会被AngularJS接管,这一部分HTML代码将可以使用AngularJS所有的功能和特性,也就是使用AngularJS开发的Web应用的主入口。通常情况下都会在body标签中使用ng-app指令。

因为使用这种方式只能指定一个应用入口模块,所以为了能使用多模块的特性,就需要抽象出一个主模块,然后将其他所有模块加入主模块的依赖关系中,这里要注意的是因为AngularJS中的模块依赖可以向下穿透,类似类的继承,所以加入主模块依赖关系中的模块并不是所有的模块,而只是处于依赖层级顶层的模块。
主模块依赖关系
如上图所示,在主模块中只需要添加DataHandleModuleNetworkModule两个模块既可。但是这种方式的弊端是在HTML页面中并不能直观的表现出页面的哪些部分使用了何种模块,而且本身提供了模块化的特性,但最终又要抽象到一个模块中去使用,始终有点不是很舒服。

手动加载方式

虽然使用ng-app的形式可以满足需求,但是还要考虑想抽象出主模块,然后依赖一大堆模块,如果更希望可以在HTML标签中指定使用的模块,此时就需要手动的加载模块了:

// modules.js
// 互相独立的模块
var mainModule = angular.module("mainModule", []);
var anotherModule = angular.module("anotherModule", []);

angular.element(document).ready(function() {
var myDiv1 = document.getElementById("myDiv1");
angular.bootstrap(myDiv1, ["mainModule"]);

var myDiv2 = document.getElementById("myDiv2");
angular.bootstrap(myDiv2, ["anotherModule"]);
});
<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Demo for Module</title>
<script src="../angular-1.5.8.js"></script>
<script src="modules.js"></script>
</head>
<body>
<div id="myDiv1">
</div>

<div id="myDiv2">
</div>
</body>
</html>

通过上述代码可以看到,我们可以使用AngularJS的bootstrap方法给HTML元素指定模块。这样感觉和模块化特性更加切合,模块的隔离性也比较好,但是这种方式依然有显著的弊端。首先从HTML页面中依然不能直观的看到模块在页面中使用和分布情况,再次这种方式将视图层的HTML代码与JS代码耦合在了一起,也有点违背了AngularJS的MVC设计模式,也许这也是AngularJS不推荐该方式的原因之一吧。

模块的生命周期

在AngularJS中,模块有两个主要的生命周期方法,那就是.config().run()

var mainModule = angular.module('mainModule', []);
mainModule.config(function(injectables) {

});

mainModule.run(function(injectables) {

});

.config()方法的作用是在模块运行加载之前对模块进行配置,比如创建各种服务、创建自定义指令、注册过滤器等。.run()方法相当于AngularJS应用的main方法,在该方法里进行的配置都是运行时态的,比如对已经创建好的服务实例在应用运行期进行修改。

.config()方法在后文还会涉及,服务、指令、过滤器也会在后文有详细的讲解。

MVC

MVC是软件工程中的一种设计模式,既把应用系统分为模型(Model)、视图(View)和控制器(Controller)三个基本部分,并且模型层与视图层之间是相互隔离的。简单的描述每个部分的职能:

  • 模型层:管理数据模型。
  • 视图层:控制UI的展现更新等。
  • 控制层:负责具体业务逻辑处理、请求处理转发等,是模型层和视图之间的桥梁。

AngularJS是为数不多实现了MVC设计模式的前端框架,为前端应用在开发时期的功能职责切分、代码复用及后期的维护提供了极大的便利。

Controller

在AngularJS中,模块是万源之本,所以AngularJS中的所有东西都是通过模块创建的,Controller也不例外。我们可以使用模块对象的.controller()方法创建控制器:

var mainModule = angular.module("mainModule", []);

mainModule.controller("GreetingController", ["$scope", function($scope) {
$scope.greeting = "Hello!";
}]);

从上面的示例代码可以看到,.controller()方法有两个参数:

  • 第一个参数类型为字符串,为Controller的名称。
  • 第二个参数类型为数组,该数组用于注入当前Controller要用到的服务及实现业务逻辑的函数,这里要注意的是实现逻辑的函数始终是作为数组的最后一个元素,并且要将前面注入的服务作为该函数的参数传入。

这里先简单描述一下$scope,在AngularJS中有一个重要的概念是服务,而$scope就是一个AngularJS内置的服务,在后面的章节中会详细讲解服务。那么$scope服务从字面理解是作用域的意思,其实也差不太多,如果用OO的思想将AngularJS的Controller看作是一个类,那么$scope服务就代表了这个类的作用域,那么就可以通过$scope服务给这个类添加属性或者方法,上面的代码示例中通过$scope服务给GreetingController控制器添加了字符串属性greeting及对象属性person

所以上面代码的含义是,首先创建了名为mainModule的模块,然后在mainModule模块中创建了名为GreetingController的控制器,并使用$scope服务给该控制器添加了名为greetingperson的属性。

创建好Controller后,来看看如何使用它:

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Demo for Controller</title>
<script src="../angular-1.5.8.js"></script>
<script src="modules.js"></script>
</head>
<body ng-app="mainModule">
<div ng-controller="GreetingController">
{{ greeting }} {{ person.name }} !
</div>
</body>
</html>

前文中介绍过在HTML页面中,通过使用ng-app指令给标签绑定模块。同理,我们可以使用ng-controller指令给标签及它的子标签绑定Controller,绑定了Controller的标签及它的子标签都可以使用Controller中的属性或者方法。我们可以使用双花括号语法访问Controller的属性或调用方法。运行效果很简单,就是将这两个属性的值输出到页面。

我们再来看看如何在Controller中添加方法:

var mainModule = angular.module("mainModule", []);

mainModule.controller("GreetingController", ["$scope", function($scope) {
$scope.personName = "Everyone";

$scope.welcomeJason = function() {
$scope.personName = "Jason";
};

$scope.welcomeGreen = function() {
$scope.personName = "Green";
};
}]);

在上面的代码中,我们给GreetingController添加了personName属性和welcomeJason()welcomeGreen()两个方法,并在这两个方法中分别对personName属性的值进行修改。再来看看HTML的代码:

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Demo for Controller</title>
<script src="../angular-1.5.8.js"></script>
<script src="modules.js"></script>
</head>
<body ng-app="mainModule">
<div ng-controller="GreetingController">
<button ng-click="welcomeJason()">Jason</button>
<button ng-click="welcomeGreen()">Green</button>
<p>
Welcome {{ personName }} !
</p>
</div>
</body>
</html>

在HTML代码中,添加了两个按钮,此时我们又看到了一个新的AngularJS指令ng-click,这个指令很好理解,就是给按钮绑定点击事件,当点击按钮时调用GreetingController中对应的方法,然后在p标签中显示personName属性。所以Controller中的方法不仅可以通过双花括号语法调用,也可以通过ng-click指令调用。我们来看看运行的效果:
Demo for Controller

Model

Model指的是数据模型,在AngularJS中使用$scope服务给Controller添加的属性就是数据模型:

var mainModule = angular.module("mainModule", []);

mainModule.controller("GreetingController", ["$scope", function($scope) {
$scope.person = {
name: "Jason",
job: "Developer"
};
}]);

上述代码示例中的person属性就是数据模型,下面看看如何使用:

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Demo for Model</title>
<script src="../angular-1.5.8.js"></script>
<script src="modules.js"></script>
</head>
<body ng-app="mainModule">
<div ng-controller="GreetingController">
<input ng-model="person.name">
<input ng-model="person.job">
<p>
Welcome {{ person.name }}, He is a {{ person.job }}!
</p>
</div>
</body>
</html>

在上述HTML代码示例中,我们又看到了新的指令ng-model,顾名思义,该指令就是用来将数据模型与HTML标签元素进行绑定的。上面的代码中分别将两个输入框与person对象的namejob属性进行了绑定,然后在p标签中输出,我们来看看运行效果:
Demo for Model

不难看出,当我们将person对象的属性与输入框绑定后,person对象属性的默认值就会显示在输入框里了,当我们修改输入框里的内容时会自动将数据通过Controller同步到person对象的相应属性上,所以p标签中的内容会实时跟着输入框的内容进行变化。

View

View层自然就是HTML中的DOM元素了,通过AngularJS提供的各个指令将DOM元素与Controller和Model进行绑定。由Controller负责将数据模型的内容通过双花括号语法或ng-model指令展现在DOM元素上,而当DOM元素中的值发生变化时会由Controller捕获到,并更新对应的数据模型。

数据双向绑定

在MVC一节中,通过几个示例介绍了如何创建和使用Controller、Model,如何与View层交互,其实也引出了AngularJS的一个重要特性。在Controller小节的例子中,通过点击按钮由Controller更改数据模型并将其展示在页面中,这是通过数据模型的变化从而影响视图层的显示。在Model小节的例子中,通过修改输入框中的值,由Controller捕获并更新对应的数据模型,这是通过视图层的变化从而影响数据模型的值。这就是AngularJS的数据双向绑定特性。

服务(Services)

AngularJS中一个重要的概念是服务,这个服务的概念比较宽泛,比如一个常量值也算做一个服务,既提供一个不可变值的服务。变量、对象、函数都算做是服务。在AngularJS中内置了好几十个服务,这些内置的服务都以$符号开头,比如$scope$http$log$timeout$interval等等,从字面意思都不难理解它们的作用,更多的内置服务可以去AngularJS官网查看API文档

服务特征

AngularJS中的服务有两个主要特点:

  • 延迟加载,当应用中的其他组建使用服务时才会实例化。
  • 单例,在应用的整个生命周期中,一个服务只存在一份实例,所以服务一般用来共享可复用的代码逻辑或者数据。

自定义服务

除了内置的服务,我们还可以创建自己的服务,在AngularJS中我们可以通过$provide这个内置的服务来创建我们的自定义服务,$provide服务提供了五个方法供我们创建不同应用场景的自定义服务,这五个方法分别是provider(name, provider)factory(name, $getFn)service(name, constructor)value(name, value)constant(name, value)

Value

我们先从value(name, value)这个方法看起,该方法有两个参数:

  • 第一个参数为服务的名称,类型为字符串。
  • 第二个参数可以是字符串、数字、数组、对象或者函数。

假设在我们的应用中,多个Controller中都使用了相同的属性,比如都需要用到客户端ID这个属性,那么我们可以将其抽象为一个服务,该服务就专门用来获取客户端ID,来看看如何创建这个服务:

var mainModule = angular.module("mainModule", []);

mainModule.value("clientId", "qazxsw123456");

上面的示例代码创建了名为clientId的服务,该服务其实就是一个字符串。不过这和$provide服务有什么关系呢?其实上面这种写法并不是完整的写法,只是一个语法糖而已,真正完整的写法是在模块的.config()方法中通过$provide服务去创建:

mainModule.config(function($provide) {
$provide.value("clientId", "qazxsw123456");
});

创建好服务后通过AngularJS的注入机制将其注入到Controller中:

mainModule.controller("FirstController", ["$scope", "clientId", function($scope, clientId) {
$scope.clientId = clientId;
}]);

mainModule.controller("SecondController", ["$scope", "clientId", function($scope, clientId) {
$scope.clientId = clientId;
}]);

然后在HTML页面中正常使用Controller就可以了:

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Demo for Value Service</title>
<script src="../angular-1.5.8.js"></script>
<script src="modules.js"></script>
</head>
<body ng-app="mainModule">
<div ng-controller="FirstController">
Client ID in FirstController: {{ clientId }}
</div>
<div ng-controller="SecondController">
Client ID in SecondController: {{ clientId }}
</div>
</body>
</html>

上文中说过$scope服务的其中一个作用就是给Controller添加属性和方法,然后可以在绑定Controller的DOM中使用双花括号语法直接访问添加的属性或调用方法。然而就$scope服务的这一功能而言,AngularJS还提供了另一种方式,我们先来看看Controller的写法:

mainModule.controller("FirstController", ["clientId", function(clientId) {
this.clientId = clientId;
}]);

mainModule.controller("SecondController", ["clientId", function(clientId) {
this.clientId = clientId;
}]);

上述代码中我们并没有将$scope服务注入到这两个Controller中,而是使用this创建了clientId属性,this代表Controller的实例。使用这种方式后在HTML页面中使用Controller也有点变化:

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Demo for Value Service</title>
<script src="../angular-1.5.8.js"></script>
<script src="modules.js"></script>
</head>
<body ng-app="mainModule">
<div ng-controller="FirstController as first">
Client ID in FirstController: {{ first.clientId }}
</div>
<div ng-controller="SecondController as second">
Client ID in SecondController: {{ second.clientId }}
</div>
</body>
</html>

我们看到在ng-conroller标签中不再是直接写Controller名称了,而是使用as关键字声明了Controller的实例,然后在双花括号中使用Controller的实例去访问属性或者调用方法。

使用$scope服务和this给Controller添加属性或方法的效果是一样的,所以不存在谁好谁坏的概念,只不过使用this的方式更贴合OO的思想,而且在HTML代码中对使用的属性或方法有更直观的可读性,能一眼看到使用了哪个Controller的属性或方法,所以使用哪种方式按个人喜好,但是不建议混用这两种方式。

这里在介绍另外一个语法糖,那就是在注入服务的时候不用繁复的在数组中和函数参数中都声明,只需要在函数的参数里声明就可以了:

mainModule.controller("FirstController", function($scope, clientId) {
$scope.clientId = clientId;
});

// 或者

mainModule.controller("FirstController", function(clientId) {
this.clientId = clientId;
});

Constant

我们再来看看constant(name, value)方法:

var mainModule = angular.module("mainModule", []);

mainModule.constant("clientId", "qazxsw123456");

该方法和value(name, value)在创建的服务内容形式上来说是一样的,但是两者创建的服务在功能性上还是有区别的:

  • 从名称就可以看出用constant(name, value)方法创建的服务是不可修改的。
  • 使用constant(name, value)创建的服务可以在模块的.config()方法中注入,也就是可以在创建其他服务时使用,而使用value(name, value)创建的服务不可以。

Service

现在又有一个需求,希望能获取到当前时间添加在客户端ID后面,那么我们可以使用service(name, constructor)方法来创建获取当前时间的服务:

var mainModule = angular.module("mainModule", []);

mainModule.value("clientId", "qazxsw123456");

mainModule.service("currentDate", Date);

mainModule.controller("FirstController", function(clientId, currentDate) {
this.clientId = clientId + "-" + currentDate;
});
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Demo for Service Service</title>
<script src="../angular-1.5.8.js"></script>
<script src="modules.js"></script>
</head>
<body ng-app="mainModule">
<div ng-controller="FirstController as first">
Client ID in FirstController: {{ first.clientId }}
</div>
</body>
</html>

输出结果:
Client ID in FirstController: qazxsw123456-Thu Sep 08 2016 17:05:30 GMT+0800 (CST)

service(name, constructor)方法的第二个参数是函数构造器,也就是函数的实例,所以currentDate服务的实体其实就是new Date()

Factory

现在,我们希望通过一个服务就可以完成客户端ID和当前时间的拼接,不需要给Controller注入两个服务,来看看如何用factory(name, $getFn)方法来实现:

var mainModule = angular.module("mainModule", []);

mainModule.constant("clientId", "qazxsw123456");

mainModule.factory("clientIdAndCurrentDate", function(clientId) {
return clientId + "-" + new Date();
});

mainModule.controller("FirstController", function(clientIdAndCurrentDate) {
this.clientId = clientIdAndCurrentDate;
});

首先我们需要用constant(name, value)方法创建clientId服务,因为需要将它注入到新的服务中,前文也介绍过constant(name, value)value(name, value)方法的区别。然后使用factory(name, $getFn)方法创建clientIdAndCurrentDate服务,该函数的第二个参数类型是函数,我们在该函数中将clientId服务返回的客户端ID与Date构造器返回的时间进行拼接然后返回,当然运行结果还是一样的:
Client ID in FirstController: qazxsw123456-Thu Sep 08 2016 17:05:30 GMT+0800 (CST)

其实这个服务还可以写成这样:

mainModule.constant("clientId", "qazxsw123456");

mainModule.service("currentDate", Date);

mainModule.factory("clientIdAndCurrentDate", function(clientId, currentDate) {
return clientId + "-" + currentDate;
});

这相当于clientIdAndCurrentDate服务对currentDate服务进行了进一步的配置或者说增加了功能,那么也就是说factory(name, $getFn)方法相比service(name, constructor)方法可以创建更复杂一些的服务。

Povider

现在又有新的需求,希望对clientId后面的时间进行格式化,但假设我们没有权限去更改clientIdAndCurrentDate服务,那么这时我们需要使用provider(name, provider)方法创建另外一个服务,然后对clientIdAndCurrentDate服务进行配置,来看看如何实现这个服务:

var mainModule = angular.module("mainModule", []);

mainModule.constant("clientId", "qazxsw123456");

mainModule.service("currentDate", Date);

mainModule.factory("clientIdAndCurrentDate", function(clientId, currentDate) {
return clientId + "-" + currentDate;
});

mainModule.provider("clientIdAndCurrentDateByFormat", function() {
this.formatFunc = function(str) {
var clientId = str.substring(0, str.indexOf("-"));
var dateStr = str.substring(str.indexOf("-"), str.length);
var dateObj = new Date(dateStr);
var year = dateObj.getFullYear().toString();
var month = (dateObj.getMonth() + 1).toString();
var day = dateObj.getDate().toString();
var hour = dateObj.getHours().toString();
var minute = dateObj.getMinutes().toString();
var second = dateObj.getSeconds().toString();
return clientId + "-" + [year, (month >= 10 ? month : 0 + month), (day > 10 ? day : 0 + day), hour, minute, second].join("");
};
this.$get = function(clientIdAndCurrentDate) {
return this.formatFunc(clientIdAndCurrentDate);
};
});

mainModule.controller("FirstController", function(clientIdAndCurrentDateByFormat) {
this.clientId = clientIdAndCurrentDateByFormat;
});

首先我们创建了formatFunc()辅助配置函数,然后实现了$get方法,通过formatFunc()辅助函数配置clientIdAndCurrentDate服务,我们来看运行结果:
Client ID in FirstController: qazxsw123456-20160909113523

要注意的一点是,通过provider(name, provider)方法创建服务时必须要显式的实现$get方法,并且只有在$get方法中才能注入其他服务。在AngularJS中服务仅指$get返回的东西,所以前四种创建服务的方法其实都是provider(name, provider)方法根据不同应用场景实现的语法糖,比如factory方法其实就是把一个函数当作了$get方法,service方法其实是将一个函数构造方法或者说函数实例当作了$get方法,valueconstant方法其实又是对factory方法的语法糖实现。所以在自定义服务时可按需选择不同的方法创建服务。

指令(Directive)

指令是AngularJS中另一个主要的特性,指令的作用可以用一句话描述,就是可以给HTML元素赋予特殊或自定义的行为,比如监听事件、视图模板代理等。在上文中我们使用过的ng-appng-controllerng-model就是AngularJS中的指令。

指令的命名

指令的命名和使用写法比较有意思,一般情况下在定义指令时推荐使用驼峰命名法,比如ngModelngApp,但是在HTML中大小写是不敏感的,所以在HTML中使用指令时推荐使用小写字母加破折号的形式,比如ng-modelng-app。除了使用小写破折号这种方式,还有以下几种使用写法:

  • ng:model
  • ng_model
  • data-ng-bind
  • x-ng-bind

大家可以根据自己喜好选择使用写法,但是尽量保持写法统一。

指令的形式

在AngularJS中,指令有四种表现形式,既标签形式、标签属性形式、标签class名称形式、注释形式:

<my-dir></my-dir>
<span my-dir="exp"></span>
<!-- directive: my-dir exp -->
<span class="my-dir: exp;"></span>

一般情况下,推荐使用标签形式和标签属性形式。

使用指令

与Controller一样,指令也是通过AngularJS的Model创建的,使用directive(name, directiveFactory)方法创建指令,该方法有两个参数:

  • 第一个参数为指令名称,命名规范在上文中已经说过了。
  • 第二个参数是一个工厂函数,该函数需要返回一个对象,我们通过配置该对象中的不同属性从而告诉AngularJS内置的$compile服务实现指令的不同功能。

指令类型

上文中说过指令的实现是通过工厂函数返回对象,然后通过配置对象的不同属性实现不同功能,所以设置指令的类型也是通过配置属性对象完成的:

var mainModule = angular.module("mainModule", []);

mainModule.directive("myDirective", function() {
return {
restrict: "A"
};
});

mainModule.directive("myDirective1", function() {
return {
restrict: "E"
};
});

mainModule.directive("myDirective2", function() {
return {
restrict: "AE"
};
});
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Demo for Directive</title>
<script src="../angular-1.5.8.js"></script>
<script src="modules.js"></script>
</head>
<body ng-app="mainModule">
<div my-directive></div>
<my-directive1></my-directive1>
<my-directive2></my-directive2>
<!-- <div my-directive2></div> -->
</body>
</html>

从上面的示例代码可以看出,可以通过配置返回对象的restrict属性设置指令的类型,可配置的值分别为:

  • "A":代表且仅可代表标签属性类型的指令。
  • "E":代表且仅可代表标签类型的指令。
  • "C":代表且仅可代表class名称类型的指令。
  • "M":代表且仅可代表注释类型的指令。

如果想设置一个多类型的指令,那么可以将类型标识写在一起,比如"AEC"代表既是标签属性类型,又是标签类型,还是class名称类型。如果不配置restrict属性,那么表示指令的类型为默认的"AE"类型。

通过指令封装UI模板

在前端应用的开发过程中,不同的页面常有很多一样的UI元素,如果每个页面都写一遍,那么在维护时就常会牵一发而动全身,AngularJS中的指令可以很好的解决这个问题,它可以将UI片段封装为一个指令,从而可以在不同的页面中复用,那么在维护时就是四两拨千斤的效果。下面来看看如何实现模板指令:

var mainModule = angular.module("mainModule", []);

mainModule.controller("MyController", function() {
this.name = "Jason";
this.job = "Developer";
});

mainModule.directive("myDirective", function() {
return {
template: "Name: {{mc.name}}, Job: {{mc.job}}"
};
});
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Demo for Directive</title>
<script src="../angular-1.5.8.js"></script>
<script src="modules.js"></script>
</head>
<body ng-app="mainModule">
<div ng-controller="MyController as mc">
<my-directive></my-directive>
<!-- <div my-directive></div> -->
</div>
</body>
</html>

上面的代码示例中,我们将本该写在HTML中的展示代码设置给了返回对象的template属性。为了能更好的管理UI模板,我们还可以将UI展示代码提炼成单独的HTML模板文件,并可以使用指令的templateUrl属性设置HTML模板文件名称,这样便可以大大降低指令和UI模板的耦合度:

mainModule.directive("myDirective", function() {
return {
templateUrl: "myTemplate.html"
};
});
<!-- myTemplate.html -->
Name: {{mc.name}}, Job: {{mc.job}}

那么问题来了,如果UI模板文件很多的话,按上面的方法就要写很多个对应的指令,而且只是UI模板文件名称不一样而已,实在有点冗余。AngularJS提供了另外一种解决方法,那就是可以通过给模板指令设置相关属性,从而动态的加载UI模板文件,我们来看看如何实现:

// modules.js
var mainModule = angular.module("mainModule", []);

mainModule.controller("MyController", function() {
this.name = "Jason";
this.job = "Developer";
});

mainModule.directive("myDirective", function() {
return {
templateUrl: function(elem, attr) {
return "myTemplate-" + attr.type + ".html";
}
};
});
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Demo for Directive</title>
<script src="../angular-1.5.8.js"></script>
<script src="modules.js"></script>
</head>
<body ng-app="mainModule">
<div ng-controller="MyController as mc">
<my-directive type="name"></my-directive>
<my-directive type="job"></my-directive>
<!-- <div my-directive type="name"></div> -->
<!-- <div my-directive type="job"></div> -->
</div>
</body>
</html>
<!-- myTemplate-name.html -->
Name: {{mc.name}}

<!-- myTemplate-job.html -->
Job: {{mc.job}}

从上面的示例代码可以看出,templatetemplateUrl两个属性的值不只是接受字符串,还接受函数。要注意的是该函数默认带两个参数:

  • 第一个参数代表当前的HTML DOM元素。
  • 第二个参数代表当前HTML DOM元素的属性对象,在函数体内可以为该对象设置任何属性。

在上面的示例中,我们给代表当前DOM元素的属性对象设置了type属性,用于标识UI模板文件名称,这样我们就可以通过一个专有的模板指令来控制所有的UI模板文件了。

指令的作用域

上面的示例中,我们通过配置可以实现动态加载UI模板文件,但是我们无法动态指定UI模板文件中显示的内容。这一节我们来了解一下如何通过指令的隔离域达到在同一个指令中动态指定UI模板文件中要显示的内容,先看看代码示例:

var mainModule = angular.module("mainModule", []);

mainModule.controller("MyController", function() {
this.jason = { name: "Jason", job: "Developer" };
this.green = { name: "Green", job: "Doctor" };
});

mainModule.directive("myDirective", function() {
return {
restrict: "E",
scope: {
personInfo: "=person"
},
templateUrl: "myTemplate.html"
};
});
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Demo for Directive</title>
<script src="../angular-1.5.8.js"></script>
<script src="modules.js"></script>
</head>
<body ng-app="mainModule">
<div ng-controller="MyController as mc">
<my-directive person="mc.jason"></my-directive>
<hr>
<my-directive person="mc.green"></my-directive>
</div>
</body>
</html>
<!-- myTemplate.html -->
Name: {{personInfo.name}}, Job: {{personInfo.job}}

从示例中可以看到,我们给指令的返回对象又添加了一个属性scope,这就是指令的作用域属性,scope属性有三种可设置的值:

  • false:默认值,这表示指令共享它父节点的Controller的作用域,也就是可以使用双花括号语法直接访问父节点Controller作用域中的属性。
  • true:创建指令自己的作用域,但是该作用域继承父节点Controller的作用域。
  • {}:第三种是设置一个对象,表示创建了指令自己独立的作用域,与父节点Controller的作用是完全隔离的。

如果我们希望指令的隔离作用域和父节点Controller的作用域之间进行交互,那么就需要将两者进行绑定,这里有三种绑定方式:

  • 使用@实现单向数据绑定,但是只限于绑定Controller作用域中值为字符串的属性,因为是单向绑定,所以父节点Controller修改绑定的属性可影响到指令作用域中对应的属性,反之则不可以。在HTML中使用双花括号语法取值,比如person=""
  • 使用=实现双向数据绑定,在父节点Controller中修改属性和在指令中修改属性可相互影响。在HTML中直接使用属性名称,比如person="jasonObj"
  • 使用&实现函数绑定,用于绑定Controller中值为函数的属性,在HTML中直接调用函数,比如action="click()"

上面的示例中我们给myDirective指令设置了隔离域并添加了名为personInfo的属性,并与父节点的MyController进行数据双向绑定,在HTML代码中,就可以通过<my-directive>指令标签的person属性与MyController的数据绑定了。另外,在进行绑定时还有一种简写的方式:

...
scope: {
personInfo: "="
// personInfo: "@"
// personInfo: "&"
},
...

等同于:

...
scope: {
personInfo: "=personInfo"
// personInfo: "@personInfo"
// personInfo: "&personInfo"
},
...

指令的Controller

在指令中也可以创建Controller,和在Module中创建Controller很类似,既定义函数,在参数中注入需要的AngularJS服务既可:

var mainModule = angular.module("mainModule", []);

mainModule.controller("MyController", function($scope) {
$scope.green = { name: "Green", job: "Doctor" };
});

mainModule.directive("myDirective", function() {
return {
restrict: "E",
scope: {
person: "="
},
controller: function($scope) {
$scope.jason = { name: "Jason", job: "Developer" };
},
templateUrl: "myTemplate.html"
};
});
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Demo for Directive</title>
<script src="../angular-1.5.8.js"></script>
<script src="modules.js"></script>
</head>
<body ng-app="mainModule">
<div ng-controller="MyController">
<my-directive person="green"></my-directive>
</div>
</body>
</html>
<!-- myTemplate.html -->
Name: {{jason.name}}, Job: {{jason.job}}
Name: {{person.name}}, Job: {{person.job}}

在上面的示例中,我们给myDirective指令添加了Controller,有一点不同的是在添加Controller时不能设置名称,指令的Controller名称默认与指令名称一样,如果需要另外指定名称,需要配置controllerAs指定Controller的名称:

...
controller: function($scope) {
$scope.jason = { name: "Jason", job: "Developer" };
},
controllerAs: "directiveController",
...

在上面示例的UI模板文件中可以看出,既可以使用指令隔离域中与父节点Controller绑定的属性,也可以使用在指令自己的Controller中定义在隔离域的属性。

指令之间的交互

指令之间的交互主要是以指令的Controller为桥梁来实现的,这里的交互指的是子指令与父指令之间的交互,我们可以使用指令的require属性设置要引用的父指令的Controller,这里有几种配置方式:

  • require: "controllerName":只查找指令自己的Controller。
  • require: "^controllerName":查找指令自己的Controller以及父指令的Controller。
  • require: "^^controllerName":只查找父指令的Controller。
  • require: ["^controllerName1", "^controllerName2"]:引用多个Controller。

如果指令查找到引用的Controller后该如何使用呢,这就要使用指令的另一个重要的属性link函数了。link函数主要用来为DOM元素添加事件监听、监视模型属性变化、以及更新DOM,该函数共有五个参数:

  • scope:指令的作用域,默认是父节点Controller的作用域,如果指令有创建自己的作用域,那么则指指令自己的作用域。
  • element:指令的jQLite(jQuery的子集)包装的DOM元素,可以通过该参数操作指令所在的DOM元素。
  • attrs:指令所在DOM元素的属性对象,通过.语法可以获取到给DOM元素添加的属性。
  • controller:指令通过require属性引用的Controller实例。
  • transcludeFn:嵌入函数。

link函数的其他几个参数后面文章中都会讲到,当指令找到通过require属性引用的Controller后,我们就可以通过link函数的第四个参数访问引用的Controller了。来看一个示例:

通过指令操作DOM元素

我们了解了link函数后就可以使用该函数实现各种有用的指令了,比如通过指令操作DOM元素:

var mainModule = angular.module("mainModule", []);

mainModule.directive("myDirective", function($interval) {
return {
restrict: "A",
link: function(scope, element, attrs) {
$interval(function() {
element.text(new Date());
}, 1000);
}
};
});
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Demo for Directive</title>
<script src="../angular-1.5.8.js"></script>
<script src="modules.js"></script>
</head>
<body ng-app="mainModule">
<div>
Current Date is: <span my-directive></span>
</div>
</body>
</html>

上面的示例中,首先我们限定了myDirective指令只能以标签属性的形式使用,然后注入了AngularJS的内置服务$interval,通过link函数的第二个参数获取到指令所在的DOM元素,然后周期性更新DOM元素显示的内容。

指令的内嵌机制

大家都知道HTML中的DOM元素是具有层级关系的,一般情况下我们使用指令封装的UI模板颗粒度都会比较小,所以就会出现指令嵌套的现象,这几需要用到指令的内嵌机制了,指令的transclude属性默认为false,如果将其设置为true,那么该指令就开启了内嵌机制,也就是说指令标签之间的内容可以被指定嵌入UI模板中被ng-transclude内置指令标记过的DOM元素中,结合之前说过的父子指令交互的内容来实现一个例子:

// modules.js
var mainModule = angular.module("mainModule", []);

mainModule.directive("myTabs", function() {
return {
restrict: "E",
transclude: true,
controller: function($scope) {
$scope.panes = [];
var panes = $scope.panes;
this.addPane = function(pane) {
if(panes.length == 0) {
$scope.select(pane);
};
panes.push(pane);
};
$scope.select = function(pane) {
angular.forEach(panes, function(pane) {
pane.selected = false;
});
pane.selected = true;
};
},
templateUrl: "myTabs.html"
};
});

mainModule.directive("myPane", function() {
return {
restrict: "E",
require: "^^myTabs",
scope: {
name: "@",
job: "@"
},
link: function(scope, element, attrs, controller) {
controller.addPane(scope);
},
templateUrl: "myPane.html"
};
})

在上面的示例中,我们创建了两个指令,myTabsmyPane,在myTabs指令中,我们限定它只能以标签形式使用,开启了内嵌机制,并定义了它自己的Controller,在Controller中定义了panes变量和addPane(pane)select(pane)两个方法,方法的具体实现内容这里就不解释了,都很简单,最后指定了UI模板文件myTabs.html

myPane指令中同样限定只能以标签形式使用,指定了要引用的父节点的Controller名称,后创建了自己的隔离域,定义了namejob两个属性,并进行了字符串的单向绑定,然后定义了link函数,通过第四个参数访问到了父节点的myTabsController,并调用addPane(pane)函数,将自己的隔离域作为参数传入,最后指定了UI模板文件myPane.html

再来看看index.htmlmyTabs.htmlmyPane.html这两个模板文件:

<!--index.html-->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Demo for Directive</title>
<script src="../angular-1.5.8.js"></script>
<script src="modules.js"></script>
</head>
<body ng-app="mainModule">
<my-tabs>
<my-pane name="Jason" job="Developer"></my-pane>
<my-pane name="Green" job="Doctor"></my-pane>
</my-tabs>
</body>
</html>

<!--myTabs.html-->
<div>
<ul>
<li ng-repeat="pane in panes">
<a href="" ng-click="select(pane)">{{pane.name}}</a>
</li>
</ul>
<div id="paneContainer" ng-transclude></div>
</div>

<!--myPane.html-->
<div ng-show="selected">
I am {{name}}, my job is {{job}}!
</div>

index.html中,myTabs指令包含两个myPane指令,这两个myPane指令所显示的内容将嵌入在myTabs.html中id为paneContainer的DIV中,也就是myPane.html中的内容会被嵌入在这个DIV里。

上面这三个文件中有几个点需要注意:

  • 因为在myPane指令的隔离域中定义了namejob属性,并进行了字符串绑定,所以在index.html文件中,可以对myPane标签里的namejob属性直接进行字符串赋值。
  • 因为在myPane指令中引用了myTabs指令的Controller,并在link函数中将隔离域作为参数传给了myTabs,既myTabs指令的Controller中的select(pane)addPane(pane)函数的参数均为myPane指令的隔离域,所以在myTabs.html文件中可以直接使用pane访问myPane指令隔离域中定义的属性,比如,并且也可以在myTabs指令在myPane的隔离域中定义属性,比如pane.selected = true,给隔离域定义了selected的属性,然后可以在myPane指令中使用。
  • ng-show是AngularJS内置的指令,用于显示或隐藏指定的DOM元素。

看看运行效果:

link函数的第五个参数transcludeFn是一个函数,该函数常用的有两个参数scopefunction(clone){},既transcludeFn(scope, function(clone){})。前者是嵌入内容的作用域,与指令的隔离作用域是平行的,后者函数的参数clone是嵌入的内容的jquery封装,可以通过它对嵌入的内容进行DOM操作。

指令的其他属性

priority用于指定指令的优先级,该属性的值从1开始。当有多个指令定义在同一个DOM元素中时就需要通过该属性明确它们的执行顺序。

replace用于判定是否将UI模板的内容替换掉指令标签本身,该属性默认值为false,既保留指令标签本身,若设置为true则替换指令标签。

组建(Component)

在AngularJS 1.5中新加了组建的概念,它的意图是希望从AngularJS 1.x向AngularJS 2.0迁移时能更加平顺,AngularJS团队也提倡使用组建化模式开发Web应用。那么组建是什么呢?其实组建就是指令的一种特性形式,它规避了一些指令中晦涩难理解的东西,比如compile函数,link函数,scoperestrict等,所以组建的目的就是能让我们更为傻瓜式的创建指令,能更好的遵循组建化的开发模式,提高性能以及更容易向AngularJS 2.0迁移。

创建组建

我们可以使用Module的component方法创建组建:

var mainModule = angular.module("mainModule", []);

mainModule.component("myComponent", {
templateUrl: "myTemplate.html",
controller: function() {

},
bindings: {
name: "="
}
});

component方法的第一个参数是组建名称,命名规则和使用方法与指令一样,第二个参数和创建指令有点不同,它并不是一个函数,而是一个对象,在该对象中对组建的配置和在指令中的配置方式很类似。

我们先来看看组建和指令之间有哪些区别:

  • 组建中不提供手动配置作用域,默认的作用域就是隔离域。
  • 组建中通过bindings属性进行数据绑定,除了=@&三种绑定方式以外还增加了一种<方式,既单向绑定,但不限于字符串。从而保证了组建有自己的清晰的输入输出API。并且通过bindings对象绑定的属性直接绑定在组建的Controller上。
  • 组建的Controller默认名称为$ctrl,当然也可以使用controllerAs属性指定Controller的名称。
  • 组建只能以标签形式使用。
  • 组建中没有link函数,compile函数,priority属性,restrict属性。
  • 组件只能控制自身的输入输出,组建不允许修改属于自己隔离域以外的任何数据和DOM元素。一般情况下,AngularJS通过作用域(Scope)继承的特性支持跨层级修改数据的能力,但是如果当修改数据职责不清晰或不恰当的时候就会导致各种问题,所以这也就是组建的作用域默认都是隔离域的原因。

使用起来和指令比较类似:

// modules.js
var mainModule = angular.module("mainModule", []);

mainModule.controller("MyController", function() {
this.person = {
name: "Jason"
}
})

mainModule.component("myComponent", {
templateUrl: "myTemplate.html",
controller: function() {

},
bindings: {
person: "="
}
});
<!--index.html-->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Demo for Component</title>
<script src="../angular-1.5.8.js"></script>
<script src="modules.js"></script>
</head>
<body ng-app="mainModule">
<div ng-controller="MyController as mc">
<my-component person="mc.person"></my-component>
</div>
</body>
</html>

<!--myTemplate.html-->
<span>Name: {{$ctrl.person.name}}</span>

组建的生命周期

在组建的整个生命周期里,AngularJS提供了五个关键点的方法,可供我们监听到组建的运行状态:

  • $onInit():该方法在组件及其所有 binding 初始化之后被调用,从而我们就有了一个清晰的地方统一存放数据初始化的逻辑:
var mainModule = angular.module("mainModule", []);

mainModule.component("myComponent", {
templateUrl: "myTemplate.html",
controller: function() {
this.name = "jason";
}
});

// 将初始化数据的逻辑放在onInit方法中

mainModule.component("myComponent", {
templateUrl: "myTemplate.html",
controller: function() {
this.$onInit = function() {
this.name = "jason";
}
}
});
  • $onChanges(changesObj):当组建中单向绑定的属性值发生变化时被调用,这里要注意的是只有绑定属性值的引用发生变化时才能监听到,如果只是在指令内对属性进行修改,该方法是无法监听到的。通过该方法的参数可以获取到被修改数据当前的值、修改之前的值、是否时第一次修改:
mainModule.component("myComponent", {
templateUrl: "myTemplate.html",
controller: function() {
this.$onChanges = function(changesObj) {
if(changesObj.name) {
// name当前的值
var nameCurrentValue = changesObj.name.currentValue;
// name修改前的值
var namePreviousValue = changesObj.name.previousValue;
// 是否是第一次修改
var isFirstChange = changesObj.name.isFirstChange();
}
}
},
bindings: {
name: "<"
}
});
  • $doCheck():该方法和$onChanges(changesObj)作用类似,但是该方法可以监听到在指令内对属性进行修改的行为:
mainModule.component("myComponent", {
templateUrl: "myTemplate.html",
controller: function() {
// 当name在指令内修改时
this.name = "Green";
this.$doCheck = function() {
// doCheck方法会被调用
}
},
bindings: {
name: "<"
}
});
  • $onDestroy():当作用域被销毁时调用该方法。
  • $postLink():当指令所在标签与子标签链接时调用该方法。

组建化开发

我们先来看看示例效果:

既然是组建化开发,那么我们来看看上面这个示例有几个组建:

从上图可以看到,整个示例一共用了三个组建,其中有两个组建进行了复用,下面我们来看看每个组建是如何定义的。

personList组建

该组建主要用来初始化数据源,定义对数据源操作的函数:

mainModule.component("personList", {
templateUrl: "personList.html",
controller: function() {
this.$onInit = function() {
this.list = [{
name: "Jason",
job: "Developer"
},{
name: "Green",
job: "Doctor"
}];
};

this.updatePerson = function(person, job, value) {
person[job] = value;
};

this.deletePerson = function(person) {
var idx = this.list.indexOf(person);
if(idx >= 0) {
this.list.splice(idx, 1);
}
};
}
});

首先在$onInit函数中初始化数据源,定义了Person对象数组,然后定义了更新指定Person对象的方法updatePerson及删除指定Person对象的方法deletePerson

再来看看它的UI模板文件personList.html

<b>Person</b><br>
<person-detail ng-repeat="person in $ctrl.list" person="person" on-update="$ctrl.updatePerson(person, job, value)" on-delete="$ctrl.deletePerson(person)"></person-detail>

该文件共有两部分,第一部分是用原生HTML标签定义了标题,第二部分是使用了另外一个组建personDetailng-repeat指令是AngularJS内置的指令,作用不言而喻,就是循环数据源,同时组建也跟据循环次数增加。personon-updateon-delete是在personDetail组建中定义的数据绑定属性,用大白话解释就是,personDetail组建中的person变量与personList组建中的Person对象进行了绑定,personDetail组建中的onUpdateonDelete方法分别与personList组建中的updatePersondeletePerson方法进行了绑定。

personDetail组建

该组建主要用于展示Person对象的具体内容:

mainModule.component("personDetail", {
templateUrl: "personDetail.html",
bindings: {
person: "<",
onUpdate: "&",
onDelete: "&"
},
controller: function() {
this.update = function(job, value) {
this.onUpdate({person: this.person, job: job, value: value});
};

this.delete = function() {
this.onDelete(this.person);
}
}
});
<hr>
<div>
Name: {{$ctrl.person.name}}<br>
Job: <editable-field field-value="$ctrl.person.job" on-update="$ctrl.update('job', value)"></editable-field>
<button ng-click="$ctrl.delete()">Delete</button>
</div>

personDetail.html文件里,首先访问了person对象的name属性,将其展示出来,注意,这里由$ctrl.preson访问到的其实是单向绑定的personList组建中的person对象。而且在update函数中调用了与personList组建的updatePerson函数绑定的onUpdate函数,也就是子组建调用了父组建的方法。然后使用了第三个组建editableField,该组建同样有一些属性和方法和personDetail组建中对应的属性和方法进行了绑定。最后增加了一个按钮,并使用ng-click指令指定了按钮的点击事件。

editableField组建

该组建的主要作用是展示并修改person对象中的job属性:

mainModule.component("editableField", {
templateUrl: "editableField.html",
bindings: {
fieldValue: "<",
onUpdate: "&"
},
controller: function() {
this.$onInit = function() {
this.editMode = false;
this.fieldValueCopy = this.fieldValue;
};

this.handModelChange = function() {
if(this.editMode) {
this.onUpdate({job: "job", value: this.fieldValue});
this.fieldValueCopy = this.fieldValue;
}
this.editMode = !this.editMode;
};

this.reset = function() {
this.fieldValue = this.fieldValueCopy;
};
}
});

从最开始的运行效果中可以看到editableField是有形态变化的,所以在$onInit函数中定义了是否为编辑模式的标识符editMode以及代表输入框内容的fieldValue变量,因为有reset功能,所以还定义存储修改之前值的变量fieldValueCopy。然后定义了点击EditSave按钮触发的函数handModelChange,并在该函数中调用了和personDetail组建的update函数绑定的onUpdate函数,同样由子组建调用了父组建的方法。还定义了点击Reset按钮触发的函数reset

<span ng-switch="$ctrl.editMode">
<input ng-switch-when="true" type="text" ng-model="$ctrl.fieldValue">
<span ng-switch-default>{{$ctrl.fieldValue}}</span>
</span>
<button ng-click="$ctrl.handModelChange()">{{$ctrl.editMode ? "Save" : "Edit"}}</button>
<button ng-if="$ctrl.editMode" ng-click="$ctrl.reset()">Reset</button>

editableField.html文件中展示了person对象的job属性,定义了修改job属性的输入框以及两个按钮。这里出现了一组之前没见过的AngularJS内置指令,ng-switchng-switch-whenng-switch-default,这三个指令一般组合使用,作用类似if else语句,通过这组指令和deitMode变量就可以达到动态变换DOM元素的功能。

最后来看看简单的index.html文件:

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Demo for Directive</title>
<script src="../angular-1.5.8.js"></script>
<script src="modules.js"></script>
</head>
<body ng-app="mainModule">
<person-list></person-list>
</body>
</html>

从上面的这个示例中可以看出在editableFieldpersonDetail组建中都没有真正意义上去修改数据源,而是通过函数绑定一路将修改数据源的行为传递到了定义数据源的组建personList中,由它最后真正完成对数据源的修改,这也遵循了组建不允许修改属于自己隔离域以外的任何数据和DOM元素的原则。

总结

这篇文章是对AngularJS 1.x的入门学习小记,对一些基本概念的介绍,如果想要深入了解AngularJS可以去读官方的文档API。此刻AngularJS 2.0已正式发布,掌握AngularJS 1.x的基本知识能更快的帮助我们迈入AngularJS 2.0的怀抱,让我们不忘AngularJS 1.x初心,去拥抱AngularJS 2.0的美好未来。

什么是ShadowSocks

ShadowSocks是基于Socks5协议,使用类似SSH隧道的方式收发网络请求的一款开源软件,该软件由 @clowwindy 开发,最初只有Python版本,随后出现C++、C#、Go等多语言版本。ShadowSocks最大的特点是可以保护网络流量、加密数据传输,可有效防止GFW(Great Firewall of China)封杀网络请求。

用ShadowSocks科学上网的概念

在以前,我们访问互联网的资源都是简单而直接的,用户的请求发送到资源服务方,比如Google、Facebook等,然后资源服务方直接将内容响应给用户,世界多么美好。

但是,在1998年时候,中国创建了互联网边界审查系统,称之为中国国家防火墙(GFW),这堵墙横在了用户和互联网资源服务方之间,用于监控和过滤互联网国际出口上的内容,监控国际网关的通讯,对认为不匹配国家官方要求的传输内容,进行干扰、阻断、屏蔽。

从此之后好多有价值的网站就被堵在了墙后。

但是由于对知识的渴望,人们想到了绕过GFW的办法,那就是在境外搭建一个国内用户的代理,国内用户与代理之间建立加密的通道,由境外代理请求被墙的网络资源,再通过加密通道返回给国内用户。代理的类型也有多种,像HTTP、Socks、VPN、SSH等。以SSH隧道为例:

因为SSH本身基于RSA加密技术,所以GFW就无法对数据传输过程加密的数据进行分析,从而避免被重置链接、阻断、屏蔽等问题。

但是GFW也不会懵B一世,人家也会学习,由于在创建SSH隧道的过程中有较为明显的特性,所以GFW还是可以通过分析连接的特性进行干扰。此时ShadowSocks横空出世,先看看图示:

简单来说,ShadowSocks分为客户端和服务端,用户发出的请求基于Socks5协议与ShadowSocks客户端进行通信,一般情况下SS客户端都在本机,通过ShadowSocksX、GoAgentX等应用启动,所以这一步是不会经过GFW的,然后ShadowSocks提供了多种加密方式供客户端和服务端之间进行通信,并且在经过GFW时是普通的TCP协议数据包,没有明显的特征,而且GFW也无法解密分析,从而实现绕墙访问资源。

搭建ShadowSocks服务器

根据上文描述的ShadowSocks的实现原理,我们首先需要搭建一个境外的ShadowSocks服务器,这部分也是唯一需要我们花点小钱的地方。

以下文章内容均在MacOS下实践。

购买VPS

我使用的是Vultr的VPS,他家的VPS可选的地域较为丰富,而且有日本东京,这对于家里是联通网络的比较亲和,速度较好:

在系统和应用方面Vultr提供的种类也比较丰富:


在价格方面,Vultr提供的最低价格为每月5美元,对应的性能足够满足ShadowSocks服务端的需求:

注册Vultr账号,绑定信用卡,选择自己中意的系统和价格后立即就会为你部署好VPS:

当VPS的状态为Running后,就表示VPS已经启动了,然后为该VPS配置我们的SSH公钥:

大家可以看看北京联通ping东京VPS的速度,还是很给力的:

使用该链接注册可获得20美元的优惠卷。

安装ShadowSocks

我们使用Python版的ShadowSocks进行安装,首先需要下载pip-8.1.2.tar.gz,然后使用工具将其上传至VPS,我使用的是Commander One,通过SFTP连接VPS:

使用Terminal连接VPS,一步一步输入以下命令解压安装pip:

tar -xzvf pip-8.1.2.tar.gz
cd pip-8.1.2
python setup.py install

然后通过pip安装ShadowSocks:

pip install shadowsocks

成功后可以看到如下信息:

Collecting shadowsocks
  Downloading shadowsocks-2.8.2.tar.gz
Installing collected packages: shadowsocks
  Running setup.py install for shadowsocks ... done
Successfully installed shadowsocks-2.8.2

配置ShadowSocks

进入到etc目录下,创建shadowsocks.json文件,并添加如下内容:

{
    "server":"你的VPS的IP",
    "server_port":8388,
    "local_port":1080,
    "password":"设置一个连接密码",
    "timeout":600,
    "method":"aes-256-cfb",
    "auth": true
}

该文件就是ShadowSocks的配置文件。
然后输入如下命令启动ShadowSocks服务:

ssserver -c /etc/shadowsocks.json -d start

最后清空防火墙规则:

iptables -F

至此ShadowSocks服务端就配置完成并可以使用了。

ShadowSocks客户端

Mac上ShadowSocks客户端的选择无非就是ShadowSocksX、GoAgentX、Surge这三个,前两者配置起来都大同小异,ShadowSocksX本身就是专门开启SS客户端服务的应用:

GoAgentX提供的服务类型更多,包括SS服务:

配置项都是一样的,把ShadowSocks服务端的配置信息一一对应就可以了。

Surge启SS服务的方式有点不同,因为应用在界面操作上没有提供创建SS服务的选项,但其实它是支持SS服务的,可能因为怕被请喝茶吧。所以需要在Surge的配置文件里添加SS服务相关的信息,如何配置网上有很多,这里就不再累赘了。

至此就打通了ShadowSocks客户端与服务端的通道,可以畅游墙外的世界了。

突破公司内网

一般情况在家我们在本机启着ShadowSocks客户端就可以了,但是如果在公司上外网需要走HTTP代理的话就不行了,因为ShadowSocks客户端需要走公司提供的代理才能连接到服务端,而上文中的客户端应用统统不支持额外代理功能,所以此时就需要用到另外一个工具Proxifier,该工具的作用就是接管运行中的所有应用发出的所有请求,然后通过代理进行转发,这样就可以让不支持代理的应用也可以走代理了。

打开Proxifier,选择Proxies选项卡设置公司的HTTP代理以及本地Socks5代理:


然后启动ShadowSocks客户端,比如启动GoAgentX,此时选择Proxifier的Rules选项卡,就会发现Proxifier自动捕获到了名为ss-local的应用,就是由GoAgentX开启的SS客户端,将其的代理规则设置为之前创建好的公司代理:

然后将Default的代理规则设置为之前创建好的本地Socks5代理:

这样就可以让ShadowSocks客户端通过公司代理连接服务端了:

你还可以设置其他的规则,让特定的应用走特定的代理,比如我让Git还是走了公司代理,这样Git服务就会从Default规则里剔除掉,相当于加了走SS服务的例外。

本文首发CSDN,如需转载请与CSDN联系。

通过Dispatch Queue执行任务

如果想让Dispatch Queue执行任务,首先就是得将任务放入队列中,我们可以异步的将任务加入队列,也可以同步的将任务加入队列,可以一个任务一个任务的加,也可以一组一组的加。这节我们就来看看将任务加入队列的那些事。

向队列添加任务

我们可以使用dispatch_async或者dispatch_async_f函数异步的向队列中添加任务,也就是说当我们添加完任务后该函数会立即返回,我们不需要等待任务执行完成,而且我们也不会知道队列到底何时开始执行任务。dispatch_async函数有两个参数,一个是目标队列,类型为dispatch_queue_t,另一个是闭包,类型为dispatch_block_t

let serialQueue = dispatch_queue_create("com.example.MySerialQueue", nil)

dispatch_async(serialQueue, {

print("Task in the queue...")

})

dispatch_async_f函数有三个参数,第一个是类型为dispatch_queue_t的目标队列,第二个是队列上下文指针,第三个是类型为dispatch_function_t的任务函数,队列上下文指针为该函数的唯一参数:

class AddTaskToQueue {

func launch() {

let serialQueue = dispatch_queue_create("com.example.MySerialQueue", nil)

dispatch_async(serialQueue, {

print("Task in the queue...")

})

dispatch_async_f(serialQueue, unsafeBitCast(0, UnsafeMutablePointer<Int>.self), taskFunction())

sleep(3)

}

func taskFunction() -> dispatch_function_t {

return { context in

print("Do some work with context...")

}

}

}

let addTaskToQueue = AddTaskToQueue()
addTaskToQueue.launch()

除了这两个函数,我们还可以使用dispatch_syncdispatch_sync_f函数同步的向队列中添加任务,并且我们要等待直到任务执行完成。这两个函数和上面的异步添加任务函数用法完全一致。

那么什么时候用异步什么时候用同步呢,大多数情况下我们都是在主线程中使用GCD分派任务,为了避免阻塞主线程,影响用户体验,所以通常情况下我们都使用异步添加任务的方式。当然为了避免任务与主线程中产生资源竞争的问题,有时候酌情也会使用同步添加任务的方式。

Dispatch Queue的Completion Block

还记得NSOperationcompletionBlock属性吗,这个回调函数在任务执行完成后调用,用于处理有些后续工作或者消息通知。在Dispatch Queue中并没有类似的属性,但是我们可以通过其他方式来实现。举一个很常见的应用场景,我们在主线程中分派一个下载图片的任务,让其在二级线程中执行,当图片下载完成后通知主线程,并由主线程将图片显示出来,我们看看简单的代码片段:

class DownloadImage {

func dispatchTaskInMainThread() {

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), downloadImage())

}

func downloadImage() -> (() -> ()) {

return {

print("Downloading image in \(NSThread.currentThread())")

dispatch_async(dispatch_get_main_queue()) {

print("Handle image and display in \(NSThread.currentThread())")

}

}

}

}

我们来看看上面代码都做了些什么,首先在dispatchTaskInMainThread方法中,我们使用dispatch_get_global_queue函数获取到全局并发队列,然后将downloadImage下载图片的方法作为任务添加到该全局队列中。在downloadImage方法里,当图片下载完成后通过dispatch_get_main_queue函数获取到主队列,也就是在主线程中对图片进行处理,这样我们就达到了Completion Block的效果。

在队列中循环执行任务

在我们的日常开发中,经常会使用到for循环来处理一些任务,而且这些任务之间也并没有先后顺序的关联,每个任务相对比较独立。遇到这种情况,我们可以用dispatch_applydispatch_apply_f函数让任务在队列中循环执行,并且可以是并发执行,这样相比for循环的串行执行要更加效率:

// for循环
let arr = ["Swift", "Objective-C", "Java", "Delphi", "C++"]

for element in arr {

print("Handle element. the element is \(element)")

}

// dispatch_apply
dispatch_apply(arr.count, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)){ index in

print("Handle element. the element is \(arr[index]). Current thread is \(NSThread.currentThread())")

}

从上面示例代码片段中可以看到,dispatch_apply函数有三个参数,第一个参数是循环次数,第二个参数是目标队列,第三个则是要执行的闭包任务,循环次数是该闭包的唯一参数。

暂停和重启队列

在Dispatch Queue执行任务时,如果我们想暂停队列,可以使用dispatch_suspend函数,重新让队列执行任务可以使用dispatch_resume。这里要注意的是暂停队列只是让队列暂时停止执行下一个任务,而不是中断当前正在执行的任务。

Dispatch Group的使用

在实际开发中,为了提升性能我们或许会经常使用dispatch_async异步的将任务添加进队列去执行,但有些时候需要之前队列中的多个任务都执行完成之后,才能获取到正确的或者说想要的结果供后续逻辑代码使用,遇到这种情况,就可以使用Dispatch Group,将多个任务在队列中归为一个组,并可以使用dispatch_group_wait函数让之后的逻辑代码等待,直到该组的任务都执行完成后再执行。

var count = 0

let concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)

dispatch_async(concurrentQueue) {

print("Task1 in dispatchGroup...")

sleep(2)

count += 1

}

dispatch_async(concurrentQueue) {

print("Task2 in dispatchGroup...")

sleep(3)

count += 1

}

dispatch_async(concurrentQueue) {

print("Task3 in dispatchGroup...")

sleep(1)

count += 1

}

print("I expect the count is 3, and the factual count is \(count)")

上面的代码片段就是我刚才描述的场景,因为使用的是并发队列,也不好在每个任务里进行回调处理,所以我们永远不会得到正确的count。如果我们使用Dispatch Group事情就简单多了:

var count = 0

let concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)

let dispatchGroup = dispatch_group_create()

dispatch_group_async(dispatchGroup, concurrentQueue) {

print("Task1 in dispatchGroup...")

sleep(2)

count += 1

}

dispatch_group_async(dispatchGroup, concurrentQueue) {

print("Task2 in dispatchGroup...")

sleep(3)

count += 1

}

dispatch_group_async(dispatchGroup, concurrentQueue) {

print("Task3 in dispatchGroup...")

sleep(1)

count += 1

}

dispatch_group_wait(dispatchGroup, DISPATCH_TIME_FOREVER)

print("I expect the count is 3, and the factual count is \(count)")

上面的代码中,先使用dispatch_group_create函数创建Dispatch Group,然后使用dispatch_group_async函数将任务分进组里,然后再添加进队列中。该函数有三个参数,分别是Dispatch Group、Dispatch Queue和要执行任务的闭包。当添加完任务后使用dispatch_group_wait函数等待,直到指定组的任务全部完成,才会继续执行后面的打印语句,该函数有两个参数,第一个是目标组,第二个是等待时间DISPATCH_TIME_NOWDISPATCH_TIME_FOREVER

Dispatch Source

前面的文章中介绍过Dispatch Source:

Dispatch Source是GCD中的一个基本类型,从字面意思可称为调度源,它的作用是当有一些特定的较底层的系统事件发生时,调度源会捕捉到这些事件,然后可以做其他的逻辑处理,调度源有多种类型,分别监听对应类型的系统事件。我们来看看它都有哪些类型:

  • Timer Dispatch Source:定时调度源。
  • Signal Dispatch Source:监听UNIX信号调度源,比如监听代表挂起指令的SIGSTOP信号。
  • Descriptor Dispatch Source:监听文件相关操作和Socket相关操作的调度源。
  • Process Dispatch Source:监听进程相关状态的调度源。
  • Mach port Dispatch Source:监听Mach相关事件的调度源。
  • Custom Dispatch Source:监听自定义事件的调度源。

这一节就来看看如何使用Dispatch Source。

用通俗一点的话说就是用GCD的函数指定一个希望监听的系统事件类型,再指定一个捕获到事件后进行逻辑处理的闭包或者函数作为回调函数,然后再指定一个该回调函数执行的Dispatch Queue即可,当监听到指定的系统事件发生时会调用回调函数,将该回调函数作为一个任务放入指定的队列中执行。也就是说当监听到系统事件后就会触发一个任务,并自动将其加入队列执行,这里与之前手动添加任务的模式不同,一旦将Diaptach Source与Dispatch Queue关联后,只要监听到系统事件,Dispatch Source就会自动将任务(回调函数)添加到关联的队列中。

有些时候回调函数执行的时间较长,在这段时间内Dispatch Source又监听到多个系统事件,理论上就会形成事件积压,但好在Dispatch Source有很好的机制解决这个问题,当有多个事件积压时会根据事件类型,将它们进行关联和结合,形成一个新的事件。

监听事件类型

Dispatch Source一共可以监听六类事件,分为11个类型,我们来看看都是什么:

  • DISPATCH_SOURCE_TYPE_DATA_ADD:属于自定义事件,可以通过dispatch_source_get_data函数获取事件变量数据,在我们自定义的方法中可以调用dispatch_source_merge_data函数向Dispatch Source设置数据,下文中会有详细的演示。
  • DISPATCH_SOURCE_TYPE_DATA_OR:属于自定义事件,用法同上面的类型一样。
  • DISPATCH_SOURCE_TYPE_MACH_SEND:Mach端口发送事件。
  • DISPATCH_SOURCE_TYPE_MACH_RECV:Mach端口接收事件。
  • DISPATCH_SOURCE_TYPE_PROC:与进程相关的事件。
  • DISPATCH_SOURCE_TYPE_READ:读文件事件。
  • DISPATCH_SOURCE_TYPE_WRITE:写文件事件。
  • DISPATCH_SOURCE_TYPE_VNODE:文件属性更改事件。
  • DISPATCH_SOURCE_TYPE_SIGNAL:接收信号事件。
  • DISPATCH_SOURCE_TYPE_TIMER:定时器事件。
  • DISPATCH_SOURCE_TYPE_MEMORYPRESSURE:内存压力事件。

创建Dispatch Source

我们可以使用dispatch_source_create函数创建Dispatch Source,该函数有四个参数:

  • type:第一个参数用于标识Dispatch Source要监听的事件类型,共有11个类型。
  • handle:第二个参数是取决于要监听的事件类型,比如如果是监听Mach端口相关的事件,那么该参数就是mach_port_t类型的Mach端口号,如果是监听事件变量数据类型的事件那么该参数就不需要,设置为0就可以了。
  • mask:第三个参数同样取决于要监听的事件类型,比如如果是监听文件属性更改的事件,那么该参数就标识文件的哪个属性,比如DISPATCH_VNODE_RENAME
  • queue:第四个参数设置回调函数所在的队列。
let dispatchQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)

let dispatchSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_ADD, 0, 0, dispatchQueue)

上面的代码就是创建Dispatch Source的简单示例。

设置事件处理器

前文中提到过,当Dispatch Source监听到事件时会调用指定的回调函数或闭包,该回调函数或闭包就是Dispatch Source的事件处理器。我们可以使用dispatch_source_set_event_handlerdispatch_source_set_event_handler_f函数给创建好的Dispatch Source设置处理器,前者是设置闭包形式的处理器,后者是设置函数形式的处理器:

dispatch_source_set_event_handler(dispatchSource, {

print("Dispatch Source 事件处理器...")

})

// 根据闭包尾随的特性,还可以有下面的写法

dispatch_source_set_event_handler(dispatchSource) {

print("Dispatch Source 事件处理器...")

}

从上面示例代码中可以看到,该函数有两个参数,第一个是设置目标Dispatch Source,第二个参数就是设置处理器了。

既然是事件处理器,那么肯定需要获取一些Dispatch Source的信息,GCD提供了三个在处理器中获取Dispatch Source相关信息的函数,比如handlemask。而且针对不同类型的Dispatch Source,这三个函数返回数据的值和类型都会不一样,下面来看看这三个函数:

  • dispatch_source_get_handle:这个函数用于获取在创建Dispatch Source时设置的第二个参数handle
    • 如果是读写文件的Dispatch Source,返回的就是描述符。
    • 如果是信号类型的Dispatch Source,返回的是int类型的信号数。
    • 如果是进程类型的Dispatch Source,返回的是pid_t类型的进程id。
    • 如果是Mach端口类型的Dispatch Source,返回的是mach_port_t类型的Mach端口。
  • dispatch_source_get_data:该函数用于获取Dispatch Source监听到事件的相关数据。
    • 如果是读文件类型的Dispatch Source,返回的是读到文件内容的字节数。
    • 如果是写文件类型的Dispatch Source,返回的是文件是否可写的标识符,正数表示可写,负数表示不可写。
    • 如果是监听文件属性更改类型的Dispatch Source,返回的是监听到的有更改的文件属性,用常量表示,比如DISPATCH_VNODE_RENAME等。
    • 如果是进程类型的Dispatch Source,返回监听到的进程状态,用常量表示,比如DISPATCH_PROC_EXIT等。
    • 如果是Mach端口类型的Dispatch Source,返回Mach端口的状态,用常量表示,比如DISPATCH_MACH_SEND_DEAD等。
    • 如果是自定义事件类型的Dispatch Source,返回使用dispatch_source_merge_data函数设置的数据。
  • dispatch_source_get_mask:该函数用于获取在创建Dispatch Source时设置的第三个参数mask。在进程类型,文件属性更改类型,Mach端口类型的Dispatch Source下该函数返回的结果与dispatch_source_get_data一样。

注册Cancellation Handler

Cancellation Handler就是当Dispatch Source被释放时用来处理一些后续事情,比如关闭文件描述符或者释放Mach端口等。我们可以使用dispatch_source_set_cancel_handler函数或者dispatch_source_set_cancel_handler_f函数给Dispatch Source注册Cancellation Handler:

dispatch_source_set_cancel_handler(dispatchSource) {

print("进行善后处理...")

}

该函数有两个参数,第一个参数是目标Dispatch Source,第二个参数就是要进行善后处理的闭包或者函数。

更改Dispatch Source的目标队列

在上文中,我们说过可以使用dispatch_source_create函数创建Dispatch Source,并且在创建时会指定回调函数执行的队列,那么如果事后想更改队列,比如说想更改队列的优先级,这时我们可以使用dispatch_set_target_queue函数实现:

let dispatchQueueDefaultPriority = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)

let dispatchSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_ADD, 0, 0, dispatchQueueDefaultPriority)

let dispatchQueueLowPriority = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0)

dispatch_set_target_queue(dispatchSource, dispatchQueueLowPriority)

这里需要注意的是,如果在更改目标队列时,Dispatch Source已经监听到相关事件,并且回调函数已经在之前的队列中执行了,那么会一直在旧的队列中执行完成,不会转移到新的队列中去。

暂停恢复Dispatch Source

暂停和恢复Dispatch Source与Dispatch Queue一样,都适用dispatch_suspenddispatch_resume函数。这里需要注意的是刚创建好的Dispatch Source是处于暂停状态的,所以使用时需要用dispatch_resume函数将其启动。

废除Dispatch Source

如果我们不再需要使用某个Dispatch Source时,可以使用dispatch_source_cancel函数废除,该函数只有一个参数,那就是目标Dispatch Source。

Dispatch Source实践

说了这么多,这一节来看看Dispatch Source到底怎么用。

用Dispatch Source监听定时器

Dispatch Source能监听的事件中有一个类型就是定时器,我们来看看如何实现:

class TestDispatchSource {

func launch() {

let dispatchQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)

let timer = createTimerDispatchSource(dispatch_time(DISPATCH_TIME_NOW, 0), interval: NSEC_PER_SEC * 5, leeway: 0, queue: dispatchQueue) {

print("处理定时任务,该任务每5秒执行一次...")

}

dispatch_resume(timer)

sleep(30)

}

func createTimerDispatchSource(startTime: dispatch_time_t, interval: UInt64, leeway: UInt64, queue: dispatch_queue_t, handler: dispatch_block_t) -> dispatch_source_t {

let timerDispatchSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue)

dispatch_source_set_timer(timerDispatchSource, startTime, interval, leeway)

dispatch_source_set_event_handler(timerDispatchSource, handler)

return timerDispatchSource

}

}

上面的代码示例中一个新的函数dispatch_source_set_timer,该函数的作用就是给监听事件类型为DISPATCH_SOURCE_TYPE_TIMER的Dispatch Source设置相关属性,该函数有四个参数:

  • source:该参数为目标Dispatch Source,类型为dispatch_source_t.
  • start:该参数为定时器的起始时间,类型为dispatch_time_t
  • interval:该参数为定时器的间隔时间,类型为UInt64,间隔时间的单位是纳秒。
  • leeway:该参数为间隔时间的精度,类型为UInt64,时间单位也是纳秒。

用Dispatch Source监听自定义事件

Dispatch Source能监听的事件中有一个类型是自定义事件,下面我们来看看如何使用:

class TestDispatchSource {

func launch() {

var totalProcess = 0

let dispatchSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_ADD, 0, 0, dispatch_get_main_queue())

dispatch_source_set_event_handler(dispatchSource) {

let process = dispatch_source_get_data(dispatchSource)

totalProcess += Int(process)

print("这里可以在主线程更新UI,显示进度条...进度为\(totalProcess)%")
}

dispatch_resume(dispatchSource)

generateCustomEvent(dispatchSource)

}

func generateCustomEvent(dispatchSource: dispatch_source_t) {

let queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)

for index in 0...100 {

dispatch_sync(queue) {

print("模拟自定义事件...进度为\(index)%")

dispatch_source_merge_data(dispatchSource, 1)

sleep(2)

}

}

}

}

我们来看看generateCustomEvent(dispatchSource: dispatch_source_t)方法,该方法的作用的是模拟自定义事件,首先创建一个全局并发队列,然后循环让其执行任务,在执行的任务里调用dispatch_source_merge_data函数,就可以触发监听类型为DISPATCH_SOURCE_TYPE_DATA_ADD或者DISPATCH_SOURCE_TYPE_DATA_OR的Dispatch Source。该函数有两个参数,第一个参数是目标Dispatch Source,第二个参数的类型是无符号长整型,用于向目标Dispatch Source中的对应变量追加指定的数。

我们再来看看如何监听自定义时间,首先创建类型为DISPATCH_SOURCE_TYPE_DATA_ADD的Dispatch Source,然后设置回调闭包,在闭包中使用dispatch_source_get_data获取追加的变量值,该函数只有一个参数,就是目标Dispatch Source,这里需要注意的是通过dispatch_source_get_data函数获取的变量值并不是累加值,而是每次调用dispatch_source_merge_data函数时设置的值,所以在上面的示例中用totalProcess变量累加每次获取到的值。

上面的示例可以用来模拟后台进行下载,根据下载的数据量使用dispatch_source_merge_data函数给目标Dispatch Source设置相应的变量值,然后在主线程中监听到Dispatch Source的自定义事件,通过dispatch_source_get_data函数获取到变量,用于更新显示进度条的UI。

本文首发CSDN,如需转载请与CSDN联系。

Operation对象的相关设置

Operation对象除了上文中讲到到基本使用方法外还有一些其他的特性,这些特性需要根据我们的应用场景去设置,设置的时机在创建Operation对象之后和运行它或者将其放入操作队列之前,下面让我们来看看Operation对象还有哪些特性。

Operation对象之间的依赖

与GCD不同,Operation Queue不遵循先进先出的原则,而且Operation Queue始终是并发执行Operation对象的,所以想让Operation对象串行执行就需要用它的Operation对象依赖特性,该特性可以让Operation对象将自己与另外一个Operation对象进行关联,并且当关联的Operation对象执行完成后才可以执行,这样就达到了串行执行Operation对象的目的。

我们可以用NSOperationaddDependency方法添加依赖的Operation对象,而且产生依赖的这两个Operation对象并不要求必须在相同的操作队列中,但是这种依赖只能是单向的,不能相互依赖。

import Foundation

class TestOperationDependency {

func launch() {

let blockOperationA = NSBlockOperation(block: {

print("Task in blockOperationA...")

sleep(3)

})

let blockOperationB = NSBlockOperation(block: {

print("Task in blockOperationB...")

sleep(5)

})

blockOperationA.addDependency(blockOperationB)

let operationQueue = NSOperationQueue()

operationQueue.addOperation(blockOperationA)

operationQueue.addOperation(blockOperationB)

sleep(10)

}

}

let testOperationDependency = TestOperationDependency()

testOperationDependency.launch()

上面的示例代码展示了如何给Operation对象添加依赖,大家可以注释掉blockOperationA.addDependency(blockOperationB)这一行看看打印结果有什么区别。

Operation对象的优先级

上文中说了,操作队列里的Operation对象都是并发执行的,如果一个操作队列中有多个Operation对象,那么谁先执行谁后执行取决于Operation对象的依赖Operation对象是否已执行完成,也就是是否处于准备执行的状态。其实Operation对象自身也有优先级的属性,如果有两个都处于准备执行状态的Operation对象,那么优先级高的会先执行,优先级低的后执行。每个Operation对象默认的优先级是NSOperationQueuePriority.Normal级别,我们可以通过设置queuePriority属性更改Operation的在队列中执行的优先级,优先级别有以下五种:

  • NSOperationQueuePriority.Normal:正常优先级
  • NSOperationQueuePriority.Low:低优先级
  • NSOperationQueuePriority.VeryLow:非常低优先级
  • NSOperationQueuePriority.High:高优先级
  • NSOperationQueuePriority.VeryHigh:非常高优先级

这里我们需要注意一下Operation对象优先级的作用域,它只能作用于相同的操作队列中,不同操作队列中的Operation对象是不受优先级影响的。另外需要注意的是,如果有两个Operation对象,一个处于准备执行状态,但优先级比较低,另一个处于等待状态,但优先级比较高,那么此时仍然是处于准备执行状态的低优先级Operation对象先执行。可见Operation对象的优先级相互影响需要满足两个条件,一是必须处在同一个操作队列中,另一个是Operation对象都处于准备执行状态。

通过Operation对象修改线程优先级

通常情况下,线程的优先级由内核自己管理,不过在OS X v10.6及以后的版本和iOS4到iOS7期间,NSOperation多了一个threadPriority属性,我们可以通过该属性设置Operation对象运行所在线程的优先级,数值范围为0.0到1.0,数字越高优先级越高。不过可能是出于线程安全等方面的考虑,Apple从iOS8开始废除了该属性。

设置Completion Block

上篇文章中说过,Operation对象其中的一个特别好的特性就是完成时回调闭包Completion Block。它的作用不言而喻,就是当主要任务执行完成之后做一些收尾的处理工作,我们可以设置completionBlock属性给Operation对象添加完成时回调闭包:

blockOperationA.completionBlock = {

print("blockOperationA has finished...")

}

执行Operation对象

虽然前面文章的示例中已经包含了对Operation对象的执行,但是并没详细说明,这节就说说Operation对象的执行。

使用Operation Queue

使用Operation Queue操作队列执行Operation对象已然是标配选项了,操作队列在Cocoa框架中对应的类是NSOperationQueue,一个操作队列中可以添加多个Operation对象,但一次到底添加多少Operation对象得根据实际情况而定,比如应用程序对内存的消耗情况、内核的空闲情况等,所以说凡事得有度,不然反而会适得其反。另外需要注意的一点是不论有多少个操作队列,它们都受制于系统的负载、内核空闲等运行情况,所以说并不是说再创建一个操作队列就能执行更多的Operation对象。

在使用操作队列时,我们首先要创建NSOperationQueue的实例:

let operationQueue = NSOperationQueue()

然后通过NSOperationQueueaddOperation方法添加Operation对象:

operationQueue.addOperation(blockOperationA)

operationQueue.addOperation(blockOperationB)

在OS X v10.6之后和iOS4之后,我们还可以用addOperations:waitUntilFinished:方法添加一组Operation对象:

operationQueue.addOperations([blockOperationA, blockOperationB], waitUntilFinished: false)

该方法有两个参数:

  • ops: [NSOperation]:Operation对象数组。
  • waitUntilFinished wait: Bool:该参数标示这个操作队列在执行Operation对象时是否会阻塞当前线程。

我们还可以通过addOperationWithBlock方法向操作队列中直接添加闭包,而不需要去创建Operation对象:

operationQueue.addOperationWithBlock({

print("The block is running in Operation Queue...")

})

除了以上这几种添加Operation对象的方法外,还可以通过NSOperationQueuemaxConcurrentOperationCount属性设置同时执行Operation对象的最大数:

operationQueue.maxConcurrentOperationCount = 2

如果设置为1,那么不管该操作队列中添加了多少Operation对象,每次都只运行一个,而且会按照添加Operation对象的顺序去执行。所以如果遇到添加到操作的队列的Operation对象延迟执行了,那么通常会有两个原因:

  • 添加的Operation对象数超过了操作队列设置的同时执行Operation对象的最大数。
  • 延迟执行的Operation对象在等待它依赖的Operation对象执行完成。

另外需要的注意的是当Operation对象添加到操作队列中后,不要再更改它任务中涉及到的任何属性或者它的依赖,因为到操作队列中的Operation对象随时会被执行,所以如果你自以为它还没有被执行而去修改它,可能并不会达到你想要的结果。

手动执行Operation对象

除了用操作队列来执行Operation对象以外,我们还可以手动执行某个Operation对象,但是这需要我们注意更多的细节问题,也要写更多的代码去确保Operation对象能正确执行。在上篇文章中,我们创建过自定义的Operation对象,其中我们知道有几个属性特别需要我们注意,那就是readyconcurrentexecutingfinishedcancelled,对应Operation对象是否出于准备执行状态、是否为异步并发执行的、是否正在执行、是否已经执行完成、是否已被终止。这些状态在我们使用操作队列时都不需要理会,都有操作队列帮我们把控判断,确保Operation对象的正确执行,我们只需要在必要的时候获取状态信息查看而已。但是如果手动执行Operation对象,那么这些状态都需要我们来把控,因为你手动执行一个Operation对象时要判断它的依赖对象是否执行完成,是否被终止了等等,所以并不是简单的调用start方法,下面来看看如果正确的手动执行Operation对象:

func performOperation(operation: NSOperation) -> Bool {

var result = false

if operation.ready && !operation.cancelled {

if operation.concurrent {

operation.start()

} else {

NSThread.detachNewThreadSelector("start", toTarget: operation, withObject: nil)

}

result = true

}

return result

}

终止Operation对象执行

一旦Operation对象被添加到操作队列中,这个Operation对象就属于这个操作队列了,并且不能被移除,唯一能让Operation对象失效的方法就是通过NSOperationcancel方法终止它执行,或者也可以通过NSOperationQueuecancelAllOperations方法终止在队列中的所有Operation对象。

暂停和恢复操作队列

在实际运用中,如果我们希望暂停操作队列执行Operation对象,可以通过设置NSOperationQueuesuspended属性为false来实现,不过这里要注意的是暂停操作队列只是暂停执行下一个Operation对象,而不是暂停当前正在执行的Operation对象,将suspended属性设置为true后,操作队列则恢复执行。

Dispatch Queues

Dispatch Queue是GCD中的核心功能,它能让我们很方便的异步或同步执行任何被封装为闭包的任务,它的运作模式与Operation Queue很相似,但是有一点不同的是Dispatch Queue是一种先进先出的数据结构,也就是执行任务的顺序永远等同于添加任务时的顺序。GCD中已经为我们提供了几种类型的Dispatch Queue,当然我们也可以根据需求自己创建Dispatch Queue,下面我们先来看看Dispatch Queue的类型:

  • 串行Dispatch Queue:该类型的队列一次只能执行一个任务,当前任务完成之后才能执行下一个任务,而且可依任务的不同而在不同的线程中执行,这类队列通常作为私有队列使用。这里需要注意的是虽然该类型的队列一次只能执行一个任务,但是可以让多个串行队列同时开始执行任务,达到并发执行的任务的目的。
  • 并行Dispatch Queue:该类队列可同时执行多个任务,但是执行任务的顺序依然是遵循先进先出的原则,同样可依任务的不同而在不同的线程中执行,这类队列通常作为全局队列使用。
  • 主Dispatch Queue:该类队列实质上也是一个串行队列,但是该队列是一个全局队列,在该队列中执行的任务都是在当前应用的主线程中执行的。通常情况下我们不需要自己创建此类队列。

Dispatch Queue与Operation Queue相似,都能让我们更方便的实现并发任务的编程工作,并且能提供更优的性能,因为我们不再需要编写关于线程管理相关的一大堆代码,这些完全都有系统接管,我们只需要将注意力放在要执行的任务即可。举个简单的例子,如果有两个任务需要在不同的线程中执行,但是他们之间存在资源竞争的情况,所以需要保证执行的先后顺序,如果我们自己创建线程实现该场景,那么就务必要用的线程锁机制,确保任务有正确的执行顺序,这势必对系统资源的开销会非常大,如果使用Dispatch Queue,我们只需要将任务安正确的顺序添加到串行队列中即可,省时省力省资源。

任务的载体是闭包

在使用Dispatch Queue时,需要将任务封装为闭包。闭包就是一个函数,或者一个指向函数的指针,加上这个函数执行的非局部变量,闭包最大的一个特性就是可以访问父作用域中的局部变量。我们在将任务封装为闭包进行使用时要注意以下这几点:

  • 虽然在闭包中可以使用父作用域中的变量,但是尽可能少的使用父作用域中比较大的变量以及不要在闭包中做类似删除清空父作用域中变量的行为。
  • 当将一个封装好任务的闭包添加至Dispatch Qeueu中,Dispatch Queue会自动复制该闭包,并且在执行完成后释放该闭包,所以不同担心闭包中一些值的变化问题,以及资源释放问题。
  • 虽然使用Dispatch Queue执行并发异步任务很方便,但是创建和执行闭包还是有一定资源开销的,所以尽量不要使用Dispatch Queue执行一些很小的任务,要物有所值。如果确实有很小的任务需要并发异步执行,那么使用NSThreaddetachNewThreadSelector方法或NSObjectperformSelectorInBackground方法去执行也未必不可。
  • 如果同一个队列中的多个任务之间需要共享数据,那么应该使用队列上下文去存储数据,供不同的任务访问。
  • 如果闭包中的任务创建了不少对象,那么应该考虑将整个任务逻辑代码放在autoreleasepool中,虽然Dispatch Queue中也有自动释放池,但是你不能保证它每次释放的时间,所以咱们自己再加一个要来的更保险一些。

创建与管理Dispatch Queues

在使用Dispatch Queue之前,我们首先需要考虑应该创建什么类型的Dispatch Queue,如何进行配置等,这一节就来说一说如何创建和管理Dispatch Queue。

全局并发Dispatch Queue

并发队列的好处人人皆知,可以方便的同时处理多个任务,在GCD中并发Dispatch Queue同样遵循先进先出的原则,但这只是在运行时适用,如果有个任务在并发队列中还没轮到它执行,那么此时完全可以移除它,而不必等它前面的任务执行完成之后。至于并发队列中没次有多少个任务在执行,这个恐怖在每一秒都在变化,因为影响它的因素有很多,所以之前说过,尽量不要移除移除已经添加进队列的任务。

OS X和iOS系统为我们提供了四种全局并发Dispatch Queue,所谓全局队列,就是我们不需要理会它们的保留和释放问题,而且不需要专门创建它。与其说是四种不如说是一种全局并发队列的四种不同优先级,因为它们之间唯一的不同之处就是队列优先级不同。与Operation Queue不同,在GCD中,Dispatch Queue只有四种优先级:

  • DISPATCH_QUEUE_PRIORITY_HIGH:高优先级。
  • DISPATCH_QUEUE_PRIORITY_DEFAULT:默认优先级,低于高优先级。
  • DISPATCH_QUEUE_PRIORITY_LOW:低优先级,低于高优先级和默认优先级。
  • DISPATCH_QUEUE_PRIORITY_BACKGROUND:后台优先级,低于高优先级和后台线程执行的任务。

我们可以通过dispatch_get_global_queue函数再根据不同的优先级获取不同的全局并发队列,类型为dispatch_queue_t

let highPriorityQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)

let defaultPriorityQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)

我们在使用全局并发队列的时候不需要保留队列的引用,随时要用随时用该函数获取即可。当然我们也可以通过dispatch_queue_create函数自己创建队列:

let concurrentQueue = dispatch_queue_create("com.example.MyConcurrentQueue", DISPATCH_QUEUE_CONCURRENT)

从上面代码可以看到,dispatch_queue_create函数有两个参数,第一个为队列的名称,第二个为队列类型,串行队列为DISPATCH_QUEUE_SERIAL,并发队列为DISPATCH_QUEUE_CONCURRENT

串行Dispatch Queue

串行队列可以让我们将任务按照一定顺序执行,能更优的处理多个任务之间的资源竞争问题,比线程锁机制有更小的资源开销和更好的性能,并且不会产生死锁的问题。

系统也为我们提供了一个串行队列,我们可以通过dispatch_get_main_queue函数获取:

let mainQueue = dispatch_get_main_queue()

该队列与当前应用的主线程相关联。当然我们也可以自己创建串行队列:

let serialQueueA = dispatch_queue_create("com.example.MySerialQueueA", DISPATCH_QUEUE_SERIAL)

// 或者

let serialQueueB = dispatch_queue_create("com.example.MySerialQueueB", nil)

dispatch_queue_create函数的第二个参数如果为nil则默认创建串行队列。当我们创建好串行队列后,系统会自动将创建好的队列与当前应用的主线程进行关联。

获取当前队列

如果需要验证或者测试当前队列,我们可以通过dispatch_get_current_queue函数获取当前队列。如果在闭包中调用,返回的是该闭包所在的队列,如果在闭包外调用,返回的则是默认的并发队列。不过该函数在OS X v10.10中和Swift中都不能使用了,取而代之的是通过DISPATCH_CURRENT_QUEUE_LABEL属性的get方法。

擅用队列上下文

很多情况下,同一个队列中的不同任务之间需要共享数据,尤其像串行队列中的任务,可能由多个任务对某个变量进行处理,或者都需要使用到某个对象,这时就要用到队列上下文:

import Foundation

class TestDispatchQueue {

func launch() {

let serialQueue = dispatch_queue_create("com.example.MySerialQueue", DISPATCH_QUEUE_SERIAL)

dispatch_set_context(serialQueue, unsafeBitCast(0, UnsafeMutablePointer<Int>.self))

dispatch_async(serialQueue, {

var taskCount = unsafeBitCast(dispatch_get_context(serialQueue), Int.self)

taskCount++

print("TaskA in the dispatch queue...and The number of task in queue is \(taskCount)")

dispatch_set_context(serialQueue, unsafeBitCast(taskCount, UnsafeMutablePointer<Int>.self))

sleep(1)

})

dispatch_async(serialQueue, {

var taskCount = unsafeBitCast(dispatch_get_context(serialQueue), Int.self)

taskCount++

print("TaskB in the dispatch queue...and The number of task in queue is \(taskCount)")

dispatch_set_context(serialQueue, unsafeBitCast(taskCount, UnsafeMutablePointer<Int>.self))

})

sleep(3)

}

}

let testDispatchQueue = TestDispatchQueue()
testDispatchQueue.launch()

从上面的代码示例中可以看到,在执行代码点,我们用dispatch_set_context函数向serialQueue队列的上下文环境中设置了一个Int类型的变量,初始值为0。该函数有两个参数,第一个是目标队列,第二个参数是上下文数据的指针。然后在闭包中我们使用dispatch_get_context函数获取上下文数据进行进一步的处理。除了基本类型,我们也可以将自定义的类放入队列上下文中:

import Foundation

class Contact: NSObject {

let name = "DevTalking"

let mobile = "10010"

}

class TestDispatchQueue {

let contact = Contact()

func launch() {

let serialQueue = dispatch_queue_create("com.example.MySerialQueue", DISPATCH_QUEUE_SERIAL)

dispatch_set_context(serialQueue, unsafeBitCast(contact, UnsafeMutablePointer<Void>.self))

dispatch_async(serialQueue, {

let contact = unsafeBitCast(dispatch_get_context(serialQueue), Contact.self)

print("The name is \(contact.name)")

sleep(1)

})

dispatch_async(serialQueue, {

let contact = unsafeBitCast(dispatch_get_context(serialQueue), Contact.self)

print("The name is \(contact.mobile)")

})

sleep(3)

}

}

let testDispatchQueue = TestDispatchQueue()
testDispatchQueue.launch()

关于unsafeBitCast函数和Swift中指针的用法在这里可以有所参考。

队列的收尾工作

虽然在ARC时代,资源释放的工作已经基本不需要我们手动去做了,但有些时候因为系统释放资源并不是很及时,也会造成内存移除等问题,所以在一些情况下我们还是需要进行手动释放资源的工作,必入添加autoreleasepool保证资源及时释放等。Dispatch Queue也给我们提供了这样的机会(机会针对于ARC时代,在MRC时代是必须要做的),那就是Clean Up Function清理扫尾函数,当队列被释放时,或者说引用计数为0时会调用该函数,并且将上下文指针也传到了该函数,以便进行清理工作:

import Foundation

class Contact: NSObject {

let name = "DevTalking"

let mobile = "10010"

}

class TestDispatchQueue {

let contact = Contact()

func testCleanUpFunction() {

launch()

sleep(15)

}

func launch() {

let serialQueue = dispatch_queue_create("com.example.MySerialQueue", DISPATCH_QUEUE_SERIAL)

dispatch_set_context(serialQueue, unsafeBitCast(contact, UnsafeMutablePointer<Void>.self))

dispatch_set_finalizer_f(serialQueue, myFinalizerFunction())

dispatch_async(serialQueue, {

let contact = unsafeBitCast(dispatch_get_context(serialQueue), Contact.self)

print("The name is \(contact.name)")

sleep(1)

})

dispatch_async(serialQueue, {

let contact = unsafeBitCast(dispatch_get_context(serialQueue), Contact.self)

print("The name is \(contact.mobile)")

})

sleep(3)

}

func myFinalizerFunction() -> dispatch_function_t {

return { context in

let contact = unsafeBitCast(context, Contact.self)

print("The name is \(contact.name) and the mobile is \(contact.mobile), The serialQueue has been released and we need clean up context data.")

// TODO...

}

}

}

let testDispatchQueue = TestDispatchQueue()
testDispatchQueue.testCleanUpFunction()

从上面的代码示例中可以看到当给队列设置完上下文时,我们使用了dispatch_set_finalizer_f函数给队列设置清理函数,dispatch_set_finalizer_f函数有两个参数,第一个是目标队列,第二个参数是类型为dispatch_function_t的函数指针,也就是清理函数,上下文数据指针是该函数唯一的参数。在上面代码中,我们添加了myFinalizerFunction函数作为清理函数,在该函数中获得上下文数据,然后进行后续的清理工作。