传送门
一、CRC - 循环冗余校验码(Cyclic Redundancy Check)
因为本人才疏学浅,这里就不做原理解释了,详细原理请自行百度~
二、Egret中生成CRC后缀
egret在 config.ts 文件中提供了相应的接口,在下方代码中可以看到 else if (command == ‘publish’) 这个判断,就是对 publish 命令的开放扩展,我们就是通过RenamePlugin(修改文件名插件) 这里对项目的资源进行添加CRC后缀。
这些是构造中参数的详细说明
其中 matchers 这个就是匹配机制,我们可以通过这个自定义命名
- 满足 from 的文件输出为 to 格式的文件
- from 采用 glob 表达式 , to 包含 [path][name][hash][ext] 四个变量 (我们可以在四个变量中间添加自己需要的分隔符,比如 _ - 等)
需要注意的是 hash egret 目前只支持 crc32
type RenamePluginOptions = {
/**
* 是否输出日志
* Whether to output the log
*/
verbose?: boolean
/**
* 采用何种 hash 算法,目前暂时只支持 crc32
* What hash algorithm is used, currently only crc32 is supported
*/
hash?: "crc32"
/**
* 设置匹配规则,将指定文件进行改名
* 该参数是个数组,允许设置多个匹配规则
* Set up matching rules to copy specified files to other folders
* This parameter is an array that allows multiple matching rules to be set
*/
matchers: Matcher[]
/**
* 回调函数,返回值里包括文件的一些信息
* The callback function, return value includes some information about the file
*/
callback?: Function
}
经过上面的修改后,就可以在Egret的控制台输入 egret publish进行打包尝试了,如果 verbose 设置的为true 的话就可以在控制台看到重命名的输出了
三、生成version文件
经过上面的操作后打出来的资源就有了我们想要的后缀,但是我们还需要一份 version 文件来进行资源对比
1. 添加自定义插件 可以在 config.ts 同级下创建一个自己的插件类,在config.ts
中导入就可以以上面的方式添加到**commands**中执行了
ResPlugin要实现plugins.Command接口,这里是接口的声明
/**
* 构建管线命令
*/
interface Command {
/**
* 项目中的每个文件都会执行此函数,返回 file 表示保留此文件,返回 null 表示将此文件从构建管线中删除,即不会发布
*/
onFile?(file: File): Promise<File | null>
/**
* 项目中所有文件均执行完后,最终会执行此函数。
* 这个函数主要被用于创建新文件
*/
onFinish?(pluginContext?: CommandContext): Promise<void>
[options: string]: any;
}
2. 实现ResPlugin插件
通过 onFile 方法 可以实现对文件进行过滤,不需要的文件返回 null 就可以了
下面以名为 ResPlugin 插件为例
export class ResPlugin implements plugins.Command {
// 文件信息
private versionConfig = {};
// 用于判断资源是否在这个目录下
private asstesVersionPath = "resource/asset/";
// 输出文件路径
private versionConfigPath = "resource/version";
// 输出文件扩展名
private versionConfigExtname = ".json";
// 截取的文本版本号
private versionNo: string;
constructor() {
}
async onFile(file: plugins.File) {
// 文件扩展名
const extname = file.extname;
// 文件初始名
const path = file.origin;
const _index = file.path.lastIndexOf("\\");
// 真实文件名
const _path = file.path.substring(_index + 1);
if (path.indexOf(this.asstesVersionPath) != -1 && (extname === ".mp3" || extname === ".fnt" || extname === ".json" || extname === ".png"
|| extname === ".jpg" || extname === ".xml" || extname === ".dbbin" || extname === ".txt" || extname === ".dat" || extname === ".bin")
|| extname === ".json") {
this.versionConfig[path] = _path;
} else {
// 截取游戏版本号
if (path.indexOf('jslauncher.js') != -1 && !this.versionNo) {
let jslauncher = file.contents.toString();
let arr = jslauncher.match('(?<=versionNo.*=.*").*?(?=\")');
this.versionNo = arr ? arr[0] : '';
}
}
return file;
}
async onFinish(commandContext: plugins.CommandContext) {
let fileName = this.versionConfigPath + this.versionNo + this.versionConfigExtname;
commandContext.createFile(fileName, new Buffer(JSON.stringify(this.versionConfig)));
}
}
3. 调用ResPlugin插件
回到 config.ts 中,通过 import { ResPlugin } from ‘./ResPlugin’; 把 ResPlugin 导入,然后在 commands 中添加就可以了
需要注意的是from后面是相对路径,不需要写文件后缀
四、CRC添加完成
经过上面一顿忙活后就可以打包出来看效果了,用命令打包和Egret Launcher打包都是可以的
五、version文件的使用
1. 自定义版本控制器
实现 RES.IVersionController 接口,我们需要通过这两个接口去实现老文件的删除
该接口有两个方法
/**
* 获取版本信息数据。<br/>
* 这个方法的调用需要在应用程序进行任何资源加载之前,建议在应用程序的入口类(Main)的开始最先进行调用处理。此方法只负责获取版本信息,不负责资源的下载。
* @version Egret 2.4
* @platform Web,Native
* @language zh_CN
*/
init(): Promise<any>;
/**
* 获取资源文件实际的URL地址。<br/>
* 由于版本控制实际已经对原来的资源文件的URL进行了改变,因此想获取指定资源文件实际的URL时需要调用此方法。<br/>
* 在开发调试阶段,这个方法会直接返回传入的参数值。
* @param url 游戏中使用的url
* @returns 实际加载的url
* @version Egret 2.4
* @platform Web,Native
* @language zh_CN
*/
getVirtualUrl(url: string): string;
思路:在 init 中获取到网络上最新的version.json文件,并读取本地的version.json文件,两个文件进行对比即可获得需要删除的老文件,然后删除他们,并把最新的version.json文件写入本地。
a. 获取最新version.josn文件
// 在游戏开始加载资源的时候就会调用这个函数
init(): Promise<any> {
if (RELEASE) {//发布模式
return new Promise((resolve, reject) => {
// 这里通过httpReques获得配置资源信息
let request = new egret.HttpRequest();
request.responseType = egret.HttpResponseType.TEXT;
request.open(this.versionConfigPath + "?v=" + Math.random(), egret.HttpMethod.GET);
request.send();
request.addEventListener(egret.Event.COMPLETE, (event: egret.Event) => {
var request = <egret.HttpRequest>event.currentTarget;
try {
this.versionConfig = JSON.parse(request.response);
// 处理文件资源的缓存
// 下面主要是对各种运行时的版本控制,
if (egret.Capabilities.runtimeType == egret.RuntimeType.WEB) {
this.versionCacheWeb();//浏览器 H5
} else {
this.versionCacheWxgame();
}
}
catch (e) {
Main.onRemoveSubPackLoading();
console.error("versionConfig error:" + e);
}
resolve();
}, this);
request.addEventListener(egret.IOErrorEvent.IO_ERROR, () => {
//egret.log("post error : " + event);
Main.onRemoveSubPackLoading();
resolve();
}, this);
})
} else {
return Promise.resolve()
}
}
b. 获取需要删除的资源路径:
根据本地缓存获取上次的资源版本,判断是否有版本变动,有的话就直接全部删除,然后写入新的version.json文件,没有变动就获取需要删除的资源路径
/**
* 处理wxgame里面的无效资源
*/
private versionCacheWxgame = () => {
// 简单处理,通过localStorage获取当前资源版本号resVersion,如果版本号不同,进行删除过期资源操作
// 这里开发者可以自己控制版本号,不一定使用localStorage
try {
const localVersion = egret.localStorage.getItem("resVersion");
console.log("resVersion -->> " + localVersion)
// 资源版本变动
if ((localVersion != this.currResourceVersion)) {
egret.localStorage.setItem("resVersion", this.currResourceVersion);
ClearLastLocalCache();
mkdir(this.currResourceVersion);
writeFile(this.currResourceVersion + "/version" + this.currentVersion + ".json", JSON.stringify(this.versionConfig));
} else {
// 这里检查要删除的旧资源
let oldRes = this.getRemoveList();
if (oldRes && oldRes.length > 0) {
// 删除旧资源
for (let i = 0; i < oldRes.length; i++) {
deleteLocalFile(oldRes[i]);
}
}
}
} catch (e) {
}
Main.onRemoveSubPackLoading();
}
/**
* 获得上版本的过期资源列表,提高效率使用字符串的形式进行比对
*/
private getRemoveList(): Array<string> {
try {
let rootPath = GetUserDataPath();
console.log('---------rootPath------ ' + rootPath)
let versionPath = this.currResourceVersion + "/version" + this.currentVersion + ".json";
let result = [];
if (fileExist(versionPath)) {
// 读取本地version文件
let content = readFileSync(versionPath, 'utf-8');
if (!content) {
// 这里替换原来的version文件
deleteLocalFile(versionPath);
writeFile(this.currResourceVersion + "/version" + this.currentVersion + ".json", JSON.stringify(this.versionConfig));
return result;
}
let oldVersion = JSON.parse(content);
let temp = '';
for (const key in oldVersion) {
if (!this.versionConfig[key] || (this.versionConfig[key] != oldVersion[key])) {
console.log('key ---- > ' + key) // resource/asset/sound/challenge/match.mp3
console.log('value -- > ' + oldVersion[key]) // value -- > match_36ef1cca.mp3
// 把文件真实名拼接出来,
temp = this.currResourceVersion + key.substring(key.indexOf('/'), key.lastIndexOf('/')) + '/' + oldVersion[key];
console.log('拼接后的路径 -->> ' + temp) // 9.9.9.9/asset/sound/challenge/match_36ef1cca.mp3
result.push(temp);
}
}
// 这里替换原来的version文件
deleteLocalFile(versionPath);
writeFile(this.currResourceVersion + "/version" + this.currentVersion + ".json", JSON.stringify(this.versionConfig));
} else {
// 没有老的version文件就直接生成
mkdir(this.currResourceVersion);
writeFile(this.currResourceVersion + "/version" + this.currentVersion + ".json", JSON.stringify(this.versionConfig));
}
return result;
} catch (e) {
console.error('getRemoveList -- error')
return null;
}
}
c. 实现 getVirtualUrl 接口,用于获取资源文件实际的URL地址
//在游戏运行时,每个资源加载url都要经过这个函数
getVirtualUrl(url: string) {
return this.getResUrlByVersion(url);
}
/**
* 获得版本控制之后的路径信息
*/
getResUrlByVersion(url: string): string {
//判断是否为版本控制的资源,其他域的资源,比如玩家头像,或是初始包体里面的资源以原始url加载
if (url.indexOf(this.resourceRoot) == -1) {
return url;
}
console.log('version ---> ' + url) // https://uqxsstest.pook.com/update/9.9.9.9/resource/asset/view/img/common/common_ui/common_ui.json
const _url = url;
//将文件的resourceRoot路径抹去,进而判读文件是否经过版本管理
url = url.replace(this.resourceRoot, "");
if (this.versionConfig) {
// 部分文件可能存在?v=加数字进行控制的形式,在引擎底层这里是不支持加v=的,只会以原始url加载路径,
// 如果项目中存在类似的情况,将其还原成普通形式
const index = url.indexOf("?v=");
if (index != -1) {
url = url.slice(0, index);
}
//取版本控制的的version
let versionKey = this.versionPath + url;
let _key = url.substring(url.lastIndexOf("/") + 1);
//console.log("versionKey:" + versionKey);
if (this.versionConfig[versionKey]) {
const version = this.versionConfig[versionKey];
const ext = url.slice(url.lastIndexOf("."));
// 原始的文件夹+crc32码+后缀扩展名
url = url.replace(_key, version);
} else {
return _url;
}
}
url = this.resourceRoot + url;
console.log('version end --> ' + url) // https://uqxsstest.pook.com/update/9.9.9.9/resource/asset/view/img/common/common_ui/common_ui_948ba063.json
return url;
}
完整代码:
declare var wx: { getFileSystemManager, env }
class VersionController implements RES.IVersionController {
// 版本控制信息
private versionConfig;
// resource根路径
private resourceRoot = getImgUrl() + "resource/";
// 版本控制的文件夹
private versionPath = "resource/";
// 版本控制信息的所在路径,相对于resource文件夹
private versionConfigPath = getImgUrl() + "/resource/version" + getVersionNo() + ".json";
// wxgame的当前版本号
private currentVersion = getVersionNo();
// 当前资源版本号
private currResourceVersion = getResourceVersion();
public constructor() {
}
// 在游戏开始加载资源的时候就会调用这个函数
init(): Promise<any> {
if (RELEASE) {//发布模式
return new Promise((resolve, reject) => {
//通过httpReques获得配置资源信息
let request = new egret.HttpRequest();
request.responseType = egret.HttpResponseType.TEXT;
request.open(this.versionConfigPath + "?v=" + Math.random(), egret.HttpMethod.GET);
request.send();
request.addEventListener(egret.Event.COMPLETE, (event: egret.Event) => {
var request = <egret.HttpRequest>event.currentTarget;
try {
this.versionConfig = JSON.parse(request.response);
//处理文件资源的缓存
// 下面主要是对各种运行时的版本控制,
if (egret.Capabilities.runtimeType == egret.RuntimeType.WEB) {
this.versionCacheWeb();//浏览器 H5
} else {
this.versionCacheWxgame();
}
}
catch (e) {
Main.onRemoveSubPackLoading();
console.error("versionConfig error:" + e);
}
resolve();
}, this);
request.addEventListener(egret.IOErrorEvent.IO_ERROR, () => {
Main.onRemoveSubPackLoading();
resolve();
}, this);
})
} else {
return Promise.resolve()
}
}
/**
* 处理wxgame里面的无效资源
*/
private versionCacheWxgame = () => {
// 简单处理,通过localStorage获取当前资源版本号resVersion,如果版本号不同,进行删除过期资源操作
// 这里开发者可以自己控制版本号,不一定使用localStorage
try {
const localVersion = egret.localStorage.getItem("resVersion");
// 资源版本变动
if ((localVersion != this.currResourceVersion)) {
egret.localStorage.setItem("resVersion", this.currResourceVersion);
ClearLastLocalCache();
mkdir(this.currResourceVersion);
writeFile(this.currResourceVersion + "/version" + this.currentVersion + ".json", JSON.stringify(this.versionConfig));
} else {
// 这里检查要删除的旧资源
let oldRes = this.getRemoveList();
if (oldRes && oldRes.length > 0) {
// 删除旧资源
for (let i = 0; i < oldRes.length; i++) {
deleteLocalFile(oldRes[i]);
}
}
}
} catch (e) {
}
Main.onRemoveSubPackLoading();
}
/**
* 处理WebH5里面的更新,如果开发者有其他平台发布的版本控制,请参照这两种进行修改
*/
private versionCacheWeb = () => {
// 简单处理,通过localStorage获取当前资源版本号resVersion,如果版本号不同,进行删除过期资源操作
// 这里开发者可以自己控制版本号,不一定使用localStorage
const localVersion = egret.localStorage.getItem("resVersion");
if (localVersion != this.currResourceVersion) {
egret.localStorage.setItem("resVersion", this.currResourceVersion);
console.log("Web版本更新");
}
}
/**
* 获得上版本的过期资源列表,提高效率使用字符串的形式进行比对
*/
private getRemoveList(): Array<string> {
try {
let rootPath = GetUserDataPath();
let versionPath = this.currResourceVersion + "/version" + this.currentVersion + ".json";
let result = [];
if (fileExist(versionPath)) {
let content = readFileSync(versionPath, 'utf-8');
if (!content) {
// 这里替换原来的version文件
deleteLocalFile(versionPath);
writeFile(this.currResourceVersion + "/version" + this.currentVersion + ".json", JSON.stringify(this.versionConfig));
return result;
}
let oldVersion = JSON.parse(content);
let temp = '';
for (const key in oldVersion) {
if (!this.versionConfig[key] || (this.versionConfig[key] != oldVersion[key])) {
temp = this.currResourceVersion + key.substring(key.indexOf('/'), key.lastIndexOf('/')) + '/' + oldVersion[key];
result.push(temp);
}
}
// 这里替换原来的version文件
deleteLocalFile(versionPath);
writeFile(this.currResourceVersion + "/version" + this.currentVersion + ".json", JSON.stringify(this.versionConfig));
} else {
// 没有老的version文件就直接生成
mkdir(this.currResourceVersion);
writeFile(this.currResourceVersion + "/version" + this.currentVersion + ".json", JSON.stringify(this.versionConfig));
}
return result;
} catch (e) {
console.error('getRemoveList -- error')
return null;
}
}
//在游戏运行时,每个资源加载url都要经过这个函数
getVirtualUrl(url: string) {
return this.getResUrlByVersion(url);
}
/**
* 获得版本控制之后的路径信息
*/
getResUrlByVersion(url: string): string {
//判断是否为版本控制的资源,其他域的资源,比如玩家头像,或是初始包体里面的资源以原始url加载
if (url.indexOf(this.resourceRoot) == -1) {
return url;
}
const _url = url;
//将文件的resourceRoot路径抹去,进而判读文件是否经过版本管理
url = url.replace(this.resourceRoot, "");
if (this.versionConfig) {
// 部分文件可能存在?v=加数字进行控制的形式,在引擎底层这里是不支持加v=的,只会以原始url加载路径,
// 如果项目中存在类似的情况,将其还原成普通形式
const index = url.indexOf("?v=");
if (index != -1) {
url = url.slice(0, index);
}
//取版本控制的的version
let versionKey = this.versionPath + url;
let _key = url.substring(url.lastIndexOf("/") + 1);
if (this.versionConfig[versionKey]) {
const version = this.versionConfig[versionKey];
const ext = url.slice(url.lastIndexOf("."));
// 原始的文件夹+crc32码+后缀扩展名
url = url.replace(_key, version);
} else {
return _url;
}
}
url = this.resourceRoot + url;
return url;
}
}
2. 注册版本控制器
根据他们的建议,我们在 Main.ts 的构造函数中注册自己实现的管理器 **RES.registerVersionController(new VersionController());**就可以了