0%

JavaScript-学习笔记(8):动画效果

动画是 CSS-DOM 最具动感的内容,让网页上的元素动起来。

基础知识

js 可以按照预定的时间重复调用函数,,这意味着我们可以随着时间的失衡而不断改变某个元素的样式。

  • 位置:网页元素在浏览器窗口里的位置是一种表示性信息,一般由 CSS 负责。例如:

    1
    2
    3
    4
    5
    element {
    position: absolute;
    top: 50px;
    left: 100px;
    }

    下面的 DOM 代码实现了同样的效果:

    1
    2
    3
    element.style.position = "absolute";
    element.style.top = "50px";
    element.style.left = "100px";

    假设有这么一个元素:

    1
    <p id="message">Wheel</p>

    再用 js 设置位置:

    1
    2
    3
    4
    5
    6
    7
    8
    function positionMessage() {
    if (!document.getElementById) return false;
    if (!document.getElementById("message")) return false;
    var elem = document.getElementById("message");
    elem.style.position = "absolute";
    elem.style.left = "50px";
    elem.style.top = "100px";
    }

    添加到windows.onload

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    function addLoadEvent(func) {
    var oldonload = window.onload;
    if (typeof window.onload != "function") {
    window.onload = func;
    } else {
    window.onload = function () {
    oldonload();
    func();
    };
    }
    }
    addLoadEvent(positionMessage);

    结果:
    Image

    再编写一个移动的函数:

    1
    2
    3
    4
    5
    6
    function moveMessage() {
    if (!document.getElementById) return false;
    if (!document.getElementById("message")) return false;
    var elem = document.getElementById("message");
    elem.style.left = "200px";
    }

    结果:
    Image

    这是立即改变位置,要想获得动画效果,还要让位置随着时间的改变而不断改变。

  • 时间

    js 的 setTimeout 能够让某个经过一段预定的时间后才开始执行。
    用法:

    1
    variable = setTimeout("function", interval);

    可以用 clearTimeout 的函数来消 “等待执行”队列的某个函数。

    1
    clearTimeout(variable);

    修改 postitonMessage 函数,让其在 5 秒后才调用 moveMessage 函数。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    function positionMessage() {
    if (!document.getElementById) return false;
    if (!document.getElementById("message")) return false;
    var elem = document.getElementById("message");
    elem.style.position = "absolute";
    elem.style.left = "50px";
    elem.style.top = "100px";
    movement = setTimeout("moveMessage()", 5000);
    }

    positionMessage 依旧在加载时调用。

  • 时间递增量

    动画应该是一个渐变的过程,元素应该从出发点逐步移动到目的点,而不是一下子跳过去。

    更新 moveMessage 函数,让元素以逐渐移动的方式发生。步骤:

    • 获得元素的当前位置;
    • 如果元素已经到达目的地,则退出;
    • 如果元素尚未到达它的目的地,则把它向目的地移动;
    • 一段时间后重复上述步骤。

    获取位置的值:

    1
    2
    var xpos = parseInt(elem.style.left);
    var ypos = parseInt(elem.style.top);

    只有使用 DOM 脚本或 style 属性分配位置后,这里的 parseInt 才起作用。

    编辑 moveMessage 函数:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    function moveMessage() {
    if (!document.getElementById) return false;
    if (!document.getElementById("message")) return false;
    var elem = document.getElementById("message");
    var xpos = parseInt(elem.style.left);
    var ypos = parseInt(elem.style.top);
    // 如果相等就退出递归
    if (xpos == 200 && ypos == 100) return true;
    // 如果不相等就继续加减
    if (xpos < 200) xpos++;
    if (xpos > 200) xpos--;
    if (ypos < 100) ypos++;
    if (ypos > 100) ypos--;
    elem.style.left = xpos + "px";
    elem.style.top = ypos + "px";
    // 每隔10毫秒递归
    movement = setTimeout("moveMessage()", 10);
    }
  • 抽象

    上面的代码只能完成特定的函数,如果把里面的常数改为变量,那么就通用性就会大大提升。

    将移动元素的 ID、元素的目的地的位置,以及停顿间隔改为变量。
    定义一个新的函数 function moveElement(elementID, final_x, final_y, interval),具体代码为。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    function moveElement(elementID, final_x, final_y, interval) {
    if (!document.getElementById) return false;
    if (!document.getElementById(elementID)) return false;
    var elem = document.getElementById(elementID);

    var xpos = parseInt(elem.style.left);
    var ypos = parseInt(elem.style.top);
    // 如果相等就退出递归
    if (xpos == final_x && ypos == final_y) return true;
    // 如果不相等就继续加减
    if (xpos < final_x) xpos++;
    if (xpos > final_x) xpos--;
    if (ypos < final_y) ypos++;
    if (ypos > final_y) ypos--;
    elem.style.left = xpos + "px";
    elem.style.top = ypos + "px";
    // 每隔 interval 毫秒递归
    var reqpeat =
    "moveElement('" +
    elementID +
    "'," +
    final_x +
    "," +
    final_y +
    "," +
    interval +
    ")";
    movement = setTimeout(reqpeat, interval);
    }

    使用 moveElement 函数。
    创建一个名为 message.html 的文件,包含一个 id=message 的 p 元素:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8" />
    <title>Position</title>
    </head>

    <body>
    <p id="message">Wheel</p>
    </body>
    </html>

    再编写一个 positionMessage.js 的文件,添加 positionMessage() 函数:
    id=message2 的 p 元素,

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    function positionMessage() {
    if (!document.getElementById) return false;
    if (!document.getElementById("message")) return false;
    var elem = document.getElementById("message");
    elem.style.position = "absolute";
    elem.style.left = "50px";
    elem.style.top = "100px";
    moveElement("message", 200, 100, 10);
    }

    addLoadEvent(positionMessage);

    再在 HTML 文件上添加相对应的 js 文件,就可以显示出简单的动画效果。
    改变 interval 的值即可改变动画的速度,改变 x 和 y 的值即可改变最终位置。

    添加一个 id=message2 的 p 元素,<p id="message2">Whoal</p>,再在 positionMessage 函数中添加相对应的操作:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    function positionMessage() {
    if (!document.getElementById) return false;
    if (!document.getElementById("message")) return false;
    var elem = document.getElementById("message");
    elem.style.position = "absolute";
    elem.style.left = "50px";
    elem.style.top = "100px";

    var elem_2 = document.getElementById("message2");
    elem_2.style.position = "absolute";
    elem_2.style.left = "50px";
    elem_2.style.top = "50px";

    moveElement("message", 125, 25, 10);
    moveElement("message2", 125, 125, 20);
    }

    结果:
    Image

