HTML DOM 允许 JavaScript 改变 HTML 元素的内容。
改变 HTML 输出流
JavaScript 能够创建动态的 HTML 内容:
今天的日期是: Thu Aug 25 2016 09:23:24 GMT+0800 (中国标准时间)
在 JavaScript 中,document.write() 可用于直接向 HTML 输出流写内容。
实例
<!DOCTYPE html>
<html>
<body>
<script>
document.write(Date());
</script>
</body>
</html>
绝对不要在文档加载完成之后使用 document.write()。这会覆盖该文档。 |
改变 HTML 内容
修改 HTML 内容的最简单的方法时使用 innerHTML 属性。
如需改变 HTML 元素的内容,请使用这个语法:
document.getElementById(id).innerHTML=新的 HTML
本例改变了 <p>元素的内容:
实例
<html>
<body>
<p id="p1">Hello World!</p>
<script>
document.getElementById("p1").innerHTML="新文本!";
</script>
</body>
</html>
本例改变了 <h1> 元素的内容:
实例
<!DOCTYPE html>
<html>
<body>
<h1 id="header">Old Header</h1>
<script>
var element=document.getElementById("header");
element.innerHTML="新标题";
</script>
</body>
</html>
实例讲解:
上面的 HTML 文档含有 id="header" 的 <h1> 元素
我们使用 HTML DOM 来获得 id="header" 的元素
JavaScript 更改此元素的内容 (innerHTML)
改变 HTML 属性
如需改变 HTML 元素的属性,请使用这个语法:
document.getElementById(id).attribute=新属性值
本例改变了 <img> 元素的 src 属性:
实例
<!DOCTYPE html>
<html>
<body>
<img id="image" src="smiley.gif">
<script>
document.getElementById("image").src="landscape.jpg";
</script>
</body>
</html>
实例讲解:
上面的 HTML 文档含有 id="image" 的 <img> 元素
我们使用 HTML DOM 来获得 id="image" 的元素
JavaScript 更改此元素的属性(把 "smiley.gif" 改为 "landscape.jpg")
如您还有不明白的可以在下面与我留言或是与我探讨QQ群308855039,我们一起飞!
档对象模型(DOM)
JS 有很多地方让咱们吐槽,但没那么糟糕。作为一种在浏览器中运行的脚本语言,它对于处理web页面非常有用。在本中,我们将看到我们有哪些方法来交互和修改HTML文档及其元素。但首先让我们来揭开文档对象模型的神秘面纱。
文档对象模型是一个基本概念,它是咱们在浏览器中所做的一切工作的基础。但那到底是什么? 当咱们访问一个 web 页面时,浏览器会指出如何解释每个 HTML 元素。这样它就创建了 HTML 文档的虚拟表示,并保存在内存中。HTML 页面被转换成树状结构,每个 HTML 元素都变成一个叶子,连接到父分支。考虑这个简单的HTML 页面:
<!DOCTYPE html> <html lang="en"> <head> <title>A super simple title!</title> </head> <body> <h1>A super simple web page!</h1> </body> </html
当浏览器扫描上面的 HTML 时,它创建了一个文档对象模型,它是HTML结构的镜像。在这个结构的顶部有一个 document 也称为根元素,它包含另一个元素:html。html 元素包含一个 head,head 又有一个 title。然后是含有 h1的 body。每个 HTML 元素由特定类型(也称为接口)表示,并且可能包含文本或其他嵌套元素
document (HTMLDocument) | | --> html (HTMLHtmlElement) | | --> head (HtmlHeadElement) | | | | --> title (HtmlTitleElement) | | --> text: "A super simple title!" | | --> body (HtmlBodyElement) | | | | --> h1 (HTMLHeadingElement) | | --> text: "A super simple web page!"
每个 HTML 元素都是从 Element 派生而来的,但是它们中的很大一部分是进一步专门化的。咱们可以检查原型,以查明元素属于什么“种类”。例如,h1 元素是 HTMLHeadingElement
document.quertSelector('h1').__proto__ // 输出:HTMLHeadingElement
HTMLHeadingElement 又是 HTMLElement 的“后代”
document.querySelector('h1').__proto__.__proto__ // Output: HTMLElement
Element 是一个通用性非常强的基类,所有 Document 对象下的对象都继承自它。这个接口描述了所有相同种类的元素所普遍具有的方法和属性。一些接口继承自 Element 并且增加了一些额外功能的接口描述了具体的行为。例如, HTMLElement 接口是所有 HTML 元素的基本接口,而 SVGElement 接口是所有 SVG 元素的基础。大多数功能是在这个类的更深层级(hierarchy)的接口中被进一步制定的。
在这一点上(特别是对于初学者),document 和 window 之间可能有些混淆。window 指的是浏览器,而 document 指的是当前的 HTML 页面。window 是一个全局对象,可以从浏览器中运行的任何 JS 代码直接访问它。它不是 JS 的“原生”对象,而是由浏览器本身公开的。window 有很多属性和方法,如下所示:
window.alert('Hello world'); // Shows an alert window.setTimeout(callback, 3000); // Delays execution window.fetch(someUrl); // makes XHR requests window.open(); // Opens a new tab window.location; // Browser location window.history; // Browser history window.navigator; // The actual device window.document; // The current page
由于这些属性是全局属性,因此也可以省略 window:
alert('Hello world'); // Shows an alert setTimeout(callback, 3000); // Delays execution fetch(someUrl); // makes XHR requests open(); // Opens a new tab location; // Browser location history; // Browser history navigator; // The actual device document; // The current page
你应该已经熟悉其中的一些方法,例如 setTimeout()或 window.navigator,它可以获取当前浏览器使用的语言:
if (window.navigator) { var lang=window.navigator.language; if (lang==="en-US") { // show something } if (lang==="it-IT") { // show something else } }
要了解更多 window 上的方法,请查看MDN文档。在下一节中,咱们深入地研究一下 DOM。
节点、元素 和DOM 操作
document 接口有许多实用的方法,比如 querySelector(),它是用于选择当前 HTML 页面内的任何 HTML 元素:
document.querySelector('h1');
window 表示当前窗口的浏览器,下面的指令与上面的相同:
window.document.querySelector('h1');
不管怎样,下面的语法更常见,在下一节中咱们将大量使用这种形式:
document.methodName();
除了 querySelector() 用于选择 HTML 元素之外,还有很多更有用的方法
// 返回单个元素 document.getElementById('testimonials'); // 返回一个 HTMLCollection document.getElementsByTagName('p'); // 返回一个节点列表 document.querySelectorAll('p');
咱们不仅可以选 择HTML 元素,还可以交互和修改它们的内部状态。例如,希望读取或更改给定元素的内部内容:
// Read or write document.querySelector('h1').innerHtml; // Read document.querySelector('h1').innerHtml=''; // Write! Ouch!
DOM 中的每个 HTML 元素也是一个“节点”,实际上咱们可以像这样检查节点类型:
document.querySelector('h1').nodeType;
上述结果返回 1,表示是 Element 类型的节点的标识符。咱们还可以检查节点名:
document.querySelector('h1').nodeName; "H1"
这里,节点名以大写形式返回。通常我们处理 DOM 中的四种类型的节点
由于元素是节点,节点可以有属性(properties )(也称为attributes),咱们可以检查和操作这些属性:
// 返回 true 或者 false document.querySelector('a').hasAttribute('href'); // 返回属性文本内容,或 null document.querySelector('a').getAttribute('href'); // 设置给定的属性 document.querySelector('a').setAttribute('href', 'someLink');
前面我们说过 DOM 是一个类似于树的结构。这种特性也反映在 HTML 元素上。每个元素都可能有父元素和子元素,我们可以通过检查元素的某些属性来查看它们:
// 返回一个 HTMLCollection document.children; // 返回一个节点列表 document.childNodes; // 返回一个节点 document.querySelector('a').parentNode; // 返回HTML元素 document.querySelector('a').parentElement;
了解了如何选择和查询 HTML 元素。那创建元素又是怎么样?为了创建 Element 类型的新节点,原生 DOM API 提供了 createElement 方法:
var heading=document.createElement('h1');
使用 createTextNode 创建文本节点:
var text=document.createTextNode('Hello world');
通过将 text 附加到新的 HTML 元素中,可以将这两个节点组合在一起。最后,还可以将heading元素附加到根文档中:
var heading=document.createElement('h1'); var text=document.createTextNode('Hello world'); heading.appendChild(text); document.body.appendChild(heading);
还可以使用 remove() 方法从 DOM 中删除节点。在元素上调用方法,该节点将从页面中消失:
document.querySelector('h1').remove();
这些是咱们开始在浏览器中使用 JS 操作 DOM 所需要知道的全部内容。在下一节中,咱们将灵活地使用 DOM,但首先要绕个弯,因为咱们还需要讨论“DOM事件”。
DOM 和事件
DOM 元素是很智能的。它们不仅可以包含文本和其他 HTML 元素,还可以“发出”和响应“事件”。浏览任何网站并打开浏览器控制台。使用以下命令选择一个元素:
document.querySelector('p')
看看这个属性
document.querySelector('p').onclick
它是什么类型:
typeof document.querySelector('p').onclick // "object"
"object"! 为什么它被称为“onclick”? 凭一点直觉我们可以想象它是元素上的某种神奇属性,能够对点击做出反应? 完全正确。
如果你感兴趣,可以查看任何 HTML 元素的原型链。会发现每个元素也是一个 Element,而元素又是一个节点,而节点又是一个EventTarget。可以使用 instanceof 来验证这一点。
document.querySelector('p') instanceof EventTarget // true
我很乐意称 EventTarget 为所有 HTML 元素之父,但在JS中没有真正的继承,它更像是任何 HTML 元素都可以看到另一个连接对象的属性。因此,任何 HTML 元素都具有与 EventTarget相同的特性:发布事件的能力。
但事件到底是什么呢?以 HTML 按钮为例。如果你点击它,那就是一个事件。有了这个.onclick对象,咱们可以注册事件,只要元素被点击,它就会运行。传递给事件的函数称为“事件监听器”或“事件句柄”。
事件和监听
在 DOM 中注册事件监听器有三种方法。第一种形式比较陈旧,应该避免,因为它耦合了逻辑操作和标签
<!-- 不好的方式 --> <button onclick="console.log('clicked')">喜欢,就点点我</button>
第二个选项依赖于以事件命名的对象。例如,咱们可以通过在对象.onclick上注册一个函数来监听click事件:
document.querySelector("button").onclick=handleClick; function handleClick() { console.log("Clicked!"); }
此语法更加简洁,是内联处理程序的不错替代方案。还有另一种基于addEventListener的现代形式:
document.querySelector("button").addEventListener("click", handleClick); function handleClick() { console.log("Clicked!"); }
就我个人而言,我更喜欢这种形式,但如果想争取最大限度的浏览器兼容性,请使用 .on 方式。现在咱们已经有了一 个 HTML 元素和一个事件监听器,接着进一步研究一下 DOM 事件。
事件对象、事件默认值和事件冒泡
作为事件处理程序传递的每个函数默认接收一个名为“event”的对象
var button=document.querySelector("button"); button.addEventListener("click", handleClick); function handleClick() { console.log(event); }
它可以直接在函数体中使用,但是在我的代码中,我更喜欢将它显式地声明为参数:
function handleClick(event) { console.log(event); }
事件对象是“必须要有的”,因为咱们可以通过调用事件上的一些方法来控制事件的行为。事件实际上有特定的特征,尤其是“默认”和“冒泡”。考虑一 个HTML 链接。使用以下标签创建一个名为click-event.html的新HTML文件:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Click event</title> </head> <body> <div> <a href="/404.html">click me!</a> </div> </body> <script src="click-event.js"></script> </html>
在浏览器中运行该文件并尝试单击链接。它将跳转到一个404的界面。链接上单击事件的默认行为是转到href属性中指定的实际页面。但如果我告诉你有办法阻止默认值呢?输入preventDefault(),该方法可用于事件对象。使用以下代码创建一个名为click-event.js的新文件:
var button=document.querySelector("a"); button.addEventListener("click", handleClick); function handleClick(event) { event.preventDefault(); }
在浏览器中刷新页面并尝试现在单击链接:它不会跳转了。因为咱们阻止了浏览器的“事件默认” 链接不是默认操作的惟一HTML 元素,表单具有相同的特性。
当 HTML 元素嵌套在另一个元素中时,还会出现另一个有趣的特性。考虑以下 HTML
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Nested events</title> </head> <body> <div id="outer"> I am the outer div <div id="inner"> I am the inner div </div> </div> </body> <script src="nested-events.js"></script> </html>
和下面的 JS 代码:
// nested-events.js var outer=document.getElementById('inner'); var inner=document.getElementById('outer'); function handleClick(event){ console.log(event); } inner.addEventListener('click', handleClick); outer.addEventListener('click', handleClick);
有两个事件监听器,一个用于外部 div,一个用于内部 div。准确地点击内部div,你会看到:
两个事件对象被打印。这就是事件冒泡在起作用。它看起来像是浏览器行为中的一个 bug,使用 stopPropagation() 方法可以禁用,这也是在事件对象上调用的
// function handleClick(event) { event.stopPropagation(); console.log(event); } ///
尽管看起来实现效果很差,但在注册过多事件监听器确实对性能不利的情况下,冒泡还是会让人眼前一亮。考虑以下示例:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Event bubbling</title> </head> <body> <ul> <li>one</li> <li>two</li> <li>three</li> <li>four</li> <li>five</li> </ul> </body> <script src="event-bubbling.js"></script> </html>
如果要兼听列表的点击事件,需要在列表中注册多少事件监听器?答案是:一个。只需要一个在ul上注册的侦听器就可以截获任何li上的所有单击:
// event-bubbling.js var ul=document.getElementsByTagName("ul")[0]; function handleClick(event) { console.log(event); } ul.addEventListener("click", handleClick);
可以看到,事件冒泡是提高性能的一种实用方法。实际上,对浏览器来说,注册事件监听器是一项昂贵的操作,而且在出现大量元素列表的情况下,可能会导致性能损失。
用 JS 生成表格
现在咱们开始编码。给定一个对象数组,希望动态生成一个HTML 表格。HTML 表格由 <table> 元素表示。每个表也可以有一个头部,由 <thead> 元素表示。头部可以有一个或多个行 <tr>,每个行都有一个单元格,由一个 <th>元 素表示。如下所示:
<table> <thead> <tr> <th>name</th> <th>height</th> <th>place</th> </tr> </thead> <!-- more stuff here! --> </table>
不止这样,大多数情况下,每个表都有一个主体,由 <tbody> 定义,而 <tbody> 又包含一组行<tr>。每一行都可以有包含实际数据的单元格。表单元格由<td>定义。完整如下所示:
<table> <thead> <tr> <th>name</th> <th>height</th> <th>place</th> </tr> </thead> <tbody> <tr> <td>Monte Falco</td> <td>1658</td> <td>Parco Foreste Casentinesi</td> </tr> <tr> <td>Monte Falterona</td> <td>1654</td> <td>Parco Foreste Casentinesi</td> </tr> </tbody> </table>
现在的任务是从 JS 对象数组开始生成表格。首先,创建一个名为build-table.html的新文件,内容如下:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Build a table</title> </head> <body> <table> <!-- here goes our data! --> </table> </body> <script src="build-table.js"></script> </html>
在相同的文件夹中创建另一个名为build-table.js的文件,并使用以下数组开始:
"use strict"; var mountains=[ { name: "Monte Falco", height: 1658, place: "Parco Foreste Casentinesi" }, { name: "Monte Falterona", height: 1654, place: "Parco Foreste Casentinesi" }, { name: "Poggio Scali", height: 1520, place: "Parco Foreste Casentinesi" }, { name: "Pratomagno", height: 1592, place: "Parco Foreste Casentinesi" }, { name: "Monte Amiata", height: 1738, place: "Siena" } ];
考虑这个表格。首先,咱们需要一个 <thead>:
document.createElement('thead')
这没有错,但是仔细查看MDN的表格文档会发现一个有趣的细节。<table> 是一个 HTMLTableElement,它还包含有趣方法。其中最有用的是HTMLTableElement.createTHead(),它可以帮助创建咱们需要的 <thead>。
首先,编写一个生成 thead 标签的函数 generateTableHead
function generateTableHead(table) { var thead=table.createTHead(); }
该函数接受一个选择器并在给定的表上创建一个 <thead>:
function generateTableHead(table) { var thead=table.createTHead(); } var table=document.querySelector("table"); generateTableHead(table);
在浏览器中打开 build-table.html:什么都没有.但是,如果打开浏览器控制台,可以看到一个新的 <thead> 附加到表。
接着填充 header 内容。首先要在里面创建一行。还有另一个方法可以提供帮助:HTMLTableElement.insertRow()。有了这个,咱们就可以扩展方法了:
function generateTableHead (table) { var thead=table,createThead(); var row=thead.insertRow(); }
此时,我们可以生成我们的行。通过查看源数组,可以看到其中的任何对象都有咱们需要信息:
var mountains=[ { name: "Monte Falco", height: 1658, place: "Parco Foreste Casentinesi" }, { name: "Monte Falterona", height: 1654, place: "Parco Foreste Casentinesi" }, { name: "Poggio Scali", height: 1520, place: "Parco Foreste Casentinesi" }, { name: "Pratomagno", height: 1592, place: "Parco Foreste Casentinesi" }, { name: "Monte Amiata", height: 1738, place: "Siena" } ];
这意味着咱们可以将另一个参数传递给我们的函数:一个遍历以生成标题单元格的数组:
function generateTableHead(table, data) { var thead=table.createTHead(); var row=thead.insertRow(); for (var i=0; i < data.length; i++) { var th=document.createElement("th"); var text=document.createTextNode(data[i]); th.appendChild(text); row.appendChild(th); } }
不幸的是,没有创建单元格的原生方法,因此求助于document.createElement("th")。同样值得注意的是,document.createTextNode(data[i])用于创建文本节点,appendChild()用于向每个标记添加新元素。
当以这种方式创建和操作元素时,我们称之为“命令式” DOM 操作。现代前端库通过支持“声明式”方法来解决这个问题。我们可以声明需要哪些 HTML 元素,而不是一步一步地命令浏览器,其余的由库处理。
回到我们的代码,可以像下面这样使用第一个函数
var table=document.querySelector("table"); var data=Object.keys(mountains[0]); generateTableHead(table, data);
现在我们可以进一步生成实际表的数据。下一个函数将实现一个类似于generateTableHead的逻辑,但这一次咱们需要两个嵌套的for循环。在最内层的循环中,使用另一种原生方法来创建一系列td。方法是HTMLTableRowElement.insertCell()。在前面创建的文件中添加另一个名为generateTable的函数
function generateTable(table, data) { for (var i=0; i < data.length; i++) { var row=table.insertRow(); for (var key in data[i]) { var cell=row.insertCell(); var text=document.createTextNode(data[i][key]); cell.appendChild(text); } } }
调用上面的函数,将 HTML表 和对象数组作为参数传递:
generateTable(table, mountains);
咱们深入研究一下 generateTable 的逻辑。参数 data 是一个与 mountains 相对应的数组。最外层的 for 循环遍历数组并为每个元素创建一行:
function generateTable(table, data) { for (var i=0; i < data.length; i++) { var row=table.insertRow(); // omitted for brevity } }
最内层的循环遍历任何给定对象的每个键,并为每个对象创建一个包含键值的文本节点
function generateTable(table, data) { for (var i=0; i < data.length; i++) { var row=table.insertRow(); for (var key in data[i]) { // inner loop var cell=row.insertCell(); var text=document.createTextNode(data[i][key]); cell.appendChild(text); } } }
最终代码:
var mountains=[ { name: "Monte Falco", height: 1658, place: "Parco Foreste Casentinesi" }, { name: "Monte Falterona", height: 1654, place: "Parco Foreste Casentinesi" }, { name: "Poggio Scali", height: 1520, place: "Parco Foreste Casentinesi" }, { name: "Pratomagno", height: 1592, place: "Parco Foreste Casentinesi" }, { name: "Monte Amiata", height: 1738, place: "Siena" } ]; function generateTableHead(table, data) { var thead=table.createTHead(); var row=thead.insertRow(); for (var i=0; i < data.length; i++) { var th=document.createElement("th"); var text=document.createTextNode(data[i]); th.appendChild(text); row.appendChild(th); } } function generateTable(table, data) { for (var i=0; i < data.length; i++) { var row=table.insertRow(); for (var key in data[i]) { var cell=row.insertCell(); var text=document.createTextNode(data[i][key]); cell.appendChild(text); } } }
其中调用:
var table=document.querySelector("table"); var data=Object.keys(mountains[0]); generateTable(table, mountains); generateTableHead(table, data);
执行结果:
当然,咱们的方法还可以该进,下个章节将介绍。
总结
DOM 是 web 浏览器保存在内存中的 web 页面的虚拟副本。DOM 操作是指从 DOM 中创建、修改和删除 HTML 元素的操作。在过去,咱们常常依赖 jQuery 来完成更简单的任务,但现在原生 API 已经足够成熟,可以让 jQuery 过时了。另一方面,jQuery 不会很快消失,但是每个 JS 开发人员都必须知道如何使用原生 API 操作 DOM。
这样做的理由有很多,额外的库增加了加载时间和 JS 应用程序的大小。更不用说 DOM 操作在面试中经常出现。
DOM 中每个可用的 HTML 元素都有一个接口,该接口公开一定数量的属性和方法。当你对使用何种方法有疑问时,参考MDN文档。操作 DOM 最常用的方法是 document.createElement()用于创建新的 HTML 元素,document.createTextNode() 用于在 DOM 中创建文本节点。最后但同样重要的是 .appendchild(),用于将新的 HTML 元素或文本节点附加到现有元素。
HTML 元素还能够发出事件,也称为DOM事件。值得注意的事件为“click”、“submit”、“drag”、“drop”等等。DOM 事件有一些特殊的行为,比如“默认”和冒泡。
JS 开发人员可以利用这些属性,特别是对于事件冒泡,这些属性对于加速 DOM 中的事件处理非常有用。虽然对原生 API 有很好的了解是件好事,但是现代前端库提供了不容置疑的好处。用 Angular、React 和 Vue 来构建一个大型的JS应用程序确实是可行的。
代码部署后可能存在的BUG没法实时知道,事后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具 Fundebug。
原文:https://github.com/valentinogagliardi/Little-JavaScript-Book/blob/v1.0.0/manuscript/chapter8.md
如今,Javascript 模块化规范非常方便、自然,但这个新规范仅执行了2年,就在 4 年前,js 的模块化还停留在运行时支持,10 年前,通过后端模版定义、注释定义模块依赖。对经历过来的人来说,历史的模块化方式还停留在脑海中,反而新上手的同学会更快接受现代的模块化规范。
但为什么要了解 Javascript 模块化发展的历史呢?因为凡事都有两面性,了解 Javascript 模块化规范,有利于我们思考出更好的模块化方案,纵观历史,从 1999 年开始,模块化方案最多维持两年,就出现了新的替代方案,比原有的模块化更清晰、强壮,我们不能被现代模块化方式限制住思维,因为现在的 ES2015 模块化方案距离发布也仅仅过了两年。
直接定义依赖 (1999): 由于当时 js 文件非常简单,模块化方式非常简单粗暴 —— 通过全局方法定义、引用模块。这种定义方式与现在的 commonjs 非常神似,区别是 commonjs 以文件作为模块,而这种方法可以在任何文件中定义模块,模块不与文件关联。
闭包模块化模式 (2003): 用闭包方式解决了变量污染问题,闭包内返回模块对象,只需对外暴露一个全局变量。
模版依赖定义 (2006): 这时候开始流行后端模版语法,通过后端语法聚合 js 文件,从而实现依赖加载,说实话,现在 go 语言等模版语法也很流行这种方式,写后端代码的时候不觉得,回头看看,还是挂在可维护性上。
注释依赖定义 (2006): 几乎和模版依赖定义同时出现,与 1999 年方案不同的,不仅仅是模块定义方式,而是终于以文件为单位定义模块了,通过 lazyjs 加载文件,同时读取文件注释,继续递归加载剩下的文件。
外部依赖定义 (2007): 这种定义方式在 cocos2d-js 开发中普遍使用,其核心思想是将依赖抽出单独文件定义,这种方式不利于项目管理,毕竟依赖抽到代码之外,我是不是得两头找呢?所以才有通过 webpack 打包为一个文件的方式暴力替换为 commonjs 的方式出现。
Sandbox模式 (2009): 这种模块化方式很简单,暴力,将所有模块塞到一个 sanbox 变量中,硬伤是无法解决明明冲突问题,毕竟都塞到一个 sandbox 对象里,而 Sandbox 对象也需要定义在全局,存在被覆盖的风险。模块化需要保证全局变量尽量干净,目前为止的模块化方案都没有很好的做到这一点。
依赖注入 (2009): 就是大家熟知的 angular1.0,依赖注入的思想现在已广泛运用在 react、vue 等流行框架中。但依赖注入和解决模块化问题还差得远。
CommonJS (2009): 真正解决模块化问题,从 node 端逐渐发力到前端,前端需要使用构建工具模拟。
Amd (2009): 都是同一时期的产物,这个方案主要解决前端动态加载依赖,相比 commonJs,体积更小,按需加载。
Umd (2011): 兼容了 CommonJS 与 Amd,其核心思想是,如果在 commonjs 环境(存在 module.exports,不存在 define),将函数执行结果交给 module.exports 实现 Commonjs,否则用 Amd 环境的 define,实现 Amd。
Labeled Modules (2012): 和 Commonjs 很像了,没什么硬伤,但生不逢时,碰上 Commonjs 与 Amd,那只有被人遗忘的份了。
YModules (2013): 既然都出了 Commonjs Amd,文章还列出了此方案,一定有其独到之处。其核心思想在于使用 provide 取代 return,可以控制模块结束时机,处理异步结果;拿到第二个参数 module,修改其他模块的定义(虽然很有拓展性,但用在项目里是个搅屎棍)。
ES2015 Modules (2015): 就是我们现在的模块化方案,还没有被浏览器实现,大部分项目已通过 babel 或 typescript 提前体验。
从语言层面到文件层面的模块化
从 1999 年开始,模块化探索都是基于语言层面的优化,真正的革命从 2009 年 CommonJS 的引入开始,前端开始大量使用预编译。
这篇文章所提供的模块化历史的方案都是逻辑模块化,从 CommonJS 方案开始前端把服务端的解决方案搬过来之后,算是看到标准物理与逻辑统一的模块化。但之后前端工程不得不引入模块化构建这一步。正是这一步给前端开发无疑带来了诸多的不便,尤其是现在我们开发过程中经常为了优化这个工具带了很多额外的成本。
从 CommonJS 之前其实都只是封装,并没有一套模块化规范,这个就有些像类与包的概念。我在10年左右用的最多的还是 YUI2,YUI2 是用 namespace 来做模块化的,但有很多问题没有解决,比如多版本共存,因此后来 YUI3 出来了。
YUI().use('node', 'event', function (Y) { // The Node and Event modules are loaded and ready to use. // Your code goes here! });
YUI3 的 sandbox 像极了差不多同时出现的 AMD 规范,但早期 yahoo 在前端圈的影响力还是很大的,而 requirejs 到 2011 年才诞生,因此圈子不是用着 YUI 要不就自己封装一套 sandbox,内部使用 jQuery。
为什么模块化方案这么晚才成型,可能早期应用的复杂度都在后端,前端都是非常简单逻辑。后来 Ajax 火了之后,web app 概念的开始流行,前端的复杂度也呈指数级上涨,到今天几乎和后端接近一个量级。工程发展到一定阶段,要出现的必然会出现。
前端三剑客的模块化展望
从 js 模块化发展史,我们还看到了 css html 模块化方面的严重落后,如今依赖编译工具的模块化增强在未来会被标准所替代。
原生支持的模块化,解决 html 与 css 模块化问题正是以后的方向。
再回到 JS 模块化这个主题,开头也说到是为了构建 scope,实则提供了业务规范标准的输入输出的方式。但文章中的 JS 的模块化还不等于前端工程的模块化,Web 界面是由 HTML、CSS 和 JS 三种语言实现,不论是 CommonJS 还是 AMD 包括之后的方案都无法解决 CSS 与 HTML 模块化的问题。
对于 CSS 本身它就是 global scope,因此开发样式可以说是喜忧参半。近几年也涌现把 HTML、CSS 和 JS 合并作模块化的方案,其中 react/css-modules 和 vue 都为人熟知。当然,这一点还是非常依赖于 webpack/rollup 等构建工具,让我们意识到在 browser 端还有很多本质的问题需要推进。
对于 css 模块化,目前不依赖预编译的方式是 styled-component,通过 js 动态创建 class。而目前 css 也引入了与 js 通信的机制 与 原生变量支持。未来 css 模块化也很可能是运行时的,所以目前比较看好 styled-component 的方向。
对于 html 模块化,小尤最近爆出与 chrome 小组调研 html Modules,如果 html 得到了浏览器,编辑器的模块化支持,未来可能会取代 jsx 成为最强大的模块化、模板语言。
对于 js 模块化,最近出现的 <script type="module"> 方式,虽然还没有得到浏览器原生支持,但也是我比较看好的未来趋势,这样就连 webpack 的拆包都不需要了,直接把源代码传到服务器,配合 http2.0 完美抛开预编译的枷锁。
上述三中方案都不依赖预编译,分别实现了 html、css、js 模块化,相信这就是未来。
模块化标准推进速度仍然缓慢
2015 年提出的标准,在 17 年依然没有得到实现,即便在 nodejs 端。
这几年 TC39 对语言终于重视起来了,慢慢有动作了,但针对模块标准制定的速度,与落实都非常缓慢,与 javascript 越来越流行的趋势逐渐脱节。nodejs 至今也没有实现 ES2015 模块化规范,所有 jser 都处在构建工具的阴影下。
Http 2.0 对 js 模块化的推动
js 模块化定义的再美好,浏览器端的支持粒度永远是瓶颈,http 2.0 正是考虑到了这个因素,大力支持了 ES 2015 模块化规范。
幸运的是,模块化构建将来可能不再需要。随着 HTTP/2 流行起来,请求和响应可以并行,一次连接允许多个请求,对于前端来说宣告不再需要在开发和上线时再做编译这个动作。
几年前,模块化几乎是每个流行库必造的轮子(YUI、Dojo、Angular),大牛们自己爽的同时其实造成了社区的分裂,很难积累。有了 ES2015 Modules 之后,JS 开发者终于可以像 Java 开始者十年前一样使用一致的方式愉快的互相引用模块。
不过 ES2015 Modules 也只是解决了开发的问题,由于浏览器的特殊性,还是要经过繁琐打包的过程,等 Import,Export 和 HTTP 2.0 被主流浏览器支持,那时候才是彻底的模块化。
Http 2.0 后就不需要构建工具了吗?
看到大家基本都提到了 HTTP/2,对这项技术解决前端模块化及资源打包等工程问题抱有非常大的期待。很多人也认为 HTTP/2 普及后,基本就没有 Webpack 什么事情了。
不过 Webpack 作者 @sokra 在他的文章 webpack & HTTP/2 里提到了一个新的 Webpack 插件 AggressiveSplittingPlugin。简单的说,这款插件就是为了充分利用 HTTP/2 的文件缓存能力,将你的业务代码自动拆分成若干个数十 KB 的小文件。后续若其中任意一个文件发生变化,可以保证其他的小 chunck 不需要重新下载。
可见,即使不断的有新技术出现,也依然需要配套的工具来将前端工程问题解决方案推向极致。
模块化是大型项目的银弹吗?
只要遵循了最新模块化规范,就可以使项目具有最好的可维护性吗? Js 模块化的目的是支持前端日益上升的复杂度,但绝不是唯一的解决方案。
分析下 JavaScript 为什么没有模块化,为什么又需要模块化:这个 95 年被设计出来的时候,语言的开发者根本没有想到它会如此的大放异彩,也没有将它设计成一种模块化语言。按照文中的说法,99 年也就是 4 年后开始出现了模块化的需求。如果只有几行代码用模块化是扯,初始的 web 开发业务逻辑都写在 server 端,js 的作用小之又小。而现在 spa 都出现了,几乎所有的渲染逻辑都在前端,如果还是没有模块化的组织,开发过程会越来越难,维护也是更痛苦。
本文中已经详细说明了模块化的发展和优劣,这里不准备做过多的赘述。但还有一个问题需要我们去关注,那就是在模块化之后还有一个模块间耦合的问题,如果模块间耦合度大也会降低代码的可重用性或者说复用性。所以也出现了降低耦合的观察者模式或者发布/订阅模式。这对于提升代码重用,复用性和避免单点故障等都很重要。说到这里,还想顺便提一下最近流行起来的响应式编程(RxJS),响应式编程中有一个很核心的概念就是 observable,也就是 Rx 中的流(stream)。它可以被 subscribe,其实也就是观察者设计模式。
未来前端复杂度不断增加已成定论,随着后端成熟,自然会将焦点转移到前端领域,而且服务化、用户体验越来越重要,前端体验早不是当初能看就行,任何网页的异常、视觉的差异,或文案的模糊,都会导致用户流失,支付中断。前端对公司营收的影响,渐渐与后端服务宕机同等严重,所以前端会越来越重,异常监控,性能检测,工具链,可视化等等都是这几年大家逐渐重视起来的。
我们早已不能将 javascript 早期玩具性质的模块化方案用于现代越来越重要的系统中,前端界必然出现同等重量级的模块化管理方案,感谢 TC39 制定的 ES2015 模块化规范,我们已经离不开它,哪怕所有人必须使用 babel。
话说回来,标准推进的太慢,我们还是把编译工具当作常态,抱着哪怕支持了 ES2015 所有特性,babel 依然还有用的心态,将预编译进行到底。一句话,模块化仍在路上。js 模块化的矛头已经对准了 css 与 html,这两位元老也该向前卫的 js 学习学习了。
未来 css、html 的模块化会自立门户,还是赋予 js 更强的能力,让两者的模块化依附于 js 的能力呢?目前 html 有自立门户的苗头(htmlModules),而 css 迟迟没有改变,社区出现的 styled-component 已经用 js 将 css 模块化得很好了,最新 css 规范也支持了与 js 的变量通信,难道希望依附于 js 吗?这里希望得到大家更广泛的讨论。
我也认同,毕竟压缩、混淆、md5、或者利用 nonce 属性对 script 标签加密,都离不开本地构建工具。
据说 http2 的优化中,有个最佳文件大小与数量的比例,那么还是脱离不了构建工具,前端未来会越来越复杂,同时也越来越美好。
至此,对于 javascript 模块化讨论已接近尾声,对其优缺点也基本达成了一致。前端复杂度不断提高,促使着模块化的改进,代理(浏览器、node) 的支持程度,与前端特殊性(流量、缓存)可能前端永远也离不开构建工具,新的标准会让这些工作做的更好,同时取代、增强部分特征,前端的未来是更加美好的,复杂度也更高。
*请认真填写需求信息,我们会在24小时内与您取得联系。