整合营销服务商

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

免费咨询热线:

在 ASP.NET CORE 中 (CORS) 跨

在 ASP.NET CORE 中 (CORS) 跨 ASP.NET Core

览器安全性可防止网页向不处理网页的域发送请求。 此限制称为同域策略。 同域策略可防止恶意站点从另一站点读取敏感数据。 有时,你可能想要允许其他站点对你的应用进行跨域请求。 有关详细信息,请参阅 MOZILLA CORS 一文。

跨源资源共享 (CORS) :

  • 是一种 W3C 标准,可让服务器放宽相同的源策略。
  • 不是一项安全功能,CORS 放宽 security。 API 不能通过允许 CORS 来更安全。 有关详细信息,请参阅 CORS 的工作原理。
  • 允许服务器明确允许一些跨源请求,同时拒绝其他请求。
  • 比早期的技术(如 JSONP)更安全且更灵活。

查看或下载示例代码(如何下载)

同一原点

如果两个 Url 具有相同的方案、主机和端口 (RFC 6454) ,则它们具有相同的源。

这两个 Url 具有相同的源:

  • https://example.com/foo.html
  • https://example.com/bar.html

这些 Url 的起源不同于前两个 Url:

  • https://example.net:不同的域
  • https://www.example.com/foo.html:不同的子域
  • http://example.com/foo.html:不同方案
  • https://example.com:9000/foo.html:不同的端口

启用 CORS

有三种方法可启用 CORS:

  • 使用 命名策略 或 默认策略的中间件。
  • 使用 终结点路由。
  • 带有 [EnableCors] 属性的。

通过命名策略使用 [EnableCors] 属性,可在限制支持 CORS 的终结点时提供最佳控制。

警告

UseCors 必须按正确的顺序调用。 有关详细信息,请参阅 中间件顺序。 例如,在 UseCors 使用时,必须调用 UseResponseCaching UseResponseCaching 。

以下各节详细介绍了每种方法。

具有命名策略和中间件的 CORS

CORS 中间件处理跨域请求。 以下代码将 CORS 策略应用到具有指定来源的所有应用的终结点:

C#

public class Startup
{
    readonly string MyAllowSpecificOrigins="_myAllowSpecificOrigins";

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddCors(options=>
        {
            options.AddPolicy(name: MyAllowSpecificOrigins,
                              builder=>
                              {
                                  builder.WithOrigins("http://example.com",
                                                      "http://www.contoso.com");
                              });
        });

        // services.AddResponseCaching();
        services.AddControllers();
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        app.UseHttpsRedirection();
        app.UseStaticFiles();
        app.UseRouting();

        app.UseCors(MyAllowSpecificOrigins);

        // app.UseResponseCaching();

        app.UseAuthorization();

        app.UseEndpoints(endpoints=>
        {
            endpoints.MapControllers();
        });
    }
}

前面的代码:

  • 将策略名称设置为 _myAllowSpecificOrigins 。 策略名称为任意名称。
  • 调用 UseCors 扩展方法并指定 _myAllowSpecificOrigins CORS 策略。 UseCors 添加 CORS 中间件。 必须将对的调用 UseCors 置于之后 UseRouting 但在之前 UseAuthorization 。 有关详细信息,请参阅 中间件顺序。
  • AddCors使用lambda 表达式调用。 Lambda 采用 CorsPolicyBuilder 对象。 本文稍后将介绍配置选项,如 WithOrigins 。
  • 启用 _myAllowSpecificOrigins 所有控制器终结点的 CORS 策略。 请参阅 终结点路由 ,将 CORS 策略应用到特定终结点。
  • 使用响应 Caching 中间件时,调用 UseCors before UseResponseCaching 。

通过终结点路由,CORS 中间件 必须 配置为在对和的调用之间执行 UseRouting UseEndpoints 。

有关测试代码的说明,请参阅 测试 CORS ,如以上代码所示。

AddCors方法调用将 CORS 服务添加到应用的服务容器:

C#

public class Startup
{
    readonly string MyAllowSpecificOrigins="_myAllowSpecificOrigins";

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddCors(options=>
        {
            options.AddPolicy(name: MyAllowSpecificOrigins,
                              builder=>
                              {
                                  builder.WithOrigins("http://example.com",
                                                      "http://www.contoso.com");
                              });
        });

        // services.AddResponseCaching();
        services.AddControllers();
    }

有关详细信息,请参阅本文档中的 CORS 策略选项 。

这些 CorsPolicyBuilder 方法可以链接在一起,如以下代码所示:

C#

public void ConfigureServices(IServiceCollection services)
{
    services.AddCors(options=>
    {
        options.AddPolicy(MyAllowSpecificOrigins,
                          builder=>
                          {
                              builder.WithOrigins("http://example.com",
                                                  "http://www.contoso.com")
                                                  .AllowAnyHeader()
                                                  .AllowAnyMethod();
                          });
    });

    services.AddControllers();
}

注意:指定的 URL 能包含尾随斜杠 (/) 。 如果 URL 以结尾 / ,则比较返回, false 不返回任何标头。

具有默认策略和中间件的 CORS

以下突出显示的代码将启用默认 CORS 策略:

C#

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddCors(options=>
        {
            options.AddDefaultPolicy(
                builder=>
                {
                    builder.WithOrigins("http://example.com",
                                        "http://www.contoso.com");
                });
        });

        services.AddControllers();
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        app.UseHttpsRedirection();
        app.UseStaticFiles();
        app.UseRouting();

        app.UseCors();

        app.UseAuthorization();

        app.UseEndpoints(endpoints=>
        {
            endpoints.MapControllers();
        });
    }
}

前面的代码将默认的 CORS 策略应用到所有控制器终结点。

通过终结点路由启用 Cors

使用在每个终结点上启用 CORS 不 RequireCors 支持 自动预检请求。 有关详细信息,请参阅此 GitHub 颁发和测试与终结点路由和 [HttpOptions] 的 CORS。

使用终结点路由,可以使用一组扩展方法在每个终结点上启用 CORS RequireCors :

C#

public class Startup
{
    readonly string MyAllowSpecificOrigins="_myAllowSpecificOrigins";

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddCors(options=>
        {
            options.AddPolicy(name: MyAllowSpecificOrigins,
                              builder=>
                              {
                                  builder.WithOrigins("http://example.com",
                                                      "http://www.contoso.com");
                              });
        });

        services.AddControllers();
        services.AddRazorPages();
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        app.UseHttpsRedirection();
        app.UseStaticFiles();
        app.UseRouting();

        app.UseCors();

        app.UseAuthorization();

        app.UseEndpoints(endpoints=>
        {
            endpoints.MapGet("/echo",
                context=> context.Response.WriteAsync("echo"))
                .RequireCors(MyAllowSpecificOrigins);

            endpoints.MapControllers()
                     .RequireCors(MyAllowSpecificOrigins);

            endpoints.MapGet("/echo2",
                context=> context.Response.WriteAsync("echo2"));

            endpoints.MapRazorPages();
        });
    }
}

在上述代码中:

  • app.UseCors 启用 CORS 中间件。 由于尚未配置默认策略,因此 app.UseCors() 单独不启用 CORS。
  • /echo和控制器端点允许使用指定策略的跨域请求。
  • /echo2和 Razor Pages 终结点 允许跨源请求,因为未指定默认策略。

[DisableCors]特性 会禁用通过终结点路由启用的 CORS RequireCors 。

请参阅 测试与终结点路由和 [HttpOptions] 的 CORS ,获取与前面类似的代码测试说明。

使用属性启用 CORS

使用 [EnableCors] 属性启用 CORS,并将命名策略应用到只有那些需要 CORS 的终结点提供了精细的控制。

[EnableCors]属性提供了一种用于全局应用 CORS 的替代方法。 [EnableCors]特性启用所选终结点的 CORS,而不是所有终结点:

  • [EnableCors] 指定默认策略。
  • [EnableCors("{Policy String}")] 指定命名策略。

[EnableCors]特性可应用于:

  • Razor 分页 PageModel
  • 控制器
  • 控制器操作方法

可以将不同的策略应用到具有属性的控制器、页面模型或操作方法 [EnableCors] 。 如果将 [EnableCors] 属性应用于控制器、页面模型或操作方法,并在中间件中启用了 CORS,则会应用 这两种 策略。 建议不要结合策略。使用 [EnableCors] 特性或中间件,而不是在同一应用中。

下面的代码将不同的策略应用于每个方法:

C#

[Route("api/[controller]")]
[ApiController]
public class WidgetController : ControllerBase
{
    // GET api/values
    [EnableCors("AnotherPolicy")]
    [HttpGet]
    public ActionResult<IEnumerable<string>> Get()
    {
        return new string[] { "green widget", "red widget" };
    }

    // GET api/values/5
    [EnableCors("Policy1")]
    [HttpGet("{id}")]
    public ActionResult<string> Get(int id)
    {
        return id switch
        {
            1=> "green widget",
            2=> "red widget",
            _=> NotFound(),
        };
    }
}

下面的代码创建两个 CORS 策略:

C#

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration=configuration;
    }

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddCors(options=>
        {
            options.AddPolicy("Policy1",
                builder=>
                {
                    builder.WithOrigins("http://example.com",
                                        "http://www.contoso.com");
                });

            options.AddPolicy("AnotherPolicy",
                builder=>
                {
                    builder.WithOrigins("http://www.contoso.com")
                                        .AllowAnyHeader()
                                        .AllowAnyMethod();
                });
        });

        services.AddControllers();
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        app.UseHttpsRedirection();

        app.UseRouting();

        app.UseCors();

        app.UseAuthorization();

        app.UseEndpoints(endpoints=>
        {
            endpoints.MapControllers();
        });
    }
}

对于限制 CORS 请求的最佳控制:

  • [EnableCors("MyPolicy")]与命名策略一起使用。
  • 不要定义默认策略。
  • 请勿使用 终结点路由。

下一节中的代码满足前面的列表。

有关测试代码的说明,请参阅 测试 CORS ,如以上代码所示。

禁用 CORS

[DisableCors] 特性不会禁用已 通过 终结点路由启用的 CORS。

以下代码定义 CORS 策略 "MyPolicy" :

C#

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddCors(options=>
        {
            options.AddPolicy(name: "MyPolicy",
                builder=>
                {
                    builder.WithOrigins("http://example.com",
                                        "http://www.contoso.com")
                            .WithMethods("PUT", "DELETE", "GET");
                });
        });

        services.AddControllers();
        services.AddRazorPages();
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        app.UseHttpsRedirection();
        app.UseStaticFiles();
        app.UseRouting();

        app.UseCors();

        app.UseAuthorization();

        app.UseEndpoints(endpoints=>
        {
            endpoints.MapControllers();
            endpoints.MapRazorPages();
        });
    }
}

以下代码禁用操作的 CORS GetValues2 :

