使用Node.js+React+EUI快速搭建网页应用

0. 起因

老板要搞个Log分析工具,数据存储选用的是Elasticsearch,起初想法是做个Kibana的插件,后来觉得依靠Kibana太庞大,而且后期想要把代码直接部署在GitHub page上,因此打算做成个独立的工具。最终选用了Node.js+React,主要原因是相中了一个React UI库EUI(Elastic Stack推出的一个开源UI库,风格与Kibana一样),毕竟,不是谁都能写出漂亮的UI,强如Linux之父Linus都表示“如果我被困在一个与世隔绝的岛上,逃离这座岛的唯一办法是写出漂亮的UI,那我估计就老死在岛上了”。
虽然最终由于内网权限问题没部署上GitHub page,但基本流程都跑通了。既然代码都写了,顺路写个总结吧。

1. 工程搭建

首先,我们需要安装Node.js,到Node.js官网随便下一个安装包安装,或者下载压缩包解压缩后手动设置环境变量使用。我比较喜欢直接使用压缩包,因为这样可以随意在多个版本间切换而且不用额外的工具辅助。例如在Ubuntu下下载压缩包解压缩并通过命令export PATH=$NODEJS_ROOT/bin:$PATH即完成了安装。安装完成后可以通过以下命令查看是否安装成功:

node --version
npm --version

安装完成后,按照以下结构建立一个目录:

my-app/
  package.json
  public/
    index.html
  src/
    index.js

其中my-app可以改成任意你喜欢的名字,剩余的部分名字必须与例子给出的一致,这是工程可以构建的前提。

然后,打开package.json,在其中填入以下内容并保存:

{
  "name": "eui-demo",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "@testing-library/jest-dom": "^4.2.4",
    "@testing-library/react": "^9.5.0",
    "@testing-library/user-event": "^7.2.1",
    "react": "^16.13.1",
    "react-dom": "^16.13.1",
    "react-scripts": "3.4.3"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  },
  "eslintConfig": {
    "extends": "react-app"
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  }
}

上面的内容中,dependenciesscripts这两个对象必须有,其他的可选择性添加,即最小要求如下:

{
  "dependencies": {
    "react": "^16.13.1",
    "react-dom": "^16.13.1",
    "react-scripts": "3.4.3"
  },
  "scripts": {
    "start": "react-scripts start",
  }
}

package.json文件准备好后执行以下命令:

npm install
npm start

执行完上面的命令,整个工程已经构建完毕,在浏览器中输入http://localhost:3000/即可访问你刚构建起来的应用,虽然目前这个应用什么也没做也没有显示任何内容。

当然,其实还有另一个更加简单的方法来构建应用,Node.js安装完成后只需要在终端输入一条命令即可 ,同样,my-app可以换成任意名字:

npx create-react-app my-app/

等待命令执行完毕即可。

2. Hello World?

如果工程是我们自己手动一步一步搭建起来的,通过浏览器访问http://localhost:3000/是什么都不会显示的。下一步,我们就需要让它显示点什么。编程嘛,就从Hello World开始吧。

首先,我们打开my-app/public/index.html,在里面输入一下信息并保存:

<html>
    <head></head>
    <body>
        <div id='content'>

        </div>
    </body>
</html>

上面的超文本标记代码很简单,如果用浏览器打开的话还是什么也看不到。它只是为后续的React UI代码提供了一个挂载点 —— id为content的一个div,在我们的小例子中,对HTML的编辑就算完了,身下的就全部交给JS代码了。

接着,我们打开my-app/src/index.js,输入一下代码:

import React from 'react';
import ReactDOM from 'react-dom';

function HelloWorld(props) {
    return (<div><p>Hello World!</p></div>)
}

ReactDOM.render(
  <HelloWorld />,
  document.getElementById('content')
);

编辑完my-app/src/index.js并保存之后,我们在my-app目录中执行npm start,就可以在浏览器中看到如图1结果:

图1 Hello World

在上面的代码中,我们做了三件事:

  1. 第一第二行代码分别从react以及react-dom这两个模块中导入了React以及ReactDOM这两个类。值得注意的是,虽然我们没有直接看到使用导入的React,但是这个导入语句是必须的,否则编译就会报错!
  2. 接下来,定义了一个名为HelloWorld的函数,这个函数在React中称为函数组件*(Function Components),它与类组件(Class Components)一起组成了React 渲染UI的核心。这个函数只有一个参数props,这个props实际上是一个字典,可以通过它传递任意参数给函数组件;函数返回一个描述如何显示UI的React元素,虽然看着像超文本标记语言(HTML),但是它却不是。它的名字叫做JSX(JavaScript eXtension),它是JavaScript语法的扩展。
  3. 第三步,就是将我们定义的函数组件通过ReactDOM.render()函数渲染出来。ReactDOM.render()需要一个挂载节点,在我们的例子中的挂载节点是前面提到的id为content的一个div,通过ReactDOM.render()渲染的界面都托管在React DOM中,由React DOM负责管理以及更新。

3. 实践

有了Hello World的铺垫,我们现在可以正式搭建一个简单点的应用了。我们选用的UI框架是Elastic UI,单然如果你有自己喜欢的其他框架也是可以的。

假设我们要搭建一个Markdown编辑器。我们确定它的结构如图2,我们需要用EUI实现我们的目标:


图2 Markdown 编辑器结构

3.1. 搭架子

我们在EUI中找到一个名叫Page的布局空间,其布局如图3,正好符合我们的期望:

图3 Page空间布局样式

同样,我们分别找到导航组件(tree-view)、标签组件(tabs)以及Markdown编辑框组件(markdown-editor),将它们搭积木一样组合起来,并做些调整就能得到如图4所示的界面:

图4 Markdwon editor界面预览

为了方便起见,我们将Page组件、导航栏组件、标签栏组件、编辑器组件代码放到独立JS文件中,分别命名为page.js, file-nav.js, tabs.js, markdown-editor.js,具体结构如下:

my-app/
  package.json
  public/
    index.html
  src/
    file-nav.js
    index.js
    markdown-editor.js
    page.js
    tabs.js

他们的代码分别如下所示:

// file-nav.js
import React from 'react';

import { EuiIcon, EuiTreeView, EuiToken } from '@elastic/eui';

export default () => {
  const showAlert = () => {
    alert('You squashed a bug!');
  };

  const items = [
    {
      label: 'src',
      id: 'src',
      icon: <EuiIcon type="folderClosed" />,
      iconWhenExpanded: <EuiIcon type="folderOpen" />,
      isExpanded: true,
      children: [
        {
          label: 'index.md',
          id: 'item_a',
          icon: <EuiIcon type="document" />,
        },
        {
          label: 'level2 folder',
          id: 'item_b',
          icon: <EuiIcon type="folderOpen" />,
          iconWhenExpanded: <EuiIcon type="folderOpen" />,
          children: [
            {
              label: 'monosodium_glutammate.md',
              id: 'item_cloud',
              icon: <EuiIcon type="document" />,
            },
            {
              label: "cobalt.md",
              id: 'item_bug',
              icon: <EuiIcon type="document" />,
              callback: showAlert,
            },
          ],
        },
        {
          label: 'xxxxx folder',
          id: 'item_c',
          icon: <EuiIcon type="folderOpen" />,
          iconWhenExpanded: <EuiIcon type="folderOpen" />,
          children: [
            {
              label: 'Another Cloud.md',
              id: 'item_cloud2',
              icon: <EuiIcon type="document" />,
            },
            {
              label:
                'elastic_link.md',
              id: 'item_bug2',
              icon: <EuiIcon type="document" />,
              callback: showAlert,
            },
          ],
        },
      ],
    },
    {
      label: 'othter',
      id: 'src2',
      icon: <EuiIcon type="folderClosed" />,
      iconWhenExpanded: <EuiIcon type="folderOpen" />,
      isExpanded: true,
    },
  ];

  return (
    <div style={{ width: '20rem' }}>
      <EuiTreeView items={items} aria-label="eui-markdown-editor" />
    </div>
  );
};
// markdown-editor.js
import React, { useCallback, useState } from 'react';

