Cocos Creator熱更新

一冯勉,添加熱更新需要的文件

1. 在項目根目錄添加 version_generator.js 文件


? version_generator.js 內(nèi)容如下:

/**
 * 此模塊用于熱更新工程清單文件的生成
 */

var fs = require('fs');
var path = require('path');
var crypto = require('crypto');

var manifest = {
    //服務(wù)器上資源文件存放路徑(src,res的路徑)
    packageUrl: 'http://192.168.200.117:8000/XiaoMing/remote-assets/',
    //服務(wù)器上project.manifest路徑
    remoteManifestUrl: 'http://192.168.200.117:8000/XiaoMing/remote-assets/project.manifest',
    //服務(wù)器上version.manifest路徑
    remoteVersionUrl: 'http://192.168.200.117:8000/XiaoMing/remote-assets/version.manifest',
    version: '1.0.0',
    assets: {},
    searchPaths: []
};

//生成的manifest文件存放目錄
var dest = 'assets/';
//項目構(gòu)建后資源的目錄
var src = 'build/jsb-link/';

/**
 * node version_generator.js -v 1.0.0 -u http://your-server-address/tutorial-hot-update/remote-assets/ -s native/package/ -d assets/
 */
// Parse arguments
var i = 2;
while ( i < process.argv.length) {
    var arg = process.argv[i];

    switch (arg) {
    case '--url' :
    case '-u' :
        var url = process.argv[i+1];
        manifest.packageUrl = url;
        manifest.remoteManifestUrl = url + 'project.manifest';
        manifest.remoteVersionUrl = url + 'version.manifest';
        i += 2;
        break;
    case '--version' :
    case '-v' :
        manifest.version = process.argv[i+1];
        i += 2;
        break;
    case '--src' :
    case '-s' :
        src = process.argv[i+1];
        i += 2;
        break;
    case '--dest' :
    case '-d' :
        dest = process.argv[i+1];
        i += 2;
        break;
    default :
        i++;
        break;
    }
}


function readDir (dir, obj) {
    var stat = fs.statSync(dir);
    if (!stat.isDirectory()) {
        return;
    }
    var subpaths = fs.readdirSync(dir), subpath, size, md5, compressed, relative;
    for (var i = 0; i < subpaths.length; ++i) {
        if (subpaths[i][0] === '.') {
            continue;
        }
        subpath = path.join(dir, subpaths[i]);
        stat = fs.statSync(subpath);
        if (stat.isDirectory()) {
            readDir(subpath, obj);
        }
        else if (stat.isFile()) {
            // Size in Bytes
            size = stat['size'];
            md5 = crypto.createHash('md5').update(fs.readFileSync(subpath, 'binary')).digest('hex');
            compressed = path.extname(subpath).toLowerCase() === '.zip';

            relative = path.relative(src, subpath);
            relative = relative.replace(/\\/g, '/');
            relative = encodeURI(relative);
            obj[relative] = {
                'size' : size,
                'md5' : md5
            };
            if (compressed) {
                obj[relative].compressed = true;
            }
        }
    }
}

var mkdirSync = function (path) {
    try {
        fs.mkdirSync(path);
    } catch(e) {
        if ( e.code != 'EEXIST' ) throw e;
    }
}

// Iterate res and src folder
readDir(path.join(src, 'src'), manifest.assets);
readDir(path.join(src, 'res'), manifest.assets);

var destManifest = path.join(dest, 'project.manifest');
var destVersion = path.join(dest, 'version.manifest');

mkdirSync(dest);

fs.writeFile(destManifest, JSON.stringify(manifest), (err) => {
  if (err) throw err;
  console.log('Manifest successfully generated');
});

delete manifest.assets;
delete manifest.searchPaths;
fs.writeFile(destVersion, JSON.stringify(manifest), (err) => {
  if (err) throw err;
  console.log('Version successfully generated');
});

