Follow me on my blog shymean.com

I mentioned previewing Vue components online in the last article, but I thought it would be easier to solve the problem of low-code editing custom components if I could load remote modules in an existing project. This paper tries to load remote modules in Vite.

The code for this article is available on Github and can be downloaded directly from NPM

npm i vite-plugin-remote-module
Copy the code

Usage scenarios

First describe the scenario for loading the remote module.

In a low-code platform (or some other similar scenario), a large number of custom components need to be extended to meet business requirements.

Developers write custom components, upload them to the server, and load and use them in the component list when using a low-code editor. One problem here is how these custom components should be saved.

One way is to save packaged javascript module files, which can be used on the client side through systemJS, the IMPORT HTTP module of es Module, or even the original script tag.

The biggest disadvantage of this save approach, which is similar to publishing to the NPM repository, is that every save requires recompilation, packaging, and uploading, which can be very cumbersome for debugging and debugging.

Another way is to save the component source files directly without going through any packaging process. Instead, you participate in compilation and packaging with the page editor at run time.

This is equivalent to postponing the compilation step until the full page is finally published. However, this requires the page editor to run in a development-like environment to support parsing packaged component source files.

And the idea is that it’s going to be something like this

<script setup>
import Demo from 'http://localhost:9999/demo.vue'
</script>

<template>
    <Demo/>
</template>
Copy the code

The demo. Vue file is supposed to be a complete VUE SFC file, and the module is imported as if it were a native component

import Demo from './demo.vue'
Copy the code

Of course, the above code is certainly not implemented at present, this article is to solve this problem.

The development environment

For local development, first prepare a static resource directory, and then start a static resource server

mkdir static && cd static
php -S localhost:9999
Copy the code

You can then put something under the static directory that you will use to create a remote module later, such as a demo.vue file or something

Then there is the front-end development environment for testing the loading of remote modules, using Vite

npm init vite@latest
Copy the code

Follow the prompts to create a directory, and then

npm i
npm run dev
Copy the code

By default, localhost:3000 is enabled, and then we modify the code in app.vue to the above test code.

First, you’ll see the first error: CORS, cross-domain.

Because Vite uses script moudle, it is restricted by the same origin policy. To solve this problem, you can configure CORS, or put the file under the vite project public and change the path to localhost:3000.

Once the cross-domain problem is resolved, a second error occurs

Refer to the MDN documentation. This is because any remote module loaded by aScript Module is considered a JavaScript module, and a Vue file with an empty MIME cannot be treated directly as a JavaScript module.

Remote Module -> Virtual module

Rollup provides the functionality of a virtual module, which is also used by the Vite plugin. Since importing HTTP URL modules directly is not an option, compromise by using virtual modules instead.

The general idea is

  • Specify a special import alias, similar to@,~That’s what we use here@remote/As a remote module
  • When the packer recognizes that a remote module needs to be loaded, it resolves the path, downloads the remote module locally on the Node side, and returns the local file as module content

Implement the plugin along these lines

export default function remoteModulePlugin() {
  return {
    name: "vite-plugin-remote-module".async resolveId(id) {
      if (/@remote\//.test(id)) {
        const [url] = id.match(/https? . *? $/igm) | | []if(! url)return id
        return await downloadFile(url)
      }
    },
  };
}
Copy the code

Then implement the downloadFile method, which uses Request.pip () directly to save trouble and does not take into account exceptions such as error compatibility and file name duplication

const path = require("path");
const fs = require("fs-extra");
const request = require('request')

function downloadFile(remoteUrl, localPath = `.remote_module`) {
  const folder = path.resolve(__dirname, localPath)
  fs.ensureDirSync(folder)

  const filename = path.basename(remoteUrl)
  const local = path.resolve(folder, `. /${filename}`)

  return new Promise((resolve, reject) = > {
    let stream = fs.createWriteStream(local);
    request(remoteUrl).pipe(stream).on("close".function (err, data) {
      if(err) reject(err) resolve(local) }); })}Copy the code

We’re done. Write some code to verify it. The first step is to register the plug-in in viet.config.js,

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

import remotePlugin from './remotePlugin'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    vue(),
    remotePlugin() / / new]})Copy the code

Then load the remote module in app.vue

<script setup>
import Demo from '@remote/http://localhost:3000/demo.vue'

</script>

<template>
  <Demo/>
</template>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>
Copy the code

Restart the service and open the browser again to see that the remote plug-in has been downloaded and parsed normally, as well as the downloaded remote file in the project directory. Remote_module

Vite is so good!!

Handling dynamic loading

Now we have taken the crucial step of loading the remote module directly through import. In real development, you often have to deal with scenarios where remote modules are dynamically loaded

Asynchronous loading

Vite and Script Module themselves support asynchronous loading

<script setup> import {shallowRef} from 'vue' const compRef = shallowRef(null) async function loadDemo1() { const ans = await import('@remote/http://localhost:3000/demo.vue') compRef.value = ans.default } </script> <template> <div Class ="preview"> <div class="preview_sd"> Component list <br> < button@click ="loadDemo1">demo1</button> </div> <div class="preview_mn"> <component :is="compRef" v-if="compRef"></component> </div> </div> </template> <style lang="scss"></style>Copy the code

This way you can handle code cutting and asynchronous loading

Dynamic loading

One problem with asynchronous loading is that the path in the import argument must be determined at compile time, and dynamic arguments cannot be passed, so this is not possible

function loadRemoteComponent(url) {
  return import(url).then(ans= > {
    return ans.default
  })
}
Copy the code

However, in the scenario mentioned at the beginning of this article, for a list of components, the resource file corresponding to the component needs to be returned through the interface, and the resource path cannot be determined at compile time.

Fortunately, rollup supports dynamic loading. See rollup plugin/dynamic-import-vars.

The idea is that if an import URL contains variables, it is compiled into a BLOB pattern (similar to a re), and all module files that match the matching rule are loaded in, and the correct module is returned at run time based on the parameters.

With that in mind, it’s easy to understand some of the limitations of this plug-in

  • The path must be. /or../At the beginning, it is convenient to determine the directory qualified by the final matching rule
  • The path must contain a file suffix to facilitate the exclusion of files that are not of the expected module type
  • If I’m loading. /The current path of the file, need to know the file name matching pattern, such as./${x}.jsIt’s not allowed. It has to be./module-${x}.jsSpecify file namemodule-xxx.jsThe file
  • If a variable named directory exists in the path, at most one layer will be generated*Directories, for example/${x}${y}/It will only generate/ * /Rather than/ * * /

These restrictions seem to be made to narrow down the number of files that eventually match the rules (after all, all files that match the rules are included at compile time)

In Vite, this qualified detection is implemented using the import-Analysis plug-in

So for the loadRemoteComponent method above, the following prompt appears on the console

You can skip this warning, but it still affects module loading

import(/* @vite-ignore */url)
Copy the code

So for a list of URL components that need to be loaded dynamically, it is not easy to implement a clean version of dynamic import, so it needs to do a bit of hacking.

ResolveId (resolveId, resolveId, resolveId, resolveId, resolveId, resolveId

function loadRemoteComponent(url) {
  return import(`./@remote/${url}? suffix=.js`).then(ans= > {
    return ans.default
  })
}
Copy the code

When this is done, the console warning disappears!

Don’t count your chickens before they hatch! The warning is gone, but whether the corresponding bloB can match the file is uncertain.

The essence of dynamic import is to package all modules that meet the matching rules, and then return a module that matches the parameters exactly at runtime. This means that the corresponding file must be downloaded now before import() can be called.

Fortunately, Vite provides a configureServer plug-in configuration item that registers the connect server middleware, so you can intercept the corresponding import request here and download the file here

configureServer(server) {
  server.middlewares.use(async (req, res, next) => {
    const id = req.url
    if (isRemoteModuleId(id)) {
      const url = parseUrl(id)
      if (url) {
        await downloadFile(url)
        next()
        return
      }
    }
    next()
  })
}
Copy the code

Bingo! This allows us to fool Vite’s import analysis and download the corresponding remote module through the middleware before requesting it, which seems pretty close to our goal.

To test this, suppose we have three custom components components

const componentList = [
  {id: 1.name: 'demo'.url: 'http://localhost:3000/demo.vue'},
  {id: 2.name: 'demo2'.url: 'http://localhost:3000/demo2.vue'},
  {id: 3.name: 'demo3'.url: 'http://localhost:3000/demo3.vue'},]Copy the code

When a component is clicked, it needs to be dynamically loaded and displayed in the preview area on the right

You can see it’s working!!

Refresh the module cache

The detailed process of loading a remote module is described above. The idea is as follows: intercept the remote module request, download the remote module to the local, and point the import to the downloaded local module.

One problem with this process is that the local module is actually just a mirror. After the remote module is updated on the server, the local module is not updated. In addition, due to Vite’s caching policy, the resources of the same module will not be pulled back to the server if the contents remain unchanged. So in this case we have to restart the Vite service to update.

So how do you flush cached local modules?

In fact, it is quite simple to add a random query parameter to the IMPORT URL, such as the following

const componentList = [
  {id: 1.name: 'demo'.url: 'http://localhost:3000/demo.vue? update=1'},
  {id: 2.name: 'demo2'.url: 'http://localhost:3000/demo2.vue'},
  {id: 3.name: 'demo3'.url: 'http://localhost:3000/demo3.vue'},]Copy the code

When HRM listens for a file change, a hot update replaces the current module content, and if you re-import the demo1 component, you will see that the remote module has been reloaded

Once the vite service loading module is triggered, the remote module will be re-downloaded at resolveId and the subsequent update process will be completed.

packaging

All of the patterns implemented above are dependent on the Vite Server implementation, the Vite development pattern. If you can package your application at the end of the day, how do you do that?

All remote modules imported statically or asynchronously in the application should go through the resolveId and then be directed to the module file downloaded locally, so they can participate in the packaging normally

# static importimport Demo from '@remote/http://localhost:3000/demo.vue'

// Async import
import('@remote/http://localhost:3000/demo.vue')
Copy the code

Dynamic modules are not so lucky. In development mode we fooled Vite with configureServer, and in production we return 404 because the module we hacked does not exist.

In the low-code platform scenario of dynamic loading, the original design is to package the custom components according to the current configuration page, and then precompile them into a separate file, without the dynamic module scenario.

For example, there are 100 custom components, and the current configuration page A uses five of them. During the production of the page, the public page file and the five custom components are packaged by local modules. This avoids common problems such as blocking page loading with a large number of asynchronous components and bulky pages with all custom components involved in packaging. The packaging of low-code pages will be explained later in the introduction to developing a low-code page platform.

In addition to converting dynamic modules into asynchronous modules that are written to death in batches, businesses can also consider replacing dynamic module file paths through the mapping table, which is not further studied here, but can be tried again when there is time.

summary

At this point, we have achieved a static, asynchronous, dynamic load remote module vite plug-in, also solved the low code platform for the creation and maintenance of multiple custom components, the next is to use Vite, to develop a low code page development platform in my ideal.