理解虚拟DOM

正文

我最近写了关于DOMshadow DOM具体是什么,以及他们的差异。回顾一下,文档对象模型是一个HTML文档基于对象的表示,和操作对象的一个接口。影子DOM(shadow DOM)可以想做是”精简版”的DOM。它也是HTML元素基于对象的表示,但不是完整独立的文档。相反的,影子DOM允许我们分离我们的DOM成为更小的、具备封装性的单元并在HTML文档中使用。

另一个相似的术语你可能遇见过的是“虚拟DOM”(virtual DOM)。尽管这个概念已经出现了许多年,让它变得流行起来的还是在React框架中的使用。在这篇文章中,我会涉及虚拟DOM具体是什么,和原始的DOM有什么差别,以及如何使用。

为什么我们需要虚拟DOM?

为了理解为什么会出现虚拟DOM,先回顾一下原始DOM。就如前面提到的,DOM中有两部分内容,基于对象的HTML文档标识和操作对象的API。

例如,让我们看一个无序列表和列表项的简单HTML文档

1
2
3
4
5
6
7
8
9
<!doctype html>
<html lang="en">
<head></head>
<body>
<ul class="list">
<li class="list__item">List item</li>
</ul>
</body>
</html>

文档会像下面的DOM树进行表示

1
2
3
4
5
6
7
html
|
|-- head lang="en"
|-- body
|-- ul class="list"
|--li class="list__item"
|--"List item"

设想一下我们想要修改第一个列表项的内容为“List item one”,并且添加第二个列表项。为了做到这一点,我们需要使用DOM API查找到想要更新的元素,创建新的元素,添加属性和内容,最终更新DOM元素。

1
2
3
4
5
6
7
8
const listItemOne = document.getElementsByClassName("list__item")[0];
listItemOne.textContent = "List item one";

const list = document.getElementsByClassName("list")[0];
const listItemTwo = document.createElement("li");
listItemTwo.classList.add("list__item");
listItemTwo.textContent = "List item two";
list.appendChild(listItemTwo);

DOM无法解决的问题

当1998年DOM的第一版规范发布时,我们构建和管理网页与现在有很大的不同。那时并没有像现在这样依赖于DOM API来创建和更新页面内容。

document.getElementsByClassName()这样的简单方法小范围使用是好的,但是如果我们每几秒去更新页面中的多个元素时,这会让查询和更新DOM变得耗性能。

更进一步,由于API的组织方式,通常执行一些更耗性能的操作,例如更新文档的一大块内容比查找然后更新具体的元素要来得简单。回到我们的例子,某种程度上替换整个无序列表成新的比修改具体的元素要容易。

1
2
3
4
5
const list = document.getElementsByClassName("list")[0];
list.innerHTML = `
<li class="list__item">List item one</li>
<li class="list__item">List item two</li>
`;

在这个例子中,两个方法之间的性能差异可能并不明显。但是,随着页面大小的增加,仅选择和修改必须的内容变得尤其重要。

虚拟DOM的解决方式

虚拟DOM的提出就是为了高效解决这些频繁更新DOM的难题。不像DOM或者影子DOM,虚拟DOM并不是官方的规范,而是和DOM交互的新方法。

一个虚拟DOM可以设想为原始DOM的一份拷贝。这份拷贝可以不使用DOM的API而进行频繁的操作和更新。一旦所有的更新都被应用到虚拟DOM后,我们可以找到要应用到原始DOM上的具体变化差异并通过高效和有针对性的进行更新。

虚拟DOM看起来像什么?

虚拟DOM的名字让这个概念给人有点神秘的感觉。实际上,虚拟DOM就是一个常规的JavaScript对象。

让我们回顾一下之前创建的DOM树:

1
2
3
4
5
6
7
html
|
|-- head lang="en"
|-- body
|-- ul class="list"
|--li class="list__item"
|--"List item"

这颗树可以通过JavaScript对象来表示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const vdom = {
tagName: "html",
children: [
{ tagName: "head" },
{
tagName: "body",
children: [
{
tagName: "ul",
attributes: { "class": "list" },
children: [
{
tagName: "li",
attributes: { "class": "list__item" },
textContent: "List item"
}
]
}
]
}
]
}

我们可以设想这个对象作为我们的虚拟DOM。就像原始DOM,这也是基于对象的HTML文档表示。但是因为这是一个纯粹的JavaScript对象,我们可以自由和频繁的操作而不用接触实际的DOM,直到需要的时候。

通常使用多个小块的虚拟DOM,而不是使用一个虚拟DOM来表示整个对象。例如,我们在使用一个list组件,对应我们的无序列表元素。

