JS常用工具方法(长文慎入)

如何隐藏所有指定的元素

const hide = (...el) => [...el].forEach(e => (e.style.display = 'none'))

ES5 ->
var hide = function hide() {
  for (var _len = arguments.length, el = Array(_len), _key = 0; _key < _len; _key++) {
    el[_key] = arguments[_key];
  }

  return [].concat(el).forEach(function (e) {
    return e.style.display = 'none';
  });
};

// 事例:隐藏页面上所有`<img>`元素?
hide(document.querySelectorAll('img'))

如何检查元素是否具有指定的类?

页面DOM里的每个节点上都有一个classList对象,程序员可以使用里面的方法新增、删除、修改节点上的CSS类。使用classList,程序员还可以用它来判断某个节点是否被赋予了某个CSS类。

const hasClass = (el, className) => el.classList.contains(className)

// 事例
hasClass(document.querySelector('p.special'), 'special') // true

如何切换一个元素的类?

const toggleClass = (el, className) => el.classList.toggle(className)

// 事例 移除 p 具有类`special`的 special 类
toggleClass(document.querySelector('p.special'), 'special')

如何获取当前页面的滚动位置?

const getScrollPosition = (el = window) => ({
  x: el.pageXOffset !== undefined ? el.pageXOffset : el.scrollLeft,
  y: el.pageYOffset !== undefined ? el.pageYOffset : el.scrollTop
});

// 事例
getScrollPosition(); // {x: 0, y: 200}

如何平滑滚动到页面顶部

const scrollToTop = () => {
  const c = document.documentElement.scrollTop || document.body.scrollTop;
  if (c > 0) {
    window.requestAnimationFrame(scrollToTop);
    window.scrollTo(0, c - c / 8);
  }
}

// 事例
scrollToTop()

window.requestAnimationFrame() 告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行。

requestAnimationFrame: 优势:由系统决定回调函数的执行时机。60Hz的刷新频率,那么每次刷新的间隔中会执行一次回调函数,不会引起丢帧,不会卡顿。

如何检查父元素是否包含子元素?

const elementContains = (parent, child) => parent !== child && parent.contains(child);

// 事例
elementContains(document.querySelector('head'), document.querySelector('title')); 
// true
elementContains(document.querySelector('body'), document.querySelector('body')); 
// false

如何检查指定的元素在视口中是否可见?

const elementIsVisibleInViewport = (el, partiallyVisible = false) => {
  const { top, left, bottom, right } = el.getBoundingClientRect();
  const { innerHeight, innerWidth } = window;
  return partiallyVisible
    ? ((top > 0 && top < innerHeight) || (bottom > 0 && bottom < innerHeight)) &&
        ((left > 0 && left < innerWidth) || (right > 0 && right < innerWidth))
    : top >= 0 && left >= 0 && bottom <= innerHeight && right <= innerWidth;
};

// 事例
elementIsVisibleInViewport(el); // 需要左右可见
elementIsVisibleInViewport(el, true); // 需要全屏(上下左右)可以见

如何获取元素中的所有图像?

const getImages = (el, includeDuplicates = false) => {
  const images = [...el.getElementsByTagName('img')].map(img => img.getAttribute('src'));
  return includeDuplicates ? images : [...new Set(images)];
};

// 事例:includeDuplicates 为 true 表示需要排除重复元素
getImages(document, true); // ['image1.jpg', 'image2.png', 'image1.png', '...']
getImages(document, false); // ['image1.jpg', 'image2.png', '...']

如何确定设备是移动设备还是台式机/笔记本电脑?

const detectDeviceType = () =>
  /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)
    ? 'Mobile'
    : 'Desktop';

// 事例
detectDeviceType(); // "Mobile" or "Desktop"

How to get the current URL?

const currentURL = () => window.location.href

// 事例
currentURL() // 'https://google.com'

如何创建一个包含当前URL参数的对象?

const getURLParameters = url =>
  (url.match(/([^?=&]+)(=([^&]*))/g) || []).reduce(
    (a, v) => ((a[v.slice(0, v.indexOf('='))] = v.slice(v.indexOf('=') + 1)), a),
    {}
  );

// 事例
getURLParameters('http://url.com/page?n=Adam&s=Smith'); // {n: 'Adam', s: 'Smith'}
getURLParameters('google.com'); // {}

如何将一组表单元素转化为对象?

