preface

Built a wheel and haven’t figured out how to use it for everyone? Read on

First, let’s assume that we’ve painstakingly polished a wheel (say, a generic component for VUe3) and can’t wait to use it to “lighten” the load for everyone else. What shape will the wheel take?

  • It may need to be an NPM package to facilitate project access.
  • You may need typescript to meet the needs of TS projects to enhance the writing experience.
  • You may need to be off the technology stack, such as being able to use it for a React project.
  • Multiple builds may be required to facilitate the CJS environment, script tag introduction, es Module.

Then let’s get started

This article uses vue3+ TS + LESS as an example

0. Preparation

I prepared a simple popover component implemented with VUE3.

<template>
<teleport to="body">
  <div class="modal-mask" v-show="localVisible">
    <div class="modal">
      <div class="modal-header">
        <span>{{title}}</span>
        <button @click="localVisible = false">Shut down</button>
      </div>
      <div class="modal-body">
        <slot></slot>
      </div>
    </div>
  </div>
</teleport>
</template>
<script lang="ts">
import { ref, defineComponent, watchEffect, watch } from 'vue'
export default defineComponent({
  name: 'Modal'.props: {
    visible: {
      type: Boolean.required: false.default: false
    },
    title: {
      type: String.required: false.default: ' '}},emits: ['update:visible'].setup(props, {emit}) {
    const localVisible = ref(false);
    watchEffect(() = > {
      localVisible.value = props.visible;
    });
    watch(localVisible, value= > {
      emit('update:visible', value);
    });
    return{localVisible}; }});</script>

