12

Preface

I have encountered a special component usage scenario. In Project A, a basic component is used to pull the component JS CDN , and then hang it in the element. The principle is sorted out and written. Narrator: I heard that such remote components are used in low-code platforms.

Vue version remote component

Component form

In what form should the remote component exist?

  • file extension is .vue form of , but the .vue file browser is unable to recognize it and needs runtime conversion. We found http-vue-loader . First get the content Ajax Template、CSS、Script and output a JS object.
  • after build JS script exists . vue-loader provided by the official website will parse the file, extract each language block, and loader if necessary, and finally assemble them into a ES Module . Its default export is an object of the Vue

image.png

Construction method

Here we use webpack to button component:

  • Set the output configuration: library: 'MyComponent',libraryTarget: 'umd' , hang the generated component option object to the window.MyComponent variable
  • mini-css-extract-plugin plug-in cannot be configured because the plug-in will pull out the css
  • Style loader needs Finally add style-loader , css-loader handles JS pattern files introduced ( import styles from './index.module.scss' ) and other style file (it depends @import url('./index.module.scss') ), load them into JS code, generating an array of storage css code. And style-loader uses this array to dynamically generate style tags, css code in the style tags, and then insert it into HTML Head . Reference: on the role of css-loader and style-loader
// webpack.config.js
const path = require('path');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const { VueLoaderPlugin } = require('vue-loader');
const webpack = require('webpack');

module.exports = {
  mode: 'production', // 生产环境构建会启动压缩
  entry: {
    'button': './src/components/Button/index.vue',
  },
  output: {
    filename: 'js/[name].js',
    path: path.resolve(__dirname, './dist'),
    library: 'MyComponent',
    libraryTarget: 'umd'
  },
  resolve: {
    extensions: ['.js', '.vue'],
  },
  module: {
    rules: [
      { 
        test: /\.js$/, 
        exclude: /node_modules/, 
        loader: "babel-loader" 
      },
      {
        test: /\.vue$/,
        loader: 'vue-loader',
        options: {
          esModule: false
        }
      },
      {
        test: /\.(sa|sc|c)ss$/,
        use: [
          'style-loader',
          'css-loader',
          'sass-loader'
        ]
      },
      { 
        test: /\.(jpg|jpeg|png|gif)$/, 
        use: ['url-loader'] 
      }
    ]
  },
  plugins: [
    new webpack.ProgressPlugin(), // 打印构建进度
    new VueLoaderPlugin(),
    new CleanWebpackPlugin({
      cleanOnceBeforeBuildPatterns: ['dist']
    }),
  ],
}

refer to:

css-loader style-loader

Mounting method

We can use the dynamic component <component :is="xxx"></component> to achieve:

image.png

Finally realized

Relevant library version description in remote component project:

"dependencies": {
    "@babel/core": "^7.15.5",
    "@vue/compiler-sfc": "^3.2.11",
    "babel-loader": "^8.2.2",
    "clean-webpack-plugin": "^4.0.0",
    "css-loader": "^6.2.0",
    "sass": "^1.40.0",
    "sass-loader": "^12.1.0",
    "style-loader": "^3.2.1",
    "url-loader": "^4.1.1",
    "vue": "3.2.2",
    "vue-loader": "^16.5.0",
    "webpack": "^5.52.1",
    "webpack-cli": "^4.8.0"
}

Notice:

  • vue3 does not use vue-template-compiler , use @vue/compiler-sfc , and install vue-loader to specify a version above 16
  • At the beginning I used vue2 build the remote component project, but when used in the remote basic component, the console reported an error createElementBlock is not a function , because my remote basic component project used vue3 , and the remote component project used vue2 , so I needed to change vue2 upgrade to vue@3.2.2 (like upgrading to 3.0.0 not enough)

Reference: https://github.com/element-plus/element-plus/issues/2907

Remote Button component

After Button components, execute http-server --cors -p 8888 start the local static resource service and configure cross-domain so that the remote basic components can request Button.js source files

// Button.vue
<template>
  <span class="btn" @click="handleClick">{{$attrs.text}}</span>
</template>

<script>
export default {
  name: 'Button',
  data() {
    return {

    }
  },
  methods: {
    handleClick() {
      this.$emit('handleClick');
    }
  }
}
</script>

<style lang="css">
  .btn {
    font-size: 16px;
    color: #da2227;
  }
