caizhendi / blog

no something!
1 stars 0 forks source link

《javascript高级程序设计(第三版)》(3) #3

Open caizhendi opened 4 years ago

caizhendi commented 4 years ago

2020.06.15

阅读进度: 14.1.3 p415

1.表单的基础知识

在 HTML 中,表单是由<form>元素来表示的,而在 JavaScript 中,表单对应的则是 HTMLFormElement 类型。HTMLFormElement 继承了 HTMLElement,因而其他 HTML 元素具有相同的默认属性。不过,HTMLFormElement 也有它自己下列独有的属性和方法。

取得<form>元素引用的方式有好几种。其中最常见的方式就是将它看成与其他元素一样,并为其添加 id 特性,然后再像下面这样使用 getElementById()方法找到它。

var form = document.getElementById("form1"); 

其次,通过 document.forms 可以取得页面中所有的表单。在这个集合中,可以通过数值索引或name 值来取得特定的表单。

var firstForm = document.forms[0]; //取得页面中的第一个表单
var myForm = document.forms["form2"]; //取得页面中名称为"form2"的表单

另外,在较早的浏览器或者那些支持向后兼容的浏览器中,也会把每个设置了 name 特性的表单作为属性保存在 document 对象中。例如,通过 document.form2 可以访问到名为"form2"的表单。不推荐使用这种方式:一是容易出错,二是将来的浏览器可能会不支持。

1.1 提交表单

使用<input><button>都可以定义提交按钮,只要将其 type 特性的值设置为"submit"即可,而图像按钮则是通过将<input>的 type 特性值设置为"image"来定义的。

<!-- 通用提交按钮 -->
<input type="submit" value="Submit Form">
<!-- 自定义提交按钮 -->
<button type="submit">Submit Form</button>
<!-- 图像按钮 -->
<input type="image" src="graphic.gif"> 

只要表单中存在上面列出的任何一种按钮,那么在相应表单控件拥有焦点的情况下,按回车键就可以提交该表单。(textarea 是一个例外,在文本区中回车会换行。)如果表单里没有提交按钮,按回车键不会提交表单。
以这种方式提交表单时,浏览器会在将请求发送给服务器之前触发 submit 事件。这样,我们就有机会验证表单数据,并据以决定是否允许表单提交。阻止这个事件的默认行为就可以取消表单提交。

var form = document.getElementById("myForm");
EventUtil.addHandler(form, "submit", function(event){
 //取得事件对象
 event = EventUtil.getEvent(event);
 //阻止默认事件
 EventUtil.preventDefault(event);
}); 

调用 prevetnDefault()方法阻止了表单提交。
在 JavaScript 中,以编程方式调用 submit()方法也可以提交表单。而且,这种方式无需表单包含提交按钮,任何时候都可以正常提交表单。

var form = document.getElementById("myForm");
//提交表单
form.submit(); 

在以调用 submit()方法的形式提交表单时,不会触发 submit 事件,因此要记得在调用此方法之前先验证表单数据。
提交表单时可能出现的最大问题,就是重复提交表单。在第一次提交表单后,如果长时间没有反应,用户可能会变得不耐烦。这时候,他们也许会反复单击提交按钮。结果往往很麻烦(因为服务器要处理重复的请求),或者会造成错误(如果用户是下订单,那么可能会多订好几份)。解决这一问题的办法有两个:在第一次提交表单后就禁用提交按钮,或者利用 onsubmit 事件处理程序取消后续的表单提交操作。

1.2 重置表单

在用户单击重置按钮时,表单会被重置。使用 type 特性值为"reset"的<input><button>都可以创建重置按钮。

<!-- 通用重置按钮 -->
<input type="reset" value="Reset Form">
<!-- 自定义重置按钮 -->
<button type="reset">Reset Form</button> 

这两个按钮都可以用来重置表单。在重置表单时,所有表单字段都会恢复到页面刚加载完毕时的初始值。如果某个字段的初始值为空,就会恢复为空;而带有默认值的字段,也会恢复为默认值。
用户单击重置按钮重置表单时,会触发 reset 事件。利用这个机会,我们可以在必要时取消重置操作。

var form = document.getElementById("myForm");
EventUtil.addHandler(form, "reset", function(event){
 //取得事件对象
 event = EventUtil.getEvent(event);
 //阻止表单重置
 EventUtil.preventDefault(event);
}); 

与提交表单一样,也可以通过 JavaScript 来重置表单。

var form = document.getElementById("myForm");
//重置表单
form.reset(); 

与调用 submit()方法不同,调用 reset()方法会像单击重置按钮一样触发 reset 事件。

caizhendi commented 4 years ago

2020.06.16

阅读进度: 14.2 p419

1.3 表单字段

可以像访问页面中的其他元素一样,使用原生 DOM 方法访问表单元素。此外,每个表单都有elements 属性,该属性是表单中所有表单元素(字段)的集合。这个 elements 集合是一个有序列表,其中包含着表单中的所有字段,例如<input><textarea><button><fieldset>

var form = document.getElementById("form1");
//取得表单中的第一个字段
var field1 = form.elements[0];
//取得名为"textbox1"的字段
var field2 = form.elements["textbox1"];
//取得表单中包含的字段的数量
var fieldCount = form.elements.length; 

如果有多个表单控件都在使用一个 name(如单选按钮),那么就会返回以该 name 命名的一个NodeList。

<form method="post" id="myForm">
 <ul>
 <li><input type="radio" name="color" value="red">Red</li>
 <li><input type="radio" name="color" value="green">Green</li>
 <li><input type="radio" name="color" value="blue">Blue</li>
 </ul>
</form> 

在这个 HTML 表单中,有 3 个单选按钮,它们的 name 都是"color",意味着这 3 个字段是一起的。在访问 elements["color"]时,就会返回一个 NodeList,其中包含这 3 个元素;不过,如果访问elements[0],则只会返回第一个元素。

var form = document.getElementById("myForm");
var colorFields = form.elements["color"];
alert(colorFields.length); //3
var firstColorField = colorFields[0];
var firstFormField = form.elements[0];
alert(firstColorField === firstFormField); //true 

也可以通过访问表单的属性来访问元素,例如 form[0]可以取得第一个表单字段,而 form["color"]则可以取得第一个命名字段。这些属性与通过 elements 集合访问到的元素是相同的。我们应该尽可能使用 elements,通过表单属性访问元素只是为了与旧浏览器向后兼容而保留的一种过渡方式。

  1. 共有的表单字段属性
    除了<fieldset>元素之外,所有表单字段都拥有相同的一组属性。由于<input>类型可以表示多种表单字段,因此有些属性只适用于某些字段,但还有一些属性是所有字段所共有的。表单字段共有的属性如下。
    • disabled:布尔值,表示当前字段是否被禁用。
    • form:指向当前字段所属表单的指针;只读。
    • name:当前字段的名称。
    • readOnly:布尔值,表示当前字段是否只读。
    • tabIndex:表示当前字段的切换(tab)序号。
    • type:当前字段的类型,如"checkbox"、"radio",等等。
    • value:当前字段将被提交给服务器的值。对文件字段来说,这个属性是只读的,包含着文件在计算机中的路径。

除了 form 属性之外,可以通过 JavaScript 动态修改其他任何属性。

var form = document.getElementById("myForm");
var field = form.elements[0];
//修改 value 属性
field.value = "Another value";
//检查 form 属性的值
alert(field.form === form); //true
//把焦点设置到当前字段
field.focus();
//禁用当前字段
field.disabled = true;
//修改 type 属性(不推荐,但对<input>来说是可行的)
field.type = "checkbox"; 

能够动态修改表单字段属性,意味着我们可以在任何时候,以任何方式来动态操作表单。例如,很多用户可能会重复单击表单的提交按钮。
最常见的解决方案,就是在第一次单击后就禁用提交按钮。只要侦听 submit 事件,并在该事件发生时禁用提交按钮即可。

//避免多次提交表单
EventUtil.addHandler(form, "submit", function(event){
 event = EventUtil.getEvent(event);
 var target = EventUtil.getTarget(event);
 //取得提交按钮
 var btn = target.elements["submit-btn"];
 //禁用它
 btn.disabled = true;
}); 

注意,不能通过 onclick 事件处理程序来实现这个功能,原因是不同浏览器之间存在“时差”:有的浏览器会在触发表单的 submit 事件之前触发 click 事件,而有的浏览器则相反。
除了<fieldset>之外,所有表单字段都有 type 属性。对于<input>元素,这个值等于 HTML 特性 type 的值。 见p417
<input><button>元素的 type 属性是可以动态修改的,而<select>元素的 type 属性则是只读的。

  1. 共有的表单字段方法
    每个表单字段都有两个方法:focus()和 blur()。
    focus()方法用于将浏览器的焦点设置到表单字段,即激活表单字段,使其可以响应键盘事件。
    可以侦听页面的 load 事件,并在该事件发生时在表单的第一个字段上调用 focus()方法。
    EventUtil.addHandler(window, "load", function(event){
    document.forms[0].elements[0].focus();
    }); 

    如果第一个表单字段是一个<input>元素,且其 type 特性的值为"hidden",那么以上代码会导致错误。另外,如果使用 CSS 的 display 和 visibility 属性隐藏了该字段,同样也会导致错误。
    HTML5 为表单字段新增了一个 autofocus 属性。在支持这个属性的浏览器中,只要设置这个属性,不用 JavaScript 就能自动把焦点移动到相应字段。
    为了保证前面的代码在设置 autofocus 的浏览器中正常运行,必须先检测是否设置了该属性,如果设置了,就不用再调用 focus()了。

    EventUtil.addHandler(window, "load", function(event){
    var element = document.forms[0].elements[0];
    if (element.autofocus !== true){
    element.focus(); console.log("JS focus");
    }
    }); 

    因为 autofocus 是一个布尔值属性,所以在支持的浏览器中它的值应该是 true。(在不支持的浏览器中,它的值将是空字符串。)为此,上面的代码只有在 autofocus 不等于 true 的情况下才会调用focus(),从而保证向前兼容。
    支持 autofocus 属性的浏览器有 Firefox 4+、Safari 5+、Chrome 和 Opera9.6。在默认情况下,只有表单字段可以获得焦点。对于其他元素而言,如果先将其tabIndex 属性设置为1,然后再调用 focus()方法,也可以让这些元素获得焦点。只有 Opera 不支持这种技术。
    与 focus()方法相对的是 blur()方法,它的作用是从元素中移走焦点。

    document.forms[0].elements[0].blur(); 
  2. 共有的表单字段事件
    除了支持鼠标、键盘、更改和 HTML 事件之外,所有表单字段都支持下列 3 个事件。
    • blur:当前字段失去焦点时触发。
    • change:对于<input><textarea>元素,在它们失去焦点且 value 值改变时触发;对于<select>元素,在其选项改变时触发。
    • focus:当前字段获得焦点时触发。

当用户改变了当前字段的焦点,或者我们调用了 blur()或 focus()方法时,都可以触发 blur 和focus 事件。这两个事件在所有表单字段中都是相同的。但是,change 事件在不同表单控件中触发的次数会有所不同。
对于<input><textarea>元素,当它们从获得焦点到失去焦点且 value 值改变时,才会触发 change 事件。
对于<select>元素,只要用户选择了不同的选项,就会触发 change 事件;换句话说,不失去焦点也会触发 change 事件。
假设有一个文本框,我们只允许用户输入数值。此时,可以利用focus 事件修改文本框的背景颜色,以便更清楚地表明这个字段获得了焦点。可以利用 blur 事件恢复文本框的背景颜色,利用 change 事件在用户输入了非数值字符时再次修改背景颜色。

var textbox = document.forms[0].elements[0];
EventUtil.addHandler(textbox, "focus", function(event){
 event = EventUtil.getEvent(event); 
  var target = EventUtil.getTarget(event);

 if (target.style.backgroundColor != "red"){
 target.style.backgroundColor = "yellow";
 }
});
EventUtil.addHandler(textbox, "blur", function(event){
 event = EventUtil.getEvent(event);
 var target = EventUtil.getTarget(event);

 if (/[^\d]/.test(target.value)){
 target.style.backgroundColor = "red";
 } else {
 target.style.backgroundColor = "";
 }
});
EventUtil.addHandler(textbox, "change", function(event){
 event = EventUtil.getEvent(event);
 var target = EventUtil.getTarget(event);

 if (/[^\d]/.test(target.value)){
 target.style.backgroundColor = "red";
 } else {
 target.style.backgroundColor = "";
 }
}); 

关于 blur 和 change 事件的关系,并没有严格的规定。在某些浏览器中,blur事件会先于 change 事件发生;而在其他浏览器中,则恰好相反。

caizhendi commented 4 years ago

2020.06.17

阅读进度: 14.2.3 p426

2 文本框脚本

在 HTML 中,有两种方式来表现文本框:一种是使用<input>元素的单行文本框,另一种是使用<textarea>的多行文本框。
要表现文本框,必须将<input>元素的 type 特性设置为"text"。而通过设置 size 特性,可以指定文本框中能够显示的字符数。通过 value 特性,可以设置文本框的初始值,而 maxlength 特性则用于指定文本框可以接受的最大字符数。

<input type="text" size="25" maxlength="50" value="initial value"> 

<textarea>元素则始终会呈现为一个多行文本框。要指定文本框的大小,可以使用 rows和 cols 特性。其中,rows 特性指定的是文本框的字符行数,而 cols 特性指定的是文本框的字符列数(类似于<inpu>元素的 size 特性)。与<input>元素不同,<textarea>的初始值必须要放在<textarea></textarea>之间。

<textarea rows="25" cols="5">initial value</textarea> 

另一个与<input>的区别在于,不能在 HTML 中给<textarea>指定最大字符数。
它们都会将用户输入的内容保存在 value 属性中,可以通过这个属性读取和设置文本框的值。

var textbox = document.forms[0].elements["textbox1"];
alert(textbox.value);
textbox.value = "Some new value"; 

我们建议读者像上面这样使用 value 属性读取或设置文本框的值,不建议使用标准的 DOM 方法。不要使用 setAttribute()设置<input>元素的 value 特性,也不要去修改<textarea>元素的第一个子节点。原因:对 value 属性所作的修改,不一定会反映在 DOM 中。因此,在处理文本框的值时,最好不要使用 DOM 方法。

2.1 选择文本

上述两种文本框都支持 select()方法,这个方法用于选择文本框中的所有文本。在调用 select()方法时,大多数浏览器(Opera 除外)都会将焦点设置到文本框中。这个方法不接受参数。

var textbox = document.forms[0].elements["textbox1"];
textbox.select();

在文本框获得焦点时选择其所有文本,这是一种非常常见的做法,特别是在文本框包含默认值的时候。

EventUtil.addHandler(textbox, "focus", function(event){
 event = EventUtil.getEvent(event);
 var target = EventUtil.getTarget(event);

 target.select();
});
  1. 选择(select)事件
    与 select()方法对应的,是一个 select 事件。在选择了文本框中的文本时,就会触发 select事件。不过,到底什么时候触发 select 事件,还会因浏览器而异。
    在 IE9+、Opera、Firefox、Chrome和 Safari 中,只有用户选择了文本(而且要释放鼠标),才会触发 select 事件。而在 IE8 及更早版本中,只要用户选择了一个字母(不必释放鼠标),就会触发 select 事件。另外,在调用 select()方法时也会触发 select 事件。
    var textbox = document.forms[0].elements["textbox1"];
    EventUtil.addHandler(textbox, "select", function(event){
    var alert("Text selected" + textbox.value);
    }); 
  2. 取得选择的文本
    虽然通过select 事件我们可以知道用户什么时候选择了文本,但仍然不知道用户选择了什么文本。HTML5 通过一些扩展方案解决了这个问题,以便更顺利地取得选择的文本。该规范采取的办法是添加两个属性:selectionStart 和 selectionEnd。这两个属性中保存的是基于 0 的数值,表示所选择文本的范围(即文本选区开头和结尾的偏移量)。
    function getSelectedText(textbox){
    return textbox.value.substring(textbox.selectionStart, textbox.selectionEnd);
    } 

    因 为 substring() 方法基于字符串的偏移量执行操作,所以将 selectionStart 和selectionEnd 直接传给它就可以取得选中的文本。
    IE9+、Firefox、Safari、Chrome 和 Opera 都支持这两个属性。IE8 及之前版本不支持这两个属性,而是提供了另一种方案。
    IE8 及更早的版本中有一个 document.selection 对象,其中保存着用户在整个文档范围内选择的文本信息;也就是说,无法确定用户选择的是页面中哪个部位的文本。不过,在与 select 事件一起使用的时候,可以假定是用户选择了文本框中的文本,因而触发了该事件。要取得选择的文本,首先必须创建一个范围(第 12 章讨论过),然后再将文本从其中提取出来。

    function getSelectedText(textbox){
    if (typeof textbox.selectionStart == "number"){
    return textbox.value.substring(textbox.selectionStart,
    textbox.selectionEnd);
    } else if (document.selection){
    return document.selection.createRange().text;
    }
    } 
  3. 选择部分文本
    HTML5 也为选择文本框中的部分文本提供了解决方案,即最早由 Firefox 引入的setSelectionRange()方法。现在除select()方法之外,所有文本框都有一个setSelectionRange()方法。这个方法接收两个参数:要选择的第一个字符的索引和要选择的最后一个字符之后的字符的索引(类似于 substring()方法的两个参数)。
    textbox.value = "Hello world!"
    //选择所有文本
    textbox.setSelectionRange(0, textbox.value.length); //"Hello world!"
    //选择前 3 个字符
    textbox.setSelectionRange(0, 3); //"Hel"
    //选择第 4 到第 6 个字符
    textbox.setSelectionRange(4, 7); //"o w" 

    要看到选择的文本,必须在调用 setSelectionRange()之前或之后立即将焦点设置到文本框。IE9、Firefox、Safari、Chrome 和 Opera 支持这种方案。
    IE8 及更早版本支持使用范围(第 12 章讨论过)选择部分文本。要选择文本框中的部分文本,必须首先使用 IE 在所有文本框上提供的 createTextRange()方法创建一个范围,并将其放在恰当的位置上。然后,再使用 moveStart()和 moveEnd()这两个范围方法将范围移动到位。不过,在调用这两个方法以前,还必须使用 collapse()将范围折叠到文本框的开始位置。此时,moveStart()将范围的起点和终点移动到了相同的位置,只要再给 moveEnd()传入要选择的字符总数即可。最后一步,就是使用范围的 select()方法选择文本。

    textbox.value = "Hello world!";
    var range = textbox.createTextRange();
    //选择所有文本
    range.collapse(true);
    range.moveStart("character", 0);
    range.moveEnd("character", textbox.value.length); //"Hello world!"
    range.select();
    //选择前 3 个字符
    range.collapse(true);
    range.moveStart("character", 0);
    range.moveEnd("character", 3);
    range.select(); //"Hel"
    //选择第 4 到第 6 个字符
    range.collapse(true);
    range.moveStart("character", 4);
    range.moveEnd("character", 3);
    range.select(); //"o w" 

    与在其他浏览器中一样,要想在文本框中看到文本被选择的效果,必须让文本框获得焦点。为了实现跨浏览器编程,可以将上述两种方案组合起来。

    function selectText(textbox, startIndex, stopIndex){
    if (textbox.setSelectionRange){
    textbox.setSelectionRange(startIndex, stopIndex);
    } else if (textbox.createTextRange){
    var range = textbox.createTextRange();
    range.collapse(true);
    range.moveStart("character", startIndex);
    range.moveEnd("character", stopIndex - startIndex);
    range.select();
    }  textbox.focus();
    } 

    这个 selectText()函数接收三个参数:要操作的文本框、要选择文本中第一个字符的索引和要选择文本中最后一个字符之后的索引。

    textbox.value = "Hello world!"
    //选择所有文本
    selectText(textbox, 0, textbox.value.length); //"Hello world!"
    //选择前 3 个字符
    selectText(textbox, 0, 3); //"Hel"
    //选择第 4 到第 6 个字符
    selectText(textbox, 4, 7); //"o w" 

2.2 过滤输入

我们经常会要求用户在文本框中输入特定的数据,或者输入特定格式的数据。例如,必须包含某些字符,或者必须匹配某种模式。由于文本框在默认情况下没有提供多少验证数据的手段,因此必须使用JavaScript 来完成此类过滤输入的操作。而综合运用事件和 DOM 手段,就可以将普通的文本框转换成能够理解用户输入数据的功能型控件。

  1. 屏蔽字符
    有时候,我们需要用户输入的文本中包含或不包含某些字符。例如,电话号码中不能包含非数值字符。如前所述,响应向文本框中插入字符操作的是 keypress 事件。因此,可以通过阻止这个事件的默认行为来屏蔽此类字符。
    EventUtil.addHandler(textbox, "keypress", function(event){
    event = EventUtil.getEvent(event);
    EventUtil.preventDefault(event);
    }); 

    运行以上代码后,由于所有按键操作都将被屏蔽,结果会导致文本框变成只读的。如果只想屏蔽特定的字符,则需要检测 keypress 事件对应的字符编码,然后再决定如何响应。只允许用户输入数值。

    EventUtil.addHandler(textbox, "keypress", function(event){
    event = EventUtil.getEvent(event);
    var target = EventUtil.getTarget(event);
    var charCode = EventUtil.getCharCode(event);
    if (!/\d/.test(String.fromCharCode(charCode))){ 
      EventUtil.preventDefault(event);
    }
    }); 

    在这个例子中,我们使用 EventUtil.getCharCode()实现了跨浏览器取得字符编码。然后,使用 String.fromCharCode()将字符编码转换成字符串,再使用正则表达式 /\d/ 来测试该字符串,从而确定用户输入的是不是数值。如果测试失败,那么就使用 EventUtil.preventDefault()屏蔽按键事件。结果,文本框就会忽略所有输入的非数值。
    理论上只应该在用户按下字符键时才触发 keypress 事件,但有些浏览器也会对其他键触发此事件。Firefox 和 Safari(3.1 版本以前)会对向上键、向下键、退格键和删除键触发 keypress 事件;Safari 3.1 及更新版本则不会对这些键触发 keypress 事件。这意味着,仅考虑到屏蔽不是数值的字符还不够,还要避免屏蔽这些极为常用和必要的键。
    要检测这些键并不困难。在 Firefox 中,所有由非字符键触发的 keypress 事件对应的字符编码为 0,而在 Safari 3 以前的版本中,对应的字符编码全部为 8。为了让代码更通用,只要不屏蔽那些字符编码小于 10 的键即可。

    EventUtil.addHandler(textbox, "keypress", function(event){
    event = EventUtil.getEvent(event);
    var target = EventUtil.getTarget(event);
    var charCode = EventUtil.getCharCode(event);
    if (!/\d/.test(String.fromCharCode(charCode)) && charCode > 9){
    EventUtil.preventDefault(event);
    }
    }); 

    即可以屏蔽非数值字符,但不屏蔽那些也会触发 keypress 事件的基本按键。
    还有一个问题需要处理:复制、粘贴及其他操作还要用到 Ctrl 键。在除 IE 之外的所有浏览器中,前面的代码也会屏蔽 Ctrl+C、Ctrl+V,以及其他使用 Ctrl 的组合键。因此,最后还要添加一个检测条件,以确保用户没有按下 Ctrl 键。

    EventUtil.addHandler(textbox, "keypress", function(event){
    event = EventUtil.getEvent(event);
    var target = EventUtil.getTarget(event);
    var charCode = EventUtil.getCharCode(event);
    if (!/\d/.test(String.fromCharCode(charCode)) && charCode > 9 &&
    !event.ctrlKey){
    EventUtil.preventDefault(event);
    }
    }); 
  2. 操作剪贴板
    IE 是第一个支持与剪贴板相关事件,以及通过 JavaScript 访问剪贴板数据的浏览器。IE 的实现成为了事实上的标准,不仅 Safari 2、Chrome 和 Firefox 3 也都支持类似的事件和剪贴板访问(Opera 不支持通过 JavaScript 访问剪贴板),HTML 5 后来也把剪贴板事件纳入了规范。
    • beforecopy:在发生复制操作前触发。
    • copy:在发生复制操作时触发。
    • beforecut:在发生剪切操作前触发。
    • cut:在发生剪切操作时触发。
    • beforepaste:在发生粘贴操作前触发。
    • paste:在发生粘贴操作时触发。

由于没有针对剪贴板操作的标准,这些事件及相关对象会因浏览器而异。
在 Safari、Chrome 和 Firefox中,beforecopy、beforecut 和 beforepaste 事件只会在显示针对文本框的上下文菜单(预期将发生剪贴板事件)的情况下触发。
IE 则会在触发 copy、cut 和 paste 事件之前先行触发这些事件。至于 copy、cut 和 paste 事件,只要是在上下文菜单中选择了相应选项,或者使用了相应的键盘组合键,所有浏览器都会触发它们。
在实际的事件发生之前,通过 beforecopy、beforecut 和 beforepaste 事件可以在向剪贴板发送数据,或者从剪贴板取得数据之前修改数据。不过,取消这些事件并不会取消对剪贴板的操作——只有取消 copy、cut 和 paste 事件,才能阻止相应操作发生。
要访问剪贴板中的数据,可以使用 clipboardData 对象:在 IE 中,这个对象是 window 对象的属性;而在 Firefox 4+、Safari 和 Chrome 中,这个对象是相应 event 对象的属性。但是,在 Firefox、Safari 和 Chorme 中,只有在处理剪贴板事件期间 clipboardData 对象才有效,这是为了防止对剪贴板的未授权访问;在 IE 中,则可以随时访问 clipboardData 对象。为了确保跨浏览器兼容性,最好只在发生剪贴板事件期间使用这个对象。
这个 clipboardData 对象有三个方法:getData()、setData()和 clearData()。
getData()用于从剪贴板中取得数据,它接受一个参数,即要取得的数据的格式。在 IE 中,有两种数据格式:"text"和"URL"。在 Firefox、Safari 和 Chrome 中,这个参数是一种 MIME 类型;不过,可以用"text"代表"text/plain"。
setData()方法的第一个参数也是数据类型,第二个参数是要放在剪贴板中的文本。对于第一个参数,IE 照样支持"text"和"URL",而 Safari 和 Chrome 仍然只支持 MIME 类型。但是,与getData()方法不同的是,Safari 和 Chrome 的 setData()方法不能识别"text"类型。这两个浏览器在成功将文本放到剪贴板中后,都会返回 true;否则,返回 false。为了弥合这些差异,我们可以向EventUtil 中再添加下列方法。

var EventUtil = {
 //省略的代码
 getClipboardText: function(event){
 var clipboardData = (event.clipboardData || window.clipboardData);
 return clipboardData.getData("text");
 },
 //省略的代码

 setClipboardText: function(event, value){
 if (event.clipboardData){
 return event.clipboardData.setData("text/plain", value); 
  } else if (window.clipboardData){
 return window.clipboardData.setData("text", value);
 }
 },
 //省略的代码
}; 

getClipboardText()方法相对简单;它只要确定 clipboardData 对象的位置,然后再以"text"类型调用 getData()方法即可。相应地,setClipboardText()方法则要稍微复杂一些。在取得 clipboardData 对象之后,需要根据不同的浏览器实现为 setData()传入不同的类型(对于 Safari和 Chrome,是"text/plain";对于 IE,是"text")。
在需要确保粘贴到文本框中的文本中包含某些字符,或者符合某种格式要求时,能够访问剪贴板是非常有用的。例如,如果一个文本框只接受数值,那么就必须检测粘贴过来的值,以确保有效。在 paste事件中,可以确定剪贴板中的值是否有效,如果无效,就可以像下面示例中那样,取消默认的行为。

