内容简介:微前端似乎是最近一个很火的话题,我们也即将使用在生产环境中,接下来会更新一系列微前端源码分析、手写微前端文章那么
写在开头:
微前端似乎是最近一个很火的话题,我们也即将使用在生产环境中,接下来会更新一系列微前端源码分析、手写微前端文章
废话不多说,直接参考目前的微前端框架注册子应用模块代码
- 下面代码,我指定的entry,就是子应用的访问入口地址
registerMicroApps( [ { name: 'rental-web', entry: isDev ? '//rental-dev.mysoft.com.cn:8809' : `//${url}:8809`, container: '#rental-web', render, activeRule: '/static', props: { value, setValue } }, { name: 'fed-rental-web', entry: isDev ? '//rental-dev.mysoft.com.cn:8006' : `//${url}:8006`, container: '#fed-rental-web', render, activeRule: '/fed', props: { value, setValue } },]
- 微前端到底是怎么回事呢? 我画了一张图
我们今天不谈其他的实现技术细节,坑点,就谈整体架构,这张图就能完全解释清楚
那么 registerMicroApps
,到底做了什么呢?
源码解析下,只看重要部分今天:
export function registerMicroApps(apps, lifeCycles) { var _this = this; // Each app only needs to be registered once var unregisteredApps = apps.filter(function (app) { return !microApps.some(function (registeredApp) { return registeredApp.name === app.name; }); }); microApps = __spread(microApps, unregisteredApps); unregisteredApps.forEach(function (app) { var name = app.name, activeRule = app.activeRule, props = app.props, appConfig = __rest(app, ["name", "activeRule", "props"]); registerApplication({ name: name, app: function app() { return __awaiter(_this, void 0, void 0, function () { return __generator(this, function (_a) { switch (_a.label) { case 0: return [4 /*yield*/ , frameworkStartedDefer.promise]; case 1: _a.sent(); return [2 /*return*/ , loadApp(__assign({ name: name, props: props }, appConfig), frameworkConfiguration, lifeCycles)]; } }); }); }, activeWhen: activeRule, customProps: props }); }); }
lifeCycles是我们自己传入的生命周期函数(这里先不解释),跟react这种框架一样,微前端针对每个子应用,也封装了一些生命周期,如果你是小白,那我就用最简单的话告诉你,生命周期钩子,其实在框架源码就是一个函数编写调用顺序而已(有的分异步和同步)
- apps就是我们传入的数组,子应用集合
- 代码里做了一些防重复注册、数据处理等
- 看源码,不要全部都看,那样很费时间,而且你也得不到利益最大化,只看最精髓、重要部分
- 无论上面做了上面子应用去重、数据处理,我只要盯着每个子应用,即app这个对象即可
- 看到了loadApp这个方法,我们可以大概猜测到,是通过这个方法加载
下面__rest是对数据进行处理
export function __rest(s, e) { var t = {}; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p]; if (s != null && typeof Object.getOwnPropertySymbols === "function") for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) t[p[i]] = s[p[i]]; } return t; }
- loadApp这个函数有大概300行,挑最重点地方看
export function loadApp(app, configuration, lifeCycles) { if (configuration === void 0) { configuration = {}; } var _a; return __awaiter(this, void 0, void 0, function () { var entry, appName, _b, singular, _c, sandbox, importEntryOpts, _d, template, execScripts, assetPublicPath, appInstanceId, strictStyleIsolation, appContent, element, container, legacyRender, render, containerGetter, global, mountSandbox, unmountSandbox, sandboxInstance, _e, _f, beforeUnmount, _g, afterUnmount, _h, afterMount, _j, beforeMount, _k, beforeLoad, scriptExports, bootstrap, mount, unmount, globalVariableExports, _l, onGlobalStateChange, setGlobalState, offGlobalStateChange; var _this = this; return __generator(this, function (_m) { switch (_m.label) { case 0: entry = app.entry, appName = app.name; _b = configuration.singular, singular = _b === void 0 ? false : _b, _c = configuration.sandbox, sandbox = _c === void 0 ? true : _c, importEntryOpts = __rest(configuration, ["singular", "sandbox"]); return [4 /*yield*/ , importEntry(entry, importEntryOpts)]; case 1: _d = _m.sent(), template = _d.template, execScripts = _d.execScripts, assetPublicPath = _d.assetPublicPath; return [4 /*yield*/ , validateSingularMode(singular, app)]; case 2: if (!_m.sent()) return [3 /*break*/ , 4]; return [4 /*yield*/ , prevAppUnmountedDeferred && prevAppUnmountedDeferred.promise]; case 3: _m.sent(); _m.label = 4; case 4: appInstanceId = appName + "_" + (appInstanceCounts.hasOwnProperty(appName) ? ((_a = appInstanceCounts[appName]) !== null && _a !== void 0 ? _a : 0) + 1 : 0); strictStyleIsolation = _typeof(sandbox) === 'object' && !!sandbox.strictStyleIsolation; appContent = getDefaultTplWrapper(appInstanceId)(template); element = createElement(appContent, strictStyleIsolation); container = 'container' in app ? app.container : undefined; legacyRender = 'render' in app ? app.render : undefined; render = getRender(appContent, container, legacyRender); // 第一次加载设置应用可见区域 dom 结构 // 确保每次应用加载前容器 dom 结构已经设置完毕 render({ element: element, loading: true }); containerGetter = getAppWrapperGetter(appInstanceId, !!legacyRender, strictStyleIsolation, function () { return element; }); global = window; mountSandbox = function mountSandbox() { return Promise.resolve(); }; unmountSandbox = function unmountSandbox() { return Promise.resolve(); }; if (sandbox) { sandboxInstance = createSandbox(appName, containerGetter, Boolean(singular)); // 用沙箱的代理对象作为接下来使用的全局对象 global = sandboxInstance.proxy; mountSandbox = sandboxInstance.mount; unmountSandbox = sandboxInstance.unmount; } _e = _mergeWith({}, getAddOns(global, assetPublicPath), lifeCycles, function (v1, v2) { return _concat(v1 !== null && v1 !== void 0 ? v1 : [], v2 !== null && v2 !== void 0 ? v2 : []); }), _f = _e.beforeUnmount, beforeUnmount = _f === void 0 ? [] : _f, _g = _e.afterUnmount, afterUnmount = _g === void 0 ? [] : _g, _h = _e.afterMount, afterMount = _h === void 0 ? [] : _h, _j = _e.beforeMount, beforeMount = _j === void 0 ? [] : _j, _k = _e.beforeLoad, beforeLoad = _k === void 0 ? [] : _k; return [4 /*yield*/ , execHooksChain(toArray(beforeLoad), app)]; case 5: _m.sent(); // cache the execScripts returned promise if (!appExportPromiseCaches[appName]) { appExportPromiseCaches[appName] = execScripts(global, !singular); } return [4 /*yield*/ , appExportPromiseCaches[appName]]; case 6: scriptExports = _m.sent(); if (validateExportLifecycle(scriptExports)) { // eslint-disable-next-line prefer-destructuring bootstrap = scriptExports.bootstrap; // eslint-disable-next-line prefer-destructuring mount = scriptExports.mount; // eslint-disable-next-line prefer-destructuring unmount = scriptExports.unmount; } else { if (process.env.NODE_ENV === 'development') { console.warn("[qiankun] lifecycle not found from " + appName + " entry exports, fallback to get from window['" + appName + "']"); } globalVariableExports = global[appName]; if (validateExportLifecycle(globalVariableExports)) { // eslint-disable-next-line prefer-destructuring bootstrap = globalVariableExports.bootstrap; // eslint-disable-next-line prefer-destructuring mount = globalVariableExports.mount; // eslint-disable-next-line prefer-destructuring unmount = globalVariableExports.unmount; } else { delete appExportPromiseCaches[appName]; throw new Error("[qiankun] You need to export lifecycle functions in " + appName + " entry"); } } _l = getMicroAppStateActions(appInstanceId), onGlobalStateChange = _l.onGlobalStateChange, setGlobalState = _l.setGlobalState, offGlobalStateChange = _l.offGlobalStateChange; return [2 /*return*/ , { name: appInstanceId, bootstrap: [bootstrap], mount: [function () { return __awaiter(_this, void 0, void 0, function () { return __generator(this, function (_a) { switch (_a.label) { case 0: return [4 /*yield*/ , validateSingularMode(singular, app)]; case 1: if (_a.sent() && prevAppUnmountedDeferred) { return [2 /*return*/ , prevAppUnmountedDeferred.promise]; } return [2 /*return*/ , undefined]; } }); }); }, // 添加 mount hook, 确保每次应用加载前容器 dom 结构已经设置完毕 function () { return __awaiter(_this, void 0, void 0, function () { return __generator(this, function (_a) { // element would be destroyed after unmounted, we need to recreate it if it not exist element = element || createElement(appContent, strictStyleIsolation); render({ element: element, loading: true }); return [2 /*return*/ ]; }); }); }, // exec the chain after rendering to keep the behavior with beforeLoad function () { return __awaiter(_this, void 0, void 0, function () { return __generator(this, function (_a) { return [2 /*return*/ , execHooksChain(toArray(beforeMount), app)]; }); }); }, mountSandbox, function (props) { return __awaiter(_this, void 0, void 0, function () { return __generator(this, function (_a) { return [2 /*return*/ , mount(__assign(__assign({}, props), { container: containerGetter(), setGlobalState: setGlobalState, onGlobalStateChange: onGlobalStateChange }))]; }); }); }, // 应用 mount 完成后结束 loading function () { return __awaiter(_this, void 0, void 0, function () { return __generator(this, function (_a) { return [2 /*return*/ , render({ element: element, loading: false })]; }); }); }, function () { return __awaiter(_this, void 0, void 0, function () { return __generator(this, function (_a) { return [2 /*return*/ , execHooksChain(toArray(afterMount), app)]; }); }); }, // initialize the unmount defer after app mounted and resolve the defer after it unmounted function () { return __awaiter(_this, void 0, void 0, function () { return __generator(this, function (_a) { switch (_a.label) { case 0: return [4 /*yield*/ , validateSingularMode(singular, app)]; case 1: if (_a.sent()) { prevAppUnmountedDeferred = new Deferred(); } return [2 /*return*/ ]; } }); }); }], unmount: [function () { return __awaiter(_this, void 0, void 0, function () { return __generator(this, function (_a) { return [2 /*return*/ , execHooksChain(toArray(beforeUnmount), app)]; }); }); }, function (props) { return __awaiter(_this, void 0, void 0, function () { return __generator(this, function (_a) { return [2 /*return*/ , unmount(__assign(__assign({}, props), { container: containerGetter() }))]; }); }); }, unmountSandbox, function () { return __awaiter(_this, void 0, void 0, function () { return __generator(this, function (_a) { return [2 /*return*/ , execHooksChain(toArray(afterUnmount), app)]; }); }); }, function () { return __awaiter(_this, void 0, void 0, function () { return __generator(this, function (_a) { render({ element: null, loading: false }); offGlobalStateChange(appInstanceId); // for gc element = null; return [2 /*return*/ ]; }); }); }, function () { return __awaiter(_this, void 0, void 0, function () { return __generator(this, function (_a) { switch (_a.label) { case 0: return [4 /*yield*/ , validateSingularMode(singular, app)]; case 1: if (_a.sent() && prevAppUnmountedDeferred) { prevAppUnmountedDeferred.resolve(); } return [2 /*return*/ ]; } }); }); }] }]; } }); }); }
- registerApplication是single-spa的方法,我们这里通过loadApp这个方法,对数据进行处理
function registerApplication(appNameOrConfig, appOrLoadApp, activeWhen, customProps) { const registration = sanitizeArguments(appNameOrConfig, appOrLoadApp, activeWhen, customProps); if (getAppNames().indexOf(registration.name) !== -1) throw Error(formatErrorMessage(21, `There is already an app registered with name ${registration.name}`, registration.name)); apps.push(assign({ loadErrorTime: null, status: NOT_LOADED, parcels: {}, devtools: { overlays: { options: {}, selectors: [] } } }, registration)); if (isInBrowser) { ensureJQuerySupport(); reroute(); } }
- 上面这个函数,应该是整个微前端框架最复杂的地方,它最终会返回一个函数,当成函数传递给single-spa这个库的registerApplication方法使用
- 它的内部是switch case逻辑,然后返回一个数组
- 这是一个逻辑判断
case 0: entry = app.entry, appName = app.name; _b = configuration.singular, singular = _b === void 0 ? false : _b, _c = configuration.sandbox, sandbox = _c === void 0 ? true : _c, importEntryOpts = __rest(configuration, ["singular", "sandbox"]); return [4 /*yield*/ , importEntry(entry, importEntryOpts)];
重点来了
- 会通过
importEntry
去加载entry
(子应用地址)
function importEntry(entry) { var opts = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; var _opts$fetch3 = opts.fetch, fetch = _opts$fetch3 === void 0 ? defaultFetch : _opts$fetch3, _opts$getTemplate = opts.getTemplate, getTemplate = _opts$getTemplate === void 0 ? defaultGetTemplate : _opts$getTemplate; var getPublicPath = opts.getPublicPath || opts.getDomain || _utils.defaultGetPublicPath; if (!entry) { throw new SyntaxError('entry should not be empty!'); } // html entry if (typeof entry === 'string') { return importHTML(entry, { fetch: fetch, getPublicPath: getPublicPath, getTemplate: getTemplate }); } // config entry if (Array.isArray(entry.scripts) || Array.isArray(entry.styles)) { var _entry$scripts = entry.scripts, scripts = _entry$scripts === void 0 ? [] : _entry$scripts, _entry$styles = entry.styles, styles = _entry$styles === void 0 ? [] : _entry$styles, _entry$html = entry.html, html = _entry$html === void 0 ? '' : _entry$html; return getEmbedHTML(getTemplate(html), styles, { fetch: fetch }).then(function (embedHTML) { return { template: embedHTML, assetPublicPath: getPublicPath('/'), getExternalScripts: function getExternalScripts() { return _getExternalScripts(scripts, fetch); }, getExternalStyleSheets: function getExternalStyleSheets() { return _getExternalStyleSheets(styles, fetch); }, execScripts: function execScripts(proxy, strictGlobal) { if (!scripts.length) { return Promise.resolve(); } return _execScripts(scripts[scripts.length - 1], scripts, proxy, { fetch: fetch, strictGlobal: strictGlobal }); } }; }); } else { throw new SyntaxError('entry scripts or styles should be array!'); } }
- 上面代码里最重要的,如果我们entry传入字符串,那么就会使用这个函数去加载HTML内容(其实微前端的所有子应用加载,都是把dom节点加载渲染到基座的index.html文件中的一个div标签内)
if (typeof entry === 'string') { return importHTML(entry, { fetch: fetch, getPublicPath: getPublicPath, getTemplate: getTemplate }); } // config entry
- importHTML这个函数,就是我们今晚最重要的一个点
- 传入url地址,发起fetch请求(此时由于域名或者端口不一样,会出现跨域,所有子应用的热更新开发模式下,webpack配置要做以下处理,部署也要考虑这个问题)
devServer: { contentBase: path.resolve(__dirname, '../'), port: CONFIG.serverPort, host: 'rental-dev.mysoft.com.cn', historyApiFallback: true, disableHostCheck: true, headers: { 'Access-Control-Allow-Origin': '*', }, stats: { timings: true, assets: false, entrypoints: false, modules: false }, proxy: { '/api/*': { target: CONFIG.proxyServer, changeOrigin: true, secure: false, pathRewrite: { '^/api': '' }, headers: { // Cookie: 'RENTALCENTER=5fc10ee067fa5d15a9a7840bd4a75dc98dc7f47a' } } }, before(app) { app.get('/cookie/set', (req, res) => { const cookies = req.query console.log('resolve cookie') for (const cookie in cookies) { if (Object.prototype.hasOwnProperty.call(cookies, cookie)) { res.cookie(cookie, cookies[cookie], { httpOnly: true }) } } res.redirect( `${req.protocol}://${req.host}:${CONFIG.serverPort}${ CONFIG.baseAlias }` ) }) }, inline: true, hot: true // 新增 },
整个importHTML函数好像很长很长,但是我们就看最重要的地方,一个框架(库),流程线很长+版本迭代原因,需要兼容老的版本,所以很多源码对于我们其实是无用的
function importHTML(url) { var opts = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; var fetch = defaultFetch; var getPublicPath = _utils.defaultGetPublicPath; var getTemplate = defaultGetTemplate; // compatible with the legacy importHTML api if (typeof opts === 'function') { fetch = opts; } else { fetch = opts.fetch || defaultFetch; getPublicPath = opts.getPublicPath || opts.getDomain || _utils.defaultGetPublicPath; getTemplate = opts.getTemplate || defaultGetTemplate; } return embedHTMLCache[url] || (embedHTMLCache[url] = fetch(url).then(function (response) { return response.text(); }).then(function (html) { var assetPublicPath = getPublicPath(url); var _processTpl = (0, _processTpl2["default"])(getTemplate(html), assetPublicPath), template = _processTpl.template, scripts = _processTpl.scripts, entry = _processTpl.entry, styles = _processTpl.styles; return getEmbedHTML(template, styles, { fetch: fetch }).then(function (embedHTML) { return { template: embedHTML, assetPublicPath: assetPublicPath, getExternalScripts: function getExternalScripts() { return _getExternalScripts(scripts, fetch); }, getExternalStyleSheets: function getExternalStyleSheets() { return _getExternalStyleSheets(styles, fetch); }, execScripts: function execScripts(proxy, strictGlobal) { if (!scripts.length) { return Promise.resolve(); } return _execScripts(entry, scripts, proxy, { fetch: fetch, strictGlobal: strictGlobal }); } }; }); })); }
- 整个函数,最后返回了一个对象,这里很明显,通过fetch请求,获取了对应子应用entry入口的资源文件后,转换成了字符串
- 这里processTpl其实就是对这个子应用的dom模版(字符串格式)进行一个数据拼装,其实也不是很复杂,由于时间关系,可以自己看看过程,重点看结果
- 这里的思想,是redux的中间件源码思想,将数据进行了一层包装,高可用使用
function processTpl(tpl, baseURI) { var scripts = []; var styles = []; var entry = null; var template = tpl /* remove html comment first */ .replace(HTML_COMMENT_REGEX, '').replace(LINK_TAG_REGEX, function (match) { /* change the css link */ var styleType = !!match.match(STYLE_TYPE_REGEX); if (styleType) { var styleHref = match.match(STYLE_HREF_REGEX); var styleIgnore = match.match(LINK_IGNORE_REGEX); if (styleHref) { var href = styleHref && styleHref[2]; var newHref = href; if (href && !hasProtocol(href)) { newHref = getEntirePath(href, baseURI); } if (styleIgnore) { return genIgnoreAssetReplaceSymbol(newHref); } styles.push(newHref); return genLinkReplaceSymbol(newHref); } } var preloadOrPrefetchType = match.match(LINK_PRELOAD_OR_PREFETCH_REGEX) && match.match(LINK_HREF_REGEX); if (preloadOrPrefetchType) { var _match$match = match.match(LINK_HREF_REGEX), _match$match2 = (0, _slicedToArray2["default"])(_match$match, 3), linkHref = _match$match2[2]; return genLinkReplaceSymbol(linkHref, true); } return match; }).replace(STYLE_TAG_REGEX, function (match) { if (STYLE_IGNORE_REGEX.test(match)) { return genIgnoreAssetReplaceSymbol('style file'); } return match; }).replace(ALL_SCRIPT_REGEX, function (match) { var scriptIgnore = match.match(SCRIPT_IGNORE_REGEX); // in order to keep the exec order of all javascripts // if it is a external script if (SCRIPT_TAG_REGEX.test(match) && match.match(SCRIPT_SRC_REGEX)) { /* collect scripts and replace the ref */ var matchedScriptEntry = match.match(SCRIPT_ENTRY_REGEX); var matchedScriptSrcMatch = match.match(SCRIPT_SRC_REGEX); var matchedScriptSrc = matchedScriptSrcMatch && matchedScriptSrcMatch[2]; if (entry && matchedScriptEntry) { throw new SyntaxError('You should not set multiply entry script!'); } else { // append the domain while the script not have an protocol prefix if (matchedScriptSrc && !hasProtocol(matchedScriptSrc)) { matchedScriptSrc = getEntirePath(matchedScriptSrc, baseURI); } entry = entry || matchedScriptEntry && matchedScriptSrc; } if (scriptIgnore) { return genIgnoreAssetReplaceSymbol(matchedScriptSrc || 'js file'); } if (matchedScriptSrc) { var asyncScript = !!match.match(SCRIPT_ASYNC_REGEX); scripts.push(asyncScript ? { async: true, src: matchedScriptSrc } : matchedScriptSrc); return genScriptReplaceSymbol(matchedScriptSrc, asyncScript); } return match; } else { if (scriptIgnore) { return genIgnoreAssetReplaceSymbol('js file'); } // if it is an inline script var code = (0, _utils.getInlineCode)(match); // remove script blocks when all of these lines are comments. var isPureCommentBlock = code.split(/[\r\n]+/).every(function (line) { return !line.trim() || line.trim().startsWith('//'); }); if (!isPureCommentBlock) { scripts.push(match); } return inlineScriptReplaceSymbol; } }); scripts = scripts.filter(function (script) { // filter empty script return !!script; }); return { template: template, scripts: scripts, styles: styles, // set the last script as entry if have not set entry: entry || scripts[scripts.length - 1] }; }
- 最终返回了一个对象,此时已经不是一个纯html的字符串了,而是一个对象,而且脚本样式都分离了
return { template: template, scripts: scripts, styles: styles, // set the last script as entry if have not set entry: entry || scripts[scripts.length - 1] };
- 这个是框架帮我们处理的,必须要设置一个入口js文件
// set the last script as entry if have not set
- 下面是真正的single-spa源码,注册子应用,用apps这个数组去收集所有的子应用(数组每一项已经拥有了脚本、html、css样式的内容)
此时我们只要根据我们之前编写的activeRule和监听前端路由变化去控制展示子应用即可,原理如下:(今天不做过多讲解这块)
window.addEventListener('hashchange', reroute); window.addEventListener('popstate', reroute); // 拦截所有注册的事件,以便确保这里的事件总是第一个执行 const originalAddEventListener = window.addEventListener; const originalRemoveEventListener = window.removeEventListener; window.addEventListener = function (eventName, handler, args) { if (eventName && HIJACK_EVENTS_NAME.test(eventName) && typeof handler === 'function') { EVENTS_POOL[eventName].indexOf(handler) === -1 && EVENTS_POOL[eventName].push(handler); } return originalAddEventListener.apply(this, arguments); }; window.removeEventListener = function (eventName, handler) { if (eventName && HIJACK_EVENTS_NAME.test(eventName) && typeof handler === 'function') { let eventList = EVENTS_POOL[eventName]; eventList.indexOf(handler) > -1 && (EVENTS_POOL[eventName] = eventList.filter(fn => fn !== handler)); } return originalRemoveEventListener.apply(this, arguments); };
也是redux的中间件思想,劫持了事件,然后进行派发,优先调用微前端框架的路由事件,然后进行过滤展示子应用:
export function getAppsToLoad() { return APPS.filter(notSkipped).filter(withoutLoadError).filter(isntLoaded).filter(shouldBeActive); }
整个微前端的触发流程图
相信通过此文,你能真正了解微前端的使用原理, 后期我会出一个手写微前端框架的文章
最后
点个赞支持我吧,转发就更好了
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- 精读《如何编译前端项目与组件》
- 精读《React 性能调试》
- 精读《Typescript 4》
- 精读《正则 ES2018》
- AIStats 2017文章精读(四)
- 精读《如何比较 Object 对象》
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。