乾坤框架解析 - 沙箱机制

1:如何配置进行控制

registerMicroApps方法里面调用loadApp会传入frameworkConfiguration, 这个对象是乾坤框架对外提供的api用于控制应用加载,切换,沙箱等行为
loadMicroApp方法参数会传入configuration?: FrameworkConfiguration

FrameworkConfiguration 里面可配置的属性如下:

export let frameworkConfiguration: FrameworkConfiguration = {};
ts:   FrameworkConfiguration = QiankunSpecialOpts & ImportEntryOpts & StartOpts;
QiankunSpecialOpts :
{
  prefetch
  sandbox?:
    | boolean
    | {
        strictStyleIsolation?: boolean;
        experimentalStyleIsolation?: boolean;
        patchers?: Patcher[];
      };
  /*
    with singular mode, any app will wait to load until other apps are unmouting
    it is useful for the scenario that only one sub app shown at one time
  */
  singular?: boolean | ((app: LoadableApp<any>) => Promise<boolean>);
  excludeAssetFilter?: (url: string) => boolean; // skip some scripts or links intercept, like JSONP
}
ImportEntryOpts :   
{
        fetch?: typeof window.fetch;
    getPublicPath?: (entry: Entry) => string;
    getTemplate?: (tpl: string) => string;
}

StartOpts: 
{
  urlRerouteOnly?: boolean;
}

2:在什么时候,什么地方调用运行

loader.ts 里面 加载完子应用方法 里面

export async function loadApp<T extends object>(
  app: LoadableApp<T>,
  configuration: FrameworkConfiguration = {},
  lifeCycles?: FrameworkLifeCycles<T>,
): Promise<ParcelConfigObject> {
  .......
  const { singular = false, sandbox = true, excludeAssetFilter, ...importEntryOpts } = configuration;
  // get the entry html content and script executor
  // 核心代码,加载app对应的入口文件,以及将文件处理成模板,和可执行脚本信息对象
  const { template, execScripts, assetPublicPath } = await importEntry(entry, importEntryOpts);
  ......
  // 根据appContent 返回对应的对象,这里dom对象里面是有沙箱机制的
  let element: HTMLElement | null = createElement(appContent, strictStyleIsolation);
  .......
// 创建沙箱
  if (sandbox) {
    const sandboxInstance = createSandbox(
      appName,
      containerGetter,
      Boolean(singular),
      enableScopedCSS,
      excludeAssetFilter,
    );
    // 用沙箱的代理对象作为接下来使用的全局对象
    global = sandboxInstance.proxy as typeof window;
  }
 // 执行子应用的脚本
  const scriptExports: any = await execScripts(global, !singular);
  ...........
  return parcelConfig;

}

在createElement里面, 利用shadow dom 构造独立的代码片段,类型iframe

// 创建元素,appContent 为里面的内容,strictStyleIsolation与沙箱有关
// 根据appContent 返回对应的对象,这里dom对象里面是有沙箱机制的
function createElement(appContent: string, strictStyleIsolation: boolean): HTMLElement {
  .........
  // 如果元素支持沙箱,否则创建沙箱
      if (appElement.attachShadow) {
        shadow = appElement.attachShadow({ mode: 'open' });
      } else {
        // createShadowRoot was proposed in initial spec, which has then been deprecated
        shadow = (appElement as any).createShadowRoot();
      }
      shadow.innerHTML = innerHTML;
  ..........
  return appElement;
}

shadow dom 会隔离css以及利用document不会找到里面的dom,但是对于js脚本而已并没有做到绝对的隔离,比如window.setInterval等里面的方法以及属性还是会与外界相互影响,此时乾坤框架createSandbox实现了对脚本的隔离。
沙箱分为3种:
1:singular=true,如果是单一应用切换则用LegacySandbox,
2:singular=false, 如果一个页面包含多个子应用则用ProxySandbox
3:如果浏览器不支持window.Proxy,则兼容用SnapshotSandbox

ProxySandbox - 多子应用情况

