整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

HTML中的JavaScript

习目标:了解JavaScript是如何与HTML结合来创建动态网页,网页中嵌入JavaScript的不同方式,JavaScript的内容类型及其与<script>的关系

<script>元素

<script>是由Netscape创造出来,后来加到HTML规范中的。

<script>有8个属性:

1、async:表示立即开始下载脚本,但不能阻止其他页面动作,比如下载资源或者等待其他脚本加载。只对外部脚本文件有效。

2、charset:使用src属性指定代码字符集。这个属性很少用,因为大多数浏览器不在乎它的值。

3、crossorigin;配置资源请求的CORS(跨源资源共享)设置。默认情况下不使用CORS。crossorigin = “anonymous”配置文件请求不用设置凭据标志。crossorigin = ”use-credentials“设置凭据标志,意味着出站请求会包含凭据。

4、defer:表示脚本可以延迟到文档全部解析和显示后再执行。新版本中只能用于外部脚本。

5、integrity:允许比对接收到的资源和指定的加密签名以验证子资源完整性(SRI,Subresource integrity),如果验证签名不匹配则脚本不会执行。这个属性可以用于确保内容分发网络(CDN,Content Delivery Network)不会提供恶意内容。

6、language:此属性已被废止。

7、src:表示包含外部要执行的代码的外部文件。

8、type:代替language,表示代码块中脚本语言的内容类型(也称为MIME类型),按照惯例这个值始终都是”text/JavaScript“,尽管”text/JavaScript“和”text/ecmascript“都已经废弃。JavaScript文件的MIME类型通常是”application/x-javascript“,不过给type属性这个值的话可能会导致脚本被忽略。在非IE的浏览器中有效的值还有”application/JavaScript“和”application/ecmascript"。如果这个值是module,则代码会被当成是ES6模块,而且只有这时候代码中才能出现import和export关键字。

使用<script>的方式有内联和外嵌两种,只要把code写入<script>code</script>中就好,code中要是包含字符串“<script>”,只要加上转义字符“\”即可。

如果要外嵌JavaScript代码只要使用src属性来链接外部文件即可如:

<script src=“example.js”></script>

XHTML 文档中,可以忽略结束标签写成<script src=“example.js”/>即可,但是这在HTML中不能使用。

标签位置

过去把JavaScript和CSS一起写在head中,但是这意味着必须下载所有code并解析和解释完成后才开始渲染页面,对于JavaScript很多的页面会导致页面渲染速度过慢,为解决这个问题,JavaScript一般写在body元素的页面内容的最后边,如下

<html>

<head></head>

<body>

message

<script>code<\script>

<\body>

</html>

推迟使用脚本

在外联JavaScript时可以使用defer属性来推迟脚本的运行。可以写成:

<html>

<head>

<script defer src = "example.js">code<\script>

</head>

<body>

message

<\body>

</html>

异步执行脚本

async属性从脚本处理方式上与defer类似,但是不同的是标记async的脚本并不能保证脚本按照他们的出现顺序执行,比如:

<html>

<head>

<script sync src = "example1.js">code<\script>

<script sync src = "example2.js">code<\script>

</head>

<body>

message

<\body>

</html>

不能保证example1比example2先执行。

动态加载脚本

除了<script>以外还可以用其他方式加载脚本。因为JavaScript可以使用DOM API,所以通过向DOM中动态地加入script元素同样可以加载指定脚本。只要创建一个script元素并将其添加到DOM即可。

let script = document.createElement('script');

script.src = 'gibberish.js';

document.head.appendChild(script);

当然,在把 HTMLElement 元素添加到 DOM 且执行到这段代码之前不会发送请求。默认情况下,以这种方式创建的<script>元素是以异步方式加载的,相当于添加了 async 属性。不过这样做可能会有问题,因为所有浏览器都支持 createElement()方法,但不是所有浏览器都支持 async 属性。因此,如果要统一动态脚本的加载行为,可以明确将其设置为同步加载:

let script = document.createElement('script');

script.src = 'gibberish.js';

script.async = false;

document.head.appendChild(script);

以这种方式获取的资源对浏览器预加载器是不可见的。这会严重影响它们在资源获取队列中的优先级。根据应用程序的工作方式以及怎么使用,这种方式可能会严重影响性能。要想让预加载器知道这些动态请求文件的存在,可以在文档头部显式声明它们:

<link rel="preload" href="gibberish.js">

XHTML中的变化

可扩展超文本标记语言(XHTML,Extensible HyperText Markup Language)是将 HTML 作为 XML的应用重新包装的结果。与 HTML 不同,在 XHTML 中使用 JavaScript 必须指定 type 属性且值为text/javascript,HTML 中则可以没有这个属性。XHTML 虽然已经退出历史舞台,但实践中偶尔可能也会遇到遗留代码,为此本节稍作介绍。在 XHTML 中编写代码的规则比 HTML 中严格,这会影响使用<script>元素嵌入 JavaScript 代码。下面的代码块虽然在 HTML 中有效,但在 XHML 中是无效的。

<script type="text/javascript">

function compare(a, b) {

if (a < b) {

console.log("A is less than B");

} else if (a > b) {

console.log("A is greater than B");

} else {

console.log("A is equal to B");

}

}

</script>

在 HTML 中,解析<script>元素会应用特殊规则。XHTML 中则没有这些规则。这意味着 a < b语句中的小于号(<)会被解释成一个标签的开始,并且由于作为标签开始的小于号后面不能有空格,这会导致语法错误。避免 XHTML 中这种语法错误的方法有两种。第一种是把所有小于号(<)都替换成对应的 HTML实体形式(<)。结果代码就是这样的:

<script type="text/javascript">