</style>

Note:

If style add the label scoped property, and finally the style may not take effect because there is no injection on the remote component elements scopeId , and style .btn privatization, of course, the option to acquire the object exists scopeId , you can manually add a remote component elements scopeId , this article did not do this, because scoped solves the problem of style privatization and introduces a new problem-the style is not easy (can be) modified, and in many cases, we need to fine-tune the style of public components.

image-20210916154801787.png

image-20210916155051822.png

Remote basic components

Here I am going to encapsulate a RemoteBaseComponent component to request and mount the button component (remote component). There are many ways to request, the details are as follows:

Dynamic script loading

First, you need to build the .vue file, and then use the dynamic Script to load the remote JS

// RemoteBaseComponent.vue
<template>
  <component :is="mode" v-bind="$attrs"></component>
</template>

<script>
import { markRaw } from 'vue';

export default {
  name: 'RemoteBaseComponent',
  props: {
    type: String,
  },
  data() {
    return {
      mode: ''
    }
  },
  inheritAttrs: false,
  mounted() {
    this.loadScript().then(() => {
      // 将组件的选项对象赋值给mode
      this.mode = markRaw(window.MyComponent.default);
    })
  },
  methods: {
    loadScript() {
      return new Promise((resolve, reject) => {
        const script = document.createElement('script');
        const target = document.getElementsByTagName('script')[0] || document.head;
        script.type = 'text/javascript';
        script.src = `http://127.0.0.1:8888/${this.type}.js`;
        script.onload = resolve;
        script.onerror = reject;
        target.parentNode.insertBefore(script, target)
      })
    }
  }
}
</script>
Partially registered components

Partially register a component with a script tag, listen for the onload event, and trigger a custom event when the loading is complete, assign the button component option object to the dynamic component

// RemoteBaseComponent.vue
<template>
  <div>
    <component :is="mode" v-bind="$attrs"></component>
    <remote-js :src="type"></remote-js>
  </div>
</template>

<script>
import { markRaw, h } from 'vue';
window.scriptLoadedevent = new CustomEvent('scriptLoaded'); // 自定义事件

export default {
  name: 'RemoteBaseComponent',
  props: {
    type: String,
  },
  data() {
    return {
      mode: ''
    }
  },
  inheritAttrs: false,
  components: {
    'remote-js': {
      render() {
        return h('script', { 
          type: 'text/javascript', 
          src: `http://127.0.0.1:8888/${this.src}.js`,
          onload: "document.dispatchEvent(scriptLoadedevent)"
        });
      },
      props: {
        src: { type: String, required: true },
      },
    },
  },
  created() {
    // created在onloaded之前执行,mounted不一定在onloaded之后执行,因此在created钩子中监听自定义事件
    document.addEventListener('scriptLoaded', () => {
      this.mode = markRaw(window.MyComponent.default);
    })
  },
}
</script>

Note :

If the console Uncaught TypeError: createElement is not a function error: 06185206a99eec, this is because in vue 2 , we perform the following operations to create a rendering function:

export default {
  render(createElement ) { // createElement  could be written h
    return createElement('div')
  }
}

In Vue 3 :

import { h } from 'vue'

export default {
  render() {
    return h('div')
  }
}

refer to:

Vue introduces remote JS files

using vue-chartjs in vue 3 : createElement is not a function

Ajax request

First, you need to build the .vue file, and then use ajax to load the remote JS . After obtaining the remote component source code, use new Function or eval execute the source code and assign the option object to the dynamic component

// RemoteBaseComponent.vue
<template>
  <component :is="mode" v-bind="$attrs"></component>
</template>

<script>
import { markRaw } from 'vue';

export default {
  name: 'RemoteBaseComponent',
  props: {
    type: String,
  },
  data() {
    return {
      mode: ''
    }
  },
  inheritAttrs: false,
  mounted() {
    this.loadScript();
  },
  methods: {
    loadScript() {
      fetch(`http://127.0.0.1:8888/${this.type}.js`).then((res) => {
        if (res.status === 200) {
          res.text().then((code) => {
            new Function(`${code}`)()
            // window.eval(code)
            this.mode = markRaw(window.MyComponent.default);
          })
        }
      })
    }
  }
}
</script>
SystemJS

