Skip to content

Commit 53dc5da

Browse files
aibayanyu20selicenstangjinzhou
authored
feat: add static-style-extract (#6713)
* docs: add ant-design-vue nuxt module * feat: add static-style-extract * docs: fix import defineProps and defineEmits * feat: add static-style extract test * fix: add ignore component * feat: calendar select support info.source param (#6697) * docs: add ant-design-vue nuxt module * feat: calendar select support info.source param * docs: synchronous config-provider demo (#6706) * revert: #6706 * feat: node error * fix: Handle the output of styles in Server Side Rendering mode * fix: fix value required error * chore: change tests dirname * fix: auchor warning * chore: add tests * chore: generate tests snap * docs: add ssr docs * docs: synchronous config-provider demo (#6706) * revert: #6706 * docs: add extract ssr doc * fix: Removing canUseDom does not have any effect. * fix: remove range picker in picker map * fix: watch source can only be a getter/effect function style warning * fix: styleContext props maybe ref * fix: remove black list component * feat: add compChildNameMap --------- Co-authored-by: selicens <[email protected]> Co-authored-by: tangjinzhou <[email protected]>
1 parent 531ae16 commit 53dc5da

File tree

14 files changed

+276
-6
lines changed

14 files changed

+276
-6
lines changed

components/_util/PortalWrapper.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ export default defineComponent({
8383
return true;
8484
};
8585
// attachToParent();
86-
const defaultContainer = document.createElement('div');
86+
const defaultContainer = canUseDom() && document.createElement('div');
8787
const getContainer = () => {
8888
if (!supportDom) {
8989
return null;

components/_util/cssinjs/StyleContext.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ export const useStyleProvider = (props: UseStyleProviderProps) => {
8888
const parentContext = useStyleInject();
8989
const context = shallowRef<Partial<StyleContextProps>>({ ...defaultStyleContext });
9090
watch(
91-
[props, parentContext],
91+
[() => unref(props), parentContext],
9292
() => {
9393
const mergedContext: Partial<StyleContextProps> = {
9494
...parentContext.value,

components/_util/cssinjs/hooks/useStyleRegister.tsx

+4-2
Original file line numberDiff line numberDiff line change
@@ -413,7 +413,7 @@ export default function useStyleRegister(
413413
// ============================================================================
414414
// == SSR ==
415415
// ============================================================================
416-
export function extractStyle(cache: Cache) {
416+
export function extractStyle(cache: Cache, plain = false) {
417417
// prefix with `style` is used for `useStyleRegister` to cache style context
418418
const styleKeys = Array.from(cache.cache.keys()).filter(key => key.startsWith('style%'));
419419

@@ -424,7 +424,9 @@ export function extractStyle(cache: Cache) {
424424
styleKeys.forEach(key => {
425425
const [styleStr, tokenKey, styleId]: [string, string, string] = cache.cache.get(key)![1];
426426

427-
styleText += `<style ${ATTR_TOKEN}="${tokenKey}" ${ATTR_MARK}="${styleId}">${styleStr}</style>`;
427+
styleText += plain
428+
? styleStr
429+
: `<style ${ATTR_TOKEN}="${tokenKey}" ${ATTR_MARK}="${styleId}">${styleStr}</style>`;
428430
});
429431

430432
return styleText;

components/_util/hooks/useScrollLocker.ts

+4
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { Ref } from 'vue';
22
import { computed, watchEffect } from 'vue';
33
import { updateCSS, removeCSS } from '../../vc-util/Dom/dynamicCSS';
44
import getScrollBarSize from '../../_util/getScrollBarSize';
5+
import canUseDom from '../../_util/canUseDom';
56

67
const UNIQUE_ID = `vc-util-locker-${Date.now()}`;
78

@@ -24,6 +25,9 @@ export default function useScrollLocker(lock?: Ref<boolean>) {
2425

2526
watchEffect(
2627
onClear => {
28+
if (!canUseDom()) {
29+
return;
30+
}
2731
if (mergedLock.value) {
2832
const scrollbarSize = getScrollBarSize();
2933
const isOverflow = isBodyOverflowing();

components/_util/static-style-extract/__tests__/__snapshots__/index.test.js.snap

+5
Large diffs are not rendered by default.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// import { StyleProvider } from '../../cssinjs';
2+
import { extractStyle } from '../index';
3+
import { ConfigProvider } from '../../../components';
4+
5+
const testGreenColor = '#008000';
6+
describe('Static-Style-Extract', () => {
7+
it('should extract static styles', () => {
8+
const cssText = extractStyle();
9+
expect(cssText).not.toContain(testGreenColor);
10+
expect(cssText).toMatchSnapshot();
11+
});
12+
it('should extract static styles with customTheme', () => {
13+
const cssText = extractStyle(node => {
14+
return (
15+
<ConfigProvider
16+
theme={{
17+
token: {
18+
colorPrimary: testGreenColor,
19+
},
20+
}}
21+
>
22+
{node}
23+
</ConfigProvider>
24+
);
25+
});
26+
expect(cssText).toContain(testGreenColor);
27+
expect(cssText).toMatchSnapshot();
28+
});
29+
// it('with custom hashPriority', () => {
30+
// const cssText = extractStyle(
31+
// (node) => (
32+
// <StyleProvider hashPriority='high'>
33+
// <ConfigProvider
34+
// theme={{
35+
// token: {
36+
// colorPrimary: testGreenColor,
37+
// },
38+
// }}
39+
// >
40+
// {node}
41+
// </ConfigProvider>
42+
// </StyleProvider>
43+
// ),
44+
// );
45+
// expect(cssText).toContain(testGreenColor);
46+
// expect(cssText).not.toContain(':where');
47+
// expect(cssText).toMatchSnapshot();
48+
//
49+
// const cssText2 = extractStyle((node) => (
50+
// <ConfigProvider
51+
// theme={{
52+
// token: {
53+
// colorPrimary: testGreenColor,
54+
// },
55+
// }}
56+
// >
57+
// {node}
58+
// </ConfigProvider>
59+
// ));
60+
// expect(cssText2).toContain(':where');
61+
// });
62+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { createCache, extractStyle as extStyle, StyleProvider } from '../cssinjs';
2+
import * as antd from '../../components';
3+
import { renderToString } from 'vue/server-renderer';
4+
import type { CustomRender } from './interface';
5+
const blackList: string[] = [
6+
'ConfigProvider',
7+
'Grid',
8+
'Tour',
9+
'SelectOptGroup',
10+
'SelectOption',
11+
'MentionsOption',
12+
'TreeNode',
13+
'TreeSelectNode',
14+
'LocaleProvider',
15+
];
16+
17+
const pickerMap = {
18+
MonthPicker: 'month',
19+
WeekPicker: 'week',
20+
QuarterPicker: 'quarter',
21+
};
22+
23+
const compChildNameMap = {
24+
MenuDivider: 'Menu',
25+
MenuItem: 'Menu',
26+
MenuItemGroup: 'Menu',
27+
SubMenu: 'Menu',
28+
TableColumn: 'Table',
29+
TableColumnGroup: 'Table',
30+
TableSummary: 'Table',
31+
TableSummaryRow: 'Table',
32+
TableSummaryCell: 'Table',
33+
TabPane: 'Tabs',
34+
TimelineItem: 'Timeline',
35+
};
36+
37+
const defaultNode = () => (
38+
<>
39+
{Object.keys(antd)
40+
.filter(name => !blackList.includes(name) && name[0] === name[0].toUpperCase())
41+
.map(compName => {
42+
const Comp = antd[compName];
43+
if (compName === 'Dropdown') {
44+
return (
45+
<Comp key={compName} menu={{ items: [] }}>
46+
<div />
47+
</Comp>
48+
);
49+
}
50+
if (compName === 'Anchor') {
51+
return <Comp key={compName} items={[]} />;
52+
}
53+
if (compName in pickerMap) {
54+
const Comp = antd['DatePicker'];
55+
const type = pickerMap[compName];
56+
return <Comp key={compName} picker={type} />;
57+
}
58+
if (compName in compChildNameMap) {
59+
const ParentComp = antd[compChildNameMap[compName]];
60+
return (
61+
<ParentComp>
62+
<Comp />
63+
</ParentComp>
64+
);
65+
}
66+
if (compName === 'QRCode' || compName === 'Segmented') {
67+
return (
68+
<Comp key={compName} value={''}>
69+
<div />
70+
</Comp>
71+
);
72+
}
73+
return <Comp key={compName} />;
74+
})}
75+
</>
76+
);
77+
78+
export function extractStyle(customTheme?: CustomRender): string {
79+
const cache = createCache();
80+
renderToString(
81+
<StyleProvider cache={cache}>
82+
{customTheme ? customTheme(defaultNode()) : defaultNode()}
83+
</StyleProvider>,
84+
);
85+
86+
// Grab style from cache
87+
const styleText = extStyle(cache, true);
88+
89+
return styleText;
90+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import type { VueNode } from '../type';
2+
3+
export type CustomRender = (node: VueNode) => VueNode;

components/drawer/demo/descriptionItem/index.vue

-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
</div>
1111
</template>
1212
<script lang="ts" setup>
13-
import { defineProps } from 'vue';
1413
interface Props {
1514
title: string;
1615
content?: string;

components/float-button/FloatButtonGroup.tsx

+4
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { useProvideFloatButtonGroupContext } from './context';
99
import { findDOMNode, initDefaultProps } from '../_util/props-util';
1010
import { floatButtonGroupProps } from './interface';
1111
import type { FloatButtonGroupProps } from './interface';
12+
import canUseDom from '../_util/canUseDom';
1213

1314
// CSSINJS
1415
import useStyle from './style';
@@ -74,6 +75,9 @@ const FloatButtonGroup = defineComponent({
7475
watch(
7576
computed(() => props.trigger),
7677
value => {
78+
if (!canUseDom()) {
79+
return;
80+
}
7781
document.removeEventListener('click', onClick);
7882
if (value === 'click') {
7983
document.addEventListener('click', onClick);

components/form/demo/price-input.vue

-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
</template>
1515

1616
<script lang="ts" setup>
17-
import { defineProps, defineEmits } from 'vue';
1817
import { Form } from 'ant-design-vue';
1918
2019
export type Currency = 'rmb' | 'dollar';

site/src/router/index.js

+10
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,16 @@ const routes = [
105105
meta: { enTitle: 'Customize Theme', title: '定制主题', category: 'docs' },
106106
component: () => import('../vueDocs/customize-theme.en-US.md'),
107107
},
108+
{
109+
path: 'vue/ssr-extract-ssr',
110+
meta: { enTitle: 'SSR Static style export', title: 'SSR 静态样式导出', category: 'docs' },
111+
component: () => import('../vueDocs/extract-ssr.en-US.md'),
112+
},
113+
{
114+
path: 'vue/ssr-extract-ssr-cn',
115+
meta: { enTitle: 'SSR Static style export', title: 'SSR 静态样式导出', category: 'docs' },
116+
component: () => import('../vueDocs/extract-ssr.zh-CN.md'),
117+
},
108118
{
109119
path: 'vue/replace-date-cn',
110120
meta: { enTitle: 'Custom Date Library', title: '自定义时间库', category: 'docs' },

site/src/vueDocs/extract-ssr.en-US.md

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# SSR Static style export
2+
3+
We are thinking about whether we can pre-bake the style of the component for front-end consumption like the v3 version, so we proposed [\[RFC\] Static Extract style](https://github.com/ant-design/ant-design/discussions/40985). Its idea is very simple that only need to render all the components once in advance to get the complete style from the cache, and then write it into the css file.
4+
5+
```tsx
6+
const cache = createCache();
7+
8+
// HTML Content
9+
renderToString(
10+
<StyleProvider cache={cache}>
11+
<Button />
12+
<Switch />
13+
<Input />
14+
{/* Rest antd components */}
15+
</StyleProvider>,
16+
);
17+
18+
// Style Content
19+
const styleText = extractStyle(cache);
20+
```
21+
22+
Of course, this is a little cumbersome for developers. so we provide a method to fulfill this requirement:
23+
24+
```tsx
25+
import { extractStyle } from 'ant-design-vue/lib/_util/static-style-extract';
26+
import fs from 'fs';
27+
28+
// `extractStyle` containers all the antd component
29+
// excludes popup like component which is no need in ssr: Modal, message, notification, etc.
30+
const css = extractStyle();
31+
32+
fs.writeFile(...);
33+
```
34+
35+
If developers use a hybrid theme, they can also implement the hybrid requirements by themselves:
36+
37+
```tsx
38+
// `node` is the components set we prepared
39+
const css = extractStyle(node => (
40+
<>
41+
<ConfigProvider theme={theme1}>{node}</ConfigProvider>
42+
<ConfigProvider theme={theme2}>{node}</ConfigProvider>
43+
<ConfigProvider theme={theme3}>{node}</ConfigProvider>
44+
</>
45+
));
46+
```

site/src/vueDocs/extract-ssr.zh-CN.md

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# SSR 静态样式导出
2+
3+
我们在思考是否可以如 v3 版本一样,预先烘焙组件的样式来使前端消费,所以提出了 [\[RFC\] Static Extract style](https://github.com/ant-design/ant-design/discussions/40985)。它的思路很简单,我们只需要提前将所有的组件进行一次渲染就可以从 cache 中获得完整的样式,然后将其写入到 css 文件中即可。
4+
5+
```tsx
6+
const cache = createCache();
7+
8+
// HTML Content
9+
renderToString(
10+
<StyleProvider cache={cache}>
11+
<Button />
12+
<Switch />
13+
<Input />
14+
{/* Rest antd components */}
15+
</StyleProvider>,
16+
);
17+
18+
// Style Content
19+
const styleText = extractStyle(cache);
20+
```
21+
22+
当然,这对于开发者而言稍微有点麻烦。所以我们提供了一个方法来实现该需求:
23+
24+
```tsx
25+
import { extractStyle } from 'ant-design-vue/lib/_util/static-style-extract';
26+
import fs from 'fs';
27+
28+
// `extractStyle` containers all the antd component
29+
// excludes popup like component which is no need in ssr: Modal, message, notification, etc.
30+
const css = extractStyle();
31+
32+
fs.writeFile(...);
33+
```
34+
35+
如果开发者使用了混合主题,也可以自行实现混合需求:
36+
37+
```tsx
38+
// `node` is the components set we prepared
39+
const css = extractStyle(node => (
40+
<>
41+
<ConfigProvider theme={theme1}>{node}</ConfigProvider>
42+
<ConfigProvider theme={theme2}>{node}</ConfigProvider>
43+
<ConfigProvider theme={theme3}>{node}</ConfigProvider>
44+
</>
45+
));
46+
```

0 commit comments

Comments
 (0)