实用的动画

上面的动画用处不大,还有可能使用户厌烦。

  • 提出问题:有一个怨念一系列链接的网页。当用用户的鼠标指针悬停在某个链接上时,展示预览一张图片。
    添加一个文件 list.html,代码为:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8" />
    <title>Web Design</title>
    </head>

    <body>
    <h1>Web Design</h1>
    <p>These are the things you should know:</p>
    <ol id="linklist">
    <li>
    <a href="./structure.html">structure</a>
    </li>
    <li>
    <a href="./presentation.html">presentation</a>
    </li>
    <li>
    <a href="./behavior.html">behavior</a>
    </li>
    </ol>
    </body>
    </html>

    这次需要在 onmouseover 事件被触发时显示一张图片。

  • 解决问题:

    • 为所有的预览图片生成一张“集体照”形式的图片;
    • 隐藏这张集体照图片的绝大部分。
    • 当用户把鼠标指针悬停在某个链接上方时,只显示这张“集体照”图片相应的部分。

    集体照:
    Image
    添加到 list.html 最后面:

    1
    <img src="./topic.png" alt="building blocks of web Design" id="preview" />
  • CSS

    css 的 overflow 属性用来处理一个元素的尺寸超出其容器尺寸的情况。
    overflow 属性有 4 种取值:visible(不作任何改动)、hidden(将溢出部分隐藏)、scroll(隐藏,但提供一个滚动条)和 auto(类似 scroll,但只有溢出时才显示滚动条)。

    这里选取 hidden 属性。

    添加 div,将 img 包进去,再添加对应的样式:

    1
    2
    3
    <div id="slideshow">
    <img src="./topic.png" alt="building blocks of web Design" id="preview" />
    </div>
    1
    2
    3
    4
    5
    6
    #slideshow {
    width: 130px;
    height: 100px;
    position: relative;
    overflow: hidden;
    }

    接下来解决的时,让网页对用户的操作做出正确响应,当鼠标指针指在某个地方时,把对应的图片显示出来。

  • JavaScript

    编写一个 prepareSlideshow 来完成工作。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    function prepareSlideshow() {
    if (!document.getElementsByTagName) return false;
    if (!document.getElementById) return false;

    if (!document.getElementById("linklist")) return false;
    if (!document.getElementById("preview")) return false;

    // 为图片应用样式
    var preview = document.getElementById("preview");
    preview.style.position = "absolute";
    preview.style.left = "0px";
    preview.style.top = "0px";

    var list = document.getElementById("linklist");
    var links = list.getElementsByTagName("a");

    links[0].onmouseover = function () {
    moveElement("preview", -100, 0, 10);
    };

    links[1].onmouseover = function () {
    moveElement("preview", -200, 0, 10);
    };

    links[2].onmouseover = function () {
    moveElement("preview", -300, 0, 10);
    };
    }

    addLoadEvent(prepareSlideshow);

    将 js 文件添加到 html 文件,运行后将鼠标移到对应的链接上会出来动画效果:
    Image

    但是来回移动时会出现混乱的情况。

  • 变量的作用域

    动画效果出错是由全局变量引起的。

    1
    movement = setTimeout(reqpeat, interval);

    但是直接使用局部变量也会出错,需要一种介乎二者之间的东西,也就是属性。
    js 允许为元素创建属性:

    1
    element.property = value;

    所以更改为属性即可解决问题:

    1
    elem.movement = setTimeout(reqpeat, interval);
  • 改进动画效果:
    每次移动 1px,速度未免过慢。
    可以设置每次前进这个距离的十分之一。

    1
    2
    dist = (final_x - xpos) / 10;
    xpos = xpos + dist;

    当差距小于 10 的时候,用这个差距除 10 的结果小于 1,将移动不了。
    解决方法是使用 Math 对象的 ceil 函数,它可以返回一个不小于 dist 的值的一个整数,用法:

    1
    Math.ceil(number);

    最终代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    function moveElement(elementID, final_x, final_y, interval) {
    if (!document.getElementById) return false;
    if (!document.getElementById(elementID)) return false;
    var elem = document.getElementById(elementID);

    // 声明 dist
    var dist = 0;
    var xpos = parseInt(elem.style.left);
    var ypos = parseInt(elem.style.top);
    // 如果相等就退出递归
    if (xpos == final_x && ypos == final_y) return true;
    // 如果不相等就继续加减
    if (xpos < final_x) {
    dist = Math.ceil((final_x - xpos) / 10);
    xpos = xpos + dist;
    }
    if (xpos > final_x) {
    dist = Math.ceil((xpos - final_x) / 10);
    xpos = xpos - dist;
    }
    if (ypos < final_y) {
    dist = Math.ceil((final_y - ypos) / 10);
    ypos = ypos + dist;
    }
    if (ypos > final_y) {
    dist = Math.ceil((ypos - final_y) / 10);
    ypos = ypos - dist;
    }
    elem.style.left = xpos + "px";
    elem.style.top = ypos + "px";
    // 每隔 interval 毫秒递归
    var reqpeat =
    "moveElement('" +
    elementID +
    "'," +
    final_x +
    "," +
    final_y +
    "," +
    interval +
    ")";
    // movement = setTimeout(reqpeat, interval);
    elem.movement = setTimeout(reqpeat, interval);
    }
  • 添加安全检查
    诸如 elem.style.left 是否存在等。

  • 生成 HTML 标记:
    一些内容是为了存在动画效果而存在的,但是如果不支持 js,那么那些内容留在那里岂不是很尴尬,所以干脆让 js 来生成这些标记。

