├── .gitignore ├── LICENSE ├── README.md ├── package.json └── src ├── const ├── mainLifeCycle.js ├── microApps.js └── subApps.js ├── customevent └── index.js ├── index.js ├── lifeCycle └── index.js ├── loader ├── index.js └── prefetch.js ├── router ├── rewriteRouter.js └── routerHandle.js ├── sandbox ├── index.js ├── performScript.js ├── proxySandbox.js └── snapShotSandbox.js ├── start.js ├── store └── index.js └── utils ├── fetchResource.js └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Directory for instrumented libs generated by jscoverage/JSCover 10 | lib-cov 11 | 12 | # Coverage directory used by tools like istanbul 13 | coverage 14 | *.lcov 15 | 16 | # Dependency directories 17 | node_modules/ 18 | 19 | # parcel-bundler cache (https://parceljs.org/) 20 | .cache 21 | 22 | # ide 23 | .vscode 24 | .idea 25 | 26 | # lock 27 | package-lock.json 28 | yarn.lock -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Lazy Engineer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # anyjs 2 | 3 | **anyjs** 是一个微前端实现框架,帮助大家能更简单、无痛的构建一个生产可用的微前端架构系统。 4 | 5 | 微前端架构的核心价值: 6 | - 技术栈无关 7 | - 主框架不限制接入应用的技术栈,子应用具备完全自主权 8 | 9 | - 独立开发部署 10 | - 子应用仓库独立,前后端开发独立,部署独立 11 | 12 | - 增量升级 13 | - 在面对各种复杂场景时,我们通常很难对一个已经存在的系统做全量的技术栈升级或重构,而微前端是一种非常好的实施渐进式重构的手段和策略 14 | 15 | - 独立运行时 16 | - 每个子应用之间状态隔离,运行时状态不共享 17 | 18 | 19 | #### 为什么不用iframe? 20 | 如果不考虑体验问题,iframe 几乎是最完美的微前端解决方案了。 21 | 22 | iframe 最大的特性就是提供了浏览器原生的硬隔离方案,不论是样式隔离、js 隔离这类问题统统都能被完美解决。但它的最大问题也在于隔离性无法被突破,导致应用间上下文无法被共享,随之带来的开发体验、产品体验的问题。 23 | 24 | iframe存在的问题 25 | 26 | - URL不同步。浏览器刷新 iframe url 状态丢失、后退前进按钮无法使用。 27 | - UI不同步,DOM 结构不共享。想象一下屏幕右下角 1/4 的 iframe 里来一个带遮罩层的弹框,同时我们要求这个弹框要浏览器居中显示,还要浏览器 resize 时自动居中... 28 | - 全局上下文完全隔离,内存变量不共享。iframe 内外系统的通信、数据同步等需求,主应用的 cookie 要透传到根域名都不同的子应用中实现免登效果。 29 | - 慢。每次子应用进入都是一次浏览器上下文重建、资源重新加载的过程。 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "anyjs", 3 | "version": "1.0.0", 4 | "description": "micro project", 5 | "main": "index.js", 6 | "scripts": { 7 | 8 | }, 9 | "author": "lazyperson", 10 | "license": "MIT", 11 | "devDependencies": {}, 12 | "dependencies": {} 13 | } 14 | -------------------------------------------------------------------------------- /src/const/mainLifeCycle.js: -------------------------------------------------------------------------------- 1 | let lifecycle = {}; 2 | 3 | export const getMainLifecycle = () => lifecycle; 4 | 5 | export const setMainLifecycle = data => lifecycle = data; 6 | -------------------------------------------------------------------------------- /src/const/microApps.js: -------------------------------------------------------------------------------- 1 | let microAppList = []; 2 | 3 | export const getMicroAppList = () => microAppList; 4 | 5 | export const setMicroAppList = appList => microAppList = appList; 6 | -------------------------------------------------------------------------------- /src/const/subApps.js: -------------------------------------------------------------------------------- 1 | let list = []; 2 | 3 | export const getList = () => list; 4 | 5 | export const setList = appList => list = appList; 6 | -------------------------------------------------------------------------------- /src/customevent/index.js: -------------------------------------------------------------------------------- 1 | export class Custom { 2 | 3 | on (name, cb) { 4 | window.addEventListener(name, (e) => { 5 | cb(e.detail); 6 | }); 7 | } 8 | 9 | emit(name, data) { 10 | const event = new CustomEvent(name, { 11 | detail: data 12 | }); 13 | window.dispatchEvent(event); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export { registerMicroApps, start } from './start'; 2 | export { createStore } from './store'; 3 | -------------------------------------------------------------------------------- /src/lifeCycle/index.js: -------------------------------------------------------------------------------- 1 | import { findAppByRoute } from '../utils'; 2 | import { getMainLifecycle } from '../const/mainLifeCycle'; 3 | import { loadHtml } from '../loader'; 4 | 5 | export const lifecycle = async () => { 6 | 7 | const prevApp = findAppByRoute(window.__ORIGIN_APP__); 8 | 9 | // 获取到要跳转到的子应用 10 | const nextApp = findAppByRoute(window.__CURRENT_SUB_APP__); 11 | 12 | if (!nextApp) { 13 | return; 14 | } 15 | 16 | if (prevApp && prevApp.unmount) { 17 | if (prevApp.proxy) { 18 | prevApp.proxy.inactive(); 19 | } 20 | await destoryed(prevApp); 21 | } 22 | 23 | const app = await beforeLoad(nextApp); 24 | 25 | await mounted(app); 26 | } 27 | 28 | export const beforeLoad = async (app) => { 29 | await runMainLifeCycle('beforeLoad'); 30 | 31 | app && app.beforeLoad && app.beforeLoad(); 32 | 33 | const subApp = await loadHtml(app); 34 | subApp && subApp.beforeLoad && subApp.beforeLoad(); 35 | 36 | return subApp; 37 | } 38 | 39 | export const mounted = async (app) => { 40 | app && app.mount && app.mount({ 41 | appInfo: app.appInfo, 42 | entry: app.entry 43 | }); 44 | 45 | await runMainLifeCycle('mounted'); 46 | } 47 | 48 | export const destoryed = async (app) => { 49 | app && app.unmount && app.unmount(); 50 | 51 | await runMainLifeCycle('destoryed'); 52 | } 53 | 54 | export const runMainLifeCycle = async (type) => { 55 | const mainlife = getMainLifecycle(); 56 | 57 | await Promise.all(mainlife[type].map(async item => await item())); 58 | } 59 | -------------------------------------------------------------------------------- /src/loader/index.js: -------------------------------------------------------------------------------- 1 | import { fetchResource } from '../utils/fetchResource'; 2 | import { sandBox } from "../sandbox"; 3 | 4 | export const loadHtml = async (app) => { 5 | // 第一个,子应用需要显示在哪里 6 | let container = app.container; // #id 内容 7 | 8 | let entry = app.entry; 9 | 10 | const [ dom, scripts ] = await parseHtml(entry, app.name); 11 | 12 | const ct = document.querySelector(container); 13 | 14 | if (!ct) { 15 | throw new Error('anyjs:容器不存在,请查看'); 16 | } 17 | 18 | ct.innerHTML = dom; 19 | 20 | scripts.forEach(item => { 21 | sandBox(app, item); 22 | }); 23 | 24 | return app; 25 | } 26 | 27 | const cache = {}; 28 | 29 | export const parseHtml = async (entry, name) => { 30 | if (cache[name]) { 31 | return cache[name]; 32 | } 33 | const html = await fetchResource(entry); 34 | 35 | let allScript = []; 36 | const div = document.createElement('div'); 37 | div.innerHTML = html; 38 | 39 | const [dom, scriptUrl, script] = await getResources(div, entry); 40 | 41 | const fetchedScripts = await Promise.all(scriptUrl.map(async item => fetchResource(item))); 42 | 43 | allScript = script.concat(fetchedScripts); 44 | 45 | cache[name] = [dom, allScript]; 46 | 47 | return [dom, allScript]; 48 | } 49 | 50 | export const getResources = async (root, entry) => { 51 | // js 链接 src href 52 | const scriptUrl = []; 53 | // 写在script中的js脚本内容 54 | const script = []; 55 | const dom = root.outerHTML; 56 | 57 | function deepParse(element) { 58 | const children = element.children 59 | const parent = element.parent; 60 | 61 | if (element.nodeName.toLowerCase() === 'script') { 62 | const src = element.getAttribute('src'); 63 | if (!src) { 64 | script.push(element.outerHTML); 65 | } else { 66 | if (src.startsWith('http')) { 67 | scriptUrl.push(src); 68 | } else { 69 | scriptUrl.push(`http:${entry}/${src}`); 70 | } 71 | } 72 | 73 | if (parent) { 74 | parent.replaceChild(document.createComment('此 js 文件已经被微前端替换'), element); 75 | } 76 | } 77 | 78 | // link 也会有js的内容 79 | if (element.nodeName.toLowerCase() === 'link') { 80 | const href = element.getAttribute('href'); 81 | 82 | if (href.endsWith('.js')) { 83 | if (href.startsWith('http')) { 84 | scriptUrl.push(href); 85 | } else { 86 | scriptUrl.push(`http:${entry}/${href}`); 87 | } 88 | } 89 | } 90 | 91 | for (let i = 0; i < children.length; i++) { 92 | deepParse(children[i]); 93 | } 94 | } 95 | 96 | deepParse(root); 97 | 98 | return [dom, scriptUrl, script]; 99 | } 100 | -------------------------------------------------------------------------------- /src/loader/prefetch.js: -------------------------------------------------------------------------------- 1 | import { getList } from '../const/subApps'; 2 | import { parseHtml } from './index'; 3 | 4 | export const prefetch = async () => { 5 | const list = getList().filter(item => !window.location.pathname.startsWith(item.activeRule)); 6 | 7 | // 预加载剩下的所有子应用 8 | await Promise.all(list.map(async item => await parseHtml(item.entry, item.name))); 9 | } 10 | -------------------------------------------------------------------------------- /src/router/rewriteRouter.js: -------------------------------------------------------------------------------- 1 | import { patchRouter } from '../utils'; 2 | import { turnApp } from './routerHandle'; 3 | 4 | // 重写window的路由跳转 5 | export const rewriteRouter = () => { 6 | window.history.pushState = patchRouter(window.history.pushState, 'micro_push'); 7 | window.history.replaceState = patchRouter(window.history.replaceState, 'micro_replace'); 8 | 9 | window.addEventListener('micro_push', turnApp); 10 | window.addEventListener('micro_replace', turnApp); 11 | 12 | // 监听返回事件 13 | window.onpopstate = async function () { 14 | await turnApp(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/router/routerHandle.js: -------------------------------------------------------------------------------- 1 | import { isTurnChild } from '../utils'; 2 | import { lifecycle } from '../lifeCycle'; 3 | 4 | export const turnApp = async () => { 5 | if (isTurnChild()) { 6 | await lifecycle(); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/sandbox/index.js: -------------------------------------------------------------------------------- 1 | import { performScriptForEval } from './performScript'; 2 | // import { SnapShotSandbox } from './snapShotSandbox'; 3 | import { ProxySandbox } from './proxySandbox'; 4 | 5 | const isCheckLifeCycle = lifecycle => lifecycle && 6 | lifecycle.bootstrap && 7 | lifecycle.mount && 8 | lifecycle.unmount; 9 | 10 | export const sandBox = (app, script) => { 11 | 12 | const proxy = new ProxySandbox(); 13 | 14 | if (!app.proxy) { 15 | app.proxy = proxy; 16 | } 17 | 18 | window.__MICRO_WEB__ = true; 19 | 20 | const lifecycle = performScriptForEval(script, app.name, app.proxy.proxy); 21 | 22 | if (isCheckLifeCycle(lifecycle)) { 23 | app.bootstrap = lifecycle.bootstrap; 24 | app.mount = lifecycle.mount; 25 | app.unmount = lifecycle.unmount; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/sandbox/performScript.js: -------------------------------------------------------------------------------- 1 | 2 | // 执行js脚本 3 | export const performScriptForFunction = (script, appName, global) => { 4 | window.proxy = global; 5 | const scriptText = ` 6 | return ((window) => { 7 | ${script} 8 | return window['${appName}'] 9 | })(window.proxy) 10 | `; 11 | return new Function(scriptText)(); 12 | } 13 | 14 | export const performScriptForEval = (script, appName, global) => { 15 | // library window.appName 16 | window.proxy = global; 17 | const scriptText = ` 18 | ((window) => { 19 | ${script} 20 | return window['${appName}'] 21 | })(window.proxy) 22 | `; 23 | return eval(scriptText); 24 | } 25 | -------------------------------------------------------------------------------- /src/sandbox/proxySandbox.js: -------------------------------------------------------------------------------- 1 | let defaultValue = {}; 2 | 3 | export class ProxySandbox{ 4 | constructor() { 5 | this.proxy = null; 6 | 7 | this.active(); 8 | } 9 | 10 | active() { 11 | 12 | this.proxy = new Proxy(window, { 13 | get(target, key) { 14 | if (typeof target[key] === 'function') { 15 | return target[key].bind(target); 16 | } 17 | return defaultValue[key] || target[key]; 18 | }, 19 | set(target, key, value) { 20 | defaultValue[key] = value; 21 | return true; 22 | } 23 | }) 24 | 25 | } 26 | 27 | inactive () { 28 | defaultValue = {}; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/sandbox/snapShotSandbox.js: -------------------------------------------------------------------------------- 1 | export class SnapShotSandbox { 2 | constructor() { 3 | 4 | this.proxy = window; 5 | 6 | this.active(); 7 | } 8 | 9 | active() { 10 | 11 | this.snapshot = new Map(); 12 | 13 | for(const key in window) { 14 | this.snapshot[key] = window[key]; 15 | } 16 | } 17 | 18 | inactive () { 19 | for (const key in window) { 20 | if (window[key] !== this.snapshot[key]) { 21 | window[key] = this.snapshot[key]; 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/start.js: -------------------------------------------------------------------------------- 1 | import { setList, getList } from './const/subApps'; 2 | import { currentApp } from './utils'; 3 | import { rewriteRouter } from './router/rewriteRouter'; 4 | import { setMainLifecycle } from './const/mainLifeCycle'; 5 | import { prefetch } from './loader/prefetch'; 6 | import { Custom } from './customevent'; 7 | 8 | const custom = new Custom(); 9 | custom.on('test', (data) => { 10 | console.log(data); 11 | }) 12 | 13 | window.custom = custom; 14 | 15 | rewriteRouter(); 16 | 17 | export const registerMicroApps = (appList, lifeCycle) => { 18 | setList(appList); 19 | 20 | setMainLifecycle(lifeCycle); 21 | } 22 | 23 | export const start = () => { 24 | 25 | const apps = getList(); 26 | 27 | if (!apps.length) { 28 | throw Error('anyjs:子应用列表为空, 请正确注册应用'); 29 | } 30 | 31 | const app = currentApp(); 32 | 33 | const { pathname, hash } = window.location; 34 | 35 | if (!hash) { 36 | // 当前没有在使用的子应用, 抛出错误 或者 访问默认路由 37 | return; 38 | } 39 | 40 | if (app && hash) { 41 | const url = pathname + hash; 42 | 43 | window.__CURRENT_SUB_APP__ = app.activeRule; 44 | 45 | window.history.pushState('', '', url); 46 | } 47 | 48 | prefetch(); 49 | } 50 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | export const createStore = (initData = {}) => (() => { 2 | let store = initData; 3 | const observers = []; 4 | 5 | const getStore = () => store; 6 | 7 | const update = (value) => { 8 | if (value !== store) { 9 | const oldValue = store; 10 | store = value; 11 | observers.forEach(async item => await item(store, oldValue)); 12 | } 13 | } 14 | 15 | const subscribe = (fn) => { 16 | observers.push(fn); 17 | } 18 | 19 | return { 20 | getStore, 21 | update, 22 | subscribe, 23 | }; 24 | 25 | })(); 26 | -------------------------------------------------------------------------------- /src/utils/fetchResource.js: -------------------------------------------------------------------------------- 1 | export const fetchResource = url => fetch(url).then(async res => await res.text()); 2 | -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | import { getList } from '../const/subApps'; 2 | 3 | export const patchRouter = (globalEvent, eventName) => { 4 | return function () { 5 | const e = new Event(eventName); 6 | globalEvent.apply(this, arguments); 7 | window.dispatchEvent(e); 8 | } 9 | } 10 | 11 | export const currentApp = () => { 12 | const currentUrl = window.location.pathname; 13 | return filterApp('activeRule', currentUrl); 14 | } 15 | 16 | export const findAppByRoute = (router) => { 17 | return filterApp('activeRule', router); 18 | } 19 | 20 | export const filterApp = (key, value) => { 21 | const currentApp = getList().filter(item => item[key] === value); 22 | return currentApp && currentApp.length ? currentApp[0] : {}; 23 | } 24 | 25 | export const isTurnChild = () => { 26 | const { pathname, hash } = window.location; 27 | const url = pathname + hash; 28 | 29 | // 当前路由无改变 30 | const currentPrefix = url.match(/(\/\w+)/g); 31 | 32 | if ( 33 | currentPrefix && 34 | (currentPrefix[0] === window.__CURRENT_SUB_APP__) && 35 | hash === window.__CURRENT_HASH__ 36 | ) { 37 | return false; 38 | } 39 | 40 | window.__ORIGIN_APP__ = window.__CURRENT_SUB_APP__; 41 | 42 | const currentSubApp = window.location.pathname.match(/(\/\w+)/); 43 | 44 | if (!currentSubApp) { 45 | return false; 46 | } 47 | 48 | window.__CURRENT_SUB_APP__ = currentSubApp[0]; 49 | 50 | window.__CURRENT_HASH__ = hash; 51 | 52 | return true; 53 | } 54 | --------------------------------------------------------------------------------