? 注意:以下幾個地方摔桦,你可能需要根據(jù)自己的需求修改调缨,本文 第三節(jié)第一點(diǎn) 會指出各個參數(shù)對應(yīng)的情況丧肴。

2. 添加熱更新組件穿挨,并掛在熱更新腳本

image.png

這里我簡單新建了一個helloWorld工程
添加了兩個button月弛,check用于檢測是否有更新,update用于更新資源科盛;
在canvas上掛載HotUpdate腳本帽衙,注意:這時ManifestUrl為空。
給button check添加點(diǎn)擊事件贞绵,綁定checkForUpdate()
給button update添加點(diǎn)擊事件厉萝,綁定hotUpdate()

HotUpdate.js
/**
 * 負(fù)責(zé)熱更新邏輯的組件
 */
cc.Class({
    extends: cc.Component,

    properties: {
        manifestUrl: cc.RawAsset,  //本地project.manifest資源清單文件
        _updating: false,
        _canRetry: false,
        _storagePath: ''
    },

    checkCb: function (event) {
        cc.log('Code: ' + event.getEventCode());
        switch (event.getEventCode()) {
            case jsb.EventAssetsManager.ERROR_NO_LOCAL_MANIFEST:
                cc.log("No local manifest file found, hot update skipped.");
                break;
            case jsb.EventAssetsManager.ERROR_DOWNLOAD_MANIFEST:
            case jsb.EventAssetsManager.ERROR_PARSE_MANIFEST:
                cc.log("Fail to download manifest file, hot update skipped.");
                break;
            case jsb.EventAssetsManager.ALREADY_UP_TO_DATE:
                cc.log("Already up to date with the latest remote version.");
                break;
            case jsb.EventAssetsManager.NEW_VERSION_FOUND:
                cc.log('New version found, please try to update.');
                this.hotUpdate();
                break;
            default:
                return;
        }

        cc.eventManager.removeListener(this._checkListener);
        this._checkListener = null;
        this._updating = false;
    },

    updateCb: function (event) {
        var needRestart = false;
        var failed = false;
        switch (event.getEventCode()) {
            case jsb.EventAssetsManager.ERROR_NO_LOCAL_MANIFEST:
                cc.log('No local manifest file found, hot update skipped...');
                failed = true;
                break;
            case jsb.EventAssetsManager.UPDATE_PROGRESSION:
                cc.log(event.getPercent());
                cc.log(event.getPercentByFile());
                cc.log(event.getDownloadedFiles() + ' / ' + event.getTotalFiles());
                cc.log(event.getDownloadedBytes() + ' / ' + event.getTotalBytes());

                var msg = event.getMessage();
                if (msg) {
                    cc.log('Updated file: ' + msg);
                }
                break;
            case jsb.EventAssetsManager.ERROR_DOWNLOAD_MANIFEST:
            case jsb.EventAssetsManager.ERROR_PARSE_MANIFEST:
                cc.log('Fail to download manifest file, hot update skipped.');
                failed = true;
                break;
            case jsb.EventAssetsManager.ALREADY_UP_TO_DATE:
                cc.log('Already up to date with the latest remote version.');
                failed = true;
                break;
            case jsb.EventAssetsManager.UPDATE_FINISHED:
                cc.log('Update finished. ' + event.getMessage());
                needRestart = true;
                break;
            case jsb.EventAssetsManager.UPDATE_FAILED:
                cc.log('Update failed. ' + event.getMessage());
                this._updating = false;
                this._canRetry = true;
                break;
            case jsb.EventAssetsManager.ERROR_UPDATING:
                cc.log('Asset update error: ' + event.getAssetId() + ', ' + event.getMessage());
                break;
            case jsb.EventAssetsManager.ERROR_DECOMPRESS:
                cc.log(event.getMessage());
                break;
            default:
                break;
        }

        if (failed) {
            cc.eventManager.removeListener(this._updateListener);
            this._updateListener = null;
            this._updating = false;
        }

        if (needRestart) {
            cc.eventManager.removeListener(this._updateListener);
            this._updateListener = null;
            // Prepend the manifest's search path
            var searchPaths = jsb.fileUtils.getSearchPaths();
            var newPaths = this._am.getLocalManifest().getSearchPaths();
            cc.log(JSON.stringify(newPaths));
            Array.prototype.unshift(searchPaths, newPaths);
            // This value will be retrieved and appended to the default search path during game startup,
            // please refer to samples/js-tests/main.js for detailed usage.
            // !!! Re-add the search paths in main.js is very important, otherwise, new scripts won't take effect.
            cc.sys.localStorage.setItem('HotUpdateSearchPaths', JSON.stringify(searchPaths));
            jsb.fileUtils.setSearchPaths(searchPaths);

            cc.audioEngine.stopAll();
            cc.game.restart();
        }
    },


    retry: function () {
        if (!this._updating && this._canRetry) {
            this._canRetry = false;

            cc.log('Retry failed Assets...');
            this._am.downloadFailedAssets();
        }
    },

    checkForUpdate: function () {
        cc.log("start checking...");
        if (this._updating) {
            cc.log('Checking or updating ...');
            return;
        }
        if (this._am.getState() === jsb.AssetsManager.State.UNINITED) {
            this._am.loadLocalManifest(this.manifestUrl);
            cc.log(this.manifestUrl);
        }
        if (!this._am.getLocalManifest() || !this._am.getLocalManifest().isLoaded()) {
            cc.log('Failed to load local manifest ...');
            return;
        }
        this._checkListener = new jsb.EventListenerAssetsManager(this._am, this.checkCb.bind(this));
        cc.eventManager.addListener(this._checkListener, 1);
        
        this._am.checkUpdate();

        this._updating = true;
    },

    hotUpdate: function () {
        if (this._am) {
            this._updateListener = new jsb.EventListenerAssetsManager(this._am, this.updateCb.bind(this));
            cc.eventManager.addListener(this._updateListener, 1);

            if (this._am.getState() === jsb.AssetsManager.State.UNINITED) {
                this._am.loadLocalManifest(this.manifestUrl);
            }

            this._failCount = 0;
            this._am.update();
            this._updating = true;
        }
    },

    show: function () {
        // if (this.updateUI.active === false) {
        //     this.updateUI.active = true;
        // }
    },

    // use this for initialization
    onLoad: function () {
        // Hot update is only available in Native build
        if (!cc.sys.isNative) {
            return;
        }
        this._storagePath = ((jsb.fileUtils ? jsb.fileUtils.getWritablePath() : '/') + 'xiaoming-remote-asset');
        cc.log('Storage path for remote asset : ' + this._storagePath);

        // Setup your own version compare handler, versionA and B is versions in string
        // if the return value greater than 0, versionA is greater than B,
        // if the return value equals 0, versionA equals to B,
        // if the return value smaller than 0, versionA is smaller than B.
        this.versionCompareHandle = function (versionA, versionB) {
            cc.log("JS Custom Version Compare: version A is " + versionA + ', version B is ' + versionB);
            var vA = versionA.split('.');
            var vB = versionB.split('.');
            for (var i = 0; i < vA.length; ++i) {
                var a = parseInt(vA[i]);
                var b = parseInt(vB[i] || 0);
                if (a === b) {
                    continue;
                }
                else {
                    return a - b;
                }
            }
            if (vB.length > vA.length) {
                return -1;
            }
            else {
                return 0;
            }
        };

        // Init with empty manifest url for testing custom manifest
        this._am = new jsb.AssetsManager('', this._storagePath, this.versionCompareHandle);
        if (!cc.sys.ENABLE_GC_FOR_NATIVE_OBJECTS) {
            this._am.retain();
        }
        // Setup the verification callback, but we don't have md5 check function yet, so only print some message
        // Return true if the verification passed, otherwise return false
        this._am.setVerifyCallback(function (path, asset) {
            // When asset is compressed, we don't need to check its md5, because zip file have been deleted.
            var compressed = asset.compressed;
            // Retrieve the correct md5 value.
            var expectedMD5 = asset.md5;
            // asset.path is relative path and path is absolute.
            var relativePath = asset.path;
            // The size of asset file, but this value could be absent.
            var size = asset.size;
            if (compressed) {
                cc.log("Verification passed : " + relativePath);
                return true;
            }
            else {
                cc.log("Verification passed : " + relativePath + ' (' + expectedMD5 + ')');
                return true;
            }
        });
        cc.log("Hot update is ready, please check or directly update.");

        if (cc.sys.os === cc.sys.OS_ANDROID) {
            // Some Android device may slow down the download process when concurrent tasks is too much.
            // The value may not be accurate, please do more test and find what's most suitable for your game.
            this._am.setMaxConcurrentTask(2);
            cc.log("Max concurrent tasks count have been limited to 2");
        }
        // this.checkUpdate();
    },

    onDestroy: function () {
        if (this._updateListener) {
            cc.eventManager.removeListener(this._updateListener);
            this._updateListener = null;
        }
        if (this._am && !cc.sys.ENABLE_GC_FOR_NATIVE_OBJECTS) {
            this._am.release();
        }
    }
});

3. 添加點(diǎn)擊事件
4. 構(gòu)建項目,生成原生資源文件

注意: 我們選擇的模板是 "default" ,發(fā)布路徑為 "./build" 谴垫,發(fā)布后的項目資源相對路徑為:build/jsb-default

5. 修改main.js(可省略)

?2018/08/23測試發(fā)現(xiàn)章母,構(gòu)建項目生成的main.js中,已包含判斷弹渔,不用修改胳施,也可以成功

根據(jù)官方文檔提示,每次構(gòu)建項目后肢专,都需要修改main.js,那我們就直接復(fù)制官方demo根目錄main.js的內(nèi)容覆蓋原有內(nèi)容焦辅。

? main.js 內(nèi)容如下:

main.js

(function () {

    if (window.cc && cc.sys.isNative) {
        var hotUpdateSearchPaths = cc.sys.localStorage.getItem('HotUpdateSearchPaths');
        if (hotUpdateSearchPaths) {
            jsb.fileUtils.setSearchPaths(JSON.parse(hotUpdateSearchPaths));
        }
    }

    'use strict';

    function boot () {

        var settings = window._CCSettings;
        window._CCSettings = undefined;

        if ( !settings.debug ) {
            // retrieve minified raw assets
            var rawAssets = settings.rawAssets;
            var assetTypes = settings.assetTypes;
            for (var mount in rawAssets) {
                var entries = rawAssets[mount];
                for (var uuid in entries) {
                    var entry = entries[uuid];
                    var type = entry[1];
                    if (typeof type === 'number') {
                        entry[1] = assetTypes[type];
                    }
                }
            }
        }

        // init engine
        var canvas;

        if (cc.sys.isBrowser) {
            canvas = document.getElementById('GameCanvas');
        }

        function setLoadingDisplay () {
            // Loading splash scene
            var splash = document.getElementById('splash');
            var progressBar = splash.querySelector('.progress-bar span');
            cc.loader.onProgress = function (completedCount, totalCount, item) {
                var percent = 100 * completedCount / totalCount;
                if (progressBar) {
                    progressBar.style.width = percent.toFixed(2) + '%';
                }
            };
            splash.style.display = 'block';
            progressBar.style.width = '0%';

            cc.director.once(cc.Director.EVENT_AFTER_SCENE_LAUNCH, function () {
                splash.style.display = 'none';
            });
        }

        var onStart = function () {
            cc.view.resizeWithBrowserSize(true);
            // UC browser on many android devices have performance issue with retina display
            if (cc.sys.os !== cc.sys.OS_ANDROID || cc.sys.browserType !== cc.sys.BROWSER_TYPE_UC) {
                cc.view.enableRetina(true);
            }
            //cc.view.setDesignResolutionSize(settings.designWidth, settings.designHeight, cc.ResolutionPolicy.SHOW_ALL);

            if (cc.sys.isBrowser) {
                setLoadingDisplay();
            }

            if (cc.sys.isMobile) {
                if (settings.orientation === 'landscape') {
                    cc.view.setOrientation(cc.macro.ORIENTATION_LANDSCAPE);
                }
                else if (settings.orientation === 'portrait') {
                    cc.view.setOrientation(cc.macro.ORIENTATION_PORTRAIT);
                }
                // qq, wechat, baidu
                cc.view.enableAutoFullScreen(
                    cc.sys.browserType !== cc.sys.BROWSER_TYPE_BAIDU &&
                    cc.sys.browserType !== cc.sys.BROWSER_TYPE_WECHAT &&
                    cc.sys.browserType !== cc.sys.BROWSER_TYPE_MOBILE_QQ
                );
            }

            // init assets
            cc.AssetLibrary.init({
                libraryPath: 'res/import',
                rawAssetsBase: 'res/raw-',
                rawAssets: settings.rawAssets,
                packedAssets: settings.packedAssets
            });

            var launchScene = settings.launchScene;

            // load scene
            if (cc.runtime) {
                cc.director.setRuntimeLaunchScene(launchScene);
            }
            cc.director.loadScene(launchScene, null,
                function () {
                    if (cc.sys.isBrowser) {
                        // show canvas
                        canvas.style.visibility = '';
                        var div = document.getElementById('GameDiv');
                        if (div) {
                            div.style.backgroundImage = '';
                        }
                    }
                    cc.loader.onProgress = null;

                    // play game
                    // cc.game.resume();

                    console.log('Success to load scene: ' + launchScene);
                }
            );
        };

        // jsList
        var jsList = settings.jsList;
        var bundledScript = settings.debug ? 'project.dev.js' : 'project.js';
        if (jsList) {
            jsList.push(bundledScript);
        }
        else {
            jsList = [bundledScript];
        }

        // anysdk scripts
        if (cc.sys.isNative && cc.sys.isMobile) {
            jsList = jsList.concat(['jsb_anysdk.js', 'jsb_anysdk_constants.js']);
        }

        jsList = jsList.map(function (x) { return 'src/' + x; });

        var option = {
            //width: width,
            //height: height,
            id: 'GameCanvas',
            scenes: settings.scenes,
            debugMode: settings.debug ? cc.DebugMode.INFO : cc.DebugMode.ERROR,
            showFPS: settings.debug,
            frameRate: 60,
            jsList: jsList,
            groupList: settings.groupList,
            collisionMatrix: settings.collisionMatrix,
            renderMode: 0
        };

        cc.game.run(option, onStart);
    }

    if (window.document) {
        var splash = document.getElementById('splash');
        splash.style.display = 'block';

        var cocos2d = document.createElement('script');
        cocos2d.async = true;
        cocos2d.src = window._CCSettings.debug ? 'cocos2d-js.js' : 'cocos2d-js-min.js';

        var engineLoaded = function () {
            document.body.removeChild(cocos2d);
            cocos2d.removeEventListener('load', engineLoaded, false);
            boot();
        };
        cocos2d.addEventListener('load', engineLoaded, false);
        document.body.appendChild(cocos2d);
    }
    else if (window.jsb) {
        require('src/settings.js');
        require('src/jsb_polyfill.js');

        boot();
    }

})();

