【unity进阶知识10】从零手搓一个UI管理器/UI框架,自带一个提示界面

server/2024/10/20 4:00:52/

最终效果

在这里插入图片描述

文章目录

  • 最终效果
  • 前言
  • UI管理器
    • 1、新增UI面板层枚举
    • 2、初始化
      • 2.1、用代码创建画布
      • 2.2、用代码创建UI面板的父物体层
      • 2.3、代码添加EventSystem物体
    • 3、ShowPanel显示面板方法
    • 4、HidePanel隐藏面板的方法
    • 5、CloseUI关闭界面的方法
    • 6、UI界面基类
  • 测试调用
  • 优化绑定按钮事件
  • 新增提示框
    • UI绘制
    • 实现
  • 使用DOTween实现动画效果
    • 提示框动画
    • 打开UI面板动画
    • 关闭UI面板动画
  • 完整代码
  • 完结

前言

unity在4.6版本之后,引入了自己的界面显示系统,全称unity graphic user interface,即我们所熟知的ugui
在这里插入图片描述毕竟是unity的亲儿子,这个系统一经推出,就与其灵活快速可视化,迅速抢占用户市场,逐渐成为unity ui的主流系统,但是它也并不是完美的,对于开发人员来说,使用这套系统往往需要面对如下困境,比如缺乏跨场景的u管理器,界面的上下层关系紊乱三,界面之间的通信手段贫乏等等,上述几个问题大大影响到我们的开发效率。
在这里插入图片描述
针对上述问题,我们可以选择制作一套UI管理器解决。

UI管理器

1、新增UI面板层枚举

/// <summary>
/// UI面板层枚举
/// </summary>
public enum E_UIPanelLayer
{None,Rearmost,//最后方Rear,//后方Middle,//中间Front,//前方Forefront//最前方
}

2、初始化

2.1、用代码创建画布

/// <summary>
/// 创建画布
/// </summary>
void CreateCanvas()
{//改LayergameObject.layer = LayerMask.NameToLayer("UI");//添加并设置Canvas组件Canvas canvas = gameObject.AddComponent<Canvas>();canvas.renderMode = RenderMode.ScreenSpaceOverlay;canvas.sortingOrder = 30000;//添加并设置CanvasScaler组件CanvasScaler canvasScaler = gameObject.AddComponent<CanvasScaler>();canvasScaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;canvasScaler.referenceResolution = new Vector2(Screen.width, Screen.height);//横版屏幕设置为1 竖版屏幕设置为0canvasScaler.matchWidthOrHeight = Screen.width > Screen.height ? 1 : 0;//添加Graphic Raycaster组件gameObject.AddComponent<GraphicRaycaster>();
}

效果
在这里插入图片描述

2.2、用代码创建UI面板的父物体层

// 用于记录每个层的父物体
private Dictionary<E_UIPanelLayer, Transform> layerParents;/// <summary>
/// 创建UI面板的父物体层
/// </summary>
void CreateLayer()
{//Rearmost层的父物体Transform rearmost = new GameObject(E_UIPanelLayer.Rearmost.ToString()).transform;rearmost.SetParent(transform, false);//Rear层的父物体Transform rear = new GameObject(E_UIPanelLayer.Rear.ToString()).transform;rear.SetParent(transform, false);//Middle层的父物体Transform middle = new GameObject(E_UIPanelLayer.Middle.ToString()).transform;middle.SetParent(transform, false);//Fornt层的父物体Transform front = new GameObject(E_UIPanelLayer.Front.ToString()).transform;front.SetParent(transform, false);//Frontmost层的父物体Transform foreFront = new GameObject(E_UIPanelLayer.ForeFront.ToString()).transform;foreFront.SetParent(transform, false);//记录每个层的父物体layerParents = new Dictionary<E_UIPanelLayer, Transform>{{ E_UIPanelLayer.Rearmost, rearmost },{ E_UIPanelLayer.Rear, rear },{ E_UIPanelLayer.Middle, middle },{ E_UIPanelLayer.Front, front },{ E_UIPanelLayer.ForeFront, foreFront }};
}

效果
在这里插入图片描述

2.3、代码添加EventSystem物体

