go开发环境搭建(vscode)

环境搭建

1.下载golang

golang官方下载页面下载压缩go1.13.6.windows-amd64.zip文件并解压到指定目录(如C:\go1.13.6.windows-amd64\go)

新建系统环境变量名GOROOT 内容为go文件解压路径(如C:\go1.13.6.windows-amd64\go)

修改系统环境变量PATH,在PATH内容后新增%GOROOT%\bin;
注意带分号

打开cmd输入测试命令go version,如能打印出go版本号则成功

新建系统环境变量名GOPATH 内容为将来的go开发工作目录(如C:\go_workspace),并在该目录下新建3个文件夹,分别是src、pkg、bin,其中将来所有项目的源码都放在src目录下,bin目录存放的是可执行文件,pkg目录存库文件

将GOPATH下的bin目录添加到PATH环境变量下,在PATH末尾新增%GOPATH%\bin;,注意带分号

配置完成后,可以通过go env查看配置好的相关环境变量

2.安装vscode

下载地址,下载对应平台的安装文件

3.配置vscode

vscode安装go插件后,还需要安装golang自动补全等辅助插件,如果没有科学上网途径的话,大概率会自动安装失败,直接下载已经编译好的可执行文件,拷贝到自己电脑上的 GOROOT/bin 目录下。 go-tools百度云下载链接,密码:4crs。

轻量级ORM框架Dapper

基本介绍

Dapper作为一款高性能、轻量级的ORM框架,目前支撑了国外知名网站stackoverflow的数据访问层,其知名度非常高。在众多ORM中,堪称性能之王。作为一款微型 ORM,很受国内开发者的欢迎,毕竟经过大网站 stackoverflow 的考验。很多自主开发的 ORM 做性能测试,都会选择 Dapper 作为比较对象。

Dapper的优势

  • 轻量。只有一个文件(SqlMapper.cs)
  • 速度快。Dapper的速度接近与IDataReader,取列表的数据超过了DataTable。
  • 支持多种数据库。支持多数据库的本质是因为Dapper是对IDBConnection接口进行了方法扩展,而如SqlConnection,MysqlConnection,OracleConnection等都是继承于DBConnection,而DBConnection又是实现了IDBConnection的接口。所以Dapper能够在所有Ado.net Providers下工作,包括sqlite, sqlce, firebird, oracle, MySQL, PostgreSQL and SQL Server。
  • 可以映射一对一,一对多,多对多等多种关系。
  • 性能高。通过Emit反射IDataReader的序列队列,来快速的得到和产生对象,性能不错。
  • 兼容性好。支持.NetFrameWork2.0之后所有版本

Dapper原理

Dapper是一个简单的ORM,专门从SQL查询结果中快速生成对象。Dapper支持执行sql查询并将其结果映射到强类型列表或动态对象列表。Dapper缓存每个查询的信息。这种全面的缓存有助于从大约两倍于LINQ到SQL的查询生成对象。当前缓存由两个ConcurrentDictionary对象处理,它们从不被清除。

Dapper通过扩展方法将两个映射函数添加到IDbConnection接口,这两个函数都命名为ExecuteMapperQuery。第一个映射结果是一个强类型列表,而第二个映射结果是一个动态对象列表。ExecuteMapperCommand执行并且不返回结果集。所有三个方法都将参数接受为匿名类,其中属性值映射到同名的SQL参数。

Dapper旨在仅处理结果集到对象映射。它不处理对象之间的关系,它不会自动生成任何类型的SQL查询。

  • Query()方法
1
2
Query<T>(this IDbConnection cnn, string sql, object param = null, 
IDbTransaction transaction = null, bool buffered = true, int? commandTimeout = null, CommandType? commandType = null)

Query()方法表示执行查询,返回按T输入的数据。该方法是Query()方法的泛型方法,有7个参数,第一个参数为IDbConnection扩展类,表示对IDbConnection接口进行扩展,该方法使用了可选参数,提高方法的扩展性。在Query方法的实现中,有一个CommandDefinition类,用来表示sql操作的关键方面。在该类下有一个GetInit()方法。

  • GetInit()方法
    Dapper通过Emit反射IDataReader的序列队列,来快速的得到和产生对象。GetInit()方法是一个静态方法,该方法的“Type commandType”参数表示连接关联的Command对象,返回一个Action委托。
1
if (SqlMapper.Link<Type, Action<IDbCommand>>.TryGet(commandInitCache, commandType, out action)){ return action; }

Link<TKey, TValue>是一个泛型分部类,这是一个微缓存,查看是否存在一个Action的委托。

1
2
 var bindByName = GetBasicPropertySetter(commandType, "BindByName", typeof(bool));
var initialLongFetchSize = GetBasicPropertySetter(commandType, "InitialLONGFetchSize", typeof(int));

以上两个操作主要获取BindByName和InitialLONGFetchSize的获取基本属性设置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
if (bindByName != null || initialLongFetchSize != null)
{
var method = new DynamicMethod(commandType.Name + "_init", null, new Type[] { typeof(IDbCommand) });
var il = method.GetILGenerator();
if (bindByName != null)
{
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Castclass, commandType);
il.Emit(OpCodes.Ldc_I4_1);
il.EmitCall(OpCodes.Callvirt, bindByName, null);
}
if (initialLongFetchSize != null)
{
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Castclass, commandType);
il.Emit(OpCodes.Ldc_I4_M1);
il.EmitCall(OpCodes.Callvirt, initialLongFetchSize, null);
}
il.Emit(OpCodes.Ret);
action = (Action<IDbCommand>)method.CreateDelegate(typeof(Action<IDbCommand>));
}

这一步是该操作的核心部分,利用Emit反射操作。根据上一步获取的对应名称的基本属性设置,采用DynamicMethod对象,定义和表示一个可以编译,执行和丢弃的动态方法。丢弃的方法可用于垃圾回收。调用该对象的GetILGenerator方法,返回方法的Microsoft中间语言(MSIL)生成器,默认的MSIL流大小为64字节。判断基本属性设置不为空后,调用ILGenerator类的Emit方法,Emit()将指定的指令放在指令流上,该方法接收一个IL流。EmitCall()将 call 或 callvirt 指令置于 Microsoft 中间语言 (MSIL) 流,以调用varargs 方法。我们看到OpCodes类,该类描述中间语言 (IL) 指令。CreateDelegate()完成动态方法并创建一个可用于执行它的委托。

通过以上的反射操作构建好对象后,就会接着执行对应的数据库操作。

  • QueryImpl()

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    private static IEnumerable<T> QueryImpl<T>(this IDbConnection cnn, CommandDefinition command, Type effectiveType)
    {
    object param = command.Parameters;
    var identity = new Identity(command.CommandText, command.CommandType, cnn, effectiveType, param == null ? null : param.GetType(), null);
    var info = GetCacheInfo(identity, param, command.AddToCache);
    IDbCommand cmd = null;
    IDataReader reader = null;
    bool wasClosed = cnn.State == ConnectionState.Closed;
    try
    {
    cmd = command.SetupCommand(cnn, info.ParamReader);
    if (wasClosed) cnn.Open();
    reader = cmd.ExecuteReader(wasClosed ? CommandBehavior.CloseConnection | CommandBehavior.SequentialAccess : CommandBehavior.SequentialAccess);
    wasClosed = false;
    var tuple = info.Deserializer;
    int hash = GetColumnHash(reader);
    if (tuple.Func == null || tuple.Hash != hash)
    {
    if (reader.FieldCount == 0)
    yield break;
    tuple = info.Deserializer = new DeserializerState(hash, GetDeserializer(effectiveType, reader, 0, -1, false));
    if (command.AddToCache) SetQueryCache(identity, info);
    }
    var func = tuple.Func;
    var convertToType = Nullable.GetUnderlyingType(effectiveType) ?? effectiveType;
    while (reader.Read())
    {
    object val = func(reader);
    if (val == null || val is T)
    {
    yield return (T)val;
    }
    else
    {
    yield return (T)Convert.ChangeType(val, convertToType, CultureInfo.InvariantCulture);
    }
    }
    while (reader.NextResult()) { }
    reader.Dispose();
    reader = null;
    command.OnCompleted();
    }
    finally
    {
    if (reader != null)
    {
    if (!reader.IsClosed) try { cmd.Cancel(); }
    catch { /* don't spoil the existing exception */ }
    reader.Dispose();
    }
    if (wasClosed) cnn.Close();
    if (cmd != null) cmd.Dispose();
    }
    }

    该方法为执行查询操作的核心方法,通过CommandDefinition类的相关操作后,获取到相应的对象后,执行这一步操作。该方法是IDbConnection的扩展方法,CommandDefinition表示sql的相关操作对象,Type表示传入的一个有效的类型。Identity对象表示Dapper中的缓存查询的标识,该类是一个分部类,可以对其进行相应的扩展。GetCacheInfo()获取缓存信息。

