简体   繁体   中英

Build an Embeddable single JS file Widget with Vue 3 and Vite

I need to produce a production deliverable (a Widget, basically a customer service chat) in the form of a single.JS file where the user will be able to pass a series of HTML data attributes (basically all of the props that are passed to the Widget component in my Vue implementation) in order to customize things like API endpoints, WebSocket URL, image paths etc. So the question will be how to produce this kind of widget from my Vue implementation:

I'm using Vue 3, Vite and Vuex to admin WS communication:

<!-- Widget -->
<script src="https://cdn.xyz.xyz/widget.min.js" 
   data-icon="path_to_icon.jpg"
   data-title="Brand support"
   data-jwt-url="jwt token url"
   data-rubiko-url="another url"
   data-company-websocket-url="websocket url"
   data-theme="default"
   data-brand-id="brand-id">
</script>
<!-- end of Widget -->

First I have created The Widget component, which is dumb - it will show only what I provide to it from the outside. Basically a series of Props that at the end will be use as html attributes coming from the a.JS file. There's a whole process of passing a JWT in order to authenticate to the API but this is not really important:

<template>
  <div class="widget-container" v-show="isShow">
    <div class="widget-content">
      <section class="widget-header"><h1>Hello There !!</h1></section>
      <div class="widget-body">
        <div class="widget-body-container">
          <h3>
            👋 Hi there {{ endUserName }} {{ endUserLastName }}! Welcome .
          </h3>
          <h4>
            You are <span v-if="isVip">a Vip user</span>
            <span v-else>not a VIP user</span>
          </h4>
          <h4>The WS connection ID is {{ connectionId }}</h4>
          <section class="messageList" v-for="category in categories">
            <p>Category ID is: {{ category.id }}</p>
            <p>Category Name is: {{ category.name }}</p>
          </section>
          <section class="messageList" v-for="category in categories">
            <p>Category ID is: {{ category.id }}</p>
            <p>Category Name is: {{ category.name }}</p>
          </section>
        </div>
      </div>
      <section class="widget-input">
        <form @submit.prevent="sendMessage" class="chat-form relative mt-6">
          <textarea
            name="message"
            id=""
            placeholder="Add your message here.."
            aria-label="Send a message"
            tabindex="0"
          ></textarea>
          <div class="widget-input-buttons">
            <button class="widget-button" type="submit" value="Send">
              <i size="16"
                ><svg
                  width="16"
                  height="16"
                  fill="none"
                  xmlns="http://www.w3.org/2000/svg"
                >
                  <circle
                    cx="8"
                    cy="8"
                    r="6.725"
                    stroke="#757575"
                    stroke-width="1.3"
                  ></circle>
                  <path
                    fill-rule="evenodd"
                    clip-rule="evenodd"
                    d="M5.818 7.534a1.1 1.1 0 100-2.2 1.1 1.1 0 000 2.2zm4.364 0a1.1 1.1 0 100-2.2 1.1 1.1 0 000 2.2z"
                    fill="#757575"
                  ></path>
                  <path
                    d="M10 10c-.44.604-1.172 1-2 1-.828 0-1.56-.396-2-1"
                    stroke="#757575"
                    stroke-width="1.3"
                    stroke-linecap="round"
                  ></path>
                </svg>
              </i>
            </button>
          </div>
        </form>
      </section>
    </div>
  </div>
</template>

<script setup>
const props = defineProps({
  categories: Array,
  connectionId: String,
  endUserName: String,
  endUserLastName: String,
  endUserEmail: String,
  endUserSub: String,
  isShow: Boolean,
  isVip: Boolean,
});
</script>

Then I have a container responsible for the Widget business logic. For the purposes of this POC it will connect to a WebSocket (I have a Vuex Store that is taking care of the communication and error handling with the WS) retrieve the connection ID coming from the WS and then will also retrieve some information as content on the widget from an API.