/// <summary>
/// 创建EventSystem
/// </summary>
void CreateEventSystem()
{//如果场景中已经有一个EventSystem了,则直接返回。if (FindObjectOfType<EventSystem>()) return;GameObject eventSystem = new GameObject("EventSystem");DontDestroyOnLoad(eventSystem);//切换场景不销毁eventSystem.AddComponent<EventSystem>();eventSystem.AddComponent<StandaloneInputModule>();
}

效果
在这里插入图片描述

3、ShowPanel显示面板方法

//存储加载过的界面的集合
private List<UIBase> uiList = new List<UIBase>();/// <summary>
/// 显示面板
/// </summary>
/// <typeparam name="T">UI面板脚本,记得UI面板预制体名要和脚本名一样</typeparam>
/// <param name="layer">父级层</param>
/// <returns>UIBase</returns>
public UIBase ShowUI<T>(E_UIPanelLayer layer = E_UIPanelLayer.Middle) where T : UIBase
{string uiName = typeof(T).Name;//获取名称UIBase ui = Find(uiName);if (ui == null){//记录该面板要放进哪个层中来显示Transform parent = layerParents[layer];//集合中没有 需要从Resources/UI文件夹加载GameObject obj = Instantiate(Resources.Load("UI/" + uiName), parent) as GameObject;//改名字,默认实例化会加上(clone),所以得重命名obj.name = uiName;//添加需要的脚本ui = obj.AddComponent<T>();//添加到集合进行存储uiList.Add(ui);}else{//显示ui.Show();}return ui;
}

调用
在这里插入图片描述

4、HidePanel隐藏面板的方法

/// <summary>
/// 隐藏面板
/// </summary>
/// <param name="uiName">面板名</param>
public void HideUI(string uiName)
{UIBase ui = Find(uiName);if (ui != null){ui.Hide();}
}

调用
在这里插入图片描述

5、CloseUI关闭界面的方法

/// <summary>
/// 关闭某个界面
/// </summary>
/// <param name="uiName">面板名</param>
public void CloseUI(string uiName)
{UIBase ui = Find(uiName);if (ui != null){uiList.Remove(ui);Destroy(ui.gameObject);}
}

6、UI界面基类

/// <summary>
/// UI界面基类
/// </summary>
public class UIBase : MonoBehaviour
{//显示public virtual void Show(){gameObject.SetActive(true);}//隐藏public virtual void Hide(){gameObject.SetActive(false);}//关闭界面(销毁)public virtual void Close(){UIManager.Instance.CloseUI(gameObject.name);}
}

测试调用

欢迎面板
在这里插入图片描述

新增WelcomeUI.cs测试面板代码,注意记得继承UIBase基类

