JavaScriptDom编程艺术——第五六七章
前言
前几天做实训作业时候发现JS功底真的不咋地,尤其是在操作DOM上。
尽管现在前端程序员直接操作DOM的机会不多了,但是不补上,总觉得缺点什么。
开始刷《JavaScript DOM编程艺术》。
平稳退化
平稳退化的意思就是当用户的浏览器不支持JavaScript时,你的网站也能让用户正常访问,而非无法使用。
尽管现在几乎没有不支持JavaScript的浏览器了,也没有什么人会禁用JavaScript,但是平稳退化也是一个成功的前端页面所必要的。
用一个弹出窗口的例子说明。
// HTML
<a href="javascript:popUp('http://cnblogs.com/lilpig')">Pop Window</a>
// JS
function popUp(link){
window.open(link,"popup","width=320,height=480");
}
打开这个页面可能出现三种情况
- 点击Pop Window,弹出一个本博客主页的窗口。
- 点击Pop Window,浏览器把
href
中的内容当成链接在当前窗口打开了。这是因为浏览器不支持JS造成的。 - 点击Pop Window,什么也没发生。这可能是用户禁用了浏览器的JS功能。
所以这个例子就不能平稳退化。
如果我们保留正常的href
属性作为保底,然后使用onclick
监听器,来调用popUp,并通过return false
来阻止a标签点击后的默认行为,这样能在用户端不支持JS时平稳退化,保留一个保底的行为,至少它会打开href
中的链接。
<a href="http://cnblogs.com/lilpig" onclick="popUp('http://cnblogs.com/lilpig');return false;">Pop Window</a>
更简单的写法如下,使用this
<a href="http://cnblogs.com/lilpig" onclick="popUp(this.href);return false;">Pop Window</a>
向CSS学习
在HTML中编写大量和JS相关的代码始终不太好,这会把HTML文档弄乱,并且利于后期的维护。想想,如果我们当前页面中有几百个上面的popUp
的调用,并且类似的调用分散在很多个页面中,这时我觉得popUp
这个名字太模糊了,表意不清淅,我想更改它的名字,这时可是一个大工程,即使现代的IDE都提供了批量替换功能,我看到这个需求也会头疼。
所以,HTML就让它专注于文档结构的表达,CSS就让它专注于页面的效果,JS就让它专注于页面的交互,各司其职。
// HTML
<a href="http://cnblogs.com/lilpig" class="popup">CNBLOG</a>
<a href="http://baidu.com" class="popup">BAIDU</a>
<a href="http://bilibili.com" class="popup">BILIBILI</a>
// JS
window.onload = function(){
var popup_elems = document.getElementsByClassName("popup");
for(var i = 0; i < popup_elems.length; i++){
var popup_elem = popup_elems[i];
console.log(popup_elem.tagName)
if(popup_elem.tagName.toLowerCase() === 'a'){
popup_elem.onclick = function(){
popUp(this.href);
return false;
}
}
}
}
这样,代码看上去虽然多了,却将不同的文件类型的工作分离了。
关于before和after的思考
CSS中有 before和after两个伪类,用于向文档中插入一些新内容。
从平稳退化的角度考虑,一些关键的、会影响一个操作是否完成的功能,千万不要使用before和after实现,因为这并非所有浏览器都支持。
谁在意?
就像上面说的,现在几乎没有一个浏览器不支持JavaScript也没有人关闭JavaScript功能。那需要因为几个极其特殊并且极少的个例而考虑平稳退化问题吗?
其实,这样更多的原因是保证网站对爬虫友好,大部分爬虫不支持解析JavaScript,如果你想要你的网站在搜索引擎的排名不那么难看,那么最好保证平稳退化。
<a href="http://cnblogs.com/lilpig" onclick="popUp(this.href);return false;">Pop Window</a>
就像上面,爬虫虽不能理解onclick
中的内容,但是不耽误它理解href
中的内容。
向后兼容
由于前期浏览器厂商的战争历史,JS有很多不好的,不规范的设计。即使现在W3C标准已经让这些问题几乎不复存在,但如果你想向后兼容,还是多写一些代码。(我不想)
就用上面的给a标签添加点击监听弹出窗口来解释,上面的代码并不能在不支持getElementsByClassName
的浏览器中运行,添加一行检测代码即可。
window.onload = function(){
if(!document.getElementsByClassName) return false;
var popup_elems = document.getElementsByClassName("popup");
for(var i = 0; i < popup_elems.length; i++){
var popup_elem = popup_elems[i];
console.log(popup_elem.tagName)
if(popup_elem.tagName.toLowerCase() === 'a'){
popup_elem.onclick = function(){
popUp(this.href);
return false;
}
}
}
}
性能
减少DOM访问
- 能只遍历一次树,千万不要遍历两次
- 合并放置脚本并放在body最后
- 压缩脚本
图片库实例
曾在第四章时有一个图片库的案例,这里使用上面的知识来改造它。
下面是第四章的源代码
<!-- body中的片段 -->
<h1>Snapshots</h1>
<ul>
<li><a href="./imgs/lion.jfif" title="狮子狮子" onclick="showPic(this);return false;">狮子</a></li>
<li><a href="./imgs/car.webp" title="汽车汽车" onclick="showPic(this);return false;">汽车</a></li>
<li><a href="./imgs/4.webp" title="三文鱼三文鱼" onclick="showPic(this);return false;">三文鱼</a></li>
<li><a href="./imgs/2.webp" title="矿山矿山" onclick="showPic(this);return false;">矿山</a></li>
</ul>
<img id="img" alt="">
<p id="desc">Choose An Image.</p>
body{
padding: 20px;
background-color: cornsilk;
}
ul {
list-style: none;
margin: 0;
padding: 0;
}
li{
float: left;
margin: 20px;
}
img{
display: block;
width: 400px;
height: 300px;
clear: both;
}
function showPic(picLink){
var img_link = picLink.getAttribute("href");
var img_desc = picLink.getAttribute("title");
var img_elem = document.getElementById("img");
var desc_elem = document.getElementById("desc").firstChild;
img_elem.setAttribute("src",img_link);
desc_elem.nodeValue = img_desc;
}
平稳退化
该代码支持平稳退化。
JS与HTML分离
这个代码中的JS与HTML并不是分离的。onclick
写在了html中。
想解决它也很简单,我们只需要在HTML中添加一个“钩子”,让JS能根据钩子来操作图片库即可。
给ul标签添加id
<ul id="gallery">
...
</ul>
在JS中完成这部分的逻辑:
function prepare(){
let gallery = document.getElementById("gallery");
let links = gallery.getElementsByTagName("a");
for(var i = 0; i < links.length; i++){
links[i].onclick = function(){
showPic(this);
return false;
}
}
}
window.onload
上面光是定义了方法,还没有执行它,我们需要让这个方法被调用。
window.onload = prepare;
这里给window.onload
赋值,将prepare
方法赋予它,方法会在前端页面加载完毕后被调用。
问题是,如果我想要在窗口加载完成后调用多个函数,怎么办。
window.onload = function(){
function1();
function2();
}
这样是一个解决办法,但是还不够优美,当js写的太长时,每次我想将我的新函数放到onload
中时,还得翻阅代码,找到onload
的定义处,再把它添加进去。
var _onloadFunctions = []
function addOnLoad(func){
_onloadFunctions.push(func);
}
window.onload = function(){
for(var i=0;i<_onloadFunctions.length;i++){
_onloadFunctions[i]()
}
}
这样我们就可以在想添加函数到onload
的位置直接调用
addOnLoad(functionName);
注意,书中的写法如下
我没这样写是因为,仔细观察这个方法,每次添加一个新方法,都会增加一层函数的调用栈,虽然这并不会太影响性能,但总觉得不太好。
下面是三次调用得到的实际window.onload
addLoadEvent(func1);
addLoadEvent(func2);
addLoadEvent(func3);
// 实际你得到的onload
window.onload = function(){
function(){
function(){
func1();
}();
func2();
}();
func3();
}
向后兼容
保证向后兼容需要添加一些检测代码。
在prepare中添加这两行代码即可。
if(!document.getElementById) return false;
if(!document.getElementsByTagName) return false;
不要做过多的假设
function showPic(picLink){
var img_link = picLink.getAttribute("href");
var img_desc = picLink.getAttribute("title");
var img_elem = document.getElementById("img");
var desc_elem = document.getElementById("desc").firstChild;
img_elem.setAttribute("src",img_link);
desc_elem.nodeValue = img_desc;
}
在showPic
方法中,我假设了id为img
和desc
的元素在页面中一定存在,而如果它们不存在,js的执行就会出错。
修改showPic方法,添加一些检测代码即可避免错误
function showPic(picLink){
if(!document.getElementById("img")) return false;
var img_link = picLink.getAttribute("href");
var img_desc = picLink.getAttribute("title");
var img_elem = document.getElementById("img");
img_elem.setAttribute("src",img_link);
if(document.getElementById("desc")) {
var desc_elem = document.getElementById("desc").firstChild;
desc_elem.nodeValue = img_desc;
}
return true;
}
但是这又会引入新的错误,如果页面中没有img
,平稳退化原则就会崩塌,这时可以修改prepare
中的一些代码:
links[i].onclick = function(){
return !showPic(this);
}
因为onclick返回false
代表阻止默认行为,true
代表不阻止,showPic返回false
代表设置图片到img中失败,true
代表成功,当设置失败了,我们就不阻止默认行为,让其平稳退化。所以这里返回需要取反。
一些思考
首先在代码里到处是return
显然违反了一个函数只能有一个入口和一个出口的规约,但是不这样写,就会出现很多的嵌套if。
if(document.getElementById){
if(document.getElementBy...){
if(...){
}
}
}
或者是很长的逻辑表达式
if(document.getElementById && document.getElementBy... && ...){
}
这两种办法可读性都不怎么好。
作者的说法是,如果非要多个返回值,就让它集中在方法开头,也就是前几行用于做这种检测,并返回,后面才是方法的逻辑。
但是这样做又会出现一个问题,你看showPic
,其中因为遵守上面的规约,所以重复的调用了很多次document.getElementById
,这是无用的调用,要知道每次使用这个都是在遍历树。
但如果你复用它就无法遵守规约
var img = document.getElementById("img");
if(!img) return;
img.setAttribute(img_link);
我的想法是,只要可读性还过得去,并且这点可读性上的牺牲来换取性能上的提高是划算的话,就写吧。
尽信书不如无书。
更进一层的分离
其实,如果这个图片库是你开发给其他人使用的,那么它必须为了支持你的图片库而在HTML代码中添加一个img
和一个p
标签来先显示图片。实际上还是将行为和结构耦合了。
能否将这里解耦?
这就需要我们的JS中自动创建这两个元素,并添加到页面中。
创建元素
复习,DOM中有哪几种节点?
答:元素节点、文本节点、属性节点。(参考书中第三章还是第四章我忘了)。
DOM API为创建这些节点提供了一些方法。
createElement(tagName)
用于创建一个元素节点,使用tagName作为元素的标签createTextNode(text)
创建一个文本节点,并把text作为其中的文本createAttribute(attributeName)
创建一个属性节点,属性名是attribute
首先,大部分浏览器支持更简单的方法,比如innerHTML
属性等,但那些并不是DOM标准中定义的,本章只用DOM API中定义的方法。
也许把createTextNode改成createText会更好????
操作元素
parentElement.appendChild(childElement)
在父元素中追加子元素,既然是追加,新增元素在所有已有子元素的后面parentElement.insertBefore(childElement,targetElement)
在父元素中插入子元素,插入的位置在targetElement
之前
为什么这两个方法名取的还是如此的丑陋!!!
加强图片库
删除之前页面中的img和desc,添加如下的代码到js中,自动创建两个图片库必要的元素并添加到页面中。
function addEssentialElement(){
if(!document.createElement) return false;
if(!document.createAttribute) return false;
if(!document.createTextNode) return false;
var img = document.createElement("img");
var img_id = document.createAttribute("id");
img_id.value = "img";
img.setAttributeNode(img_id);
var desc = document.createElement("p");
var desc_id = document.createAttribute("id");
desc_id.value = "desc";
desc.setAttributeNode(desc_id);
desc.appendChild(document.createTextNode("Choose an image."))
document.body.appendChild(img);
document.body.appendChild(desc);
}
addOnLoad(addEssentialElement);
好几拨丑啊这代码说实话,就他妈像swing那种丑陋。
于是我写了一个简单的库,尽管库里的代码很丑,也没有太多的验证机制,但是调用方可以很优雅的调用它,就像你在使用Flutter
和Jetpack Compose
,使用组合来在js中创建视图。
如下是使用这个库后的代码,下面是为了展示出更多库的特性所写的代码,实际上并不需要这么多。
function addEssentialElement(){
append(
document.body,
div({
id: "gallery",
styles: {
width: "400px",
height: "300px",
},
children: [
img({
id: "img"
}),
p({
id: "desc",
text: text("Choose an Image.")
})
]
})
)
}
简化后的代码:
function addEssentialElement(){
appendAll(document.body,[
img({
id: "img"
}),
p({
id: "desc",
text: text("Choose an Image.")
})
]);
};
以下为库的代码
// jsui.js 只是写着玩 请勿将此库用于生产环境!!!!!!!!!!!
function _autoMixinToAttribute(attrName,metaData){
if(metaData[attrName]){
if(!metaData.attributes) metaData.attributes = {};
if(!metaData.attributes[attrName])
metaData.attributes[attrName] = metaData[attrName];
}
}
function element(metaData){
if(!metaData.tag)return p({
styles: {
color: "red"
},
text: text("ERROR Element has no tag!!!")
});
let e = document.createElement(metaData.tag);
if(metaData.styles){
let styleString="";
for(let style in metaData.styles){
styleString += (style+": "+metaData.styles[style] + "; ");
}
if(!metaData.attributes) metaData.attributes = {};
metaData.attributes["style"] = styleString;
}
if(metaData.attributes){
for(let attr in metaData.attributes){
let attrNode = document.createAttribute(attr);
attrNode.value = metaData.attributes[attr];
e.setAttributeNode(attrNode);
}
}
if(metaData.text){
e.appendChild(document.createTextNode(metaData.text));
}
if(metaData.children){
appendAll(e,metaData.children)
}
return e;
}
function _elementWithTag(tag,metaData){
metaData["tag"] = tag;
_autoMixinToAttribute("id",metaData);
_autoMixinToAttribute("class",metaData);
return element(metaData);
}
function text(content){
return content;
}
function p(metaData){
return _elementWithTag("p",metaData);
}
function div(metaData){
return _elementWithTag("div",metaData);
}
function img(metaData){
// 提供src的简易写法
_autoMixinToAttribute("src",metaData);
return _elementWithTag("img",metaData);
}
function span(metaData){
return _elementWithTag("span",metaData);
}
function head(metaData){
if(!head.level)return head({
level: 1,
styles: {
color: "red"
},
text: text("ERROR HEAD NO LEVEL!!!")
});
if(head.level==1) return _elementWithTag("h1",metaData);
if(head.level==2) return _elementWithTag("h2",metaData);
if(head.level==3) return _elementWithTag("h3",metaData);
if(head.level==4) return _elementWithTag("h4",metaData);
if(head.level==5) return _elementWithTag("h5",metaData);
if(head.level==6) return _elementWithTag("h6",metaData);
else return head({
level: 1,
styles: {
color: "red"
},
text: text("ERROR HEAD LEVEL IS NOT VALID!!! YOU CAN USE 1~6 ONLY")
})
}
function br(metaData){
if(!metaData) metaData = {}
return _elementWithTag("br",metaData);
}
function append(parentElement,childElement){
parentElement.appendChild(childElement);
}
function appendAll(parentElement,childElements){
for(let i in childElements){
append(parentElement,childElements[i]);
}
}