C#

[EnableCors("MyPolicy")]
[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
    // GET api/values
    [HttpGet]
    public IActionResult Get()=>
        ControllerContext.MyDisplayRouteInfo();

    // GET api/values/5
    [HttpGet("{id}")]
    public IActionResult Get(int id)=>
        ControllerContext.MyDisplayRouteInfo(id);

    // PUT api/values/5
    [HttpPut("{id}")]
    public IActionResult Put(int id)=>
        ControllerContext.MyDisplayRouteInfo(id);


    // GET: api/values/GetValues2
    [DisableCors]
    [HttpGet("{action}")]
    public IActionResult GetValues2()=>
        ControllerContext.MyDisplayRouteInfo();

}

前面的代码:

  • 不会使用 终结点路由启用 CORS。
  • 不定义 默认的 CORS 策略。
  • 使用 [EnableCors ( "MyPolicy" ) ] 启用控制器的 "MyPolicy" CORS 策略。
  • 为方法禁用 CORS GetValues2 。

有关测试上述代码的说明,请参阅 测试 CORS 。

CORS 策略选项

本部分介绍可在 CORS 策略中设置的各种选项:

  • 设置允许的来源
  • 设置允许的 HTTP 方法
  • 设置允许的请求标头
  • 设置公开的响应标头
  • 跨域请求中的凭据
  • 设置预检过期时间

AddPolicy 在中调用 Startup.ConfigureServices 。 对于某些选项,最好先阅读 CORS 如何工作 部分。

设置允许的来源

AllowAnyOrigin:允许所有来源的 CORS 请求与任何方案 (http 或 https) 。 AllowAnyOrigin 是不安全的,因为 任何网站 都可以向应用程序发出跨域请求。

备注

指定 AllowAnyOrigin 和 AllowCredentials 是不安全的配置,可能会导致跨网站请求伪造。 同时使用这两种方法来配置应用时,CORS 服务会返回无效的 CORS 响应。

AllowAnyOrigin 影响预检请求和 Access-Control-Allow-Origin 标头。 有关详细信息,请参阅 预检请求 部分。

SetIsOriginAllowedToAllowWildcardSubdomains:将 IsOriginAllowed 策略的属性设置为一个函数,当计算是否允许源时,此函数允许源匹配已配置的通配符域。

C#

options.AddPolicy("MyAllowSubdomainPolicy",
    builder=>
    {
        builder.WithOrigins("https://*.example.com")
            .SetIsOriginAllowedToAllowWildcardSubdomains();
    });

设置允许的 HTTP 方法

AllowAnyMethod:

  • 允许任何 HTTP 方法:
  • 影响预检请求和 Access-Control-Allow-Methods 标头。 有关详细信息,请参阅 预检请求 部分。

设置允许的请求标头

若要允许在 CORS 请求中发送特定标头(称为 作者请求标头),请调用 WithHeaders 并指定允许的标头:

C#

options.AddPolicy("MyAllowHeadersPolicy",
    builder=>
    {
        // requires using Microsoft.Net.Http.Headers;
        builder.WithOrigins("http://example.com")
               .WithHeaders(HeaderNames.ContentType, "x-custom-header");
    });

若要允许所有 作者请求标头,请调用 AllowAnyHeader :

C#复制

options.AddPolicy("MyAllowAllHeadersPolicy",
    builder=>
    {
        builder.WithOrigins("https://*.example.com")
               .AllowAnyHeader();
    });

AllowAnyHeader 影响预检请求和 访问控制请求标 头。 有关详细信息,请参阅 预检请求 部分。

WithHeaders仅当发送的标头 Access-Control-Request-Headers 与中所述的标头完全匹配时,才可以使用 CORS 中间件策略匹配指定的特定标头 WithHeaders 。

例如,考虑按如下方式配置的应用:

C#

app.UseCors(policy=> policy.WithHeaders(HeaderNames.CacheControl));

