Egret社区

超大型H5转小游戏要点

2019-5-21 15:34
65113
本帖最后由 落幕夜未央 于 2019-5-21 15:34 编辑

这里所谓的超大型H5,是指H5项目使用egret publish发布的压缩之后的main.min.js已经接近或者超过4M(超过7M你这游戏肯定有毒……然后是铁定转不了的,因为稍微瞄一眼小游戏工程,白鹭与小游戏的适配库,然后白鹭引擎自身的相关库就差不多合起来有1M)的大型游戏,
这里列出的思路同时适用于微信小游戏、百度小游戏(oppo小游戏我没尝试去做过,但是oppo小游戏是基于微信小游戏的,想来也没什么大问题)。微信小游戏和百度小游戏之间有差异的部分,文末会单独列出来。

一、升级引擎版本将h5游戏引擎升级到小游戏所需要的引擎版本,最好是直接升级到最新的引擎版本(写此文时最新引擎为5.2.19),确保H5工程能够无报错进游戏。还是可以继续选用res库,不用强行使用assetmanager。


二、皮肤命名空间与代码命名空间
官方文档说的很明确了,小游戏皮肤exml文件中的class定义和代码中命名空间不能一样。比如聊天主界面皮肤类名是chat.ChatWinSkin,那代码类中就不能再用chat作为命名空间了,否则很有可能在运行时提示找不到xxx类定义。
既然说了是超大游戏,那么代码量那么大,手动去检查已经不可能了,下面贡献一个脚本。放在项目根目录/publish/publishSmallGame.后面两个目录名你可以自行定义。
[JavaScript] 纯文本查看 复制代码
/**
 * @User: liuliqianxiao
 * @Date: 2019-05-10
 * @Time: 17:04
 * @Desc: 检测命名空间,代码最好不要带命名空间,否则就很容易和皮肤的命名空间冲突
 * 然而小游戏不允许组件的类与皮肤的类在同一命名空间之下
 **/
var path = require("path");
var fs = require("fs");