1: fakewindow + window的组合,每次new ProxySandbox() 会创建fakewindow实例作为proxy
2:set时值放到fakewindow里面,get时先从fakewindow里面取,取不到就到window里面取
2:判断是特殊属性比如不可配置,编辑,修改的属性,就直接从window里面取

export default class ProxySandbox implements SandBox {
    /** window 值变更记录 */
    private updatedValueSet = new Set<PropertyKey>();
    name: string;
    type: SandBoxType;
    proxy: WindowProxy;
    sandboxRunning = true; // 沙箱状态
    active() {
      // 记录激活的沙箱数量
      if (!this.sandboxRunning) activeSandboxCount++;
      this.sandboxRunning = true;
    }
    inactive() {
      .........
      this.sandboxRunning = false;
    }
  
    constructor(name: string) {
      // 变量配置,这里rowWindow = window
       .........
      // 将不可编辑的特殊属性记录到propertiesWithGetter
      const { fakeWindow, propertiesWithGetter } = createFakeWindow(rawWindow);
      .........
  
      const proxy = new Proxy(fakeWindow, {
        set(target: FakeWindow, p: PropertyKey, value: any): boolean {
          // 如果本实例的沙箱正在运行
          if (self.sandboxRunning) {
            .........
            // @ts-ignore
            target[p] = value;
            // 记录修改的值
            updatedValueSet.add(p);
            .........
          }
          .........
          // 在 strict-mode 下,Proxy 的 handler.set 返回 false 会抛出 TypeError,在沙箱卸载的情况下应该忽略错误
          return true;
        },
  
        get(target: FakeWindow, p: PropertyKey): any {
            // 一些特殊属性,如[window,self,top,hasOwnProperty,document] 的特殊处理以及返回
            .........
  
          // eslint-disable-next-line no-bitwise
          // 有getter的属性,直接访问window.p, 否则访问fakewindow.p
          // 如果没有不可编辑且具有getter的属性,就先从fakewindow里面取,取不到就从window里面取
          const value = propertiesWithGetter.has(p) ? (rawWindow as any)[p] : (target as any)[p] || (rawWindow as any)[p];
  
          return getTargetValue(rawWindow, value);
        },
  
        // trap in operator
        // see https://github.com/styled-components/styled-components/blob/master/packages/styled-components/src/constants.js#L12
        has() {},
        // 获取 FakeWindow || window 里面的自有属性
        getOwnPropertyDescriptor() {}
  
        // trap to support iterator with sandbox
        // FakeWindow + window 里面的不重复的属性canvcat
        ownKeys() {}
        // 首先看这个属性是从哪来的,从window里面来的就在window定义
        defineProperty() {},
        // 这里只删除 fakeWindow里面的属性
        deleteProperty() {},
      });
  
      this.proxy = proxy;
    }
  }

图示如下:

proxy.png

LegacySandbox - 单应用的情况,之后会使用ProxySandbox替代

/**
 * 基于 Proxy 实现的沙箱
 * TODO: 为了兼容性 singular 模式下依旧使用该沙箱,等新沙箱稳定之后再切换
 * 生成一个代替window对象的委托,set,get时实际操作的window对象属性同时记录操作行为,active,inactive时释放操作行为使window对象还原
 */
export default class SingularProxySandbox implements SandBox {
    /** 沙箱期间新增的全局变量 */
    private addedPropsMapInSandbox = new Map<PropertyKey, any>();
  
    /** 沙箱期间更新的全局变量 */
    private modifiedPropsOriginalValueMapInSandbox = new Map<PropertyKey, any>();
  
    /** 持续记录更新的(新增和修改的)全局变量的 map,用于在任意时刻做 snapshot */
    private currentUpdatedPropsValueMap = new Map<PropertyKey, any>();
  
    .......
  
    active() {
        .......
        // 根据之前修改的记录重新修改window的属性,即还原沙箱之前的状态
        this.currentUpdatedPropsValueMap.forEach((v, p) => setWindowProp(p, v));
        .......
    }
  
    inactive() {
        .......
      // renderSandboxSnapshot = snapshot(currentUpdatedPropsValueMapForSnapshot);
      // restore global props to initial snapshot
      // 将沙箱期间修改的属性还原为原先的属性
      this.modifiedPropsOriginalValueMapInSandbox.forEach((v, p) => setWindowProp(p, v));
      // 将沙箱期间新增的全局变量消除
      this.addedPropsMapInSandbox.forEach((_, p) => setWindowProp(p, undefined, true));
      this.sandboxRunning = false;
    }
  
    constructor(name: string) {
      .......

      const proxy = new Proxy(fakeWindow, {
        // 在fakeWindow.p = v 执行前,会将p,v增加/编辑队列记录
        set(_: Window, p: PropertyKey, value: any): boolean {
            .......
            // 新增p属性放入新增队列
            addedPropsMapInSandbox.set(p, value);
            .......

            .......
            // 如果当前 window 对象存在该属性,且 record map 中未记录过,则记录该属性初始值
            const originalValue = (rawWindow as any)[p];
            // 记录原始属性
            modifiedPropsOriginalValueMapInSandbox.set(p, originalValue);
            // 记录修改属性以及修改后的值
            currentUpdatedPropsValueMap.set(p, value);
            // 设置值
            (rawWindow as any)[p] = value;

            .......
  
          // 在 strict-mode 下,Proxy 的 handler.set 返回 false 会抛出 TypeError,在沙箱卸载的情况下应该忽略错误
          return true;
        },
  
        get(_: Window, p: PropertyKey): any {
            // 特殊属性处理
          .......
          const value = (rawWindow as any)[p];
          return getTargetValue(rawWindow, value);
        },
  
        .......
        },
      });
  
      this.proxy = proxy;
    }
  }

SnapshotSandbox- 不兼容window.Proxy的情况

基于 diff 方式实现的沙箱,用于不支持 Proxy 的低版本浏览器

  • 代理实质为window,get,set,修改的是window的属性,但是active的时候,会生成window的快照,inactive的时候会根据快照还原

3:有什么用处,出于什么原因要设计沙箱机制
页面上多个子应用会造成 全局变量等Js冲突,Css&DOM冲突:样式文件相互影响,dom可能带有相同的class,id造成选中困难
Css&DOM冲突 可以用shadow dom来解决,但是js目前只能使用ProxySandbox脚本hack

5:流程图+结构图


createSandBox.png

沙箱补丁

启动阶段补丁:patchAtBootstrapping,生成沙箱createSandbox()的时候执行,在loadApp()加载应用文件,生成shadow dom后执行,之后才是导出并执行应用的启动阶段
挂载阶段补丁:生成沙箱createSandbox()的时候导出patchAtMounting,在应用的mount阶段执行

patchAtBootstrapping 主要是对dom的创建,插入,移除等原生方法进行了一层封装,主要有插入style后的css的样式生效以及scoped的隔离逻辑,插入script后自动执行脚本功能逻辑

const basePatchers = [
    () => patchDynamicAppend(false),
  ]
// 执行并返回资源释放,原生方法还原的接口
return basePatchers .map(patch => patch()); 

patchAtMounting 除了上述patchAtBootstrapping的功能外,对Interval,addEventListener,historyListener等方法的封装

const basePatchers = [
    () => patchInterval(sandbox.proxy),
    () => patchWindowListener(sandbox.proxy),
    () => patchHistoryListener(),
    () => patchDynamicAppend(true),
  ]
// 执行并返回资源释放,原生方法还原的接口
return basePatchers .map(patch => patch());

调用时机:

loadApp() {
    .......
    // 创建沙箱
  if (sandbox) {
    const sandboxInstance = createSandbox(...); // 里面已经执行了  patchAtBootstrapping
    // 用沙箱的代理对象作为接下来使用的全局对象
    global = sandboxInstance.proxy as typeof window;

    mountSandbox = sandboxInstance.mount;

    unmountSandbox = sandboxInstance.unmount;
  }
  .......
  // 返回封装好的生命周期钩子
  return {
      bootstrap: [...],
      mount: [..., mountSandbox, ...],
      unmount: [..., unmountSandbox, ...]
  }  
}

补丁在沙箱中如何执行:

createSandbox(...) {
    sandbox = [LegacySandbox, ProxySandbox, SnapshotSandbox] // 根据条件选择里面一种

    // some side effect could be be invoked while bootstrapping, 
    // such as dynamic stylesheet injection with style-loader, especially during the development phase
    // 执行启动阶段补丁, 返回释放还原的接口列表
    const bootstrappingFreers = patchAtBootstrapping()

    return {
        proxy: sandbox.proxy,
        mount() {
            sandbox.active();

            // sideEffectsRebuilders 包含启动阶段的rebuild,和mount阶段的rebuld,将其拆分出来
            // 启动阶段的rebuild执行
            const satb = sideEffectsRebuilders.slice(0, bootstrappingFreers.length);
            satb.forEach(rebuild => rebuild());
            
            // 返回挂载阶段的释放还原的接口
            mountingFreers = patchAtMounting()

            // mount阶段的rebuld
            const satm = sideEffectsRebuilders.slice(bootstrappingFreers.length);
            satm.forEach(rebuild => rebuild());
        },
        unmount() {
            // record the rebuilders of window side effects (event listeners or timers)
            // note that the frees of mounting phase are one-off as it will be re-init at next mounting
            // 启动阶段,挂载阶段的所有释放资源,同时返回rebuild列表并记录下来,下次挂载用
            sideEffectsRebuilders = [...bootstrappingFreers, ...mountingFreers].map(free => free());

            sandbox.inactive();
        }
    }
}

dom操作的封装 :patchDynamicAppend

patchDynamicAppend 是 patchAtBootstrapping 和 patchAtMounting的核心方法

这里有一个场景,微应用加载后,执行微应用里面的脚本,我们知道由于沙盒的设置,里面的全局变量访问以proxy的方式进行,但是通过生成<script src>
加载并执行远程脚本的时候,proxy就无效了,通过js生成<style>内容(无论内联还是远程)的时候样式文件的scoped并未绑定对应微应用,这会导致样式与脚本变量会影响到全局,

为解决这个问题,乾坤在生成沙箱的时候会去mock掉createElement,apend,insertBefore

/**
 * Just hijack dynamic head append, that could avoid accidentally hijacking the insertion of elements except in head.
 * Such a case: ReactDOM.createPortal(<style>.test{color:blue}</style>, container),
 * this could made we append the style element into app wrapper but it will cause an error while the react portal unmounting, as ReactDOM could not find the style in body children list.
 */
export default function patch(
    appName: string,
    appWrapperGetter: () => HTMLElement | ShadowRoot,
    proxy: Window,
    mounting = true,
    singular = true,
    scopedCSS = false,
    excludeAssetFilter?: CallableFunction,
  ): Freer {
      // 缓存,用于free和rebuild资源
    let dynamicStyleSheetElements: Array<HTMLLinkElement | HTMLStyleElement> = [];
  
    // 重写creatElement,返回还原的方法
    const unpatchDocumentCreate = patchDocumentCreateElement();
  
    // 重写append,返回还原的方法
    //HTMLHeadElement.prototype.appendChild = rawHeadAppendChild;
    //HTMLHeadElement.prototype.removeChild = rawHeadRemoveChild;
    //HTMLBodyElement.prototype.appendChild = rawBodyAppendChild;
    //HTMLBodyElement.prototype.removeChild = rawBodyRemoveChild;
    //HTMLHeadElement.prototype.insertBefore = rawHeadInsertBefore;
    const unpatchDynamicAppendPrototypeFunctions = patchHTMLDynamicAppendPrototypeFunctions();
  
    ............
  
    // 返回释放的方法
    return function free() {
      ............
      unpatchDynamicAppendPrototypeFunctions(allMicroAppUnmounted);
      unpatchDocumentCreate(allMicroAppUnmounted);
  
      dynamicStyleSheetElements.forEach(setCachedRules);

      return function rebuild() {
        dynamicStyleSheetElements.forEach(getCachedRules);
        ............
      };
    };
  }