ABP、SaaS与多租户

ABP

ASP.NET Boilerplate是一个用最佳实践和流行技术开发现代WEB应用程序的新起点,它旨在成为一个通用的WEB应用程序框架和项目模板。ASP.NET Boilerplate 基于DDD的经典分层架构思想,实现了众多DDD的概念(但没有实现所有DDD的概念)。ABP不仅架构设计和代码写的好,文档也很全面详实(这是一个开发框架被技术选型的基础)。

多租户

多租户用于创建Saas(Software as-a service)应用(云处理)。

维基百科对多租户的解释是:软件多租户是指一种软件架构,在这种软件架构中,软件的一个实例运行在服务器上并且为多个租户服务。

一个租户是一组共享该软件实例特定权限的用户。有了多租户架构,软件应用被设计成为每个租户提供一个专用的实例包括该实例的数据的共享,还可以共享配置,用户管理,租户自己的功能和非功能属性。多租户和多实例架构相比,多租户分离了代表不同的租户操作的多个实例。

多租户一般涉及如下几种场景:

  • 多部署-多数据库:即针对每个租户独立部署一套应用程序实例,每个实例对应一个数据库。这种不算是真正的多租户,不过对于在设计时没有考虑多租户的遗留系统采用这种部署方式不失为一种折中办法。
  • 单部署-多数据库:只有唯一的一个应用程序实例,每个租户分别连接不同的数据库。
  • 单部署-单数据库:应用程序实例和数据库都是一个。通过在需要隔离数据的数据表中加入一个类似TanantId或EnterpriseId来区分。如果我们有很多具有大量数据的租户,那么这种方法可能会有性能问题。我们可以使用关系型数据库的表分割特征或者将租户按组分到不同的服务器上。
  • 单部署-混搭数据库:应用程序实例一个,但是数据库根据情况,可以是单个或者多个。比如免费用户放到一个数据库中,高级用户分别有自己的数据库。
  • 集群部署-单/多/混搭数据库:应用程序的逻辑实例还是一个(只是为了高可用和性能部署为一个集群),然后对应的数据库可以是单个、多个和混搭。

另外,除了针对租户的数据库以外,可能还需要一个全局的数据库(称之为主机数据库)来保存全局范围的配置数据。在单数据库情况下,主机数据可能就和租户数据放在一起(甚至同一个数据表中)。

ABP中的多租户

ABP提供了创建单部署,单数据库,多租户架构的基础设施。

从零搭建单部署-单数据库的多租户架构比较麻烦,因为我们必须要阻止一个租户读取或写入其他租户的数据。我们可以为每个数据库的读取(select)操作添加一个TenantId过滤器。而且,我们可以在每次写入的时候检查一下该实体是否和当前的租户相关。这是乏味而易于出错的,但ABP通过使用自动的数据过滤帮助我们处理这个事情。

租主与租户

  • 租主(Host):租主是单例的(只有一个租主)。租主会对创建和管理租户负责。因此,一个“租主用户”比所有的租户等级更高,并独立于所有租户,同时还能控制他们。
  • 租户(Tenant):租主的一个客户,具有自己的用户角色,权限,设置等。每个租户都可以完全独立于其他租户使用应用。一个多租户应用会有一个或多个租户。如果是一个CRM应用,那么不同的租户也有它们自己的账户,契约,产品和订单。因此,当我们说“**租户用户”的时候,意思就是一个租户拥有的用户。

Session

ABP定义了一个获取当前用户和租户id的IAbpSession接口。该接口用于多租户获取当前的租户id。因此,它可以基于当前的租户id过滤数据。ABP中有以下规则:

  • 如果UserId和TenantId都是null,那么当前的用户没有登录到系统。因此,我们可以不知道当前用户是否是一个租主用户还是一个租户用户。在这种情况下,用户不能访问授权的内容。
  • 如果UserId不是null,TenantId是null,那么当前用户是一个租主用户。
  • 如果UserId不是null,TenantId也不是null,那么当前用户是租户用户。

数据过滤器

当从数据库中检索实体时,我们必须添加一个TenantId过滤器来只获得当前的租户实体。当你为实体实现了IMustHaveTenant和IMayHaveTenant两个接口之一时,ABP会自动地完成数据过滤。

IMustHaveTenant接口

该接口通过定义TenantId属性来区分不同租户的实体。一个实现了IMustHaveTenant的实体例子如下:

1
2
3
4
5
6
7
8
public class Product : Entity, IMustHaveTenant
{
public int TenantId { get; set; }

public string Name { get; set; }

//...其他属性
}

这样,ABP知道这是一个特定租户的实体,并且会自动地将一个租户的实体从其他实体中分离出来。

IMayHaveTenant接口

我们可能需要在租户和租户之间共享一个实体类型。因此,一个实体可能会被一个租户或租主拥有。IMayHaveTenant接口也定义了TenantId(类似于IMustHaveTenant),但在这种情况下是nullable。实现了IMayHaveTenant的一个实体例子:

1
2
3
4
5
6
7
8
public class Role : Entity, IMayHaveTenant
{
public int? TenantId { get; set; }

public string RoleName { get; set; }

//...其他属性
}

我们可能会使用相同的Role类来存储租主角色和租户角色。这种情况下,TenantId表明这是一个租户实体还是一个租主实体。null值表示这是一个租主实体,非null值表示这被一个租户拥有,该租户的Id是TenantId。

IMayHaveTenant不像IMustHaveTenant一样常用。比如,一个Product类可以不实现IMayHaveTenant接口,因为Product和实际的应用功能相关,和管理租户不相干。因此,要小心使用IMayHaveTenant接口,因为它更难维护租户和租主共享的代码。

保存实体

一个租户用户不应该创建或编辑其他租户的实体。如果相关的数据过滤器开启了,那么ABP会检查该实体相对于数据库的改变。

ABP对多租户的支持

在模块的PreInitialize方法中开启多租户模式

1
Configuration.MultiTenancy.IsEnabled = true;

ABP中内置了处理TenantId的机制(通过接口IMustHaveTenant或IMayHaveTenant来实现)。实体实现了IMustHaveTenant接口,会包含一个不能为空的TenantId属性,即意味着其中的数据库需要基于TenantId来进行隔离。实现了IMayHaveTenant接口,会包含一个能为空的TenantId属性,在TenantId为空的时候代表数据属于主机范围的,不为空的时候表示数据基于租户来隔离。

而ABP通过一个特殊封装的IAbpSession来给使用者提供当前TenantId的获取,如果是主机用户登录系统,那么TenantId就是为空的,否则就是登录用户所在租户的Id。

ABP在多租户下读取数据

