Electron: 從零開始寫一個記事本app

Electron介紹

簡單來說,Electron就是可以讓你用Javascript、HTML、CSS來編寫運行于Windows、macOS、Linux系統(tǒng)之上的桌面應用的庫。本文的目的是通過使用Electron開發(fā)一個完整但簡單的小應用:記事本,來體驗一下這個神器的開發(fā)過程。本文猶如Hello World一樣的存在,是個入門級筆記,但如果你之前從未接觸過Electron,而又對它有興趣,某想信這會是一篇值得一看的入門教程。
  PS:這篇文章是基于Windows的開發(fā)過程,未對macOS、Linux作測試。

開發(fā)環(huán)境安裝

安裝Node.js

點擊 這里 進入官網(wǎng)下載、安裝。

安裝cnpm

由于眾所周知的原因,你需要一個cnpm代替npm這里 是官網(wǎng)。安裝命令(打開系統(tǒng)的cmd.exe來執(zhí)行命令):

npm install -g cnpm --registry=https://registry.npm.taobao.org

安裝Electron

cnpm install -g electron

安裝Electron-forge

這是一個類似于傻瓜開發(fā)包的Electron工具整合項目。具體介紹點擊 這里

cnpm install -g electron-forge

新建項目

  1. 假設項目要放到H:\Electron目錄下,項目名為notepad(字母全部小寫,多個單詞之間可以用“-”連接)。
  2. 打開cmd.exe,一路cd到H:\Electron。(也可以在Electron文件夾下,按住Shift鍵并右鍵單擊空白處,選擇在此處打開命令窗口來啟動cmd.exe。)
  3. 執(zhí)行下面的命令來生成名為notepad的項目文件夾,同時安裝項目所需要的模塊、依賴項等。
electron-forge init notepad
  1. cd到notepad目錄下,執(zhí)行下面的命令來啟動app(也可以簡單的用npm start來運行)。
electron-forge start
cmd.exe
  1. 這樣就可以看到基本的app界面了。


    app界面

模板文件

  1. 這里某使用Visual Studio Code來開發(fā)app。
  2. notepad文件夾整個拖到VS Code中打開(或者點菜單文件-打開文件夾選擇notepad文件夾打開項目),可以看一下項目的目錄結構:node_modules文件夾下是各種模塊、類庫,src下是app的源代碼文件,package.json是描述包的文件。
    Catalog
  3. 看一下package.json,注意這里默認已經(jīng)將主進程入口文件配置為index.js(而不是main.js)。
    main

    為避免后面混亂,某還是將這里的src/index.js改成src/main.js,同時也要將文件index.js改名為main.js
    main.js
  4. 看一下main.js,這是app主進程的入口,在這里創(chuàng)建了mainWindow瀏覽器窗口,使用mainWindow.loadURL("file://${__dirname}/index.html")來加載index.html主頁;使用mainWindow.webContents.openDevTools()來打開開發(fā)者工具用于調試(這個操作通常在發(fā)布app時刪除)。然后是app的事件處理:
  • ready: 當Electron完成初始化后觸發(fā),這里初始化后就會去創(chuàng)建瀏覽器窗口并加載主頁面。
  • window-all-closed: 當所有瀏覽器窗口被關閉后觸發(fā),一般此時就退出應用了。
  • activate: 當app激活時觸發(fā),一般針對macOS要需要處理。
  1. 看一眼index.html,這是主頁面,除了顯示Well hey there!!!的信息外,沒什么具體內容。
  2. 于是,現(xiàn)在整個app只有二個源碼文件:main.jsindex.htmlmain.js是主進程入口,index.html是一個web頁面,它需要使用一個瀏覽器窗口(BrowserWindow)來加載和顯示,作為應用的UI,它處在一個獨立的渲染進程中。app啟動時執(zhí)行main.js中的代碼創(chuàng)建窗口,加載頁面等。主進程與渲染進程之間不能直接互相訪問,需要通過ipcMainipcRenderer進行IPC通信(Inter-process communication),或者使用remote模塊在渲染進程中使用主進程中的資源(反過來,在主進程中使用webContents.executeJavascript方法可以訪問渲染進程)。