1
2
3
4
5
6
7
8
9
10
11
const list = {
tagName: "ul",
attributes: { "class": "list" },
children: [
{
tagName: "li",
attributes: { "class": "list__item" },
textContent: "List item"
}
]
}

虚拟DOM的工作原理

现在我们看到了虚拟DOM的结构,接下来看一下它是如何在DOM中解决性能和使用问题的。

就如我提到的,我们可以使用虚拟DOM找到需要应用到DOM中的具体变更内容,然后单独更新这些变更。让我们回到无序列表的例子并做一些之前使用DOM API操作的变更。

首先我们要创建一份虚拟DOM的拷贝,包含我们想要的修改。因为我们不需要使用DOM API,我们可以只创建一个新的对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const copy = {
tagName: "ul",
attributes: { "class": "list" },
children: [
{
tagName: "li",
attributes: { "class": "list__item" },
textContent: "List item one"
},
{
tagName: "li",
attributes: { "class": "list__item" },
textContent: "List item two"
}
]
};

这份拷贝是用于创建一个和原始虚拟DOM的变更内容(“diff”),在这里是列表和更新的列表。变更内容如下:

1
2
3
4
5
6
7
8
9
10
11
const diffs = [
{
newNode: { /* 新版的列表1 */ },
oldNode: { /* 原始版本的列表1 */ },
index: /* 在父元素的子节点列表中元素索引 */
},
{
newNode: { /* 列表2 */ },
index: { /* */ }
}
]

变更内容提供了如何更新实际DOM的指南。一旦所有的变更内容都收集到了,我们就可以批量更新DOM,而且是仅更新必需的内容。

例如,我们可以遍历每一个的变更内容,然后添加一个新元素或者基于变更内容更新旧的内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const domElement = document.getElementsByClassName("list")[0];

diffs.forEach((diff) => {
const newElement = document.createElement(diff.newNode.tagName);
/* 添加属性 */

if (diff.oldNode) {
// 如果有旧的版本,用新的版本替换
domElement.replaceChild(newElement, diff.index);
} else {
// 如果没有旧的版本,创建一个新的节点
domElement.appendChild(newElement);
}
})

注意这里只是简单讲述虚拟DOM是如何工作的,有很多案例并没有涉及到。

虚拟DOM和框架

通常虚拟DOM会和框架一同使用,而不是像上面这样直接操作。

像React和Vue这样的框架使用虚拟DOM来执行高效的DOM更新。例如,我们的list组件通过React可以这样写:

1
2
3
4
5
6
import React from 'react';
import ReactDOM from 'react-dom';

const list = React.createElement("ul", { className: "list" },
React.createElement("li", { className: "list__item" }, "List item")
);

如果我们更新我们的列表,我们可以只是重写整个列表模板,然后再次调用ReactDOM.render()方法,传递一个新的列表。

1
2
3
4
5
6
const newList = React.createElement("ul", { className: "list" },
React.createElement("li", { className: "list__item" }, "List item one"),
React.createElement("li", { className: "list__item" }, "List item two")
);

setTimeout(() => ReactDOM.render(newList, document.body), 5000));

因为React使用了虚拟DOM,即使我们重新渲染了整个模板,只有实际的变化会被更新。如果通过开发者工具进行查看,可以查看具体的元素和具体元素的部分有变化。

DOM和虚拟DOM

总结一下,虚拟DOM是一个可以让我们和DOM元素进行简单高效的交互工具。它是DOM的JavaScript对象表示,我们可以根据我们的需要频繁的修改。对于这个对象的修改会被整理后,有针对性的对实际DOM进行更新。

Web Component入门

说明: 以下使用Web组件来表示Web Component

正文:

在web开发中,代码复用已成为一个聚焦重点。作为一名开发者,我们可能会遇到这样的场景,在多个地方使用一个代码片段来表示自定义的UI。如果我们不是很小心的写出,这可能会让整个代码结构变得不可管理。Web组件提供了一个原生的API来构建可复用的UI块。

什么是Web组件?

Web组件是一系列用来帮助我们创建可复用、具备封装的自定义HTML UI元素的浏览器底层的API。Web组件被认为更加好,是因为他们可以用任意的库或者框架创建,而且你可以立马使用原生JavaScript开始创建你自己的Web组件。

使用Web组件的一大优势在于他们已经在除了微软Edge外其他浏览器中可用,但是我们并不需要担心,因为已经有Polyfills可以解决这个问题。

Web组件由3个主要的技术组成,他们是主要支柱并作为API来构建Web组件。

  • 自定义元素(Custom Elements)
  • 模板(Templates)
  • 影子DOM(Shadow DOM)

