简单封装一个白鹭H5开发脚手架

前言

       用白鹭开发H5小游戏有一段时间了,也开发了几款H5小游戏,这篇文章主要整理下开发过程的一些经验以及自己逐渐封装出来的一个开发脚手架。

脚手架源码地址

github.com/CB-ysx/egre…

创建项目

直接使用EgretLauncher创建一个项目

创建后,我们主要修改的就是src目录下的文件,先对文件内容进行一些删除(去掉一些没必要,或者说暂时用不到的代码)

Platform.ts文件

LoadingUI.ts文件

整个文件删除掉,等会我们再创建一个(相当于一个page,加载页面)

Main.ts文件

创建项目目录

先在项目src目录下创建一些文件夹,如下:

┗src
  ┣━base
  ┃  ┗━存放一些基类
  ┣━common
  ┃  ┗━存放一些公共的类
  ┣━component
  ┃  ┗━存放一些组件
  ┣━data
  ┃  ┗━存放数据
  ┣━net
  ┃  ┗━api、网络请求
  ┣━page
  ┃  ┗━页面
  ┣━Main.ts----入口文件
  ┗━Platform.ts----一些变量等的声明
复制代码

项目大概就按上面的结构划分,接下来就来写一些基类、公共类等的实现

创建视图的基类BaseView.ts

在base目录下创建BaseView.ts文件,BaseView类继承自egret.DisplayObjectContainer,可以当成是一个view容器,主要是实现一些view(弹窗、页面等)里面经常需要用到的方法。

class BaseView extends egret.DisplayObjectContainer {


    /**
     * 加载框(可用于网络请求前调用,或一些异步耗时操作前调用)
     */
    private _loading: egret.DisplayObjectContainer
    private _loadingIcon: egret.Bitmap

    /**
     * 记录是否弹出加载框
     */
    private isLoading: boolean = false


    /**
     * 加载动画
     */
    private loadingTween: egret.Tween

    public constructor() {
        super()
        this.addEventListener(egret.Event.ADDED_TO_STAGE, this.onAddToStage, this)
    }
    
    /**
     * 被添加到舞台
     */
    public onAddToStage(event: egret.Event) {
    }

    /**
     * 视图销毁
     */
    public onDestory() {
    }


    /**
     * 消息提示
     */
    protected toast(msg: string, duration = 2000, cb?: Function) {
	// 后面会写到如何封装该Toast类
        Toast.instance.show(msg, duration, cb)
    }

    /**
     * 显示加载框
     */
    protected showLoading() {
        if (this.isLoading) {
            return
        }
        this.isLoading = true
        if (!this._loading) {
            this._loading = new egret.DisplayObjectContainer()
            let bg = new egret.Shape()
            bg.graphics.beginFill(0x000000, 1)
            bg.graphics.drawRect(0, 0, this.width, this.height)
            bg.graphics.endFill()
            bg.alpha = 0.3
            bg.touchEnabled = true
            this._loading.addChild(bg)
        }
        if (!this._loadingIcon) {
            this._loadingIcon = ResUtil.createBitmap('loading_icon') // 这是自己封装的获取bitmap的方法,后面会写到
            this._loadingIcon.width = 50
            this._loadingIcon.height = 50
            this._loadingIcon.anchorOffsetX = this._loadingIcon.width / 2
            this._loadingIcon.anchorOffsetY = this._loadingIcon.height / 2
            this._loadingIcon.x = this.width / 2
            this._loadingIcon.y = this.height / 2
            this._loading.addChild(this._loadingIcon)
        }
        egret.MainContext.instance.stage.addChild(this._loading)
        this.loadingTween = egret.Tween
                                .get(this._loadingIcon, {loop: true})
                                .to({
                                    rotation: 360
                                }, 500, egret.Ease.sineIn)
    }

    /**
     * 关闭加载框
     */
    protected closeLoading() {
        if (!this.isLoading) {
            return
        }
        if (this.loadingTween) {
            this.loadingTween.setPaused(true)
        }
        this.loadingTween = null
        egret.MainContext.instance.stage.removeChild(this._loading)
        this.isLoading = false
    }
}
复制代码