const formToObject = form =>
  Array.from(new FormData(form)).reduce(
    (acc, [key, value]) => ({
      ...acc,
      [key]: value
    }),
    {}
  );

// 事例
formToObject(document.querySelector('#form')); 
// { email: 'test@email.com', name: 'Test Name' }

如何从对象检索给定选择器指示的一组属性?

const get = (from, ...selectors) =>
  [...selectors].map(s =>
    s
      .replace(/\[([^\[\]]*)\]/g, '.$1.')
      .split('.')
      .filter(t => t !== '')
      .reduce((prev, cur) => prev && prev[cur], from)
  );
const obj = { selector: { to: { val: 'val to select' } }, target: [1, 2, { a: 'test' }] };

// Example
get(obj, 'selector.to.val', 'target[0]', 'target[2].a'); 
// ['val to select', 1, 'test']

如何在等待指定时间后调用提供的函数?

const delay = (fn, wait, ...args) => setTimeout(fn, wait, ...args);
delay(
  function(text) {
    console.log(text);
  },
  1000,
  'later'
); 

// 1秒后打印 'later'

如何在给定元素上触发特定事件且能选择地传递自定义数据?

const triggerEvent = (el, eventType, detail) =>
  el.dispatchEvent(new CustomEvent(eventType, { detail }));

// 事例
triggerEvent(document.getElementById('myId'), 'click');
triggerEvent(document.getElementById('myId'), 'click', { username: 'bob' });

自定义事件的函数有 EventCustomEventdispatchEvent

// 向 window派发一个resize内置事件
window.dispatchEvent(new Event('resize'))


// 直接自定义事件,使用 Event 构造函数:
var event = new Event('build');
var elem = document.querySelector('#id')
// 监听事件
elem.addEventListener('build', function (e) { ... }, false);
// 触发事件.
elem.dispatchEvent(event);

tips: 如果是在 iframe 内部调用的话则记得使用 top 替换 window,它代表顶层的窗口。

CustomEvent 可以创建一个更高度自定义事件,还可以附带一些数据,具体用法如下:

var myEvent = new CustomEvent(eventname, options);
其中 options 可以是:
{
  detail: {
    ...
  },
  bubbles: true,    //是否冒泡
  cancelable: false //是否取消默认事件
}

其中 detail 可以存放一些初始化的信息,可以在触发的时候调用。其他属性就是定义该事件是否具有冒泡等等功能。

内置的事件会由浏览器根据某些操作进行触发,自定义的事件就需要人工触发。 dispatchEvent 函数就是用来触发某个事件:

element.dispatchEvent(customEvent);

上面代码表示,在 element 上面触发 customEvent 这个事件。

// add an appropriate event listener
obj.addEventListener("cat", function(e) { process(e.detail) });

// create and dispatch the event
var event = new CustomEvent("cat", {"detail":{"hazcheeseburger":true}});
obj.dispatchEvent(event);
使用自定义事件需要注意兼容性问题,而使用 jQuery 就简单多了:

// 绑定自定义事件
$(element).on('myCustomEvent', function(){});

// 触发事件
$(element).trigger('myCustomEvent');
// 此外,你还可以在触发自定义事件时传递更多参数信息:

$( "p" ).on( "myCustomEvent", function( event, myName ) {
  $( this ).text( myName + ", hi there!" );
});
$( "button" ).click(function () {
  $( "p" ).trigger( "myCustomEvent", [ "John" ] );
});

如何从元素中移除事件监听器?

const off = (el, evt, fn, opts = false) => el.removeEventListener(evt, fn, opts);

const fn = () => console.log('!');
document.body.addEventListener('click', fn);
off(document.body, 'click', fn); 

如何获得给定毫秒数的可读格式?

const formatDuration = ms => {
  if (ms < 0) ms = -ms;
  const time = {
    day: Math.floor(ms / 86400000),
    hour: Math.floor(ms / 3600000) % 24,
    minute: Math.floor(ms / 60000) % 60,
    second: Math.floor(ms / 1000) % 60,
    millisecond: Math.floor(ms) % 1000
  };
  return Object.entries(time)
    .filter(val => val[1] !== 0)
    .map(([key, val]) => `${val} ${key}${val !== 1 ? 's' : ''}`)
    .join(', ');
};

// 事例
formatDuration(1001); // '1 second, 1 millisecond'
formatDuration(34325055574); 
// '397 days, 6 hours, 44 minutes, 15 seconds, 574 milliseconds'

如何对传递的URL发出POST请求?