让我们来进一步了解这些技术。

1. 自定义元素(Custom Elements)

这些是JavaScript API的集合,可以帮助你创建自己的HTML元素,并且控制你的DOM和行为。我们可以构建他们的层级和指示他们对行为变化做出的响应。例如,你可以创建一个元素像这样<my-element></my-element>

2. 模板(Templates)

模板是用户定义的模板在页面加载时并不渲染。之后可以通过创建一个组件实例来多次复用。

3. 影子DOM(Shadow DOM)

影子DOM是JavaScript API组合用以连接封装的DOM。这将会从主文档对象模型中独立渲染,并且他们的行为特性将会保持私有,因此代码片段就不会和代码结构中的其他部分冲突。使用影子DOM后CSS和JavaScript就会像<iframe>一样分离。

生命周期回调

生命周期回调是定义在自定义元素类定义中的函数。他们有自己唯一的定义目的。他们用于操作我们自定义元素的行为。

  • connectedCallback: 这个特殊的函数会在我们的自定义元素初始连接到DOM时进行调用。
  • adoptedCallback: 这个函数会在我们的自定义函数移动到一个新的文档时调用。
  • attributeChangedCallback: 如果在我们的自定义元素中有属性变化,例如属性的变更、增加或者删掉,这个特殊的函数会被调用。
  • disconnectedCallback: 这个特殊的函数当我们的自定义元素从DOM中断开时调用。

现在让我们来看看如何使用原生JavaScript来创建一个Web组件。通过做完这个教程,你可以了解Web组件。

实战

我们要构建什么?

我们要构建一个显示一张当前热门图片的Web组件。我们会使用Giphy API来获取gif,你的代码结构在实现完成后会是如下:

1
2
3
--index.html
--card.js
--services.js

构建教程

首先,我们要创建一个类来包含我们想创建的Web组件的行为。创建一个card.js的文件,并创建一个如下的类。

1
2
3
4
5
6
7
8
class CardComponent extends HTMLElement {
constructor (){
super();

//Your implementaion goes here

}
}

在类的构造函数中,你需要通过Element.attachShadow()方法将影子DOM的影子根(shadow root)添加到文档的当前HTML元素中。接着使用<template>标签在index.html文件中创建HTML模板。这个模板如下:

1
2
3
4
5
<template id="card-view">
<h1>Web Component</h1>
<p id="card-title">Example</p>
<img id="gif-view"/>
</template>

在添加模板到我们的index.html文件中后,我们可以使用DOM方法来克隆上面的模板并添加到我们的影子DOM。这需要在构造函数中完成。

1
2
3
4
5
6
7
8
9
10
11
class CardComponent extends HTMLElement {
constructor (){
super();
const shadow = this.attachShadow({mode: 'open'});

// Clone the template so that it can be attched to the shadowroot
const template = document.getElementById('card-view');
const templateInstance = template.content.cloneNode(true);
shadow.appendChild(templateInstance);
}
}

就像我之前提到的,我们应该再写一个函数来从Giphy API中获取gif。从API中我们将获取到当前热门的图片,以及这个图片上传者提供的标题。在我们开始写这个函数前,先创建一个单独的文件services.js用以放置URL和API key。创建文件并放置以下代码和你申请的API key。

1
2
3
const API_KEY = '*YOUR_API_KEY*';
const url = `http://api.giphy.com/v1/gifs/trending?api_key=` + API_KEY + `&limit=1`;
export {API_KEY, url}; // export the url so that i can be used extrnally.

创建services.js文件后,添加以下的代码到你的card.js文件顶部,这样你就可以使用URL来获取gif图片了。

1
import { url } from './services.js';

你可以从这个链接获取你自己的API key: https://developers.giphy.com/

跳回到card.js文件,并添加以下函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
render(shadowElem, data){
const shadowRoot = shadowElem.shadowRoot;
shadowRoot.getElementById('card-title').innerHTML = data.name;
shadowRoot.getElementById('gif-view').src = data.url;
}

async fetchFromGiphy (){
const res = await fetch(url);
const json = await res.json();
const gifUrl = json['data']['0'].images['fixed_height_small'].url;
const gifName = json['data']['0'].title;
const gifObject = {
name: gifName,
url: gifUrl
}
return gifObject;
}

让我来解释一下这些函数。

fetchFromGiphy(): 这个函数使用async/await获取热门的gif和这个gif的标题,并作为对象进行返回。

render(): 这个函数用于注入值到影子DOM的元素中。