小结

实现动画效果并不难,问题是尖不应该使用动画。
动画技术可以让我们创建出很多非常酷的效果,但四处移动的元素用处不大。现在有一个通用性的函数,在有必要创建动画效果时帮上大忙。

2020 年,主要就是使用下面这些扩展。

1. 去广告插件——uBlock Origin

一般使用浏览器都需要一个去广告插件,否则浏览网页的体验太差了。
这个插件比 Adblock 占用资源低,而且知名度也更低一点,没有那么多网站进行针对。不过一般网站要针对去广告插件的话也不会单针对某一款,除了某乎头上的那个显示。

地址

2. epub 阅读扩展——QiuReader

在电脑上阅读 epub 文件的体验一直不算好,直到我找到了这款扩展。

清爽的首页:
Image
舒服的界面:
Image

配置选项:背景颜色、字体大小、阅读方式、空格等;
其它功能:添加书签、高亮某一段文字。
美中不足的是不能添加书注,不过也无伤大雅,单是这漂亮的 UI 就足够了。

我单方面宣布这是 window 上最好用 epub 阅读器。

地址

3. 美化网页——Stylus

官方网站上,由于种种限制,导致网站有很多广告和很多大多数人一辈子都用不上的功能,并且 UI 总是有点丑。这时就需要一个美化网页的插件了,把多余的东西去掉,留一个简洁干净的页面。一般有点流量的网站都有很人编写相对应的样式。
贴吧使用前:
Image
使用后:
Image

页面干净了许多。

地址

4. 脚本扩展——暴力猴

脚本扩展在某种程度上也算是必装的一款插件了吧,已经有很多人在 Greasy Fork 等网站写了大量的脚本,有需要就下载安装即可。脚本可以解决相当多的问题,并且也不会占用很多内存。找脚本也很方便,直接点击相应的图标,即跳出一个“为此站点查找脚本”的选项。
Image
其它常见的脚本插件有油猴等,不过我还是更喜欢暴力猴,或许是更好看点吧。

地址

5. 密码管理——LastPass

跨平台的的密码保存管理扩展,有了它,再也不用担心忘记密码了。
该扩展的主要功能是,输入一个新的账号密码时,会提示是否保存;若账号相同,则提示是否更新密码;会在账号密码自动填充最后使用的密码。
其它的功能还有生成随机密码之类的。

地址

6. 下载管理——IDM

这个插件要搭配 IDM 下载器使用。该插件几乎可以下载所有的网页上的视频、图像等。
具体写过一篇文章专门介绍如何配置使用 IDM的。

地址

7. 操作网页——viumium

不需要用鼠标,仅使用键盘能完成 90%的操作。
有很多类似的扩展,为什么推荐这个,只是使用习惯了。

按键功能和 vim 有一定的相似之处。
上、下、左、右的移动分别是:k, j, h, l;

向右切换标签页:g+t 或者 K;

向左切换标签页:g+T 或者 J;

跳到顶部:gg;

跳到底部:G;

向上、下翻半页分别是:d, u;

网页前进:L;

页面后退:H;

显示点击链接:f;

新建标签页显示点击链接:F;

搜索:o;

新建标签页搜索:O;

新建空白标签页:t;

新建空白标签页打开链接:T;

关闭标签页:x;

打开上一个关闭的标签页:X;

焦点移到输入框:g+i;

焦点从输入框移走:tab 或者 esc;

值得注意的一点是,上述按键功能只能在常见的网页上生效,在系统标签页是用不了的。比如空白标签页、系统设置页,因此学一些默认的快捷键是有必要的,比如 ctrl + t 是新建空白标签页、ctrl + tab 是向右切换标签页、ctrl + shift + tab 是向左切换,一般加上个 shift 就是相反的功能。

还有一些按键就不一一赘述了。

使用该插件能解放你的鼠标,极大地提高阅读网页浏览体验。

地址

8. 网盘助手——网盘助手

支持主流网盘,如百度网盘以及蓝奏云等。
可以自动提取 提取码 。
Image

大概是最优雅好用的网盘助手了。
有使用网盘的都可以试试安装。

地址

基本就这些比较实用的扩展。

9. 切换浏览器头——User-Agent Switcher and Manager

当你需要访问移动版网页或者只有其它浏览器才能访问的网站时,可以用它切换相对应的浏览器头。

地址

笔记内容:

  • styele 属性
  • 检索样式
  • 改变样式

网页的结构

  • 三位一体:

    • 结构层:由 HTML 或 XHTML 之类的标记语言负责创建;
    • 表示层:由 CSS 负责完成,CSS 描述页面内容应该如何呈现;
    • 行为层:负责内容应该如何响应事件这一问题,js 和 DOM 主宰的领域。
  • 分离:

    • 使用(X)HTML 搭建文档的结构;
    • 使用 CSS 去设置文档的呈现效果;
    • 使用 DOM 脚本去实现文档的行为。

    这三种技术之间存在重叠区域。

style

style 属性 包含元素的样式,查询该属性返蜀犬吠日的是一个对象而不是一个简单的字符串。样式存在对象的属性里:

1
element.style.property;
  • 获取样式

    1
    2
    3
    4
    // 获取 style 的 color 属性:
    element.style.color;
    // 获取 style 的 font-family 属性:
    elements.style.fontFamily;

    一般样式属性的返回值与它们的设置值都采用同样的计量单位。如果果 font-size 以 em 为单位,那么返回的值也是以 em 为单位。

    style 属性只能能过内嵌的方式来获取,一旦样式在外部,则获取不到。

  • 设置样式
    style 对象的名个属性是可以读写的,可以使用赋值操作来更新样式:

    1
    element.style.property = value;

    赋值操作符可以设置任何一种样式,比如 font 之类的速记属性:

    1
    para.style.font = "2em 'times', serif";

DOM 脚本设置样式

虽然 DOM 设置样式很容易,但是在大多数场合下,应该使用 CSS 去声明样式。

  • 根据元素的位置设置样式
    找到该元素后,直接给该元素的 style 对应的属性赋值即可。

  • 根据条件反复设置样式
    找到元素后,根据条件设置样式。比如表格里隔一个行设置一个背景色,就需要根据条件设置值。

  • 响应事件
    一般最好使用 CSS 为文档设置样式,但也有一些 CSS 不能处理或者难以部署的情况,这时需要用上 DOM。比如浏览器不能识别伪类的时候,可以使用 js 来解决。

  • className 属性
    之前的都是用 DOM 直接设置或修改样式,这是直接让“行为层”干“表示层”的活。
    与其让 DOM 直接改变元素的样式,不如能过 js 更新元素的 classic 属性。

对函数进行抽象

一些函数可以做一些小小的改动可以变得更通用,这个过程叫抽象(abstraction)。
比如下面这个函数的对象仅适用于 h1 元素,以及 classname 的值为 intro 。

1
2
3
4
5
6
7
8
9
10
function styleHeaderSiblings() {
if (!document.getElementsByTagName) return false;
var headers = document.getElementsByTagName("h1");
var elem;
for (var i = 0; i < headers.length; i++) {
// 查找下一个元素节点
elem = getNextElement(headers[i].nextSibling);
elem.className += "intro";
}
}

添加两个具体的参数,就可以使其成为更通用的函数:添加 tag 和 theclass 两个参数,并把 h1 改为 tag,intro 改为 theclass。为了增加可读性,把 headers 改为 elems.

1
2
3
4
5
6
7
8
9
10
function styleHeaderSiblings(tag, theclass) {
if (!document.getElementsByTagName) return false;
var elems = document.getElementsByTagName(tag);
alert(theclass);
for (var i = 0; i < elems.length; i++) {
// 查找下一个元素节点
elem = getNextElement(elems[i].nextSibling);
elem.className += " " + theclass;
}
}

这样,函数的抽象化就完成了。
无论何时,发现函数可以抽象,都应该马上去做,这总是一个好主意。

小结

CSS-DOM 针对的是如何得到和设置 style 对象的各种属性。
style 属性不支持外部 css 设置的样式,不过仍然可以利用 style 属性去改变 html 元素的呈现效果。
不过一般都应该选择更新 classname 属性,而不是直接更新 style 对象的相关属性。
使用 js 干 css 的活无非就俩原因,一是 css 无法干这种活,二是一些残疾浏览器不支持最新属性。js 能够重复执行一组操作,这是 css 做不了的。

网页的结构由标记语言创建,js 可用来改变细节而不改变结构。
同样,js 也可用来改变网页的结构和内容。

传统方法

  1. document.write
    write方法可以把字符串插入到文档里。
    使用方法,编辑 test.html 文件:

    1
    2
    3
    4
    5
    <body>
    <script>
    document.write("<p> 这是一个段落 </p>");
    </script>
    </body>

    结果:
    Image

    缺点:这样做会使得标记与 js 代码混杂在一起,代码即不容易理解,也无法享受代码与结构分离的好处。应避免<body>乱用<script>标签,尽量减少使用document.write

  2. innerHTML 属性
    innerHTML 属性可以用来读写给定元素里的 HTML 内容。
    读,编辑 test.html 文件:

    1
    2
    3
    <div id="test">
    <p>This is <em>my</em> content!</p>
    </div>

    然后编写 example.js (记得把 example.js 文件加到 HTML 文件里面):

    1
    2
    3
    4
    window.onload = function () {
    var test = document.getElementById("test");
    alert(test.innerHTML);
    };

    结果:
    Image
    结果显示,innerHTML 只是粗暴地把元素里面所有的代码找出来。当需要把插入大段的 HTML 文件时,它就有用武之地了。

    写:

    1
    2
    3
    4
    window.onload = function () {
    var test = document.getElementById("test");
    test.innerHTML = "<p> Insert in <em>innerHTML</em> </p>";
    };

    结果:
    Image
    直接赋值对应的即可,赋值后会把原来的 html 代码覆盖掉。
    这是 HTML 专有属性,不能用于其它标记语言文档,如 xhtml 文档。

