整合營銷服務商

          電腦端+手機端+微信端=數據同步管理

          免費咨詢熱線:

          在 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() {
           // ...
          };

          你可以不模仿,但不能阻止你不學習


          主站蜘蛛池模板: 日韩一区二区视频在线观看| 亚洲一区二区三区91| 精品日韩一区二区| 一区二区三区四区视频在线| 亚洲国产美国国产综合一区二区 | 久久精品无码一区二区三区日韩| 国产主播福利一区二区| 熟女少妇精品一区二区| 国产精品一区二区电影| 无码人妻一区二区三区免费手机| 亚洲无线码在线一区观看| 日韩精品电影一区亚洲| 精品一区二区三区| 在线成人一区二区| 中文字幕av人妻少妇一区二区 | 中文字幕日韩人妻不卡一区| 日韩视频在线观看一区二区| 国产婷婷色一区二区三区深爱网| 国产精品揄拍一区二区| 蜜桃视频一区二区三区在线观看| 国产亚洲无线码一区二区| 亚洲线精品一区二区三区影音先锋 | 国产激情无码一区二区三区| 国产精品一区二区AV麻豆| 国内精品视频一区二区八戒| 无码喷水一区二区浪潮AV| 精品无码综合一区二区三区| 国产怡春院无码一区二区| 久久一区二区三区精华液使用方法| 亚洲AV噜噜一区二区三区| 波多野结衣一区二区三区| 日本一区二区视频| 国产精品一区二区久久沈樵| 无码精品人妻一区二区三区免费| 无码AV一区二区三区无码| 日韩人妻无码一区二区三区久久99| 中文字幕一区二区区免| 国产成人精品一区二区秒拍| 日产亚洲一区二区三区| 国产一区二区三区小说| 一区二区三区无码高清视频|