接着,让这些函数在生命周期回调中被调用。实际上,我们需要当我们的自定义元素连接到DOM时调用这两个函数。我们有connectedCallback()函数来实现。

1
2
3
4
async connectedCallback() {
this.gifObj = await this.fetchFromGiphy();
this.render(this, this.gifObj);
}

最后,使用customElements.define()函数来定义我们的自定义元素。当定义一个自定义元素时,有一些基本原则需要记在心里。define()函数的第一个参数应该是代码自定义元素名称的字符串。他们不能是一个单独的单词,而是由-字符在中间。第二个参数我们的定义元素行为的类对象。

1
customElements.define(‘card-component’, CardComponent);

现在你已经定义了你的组件,添加card.js文件到你的index.html文件中。你可以在HTML文档的任意地方使用<card-component>元素。最后的index.html如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!DOCTYPE html>
<html lang="en">
<head>
<title>Web Component</title>
</head>
<body>

<template id="card-view">
<h1>Web Component</h1>
<p id="card-title">Example</p>
<img id="gif-view"/>
</template>

<card-component></card-component>
<script src="./card.js" type="module"></script>
</body>
</html>

为了运行,你需要一个服务器。从命令行中全局安装static-server:

1
npm install -g static-server

从你的Web组件项目目录下运行static-server命令:

1
static-server

好了,恭喜!你现在已经拥有你自己的组件。

结论

这篇文章总结了Web组件的基础。这是Web组件的理论和实现。Web组件在帮助代码复用上很有用。你可以从这个项目中检出所有代码。

ES6模块-团队分享

大纲

  • 前端模块化的历史
  • ES6模块的使用
  • ES6模块背后的思考
  • 组件和模块
  • 参考文章

前端模块化的历史

前端模块化的历史

  • 全局Function模式
  • 命名空间模式
  • IIFE模式
  • IIFE模式(引入依赖)
  • CommonJS
  • AMD
  • CMD
  • ES6 模块

ES6模块的使用

什么是模块?

模块是通过不同于脚本的形式进行加载的JavaScript文件。区别在于:

  1. 模块代码自动运行在严格模式,并无法退出
  2. 模块中定义的变量只在模块的作用域中存在,不会自动添加到全局的作用域
  3. 模块中的thisundefined
  4. 模块中不支持HTML风格的注释<!-- comments -->
  5. 模块必须要导出任何可以在模块外可用的一切
  6. 模块可以从其他模块导入内容

基本导出

使用export关键字可以从模块中导出变量、函数和类:

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
// export data
export var color = "red";
export let name = "Nicholas";
export const magicNumber = 7;

// export function
export function sum(num1, num2) {
return num1 + num1;
}

// export class
export class Rectangle {
constructor(length, width) {
this.length = length;
this.width = width;
}
}

// this function is private to the module
function subtract(num1, num2) {
return num1 - num2;
}

// define a function...
function multiply(num1, num2) {
return num1 * num2;
}

// ...and then export it later
export { multiply };

导出说明:

  1. 每个导出的函数和类都有一个名字,除了使用default关键字外无法使用上面的语法导出匿名函数和类
  2. 查看multiply()函数,你可以不导出声明,支持导出引用
  3. 查看substract()函数,外部不可访问,没有显式导出的变量、函数和类对于模块是私有的

基本导入

基本格式:

1
import { identifier1, identifier2 } from "./example.js";

说明: 从一个模块中导入一个绑定,就类似定义一个const的内容,也就是无法定义重名变量、导入前使用和修改值。

导入一个单一绑定

1
2
3
4
5
6
// import just one
import { sum } from "./example.js";

console.log(sum(1, 2)); // 3

sum = 1; // error

尽管example.js中定义了多个导出,你可以只导入其中一个;为了在浏览器和Node.js中保持兼容性,尽量在文件名前使用相对路径。

导入多个绑定

1
2
3
4
// import multiple
import { sum, multiply, magicNumber } from "./example.js";
console.log(sum(1, magicNumber)); // 8
console.log(multiply(1, 2)); // 2

全部导入

特殊的应用场景支持导入整个模块作为一个对象,类似导入一个库文件,所有的模块导出均作为对象属性。

1
2
3
4
5
// import everything
import * as example from "./example.js";
console.log(example.sum(1,
example.magicNumber)); // 8
console.log(example.multiply(1, 2)); // 2

不管多少次使用import对一个模块进行导入操作,模块只会执行一次;在导入模块的代码执行后,实例化的模块在保存在内存中并在其他import语句使用时复用。

1
2
3
import { sum } from "./example.js";
import { multiply } from "./example.js";
import { magicNumber } from "./example.js";

