├── example
├── project.json
├── assets
│ ├── script.meta
│ └── script
│ │ ├── AssetsDownload.js.meta
│ │ ├── AssetsManager.js.meta
│ │ ├── GameConfig.js.meta
│ │ ├── HotUpdateManager.js.meta
│ │ ├── GameConfig.js
│ │ ├── HotUpdateManager.js
│ │ ├── AssetsDownload.js
│ │ └── AssetsManager.js
├── settings
│ └── project.json
├── packages
│ └── hot_update
│ │ ├── main.js
│ │ ├── package.json
│ │ ├── comp-inspectors
│ │ └── custom-comp.js
│ │ └── panel
│ │ ├── copy.js
│ │ └── index.js
├── jsconfig.json
└── .gitignore
└── README.md
/example/project.json:
--------------------------------------------------------------------------------
1 | {
2 | "engine": "cocos-creator-js",
3 | "packages": "packages"
4 | }
--------------------------------------------------------------------------------
/example/assets/script.meta:
--------------------------------------------------------------------------------
1 | {
2 | "ver": "1.0.1",
3 | "uuid": "ec32abaf-7a1b-465a-89f0-dc0286d623a0",
4 | "isGroup": false,
5 | "subMetas": {}
6 | }
--------------------------------------------------------------------------------
/example/assets/script/AssetsDownload.js.meta:
--------------------------------------------------------------------------------
1 | {
2 | "ver": "1.0.2",
3 | "uuid": "492f2922-4c28-4f79-a4c0-a77b381ba7d3",
4 | "isPlugin": false,
5 | "subMetas": {}
6 | }
--------------------------------------------------------------------------------
/example/assets/script/AssetsManager.js.meta:
--------------------------------------------------------------------------------
1 | {
2 | "ver": "1.0.2",
3 | "uuid": "37490f16-4cc3-4009-8a4f-9e2e0d95f105",
4 | "isPlugin": false,
5 | "subMetas": {}
6 | }
--------------------------------------------------------------------------------
/example/assets/script/GameConfig.js.meta:
--------------------------------------------------------------------------------
1 | {
2 | "ver": "1.0.2",
3 | "uuid": "8cb1a2b4-669f-4b9e-ac9d-072516d827e8",
4 | "isPlugin": false,
5 | "subMetas": {}
6 | }
--------------------------------------------------------------------------------
/example/assets/script/HotUpdateManager.js.meta:
--------------------------------------------------------------------------------
1 | {
2 | "ver": "1.0.2",
3 | "uuid": "7ffddbcc-f926-45a4-bd4a-99e1130c95af",
4 | "isPlugin": false,
5 | "subMetas": {}
6 | }
--------------------------------------------------------------------------------
/example/settings/project.json:
--------------------------------------------------------------------------------
1 | {
2 | "start-scene": "current",
3 | "group-list": [
4 | "default"
5 | ],
6 | "collision-matrix": [
7 | [
8 | true
9 | ]
10 | ],
11 | "excluded-modules": []
12 | }
--------------------------------------------------------------------------------
/example/packages/hot_update/main.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = {
4 | load () {
5 | },
6 |
7 | unload () {
8 | },
9 |
10 | messages: {
11 | open() {
12 | Editor.Panel.open('hot_update');
13 | },
14 | },
15 | };
16 |
--------------------------------------------------------------------------------
/example/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es6",
4 | "module": "commonjs"
5 | },
6 | "exclude": [
7 | "node_modules",
8 | "library",
9 | "local",
10 | "settings",
11 | "temp"
12 | ]
13 | }
--------------------------------------------------------------------------------
/example/packages/hot_update/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "hot_update",
3 | "main": "main.js",
4 | "main-menu": {
5 | "自定义工具/热更配置生成": {
6 | "message": "hot_update:open"
7 | }
8 | },
9 | "panel": {
10 | "main" : "panel/index.js",
11 | "type" : "dockable",
12 | "title" : "热更配置生成",
13 | "width" : 400,
14 | "height": 300
15 | }
16 | }
--------------------------------------------------------------------------------
/example/packages/hot_update/comp-inspectors/custom-comp.js:
--------------------------------------------------------------------------------
1 | Vue.component('custom-comp-inspector', {
2 | template: `
3 |
4 |
5 |
6 |
7 | `,
8 |
9 | props: {
10 | target: {
11 | twoWay: true,
12 | type: Object,
13 | },
14 | },
15 | });
16 |
--------------------------------------------------------------------------------
/example/assets/script/GameConfig.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 全局环境设置
3 | * Author : donggang
4 | * Create Time : 2016.7.26
5 | */
6 | /** 外网测试服 */
7 | var debug_extranet = {
8 | gateSocketIp : "121.41.4.17", // 网关地址
9 | gateSocketPort : 3101, // 网关端口
10 |
11 | useSSL : false, // 是否使用https
12 | textUpdate : true, // 是否开启测试热更新
13 | };
14 |
15 | window.game = window.game || {};
16 | game.config = module.exports = debug_extranet;
17 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Cocos Creator 脚本热更新
2 | 通过脚本实现了官方 C++ 版热更功能,提高的稳定性和易维护性,使用者可在脚本上扩展自定义的版本更新业务。
3 |
4 | # 实现功能清单
5 | 1. 版本配置文件对比,提示有新版本
6 | 2. 版本清单文件对比,分析出添加、更新、删除的资源进行下载或删除
7 | 3. 更新失败或异常中断时,记录更新状态,下次触发更新时恢复更新进度,不在下载已更新的文件
8 | 4. 支持多模块热更新管理
9 | 5. 基于CocosCrator编辑器的版本配置文件生成工具插件
10 |
11 | # 使用方式
12 | 1. 打开 Cocos Creator 的 XXGAME 项目
13 | 2. 菜单选择“项目 > 构建发布”
14 | 3. 点击构建按钮
15 | 4. 菜单选择“自定义工具 > 热更配置生成”
16 |
17 | # 其它说明
18 | 1. 热更脚本代码在assets/script文件下AssetsManager.js、AssetsDownload.js两个文件,代码上注释比较全
19 | 2. 热更工具代码在packages/hot_update文件下
20 |
21 | # 联系方式
22 | gdong@bainainfo.com
--------------------------------------------------------------------------------
/example/.gitignore:
--------------------------------------------------------------------------------
1 | #/////////////////////////////////////////////////////////////////////////////
2 | # Fireball Projects
3 | #/////////////////////////////////////////////////////////////////////////////
4 |
5 | library/
6 | temp/
7 | local/
8 | build/
9 |
10 | #/////////////////////////////////////////////////////////////////////////////
11 | # Logs and databases
12 | #/////////////////////////////////////////////////////////////////////////////
13 |
14 | *.log
15 | *.sql
16 | *.sqlite
17 |
18 | #/////////////////////////////////////////////////////////////////////////////
19 | # files for debugger
20 | #/////////////////////////////////////////////////////////////////////////////
21 |
22 | *.sln
23 | *.csproj
24 | *.pidb
25 | *.unityproj
26 | *.suo
27 |
28 | #/////////////////////////////////////////////////////////////////////////////
29 | # OS generated files
30 | #/////////////////////////////////////////////////////////////////////////////
31 |
32 | .DS_Store
33 | ehthumbs.db
34 | Thumbs.db
35 |
36 | #/////////////////////////////////////////////////////////////////////////////
37 | # exvim files
38 | #/////////////////////////////////////////////////////////////////////////////
39 |
40 | *UnityVS.meta
41 | *.err
42 | *.err.meta
43 | *.exvim
44 | *.exvim.meta
45 | *.vimentry
46 | *.vimentry.meta
47 | *.vimproject
48 | *.vimproject.meta
49 | .vimfiles.*/
50 | .exvim.*/
51 | quick_gen_project_*_autogen.bat
52 | quick_gen_project_*_autogen.bat.meta
53 | quick_gen_project_*_autogen.sh
54 | quick_gen_project_*_autogen.sh.meta
55 | .exvim.app
56 |
57 | #/////////////////////////////////////////////////////////////////////////////
58 | # webstorm files
59 | #/////////////////////////////////////////////////////////////////////////////
60 |
61 | .idea/
62 |
--------------------------------------------------------------------------------
/example/assets/script/HotUpdateManager.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 热更新管理
3 | * Author : donggang
4 | * Create Time : 2016.7.29
5 | *
6 | * 需求说明:
7 | * 1、可后台更新版本资源
8 | */
9 |
10 | var AssetsManager = require("AssetsManager");
11 |
12 | var amManager = cc.Class({
13 | ctor : function(){
14 | this._updates = {}; // 更新模块集合
15 | this._queue = []; // 更新对列
16 | this._isUpdating = false; // 是否正在更新中
17 | this._current = null; // 当前正在更新的模块
18 | this._noComplete = {}; // 上次未完成的热更项
19 | },
20 |
21 | /**
22 | * 获取模块版本信息
23 | * @param localVersionCb(function) 本地版本信息加载完成
24 | * @param remoteVersionCb(function) 远程版本信息加载完成
25 | *
26 | */
27 | getModules : function(remoteVersionCb){
28 | if (!cc.sys.isNative) {
29 | if (remoteVersionCb) remoteVersionCb();
30 | return;
31 | }
32 |
33 | game.asset.check(function(modules, versions){
34 | this.modules = modules;
35 | this.versions = versions;
36 | if (remoteVersionCb) remoteVersionCb();
37 | }.bind(this));
38 | },
39 |
40 | /** 载入没更新完的模块状态 */
41 | load : function(){
42 | if (!cc.sys.isNative) return;
43 |
44 | var data = cc.sys.localStorage.getItem("update_no_complete");
45 | if (data){
46 | var json = JSON.parse(data);
47 | for(var i = 0; i < json.length; i++){
48 | this._noComplete[json[i]] = json[i];
49 | }
50 | cc.sys.localStorage.removeItem("update_no_complete");
51 | }
52 | },
53 |
54 | getProgress : function(name){
55 | var am = this._updates[name];
56 | return am.getProgress();
57 | },
58 |
59 | /**
60 | * 初始化更新模块名
61 | * @param name(string) 模块名
62 | * @param onCheckComplete(function) 检查版本完成
63 | * @param onComplete(function) 模块名
64 | * @param onProgress(function) 更新完成
65 | * @param onNewVersion(function) 已是最新版本
66 | */
67 | init : function(name, onCheckComplete, onComplete, onProgress, onNewVersion) {
68 | game.AssetConfig.concurrent = 2;
69 |
70 | var am = new AssetsManager();
71 | am.name = name;
72 | am.on(game.AssetEvent.NEW_VERSION , onNewVersion);
73 | am.on(game.AssetEvent.PROGRESS , onProgress);
74 | am.on(game.AssetEvent.FAILD , this._onFailed.bind(this));
75 | am.on(game.AssetEvent.NEW_VERSION_FOUND , this._onCheckComplete.bind(this));
76 | am.on(game.AssetEvent.SUCCESS , this._onUpdateComplete.bind(this));
77 | am.on(game.AssetEvent.REMOTE_VERSION_MANIFEST_LOAD_FAILD, this._onNetError.bind(this));
78 | am.on(game.AssetEvent.REMOTE_PROJECT_MANIFEST_LOAD_FAILD, this._onNetError.bind(this));
79 | am.on(game.AssetEvent.NO_NETWORK , this._onNetError.bind(this));
80 | am.onCheckComplete = onCheckComplete;
81 | am.onComplete = onComplete;
82 |
83 | this._updates[name] = am;
84 | },
85 |
86 | /** 是否没完成 */
87 | isNoComplete : function(name){
88 | if (this._noComplete[name] == null)
89 | return false;
90 |
91 | return true;
92 | },
93 |
94 | /**
95 | * 检查版本是否需要更新
96 | */
97 | check : function(name){
98 | var am = this._updates[name];
99 | am.check(name);
100 | },
101 |
102 | /** 断网后恢复状态 */
103 | recovery : function(name){
104 | if (this._current && this._isUpdating == false){
105 | this._isUpdating = true;
106 | this._current.recovery();
107 | }
108 | },
109 |
110 | _onFailed : function(event){
111 | this._isUpdating = false;
112 | event.target.check(event.target.name);
113 | },
114 |
115 | _onNetError : function(event){
116 | this._isUpdating = false;
117 | },
118 |
119 | /** 检查版本完成 */
120 | _onCheckComplete : function(event){
121 | this._queue.push(event.target);
122 |
123 | // 保存下在下载的模块状态
124 | this._saveNoCompleteModule();
125 |
126 | if (event.target.onCheckComplete) event.target.onCheckComplete();
127 |
128 | if (this._isUpdating == false){
129 | this._isUpdating = true;
130 | this._current = event.target;
131 | this._current.update();
132 | }
133 | },
134 |
135 | _onUpdateComplete : function(event){
136 | if (event.target.onComplete) event.target.onComplete();
137 |
138 | // 删除当前完成的更新对象
139 | this._queue.shift();
140 | this._isUpdating = false;
141 |
142 | // 保存下在下载的模块状态
143 | this._saveNoCompleteModule();
144 |
145 | // 更新对列中下一个更新对象
146 | if (this._queue.length > 0){
147 | this._isUpdating = true;
148 | this._current = this._queue[0];
149 | this._current.update();
150 | }
151 | },
152 |
153 | // 保存下在下载的模块状态
154 | _saveNoCompleteModule : function(){
155 | var names = [];
156 | for(var i = 0; i < this._queue.length; i++){
157 | names.push(this._queue[i].name);
158 | }
159 | cc.sys.localStorage.setItem("update_no_complete", JSON.stringify(names));
160 | }
161 | });
--------------------------------------------------------------------------------
/example/packages/hot_update/panel/copy.js:
--------------------------------------------------------------------------------
1 | //运行需要安装 async.js npm install async
2 | //更新完成后,拷贝发布文件到HTTP服务器更新目录
3 | var fs = require('fs');
4 | var path = require("path");
5 | var async = require("async");
6 |
7 | // cursively make dir
8 | function mkdirs(p, mode, f, made) {
9 | if (typeof mode === 'function' || mode === undefined) {
10 | f = mode;
11 | mode = 0777 & (~process.umask());
12 | }
13 | if (!made)
14 | made = null;
15 |
16 | var cb = f || function () {};
17 | if (typeof mode === 'string')
18 | mode = parseInt(mode, 8);
19 | p = path.resolve(p);
20 |
21 | fs.mkdir(p, mode, function (er) {
22 | if (!er) {
23 | made = made || p;
24 | return cb(null, made);
25 | }
26 | switch (er.code) {
27 | case 'ENOENT':
28 | mkdirs(path.dirname(p), mode, function (er, made) {
29 | if (er) {
30 | cb(er, made);
31 | } else {
32 | mkdirs(p, mode, cb, made);
33 | }
34 | });
35 | break;
36 |
37 | // In the case of any other error, just see if there's a dir
38 | // there already. If so, then hooray! If not, then something
39 | // is borked.
40 | default:
41 | fs.stat(p, function (er2, stat) {
42 | // if the stat fails, then that's super weird.
43 | // let the original error be the failure reason.
44 | if (er2 || !stat.isDirectory()) {
45 | cb(er, made);
46 | } else {
47 | cb(null, made)
48 | };
49 | });
50 | break;
51 | }
52 | });
53 | }
54 | // single file copy
55 | function copyFile(file, toDir, cb) {
56 | async.waterfall([
57 | function (callback) {
58 | fs.exists(toDir, function (exists) {
59 | if (exists) {
60 | callback(null, false);
61 | } else {
62 | callback(null, true);
63 | }
64 | });
65 | }, function (need, callback) {
66 | if (need) {
67 | mkdirs(path.dirname(toDir), callback);
68 | } else {
69 | callback(null, true);
70 | }
71 | }, function (p, callback) {
72 | var reads = fs.createReadStream(file);
73 | var writes = fs.createWriteStream(path.join(path.dirname(toDir), path.basename(file)));
74 | reads.pipe(writes);
75 | //don't forget close the when all the data are read
76 | reads.on("end", function () {
77 | writes.end();
78 | callback(null);
79 | });
80 | reads.on("error", function (err) {
81 | console.log("error occur in reads");
82 | callback(true, err);
83 | });
84 |
85 | }
86 | ], cb);
87 |
88 | }
89 |
90 | // cursively count the files that need to be copied
91 |
92 | function _ccoutTask(from, to, cbw) {
93 | async.waterfall([
94 | function (callback) {
95 | fs.stat(from, callback);
96 | },
97 | function (stats, callback) {
98 | if (stats.isFile()) {
99 | cbw.addFile(from, to);
100 | callback(null, []);
101 | } else if (stats.isDirectory()) {
102 | fs.readdir(from, callback);
103 | }
104 | },
105 | function (files, callback) {
106 | if (files.length) {
107 | for (var i = 0; i < files.length; i++) {
108 | _ccoutTask(path.join(from, files[i]), path.join(to, files[i]), cbw.increase());
109 | }
110 | }
111 | callback(null);
112 | }
113 | ], cbw);
114 |
115 | }
116 | // wrap the callback before counting
117 | function ccoutTask(from, to, cb) {
118 | var files = [];
119 | var count = 1;
120 |
121 | function wrapper(err) {
122 | count--;
123 | if (err || count <= 0) {
124 | cb(err, files)
125 | }
126 | }
127 | wrapper.increase = function () {
128 | count++;
129 | return wrapper;
130 | }
131 | wrapper.addFile = function (file, dir) {
132 | files.push({
133 | file : file,
134 | dir : dir
135 | });
136 | }
137 |
138 | _ccoutTask(from, to, wrapper);
139 | }
140 |
141 | /** 拷贝目录 */
142 | copyDir = function (from, to, cb) {
143 | if(!cb){
144 | cb=function(){};
145 | }
146 | async.waterfall([
147 | function (callback) {
148 | fs.exists(from, function (exists) {
149 | if (exists) {
150 | callback(null, true);
151 | } else {
152 | console.log(from + " not exists");
153 | callback(true);
154 | }
155 | });
156 | },
157 | function (exists, callback) {
158 | fs.stat(from, callback);
159 | },
160 | function (stats, callback) {
161 | if (stats.isFile()) {
162 | // one file copy
163 | copyFile(from, to, function (err) {
164 | if (err) {
165 | // break the waterfall
166 | callback(true);
167 | } else {
168 | callback(null, []);
169 | }
170 | });
171 | } else if (stats.isDirectory()) {
172 | ccoutTask(from, to, callback);
173 | }
174 | },
175 | function (files, callback) {
176 | // prevent reaching to max file open limit
177 | async.mapLimit(files, 10, function (f, cb) {
178 | copyFile(f.file, f.dir, cb);
179 | }, callback);
180 | }
181 | ], cb);
182 | }
--------------------------------------------------------------------------------
/example/assets/script/AssetsDownload.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 队列加载资源
3 | * Author : donggang
4 | * Create Time : 2016.12.15
5 | *
6 | * 事件:
7 | * this.onProgress 更新进度
8 | * this.onNoNetwork 断网
9 | * this.onComplete 更新完成
10 | */
11 |
12 | module.exports = cc.Class({
13 | /** 分析并获取需要更新的资源 */
14 | download: function (storagePath, localManifest, remoteManifest) {
15 | this._storagePath = storagePath;
16 | this._localManifest = localManifest;
17 | this._remoteManifest = remoteManifest;
18 |
19 | this._nocache = (new Date()).getTime();
20 |
21 | this._downloadUnits = []; // 下载文件对象集合
22 | this._completeUnits = []; // 已下载完成对象集合
23 | this._failedUnits = []; // 下载失败文件对象集合
24 | this._deleteUnits = []; // 需要删除文件对象集合
25 |
26 | this._downloadComplete = 0; // 下载完成的文件数量
27 | this._downloadFailed = 0; // 下载失败的文件数量
28 | this._failCount = 0; // 下载失败的次数
29 | this._concurrentCurrent = 0; // 并发数量当前值
30 |
31 | this._analysisDownloadUnits();
32 | this._analysisDeleteUnits();
33 |
34 | // 当前总更新单位数量 = 更新完成文件数量 + 待更新文件数量
35 | this._totalUnits = this._downloadComplete + this._downloadUnits.length;
36 |
37 | cc.log("【更新】共有{0}个文件需要更新".format(this._downloadUnits.length));
38 | cc.log("【更新】共有{0}个文件需要删除".format(this._deleteUnits.length));
39 |
40 | this._items = this._downloadUnits.slice(0);
41 |
42 | if (this._items.length > 0){
43 | this._downloadAsset();
44 | }
45 | else{
46 | cc.log("【更新】无更新文件,更新完成");
47 | if (this.onComplete) this.onComplete();
48 | }
49 | },
50 |
51 | /** 对比本地项目清单数据和服务器清单数据,找出本地缺少的以及和服务器不一样的资源 */
52 | _analysisDownloadUnits : function(){
53 | for (var key in this._remoteManifest.assets) {
54 | if (this._localManifest.assets.hasOwnProperty(key)) {
55 | if (game.AssetConfig.debugRes || this._remoteManifest.assets[key].md5 != this._localManifest.assets[key].md5) {
56 | // cc.log("【更新】准备下载更新的资源 {0}".format(key));
57 | this._addDownloadUnits(key);
58 | }
59 | }
60 | else {
61 | // cc.log("【更新】准备下载本是不存在的资源 {0}".format(key));
62 | this._addDownloadUnits(key);
63 | }
64 | }
65 | },
66 |
67 | /** 对比本地项目清单数据和服务器清单数据,找出本地多出的资源 */
68 | _analysisDeleteUnits : function(){
69 | for (var key in this._localManifest.assets) {
70 | if (this._remoteManifest.assets.hasOwnProperty(key) == false) {
71 | // cc.log("【更新】准备删除的资源{0}".format(key));
72 | this._deleteUnits.push(key);
73 | }
74 | }
75 | },
76 |
77 | /** 添加下载单位 */
78 | _addDownloadUnits : function(key){
79 | if (this._remoteManifest.assets[key].state != true){
80 | this._downloadUnits.push(key); // 远程版本的文件 MD5 值和本地不同时文件需要下载
81 | }
82 | else {
83 | this._downloadComplete++; // 恢复状态时的下载完成数量
84 | }
85 | },
86 |
87 | /** 断网后恢复更新状态 */
88 | recovery : function(){
89 | this._downloadAsset();
90 | },
91 |
92 | /** 下载资源 */
93 | _downloadAsset : function(){
94 | if (game.config.textUpdate) this._remoteManifest.server = game.AssetConfig.testCdn;
95 |
96 | var relativePath = this._items.shift();
97 | var url = cc.path.join(this._remoteManifest.server, game.AssetConfig.line, relativePath);
98 |
99 | // 下载成功
100 | var complete = function (asset) {
101 | // 文件保存到本地
102 | this._saveAsset(relativePath, asset);
103 |
104 | // 记录更新完成的文件
105 | this._completeUnits.push(relativePath);
106 |
107 | // 下载完成的文件数量加 1
108 | this._downloadComplete++;
109 |
110 | if (game.AssetConfig.debugProgress)
111 | cc.log("【更新】进度 {0}/{1},当前有 {2} 个资源并行下载".format(this._downloadComplete, this._totalUnits, this._concurrentCurrent));
112 |
113 | // 还原并发数量
114 | this._concurrentCurrent--;
115 |
116 | // 更新进度事件
117 | if (this.onProgress) {
118 | this.onProgress(relativePath, this._downloadComplete / this._totalUnits);
119 | }
120 |
121 | // 判断是否下载完成
122 | this._isUpdateCompleted();
123 | }.bind(this);
124 |
125 | // 下载失败
126 | var error = function (error) {
127 | this._failedUnits.push(relativePath);
128 | this._concurrentCurrent--;
129 | this._downloadFailed++;
130 |
131 | if (game.HttpEvent.NO_NETWORK){ // 触发断网事件
132 | if (this._concurrentCurrent == 0){
133 | if (this.onNoNetwork) this.onNoNetwork();
134 | }
135 | }
136 | else {
137 | cc.log("【更新】下载远程路径为 {0} 的文件失败,错误码为 {1}".format(url, error));
138 | cc.log("【更新】进度 {0}/{1}, 总处理文件数据为 {2}".format(this._downloadComplete, this._totalUnits, this._downloadComplete + this._downloadFailed));
139 |
140 | this._isUpdateCompleted();
141 | }
142 | }.bind(this);
143 |
144 | game.http.getByArraybuffer(this._noCache(url), complete, error);
145 |
146 | // 开启一个并行下载队列
147 | this._concurrentCurrent++;
148 | if (this._concurrentCurrent < game.AssetConfig.concurrent){
149 | this._downloadAsset();
150 | }
151 | },
152 |
153 | /** 下载失败的资源 */
154 | _downloadFailedAssets: function () {
155 | // 下载失败的文件数量重置
156 | this._downloadFailed = 0;
157 | this._downloadUnits = this._failedUnits;
158 | this._failedUnits = [];
159 | this._items = this._downloadUnits.slice(0);
160 |
161 | if (this._items.length > 0){
162 | this._downloadAsset();
163 | }
164 | },
165 |
166 | /** 判断是否全部更新完成 */
167 | _isUpdateCompleted: function () {
168 | var handleCount = this._downloadComplete + this._downloadFailed; // 处理完成数量
169 |
170 | if (this._totalUnits == this._downloadComplete) { // 全下载完成
171 | cc.log("【更新】更新完成");
172 |
173 | // 触发热更完成事件
174 | if (this.onComplete) this.onComplete();
175 |
176 | // 删除本地比服务器多出的文件
177 | this._deleteAssets();
178 | }
179 | else if (this._totalUnits == handleCount) { // 全处理完成,有下载失败的文件,需要重试
180 | cc.log("【更新】下载文件总数量 :", this._totalUnits);
181 | cc.log("【更新】下载成功的文件数量:", this._downloadComplete);
182 | cc.log("【更新】下载失败的文件数量:", this._downloadFailed);
183 |
184 | // 更新失败的次数加 1
185 | this._failCount++;
186 |
187 | if (this._failCount < 3) {
188 | cc.log("【更新】更新重试第 {0} 次".format(this._failCount));
189 |
190 | this._downloadFailedAssets();
191 | }
192 | else {
193 | cc.log("【更新】更新失败");
194 |
195 | // 触发热更失败事件
196 | if (this.onFaild) this.onFaild();
197 | }
198 | }
199 | else if (this._items.length > 0 && this._concurrentCurrent < game.AssetConfig.concurrent) { // 队列下载
200 | this._downloadAsset();
201 | }
202 | },
203 |
204 | /** 删除本地比服务器多出的文件 */
205 | _deleteAssets: function () {
206 | for (var i = 0; i < this._deleteUnits.length; i++) {
207 | var relativePath = this._deleteUnits[i];
208 | var filePath = cc.path.join(this._storagePath, relativePath);
209 | if (jsb.fileUtils.removeFile(filePath)) {
210 | cc.log("【更新】版本多余资源 {0} 删除成功".format(filePath));
211 | }
212 | else {
213 | cc.log("【更新】版本多余资源 {0} 删除失败".format(filePath));
214 | };
215 | }
216 | },
217 |
218 | /** 文件保存到本地 */
219 | _saveAsset : function(relativePath, asset){
220 | if (cc.sys.isNative){
221 | var storeDirectory = cc.path.join(this._storagePath, relativePath.substr(0, relativePath.lastIndexOf("/")));
222 | var storePath = cc.path.join(this._storagePath, relativePath);
223 |
224 | // 存储目录
225 | if (jsb.fileUtils.isDirectoryExist(storeDirectory) == false) {
226 | jsb.fileUtils.createDirectory(storeDirectory);
227 | }
228 |
229 | // 存储文件
230 | jsb.fileUtils.writeDataToFile(new Uint8Array(asset), storePath);
231 | }
232 | },
233 |
234 | /** 规避 HTTP 缓存问题 */
235 | _noCache: function (url) {
236 | return url + "?t=" + this._nocache;
237 | }
238 | })
--------------------------------------------------------------------------------
/example/packages/hot_update/panel/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | /** 模块引用 */
4 | var fs = require('fs');
5 | var path = require('path');
6 | var crypto = require('crypto');
7 |
8 | require(path.join(Editor.projectInfo.path, 'packages/hot_update/panel/copy.js'));
9 |
10 | // 版本配置
11 | var config = {
12 | "isUpdateVersion" : true,
13 | "line" : "line1", // 版本线路文件夹,用于更新时优先更新没有用的线路,测试成功后切换热更
14 | "server" : "http://172.18.254.56:8080/update/",
15 | "modules": {
16 | "common" : {
17 | initial : "1.0.0.0015", // 发安装包时,initial 值和 initial 的值保持一致
18 | current : "1.0.0.0015"
19 | },
20 | // 农场主题
21 | "10001" : {
22 | initial : "1.0.0.0001",
23 | current : "1.0.0.0001"
24 | },
25 | // 水晶主题
26 | "10002" : {
27 | initial : "0",
28 | current : "1.0.0.0001"
29 | },
30 | // 中国风主题
31 | "10003" : {
32 | initial : "0",
33 | current : "1.0.0.0001"
34 | },
35 | // 社交主题
36 | "11001" : {
37 | initial : "0",
38 | current : "1.0.0.0001"
39 | }
40 | }
41 | }
42 |
43 |
44 | // 输入参数
45 | var args = {};
46 | args.versionInitialPackPlayId = "10001"; // 初始版本打包进去的玩法编号
47 | args.server = config.server;
48 | args.releasePath = path.join(Editor.projectInfo.path, "build/jsb-default/"); // 发布资源文件路径
49 | args.projectVersionInargsPath = path.join(Editor.projectInfo.path, "assets/resources/version/"); // 项目初始热更配置文件路径
50 | args.releaseVersionInargsPath = path.join(args.releasePath , "res/raw-assets/resources/version"); // 发布初始热更配置文件路径
51 | args.releaseCdnPath = path.join(Editor.projectInfo.path, "../game-slots-update/update/", config.line, "/"); // 发布到CND路径
52 | args.releaseVersionConfigPath = path.join(Editor.projectInfo.path, "../game-slots-update-version/constinfo/version.json"); // 发布的所有模块版本信息
53 |
54 | /*----------------------------------------------------------------------------------------------------*/
55 |
56 | // 生成初始热更配置
57 | var VersionInitial = function(moduleName, version, mPaths) {
58 | var mPaths = mPaths;
59 |
60 | var projectName = moduleName + "_project.manifest";
61 | var versionName = moduleName + "_version.manifest";
62 |
63 | var data = {
64 | server : args.server,
65 | remoteManifest : projectName,
66 | remoteVersion : versionName,
67 | version : version,
68 | assets : {},
69 | searchPaths : []
70 | }
71 |
72 | /** 处理配置信息 */
73 | this.process = function(){
74 | this.traversal(path.join(args.releasePath, 'res'), data.assets, moduleName);
75 |
76 | if (moduleName == "common")
77 | this.traversal(path.join(args.releasePath, 'src'), data.assets);
78 |
79 | var projectManifest = path.join(args.projectVersionInargsPath, projectName);
80 | var releaseManifest = path.join(args.releaseVersionInargsPath, projectName);
81 |
82 | // 生成配置配置文件
83 | fs.writeFile(projectManifest, JSON.stringify(data), (err) => {
84 | if (err) throw err;
85 | // Editor.log("生成初始热更配置文件 : " + projectName);
86 | });
87 |
88 | // 拷贝到发布目录
89 | copyDir(projectManifest, releaseManifest, function (err) {
90 | if (err) Editor.log("生成初始热更配置文件出错 : \r\n" + path.resolve(releaseManifest));
91 | else {
92 | Editor.log("生成初始热更配置文件到发布目录 : " + projectName);
93 | }
94 | });
95 | }
96 |
97 | /**
98 | * 避难遍历文件和目录
99 | * @param folder(string) 目录
100 | * @param data(object) 数据
101 | * @param moduleName(string) 模块名
102 | */
103 | this.traversal = function(folder, data, moduleName) {
104 | var stat = fs.statSync(folder);
105 | if (!stat.isDirectory()) {
106 | return;
107 | }
108 |
109 | var mPath = "resources/" + moduleName + "/"; // 模块路径
110 | var subpaths = fs.readdirSync(folder), subpath, md5, relative;
111 | for (var i = 0; i < subpaths.length; ++i) {
112 | if (subpaths[i][0] === '.') {
113 | continue;
114 | }
115 | subpath = path.join(folder, subpaths[i]);
116 | stat = fs.statSync(subpath);
117 |
118 | if (stat.isDirectory()) {
119 | this.traversal(subpath, data, moduleName);
120 | }
121 | else if (stat.isFile()) {
122 | md5 = crypto.createHash('md5').update(fs.readFileSync(subpath, 'utf8')).digest('hex');
123 | relative = path.relative(args.releasePath, subpath);
124 | relative = relative.replace(/\\/g, '/');
125 |
126 | if (relative.indexOf("src/") > -1){
127 | data[relative] = {'md5' : md5};
128 | }
129 | else if(relative.indexOf("res/") > -1){
130 | if (moduleName.indexOf("common") == -1 && relative.indexOf(mPath) > -1 && moduleName == args.versionInitialPackPlayId){ // 不为主模块
131 | data[relative] = {'md5' : md5};
132 | }
133 | else if (moduleName.indexOf("common") > -1 &&
134 | (relative.indexOf(mPath) > -1 || relative.indexOf("/import/") > -1 || relative.indexOf("/raw-internal/") > -1)){
135 | data[relative] = {'md5' : md5};
136 | }
137 | }
138 | }
139 | }
140 | }
141 | }
142 |
143 | // 发布游戏初始版本配置文件
144 | function releaseVersionInitial(){
145 | // 找到目标模块目录下的所有文件
146 | var mPaths = []; // 模块名
147 | var mVersions = []; // 版本号
148 | for(var moduleNmae in config.modules){
149 | mPaths.push(moduleNmae);
150 | mVersions.push(config.modules[moduleNmae].initial);
151 | }
152 |
153 | // 生成不同模块的更新配置文件
154 | for(var i = 0; i < mPaths.length; i++){
155 | var vi = new VersionInitial(mPaths[i], mVersions[i], mPaths);
156 | vi.process();
157 | }
158 |
159 | // 发布CDN版本更新资源
160 | releaseVersionCdn();
161 | }
162 |
163 | /*----------------------------------------------------------------------------------------------------*/
164 |
165 | // 生成初始热更配置
166 | var VersionCdn = function(moduleName, version) {
167 | var projectName = moduleName + "_project.manifest";
168 | var versionName = moduleName + "_version.manifest";
169 |
170 | var data = {
171 | server : args.server,
172 | remoteManifest : projectName,
173 | remoteVersion : versionName,
174 | version : version,
175 | assets : {},
176 | searchPaths : []
177 | }
178 |
179 | /** 处理配置信息 */
180 | this.process = function(){
181 | this.traversal(path.join(args.releasePath, 'res'), data.assets, moduleName);
182 |
183 | if (moduleName == "common")
184 | this.traversal(path.join(args.releasePath, 'src'), data.assets);
185 |
186 | var projectProjectManifest = path.join(args.releaseCdnPath, projectName);
187 |
188 | // 生成配置配置文件 project.manifest
189 | fs.writeFile(projectProjectManifest, JSON.stringify(data), (err) => {
190 | if (err) throw err;
191 | // Editor.log("生成CDN更配置文件 : " + projectName);
192 | });
193 |
194 | var projectVersionManifest = path.join(args.releaseCdnPath, versionName);
195 |
196 | delete data.assets;
197 | delete data.searchPaths;
198 |
199 | // 生成配置配置文件 version.manifest
200 | fs.writeFile(projectVersionManifest, JSON.stringify(data), (err) => {
201 | if (err) throw err;
202 | // Editor.log("生成CDN更配置文件 : " + versionName);
203 | });
204 | }
205 |
206 | /**
207 | * 避难遍历文件和目录
208 | * @param folder(string) 目录
209 | * @param data(object) 数据
210 | * @param moduleName(string) 模块名
211 | */
212 | this.traversal = function(folder, data, moduleName) {
213 | var stat = fs.statSync(folder);
214 | if (!stat.isDirectory()) {
215 | return;
216 | }
217 |
218 | var mPath = "resources/" + moduleName + "/"; // 模块路径
219 | var subpaths = fs.readdirSync(folder), subpath, md5, relative;
220 | for (var i = 0; i < subpaths.length; ++i) {
221 | if (subpaths[i][0] === '.') {
222 | continue;
223 | }
224 | subpath = path.join(folder, subpaths[i]);
225 | stat = fs.statSync(subpath);
226 |
227 | if (stat.isDirectory()) {
228 | this.traversal(subpath, data, moduleName);
229 | }
230 | else if (stat.isFile()) {
231 | md5 = crypto.createHash('md5').update(fs.readFileSync(subpath, 'utf8')).digest('hex');
232 | relative = path.relative(args.releasePath, subpath);
233 | relative = relative.replace(/\\/g, '/');
234 |
235 | if (relative.indexOf("src/") > -1){
236 | data[relative] = {'md5' : md5};
237 | }
238 | else if(relative.indexOf("res/") > -1){
239 | if (moduleName.indexOf("common") == -1 && relative.indexOf(mPath) > -1){ // 不为主模块
240 | data[relative] = {'md5' : md5};
241 | }
242 | else if (moduleName.indexOf("common") > -1 &&
243 | (relative.indexOf(mPath) > -1 || relative.indexOf("/import/") > -1 || relative.indexOf("/raw-internal/") > -1)){
244 | data[relative] = {'md5' : md5};
245 | }
246 | }
247 | }
248 | }
249 | }
250 | }
251 |
252 | // 发布CDN版本更新资源
253 | function releaseVersionCdn(){
254 | var mPaths = []; // 模块名
255 | var mVersions = []; // 版本号
256 | var playData = {}; // 玩法版本信息
257 | playData.line = config.line;
258 | playData.modules = {};
259 | for(var moduleNmae in config.modules){
260 | mPaths.push(moduleNmae);
261 | mVersions.push(config.modules[moduleNmae].current);
262 |
263 | playData.modules[moduleNmae] = config.modules[moduleNmae].current;
264 | }
265 |
266 | // 生成CDN所有版本配置文件
267 | if (config.isUpdateVersion){
268 | fs.writeFile(args.releaseVersionConfigPath, JSON.stringify(playData), (err) => {
269 | if (err) throw err;
270 | Editor.log("生成CDN所有版本配置文件完成");
271 | });
272 | }
273 |
274 | // 生成不同模块的更新配置文件
275 | for(var i = 0; i < mPaths.length; i++){
276 | var vc = new VersionCdn(mPaths[i], mVersions[i]);
277 | vc.process();
278 | }
279 |
280 | /** 删除文件夹 */
281 | function deleteFolderRecursive(path) {
282 | var files = [];
283 | if (fs.existsSync(path)) {
284 | files = fs.readdirSync(path);
285 | files.forEach(function(file, index){
286 | var curPath = path + "/" + file;
287 | if (fs.statSync(curPath).isDirectory()) {
288 | deleteFolderRecursive(curPath);
289 | }
290 | else {
291 | fs.unlinkSync(curPath);
292 | }
293 | });
294 | }
295 | };
296 |
297 | deleteFolderRecursive(path.resolve(args.releaseCdnPath + "src"))
298 | deleteFolderRecursive(path.resolve(args.releaseCdnPath + "res"))
299 |
300 | copyDir(path.resolve(args.releasePath + "src"), path.resolve(args.releaseCdnPath + "src"), function (err) {
301 | if (err) Editor.log("发布到CND的 src 目录出错");
302 | else Editor.log("发布到CND的 src 目录完成")
303 | });
304 |
305 | copyDir(path.resolve(args.releasePath + "res"), path.resolve(args.releaseCdnPath + "res"), function (err) {
306 | if (err) Editor.log("发布到CND的 res 目录出错");
307 | else Editor.log("发布到CND的 res 目录完成");
308 | });
309 | }
310 |
311 |
312 |
313 | /*----------------------------------------------------------------------------------------------------*/
314 |
315 | Editor.Panel.extend({
316 | ready () {
317 | var realPath = path.join(args.releaseCdnPath);
318 | Editor.log(realPath);
319 | if (!fs.existsSync(realPath)) {
320 | fs.mkdirSync(realPath);
321 | }
322 | releaseVersionInitial();
323 | },
324 | });
325 |
--------------------------------------------------------------------------------
/example/assets/script/AssetsManager.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 资源管理
3 | * Author : donggang
4 | * Create Time : 2016.11.26
5 | *
6 | * 需求:
7 | * 1、版本配置文件对比,提示有新版本
8 | * 2、版本清单文件对比,分析需要更新的资源文件
9 | * 3、批量下载更新的资源文件保存到系统本地存储目录
10 | * 4、批量删除服务器清单文件中没有的资源文件
11 | * 5、更新失败或异常中断时,记录更新状态,下次触发更新时恢复更新进度,不下载已更新的文件
12 | * 6、智能队列更新资源文件,以游戏 fps 数据做为动态计算更新速度(手机性能不够,暂时没必要做)
13 | * 7、版本回退功能,通过查询目录的顺序,把版本版本的更新文件放到查询目录中(思考)
14 | * 8、版本文件摘要验证功能(思考)
15 | * 9、配置文件的数据内存是没有删除的(思考是否需要做处理)
16 | */
17 |
18 | // 本地缓存关键字
19 | var LOCAL_STORAGE_FOLDER = "game-remote-asset"; // 版本资源目录
20 | var LOCAL_STORAGE_KEY_PROJECT = "update_project"; // 本地缓存清单数据
21 | var LOCAL_STORAGE_KEY_UPDATE_STATE = "update_state"; // 本地缓存更新状态数据(每完成一个资源下载会记录完成数据队列)
22 | var MODULE_PROJECT_MANIFEST_PATH = "version/{0}_project"; // 模块清单路径
23 |
24 | // 事件
25 | game.AssetEvent = {};
26 | game.AssetEvent.NEW_VERSION = "asset_new_version"; // 已是最新版本
27 | game.AssetEvent.NEW_VERSION_FOUND = "asset_new_version_found"; // 找到新版本
28 | game.AssetEvent.SUCCESS = "asset_success"; // 更新成功
29 | game.AssetEvent.FAILD = "asset_failed"; // 更新失败
30 | game.AssetEvent.PROGRESS = "asset_progress"; // 更新进度
31 | game.AssetEvent.LOCAL_PROJECT_MANIFEST_LOAD_FAIL = "asset_local_project_manifest_load_fail"; // 获取游戏中路安装包中资源清单文件失败
32 | game.AssetEvent.REMOTE_VERSION_MANIFEST_LOAD_FAILD = "asset_remote_version_manifest_load_faild"; // 获取远程版本配置文件失败
33 | game.AssetEvent.REMOTE_PROJECT_MANIFEST_LOAD_FAILD = "asset_remote_project_manifest_load_faild"; // 获取远程更新单清文件失败
34 | game.AssetEvent.NO_NETWORK = "asset_no_network"; // 断网
35 |
36 | // 配置
37 | game.AssetConfig = {};
38 | game.AssetConfig.debugVersion = false; // 无视版本号测试
39 | game.AssetConfig.debugRes = false; // 无视资源版本对比测试
40 | game.AssetConfig.debugProgress = false; // 打印进度日志
41 | game.AssetConfig.testIp = "172.18.254.56"; // 测试服务器地址
42 | game.AssetConfig.testCdn = "http://" + game.AssetConfig.testIp + "/update/"; // 测试 CDN 服务器地址
43 |
44 | game.AssetConfig.concurrent = 1; // 最大并发更新文件数量(有网络IO和文件IO并载数量在边玩游戏边下载时建议不超过2)
45 | game.AssetConfig.line = "line1"; // 版本线路文件夹,用于更新时优先更新没有用的线路,测试成功后切换热更
46 |
47 | var AssetsDownload = require("AssetsDownload");
48 | var AssetsManager = cc.Class({
49 | extends : cc.EventTarget,
50 |
51 | /** 更新进度 */
52 | getProgress : function(){
53 | return this._progress;
54 | },
55 |
56 | /**
57 | * 对比服务器版本信息
58 | * @param appManifestPath(string) 本地清单文件路径
59 | */
60 | check : function (moduleName) {
61 | if (this._isUpdate == true) {
62 | cc.log("【更新】模块{0}正在更新中".format(moduleName));
63 | return;
64 | }
65 |
66 | if (cc.sys.isNative) {
67 | this._storagePath = ((jsb.fileUtils ? jsb.fileUtils.getWritablePath() : '/') + LOCAL_STORAGE_FOLDER);
68 |
69 | if (jsb.fileUtils.isDirectoryExist(this._storagePath) == false) {
70 | jsb.fileUtils.createDirectory(this._storagePath);
71 | }
72 |
73 | cc.log("【更新】版本本地存储路径 {0}".format(this._storagePath));
74 | }
75 | else {
76 | this._storagePath = "";
77 | }
78 |
79 | this._nocache = game.getLocalTime();
80 | this._ad = new AssetsDownload();
81 |
82 | this._moduleName = moduleName; // 模块名
83 | this._moduleManifest = LOCAL_STORAGE_KEY_PROJECT + "_" + moduleName;
84 | this._moduleState = LOCAL_STORAGE_KEY_UPDATE_STATE + "_" + moduleName;
85 |
86 | this._appManifest; // 安装里的清单数据(JSON)
87 | this._localManifest; // 本地存储里的清单数据(JSON)
88 | this._remoteManifest; // 远程更新服务器的清单数据(JSON)
89 |
90 | this._progress = 0; // 更新进度
91 | this._isUpdate = true; // 是否正在更新中
92 |
93 | this._loadLocalManifest(MODULE_PROJECT_MANIFEST_PATH.format(moduleName));
94 | },
95 |
96 | /** 开始更新版本 */
97 | update : function () {
98 | // 获取本地存储更新状态数据,如果有则继续上次的更新,无则下载远程服务器版本清单数据
99 | var tempManifest = cc.sys.localStorage.getItem(this._moduleState);
100 | if (tempManifest == null) {
101 | var complete = function (content) {
102 | // 解析远程资源清单数据
103 | try {
104 | this._remoteManifest = JSON.parse(content);
105 | }
106 | catch(e) {
107 | cc.error("【更新】远程路版本清单数据解析错误");
108 | }
109 |
110 | // 分析并下载资源
111 | this._downloadAssets();
112 | }.bind(this);
113 |
114 | var error = function (error) {
115 | cc.log("【更新】获取远程路径为 {0} 的版本清单文件失败".format(this._remoteManifest.remoteManifest));
116 |
117 | this._isUpdate = false;
118 |
119 | this._dispatchEvent(game.AssetEvent.REMOTE_PROJECT_MANIFEST_LOAD_FAILD);
120 | }.bind(this);
121 |
122 | if (game.config.textUpdate) this._localManifest.server = game.AssetConfig.testCdn;
123 |
124 | var url = this._localManifest.server + game.AssetConfig.line + "/" + this._localManifest.remoteManifest;
125 | game.http.get(this._noCache(url), complete, error);
126 | }
127 | else {
128 | cc.log("【更新】获取上次没更新完的版本清单更新状态");
129 |
130 | this._remoteManifest = JSON.parse(tempManifest);
131 |
132 | // 分析并下载资源
133 | this._downloadAssets();
134 | }
135 | },
136 |
137 | /** 加载本地项目中资源清单数据 */
138 | _loadLocalManifest: function (appManifestPath) {
139 | // 加载本地项目中资源清单数据
140 | cc.loader.loadRes(appManifestPath, function (error, content) {
141 | if (error) {
142 | cc.log("【更新】获取游戏中路安装包中路径为 {0} 的资源清单文件失败".format(appManifestPath));
143 | this._dispatchEvent(game.AssetEvent.LOCAL_PROJECT_MANIFEST_LOAD_FAIL);
144 | return;
145 | }
146 |
147 | // 安装包中版本清单数据解析
148 | try {
149 | this._appManifest = JSON.parse(content);
150 | }
151 | catch(e) {
152 | cc.error("【更新】安装包中的版本清单数据解析错误");
153 | }
154 |
155 | // 获取本地存储中版本清单数据(上次更新成功后的远程清单数据)
156 | var data = cc.sys.localStorage.getItem(this._moduleManifest);
157 | if (data) {
158 | try {
159 | this._localManifest = JSON.parse(data);
160 | }
161 | catch(e) {
162 | cc.error("【更新】本地版本清单数据解析错误");
163 | }
164 |
165 | // 安装包中的版本高于本地存储版本,则替换本地存储版本数据
166 | if (this._localManifest.version < this._appManifest.version){
167 | // 删除本地存储中的当前模块的旧的资源
168 | for (var key in this._localManifest.assets) {
169 | var filePath = cc.path.join(this._storagePath, key);
170 | if (jsb.fileUtils.isFileExist(filePath)){
171 | jsb.fileUtils.removeFile(filePath);
172 | }
173 | }
174 |
175 | cc.log("【更新】安装包的版本号为{0},本地存储版本号为{1},替换本地存储版本数据".format(this._appManifest.version, this._localManifest.version))
176 | this._localManifest = this._appManifest;
177 | }
178 | }
179 | else {
180 | cc.log("【更新】第一次安装,获取安装版中的版本清单数据");
181 | this._localManifest = this._appManifest;
182 | }
183 |
184 | // 检查版本号
185 | this._checkVersion();
186 | }.bind(this));
187 | },
188 |
189 | /** 检查版本号 */
190 | _checkVersion: function () {
191 | var complete = function (content) {
192 | /** 远程版本数据解析 */
193 | try {
194 | var remoteVersion = JSON.parse(content);
195 |
196 | // 游戏中路资源版本小于远程版本时,提示有更新
197 | if (game.AssetConfig.debugVersion || this._localManifest.version < remoteVersion.version) {
198 | cc.log("【更新】当前版本号为 {0},服务器版本号为 {1}, 有新版本可更新".format(this._appManifest.version, remoteVersion.version));
199 | this._dispatchEvent(game.AssetEvent.NEW_VERSION_FOUND); // 触发有新版本事件
200 | }
201 | else{
202 | cc.log("【更新】当前为最新版本");
203 | this._isUpdate = false;
204 | this._dispatchEvent(game.AssetEvent.NEW_VERSION); // 触发已是最新版本事件
205 | }
206 | }
207 | catch(e) {
208 | cc.error("【更新】远程路版本数据解析错误");
209 | }
210 | }.bind(this);
211 |
212 | var error = function (error) {
213 | cc.log("【更新】获取远程路径为 {0} 的版本文件失败".format(this._localManifest.remoteVersion));
214 | this._isUpdate = false;
215 | this._dispatchEvent(game.AssetEvent.REMOTE_VERSION_MANIFEST_LOAD_FAILD);
216 | }.bind(this);
217 |
218 | if (game.config.textUpdate) this._localManifest.server = game.AssetConfig.testCdn;
219 |
220 | // 获取远程版本数据
221 | var url = this._localManifest.server + game.AssetConfig.line + "/" + this._localManifest.remoteVersion;
222 | game.http.get(this._noCache(url), complete, error);
223 | },
224 |
225 | /** 开始下载资源 */
226 | _downloadAssets: function () {
227 | // 触发热更进度事件
228 | this._ad.onProgress = function(relativePath, percent){
229 | this._progress = percent;
230 |
231 | // 记录当前更新状态,更新失败时做为恢复状态使用
232 | this._remoteManifest.assets[relativePath].state = true;
233 | cc.sys.localStorage.setItem(this._moduleState, JSON.stringify(this._remoteManifest));
234 |
235 | this._dispatchEvent(game.AssetEvent.PROGRESS);
236 | }.bind(this);
237 |
238 | // 触发热更完成事件
239 | this._ad.onComplete = function(){
240 | this._isUpdate = false;
241 |
242 | // 删除更新状态数据
243 | cc.sys.localStorage.removeItem(this._moduleState);
244 |
245 | // 更新本地版本清单数据,用于下次更新时做版本对比
246 | for (var key in this._remoteManifest.assets) {
247 | var asset = this._remoteManifest.assets[key];
248 | if (asset.state) delete asset.state;
249 | }
250 | cc.sys.localStorage.setItem(this._moduleManifest, JSON.stringify(this._remoteManifest));
251 |
252 | // 触发热更完成事件
253 | this._dispatchEvent(game.AssetEvent.SUCCESS);
254 | }.bind(this);
255 |
256 | // 触发热更失败事件
257 | this._ad.onFaild = function(){
258 | this._isUpdate = false;
259 | this._dispatchEvent(game.AssetEvent.FAILD);
260 | }.bind(this);
261 |
262 | // 触发断网事件
263 | this._ad.onNoNetwork = function(){
264 | this._isUpdate = false;
265 | this._dispatchEvent(game.AssetEvent.NO_NETWORK);
266 | }.bind(this);
267 |
268 | this._ad.download(this._storagePath, this._localManifest, this._remoteManifest);
269 | },
270 |
271 | /** 断网后恢复状态 */
272 | recovery : function(){
273 | this._ad.recovery();
274 | },
275 |
276 | /**
277 | * 触发事件
278 | * @param type(string) 事件类型
279 | * @param args(object) 事件参数
280 | */
281 | _dispatchEvent : function(type, args){
282 | var event = new cc.Event.EventCustom();
283 | event.type = type;
284 | event.bubbles = false;
285 | event.target = this;
286 | event.currentTarget = this;
287 | this.dispatchEvent(event);
288 | },
289 |
290 | /** 规避 HTTP 缓存问题 */
291 | _noCache: function (url) {
292 | return url + "?t=" + this._nocache;
293 | }
294 | })
295 |
296 | /** 验证是否有覆盖安装,安装包中版本高于本地资源时,删除本地资源的模块资源 */
297 | AssetsManager.check = function(remoteVersion){
298 | var moduleTotal = 0;
299 | var moduleCurrent = 0;
300 |
301 | // 加载安装包中的版本清单文件
302 | var loadAppManifest = function(moduleName, modules, versions, remoteVersion){
303 | var appManifestPath = MODULE_PROJECT_MANIFEST_PATH.format(moduleName);
304 | cc.loader.loadRes(appManifestPath, function (error, content) {
305 | if (error) {
306 | cc.error("【更新】验证是否有覆盖安装时,获取游戏中路安装包中路径为 {0} 的资源清单文件失败".format(appManifestPath));
307 | return;
308 | }
309 |
310 | var storagePath = ((jsb.fileUtils ? jsb.fileUtils.getWritablePath() : '/') + LOCAL_STORAGE_FOLDER);
311 | var appManifest = JSON.parse(content);
312 | var appVersion = appManifest.version;
313 |
314 | // 获取本地版本清单信息
315 | var moduleManifest = LOCAL_STORAGE_KEY_PROJECT + "_" + moduleName;
316 | var manifest = cc.sys.localStorage.getItem(moduleManifest);
317 | if (manifest) {
318 | var localManifest = JSON.parse(manifest);
319 | var localVersion = localManifest.version;
320 |
321 | // 安装包中的版本高于本地存储版本,则替换本地存储版本数据
322 | if (localVersion < appVersion){
323 | // 删除本地存储中的当前模块的旧的资源
324 | for (var key in localManifest.assets) {
325 | var filePath = cc.path.join(storagePath, key);
326 | if (jsb.fileUtils.isFileExist(filePath)){
327 | jsb.fileUtils.removeFile(filePath);
328 | }
329 | }
330 |
331 | versions[moduleName] = appVersion; // 有本地清单数据时,当前版本号为安装包版本号
332 | modules[moduleName] = modules[moduleName] > appVersion; // 有本地清单数据时,安卓包版本号小于远程版本号
333 | }
334 | else{
335 | versions[moduleName] = localVersion; // 有本地清单数据时,当前版本号为本地版本号
336 | modules[moduleName] = modules[moduleName] > localVersion; // 有本地清单数据时,本地清单版本号小于远程版本号
337 | }
338 | }
339 | else{
340 | versions[moduleName] = appVersion; // 没有本地清单数据时,当前版本号为安装包版本号
341 | modules[moduleName] = modules[moduleName] > appVersion; // 没有本地清单数据时,安卓包版本号小于远程版本号
342 | }
343 |
344 | moduleCurrent++;
345 |
346 | if (moduleCurrent == moduleTotal){
347 | if (remoteVersion) remoteVersion(modules, versions);
348 | }
349 | });
350 | }
351 |
352 | // 游戏所有模块的配置文件
353 | var url = "";
354 |
355 | if (game.config.useSSL){
356 | url = "https://{0}:3001/constinfo/version?t={1}".format(game.config.gateSocketIp, game.getLocalTime());
357 | }
358 | else{
359 | url = "http://{0}:3001/constinfo/version?t={1}".format(game.config.gateSocketIp, game.getLocalTime());
360 | }
361 |
362 | if (game.config.textUpdate) url = "http://{0}:3001/constinfo/version.json?t={1}".format(game.AssetConfig.testIp, game.getLocalTime());
363 |
364 | // 加载游戏模块当前最前版本号数据
365 | game.http.get(url, function(version_json){
366 | var json = JSON.parse(version_json);
367 | var modules = json.modules;
368 | var versions = {};
369 | game.AssetConfig.line = json.line;
370 |
371 | // 计算游戏共有多少个模块
372 | for(var moduleName in modules){
373 | moduleTotal++;
374 | }
375 |
376 | // 载入游戏所有安装包中的模块版本数据
377 | for(var moduleName in modules){
378 | loadAppManifest(moduleName, modules, versions, remoteVersion);
379 | }
380 | }.bind(this));
381 | }
382 |
383 | /**
384 | * 删除模块
385 | * @param moduleName(string) 模块名
386 | */
387 | AssetsManager.delete = function(moduleName){
388 | var storagePath = ((jsb.fileUtils ? jsb.fileUtils.getWritablePath() : '/') + LOCAL_STORAGE_FOLDER);
389 | var data = cc.sys.localStorage.getItem(LOCAL_STORAGE_KEY_PROJECT + "_" + moduleName);
390 | if (data) {
391 | try {
392 | var localManifest = JSON.parse(data);
393 |
394 | for (var key in localManifest.assets) {
395 | var filePath = cc.path.join(storagePath, key);
396 | if (jsb.fileUtils.isFileExist(filePath)){
397 | jsb.fileUtils.removeFile(filePath);
398 | }
399 | }
400 | }
401 | catch(e) {
402 | cc.error("【更新】删除模块时,本地版本清单数据解析错误");
403 | }
404 |
405 | cc.sys.localStorage.removeItem(LOCAL_STORAGE_KEY_PROJECT + "_" + moduleName);
406 | cc.sys.localStorage.removeItem(LOCAL_STORAGE_KEY_UPDATE_STATE + "_" + moduleName);
407 | }
408 | }
409 |
410 | game.asset = module.exports = AssetsManager;
411 |
--------------------------------------------------------------------------------