❝前沿:文章的起源在于开发环境中使用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。下面我跟大家聊聊关于--modern
、Javascript 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是“何方神圣”,让我们进一步熟悉,首先先看看这个属性各浏览器的兼容情况
提到模块,我相信印入你眼帘的会是export
和import
这些关键字,还有就是模块自己的作用域,不会污染全局变量,只在模块中,而对这些特征的使用,也是要你告诉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.
🌲 拓展阅读:
- Deploying ES2015+ Code in Production Today
- JavaScript modules 模块 - JavaScript | MDN
- 在浏览器中高效使用JavaScript module(模块)
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方法是解析模板的核心函数。以下是其中的部分正则