SystemJS is a hookable, standards-based module loader. It provides a workflow in which the code written for the production workflow of the ES Rollup code split construction) can be converted to the System.register module format to work in old browsers that do not support native modules. Almost can run-local module speed, while supporting top-level waiting, dynamic import, circular reference and real-time binding, import.meta.url , module type, import mapping, integrity and content security policy, and compatible back to IE11 in old browsers.

systemjs in the project

<!DOCTYPE html>
<html lang="">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <link rel="icon" href="<%= BASE_URL %>favicon.ico">
    <title><%= htmlWebpackPlugin.options.title %></title>
  </head>
  <body>
    <script src='https://unpkg.com/systemjs@6.10.1/dist/system.js'></script>
    <script src='https://unpkg.com/systemjs@6.10.1/dist/extras/amd.js'></script>
    <script src='https://unpkg.com/systemjs@6.10.1/dist/extras/named-exports.js'></script>
    <script src='https://unpkg.com/systemjs@6.10.1/dist/extras/use-default.js'></script>
    <noscript>
      <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
    </noscript>
    <div id="app"></div>
    <!-- built files will be auto injected -->
  </body>
</html>

System.import in the remote basic component to get the remote component source code

<template>
  <component :is="mode" v-bind="$attrs"></component>
</template>

<script>
import { markRaw, h } from 'vue';

export default {
  name: 'RemoteBaseComponent',
  props: {
    type: String,
  },
  data() {
    return {
      mode: ''
    }
  },
  inheritAttrs: false,
  mounted() {
    this.loadScript();
  },
  methods: {
    loadScript() {
      window.System.import(`http://127.0.0.1:8888/${this.type}.js`).then((module) => {
        this.mode = module.default;
      })
    }
  }
}
</script>

Component communication

In Vue 2.x , we can pass attributes and listeners to the component this.$attrs and this.$listeners In combination with inheritAttrs: false , you can even bind these attributes and listeners to other elements instead of the root element:

In Vue 3.x virtual DOM , the event listener just to on prefixed properties. Therefore, the listener was summarized as $attrs , which removed $listerners .

Reference: Vue 3 migration strategy notes-Section 19: Remove $listeners

final effect

<template>
    <RemoteBaseComponent type="button" text="提交" v-on:handleClick="handleClick"/>
</template>

<script>
import RemoteBaseComponent from './components/RemoteBaseComponent.vue';

export default {
  name: 'App',
  components: {
    RemoteBaseComponent,
  },
  data() {
    return {
    }
  },
  methods: {
    handleClick() {
      console.log('click');
    }
  }
}
</script>

image-20210916155532028.png

React version of remote component core

Component form

Consider the following code, which implements a simple clock. Guess what is finally printed in the chrome console?

const Text = () =>  {
  console.log('Text');
  return <p>Just text.</p>;
};

const App = () => {
  const [clock, setClock] = React.useState(new Date().toISOString());
  console.log('App');

  React.useEffect(() => {
     const interval = setInterval(
       () => setClock(new Date().toISOString()),
       1000
     );
     return () => clearInterval(interval);
  }, []);
  
  return (
    <>
      <div>clock: {clock}</div>
      <Text />
    </>
  );
};

The fact is that every second, App and Text are printed together in the console.

React component is just a function. If you React used project TypeScript , you may have met a type: React.FC , it is FunctionComponent abbreviations are defined as follows:

type FC<P> = (props: P) => ReactElement

This means that a React function component represents a function, which accepts props as a parameter and finally returns a React element .

Construction method

Here we use webpack to button remote component of 06185206a9a50a:

  • Set the output configuration: library: 'MyComponent',libraryTarget: 'umd' , hang the generated component option object to the window.MyComponent variable
  • mini-css-extract-plugin plug-in cannot be configured because the plug-in will pull out the css
  • Style loader needs Finally add style-loader , css-loader handles JS style file (introduced import styles from './index.module.scss' ) and other style file (it depends @import url('./index.module.scss') ), load them into JS code, generating an array of storage css code. style-loader uses this array to dynamically generate the style tag, css code in the style tag, and then insert it into the HTML Head . Reference: Reference: on the role of css-loader and style-loader
  • Set css-loader in modules to false to avoid style modularization, because the external may need to fine-tune the style of public components. In order to prevent style conflicts, you can use BEM naming convention
// webpack.config.js
const path = require('path');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const webpack = require('webpack');