EventUtil.addHandler(textbox, "paste", function(event){
 event = EventUtil.getEvent(event);
 var text = EventUtil.getClipboardText(event);
 if (!/^\d*$/.test(text)){
 EventUtil.preventDefault(event);
 }
}); 

在这里,onpaste 事件处理程序可以确保只有数值才会被粘贴到文本框中。如果剪贴板的值与正则表达式不匹配,则会取消粘贴操作。Firefox、Safari 和 Chrome 只允许在 onpaste 事件处理程序中访问 getData()方法。
由于并非所有浏览器都支持访问剪贴板,所以更简单的做法是屏蔽一或多个剪贴板操作。在支持copy、cut 和 paste 事件的浏览器中(IE、Safari、Chrome 和 Firefox 3 及更高版本),很容易阻止这些事件的默认行为。在 Opera 中,则需要阻止那些会触发这些事件的按键操作,同时还要阻止在文本框中显示上下文菜单。

caizhendi commented 4 years ago

2020.06.18

阅读进度: 14.3 p431

2.3. 自动切换焦点

使用 JavaScript 可以从多个方面增强表单字段的易用性。其中,最常见的一种方式就是在用户填写完当前字段时,自动将焦点切换到下一个字段。通常,在自动切换焦点之前,必须知道用户已经输入了既定长度的数据(例如电话号码)。例如,美国的电话号码通常会分为三部分:区号、局号和另外 4 位数字。为取得完整的电话号码,很多网页中都会提供下列 3 个文本框:

<input type="text" name="tel1" id="txtTel1" maxlength="3">
<input type="text" name="tel2" id="txtTel2" maxlength="3">
<input type="text" name="tel3" id="txtTel3" maxlength="4"> 

为增强易用性,同时加快数据输入,可以在前一个文本框中的字符达到最大数量后,自动将焦点切换到下一个文本框。

(function(){
 function tabForward(event){
 event = EventUtil.getEvent(event);
 var target = EventUtil.getTarget(event);
 if (target.value.length == target.maxLength){
 var form = target.form;
 for (var i=0, len=form.elements.length; i < len; i++) {
 if (form.elements[i] == target) {
 if (form.elements[i+1]){
 form.elements[i+1].focus();
 }
 return;
 }
 }
 }
 }
 var textbox1 = document.getElementById("txtTel1");
 var textbox2 = document.getElementById("txtTel2");
 var textbox3 = document.getElementById("txtTel3");
 EventUtil.addHandler(textbox1, "keyup", tabForward);
 EventUtil.addHandler(textbox2, "keyup", tabForward);
 EventUtil.addHandler(textbox3, "keyup", tabForward);
})(); 

2.4 HTML5 约束验证API

为了在将表单提交到服务器之前验证数据,HTML5 新增了一些功能。即便 JavaScript被禁用或者由于种种原因未能加载,也可以确保基本的验证。浏览器自己会根据标记中的规则执行验证,然后自己显示适当的错误消息(完全不用 JavaScript 插手)。
这个功能只有在支持HTML5 这部分内容的浏览器中才有效,这些浏览器有 Firefox 4+、Safari 5+、Chrome 和 Opera 10+。
只有在某些情况下表单字段才能进行自动验证。就是要在 HTML 标记中为特定的字段指定一些约束,然后浏览器才会自动执行表单验证。

  1. 必填字段
    在表单字段中指定了 required 属性。
    <input type="text" name="username" required>

    任何标注有 required 的字段,在提交表单时都不能空着。这个属性适用于<input><textarea><select>字段(Opera 11 及之前版本还不支持<select>的 required 属性)。在 JavaScript 中,通过对应的 required 属性,可以检查某个表单字段是否为必填字段。

    var isUsernameRequired = document.forms[0].elements["username"].required; 

    使用下面这行代码可以测试浏览器是否支持 required 属性。

    var isRequiredSupported = "required" in document.createElement("input"); 

    以上代码通过特性检测来确定新创建的<input>元素中是否存在 required 属性。
    对于空着的必填字段,不同浏览器有不同的处理方式。Firefox 4 和 Opera 11 会阻止表单提交并在相应字段下方弹出帮助框,而 Safari(5 之前)和 Chrome(9 之前)则什么也不做,而且也不阻止表单提交。

  2. 其他输入类型
    HTML5 为<input>元素的 type 属性又增加了几个值。这些新的类型不仅能反映数据类型的信息,而且还能提供一些默认的验证功能。其中,"email"和"url"是两个得到支持最多的类型,各浏览器也都为它们增加了定制的验证机制。
    <input type="email" name ="email">
    <input type="url" name="homepage"> 

    "email"类型要求输入的文本必须符合电子邮件地址的模式,而"url"类型要求输入的文本必须符合 URL 的模式。
    前面提到的浏览器在恰当地匹配模式方面都存在问题。最明显的是"-@-"会被当成一个有效的电子邮件地址。
    要检测浏览器是否支持这些新类型,可以在 JavaScript 创建一个<input>元素,然后将 type 属性设置为"email"或"url",最后再检测这个属性的值。不支持它们的旧版本浏览器会自动将未知的值设置为"text",而支持的浏览器则会返回正确的值。

    var input = document.createElement("input");
    input.type = "email";
    var isEmailSupported = (input.type == "email"); 

    如果不给<input>元素设置 required 属性,那么空文本框也会验证通过。另一方面,设置特定的输入类型并不能阻止用户输入无效的值,只是应用某些默认的验证而已。

  3. 数值范围
    除了"email"和"url",HTML5 还定义了另外几个输入元素。这几个元素都要求填写某种基于数字的值:"number"、"range"、"datetime"、"datetime-local"、"date"、"month"、"week",还有"time"。浏览器对这几个类型的支持情况并不好。
    对所有这些数值类型的输入元素,可以指定 min 属性(最小的可能值)、max 属性(最大的可能值)和 step 属性(从 min 到 max 的两个刻度间的差值)。例如,想让用户只能输入 0 到 100 的值,而且这个值必须是 5 的倍数。
    <input type="number" min="0" max="100" step="5" name="count"> 

    在不同的浏览器中,可能会也可能不会看到能够自动递增和递减的数值调节按钮(向上和向下按钮)。
    以上这些属性在 JavaScript 中都能通过对应的元素访问(或修改)。此外,还有两个方法:stepUp()和 stepDown(),都接收一个可选的参数:要在当前值基础上加上或减去的数值。(默认是加或减 1。)这两个方法还没有得到任何浏览器支持。

    input.stepUp(); //加 1
    input.stepUp(5); //加 5
    input.stepDown(); //减 1
    input.stepDown(10); //减 10 
  4. 输入模式
    HTML5 为文本字段新增了 pattern 属性。这个属性的值是一个正则表达式,用于匹配文本框中的值。例如,如果只想允许在文本字段中输入数值,可以像下面的代码一样应用约束:
    <input type="text" pattern="\d+" name="count"> 

    模式的开头和末尾不用加^和$符号(假定已经有了)。这两个符号表示输入的值必须从头到尾都与模式匹配。
    与其他输入类型相似,指定 pattern 也不能阻止用户输入无效的文本。这个模式应用给值,浏览器来判断值是有效,还是无效。在 JavaScript 中可以通过 pattern 属性访问模式。

    var pattern = document.forms[0].elements["count"].pattern; 

    使用以下代码可以检测浏览器是否支持 pattern 属性。

    var isPatternSupported = "pattern" in document.createElement("input"); 
  5. 检测有效性
    使用 checkValidity()方法可以检测表单中的某个字段是否有效。所有表单字段都有个方法,如果字段的值有效,这个方法返回 true,否则返回 false。字段的值是否有效的判断依据是本节前面介绍过的那些约束。换句话说,必填字段中如果没有值就是无效的,而字段中的值与 pattern 属性不配也是无效的。
    if (document.forms[0].elements[0].checkValidity()){
    //字段有效,继续
    } else {
    //字段无效
    } 

    要检测整个表单是否有效,可以在表单自身调用 checkValidity()方法。如果所有表单字段都有效,这个方法返回 true;即使有一个字段无效,这个方法也会返回 false。

    if(document.forms[0].checkValidity()){
    //表单有效,继续
    } else {
    //表单无效
    } 

    与 checkValidity()方法简单地告诉你字段是否有效相比,validity 属性则会告诉你为什么字段有效或无效。这个对象中包含一系列属性,每个属性会返回一个布尔值。

    • customError :如果设置了setCustomValidity(),则为true,否则返回false。
    • patternMismatch:如果值与指定的pattern 属性不匹配,返回true。
    • rangeOverflow:如果值比max 值大,返回true。
    • rangeUnderflow:如果值比min 值小,返回true。
    • stepMisMatch:如果min 和max 之间的步长值不合理,返回true。
    • tooLong:如果值的长度超过了maxlength 属性指定的长度,返回true。有的浏览器(如Firefox 4)会自动约束字符数量,因此这个值可能永远都返回false。
    • typeMismatch:如果值不是"mail"或"url"要求的格式,返回true。
    • valid:如果这里的其他属性都是false,返回true。checkValidity()也要求相同的值。
    • valueMissing:如果标注为required 的字段中没有值,返回true。

想得到更具体的信息,就应该使用 validity 属性来检测表单的有效性。

if (input.validity && !input.validity.valid){
 if (input.validity.valueMissing){
 alert("Please specify a value.")
 } else if (input.validity.typeMismatch){
 alert("Please enter an email address.");
 } else {
 alert("Value is invalid.");
 }
} 
  1. 禁用验证
    通过设置 novalidate 属性,可以告诉表单不进行验证。
    <form method="post" action="signup.php" novalidate>
    <!--这里插入表单元素-->
    </form> 

    在 JavaScript 中使用 noValidate 属性可以取得或设置这个值,如果这个属性存在,值为 true,如果不存在,值为 false。

    document.forms[0].noValidate = true; //禁用验证

    如果一个表单中有多个提交按钮,为了指定点击某个提交按钮不必验证表单,可以在相应的按钮上添加 formnovalidate 属性。

    <form method="post" action="foo.php">
    <!--这里插入表单元素-->
    <input type="submit" value="Regular Submit"> 
    <input type="submit" formnovalidate name="btnNoValidate"
    value="Non-validating Submit">
    </form> 

    使用 JavaScript 也可以设置这个属性。

    document.forms[0].elements["btnNoValidate"].formNoValidate = true; 
caizhendi commented 4 years ago

2020.06.19

阅读进度: 14.3.2 p434

3. 选择框脚本

选择框是通过<select><option>元素创建的。为了方便与这个控件交互,除了所有表单字段共有的属性和方法外,HTMLSelectElement 类型还提供了下列属性和方法。

选择框的 type 属性不是"select-one",就是"select-multiple",这取决于 HTML 代码中有没有 multiple 特性。选择框的 value 属性由当前选中项决定,相应规则如下。

<select name="location" id="selLocation">
 <option value="Sunnyvale, CA">Sunnyvale</option>
 <option value="Los Angeles, CA">Los Angeles</option>
 <option value="Mountain View, CA">Mountain View</option>
 <option value="">China</option>
 <option>Australia</option>
</select> 

如果用户选择了其中第一项,则选择框的值就是"Sunnyvale, CA"。如果文本为"China"的选项被选中,则选择框的值就是一个空字符串,因为其 value 特性是空的。如果选择了最后一项,那么由于<option>中没有指定 value 特性,则选择框的值就是"Australia"。
在 DOM 中,每个<option>元素都有一个 HTMLOptionElement 对象表示。为便于访问数据,HTMLOptionElement 对象添加了下列属性:

其中大部分属性的目的,都是为了方便对选项数据的访问。虽然也可以使用常规的 DOM 功能来访问这些信息,但效率是比较低的。

var selectbox = document.forms[0].elements["location"];
//不推荐
var text = selectbox.options[0].firstChild.nodeValue; //选项的文本
var value = selectbox.options[0].getAttribute("value"); //选项的值

以上代码使用标准 DOM 方法,取得了选择框中第一项的文本和值。可以与下面使用选项属性的代码作一比较:

var selectbox = document.forms[0]. elements["location"];
//推荐
var text = selectbox.options[0].text; //选项的文本
var value = selectbox.options[0].value; //选项的值

在操作选项时,我们建议最好是使用特定于选项的属性,因为所有浏览器都支持这些属性。在将表单控件作为 DOM 节点的情况下,实际的交互方式则会因浏览器而异。我们不推荐使用标准 DOM 技术修改<option>元素的文本或者值。
选择框的 change 事件与其他表单字段的 change 事件触发的条件不一样。其他表单字段的 change 事件是在值被修改且焦点离开当前字段时触发,而选择框的change 事件只要选中了选项就会触发。
但是,在所有浏览器中,value 属性始终等于 value 特性。在未指定 value 特性的情况下,IE8 会返回空字符串,而 IE9+、Safari、Firefox、Chrome 和 Opera 则会返回与 text 特性相同的值。

3.1 选择选项

对于只允许选择一项的选择框,访问选中项的最简单方式,就是使用选择框的 selectedIndex 属性。

var selectedOption = selectbox.options[selectbox.selectedIndex]; 

取得选中项之后,可以像下面这样显示该选项的信息:

var selectedIndex = selectbox.selectedIndex;
var selectedOption = selectbox.options[selectedIndex];
alert("Selected index: " + selectedIndex + "\nSelected text: " +
 selectedOption.text + "\nSelected value: " + selectedOption.value); 

对于可以选择多项的选择框,selectedfIndex 属性就好像只允许选择一项一样。设置selectedIndex 会导致取消以前的所有选项并选择指定的那一项,而读取 selectedIndex 则只会返回选中项中第一项的索引值。
另一种选择选项的方式,就是取得对某一项的引用,然后将其 selected 属性设置为 true。

selectbox.options[0].selected = true; 

与 selectedIndex 不同,在允许多选的选择框中设置选项的 selected 属性,不会取消对其他选中项的选择,因而可以动态选中任意多个项。但是,如果是在单选选择框中,修改某个选项的 selected 属性则会取消对其他选项的选择。需要注意的是,将 selected 属性设置为 false 对单选选择框没有影响。
实际上,selected 属性的作用主要是确定用户选择了选择框中的哪一项。要取得所有选中的项,可以循环遍历选项集合,然后测试每个选项的 selected 属性。

function getSelectedOptions(selectbox){
 var result = new Array();
 var option = null;
 for (var i=0, len=selectbox.options.length; i < len; i++){
 option = selectbox.options[i];
 if (option.selected){
 result.push(option);
 }
 }
 return result;
} 

下面是一个使用 getSelectedOptions()函数取得选中项的示例。

var selectbox = document.getElementById("selLocation");
var selectedOptions = getSelectedOptions(selectbox);
var message = "";
for (var i=0, len=selectedOptions.length; i < len; i++){
 message += "Selected index: " + selectedOptions[i].index +
 "\nSelected text: " + selectedOptions[i].text +
 "\nSelected value: " + selectedOptions[i].value + "\n\n";
}
alert(message); 
caizhendi commented 4 years ago

2020.06.20

阅读进度: 14.4 p436

3.2 添加选项

可以使用 JavaScript 动态创建选项,并将它们添加到选择框中。

var newOption = document.createElement("option");
newOption.appendChild(document.createTextNode("Option text"));
newOption.setAttribute("value", "Option value");
selectbox.appendChild(newOption); 

第二种方式是使用 Option 构造函数来创建新选项,这个构造函数是 DOM 出现之前就有的,一直遗留到现在。Option 构造函数接受两个参数:文本(text)和值(value);第二个参数可选。虽然这个构造函数会创建一个 Object 的实例,但兼容 DOM 的浏览器会返回一个<option>元素。

var newOption = new Option("Option text", "Option value");
selectbox.appendChild(newOption); //在 IE8 及之前版本中有问题

这种方式在除 IE 之外的浏览器中都可以使用。
第三种添加新选项的方式是使用选择框的 add()方法。DOM 规定这个方法接受两个参数:要添加的新选项和将位于新选项之后的选项。如果想在列表的最后添加一个选项,应该将第二个参数设置为null。
在 IE 对 add()方法的实现中,第二个参数是可选的,而且如果指定,该参数必须是新选项之后选项的索引。兼容 DOM 的浏览器要求必须指定第二个参数,因此要想编写跨浏览器的代码,就不能只传入一个参数。这时候,为第二个参数传入 undefined,就可以在所有浏览器中都将新选项插入到列表最后了。

var newOption = new Option("Option text", "Option value");
selectbox.add(newOption, undefined); //最佳方案

在 IE 和兼容 DOM 的浏览器中,上面的代码都可以正常使用。如果你想将新选项添加到其他位置(不是最后一个),就应该使用标准的 DOM 技术和 insertBefore()方法。

.3.3 移除选项

与添加选项类似,移除选项的方式也有很多种。首先,可以使用 DOM 的 removeChild()方法,为其传入要移除的选项。

selectbox.removeChild(selectbox.options[0]); //移除第一个选项

其次,可以使用选择框的 remove()方法。这个方法接受一个参数,即要移除选项的索引。

selectbox.remove(0); //移除第一个选项

最后一种方式,就是将相应选项设置为 null。这种方式也是 DOM 出现之前浏览器的遗留机制。

selectbox.options[0] = null; //移除第一个选项

要清除选择框中所有的项,需要迭代所有选项并逐个移除它们。

function clearSelectbox(selectbox){
 for(var i=0, len=selectbox.options.length; i < len; i++){
 selectbox.remove(i);
 }
} 

3.4 移动和重排选项

使用 DOM 的 appendChild()方法,就可以将第一个选择框中的选项直接移动到第二个选择框中。我们知道,如果为 appendChild()方法传入一个文档中已有的元素,那么就会先从该元素的父节点中移除它,再把它添加到指定的位置。

var selectbox1 = document.getElementById("selLocations1");
var selectbox2 = document.getElementById("selLocations2");
selectbox2.appendChild(selectbox1.options[0]); 

移动选项与移除选项有一个共同之处,即会重置每一个选项的 index 属性。
要将选择框中的某一项移动到特定位置,最合适的 DOM 方法就是 insertBefore();appendChild()方法只适用于将选项添加到选择框的最后。要在选择框中向前移动一个选项的位置。

var optionToMove = selectbox.options[1];
selectbox.insertBefore(optionToMove, selectbox.options[optionToMove.index-1]); 

可以使用下列代码将选择框中的选项向后移动一个位置。

var optionToMove = selectbox.options[1];
selectbox.insertBefore(optionToMove, selectbox.options[optionToMove.index+2]); 

IE7 存在一个页面重绘问题,有时候会导致使用 DOM 方法重排的选项不能马上正确显示。

caizhendi commented 4 years ago

2020.06.21

阅读进度: 14.5.2 p439

4 表单序列化

在 JavaScript 中,可以利用表单字段的 type 属性,连同 name 和 value 属性一起实现对表单的序列化。在编写代码之前,有必须先搞清楚在表单提交期间,浏览器是怎样将数据发送给服务器的。

在表单序列化过程中,一般不包含任何按钮字段,因为结果字符串很可能是通过其他方式提交的。除此之外的其他上述规则都应该遵循。表单序列化:

function serialize(form){
 var parts = [],
 field = null,
 i,
 len,
 j,
 optLen,
 option,
 optValue;

 for (i=0, len=form.elements.length; i < len; i++){
 field = form.elements[i];

 switch(field.type){
 case "select-one":
 case "select-multiple":
 if (field.name.length){
 for (j=0, optLen = field.options.length; j < optLen; j++){ 
      option = field.options[j];
 if (option.selected){
 optValue = "";
 if (option.hasAttribute){
 optValue = (option.hasAttribute("value") ?
 option.value : option.text);
 } else {
 optValue = (option.attributes["value"].specified ?
 option.value : option.text);
 }
 parts.push(encodeURIComponent(field.name) + "=" +
 encodeURIComponent(optValue));
 }
 }
 }
 break;

 case undefined: //字段集
 case "file": //文件输入
 case "submit": //提交按钮
 case "reset": //重置按钮
 case "button": //自定义按钮
 break;

 case "radio": //单选按钮
 case "checkbox": //复选框
 if (!field.checked){
 break;
 }
 /* 执行默认操作 */
 default:
 //不包含没有名字的表单字段
 if (field.name.length){
 parts.push(encodeURIComponent(field.name) + "=" +
 encodeURIComponent(field.value));
 }
 }
 }
 return parts.join("&");
} 

上面这个 serialize()函数首先定义了一个名为 parts 的数组,用于保存将要创建的字符串的各个部分。然后,通过 for 循环迭代每个表单字段,并将其保存在 field 变量中。在获得了一个字段的引用之后,使用 switch 语句检测其 type 属性。序列化过程中最麻烦的就是<select>元素,它可能是单选框也可能是多选框。为此,需要遍历控件中的每一个选项,并在相应选项被选中的情况下向数组中添加一个值。对于单选框,只可能有一个选中项,而多选框则可能有零或多个选中项。这里的代码适用于这两种选择框,至于可选项的数量则是由浏览器控制的。在找到一个选中项之后,需要确定使用什么值。如果不存在 value 特性,或者虽然存在该特性,但值为空字符串,都要使用选项的文本来代替。为检查这个特性,在 DOM 兼容的浏览器中需要使用 hasAttribute()方法,而在 IE 中需要使用特性的 specified 属性。
如果表单中包含<fieldset>元素,则该元素会出现在元素集合中,但没有 type 属性。因此,如果 type属性未定义,则不需要对其进行序列化。同样,对于各种按钮以及文件输入字段也是如此(文件输入字段在表单提交过程中包含文件的内容;但是,这个字段是无法模仿的,序列化时一般都要忽略)。

5 富文本编辑

富文本编辑,又称为 WYSIWYG(What You See Is What You Get,所见即所得)。
但在 IE 最早引入的这一功能基础上,已经出现了事实标准。而且,Opera、Safari、Chrome 和 Firefox 都已经支持这一功能。这一技术的本质,就是在页面中嵌入一个包含空 HTML 页面的 iframe。通过设置 designMode 属性,这个空白的 HTML 页面可以被编辑,而编辑对象则是该页面<body>元素的 HTML 代码。designMode 属性有两个可能的值:"off"(默认值)和"on"。在设置为"on"时,整个文档都会变得可以编辑(显示插入符号),然后就可以像使用字处理软件一样,通过键盘将文本内容加粗、变成斜体,等等。
可以给 iframe 指定一个非常简单的 HTML 页面作为其内容来源。

<!DOCTYPE html>
<html>
 <head>
 <title>Blank Page for Rich Text Editing</title>
 </head>
 <body>
 </body>
</html> 

这个页面在 iframe 中可以像其他页面一样被加载。要让它可以编辑,必须要将 designMode 设置为"on",但只有在页面完全加载之后才能设置这个属性。因此,在包含页面中,需要使用 onload 事件处理程序来在恰当的时刻设置 designMode。

<iframe name="richedit" style="height:100px;width:100px;" src="blank.htm"></iframe>
<script type="text/javascript">
EventUtil.addHandler(window, "load", function(){
 frames["richedit"].document.designMode = "on";
});
</script> 

5.1 使用contenteditable属性

另一种编辑富文本内容的方式是使用名为 contenteditable 的特殊属性,这个属性也是由 IE 最早实现的。可以把 contenteditable 属性应用给页面中的任何元素,然后用户立即就可以编辑该元素。这种方法之所以受到欢迎,是因为它不需要 iframe、空白页和 JavaScript,只要为元素设置contenteditable 属性即可。

<div class="editable" id="richedit" contenteditable></div> 

通过在这个元素上设置 contenteditable 属性,也能打开或关闭编辑模式。

var div = document.getElementById("richedit");
div.contentEditable = "true"; 

contenteditable 属性有三个可能的值:"true"表示打开、"false"表示关闭,"inherit"表示从父元素那里继承(因为可以在 contenteditable 元素中创建或删除元素)。
支持 contenteditable属性的元素有 IE、Firefox、Chrome、Safari 和 Opera。在移动设备上,支持 contenteditable 属性的浏览器有 iOS 5+中的 Safari 和 Android 3+中的 WebKit。

caizhendi commented 4 years ago

2020.06.22

阅读进度: 15章 p445

5.2 操作富文本

与富文本编辑器交互的主要方式,就是使用 document.execCommand()。这个方法可以对文档执行预定义的命令,而且可以应用大多数格式。
document.execCommand()方法传递 3 个参数:要执行的命令名称、表示浏览器是否应该为当前命令提供用户界面的一个布尔值、执行命令必须的一个值(如果不需要值,则传递 null)。
为了确保跨浏览器的兼容性,第二个参数应该始终设置为 false,因为 Firefox 会在该参数为 true 时抛出错误。
不同浏览器支持的预定义命令也不一样。下表列出了那些被支持最多的命令。见p439
其中,与剪贴板有关的命令在不同浏览器中的差异极大。Opera 根本没有实现任何剪贴板命令,而Firefox 在默认情况下会禁用它们(必须修改用户的首选项来启用它们)。Safari 和 Chrome 实现了 cut 和copy,但没有实现 paste。不过,即使不能通过 document.execCommand()来执行这些命令,但却可以通过相应的快捷键来实现同样的操作。
可以在任何时候使用这些命令来修改富文本区域的外观。

//转换粗体文本
frames["richedit"].document.execCommand("bold", false, null);
//转换斜体文本
frames["richedit"].document.execCommand("italic", false, null);
//创建指向 www.wrox.com 的链接
frames["richedit"].document.execCommand("createlink", false, "http://www.wrox.com");
//格式化为 1 级标题
frames["richedit"].document.execCommand("formatblock", false, "<h1>"); 

同样的方法也适用于页面中 contenteditable 属性为"true"的区块,只要把对框架的引用替换成当前窗口的 document 对象即可。

//转换粗体文本
document.execCommand("bold", false, null);
//转换斜体文本
document.execCommand("italic", false, null);
//创建指向 www.wrox.com 的链接
document.execCommand("createlink", false, "http://www.wrox.com");
//格式化为 1 级标题
document.execCommand("formatblock", false, "<h1>");

注意,虽然所有浏览器都支持这些命令,但这些命令所产生的 HTML 仍然有很大不同。
执行 bold 命令时,IE 和 Opera 会使用<strong>标签包围文本,Safari 和 Chrome 使用<b>标签,而 Firefox 则使用<span>标签。
除了命令之外,还有一些与命令相关的方法。第一个方法就是 queryCommandEnabled(),可以用它来检测是否可以针对当前选择的文本,或者当前插入字符所在位置执行某个命令。这个方法接收一个参数,即要检测的命令。如果当前编辑区域允许执行传入的命令,这个方法返回 true,否则返回 false。

var result = frames["richedit"].document.queryCommandEnabled("bold"); 

如果能够对当前选择的文本执行"bold"命令,以上代码会返回 true。需要注意的是,queryCommandEnabled()方法返回 true,并不意味着实际上就可以执行相应命令,而只能说明对当前选择的文本执行相应命令是否合适。例如,Firefox 在默认情况下会禁用剪切操作,但执行 queryCommandEnabled("cut")也可能会返回 true。
另外,queryCommandState()方法用于确定是否已将指定命令应用到了选择的文本。例如,要确定当前选择的文本是否已经转换成了粗体。

var isBold = frames["richedit"].document.queryCommandState("bold"); 