ABP并非只是简单的给你的实体类添加一个TenantId属性,而是通过识别IMustHaveTenant或IMayHaveTenant接口,使用数据过滤机制(根据底层所用ORM不同有不同的实现方式)自动在你读取数据的时候,基于当前AbpSession中的TenantId来过滤数据。也就是说,你查询读取数据的时候,写“where item.TenantId == AbpSession.TenantId” 这样的代码是毫无必要的。

需要注意的是,如果实体实现的是IMustHaveTenant接口,且AbpSession.TenantId为null的时候(即主机用户),获取到的数据是所有租户的,除非你自己显式进行过滤。而在IMayHaveTenant情况下,AbpSession.TenantId为null获取到的是主机用户的数据。

ABP在多租户下写入数据

在多租户的情形下,写入数据也通过拦截机制(比如重写DbContext的SaveChanges方法),可以自动为你的实体设置TenantId属性,不管你用的是IMustHaveTenant还是IMayHaveTenant。虽然官方文档是推荐在创建实体的时候,总是显示设置TenantId的,尤其在使用IMayHaveTenant的时候(这也是abp使用者唯一需要关系这个属性的地方)。但是,就我个人的看法而言,利用框架的原因就是为了让编码简单,所以我还是倾向于建议大家不用显式设置TenantId。

参考资料:ABP多租户官方文档

持续集成与jenkins

持续集成

开发中,我们经常遇到一些奇怪问题,比如本地可以编译成功的代码但是同事们更新代码后编译出错,或者在项目有多个Target的时候,资源文件只添加到了当前的Target,另外一个Target这个时候是不能正常编译的,再比如写的工具类,被同事改了,或者自己有改动,很多地方用到了,怎么保证这个类的行为没有发生变化而影响到项目中的其它模块呢?诸如此类。

那么这些问题原因在哪,可否避免呢?当然是可以避免的,如果代码有新的改动,提交到版本库中的时候,有专人检查必要事项,并进行测试。

引起各种奇怪问题的原因有很多,比如开发环境比较复杂不干净,IDE的bug,提交前有一些必要的检查需要做,但是开发时因为各种原因没做,这些机械重复的事情我们可以找一个工具来帮我们完成,而且这个工具跑在一个专门的服务器上,该服务器环境相对干净,可以运行一些自动化操作,而自动编译,代码检查,测试等环节,那么这种东西,就是接下来讲的[持续集成]。

持续集成是指为解决程序代码提交质量低,提交内容导致原有系统的bug,按时或按需自动编译版本,自动进行自动化测试的实践工作。

持续集成的特性:

  1. 对重复的编译发布等操作进行抽象,减少重复过程。

  2. 及早发现各种冲突和错误,减少风险。

  3. 任何时间、任何地点生成可部署的软件

持续集成方案:

实践持续集成方案,需要一些必要条件

  1. 一个自动构建过程,包括自动编译、分发、部署和测试等

  2. 一个代码存储库,即需要版本控制软件来保障代码的可维护性,同时作为构建过程的素材库。

  3. 一个持续集成服务器。

自动化构建成过程,可帮助我们节省大量时间,完成这个过程的自动化后,在以后的开发过程中,我们需要做的,就是只是提交代码到版本库中,构建自动完成,基本不再需要人工干预。

代码仓库作为构建的素材库,构建所需的代码从代码库中获得。

最好有一台服务器单独作为持续集成服务器,一方面保证了环境的纯净,一方面不影响开发,而且持续集成服务器一般是随时准备开始构建的,所以一般也不关机。

首先要有统一的代码库,服务器不断从版本控制服务器上检查代码状态,看代码是否有更新。如果发现有代码更新,那么就从版本控制服务器下载最新的代码。等代码完全更新以后,调用自动化编译脚本,进行代码编译。然后运行所有的自动化测试,并且进行代码分析。如果其中任何一个步骤失败,就表示build失败,持续集成服务器会给予响应的反馈。每次代码提交之后,都会在持续集成服务器上触发一个定时构建,然后进行编译、部署。

持续集成原则:

  1. 开发人员必须及时向版本控制库中提交代码,也必须经常性地从版本控制库中更新代码到本地;

  2. 需要有专门的集成服务器来执行集成构建。根据项目的具体实际,集成构建可以被软件的修改来直接触发,也可以定时启动,如每半个小时构建一次;

  3. 必须保证构建的成功。如果构建失败,修复构建过程中的错误是优先级最高的工作。一旦修复,需要手动启动一次构建。

  4. 不更新构建失败的代码

开发人员及时的提交代码进行构建是符合上述实践的,及时拉取代码可以防止工作中的分支偏离主干分支太多。定时触发构建或者通过检测代码的修改情况在触发构建都是可以的,主要是根及时的构建新的代码。如果构建失败,则必要及时处理导致失败的问题,修复后重新构建。当然构建失败的代码就不要拉到本地了,会污染一个本来是可以运行的工作区。

持续集成工具jenkins

jenkins作为持续集成工具比较常用,各种开发实践都可以通过大量的插件来组合实现,可定制性很好。

jenkins是一个开源项目,提供了一种易于使用的持续集成系统,使开发者从繁杂的集成中解脱出来,专注于更为重要的业务逻辑实现上。同时 Jenkins 能实施监控集成中存在的错误,提供详细的日志文件和提醒功能,还能用图表的形式形象地展示项目构建的趋势和稳定性。— 维基百科

Jenkins功能:

1.定时拉取代码并编译

2.静态代码分析

3.定时打包发布测试版

4.自定义额外的操作,如跑单元测试等

5.出错提醒

基本上都是持续集成实践中的要求和周边的一些实现措施,如提醒功能等,出错后及时提醒开发者修复,Jenkins 中通过配置 SMTP 配置信息(这个一般的邮件服务提供商都有提供),邮件模板等,创建事件触发器,在事件(如编译失败)发生时,及时发送邮件通知开发者,挺方便的。

Jenkins 有很多种触发构建的方式,如 webhook,定时更新代码等,同时可以在触发构建后执行自定义的构建操作,通过编辑自定义的构建脚本,几乎可以进行任何构建操作。

Jenkins理解

Jenkins理解

这里是选择Gitlab作为git server。Gitlab的功能和Github差不多,但是是开源的,可以用来搭建私有git server,也提供非常强大的web GUI,比如开发者互相review源代码的时候就会很方便。

系统的工作流程大概分为以下几步:

  1. 开发者将新版本push到git server (Gitlab)。

  2. Gitlab随后触发jenkins master结点进行一次build。(通过web hook或者定时检测)

  3. jenkins master结点将这个build任务分配给若干个注册的slave结点中的一个,这个slave结点根据一个事先设置好的脚本进行build。这个脚本可以做的事情很多,比如编译,测试,生成测试报告等等。这些原本需要手动完成的任务都可以交给jenkins来做。

  4. 我们在build中要进行编译,这里使用了分布式编译器distcc来加快编译速度。

注意:

jenkins的工作原理是先将源代码从gitlab中拷贝一份到本地,然后根据设置的脚本进行build。我们可以看出,整个系统的关键就是那个build脚本,用来告诉jenkins在一次集成中需要执行的任务。

ABP框架

ABP简介

我们根据不同的需求,会开发出各种应用程序。但是在不同系统开发的过程中,我们总会遇到很多具有通用性的程序结构。例如授权、认证、异常处理、日志、本地化、数据库链接、配置管理、审计日志等具有通用性的程序结构。软件工程师们通过分层、模块化架构、领域驱动设计、依赖注入等方法追寻着最佳实践相关的软件架构。同时会基于一些良好的习惯进行程序开发。

在以前,以上所述的工作会耗费大量时间,并且很难从每个项目中解耦分离,很多公司搭建了自己的框架,用于快速开发及调试程序。事实上,并不是每一家公司都能够有搭建优质框架的能力,以及整理相关文档、对其进行维护

