@xingtukeji/micro 1.0.1 → 1.0.3

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/src/parent.ts ADDED
@@ -0,0 +1,268 @@
1
+ import { ref } from 'vue'
2
+ import {AppOption, App} from './index.d'
3
+ import {CONSTANT} from './types'
4
+ import {getQueryParameter, addOrReplaceUrlParam, getAbsolutePath} from './utils'
5
+
6
+ /**
7
+ * 判断是否在微应用中
8
+ */
9
+ const _isMicro = ref(false)
10
+ Object.defineProperty(window, CONSTANT.IS_MICRO, {
11
+ get: function () {
12
+ return _isMicro.value
13
+ },
14
+ set: function (newValue) {
15
+ _isMicro.value = newValue
16
+ },
17
+ })
18
+ export const isMicro = _isMicro
19
+
20
+ /**
21
+ * 生成微应用id
22
+ * 增加前缀
23
+ * @param name 微应用名称(唯一)
24
+ * @returns 返回dom id
25
+ */
26
+ const generateId = (name: string) => {
27
+ return `${CONSTANT.DOM_ID_PREFIX}${name}`
28
+ }
29
+ /**
30
+ * 处理微应用url
31
+ * @param url 微应用url
32
+ * @param params 携带参数
33
+ * @returns 拼接后的url
34
+ */
35
+ const generateURL = (app: AppOption) => {
36
+ // 判断变量是否为对象
37
+ function isObject(variable: unknown) {
38
+ return variable !== null && typeof variable === 'object' && !Array.isArray(variable)
39
+ }
40
+ // 获取params参数
41
+ const _parmas = typeof app.params === 'function' ? app.params() : isObject(app.params) ? app.params : {}
42
+ // 获取token参数
43
+ const _token = typeof app.token === 'function' ? app.token() : isObject(app.token) ? app.token : ''
44
+ // 创建URL对象
45
+ const url = new URL(app.url.indexOf('http') > -1 ? app.url : window.location.origin + app.url)
46
+ // 获取microBakUrl参数
47
+ const microBakUrl = getQueryParameter(app.name)
48
+ // 如果microBakUrl存在
49
+ if(microBakUrl){
50
+ // 获取绝对路径
51
+ const temp = getAbsolutePath(microBakUrl, app.url, true)
52
+ // 如果绝对路径存在
53
+ if(temp){
54
+ // 创建URL对象
55
+ const tempURL = new URL(temp);
56
+ // 获取searchParams参数
57
+ const tempSearchParams = tempURL.searchParams
58
+ // 遍历searchParams参数
59
+ tempSearchParams.forEach((value, key) => {
60
+ // 设置url的searchParams参数
61
+ url.searchParams.set(key, value)
62
+ })
63
+ // 设置url的hash参数
64
+ url.hash = tempURL.hash
65
+ }
66
+ }
67
+
68
+ // 设置url的searchParams参数
69
+ url.searchParams.set(CONSTANT.IS_MICRO, true+'')
70
+ url.searchParams.set(CONSTANT.MICRO_NAME, app.name)
71
+ _token && url.searchParams.set(CONSTANT.MICRO_TOKEN, _token)
72
+
73
+ if(_parmas){
74
+ // 设置url的searchParams参数
75
+ for(let key of Object.keys(_parmas)){
76
+ url.searchParams.set(key, _parmas[key])
77
+ }
78
+ }
79
+
80
+ // 清空主路由上的子应用路由sync参数
81
+ const mainURL = new URL(window.location.href)
82
+ mainURL.searchParams.delete(app.name)
83
+ window.history.replaceState({}, '', mainURL.href)
84
+ // 返回url的href属性
85
+ return url.href
86
+ }
87
+
88
+ export class MicroApp {
89
+ app: App | AppOption
90
+ dom: HTMLIFrameElement | undefined
91
+ panel: HTMLElement | undefined
92
+
93
+ constructor(app: AppOption) {
94
+ this.app = app
95
+ app.preload && this.preload()
96
+ this.dom = undefined
97
+ this.panel = undefined
98
+ return this
99
+ }
100
+
101
+ mount(el: HTMLElement | string | undefined, isHide = false) {
102
+ let panel: HTMLElement | null
103
+ if (el) {
104
+ if (typeof el === 'string') {
105
+ panel = document.querySelector(el)
106
+ } else {
107
+ panel = el
108
+ }
109
+ //这里清空了容器的内容,为了方便,如果还有其他元素需要特别处理,而且也影响元素再复用
110
+ panel!.innerHTML = ''
111
+ } else {
112
+ panel = document.body
113
+ }
114
+
115
+ if (panel) {
116
+ let ifr = <HTMLIFrameElement>document.getElementById(generateId(this.app.name))
117
+ if (!ifr) {
118
+ ifr = document.createElement('iframe')
119
+ ifr.id = generateId(this.app.name)
120
+ const url = generateURL(this.app)
121
+ ifr.src = url
122
+ ifr.classList.add('micro-iframe')
123
+ ifr.addEventListener('load', (event) => {
124
+ this.onLoad.call(this, event)
125
+ })
126
+ ifr.addEventListener('error', this.onError)
127
+ ifr.addEventListener('unload', this.onUnload)
128
+ this.dom = ifr
129
+ }
130
+ isHide ? (ifr.style.display = 'none') : ifr.style.removeProperty('display')
131
+ this.panel = panel
132
+ panel.appendChild(ifr)
133
+ } else {
134
+ throw new Error('el not found')
135
+ }
136
+ }
137
+
138
+ unmount() {
139
+ const appDom = document.getElementById(generateId(this.app.name))
140
+ if (appDom) {
141
+ if (this.app.preload) {
142
+ // if (this.preload){
143
+ appDom.style.display = 'none'
144
+ document.body.appendChild(appDom)
145
+ } else {
146
+ this.panel?.removeChild(appDom)
147
+ }
148
+ } else {
149
+ throw new Error('app dom not found')
150
+ }
151
+ }
152
+
153
+ /**
154
+ * 预加载渲染
155
+ * TODO 还差用户切走之后,如何将ifr还原回去,等待下次载入
156
+ * @param el 挂载dom对象
157
+ */
158
+ preloadRender(el: HTMLElement | string | undefined) {
159
+ // let appDom: HTMLElement | undefined = this.dom
160
+ this.mount(el)
161
+
162
+ // const appDom = document.getElementById(generateId(this.app.name))
163
+ // if (!appDom) {
164
+ //
165
+ // this.render(el)
166
+ // return
167
+ // }
168
+ //
169
+ // let panel
170
+ // if (typeof el === 'string') {
171
+ // panel = document.querySelector(el)
172
+ // } else {
173
+ // panel = el
174
+ // }
175
+ // appDom!.style.removeProperty('display')
176
+ // panel?.appendChild(appDom)
177
+ }
178
+
179
+ /**
180
+ * 预加载app,先放到body,待启动时,挪过来
181
+ */
182
+ preload() {
183
+ this.mount(undefined, true)
184
+ }
185
+
186
+ changeOptions(options: Partial<AppOption>) {
187
+ this.app = {
188
+ ...this.app,
189
+ ...options,
190
+ }
191
+ }
192
+
193
+ onLoad(_event: Event) {
194
+ // console.log('子应用 onload', this.app.name)
195
+ // console.log('onLoad',this.app, this.panel, this.dom)
196
+ // if(this.dom){
197
+ // const headerDom = this.dom.contentWindow.document.querySelector('._hedaui-header')
198
+ // headerDom && (headerDom.style.display = 'none')
199
+ // }
200
+
201
+ //监听message,设置环境变量
202
+ // console.log('window', window['__XT_MICRO'])
203
+ // window.addEventListener('message', (e) => {
204
+ // console.log('1111', this.app.name, window.document)
205
+ // if (e.data.cmd === `$xt/micro/${this.app.name}`) {
206
+ // // console.log('micro', e.data.data)
207
+ // window[CONSTANT.MICRO_APP] = e.data.data
208
+ // const headerDom = document.querySelector('._hedaui-header')
209
+ // headerDom && (headerDom.style.display = 'none')
210
+ // }
211
+ // })
212
+ _postMicro(this)
213
+ }
214
+
215
+ onUnload(_event: Event) {}
216
+
217
+ onError() {}
218
+ }
219
+
220
+
221
+
222
+
223
+
224
+ /**
225
+ * 父应用给子应用发送消息
226
+ * @param app 微应用对象
227
+ */
228
+ const _postMicro = (app: MicroApp) => {
229
+ // console.log(`父应用给子应用「${app.app.name}」发送消息`, app.dom)
230
+ app.dom?.contentWindow?.postMessage(
231
+ {
232
+ cmd: `$xt/micro/${app.app.name}`,
233
+ data: {
234
+ name: app.app.name,
235
+ url: app.app.url,
236
+ preload: app.app.preload,
237
+ sync: app.app.sync || true
238
+ // params: app.params
239
+ },
240
+ },
241
+ '*'
242
+ )
243
+ }
244
+
245
+
246
+
247
+ /**
248
+ * 初始化主应用微前端
249
+ */
250
+ export const mainMicroInit = () => {
251
+ // 监听window对象的消息事件
252
+ window.addEventListener('message', (e)=>{
253
+ // 同步子应用路由到主应用上
254
+ if (e.data.cmd === '$xt/micro/sync') {
255
+ // 过滤掉微应用的默认参数
256
+ const whitelist = [CONSTANT.IS_MICRO, CONSTANT.MICRO_NAME, CONSTANT.MICRO_TOKEN]
257
+ const tempUrl = new URL(window.decodeURIComponent(e.data.data.url), e.origin)
258
+ for(let key of whitelist){
259
+ tempUrl.searchParams.delete(key)
260
+ }
261
+ console.log('tempUrl', tempUrl.href)
262
+ // 调用addOrReplaceUrlParam函数,将e.data.data.name和e.data.data.url作为参数传入
263
+ const curUrl = addOrReplaceUrlParam(window.location.href, e.data.data.name, tempUrl.href)
264
+ // 使用replaceState方法替换当前页面的URL
265
+ window.history.replaceState(null, "", curUrl);
266
+ }
267
+ })
268
+ }
package/src/types.ts ADDED
@@ -0,0 +1,8 @@
1
+ export const CONSTANT: Record<string, string> = {
2
+ KEY: '$xtm', // 父应用注入子应用实例的key
3
+ IS_MICRO: '__XT_MICRO', // 是否在微应用中
4
+ MICRO_NAME: '__XT_MICRO_NAME', // 微应用名称
5
+ MICRO_TOKEN: '__XT_MICRO_TOKEN', // 微应用token
6
+ MICRO_APP: '__XT_MICRO_APP', // 微应用实例
7
+ DOM_ID_PREFIX: 'xt-micro-ifr-', // dom id前缀
8
+ }
package/src/utils.ts ADDED
@@ -0,0 +1,94 @@
1
+ // 获取URL中的查询参数
2
+ // 导出一个函数,用于获取查询参数
3
+ export function getQueryParameter(name: string, url = window.location.href): string | null {
4
+ // 将参数名中的中括号替换为转义字符
5
+ name = name.replace(/[\[\]]/g, '\\$&')
6
+ // 创建正则表达式,匹配查询参数
7
+ var regex = new RegExp('[?&]' + name + '(=([^&#]*)|&|#|$)'),
8
+ results = regex.exec(url)
9
+ // 如果没有匹配到查询参数,返回null
10
+ if (!results) return null
11
+ // 如果没有匹配到参数值,返回空字符串
12
+ if (!results[2]) return ''
13
+ // 对参数值进行解码,并去除首尾空格
14
+ return decodeURIComponent(results[2].replace(/\+/g, ' ')).trim()
15
+ }
16
+
17
+ // 导出一个函数,用于获取绝对路径
18
+ export function getAbsolutePath(url: string|undefined, base: string, hash?: boolean): string | undefined {
19
+ try {
20
+ // 为空值无需处理
21
+ if (url) {
22
+ // 需要处理hash的场景
23
+ if (hash && url.startsWith("#")) return url;
24
+ return new URL(url, base).href;
25
+ } else return url;
26
+ } catch {
27
+ return url;
28
+ }
29
+ }
30
+
31
+ // 导出一个函数,用于生成一个HTMLAnchorElement元素
32
+ export function anchorElementGenerator(url: string): HTMLAnchorElement {
33
+ // 创建一个a标签元素
34
+ const element = window.document.createElement("a");
35
+ // 设置a标签的href属性为传入的url
36
+ element.href = url;
37
+ element.href = element.href; // hack ie
38
+ return element;
39
+ }
40
+
41
+ // 导出一个函数,用于获取锚点元素的查询字符串映射
42
+ export function getAnchorElementQueryMap(anchorElement: HTMLAnchorElement): { [key: string]: string } {
43
+ // 获取锚点元素的查询字符串
44
+ const queryString = anchorElement.search || "";
45
+ // 将查询字符串转换为URLSearchParams对象,并使用reduce方法将其转换为键值对映射
46
+ return [...new URLSearchParams(queryString).entries()].reduce((p, c) => {
47
+ // 将键值对添加到映射中
48
+ p[c[0]] = c[1];
49
+ // 返回映射
50
+ return p;
51
+ }, {} as Record<string, string>);
52
+ }
53
+
54
+
55
+ //防抖函数,支持传过来一个函数,返回一个防抖后的函数
56
+ export function debounce<T extends (...args: any[]) => any>(func: T, delay: number): (...args: Parameters<T>) => void {
57
+ let timer: ReturnType<typeof setTimeout> | null = null;
58
+
59
+ return function (this: ThisParameterType<T>, ...args: Parameters<T>): void {
60
+ const context = this;
61
+
62
+ if (timer) {
63
+ clearTimeout(timer);
64
+ }
65
+
66
+ timer = setTimeout(() => {
67
+ func.apply(context, args);
68
+ }, delay);
69
+ };
70
+ }
71
+
72
+ export function addOrReplaceUrlParam(url: string, paramName: string, paramValue: string): string {
73
+ // 先分割出 URL 的基础部分和哈希部分
74
+ const [baseUrl, hash] = url.split('#');
75
+ const urlObj = new URL(baseUrl);
76
+ const searchParams = urlObj.searchParams;
77
+
78
+ // 如果参数已经存在,则替换其值
79
+ if (searchParams.has(paramName)) {
80
+ searchParams.set(paramName, paramValue);
81
+ } else {
82
+ // 如果参数不存在,则添加该参数
83
+ searchParams.append(paramName, paramValue);
84
+ }
85
+ // 如果原 URL 有哈希部分,再把它加回去
86
+ if (hash) {
87
+ urlObj.hash = hash
88
+ }
89
+
90
+ urlObj.search = searchParams.toString();
91
+ let newUrl = urlObj.toString();
92
+
93
+ return newUrl;
94
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "esnext",
4
+ "useDefineForClassFields": true,
5
+ "module": "esnext",
6
+ "moduleResolution": "node",
7
+ "strict": false,
8
+ "jsx": "preserve",
9
+ "sourceMap": false,
10
+ "resolveJsonModule": true,
11
+ "esModuleInterop": true,
12
+ "lib": ["esnext", "dom"],
13
+ "paths": {
14
+ "@/*": ["./src/*"]
15
+ },
16
+ "types": ["element-plus/global"],
17
+ "declaration": true,
18
+ "declarationDir": "dist",
19
+ "outDir": "./dist",
20
+ "rootDir": "./src",
21
+ },
22
+ "exclude": ["node_modules", "dist"],
23
+ "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"]
24
+ }
package/vite.config.ts ADDED
@@ -0,0 +1,28 @@
1
+ import { defineConfig } from 'vite'
2
+ import vue from '@vitejs/plugin-vue'
3
+ import dts from 'vite-plugin-dts';
4
+ // import { name, version } from './package.json'
5
+
6
+ export default defineConfig(({ mode }) => {
7
+ return {
8
+ base: './',
9
+ build: {
10
+ lib: {
11
+ entry: ['src/index.ts'],
12
+ name: '__xt_micro__',
13
+ formats: ['es'],
14
+ fileName: (format, entryName) => `index.js`,
15
+ cssFileName: 'my-lib-style',
16
+ },
17
+ target: 'es2015',
18
+ emptyOutDir: true, //去掉打包到项目外的警告
19
+ outDir: `./dist`,
20
+ },
21
+ plugins: [
22
+ vue(),
23
+ dts({
24
+ outDir: 'dist/types'
25
+ })
26
+ ],
27
+ }
28
+ })