zoukankan      html  css  js  c++  java
  • Web API

    Web API 是ASP.NET平台新加的一个特性,它可以简单快速地创建Web服务为HTTP客户端提供API。Web API 使用的基础库是和一般的MVC框架一样的,但Web API并不是MVC框架的一部分,微软把Web API相关的类从 System.Web.Mvc 命名空间下提取了出来放在 System.Web.Http 命名空间下。这种理念是把 Web API 作为ASP.NET 平台的核心之一,以使Web API能使用在其他的Web应用中,或作为一个独立的服务引擎。本文将先带大家理解Web API,再教大家在MVC中使用Web API。

    本文目录

    理解 REST 和 RESTful Web API

    为了更好的理解Web API,先带大家了解一下 REST 和 RESTful Web API。以下内容大多来自维基百科

    REST(全名:Representational State Transfer),中文翻译是表征状态转移(也有叫表述性状态转移),是Roy Fielding博士在2000年他的博士论文中提出来的一种软件架构风格。

    REST 从资源的角度来观察整个网络,分布在各处的资源由URI确定,而客户端的应用通过URI来获取资源的表征。获得这些表征致使这些应用程序转变了其状态。随着不断获取资源的表征,客户端应用不断地在转变着其状态,所谓表征状态转移(Representational State Transfer)。

    目前使用Web服务的三种主流的方式是:远程过程调用(RPC),面向服务架构(SOA)以及表征性状态转移(REST),其中REST模式的Web服务与复杂的SOA和RPC对比来讲显的更加简洁,越来越多的web服务开始采用REST风格设计和实现。

    需要注意的是,REST是设计风格而不是标准,但REST设计风格常基于使用HTTP,URI,和XML以及HTML这些现有的广泛流行的协议和标准。REST设计风格有如下要点:

    • 资源是由URI来指定。
    • 对资源的操作包括获取、创建、修改和删除资源,这些操作正好对应HTTP协议提供的GET、POST、PUT和DELETE方法。
    • 通过操作资源的表现形式来操作资源。
    • 资源的表现形式则是XML或HTML,取决于读者是机器还是人,是消费web服务的客户软件还是web浏览器。当然也可以是任何其他的格式,如JSON。

    另外,使用REST需要满足一些要求,如客户端和服务器结构、连接协议具有无状态性、能够利用Cache机制增进性能等。

    RESTful Web API(也称为RESTful Web服务)是一个使用HTTP并遵循REST原则的Web服务。它从以下请求资源的三个方面进行定义:

    • URI,比如:http://example.com/resources/。
    • Web服务接受与返回的互联网媒体类型,比如:JSON,XML ,YAML 等。
    • Web服务在该资源上所支持的一系列请求方法(比如:POST,GET,PUT或DELETE)。

    本文要讲的ASP.NET Web API 就是RESTful Web API的一种。下表列出了在实现 RESTful Web API 时HTTP请求方法的典型用途:

    不像基于SOAP的Web服务,RESTful Web服务并没有“正式”的标准。这是因为REST是一种架构,而SOAP只是一个协议。虽然REST不是一个标准,但在实现RESTful Web服务时可以使用其他各种标准(比如HTTP,URL,XML,PNG等)。

    那么REST和本文要讲的ASP.NET API又有什么关系呢?请继续往下阅读。

    理解 ASP.NET Web API

    ASP.NET Web API(本文简称Web API),是基于ASP.NET平台构建RESTful应用程序的框架。可以说 Web API 就是为在.NET平台下构建RESTful应用程序而生的,这也是本文要先介绍REST的原因。

    Web API基于在 MVC 应用程序中添加的一个特殊的 Controller,这种 Controller 称为 API Controller,和MVC普通的 Controller 相比它主要有如下两个不同的特点:

    1. Action 方法返回的是 Model 对象,而不是ActionResult。
    2. 在请求时,Action 方法是基于 HTTP 请求方式来选择的。

    第一个不同点很好理解,第二个不同点可能读者不太理解,一会看完本文的示例就理解了。

    从API Controller的Action方法返回给客户端的Model对象是经过JSON编码的。API Controller的设计仅是为了提供传递Web数据的服务,因此它不支持View、Layout 和其它HTML呈现相关的特性。Web API 能支持任何有Web 功能的客户端,但最常用的是为Web应用程序中的Ajax请求提供服务。

    正如在 ASP.NET MVC 小牛之路]14 - Unobtrusive Ajax 中演示的,我们也可以通过普通的Controller中创建Action方法来返回JSON格式的数据来支持Ajax,但 API controller 的方式可以使得数据相关的Action和View相关的Action分开,并且使得创建只为数据服务的应用更快更简单。

    一般我们会在下面这两种情况下选择使用API Controler:

    1. 需要大量的返回JSON格式数据的Action方法。
    2. 和HTML无关,只是纯粹为数据提供服务。

    如果你对上面这些概念还不太理解,没关系,当你阅读完本文后再回头看一下,你会对这些概念理解得更深些。

    下面我会通过例子来解释Web API是如何工作的,它非常简单,因为很多东西都和我们之前讲过的MVC的东西相同。

    创建 Web API 应用程序

    作为本文的示例,我们创建一个名为 WebServices 的MVC应用程序,选择Web API模板。在本文的这个例子中,我们创建一个名为 Reservation 的Model,代码如下:

    namespace WebServices.Models {
        public class Reservation {
            public int ReservationId { get; set; }
            public string ClientName { get; set; }
            public string Location { get; set; }
        }
    }

    为这个Model创建一个Repository 接口和它的一个简单的实现。如果你对 Repository 这个词不太理解,可以阅读:[ASP.NET MVC 小牛之路]05 - 使用 Ninject 。为了简单,我们直接在Models文件夹中添加一个名为 IReservationRepository 的接口,代码如下:

    namespace WebServices.Models {
        public interface IReservationRepository {
            IEnumerable<Reservation> GetAll();
            Reservation Get(int id);
            Reservation Add(Reservation item);
            void Remove(int id);
            bool Update(Reservation item);
        }
    }

    创建一个名为 ReservationRepository 的类,实现 IReservationRepository 接口,代码如下:

     ReservationRepository

    简单起见,我们这里没有真正实现数据持久化,只是简单的模拟。

    在我们创建好Web API应用程序时,VS已经添加好了一个默认的HomeController和Index.cshtml View。我们不打算在HomeControllerr 的Action中传递Model给View,因为一会要在View中使用JavaScript调用Web API来获取所有数据。在一个MVC工程中,你可以自由混合地使用普通Controller和API Controller。

    修改 Index.cshtml 如下:

     Index.cshtml

    并把 _Layout.cshtml 中的一些没用的内容清理一下,删除 /Content/Site.css 中的样式后添加如下样式代码:

     CSS

    到这,我们的程序运行起来是这样的:

    接下来我们需要创建一个API Controller,通过它我们可以用JavaScript代码和Repository的内容进行交互。

    右击  Controllers 文件夹,选择“添加”--"控制器",在弹出的对话框中修改Controller的名称为 ReservationController,并从模板中选择 Empty API。添加好后,修改 ReservationController 如下:

     ReservationController

    ReservationController的基类是  System.Web.Http.ApiController,也实现了 IController 接口(在[ASP.NET MVC 小牛之路]09 - Controller 和 Action (1)中有介绍),它和普通Controller的基类System.Web.Mvc.Controller 处理请求的方式基本上是一样的。

    到这我们就已经创建好了一个Web API了。

    测试 API Controller

    我们先来看看创建好的 API Controller 能否工作,下文将通过 API Controller 对数据的处理结果来解释API Controller 如何工作。

    运行程序,URL定位到 /api/reservation。你看到的结果将依赖于你所使用的版本,如果你使用的是IE 10/11,会弹出一个提示保存文件的对话框,该文件的内容是以下JSON数据:

    [{"ReservationId":1,"ClientName":"Adam","Location":"London"}, 
     {"ReservationId":2,"ClientName":"Steve","Location":"New York"}, 
     {"ReservationId":3,"ClientName":"Jacqui","Location":"Paris"}]

    如果你使用的是另外一种浏览器,如Chrome或Firefox,浏览器将显示如下XML数据:

    <ArrayOfReservation> 
        <Reservation> 
            <ClientName>Adam</ClientName> 
            <Location>London</Location> 
            <ReservationId>1</ReservationId> 
        </Reservation> 
        <Reservation> 
            <ClientName>Steve</ClientName> 
           <Location>New York</Location> 
           <ReservationId>2</ReservationId> 
        </Reservation> 
        <Reservation> 
            <ClientName>Jacqui</ClientName> 
            <Location>Paris</Location> 
            <ReservationId>3</ReservationId> 
        </Reservation> 
    </ArrayOfReservation>

    对于看到的结果,我们有两点感兴趣。第一,我们请求 /api/reservation URL时,服务器返回了Model对象的列表,根据该列表的数据,我们可以推断调用的是ReservationController中的 GetAllReservations Action方法。第二,不同的浏览器接收到了不同格式的数据,我们可以猜测是由于不同版本的浏览器发送请求的方式不一样。

    实际上,之所以会有不同格式的数据结果,是因为 Web API 使用HTTP请求报文头部的Accept信息来判断客户端更愿意接收何种类型的数据。IE 接收到JSON格式的数据是因为它发送的Accept头部信息是:

    ...
    Accept: text/html, application/xhtml+xml, */* 
    ...

    浏览器通过这段报文信息告诉服务器它最想要的是 text/html 内容,其次是 application/xhtml+xml。最后的  */* 意思是如果前两种不满足,就接收任何类型的数据。

    Web API 支持JSON和XML,但它会优先选择JSON格式。即IE的发送Accept信息中的 */* 使得Web API生成了JSON格式的数据。下面是 Chrome 浏览器发送的Accept头部信息:

    ... 
    Accept:text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 
    ...

    这段信息的 application/xml 比 */* 优先级高,所以Web API选择为Chrome浏览器返回XML格式的数据。

    对于Web服务,JSON已经开始大幅度替代XML了,因为XML冗长难以处理,尤其是在JavaScript中。

    API Controller 如何工作

    通过观察Web API返回结果的数据,我们似乎有点理解API Controller是如何工作的了。如果你再将请求URL改为 /api/reservation/3,你将看到这样的JSON数据(使用IE):

    {"ReservationId":3,"ClientName":"Jacqui","Location":"Paris"}

    这时,你可能更加明白了什么。这时返回的数据是 ReservationId 为 3 的Reservation对象,我们可以推测Web API根据请求的URL(/api/reservation/3)调用的是GetReservation这个Action方法。这让我们想到了之前讲过的路由知识[ASP.NET MVC 小牛之路]07 - URL Routing

    API Controller 在 /App_Start/WebApiConfig.cs 中有它们自己的路由配置,你可以打开该文件看看VS默认注册好的路由:

    public static void Register(HttpConfiguration config) {
        config.Routes.MapHttpRoute(
            name: "DefaultApi",
            routeTemplate: "api/{controller}/{id}",
            defaults: new { id = RouteParameter.Optional }
        );
    }

    API Controller使用的路由配置(WebApiConfig.cs)和普通MVC使用的配置(RouteConfig.cs)是在两个分开的文件中,注册方法接收的参数(HtttpConfiguration 对象)和添加路由配置的方法(MapHttpRoute)也不一样。原理都是和 [ASP.NET MVC 小牛之路]07 - URL Routing 讲的一样的,这里不再累述了。

    这个默认的Web API路由有一个静态片段(api),还有controller和 id片段变量。和MVC路由一个关键不同点是,它没有action片段变量。当然也可以和MVC一样定义action片段变量,你可以阅读 Routing in ASP.NET Web API 文章来了解更多Web API路由的细节。

    当应用程序接收到一个和Web API 路由匹配的请求时,Action方法的调用将取决于发送HTTP请求的方式。当我们用 /api/reservation URL测试API Controller时,浏览器指定的是GET方式的请求。API Controller的基类 ApiController根据路由信息知道需要调用哪个Controller,并根据HTTP请求的方式寻找适合的Action方法。

    一般约定在Action方法前加上HTTP请求方式名作为前缀。这里前缀只是个约定,Web API能够匹配到任何包含了HTTP请求方式名的Action方法。也就是说,本文示例的GET请求将会匹配 GetAllReservations 和 GetReservation,也能够匹配 DoGetReservation 或 ThisIsTheGetAction。

    对于两个含有相同HTTP请求方式的Action方法,API Controlller会根据它们的参数和路由信息来寻找最佳的匹配。例如请求 /api/reservation URL,GetAllReservations 方法会被匹配,因为它没有参数;请求 /api/reservation/3 URL,GetReservation 方法会被匹配,因为该方法的参数名和URL的 /3 片段对应的片段变量名相同。我们还可以使用 POST、DELETE 和 PUT请求方式来指定ReservationController的其它Action方法。这就是前文提到的REST的风格。

    但有的时候为了用HTTP方式名来给Action方法命名会显得很不自然,比如 PutReservation,习惯上会用 UpdateReservation。不仅用PUT命名不自然,POST也是一样的。这时候就需要使用类似于MVC的Controller中使用的Action方法选择器了。在System.Web.Http 命名空间下同样包含了一系列用于指定HTTP请求方式的特性,如下所示:

    public class ReservationController : ApiController {
        ...
        [HttpPost]
        public Reservation CreateReservation(Reservation item) {
            return repo.Add(item);
        }
        [HttpPut]
        public bool UpdateReservation(Reservation item) {
            return repo.Update(item);
        } 
        ...
    }

    从这我们也看到,在API Controller中使用东西很多都是和MVC中的Controller是一样的,但它们分别在 System.Web.Http 和 System.Web.Mvc两个不同的命名空间下,正如本文开始所说,Web API把MVC中的很多东西抽取出来放在System.Web.Http命名空间中,以使Web API作为ASP.NET平台的一个独立的核心。

    使用 JavaScript 和 Web API 交互

    在Scripts文件夹下添加一个Home文件夹,在该文件夹下添加一个 Index.js 文件。在我们写JS代码之前,先把这个JS文件引用到Index.cshtml中,如下:

    ...
    @section scripts {
        <script src="~/Scripts/jquery.unobtrusive-ajax.js"></script>
        <script src="~/Scripts/Home/Index.js"></script>
    }
    ...

    在 Index.js 中先把一些基本的方法写好,如下:

     Index.js

    我们定义了三个方法。第一个 selectView 方法用于控制div的显示和隐藏。第二个 getData 方法使用jQuery Ajax通过GET请求/api/reservation URL,并将返回的JSON数据填充到summaryDisplay的table中。第三个是jQuery的ready函数,在页面内容加载完成时执行。这时的效果如下:

      

    接下来一步一步完美编辑、保存和删除的功能。添加“编辑”功能代码如下:

    ...
    case "edit":
        $.ajax({
            type: "GET",
            url: "/api/reservation/" + selectedRadio.attr('value'),
            success: function (data) {
                $('#editReservationId').val(data.ReservationId);
                $('#editClientName').val(data.ClientName);
                $('#editLocation').val(data.Location);
                selectView("edit");
            }
        });
        break;
    ...

    当用户点击编辑时,先会取得radio button的value值,并以此组成一个URL(如/api/reservation/1),HTTP请求方式(GET)和URL将使API Controller调用 GetReservation 方法获取一个Reservation对象的JSON,并将其填充到editDisplay的文本框中。效果如下:

     

    下面再完善一下删除和保存功能。代码如下:

    ...
    case "delete":
        $.ajax({
            type: "DELETE",
            url: "/api/reservation/" + selectedRadio.attr('value'),
            success: function (data) {
                selectedRadio.closest('tr').remove();
            }
        });
        break;
    ...
    case "submitEdit":
        $.ajax({
            type: "PUT",
            url: "/api/reservation/" + selectedRadio.attr('value'),
            data: $('#editForm').serialize(),
            success: function (result) {
                if (result) {
                    var cells = selectedRadio.closest('tr').children();
                    cells[1].innerText = $('#editClientName').val();
                    cells[2].innerText = $('#editLocation').val();
                    selectView("summary");
                }
            }
        });
        break;
    ...

    根据Ajax的DELETE请求,API Controller将调用 DeleteReservation 将选中的Reservation对象从集合中删除。同样,根据PUT请求API Controller 将调用PutReservation方法。

    最后完善一下添加功能,该ajax请求使用的是 Unobtrusive Ajax,修改 Index.cshtml 如下:

    ...
    <h4>Add New Reservation</h4>
    @{
        AjaxOptions addAjaxOpts = new AjaxOptions {
            OnSuccess = "getData",
            Url = "/api/reservation" 
        };
    }
    @using (Ajax.BeginForm(addAjaxOpts)) {
        @Html.Hidden("ReservationId", 0)
        <p><label>Name:</label>@Html.Editor("ClientName")</p>
        <p><label>Location:</label>@Html.Editor("Location")</p>
        <button type="submit">Submit</button>
    }
    ...

    Ajax.BeginForm生成的表单默认使用的是POST请求,相应的 API Controller将调用PostReservation 方法添加Reservation对象。效果如下:

     

    通过这个完整的示例我们可以看到,熟悉MVC后,使用Web API也非常简单,操作上基本和MVC类似,主要的不同体现在 ApiController 和 Action方法的匹配上。


    参考:《Pro ASP.NET MVC 4 4th Edition》

    作者:Liam Wang

    出处:http://www.cnblogs.com/willick/

    联系:liam.wang@live.com

    本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。如有问题或建议,请多多赐教,非常感谢。

     
     
    标签: MVCASP.NETWeb API

    Web API路由

    Asp.Net Web API 导航

        Asp.Net Web API第一课——入门 http://www.cnblogs.com/aehyok/p/3432158.html

          Asp.Net Web API第二课——CRUD操作 http://www.cnblogs.com/aehyok/p/3434578.html

          Asp.Net Web API第三课——.NET客户端调用Web API http://www.cnblogs.com/aehyok/p/3439698.html

          Asp.Net Web API第四课——HTTPClient消息处理器 http://www.cnblogs.com/aehyok/p/3441915.html

    前言

    本文描述了 ASP.NET Web API 如何将 HTTP 请求路由到控制器。

    如果你熟悉Asp.Net MVC,Web API的路由与Asp.Net MVC的路由是非常类似的。这主要的区别就是Web API使用的是HTTP方法,而不是URI路径来选择Action。你也可以在Web API中使用MVC风格的路由。本文不需要有任何Asp.Net MVC的基础。

    Routing Tables路由表

      在Asp.Net Web API中,一个控制器就是一个处理HTTP请求的类,控制器的public 方法被叫做action方法或者简单的Aciton。当Web API接收到一个请求的时候,它将这个请求路由到一个Action。

      为了确定那个Action被调用,这个框架使用了一个路由表。Visual Studio中Web API的项目模板会创建一个默认路由:

                config.Routes.MapHttpRoute(
                    name: "DefaultApi",
                    routeTemplate: "api/{controller}/{id}",
                    defaults: new { id = RouteParameter.Optional }
                );

    这个路由是在WebApiConfig.cs文件中定义的,该文件位于App_Start目录。

    关于WebApiConfig类的更多信息参阅“配置ASP.NET Web API”(暂未实现)

    如果你要自己托管(self-host )Web API,你必须直接在HttpSelfHostConfiguration对象上设置路由表。更多信息参阅“自托管Web API"。(暂未实现)

      路由表中的每一个条目都包含一个路由模板。这个Web API默认的路由模版是"api/{controller}/{id}"。在这个模版中,“api”是一个文字式路径片段,而{controller}和{id}则是占位符变量。

      当Web API框架接收一个HTTP请求时,它会试图根据路由表中的一个路由模板来匹配其URI。如果无路由匹配,客户端会接收到一个404(未找到)错误。例如,以下URI与这个默认路由的匹配:

    • /api/contacts
    • /api/contacts/1
    • /api/products/gizmo1

    然而,以下URI不匹配,因为它缺少“api”片段:

    • /contacts/1

    在路由中使用“api”的原因是为了避免与ASP.NET MVC的路由冲突。通过这种方式,可以用“/contacts”进入一个MVC控制器,而“/api/contacts”进入一个Web API控制器。当然,如果你不喜欢这种约定,你也可以修改这个默认路由表。

     一旦一个匹配的路由被发现,Web API便会选择相应的Controller和Action。

      1.为了找到Controller,Web API会把“控制器”加到{controller}变量的值。

      2.为了找到Action,Web API会查找HTTP方法,然后寻找一个名称以HTTP方法名开头的方法。例如,对于一个Get请求,Web API会查找一个以“Get…”开头的动作,如“GetContact”或“GetAllContacts”等。这种约定只应用于GET、POST、PUT和DELETE方法。通过在你的Controller上使用attributes,你可以启用其他的HTTP方法。稍后我们就会看到一个例子。

      3.路由模版中其他的占位变量,例如{id},将被映射成Action的参数。

    让我们来看一个简单的例子,假设你定义了以下控制器:

    public class ProductsController : ApiController 
    { 
        public void GetAllProducts() { } 
        public IEnumerable<Product> GetProductById(int id) { } 
        public HttpResponseMessage DeleteProduct(int id){ } 
    }

    以下是一些可能的HTTP请求,以及要被调用的每个动作:

    注意,URI中的{id}片段如果出现,会被映射成Action的id参数。在这个例子中,这个控制器定义了两个GET方法,一个带有id参数的和一个不带有id参数的。

    另外要注意,POST请求是失败的,因为该控制器未定义“Post…”方法。

    Routing Variations路由变化

     上一节描述了ASP.NET Web API基本的路由机制。本小节描述一些变化。

    HTTP方法

    替代使用HTTP方法的命名约定,你可以明确的为一个Action指定HTTP方法,通过以HttpGet、HttpPost、HttpPut或者HttpDelete属性来对Action方法进行修饰。

    在下列示例中,FindProduct方法被映射到GET请求:

    public class ProductsController : ApiController 
    { 
        [HttpGet] 
        public Product FindProduct(id) {} 
    }

    允许一个Action对应多个HTTP方法,或者允许除了Get、Put、Post、Delete方法之外的HTTP方法,需要使用AcceptVerbs注解属性,它以HTTP方法列表作为参数。

    复制代码
    public class ProductsController : ApiController
    {
        [AcceptVerbs("GET", "HEAD")]
        public Product FindProduct(id) { }
    
        // WebDAV method
        [AcceptVerbs("MKCOL")]
        public void MakeCollection() { }
    }
    复制代码

    第一个方法:指示该动作接收HTTP的GET和HEAD方法(这个HEAD没测试过)

    第二个方法:WebDAV方法(基于Web的分布式著作与版本控制的HTTP方法,是一个扩展的HTTP方法

    MKCOL是隶属于WebDAV的一个方法,它在URI指定的位置创建集合(WebDAV更没见过)

    通过Action名称路由

    在默认的路由模版中,这个Web API使用HTTP方法去选择Action。然而,你也可以在URI中创建包含动作名的路由:

    routes.MapHttpRoute( 
        name: "ActionApi", 
        routeTemplate: "api/{controller}/{action}/{id}", 
        defaults: new { id = RouteParameter.Optional } 
    );

    在这个路由模板中,{action}参数命名了控制器上的动作方法。采用这种风格的路由,需要使用注解属性来指明所允许的HTTP方法。例如,假设你的控制器已有如下方法:

    public class ProductsController : ApiController 
    { 
        [HttpGet] 
        public string Details(int id); 
    }

    在这种情况下,一个Get请求"api/Products/Details/1"将会映射到这个这个Details方法。这种风格的路由类似于Asp.Net MVC,而且可能与RPC式的API相接近。(RPC风格不太懂,还没查资料

    你也可以通过使用ActionName注解属性来覆盖动作名。在以下例子中,有两个动作映射到“api/products/thumbnail/id”。一个支持GET,而另一个支持POST:

    复制代码
    public class ProductsController : ApiController 
    { 
        [HttpGet] 
        [ActionName("Thumbnail")] 
        public HttpResponseMessage GetThumbnailImage(int id); 
    
        [HttpPost] 
        [ActionName("Thumbnail")] 
        public void AddThumbnailImage(int id); 
    }
    复制代码

    Non-Actions

    为了防止一个方法被作为一个动作所请求,可以使用NonAction注解属性。它对框架发出信号:这个方法不是一个动作,,即使它可能与路由规则匹配。

    总结

     本节课主要是提供了关于路由的整体概述。下一课的内容将会精确的描述框架如何把URL匹配到路由、如何选择控制器、以及选择动作进行调用。

    本文参考链接:http://www.asp.net/web-api/overview/web-api-routing-and-actions/routing-in-aspnet-web-api

     
     
  • 相关阅读:
    Codevs 4189 字典(字典树Trie)
    Codevs 1697 ⑨要写信
    Codevs 1904 最小路径覆盖问题
    特殊性
    继承
    分组选择符
    伪类选择符
    包含(后代)选择器
    子选择器
    类和ID选择器的区别
  • 原文地址:https://www.cnblogs.com/Leo_wl/p/3446811.html
Copyright © 2011-2022 走看看