ASP.NET Boilerplate (ABP)是一个开源且文档丰富的应用框架,ABP的愿景是成为一个能够适用于所有公司和所有开发人员的通用框架。它的意义不仅仅是一个程序框架,同时也提供了一种健壮的、基于领域驱动设计及最佳实践架构模型

快速入门

通过一个简单的示例来体验ABP的特性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class TaskAppService : Application, ITaskAppService
{
private readonly IRepository<Task> _taskRepository;

public TaskAppService(IRepository<Task> taskRepository)
{
_taskRepository = taskRepository;
}

[AbpAuthorize(MyPermission.UpdatingTasks)]
public async Task UpdateTask(UpdateTaskInput input)
{
Logger.Info("更新任务信息情况:" + input);

var task = await _taskRepository.FirstOrDefaultAsync(input.TaskId);
if(task == null)
{
throw new UserFriendlyException(L("未找到任务信息"));
}

input.MapTo(task);
}
}

我们展示了应用层的一个方法。在领域驱动设计中,应用层的作用是为展现层实现其相关的用例。我们可以认为UpdateTask这个方法会通过AJAX被调用。而ABP具有如下特性:

  • 依赖注入
  • 仓储
  • 授权
  • 验证
  • 审计日志
  • 工作单元
  • 异常处理
  • 日志
  • 本地化
  • 自动映射
  • 动态WebApi层
  • 动态AJAX代理

我们通过这个简单的示例就可以看到这些特性。这些工作通常需要大量的时间,但是现在它们都由ABP框架自动处理。

针对高并发、高吞吐的应用设计思路

总体设计

解决高并发要有思维宽度,需要从功能、使用、设计、数据库、缓存、OS各个层面去思考及其解决方法,深入的剖析的各个场景;同时针对高并发也要有一定的技术深度,考虑到应用程序的高并发性能,主要按如下几个思路进行方案设计。

数据评估

对应用需要负载的数据量进行评估,数据以理想状态的发展为基础进行库表设计。根据数据量级,制定数据库策略(是否需要对数据库进行分库分表处理)。

应用性能评估

对应用的性能进行评估,如应用需要10W用户同时在线通信。

网络性能分析

  • 网络负载,包括公网负载和内网负载
  • 应用服务器性能,包括CPU、内存、硬盘I/O访问、并发TCP/IP连接
  • 数据库服务器性能,包括参数配置、服务器性能(CPU、内存、硬盘)、数据结构的合理性
  • 不同的网络,不同WEB请求的处理方式,包括静态数据和动态数据

负载均衡

  • 主要难点是环上节点分布均衡与节点处理请求数均衡:
  • DNS负载均衡
  • 软四层交换负载均衡
  • 反向代理实现负载均衡,并实现页码请求缓存

数据存储设计思路

  • 数据库拆分,读写分离(主写,备读)
  • 将热数据保存进LRU队列中,提高CPU处理速度,主要针对数据库中高频、小字段进行缓存,保证50%的命中率才值得缓存IO开销。
  • 分布式Key-Value存储系统存储关键性信息
  • 采用高效的网络文件共享策略,采用图片服务器来实现页面的图片存储

不同网络用户访问考虑

  • 引入CDN来解决不同网络服务商的接入速度问题
  • 在不同运营商机房部署服务器,通过镜像技术来实现不同网络服务商的接入速度问题
  • 通过域名解析,访问所在网络运营商的nginx,然后通过专线传输到异地局域网访问应用服务器
  • 租用代理结点,转发请求

不同地域用户访问考虑

  • 不同区域部署服务器
  • DNS解析域名指向该地域所在接入的nginx,Nginx通过专线内网访问应用服务器。
  • 租用代理结点,转发请求

高并发(高负载)的定义

  • CPU/内存开销,都有哪些进程和服务占用,IO开销,服务读写频率;
  • 增长趋势线性增加、指数增加(无索引遍历)、收敛增加(支撑性);
  • 系统阀值请求超越了OS阀值:如https超时太长导致https超过最大值;mysql链接越界;
  • 峰谷的规律和预测原因分析;
  • 异常的监控和跟踪异常比例不超过万分之几可以忽略,而千分之几就要去研究了。

数据容灾

  • 备份恢复,提供如何进行备份以及进行恢复工作
  • 备份策略,提供不同备份策略,应对各种情况数据恢复问题,例如数据库升级
  • 灾难演练,发生灾难后是否能继续提供正常服务

前端技术栈

前端技术栈

技术栈

框架集

  • parallel.js: 前后端通用的一个并行库
  • zepto: 用于现代浏览器的兼容 jQuery 的库
  • totoro: 稳定的跨浏览器测试工具
  • TheaterJS: 一个用于模拟人输入状态的 JS 库
  • stellar.js: 前端用于实现异步滚动效果的库,现已不再维护
  • skrollr: 另一款实现一步滚动的开源库,使用人数众多,可实现各种狂拽酷炫掉渣天的前端效果,看真相
  • Framework7: 前端框架,是开发人员可以基于 web 技术构建 IOS7 程序
  • regulex: 用于生成 正则表达式 的可视化流程图
  • markdown-it: 新型 Markdown 解析器,快速,支持插件
  • multiline: 用于 Javascript 中的多行文本,类似于 Ruby 的 HERE Doc
  • screenfull.js: 全屏插件,支持各大浏览器
  • lunr.js: 类似于 Solr, 但是用于浏览器上的全文搜索引擎,可以为 JSON 创建索引,离线也可以使用
  • jquery.hotkeys: jQuery 插件,用于绑定热键
  • breach_core: Javascript 编写的 Browser (浏览器)
  • octocard: 用于生成 Github 信息卡片的库
  • github-cards: 用于生成 Github 信息卡片的库
  • money.js: 轻量级货币转换库,web 和 node 皆可用
  • accounting.js: 轻量级的数字、货币转换库
  • javascript-algorithms: Javascript 实现的各种算法集合
  • lazy.js: 类似于 underscore, 但是会延迟执行,某些场景下,性能会有很大的提升
  • seajs: 前端模块加载器,解决模块化、依赖等问题
  • jQuery-One-Page-Nav: 单页应用中一个用于处理导航栏的库
  • js.js: Javascript 实现的 javascript JIT
  • jquery-ui: jQuery 团队开发的 UI 相关的前端库,功能强大
  • todomvc: 分别基于 AngularJS/EmberJS/Backbone等实现的 TODO List, 帮助开发者选择前端 MVC 库
  • localForage: Mozilla 出品,用于离线存储,基于IndexedDB, WebSQL 或者localStorage,提供一致的接口
  • EventEmitter: 浏览器版的 EventEmitter
  • jquery.serializeJSON: jQuery 插件,用于将 form 表单序列化成 JSON 数据
  • knockout: 前端 MVVM 框架,用于开发富前端应用
  • mermaid: 可以根据文本生成流程图,类似于 Markdown 的语法
  • js-sequence-diagrams: 另一款可以根据文本生成流程图的库,类似于 Markdown 的语法
  • flow: 一个用来检测 Javascript 语法错误的库, Facebook 出品
  • zoomooz: jQuery 插件,用来处理浏览器缩放
  • fancyBox: 一个用于放大缩小图片、Web 内容或者多媒体元素的库,优雅大方
  • mithril.js: 轻量型前端 MVC 框架,部分使用场景下性能优于 Angular.js 和 React
  • backbone: 强大的前端 MVC 库,鼻祖级前端库,最初为了配合 Rails 来模块化前端应用,兼容性良好 (兼容到 IE6),插件丰富,性能良好
  • jquery.smartbanner: smartbanner 是从 IOS6 开始支持的一个新特性, 这个插件提供了对早期 IOS4/5 和 Android 的支持
  • jquery.scrollTo: 在页面上以一个元素为起始以动画的方式移动(ScrollTo)到另一个元素, 支持回退等
  • jScrollPane: 自定义的滚动条,让所有浏览器都显示一样的滚动条
  • onepage-scroll: 提供类似于 iPhone6 展示页类似的效果,适用于单页应用,兼容到 IE8
  • scrollMonitor: 前端插件用来监控元素的滚动事件(进入、退出等),性能很好
  • ScrollMagic: 神奇的滚动交互效果插件,可以在滚动的过程中设置各种各样的动态效果
  • infinite-scroll: 滚动加载,滚动到最下到自动加载, Paul Irish 大神之作
  • animatable: 仅仅依靠 border-width 和 background-position 实现的各种动态效果,看真相
  • Fluidbox: 页面上内嵌图片的放大缩小效果,类似于 Medium 中的效果
  • jquery-validation: jQuery 的一个插件,用于校验 Form 表单
  • BigVideo.js: jQuery 的一个插件, 用于实现大背景(视频、图片)效果
  • emscripten: 一款基于 LLVM, 可以将 C/C++ 转换成 Javascript 的工具,使得 Javascript 可以近乎 Native 的速度
  • qrcode-generator: 各种语言的二维码生成工具
  • device.js: 一个可以检测设备类型的工具,可以让我们根据不同的设备来为其定制响应的 Javascript 和 CSS
  • jquery-qrcode: jQuery 插件,用来生成二维码
  • Wookmark-jQuery: jQuery 的一个插件,可以用来实现瀑布流的效果
  • isotope: 可以用来过滤、排列布局,实现美观的动态布局切换效果,Demo
  • lazysizes: 功能强大的图片延迟加载工具,可以首先加载一个低质量的图片,然后再加载高质量的图片
  • progressbar.js: 简洁美观的进度条,扁平化
  • pigshell: 一个由 Javascript 实现的Shell, 将互联网当做一个大的文件系统, 通过 cd/ls/cat…..等命令, 可以访问 Facebook/Twitter/Google Drive 等网络服务
  • spectrum: Js实现的颜色选择器 (Colorpicker)
  • jQuery.countdown: jQuery 倒计时插件
  • summernote: WYSIWYG 富文本编辑器
  • awesomplete: 非常轻型的一个自动补全 JS 库, 没有任何依赖, 配置简单, 美观
  • switchery: IOS 7 上 Switch 的 JS 实现, 支持 IE8 及以上浏览器
  • trix: Basecamp 公司出品的富文本编辑器,简洁小巧
  • sensor.js: 在智能移动设备浏览器上,通过HTML5的api使用移动设备的功能。定位、运动、倾斜等
  • hyhyhy: 用于创建 基于 HTML5 的 演示文稿
  • swipebox: jQuery 插件,用于处理移动端的触摸事件
  • FileAPI: 前端用户处理文件(拖放、多文件上传等)
  • Sortable: 现代浏览器上用于实现元素拖拽排序的功能,支持 Meteor, AngularJS, React,不依赖 jQuery
  • Swiper: 用于实现浏览器上的滑动切换效果,支持硬件加速
  • matter-js: 2D 物理效果引擎,碰撞、弹跳等
  • jQTouch: 用于辅助创建手机端的 Web 应用,支持主题、Zepto.js 等
  • snabbt.js: 一个利用 Javascript 和 CSS transform 的 animation 库
  • c3: 基于 D3 的图表库
  • echarts: 企业级图表库,百度开发
  • parallax.js: 一个用于响应智能手机 orientation 的库
  • jQuery-Animate-Enhanced: jQuery 动画库的一个增强,用于现代浏览器
  • wysihtml: 富文本编辑器,适用于现代浏览器
  • slip: 一个通过滑动或者拖拽来操控列表的库
  • evil-icons: 一个矢量图库,提供 Ruby/Node 等支持
  • PhotoSwipe: JS 的一个图片展示库
  • focusable: 是页面上一个元素高亮的库,有图有真相
  • firefox.html: Firefox 在浏览器端的实现 —— HTML 版的 Firefox
  • jquery-mobile: jQuery 团队开发的用于辅助手机端 web app 开发的库,基于 HTML5
  • mobile-angular-ui: 基于angularjs和bootstarp的web app开发框架
  • interact.js: 一个适用于现代浏览器的,用于处理 手势、拖放、缩放等的库
  • rebound-js: 实现部分物理效果,Facebook 出品
  • basket.js: 基于 LocalStorage 的资源加载器,可以用来缓存 script 和 css, 手机端使用速度快于浏览器直接缓存
  • iscroll: 高性能的滚动(scroll)处理库,功能强大,支持各种事件,不依赖任何的库,且插件丰富, 大众点评的手机端列表滚动就是用这个库处理的
  • metrics-graphics: 基于 D3 的图表库,简洁、高效,Mozilla 出品
  • accessible-html5-video-player: Paypal 出品的 Video 播放器
  • loading: 几种 Loading 效果,基于 SVG
  • flippant.js: 一款能够漂亮的网页元素翻转效果库,代码许久不更新,不过作为源码学习还是不错的
  • move.js: 基于 CSS3 的前端动画框架
  • scrollReveal.js: 使元素以非常酷帅的方式进入画布 (Viewpoint),看 Demo
  • Modernizr: 一个用来检测 HTML5 和 CSS3 支持情况的库
  • foundation: 另一款前端模版框架,类似于 Bootstrap
  • Flat-UI: Bootstrap 的一款主题,简洁美观
  • iCheck: 一款漂亮的 Checkbox 插件
  • Swipe: 非常轻量级的一个图片滑动切换效果库, 性能良好, 尤其是对手机的支持, 压缩后的大小约 5kb
  • slick: 功能异常强大的一个图片滑动切换效果库
  • SocialButtons: 漂亮的社交按钮
  • sweetalert: 一个非常美观的用于替换浏览器默认 alert 的库
  • web-animations-js: Javascript 实现的 Web Animation API
  • vivus: 可以动态描绘 SVG 的 JS 库, 支持多种动画
  • plyr: 轻量, 小巧, 美观的 HTML5 视频播放器
  • timesheet.js: 基于 HTML5 & CSS3 时间表
  • slideout: 一个非常美观的侧滑菜单

包管理工具 Package Managers

NPM

WebStorm

Atom

