Modern模式引发qiankun的一场“命案”

2,516 阅读7分钟

前沿:文章的起源在于开发环境中使用qiankun框架,父应用加载子应用遇到一则报错 Failed to load module script: The server responded with a non-JavaScript MIME type of "text/html". Strict MIME type checking is enforced for module scripts per HTML spec 直接的翻译意思就是加载模块脚本失败:服务器以非JavaScript MIME类型“text/html”去响应。而也是这个问题引发了我的思考,到底是什么问题导致?

1.思考🤔

通过分析和定位,我怀疑是在是因为我在子应用的vue-cli中使用了--modern模式编译,该模式编译会构建两份js包,一个面向支持现代浏览器的原生 ES6 包,以及一个针对其他旧浏览器的包(后面会详细说明),因为我使用的浏览器支持生 ES6,同时我们知道qiankun父应用引入子应用,本质上是将html做为入口文件,并通过import-html-entry这个库去加载子应用所需要的资源列表(js和css等),应该是因为路径的问题,导致加载js错误,再加nginx配置了try_file导致返回的html。下面我跟大家聊聊关于--modernJavascript module 以及import-html-entry原理

1.1 modern模式

Modern模式究竟是个啥,其实就是vue-cli命令行工具中的一种构建模式, 更多使用可以阅读Vue CLI

根据官网的介绍:--modern模式是使用现代模式构建应用,为现代浏览器交付原生支持的 ES2015 代码,并生成一个兼容老浏览器的包用来自动回退。

换句话讲就是该模式下Vue CLI会构建两个版本的 js 包:一个支持现代浏览器的原生 ES2015+ 的包,以及一个兼容其他旧浏览器的包。

👨‍🎓 阿呆同学:生成两套js?这样做的目的是为了啥

答案: 我们知道低版本浏览器不支持es2015中的新特征,我们往往需要通过Babel来进行转换或者加入polyfill来支持旧版本的浏览器,但随着问题也暴露了,经过babel转换的代码code往往比原本的es2015代码更加冗长。导致文件更加庞大、性能欠缺,但其实目前有不少浏览器对es2015+(既ES6)有这不错的兼容。针对以上两种情况,Vue CLI才提供了Modern模式来解决这个问题,用来生成两套js体系,如下所示一个基于该模式编译后的html入口👇

我们可以清晰看到对js的引入,分为下面两种情况:

  • 支持原生 ES2015+ 的浏览器: js会通过<script type="module"> 加载,并且可以使用 <link rel="modulepreload"> 预加载。
  • 不支持的浏览器: js会使用 <script nomodule> 来加载,同时忽略被定义为Module的js的文件加载

换句话说: 支持 <script type="module"> 的浏览器会忽略 <script nomodule></script> 方式引入的脚本该模式下生成的 HTML 会通过 <script type="module"><script nomodule> 进行自动降级,不需要任何特殊部署配置,原生 ES2015+ 包基本上不需添加 polyfill和Babel进行编译,能为现代浏览器提供更小、很大程度上不需要再编译的代码

👨‍🎓 啊峰 :树酱君,那你上面那个截图中间(夹在nomodule和module中间)还有一段js代码,是干嘛的?

答案:之前说到现代浏览器中都可以通过 <script type="module"> 来引入来使用ES2015+语法的代码,而能识别 type=module语法的浏览器会忽略具有nomodule属性的scripts,但Safari 10.1却不同,它并不支持nomodule属性,他会同时也把nomodule的js文件下载了,但不执行,而上述这段代码则是用来修复这个问题的。

🌲 拓展:

1.2 JavaScript modules

上一小节我们介绍了关于在Vue-cli的Module模式下生成的js文件,其中构建出版本中有一个是支持现代浏览器的原生 ES2015+ 的包,本质上就是依赖 type=module去加载 ES 模块的,而这个module是“何方神圣”,让我们进一步熟悉,首先先看看这个属性各浏览器的兼容情况

提到模块,我相信印入你眼帘的会是exportimport这些关键字,还有就是模块自己的作用域,不会污染全局变量,只在模块中,而对这些特征的使用,也是要你告诉javascript运行环境运行的脚本是普通脚本还是一个JS module

这个时候就得使用上一节我们使用到的 <script type="module"> 来做区分引入模块文件

👩‍🎓 啊琪同学: 普通脚本和JS module之间有什么区别?

答案: 区别如下👇 :

  • 同个文件引入多次 ,JS module只会执行一次,普通脚本则会根据引入次数多次执行和解析
  • 跨域的限制:JS module需要在请求头支持Access-Control-Allow-Origin: *,普通脚本不需要
  • 执行顺序:JS module脚本默认会有defer属性,延迟脚本的执行。普通js脚本默认会阻塞html解析

当然你也可以使用<link rel="modulepreload">让浏览器对模块进行预加载和预编译:模块和它的依赖

我们来对比一下下图,同样一个文件,如果直接用Module引入,相对用babel转译过的文件引入的大小体积区别

👨‍🎓 啊乐同学:我看有些引用的模块文件是使用 mjs 为后缀命名的哦,而不是js命名?

答案:是为了更好的区分引入的哪些文件是模块,哪些文件是常规的Javascript,也能保证你的模块可以被运行时的环境或者构建工具识别如babel和Babel等。

⏰ 要注意的是,如果你是用mjs,则需要在服务器配置mime type类型,否则会报错,和文章前沿描述相似的错误 Failed to load module script: The server responded with a non-JavaScript MIME type of “”. Strict MIME type checking is enforced for module scripts per HTML spec.

🌲 拓展阅读:

1.3 import-html-entry

在qiankun架构中本质上是(single-spa + sandbox + import-html-entry)用html作为入口,就是通过import-html-entry将对的子应用html中的资源进行加载,然后从入口脚本中到导出文档链接 ,看看如何使用这个库

让我们看看这个 importHTML 方法的实现

importHTML(url, opts = {})方法传入参数有两个

  • url:需要解析的html模版路径
  • opts:其中包括如下几个属性:fetch(用于获取远端的脚本和样式文件内容,浏览器支持的请求库)、getPublicPath(用于获取静态资源publicPath,将模板中外部资源为相对路径的转换为绝对路径)、getTemplate(支持在模板解析前,做预处理)

其中还涉及到对js、css等资源加载相关函数,在qiankun中也被使用 链接

  • getExternalStyleSheets: 用于区分css是内联还是外引(style还是link),如果是link则fetch请求资源,然后提取出内容,组成一个数组
  • getExternalScripts: 用来获取模版中出现的所有script标签,提取出内容,组成一个数组
  • execScripts: 执行所有的script中的代码(通过eval),并返回为html模板入口脚本链接entry指向的模块导出对象

👨‍🎓 啊斌同学:那import-html-entry是如何将不同资源进行提取的?

答案: import-html-entry是使用了正则表达式来对进行 style、 link和 script等标签进行提取的,相关正则在process-tpl.js文档链接文件中,processTpl方法是解析模板的核心函数。以下是其中的部分正则

🌲酱往期文章:

请你喝杯🍵 记得三连哦~

1.阅读完记得给🌲 酱点个赞哦,有👍 有动力

2.关注公众号前端那些趣事,陪你聊聊前端的趣事

3.文章收录在Github frontendThings 感谢Star✨