diff --git a/examples/main-react/package.json b/examples/main-react/package.json index ad868be01..ded9d1d7c 100644 --- a/examples/main-react/package.json +++ b/examples/main-react/package.json @@ -62,6 +62,7 @@ }, "scripts": { "start": "node scripts/start.js", + "integration": "node scripts/integration.js", "build": "node scripts/build.js", "test": "node scripts/test.js" }, diff --git a/examples/main-react/scripts/integration.js b/examples/main-react/scripts/integration.js new file mode 100644 index 000000000..7def77f71 --- /dev/null +++ b/examples/main-react/scripts/integration.js @@ -0,0 +1,153 @@ + + +// Do this as the first thing so that any code reading it knows the right env. +process.env.BABEL_ENV = 'development'; +process.env.NODE_ENV = 'development'; + +// Makes the script crash on unhandled rejections instead of silently +// ignoring them. In the future, promise rejections that are not handled will +// terminate the Node.js process with a non-zero exit code. +process.on('unhandledRejection', err => { + throw err; +}); + +// Ensure environment variables are read. +require('../config/env'); + +const fs = require('fs'); +const chalk = require('react-dev-utils/chalk'); +const webpack = require('webpack'); +const WebpackDevServer = require('webpack-dev-server'); +const clearConsole = require('react-dev-utils/clearConsole'); +const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles'); +const { + choosePort, + createCompiler, + prepareProxy, + prepareUrls, +} = require('react-dev-utils/WebpackDevServerUtils'); +const openBrowser = require('react-dev-utils/openBrowser'); +const semver = require('semver'); +const paths = require('../config/paths'); +const configFactory = require('../config/webpack.config'); +const createDevServerConfig = require('../config/webpackDevServer.config'); +const getClientEnvironment = require('../config/env'); +const react = require(require.resolve('react', { paths: [paths.appPath] })); + +const env = getClientEnvironment(paths.publicUrlOrPath.slice(0, -1)); +const useYarn = fs.existsSync(paths.yarnLockFile); +const isInteractive = process.stdout.isTTY; + +// Warn and crash if required files are missing +if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) { + process.exit(1); +} + +// Tools like Cloud9 rely on this. +const DEFAULT_PORT = parseInt(process.env.PORT, 10) || 3000; +const HOST = process.env.HOST || '0.0.0.0'; + +if (process.env.HOST) { + console.log( + chalk.cyan( + `Attempting to bind to HOST environment variable: ${chalk.yellow( + chalk.bold(process.env.HOST) + )}` + ) + ); + console.log( + `If this was unintentional, check that you haven't mistakenly set it in your shell.` + ); + console.log( + `Learn more here: ${chalk.yellow('https://cra.link/advanced-config')}` + ); + console.log(); +} + +// We require that you explicitly set browsers and do not fall back to +// browserslist defaults. +const { checkBrowsers } = require('react-dev-utils/browsersHelper'); +checkBrowsers(paths.appPath, isInteractive) + .then(() => { + // We attempt to use the default port but if it is busy, we offer the user to + // run on a different port. `choosePort()` Promise resolves to the next free port. + return choosePort(HOST, DEFAULT_PORT); + }) + .then(port => { + if (port == null) { + // We have not found a port. + return; + } + + const config = configFactory('development'); + const protocol = process.env.HTTPS === 'true' ? 'https' : 'http'; + const appName = require(paths.appPackageJson).name; + + const useTypeScript = fs.existsSync(paths.appTsConfig); + const urls = prepareUrls( + protocol, + HOST, + port, + paths.publicUrlOrPath.slice(0, -1) + ); + // Create a webpack compiler that is configured with custom messages. + const compiler = createCompiler({ + appName, + config, + urls, + useYarn, + useTypeScript, + webpack, + }); + // Load proxy config + const proxySetting = require(paths.appPackageJson).proxy; + const proxyConfig = prepareProxy( + proxySetting, + paths.appPublic, + paths.publicUrlOrPath + ); + // Serve webpack assets generated by the compiler over a web server. + const serverConfig = { + ...createDevServerConfig(proxyConfig, urls.lanUrlForConfig), + host: HOST, + port, + }; + const devServer = new WebpackDevServer(serverConfig, compiler); + // Launch WebpackDevServer. + devServer.startCallback(() => { + if (isInteractive) { + clearConsole(); + } + + if (env.raw.FAST_REFRESH && semver.lt(react.version, '16.10.0')) { + console.log( + chalk.yellow( + `Fast Refresh requires React 16.10 or higher. You are using React ${react.version}.` + ) + ); + } + + console.log(chalk.cyan('Starting the development server...\n')); + }); + + ['SIGINT', 'SIGTERM'].forEach(function (sig) { + process.on(sig, function () { + devServer.close(); + process.exit(); + }); + }); + + if (process.env.CI !== 'true') { + // Gracefully exit when stdin ends + process.stdin.on('end', function () { + devServer.close(); + process.exit(); + }); + } + }) + .catch(err => { + if (err && err.message) { + console.log(err.message); + } + process.exit(1); + }); diff --git a/examples/main-react/scripts/start.js b/examples/main-react/scripts/start.js index 41047988f..3f59d7a8f 100644 --- a/examples/main-react/scripts/start.js +++ b/examples/main-react/scripts/start.js @@ -1,4 +1,4 @@ -'use strict'; + // Do this as the first thing so that any code reading it knows the right env. process.env.BABEL_ENV = 'development'; diff --git a/examples/main-vue/vue.config.js b/examples/main-vue/vue.config.js index e1619d310..bb314ab1b 100644 --- a/examples/main-vue/vue.config.js +++ b/examples/main-vue/vue.config.js @@ -9,7 +9,7 @@ module.exports = { headers: { "Access-Control-Allow-Origin": "*", }, - open: true, + open: process.env.NODE_ENV === "development", port: "8000", }, }; diff --git a/examples/react16/package.json b/examples/react16/package.json index 20762056b..8b5903673 100644 --- a/examples/react16/package.json +++ b/examples/react16/package.json @@ -52,6 +52,7 @@ "source-map-loader": "^3.0.0", "style-loader": "^3.3.1", "tailwindcss": "^3.0.2", + "tdesign-icons-react": "^0.1.5", "terser-webpack-plugin": "^5.2.5", "web-vitals": "^2.1.4", "webpack": "^5.64.4", diff --git a/examples/react16/src/App.js b/examples/react16/src/App.js index bc5b29208..58ba1627c 100644 --- a/examples/react16/src/App.js +++ b/examples/react16/src/App.js @@ -3,7 +3,8 @@ import { NavLink, Route, Switch, Redirect } from "react-router-dom"; import Dialog from "./Dialog"; import Location from "./Location"; import Communication from "./Communication"; -import React17 from "./nest"; +import React17 from "./nest"; +import Font from "./Font"; import logo from "./logo.svg"; import Tag from "antd/es/tag"; import Button from "antd/es/button"; @@ -42,7 +43,7 @@ export default function App() {
@@ -65,6 +66,9 @@ export default function App() { + + + diff --git a/examples/react16/src/Font.js b/examples/react16/src/Font.js new file mode 100644 index 000000000..ef6009feb --- /dev/null +++ b/examples/react16/src/Font.js @@ -0,0 +1,34 @@ +import React from "react"; +import { IconFont } from "tdesign-icons-react"; + +export default class Font extends React.Component { + componentDidMount() { + console.log("react16 font mounted") + } + render() { + return ( +
+

字体处理

+
+

背景

+

+ 子应用的 dom 挂载在 Web Component 的 shadowRoot 内, + + shadowRoot 内部的字体文件不会加载 + +

+

解决

+

+ 框架加载子应用时将自定义字体样式提取到 shadowRoot 的外部,注意主应用和子应用的 @font-face 的 font-family + 不要重名,否则会有字体覆盖的问题。 +

+

IconFont 图标示例

+

TDesign icon

+ + + +
+
+ ); + } +} diff --git a/packages/wujie-core/__test__/integration/common.ts b/packages/wujie-core/__test__/integration/common.ts index 7e999ce0c..21b26c4c3 100644 --- a/packages/wujie-core/__test__/integration/common.ts +++ b/packages/wujie-core/__test__/integration/common.ts @@ -17,6 +17,8 @@ export const reactMainAppInfoMap = { routeMountedMessage: "react16 location mounted", communicationNavSelectorInAll: `document.querySelector("#root > div > div.content > div > div:nth-child(1) > div > wujie-app").shadowRoot.querySelector("#root > div > nav > a:nth-child(4)")`, nestNavSelectorInAll: `document.querySelector("#root > div > div.content > div > div:nth-child(1) > div > wujie-app").shadowRoot.querySelector("#root > div > nav > a:nth-child(5)")`, + fontNavSelector: `document.querySelector("#root > div > div.content > div > wujie-app").shadowRoot.querySelector("#root > div > nav > a:nth-child(6)")`, + fontMountedMessage: `react16 font mounted`, preloadTitleJsSelector: 'window.frames.react16.document.querySelector("#root > div > div:nth-child(3) > h2")', degradeTitleJsSelector: `window.document.querySelector("iframe[data-wujie-id='react16']").contentDocument.querySelector("#root > div > div:nth-child(3) > h2")`, titleJsSelector: @@ -153,6 +155,8 @@ export const vueMainAppInfoMap = { routeMountedMessage: "react16 location mounted", communicationNavSelectorInAll: `document.querySelector("#app > div.content > div > div:nth-child(1) > wujie-app").shadowRoot.querySelector("#root > div > nav > a:nth-child(4)")`, nestNavSelectorInAll: `document.querySelector("#app > div.content > div > div:nth-child(1) > wujie-app").shadowRoot.querySelector("#root > div > nav > a:nth-child(5)")`, + fontNavSelector: `document.querySelector("#app > div.content > div > wujie-app").shadowRoot.querySelector("#root > div > nav > a:nth-child(6)")`, + fontMountedMessage: `react16 font mounted`, preloadTitleJsSelector: 'window.frames.react16.document.querySelector("#root > div > div:nth-child(3) > h2")', degradeTitleJsSelector: `window.document.querySelector("iframe[data-wujie-id='react16']").contentDocument.querySelector("#root > div > div:nth-child(3) > h2")`, titleJsSelector: diff --git a/packages/wujie-core/__test__/integration/font.test.ts b/packages/wujie-core/__test__/integration/font.test.ts new file mode 100644 index 000000000..ce8b3a704 --- /dev/null +++ b/packages/wujie-core/__test__/integration/font.test.ts @@ -0,0 +1,57 @@ +import { awaitConsoleLogMessage, triggerClickByJsSelector } from "./utils"; +import { reactMainAppInfoMap, vueMainAppInfoMap } from "./common"; + +describe("main react startApp", () => { + beforeAll(async () => { + await page.evaluateOnNewDocument(() => { + // 关闭预加载 + localStorage.clear(); + localStorage.setItem("preload", "false"); + localStorage.setItem("degrade", "false"); + }); + await page.goto("http://localhost:7700/"); + }); + it("check react16 font-face", async () => { + const appInfo = reactMainAppInfoMap.react16; + const appInfoMountedPromise = awaitConsoleLogMessage(page, appInfo.mountedMessage); + expect(await page.evaluate(() => document.fonts.check("12px t", "E07F"))).toBe(false); + await page.click(appInfo.linkSelector); + await appInfoMountedPromise; + const appInfoFontMountedPromise = awaitConsoleLogMessage(page, appInfo.fontMountedMessage); + await triggerClickByJsSelector(page, appInfo.fontNavSelector); + await appInfoFontMountedPromise; + // 等待字体加载 + await page.waitForResponse((response) => response.url().includes("https://tdesign.gtimg.com/icon/0.1.1/fonts")); + // 等待字体装载 + await new Promise((resolve) => setTimeout(resolve, 100)); + // 检查字体是否生效 + expect(await page.evaluate(() => document.fonts.check("12px t", "E07F"))).toBe(true); + }); +}); +describe("main vue startApp", () => { + beforeAll(async () => { + await page.evaluateOnNewDocument(() => { + // 关闭预加载 + localStorage.clear(); + localStorage.setItem("preload", "false"); + localStorage.setItem("degrade", "false"); + }); + await page.goto("http://localhost:8000/"); + }); + it("check react16 font-face", async () => { + const appInfo = vueMainAppInfoMap.react16; + const appInfoMountedPromise = awaitConsoleLogMessage(page, appInfo.mountedMessage); + expect(await page.evaluate(() => document.fonts.check("12px t", "E07F"))).toBe(false); + await page.click(appInfo.linkSelector); + await appInfoMountedPromise; + const appInfoFontMountedPromise = awaitConsoleLogMessage(page, appInfo.fontMountedMessage); + await triggerClickByJsSelector(page, appInfo.fontNavSelector); + await appInfoFontMountedPromise; + // 等待字体加载 + await page.waitForResponse((response) => response.url().includes("https://tdesign.gtimg.com/icon/0.1.1/fonts")); + // 等待字体装载 + await new Promise((resolve) => setTimeout(resolve, 100)); + // 检查字体是否生效 + expect(await page.evaluate(() => document.fonts.check("12px t", "E07F"))).toBe(true); + }); +}); diff --git a/packages/wujie-core/jest-puppeteer.config.js b/packages/wujie-core/jest-puppeteer.config.js index 53529068e..24492ef3e 100644 --- a/packages/wujie-core/jest-puppeteer.config.js +++ b/packages/wujie-core/jest-puppeteer.config.js @@ -48,7 +48,7 @@ module.exports = { port: 7400, }, { - command: 'lerna run start --scope main-react', + command: 'lerna run integration --scope main-react', usedPortAction: 'kill', launchTimeout: 60000, host: '0.0.0.0', diff --git a/packages/wujie-core/src/effect.ts b/packages/wujie-core/src/effect.ts index 8b4118878..13ad1a97a 100644 --- a/packages/wujie-core/src/effect.ts +++ b/packages/wujie-core/src/effect.ts @@ -16,10 +16,11 @@ import { nextTick, isExcludeUrl, getExcludes, + getCurUrl, } from "./utils"; import { insertScriptToIframe } from "./iframe"; import Wujie from "./sandbox"; -import { getHostCssRules } from "./shadow"; +import { getPatchStyleElements } from "./shadow"; import { WUJIE_DATA_ID, WUJIE_DATA_FLAG, WUJIE_TIPS_REPEAT_RENDER } from "./constant"; import { ScriptObject } from "./template"; @@ -55,12 +56,19 @@ function manualInvokeElementEvent(element: HTMLLinkElement | HTMLScriptElement, /** * 样式元素的css变量处理 */ -function handleStylesheetElementHost(stylesheetElement: HTMLStyleElement, sandbox: Wujie) { +function handleStylesheetElementPatch(stylesheetElement: HTMLStyleElement, sandbox: Wujie, baseUrl?: string) { if (!stylesheetElement.innerHTML || sandbox.degrade) return; - const hostStyleSheetElement = getHostCssRules([stylesheetElement.sheet]); + const curUrl = getCurUrl(sandbox.proxyLocation as Location); + const [hostStyleSheetElement, fontStyleSheetElement] = getPatchStyleElements( + [stylesheetElement.sheet], + baseUrl ? baseUrl : curUrl + ); if (hostStyleSheetElement) { sandbox.shadowRoot.head.appendChild(hostStyleSheetElement); } + if (fontStyleSheetElement) { + sandbox.shadowRoot.host.appendChild(fontStyleSheetElement); + } } /** @@ -81,7 +89,7 @@ function patchStylesheetElement( }, set: function (code: string) { innerHTMLDesc.set.call(stylesheetElement, cssLoader(code)); - nextTick(() => handleStylesheetElementHost(this, sandbox)); + nextTick(() => handleStylesheetElementPatch(this, sandbox)); }, }, innerText: { @@ -90,7 +98,7 @@ function patchStylesheetElement( }, set: function (code: string) { innerTextDesc.set.call(stylesheetElement, cssLoader(code)); - nextTick(() => handleStylesheetElementHost(this, sandbox)); + nextTick(() => handleStylesheetElementPatch(this, sandbox)); }, }, textContent: { @@ -99,12 +107,12 @@ function patchStylesheetElement( }, set: function (code: string) { textContentDesc.set.call(stylesheetElement, cssLoader(code)); - nextTick(() => handleStylesheetElementHost(this, sandbox)); + nextTick(() => handleStylesheetElementPatch(this, sandbox)); }, }, appendChild: { value: function (node: Node): Node { - nextTick(() => handleStylesheetElementHost(this, sandbox)); + nextTick(() => handleStylesheetElementPatch(this, sandbox)); if (node.nodeType === Node.TEXT_NODE) { return rawAppendChild.call( stylesheetElement, @@ -161,9 +169,9 @@ function rewriteAppendOrInsertChild(opts: { src ); styleSheetElements.push(stylesheetElement); - // 处理host的情况 - handleStylesheetElementHost(stylesheetElement, sandbox); rawDOMAppendOrInsertBefore.call(this, stylesheetElement, refChild); + // 处理样式补丁 + handleStylesheetElementPatch(stylesheetElement, sandbox, href); manualInvokeElementEvent(element, "load"); element = null; }, @@ -186,9 +194,10 @@ function rewriteAppendOrInsertChild(opts: { compose(plugins.map((plugin) => plugin.cssLoader))(replace ? replace(content) : content); content && (stylesheetElement.innerHTML = cssLoader(content)); patchStylesheetElement(stylesheetElement, cssLoader, sandbox); - // 处理host的情况 - nextTick(() => handleStylesheetElementHost(stylesheetElement, sandbox)); - return rawDOMAppendOrInsertBefore.call(this, element, refChild); + const res = rawDOMAppendOrInsertBefore.call(this, element, refChild); + // 处理样式补丁 + handleStylesheetElementPatch(stylesheetElement, sandbox); + return res; } case "SCRIPT": { const { src, text, type, crossOrigin } = element as HTMLScriptElement; diff --git a/packages/wujie-core/src/sandbox.ts b/packages/wujie-core/src/sandbox.ts index b68285914..caaa78a02 100644 --- a/packages/wujie-core/src/sandbox.ts +++ b/packages/wujie-core/src/sandbox.ts @@ -9,7 +9,7 @@ import { syncUrlToWindow, syncUrlToIframe, clearInactiveAppUrl } from "./sync"; import { createWujieWebComponent, clearChild, - getHostCssRules, + getPatchStyleElements, renderElementToContainer, renderTemplateToShadowRoot, createIframeContainer, @@ -25,7 +25,7 @@ import { rawDocumentQuerySelector, } from "./common"; import { EventBus, appEventObjMap, EventObj } from "./event"; -import { isFunction, wujieSupport, appRouteParse, requestIdleCallback } from "./utils"; +import { isFunction, wujieSupport, appRouteParse, requestIdleCallback, getCurUrl } from "./utils"; import { WUJIE_DATA_ATTACH_CSS_FLAG } from "./constant"; import { plugin, ScriptObjectLoader, loadErrorHandler } from "./index"; @@ -222,6 +222,7 @@ export default class Wujie { } await renderTemplateToShadowRoot(this.shadowRoot, iframeWindow, this.template); + this.patchCssRules(); // inject shadowRoot to app this.provide.shadowRoot = this.shadowRoot; @@ -321,7 +322,6 @@ export default class Wujie { */ public mount(): void { if (this.mountFlag) return; - this.attachHostCssRules(); if (isFunction(this.iframe.contentWindow.__WUJIE_MOUNT)) { this.lifecycles?.beforeMount?.(this.iframe.contentWindow); this.iframe.contentWindow.__WUJIE_MOUNT(); @@ -395,23 +395,33 @@ export default class Wujie { rawElementAppendChild.call(this.degrade ? this.document.head : this.shadowRoot.head, styleSheetElement); }); } - this.attachHostCssRules(); + this.patchCssRules(); } - /** 兼容:root选择器样式到:host选择器上 */ - public attachHostCssRules(): void { + /** + * 子应用样式打补丁 + * 1、兼容:root选择器样式到:host选择器上 + * 2、将@font-face定义到shadowRoot外部 + */ + public patchCssRules(): void { + const curUrl = getCurUrl(this.proxyLocation as Location); if (this.degrade) return; if (this.shadowRoot.host.hasAttribute(WUJIE_DATA_ATTACH_CSS_FLAG)) return; - const hostStyleSheetElement = getHostCssRules( + const [hostStyleSheetElement, fontStyleSheetElement] = getPatchStyleElements( Array.from(this.iframe.contentDocument.querySelectorAll("style")).map( (styleSheetElement) => styleSheetElement.sheet - ) + ), + curUrl ); if (hostStyleSheetElement) { this.shadowRoot.head.appendChild(hostStyleSheetElement); this.styleSheetElements.push(hostStyleSheetElement); - this.shadowRoot.host.setAttribute(WUJIE_DATA_ATTACH_CSS_FLAG, ""); } + if (fontStyleSheetElement) { + this.shadowRoot.host.appendChild(fontStyleSheetElement); + } + (hostStyleSheetElement || fontStyleSheetElement) && + this.shadowRoot.host.setAttribute(WUJIE_DATA_ATTACH_CSS_FLAG, ""); } /** diff --git a/packages/wujie-core/src/shadow.ts b/packages/wujie-core/src/shadow.ts index 547bd2208..68cfc5fd8 100644 --- a/packages/wujie-core/src/shadow.ts +++ b/packages/wujie-core/src/shadow.ts @@ -218,27 +218,54 @@ export function clearChild(root: ShadowRoot | Node): void { } /** - * 获取:root选择器的样式到shadow的:host + * 获取修复好的样式元素 + * 主要是针对对root样式和font-face样式 */ -export function getHostCssRules(rootStyleSheets: Array): void | HTMLStyleElement { - const rootCssRule = []; +export function getPatchStyleElements( + rootStyleSheets: Array, + baseUrl: string +): Array { + const rootCssRules = []; + const fontCssRules = []; const rootStyleReg = /:root/g; + const fontStyleReg = /(url\()([^)]*)(\))/g; // 找出root的cssRules for (let i = 0; i < rootStyleSheets.length; i++) { const cssRules = rootStyleSheets[i]?.cssRules ?? []; for (let j = 0; j < cssRules.length; j++) { const cssRuleText = cssRules[j].cssText; + // 如果是root的cssRule if (rootStyleReg.test(cssRuleText)) { - rootCssRule.push(cssRuleText.replace(rootStyleReg, (match) => cssSelectorMap[match])); + rootCssRules.push(cssRuleText.replace(rootStyleReg, (match) => cssSelectorMap[match])); + } + // 如果是font-face的cssRule + if (cssRules[j].type === CSSRule.FONT_FACE_RULE) { + fontCssRules.push( + // 相对地址改绝对地址 + cssRuleText.replace(fontStyleReg, (_m, pre, url, post) => { + const urlString = url.replace(/["']/g, ""); + const absoluteUrl = new URL(urlString, baseUrl).href; + return pre + "'" + absoluteUrl + "'" + post; + }) + ); } } } + let rootStyleSheetElement = null; + let fontStyleSheetElement = null; + // 复制到host上 - if (rootCssRule.length) { - const styleSheetElement = window.document.createElement("style"); - styleSheetElement.innerHTML = rootCssRule.join(""); - return styleSheetElement; + if (rootCssRules.length) { + rootStyleSheetElement = window.document.createElement("style"); + rootStyleSheetElement.innerHTML = rootCssRules.join(""); } + + if (fontCssRules.length) { + fontStyleSheetElement = window.document.createElement("style"); + fontStyleSheetElement.innerHTML = fontCssRules.join(""); + } + + return [rootStyleSheetElement, fontStyleSheetElement]; } diff --git a/packages/wujie-core/src/utils.ts b/packages/wujie-core/src/utils.ts index dc6357b6b..05b5c13f9 100644 --- a/packages/wujie-core/src/utils.ts +++ b/packages/wujie-core/src/utils.ts @@ -170,6 +170,10 @@ export function fixElementCtrSrcOrHref( // TODO: innerHTML的处理 } +export function getCurUrl(proxyLocation: Location): string { + return proxyLocation.protocol + "//" + proxyLocation.host + proxyLocation.pathname; +} + /** * 获取需要同步的url */ diff --git a/packages/wujie-doc/docs/question/README.md b/packages/wujie-doc/docs/question/README.md index ec8cd8a69..37e7db18a 100644 --- a/packages/wujie-doc/docs/question/README.md +++ b/packages/wujie-doc/docs/question/README.md @@ -38,7 +38,7 @@ ctx.set("Access-Control-Allow-Origin", ctx.headers.origin); **原因:** `@font-face`不会在`shadow`内部加载,[详见](https://github.com/mdn/interactive-examples/issues/887) -**解决方案:** 将子应用需要到的`@font-face`在主应用进行加载 +**解决方案:** 框架已解决,会将子应用的`@font-face`放到`shadow`外部执行,注意子应用的自定义字体名和主应用的自定义字体名不能重复,否则可能存在覆盖问题 ## 4、冒泡系列组件(比如下拉框)弹出位置不正确