CORS 中间件使用以下请求标头拒绝预检请求,因为 Content-Language) 中未列出 (HeaderNames. ContentLanguage WithHeaders :

复制

Access-Control-Request-Headers: Cache-Control, Content-Language

应用返回 200 OK 响应,但不会向后发送 CORS 标头。 因此,浏览器不会尝试跨域请求。

设置公开的响应标头

默认情况下,浏览器不会向应用程序公开所有的响应标头。 有关详细信息,请参阅 W3C 跨域资源共享 (术语) :简单的响应标头。

默认情况下可用的响应标头包括:

  • Cache-Control
  • Content-Language
  • Content-Type
  • Expires
  • Last-Modified
  • Pragma

CORS 规范将这些标头称为 简单的响应标头。 若要使其他标头可用于应用程序,请调用 WithExposedHeaders :

C#

options.AddPolicy("MyExposeResponseHeadersPolicy",
    builder=>
    {
        builder.WithOrigins("https://*.example.com")
               .WithExposedHeaders("x-custom-header");
    });

跨域请求中的凭据

凭据需要在 CORS 请求中进行特殊处理。 默认情况下,浏览器不会使用跨域请求发送凭据。 凭据包括 cookie s 和 HTTP 身份验证方案。 若要使用跨域请求发送凭据,客户端必须设置 XMLHttpRequest.withCredentials 为 true 。

XMLHttpRequest直接使用:

JavaScript

var xhr=new XMLHttpRequest();
xhr.open('get', 'https://www.example.com/api/test');
xhr.withCredentials=true;

使用 jQuery:

JavaScript

$.ajax({
  type: 'get',
  url: 'https://www.example.com/api/test',
  xhrFields: {
    withCredentials: true
  }
});

使用 提取 API:

JavaScript复制

fetch('https://www.example.com/api/test', {
    credentials: 'include'
});

服务器必须允许凭据。 若要允许跨域凭据,请调用 AllowCredentials :

C#

options.AddPolicy("MyMyAllowCredentialsPolicy",
    builder=>
    {
        builder.WithOrigins("http://example.com")
               .AllowCredentials();
    });

HTTP 响应包含一个 Access-Control-Allow-Credentials 标头,通知浏览器服务器允许跨源请求的凭据。

如果浏览器发送凭据,但响应不包含有效的 Access-Control-Allow-Credentials 标头,则浏览器不会向应用程序公开响应,而且跨源请求会失败。

允许跨域凭据会带来安全风险。 另一个域中的网站可以代表用户将登录用户的凭据发送给该应用程序,而无需用户的知识。

CORS 规范还指出, "*" 如果 Access-Control-Allow-Credentials 标头存在,则 (所有源) 的设置源无效。

预检请求

对于某些 CORS 请求,浏览器会在发出实际请求之前发送额外的 OPTIONS 请求。 此请求称为 预检请求。 如果满足以下 所有 条件,浏览器可以跳过预检请求:

  • 请求方法为 GET、HEAD 或 POST。
  • 应用不会设置、、、或以外的请求标头 Accept Accept-Language Content-Language Content-Type Last-Event-ID 。
  • Content-Type标头(如果已设置)具有以下值之一:application/x-www-form-urlencodedmultipart/form-datatext/plain

为客户端请求设置的请求标头上的规则适用于应用通过在对象上调用来设置的标头 setRequestHeader XMLHttpRequest 。 CORS 规范调用这些标头 作者请求标头。 此规则不适用于浏览器可以设置的标头,如 User-Agent 、 Host 或 Content-Length 。

下面是一个示例响应,它类似于在本文档的 "测试 CORS " 部分中通过 " Put test " 按钮发出的预检请求。

General:
Request URL: https://cors3.azurewebsites.net/api/values/5
Request Method: OPTIONS
Status Code: 204 No Content

Response Headers:
Access-Control-Allow-Methods: PUT,DELETE,GET
Access-Control-Allow-Origin: https://cors1.azurewebsites.net
Server: Microsoft-IIS/10.0
Set-Cookie: ARRAffinity=8f8...8;Path=/;HttpOnly;Domain=cors1.azurewebsites.net
Vary: Origin

Request Headers:
Accept: */*
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Access-Control-Request-Method: PUT
Connection: keep-alive
Host: cors3.azurewebsites.net
Origin: https://cors1.azurewebsites.net
Referer: https://cors1.azurewebsites.net/
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: cross-site
User-Agent: Mozilla/5.0

预检请求使用 HTTP OPTIONS 方法。 它可能包含以下标头:

  • 访问控制-请求-方法:将用于实际请求的 HTTP 方法。
  • 访问控制-请求标头:应用在实际请求上设置的请求标头的列表。 如前文所述,这不包含浏览器设置的标头,如 User-Agent 。
  • 访问控制-允许-方法

如果预检请求被拒绝,应用将返回响应, 200 OK 但不会设置 CORS 标头。 因此,浏览器不会尝试跨域请求。 有关拒绝的预检请求的示例,请参阅本文档的 测试 CORS 部分。

使用 F12 工具时,控制台应用会显示类似于以下内容之一的错误,具体取决于浏览器:

  • Firefox:跨源请求被阻止:相同的源策略不允许读取上的远程资源 https://cors1.azurewebsites.net/api/TodoItems1/MyDelete2/5 。 (原因: CORS 请求未成功) 。 了解详细信息
  • 基于 Chromium:从源 "" 中的 "" 提取的访问已被 https://cors1.azurewebsites.net/api/TodoItems1/MyDelete2/5 https://cors3.azurewebsites.net CORS 策略阻止:响应预检请求未通过访问控制检查:请求的资源上没有 "访问控制-允许" 标头。 如果非跳转响应可满足需求,请将请求的模式设置为“no-cors”,以便在禁用 CORS 的情况下提取资源。

若要允许特定标头,请调用 WithHeaders :

C#

options.AddPolicy("MyAllowHeadersPolicy",
    builder=>
    {
        // requires using Microsoft.Net.Http.Headers;
        builder.WithOrigins("http://example.com")
               .WithHeaders(HeaderNames.ContentType, "x-custom-header");
    });

若要允许所有 作者请求标头,请调用 AllowAnyHeader :

C#

options.AddPolicy("MyAllowAllHeadersPolicy",
    builder=>
    {
        builder.WithOrigins("https://*.example.com")
               .AllowAnyHeader();
    });

浏览器的设置方式并不一致 Access-Control-Request-Headers 。 如果:

  • 标头设置为以外的任何内容 "*"
  • AllowAnyHeader 调用:至少包含 Accept 、 Content-Type 和 Origin ,以及要支持的任何自定义标头。

自动预检请求代码

应用 CORS 策略的时间:

  • 通过 app.UseCors 在中调用 Startup.Configure 。
  • 使用 [EnableCors] 特性。

ASP.NET Core 对 "预检选项" 请求做出响应。

目前使用每个终结点启用 CORS 不 RequireCors 支持自动预检请求。

本文档的 " 测试 CORS " 部分说明了此行为。

用于预检请求的 [HttpOptions] 属性

如果为 cors 启用了适当的策略,ASP.NET Core 通常会自动响应 cors 预检请求。 在某些情况下,可能不会出现这种情况。 例如,将 CORS 用于终结点路由。

下面的代码使用 [HttpOptions] 特性为 OPTIONS 请求创建终结点:

C#

[Route("api/[controller]")]
[ApiController]
public class TodoItems2Controller : ControllerBase
{
    // OPTIONS: api/TodoItems2/5
    [HttpOptions("{id}")]
    public IActionResult PreflightRoute(int id)
    {
        return NoContent();
    }

    // OPTIONS: api/TodoItems2 
    [HttpOptions]
    public IActionResult PreflightRoute()
    {
        return NoContent();
    }

    [HttpPut("{id}")]
    public IActionResult PutTodoItem(int id)
    {
        if (id < 1)
        {
            return BadRequest();
        }

        return ControllerContext.MyDisplayRouteInfo(id);
    }

有关测试上述代码的说明,请参阅 通过终结点路由测试 CORS 和 [HttpOptions] 。

设置预检过期时间

Access-Control-Max-Age标头指定可缓存对预检请求的响应的时间长度。 若要设置此标头,请调用 SetPreflightMaxAge :

C#

options.AddPolicy("MySetPreflightExpirationPolicy",
    builder=>
    {
        builder.WithOrigins("http://example.com")
               .SetPreflightMaxAge(TimeSpan.FromSeconds(2520));
    });

CORS 如何工作

本部分介绍 HTTP 消息级别的 CORS 请求中发生的情况。

  • CORS 是一种安全功能。 CORS 是一种 W3C 标准,可让服务器放宽相同的源策略。例如,恶意执行组件可能对站点使用 跨站点脚本 (XSS) ,并向启用了 CORS 的站点执行跨站点请求来窃取信息。
  • API 不能通过允许 CORS 来更安全。
    • 它由客户端 (浏览器) 来强制执行 CORS。 服务器执行请求并返回响应,这是返回错误并阻止响应的客户端。 例如,以下任何工具都将显示服务器响应:
      • Fiddler
      • Postman
      • .NET HttpClient
      • Web 浏览器,方法是在地址栏中输入 URL。
  • 这是一种方法,使服务器能够允许浏览器执行跨源 XHR 或 获取 API 请求,否则将被禁止。没有 CORS 的浏览器不能执行跨域请求。 在 CORS 之前,使用 JSONP 来绕过此限制。 JSONP 不使用 XHR,而是使用 <script> 标记接收响应。 允许跨源加载脚本。

CORS 规范介绍了几个新的 HTTP 标头,它们启用了跨域请求。 如果浏览器支持 CORS,则会自动为跨域请求设置这些标头。 若要启用 CORS,无需自定义 JavaScript 代码。

部署的示例上的 " PUT 测试" 按钮

下面是一个从 " 值 " 测试按钮到的跨源请求的示例 https://cors1.azurewebsites.net/api/values 。 Origin标头:

  • 提供发出请求的站点的域。
  • 是必需的,并且必须与主机不同。

常规标头

Request URL: https://cors1.azurewebsites.net/api/values
Request Method: GET
Status Code: 200 OK

响应标头

Content-Encoding: gzip
Content-Type: text/plain; charset=utf-8
Server: Microsoft-IIS/10.0
Set-Cookie: ARRAffinity=8f...;Path=/;HttpOnly;Domain=cors1.azurewebsites.net
Transfer-Encoding: chunked
Vary: Accept-Encoding
X-Powered-By: ASP.NET

请求标头

Accept: */*
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Connection: keep-alive
Host: cors1.azurewebsites.net
Origin: https://cors3.azurewebsites.net
Referer: https://cors3.azurewebsites.net/
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: cross-site
User-Agent: Mozilla/5.0 ...

在 OPTIONS 请求中,服务器设置响应中的 响应标头 Access-Control-Allow-Origin: {allowed origin} 标头。 例如,已部署的 示例 Delete [EnableCors] button OPTIONS 请求包含以下标头:

常规标头

Request URL: https://cors3.azurewebsites.net/api/TodoItems2/MyDelete2/5
Request Method: OPTIONS
Status Code: 204 No Content

响应标头

Access-Control-Allow-Headers: Content-Type,x-custom-header
Access-Control-Allow-Methods: PUT,DELETE,GET,OPTIONS
Access-Control-Allow-Origin: https://cors1.azurewebsites.net
Server: Microsoft-IIS/10.0
Set-Cookie: ARRAffinity=8f...;Path=/;HttpOnly;Domain=cors3.azurewebsites.net
Vary: Origin
X-Powered-By: ASP.NET

请求标头

Accept: */*
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Access-Control-Request-Headers: content-type
Access-Control-Request-Method: DELETE
Connection: keep-alive
Host: cors3.azurewebsites.net
Origin: https://cors1.azurewebsites.net
Referer: https://cors1.azurewebsites.net/test?number=2
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: cross-site
User-Agent: Mozilla/5.0

在前面的 响应标头 中,服务器设置响应中的 访问控制允许源 标头。 https://cors1.azurewebsites.net此标头的值与 Origin 请求中的标头相匹配。

如果 AllowAnyOrigin 调用了,则将 Access-Control-Allow-Origin: * 返回通配符值。 AllowAnyOrigin 允许任何源。

如果响应不包含 Access-Control-Allow-Origin 标头,则跨域请求会失败。 具体而言,浏览器不允许该请求。 即使服务器返回成功的响应,浏览器也不会将响应提供给客户端应用程序。

显示选项请求

默认情况下,Chrome 和 Edge 浏览器不会在 F12 工具的 "网络" 选项卡上显示 "请求" 选项。 若要在这些浏览器中显示选项请求:

  • chrome://flags/#out-of-blink-cors 或 edge://flags/#out-of-blink-cors
  • 禁用标志。
  • 重新启动.

默认情况下,Firefox 显示 "选项请求"。

IIS 中的 CORS

部署到 IIS 时,如果未将服务器配置为允许匿名访问,则必须在 Windows Authentication 之前运行 CORS。 若要支持此方案,需要为应用安装和配置 IIS CORS 模块 。

测试 CORS

示例下载包含测试 CORS 的代码。 请参阅如何下载。 该示例是一个 API 项目,其中 Razor 添加了页面:

C#

public class StartupTest2
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddCors(options=>
        {
            options.AddPolicy(name: "MyPolicy",
                builder=>
                {
                    builder.WithOrigins("http://example.com",
                        "http://www.contoso.com",
                        "https://cors1.azurewebsites.net",
                        "https://cors3.azurewebsites.net",
                        "https://localhost:44398",
                        "https://localhost:5001")
                            .WithMethods("PUT", "DELETE", "GET");
                });
        });

        services.AddControllers();
        services.AddRazorPages();
    }

    public void Configure(IApplicationBuilder app)
    {
        app.UseHttpsRedirection();
        app.UseStaticFiles();
        app.UseRouting();

        app.UseCors();

        app.UseAuthorization();

        app.UseEndpoints(endpoints=>
        {
            endpoints.MapControllers();
            endpoints.MapRazorPages();
        });
    }
}

警告

WithOrigins("https://localhost:<port>"); 应仅用于测试示例应用程序,类似于 下载示例代码。

下面 ValuesController 提供用于测试的终结点:

C#复制

[EnableCors("MyPolicy")]
[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
    // GET api/values
    [HttpGet]
    public IActionResult Get()=>
        ControllerContext.MyDisplayRouteInfo();

    // GET api/values/5
    [HttpGet("{id}")]
    public IActionResult Get(int id)=>
        ControllerContext.MyDisplayRouteInfo(id);

    // PUT api/values/5
    [HttpPut("{id}")]
    public IActionResult Put(int id)=>
        ControllerContext.MyDisplayRouteInfo(id);


    // GET: api/values/GetValues2
    [DisableCors]
    [HttpGet("{action}")]
    public IActionResult GetValues2()=>
        ControllerContext.MyDisplayRouteInfo();

}

MyDisplayRouteInfo 由 Rick.Docs.Samples.RouteInfo NuGet 包提供,会显示路由信息。

使用以下方法之一测试前面的示例代码:

  • 使用部署的示例应用 https://cors3.azurewebsites.net/ 。 无需下载示例。
  • dotnet run使用的默认 URL 运行示例 https://localhost:5001 。
  • 运行 Visual Studio 中的示例,其中的 URL 为设置为44398的 https://localhost:44398 。

使用带有 F12 工具的浏览器:

  • 选择 " " 按钮,然后查看 " 网络 " 选项卡中的标头。
  • 选择 " 放置测试 " 按钮。 请参阅 显示选项请求 ,以获取有关显示选项请求的说明。 PUT 测试 创建两个请求:一个选项预检请求和 PUT 请求。
  • 选择此 GetValues2 [DisableCors] 按钮可触发失败的 CORS 请求。 如文档中所述,响应返回200成功,但不进行 CORS 请求。 选择 " 控制台 " 选项卡以查看 CORS 错误。 根据浏览器,将显示类似于以下内容的错误:'https://cors1.azurewebsites.net/api/values/GetValues2'CORS 策略已阻止从原始位置获取的访问权限 'https://cors3.azurewebsites.net' :请求的资源上没有 "访问控制-允许" 标头。 如果非跳转响应可满足需求,请将请求的模式设置为“no-cors”,以便在禁用 CORS 的情况下提取资源。

可以使用Fiddler或Postman等工具来测试启用了CORS 的终结点。 使用工具时,标头指定的请求源 Origin 必须与接收请求的主机不同。 如果请求不是基于标头值 域的,则 Origin :

  • 不需要 CORS 中间件来处理请求。
  • 不会在响应中返回 CORS 标头。

以下命令使用 curl 发出带有以下信息的选项请求:

Bash

curl -X OPTIONS https://cors3.azurewebsites.net/api/TodoItems2/5 -i

通过终结点路由和 [HttpOptions] 测试 CORS

目前使用每个终结点启用 CORS 不 RequireCors 支持自动预检请求。 请考虑以下代码,它使用 终结点路由启用 CORS:

C#

public class StartupEndPointBugTest
{
    readonly string MyPolicy="_myPolicy";

    // .WithHeaders(HeaderNames.ContentType, "x-custom-header")
    // forces browsers to require a preflight request with GET

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddCors(options=>
        {
            options.AddPolicy(name: MyPolicy,
                builder=>
                {
                    builder.WithOrigins("http://example.com",
                                        "http://www.contoso.com",
                                        "https://cors1.azurewebsites.net",
                                        "https://cors3.azurewebsites.net",
                                        "https://localhost:44398",
                                        "https://localhost:5001")
                           .WithHeaders(HeaderNames.ContentType, "x-custom-header")
                           .WithMethods("PUT", "DELETE", "GET", "OPTIONS");
                });
        });

        services.AddControllers();
        services.AddRazorPages();
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        app.UseHttpsRedirection();
        app.UseStaticFiles();
        app.UseRouting();

        app.UseCors();

        app.UseAuthorization();

        app.UseEndpoints(endpoints=>
        {
            endpoints.MapControllers().RequireCors(MyPolicy);
            endpoints.MapRazorPages();
        });
    }
}

下面 TodoItems1Controller 提供用于测试的终结点:

C#

[Route("api/[controller]")]
[ApiController]
public class TodoItems1Controller : ControllerBase
{
    // PUT: api/TodoItems1/5
    [HttpPut("{id}")]
    public IActionResult PutTodoItem(int id)
    {
        if (id < 1)
        {
            return Content($"ID={id}");
        }

        return ControllerContext.MyDisplayRouteInfo(id);
    }

    // Delete: api/TodoItems1/5
    [HttpDelete("{id}")]
    public IActionResult MyDelete(int id)=>
        ControllerContext.MyDisplayRouteInfo(id);

    // GET: api/TodoItems1
    [HttpGet]
    public IActionResult GetTodoItems()=>
        ControllerContext.MyDisplayRouteInfo();

    [EnableCors]
    [HttpGet("{action}")]
    public IActionResult GetTodoItems2()=>
        ControllerContext.MyDisplayRouteInfo();

    // Delete: api/TodoItems1/MyDelete2/5
    [EnableCors]
    [HttpDelete("{action}/{id}")]
    public IActionResult MyDelete2(int id)=>
        ControllerContext.MyDisplayRouteInfo(id);
}

从已部署示例的测试页测试前面的代码。

Delete [EnableCors]GET [EnableCors] 按钮成功,因为终结点具有 [EnableCors] 和响应预检请求。 其他终结点失败。 " 获取 " 按钮失败,因为 JavaScript 发送:

JavaScript

 headers: {
      "Content-Type": "x-custom-header"
 },

下面 TodoItems2Controller 提供了类似的终结点,但包含响应选项请求的显式代码:

C#

[Route("api/[controller]")]
[ApiController]
public class TodoItems2Controller : ControllerBase
{
    // OPTIONS: api/TodoItems2/5
    [HttpOptions("{id}")]
    public IActionResult PreflightRoute(int id)
    {
        return NoContent();
    }

    // OPTIONS: api/TodoItems2 
    [HttpOptions]
    public IActionResult PreflightRoute()
    {
        return NoContent();
    }

    [HttpPut("{id}")]
    public IActionResult PutTodoItem(int id)
    {
        if (id < 1)
        {
            return BadRequest();
        }

        return ControllerContext.MyDisplayRouteInfo(id);
    }

    // [EnableCors] // Not needed as OPTIONS path provided
    [HttpDelete("{id}")]
    public IActionResult MyDelete(int id)=>
        ControllerContext.MyDisplayRouteInfo(id);

    [EnableCors]  // Rquired for this path
    [HttpGet]
    public IActionResult GetTodoItems()=>
        ControllerContext.MyDisplayRouteInfo();

    [HttpGet("{action}")]
    public IActionResult GetTodoItems2()=>
        ControllerContext.MyDisplayRouteInfo();

    [EnableCors]  // Rquired for this path
    [HttpDelete("{action}/{id}")]
    public IActionResult MyDelete2(int id)=>
        ControllerContext.MyDisplayRouteInfo(id);
}

从已部署示例的 测试页 测试前面的代码。 在 " 控制器 " 下拉列表中,选择 " 预检 ",然后 设置 "控制器"。 对终结点的所有 CORS 调用都将 TodoItems2Controller 成功。

要获取特定网站的信息,却手动复制粘贴又太过费时?这时候,使用PHP采集工具就能轻松实现你的数据梦想。本文将从如何安装、基础语法、常见问题等9个方面进行详细分析,帮助读者快速掌握PHP采集技术。

一、安装

在使用PHP采集工具之前,需要先安装相关环境。建议使用XAMPP或WAMPP这样的集成开发环境,可以轻松搭建一个本地服务器,并且内置了PHP环境。

二、基础语法

在进行PHP采集时,需要使用到以下几个函数:

1. file_get_contents():获取指定URL的HTML内容;

2. preg_match_all():通过正则表达式匹配指定HTML内容;

3. foreach():遍历匹配到的结果。

以下是一个简单示例:

php
$url=";;
$content=file_get_contents($url);
preg_match_all('/<a href="(.*?)">(.*?)<\/a>/s',$content,$matches);
foreach ($matches[2] as $value){
    echo $value ."<br>";
}

以上代码会获取中所有链接文字,并输出到页面上。

三、选择器

除了正则表达式外,还可以使用选择器来匹配HTML内容。PHP采集工具中常用的选择器有Simple HTML DOM和QueryList。

Simple HTML DOM是一个纯PHP的解析HTML的类库,可以通过类似jQuery的语法来匹配HTML内容。以下是一个示例:

php
include 'simple_html_dom.php';
$url=";;
$html=file_get_html($url);
foreach ($html->find('a') as $value){
    echo $value->plaintext ."<br>";
}

以上代码也会获取中所有链接文字,并输出到页面上。

QueryList是基于GuzzleHttp封装的PHP采集工具,支持CSS选择器、XPath等多种选择器语法。以下是一个示例:

php
use QL\QueryList;
$url=";;
$html=QueryList::get($url)->find('a')->texts();
foreach ($html as $value){
    echo $value ."<br>";
}

以上代码同样会获取中所有链接文字,并输出到页面上。

四、伪造User-Agent

有些网站为了防止爬虫,会检测User-Agent信息。此时需要在请求头中添加一个随机的User-Agent信息,以模拟浏览器访问。

以下是一个示例:

php
$url=";;
$options=[
    'http'=>[
        'method'=>'GET',
        'header'=>'User-Agent:a9694ebf4d02ef427830292349e3172c/5.0(Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3'
    ]
];
$context=stream_context_create($options);
$content=file_get_contents($url, false,$context);

以上代码会在请求头中添加一个随机的User-Agent信息,并获取的HTML内容。

五、处理编码

在采集HTML内容时,有些网站会使用不同的编码方式。此时需要对采集到的内容进行编码转换,以正确显示中文或其他特殊字符。

以下是一个示例:

php
$url=";;
$content=file_get_contents($url);
$content=iconv('gb2312','utf-8',$content);

以上代码会将的HTML内容从gb2312编码转换为utf-8编码。

六、处理分页

在采集多页数据时,需要处理分页问题。一般可以通过循环遍历来实现。

以下是一个示例:

php
for ($i=1;$i<=10;$i++){
    $url="{$i}";
    $content=file_get_contents($url);
    //处理每一页的数据
}

以上代码会循环遍历1~10页,获取每一页的数据。

七、处理异常

在采集过程中,可能会出现网络异常、页面不存在等问题。此时需要对异常进行处理,以保证程序正常运行。

以下是一个示例:

php
$url=";;
$content=@file_get_contents($url);
if ($content===false){
    //处理异常
}

以上代码会在获取HTML内容时加上@符号,忽略掉所有错误信息。如果获取失败,则会进入异常处理流程。

八、使用代理

在进行大规模采集时,可能会被目标网站封禁IP。此时可以使用代理IP来避免被封禁。

以下是一个示例:

php
$url=";;
$options=[
    'http'=>[
        'proxy'=>'tcp://127.0.0.1:8080',
        'request_fulluri'=> true
    ]
];
$context=stream_context_create($options);
$content=file_get_contents($url, false,$context);

以上代码会在请求中添加一个代理IP,并获取的HTML内容。

九、常见问题

1.如何处理乱码问题?

可以使用iconv()函数对采集到的内容进行编码转换。

2.如何处理页面重定向问题?

可以在请求头中添加"Location"信息,指定重定向后的URL地址。

3.如何处理SSL证书问题?

可以在选项中添加verify_peer和verify_host参数,以跳过SSL证书验证。

4.如何处理页面加载速度慢的问题?

可以使用curl_multi_init()函数同时发起多个请求,以提高采集效率。

5.如何处理页面js渲染的问题?

可以使用无头浏览器工具,如PhantomJS、Selenium等,来模拟浏览器行为,获取动态生成的内容。

本文介绍了PHP采集工具的安装、基础语法、常见问题等9个方面内容,希望读者能够通过本文快速掌握PHP采集技术,实现自己的数据梦想。

源自 Robert C. Martin 的 Clean Code 的软件工程原则适配到 JavaScript 。 这不是一个代码风格指南, 它是一个使用 JavaScript 来生产 可读的, 可重用的, 以及可重构的软件的指南。

这里的每一项原则都不是必须遵守的, 甚至只有更少的能够被广泛认可。 这些仅仅是指南而已, 但是却是 Clean Code 作者多年经验的结晶。

我们的软件工程行业只有短短的50年, 依然有很多要我们去学习。 当软件架构与建筑架构一样古老时, 也许我们将会有硬性的规则去遵守。 而现在,让这些指南做为你和你的团队生产的 JavaScript 代码的 质量的标准。

还有一件事:知道这些指南并不能马上让你成为一个更加出色的软件开发者, 并且使用它们工作多年也并不意味着你不再会犯错误。 每一段代码最开始都是草稿, 像湿粘土一样被打造成最终的形态。 最后当我们和搭档们一起审查代码时清除那些不完善之处, 不要因为最初需要改善的草稿代码而自责, 而是对那些代码下手。

变量

使用有意义并且可读的变量名称

不好的:

const yyyymmdstr=moment().format('YYYY/MM/DD');

好的:

const currentDate=moment().format('YYYY/MM/DD');

为相同类型的变量使用相同的词汇

不好的:

getUserInfo();
getClientData();
getCustomerRecord();

好的:

getUser();

使用可搜索的名称

我们要阅读的代码比要写的代码多得多, 所以我们写出的代码的可读性和可搜索性是很重要的。 使用没有 意义的变量名将会导致我们的程序难于理解, 将会伤害我们的读者, 所以请使用可搜索的变量名。 类似 buddy.js 和 ESLint 的工具可以帮助我们找到未命名的常量。

不好的:

// 艹, 86400000 是什么鬼?
setTimeout(blastOff, 86400000);

好的:

// 将它们声明为全局常量 `const` 。
const MILLISECONDS_IN_A_DAY=86400000;

setTimeout(blastOff, MILLISECONDS_IN_A_DAY);

使用解释性的变量

不好的:

const address='One Infinite Loop, Cupertino 95014';
const cityZipCodeRegex=/^[^,\\]+[,\\\s]+(.+?)\s*(\d{5})?$/;
saveCityZipCode(address.match(cityZipCodeRegex)[1], address.match(cityZipCodeRegex)[2]);

好的:

const address='One Infinite Loop, Cupertino 95014';
const cityZipCodeRegex=/^[^,\\]+[,\\\s]+(.+?)\s*(\d{5})?$/;
const [, city, zipCode]=address.match(cityZipCodeRegex) || [];
saveCityZipCode(city, zipCode);

避免心理映射,显示比隐式更好

不好的:

const locations=['Austin', 'New York', 'San Francisco'];
locations.forEach((l)=> {
 doStuff();
 doSomeOtherStuff();
 // ...
 // ...
 // ...
 // 等等, `l` 是啥?
 dispatch(l);
});

好的:

const locations=['Austin', 'New York', 'San Francisco'];
locations.forEach((location)=> {
 doStuff();
 doSomeOtherStuff();
 // ...
 // ...
 // ...
 dispatch(location);
});

不添加不必要的上下文。如果你的类名/对象名有意义, 不要在变量名上再重复。

不好的:

const Car={
 carMake: 'Honda',
 carModel: 'Accord',
 carColor: 'Blue'
};

function paintCar(car) {
 car.carColor='Red';
}

好的:

const Car={
 make: 'Honda',
 model: 'Accord',
 color: 'Blue'
};

function paintCar(car) {
 car.color='Red';
}

使用默认变量替代短路运算或条件

不好的:

function createMicrobrewery(name) {
 const breweryName=name || 'Hipster Brew Co.';
 // ...
}

好的:

function createMicrobrewery(breweryName='Hipster Brew Co.') {
 // ...
}

函数

函数参数 (两个以下最理想)

限制函数参数的个数是非常重要的, 因为这样将使你的函数容易进行测试。 一旦超过三个参数将会导致组 合爆炸, 因为你不得不编写大量针对每个参数的测试用例。

没有参数是最理想的, 一个或者两个参数也是可以的, 三个参数应该避免, 超过三个应该被重构。 通常, 如果你有一个超过两个函数的参数, 那就意味着你的函数尝试做太多的事情。 如果不是, 多数情况下一个 更高级对象可能会满足需求。

由于 JavaScript 允许我们不定义类型/模板就可以创建对象, 当你发现你自己需要大量的参数时, 你 可以使用一个对象。

不好的:

function createMenu(title, body, buttonText, cancellable) {
 // ...
}

好的:

const menuConfig={
 title: 'Foo',
 body: 'Bar',
 buttonText: 'Baz',
 cancellable: true
};

function createMenu(config) {
 // ...
}

函数应当只做一件事情

这是软件工程中最重要的一条规则, 当函数需要做更多的事情时, 它们将会更难进行编写、 测试和推理。 当你能将一个函数隔离到只有一个动作, 他们将能够被容易的进行重构并且你的代码将会更容易阅读。 如 果你严格遵守本指南中的这一条, 你将会领先于许多开发者。

不好的:

function emailClients(clients) {
 clients.forEach((client)=> {
 const clientRecord=database.lookup(client);
 if (clientRecord.isActive()) {
 email(client);
 }
 });
}

好的:

function emailClients(clients) {
 clients
 .filter(isClientActive)
 .forEach(email);
}

function isClientActive(client) {
 const clientRecord=database.lookup(client);
 return clientRecord.isActive();
}

函数名称应该说明它要做什么

不好的:

function addToDate(date, month) {
 // ...
}

const date=new Date();

// 很难从函数名看出加了什么
addToDate(date, 1);

好的:

function addMonthToDate(month, date) {
 // ...
}

const date=new Date();
addMonthToDate(1, date);

函数应该只有一个抽象级别

当在你的函数中有多于一个抽象级别时, 你的函数通常做了太多事情。 拆分函数将会提升重用性和测试性。

不好的:

function parseBetterJSAlternative(code) {
 const REGEXES=[
 // ...
 ];

 const statements=code.split(' ');
 const tokens=[];
 REGEXES.forEach((REGEX)=> {
 statements.forEach((statement)=> {
 // ...
 });
 });

 const ast=[];
 tokens.forEach((token)=> {
 // lex...
 });

 ast.forEach((node)=> {
 // parse...
 });
}

好的:

function tokenize(code) {
 const REGEXES=[
 // ...
 ];

 const statements=code.split(' ');
 const tokens=[];
 REGEXES.forEach((REGEX)=> {
 statements.forEach((statement)=> {
 tokens.push( /* ... */ );
 });
 });

 return tokens;
}

