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牵扯过多为妙:)