创建页面基类BaseScene.ts

在base目录下创建BaseScene.ts文件,BaseScene类继承自BaseView,游戏一般以场景区分,比如开始场景、游戏场景、结束场景等,这里就理解为页面,所以创建一个场景基类,设置一些默认值,比如全屏宽高等,当然,可能部分场景不需要全屏,那可以继承BaseScene然后重写宽高,或者使用BaseView。

class BaseScene extends BaseView {

    /**
     * 场景名称
     */
    private _pageName: string = ''

    public constructor(pageName: string = '') {
        super()
        this._pageName = pageName
        // 设置场景宽高为舞台的宽高
        this.width = GameUtil.getStageWidth()
        this.height = GameUtil.getStageHeight()
        // 防止页面点击穿透
        this.touchEnabled = true
    }

    public get pageName() {
        return this._pageName
    }

    /**
     * 关于场景动画部分,后面写路由时会写到
     */
    /**
     * 进入动画执行结束
     */
    public afterEnterAnimation() {
        // console.log(this._pageName, 'afterEnterAnimation')
    }

    /**
     * 离开动画执行结束
     */
    public afterLeaveAnimation() {
        // console.log(this._pageName, 'afterLeaveAnimation')
    }
}
复制代码

我们的场景基类就这么简单,以后如果还有通用的场景方法,可以继续添加到这个类中。 因为继承自BaseView,所以场景中也可以直接使用BaseView中封装的public方法,比如:用 this.toast() 方法来弹出提示。

Toast.ts

这是一个提示组件,在component目录下创建Toast.ts文件,使用单例模式,统一管理提示。

class ToastMsg { // 消息格式
    public msg: string
    public duration: number = 2000
    public cb: Function

    public constructor(msg, duration, cb) {
        this.msg = msg
        this.duration = duration
        this.cb = cb
    }
}

/**
 * 这是模仿Android中的Toast
 * 实现弹出消息提示,并自动关闭
 */
class Toast {

    /**
     * 消息列表
     */
    private _msgList: Array<ToastMsg> = new Array<ToastMsg>()

    private _toasting: boolean = false

    private _stage: egret.Stage // 舞台

    private _toastContainer: egret.DisplayObjectContainer
    private _shapeBg: egret.Bitmap
    
    public static toast: Toast

    public constructor() {
        // toast到舞台上,保证消息显示在最上层
        this._stage = egret.MainContext.instance.stage
        this._toastContainer = new egret.DisplayObjectContainer()
    }

    public static get instance() {
        if (!this.toast) {
            this.toast = new Toast()
        }
        return this.toast
    }

    public show(msg: string, duration = 2000, cb?: Function) {
        // 将消息存进列表
        this._msgList.push(new ToastMsg(msg, duration, cb))

        // 如果当前在显示消息,那么不掉用显示方法,里面轮询会调用到的
        if (!this._toasting) {
            this._toasting = true
            this._show()
        }
    }

    private _show() {
	// 列表长度大于0,说明队列中还有消息,继续弹出
        if(this._msgList.length > 0) {
	    // 获取第一条消息
            let msg = this._msgList.shift()

            this._toastContainer.alpha = 0

            // 创建文本
            let textField: egret.TextField = new egret.TextField()
            textField.text = msg.msg
            textField.textAlign = 'center'
            textField.textColor = 0xffffff
            textField.size = 24

            // 文本与黑色背景边缘的宽度
            let space = 40

            // 如果文字宽度超过屏幕宽度的0.8倍,就设置一下(相当于设置外容器的最大宽度,暂时只想到这个办法)
            if (textField.textWidth >= GameUtil.getStageWidth() * 0.8) {
                textField.width = GameUtil.getStageWidth() * 0.8 - space
            }
            // 设置装文本的容器的宽高和锚点,文本的锚点
            this._toastContainer.width = textField.width + space
            this._toastContainer.height = textField.height + space

            this._toastContainer.anchorOffsetX = this._toastContainer.width / 2
            this._toastContainer.anchorOffsetY = this._toastContainer.height

            textField.anchorOffsetX = textField.width / 2
            textField.anchorOffsetY = textField.height / 2

            textField.x = this._toastContainer.width / 2
            textField.y = this._toastContainer.height / 2

            // 容器的位置,在底部横向居中
            this._toastContainer.x = GameUtil.getStageWidth() / 2
            this._toastContainer.y = GameUtil.getStageHeight() * 0.8

            let shapeBg = new egret.Shape()
            shapeBg.graphics.beginFill(0x000000, 1)
            shapeBg.graphics.drawRoundRect(0, 0, this._toastContainer.width, this._toastContainer.height, 20, 20)
            shapeBg.graphics.endFill()

            this._toastContainer.addChild(shapeBg)
            this._toastContainer.addChild(textField)

            this._stage.addChild(this._toastContainer)
            
            // 弹出和消失动画
            egret.Tween
                .get(this._toastContainer)
                .to({
                    alpha: 1
                }, 200, egret.Ease.sineIn)
                .wait(msg.duration) // 等待xxx毫秒后再消失
                .to({
                    alpha: 0
                }, 200, egret.Ease.sineIn)
                .wait(100)
                .call(()=> {
                    this._toastContainer.removeChild(shapeBg)
                    this._toastContainer.removeChild(textField)
                    this._stage.removeChild(this._toastContainer)
                    msg.cb && msg.cb()
                    this._show() // 继续显示下一条消息
                }, this)
        } else {
            this._toasting = false
        }
    }
}
复制代码

GameUtil.ts

接下来创建一个工具类,方便用于获取舞台宽高等,在common中创建GameUtil.ts

/**
 * 工具类
 */
/**
 * 工具类
 */
class GameUtil {

    public static isIos: boolean = /iphone|ipad|ipod/.test(navigator.userAgent.toLowerCase())

    public static getTopStage(): egret.Stage {
        return egret.MainContext.instance.stage
    }
    /**
     * 获取舞台高度
     */
    public static getStageHeight(): number {
        return egret.MainContext.instance.stage.stageHeight
    }

    /*
     * 获取舞台宽度
     */
    public static getStageWidth(): number {
        return egret.MainContext.instance.stage.stageWidth
    }

    // 是容器可点击
    public static tap(bitmap: egret.DisplayObject, callback, thisObject) {
        bitmap.touchEnabled = true
        bitmap.addEventListener(egret.TouchEvent.TOUCH_TAP, callback, thisObject)
    }

    // 创建圆角矩形
    public static drawRoundRect(shape: egret.Shape, color: number, width: number, height: number, round: number, rArr: Array<number>) {
        shape.graphics.clear()
        shape.graphics.beginFill(color, 1)
        shape.graphics.drawRoundRect(0, 0, width, height, round, round)
        shape.graphics.endFill()
        let roundArr: Array<number> = [0, 1, 2, 3].filter(item=> rArr.indexOf(item) === -1)
        let rectData: Array<Array<number>> = [
            [0, 0],
            [1, 0],
            [0, 1],
            [1, 1]
        ]
        for (let i = 0;i < roundArr.length;++i) {
            let x = (width - round) * rectData[roundArr[i]][0]
            let y = (height - round) * rectData[roundArr[i]][1]
            shape.graphics.beginFill(color, 1)
            shape.graphics.drawRect(x, y, round, round)
            shape.graphics.endFill()
        }
    }
}
复制代码

ResUtil.ts

资源管理,用于获取图片等,在common中创建ResUtil.ts

class ResUtil {
    private bitmapList: {[index: string]: egret.Texture} = {}
    private movieClipList: {[index: string]: egret.MovieClip} = {}

    private static resUtil: ResUtil

    public static get instance() {
        if (!this.resUtil) {
            this.resUtil = new ResUtil()
        }
        return this.resUtil
    }

    /**
     * 加载网络图片
     * item: {
     *  url: 'xxxx' // 图片地址
     *  xxxx // 自己附带其他参数
     * }
     */
    public static loadImageByUrl(item, callback) {
        try {
            RES.getResByUrl(item.url, (event)=> {
                callback && callback({
                    status: 1,
                    event: event,
                    item: item
                })
            }, this, RES.ResourceItem.TYPE_IMAGE)
        } catch (e) {
            console.error(e)
            callback && callback({
                status: 0
            })
        }
    }