function lexer(tokens) {
 const ast=[];
 tokens.forEach((token)=> {
 ast.push( /* ... */ );
 });

 return ast;
}

function parseBetterJSAlternative(code) {
 const tokens=tokenize(code);
 const ast=lexer(tokens);
 ast.forEach((node)=> {
 // parse...
 });
}

移除冗余代码

竭尽你的全力去避免冗余代码。 冗余代码是不好的, 因为它意味着当你需要修改一些逻辑时会有多个地方 需要修改。

想象一下你在经营一家餐馆, 你需要记录所有的库存西红柿, 洋葱, 大蒜, 各种香料等等。 如果你有多 个记录列表, 当你用西红柿做一道菜时你得更新多个列表。 如果你只有一个列表, 就只有一个地方需要更 新!

你有冗余代码通常是因为你有两个或多个稍微不同的东西, 它们共享大部分, 但是它们的不同之处迫使你使 用两个或更多独立的函数来处理大部分相同的东西。 移除冗余代码意味着创建一个可以处理这些不同之处的 抽象的函数/模块/类。

让这个抽象正确是关键的, 这是为什么要你遵循 Classes 那一章的 SOLID 的原因。 不好的抽象比冗 余代码更差, 所以要谨慎行事。 既然已经这么说了, 如果你能够做出一个好的抽象, 才去做。 不要重复 你自己, 否则你会发现当你要修改一个东西时时刻需要修改多个地方。

