3

表单介绍

HTML表单是用户和web站点或应用程序之间交互的主要内容之一。它们允许用户将数据发送到web站点。大多数情况下,数据被发送到web服务器,但是web页面也可以拦截它自己并使用它。

HTML表单是由一个或多个小部件组成的。这些小部件可以是文本字段(单行或多行)、选择框、按钮、复选框或单选按钮。大多数情况下,这些小部件与描述其目的的标签配对——正确实现的标签能够清楚地指示视力正常的用户和盲人用户输入表单输入的内容。

HTML表单和常规HTML文档的主要区别在于,大多数情况下,表单收集的数据被发送到web服务器。在这种情况下,您需要设置一个web服务器来接收和处理数据。

第一个表单

<form action="/my-handling-form-page" method="post">
    <div>
        <label for="name">Name:</label>
        <input type="text" id="name" />
    </div>
    <div>
        <label for="mail">E-mail:</label>
        <input type="email" id="mail" />
    </div>
    <div>
        <label for="msg">Message:</label>
        <textarea id="msg"></textarea>
    </div>
</form>

<label>元素上使用for属性;它是将标签链接到表单小部件的一种正式方式。这个属性引用相应的小部件的id。最明显的一个好处是允许用户单击标签以激活相应的小部件。(点击标签名,对应的输入框自动获取焦点)

<button> 元素

<div class="button">
<button type="submit">Send your message</button>
</div>

<button>元素也接受一个 type属性,它接受三个值中的一个:submit, reset或者 button。

  • 单击 submit 按钮 发送表单的数据到<form>元素的action 属性所定义的网页。
  • 单击 reset 按钮 将所有表单小部件重新设置为它们的默认值。
  • 单击button 按钮……不会发生任何事!这听起来很傻,但是用JavaScript构建定制按钮非常有用。

基本表单样式

clipboard.png

form {
  /* 居中表单 */
  margin: 0 auto;
  width: 400px;
  /* 显示表单的轮廓 */
  padding: 1em;
  border: 1px solid #CCC;
  border-radius: 1em;
}

/* 选择 <div> 元素之后紧跟的每个 <div> 元素。 */
form div + div {
  margin-top: 1em;
}

label {
  /* 确保所有label大小相同并正确对齐 */
  display: inline-block;
  width: 90px;
  text-align: right;
}

input, textarea {
  /* 确保所有文本输入框字体相同 textarea默认是等宽字体 */
  font: 1em sans-serif;

  /* 使所有文本输入框大小相同 */
  width: 300px;
  box-sizing: border-box;

  /* 调整文本输入框的边框样式 */
  border: 1px solid #999;
}

/* 选择获得焦点的输入字段(元素),并设置其样式 */
input:focus, textarea:focus {
  /* 给激活的元素一点高亮效果 */
  border-color: #000;
}

textarea {
  /* 使多行文本输入框和它们的label正确对齐 */
  vertical-align: top;

  /* 给文本留下足够的空间 */
  height: 5em;
}

.button {
  /* 把按钮放到和文本输入框一样的位置 */
  padding-left: 90px; /* 和label的大小一样 */
}

button {
  /* 这个外边距的大小与label和文本输入框之间的间距差不多 */
  margin-left: .5em;
}

display: inline-block; 元素能够在同一行显示。
display:inline-block 的布局方式和浮动的布局方式。

  • 对于横向排列东西来说,我更倾向与使用inline-block来布局,因为这样清晰,也不用再像浮动那样清除浮动,害怕布局混乱等等。
  • 对于浮动布局就用于需要文字环绕的时候,毕竟这才是浮动真正的用武之地,水平排列的是就交给inline-block了。

web服务器发送表单数据

  • <form> 元素将定义如何通过action 属性和 method属性来发送数据的位置和方式。
  • 还需要为我们的数据提供一个名称。这些名字对双方都很重要;在浏览器端,它告诉浏览器哪个名称提供每个数据,在服务器端,它允许服务器按名称处理每个数据块。
  • 要将数据命名为表单,您需要在每个表单小部件上使用 name 属性来收集特定的数据块。
<form action="/my-handling-form-page" method="post"> 
  <div>
    <label for="name">Name:</label>
    <input type="text" id="name" name="user_name" />
  </div>
  <div>
    <label for="mail">E-mail:</label>
    <input type="email" id="mail" name="user_email" />
  </div>
  <div>
    <label for="msg">Message:</label>
    <textarea id="msg" name="user_message"></textarea>
  </div>

  ...