二博杖,服務(wù)器搭建(僅局域網(wǎng)內(nèi)可訪問)

1. 編寫服務(wù)器腳本
server.py

import SimpleHTTPServer
import SocketServer

PORT = 8000

Handler = SimpleHTTPServer.SimpleHTTPRequestHandler

httpd = SocketServer.TCPServer(("", PORT), Handler)

print "serving at port", PORT
httpd.serve_forever()
2. 啟動服務(wù)

? 打開命令行
? 切換目錄到 server.py 所在目錄下
? 輸入下方命令,啟動服務(wù):

python -m server 8000


? 啟動完成后筷登,服務(wù)器的地址即: http://192.168.200.117:8000
? 對應(yīng)的根目錄即為server.py所在的目錄下剃根,我們在服務(wù)器根目錄下,新建HotUpdate文件夾前方,用于存儲新版本資源狈醉,通過網(wǎng)頁訪問如下:

? 詳細(xì)參見官方文檔給出的地址:https://docs.python.org/2/library/simplehttpserver.html

三,生成舊版清單文件

1. 修改version_generator.js

packageUrl:服務(wù)器存放資源文件(src res)的路徑
remoteManifestUrl:服務(wù)器存放資源清單文件(project.manifest)的路徑
remoteVersionUrl:服務(wù)器存放version.manifest的路徑
dest:要生成的manifest文件存放路徑
src:項目構(gòu)建后的資源目錄