<style lang="less">
.modal-mask {
  position: fixed;
  top: 0;
  left: 0;
  width: 100vw;
  height: 100vh;
  background-color: rgba(0.0.0.45);
  .modal {
    width: 520px;
    margin: 20% auto;
    background-color: #fff;
    position: relative; // Modal body style}}</style>
Copy the code

For component Modal, I designed two prop, title and Visible, as well as a slot to configure the popover body content, where Visible has bidirectional binding.

1. An entry file

You may need to be off the technology stack, such as being able to use it for a React project.

Imagine a React project using this Modal component. Of course, you can’t register the VUE component. This may require a “generic form” of the JS world, a constructor, or a Class.

1.1 Complete the instantiation process

Create a new entry. Ts in the SRC directory, expose a Class, and when the user instantiates it, generate the vue instance (doing the same work as main.ts).

import {createApp, App, h, ref} from 'vue';
import ModalComp from './components/Modal.vue';
export class Modal {
    vm: App<Element>
    constructor(el: Element, params: { title? :string; visible? :boolean;
        'onUpdate:visible'? : (value:boolean) => unknown;
    }) {
        const vm = createApp({
            setup() {
                const visible = ref(params.visible || false);
                return () = > h(
                    ModalComp,
                    {   
                        title: params.title,
                        visible: visible.value,
                        'onUpdate:visible': params['onUpdate:visible'] | | ((value: boolean) = >{ visible.value = value; })}, {/ / todo: slot}}})); vm.mount(el);this.vm = vm; }};Copy the code

Most component parameters are instantiated as Class parameters, which retain the default behavior of the component and pass through the component. But the crucial slots are still missing. For Modal, the slot passes no parameters, and the user can’t touch the responsive data inside the component. You can supply a parameter to renderContent, pass it in as a string, or it can be a function.

constructor(el: Element, params: { title? :string; visible? :boolean; renderContent? :string| (() = >string);
    'onUpdate:visible'? : (value:boolean) => unknown;
}) {

    const renderCardFun = typeof params.renderContent === 'function'
        ? params.renderContent
        : new Function(' '.`return \`${params.renderContent || ' '}\ `; `);
    const vm = createApp({
        setup() {
            const visible = ref(params.visible || false);
            return () = > h(
                ModalComp,
                { // ... },
                {
                    default() {
                        return h('div', {innerHTML: renderCardFun(), class: 'modal-content'}); }})}}); vm.mount(el);this.vm = vm;
}
Copy the code
  • If the incomingrenderContentIs it a string, is it necessary to convert it to a function? It is necessary when the slot has parameter transmission.

This allows the user to write the slot contents through the template string syntax, and the slot is updated with each rendering.

const renderCardFun = typeof params.renderContent === 'function'
    ? params.renderContent
    : new Function('data'.`return \`${params.renderContent || ' '}\ `; `);
// ...
default(data: unkown) {
    return h('div', {innerHTML: renderCardFun(unkown), class: 'modal-content'});
}
// test
// renderCardFun = 'username: ${data.name}';
Copy the code
  • Why is itinnerHTML? The Slot function of vue expects the user to return a virtual DOM generated by a Vue render function (such as JSX). To render such a DOM to a React user would require a lot of vUE background. In addition, JSX in VUE and React is only writing experience, virtual DOM is not compatible, even if writing a whitelist conversion, it can not meet the complex needs of user-defined components, not meaningful. So I choseCommon structure innerHTML, just need a layer of DOM as the container.

At this point, as you may have noticed, the outside world can’t intervene in the visible state after instantiation, so a popover has the opportunity it needs to appear.

1.2 Behavior of exposed components

Visible passed in by the user at instantiation does not explicitly perform bidirectional binding (requiring the user to wrap as a Proxy is common sense, and the user also needs to understand “bidirectional binding”). So, the idea was to expose a pair of show Hidden methods.

The visible ref variable will be modified by show Hidden, where the show hidden method is scoped so that setup is no longer appropriate.

constructor(el: Element, params: { title? :string; visible? :boolean; renderContent? :string| (() = >string);
    'onUpdate:visible'? : (value:boolean) => unknown;
}) {

    const renderCardFun = typeof params.renderContent === 'function'
        ? params.renderContent
        : new Function(' '.`return \`${params.renderContent || ' '}\ `; `);
    const visible = ref(params.visible || false); // The scope is raised to make it easier to use the show method
    const vm = createApp({
        render() { Setup does not include any initialization, use render instead
            return h(ModalComp, { / *... * / }, { / *... * /}); }}); vm.mount(el);this.vm = Object.assign(vm, { // Mix the show and hidden methods into the instance
        show() {
            visible.value = true;
        },
        hidden() {
            visible.value = false; }}); }Copy the code

Since the vue3 example has not yet exposed methods in a production environment, we used manual blending of custom methods.

1.3 the export default

To meet different usage habits, export default should also be provided.

export class Modal {
    vm: App<Element>&{
        show(): void;
        hidden(): void
    }
    constructor(el: Element, params: { title? :string; visible? :boolean; renderContent? :string| (() = >string);
        'onUpdate:visible'? : (value:boolean) => unknown;
    }) { / *... * /}}export default Modal;
Copy the code

The entry file to this Modal component is complete, assuming the user is importing our module, it should be used like this:

// The container node is rendered
const modal = new Modal(el, {/ *... * /});
// when a popover is needed
modal.vm.show();
// When the popover needs to be hidden
modal.vm.hidden();
Copy the code

However, if we give such a source file to the user, the user will have to install Vue less TS themselves and do a lot of configuration to try to parse the Vue project.

2. Build

Finally, it’s time for vite to show the build artifacts to the user so he doesn’t have to bother parsing the source code that he doesn’t maintain.

// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  build: {
    lib: { // Library pattern builds related configurations
      entry: './src/entry.ts'.name: 'Modal'.fileName: 'modal-vue'}},plugins: [vue()]
})
Copy the code
  • Entry: Provides an entry file, which can be a relative path or an absolute path.
  • Name: The variable name of the global object bound when script is introduced, included in the formats'umd''iife'Is a must.
  • FileName: Specifies the output package name. By default, fileName is the name option of package.json.
  • Formats: Modular build, default['es', 'umd']And also supports'cjs''iife'.

performnpm run buildYou get the build artifacts in the dist directory.

Style.css -- Modal-vue.umd. js -- UMD builds for commonJS environments, script introductions, and other modular modal-vue.es module builds, Project introduction to support ES Module.Copy the code

Learn about the Vite-library mode by clicking here

Pay attention to

  • Library pattern builds currently cannot use @Vitejs/plugin-Legacy, so compatibility is a subset of dependencies and source code.
  • The library pattern mentions that you can build a dependency without naming it. This time, because you don’t want users to install and load additional nested dependencies, you choose to build it all.

A type declaration file

You may need typescript to meet the needs of TS projects to enhance the writing experience.

Assuming that the user uses the JS package in a TS project, the user needs to reluctantly write a modal-vue.d.ts file or assert an ANY roughly. A built-in type declaration file prevents this terrible problem from happening.

Create a new index.d.ts in the root directory to declare the values exposed by the component package (corresponding to export in the source code) and any type declarations you want to provide (so you can define some key structures).

// index.d.ts
export declare class Modal {
    vm: {
        show(): void;
        hidden(): void
    } // vue3-App
    constructor(el: Element, params: { title? :string; visible? :boolean; renderContent? :string| (() = >string);
        'onUpdate:visible'? : (value:boolean) => unknown;
    })}export default Modal;
Copy the code

Modal declarations look much smaller than entry files because the type declaration file does not need to contain implementation details. Personal principles for declaration of document contents:

  • The user can correctly invoke the exposed structure with only the declaration file.
  • Input arguments that match the declaration file should be handled by the source code.

For this declaration file to work, you also need to refer to the types directive here in package.json.

{
    / /...
    "types": "./index.d.ts"./ /...
}
Copy the code

4. package.json

It may need to be an NPM package to facilitate project access.

In addition to types, an NPM package also needs to specify entry files for different modularity.

{
    / /...
    "main": "dist/modal-vue.umd.js"."module": "./dist/modal-vue.es.js"./ /...
}
Copy the code

Finally, remove the dependencies, devDependencies option because the artifacts in DIST already contain dependencies.

Above, the package has been synthesized, although not yet released. You can put the project directory (dist, package.json, and index.d.ts) into node_modules for other projects.

portal

  • Published to the NPM