如果此前已经对选择的文本执行了"bold"命令,那么上面的代码会返回 true。一些功能全面的富文本编辑器,正是利用这个方法来更新粗体、斜体等按钮的状态的。
最后一个方法是 queryCommandValue(),用于取得执行命令时传入的值(即前面例子中传给document.execCommand()的第三个参数)。例如,在对一段文本应用"fontsize"命令时如果传入了7,那么下面的代码就会返回"7":

var fontSize = frames["richedit"].document.queryCommandValue("fontsize"); 

5.3 富文本选区

在富文本编辑器中,使用框架(iframe)的 getSelection()方法,可以确定实际选择的文本。这个方法是 window 对象和 document 对象的属性,调用它会返回一个表示当前选择文本的 Selection对象。每个 Selection 对象都有下列属性。

该对象的下列方法提供了更多信息,并且支持对选区的操作。

Selection 对象的这些方法都极为实用,它们利用了DOM 范围来管理选区。由于可以直接操作选择文本的 DOM 表现,因此访问 DOM 范围与使用 execCommand()相比,能够对富文本编辑器进行更加细化的控制。

var selection = frames["richedit"].getSelection();
//取得选择的文本
var selectedText = selection.toString();
//取得代表选区的范围
var range = selection.getRangeAt(0);
//突出显示选择的文本
var span = frames["richedit"].document.createElement("span");
span.style.backgroundColor = "yellow";
range.surroundContents(span); 

HTML5 将 getSelection()方法纳入了标准,而且 IE9、Firefox、Safari、Chrome 和 Opera 8 都实现了它。由于历史原因,在 Firefox 3.6+中调用 document.getSelection()会返回一个字符串。为此,可以在 Firefox 3.6+中改作调用 window.getSelection(),从而返回 selection 对象。Firefox 8 修复了 document.getSelection()的 bug,能返回与 window.getSelection()相同的值。
IE8 及更早的版本不支持 DOM 范围,但我们可以通过它支持的 selection 对象操作选择的文本。IE 中的 selection 对象是 document 的属性,本章前面曾经讨论过。要取得富文本编辑器中选择的文本,首先必须创建一个文本范围,然后再像下面这样访问其 text 属性。

var range = frames["richedit"].document.selection.createRange();
var selectedText = range.text; 

要像前面使用 DOM 范围那样实现相同的文本高亮效果,可以组合使用 htmlText 属性和pasteHTML()方法。

var range = frames["richedit"].document.selection.createRange();
range.pasteHTML("<span style=\"background-color:yellow\"> " + range.htmlText +"</span>"); 

5.4 表单与富文本

通常可以添加一个隐藏的表单字段,让它的值等于从 iframe 中提取出的 HTML。具体来说,就是在提交表单之前,从 iframe 中提取出 HTML,并将其插入到隐藏的字段中。下面就是通过表单的 onsubmit 事件处理程序实现上述操作的代码。

EventUtil.addHandler(form, "submit", function(event){
 event = EventUtil.getEvent(event);
 var target = EventUtil.getTarget(event);
 target.elements["comments"].value = frames["richedit"].document.body.innerHTML;
}); 

对于 contenteditable元素,也可以执行类似操作。

EventUtil.addHandler(form, "submit", function(event){
 event = EventUtil.getEvent(event);
 var target = EventUtil.getTarget(event);
 target.elements["comments"].value =document.getElementById("richedit").innerHTML;
}); 

6. 小结

下面是本章的几个概念。

除 Opera 之外的所有浏览器都支持剪贴板事件,包括 copy、cut 和 paste。其他浏览器在实现剪贴板事件时也可以分为几种不同的情况。

富文本编辑功能是通过一个包含空 HTML 文档的 iframe 元素来实现的。通过将空文档的designMode 属性设置为"on",就可以将该页面转换为可编辑状态,此时其表现如同字处理软件。另外,也可以将某个元素设置为 contenteditable。在默认情况下,可以将字体加粗或者将文本转换为斜体,还可以使用剪贴板。JavaScript 通过使用 execCommand()方法也可以实现相同的一些功能。
另外,使用queryCommandEnabled()、queryCommandState()和 queryCommandValue()方法则可以取得有关文本选区的信息。由于以这种方式构建的富文本编辑器并不是一个表单字段,因此在将其内容提交给服务器之前,必须将 iframe 或 contenteditable 元素中的 HTML 复制到一个表单字段中。

caizhendi commented 4 years ago

2020.06.24

阅读进度: 15.2.3 p449

15. 使用canvas绘图

这个元素负责在页面中设定一个区域,然后就可以通过 JavaScript 动态地在这个区域中绘制图形。<canvas>元素最早是由苹果公司推出的,当时主要用在其 Dashboard 微件中。很快,HTML5 加入了这个元素,主流浏览器也迅速开始支持它。
IE9+、Firefox 1.5+、Safari 2+、Opera 9+、Chrome、iOS 版 Safari 以及 Android 版 WebKit都在某种程度上支持<canvas>
除了具备基本绘图能力的 2D 上下文,<canvas>还建议了一个名为 WebGL 的 3D 上下文。目前,支持该元素的浏览器都支持 2D 上下文及文本 API,但对 WebGL 的支持还不够好。由于 WebGL 还是实验性的,因此要得到所有浏览器支持还需要很长一段时间。Firefox 4+和 Chrome 支持 WebGL 规范的早期版本。

1. 基本用法

要使用<canvas>元素,必须先设置其 width 和 height 属性,指定可以绘图的区域大小。出现在开始和结束标签中的内容是后备信息,如果浏览器不支持<canvas>元素,就会显示这些信息。

<canvas id="drawing" width=" 200" height="200">A drawing of something.</canvas> 

<canvas>元素对应的 DOM 元素对象也有 width 和 height 属性,可以随意修改。而且,也能通过 CSS 为该元素添加样式,如果不添加任何样式或者不绘制任何图形,在页面中是看不到该元素的。
要在这块画布(canvas)上绘图,需要取得绘图上下文。而取得绘图上下文对象的引用,需要调用getContext()方法并传入上下文的名字。传入"2d",就可以取得 2D 上下文对象。

var drawing = document.getElementById("drawing"); 
//确定浏览器支持<canvas>元素
if (drawing.getContext){ 
    var context = drawing.getContext("2d");
//更多代码
} 

在使用<canvas>元素之前,首先要检测 getContext()方法是否存在,这一步非常重要。有些浏览器会为 HTML 规范之外的元素创建默认的 HTML 元素对象。在这种情况下,即使 drawing 变量中保存着一个有效的元素引用,也检测不到 getContext()方法。
使用 toDataURL()方法,可以导出在<canvas>元素上绘制的图像。这个方法接受一个参数,即图像的 MIME 类型格式,而且适合用于创建图像的任何上下文。比如,要取得画布中的一幅 PNG 格式的图像。

var drawing = document.getElementById("drawing");
//确定浏览器支持<canvas>元素
if (drawing.getContext){
 //取得图像的数据 URI
 var imgURI = drawing.toDataURL("image/png");
 //显示图像
 var image = document.createElement("img");
 image.src = imgURI;
 document.body.appendChild(image);
} 

默认情况下,浏览器会将图像编码为 PNG 格式(除非另行指定)。Firefox 和 Opera 也支持基于"image/jpeg"参数的 JPEG 编码格式。由于这个方法是后来才追加的,所以支持<canvas>的浏览器也是在较新的版本中才加入了对它的支持,比如 IE9、Firefox 3.5 和 Opera 10。

2. 2D 上下文

使用 2D 绘图上下文提供的方法,可以绘制简单的 2D 图形,比如矩形、弧线和路径。2D 上下文的坐标开始于<canvas>元素的左上角,原点坐标是(0,0)。所有坐标值都基于这个原点计算,x 值越大表示越靠右,y 值越大表示越靠下。默认情况下,width 和 height 表示水平和垂直两个方向上可用的像素数目。

2.1 填充和描边

2D 上下文的两种基本绘图操作是填充和描边。填充,就是用指定的样式(颜色、渐变或图像)填充图形;描边,就是只在图形的边缘画线。大多数 2D 上下文操作都会细分为填充和描边两个操作,而操作的结果取决于两个属性:fillStyle 和 strokeStyle。
这两个属性的值可以是字符串、渐变对象或模式对象,而且它们的默认值都是"#000000"。如果为它们指定表示颜色的字符串值,可以使用 CSS 中指定颜色值的任何格式,包括颜色名、十六进制码、rgb、rgba、hsl 或 hsla。

var drawing = document.getElementById("drawing");
//确定浏览器支持<canvas>元素
if (drawing.getContext){
 var context = drawing.getContext("2d");
 context.strokeStyle = "red";
 context.fillStyle = "#0000ff";
} 

以上代码将 strokeStyle 设置为 red(CSS 中的颜色名),将 fillStyle 设置为#0000ff(蓝色)。

2.2 绘制矩形

矩形是唯一一种可以直接在 2D 上下文中绘制的形状。与矩形有关的方法包括 fillRect()、strokeRect()和 clearRect()。这三个方法都能接收 4 个参数:矩形的 x 坐标、矩形的 y 坐标、矩形宽度和矩形高度。这些参数的单位都是像素。
首先,fillRect()方法在画布上绘制的矩形会填充指定的颜色。填充的颜色通过 fillStyle 属性指定

var drawing = document.getElementById("drawing");
//确定浏览器支持<canvas>元素
if (drawing.getContext){
 var context = drawing.getContext("2d");
 /*
 * 根据 Mozilla 的文档
 * http://developer.mozilla.org/en/docs/Canvas_tutorial:Basic_usage
 */
 //绘制红色矩形
 context.fillStyle = "#ff0000";
 context.fillRect(10, 10, 50, 50);
 //绘制半透明的蓝色矩形
 context.fillStyle = "rgba(0,0,255,0.5)";
 context.fillRect(30, 30, 50, 50);
} 

以上代码首先将 fillStyle 设置为红色,然后从(10,10)处开始绘制矩形,矩形的宽和高均为 50 像素。然后,通过 rgba()格式再将 fillStyle 设置为半透明的蓝色,在第一个矩形上面绘制第二个矩形。
strokeRect()方法在画布上绘制的矩形会使用指定的颜色描边。描边颜色通过 strokeStyle 属性指定。

var drawing = document.getElementById("drawing");
//确定浏览器支持<canvas>元素
if (drawing.getContext){
 var context = drawing.getContext("2d");
 /*
 * 根据 Mozilla 的文档
 * http://developer.mozilla.org/en/docs/Canvas_tutorial:Basic_usage
 */
 //绘制红色描边矩形
 context.strokeStyle = "#ff0000";
 context.strokeRect(10, 10, 50, 50);
 //绘制半透明的蓝色描边矩形
 context.strokeStyle = "rgba(0,0,255,0.5)";
 context.strokeRect(30, 30, 50, 50);
} 

描边线条的宽度由 lineWidth 属性控制,该属性的值可以是任意整数。另外,通过 lineCap 属性可以控制线条末端的形状是平头、圆头还是方头("butt"、"round"或"square"),通过 lineJoin 属性可以控制线条相交的方式是圆交、斜交还是斜接("round"、"bevel"或"miter")。
clearRect()方法用于清除画布上的矩形区域。本质上,这个方法可以把绘制上下文中的某一矩形区域变透明。通过绘制形状然后再清除指定区域,就可以生成有意思的效果,例如把某个形状切掉一块。

var drawing = document.getElementById("drawing");
//确定浏览器支持<canvas>元素
if (drawing.getContext){
 var context = drawing.getContext("2d"); 
  /*
 * 根据 Mozilla 的文档
 * http://developer.mozilla.org/en/docs/Canvas_tutorial:Basic_usage
 */
 //绘制红色矩形
 context.fillStyle = "#ff0000";
 context.fillRect(10, 10, 50, 50);
 //绘制半透明的蓝色矩形
 context.fillStyle = "rgba(0,0,255,0.5)";
 context.fillRect(30, 30, 50, 50);
 //在两个矩形重叠的地方清除一个小矩形
 context.clearRect(40, 40, 10, 10);
} 
caizhendi commented 4 years ago

2020.06.25

阅读进度: 15.2.5 p453

2.3 绘制路径

2D 绘制上下文支持很多在画布上绘制路径的方法。通过路径可以创造出复杂的形状和线条。要绘制路径,首先必须调用 beginPath()方法,表示要开始绘制新路径。然后,再通过调用下列方法来实际地绘制路径。

caizhendi commented 4 years ago

2020.06.26

阅读进度: 15.2.6 p456

2.5 变换

通过上下文的变换,可以把处理后的图像绘制到画布上。2D 绘制上下文支持各种基本的绘制变换。创建绘制上下文时,会以默认值初始化变换矩阵,在默认的变换矩阵下,所有处理都按描述直接绘制。

变换有可能很简单,但也可能很复杂,这都要视情况而定。比如,就拿前面例子中绘制表针来说,如果把原点变换到表盘的中心,然后再绘制表针就容易多了。

var drawing = document.getElementById("drawing");
//确定浏览器支持<canvas>元素
if (drawing.getContext){ 
     var context = drawing.getContext("2d");
 //开始路径
 context.beginPath();
 //绘制外圆
 context.arc(100, 100, 99, 0, 2 * Math.PI, false);
 //绘制内圆
 context.moveTo(194, 100);
 context.arc(100, 100, 94, 0, 2 * Math.PI, false);
 //变换原点
 context.translate(100, 100);
 //绘制分针
 context.moveTo(0,0);
 context.lineTo(0, -85);
 //绘制时针
 context.moveTo(0, 0);
 context.lineTo(-65, 0);
 //描边路径
 context.stroke();
} 

把原点变换到时钟表盘的中心点(100,100)后,在同一方向上绘制线条就变成了简单的数学问题了。所有数学计算都基于(0,0),而不是(100,100)。还可以更进一步,像下面这样使用 rotate()方法旋转时钟的表针。

var drawing = document.getElementById("drawing");
//确定浏览器支持<canvas>元素
if (drawing.getContext){
 var context = drawing.getContext("2d");
 //开始路径
 context.beginPath();
 //绘制外圆
 context.arc(100, 100, 99, 0, 2 * Math.PI, false);
 //绘制内圆
 context.moveTo(194, 100);
 context.arc(100, 100, 94, 0, 2 * Math.PI, false);
 //变换原点
 context.translate(100, 100);
 //旋转表针
 context.rotate(1);
 //绘制分针
  context.moveTo(0,0);
 context.lineTo(0, -85);
 //绘制时针
 context.moveTo(0, 0);
 context.lineTo(-65, 0);
 //描边路径
 context.stroke();
} 

无论是刚才执行的变换,还是 fillStyle、strokeStyle 等属性,都会在当前上下文中一直有效,除非再对上下文进行什么修改。虽然没有什么办法把上下文中的一切都重置回默认值,但有两个方法可以跟踪上下文的状态变化。
如果你知道将来还要返回某组属性与变换的组合,可以调用 save()方法。调用这个方法后,当时的所有设置都会进入一个栈结构,得以妥善保管。然后可以对上下文进行其他修改。等想要回到之前保存的设置时,可以调用 restore()方法,在保存设置的栈结构中向前返回一级,恢复之前的状态。连续调用 save()可以把更多设置保存到栈结构中,之后再连续调用 restore()则可以一级一级返回。

context.fillStyle = "#ff0000";
context.save();
context.fillStyle = "#00ff00";
context.translate(100, 100);
context.save();
context.fillStyle = "#0000ff";
context.fillRect(0, 0, 100, 200); //从点(100,100)开始绘制蓝色矩形
context.restore();
context.fillRect(10, 10, 100, 200); //从点(110,110)开始绘制绿色矩形
context.restore(); 
context.fillRect(0, 0, 100, 200); //从点(0,0)开始绘制红色矩形

save()方法保存的只是对绘图上下文的设置和变换,不会保存绘图上下文的内容。

caizhendi commented 4 years ago

2020.06.27

阅读进度: 15.2.9 p460

2.6 绘制图像

2D 绘图上下文内置了对图像的支持。如果你想把一幅图像绘制到画布上,可以使用 drawImage()方法。根据期望的最终结果不同,调用这个方法时,可以使用三种不同的参数组合。最简单的调用方式是传入一个 HTML <img>元素,以及绘制该图像的起点的 x 和 y 坐标。

var image = document.images[0];
context.drawImage(image, 10, 10); 

这两行代码取得了文档中的第一幅图像,然后将它绘制到上下文中,起点为(10,10)。绘制到画布上的图像大小与原始大小一样。如果你想改变绘制后图像的大小,可以再多传入两个参数,分别表示目标宽度和目标高度。通过这种方式来缩放图像并不影响上下文的变换矩阵。

context.drawImage(image, 50, 10, 20, 30); 

除了上述两种方式,还可以选择把图像中的某个区域绘制到上下文中。drawImage()方法的这种调用方式总共需要传入 9 个参数:要绘制的图像、源图像的 x 坐标、源图像的 y 坐标、源图像的宽度、源图像的高度、目标图像的 x 坐标、目标图像的 y 坐标、目标图像的宽度、目标图像的高度。这样调用drawImage()方法可以获得最多的控制。

context.drawImage(image, 0, 10, 50, 50, 0, 100, 40, 60); 

这行代码只会把原始图像的一部分绘制到画布上。原始图像的这一部分的起点为(0,10),宽和高都是 50 像素。最终绘制到上下文中的图像的起点是(0,100),而大小变成了 40×60 像素。
除了给 drawImage()方法传入 HTML <img>元素外,还可以传入另一个<canvas>元素作为其第一个参数。这样,就可以把另一个画布内容绘制到当前画布上。
结合使用 drawImage()和其他方法,可以对图像进行各种基本操作。而操作的结果可以通过toDataURL()方法获得①。不过,有一个例外,即图像不能来自其他域。如果图像来自其他域,调用toDataURL()会抛出一个错误。

2.7 阴影

2D 上下文会根据以下几个属性的值,自动为形状或路径绘制出阴影。

这些属性都可以通过 context 对象来修改。只要在绘制前为它们设置适当的值,就能自动产生阴影。

var context = drawing.getContext("2d");
//设置阴影
context.shadowOffsetX = 5;
context.shadowOffsetY = 5;
context.shadowBlur = 4;
context.shadowColor = "rgba(0, 0, 0, 0.5)";
//绘制红色矩形
context.fillStyle = "#ff0000";
context.fillRect(10, 10, 50, 50);
//绘制蓝色矩形
context.fillStyle = "rgba(0,0,255,1)"; 
context.fillRect(30, 30, 50, 50); 

不同浏览器对阴影的支持有一些差异。
IE9、Firefox 4 和 Opera 11 的行为最为规范,其他浏览器多多少少会有一些奇怪的现象,甚至根本不支持阴影。Chrome(直至第 10 版)不能正确地为描边的形状应用实心阴影。Chrome 和Safari(直至第 5 版)在为带透明像素的图像应用阴影时也会有问题:不透明部分的下方本来是该有阴影的,但此时则一概不见了。Safari 也不能给渐变图形应用阴影,其他浏览器都可以。

2.8 渐变

渐变由 CanvasGradient 实例表示,很容易通过 2D 上下文来创建和修改。要创建一个新的线性渐变,可以调用 createLinearGradient()方法。
这个方法接收 4 个参数:起点的 x 坐标、起点的 y 坐标、终点的 x 坐标、终点的 y 坐标。调用这个方法后,它就会创建一个指定大小的渐变,并返回CanvasGradient 对象的实例。
创建了渐变对象后,下一步就是使用 addColorStop()方法来指定色标。这个方法接收两个参数:色标位置和 CSS 颜色值。色标位置是一个 0(开始的颜色)到 1(结束的颜色)之间的数字。

var gradient = context.createLinearGradient(30, 30, 70, 70);
gradient.addColorStop(0, "white");
gradient.addColorStop(1, "black"); 

gradient 对象表示的是一个从画布上点(30,30)到点(70,70)的渐变。起点的色标是白色,终点的色标是黑色。然后就可以把 fillStyle 或 strokeStyle 设置为这个对象,从而使用渐变来绘制形状或描边:

//绘制红色矩形
context.fillStyle = "#ff0000";
context.fillRect(10, 10, 50, 50);
//绘制渐变矩形
context.fillStyle = gradient;
context.fillRect(30, 30, 50, 50);

为了让渐变覆盖整个矩形,而不是仅应用到矩形的一部分,矩形和渐变对象的坐标必须匹配才行。
如果没有把矩形绘制到恰当的位置,那可能就只会显示部分渐变效果。

context.fillStyle = gradient; 
context.fillRect(50, 50, 50, 50); 

这两行代码执行后得到的矩形只有左上角稍微有一点白色。这主要是因为矩形的起点位于渐变的中间位置,而此时渐变差不多已经结束了。由于渐变不重复,所以矩形的大部分区域都是黑色。确保渐变与形状对齐非常重要,有时候可以考虑使用函数来确保坐标合适。

function createRectLinearGradient(w, x, y, width, height){
 return context.createLinearGradient(x, y, x+width, y+height);
} 

这个函数基于起点的x和y坐标以及宽度和高度值来创建渐变对象,从而让我们可以在fillRect()中使用相同的值。

var gradient = createRectLinearGradient(context, 30, 30, 50, 50);
gradient.addColorStop(0, "white");
gradient.addColorStop(1, "black");
//绘制渐变矩形
context.fillStyle = gradient;
context.fillRect(30, 30, 50, 50); 

使用画布的时候,确保坐标匹配很重要,也需要一些技巧。类似 createRectLinearGradient()这样的辅助方法可以让控制坐标更容易一些。
要创建径向渐变(或放射渐变),可以使用 createRadialGradient()方法。这个方法接收 6 个参数,对应着两个圆的圆心和半径。前三个参数指定的是起点圆的原心(x 和 y)及半径,后三个参数指定的是终点圆的原心(x 和 y)及半径。
可以把径向渐变想象成一个长圆桶,而这 6 个参数定义的正是这个桶的两个圆形开口的位置。如果把一个圆形开口定义得比另一个小一些,那这个圆桶就变成了圆锥体,而通过移动每个圆形开口的位置,就可达到像旋转这个圆锥体一样的效果。
如果想从某个形状的中心点开始创建一个向外扩散的径向渐变效果,就要将两个圆定义为同心圆。比如,就拿前面创建的矩形来说,径向渐变的两个圆的圆心都应该在(55,55),因为矩形的区域是从(30,30)到(80,80)。

var gradient = context.createRadialGradient(55, 55, 10, 55, 55, 30);
gradient.addColorStop(0, "white");
gradient.addColorStop(1, "black");
//绘制红色矩形
context.fillStyle = "#ff0000";
context.fillRect(10, 10, 50, 50);
//绘制渐变矩形
context.fillStyle = gradient;
context.fillRect(30, 30, 50, 50); 
caizhendi commented 4 years ago

2020.06.28

阅读进度: 15.3 p463

2.9 模式

模式其实就是重复的图像,可以用来填充或描边图形。要创建一个新模式,可以调用createPattern()方法并传入两个参数:一个 HTML <img>元素和一个表示如何重复图像的字符串。其中,第二个参数的值与 CSS 的 background-repeat 属性值相同,包括"repeat"、"repeat-x"、"repeat-y"和"no-repeat"。

var image = document.images[0],
 pattern = context.createPattern(image, "repeat");
//绘制矩形
context.fillStyle = pattern;
context.fillRect(10, 10, 150, 150);

模式与渐变一样,都是从画布的原点(0,0)开始的。将填充样式(fillStyle)设置为模式对象,只表示在某个特定的区域内显示重复的图像,而不是要从某个位置开始绘制重复的图像。
createPattern()方法的第一个参数也可以是一个<video>元素,或者另一个<canvas>元素。

2.10 使用图像数据

2D 上下文的一个明显的长处就是,可以通过 getImageData()取得原始图像数据。这个方法接收4 个参数:要取得其数据的画面区域的 x 和 y 坐标以及该区域的像素宽度和高度。例如,要取得左上角坐标为(10,5)、大小为 50×50 像素的区域的图像数据

var imageData = context.getImageData(10, 5, 50, 50); 

这里返回的对象是 ImageData 的实例。每个 ImageData 对象都有三个属性:width、height 和data。其中 data 属性是一个数组,保存着图像中每一个像素的数据。在 data 数组中,每一个像素用4 个元素来保存,分别表示红、绿、蓝和透明度值。因此,第一个像素的数据就保存在数组的第 0 到第3 个元素中

var data = imageData.data,
 red = data[0],
 green = data[1],
 blue = data[2],
 alpha = data[3]; 

数组中每个元素的值都介于 0 到 255 之间(包括 0 和 255)。能够直接访问到原始图像数据,就能够以各种方式来操作这些数据。例如,通过修改图像数据,可以像下面这样创建一个简单的灰阶过滤器。

var drawing = document.getElementById("drawing");
//确定浏览器支持<canvas>元素
if (drawing.getContext){
 var context = drawing.getContext("2d"),
 image = document.images[0],
 imageData, data,
 i, len, average,
 red, green, blue, alpha;
 //绘制原始图像
 context.drawImage(image, 0, 0);
 //取得图像数据
 imageData = context.getImageData(0, 0, image.width, image.height);
 data = imageData.data;
 for (i=0, len=data.length; i < len; i+=4){
 red = data[i];
 green = data[i+1];
 blue = data[i+2];
 alpha = data[i+3];
 //求得 rgb 平均值
 average = Math.floor((red + green + blue) / 3);
 //设置颜色值,透明度不变
 data[i] = average;
 data[i+1] = average;
 data[i+2] = average;
 }

 //回写图像数据并显示结果
 imageData.data = data;
 context.putImageData(imageData, 0, 0);
} 

只有在画布“干净”的情况下(即图像并非来自其他域),才可以取得图像数据。如果画布“不干净”,那么访问图像数据时会导致 JavaScript 错误。

2.11 合成

还有两个会应用到 2D 上下文中所有绘制操作的属性:globalAlpha 和 globalCompositionOperation。其中,globalAlpha 是一个介于 0 和 1 之间的值(括 0 和 1),用于指定所有绘制的透明度。默认值为 0。如果所有后续操作都要基于相同的透明度,就可以先把 globalAlpha 设置为适当值,然后绘制,最后再把它设置回默认值 0。

//绘制红色矩形
context.fillStyle = "#ff0000";
context.fillRect(10, 10, 50, 50);
//修改全局透明度
context.globalAlpha = 0.5;
//绘制蓝色矩形
context.fillStyle = "rgba(0,0,255,1)";
context.fillRect(30, 30, 50, 50);
//重置全局透明度
context.globalAlpha = 0; 

第二个属性 globalCompositionOperation 表示后绘制的图形怎样与先绘制的图形结合。这个属性的值是字符串,可能的值如下。

这个合成操作实际上用语言或者黑白图像是很难说清楚的。要了解每个操作的具体效果,请参见https://developer.mozilla.org/samples/canvas-tutorial/6_1_canvas_composite.html。推荐使用 IE9+或 Firefox4+访问前面的网页。

//绘制红色矩形
context.fillStyle = "#ff0000";
context.fillRect(10, 10, 50, 50);
//设置合成操作
context.globalCompositeOperation = "destination-over";
//绘制蓝色矩形
context.fillStyle = "rgba(0,0,255,1)";
context.fillRect(30, 30, 50, 50); 

