僅需簡單 5 步,給你的 Golang 程序添加 GUI (使用 Electron )

創(chuàng)建一個 Golang app 是一件簡單又輕松的事情煮嫌,但是有時候你想給你的應(yīng)用錦上添花:創(chuàng)建一個 GUI!
在本篇文章中抱虐,我將通過使用 astilectron 工具中的 bootstrap 以及 bundler 給一個簡單的 Golang 程序添加 GUI昌阿。
我們的帶有 GUI 的 Golang app 能夠打開一個文件夾并且展示其中的內(nèi)容。
你可以在這里找到完成后的 代碼

image

第一步:組織項目結(jié)構(gòu)

文件夾結(jié)構(gòu)如下:

|--+ resources
  |--+ app
     |--+ static
        |--+ css
           |--+ base.css
        |--+ js
           |--+ index.js
        |--+ lib
           |--+ ... (all the css/js libs we need)
     |--+ index.html
  |--+ icon.icns
  |--+ icon.ico
  |--+ icon.png
|--+ bundler.json
|--+ main.go
|--+ message.go

你將看到,我們需要3種不同格式的圖標(biāo)以完成不同平臺的編譯:
.icns 用于 darwin 平臺
.ico 用于 windows 平臺
.png 用于 linux 平臺
我們將使用以下CSS/JS庫

第二步:搭建基礎(chǔ)架構(gòu)

Go

首先我們需要在 main.go 中導(dǎo)入 astilectron 的 bootstrap 源碼包 :

package main
import (
   "flag"
   "github.com/asticode/go-astilectron"
   "github.com/asticode/go-astilectron-bootstrap"
   "github.com/asticode/go-astilog"
   "github.com/pkg/errors"
)
// Vars
var (
   AppName string
   BuiltAt string
   debug   = flag.Bool("d", false, "enables the debug mode")
   w       *astilectron.Window
)
func main() {
   // Init
   flag.Parse()
   astilog.FlagInit()
   // Run bootstrap
   astilog.Debugf("Running app built at %s", BuiltAt)
   if err := bootstrap.Run(bootstrap.Options{
       AstilectronOptions: astilectron.Options{
           AppName:            AppName,
           AppIconDarwinPath:  "resources/icon.icns",
           AppIconDefaultPath: "resources/icon.png",
       },
       Debug:    *debug,
       Homepage: "index.html",
       MenuOptions: []*astilectron.MenuItemOptions{{
           Label: astilectron.PtrStr("File"),
           SubMenu: []*astilectron.MenuItemOptions{
               {Label: astilectron.PtrStr("About")},
               {Role: astilectron.MenuItemRoleClose},
           },
       }},
       OnWait: func(_ *astilectron.Astilectron, iw *astilectron.Window, _ *astilectron.Menu, _ *astilectron.Tray, _ *astilectron.Menu) error {
           w = iw
           return nil
       },
       WindowOptions: &astilectron.WindowOptions{
           BackgroundColor: astilectron.PtrStr("#333"),
           Center:          astilectron.PtrBool(true),
           Height:          astilectron.PtrInt(700),
           Width:           astilectron.PtrInt(700),
       },
   }); err != nil {
       astilog.Fatal(errors.Wrap(err, "running bootstrap failed"))
   }
}

2個全局變量 AppNameBuiltAt 將會通過 bundler 打包自動添加進(jìn)去懦冰。
隨后我們將發(fā)現(xiàn)我們的主頁變成了 index.html 灶轰,我們將有一個含有2個項目( aboutclose )的菜單并且會出現(xiàn)一個 700x700 , 中心對齊的#333 背景色的窗口刷钢。
我們要在 go 上添加 debug 選項笋颤,因為我們需要使用 HTML/JS/CSS 調(diào)試工具。
最后我們將指向 astilectron.Window 的指針存入全局變量 w内地,以備后續(xù)在使用 OnWait 選項時伴澄,它包含一個在窗口、菜單及其他所有對象被創(chuàng)建時立即執(zhí)行的回調(diào)函數(shù)阱缓。

HTML

現(xiàn)在我們需要在 resources/app/index.html 中創(chuàng)建我們的 HTML 主頁:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <link rel="stylesheet" href="static/css/base.css"/>
    <link rel="stylesheet" href="static/lib/astiloader/astiloader.css">
    <link rel="stylesheet" href="static/lib/astimodaler/astimodaler.css">
    <link rel="stylesheet" href="static/lib/astinotifier/astinotifier.css">
    <link rel="stylesheet" href="static/lib/font-awesome-4.7.0/css/font-awesome.min.css">
</head>
<body>
    <div class="left" id="dirs"></div>
    <div class="right">
        <div class="title"><span id="path"></span></div>
        <div class="panel"><span class="stat" id="files_count"></span> file(s)</div>
        <div class="panel"><span class="stat" id="files_size"></span> of file(s)</div>
        <div class="panel" id="files_panel">
            <div class="chart_title">Files repartition</div>
            <div id="files"></div>
        </div>
    </div>
    <script src="static/js/index.js"></script>
    <script src="static/lib/astiloader/astiloader.js"></script>
    <script src="static/lib/astimodaler/astimodaler.js"></script>
    <script src="static/lib/astinotifier/astinotifier.js"></script>
    <script src="static/lib/chart/chart.min.js"></script>
    <script type="text/javascript">
        index.init();
    </script>
</body>
</html>

這里沒什么特殊的地方非凌,我們聲明我們的 cssjs 文件,我們設(shè)置html文件結(jié)構(gòu)并且我們需要確保我們的 js 腳本通過 index.init() 進(jìn)行了初始化

CSS

現(xiàn)在我們需要在 resources/app/static/css/base.css 文件中創(chuàng)建我們的 CSS:

 * {
    box-sizing:  border-box;
}
 html, body {
    background-color: #fff;
    color: #333;
    height: 100%;
    margin: 0;
    width: 100%;
}
 .left {
    background-color: #333;
    color: #fff;
    float: left;
    height: 100%;
    overflow: auto;
    padding: 15px;
    width: 40%;
}
 .dir {
    cursor: pointer;
    padding: 3px;
}
 .dir .fa {
    margin-right: 5px;
}
 .right {
    float: right;
    height: 100%;
    overflow: auto;
    padding: 15px;
    width: 60%;
}
 .title {
    font-size: 1.5em;
    text-align: center;
    word-wrap: break-word;
}
 .panel {
    background-color: #f1f1f1;
    border: solid 1px #e1e1e1;
    border-radius: 4px;
    margin-top: 15px;
    padding: 15px;
    text-align: center;
}
 .stat {
    font-weight: bold;
}
 .chart_title {
    margin-bottom: 5px;
}

JS

然后我們在 resources/app/static/js/index.js 中創(chuàng)建 JS :

let index = {
    init: function() {
        // Init
        asticode.loader.init();
        asticode.modaler.init();
        asticode.notifier.init();
    }
};

通過 init 方法正確的將庫初始化

第三步:建立起 GO 與 Javascript 間的通信

萬事俱備荆针,只欠東風(fēng):我們需要將 GO 與 Javascript 建立起通信

Javascript 通信 GO

為了讓 Javascript 與 Go 進(jìn)行通信敞嗡,首先從 Javascript 向 GO 發(fā)送一條消息,并且在 GO 接受到消息后執(zhí)行回調(diào)函數(shù):

// This will wait for the astilectron namespace to be ready
document.addEventListener('astilectron-ready', function() {
    // This will send a message to GO
    astilectron.sendMessage({name: "event.name", payload: "hello"}, function(message) {
        console.log("received " + message.payload)
    });
})

同時我們在 GO 中監(jiān)聽來自 Javascript 的消息航背,并且通過 bootstrap 的 MessageHandler 給 Javascript 發(fā)送消息:

func main() {
    bootstrap.Run(bootstrap.Options{
        MessageHandler: handleMessages, 
    })
}
 // handleMessages handles messages
func handleMessages(_ *astilectron.Window, m bootstrap.MessageIn) (payload interface{}, err error) {
    switch m.Name {
    case "event.name":
        // Unmarshal payload
        var s string
        if err = json.Unmarshal(m.Payload, &path); err != nil {
            payload = err.Error()
            return
        }
        payload = s + " world"
    }
    return
}

