The original link

Demand background

Since the customer service chat component needs to be opened in the form of a new window, that is to say, it needs to be opened in the new window of Window. open, and at the same time it needs to be embedded into the company’s own product end, we have encountered a lot of problems in the process of technical breakthrough, this is to record

Problems and solutions

Those marked with a horizontal line are those that do not need to be done at this stage after completing the requirements

  1. Communication between Windows across domains:Write the underlying communication library, passed by URL
  2. Embedded in the company product side: the running environment for the standalone customer service chat component
  3. Setting the location of the new window: Pay attention to the compatibility of split screen
  4. Listen for new window opening:Send messages through the underlying library, gets the closed attribute of window.open
  5. Listen for the new window to finish loading:Send messages through the underlying library
  6. Chat message input HTML characters: Match certain characters for HTML rendering, other characters as text rendering

The concrete solution

Inter-domain window communication

Low-level library writing

Because we need to know which skill group (equivalent to group) to open, and need to transfer the relevant information of skill group, we need to write the underlying library first, the core code is as follows

export default class Bus {
    private syncFns: Array<SyncFn> = []

    constructor(public origin: string, syncFn: SyncFn | Array<SyncFn>) {
        this.init()
        this.syncFns = Array.isArray(syncFn) ? syncFn : [syncFn]
    }

    private init(): void {
        window.addEventListener("message".this.receiveMsg.bind(this), false)}// Get the cross-domain message
    private receiveMsg(e) {
        const { origin } = e
        if (this.origin ! = =The '*'&& origin ! = =this.origin) return
        const { name, data } = e.data
        this.syncFns.forEach(item= > {
            if (item.name === name) {
                item.fn(data, e)
            }
        })
    }

    // Send cross-domain messages
    postMessage(windowInstance: object, swMessage: SWMessage, targetOrigin: string) {
        windowInstance && windowInstance.postMessage(swMessage, targetOrigin)
    }
}
Copy the code

To transfer data

Because the above code does not know when the new window is loaded, sending the message in advance will have no effect, so set the delay

// home page index.html, accessed at http://localhost:10001
var caller = new Bus('http://127.0.0.1:10001'[{name: 'message'.fn: (data) = > {
      console.log('Received message for new window', data); }},])document.querySelector('#button').addEventListener('click'.function() {
  var popup = window.open('http://127.0.0.1:10001/popup.html'.' '.'width=300,height=300,top=100,left=200');
  setTimeout(() = > {
    caller.postMessage(
      popup,
      {
        name: 'nickname'.data: {
          name: 'towavephone'}},'http://127.0.0.1:10001')},2000)})Copy the code
/ / the new popup window page. HTML, access to the domain name at http://127.0.0.1:10001/popup.html
var caller = new Bus('http://localhost:10001'[{name: 'nickname'.fn: (data, e) = > {
      console.log('Received message from main page', data);
      caller.postMessage(
        e.source,
        {
          name: 'message'.data: {
            name: `hello ${data.name}`}},'http://localhost:10001')}},]);Copy the code

Execution effect:

Listen for the new window to finish loading

The delay set by setTimeout is not accurate. The following solution is required

Listen for window.open’s onload event (cross-domain failure)

Var caller = new Bus('http://127.0.0.1:10001', [{name: 'message', fn: (data) => {console.log(' received message for new window ', data);}},]) document.querySelector('#button').adDeventListener ('click', = function () {var popup window. The open (' http://127.0.0.1:10001/popup.html ', ' 'and' width = 300, height = 300, top = 100, left = 200 '); popup.onload = function() { caller.postMessage( popup, { name: 'nickname', data: { name: 'towavephone'}}, 'http://127.0.0.1:10001')}})Copy the code

At first I tried to listen for the onload event of window.open, but an error was reported under different domains

The cross-domain sending message detection load is complete

