让我们编写非侵入式 JavaScript
一篇关于编写非侵入式 JavaScript 的不同方法的文章。
引言
非侵入式 JavaScript 是一种编写 JavaScript 代码的方式,在这种方式中,我们能够很好地将文档内容和脚本内容分离开来,从而可以清晰地区分它们。这种方法有很多好处,因为它能使我们的代码不易出错,易于更新和调试。
目标读者
本文档旨在为具有中级到高级 JavaScript 编码能力的开发者提供参考。其中一些 API 和功能可能对某些开发者来说是新颖的,我已尽力将它们与代码分开描述。
概念
非侵入式编程的基本概念是,JavaScript 应该作为我们网页的增强功能,而不是绝对的必需品。如果您不需要 JavaScript,那就不要使用它;您的静态内容将通过纯粹的 HTML 和 CSS 正常显示。许多开发者犯的错误是在尚未确定是否需要的情况下就导入代码库。例如,jQuery 在所有基本需求都可以轻松通过 CSS 和纯 JavaScript 代码满足的情况下被滥用了。
如果您的应用程序是动态的,需要异步调用服务器代码,更新 HTML 内容,并执行各种事件绑定,那么请务必使用 JavaScript,以及 jQuery 等库,但要尝试系统地将它们编码在单独的层中,以保持 HTML 内容干净且语义上分离。
以下是 Web 应用程序开发中,编写非侵入式 JavaScript 会产生明显积极影响的几个方面:
代码分离
JavaScript 代码不嵌入到 HTML 中,从而方便更新或升级代码。
稳定性
非侵入式 JavaScript 生成的错误信息更少。即使脚本运行失败,网页也将始终显示来自静态标记和 CSS 的所有内容。
优雅降级和渐进增强
如果浏览器不支持某个功能,代码应静默禁用该功能,而不是抛出错误消息。同样,如果存在某个功能的更高级版本,代码应使用升级后的功能,以根据浏览器的高级特性为用户提供体验。
更干净的全局作用域
全局作用域或窗口作用域将不会充斥不必要或不需要的对象、属性、函数和变量。非侵入式方法是通过命名空间来访问应用程序数据的单一入口点。
Using the Code
本文档侧重于非侵入式 JavaScript 编程的实际应用,而非理论层面。下一节将展示编写代码的侵入式和非侵入式方法。核心思想是将 JavaScript 与 HTML 分离,并在稍后通过 JavaScript 修改 DOM 时扩展这种方法。
在我们开始比较好的代码和坏的代码之前,我想明确一点,这种方法不应被过度使用,以免给其他开发者的代码库修改带来太多困难。最终,我们都同意错误和 bug 是难免的,任何人都能以最快的时间修复它们;我们的非侵入式代码不应妨碍这一点。
让我们开始看看可以应用非侵入式编码实践的常用代码片段。
HTML 锚点
在 HTML 标记声明中为锚点的单击事件进行连接是一个非常普遍的做法。这可能会带来很多问题;比如代码调试困难,而且如果我们因为任何原因需要更改 JavaScript 代码,就需要修改锚点标签。
<body>
Search Engines
<br />
<a href="http://www.google.com" onclick="window.open(this.href);return false;">Google</a>
<a href="http://www.search.yahoo.com" onclick="window.open(this.href);return false;">Yahoo Search
</a>
<a href="http://www.bing.com" onclick="window.open(this.href);return false;">Bing</a>
</body>
解决方案是通过分离 HTML 和脚本代码来实现非侵入式。
非侵入式方法
<body>
Search Engines
<br />
<a href="http://www.google.com">Google</a>
<a href="http://www.search.yahoo.com">Yahoo Search</a>
<a href="http://www.bing.com">Bing</a>
<script>
document.addEventListener('DOMContentLoaded', function () {
var anchors = document.getElementsByTagName('a');
WireupAnchors.bind(null, anchors)();
});
function WireupAnchors(anchors) {
anchors = anchors || []; //If anchors is null or undefined then use empty array
for (var i = 0; i < anchors.length; i++) {
iterator.call(anchors[i]);
}
function iterator() {
this.onclick = function () {
window.open(this.href);
return false;
}
}
}
</script>
</body>
在上面的代码中,当文档内容被解析且所有 HTML 都已加载时,将触发 DOMContentLoaded
事件。这不包括样式表、外部 CSS 和图像文件。
当 DOM 准备就绪后,将调用 WireupAnchors
。锚点作为数组传递给 WireupAnchors,并为每个项调用 iterator
来将锚点的单击事件绑定到一个匿名函数,该函数将打开一个新窗口。href
属性的值用作 URL。
事件绑定
以下代码与上面的代码大体相同,click
事件绑定在按钮声明的内联代码中。
<body>
<input type="button" value="Button1" onclick="button1_Click(); return false;" />
<input type="button" value="Button2" onclick="button2_Click(); return false;"/>
<script>
function button1_Click() {
alert('Button1 was Clicked!');
}
function button2_Click() {
alert('Button2 was Clicked!');
}
</script>
</body>
非侵入式方法
没有使用内联代码,而是为按钮分配了特定的 ID,并且通过与 HTML 分开的 JavaScript 代码进行事件绑定。
<body>
<input id="button1" type="button" value="Button1" />
<input id="button2" type="button" value="Button2"/>
<script>
document.addEventListener('DOMContentLoaded', WireUpEvents);
function WireUpEvents() {
var button1 = document.getElementById('button1'),
button2 = document.getElementById('button2');
button1.addEventListener('click', button1_Click);
button2.addEventListener('click', button2_Click);
}
function button1_Click() {
alert('Button1 was Clicked!');
return false;
}
function button2_Click() {
alert('Button2 was Clicked!');
}
</script>
</body>
在上面的代码中,addEventListener
只是被用来绑定按钮的单击事件。
设置 CSS 属性
当我们需要使用 JavaScript 为任何控件设置 HTML 样式时,我们的第一反应是设置特定的 CSS 属性值。虽然这样做对我们的 Web 应用程序的健康状况没有直接威胁,但问题出现在我们需要更新代码时。我们可能需要找到所有需要更改的代码行的出现处,这是一个极其耗时的过程。
<body>
<div id="myDiv"></div>
<script>
document.addEventListener('DOMContentLoaded', onLoad);
function onLoad() {
var myDiv = document.getElementById('myDiv');
myDiv.style.width = '100px';
myDiv.style.height = '100px';
myDiv.style.borderRadius = '5px';
myDiv.style.borderColor = 'black';
myDiv.style.borderWidth = '2px';
myDiv.style.borderStyle = 'solid';
myDiv.style.backgroundColor = 'green';
myDiv.style.display = 'block';
}
</script>
</body>
非侵入式方法
解决上述问题的方法是创建特定的 CSS 类,然后直接更新元素的 CSS 类值,而不是设置特定的 CSS 属性。这种方法将使代码更新更加容易,并且代码库将不易出错。
<head>
<title></title>
<style type="text/css">
.GreenBox {
width: 100px;
height: 100px;
border-radius: 5px;
border: 2px solid black;
display: block;
background-color: green;
}
</style>
</head>
<body>
<div id="myDiv"></div>
<script>
document.addEventListener('DOMContentLoaded', onLoad);
function onLoad() {
var myDiv = document.getElementById('myDiv');
myDiv.className = 'GreenBox';
}
</script>
</body>
GreenBox
是 <style>
标签中的一个 CSS 类,其中包含之前代码片段中通过 JavaScript 代码单独设置的所有属性值。在这段代码中,我们只是设置元素的类名。
使用 HTML 属性
以下代码用于在文本框为空时为其设置红色边框,以告知用户该字段是必填的。在以下代码中,有一个名为 RequiredFieldValidation
的函数,该函数通常用于所有字段。但问题在于,change 事件被连接到所有单独的字段,这使得我们的代码不必要地冗长。
<head>
<title></title>
<style type ="text/css">
.RedBox{
padding:2px;
border:2px solid red;
border-radius:5px;
}
.BlackBox{
padding:2px;
border:2px solid black;
border-radius:5px;
}
</style>
</head>
<body>
<input id="txtFistName" type="text" placeholder="First Name" class="RedBox" />
<input id="txtLastName" type="text" placeholder="Last Name" class="RedBox" />
<br /><br />
<input id="txtEmail" type="text" placeholder="Email Id" class="RedBox" />
<br /><br />
<input id="txtPhone" type="text" placeholder="Phone" class="RedBox" />
<script>
document.addEventListener('DOMContentLoaded', onLoad);
function onLoad() {
var txtFirstName = document.getElementById('txtFistName'),
txtLastName = document.getElementById('txtLastName'),
txtEmail = document.getElementById('txtEmail'),
txtPhone = document.getElementById('txtPhone');
txtFistName.addEventListener('change', RequiredFieldValidation.bind(txtFistName));
txtLastName.addEventListener('change', RequiredFieldValidation.bind(txtLastName));
txtEmail.addEventListener('change', RequiredFieldValidation.bind(txtEmail));
txtPhone.addEventListener('change', RequiredFieldValidation.bind(txtPhone));
}
function RequiredFieldValidation() {
this.className = (this.value.length === 0) ? 'RedBox' : 'BlackBox';
}
</script>
</body>
非侵入式方法
上面的代码可以通过为需要遵循相同规则进行验证的所有输入添加通用属性来改进。以下代码通过为文本输入添加 Required
属性来实现这一点。在以下代码中,我们将 RequiredFieldValidation
函数与所有具有 Required
属性的文本字段的 change 事件进行绑定。
<head>
<title></title>
<style type ="text/css">
.RedBox{
padding:2px;
border:2px solid red;
border-radius:5px;
}
.BlackBox{
padding:2px;
border:2px solid black;
border-radius:5px;
}
</style>
</head>
<body>
<input id="txtFistName" type="text" placeholder="First Name" class="RedBox" Required/>
<input id="txtLastName" type="text" placeholder="Last Name" class="RedBox" Required/>
<br /><br />
<input id="txtEmail" type="text" placeholder="Email Id" class="RedBox" Required/>
<br /><br />
<input id="txtPhone" type="text" placeholder="Phone" class="RedBox" Required/>
<script>
document.addEventListener('DOMContentLoaded', onLoad);
function onLoad() {
var required = document.querySelectorAll('[Required]');
for (var i = 0; i < required.length; i++) {
required[i].addEventListener('change', RequiredFieldValidation.bind(required[i]));
}
}
function RequiredFieldValidation() {
this.className = (this.value.length === 0) ? 'RedBox' : 'BlackBox';
}
</script>
</body>
支持 Html5 的浏览器会在表单提交时,为带有 required
属性的字段显示错误消息(如果字段为空)。有关更多信息,您可以参考以下链接:
在上面的代码中,required 属性被扩展用于直观地突出显示字段,并带有红色边框。代码本身基本可以自解释,querySelectorAll
用于查找所有带有 required
属性的元素。对于找到的每个元素,代码将 change
事件与 RequiredFieldValidation
函数进行绑定。
命名空间
接下来的代码是一个小型计算器应用程序。您可以轻松注意到,每个变量和函数都存在于全局作用域中。这种方法在不导入外部库的情况下是可以接受的,但我们知道在任何像样的 Web 应用程序中这种情况很少见。我们有各种外部 API 来帮助我们创建更好的用户界面和编写更好的业务逻辑。
因此,我们不能让任何小变量、属性、函数或对象污染全局作用域,因为它可能会覆盖一些现有的代码实现。
<body>
<input type="text" id="txtNumber1" /> <input type="text" id="txtNumber2" />
<br /><br />
<input type="button" value="Add" add/>
<input type="button" value="Subtract" subtract/>
<input type="button" value="Multiply" multiply/>
<input type="button" value="Divide" divide/>
<br /><br />
<input type="text" id="txtResult" />
<script>
var txtNumber1, txtNumber2, txtResult;
document.addEventListener('DOMContentLoaded', onLoad);
function onLoad() {
txtNumber1 = document.getElementById('txtNumber1');
txtNumber2 = document.getElementById('txtNumber2');
txtResult = document.getElementById('txtResult');
document.querySelector('[add]').addEventListener('click', add_click);
document.querySelector('[subtract]').addEventListener('click', subtract_click);
document.querySelector('[multiply]').addEventListener('click', multiply_click);
document.querySelector('[divide]').addEventListener('click', divide_click);
}
function calculate(number1, number2, operationType) {
var result = 0;
switch (operationType.toLowerCase()) {
case 'add':
result = number1 + number2;
break;
case 'subtract':
result = number1 - number2;
break;
case 'multiply':
result = number1 * number2;
break;
case 'divide':
result = number1 / number2;
break;
}
return result;
}
function add_click() {
txtResult.value = calculate(parseFloat(txtNumber1.value)
, parseFloat(txtNumber2.value), 'Add');
}
function subtract_click() {
txtResult.value = calculate(parseFloat(txtNumber1.value)
, parseFloat(txtNumber2.value), 'Subtract');
}
function multiply_click() {
txtResult.value = calculate(parseFloat(txtNumber1.value)
, parseFloat(txtNumber2.value), 'Multiply');
}
function divide_click() {
txtResult.value = calculate(parseFloat(txtNumber1.value)
, parseFloat(txtNumber2.value), 'Divide');
}
</script>
</body>
非侵入式方法
上述问题有一个非常简单的解决方案,那就是使用命名空间。JavaScript 中的命名空间通过对象实现,它们为我们提供了访问应用程序不同模块的单一入口点。这种方法消除了全局作用域污染及其产生的副作用的可能性。
在以下代码中,App
对象被视为根命名空间。Operations
、Inputs
、Events
等属性是子命名空间,其中包含特定于它们的信息。
<body>
<input type="text" id="txtNumber1" />
<input type="text" id="txtNumber2" /> <br /><br />
<input type="button" value="Add" add />
<input type="button" value="Subtract" subtract />
<input type="button" value="Multiply" multiply />
<input type="button" value="Divide" divide />
<br /><br />
<input type="text" id="txtResult" />
<script>
var App = { 'Operations': {}, 'Inputs': {}, 'Outputs': {}, 'Commands': {}, 'Events': {} };
App.Events = {
'Add': function () {
App.Outputs.Result.value = App.Operations.Calculate(
parseFloat(App.Inputs.Number1.value)
, parseFloat(App.Inputs.Number2.value), 'Add');
},
'Subtract': function () {
App.Outputs.Result.value = App.Operations.Calculate(
parseFloat(App.Inputs.Number1.value)
, parseFloat(App.Inputs.Number2.value), 'Subtract');
},
'Multiply': function () {
App.Outputs.Result.value = App.Operations.Calculate(
parseFloat(App.Inputs.Number1.value)
, parseFloat(App.Inputs.Number2.value), 'Multiply');
},
'Divide': function () {
App.Outputs.Result.value = App.Operations.Calculate(
parseFloat(App.Inputs.Number1.value)
, parseFloat(App.Inputs.Number2.value), 'Divide');
},
'Load': function () {
App.Inputs = {
'Number1': document.getElementById('txtNumber1'),
'Number2': document.getElementById('txtNumber2'),
};
App.Outputs = {
'Result': document.getElementById('txtResult')
};
App.Commands = {
'Add': document.querySelector('[add]'),
'Subtract': document.querySelector('[subtract]'),
'Multiply': document.querySelector('[multiply]'),
'Divide': document.querySelector('[divide]')
};
App.Commands.Add.addEventListener('click', App.Events.Add);
App.Commands.Subtract.addEventListener('click', App.Events.Subtract);
App.Commands.Multiply.addEventListener('click', App.Events.Multiply);
App.Commands.Divide.addEventListener('click', App.Events.Divide);
}
};
App.Operations.Calculate = function (number1, number2, operationType) {
var result = 0;
switch (operationType.toLowerCase()) {
case 'add':
result = number1 + number2;
break;
case 'subtract':
result = number1 - number2;
break;
case 'multiply':
result = number1 * number2;
break;
case 'divide':
result = number1 / number2;
break;
}
return result;
};
document.addEventListener('DOMContentLoaded', App.Events.Load);
</script>
</body>
上面代码片段最好的地方在于,它被整齐地组织到命名空间的相关部分,并且每个信息都有自己的命名空间地址。
优雅降级
优雅降级简单来说就是,如果某个功能不支持,或者 JavaScript 不可用,就静默回退到该功能的原始版本。下面的代码演示了 <noscript>
标签的使用。如果禁用了 JavaScript,此标签的内容将添加到网页中。
<body>
<noscript>
JavaScript seems to be disabled!
<br />
</noscript>
<a href="www.codeproject.com">CodeProject</a>
</body>
渐进增强
渐进增强是利用最新可用功能来增强用户体验或代码效率的艺术。最好的例子可以看到在以前 CSS 不可用且难以作为自定义解决方案实现的圆角。其他例子可以在现代浏览器中提供的新 API 中看到。
在下面的代码示例中,通过创建 button_click
函数的委托来连接 click 事件。为此,使用了 Function.prototype.bind
,但如果我们的代码在不支持 bind() 的旧浏览器上运行会怎样?为了处理这种情况,我们需要添加自定义的 bind API 来模仿现代浏览器,并确保我们的应用程序能够运行,无论使用什么浏览器。
<body>
<input id="button1" type="button" value="Hello" />
<script>
var button1,
messages = {'Message1': 'Hello World'};
document.addEventListener('DOMContentLoaded', onLoad);
function onLoad(){
button1 = document.getElementById('button1');
button1.addEventListener('click', button_click.bind(messages));
}
function button_click() {
alert(this.Message1);
}
//If Function.prototype.bind is not available then create a custom bind function.
//Polyfill: https://mdn.org.cn/en-US/docs/Web/JavaScript/Reference/Global_Objects
/Function/bind
if (!Function.prototype.bind) {
Function.prototype.bind = function (oThis) {
if (typeof this !== 'function') {
// closest thing possible to the ECMAScript 5
// internal IsCallable function
throw new TypeError('Function.prototype.bind - what is trying to be bound is not
callable');
}
var aArgs = Array.prototype.slice.call(arguments, 1),
fToBind = this,
fNOP = function () { },
fBound = function () {
return fToBind.apply(this instanceof fNOP && oThis
? this
: oThis,
aArgs.concat(Array.prototype.slice.call(arguments)));
};
fNOP.prototype = this.prototype;
fBound.prototype = new fNOP();
return fBound;
};
}
</script>
</body>
非侵入式 JavaScript 编程还有许多其他应用领域。我们应该时刻关注那些可以让我们通过不同方式编写代码以改进代码库并使其不易出错的地方。
结束语
是否遵循非侵入式编码实践的决定取决于开发者的偏好,但遵循防御性措施来构建我们的 Web 应用程序总是有益的。当我们的代码在互联网上运行时,可能有一千种方式出现问题;例如,未知设备、不支持的 API、不必要的动态脚本等。因此,遵循某些标准编码实践,例如本文中的实践,总是明智之举,可以让我们生活得更轻松一些。