const httpPost = (url, data, callback, err = console.error) => {
  const request = new XMLHttpRequest();
  request.open('POST', url, true);
  request.setRequestHeader('Content-type', 'application/json; charset=utf-8');
  request.onload = () => callback(request.responseText);
  request.onerror = () => err(request);
  request.send(data);
};

const newPost = {
  userId: 1,
  id: 1337,
  title: 'Foo',
  body: 'bar bar bar'
};
const data = JSON.stringify(newPost);
httpPost(
  'https://jsonplaceholder.typicode.com/posts',
  data,
  console.log
); 

// {"userId": 1, "id": 1337, "title": "Foo", "body": "bar bar bar"}

如何将字符串复制到剪贴板?

const copyToClipboard = str => {
  const el = document.createElement('textarea');
  el.value = str;
  el.setAttribute('readonly', '');
  el.style.position = 'absolute';
  el.style.left = '-9999px';
  document.body.appendChild(el);
  const selected =
    document.getSelection().rangeCount > 0 ? document.getSelection().getRangeAt(0) : false;
  el.select();
  document.execCommand('copy');
  document.body.removeChild(el);
  if (selected) {
    document.getSelection().removeAllRanges();
    document.getSelection().addRange(selected);
  }
};

// 事例
copyToClipboard('Lorem ipsum'); 
// 'Lorem ipsum' copied to clipboard

如何确定页面的浏览器选项卡是否聚焦?

const isBrowserTabFocused = () => !document.hidden;

// 事例
isBrowserTabFocused(); // true

如何创建目录(如果不存在)?

const fs = require('fs');
const createDirIfNotExists = dir => (!fs.existsSync(dir) ? fs.mkdirSync(dir) : undefined);

// 事例
createDirIfNotExists('test'); 

请求、解析响应内容并下载

import axios from 'axios'
import { getToken } from '@/utils/auth'

const mimeMap = {
  xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
  zip: 'application/zip'
}

const baseUrl = process.env.VUE_APP_BASE_API
export function downLoadZip(str, filename) {
  var url = baseUrl + str
  axios({
    method: 'get',
    url: url,
    responseType: 'blob',
    headers: { 'Authorization': 'Bearer ' + getToken() }
  }).then(res => {
    resolveBlob(res, mimeMap.zip)
  })
}
/**
 * 解析blob响应内容并下载
 * @param {*} res blob响应内容
 * @param {String} mimeType MIME类型
 */
export function resolveBlob(res, mimeType) {
  const aLink = document.createElement('a')
  var blob = new Blob([res.data], { type: mimeType })
  // //从response的headers中获取filename, 后端response.setHeader("Content-disposition", "attachment; filename=xxxx.docx") 设置的文件名;
  var patt = new RegExp('filename=([^;]+\\.[^\\.;]+);*')
  var contentDisposition = decodeURI(res.headers['content-disposition'])
  var result = patt.exec(contentDisposition)
  var fileName = result[1]
  fileName = fileName.replace(/\"/g, '')
  aLink.href = URL.createObjectURL(blob)
  aLink.setAttribute('download', fileName) // 设置下载文件名称
  document.body.appendChild(aLink)
  aLink.click()
  document.body.appendChild(aLink)
}

一些JS方法

/**

 * 表格时间格式化

 */

export function formatDate(cellValue) {

  if (cellValue == null || cellValue == "") return "";

  var date = new Date(cellValue) 

  var year = date.getFullYear()

  var month = date.getMonth() + 1 < 10 ? '0' + (date.getMonth() + 1) : date.getMonth() + 1

  var day = date.getDate() < 10 ? '0' + date.getDate() : date.getDate() 

  var hours = date.getHours() < 10 ? '0' + date.getHours() : date.getHours() 

  var minutes = date.getMinutes() < 10 ? '0' + date.getMinutes() : date.getMinutes() 

  var seconds = date.getSeconds() < 10 ? '0' + date.getSeconds() : date.getSeconds()

  return year + '-' + month + '-' + day + ' ' + hours + ':' + minutes + ':' + seconds

}

 

/**

 * 首字母大小

 */

export function titleCase(str) {

  return str.replace(/( |^)[a-z]/g, L => L.toUpperCase())

}

/**

 * 下划转驼峰

 */

export function camelCase(str) {

  return str.replace(/-[a-z]/g, str1 => str1.substr(-1).toUpperCase())

}

/**

 * @param {number} time

 * @param {string} option

 * @returns {string}

 */

export function formatTime(time, option) {

  if (('' + time).length === 10) {

    time = parseInt(time) * 1000

  } else {

    time = +time

  }

  const d = new Date(time)

  const now = Date.now()

 

  const diff = (now - d) / 1000

 

  if (diff < 30) {

    return '刚刚'

  } else if (diff < 3600) {

    // less 1 hour

    return Math.ceil(diff / 60) + '分钟前'

  } else if (diff < 3600 * 24) {

    return Math.ceil(diff / 3600) + '小时前'

  } else if (diff < 3600 * 24 * 2) {

    return '1天前'

  }

  if (option) {

    return parseTime(time, option)

  } else {

    return (

      d.getMonth() +

      1 +

      '月' +

      d.getDate() +

      '日' +

      d.getHours() +

      '时' +

      d.getMinutes() +

      '分'

    )

  }

}

 

/**

 * @param {string} url

 * @returns {Object}

 */

export function getQueryObject(url) {

  url = url == null ? window.location.href : url

  const search = url.substring(url.lastIndexOf('?') + 1)

  const obj = {}

  const reg = /(?&=+)=(?&=*)/g

  search.replace(reg, (rs, 1, 2) => {

    const name = decodeURIComponent($1)

    let val = decodeURIComponent($2)

    val = String(val)

    obj[name] = val

    return rs

  })

  return obj

}

 

/**

 * @param {string} input value

 * @returns {number} output value

 */

export function byteLength(str) {

  // returns the byte length of an utf8 string

  let s = str.length

  for (var i = str.length - 1; i >= 0; i--) {

    const code = str.charCodeAt(i)

    if (code > 0x7f && code <= 0x7ff) s++

    else if (code > 0x7ff && code <= 0xffff) s += 2

    if (code >= 0xDC00 && code <= 0xDFFF) i--

  }

  return s

}

 

/**

 * @param {Array} actual

 * @returns {Array}

 */

export function cleanArray(actual) {

  const newArray = []

  for (let i = 0; i < actual.length; i++) {

    if (actual[i]) {

      newArray.push(actual[i])

    }

  }

  return newArray

}

 

/**

 * @param {Object} json

 * @returns {Array}

 */

export function param(json) {

  if (!json) return ''

  return cleanArray(

    Object.keys(json).map(key => {

      if (json[key] === undefined) return ''

      return encodeURIComponent(key) + '=' + encodeURIComponent(json[key])

    })

  ).join('&')

}

 

/**

 * @param {string} url

 * @returns {Object}

 */

export function param2Obj(url) {

  const search = url.split('?')[1]

  if (!search) {

    return {}

  }

  return JSON.parse(

    '{"' +

      decodeURIComponent(search)

        .replace(/"/g, '\"')

        .replace(/&/g, '","')

        .replace(/=/g, '":"')

        .replace(/+/g, ' ') +

      '"}'

  )

}

 

/**

 * @param {string} val

 * @returns {string}

 */

export function html2Text(val) {

  const div = document.createElement('div')

  div.innerHTML = val

  return div.textContent || div.innerText

}

 

/**

 * Merges two objects, giving the last one precedence

 * @param {Object} target

 * @param {(Object|Array)} source

 * @returns {Object}

 */

export function objectMerge(target, source) {

  if (typeof target !== 'object') {

    target = {}

  }

  if (Array.isArray(source)) {

    return source.slice()

  }

  Object.keys(source).forEach(property => {

    const sourceProperty = source[property]

    if (typeof sourceProperty === 'object') {

      target[property] = objectMerge(target[property], sourceProperty)

    } else {

      target[property] = sourceProperty

    }

  })

  return target

}

 

/**

 * @param {HTMLElement} element

 * @param {string} className

 */

export function toggleClass(element, className) {

  if (!element || !className) {

    return

  }

  let classString = element.className

  const nameIndex = classString.indexOf(className)

  if (nameIndex === -1) {

    classString += '' + className

  } else {

    classString =

      classString.substr(0, nameIndex) +

      classString.substr(nameIndex + className.length)

  }

  element.className = classString

}

 

/**

 * @param {string} type

 * @returns {Date}

 */

export function getTime(type) {

  if (type === 'start') {

    return new Date().getTime() - 3600 * 1000 * 24 * 90

  } else {

    return new Date(new Date().toDateString())

  }

}

 

/**

 * @param {Function} func

 * @param {number} wait

 * @param {boolean} immediate

 * @return {*}

 */

export function debounce(func, wait, immediate) {

  let timeout, args, context, timestamp, result

 

  const later = function() {

    // 据上一次触发时间间隔

    const last = +new Date() - timestamp

 

    // 上次被包装函数被调用时间间隔 last 小于设定时间间隔 wait

    if (last < wait && last > 0) {

      timeout = setTimeout(later, wait - last)

    } else {

      timeout = null

      // 如果设定为immediate===true,因为开始边界已经调用过了此处无需调用

      if (!immediate) {

        result = func.apply(context, args)

        if (!timeout) context = args = null

      }

    }

  }

 

  return function(...args) {

    context = this

    timestamp = +new Date()

    const callNow = immediate && !timeout

    // 如果延时不存在,重新设定延时

    if (!timeout) timeout = setTimeout(later, wait)

    if (callNow) {

      result = func.apply(context, args)

      context = args = null

    }

 

    return result

  }

}

 

/**

 * This is just a simple version of deep copy

 * Has a lot of edge cases bug

 * If you want to use a perfect deep copy, use lodash's _.cloneDeep

 * @param {Object} source

 * @returns {Object}

 */

export function deepClone(source) {

  if (!source && typeof source !== 'object') {

    throw new Error('error arguments', 'deepClone')

  }

  const targetObj = source.constructor === Array ? [] : {}

  Object.keys(source).forEach(keys => {

    if (source[keys] && typeof source[keys] === 'object') {

      targetObj[keys] = deepClone(source[keys])

    } else {

      targetObj[keys] = source[keys]

    }

  })

  return targetObj

}

 

/**

 * @param {Array} arr

 * @returns {Array}

 */

export function uniqueArr(arr) {

  return Array.from(new Set(arr))

}

 

/**

 * @returns {string}

 */

export function createUniqueString() {

  const timestamp = +new Date() + ''

  const randomNum = parseInt((1 + Math.random()) * 65536) + ''

  return (+(randomNum + timestamp)).toString(32)

}

 

/**

 * Check if an element has a class

 * @param {HTMLElement} elm

 * @param {string} cls

 * @returns {boolean}

 */

export function hasClass(ele, cls) {

  return !!ele.className.match(new RegExp('(\s|^)' + cls + '(\s|$)'))

}

 

/**

 * Add class to element

 * @param {HTMLElement} elm

 * @param {string} cls

 */

export function addClass(ele, cls) {

  if (!hasClass(ele, cls)) ele.className += ' ' + cls

}

 

/**

 * Remove class from element

 * @param {HTMLElement} elm

 * @param {string} cls

 */

export function removeClass(ele, cls) {

  if (hasClass(ele, cls)) {

    const reg = new RegExp('(\s|^)' + cls + '(\s|$)')

    ele.className = ele.className.replace(reg, ' ')

  }

}

 

export function makeMap(str, expectsLowerCase) {

  const map = Object.create(null)

  const list = str.split(',')

  for (let i = 0; i < list.length; i++) {

    map[list[i]] = true

  }

  return expectsLowerCase

    ? val => map[val.toLowerCase()]

    : val => map[val]

}

 

export const exportDefault = 'export default '

 

export const beautifierConf = {

  html: {

    indent_size: '2',

    indent_char: ' ',

    max_preserve_newlines: '-1',

    preserve_newlines: false,

    keep_array_indentation: false,

    break_chained_methods: false,

    indent_scripts: 'separate',

    brace_style: 'end-expand',

    space_before_conditional: true,

    unescape_strings: false,

    jslint_happy: false,

    end_with_newline: true,

    wrap_line_length: '110',

    indent_inner_html: true,

    comma_first: false,

    e4x: true,

    indent_empty_lines: true

  },

  js: {

    indent_size: '2',

    indent_char: ' ',

    max_preserve_newlines: '-1',

    preserve_newlines: false,

    keep_array_indentation: false,

    break_chained_methods: false,

    indent_scripts: 'normal',

    brace_style: 'end-expand',

    space_before_conditional: true,

    unescape_strings: false,

    jslint_happy: true,

    end_with_newline: true,

    wrap_line_length: '110',

    indent_inner_html: true,

    comma_first: false,

    e4x: true,

    indent_empty_lines: true

  }

}

 

 

 

export function isNumberStr(str) {

  return /^[+-]?(0|([1-9]\d*))(.\d+)?$/g.test(str)

}

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