如果不修改 globalCompositionOperation,那么蓝色矩形应该位于红色矩形之上。但把globalCompositionOperation 设置为"destination-over"之后,红色矩形跑到了蓝色矩形上面。
在使用 globalCompositionOperation 的情况下,一定要多测试一些浏览器。因为不同浏览器对这个属性的实现仍然存在较大的差别。Safari 和 Chrome 在这方面还有问题,至于有什么问题,大家可以比较在打开上述页面的情况下,IE9+和 Firefox 4+与它们有什么差异。

caizhendi commented 4 years ago

2020.06.29

阅读进度: 15.3.2 p468

3. WebGL

WebGL 是针对 Canvas 的 3D 上下文。WebGL 并不是 W3C 制定的标准,而是由 Khronos Group 制定的。
其官方网站是这样介绍的:“Khronos Group 是一个非盈利的由会员资助的协会,专注于为并行计算以及各种平台和设备上的图形及动态媒体制定无版税的开放标准。” KhronosGroup 也设计了其他图形处理 API,比如 OpenGL ES 2.0。浏览器中使用的 WebGL 就是基于 OpenGL ES2.0 制定的。

3.1 类型化数组

WebGL 涉及的复杂计算需要提前知道数值的精度,而标准的 JavaScript 数值无法满足需要。为此,WebGL 引入了一个概念,叫类型化数组(typed arrays)。类型化数组也是数组,只不过其元素被设置为特定类型的值。
类型化数组的核心就是一个名为 ArrayBuffer 的类型。每个 ArrayBuffer 对象表示的只是内存中指定的字节数,但不会指定这些字节用于保存什么类型的数据。通过 ArrayBuffer 所能做的,就是为了将来使用而分配一定数量的字节。下面这行代码会在内存中分配 20B。

var buffer = new ArrayBuffer(20); 

创建了 ArrayBuffer 对象后,能够通过该对象获得的信息只有它包含的字节数,方法是访问其byteLength 属性:

var bytes = buffer.byteLength; 
  1. 视图
    使用 ArrayBuffer(数组缓冲器类型)的一种特别的方式就是用它来创建数组缓冲器视图。其中,最常见的视图是 DataView,通过它可以选择 ArrayBuffer 中一小段字节。为此,可以在创建 DataView实例的时候传入一个 ArrayBuffer、一个可选的字节偏移量(从该字节开始选择)和一个可选的要选择的字节数。
    //基于整个缓冲器创建一个新视图
    var view = new DataView(buffer);
    //创建一个开始于字节 9 的新视图
    var view = new DataView(buffer, 9);
    //创建一个从字节 9 开始到字节 18 的新视图
    var view = new DataView(buffer, 9, 10);   

    实例化之后,DataView 对象会把字节偏移量以及字节长度信息分别保存在 byteOffset 和byteLength 属性中。

    alert(view.byteOffset);
    alert(view.byteLength);

    通过这两个属性可以在以后方便地了解视图的状态。另外,通过其 buffer 属性也可以取得数组缓冲器。
    读取和写入 DataView 的时候,要根据实际操作的数据类型,选择相应的 getter 和 setter 方法。DataView 支持的数据类型以及相应的读写方法。见p464
    所有这些方法的第一个参数都是一个字节偏移量,表示要从哪个字节开始读取或写入。不要忘了,要保存有些数据类型的数据,可能需要不止 1B。比如,无符号 8 位整数要用 1B,而 32 位浮点数则要用4B。

    var buffer = new ArrayBuffer(20),
    view = new DataView(buffer),
    value;
    view.setUint16(0, 25);
    view.setUint16(2, 50); //不能从字节 1 开始,因为 16 位整数要用 2B
    value = view.getUint16(0); 

    用于读写 16 位或更大数值的方法都有一个可选的参数 littleEndian。这个参数是一个布尔值,表示读写数值时是否采用小端字节序(即将数据的最低有效位保存在低内存地址中),而不是大端字节序(即将数据的最低有效位保存在高内存地址中)。如果你也不确定应该使用哪种字节序,那不用管它,就采用默认的大端字节序方式保存即可。
    因为在这里使用的是字节偏移量,而非数组元素数,所以可以通过几种不同的方式来访问同一字节。

    var buffer = new ArrayBuffer(20),
    view = new DataView(buffer),
    value;
    view.setUint16(0, 25);
    value = view.getInt8(0);
    alert(value); //0 

    在这个例子中,数值 25 以 16 位无符号整数的形式被写入,字节偏移量为 0。然后,再以 8 位有符号整数的方式读取该数据,得到的结果是 0。这是因为 25 的二进制形式的前 8 位(第一个字节)全部是0。

  2. 类型化视图
    类型化视图一般也被称为类型化数组,因为它们除了元素必须是某种特定的数据类型外,与常规的数组无异。类型化视图也分几种,而且它们都继承了 DataView。
    • Int8Array:表示 8 位二补整数。
    • Uint8Array:表示 8 位无符号整数。
    • Int16Array:表示 16 位二补整数。
    • Uint16Array:表示 16 位无符号整数。
    • Int32Array:表示 32 位二补整数。
    • Uint32Array:表示 32 位无符号整数。
    • Float32Array:表示 32 位 IEEE 浮点值。
    • Float64Array:表示 64 位 IEEE 浮点值。

每种视图类型都以不同的方式表示数据,而同一数据视选择的类型不同有可能占用一或多字节。例如,20B 的 ArrayBuffer 可以保存 20 个 Int8Array 或 Uint8Array,或者 10 个 Int16Array 或Uint16Array,或者 5 个 Int32Array、Uint32Array 或 Float32Array,或者 2 个 Float64Array。
由于这些视图都继承自 DataView,因而可以使用相同的构造函数参数来实例化。第一个参数是要使用 ArrayBuffer 对象,第二个参数是作为起点的字节偏移量(默认为 0),第三个参数是要包含的字节数。三个参数中只有第一个是必需的。

//创建一个新数组,使用整个缓冲器
var int8s = new Int8Array(buffer);
//只使用从字节 9 开始的缓冲器
var int16s = new Int16Array(buffer, 9);
//只使用从字节 9 到字节 18 的缓冲器
var uint16s = new Uint16Array(buffer, 9, 10); 

能够指定缓冲器中可用的字节段,意味着能在同一个缓冲器中保存不同类型的数值。比如,下面的代码就是在缓冲器的开头保存 8 位整数,而在其他字节中保存 16 位整数。

//使用缓冲器的一部分保存 8 位整数,另一部分保存 16 位整数
var int8s = new Int8Array(buffer, 0, 10);
var uint16s = new Uint16Array(buffer, 11, 10); 

每个视图构造函数都有一个名为 BYTES_PER_ELEMENT 的属性,表示类型化数组的每个元素需要多少字节。因此,Uint8Array.BYTES_PER_ELEMENT 就是 1,而 Float32Array.BYTES_PER_ELEMENT则为 4。

//需要 10 个元素空间
var int8s = new Int8Array(buffer, 0, 10 * Int8Array.BYTES_PER_ELEMENT);
//需要 5 个元素空间
var uint16s = new Uint16Array(buffer, int8s.byteOffset + int8s.byteLength,
 5 * Uint16Array.BYTES_PER_ELEMENT); 

类型化视图的目的在于简化对二进制数据的操作。除了前面看到的优点之外,创建类型化视图还可以不用首先创建 ArrayBuffer 对象。只要传入希望数组保存的元素数,相应的构造函数就可以自动创建一个包含足够字节数的 ArrayBuffer 对象。

//创建一个数组保存 10 个 8 位整数(10 字节)
var int8s = new Int8Array(10);
//创建一个数组保存 10 个 16 位整数(20 字节)
var int16s = new Int16Array(10); 

另外,也可以把常规数组转换为类型化视图,只要把常规数组传入类型化视图的构造函数即可:

//创建一个数组保存 5 个 8 位整数(10 字节)
var int8s = new Int8Array([10, 20, 30, 40, 50]); 

使用类型化视图时,可以通过方括号语法访问每一个数据成员,可以通过 length 属性确定数组中有多少元素。

for (var i=0, len=int8s.length; i < len; i++){
 console.log("Value at position " + i + " is " + int8s[i]);
} 

当然,也可以使用方括号语法为类型化视图的元素赋值。如果为相应元素指定的字节数放不下相应的值,则实际保存的值是最大可能值的模。例如,无符号 16 位整数所能表示的最大数值是 65535,如果你想保存 65536,那实际保存的值是 0;如果你想保存 65537,那实际保存的值是 1,依此类推。

var uint16s = new Uint16Array(10);
uint16s[0] = 65537;
alert(uint16s[0]); //1 

类型化视图还有一个方法,即 subarray(),使用这个方法可以基于底层数组缓冲器的子集创建一个新视图。这个方法接收两个参数:开始元素的索引和可选的结束元素的索引。返回的类型与调用该方法的视图类型相同。

var uint16s = new Uint16Array(10),
 sub = uint16s.subarray(2, 5); 

在以上代码中,sub 也是 Uint16Array 的一个实例,而且底层与 uint16s 都基于同一个ArrayBuffer。通过大视图创建小视图的主要好处就是,在操作大数组中的一部分元素时,无需担心意外修改了其他元素。 类型化数组是 WebGL 项目中执行各种操作的重要基础。

caizhendi commented 4 years ago

2020.07.01

阅读进度: 15.3.2.5 p470

3.2 WebGL上下文

在支持的浏览器中,WebGL 的名字叫"experimental-webgl",这是因为 WebGL 规范仍然未制定完成。制定完成后,这个上下文的名字就会变成简单的"webgl"。如果浏览器不支持 WebGL,那么取得该上下文时会返回 null。在使用 WebGL 上下文时,务必先检测一下返回值。

var drawing = document.getElementById("drawing");
//确定浏览器支持<canvas>元素
if (drawing.getContext){
 var gl = drawing.getContext("experimental-webgl");
 if (gl){
 //使用 WebGL
 }
} 

一般都把WebGL上下文对象命名为gl。大多数WebGL应用和示例都遵守这一约定,因为 OpenGLES 2.0 规定的方法和值通常都以"gl"开头。这样做也可以保证 JavaScript 代码与 OpenGL 程序更相近。
WebGL 是 OpenGL ES 2.0 的 Web版,因此本节讨论的概念实际上就是 OpenGL 概念在 JavaScript 中的实现。
给 getContext()传递第二个参数,可以为 WebGL 上下文设置一些选项。这个参数本身是一个对象,可以包含下列属性。

建议确实有必要的情况下再开启这个值,因为可能影响性能。
传递这个选项对象的方式如下:

var drawing = document.getElementById("drawing");
//确定浏览器支持<canvas>元素
if (drawing.getContext){
 var gl = drawing.getContext("experimental-webgl", { alpha: false});
 if (gl){
 //使用 WebGL 
  }
} 

如果 getContext()无法创建 WebGL 上下文,有的浏览器会抛出错误。为此,最好把调用封装到一个 try-catch 块中。

//确定浏览器支持<canvas>元素
if (drawing.getContext){
 try {
 gl = drawing.getContext("experimental-webgl");
 } catch (ex) {
 //什么也不做
 }
 if (gl){
 //使用 WebGL
 } else {
 alert("WebGL context could not be created.");
 }
} 
  1. 常量
    如果你熟悉 OpenGL,那肯定会对各种操作中使用非常多的常量印象深刻。这些常量在 OpenGL 中都带前缀 GL_。 在 WebGL 中,保存在上下文对象中的这些常量都没有 GL_前缀。比如说,GL_COLOR_BUFFER_BIT 常量在 WebGL 上下文中就是 gl.COLOR_BUFFER_BIT。WebGL 以这种方式支持大多数 OpenGL 常量(有一部分常量是不支持的)。
  2. 方法命名
    OpenGL(以及 WebGL)中的很多方法都试图通过名字传达有关数据类型的信息。如果某方法可以接收不同类型及不同数量的参数,看方法名的后缀就可以知道。方法名的后缀会包含参数个数(1 到 4)和接收的数据类型(f 表示浮点数,i 表示整数)。例如,gl.uniform4f()意味着要接收 4 个浮点数,而 gl.uniform3i()则表示要接收 3 个整数。
    也有很多方法接收数组参数而非一个个单独的参数。这样的方法其名字中会包含字母 v(即 vector,矢量)。因此,gl.uniform3iv()可以接收一个包含 3 个值的整数数组。
  3. 准备绘图
    在实际操作 WebGL 上下文之前,一般都要使用某种实色清除<canvas>,为绘图做好准备。为此,首先必须使用 clearColor()方法来指定要使用的颜色值,该方法接收 4 个参数:红、绿、蓝和透明度。每个参数必须是一个 0 到 1 之间的数值,表示每种分量在最终颜色中的强度。
    gl.clearColor(0,0,0,1); //black
    gl.clear(gl.COLOR_BUFFER_BIT);

    以上代码把清理颜色缓冲区的值设置为黑色,然后调用了 clear()方法,这个方法与 OpenGL 中的glClear()等价。传入的参数 gl.COLOR_BUFFER_BIT 告诉 WebGL 使用之前定义的颜色来填充相应区域。一般来说,都要先清理缓冲区,然后再执行其他绘图操作。

  4. 视口与坐标
    开始绘图之前,通常要先定义 WebGL 的视口(viewport)。默认情况下,视口可以使用整个<canvas>区域。要改变视口大小,可以调用 viewport()方法并传入 4 个参数:(视口相对于<canvas>元素的)x 坐标、y 坐标、宽度和高度。
    gl.viewport(0, 0, drawing.width, drawing.height); 

    视口坐标的原点(0,0)在<canvas>元素的左下角,x轴和 y 轴的正方向分别是向右和向上,可以定义为(width1, height1)。
    知道怎么定义视口大小,就可以只在<canvas>元素的部分区域中绘图。

    //视口是<canvas>左下角的四分之一区域
    gl.viewport(0, 0, drawing.width/2, drawing.height/2);
    //视口是<canvas>左上角的四分之一区域
    gl.viewport(0, drawing.height/2, drawing.width/2, drawing.height/2);
    //视口是<canvas>右下角的四分之一区域
    gl.viewport(drawing.width/2, 0, drawing.width/2, drawing.height/2); 

    另外,视口内部的坐标系与定义视口的坐标系也不一样。在视口内部,坐标原点(0,0)是视口的中心点,因此视口左下角坐标为(1,1),而右上角坐标为(1,1)。
    如果在视口内部绘图时使用视口外部的坐标,结果可能会被视口剪切。比如,要绘制的形状有一个顶点在(1,2),那么该形状在视口右侧的部分会被剪切掉。

caizhendi commented 4 years ago

2020.07.02

阅读进度: 15.3.2.9 p473

  1. 缓冲区
    顶点信息保存在 JavaScript 的类型化数组中,使用之前必须转换到 WebGL 的缓冲区。要创建缓冲区,可以调用 gl.createBuffer(),然后使用 gl.bindBuffer()绑定到 WebGL 上下文。这两步做完之后,就可以用数据来填充缓冲区了。
    var buffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([0, 0.5, 1]), gl.STATIC_DRAW); 

    调用 gl.bindBuffer()可以将 buffer 设置为上下文的当前缓冲区。此后,所有缓冲区操作都直接在 buffer 中执行。因此,调用 gl.bufferData()时不需要明确传入 buffer 也没有问题。最后一行代码使用 Float32Array 中的数据初始化了 buffer(一般都是用 Float32Array 来保存顶点信息)。如果想使用 drawElements()输出缓冲区的内容,也可以传入 gl.ELEMENT_ARRAY_BUFFER。
    gl.bufferData()的最后一个参数用于指定使用缓冲区的方式,取值范围是如下几个常量。

    • gl.STATIC_DRAW:数据只加载一次,在多次绘图中使用。
    • gl.STREAM_DRAW:数据只加载一次,在几次绘图中使用。
    • gl.DYNAMIC_DRAW:数据动态改变,在多次绘图中使用。

多数情况下将缓冲区使用方式设置为 gl.STATIC_DRAW即可。
在包含缓冲区的页面重载之前,缓冲区始终保留在内存中。如果你不想要某个缓冲区了,可以直接调用 gl.deleteBuffer()释放内存:

gl.deleteBuffer(buffer); 
  1. 错误
    JavaScript 与 WebGL 之间的一个最大的区别在于,WebGL 操作一般不会抛出错误。为了知道是否有错误发生,必须在调用某个可能出错的方法后,手工调用 gl.getError()方法。这个方法返回一个表示错误类型的常量。
    • gl.NO_ERROR:上一次操作没有发生错误(值为 0)。
    • gl.INVALID_ENUM:应该给方法传入 WebGL 常量,但却传错了参数。
    • gl.INVALID_VALUE:在需要无符号数的地方传入了负值。
    • gl.INVALID_OPERATION:在当前状态下不能完成操作。
    • gl.OUT_OF_MEMORY:没有足够的内存完成操作。
    • gl.CONTEXT_LOST_WEBGL:由于外部事件(如设备断电)干扰丢失了当前 WebGL 上下文。

每次调用 gl.getError()方法返回一个错误值。第一次调用后,后续对 gl.getError()的调用可能会返回另一个错误值。如果发生了多个错误,需要反复调用 gl.getError()直至它返回gl.NO_ERROR。在执行了很多操作的情况下,最好通过一个循环来调用 getError()。

var errorCode = gl.getError();
while(errorCode){
 console.log("Error occurred: " + errorCode);
 errorCode = gl.getError();
} 
  1. 着色器
    着色器(shader)是 OpenGL 中的另一个概念。WebGL 中有两种着色器:顶点着色器和片段(或像素)着色器。顶点着色器用于将 3D 顶点转换为需要渲染的 2D 点。片段着色器用于准确计算要绘制的每个像素的颜色。WebGL 着色器的独特之处也是其难点在于,它们并不是用 JavaScript 写的。这些着色器是使用 GLSL(OpenGL Shading Language,OpenGL 着色语言)写的,GLSL 是一种与 C 和 JavaScript完全不同的语言。
  2. 编写着色器
    GLSL 是一种类 C 语言,专门用于编写 OpenGL 着色器。因为 WebGL 是 OpenGL ES 2.0 的实现,所以 OpenGL 中使用的着色器可以直接在 WebGL 中使用。
    每个着色器都有一个 main()方法,该方法在绘图期间会重复执行。为着色器传递数据的方式有两种:Attribute 和 Uniform。通过 Attribute 可以向顶点着色器中传入顶点信息,通过 Uniform 可以向任何着色器传入常量值。Attribute 和 Uniform 在 main()方法外部定义,分别使用关键字 attribute 和uniform。在这两个值类型关键字之后,是数据类型和变量名。
    //OpenGL 着色语言
    //着色器,作者 Bartek Drozdz,摘自他的文章
    //http://www.netmagazine.com/tutorials/get-started-webgl-draw-square
    attribute vec2 aVertexPosition;
    void main() {
    gl_Position = vec4(aVertexPosition, 0.0, 1.0);
    } 

    这个顶点着色器定义了一个名为 aVertexPosition 的 Attribute,这个 Attribute 是一个数组,包含两个元素(数据类型为 vec2),表示 x 和 y 坐标。即使只接收到两个坐标,顶点着色器也必须把一个包含四方面信息的顶点赋值给特殊变量 gl_Position。这里的着色器创建了一个新的包含四个元素的数组(vec4),填补缺失的坐标,结果是把 2D 坐标转换成了 3D 坐标。
    除了只能通过 Uniform 传入数据外,片段着色器与顶点着色器类似。

    //OpenGL 着色语言
    //着色器,作者 Bartek Drozdz,摘自他的文章
    //http://www.netmagazine.com/tutorials/get-started-webgl-draw-square
    uniform vec4 uColor;
    void main() {
    gl_FragColor = uColor;
    } 

    片段着色器必须返回一个值,赋给变量gl_FragColor,表示绘图时使用的颜色。这个着色器定义了一个包含四方面信息(vec4)的统一的颜色 uColor。从以上代码看,这个着色器除了把传入的值赋给 gl_FragColor 什么也没做。uColor 的值在这个着色器内部不能改变。

hzjjg commented 4 years ago

牛逼

发自我的小米手机 在 2020年7月3日 00:24,caizhendi notifications@github.com写道:

2020.07.02

阅读进度: 15.3.2.9 p473

  1. 缓冲区 顶点信息保存在 JavaScript 的类型化数组中,使用之前必须转换到 WebGL 的缓冲区。要创建缓冲区,可以调用 gl.createBuffer(),然后使用 gl.bindBuffer()绑定到 WebGL 上下文。这两步做完之后,就可以用数据来填充缓冲区了。

var buffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, buffer); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([0, 0.5, 1]), gl.STATIC_DRAW);

调用 gl.bindBuffer()可以将 buffer 设置为上下文的当前缓冲区。此后,所有缓冲区操作都直接在 buffer 中执行。因此,调用 gl.bufferData()时不需要明确传入 buffer 也没有问题。最后一行代码使用 Float32Array 中的数据初始化了 buffer(一般都是用 Float32Array 来保存顶点信息)。如果想使用 drawElements()输出缓冲区的内容,也可以传入 gl.ELEMENT_ARRAY_BUFFER。 gl.bufferData()的最后一个参数用于指定使用缓冲区的方式,取值范围是如下几个常量。

多数情况下将缓冲区使用方式设置为 gl.STATIC_DRAW即可。 在包含缓冲区的页面重载之前,缓冲区始终保留在内存中。如果你不想要某个缓冲区了,可以直接调用 gl.deleteBuffer()释放内存:

gl.deleteBuffer(buffer);

  1. 错误 JavaScript 与 WebGL 之间的一个最大的区别在于,WebGL 操作一般不会抛出错误。为了知道是否有错误发生,必须在调用某个可能出错的方法后,手工调用 gl.getError()方法。这个方法返回一个表示错误类型的常量。

    • gl.NO_ERROR:上一次操作没有发生错误(值为 0)。
    • gl.INVALID_ENUM:应该给方法传入 WebGL 常量,但却传错了参数。
    • gl.INVALID_VALUE:在需要无符号数的地方传入了负值。
    • gl.INVALID_OPERATION:在当前状态下不能完成操作。
    • gl.OUT_OF_MEMORY:没有足够的内存完成操作。
    • gl.CONTEXT_LOST_WEBGL:由于外部事件(如设备断电)干扰丢失了当前 WebGL 上下文。

每次调用 gl.getError()方法返回一个错误值。第一次调用后,后续对 gl.getError()的调用可能会返回另一个错误值。如果发生了多个错误,需要反复调用 gl.getError()直至它返回gl.NO_ERROR。在执行了很多操作的情况下,最好通过一个循环来调用 getError()。

var errorCode = gl.getError(); while(errorCode){ console.log("Error occurred: " + errorCode); errorCode = gl.getError(); }

  1. 着色器 着色器(shader)是 OpenGL 中的另一个概念。WebGL 中有两种着色器:顶点着色器和片段(或像素)着色器。顶点着色器用于将 3D 顶点转换为需要渲染的 2D 点。片段着色器用于准确计算要绘制的每个像素的颜色。WebGL 着色器的独特之处也是其难点在于,它们并不是用 JavaScript 写的。这些着色器是使用 GLSL(OpenGL Shading Language,OpenGL 着色语言)写的,GLSL 是一种与 C 和 JavaScript完全不同的语言。
  2. 编写着色器 GLSL 是一种类 C 语言,专门用于编写 OpenGL 着色器。因为 WebGL 是 OpenGL ES 2.0 的实现,所以 OpenGL 中使用的着色器可以直接在 WebGL 中使用。 每个着色器都有一个 main()方法,该方法在绘图期间会重复执行。为着色器传递数据的方式有两种:Attribute 和 Uniform。通过 Attribute 可以向顶点着色器中传入顶点信息,通过 Uniform 可以向任何着色器传入常量值。Attribute 和 Uniform 在 main()方法外部定义,分别使用关键字 attribute 和uniform。在这两个值类型关键字之后,是数据类型和变量名。

//OpenGL 着色语言 //着色器,作者 Bartek Drozdz,摘自他的文章 //http://www.netmagazine.com/tutorials/get-started-webgl-draw-square attribute vec2 aVertexPosition; void main() { gl_Position = vec4(aVertexPosition, 0.0, 1.0); }

这个顶点着色器定义了一个名为 aVertexPosition 的 Attribute,这个 Attribute 是一个数组,包含两个元素(数据类型为 vec2),表示 x 和 y 坐标。即使只接收到两个坐标,顶点着色器也必须把一个包含四方面信息的顶点赋值给特殊变量 gl_Position。这里的着色器创建了一个新的包含四个元素的数组(vec4),填补缺失的坐标,结果是把 2D 坐标转换成了 3D 坐标。 除了只能通过 Uniform 传入数据外,片段着色器与顶点着色器类似。

//OpenGL 着色语言 //着色器,作者 Bartek Drozdz,摘自他的文章 //http://www.netmagazine.com/tutorials/get-started-webgl-draw-square uniform vec4 uColor; void main() { gl_FragColor = uColor; }

片段着色器必须返回一个值,赋给变量gl_FragColor,表示绘图时使用的颜色。这个着色器定义了一个包含四方面信息(vec4)的统一的颜色 uColor。从以上代码看,这个着色器除了把传入的值赋给 gl_FragColor 什么也没做。uColor 的值在这个着色器内部不能改变。

― You are receiving this because you are subscribed to this thread. Reply to this email directly, view it on GitHubhttps://eur06.safelinks.protection.outlook.com/?url=https%3A%2F%2Fgithub.com%2Fcaizhendi%2Fblog%2Fissues%2F3%23issuecomment-653104564&data=02%7C01%7C%7C838d46dfca0d4f6a382a08d81ea462fb%7C84df9e7fe9f640afb435aaaaaaaaaaaa%7C1%7C0%7C637293038662474141&sdata=cDSz7YCfDtsI%2F%2BVSuCD%2F4qcjn0GTGuFGOEXyhIJL1%2B8%3D&reserved=0, or unsubscribehttps://eur06.safelinks.protection.outlook.com/?url=https%3A%2F%2Fgithub.com%2Fnotifications%2Funsubscribe-auth%2FADNAJT3TXHDY4EJADH6CSGDRZSYDRANCNFSM4N6ENZUA&data=02%7C01%7C%7C838d46dfca0d4f6a382a08d81ea462fb%7C84df9e7fe9f640afb435aaaaaaaaaaaa%7C1%7C0%7C637293038662474141&sdata=VqZL6Ory%2FRAimfNk7fX7w%2BdytnC3vCFl9XeXkehkZm4%3D&reserved=0.

caizhendi commented 4 years ago

2020.07.03

