├── example ├── subapp2 │ ├── index.css │ ├── index.js │ └── index.html ├── subapp1 │ ├── index.js │ ├── test.css │ └── index.html └── server │ ├── subapp2.js │ └── subapp1.js ├── .babelrc ├── .gitignore ├── index.js ├── ReadMe.md ├── index.html ├── package.json └── src ├── route.js └── index.js /example/subapp2/index.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example/subapp1/index.js: -------------------------------------------------------------------------------- 1 | console.log('subapp1 index.js loaded'); 2 | -------------------------------------------------------------------------------- /example/subapp1/test.css: -------------------------------------------------------------------------------- 1 | body{ 2 | /* font-size: 50px; */ 3 | } -------------------------------------------------------------------------------- /example/subapp2/index.js: -------------------------------------------------------------------------------- 1 | console.log('subapp2 index.js loaded'); 2 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "@babel/plugin-transform-runtime" 4 | ] 5 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | .parcel-cache 4 | .cache 5 | dist 6 | yarn-error.log -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import { registryApp, start } from './src/index'; 2 | registryApp('http://localhost:8889', (location) => location.pathname === '/subapp1'); 3 | registryApp('http://localhost:8890', (location) => location.pathname === '/subapp2'); 4 | start(); 5 | -------------------------------------------------------------------------------- /example/subapp2/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | subapp2 8 | 9 | 10 | 11 |
subapp2
12 | 13 | 16 | -------------------------------------------------------------------------------- /ReadMe.md: -------------------------------------------------------------------------------- 1 | #### 欢迎来到chunchao微前端框架 2 | 3 | * 乞丐版,仅仅能跑 4 | 5 | #### 启用流程 6 | 7 | * git clone https://github.com/JinJieTan/chunchao.git 8 | 9 | * cd chunchao 10 | 11 | * yarn global add parcel-bundler 12 | 13 | * yarn 14 | 15 | * yarn startSubApp1 16 | 17 | * yarn startSubApp2 18 | 19 | * yarn start-main 20 | 21 | * 访问浏览器:http://localhost:1234/ 22 | 23 | > 恭喜你已经开启了chunchao的第一步了 24 | 25 | #### 接下来会做什么 26 | 27 | * 完善微前端功能 28 | 29 | * 支持IE11 30 | 31 | * 持续更新... 32 | 33 | 欢迎加我的微信:CALASFxiaotan 34 | 35 | 记得备注:微前端进群交流 36 | -------------------------------------------------------------------------------- /example/subapp1/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | subapp1 8 | 13 | 14 | 15 | 16 | 17 |
subapp1
18 | 19 | 20 | 23 | 24 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 | 11 |
12 |

基座