import {
  EuiMarkdownEditor,
  EuiSpacer,
  EuiCodeBlock,
  EuiButtonToggle,
} from '@elastic/eui';

const initialContent = `## Hello world!

Basic "github flavored" markdown will work as you'd expect.

The editor also ships with some built in plugins. For example it can handle checkboxes. Notice how they toggle state even in the preview mode.

- [ ] Checkboxes
- [x] Can be filled
- [ ] Or empty
`;

const dropHandlers = [
  {
    supportedFiles: ['.jpg', '.jpeg'],
    accepts: itemType => itemType === 'image/jpeg',
    getFormattingForItem: item => {
      // fake an upload
      return new Promise(resolve => {
        setTimeout(() => {
          const url = URL.createObjectURL(item);
          resolve({
            text: `![${item.name}](${url})`,
            config: { block: true },
          });
        }, 1000);
      });
    },
  },
];

export default () => {
  const [value, setValue] = useState(initialContent);
  const [messages, setMessages] = useState([]);
  const [ast, setAst] = useState(null);
  const [isAstShowing, setIsAstShowing] = useState(false);
  const onParse = useCallback((err, { messages, ast }) => {
    setMessages(err ? [err] : messages);
    setAst(JSON.stringify(ast, null, 2));
  }, []);
  return (
    <>
      <EuiMarkdownEditor
        aria-label="EUI markdown editor demo"
        value={value}
        onChange={setValue}
        height={400}
        onParse={onParse}
        errors={messages}
        dropHandlers={dropHandlers}
      />
     
      {isAstShowing && <EuiCodeBlock language="json">{ast}</EuiCodeBlock>}
    </>
  );
};
// page.js
import React from 'react';

import {
  EuiPage,
  EuiPageBody,
  EuiPageContent,
  EuiPageContentBody,
  EuiPageContentHeader,
  EuiPageContentHeaderSection,
  EuiPageHeader,
  EuiPageSideBar,
} from '@elastic/eui';

import FileNav from './file-nav';
import MarkdownEditor from './markdown-editor';
import Tabs from './tabs';

export default () => (
  <EuiPage>
    <EuiPageSideBar>
      <FileNav />
    </EuiPageSideBar>
    <EuiPageBody component="div">

      <EuiPageHeader>

      </EuiPageHeader>
      <EuiPageContent>

        <EuiPageContentHeader>
          <EuiPageContentHeaderSection>
            <Tabs />
          </EuiPageContentHeaderSection>
          <EuiPageContentHeaderSection>

          </EuiPageContentHeaderSection>
        </EuiPageContentHeader>
        <EuiPageContentBody>
          <MarkdownEditor />
        </EuiPageContentBody>
      </EuiPageContent>
    </EuiPageBody>
  </EuiPage>
);
// tabs.js
import React, { useState, Fragment } from 'react';

import {
  EuiIcon,
  EuiTabs,
  EuiTab,
  EuiSpacer,
} from '@elastic/eui';

const tabs = [
  {
    id: 'cobalt',
    name: (<span>
        cobalt.md&nbsp;<EuiIcon type="cross" />
      </span>),
    disabled: false,
  },
  {
    id: 'dextrose',
    name: (<span>
        dextrose.md&nbsp;<EuiIcon type="cross" />
      </span>),
    disabled: false,
  },
  {
    id: 'hydrogen',
    name: (
      <span>
        Hydrogen&nbsp;<EuiIcon type="cross" />
      </span>
    ),
    disabled: false,
  },
  {
    id: 'monosodium_glutammate',
    name: (<span>
        monosodium_glutammate.md&nbsp;<EuiIcon type="cross" />
      </span>),
    disabled: false,
  },
  {
    id: 'elastic_link',
    name: (<span>
        elastic_link.md&nbsp;<span onClick={e =>{alert('close me?')}}><EuiIcon type="cross" /></span>
      </span>),
    disabled: false,
  },
];