2. 根據(jù)構(gòu)建后的資源目錄惠险,執(zhí)行version_generator.js苗傅,生成manifest清單文件

? 打開cmd,切換到當(dāng)前項目根目錄下班巩,執(zhí)行下方命令:

//官方給出的命令格式
>node version_generator.js -v 1.0.0 -u http://your-server-address/tutorial-hot-update/remote-assets/ -s native/package/ -d assets/

//我的命令
>node version_generator.js -v 1.0.0 -u http://192.168.200.117:8000/HotUpdate/ -s build/jsb-default/ -d assets

//由于我們version_generator文件中渣慕,都配置好了參數(shù)
//因此可以簡單調(diào)用以下命令即可
>node version_generator.js
  • -v: 指定 Manifest 文件的主版本號。
  • -u: 指定服務(wù)器遠(yuǎn)程包的地址抱慌,這個地址需要和最初發(fā)布版本中 Manifest 文件的遠(yuǎn)程包地址一致逊桦,否則無法檢測到更新。
  • -s: 本地原生打包版本的目錄相對路徑抑进。
  • -d: 保存 Manifest 文件的地址强经。

??生成的Manifest文件目錄,如下:

PS:如果version_generator.js中的配置都正確寺渗,特別是版本號匿情,可以直接執(zhí)行 node version_generator.js