代码中example.js只会执行一次.

模块语法不支持在语句或者函数中使用,也就是说不支持动态的导入和导出。ECMAScript dynamic-import Stage3

导入绑定的修改

对导入的内容是无法修改的,但是在模块内部可以。

模块导出:

1
2
3
4
export var name = "Nicholas";
export function setName(newName) {
name = newName;
}

模块导入:

1
2
3
4
5
6
7
import { name, setName } from "./example.js";

console.log(name); // "Nicholas"
setName("Greg");
console.log(name); // "Greg"

name = "Nicholas"; // error

setName函数回到模块内执行并修改变量name的内容,随后这个修改会反映到导入的name这个绑定。

重命名导出和导入

有时候,你可能不想使用变量、函数、类在模块中的原始名字,你可以在导入和导出的时候使用as关键字来指定新的名字

导出时重命名

1
2
3
4
5
function sum(num1, num2) {
return num1 + num2;
}

export { sum as add };

导入

1
import { add } from "./example.js";

导入时重命名

1
2
3
import { add as sum } from "./example.js";
console.log(typeof add); // "undefined"
console.log(sum(1, 2)); // 3

模块中的默认值

模块中的默认值是指使用default指定的一个单独变量、函数或者类,每个模块中只能设定一个默认导出,指定多个默认导出会报语法错误。

导出默认值

直接导出

1
2
3
export default function(num1, num2) {
return num1 + num2;
}

函数可以不写名字,因为模块本身代表这个函数。

导出引用

1
2
3
4
5
function sum(num1, num2) {
return num1 + num2;
}

export default sum;

重命名导出

1
2
3
4
5
function sum(num1, num2) {
return num1 + num2;
}

export { sum as default };

导入默认值

javascript
// import the default
import sum from “./example.js”;

console.log(sum(1, 2)); // 3

1
2
3
4
5
6
7
8
9
10
11
12

1. 默认导入不需要大括号
2. 名称可以代表任意模块中的默认函数
3. 默认导入的语法是最精简的,ES6模块规范制定者希望默认值作为主流的使用场景