function compare(a, b) {

if (a < b) {

console.log("A is less than B");

} else if (a > b) {

console.log("A is greater than B");

} else {

console.log("A is equal to B");

}

}

</script>

这样代码就可以在 XHTML 页面中运行了。不过,缺点是会影响阅读。好在还有另一种方法。第二种方法是把所有代码都包含到一个 CDATA 块中。在 XHTML(及 XML)中,CDATA 块表示文档中可以包含任意文本的区块,其内容不作为标签来解析,因此可以在其中包含任意字符,包括小于号,并且不会引发语法错误。使用 CDATA 的格式如下:

<script type="text/javascript"><![CDATA[

function compare(a, b) {

if (a < b) {

console.log("A is less than B");

} else if (a > b) {

console.log("A is greater than B");

} else {

console.log("A is equal to B");

}

}

]]></script>

在兼容 XHTML 的浏览器中,这样能解决问题。但在不支持 CDATA 块的非 XHTML 兼容浏览器中则不行。为此,CDATA 标记必须使用 JavaScript 注释来抵消:

<script type="text/javascript">

//<![CDATA[

function compare(a, b) {

if (a < b) {

console.log("A is less than B");

} else if (a > b) {

console.log("A is greater than B");

} else {

console.log("A is equal to B");

}

}

//]]>

</script>

这种格式适用于所有现代浏览器。虽然有点黑科技的味道,但它可以通过 XHTML 验证,而且对XHTML 之前的浏览器也能优雅地降级。

废弃的语法

自 1995 年 Netscape 2 发布以来,所有浏览器都将 JavaScript 作为默认的编程语言。type 属性使用一个 MIME 类型字符串来标识<script>的内容,但 MIME 类型并没有跨浏览器标准化。即使浏览器默认使用 JavaScript,在某些情况下某个无效或无法识别的 MIME 类型也可能导致浏览器跳过(不执行)相关代码。因此,除非你使用 XHTML 或<script>标签要求或包含非 JavaScript 代码,最佳做法是不指定 type 属性。在最初采用 script 元素时,它标志着开始走向与传统 HTML 解析不同的流程。对这个元素需要应用特殊的解析规则,而这在不支持 JavaScript 的浏览器(特别是 Mosaic)中会导致问题。不支持的浏览器会把<script>元素的内容输出到页面上,从而破坏页面的外观。Netscape 联合 Mosaic 拿出了一个解决方案,对不支持 JavaScript 的浏览器隐藏嵌入的 JavaScript 代码。最终方案是把脚本代码包含在一个 HTML 注释中,像这样:

<script><!--

function sayHi(){

console.log("Hi!");

}

//--></script>

使用这种格式,Mosaic 等浏览器就可以忽略<script>标签中的内容,而支持 JavaScript 的浏览器则必须识别这种模式,将其中的内容作为 JavaScript 来解析。虽然这种格式仍然可以被所有浏览器识别和解析,但已经不再必要,而且不应该再使用了。在XHTML 模式下,这种格式也会导致脚本被忽略,因为代码处于有效的 XML 注释当中。

行内代码与外部文件

虽然可以直接在 HTML 文件中嵌入 JavaScript 代码,但通常认为最佳实践是尽可能将 JavaScript 代码放在外部文件中。不过这个最佳实践并不是明确的强制性规则。推荐使用外部文件的理由如下。

 可维护性。JavaScript 代码如果分散到很多 HTML 页面,会导致维护困难。而用一个目录保存所有 JavaScript 文件,则更容易维护,这样开发者就可以独立于使用它们的 HTML 页面来编辑代码。

 缓存。浏览器会根据特定的设置缓存所有外部链接的 JavaScript 文件,这意味着如果两个页面都用到同一个文件,则该文件只需下载一次。这最终意味着页面加载更快。

 适应未来。通过把 JavaScript 放到外部文件中,就不必考虑用 XHTML 或前面提到的注释黑科技。包含外部 JavaScript 文件的语法在 HTML 和 XHTML 中是一样的。在配置浏览器请求外部文件时,要重点考虑的一点是它们会占用多少带宽。在 SPDY/HTTP2 中,预请求的消耗已显著降低,以轻量、独立 JavaScript 组件形式向客户端送达脚本更具优势。比如,第一个页面包含如下脚本:

<script src="mainA.js"></script>

<script src="component1.js"></script>

<script src="component2.js"></script>

<script src="component3.js"></script>

...

后续页面可能包含如下脚本:

<script src="mainB.js"></script>

<script src="component3.js"></script>

<script src="component4.js"></script>

<script src="component5.js"></script>

...

在初次请求时,如果浏览器支持 SPDY/HTTP2,就可以从同一个地方取得一批文件,并将它们逐个放到浏览器缓存中。从浏览器角度看,通过 SPDY/HTTP2 获取所有这些独立的资源与获取一个大JavaScript 文件的延迟差不多。在第二个页面请求时,由于你已经把应用程序切割成了轻量可缓存的文件,第二个页面也依赖的某些组件此时已经存在于浏览器缓存中了。当然,这里假设浏览器支持 SPDY/HTTP2,只有比较新的浏览器才满足。如果你还想支持那些比较老的浏览器,可能还是用一个大文件更合适。

文档模式

IE5.5 发明了文档模式的概念,即可以使用 doctype 切换文档模式。最初的文档模式有两种:混杂模式(quirks mode)和标准模式(standards mode)。前者让 IE 像 IE5 一样(支持一些非标准的特性),后者让 IE 具有兼容标准的行为。虽然这两种模式的主要区别只体现在通过 CSS 渲染的内容方面,但对JavaScript 也有一些关联影响,或称为副作用。本书会经常提到这些副作用。