Backbone

  • Backbone.js API中文文档Backbone.js API中文文档

    AngularJs

  • 中文官方文档Angular

  • (angularjs中文网AngularJS中文网
  • (angularjs教程AngularJS中文网
  • (Angular 基础入门Angular 基础入门
  • (angular-masonry: Masonry 的 AngularJS 插件,用于瀑布流
  • angular-schema-form: 根据 JSON 生成响应的 Form 表单
  • restangular: Angular 中用来处理 RESTful API 的插件,可替代 $resource
  • ng-cordova: Cordova 常用组件的 Angular 版本
  • angular-translate: Angular 的国际化 (I18n)
  • ng-inspector: Chrome 插件,用于调试 Angular
  • angularjs-style-guide: AngularJS 代码风格
  • ngReact: React 的 Angular 插件,可以在 Angular 中使用 React Components
  • material: Google Material Design 效果的 Angular 实现
  • angular-local-storage: Angular 插件, 提供了对 localStorage 的友好支持,并对不支持的浏览器使用 cookie 优雅降级
  • angular-filter: 一组有用的 Angular Filters
  • bindonce: Angular 插件, 用于减少 Watcher 的数量, 提升性能

React

Vue

UI框架 UI Frameworks

Bootstrap

Ionic

Foundation

FrozenUI

materializecss

mui

AntDesign

eleme

JS预处理 JS Preprocessors

TypeScript

CoffeeScript

过程自动化 Process Automation

Grunt

Gulp

模板引擎 Templating

Handlebars

Haml

Jade

构建工具 Build Tools

RequireJS

seajs

Browserify

Webpack

CSS预处理器 CSS Preprocessors

Sass

Less

stylus

核心基础

HTML5

CSS3

JS

jQuery

ES6

DDD领域驱动设计

DDD的全称为Domain-driven Design,即领域驱动设计。

基本概念

Domain-领域

任何一个系统都会属于某个特定的领域。比如论坛是一个领域,只要你想做一个论坛,那这个论坛的核心业务是确定的,比如都有用户发帖、回帖等核心基本功能。比如电商平台、普通电商系统,这种都属于网上电商领域,只要是这个领域的系统,那都有商品浏览、购物车、下单、减库存、付款交易等核心环节。所以,同一个领域的系统都具有相同的核心业务,因为他们要解决的问题的本质是类似的。

所以,只要我们确定了系统所属的领域,那这个系统的核心业务,即要解决的关键问题、问题的范围边界就基本确定了。通常我们说,要成为一个领域的专家,必须要在这个领域深入研究很多年才行。因为只有你研究了很多年,你才会遇到非常多的该领域的问题,同时你解决这个领域中的问题的经验也非常丰富。很多时候,领域专家比技术专家更加吃香,比如金融领域的专家。

Design-设计

DDD中的设计主要指领域模型的设计。为什么是领域模型的设计而不是架构设计或其他的什么设计呢?因为DDD是一种基于模型驱动开发的软件开发思想,强调领域模型是整个系统的核心,领域模型也是整个系统的核心价值所在。每一个领域,都有一个对应的领域模型,领域模型能够很好的帮我们解决复杂的业务问题。

从领域和代码实现的角度来理解,领域模型绑定了领域和代码实现,确保了最终的代码实现就一定是解决了领域中的核心问题的。因为:1)领域驱动领域模型设计;2)领域模型驱动代码实现。我们只要保证领域模型的设计是正确的,就能确定领域模型可以解决领域中的核心问题;同理,我们只要保证代码实现是严格按照领域模型的意图来落地的,那就能保证最后出来的代码能够解决领域的核心问题的。这个思路,和传统的分析、设计、编码这几个阶段被割裂(并且每个阶段的产物也不同)的软件开发方法学形成鲜明的对比。

Driven-驱动

上面其实已经提到了,就是:1)领域驱动领域模型设计;2)领域模型驱动代码实现。这个就和我们传统的数据库驱动开发的思路形成对比了。DDD中,我们总是以领域为边界,分析领域中的核心问题(核心关注点),然后设计对应的领域模型,再通过领域模型驱动代码实现。而像数据库设计、持久化技术等这些都不是DDD的核心,而是外围的东西。

领域驱动设计(DDD)告诉我们的最大价值我觉得是:当我们要开发一个系统时,应该尽量先把领域模型想清楚,然后再开始动手编码,这样的系统后期才会很好维护。但是,很多项目(尤其是互联网项目,为了赶工)都是一开始模型没想清楚,一上来就开始建表写代码,代码写的非常冗余,完全是过程是的思考方式,最后导致系统非常难以维护。而且更糟糕的是,出来混总是要还的,前期的领域模型设计的不好,不够抽象,如果你的系统会长期需要维护和适应业务变化,那后面你一定会遇到各种问题维护上的困难,比如数据结构设计不合理,代码到处冗余,改BUG到处引入新的BUG,新人对这种代码上手困难,等。而那时如果你再想重构模型,那要付出的代价会比一开始重新开发还要大,因为你还要考虑兼容历史的数据,数据迁移,如何平滑发布等各种头疼的问题。所以,就导致我们最后天天加班。

事实上,编程习惯很难改变,从面向过程式的想到哪里写到哪里的思想转变为基于系统化的模型驱动的思维并不是一件容易的事情。

概念总结

  1. 领域就是问题域,有边界,领域中有很多问题;
  2. 任何一个系统要解决的那个大问题都对应一个领域;
  3. 通过建立领域模型来解决领域中的核心问题,模型驱动的思想;
  4. 领域建模的目标针对我们在领域中所关心的问题,即只针对核心关注点,而不是整个领域中的所有问题;
  5. 领域模型在设计时应考虑一定的抽象性、通用性,以及复用价值;
  6. 通过领域模型驱动代码的实现,确保代码让领域模型落地,代码最终能解决问题;
  7. 领域模型是系统的核心,是领域内的业务的直接沉淀,具有非常大的业务价值;
  8. 技术架构设计或数据存储等是在领域模型的外围,帮助领域模型进行落地;

理解、拆分、细化领域

理解领域

在拿到初步的系统开发需求时,往往还无法开始进行真正的的需求分析和模型设计工作,我们还必须将我们的问题进行拆分,需求进行细化。有些时候,需求方,即提出问题的人,很可能自己不清楚具体想要什么。他只知道一个概念,一个大的目标。比如他只知道要做一个社交软件、电商网站等。但是他不清楚这些系统应该具体做成什么样子。这个时候,只有领域专家(指对该领域内的各种业务场景和各种业务规则都非常清楚的人)能够正确的表达出系统该做成什么样子,所以,要知道一个系统到底该做成什么样子,到底哪些是核心业务关注点,只能靠沉淀领域内的各种知识,别无他法。因此,假设你现在打算做一个电商平台,但是你对这个领域没什么了解,那你一定得先去了解下该领域内主流的电商平台,比如淘宝、天猫、京东、亚马逊等。这个了解的过程就是你沉淀领域知识的过程。如果你不了解,就算你领域建模的能力再强,各种技术架构能力再强也是使不上力。领域专家不是某个固定的角色,而是某一类人,这类人对这个领域非常了解。比如,一个开发人员也可以是一个领域专家。假设你在一个公司开发和维护一个系统已经好几年了,但是这个系统的产品经理(PD)可能已经换过好几任了,这种情况下,我们有理由相信这几任产品经理都没有比你更熟悉这个领域。

拆分领域

领域建模的基础是要先理解领域,让自己成为领域专家。如果做到了这点,我们就打好了坚实的基础了。但是,有时一个领域往往太复杂,涉及到的领域概念、业务规则、交互流程太多,导致我们没办法直接针对这个大的领域进行领域建模。所以,我们需要将领域进行拆分,本质上就是把大问题拆分为小问题,然后各个击破的思路。然后既然把一个大的领域划分为了多个小的领域(子域),那最关键的就是要理清每个子域的边界;然后要搞清楚哪些子域是核心子域,哪些是非核心子域,哪些是公共支撑子域;然后,还要思考子域之间的联系是什么。那么,我们该如何划分子域呢?我的个人看法是从业务相关性的角度去思考,也就是我们平时说的按业务功能为出发点进行划分。还是拿经典的电商系统来分析,通常一个电商系统都会包含好几个大块,比如:

  • 会员中心:负责用户账号登录、用户信息的管理;
  • 商品中心:负责商品的展示、导航、维护;
  • 订单中心:负责订单的生成和生命周期管理;
  • 交易中心:负责交易相关的业务;
  • 库存中心:负责维护商品的库存;
  • 促销中心:负责各种促销活动的支持;

上面这些中心看起来很自然,因为大家对电子商务的这个领域都已经非常熟悉了,所以都没什么疑问,好像很自然的样子。所以,领域划分是不是就是没什么挑战了呢?显然不是。之所以我们觉得子域划分很简单,是因为我们对整个大领域非常了解了。如果我们遇到一个冷门的领域,就没办法这么容易的去划分子域了。这就需要我们先去努力理解领域内的知识。所以,子域划分没有任何技巧可言,因为这个工作没有所谓的诀窍。当我们不了解一个东西的时候,如何去拆解它?当我们对整个领域有一定的熟悉了,了解了领域内的相关业务的本质和关系,我们就自然而然的能划分出合理的子域了。不过并不是所有的系统都需要划分子域的,有些系统只是解决一个小问题,这个问题不复杂,可能只有一两个核心概念。所以,这种系统完全不需要再划分子域。但不是绝对的,当一个领域,我们的关注点越来越多,每个关注点我们关注的信息越来越多的时候,我们会不由自主的去进一步的划分子域。比如,也许我们一开始将商品和商品的库存都放在商品中心里,但是后来由于库存的维护越来越复杂,导致揉在一起对我们的系统维护带来一定的困难时,我们就会考虑将两者进行拆分,这个就是所谓的业务垂直分割。