不好的:

function showDeveloperList(developers) {
 developers.forEach((developer)=> {
 const expectedSalary=developer.calculateExpectedSalary();
 const experience=developer.getExperience();
 const githubLink=developer.getGithubLink();
 const data={
 expectedSalary,
 experience,
 githubLink
 };

 render(data);
 });
}

function showManagerList(managers) {
 managers.forEach((manager)=> {
 const expectedSalary=manager.calculateExpectedSalary();
 const experience=manager.getExperience();
 const portfolio=manager.getMBAProjects();
 const data={
 expectedSalary,
 experience,
 portfolio
 };

 render(data);
 });
}

好的:

function showList(employees) {
 employees.forEach((employee)=> {
 const expectedSalary=employee.calculateExpectedSalary();
 const experience=employee.getExperience();

 let portfolio=employee.getGithubLink();

 if (employee.type==='manager') {
 portfolio=employee.getMBAProjects();
 }

 const data={
 expectedSalary,
 experience,
 portfolio
 };

 render(data);
 });
}

使用 Object.assign 设置默认对象

不好的:

const menuConfig={
 title: null,
 body: 'Bar',
 buttonText: null,
 cancellable: true
};

function createMenu(config) {
 config.title=config.title || 'Foo';
 config.body=config.body || 'Bar';
 config.buttonText=config.buttonText || 'Baz';
 config.cancellable=config.cancellable===undefined ? config.cancellable : true;
}

createMenu(menuConfig);

好的:

const menuConfig={
 title: 'Order',
 // User did not include 'body' key
 buttonText: 'Send',
 cancellable: true
};

function createMenu(config) {
 config=Object.assign({
 title: 'Foo',
 body: 'Bar',
 buttonText: 'Baz',
 cancellable: true
 }, config);

 // config now equals: {title: "Order", body: "Bar", buttonText: "Send", cancellable: true}
 // ...
}

createMenu(menuConfig);

不要使用标记位做为函数参数

标记位是告诉你的用户这个函数做了不只一件事情。 函数应该只做一件事情。 如果你的函数因为一个布尔值 出现不同的代码路径, 请拆分它们。

不好的:

function createFile(name, temp) {
 if (temp) {
 fs.create(`./temp/${name}`);
 } else {
 fs.create(name);
 }
}

好的:

function createFile(name) {
 fs.create(name);
}

function createTempFile(name) {
 createFile(`./temp/${name}`);
}

避免副作用

如果一个函数做了除接受一个值然后返回一个值或多个值之外的任何事情, 它将会产生副作用, 它可能是 写入一个文件, 修改一个全局变量, 或者意外的把你所有的钱连接到一个陌生人那里。

现在在你的程序中确实偶尔需要副作用, 就像上面的代码, 你也许需要写入到一个文件, 你需要做的是集 中化你要做的事情, 不要让多个函数或者类写入一个特定的文件, 用一个服务来实现它, 一个并且只有一 个。

重点是避免这些常见的易犯的错误, 比如在对象之间共享状态而不使用任何结构, 使用任何地方都可以写入 的可变的数据类型, 没有集中化导致副作用。 如果你能做到这些, 那么你将会比其它的码农大军更加幸福。

不好的:

// Global variable referenced by following function.
// 全局变量被下面的函数引用
// If we had another function that used this name, now it'd be an array and it
// could break it.
// 如果我们有另一个函数使用这个 name , 现在它应该是一个数组, 这可能会出现错误。
let name='Ryan McDermott';

function splitIntoFirstAndLastName() {
 name=name.split(' ');
}

splitIntoFirstAndLastName();

console.log(name); // ['Ryan', 'McDermott'];

好的:

function splitIntoFirstAndLastName(name) {
 return name.split(' ');
}

const name='Ryan McDermott';
const newName=splitIntoFirstAndLastName(name);

console.log(name); // 'Ryan McDermott';
console.log(newName); // ['Ryan', 'McDermott'];

不要写入全局函数

污染全局在 JavaScript 中是一个不好的做法, 因为你可能会和另外一个类库冲突, 你的 API 的用户 可能不够聪明, 直到他们得到在生产环境得到一个异常。 让我们来考虑这样一个例子: 假设你要扩展 JavaScript 的 原生 Array , 添加一个可以显示两个数组的不同之处的 diff 方法, 你可以在 Array.prototype 中写一个新的方法, 但是它可能会和尝试做相同事情的其它类库发生冲突。 如果有 另外一个类库仅仅使用 diff 方法来查找数组的第一个元素和最后一个元素之间的不同之处呢? 这就是 为什么使用 ES2015/ES6 的类是一个更好的做法的原因, 只要简单的扩展全局的 Array 即可。

不好的:

Array.prototype.diff=function diff(comparisonArray) {
 const hash=new Set(comparisonArray);
 return this.filter(elem=> !hash.has(elem));
};

好的:

class SuperArray extends Array {
 diff(comparisonArray) {
 const hash=new Set(comparisonArray);
 return this.filter(elem=> !hash.has(elem));
 }
}

函数式编程优于指令式编程

JavaScript 不是 Haskell 那种方式的函数式语言, 但是它有它的函数式风格。 函数式语言更加简洁 并且更容易进行测试, 当你可以使用函数式编程风格时请尽情使用。