DOM 方法

  1. 添加节点
    编辑 test.html,让id=test的 div 标签内容为空。

    1
    <div id="test"></div>

    把一段文本插入到上面的 div 元素,用 DOM 的话来形容,添加一个 p 元素节点,再把这个节点作为 div 元素的子节点。
    具体步骤:

    1. createElement 创建一个新元素:
      语法:
      1
      document.createElement(nodeName);
      创造一个 p 元素结点并赋值给一个变量:
      1
      var para = document.createElement("p");
    2. appendChild 把新元素插入节点数:
      使用 appendChild 方法。
      语法:
      1
      parent.appendChild(child);
      例子:
      1
      2
      3
      var test = document.getElementById("test");
      var para = document.createElement("p");
      test.appendChild(para);
      或者:
      1
      document.getElementById("test").appendChild(document.createElement("p"));
  2. 添加文本节点
    使用 crateTextNode 方法。
    语法与 createElement 相似:

    1
    document.createTextNode(text);

    创建一个内容为 “test”的文本节点并赋值变量。

    1
    var txt = document.createTextNode("test");

    使用 appendChild 添加到 p 元素结点:

    1
    para.appendChild(txt);

    最终代码:

    1
    2
    3
    4
    5
    6
    7
    window.onload = function () {
    var test = document.getElementById("test");
    var para = document.createElement("p");
    test.appendChild(para);
    var txt = document.createTextNode("test");
    para.appendChild(txt);
    };

    结果:
    Image

  3. 一个复杂的例子:
    创建一个以下节点:

    1
    <p>This is <em>my</em> content.</p>

    这个 html 包含一个 p 元素节点,p 元素里又包含一个文本节点“This is”,一个 em 结点和一个文本节点“contents.”。
    那么只要依次创建加入即可。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    window.onload = function () {
    var test = document.getElementById("test");
    var para = document.createElement("p");
    test.appendChild(para);
    // 添加第一个文本节点
    var txt_0 = document.createTextNode("This is ");
    para.appendChild(txt_0);
    // 添加em节点
    var em = document.createElement("em");
    var em_txt = document.createTextNode("my");
    em.appendChild(em_txt);
    para.appendChild(em);
    // 添加第三个节点
    var text_1 = document.createTextNode(" content.");
    para.appendChild(text_1);
    };

    结果:
    Image

图片库

里面只有一个图片和一段文字是为show_pic服务,这很些元素的存在是为了让 DOM 处理它们,那么使用 DOM 来创建它们是最合适的。

  1. 先把文档里的相关元素删掉。

  2. 先编写一个函数 prepare_placeholder 并把它放进 show_pic.js文件,然后在文档加载时调用此函数。

  3. 函数的任务:

    1. 创建一个 img 元素节点;
    2. 设置这个节点的 id 属性;
    3. 设置这个节点的 src 属性;
    4. 设置这个节点的 alt 属性;
    5. 创建一个 p 元素节点;
    6. 设置这个节点的 id 属性;
    7. 创建一个文本节点;
    8. 把这个文本节点追加到 p 元素上;
    9. 把 p 元素和 img 元素插入到 gallery.html 文档里。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // 创建img元素
    var placeholder = document.createElement("img");
    placeholder.setAttribute("id", "default");
    placeholder.setAttribute("src", "images/default.jpg");
    placeholder.setAttribute("alt", "my image gallery");

    // 创建p元素
    var description = document.createElement("p");
    description.setAttribute("id", "description");
    var txt = document.createTextNode("choose an image");
    description.appendChild(txt);

    // 插入到文档中
    var body_element = document.getElementsByTagName("body")[0];
    body_element.appendChild(placeholder);
    body_element.appendChild(description);

    上述代码有一个完成工作了,但是一切都是依赖于这些元素刚好在 body 标签的末尾。如果在中间的话,就需要用到其它方法了。

  4. 已有元素前插入元素
    DOM 提供了名为 insertBefore() 方法。使用此方法时,必须告诉它三件事:

    1. 新元素:你想插入的元素;
    2. 目标元素:你想反把这个元素插入到哪个元素之前;
    3. 父元素:目标元素的父元素。

    语法:

    1
    parentElement.insertBefore(newElement, targetElement);

    例子:

    1
    2
    var gallery = document.getElementById("image_gallery");
    gallery.parentElement.insertBefore(placeholder, gallery);

    这样就把元素插入到图片元素之前了。

  5. 元素后插入元素
    DOM 没有提供这个方法,但是可以自己写一个 insertAtfer 方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    function insertAfter(newElement, targetElement) {
    // 获取父元素
    var parent = targetElement.parentNode;
    // 如果最后一个结点元素与目标结点相同
    if (parent.lastChild == targetElement) {
    // 直接加到末尾
    parent.appendChild(newElement);
    } else {
    // 找到目标结点的下一个结点,再插入到结点前
    parent.insertBefore(newElement, targetElement.nextSibling);
    }
    }

    使用方法和insertAfter一样。

    1
    parentElement.insertAfter(newElement, targetElement);

    还需要测试浏览器是否支持,所以最终代码清单为:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    function prepare_placeholder() {
    if (!document.createElement) return false;
    if (!document.createTextNode) return false;
    if (!document.getElementById) return false;
    if (!document.getElementById("image_gallery")) return false;

    // 创建img元素
    var placeholder = document.createElement("img");
    placeholder.setAttribute("id", "default");
    placeholder.setAttribute("src", "images/default.jpg");
    placeholder.setAttribute("alt", "my image gallery");

    // 创建p元素
    var description = document.createElement("p");
    description.setAttribute("id", "description");
    var txt = document.createTextNode("choose an image");
    description.appendChild(txt);

    var gallery = document.getElementById("image_gallery");
    insertAfter(placeholder, gallery);
    insertAfter(description, placeholder);
    }

    目前为止,创建的这些新内容对这个页面来说并不算是新的,比如,页面加载后,标记中就已经存在 title 属性了。通过createElement添加的新段落也是基于嵌入在脚本中的标记添加的,实际上,我们创建的所有一切都包含在初始页面。