细化子域

仅仅领域进行子域划分是远远不够的,凭这些我们还无法进行后续的领域模型设计。我们还必须再进一步细化每个子域,进一步明确每个子域的核心关注点,即需求细化。需要细化的方面有以下几点:

  1. 梳理领域概念:梳理出领域内我们关注的概念、概念的关系,并统一交流词汇,形成统一语言;
  2. 梳理业务规则:梳理出领域内我们关注的各种业务规则,DDD中叫不变性(invariants),比如唯一性规则,余额不能小于零等;
  3. 梳理业务场景:梳理出领域内的核心业务场景,比如电商平台中的加入购物车、提交订单、发起付款等核心业务场景;
  4. 梳理业务流程:梳理出领域内的关键业务流程,比如订单处理流程,退款流程等;
    从上面这4个方面进行整理工作,这是一个非常具有创造性和有难度的工作。这个工作一方面会主观的定义我们想要什么;另一方面,也需要思考该需求的合理性。细化子域是一个合格的产品经理应有的职责,把产品充分设计好,从各个方面去考虑,如何设计一个产品,才能更好的解决用户的核心诉求,即领域内的核心问题。这需要对领域有足够的了解。

关于领域概念的梳理,推荐采用四色原型分析法,这个分析法通过系统的方法,将概念划分为不同的种类,为不同种类的概念标注不同的颜色。然后将这些概念有机的组合起来,从而让人们可以清晰的分析出概念和概念之间的关系。

细化子域工作强调梳理功能,不强调如何实现功能。

领域模型设计

领域建模的方法

  1. 划分好边界上下文,通常每个子域(sub domain)对应一个边界上下文(bounded context),同一个边界上下文中的概念是明确的,没有任何歧义;
  2. 在每个边界上下文中设计领域模型,具体的领域模型设计方法有很多种,如以场景为出发点的四色原型分析法,这个步骤最核心的就是找出聚合根,并找出每个聚合根包含的信息;
  3. 画出领域模型图,圈出每个模型中的聚合边界;
  4. 设计领域模型时,要考虑该领域模型是否满足业务规则,同时还要综合考虑技术实现等问题,比如并发问题;领域模型不是概念模型,概念模型不关注技术实现,领域模型关心;所以领域模型才能直接指导编码实现;
  5. 思考领域模型是如何在业务场景中发挥作用的,以及是如何参与到业务流程的每个环节的;
  6. 场景走查,确认领域模型是否能满足领域中的业务场景和业务流程;
  7. 模型持续重构、完善、精炼;

领域模型的核心作用

  1. 抽象了领域内的核心概念,并建立概念之间的关系;
  2. 领域模型承担了领域内的状态的维护;
  3. 领域模型维护了领域内的数据之间的业务规则,数据一致性;

领域模型设计只是软件设计中的一小部分

领域模型设计只是整个软件设计中的很小一部分。除了领域模型设计之外,要落地一个系统,还有非常多的其他设计要做:

  • 容量规划
  • 架构设计
  • 数据库设计
  • 缓存设计
  • 框架选型
  • 发布方案
  • 数据迁移、同步方案
  • 分库分表方案
  • 回滚方案
  • 高并发解决方案
  • 一致性选型
  • 性能压测方案
  • 监控报警方案

web前端工程化

在web业务日益复杂和多元化的今天,“意大利面条式”的jQuery程序已显得重负不堪,不论是繁杂的DOM操作,亦或是AJAX的“回调地狱”等问题都预示着jQuery和AJAX等“传统”的前端技术注定无法保证项目的可维护性和开发质量。

现代化的前端架构提出了前端工程化这一概念,主要涉及到了模块化、组件化、规范化、自动化四个方面。

模块化

简单来说,模块化就是将一个大文件拆分成相互依赖的小文件,再进行统一的拼装和加载。模块化的概念为多人协作开发提供了可能性。

JS的模块化

在ES6之前,JavaScript一直没有模块系统,这对开发大型复杂的前端工程造成了巨大的障碍。对此社区制定了一些模块加载方案,如CommonJS、AMD和CMD等,某些框架也会有自己模块系统,比如Angular1.x。

现在ES6已经在语言层面上规定了模块系统,完全可以取代现有的CommonJS和AMD规范,而且使用起来相当简洁,并且有静态加载的特性。规范确定了,然后就是模块的打包和加载问题:

  1. 用Webpack+Babel将所有模块打包成一个文件同步加载;
  2. 用SystemJS+Babel分模块异步加载;
  3. 将两者结合在一起。

CSS的模块化

虽然SASS、LESS、Stylus等预处理器实现了CSS的文件拆分,但没有解决CSS模块化的一个重要问题:选择器的全局污染问题。

按道理,一个模块化的文件应该要隐藏内部作用域,只暴露少量接口给使用者。而按照目前预处理器的方式,导入一个CSS模块后,已存在的样式有被覆盖的风险。虽然重写样式是CSS的一个优势,但这并不利于多人协作。

为了避免全局选择器的冲突,可以采用CSS Modules解决方案:

  • CSS Modules通过JS来管理依赖。它能够最大化地结合CSS生态和JS模块化能力,Vue的scoped style就属于这一种。

组件化

组件化和模块化并不是同一个概念。

模块化只是在语言层面上,对代码的拆分;而组件化是基于模块化,在设计层面上,对UI(用户界面)的拆分。

从UI拆分下来的每个包含模板(HTML)+样式(CSS)+逻辑(JS)功能完备的结构单元,我们称之为组件。

其实,组件化更重要的是一种分治思想。

Keep Simple. Everything can be a component.

简单的说,页面上所有的东西都是组件。页面是个大型组件,可以拆成若干个中型组件,然后中型组件还可以再拆,拆成若干个小型组件,小型组件也可以再拆,直到拆成DOM元素为止。DOM元素可以看成是浏览器自身的组件,作为组件的基本单元。

传统前端框架/类库的思想是先组织DOM,然后把某些可复用的逻辑封装成组件来操作DOM,是DOM优先;而组件化框架/类库的思想是先来构思组件,然后用DOM这种基本单元结合相应逻辑来实现组件,是组件优先。这是两者本质的区别。

其次,组件化实际上是一种按照模板(HTML)+样式(CSS)+逻辑(JS)三位一体的形式对面向对象的进一步抽象。

所以我们除了封装组件本身,还要合理处理组件之间的关系,比如(逻辑)继承、(样式)扩展、(模板)嵌套和包含等,这些关系都可以归为依赖。

经典的客户端框架都是基于组件化思想的,如WinForm、WPF、Android等,它们从诞生的那天起就是组件化的。而前端领域发展曲折,是从展示页面为主的WebPage模式走过来的,近两年才从客户端框架经验中引入了组件化思想。许多前端工程化的问题都可以从客户端那里寻求解决方案。

目前市面上的组件化框架很多,主要的有Vue2、React、Angular2等。

规范化

模块化和组件化确定了开发模型,而这些东西的实现就需要规范去落实。

规范化其实是工程化中很重要的一个部分,项目初期规范制定的好坏会直接影响到后期的开发质量。

主要涉及到如下内容:

  • 目录结构的制定
  • 编码规范
  • 前后端接口规范
  • 文档规范
  • 组件管理
  • Git分支管理
  • Commit描述规范
  • 定期CodeReview
  • 视觉图标规范

编码规范可以采取ESLintStyleLint等强制措施,例如Lint通不过不能提交代码等。

前后端接口管理可以了解一下网易的NEI - 接口管理平台

