一次手动搭建前端项目的全过程

764 阅读9分钟

大家好,最近接了一个需求:基于已有系统拆出一个适配移动端的h5页面,并提供sdk引入方式,完成之后写下此文作为总结,如感兴趣请往下看。

项目背景

  • 本来的项目是umi框架快捷生成的项目,考虑到拆除页面需要轻量级,所以不会沿用umi,cra也不再提供支持,所以整个项目的构建直接手动搭建
  • sdk包需要同时支持原本项目的接入,所以sdk会是独立的一个项目,h5页面单独一个项目
  • 项目仅需要支持一对一聊天即可,后端接口沿用原项目,页面相对简单,打包不会有负担(如果需要打包产出有明显的体积优势,可选择rollup),所以h5页面的打包选择是webpack(社区支持较为丰富,配置较为简单)
  • 最终项目的技术栈是react+webpack+less+ts+websocket 可以先看看项目的最终结构是:

image.png

项目入口准备

首先是package.json文件,这是整个项目的管理文件,我的文件内容如下:

{
  "name": "demo",//项目名称
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "type": "module",//项目代码采用esm,可选esm或cjs
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "webpack --mode development",//打包并区分环境
    "build:pro": "webpack --mode production",
    "start": "webpack serve --open --config webpack.config.js",//通过自己定义的webpack配置启动项目
    "format": "prettier --cache --write .",//代码格式检查
    "prepare": "husky",
    "lint": "eslint src",
    "lint:fix": "eslint src --fix",
    "upload": "chmod +x up.sh && ./up.sh",//上传打包结果到服务器的sh脚本
    "upload:pro": "chmod +x up.sh && ./up-pro.sh"
  },
  "dependencies": {
    "antd-mobile": "^5.37.1",
    "axios": "^1.7.7",
    "moment-timezone": "^0.5.46",
    "react": "18.x",
    "react-dom": "18.x",
    "rxjs": "^7.8.1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@commitlint/cli": "^19.5.0",
    "@commitlint/config-conventional": "^19.5.0",
    ...(内容过多省略了)
    "terser-webpack-plugin": "^5.3.10",
    "ts-loader": "^9.5.1",
    "tslib": "^2.7.0",
    "typescript": "^5.6.3",
    "webpack": "^5.95.0",
    "webpack-cli": "^5.1.4",
    "webpack-dev-server": "^5.1.0"
  }
}

这个入口文件没有太多需要赘述的,注意一下项目命令行脚本配置就好

webpack配置及环境变量

这里可以先构思一下我们需要什么样的配置:

  • 项目后续要上线,所以请求路径需要通过环境变量管理(这里gpt告诉我可以用dotenv,但是webpack可以自己配置环境管理,并且在尽量少依赖的前提下,我选择不用其他依赖)
  • 开发环境下,发网络请求需要通过devServer解决跨域问题,所以需要devServer
  • ts+less项目,需要对应loader(ts-loader, less-loader, css-loader)支持编译,项目采用的是模块化less,所以还需要style-loader
  • 项目主要用于移动端,需要做适配(px转为rem),所以还需要做postcss相关配置

梳理完这些点后,其实配置已经大体清晰了,首先是环境变量:

// config/env.js
const envConfig = {
    development: {
        API_BASE_URL: '/api',
        WS_URL: 'websocke地址',
    },
    production: {
        API_BASE_URL: '生产接口请求路径',
        WS_URL: '生产websocket地址',
    }
};

export default envConfig;

// config/index.js
const config = {
    apiBaseUrl: process.env.API_BASE_URL,
    wsUrl: process.env.WS_URL,
    isDev: process.env.NODE_ENV === 'development',
};
console.info('baseUrl', process.env.API_BASE_URL)

export default config;

然后是关于rem的配置(配置并且引入webpack之后,就可以在启动项目后审查元素查看效果了):

// postcss.config.js
import pxtorem from 'postcss-pxtorem';
export default {
  plugins: [
    pxtorem({
      rootValue: 16, // 你的根元素字体大小,可以根据设计稿调整
      propList: ['*'], // 可以从 px 转换到 rem 的属性列表
      selectorBlackList: [], // 过滤掉一些不转换的选择器
      // 只对 src/styles 文件夹内的样式文件进行转换
      include: /src\/pages\/Welcome/,
      // 排除 node_modules 下的所有文件
      exclude: /node_modules/,
    })
  ]
};