public class WelcomeUI : UIBase {void Awake(){//绑定按钮事件transform.Find("bg/退出按钮").GetComponent<Button>().onClick.AddListener(onCloseBtn);transform.Find("bg/确定").GetComponent<Button>().onClick.AddListener(onConfirmBtn);}void onCloseBtn(){//关闭界面Close();}void onConfirmBtn(){//隐藏Hide();}
}

新增UITest ,绘制按钮显示WelcomeUI欢迎面板

public class UITest : MonoBehaviour {private void OnGUI(){// 创建一个新的 GUIStyleGUIStyle buttonStyle = new GUIStyle(GUI.skin.button);// 设置字体大小buttonStyle.fontSize = 50; // 替换为你想要的字体大小buttonStyle.alignment = TextAnchor.MiddleCenter; // 可选择设置对齐方式if (GUI.Button(new Rect(0, 0, 500, 200), "显示欢迎面板", buttonStyle)){//显示WelcomeUI面板,创建的脚本名字记得跟预制体物体名字一致UIManager.Instance.ShowUI<WelcomeUI>("WelcomeUI");}}
}

效果
在这里插入图片描述

优化绑定按钮事件

每次绑定按钮事件都需要写这么多代码很麻烦,我们可以继续进行封装

新增UIEventTrigger事件监听代码

/// <summary>
/// 事件监听
/// </summary>
public class UIEventTrigger : MonoBehaviour, IPointerClickHandler
{//这是一个公共的委托,它接受两个参数,一个是被点击的游戏对象,另一个是关于点击事件的数据。public Action<GameObject, PointerEventData> onClick;//用于获取或添加 UIEventTrigger 组件public static UIEventTrigger Get(GameObject obj){UIEventTrigger trigger = obj.GetComponent<UIEventTrigger>();if (trigger == null){trigger = obj.AddComponent<UIEventTrigger>();}return trigger;}public void OnPointerClick(PointerEventData eventData){//这是 IPointerClickHandler 接口的方法,当 UI 元素被点击时,它将被调用。if (onClick != null) onClick(gameObject, eventData);}
}

修改UI界面基类UIBase

//注册事件
public UIEventTrigger Register(string name)
{Transform tf = transform.Find(name);return UIEventTrigger.Get(tf.gameObject);
}

测试调用

public class WelcomeUI : UIBase {void Awake(){//绑定按钮事件// transform.Find("bg/退出按钮").GetComponent<Button>().onClick.AddListener(onCloseBtn);// transform.Find("bg/确定").GetComponent<Button>().onClick.AddListener(onConfirmBtn);Register("bg/退出按钮").onClick = onCloseBtn;Register("bg/确定").onClick = onConfirmBtn;}void onCloseBtn(GameObject obj, PointerEventData pData){//关闭界面Close();}void onConfirmBtn(GameObject obj, PointerEventData pData){//隐藏Hide();}
}

效果,和前面一样
在这里插入图片描述

新增提示框

UI绘制

背景图片
在这里插入图片描述
配置
在这里插入图片描述

实现

修改UIManager

/// <summary>
/// 提示界面
/// </summary>
/// <param name="msg">文本</param>
/// <param name="color">颜色</param>
/// <param name="callback">完成回调事件</param>
public void ShowTips(string msg, Color color, float showTime = 0.5f, UnityAction callback = null, E_UIPanelLayer layer = E_UIPanelLayer.ForeFront)
{UIBase ui = ShowUI<TipsUI>(layer);TextMeshProUGUI text = ui.transform.Find("bg/text").GetComponent<TextMeshProUGUI>();text.color = color;text.text = msg;
}

测试调用

UIManager.Instance.ShowTips("成功!", Color.green);

效果
在这里插入图片描述

使用DOTween实现动画效果

参考:【推荐100个unity插件之2】DoTween动画插件的安装和使用整合(最全)

提示框动画

实现面板先经过0.4sY轴缩放从0变为1,再暂停showTime秒后,经过0.4sY轴缩放从1变回0,动画播放完成调用callback事件

/// <summary>
/// 提示界面
/// </summary>
/// <param name="msg">文本</param>
/// <param name="color">颜色</param>
/// <param name="showTime">显示时间</param>
/// <param name="callback">完成回调事件</param>
public void ShowTips(string msg, Color color, float showTime = 0.5f, UnityAction callback = null, E_UIPanelLayer layer = E_UIPanelLayer.ForeFront)
{DOTween.CompleteAll(true);UIBase ui = ShowUI<TipsUI>("TipsUI", layer);TextMeshProUGUI text = ui.transform.Find("bg/text").GetComponent<TextMeshProUGUI>();text.color = color;text.text = msg;// DOTween动画 方法一// Sequence sequence = DOTween.Sequence();// sequence.Append(ui.transform.DOScaleY(1, 0.4f).From(0)) // 第一个动画,缩放到 1// .Append(DOVirtual.DelayedCall(showTime, () => { })) // 延迟// .Append(ui.transform.DOScaleY(0, 0.4f).From(1)) // 第二个动画,缩放到 0// .OnComplete(() =>// {//     ui.gameObject.SetActive(false); // 隐藏 UI//     callback?.Invoke(); // 调用回调// });// DOTween动画 方法二ui.transform.DOScaleY(1, 0.4f).From(0).OnComplete(() =>{// 延迟显示时间DOVirtual.DelayedCall(showTime, () =>{ui.transform.DOScaleY(0, 0.4f).From(1).OnComplete(() =>{ui.gameObject.SetActive(false);callback?.Invoke();});});});
}

效果
在这里插入图片描述

打开UI面板动画

修改UIManager里的ShowUI方法,新增DOTween代码即可

ui.transform.DOScale(Vector3.one, 0.5f).From(Vector3.zero);

效果
在这里插入图片描述

关闭UI面板动画

//动画
CanvasGroup canvasGroup = ui.transform.GetComponent<CanvasGroup>();
Sequence closeSequence = DOTween.Sequence();
closeSequence.Append(canvasGroup.DOFade(0, 0.8f)) // 淡出
.Join(ui.transform.DOLocalMoveX(2000f, 0.8f)) // 同时移动
.OnComplete(() =>
{ui.gameObject.SetActive(false);
});

效果
在这里插入图片描述

完整代码

UIEventTrigger.cs

/// <summary>
/// 事件监听
/// </summary>
public class UIEventTrigger : MonoBehaviour, IPointerClickHandler
{//这是一个公共的委托,它接受两个参数,一个是被点击的游戏对象,另一个是关于点击事件的数据。public Action<GameObject, PointerEventData> onClick;//用于获取或添加 UIEventTrigger 组件public static UIEventTrigger Get(GameObject obj){UIEventTrigger trigger = obj.GetComponent<UIEventTrigger>();if (trigger == null){trigger = obj.AddComponent<UIEventTrigger>();}return trigger;}public void OnPointerClick(PointerEventData eventData){//这是 IPointerClickHandler 接口的方法,当 UI 元素被点击时,它将被调用。if (onClick != null) onClick(gameObject, eventData);}
}

UIBase.cs

/// <summary>
/// UI界面基类
/// </summary>
public class UIBase : MonoBehaviour
{//显示public virtual void Show(){transform.localPosition = Vector3.zero;gameObject.SetActive(true);}//隐藏public virtual void Hide(){UIManager.Instance.HideUI(gameObject.name);}//关闭界面(销毁)public virtual void Close(){UIManager.Instance.CloseUI(gameObject.name);}//注册事件public UIEventTrigger Register(string name){Transform tf = transform.Find(name);return UIEventTrigger.Get(tf.gameObject);}
}

UIManager.cs

/// <summary>
/// UI管理器
/// </summary>
public class UIManager : SingletonMono<UIManager>
{// 用于记录每个层的父物体private Dictionary<E_UIPanelLayer, Transform> layerParents;//存储加载过的界面的集合private List<UIBase> uiList = new List<UIBase>();#region 初始化void Awake(){//创建画布CreateCanvas();//创建UI面板的父物体层CreateLayer();//创建EventSystemCreateEventSystem();}/// <summary>/// 创建画布/// </summary>void CreateCanvas(){//改LayergameObject.layer = LayerMask.NameToLayer("UI");//添加并设置Canvas组件Canvas canvas = gameObject.AddComponent<Canvas>();canvas.renderMode = RenderMode.ScreenSpaceOverlay;canvas.sortingOrder = 30000;//添加并设置CanvasScaler组件CanvasScaler canvasScaler = gameObject.AddComponent<CanvasScaler>();canvasScaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;canvasScaler.referenceResolution = new Vector2(Screen.width, Screen.height);//横版屏幕设置为1 竖版屏幕设置为0canvasScaler.matchWidthOrHeight = Screen.width > Screen.height ? 1 : 0;//添加Graphic Raycaster组件gameObject.AddComponent<GraphicRaycaster>();}/// <summary>/// 创建UI面板的父物体层/// </summary>void CreateLayer(){//Rearmost层的父物体Transform rearmost = new GameObject(E_UIPanelLayer.Rearmost.ToString()).transform;rearmost.SetParent(transform, false);//Rear层的父物体Transform rear = new GameObject(E_UIPanelLayer.Rear.ToString()).transform;rear.SetParent(transform, false);//Middle层的父物体Transform middle = new GameObject(E_UIPanelLayer.Middle.ToString()).transform;middle.SetParent(transform, false);//Fornt层的父物体Transform front = new GameObject(E_UIPanelLayer.Front.ToString()).transform;front.SetParent(transform, false);//Frontmost层的父物体Transform foreFront = new GameObject(E_UIPanelLayer.ForeFront.ToString()).transform;foreFront.SetParent(transform, false);//记录每个层的父物体layerParents = new Dictionary<E_UIPanelLayer, Transform>{{ E_UIPanelLayer.Rearmost, rearmost },{ E_UIPanelLayer.Rear, rear },{ E_UIPanelLayer.Middle, middle },{ E_UIPanelLayer.Front, front },{ E_UIPanelLayer.ForeFront, foreFront }};}/// <summary>/// 创建EventSystem/// </summary>void CreateEventSystem(){//如果场景中已经有一个EventSystem了,则直接返回。if (FindObjectOfType<EventSystem>()) return;GameObject eventSystem = new GameObject("EventSystem");DontDestroyOnLoad(eventSystem);//切换场景不销毁eventSystem.AddComponent<EventSystem>();eventSystem.AddComponent<StandaloneInputModule>();}#endregion/// <summary>/// 显示面板/// </summary>/// <typeparam name="T">UI面板脚本,记得UI面板预制体名要和脚本名一样</typeparam>/// <param name="layer">父级层</param>/// <param name="doTween">是否使用doTween动画</param>/// <returns>UIBase</returns>public UIBase ShowUI<T>(E_UIPanelLayer layer = E_UIPanelLayer.Middle, bool doTween = true) where T : UIBase{DOTween.CompleteAll(true);string uiName = typeof(T).Name;//获取名称UIBase ui = Find(uiName);if (ui == null){//记录该面板要放进哪个层中来显示Transform parent = layerParents[layer];//集合中没有 需要从Resources/UI文件夹加载GameObject obj = Instantiate(Resources.Load("UI/" + uiName), parent) as GameObject;//改名字,默认实例化会加上(clone),所以得重命名obj.name = uiName;//添加需要的脚本ui = obj.AddComponent<T>();//添加CanvasGroup组件,用于后面渐变使用obj.AddComponent<CanvasGroup>();//添加到集合进行存储uiList.Add(ui);}else{//显示ui.Show();}//透明度设置为1CanvasGroup canvasGroup = ui.transform.GetComponent<CanvasGroup>();canvasGroup.alpha = 1f;//动画if (doTween) ui.transform.DOScale(Vector3.one, 0.5f).From(Vector3.zero);return ui;}/// <summary>/// 隐藏面板/// </summary>/// <param name="uiName">面板名</param>public void HideUI(string uiName, bool doTween = true){DOTween.CompleteAll(true);UIBase ui = Find(uiName);if (ui == null) return;if (doTween){//动画CanvasGroup canvasGroup = ui.transform.GetComponent<CanvasGroup>();Sequence closeSequence = DOTween.Sequence();closeSequence.Append(canvasGroup.DOFade(0, 0.8f)) // 淡出.Join(ui.transform.DOLocalMoveX(2000f, 0.8f)) // 同时移动.OnComplete(() =>{ui.gameObject.SetActive(false);});}else{ui.gameObject.SetActive(false);}}/// <summary>/// 关闭某个界面/// </summary>/// <param name="uiName">面板名</param>public void CloseUI(string uiName, bool doTween = true){DOTween.CompleteAll(true);UIBase ui = Find(uiName);if (ui == null) return;if (doTween){//动画CanvasGroup canvasGroup = ui.transform.GetComponent<CanvasGroup>();Sequence closeSequence = DOTween.Sequence();closeSequence.Append(canvasGroup.DOFade(0, 0.8f)) // 淡出.Join(ui.transform.DOLocalMoveX(2000f, 0.8f)) // 同时移动.OnComplete(() =>{uiList.Remove(ui);Destroy(ui.gameObject);});}else{uiList.Remove(ui);Destroy(ui.gameObject);}}//关闭所有界面public void CloseAllUI(){for (int i = uiList.Count - 1; i >= 0; i--){Destroy(uiList[i].gameObject);}uiList.Clear();//清空合集}/// <summary>/// 从集合中找到名字对应的界面脚本/// </summary>/// <param name="uiName">面板名</param>/// <returns>UIBase</returns>public UIBase Find(string uiName){for (int i = 0; i < uiList.Count; i++){if (uiList[i].name == uiName) return uiList[i];}return null;}/// <summary>/// 提示界面/// </summary>/// <param name="msg">文本</param>/// <param name="color">颜色</param>/// <param name="showTime">显示时间</param>/// <param name="callback">完成回调事件</param>public void ShowTips(string msg, Color color, float showTime = 0.5f, UnityAction callback = null, E_UIPanelLayer layer = E_UIPanelLayer.ForeFront){DOTween.CompleteAll(true);UIBase ui = ShowUI<TipsUI>(layer, false);TextMeshProUGUI text = ui.transform.Find("bg/text").GetComponent<TextMeshProUGUI>();text.color = color;text.text = msg;//动画Sequence sequence = DOTween.Sequence();sequence.Append(ui.transform.DOScaleY(1, 0.4f).From(0)) // 第一个动画,缩放到 1.Append(DOVirtual.DelayedCall(showTime, () => { })) // 延迟.Append(ui.transform.DOScaleY(0, 0.4f).From(1)) // 第二个动画,缩放到 0.OnComplete(() =>{ui.gameObject.SetActive(false); // 隐藏 UIcallback?.Invoke(); // 调用回调});}
}

完结

赠人玫瑰,手有余香!如果文章内容对你有所帮助,请不要吝啬你的点赞评论和关注,你的每一次支持都是我不断创作的最大动力。当然如果你发现了文章中存在错误或者有更好的解决方法,也欢迎评论私信告诉我哦!

好了,我是向宇,https://xiangyu.blog.csdn.net

一位在小公司默默奋斗的开发者,闲暇之余,边学习边记录分享,站在巨人的肩膀上,通过学习前辈们的经验总是会给我很多帮助和启发!如果你遇到任何问题,也欢迎你评论私信或者加群找我, 虽然有些问题我也不一定会,但是我会查阅各方资料,争取给出最好的建议,希望可以帮助更多想学编程的人,共勉~
在这里插入图片描述


http://www.ppmy.cn/server/128598.html

相关文章

LLM端侧部署系列 | 手机上运行47B大模型?上交推理框架PowerInfer-2助力AI手机端侧部署

0. 引言 黄梅时节家家雨&#xff0c;青草池塘处处蛙。 有约不来过夜半&#xff0c;闲敲棋子落灯花。 当下&#xff0c;在移动设备上部署大型模型的趋势是愈演愈烈。Google推出了AI Core&#xff0c;使得Gemini Nano可以在智能手机上部署。此外&#xff0c;近期传闻苹果在iOS …

C++并发编程实战—单例模式与线程池实现

线程池 C线程池是一种用于管理和复用线程的机制&#xff0c;它可以提高程序的性能和效率&#xff0c;特别是在处理大量并发任务时。以下是C线程池的具体细节&#xff1a; 一、定义与功能 定义&#xff1a;线程池是一种设计模式&#xff0c;它预先创建并维护一定数量的线程&a…

教程:在Linux上启动、运行、杀掉和管理项目程序

笔记 1. 启动并运行一个项目程序 假设你的项目程序是一个可执行文件 my_project&#xff0c;位于 /data 目录下。 cd /data ./my_project 2. 杀掉一个正在运行的项目程序 首先&#xff0c;找到程序的进程ID (PID)。 ps aux | grep my_project 找到对应的PID&#xff0c;然后…

用HTML5+CSS+JavaScript庆祝国庆

用HTML5CSSJavaScript庆祝国庆 中华人民共和国的国庆日是每年的10月1日。 1949年10月1日&#xff0c;中华人民共和国中央人民政府成立&#xff0c;在首都北京天安门广场举行了开国大典&#xff0c;中央人民政府主席毛泽东庄严宣告中华人民共和国成立&#xff0c;并亲手升起了…

十大时间序列预测模型

目录 1. 自回归模型 原理 核心公式 推导过程: 完整案例 2. 移动平均模型 原理 核心公式 推导过程: 完整案例 3. 自回归移动平均模型 原理 核心公式 推导过程: 完整案例 4. 自回归积分移动平均模型 原理 核心公式 推导过程 完整案例 5. 季节性自回归积分…

Koa学习

Koa 安装与配置 1. 初始化项目 在终端中执行以下命令&#xff1a; # 创建项目文件夹 mkdir koa cd koa# 初始化并安装依赖 npm init -y npm install koa npm install nodemon --save-dev2. 修改 package.json 在 package.json 文件中进行如下修改&#xff1a; {"type…

STL的位图:bitset

引言 在C标准模板库&#xff08;STL&#xff09;中&#xff0c;bitset是一种用于表示固定大小序列的位集合的容器。每个位&#xff08;bit&#xff09;可以被独立地设置或清除&#xff0c;即它可以单独地表示0或1。bitset在处理二进制数据时非常有用&#xff0c;尤其是在需要节…

Docker 实践与应用举例

一、容器化Web应用&#xff1a; 创建一个Docker容器来运行一个简单的Web应用&#xff0c;例如一个基于Node.js的Express应用。首先&#xff0c;编写Dockerfile来定义容器的构建过程&#xff0c;然后使用Docker命令来构建和运行容器。 使用Docker Compose来定义和管理多个容器组…