    /**
     * 创建图片
     * @param name 
     * @param suffix 
     */
    public static createBitmap(name: string, suffix: string = 'png'): egret.Bitmap {
        const key = `${name}_${suffix}`
        let result = new egret.Bitmap()
        if (!this.instance.bitmapList[key]) {
            this.instance.bitmapList[key] = RES.getRes(key)
        }
        result.texture = this.instance.bitmapList[key]
        return result
    }

    /**
     * 创建动图
     * @param name 
     * @param suffix 
     */
    public static createMovieClip(name: string): egret.MovieClip {
        let result = new egret.MovieClip()
        if (!this.instance.movieClipList[name]) {
            const data_stay: any = RES.getRes(name + '_json')
            const texture_stay: egret.Texture = RES.getRes(name + '_png')
            const mcFactory_stay: egret.MovieClipDataFactory = new egret.MovieClipDataFactory(data_stay, texture_stay)
            this.instance.movieClipList[name] = new egret.MovieClip(mcFactory_stay.generateMovieClipData(name))
        }
        result.movieClipData = this.instance.movieClipList[name].movieClipData
        return result
    }

    /**
     * 获取动图的最大宽高
     * @param name 
     * @param suffix 
     */
    public static getMoviceClipWH(name: string): {w: number, h: number} {
        let mw = 0
        let mh = 0
        const movie = ResUtil.createMovieClip(name)
        const tData = movie.movieClipData.textureData
        for (let key in tData) {
            mw = Math.max(tData[key].w, mw)
            mh = Math.max(tData[key].h, mh)
        }
        return {
            w: mw,
            h: mh
        }
    }
}
复制代码

路由

有了以上一些基类,我们还需要做一个页面路由跳转控制,这里模仿vue-router的使用方法,自己实现一个简单的Router,直接在src目录下创建Router.ts文件。

class Router {

	private static router: Router

    	private _stage: egret.DisplayObjectContainer // 场景容器

	/**
	 * 当前路由信息
	 */
	private _route: {
		name: string,
		meta: any,
		params: any,
		scene: BaseScene
	} = {name: '', meta: null, scene: null, params: null}

	private _params: {
		meta: any,
		params: any,
	} = {meta: null, params: null}

	/**
	 * 路由映射表
	 */
	private _routeMap: SelfMap<{
		className: any,
		name: string,
		meta: any,
		params?: any
	}> = new SelfMap<{
		className: any,
		name: string,
		meta: any,
		params?: any
	}>()

	/**
	 * 页面离开动画
	 */
	private _leaveAnimation: SelfAnimation

	/**
	 * 页面进入动画
	 */
	private _enterAnimation: SelfAnimation

	/**
	 * 
	 */
	private _beforeEach: Function = (to, from, next)=> {next()}

	private constructor() {
	}

    	public static get instance() {
        	if (!this.router) {
            		this.router = new Router()
        	}
        	return this.router
    	}

	public static get params() {
		return this.instance._params.params
	}

	public static get meta() {
		return this.instance._params.meta
	}

	/**
	 * 初始化,设置放置页面的容器
	 */
	public static init(main: egret.DisplayObjectContainer) {
        	if (this.instance._stage) {
            		console.log('已经创建过场景容器')
            		return
        	}
        	if (!main) {
            		console.log('主场景不能为空')
            		return
        	}
		// 创建一个新容器放在main上,专门用来存放页面,这样能保证toast一直在所有页面上
		this.instance._stage = new egret.DisplayObjectContainer()
        	main.addChild(this.instance._stage)
		return this
	}

	/**
	 * 注册页面
	 */
	public static register(key: string, className: any, meta: any = null) {
		this.instance._routeMap.put(key, {className: className, name: key, meta: meta})
		return this
	}

	/**
	 * 跳转路由之前做的事
	 */
	public static beforeEach(beforeEach: Function) {
		this.instance._beforeEach = beforeEach
		return this
	}

	/**
	 * 设置页面离开动画
	 */
	public static setLeaveAnimation(animation?: SelfAnimation) {
		this.instance._leaveAnimation = animation
		return this
	}

	/**
	 * 设置页面进入动画
	 */
	public static setEnterAnimation(animation?: SelfAnimation) {
		this.instance._enterAnimation = animation
		return this
	}

	/**
	 * 退出页面,有动画就执行动画
	 */
	private leavePage(page: BaseScene, callback: Function, leaveAnimation: SelfAnimation) {
		if (!page) {
			callback && callback()
			return
		}
		let animation = leaveAnimation || this._leaveAnimation

		page.onDestory()
		if (animation) {
			animation.execute(page, ()=> {
				callback && callback()
			})
		} else {
			callback && callback()
		}
	}

	/**
	 * 加载页面,有动画就执行动画
	 */
	private enterPage(page: BaseScene, callback: Function, enterAnimation: SelfAnimation) {
		let animation = enterAnimation || this._enterAnimation
		if (animation) {
			animation.beforeAnim(page)
		}
		this._stage.addChild(page)
		if (animation) {
			animation.execute(page, ()=> {
				callback && callback()
			})
		} else {
			callback && callback()
		}
	}

	private _currentLeavePage: BaseScene = null

	public static to(config: {
		name: string,
		params?: any,
		leaveAnimation?: SelfAnimation,
		enterAnimation?: SelfAnimation
	}) {
		let page = this.instance._routeMap.get(config.name)
		if (page == undefined) {
			console.error(`scene ${config.name} is not register`)
			return
		}
		let currentRoute = this.instance._route
		this.instance._beforeEach(page, currentRoute, (to: string = config.name)=> {
			if (to != config.name) {
				config.name = to
				page = this.instance._routeMap.get(config.name)
				if (page == undefined) {
					console.error(`scene ${config.name} is not register`)
					return
				}
			}
			let currentLeavePage = this.instance._currentLeavePage
			// 如果当前离开的页面跟正要离开的页面一样,不执行
			if (currentLeavePage && currentRoute.scene && currentLeavePage.pageName === currentRoute.scene.pageName) {
				return
			}
			// 如果要进入的页面跟当前页面一样,不执行
			if (config.name === currentRoute.name) {
				return
			}
			this.instance._currentLeavePage = currentRoute.scene
			currentLeavePage = currentRoute.scene
			this.instance.leavePage(currentRoute.scene, ()=> {
				currentLeavePage && currentLeavePage.afterLeaveAnimation()
				let newPage = new page.className(config.name)
				this.instance._params.meta = page.meta
				this.instance._params.params = config.params
				this.instance.enterPage(newPage, ()=> {
					currentLeavePage && this.instance._stage.removeChild(currentLeavePage)
					currentLeavePage = this.instance._currentLeavePage = null
					currentRoute.name = config.name
					currentRoute.meta = page.meta
					currentRoute.scene = newPage
					currentRoute.params = config.params
					newPage.afterEnterAnimation()
				}, config.enterAnimation)
			}, config.leaveAnimation)
		})
	}
}
复制代码

其中用到了SelfMap和SelfAnimation都是自己实现的简单的工具类,这里就不介绍,可以在源码中查看。

使用方法

在入口文件Main.ts的runGame方法中注册路由

...
private async runGame() {
        Router.init(this)
            .setLeaveAnimation(new SelfAnimation().add({alpha: 0}, 300))// 设置页面离开动画
            .setEnterAnimation(new Animation().init({alpha: 0}).add({alpha: 1}, 300)) // 设置页面进入动画
            .register('loading', LoadingScene) // 注册加载页
            .register('game', GameScene) // 注册游戏场景路由
            .to({ // 跳转到加载页页面
                name: 'loading'
            })
        ...
}
...

// 在其他地方跳转页面可以直接使用
// Router.to({name: xxxx})
复制代码

加载页

主要用于显示加载资源进度以及做一些初始化操作

class LoadingScene extends BaseScene {

    private progressBar: egret.Shape
    private progressText: egret.TextField
    private barWidth: number = 0
    private barHeight: number = 0
    private startTime: number = 0

    private preload: number = 2 // preload资源组的数量
    private main: number = 100 - this.preload

    public async onAddToStage() {
        this.drawBg();
        this.initProgress();

        try {
            this.startTime = new Date().getTime()
            await RES.loadConfig(`resource/default.res.json?v=${Math.random()}`, "resource/")
            await RES.loadGroup("preload", 0, {
                onProgress: (current: number, total: number)=> {
                    let progress = current / total * this.preload
                    let width = this.barWidth * progress / 100
                    this.drawProgress(width, Math.floor(progress))
                }
            })
            // 因为logo是图片,需要等资源加载回来才可以绘制
            this.drawLogo();
            this.load()
        } catch(e) {
            console.error(e)
        }
    }

    private async load() {
        await RES.loadGroup("main", 0, {
            onProgress: (current: number, total: number)=> {
                let progress = current / total * this.main + this.preload
                let width = this.barWidth * progress / 100
                this.drawProgress(width, Math.floor(progress))
            }
        })
    }

    private initProgress() {
        this.barWidth = this.width * 0.6
        this.barHeight = 20
        this.progressBar = new egret.Shape()
        this.progressBar.width = this.barWidth
        this.progressBar.height = this.barHeight
        this.progressBar.x = (this.width - this.barWidth) / 2
        this.progressBar.y = (this.height - this.barHeight) / 2
        this.addChild(this.progressBar)

        this.progressText = new egret.TextField()
        this.progressText.textColor = 0x005660
        this.progressText.size = 28
        this.progressText.strokeColor = 0xFFFFFF
        this.progressText.stroke = 3
        this.progressText.width = this.width
        this.progressText.textAlign = 'center'
        this.progressText.text = '0%'
        this.progressText.y = this.progressBar.y - this.progressText.height - 20
        this.addChild(this.progressText)
    }

    private drawProgress(width, progress) {
        this.progressBar.graphics.clear()
        this.progressBar.graphics.beginFill(0xFFFFFF, 1)
        this.progressBar.graphics.drawRoundRect(0, 0, width, this.barHeight, this.barHeight, this.barHeight)
        this.progressBar.graphics.endFill()
        this.progressText.text = `${progress}%`
        if (progress == 100) {
            let diff = new Date().getTime() - this.startTime
            diff = diff < 1000 ? (1000 - diff) : 300
            egret.setTimeout(()=> {
                // 加载完成跳转到游戏页面
                Router.to({name: 'game'})
            }, this, diff)
        }
    }

    private drawBg() {
        let bg: egret.Shape = new egret.Shape();
        bg.graphics.beginFill(0x56A1D2);
        bg.graphics.drawRect(0, 0, this.width, this.height);
        bg.graphics.endFill();
        this.addChild(bg);
    }

    private drawLogo() {
        let logo: egret.Bitmap = ResUtil.createBitmap('egret_icon');
        logo.x = (this.width - logo.width) / 2
        this.addChild(logo);
    }
}
复制代码

游戏页面

class GameScene extends BaseScene {

    public async onAddToStage() {
        let bg: egret.Bitmap = ResUtil.createBitmap('bg', 'jpg');
        bg.width = this.width;
        bg.height = this.height;
        this.addChild(bg);

        let btn: egret.Shape = new egret.Shape();
        GameUtil.drawRoundRect(btn, 0xFFFFFF, 300, 100, 10, [0, 1, 2, 3]);
        btn.x = (this.width - btn.width) / 2;
        btn.y = (this.height - btn.height) / 2;
        this.addChild(btn);

        let btnText: egret.TextField = new egret.TextField();
        btnText.text = '点击';
        btnText.textColor = 0x000000;
        btnText.x = (this.width - btnText.width) / 2;
        btnText.y = (this.height - btnText.height) / 2;
        this.addChild(btnText);

        GameUtil.tap(btn, ()=> {
            this.toast('点击按钮');
        }, this)
    }
}
复制代码

封装网络请求工具

这里使用的是egret.HttpRequest来封装的,当然也可以自己引入axios等框架。

Http.ts

在net目录下创建Http.ts,对egret.HttpRequest封装,支持promise

class Http {

    private baseUrl: string = ''

    public static http: Http

    public static get instance() {
        if (!this.http) {
            this.http = new Http()
        }
        return this.http
    }

    private constructor() {
        
    }

