style-loader与iframe的问题
通常情况下其实遇不到style-loader与iframe纠缠的情况,不过由于自己所做项目的特殊性,所以不得不经常与iframe打些交道,并且往往遇到问题能参考的资料也非常有限。
style-loader是webpack的常用插件,作用是将CSS注入进DOM。正因其注入CSS是根据所处运行环境决定,所以如果页面中存在iframe的话,那么就会存在样式注入点与期望不同的情况。由于页面代码可能无法调整执行环境,而样式也无法再不同文档环境相互影响,所以有必要调整注入点。根据情况的不同,运行于父窗口向子iframe注入样式,以及运行于子iframe反过来向父窗口注入,甚至是多层嵌套iframe的情况等都可能遇到(至少我都遇到了)。
在保证同源的大前提下,好在大部分的样式注入库都提供了挂载点变更的配置(比如更早前自己遇到的JSS的修改注入点),甚至是运行时方法,style-loader也不例外,提供了相关配置insert: https://github.com/webpack-contrib/style-loader#insert。由于是所用于打包流程所以也只有配置可用。
module.exports = {
module: {
rules: [
{
test: /\.css$/i,
use: [
{
loader: 'style-loader',
options: {
insert: 'body',
},
},
'css-loader',
],
},
],
},
};
虽然文档并没有展示对insert配置一个function的例子和说明,但insert确实支持`{String|Function}
`。既然没有特别说明清楚,那么可以查看一下其源代码确认使用方式,关键代码(insertStyleElement):
if (typeof options.insert === 'function') {
options.insert(style);
} else {
const target = getTarget(options.insert || 'head');
if (!target) {
throw new Error(
"Couldn't find a style target. This probably means that the value for the 'insert' parameter is invalid."
);
}
target.appendChild(style);
}
由此可见如果配置了一个function回调,那么运行时就会优先执行方法,并传入已经生成好的style元素,所以,在这个回调里只需要简单的获取到相应的窗体即可。这一配置相当灵活和强大,可以在多层嵌套的iframe里精确指定注入点:
{
test: /\.css$/i,
exclude: /src/,
use: [
{
loader: 'style-loader',
options: {
insert: function(style) {
// find iframe document here, parent or children
var head = window.parent.parent.document.querySelector('head');
head.appendChild(style);
},
}
},
'css-loader'
]
}
虽然大部分问题已经迎刃而解,遗憾的是,我自己遇到过的诸多问题并没有这么简单的被全部解决。回到之前的各种场景的问题,同窗体如果只是要改变注入点位置,那么指定insert的target就可以了,但如果牵扯到到iframe,那么还会催生出好几种情况需要继续处理。
其一,父窗体往子iframe注入,虽然通过上面代码中的insert回调可以准确注入,但往往遇到注入时子窗体还没有写入DOM,出现在用前端渲染iframe的情况,此时加载代码已经准备注入样式,而iframe却还没有就位。我没能找到非常顺的处理方式,由于自己是在做iframe开发环境时遇到的,所以就变通了一下,将样式注入在一个临时区域中,然后在iframe中通过MutationObserver来将样式的变动同步过来。
其二,insert的配置必须用es5的写法,由于通常并不编译配置文件,而webpack的这个配置是直接将方法打包进运行代码里的,所以会保持代码原样。所以如果写作箭头函数,将会在低版本浏览器遇报错。
其三,与iframe打交道,时刻要注意注入的范围和影响面,通过使用exclude,include等配置,只控制所需的最小样式。
做完这一茬,基本style-loader与iframe的问题就告一段了。在实际项目中,和iframe纠缠在一起的,往往还有react-dom,还有requirejs等等,大部分都牵扯一个挂载点(注入点)问题,解决这些问题需要时刻清晰的意识到代码在哪里运行,到底是哪个window对象。当然为了保住发际线,还是不要和iframe牵扯过多为妙:)