Web Worker使用和qiankun跨域问题

955 阅读5分钟

Web Worker

不想太多介绍Web Worker,这里主要是想处理qiankun中的跨域问题,Web Worker的介绍可以看使用教程

Web Worker是为JavaScript创建多线程环境,将一些任务分配给后者运行,等到Worker线程完成计算任务,再把结果返回给主线程

注意点

  • 同源限制:分配给Worker线程运行的脚本文件,必须与主线程的脚本文件同源
  • DOM限制:无法读取DOM对象;无法使用document、window、parent。可以访问navigato和location对象
  • 通信联系:必须通过消息完成
  • 脚本限制:不能执行alert、confirm
  • 文件限制:无法读取本地文件

JS

webpack5 - Web Workers

我们先从单个项目,不存在跨域问题项目入手。通过create-react-app创建一个简单的项目.对应的webpack是5.x版本

webpack5开始可以使用Web Workers代替worker-loader

1.app.jsx

// App.jsx

useEffect(() => {
    const worker = new Worker(new URL('./file.worker.js', import.meta.url));
    worker.postMessage({ type: 'start' });
    worker.onmessage = ({ data: { answer } }) => {
      console.log('web worker 接受到的数据', answer);
    };
    return () => worker.terminate();
  }, [])

2.file.worker.js

// file.worker.js

self.onmessage = ({ data: { type } }) => {
    if (type === 'start') {
      self.postMessage({
        answer: 42,
      });
    }
};

3.运行截图

image.png

webpack4 - worker-loader

方式一:修改webpack配置

第二种方式是通过worker-loader

1.config-overrides.js

// config-overrides.js

const { override, addWebpackModuleRule } = require('customize-cra');

module.exports = override(
  addWebpackModuleRule({
    test: /\.worker\.js$/,
    use: { loader: 'worker-loader' },
  })
);

2.file.worker.js

// file.worker.js

self.onmessage = ({ data: { type } }) => {
    if (type === 'start') {
      self.postMessage({
        answer: 42,
      });
    }
};

3.app.jsx

// App.jsx

import Worker from './file.worker.js';

useEffect(() => {
    const worker = new Worker();
    worker.postMessage({ type: 'start' });
    worker.onmessage = function (event) {
      console.log("Received message", event.data.answer);
    };
    return () => worker.terminate();
  }, [])

4.运行截图

image.png

方式二:显示指定 worker-loader 作为加载器处理

1.不需要上面的 config-overrides.js 配置

2.file.worker.js

// file.worker.js

self.onmessage = ({ data: { type } }) => {
    if(type === 'start') {
      self.postMessage({
        answer: 42,
      });
    } 
};

3.app.jsx

// App.jsx

import Worker from 'worker-loader!./file.worker.js';

useEffect(() => {
    const worker = new Worker();
    worker.postMessage({ type: 'start' });
    worker.onmessage = function (event) {
      console.log("Received message worker-loader", event.data.answer);
    };
    return () => worker.terminate();
  }, [])

4.运行截图

image.png

注:还有一些其他配置项,可以通过文档查看

TS

js实现起来还是比较简单的,ts使用的时候可能需要加点配置(也不难)

webpack4 - worker-loader

1.config-overrides.js

// config-overrides.js

const { override, addWebpackModuleRule } = require('customize-cra');

module.exports = override(
  addWebpackModuleRule({
    test: /\.worker\.ts$/,
    use: ['worker-loader', 'ts-loader'],  // 这里加上ts-loader
  }),
);

2.App.tsx

   // App.tsx
    
  import MyWorker from './worker.worker.ts'; // 使用静态加载

  useEffect(() => {
    const worker = new MyWorker();

    worker.postMessage({ type: 'start' });
    worker.onmessage = function (event) {
      console.log("Received message - ts", event.data.answer);
    };
    return () => worker.terminate();
  }, [])

3.worker.worker.ts

self.onmessage = ({ data: { type } }) => {
    if (type === 'start') {
      self.postMessage({
        answer: 42,
      });
    }
};

export {};

4.运行截图

image.png

Webpack5 - Web Worker

ts直接使用web worker会有点特别,webpack将ts转换为js文件,所以需要将worker.ts文件转换为worker.js文件。所以就需要手动将worker.ts文件转换为worker.js文件(具体配置看config-overrides.js)

上面的worker-loader为什么不需要呢?因为这个loader文件进行自动转换并导出一个Worker的构造函数,从而实现直接在主文件中引入ts worker文件的功能

1.config-overrides.js

// config-overrides.js

const { override } = require('customize-cra');

const addMultipleModuleRules = () => config => {
  config.entry = {
    main: './src/index.tsx',
    worker: './src/worker.ts', // 手动将worker ts文件转换为js文件
  };

  config.output = {
    ...config.output, // 保留原有输出配置
    filename: '[name].bundle.js',
  }
  return config;
};

module.exports = override(
  addMultipleModuleRules()
);

2.worker.ts

// worker.ts

self.addEventListener('message', async (e: MessageEvent) => {
  if (e.data.type === 'start') {
    self.postMessage({
      answer: 44,
    });
  }

});
export {};

3.App.tsx

  useEffect(() => {
    const worker = new Worker('./worker.bundle.js'); // 这里引入的是转换后的js文件
    worker.postMessage({
      type: 'start',
    });
    worker.onmessage = function (event) {
      console.log("Received message - ts - webpack5 - webWorker", event.data.answer);
    };
    return () => worker.terminate();
  }, [])

4.打包结果

打包结果可以看到worker.bundle.js

image.png 5.运行截图

image.png

qiankun

文章最开始的时候有提到Web Worker必须遵循同源限制。但是我们知道qiankun往往是不同源的,当然也存在同源的情况,但是这个不是我们今天需要讨论的。

创建DEMO

先创建官网提供的demo

image.png

React15使用web worker

我们先以React15这个例子进行修改配置。因为demo提供的webpack是4.x版本,所以需要使用到worker-loader

1.examples/react15/App.jsx

  // examples/React/App.jsx
  
  import Worker from './worker.worker.js';

  componentDidMount() {
    const worker = new Worker();
    worker.postMessage('Hello from main thread');

    worker.postMessage({
      type: 'start'
    });
  }

2.examples/react15/worker.worker.js

self.addEventListener('message', async (e) => {
  const { type } = e.data;
  if (type === 'start') {
    console.info('react15 - 获取到start消息');
  }
});
export {};

3.examples/react15/webpack.config.js

module: {
    rules: [
      {
        test: /\.worker\.js$/,    // 增加这个rule
        loader: 'worker-loader',
      },
      {
        test: /\.jsx?$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env'],
            plugins: ['@babel/plugin-transform-react-jsx'],
          },
        },
      },
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader'],
      },
    ],
  },

4.启动react15

cd examples/react15
npm start

5.运行截图

image.png

可以看到单独运行的时候是不存在跨域,worker也可以正常使用

qiankun跨域

启动qiankun项目后访问react15,在devtool可以看到报错

image.png 上面的错误提示表明Web Worker模块无法正确实例化为一个构建函数

方式一:worker-loader - inline配置

image.png 1.examples/react15/webpack.config.js

  module: {
    rules: [
      {
        test: /\.worker.js$/,
        use: {
            loader: 'worker-loader',
            options: { 
              inline:"fallback" // 修改inline配置
            }   
        }
    },
      {
        test: /\.jsx?$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env'],
            plugins: ['@babel/plugin-transform-react-jsx'],
          },
        },
      },
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader'],
      },
    ],
  },

react15中的jsx和worker.js都不需要修改

image.png

方式二:Blob URL

1.App.jsx

  componentDidMount() {
    const workerCode = `
      self.onmessage = function(event) {
        console.info(event.type)
      };
    `;
    const blob = new Blob([workerCode], { type: 'application/javascript' });
    const blobUrl = URL.createObjectURL(blob);

    const worker = new Worker(blobUrl);
    worker.postMessage({ type: 'start' });
  }

方式三:workerPublicPath

webpack5新出的workerPublicPath可以将worker输出到指定位置(主应用目录下)

方式四:自定义Web Worker类解决跨域访问

自己写一个处理跨域访问的问题

1.cors-worker.js

export class CorsWorker extends window.Worker {
  constructor(url, options) {
    console.info('url', url)
    const parsedUrl = new URL(url);
    const webpackWorkerOrigin = `__webpack_worker_origin__ = ${JSON.stringify(
      parsedUrl.origin
    )};`;
    const importScripts = `importScripts(${JSON.stringify(url.toString())});`;
    const objectURL = URL.createObjectURL(
      new Blob([`${webpackWorkerOrigin};\n${importScripts}`], {
        type: "application/javascript",
      })
    );
    super(objectURL, options);
    URL.revokeObjectURL(objectURL);
  }
}

代码解释:

  1. 代码功能

    • 这段代码的主要功能是创建一个特定的Web Worker(Worker)实例,通过处理跨域请求的方式来解决跨域问题。在这里,通过创建一个继承自window.Worker的 CorsWorker 类,来自定义处理跨域请求的逻辑。
  2. 构造函数

    • 构造函数接受两个参数 url 和 optionsurl 是要加载的脚本地址,options 是Worker的配置选项。
    • 首先打印出目标url。
  3. 解析URL

    • 使用new URL(url)方法从传入的url字符串中解析出一个URL对象parsedUrl,含有协议、域名、路径等信息。
  4. 生成Webpack Worker Origin 和 importScripts代码

    • webpackWorkerOrigin 定义了一个全局变量 __webpack_worker_origin__,用于记录 Web Worker 的origin(域名),解决跨域问题。
    • importScripts 是一个函数,用于动态加载Web Worker的脚本。脚本地址从url参数中获取。
  5. 创建Blob URL

    • 使用Blob对象创建一个包含webpackWorkerOriginimportScripts的JavaScript脚本的 Blob。
    • 使用URL.createObjectURL(blob)方法将Blob对象转换为一个唯一的Blob URL,作为创建Worker实例时的脚本地址。
  6. 调用父类构造函数

    • 使用super(objectURL, options)调用父类Worker的构造函数,传入Blob URL和配置选项,创建新的Worker实例。
  7. 释放Object URL

    • 使用URL.revokeObjectURL(objectURL)方法释放之前创建的 Blob URL,以避免内存泄漏。

2.App.jsx

  useEffect(() => {
    const workerUrl = new URL('./worker.worker.js', import.meta.url); // 使用动态加载
    const worker = new CorsWorker(workerUrl);
    worker.postMessage({
      type: 'start',
    });
  }, [])

上面需要使用动态加载,也就是需要支持webpack5。webpack4的实现方式需要自行查找

文档

其他

上面简单写了几个解决web worker跨域的问题,其实解决方式当然不止一个,只要解决同源规则的情况下也就解决了跨域问题呗(简直是废话)

如果还有其他解决方式,欢迎一起讨论