@songcf/micro-web-project-npm 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/const/mainLifeCycle.js +5 -0
- package/const/subApps.js +5 -0
- package/event/index.js +17 -0
- package/index.js +2 -0
- package/lifeCycle/index.js +54 -0
- package/loader/htmlLoader.js +94 -0
- package/loader/prefetch.js +10 -0
- package/package.json +14 -0
- package/router/rewriteRouter.js +15 -0
- package/router/routerHandle.js +9 -0
- package/sandbox/performScript.js +37 -0
- package/sandbox/proxySandBox.js +30 -0
- package/sandbox/sandbox.js +45 -0
- package/sandbox/snapshotSandBox.js +22 -0
- package/start.js +46 -0
- package/store/index.js +25 -0
- package/utils/fetchResource.js +1 -0
- package/utils/index.js +67 -0
package/const/subApps.js
ADDED
package/event/index.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
|
|
2
|
+
export class Custom {
|
|
3
|
+
constructor() { }
|
|
4
|
+
|
|
5
|
+
on(eventName, cb) {
|
|
6
|
+
window.addEventListener(eventName, function (e) {
|
|
7
|
+
cb(e.detail)
|
|
8
|
+
})
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
emit(eventName, data) {
|
|
12
|
+
const event = new CustomEvent(eventName, {
|
|
13
|
+
detail: data
|
|
14
|
+
})
|
|
15
|
+
window.dispatchEvent(event);
|
|
16
|
+
}
|
|
17
|
+
}
|
package/index.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { findAppByRoute } from '../utils/index'
|
|
2
|
+
import { getMainLifeCycle } from '../const/mainLifeCycle'
|
|
3
|
+
import { loadHtml } from '../loader/htmlLoader'
|
|
4
|
+
|
|
5
|
+
// 执行主应用生命周期
|
|
6
|
+
export const runMainLifeCycle = async (type, app) => {
|
|
7
|
+
const mainLief = getMainLifeCycle()
|
|
8
|
+
await Promise.all(mainLief[type].map(async fn => await fn(app)))
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// 装载应用
|
|
12
|
+
export const bootstrap = async (app) => {
|
|
13
|
+
await runMainLifeCycle('beforeLoad', app);
|
|
14
|
+
// console.log(app)
|
|
15
|
+
await loadHtml(app)
|
|
16
|
+
|
|
17
|
+
app && await app.bootstrap()
|
|
18
|
+
|
|
19
|
+
// return subApp
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// 渲染应用
|
|
23
|
+
export const mount = async (app) => {
|
|
24
|
+
app && app.mount && await app.mount(app)
|
|
25
|
+
await runMainLifeCycle('mounted', app);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// 卸载
|
|
29
|
+
export const unmount = async (app) => {
|
|
30
|
+
app && app.unmount && await app.unmount(app);
|
|
31
|
+
await runMainLifeCycle('destroyed', app);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const lifeCycle = async () => {
|
|
35
|
+
// 获取上一个子应用
|
|
36
|
+
const prevApp = findAppByRoute(window.window.__ORIGIN_APP__)
|
|
37
|
+
|
|
38
|
+
// 获取下一个要跳转到的子应用
|
|
39
|
+
const nextApp = findAppByRoute(window.__CURRENT_SUB_APP__)
|
|
40
|
+
|
|
41
|
+
// console.log(prevApp, nextApp)
|
|
42
|
+
|
|
43
|
+
if (!nextApp) {
|
|
44
|
+
return
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (prevApp) {
|
|
48
|
+
await unmount(prevApp)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
await bootstrap(nextApp)
|
|
52
|
+
|
|
53
|
+
await mount(nextApp)
|
|
54
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { fetchResource } from '../utils/fetchResource'
|
|
2
|
+
import { sandbox } from '../sandbox/sandbox'
|
|
3
|
+
// import { performScriptForEval } from '../sandbox/performScript'
|
|
4
|
+
|
|
5
|
+
const cache = {};
|
|
6
|
+
|
|
7
|
+
export const loadHtml = async (app) => {
|
|
8
|
+
let { container: containerName, entry, name } = app;
|
|
9
|
+
const [dom, scriptsArray] = await parseHtml(entry, name)
|
|
10
|
+
|
|
11
|
+
const container = document.querySelector(containerName);
|
|
12
|
+
if (!container) {
|
|
13
|
+
console.log(app)
|
|
14
|
+
throw Error(`${ name } 的容器不存在,请查看是否正确指定`)
|
|
15
|
+
}
|
|
16
|
+
container.innerHTML = dom;
|
|
17
|
+
scriptsArray.map((item) => {
|
|
18
|
+
sandbox(item, name)
|
|
19
|
+
// performScriptForEval(item)
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
// return app
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const parseHtml = async (entry, appName) => {
|
|
26
|
+
if (cache[appName]) {
|
|
27
|
+
return cache[appName];
|
|
28
|
+
}
|
|
29
|
+
const html = await fetchResource(entry);
|
|
30
|
+
|
|
31
|
+
let scriptsArray = []
|
|
32
|
+
const div = document.createElement('div')
|
|
33
|
+
div.innerHTML = html;
|
|
34
|
+
|
|
35
|
+
const [elements, scriptUrl, script] = await getResources(div, entry)
|
|
36
|
+
|
|
37
|
+
// console.log(elements, scriptUrl, script)
|
|
38
|
+
const fetchedScripts = await Promise.all(scriptUrl.map(async url => await fetchResource(url)))
|
|
39
|
+
|
|
40
|
+
scriptsArray = script.concat(fetchedScripts)
|
|
41
|
+
// console.log(fetchedScripts, elements, script)
|
|
42
|
+
cache[appName] = [elements, scriptsArray];
|
|
43
|
+
|
|
44
|
+
return [elements, scriptsArray]
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const getResources = async (root, entry) => {
|
|
48
|
+
const scriptUrl = [];
|
|
49
|
+
const script = [];
|
|
50
|
+
const div = root.outerHTML
|
|
51
|
+
|
|
52
|
+
// 深度解析
|
|
53
|
+
function deepParse(element) {
|
|
54
|
+
const children = element.children
|
|
55
|
+
const parent = element.parent;
|
|
56
|
+
|
|
57
|
+
// 处理位于 script 中的资源
|
|
58
|
+
if (element.nodeName.toLowerCase() === 'script') {
|
|
59
|
+
const src = element.getAttribute('src')
|
|
60
|
+
if (!src) {
|
|
61
|
+
script.push(element.outerHTML)
|
|
62
|
+
} else {
|
|
63
|
+
if (src.startsWith('http')) {
|
|
64
|
+
scriptUrl.push(src)
|
|
65
|
+
} else {
|
|
66
|
+
scriptUrl.push(`http:${ entry }/${ src }`)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (parent) {
|
|
71
|
+
parent.replaceChild(document.createComment('此 js 文件已经被微前端替换'), element)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (element.nodeName.toLowerCase() === 'link') {
|
|
76
|
+
const href = element.getAttribute('href')
|
|
77
|
+
|
|
78
|
+
if (href.endsWith('.js')) {
|
|
79
|
+
if (href.startsWith('http')) {
|
|
80
|
+
scriptUrl.push(href)
|
|
81
|
+
} else {
|
|
82
|
+
scriptUrl.push(`http:${ entry }/${ href }`)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
for (let i = 0; i < children.length; i++) {
|
|
88
|
+
deepParse(children[i])
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
deepParse(root)
|
|
93
|
+
return [div, scriptUrl, script]
|
|
94
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { parseHtml } from './htmlLoader';
|
|
2
|
+
import { getList } from '../const/subApps';
|
|
3
|
+
|
|
4
|
+
export const prefetch = async () => {
|
|
5
|
+
// 获取其余子应用
|
|
6
|
+
const appPieces = getList().filter(item => !window.location.pathname.startsWith(item.activeRule));
|
|
7
|
+
|
|
8
|
+
// 加载所有子应用
|
|
9
|
+
await Promise.all(appPieces.map(async app => await parseHtml(app.entry, app.name)))
|
|
10
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@songcf/micro-web-project-npm",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "这是实验的微前端框架,请勿要投入生产环境使用,请谨慎使用",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
8
|
+
},
|
|
9
|
+
"keywords": [
|
|
10
|
+
"micro-web-fe"
|
|
11
|
+
],
|
|
12
|
+
"author": "songcf",
|
|
13
|
+
"license": "ISC"
|
|
14
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { patchRouter } from '../utils/index'
|
|
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
|
+
window.onpopstate = function () {
|
|
13
|
+
turnApp()
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
// 执行应用中的 js 内容 new Function 篇
|
|
4
|
+
export const performScript = (script, appName, global) => {
|
|
5
|
+
window.proxy = global
|
|
6
|
+
const scriptText = `
|
|
7
|
+
return ((window) => {
|
|
8
|
+
try{
|
|
9
|
+
${ script }
|
|
10
|
+
} catch (e) {
|
|
11
|
+
console.error('run script error: ' + e)
|
|
12
|
+
}
|
|
13
|
+
return window['${ appName }']
|
|
14
|
+
})(window.proxy)
|
|
15
|
+
`
|
|
16
|
+
|
|
17
|
+
return new Function(scriptText)()
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// 执行应用中的 js 内容 eval篇
|
|
21
|
+
export const performScriptForEval = (script, appName, global) => {
|
|
22
|
+
const globalWindow = (0, eval)(window)
|
|
23
|
+
globalWindow.proxy = global;
|
|
24
|
+
// console.log('===---', eval(script), window[appName])
|
|
25
|
+
const scriptText = `
|
|
26
|
+
((window) => {
|
|
27
|
+
try{
|
|
28
|
+
${ script }
|
|
29
|
+
} catch (e) {
|
|
30
|
+
console.error('run script error: ' + e)
|
|
31
|
+
}
|
|
32
|
+
return window['${ appName }']
|
|
33
|
+
}).bind(window.proxy)(window.proxy)
|
|
34
|
+
`
|
|
35
|
+
return eval(scriptText)// app module mount
|
|
36
|
+
|
|
37
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
let defaultValue = {}
|
|
2
|
+
|
|
3
|
+
// 代理沙箱
|
|
4
|
+
export class ProxySandBox {
|
|
5
|
+
constructor() {
|
|
6
|
+
this.active()
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
active() {
|
|
10
|
+
this.proxy = new Proxy(window, {
|
|
11
|
+
set(target, name, value) {
|
|
12
|
+
defaultValue[name] = value;
|
|
13
|
+
return true;
|
|
14
|
+
},
|
|
15
|
+
|
|
16
|
+
get(target, name) {
|
|
17
|
+
if (typeof target[name] === 'function' && /^[a-z]/.test(name)) {
|
|
18
|
+
return target[name].bind && target[name].bind(target);
|
|
19
|
+
} else {
|
|
20
|
+
return defaultValue[name] || target[name];
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
inactive() {
|
|
27
|
+
defaultValue = {}
|
|
28
|
+
console.log('关闭沙箱');
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { findAppByName } from '../utils/index'
|
|
2
|
+
import { ProxySandBox } from './proxySandBox';
|
|
3
|
+
import { performScriptForEval } from './performScript'
|
|
4
|
+
import { SnapShotSandBox } from './snapshotSandBox'
|
|
5
|
+
|
|
6
|
+
// 检测是否漏掉了生命周期方法
|
|
7
|
+
export const lackOfLifecycle = (lifecycles) => !lifecycles ||
|
|
8
|
+
!lifecycles.bootstrap ||
|
|
9
|
+
!lifecycles.mount ||
|
|
10
|
+
!lifecycles.unmount;
|
|
11
|
+
|
|
12
|
+
// 创建沙箱环境
|
|
13
|
+
export const sandbox = (script, appName) => {
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
// 获取当前子应用
|
|
17
|
+
const app = findAppByName(appName);
|
|
18
|
+
|
|
19
|
+
// 创建沙箱环境
|
|
20
|
+
const global = new ProxySandBox();
|
|
21
|
+
// 快照沙箱
|
|
22
|
+
// const proxy = new SnapShotSandBox()
|
|
23
|
+
// if (!app.proxy) {
|
|
24
|
+
// app.proxy = proxy;
|
|
25
|
+
// }
|
|
26
|
+
|
|
27
|
+
// 设置微前端环境
|
|
28
|
+
window.__MICRO_WEB__ = true;
|
|
29
|
+
|
|
30
|
+
// 获取子应用生命周期
|
|
31
|
+
const lifeCycles = performScriptForEval(script, appName, global.proxy);
|
|
32
|
+
|
|
33
|
+
app.sandBox = global;
|
|
34
|
+
|
|
35
|
+
// 检查子应用是否包含必须的方法
|
|
36
|
+
const isLack = lackOfLifecycle(lifeCycles)
|
|
37
|
+
if (isLack) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
app.bootstrap = lifeCycles.bootstrap;
|
|
42
|
+
app.mount = lifeCycles.mount;
|
|
43
|
+
app.unmount = lifeCycles.unmount;
|
|
44
|
+
|
|
45
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// 快照沙箱
|
|
2
|
+
|
|
3
|
+
export class SnapShotSandBox {
|
|
4
|
+
constructor() {
|
|
5
|
+
this.proxy = window;
|
|
6
|
+
this.active()
|
|
7
|
+
}
|
|
8
|
+
active() {
|
|
9
|
+
this.snapshot = new Map()
|
|
10
|
+
for (const key in window) {
|
|
11
|
+
this.snapshot[key] = window[key]
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
inactive() {
|
|
15
|
+
console.log('VV3')
|
|
16
|
+
for (const key in window) {
|
|
17
|
+
if (window[key] !== this.snapshot[key]) {
|
|
18
|
+
window[key] = this.snapshot[key]
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
package/start.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { prefetch } from './loader/prefetch';
|
|
2
|
+
import { getList, setList } from './const/subApps'
|
|
3
|
+
import { setMainLifeCycle } from './const/mainLifeCycle'
|
|
4
|
+
import { rewriteRouter } from './router/rewriteRouter'
|
|
5
|
+
import { currentApp } from './utils/index'
|
|
6
|
+
import { Custom } from './event/index'
|
|
7
|
+
|
|
8
|
+
const custom = new Custom()
|
|
9
|
+
|
|
10
|
+
custom.on('test', (data) => { console.log('test event', data) })
|
|
11
|
+
|
|
12
|
+
window.custom = custom
|
|
13
|
+
// 实现路由拦截
|
|
14
|
+
rewriteRouter()
|
|
15
|
+
|
|
16
|
+
export const registerMicroApps = (appList, lifeCycle) => {
|
|
17
|
+
// window.appList = appList
|
|
18
|
+
setList(appList)
|
|
19
|
+
setMainLifeCycle(lifeCycle)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// 启动微前端框架
|
|
23
|
+
export const start = async () => {
|
|
24
|
+
// 首先验证子应用列表是否为空
|
|
25
|
+
const apps = getList()
|
|
26
|
+
if (!apps.length) {
|
|
27
|
+
throw new Error('子应用列表为空,请使用registerMicroApps注册子应用')
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// 查找到符合当前路由的子应用
|
|
31
|
+
const app = currentApp()
|
|
32
|
+
|
|
33
|
+
if (app) {
|
|
34
|
+
const { pathname, hash } = window.location
|
|
35
|
+
|
|
36
|
+
const url = pathname + hash
|
|
37
|
+
|
|
38
|
+
window.history.pushState('', '', url)
|
|
39
|
+
|
|
40
|
+
// 将当前子应用做标记
|
|
41
|
+
window.__CURRENT_SUB_APP__ = app.activeRule
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// 预加载
|
|
45
|
+
await prefetch();
|
|
46
|
+
}
|
package/store/index.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
|
|
2
|
+
export const createStore = (initData = {}) => (() => {
|
|
3
|
+
let store = initData
|
|
4
|
+
const observers = []
|
|
5
|
+
|
|
6
|
+
const getStore = () => store
|
|
7
|
+
const update = (newValue) => {
|
|
8
|
+
if (newValue !== store) {
|
|
9
|
+
let oldValue = store;
|
|
10
|
+
store = newValue;
|
|
11
|
+
// res(store);
|
|
12
|
+
|
|
13
|
+
observers.forEach(async fn => await fn(newValue, oldValue));
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
const subscribeStore = (observer) => {
|
|
17
|
+
observers.push(observer)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
getStore,
|
|
22
|
+
update,
|
|
23
|
+
subscribeStore
|
|
24
|
+
}
|
|
25
|
+
})()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const fetchResource = (url) => fetch(url).then(async res => await res.text())
|
package/utils/index.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { getList } from "../const/subApps"
|
|
2
|
+
|
|
3
|
+
// 为当前的路由跳转打补丁
|
|
4
|
+
export const patchRouter = (globalEvent, eventName) => {
|
|
5
|
+
return function () {
|
|
6
|
+
const e = new Event(eventName)
|
|
7
|
+
globalEvent.apply(this, arguments)
|
|
8
|
+
window.dispatchEvent(e)
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// 获取当前应用
|
|
13
|
+
export const currentApp = () => {
|
|
14
|
+
const currentUrl = window.location.pathname;
|
|
15
|
+
return filterApp('activeRule', currentUrl)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// 根据 路由 查找子应用
|
|
19
|
+
export const findAppByRoute = (router) => {
|
|
20
|
+
return filterApp('activeRule', router)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// 根据 name 查找子应用
|
|
24
|
+
export const findAppByName = (name) => {
|
|
25
|
+
return filterApp('name', name);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
function filterApp(key, value) {
|
|
29
|
+
const currentApp = getList().filter(app => app[key] === value)
|
|
30
|
+
// console.log(currentApp, '---')
|
|
31
|
+
return currentApp.length ? currentApp[0] : false
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// 查看当前路由是否有变化
|
|
35
|
+
export const isTurnChild = () => {
|
|
36
|
+
const { pathname, hash } = window.location
|
|
37
|
+
const url = pathname + hash
|
|
38
|
+
// console.log(url)
|
|
39
|
+
|
|
40
|
+
// 当前路由无改变。
|
|
41
|
+
const currentPrefix = url.match(/(\/\w+)/g)
|
|
42
|
+
// console.log('currentPrefix: ', currentPrefix)
|
|
43
|
+
|
|
44
|
+
if (
|
|
45
|
+
currentPrefix &&
|
|
46
|
+
(currentPrefix[0] === window.__CURRENT_SUB_APP__) &&
|
|
47
|
+
hash === window.__CURRENT_HASH__
|
|
48
|
+
// window.__CURRENT_SUB_APP__ === window.location.pathname
|
|
49
|
+
) {
|
|
50
|
+
return false
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
window.__ORIGIN_APP__ = window.__CURRENT_SUB_APP__
|
|
54
|
+
const currentApp = window.location.pathname.match(/(\/\w+)/)
|
|
55
|
+
if (!currentApp) {
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// 当前路由以改变,修改当前路由
|
|
60
|
+
window.__CURRENT_SUB_APP__ = currentApp[0]
|
|
61
|
+
// console.log(111, window.__ORIGIN_APP__, window.__CURRENT_SUB_APP__)
|
|
62
|
+
|
|
63
|
+
// 判断当前hash值是否改变
|
|
64
|
+
window.__CURRENT_HASH__ = hash
|
|
65
|
+
|
|
66
|
+
return true
|
|
67
|
+
}
|