3. 并綁定到熱更新腳本上

? 如果指定的Manifest文件生成的目錄不在assets下户秤,則需將project.manifest復(fù)制到assets目錄下

? 并將project.manifest綁定到HotUpdate.js熱更新腳本上
4. 打包舊版本

??構(gòu)建項目->編譯
??在真機(jī)上運(yùn)行build/jsb-default/simulator目錄下的apk
??1.0.0版本運(yùn)行如下:

三码秉,生成新版本

1. 更改代碼,更改version_generator.js中的版本號

? 修改logo圖片鸡号,1.0.1版本转砖,本地運(yùn)行結(jié)果,如下:
2. 構(gòu)建項目 && 重新生成資源清單文件

??構(gòu)建項目:此步驟和生成舊版本中一樣,這里就不截圖啦府蔗。
??重新生成資源清單文件:修改version_generator.js中的版本號后晋控,可以直接調(diào)用以下命令:

>node version_generator.js
3. 將manifest文件以及src,res拷貝到服務(wù)器
4. 運(yùn)行舊版本

? 運(yùn)行結(jié)果姓赤,點(diǎn)擊 檢測更新 后赡译,很快app重啟,logo變成了head.png不铆。
? 成功蝌焚!

由于這里檢測到新版本后,就開始自動更新誓斥。

你可以修改這部分邏輯只洒,檢測到有新版后,彈窗提示是否需要更新劳坑。