表单会发送三个已命名的数据块 "user_name", "user_email", 和 "user_message"。这些数据将用使用HTTP POST 方法,把信息发送到URL为 "/my-handling-form-page"目录下。

如何构造HTML表单

  • <form> 元素: 严格禁止在一个表单内嵌套另一个表单。
  • <fieldset> 和 <legend> 元素:
  • <label> 元素
  • <div> 元素:包装标签和它的小部件。
  • 使用 HTML标题(例如,<h1>、<h2>)和分段(如<section>)来构造一个复杂的表单。
<form>
  <fieldset>
    <legend>Fruit juice size</legend>
    <p>
      <input type="radio" name="size" id="size_1" value="small">
      <label for="size_1">Small</label>
    </p>
    <p>
      <input type="radio" name="size" id="size_2" value="medium">
      <label for="size_2">Medium</label>
    </p>
    <p>
      <input type="radio" name="size" id="size_3" value="large">
      <label for="size_3">Large</label>
    </p>
  </fieldset>
</form>

clipboard.png

多个标签

<div>
  <label for="username">Name: <abbr title="required">*</abbr></label>
  <input id="username" type="text" name="username">
</div>

clipboard.png

原生表单部件

通用属性

  • autofocus :默认(false)这个布尔属性允许您指定当页面加载时元素应该自动具有输入焦点,除非用户覆盖它,例如通过键入不同的控件。文档中只有一个与表单相关的元素可以指定这个属性。
  • disabled :默认(false)这个布尔属性表示用户不能与元素交互。如果没有指定这个属性,元素将从包含的元素继承它的设置,例如<fieldset>;如果没有包含disabled属性集的元素,那么就启用了元素。
  • name : 元素的名称;这是用于表单数据提交的。
  • value : 元素的初始值。

文本输入域

  • 它们可以被标记为 readonly (用户不能修改输入值)甚至是 disabled (输入值永远不会与表单数据的其余部分一起发送)。
  • 它们可以有一个 placeholder; 这是文本输入框中出现的文本,用来简略描述输入框的目的。
  • 它们可以被限制在size (框的物理尺寸) 和 长度 (可以输入的最大字符数)。

单行文本域

<input type="text" id="comment" name="comment" value="I'm a text field">
  • 指定的type属性的值在浏览器中是未知的(比如你指定 type="date",但是浏览器不支持原生日期选择器),属性值text就是是备用值。
  • 单行文本域只有一个真正的约束:如果您输入带有换行符的文本,浏览器会在发送数据之前删除这些换行符。

下拉内容

  1. 选择框 <select>
<select id="groups" name="groups">
  <optgroup label="fruits">
    <option>Banana</option>
    <option selected>Cherry</option>
    <option>Lemon</option>
  </optgroup>
  <optgroup label="vegetables">
    <option>Carrot</option>
    <option>Eggplant</option>
    <option>Potato</option>
  </optgroup>
</select>
  • 如果一个<option>元素设置了value属性,那么当提交表单时该属性的值就会被发送。如果忽略了value属性,则使用<option>元素的内容作为选择框的值。
  • 在<optgroup>元素中,label属性显示在值之前,但即使它看起来有点像一个选项,它也不是可选的。

2.多选选择框

通过将multiple属性添加到<select>元素,您可以允许用户通过操作系统提供的默认机制来选择几个值。

<select multiple id="multi" name="multi">
  <option>Banana</option>
  <option>Cherry</option>
  <option>Lemon</option>
</select>

3.自动补全输入框 <datalist>

<label for="myFruit">What's your favorite fruit?</label>
<input type="text" name="myFruit" id="myFruit" list="mySuggestion">
<datalist id="mySuggestion">
  <option>Apple</option>
  <option>Banana</option>
  <option>Blackberry</option>
  <option>Blueberry</option>
  <option>Lemon</option>
  <option>Lychee</option>
  <option>Peach</option>
  <option>Pear</option>
</datalist>
  • 然后使用 list 属性将数据列表绑定到一个文本域(通常是一个 <input> 元素)。

