概述
说起script
标签, 相信对我们前端开发工程师来说一定不陌生, 相反, 可能还非常熟悉, 毕竟它是我们使用频率非常高的一个标签, 这个标签能设置很多属性, 其中我们比较熟悉的可能是src
和type
属性, 而今天, 我们就来聊一聊它的type
属性
type
type
属性的值为MIME 类型, 全称叫做Multipurpose Internet Mail Extensions Type
, 多用途互联网邮件扩展类型
, 也被叫做媒体类型
, 大部分时候媒体类型
是服务端通过http协议
告知客户端(浏览器)的, 确切的说是服务端通过Content-Type
这个响应头
来告诉浏览器接收到的响应体
的媒体类型
到底是什么, 这个媒体类型
决定了服务端返回的内容(响应体
)究竟该如何被我们的浏览器处理, 比如一个常见的Content-Type
:
Content-Type: text/html;
此时浏览器将会将响应的内容当做是一个html
文件来解析处理
text/javascript
提到script
标签的type
属性值, 我相信我们大多数人最熟悉的就是text/javascript
, 然而自己手动打这几个单词的时候可能比较少, 毕竟我们使用的编辑器在我们敲入script
的时候, 编辑器自带的emmet
插件就能自动提示我们, 此时我们敲tab
, 那么它就会帮我们补全script
:
<script></script>
很多年之前写代码, 我使用的编辑器是sublime text
, 那个时候自动补全script
标签的时候会不会带type
属性我忘了, 但是现在的vs code
是不会的, 但我每次都会自己手动写上:
type="text/javascript"
那么最终的结果就是:
<script type="text/javascript"></script>
当然了, 省略也是可以的, 毕竟可能大部分人都是省略的, 只是我一开始学习前端, 学习js
的时候看到的script
标签都有type="text/javascript"
, 因此这个习惯沿用到了现在
同时, 如果我们省略了这个属性, 那么默认值也将是text/javascript
, 这是摘自红宝书
: <JavaScript高级程序设计(第三版)>
中的原话:
考虑到约定俗成和最大限度的浏览器兼容, 目前type属性的值依旧是text/javascript, 不过这个属性并不是必须的, 如果没有指定这个属性, 则其默认值仍为text/javascript
所以, 当我们想要表示一个script
的type
为text/javascript
时有两种方法:
- 省略
type
属性:
<script></script>
- 写上完整的
type
属性值text/javascript
:
<script type="text/javascript"></script>
而我个人的习惯是写上完整的type
属性值text/javascript
, 具体书写方式可以按个人喜好来, 不必拘泥于某一种
type
值为text/javascript
, 此时浏览器会将里面的内容当做JavaScript
来执行, 因此我们写在里面的代码要符合js
的语法, 否则就会报错
写在script
里的type
值除了text/javascript
, 还有一个也挺常见, 我想大多数人可能和我一样, 第一次看到这个值是在右键
>查看网页源代码
的时候看到的, 它就是application/json
application/json
看到这个type
值, 我们可能会想到服务端的同学给我们返回数据的时候, 里面的一个响应头
:
Content-Type: application/json;
结合上面的内容, 我们不禁会想: 既然<script type="text/javascript"></script>
是让浏览器把标签里的内容当做js
来执行, 那如果是application/json
呢? 也就是:
<script type="application/json"></script>
此时浏览器会怎么处理里面的内容呢? 当做json
来处理吗? 我们可以来试一试:
<script type="text/javascript">
const num = 123;
console.log(num);
</script>
浏览器打开html
, 再打开控制台, 我们可以看到输出了123
, 没问题, 符合预期, 换成这样:
<script type="application/json">
const num = 123;
console.log(num);
</script>
这个时候刷新页面再次打开控制台查看, 我们发现123
并没有被输出, 控制台一片空白, 没有报错也没有输出内容, 而仔细一想, 这是因为当script
的type
属性为text/javascript
时, script
里面的内容被浏览器当做js
来执行, 因此一开始的js
执行了, 输出了123
, 而script
的type
属性为application/json
时浏览器则不会把script
里面的内容当做js
执行, 那倘若此时将script
标签里面的内容修改为json
格式的呢:
<script type="application/json">
{
"data": [
{
"a": 1,
"b": 2
},
{
"c": 3,
"d": 4
}
],
"total": 100
}
</script>
此时打开控制台依然没有任何的变化, 试想一下, 如果我们要获取这里面的内容呢? 我们的知识告诉我们, 一个标签里面的内容是字符串, 而这个标签中的内容是符合json
语法的字符串, 那么我们是不是就可以直接转成json
了呢?
试试看
为了方便我们获取这个script[type="application/json"]
中的内容, 我们给它加一个id
:
<script id="json-script" type="application/json">
{
"data": [
{
"a": 1,
"b": 2
},
{
"c": 3,
"d": 4
}
],
"total": 100
}
</script>
然后再通过一段js
去获取这个标签里面的内容再转成json
:
<script type="text/javascript">
const node = document.getElementById('json-script');
const jsonStr = node.innerText;
const json = JSON.parse(jsonStr);
console.log(json);
</script>
此时我们再次刷新页面并打开控制台, 可以看到json
被输出了出来:
那如果书写的时候不符合json
的语法, 或者说故意破坏json
的语法呢? 比如先破坏json
的语法, 然后不获取也不解析:
<script id="json-script" type="application/json">
{
"data": [
{
"a": 1,
"b": 2
} //这删除了逗号
{
"c": 3,
"d": 4
}
],
"total": 100
}
</script>
这么写是没问题的, 但如果我们获取并解析, 那么就会报错了:
Uncaught SyntaxError: Unexpected token / in JSON at position
当然了, 这是我这里行文的时候加了解释内容, 是一段注释, 将注释删了:
<script id="json-script" type="application/json">
{
"data": [
{
"a": 1,
"b": 2
}
{
"c": 3,
"d": 4
}
],
"total": 100
}
</script>
此时报错:
Uncaught SyntaxError: Unexpected token { in JSON at position
也就是json
语法有问题
由此可见, 当script
标签type
的值为application/json
的时候, 浏览器确实不会把里面的内容当做js
去解析, 因为我们在里面写js
代码的时候不会执行, 而当我们在里面书写符合json
语法的内容时, 我们则能够获取到这些内容, 并将其转成json
, 但当不符合json
语法的时候, 我们就不能将其转成json
了
查看mdn
的文章script我们发现了答案:
如果MIME类型不是JavaScript类型(上述支持的类型),则该元素所包含的内容会被当作数据块而不会被浏览器执行。
也就是说当script
标签type
的值为application/json
的时候, 浏览器会将里面的内容当做数据块, 而不会被当做是js
执行, 而我们的数据块就是json
为何会在页面源码中看到
前面也提到我第一次看到这个script
的type
值的时候是在网页源码中看到, 确切的说是在nextjs开发的服务端渲染
, 多称作ssr
, Server Side Render
的react app
中看到, 为何这么说呢? 我们来看几个例子
720yun
我们打开720yun, 然后右击
>查看网页源代码
, 然后搜索application/json
, 那么此时我们就能看到type
值为application/json
的script
标签了, 这里截取一部分:
<script id="__NEXT_DATA__" type="application/json">{"props":{"pageProps":{"isMobile":false,"coverpano":[{"product":{"xml":"\u003c?xml version...
720yun论坛
再打开一个720yun论坛, 里面同样有script[type="application/json"]
, 内容结构也和720yun一样
这两个网站都是用nextjs开发的服务端渲染
的react app
, 我们能看到页面上有一个id
为__NEXT_DATA__
, type
值为application/json
的script
标签, 同时里面有页面所需的数据, 并且还能通过window.__NEXT_DATA__
查看到网站的一些配置信息, 这是nextjs
这个框架设置的一个全局上下文
这里我们再看一个ssr
的网站, 九机网
九机网
同样的操作, 我们打开九机网并右击
>查看网页源代码
, 此时我们搜索页面内容, 搜索application/json
, 发现里面并没有script[type="application/json"]
, 但这个网站依旧是ssr
的, 这就说明ssr
的项目中script[type="application/json"]
标签并不是必须的
小结
我之所以在页面中看到了script[type="application/json"]
, 是因为我自己也在用这个框架, 因此求知欲和好奇心驱动我相对进一步的去做了一些探索, 也因此有了这篇文章, 而后续也发现script[type="application/json"]
并不是ssr
的标配, 只是用了nextjs
之后它会创建这么一个标签, 然后里面放有页面的数据, 至于原因, 我自己也做了一些查找, 详情可以看这个issue
: Why does __NEXT_DATA__
exists?, 里面有一位nextjs
的开发人员给了我们解答:
- Inside the server, it pre-render HTML
- Then inside the client, React need to map this HTML with the components related to this page
- Otherwise, there'll be a flash in the screen, which looks ugly
- So, it need to generate the same HTML inside the client.
- If those two are the same, React is happy and continue the client side work
翻译过来就是:
- 在服务器内部,它预先渲染了HTML
- 然后在客户端,React需要将这个HTML与这个页面相关的组件进行映射
- 否则,屏幕上会出现闪烁,看起来很难看
- 因此,它需要在客户端生成相同的HTML
- 如果这两个是相同的,React就会很高兴,继续进行客户端的工作
简而言之就是: 这是服务端进行了一个预渲染, 然后在客户端(浏览器)需要用这个数据进行页面相关组件的映射, 同时个人觉得当我们的web app
运行在了浏览器端, 然后再进行页面切换的时候, 这个数据也能起到给web app
使用的目的, 当然了, 页面切换需要请求新数据的情况除外
应用场景
聊了那么多, 说一说我遇到的这个application/json
的使用场景吧
ssr项目额外数据处理
最近做了一个nextjs
+graphql
的react app
, ssr
的项目, 获取完数据渲染页面, 页面呈递的效果与预期相符, 与设计稿一致, 交互也合理顺畅, 那么一个项目的生命周期基本就告一段落了, 但此时遇到了一些特殊的数据:
- 在一开始设计的业务体系之外
- 服务端不方便在原有的表中增加字段
- 不方便在给前端返回的时候添加一个字段, 如果加, 那么流程将会很复杂, 因为这里架构层面逻辑复杂, 多层结构, 做扩展字段的处理需要修改数据流转的相关逻辑, 得不偿失
因此服务端将数据放到了某个已经存在了的字段中, 初步考虑是给一个script[type="text/javascript"]
, 大概长这样:
<script type="text/javascript">
const xxxData = [
{
a: 1,
b: 2
},
{
c: 3,
d: 4
}
]
</script>
当然了, 实际的代码是压缩过的, 这里只是便于观看理解
数据给过来之后我这边在我的ssr
服务中使用正则表达式
来获取数据, 然后再使用, 这样的方式是可以满足需求的, 但有这么几个问题:
- 对于服务端来说这只是一段没有实际意义的字符串, 但对于客户端(浏览器)来说这是一段
js
代码, 并不是数据 - 正因为对于浏览器来说它是一段
js
代码, 所以浏览器会去执行它
而服务端给我的是数据, 同时我这边浏览器也应该把它当做是数据, 而不是当做js
去执行, 因此最后采用application/json
来处理:
<script id="xxxData" type="application/json">
[
{
a: 1,
b: 2
},
{
c: 3,
d: 4
}
]
</script>
我这边同样是在我的ssr
服务中使用正则表达式
来获取数据, 因为数据是加入到一个已有字段中, 那么我们还要考虑这个已有字段中没有这段数据的情况, 最后代码如下:
//匹配后端返回的script中的json字符串
const handleMatchJsonStrInScript = str => {
if(!str) {
return null;
}
const scriptRegExp = /<script.*id="xxxData".*>(.*)<\/script>/;
const matchResArr = str.match(scriptRegExp);
if(!matchResArr) {
return null;
}
//script中的json字符串
const jsonStr = matchResArr[1];
//将string转换为json obj
const jsonObj = JSON.parse(jsonStr);
return jsonObj;
};
此时得到的结果就是一段json
数据了:
[
{
a: 1,
b: 2
},
{
c: 3,
d: 4
}
]
这样既获得了我们需要的数据, 同时浏览器又不会去解析和执行这段数据, 因为它并不是一段js
代码, 一举两得
显示固定数据的静态页面项目
比如一些toG
的项目, 介绍学校或者说学校目录之类的项目, 学校的数据由校方或者其他相关小伙伴提供, 这份数据一般是word
文档或者是excel
, 我们页面上展示这些固定的数据, 那么加载数据的时候除了创建一个js
文件加载数据外还可以将数据写到html
页面上:
<script id="json-script" type="application/json">
{
"data": [
{
"name": "a小学",
"desc": "aaaaa"
},
{
"name": "b小学",
"desc": "bbbbb"
}
],
"total": 100
}
</script>
然后我们再通过js
去获取, 关键代码如下:
const jsonScriptNode = document.getElementById('json-script');
const jsonStr = jsonScriptNode.innerText;
const json = JSON.parse(jsonStr);
这样可以避免使用js
加载数据时多余的网络请求, 同时语义也更加的明确
module
module
, 顾名思义就是模块化, 就是我们的CommonJS
CMD
AMD
这些, 一开始都是社区, 前端圈的大佬们自己实现模块化, 比如seajs
, nodejs
使用的CommonJS
规范, 以及异步模块定义
AMD
, 再到最后一统天下, 官方的ES6 Module
, 同时ES6 Module
也是我们目前前端工程化开发中运用最广泛的模块化规范
详细的module
相关内容可以查看这篇文章: ES6 In Depth: Modules, 这里仅做一个这个特性的探讨, 具体的import
export
export default
相关内容会单独写一篇文章, 这里不再赘述
引用上述文章中的一段内容做一个简要的概述:
An ES6 module is a file containing JS code. There’s no special module keyword; a module mostly reads just like a script. There are two differences.
- ES6 modules are automatically strict-mode code, even if you don’t write "use strict"; in them.
- You can use import and export in modules.
一个
ES6
模块是一个包含js
代码的文件, 没有特别的module
关键字, 一个模块通常就像一个脚本一样阅读, 只是和脚本相比有两个不同:
ES6
模块会自动启用严格模式
, 即使你没有在里面写"use strict"
- 你能够在里面使用
import
和export
由此我们便知道了这个模块的一个简单定义: 可以使用import
和export
的script
标签, 这样我们就可以尝试一下module
的情形了
直接导入使用
我们建一个目录, 里面有一个module.html
文件, /public
目录, /public/js/a-module.js
project
|--public
| |--js
| | |--a-module.js
| |--module.html
module.html
:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>模块化</title>
</head>
<body>
<script type="module">
import { funca } from './public/js/a-module.js';
funca();
</script>
</body>
</html>
/public/js/a-module.js
:
export const funca = () => {
console.log('我是a-module');
};
此时打开我们的module.html
文件发现报了跨域的错:
Access to script at 'file:///D:/js/a-module.js' from origin 'null' has been blocked by CORS policy: Cross origin requests are only supported for protocol schemes: http, data, chrome, chrome-extension, chrome-untrusted, https.
GET file:///D:/js/a-module.js net::ERR_FAILED
这是因为script[type="module"]
会受到CORS
策略的限制, 直接在浏览器中打开文件用的是file
协议, 而只有http
, data
, chrome
, chrome-extension
, chrome-untrusted
, https
这几个协议能使用, 那么我们就本地起一个服务即可, 创建一个server.js
文件:
project
|--public
| |--js
| | |--a-module.js
| |--module.html
|--server.js
server.js
:
const path = require('path');
const express = require('express');
const app = express();
const DIST_DIR = path.join(__dirname, 'public');
const PORT = 9090;
app.use(express.static(DIST_DIR));
app.listen(PORT, function() {
console.log(`成功启动: http://localhost:${PORT}`);
});
需要注意的是, 因为上面的server.js
将整个public
目录作为静态资源目录
, 所以从这里开始, 里面的module.html
引用的js
的路径要做相应的修改, 即:
module.html
:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>模块化</title>
</head>
<body>
<script type="module">
import { funca } from './js/a-module.js';
funca();
</script>
</body>
</html>
原来的./public/js/a-module.js
会404
, 而要使用./js/a-module.js
, 因为/public
目录中没有/public
目录, 只有/js
这一个目录
这里我使用的是9090
, 主要是为了防止和其他项目端口冲突, 此时我们启动这个服务:
$ node server.js
然后打开这个地址:
http://localhost:9090/module.html
此时就能正常使用我们的script[type="module"]
了, 打开控制台我们可以看到a-module.js
中的funca
方法执行了:
我是a-module
模块中导入使用
上一个例子是直接导入使用, 这次我们试试模块中导入的情况, 创建一个/public/js/b-module.js
文件, 并在其中导入我们的a-module.js
文件:
import { funca } from './a-module.js';
console.log('我是b-module, 我执行a-module里的方法:');
funca();
此时目录结构如下:
project
|--public
| |--js
| | |--a-module.js
| | |--b-module.js
| |--module.html
|--server.js
同时在我们的module.html
中导入b-module.js
:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>模块化</title>
</head>
<body>
<script type="module">
import { funca } from './js/a-module.js';
funca();
</script>
<script type="module" src="/js/b-module.js"></script>
</body>
</html>
刷新页面, 控制台输出如下内容:
我是a-module
我是b-module, 我执行a-module里的方法:
我是a-module
符合预期
这里再试试ES6 class
的形式, 我们创建一个c-module.js
:
class C {
name = 'c-module'
selfIntro = '我是个class'
sayHello() {
console.log(`我的名字是${this.name}, ${this.selfIntro}`);
}
}
export default C;
再创建一个d-module.js
并在其中导入c-module.js
同时将其实例化
:
import C from './c-module.js';
console.log('我是d-module, 我要执行c-module中的代码:');
const instance = new C();
instance.sayHello();
此时我们的目录结构如下:
project
|--public
| |--js
| | |--a-module.js
| | |--b-module.js
| | |--c-module.js
| | |--d-module.js
| |--module.html
|--server.js
修改html
文件:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>模块化</title>
</head>
<body>
<script type="module">
import { funca } from './js/a-module.js';
funca();
</script>
<script type="module" src="/js/b-module.js"></script>
<script type="module" src="/js/d-module.js"></script>
</body>
</html>
刷新页面后控制台输出:
我是a-module
我是b-module, 我执行a-module里的方法:
我是a-module
我是d-module, 我要执行c-module中的代码:
我的名字是c-module, 我是个class
当然了, 模块化
的开发不可能使用script[type="module"]
来做, 而是使用webpack
打包处理, 也就是工程化
的方式来处理, 一来script[type="module"]
兼容性堪忧: can i use module?, 二来工程化
能让我们以分模块
或者说组件
的方式来进行开发, webpack
则将之打包处理成IIFE
形式的代码, 同时辅以babel
将我们的ES6
ES6+
的代码转为更具兼容性的ES5
的代码, 既提升了我们的编程体验, 同时还兼顾兼容性问题
到这里, script
常用的几个type
就介绍完了, 平时我们用的多的还是text/javascript
和application/json
, module
只是看到了之后顺便做一个记录
如果你觉得这篇文章对你有用的话记得给我点个赞, 点个收藏, 众人拾柴, 愿没有难写的代码, 没有难实现的需求
参考文章: