软件开发
快来分享你的内容吧~
2023-03-28·后端
【鸿蒙实战开发】基于子窗口实现应用内悬浮窗
## **场景描述** app应用会使用悬浮窗/悬浮球的方式来给用户展示一些应用重要&便捷功能的入口,类似android和iOS应用中常见的应用内可拖拽的悬浮球和小窗口视频悬浮窗,点击悬浮窗修改悬浮窗样式和响应事件跳转页面,在跳转页面后依然可以显示在屏幕中上个页面拖拽后的固定位置等。 应用经常会遇到如下的业务诉求: **场景一**:通过事件添加和移除悬浮窗,悬浮窗样式可定制(暂定两种,无白边圆球形和小视频播放窗口类型),可代码修改位置和布局。 **场景二**:创建悬浮窗后,主窗口的系统侧滑返回事件可正常使用。 **场景三**:可响应正常点击事件,可通过触发拖动使悬浮窗的移动,根据最后手势停留位置,做动画靠屏幕左或靠右显示,跳转和返回上级页面后悬浮窗依然存在,且相对手机屏幕位置不变。 **场景四**:悬浮窗内组件事件触发主窗口的页面跳转(Router和Navigation两种都要有)。 **场景五**:悬浮窗的窗口大小自适应组件,子窗口中页面设置了宽高,需要让子窗口自适应页面组件大小。 **场景六**:支持控制悬浮窗隐藏和销毁。 **场景七**:视频类应用主动调用画中画完成后台播放,以及返回桌面时自动启动画中画。 ## **方案描述** ### **场景一:** 通过事件添加和移除悬浮窗,悬浮窗样式可定制(暂定两种,无白边圆球形和小视频播放窗口类型),可代码修改位置和布局。 **效果图** <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/GOIBgfx6QwEJeoqZ.webp" alt="" width="432px" /> **方案** 通过子窗口创建windowStage.*createSubWindow*('mySubWindow'),和windowClass.setWindowLayoutFullScreen去除白边。 **核心代码** ``` 在EntryAbility中获取WindowStage。 onWindowStageCreate(windowStage: window.WindowStage): void { // Main window is created, set main page for this ability hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageCreate'); windowStage.loadContent('pages/Page', (err, data) => { if (err.code) { hilog.error(0x0000, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err) ?? ''); return; } // 保存窗口管理器 AppStorage.setOrCreate("windowStage", windowStage); hilog.info(0x0000, 'testTag', 'Succeeded in loading the content. Data: %{public}s', JSON.stringify(data) ?? ''); }); } ``` 创建子窗口,子窗口样式由子窗口加载的页面组件样式决定。 ``` this.windowStage.createSubWindow("mySubWindow", (err, windowClass) => { if (err.code > 0) { console.error("failed to create subWindow Cause:" + err.message) return; } try { // 设置子窗口加载页 windowClass.setUIContent("pages/MySubWindow", () => { windowClass.setWindowBackgroundColor("#00000000") }); // 设置子窗口左上角坐标 windowClass.moveWindowTo(0, 200) // 设置子窗口大小 windowClass.resize(vp2px(75), vp2px(75)) // 展示子窗口 windowClass.showWindow(); // 设置子窗口全屏化布局不避让安全区 windowClass.setWindowLayoutFullScreen(true); } catch (err) { console.error("failed to create subWindow Cause:" + err) } }) ``` ## **场景二:** 创建悬浮窗后,主窗口的系统侧滑返回事件可正常使用。 **效果图** <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/Pa5H5hVZkhFt3gjN.webp" alt="" width="424px" /> **方案** 通过window.*shiftAppWindowFocus*转移窗口焦点实现创建子窗口后,主窗口依然可以响应事件。**核心代码** 在子窗口中将焦点转移到主窗口。 ``` onPageShow(): void { setTimeout(() => { // 获取子窗口ID let subWindowID: number = window.findWindow("mySubWindow").getWindowProperties().id // 获取主窗口ID let mainWindowID: number = this.windowStage.getMainWindowSync().getWindowProperties().id // 将焦点从子窗口转移到主窗口 window.shiftAppWindowFocus(subWindowID, mainWindowID) }, 500) } ``` ### **场景三:** 可响应正常点击事件,可通过拖动触发悬浮窗的拖拽移动,根据最后手势停留位置,做动画靠屏幕左或靠右显示,跳转和返回上级页面后悬浮窗依然存在,且相对手机屏幕位置不变。 **效果图** <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/Rg2iQeca6WtrhAbU.webp" alt="" width="424px" /> **方案** 通过设置手势顺序模式识别PanGesture,实现拖拽悬浮窗。 **核心代码** 创建Position。 ``` interface Position { x: number, y: number } ``` 设置拖拽选项。 ``` private panOption: PanGestureOptions = new PanGestureOptions({ direction: PanDirection.All }); ``` 通过在子窗口父组件绑定拖拽动作完成悬浮窗坐标移动。 ``` .gesture( // 声明该组合手势的类型为Sequence类型 PanGesture(this.panOption) .onActionStart((event: GestureEvent) => { console.info('Pan start'); })// 发生拖拽时,获取到触摸点的位置,并将位置信息传递给windowPosition .onActionUpdate((event: GestureEvent) => { this.windowPosition.x += event.offsetX; this.windowPosition.y += event.offsetY; this.subWindow.moveWindowTo(this.windowPosition.x, this.windowPosition.y) }) .onActionEnd((event: GestureEvent) => { // 贴边判断 if (event.offsetX > 0) { this.windowPosition.x = display.getDefaultDisplaySync().width - this.subWindow.getWindowProperties() .windowRect .width; } else if (event.offsetX < 0) { this.windowPosition.x = 0; } this.subWindow.moveWindowTo(this.windowPosition.x, this.windowPosition.y) console.info('Pan end'); }) ) ``` ## **场景四:** 悬浮窗内组件事件触发主窗口的页面跳转(Router和Navigation两种都要有)。 <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/3u0f6xjmS4y7fu6R.webp" alt="" width="424px" /> <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/RdtQFOpK3bxXUCI4.webp" alt="" width="424px" /> **方案** 通过获取窗口上下文,实现在悬浮窗点击后,实现主窗口Router跳转。 通过配置NavPathStack全局变量,实现主窗口navigation跳转 。 **核心代码** 通过windowStage获取主窗口的Router,实现主窗口的Router跳转。 ``` .onClick((event: ClickEvent) => { this.windowStage.getMainWindowSync() .getUIContext() .getRouter() .back() }) ``` 通过AppStorage获取NavPathStack,实现主窗口navigation跳转。 ``` .onClick((event: ClickEvent) => { let navPath = AppStorage.get("pageInfos") as NavPathStack; navPath.pushPath({ name: 'pageOne' }) }) ``` ## **场景五:** 悬浮窗的窗口大小自适应组件,子窗口中页面设置了宽高,需要让子窗口自适应页面组件大小。 **效果图** <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/f1lyPCtcfzf5OwTh.webp" alt="" width="424px" /> **方案** 通过监听通用事件ComponentObserver,设置window的resize调整窗口大小。 **核心代码** 查找子窗口。 ``` @State subWindow: window.Window = window.findWindow("mySubWindow"); ``` 注册监听事件。 ``` //监听id为COMPONENT_ID的组件回调事件listener: inspector.ComponentObserver = inspector.createComponentObserver('COMPONENT_ID'); 通过onClick()事件,实现对组件变化的监听。 if (this.flag) { Image($r("app.media.voice2")) .id("COMPONENT_ID") .borderRadius(5) .width(75) .height(75) .onClick(() => { // 设置图标切换标识 this.flag = !this.flag this.listener.on('layout', () => { // 监听布局变更后调整子窗大小 this.subWindow.resize(componentUtils.getRectangleById("COMPONENT_ID").size.width, componentUtils.getRectangleById("COMPONENT_ID").size.height) }) }) } else { Image($r("app.media.voice")) .id("COMPONENT_ID") .borderRadius(50) .width(100) .height(100) .onClick(() => { this.flag = !this.flag this.listener.on('layout', () => { this.subWindow.resize(componentUtils.getRectangleById("COMPONENT_ID").size.width, componentUtils.getRectangleById("COMPONENT_ID").size.height) }) }) ``` ## **场景六:** 支持控制悬浮窗隐藏和销毁。 **效果图** <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/aaaFXKX3ywLhVjx3.webp" alt="" width="424px" /> **方案** 通过设置窗口windowClass.minimize和windowClass.destroyWindow,实现悬浮窗的隐藏和销毁。 **核心代码** 通过调用minimize,实现子窗口最小化。 ``` .onClick((event: ClickEvent) => { this.subWindow.minimize() }) ``` 通过实现destroyWindow,实现子窗口的资源销毁。 ``` // 通过查找子窗口名称对子窗口进行销毁 window.findWindow("mySubWindow").destroyWindow() ``` **场景七:** 视频类应用主动调用画中画完成后台播放,以及返回桌面时自动启动画中画。 **效果图** <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/YdI2gwWdE6sLoFvF.webp" alt="" width="424px" /> **方案** 1.通过pipController.startPiP()完成主动调用画中画功能。 2.通过pipController.setAutoStartEnabled(true)在返回桌面时完成全局画中画播放。 **核心代码** 创建XComponent组件。 ``` XComponent({ id: 'pipDemo', type: 'surface', controller: this.mXComponentController }) .onLoad(() => { this.surfaceId = this.mXComponentController.getXComponentSurfaceId(); // 需要设置AVPlayer的surfaceId为XComponentController的surfaceId this.player = new AVPlayerDemo(this.surfaceId); this.player.avPlayerFdSrcDemo(); }) .onDestroy(() => { console.info(`[${TAG}] XComponent onDestroy`); }) .size({ width: '100%', height: '800px' }) 创建pipWindowController和startPip方法。 startPip() { if (!pipWindow.isPiPEnabled()) { console.error(`picture in picture disabled for current OS`); return; } let config: pipWindow.PiPConfiguration = { context: getContext(this), componentController: this.mXComponentController, // 当前page导航id navigationId: this.navId, // 对于视频通话、视频会议等场景,需要设置相应的模板类型 templateType: pipWindow.PiPTemplateType.VIDEO_PLAY, // 可选,创建画中画控制器时系统可通过XComponent组件大小设置画中画窗口比例 contentWidth: 800, // 可选,创建画中画控制器时系统可通过XComponent组件大小设置画中画窗口比例 contentHeight: 600, }; // 步骤1:创建画中画控制器,通过create接口创建画中画控制器实例 let promise: Promise<pipWindow.PiPController> = pipWindow.create(config); promise.then((controller: pipWindow.PiPController) => { this.pipController = controller; // 步骤1:初始化画中画控制器 this.initPipController(); // 步骤2:通过startPiP接口启动画中画 this.pipController.startPiP().then(() => { console.info(`Succeeded in starting pip.`); }).catch((err: BusinessError) => { console.error(`Failed to start pip. Cause:${err.code}, message:${err.message}`); }); }).catch((err: BusinessError) => { console.error(`Failed to create pip controller. Cause:${err.code}, message:${err.message}`); }); } ``` 初始化pipWindowController。 ``` initPipController() { if (!this.pipController) { return; } // 通过setAutoStartEnabled接口设置是否需要在应用返回桌面时自动启动画中画,注册stateChange和controlPanelActionEvent回调 this.pipController.setAutoStartEnabled(true/*or true if necessary*/); // 默认为false this.pipController.on('stateChange', (state: pipWindow.PiPState, reason: string) => { this.onStateChange(state, reason); }); this.pipController.on('controlPanelActionEvent', (event: pipWindow.PiPActionEventType) => { this.onActionEvent(event); }); } ``` 完成画中画播放使用stopPip方法停止。 ``` stopPip() { if (this.pipController) { let promise: Promise<void> = this.pipController.stopPiP(); promise.then(() => { console.info(`Succeeded in stopping pip.`); this.pipController?.off('stateChange'); // 如果已注册stateChange回调,停止画中画时取消注册该回调 this.pipController?.off('controlPanelActionEvent'); // 如果已注册controlPanelActionEvent回调,停止画中画时取消注册该回调 }).catch((err: BusinessError) => { console.error(`Failed to stop pip. Cause:${err.code}, message:${err.message}`); }); } } ``` **其他常见问题** Q:windowStage怎么获取? A:WindowStage需要在EntryAbility中的onWindowStageCreate中用AppStorage.setOrCreate()获取。 Q:子窗口可以用于应用外么? A:子窗口只能在应用内使用。 Q:子窗口的默认大小是多大? A:子窗口默认不设置大小的话是除安全区外的屏幕区域。 Q:UIExtension可以用子窗口么? A:UIExtension不是窗口对象,没有办法调用窗口接口。 Q:Har和Hsp中可以使用子窗口么? A:只要能获取到windowStage就能创建并使用子窗口。 ## 写在最后 **如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙**: * 点赞,转发,有你们的 『点赞和评论』,才是我创造的动力。 * 关注小编,同时可以期待后续文章ing🚀,不定期分享原创知识。 * **想要获取更多完整鸿蒙最新学习知识点,请移步前往小编:[`https://gitee.com/MNxiaona/733GH/blob/master/qita.md`](https://gitee.com/MNxiaona/733GH/blob/master/qita.md)** <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/dYF038ZRZeP2lmAz.webp" alt="" width="100%" />
【鸿蒙实战开发】基于ArkUI现有能力实现自定义弹窗封装方案
## **场景描述** 自定义弹窗是应用开发需要实现的基础功能,包括但不限于HarmonyOS开发者文档中定义的模态、半模态、Toast等形式,封装一个好用且和UI组件解耦的弹窗组件是开发者的高频诉求 自定义弹窗通常的使用场景有: ### **场景一:在公共逻辑中触发弹窗** 登录提示弹窗、全屏广告弹窗、网络请求与其他操作行为的提示、异常弹窗 ### **场景二:侧滑手势拦截** 隐私弹窗的拦截,退出登录时的确认弹窗 ### **场景三:切换页面弹窗不消失** 隐私弹窗和二级页面中的半模态弹窗 ### **场景四:自定义弹出、关闭动画** 从下往上的抽屉式弹出、关闭时从上往下收回 ### **场景五:透明、模态、半模态背景** 应用实现自定义的背景颜色 ## **方案描述** **1. 使用Navigation.Dialog** 基于Navigation.Dialog的透明页面特性,可以用于实现弹窗效果 而且Navigation.Dialog存在于路由栈中,天然可以实现切换页面弹窗不消失 **当前限制:** 弹窗组件中的动效建议开发者自行实现 Navigation.Dialog自身无颜色,需要开发者自行实现模态遮罩,以及手势事件。 **演示效果:** <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/5mKY4x0r0rM8ITDJ.webp" alt="" width="320px" /> <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/y6duoEFoUHx6qORW.webp" alt="" width="320px" /> <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/EDvOzYeCEwBILTKG.webp" alt="" width="320px" /> <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/FLyFNuPgSvUoFlmD.webp" alt="" width="320px" /> 对于少量弹窗的实现,可以直接使用Navigation来进行路由跳转,参考 Navigation常见场景及解决方案 其他`Navigation`的使用也可参考上述文章 ## **步骤一:封装路由工具类,并注册自定义弹窗组件** 定义路由工具类AppRouter,并创建路由栈NavPathStack ``` export class AppRouter { private static instance = new AppRouter(); private pathStack: NavPathStack = new NavPathStack(); // 初始化路由栈 public static getInstance(): AppRouter { return AppRouter.instance; } public getPathStack(): NavPathStack { return this.pathStack; } ... } ``` 在根页面中注册NavPathStack ``` @Entry @Component struct Index { build() { Navigation(AppRouter.getInstance().getPathStack()) { ... } } } ``` 在.navDestination注册封装的自定义弹窗组件DefaultDialog ``` @Builder PageMap(name: string) { if (name === CommonConstants.DEFAULT_DIALOG) { DefaultDialog() } ... } Navigation(AppRouter.getInstance().getPathStack()) { ... }.navDestination(this.PageMap) ``` 进阶用法:可以参考动态路由案例实现动态路由, HarmonyOS NEXT应用开发案例集 - Gitee.com ### **步骤二:封装弹窗UI 组件** 定义弹窗选项类AppDialogOption ``` export class AppDialogOption { view?: WrappedBuilder<Object[]> // 自定义弹窗内容组件 buildParams?: Object // 自定义弹窗内容参数 params?: Object // 打开时传递参数 autoClose?: number // 自动关闭时间 onPop?: (data: PopInfo) => void // 接收上一个弹窗关闭时的参数回调 onBackPressed?: () => boolean // 侧滑返回拦截 styles?: AppDialogStyle = new AppDialogStyle() // 弹窗样式 animation?: TransitionEffect // 弹窗动画 instance?: AppDialog // 弹窗操作对象 } 定义弹窗样式类AppDialogStyle export class AppDialogStyle { transparent: boolean = false background: string = 'rgba(0,0,0,0.5)' radius: Length = 5 align: Alignment = Alignment.Center } ``` 创建自定义弹窗组件DefaultDialog 通过Stack布局及2个Column容器实现模态遮罩和自定义弹窗内容,通过NavDestinationMode定义页面类型 ``` @Component export struct DefaultDialog { private dialogOptions?: AppDialogOption; build() { NavDestination() { Stack() { Column() { // 模态遮罩 } Column() { // 弹窗内容 } } .width("100%") .height("100%") } .mode(NavDestinationMode.DIALOG) // 页面类型为dialog } } ``` 通过.backgroundColor设置模态遮罩的背景颜色 ``` ... Stack() { Column() { // 模态遮罩 } .backgroundColor(this.dialogOptions?.styles?.transparent ? Color.Transparent : this.dialogOptions?.styles?.background) // 背景颜色 Column() { // 弹窗内容 } } ``` 通过Stack.alignContent设置弹窗定位 ``` Stack({ alignContent: this.dialogOptions?.styles?.align }) { Column() { // 模态遮罩 } Column() { // 弹窗内容 } } ``` ### **步骤三:封装弹窗控制器,与UI 组件解耦** 提供链式调用的Api ``` export class AppDialog { static indexArr: number[] = []; private stackIndex: number = 0; private options?: AppDialogOption; public static buildWithOptions(options?: AppDialogOption): AppDialog { let instance: AppDialog = new AppDialog(); // 获取并保存弹窗的路由栈序号 let index: number = AppRouter.getInstance().getPathStack().size() - 1; AppDialog.indexArr.push(index); instance.stackIndex = index; instance.options = options; options!.instance = instance; return instance; } public static build(builder: WrappedBuilder<Object[]>): AppDialog { let options: AppDialogOption = new AppDialogOption(); options.view = builder; return AppDialog.buildWithOptions(options); } public static toast(msg: string): AppDialog { let options: AppDialogOption = new AppDialogOption(); options.view = AppDialog.toastBuilder; options.buildParams = msg; return AppDialog.buildWithOptions(options); } public static closeAll(): void { AppRouter.getInstance().getPathStack().removeByName(CommonConstants.DEFAULT_DIALOG); } public static closeLast(params?: Object): void { let lastIndex = AppDialog.indexArr.pop() if (!lastIndex) { AppDialog.closeAll(); } else if (lastIndex && AppRouter.getInstance().getPathStack().size() > lastIndex) { AppRouter.getInstance().getPathStack().popToIndex(lastIndex, params); } } public open(): AppDialog { AppRouter.getInstance() .getPathStack() .pushPathByName(CommonConstants.DEFAULT_DIALOG, this.options, this.options!.onPop!, true); return this; } public close(params?: Object): void { if (AppRouter.getInstance().getPathStack().size() > this.stackIndex) { AppRouter.getInstance().getPathStack().popToIndex(this.stackIndex, params); } } public buildParams(buildParams: Object): AppDialog { this.options!.buildParams = buildParams; return this; } public params(params: Object): AppDialog { this.options!.params = params; return this; } public onBackPressed(callback: () => boolean): AppDialog {...} public onPop(callback: (data: PopInfo) => void): AppDialog {...} public animation(animation: TransitionEffect): AppDialog {...} public autoClose(time: number): AppDialog {...} public align(align: Alignment): AppDialog {...} public transparent(transparent: boolean): AppDialog {...} } ``` ### **步骤四:页面与弹窗,弹窗与弹窗之间传递参数** 通过路由跳转NavPathStack.pushPathByName传递参数 在弹窗组件的.onReady事件中获取路由跳转参数。 ``` @Component export struct DefaultDialog { private dialogOptions?: AppDialogOption; build() { NavDestination() { ... } .onReady((ctx: NavDestinationContext) => { console.log("onReady") this.dialogOptions = ctx.pathInfo.param as AppDialogOption; }) } } ``` 使用NavPathStack中的onPop回调来接收上一个弹窗返回的参数。 ``` onPop = (data: PopInfo) => { console.log("onPop") // 更新状态变量 this.params[index] = JSON.stringify(data.result) } navPathStack.pushPathByName(CommonConstants.DEFAULT_DIALOG, this.options, this.options!.onPop!, true) ``` 上一个弹窗在关闭时传入参数 ``` navPathStack.popToIndex(this.stackIndex, params); ``` ### **步骤五:实现弹窗自定义动画** 通过.transition属性分别实现背景和内容的转场动画 ``` ... Stack() { Column() { // 模态遮罩 } .transition( // 转场动画 TransitionEffect.OPACITY.animation({ duration: 300, curve: Curve.Friction }) ) Column() { // 弹窗内容 } .transition( // 转场动画 this.dialogOptions?.animation ? this.dialogOptions?.animation : TransitionEffect.scale({ x: 0, y: 0 }).animation({ duration: 300, curve: Curve.Friction }) ) } ``` 通过监听模态遮罩的点击事件实现关闭动画 ``` ... Stack() { Column() { // 模态遮罩 } .opacity(this.opacityNum) .onClick(() => { animateTo({ duration: 200, curve: Curve.Friction, onFinish: () => { this.dialogOptions?.instance?.close(); } }, () => { this.opacityNum = 0 // 修改模态遮罩的透明度 if (this.dialogOptions?.styles?.align === Alignment.Bottom) { this.translateY = "100%" } }) }) Column() { // 弹窗内容 } .translate({ x: 0, y: this.translateY }) } ``` ### **步骤五:实现自定义弹窗内容** 在弹窗内容的Column容器中传入WrappedBuilder来实现动态的自定义弹窗内容。 ``` Stack() { Column() { // 模态遮罩 } Column() { // 弹窗内容 this.dialogOptions?.view?.builder(this.dialogOptions); } } ``` 定义弹窗内容组件 ``` @Builder export function DialogViewBuilder(dialogOptions: AppDialogOption) { DialogView({ options: dialogOptions }) } @Component struct DialogView { private options?: dialogOptions ; build() { Column() { } ... } } ``` ### **步骤六:侧滑手势拦截** 在弹窗组件的.onBackPressed事件中进行拦截 ``` @Component export struct DefaultDialog { private dialogOptions?: AppDialogOption; build() { NavDestination() { ... } .onBackPressed((): boolean => { // true为拦截 if (this.dialogOptions?.onBackPressed) { return this.dialogOptions?.onBackPressed() } else { return false; } }) } } ``` **使用效果:** 使用弹窗控制器即可在非UI业务逻辑中打开弹窗 ``` export class AppService { buzz(): void { setTimeout(() => { AppDialog .toast("登录成功") .onBackPressed(() => true) .autoClose(2000) .transparent(true) .open(); }, 1000) // 模拟业务接口调用耗时 } } AppDialog.toastBuilder = wrapBuilder(ToastViewBuilder) @Builder export function ToastViewBuilder(dialogOptions: AppDialogOption) { ToastView({ msg: dialogOptions.buildParams as string }) } @Component struct ToastView { private msg?: string; build() { Column() { Text(this.msg) .fontSize(14) .fontColor(Color.White) .padding(10) } .backgroundColor("rgba(0,0,0,0.8)") .justifyContent(FlexAlign.Center) .borderRadius(12) .width(100) } } ``` 关闭弹窗 ``` // 全局使用 AppDialog.closeLast(); AppDialog.closeAll(); // 弹窗页面中使用 this.dialogOptions?.instance?.close(); ``` ## 写在最后 **如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙:** * 点赞,转发,有你们的 『点赞和评论』,才是我创造的动力; * 关注小编,同时可以期待后续文章ing🚀,不定期分享原创知识; * **想要获取更多完整鸿蒙最新学习知识点,可关注B站:码牛课堂鸿蒙开发;**
【鸿蒙实战开发】基于Taskpool的多线程操作
## **场景描述** **场景一**:周期性任务处理,业务通过taskpool周期性处理业务。 **场景二**:延迟业务处理,业务一段时间后,通过taskpool处理业务。 **场景三**:串行业务处理,业务开展过程中,需要处理一系列的事务,事务处理过程中,存在先后次序。 **场景四**:业务的处理存在紧急优先次序,支持设置taskpool优先级处理。 **场景五**:ArkTS与Native协作开展业务,在ArkTS层触发业务,通过NAPI接口,传递到Native C++层,作业务管理等处理。 ## **方案描述** ### **场景一:周期性任务** 方案: 1)定时器判断周期性事务执行。 2)Taskpool来处理任务执行。 <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/Oy7PWV7vDzLNJfyH.webp" alt="" width="100%" /> 核心代码: ``` @Concurrent function ServiceHandle(pars: number): number { hilog.info(0x0000, 'testTag', 'start ServiceHandle:%{public}d', pars); // 业务处理过程,并将结果返回 let result = 0; return result; } let count = 0; function TimerOutHandle(pars:number) { count++; let task: taskpool.Task = new taskpool.Task(ServiceHandle, pars); hilog.info(0x0000, 'testTag', 'Timer handle count :%{public}d,pars %{public}d', count, pars); taskpool.execute(task, taskpool.Priority.HIGH).then((res: object) => { hilog.info(0x0000, 'testTag', 'ServiceHandle result :%{public}d', res); if (g_callback != null) { g_callback(count); } }); } let timerId = -1; export function TimerTest() { count = 0; let value = 88; timerId = setInterval(TimerOutHandle, 3000, value); } ``` 定时器每3秒超时一次,进入TimerOutHandle函数处理,TimerOutHandle函数体中,通过taskpool创建异步并发任务执行业务。 **运行结果:** <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/oWYY1Lceok77VNxd.webp" alt="" width="100%" /> 界面上,每超时一次,会呈现运行次数: <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/koRLKLR306s6e9cy.webp" alt="" width="417px" /> ### **场景二:延迟任务** 方案: 1)通过setTimeout来延迟处理。 2) 通过executeDelayed来延迟处理。 核心代码: 1)setTimeout的处理如下: ``` @Concurrent function ServiceHandle(pars: number): number { hilog.info(0x0000, 'testTag', 'start ServiceHandle:%{public}d', pars); // 业务处理过程,并将结果返回 let result = 0; return result; } let count = 0; function TimerOutHandle(pars:number) { count++; let task: taskpool.Task = new taskpool.Task(ServiceHandle, pars); hilog.info(0x0000, 'testTag', 'Timer handle count :%{public}d,pars %{public}d', count, pars); taskpool.execute(task, taskpool.Priority.HIGH).then((res: object) => { hilog.info(0x0000, 'testTag', 'ServiceHandle result :%{public}d', res); if (g_callback != null) { g_callback(count); } }); } export function OneTimerCallTest() { count = 0; if (g_callback != null) { g_callback(count); } let value = 99; hilog.info(0x0000, 'testTag', 'start setTimeout'); timerId = setTimeout(TimerOutHandle, 3000, value); } ``` 定时器3秒超时(仅仅执行一次)后,就会进入TimerOutHandle函数处理,TimerOutHandle函数体中,通过taskpool创建异步并发任务执行业务。 2)executeDelayed来延迟 ``` @Concurrent function TaskDelayServiceHandle(pars: number): number { let t: number = Date.now(); hilog.info(0x0000, 'testTag', 'enter TaskDelayServiceHandle, timer is :%{public}d', t); // 业务处理过程,并将结果返回 let result = 0; return result; } export function TaskPoolDelayTest() { count = 0; if (g_callback != null) { g_callback(count); } let value = 100; let t: number = Date.now(); hilog.info(0x0000, 'testTag', 'taskpool start time is :%{public}d', t); let task: taskpool.Task = new taskpool.Task(TaskDelayServiceHandle, value); taskpool.executeDelayed(3000, task).then(() => { count++; let t: number = Date.now(); hilog.info(0x0000, 'testTag', 'taskpool execute success, time is :%{public}d', t); if (g_callback != null) { g_callback(count); } }).catch((e: BusinessError) => { console.error(`taskpool execute: Code: ${e.code}, message: ${e.message}`); }) } ``` 调用executeDelayed函数3秒后,会进入TaskDelayServiceHandle函数执行,返回返回后,会进入executeDelayed后面的then的函数体中执行。 **运行结果:** 1)使用setTimeout运行结果 <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/Fc84EA4Dl4BwLvdc.webp" alt="" width="100%" /> 2)使用executeDelayed运行结果 <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/RezpwzM8Zh2GkzFZ.webp" alt="" width="100%" /> ### **场景三:串行任务** 方案: 1)最简单的方案就是后面任务执行时,根据前面任务的执行结果来处理。 <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/4qW5I7ZwqqEUKlNa.webp" alt="" width="286px" /> 2)后面任务的执行,依赖另一个任务的一些处理结果后,继续执行。 <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/MJyjjvOH2JYhD6It.webp" alt="" width="459px" /> 核心代码: 1)通过业务逻辑的结果来处理 ``` @Concurrent function ServiceHandle1(pars: number): number { hilog.info(0x0000, 'testTag', 'start ServiceHandle1:%{public}d', pars); // 业务处理过程,并将结果返回 let result = 0; return result; } @Concurrent function ServiceHandle2(pars: number): number { hilog.info(0x0000, 'testTag', 'start ServiceHandle2:%{public}d', pars); // 业务处理过程,并将结果返回 let result = 1; return result; } export function SyncHandle() { let task1: taskpool.Task = new taskpool.Task(ServiceHandle1, 1); hilog.info(0x0000, 'testTag', 'sync handle'); taskpool.execute(task1, taskpool.Priority.HIGH).then((res1: object) => { hilog.info(0x0000, 'testTag', 'ServiceHandle result :%{public}d', res1); if (g_callback != null) { g_callback('task1 finish.'); } if ((res1 as Number) == 0) { let task2: taskpool.Task = new taskpool.Task(ServiceHandle2, 2); taskpool.execute(task2, taskpool.Priority.HIGH).then((res2: object) => { hilog.info(0x0000, 'testTag', 'ServiceHandle2 result :%{public}d', res2); if (g_callback != null) { g_callback('task2 finish.'); } }); } }); } ``` task1执行完毕后,根据if判断启动task2任务执行。 2)通过addDependency或SequenceRunner处理。 ``` @Concurrent function DependencyHandle(args: number): number { let t: number = Date.now(); while ((Date.now() - t) < 1000) { continue; } return args; } export function AddDependencyTest() { let task1:taskpool.Task = new taskpool.Task(DependencyHandle, 100); let task2:taskpool.Task = new taskpool.Task(DependencyHandle, 200); let task3:taskpool.Task = new taskpool.Task(DependencyHandle, 300); hilog.info(0x0000, 'testTag', 'dependency: add dependency start'); task1.addDependency(task2); task2.addDependency(task3); hilog.info(0x0000, 'testTag', 'dependency: add dependency end'); hilog.info(0x0000, 'testTag', 'dependency: start execute second'); taskpool.execute(task1).then(() => { hilog.info(0x0000, 'testTag', 'dependency: first task1 success'); if (g_callback != null) { g_callback('task1 finish.'); } }) taskpool.execute(task2).then(() => { hilog.info(0x0000, 'testTag', 'dependency: second task2 success'); if (g_callback != null) { g_callback('task2 finish.'); } }) taskpool.execute(task3).then(() => { hilog.info(0x0000, 'testTag', 'dependency: third task3 success'); if (g_callback != null) { g_callback('task3 finish.'); } }) } ``` task1依赖task2,task2依赖task3,上面任务执行的顺序是:task3执行完毕后再执行task2,最后执行task。 ``` @Concurrent function additionDelay(delay:number): void { let start: number = new Date().getTime(); while (new Date().getTime() - start < delay) { continue; } } @Concurrent function waitForRunner(finalString: string): string { return finalString; } export async function SeqRunnerTest() { let finalString:string = ""; let task1:taskpool.Task = new taskpool.Task(additionDelay, 3000); let task2:taskpool.Task = new taskpool.Task(additionDelay, 2000); let task3:taskpool.Task = new taskpool.Task(additionDelay, 1000); let task4:taskpool.Task = new taskpool.Task(waitForRunner, finalString); let runner:taskpool.SequenceRunner = new taskpool.SequenceRunner(); runner.execute(task1).then(() => { finalString += 'task1 finish.'; hilog.info(0x0000, 'testTag', 'seqrunner: task1 done.'); if (g_callback != null) { g_callback('task1 finish.'); } }); runner.execute(task2).then(() => { finalString += 'task2 finish.'; hilog.info(0x0000, 'testTag', 'seqrunner: task2 done.'); if (g_callback != null) { g_callback('task2 finish.'); } }); runner.execute(task3).then(() => { finalString += 'task3 finish.'; hilog.info(0x0000, 'testTag', 'seqrunner: task3 done.'); if (g_callback != null) { g_callback('task3 finish.'); } }); await runner.execute(task4); hilog.info(0x0000, 'testTag', 'seqrunner: task4 done, finalString is %{public}s', finalString); } ``` task1执行完毕后,执行task2,最后是task3执行完毕。 运行结果: 1)通过业务逻辑的结果来处理 <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/ag2Xtr1MArKC8o7p.webp" alt="" width="100%" /> 2)通过addDependency或SequenceRunner处理 <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/xSei3GWXbe23FxuU.webp" alt="" width="100%" /> ## **场景四:优先级任务** 方案: 在taskpool.execute的参数二种设置线程的优先级,优先级分三个级别:LOW、MEDIUM(默认)、HIGH。通过设置优先级来运行taskpool任务。 核心代码: ``` @Concurrent function ServiceHandle(pri: string): string { hilog.info(0x0000, 'testTag', 'enter ServiceHandle:%{public}s', pri); hilog.info(0x0000, 'testTag', 'end ServiceHandle:%{public}s', pri); return pri; } export function CallPriorityHanel() { let task1: taskpool.Task = new taskpool.Task(ServiceHandle, "LOW"); let task2: taskpool.Task = new taskpool.Task(ServiceHandle, "MEDIUM"); let task3: taskpool.Task = new taskpool.Task(ServiceHandle, "HIGH"); taskpool.execute(task1, taskpool.Priority.LOW).then((res: object) => { hilog.info(0x0000, 'testTag', 'task return result :%{public}s', res); }); taskpool.execute(task2, taskpool.Priority.MEDIUM).then((res: object) => { hilog.info(0x0000, 'testTag', 'task return result :%{public}s', res); }); taskpool.execute(task3, taskpool.Priority.HIGH).then((res: object) => { hilog.info(0x0000, 'testTag', 'task return result :%{public}s', res); }); } ``` 当前的设备都是多核的,并不是说将优先级设置程HIGH,该任务就会最先调度。 **运行结果:** <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/vdhpoXsJVLhBw5VZ.webp" alt="" width="100%" /> **场景五:taskpool****的Napi****调用** 方案:C++层编译的库,在ArkTS层通过import库的方式引用后,在taskpool的回调函数中调用接口。核心代码: ``` @Concurrent function ServiceHandle(pars: number): number { hilog.info(0x0000, 'testTag', 'start ServiceHandle:%{public}d', pars); // 业务处理过程,并将结果返回 testNapi.jsServiceHandle(88, 99); return 0; } export function CallHandle() { let task: taskpool.Task = new taskpool.Task(ServiceHandle, 1); taskpool.execute(task,).then((res: object) => { hilog.info(0x0000, 'testTag', 'printArgs result :%{public}d', res); }); } typedef struct TestData { int data; int type; } TestData; static napi_value JsServiceHandle(napi_env env, napi_callback_info info) { size_t argc = 2; napi_value args[2] = {nullptr}; napi_get_cb_info(env, info, &argc, args, nullptr, nullptr); TestData testData; napi_get_value_int32(env, args[0], &testData.data); napi_get_value_int32(env, args[1], &testData.type); OH_LOG_INFO(LOG_APP, "Native C++ Service handle:%{public}d,type:%{public}d", testData.data, testData.type); return nullptr; } EXTERN_C_START static napi_value Init(napi_env env, napi_value exports) { napi_property_descriptor desc[] = { {"jsServiceHandle", nullptr, JsServiceHandle, nullptr, nullptr, nullptr, napi_default, nullptr} }; napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc); return exports; } EXTERN_C_END ``` **运行结果:** <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/QwLDkk5Y0LbuFqTw.webp" alt="" width="100%" />
【鸿蒙实战开发】基于Napi调用ArkTS/系统接口
**场景描述:** app应用在native侧调用 系统库/arkts模块的方法。 应用经常会遇到如下的业务诉求: 场景一:系统提供了ArkTS 接口,但未提供对应的NDK接口,当伙伴使用C++ 代码实现业务逻辑时,部分系统能力需要依赖系统ArkTS接口; 场景二: 系统仅提供了ArkTS 异步接口,未提供对应的NDK接口,当伙伴使用C++ 代码实现业务逻辑时,部分系统能力需要依赖系统ArkTS 异步接口; 场景三:伙伴在 TS 侧已定义接口,未实现对应的NDK接口,当伙伴使用C++ 代码实现业务逻辑时,想直接使用已有的TS 接口; **方案描述:** **场景一:**系统提供了ArkTS 接口,但未提供对应的NDK接口,当伙伴使用C++ 代码实现业务逻辑时,部分系统能力需要依赖系统ArkTS接口; 例如: 获取设备的屏幕宽高。 **方案:** 通过napi_load_module 的方式调用系统模块接口。 <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/gg84tEklygBVxuK6.webp" alt="" width="100%" /> **核心代码** ``` static napi_value GetDisplaySize(napi_env env, napi_callback_info info) { // 获取arkts侧的系统库路径 char path[64] = "@ohos.display"; size_t typeLen = 0; napi_value string; napi_create_string_utf8(env, path, typeLen, &string); // 加载系统库 napi_value sysModule; napi_load_module(env, path, &sysModule); // 获取系统库中的"getDefaultDisplaySync"方法 napi_value func = nullptr; napi_get_named_property(env, sysModule, "getDefaultDisplaySync", &func); napi_value funcResult; napi_call_function(env, sysModule, func, 0, nullptr, &funcResult); napi_value widthValue = nullptr; napi_get_named_property(env, funcResult, "width", &widthValue); double width; napi_get_value_double(env, widthValue, &width); OH_LOG_INFO( LOG_APP, "width: %{public}f", width); napi_value heightValue = nullptr; napi_get_named_property(env, funcResult, "height", &heightValue); double height; napi_get_value_double(env, heightValue, &height); OH_LOG_INFO(LOG_APP, "height: %{public}f", height); // TODO: 业务拿到width 和 height,可以进一步处理具体业务逻辑 return nullptr; } ``` **运行结果:** <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/6KKd0M5VfSy6B0r3.webp" alt="" width="100%" /> **场景二:** 系统仅提供了ArkTS 异步接口,未提供对应的NDK接口,当伙伴使用C++ 代码实现业务逻辑时,部分系统能力需要依赖系统ArkTS 异步接口; 例如: 如何访问系统定义的异步ArkTS方法。 **方案** 通过创建线程安全函数的方式 调用系统的异步接口 <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/b8E6Hmx29eyi2HT0.webp" alt="" width="100%" /> 核心代码: 回调到JS层 ``` static void CallJs(napi_env env, napi_value jsCb, void *context, void *data) { if (env == nullptr) { return; } napi_value undefined = nullptr; napi_value promise = nullptr; char str[] = "wlan0"; napi_value sysModule; napi_load_module(env, "@ohos.net.statistics", &sysModule); napi_value param; napi_create_string_utf8(env, str, NAPI_AUTO_LENGTH, ¶m); napi_call_function(env, sysModule, jsCb, 1, ¶m, &promise); napi_value thenFunc = nullptr; if (napi_get_named_property(env, promise, "then", &thenFunc) != napi_ok) { return; } napi_value resolvedCallback; napi_value rejectedCallback; napi_create_function(env, "resolvedCallback", NAPI_AUTO_LENGTH, ResolvedCallback, data, &resolvedCallback); napi_create_function(env, "rejectedCallback", NAPI_AUTO_LENGTH, RejectedCallback, data, &rejectedCallback); napi_value argv[2] = {resolvedCallback, rejectedCallback}; napi_call_function(env, promise, thenFunc, 2, argv, nullptr); } // 执行异步任务 static void ExecuteWork(napi_env env, void *data) { CallbackData *callbackData = reinterpret_cast<CallbackData *>(data); std::promise<double> promise; auto future = promise.get_future(); // 调用线程安全函数 napi_call_threadsafe_function(callbackData->tsfn, &promise, napi_tsfn_nonblocking); try { auto result = future.get(); OH_LOG_INFO(LOG_APP, "getIfaceRxBytes Result from JS %{public}f", result); } catch (const std::exception &e) { // OH_LOG_INFO(LOG_APP, "XXX, Result from JS %{public}s", e.what()); } } // 异步任务完成回调 static void WorkComplete(napi_env env, napi_status status, void *data) { CallbackData *callbackData = reinterpret_cast<CallbackData *>(data); napi_release_threadsafe_function(callbackData->tsfn, napi_tsfn_release); napi_delete_async_work(env, callbackData->work); callbackData->tsfn = nullptr; callbackData->work = nullptr; } static napi_value CallAsyncFunc(napi_env env, napi_callback_info info) { size_t argc = 1; napi_value jsCb = nullptr; CallbackData *callbackData = nullptr; napi_get_cb_info(env, info, &argc, &jsCb, nullptr, reinterpret_cast<void **>(&callbackData)); napi_value sysModule; napi_load_module(env, "@ohos.net.statistics", &sysModule); napi_value getIfaceRxBytesFunc ; napi_get_named_property(env, sysModule, "getIfaceRxBytes", &getIfaceRxBytesFunc); // 创建一个线程安全函数 napi_value resourceName = nullptr; napi_create_string_utf8(env, "CallAsyncFunc", NAPI_AUTO_LENGTH, &resourceName); napi_create_threadsafe_function(env, getIfaceRxBytesFunc, nullptr, resourceName, 0, 1, callbackData, nullptr, callbackData, CallJs, &callbackData->tsfn); // 创建一个异步任务 napi_create_async_work(env, nullptr, resourceName, ExecuteWork, WorkComplete, callbackData, &callbackData->work); // 将异步任务加入到异步队列中 napi_queue_async_work(env, callbackData->work); return nullptr; } ``` **运行结果** <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/cDCCv8T6eJFuV5TR.webp" alt="" width="100%" /> **场景三:** 伙伴在 ArkTS/TS 侧已定义接口,当伙伴使用C++ 代码实现业务逻辑时,想直接使用已有的TS 接口; 例如: 如何调用自定义的 ArkTS/TS方法; **方案** <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/8TgxEoXr3jyoeDQ4.webp" alt="" width="100%" /> **核心代码** 步骤一: ObjectUtil.ts 导出相关接口 ``` namespace ObjectUtil { export function isNull(obj) { return obj === null; } export function isUndefined(obj) { return obj === undefined; } export function isNullOrUndefined(obj) { return isNull(obj) || isUndefined(obj); } export function toString(obj, defaultValue = '') { if (this.isNullOrUndefined(obj)) { return defaultValue; } else { return obj.toString(); } } } export default ObjectUtil; ``` 步骤二:必须在 build-profile.json5添加ets文件配置 ``` // 必须在build-profile.json5添加配置 "arkOptions": { "runtimeOnly": { "sources": [ './src/main/ets/common/ObjectUtil.ts', ], "packages": [ ] } }, ``` 步骤三: 在native 获取ets 模块导出的变量 ``` /* * 获取某个TS/JS模块导出变量 * 入参: * path - 在工程文件夹下从ets开始的绝对路径名 * key - 待加载TS/JS模块导出变量的属性名 * 返回值:获取的TS/JS模块导出变量 */ static napi_value GetNativeModule(napi_env env, const char *modulePath, const char *key) { napi_value module; // 通过modulePath获取对应TS/JS模块的导出对象 napi_load_module(env, modulePath, &module); napi_value outputObject = nullptr; // 通过对象属性名key,从导出模块变量 napi_get_named_property(env, module, key, &outputObject ); return outputObject ; } ``` 步骤四:获取方法并调用demo示例(获取/ets/common/ObjectUtil 的isNull方法,并调用isNull方法判断一个对象是否为null) ``` /* * 调用系统库/ets/common/ObjectUtil模块导出方法 /ets/common/ObjectUtil 的isNull方法 * 返回值:标志是否加载成功的布尔值 */ static napi_status CallIsNullFun(napi_env env, napi_value inputObject) { const char moduleName[] = "/ets/common/ObjectUtil"; const char funcName[] = "isNull"; napi_value isNullFun = GetNativeModule(env, moduleName, funcName); napi_value inputArgs[1] = inputObject; napi_value result; // infoFun为"/ets/common/ObjectUtil"模块中获取的函数方法 // inputArgs为待执行方法的入参,result为出参 napi_call_function(env, undefined, isNullFun , 1, inputArgs, result); return result ; } ```
初来乍到请多关照
软件工程学习路线 | 26 年最新零基础到精通一条龙(万人收藏⭐️)
> 编程导航学习网站:[学编程、做项目、拿 Offer!](https://www.codefather.cn) > > 企业高频面试题库:[开始刷题,面试遇原题!](https://www.mianshiya.com) > > 精选简历模板大全:[1 分钟搞定简历!](https://www.laoyujianli.com) > > AI 资源导航网站:[获取最新 AI 黑科技!](https://ai.codefather.cn) > > 1 对 1 模拟面试:[随时随地提升面试能力](https://ai.mianshiya.com) ## 开篇介绍 软件工程是什么? 听起来好像挺唬人的,简单来说,它是一种系统性、规范化的软件开发方法论,就像建筑工程中的设计、施工、验收等阶段一样。开发一个软件就好比建造一座大楼,必须严格执行规划、设计、施工、检测等步骤,而且每个步骤都要遵循一定的规范、流程和方法,才能最终建造出符合要求的高楼大厦。否则可能你建楼建到一半,发现无法再盖更多层了。 软件工程强调在软件开发过程中,按照一定的流程和方法来组织、规划、管理和控制软件开发过程,以保证开发出高质量、可靠、易维护的软件项目。随着软件系统越来越复杂,团队协作越来越重要,软件工程的价值也越来越凸显。 **为什么要学软件工程?** 很多同学刚开始学编程时,都是自己一个人写代码,想怎么写就怎么写。但当你进入企业、参加团队开发时,就会发现:如果大家都不遵循规范和流程,代码会变得越来越混乱,随便加个新功能、老功能就没法用了,也就是所谓的 “屎山代码”。学习软件工程,能让你理解如何在团队中高效协作,如何开发出高质量的软件,如何避免走弯路。 软件工程能解决什么问题?核心是 4 个字 —— 提质增效。提质,指的是通过规范的流程、方法和工具来提高软件质量,降低软件维护成本;增效,指的是通过设计模式、项目框架、自动化工具来提高开发效率,缩短软件开发周期。此外,软件工程还能提高团队协作效率、提高软件安全性、培养系统性思维。 需要注意的是,软件工程包含大量的理论知识,学起来可能会比较枯燥。鱼皮建议当你 **已经能够独立开发一个完整的项目后**,再抽时间系统学习软件工程。如果你是想快速找工作的同学,可以先忽略,因为面试基本不会直接考软件工程理论,但会考设计模式和开发规范。 在 AI 时代,软件工程的重要性不降反增。AI 应用开发同样需要遵循软件工程的规范和流程,从需求分析、系统设计到测试部署,每个环节都不能少。而且 AI 工具(如 Cursor、GitHub Copilot)能帮我们自动生成代码、自动测试、自动部署,让软件工程的实践变得更加高效。 举个例子,如果你让 AI 从 0 开发一个项目,最好也是先提供和确认方案,而不是无脑写代码:  ### 就业方向 软件工程不是一个独立的岗位,而是软件开发、架构师、项目经理等岗位的核心能力。掌握软件工程后,可以从事: 1. 软件开发工程师:遵循软件工程规范开发高质量代码 2. 系统架构师:设计复杂系统的架构,应用软件工程原理 3. 项目经理 / 技术 Leader:管理团队和项目,确保开发流程规范 4. 质量保证工程师:负责软件质量管理和流程优化 5. DevOps 工程师:实现自动化开发、测试、部署流程 ### 学习路线图  ## 整体学习建议 1)先做项目再学理论:软件工程的很多概念和方法只有在实践中才能理解。建议先独立完成 1-2 个项目,有了实际经验后再学习软件工程,会有 "醍醐灌顶" 的感觉。 2)重点学习实用内容:如果你是软件开发同学,优先学习开发原则、设计模式、开发规范、软件测试这些直接用得上的内容。其他理论知识可以简单了解。 3)不要死记硬背:软件工程的知识点很多,不要死记硬背。建议在实际开发中应用这些原则和方法,感受软件工程的有效性。 4)结合实际项目学习:建议在学习设计模式时,分析一些知名框架的源码(如 Spring、MyBatis),看看它们是如何应用设计模式的。关于设计模式的详细学习,可以查看 [设计模式学习路线](https://www.codefather.cn/course/1789189862986850306/section/1789190698894860290)。 5)善用 AI 工具:学习软件工程时可以用 AI 工具(如 ChatGPT / 豆包)辅助理解概念、生成代码、分析设计方案。推荐使用 [AI 资源大全](https://ai.codefather.cn/) 中的工具。  ## 阶段 1:软件工程基础(3-5 天) ### 学习目标 理解软件工程的基本概念、特点和应用价值。 ### 知识点 **软件工程基本概念【必学】:** - 软件的本质和特性 - 软件工程的定义和目标 - 软件开发的挑战(复杂性、一致性、可变性、不可见性) - 软件危机和软件工程的诞生 **软件过程【必学】:** - 什么是软件过程 - 软件生命周期(需求、设计、编码、测试、维护) - 软件过程模型 - 瀑布模型 - 迭代模型 - 敏捷开发(Scrum、看板)【建议学】 - DevOps【建议学】 **软件质量【必学】:** - 软件质量的定义 - 软件质量要素(功能性、可靠性、易用性、效率、可维护性、可移植性) - 如何保证软件质量 ### 学习建议 1)软件工程的理论知识比较抽象,不要一开始就深入学习。建议快速过一遍基本概念,理解软件工程的核心价值即可。 2)敏捷开发是目前最主流的软件开发方法,要重点理解其核心思想:拥抱变化、快速迭代、持续交付。很多互联网公司都在使用敏捷开发。 3)这个阶段不需要深入学习每种过程模型的细节,重点理解不同模型的特点和适用场景即可。 ### 学习资源 - [北京大学软件工程](https://www.bilibili.com/video/BV1Vt411V72G/):深入浅出的软件工程课程 - 《软件工程:实践者的研究方法》:经典大黑书,内容全面 ## 阶段 2:软件开发原则(5-7 天) 软件开发原则包括 SOLID、DRY、KISS 等设计原则,指导开发者编写高质量的代码。 ### 学习目标 掌握软件开发的核心原则,能够在编码时应用这些原则。 ### 知识点 **SOLID 原则【必学】:** - 单一职责原则(SRP):一个类只负责一个职责 - 开闭原则(OCP):对扩展开放,对修改关闭 - 里氏替换原则(LSP):子类可以替换父类 - 接口隔离原则(ISP):接口要小而精 - 依赖倒置原则(DIP):依赖抽象而不是具体实现 **其他开发原则【建议学】:** - DRY(Don't Repeat Yourself):不要重复自己 - KISS(Keep It Simple, Stupid):保持简单 - YAGNI(You Aren't Gonna Need It):不要过度设计 - 迪米特法则(最少知道原则) - 组合优于继承 ### 学习建议 1)SOLID 原则是面向对象设计的核心原则,也是设计模式的理论基础。要深入理解每个原则的含义和应用场景。 2)这些原则不要死记硬背,要在实际编码中体会。比如当你发现一个类越写越长、职责越来越多时,就应该想到单一职责原则,考虑拆分。 3)开闭原则是最重要的原则,很多设计模式(如工厂模式、策略模式)都是为了实现开闭原则。要理解如何通过抽象和多态来实现 "对扩展开放,对修改关闭"。 4)DRY、KISS、YAGNI 这些原则虽然简单,但在实际开发中非常实用。很多同学容易过度设计,写出过于复杂的代码,要记住:简单就是美。 ### 学习资源 - ⭐ [鱼皮的设计模式教程](https://www.codefather.cn/course/1930517607799193602):包含 SOLID 原则详解 - [代码整洁之道](https://book.douban.com/subject/4199741/):经典书籍,讲解编程原则 ## 阶段 3:设计模式(15-30 天) 设计模式是软件设计中的经典解决方案,掌握常用设计模式能够提升代码的可维护性和可扩展性。 ### 学习目标 掌握 23 种经典设计模式,能够在实际项目中灵活运用。  ### 知识点 **设计模式基础【必学】:** - 什么是设计模式 - 设计模式的分类(创建型、结构型、行为型) - 设计模式和设计原则的关系 **创建型模式【必学】:** - 单例模式:确保一个类只有一个实例 - 工厂方法模式:定义创建对象的接口 - 抽象工厂模式:创建相关对象家族 - 建造者模式:构建复杂对象 - 原型模式:通过复制创建对象【建议学】 **结构型模式【必学】:** - 适配器模式:接口转换 - 装饰器模式:动态添加职责 - 代理模式:控制对象访问 - 外观模式:简化子系统接口 - 桥接模式:抽象和实现分离【建议学】 - 组合模式:树形结构处理【建议学】 - 享元模式:共享对象节省内存【可不学】 **行为型模式【必学】:** - 策略模式:算法封装和切换 - 模板方法模式:定义算法骨架 - 观察者模式:对象间的依赖通知 - 责任链模式:请求传递处理 - 迭代器模式:遍历聚合对象 - 状态模式:对象状态变化【建议学】 - 命令模式:请求封装为对象【建议学】 - 访问者模式:操作对象结构【可不学】 - 中介者模式:对象间的中介【可不学】 - 备忘录模式:保存和恢复状态【可不学】 - 解释器模式:语言解释【可不学】 ### 学习建议 1)设计模式是软件工程中最重要的内容,也是面试的高频考点。23 种设计模式不需要全部精通,建议重点学习常用的 10-12 种。 2)学习设计模式的顺序建议按使用频率:单例模式 → 工厂模式 → 策略模式 → 代理模式 → 观察者模式 → 模板方法模式 → 适配器模式 → 责任链模式。 3)每学一个设计模式,建议:先理解是什么和为什么 → 看 UML 类图理解结构 → 手写代码实现 → 分析框架源码中的应用。不要死记硬背代码。 4)鱼皮的设计模式教程非常详细,全面覆盖 23 种设计模式,包含 Java 示例代码、UML 类图、使用场景详解和源码解析,强烈推荐。 5)学完基础的设计模式后,建议阅读 Spring、MyBatis 等框架的源码,看看大厂是如何应用设计模式的。这会让你对设计模式有更深的理解。 6)**不要过度使用设计模式!** 设计模式不是银弹,过度使用只会增加系统复杂度。要根据实际需求选择合适的设计模式。 ### 经典面试题 1. 什么是设计模式?设计模式分为哪几类? 2. 请说说你了解的常见设计模式及其应用场景 3. 单例模式有哪几种实现方式?各有什么优缺点? 4. 工厂模式和抽象工厂模式有什么区别? 5. 策略模式和状态模式有什么区别? 6. Spring 框架中使用了哪些设计模式? 7. 装饰器模式、适配器模式、代理模式、桥接模式有什么区别? ### 学习资源 - ⭐ [鱼皮的设计模式教程](https://www.codefather.cn/course/1930517607799193602):23 种设计模式完整教程 - ⭐ [鱼皮的设计模式 GitHub](https://github.com/liyupi/design-pattern-guide):开源设计模式教程 - [图解设计模式](https://www.bilibili.com/video/BV1Np4y1z7BU/):生动形象的设计模式讲解 - [Head First 设计模式](https://book.douban.com/subject/2243615/):经典设计模式书籍 - [设计模式面试题 - 面试鸭](https://www.mianshiya.com/bank/1801559627969929217):全面的设计模式面试题 ## 阶段 4:开发规范(3-7 天) 开发规范包括代码规范、版本管理、文档编写等内容,是团队协作的基础。 ### 学习目标 掌握代码规范、文档规范、Git 规范等开发规范,养成良好的编程习惯。 ### 知识点 **代码规范【必学】:** - 命名规范(变量、函数、类、包等) - 代码格式规范(缩进、空格、换行等) - 注释规范(单行注释、多行注释、文档注释) - 编码原则(SOLID、DRY、KISS 等) - 异常处理规范 - 并发编程规范【建议学】 **文档规范【建议学】:** - 需求文档(PRD) - 设计文档(架构设计、详细设计) - 接口文档(API 文档) - README 文档 - 变更日志(CHANGELOG) **Git 规范【必学】:** - 分支管理规范(Git Flow、GitHub Flow) - Commit 提交规范(Conventional Commits) - 代码审查规范(Code Review) - Pull Request 规范 **代码检查工具【必学】:** - Java:CheckStyle、SonarQube、SpotBugs - Python:Pylint、Flake8、Black - JavaScript/TypeScript:ESLint、Prettier - Go:golint、gofmt ### 学习建议 1)开发规范是整个软件工程中最重要、最应该优先学习的内容!因为:**开发不规范,同事两行泪!** 遵守规范有利于提高项目的开发效率、降低维护成本。 2)每个团队的开发规范可能不完全相同,千万不要死记硬背!建议先过一遍大厂的代码规范手册(如阿里巴巴 Java 开发手册),做项目时想到了某个规范,就去查阅确认。 3)善用代码检查工具。现代 IDE(如 IDEA、VSCode)都集成了代码检查功能,会自动提示不规范的代码。建议多观察编辑器给出的警告,尝试消除它们。 4)项目做得多了、慢慢养成习惯后,你会发现遵循规范是一件 "水到渠成" 的事情,不用刻意去记。 ### 学习资源 - ⭐ [阿里巴巴 Java 开发手册](https://developer.aliyun.com/graph/java):搜索 "Java 开发手册" - ⭐ [华山版《Java 开发手册》讲解](https://developer.aliyun.com/live/1201):官方讲解视频 - [Google Java Style Guide](https://google.github.io/styleguide/javaguide.html):Google Java 代码规范 - [Airbnb JavaScript 代码规范](https://github.com/airbnb/javascript):前端代码规范 - [Conventional Commits](https://www.conventionalcommits.org/):Git 提交规范 ## 阶段 5:软件测试(3-7 天) 软件测试是保证软件质量的重要手段,包括单元测试、集成测试、系统测试等。 ### 学习目标 掌握软件测试的基本方法,能够编写单元测试和集成测试。 ### 知识点 **测试基础【必学】:** - 软件测试的目的和意义 - 测试的分类(黑盒测试、白盒测试) - 测试的层次(单元测试、集成测试、系统测试、验收测试) **单元测试【必学】:** - 什么是单元测试 - 单元测试框架(JUnit、PyTest、Jest 等) - 测试覆盖率(Jacoco) - Mock 和 Stub【建议学】 **集成测试【建议学】:** - 什么是集成测试 - 集成测试的策略 - 接口测试工具(Postman、JMeter) **自动化测试【建议学】:** - 测试自动化的价值 - 持续集成(CI)和持续测试 - 自动化测试框架(Selenium、Cypress) **性能测试【可不学】:** - 压力测试、负载测试 - 性能测试工具(JMeter、Gatling) ### 学习建议 1)单元测试是开发者的基本功,要重点掌握。很多同学觉得写单元测试浪费时间,但实际上单元测试能帮你发现代码中的问题,提高代码质量。 2)测试覆盖率不是越高越好,要关注核心业务逻辑的测试。100% 的测试覆盖率往往意味着很多无意义的测试。 3)Mock 和 Stub 是单元测试中的重要技术,用于隔离外部依赖(如数据库、网络请求)。要理解它们的概念和使用场景。 4)集成测试和系统测试一般由专业的测试工程师负责,开发者了解基本概念即可。 ### 学习资源 - 《软件测试》(Ron Patton 著):经典测试书籍 ## 阶段 6:软件项目管理(选学,3-5 天) 软件项目管理包括需求管理、进度管理、风险管理等内容,是项目成功的关键因素。 ### 学习目标 了解软件项目管理的基本概念和方法,理解敏捷开发流程。 ### 知识点 **项目管理基础【建议学】:** - 项目管理的定义和目标 - 项目管理的要素(范围、时间、成本、质量) - 项目管理的过程(启动、规划、执行、监控、收尾) **敏捷项目管理【建议学】:** - Scrum 框架(Sprint、Daily Standup、Sprint Review) - 看板方法(Kanban) - 用户故事(User Story) - 迭代计划和估算 **项目管理工具【可不学】:** - Jira:最流行的项目管理工具 - 禅道:国产项目管理工具 - Trello:轻量级看板工具 - Teambition:阿里的协作工具 ### 学习建议 1)项目管理对于普通开发者来说不是必学的,但对于 Team Leader、项目经理来说是必备技能。如果你未来想往管理方向发展,建议学习。 2)Scrum 是最流行的敏捷框架,要理解其核心概念:Sprint(冲刺)、Product Backlog(产品待办列表)、Sprint Planning(迭代计划会)、Daily Standup(每日站会)等。  3)项目管理工具的使用很简单,不需要专门学习。入职后跟着团队使用即可。 ### 学习资源 - 《敏捷软件开发》(Robert C. Martin 著):敏捷开发经典书籍 - 《人月神话》(Frederick P. Brooks Jr. 著):软件项目管理经典 ## 阶段 7:实践和进阶(持续学习) ### 学习目标 将软件工程知识应用到实际项目中,持续提升软件工程能力。 ### 知识点 **需求工程【可不学】:** - 需求获取和分析 - 需求规格说明 - 需求验证 **UML 建模【可不学】:** - UML 图的类型(类图、时序图、用例图等) - UML 建模工具(PlantUML、draw.io) **软件架构【建议学】:** - 分层架构、MVC 架构 - 微服务架构 - 事件驱动架构 - 领域驱动设计(DDD)【建议学】 **DevOps 实践【建议学】:** - 持续集成(CI) - 持续交付(CD) - 自动化部署 - 基础设施即代码(IaC)【可不学】 ### 学习建议 1)软件架构是高级内容,建议有一定项目经验后再学习。可以参考 [后端系统设计学习路线](https://www.codefather.cn/course/1789189862986850306/section/1990754702744137730)。 2)领域驱动设计(DDD)是一种复杂系统的设计方法论,强调业务领域建模。鱼皮的智能协同云图库项目就采用了 DDD 架构设计,可以通过 [云图库项目](https://www.codefather.cn/course/1864210260732116994) 实战学习 DDD 的应用。 3)DevOps 是现代软件开发的重要实践,建议了解 CI/CD 的基本概念。可以参考 [DevOps 学习路线](https://www.codefather.cn/course/1789189862986850306/section/1990755061927555073) 和 [CI/CD 学习路线](https://www.codefather.cn/course/1789189862986850306/section/1990755091384152066)。 4)UML 建模在实际工作中使用频率不高,简单了解即可。更多时候我们用流程图、架构图等更直观的方式来表达设计。 ### 学习资源 - [后端系统设计学习路线](https://www.codefather.cn/course/1789189862986850306/section/1990754702744137730):深入学习系统设计 - ⭐ [编程导航 - 智能协同云图库项目](https://www.codefather.cn/course/1864210260732116994):DDD 领域驱动设计实战 - [DevOps 学习路线](https://www.codefather.cn/course/1789189862986850306/section/1990755061927555073):了解 DevOps 实践 - [CI/CD 学习路线](https://www.codefather.cn/course/1789189862986850306/section/1990755091384152066):学习持续集成和持续交付 ## 持续学习资源 ### 经典书籍 - ⭐ 《软件工程:实践者的研究方法》:经典大黑书,内容全面(但比较厚重) - ⭐ 《代码整洁之道》:教你写出整洁易读的代码 - 《软件开发的 201 个原则》:工具书,实用性强 - 《重构:改善既有代码的设计》:经典重构书籍 - 《设计模式:可复用面向对象软件的基础》:设计模式圣经(GoF) ### 知识总结 - ⭐ [编程导航](https://www.codefather.cn/):学习路线、项目教程、面试题、编程资源一站式平台 - [设计模式学习路线](https://www.codefather.cn/course/1789189862986850306/section/1789190698894860290):专门的设计模式学习路线 - [后端系统设计学习路线](https://www.codefather.cn/course/1789189862986850306/section/1990754702744137730):系统设计方法论 ### 技术博客 - [Martin Fowler Blog](https://martinfowler.com/):软件工程大师博客 - [Google Engineering Practices](https://google.github.io/eng-practices/):谷歌工程实践 - [Spotify Engineering](https://engineering.atspotify.com/):Spotify 工程文化 - [Netflix TechBlog](https://netflixtechblog.com/):Netflix 工程实践 ## 写在最后 软件工程是一门实践性很强的学科,理论学习只是第一步,更重要的是在实际项目中应用。 建议大家在学习软件工程时,不要追求面面俱到,重点掌握软件开发原则、设计模式、开发规范、软件测试这些最实用的内容。当你成为团队的负责人后,再去深入学习项目管理、需求工程等内容。 希望这份学习路线能够帮助大家系统地掌握软件工程知识,写出更高质量的代码,成为更优秀的软件工程师。 加油小伙伴们 💪🏻!  ## 程序员必备资源 1)程序员学习交流圈:[极客教程、实战项目、求职宝典](https://www.codefather.cn) 2)程序员面试八股文:[实习/校招/社招高频考点、企业真题解析](https://www.mianshiya.com) 3)程序员写简历神器:[专业模板、丰富例句、直通面试](https://www.laoyujianli.com) 4)AI 知识资源大全:[前沿技术、最新 AI 资讯、提示词大全](https://ai.codefather.cn) 5)1 对 1 模拟面试:[实习/校招/社招面试拿 Offer 必备](https://ai.mianshiya.com)
企业项目研发流程 | 26 年最新零基础到精通一条龙(万人收藏⭐️)
> 编程导航学习网站:[学编程、做项目、拿 Offer!](https://www.codefather.cn) > > 企业高频面试题库:[开始刷题,面试遇原题!](https://www.mianshiya.com) > > 精选简历模板大全:[1 分钟搞定简历!](https://www.laoyujianli.com) > > AI 资源导航网站:[获取最新 AI 黑科技!](https://ai.codefather.cn) > > 1 对 1 模拟面试:[随时随地提升面试能力](https://ai.mianshiya.com) ## 开篇介绍 很多未工作过的小伙伴都很好奇:企业中做项目是怎样的流程?尤其是大厂那些百万用户的项目,和自己学编程时做项目到底有什么区别呢? **实话说,区别可大了!** 自己开发项目那是单打独斗,自己掌握命运,不会拖垮队友;但企业中开发项目是开团打本,大家是一根绳上的蚂蚱,每个人都会影响整个项目。  鱼皮自己也在几家公司实习过,不得不说,大厂和其他公司的研发流程也有很大的区别。因此,对于大多数同学,如果没有在大厂工作过,对很多研发环节可能都是一无所知的。 所以这篇文章就来揭秘一下大厂的项目研发流程,帮大家开拓思路。正好之前有同志质疑我的日常工作就只有写代码和摸鱼?这篇文章就作为回击,让他明白,在大厂做项目,可不止写代码那么简单!  如果你想更直观地了解大厂项目开发流程,强烈推荐观看鱼皮的视频:[一线大厂项目是怎么做出来的](https://www.bilibili.com/video/BV11q4y1T7kY/),结合视频和本文一起学习,效果更好。 ### 研发流程全景图 为了规范团队、保证项目的进展,大厂研发流程一般还是比较复杂的。可以分为很多个阶段,用一张思维导图来概括:  需要注意的是,以上阶段并不是完全按从上到下的顺序执行,阶段间可能存在交叉,比如 **技术选型** 其实在 **设计阶段** 就应该考虑。 正式工作一年多,我也是经历过多次项目的完整研发流程的。下面就以我的视角,带大家快速过一遍~ (为了内容更有趣,以下故事有虚构成分) ## 需求阶段 今天是周一,鱼皮像往常一样骑着他的小电动车来到公司,殊不知,等待他的是一场噩梦的开始。 ### 需求产生 上午十点,产品妹子找到鱼皮,告诉他:咱们的系统上线后,用户表示很多功能并不好用,需要大改。 老板也找到鱼皮,告诉他:我今天打开页面竟然加载了十几秒,咱们这个系统的性能太烂了吧! 鱼皮心想:呕豁,完蛋!估计得做个新的项目了,又要开会了。  果然,没过多久,屏幕上弹出了一条 "欢迎加入会议" 的邀请。 ### 需求评审 第二天上午,老板、产品、测试、几位开发大哥和鱼皮一起来到会议室,具体讨论昨天提到的那些需求 **是否合理、要不要做**? 产品妹子打开文档,说到:这一期呢,我们要做这几个需求,下面我来详细讲一下,大家一起评估下有没有问题。 ### 需求分析 接下来,产品妹子正在对着屏幕侃侃而谈、疯狂输出时,旁边的开发大哥坐不住了。 开发大哥:这个需求不合理啊! 产品:为啥不合理?用户就是有这个需求啊! 开发大哥:我知道,实现不了啊! 于是开始了经典的产品开发撕逼大战。。。  而鱼皮正躲在角落冷静分析 **这个需求怎么做**,过了一会儿,提出了一种改动低、实现快的解决方案,平息了这场战争。 在 AI 时代,我们还可以用 AI 工具(如 ChatGPT、Cursor)来辅助分析需求,快速生成多种技术方案进行对比,大大提升需求分析的效率。 ### 排期 确定需求合理、可实现之后,产品妹子问到:那这个需求啥时候能上线呀? 开发大哥:我这周忙,下周吧。 产品:用户可能比较着急,这周就要呢! 开发大哥:我知道,做不完啊! 于是开始了经典的产品开发撕逼大战。。。  鱼皮:要不我们把这个需求拆解为功能 A 和功能 B,这周我先把功能 A 做了,功能 B 排到下周二测试,下周四上线? 就这样,我们一个个安排了需求的计划完成日期。 ## 设计阶段 终于开完会了,看了下时间,都该下班了! 唉,需求讨论完了,产品的工作是完成了一些,可鱼皮的工作才刚刚开始。 急着开始写代码么? **不,想好怎么写代码比写代码更重要。** ### 架构设计 鱼皮打开写文档软件和画图软件,开始梳理整个系统,从整体到局部,依次设计出系统的层次结构、各层间交互的接口和通讯方式、每层之间包含哪些重要模块、模块选择何种物理部署方式等。  现在有了 AI 工具的加持,我们可以用 AI 辅助生成架构图、分析架构方案的优缺点,甚至让 AI 帮我们做技术调研,大大提升设计效率。建议观看鱼皮分享的 [AI 画图保姆级指南和技巧分享视频](https://www.bilibili.com/video/BV1DP7JzAE7k/)。 ### 概要设计 写完架构设计后,鱼皮开始对着产品妹子写的 PRD(产品需求文档),分析需求,然后依然是从整体到局部,先整理出系统需要的功能模块,再分析每个功能模块内有哪些子模块。 和抽象的架构设计相比,概要设计和需求的关系更紧密,是对架构设计的细化。 打个比方大家就明白了,你要盖一栋楼,架构设计就是从整体来考虑,总共有几层、每层管道怎么接、每层有几户、地基怎么打等;而概要设计就是考虑每户套件的内部怎么划分,哪里是客厅、哪里是卫生间。 > 很多情况下,概要设计和架构设计可能会在一个文档中进行,划分并不明确。 ### 详细设计 想好系统有哪些功能后,鱼皮就开始具体分析每个功能如何实现,用到哪些算法、需要注重哪些细节等。 ### 方案对齐 写好设计文档后,下次会议上,鱼皮和其他的开发同学(前端、后端等)一起针对自己设计的方案展开讨论,最终产生一个统一的方案,然后大家分工去做就好了。 ### 测试用例设计 为了保证系统功能的正常稳定,测试同学(或者叫 QA)是非常重要的。关于软件测试的详细学习,可以查看 [软件测试学习路线](https://www.codefather.cn/course/1789189862986850306/section/1990755616108359682)。测试不是像我们自己做项目一样对着网页点几下就 ok 了。 在大公司中,为了保证测试的覆盖度、提高测试效率,一般是要设计测试用例的,比如:用户点击 "登录",未传任何数据,期望结果是警告用户输入用户名和密码。  测试用例设计完后,需要其他同学一起来评审把关,而不是只交给测试同学。因为一个人很容易忽略掉很多测试细节,最好让更熟悉代码的开发同学一起帮忙补充。 鱼皮自己也写了几个测试可能会遗漏的用例,和测试同学一起进行了确认,尽量让问题暴露在测试阶段而不是线上。 ## 研发准备 写了快一周的设计文档,终于准备开始动手搭建项目了。但在此之前,还有一些准备工作要进行。 ### 技术预研 如今技术发展太快,新技术层出不穷,所以鱼皮首先对项目中需要或可能需要用到的技术进行了调研。 在 AI 时代,技术调研变得更加高效。我们可以用 AI 工具快速了解新技术、对比不同方案,甚至让 AI 帮我们生成技术选型报告。 ### 技术选型 通过调研,鱼皮得到了几个可以满足需求的技术,但他开始纠结:这么多技术,我该用哪一个呢?是用 SSM 框架还是 Spring Boot 呢?用 Guava 包还是 Apache Commons 呢? 鱼皮又打开了写文档软件,开始对比不同技术的优劣,头疼啊,技术选型要考量的因素太多了,比如: - 单从技术考虑:性能、易用性、稳定性、主流程度和生态、文档详细度 - 结合团队:团队成员对技术的熟悉度、掌控度(有无精通该技术的人) - 结合业务:是否适应业务的量级(单机 or 微服务)、是否适应业务(读多、写多 or 分析多) 对于关键的项目,鱼皮自己还不敢完全确定选型,因此在写好自己的选型文档后,和同事和 Leader 一起讨论,才最终确认。 ### 资源申请 确认好技术后,就要申请资源。比如鱼皮用到了 MySQL 数据库,但是这个 MySQL 从哪儿来呢? 以前的话,鱼皮都是去买一台云服务器,自己搭建 MySQL。但是在企业中,一般是有集中管理和分配资源的平台的,直接到平台填写预算、等领导审批、然后等着下发资源就好了。千万不能私自用自己的或买外部的服务器来部署项目,不安全! 鱼皮这次直接申请到了 2 万多一年的云数据库,真的是爽死了。  ### 环境准备 申请好数据库等资源后,鱼皮按照申请机器的版本搭建了一模一样的本地开发环境和测试环境,后面就可以直接连接了。 ### 项目初始化 环境准备妥当后,由于是新项目,鱼皮要搞一个最小可运行的初始化项目 Demo,使用 **脚手架** 自动生成代码,而不是从零开始一个个新建文件、手敲重复代码。 现在还有更智能的方式:使用 AI 工具(如 Cursor、GitHub Copilot)来辅助初始化项目、生成模板代码,大大节省时间。 ### 依赖安装 生成了项目代码后,鱼皮使用包管理工具(前端 yarn/npm、Java Maven / Gradle 等)自动安装依赖,然后项目 Demo 就可以运行啦! ## 研发阶段 前期准备完成后,这才到了程序员朋友们最熟悉的写代码环节,也是鱼皮最爱的环节。  因为之前设计方案时需要保持冷静、仔细思考,没法边听歌儿边做;而方案设计好后,已经明确了该怎么做,写代码实现就很简单了,顶多是遇到一些坑,上网搜索去解决就好了。 ### 本地开发 开发时,一般鱼皮会先在本地写代码,通过配置热更新工具,实现代码更新时自动重新编译打包,而不用手动重启项目,大大提高了开发效率。 对了,企业开发都会使用版本控制系统的,比如 Git,开发前记得先创建一个自己的分支,在这个分支上开发。 ### 远程开发 现在还有一种比较流行的远程开发方式,就是可以像编辑本地文件一样编辑远程文件,直接修改服务器上的代码。一般我们每位研发同学是有自己的开发机的,通过远程开发就省去了反复部署调试的麻烦,提高效率。一般用 VSCode 等开发工具,安装远程开发插件就可以实现了。鱼皮之前分享过一个简单的 [VSCode 远程开发实操视频](https://www.bilibili.com/video/BV1s64y167cM),可以看一下。 ### AI 辅助开发 在 2025 年,AI 辅助开发已经成为主流。鱼皮现在写代码时经常使用 Cursor、GitHub Copilot 等 AI 工具,它们能够: - 自动补全代码,提升编码效率 - 生成单元测试,保证代码质量 - 解释复杂代码,帮助理解 - 发现潜在 Bug,提前预警 - 重构优化代码,提升可维护性 推荐大家使用 [AI 资源大全](https://ai.codefather.cn/) 中的工具,提升开发效率。  ### 代码优化 鱼皮在写代码的时候,始终保持主动优化代码的好习惯,注重代码的时空复杂度;并且当重复代码多了,会想办法抽象成函数或者使用设计模式。之前专门写文章分享过我的编程习惯:[我写代码时的小倔强](https://mp.weixin.qq.com/s?__biz=MzI1NDczNTAwMA==&mid=2247497781&idx=1&sn=9c5ec35cda90ca080ba1dbfa78429275&scene=21#wechat_redirect)。 ### 单元测试 注意!不要听到测试就以为是测试同学的工作,开发同学也同样需要编写小粒度的测试来为自己的代码负责。 鱼皮一般会为每个数据库读写函数和业务逻辑函数编写单元测试,像 Java 的话一般用 JUnit 等工具,还可以用 Jacoco 生成测试覆盖度报告。每次修改关键代码后,都要执行一遍单元测试,防止意外错误。  现在还可以用 AI 工具自动生成单元测试代码,省去很多重复劳动。 ### 开发联调 鱼皮终于写好了后端代码,也自测完成了,下面就是把写好的代码打包构建,然后把可执行项目包发布到测试服务器上,和前端同学一起联调,让他请求我的接口,验证系统的功能是否可用。 ## 测试验证 鱼皮和前端联调完毕后,告知了测试和产品同学。 测试验证是企业中至关重要的环节,甚至可以说是最后一道防线。测试的目的是找 Bug,尽量发现系统中的问题,把它们扼杀在测试阶段。 在企业中,测试验证又有很多类型。 ### 集成测试 集成测试比单元测试粒度更大,是把多个模块或代码单元放在一起,验证模块之间的集成和调用关系。 因为单个函数的执行可能是正常的,但把多个函数组合在一起顺序调用,可能就会出现问题。 打个比方,我们有个吃面包系统: 功能 A:小鱼吃一个面包 功能 B:小皮吃一个面包 每次只有一个面包,独立执行功能 A 和 B 都是允许的。但如果两个一起执行,后执行的那个功能就会报错。  ### 系统测试 系统测试比集成测试的粒度更大,测试对象是整个系统,不仅包括软件,还可能覆盖对硬件的测试。 ### 产品体验 除了测试同学要验证系统可用性,产品妹子也要体验下功能是否符合预期、是否易用。大多数情况下,产品会在体验时提出修改建议,开发可能还要再去做一些修改。 ### 验收测试 测试和产品妹子终于表示没有问题啦,那就到了最后一步,把整个产品或功能给最终的用户来体验。老板/用户说没问题,才是真的没问题! ## 提交阶段 系统没问题之后,鱼皮就可以把代码发布到远程仓库了,一般使用 Git 和 SVN 等版本控制系统。 ### 代码提交 鱼皮首先在本地触发代码提交(git commit),为保证规范,在大项目中一般会使用提交检测插件(如 Husky、pre-commit hook),防止你把错误的代码进行了提交。 现代的代码提交还会自动触发: - 代码格式检查(Prettier、ESLint) - 单元测试执行 - 代码规范检查(CheckStyle、SonarLint) ### 代码推送 下一步就是把本地的提交推送到远程的同名分支。一般大厂会有推送检测工具,检测代码的错误、圈复杂度、代码规范等,和提交检测一样,防止你把错误或不规范的代码进行了推送。 ### 合并请求 代码分支推送到远程之后,鱼皮发起了一个分支合并请求(MR 或 PR),希望把该分支的代码合并到主干分支(没问题的代码)。  ### 代码审查 并不是发起了合并请求就能直接合并,还要通过代码审查,即 CR(Code Review)。 审查又分为两种方式:人审和机审。 相信不少同学都知道人审,一般是由你的上级和其他项目负责人来阅读和评论你的代码,觉得没问题就 Approve(通过),否则打回去修改。 那机审是个啥呢?其实就是机器自动检测你的代码是否符合规范,是否能够成功自动化构建等,一般是由项目负责人配置的,可以帮助发现一些人工难以发现的问题。 刚接触新项目的时候,鱼皮经常被机审折磨得苦不堪言,经常被提示一些莫名其妙的代码问题,比如加号要换行,文件行末要加空行等。但后来注意编码习惯后,就很自然地适应了,的确不错。 现在还有 AI Code Review 工具,能够自动发现代码中的问题、提出优化建议,进一步提升代码质量。 ## 发布阶段 代码审查通过后,鱼皮的项目代码就可以发布上线啦。  ### 打包构建 传统上线方式是开发人员到正式服务器上拉取代码,然后安装依赖,再通过工具把代码打包构建,得到部署包,通过 Nginx、Tomcat、Docker 等技术运行。 但这样效率很低,有很多重复工作。所以大厂一般是用自动化构建的,像 Jenkins、GitLab CI、GitHub Actions 等 CI/CD 工具。代码合并到主分支后,由机器自动把代码打包构建为最终的部署包。 ### 预发布 为了防止上线出问题,一般我们会先在预发布环境部署项目,再观察一下是否能够正常运行。预发布环境的配置和生产环境完全一致,但只有内部人员能访问。 ### 正式发布 预发布测试正常后,鱼皮终于等到了上线的这一刻。大项目一般都会部署在多台机器上,所以不可能一台台登录机器去发布部署包。 一般公司会提供可视化发布平台,点选需要发布的机器(一般先灰度,选一小部分机器,再全量发布),点击一键发布,等项目管理员审批通过之后,就交给机器自动部署吧! 在云原生时代,很多公司使用 Kubernetes 进行容器编排和自动部署,发布流程变得更加自动化和标准化。 ## 后续 鱼皮曾天真地以为项目上线之后,就可以高枕无忧了。但后来发现,项目上线之后,同样需要保持警觉。虽然已经测试过,但仍然时不时会出现个预期之外的小 Bug,还是很考验心态的。  来看看上线之后,鱼皮做了哪些事呢? ### 监控运维 鱼皮会定期查看项目的监控面板,观察项目的运行情况、机器的负载、接口的响应时间、错误率等关键指标。 现代的监控系统非常强大,可以实时监控系统的各项指标,一旦发现异常就会立即告警。常用的监控工具有: - Prometheus + Grafana:开源监控方案 - ELK(Elasticsearch + Logstash + Kibana):日志收集和分析 - SkyWalking:分布式追踪系统 - 云厂商的监控服务(阿里云监控、腾讯云监控等) 鱼皮把自己学到的东西分享了出来,如果想要了解大厂是怎么监控项目的,可以看看 [Prometheus + Grafana 可观测性实战保姆级教程视频](https://www.bilibili.com/video/BV1QPYDztEtW)。 ### 统计分析 鱼皮在代码中添加了一些日志,可以利用 ELK 等日志收集可视化平台对这些日志进行分析,从而感知到用户的行为,进一步优化业务和系统。 比如我会统计用户执行 SQL 查询的耗时,对重复率高的慢 SQL 进行针对性地优化。 ### 事件反馈 有的时候,用户自己都不能清楚地描述 Bug,而且历史 Bug 也不方便找到。所以公司内部一般会有事件反馈平台,产品等内部同学在接收到 Bug 时,会在该平台发布一个 Bug 事件,详细描述 Bug 出现的时间、状况、详情等,便于我们开发集中分析和处理问题。  ### 文档沉淀 每次上线了新功能和项目,鱼皮都会通过写文档来记录项目的背景、设计方案、开发过程和一些坑点,便于后续其他同学了解项目,这是非常重要的!利人利己。 好的文档能够: - 帮助新人快速了解项目 - 记录技术决策和演进历程 - 避免重复踩坑 - 方便后续维护和迭代 ### 迭代优化 最后,一个需求的结束往往只是另一个需求的开始。像鱼皮最近在跟进的项目,一期做完做二期,二期还没做完三期就来了;还要抽出时间去优化以前的代码,这日子遥遥无期,没盼头啊! ## 研发流程对比 为了让大家更清楚地理解企业项目开发和个人项目开发的区别,鱼皮整理了一个对比表格: | 环节 | 个人项目 | 企业项目 | |------|----------|----------| | **需求** | 自己想做什么就做什么 | 需求评审、需求分析、排期 | | **设计** | 想到哪写到哪 | 架构设计、概要设计、详细设计、方案评审 | | **开发** | 一个人写代码 | 多人协作、分支管理、代码规范 | | **测试** | 自己点点看 | 单元测试、集成测试、系统测试、测试用例 | | **部署** | 随便部署 | 预发布、灰度发布、全量发布 | | **上线后** | 不管了 | 监控、日志分析、Bug 修复、迭代优化 | 可以看到,企业项目开发的流程要复杂得多,但这些流程都是为了保证项目的质量和稳定性。 ## 学习建议 ### 如何学习企业项目开发流程? 1)通过项目实战学习:最好的学习方式是参加真实的企业项目。如果还是学生或者刚入职,建议多观察团队的开发流程,主动参加各个环节。 2)跟着教程做完整项目:如果没有企业项目经验,可以跟着 [鱼皮的实战项目教程](https://www.codefather.cn/post/1797431216467001345) 学习。[编程导航的项目教程](https://www.codefather.cn/post/1797431216467001345) 都是按照企业开发流程来设计的,包含需求分析、架构设计、开发、测试、部署等完整环节。 3)学习 Git 和 CI/CD:版本控制和自动化部署是企业开发的基础,要重点学习。建议学习 [Git 学习路线](https://www.codefather.cn/course/1789189862986850306/section/1789190804671012866) 和 [CI/CD 持续集成学习路线](https://www.codefather.cn/course/1789189862986850306/section/1990755091384152066)。 4)了解敏捷开发:现代互联网公司大多采用敏捷开发,要了解 Scrum、Sprint、每日站会等概念。 ### 推荐的学习路线 总结一下,如果你想系统学习企业项目开发,建议按以下顺序学习相关技术: 1. [Git 和 GitHub 学习路线](https://www.codefather.cn/course/1789189862986850306/section/1789190804671012866):掌握版本控制 2. [软件工程学习路线](https://www.codefather.cn/course/1789189862986850306/section/1789190653197918210):理解开发规范和设计模式 3. [DevOps 工程师学习路线](https://www.codefather.cn/course/1789189862986850306/section/1990755061927555073):了解 DevOps 实践 4. [CI/CD 学习路线](https://www.codefather.cn/course/1789189862986850306/section/1990755091384152066):掌握自动化部署 5. 跟着 [鱼皮的项目教程](https://www.codefather.cn/post/1797431216467001345) 做完整项目:体验企业级项目开发流程 ## 推荐资源 ### 视频教程 - ⭐ [鱼皮的大厂项目开发流程视频](https://www.bilibili.com/video/BV11q4y1T7kY/):一线大厂项目是怎么做出来的 ### 项目实战 - ⭐ [编程导航 - 鱼皮原创项目教程系列](https://www.codefather.cn/post/1797431216467001345):企业级项目开发完整流程 - [项目学习建议](https://www.codefather.cn/post/xiangmu-xuexijianyi):如何学习项目 ### 工具平台 - [Git](https://git-scm.com/):版本控制系统 - [GitHub](https://github.com/) / [GitLab](https://about.gitlab.com/):代码托管平台 - [Jenkins](https://www.jenkins.io/):自动化构建工具 - [Jira](https://www.atlassian.com/software/jira):项目管理工具 - [禅道](https://www.zentao.net/):国产项目管理工具 ### 技术博客 - [Atlassian Blog](https://www.atlassian.com/blog):敏捷开发和项目管理 - [GitLab Blog](https://about.gitlab.com/blog/):DevOps 和协作流程 - [Spotify Engineering](https://engineering.atspotify.com/):Spotify 团队协作 - [Netflix TechBlog](https://netflixtechblog.com/):Netflix 研发效能 ## 写在最后 看完本文后,相信大家对企业项目开发流程有了更清晰的认识。企业开发和个人开发最大的区别在于:企业开发强调流程、规范、协作、质量保证,而个人开发更注重快速实现功能。 如果你想进入大厂工作,建议提前了解企业项目开发流程,学习相关的工具和方法论。跟着鱼皮的项目教程做几个完整的项目,实践企业级项目的开发流程,这会让你在求职和工作中更有竞争力。 推荐阅读鱼皮之前的文章:[大厂机密!30 个提升团队研发效能的锦囊](https://www.codefather.cn/post/1996475461198192641),了解更多大厂研发实践。 加油,鱼友们!  ## 程序员必备资源 1)程序员学习交流圈:[极客教程、实战项目、求职宝典](https://www.codefather.cn) 2)程序员面试八股文:[实习/校招/社招高频考点、企业真题解析](https://www.mianshiya.com) 3)程序员写简历神器:[专业模板、丰富例句、直通面试](https://www.laoyujianli.com) 4)AI 知识资源大全:[前沿技术、最新 AI 资讯、提示词大全](https://ai.codefather.cn) 5)1 对 1 模拟面试:[实习/校招/社招面试拿 Offer 必备](https://ai.mianshiya.com)
HBuilder (X)
地址:https://www.dcloud.io/hbuilderx.html
蓝湖
地址:https://lanhuapp.com/?home
ffmpeg
能够解码、编码、转码、混合、解密、流媒体、过滤和播放人类和机器创造的几乎所有东西。 地址:https://gitee.com/mirrors/ffmpeg