    private request({method = egret.HttpMethod.GET, url, params = {}, headers = {}}): Promise<any> {
        if (!(/http(|s):\/\//.test(url))) {
            url = this.baseUrl + url
        }
        let _params: any = ''
        let _headers = {}
        _headers['Content-Type'] = 'application/x-www-form-urlencoded'
        // 如果有传入,则覆盖掉
        for (let key in headers) {
            _headers[key] = headers[key]
        }
        if (_headers['Content-Type'] === 'application/json') {
            _params = JSON.stringify(params)
            _params = _params.replace(/\+/g, "%2B").replace(/\&/g, "%26")
            // console.log(_params)
        } else {
            for (let key in params) {
                _params += `${key}=${('' + params[key]).replace(/\&/g, "%26")}&`
            }
            _params = _params.replace(/\+/g, "%2B")
            // console.log(_params)
            if (_params.length > 0) {
                _params = _params.substring(0, _params.length - 1)
            }
            if (method === egret.HttpMethod.GET) {
                url += `?${_params}`
            }
        }

        return new Promise((resolve, reject)=> {
            let request = new egret.HttpRequest()
            request.responseType = egret.HttpResponseType.TEXT
            request.open(url, method)
            for (let key in _headers) {
                request.setRequestHeader(key, _headers[key])
            }
            if (method === egret.HttpMethod.GET) {
                request.send()
            } else {
                request.send(_params)
            }
            request.addEventListener(egret.Event.COMPLETE, (event)=> {
                dealResult(event)
            }, this)
            request.addEventListener(egret.IOErrorEvent.IO_ERROR, (event)=> {
                dealResult(event, false)
            }, this)
            function dealResult(event, success = true) {
                let response
                try {
                    response = JSON.parse(request.response)
                } catch(e) {
                    response = request.response
                }
                if (success) {
                    resolve(response)
                } else {
                    reject(response)
                }
            }
        })
    }

    public setBaseUrl(baseUrl: string) {
        this.baseUrl = baseUrl
    }

    public get(url: string, params = {}, headers = {}): Promise<any> {
        return this.request({
            url: url,
            params: params,
            headers: headers
        })
    }

    public post(url: string, params = {}, headers = {}): Promise<any> {
        return this.request({
            method: egret.HttpMethod.POST,
            url: url,
            params: params,
            headers: headers
        })
    }
}
复制代码

Api.ts

同样在net目录下创建Api.ts文件,用于管理自己的api接口

class Api {

    private static api: Api

    private constructor() {
        let baseUrl: string = 'http://xxx.com'
        if (DEBUG) {
            baseUrl = 'http://localhost:3000'
        }
        Http.instance.setBaseUrl(baseUrl)
    }

    public static get instance() {
        if (!this.api) {
            this.api = new Api()
        }
        return this.api
    }

    /**
     * 统一处理结果
     */
    private predeal(http: Promise<any>): Promise<any> {
        return http.then(response=> {
                    if (response.code == '0') {
                        // 这个结构根据自己的数据确定
                        return Promise.resolve(response.payload.data || response.payload || response)
                    } else {
                        return Promise.reject(response)
                    }
                })
                .catch(response=> {
                    return Promise.reject(response.msg || '请求出错')
                })
    }

    /**
     * get
     */
    private get(url: string, params = {}, headers = {}): Promise<any> {
        return this.predeal(Http.instance.get(url, params, headers))
    }

    /**
     * post
     */
    private post(url: string, params = {}, headers = {}): Promise<any> {
        return this.predeal(Http.instance.post(url, params, headers))
    }

    /**
     * get例子
     */
    public getInfo(): Promise<any> {
        return this.get('/info')
    }