不好的:

const programmerOutput=[
 {
 name: 'Uncle Bobby',
 linesOfCode: 500
 }, {
 name: 'Suzie Q',
 linesOfCode: 1500
 }, {
 name: 'Jimmy Gosling',
 linesOfCode: 150
 }, {
 name: 'Gracie Hopper',
 linesOfCode: 1000
 }
];

let totalOutput=0;

for (let i=0; i < programmerOutput.length; i++) {
 totalOutput +=programmerOutput[i].linesOfCode;
}

好的:

const programmerOutput=[
 {
 name: 'Uncle Bobby',
 linesOfCode: 500
 }, {
 name: 'Suzie Q',
 linesOfCode: 1500
 }, {
 name: 'Jimmy Gosling',
 linesOfCode: 150
 }, {
 name: 'Gracie Hopper',
 linesOfCode: 1000
 }
];

const totalOutput=programmerOutput
 .map((programmer)=> programmer.linesOfCode)
 .reduce((acc, linesOfCode)=> acc + linesOfCode, 0);

封装条件语句

不好的:

if (fsm.state==='fetching' && isEmpty(listNode)) {
 // ...
}

好的:

function shouldShowSpinner(fsm, listNode) {
 return fsm.state==='fetching' && isEmpty(listNode);
}

if (shouldShowSpinner(fsmInstance, listNodeInstance)) {
 // ...
}

避免负面条件

不好的:

function isDOMNodeNotPresent(node) {
 // ...
}

if (!isDOMNodeNotPresent(node)) {
 // ...
}

好的:

function isDOMNodePresent(node) {
 // ...
}

if (isDOMNodePresent(node)) {
 // ...
}

避免条件语句

这看起来似乎是一个不可能的任务。 第一次听到这个时, 多数人会说: “没有 if 语句还能期望我干 啥呢”, 答案是多数情况下你可以使用多态来完成同样的任务。 第二个问题通常是 “好了, 那么做很棒, 但是我为什么想要那样做呢”, 答案是我们学到的上一条代码整洁之道的理念: 一个函数应当只做一件事情。 当你有使用 if 语句的类/函数是, 你在告诉你的用户你的函数做了不止一件事情。 记住: 只做一件 事情。

不好的:

class Airplane {
 // ...
 getCruisingAltitude() {
 switch (this.type) {
 case '777':
 return this.getMaxAltitude() - this.getPassengerCount();
 case 'Air Force One':
 return this.getMaxAltitude();
 case 'Cessna':
 return this.getMaxAltitude() - this.getFuelExpenditure();
 }
 }
}

好的:

class Airplane {
 // ...
}

class Boeing777 extends Airplane {
 // ...
 getCruisingAltitude() {
 return this.getMaxAltitude() - this.getPassengerCount();
 }
}

class AirForceOne extends Airplane {
 // ...
 getCruisingAltitude() {
 return this.getMaxAltitude();
 }
}

class Cessna extends Airplane {
 // ...
 getCruisingAltitude() {
 return this.getMaxAltitude() - this.getFuelExpenditure();
 }
}

避免类型检查 (part 1)

JavaScript 是无类型的, 这意味着你的函数能接受任何类型的参数。 但是有时又会被这种自由咬伤, 于是又尝试在你的函数中做类型检查。 有很多种方式来避免这个, 第一个要考虑的是一致的 API 。

不好的:

function travelToTexas(vehicle) {
 if (vehicle instanceof Bicycle) {
 vehicle.peddle(this.currentLocation, new Location('texas'));
 } else if (vehicle instanceof Car) {
 vehicle.drive(this.currentLocation, new Location('texas'));
 }
}

好的:

function travelToTexas(vehicle) {
 vehicle.move(this.currentLocation, new Location('texas'));
}

避免类型检查 (part 2)

如果你使用原始的字符串、 整数和数组, 并且你不能使用多态, 但是你依然感觉到有类型检查的需要, 你应该考虑使用 TypeScript 。 它是一个常规 JavaScript 的优秀的替代品, 因为它在标准的 JavaScript 语法之上为你提供静态类型。 对常规 JavaScript 做人工类型检查的问题是需要大量的冗词来仿造类型安 全而不缺失可读性。 保持你的 JavaScript 简洁, 编写良好的测试, 并有良好的代码审阅, 否则使用 TypeScript (就像我说的, 它是一个伟大的替代品)来完成这些。

不好的:

function combine(val1, val2) {
 if (typeof val1==='number' && typeof val2==='number' ||
 typeof val1==='string' && typeof val2==='string') {
 return val1 + val2;
 }

 throw new Error('Must be of type String or Number');
}

好的:

function combine(val1, val2) {
 return val1 + val2;
}

不要过度优化

现代化浏览器运行时在幕后做大量的优化, 在大多数的时间, 做优化就是在浪费你的时间。 这些是好的 资源, 用来 查看那些地方需要优化。 为这些而优化, 直到他们被修正。

不好的:

// On old browsers, each iteration with uncached `list.length` would be costly
// because of `list.length` recomputation. In modern browsers, this is optimized.
// 在旧的浏览器上, 每次循环 `list.length` 都没有被缓存, 会导致不必要的开销, 因为要重新计
// 算 `list.length` 。 在现代化浏览器上, 这个已经被优化了。
for (let i=0, len=list.length; i < len; i++) {
 // ...
}

好的:

for (let i=0; i < list.length; i++) {
 // ...
}

移除僵尸代码

僵死代码和冗余代码同样糟糕。 没有理由在代码库中保存它。 如果它不会被调用, 就删掉它。 当你需要 它时, 它依然保存在版本历史记录中。

不好的:

function oldRequestModule(url) {
 // ...
}

function newRequestModule(url) {
 // ...
}

const req=newRequestModule;
inventoryTracker('apples', req, 'www.inventory-awesome.io');

好的:

function newRequestModule(url) {
 // ...
}

const req=newRequestModule;
inventoryTracker('apples', req, 'www.inventory-awesome.io');

对象和数据结构

使用 getters 和 setters

JavaScript 没有接口或类型, 所以坚持这个模式是非常困难的, 因为我们没有 public 和 private 关键字。 正因为如此, 使用 getters 和 setters 来访问对象上的数据比简单的在一个对象上查找属性 要好得多。 “为什么?” 你可能会问, 好吧, 原因请看下面的列表:

  • 当你想在获取一个对象属性的背后做更多的事情时, 你不需要在代码库中查找和修改每一处访问;
  • 使用 set 可以让添加验证变得容易;
  • 封装内部实现;
  • 使用 getting 和 setting 时, 容易添加日志和错误处理;
  • 继承这个类, 你可以重写默认功能;
  • 你可以延迟加载对象的属性, 比如说从服务器获取。

不好的:

class BankAccount {
 constructor() {
 this.balance=1000;
 }
}

const bankAccount=new BankAccount();

// Buy shoes...
bankAccount.balance -=100;

好的:

class BankAccount {
 constructor(balance=1000) {
 this._balance=balance;
 }

 // It doesn't have to be prefixed with `get` or `set` to be a getter/setter
 set balance(amount) {
 if (verifyIfAmountCanBeSetted(amount)) {
 this._balance=amount;
 }
 }

 get balance() {
 return this._balance;
 }

 verifyIfAmountCanBeSetted(val) {
 // ...
 }
}

const bankAccount=new BankAccount();

// Buy shoes...
bankAccount.balance -=shoesPrice;

// Get balance
let balance=bankAccount.balance;

让对象拥有私有成员

这个可以通过闭包来实现(针对 ES5 或更低)。

不好的:

const Employee=function(name) {
 this.name=name;
};

Employee.prototype.getName=function getName() {
 return this.name;
};

const employee=new Employee('John Doe');
console.log(`Employee name: ${employee.getName()}`); // Employee name: John Doe
delete employee.name;
console.log(`Employee name: ${employee.getName()}`); // Employee name: undefined

好的:

const Employee=function (name) {
 this.getName=function getName() {
 return name;
 };
};

const employee=new Employee('John Doe');
console.log(`Employee name: ${employee.getName()}`); // Employee name: John Doe
delete employee.name;
console.log(`Employee name: ${employee.getName()}`); // Employee name: John Doe

ES2015/ES6 类优先与 ES5 纯函数

很难为经典的 ES5 类创建可读的的继承、 构造和方法定义。 如果你需要继承(并且感到奇怪为啥你不需 要), 则优先用 ES2015/ES6的类。 不过, 短小的函数优先于类, 直到你发现你需要更大并且更复杂的 对象。

不好的:

const Animal=function(age) {
 if (!(this instanceof Animal)) {
 throw new Error('Instantiate Animal with `new`');
 }

 this.age=age;
};

Animal.prototype.move=function move() {};

const Mammal=function(age, furColor) {
 if (!(this instanceof Mammal)) {
 throw new Error('Instantiate Mammal with `new`');
 }

 Animal.call(this, age);
 this.furColor=furColor;
};

Mammal.prototype=Object.create(Animal.prototype);
Mammal.prototype.constructor=Mammal;
Mammal.prototype.liveBirth=function liveBirth() {};

const Human=function(age, furColor, languageSpoken) {
 if (!(this instanceof Human)) {
 throw new Error('Instantiate Human with `new`');
 }

 Mammal.call(this, age, furColor);
 this.languageSpoken=languageSpoken;
};

Human.prototype=Object.create(Mammal.prototype);
Human.prototype.constructor=Human;
Human.prototype.speak=function speak() {};

好的:

class Animal {
 constructor(age) {
 this.age=age;
 }

 move() { /* ... */ }
}

class Mammal extends Animal {
 constructor(age, furColor) {
 super(age);
 this.furColor=furColor;
 }

 liveBirth() { /* ... */ }
}

class Human extends Mammal {
 constructor(age, furColor, languageSpoken) {
 super(age, furColor);
 this.languageSpoken=languageSpoken;
 }

 speak() { /* ... */ }
}

使用方法链

这个模式在 JavaScript 中是非常有用的, 并且你可以在许多类库比如 jQuery 和 Lodash 中见到。 它使你的代码变得富有表现力, 并减少啰嗦。 因为这个原因, 我说, 使用方法链然后再看看你的代码 会变得多么简洁。 在你的类/方法中, 简单的在每个方法的最后返回 this , 然后你就能把这个类的 其它方法链在一起。

不好的:

class Car {
 constructor() {
 this.make='Honda';
 this.model='Accord';
 this.color='white';
 }

 setMake(make) {
 this.make=make;
 }

 setModel(model) {
 this.model=model;
 }

 setColor(color) {
 this.color=color;
 }

 save() {
 console.log(this.make, this.model, this.color);
 }
}

const car=new Car();
car.setColor('pink');
car.setMake('Ford');
car.setModel('F-150');
car.save();

好的:

class Car {
 constructor() {
 this.make='Honda';
 this.model='Accord';
 this.color='white';
 }

 setMake(make) {
 this.make=make;
 // NOTE: Returning this for chaining
 return this;
 }

 setModel(model) {
 this.model=model;
 // NOTE: Returning this for chaining
 return this;
 }

 setColor(color) {
 this.color=color;
 // NOTE: Returning this for chaining
 return this;
 }

 save() {
 console.log(this.make, this.model, this.color);
 // NOTE: Returning this for chaining
 return this;
 }
}

const car=new Car()
 .setColor('pink')
 .setMake('Ford')
 .setModel('F-150')
 .save();

组合优先于继承

正如设计模式四人帮所述, 如果可能, 你应该优先使用组合而不是继承。 有许多好的理由去使用继承, 也有许多好的理由去使用组合。这个格言 的重点是, 如果你本能的观点是继承, 那么请想一下组合能否更好的为你的问题建模。 很多情况下它真的 可以。

那么你也许会这样想, “我什么时候改使用继承?” 这取决于你手上的问题, 不过这儿有一个像样的列表说 明什么时候继承比组合更好用:

  1. 你的继承表示"是一个"的关系而不是"有一个"的关系(人类->动物 vs 用户->用户详情);
  2. 你可以重用来自基类的代码(人可以像所有动物一样行动);
  3. 你想通过基类对子类进行全局的修改(改变所有动物行动时的热量消耗);

不好的:

class Employee {
 constructor(name, email) {
 this.name=name;
 this.email=email;
 }

 // ...
}

// 不好是因为雇员“有”税率数据, EmployeeTaxData 不是一个 Employee 类型。
class EmployeeTaxData extends Employee {
 constructor(ssn, salary) {
 super();
 this.ssn=ssn;
 this.salary=salary;
 }

 // ...
}

好的:

class EmployeeTaxData {
 constructor(ssn, salary) {
 this.ssn=ssn;
 this.salary=salary;
 }

 // ...
}

class Employee {
 constructor(name, email) {
 this.name=name;
 this.email=email;
 }

 setTaxData(ssn, salary) {
 this.taxData=new EmployeeTaxData(ssn, salary);
 }
 // ...
}

设计模式

单一职责原则 (SRP)

正如代码整洁之道所述, “永远不要有超过一个理由来修改一个类”。 给一个类塞满许多功能, 就像你在航 班上只能带一个行李箱一样, 这样做的问题你的类不会有理想的内聚性, 将会有太多的理由来对它进行修改。 最小化需要修改一个类的次数时很重要的, 因为如果一个类拥有太多的功能, 一旦你修改它的一小部分, 将会很难弄清楚会对代码库中的其它模块造成什么影响。

不好的:

class UserSettings {
 constructor(user) {
 this.user=user;
 }

 changeSettings(settings) {
 if (this.verifyCredentials()) {
 // ...
 }
 }

 verifyCredentials() {
 // ...
 }
}

好的:

class UserAuth {
 constructor(user) {
 this.user=user;
 }

 verifyCredentials() {
 // ...
 }
}


class UserSettings {
 constructor(user) {
 this.user=user;
 this.auth=new UserAuth(user);
 }

 changeSettings(settings) {
 if (this.auth.verifyCredentials()) {
 // ...
 }
 }
}

开闭原则 (OCP)

Bertrand Meyer 说过, “软件实体 (类, 模块, 函数等) 应该为扩展开放, 但是为修改关闭。” 这 是什么意思呢? 这个原则基本上说明了你应该允许用户添加功能而不必修改现有的代码。

不好的:

class AjaxAdapter extends Adapter {
 constructor() {
 super();
 this.name='ajaxAdapter';
 }
}

class NodeAdapter extends Adapter {
 constructor() {
 super();
 this.name='nodeAdapter';
 }
}

class HttpRequester {
 constructor(adapter) {
 this.adapter=adapter;
 }

 fetch(url) {
 if (this.adapter.name==='ajaxAdapter') {
 return makeAjaxCall(url).then((response)=> {
 // transform response and return
 });
 } else if (this.adapter.name==='httpNodeAdapter') {
 return makeHttpCall(url).then((response)=> {
 // transform response and return
 });
 }
 }
}

function makeAjaxCall(url) {
 // request and return promise
}

function makeHttpCall(url) {
 // request and return promise
}

好的:

class AjaxAdapter extends Adapter {
 constructor() {
 super();
 this.name='ajaxAdapter';
 }

 request(url) {
 // request and return promise
 }
}

class NodeAdapter extends Adapter {
 constructor() {
 super();
 this.name='nodeAdapter';
 }

 request(url) {
 // request and return promise
 }
}

class HttpRequester {
 constructor(adapter) {
 this.adapter=adapter;
 }

 fetch(url) {
 return this.adapter.request(url).then((response)=> {
 // transform response and return
 });
 }
}

里氏代换原则 (LSP)

这是针对一个非常简单的里面的一个恐怖意图, 它的正式定义是: “如果 S 是 T 的一个子类型, 那么类 型为 T 的对象可以被类型为 S 的对象替换(例如, 类型为 S 的对象可作为类型为 T 的替代品)儿不需 要修改目标程序的期望性质 (正确性、 任务执行性等)。” 这甚至是个恐怖的定义。

最好的解释是, 如果你又一个基类和一个子类, 那个基类和字类可以互换而不会产生不正确的结果。 这可 能还有有些疑惑, 让我们来看一下这个经典的正方形与矩形的例子。 从数学上说, 一个正方形是一个矩形, 但是你用 "is-a" 的关系用继承来实现, 你将很快遇到麻烦。

不好的:

class Rectangle {
 constructor() {
 this.width=0;
 this.height=0;
 }

 setColor(color) {
 // ...
 }

 render(area) {
 // ...
 }

 setWidth(width) {
 this.width=width;
 }

 setHeight(height) {
 this.height=height;
 }

 getArea() {
 return this.width * this.height;
 }
}

class Square extends Rectangle {
 setWidth(width) {
 this.width=width;
 this.height=width;
 }

 setHeight(height) {
 this.width=height;
 this.height=height;
 }
}

function renderLargeRectangles(rectangles) {
 rectangles.forEach((rectangle)=> {
 rectangle.setWidth(4);
 rectangle.setHeight(5);
 const area=rectangle.getArea(); // BAD: Will return 25 for Square. Should be 20.
 rectangle.render(area);
 });
}

const rectangles=[new Rectangle(), new Rectangle(), new Square()];
renderLargeRectangles(rectangles);

好的:

class Shape {
 setColor(color) {
 // ...
 }

 render(area) {
 // ...
 }
}

class Rectangle extends Shape {
 constructor(width, height) {
 super();
 this.width=width;
 this.height=height;
 }

 getArea() {
 return this.width * this.height;
 }
}

class Square extends Shape {
 constructor(length) {
 super();
 this.length=length;
 }

 getArea() {
 return this.length * this.length;
 }
}

function renderLargeShapes(shapes) {
 shapes.forEach((shape)=> {
 const area=shape.getArea();
 shape.render(area);
 });
}

const shapes=[new Rectangle(4, 5), new Rectangle(4, 5), new Square(5)];
renderLargeShapes(shapes);

接口隔离原则 (ISP)

JavaScript 没有接口, 所以这个原则不想其它语言那么严格。 不过, 对于 JavaScript 这种缺少类 型的语言来说, 它依然是重要并且有意义的。

接口隔离原则说的是 “客户端不应该强制依赖他们不需要的接口。” 在 JavaScript 这种弱类型语言中, 接口是隐式的契约。

在 JavaScript 中能比较好的说明这个原则的是一个类需要一个巨大的配置对象。 不需要客户端去设置大 量的选项是有益的, 因为多数情况下他们不需要全部的设置。 让它们变成可选的有助于防止出现一个“胖接 口”。

不好的:

class DOMTraverser {
 constructor(settings) {
 this.settings=settings;
 this.setup();
 }

 setup() {
 this.rootNode=this.settings.rootNode;
 this.animationModule.setup();
 }

 traverse() {
 // ...
 }
}

const $=new DOMTraverser({
 rootNode: document.getElementsByTagName('body'),
 animationModule() {} // Most of the time, we won't need to animate when traversing.
 // ...
});

好的:

class DOMTraverser {
 constructor(settings) {
 this.settings=settings;
 this.options=settings.options;
 this.setup();
 }

 setup() {
 this.rootNode=this.settings.rootNode;
 this.setupOptions();
 }

 setupOptions() {
 if (this.options.animationModule) {
 // ...
 }
 }

 traverse() {
 // ...
 }
}

const $=new DOMTraverser({
 rootNode: document.getElementsByTagName('body'),
 options: {
 animationModule() {}
 }
});

依赖反转原则 (DIP)

这个原则阐述了两个重要的事情:

  1. 高级模块不应该依赖于低级模块, 两者都应该依赖与抽象;
  2. 抽象不应当依赖于具体实现, 具体实现应当依赖于抽象。

这个一开始会很难理解, 但是如果你使用过 Angular.js , 你应该已经看到过通过依赖注入来实现的这 个原则, 虽然他们不是相同的概念, 依赖反转原则让高级模块远离低级模块的细节和创建, 可以通过 DI 来实现。 这样做的巨大益处是降低模块间的耦合。 耦合是一个非常糟糕的开发模式, 因为会导致代码难于 重构。

如上所述, JavaScript 没有接口, 所以被依赖的抽象是隐式契约。 也就是说, 一个对象/类的方法和 属性直接暴露给另外一个对象/类。 在下面的例子中, 任何一个 Request 模块的隐式契约 InventoryTracker 将有一个 requestItems 方法。

不好的:

class InventoryRequester {
 constructor() {
 this.REQ_METHODS=['HTTP'];
 }

 requestItem(item) {
 // ...
 }
}

class InventoryTracker {
 constructor(items) {
 this.items=items;

 // 不好的: 我们已经创建了一个对请求的具体实现的依赖, 我们只有一个 requestItems 方法依
 // 赖一个请求方法 'request'
 this.requester=new InventoryRequester();
 }

 requestItems() {
 this.items.forEach((item)=> {
 this.requester.requestItem(item);
 });
 }
}

const inventoryTracker=new InventoryTracker(['apples', 'bananas']);
inventoryTracker.requestItems();

好的:

class InventoryTracker {
 constructor(items, requester) {
 this.items=items;
 this.requester=requester;
 }

 requestItems() {
 this.items.forEach((item)=> {
 this.requester.requestItem(item);
 });
 }
}

class InventoryRequesterV1 {
 constructor() {
 this.REQ_METHODS=['HTTP'];
 }

 requestItem(item) {
 // ...
 }
}

class InventoryRequesterV2 {
 constructor() {
 this.REQ_METHODS=['WS'];
 }

 requestItem(item) {
 // ...
 }
}