Notepad App功能設計

這里將實現(xiàn)一個類似于Windows的記事本的App。這個App具備以下功能:

  1. 主菜單:包括File, Edit, View, Help四個主菜單。重點是File菜單下的三個子菜單:New(新建文件)、Open(打開文件)、Save(保存文件),這三個菜單需要自定義點擊事件,其它的菜單基本使用內建的方法處理,所以沒什么難度。
  2. 文本框:用于文本編輯。這也是這個App上的唯一一個組件,它的寬和高自動平鋪滿整個窗口大小。當修改了文本框中的文字后,會在App標題欄上最右側添加一個*號以表示文檔尚未保存。
  3. 加載和保存文本:可以打開本地文本文件,支持.txt, .js, .html, .md等文本文件;可以將文本內容保存為本地文本文件。在打開新建文件前,如果當前文檔尚未保存,會提示用戶先保存文檔。
  4. 退出程序:退出窗口或程序時,會檢測當前文檔是否需要保存,如果尚未保存,提示用戶保存。
  5. 右鍵菜單:支持右鍵菜單,可以通過菜單右鍵執(zhí)行一些基本的操作,如:復制、粘貼等。
    下面是這個記事本App的演示效果,源碼下載點擊 這里
    Demo

Notepad App功能細節(jié)

由于主進程與渲染進程不能直接互相訪問,所以部分細節(jié)有必要先考慮清楚。

  1. 主菜單:因為菜單只存在于主進程中,所以在執(zhí)行某些涉及頁面(渲染進程)的菜單命令時,比如Open(打開文件)命令,就需要與渲染進程進行通信,這可以使用ipcMainipcRenderer來實現(xiàn)。
  2. 右鍵菜單、對話框:所謂右鍵菜單其實和主菜單并無分別,只是顯示方式不同。由于菜單、對話框等都只存在于主進程中,要在渲染進程中使用它們,就需要向主進程發(fā)送進程間消息,為簡化操作,Electron提供了一個remote模塊,可以在渲染進程中調用主進程的對象和方法,而無需顯式地發(fā)送進程間消息,所以這一部分可以由它來實現(xiàn)。PS:對于從主進程訪問渲染進程(反向操作),可以使用webContents.executeJavascript方法。
  3. 退出時保存檢測:用戶點擊窗口的關閉按鈕,或者點擊Exit菜單就會關閉窗口退出程序。在退出時,有必要檢查文檔是否需要保存,如果尚未保存就提示用戶保存。要實現(xiàn)這一效果,首先,在主進程監(jiān)測到用戶關閉窗口時,向渲染進程發(fā)送一個特定的消息表明窗口準備關閉,渲染進程獲得該消息后查看文檔是否需要保存,如果需要就彈窗提示用戶保存,用戶保存或取消保存后,渲染進程再向主進程發(fā)送一個消息表明可以關閉程序了,主進程獲得該消息后關閉窗口退出程序。這個過程也由ipcMainipcRenderer來實現(xiàn)。

Notepad App的實現(xiàn)

整個App功能比較簡單,最終實現(xiàn)后也只用到了三個主要文件,包括:main.jsindex.htmlindex.js

main.js

這是主進程的入口,在這里創(chuàng)建App窗口,生成菜單,載入頁面等。下面是該文件的完整源碼,二個//-------之間是某根據(jù)功能需要添加的代碼,其余是模板自動生成的代碼。

import { app, BrowserWindow } from 'electron';
//-----------------------------------------------------------------
import { Menu, MenuItem, dialog, ipcMain } from 'electron';
import { appMenuTemplate } from './appmenu.js';
//是否可以安全退出
let safeExit = false;
//-----------------------------------------------------------------

// Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the JavaScript object is garbage collected.
let mainWindow;

const createWindow = () => {
  // Create the browser window.
  mainWindow = new BrowserWindow({
    width: 800,
    height: 600,
  });

  // and load the index.html of the app.
  mainWindow.loadURL(`file://${__dirname}/index.html`);

  // Open the DevTools.
  //mainWindow.webContents.openDevTools();

  //-----------------------------------------------------------------
  //增加主菜單(在開發(fā)測試時會有一個默認菜單,但打包后這個菜單是沒有的,需要自己增加)
  const menu=Menu.buildFromTemplate(appMenuTemplate); //從模板創(chuàng)建主菜單
  //在File菜單下添加名為New的子菜單
  menu.items[0].submenu.append(new MenuItem({ //menu.items獲取是的主菜單一級菜單的菜單數(shù)組,menu.items[0]在這里就是第1個File菜單對象,在其子菜單submenu中添加新的子菜單
    label: "New",
    click(){
      mainWindow.webContents.send('action', 'new'); //點擊后向主頁渲染進程發(fā)送“新建文件”的命令
    },
    accelerator: 'CmdOrCtrl+N' //快捷鍵:Ctrl+N
  }));
  //在New菜單后面添加名為Open的同級菜單
  menu.items[0].submenu.append(new MenuItem({
    label: "Open",
    click(){
      mainWindow.webContents.send('action', 'open'); //點擊后向主頁渲染進程發(fā)送“打開文件”的命令
    },
    accelerator: 'CmdOrCtrl+O' //快捷鍵:Ctrl+O
  })); 
  //再添加一個名為Save的同級菜單
  menu.items[0].submenu.append(new MenuItem({
    label: "Save",
    click(){
      mainWindow.webContents.send('action', 'save'); //點擊后向主頁渲染進程發(fā)送“保存文件”的命令
    },
    accelerator: 'CmdOrCtrl+S' //快捷鍵:Ctrl+S
  }));
  //添加一個分隔符
  menu.items[0].submenu.append(new MenuItem({
    type: 'separator'
  }));
  //再添加一個名為Exit的同級菜單
  menu.items[0].submenu.append(new MenuItem({
    role: 'quit'
  }));
  Menu.setApplicationMenu(menu); //注意:這個代碼要放到菜單添加完成之后,否則會造成新增菜單的快捷鍵無效

  mainWindow.on('close', (e) => {
    if(!safeExit){
      e.preventDefault();
      mainWindow.webContents.send('action', 'exiting');
    }
  });
  //-----------------------------------------------------------------

  // Emitted when the window is closed.
  mainWindow.on('closed', () => {
    // Dereference the window object, usually you would store windows
    // in an array if your app supports multi windows, this is the time
    // when you should delete the corresponding element.
    mainWindow = null;
  });
};

// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.on('ready', createWindow);

// Quit when all windows are closed.
app.on('window-all-closed', () => {
  // On OS X it is common for applications and their menu bar
  // to stay active until the user quits explicitly with Cmd + Q
  if (process.platform !== 'darwin') {
    app.quit();
  }
});

app.on('activate', () => {
  // On OS X it's common to re-create a window in the app when the
  // dock icon is clicked and there are no other windows open.
  if (mainWindow === null) {
    createWindow();
  }
});

// In this file you can include the rest of your app's specific main process
// code. You can also put them in separate files and import them here.

//-----------------------------------------------------------------
//監(jiān)聽與渲染進程的通信
ipcMain.on('reqaction', (event, arg) => {
  switch(arg){
    case 'exit':
      //做點其它操作:比如記錄窗口大小、位置等,下次啟動時自動使用這些設置;不過因為這里(主進程)無法訪問localStorage,這些數(shù)據(jù)需要使用其它的方式來保存和加載,這里就不作演示了。這里推薦一個相關的工具類庫,可以使用它在主進程中保存加載配置數(shù)據(jù):https://github.com/sindresorhus/electron-store
      //...
      safeExit=true;
      app.quit();//退出程序
      break;
  }
});
//-----------------------------------------------------------------

