主要代码
给window绑定我们的embed,用于控制嵌入页
/* eslint no-undef: 0 */
/* eslint promise/param-names: 0 */
/* eslint-disable */
//
import iFrameResize from 'iframe-resizer';
import { IOptions } from './models/options';
import {InitOptions} from "./models/InitOptions";
import {PostMessage} from "./models/PostMessage";
import {BtnWidth} from "./models/BtnWidth";
import {EMBED_CARE_RELOAD} from "./models/PostMsgType";
// 随便找一个嵌入页
const IFRAME_URL='https://3oz0g9vvay2n.webertest.top/';
const WEASL_WRAPPER_ID = 'embed-container';
const IFRAME_ID = 'embed-iframe-element';
class Embed {
private debugMode: boolean;
private onloadFunc: (b: any) => void;
private iframe: HTMLIFrameElement | undefined;
private wrapper: HTMLDivElement | undefined;
private floatingBtn: HTMLDivElement | undefined;
private countDiv: HTMLDivElement|undefined;
private options?: IOptions;
private wrapper_display: true | false | undefined = false;
private listeners: { [key: string]: (data: any) => void } = {};
private is_pc:boolean=true;
private init_otps: InitOptions ={} as InitOptions;
private msgCount: number=0;
private floatBtnWidth: number =40;
constructor(onloadFunc = function () {}) {
this.debugMode = false;
this.onloadFunc = onloadFunc;
}
on = (name: string, cb: (data: any) => void) => {
this.listeners[name] = cb;
};
init =async (opts:InitOptions) => {
const def={
locale:'zh-CN',
enable_log:false,
} as InitOptions;
this.init_otps=Object.assign(def,opts);
const that = this;
this.is_pc=this.isPC();
const defaultOptions={
position:{
bottom:'40px',
bottom_mobile:'20px',
right:'20px',
left:'20px',
frame_width:'350px',
btn_width:'60px',
btn_size:'normal',
btn_size_mobile:'normal',
frame_height:'70%',
side:'left',
},
titleList:[
'Hello, do you need help?',
'Hi, welcome to contact me if you have any questions?',
'Welcome to Chat with us.'
],
} as IOptions;
this.options=defaultOptions;
this.floatBtnWidth= this.getFloatBtnWidth();
this.initFloatingButton();
this.initializeIframe();
this.mountIframe();
window.onmessage=(event: any)=>{
// 接收嵌入页的事件,并关闭展开嵌入页
}
window.onresize=()=>{
const old=this.is_pc;
this.is_pc=that.isPC();
if(old!==this.is_pc){
this.log('窗口调整',this.is_pc)
that.calcPosition();
}
}
// 模拟初始化完成后,显示浮动按钮
setTimeout(function () {
that.showFloatingBtn();
},1000)
};
log(...data:any[]){
if(this.init_otps?.enable_log){
console.log(...data);
}
}
setOpts(opts:InitOptions){
this.init_otps=Object.assign(this.init_otps,opts);
if(this.iframe){
this.setIframeSrc(this.iframe)
}
this.log('配置更新','参数->',opts,'新配置->',this.init_otps)
return true;
}
initFloatingButton = () => {
let that=this;
const btn = document.createElement('div');
btn.className = 'embed-floating-btn';
// @ts-ignore
const bottom=this.options?.position[this.is_pc?'bottom':'bottom_mobile'];
(
btn as any
).style = `z-index: ${Number.MAX_SAFE_INTEGER - 10 };
width: ${this.floatBtnWidth}px;
height: ${this.floatBtnWidth}px;
bottom: ${bottom};
background:#408eff;
border-radius: 50%;
cursor: pointer;
box-shadow: 0 0 20px #777777a3;
border: 0;
justify-content: center;
align-items: center;
display: none;
-webkit-tap-highlight-color:rgba(255,255,255,0);
position: fixed;`;
// @ts-ignore
( btn as any).style[this.getFloatingBtnSide()]=`${this.options?.position[this.getFloatingBtnSide()]}`
let inner = document.createElement('div');
(inner as any).style= `
flex: 1;
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
-webkit-tap-highlight-color:rgba(255,255,255,0);
position: relative;`;
let _style=`border-radius: 50%;height:${this.floatBtnWidth / 6 }px;width:${this.floatBtnWidth / 6 }px;background-color:#FFF;`;
let ch1 = document.createElement('div');
(ch1 as any).style=_style;
let ch2 = document.createElement('div');
(ch2 as any).style=_style+`margin:0 ${this.floatBtnWidth / 10 }px;`;
let ch3 = document.createElement('div');
(ch3 as any).style=_style;
inner.appendChild(ch1);
inner.appendChild(ch2);
inner.appendChild(ch3);
let countDiv = document.createElement('div');
(countDiv as any).style=`
border-radius: 50%;
background: #fa3c4c;
color: white;
z-index: ${Number.MAX_SAFE_INTEGER - 9 };
display: none;
position: absolute;
justify-content: center;
align-items: center;
width: ${this.floatBtnWidth / 2.4 }px;
height: ${this.floatBtnWidth / 2.4 }px;
font-size: ${this.floatBtnWidth / 3.75 }px;
bottom: 0;
left: 45%;
`;
inner.appendChild(countDiv);
btn.appendChild(inner);
btn.onclick = ()=>{
that.toggleWrapper(undefined);
}
this.randemTitle(btn);
btn.onmouseleave=()=>{
that.randemTitle(btn);
}
this.floatingBtn=btn;
this.countDiv=countDiv;
document.body.appendChild(btn);
};
showFloatingBtn(){
// @ts-ignore
this.floatingBtn?.style.display='flex';
}
randemTitle=(btn: HTMLDivElement) => {
if (this.options && this.options?.titleList?.length) {
const _index =parseInt((Math.random() * 1000) % (this.options?.titleList.length)+'');
btn.title = this.options?.titleList[_index] as string
}
};
initializeIframe= () => {
let that=this;
if (!document.getElementById(IFRAME_ID)) {
const iframe = document.createElement('iframe');
this.setIframeSrc(iframe);
iframe.onload = () =>{
(iFrameResize as any).iframeResize(
{
log: false,
autoResize: true,
onMessage: ({ message }: any) => { },
enablePublicMethods: true, // Enable methods within iframe hosted page
heightCalculationMethod: 'max',
widthCalculationMethod: 'max',
sizeWidth: true,
},
`#${IFRAME_ID}`
);
};
iframe.id = IFRAME_ID;
(iframe as any).style=`border:none;width: ${this.isPC()?this.options?.position?.frame_width:'100%'};height: 100%;`;
(iframe as any).crossorigin = 'anonymous';
this.iframe = iframe;
}
}
private setIframeSrc(iframe:HTMLIFrameElement) {
let query = '';
for (const file in this.init_otps) {
// @ts-ignore
let val=this.init_otps[file];
if(typeof val === 'object'){
val=escape(JSON.stringify(val));
}else{
val=escape(val);
}
query += `&${file}=${val}`
}
query = query.replace('&', '');
const url=`${IFRAME_URL}?${query}`;
this.log('嵌入链接',url);
if(!iframe.src){
iframe.src = url;
}else{
// 延迟载入页面
setTimeout(function (){
iframe?.contentWindow?.postMessage({
type:EMBED_CARE_RELOAD,
payload:{
url,
}
} as PostMessage,IFRAME_URL);
},1000)
}
}
mountIframe = () => {
let that=this;
if (!document.getElementById(IFRAME_ID) && this.iframe) {
// window.addEventListener('message', this.receiveMessage, false);
const wrapper = document.createElement('div');
wrapper.className = 'wrapper-embed-widget';
// wrapper.style.display = 'none';
wrapper.id = WEASL_WRAPPER_ID;
(wrapper as any ).style = `
z-index: ${Number.MAX_SAFE_INTEGER - 9 };
width: 0;
height: 0;
opacity: 0;
box-shadow: 0 4px 8px 0 rgb(0 0 0 / 20%), 0 6px 20px 0 rgb(0 0 0 / 19%);
overflow: hidden;
background-color:#FFFFFF;
-webkit-transition: width 0.3s, height 0.3s, opacity 0.3s, visibility 0.3s;
transition: width 0.3s, height 0.3s, opacity 0.3s, visibility 0.3s;
position: fixed;`;
wrapper.appendChild(this.iframe);
this.wrapper=wrapper;
document.body.appendChild(wrapper);
this.calcPosition();
}
};
calcPosition(){
if(this.is_pc){
if(this.wrapper){
(this.wrapper as any ).style.bottom=`calc( ${this.options?.position?.bottom} + 55px)`;
// @ts-ignore
(this.wrapper as any ).style[this.getFloatingBtnSide()]=this.options?.position[this.getFloatingBtnSide()];
(this.wrapper as any ).style.borderRadius='8px';
}
}else{
if(this.wrapper){
(this.wrapper as any ).style.bottom=0;
(this.wrapper as any ).style[this.getFloatingBtnSide()]=0;
(this.wrapper as any ).style.borderRadius='0px';
}
}
this.toggleWrapper(this.wrapper_display);
};
private getFloatingBtnSide() {
return this.options?.position?.side || 'right';
}
toggleWrapper(display:true|false|undefined){
let that=this;
let _display=this.wrapper_display;
let wrapper=this.wrapper as any;
if(display===undefined){
_display = !_display;
}else{
_display = display;
}
if(_display===false){
wrapper.style.opacity='0';
wrapper.style.height='0';
wrapper.style.width='0';
}else{
wrapper.style.opacity='1';
if(this.is_pc){
wrapper.style.height=`${this.options?.position?.frame_height}`;
wrapper.style.width=`${this.options?.position?.frame_width}`;
}else{
wrapper.style.height='100%';
wrapper.style.width='100%';
}
}
this.wrapper_display=_display;
}
isPC=() => {
const userAgentInfo = navigator.userAgent;
const Agents = ["Android", "iPhone",
"SymbianOS", "Windows Phone",
"iPad", "iPod"];
let flag = true;
for (let v = 0; v < Agents.length; v++) {
if (userAgentInfo.indexOf(Agents[v]) > 0) {
flag = false;
break;
}
}
if(flag===true&&window.innerWidth<400){
flag=false;
}
return flag;
};
private getFloatBtnWidth() {
const ispc=this.isPC();
let dev=ispc?'pc':'mobile';
let btn_size=ispc?'btn_size':'btn_size_mobile';
this.log('判断设备类型',dev,btn_size);
// @ts-ignore
const size=BtnWidth[dev][this.options?.position[btn_size]]
return size;
}
}
export default ((window: any) => {
const onloadFunc =
window.embed && window.embed.onload && typeof window.embed.onload === 'function' ? window.embed.onload : function () {};
const initCall = window.embed._c.find((call: string[]) => call[0] === 'init');
const embedApi: any = () => {};
const embed = new Embed(onloadFunc);
embedApi.init = embed.init;
embedApi.on = embed.on;
if (initCall) {
// eslint-disable-next-line prefer-spread
embedApi[initCall[0]].apply(embedApi, initCall[1]);
const onCall = window.embed._c.find((call: string[]) => call[0] === 'on');
if (onCall) {
embedApi[onCall[0]].apply(embedApi, onCall[1]);
}
} else {
// eslint-disable-next-line no-param-reassign
(window as any).embed.init = embed.init;
// eslint-disable-next-line no-param-reassign
(window as any).embed.on = embed.on;
}
(window as any).embed.setOpts = embed.setOpts.bind(embed);
})(window);
package.json
build 构建目标脚本,start:docker用于本地测试
{
"name": "care_embed",
"version": "1.0.0",
"description": "客服嵌入脚本",
"main": "dist/embed.umd.min.js",
"module": "dist/embed.es5.min.js",
"typings": "dist/types/embed.d.ts",
"files": [
"dist"
],
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "tsc && cross-env ENVIRONMENT=local rollup -c rollup.config.ts",
"start:docker": "pnpm build && http-server -p 4701 ."
},
"author": "",
"license": "ISC",
"lint-staged": {
"*.{js,jsx,ts,tsx}": [
"eslint"
],
"{*.json,.{babelrc,eslintrc,prettierrc,stylelintrc}}": [
"prettier --ignore-path .eslintignore --parser json --write"
],
"*.{html,md,yml}": [
"prettier --ignore-path .eslintignore --single-quote --write"
]
},
"config": {
"commitizen": {
"path": "node_modules/cz-conventional-changelog"
}
},
"jest": {
"transform": {
".(ts|tsx)": "ts-jest"
},
"testEnvironment": "node",
"testRegex": "(/__tests__/.*|\\.(test|spec))\\.(ts|tsx|js)$",
"moduleFileExtensions": [
"ts",
"tsx",
"js"
],
"coveragePathIgnorePatterns": [
"/node_modules/",
"/test/"
],
"coverageThreshold": {
"global": {
"branches": 90,
"functions": 95,
"lines": 95,
"statements": 95
}
},
"collectCoverageFrom": [
"src/*.{js,ts}"
]
},
"devDependencies": {
"@commitlint/cli": "^7.1.2",
"@commitlint/config-conventional": "^7.1.2",
"@novu/notification-center": "^0.7.1",
"@rollup/plugin-replace": "^2.4.2",
"@types/jest": "27.4.0",
"@types/node": "^14.14.16",
"colors": "1.4.0",
"commitizen": "^3.0.0",
"concurrently": "^5.3.0",
"coveralls": "^3.0.2",
"cross-env": "^5.2.0",
"cz-conventional-changelog": "^2.1.0",
"http-server": "^0.12.3",
"husky": "^1.0.1",
"jest": "^27.0.6",
"jest-config": "^27.4.7",
"lint-staged": "^8.0.0",
"lodash.camelcase": "^4.3.0",
"prettier": "^1.14.3",
"prompt": "^1.0.0",
"replace-in-file": "^3.4.2",
"rimraf": "^2.6.2",
"rollup": "^0.67.0",
"rollup-plugin-commonjs": "^9.1.8",
"rollup-plugin-json": "^3.1.0",
"rollup-plugin-node-resolve": "^3.4.0",
"rollup-plugin-sourcemaps": "^0.4.2",
"rollup-plugin-terser": "^7.0.2",
"rollup-plugin-typescript2": "^0.18.0",
"rollup-plugin-uglify": "^6.0.4",
"semantic-release": "^19.0.3",
"shelljs": "^0.8.3",
"travis-deploy-once": "^5.0.9",
"ts-jest": "^27.1.3",
"ts-node": "^7.0.1",
"tslib": "^2.3.1",
"typescript": "4.1.3"
},
"dependencies": {
"@types/iframe-resizer": "^3.5.8",
"iframe-resizer": "^4.3.1"
}
}
rollup.config.ts文件
配置构建工具rollup
import builtins from 'rollup-plugin-node-builtins';
import globals from 'rollup-plugin-node-globals';
import resolve from 'rollup-plugin-node-resolve';
import { uglify } from 'rollup-plugin-uglify';
import commonjs from 'rollup-plugin-commonjs';
import sourceMaps from 'rollup-plugin-sourcemaps';
import {terser} from 'rollup-plugin-terser';
import camelCase from 'lodash.camelcase';
import typescript from 'rollup-plugin-typescript2';
import json from 'rollup-plugin-json';
import replace from '@rollup/plugin-replace';
const pkg = require('./package.json');
const libraryName = 'embed';
export default {
input: `src/${libraryName}.ts`,
output: [
{
file: pkg.main,
name: camelCase(libraryName),
format: 'iife',
sourcemap: false,
plugins: [],
},
{
file: pkg.module,
format: 'es',
sourcemap: false,
plugins: [],
},
],
// Indicate here external modules you don't want to include in your bundle (i.e.: 'lodash')
external: [],
watch: {
include: 'src/**',
},
plugins: [
replace({
preventAssignment: true,
values: {
'process.env.ENVIRONMENT': JSON.stringify(process.env.ENVIRONMENT),
'process.env.WIDGET_URL': JSON.stringify(process.env.WIDGET_URL),
},
}),
// Allow json resolution
json(),
// Compile TypeScript files
typescript({ useTsconfigDeclarationDir: true }),
// Allow bundling cjs modules (unlike webpack, rollup doesn't understand cjs)
commonjs({ extensions: ['.js', '.ts'] }),
/*
* Allow node_modules resolution, so you can use 'external' to control
* which external modules to include in the bundle
* https://github.com/rollup/rollup-plugin-node-resolve#usage
*/
resolve(),
// Resolve source maps to the original source
sourceMaps(),
// uglify(),
globals(),
builtins(),
terser(),
],
};
在主页面放置嵌入脚本
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<title>嵌入测试</title>
<script>
(function(n,o,t,i,f) {
n[i] = {}; var m = ['init','setOpts']; n[i]._c = [];m.forEach(me => n[i][me] = function() {n[i]._c.push([me, arguments])});
var elt = o.createElement(f); elt.type = "text/javascript"; elt.async = true; elt.src = t;
var before = o.getElementsByTagName(f)[0]; before.parentNode.insertBefore(elt, before);
})(window, document, 'http://127.0.0.1:4701/dist/embed.umd.min.js', 'embed', 'script');
</script>
<script>
embed.init({
locale:'zh_HK',
enable_log:true,
});
// setTimeout(function () {
// const rs=tkcare.setOpts({locale:'en'});
// console.log(rs)
// },3000);
</script>
</head>
<body style="background-color: azure">
</body>
</html>
显示效果
展开的情形
移动端自动全屏