├── 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 |
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 |
--------------------------------------------------------------------------------