首先,app.on('ready', createWindow)也就是當Electron完成初始化后,就調用createWindow方法來創(chuàng)建瀏覽器窗口mainWindow(與主進程只能有1個不同,可以根據(jù)需要適時創(chuàng)建更多個瀏覽器窗口,這些窗口由主進程負責創(chuàng)建和管理,每個瀏覽器窗口使用一個獨立的渲染進程;本文只需使用一個瀏覽器窗口,即mainWindow)。同時,使用Menu.buildFromTemplate(appMenuTemplate)通過一個菜單模板來創(chuàng)建app應用主菜單,模板代碼存放在appmenu.js文件中(這個文件包含在本文的源碼中,也可以點擊這里查看),這個模板的寫法可以參考官方的 Electron API Demos
Customize Menus的例子。模板的第一個菜單是File菜單,它的子菜單被設計成空的,在這里使用menu.items[0].submenu.append方法向這個File菜單添加四個子菜單,分別是:New(新建文檔),Open(打開文檔),Save(保存文檔),Exit(退出程序)。其中,前三個菜單在點擊后都會向渲染進程發(fā)送信息,通知渲染進程執(zhí)行相關處理。如對于New菜單,使用mainWindow.webContents.send('action', 'new')的方式,通知渲染進程要新建一個文檔。渲染進程會使用ipcRenderer.on方法來執(zhí)行監(jiān)聽,監(jiān)聽到消息后就會執(zhí)行相應處理(這部分在index.js中實現(xiàn))。最后使用Menu.setApplicationMenu(menu)將主菜單安裝到瀏覽器窗體中(所有窗體會共享主菜單)。

index.html

這是App的文本編輯頁面。這個頁面很簡單,整個頁面就只有一個TextArea控件(id為txtEditor),平鋪滿整個窗口。該頁面使用require('./index.js')載入index.js

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Notepad</title>
  <style type="text/css">
    body,html{
        margin:0px;
        height:100%;
    }
    
    #txtEditor{
        width:100%;
        height:99.535%;
        padding:0px;
        margin:0px;
        border:0px;
        font-size: 18px;
    }
  </style>
  </head>
  <body>
  <textarea id="txtEditor"></textarea>
</body>
  <script>
    require('./index.js');
  </script>
</html>

index.js

所有主頁面index.html涉及到的頁面處理、與主進程交互等的操作都會放到該js文件中。該文件完整代碼:

import { ipcRenderer, remote } from 'electron';
const { Menu, MenuItem, dialog } = remote;

let currentFile = null; //當前文檔保存的路徑
let isSaved = true;     //當前文檔是否已保存
let txtEditor = document.getElementById('txtEditor'); //獲得TextArea文本框的引用

document.title = "Notepad - Untitled"; //設置文檔標題,影響窗口標題欄名稱

//給文本框增加右鍵菜單
const contextMenuTemplate=[
    { role: 'undo' },       //Undo菜單項
    { role: 'redo' },       //Redo菜單項
    { type: 'separator' },  //分隔線
    { role: 'cut' },        //Cut菜單項
    { role: 'copy' },       //Copy菜單項
    { role: 'paste' },      //Paste菜單項
    { role: 'delete' },     //Delete菜單項
    { type: 'separator' },  //分隔線
    { role: 'selectall' }   //Select All菜單項
];
const contextMenu=Menu.buildFromTemplate(contextMenuTemplate);
txtEditor.addEventListener('contextmenu', (e)=>{
    e.preventDefault();
    contextMenu.popup(remote.getCurrentWindow());
});

//監(jiān)控文本框內容是否改變
txtEditor.oninput=(e)=>{
    if(isSaved) document.title += " *";
    isSaved=false;
};

