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 contentAjax
Template、CSS、Script
and output aJS
object. - after build
JS
script exists .vue-loader
provided by the official website will parse the file, extract each language block, andloader
if necessary, and finally assemble them into aES Module
. Its default export is an object of theVue
Construction method
Here we use webpack
to button
component:
- Set the output configuration:
library: 'MyComponent',libraryTarget: 'umd'
, hang the generated component option object to thewindow.MyComponent
variable mini-css-extract-plugin
plug-in cannot be configured because the plug-in will pull out thecss
- Style loader needs Finally add
style-loader
,css-loader
handlesJS
pattern files introduced (import styles from './index.module.scss'
) and other style file (it depends@import url('./index.module.scss')
), load them intoJS
code, generating an array of storagecss
code. Andstyle-loader
uses this array to dynamically generatestyle
tags,css
code in thestyle
tags, and then insert it intoHTML 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:
Mounting method
We can use the dynamic component <component :is="xxx"></component>
to achieve:
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 usevue-template-compiler
, use@vue/compiler-sfc
, and installvue-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 errorcreateElementBlock is not a function
, because my remote basic component project usedvue3
, and the remote component project usedvue2
, so I needed to changevue2
upgrade tovue@3.2.2
(like upgrading to3.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.
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>
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 thewindow.MyComponent
variable mini-css-extract-plugin
plug-in cannot be configured because the plug-in will pull out thecss
- Style loader needs Finally add
style-loader
,css-loader
handlesJS
style file (introducedimport styles from './index.module.scss'
) and other style file (it depends@import url('./index.module.scss')
), load them intoJS
code, generating an array of storagecss
code.style-loader
uses this array to dynamically generate thestyle
tag,css
code in thestyle
tag, and then insert it into theHTML Head
. Reference: Reference: on the role of css-loader and style-loader - Set
css-loader
inmodules
tofalse
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 useBEM
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
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。