Qiankun+Vite样式隔离解决方案
问题
Qiankun
+Vite
作为子应用时,样式隔离无效, 即使配置了experimentalStyleIsolation: true
也无效Ant Design
组件库样式隔离无效
解决方案
使用 Postcss-Prefix-Selector 插件为样式添加前缀
Ant Design ConfigProvider 配置
prefixCls
样式前缀
配置插件样式隔离
在子应用
vite.config.ts
中配置postcss-prefix-selector
插件(官方示例)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32import prefixer from 'postcss-prefix-selector';
export default defineConfig({
css: {
postcss: {
plugins: [
prefixer({
prefix: '[data-qiankun-app]', // 这里的值要和 main.tsx 中的属性名保持一致
transform(prefix, selector, prefixedSelector, filePath, rule) {
if (selector.match(/^(html|body)/)) {
return selector.replace(/^([^\s]*)/, `$1 ${prefix}`);
}
if (filePath.match(/node_modules/)) {
return selector; // Do not prefix styles imported from node_modules
}
const annotation = rule.prev();
if (
annotation?.type === 'comment' &&
annotation.text.trim() === 'no-prefix'
) {
return selector; // Do not prefix style rules that are preceded by: /* no-prefix */
}
return prefixedSelector;
},
}),
],
},
},
});在子应用
main.tsx
中为根节点添加data-qiankun-app
属性1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36import { createRoot } from 'react-dom/client';
import {
QiankunProps,
qiankunWindow,
renderWithQiankun,
} from 'vite-plugin-qiankun/dist/helper';
import App from './App.tsx';
import './index.css';
const render = (container?: HTMLElement) => {
const app =
container || (document.getElementById('root') as HTMLDivElement);
/**
* 添加属性,用于样式隔离
* 注意:这里的属性名要和 postcss-prefix-selector 插件中的 prefix 保持一致
*/
app.setAttribute('data-qiankun-app', 'true');
createRoot(app).render(<App />);
};
/** Qiankun 生命周期钩子 */
const qiankun = () => {
renderWithQiankun({
bootstrap() {},
async mount(props: QiankunProps) {
render(props.container);
},
update: (props: QiankunProps) => {},
unmount: (props: QiankunProps) => {},
});
};
if (qiankunWindow.__POWERED_BY_QIANKUN__) qiankun();
else render();下面是一个示例,可以在控制台中看到样式被添加了前缀,实现了样式隔离
1
2
3
4
5
6
7
8import React from 'react';
import './App.css';
const App = () => {
return <div className="app">Hello, Qiankun!</div>;
};
export default App;1
2
3
4
5
6
7
8.app {
color: red;
}
// 在控制台中被转换成
[data-qiankun-app] .app {
color: red;
}实现跟 qiankun experimentalStyleIsolation 类似的效果
配置组件库样式隔离
上面样式对自己写的类名生效,但是对于引入的组件库样式隔离可能无效
1
2
3
4
5
6import { ConfigProvider } from 'antd';
import App from './App';
<ConfigProvider prefixCls="app">
<App />
</ConfigProvider>;1
2
3.app-btn {
color: red;
}
项目地址
项目实践(可选)
包版本
node
:18.20.7
npm
:10.8.2
lerna
:8.1.8
vite
:6.1.0
qiankun
:2.10.16
项目目录
1 | ├── 根目录 |
搭建 monorepo
项目 (可选)
通过
npm install lerna -g
全局安装lerna
命令行执行
npx lerna init
初始化项目配置项目
1
2
3
4
5{
"npmClient": "npm", // 使用 npm
"packages": ["packages/*"], // 指定包目录
... // 其他配置
}1
2
3
4{
"workspaces": ["packages/*"],
... // 其他配置
}
创建主/子应用
依次执行下面命令
1
2
3
4
5
6
7
8
9
10
11
12
13
14# 从根目录进入 packages 目录
cd ./packages
# 创建主应用 app
npm create vite@latest
# Project name
app
# Select a framework
React
# Select a variant
TypeScript + SWC在
packages/app/vite.config.ts
中配置启动端口
1
2
3
4
5
6export default defineConfig({
plugins: [react()],
server: {
port: 8000,
},
});
依次执行下面命令
1
2
3
4
5
6
7
8
9
10
11
12
13
14# 从根目录进入 packages 目录
cd ./packages
# 创建子应用 app1
npm create vite@latest
# Project name
app1
# Select a framework
React
# Select a variant
TypeScript + SWC在
packages/app1/vite.config.ts
中配置启动端口
1
2
3
4
5
6export default defineConfig({
plugins: [react()],
server: {
port: 8001,
},
});
依次执行下面命令
1
2
3
4
5
6
7
8
9
10
11
12
13
14# 从根目录进入 packages 目录
cd ./packages
# 创建子应用 app2
npm create vite@latest
# Project name
app2
# Select a framework
React
# Select a variant
TypeScript + SWC在
packages/app2/vite.config.ts
中配置启动端口
1
2
3
4
5
6export default defineConfig({
plugins: [react()],
server: {
port: 8002,
},
});
启动项目
在根目录下执行 npm install
安装依赖
方式一:进入各个应用目录, 分别执行
npm run dev
1
2
3
4
5
6
7
8
9
10
11
12
13
14# 进入 packages/app 目录
cd ./packages/app
# 启动主应用
npm run dev
# 进入 packages/app1 目录
cd ./packages/app1
# 启动子应用
npm run dev
# 进入 packages/app2 目录
cd ./packages/app2
# 启动子应用
npm run dev方式二:进入根目录, 执行
npm run dev -w=xxx
,-w
就是--workspace
,xxx
对应各个应用package.json
的name
, 详见:npm workspace1
2
3
4
5
6
7
8# 根目录下启动主应用
npm run dev -w=app
# 根目录下启动子应用
npm run dev -w=app1
# 根目录下启动子应用
npm run dev -w=app2
安装依赖项
全局安装 antd、react-router-dom
1
2# 在根目录下执行
npm install antd react-router-dom -w=app -w=app1 -w=app2主应用
app
安装 qiankun1
2# 在根目录下执行
npm install qiankun -w=app子应用
app1
、app2
安装 vite-plugin-qiankun1
2# 在根目录下执行
npm install vite-plugin-qiankun -D -w=app1 -w=app2子应用
app2
安装 postcss-prefix-selector1
2# 在根目录下执行
npm install postcss-prefix-selector -D -w=app2
链接主/子应用
在
packages/app/src/main.tsx
中注册并加载子应用1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44import { createRoot } from 'react-dom/client';
import './index.css';
import App from './App.tsx';
import { registerMicroApps, start } from 'qiankun';
/** 注册子应用 */
registerMicroApps(
[
{
name: 'app1', // 子应用名称(全局唯一)
entry: 'http://localhost:8001', // 这里的端口号要和子应用的端口号一致
container: '#subapp1', // 子应用挂载点
activeRule: '/app1', // 这里的路径要和子应用的路由路径一致
},
{
name: 'app2', // 子应用名称(全局唯一)
entry: 'http://localhost:8002', // 这里的端口号要和子应用的端口号一致
container: '#subapp2', // 子应用挂载点
activeRule: '/app2', // 这里的路径要和子应用的路由路径一致
},
],
{
beforeLoad: async (app) => {
console.log(`%c before load: ${app.name}`, 'color: green');
},
beforeMount: async (app) => {
console.log(`%c before mount: ${app.name}`, 'color: green');
},
afterMount: async (app) => {
console.log(`%c after mount: ${app.name}`, 'color: yellow');
},
beforeUnmount: async (app) => {
console.log(`%c before unmount: ${app.name}`, 'color: red');
},
afterUnmount: async (app) => {
console.log(`%c after unmount: ${app.name}`, 'color: red');
},
}
);
/** 启动子应用 */
start();
createRoot(document.getElementById('root')!).render(<App />);在
packages/app/src/App.tsx
中配置路由, 并设置挂载子应用dom
节点1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61import {
createBrowserRouter,
Outlet,
RouterProvider,
useNavigate,
} from 'react-router-dom';
const BaseLayout = () => {
const navigate = useNavigate();
const handleClick = (name: string) => {
navigate(`/${name}`);
};
return (
<div className="home">
<h1>主应用</h1>
<div className="btn-group">
<button onClick={() => handleClick('')}>主应用 app</button>
<button onClick={() => handleClick('app1')}>子应用 app1</button>
<button onClick={() => handleClick('app2')}>子应用 app2</button>
</div>
<div className="children">
<Outlet />
</div>
</div>
);
};
/** 创建路由 */
const routes = createBrowserRouter(
[
{
path: '/',
element: <BaseLayout />,
children: [
{
path: '/',
element: <h2>app</h2>,
},
{
path: 'app1/*', // 通配符 * 表示匹配所有子路由
element: <div id="subapp1"></div>, // 子应用挂载点 对应 main.tsx 注册子应用的 container
},
{
path: 'app2/*', // 通配符 * 表示匹配所有子路由
element: <div id="subapp2"></div>, // 子应用挂载点 对应 main.tsx 注册子应用的 container
},
],
},
],
{ basename: '/' }
);
function App() {
return <RouterProvider router={routes} />;
}
export default App;在
packages/app/src/index.css
中配置样式1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18.home {
display: flex;
align-items: center;
flex-direction: column;
}
.btn-group {
width: 300px;
display: flex;
justify-content: space-around;
}
.children {
width: 100%;
height: 400px;
margin-top: 40px;
border: 2px solid #000;
}
在
packages/app1/vite.config.ts
中配置跨域
与vite-plugin-qiankun
插件1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react-swc';
import qiankun from 'vite-plugin-qiankun';
// https://vite.dev/config/
export default ({ mode }) => {
const useDevMode = mode === 'development';
const host = '127.0.0.1';
const port = 8001;
const subAppName = 'app1';
const base = useDevMode
? `http://${host}:${port}/${subAppName}`
: `/${subAppName}`; // 这里 subAppName 对应 createBrowserRouter 的 basename
return defineConfig({
base,
server: {
port,
cors: true, // 作为子应用时,如果不配置,则会引起跨域问题
origin: `http://${host}:${port}`, // 必须配置,否则无法访问静态资源
},
plugins: [
// 在开发模式下需要把react()关掉
// https://github.com/tengmaoqing/vite-plugin-qiankun?tab=readme-ov-file#3dev%E4%B8%8B%E4%BD%9C%E4%B8%BA%E5%AD%90%E5%BA%94%E7%94%A8%E8%B0%83%E8%AF%95
...[useDevMode ? [] : [react()]],
qiankun(subAppName, { useDevMode }),
],
});
};在
packages/app1/src/main.tsx
中设置是否以子应用的方式启动1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33import { createRoot } from 'react-dom/client';
import {
QiankunProps,
qiankunWindow,
renderWithQiankun,
} from 'vite-plugin-qiankun/dist/helper';
import App from './App.tsx';
import './index.css';
/** 渲染函数 */
const render = (container?: HTMLElement) => {
const app =
container || (document.getElementById('root') as HTMLDivElement);
createRoot(app).render(<App />);
};
/** Qiankun 生命周期钩子 */
const qiankun = () => {
renderWithQiankun({
bootstrap() {},
async mount(props: QiankunProps) {
render(props.container);
},
update: () => {},
unmount: () => {},
});
};
// 检查是否在 Qiankun 环境中
console.log('qiankunWindow', qiankunWindow.__POWERED_BY_QIANKUN__);
if (qiankunWindow.__POWERED_BY_QIANKUN__) qiankun(); // 以子应用的方式启动
else render();在
packages/app1/src/App.tsx
中配置路由1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18import { createBrowserRouter, RouterProvider } from 'react-router-dom';
/** 创建路由 */
const routes = createBrowserRouter(
[
{
path: '/',
element: <h2>app1</h2>,
},
],
{ basename: '/app1' } // 设置路由前缀
);
function App() {
return <RouterProvider router={routes} />;
}
export default App;
在
packages/app2/vite.config.ts
中配置跨域
与vite-plugin-qiankun
插件1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react-swc';
import qiankun from 'vite-plugin-qiankun';
// https://vite.dev/config/
export default ({ mode }) => {
const useDevMode = mode === 'development';
const host = '127.0.0.1';
const port = 8002;
const subAppName = 'app2';
const base = useDevMode
? `http://${host}:${port}/${subAppName}`
: `/${subAppName}`; // 这里 subAppName 对应 createBrowserRouter 的 basename
return defineConfig({
base,
server: {
port,
cors: true, // 作为子应用时,如果不配置,则会引起跨域问题
origin: `http://${host}:${port}`, // 必须配置,否则无法访问静态资源
},
plugins: [
// 在开发模式下需要把react()关掉
// https://github.com/tengmaoqing/vite-plugin-qiankun?tab=readme-ov-file#3dev%E4%B8%8B%E4%BD%9C%E4%B8%BA%E5%AD%90%E5%BA%94%E7%94%A8%E8%B0%83%E8%AF%95
...[useDevMode ? [] : [react()]],
qiankun(subAppName, { useDevMode }),
],
});
};在
packages/app2/src/main.tsx
中设置是否以子应用的方式启动1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33import { createRoot } from 'react-dom/client';
import {
QiankunProps,
qiankunWindow,
renderWithQiankun,
} from 'vite-plugin-qiankun/dist/helper';
import App from './App.tsx';
import './index.css';
/** 渲染函数 */
const render = (container?: HTMLElement) => {
const app =
container || (document.getElementById('root') as HTMLDivElement);
createRoot(app).render(<App />);
};
/** Qiankun 生命周期钩子 */
const qiankun = () => {
renderWithQiankun({
bootstrap() {},
async mount(props: QiankunProps) {
render(props.container);
},
update: () => {},
unmount: () => {},
});
};
// 检查是否在 Qiankun 环境中
console.log('qiankunWindow', qiankunWindow.__POWERED_BY_QIANKUN__);
if (qiankunWindow.__POWERED_BY_QIANKUN__) qiankun(); // 以子应用的方式启动
else render();在
packages/app2/src/App.tsx
中配置路由1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18import { createBrowserRouter, RouterProvider } from 'react-router-dom';
/** 创建路由 */
const routes = createBrowserRouter(
[
{
path: '/',
element: <h2>app2</h2>,
},
],
{ basename: '/app2' } // 设置路由前缀
);
function App() {
return <RouterProvider router={routes} />;
}
export default App;
配置样式隔离
在
packages/app/src/App.tsx
中添加测试div
和Button
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69import { Button } from 'antd';
import {
createBrowserRouter,
Outlet,
RouterProvider,
useNavigate,
} from 'react-router-dom';
const BaseLayout = () => {
const navigate = useNavigate();
const handleClick = (name: string) => {
navigate(`/${name}`);
};
return (
<div className="home">
<h1>主应用</h1>
<div className="btn-group">
<button onClick={() => handleClick('')}>主应用 app</button>
<button onClick={() => handleClick('app1')}>子应用 app1</button>
<button onClick={() => handleClick('app2')}>子应用 app2</button>
</div>
<div className="children">
<Outlet />
</div>
</div>
);
};
/** 创建路由 */
const routes = createBrowserRouter(
[
{
path: '/',
element: <BaseLayout />,
children: [
{
path: '/',
element: (
<>
{/* 测试样式 */}
<h2>app</h2>
<div className="test">测试样式隔离</div>
<Button>测试组件库样式</Button>
</>
),
},
{
path: 'app1/*', // 通配符 * 表示匹配所有子路由
element: <div id="subapp1"></div>, // 子应用挂载点 对应 main.tsx 注册子应用的 container
},
{
path: 'app2/*', // 通配符 * 表示匹配所有子路由
element: <div id="subapp2"></div>, // 子应用挂载点 对应 main.tsx 注册子应用的 container
},
],
},
],
{ basename: '/' }
);
function App() {
return <RouterProvider router={routes} />;
}
export default App;在
packages/app/src/index.css
添加测试样式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28.home {
display: flex;
align-items: center;
flex-direction: column;
}
.btn-group {
width: 300px;
display: flex;
justify-content: space-around;
}
.children {
width: 100%;
height: 400px;
margin-top: 40px;
border: 2px solid #000;
}
/* 测试样式 */
.test {
color: red;
}
.ant-btn {
height: 60px;
}
在
packages/app1/src/App.tsx
中添加测试div
和Button
, 并设置组件库样式前缀
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31import { Button, ConfigProvider } from 'antd';
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
/** 创建路由 */
const routes = createBrowserRouter(
[
{
path: '/',
element: (
<>
{/* 测试样式 */}
<h2>app1</h2>
<div className="test">测试样式隔离</div>
<Button>测试组件库样式</Button>
</>
),
},
],
{ basename: '/app1' } // 设置路由前缀
);
function App() {
return (
// 设置组件库样式前缀
<ConfigProvider prefixCls="app1">
<RouterProvider router={routes} />
</ConfigProvider>
);
}
export default App;在
packages/app1/src/index.css
添加测试样式
1
2
3
4
5
6
7.test {
color: blue;
}
.ant-btn {
height: 60px;
}
在
packages/app2/vite.config.ts
中配置postcss-prefix-selector
插件1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react-swc';
import qiankun from 'vite-plugin-qiankun';
import prefixer from 'postcss-prefix-selector';
// https://vite.dev/config/
export default ({ mode }) => {
const useDevMode = mode === 'development';
const host = '127.0.0.1';
const port = 8002;
const subAppName = 'app2';
const base = useDevMode
? `http://${host}:${port}/${subAppName}`
: `/${subAppName}`; // 这里 subAppName 对应 createBrowserRouter 的 basename
return defineConfig({
base,
server: {
port,
cors: true, // 作为子应用时,如果不配置,则会引起跨域问题
origin: `http://${host}:${port}`, // 必须配置,否则无法访问静态资源
},
plugins: [
// 在开发模式下需要把react()关掉
// https://github.com/tengmaoqing/vite-plugin-qiankun?tab=readme-ov-file#3dev%E4%B8%8B%E4%BD%9C%E4%B8%BA%E5%AD%90%E5%BA%94%E7%94%A8%E8%B0%83%E8%AF%95
...[useDevMode ? [] : [react()]],
qiankun(subAppName, { useDevMode }),
],
css: {
postcss: {
plugins: [
prefixer({
prefix: `[data-qiankun-${subAppName}]`, // 这里的值要和 main.tsx 中的属性名保持一致
transform(prefix, selector, prefixedSelector, filePath, rule) {
if (selector.match(/^(html|body)/)) {
return selector.replace(/^([^\s]*)/, `$1 ${prefix}`);
}
if (filePath.match(/node_modules/)) {
return selector; // Do not prefix styles imported from node_modules
}
const annotation = rule.prev();
if (
annotation?.type === 'comment' &&
annotation.text.trim() === 'no-prefix'
) {
return selector; // Do not prefix style rules that are preceded by: /* no-prefix */
}
return prefixedSelector;
},
}),
],
},
},
});
};在
packages/app2/src/main.tsx
中给根节点
添加data-qiankun-app2
属性1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40import { createRoot } from 'react-dom/client';
import {
QiankunProps,
qiankunWindow,
renderWithQiankun,
} from 'vite-plugin-qiankun/dist/helper';
import App from './App.tsx';
import './index.css';
/** 渲染函数 */
const render = (container?: HTMLElement) => {
const app =
container || (document.getElementById('root') as HTMLDivElement);
/**
* 添加属性,用于样式隔离
* 注意:这里的属性名要和 postcss-prefix-selector 插件中的 prefix 保持一致
*/
app.setAttribute('data-qiankun-app2', 'true');
createRoot(app).render(<App />);
};
/** Qiankun 生命周期钩子 */
const qiankun = () => {
renderWithQiankun({
bootstrap() {},
async mount(props: QiankunProps) {
render(props.container);
},
update: () => {},
unmount: () => {},
});
};
// 检查是否在 Qiankun 环境中
console.log('qiankunWindow', qiankunWindow.__POWERED_BY_QIANKUN__);
if (qiankunWindow.__POWERED_BY_QIANKUN__) qiankun(); // 以子应用的方式启动
else render();在
packages/app2/src/App.tsx
中添加测试div
和Button
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26import { Button } from 'antd';
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
/** 创建路由 */
const routes = createBrowserRouter(
[
{
path: '/',
element: (
<>
{/* 测试样式 */}
<h2>app2</h2>
<div className="test">测试样式隔离</div>
<Button>测试组件库样式</Button>
</>
),
},
],
{ basename: '/app2' } // 设置路由前缀
);
function App() {
return <RouterProvider router={routes} />;
}
export default App;在
packages/app2/src/index.css
添加测试样式
1
2
3
4
5
6
7.test {
color: green;
}
.ant-btn {
height: 60px;
}
效果
主应用
app
和 子应用app1
样式没有隔离(出现样式混乱的情况), 但是组件库样式隔离生效主应用
app
和 子应用app2
样式隔离, 但是组件库样式没有隔离