//監(jiān)聽與主進程的通信
ipcRenderer.on('action', (event, arg) => {
    switch(arg){        
    case 'new': //新建文件
        askSaveIfNeed();
        currentFile=null;
        txtEditor.value='';   
        document.title = "Notepad - Untitled";
        //remote.getCurrentWindow().setTitle("Notepad - Untitled *");
        isSaved=true;
        break;
    case 'open': //打開文件
        askSaveIfNeed();
        const files = remote.dialog.showOpenDialog(remote.getCurrentWindow(), {
            filters: [
                { name: "Text Files", extensions: ['txt', 'js', 'html', 'md'] }, 
                { name: 'All Files', extensions: ['*'] } ],
            properties: ['openFile']
        });
        if(files){
            currentFile=files[0];
            const txtRead=readText(currentFile);
            txtEditor.value=txtRead;
            document.title = "Notepad - " + currentFile;
            isSaved=true;
        }
        break;
    case 'save': //保存文件
        saveCurrentDoc();
        break;
    case 'exiting':
        askSaveIfNeed();
        ipcRenderer.sendSync('reqaction', 'exit');
        break;
    }
});

//讀取文本文件
function readText(file){
    const fs = require('fs');
    return fs.readFileSync(file, 'utf8');
}
//保存文本內容到文件
function saveText(text, file){
    const fs = require('fs');
    fs.writeFileSync(file, text);
}

//保存當前文檔
function saveCurrentDoc(){
    if(!currentFile){
        const file = remote.dialog.showSaveDialog(remote.getCurrentWindow(), {
            filters: [
                { name: "Text Files", extensions: ['txt', 'js', 'html', 'md'] }, 
                { name: 'All Files', extensions: ['*'] } ]
        });
        if(file) currentFile=file;
    }
    if(currentFile){
        const txtSave=txtEditor.value;
        saveText(txtSave, currentFile);
        isSaved=true;
        document.title = "Notepad - " + currentFile;
    }
}

//如果需要保存,彈出保存對話框詢問用戶是否保存當前文檔
function askSaveIfNeed(){
    if(isSaved) return;
    const response=dialog.showMessageBox(remote.getCurrentWindow(), {
        message: 'Do you want to save the current document?',
        type: 'question',
        buttons: [ 'Yes', 'No' ]
    });
    if(response==0) saveCurrentDoc(); //點擊Yes按鈕后保存當前文檔
}

首先,前面說了,在渲染進程中不能直接訪問菜單,對話框等,它們只存在于主進程中,但可以通過remote來使用這些資源。

import { remote } from 'electron';
const { Menu, MenuItem, dialog } = remote;

然后,const contextMenu=Menu.buildFromTemplate(contextMenuTemplate)即使用contextMenuTemplate模板來創(chuàng)建編輯器的右鍵菜單(雖然創(chuàng)建過程在渲染進程中進行,但實際上使用remote來創(chuàng)建的菜單、對話框等,仍然只存在于主進程內),由于這里涉及到的菜單都只需要使用系統(tǒng)的內建功能,不需要自定義,所以這里比較簡單。使用txtEditor.addEventListener('contextmenu')來監(jiān)聽右鍵菜單請求,使用contextMenu.popup(remote.getCurrentWindow())來彈出右鍵菜單。
  txtEditor.oninput用于監(jiān)控文本框內容變化,如果有改變,則將文檔標記為尚未保存,并在標題欄最右側顯示一個*號作為提示。
  PS:在Win7上如果沒有啟用Aero效果,使用document.title = xxxremote.getCurrentWindow().setTitle(xxx)都看不到程序標題欄的標題變化,只當你比如縮放一下窗口后這個修改才會被刷新。
  ipcRenderer.on用于監(jiān)聽由主進程發(fā)來的消息。前面說過,主進程使用mainWindow.webContents.send('action', 'new')的方式向渲染進程發(fā)送特定消息,渲染進程監(jiān)聽到消息后,根據(jù)消息內容做出相應處理。比如,這里,當主進程發(fā)來new的消息后,渲染進程就開始著手新建一個文檔,在新建前會使用askSaveIfNeed方法檢測文檔是否需要保存,并提示用戶保存;對于open的消息就會調用remote.dialog.showOpenDialog來顯示一個文件打開對話框,由用戶選擇要打開的文檔然后加載文本數(shù)據(jù);而對于save消息就會對當前文檔進行保存操作。