后面再繼續(xù)研究 大廳+子游戲的模式毕谴,以及不重啟加載子游戲方案...

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市距芬,隨后出現(xiàn)的幾起案子涝开,更是在濱河造成了極大的恐慌,老刑警劉巖框仔,帶你破解...
    沈念sama閱讀 211,194評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件舀武,死亡現(xiàn)場離奇詭異,居然都是意外死亡存和,警方通過查閱死者的電腦和手機(jī)奕剃,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,058評論 2 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來捐腿,“玉大人纵朋,你說我怎么就攤上這事∏研洌” “怎么了操软?”我有些...
    開封第一講書人閱讀 156,780評論 0 346
  • 文/不壞的土叔 我叫張陵,是天一觀的道長宪祥。 經(jīng)常有香客問我聂薪,道長,這世上最難降的妖魔是什么蝗羊? 我笑而不...
    開封第一講書人閱讀 56,388評論 1 283
  • 正文 為了忘掉前任藏澳,我火速辦了婚禮,結(jié)果婚禮上耀找,老公的妹妹穿的比我還像新娘翔悠。我一直安慰自己业崖,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,430評論 5 384
  • 文/花漫 我一把揭開白布蓄愁。 她就那樣靜靜地躺著双炕,像睡著了一般。 火紅的嫁衣襯著肌膚如雪撮抓。 梳的紋絲不亂的頭發(fā)上妇斤,一...
    開封第一講書人閱讀 49,764評論 1 290
  • 那天,我揣著相機(jī)與錄音丹拯,去河邊找鬼站超。 笑死,一個胖子當(dāng)著我的面吹牛咽笼,可吹牛的內(nèi)容都是我干的顷编。 我是一名探鬼主播,決...
    沈念sama閱讀 38,907評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼剑刑,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了双肤?” 一聲冷哼從身側(cè)響起施掏,我...
    開封第一講書人閱讀 37,679評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎茅糜,沒想到半個月后七芭,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,122評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡蔑赘,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,459評論 2 325
  • 正文 我和宋清朗相戀三年狸驳,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片缩赛。...
    茶點(diǎn)故事閱讀 38,605評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡耙箍,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出酥馍,到底是詐尸還是另有隱情辩昆,我是刑警寧澤,帶...
    沈念sama閱讀 34,270評論 4 329
  • 正文 年R本政府宣布旨袒,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏渡处。R本人自食惡果不足惜容劳,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,867評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望必孤。 院中可真熱鬧猾骡,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,734評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至襟企,卻和暖如春嘱么,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背顽悼。 一陣腳步聲響...
    開封第一講書人閱讀 31,961評論 1 265
  • 我被黑心中介騙來泰國打工曼振, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人蔚龙。 一個月前我還...
    沈念sama閱讀 46,297評論 2 360
  • 正文 我出身青樓冰评,卻偏偏與公主長得像,于是被迫代替她去往敵國和親木羹。 傳聞我的和親對象是個殘疾皇子甲雅,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,472評論 2 348

推薦閱讀更多精彩內(nèi)容

  • 1.下載version_generator.js文件放置項目內(nèi)用于生成manifest文件 2 .生成manife...
    Betta1閱讀 4,614評論 3 3
  • 1. 下載之前,項目菜單下沒有熱更新相關(guān)的東西坑填。 2. 點(diǎn)擊?擴(kuò)展--->擴(kuò)展商店 3. 點(diǎn)擊 下載 4. 下載完...
    陌上冰火閱讀 2,897評論 1 8
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理抛人,服務(wù)發(fā)現(xiàn),斷路器脐瑰,智...
    卡卡羅2017閱讀 134,629評論 18 139
  • Spring Boot 參考指南 介紹 轉(zhuǎn)載自:https://www.gitbook.com/book/qbgb...
    毛宇鵬閱讀 46,773評論 6 342
  • 正 負(fù) 1領(lǐng)導(dǎo) 自我為中心 2溝通 猶豫 3行動 壞脾氣 4策劃 單刀直入 5方向 固執(zhí) ...
    愛心幼兒園閱讀 95評論 0 0