跳到正文
原文
Google AI:DEV 作者专属(RSS)· Elena·· 4 小时前AI 评分36

Chatim 如何让聊天挂件在任意网站上正确运行

A chat widget that runs on websites: what we had to get right

AI 导读

Chatim 为小企业网站提供在线聊天与 AI 聊天机器人挂件,其安装代码用 window.chatim 对象复用避免重复粘贴导致的多挂件问题,并以 cmd 队列让异步加载前的调用按序执行。

正文

I'm Elena, CMO at Chatim. We make a live chat and AI chatbot widget for small business websites, and this is our first post on DEV. I handle marketing, so the engineering below is our team's work. I'm the one who asked the annoying questions and wrote the answers down.

The thing about an embeddable widget is that it runs on sites you don't control. You don't know the framework, the CSS, the tag manager, or how many times someone will paste your snippet. The host site always comes first. Here is what that meant for us in practice, with code.

1. The install snippet has to survive being pasted badly

This is the whole install:

<script>
  window.chatim = window.chatim || {};
  window.chatim.cmd = window.chatim.cmd || [];
  window.chatim.settings = { projectId: "YOUR_PROJECT_ID" };
</script>
<script src="https://widget.chatim.app/widget.js" async></script>

Three boring lines, each there for a reason.

window.chatim = window.chatim || {} exists because the snippet gets pasted twice more often than you would think. A theme footer plus a Google Tag Manager tag is the classic case. "Multiple widgets appearing" has its own section in our troubleshooting docs for a reason. Reusing the existing object means the second copy doesn't wipe out settings the first one already set.

The settings object comes before the script tag so the config exists the moment our code starts running. No waiting, no polling for it.

The script is async, not defer, because the widget has no dependency on anything else on the page and should not hold up anything either. The browser fetches it in parallel and runs it whenever it arrives. MDN has a good explanation of the difference in its script element reference.

2. A command queue, so nobody has to wait for us

The downside of async is that you never know when window.chatim.widget will exist. If a developer wires a "Chat with us" button to window.chatim.widget.open() and a visitor clicks it one second after page load, on a slow connection that call throws.

So the snippet creates window.chatim.cmd, a plain array. Anything pushed into it before the widget loads is executed once the widget boots, in order. Anything pushed after boot runs immediately. Same pattern Google Analytics uses with dataLayer.push in gtag.js, and most analytics SDKs have some version of it.

// Safe to call at any time, loaded or not
document.querySelector("#chat-button").addEventListener("click", () => {
  window.chatim.cmd.push(() => {
    window.chatim.widget.open();
  });
});

We accept three formats in the queue: a function (the one we recommend), an array like ['widget.open'], and an object with method and args. The array form is the legacy one from our first SDK. It stays because it is already sitting in footers we cannot update. That is the recurring theme of this post: once something is pasted into a thousand footers, it is an API forever.

The full method list (open, close, setConfig, getConfig, setParams, restartChat) and the queue formats are in the Widget SDK docs.

3. Custom params are flat on purpose

Sites pass visitor context to the widget so the person answering the chat knows who they are talking to: user ID, plan, cart value, the campaign that brought them. We call these custom params, and we limited them hard:

  • Values can be strings, numbers, booleans, or null. No objects, no arrays, no functions.
  • Max 20 params per visitor. Names up to 50 characters, values up to 500.
  • Names must start with a letter or underscore and contain only letters, digits and underscores.
  • projectId, demo, __proto__, constructor and prototype are reserved and ignored. The last line is the one developers ask about. Params are merged into plain objects, and if we accepted arbitrary keys, __proto__ would be a prototype pollution bug waiting to happen. So those names are rejected up front, before anything else looks at them.

Flatness is a product decision as much as a technical one. Params are displayed in a visitor panel that a support agent reads in the middle of a conversation. Twenty labeled values are scannable. A nested JSON blob is not. Flat values are also trivial to validate and truncate, which matters when the input is coming from code we have never seen.