這是一個簡單的例子將在js的輸出中打印出 received hello world 喉悴。
在這種情形中,我們需要更多的邏輯因為我們想要允許打開一個文件夾并且展示其中的內(nèi)容玖媚。
因此我們將下面的代碼加入到 resources/app/static/js/index.js 中:

 let index = {
    addFolder(name, path) {
        let div = document.createElement("div");
        div.className = "dir";
        div.onclick = function() { index.explore(path) };
        div.innerHTML = `<i class="fa fa-folder"></i><span>` + name + `</span>`;
        document.getElementById("dirs").appendChild(div)
    },
    init: function() {
        // Wait for astilectron to be ready
        document.addEventListener('astilectron-ready', function() {
            // Explore default path
            index.explore();
        })
    },
    explore: function(path) {
        // Create message
        let message = {"name": "explore"};
        if (typeof path !== "undefined") {
            message.payload = path
        }
         // Send message
        asticode.loader.show();
        astilectron.sendMessage(message, function(message) {
            // Init
            asticode.loader.hide();
             // Check error
            if (message.name === "error") {
                asticode.notifier.error(message.payload);
                return
            }
             // Process path
            document.getElementById("path").innerHTML = message.payload.path;
             // Process dirs
            document.getElementById("dirs").innerHTML = ""
            for (let i = 0; i < message.payload.dirs.length; i++) {
                index.addFolder(message.payload.dirs[i].name, message.payload.dirs[i].path);
            }
             // Process files
            document.getElementById("files_count").innerHTML = message.payload.files_count;
            document.getElementById("files_size").innerHTML = message.payload.files_size;
            document.getElementById("files").innerHTML = "";
            if (typeof message.payload.files !== "undefined") {
                document.getElementById("files_panel").style.display = "block";
                let canvas = document.createElement("canvas");
                document.getElementById("files").append(canvas);
                new Chart(canvas, message.payload.files);
            } else {
                document.getElementById("files_panel").style.display = "none";
            }
        })
    }
};

一旦 Javascript 的 astilectron 命名空間準(zhǔn)備好粥惧,它執(zhí)行新的 explore 方法,該方法會給 GO 發(fā)送一條消息最盅,接收返回的信息,并且更新相應(yīng)的 HTML 起惕。

然后我們將下面代碼加入到 message.go 中:

package main
 import (
    "encoding/json"
    "io/ioutil"
    "os"
    "os/user"
    "path/filepath"
    "sort"
    "strconv"
    "github.com/asticode/go-astichartjs"
    "github.com/asticode/go-astilectron"
    "github.com/asticode/go-astilectron-bootstrap"
)
 // handleMessages handles messages
func handleMessages(_ *astilectron.Window, m bootstrap.MessageIn) (payload interface{}, err error) {
    switch m.Name {
    case "explore":
        // Unmarshal payload
        var path string
        if len(m.Payload) > 0 {
            // Unmarshal payload
            if err = json.Unmarshal(m.Payload, &path); err != nil {
                payload = err.Error()
                return
            }
        }
        // Explore
        if payload, err = explore(path); err != nil {
            payload = err.Error()
            return
        }
    }
    return
}
 // Exploration represents the results of an exploration
type Exploration struct {
    Dirs       []Dir              `json:"dirs"`
    Files      *astichartjs.Chart `json:"files,omitempty"`
    FilesCount int                `json:"files_count"`
    FilesSize  string             `json:"files_size"`
    Path       string             `json:"path"`
}
 // PayloadDir represents a dir payload
type Dir struct {
    Name string `json:"name"`
    Path string `json:"path"`
}
 // explore explores a path.