那么怎么才能真正得到原来不存在初始页面的内容呢?

Ajax

使用 Ajax 就可以做到只更新页面中的一小部分,其它内容都不需要重新加载。
Ajax 的主要优势是对页面的请求以异步方式发送到服务器。

  1. XMLHttpRequest 对象
    将下面的代码放进 ajax.html。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19

    <!DOCTYPE html>
    <html lang="en">

    <head>
    <meta charset="UTF-8">
    <title>Ajax</title>
    </head>

    <body>
    <div id="new">
    </div>

    <script src="./scripts/addLoadEvent.js"></script>
    <script src="./scripts/getHTTPObject.js"></script>
    <script src="./scripts/getNewContent.js"></script>
    </body>

    </html>

    其中 addLoadEvent.js、getHTTPObject.js 和 getNewContent.js 都位于 scripts 的文件夹中。
    为了模拟服务器响应,在 ajax.html 同目录下创建一个 example.txt 文件,包含以下内容:

    1
    This was loaded as asynchronously!

    该文件充当服务器的输出。

    为了兼容不同 IE 版本的浏览器,需要写很多兼容代码,不过 IE 早就应该扫进历史的垃圾堆里了,所以就不兼容了。
    创建一个 XMLHttpRequest 对象:

    1
    2
    3
    4
    function getHTTPObject() {
    var request = new XMLHttpRequest();
    return request;
    }

    使用:

    1
    var request = getHTTPObject();

    XMLHttpRequest 对象有许多方法,其中最有用的是 open 方法,它用来指定服务器上将要访问的文件,指定请求的类型:GET、POST 或 SEND。该方法第三个参数用于指定请求是否以异步方式发送和处理。

    在 getNewContent.js 文件中添加以下代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    function getNewContent() {
    var request = getHTTPObject();
    if (request) {
    // 发起请求同一目录下的文件
    request.open("GET", "example.txt", true);
    request.onreadystatechange = function () {
    if (request.readyState === 4) {
    // 取出并保存数据
    var para = document.createElement("p");
    var txt = document.createTextNode(request.responseText);
    para.appendChild(txt);
    document.getElementById("new").appendChild(para);
    }
    };
    // 发送请求
    request.send(null);
    } else {
    alert("Sorry, your browser doesn't support XMLHttpRequest");
    }
    }

    request.readyState 有 5 个可能的值:

    • 0 表示初始化
    • 1 表示正在加载
    • 2 表示加载完毕
    • 3 表示正在交互
    • 4 表示完成

    只要 readyState 属性的值变成了 4,就可以访问服务器发送回来的数据。
    responseText 是保存文本字符串的数据,responseXML 属性是保存 Content-Type 头部中指定为“text/xml”的数据,一个是 DocumentFragment 对象。

    结果:
    Image

    异步请求有一个异步性的问题,脚本发送 XMLHttpRequest 请求之后,仍会继续执行,不会等待响应返回。
    可以试着加两个警告框,在函数结尾和 onreadystateChange 后添加。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    function getNewContent() {
    var request = getHTTPObject();
    if (request) {
    // 发起请求同一目录下的文件
    request.open("GET", "example.txt", true);
    request.onreadystatechange = function () {
    if (request.readyState == 4) {
    alert("respose received");
    var para = document.createElement("p");
    var txt = document.createTextNode(request.responseText);
    para.appendChild(txt);
    document.getElementById("new").appendChild(para);
    }
    };
    // 发送请求
    request.send(null);
    } else {
    alert("Sorry, your browser doesn't support XMLHttpRequest");
    }
    alert("function done");
    }

    结果显示在函数结尾处的弹窗更快。

  2. Hijax,渐进增强地使用 Ajax。
    登录表单添加 Ajax 功能,需要拦截提交表单的请求(Hijax),让 XMLHttpRequest 请求代为发送,再取消 onsubmit 的默认请求。这样登录就更方便,用户感觉就更快了。

小结

本次学习了几种不同的向浏览器里的文档动态添加标记的方法。
传统:

  • document.write 方法
  • innerHTML 属性

DOM 方法:

  • createElement 方法
  • createTextNode 方法
  • appendChild 方法
  • insertBefore 方法

用 createElement 和 createTextNode 创造的只是孤儿,要用 appendChild 和 insertBefore 才能把这些孤儿插入到文档的节点树,呈现在浏览器的窗口。

