#1 从零开始制作在线 代码编辑器

上一篇
#0 从零开始制作在线 代码编辑器

目录结构和说明


目录初始结构

创建一些目录以及文件,如图1-1 所示。这里只是随手创的,也可以自定目录结构。

1-1 目录结构

目录说明

  • serval/script/harusame-dom.js 提供了创建 DOM 的工具,比createElement(tagName) 那种要稍微方便一点,创建方式借鉴了虚拟DOM。使用方式就在这里说明了,之后也不细说。
// 创建元素节点
var $node = SatoriDom.compile(e('div', {'class': 'demo', 'id': 'demo'}, '这里是文本节点: parent', [
    e('div', {'class': 'child'}, [
        e('p', '这里是文本节点: child x child')
    ])
]))

console.log($node.tagName) // DIV

// 其他可能用到的方式
function template_submit (data) {
    var $event_node = SatoriDom.compile(e('input', {'class': 'i-have-event', 'type': 'submit'}))

    $event_node.addEventListener('click', function (event) {
        console.log('You click me:' + data.content)
    })

    return SatoriDom.compile(e('form', {'class': 'this-is-a-form'}, [
        $event_node
    ]))
}

  • serval/script/harusame-template.js 存放了一些模板,把与创建DOM 相关的代码抽离出来,避免其在逻辑代码中占用很多篇幅。

  • serval/script/harusame-cursor.js 光标会被单独抽象成一个类。

  • serval/script/harusame-serval.js 绑定整个编辑器的事件,以及逻辑的处理,最重要的部分。

  • serval/style/normalize.scssnormalize.scss 官网,这里改了后缀只是方便后缀格式相同....

  • serval/style/harusame-code.scss 是目录 serval/style/code/ 下所有需要高亮的语言的样式的 入口,这样便于以后做其他语言的样式扩展。这里先有能力解决 js 的语法高亮再想着其他语言吧。

  • serval/style/harusame-serval.scss 描述了编辑器样式。

先直接描绘出成型后的编辑器


对着已有的优秀的编辑器观察,把看到的东西抽象成几部分,再根据这些编写成 DOMs 。
这里对着 Sublime Text 3 截了一张图 1-2:

1-2 Sublime Text 3

挺小也挺简单的一张图,但是包含了想做的编辑器的大部分所需 DOMs,或者称他为组件,这里说下明确当前要做的部分:

  • 行号 ( 6 ~ 15)
  • 光标 ( 第七行最后的白色竖线),与多重光标(7, 9, 15)
  • 选中行提示(比如第七行)
  • 选中内容提示(比如第九行,第 13 ~ 15 行)
    这里插入一点,以下内容会是显而易见的,但是也是编辑器组成中最最重要的,尽量把这些理所当然的也把它梳理出来,毕竟他们也需要 DOM 来显示
  • 行的内容(比如第七行的 state = {
  • 编辑器样式(比如这里深灰色,行号的灰色,非关键词的白色)
    以下是一些肉眼看不到的
  • 行能够接受键盘输入
  • 点击一个位置,光标会自动偏移到该行中,离点击字符最近的位置。
  • 行号无法选中

至于一些其他的细节:代码高亮,成对的括号提示等,这里会放在以后再做/说。

于是根据这些梳理好的内容,转换成 DOMs:

  • 编辑器容器 设置编辑器的背景颜色等,也是存放 其他容器的容器(父节点)
  • 光标容器 存放 所有光标 的元素节点
  • 行容器 存放 一行 的元素节点,包括它的行号,以及行的内容
  • 选择容器 存放比如 当前选择行,选择内容 的背景高亮的元素节点
  • 键盘输入事件接收器 正如之前所说(好像说了),一个 div 本身是不能接受键盘事件的,需要一个接受键盘事件的容器,再将事件的处理逻辑与相应的元素节点绑定。

以上就是需要的 DOMs 了,包含关系也基本没啥问题了:

文件路径 serval/index

<div id="input-container" class="input-container">
    <!-- 创建一个 serval 专属的区域,且总是铺满 编辑器的容器 -->
    <div class="serval theme-harusame">
        <!-- 包裹区域,总是由 行 的高度所撑开 -->
        <div class="serval-container">
            <!-- 所有 行 的 容器 -->
            <div class="line-container">
                <div class="line">
                        <div class="line-number-wrap">
                            <span class="line-number">1</span>
                        </div>
                       <!--  在pre中,添加使用等宽字体的 buff,使用等宽字体是为了之后,便于文字宽度的计算 -->
                        <pre class="code-wrap">
                            <!-- 因为一般放的是代码,codeMirror 是用的 span,试试 code -->
                            <code class="code-content">const PI = 3.1415</code>
                        </pre>
                </div>
                <div class="line">
                        <div class="line-number-wrap">
                            <span class="line-number">2</span>
                        </div>
                        <pre class="code-wrap">
                            <code class="code-content">console.info('PI', PI)</code>
                        </pre>
                </div>
                <div class="line">
                        <div class="line-number-wrap">
                            <span class="line-number">3</span>
                        </div>
                        <pre class="code-wrap">
                            <code class="code-content"></code>
                        </pre>
                </div>
            </div>
            <!--  接受键盘事件 的容器 的容器 -->
            <div class="inputer-container">
                <textarea class="inputer"> 
                <!-- 这里使用一个 看不见的(并不是隐藏哦) textarea 来接受键盘事件 -->
            </div>
            <!--  所有 光标 的容器 -->
            <div class="cursor-container">
                <i class="fake-cursor blink"></i>
            </div>
            <!-- 选择行 提示 -->
            <div class="selected-container">
                <div class="selected-line"></div>
            </div>
        </div>
    </div>
</div>

文件路径 serval/style/harusame-serval.css
这里就直接给了编译后的
还是挺暴力的,所有都直接嵌套,防止命名冲突

.serval {
  position: relative;
  height: 100%;
}
.serval .serval-container {
  height: 100%;
}
.serval .serval-container .line-container {
  margin-left: 50px;
  font-size: 0;
}
.serval .serval-container .line-container .line .line-number-wrap {
  position: absolute;
  left: 0;
  text-align: right;
}
.serval .serval-container .line-container .line .line-number-wrap .line-number {
  display: inline-block;
  padding-right: 15px;
  box-sizing: border-box;
  width: 50px;
  height: 20px;
  line-height: 20px;
  font-size: 12px;
}
.serval .serval-container .line-container .line .code-wrap .code-content {
  display: block;
  height: 20px;
  line-height: 20px;
  font-size: 12px;
}
.serval .serval-container .cursor-container {
  position: absolute;
  top: 0;
  margin-left: 50px;
}
.serval .serval-container .cursor-container .fake-cursor {
  display: block;
  position: absolute;
  left: 0;
  top: 0;
  width: 0;
  height: 18px;
  margin-top: 1px;
}
.serval .serval-container .selected-container {
  position: absolute;
  top: 0;
  width: 100%;
}
.serval .serval-container .selected-container .selected-line {
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  height: 20px;
}
.serval .serval-container .inputer-container {
  position: absolute;
  top: 0;
  margin-left: 50px;
}
.serval .serval-container .inputer-container .inputer {
  position: absolute;
  left: 0;
  top: 0;
  border: 0;
  resize: none;
  width: 0;
  height: 20px;
  line-height: 20px;
  opacity: 0;
}

.theme-harusame {
  background-color: rgba(0, 0, 0, 0.8);
}
.theme-harusame .serval-container .line-container {
  font-family: Consolas;
}
.theme-harusame .serval-container .line-container .line .line-number-wrap .line-number {
  color: white;
  cursor: default;
}
.theme-harusame .serval-container .line-container .line .code-wrap {
  cursor: text;
}
.theme-harusame .serval-container .line-container .line .code-wrap .code-content {
  font-family: Consolas;
  color: white;
}
.theme-harusame .serval-container .cursor-container .fake-cursor {
  border-right: 1px solid rgba(255, 255, 255, 0.9);
}
.theme-harusame .serval-container .selected-container .selected-line {
  background-color: rgba(255, 255, 255, 0.15);
}

效果图,见图1-3 ,嗯嗯,不错的感觉。

1-3 效果图

HTML 方面应该没有什么问题,就把它们转换成为模板,写入 template.js中,并且删掉 index.html 中刚才写的 HTML标签。

文件路径 serval/script/harusame-template.js

;
(function () {
    Template = {
        /**
         * 编辑器
         */
        editor: function () {
            var $line_container = SatoriDom.compile(e('div', {'class': 'line-container'}))
            var $inputer_container = SatoriDom.compile(e('div', {'class': 'inputer-container'}))
            var $cursor_container = SatoriDom.compile(e('div', {'class': 'cursor-container'}))
            var $selected_container = SatoriDom.compile(e('div', {'class': 'selected-container'}))

            var $serval_container = SatoriDom.compile(e('div', {'class': 'serval-container'}, [
                $inputer_container,
                $selected_container,
                $cursor_container,
                $line_container
            ]))

            var $fragment = SatoriDom.compile(
                e('div', {'class': 'serval theme-harusame'}, [
                    $serval_container
                ])
            )

            return {
                $editor: $fragment,
                nodes: {
                    $serval_container: $serval_container,
                    $line_container: $line_container,
                    $inputer_container: $inputer_container,
                    $cursor_container: $cursor_container,
                    $selected_container: $selected_container
                }
            }
        },

        /**
         * 行
         * @param line_number {string} 行号
         * @param initial_content {string} 该行初始内容
         */
        line: function (params) {
            console.info('params', params)
            return SatoriDom.compile(
                e('div', {'class': 'line'}, [
                    e('div', {'class': 'line-number-wrap'}, [
                        e('span', {'class': 'line-number'}, params.line_number)
                    ]),
                    e('div', {'class': 'code-wrap'}, [
                        e('code', {'class': 'code-content'}, params.initial_content || '')
                    ])
                ])
            )
        },

        /**
         * 当前选择行
         */
        selectedLine: function () {
            return SatoriDom.compile(
                e('div', {'class': 'selected-line'})
            )
        },

        /**
         * 光标
         */
        cursor: function () {
            return SatoriDom.compile(
                e('i', {'class': 'fake-cursor blink'})
            )
        },

        /**
         * 键盘事件接收器
         */
        inputer: function () {
            return SatoriDom.compile(
                e('textarea', {'class': 'inputer'})
            )
        }
    }

    window.Template = Template
})()
文件路径 serval/script/cursor.js

;
(function () {
    var Cursor = function () {
        this.$ref = null

        this._generateCursor()
    }

    Cursor.prototype = {
        constructor: Cursor,

        /**
         * 创建一个游标对象
         */
        _generateCursor: function () {
            this.$ref = SatoriDom.compile(
                e('i', {'class': 'fake-cursor'})
            )
        }
    }

    window.Cursor = Cursor
})()
文件路径 serval/script/serval.js

;
(function () {
    /**
     * 1. 存放所有的存在游标
     */
    var Serval = function (config) {
        /* 1 */
        this.cursor_list = []

        this._generateEditor(config)
        this._generateInputer()
        this._generateCursor()
        this._generateSelectedLine()

        this._generateLine() // 测试用!!!
    }

    Serval.prototype = {
        constructor: Serval,

        /**
         * 生成编辑器的主要 DOM结构
         */
        _generateEditor: function (config) {
            var temp = Template.editor()
            var nodes = temp.nodes

            this.$serval_container = nodes.$serval_container
            this.$line_container = nodes.$line_container
            this.$inputer_container = nodes.$inputer_container
            this.$cursor_container = nodes.$cursor_container
            this.$selected_container = nodes.$selected_container

            config['editor-container'].appendChild(temp.$editor)
        },

        /**
         * 生成键盘事件接收器,并渲染
         */
        _generateInputer: function () {
            var $inputer = Template.inputer()

            this.$inputer = $inputer
            this.$inputer_container.appendChild($inputer)
        },

        /**
         * 生成一个光标,并渲染
         * 1. 创建 cursor 实例
         * 2. 持久化该 cursor 实例
         * 3. 得到该 cursor 实例的元素节点,渲染到 光标容器 中
         */
        _generateCursor: function () {
            var cursor = new Cursor() /* 1 */

            this.cursor_list.push(cursor) /* 1 */

            this.$cursor_container.appendChild(cursor.$ref) /* 3 */
        },

        /**
         * 生成一行,并渲染
         */
        _generateLine: function () {
            var $line = Template.line({line_number: '1', initial_content: '初始化内容'})

            this.$line_container.appendChild($line)
        },

        /**
         * 生成当前选择行的背景颜色提示,并渲染
         */
        _generateSelectedLine: function () {
            var $selected_line = Template.selectedLine()

            this.$selected_line = $selected_line
            this.$selected_container.appendChild($selected_line)
        }
    }

    window.Serval = Serval
})()

效果图,见图1-4

1-4 效果图

嗯嗯,好像没什么毛病...
随手框选了一下,发现,这几个字没法选中,有点不符合预期,在之后做选取内容时,肯定会坑,所以立马填了。
额...因为在当前层叠上下文中,选择行<.selected_container> 的层叠样式最大(他在最下面) 并且 他有position: absolute 的属性,导致覆盖了<.line_container>,所以

  1. template.js 中调整一下位置,令他们的覆盖情况更合逻辑。
  2. <.line_container> 添加一个 position: relative 属性
  3. 另外2的副作用就是,由于 <.line_container>具备了定位属性,行的 left: 0,得手工设置为left: -50px了,不然布局会乱哦

做这些理论上就可以了,当然如果懒得改的话,设置z-indexpointer-events之类的也可以,这里就不记了:

文件路径 serval/script/harusame-template.js
改成这样 ↓

var $fragment = SatoriDom.compile(
    e('div', {'class': 'serval theme-harusame'}, [
        e('div', {'class': 'serval-container'}, [
            $inputer_container,
            $selected_container,
            $cursor_container,
            $line_container
        ])
    ])
)
文件路径 serval/style/harusame-serval.scss

.line-container {
    // 添加一条
    position: relative;
}

.line-number-wrap {
    // 添加一条
    left: -50px;
    // 这里的 50px 取决于 .line-number 的 width 属性
}

光标


先说说光标的逻辑。
可以在 #0 节的例子中了解到

// ...
... substring(0, logicalX) ...
// ...

编辑器实际上通过 字符的位置 来添加,删除文本/代码,这里所说的字符的位置,我自作主张把它称为 逻辑位置,对应到 Cursor实例 中,给他命名为 logical,相应地,也有 物理位置(视图位置),对应到 Cursor实例 的DOM 中,给他命名为 psysical

logicalY 决定了光标在编辑器中的 行号
psysicalY 决定了光标 DOM 的 top
logicalX 决定了光标在编辑器中的 列号,或者说 第 logicalX 个字符
psysicalX 决定了光标 DOM 的 left

除了这些,光标还需要一个保存选区的属性,这里命名为selection

selection 是一个对象,它包含一个起始点坐标start,与一个终点坐标end

举个栗子,对于图 1-5:

1-5

为了计数方便,删除了部分缩进,以及这里的缩进都是 4个空格。
有以下光标:

  • 第三行的光标 cursor_1
  • 第四行的光标 cursor_2
  • 第五行的光标 cursor_3
  • 第六行的光标 cursor_4

它们分别有以下属性:
(如果看懂了文字说明,以下代码就随便看看)

cursor_1 = {
    logicalY: 2, /* 从零开始计算 */
    psysicalY: 40, /* 约定一行的高度为 20px */

    logicalX: 2, /* 从零开始计算 */
    psysicalX: 14, 
    /* 字符'l',字符'e'的宽度,通过标尺类工具可以得到一个字符(英文)的宽度约等于 7px
     * 手工写死的字符宽度面对不同的情况或者说样式上变化,会比较难维护
     * 接下来马上会说怎么使用浏览器的计算,虽然也不怎么优雅......_(:3」∠)...
     * /

    selection: null
}

cursor_2 = {
    logicalY: 3, 
    psysicalY: 60, 

    logicalX: 0, 
    psysicalX: 0, 

    selection: null
}

cursor_3 = {
    logicalY: 4, 
    psysicalY: 80, 

    logicalX: 5, 
    psysicalX: 35, // 5 * 7

    selection = {
        start: {
            logicalY: 4,
            psysicalY: 80,
            logicalX: 0,
            psysicalX: 0
        },

       /* end: {}
        * 写到这里的时候发现,end 的位置总是与当前位置相同,于是决定不要这个冗余的部分,
        * 虽然与 start 相对应的 end 缺失了,可能会 '引起不适',
        * 但是暂时觉得没必要多出一份冗余数据
        * 所以这里改变以下 selection的写法 ↓,把 selection 删了,只留下 selection_start
        */
    }

    selection_start = {
        logicalY: 4,
        psysicalY: 80,
        logicalX: 0,
        psysicalX: 0
    }
}

cursor_4 = {
    logicalY: 5, 
    psysicalY: 100,
    logicalX: 4,
    /*
     * 刚好四个空格,观察仔细会发现,图1-5 的距离不会是 4格,
     * 这里因为偷懒用了 tab进行缩进,导致如此。
     * 不要在意细节..._(:3」∠)...啊,不对,编程还是得很注意细节的
     */

    psysicalX: 28, /* 空格也约为 7px */

    selection_start = {
        logicalY: 5,
        psysicalY: 100,
        logicalX: 4,
        psysicalX: 28
    }
}

如果看完了甚至没看上面的代码,都可以 显然 得到:

psysicalY = line_height * logicalY
logicalY = psysicalY / line_height
$current_line = document.querySelector(SIGN_LINE + logicalY)

psysicalX = single_byte_length * 7 + double_byte_length * 12
logicalX = single_byte_length + double_byte_length

  • 这里的 line_height,不是指 css 中的 line-height 哦,是指 该行的高度

  • 这里的SIGN_LINE,是一个自己约定的名字,这里倾向于使用id,而不是class,个人觉得理由如下:

    1.每行都是唯一的
    2.性能稍微高一点
    3.可以利用锚点方便地跳转到该行位置

比如 github 中的代码编辑器(也可以看 codeMirror

github中的代码编辑器

  • single_byte_length 这个变量名字译为单字节字符的长度(数量),同理double_byte_length 变量名字译为双字节字符的长度(数量)。
    (这个名字我不清楚对不对,之后再查阅下文献。

为什么这样分呢?
因为在计算光标的psysicalX时,需要计算字符的 宽度,所以根据宽度来分。
font-size: 12px 的前提下,
单字节字符的宽度,比如数字,英文字符,半角标点符号
a b c 1 2 3 . , /
等等 都约为 7px

双字节字符的宽度,比如汉字,日语,全角标点符号
你 好 啊 こ は お ア 。,、
等等 都约为 12px

说明就到这里。
之所以把这些类似公式一样的东西列出来,就是想说它们之间总是一一对应,改变其中一个,另外一个也相应地改变。这个情况,是不是有点类似于双向绑定的概念呢。提到双向绑定,在如今,前端大佬们有个比较好的处理方法就是使用Object.defineProperty哦。

接下来终于可以开始写代码。

计算光标位置

首先 先去serval/script/harusame-serval.js 中提供事件

文件位置 serval/script/harusame-serval.js
为了能显示更多的有效内容,先把写过的代码在这里删掉了。

;
(function () {
    var Serval = function (config) {
        this._bindMouseEvent()
    }

    Serval.prototype = {
        /**
         * 绑定各种鼠标事件
         */
        _bindMouseEvent: function () {
            var self = this

            /**
             * addEventListener 是指自己写的方法,见最下面
             * mousedown 时,就对光标位置进行计算
             */
            addEventListener(self.$serval_container, 'mousedown', function (event) {
                console.log(event) // 先看看 event 中有什么好用的属性。
            })
        },
    }

    /**
     * 可能会对 addEventListener 进行一些兼容处理
     * 实际上并没有处理,不过也好,至少留一条后路...
     */
    function addEventListener (v_el, v_type, v_callback) {
        v_el.addEventListener(v_type, v_callback)
    }

    window.Serval = Serval
})()

先看看 event 中有什么好用的属性。
这里说一下由于一些事件在不同的浏览器行为是不同的,如果要兼容某个版本的浏览器,所以最好调试的时候看看预先各个浏览器的事件行为。这里在 Firefox 以及 Google 是没什么差异的,所以就不截图了。

onmousedown 下的 event

这里可以看到有二个 layer 前缀的属性,它总是能得到 相对于 event.target 元素的 点击位置。这个特性就很好用!

于是决定使用 event.layerYevent.layerX 来进行光标位置的计算!
这里要插一句_(:3」∠)...,在 demo 中 Y的位置 是使用 event.target.id.split[SIGN][1]来获得的,但是在以后的开发中,记得会出现一些麻烦的判断,所以这里用 layerY 尝试一下。嘛,各有利弊,layerY的话,也可能得考虑padding margin height line-heightcss造成的计算偏差。

位置: serval/script/harusame-serval.js
为了突出主要说明部分,删掉了其他代码

;
(function () {
    var Serval = function (config) {
        this._bindMouseEvent()
    }

    Serval.prototype = {
        constructor: Serval,

        /**
         * 绑定各种鼠标事件
         */
        _bindMouseEvent: function () {
            var self = this

            /**
             * addEventListener 是指自己写的方法,见最下面
             * 当 mousedown 时,就对光标位置进行计算
             */
            addEventListener(self.$serval_container, 'mousedown', function (event) {
                self.allocTask(function (v_cursor) {
                    v_cursor.psysicalY = event.layerY
                    v_cursor.psysicalX = event.layerX
                })
            })
        },

        /**
         * 为所有光标分配任务
         * 对光标操作时,统一通过这个接口
         * 这里约定必须传入一个回调函数,所以不使用 v_task && v_task() 进行判断
         */
        allocTask: function (v_task) {
            var self = this

            for (var i = 0, len = self.cursor_list.length; i < len; i++) {
                v_task(self.cursor_list[i])
            }
        },
    }
})()

做好了入口,接下来去 serval/script/harusame-cursor.js 写逻辑哦。
先写logicalYpsysicalY部分

文件位置 serval/script/harusame-cursor.js

;
(function () {
    /**
     * 这里先约(写)定(死) 行高
     */
    var LINE_HEIGHT = 20

    /**
     * 1. 光标本身的元素节点
     * 2. 光标所在行的元素节点
     */
    var Cursor = function (config) {
        this.$ref = null /* 1 */
        this.$line = null /* 2 */
        this._logicalY = 0
        this._logicalX = 0
        this._psysicalY = 0
        this._psysicalX = 0

        this.selection_start = null

        this._generateCursor()
        this._setObserver()
    }

    Cursor.prototype = {
        constructor: Cursor,

        /**
         * 创建一个游标对象
         */
        _generateCursor: function () {
            this.$ref = SatoriDom.compile(
                e('i', {'class': 'fake-cursor'})
            )
        },

        /**
         * 绑定 逻辑位置 与 物理位置 之间的关系
         */
        _setObserver: function () {
            /**
             * 这里的 self 由于也是 js关键字,所以会高亮
             * self 原本指向 window,一般用不到
             */
            var self = this

            /**
             * 1. 这里赋值的是  _logicalY 哦,下面也是
             * 2. 更新 psysicalY 的值
             * 3. 更新 DOM 位置
             * 4. 写到这里发现有点问题......
             */
            Object.defineProperty(self, 'logicalY', {
                set: function (v_logicalY) {
                    self._logicalY = v_logicalY /* 1 */
                    self._psysicalY = self.calcPsysicalY(v_logicalY) /* 2 */
                    self.setY(self._psysicalY) /* 3 */

                    self.$line = document.getElementById(LINE) /* 4 */
                },

                get: function () {
                    return self._logicalY
                }
            })

            Object.defineProperty(self, 'psysicalY', {
                set: function (v_psysicalY) {
                    self.logicalY = self.calcLogicalY(v_psysicalY)
                },

                get: function () {
                    return self._psysicalY
                }
            })
        },

        setY: function (v_psysicalY) {
            this.$ref.style.top = v_psysicalY + 'px'
        },

        /**
         * 计算 物理 Y
         */
        calcPsysicalY: function (v_logicalY) {
            return v_logicalY * LINE_HEIGHT
        },

        /**
         * 计算 逻辑 Y
         */
        calcLogicalY: function (v_psysicalY) {
            return parseInt(v_psysicalY / LINE_HEIGHT)
        }
    }

    window.Cursor = Cursor
})()

这里可以看看效果,截图软件截不到鼠标_(:3」∠)...。

但是也注意到了设计上的问题:在self.logicalY 中,需要用到 $line,这个东西不应该分在Cursor中,毕竟他不属于光标,以及LINE_HEIGHT 属性,也不属于。甚至在接下来的 self.logicalX 中,也需要用到 $line,这个时候,不如把与 有关的,再单独抽象成一个类会比较合适。

休息会

上一篇
#0 从零开始制作在线 代码编辑器

下一篇
#2 从零开始制作在线 代码编辑器

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

推荐阅读更多精彩内容