// If path is empty, it explores the user's home directory
func explore(path string) (e Exploration, err error) {
    // If no path is provided, use the user's home dir
    if len(path) == 0 {
        var u *user.User
        if u, err = user.Current(); err != nil {
            return
        }
        path = u.HomeDir
    }
    // Read dir
    var files []os.FileInfo
    if files, err = ioutil.ReadDir(path); err != nil {
        return
    }
    // Init exploration
    e = Exploration{
        Dirs: []Dir{},
        Path: path,
    }
    // Add previous dir
    if filepath.Dir(path) != path {
        e.Dirs = append(e.Dirs, Dir{
            Name: "..",
            Path: filepath.Dir(path),
        })
    }
    // Loop through files
    var sizes []int
    var sizesMap = make(map[int][]string)
    var filesSize int64
    for _, f := range files {
        if f.IsDir() {
            e.Dirs = append(e.Dirs, Dir{
                Name: f.Name(),
                Path: filepath.Join(path, f.Name()),
            })
        } else {
            var s = int(f.Size())
            sizes = append(sizes, s)
            sizesMap[s] = append(sizesMap[s], f.Name())
            e.FilesCount++
            filesSize += f.Size()
        }
    }
    // Prepare files size
    if filesSize < 1e3 {
        e.FilesSize = strconv.Itoa(int(filesSize)) + "b"
    } else if filesSize < 1e6 {
        e.FilesSize = strconv.FormatFloat(float64(filesSize)/float64(1024), 'f', 0, 64) + "kb"
    } else if filesSize < 1e9 {
        e.FilesSize = strconv.FormatFloat(float64(filesSize)/float64(1024*1024), 'f', 0, 64) + "Mb"
    } else {
        e.FilesSize = strconv.FormatFloat(float64(filesSize)/float64(1024*1024*1024), 'f', 0, 64) + "Gb"
    }
    // Prepare files chart
    sort.Ints(sizes)
    if len(sizes) > 0 {
        e.Files = &astichartjs.Chart{
            Data: &astichartjs.Data{Datasets: []astichartjs.Dataset{{
                BackgroundColor: []string{
                    astichartjs.ChartBackgroundColorYellow,
                    astichartjs.ChartBackgroundColorGreen,
                    astichartjs.ChartBackgroundColorRed,
                    astichartjs.ChartBackgroundColorBlue,
                    astichartjs.ChartBackgroundColorPurple,
                },
                BorderColor: []string{
                    astichartjs.ChartBorderColorYellow,
                    astichartjs.ChartBorderColorGreen,
                    astichartjs.ChartBorderColorRed,
                    astichartjs.ChartBorderColorBlue,
                    astichartjs.ChartBorderColorPurple,
                },
            }}},
            Type: astichartjs.ChartTypePie,
        }
        var sizeOther int
        for i := len(sizes) - 1; i >= 0; i-- {
            for _, l := range sizesMap[sizes[i]] {
                if len(e.Files.Data.Labels) < 4 {
                    e.Files.Data.Datasets[0].Data = append(e.Files.Data.Datasets[0].Data, sizes[i])
                    e.Files.Data.Labels = append(e.Files.Data.Labels, l)
                } else {
                    sizeOther += sizes[i]
                }
            }
        }
        if sizeOther > 0 {
            e.Files.Data.Datasets[0].Data = append(e.Files.Data.Datasets[0].Data, sizeOther)
            e.Files.Data.Labels = append(e.Files.Data.Labels, "other")
        }
    }
    return
}

在接收到正確的信息時涡贱,它將執(zhí)行新的 explore 方法,并返回關(guān)于目錄的有價值的信息惹想。

建立從 Go 向 Javascript 通信

為了建立從 GO 向 Javascript 的通信问词,我們首先需要從 GO 中向 Javascript 發(fā)送一條消息并且在 Javascript 收到消息后執(zhí)行回調(diào)。

// This will send a message and execute a callback
// Callbacks are optional
bootstrap.SendMessage(w, "event.name", "hello", func(m *bootstrap.MessageIn) {
    // Unmarshal payload
    var s string
    json.Unmarshal(m.Payload, &s)
     // Process message
    log.Infof("received %s", s)
})

同時我們在 Javascript 中監(jiān)聽來自 GO 的消息并發(fā)送一個選項消息給 GO:

// This will wait for the astilectron namespace to be ready
document.addEventListener('astilectron-ready', function() {
   // This will listen to messages sent by GO
   astilectron.onMessage(function(message) {
       // Process message
       if (message.name === "event.name") {
           return {payload: message.message + " world"};
       }
   });
})

這個簡單的例子將在 GO 的輸出中打印 received hello world 嘀粱。在我們的項目里激挪,我們先將下面的代碼加入到 main.go 中:

func main() {
    bootstrap.Run(bootstrap.Options{
        MenuOptions: []*astilectron.MenuItemOptions{{
            Label: astilectron.PtrStr("File"),
            SubMenu: []*astilectron.MenuItemOptions{
                {
                    Label: astilectron.PtrStr("About"),
                    OnClick: func(e astilectron.Event) (deleteListener bool) {
                        if err := bootstrap.SendMessage(w, "about", htmlAbout, func(m *bootstrap.MessageIn) {
                            // Unmarshal payload
                            var s string
                            if err := json.Unmarshal(m.Payload, &s); err != nil {
                                astilog.Error(errors.Wrap(err, "unmarshaling payload failed"))
                                return
                            }
                            astilog.Infof("About modal has been displayed and payload is %s!", s)
                        }); err != nil {
                            astilog.Error(errors.Wrap(err, "sending about event failed"))
                        }
                        return
                    },
                },
                {Role: astilectron.MenuItemRoleClose},
            },
        }},
        OnWait: func(_ *astilectron.Astilectron, iw *astilectron.Window, _ *astilectron.Menu, _ *astilectron.Tray, _ *astilectron.Menu) error {
            w = iw
            go func() {
                time.Sleep(5 * time.Second)
                if err := bootstrap.SendMessage(w, "check.out.menu", "Don't forget to check out the menu!"); err != nil {
                    astilog.Error(errors.Wrap(err, "sending check.out.menu event failed"))
                }
            }()
            return nil
        },
    })
}

它使得關(guān)于選項變成可點(diǎn)擊的,并且渲染出一個有合適內(nèi)容的模態(tài)框锋叨,在 GO app 完成初始化 5 s 后它會顯示一個提示框垄分。

最后我們將下面的代碼加入到
resources/app/static/js/index.js 中:

let index = {
    about: function(html) {
        let c = document.createElement("div");
        c.innerHTML = html;
        asticode.modaler.setContent(c);
        asticode.modaler.show();
    },
    init: function() {
        // Wait for astilectron to be ready
        document.addEventListener('astilectron-ready', function() {
            // Listen
            index.listen();
        })
    },
    listen: function() {
        astilectron.onMessage(function(message) {
            switch (message.name) {
                case "about":
                    index.about(message.payload);
                    return {payload: "payload"};
                    break;
                case "check.out.menu":
                    asticode.notifier.info(message.payload);
                    break;
            }
        });
    }
};

它將監(jiān)聽 GO 發(fā)送過來的消息并做出相應(yīng)的反應(yīng)。

第四步: 打包到 app

現(xiàn)在代碼已經(jīng)完成娃磺,我們需要確保我們能夠以最好的方式把我們的 Golang GUI app 呈現(xiàn)給我們的用戶:

  • 一個 MacOSX app 給 darwin 用戶
  • 一個含有好看圖標(biāo)的 .exewindows 用戶
  • 一個簡單的源碼文件給 linux 用戶

幸運(yùn)的是薄湿,我們可以通過 astilectron 的 bundler 來進(jìn)行操作。
首先我們通過下面命令進(jìn)行安裝:

$ go get -u github.com/asticode/go-astilectron-bundler/...

然后我們在 main.go 中給 bootstrap 添加配置項:

func main() {
    bootstrap.Run(bootstrap.Options{
        Asset: Asset,
        RestoreAssets:  RestoreAssets,
    })
}

然后我們創(chuàng)建配置文件,命名為 bundler.json

{
  "app_name": "Astilectron demo",
  "icon_path_darwin": "resources/icon.icns",
  "icon_path_linux": "resources/icon.png",
  "icon_path_windows": "resources/icon.ico",
  "output_path": "output"
}

最后我們在項目文件夾下運(yùn)行下面的命令(確保 $GOPATH/bin 在你的 $PATH 中)