    /**
     * post例子
     */
    public postInfo(params): Promise<any> {
        return this.post('/info', params)
    }
}
复制代码

音频管理

现在H5基本都离不开bgm等音乐的播放,因此,脚手架也封装了一个音频管理类,方便使用,这个写得比较粗糙,还需要优化。具体代码在项目中可查看。

localstorage存储

封装了CacheStorge类,用于管理localstorage存储数据,可直接保存获取对象等数据,详见common/CacheStorge.ts文件

微信jssdk相关

引入jssdk

引入外部js库,需要特殊处理,这里处理过,可以参照项目,在添加lib目录里的wxjssdk,并且在egretProperties.json的modules里面配置路径

{
  "engineVersion": "5.2.17",
  "compilerVersion": "5.2.17",
  "template": {},
  "target": {
    "current": "web"
  },
  "modules": [
    {
      "name": "egret"
    },
    {
      "name": "game"
    },
    {
      "name": "tween"
    },
    {
      "name": "assetsmanager"
    },
    {
      "name": "promise"
    },
		{
			"name": "wxjssdk",
			"path": "./libs/wxjssdk"
		}
  ]
}
复制代码

封装自己的wxsdk

封装了获取签名信息,已经分享接口,先调用getWxSignPackage获取签名配置,该方法里的数据处理需要根据自己的接口数据做处理。

class WxSdk {

    private signPackage: BodyConfig = new BodyConfig()

    public static wxsdk: WxSdk

    public static get instance() {
        if (!this.wxsdk) {
            this.wxsdk = new WxSdk()
        }
        return this.wxsdk
    }

    private constructor() {
        
    }

    /**
     * 配置微信签名
     */
    public configWx(): Promise<any> {
        // 这里有个坑。用//api.24haowan.com时,nonceStr是大写。用平台时是:noncestr。切换时记得切换
        if (!this.signPackage.appId || !this.signPackage.timestamp ||
            !this.signPackage.nonceStr || !this.signPackage.signature) {
            return Promise.reject('微信签名参数错误')
        }
        this.signPackage.debug = false
        this.signPackage.jsApiList = [
                // 所有要调用的 API 都要加到这个列表中
                'onMenuShareTimeline', 'onMenuShareAppMessage', 'hideMenuItems',
                // 录音相关
                'startRecord', 'stopRecord', 'onVoiceRecordEnd', 'playVoice', 'onVoicePlayEnd', 'pauseVoice', 'stopVoice', 'uploadVoice', 'downloadVoice',
            ]
        /* 微信接口 */
        wx.config(this.signPackage)
        wx.error(err=> {
            console.error('error: configWx of wxsdk.ts', err)
        })
        return Promise.resolve('微信签名配置完成')
    }

    /**
     * 获取微信签名信息
     */
    public getWxSignPackage(): Promise<any> {
        let params = {
            url: encodeURIComponent(window.location.href.split('#')[0])
        }
        // 获取微信签名
        return Http.instance.get('xxxxx', params)
                .then(response=> {
                    let data = response.payload
                    if (!data.appId) {
                        return Promise.reject(response.msg || response)
                    }
                    this.signPackage.appId = data.appId
                    this.signPackage.timestamp = data.timestamp
                    this.signPackage.nonceStr = data.nonceStr
                    this.signPackage.signature = data.signature
                    return Promise.resolve(this.configWx())
                })
                .catch(error=> {
                    console.error('error: getWxSignPackage of wxsdk.js', JSON.stringify(error))
                    return Promise.reject(error)
                })
    }

    public share(shareInfo: ShareBody) {
        if (!shareInfo.title) {
            shareInfo.title = '分享标题'
        }
        if (!shareInfo.link) {
            shareInfo.link = window.location.href.split('#')[0]
        }
        if (!shareInfo.desc) {
            shareInfo.desc = '分享描述'
        }
        wx.ready(()=> {
            console.log('wx ready', shareInfo)
            wx.onMenuShareAppMessage(shareInfo)
            wx.onMenuShareTimeline(shareInfo)
            wx.onMenuShareQQ(shareInfo)
            wx.onMenuShareWeibo(shareInfo)
        })
    }
}
复制代码

结束

至此,一个简单的框架就基本完成了,可能还不太完善,但已经可以满足一些简单的需求了,本人已经使用这个结构开发了三四个游戏并都顺利上线。

最后再附上github地址: github.com/CB-ysx/egre…

原文链接:codebear.cn/article?id=…

转载于:https://juejin.im/post/5cfa31fd518825035746e5b4

猜你喜欢

转载自blog.csdn.net/weixin_34025151/article/details/91413666