内容更新
innerHTML
当我们需要前端来更新 div 的内容时,第一想到的方案就是设置innerHTML属性,下面是一个简单的例子:
<!DOCTYPE html>
<html lang="zh-cn">
<head>
<title></title>
</head>
<body>
<div id="divContainer"></div>
</body>
<script>
var html = "";
html += '<div class="col-md-3">'
html += '<div class="thumbnail">'
html += '<div class="caption">'
html += '<h3><span class="glyphicon glyphicon-globe"></span>你好,2020年!</h3>'
html += '<p>'
html += '让我们一起欢呼吧!'
html += '</p>'
html += '</div>'
html += '</div>'
html += '</div>'
document.getElementById("divContainer").innerHTML = html;
</script>
</html>
运行效果如下:
我们可以将 2020 改为动态取值:
html += '<h3><span class="glyphicon glyphicon-globe"></span>你好,' + new Date().getFullYear() + '年!</h3>'
痛点
使用 “+” 拼接的字符串过多,当html的结构比较复杂时,不利于后期维护。
改进
使用转义符来减少 “+” 的使用
var html = '<div class=\"col-md-3\">\
<div class=\"thumbnail\">\
<div class=\"caption\">\
<h3><span class=\"glyphicon glyphicon-globe\"></span>你好,2020年!</h3>\
<p>让我们一起欢呼吧!</p>\
</div>\
</div>\
</div>'
document.getElementById("divContainer").innerHTML = html;
虽然减少了 “+” 的使用,并且代码看起来没有之前的那么零碎,但仍然需要将 html 代码作为常量写入到 js 逻辑中,并且如果中间有动态内容仍然需要进行字符串的拼接。效果:
- 此时我们更应该关注的一个问题就是:IDE 并不支持对这种写法的语法高亮等识别。
模板更新
很多时候,我们需要某个 html 内容中的一小部分内容为动态的,它的数据来源于 js 的计算结果。最简单的使用 replace 来替换动态内容,方式如下:
var html = '<h1>你好,[year]年!</h1>';
html = html.replace('[year]', new Date().getFullYear());
document.getElementById("divContainer").innerHTML = html;
痛点
这里,我们规定了动态的部分,此内容最终由 js 计算出来。
这种自定义的方式有非常大的局限性,它只能填充简单的最终结果。并且,每个开发者对模板标签的规定都不一样,导致用法会千奇百怪。对于边界字符的定义更是灵活而无限制,有的人设置为 #year#, 或 @year@ 等。
改进
模板的计算部分应该要统一实现,尽可能地复用内部逻辑。
模板引擎
上述的目的均是修改一个 DOM 元素的 html 内容,同时也要保持 html 内容有一个最基础的模板部分,这部分可以作为一个字符串常量。动态部分由 js 去计算。由一个公共方法把最终处理好的 html 字符串再赋值给 DOM 元素的 innerHTML 属性即可。
在这里,我们介绍一下国内比较流行的模板引擎 art-template,它的作用是封装了对模板的解析过程,很大程度上节省了开发时间,让开发者只需要关注模板与数据本身,而不再需要关注解析逻辑。
示例:
<!DOCTYPE html>
<html lang="zh-cn">
<head>
<title></title>
<script src="https://unpkg.com/art-template@4.13.2/lib/template-web.js"></script>
</head>
<body>
<div id="divContainer"></div>
</body>
<script type="text/html" id="divContainerTemplate">
<h1>你好,{{year}}年!</h1>
{{each items as m i}}
<span>{{m}}</span>
{{/each}}
</script>
<script>
//获取模板字符串
var tplHTML = document.getElementById("divContainerTemplate").innerHTML;
//设置模板中的动态数据
var data = {
year: new Date().getFullYear(),
items: [1, 2, 3, 4, 5, 6]
}
//根据动态数据和静态模板计算出最终的 html 字符串
var html = template.render(tplHTML, data)
//更新界面
document.getElementById("divContainer").innerHTML = html;
</script>
</html>
效果:
这种写法比之前拼接方式要清晰得多,并且 IDE 在语法上面经过一些插件的辅助能够很好的进行识别,同时我们也可以利用模块化将模板字符串单独保存到一个 html 文件中。
事件绑定
原生
直接设置 DOM 元素的 onClick 属性,在 html 元素中直接设置属性内容:
<input type="button" value="按钮" onclick="alert(1);">
痛点
html 与 js 内容混合,并且 onclick 中暴露的方法必须是全局的。
改进
将 js 逻辑部分独立出来,因此,同样我们可以通过 js 设置 onclick 属性内容:
<input type="button" value="按钮">
<script type="text/javascript">
var bt = document.getElementsBytagname("input")[0];
bt.onclick = function(){
alert(2)
}
</script>
如果业务逻辑比较多,维护者不一定能知晓对此按钮绑定的所有 onclick 事件,或者说是因为一些业务场景的原因,可能会出现需要设置多次 onclick 的情况。如下:痛点
<input type="button" value="按钮">
<script type="text/javascript">
var bt = document.getElementsBytagname("input")[0];
bt.onclick = function(){
alert(2)
}
bt.onclick = function(){
alert(3)
}
</script>
改进这里设置了多个 onclick 属性,但最终只会有最后一个 onclick 设置会生效,因为后面的属性设置会覆盖前者的属性设置。
使用 DOM API 中的 EventTarget.addEventListener 来绑定事件。
EventTarget.addEventListener() 方法将指定的监听器注册到 EventTarget 上,当该对象触发指定的事件时,指定的回调函数就会被执行。
事件目标可以是一个文档上的元素 Element,Document和Window或者任何其他支持事件的对象 (比如 XMLHttpRequest)。
addEventListener()的工作原理是将实现EventListener的函数或对象添加到调用它的EventTarget上的指定事件类型的事件侦听器列表中。
<input type="button" value="按钮">
<script type="text/javascript">
var bt = document.getElementsBytagname("input")[0];
bt.addEventListener("click", function(){
alert(1)
})
bt.addEventListener("click", function(){
alert(2)
})
</script>
有关 addEventListener 的详细使用,参见:https://developer.mozilla.org/zh-CN/docs/Web/API/EventTarget/addEventListener
痛点
不论使用哪一种方案,对于一些复杂的事件绑定,以及更加灵活的做法,如:删除事件、只触发一次事件、重新绑定事件等,在各个浏览器上的表现均不一样。因此,我们不得不写很多兼容性的代码来满足用户的需求。
改进
需要把事件绑定、DOM 操作这些公共逻辑封装在一个库中,让开发者忽略浏览器之间的差异,从而只关心自己要实现的业务逻辑。
jQuery
它是一个快速,小型且功能丰富的 JavaScript 库。借助易于使用的API(可在多种浏览器中使用),使HTML文档的遍历和操作,事件处理,动画和AJAX等事情变得更加简单。兼具多功能性和可扩展性,jQuery改变了数百万人编写JavaScript的方式。
示例:
DOM 内容更新
$( "button.continue" ).html( "Next Step..." )
var hiddenBox = $( "#banner-message" );
$( "#button-container button" ).on( "click", function( event ) {
hiddenBox.show();
});
DOM 事件绑定
很多人不理解这里的 $ 是什么意思,其实它就是一个全局的变量,这个变量就是一个函数,我们一起看看:
其实 jQuery 同时也暴露了另一个全局变量 jQuery ,它们两者是等效的,只是为了代码的简洁,就使用一个字符 $ 来替代它:
jQuery 的使用,极大程度上简化了前端开发者对一些高度重复代码的编写,而且 jQuery 本身是一个非常稳定的库,在不同浏览器上面能够表现出相同的效果。
请注意,jQuery 的本质只是一个封装好的工具库,它并不是一个框架。
AJAX
使用 AJAX 的目的只是为了提升用户体验,将一些可以延迟加载的数据推迟到某个事件触发时再加载,这样可以减少页面首次加载的内容,从而提升首次加载的速度。
我们可以使用 XMLHttpRequest 来实现 AJAX 的相关功能,详细的文档可参见:https://developer.mozilla.org/zh-CN/docs/Web/API/XMLHttpRequest
原生
//构造表单数据
var formData = new FormData();
formData.append('username', 'johndoe');
formData.append('id', 123456);
//创建xhr对象
var xhr = new XMLHttpRequest();
//设置xhr请求的超时时间
xhr.timeout = 3000;
//设置响应返回的数据格式
xhr.responseType = "text";
//创建一个 post 请求,采用异步
xhr.open('POST', '/server', true);
//注册相关事件回调处理函数
xhr.onload = function(e) {
if(this.status == 200||this.status == 304){
alert(this.responseText);
}
};
xhr.ontimeout = function(e) { ... };
xhr.onerror = function(e) { ... };
xhr.upload.onprogress = function(e) { ... };
//发送数据
xhr.send(formData);
var xmlhttp;
if (window.XMLHttpRequest)
{
// IE7+, Firefox, Chrome, Opera, Safari 浏览器执行代码
xmlhttp=new XMLHttpRequest();
}
else
{
// IE6, IE5 浏览器执行代码
xmlhttp=new ActiveXObject("Microsoft.XMLHTTP");
}
XMLHttpRequest 对象并不是所有浏览器都支持,因此,如果我们的代码需要在旧版浏览器上运行,还需要写一些兼容性的代码,比如:
痛点
和之前碰到的类似,使用原生 API 的写法依然要考虑一些代码兼容问题,并且里面存在相当多的可复用代码。
改进
需要将 AJAX 对象封装起来
jQuery AJAX
使用 jQuery 中的 AJAX 方法,开发者不再需要考虑实现的细节,并且它提供了比原生对象更为简洁的方法,示例如下:
$.ajax({
url: "/api/getWeather",
data: {
zipcode: 97201
},
success: function( result ) {
$( "#weather-temp" ).html( "<strong>" + result + "</strong> degrees" );
}
});
除此之外,jQuery 提供了更丰富的 AJAX API,供大家使用,详细文档参见:https://api.jquery.com/jquery.ajax/
痛点
要想使用 jQuery 提供的 AJAX 方法,必须要引用 jQuery 的库,当我们只是想实现一个简单的功能时,就不得不为页面添加额外的 js 文件引用。
改进
当前现代浏览器都已经支持了一个新的 AJAX 操作方式:Fetch。Fetch API 提供了一个 JavaScript 接口,用于访问和操纵HTTP管道的部分,例如请求和响应。它还提供了一个全局 fetch()方法,该方法提供了一种简单,合理的方式来跨网络异步获取资源。
fetch('http://example.com/movies.json')
.then(function(response) {
return response.json();
})
.then(function(myJson) {
console.log(myJson);
});
虽然原生的 Fetch 很好,但是浏览器厂商却对它的支持并不完整,特别是 IE 浏览器,因此我们还需要引入额外的兼容库才行。
以下是 Fetch 的支持情况:
总结
困扰前端开发者最主要的一点就是 html 代码与 js 代码如何进行友好地加载、拆分、合并等。最复杂的部分就是对业务逻辑上的实现,很多代码混乱的项目并不是一开始就是那么乱,而是经过无数次的迭代导致最终的场面。首当其冲的基本任务是要保证项目结构的清晰。
js 代码的最终目的是更新 DOM 的内容,如何使用一种高效的方式去更新 DOM 是每一位前端开发者要思考的问题。当一个项目变得复杂后,很多模块与模块之间的关系开始不可控,我们最应该在当初就引入优秀的设计思想与理念来尽可能地避免这些问题。
总之,我们先关注一下这个问题:如何将一个 js 数据源,高效地填充到 html 中?