4.可选中项

  • 对于大多数表单部件,一旦表单提交,所有具有name属性的小部件都会被发送,即使没有任何值被填。对于可选中项,只有在勾选时才发送它们的值。如果他们没有被勾选,就不会发送任何东西,甚至连他们的名字也没有。
// 复选
<input type="checkbox" checked id="carrots" name="carrots" value="carrots">

// 单选
<input type="radio" checked id="soup" name="meal">
  • 几个单选按钮可以连接在一起。如果它们的name属性共享相同的值,那么它们将被认为属于同一组的按钮。同一组中只有一个按钮可以同时被选;这意味着当其中一个被选中时,所有其他的都将自动未选中。如果没有选中任何一个,那么整个单选按钮池就被认为处于未知状态,并且没有以表单的形式发送任何值。
<fieldset>
  <legend>What is your favorite meal?</legend>
  <ul>
    <li>
      <label for="soup">Soup</label>
      <input type="radio" checked id="soup" name="meal" value="soup">
    </li>
    <li>
      <label for="curry">Curry</label>
      <input type="radio" id="curry" name="meal" value="curry">
    </li>
    <li>
      <label for="pizza">Pizza</label>
      <input type="radio" id="pizza" name="meal" value="pizza">
    </li>
  </ul>
</fieldset>
  • 为了获得最大的可用性和可访问性,建议您在 <fieldset> 中包围每个相关项目的列表,并使用<legend>提供对列表的全面描述。
  • 还需要为这些类型的输入提供value属性,如果您想让它们具有意义——如果没有提供任何值,则复选框和单选按钮被赋予一个 on值。

高级表单部件

1.数字

// 这将创建一个数字小部件,其值被限制为1到10之间的任何值,而其增加和减少按钮的值将更改为2。

<input type="number" name="age" id="age" min="1" max="10" step="2">
  • 通过设置min和max属性来约束该值。
  • 通过设置step属性来指定增加和减少按钮更改小部件的值的数量。
  • 在10 以下的Internet Explorer版本中不支持number 输入。

2.滑块

// 滑块的一个问题是,它们不提供任何形式的视觉反馈,以了解当前的值是什么。您需要使用JavaScript来添加这一点
<label for="beans">How many beans can you eat?</label>

// 这个例子创建了一个滑块,它可能的值在0到500之间,而它的递增/递减按钮改变值的值是+10和-10。
<input type="range" name="beans" id="beans" min="0" max="500" step="10">
<span class="beancount"></span>

var beans = document.querySelector('#beans');
var count = document.querySelector('.beancount');

// 当前值
count.textContent = beans.value;

// 改变时的函数
beans.oninput = function() {
  count.textContent = beans.value;
}

https://developer.mozilla.org...

发送表单数据

GET 方法

浏览器发送一个空的主体。因为主体是空的,如果使用该方法发送一个表单,那么发送到服务器的数据将被追加到URL。

POST 方法

需要考虑到HTTP请求体中提供的数据:“嘿,服务器,看一下这些数据,然后给我回一个适当的结果。”如果使用该方法发送表单,则将数据追加到HTTP请求的主体中。

特殊案例:发送文件

文件是二进制数据——或者被认为是这样的——而所有其他数据都是文本数据。由于HTTP是一种文本协议,所以处理二进制数据有特殊的要求。

enctype 属性

该属性允许您指定在提交表单时所生成的请求中的Content-Type的HTTP数据头的值。这个数据头非常重要,因为它告诉服务器正在发送什么样的数据。默认情况下,它的值是application/x-www-form-urlencoded。它的意思是:“这是已编码为URL参数的表单数据。”

如果你想要发送文件,你需要额外的三个步骤:

  • 将method属性设置为POST,因为文件内容不能放入URL参数中。
  • 将enctype的值设置为multipart/form-data,因为数据将被分成多个部分,每个文件分别对应一个文件以及表单正文中包含的文本数据(如果文本也输入到表单中)。
  • 包含一个或多个File picker小部件,允许用户选择将要上传的文件。
<form method="post" enctype="multipart/form-data">
  <div>
    <label for="file">Choose a file</label>
    <input type="file" id="file" name="myFile">
  </div>
  <div>
    <button>Send the file</button>
  </div>
</form>

常见的安全问题