patchDocumentCreateElement 方法对document.createElement方法封装,如果创建的标签是script,style,link,则通过document[attachDocProxySymbol]检查当前运行环境是否在微应用内部,若是,则创建的元素绑定attachElementContainerSymbol标识,用作后续的apend,insert

// 在创建元素时,会判断当前环境是否为shadow dom且生成tag为style,script,link, 若是则将生成的元素里面注入一个标识
function patchDocumentCreateElement() {
    // 将proxy以及应用信息缓存起来,后续apend,insert操作会用上
    proxyContainerInfoMapper.set(proxy, { appName, proxy, appWrapperGetter, dynamicStyleSheetElements, singular });
  
    Document.prototype.createElement = function createElement(): HTMLElement {
        // 执行原方法
        const element = rawDocumentCreateElement.call(this, tagName, options);
        // 是否style,script,link
        if (tagName == [style,script,link]) {
          // 判断当前documnent.createElement运行环境是否为对应微应用
          const proxy = (this即document)[attachDocProxySymbol]
          const proxyContainerInfo = proxyContainerInfoMapper.get(proxy);
          if (proxyContainerInfo) {
            // 将应用信息赋值给element,如果element为style,script,link
            // 这里是为插入的时候,据此判断是否为shadow dom 插入style,script,若是,则作相关处理
            Object.defineProperty(element, attachElementContainerSymbol, {
              value: proxyContainerInfo,
              enumerable: false,
            });
          }
        }
  
        return element;
    };

    // 还原  
    return function unpatch(recoverPrototype: boolean) {
      proxyContainerInfoMapper.delete(proxy);
      if (recoverPrototype) {
        Document.prototype.createElement = rawDocumentCreateElement;
      }
    };
  }

在上面上面补丁执行之前,在生成沙盒对象ProxySandbox的时候,会对proxy下访问document做处理

// mark the symbol to document while accessing as document.createElement could know is invoked by which 
// sandbox for dynamic append patcher
    class ProxySandbox {
        get() {}
        if (p === 'document')  {
              document[attachDocProxySymbol] = proxy;
              // remove the mark in next tick, thus we can identify whether it in micro app or not
              // this approach is just a workaround, it could not cover all the complex scenarios, such as the micr app     
              // runs in the same task context with master in som case
              // fixme if you have any other good ideas
              nextTick(() => delete document[attachDocProxySymbol]);
              return document;
       }
    }

getOverwrittenAppendChildOrInsertBefore 封装原生apendChild,insertBefore方法,在此过程中,
若增加或插入的元素绑定了微应用,且绑定的应用是激活状态则
style:则将其存在数组中,将元素插入到mountDOM中,scrope绑定mountDOM
script:则加载并执行脚本,将对应注释插入到mountDOM中