IE 初次支持文档模式切换以后,其他浏览器也跟着实现了。随着浏览器的普遍实现,又出现了第三种文档模式:准标准模式(almost standards mode)。这种模式下的浏览器支持很多标准的特性,但是没有标准规定得那么严格。主要区别在于如何对待图片元素周围的空白(在表格中使用图片时最明显)。

混杂模式在所有浏览器中都以省略文档开头的 doctype 声明作为开关。这种约定并不合理,因为混杂模式在不同浏览器中的差异非常大,不使用黑科技基本上就没有浏览器一致性可言。标准模式通过下列几种文档类型声明开启:

<!-- HTML 4.01 Strict -->

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"

"http://www.w3.org/TR/html4/strict.dtd">

<!-- XHTML 1.0 Strict -->

<!DOCTYPE html PUBLIC

"-//W3C//DTD XHTML 1.0 Strict//EN"

"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">

<!-- HTML5 -->

<!DOCTYPE html>

准标准模式通过过渡性文档类型(Transitional)和框架集文档类型(Frameset)来触发:

<!-- HTML 4.01 Transitional -->

<!DOCTYPE HTML PUBLIC

"-//W3C//DTD HTML 4.01 Transitional//EN"

"http://www.w3.org/TR/html4/loose.dtd">

<!-- HTML 4.01 Frameset -->

<!DOCTYPE HTML PUBLIC

"-//W3C//DTD HTML 4.01 Frameset//EN"

"http://www.w3.org/TR/html4/frameset.dtd">

<!-- XHTML 1.0 Transitional -->

<!DOCTYPE html PUBLIC

"-//W3C//DTD XHTML 1.0 Transitional//EN"

"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<!-- XHTML 1.0 Frameset -->

<!DOCTYPE html PUBLIC

"-//W3C//DTD XHTML 1.0 Frameset//EN"

"http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd">

准标准模式与标准模式非常接近,很少需要区分。人们在说到“标准模式”时,可能指其中任何一个。而对文档模式的检测(本书后面会讨论)也不会区分它们。本书后面所说的标准模式,指的就是除混杂模式以外的模式。

<noscript>元素

针对早期浏览器不支持 JavaScript 的问题,需要一个页面优雅降级的处理方案。最终,<noscript>元素出现,被用于给不支持 JavaScript 的浏览器提供替代内容。虽然如今的浏览器已经 100%支持JavaScript,但对于禁用 JavaScript 的浏览器来说,这个元素仍然有它的用处。<noscript>元素可以包含任何可以出现在<body>中的 HTML 元素,<script>除外。在下列两种情况下,浏览器将显示包含在<noscript>中的内容:

 浏览器不支持脚本;

 浏览器对脚本的支持被关闭。任何一个条件被满足,包含在<noscript>中的内容就会被渲染。否则,浏览器不会渲染<noscript>中的内容。

下面是一个例子:

<!DOCTYPE html>

<html>

<head>

<title>Example HTML Page</title>

<script defer="defer" src="example1.js"></script>

<script defer="defer" src="example2.js"></script>

</head>

<body>

<noscript>

<p>This page requires a JavaScript-enabled browser.</p>

</noscript>

</body>

</html>

这个例子是在脚本不可用时让浏览器显示一段话。如果浏览器支持脚本,则用户永远不会看到它。

小结

JavaScript 是通过<script>元素插入到 HTML 页面中的。这个元素可用于把 JavaScript 代码嵌入到HTML 页面中,跟其他标记混合在一起,也可用于引入保存在外部文件中的 JavaScript。本章的重点可以总结如下。

 要包含外部 JavaScript 文件,必须将 src 属性设置为要包含文件的 URL。文件可以跟网页在同一台服务器上,也可以位于完全不同的域。

 所有<script>元素会依照它们在网页中出现的次序被解释。在不使用 defer 和 async 属性的情况下,包含在<script>元素中的代码必须严格按次序解释。

 对不推迟执行的脚本,浏览器必须解释完位于<script>元素中的代码,然后才能继续渲染页面的剩余部分。为此,通常应该把<script>元素放到页面末尾,介于主内容之后及</body>标签之前。

 可以使用 defer 属性把脚本推迟到文档渲染完毕后再执行。推迟的脚本原则上按照它们被列出的次序执行。

 可以使用 async 属性表示脚本不需要等待其他脚本,同时也不阻塞文档渲染,即异步加载。异步脚本不能保证按照它们在页面中出现的次序执行。

 通过使用<noscript>元素,可以指定在浏览器不支持脚本时显示的内容。如果浏览器支持并启用脚本,则<noscript>元素中的任何内容都不会被渲染。

本文主要理理js模块化相关知识。
涉及到内联脚本、外联脚本、动态脚本、阻塞、
deferasyncCommonJSAMDCMDUMDES Module。顺带探究下Vite

内联脚本

假设你是一个前端新手,现在入门,那么我们创建一个html页面,需要新建一个index.html文件:

<!DOCTYPE html>
<html>
<head>
  <title>test</title>
</head>
<body>
  <p id="content">hello world</p>
</body>
</html>

如果需要在页面中执行javascript代码,我们就需要在 HTML 页面中插入 <script> 标签。

有2种插入方式:
1、放在
<head>
2、放在<body>

比如,点击hello world之后,在hello world后面加3个感叹号的功能,我们在head中加入script标签,并给hello world绑定点击事件:

<!DOCTYPE html>
<html>
<head>
  <title>test</title>
  <script>
    function myFunction() {
      document.getElementById('content').innerHTML = 'hello world!!!'
    }
  </script>
</head>

<body>
  <p id="content" onclick="myFunction()">hello world</p>
</body>
</html>

如果加在body中,一般放在body的最后面:

<!DOCTYPE html>
<html>
<head>
  <title>test</title>
</head>