每次向服务器发送数据时,都需要考虑安全性。到目前为止,HTML表单是最常见的攻击媒介(可能发生攻击的地方)。这些问题从来都不是来自HTML表单本身,它们来自于服务器如何处理数据。

根据你所做的事情,你会遇到一些非常有名的安全问题:

XSS 和 CSRF

跨站脚本(XSS)和跨站点请求伪造(CSRF)是常见的攻击类型,它们发生在当您将用户发送的数据显示给用户或另一个用户时。

XSS允许攻击者将客户端脚本注入到其他用户查看的Web页面中。攻击者可以使用跨站点脚本攻击的漏洞来绕过诸如同源策略之类的访问控制。这些攻击的影响可能从一个小麻烦到一个重大的安全风险。

CSRF攻击类似于XSS攻击,因为它们以相同的方式攻击——向Web页面中注入客户端脚本——但它们的目标是不同的。CSRF攻击者试图将特权升级到特权用户(比如站点管理员)的权限,以执行他们不应该执行的操作(例如,将数据发送给一个不受信任的用户)。

XSS攻击利用用户对web站点的信任,而CSRF攻击则利用网站为其用户提供的信任。

为了防止这些攻击,您应该始终检查用户发送给服务器的数据(如果需要显示),尽量不要显示用户提供的HTML内容。相反,您应该处理用户提供的数据,这样您就不会逐字地显示它。当今市场上几乎所有的框架都实现了一个最小的过滤器,它可以从任何用户发送的数据中删除HTML<script>、<iframe> 和<object> 元素。这有助于降低风险,但并不一定会消除风险。

SQL注入

SQL 注入是一种试图在目标web站点使用的数据库上执行操作的攻击类型。这通常包括发送一个SQL请求,希望服务器能够执行它(通常,当应用服务器试图存储由用户发送的数据时)。这实际上是攻击网站的主要途径之一。

