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 <1244620067@qq.com>
Co-authored-by: tangjinzhou <415800467@qq.com>
This commit is contained in:
Zev Zhu 2023-08-06 12:28:22 +08:00 committed by GitHub
parent 531ae16553
commit 53dc5daaaf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 276 additions and 6 deletions

View File

@ -83,7 +83,7 @@ export default defineComponent({
return true;
};
// attachToParent();
const defaultContainer = document.createElement('div');
const defaultContainer = canUseDom() && document.createElement('div');
const getContainer = () => {
if (!supportDom) {
return null;

View File

@ -88,7 +88,7 @@ export const useStyleProvider = (props: UseStyleProviderProps) => {
const parentContext = useStyleInject();
const context = shallowRef<Partial<StyleContextProps>>({ ...defaultStyleContext });
watch(
[props, parentContext],
[() => unref(props), parentContext],
() => {
const mergedContext: Partial<StyleContextProps> = {
...parentContext.value,

View File

@ -413,7 +413,7 @@ export default function useStyleRegister(
// ============================================================================
// == SSR ==
// ============================================================================
export function extractStyle(cache: Cache) {
export function extractStyle(cache: Cache, plain = false) {
// prefix with `style` is used for `useStyleRegister` to cache style context
const styleKeys = Array.from(cache.cache.keys()).filter(key => key.startsWith('style%'));
@ -424,7 +424,9 @@ export function extractStyle(cache: Cache) {
styleKeys.forEach(key => {
const [styleStr, tokenKey, styleId]: [string, string, string] = cache.cache.get(key)![1];
styleText += `<style ${ATTR_TOKEN}="${tokenKey}" ${ATTR_MARK}="${styleId}">${styleStr}</style>`;
styleText += plain
? styleStr
: `<style ${ATTR_TOKEN}="${tokenKey}" ${ATTR_MARK}="${styleId}">${styleStr}</style>`;
});
return styleText;

View File

@ -2,6 +2,7 @@ import type { Ref } from 'vue';
import { computed, watchEffect } from 'vue';
import { updateCSS, removeCSS } from '../../vc-util/Dom/dynamicCSS';
import getScrollBarSize from '../../_util/getScrollBarSize';
import canUseDom from '../../_util/canUseDom';
const UNIQUE_ID = `vc-util-locker-${Date.now()}`;
@ -24,6 +25,9 @@ export default function useScrollLocker(lock?: Ref<boolean>) {
watchEffect(
onClear => {
if (!canUseDom()) {
return;
}
if (mergedLock.value) {
const scrollbarSize = getScrollBarSize();
const isOverflow = isBodyOverflowing();

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,62 @@
// import { StyleProvider } from '../../cssinjs';
import { extractStyle } from '../index';
import { ConfigProvider } from '../../../components';
const testGreenColor = '#008000';
describe('Static-Style-Extract', () => {
it('should extract static styles', () => {
const cssText = extractStyle();
expect(cssText).not.toContain(testGreenColor);
expect(cssText).toMatchSnapshot();
});
it('should extract static styles with customTheme', () => {
const cssText = extractStyle(node => {
return (
<ConfigProvider
theme={{
token: {
colorPrimary: testGreenColor,
},
}}
>
{node}
</ConfigProvider>
);
});
expect(cssText).toContain(testGreenColor);
expect(cssText).toMatchSnapshot();
});
// it('with custom hashPriority', () => {
// const cssText = extractStyle(
// (node) => (
// <StyleProvider hashPriority='high'>
// <ConfigProvider
// theme={{
// token: {
// colorPrimary: testGreenColor,
// },
// }}
// >
// {node}
// </ConfigProvider>
// </StyleProvider>
// ),
// );
// expect(cssText).toContain(testGreenColor);
// expect(cssText).not.toContain(':where');
// expect(cssText).toMatchSnapshot();
//
// const cssText2 = extractStyle((node) => (
// <ConfigProvider
// theme={{
// token: {
// colorPrimary: testGreenColor,
// },
// }}
// >
// {node}
// </ConfigProvider>
// ));
// expect(cssText2).toContain(':where');
// });
});

View File

@ -0,0 +1,90 @@
import { createCache, extractStyle as extStyle, StyleProvider } from '../cssinjs';
import * as antd from '../../components';
import { renderToString } from 'vue/server-renderer';
import type { CustomRender } from './interface';
const blackList: string[] = [
'ConfigProvider',
'Grid',
'Tour',
'SelectOptGroup',
'SelectOption',
'MentionsOption',
'TreeNode',
'TreeSelectNode',
'LocaleProvider',
];
const pickerMap = {
MonthPicker: 'month',
WeekPicker: 'week',
QuarterPicker: 'quarter',
};
const compChildNameMap = {
MenuDivider: 'Menu',
MenuItem: 'Menu',
MenuItemGroup: 'Menu',
SubMenu: 'Menu',
TableColumn: 'Table',
TableColumnGroup: 'Table',
TableSummary: 'Table',
TableSummaryRow: 'Table',
TableSummaryCell: 'Table',
TabPane: 'Tabs',
TimelineItem: 'Timeline',
};
const defaultNode = () => (
<>
{Object.keys(antd)
.filter(name => !blackList.includes(name) && name[0] === name[0].toUpperCase())
.map(compName => {
const Comp = antd[compName];
if (compName === 'Dropdown') {
return (
<Comp key={compName} menu={{ items: [] }}>
<div />
</Comp>
);
}
if (compName === 'Anchor') {
return <Comp key={compName} items={[]} />;
}
if (compName in pickerMap) {
const Comp = antd['DatePicker'];
const type = pickerMap[compName];
return <Comp key={compName} picker={type} />;
}
if (compName in compChildNameMap) {
const ParentComp = antd[compChildNameMap[compName]];
return (
<ParentComp>
<Comp />
</ParentComp>
);
}
if (compName === 'QRCode' || compName === 'Segmented') {
return (
<Comp key={compName} value={''}>
<div />
</Comp>
);
}
return <Comp key={compName} />;
})}
</>
);
export function extractStyle(customTheme?: CustomRender): string {
const cache = createCache();
renderToString(
<StyleProvider cache={cache}>
{customTheme ? customTheme(defaultNode()) : defaultNode()}
</StyleProvider>,
);
// Grab style from cache
const styleText = extStyle(cache, true);
return styleText;
}

View File

@ -0,0 +1,3 @@
import type { VueNode } from '../type';
export type CustomRender = (node: VueNode) => VueNode;

View File

@ -10,7 +10,6 @@
</div>
</template>
<script lang="ts" setup>
import { defineProps } from 'vue';
interface Props {
title: string;
content?: string;

View File

@ -9,6 +9,7 @@ import { useProvideFloatButtonGroupContext } from './context';
import { findDOMNode, initDefaultProps } from '../_util/props-util';
import { floatButtonGroupProps } from './interface';
import type { FloatButtonGroupProps } from './interface';
import canUseDom from '../_util/canUseDom';
// CSSINJS
import useStyle from './style';
@ -74,6 +75,9 @@ const FloatButtonGroup = defineComponent({
watch(
computed(() => props.trigger),
value => {
if (!canUseDom()) {
return;
}
document.removeEventListener('click', onClick);
if (value === 'click') {
document.addEventListener('click', onClick);

View File

@ -14,7 +14,6 @@
</template>
<script lang="ts" setup>
import { defineProps, defineEmits } from 'vue';
import { Form } from 'ant-design-vue';
export type Currency = 'rmb' | 'dollar';

View File

@ -105,6 +105,16 @@ const routes = [
meta: { enTitle: 'Customize Theme', title: '定制主题', category: 'docs' },
component: () => import('../vueDocs/customize-theme.en-US.md'),
},
{
path: 'vue/ssr-extract-ssr',
meta: { enTitle: 'SSR Static style export', title: 'SSR 静态样式导出', category: 'docs' },
component: () => import('../vueDocs/extract-ssr.en-US.md'),
},
{
path: 'vue/ssr-extract-ssr-cn',
meta: { enTitle: 'SSR Static style export', title: 'SSR 静态样式导出', category: 'docs' },
component: () => import('../vueDocs/extract-ssr.zh-CN.md'),
},
{
path: 'vue/replace-date-cn',
meta: { enTitle: 'Custom Date Library', title: '自定义时间库', category: 'docs' },

View File

@ -0,0 +1,46 @@
# SSR Static style export
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.
```tsx
const cache = createCache();
// HTML Content
renderToString(
<StyleProvider cache={cache}>
<Button />
<Switch />
<Input />
{/* Rest antd components */}
</StyleProvider>,
);
// Style Content
const styleText = extractStyle(cache);
```
Of course, this is a little cumbersome for developers. so we provide a method to fulfill this requirement:
```tsx
import { extractStyle } from 'ant-design-vue/lib/_util/static-style-extract';
import fs from 'fs';
// `extractStyle` containers all the antd component
// excludes popup like component which is no need in ssr: Modal, message, notification, etc.
const css = extractStyle();
fs.writeFile(...);
```
If developers use a hybrid theme, they can also implement the hybrid requirements by themselves:
```tsx
// `node` is the components set we prepared
const css = extractStyle(node => (
<>
<ConfigProvider theme={theme1}>{node}</ConfigProvider>
<ConfigProvider theme={theme2}>{node}</ConfigProvider>
<ConfigProvider theme={theme3}>{node}</ConfigProvider>
</>
));
```

View File

@ -0,0 +1,46 @@
# SSR 静态样式导出
我们在思考是否可以如 v3 版本一样,预先烘焙组件的样式来使前端消费,所以提出了 [\[RFC\] Static Extract style](https://github.com/ant-design/ant-design/discussions/40985)。它的思路很简单,我们只需要提前将所有的组件进行一次渲染就可以从 cache 中获得完整的样式,然后将其写入到 css 文件中即可。
```tsx
const cache = createCache();
// HTML Content
renderToString(
<StyleProvider cache={cache}>
<Button />
<Switch />
<Input />
{/* Rest antd components */}
</StyleProvider>,
);
// Style Content
const styleText = extractStyle(cache);
```
当然,这对于开发者而言稍微有点麻烦。所以我们提供了一个方法来实现该需求:
```tsx
import { extractStyle } from 'ant-design-vue/lib/_util/static-style-extract';
import fs from 'fs';
// `extractStyle` containers all the antd component
// excludes popup like component which is no need in ssr: Modal, message, notification, etc.
const css = extractStyle();
fs.writeFile(...);
```
如果开发者使用了混合主题,也可以自行实现混合需求:
```tsx
// `node` is the components set we prepared
const css = extractStyle(node => (
<>
<ConfigProvider theme={theme1}>{node}</ConfigProvider>
<ConfigProvider theme={theme2}>{node}</ConfigProvider>
<ConfigProvider theme={theme3}>{node}</ConfigProvider>
</>
));
```