对图片库的改进和 insertAfter 方法的创建,还有 Ajax 和 异步请求。

审视一下之前的图片库,思考一下:

支持平稳退化?

  1. 当 js 被禁用后,用户点击链接时会自动调动原来 a 标签的方法,同样可以查看所有的图片,网页的基本功能没有受到损害。

  2. 使用伪协仪的话就没戏了。

js 与 html 标记分离吗?

  1. 不分离,onclick事件直接插在标记文档里。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    <li>
    <a
    onclick="show_pic(this); return false;"
    href="images/dog.jpg"
    title="a dog east something"
    >
    dog
    </a>
    </li>
  2. 为了让浏览器知道与哪些链接有着不一样的行为,可以添加一个class属性:

    1
    2
    3
    4
    5
    <li>
    <a href="images/dog.jpg" class="gallerypic" title="a dog east something">
    dog
    </a>
    </li>

    但是这种方法太麻烦了,可以直接给整个清单设置一个 ID。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    <ul id="image_gallery">
    <li>
    <a href="images/dog.jpg" title="a dog east something"> dog </a>
    </li>
    <li>
    <a href="images/follower.jpg" title="a follower ">follower</a>
    </li>
    <li>
    <a href="images/part.jpg" title="a part here">part</a>
    </li>
    <li>
    <a href="images/sea.jpg" title="the sea ">sea</a>
    </li>
    <li>
    <a href="images/star.jpg" title="water star">star</a>
    </li>
    </ul>

    只有一个“挂钩”,但是也足够了。

  3. 添加事件处理函数
    具体操作:

    • 检查是否支持getElementsByTagNamegetElementById

    • 检查是否存在有id=image_gallery的元素;

    • 遍历image_gallery的所有链接;

    • 设置 onclick 事件,让其关联操作:

      • 将这个链接作为参数传递给show_pic函数;
      • 取消链接被点击时的默认行为,不让浏览打开该链接。

    具体代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    function prepare_gallery() {
    if (!(document.getElementsByTagName || document.getElementById))
    return false;
    var gallery = document.getElementById("image_gallery");
    if (!gallery) return false;
    links = gallery.getElementsByTagName("a");
    for (var i = 0; i < links.length; i++) {
    links[i].onclick = function () {
    // 把整个link元素传进去
    show_pic(this);
    return false;
    };
    }
    }
  4. 共享 onload
    当有多个函数需要 onload 时使用,那么可以创建一个匿名函数来绑定指令。

    1
    2
    3
    4
    window.onload = function () {
    first_function();
    second_function();
    };

    这是最简单的处理方式,还有一个弹性最佳的解决方案。这个方案需要写一点代码,但一不旦有了那些代码,把函数绑定到window.onload事件就行了。
    这个函数名是addLoadEvent,要完成的操作有:

    • 把现在的window.load事件处理函数的值存入变量oldonload
    • 如果处理函数还没有绑定函数,就像平时那样把新函数添加给它。
    • 如果已经绑定了函数,就把新函数追加到现有指令的末尾。
      具体代码:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      function addLoadEvent(func) {
      var oldonload = window.onload;
      if (typeof window.onload != "function") {
      window.onload = func;
      } else {
      window.onload = function () {
      oldonload();
      func();
      };
      }
      }
      如果需要把函数加到队列里去,只需写以下代码:
      1
      2
      addLoadEvent(first_function);
      addLoadEvent(second_function);

不要做太多的假设

  1. 检查元素是否存在

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    if (!document.getElementById("placeholder")) return false;
    // 如果 id为 `description`存在,那么执行修改文字,否则忽略
    if (document.getElementById("description")) {
    // 获取title属性
    var title = whic_pic.getAttribute("title");
    // 获取p结点
    var p_element = document.getElementById("description");
    // 将p元素赋值给nodeValue
    p_element.firstChild.nodeValue = title;
    }

    全部代码为:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    function show_pic(whic_pic) {
    if (!document.getElementById("default")) return false;
    // 获取 href 属性
    var source = whic_pic.getAttribute("href");
    // 获取默认结点
    var placeholder = document.getElementById("default");
    // 如果 id为 `description`存在,那么执行修改文字,否则忽略
    if (document.getElementById("description")) {
    // 设置属性
    placeholder.setAttribute("src", source);
    // 获取 title 属性
    var title = whic_pic.getAttribute("title");
    // 获取 p 结点
    var p_element = document.getElementById("description");
    // 将 p 元素赋值给 nodeValue
    p_element.firstChild.nodeValue = title;
    }
    return true;
    }

    改进后即使不存在要找的元素,也不会发生错误。

  2. 如果 palceholder 图片在文档中删去,并在浏览器刷新页面,就会出现无论点击哪个链接,都没有任何响应。
    因为 prepare_gallery 函数取消了 onclick 事件的默认行为。其实是否要返回一个 false 用来取消默认行为,应该由 show_pic 函数决定。show_pic 返回两个可值的值。

    • 如果切换图片成功,返回 true;
    • 否则,返回 false。
      为了修正该问题,应该在返回前验证返回值,来决定是否阻止默认行为,如果返回 true,则更新。可以利用!来对 show_pic 的返回值取反。
      1
      2
      3
      links[i].onclick = function () {
      return !show_pic(this);
      };
      如果 show_pic 返回 true,那么就返回 false,浏览器会取消默认行为;
      如果返回 false,那就返回 true,浏览器会使用 onclick 的默认行为。
      所以最终代码为:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      function prepare_gallery() {
      if (!(document.getElementsByTagName || document.getElementById))
      return false;
      var gallery = document.getElementById("image_gallery");
      if (!gallery) return false;
      links = gallery.getElementsByTagName("a");
      for (var i = 0; i < links.length; i++) {
      links[i].onclick = function () {
      return !show_pic(this);
      };
      }
      }