var nameSpaceRegExp = /namespace\s+([a-zA-Z0-9$_]*)/g;//代码中命名空间匹配组
var themClassRegExp = /class\s*=\s*"([^"]+)"/g;//皮肤中的类名检测
var classRegExp = /(class|enum)\s+([a-zA-Z0-9$_]*)(?=(\s+extends)|(\s+implements)|(\s*{)|(<[^>]*>))/g;//代码中类名匹配组

//TODO --- 源代码目录以及exml皮肤目录路径定义
var codeDir = path.resolve("../../src");
var exmlDirs = [
    path.resolve("../../resource/gameLoginViews"),
    path.resolve("../../resource/views"),
];

var codeUsedNameSpace = [];
var themUsedNameSpace = [];

var confitFullClassList = [];

exmlDirs.forEach(dirPath => {
    loopHandleExmls(dirPath, false);
});
loopHandleTsFiles(codeDir);
exmlDirs.forEach(dirPath => {
    loopHandleExmls(dirPath, true);
});

console.log(confitFullClassList);


function loopHandleTsFiles(dirPath) {
    let fileList = fs.readdirSync(dirPath);
    fileList.forEach((fileName) => {
        let fullPath = path.join(dirPath, fileName);
        if (fs.statSync(fullPath).isDirectory()) {
            loopHandleTsFiles(fullPath);
        } else {
            handleOneTsFile(fullPath);
        }
    });
}

function handleOneTsFile(filePath) {
    if (path.extname(filePath) !== ".ts") {
        return;
    }
    //game.ts是协议文件,协议文件设定必须要命名空间
    if (path.basename(filePath) === "game.ts") {
        return;
    }
    //不处理*.d.ts文件
    if (filePath.indexOf(".d.ts") !== -1) {
        return;
    }
    let codeContent = fs.readFileSync(filePath, "utf-8");

    let currentNameSpace = "";
    let nameSpaceMatchCount = 0;

    let nameSpaceMatch = nameSpaceRegExp.exec(codeContent);
    while (nameSpaceMatch) {
        currentNameSpace = nameSpaceMatch[1];
        nameSpaceMatchCount++;
        if (nameSpaceMatchCount > 1) {
            console.log(`${filePath}含有多个命名空间,请检查!`);
            return;
        }
        nameSpaceMatch = nameSpaceRegExp.exec(codeContent);
    }

    if (!currentNameSpace)
        return;

    if (codeUsedNameSpace.indexOf(currentNameSpace) === -1) {
        codeUsedNameSpace.push(currentNameSpace);
        // console.log(`${filePath}用到了命名空间${currentNameSpace}`);
    }

    if (themUsedNameSpace.indexOf(currentNameSpace) !== -1) {
        console.warn(`${filePath}代码使用了和皮肤类定义相同的命名空间${currentNameSpace}`);

        let classMatch = classRegExp.exec(codeContent);
        while (classMatch) {
            let className = classMatch[2];
            let fullClassName = currentNameSpace + "." + className;
            confitFullClassList.push(fullClassName);
            classMatch = classRegExp.exec(codeContent);
        }
    }
}


//遍历所有exml文件,找出所有皮肤中引用到的自定义组件。这些基本上都是要暴露到全局中去的
function loopHandleExmls(dirPath, isReplaceCode) {
    let fileList = fs.readdirSync(dirPath);
    fileList.forEach((fileName) => {
        let fullPath = path.join(dirPath, fileName);
        if (fs.statSync(fullPath).isDirectory()) {
            loopHandleExmls(fullPath, isReplaceCode);
        } else {
            if (isReplaceCode) {
                replaceOneExmlFile(fullPath);
            } else {
                handleOneExmlFile(fullPath);
            }
        }
    });
}

function replaceOneExmlFile(filePath) {
    if (path.extname(filePath) !== ".exml") {
        return;
    }
    let exmlContent = fs.readFileSync(filePath, "utf-8");

    let hasChange = false;
    //直接去除有冲突代码命名空间
    confitFullClassList.forEach(item => {
        let tempArray = item.split(".");
        let confitText = tempArray.join(":");
        if (exmlContent.indexOf(confitText) !== -1) {
            let replaceRegExp = new RegExp(confitText, "g");
            let replaceRegExp1 = new RegExp("xmlns:" + tempArray[0] +
                "\\s*=\\s*\"" + tempArray[0] + "\.\*\"", "g");
            console.log(replaceRegExp1.source);
            exmlContent = exmlContent.replace(replaceRegExp, "ns1:" + tempArray[1]);

            if (exmlContent.indexOf("xmlns:ns1=\"*\"") === -1) {
                exmlContent = exmlContent.replace(replaceRegExp1, "xmlns:ns1=\"*\"");
            } else {
                exmlContent = exmlContent.replace(replaceRegExp1, "");
            }

            hasChange = true;
        }
    });

    if (hasChange) {
        fs.writeFileSync(filePath, exmlContent, "utf-8");
    }

}

function handleOneExmlFile(filePath) {
    if (path.extname(filePath) !== ".exml") {
        return;
    }
    let exmlContent = fs.readFileSync(filePath, "utf-8");

    let matchArray = themClassRegExp.exec(exmlContent);
    while (matchArray) {

        let fullClassName = matchArray[1];

        if (fullClassName.indexOf(".") !== -1) {
            let nameSpace = fullClassName.split(".")[0];

            if (themUsedNameSpace.indexOf(nameSpace) === -1) {
                themUsedNameSpace.push(nameSpace);
            }
        } else {
            console.info(`${filePath}的皮肤类名${fullClassName}竟然没有带命名空间,请规范类名~~~`);
        }
        matchArray = themClassRegExp.exec(exmlContent);
    }
}

这个脚本有这么几个限制条件:①只支持一层命名空间,皮肤类定义chat.ChatWinSkin,组件类名chat.ChatWin,都是一层命名空间,当然想支持多层你就得自行修改脚本了,也是很容易的啦。②自己的源代码都放在src下。③不允许在不同的命名空间下有相同的类名。比如chat命名空间下有个Test类,然后friend命名空间下也有个Test类。(顺便解释下为什么有③这条奇怪的限制,本来ts语法支持命名空间不就是为了解决类名重复问题的么。。其实这是发布小游戏时的限制,皮肤中引用到的自定义组件都需要显示申明在window上,比如两个组件全类名chat.Test与friend.Test都需要申明在window上,window["Test"]=chat.Test;window["Test"]=friend.Test,会只保留后者)

这个脚本有这么几个作用:①扫描所有exml用到的命名空间 ②扫描所有代码用到的命名空间,并判断是否与exml有用到相同的。③src下的代码是否有同名的类(即上面所说的同类名,单不同包名)。
TODO标记的下方路径需要按需要换成你自己的,运行这个node.js脚本就能输出结果,按照需求,看是改代码的命名空间,还是改皮肤的命名空间,最好是修改皮肤的命名空间吧,直接全局搜索替换就行,代码的命名空间还设计到代码重构,同时皮肤里面引用到该组件的也需要替换,复杂度明显提升了。


三、自定义组件需要显示申明在window上
大型H5那么多皮肤,那么多自定义的组件,手动也是不可能的啦,下面也贡献一个脚本,放在项目根目录/scripts下即可。
addGlobalReference.ts
[JavaScript] 纯文本查看 复制代码
/**
 * @User: liuliqianxiao
 * @Date: 2018/1/30
 * @Time: 上午11:35
 * @Desc: 发布微信小游戏时将exml中引用到的自定义组件暴露到全局对象上去
 **/
import * as path from 'path';
import * as fs from "fs";

export class AddGlobalReferencePlugin implements plugins.Command {

    private referenceRegExp: RegExp = /\<(?!\s*\/)\s*([\w:$]*)/g;//组件类名匹配
    private nameSpaceRegExp: RegExp = /namespace\s+([a-zA-Z0-9$_]*)/g;//代码中类名匹配组
    private classRegExp: RegExp = /class\s+([a-zA-Z0-9$_]*)\s+extends\s+/g;//代码中命名空间匹配组
    private needAddGlobalReferenceClassList: Array<string> = [];
    private referenceMap: Object = {};//类名到全限定类名映射
    private usedNameSpace: boolean;//你自定义的组件是否用到了命名空间。
    private needAddToGlobalNameSpaces: Array<any> = [];//需要添加到window上的命名空间


    private egretPropertiesFilePath: string = path.resolve("./egretProperties.json");//白鹭工程配置文件路径
    private codeDirs = [
        "./src"
    ];//源代码目录

    constructor(usedNameSpace: boolean = true) {
        this.usedNameSpace = usedNameSpace;
    }

    async onFile(file: plugins.File) {
        if (file.extname == '.js') {
            const filename = file.origin;
            let content = file.contents.toString();
            if (filename == 'main.js') {
                content += this.generateGlobalReferenceCode();
            }
            file.contents = new Buffer(content);
        }
        return file;
    }

    async onFinish(commandContext: plugins.CommandContext) {

    }

    /**
     * 找出所有exml皮肤文件中用到的自定义的组件
     * 生成在window对象上的引用代码
     * @returns {string}
     */
    private generateGlobalReferenceCode(): string {
        //从工程属性文件中动态读取exml文件的存放路径
        let jsonStr = fs.readFileSync(this.egretPropertiesFilePath, "utf-8");
        let exmlDirs = JSON.parse(jsonStr).eui.exmlRoot;
        exmlDirs.forEach((shortPath) => {
            let fullDirPath = path.join(path.resolve("./"), shortPath);
            this.loopHandleExmls(fullDirPath);
        });

        let referenceCodeStr = ";";
        if (this.needAddGlobalReferenceClassList.length == 0)
            return referenceCodeStr;

        //如果是自己的组件代码用到了命名空间。要扫描源代码目录,找到类所处的命名空间
        if (this.usedNameSpace) {
            this.codeDirs.forEach((shortPath) => {
                let fullDirPath = path.join(path.resolve("./"), shortPath);
                this.loopHandleTsFiles(fullDirPath);
            });
        }

        this.needAddToGlobalNameSpaces.forEach((nameSpace) => {
            referenceCodeStr += "window[\"" + nameSpace + "\"]=" + nameSpace + ";"
        });

        this.needAddGlobalReferenceClassList.forEach((className) => {
            referenceCodeStr += "window[\"" + className + "\"]=" + this.referenceMap[className] + ";"
        });
        return referenceCodeStr;
    }

    private loopHandleTsFiles(dirPath: string): void {
        let fileList = fs.readdirSync(dirPath);
        fileList.forEach((fileName) => {
            let fullPath = path.join(dirPath, fileName);
            if (fs.statSync(fullPath).isDirectory()) {
                this.loopHandleTsFiles(fullPath);
            } else {
                this.handleOneTsFile(fullPath);
            }
        });
    }

    private handleOneTsFile(filePath: string): void {
        if (path.extname(filePath) !== ".ts") {
            return;
        }
        //不处理*.d.ts文件
        if (filePath.indexOf(".d.ts") !== -1) {
            return;
        }
        let codeContent = fs.readFileSync(filePath, "utf-8");

        /*
        这里有一些前提假设:
        第一:一个ts文件中的如果有一个或者多个类,那么所有类要么都不处于命名空间下,要么所有类都处于同一命名空间下
            比如这种就不行(c和d和不处于同一命名空间,C、D类有命名空间,E没有):
            namesapce A {
                class C extends eui.Component{

                }
            }

            namespace B{
                class D extends eui.Component{

                }
            }

            class E extends eui.Component{

            }

            下面这种也不行
        第二:不同的命名空间下不能有相同的类名
            如下这种就不行:
            namesapce A {
                class Test extends eui.Component{

                }
            }

            namespace B{
                class Test extends eui.Component{

                }
            }
        第三:其他注释中可能包含了能恰好被正则匹配到的内容。。。。。。。。。(无语无解)
        */

        let nameSpaceMatch = this.nameSpaceRegExp.exec(codeContent);
        let currentNameSpace: string = "";
        let nameSpaceMatchCount = 0;
        if (!nameSpaceMatch)
            return;

        while (nameSpaceMatch) {
            currentNameSpace = nameSpaceMatch[1];
            nameSpaceMatchCount++;
            if (nameSpaceMatchCount > 1) {
                console.log(`${filePath}含有多个命名空间,请检查!`);
                return;
            }
            nameSpaceMatch = this.nameSpaceRegExp.exec(codeContent);
        }

        //代码中没有定义命名空间
        if (nameSpaceMatchCount == 0) {
            console.log(`${filePath}不包含命名空间!`);
            return;
        }

        let classMatch = this.classRegExp.exec(codeContent);
        while (classMatch) {
            let className = classMatch[1];
            let fullClassName = currentNameSpace + "." + className;
            if (this.referenceMap[className]) {
                this.referenceMap[className] = fullClassName;
                if (this.needAddToGlobalNameSpaces.indexOf(currentNameSpace) == -1) {
                    this.needAddToGlobalNameSpaces.push(currentNameSpace);
                }
            }
            classMatch = this.classRegExp.exec(codeContent);
        }
    }

    //遍历所有exml文件,找出所有皮肤中引用到的自定义组件。这些基本上都是要暴露到全局中去的
    private loopHandleExmls(dirPath: string): void {
        let fileList = fs.readdirSync(dirPath);
        fileList.forEach((fileName) => {
            let fullPath = path.join(dirPath, fileName);
            if (fs.statSync(fullPath).isDirectory()) {
                this.loopHandleExmls(fullPath);
            } else {
                this.handleOneExml(fullPath);
            }
        });
    }

    private handleOneExml(filePath: string): void {
        if (path.extname(filePath) !== ".exml") {
            return;
        }
        let exmlContent = fs.readFileSync(filePath, "utf-8");

        let matchArray = this.referenceRegExp.exec(exmlContent);
        while (matchArray) {

            let fullClassName = matchArray[1];
            if (fullClassName.indexOf(":") !== -1) {
                let nameSpace = fullClassName.split(":")[0];
                let className = fullClassName.split(":")[1];
                if (nameSpace === "e" || nameSpace === "w" || className == "skinName") {
                    //匹配到这些,说明是exml内置的命名空间,不用处理
                } else {
                    if (this.needAddGlobalReferenceClassList.indexOf(className) == -1) {
                        this.needAddGlobalReferenceClassList.push(className);
                        this.referenceMap[className] = className;
                    }
                }
            } else {
                //皮肤里面的都是带命名空间的
            }
            matchArray = this.referenceRegExp.exec(exmlContent);
        }
    }

}


然后修改config.baidugame.ts或者config.wxgame.ts如下部分:
[JavaScript] 纯文本查看 复制代码
const config: ResourceManagerConfig = {

    buildConfig: (params) => {

        const { target, command, projectName, version } = params;
        const outputDir = `../${projectName}_baidugame`;
        if (command == 'build') {
            return {
                outputDir,
                commands: [
                    new CleanPlugin({ matchers: ["js", "resource"] }),
                    new CompilePlugin({ libraryType: "debug", defines: { DEBUG: true, RELEASE: false } }),
                    new ExmlPlugin('commonjs'), // 非 EUI 项目关闭此设置
                    new BaidugamePlugin(),
                    new ManifestPlugin({ output: 'manifest.js' })
                ]
            }
        }
        else if (command == 'publish') {
            return {
                outputDir,
                commands: [
                    new CleanPlugin({ matchers: ["js", "resource"] }),
                    new CompilePlugin({ libraryType: "release", defines: { DEBUG: false, RELEASE: true } }),
                    new ExmlPlugin('commonjs2'), // 非 EUI 项目关闭此设置
                    new BaidugamePlugin(),
                    // new UglifyPlugin([{
                    //     sources: ["main.js"],
                    //     target: "main.min.js"
                    // }
                    // ]),
                    new AddGlobalReferencePlugin(true),
                    new ManifestPlugin({ output: 'manifest.js' })
                ]
            }
        }
        else {
            throw `unknown command : ${params.command}`;
        }
    },

    mergeSelector: defaultConfig.mergeSelector,

    typeSelector: defaultConfig.typeSelector
}
注意看32行的代码,构建流程加上自己的插件即可,最上面也要记得import自己的构建插件类

注意啦:我为了保证发布时搜索exml在一个很小的范围内,保留了老版本的egretProperties.json中的exml根目录配置,新版本配置文件貌似没这个了,要记得在根对象上手动加上去如下代码哦,目录换成你自己的exml文件夹配置,
[AppleScript] 纯文本查看 复制代码
"eui": {
    "exmlRoot": [
      "resource/views",
      "resource/gameLoginViews"
    ]
  },


四、皮肤发布模式选择commonjs2
先从理论上来分析为啥要用commonjs2:
①由于小游戏不能eval动态执行代码,所以content以及嵌入exml以及外部动态加载exml文件这三种方式都是不可行的了。
②发布成gjs或者commonjs代码,这对于一个大型h5项目来说皮肤文件发布成硬代码会明显增加包内大小,加入你代码都有4M,保守估计你的皮肤发不成纯代码也会有3M左右,明显不可行。
③commonjs2是吧exml皮肤文件发布成json,然后用代码去解析json得到类,json可以动态加载,然后解析json又没违反小游戏不能eval的规则。也就是说相当于只多了一个解析json皮肤配置文件的几十kb的库而已,皮肤配置json文件完全可以分离式加载,完美解决了大型H5发布小游戏的皮肤问题。

修改发布配置文件config.baidugame.ts或者config.wxgame.ts,搜索commonjs,替换成commonjs2。
修改ThemAdapter.ts,增加对commonjs2的支持:
[JavaScript] 纯文本查看 复制代码
class ThemeAdapter implements eui.IThemeAdapter {

    /**
     * 解析主题
     * @param url 待解析的主题url
     * @param onSuccess 解析完成回调函数,示例:compFunc(e:egret.Event):void;
     * @param onError 解析失败回调函数,示例:errorFunc():void;
     * @param thisObject 回调的this引用
     */
    public getTheme(url: string, onSuccess: Function, onError: Function, thisObject: any): void {
        function onResGet(e: string): void {
            onSuccess.call(thisObject, e);
        }

        function onResError(e: RES.ResourceEvent): void {
            if (e.resItem.url == url) {
                RES.removeEventListener(RES.ResourceEvent.ITEM_LOAD_ERROR, onResError, null);
                onError.call(thisObject);
            }
        }

        if (typeof window["generateEUI"] !== 'undefined') {
            egret.callLater(() => {
                onSuccess.call(thisObject, window["generateEUI"]);
            }, this);
        } else if (typeof window["generateEUI2"] !== 'undefined') {
            egret.callLater(() => {
                onSuccess.call(thisObject, window["generateEUI2"]);
            }, this);
        } else {
            RES.addEventListener(RES.ResourceEvent.ITEM_LOAD_ERROR, onResError, null);
            RES.getResByUrl(url, onResGet, this, RES.ResourceItem.TYPE_TEXT);
        }
    }
}



这样就完了吗?并没有,且往下看。
执行一次egret publish --target wxgame后,在小游戏工程目录下可以看到多了一个gameEui.json文件,一看文件大小,也还是好几兆啊,放在小游戏包内还是不合适,上面说了json文件我们是阔以动态加载执行滴。所以我们把公用的皮肤json保留在gameEui.json中,其他各模块的皮肤josn配置我们要想方法抽出来按需动态加载解析,这才是这一条的难点。
下面贡献一个脚本,当然所有的都是使用与我自己的项目的,这里只是让你看一下解决问题的思路,如果要 搬动自己的项目中肯定是需要自己动手修修改改的
[JavaScript] 纯文本查看 复制代码
/**
 * @User: liuliqianxiao
 * @Date: 2018/1/25
 * @Time: 下午2:19
 * @Desc: 每个模块的皮肤配置json文件合并成一个
 **/
var fs = require("fs");
var path = require("path");


//TODO---这里写上你的小游戏工程发布目录下的resource/gameEui.json文件全路径
var commonJs2JsonFile = "xx/xx/xx";

//不知道为何会出现转换得到结果会有this的情况~~~
var commonJs2JsonObj = JSON.parse(fs.readFileSync(commonJs2JsonFile, "utf-8").replace(/this\./g, ""));
var commonJs2JsonObjCopy = JSON.parse(fs.readFileSync(commonJs2JsonFile, "utf-8").replace(/this\./g, ""));


//最终剩下的重新保存,这部分就是公用的要放在包内的皮肤json文件
fs.writeFileSync(commonJs2JsonFile, JSON.stringify(commonJs2JsonObjCopy, null, null), "utf-8");

//TODO 如下假设
//resource/views目录下有若干模块,比如chat聊天模块,friend模块,每个模块的皮肤都存在自己的模块的目录下,假设没有其他子目录了
//resource/commonViews目录下存放的是公用组件的皮肤,不隶属于任何模块。
var moduleViewDir = path.resolve("../../resource/views");
var moduleList = fs.readdirSync(moduleViewDir);
moduleList.forEach(moduleName => {
    var exmlList = fs.readdirSync(path.join(moduleViewDir, moduleName));
    var exmlConfigList = [];
    exmlList.forEach(exmlName => {
            var result = getExmlDefineJson(moduleName + "/" + exmlName);
            if (result && result.length > 0) {
                exmlConfigList.concat(result);
            }
        }
    );
    var jsonObj = {};
    exmlConfigList.forEach(tempList => {
        jsonObj[tempList[0]] = tempList[1];
    })
    fs.writeFileSync(path.join(moduleViewDir, moduleName, moduleName + "combine_exml_config.json"), JSON.stringify(jsonObj), "utf-8");
});


function getExmlDefineJson(relativePath) {
    var fullRelativePath = "resource/" + relativePath;
    let result = [];
    let skinClassName;
    for (var className in commonJs2JsonObj) {
        var item = commonJs2JsonObj[className];
        if (item.$path === fullRelativePath) {
            skinClassName = className;
            result.push([className, item]);

            if (commonJs2JsonObjCopy[className]) {
                commonJs2JsonObjCopy[className] = null;
                delete commonJs2JsonObjCopy[className];
            }

        }
    }
    if (!skinClassName)
        return result;

    //此皮肤关联的皮肤名字,这里是分析commonjs2的最终gameEui.json文件得到的结论。。目前并无问题
    let relativeSkinClassNamePrefix = skinClassName + "$";
    for (var className in commonJs2JsonObj) {
        if (className.indexOf(relativeSkinClassNamePrefix) !== -1) {
            result.push([className, commonJs2JsonObj[className]]);

            if (commonJs2JsonObjCopy[className]) {
                commonJs2JsonObjCopy[className] = null;
                delete commonJs2JsonObjCopy[className];
            }
        }
    }

    return result;
}

每个模块下的皮肤exml对应的json配置文件都抽取出来合并成了一个,打开对应模块时预先加载对应模块下的皮肤json配置文件,然后调用commonjs2的解析json布局文件的相关函数解析即可。伪代码如下
[AppleScript] 纯文本查看 复制代码
let exmlJson: Object = RES.getRes(resourceKeey);
                    if (exmlJson) {
                        window["JSONParseClass"]["setData"](exmlJson);
                    }


五、protobuf优化
如果你在项目中用到了protobuf,那么对于超大型h5,在H5中我们可以直接加载proto文件然后调用protobuf库动态解析proto文件得到类,但是在小游戏中不能动态执行代码,所以必须要把proto文件转成js代码文件,这里直接按照官方的pb-egret的提示一步步来就可以完美解决了。
说一下小优化:在执行了pb-egret的一系列命令后,项目更目录下的protobuf目录下面有个pbconfig.json文件,做如下修改
[AppleScript] 纯文本查看 复制代码
{
  "options": {
    "no-create": true,
    "no-verify": true,
    "no-convert": true,
    "no-delimited": true,
    "no-encode": false,
    "no-decode": false
  },
  "sourceRoot": "protofile",
  "outputFile": "bundles/protobuf-bundles.js"
}


代码中只使用encode和decode两个函数,这样生成的js代码量是最小的,省略掉其他辅助函数。
六、资源拆分包内和包外
这个官方自带的ResSplitPlugin可以用,自己写一套也很简单,无非是根据配置文件,移动,删除文件而已。
代码层面的修改,参考我一年多以前写的文章,还是原来的套路,一点都没变化。
https://bbs.egret.com/thread-46028-1-1.html

七、第三方库报错解决
这个还是大同小异,参考我以前写的第三方库报错解决合集:
https://bbs.egret.com/thread-46130-1-1.html
百度小游戏与微信小游戏不同之处,百度小游戏window=global,微信小游戏window!=global,所以greensock相关类定义的位置在微信小游戏上需要修改,而百度小游戏不需要,jszip有个独特的createElementNS报错,而百度小游戏没有这个报错:
微信小游戏要特殊处理jszip和greensock这两个库,如果你用到了的话:
[JavaScript] 纯文本查看 复制代码
if (filename == "libs/modules/greensock/greensock.js" || filename == "libs/modules/greensock/greensock.min.js") {
                    content += ";Object.assign(window,window.global);"
                }
                if(filename == "libs/modules/jszip/jszip.js" || filename == "libs/modules/jszip/jszip.min.js"){
                    content = content.replace(/createElementNS/g,"createElement");
                }

以前貌似没有createElementNs报错问题,换成createElement即可不报错。


八、小游戏分包加载
小游戏规定子包不超过4M,总包不超过8M,不限制子包个数。对于大型H5,必须要分包,否则单独一个main.min.js再加几个白鹭引擎自身相关的库,很容易就超出了4M的限制。
如下几种分包需要后续处理:
①main.min.js自己的游戏逻辑文件已经超过了4M。
②main.min.js没有超过4M的情况,但是分包之后main.min.js加同级包内其他资源和代码超了4M。
③游戏发布流程中已经采用了H5传统的分包策略,就是一个登录壳文件+一个游戏逻辑主文件

①和②情况说明需要在逻辑上继续分割 main.min.js文件,同时按照白鹭官方给的分包实例,引入一个loading,在loading中去处理分包的加载,那么如何分割main.min.js
这里有两种思路可以提供参考。

1. 编译拆分多个包:
具体实现就是调用多次egret publish,每次发布时修改tsconfig.json使之包含不同的代码源文件,修改egretProperties.json使之引用不同的库。这个需要自己写发布流程脚本实现自动化发布,每次发布得到的一个main.min.js就是一个分包。这个思路也就是③提到的H5常用的登录优化套路,即发布一个登录壳文件,发布一个游戏逻辑主文件。但是这个实现难度有点大,首先代码层面你得明确区分哪些可以独立发布,不然代码来回循环引用,很难发布成功,其次对于公用代码需要编写*.d.ts接口定义文件,后者调用tsc编译成成*.d.ts接口文件。

2.代码人为粗暴分割成多个包:
首先只调用一次egret publish,对得到的main.min.js文件进行人为的拆分成几部分,比如拆分main_part1.min.js,和main_part2.min.js,打开main.min.js,在文件1/2的位置处找一行如下代码__reflect(XXXClassName.prototype,\"XXXClassName\");//这里的XXXClassName是指你选的那一行里的__reflect函数的参数作为你以后分割代码的分割标记。这个分割标记前面的内容包括这个标记本身的内容作为第一部分main_part1.min.js,这个分号之后的内容作为main_part2.min.js的内容。
其次,我们打开main_part2.min.js文件发现逻辑是不完整的,没有main_part1.min.js开头的那部分对__reflect和extens函数的定义,加上去。

不管是你按照1做,还是2的思路去实现,你都得显示把某些类和命名空间申明到window上去,不然不同包调用会报错。
总之听起来复杂,但是我还是会热心的奉献上我自己的node脚本啦,这个脚本是按照思路2编写的一个自动处理脚本。你如果想用自己修修改改呗:
splitCode.js
[JavaScript] 纯文本查看 复制代码
/**
 * @User: liuliqianxiao
 * @Date: 2018/1/30
 * @Time: 上午11:35
 * @Desc: 发布微信小游戏时分包里面的每个类都和命名空间都需要暴露到window上,否则其他包无法调用
 **/
var path = require("path");
var fs = require("fs");

var referenceRegExp = /\<(?!\s*\/)\s*([\w:$]*)/g;//组件类名匹配
var nameSpaceRegExp = /namespace\s+([a-zA-Z0-9$_]*)/g;//代码中命名空间匹配组
var classRegExp = /(class|enum)\s+([a-zA-Z0-9$_]*)(?=(\s+extends)|(\s+implements)|(\s*{)|(<[^>]*>))/g;//代码中类名匹配组
var referenceMap = {};//类名到全限定类名映射
var needAddToGlobalNameSpaces = [];//需要添加到window上的命名空间
var needAddGlobalReferenceClassList = [];//需要添加都window上的类名
var componentReferenceClassList = [];//自定义组件的类名

//TODO --- 注意换成你自己的配置啦……
var splitConfig = {
    codeMinifyPath: path.resolve("../../../GameProject_wxgame/js/main.min.js"),
    firstPartCodeFileName: "game_main_part1.min.js",
    secondPartCodeFileName: "game_main_part2.min.js",
    splitSepStr: "__reflect(KingMediator.prototype,\"KingMediator\");"
};


generateClassReferenceCode();

/**
 * 找出所有exml皮肤文件中用到的自定义的组件
 * 生成在window对象上的引用代码
 * @returns {string}
 */
function generateClassReferenceCode() {


    let codeContent = fs.readFileSync(splitConfig.codeMinifyPath, "utf-8");

    let index = codeContent.indexOf(splitConfig.splitSepStr);
    if (index === -1) {
        console.warn(`在代码中未找到分割字符串${splitConfig.splitSepStr}`);
        return;
    }

    let codeContent1 = codeContent.substring(0, index + splitConfig.splitSepStr.length);
    let codeContent2 = codeContent.substr(index + splitConfig.splitSepStr.length);

    let testRegExp = /__reflect\s*\(\s*[^,]+,\s*"([^\"]*)"/g;

    //处理part1
    needAddGlobalReferenceClassList.length = 0;
    needAddToGlobalNameSpaces.length = 0;
    referenceMap = {};
    let mathArray = testRegExp.exec(codeContent1);
    let className;
    let nameSpaceName;
    while (mathArray) {
        let fullClassName = mathArray[1];
        if (fullClassName.indexOf(".") !== -1) {
            nameSpaceName = fullClassName.split(".")[0];
            className = fullClassName.split(".")[1];
            referenceMap[className] = fullClassName;
            if (needAddToGlobalNameSpaces.indexOf(nameSpaceName) === -1) {
                needAddToGlobalNameSpaces.push(nameSpaceName);
            }
            if (needAddGlobalReferenceClassList.indexOf(className) === -1) {
                needAddGlobalReferenceClassList.push(className);
            } else {
                console.log(`代码中存在两个相同的类名${className},即使在同样的命名空间下也必行!`);
            }
        } else {
            needAddGlobalReferenceClassList.push(fullClassName);
            referenceMap[fullClassName] = fullClassName;
        }
        mathArray = testRegExp.exec(codeContent1);
    }
    let referenceCodeStr = ";window.egret=egret;";
    needAddToGlobalNameSpaces.forEach((nameSpace) => {
        referenceCodeStr += "window[\"" + nameSpace + "\"]=" + nameSpace + ";"
    });
    needAddGlobalReferenceClassList.forEach((className) => {
        referenceCodeStr += "window[\"" + className + "\"]=" + referenceMap[className] + ";"
    });
    codeContent1 += referenceCodeStr;

    //处理part2
    let firstPartNameSpaces = needAddToGlobalNameSpaces.concat();
    let firstPartClassReference = needAddGlobalReferenceClassList.concat();
    needAddGlobalReferenceClassList.length = 0;
    needAddToGlobalNameSpaces.length = 0;
    referenceMap = {};
    mathArray = testRegExp.exec(codeContent2);
    while (mathArray) {
        let fullClassName = mathArray[1];
        if (fullClassName.indexOf(".") !== -1) {
            nameSpaceName = fullClassName.split(".")[0];
            className = fullClassName.split(".")[1];
            referenceMap[className] = fullClassName;
            if (needAddToGlobalNameSpaces.indexOf(nameSpaceName) === -1) {
                needAddToGlobalNameSpaces.push(nameSpaceName);
            }
            if (needAddGlobalReferenceClassList.indexOf(className) === -1) {
                needAddGlobalReferenceClassList.push(className);
            } else {
                console.log(`代码中存在两个相同的类名${className},即使在同样的命名空间下也必行!`);
            }

        } else {
            needAddGlobalReferenceClassList.push(fullClassName);
            referenceMap[fullClassName] = fullClassName;
        }
        mathArray = testRegExp.exec(codeContent2);
    }
    referenceCodeStr = ";window.egret=egret;";
    needAddToGlobalNameSpaces.forEach((nameSpace) => {
        referenceCodeStr += "window[\"" + nameSpace + "\"]=" + nameSpace + ";"
    });
    needAddGlobalReferenceClassList.forEach((className) => {
        referenceCodeStr += "window[\"" + className + "\"]=" + referenceMap[className] + ";"
    });

    //part2中必须引入part1中的所有命名空间以及类名定义,否则part2运行时一堆找不到
    let part1CodeDeclare = "";
    if (firstPartNameSpaces.length) {
        firstPartNameSpaces.forEach(ns => {
            part1CodeDeclare += "var " + ns + "=window[\"" + ns + "\"];";
        })
    }
    if (firstPartClassReference) {
        firstPartClassReference.forEach(classzz => {
            part1CodeDeclare += "var " + classzz + "=window[\"" + classzz + "\"];";
        })
    }


    codeContent2 = "var __reflect=this&&this.__reflect||function(t,e,i){t.__class__=e,i?i.push(e):i=[e],t.__types__=t.__types__?i.concat(t.__types__):i},__extends=this&&this.__extends||function(t,e){function i(){this.constructor=t}for(var n in e)e.hasOwnProperty(n)&&(t[n]=e[n]);i.prototype=e.prototype,t.prototype=new i};" + part1CodeDeclare + codeContent2 + referenceCodeStr;

    fs.writeFileSync(path.join(path.dirname(path.resolve(item.codeMinifyPath)), item.firstPartCodeFileName), codeContent1, "utf-8");
    fs.writeFileSync(path.join(path.dirname(path.resolve(item.codeMinifyPath)), item.secondPartCodeFileName), codeContent2, "utf-8");


}

也许有人质疑我这种做法的合理性,但是不得不说这是可行的,因为我基于这样一个事实,白鹭在编译合并生成main.min.js的时候是有进行过类的依赖排序的,也就是说被依赖的项的代码内容始终在前面的,debug模式编译,manifest.json中的js文件也是进行过排序的,这样就保证了引用顺序啦……这个你得去看白鹭的egret命令行发布用到的脚本才会知道这些……
OK,搞定了包大小问题,咋们去真正的套用小游戏的分包实现即可。
注意:①有些朋友对于微信小游戏分包加载中提到的按需加载子模块不是很明了。官方的分包加载写在了小游戏的工程下的game.js文件中,但其实我们也可以在代码里面写分包加载的代码,来看看我的吧,我这里是登录完成之后要加载游戏主文件,游戏主文件由于代码量大又拆分成了game_main_part1和game_main_part2两个分包,我这里是一套代码发布微信小游戏和百度小游戏,所以对此也做了处理:
[JavaScript] 纯文本查看 复制代码
//加载分包1
    private loadSmallGameSubpackages1(): void {

        let loadSubPackageFun: Function = window["wx"] ? window["wx"]["loadSubpackage"] : window["swan"]["loadSubpackage"];

        const loadTask: any = loadSubPackageFun({
            name: 'game_main_part1',
            success(res) {
                // 分包加载成功后通过 success 回调
                GameLoginMediator.instance._hasLoadedCount++;
                GameLoginMediator.instance.loadSmallGameSubpackages2();
            },
            fail(res) {
                // 分包加载失败通过 fail 回调
                console.log(`加载分包game_main_part1失败!!!`)
            }
        });

        loadTask.onProgressUpdate(res => {
            GameLoginMediator.instance.updateProgressInfo("game_main_part1",
                res.totalBytesWritten / res.totalBytesExpectedToWrite);
        })
    }

    //加载分包2
    private loadSmallGameSubpackages2(): void {

        let loadSubPackageFun: Function = window["wx"] ? window["wx"]["loadSubpackage"] : window["swan"]["loadSubpackage"];

        const loadTask: any = loadSubPackageFun({
            name: 'game_main_part2',
            success(res) {
                //分包2加载成功之后,执行GameMain相关逻辑
                GameLoginMediator.instance._hasLoadedCount++;
                window["GameMain"].instance.init();
            },
            fail(res) {
                // 分包加载失败通过 fail 回调
                console.log(`加载分包game_main_part2失败!!!`)
            }
        });

        loadTask.onProgressUpdate(res => {
            GameLoginMediator.instance.updateProgressInfo("game_main_part2",
                res.totalBytesWritten / res.totalBytesExpectedToWrite);
        })
    }

②分包的时候百度小游戏和微信校友还略有不同,如果配置分包是按照目录去配置,百度小游戏要求子包目录下面的入口文件必须叫index.js,而微信小游戏子包的入口文件必须交game.js,否则加载不到分包。这点白鹭官方完全是丝毫没有提及,直接指向了微信小游戏。

大提示:
所有脚本都是自己原创,暂时只适用于我自己的项目,如果你想套用肯定是套不上的,必须要看懂脚本在干啥,然后改成你自己的。
如有问题或者更好的思路,欢迎评论指出。我也是一年多没有搞微信小游戏了,微信小游戏刚出的时候发了几篇帖子,得到了很不错的反响,然而我并没有真正发布过一款小游戏。现在是真的需要转小游戏,所以重新研究了一下,并把这之间的细微之处写出来分享给大家。


分享到 :
8 人收藏

13 个回复

倒序浏览
Fstupid  登堂入室 | 2019-5-21 17:13:43
占个沙发,满满的踩坑经验,等你接过oppo小游戏后你会发现,百度和微信是多么多么的友好。
都斌大苏打  登堂入室 | 2019-5-21 17:37:35
学习学习
落幕夜未央  圆转纯熟 | 2019-5-21 19:38:21
Fstupid 发表于 2019-5-21 17:13
占个沙发,满满的踩坑经验,等你接过oppo小游戏后你会发现,百度和微信是多么多么的友好。 ...

只要给时间,别催我,我愿意踩。。就怕给一个很短的事件让我去接oppo,如果各种问题的话就很烦
落幕夜未央  圆转纯熟 | 2019-5-21 19:39:23
Fstupid 发表于 2019-5-21 17:13
占个沙发,满满的踩坑经验,等你接过oppo小游戏后你会发现,百度和微信是多么多么的友好。 ...

到时候接oppo有问题可以请教下你,我就不用再踩坑了,哈哈哈哈
3315213587  初学乍练 | 2019-5-21 19:54:53
接oppo小游戏的时候貌似也没啥坑啊 首屏8M 根本用不到分包 ARPG项目
当时egret官方都还没出oppo打包rpk方式呢
oppo官方已经给了很详细的文档 无非要装些npm依赖 没有vconsole 要用adb 其他没啥
ISam  登堂入室 | 2019-5-21 20:21:50
感谢分享,大赞
冰湖  官方团队 | 2019-5-22 09:32:00
赞一个。!
熊猫少女  官方团队 | 2019-5-22 09:47:13
顶!!
a22aaass  登堂入室 | 2019-5-24 10:23:29
前排占位会火的
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|京网文[2014]0791-191号|京ICP证150115号|Egret社区 ( 京ICP备14025619号

Powered by Discuz! X3.2 © 2001-2019 Comsenz Inc.

返回顶部