// 获取重写的AppendChild, InsertBefore 方法,返回封装后的方法
// 用于挂载的时候
// 是shadow dom,且绑定的应用是激活状态,且为style,则将其存在数组中,将元素插入到mountDOM中,scoped绑定mountDOM
// 是shadow dom,且绑定的应用是激活状态,且为script,则加载并以proxy为上下文代替window执行脚本,将对应注释插入到mountDOM中
// 其他走正常流程插入
function getOverwrittenAppendChildOrInsertBefore(opts: {
    appName: string;
    proxy: WindowProxy;
    singular: boolean;
    dynamicStyleSheetElements: HTMLStyleElement[];
    appWrapperGetter: CallableFunction;
    rawDOMAppendOrInsertBefore: <T extends Node>(newChild: T, refChild?: Node | null) => T;
    scopedCSS: boolean;
    excludeAssetFilter?: CallableFunction;
  }) {
    return function appendChildOrInsertBefore(
      this: HTMLHeadElement | HTMLBodyElement,
      newChild: T,
      refChild?: Node | null,
    ) {
      let element = newChild as any;
      // 原生方法
      const { rawDOMAppendOrInsertBefore } = opts;

      // 这里传入的element可能是字符串??正常dom对象是有tagName的
      if (element.tagName) {
        ..........

        // 如果element里面有微应用相关信息,说明当前dom操作是在微应用中,在之前createElement时缓存的信息
        const storedContainerInfo = element[attachElementContainerSymbol];

        // 如果要插入的元素是shadow dom,则将shadow dom里面携带的应用相关的配置信息覆盖掉上面参数传入的配置信息
        if (storedContainerInfo) {
          // 覆盖应用信息
        }
        // 检查element对应的应用是否是shadow dom且激活状态
        ..........
  
        switch (element.tagName) {
          // 如果是样式dom
          case LINK_TAG_NAME:
          case STYLE_TAG_NAME: {
            const stylesheetElement = newChild;

            // 如果不是shadow dom或者绑定的应用没激活,或者href被excludeAssetFilter排除在外
            // 则直接走正常插入方法
            if (!invokedByMicroApp || (excludeAssetFilter && href && excludeAssetFilter(href))) {
              return rawDOMAppendOrInsertBefore.call(this, element, refChild) as T;
            }
  
            const mountDOM = appWrapperGetter();
  
  
            // 需要将新插入的style元素对应的css样式作用域绑定到mountDOM上
            if (scopedCSS) {
              css.process(mountDOM, stylesheetElement, appName);
            }
  
            // eslint-disable-next-line no-shadow
            // 将样式元素缓存起来,将来free,rebuild有用
            dynamicStyleSheetElements.push(stylesheetElement);

            // 将样式元素加入到mountDOM中
            const referenceNode = mountDOM.contains(refChild) ? refChild : null;
            return rawDOMAppendOrInsertBefore.call(mountDOM, stylesheetElement, referenceNode);
          }
  
          case SCRIPT_TAG_NAME: {
            const { src, text } = element;

            // some script like jsonp maybe not support cors which should't use execScripts
            // 如果不是shadow dom或者绑定的应用没激活,或者href被excludeAssetFilter排除在外
            // 则直接走正常插入方法
            if (!invokedByMicroApp || (excludeAssetFilter && src && excludeAssetFilter(src))) {
              return rawDOMAppendOrInsertBefore.call(this, element, refChild) as T;
            }
  
            const mountDOM = appWrapperGetter();
            const { fetch } = frameworkConfiguration;
            const referenceNode = mountDOM.contains(refChild) ? refChild : null;
            
            // 远程链接脚本执行,并触发onload事件,execScripts里面会将proxy作为执行环境上下文,import-html-entry里面
            if (src) {
              execScripts(null, [src], proxy, {
                fetch,
                strictGlobal: !singular,
                beforeExec: () => {
                  Object.defineProperty(document, 'currentScript', {
                    get(): any {
                      return element;
                    },
                    configurable: true,
                  });
                },
                // 手动触发onload事件
                success: () => {
                    element.onload(loadEvent) || element.dispatchEvent(loadEvent);
                },
                error: () => {
                    element.onerror(errorEvent) || element.dispatchEvent(errorEvent);
                },
              });
              // 将注释插入到mountDOM中
              const dynamicScriptCommentElement = document.createComment(`dynamic script ${src} replaced by qiankun`);
              return rawDOMAppendOrInsertBefore.call(mountDOM, dynamicScriptCommentElement, referenceNode);
            }
  
            // 内联脚本的执行
            execScripts(null, [`<script>${text}</script>`], proxy, {
              strictGlobal: !singular,
              success: element.onload,
              error: element.onerror,
            });
            // 将注释插入到mountDOM中
            const dynamicInlineScriptCommentElement = document.createComment('dynamic inline script replaced by qiankun');
            return rawDOMAppendOrInsertBefore.call(mountDOM, dynamicInlineScriptCommentElement, referenceNode);
          }
  
          default:
            break;
        }
      }
  
      // refChild为null,则为appendChild, 否则为insertBefore
      return rawDOMAppendOrInsertBefore.call(this, element, refChild);
    };
  }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,445评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,889评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,047评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,760评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,745评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,638评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,011评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,669评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,923评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,655评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,740评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,406评论 4 320
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,995评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,961评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,023评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,483评论 2 342