**具名和默认混合**
```javascript
export let color = "red";

export default function(num1, num2) {
return num1 + num2;
}
1
2
3
4
import sum, { color } from "./example.js";

console.log(sum(1, 2)); // 3
console.log(color); // "red"

默认值要在非默认值前面导入。

重命名默认值

1
2
3
4
5
// equivalent to previous example
import { default as sum, color } from "example";

console.log(sum(1, 2)); // 3
console.log(color); // "red"

重新导出一个绑定

几种重新导出的方式

1
2
import { sum } from "./example.js";
export { sum }
1
export { sum } from "./example.js";
1
export { sum as add } from "./example.js";

全部导出

1
export * from "./example.js";

这里只能导出所有的具名导出,不包括默认导出。如果要处理默认导出,你需要显式的导入后再导出。

没有绑定的导入

有些模块并没有导出任何内容,仅仅是在全局作用域中做了些修改;虽然模块无法直接修改全局作用域的变量、函数和类,但是对于內建对象的修改可以反映到其他模块中。

1
2
3
4
5
6
7
8
9
10
11
// module code without exports or imports
Array.prototype.pushAll = function(items) {

// items must be an array
if (!Array.isArray(items)) {
throw new TypeError("Argument must be an array.");
}

// use built-in push() and spread operator
return this.push(...items);
};

这个模块对Array原型增加一个pushAll方法.

1
2
3
4
5
6
import "./example.js";

let colors = ["red", "green", "blue"];
let items = [];

items.pushAll(colors);

这段代码导入和执行模块代码,添加pushAll方法到array原型中。

没有绑定的导入通常用于创建polyfill或者shim。

加载ES6 模块

  • 使用打包工具和Babel转译为ES5

  • 使用script标签,并设置typemodule(IE11不兼容,使用前查看兼容性)

默认使用defer属性,按照代码出现的顺序进行加载,执行需要在文档解析完成后,执行顺序也是按照代码出现的顺序。

1
2
3
4
5
6
7
8
9
10
11
12
<!-- this will execute first -->
<script type="module" src="module1.js"></script>

<!-- this will execute second -->
<script type="module">
import { sum } from "./example.js";

let result = sum(1, 2);
</script>

<!-- this will execute third -->
<script type="module" src="module2.js"></script>

如果模块代码中又引入其他的模块,执行的顺序会变得复杂一些:

加载顺序:

  1. Download and parse module1.js.
  2. Recursively download and parse import resources in module1.js.
  3. Parse the inline module.
  4. Recursively download and parse import resources in the inline module.
  5. Download and parse module2.js.
  6. Recursively download and parse import resources in module2.js

执行顺序:

  1. Recursively execute import resources for module1.js.
  2. Execute module1.js.
  3. Recursively execute import resources for the inline module.
  4. Execute the inline module.
  5. Recursively execute import resources for module2.js.
  6. Execute module2.js.
  • web worker
1
2
// load module.js as a module
let worker = new Worker("module.js", { type: "module" });

ES6模块背后的思考

对比CommonJS和AMD

CommonJS

在Node.js中的主要实现方式,特性如下:

  • 紧凑的语法
  • 同步加载
  • 主要使用: 服务端

Asynchronous Module Definition(AMD)

主要的实现是RequireJS,特性如下:

  • 稍微复杂的语法
  • 异步加载
  • 主要使用: 浏览器

ES6模块

目标:

  • 偏爱默认导出
  • 静态模块结构
  • 支持同步和异步加载
  • 支持模块间的循环依赖

优势:

  • 比CommonJS更紧凑的语法
  • 结构可以被静态分析(静态检测、优化等)
  • 对循环引用比CommonJS更好
  • 支持异步加载

八卦: ES6 Module的主要设计者Dave Herman和Sam Tobin Hochstadt。

组件和模块

  • 模块为基础,组件基于模块
  • 组件侧重UI封装,针对,包括html,css,js和image;模块侧重数据、功能封装

组件化参考张云龙-前端工程

参考文章

ES6中的模块化

这里主要是针对ES6 Module技术出现的讨论,对比历史的一些模块化解决方案。

背景、初衷、目标

背景

简单的讲,随着前端应用的日益庞大和复杂,对于代码的拆分复用要求更高,也就出现了对代码模块化的需求。

在ES6 Module出现前,社区中出现的两个方案CommonJS和AMD:

CommonJS

在Node.js中的主要实现方式,特性如下:

  • 紧凑的语法
  • 同步加载
  • 主要使用: 服务端

Asynchronous Module Definition(AMD)

主要的实现是RequireJS,特性如下:

  • 稍微复杂的语法
  • 异步加载
  • 主要使用: 浏览器

这里提一句,ES6 Module的主要设计者Dave Herman 和 Sam Tobin Hochstadt。

初衷

这里讲的ES6 Module的设计初衷,在ES6 Module出现前社区已经在模块化上有一些令人印象深刻的变通方案,以CommonJS和AMD为代表,这两者各有优缺点并且不兼容,因此ES6 Module的设计初衷是为了吸取这两者的优点实现ECMAScript的标准。

目标

ES6 Module的目标是出一个让CommonJS和AMD社区都能接受的方案:

  • 偏爱默认导出
  • 静态模块结构
  • 支持同步和异步加载
  • 支持模块间的循环依赖

优势和劣势(trade-off)

ES6 Module实际达成的方案特性如下:

  • 比CommonJS更紧凑的语法
  • 结构可以被静态分析(静态检测、优化等)
  • 对循环引用比CommonJS更好
  • 支持异步加载

适用的场景(业务场景、技术场景)

适用于浏览器端和服务端(Nodejs),目前(2019年)主流的浏览器大部分都不支持ES6 Module的特性,因此使用的话还得使用Babel来转译。

组成部分和关键点

ES6 Module分两部分:

  • 声明式语法(import和export)
  • 编程式加载API: 支持配置如何加载模块以及条件加载

使用场景

在浏览器侧的使用

使用<script>进行加载

通过type属性值为module来标识模块进行加载,支持外部文件和内联的方式,默认使用defer的行为,也可以指定async

1
2
3
4
5
6
7
8
9
10
11
<!-- load a module JavaScript file -->
<script type="module" src="module.js"></script>

<!-- include a module inline -->
<script type="module">

import { sum } from "./example.js";

let result = sum(1, 2);

</script>

通过Worker进行加载

1
2
// load module.js as a module
let worker = new Worker("module.js", { type: "module" });

通过打包工具进行打包

使用webpack或者browserify等打包工具进行打包后使用。

声明式语法

声明式语法分为两种类型:具名导出(每个模块有多个)和默认导出(每个模块一个)。

具名导出(named exports)

通过关键字export前缀可以导出多个内容,并有名称来进行区分。

1
2
3
4
5
6
7
8
9
10
11
12
13
//------ lib.js ------
export const sqrt = Math.sqrt;
export function square(x) {
return x * x;
}
export function diag(x, y) {
return sqrt(square(x) + square(y));
}

//------ main.js ------
import { square, diag } from 'lib';
console.log(square(11)); // 121
console.log(diag(4, 3)); // 5

默认导出(default exports)

一个模块只有一个默认导出,并且默认导出尤其容易进行导入。

1
2
3
4
5
6
//------ myFunc.js ------
export default function () { ... };

//------ main1.js ------
import myFunc from 'myFunc';
myFunc();

对于class的使用

1
2
3
4
5
6
//------ MyClass.js ------
export default class { ... };

//------ main2.js ------
import MyClass from 'MyClass';
let inst = new MyClass();

默认导出其实可以理解为特殊的具名导出

导入:

1
2
import { default as foo } from 'lib';
import foo from 'lib';

导出:

1
2
3
4
5
6
//------ module1.js ------
export default 123;

//------ module2.js ------
const D = 123;
export { D as default };

答疑

  • 为什么我们需要具名导出(named exports)?

The answer is that you can’t enforce a static structure via objects and lose all of the associated advantages (described in the next section).

如果通过对象进行导出会丢失静态结构,从而失去原先的优势。

  • ES6 Module输出的是值的引用,并且是在编译时输出接口。

编程式语法

针对ES6 Module的编程式实现,目前规范处于stage 3。

使用编程式语法,可以做到:

  • 对模块和脚本进行编程操作
  • 配置模块的加载
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
<!DOCTYPE html>
<nav>
<a href="books.html" data-entry-module="books">Books</a>
<a href="movies.html" data-entry-module="movies">Movies</a>
<a href="video-games.html" data-entry-module="video-games">Video Games</a>
</nav>

<main>Content will load here!</main>

<script>
const main = document.querySelector("main");
for (const link of document.querySelectorAll("nav > a")) {
link.addEventListener("click", e => {
e.preventDefault();

import(`./section-modules/${link.dataset.entryModule}.js`)
.then(module => {
module.loadPageInto(main);
})
.catch(err => {
main.textContent = err.message;
});
});
}
</script>

已有的实现和它之间的对比

有关模块化的进程,前端模块化详解一句讲解得比较详细了,可以参考。

参考文档

create-react-app-from-scratch

React社区中提供了类似create-react-app这样的命令行工具,作为React项目的脚手架,这个工具提供了大而全的功能,直接拿过来使用,你可能不明白背后的原理,这里跟着Creating a React App… From Scratch这篇文章一起从零搭建一个React App,有条件的同学可以直接阅读英文版,这里实现的是一个最简单的功能,支持ES6+和JSX语法、热加载。

起步

创建一个目录并且通过npm init初始化一个项目,有需要也可以通过git init创建版本管理,新项目的文件结构如下:

1
2
3
.
+-- public
+-- src

此时可以添加一个.gitignore文件,并把node_modulesdist目录排除在提交范围。

public目录中,将会存放一些静态的资产,最主要的是放置index.html文件,让react用来渲染app用。

index.html如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!-- sourced from https://raw.githubusercontent.com/reactjs/reactjs.org/master/static/html/single-file-example.html -->
<!DOCTYPE html>
<html>

<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>React Starter</title>
</head>

<body>
<div id="root"></div>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<script src="../dist/bundle.js"></script>
</body>

</html>

这里react通过root这个id进行关联渲染元素,我们后续打包的文件名称为bundle.js

Babel

接下来添加babel相关的软件包:

1
npm install --save-dev @babel/core@7.1.0 @babel/cli@7.1.0 @babel/preset-env@7.1.0 @babel/preset-react@7.0.0

babel-core是主要的babel包,用于转译代码。babel-cli用于在命令行中编译文件。preset-reactpreset-env用于预先配置要转换的代码类型,env预置用以转换ES6+的代码到ES5的代码,react预置转换JSX的代码。

接着在项目的根目录,创建一个文件.babelrc,用于告诉bebel我们要配置的预置项。

1
2
3
{
"presets": ["@babel/env", "@babel/preset-react"]
}

Babel还有很多的插件值得研究。

Webpack

接下来需要配置Webpack来打包和启用开发热加载等功能。

安装开发依赖:

1
npm install --save-dev webpack@4.19.1 webpack-cli@3.1.1 webpack-dev-server@3.1.8 style-loader@0.23.0 css-loader@1.0.0 babel-loader@8.0.2

Webpack使用loader来处理不同类型的文件并进行打包,并提供一个开发服务器方便高效的进行开发。

在项目根目录下创建一个新文件webapck.config.js,在这里导出一个包含webpack配置的对象:

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
const path = require("path");
const webpack = require("webpack");

module.exports = {
entry: "./src/index.js",
mode: "development",
module: {
rules: [
{
test: /\.(js|jsx)$/,
exclude: /(node_modules|bower_components)/,
loader: "babel-loader",
options: { presets: ["@babel/env"] }
},
{
test: /\.css$/,
use: ["style-loader", "css-loader"]
}
]
},
resolve: { extensions: ["*", ".js", ".jsx"] },
output: {
path: path.resolve(__dirname, "dist/"),
publicPath: "/dist/",
filename: "bundle.js"
},
devServer: {
contentBase: path.join(__dirname, "public/"),
port: 3000,
publicPath: "http://localhost:3000/dist/",
hotOnly: true
},
plugins: [new webpack.HotModuleReplacementPlugin()]
};

这里webpack的配置项暂时不解释,接下来进行React的配置。

React

首先,我们要添加两个软件包: react@16.5.2react-dom@16.5.2

1
npm install --save react@16.5.2 react-dom@16.5.2

接下来,要告诉React app在哪里关联DOM,在src文件夹中创建一个index.js文件:

1
2
3
4
import React from "react";
import ReactDOM from "react-dom";
import App from "./App.js";
ReactDOM.render(<App />, document.getElementById("root"));

ReactDOM.render这个函数告诉React渲染什么和在哪里渲染,这里渲染一个叫做App的组件,并且渲染在ID为root的DOM元素上。

接下来在src文件夹下创建一个App.js文件,这是一个React组件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import React, { Component} from "react";
import "./App.css";

class App extends Component{
render(){
return(
<div className="App">
<h1> Hello, World! </h1>
</div>
);
}
}

export default App;

src目录下创建一个App.css文件

1
2
3
4
.App {
margin: 1rem;
font-family: Arial, Helvetica, sans-serif;
}

最终的项目结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
.
+-- public
| +-- index.html
+-- src
| +-- App.css
| +-- App.js
| +-- index.js
+-- .babelrc
+-- .gitignore
+-- package-lock.json
+-- package.json
+-- webpack.config.js

在webpack的script中增加一个脚本命令,start

1
start: webpack-dev-server --mode development

使用npm start命令即可启动

Finishing HMR

这个时候修改文件内容,页面并不会自动更新,要增加热加载功能,需要另外一个软件包的支持

1
npm install --save react-hot-loader@4.3.11

这个包可以安装在依赖中,而不是开发依赖,因为包会自动判断是否执行。

App.js中导入react-hot-loader

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import React, { Component} from "react";
import {hot} from "react-hot-loader";
import "./App.css";

class App extends Component{
render(){
return(
<div className="App">
<h1> Hello, World! </h1>
</div>
);
}
}

export default hot(module)(App);

这个时候修改文件的内容,页面就会即时刷新了。

Build

最后,可以在package.json中增加一个build的脚本命令,

1
build: webpack --mode production

HTTP Learning Notes

Intro

HTTP is a protocol which allows the fetching of resources, such as HTML documents.
Clients and servers communicate by exchanging individual messages(as opposed to stream of data).The messages sent by the client, usually a Web browser, are called requests and the messages sent by server as an answer are called responses.

阅读更多

Reactjs的常见问题

React的FAQ,来自官网的FAQ,学习后做的笔记,顺带翻译(不完全翻译)

AJAX and APIs

libaraies

你可以在React中使用任何你喜欢的AJAX库

Axio, jQuery AJAX, window.fetch

where did ajax call

componentDidMount

Babel, JSX, and Build Steps

JSX是否必要

不必要

ES6(+)是否必要

不必要

如何在JSX中写注释

支持多行注释

1
2
3
4
<div>
{/* Comment goes here */}
Hello, {name}!
</div>
阅读更多

你是否应该在URL中使用www

文章翻译自于Should you use “www” in your URL or not?,这篇文章是写于2016,该不该在你的网站URL中使用www? 现在来看对于网站的SEO而言,了解一下还是有帮助的,并且MDN上也链接了这篇文章。

正文:

这是一个有很长时间的问题: 使用www或者不使用www? 多年来,人们会被问到这两者的区别,哪个对SEO更好,或者是否有必要修改他们的网址。

没有什么可惊讶的,网络上充满了关于这个话题的观点。一方面,支持www的人争论说“使用www能为超出一台服务器时网站扩展带来的挑战做好准备”;另一方面,支持无www的人坚称“使用www是冗余的并浪费时间”。

然而我们可以两方都支持,我们的观点是其实这不是太影响(Google也同意!)。对于普通的博客或自由职业者或者甚至机构,使用www或者不使用,都是绝对零优势。也就是说,他们在技术上是有区别的,并且有一个非常好的理由说明你为什么不应该同时使用。

阅读更多