({
containerRef: null,
microApp: null,
});
const loadApp = () => {
if (current.containerRef) {
current.microApp = loadMicroApp({
...props.microAppConfig,
container: current.containerRef,
}, {
sandbox: {
experimentalStyleIsolation: true,
},
});
}
};
useEffect(() => {
loadApp();
return () => {
current.microApp?.unmount();
};
}, []);
useEffect(() => {
if (current.microApp && current.microApp.update) {
current.microApp.update({});
}
}, []);
useImperativeHandle(microAppRef, () => {
return {
appStore: current,
};
});
return (
{
current.containerRef = ref;
}} />
);
});
================================================
FILE: packages/core/src/common/constants.ts
================================================
export const JS_PREFIX = 'static/js';
export const CSS_PREFIX = 'static/css';
export const IMG_PREFIX = 'static/images';
export const FONT_PREFIX = 'static/fonts';
export const FILE_PREFIX = 'static/files';
================================================
FILE: packages/core/src/common/paths.ts
================================================
import * as path from 'path';
export const sourcePath = path.resolve(process.cwd(), 'src');
export const publicPath = path.resolve(process.cwd(), 'public');
export const appConfigPath = path.resolve(process.cwd(), 'app-config.ts');
================================================
FILE: packages/core/src/index.ts
================================================
export { MicroAppConfig } from './node/micro-fe-app-config';
export { MicroAppWebpackConfigOptions } from './node/get-micro-app-webpack-config';
export { webpackBuild, webpackPromisify, webpackServe } from './node/webpack-command';
export { getMicroAppConfigManager } from './node/micro-fe-app-config';
export { generateMfExposeDeclaration } from './node/generate-mf-expose-declaration';
export { bundleTsDeclaration } from './node/bundle-ts-declaration';
export { generateDtsBundle } from 'dts-bundle-generator';
export { bundleModuleDeclare } from './node/bundle-module-declare';
export { emitMfExposeDeclaration } from './node/emit-mf-expose-declaration';
export { EmitMfExposeWebpackPlugin } from './node/emit-mf-expose-webpack-plugin';
export { getMicroAppWebpackConfig } from './node/get-micro-app-webpack-config';
export { getModuleFederationExposes } from './node/get-module-federation-exposes';
================================================
FILE: packages/core/src/node/add-entry-attribute-webpack-plugin.ts
================================================
import webpack from 'webpack';
import { Hooks } from 'html-webpack-plugin';
/**
* 向 html-webpack-plugin 导出的 HTML 模板 script 添加属性
*
* @author yuzhanglong
* @date 2021-10-10 02:31:52
*/
export class AddEntryAttributeWebpackPlugin {
private readonly entryMatchCallback;
constructor(matchCallback: (src: string) => boolean) {
this.entryMatchCallback = matchCallback;
}
apply(compiler: webpack.Compiler) {
compiler.hooks.compilation.tap('AddEntryAttributeWebpackPlugin', (compilation) => {
// 通过最终的 webpack 配置的 plugins 属性,根据插件的 constructor.name 拿到 html-webpack-plugin 实例
const HtmlWebpackPluginInstance: any = compiler.options.plugins
.map(({ constructor }) => constructor)
.find(constructor => constructor && constructor.name === 'HtmlWebpackPlugin');
if (HtmlWebpackPluginInstance) {
// 获取 html-webpack-plugin 所有的 hooks
const hooks = HtmlWebpackPluginInstance.getHooks(compilation) as Hooks;
// 在插入标签之前做些什么
hooks.alterAssetTagGroups.tap(
'AddEntryAttributeWebpackPlugin', (data) => {
// 拿到所有的标签,如果是 script 标签,并且满足我们的匹配函数,则将其 attributes['entry'] 设为 true
data.headTags.forEach(tag => {
if (tag.tagName === 'script' && this.entryMatchCallback(tag.attributes?.src)) {
// eslint-disable-next-line no-param-reassign
tag.attributes.entry = true;
}
});
return data;
},
);
}
});
}
}
================================================
FILE: packages/core/src/node/bundle-module-declare.ts
================================================
import { ModuleDeclarationKind, Project, SyntaxKind } from 'ts-morph';
export interface FileOptions {
// 声明文件路径
path: string;
// 声明文件模块名称
moduleName: string;
}
/**
* 打包类型定义文件
*
* @author yuzhanglong
* @date 2021-10-03 19:28:19
* @param fileOptions 文件相关选项,可参考上面的类型定义
*/
export const bundleModuleDeclare = (fileOptions: FileOptions[]) => {
const project = new Project();
const content = [];
fileOptions.forEach(file => {
// 添加源代码
const source = project.addSourceFileAtPath(file.path);
// 遍历每一个子节点,如果是 SyntaxKind.DeclareKeyword(即 declare 关键词),进行文本替换
source.forEachDescendant(item => {
if (item.getKind() === SyntaxKind.DeclareKeyword) {
// 删除即可, 需要判断是不是第一个节点,否则会报异常
item.replaceWithText(item.isFirstNodeOnLine() ? 'export' : '');
}
});
// 备份根节点
const baseStatements = source.getStructure().statements;
// 移除现存的所有节点
source.getStatements().forEach(res => res.remove());
// 创建一个 module declaration,将上面备份的根节点插入之
source.addModule({
name: `'${file.moduleName}'`,
declarationKind: ModuleDeclarationKind.Module,
hasDeclareKeyword: true,
statements: baseStatements,
});
// 格式化代码
source.formatText();
// 补充一些注释
content.push(`// module name: ${file.moduleName}\n\n`);
content.push(source.getText());
content.push('\n');
});
return content.join('');
};
================================================
FILE: packages/core/src/node/bundle-ts-declaration.ts
================================================
import * as os from 'os';
import * as path from 'path';
import { runCommand } from '@attachments/utils/lib/node/run-command';
export interface BundleFileConfig {
// 入口文件路径
entryPath: string;
// 输出路径
outputPath: string;
}
/**
* 封装 dts-bundle-generator
*
* @author yuzhanglong
* @date 2021-10-01 20:09:23
* @param entries 一个数组,详见 BundleFileConfig
* @see BundleFileConfig
*/
export const bundleTsDeclaration = async (
entries: BundleFileConfig[],
) => {
// 最大并行工作数目为 cpu 核心数 - 1
const maxWorkSize = os.cpus().length - 1;
while (entries.length > 0) {
const runningItems = entries.splice(0, maxWorkSize);
await Promise.all(runningItems.map((item) => {
const { entryPath, outputPath } = item;
return runCommand(
path.resolve(
require.resolve('dts-bundle-generator'),
'../bin/dts-bundle-generator.js',
), [
entryPath,
'--out-file',
outputPath,
'--project',
path.resolve(process.cwd(), 'tsconfig.json'),
'--no-banner',
]);
}));
}
};
================================================
FILE: packages/core/src/node/emit-mf-expose-declaration.ts
================================================
import * as path from 'path';
import * as fs from 'fs-extra';
import { MicroAppConfig } from './micro-fe-app-config';
import { BundleFileConfig, bundleTsDeclaration } from './bundle-ts-declaration';
import { bundleModuleDeclare } from './bundle-module-declare';
/**
* 供公共组件的提供者使用,用来将相应的类型定义写入某个文件目录下
* 我们可以在 webpack 写入文件之后 hook 到相应的生命周期中追加内容
*
* @author yuzhanglong
* @date 2021-10-02 14:51:10
* @param appConfig app 配置
* @param baseUrl 写入的根路径
*/
export const emitMfExposeDeclaration = async (appConfig: MicroAppConfig, baseUrl: string) => {
// 增加临时缓存文件,用来打包每个小 bundle
await fs.ensureDir(path.resolve(baseUrl, '.cache'));
const entries: (BundleFileConfig & { name: string })[] = [];
for (const expose of appConfig.exposes) {
// 只处理 module 类型
if (typeof expose === 'object' && expose.type !== 'package') {
entries.push({
name: expose.name,
entryPath: expose.path,
outputPath: path.resolve(baseUrl, '.cache', `${expose.name}.d.ts`),
});
}
}
// 并行打包
await bundleTsDeclaration(entries.slice());
// 合并上面的所有小 bundle
const content = bundleModuleDeclare(
entries.map(res => {
return {
path: res.outputPath,
moduleName: `${appConfig.name}/${res.name}`,
};
}),
);
await fs.writeFile(path.resolve(baseUrl, 'exposes.d.ts'), content);
await fs.remove(path.resolve(baseUrl, '.cache'));
};
================================================
FILE: packages/core/src/node/emit-mf-expose-webpack-plugin.ts
================================================
import webpack from 'webpack';
import * as path from 'path';
import { MicroAppConfig } from './micro-fe-app-config';
import { emitMfExposeDeclaration } from './emit-mf-expose-declaration';
interface EmitMfExposeWebpackPluginOptions {
// app 配置
appConfig: MicroAppConfig;
// 输出内容的基础路径,如果没有指定则为 compilation.compiler.outputPath
// 由于 serve 模式 build 模式输出位置不同,这个选项是有必要的,降低开发成本
outputBasePath?: string;
}
/**
* 防抖函数
*
* @author yuzhanglong
* @date 2021-12-01 00:51:19
*/
export const debounce = any>(fn: T, timeout: number = 0) => {
if (typeof fn !== 'function') {
throw new Error('fn should be a function');
}
let timer = null;
return function(...args: Parameters) {
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(() => {
fn.call(this, ...args);
}, timeout);
};
};
/**
* 向 build 打包产物注入类型定义的 webpack-plugin
*
* @author yuzhanglong
* @date 2021-10-03 19:31:02
*/
export class EmitMfExposeWebpackPlugin {
private readonly config: EmitMfExposeWebpackPluginOptions;
constructor(config: EmitMfExposeWebpackPluginOptions) {
this.config = config;
}
apply(compiler: webpack.Compiler) {
const { appConfig, outputBasePath } = this.config;
const handler = debounce(async (compilation: webpack.Compilation) => {
try {
// 用 try catch 包裹一下防止 webpack-dev-server 热更新过程中偶发的强制 exit 现象
if (appConfig) {
// 拿到本项目的 outputPath
const { outputPath } = compilation.compiler;
// 生成相关目录
const target = path.resolve(outputBasePath ?? outputPath, 'mf-expose-types');
console.log('[mf-lite] compiling shared remote module declarations...');
// 基于用户的配置 appConfig 生成类型定义
await emitMfExposeDeclaration(appConfig, target);
}
} catch (e) {
console.log(e);
}
}, 1500);
// afterEmit 生命周期的时机:输出 asset 到 output 目录之后
// 实践证明,它不会阻塞 webpack dev-server 的流程,不会影响开发体验。
compiler.hooks.afterEmit.tap('EmitMfExposeWebpackPlugin', handler);
// TODO: 使用文件 hash 进行缓存,避免相同的内容重复打包,可以参考下面的的注释 DEMO
// compiler.hooks.thisCompilation.tap('EmitMfExposeWebpackPlugin', (compilation) => {
// compilation.hooks.processAssets.tapAsync({
// name: 'EmitMfExposeWebpackPlugin',
// stage: webpack.Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE_INLINE,
// }, (compilationAssets, callback) => {
// // 首先收集所有的文件,进行 content hash 值比对
// // @ts-ignore
// const data = [...compilation.modules].filter(res => res.request && res.request.includes('shared-utils.ts'))[0];
// console.log(data.buildInfo);
// return callback();
// });
// });
}
}
================================================
FILE: packages/core/src/node/generate-mf-expose-declaration.ts
================================================
/**
* 生成 module federation expose 声明,一般用于消费者调用
*
* @author yuzhanglong
* @date 2021-10-02 14:53:24
*/
import axios from 'axios';
import * as url from 'url';
import * as fs from 'fs-extra';
import * as path from 'path';
import { MicroAppConfig } from './micro-fe-app-config';
import { sourcePath } from '../common/paths';
export const generateMfExposeDeclaration = async (appConfig: MicroAppConfig) => {
const declareTypeRoot = path.resolve(sourcePath, 'types', 'mf-remotes');
await fs.ensureDir(declareTypeRoot);
for (const { name, url: remoteUrl } of appConfig.remotes) {
const targetFileName = `${name}-exposes.d.ts`;
// example: https://base-40kkvlqeq-yzl.vercel.app/mf-expose-types/exposes.d.ts
const remote = url.resolve(remoteUrl, 'mf-expose-types/exposes.d.ts');
console.log(`fetching remotes types declarations from ${remote}...`);
const declarations = await axios.get(remote);
await fs.writeFile(path.resolve(declareTypeRoot, targetFileName), declarations.data);
}
console.log('Done!');
};
================================================
FILE: packages/core/src/node/get-micro-app-webpack-config.ts
================================================
import webpack, { NormalModuleReplacementPlugin } from 'webpack';
import HtmlWebpackPlugin from 'html-webpack-plugin';
import ReactRefreshWebpackPlugin from '@pmmmwh/react-refresh-webpack-plugin';
import MiniCssExtractPlugin from 'mini-css-extract-plugin';
import moment from 'moment';
import * as path from 'path';
import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer';
import merge from 'webpack-merge';
import { publicPath as assetPublicPath, sourcePath } from '../common/paths';
import { CSS_PREFIX, FILE_PREFIX, JS_PREFIX } from '../common/constants';
import { getMicroAppConfigManager, MicroAppConfig } from './micro-fe-app-config';
import { getModuleFederationExposes } from './get-module-federation-exposes';
import { EmitMfExposeWebpackPlugin } from './emit-mf-expose-webpack-plugin';
import { AddEntryAttributeWebpackPlugin } from './add-entry-attribute-webpack-plugin';
const TerserWebpackPlugin = require('terser-webpack-plugin');
const { ModuleFederationPlugin } = require('webpack').container;
export interface MicroAppWebpackConfigOptions {
// 项目类型,base: 基座, micro-app 微应用
type: 'base-app' | 'micro-app';
// 项目运行的端口
port: number;
// 微应用配置
appConfig: MicroAppConfig;
// 是否在构建环境
isBuildMode: boolean;
// 开启分析模式
isAnalyzeMode?: boolean;
}
/**
* 基座或者微应用的 webpack 配置封装
*
* @author yuzhanglong
* @date 2021-10-03 19:32:18
* @param options 相关配置选项,可参考上面的类型定义
* @see MicroAppWebpackConfigOptions
*/
export const getMicroAppWebpackConfig = (options: MicroAppWebpackConfigOptions) => {
const { port, appConfig, isBuildMode, isAnalyzeMode } = options;
const websocketPath = `ws://localhost:${port.toString()}/ws`;
// 是否为生产环境
const isProductionEnvironment = process.env.NODE_ENV === 'production';
// 微应用工具管理类
const microAppConfigManager = getMicroAppConfigManager(appConfig);
// 样式默认配置
const baseStyleConfigRules = [
isProductionEnvironment ? MiniCssExtractPlugin.loader : require.resolve('style-loader'),
require.resolve('css-loader'),
{
loader: require.resolve('postcss-loader'),
options: {
postcssOptions: {
plugins: [
require('autoprefixer'),
],
},
},
},
].filter(Boolean);
// noinspection UnnecessaryLocalVariableJS
const config = {
mode: isProductionEnvironment ? 'production' : 'development',
// 打包入口,默认为 index.tsx
entry: path.resolve(sourcePath, 'index.tsx'),
// 在生产环境下默认不会打包 sourcemap (但有时候可能还是有必要的,比如接入监控平台)
devtool: isProductionEnvironment ? false : 'source-map',
// 开启打包文件缓存,第二次打开可以节约大量的时间
// 在 build 模式下不要打开,否则会报错
cache: isBuildMode ? false : {
type: 'filesystem',
},
// 代码优化配置
optimization: {
// 这里保证了基座热更新的实现
runtimeChunk: 'single',
minimize: isProductionEnvironment,
minimizer: [
new TerserWebpackPlugin({
terserOptions: {
format: {
comments: false,
},
},
extractComments: false,
}),
],
// 一些常见依赖的代码分割
splitChunks: {
chunks: 'all',
cacheGroups: {
thirdVendors: {
name: 'initial-third-vendors',
test: /moment|lodash|mobx|qiankun/,
priority: 20,
enforce: true,
},
reactVendors: {
name: 'initial-react-vendors',
test: /react\/|react-dom\/|react-router\/|react-router-dom\/|axios/,
priority: 20,
enforce: true,
},
uiComponents: {
name: 'initial-ui-components-vendors',
test: /antd/,
priority: 20,
enforce: true,
},
uiIcons: {
name: 'initial-ui-icons-vendors',
test: /@ant-design\/icons/,
priority: 20,
enforce: true,
},
uiOthers: {
name: 'initial-material-ui-others-vendors',
test: /@ant-design\/*/,
priority: 10,
enforce: true,
},
},
},
},
// webpack dev-server
// @ts-ignore
devServer: {
client: {
webSocketURL: websocketPath,
},
static: {
directory: assetPublicPath,
watch: {
ignored: (f: string) => {
// 生成的类型定义不要监听,否则会引发全局的 reload 使 HMR 失去意义
return f.endsWith('.d.ts');
},
},
},
allowedHosts: 'all',
hot: true,
port: port,
historyApiFallback: true,
headers: {
'Access-Control-Allow-Origin': '*',
},
},
// 输出
output: {
// library 名称
library: `${microAppConfigManager.config.name}`,
// 输出的文件名称
// 如果你要修改此内容,请看一下下面调用 html-webpack-plugin 代码的相关注释
filename: isProductionEnvironment
? `${JS_PREFIX}/[name].[contenthash:8].bundle.js`
: `${JS_PREFIX}/[name].bundle.js`,
// 输出文件名称,和 fileName 不同,这里的输出文件为非初始(non-initial)文件,例如我们熟悉的路由懒加载
chunkFilename: isProductionEnvironment
? `${JS_PREFIX}/[name].[contenthash:8].chunk.js`
: `${JS_PREFIX}/[name].chunk.js`,
// asset/resource 模块以 [hash][ext][query] 文件名发送到输出目录
assetModuleFilename: `${FILE_PREFIX}/[name].[hash][ext]`,
// 公共路径
publicPath: microAppConfigManager.config.url,
// 输出 umd 类型的 bundle
libraryTarget: 'umd',
},
plugins: [
// NormalModuleReplacementPlugin 需要我们传入一个回调
// 我们可以在这里将默认的公共的 package 级别依赖重定向到 remote(即共享模块)
// 这个回调已被封装成公共方法,它会从你的 app-config 目录下读取 remote 字段,从中找到匹配的 sharedLibraries
new NormalModuleReplacementPlugin(
/(.*)/,
microAppConfigManager.getNormalModuleReplacementPluginCallBack(),
),
// 输出 html 入口文件
new HtmlWebpackPlugin({
template: path.resolve(assetPublicPath, 'index.html'),
}),
// qiankun 底层依赖的 import-html-entry 会取所有 scripts 里面排在最后的 script 作为 entry。
// 具体代码可查看:https://github.com/kuitos/import-html-entry/blob/master/src/index.js#L321
// 但是我们通过 html-webpack-plugin 导出的 HTML,一般情况下是 main 在最后,但是在 webpack module federation 中,会生成一个额外的 entry 排在 main 的后面。
// 从而导致拿不到 main 入口的生命周期函数, 我们可以向 script 标签加入 entry 属性解决这个问题
new AddEntryAttributeWebpackPlugin((src => {
return !!(src.match(/main\.(.*)\.bundle.js$/) || src.match('main.bundle.js'));
})),
// webpack module federation 的插件,其配置基于 app-config 封装,一般无需改动
new ModuleFederationPlugin({
name: microAppConfigManager.config.name,
filename: 'module-federation-entry.js',
remotes: microAppConfigManager.getModuleFederationRemotes(),
exposes: getModuleFederationExposes(microAppConfigManager.config.exposes),
}),
// 非生产环境下启动 react 热更新插件
// 注意,如果将代码打包到测试服务器上(非生产环境),那么也应该开启这个插件以向主应用插入相关胶水代码
// 由于子应用的 react 是由主应用 share 的,子应用如果需要热更新必须依赖这些胶水代码
// 另外这需要和 src/utils 目录下的 init-common 配合使用,开发者无需额外处理
!isProductionEnvironment && new ReactRefreshWebpackPlugin({
overlay: {
sockPath: websocketPath,
},
}),
// css 压缩
isProductionEnvironment && new MiniCssExtractPlugin({
filename: `${CSS_PREFIX}/${isProductionEnvironment ? '[name].[contenthash].css' : '[name].css'}`,
chunkFilename: `${CSS_PREFIX}/${isProductionEnvironment ? '[id].[contenthash].css' : '[id].css'}`,
ignoreOrder: true,
}),
// 定义一些全局的变量,例如版本、打包时间、环境信息
new webpack.DefinePlugin({
// eslint-disable-next-line import/no-dynamic-require
__APP_VERSION__: JSON.stringify(require(path.resolve(process.cwd(), 'package.json')).version),
__MODE__: JSON.stringify(process.env.NODE_ENV?.toUpperCase()),
__BUILD_TIME__: JSON.stringify(moment().format('MMMM Do YYYY, h:mm:ss A')),
}),
// Moment.js 默认情况下它会打包所有的 locale 文件
// 我们需要用户选择导入特定的区域设置
new webpack.IgnorePlugin({
resourceRegExp: /^\.\/locale$/,
contextRegExp: /moment$/,
}),
// 写入共享模块(非 package)的类型定义
new EmitMfExposeWebpackPlugin({
appConfig: microAppConfigManager.config,
outputBasePath: isBuildMode ? undefined : assetPublicPath,
}),
isAnalyzeMode && new BundleAnalyzerPlugin(),
].filter(Boolean),
// 解析配置
resolve: {
// 扩展名省略
extensions: ['.ts', '.tsx', '.js', '.jsx'],
// 实用 alias
alias: {
'~src': sourcePath,
},
},
// 模块解析
module: {
rules: [
{
oneOf: [
// babel 相关
{
test: [/\.[jt]sx?$/],
include: [sourcePath],
use: {
loader: require.resolve('babel-loader'),
options: {
presets: [
[
require.resolve('@babel/preset-env'),
],
[
require.resolve('@babel/preset-react'),
{
runtime: 'automatic',
},
],
[
require.resolve('@babel/preset-typescript'),
],
],
plugins: [
[
require.resolve('@babel/plugin-proposal-decorators'),
{
legacy: true,
},
],
[require.resolve('@babel/plugin-transform-runtime')],
].filter(Boolean),
},
},
},
// css 预处理相关 css 和 less
{
test: [/\.(le|c)ss$/],
use: [
...baseStyleConfigRules,
{
loader: require.resolve('less-loader'),
options: {
lessOptions: {
javascriptEnabled: true,
},
},
},
],
},
// css 预处理相关 sass
{
test: [/\.s[ac]ss$/],
use: [
...baseStyleConfigRules,
{
loader: require.resolve('sass-loader'),
},
],
},
// 其它内容全部以 asset/resource 输出
{
exclude: [/^$/, /\.(js|mjs|jsx|ts|tsx)$/, /\.html$/, /\.json$/],
type: 'asset/resource',
},
],
},
],
},
};
// 合并用户自定义的 webpack 配置
const additionalConfig = microAppConfigManager.config.webpackConfig;
if (typeof additionalConfig === 'function') {
// 用户可以二次修改
additionalConfig(config as webpack.Configuration);
return config;
}
// @ts-ignore
return merge(config, additionalConfig || {});
};
================================================
FILE: packages/core/src/node/get-module-federation-exposes.ts
================================================
/**
* File: get-mf-exposes.ts
* Description: 根据配置的目录(src/externals 下), 生成 webpack module federation 配置
* Created: 2021-08-29 13:39:40
* Author: yuzhanglong
* Email: yuzl1123@163.com
*/
import { CachedInputFileSystem, ResolverFactory } from 'enhanced-resolve';
import * as fs from 'fs';
type MfExposesModule = string | {
name: string;
path: string;
}
const myResolver = ResolverFactory.createResolver({
fileSystem: new CachedInputFileSystem(fs, 4000),
conditionNames: ['node'],
extensions: ['.js', '.json', '.node'],
useSyncFileSystemCalls: true,
mainFields: ['esm', 'module', 'main'],
});
export function getModuleFederationExposes(modules: MfExposesModule[]) {
const exposes: Record = {};
for (const module of modules) {
if (typeof module === 'string') {
const key = `./${module}`;
const resolveResult = myResolver.resolveSync({}, process.cwd(), module);
if (typeof resolveResult !== 'string') {
throw new Error(`resolve error: ${module}`);
}
exposes[key] = resolveResult;
} else if (typeof module === 'object') {
exposes[`./${module.name}`] = module.path;
}
}
return exposes;
}
================================================
FILE: packages/core/src/node/micro-fe-app-config.ts
================================================
import webpack from 'webpack';
type SharedLibraryExpose = string | {
name: string;
path: string;
type?: 'package' | 'module'
};
type SharedLibrary = string | {
name: string;
type?: 'package' | 'module'
};
export interface MicroAppConfig {
name: string;
url: string;
remotes: {
name: string;
url: string;
sharedLibraries?: SharedLibrary[]
}[];
exposes: SharedLibraryExpose[];
webpackConfig?: Partial | ((config: webpack.Configuration) => void);
}
/**
* 基于 micro app config 生成目录
*
* @author yuzhanglong
* @date 2021-09-28 00:40:39
* @return object 一个对象, key 表示远程应用的名称,value 表示其入口 url
*/
export const getModuleFederationRemotes = (microAppConfig: MicroAppConfig) => {
const remotes: Record = {};
// example: 'base_app': `base_app@https://base-yzl.vercel.app/base_app_entry.js`,
for (const remote of microAppConfig.remotes) {
remotes[remote.name] = `${remote.name}@${remote.url.endsWith('/') ? remote.url : `${remote.url}/`}module-federation-entry.js`;
}
return remotes;
};
/**
* 获取 share library 的导入替换回调函数
*
* 基座暴露一些公共库,我们称为 share library,我们通过 NormalModuleReplacementPlugin 将所有的公共依赖导向相应的 app
*
* @author yuzhanglong
* @date 2021-09-28 00:47:33
*/
export const getNormalModuleReplacementPluginCallBack = (microAppConfig: MicroAppConfig) => {
return (v: { request: string }) => {
// 寻找相应的 request,例如我们要重定向 react,那么我们要从所有的 remotes 中找到第一个其 shareLibrary 中有 react 的远程模块
const externalRemoteApp = microAppConfig.remotes
.find(res => {
if (!res.sharedLibraries) {
return false;
}
return res.sharedLibraries
.some(i => {
// 如果直接是 string 类型表示是一个 package
if (typeof i === 'string') {
return i === v.request;
}
return i.type === 'package' && i.name === v.request;
});
});
if (externalRemoteApp) {
// eslint-disable-next-line no-param-reassign
v.request = `${externalRemoteApp.name}/${v.request}`;
}
};
};
/**
* 初始化全局 manager 方便调用
*
* @author yuzhanglong
* @date 2021-09-28 00:53:32
*/
export const getMicroAppConfigManager = (microAppConfig: MicroAppConfig) => {
return {
getNormalModuleReplacementPluginCallBack: () => getNormalModuleReplacementPluginCallBack(microAppConfig),
getModuleFederationRemotes: () => getModuleFederationRemotes(microAppConfig),
config: microAppConfig,
};
};
================================================
FILE: packages/core/src/node/webpack-command.ts
================================================
import webpack from 'webpack';
import WebpackDevServer from 'webpack-dev-server';
export const webpackPromisify = (config: Record) => {
return new Promise((resolve, reject) => {
webpack(config, (err: any, state) => {
if (err) {
console.error(err.stack || err);
if (err.details) {
console.error(err.details);
}
reject();
}
const info = state.toJson();
if (state.hasErrors()) {
console.error(info.errors);
reject();
}
if (state.hasWarnings()) {
console.warn(info.warnings);
reject();
}
resolve(true);
});
});
};
export const webpackBuild = async (config: Record) => {
await webpackPromisify(config);
};
export const webpackServe = async (config: Record) => {
const compiler = webpack(config);
const devServerOptions = {
...config.devServer,
};
// @ts-ignore
const server = new WebpackDevServer(devServerOptions, compiler);
server.startCallback(() => {
console.log('webpack dev server is running...');
});
};
================================================
FILE: packages/core/tsconfig.json
================================================
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"baseUrl": "./",
"outDir": "lib",
"rootDir": "./src",
"jsx": "react"
},
"include": [
"src/**/*"
]
}
================================================
FILE: packages/proxy/README.md
================================================
This project has moved [here](https://github.com/yuzhanglong/attachments/tree/main/packages/proxy)
================================================
FILE: pnpm-workspace.yaml
================================================
packages:
- 'packages/*'
================================================
FILE: tsconfig.json
================================================
{
"compilerOptions": {
"target": "ESNext",
"module": "CommonJS",
"declaration": true,
"moduleResolution": "Node",
"resolveJsonModule": true,
"types": [
"jest",
"node"
],
"emitDecoratorMetadata": true,
"sourceMap": true,
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"isolatedModules": false,
"experimentalDecorators": true,
"jsx": "preserve",
"downlevelIteration": true
}
}