自动化

任何简单机械的重复劳动都应该让机器去完成。

前端工程化的很多脏活累活都应该交给自动化工具来完成。

图标合并

  • 不要再用PS拼雪碧图了,有Gulp+SpriteSmith;
  • 不要再用Icomoon了,这仍然是半自动的,有FontCustom。

自动化测试

前端自动化测试能够提高代码质量、减少人肉测试等,这些优点是不言而喻的。市面上前端测试框架有很多,选择哪个都不会有太大问题,例如:Karma + Mocha + Chai

构建工具

最后就是你的团队可能不只一个项目,如果每个项目都搭一套gulp+webpack+babel+…,维护成本比较高,而且不能保证统一性。

因此基于Gulp实现一套独立于项目的构建工具是最好的解决方案。

其他

持续集成,性能优化,项目部属等问题有待进一步分析。

大型网站技术架构

1.初始阶段的网站架构

一般来讲,大型网站都是从小型网站发展而来,一开始的架构都比较简单,随着业务复杂和用户量的激增,才开始做很多架构上的改进。当它还是小型网站的时候,没有太多访客,一般来讲只需要一台服务器就够了,这时应用程序、数据库、文件等所有资源都在一台服务器上,网站架构如下图所示:

初级网站架构

2.应用服务和数据服务分离

随着网站业务的发展和用户量的增加,一台服务器就无法再满足需求了。大量用户访问导致访问速度越来越慢,而逐渐增加的数据也会导致存储空间不足。这时就需要将应用和数据分离,应用和数据分离后整个网站使用3台服务器:应用服务器、文件服务器和数据库服务器。这3台服务器对硬件资源的要求各不相同:

  • 应用服务器业务逻辑,需要强大的CPU
  • 数据库服务器对磁盘读写操作很多,需要更快的磁盘和更大的内存
  • 文件服务器存储用户上传的文件,因此需要更大的磁盘空间

此时,网站系统的架构如下图所示:

应用服务与数据服务分离

3.使用缓存改善网站性能

随着用户再增加,网站又会一次面临挑战:数据库压力太大导致整站访问效率再此下降,用户体验受到影响。一个网站,往往 80% 的业务访问集中在 20% 的数据上,比如微博请求量最多的肯定是那些千万级粉丝的大 V 的微博,而几乎没有人关注的你的首页,除了自己想起来之外根本不会被打开。既然大部分业务访问集中在一小部分数据上,那就把这一小部分数据先提前缓存在内存中,而不是每次都去数据库读取,这样就可以减少数据库的访问压力,从而提高整个网站的访问速度。

网站使用的缓存一般分为缓存到应用服务器或者缓存在专门的分布式缓存服务器。缓存到应用服务器自己的访问速度快很多,但是受自身内存限制,往往不太适用。远程分布式缓存使用一个集群专门负责缓存服务,当内存不够还可以轻松得动态扩容。

缓存

4.使用应用服务器集群改善网站的并发处理能力

使用缓存后,数据访问压力得到了缓解,但是单一应用服务器能够处理的请求连接有限,在网站访问高峰期,应用服务器就成了整个网站的效率瓶颈。使用分布式集群是网站解决高并发、海量数据问题的常用手段。当一台服务器的处理能力和存储空间不足时,不要尝试去更换更强大的服务器,对大型网站而言,多么强大的服务器,都满足不了网站持续增长的业务需求。这种情况下,更恰当的做法是增加一台服务器分担原有服务器的访问及存储压力。 对网站架构而言,只要能通过增加一台服务器的方式改善负载压力,就可以以同样的方式持续增加服务器不断改善系统性能,从而实现系统的可伸缩性。

应用服务器实现集群是网站可伸缩架构设计中较为简单成熟的一种,如下图所示:

集群

通过负载均衡调度服务器,可以将来自用户浏览器的访问请求分发到应用服务器集群中的任何一台服务器上,如果有更多用户,就在集群中加入更多的应用服务器,使应用服务器的压力不再成为整个网站的瓶颈。

5.数据库读写分离

网站在使用缓存后,使对大部分数据读操作访问都可以不通过数据库就能完成,但是仍有一部分读操作(缓存访问不命中、缓存过期)和全部的写操作都需要访问数据库,在网站的用户达到一定规模后,数据库因为负载压力过高而成为网站的瓶颈。 目前大部分的主流数据库都提供主从热备功能,通过配置两台数据库主从关系,可以将一台数据库服务器的数据更新同步到另一台服务器上。网站利用数据库的这一功能,实现数据库读写分离,从而改善数据库负载压力。如下图所示:

数据库读写分离

应用服务器在写数据的时候,访问主数据库,主数据库通过主从复制机制将数据更新同步到从数据库,这样当应用服务器读数据的时候,就可以通过从数据库获得数据。为了便于应用程序访问读写分离后的数据库,通常在应用服务器端使用专门的数据访问模块,使数据库读写分离对应用透明。

6.使用反向代理和 CDN 加速网站响应

随着网站业务不断发展,用户规模越来越大,由于中国复杂的网络环境,不同地区的用户访问网站时,速度差别也极大。有研究表明,网站访问延迟和用户流失率正相关,网站访问越慢,用户越容易失去耐心而离开。为了提供更好的用户体验,留住用户,网站需要加速网站访问速度。主要手段有使用 CDN 和反向代理。如下图所示:

CDN反向代理

7.使用分布式文件系统和分布式数据库系统

任何强大的单一服务器都满足不了大型网站持续增长的业务需求。数据库经过读写分离后,从一台服务器拆分成两台服务器,但是随着网站业务的发展依然不能满足需求,这时需要使用分布式数据库。文件系统也一样,需要使用分布式文件系统。如下图所示:

分布式文件系统/数据库系统

分布式数据库是网站数据库拆分的最后手段,只有在单表数据规模非常庞大的时候才使用。不到不得已时,网站更常用的数据库拆分手段是业务分库,将不同业务的数据部署在不同的物理服务器上。

8.使用 NoSQL 和搜索引擎

随着网站业务越来越复杂,对数据存储和检索的需求也越来越复杂,网站需要采用一些非关系数据库技术如 NoSQL 和非数据库查询技术如搜索引擎。如下图所示:

NoSQL及搜索引擎

NoSQL 和搜索引擎都是源自互联网的技术手段,对可伸缩的分布式特性具有更好的支持。应用服务器则通过一个统一数据访问模块访问各种数据,减轻应用程序管理诸多数据源的麻烦。

9.业务拆分

大型网站为了应对日益复杂的业务场景,通过使用分而治之的手段将整个网站业务分成不同的产品线。如大型购物交易网站都会将首页、商铺、订单、买家、卖家等拆分成不同的产品线,分归不同的业务团队负责。

具体到技术上,也会根据产品线划分,将一个网站拆分成许多不同的应用,每个应用独立部署。应用之间可以通过一个超链接建立关系(在首页上的导航链接每个都指向不同的应用地址),也可以通过消息队列进行数据分发,当然最多的还是通过访问同一个数据存储系统来构成一个关联的完整系统,如下图所示:

业务拆分

10.分布式服务

随着业务拆分越来越小,存储系统越来越庞大,应用系统的整体复杂度呈指数级增加,部署维护越来越困难。由于所有应用要和所有数据库系统连接,在数万台服务器规模的网站中,这些连接的数目是服务器规模的平方,导致数据库连接资源不足,拒绝服务。

既然每一个应用系统都需要执行许多相同的业务操作,比如用户管理、商品管理等,那么可以将这些共用的业务提取出来,独立部署。由这些可复用的业务连接数据库,提供共用业务服务,而应用系统只需要管理用户界面,通过分布式服务调用共用业务服务完成具体业务操作。如下图所示:

分布式服务

大型网站的架构演化到这里,基本上大多数的技术问题都可以得以解决了。