阅读进度: 15.3.2.12 p475

  1. 编写着色器程序
    浏览器不能理解 GLSL 程序,因此必须准备好字符串形式的 GLSL 程序,以便编译并链接到着色器程序。为便于使用,通常是把着色器包含在页面的<script>标签内,并为该标签指定一个自定义的 type属性。由于无法识别 type 属性值,浏览器不会解析<script>标签中的内容,但这不影响你读写其中的代码。
    <script type="x-webgl/x-vertex-shader" id="vertexShader">
    attribute vec2 aVertexPosition;
    void main() {
    gl_Position = vec4(aVertexPosition, 0.0, 1.0);
    }
    </script>
    <script type="x-webgl/x-fragment-shader" id="fragmentShader">
    uniform vec4 uColor;
    void main() {
    gl_FragColor = uColor;
    }
    </script> 

    然后,可以通过 text 属性提取出<script>元素的内容:

    var vertexGlsl = document.getElementById("vertexShader").text,
    fragmentGlsl = document.getElementById("fragmentShader").text; 

    使用着色器的关键是要有字符串形式的 GLSL 程序。
    取得了 GLSL 字符串之后,接下来就是创建着色器对象。要创建着色器对象,可以调用 gl.createShader()方法并传入要创建的着色器类型(gl.VERTEX_SHADE或 gl.FRAGMENT_SHADER)。编译着色器使用的是 gl.compileShader()。

    var vertexShader = gl.createShader(gl.VERTEX_SHADER);
    gl.shaderSource(vertexShader, vertexGlsl);
    gl.compileShader(vertexShader);
    var fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
    gl.shaderSource(fragmentShader, fragmentGlsl);
    gl.compileShader(fragmentShader); 

    以上代码创建了两个着色器,并将它们分别保存在 vertexShader 和 fragmentShader 中。而使用下列代码,可以把这两个对象链接到着色器程序中。

    var program = gl.createProgram();
    gl.attachShader(program, vertexShader);
    gl.attachShader(program, fragmentShader);
    gl.linkProgram(program); 

    第一行代码创建了程序,然后调用attachShader()方法又包含了两个着色器。最后调用gl.linkProgram()则把两个着色器封装到了变量 program 中。链接完程序之后,就可以通过 gl.useProgram()方法通知 WebGL 使用这个程序了。

    gl.useProgram(program); 
  2. 为着色器传入值
    前面定义的着色器都必须接收一个值才能工作。为了给着色器传入这个值,必须先找到要接收这个值的变量。对于 Uniform 变量,可以使用 gl.getUniformLocation(),这个方法返回一个对象,表示Uniform 变量在内存中的位置。然后可以基于变量的位置来赋值。
    var uColor = gl.getUniformLocation(program, "uColor");
    gl.uniform4fv(uColor, [0, 0, 0, 1]); 

    第一行代码从 program 中找到 Uniform 变量 uColor,返回了它在内存中的位置。第二行代码使用gl.uniform4fv()给 uColor 赋值。
    对于顶点着色器中的 Attribute 变量,也是差不多的赋值过程。要找到 Attribute 变量在内存中的位置,可以调用 gl.getAttribLocation()。取得了位置之后,就可以像下面这样赋值了:

    var aVertexPosition = gl.getAttribLocation(program, "aVertexPosition");
    gl.enableVertexAttribArray(aVertexPosition);
    gl.vertexAttribPointer(aVertexPosition, itemSize, gl.FLOAT, false, 0, 0); 

    取得了 aVertexPosition 的位置,然后又通过 gl.enableVertexAttribArray()启用它。最后一行创建了指针,指向由 gl.bindBuffer() 指定的缓冲区,并将其保存在aVertexPosition 中,以便顶点着色器使用。

  3. 调试着色器和程序
    与 WebGL 中的其他操作一样,着色器操作也可能会失败,而且也是静默失败。如果你想知道着色器或程序执行中是否发生了错误,必须亲自询问 WebGL 上下文。
    对于着色器,可以在操作之后调用 gl.getShaderParameter(),取得着色器的编译状态:
    if (!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)){
    alert(gl.getShaderInfoLog(vertexShader));
    } 

    这个例子检测了 vertexShader 的编译状态。如果着色器编译成功,调用 gl.getShaderParameter()会返回true。如果返回的是 false,说明编译期间发生了错误,此时调用gl.getShaderInfoLog()并传入相应的着色器就可以取得错误消息。错误消息就是一个表示问题所在的字符串。无论是顶点着色器,还是片段着色器,都可以使用gl.getShaderParameter()和gl.getShaderInfoLog()方法。
    程序也可能会执行失败,因此也有类似的方法——gl.getProgramParameter(),可以用来检测执行状态。最常见的程序失败发生在链接过程中,要检测链接错误。

    if (!gl.getProgramParameter(program, gl.LINK_STATUS)){
    alert(gl.getProgramInfoLog(program));
    } 

    gl.getProgramParameter()返回 true 表示链接成功,返回 false 表示链接失败。同样,也有一个 gl.getProgramInfoLog()方法,用于捕获程序失败的消息。

caizhendi commented 4 years ago

2020.07.04

阅读进度: 16章 p480

  1. 绘图
    WebGL 只能绘制三种形状:点、线和三角。其他所有形状都是由这三种基本形状合成之后,再绘制到三维空间中的。执行绘图操作要调用 gl.drawArrays()或 gl.drawElements()方法,前者用于数组缓冲区,后者用于元素数组缓冲区。
    gl.drawArrays()或 gl.drawElements()的第一个参数都是一个常量,表示要绘制的形状。
    • gl.POINTS:将每个顶点当成一个点来绘制。
    • gl.LINES:将数组当成一系列顶点,在这些顶点间画线。每个顶点既是起点也是终点,因此数组中必须包含偶数个顶点才能完成绘制。
    • gl.LINE_LOOP:将数组当成一系列顶点,在这些顶点间画线。线条从第一个顶点到第二个顶点,再从第二个顶点到第三个顶点,依此类推,直至最后一个顶点。然后再从最后一个顶点到第一个顶点画一条线。结果就是一个形状的轮廓。
    • gl.LINE_STRIP:除了不画最后一个顶点与第一个顶点之间的线之外,其他与 gl.LINE_LOOP相同。
    • gl.TRIANGLES:将数组当成一系列顶点,在这些顶点间绘制三角形。除非明确指定,每个三角形都单独绘制,不与其他三角形共享顶点。
    • gl.TRIANGLES_STRIP:除了将前三个顶点之后的顶点当作第三个顶点与前两个顶点共同构成一个新三角形外,其他都与 gl.TRIANGLES 相同。例如,如果数组中包含 A、B、C、D 四个顶点,则第一个三角形连接 ABC,而第二个三角形连接 BCD。
    • gl. TRIANGLES_FAN:除了将前三个顶点之后的顶点当作第三个顶点与前一个顶点及第一个顶点共同构成一个新三角形外,其他都与 gl.TRIANGLES 相同。例如,如果数组中包含 A、B、C、D 四个顶点,则第一个三角形连接 ABC,而第二个三角形连接 ACD。

gl.drawArrays()方法接收上面列出的常量中的一个作为第一个参数,接收数组缓冲区中的起始索引作为第二个参数,接收数组缓冲区中包含的顶点数(点的集合数)作为第三个参数。下面的代码使用 gl.drawArrays()在画布上绘制了一个三角形。

//假设已经使用本节前面定义的着色器清除了视口
//定义三个顶点以及每个顶点的 x 和 y 坐标
var vertices = new Float32Array([ 0, 1, 1, -1, -1, -1 ]),
 buffer = gl.createBuffer(),
 vertexSetSize = 2,
 vertexSetCount = vertices.length/vertexSetSize,
 uColor, aVertexPosition;
//把数据放到缓冲区
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
//为片段着色器传入颜色值
uColor = gl.getUniformLocation(program, "uColor");
gl.uniform4fv(uColor, [ 0, 0, 0, 1 ]);
//为着色器传入顶点信息
aVertexPosition = gl.getAttribLocation(program, "aVertexPosition");
gl.enableVertexAttribArray(aVertexPosition);
gl.vertexAttribPointer(aVertexPosition, vertexSetSize, gl.FLOAT, false, 0, 0);
//绘制三角形
gl.drawArrays(gl.TRIANGLES, 0, vertexSetCount); 

这个例子定义了一个 Float32Array,包含三组顶点(每个顶点由两点表示)。这里关键是要知道顶点的大小及数量,以便将来计算时使用。把 vertexSetSize 设置为 2 之后,就可以计算出vertexSetCount 的值。把顶点的信息保存在缓冲区中后,又把颜色信息传给了片段着色器。

  1. 纹理
    WebGL 的纹理可以使用 DOM 中的图像。要创建一个新纹理,可以调用 gl.createTexture(),然后再将一幅图像绑定到该纹理。如果图像尚未加载到内存中,可能需要创建一个 Image 对象的实例,以便动态加载图像。图像加载完成之前,纹理不会初始化,因此,必须在 load 事件触发后才能设置纹理。
    var image = new Image(),
    texture;
    image.src = "smile.gif";
    image.onload = function(){
    texture = gl.createTexture();
    gl.bindTexture(gl.TEXTURE_2D, texture);
    gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
    //清除当前纹理
    gl.bindTexture(gl.TEXTURE_2D, null);
    }

    除了使用 DOM 中的图像之外,以上步骤与在 OpenGL 中创建纹理的步骤相同。最大的差异是使用gl.pixelStore1()设置像素存储格式。gl.UNPACK_FLIP_Y_WEBGL 是 WebGL 独有的常量,在加载Web 中的图像时,多数情况下都必须使用这个常量。这主要是因为 GIF、JPEG 和 PNG 图像与 WebGL使用的坐标系不一样,如果没有这个标志,解析图像时就会发生混乱。
    用作纹理的图像必须与包含页面来自同一个域,或者是保存在启用了 CORS(Cross-Origin ResourceSharing,跨域资源共享)的服务器上。

  2. 读取像素
    与 2D 上下文 类似,通过 WebGL 上下文也能读取像素值。读取像素值的方法 readPixels()与OpenGL 中的同名方法只有一点不同,即最后一个参数必须是类型化数组。像素信息是从帧缓冲区读取的,然后保存在类型化数组中。readPixels()方法的参数有:x、y、宽度、高度、图像格式、数据类型和类型化数组。前 4 个参数指定读取哪个区域中的像素。图像格式参数几乎总是 gl.RGBA。数据类型参数用于指定保存在类型化数组中的数据的类型,但有以下限制。
    • 如果类型是 gl.UNSIGNED_BYTE,则类型化数组必须是 Uint8Array。
    • 如果类型是 gl.UNSIGNED_SHORT_5_6_5、gl.UNSIGNED_SHORT_4_4_4_4 或 gl.UNSIGNED_SHORT_5_5_5_1,则类型化数组必须是 Uint16Array。
var pixels = new Uint8Array(25*25);
gl.readPixels(0, 0, 25, 25, gl.RGBA, gl.UNSIGNED_BYTE, pixels); 

以上代码从帧缓冲区中读取了 25×25 像素的区域,将读取到的像素信息保存到了 pixels 数组中。其中,每个像素的颜色由 4 个数组元素表示,分别代表红、绿、蓝和透明度。每个数组元素的值介于 0到 255 之间(包含 0 和 255)。不要忘了根据返回的数据大小初始化类型化数组。
在浏览器绘制更新的 WebGL 图像之前调用 readPixels()不会有什么意外。绘制发生后,帧缓冲区会恢复其原始的干净状态,而调用 readPixels()返回的像素数据反映的就是清除缓冲区后的状态。如果你想在绘制发生后读取像素数据,那在初始化 WebGL 上下文时必须传入适当的preserveDrawingBuffer 选项。

var gl = drawing.getContext("experimental-webgl", { preserveDrawingBuffer: true; }); 

3. 支持

Firefox 4+和 Chrome 都实现了 WebGL API。Safari 5.1 也实现了 WebGL,但默认是禁用的。WebGL比较特别的地方在于,某个浏览器的某个版本实现了它,并不一定意味着就真能使用它。某个浏览器支持 WebGL,至少意味着两件事:首先,浏览器本身必须实现了 WebGL API;其次,计算机必须升级显示驱动程序。运行 Windows XP 等操作系统的一些老机器,其驱动程序一般都不是最新的。因此,这些计算机中的浏览器都会禁用 WebGL。从稳妥的角度考虑,在使用 WebGL 之前,最好检测其是否得到了支持,而不是只检测特定的浏览器版本。

4. 小结

HTML5 的<canvas>元素提供了一组 JavaScript API,让我们可以动态地创建图形和图像。图形是在一个特定的上下文中创建的,而上下文对象目前有两种。第一种是 2D 上下文,可以执行原始的绘图操作

第二种是 3D 上下文,即 WebGL 上下文。WebGL 是从 OpenGL ES 2.0 移植到浏览器中的,而 OpenGLES 2.0 是游戏开发人员在创建计算机图形图像时经常使用的一种语言。WebGL 支持比 2D 上下文更丰富和更强大的图形图像处理能力

目前,主流浏览器的较新版本大都已经支持<canvas>标签。同样地,这些版本的浏览器基本上也都支持 2D 上下文。但对于 WebGL 而言,目前还只有 Firefox 4+和 Chrome 支持它。

caizhendi commented 4 years ago

2020.07.05

阅读进度: 16.2 p481

16.1 跨文档消息传递

跨文档消息传送(cross-document messaging),有时候简称为 XDM,指的是在来自不同域的页面间传递消息。例如,www.wrox.com 域中的页面与位于一个内嵌框架中的 p2p.wrox.com 域中的页面通信。在 XDM 机制出现之前,要稳妥地实现这种通信需要花很多工夫。XDM 把这种机制规范化,让我们能既稳妥又简单地实现跨文档通信。
XDM 的核心是 postMessage()方法。在 HTML5 规范中,除了 XDM 部分之外的其他部分也会提到这个方法名,但都是为了同一个目的:向另一个地方传递数据。对于 XDM 而言,“另一个地方”指的是包含在当前页面中的<iframe>元素,或者由当前页面弹出的窗口。
postMessage()方法接收两个参数:一条消息和一个表示消息接收方来自哪个域的字符串。第二个参数对保障安全通信非常重要,可以防止浏览器把消息发送到不安全的地方。

//注意:所有支持 XDM 的浏览器也支持 iframe 的 contentWindow 属性
var iframeWindow = document.getElementById("myframe").contentWindow;
iframeWindow.postMessage("A secret", "http://www.wrox.com"); 

最后一行代码尝试向内嵌框架中发送一条消息,并指定框架中的文档必须来源于"http://www.wrox.com"域。如果来源匹配,消息会传递到内嵌框架中;否则,postMessage()什么也不做。这一限制可以避免窗口中的位置在你不知情的情况下发生改变。如果传给 postMessage()的第二个参数是"*",则表示可以把消息发送给来自任何域的文档。
接收到 XDM 消息时,会触发 window 对象的 message 事件。这个事件是以异步形式触发的,因此从发送消息到接收消息(触发接收窗口的 message 事件)可能要经过一段时间的延迟。触发 message事件后,传递给 onmessage 处理程序的事件对象包含以下三方面的重要信息。

接收到消息后验证发送窗口的来源是至关重要的。就像给 postMessage()方法指定第二个参数,以确保浏览器不会把消息发送给未知页面一样,在 onmessage 处理程序中检测消息来源可以确保传入的消息来自已知的页面。

EventUtil.addHandler(window, "message", function(event){
 //确保发送消息的域是已知的域
 if (event.origin == "http://www.wrox.com"){
 //处理接收到的数据
 processMessage(event.data);
 //可选:向来源窗口发送回执
 event.source.postMessage("Received!", "http://p2p.wrox.com");
 }
}); 

event.source 大多数情况下只是 window 对象的代理,并非实际的 window 对象。换句话说,不能通过这个代理对象访问 window 对象的其他任何信息。只通过这个代理调用postMessage()就好,这个方法永远存在,永远可以调用。
XDM 还有一些怪异之处。首先,postMessage()的第一个参数最早是作为“永远都是字符串”来实现的。但后来这个参数的定义改了,改成允许传入任何数据结构。可是,并非所有浏览器都实现了这一变化。为保险起见,使用 postMessage()时,最好还是只传字符串。如果你想传入结构化的数据,最佳选择是先在要传入的数据上调用 JSON.stringify(),通过 postMessage()传入得到的字符串,然后再在 onmessage 事件处理程序中调用 JSON.parse()。
支持 XDM 的浏览器有 IE8+、Firefox 3.5+、Safari 4+、Opera、Chrome、iOS 版 Safari 及 Android 版WebKit。XDM 已经作为一个规范独立出来,现在它的名字叫 Web Messaging,官方页面是http://dev.w3.org/html5/postmsg/。

caizhendi commented 4 years ago

2020.07.06

阅读进度: 16.2.4 p484

16.2 原生拖放

最早在网页中引入 JavaScript 拖放功能的是 IE4。当时,网页中只有两种对象可以拖放:图像和某些文本。拖动图像时,把鼠标放在图像上,按住鼠标不放就可以拖动它。拖动文本时,要先选中文本,然后可以像拖动图像一样拖动被选中的文本。在 IE 4 中,唯一有效的放置目标是文本框。到了 IE5,拖放功能得到扩展,添加了新的事件,而且几乎网页中的任何元素都可以作为放置目标。IE5.5 更进一步,让网页中的任何元素都可以拖放。(IE6 同样也支持这些功能。)HTML5 以 IE 的实例为基础制定了拖放规范。Firefox 3.5、Safari 3+和 Chrome 也根据 HTML5 规范实现了原生拖放功能。

2.1 拖放事件

通过拖放事件,可以控制拖放相关的各个方面。其中最关键的地方在于确定哪里发生了拖放事件,有些事件是在被拖动的元素上触发的,而有些事件是在放置目标上触发的。拖动某元素时,将依次触发下列事件:
(1) dragstart
(2) drag
(3) dragend

按下鼠标键并开始移动鼠标时,会在被拖放的元素上触发 dragstart 事件。此时光标变成“不能放”符号(圆环中有一条反斜线),表示不能把元素放到自己上面。
触发 dragstart 事件后,随即会触发 drag 事件,而且在元素被拖动期间会持续触发该事件。这个事件与 mousemove 事件相似,在鼠标移动过程中,mousemove 事件也会持续发生。当拖动停止时(无论是把元素放到了有效的放置目标,还是放到了无效的放置目标上),会触发 dragend 事件。
上述三个事件的目标都是被拖动的元素。默认情况下,浏览器不会在拖动期间改变被拖动元素的外观,但你可以自己修改。不过,大多数浏览器会为正被拖动的元素创建一个半透明的副本,这个副本始终跟随着光标移动。
当某个元素被拖动到一个有效的放置目标上时,下列事件会依次发生:
(1) dragenter
(2) dragover
(3) dragleave 或 drop

只要有元素被拖动到放置目标上,就会触发 dragenter 事件(类似于 mouseover 事件)。紧随其后的是 dragover 事件,而且在被拖动的元素还在放置目标的范围内移动时,就会持续触发该事件。如果元素被拖出了放置目标,dragover 事件不再发生,但会触发 dragleave 事件(类似于 mouseout事件)。如果元素被放到了放置目标中,则会触发 drop 事件而不是 dragleave 事件。上述三个事件的目标都是作为放置目标的元素。

2.2 自定义放置目标

在拖动元素经过某些无效放置目标时,可以看到一种特殊的光标(圆环中有一条反斜线),表示不能放置。虽然所有元素都支持放置目标事件,但这些元素默认是不允许放置的。
如果拖动元素经过不允许放置的元素,无论用户如何操作,都不会发生 drop 事件。不过,你可以把任何元素变成有效的放置目标,方法是重写 dragenter 和 dragover 事件的默认行为。例如,假设有一个 ID 为"droptarget"的<div>元素,可以用如下代码将它变成一个放置目标。

var droptarget = document.getElementById("droptarget");
EventUtil.addHandler(droptarget, "dragover", function(event){
 EventUtil.preventDefault(event);
});
EventUtil.addHandler(droptarget, "dragenter", function(event){
 EventUtil.preventDefault(event);
}); 

在 Firefox 3.5+中,放置事件的默认行为是打开被放到放置目标上的 URL。换句话说,如果是把图像拖放到放置目标上,页面就会转向图像文件;而如果是把文本拖放到放置目标上,则会导致无效 URL错误。因此,为了让 Firefox 支持正常的拖放,还要取消 drop 事件的默认行为,阻止它打开 URL:

EventUtil.addHandler(droptarget, "drop", function(event){
 EventUtil.preventDefault(event);
}); 

2.3 dataTransfer对象

为了在拖放操作时实现数据交换,IE 5 引入了dataTransfer 对象,它是事件对象的一个属性,用于从被拖动元素向放置目标传递字符串格式的数据。因为它是事件对象的属性,所以只能在拖放事件的事件处理程序中访问 dataTransfer 对象。在事件处理程序中,可以使用这个对象的属性和方法来完善拖放功能。HTML5 规范草案也收入了dataTransfer 对象。
dataTransfer 对象有两个主要方法:getData()和 setData()。不难想象,getData()可以取得由 setData()保存的值。setData()方法的第一个参数,也是 getData()方法唯一的一个参数,是一个字符串,表示保存的数据类型,取值为"text"或"URL"。

//设置和接收文本数据
event.dataTransfer.setData("text", "some text");
var text = event.dataTransfer.getData("text");
//设置和接收 URL
event.dataTransfer.setData("URL", "http://www.wrox.com/");
var url = event.dataTransfer.getData("URL"); 

IE只定义了"text"和"URL"两种有效的数据类型,而HTML5则对此加以扩展,允许指定各种MIME类型。考虑到向后兼容,HTML5 也支持"text"和"URL",但这两种类型会被映射为"text/plain"和"text/uri-list"。
dataTransfer 对象可以为每种 MIME 类型都保存一个值。换句话说,同时在这个对象中保存一段文本和一个 URL 不会有任何问题。不过,保存在 dataTransfer 对象中的数据只能在 drop事件处理程序中读取。如果在 ondrop 处理程序中没有读到数据,那就是 dataTransfer 对象已经被销毁,数据也丢失了。
在拖动文本框中的文本时,浏览器会调用 setData()方法,将拖动的文本以"text"格式保存在dataTransfer 对象中。类似地,在拖放链接或图像时,会调用 setData()方法并保存 URL。然后,在这些元素被拖放到放置目标时,就可以通过 getData()读到这些数据。
也可以在 dragstart 事件处理程序中调用 setData(),手工保存自己要传输的数据,以便将来使用。
将数据保存为文本和保存为 URL 是有区别的。如果将数据保存为文本格式,那么数据不会得到任何特殊处理。而如果将数据保存为 URL,浏览器会将其当成网页中的链接。换句话说,如果你把它放置到另一个浏览器窗口中,浏览器就会打开该 URL。
Firefox 在其第 5 个版本之前不能正确地将 "url" 和 "text" 映射为 "text/uri-list" 和"text/plain"。但是却能把"Text"(T 大写)映射为"text/plain"。为了更好地在跨浏览器的情况下从 dataTransfer 对象取得数据,最好在取得 URL 数据时检测两个值,而在取得文本数据时使用"Text"。

var dataTransfer = event.dataTransfer;
//读取 URL
var url = dataTransfer.getData("url") ||dataTransfer.getData("text/uri-list");
//读取文本
var text = dataTransfer.getData("Text"); 

一定要把短数据类型放在前面,因为 IE 10 及之前的版本仍然不支持扩展的 MIME 类型名,而它们在遇到无法识别的数据类型时,会抛出错误。

caizhendi commented 4 years ago

2020.07.08

阅读进度: 16.3 p486

2.4 dropEffect与effectAllowed

利用 dataTransfer 对象,可不光是能够传输数据,还能通过它来确定被拖动的元素以及作为放置目标的元素能够接收什么操作。为此,需要访问 dataTransfer 对象的两个属性:dropEffect 和effectAllowed。
通过 dropEffect 属性可以知道被拖动的元素能够执行哪种放置行为。这个属性有下列 4个可能的值。

在把元素拖动到放置目标上时,以上每一个值都会导致光标显示为不同的符号。然而,要怎样实现光标所指示的动作完全取决于你。换句话说,如果你不介入,没有什么会自动地移动、复制,也不会打开链接。总之,浏览器只能帮你改变光标的样式,而其他的都要靠你自己来实现。要使用 dropEffect属性,必须在 ondragenter 事件处理程序中针对放置目标来设置它。
dropEffect 属性只有搭配 effectAllowed 属性才有用。effectAllowed 属性表示允许拖动元素的哪种 dropEffect,effectAllowed 属性可能的值如下。

必须在 ondragstart 事件处理程序中设置 effectAllowed 属性。
假设你想允许用户把文本框中的文本拖放到一个<div>元素中。首先,必须将 dropEffect 和effectAllowed 设置为"move"。但是,由于<div>元素的放置事件的默认行为是什么也不做,所以文本不可能自动移动。重写这个默认行为,就能从文本框中移走文本。然后你就可以自己编写代码将文本插入到<div>中,这样整个拖放操作就完成了。如果你将 dropEffect 和 effectAllowed 的值设置为"copy",那就不会自动移走文本框中的文本。

2.5 可拖动

默认情况下,图像、链接和文本是可以拖动的,也就是说,不用额外编写代码,用户就可以拖动它们。文本只有在被选中的情况下才能拖动,而图像和链接在任何时候都可以拖动。
让其他元素可以拖动也是可能的。HTML5 为所有 HTML 元素规定了一个 draggable 属性,表示元素是否可以拖动。图像和链接的 draggable 属性自动被设置成了 true,而其他元素这个属性的默认值都是 false。要想让其他元素可拖动,或者让图像或链接不能拖动,都可以设置这个属性。

<!-- 让这个图像不可以拖动 -->
<img src="smile.gif" draggable="false" alt="Smiley face">
<!-- 让这个元素可以拖动 -->
<div draggable="true">...</div> 

支持 draggable 属性的浏览器有 IE 10+、Firefox 4+、Safari 5+和 Chrome。Opera 11.5 及之前的版本都不支持 HTML5 的拖放功能。另外,为了让 Firefox 支持可拖动属性,还必须添加一个 ondragstart事件处理程序,并在 dataTransfer 对象中保存一些信息。

2.6 其他成员

HTML5 规范规定 dataTransfer 对象还应该包含下列方法和属性。

caizhendi commented 4 years ago

2020.07.09

阅读进度: 16.3.3 p488

3.媒体元素

HTML5 新增了两个与媒体相关的标签,让开发人员不必依赖任何插件就能在网页中嵌入跨浏览器的音频和视频内容。这两个标签就是<audio><video>
这两个标签除了能让开发人员方便地嵌入媒体文件之外,都提供了用于实现常用功能的 JavaScriptAPI,允许为媒体创建自定义的控件。这两个元素的用法如下。

<!-- 嵌入视频 -->
<video src="conference.mpg" id="myVideo">Video player not available.</video>
<!-- 嵌入音频 -->
<audio src="song.mp3" id="myAudio">Audio player not available.</audio> 

使用这两个元素时,至少要在标签中包含 src 属性,指向要加载的媒体文件。还可以设置 width和 height 属性以指定视频播放器的大小,而为 poster 属性指定图像的 URI 可以在加载视频内容期间显示一幅图像。另外,如果标签中有 controls 属性,则意味着浏览器应该显示 UI 控件,以便用户直接操作媒体。位于开始和结束标签之间的任何内容都将作为后备内容,在浏览器不支持这两个媒体元素的情况下显示。
因为并非所有浏览器都支持所有媒体格式,所以可以指定多个不同的媒体来源。为此,不用在标签中指定 src 属性,而是要像下面这样使用一或多个<source>元素。

<!-- 嵌入视频 -->
<video id="myVideo">
 <source src="conference.webm" type="video/webm; codecs='vp8, vorbis'">
 <source src="conference.ogv" type="video/ogg; codecs='theora, vorbis'">
 <source src="conference.mpg">
 Video player not available.
</video>
<!-- 嵌入音频 -->
<audio id="myAudio">
 <source src="song.ogg" type="audio/ogg">
 <source src="song.mp3" type="audio/mpeg">
 Audio player not available.
</audio>

不同的浏览器支持不同的编解码器,因此一般来说指定多种格式的媒体来源是必需的。支持这两个媒体元素的浏览器有IE9+、Firefox 3.5+、Safari 4+、Opera 10.5+、Chrome、iOS 版 Safari 和 Android 版 WebKit。