export default () => {
  const [selectedTabId, setSelectedTabId] = useState('cobalt');

  const onSelectedTabChanged = id => {
    setSelectedTabId(id);
  };

  const renderTabs = () => {
    return tabs.map((tab, index) => (
      <EuiTab
        {...(tab.href && { href: tab.href, target: '_blank' })}
        onClick={() => onSelectedTabChanged(tab.id)}
        isSelected={tab.id === selectedTabId}
        disabled={tab.disabled}
        key={index}>
        {tab.name}
      </EuiTab>
    ));
  };

  return (
    <Fragment>
      <EuiTabs size="s">{renderTabs()}</EuiTabs>
    </Fragment>
  );
};
// index.js
import React, {useState} from 'react';
import ReactDOM from 'react-dom';
import "@elastic/eui/dist/eui_theme_light.css";

import Editor from './page';

ReactDOM.render(
  <Editor />,
  document.getElementById('content')
);

好了,到这里,我们的架子已经搭起来了,我们接下来就需要为他们注入灵魂,让各个组件之间互动起来。例如我们希望点击不同的文件标签,编辑框显示的是不同的文件内容。

3.2. 关联UI

点击不同的文件标签让编辑框显示不同内容主要涉及的就是UI如何更新自己的状态或者UI如何通知别的UI更新其状态。在React中,UI被前面提到的类组件(Class Component)以及函数组件(Function Component)分为一个个独立、可复用的模块。类组件和函数组件的形式分别如下:

// Class components
class ClazzComponent extends React.Component {
  constructor(props) {
    super(props);
    this.state = {};
  }
  render() {
    return <h1>Hello, {this.props.name}</h1>;
  }
}

// Function Components
function FuncComponent(props) {
    return <h1>Hello, {props.name}</h1>;
}

从显示效果上看,它们是一完全一样的。类组件相对复杂但是拥有更多的特性,例如类组件就有一个state字典,通过setState方法类组件可以更新state的内容,一旦state改变了,那么直接或者间接使用state的React元素(React Elements)就会被更新。例如我们把上面的ClazzComponent做下修改:

// Class components
export default class ClazzComponent extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
        what: 'World',
    };
  }

  render() {
    return <h1 onClick={e => this.setState({what: this.state.what + ' World'})}>Hello, {this.state.what}</h1>;
  }
}

当我们点击界面上的Hello, World,它就会变成Hello, World WorldHello, World World World等。函数组件相比于类组件最大的区别就是很多类组件有的特性它没有,例如它就没有state这个成员以及setState方法,因为理论上它是一个函数(至于说在JavaScript中“万物皆对象”,方法其实也是个对象,那不是本文关注的重点)。但是,在React中通过一些钩子函数,就能让函数组件具有类组件的一些特性,例如“state”。

让stateless的方法函数变得state,可以通过useState这个钩子函数。例如,我们把上面的示例也做下修改:

// Function Components
import React, {useState} from 'react';

function FuncComponent(props) {
    const [name, setName] = useState('World');
    return <h1 onClick={e => setName(name + ' World')}>Hello, {name}</h1>;
}

经过修改,函数组价的的表现也和类组件一样了。首先,我们从React模块中导入useState这个钩子,然后我们在函数组件中通过它获得了一个厨师长name已经更新方法setName,这里namesetName可以是任意的名字。

知道了如何更新组件的状态,接下来我们就能着手进行我们的Markdown编辑器的编码了。篇幅有限,我们搞得简单点。主要分以下三步:

  1. 首先,我们定义一个回调函数,将这个回调函数注册到tabs.js的函数组件中;
  2. 然后,当tabs的标签有改变的时候,tabs调用我们注册的回调函数,并将被选中的tab的id传给我们回调函数,这样我们就能知道当前那个标签被选中了;
  3. 最后,在回调函数中,我们通过判断id知道用户希望显示的内容,通过useState导出的setContent方法通知Markdown编辑器控件更改其显示的内容。

具体代码如下所示,我们只更改了page.js, markdown-editor.j, tabs.js,其他代码保持不变:

// page.js
import React, {useState} from 'react';

import {
  EuiPage,
  EuiPageBody,
  EuiPageContent,
  EuiPageContentBody,
  EuiPageContentHeader,
  EuiPageContentHeaderSection,
  EuiPageHeader,
  EuiPageSideBar,
} from '@elastic/eui';

import FileNav from './file-nav';
import MarkdownEditor from './markdown-editor';
import Tabs from './tabs';
import tabs from './tabs';

const tab1Content = `## Hello world!

Basic "github flavored" markdown will work as you'd expect.

The editor also ships with some built in plugins. For example it can handle checkboxes. Notice how they toggle state even in the preview mode.

- [ ] Checkboxes
- [x] Can be filled
- [ ] Or empty
`;


const tab2Content = `## I am tab two, name , not Tattoo!
#### I am tab two, not Tattoo!
`; 

const tab3Content = `
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).

## Available Scripts

In the project directory, you can run:

### \`npm start\`

Runs the app in the development mode.<br />
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.

The page will reload if you make edits.<br />
You will also see any lint errors in the console.
`;

export default () => {
  const [content, setContent] = useState('init content')
  let tabSelected = (tabId) => {
    if (tabId == 'cobalt')
      setContent(tab1Content);
    else if (tabId == 'dextrose')
      setContent(tab2Content);
    else if (tabId == 'hydrogen')
      setContent(tab3Content);


  }
  return (
  <EuiPage>
    <EuiPageSideBar>
      <FileNav />
    </EuiPageSideBar>
    <EuiPageBody component="div">

      <EuiPageHeader>

      </EuiPageHeader>
      <EuiPageContent>

        <EuiPageContentHeader>
          <EuiPageContentHeaderSection>
            <Tabs onTabSelected={tabSelected}/>
          </EuiPageContentHeaderSection>
          <EuiPageContentHeaderSection>

          </EuiPageContentHeaderSection>
        </EuiPageContentHeader>
        <EuiPageContentBody>
          <MarkdownEditor content={content}/>
        </EuiPageContentBody>
      </EuiPageContent>
    </EuiPageBody>
  </EuiPage>);
};
//markdown-editor.js
import React, { useCallback, useState } from 'react';

import {
  EuiMarkdownEditor,
  EuiSpacer,
  EuiCodeBlock,
  EuiButtonToggle,
} from '@elastic/eui';

const dropHandlers = [
  {
    supportedFiles: ['.jpg', '.jpeg'],
    accepts: itemType => itemType === 'image/jpeg',
    getFormattingForItem: item => {
      // fake an upload
      return new Promise(resolve => {
        setTimeout(() => {
          const url = URL.createObjectURL(item);
          resolve({
            text: `![${item.name}](${url})`,
            config: { block: true },
          });
        }, 1000);
      });
    },
  },
];

export default (props) => {
  const [value, setValue] = useState(props.content);
  const [messages, setMessages] = useState([]);
  const [ast, setAst] = useState(null);
  const [isAstShowing, setIsAstShowing] = useState(false);
  const onParse = useCallback((err, { messages, ast }) => {
    setMessages(err ? [err] : messages);
    setAst(JSON.stringify(ast, null, 2));
  }, []);
  return (
    <>
      <EuiMarkdownEditor
        aria-label="EUI markdown editor demo"
        value={props.content}
        onChange={setValue}
        height={400}
        onParse={onParse}
        errors={messages}
        dropHandlers={dropHandlers}
      />
     
      {isAstShowing && <EuiCodeBlock language="json">{ast}</EuiCodeBlock>}
    </>
  );
};
// tabs.js
import React, { useState, Fragment } from 'react';

import {
  EuiIcon,
  EuiTabs,
  EuiTab,
  EuiSpacer,
} from '@elastic/eui';

