引入CDN的JS文件为什么不可执行?

背景

最近在发布j-weapons前端包的时候遇到了一个问题,引入CDN编译以后的JS文件,在浏览器中查看该脚本文件实际并没有执行。

问题过程

代码文件如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<!doctype html>  
<html>  
<head>  
  <meta charset="UTF-8">  
  <title>testing</title>  
</head>  
<body>  
<script src="https://unpkg.com/axios@1.7.2/dist/browser/axios.cjs"></script>
<script>console.log(axios)</script>
</body>  
</html>

这里以axios为例,情况是如出一辙的。

控制台得到一个错误:

1
2
Uncaught ReferenceError: axios is not defined
    at dist/:14:21

script src文件并没有被执行。

赶紧看Network对应资源文件的请求,如下图:

Status failed

可以看到这里的Status显示了(failed) net::ERR_BLOCKED_BY_ORB(而General.Status Code是200正常的)一番搜寻没找到有用的信息,AI介绍是浏览器应用层处理网络请求时的错误,既不属于HTTP也不是TCP的错误,暂不展开讨论这块。

接着看响应头,如图:

response headers

重点在Content-Type,返回的竟然是text/plain

接着换另一个CDN:jsdelivr再试试:

1
<script src="https://cdn.jsdelivr.net/npm/axios@1.7.2/dist/browser/axios.cjs"></script>

同样,脚本也并没有被执行,查看响应头,如图:

response headers

Status Code也是200,并没有出现(failed) net::ERR_BLOCKED_BY_ORB,而响应类型变成了:Content-Type: application/node

我在本地用Nginx引入本地文件,试了一下,响应类型是Content-Type: application/javascript,能够正常执行脚本。

证明文件本身没有问题,问题在于响应头的响应类型。

另外在Console有一个错误:

1
Refused to execute script from 'https://cdn.jsdelivr.net/npm/axios@1.7.2/dist/browser/axios.cjs' because its MIME type ('application/node') is not executable, and strict MIME type checking is enabled.

这个错误就比较有意思。


MIME

它说CDN文件的MIME type,也就是指响应头里的Content-Type,是application/node类型,不可以执行,并且启用了严格检查MIME type类型的模式,应该就是指响应头返回的X-Content-Type-Options: nosniff

稍加解释一下:

  • MIME type也称为IANA媒体类型,是一种标准,用来表示文档、文件或一组数据的性质和格式,它在IETF的RFC 6838中进行了定义和标准化。它一般包含两个部分:类型子类型,中间用/分割,不能有空格,例如:text/plaintext/javascript等。
  • Content-Type则表示标头用于指定资源的原始媒体类型,就是指上面说的MIME类型,它是由服务端返回的。

它们都属于HTTP的知识。

MIME嗅探

假设我们有一个HTML标签:

1
<script src="https://cdn.jsdelivr.net/npm/axios@1.7.2/dist/browser/axios.cjs" type="text/javascript"></script>

当然你可能会看到有使用type="application/javascript"的,不过该MIME类型已被弃用。

一般情况下,我们是不需要加type="text/javascript"的attributes,浏览器会默认将它视为text/javascript

浏览器在获取标签上的文件时,如果该文件的响应头没有Content-Type或者是Content-type: application/octet-stream的时候,就会通过读取文件的前几个字节(通常是512字节)分析文件内容的特征从而猜测MIME的类型,这个过程称为MIME嗅探。

MIME嗅探会带来安全问题,虽然我们给script src标签指定了type="text/javascript",但是浏览器一般会根据服务端返回的MIME类型来处理资源,用户可能会因此在不知情的情况下,遭受到XSS攻击。

防御的措施一般有:

  • 服务端始终响应正确的Content-Type,避免浏览器MIME嗅探的触发。
  • 服务端携带X-Content-Type-Options: nosniff响应头,明确告知浏览器不要进行MIME嗅探。

总结

回归正题,也就是说报错是因为CDN返回的Content-Type的MIME类型不是text/application或者不是application/javascript才导致script src标签不执行的,猜测CDN是根据不同的文件后缀给出不同的Content-Type

  • unpkg
    • .cjs的文件,Content-Typetext/plain,通过brose axios files文件右侧可以进一步确认。
    • .mapContent-Typeapplication/json
  • jsdelivr
    • .cjsContent-Typeapplication/node

这也解释了为什么我在本地用nginx打开页面是能够正常执行脚本的原因。

那为什么我打包以后的文件会是index.umd.cjs的后缀的?

简单提一下,我用的是Vite打包工具,配置项如下:

1
2
3
4
5
6
7
8
9
...
build: {
  lib: {  
    entry: 'src/index.ts',
    formats: ['umd'],  
    fileName: 'index'
  },
}
...

正是fileNameformatsumd的情况下,它会默认将entry入口文件处理成index.umd.cjs的后缀,解决这个问题只需要将fileName改成函数自定义返回即可,详情请自行查看文档。

参考

Built with Hugo
主题 StackJimmy 设计