$ astilectron-bundler -v

第五步: 實(shí)際效果

啊哈豺瘤!結(jié)果在 output/<os>-<arch> 文件夾下吆倦,快來去試一試 :)

image

你當(dāng)然可以打包你的 Golang GUI app 給其他環(huán)境,在 bundler 打包文檔中查看如何將程序打包到其他環(huán)境

結(jié)論

感謝 astilectron 的 bootstrap 和 bundler 坐求,通過一點(diǎn)對組織結(jié)構(gòu)變動工作蚕泽,給你的 Golang 程序添加 GUI 從未如此簡單。
需要指出的是這種方法有 2 個主要的缺點(diǎn):

  • 代碼包的大小至少有 50 MB桥嗤,第一次執(zhí)行后须妻,文件大小至少將有 200 MB
  • 內(nèi)存的消耗有些瘋狂,因為 Electron 并不擅長對內(nèi)存的管理
    但是如果你準(zhǔn)備掌握它砸逊,那么你在給你的程序添加 GUI 時將非常便利璧南!
    GUI 編碼愉快!

via: https://medium.com/@social_57971/how-to-add-a-gui-to-your-golang-app-in-5-easy-steps-c25c99d4d8e0
作者:Asticode
譯者:fengchunsgit
校對:rxcai
本文由 GCTT 原創(chuàng)編譯师逸,Go 中文網(wǎng) 榮譽(yù)推出
[1]: http://p0khjtoyx.bkt.clouddn.com/0.png?e=1512614718&token=U9_WlpL9xDhIuC0OvwzgYz5OmwFSaT0mRch0uuLm:El4EfsIVIW0SLlHyFWyp0NGX_34

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末司倚,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子篓像,更是在濱河造成了極大的恐慌动知,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,185評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件员辩,死亡現(xiàn)場離奇詭異盒粮,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)奠滑,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,652評論 3 393
  • 文/潘曉璐 我一進(jìn)店門丹皱,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人宋税,你說我怎么就攤上這事摊崭。” “怎么了杰赛?”我有些...
    開封第一講書人閱讀 163,524評論 0 353
  • 文/不壞的土叔 我叫張陵呢簸,是天一觀的道長。 經(jīng)常有香客問我乏屯,道長根时,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,339評論 1 293
  • 正文 為了忘掉前任辰晕,我火速辦了婚禮蛤迎,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘伞芹。我一直安慰自己忘苛,他們只是感情好蝉娜,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,387評論 6 391
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著扎唾,像睡著了一般召川。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上胸遇,一...
    開封第一講書人閱讀 51,287評論 1 301
  • 那天荧呐,我揣著相機(jī)與錄音,去河邊找鬼纸镊。 笑死倍阐,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的逗威。 我是一名探鬼主播峰搪,決...
    沈念sama閱讀 40,130評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼凯旭!你這毒婦竟也來了概耻?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,985評論 0 275
  • 序言:老撾萬榮一對情侶失蹤罐呼,失蹤者是張志新(化名)和其女友劉穎鞠柄,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體嫉柴,經(jīng)...
    沈念sama閱讀 45,420評論 1 313
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡厌杜,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,617評論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了计螺。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片夯尽。...
    茶點(diǎn)故事閱讀 39,779評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖登馒,靈堂內(nèi)的尸體忽然破棺而出呐萌,到底是詐尸還是另有隱情,我是刑警寧澤谊娇,帶...
    沈念sama閱讀 35,477評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站罗晕,受9級特大地震影響济欢,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜小渊,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,088評論 3 328
  • 文/蒙蒙 一法褥、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧酬屉,春花似錦半等、人聲如沸揍愁。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,716評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽莽囤。三九已至,卻和暖如春切距,著一層夾襖步出監(jiān)牢的瞬間朽缎,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,857評論 1 269
  • 我被黑心中介騙來泰國打工谜悟, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留话肖,地道東北人。 一個月前我還...
    沈念sama閱讀 47,876評論 2 370
  • 正文 我出身青樓葡幸,卻偏偏與公主長得像最筒,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子蔚叨,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,700評論 2 354

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