一个远程加载JS模块的库。支持webpack4/5,可以通过使你的项目支持微前端架构。
它的初衷是:
-
希望像
html-webpack-plugin
插件生成HTML入口文件那样生成一个JS入口文件; -
插件的配置参数基本和
html-webpack-plugin
保持一致,以便只需将html-webpack-plugin
插件替换为import-remote/plugin
插件即可在html
和JS
入口间切换; -
解决JS模块依赖传递的问题:不再是通过
window
变量来传递依赖; -
支持自动从公共包中加载依赖;
-
利于
webpack
的懒加载功能实现公共包中的导出组件按需加载; -
提供
global.xxx
、window.xxx
这些全局变量私有化作用域的功能,避免污染window
; -
为
JS/CSS
动态添加publicPath
,方便一份打包成果多处部署; -
JS/CSS
资源异步获取,避免通过script
、link
加载资源导致的同步加载; -
支持多入口文件,实现多模块代码/作用域共享;
-
html-webpack-plugin
生成的是HTML入口;import-remote/plugin
生成的JS入口; -
HTML入口通过浏览器或
iframe
标签加载;JS入口通过import-remote
提供的加载方法(remote
)加载;
-
webpack library
的入口JS可以直接通过script
标签加载,支持资源跨域;import-remote
的入口JS需要专门提供的加载方法(remote
)加载,该方法通过ajax
请求加载资源,所以资源间的跨域问题需要使用者自行解决; -
webpack library
的模块依赖需要通过从window
变量获取;import-remote
的模块依赖只需在加载方法(remote
)中传递进去即可; -
webpack library
包必须配置publicPath
;import-remote
无需配置publicPath
或只需配置为/
,加载方法(remote
)在加载资源时会自动配置JS的`publicPath,并且查找CSS源代码中的相对路径,将其替换为绝对路径;
webpack Module Fedetation
需要应用和宿主都是webpack5
,import-remote
没有这个要求,webpack4/5
都可以,甚至宿主的打包环境使用的不是webpack
(gulp、vite等)也没问题;
参考html-webpack-plugin
生成的js入口里,实际上是该入口依赖的mainfest信息,包含入口JS/CSS文件、external等信息;
import-remote
的加载方法调用时:
- 加载入口JS文件;
- 根据入口JS里的信息,通过ajax请求下载入口JS/CSS资源;
- 通过内置的重写
webpack运行时
加载相关资源; - 根据
external
信息,替换调加载模块中的这些外部模块; - 调用入口模块,获取它的
modlule.exports
; - 返回获取到的
modlule.exports
;
npm install --save import-remote
或者:
yarn add import-remote
通过import-remote/plugin
插件可以为你的webpack
包生成一个JS入口文件。下面是一个webpack
配置示例:
webpack.config.js
const ImportRemotePlugin = require('import-remote/plugin')
const entryFiles = ['index', 'foo', 'bar'];
module.exports = {
// 配置入口,可以配置多个入口
entry: {
...(entryFiles.reduce((p, file) => {
p[file] = `${file}.js`;
return p;
}, {}))
}
// 配置输出目录
output: {
path: __dirname + '/dist',
filename: 'assets/[name]-[chunkhash:5].js',
chunkFilename: 'assets/[name]-[chunkhash:5]-chunk.js',
},
// 配置外部依赖
externals: [
'react',
'react-dom',
'lodash'
],
plugins: [
// 装载插件
...entryFiles.map(file => new ImportRemotePlugin({
template: `auto`,
// 生成的入口文件名
filename: `${file}.js`,
// 包含的入口chunk
chunks: [file],
}))
]
}
入口文件示例:
src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import _ from 'lodash',
const test = {
dosomething() {
console.log('dosomething', React, ReactDOM, _);
}
}
// 导出你要导出的内容
module.exports = test;
宿主端使用示例
import remote from 'import-remote';
async function run() {
// 通过await等待模块加载完毕
const testIndex = await remote('http://localhost:3000/test/index.js', {
// 配置模块需要依赖
externals: {
react: require('react'),
'react-dom': require('react-dom'),
'lodash': require('lodash'),
}
});
// 拿到模块输出对象后,即可做你想做的任何事了
testIndex.dosomething();
// 也可以将依赖配置成全局依赖,这样importRemote时就不必再传依赖了
Object.assign(remote.externals, {
react: require('react'),
'react-dom': require('react-dom'),
'lodash': require('lodash'),
});
const testOther = await remote('http://localhost:3000/test/other.js');
testOther.dosomething();
}
run();
import-remote
还提供了远程模块(RemoteModule
)类,方便管理同一组模块:
import { RemoteModule } from 'import-remote';
// 配置RemoteModule的host和依赖
const testModule = new RemoteModule('http://localhost:3000/test', {
externals: {
react: require('react'),
'react-dom': require('react-dom'),
'lodash': require('lodash'),
}
});
// 具体的加载路径为RemoteModule的`${host}${requireName}.js`
const testIndex = await testModule.require('index');
const testOther = await testModule.require('other');
testIndex.dosomething();
testOther.dosomething();
-
scopeName: string
- 作用域名,不配置时,会使用远程模块项目的项目名(package.json->name
); -
timeout:number
- 资源加载的请求时间,为0表示不会超时; -
externals: Record<string, any>
- 传递给远程模块的依赖模块(webpack配置的externals); -
getManifestCallback: (manifest: RemoteManifest) => any|Promise<any>
- 当加载到manifest
后的回调函数; -
afterCreateRuntime: (webpack_require: any, ctx: RemoteModuleRuntime) => void
- 模块的runtime创建好后的回调函数; -
host: string
- 远程模块的host
,相同host
的远程模块会认为来自同一个项目,它们会共享同一个runtime运行时。当不传时,host将从url中去取(去掉url中最后一个/
前的部分会认为是host
); -
sync: boolean
- 使用使用ajax
的同步请求来加载资源; -
cacheDB: boolean
- 是否开启cacheDB
,当开启后,每次加载成功的远程资源会保存在一个名叫import-remote-global-db
的indexedDB
数据库中,当遇到某个资源ajax请求失败,xhr.status
为0
时,将会改为尝试从该数据库加载该资源。该数据每次启动时会清楚一个月之前的缓存,以避免缓存资源膨胀。注
:你也可以通过import-remote/cache
导出的enableCacheDB
方法来为所有模块开启cacheDB
;
import { enableCacheDB } from 'import-remote/cache';
enableCacheDB();
useEsModuleDefault: boolean
- 是否在加载到远程模块后直接返回入口模块的default
导出,而不是返回整个该入口模块;
import-remote/plugin
的基本配置选项基本和html-webpack-plugin
一致,这样目的是方便两个插件间切换。
属性名 | 类型 | 默认值 | 说明 |
---|---|---|---|
filename |
{String} |
'index.js' |
生成的入口文件名。默认为index.js . 你也可以在这里指的子目录 (比如: entrys/index.js ) |
hash |
{Boolean} |
false |
如果值为true,就添加一个唯一的webpack compilation hash给所有JS和CSS文件。这对缓存清除(cache busting)十分有用 |
cache |
{Boolean} |
true |
如果为true (默认),只要文件被更改了就生成文件 |
showErrors |
{Boolean} |
true |
如果为true (默认),详细的错误信息将被写入到JS文件中 |
chunks |
{?} |
? |
允许你只添加某些chunks(e.g only the unit-test chunk) |
chunksSortMode |
{String|Function} |
auto |
在chunks被include到html文件中以前,允许你控制chunks 应当如何被排序。允许的值: none |
excludeChunks |
{Array.<string>} |
`` | 允许你跳过某些chunks (e.g don't add the unit-test chunk) |
entriesManifest |
`{Boolean | String}` | '' |
libraryFileName |
`{Boolean | String}` | '' |
要生成多个输出入口文件,只需要在你的插件列表中配置多个ImportRemotePlugin
插件:
{
entry: 'index.js',
output: {
path: __dirname + '/dist',
filename: 'index_bundle.js'
},
plugins: [
new ImportRemotePlugin(), // 生成默认的index.js
new ImportRemotePlugin({ // 同时生成一个test.js
filename: 'test.js',
})
]
}
要只打包特定的chunk包,你可以通过chunks
属性来控制:
plugins: [
new ImportRemotePlugin({
chunks: ['app']
})
]
你也可以配置要排除特定的包,通过excludeChunks
选项来控制:
plugins: [
new ImportRemotePlugin({
excludeChunks: [ 'dev-helper' ]
})
]
要支持长期缓存,你可以通过为filename
添加contenthash/templatehash
:
plugins: [
new ImportRemotePlugin({
filename: 'index.[contenthash].js'
})
]
contenthash/templatehash
是输出文件内容的hash。
另外, 你也可以这样配置hash: [<hashType>:contenthash:<digestType>:<length>]
hashType
-sha1
、md5
、sha256
、sha512
之一,或者任意一种node.js支持的hash类型digestType
-hex
、base26
、base32
、base36
、base49
、base52
、base58
、base62
、base64
之一maxlength
- 生成的hash字符的最大程度
默认为: [md5:contenthash:hex:9999]
除了从html-webpack-plugin
继承的配置选项,import-remote/plugin
也提供了一些自己的配置选项:
属性名 | 类型 | 默认值 | 说明 |
---|---|---|---|
commonModules |
[{ name?: string, url: string, scoped?: boolean }] |
[] |
依赖的公共模块,如果配置了公共模块,则模块的依赖将尝试从这些公共模块中加载依赖。其中name 是可选参数,为某个外部依赖模块名,如果配置了name ,表示该公共包只是该名称的模块,则只有该模块依赖该名称的外部依赖模块时才会加载该模块包。该功能依赖于webpack的externals |
shareModules |
[string or { name: string, var: string }] |
[] |
共享模块列表,如果配置了共享模块,则模块的这些依赖将优先使用宿主应用externals参数中传递的依赖,如果未在宿主应用中找到,才使用自己的该模块。该功能不依赖webpack的externals。 |
globalToScopes |
string[] |
[] |
需要局部化的全局变量,该数组中声明的名称,在加载资源时将会从源代码中将其替换为从某个私有变量中读取这些值 |
提供公共模块包的目的是方便公共模块的共享,同时利用webpack
的懒加载功能实现组件的按需加载。
这是一个公共模块包的示例:
// index.js
import { createRequireFactory } from 'import-remote';
export default createRequireFactory({
'import-remote': () => require('import-remote'),
react: () => import(/* webpackChunkName: 'react' */ 'react'),
'react-dom': () => import(/* webpackChunkName: 'react-dom' */ 'react-dom'),
'prop-types': () => import(/* webpackChunkName: 'prop-types' */ 'prop-types'),
mobx: () => import(/* webpackChunkName: 'mobx' */ 'mobx'),
'mobx-react': () => import(/* webpackChunkName: 'mobx-react' */ 'mobx-react'),
qs: () => import(/* webpackChunkName: 'qs' */ 'qs'),
});
webpack.config.js
const ImportRemotePlugin = require('import-remote/plugin')
module.exports = {
entry: {
index: 'index.js'
}
output: {
filename: '[name]-[chunkhash:5].js'
},
externals: [
{
'import-remote': {
root: 'importRemote',
amd: 'import-remote',
commonjs2: 'import-remote',
commonjs: 'import-remote',
},
},
],
plugins: [
new ImportRemotePlugin()
]
}
这是一个公共lib模块包的示例:
// index.js
import axios from 'axios';
export default axios;
webpack.config.js
const ImportRemotePlugin = require('import-remote/plugin')
module.exports = {
entry: {
index: 'index.js'
}
output: {
filename: '[name]-[chunkhash:5].js'
},
plugins: [
new ImportRemotePlugin()
]
}
一个依赖公共包的webpack配置如下:
webpack.config.js
const ImportRemotePlugin = require('import-remote/plugin')
module.exports = {
entry: {
index: 'index.js'
}
output: {
filename: '[name]-[chunkhash:5].js'
},
externals: {
react: {
root: 'React',
amd: 'react',
commonjs2: 'react',
commonjs: 'react',
},
'react-dom': {
root: 'ReactDOM',
amd: 'react-dom',
commonjs2: 'react-dom',
commonjs: 'react-dom',
},
mobx: {
root: 'mobx',
commonjs: 'mobx',
commonjs2: 'mobx',
},
'mobx-react': {
root: 'mobxReact',
commonjs: 'mobx-react',
commonjs2: 'mobx-react',
},
},
plugins: [
new ImportRemotePlugin({
commonModules: [
// 当指定了name时,只有该name在externals中存在时,才会加载该公共包
{ name: 'axios', url: 'http://localhost:3000/lib/axios.min.js' },
// 未指定name事,只要externals不为空,就会加载该公共包
{ url: 'http://localhost:3000/lib/commonLibs.js' },
]
})
]
}
则这个模块的依赖:
-
首先从宿主使用加载方法
remote
中配置的externals
中寻找依赖; -
第一步没找到依赖,将会从模块中寻找依赖;
通过共享模块,使多个远程应用间使用共同的依赖。
这是一个共享模块包的配置示例:
webpack.config.js
const ImportRemotePlugin = require('import-remote/plugin')
module.exports = {
entry: {
index: 'index.js'
}
output: {
filename: '[name]-[chunkhash:5].js'
},
plugins: [
new ImportRemotePlugin({
shareModules: [
{ name: 'react', var: 'react' },
{ name: 'react-dom', var: 'ReactDOM' }
]
})
]
}
宿主传递共享模块的方式:
import remote from 'import-remote';
// 通过await等待模块加载完毕
const testIndex = await remote('http://localhost:3000/test/index.js', {
// 共享模块也可以通过externals来传递
externals: {
react: require('react'),
'react-dom': require('react-dom'),
}
});
通过globalToScopes
选项可以将你的源代码中一些window.xxx
、global.xxx
的变量替换为一个同组模块共享的私有变量中。这在相互集成时全局变量冲突时的一种解决办法;
如果你的webpack配置是这样的:
const ImportRemotePlugin = require('import-remote/plugin')
module.exports = {
entry: {
index: 'index.js'
}
output: {
filename: '[name]-[chunkhash:5].js'
},
plugins: [
new ImportRemotePlugin({
globalToScopes: ['app', 'someVar']
})
]
}
则宿主在加载你的模块时,将会把你源代码中的global.app
、window.app
、global.someVar
、window.someVar
替换成_wp_.g.app
、_wp_.g.someVar
。
注:全局变量私有化可能有风险,需要您仔细斟酌是否使用。毕竟最好的方案就是没有全局变量。该选项只是用于解决您目前无法或没时间去掉全局变量时的一种托底解决办法。
函数签名:
/**
* @param {string} url 远程模块的url地址
* @param {object} options 加载选项
* @param {object} options.externals 远程模块的依赖模块
* @param {boolean} options.useEsModuleDefault 当远程模块导出的是ES模块时,是否只返回它的default导出部分
* @returns {Promise<any>} 远程模块的导出内容
**/
function remote(url: string, options: { externals: { [key]: any } }): Promise<any>;
使用方法:
import remote from 'import-remote';
// 通过await等待模块加载完毕
const testIndex = await remote('http://localhost:3000/test/index.js', {
// 配置模块需要依赖
externals: {
react: require('react'),
'react-dom': require('react-dom'),
'lodash': require('lodash'),
}
});
// 拿到模块输出对象后,即可做你想做的任何事了
testIndex.dosomething();
通过远程模块可以用于管理一个项目生成的所有模块:
import { RemoteModule } from 'import-remote';
// 配置RemoteModule的host和依赖
const testModule = new RemoteModule('http://localhost:3000/test', {
externals: {
react: require('react'),
'react-dom': require('react-dom'),
'lodash': require('lodash'),
}
});
// 具体的加载路径为RemoteModule的`${host}${requireName}.js`
const testIndex = await testModule.require('index');
const testOther = await testModule.require('other');
testIndex.dosomething();
testOther.dosomething();
RemoteModule
的的方法有:
{
/**
* @param {string} host 该项目生成的模块的基地址(host)
* @param {object} options 加载选项
* @param {object} options.externals 远程模块的依赖模块
* @param {boolean} options.useEsModuleDefault 当远程模块导出的是ES模块时,是否只返回它的default导出部分
* @returns {void}
**/
constructor(host: string, options: { externals: { [key]: any } }): void;
/**
* 根据模块名生成该模块的入口地址
* @param {string} moduleName 模块名
* @returns {string} 该模块的入口地址
**/
resolveModuleUrl(moduleName: string = 'index'): string;
/**
* 发送HEAD请求判断`moduleName`是否存在,当存在时返回请求的响应头信息
* @param {string} moduleName 模块名
* @param {object} options 加载选项
* @returns {Promise<object|null>} HEAD请求头信息或为空
**/
exist(moduleName: string = 'index', options: {}): Promise<any>;
/**
* 获取`moduleName`的meta信息
* @param {string} entryName 入口模块名
* @param {object} options 加载选项,和remote方法的option一致
* @returns {Promise<any>} 所有入口的描述文件
**/
requireEntries(entryName: string = 'index', options: {}): Promise<any>;
/**
* 获取`moduleName`的meta信息
* @param {string} moduleName 模块名
* @param {object} options 加载选项,和remote方法的option一致
* @returns {Promise<any>} 模块的meta信息
**/
requireMeta(moduleName: string = 'index', options: {}): Promise<any>;
/**
* 功能和requireMeta相同,只是将加载资源时的ajax设置为同步请求
* @param {string} moduleName 模块名
* @param {object} options 加载选项,和remote方法的option一致
* @returns {any} 远程模块的导出内容
**/
requireMetaSync(moduleName: string = 'index', options: {}): any;
/**
* 异步加载方法
* @param {string} moduleName 模块名,调用
* @param {object} options 加载选项,和remote方法的option一致
* @returns {Promise<any>} 远程模块的导出内容
**/
require(moduleName: string = 'index', options: {}): Promise<any>;
/**
* 功能和require相同,只是将加载资源时的ajax设置为同步请求
* @param {string} moduleName 模块名,调用
* @param {object} options 加载选项,和remote方法的option一致
* @returns {any} 远程模块的导出内容
**/
requireSync(moduleName: string = 'index', options: {}): any;
/**
* 异步加载方法,功能和require相同,只是在导出模块为es模块时,只导出它的default部分
* @param {string} moduleName 模块名,调用
* @param {object} options 加载选项,和remote方法的option一致
* @returns {Promise<any>} 远程模块的导出内容
**/
import(moduleName: string = 'index', options: {}): Promise<any>;
/**
* 功能和import相同,只是将加载资源时的ajax设置为同步请求
* @param {string} moduleName 模块名,调用
* @param {object} options 加载选项,和remote方法的option一致
* @returns {any} 远程模块的导出内容
**/
importSync(moduleName: string = 'index', options: {}): any;
}
RemoteView
是一个React
组件,模拟iframe
,内部创建3个元素模拟html
、head
、body
,通过src
属性加载远程React组件
, 通过该组件加载的资源会有自己独立的全局空间,移除该组件时,也将移除加载该远程模块时的所有资源。
示例:
import React from 'react';
import RemoteView from 'import-remote/view';
function Test(props) {
return <div>
<RemoteView
src="http://localhost:3000/test.js"
props={{
aa: 1,
bb: 2
}}
externals={{
'react': require('react'),
'react-dom': require('react-dom')
}}
/>
</div>;
}
或者使用RemoteModule
来加载:
import React from 'react';
import { RemoteModule } from 'import-remote';
import RemoteView from 'import-remote/view';
const testModule = new RemoteModule('http://localhost:3000/test', {
externals: {
react: require('react'),
'react-dom': require('react-dom'),
'lodash': require('lodash'),
}
});
function Test(props) {
return <div>
<RemoteView
module={testModule}
moduleName="index"
props={{
aa: 1,
bb: 2
}}
/>
</div>;
}
RemoteView
除了支持远程模块导出的React
组件,也支持以下格式的组件导出接口:
{
namespace?: string,
bootstrap: (props, children) => void,
mounted: (el, props) => primise|void,
update: (el, props, prevProps) => void,
unmount: (el) => void,
}
和
{
namespace?: string,
init: (props, options) => primise|void,
render: (el, props) => void,
destroy: (el) => void,
}
注:如果namespace
不为空,这将会将其添加到body
模拟元素的className
中。
如果想将这样的对象转换成React
组件,可以通过下面的办法:
- 直接将一个导出对象转换成
React
组件:
import { createAppView } from 'import-remote/view';
import testObject from './test-object';
const Test = createAppView(testObject);
- 远程加载一个导出对象,并直接将其转换成
React
组件:
import { requireApp } from 'import-remote/view';
const Test = await requireApp('http://localhost:3000/test.js');
-
scopeStyle: boolean
- 当为true,并且shadow
为false时,将为引用的样式添加一个hash
作用域,避免样式影响到外部。是一种在不支持shadow DOM
的情况下解决样式冲突的一种解决办法 -
scopePrefix: string = 'v-'
- 为引用样式创建的作用域名的前缀 -
classPrefix: string = 'import-remote-'
- 创建的html
、head
、body
模拟元素的类名前缀 -
tag: string = 'div'
- 创建的html
、head
、body
模拟元素的元素类型 -
src: string
- 远程模块的入口文件地址 -
module: RemoteModule
- 远程模块对象,和moduleName
配合使用,与src
属性互斥 -
moduleName: string
- 远程模块名称,和module
配合使用,与src
属性互斥 -
props: object
- 传递给远程模块导出组件的props
-
externals: object
- 远程组件的外部依赖 -
shadow: boolean
- 是否将html
、head
、body
创建在shadow DOM
中 -
onViewLoading: (loading) => void
- 远程模块加载中的回调事件,分别会在加载前后调用 -
onViewError: (error) => void
- 远程模块加载失败的回调事件 -
hoc: Function
- 对导出组件的高阶封装,参数格式:(Component: React.ComponentType | React.ForwardRefExoticComponent, error?: any) => React.ComponentType | React.ForwardRefExoticComponent
RemoteApp
是一个即插即用的React组件,当远程模块返回的结果本身就是React组件时,可以使用该组件加载对应的模块。它和RemoteView
的区别是:RemoteView
会局部化远程模块的作用域空间,而RemoteApp
不会:
import React from 'react';
import { RemoteModule } from 'import-remote';
import { RemoteApp } from 'import-remote/view';
// or
import { RemoteApp } from 'import-remote/view/app';
const testModule = new RemoteModule('http://localhost:3000/test', {
externals: {
react: require('react'),
'react-dom': require('react-dom'),
'lodash': require('lodash'),
}
});
function Test(props) {
return <div>
<RemoteApp
module={testModule}
moduleName="index"
aa="1"
bb="2"
/>
</div>;
}
-
src: string
- 远程模块的入口文件地址 -
module: RemoteModule
- 远程模块对象,和moduleName
配合使用,与src
属性互斥 -
moduleName: string
- 远程模块名称,和module
配合使用,与src
属性互斥 -
exportName: string
- 当远程模块的结果为一个esModule
时,取esModule
的导出key
名,默认为default
-
hoc: Function
- 对导出组件的高阶封装,参数格式:(Component: React.ComponentType | React.ForwardRefExoticComponent, error?: any) => React.ComponentType | React.ForwardRefExoticComponent
-
clearWhenError: boolean
- 当加载远程组件失败时是否清除当前显示的组件,即上次加载的组件。默认为true
。