module.exports = {
  entry: {
    'button': './components/Button/index.jsx',
  },
  output: {
    filename: 'js/[name].js',
    path: path.resolve(__dirname, './dist'),
    library: 'MyComponent',
    libraryTarget: 'umd',
  },
  resolve: {
    extensions: ['.js', '.jsx', '.tsx'],
  },
  module: {
    rules: [
      { 
        test: /\.(js|jsx)$/, 
        exclude: /node_modules/, 
        loader: "babel-loader" 
      },
      {
        test: /\.(sa|sc|c)ss$/,
        use: [
          'style-loader',
          {
            loader: 'css-loader',
            options: {
              modules: false, // 禁止css modules
            }
          },
          'sass-loader'
        ]
      },
      { 
        test: /\.(jpg|jpeg|png|gif)$/, 
        use: ['url-loader'] 
      }
    ]
  },
  plugins: [
    new webpack.ProgressPlugin(),
    new CleanWebpackPlugin({
      cleanOnceBeforeBuildPatterns: ['dist']
    }),
  ],
}

Mounting method

JSX is essentially a kind of syntactic sugar, it will be compiled into some function calls:

const Something = () => {
  return (
    <p>
      foo
      <span>bar</span>
    </p>
  )
}
// 编译为
const Something = () => {
  return React.createElement('p', null,
    'foo',
    React.createElement('span', null, 'bar')
  )
}

In the React element, he will recursively create all the child elements, and finally generate an element tree.

So the rendering process of a component is actually a function call. This is why in the previous example we will get App and Text every second. The update of the component state caused the re-rendering of the component and triggered the function call.

jsx should be strings (for built-in components) or classes/functions (for composite components)

Finally realized

Remote Button component

After Button components, execute http-server --cors -p 8888 start the local static resource service and configure cross-domain so that remote basic components can request Button.js source files

// button.jsx
import React from 'react';

import './index.module.scss'; // modules设置为false,不能使用import styles from './index.module.scss';

const Button = (props) => {
  const { handleClick, text } = props;
  return <button className="btn" onClick={handleClick}>{text}</button>
}

export default Button;

Remote basic components

Here I am going to encapsulate a RemoteBaseComponent component to request and mount the button component (remote component). There are many ways to request, similar to vue . Here I use the ajax request to load as an example:

Function component form:

import React, { useCallback, useEffect, useState } from 'react';
import axios from 'axios';

const RemoteBaseComponent = (props: any) => {

  const { type } = props;

  const [Comp, setComponent] = useState<React.FC | null>(null);

  const importComponent = useCallback(() => {
    return axios.get(`http://127.0.0.1:8888/${type}.js`).then(res => res.data);
  }, [type])

  const loadComp = useCallback(async () => {
    // new Function(`${await importComponent()}`)();
    window.eval(`${await importComponent()}`)
    const { default: component } = (window as any).MyComponent;
    setComponent(() => component); // 这里需要注意,不能用setComponent(component),因为compoennt是一个函数,而setComponent接受两种形式的参数,一种是字面量,一种是函数,函数会被执行
  }, [importComponent, setComponent])

  useEffect(() => {
    loadComp();
  }, [loadComp]);

  if (Comp) {
    return <Comp {...props}/>
  }

  return null;
}

export default RemoteBaseComponent;

Class Component form:

import React, { useCallback, useEffect, useState } from 'react';
import axios from 'axios';

class RemoteBaseComponent extends React.Component<any, any> {
  constructor(props: any) {
    super(props);
    this.state = {
      component: null,
    };
  }

  importComponent(): any {
    return axios.get(`http://127.0.0.1:8888/${this.props.type}.js`).then(res => res.data);
  }

  async componentDidMount() {
    new Function(`${await this.importComponent()}`)();
    const { default: component } = (window as any).MyComponent;
    
    this.setState({
      component: component
    });
  }
  
  render() {
    const C = this.state.component;

    return C
      ? <C {...this.props} />
      : null;
  }
}

export default RemoteBaseComponent;

Project address: https://github.com/Revelation2019/vue-remote-component

refer to:

remote loading of sfc component ideas

Dynamic component gameplay you may not know

Dynamic loading of components in vue and react


记得要微笑
1.9k 声望4.5k 粉丝

知不足而奋进,望远山而前行,卯足劲,不减热爱。