3.1 属性

<video><audio>元素都提供了完善的 JavaScript 接口。两个元素共有的属性见p487

3.2 事件

媒体元素相关的事件。见p488

caizhendi commented 4 years ago

2020.07.11

阅读进度: 17章 p493

3. 自定义媒体播放器

使用<audio><video>元素的 play()和 pause()方法,可以手工控制媒体文件的播放。组合使用属性、事件和这两个方法,很容易创建一个自定义的媒体播放器。

<div class="mediaplayer">
 <div class="video">
 <video id="player" src="movie.mov" poster="mymovie.jpg"
 width="300" height="200">
 Video player not available.
 </video>
 </div>
 <div class="controls">
 <input type="button" value="Play" id="video-btn">
 <span id="curtime">0</span>/<span id="duration">0</span>
 </div>
</div> 

以上基本的 HTML 再加上一些 JavaScript 就可以变成一个简单的视频播放器。

//取得元素的引用
var player = document.getElementById("player"),
 btn = document.getElementById("video-btn"),
 curtime = document.getElementById("curtime"),
 duration = document.getElementById("duration");
//更新播放时间
duration.innerHTML = player.duration;
//为按钮添加事件处理程序
EventUtil.addHandler(btn, "click", function(event){
 if (player.paused){
 player.play();
 btn.value = "Pause";
 } else {
 player.pause();
 btn.value = "Play";
 }
});
//定时更新当前时间
setInterval(function(){
 curtime.innerHTML = player.currentTime;
}, 250); 

4. 检测编解码器的支持情况

有一个 JavaScript API 能够检测浏览器是否支持某种格式和编解码器。
这两个媒体元素都有一个 canPlayType()方法,该方法接收一种格式/编解码器字符串,返回"probably"、"maybe"或""( 空字符串)。空字符串是假值,因此可以像下面这样在 if 语句中使用canPlayType():

if (audio.canPlayType("audio/mpeg")){
 //进一步处理
} 

而"probably"和"maybe"都是真值,因此在 if 语句的条件测试中可以转换成 true。
如果给 canPlayType()传入了一种 MIME 类型,则返回值很可能是"maybe"或空字符串。这是因为媒体文件本身只不过是音频或视频的一个容器,而真正决定文件能否播放的还是编码的格式。在同时传入 MIME 类型和编解码器的情况下,可能性就会增加,返回的字符串会变成"probably"。

var audio = document.getElementById("audio-player");
//很可能"maybe"
if (audio.canPlayType("audio/mpeg")){
 //进一步处理
}
//可能是"probably"
if (audio.canPlayType("audio/ogg; codecs=\"vorbis\"")){
 //进一步处理
}

编解码器必须用引号引起来才行。下表列出了已知的已得到支持的音频格式和编解码器。见p490
也可以使用 canPlayType()来检测视频格式。下表列出了已知的已得到支持的音频格式和编解码器。见p490

5. Audio类型

<audio>元素还有一个原生的 JavaScript 构造函数 Audio,可以在任何时候播放音频。从同为 DOM元素的角度看,Audio 与 Image 很相似,但 Audio 不用像 Image 那样必须插入到文档中。只要创建一个新实例,并传入音频源文件即可。

var audio = new Audio("sound.mp3");
EventUtil.addHandler(audio, "canplaythrough", function(event){
 audio.play();
}); 

在 iOS 中,调用 play()时会弹出一个对话框,得到用户的许可后才能播放声音。如果想在一段音频播放后再播放另一段音频,必须在 onfinish 事件处理程序中调用 play()方法。

4. 历史状态管理

HTML5 通过更新 history 对象为管理历史状态提供了方便。
通过 hashchange 事件,可以知道 URL 的参数什么时候发生了变化,即什么时候该有所反应。而通过状态管理 API ,能够在不加载新页面的情况下改变浏览器的 URL 。为此,需要使用history.pushState()方法,该方法可以接收三个参数:状态对象、新状态的标题和可选的相对 URL。

history.pushState({name:"Nicholas"}, "Nicholas' page", "nicholas.html"); 

执行 pushState()方法后,新的状态信息就会被加入历史状态栈,而浏览器地址栏也会变成新的相对 URL。但是,浏览器并不会真的向服务器发送请求,即使状态改变之后查询 location.href 也会返回与地址栏中相同的地址。另外,第二个参数目前还没有浏览器实现,因此完全可以只传入一个空字符串,或者一个短标题也可以。而第一个参数则应该尽可能提供初始化页面状态所需的各种信息。
因为 pushState()会创建新的历史状态,所以你会发现“后退”按钮也能使用了。按下“后退”按钮,会触发 window 对象的 popstate 事件①。popstate 事件的事件对象有一个 state 属性,这个属性就包含着当初以第一个参数传递给 pushState()的状态对象。

EventUtil.addHandler(window, "popstate", function(event){
 var state = event.state;
 if (state){ //第一个页面加载时 state 为空
 processState(state);
 }
});

得到这个状态对象后,必须把页面重置为状态对象中的数据表示的状态(因为浏览器不会自动为你做这些)。记住,浏览器加载的第一个页面没有状态,因此单击“后退”按钮返回浏览器加载的第一个页面时,event.state 值为 null。
要更新当前状态,可以调用 replaceState(),传入的参数与 pushState()的前两个参数相同。调用这个方法不会在历史状态栈中创建新状态,只会重写当前状态。

history.replaceState({name:"Greg"}, "Greg's page"); 

支持 HTML5 历史状态管理的浏览器有 Firefox 4+、Safari 5+、Opera 11.5+和 Chrome。在 Safari 和Chrome 中,传递给 pushState()或 replaceState()的状态对象中不能包含 DOM 元素。而 Firefox支持在状态对象中包含 DOM 元素。Opera 还支持一个 history.state 属性,它返回当前状态的状态对象。
在使用 HTML5 的状态管理机制时,请确保使用 pushState()创造的每一个“假”URL,在 Web 服务器上都有一个真的、实际存在的 URL 与之对应。否则,单击“刷新”按钮会导致 404 错误。

5. 小结

caizhendi commented 4 years ago

2020.07.13

阅读进度: 17.2 p499

错误处理与调试

ECMAScript 第 3 版致力于解决这个问题,专门引入了 try-catch 和 throw 语句以及一些错误类型,意在让开发人员能够适当地处理错误。几年之后,Web 浏览器中也出现了一些 JavaScript 调试程序和工具。2008 年以来,大多数 Web 浏览器都已经具备了一些调试 JavaScript 代码的能力。

1. 浏览器报告的错误

IE、Firefox、Safari、Chrome 和 Opera 等主流浏览器,都具有某种向用户报告 JavaScript 错误的机制。默认情况下,所有浏览器都会隐藏此类信息,毕竟除了开发人员之外,很少有人关心这些内容。因此,在基于浏览器编写 JavaScript 脚本时,别忘了启用浏览器的 JavaScript 报告功能,以便及时收到错误通知。

1.1 IE

IE 是唯一一个在浏览器的界面窗体(chrome)中显示 JavaScript 错误信息的浏览器。在发生 JavaScript错误时,浏览器左下角会出现一个黄色的图标,图标旁边则显示着"Error on page"(页面中有错误)。假如不是存心去看的话,你很可能不会注意这个图标。双击这个图标,就会看到一个包含错误消息的对话框,其中还包含诸如行号、字符数、错误代码及文件名(其实就是你在查看的页面的 URL)等相关信息。
可以通过设置让错误对话框一发生错误就显示出来。为此,要打开“Tools”(工具)菜单中的“Internet Options”(Internet 选项)对话框,切换到“Advanced”(高级)选项卡,选中“Display a notification about every script error”(显示每个脚本错误的通知)复选框(参见图 17-2)。单击“OK”(确定)按钮保存设置。
如果启用了脚本调试功能的话(默认是禁用的),那么在发生错误时,你不仅会显示错误通知,而且还会看到另一个对话框,询问是否想要调试错误。
要启用脚本调试功能,必须要在 IE 中安装某种脚本调试器。

1.2 Firefox

默认情况下,Firefox 在 JavaScript 发生错误时不会通过浏览器界面给出提示。但它会在后台将错误记录到错误控制台中。单击“Tools”(工具)菜单中的“Error Console”(错误控制台)可以显示错误控制台(见图 17-4)。你会发现,错误控制台中实际上还包含与 JavaScript、CSS 和 HTML 相关的警告和信息,可以通过筛选找到错误。
在发生 JavaScript 错误时,Firefox 会将其记录为一个错误,包括错误消息、引发错误的 URL 和错误所在的行号等信息。单击文件名即可以只读方式打开发生错误的脚本,发生错误的代码行会突出显示。
目前,最流行的 Firefox 插件 Firebug,已经成为开发人员必备的 JavaScript 纠错工具。
在 Firebug 中单击导致错误的代码行,将在一个新 Firebug 视图中打开整个脚本,该代码行在其中突出显示。

1.3 Safari

Windows 和 Mac OS 平台的 Safari 在默认情况下都会隐藏全部 JavaScript 错误。为了访问到这些信息,必须启用“Develop”(开发)菜单。为此,需要单击“Edit”(编辑)菜单中的“Preferences”(偏好设置),然后在“Advanced”(高级)选项卡中,选中“Show develop menu in menubar”(在菜单栏中显示“开发”菜单)。启用此项设置之后,就会在 Safari 的菜单栏中看到一个“Develop”菜单(参见图 17-6)。
“Develop”菜单中提供了一些与调试有关的选项,还有一些选项可以影响当前加载的页面。单击“Show Error Console”(显示错误控制台)选项,将会看到一组 JavaScript 及其他错误。控制台中显示着错误消息、错误的 URL 及错误的行号(参见图 17-7)。

1.4 Opera

Opera 在默认情况下也会隐藏 JavaScript 错误,所有错误都会被记录到错误控制台中。要打开错误控制台,需要单击“Tools”(工具)菜单,在“Advanced”(高级)子菜单项下面再单击“Error Console”(错误控制台)。与 Firefox 一样,Opera 的错误控制台中也包含了除 JavaScript 错误之外的很多来源(如HTML、CSS、XML、XSLT 等)的错误和警告信息。要分类查看不同来源的消息,可以使用左下角的下拉选择框(参见图 17-8)。
也可以让 Opera 一发生错误就弹出错误控制台。为此,要在“Tools”(工具)菜单中单击“Preferences”(首选项),再单击“Advanced”(高级)选项卡,然后从左侧菜单中选择“Content”(内容)。单击“JavaScripOptions”(JavaScript 选项)按钮,显示选项对话框(如图 17-9 所示)。

1.5 Chrome

与 Safari 和 Opera 一样,Chrome 在默认情况下也会隐藏 JavaScript 错误。所有错误都将被记录到Web Inspector 控制台中。要查看错误消息,必须打开 Web Inspector。为此,要单击位于地址栏右侧的“Control this page”(控制当前页)按钮,选择“Developer”(开发人员)、“JavaScript console”(JavaScript控制台),参见图 17-10。

caizhendi commented 4 years ago

2020.07.14

阅读进度: 17.2.2 p503

2. 错误处理

错误处理在程序设计中的重要性是勿庸置疑的。

2.1 try-catch语句

ECMA-262 第 3 版引入了 try-catch 语句,作为 JavaScript 中处理异常的一种标准方式。基本的语法如下所示,显而易见,这与 Java 中的 try-catch 语句是完全相同的。

try{
 // 可能会导致错误的代码
} catch(error){
 // 在错误发生时怎么处理
} 

也就是说,我们应该把所有可能会抛出错误的代码都放在 try 语句块中,而把那些用于错误处理的代码放在 catch 块中。

try {
 window.someNonexistentFunction();
} catch (error){
 alert("An error happened!");
} 

catch 块会接收到一个包含错误信息的对象。与在其他语言中不同的是,即使你不想使用这个错误对象,也要给它起个名字。这个对象中包含的实际信息会因浏览器而异,但共同的是有一个保存着错误消息的 message 属性。ECMA-262 还规定了一个保存错误类型的 name 属性;当前所有浏览器都支持这个属性(Opera 9 之前的版本不支持这个属性)。

try {
 window.someNonexistentFunction();
} catch (error){
 alert(error.message);
} 

这个 message 属性是唯一一个能够保证所有浏览器都支持的属性,除此之外,IE、Firefox、Safari、Chrome 以及 Opera 都为事件对象添加了其他相关信息。IE 添加了与 message 属性完全相同的 description 属性,还添加了保存着内部错误数量的 number 属性。Firefox 添加了 fileName、lineNumber 和 stack(包含栈跟踪信息)属性。Safari 添加了 line(表示行号)、sourceId(表示内部错误代码)和 sourceURL 属性。当然,在跨浏览器编程时,最好还是只使用 message 属性。

  1. finally 子句
    虽然在 try-catch 语句中是可选的,但 finally 子句一经使用,其代码无论如何都会执行。换句话说,try 语句块中的代码全部正常执行,finally 子句会执行;如果因为出错而执行了 catch 语句块,finally 子句照样还会执行。只要代码中包含 finally 子句,则无论 try 或 catch 语句块中包含什么代码——甚至 return 语句,都不会阻止 finally 子句的执行。
    function testFinally(){
    try {
    return 2;
    } catch (error){
    return 1;
    } finally {
    return 0;
    }
    } 

    表面上看,调用这个函数会返回 2,因为返回 2 的 return 语句位于 try 语句块中,而执行该语句又不会出错。可是,由于最后还有一个 finally 子句,结果就会导致该 return 语句被忽略;也就是说,调用这个函数只能返回 0。如果把 finally 子句拿掉,这个函数将返回 2。
    如果提供 finally 子句,则 catch 子句就成了可选的(catch 或 finally 有一个即可)。IE7 及更早版本中有一个 bug:除非有 catch 子句,否则 finally 中的代码永远不会执行。如果你仍然要考虑 IE 的早期版本,那就只好提供一个 catch 子句,哪怕里面什么都不写。IE8 修复了这个 bug。
    只要代码中包含finally 子句,那么无论try 还是catch 语句块中的return 语句都将被忽略。因此,在使用finally 子句之前,一定要非常清楚你想让代码怎么样。

  2. 错误类型
    每种错误都有对应的错误类型,而当错误发生时,就会抛出相应类型的错误对象。ECMA-262 定义了下列 7 种错误类型:
    • Error
    • EvalError
    • RangeError
    • ReferenceError
    • SyntaxError
    • TypeError
    • URIError

其中,Error 是基类型,其他错误类型都继承自该类型。因此,所有错误类型共享了一组相同的属性(错误对象中的方法全是默认的对象方法)。Error 类型的错误很少见,如果有也是浏览器抛出的;这个基类型的主要目的是供开发人员抛出自定义错误。
EvalError 类型的错误会在使用 eval()函数而发生异常时被抛出。ECMA-262 中对这个错误有如下描述:“如果以非直接调用的方式使用 eval 属性的值(换句话说,没有明确地将其名称作为一个Identifier,即用作 CallExpression 中的 MemberExpression),或者为 eval 属性赋值。”简单地说,如果没有把 eval()当成函数调用,就会抛出错误,例如:

new eval(); //抛出 EvalError
eval = foo; //抛出 EvalError 

在实践中,浏览器不一定会在应该抛出错误时就抛出 EvalError。例如,Firefox 4+和 IE8 对第一种情况会抛出 TypeError,而第二种情况会成功执行,不发生错误。有鉴于此,加上在实际开发中极少会这样使用 eval(),所以遇到这种错误类型的可能性极小。
RangeError 类型的错误会在数值超出相应范围时触发。例如,在定义数组时,如果指定了数组不支持的项数(如-20 或 Number.MAX_VALUE),就会触发这种错误。

var items1 = new Array(-20); //抛出 RangeError
var items2 = new Array(Number.MAX_VALUE); //抛出 RangeError 

在找不到对象的情况下,会发生 ReferenceError(这种情况下,会直接导致人所共知的"objectexpected"浏览器错误)。通常,在访问不存在的变量时,就会发生这种错误。

var obj = x; //在 x 并未声明的情况下抛出 ReferenceError 

至于 SyntaxError,当我们把语法错误的 JavaScript 字符串传入 eval()函数时,就会导致此类错误。

eval("a ++ b"); //抛出 SyntaxError 

TypeError 类型在 JavaScript 中会经常用到,在变量中保存着意外的类型时,或者在访问不存在的方法时,都会导致这种错误。错误的原因虽然多种多样,但归根结底还是由于在执行特定于类型的操作时,变量的类型并不符合要求所致。

var o = new 10; //抛出 TypeError
alert("name" in true); //抛出 TypeError
Function.prototype.toString.call("name"); //抛出 TypeError

最常发生类型错误的情况,就是传递给函数的参数事先未经检查,结果传入类型与预期类型不相符。
在使用 encodeURI()或 decodeURI(),而 URI 格式不正确时,就会导致 URIError 错误。这种错误也很少见,因为前面说的这两个函数的容错性非常高。
利用不同的错误类型,可以获悉更多有关异常的信息,从而有助于对错误作出恰当的处理。要想知道错误的类型,可以像下面这样在 try-catch 语句的 catch 语句中使用 instanceof 操作符。

try {
 someFunction();
} catch (error){
 if (error instanceof TypeError){
 //处理类型错误
 } else if (error instanceof ReferenceError){
 //处理引用错误
 } else {
 //处理其他类型的错误
 }
} 
  1. 合理使用 try-catch
    当 try-catch 语句中发生错误时,浏览器会认为错误已经被处理了,因而不会通过本章前面讨论的机制记录或报告错误。对于那些不要求用户懂技术,也不需要用户理解错误的 Web 应用程序,这应该说是个理想的结果。不过,try-catch 能够让我们实现自己的错误处理机制。
caizhendi commented 4 years ago

2020.07.15

阅读进度: 17.2.4 p506

2.2 抛出错误

与 try-catch 语句相配的还有一个 throw 操作符,用于随时抛出自定义错误。抛出错误时,必须要给 throw 操作符指定一个值,这个值是什么类型,没有要求。

throw 12345;
throw "Hello world!";
throw true;
throw { name: "JavaScript"}; 

在遇到 throw 操作符时,代码会立即停止执行。仅当有 try-catch 语句捕获到被抛出的值时,代码才会继续执行。
通过使用某种内置错误类型,可以更真实地模拟浏览器错误。每种错误类型的构造函数接收一个参数,即实际的错误消息。

throw new Error("Something bad happened."); 

这行代码抛出了一个通用错误,带有一条自定义错误消息。浏览器会像处理自己生成的错误一样,来处理这行代码抛出的错误。换句话说,浏览器会以常规方式报告这一错误,并且会显示这里的自定义错误消息。像下面使用其他错误类型,也可以模拟出类似的浏览器错误。

throw new SyntaxError("I don’t like your syntax.");
throw new TypeError("What type of variable do you take me for?");
throw new RangeError("Sorry, you just don’t have the range.");
throw new EvalError("That doesn’t evaluate.");
throw new URIError("Uri, is that you?");
throw new ReferenceError("You didn’t cite your references properly.");

在创建自定义错误消息时最常用的错误类型是 Error、RangeError、ReferenceError 和 TypeError。
利用原型链还可以通过继承 Error 来创建自定义错误类型(原型链在第 6 章中介绍)。此时,需要为新创建的错误类型指定 name 和 message 属性。

function CustomError(message){
 this.name = "CustomError";
 this.message = message;
} 
CustomError.prototype = new Error();
throw new CustomError("My message"); 

IE 只有在抛出 Error 对象的时候才会显示自定义错误消息。对于其他类型,它都无一例外地显示"exception thrown and not caught"(抛出了异常,且未被捕获)。

  1. 抛出错误的时机
    要针对函数为什么会执行失败给出更多信息,抛出自定义错误是一种很方便的方式。应该在出现某种特定的已知错误条件,导致函数无法正常执行时抛出错误。换句话说,浏览器会在某种特定的条件下执行函数时抛出错误。
    function process(values){
    values.sort();
    for (var i=0, len=values.length; i < len; i++){
    if (values[i] > 100){
    return values[i];
    }
    }
    return -1;
    } 

    如果执行这个函数时传给它一个字符串参数,那么对 sort()的调用就会失败。对此,不同浏览器会给出不同的错误消息,但都不是特别明确,如下所示。

    • IE:属性或方法不存在。
    • Firefox:values.sort()不是函数。
    • Safari:值 undefined(表达式 values.sort 的结果)不是对象。
    • Chrome:对象名没有方法'sort'。
    • Opera:类型不匹配(通常是在需要对象的地方使用了非对象值)。

在面对包含数千行 JavaScript 代码的复杂的 Web 应用程序时,要想查找错误来源就没有那么容易了。这种情况下,带有适当信息的自定义错误能够显著提升代码的可维护性。

function process(values){
 if (!(values instanceof Array)){
 throw new Error("process(): Argument must be an array.");
 }
 values.sort();

 for (var i=0, len=values.length; i < len; i++){
 if (values[i] > 100){
 return values[i];
 }
 }

 return -1;
} 

建议在开发 JavaScript 代码的过程中,重点关注函数和可能导致函数执行失败的因素。良好的错误处理机制应该可以确保代码中只发生你自己抛出的错误。

  1. 抛出错误与使用 try-catch
    抛出错误与捕获错误,我们认为只应该捕获那些你确切地知道该如何处理的错误。捕获错误的目的在于避免浏览器以默认方式处理它们;而抛出错误的目的在于提供错误发生具体原因的消息。

2.3 错误(error)事件

任何没有通过 try-catch 处理的错误都会触发 window 对象的 error 事件。这个事件是 Web 浏览器最早支持的事件之一,IE、Firefox 和 Chrome 为保持向后兼容,并没有对这个事件作任何修改(Opera和 Safari 不支持 error 事件)。
在任何 Web 浏览器中,onerror 事件处理程序都不会创建 event 对象,但它可以接收三个参数:错误消息、错误所在的 URL 和行号。多数情况下,只有错误消息有用,因为URL 只是给出了文档的位置,而行号所指的代码行既可能出自嵌入的 JavaScript 代码,也可能出自外部的文件。要指定 onerror 事件处理程序,必须使用如下所示的 DOM0 级技术,它没有遵循“DOM2 级事件”的标准格式。

window.onerror = function(message, url, line){
 alert(message);
}; 

只要发生错误,无论是不是浏览器生成的,都会触发 error 事件,并执行这个事件处理程序。然后,浏览器默认的机制发挥作用,像往常一样显示出错误消息。像下面这样在事件处理程序中返回false,可以阻止浏览器报告错误的默认行为。

window.onerror = function(message, url, line){
 alert(message);
 return false;
}; 

通过返回 false,这个函数实际上就充当了整个文档中的 try-catch 语句,可以捕获所有无代码处理的运行时错误。这个事件处理程序是避免浏览器报告错误的最后一道防线,理想情况下,只要可能就不应该使用它。只要能够适当地使用 try-catch 语句,就不会有错误交给浏览器,也就不会触发error 事件。
在 IE 中,即使发生 error事件,代码仍然会正常执行;所有变量和数据都将得到保留,因此能在 onerror 事件处理程序中访问它们。但在 Firefox 中,常规代码会停止执行,事件发生之前的所有变量和数据都将被销毁,因此几乎就无法判断错误了。
图像也支持 error 事件。只要图像的 src 特性中的 URL 不能返回可以被识别的图像格式,就会触发 error 事件。此时的 error 事件遵循 DOM 格式,会返回一个以图像为目标的 event 对象。

var image = new Image();
EventUtil.addHandler(image, "load", function(event){
 alert("Image loaded!");
});
EventUtil.addHandler(image, "error", function(event){
 alert("Image not loaded!");
});
image.src = "smilex.gif"; //指定不存在的文件
caizhendi commented 4 years ago

2020.07.17

阅读进度: 17.2.6 p510

2.4 处理错误的策略

在 Web 应用程序的 JavaScript 这一端,错误处理策略也同样重要。由于任何 JavaScript 错误都可能导致网页无法使用,因此搞清楚何时以及为什么发生错误至关重要。

2.5 常见的错误类型

错误处理的核心,是首先要知道代码里会发生什么错误。由于 JavaScript 是松散类型的,而且也不会验证函数的参数,因此错误只会在代码运行期间出现。一般来说,需要关注三种错误:

以上错误分别会在特定的模式下或者没有对值进行足够的检查的情况下发生。

  1. 类型转换错误
    类型转换错误发生在使用某个操作符,或者使用其他可能会自动转换值的数据类型的语言结构时。在使用相等(==)和不相等(!=)操作符,或者在 if、for 及 while 等流控制语句中使用非布尔值时,最常发生类型转换错误。
    JavaScript 中往往也会以相同方式错误地使用它们。多数情况下,我们建议使用全等(===)和不全等(!==)操作符,以避免类型转换。
    alert(5 == "5"); //true
    alert(5 === "5"); //false
    alert(1 == true); //true
    alert(1 === true); //false

    使用全等和非全等操作符,可以避免发生因为使用相等和不相等操作符引发的类型转换错误,因此我们强烈推荐使用。
    容易发生类型转换错误的另一个地方,就是流控制语句。像 if 之类的语句在确定下一步操作之前,会自动把任何值转换成布尔值。尤其是 if 语句,如果使用不当,最容易出错。

    function concat(str1, str2, str3){
    var result = str1 + str2;
    if (3){ //绝对不要这样!!!
    result += str3;
    }
    return result;
    } 

    未使用过的命名变量会自动被赋予 undefined 值。而 undefined 值可以被转换成布尔值 false,因此这个函数中的 if 语句实际上只适用于提供了第三个参数的情况。问题在于,并不是只有 undefined 才会被转换成 false,也不是只有字符串值才可以转换为 true。例如,假设第三个参数是数值 0,那么 if 语句的测试就会失败,而对数值 1 的测试则会通过。
    在流控制语句中使用非布尔值,是极为常见的一个错误来源。为避免此类错误,就要做到在条件比较时切实传入布尔值。实际上,执行某种形式的比较就可以达到这个目的。

    function concat(str1, str2, str3){
    var result = str1 + str2;
    if (typeof str3 == "string"){ //恰当的比较
    result += str3;
    }
    return result;
    } 
  2. 数据类型错误
    JavaScript 是松散类型的,也就是说,在使用变量和函数参数之前,不会对它们进行比较以确保它们的数据类型正确。为了保证不会发生数据类型错误,只能依靠开发人员编写适当的数据类型检测代码。在将预料之外的值传递给函数的情况下,最容易发生数据类型错误。
    在前面的例子中,通过检测第三个参数可以确保它是一个字符串,但是并没有检测另外两个参数。如果该函数必须要返回一个字符串,那么只要给它传入两个数值,忽略第三个参数,就可以轻易地导致它的执行结果错误。
    //不安全的函数,任何非字符串值都会导致错误
    function getQueryString(url){
    var pos = url.indexOf("?");
    if (pos > -1){
    return url.substring(pos +1);
    }
    return "";
    } 

    这个例子中的两个函数只能操作字符串,因此只要传入其他数据类型的值就会导致错误。而添加一条简单的类型检测语句,就可以确保函数不那么容易出错。

    function getQueryString(url){
    if (typeof url == "string"){ //通过检查类型确保安全
    var pos = url.indexOf("?");
    if (pos > -1){
    return url.substring(pos +1);
    }
    }
    return "";
    } 

    前一节提到过,在流控制语句中使用非布尔值作为条件很容易导致类型转换错误。同样,这样做也经常会导致数据类型错误。

    //不安全的函数,任何非数组值都会导致错误
    function reverseSort(values){
    if (values){ //绝对不要这样!!!
    values.sort();
    values.reverse();
    }
    } 

    这个 reverseSort()函数可以将数组反向排序,其中用到了 sort()和 reverse()方法。对于 if语句中的控制条件而言,任何会转换为 true 的非数组值都会导致错误。另一个常见的错误就是将参数与 null 值进行比较。

    //不安全的函数,任何非数组值都会导致错误
    function reverseSort(values){
    if (values != null){ //绝对不要这样!!!
    values.sort();
    values.reverse();
    }
    } 

    与 null 进行比较只能确保相应的值不是 null 和 undefined(这就相当于使用相等和不相等操作)。要确保传入的值有效,仅检测 null 值是不够的;因此,不应该使用这种技术。同样,我们也不推荐将某个值与 undefined 作比较。
    另一种错误的做法,就是只针对要使用的某一个特性执行特性检测。

    //还是不安全,任何非数组值都会导致错误
    function reverseSort(values){
    if (typeof values.sort == "function"){ //绝对不要这样!!!
    values.sort();
    values.reverse();
    }
    } 

    在确切知道应该传入什么类型的情况下,最好是使用 instanceof 来检测其数据类型,如下所示。

    //安全,非数组值将被忽略
    function reverseSort(values){
    if (values instanceof Array){ //问题解决了
    values.sort();
    values.reverse();
    }
    } 

    大体上来说,基本类型的值应该使用 typeof 来检测,而对象的值则应该使用 instanceof 来检测。根据使用函数的方式,有时候并不需要逐个检测所有参数的数据类型。但是,面向公众的 API 则必须无条件地执行类型检查,以确保函数始终能够正常地执行。

  3. 通信错误
    JavaScript 与服务器之间的任何一次通信,都有可能会产生错误。
    第一种通信错误与格式不正确的 URL 或发送的数据有关。最常见的问题是在将数据发送给服务器之前,没有使用 encodeURIComponent()对数据进行编码。例如,下面这个 URL 的格式就是不正确的:
    <http://www.yourdomain.com/?redir=http://www.someotherdomain.com?a=b&c=d >
    针对"redir="后面的所有字符串调用 encodeURIComponent()就可以解决这个问题,结果将产生如下字符串:
    <http://www.yourdomain.com/?redir=http%3A%2F%2Fwww.someotherdomain.com%3Fa%3Db%26c%3Dd >
    对于查询字符串,应该记住必须要使用 encodeURIComponent()方法。为了确保这一点,有时候可以定义一个处理查询字符串的函数,例如:
    function addQueryStringArg(url, name, value){
    if (url.indexOf("?") == -1){
    url += "?";
    } else {
    url += "&";
    }
    url += encodeURIComponent(name) + "=" + encodeURIComponent(value);
    return url;
    }

    这个函数接收三个参数:要追加查询字符串的 URL、参数名和参数值。如果传入的 URL 不包含问号,还要给它添加问号;否则,就要添加一个和号,因为有问号就意味着有其他查询字符串。然后,再将经过编码的查询字符串的名和值添加到 URL 后面。

    var url = "http://www.somedomain.com";
    var newUrl = addQueryStringArg(url, "redir",
    "http://www.someotherdomain.com?a=b&c=d");
    alert(newUrl);
caizhendi commented 4 years ago

2020.07.23

阅读进度: 17.3 p512

2.6 区分致命错误和非致命错误

任何错误处理策略中最重要的一个部分,就是确定错误是否致命。对于非致命错误,可以根据下列一或多个条件来确定:

致命错误,可以通过以下一或多个条件来确定:

要想采取适当的措施,必须要知道 JavaScript 在什么情况下会发生致命错误。在发生致命错误时,应该立即给用户发送一条消息,告诉他们无法再继续手头的事情了。假如必须刷新页面才能让应用程序正常运行,就必须通知用户,同时给用户提供一个点击即可刷新页面的按钮。
区分非致命错误和致命错误的主要依据,就是看它们对用户的影响。设计良好的代码,可以做到应用程序某一部分发生错误不会不必要地影响另一个实际上毫不相干的部分。
如果每个模块都需要通过 JavaScript调用来初始化,那么你可能会看到类似下面这样的代码:

for (var i=0, len=mods.length; i < len; i++){
 mods[i].init(); //可能会导致致命错误
} 

表面上看,这些代码没什么问题:依次对每个模块调用 init()方法。问题在于,任何模块的 init()方法如果出错,都会导致数组中后续的所有模块无法再进行初始化。从逻辑上说,这样编写代码没有什么意义。毕竟,每个模块相互之间没有依赖关系,各自实现不同功能。可能会导致致命错误的原因是代码的结构。不过,经过下面这样修改,就可以把所有模块的错误变成非致命的:

for (var i=0, len=mods.length; i < len; i++){
 try {
 mods[i].init();
 } catch (ex) {
 //在这里处理错误
 }
} 

2.7 把错误记录到服务器

开发 Web 应用程序过程中的一种常见的做法,就是集中保存错误日志,以便查找重要错误的原因。
要建立这样一种 JavaScript 错误记录系统,首先需要在服务器上创建一个页面(或者一个服务器入口点),用于处理错误数据。这个页面的作用无非就是从查询字符串中取得数据,然后再将数据写入错误日志中。这个页面可能会使用如下所示的函数:

function logError(sev, msg){
 var img = new Image();
 img.src = "log.php?sev=" + encodeURIComponent(sev) + "&msg=" +
 encodeURIComponent(msg);
} 

这个 logError()函数接收两个参数:表示严重程度的数值或字符串(视所用系统而异)及错误消息。其中,使用了 Image 对象来发送请求,这样做非常灵活,主要表现如下几方面。

只要是使用 try-catch 语句,就应该把相应错误记录到日志中。

for (var i=0, len=mods.length; i < len; i++){
 try {
 mods[i].init();
 } catch (ex){
 logError("nonfatal", "Module init failed: " + ex.message);
 }
} 

在这里,一旦模块初始化失败,就会调用 logError()。第一个参数是"nonfatal"(非致命),表示错误的严重程度。第二个参数是上下文信息加上真正的 JavaScript 错误消息。记录到服务器中的错误消息应该尽可能多地带有上下文信息,以便鉴别导致错误的真正原因。

hzjjg commented 4 years ago

牛逼

发自我的小米手机 在 caizhendi notifications@github.com,2020年7月23日 22:51写道:

2020.07.23

阅读进度: 17.3 p512

2.6 区分致命错误和非致命错误

任何错误处理策略中最重要的一个部分,就是确定错误是否致命。对于非致命错误,可以根据下列一或多个条件来确定:

致命错误,可以通过以下一或多个条件来确定:

要想采取适当的措施,必须要知道 JavaScript 在什么情况下会发生致命错误。在发生致命错误时,应该立即给用户发送一条消息,告诉他们无法再继续手头的事情了。假如必须刷新页面才能让应用程序正常运行,就必须通知用户,同时给用户提供一个点击即可刷新页面的按钮。 区分非致命错误和致命错误的主要依据,就是看它们对用户的影响。设计良好的代码,可以做到应用程序某一部分发生错误不会不必要地影响另一个实际上毫不相干的部分。 如果每个模块都需要通过 JavaScript调用来初始化,那么你可能会看到类似下面这样的代码:

for (var i=0, len=mods.length; i < len; i++){ mods[i].init(); //可能会导致致命错误 }

表面上看,这些代码没什么问题:依次对每个模块调用 init()方法。问题在于,任何模块的 init()方法如果出错,都会导致数组中后续的所有模块无法再进行初始化。从逻辑上说,这样编写代码没有什么意义。毕竟,每个模块相互之间没有依赖关系,各自实现不同功能。可能会导致致命错误的原因是代码的结构。不过,经过下面这样修改,就可以把所有模块的错误变成非致命的:

for (var i=0, len=mods.length; i < len; i++){ try { mods[i].init(); } catch (ex) { //在这里处理错误 } }

2.7 把错误记录到服务器

开发 Web 应用程序过程中的一种常见的做法,就是集中保存错误日志,以便查找重要错误的原因。 要建立这样一种 JavaScript 错误记录系统,首先需要在服务器上创建一个页面(或者一个服务器入口点),用于处理错误数据。这个页面的作用无非就是从查询字符串中取得数据,然后再将数据写入错误日志中。这个页面可能会使用如下所示的函数:

function logError(sev, msg){ var img = new Image(); img.src = "log.php?sev=" + encodeURIComponent(sev) + "&msg=" + encodeURIComponent(msg); }

这个 logError()函数接收两个参数:表示严重程度的数值或字符串(视所用系统而异)及错误消息。其中,使用了 Image 对象来发送请求,这样做非常灵活,主要表现如下几方面。

只要是使用 try-catch 语句,就应该把相应错误记录到日志中。

for (var i=0, len=mods.length; i < len; i++){ try { mods[i].init(); } catch (ex){ logError("nonfatal", "Module init failed: " + ex.message); } }

在这里,一旦模块初始化失败,就会调用 logError()。第一个参数是"nonfatal"(非致命),表示错误的严重程度。第二个参数是上下文信息加上真正的 JavaScript 错误消息。记录到服务器中的错误消息应该尽可能多地带有上下文信息,以便鉴别导致错误的真正原因。

— You are receiving this because you commented. Reply to this email directly, view it on GitHubhttps://nam02.safelinks.protection.outlook.com/?url=https%3A%2F%2Fgithub.com%2Fcaizhendi%2Fblog%2Fissues%2F3%23issuecomment-663052129&data=02%7C01%7C%7C78176c141cdd422c615908d82f17e2cd%7C84df9e7fe9f640afb435aaaaaaaaaaaa%7C1%7C0%7C637311126913474865&sdata=LToU6Oq5%2F%2B12SJyTNzUBVDLobnBZmmcTTjCMr%2BmWl4o%3D&reserved=0, or unsubscribehttps://nam02.safelinks.protection.outlook.com/?url=https%3A%2F%2Fgithub.com%2Fnotifications%2Funsubscribe-auth%2FADNAJT27W2NHCMSCITJMP6DR5BE7FANCNFSM4N6ENZUA&data=02%7C01%7C%7C78176c141cdd422c615908d82f17e2cd%7C84df9e7fe9f640afb435aaaaaaaaaaaa%7C1%7C0%7C637311126913484846&sdata=fcnkvAS6Tws0FklqQI%2FT9biUm7SCwufETxkBFmI3z3s%3D&reserved=0.

caizhendi commented 4 years ago

2020.07.24

阅读进度: 17.4 p516

3. 调试技术

最常见的做法就是在要调试的代码中随处插入 alert()函数。但这种做法一方面比较麻烦(调试之后还需要清理),另一方面还可能引入新问题(想象一下把某个 alert()函数遗留在产品代码中的结果)。

3.1 将消息记录到控制台

IE8、Firefox、Opera、Chrome 和 Safari 都有 JavaScript 控制台,可以用来查看 JavaScript 错误。而且,在这些浏览器中,都可以通过代码向控制台输出消息。对 Firefox 而言,需要安装 Firebug(www.getfirebug.com),因为 Firefox 要使用 Firebug 的控制台。对 IE8、Firefox、Chrome 和 Safari 来说,则可以通过 console 对象向 JavaScript 控制台中写入消息,这个对象具有下列方法。

在 IE8、Firebug、Chrome 和 Safari 中,用来记录消息的方法不同,控制台中显示的错误消息也不一样。错误消息带有红色图标,而警告消息带有黄色图标。以下函数展示了使用控制台输出消息的一个示例。

function sum(num1, num2){
 console.log("Entering sum(), arguments are " + num1 + "," + num2);
 console.log("Before calculation");
 var result = num1 + num2;
 console.log("After calculation");
 console.log("Exiting sum()");
 return result;
} 

在调用这个 sum()函数时,控制台中会出现一些消息,可以用来辅助调试。在 Safari 中,通过“Develop”(开发)菜单可以打开其 JavaScript 控制台(前面讨论过);在 Chrome 中,单击“Control thispage”(控制当前页)按钮并选择“Developer”(开发人员)和“JavaScript console”(JavaScript 控制台)即可;而在 Firefox 中,要打开控制台需要单击 Firefox 状态栏右下角的图标。IE8 的控制台是其 DeveloperTools(开发人员工具)扩展的一部分,通过“Tools”(工具)菜单可以找到,其控制台在“Script”(脚本)选项卡中。
Opera 10.5 之前的版本中,JavaScript 控制台可以通过 opera.postError()方法来访问。这个方法接受一个参数,即要写入到控制台中的参数,其用法如下。

function sum(num1, num2){
 opera.postError("Entering sum(), arguments are " + num1 + "," + num2);
 opera.postError("Before calculation");
 var result = num1 + num2;
 opera.postError("After calculation");
 opera.postError("Exiting sum()");
 return result;
} 

别看 opera.postError()方法的名字好像是只能输出错误,但实际上能通过它向 JavaScript 控制台中写入任何信息。
还有一种方案是使用 LiveConnect,也就是在 JavaScript 中运行 Java 代码。Firefox、Safari 和 Opera都支持 LiveConnect,因此可以操作 Java 控制台。例如,通过下列代码就可以在 JavaScript 中把消息写入到 Java 控制台。

java.lang.System.out.println("Your message"); 

可以用这行代码替代 console.log()或 opera.postError(),如下所示。

function sum(num1, num2){
 java.lang.System.out.println("Entering sum(), arguments are " + num1 + "," + num2);
 java.lang.System.out.println("Before calculation");
 var result = num1 + num2;
 java.lang.System.out.println("After calculation");
 java.lang.System.out.println("Exiting sum()");
 return result;
} 

如果系统设置恰当,可以在调用 LiveConnect 时就立即显示 Java 控制台。在 Firefox 中,通过“Tools”(工具)菜单可以打开 Java 控制台;在 Opera 中,要打开 Java 控制台,可以选择菜单“Tools”(工具)及“Advanced”(高级)。Safari 没有内置对 Java 控制台的支持,必须单独运行。
不存在一种跨浏览器向 JavaScript 控制台写入消息的机制,但下面的函数倒可以作为统一的接口。

function log(message){
 if (typeof console == "object"){
 console.log(message);
 } else if (typeof opera == "object"){
 opera.postError(message);
 } else if (typeof java == "object" && typeof java.lang == "object"){
 java.lang.System.out.println(message);
 }
} 

这个 log()函数检测了哪个 JavaScript 控制台接口可用,然后使用相应的接口。可以在任何浏览器中安全地使用这个函数,不会导致任何错误,例如:

function sum(num1, num2){
 log("Entering sum(), arguments are " + num1 + "," + num2);
 log("Before calculation");
 var result = num1 + num2;
 log("After calculation");
 log("Exiting sum()");
 return result;
} 

3.2 将消息记录到当前页面

另一种输出调试消息的方式,就是在页面中开辟一小块区域,用以显示消息。这个区域通常是一个元素,而该元素可以总是出现在页面中,但仅用于调试目的;也可以是一个根据需要动态创建的元素。例如,可以将 log()函数修改为如下所示:

function log(message){
 var console = document.getElementById("debuginfo");
 if (console === null){
 console = document.createElement("div");
 console.id = "debuginfo";
 console.style.background = "#dedede";
 console.style.border = "1px solid silver";
 console.style.padding = "5px";
 console.style.width = "400px";
 console.style.position = "absolute";
 console.style.right = "0px";
 console.style.top = "0px";
 document.body.appendChild(console);
 }
 console.innerHTML += "<p>" + message + "</p>";
} 

3.3 抛出错误

function divide(num1, num2){
 return num1 / num2;
} 

这个简单的函数计算两个数的除法,但如果有一个参数不是数值,它会返回 NaN。类似这样简单的计算如果返回 NaN,就会在 Web 应用程序中导致问题。对此,可以在计算之前,先检测每个参数是否都是数值。例如:

function divide(num1, num2){
 if (typeof num1 != "number" || typeof num2 != "number"){
 throw new Error("divide(): Both arguments must be numbers.");
 }
 return num1 / num2;
} 

对于大型应用程序来说,自定义的错误通常都使用 assert()函数抛出。这个函数接受两个参数,一个是求值结果应该为 true 的条件,另一个是条件为 false 时要抛出的错误。以下就是一个非常基本的 assert()函数。

function assert(condition, message){
 if (!condition){
 throw new Error(message);
 }
} 

可以用这个 assert()函数代替某些函数中需要调试的 if 语句,以便输出错误消息。下面是使用这个函数的例子。

function divide(num1, num2){
 assert(typeof num1 == "number" && typeof num2 == "number",
 "divide(): Both arguments must be numbers.");
 return num1 / num2;
} 
caizhendi commented 4 years ago

2020.07.27

阅读进度: 18章 p521

4. 常见的 IE 错误

作为用户最多的浏览器,如何看懂 IE 给出的错误也是最受关注的。

4.1 操作终止

在 IE8 之前的版本中,存在一个相对于其他浏览器而言,最令人迷惑、讨厌,也最难于调试的错误:操作终止(operation aborted)。在修改尚未加载完成的页面时,就会发生操作终止错误。发生错误时,会出现一个模态对话框,告诉你“操作终止。”单击确定(OK)按钮,则卸载整个页面,继而显示一张空白屏幕;此时要进行调试非常困难。下面的示例将会导致操作终止错误。

<!DOCTYPE html>
<html>
<head>
 <title>Operation Aborted Example</title>
</head>
<body>
 <p>The following code should cause an Operation Aborted error in IE versions
 prior to 8.</p> 
  <div>
 <script type="text/javascript">
 document.body.appendChild(document.createElement("div"));
 </script>
 </div>
</body>
</html> 

这个例子中存在的问题是:JavaScript 代码在页面尚未加载完毕时就要修改 document.body,而且<script>元素还不是<body>元素的直接子元素。准确一点说,当<script>节点被包含在某个元素中,而且 JavaScript 代码又要使用 appendChild()、innerHTML 或其他 DOM 方法修改该元素的父元素或祖先元素时,将会发生操作终止错误(因为只能修改已经加载完毕的元素)。
要避免这个问题,可以等到目标元素加载完毕后再对它进行操作,或者使用其他操作方法。例如,为 document.body 添加一个绝对定位在页面上的覆盖层,就是一种非常常见的操作。通常,开发人员都是使用 appendChild()方法来添加这个元素的,但换成使用 insertBefore()方法也很容易。

<!DOCTYPE html>
<html>
<head>
 <title>Operation Aborted Example</title>
</head>
<body>
 <p>The following code should not cause an Operation Aborted error in IE
 versions prior to 8.</p>
 <div>
 <script type="text/javascript">
 document.body.insertBefore(document.createElement("div"),
 document.body.firstChild);
 </script>
 </div>
</body>
</html> 

在这个例子中,新的<div>元素被添加到 document.body 的开头部分而不是末尾。因为完成这一操作所需的所有信息在脚本运行时都是已知的,所以这不会引发错误。
除了改变方法之外,还可以把<script>元素从包含元素中移出来,直接作为<body>的子元素。

<!DOCTYPE html>
<html>
<head>
 <title>Operation Aborted Example</title>
</head>
<body>
 <p>The following code should not cause an Operation Aborted error in IE
 versions prior to 8.</p>
 <div>
 </div>
 <script type="text/javascript">
 document.body.appendChild(document.createElement("div"));
 </script> 
 </body>
</html> 

在同样的情况下,IE8 不再抛出操作终止错误,而是抛出常规的 JavaScript 错误,带有如下错误消息:
HTML Parsing Error: Unable to modify the parent container element before the childelement is closed (KB927917).

4.2 无效字符

根据语法,JavaScript 文件必须只包含特定的字符。在 JavaScript 文件中存在无效字符时,IE 会抛出无效字符(invalid character)错误。所谓无效字符,就是 JavaScript 语法中未定义的字符。例如,有一个很像减号但却由 Unicode 值 8211 表示的字符(\u2013),就不能用作常规的减号(ASCII 编码为 45),因为 JavaScript 语法中没有定义该字符。这个字符通常是在 Word 文档中自动插入的。如果你的代码是从 Word 文档中复制到文本编辑器中,然后又在 IE 中运行的,那么就可能会遇到无效字符错误。其他浏览器对无效字符做出的反应与 IE 类似,Firefox 会抛出非法字符(illegal character)错误,Safari 会报告发生了语法错误,而 Opera 则会报告发生了 ReferenceError(引用错误),因为它会将无效字符解释为未定义的标识符。

4.3 未找到成员

IE 中的所有 DOM 对象都是以 COM 对象,而非原生 JavaScript 对象的形式实现的。这会导致一些与垃圾收集相关的非常奇怪的行为。IE 中的未找到成员(Member not found)错误,就是由于垃圾收集例程配合错误所直接导致的。
具体来说,如果在对象被销毁之后,又给该对象赋值,就会导致未找到成员错误。而导致这个错误的,一定是 COM 对象。发生这个错误的最常见情形是使用 event 对象的时候。IE 中的 event 对象是window 的属性,该对象在事件发生时创建,在最后一个事件处理程序执行完毕后销毁。假设你在一个闭包中使用了 event 对象,而该闭包不会立即执行,那么在将来调用它并给 event 的属性赋值时,就会导致未找到成员错误,如下面的例子所示。

document.onclick = function(){
 var event = window.event;
 setTimeout(function(){
 event.returnValue = false; //未找到成员错误
 }, 1000);
}; 

当单击事件处理程序执行完毕后,event 对象就会被销毁,因而闭包中引用对象的成员就成了不存在的了。由于不能在 COM 对象被销毁之后再给其成员赋值,在闭包中给 returnValue 赋值就会导致未找到成员错误。

4.4 未知运行时错误

当使用 innerHTML 或 outerHTML 以下列方式指定 HTML 时,就会发生未知运行时错误(Unknownruntime error):一是把块元素插入到行内元素时,二是访问表格任意部分(<table><tbody>等)的任意属性时。例如,从技术角度说,<span>标签不能包含<div>之类的块级元素,因此下面的代码就会导致未知运行时错误:

span.innerHTML = "<div>Hi</div>"; //这里,span 包含了<div>元素

4.5 语法错误

只要 IE 一报告发生了语法错误(syntax error),都可以很快找到错误的原因。这时候,原因可能是代码中少了一个分号,或者花括号前后不对应。然而,还有一种原因不十分明显的情况需要格外注意。
如果你引用了外部的 JavaScript 文件,而该文件最终并没有返回 JavaScript 代码,IE 也会抛出语法错误。例如,<script>元素的 src 特性指向了一个 HTML 文件,就会导致语法错误。报告语法错误的位置时,通常都会说该错误位于脚本第一行的第一个字符处。Opera 和 Safari 也会报告语法错误,但它们会给出导致问题的外部文件的信息;IE 就不会给出这个信息,因此就需要我们自己重复检查一遍引用的外部 JavaScript 文件。但 Firefox 会忽略那些被当作 JavaScript 内容嵌入到文档中的非 JavaScript 文件中的解析错误。

4.6 系统无法找到指定资源

系统无法找到指定资源(The system cannot locate the resource specified)这种说法,恐怕要算是 IE给出的最有价值的错误消息了。在使用 JavaScript 请求某个资源 URL,而该 URL 的长度超过了 IE 对 URL最长不能超过 2083 个字符的限制时,就会发生这个错误。IE 不仅限制 JavaScript 中使用的 URL 的长度,而且也限制用户在浏览器自身中使用的 URL 长度(其他浏览器对 URL 的限制没有这么严格)。IE 对 URL路径还有一个不能超过 2048 个字符的限制。

function createLongUrl(url){
 var s = "?";
 for (var i=0, len=2500; i < len; i++){
 s += "a";
 }
 return url + s;
}
var x = new XMLHttpRequest(); 
x.open("get", createLongUrl("http://www.somedomain.com/"), true);
x.send(null); 

在这个例子中,XMLHttpRequest 对象试图向一个超出最大长度限制的 URL 发送请求。在调用open()方法时,就会发生错误。避免这个问题的办法,无非就是通过给查询字符串参数起更短的名字,或者减少不必要的数据,来缩短查询字符串的长度。另外,还可以把请求方法改为 POST,通过请求体而不是查询字符串来发送数据。

5. 小结

下面是几种避免浏览器响应 JavaScript 错误的方法。

另外,对任何 Web 应用程序都应该分析可能的错误来源,并制定处理错误的方案。

IE、Firefox、Chrome、Opera 和 Safari 都有 JavaScript 调试器,有的是内置的,有的是以需要下载的扩展形式存在的。这些调试器都支持设置断点、控制代码执行及在运行时检测变量的值。

caizhendi commented 4 years ago

2020.07.28

阅读进度: 18.1.3 p523

18. JavaScript 与 XML

DOM 规范的制定,不仅是为了方便在 Web 浏览器中使用XML,也是为了在桌面及服务器应用程序中处理 XML 数据。此前,由于浏览器无法解析 XML 数据,很多开发人员都要动手编写自己的 XML 解析器。而自从 DOM 出现后,所有浏览器都内置了对 XML 的原生支持(XML DOM),同时也提供了一系列相关的技术支持。 作为用户最多的浏览器,如何看懂 IE 给出的错误也是最受关注的。

1. 浏览器对 XML DOM 的支持

在正式的规范诞生以前,浏览器提供商实现的 XML 解决方案不仅对 XML 的支持程度参差不齐,而且对同一特性的支持也各不相同。DOM2 级是第一个提到动态创建 XML DOM 概念的规范。DOM3级进一步增强了 XML DOM,新增了解析和序列化等特性。然而,当 DOM3 级规范的各项条款尘埃落定之后,大多数浏览器也都实现了各自不同的解决方案。

1.1 DOM2 级核心

DOM2级在document.implementation 中引入了createDocument()方法。IE9+、Firefox、Opera、Chrome 和 Safari 都支持这个方法。想一想,或许你还记得可以在支持 DOM2级的浏览器中使用以下语法来创建一个空白的 XML 文档:

var xmldom = document.implementation.createDocument(namespaceUri, root, doctype); 

在通过 JavaScript 处理 XML 时,通常只使用参数 root,因为这个参数指定的是 XML DOM 文档元素的标签名。而 namespaceUri 参数则很少用到,原因是在 JavaScrip 中管理命名空间比较困难。最后,doctype 参数用得就更少了。
要创建一个新的、文档元素为<root>的 XML 文档,可以使用如下代码:

var xmldom = document.implementation.createDocument("", "root", null);
alert(xmldom.documentElement.tagName); //"root"
var child = xmldom.createElement("child");
xmldom.documentElement.appendChild(child); 

这个例子创建了一个 XML DOM 文档,没有默认的命名空间,也没有文档类型。尽管不需要指定命名空间和文档类型,也必须传入相应的参数。具体来说,给命名空间 URI 传入一个空字符串,就意味着未指定命名空间,而给文档类型传入 null,就意味着不指定文档类型。变量 xmldom中保存着一个 DOM2 级 Document 类型的实例,带有第 12 章讨论过的所有 DOM 方法和属性。我们这个例子显示了文档元素的标签名,然后又创建并给文档元素添加了一个新的子元素。
要检测浏览器是否支持 DOM2 级 XML,可以使用下面这行代码:

var hasXmlDom = document.implementation.hasFeature("XML", "2.0"); 

1.2 DOMParser类型

为了将 XML 解析为 DOM 文档,Firefox 引入了 DOMParser 类型;后来,IE9、Safari、Chrome 和Opera 也支持了这个类型。在解析 XML 之前,首先必须创建一个 DOMParser 的实例,然后再调用parseFromString()方法。这个方法接受两个参数:要解析的 XML 字符串和内容类型(内容类型始终都应该是"text/xml")。返回的值是一个 Document 的实例。

var parser = new DOMParser();
var xmldom = parser.parseFromString("<root><child/></root>", "text/xml");
alert(xmldom.documentElement.tagName); //"root"
alert(xmldom.documentElement.firstChild.tagName); //"child"
var anotherChild = xmldom.createElement("child");
xmldom.documentElement.appendChild(anotherChild);
var children = xmldom.getElementsByTagName("child");
alert(children.length); //2 

DOMParser 只能解析格式良好的 XML,因而不能把 HTML 解析为 HTML 文档。在发生解析错误时,仍然会从 parseFromString()中返回一个 Document 对象,但这个对象的文档元素是<parsererror>,而文档元素的内容是对解析错误的描述。下面是一个例子。

<parsererror xmlns="http://www.mozilla.org/newlayout/xml/parsererror.xml">XML
Parsing Error: no element found Location: file:///I:/My%20Writing/My%20Books/
Professional%20JavaScript/Second%20Edition/Examples/Ch15/DOMParserExample2.htm Line
Number 1, Column 7: <sourcetext> & lt;root & gt; ------^</sourcetext > < /parsererror> 

Firefox和 Opera都会返回这种格式的文档。Safari和 Chrome 返回的文档也包含<parsererror>元素,但该元素会出现在发生解析错误的地方。IE9 会在调用 parseFromString()的地方抛出一个解析错误。由于存在这些差别,因此确定是否发生解析错误的最佳方式就是,使用一个 try-catch 语句块,如果没有错误,则通过 getElementsByTagName()来查找文档中是否存在<parsererror>元素,如下面的例子所示。

var parser = new DOMParser(),
 xmldom,
 errors;
try {
 xmldom = parser.parseFromString("<root>", "text/xml");
 errors = xmldom.getElementsByTagName("parsererror");
 if (errors.length > 0){
 throw new Error("Parsing error!");
 }
} catch (ex) {
 alert("Parsing error!");
} 

这例子显示,要解析的字符串中缺少了闭标签</root>,而这会导致解析错误。在 IE9+中,此时会抛出错误。在 Firefox 和 Opera 中,文档元素将是<parsererror>,而在 Safari 和 Chrome 中,<parsererror><root>的第一个子元素。调用 getElementsByTagName("parsererror")能够应对这两种情况。如果这个方法返回了元素,就说明有错误发生,继而通过一个警告框显示出来。当然,你还可以更进一步,从错误元素中提取出错误信息。

caizhendi commented 4 years ago

2020.07.30

阅读进度: 18.1.5 p527

1.3 XMLSerializer类型

在引入 DOMParser 的同时,Firefox 还引入了 XMLSerializer 类型,提供了相反的功能:将 DOM文档序列化为 XML 字符串。后来,IE9+、Opera、Chrome 和 Safari 都支持了 XMLSerializer。
要序列化 DOM 文档,首先必须创建 XMLSerializer 的实例,然后将文档传入其 serializeToString ()方法,如下面的例子所示。

var serializer = new XMLSerializer();
var xml = serializer.serializeToString(xmldom);
alert(xml); 

serializeToString()方法返回的字符串并不适合打印,因此看起来会显得乱糟糟的。XMLSerializer 可以序列化任何有效的 DOM 对象,不仅包括个别的节点,也包括 HTML 文档。将 HTML 文档传入 serializeToString()以后,HTML 文档将被视为 XML 文档,因此得到的代码也将是格式良好的。
如果将非 DOM 对象传入 serializeToString(),会导致错误发生。

1.4 IE8 及之前版本中的XML

IE 是第一个原生支持 XML 的浏览器,而这一支持是通过 ActiveX 对象实现的。为了便于桌面应用程序开发人员处理 XML,微软创建了 MSXML 库;但微软并没有针对 JavaScript 创建不同的对象,而只是让 Web 开发人员能够通过浏览器访问相同的对象。
要创建一个 XML 文档的实例,也要使用 ActiveXObject 构造函数并为其传入一个表示XML 文档版本的字符串。有 6 种不同的 XML 文档版本可以供选择。

微软只推荐使用 MSXML2.DOMDocument.6.0 或 MSXML2.DOMDocument.3.0;前者是最新最可靠的版本,而后者则是大多数 Windows 操作系统都支持的版本。可以作为后备版本的MSXML2.DOMDocument,仅在针对 IE5.5 之前的浏览器开发时才有必要使用。
通过尝试创建每个版本的实例并观察是否有错误发生,可以确定哪个版本可用。

function createDocument(){
 if (typeof arguments.callee.activeXString != "string"){
 var versions = ["MSXML2.DOMDocument.6.0", "MSXML2.DOMDocument.3.0",
 "MSXML2.DOMDocument"],
 i, len;
 for (i=0,len=versions.length; i < len; i++){
 try {
 new ActiveXObject(versions[i]);
 arguments.callee.activeXString = versions[i];
 break;
 } catch (ex){
 //跳过
 }
 }
 }
 return new ActiveXObject(arguments.callee.activeXString);
} 

要解析 XML 字符串,首先必须创建一个 DOM 文档,然后调用 loadXML()方法。新创建的 XML文档完全是一个空文档,因而不能对其执行任何操作。为 loadXML()方法传入的 XML 字符串经解析之后会被填充到 DOM 文档中。

var xmldom = createDocument();
xmldom.loadXML("<root><child/></root>");
alert(xmldom.documentElement.tagName); //"root"
alert(xmldom.documentElement.firstChild.tagName); //"child"
var anotherChild = xmldom.createElement("child");
xmldom.documentElement.appendChild(anotherChild);
var children = xmldom.getElementsByTagName("child");
alert(children.length); //2 

在新 DOM 文档中填充了 XML 内容之后,就可以像操作其他 DOM 文档一样操作它了(可以使用任何方法和属性)。
如果解析过程中出错,可以在 parseError 属性中找到错误消息。这个属性本身是一个包含多个属性的对象,每个属性都保存着有关解析错误的某一方面信息。

parseError 的 valueOf()方法返回 errorCode 的值,因此可以通过下列代码检测是否发生了解析错误。

if (xmldom.parseError != 0){
 alert("Parsing error occurred.");
} 

错误类型的数值编码可能是正值,也可能是负值,因此我们只需检测它是不是等于 0。要取得有关解析错误的详细信息也很容易,而且可以将这些信息组合起来给出更有价值的解释。

if (xmldom.parseError != 0){
 alert("An error occurred:\nError Code: "
 + xmldom.parseError.errorCode + "\n"
 + "Line: " + xmldom.parseError.line + "\n"
 + "Line Pos: " + xmldom.parseError.linepos + "\n"
 + "Reason: " + xmldom.parseError.reason);
} 

应该在调用 loadXML()之后、查询 XML 文档之前,检查是否发生了解析错误。

  1. 序列化 XML
    IE 将序列化 XML 的能力内置在了 DOM 文档中。每个 DOM 节点都有一个 xml 属性,其中保存着表示该节点的 XML 字符串。
    alert(xmldom.xml); 
  2. 加载 XML 文件
    IE 中的 XML 文档对象也可以加载来自服务器的文件。与 DOM3 级中的功能类似,要加载的 XML文档必须与页面中运行的 JavaScript 代码来自同一台服务器。同样与 DOM3 级规范类似,加载文档的方式也可以分为同步和异步两种。要指定加载文档的方式,可以设置 async 属性,true 表示异步,false表示同步(默认值为 true)。

    var xmldom = createDocument();
    xmldom.async = false; 

    在确定了加载 XML 文档的方式后,调用 load()可以启动下载过程。这个方法接受一个参数,即要加载的 XML 文件的 URL。在同步方式下,调用 load()后可以立即检测解析错误并执行相关的 XML处理

    var xmldom = createDocument();
    xmldom.async = false;
    xmldom.load("example.xml");
    if (xmldom.parseError != 0){
    //处理错误
    } else {
    alert(xmldom.documentElement.tagName); //"root"
    alert(xmldom.documentElement.firstChild.tagName); //"child"
    
    var anotherChild = xmldom.createElement("child");
    xmldom.documentElement.appendChild(anotherChild);
    
    var children = xmldom.getElementsByTagName("child");
    alert(children.length); //2
    
    alert(xmldom.xml);
    } 

    在异步加载 XML 文件的情况下,需要为 XML DOM 文档的 onreadystatechange 事件指定处理程序。有 4 个就绪状态(ready state)。

    • 1:DOM 正在加载数据。
    • 2:DOM 已经加载完数据。
    • 3:DOM 已经可以使用,但某些部分可能还无法访问。
    • 4:DOM 已经完全可以使用。

要关注的只有一个就绪状态:4。这个状态表示 XML 文件已经全部加载完毕,而且已经全部解析为 DOM 文档。通过 XML 文档的 readyState 属性可以取得其就绪状态。以异步方式加载 XML 文件的典型模式如下。

var xmldom = createDocument();
xmldom.async = true;
xmldom.onreadystatechange = function(){
 if (xmldom.readyState == 4){
 if (xmldom.parseError != 0){
 alert("An error occurred:\nError Code: "
 + xmldom.parseError.errorCode + "\n"
 + "Line: " + xmldom.parseError.line + "\n"
 + "Line Pos: " + xmldom.parseError.linepos + "\n"
 + "Reason: " + xmldom.parseError.reason);
 } else {
 alert(xmldom.documentElement.tagName); //"root"
 alert(xmldom.documentElement.firstChild.tagName); //"child"

 var anotherChild = xmldom.createElement("child");
 xmldom.documentElement.appendChild(anotherChild);

 var children = xmldom.getElementsByTagName("child");
 alert(children.length); //2

 alert(xmldom.xml);
 }
 }
};
xmldom.load("example.xml"); 

为 onreadystatechange 事件指定处理程序的语句,必须放在调用 load()方法的语句之前;这样,才能确保在就绪状态变化时调用该事件处理程序。另外,在事件处理程序内部,还必须注意要使用 XML 文档变量的名称(xmldom),不能使用 this 对象。原因是 ActiveX 控件为预防安全问题不允许使用 this 对象。当文档的就绪状态变化为 4 时,就可以放心地检测是否发生了解析错误,并在未发生错误的情况下处理 XML 了。
虽然可以通过XML DOM文档对象加载XML文件,但公认的还是使用XMLHttpRequest 对象比较好。

caizhendi commented 4 years ago

2020.08.10

阅读进度: 18.2 p529

1.5 跨浏览器处理XML

对解析 XML 而言,下面这个函数可以在所有四种主要浏览器中使用。

function parseXml(xml){
 var xmldom = null;
 if (typeof DOMParser != "undefined"){
 xmldom = (new DOMParser()).parseFromString(xml, "text/xml");  
 var errors = xmldom.getElementsByTagName("parsererror");
 if (errors.length){
 throw new Error("XML parsing error:" + errors[0].textContent);
 }
 } else if (typeof ActiveXObject != "undefined"){
 xmldom = createDocument();
 xmldom.loadXML(xml);
 if (xmldom.parseError != 0){
 throw new Error("XML parsing error: " + xmldom.parseError.reason);
 }
 } else {
 throw new Error("No XML parser available.");
 }
 return xmldom;
} 

这个 parseXml()函数只接收一个参数,即可解析的 XML 字符串。在函数内部,我们通过能力检测来确定要使用的 XML 解析方式。DOMParser 类型是受支持最多的解决方案,因此首先检测该类型是否有效。如果是,则创建一个新的 DOMParser 对象,并将解析 XML 字符串的结果保存在变量 xmldom中。由于 DOMParser 对象在发生解析错误时不抛出错误(除 IE9+之外),因此还要检测返回的文档以确定解析过程是否顺利。如果发现了解析错误,则根据错误消息抛出一个错误。
如果上述 XML 解析器都不可用,函数就会抛出一个错误,表示无法解析了。
在使用这个函数解析 XML 字符串时,应该将它放在 try-catch 语句当中,以防发生错误。

var xmldom = null;
try {
 xmldom = parseXml("<root><child/></root>");
} catch (ex){
 alert(ex.message);
}
//进一步处理

对序列化 XML 而言,也可以按照同样的方式编写一个能够在四大浏览器中运行的函数。

function serializeXml(xmldom){
 if (typeof XMLSerializer != "undefined"){
 return (new XMLSerializer()).serializeToString(xmldom);
 } else if (typeof xmldom.xml != "undefined"){ 
      return xmldom.xml;
 } else {
 throw new Error("Could not serialize XML DOM.");
 }
}

这个 serializeXml()函数接收一个参数,即要序列化的 XML DOM 文档。与 parseXml()函数一样,这个函数首先也是检测受到最广泛支持的特性,即 XMLSerializer。如果这个类型有效,则使用它来生成并返回文档的 XML 字符串。由于 ActiveX 方案比较简单,只使用了一个 xml 属性,因此这个函数直接检测了该属性。如果上述两方面尝试都失败了,函数就会抛出一个错误,说明序列化不能进行。一般来说,只要针对浏览器使用了适当的 XML DOM 对象,就不会出现无法序列化的情况,因而也就没有必要在 try-catch 语句中调用 serializeXml()。

var xml = serializeXml(xmldom); 

由于序列化过程的差异,相同的 DOM 对象在不同的浏览器下,有可能会得到不同的 XML字符串。

caizhendi commented 4 years ago

2020.08.11

阅读进度: 18.2.1.1 p531

2. 浏览器对 XPath 的支持

XPath 是设计用来在 DOM 文档中查找节点的一种手段,因而对 XML 处理也很重要。但是,DOM3级以前的标准并没有就 XPath 的 API 作出规定;XPath 是在 DOM3 级 XPath 模块中首次跻身推荐标准行列的。很多浏览器都实现了这个推荐标准,但 IE 则以自己的方式实现了 XPath。

2.1 DOM3 级XPath

DOM3级 XPath规范定义了在 DOM中对 XPath表达式求值的接口。要确定某浏览器是否支持 DOM3级 XPath,可以使用以下 JavaScript 代码:

var supportsXPath = document.implementation.hasFeature("XPath", "3.0"); 

在 DOM3 级 XPath 规范定义的类型中,最重要的两个类型是 XPathEvaluator 和 XPathResult。XPathEvaluator 用于在特定的上下文中对 XPath 表达式求值。这个类型有下列 3 个方法。

在 Firefox、Safari、Chrome 和 Opera 中,Document 类型通常都是与 XPathEvaluator 接口一起实现的。换句话说,在这些浏览器中,既可以创建 XPathEvaluator 的新实例,也可以使用 Document实例中的方法(XML 或 HTML 文档均是如此)。
在上面这三个方法中,evaluate()是最常用的。这个方法接收 5 个参数:XPath 表达式、上下文节点、命名空间求解器、返回结果的类型和保存结果的 XPathResult 对象(通常是 null,因为结果也会以函数值的形式返回)。其中,第三个参数(命名空间求解器)只在 XML 代码中使用了 XML 命名空间时有必要指定;如果 XML 代码中没有使用命名空间,则这个参数应该指定为 null。第四个参数(返回结果的类型)的取值范围是下列常量之一。

指定的结果类型决定了如何取得结果的值。

var result = xmldom.evaluate("employee/name", xmldom.documentElement, null,
 XPathResult.ORDERED_NODE_ITERATOR_TYPE, null);
if (result !== null) {
 var node = result.iterateNext();
 while(node) {
 alert(node.tagName);
 node = node.iterateNext();
 }
} 

这个例子中为返回结果指定的是 XPathResult.ORDERED_NODE_ITERATOR_TYPE,也是最常用的结果类型。如果没有节点匹配 XPath 表达式,evaluate()返回 null;否则,它会返回一个 XPathResult对象。这个 XPathResult 对象带有的属性和方法,可以用来取得特定类型的结果。如果节点是一个节点迭代器,无论是次序一致还是次序不一致的,都必须要使用 iterateNext()方法从节点中取得匹配的节点。在没有更多的匹配节点时,iterateNext()返回 null。
如果指定的是快照结果类型(不管是次序一致还是次序不一致的),就必须使用 snapshotItem()方法和 snapshotLength 属性。

var result = xmldom.evaluate("employee/name", xmldom.documentElement, null,
 XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
if (result !== null) {
 for (var i=0, len=result.snapshotLength; i < len; i++) {
 alert(result.snapshotItem(i).tagName);
 }
} 

这里,snapshotLength 返回的是快照中节点的数量,而 snapshotItem()则返回快照中给定位置的节点(与 NodeList 中的 length 和 item()相似)。

hzjjg commented 4 years ago

牛逼

发送自 Windows 10 版邮件https://go.microsoft.com/fwlink/?LinkId=550986应用

发件人: caizhendimailto:notifications@github.com 发送时间: 2020年8月11日 23:25 收件人: caizhendi/blogmailto:blog@noreply.github.com 抄送: hzjjgmailto:hzjio@outlook.com; Commentmailto:comment@noreply.github.com 主题: Re: [caizhendi/blog] 《javascript高级程序设计(第三版)》(3) (#3)

2020.08.11

阅读进度: 18.2.1.1 p531

  1. 浏览器对 XPath 的支持

XPath 是设计用来在 DOM 文档中查找节点的一种手段,因而对 XML 处理也很重要。但是,DOM3级以前的标准并没有就 XPath 的 API 作出规定;XPath 是在 DOM3 级 XPath 模块中首次跻身推荐标准行列的。很多浏览器都实现了这个推荐标准,但 IE 则以自己的方式实现了 XPath。

2.1 DOM3 级XPath

DOM3级 XPath规范定义了在 DOM中对 XPath表达式求值的接口。要确定某浏览器是否支持 DOM3级 XPath,可以使用以下 JavaScript 代码:

var supportsXPath = document.implementation.hasFeature("XPath", "3.0");

在 DOM3 级 XPath 规范定义的类型中,最重要的两个类型是 XPathEvaluator 和 XPathResult。XPathEvaluator 用于在特定的上下文中对 XPath 表达式求值。这个类型有下列 3 个方法。

在 Firefox、Safari、Chrome 和 Opera 中,Document 类型通常都是与 XPathEvaluator 接口一起实现的。换句话说,在这些浏览器中,既可以创建 XPathEvaluator 的新实例,也可以使用 Document实例中的方法(XML 或 HTML 文档均是如此)。 在上面这三个方法中,evaluate()是最常用的。这个方法接收 5 个参数:XPath 表达式、上下文节点、命名空间求解器、返回结果的类型和保存结果的 XPathResult 对象(通常是 null,因为结果也会以函数值的形式返回)。其中,第三个参数(命名空间求解器)只在 XML 代码中使用了 XML 命名空间时有必要指定;如果 XML 代码中没有使用命名空间,则这个参数应该指定为 null。第四个参数(返回结果的类型)的取值范围是下列常量之一。

指定的结果类型决定了如何取得结果的值。

var result = xmldom.evaluate("employee/name", xmldom.documentElement, null,

XPathResult.ORDERED_NODE_ITERATOR_TYPE, null);

if (result !== null) {

var node = result.iterateNext();

while(node) {

alert(node.tagName);

node = node.iterateNext();

}

}

这个例子中为返回结果指定的是 XPathResult.ORDERED_NODE_ITERATOR_TYPE,也是最常用的结果类型。如果没有节点匹配 XPath 表达式,evaluate()返回 null;否则,它会返回一个 XPathResult对象。这个 XPathResult 对象带有的属性和方法,可以用来取得特定类型的结果。如果节点是一个节点迭代器,无论是次序一致还是次序不一致的,都必须要使用 iterateNext()方法从节点中取得匹配的节点。在没有更多的匹配节点时,iterateNext()返回 null。 如果指定的是快照结果类型(不管是次序一致还是次序不一致的),就必须使用 snapshotItem()方法和 snapshotLength 属性。

var result = xmldom.evaluate("employee/name", xmldom.documentElement, null,

XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);

if (result !== null) {

for (var i=0, len=result.snapshotLength; i < len; i++) {

alert(result.snapshotItem(i).tagName);

}

}

这里,snapshotLength 返回的是快照中节点的数量,而 snapshotItem()则返回快照中给定位置的节点(与 NodeList 中的 length 和 item()相似)。

― You are receiving this because you commented. Reply to this email directly, view it on GitHubhttps://eur06.safelinks.protection.outlook.com/?url=https%3A%2F%2Fgithub.com%2Fcaizhendi%2Fblog%2Fissues%2F3%23issuecomment-672014619&data=02%7C01%7C%7C865d3bbfd2144022340908d83e0acc03%7C84df9e7fe9f640afb435aaaaaaaaaaaa%7C1%7C0%7C637327563370800312&sdata=TU%2FL4SPl450UKZTKP9Bg%2FqDzdIAf4p1trrZ%2BfYIMFEk%3D&reserved=0, or unsubscribehttps://eur06.safelinks.protection.outlook.com/?url=https%3A%2F%2Fgithub.com%2Fnotifications%2Funsubscribe-auth%2FADNAJTZJMRP3HNF6ISBESX3SAFPG7ANCNFSM4N6ENZUA&data=02%7C01%7C%7C865d3bbfd2144022340908d83e0acc03%7C84df9e7fe9f640afb435aaaaaaaaaaaa%7C1%7C0%7C637327563370800312&sdata=n3V1BMuNOjt5Jh%2BJALA6QaTGgY%2FW7ndIdUhIFBATJ80%3D&reserved=0.

caizhendi commented 4 years ago

2020.08.13

阅读进度: 18.2.2 p534

  1. 单节点结果

指定常量 XPathResult.FIRST_ORDERED_NODE_TYPE 会返回第一个匹配的节点,可以通过结果的 singleNodeValue 属性来访问该节点。

var result = xmldom.evaluate("employee/name", xmldom.documentElement, null,
 XPathResult.FIRST_ORDERED_NODE_TYPE, null);
if (result !== null) {
 alert(result.singleNodeValue.tagName);
} 

与前面的查询一样,在没有匹配节点的情况下,evaluate()返回 null。如果有节点返回,那么就可以通过 singleNodeValue 属性来访问它。

  1. 简单类型结果

通过 XPath 也可以取得简单的非节点数据类型,这时候就要使用 XPathResult 的布尔值、数值和字符串类型了。这几个结果类型分别会通过 booleanValue、numberValue 和 stringValue 属性返回一个值。对于布尔值类型,如果至少有一个节点与 XPath 表达式匹配,则求值结果返回 true,否则返回 false。

var result = xmldom.evaluate("employee/name", xmldom.documentElement, null,
 XPathResult.BOOLEAN_TYPE, null);
alert(result.booleanValue); 

在这个例子中,如果有节点匹配"employee/name",则 booleanValue 属性的值就是 true。
对于数值类型,必须在 XPath 表达式参数的位置上指定一个能够返回数值的 XPath 函数,例如计算与给定模式匹配的所有节点数量的 count()。

var result = xmldom.evaluate("count(employee/name)", xmldom.documentElement,
 null, XPathResult.NUMBER_TYPE, null);
alert(result.numberValue); 

以上代码会输出与"employee/name"匹配的节点数量(即 2)。如果使用这个方法的时候没有指定与前例类似的 XPath 函数,那么 numberValue 的值将等于 NaN。
对于字符串类型,evaluate()方法会查找与 XPath 表达式匹配的第一个节点,然后返回其第一个子节点的值(实际上是假设第一个子节点为文本节点)。如果没有匹配的节点,结果就是一个空字符串。

var result = xmldom.evaluate("employee/name", xmldom.documentElement, null,
 XPathResult.STRING_TYPE, null);
alert(result.stringValue); 

这个例子的输出结果中包含着与"element/name"匹配的第一个元素的第一个子节点中包含的字符串。

  1. 默认类型结果

所有 XPath 表达式都会自动映射到特定的结果类型。像前面那样设置特定的结果类型,可以限制表达式的输出。而使用 XPathResult.ANY_TYPE 常量可以自动确定返回结果的类型。一般来说,自动选择的结果类型可能是布尔值、数值、字符串值或一个次序不一致的节点迭代器。要确定返回的是什么结果类型,可以检测结果的 resultType 属性,如下面的例子所示。

var result = xmldom.evaluate("employee/name", xmldom.documentElement, null,
 XPathResult.ANY_TYPE, null);
if (result !== null) {
 switch(result.resultType) {
 case XPathResult.STRING_TYPE:
 //处理字符串类型
 break;
 case XPathResult.NUMBER_TYPE:
 //处理数值类型
 break;
 case XPathResult.BOOLEAN_TYPE:
 //处理布尔值类型
 break;
 case XPathResult.UNORDERED_NODE_ITERATOR_TYPE:
 //处理次序不一致的节点迭代器类型
 break;
 default:
 //处理其他可能的结果类型
 }
} 

XPathResult.ANY_TYPE 可以让我们更灵活地使用 XPath,但是却要求有更多的处理代码来处理返回的结果。

  1. 命名空间支持

对于利用了命名空间的 XML 文档,XPathEvaluator 必须知道命名空间信息,然后才能正确地进行求值。处理命名空间的方法也不止一种。

<?xml version="1.0" ?>
<wrox:books xmlns:wrox="http://www.wrox.com/">
 <wrox:book>
 <wrox:title>Professional JavaScript for Web Developers</wrox:title>
 <wrox:author>Nicholas C. Zakas</wrox:author>
 </wrox:book>
 <wrox:book>
 <wrox:title>Professional Ajax</wrox:title>
 <wrox:author>Nicholas C. Zakas</wrox:author>
 <wrox:author>Jeremy McPeak</wrox:author>
 <wrox:author>Joe Fawcett</wrox:author>
 </wrox:book>
</wrox:books> 

在这个 XML 文档中,所有元素定义都来自 http://www.wrox.com/命名空间,以前缀 wrox 标识。如果要对这个文档使用 XPath,就需要定义要使用的命名空间;否则求值将会失败。
处理命名空间的第一种方法是通过 createNSResolver()来创建 XPathNSResolver 对象。这个方法接受一个参数,即文档中包含命名空间定义的节点。对于前面的 XML 文档来说,这个节点就是文档元素<wrox:books>,它的 xmlns 特性定义了名空间。可以把这个节点传递给 createNSResolver(),然后可以像下面这样在 evaluate()中使用返回的结果。

var nsresolver = xmldom.createNSResolver(xmldom.documentElement);
var result = xmldom.evaluate("wrox:book/wrox:author",
 xmldom.documentElement, nsresolver,
 XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
alert(result.snapshotLength); 

在将 nsresolver 对象传入到 evaluate()之后,就可以确保它能够理解 XPath 表达式中使用的wrox 前缀。可以试一试使用相同的表达式,如果不使用 XPathNSResolver 的话,就会导致错误。
处理命名空间的第二种方法就是定义一个函数,让它接收一个命名空间前缀,返回关联的 URI

var nsresolver = function(prefix){
 switch(prefix){
 case "wrox": return "http://www.wrox.com/";
 //其他前缀
 }
};
var result = xmldom.evaluate("count(wrox:book/wrox:author)",
 xmldom.documentElement, nsresolver, XPathResult.NUMBER_TYPE, null);
alert(result.numberValue); 

在不确定文档中的哪个节点包含命名空间定义的情况下,这个命名空间解析函数就可以派上用场了。只要你知道前缀和 URI,就可以定义一个返回该信息的函数,然后将它作为第三个参数传递给evaluate()即可。