最后就是webpack的配置,我认为需要注意的一些点都写在注释里:

import path from 'path';
import url from 'url';
import HtmlWebpackPlugin from 'html-webpack-plugin';
import { CleanWebpackPlugin } from 'clean-webpack-plugin';
import MiniCssExtractPlugin from 'mini-css-extract-plugin';
import TerserPlugin from 'terser-webpack-plugin';
import CssMinimizerPlugin from 'css-minimizer-webpack-plugin';
import envConfig from './config/env.js';
import webpack from 'webpack';

// 获取当前模块的目录名
const __filename = url.fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

export default (env, argv) => {
  const mode = argv.mode || 'development';
  const currentEnv = envConfig[mode];

  return {
    mode,
    entry: './src/index.tsx',
    output: {
      path: path.resolve(__dirname, 'dist'),
      filename: 'bundle.[contenthash].js',//打包产出文件添加hash,避免上传服务器后被认为文件没改变从而使用缓存,不立即生效的问题
      clean: true, // 启用清理
    },
    resolve: {
      extensions: ['.ts', '.tsx', '.js', '.json'],
      alias: {//路径别名,方便开发,尽量规避相对路径
        "@pages": path.resolve(__dirname, 'src/pages'),
        "@services": path.resolve(__dirname, 'src/services'),
        "@utils": path.resolve(__dirname, 'src/utils'),
        "@components": path.resolve(__dirname, 'src/components'),
        "@public": path.resolve(__dirname, 'src/public'),
        "@contexts": path.resolve(__dirname, 'src/contexts'),
      },
    },
    module: {
      rules: [
        {
          test: /\.tsx?$/,
          use: 'ts-loader',
          exclude: /node_modules/,
        },
        {
        //对于less文件的处理,这里需要注意loader的顺序是从下到上,
        //并且这份配置只支持模块化(import styles from './index.less'这种形式),
        //如果想要支持普通方式(import './index.less'),还需修改
          test: /\.less$/,
          use: [
            {
              loader: 'style-loader',
              options: { esModule: false }
            },
            {
              loader: 'css-loader',
              options: {
                modules: {
                  localIdentName: '[local]--[hash:base64:5]'
                },
                esModule: false,
              }
            },
            'postcss-loader',
            'less-loader'
          ],
        },
        {
          test: /\.css$/,
          use: ['style-loader', 'css-loader'],
        },
        {
          test: /\.(png|svg|jpg|jpeg|gif)$/i,//图片资源
          type: 'asset/resource',
        },
        {
          test: /\.(woff|woff2|eot|ttf|otf)$/i,//字体图标资源
          type: 'asset/resource',
        },
      ],
    },
    plugins: [
      new HtmlWebpackPlugin({
        template: 'index.html',//打包时产出的html文件使用这里定义的html模板
        favicon: path.resolve(__dirname, 'src/public/favicon.ico'), // 添加这行
      }),
      new CleanWebpackPlugin(),
      new MiniCssExtractPlugin({
        filename: '[name].[contenthash].css',
      }),
      new webpack.DefinePlugin({
        'process.env': {//注入环境变量
          NODE_ENV: JSON.stringify(mode),
          ...Object.fromEntries(
            Object.entries(currentEnv).map(([key, value]) => [
              key,
              JSON.stringify(value)
            ])
          )
        }
      })
    ].filter(Boolean),
    optimization: {//生产环境打包体积优化
      minimize: mode === 'production',
      minimizer: [new TerserPlugin(), new CssMinimizerPlugin()],
    },
    devServer: {
      static: {
        directory: path.join(__dirname, 'dist'),
      },
      compress: true,
      port: 8080,
      hot: true,
      proxy: [
        {
          context: ['/api'], 
          target: 'https://your.url.com',//后端接口地址,devServer帮你解决跨域问题
          changeOrigin: true,
          secure: false,
          pathRewrite: { '^/api': '' },
        },
      ]
    }
  };
};

以及项目的html模板和入口样式文件:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <!-- 下面的meta标签参数是解决移动端键盘弹出引起页面缩放的问题 -->
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
    <title>Typex</title>
</head>

<body>
    <div id="root"></div>
</body>
</html>
//index.css
html,
body {
  width: 100%;
  height: 100%;
  overscroll-behavior: none;
  //这里是为了解决移动端部分浏览器的地址栏/工具栏遮挡页面的问题
  padding: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left);
}

最后是当项目完成之后,需要打包发布到测试/生产环境的脚本:

#!/bin/bash

# 获取当前脚本所在目录
SCRIPT_DIR=$(cd "$(dirname "$0")"; pwd)

# 远程服务器信息
REMOTE_USER="ubuntu"
REMOTE_HOST="16.your.host.address"
REMOTE_PATH="/var/www/html/h5/" # 这是服务器文件位置,需要和nginx配置保持同步

# 使用 rsync 将 dist 目录同步到远程服务器
rsync -avz --delete --exclude 'sdk/*' "${SCRIPT_DIR}/dist/" "${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_PATH}"

那么项目的基本配置到此就结束了,还有一些例如eslint的配置因人而异,这里就不贴了 接下来就是项目内的逻辑代码的实现了,那么h5这块就已经完成了。

SDK

SDK这里主要是通过iframe连接到需要引入的页面(例如我们前面的h5页面),并暴露一些使用者可自定义的配置,SDK包提供cdn引入模式(不提供下载模式) 这里我们的SDK通过rollup打包,主要实现方式是:

  • 封装一个类,用户使用时new出我们的实例
  • 将需要给用户使用的方法暴露出来
  • 将内部系统的iframe封装到类内部私有化,只提供方法调用,不允许修改

从下图可以看出,这个sdk的结构非常简单:

image.png 这里rollup配置也特别简单(如下),因为rollup是一个插件化的打包工具,所以大家使用的时候可能更多需要注意的是,当你需要某个功能的时候对于插件的选择,目前来讲我使用过的有一些插件官方不提供,社区支持又不够完善,这是一个比较麻烦的事情:

import typescript from '@rollup/plugin-typescript';
import { terser } from 'rollup-plugin-terser';
import copy from 'rollup-plugin-copy';

export default {
  input: 'src/index.ts',
  output: [
    {
      file: './SDK/typeX.min.js',
      format: 'iife',
      name: 'TypeXSDK',
      sourcemap: true
    }
  ],
  plugins: [
    typescript(),
    terser(),
    copy({
      targets: [
        { src: 'src/assets/icon.png', dest: 'SDK/assets' }, // 复制图像文件到 dist/assets
        { src: 'index.html', dest: 'SDK' }
      ],
      hook: 'writeBundle' // 指定在何时复制文件
    })
  ]
};

完成写逻辑功能的前置事项之后,来到我们主要的sdk类(这里我认为最麻烦的是:我们日常开发使用框架习惯了,通过原生来实现页面及样式会感觉有点吃力😥):

interface SDKOptions {
  type: 'pc' | 'h5';
  mode: 'icon' | 'normal';
  iconUrl?: string;
  iconPosition?: { bottom: string; right: string };
  draggable?: boolean;
  iframeSize?: { width: string; height: string };
  iframePosition?: 'follow' | 'center'; // 新增
  mask?: boolean; // 新增
  draggableIframe?: boolean; // 新增
  styles?: object; // 新增
  showCloseIcon?: boolean;
  source?: string; // 接入来源
  code: string; // 登录状态交换
}

class DemoSDK {
  private options: SDKOptions;
  private icon: HTMLElement | null = null;
  private iframe: HTMLIFrameElement | null = null;
  private isDragging: boolean = false;  // 添加标志以跟踪拖动状态
  private iframeWrapper: HTMLElement | null = null; // iframe 的包裹层
  private maskElement: HTMLElement | null = null; // 遮罩层
  private dragOffsetX: number = 0;  // 用于拖拽的偏移
  private dragOffsetY: number = 0;
  private isFullScreen: boolean = false;  // 全屏状态标志;
  static currentInstance: TypeXSDK | null = null;  // 静态属性跟踪当前实例

  constructor(options: Partial<SDKOptions>) {
    if (TypeXSDK.currentInstance) {
      TypeXSDK.currentInstance.destroy();
    }
    TypeXSDK.currentInstance = this;  // 更新当前实例

    this.options = {
      type: 'h5',
      mode: 'normal',
      iconUrl: 'data:image/png;base64,就是一个url的base64编码==',
      iconPosition: { bottom: '20px', right: '20px' },
      draggable: false,
      iframeSize: { width: '1080px', height: '768px' },
      mask: false,
      code: '',
      draggableIframe: false,
      iframePosition: 'follow', // 默认居中
      showCloseIcon: false, // 默认不打开h5关闭按钮
      styles: {},
      ...options
    };
    this.init();
  }

  private init(): void {
    if (this.options.code.length < 1) {
      console.error("TypeXSDK init failed, code is empty");
      return
    }

    if (this.options.mode === 'icon') {
      this.createIcon();
    }
  }

  // 创建图标的函数
  private createIcon(): void {
    const icon = document.createElement('img');
    icon.src = this.options.iconUrl!;
    icon.style.position = 'fixed';
    icon.style.bottom = this.options.iconPosition!.bottom;
    icon.style.right = this.options.iconPosition!.right;
    icon.style.width = '50px';  // Default icon size
    icon.style.height = '50px';
    icon.style.cursor = 'pointer';
    icon.style.zIndex = '1000';

    if (this.options.draggable) {
      this.makeDraggable(icon);
    }

    document.body.appendChild(icon);
    this.icon = icon;
  }

  private makeDraggable(element: HTMLElement): void {
    let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
    let threshold = 10;  // 设置一个阈值,超过这个值才认为是拖拽
    let moved = false;  // 用于跟踪是否有显著移动

    const dragMouseDown = (e: MouseEvent | TouchEvent) => {
      e.preventDefault();
      // 判断是触摸事件还是鼠标事件,并据此获取位置
      pos3 = 'touches' in e ? e.touches[0].clientX : e.clientX;
      pos4 = 'touches' in e ? e.touches[0].clientY : e.clientY;
      this.isDragging = false;
      moved = false;

      document.onmouseup = closeDragElement;
      document.onmousemove = elementDrag;
      document.ontouchend = closeDragElement;
      document.ontouchmove = elementDrag;
    }

    element.onmousedown = dragMouseDown;
    element.ontouchstart = dragMouseDown;

    const elementDrag = (e: MouseEvent | TouchEvent) => {
      e.preventDefault();
      let currentX = 'touches' in e ? e.touches[0].clientX : e.clientX;
      let currentY = 'touches' in e ? e.touches[0].clientY : e.clientY;
      pos1 = pos3 - currentX;
      pos2 = pos4 - currentY;

      // 如果移动的距离小于阈值,则还没有发生拖拽
      if (!moved && Math.abs(pos1) < threshold && Math.abs(pos2) < threshold) {
        return; // 不更新位置,也不视为拖拽
      }

      moved = true; // 一旦移动超过阈值,标记为拖拽
      this.isDragging = true; // 更新拖拽状态为真

      pos3 = currentX;
      pos4 = currentY;
      element.style.top = (element.offsetTop - pos2) + "px";
      element.style.left = (element.offsetLeft - pos1) + "px";
    }

    const closeDragElement = (e: any) => {
      document.onmouseup = null;
      document.onmousemove = null;
      document.ontouchend = null;
      document.ontouchmove = null;

      // 检查是否发生了显著移动,如果没有则执行点击逻辑
      if (!moved) {
        this.toggleIframe(e);
      }
      // 重置移动标志
      moved = false;
    }
  }

  // 处理iframe的显示和隐藏
  private toggleIframe(e: any): void {
    if (!this.iframeWrapper) {
      if (this.options.type === 'h5') {
        this.createIframe();
      } else {
        this.createIframeWrapper(e);
      }
      if (this.icon) {
        this.icon.style.display = 'none';
      }
    } else {
      const isVisible = this.iframeWrapper.style.display !== 'none';
      this.iframeWrapper.style.display = isVisible ? 'none' : 'block';
      if (this.maskElement) {
        this.maskElement.style.display = isVisible ? 'none' : 'block';
      }
      if (this.icon) {
        this.icon.style.display = isVisible ? 'block' : 'none';
      }
    }
  }

  private createIframe(): void {
    if (!this.iframeWrapper) {
      this.iframeWrapper = document.createElement('div');
      this.iframeWrapper.style.position = 'fixed';
      this.iframeWrapper.style.top = '0';
      this.iframeWrapper.style.left = '0';
    }

    if (this.options.mode === 'normal' || this.options.type === 'h5') {
      this.iframeWrapper.style.width = '100vw';
      this.iframeWrapper.style.height = '100vh';
    }
    this.iframeWrapper.style.backgroundColor = 'rgba(0,0,0,0.5)';
    this.iframeWrapper.style.zIndex = '999';
    this.iframe = document.createElement('iframe');
    this.iframe.src = this.getUrl();
    this.iframe.style.width = '100%';
    this.iframe.style.height = '100%';
    this.iframe.style.border = 'none';
    this.iframe.style.border = 'none';
    document.body.appendChild(this.iframeWrapper);
    this.iframeWrapper.appendChild(this.iframe);
    if (this.options.type === 'h5' && this.options.showCloseIcon) {
      const closeButton = document.createElement('div');
      closeButton.style.position = 'absolute';
      closeButton.style.top = '20px';
      closeButton.style.right = '20px';
      closeButton.style.width = '20px';
      closeButton.style.height = '20px';
      closeButton.style.color = '#ffffff';
      closeButton.style.zIndex = '1000';
      closeButton.style.display = 'flex';
      closeButton.style.justifyContent = 'center';
      closeButton.style.alignItems = 'center';
      const close = document.createElement('img');
      close.src = "data:image/png;base64,这里也是一个url的base64编码=";
      close.style.width = '20px';
      close.style.height = '20px';
      closeButton.appendChild(close);
      closeButton.style.display = 'flex';
      closeButton.style.justifyContent = 'center';
      closeButton.style.alignItems = 'center';
      closeButton.style.fontSize = '20px';
      closeButton.addEventListener('click', () => {
        this.destroy();
      })
      this.iframeWrapper.appendChild(closeButton);
    }
  }

  // 创建蒙层和iframe的容器
  private createIframeWrapper(e: any): void {
    // 创建蒙层
    if (this.options.mask) {
      this.maskElement = document.createElement('div');
      this.maskElement.style.position = 'fixed';
      this.maskElement.style.top = '0';
      this.maskElement.style.left = '0';
      this.maskElement.style.width = '100vw';
      this.maskElement.style.height = '100vh';
      this.maskElement.style.backgroundColor = 'rgba(0,0,0,0.5)';
      this.maskElement.style.zIndex = '999';
      document.body.appendChild(this.maskElement);
    }

    // 创建 iframe 包裹层
    this.iframeWrapper = document.createElement('div');
    this.iframeWrapper.style.position = 'fixed';
    this.iframeWrapper.style.zIndex = '1000';
    this.iframeWrapper.style.width = this.options.iframeSize!.width;
    this.iframeWrapper.style.height = this.options.iframeSize!.height;
    this.applyIframePosition(e); // 应用 iframe 的位置设置

    Object.assign(this.iframeWrapper.style, this.options.styles);

    document.body.appendChild(this.iframeWrapper);

    this.createIframe()

    this.createControls(); // 创建顶部控制条

    if (this.options.draggableIframe) {
      this.makeIframeDraggable();
    }
  }

  // 创建顶部控制栏,支持关闭和全屏
  private createControls(): void {
    const controls = document.createElement('div');
    controls.style.position = 'absolute';
    controls.style.top = '0';
    controls.style.left = '0';
    controls.style.right = '0';
    controls.style.height = '30px';
    controls.style.backgroundColor = 'rgba(255,255,255, 0)';
    controls.style.color = '#000000';
    controls.style.display = 'flex';
    controls.style.alignItems = 'center';
    controls.style.justifyContent = 'space-between';
    controls.style.padding = '0 10px';
    controls.style.cursor = 'move'; // 使控制栏可拖拽

    const closeButton = document.createElement('div');
    closeButton.style.width = '30px';
    closeButton.style.height = '30px';
    closeButton.style.position = 'absolute';
    closeButton.style.top = '0';
    closeButton.style.right = '20px';
    closeButton.style.zIndex = '1';
    closeButton.style.display = 'flex';
    closeButton.style.justifyContent = 'center';
    closeButton.style.alignItems = 'center';
    closeButton.style.cursor = 'pointer';
    const close = document.createElement('img');
    close.src = "data:image/png;base64,这里也是一个url的base64编码";
    close.style.width = '20px';
    close.style.height = '20px';
    closeButton.appendChild(close);
    closeButton.onclick = () => this.destroy();

    const fullscreenButton = document.createElement('div');
    fullscreenButton.style.width = '30px';
    fullscreenButton.style.height = '30px';
    fullscreenButton.style.position = 'absolute';
    fullscreenButton.style.top = '0';
    fullscreenButton.style.right = '50px';
    fullscreenButton.style.zIndex = '1';
    fullscreenButton.style.display = 'flex';
    fullscreenButton.style.justifyContent = 'center';
    fullscreenButton.style.alignItems = 'center';
    fullscreenButton.style.cursor = 'pointer';
    const fullscreen = document.createElement('img');
    fullscreen.src = "data:image/png;base64,这里也是一个url的base64编码==";
    fullscreen.style.width = '20px';
    fullscreen.style.height = '20px';
    fullscreenButton.appendChild(fullscreen);
    fullscreenButton.onclick = () => {
      if (!this.isFullScreen) {
        this.iframeWrapper!.style.width = "100vw";
        this.iframeWrapper!.style.height = "100vh";
        this.iframeWrapper!.style.top = "0";
        this.iframeWrapper!.style.left = "0";
        this.isFullScreen = true
      } else {
        this.iframeWrapper!.style.width = this.options.iframeSize!.width;
        this.iframeWrapper!.style.height = this.options.iframeSize!.height;
        this.isFullScreen = false
      }
    };

    controls.appendChild(closeButton);
    controls.appendChild(fullscreenButton);
    this.iframeWrapper?.appendChild(controls);
  }