Var isLoaded = false; Var caller = new Bus('http://127.0.0.1:10001', [{name: 'message', fn: (data) => {console.log(' received message for new window ', data);}}, {name: 'newWindowLoad', fn: () => {isLoaded = true; console.log(' new window loaded ');}}]) document.querySelector('#button').adDeventListener ('click', = function () {var popup window. The open (' http://127.0.0.1:10001/popup.html ', ' 'and' width = 300, height = 300, top = 100, left = 200 '); isLoaded = false; var timer = setInterval(() => { if (! isLoaded) { return; } timer && clearInterval(timer); Caller. PostMessage (popup, {name: 'nickname', data: {name: 'towavephone'}}, 'http://127.0.0.1:10001')}, 0); })Copy the code
/ / the new popup window page. HTML, visit the domain name http://127.0.0.1:10001/popup.html var caller = new Bus (' http://localhost:10001 ', [{name: 'nickname', fn: (data, e) => {console.log(' received message from home ', data); { name: `hello ${data.name}` } }, 'http://localhost:10001' ) } }, ]); caller.postMessage( window.opener, { name: 'newWindowLoad', }, 'http://localhost:10001' )Copy the code

The newWindowLoad message is sent when the page of the new window isLoaded. After the main page is received, the flag variable isLoaded is set to true, and the polling message can be sent.

Pass the data through the URL

After that, the current skill group should be kept open after the new window is refreshed. In the above scheme, data will be lost after the new window is refreshed, so data will be transferred through URL

Independent customer service component running environment

Since the customer service component needs to be embedded in the product side of the company, because the technology stack used is the same as the technology stack used on the product side of the company, many problems will be encountered:

  1. React 16 and React 15 cannot be added to the same page at the same timeGithub.com/facebook/re…
  2. Babel-polyfill is introduced twice causing it to not load properly

Independent operating environment

Since it is normal in the development server, the conflict occurred after compiling to the production version, first of all, I thought it was the conflict of the js number of the generated file, so I made the following changes

optimization: {
  // The module file path is used as the key of the module to be loaded. If this parameter is not set, the incrementing key is used by default
  namedModules: true.// Same for subcontract documents
  namedChunks: true,}Copy the code

When react and React – Lite coexist, an error will be reported unless the scripts of the customer service components are loaded before the product side of the company. In this case, the index.html files of each product side will need to be moved. However, in this case, the changes will have a large impact. It is also difficult to read environment variables in index. HTML

Finally, I found that all loading files on the product side are under the webpackJsonp variable by default, and the customer service component projects need to be loaded into different variables to achieve isolation in the environment

output: {
  filename: '[name].js'.chunkFilename: '[name].[chunkhash].chunk.js'.// The key step is to make multiple Webpacks run on the same page without affecting each other
  jsonpFunction: 'ponyWebpackJsonp',},Copy the code

For details, please refer to Module Federation

Babel-polyfill is introduced twice

In this case, because babel-Polyfil is introduced, we need to set global._babelPolyfill = true in the customer service component project; This line is shielded, to prevent the company’s product end error, temporarily did not think of a better way

// global._babelPolyfill = true;
Copy the code

Sets the new window location

We need to pay attention to the situation of split screen. The positioning of the new window on the secondary screen is wrong and special treatment is needed. There is no problem on the main screen

// Compatible with left and right split screen
const {
  availTop,
  availLeft,
} = window.screen;
if (availLeft) {
  left += availLeft;
}
if (availTop) {
  top += availTop;
}
Copy the code

Analysis of the left center location of chrome popup in dual-screen mode

Listen for new Windows to open

By listening for the Closed property of window.open, because multiple new Windows are open, you need to traverse the Window. open array

this.popups = [];
const popup = window.open(url, ' ');
this.popups.push(popup);

// Get a new window that opens
this.popups.filter((item) = >! item.closed);Copy the code

Address chat box code injection

Since the chat emoji is made up of a limited number of characters in the [happy] format, it is simply a matter of matching these characters and rendering them as dangerouslySetInnerHTML, with the rest rendered as normal text characters

// EmojiList: list of emojis, symbolToHTML: method for converting a particular character to HTML
renderText = (text) = > {
  const renderContent = [];
  text.replace(/\[([^[\]]+?)\]|./g.(symbol) = > {
    if (/ \ [([^ [\]] +?) \] /.test(symbol) && EmojiList.find((item) = > item.symbol === symbol)) {
      renderContent.push(<span
        dangerouslySetInnerHTML={{ __html: symbolToHTML(symbol) }}
      />);
    } else{ renderContent.push(symbol); }});return (<div>
    {renderContent}
  </div>);
}
Copy the code

Effect of screenshots

conclusion

  1. The URL passed to the new window is too long and requires encoding
  2. React conflict in the independent operating environment may be caused by scope promotion, which needs to be studied in detail
  3. Babel-polyfill is introduced twice, how to reuse ployfill code to be studied