const tabs = [
  {
    id: 'cobalt',
    name: (<span>
        cobalt.md&nbsp;<EuiIcon type="cross" />
      </span>),
    disabled: false,
  },
  {
    id: 'dextrose',
    name: (<span>
        dextrose.md&nbsp;<EuiIcon type="cross" />
      </span>),
    disabled: false,
  },
  {
    id: 'hydrogen',
    name: (
      <span>
        Hydrogen&nbsp;<EuiIcon type="cross" />
      </span>
    ),
    disabled: false,
  },
  {
    id: 'monosodium_glutammate',
    name: (<span>
        monosodium_glutammate.md&nbsp;<EuiIcon type="cross" />
      </span>),
    disabled: false,
  },
  {
    id: 'elastic_link',
    name: (<span>
        elastic_link.md&nbsp;<span onClick={e =>{alert('close me?')}}><EuiIcon type="cross" /></span>
      </span>),
    disabled: false,
  },
];

export default (props) => {
  const [selectedTabId, setSelectedTabId] = useState('cobalt');

  const onSelectedTabChanged = id => {
    setSelectedTabId(id);
    if (props && props.onTabSelected)
      props.onTabSelected(id);
  };

  const renderTabs = () => {
    return tabs.map((tab, index) => (
      <EuiTab
        {...(tab.href && { href: tab.href, target: '_blank' })}
        onClick={() => onSelectedTabChanged(tab.id)}
        isSelected={tab.id === selectedTabId}
        disabled={tab.disabled}
        key={index}>
        {tab.name}
      </EuiTab>
    ));
  };

  return (
    <Fragment>
      <EuiTabs size="s">{renderTabs()}</EuiTabs>
    </Fragment>
  );
};

这样,我们点击不同的标签就能看到不同的内容了。

4. 托管

想要将我们的应用托管在GitHub,需要做一下几步:

  1. 执行npm build命令进行编译;
  2. 然后在将远程仓库中的gh-pages拉取到本地;
  3. 清空gh-pages分支;
  4. 对编译出的文件做些调整,因为编译的路径如果不做修改很多文件提示找不到;
  5. 将修改后的文件复制到gh-pages目录;
  6. 提交。

整个过程的命令如下(Linux下),唯一需要修改的就是仓库地址:

# 一下内容位于 my-app/Makefile
publish_github_pages:
    rm -rf ./build
    rm -rf ./gh-pages
    npm run build
    git clone --depth=1 https://github.com/SunnyZhou-1024/eui-markdown-editor.git --branch gh-pages ./gh-pages 2>&1 > /dev/null
    rm -rf ./gh-pages/*
    cp -R ./build/* ./gh-pages/
    sed -i -e "s/\/static/.\/static/g" ./gh-pages/index.html
    sed -i -e "s/\/favi/.\/favi/g" ./gh-pages/index.html
    sed -i -e "s/\/logo/.\/logo/g" ./gh-pages/index.html
    sed -i -e "s/\/mani/.\/mani/g" ./gh-pages/index.html
    git -C ./gh-pages add --all
    git -C ./gh-pages commit --amend --no-edit
    git -C ./gh-pages push --force origin gh-pages

5. 总结

由于篇幅所限,在本文例子中,只选取了一些关键的点来讲解,主要讲解的是从如何搭建一个React App以及React 元素如何更新,到将其部署到GitHub的整个流程。具体代码逻辑可能和一个真正的编辑器有很大出入,并且非常不完善,例如显示的内容应该来自文件而不是硬编码。

本文的目的并不是讲解如何写出漂亮的UI,这不是我擅长的;也不是深入的讲解React,这一部分我觉得React官网的文档已经非常完善了;更不是介绍如何使用EUI。本文的只是想以EUI为例,介绍如何通过现有的UI框架、工具链构建起一个可用,也还看得过去的网页应用。

本文例子代码位于个人GitHub仓库:https://github.com/zmychou/eui-markdown-editor

本文首发于个人微信公众号TensorBoy,微信扫描上方二维码或者微信搜索TensorBoy并关注,及时获取最新文章。C++ | Python | Linux | 原理 | 源码,有一起玩耍的么?

6. References

[1] https://reactjs.org/docs/getting-started.html
[2] https://create-react-app.dev/docs/getting-started/
[3] https://elastic.github.io/eui/#/

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335