  // 应用iframe的位置:居中或跟随
  private applyIframePosition(e: any): void {
    if (this.options.iframePosition === 'center') {
      this.iframeWrapper!.style.top = '50%';
      this.iframeWrapper!.style.left = '50%';
      this.iframeWrapper!.style.transform = 'translate(-50%, -50%)';
    } else if (this.options.iframePosition === 'follow') {
      // this.iframeWrapper!.style.right = '20px';
      // this.iframeWrapper!.style.bottom = '20px';

      const mouseX = e.clientX;
      const mouseY = e.clientY;
      const screenWidth = window.innerWidth;
      const screenHeight = window.innerHeight;
      const width = parseInt(this.options.iframeSize!.width);
      const height = parseInt(this.options.iframeSize!.height);

      let left = mouseX;
      let top = mouseY;

      // 边界处理
      if (mouseX + width > screenWidth) {
        left = screenWidth - width;
      }
      if (mouseY + height > screenHeight) {
        top = screenHeight - height;
      }

      this.iframeWrapper!.style.top = `${top}px`;
      this.iframeWrapper!.style.left = `${left}px`;
    }
  }

  // 使iframe可拖拽
  private makeIframeDraggable(): void {
    const controls = this.iframeWrapper!.querySelector('div');
    let dragging = false;

    const mouseDown = (e: MouseEvent) => {
      dragging = true;
      this.dragOffsetX = e.clientX - this.iframeWrapper!.offsetLeft;
      this.dragOffsetY = e.clientY - this.iframeWrapper!.offsetTop;
      document.addEventListener('mousemove', mouseMove);
      document.addEventListener('mouseup', mouseUp);
    };

    const mouseMove = (e: MouseEvent) => {
      if (dragging) {
        this.iframeWrapper!.style.left = `${e.clientX - this.dragOffsetX}px`;
        this.iframeWrapper!.style.top = `${e.clientY - this.dragOffsetY}px`;
      }
    };

    const mouseUp = () => {
      dragging = false;
      document.removeEventListener('mousemove', mouseMove);
      document.removeEventListener('mouseup', mouseUp);
    };

    controls!.addEventListener('mousedown', mouseDown);
  }

  private getUrl(): string {
    const urls = {
      h5: `https://your.project.com/?user_code=${this.options.code}${this.options.source ? `&source=${this.options.source}` : ''}`,
      pc: `https://your.project.com/?user_code=${this.options.code}${this.options.source ? `&source=${this.options.source}` : ''}`
    };
    return urls[this.options.type];
  }

  // 移除iframe和遮罩层
  public destroy(): void {
    if (this.icon) {
      this.icon.style.display = 'block'; // 重新显示图标
    }

    if (this.maskElement) {
      this.maskElement.remove();
      this.maskElement = null;
    }

    if (this.iframeWrapper) {
      this.iframeWrapper.remove();
      this.iframeWrapper = null;
    }

    TypeXSDK.currentInstance = null;
  }

  public showDemo(): void {
    if (!this.iframe || this.options.code.length > 0) {
      this.createIframe()
    } else {
      console.error('DemoSDK init failed, code is empty')
    }
  }
}

export default DemoSDK;

总结

对于整个项目的搭建到这里就结束了,我觉得像配置文件之类的实现哪里都能搜到,关键在于当我们遇到这样的情况时,如何去构思,如何去选择。 最后,感谢阅读。