script标签常用的几个type属性

10,460 阅读11分钟

概述

说起script标签, 相信对我们前端开发工程师来说一定不陌生, 相反, 可能还非常熟悉, 毕竟它是我们使用频率非常高的一个标签, 这个标签能设置很多属性, 其中我们比较熟悉的可能是srctype属性, 而今天, 我们就来聊一聊它的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

所以, 当我们想要表示一个scripttypetext/javascript时有两种方法:

  1. 省略type属性:
<script></script>
  1. 写上完整的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并没有被输出, 控制台一片空白, 没有报错也没有输出内容, 而仔细一想, 这是因为当scripttype属性为text/javascript时, script里面的内容被浏览器当做js来执行, 因此一开始的js执行了, 输出了123, 而scripttype属性为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被输出了出来:

image.png

那如果书写的时候不符合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

为何会在页面源码中看到

前面也提到我第一次看到这个scripttype值的时候是在网页源码中看到, 确切的说是在nextjs开发的服务端渲染, 多称作ssr, Server Side Renderreact app中看到, 为何这么说呢? 我们来看几个例子

720yun

我们打开720yun, 然后右击>查看网页源代码, 然后搜索application/json, 那么此时我们就能看到type值为application/jsonscript标签了, 这里截取一部分:

<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/jsonscript标签, 同时里面有页面所需的数据, 并且还能通过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+graphqlreact app, ssr的项目, 获取完数据渲染页面, 页面呈递的效果与预期相符, 与设计稿一致, 交互也合理顺畅, 那么一个项目的生命周期基本就告一段落了, 但此时遇到了一些特殊的数据:

  1. 在一开始设计的业务体系之外
  2. 服务端不方便在原有的表中增加字段
  3. 不方便在给前端返回的时候添加一个字段, 如果加, 那么流程将会很复杂, 因为这里架构层面逻辑复杂, 多层结构, 做扩展字段的处理需要修改数据流转的相关逻辑, 得不偿失

因此服务端将数据放到了某个已经存在了的字段中, 初步考虑是给一个script[type="text/javascript"], 大概长这样:

  <script type="text/javascript">
    const xxxData = [
      {
        a: 1,
        b: 2
      },
      {
        c: 3,
        d: 4
      }
    ]
  </script>

当然了, 实际的代码是压缩过的, 这里只是便于观看理解

数据给过来之后我这边在我的ssr服务中使用正则表达式来获取数据, 然后再使用, 这样的方式是可以满足需求的, 但有这么几个问题:

  1. 对于服务端来说这只是一段没有实际意义的字符串, 但对于客户端(浏览器)来说这是一段js代码, 并不是数据
  2. 正因为对于浏览器来说它是一段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"
  • 你能够在里面使用importexport

由此我们便知道了这个模块的一个简单定义: 可以使用importexportscript标签, 这样我们就可以尝试一下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.js404, 而要使用./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/javascriptapplication/json, module只是看到了之后顺便做一个记录

如果你觉得这篇文章对你有用的话记得给我点个赞, 点个收藏, 众人拾柴, 愿没有难写的代码, 没有难实现的需求

参考文章:

  1. script
  2. MIME 类型
  3. ES6 In Depth: Modules