<template>
  <Widget
    :connectionId="retrieveConnectionId"
    :endUserName="name"
    :endUserLastName="lastName"
    :endUserEmail="email"
    :endUserSub="sub"
    :isVip="isVip"
    :categories="categories"
    :isShow="isShow"
  />
  <WidgetTrigger @click="isShow = !isShow" :isOpened="!isShow" />
</template>

<script>
import { ref, onMounted } from "vue";
import { Buffer } from "buffer";
import { useActions } from "vuex-composition-helpers";
import { useGetters } from "vuex-composition-helpers";
import Widget from "@/components/Widget.vue";
import WidgetTrigger from "@/components/WidgetTrigger.vue";
export default {
  components: {
    Widget,
    WidgetTrigger,
  },
  setup() {
    const categories = ref([]);
    const email = ref("");
    const error = ref(null);
    const isShow = ref(false);
    const isVip = ref();
    const lastName = ref("");
    const license = ref("");
    const name = ref("");
    const token = ref("");
    const sub = ref("");
    const { callWebsocket } = useActions({
      callWebsocket: "websocket/processWebsocket",
    });
    const { retrieveConnectionId } = useGetters({
      retrieveConnectionId: "websocket/getConnectionId",
    });
    const retrieveSignedJWT = async () => {
      fetch("http://xyz")
        .then(async (response) => {
          const data = await response.text();
          // check for error response
          if (!response.ok) {
            // get error message from body or default to response statusText
            const error = (data && data.message) || response.statusText;
            return Promise.reject(error);
          }
          let jwt = data;
          token.value = data;
          decodeToken(jwt);
          retrieveCategories();
        })
        .catch((error) => {
          error.value = error;
          console.error("There was an error!", error);
        });
    };
    const retrieveCategories = async () => {
      fetch(
        " http://xyz/categories",
        {
          method: "GET",
          headers: {
            "Content-Type": "application/x-www-form-urlencoded",
            Authorization: `Bearer ${token.value}`,
          },
        }
      )
        .then(async (response) => {
          const data = await response.json();
          // check for error response
          if (!response.ok) {
            // get error message from body or default to response statusText
            const error = (data && data.message) || response.statusText;
            return Promise.reject(error);
          }
          categories.value = data;
        })
        .catch((error) => {
          error.value = error;
          console.error("There was an error!", error);
        });
    };
    const decodeToken = async (jwt) => {
      let base64Url = jwt.split(".")[1];
      let base64 = base64Url.replace("-", "+").replace("_", "/");
      let decodedData = JSON.parse(
        Buffer.from(base64, "base64").toString("binary")
      );
      sub.value = decodedData.sub;
      name.value = decodedData.name;
      lastName.value = decodedData.last_name;
      email.value = decodedData.email;
      isVip.value = decodedData.is_vip;
    };
    onMounted(async () => {
      await retrieveSignedJWT();
      await callWebsocket();
    });
    return {
      categories,
      callWebsocket,
      decodeToken,
      email,
      error,
      retrieveConnectionId,
      retrieveSignedJWT,
      retrieveCategories,
      isShow,
      isVip,
      lastName,
      license,
      name,
      retrieveConnectionId,
      sub,
      token,
    };
  },
};
</script>

So far everything's working as expected but I can't find feedback on which is the right approach to have a single.JS file in the form of a widget where I can use HTML data attributes so the widget can use them as props.

you can use Lit Element

an example of use is google model viewer

it uses rollup (look at roolup.config.js) to makes a single js, and you can add a custom tag to your html, it has no dependecies so it can be used with any framework

eg:

<script type="module" src="generated.js"></script>
<yourcustomtag withstuff="1">

I'm not sure if you can write a lit element using Vue, but you can still use rollup for your widget. I don't known Vue and how it can be standalone (lit element uses shadow dom so it's designed to be standalone)

There are two approaches for injecting the JavaScript scripts:

1.You load it from an external source – a CDN or from a separate file. 2.You inject the script inside the HTML element. However, sometimes some scripts are large and if you need to run on a specific page only, then it is recommended to add it locally.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM