Browse Source

minor: 微前端框架问题比较多

tuonian 1 week ago
parent
commit
c2319989cc

+ 2 - 1
.gitignore

@@ -40,4 +40,5 @@ server/data/sessions
 /conf/app.local.conf
 /build/
 
-frontend/.idea
+frontend/.idea
+views/

File diff suppressed because it is too large
+ 0 - 0
frontend/dist/assets/index-6a157cc6.css


File diff suppressed because it is too large
+ 0 - 0
frontend/dist/assets/index-8e85d0ec.js


File diff suppressed because it is too large
+ 0 - 0
frontend/dist/assets/index-c330d4de.js


File diff suppressed because it is too large
+ 1 - 1
frontend/dist/cdn/react-all-18.2.0.js


+ 2 - 2
frontend/dist/cdn/rich.js

@@ -1,2 +1,2 @@
-import{a as f,R as D,x as I}from"./react-all-18.2.0.js";import"./ace-builds-1.23.0.js";import{j as S}from"../assets/index-c330d4de.js";var g=function(t){var e;return typeof t=="string"||!t||isNaN(t)?e=t:e="".concat(t,"px"),e};function j(r,t){return E(r)||x(r,t)||w(r,t)||T()}function T(){throw new TypeError(`Invalid attempt to destructure non-iterable instance.
-In order to be iterable, non-array objects must have a [Symbol.iterator]() method.`)}function w(r,t){if(r){if(typeof r=="string")return O(r,t);var e=Object.prototype.toString.call(r).slice(8,-1);if(e==="Object"&&r.constructor&&(e=r.constructor.name),e==="Map"||e==="Set")return Array.from(r);if(e==="Arguments"||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(e))return O(r,t)}}function O(r,t){(t==null||t>r.length)&&(t=r.length);for(var e=0,o=new Array(t);e<t;e++)o[e]=r[e];return o}function x(r,t){var e=r==null?null:typeof Symbol<"u"&&r[Symbol.iterator]||r["@@iterator"];if(e!=null){var o,i,u,l,n=[],a=!0,s=!1;try{if(u=(e=e.call(r)).next,t===0){if(Object(e)!==e)return;a=!1}else for(;!(a=(o=u.call(e)).done)&&(n.push(o.value),n.length!==t);a=!0);}catch(c){s=!0,i=c}finally{try{if(!a&&e.return!=null&&(l=e.return(),Object(l)!==l))return}finally{if(s)throw i}}return n}}function E(r){if(Array.isArray(r))return r}var L=function(t,e){return!e||!t.saveAsObj?e:t.mode==="json"?JSON.parse(e):t.mode==="yaml"||t.mode==="yml"?S.load(e):e},M=function(t,e){return typeof e=="string"||!e?e:t.mode==="json"?JSON.stringify(e,null,2):t.mode==="yaml"||t.mode==="yml"?S.dump(e):e};const k=function(r){var t=r.value,e=r.className,o=e===void 0?"":e,i=r.column,u=r.onChange,l=r.readonly,n=i,a=n.mode;a==="yml"?a="yaml":a||(a="json");var s=f.useState(),c=j(s,2),A=c[0],y=c[1],C=f.useState(""),h=j(C,2),N=h[0],p=h[1],m=f.useRef(),R=function(v){y(v);try{var b=L(n,v);m.current=b,u==null||u(b),p("")}catch{p("has-err")}};return f.useEffect(function(){if(!(m.current&&t===m.current))try{y(M(n,t))}catch(d){console.warn("fromObjData: ",d)}},[t]),D.createElement(I,{mode:a,value:A,showPrintMargin:n.printMargin,showGutter:!n.hiddenLines,highlightActiveLine:!0,theme:"chrome",height:g(i.height||120),width:g(i.width)||"80%",onChange:R,readOnly:!!i.disabled||l,setOptions:n.options,className:"rich-input json-input ".concat(o," ").concat(N),placeholder:n.placeholder})};export{k as default,M as fromObjData,L as toObjData};
+import{a as f,R as w,w as D}from"./react-all-18.2.0.js";import"./ace-builds-1.23.0.js";import{j as S}from"../assets/index-8e85d0ec.js";var g=function(t){var e;return typeof t=="string"||!t||isNaN(t)?e=t:e="".concat(t,"px"),e};function j(r,t){return x(r)||E(r,t)||T(r,t)||I()}function I(){throw new TypeError(`Invalid attempt to destructure non-iterable instance.
+In order to be iterable, non-array objects must have a [Symbol.iterator]() method.`)}function T(r,t){if(r){if(typeof r=="string")return O(r,t);var e=Object.prototype.toString.call(r).slice(8,-1);if(e==="Object"&&r.constructor&&(e=r.constructor.name),e==="Map"||e==="Set")return Array.from(r);if(e==="Arguments"||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(e))return O(r,t)}}function O(r,t){(t==null||t>r.length)&&(t=r.length);for(var e=0,o=new Array(t);e<t;e++)o[e]=r[e];return o}function E(r,t){var e=r==null?null:typeof Symbol<"u"&&r[Symbol.iterator]||r["@@iterator"];if(e!=null){var o,i,u,l,n=[],a=!0,s=!1;try{if(u=(e=e.call(r)).next,t===0){if(Object(e)!==e)return;a=!1}else for(;!(a=(o=u.call(e)).done)&&(n.push(o.value),n.length!==t);a=!0);}catch(c){s=!0,i=c}finally{try{if(!a&&e.return!=null&&(l=e.return(),Object(l)!==l))return}finally{if(s)throw i}}return n}}function x(r){if(Array.isArray(r))return r}var L=function(t,e){return!e||!t.saveAsObj?e:t.mode==="json"?JSON.parse(e):t.mode==="yaml"||t.mode==="yml"?S.load(e):e},M=function(t,e){return typeof e=="string"||!e?e:t.mode==="json"?JSON.stringify(e,null,2):t.mode==="yaml"||t.mode==="yml"?S.dump(e):e};const k=function(r){var t=r.value,e=r.className,o=e===void 0?"":e,i=r.column,u=r.onChange,l=r.readonly,n=i,a=n.mode;a==="yml"?a="yaml":a||(a="json");var s=f.useState(),c=j(s,2),A=c[0],y=c[1],C=f.useState(""),h=j(C,2),N=h[0],p=h[1],m=f.useRef(),R=function(v){y(v);try{var b=L(n,v);m.current=b,u==null||u(b),p("")}catch{p("has-err")}};return f.useEffect(function(){if(!(m.current&&t===m.current))try{y(M(n,t))}catch(d){console.warn("fromObjData: ",d)}},[t]),w.createElement(D,{mode:a,value:A,showPrintMargin:n.printMargin,showGutter:!n.hiddenLines,highlightActiveLine:!0,theme:"chrome",height:g(i.height||120),width:g(i.width)||"80%",onChange:R,readOnly:!!i.disabled||l,setOptions:n.options,className:"rich-input json-input ".concat(o," ").concat(N),placeholder:n.placeholder})};export{k as default,M as fromObjData,L as toObjData};