13 |
14 |
15 | 子应用1 16 |
17 |
18 | 子应用2 19 |
20 |
21 |
22 |
23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /example/server/subapp2.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const app = express(); 3 | const { resolve } = require('path'); 4 | //设置跨域访问 5 | app.all('*', function (req, res, next) { 6 | res.header('Access-Control-Allow-Origin', '*'); 7 | res.header('Access-Control-Allow-Headers', 'X-Requested-With'); 8 | res.header('Access-Control-Allow-Methods', 'PUT,POST,GET,DELETE,OPTIONS'); 9 | res.header('X-Powered-By', ' 3.2.1'); 10 | res.header('Content-Type', 'application/json;charset=utf-8'); 11 | next(); 12 | }); 13 | app.use(express.static(resolve(__dirname, '../subapp2'))); 14 | 15 | app.listen(8890, (err) => { 16 | !err && console.log('8890端口成功'); 17 | }); 18 | -------------------------------------------------------------------------------- /example/server/subapp1.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const app = express(); 3 | const { resolve } = require('path'); 4 | 5 | 6 | //设置跨域访问 7 | app.all('*', function (req, res, next) { 8 | res.header('Access-Control-Allow-Origin', '*'); 9 | res.header('Access-Control-Allow-Headers', 'X-Requested-With'); 10 | res.header('Access-Control-Allow-Methods', 'PUT,POST,GET,DELETE,OPTIONS'); 11 | res.header('X-Powered-By', ' 3.2.1'); 12 | res.header('Content-Type', 'application/json;charset=utf-8'); 13 | next(); 14 | }); 15 | app.use(express.static(resolve(__dirname, '../subapp1'))); 16 | 17 | app.listen(8889, (err) => { 18 | !err && console.log('8889端口成功'); 19 | }); 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chunchao", 3 | "version": "1.0.1", 4 | "description": "微前端框架", 5 | "main": "src/index.js", 6 | "repository": "https://github.com/JinJieTan", 7 | "author": "Peter 谭金杰", 8 | "license": "MIT", 9 | "dependencies": { 10 | "express": "^4.17.1", 11 | "parcel": "^2.0.0-alpha.3.2", 12 | "react": "^16.13.1", 13 | "react-dom": "^16.13.1" 14 | }, 15 | "scripts": { 16 | "startSubApp1": "node ./example/server/subapp1.js", 17 | "startSubApp2": "node ./example/server/subapp2.js", 18 | "start-main": "parcel index.html" 19 | }, 20 | "devDependencies": { 21 | "@babel/core": "^7.10.2", 22 | "@babel/plugin-transform-runtime": "^7.10.1", 23 | "chunchao": "1.0.1" 24 | }, 25 | "files": [ 26 | "src" 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /src/route.js: -------------------------------------------------------------------------------- 1 | import { loadApp } from "./index"; 2 | const HIJACK_EVENTS_NAME = /^(hashchange|popstate)$/i; 3 | const EVENTS_POOL = { 4 | hashchange: [], 5 | popstate: [], 6 | }; 7 | 8 | function reroute() { 9 | console.log('reroute') 10 | loadApp(); 11 | } 12 | 13 | window.addEventListener("hashchange", reroute); 14 | window.addEventListener("popstate", reroute); 15 | 16 | const originalAddEventListener = window.addEventListener; 17 | const originalRemoveEventListener = window.removeEventListener; 18 | window.addEventListener = function (eventName, handler) { 19 | if ( 20 | eventName && 21 | HIJACK_EVENTS_NAME.test(eventName) && 22 | typeof handler === "function" 23 | ) { 24 | EVENTS_POOL[eventName].indexOf(handler) === -1 && 25 | EVENTS_POOL[eventName].push(handler); 26 | } 27 | return originalAddEventListener.apply(this, arguments); 28 | }; 29 | window.removeEventListener = function (eventName, handler) { 30 | if (eventName && HIJACK_EVENTS_NAME.test(eventName)) { 31 | let eventsList = EVENTS_POOL[eventName]; 32 | eventsList.indexOf(handler) > -1 && 33 | (EVENTS_POOL[eventName] = eventsList.filter((fn) => fn !== handler)); 34 | } 35 | return originalRemoveEventListener.apply(this, arguments); 36 | }; 37 | 38 | function mockPopStateEvent(state) { 39 | return new PopStateEvent("popstate", { state }); 40 | } 41 | 42 | // 拦截history的方法,因为pushState和replaceState方法并不会触发onpopstate事件,所以我们即便在onpopstate时执行了reroute方法,也要在这里执行下reroute方法。 43 | const originalPushState = window.history.pushState; 44 | const originalReplaceState = window.history.replaceState; 45 | window.history.pushState = function (state, title, url) { 46 | let result = originalPushState.apply(this, arguments); 47 | reroute(mockPopStateEvent(state)); 48 | return result; 49 | }; 50 | window.history.replaceState = function (state, title, url) { 51 | let result = originalReplaceState.apply(this, arguments); 52 | reroute(mockPopStateEvent(state)); 53 | return result; 54 | }; 55 | 56 | // 再执行完load、mount、unmout操作后,执行此函数,就可以保证微前端的逻辑总是第一个执行。然后App中的Vue或React相关Router就可以收到Location的事件了。 57 | export function callCapturedEvents(eventArgs) { 58 | if (!eventArgs) { 59 | return; 60 | } 61 | if (!Array.isArray(eventArgs)) { 62 | eventArgs = [eventArgs]; 63 | } 64 | let name = eventArgs[0].type; 65 | if (!HIJACK_EVENTS_NAME.test(name)) { 66 | return; 67 | } 68 | EVENTS_POOL[name].forEach((handler) => handler.apply(window, eventArgs)); 69 | } 70 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import './route' 2 | /** 3 | * 4 | * @param {string} entry 5 | * @param {function} activeRule 6 | */ 7 | const Apps = []; //子应用队列 8 | 9 | export function registryApp(entry, activeRule) { 10 | Apps.push({ 11 | entry: entry, 12 | activeRule: activeRule, 13 | }); 14 | } 15 | 16 | async function loadApp() { 17 | const shouldMountApp = Apps.filter(shouldBeActive); 18 | const App = shouldMountApp.pop(); 19 | fetch(App.entry) 20 | .then(function (response) { 21 | return response.text(); 22 | }) 23 | .then(async function (text) { 24 | const dom = document.createElement('div'); 25 | dom.innerHTML = text; 26 | const entryPath = App.entry; 27 | const subapp = document.querySelector('#subApp-content'); 28 | subapp.appendChild(dom); 29 | handleScripts(entryPath, subapp, dom); 30 | handleStyles(entryPath, subapp, dom); 31 | }); 32 | } 33 | 34 | function shouldBeActive(app) { 35 | return app.activeRule(window.location); 36 | } 37 | 38 | export function start() { 39 | loadApp(); 40 | } 41 | 42 | async function handleScripts(entryPath, subapp, dom) { 43 | const scripts = dom.querySelectorAll('script'); 44 | const paromiseArr = 45 | scripts && 46 | Array.from(scripts).map((item) => { 47 | if (item.src) { 48 | const url = window.location.protocol + '//' + window.location.host; 49 | const path = item.src.replace(url, ''); 50 | const needEntryPath = !/(http|https):\/\/([\w.]+\/?)\S*/.test(path); 51 | return fetch(`${needEntryPath ? entryPath : ''}${item.src}`.replace(url, '')).then( 52 | function (response) { 53 | return response.text(); 54 | } 55 | ); 56 | } else { 57 | return Promise.resolve(item.textContent); 58 | } 59 | }); 60 | const res = await Promise.all(paromiseArr); 61 | if (res && res.length > 0) { 62 | res.forEach((item) => { 63 | const script = document.createElement('script'); 64 | script.innerText = item; 65 | subapp.appendChild(script); 66 | }); 67 | } 68 | } 69 | 70 | export async function handleStyles(entryPath, subapp, dom) { 71 | const arr = []; 72 | const styles = dom.querySelectorAll('style'); 73 | const links = Array.from(dom.querySelectorAll('link')).filter( 74 | (item) => item.rel === 'stylesheet' 75 | ); 76 | const realArr = arr.concat(styles,links) 77 | const paromiseArr = 78 | arr && 79 | Array.from(realArr).map((item) => { 80 | if (item.rel) { 81 | const url = window.location.protocol + '//' + window.location.host; 82 | const path = item.href.replace(url, ''); 83 | const needEntryPath = !/(http|https):\/\/([\w.]+\/?)\S*/.test(path); 84 | return fetch(`${needEntryPath ? entryPath : ''}${item.href}`.replace(url, '')).then( 85 | function (response) { 86 | return response.text(); 87 | } 88 | ); 89 | } else { 90 | return Promise.resolve(item.textContent); 91 | } 92 | }); 93 | const res = await Promise.all(paromiseArr); 94 | if (res && res.length > 0) { 95 | res.forEach((item) => { 96 | const style = document.createElement('style'); 97 | style.innerHTML = item; 98 | subapp.appendChild(style); 99 | }); 100 | } 101 | } 102 | --------------------------------------------------------------------------------