Params come from three places and merge in a fixed priority order:

  1. URL parameters, lowest priority. If the site owner enables it, the widget captures utm_source, utm_medium, utm_campaign, utm_term, utm_content and ref from the page URL without any code.
  2. window.chatim.settings.params, set in the snippet.
  3. window.chatim.widget.setParams(), called at runtime. Highest priority, and it merges rather than replaces, so you only send what changed.
// After login
window.chatim.widget.setParams({
  userId: user.id,
  email: user.email,
  planType: user.plan
});

// On logout: clears stored chat history and starts a fresh session.
// Matters on shared devices, like a PC in a shop or a library.
window.chatim.widget.restartChat();

4. Next.js: render nothing on the server

The widget touches window and document, and it injects its own DOM after it loads. That is deliberate. Nothing widget-related is ever part of the server-rendered HTML, so there is nothing for React to find a mismatch on during hydration. If you have ever chased a hydration error caused by a third-party script, you know why we care.

For Next.js the whole integration is one client component:

'use client';

import { useEffect } from 'react';
import Script from 'next/script';

export default function ChatWidget() {
  useEffect(() => {
    window.chatim = window.chatim || {};
    window.chatim.cmd = window.chatim.cmd || [];
    window.chatim.settings = { projectId: 'YOUR_PROJECT_ID' };
  }, []);

  return (
    <Script
      src="https://widget.chatim.app/widget.js"
      strategy="lazyOnload"
    />
  );
}

Drop <ChatWidget /> into the root layout and you're done. The ordering works out on its own: useEffect runs after hydration, and lazyOnload loads the script during browser idle time after the page has finished loading, so the settings object is always in place before our script executes. The next/script strategy options are worth reading if you have never looked at them. lazyOnload is the right default for anything that is not needed for the first paint, and a chat bubble is the definition of not needed for the first paint.

One consequence to keep in mind: with lazyOnload, the script can arrive seconds after a page is interactive. Any "open chat" button in your own UI should go through the command queue from section 2, not call widget.open() directly. The step-by-step version with the layout file is in our Next.js install guide.

5. The rule we hold ourselves to on performance

The widget is built with Preact and ships as a single script of about 50 KB gzipped at the time of writing. That covers the launcher and the closed state. The open chat UI is loaded when someone actually clicks, because most visitors never do.

The launcher is fixed-position and takes no space in the document flow, so it cannot shift anything. The script loads after the page content. Our internal rule is simple: Lighthouse scores on the host site should not move when the widget is installed. If yours do, we want the URL. The web.dev guide on loading third-party JavaScript is a good checklist for anyone on either side of this, embedding a widget or building one.

6. Things we are not happy with yet

Writing this down forced us to list the rough edges, so here they are.

No ready event. getConfig() returns null until the widget has initialized, and our docs currently suggest polling with setInterval. That is a workaround, not a design. A ready callback or a promise is the obvious fix and it is on the list.

Legacy globals. window.chatimSettings and window.chatimWidget still work because old installs depend on them. Every release has to keep them working. See the theme above about footers and forever.

Script tag only. There is no npm package and no published TypeScript types for window.chatim. Today you declare them yourself. Whether we ship a package depends a lot on whether people want one, which brings me to the questions.

Questions for people who have shipped embeddable widgets

  • If you have embedded a third-party widget in a React or Next.js app, what annoyed you most? Hydration, CSS leaking in either direction, bundle size, something else?
  • Would you want an npm package and a React component, or is a script tag plus a small client component fine?
  • Twenty flat params, 500 characters each. Too tight, about right, or do you actually need nested data on the agent side? If you want to poke at it, the free plan needs no card. The website live chat widget is the thing described above. Break it on a side project and tell us in the comments. Real bug reports are the best outcome a first post can have.

来源:Google AI:DEV 作者专属(RSS) · dev.to