主从独立开发
在一个项目中或者一个通用开源项目中,可能会需要将基础项目或者主项目和各个模块功能分开开发,或者以插件的形式开发。这时候就会需要将主项目和从项目或者叫子项目、插件分开开发。

这里采用的是以 URL 远程加载的方式实现主从分开加载,子项目不需要携带主项目已经携带的框架代码,如 React 等。
动态加载远程组件
import {createElement, ReactNode, useEffect, useState} from "react";
export const RemoteComponentLoader = <T extends Record<string, unknown>>(
{
url,
name,
props,
withCss = false,
children = 'Loading...',
errorMessage = 'Load Remote Component Error!'
}: {
url: string
name?: string
props?: T
withCss?: boolean
children?: ReactNode
errorMessage?: ReactNode
}
) => {
const [loading, setLoading] = useState(true)
const scriptFileName = url.replace(/.*?([^\/.]+)[.\w]+$/, '$1');
useEffect(() => {
setLoading(true)
const scriptElement = document.createElement('script');
scriptElement.src = url;
scriptElement.onload = () => {
setLoading(false)
scriptElement.remove()
}
scriptElement.onerror = () => {
setLoading(false)
scriptElement.remove()
}
document.body.appendChild(scriptElement);
}, [url]);
useEffect(() => {
if (withCss) {
const linkElement = document.createElement('link');
linkElement.href = url.replace(/[^/]+$/, `${scriptFileName}.css`);
linkElement.rel = 'stylesheet';
document.head.appendChild(linkElement);
return () => {
linkElement.remove()
}
}
}, [withCss]);
if (loading) {
return children
}
if (window[(name || scriptFileName) as never]) {
return createElement((window[(name || scriptFileName) as never] as any), props)
}
return errorMessage
}
Code language: TypeScript (typescript)
注意事项
父项目需要将提供的包挂载到全局对象中,全局对象一般为 window
:
// 在入口文件中挂载,如 src/main.tsx
import React from 'react';
window.WegarPackageReact = React;
Code language: TypeScript (typescript)
开发从项目(子项目)
// vite.config.ts
export default defineConfig({
define: {
'process.env': process.env
},
build: {
lib: {
entry: './src/App.tsx',
name: 'WegarRemoteSubComponentTest',
formats: ['umd'],
fileName: 'WegarRemoteSubComponentTest',
// ...
},
rollupOptions: {
external: ['react'],
output: {
globals: {
react: 'WegarPackageReact'
},
// ...
},
// ...
},
// ...
},
// ...
})
Code language: TypeScript (typescript)
开发过程中可以使用 src/main.ts
、进行预览和模拟主项目环境,整个从项目以入口组件为桥梁和父项目进行通信交互。
混合 Vue 或者其他框架开发的模块
本篇内容是以 React 为主项目开发基础的,所以如果想需要融合其他框架的时候也需要以 React 组件为基础入口。
// vite.config.ts
export default defineConfig({
plugins: [
// ...
vue(),
vueDevTools(),
],
define: {
'process.env': process.env,
// ...
},
build: {
lib: {
entry: './src/App.tsx',
name: 'WegarRemoteSubComponentTest',
formats: ['umd'],
fileName: 'WegarRemoteSubComponentTest',
// ...
},
rollupOptions: {
external: ['react'],
output: {
globals: {
react: 'WegarPackageReact',
// ...
},
// ...
},
// ...
},
// ...
},
// ...
})
Code language: TypeScript (typescript)
// src/App.tsx
import {ComponentPublicInstance, createApp, ref} from "vue";
function App(props: PropsType) {
const [reactState] = useState(ref({}))
const vueRoot = useRef<HTMLDivElement>(null)
const [vueAppInstance, setVueAppInstance] = useState<ComponentPublicInstance>()
useEffect(() => {
const app = createApp(VueApp)
app.provide('reactState', reactState)
setVueAppInstance(app.mount(vueRoot.current!!,))
}, []);
useEffect(() => {
// 状态更新时刷新 Vue 项目
reactState.value = {
// ...
}
vueAppInstance?.$forceUpdate()
}, [props, vueAppInstance]);
return <div ref={vueRoot}></div>
}
export default App
Code language: TypeScript (typescript)
<!-- src/VueApp.vue -->
<script setup lang="ts" ref="main">
import {inject, onBeforeUpdate, Ref} from "vue";
let reactState = ((inject('reactState') as Ref<{ count: number, setCount: any }>).value)
onBeforeUpdate(() => {
// 获取父项目最新状态
reactState = ((inject('reactState') as Ref<{ count: number, setCount: any }>).value)
// ...
})
</script>
<template>
<div>Hello Vue</div>
</template>
<style scoped>
/** ... */
</style>
Code language: TypeScript (typescript)
背景
如果需要开发一个管理后台,目前很多框架都是不管项目如何,后台整个前端项目的代码都是在一起的。如果要开发某个功能不管功能大小都要拉取整个项目代码开发调试运行之后再整个打包发布,要不就是使用 iframe
。这样的话前者相对太庞大和复杂,后者虽然兼容性更好但太过于自由。
为什么
上文提供的方案可以实现同一大项目分割开发,而从项目之间只需要统一相关的通信机制和做好全局的状态管理。这样在主项目基础架构相对有雏形可用的时候就可以实现从项目并行开发,很少需要考虑从项目之间的代码互相影响。