<body>
  <p id="content" onclick="myFunction()">hello world</p>
  <script>
    function myFunction() {
      document.getElementById('content').innerHTML = 'hello world!!!'
    }
  </script>
</body>
</html>

简单的逻辑我们可以用这2种方式写,这种方式叫做内联脚本。

外联脚本

当逻辑复杂时,我们可以把上面的script标签中的代码抽取出来,比如在html的同级目录创建一个js文件夹,里面新建一个a.js的文件。

a.js中写上面script标签中的代码:

function myFunction() {
  document.getElementById('content').innerHTML = 'hello world!!!'
}

上面的script标签则可以改成:

<script src="./js/a.js"></script>

阻塞

上面的2种写法,浏览器在加载html时,遇到script标签,会停止解析html。
内联脚本会立刻执行;外联脚本会先下载再立刻执行。
等脚本执行完毕才会继续解析html。
(html解析到哪里,页面就能显示到哪里,用户也能看到哪里)

比如下面的代码:

<p>...content before script...</p>

<script src="./js/a.js"></script>

<p>...content after script...</p>

解析到第一个p标签,我们能看到...content before script...显示在了页面中,然后浏览器遇到script标签,会停止解析html,而去下载a.js并执行,执行完a.js才会继续解析html,然后页面中才会出现...content after script...

我们可以通过Chrome的Developer Tools分析一下index.html加载的时间线:

这会导致2个问题:
1、脚本无法访问它下面的dom;
2、如果页面顶部有个笨重的脚本,在它执行完之前,用户都看不到完整的页面。

对于问题2,我们可以把脚本放在页面底部,这样它可以访问到上面的dom,且不会阻塞页面的显示:

<body>
  ...all content is above the script...

  <script src="./js/a.js"></script>
</body>

但这不是最好的办法,我们接着往下看。

defer

我们给script标签加defer属性,就像下面这样:

<p>...content before script...</p>

<script defer src="./js/a.js"></script>

<p>...content after script...</p>

defer 特性告诉浏览器不要等待脚本。于是,浏览器将继续解析html,脚本会并行下载,然后等 DOM 构建完成后,脚本才会执行。

这样script标签不再阻塞html的解析。

这时再看时间线:

需要注意的是,具有 defer 特性的脚本保持其相对顺序。

比如:

<script defer src="./js/a.js"></script>
<script defer src="./js/b.js"></script>

上面的2个脚本会并行下载,但是不论哪个先下载完成,都是先执行a.js,a.js执行完才会执行b.js。
这时,如果b.js依赖a.js,这种写法将很有用。

另外需要注意的是,defer 特性仅适用于外联脚本,即如果 script标签没有 src属性,则会忽略 defer 特性。

async

我们可以给script标签加async属性,就像下面这样:

<script async src="./js/a.js"></script>

这会告诉浏览器,该脚本完全独立。
独立的意思是,DOM 和其他脚本不会等待它,它也不会等待其它东西。async 脚本就是一个会在加载完成时立即执行的完全独立的脚本。

这时再看时间线:

可以看到,虽然下载a.js不阻塞html的解析,但是执行a.js会阻塞。

还需要注意多个async时的执行顺序,比如下面这段代码:

<p>...content before script...</p>

<script async src="./js/a.js"></script>
<script async src="./js/b.js"></script>

<p>...content after script...</p>

两个p标签的内容会立刻显示出来,a.js和b.js则并行下载,且下载成功后立刻执行,所以多个async时的执行顺序是谁先下载成功谁先执行。
一些比较独立的脚本,比如性能监控,就很适合用这种方式加载。

另外,和defer一样,async 特性也仅适用于外联脚本。

动态脚本

我们可以动态地创建一个script标签并append到文档中。

let script = document.createElement('script')
script.src = '/js/a.js'
document.body.append(script)

append后脚本就会立刻开始加载,表现默认和加了async属性一致。
我们可以显示的设置
script.async = false来改变这个默认行为,那么这时表现就和加了defer属性一致。

上面的这些写法,当script标签变多时,容易导致全局作用域污染,还要维护书写顺序,要解决这个问题,需要一种将 JavaScript 程序拆分为可按需导入的单独模块的机制,即js模块化,我们接着往下看。

CommonJS

很长一段时间 JavaScript 没有模块化的概念,直到 Node.js 的诞生,把 JavaScript 带到服务端,这时,CommonJS诞生了。

CommonJS定义了三个全局变量:

require,exports,module

require 读入并执行一个 js 文件,然后返回其 exports 对象;
exports 对外暴露模块的接口,可以是任何类型,指向 module.exports;
module 是当前模块,exports 是 module 上的一个属性。

Node.js 使用了CommonJS规范。

比如:

// a.js
let name = 'Lily'
export.name = name

// b.js
let a = require('a.js')
console.log(a.name) // Lily

由于CommonJS不适合浏览器端,于是出现了AMD和CMD规范。

AMD

AMD(Asynchronous Module Definition) 是 RequireJS 在推广过程中对模块定义的规范化产出。

基本思想是,通过 define 方法,将代码定义为模块。当这个模块被 require 时,开始加载依赖的模块,当所有依赖的模块加载完成后,开始执行回调函数,返回该模块导出的值。

使用时,需要先引入require.js:

<script src="require.js"></script>
<script src="a.js"></script>

然后可以这样写:

// a.js
define(function() {
    let name = 'Lily'
    return {
        name
    }
})
// b.js
define(['a.js'], function(a) {
    let name = 'Bob'
    console.log(a.name) // Lily
    return {
        name
    }
})

CMD

CMD(Common Module Definition) 是 Sea.js 在推广过程中对模块定义的规范化产出。

使用时,需要先引入sea.js:

<script src="sea.js"></script>
<script src="a.js"></script>

