在开发Monibuca的过程中,为了方便访问每一个插件的界面,我们需要将所有插件的自定义的界面集中在一起显示。 我们需要实现一下功能:
为了方便访问每一个插件的界面,我们需要将所有插件的自定义的界面集中在一起显示。
我们需要实现一下功能:
- 在主界面中可以动态加载插件的界面,并实现切换
- 可以将参数传入插件界面中。
- 显示插件界面要快速流畅。
可供选择的方案有:
- 使用iframe加载各个插件的界面
- 使用vue动态编译
- 使用vuecli的编译成WebComponent方式
其中方案1是最差选择,iframe有各种弊端,现在的趋势是尽量不使用iframe方式。
方案2是一个可以实现的方案,缺陷是无法利用vue的编译特性,需要手动管理前端资源,不利于工程化。
方案3是最佳方案,可以完美的避开上述两个方案的所有缺点。
方案三用到的技术:https://cli.vuejs.org/guide/build-targets.html#web-component
当然在采用方案3的过程中也并非一帆风顺。其中最大的问题是CSS样式加载的问题。由于WebComponent的特殊性,WebComponent内部的CSS和外部是完全隔离的。所以需要单独加载CSS。在我们的项目中,采用的是iview的UI框架,所以需要载入iview的css文件才能正常显示。
第一次尝试:动态添加link标签
最初想到的自然是用js动态添加link标签,由于vue文件中,如果要取得dom元素,必须要等到mounted函数中才能操作,所以有一段时间的界面显示错乱。
let linkTag = document.createElement('link');
linkTag.href = "iview.css";
linkTag.setAttribute('rel','stylesheet');
linkTag.setAttribute('type','text/css');
el.appendChild(linkTag);
第二次尝试:使用import方式
比第一种方式更优雅的是在vue文件中的style标签里写入import
<style>
@import url("/iview.css")
</style>
这样就能动态加载css文件了。但是弊端很快也出现了,就是每次加载WebComponent,都会再次加载这个css文件,页面上还是会有一段时间的错位。那么如何才能避免每次渲染组件时加载css文件呢?
第三次尝试:使用动态注入css对象方式
为了深入理解WebComponent的样式机制,打开
https://github.com/w3c/webcomponents 查看官方说法。
仔细翻找有关CSS的内容,找到这个例子:
import styles from "styles.css";
document.adoptedStyleSheets = [...document.adoptedStyleSheets, styles];
经过尝试,在vue文件内部import写法无法构造出CSSStyleSheet对象,于时放在父页面的html里面
<script type="module">
import styles from "iview.css";
window.iviewCSS = styles
</script>
试图通过window这个全局变量来操作。结果也是失败而告终。报错信息的意思是加载的文件MIME头必须是javascript,而我返回的是stylesheet。
此时我想到一个办法,既然无法直接导入,那我手动构建一个CSSStyleSheet对象不就行了?
最先尝试直接拿父页面的document.stylesheets传入WebComponent中,结果报错:必须使用带构造函数的CSSStyleSheet对象,WTF。
https://wicg.github.io/construct-stylesheets/#dom-cssstylesheet-cssstylesheet
查文档,要使用构造的CSSStyleSheet必须用js new出来即:
var style = new CSSStyleSheet()
才能传入shadowRoot.adoptedStyleSheets
那外部的CSS文件怎么传入到CSSStyleSheet对象中呢。从文档上看,加载CSS文件的方式已经全部堵死。
CSSStyleSheet(options)
When called, execute these steps:
Construct a new CSSStyleSheet object sheet with the following properties:
location set to the base URL of the associated Document for the current global object
No parent CSS style sheet.
No owner node.
No owner CSS rule.
title set to the title attribute of options.
Set alternate flag if the alternate attribute of options is true, otherwise unset the alternate flag.
Set origin-clean flag.
Set constructed flag.
Constructor document set to the associated Document for the current global object.
If the media attribute of options is a string, create a MediaList object from the string and assign it as sheet’s media. Otherwise, serialize a media query list from the attribute and then create a MediaList object from the resulting string and set it as sheet’s media.
If the disabled attribute of options is true, set sheet’s disabled flag.
Return sheet.
最后,用一个比较粗暴的方式解决了这个难题。
const appStyle = new CSSStyleSheet();
const appCSSs = document.styleSheets;
for (var i = 0; i < appCSSs.length; i++) {
for (var j = 0; j < appCSSs[i].cssRules.length; j++) {
appStyle.insertRule(appCSSs[i].cssRules[j].cssText);
}
}
我们遍历了父页面所有的样式规则,通过调用insertRule来手工填充CSSStyleSheet对象。
其中就包含了link标签里面载入的CSS文件的CSS规则。然后我们再赋给shadowRoot。最终结果是WebComponent不再需要import CSS,去下载CSS,页面瞬间渲染完成。