// 通过外部创建依赖项并将它们注入, 我们可以轻松的用一个崭新的使用 WebSockets 的请求模块进行
// 替换。
const inventoryTracker=new InventoryTracker(['apples', 'bananas'], new InventoryRequesterV2());
inventoryTracker.requestItems();

测试

测试比发布更加重要。 如果你没有测试或者测试不够充分, 每次发布时你就不能确认没有破坏任何事情。 测试的量由你的团队决定, 但是拥有 100% 的覆盖率(包括所有的语句和分支)是你为什么能达到高度自信 和内心的平静。 这意味着需要一个额外的伟大的测试框架, 也需要一个好的覆盖率工具。

没有理由不写测试。 这里有大量的优秀的 JS 测试框架, 选一个适合你的团队的即可。 当为团队选择了测试框架之后, 接下来的目标是为生产的每一个新的功能/模 块编写测试。 如果你倾向于测试驱动开发(TDD), 那就太棒了, 但是要点是确认你在上线任何功能或者重 构一个现有功能之前, 达到了需要的目标覆盖率。

一个测试一个概念

不好的:

const assert=require('assert');

describe('MakeMomentJSGreatAgain', ()=> {
 it('handles date boundaries', ()=> {
 let date;

 date=new MakeMomentJSGreatAgain('1/1/2015');
 date.addDays(30);
 date.shouldEqual('1/31/2015');

 date=new MakeMomentJSGreatAgain('2/1/2016');
 date.addDays(28);
 assert.equal('02/29/2016', date);

 date=new MakeMomentJSGreatAgain('2/1/2015');
 date.addDays(28);
 assert.equal('03/01/2015', date);
 });
});

好的:

const assert=require('assert');

describe('MakeMomentJSGreatAgain', ()=> {
 it('handles 30-day months', ()=> {
 const date=new MakeMomentJSGreatAgain('1/1/2015');
 date.addDays(30);
 date.shouldEqual('1/31/2015');
 });

 it('handles leap year', ()=> {
 const date=new MakeMomentJSGreatAgain('2/1/2016');
 date.addDays(28);
 assert.equal('02/29/2016', date);
 });

 it('handles non-leap year', ()=> {
 const date=new MakeMomentJSGreatAgain('2/1/2015');
 date.addDays(28);
 assert.equal('03/01/2015', date);
 });
});

并发

使用 Promises, 不要使用回调

回调不够简洁, 因为他们会产生过多的嵌套。 在 ES2015/ES6 中, Promises 已经是内置的全局类型了,使用它们吧!

不好的:

require('request').get('https://en.wikipedia.org/wiki/Robert_Cecil_Martin', (requestErr, response)=> {
 if (requestErr) {
 console.error(requestErr);
 } else {
 require('fs').writeFile('article.html', response.body, (writeErr)=> {
 if (writeErr) {
 console.error(writeErr);
 } else {
 console.log('File written');
 }
 });
 }
});

好的:

require('request-promise').get('https://en.wikipedia.org/wiki/Robert_Cecil_Martin')
 .then((response)=> {
 return require('fs-promise').writeFile('article.html', response);
 })
 .then(()=> {
 console.log('File written');
 })
 .catch((err)=> {
 console.error(err);
 });

Async/Await 比 Promises 更加简洁

Promises 是回调的一个非常简洁的替代品, 但是 ES2017/ES8 带来的 async 和 await 提供了一个 更加简洁的解决方案。 你需要的只是一个前缀为 async 关键字的函数, 接下来就可以不需要 then 函数链来编写逻辑了。 如果你能使用 ES2017/ES8 的高级功能的话, 今天就使用它吧!

不好的:

require('request-promise').get('https://en.wikipedia.org/wiki/Robert_Cecil_Martin')
 .then((response)=> {
 return require('fs-promise').writeFile('article.html', response);
 })
 .then(()=> {
 console.log('File written');
 })
 .catch((err)=> {
 console.error(err);
 });

好的:

async function getCleanCodeArticle() {
 try {
 const response=await require('request-promise').get('https://en.wikipedia.org/wiki/Robert_Cecil_Martin');
 await require('fs-promise').writeFile('article.html', response);
 console.log('File written');
 } catch(err) {
 console.error(err);
 }
}

错误处理

抛出错误是一件好事情! 他们意味着当你的程序有错时运行时可以成功确认, 并且通过停止执行当前堆栈 上的函数来让你知道, 结束当前进程(在 Node 中), 在控制台中用一个堆栈跟踪提示你。

不要忽略捕捉到的错误

对捕捉到的错误不做任何处理不能给你修复错误或者响应错误的能力。 向控制台记录错误 (console.log) 也不怎么好, 因为往往会丢失在海量的控制台输出中。 如果你把任意一段代码用 try/catch 包装那就 意味着你想到这里可能会错, 因此你应该有个修复计划, 或者当错误发生时有一个代码路径。

不好的:

try {
 functionThatMightThrow();
} catch (error) {
 console.log(error);
}

好的:

try {
 functionThatMightThrow();
} catch (error) {
 // One option (more noisy than console.log):
 console.error(error);
 // Another option:
 notifyUserOfError(error);
 // Another option:
 reportErrorToService(error);
 // OR do all three!
}

不要忽略被拒绝的 promise

与你不应忽略来自 try/catch 的错误的原因相同。

不好的:

getdata()
.then((data)=> {
 functionThatMightThrow(data);
})
.catch((error)=> {
 console.log(error);
});

好的:

getdata()
.then((data)=> {
 functionThatMightThrow(data);
})
.catch((error)=> {
 // One option (more noisy than console.log):
 console.error(error);
 // Another option:
 notifyUserOfError(error);
 // Another option:
 reportErrorToService(error);
 // OR do all three!
});

格式化

格式化是主观的。 就像其它规则一样, 没有必须让你遵守的硬性规则。 重点是不要因为格式去争论, 这 里有大量的工具来自动格式化, 使用其中的一个即可! 因 为做为工程师去争论格式化就是在浪费时间和金钱。

针对自动格式化工具不能涵盖的问题(缩进、 制表符还是空格、 双引号还是单引号等), 这里有一些指南。

使用一致的大小写

JavaScript 是无类型的, 所以大小写告诉你关于你的变量、 函数等的很多事情。 这些规则是主观的, 所以你的团队可以选择他们想要的。 重点是, 不管你们选择了什么, 要保持一致。

不好的:

const DAYS_IN_WEEK=7;
const daysInMonth=30;

const songs=['Back In Black', 'Stairway to Heaven', 'Hey Jude'];
const Artists=['ACDC', 'Led Zeppelin', 'The Beatles'];

function eraseDatabase() {}
function restore_database() {}

class animal {}
class Alpaca {}

好的:

const DAYS_IN_WEEK=7;
const DAYS_IN_MONTH=30;

const songs=['Back In Black', 'Stairway to Heaven', 'Hey Jude'];
const artists=['ACDC', 'Led Zeppelin', 'The Beatles'];

function eraseDatabase() {}
function restoreDatabase() {}

class Animal {}
class Alpaca {}

函数的调用方与被调用方应该靠近

如果一个函数调用另一个, 则在代码中这两个函数的竖直位置应该靠近。 理想情况下,保持被调用函数在被 调用函数的正上方。 我们倾向于从上到下阅读代码, 就像读一章报纸。 由于这个原因, 保持你的代码可 以按照这种方式阅读。

不好的:

class PerformanceReview {
 constructor(employee) {
 this.employee=employee;
 }

 lookupPeers() {
 return db.lookup(this.employee, 'peers');
 }

 lookupManager() {
 return db.lookup(this.employee, 'manager');
 }

 getPeerReviews() {
 const peers=this.lookupPeers();
 // ...
 }

 perfReview() {
 this.getPeerReviews();
 this.getManagerReview();
 this.getSelfReview();
 }

 getManagerReview() {
 const manager=this.lookupManager();
 }

 getSelfReview() {
 // ...
 }
}

const review=new PerformanceReview(user);
review.perfReview();

好的:

class PerformanceReview {
 constructor(employee) {
 this.employee=employee;
 }

 perfReview() {
 this.getPeerReviews();
 this.getManagerReview();
 this.getSelfReview();
 }

 getPeerReviews() {
 const peers=this.lookupPeers();
 // ...
 }

 lookupPeers() {
 return db.lookup(this.employee, 'peers');
 }

 getManagerReview() {
 const manager=this.lookupManager();
 }

 lookupManager() {
 return db.lookup(this.employee, 'manager');
 }

 getSelfReview() {
 // ...
 }
}

const review=new PerformanceReview(employee);
review.perfReview();

注释

仅仅对包含复杂业务逻辑的东西进行注释

注释是代码的辩解, 不是要求。 多数情况下, 好的代码就是文档。

不好的:

function hashIt(data) {
 // The hash
 let hash=0;

 // Length of string
 const length=data.length;

 // Loop through every character in data
 for (let i=0; i < length; i++) {
 // Get character code.
 const char=data.charCodeAt(i);
 // Make the hash
 hash=((hash << 5) - hash) + char;
 // Convert to 32-bit integer
 hash &=hash;
 }
}

好的:

function hashIt(data) {
 let hash=0;
 const length=data.length;

 for (let i=0; i < length; i++) {
 const char=data.charCodeAt(i);
 hash=((hash << 5) - hash) + char;

 // Convert to 32-bit integer
 hash &=hash;
 }
}

不要在代码库中保存注释掉的代码

因为有版本控制, 把旧的代码留在历史记录即可。

不好的:

doStuff();
// doOtherStuff();
// doSomeMoreStuff();
// doSoMuchStuff();

好的:

doStuff();

? 返回顶部

不要有日志式的注释

记住, 使用版本控制! 不需要僵尸代码, 注释掉的代码, 尤其是日志式的注释。 使用 git log 来 获取历史记录。

不好的:

/**
 * 2016-12-20: Removed monads, didn't understand them (RM)
 * 2016-10-01: Improved using special monads (JP)
 * 2016-02-03: Removed type-checking (LI)
 * 2015-03-14: Added combine with type-checking (JR)
 */
function combine(a, b) {
 return a + b;
}

好的:

function combine(a, b) {
 return a + b;
}

避免占位符

它们仅仅添加了干扰。 让函数和变量名称与合适的缩进和格式化为你的代码提供视觉结构。

不好的:

////////////////////////////////////////////////////////////////////////////////
// Scope Model Instantiation
////////////////////////////////////////////////////////////////////////////////
$scope.model={
 menu: 'foo',
 nav: 'bar'
};

////////////////////////////////////////////////////////////////////////////////
// Action setup
////////////////////////////////////////////////////////////////////////////////
const actions=function() {
 // ...
};

好的:

$scope.model={
 menu: 'foo',
 nav: 'bar'
};

const actions=function() {
 // ...
};

你可以不模仿,但不能阻止你不学习