其后果可能是可怕的,从数据丢失到通过使用特权升级控制整个网站基础设施的攻击。这是一个非常严重的威胁,您永远不应该存储用户发送的数据,而不执行一些清理工作(例如,在php/mysql基础设施上使用mysql_real_escape_string()。

HTTP数据头注入和电子邮件注入

当您的应用程序基于表单上用户的数据输入构建HTTP头部或电子邮件时,就会出现这种类型的攻击。这些不会直接损害您的服务器或影响您的用户,但是它们是一个更深入的问题,例如会话劫持或网络钓鱼攻击。

这些攻击大多是无声的,并且可以将您的服务器变成僵尸。

偏执:永远不要相信你的用户

那么,你如何应对这些威胁呢?这是一个远远超出本指南的主题,但是有一些规则需要牢记。最重要的原则是:永远不要相信你的用户,包括你自己;即使是一个值得信赖的用户也可能被劫持。

所有到达服务器的数据都必须经过检查和消毒。

  • 有潜在危险的字符转义。应该如何谨慎使用的特定字符取决于所使用的数据的上下文和所使用的服务器平台,但是所有的服务器端语言都有相应的功能。
  • 限制输入的数据量,只允许有必要的数据。
  • 沙箱上传文件(将它们存储在不同的服务器上,只允许通过不同的子域访问文件,或者通过完全不同的域名访问文件更好)。

表单数据校验

https://developer.mozilla.org...

如何构建表单小工具

https://developer.mozilla.org...

使用JavaScript发送表单

一共有三种方式来发送表单数据:包括两种传统的方法和一种利用formData对象的新方法.

在DOM中构建一个隐藏的iframe

异步发送表单数据的最古老方法是用DOM API构建表单,然后将其数据发送到隐藏的<iframe>。 要访问提交的结果,请获取<iframe>的内容。

<button onclick="sendData({test:'ok'})">点击我!</button>

// 首先创建一个用来发送数据的iframe.
var iframe = document.createElement("iframe");
iframe.name = "myTarget";

// 必须把这个iframe插入当前文档.
window.addEventListener("load", function () {
  iframe.style.display = "none";
  document.body.appendChild(iframe);
});

// 下面这个函数是真正用来发送数据的.
// 它只有一个参数,一个包含键值对数据格式的对象.
function sendData(data) {
  var name,
      form = document.createElement("form"),
      node = document.createElement("input");

  // 注册iframe的load事件处理程序,如果你需要在响应返回时执行一些操作的话.
  iframe.addEventListener("load", function () {
    alert("Yeah! Data sent.");
  });
    
  form.action = "http://www.cs.tut.fi/cgi-bin/run/~jkorpela/echo.cgi";
  form.target = iframe.name;

  for(name in data) {
    node.name  = name;
    node.value = data[name].toString();
    form.appendChild(node.cloneNode());
  }

  // 表单元素需要添加到主文档中.
  form.style.display = "none";
  document.body.appendChild(form);

  form.submit();

  // 表单提交后,就可以删除这个表单,不影响下次的数据发送.
  document.body.removeChild(form);
}

手动构建XMLHttpRequest

XMLHttpRequest是进行HTTP请求的最安全和最可靠的方式。 要使用XMLHttpRequest发送表单数据,请通过对URL进行编码来准备数据,并遵守表单数据请求的具体内容。

<button type="button" onclick="sendData({test:'ok'})">点击我!</button>

function sendData(data) {
  var XHR = new XMLHttpRequest();
  var urlEncodedData = "";
  var urlEncodedDataPairs = [];
  var name;

  // 将数据对象转换为URL编码的键/值对数组。
  for(name in data) {
    urlEncodedDataPairs.push(encodeURIComponent(name) + '=' + encodeURIComponent(data[name]));
  }

  // 将配对合并为单个字符串,并将所有%-编码空间替换为
  // “+”字符;匹配浏览器窗体提交的行为。
  urlEncodedData = urlEncodedDataPairs.join('&').replace(/%20/g, '+');

  // 定义成功数据提交时发生的情况
  XHR.addEventListener('load', function(event) {
    alert('Yeah! Data sent and response loaded.');
  });

  // 定义错误提示
  XHR.addEventListener('error', function(event) {
    alert('哎呀!出了问题。');
  });

  // 建立我们的请求
  XHR.open('POST', 'https://example.com/cors.php');

  // 为表单数据POST请求添加所需的HTTP头
  XHR.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');

  // 最后,发送我们的数据。
  XHR.send(urlEncodedData);
}

使用 XMLHttpRequest 和 the FormData object(表单对象)

可以使用 FormData 对象来构建用于传输的表单数据,或者获取表单元素中的数据来管理它的发送方式。 请注意,FormData 对象是“只写”,这意味着您可以更改它们,但不检索其内容。

<button type="button" onclick="sendData({test:'ok'})">点击我!</button>

function sendData(data) {
  var XHR = new XMLHttpRequest();
  var FD  = new FormData();

  // 把我们的数据添加到这个FormData对象中
  for(name in data) {
    FD.append(name, data[name]);
  }

  // 定义数据成功发送并返回后执行的操作
  XHR.addEventListener('load', function(event) {
    alert('Yeah! Data sent and response loaded.');
  });

  // 定义发生错误时执行的操作
  XHR.addEventListener('error', function(event) {
    alert('Oups! Something goes wrong.');
  });

  // 设置请求地址和方法
  XHR.open('POST', 'http://ucommbieber.unl.edu/CORS/cors.php');

  // 发送这个formData对象,HTTP请求头会自动设置
  XHR.send(FD);
}

使用绑定到表单元素上的 FormData

你也可以绑定一个 FormData 对象到一个 <form> 元素上。这会创建一个 FormData ,代表表单中包含的元素。

<form id="myForm">
  <label for="myName">告诉我你的名字:</label>
  <input id="myName" name="name" value="John">
  <input type="submit" value="提交">
</form>
window.addEventListener("load", function () {
  function sendData() {
    var XHR = new XMLHttpRequest();

    // 我们把这个 FormData 和表单元素绑定在一起。
    var FD  = new FormData(form);

    // 我们定义了数据成功发送时会发生的事。
    XHR.addEventListener("load", function(event) {
      alert(event.target.responseText);
    });

    // 我们定义了失败的情形下会发生的事
    XHR.addEventListener("error", function(event) {
      alert('哎呀!出了问题。');
    });

    // 我们设置了我们的请求
    XHR.open("POST", "http://ucommbieber.unl.edu/CORS/cors.php");

    // 发送的数据是由用户在表单中提供的
    XHR.send(FD);
  }
 
  // 我们需要获取表单元素
  var form = document.getElementById("myForm");

  // 接管表单的提交事件
  form.addEventListener("submit", function (event) {
    event.preventDefault();

    sendData();
  });
});

发送二进制数据

使用formData发送二进制数据非常简单,只需要调用append方法将你需要发送的File对象或者Blob对象添加进去.

<form id="myForm">
  <p>
    <label for="i1">文本数据:</label>
    <input id="i1" name="myText" value="一些文本数据">
  </p>
  <p>
    <label for="i2">文件数据:</label>
    <input id="i2" name="myFile" type="file">
  </p>
  <button>提交</button>
</form>
// 因为我们想获取DOM节点,
// 我们在页面加载时初始化我们的脚本.
window.addEventListener('load', function () {

  // 这些变量用于存储表单数据
  var text = document.getElementById("i1");
  var file = {
        dom    : document.getElementById("i2"),
        binary : null
      };
 
  // 使用 FileReader API 获取文件内容
  var reader = new FileReader();

  // 因为 FileReader 是异步的, 会在完成读取文件时存储结果
  reader.addEventListener("load", function () {
    file.binary = reader.result;
  });

  // 页面加载时, 如果一个文件已经被选择, 那么读取该文件.
  if(file.dom.files[0]) {
    reader.readAsBinaryString(file.dom.files[0]);
  }

  // 如果没有,一旦用户选择了它,就读取文件。
  file.dom.addEventListener("change", function () {
    if(reader.readyState === FileReader.LOADING) {
      reader.abort();
    }
    
    reader.readAsBinaryString(file.dom.files[0]);
  });

  // 在我们的主函数中发送数据
  function sendData() {
    // 如果存在被选择的文件,等待它读取完成
    // 如果没有, 延迟函数的执行
    if(!file.binary && file.dom.files.length > 0) {
      setTimeout(sendData, 10);
      return;
    }

    // 要构建我们的多部分表单数据请求,
    // 我们需要一个XMLHttpRequest 实例
    var XHR = new XMLHttpRequest();

    // 我们需要一个分隔符来定义请求的每一部分。
    var boundary = "blob";

    // 将我们的请求主体存储于一个字符串中
    var data = "";

    // 所以,如果用户已经选择了一个文件
    if (file.dom.files[0]) {
      // 在请求体中开始新的一部分
      data += "--" + boundary + "\r\n";

      // 把它描述成表单数据
      data += 'content-disposition: form-data; '
      // 定义表单数据的名称
            + 'name="'         + file.dom.name          + '"; '
      // 提供文件的真实名字
            + 'filename="'     + file.dom.files[0].name + '"\r\n';
      // 和文件的MIME类型
      data += 'Content-Type: ' + file.dom.files[0].type + '\r\n';

      // 元数据和数据之间有一条空行。
      data += '\r\n';
      
      // 添加二进制数据到请求体中
      data += file.binary + '\r\n';
    }

    // 文本数据是简单的
    // 开始一个新的部分在请求体中
    data += "--" + boundary + "\r\n";

    // 说它是表单数据,并命名它
    data += 'content-disposition: form-data; name="' + text.name + '"\r\n';
    // 元数据和数据之间有一条空行。
    data += '\r\n';

    // 添加文本数据到请求体中
    data += text.value + "\r\n";

    // 一旦完成,关闭请求体
    data += "--" + boundary + "--";

    // 定义成功提交数据执行的语句
    XHR.addEventListener('load', function(event) {
      alert('✌!数据已发送且响应已加载。');
    });

    // 定义发生错误时做的事
    XHR.addEventListener('error', function(event) {
      alert('哎呀!出了问题。');
    });

    // 建立请求
    XHR.open('POST', 'https://example.com/cors.php');

    // 添加需要的HTTP报头来处理多部分表单数据POST请求
    XHR.setRequestHeader('Content-Type','multipart/form-data; boundary=' + boundary);

    // 最后,发送数据。
    XHR.send(data);
  }

  // 访问表单…
  var form = document.getElementById("myForm");

  // ……接管提交事件
  form.addEventListener('submit', function (event) {
    event.preventDefault();
    sendData();
  });
});

旧式浏览器中的HTML 表单

https://developer.mozilla.org...

样式化表单

https://developer.mozilla.org...

高级设计 HTML 表单

https://developer.mozilla.org...


大导演里维斯
15 声望0 粉丝

半个小前端,正在?ing!