退出時保存檢測的實現(xiàn)過程

正如前面在App功能細節(jié)中討論的一樣,在關閉程序前,友好的做法是檢測文檔是否需要保存,如果尚未保存,通知用戶保存。要實現(xiàn)這一功能,需要在主進程和渲染進程間進行相互通信,以獲得窗體關閉和文檔保存的確認,實現(xiàn)安全退出。

主進程端

首先在main.js中,使用mainWindow.on('close')來監(jiān)控mainWindow窗口的關閉。

mainWindow.on('close', (e) => {
    if(!safeExit){
      e.preventDefault();
      mainWindow.webContents.send('action', 'exiting');
    }
  });

這里safeExit開關用于標記渲染進程是否已經(jīng)向主進程反饋它已經(jīng)完成所有操作了。如果尚未反饋,則使用e.preventDefault()阻止窗口關閉,并使用mainWindow.webContents.send('action', 'exiting')向渲染進程發(fā)送一個exiting消息,告訴渲染進程:嘿,我要關掉窗口了,你趕緊看看還要什么沒做完的,做完后通知我。
  既然主進程要等渲染進程的反饋,就需要監(jiān)聽渲染進程發(fā)回的消息,所以主進程使用ipcMain.on來執(zhí)行監(jiān)聽。如果渲染進程發(fā)送一個exit消息過來,就表示可以安全退出了。

ipcMain.on('reqaction', (event, arg) => {
  switch(arg){
    case 'exit':
      safeExit=true;
      app.quit();
      break;
  }
});

渲染進程端

在渲染進程這邊的index.js中,在ipcRenderer.on監(jiān)聽方法中,相應的有一個消息處理是針對主進程發(fā)來的exiting消息的,當獲知主進程準備關閉窗口,渲染進程就先去檢查文檔是否保存過了,如果尚未保存就通知用戶保存,用戶保存或取消保存后,使用ipcRenderer.sendSync('reqaction', 'exit')來向主進程發(fā)送一個exit消息,表示:我要做的都做完了,你想退就退吧。

case 'exiting':
        askSaveIfNeed();
        ipcRenderer.sendSync('reqaction', 'exit');
        break;

主進程監(jiān)聽到這個消息后,將safeExit標記為true,表示已經(jīng)得到渲染進程的確認,然后就可以使用app.quit()安全退出了。當然,在退出前,可以再執(zhí)行一些其它操作(比如保存參數(shù)配置等)。

編譯打包

  1. 鍵入以下命令進行編譯打包:
npm run make

該命令會將文件打包到當前項目目錄下的out文件夾下。打包后發(fā)現(xiàn),源碼直接暴露在[app項目目錄]\out\notepad-win32-x64\resources\app\src目錄下。

  1. 修改package.json,在electronPackagerConfig部分添加"asar": true
"electronPackagerConfig": {
        "asar": true
      }

重新打包后源碼文件會被打包進app.asar文件中(該文件仍然在src目錄下)。

  1. 可以直接運行打包后的notepad.exe啟動程序。

by Mandarava(鰻駝螺) 2017.07.12

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,238評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,430評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,134評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,893評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,653評論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,136評論 1 323
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,212評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,372評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,888評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,738評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,939評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,482評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,179評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,588評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,829評論 1 283
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,610評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,916評論 2 372

推薦閱讀更多精彩內容