然后可以这样写:

// a.js
define(function(require, exports, module) {
    var name = 'Lily'
    exports.name = name
})

// b.js
define(function(require, exports, module) {
    var name = 'Bob'
    var a = require('a.js')
    console.log(a.name) // 'Lily'
    exports.name = name
})

UMD

UMD (Universal Module Definition) 目的是提供一个前后端跨平台的解决方案(兼容全局变量、AMD、CMD和CommonJS)。

实现很简单,判断不同的环境,然后以不同的方式导出模块:

(function (root, factory) {
    if (typeof define === 'function' && (define.amd || define.cmd)) {
        // AMD、CMD
        define([], factory);
    } else if (typeof module !== 'undefined' && typeof exports === 'object') {
        // Node、CommonJS
        module.exports = factory();
    } else {
        // 浏览器全局变量
        root.moduleName = factory();
  }
}(this, function () {
    // 只需要返回一个值作为模块的export
    // 这里我们返回了一个空对象
    // 你也可以返回一个函数
    return {};
}));

ES Module

AMD 和 CMD 是社区的开发者们制定的模块加载方案,并不是语言层面的标准。从 ES6 开始,在语言标准的层面上,实现了模块化功能,而且实现得相当简单,完全可以取代上文的规范,成为浏览器和服务器通用的模块解决方案。

ES6 的模块自动采用严格模式。模块功能主要由两个命令构成:export和import。

export命令用于规定模块的对外接口;
import命令用于输入其他模块提供的功能。

比如上面的代码,我们可以这样写:

// a.js
const name = 'Lily'

export {
  name
}

// 等价于
export const name = 'Lily'

// b.js
import { name } from 'a.js'
console.log(name) // Lily

// b.js
import * as a from 'a.js'
console.log(a.name) // Lily

此外,还可以用export default默认导出的写法:

// a.js
const name = 'Lily'

export default {
  name
}

// b.js
import a from 'a.js'
console.log(a.name) // Lily

如果只想运行a.js,可以只import:

// b.js
import 'a.js'

我们可以给script标签加type=module让浏览器以 ES Module 的方式加载脚本:

<script type="module" src="./js/b.js"></script>

这时,script标签会默认有defer属性(也可以设置成async),支持内联和外联脚本。

这时我们运行打开index.html,会发现浏览器报错了:

这是因为 type=module 的 script 标签加强了安全策略,浏览器加载不同域的脚本资源时,如果服务器未返回有效的 Allow-Origin 相关 CORS 头,会禁止加载改脚本。而这里启动的index.html是一个本地文件(地址是file://路径),将会遇到 CORS 错误,需要通过一个服务器来启动 HTML 文件。

Vite

在浏览器支持 ES Module 之前,我们用工具实现JavaScript模块化的开发,比如webpack、Rollup 和 Parcel 。但是当项目越来越大后,本地热更新越来越慢,而 Vite 旨在利用ESM解决上述问题。

Vite使用简单,可以去官网(https://cn.vitejs.dev/)看看。

总结

老的规范了解即可,未来是ES Module的,用Vite可以极大的提升开发时的体验,生产环境用Rollup打包。

ue

https://cn.vuejs.org/index.html

需要你做一下预习:https://cn.vuejs.org/v2/guide/index.html

数据驱动。如果我们要改变页面效果,不再需要直接操作dom元素,只需要改变数据就好。数据改变之后框架会自动的帮我们进行页面更新。

历史

js最初的出现就是为了解决一个页面中弹出一个提示,或者做一个简单的计算。当时的环境下,浏览器可用的内存很小,为了解决这些问题,js语言必须简单、没有太复杂的数据结构、占用内存小。

但是随着时间的发展,网页的功能越来越复杂,需要的交互越来越多,js需要做的事情就更多。随着时代的发展,浏览器的厂家也越来越多,每家浏览器对js语法的支持也不一样。ECMA这个组织,建立一个统一的标准,在不停的制定一些语言语法的规范。

语言在发展的过程中,会吸取或者借鉴一下同行的一些优势,来完善自身。


为了解决业务场景的复杂化,出现了很多框架或者开发模式:

jQuery是前期出现的一个神级的插件,它提供了一个标准的元素选择方案,让我们可以快速的做元素选择。选中之后做后序的各种操作。它统一了各个浏览器中js语法的差异,使用jQuery写代码就不需要考虑各个版本浏览器中语法的差别

MVC框架Backbone,是早期经典的前端开发框架(jQuery+underscore.js+backbone.js+require.js);做SPA单页面应用程序开发

angular.js,是google的。分为两类:angular.js和angular

react.js,是facebook出的框架,目前是全球使用最广泛的。国内十家公司的react可能有十种写法

vue.js,是一个个人项目,目前是国内使用比较广泛的。国内十家公司的vue只能有一种写法

基础api

  • v-model
  • 作用是实现页面中表单元素和data数据的双向绑定。是一个语法糖,相当于绑定了表单元素的input事件和value值。当输入框的值变化的时候data中的数据跟着变化。实现的原理是Object.defineProperty
  • 可以添加修饰符:number(转换为数字)、trim(去空格)
  • v-bind
  • 作用是绑定标签的属性,可以简写为:
  • 绑定样式的时候可以为样式写成对象的形式:属性名表示样式名,属性值为bool值,如果值为true表示这个样式生效,否则这个样式效果无效
  • v-html
  • 是绑定一个富文本内容,相当于设置innerHTML属性
  • v-text
  • 相当于设置innerText属性
  • v-on
  • 作用是事件绑定,可以简写为@符号。在vue中绑定事件的时候只写事件名字就好
  • 处理键盘事件的时候可以加修饰符,具体的可用修饰符可以参考官网https://cn.vuejs.org/v2/guide/events.html#%E6%8C%89%E9%94%AE%E4%BF%AE%E9%A5%B0%E7%AC%A6
  • 最常用的一个按键修饰符是enter
  • v-for
  • 循环遍历,展示数据。是以后用到的最多的一个指令
  • 可以对数组,数字,字符串,对象这些数据做循环
  • <ul>
    <!-- list表示需要循环的数据 item表示循环到的每一项,index表示索引;其中item和index是形参,叫什么都可以; 循环的时候建议为每一项设置一个唯一的key,他的作用是做节点替换时候性能优化使用 -->
    <li v-for="(item, index) in list" :key="item.id">{{index}}-{{item}}</li>
    </ul>
  • v-if
  • 是直接删除或者插入标签
  • v-show
  • 是控制标签的display属性
  • 在实际使用的时候v-show的性能更高一些,建议使用
  • filter
  • 过滤器,作用是对数据做过滤显示
  • watch
  • 监听数据的变化,数据改变之后会触发一些列其他的操作
  • watch的应用场景:一个数据改变之后引起的一些列其他连锁反应
  • computed
  • 当依赖的数据改变之后重新计算一个结果。计算属性具有缓存功能,当依赖数据没有改变的时候不会重新计算
  • 计算属性和方法调用的区别:
  • 方法调用需要加括号,计算属性不用
  • 方法调用每一次页面更新的时候都会执行
  • 计算属性只有在依赖的数据改变的时候才会执行
  • 有两种写法:
  • ...
    computed: {
    val1() {
    return 'xxx'
    },
    val2: {
    set(v) {

    },
    get() {
    return ......
    }
    }
    }
    ...
  • 计算属性的应用场景:多个数据改变之后引起的一个计算结果
  • 组件
  • 组件是vue中很重要的一个东西,是vue项目的主要组成部分。组件可以理解为自定义标签
    • 组件定义
    • 组件定义的时候有常见的两种:局部组件和全局组件
      • 局部组件
      • 局部组件在使用的时候需要先进行注册,使用components属性进行注册
      • const hello = {
        template: ``//....
        }
      • 全局组件
      • 可以直接使用不需要注册
      • Vue.component('xx', {
        template: ''
        })
    • 组件嵌套
    • 组件中可以调用另一个组件。组件之间时可以相互嵌套的,在使用之前也是需要先注册
    • 生命周期
    • 生命周期里面包含很重要的一个东西叫生命周期钩子函数。就是在组件存在的周期中,在不同的时间点执行的函数

      • 生命周期钩子函数
      • 分为四个阶段
        • 创建
          • beforeCreate
          • created【重要】
          • 组件创建成功之后执行,主要用来在这个钩子函数中调接口取数据
        • 挂载
          • beforeMount
          • mounted【重要】
          • 挂载成功之后执行,在这个钩子函数中可以获取组件的dom元素
          • 可以使用$el属性获取当前组件的dom元素
        • 更新
          • beforeUpdate
          • updated
        • 销毁
          • beforeDestroy
          • destroyed
      • 更新阶段的钩子函数是在组件存在的周期中重复执行的,其他六个都是只执行一次。在更新阶段不能改变数据,数据改变会引起死循环。因为数据或者属性改变的话组件会触发更新阶段的钩子函数
      • 嵌套组件的生命周期函数执行顺序
      • 在嵌套组件中,当执行到父组件的挂载之前的时候,会执行所有子组件的创建到挂载完成的钩子函数。当子组件都挂载完成之后会执行父组件的挂载完成函数
      • keep-alive动态组件
      • activitedkeep-alive 专属,组件被激活时调用,deactivatedkeep-alive 专属,组件被销毁时调用,
    • 传参
    • 三种形式:
    • 父传子(从外往内)
    • 属性props
    • 子传父(从内往外)
    • 事件派发,在vue中使用$on添加一个事件,使用$emit派发一个事件
    • 非相关组件(平行组件)
    • 事件总线或者vuex
    • 事件总线是一个开发思想或者一种开发模式。
    • 在vue中,定义一个空白的vue实例,它什么都不干,就做一件事情,事件派发和监听
  • component动态组件
  • 是vue中的一个内置标签,可以通过is属性控制当前显示的组件
  • slot插槽
  • 可以理解为占位符,这个位置我们先占了,放什么以后再说
  • 插槽分为一般插槽和具名(有名字的插槽,加了name属性)插槽
  • <div id="app">
    <home>
    <template #end>
    <h2>你草率了,我才是最后一行</h2>
    </template>
    <h3>我是展示在slot中的内容</h3>
    <h2>我也是</h2>
    </home>
    </div>
    <script src="./libs/vue.js"></script>
    <script>
    // slot表示插槽,就是占位符,这个位置我们先占了,放什么以后再说
    const home = {
    template: `<div class="home">
    <h1>我是首页</h1>
    <slot></slot>
    <p>我是一段文字介绍</p>
    <p>这里应该是最后了</p>
    <slot name="end"></slot>
    </div>`,
    };
    new Vue({
    el: '#app',
    components: { home },
    });
    </script>
  • keepalive
  • keepalive就是保证这个组件一直在不销毁,只要组件创建了就不会被销毁
    • include
    • 包含,为每一个组件设置一个name属性。在include的时候写上这个属性值,表示keep-alive的时候包含这个组件。include表示包含,多个用,隔开
    • exclude
    • exclude表示不包含,多个用,隔开
  • 在设置了keep-alive的时候,页面切换的场景下会触发这两个钩子函数:activated和deactivated
  • 单文件组件
  • 我们在实际开发的时候vue中的组件都是被定义为一个一个单独的文件的,每一个文件都是.vue结尾的,每一个.vue文件就是一个单独的组件。每一个组件中包含有html、js、css他们都是独立针对这个组件的。
  • <template>
    <div>
    <h1>这是一个组件</h1>
    </div>
    </template>
    <script>
    export defaule {
    ....
    }
    </script>
    <style scoped>
    /* scoped表示只在当前组件内有效 */
    </style>
  • 因为浏览器是只认识html、css和js文件的,不认识.vue文件,所以我们需要借助插件对vue文件进行编译,生成js代码。这个需要配合webpack和使用一个vue-loader的加载器。这些知识作为了解,知道就行。
  • webpack是一个前端自动化模块打包工具

脚手架

https://cli.vuejs.org/zh/

npm install -g @vue/cli # 全局安装vue脚手架,希望你成功

刚才我初始化项目时选择的内容

? Please pick a preset: Manually select features
? Check the features needed for your project: Choose Vue version, Babel, CSS Pre-processors, Linter
? Choose a version of Vue.js that you want to start the project with 2.x
? Pick a CSS pre-processor (PostCSS, Autoprefixer and CSS Modules are supported by default): Sass/SCSS (with node-sass)
? Pick a linter / formatter config: Basic
? Pick additional lint features: Lint on save
? Where do you prefer placing config for Babel, ESLint, etc.? In dedicated config files
? Save this as a preset for future projects? No

翻译之后的中文版本

? Please pick a preset: Manually select features(选择使用手动方式创建项目)
? Check the features needed for your project: Choose Vue version, Babel, CSS Pre-processors, Linter(我现择了四项:1. 手动选择vue的版本【必选】,2.安装babel【必选】,3.css预处理,后面可以选择使用sass或者less等预处理语言,4.选择了代码规范性检测,写代码不符合规范时报错)
? Choose a version of Vue.js that you want to start the project with 2.x(选择vue2)
? Pick a CSS pre-processor (PostCSS, Autoprefixer and CSS Modules are supported by default): Sass/SCSS (with node-sass)(选择使用sass)
? Pick a linter / formatter config: Basic(选择lint规范性检测的基础配置)
? Pick additional lint features: Lint on save(在保存的时候检测代码)
? Where do you prefer placing config for Babel, ESLint, etc.? In dedicated config files(把每一个插件的配置文件单独放置)
? Save this as a preset for future projects? No(以后都不使用这种配置)

npm的配置文件,路径在windows的c:/users/你的用户名/.npmrc

registry=https://registry.npm.taobao.org/
init.author.email=你的邮箱
init.author.name=你的名字
sass_binary_site=https://npm.taobao.org/mirrors/node-sass/

路由

vue中的路由插件,路由的作用是实现页面跳转。简单点理解就是再浏览器中访问指定的地址的时候展示的组件或者页面内容

https://router.vuejs.org/zh/

路由分两种模式:hash和history

区别:hash浏览器支持性好,不需要做额外的配置,可以直接使用;history模式再发布的时候需要做特殊的设置,在web服务器上做了配置之后才能使用;hash模式的路由,地址路径中有#进行分割,#后面的表示路径,history模式中没有#

安装

npm i vue-router # 安装路由插件

使用

import Vue from 'vue'
import VueRouter from 'vue-router'
// 可以直接再组件中使用router-view和router-link等路由内置组件和对象($route和$router)
Vue.use(VueRouter)
const router = new VueRouter({
    routes: [] // 路由表,或者叫路由数据,就是我们网文指定地址时候展示的组件
})
  • router-view,路由对应的组件展示的位置。相当于一个容器,用来展示路由地址对应的组件
  • router-link,生成跳转链接
  • $route,就是路由数据对象
  • $router,就是路由对象,可以获取路由上的属性和方法,实现编程式跳转

路由传参

  • query,query传参是在url地址中传递参数,在url后面跟在?之后,页面刷新之后参数还在
  • params,页面刷新之后参数就没了。如果想让页面刷新之后参数还在,需要在路由设置的时候配置占位符

参数传递之后,在对应的页面使用$route属性可以直接获取参数

路由拦截和路由守卫

https://router.vuejs.org/zh/guide/advanced/navigation-guards.html#%E7%BB%84%E4%BB%B6%E5%86%85%E7%9A%84%E5%AE%88%E5%8D%AB,抽空了看看

beforeEach

afterEach

路由嵌套

children

命名视图

在一个组件中放多个router-view,通过name属性进行命名指定

在定义路由的时候使用components属性指定展示的组件,可以通过设置属性名为router-view的name属性,属性值为对应的组件的方式实现

new Router({
    routes: [{
        path: '/demo',
        name: 'Demo',
        components: {
            default: ()=>import('.....'),
            first: ()=>import('...')
        }
    }]
})

路由守卫其他的

https://router.vuejs.org/zh/guide/advanced/navigation-guards.html#%E7%BB%84%E4%BB%B6%E5%86%85%E7%9A%84%E5%AE%88%E5%8D%AB

beforeRouteUpdate(to, from, next) {
    // 在当前路由改变,但是该组件被复用时调用
    // 举例来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,
    // 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
    // 可以访问组件实例 `this`
},

vue中的其他API

https://cn.vuejs.org/v2/api/#%E5%85%A8%E5%B1%80-API

  • $refs
  • 获取所有设置了ref属性的标签
  • 我们可以通过它获取子组件中的数据和方法
  • $el
  • 可以获取组件的dom元素
  • $root
  • 表示根组件
  • $nextTick
  • dom元素更新成功之后的回调函数
  • vue中的dom更新是异步的

ui组件库

  • vant
  • https://vant-contrib.gitee.io/vant/#/zh-CN/
  • npm i vant -S # 安装依赖项import Vue from 'vue';
    import Vant from 'vant';
    import 'vant/lib/index.css';

    Vue.use(Vant);
  • element-ui
  • npm i element-ui # 安装依赖
  • 使用
  • import Vue from 'vue';
    import ElementUI from 'element-ui';
    import 'element-ui/lib/theme-chalk/index.css';
    import App from './App.vue';

    Vue.use(ElementUI);

    new Vue({
    el: '#app',
    render: h => h(App)
    });
  • https://github.com/PanJiaChen/vue-admin-template 管理后台模板
  • 解决node-sass安装失败的问题
  • # 设置node-sass的下载路径
    npm config set sass_binary_site https://npm.taobao.org/mirrors/node-sass/

vuex

https://vuex.vuejs.org/zh/

vuex是vue中的一个状态管理插件,通俗的讲就是一个全局的数据管理工具。作用是实现项目中数据的集中式管理。

vuex是遵循单向数据流机制的:就是数据是单向流动的,分为三部分(view,state,action)

  • view表示视图
  • state表示数据
  • action表示行为

在view视图中,通过dispatch派发一个action改变数据,数据改变之后view视图重新渲染


vuex中的核心模块

  • state
  • 数据,所有的数据存储的地方
  • mutations
  • 改变数据的地址,所有的数据改变都在这里进行。每一次数据的变化都会被vue-devtools记录下来
  • actions
  • 行为,用来处理异步操作。在actions中,也能改变数据,但是不建议这样做。因为所有的数据改变都是需要可以追踪到,就是被记录下来。所以必须在mutations中改变数据
  • modules
  • 模块拆分
  • getters
  • 相当于计算属性

vuex中的数据流向:在组件中通过dispatch派发一个action,在action中获取数据,然后通过commit提交一个mutation改变数据,数据改变之后组件重新渲染

api

他们可以接收的参数为:

mapXxx('命名空间', [数组])
mapXxx('命名空间', {对象})
如果没有命名空间空间参数,表示获取根节点上的内容
  • mapState
  • 作用是把state数据映射到组件的计算属性上
  • mapMutations
  • 作用是把mutation数据映射到组件的methods上
  • mapActions
  • 作用是吧action数据映射到组件的methods上

你有没有用过vuex?

两种回答方式:

  1. 用过,给他讲讲单向数据流和vuex中的数据流向
  2. 没有,我们项目中用不上。跨组件传参我们使用事件总线

vuex不是项目开发的时候必选的一个插件,但是在需要用的时候你要知道它的存在,它的作用就是显示数据在不同的组件之间进行共享的。

项目

手机app

管理后台

面试题和其他内容

https://lurongtao.gitee.io/felixbooks-interview2/

插件

  • animate.css
  • https://animate.style/
  • 动画库
  • moment
  • 日期格式化插件
  • http://momentjs.cn/
  • nprogress
  • 进度条插件
  • swiper
  • 轮播图
  • https://swiperjs.com/
  • https://www.swiper.com.cn/
  • wangEditor
  • 富文本编辑器
  • https://www.wangeditor.com/
  • https://github.com/wangeditor-team/wangEditor
  • awesome-vue
  • vue的各种资源汇总
  • https://github.com/vuejs/awesome-vue
  • vue-clipboard2
  • 复制到剪切板
  • echarts
  • 数据可视化,图表插件
  • https://echarts.apache.org/zh/index.html
  • https://v-charts.js.org/#/ (echarts的vue封装),v-charts目前不支持echarts5,需要重新安装4.x的echarts
  • datav
  • 数据可视化的图表插件
  • http://datav.jiaminghi.com/

vue配置优化

vue.config.js,所有的相关配置信息都在vue-cli的官网上:https://cli.vuejs.org/zh/config/#vue-config-js

https://webpack.docschina.org/ webpack官网,作为了解

module.exports = {
  publicPath: "./", // 表示打包之后资源文件的加载路径
  // 再做性能优化的时候,需要做到
  /**
   * 1. 路由文件的懒加载,使用 ()=> import('xxx')的方式引入,可以把路由组件单独打包成js文件,在需要使用的时候再引入
   * 2. 使用cdn的方式引入第三方资源库
   *
   * **/
  // configureWebpack,对webpack工具做额外的设置
  configureWebpack: {
    externals: {
      // 属性名是js源代码中引入的时候使用的包名,属性值是引入js文件后再浏览器中可以直接使用的名字
      vue: "Vue",
      vuex: "Vuex",
      "vue-router": "VueRouter",
      axios: "axios",
      "element-ui": "ELEMENT",
    },
  },
  // 脚手架内置了一个node的开发服务器,可以直接让我们通过网络路径访问代码
  devServer: {
    // port: 998, // 改变开发服务器的端口号
    proxy: {
      // 访问以/api开头的地址时做一个代理转发
      // 代理只有再开发的时候有用,打包之后就没用了
      "/api": {
        target: "https://papi.jiemian.com/page/api", // 目标服务器
        ws: true, // 开启ws
        changeOrigin: true, // 改变origin
        pathRewrite: { "^/api": "" }, // 路径重写,把/api替换成空白
      },
    },
  },
};

nuxt

是一个基于vue语法的服务器端渲染(SSR)框架。使用vue语法编写多页面应用程序,就是每一次路由跳转打开的都是一个新的html文件。它解决了SPA单页面应用程序的一个通病(最怕刷新)。

https://www.nuxtjs.cn/

yarn

yarn是facebook出的一款包管理工具,和npm一样的功能

https://yarnpkg.com/

安装使用

npm i yarn -g # 全局安装yarn
yarn add xx # 安装模块,相当于 npm i xx
yarn remove xx # 删除模块,相当于 npm uninstall xx

nuxt上线

.nuxt打包文件、nuxt.config.js配置文件、package.json依赖配置文件、static静态文件放在服务器