+ 1 - 1
frontend/dist/config.js

@@ -1,6 +1,6 @@
 // config.js
 window.CONFIG = {
     baseApi: '/api/nginx-ui/api',
-    SSO: true
+    SSO: false
 }
 window.MICRO_APPS = []

+ 3 - 3
frontend/dist/index.html

@@ -3,8 +3,8 @@
     <link rel="icon" type="image/svg+xml" href="/nginx-ui/vite.svg">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
     <title>NginxUI</title>
-    <script type="application/javascript" src="./config.js"></script>
-    <script crossorigin="">import('/nginx-ui/assets/index-c330d4de.js').finally(() => {
+    <script type="application/javascript" src="/nginx-ui/config.js"></script>
+    <script crossorigin="">import('/nginx-ui/assets/index-8e85d0ec.js').finally(() => {
             
     const qiankunLifeCycle = window.moudleQiankunAppLifeCycles && window.moudleQiankunAppLifeCycles['nginx-ui'];
     if (qiankunLifeCycle) {
@@ -17,7 +17,7 @@
           })</script>
     <link rel="modulepreload" crossorigin="" href="/nginx-ui/cdn/ace-builds-1.23.0.js">
     <link rel="modulepreload" crossorigin="" href="/nginx-ui/cdn/react-all-18.2.0.js">
-    <link rel="stylesheet" href="/nginx-ui/assets/index-e9737aa5.css">
+    <link rel="stylesheet" href="/nginx-ui/assets/index-6a157cc6.css">
   </head>
   <body>
     <div id="nginx_ui_root"></div>

+ 1 - 1
frontend/index.html

@@ -5,7 +5,7 @@
     <link rel="icon" type="image/svg+xml" href="/vite.svg" />
     <meta name="viewport" content="width=device-width, initial-scale=1.0" />
     <title>NginxUI</title>
-    <script type="application/javascript" src="./config.js"></script>
+    <script type="application/javascript" src="%BASE_URL%config.js"></script>
   </head>
   <body>
     <div id="nginx_ui_root"></div>

+ 1 - 1
frontend/public/config.js

@@ -1,6 +1,6 @@
 // config.js
 window.CONFIG = {
     baseApi: '/api/nginx-ui/api',
-    SSO: true
+    SSO: false
 }
 window.MICRO_APPS = []

+ 1 - 1
frontend/src/api/request.ts

@@ -8,7 +8,7 @@ console.log('env', import.meta.env)
 
 // eslint-disable-next-line @typescript-eslint/ban-ts-comment
 // @ts-ignore
-const CONFIG = window.CONFIG;
+const CONFIG = window.CONFIG || {};
 if (!CONFIG.baseApi){
   CONFIG.baseApi = '/api'
 }

+ 4 - 0
frontend/src/components/app/index.less

@@ -0,0 +1,4 @@
+.micro-app-container{
+  width: 100%;
+  height: 100%;
+}

+ 48 - 0
frontend/src/components/app/index.tsx

@@ -0,0 +1,48 @@
+
+import {useEffect, useRef} from "react";
+import {startApp,startOptions } from "wujie";
+import {Settings} from "../../api/settings.ts";
+import './index.less'
+
+type IProps = {
+    app: Settings.Route
+    config: Partial<startOptions>
+}
+
+export const MicroApp = ({app, config}: IProps) => {
+
+    const domRef = useRef<HTMLDivElement>(null);
+    const destroyRef = useRef<any>()
+
+    const handleStartApp = () => {
+        if (!domRef.current) {
+            return
+        }
+        console.log('startApp', app.path)
+        startApp({
+            name: app.id,
+            url: app.path,
+            alive: true,
+            ...config,
+            el: domRef.current,
+        }).then(func=>{
+            destroyRef.current = func
+        }).catch(err=>{
+            console.log('startApp error ',err)
+        })
+    }
+
+    useEffect(() => {
+        handleStartApp()
+    }, [domRef.current]);
+
+    useEffect(() => {
+        console.log('MicroApp mounted')
+        return () => {
+            console.log('MicroApp unmounted')
+        }
+    }, []);
+
+    return <div id={'micro_app_'+app.id} className="micro-app-container" ref={domRef} />;
+
+}

+ 3 - 0
frontend/src/main.tsx

@@ -6,6 +6,9 @@ import './index.css'
 import './styles/index.less'
 import renderWithQiankun from "vite-plugin-qiankun/es/helper";
 import './components/form/index.ts'
+import microApp from '@micro-zoe/micro-app'
+
+microApp.start()
 
 let root: Root | null
 

+ 3 - 3
frontend/src/pages/home/index.tsx

@@ -29,7 +29,7 @@ export const HomePage = () => {
                 <div className="home-folder-title">我的收藏</div>
                 <div className="home-folder-links">
                     {
-                        myLinks?.map(link => (<QuickLink route={link}/>))
+                        myLinks?.map(link => (<QuickLink key={link.id} route={link}/>))
                     }
                     <Button onClick={()=>addLinkRef?.current?.onShow()} style={{color: 'white'}} type="text" icon={<PlusOutlined />}>添加</Button>
                 </div>
@@ -38,11 +38,11 @@ export const HomePage = () => {
                 folders.map(links => {
                     if (links.type == 'FOLDER') {
                         return (
-                            <div className="home-folder">
+                            <div key={links.id} className="home-folder">
                                 <div className="home-folder-title">{links.title}</div>
                                 <div className="home-folder-links">
                                     {
-                                        links.children?.map(link => (<QuickLink route={link}/>))
+                                        links.children?.map(link => (<QuickLink key={link.id} route={link}/>))
                                     }
                                 </div>
                             </div>

+ 0 - 1
frontend/src/pages/layout/MainLayout.tsx

@@ -163,7 +163,6 @@ export const MainLayout = () => {
                     <Outlet/>
                 </Content>
             </Layout>
-            <Fullscreen hideInNonFullscreen={true} />
         </Layout>
     );
 };

+ 13 - 0
frontend/src/pages/microApp/index.less

@@ -2,4 +2,17 @@
   height:100%;
   width:100%;
   overflow:hidden;
+  display:flex;
+  flex-direction: column;
+  .micro-app-container{
+    flex:1;
+    width:100%;
+    overflow:hidden;
+
+    .wujie_iframe{
+      height:100%;
+      width:100%;
+      overflow:hidden;
+    }
+  }
 }

+ 33 - 25
frontend/src/pages/microApp/index.tsx

@@ -1,11 +1,11 @@
-import WujieReact from 'wujie-react'
 import './index.less'
 import {useParams} from "react-router";
-import {useCallback, useEffect, useMemo, useState} from "react";
+import {useEffect, useMemo, useRef, useState} from "react";
 import {ErrorComponent} from "../../components/error/Error.tsx";
 import {useAppDispatch, useAppSelector} from "../../store";
 import {LoadingText} from "../../components/loading";
 import {settingsActions} from "../../store/slice/settings.ts";
+import {startApp} from "wujie";
 
 // 腾讯无界: https://wujie-micro.github.io/doc/guide/start.html
 export const MicroAppPage = () => {
@@ -16,6 +16,9 @@ export const MicroAppPage = () => {
 
     const routeMap = useAppSelector(state => state.user.routeMap)
     const dispatch = useAppDispatch();
+    const domRef = useRef<HTMLDivElement>(null);
+    const destroyRef = useRef<any>()
+
 
     const setFullScreen = (full:boolean) => {
         dispatch(settingsActions.setFullScreen(full))
@@ -38,26 +41,28 @@ export const MicroAppPage = () => {
         console.log('afterMounted', app)
     }
 
-    const renderContent = useCallback(() => {
-        if (error) {
-            return <ErrorComponent noRedirect={true} error={error}/>
-        }
-        if (app) {
-            return (
-                <WujieReact
-                    name={app.title}
-                    url={app.path}
-                    height="100%"
-                    width="100%"
-                    beforeMount={() => setLoading(false)}
-                    afterMount={afterMounted}
-                    loadError={onLoadFail}
-                    activated={onActivated}/>
-            )
+    const handleStartApp = () => {
+        if (!domRef.current || !app) {
+            return
         }
-        return null
-        //     @ts-ignore
-    }, [error, app])
+        console.log('startApp', app.path)
+        startApp({
+            name: app.id,
+            url: app.path,
+            alive: true,
+            el: domRef.current,
+            beforeMount: () => setLoading(true),
+            afterMount: afterMounted,
+            loadError: onLoadFail,
+            activated: onActivated,
+            degrade: true,
+            sync: true,
+        }).then(func=>{
+            destroyRef.current = func
+        }).catch(err=>{
+            console.log('startApp error ',err)
+        })
+    }
 
     useEffect(() => {
         setFullScreen(true)
@@ -66,10 +71,13 @@ export const MicroAppPage = () => {
         }
     }, []);
 
-    return (<div className="micro-app">
+    useEffect(() => {
+        handleStartApp()
+    }, [domRef.current,app]);
+
+    return (<div id={'micro_app_'+app?.id} className="micro-app">
         <LoadingText loading={loading} text="加载中,请稍后..."/>
-        {
-            renderContent()
-        }
+        {error ?<ErrorComponent noRedirect={true} error={error}/>: null}
+        <div className="micro-app-container" ref={domRef} />
     </div>)
 }

+ 23 - 19
frontend/src/routes/wrap.tsx

@@ -24,23 +24,6 @@ export const RouteWrapper = ({Component, ...props}: RouteWrapperProps) => {
     // 使用useRoutes加载的路由也是二级路由,无法匹配
     const matches = useMatches()
 
-    useEffect(() => {
-        const current = matches[matches.length - 1]
-        if (current.id && !routeMap[current.id] && !(current.handle as any)?.skipAuth){
-            console.log('current match NOT FOUND',current)
-            navigate('/404', { replace: true })
-        }
-    }, [matches, routeMap]);
-
-
-    useEffect(() => {
-        setTimeout(()=>{
-            if (!isLogin) {
-                navigate('/login')
-            }
-        },500)
-    }, [isLogin]);
-
     const fetchUser = () => {
         setLoading(true);
         LoginApis.userinfo().then(({data}) => {
@@ -56,8 +39,29 @@ export const RouteWrapper = ({Component, ...props}: RouteWrapperProps) => {
     }
 
     useEffect(() => {
-        fetchUser()
-    }, [])
+        const current = matches[matches.length - 1]
+        if (!isLogin){
+            navigate('/login?to='+ current.pathname)
+            return;
+        }
+        if (!routeMap){
+            fetchUser()
+            return
+        }
+        if ((current.handle as any)?.skipAuth){
+            return;
+        }
+        if (current.id && !routeMap[current.id]){
+            console.log('current match NOT FOUND',current)
+            navigate('/404', { replace: true })
+        }
+    }, [matches, routeMap,isLogin]);
+
+    useEffect(() => {
+        if (isLogin){
+            fetchUser()
+        }
+    }, []);
 
     if (!isLogin || loading) {
         return (<div className="empty-loading">

+ 10 - 1
server/base/error.go

@@ -6,12 +6,14 @@ import (
 	"net/http"
 	config2 "nginx-ui/server/config"
 	"nginx-ui/server/models"
+	"nginx-ui/server/utils"
 	"strings"
 )
 
 type ErrorController struct {
 	beego.Controller
 	WebPath string
+	WebDir  string
 }
 
 func getWebPath() string {
@@ -31,6 +33,9 @@ func (c *ErrorController) Error404() {
 	if c.WebPath == "" {
 		c.WebPath = getWebPath()
 	}
+	if c.WebDir == "" {
+		c.WebDir = utils.GetStaticDir()
+	}
 
 	c.EnableRender = false
 	request := c.Ctx.Request
@@ -45,7 +50,11 @@ func (c *ErrorController) Error404() {
 		}
 		c.ServeJSON()
 	} else if c.WebPath != request.RequestURI {
-		c.Redirect(c.WebPath, http.StatusMovedPermanently)
+		c.EnableRender = true
+		//c.Redirect(c.WebPath, http.StatusMovedPermanently)
+		// 适配vue的history模式
+		c.Ctx.Output.SetStatus(http.StatusOK)
+		c.TplName = "index.html"
 	} else {
 		c.Data["json"] = models.RespData{
 			Code: 404,

+ 11 - 0
server/init/init.go

@@ -8,8 +8,18 @@ import (
 	"nginx-ui/server/db"
 	"nginx-ui/server/models"
 	_ "nginx-ui/server/routers"
+	"nginx-ui/server/utils"
+	"os"
 )
 
+func ensureIndexHtml() {
+	if !utils.IsExist("views") {
+		os.Mkdir("views", 0777)
+	}
+	sourceDir := utils.GetStaticDir()
+	utils.CopyFile(sourceDir+"/index.html", "views/index.html")
+}
+
 func init() {
 	fmt.Printf("-------init---")
 	gob.Register(models.User{})
@@ -17,4 +27,5 @@ func init() {
 	config.InitAdmin()
 	fmt.Println("init success")
 	ensureRoutes()
+	ensureIndexHtml()
 }

+ 2 - 12
server/routers/router.go

@@ -87,25 +87,15 @@ func init() {
 
 	beego.Router(fmt.Sprintf("%s/config.js", config.ContextPath), &nginx_controller.ConfigController{})
 	// portal static assets
-	if utils.IsExist("static/web") {
-		beego.SetStaticPath(config.ContextPath, "static/web")
-	} else if utils.IsExist("frontend/dist") {
-		beego.SetStaticPath(config.ContextPath, "frontend/dist")
-	} else {
-		beego.SetStaticPath(config.ContextPath, "static/web")
-		logs.Warn("no static web dir for static/web or frontend/dist, please check")
-	}
+	beego.SetStaticPath(config.ContextPath, utils.GetStaticDir())
 	//
 	webPrefix := config.ContextPath
 	if !strings.HasSuffix(webPrefix, "/") {
 		webPrefix = webPrefix + "/"
 	}
-
 	beego.Get("/", func(ctx *context.Context) {
 		ctx.Redirect(301, webPrefix)
 	})
 
-	beego.ErrorController(&base.ErrorController{
-		WebPath: webPrefix,
-	})
+	beego.ErrorController(&base.ErrorController{})
 }

+ 18 - 0
server/utils/file.go

@@ -5,6 +5,7 @@ import (
 	"crypto/md5"
 	"encoding/hex"
 	"github.com/mholt/archiver/v4"
+	"io"
 	"os"
 	"path/filepath"
 	"strings"
@@ -59,3 +60,20 @@ func CalcMd5(content string) string {
 	pass := md5.Sum([]byte(content))
 	return hex.EncodeToString(pass[:])
 }
+
+func CopyFile(src, dst string) (err error) {
+	in, err := os.Open(src)
+	if err != nil {
+		return
+	}
+	defer in.Close()
+
+	out, err := os.Create(dst)
+	if err != nil {
+		return
+	}
+	defer out.Close()
+
+	_, err = io.Copy(out, in)
+	return
+}

+ 18 - 0
server/utils/web.go

@@ -0,0 +1,18 @@
+package utils
+
+import (
+	"github.com/astaxie/beego/logs"
+)
+
+func GetStaticDir() string {
+
+	if IsExist("static/web") {
+		return "static/web"
+
+	} else if IsExist("frontend/dist") {
+		return "frontend/dist"
+	} else {
+		logs.Warn("no static web dir for static/web or frontend/dist, please check")
+	}
+	return "static/web"
+}

Some files were not shown because too many files changed in this diff