优化

  1. 检查 title 属性

    1
    var text = which_pic.getAttribute("title");

    为了检查 title 属性是否真的存在,可以测试是否为 null。当不存在时,可以设置为空字符

    1
    2
    3
    4
    5
    if (which_pic.getAttribute("title")) {
    var text = which_pic.getAttribute("title");
    } else {
    var text = "";
    }

    完成同一操作的其它方法:

    1
    2
    3
    var text = which_pic.getAttribute("title")
    ? which_pic.getAttribute("title")
    : "";

    上面的?是一个三元操作符(ternary operator)。问号的后面是text的两种取值可能,如果which_pic.getAttribute("title")的返回值不为空,那么将其赋给text;如果返回值为空值,那么text将被赋为第二个值。
    三元操作符是if/else的一种变形体,比较简短,逻辑表达不怎么明显,不习惯的话可以用回if/else表达式。

  2. 增加了几项检查,函数的代码也变多了。实际工作中,需要自己决定是否真的需要检查,它们针对的是 HTML 文档有可能不在你控制范围内的情况。理想情况下,应该不需要对 HTML 文档做太多的假设。

键盘访问

  1. prepare_gallery()的核心代码:

    1
    2
    3
    links[i].onclick = function () {
    return !show_pic(this);
    };

    也可以使用三元操作符:

    1
    2
    3
    links[i].onclick = function () {
    return show_pic(this) ? false : true;
    };

    用鼠标点击时是没有任何问题的,但是用键盘来的话就不一定了。
    使用 tab 键是可以移动链接的,再按回车键就可以启用当前链接。
    使用onkeypress来处理键盘事件,按下键盘的按键会触发该事件。复制一份指令即可。

    1
    2
    3
    4
    5
    6
    links[i].onclick = function () {
    return show_pic(this) ? false : true;
    };
    links[i].onkeypress = function () {
    return show_pic(this) ? false : true;
    };

    更简单的方法:

    1
    2
    3
    4
    5
    links[i].onclick = function () {
    return show_pic(this) ? false : true;
    };
    // js函数之间是可以赋值的
    links[i].onkeypress = links[i].onclick;
  2. 这就是 js 和 html 分离带来的好处
    把所有的函数和事件都放在了外部文件,只需要修改 js 代码,而不用去修改 html 代码。

  3. onkeypress 的问题
    在某些浏览器里,按 Tag 键都会触发该函数,现在应该没有这种奇怪的浏览器了。
    最好不要使用onkeypress事件处理函数,onclick事件处理已经能满足要求,对键盘的支持也相当完美。
    最终代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    function show_pic(whic_pic) {
    if (!document.getElementById("default")) return false;
    // 获取 href 属性
    var source = whic_pic.getAttribute("href");
    // 获取默认结点
    var placeholder = document.getElementById("default");
    // 设置属性
    // 如果 id为 `description`存在,那么执行修改文字,否则忽略
    if (document.getElementById("description")) {
    placeholder.setAttribute("src", source);
    // 获取 title 属性
    var title = whic_pic.getAttribute("title");
    // 获取 p 结点
    var p_element = document.getElementById("description");
    // 将 p 元素赋值给 nodeValue
    p_element.firstChild.nodeValue = title;
    }
    return true;
    }
    function prepare_gallery() {
    if (!(document.getElementsByTagName || document.getElementById))
    return false;
    var gallery = document.getElementById("image_gallery");
    if (!gallery) return false;
    links = gallery.getElementsByTagName("a");
    for (var i = 0; i < links.length; i++) {
    links[i].onclick = function () {
    return show_pic(this) ? false : true;
    };
    }
    }

js 与 css 结合

  1. id 可以使用在 CSS 样式表里,比如,把图片清单的项目符号去掉,可以得用image_gallery写如下的 CSS 语句。

    1
    2
    3
    #image_gallery {
    list-style: none;
    }

    将这些 CSS 语句存入一个外部文件如layout.css,然后再从 gallery.html 的头部引用其。

    1
    <link rel="stylesheet" href="layout.css" />

    可以将其改变为横向

    1
    2
    3
    #image_gallery li {
    display: inline;
    }

    Image

DOM Core 和 HTML-DOM

  1. 已经使用的 DOM 方法:

    • getElementById
    • getElementsByTagName
    • getAttribute
    • setAttribute

    HTML-DOM 提供了一个 forms 对象,可以把下面的代码:

    1
    document.getElementsByTagName("form");

    简化为:

    1
    document.forms;

    把图片 src 取出来的语句:

    1
    element.getAttribute("src");

    简化为:

    1
    element.src;

    HTML-DOM 的代码会很短,但是只能用来处理 Web 文档。
    用 HTML-DOM 重写 show_pic 的话,可以变得更简短:

    1
    var source = whic_pic.getAttribute("href");

    简化为:

    1
    var source = whic_pic.href;

    设置属性

    1
    placeholder.setAttribute("src", source);

    可改为

    1
    placeholder.src = source;
  2. 即使了解 HTML-DOM,也要了解 DOM-sore,即使你只决定使用一种,毕竟要阅读别人编写的脚本。

小结

欢迎访问我的个人博客