├── .gitignore ├── LICENSE ├── README.md ├── package-lock.json ├── package.json └── src ├── game-start.js ├── new-res-loader.js └── start.ts /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /src/web-mobile 3 | /dist 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 fengyong 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2020.7.28,最近工作较忙,没有精力将版本支持到2.4,更多的支持会在2020年9月后更新。 2 | 3 | # cocos-to-playable-ad 4 | 将 cocos creator 构建出来的 web-mobile 项目打包为 playable-ad 项目,即单 html 文件。 5 | 一些说明: 6 | - 参考资料: 7 | - https://github.com/chongshengzhujue/playableFBCompile 8 | - https://www.ifeelgame.net/cocoscreator/%E4%BD%BF%E7%94%A8cocoscreator%E5%88%B6%E4%BD%9Cfacebook%E7%9A%84playable-ad/ 9 | - 其他网络资料 10 | - 改进部分: 11 | - 支持 cocos creator 到 2.1.3 12 | - 完善了核心算法描述(请参考 README.md) 13 | - 精简了使用流程,并将游戏项目与打包项目完全分开,游戏项目只需要提供 web-mobile 文件夹即可 14 | - 使用 node.js 完成,完善了开发环境描述,代码注释 15 | - 本项目不包括对图片,声音资源的压缩,需要自行压缩。 16 | - 本项目不包括使用 cocos creator 打包时的模块选择,需要自行筛选。 17 | - 如果使用过程中出现问题,请提交到项目下的 Issues,或者参与论坛讨论,https://forum.cocos.com/t/cocos-creator-web-mobile-playable-ad-html/84260 18 | 19 | ## 如何使用? 20 | - 开发环境: 21 | - macOs Cataline 10.15 22 | - node.js 12.9.0 23 | - cocos creator 2.1.3 24 | - Chrome 77 25 | - 输入:使用 cocos creator 构建出来的 web-mobile 项目文件夹。 26 | - 输出:index.html。 27 | - 使用方法: 28 | 1. 将构建出来的 web-mobile 整个文件夹整个放入项目的 src 目录下。此时目录为:src/web-mobile/... 29 | 2. 修改 src/web-mobile/main.js,注释掉 154 到 163 行,**目的是不在代码中载入 project.js**,而是在流程中载入。 30 | * **特别说明**:考虑不同版本下打出来的 main.js 代码位置可能会有差异,未必在相应的行,所以我把需要注释掉的代码补充进来。 31 | ```javascript 32 | // jsList 33 | var jsList = settings.jsList; 34 | 35 | // var bundledScript = settings.debug ? 'src/project.dev.js' : 'src/project.js'; 36 | // if (jsList) { 37 | // jsList = jsList.map(function (x) { 38 | // return 'src/' + x; 39 | // }); 40 | // jsList.push(bundledScript); 41 | // } 42 | // else { 43 | // jsList = [bundledScript]; 44 | // } 45 | ``` 46 | 3. 在根目录下执行 npm run build,会显示流程执行过程以及相应的消耗时间。 47 | 4. 点击输出文件 dist/index.html,检查在浏览器中是否显示正常。 48 | 49 | ## 核心算法 50 | - 将项目所依赖的资源读取并写入到 window.res,保存为 res.js。 51 | - 通过 cc.loader.addDownloadHandlers 修改资源载入方式,从 window.res 中载入资源,参考 new-res-loader.js。 52 | - 将 index.html 中所依赖的 css 和 js 文件,包括一些新的 js 文件写入到 html 文件本身。 53 | 54 | ## 依赖模块: 55 | - https://github.com/GoalSmashers/clean-css 压缩 css。 56 | - https://github.com/mishoo/UglifyJS2 压缩 js。 57 | - fs 模块,读写文件。 58 | - path 模块,处理路径相关(其实在项目中只用来获取文件的后缀名)。 59 | - typescript 相关,本项目使用 ts 编写,使用 ts-node 执行。 60 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cocos-to-playable-ad", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@types/clean-css": { 8 | "version": "4.2.1", 9 | "resolved": "https://registry.npmjs.org/@types/clean-css/-/clean-css-4.2.1.tgz", 10 | "integrity": "sha512-A1HQhQ0hkvqqByJMgg+Wiv9p9XdoYEzuwm11SVo1mX2/4PSdhjcrUlilJQoqLscIheC51t1D5g+EFWCXZ2VTQQ==", 11 | "dev": true, 12 | "requires": { 13 | "@types/node": "*" 14 | } 15 | }, 16 | "@types/node": { 17 | "version": "12.7.8", 18 | "resolved": "https://registry.npmjs.org/@types/node/-/node-12.7.8.tgz", 19 | "integrity": "sha512-FMdVn84tJJdV+xe+53sYiZS4R5yn1mAIxfj+DVoNiQjTYz1+OYmjwEZr1ev9nU0axXwda0QDbYl06QHanRVH3A==", 20 | "dev": true 21 | }, 22 | "@types/uglify-js": { 23 | "version": "3.0.4", 24 | "resolved": "https://registry.npmjs.org/@types/uglify-js/-/uglify-js-3.0.4.tgz", 25 | "integrity": "sha512-SudIN9TRJ+v8g5pTG8RRCqfqTMNqgWCKKd3vtynhGzkIIjxaicNAMuY5TRadJ6tzDu3Dotf3ngaMILtmOdmWEQ==", 26 | "dev": true, 27 | "requires": { 28 | "source-map": "^0.6.1" 29 | } 30 | }, 31 | "arg": { 32 | "version": "4.1.1", 33 | "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.1.tgz", 34 | "integrity": "sha512-SlmP3fEA88MBv0PypnXZ8ZfJhwmDeIE3SP71j37AiXQBXYosPV0x6uISAaHYSlSVhmHOVkomen0tbGk6Anlebw==", 35 | "dev": true 36 | }, 37 | "buffer-from": { 38 | "version": "1.1.1", 39 | "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", 40 | "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", 41 | "dev": true 42 | }, 43 | "clean-css": { 44 | "version": "4.2.1", 45 | "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.1.tgz", 46 | "integrity": "sha512-4ZxI6dy4lrY6FHzfiy1aEOXgu4LIsW2MhwG0VBKdcoGoH/XLFgaHSdLTGr4O8Be6A8r3MOphEiI8Gc1n0ecf3g==", 47 | "dev": true, 48 | "requires": { 49 | "source-map": "~0.6.0" 50 | } 51 | }, 52 | "commander": { 53 | "version": "2.20.1", 54 | "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.1.tgz", 55 | "integrity": "sha512-cCuLsMhJeWQ/ZpsFTbE765kvVfoeSddc4nU3up4fV+fDBcfUXnbITJ+JzhkdjzOqhURjZgujxaioam4RM9yGUg==", 56 | "dev": true 57 | }, 58 | "diff": { 59 | "version": "4.0.1", 60 | "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.1.tgz", 61 | "integrity": "sha512-s2+XdvhPCOF01LRQBC8hf4vhbVmI2CGS5aZnxLJlT5FtdhPCDFq80q++zK2KlrVorVDdL5BOGZ/VfLrVtYNF+Q==", 62 | "dev": true 63 | }, 64 | "make-error": { 65 | "version": "1.3.5", 66 | "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.5.tgz", 67 | "integrity": "sha512-c3sIjNUow0+8swNwVpqoH4YCShKNFkMaw6oH1mNS2haDZQqkeZFlHS3dhoeEbKKmJB4vXpJucU6oH75aDYeE9g==", 68 | "dev": true 69 | }, 70 | "source-map": { 71 | "version": "0.6.1", 72 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", 73 | "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", 74 | "dev": true 75 | }, 76 | "source-map-support": { 77 | "version": "0.5.13", 78 | "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", 79 | "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", 80 | "dev": true, 81 | "requires": { 82 | "buffer-from": "^1.0.0", 83 | "source-map": "^0.6.0" 84 | } 85 | }, 86 | "ts-node": { 87 | "version": "8.4.1", 88 | "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-8.4.1.tgz", 89 | "integrity": "sha512-5LpRN+mTiCs7lI5EtbXmF/HfMeCjzt7DH9CZwtkr6SywStrNQC723wG+aOWFiLNn7zT3kD/RnFqi3ZUfr4l5Qw==", 90 | "dev": true, 91 | "requires": { 92 | "arg": "^4.1.0", 93 | "diff": "^4.0.1", 94 | "make-error": "^1.1.1", 95 | "source-map-support": "^0.5.6", 96 | "yn": "^3.0.0" 97 | } 98 | }, 99 | "typescript": { 100 | "version": "3.6.3", 101 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.6.3.tgz", 102 | "integrity": "sha512-N7bceJL1CtRQ2RiG0AQME13ksR7DiuQh/QehubYcghzv20tnh+MQnQIuJddTmsbqYj+dztchykemz0zFzlvdQw==", 103 | "dev": true 104 | }, 105 | "uglify-js": { 106 | "version": "3.6.0", 107 | "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.6.0.tgz", 108 | "integrity": "sha512-W+jrUHJr3DXKhrsS7NUVxn3zqMOFn0hL/Ei6v0anCIMoKC93TjcflTagwIHLW7SfMFfiQuktQyFVCFHGUE0+yg==", 109 | "dev": true, 110 | "requires": { 111 | "commander": "~2.20.0", 112 | "source-map": "~0.6.1" 113 | } 114 | }, 115 | "yn": { 116 | "version": "3.1.1", 117 | "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", 118 | "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", 119 | "dev": true 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cocos-to-playable-ad", 3 | "version": "1.0.0", 4 | "description": "将 cocos creator 构建出来的 web-mobile 项目打包为 playable-ad 项目,即单 html 文件。", 5 | "main": "start.ts", 6 | "dependencies": {}, 7 | "devDependencies": { 8 | "@types/clean-css": "^4.2.1", 9 | "@types/node": "^12.7.8", 10 | "@types/uglify-js": "^3.0.4", 11 | "clean-css": "^4.2.1", 12 | "ts-node": "^8.4.1", 13 | "typescript": "^3.6.3", 14 | "uglify-js": "^3.6.0" 15 | }, 16 | "scripts": { 17 | "build": "ts-node ./src/start.ts" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/fkworld/cocos-to-playable-ad.git" 22 | }, 23 | "author": "", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/fkworld/cocos-to-playable-ad/issues" 27 | }, 28 | "homepage": "https://github.com/fkworld/cocos-to-playable-ad#readme" 29 | } 30 | -------------------------------------------------------------------------------- /src/game-start.js: -------------------------------------------------------------------------------- 1 | // 游戏启动脚本 2 | 3 | window.boot() 4 | -------------------------------------------------------------------------------- /src/new-res-loader.js: -------------------------------------------------------------------------------- 1 | // 新的资源载入方式脚本 2 | 3 | /** 官网范例,反正看不懂 4 | * - https://developer.mozilla.org/zh-CN/docs/Web/API/WindowBase64/Base64_encoding_and_decoding#Solution_1_%E2%80%93_JavaScript's_UTF-16_%3E_base64 5 | */ 6 | function b64ToUint6(nChr) { 7 | return nChr > 64 && nChr < 91 8 | ? nChr - 65 : nChr > 96 && nChr < 123 9 | ? nChr - 71 : nChr > 47 && nChr < 58 10 | ? nChr + 4 : nChr === 43 11 | ? 62 : nChr === 47 12 | ? 63 : 0 13 | } 14 | 15 | /** 官网范例+1,看不懂+1,作用是将base64编码的字符串转为ArrayBuffer */ 16 | function base64DecToArr(sBase64, nBlockSize) { 17 | var sB64Enc = sBase64.replace(/[^A-Za-z0-9\+\/]/g, ""), nInLen = sB64Enc.length 18 | var nOutLen = nBlockSize ? Math.ceil((nInLen * 3 + 1 >>> 2) / nBlockSize) * nBlockSize : nInLen * 3 + 1 >>> 2 19 | var aBytes = new Uint8Array(nOutLen) 20 | for (var nMod3, nMod4, nUint24 = 0, nOutIdx = 0, nInIdx = 0; nInIdx < nInLen; nInIdx++) { 21 | nMod4 = nInIdx & 3 22 | nUint24 |= b64ToUint6(sB64Enc.charCodeAt(nInIdx)) << 18 - 6 * nMod4 23 | if (nMod4 === 3 || nInLen - nInIdx === 1) { 24 | for (nMod3 = 0; nMod3 < 3 && nOutIdx < nOutLen; nMod3++ , nOutIdx++) { 25 | aBytes[nOutIdx] = nUint24 >>> (16 >>> nMod3 & 24) & 255; 26 | } 27 | nUint24 = 0 28 | } 29 | } 30 | return aBytes 31 | } 32 | 33 | /** 34 | * 修改部分资源的载入方式,可以根据项目中实际用到的资源进行修改 35 | * - [注意] window.res 是自己定义的,名称可以修改 36 | */ 37 | cc.loader.addDownloadHandlers({ 38 | json: function (item, callback) { 39 | callback(null, window.res[item.url]) 40 | }, 41 | plist: function (item, callback) { 42 | callback(null, window.res[item.url]) 43 | }, 44 | png: function (item, callback) { 45 | var img = new Image() 46 | img.src = "data:image/png;base64," + window.res[item.url] // 注意需要给base64编码添加前缀 47 | callback(null, img) 48 | }, 49 | jpg: function (item, callback) { 50 | var img = new Image() 51 | img.src = "data:image/jpeg;base64," + window.res[item.url] 52 | callback(null, img) 53 | }, 54 | webp: function (item, callback) { 55 | var img = new Image() 56 | img.src = "data:image/webp;base64," + window.res[item.url] 57 | callback(null, img) 58 | }, 59 | mp3: function (item, callback) { 60 | // 只支持以webAudio形式播放的声音 61 | // 将base64编码的声音文件转化为ArrayBuffer 62 | cc.sys.__audioSupport.context.decodeAudioData( 63 | base64DecToArr(window.res[item.url]).buffer, 64 | // success 65 | function (buffer) { 66 | callback(null, buffer) 67 | }, 68 | // fail 69 | function (buffer) { 70 | callback(new Error("mp3-res-fail"), null) 71 | } 72 | ) 73 | }, 74 | }) 75 | -------------------------------------------------------------------------------- /src/start.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs" 2 | import * as path from "path" 3 | import * as uglify from "uglify-js" 4 | import CleanCSS = require("clean-css") 5 | 6 | export namespace X { 7 | 8 | /** 一些配置参数 9 | * - [注意] 路径问题.start脚本与web-mobile同层级,因此相对路径需要带上web-mobile;cocos在调用资源时没有web-mobile,需要在最后去掉 10 | */ 11 | const C = { 12 | BASE_PATH: "src/web-mobile", // web-mobile包基础路径 13 | RES_PATH: "src/web-mobile/res", // web-mobile包下的res路径 14 | RES_BASE64_EXTNAME_SET: new Set([ // 需要使用base64编码的资源后缀(根据项目自行扩充) 15 | ".png", ".jpg", ".webp", ".mp3", 16 | ]), 17 | OUTPUT_RES_JS: "dist/res.js", // 输出文件res.js 18 | OUTPUT_INDEX_HTML: "dist/index.html", // 输出文件index.html的路径 19 | INPUT_HTML_FILE: "src/web-mobile/index.html", 20 | INPUT_CSS_FILES: [ 21 | "src/web-mobile/style-mobile.css" 22 | ], 23 | INPUT_JS_FILES: [ 24 | "dist/res.js", // 注意这里先输出再输入 25 | "src/web-mobile/cocos2d-js-min.js", 26 | "src/web-mobile/main.js", 27 | "src/web-mobile/src/settings.js", 28 | "src/web-mobile/src/project.js", 29 | "src/new-res-loader.js", 30 | "src/game-start.js", 31 | ], 32 | } 33 | 34 | /** 35 | * 读取文件内容 36 | * - 特定后缀返回base64编码后字符串,否则直接返回文件内容字符串 37 | * @param filepath 38 | */ 39 | function get_file_content(filepath: string): string { 40 | let file = fs.readFileSync(filepath) 41 | return C.RES_BASE64_EXTNAME_SET.has(path.extname(filepath)) ? file.toString("base64") : file.toString() 42 | } 43 | 44 | /** 45 | * 获取路径下的所有子文件路径(深度遍历) 46 | * @param filepath 47 | */ 48 | function get_all_child_file(filepath: string): string[] { 49 | let children = [filepath] 50 | for (; ;) { 51 | // 如果都是file类型的,则跳出循环 52 | if (children.every(v => fs.statSync(v).isFile())) { break } 53 | // 如果至少有1个directroy类型,则删除这一项,并加入其子项 54 | children.forEach((child, i) => { 55 | if (fs.statSync(child).isDirectory()) { 56 | delete children[i] 57 | let child_children = fs.readdirSync(child).map(v => `${child}/${v}`) 58 | children.push(...child_children) 59 | } 60 | }) 61 | } 62 | return children 63 | } 64 | 65 | /** 66 | * 将所有res路径下的资源转化为res.js 67 | * - 存储方式为:res-url(注意是相对的),res文件内容字符串或编码 68 | */ 69 | function write_resjs() { 70 | // 读取并写入到一个对象中 71 | let res_object = {} 72 | get_all_child_file(C.RES_PATH).forEach(path => { 73 | // 注意,存储时删除BASE_PATH前置 74 | let store_path = path.replace(new RegExp(`^${C.BASE_PATH}/`), "") 75 | res_object[store_path] = get_file_content(path) 76 | }) 77 | // 写入文件 78 | fs.writeFileSync(C.OUTPUT_RES_JS, `window.res=${JSON.stringify(res_object)}`) 79 | } 80 | 81 | /** 将js文件转化为html文件内容(包括压缩过程) */ 82 | function get_html_code_by_js_file(js_filepath: string): string { 83 | let js = get_file_content(js_filepath) 84 | let min_js = uglify.minify(js).code 85 | return `` 86 | } 87 | 88 | /** 将css文件转化为html文件内容(包括压缩过程) */ 89 | function get_html_code_by_css_file(css_filepath: string): string { 90 | let css = get_file_content(css_filepath) 91 | let min_css = new CleanCSS().minify(css).styles 92 | return `` 93 | } 94 | 95 | /** 执行任务 */ 96 | export function do_task() { 97 | // 前置:将res资源写成res.js 98 | console.time("写入res.js") 99 | write_resjs() 100 | console.timeEnd("写入res.js") 101 | 102 | // 清理html 103 | console.time("清理html") 104 | let html = get_file_content(C.INPUT_HTML_FILE) 105 | html = html.replace(//gs, "") 106 | html = html.replace(//gs, "") 107 | console.timeEnd("清理html") 108 | 109 | // 写入css 110 | console.log("写入所有css文件") 111 | C.INPUT_CSS_FILES.forEach(v => { 112 | console.time(`---${path.basename(v)}`) 113 | html = html.replace(/<\/head>/, `${get_html_code_by_css_file(v)}\n`) 114 | console.timeEnd(`---${path.basename(v)}`) 115 | }) 116 | 117 | // 写入js 118 | console.log("写入所有js到html") 119 | C.INPUT_JS_FILES.forEach(v => { 120 | console.time(`---${path.basename(v)}`) 121 | html = html.replace("", () => `${get_html_code_by_js_file(v)}\n`) 122 | console.timeEnd(`---${path.basename(v)}`) 123 | }) 124 | 125 | // 写入文件并提示成功 126 | console.time("输出html文件") 127 | fs.writeFileSync(C.OUTPUT_INDEX_HTML, html) 128 | console.timeEnd("输出html文件") 129 | } 130 | } 131 | 132 | X.do_task() 133 | --------------------------------------------------------------------------------