Weex 中別具匠心的 JS Framework

前言

Weex為了提高Native的極致性能,做了很多優化的工作

為了達到所有頁面在用戶端達到秒開,也就是網絡(JS Bundle下載)和首屏渲染(展現在用戶第一屏的渲染時間)時間和小于1s。

手淘團隊在對Weex進行性能優化時,遇到了很多問題和挑戰:

JS Bundle下載慢,壓縮后60k左右大小的JS Bundle,在全網環境下,平均下載速度大于800ms(在2G/3G下甚至是2s以上)。
JS和Native通信效率低,拖慢了首屏加載時間。

最終想到的辦法就是把JSFramework內置到SDK中,達到極致優化的作用。

  1. 客戶端訪問Weex頁面時,首先會網絡請求JS Bundle,JS Bundle被加載到客戶端本地后,傳入JSFramework中進行解析渲染。JS Framework解析和渲染的過程其實是根據JS Bundle的數據結構創建Virtual DOM 和數據綁定,然后傳遞給客戶端渲染。
    由于JSFramework在本地,所以就減少了JS Bundle的體積,每個JS Bundle都可以減少一部分體積,Bundle里面只保留業務代碼。每個頁面下載Bundle的時間都可以節約10-20ms。如果Weex頁面非常多,那么每個頁面累計起來節約的時間就很多了。 Weex這種默認就拆包加載的設計,比ReactNative強,也就不需要考慮一直困擾ReactNative頭疼的拆包的問題了。

  2. 整個過程中,JSFramework將整個頁面的渲染分拆成一個個渲染指令,然后通過JS Bridge發送給各個平臺的RenderEngine進行Native渲染。因此,盡管在開發時寫的是 HTML / CSS / JS,但最后在各個移動端(在iOS上對應的是iOS的Native UI、在Android上對應的是Android的Native UI)渲染后產生的結果是純Native頁面。
    由于JSFramework在本地SDK中,只用在初始化的時候初始化一次,之后每個頁面都無須再初始化了。也進一步的提高了與Native的通信效率。

JSFramework在客戶端的作用在前幾篇文章里面也提到了。它的在Native端的職責有3個:

  1. 管理每個Weex instance實例的生命周期。
  2. 不斷的接收Native傳過來的JS Bundle,轉換成Virtual DOM,再調用Native的方法,構建頁面布局。
  3. 響應Native傳過來的事件,進行響應。

接下來,筆者從源碼的角度詳細分析一下Weex 中別具匠心的JS Framework是如何實現上述的特性的。

目錄

  • 1.Weex JS Framework 初始化
  • 2.Weex JS Framework 管理實例的生命周期
  • 3.Weex JS Framework 構建Virtual DOM
  • 4.Weex JS Framework 處理Native觸發的事件
  • 5.Weex JS Framework 未來可能做更多的事情

一. Weex JS Framework 初始化

分析Weex JS Framework 之前,先來看看整個Weex JS Framework的代碼文件結構樹狀圖。以下的代碼版本是0.19.8。


weex/html5/frameworks
    ├── index.js
    ├── legacy   
    │     ├── api         // 定義 Vm 上的接口
    │     │   ├── methods.js        // 以$開頭的一些內部方法
    │     │   └── modules.js        // 一些組件的信息
    │     ├── app        // 頁面實例相關代碼
    │     │   ├── bundle            // 打包編譯的主代碼
    │     │   │     ├── bootstrap.js
    │     │   │     ├── define.js
    │     │   │     └── index.js  // 處理jsbundle的入口
    │     │   ├── ctrl              // 處理Native觸發回來方法
    │     │   │     ├── index.js
    │     │   │     ├── init.js
    │     │   │     └── misc.js
    │     │   ├── differ.js        // differ相關的處理方法
    │     │   ├── downgrade.js     //  H5降級相關的處理方法
    │     │   ├── index.js
    │     │   ├── instance.js      // Weex實例的構造函數
    │     │   ├── register.js      // 注冊模塊和組件的處理方法
    │     │   ├── viewport.js
    │     ├── core       // 數據監聽相關代碼,ViewModel的核心代碼
    │     │   ├── array.js
    │     │   ├── dep.js
    │     │   ├── LICENSE
    │     │   ├── object.js
    │     │   ├── observer.js
    │     │   ├── state.js
    │     │   └── watcher.js
    │     ├── static     // 一些靜態的方法
    │     │   ├── bridge.js
    │     │   ├── create.js
    │     │   ├── life.js
    │     │   ├── map.js
    │     │   ├── misc.js
    │     │   └── register.js
    │     ├── util        // 工具函數如isReserved,toArray,isObject等方法
    │     │   ├── index.js
    │     │   └── LICENSE
    │     │   └── shared.js
    │     ├── vm         // 組件模型相關代碼
    │     │   ├── compiler.js     // ViewModel模板解析器和數據綁定操作
    │     │   ├── directive.js    // 指令編譯器
    │     │   ├── dom-helper.js   // Dom 元素的helper
    │     │   ├── events.js       // 組件的所有事件以及生命周期
    │     │   └── index.js        // ViewModel的構造器和定義
    │     ├── config.js
    │     └── index.js // 入口文件
    └── vanilla
          └── index.js

還會用到runtime文件夾里面的文件,所以runtime的文件結構也梳理一遍。


weex/html5/runtime
    ├── callback-manager.js
    ├── config.js  
    ├── handler.js 
    ├── index.js 
    ├── init.js 
    ├── listener.js 
    ├── service.js 
    ├── task-center.js 
    └── vdom  
          ├── comment.js        
          ├── document.js 
          ├── element-types.js 
          ├── element.js 
          ├── index.js 
          ├── node.js 
          └── operation.js 



接下來開始分析Weex JS Framework 初始化。

Weex JS Framework 初始化是從對應的入口文件是 html5/render/native/index.js


import { subversion } from '../../../package.json'
import runtime from '../../runtime'
import frameworks from '../../frameworks/index'
import services from '../../services/index'

const { init, config } = runtime
config.frameworks = frameworks
const { native, transformer } = subversion

// 根據serviceName注冊service
for (const serviceName in services) {
  runtime.service.register(serviceName, services[serviceName])
}

// 調用runtime里面的freezePrototype()方法,防止修改現有屬性的特性和值,并阻止添加新屬性。
runtime.freezePrototype()

// 調用runtime里面的setNativeConsole()方法,根據Native設置的logLevel等級設置相應的Console
runtime.setNativeConsole()

// 注冊 framework 元信息
global.frameworkVersion = native
global.transformerVersion = transformer

// 初始化 frameworks
const globalMethods = init(config)

// 設置全局方法
for (const methodName in globalMethods) {
  global[methodName] = (...args) => {
    const ret = globalMethods[methodName](...args)
    if (ret instanceof Error) {
      console.error(ret.toString())
    }
    return ret
  }
}




上述方法中會調用init( )方法,這個方法就會進行JS Framework的初始化。

init( )方法在weex/html5/runtime/init.js里面。



export default function init (config) {
  runtimeConfig = config || {}
  frameworks = runtimeConfig.frameworks || {}
  initTaskHandler()

  // 每個framework都是由init初始化,
  // config里面都包含3個重要的virtual-DOM類,`Document`,`Element`,`Comment`和一個JS bridge 方法sendTasks(...args)
  for (const name in frameworks) {
    const framework = frameworks[name]
    framework.init(config)
  }

  // @todo: The method `registerMethods` will be re-designed or removed later.
  ; ['registerComponents', 'registerModules', 'registerMethods'].forEach(genInit)

  ; ['destroyInstance', 'refreshInstance', 'receiveTasks', 'getRoot'].forEach(genInstance)

  adaptInstance('receiveTasks', 'callJS')

  return methods
}



在初始化方法里面傳入了config,這個入參是從weex/html5/runtime/config.js里面傳入的。


import { Document, Element, Comment } from './vdom'
import Listener from './listener'
import { TaskCenter } from './task-center'

const config = {
  Document, Element, Comment, Listener,
  TaskCenter,
  sendTasks (...args) {
    return global.callNative(...args)
  }
}

Document.handler = config.sendTasks

export default config



config里面包含Document,Element,Comment,Listener,TaskCenter,以及一個sendTasks方法。

config初始化以后還會添加一個framework屬性,這個屬性是由weex/html5/frameworks/index.js傳進來的。


import * as Vanilla from './vanilla/index'
import * as Vue from 'weex-vue-framework'
import * as Weex from './legacy/index'
import Rax from 'weex-rax-framework'

export default {
  Vanilla,
  Vue,
  Rax,
  Weex
}

init( )獲取到config和config.frameworks以后,開始執行initTaskHandler()方法。


import { init as initTaskHandler } from './task-center'

initTaskHandler( )方法來自于task-center.js里面的init( )方法。


export function init () {
  const DOM_METHODS = {
    createFinish: global.callCreateFinish,
    updateFinish: global.callUpdateFinish,
    refreshFinish: global.callRefreshFinish,

    createBody: global.callCreateBody,

    addElement: global.callAddElement,
    removeElement: global.callRemoveElement,
    moveElement: global.callMoveElement,
    updateAttrs: global.callUpdateAttrs,
    updateStyle: global.callUpdateStyle,

    addEvent: global.callAddEvent,
    removeEvent: global.callRemoveEvent
  }
  const proto = TaskCenter.prototype

  for (const name in DOM_METHODS) {
    const method = DOM_METHODS[name]
    proto[name] = method ?
      (id, args) => method(id, ...args) :
      (id, args) => fallback(id, [{ module: 'dom', method: name, args }], '-1')
  }

  proto.componentHandler = global.callNativeComponent ||
    ((id, ref, method, args, options) =>
      fallback(id, [{ component: options.component, ref, method, args }]))

  proto.moduleHandler = global.callNativeModule ||
    ((id, module, method, args) =>
      fallback(id, [{ module, method, args }]))
}


這里的初始化方法就是往prototype上11個方法:createFinish,updateFinish,refreshFinish,createBody,addElement,removeElement,moveElement,updateAttrs,updateStyle,addEvent,removeEvent。

如果method存在,就用method(id, ...args)方法初始化,如果不存在,就用fallback(id, [{ module: 'dom', method: name, args }], '-1')初始化。

最后再加上componentHandler和moduleHandler。

initTaskHandler( )方法初始化了13個方法(其中2個handler),都綁定到了prototype上


    createFinish(id, [{ module: 'dom', method: createFinish, args }], '-1')
    updateFinish(id, [{ module: 'dom', method: updateFinish, args }], '-1')
    refreshFinish(id, [{ module: 'dom', method: refreshFinish, args }], '-1')
    createBody:(id, [{ module: 'dom', method: createBody, args }], '-1')

    addElement:(id, [{ module: 'dom', method: addElement, args }], '-1')
    removeElement:(id, [{ module: 'dom', method: removeElement, args }], '-1')
    moveElement:(id, [{ module: 'dom', method: moveElement, args }], '-1')
    updateAttrs:(id, [{ module: 'dom', method: updateAttrs, args }], '-1')
    updateStyle:(id, [{ module: 'dom', method: updateStyle, args }], '-1')

    addEvent:(id, [{ module: 'dom', method: addEvent, args }], '-1')
    removeEvent:(id, [{ module: 'dom', method: removeEvent, args }], '-1')

    componentHandler(id, [{ component: options.component, ref, method, args }]))
    moduleHandler(id, [{ module, method, args }]))

回到init( )方法,處理完initTaskHandler()之后有一個循環:


  for (const name in frameworks) {
    const framework = frameworks[name]
    framework.init(config)
  }

在這個循環里面會對frameworks里面每個對象調用init方法,入參都傳入config。

比如Vanilla的init( )實現如下:


function init (cfg) {
  config.Document = cfg.Document
  config.Element = cfg.Element
  config.Comment = cfg.Comment
  config.sendTasks = cfg.sendTasks
}

Weex的init( )實現如下:


export function init (cfg) {
  config.Document = cfg.Document
  config.Element = cfg.Element
  config.Comment = cfg.Comment
  config.sendTasks = cfg.sendTasks
  config.Listener = cfg.Listener
}

初始化config以后就開始執行genInit


['registerComponents', 'registerModules', 'registerMethods'].forEach(genInit)


function genInit (methodName) {
  methods[methodName] = function (...args) {
    if (methodName === 'registerComponents') {
      checkComponentMethods(args[0])
    }
    for (const name in frameworks) {
      const framework = frameworks[name]
      if (framework && framework[methodName]) {
        framework[methodName](...args)
      }
    }
  }
}

methods默認有3個方法


const methods = {
  createInstance,
  registerService: register,
  unregisterService: unregister
}


除去這3個方法以外都是調用framework對應的方法。



export function registerComponents (components) {
  if (Array.isArray(components)) {
    components.forEach(function register (name) {
      /* istanbul ignore if */
      if (!name) {
        return
      }
      if (typeof name === 'string') {
        nativeComponentMap[name] = true
      }
      /* istanbul ignore else */
      else if (typeof name === 'object' && typeof name.type === 'string') {
        nativeComponentMap[name.type] = name
      }
    })
  }
}


上述方法就是注冊Native的組件的核心代碼實現。最終的注冊信息都存在nativeComponentMap對象中,nativeComponentMap對象最初里面有如下的數據:


export default {
  nativeComponentMap: {
    text: true,
    image: true,
    container: true,
    slider: {
      type: 'slider',
      append: 'tree'
    },
    cell: {
      type: 'cell',
      append: 'tree'
    }
  }
}



接著會調用registerModules方法:


export function registerModules (modules) {
  /* istanbul ignore else */
  if (typeof modules === 'object') {
    initModules(modules)
  }
}


initModules是來自./frameworks/legacy/app/register.js,在這個文件里面會調用initModules (modules, ifReplace)進行初始化。這個方法里面是注冊Native的模塊的核心代碼實現。

最后調用registerMethods



export function registerMethods (methods) {
  /* istanbul ignore else */
  if (typeof methods === 'object') {
    initMethods(Vm, methods)
  }
}

initMethods是來自./frameworks/legacy/app/register.js,在這個方法里面會調用initMethods (Vm, apis)進行初始化,initMethods方法里面是注冊Native的handler的核心實現。

當registerComponents,registerModules,registerMethods初始化完成之后,就開始注冊每個instance實例的方法


['destroyInstance', 'refreshInstance', 'receiveTasks', 'getRoot'].forEach(genInstance)

這里會給genInstance分別傳入destroyInstance,refreshInstance,receiveTasks,getRoot四個方法名。


function genInstance (methodName) {
  methods[methodName] = function (...args) {
    const id = args[0]
    const info = instanceMap[id]
    if (info && frameworks[info.framework]) {
      const result = frameworks[info.framework][methodName](...args)

      // Lifecycle methods
      if (methodName === 'refreshInstance') {
        services.forEach(service => {
          const refresh = service.options.refresh
          if (refresh) {
            refresh(id, { info, runtime: runtimeConfig })
          }
        })
      }
      else if (methodName === 'destroyInstance') {
        services.forEach(service => {
          const destroy = service.options.destroy
          if (destroy) {
            destroy(id, { info, runtime: runtimeConfig })
          }
        })
        delete instanceMap[id]
      }

      return result
    }
    return new Error(`invalid instance id "${id}"`)
  }
}

上面的代碼就是給每個instance注冊方法的具體實現,在Weex里面每個instance默認都會有三個生命周期的方法:createInstance,refreshInstance,destroyInstance。所有Instance的方法都會存在services中。

init( )初始化的最后一步就是給每個實例添加callJS的方法


adaptInstance('receiveTasks', 'callJS')


function adaptInstance (methodName, nativeMethodName) {
  methods[nativeMethodName] = function (...args) {
    const id = args[0]
    const info = instanceMap[id]
    if (info && frameworks[info.framework]) {
      return frameworks[info.framework][methodName](...args)
    }
    return new Error(`invalid instance id "${id}"`)
  }
}

當Native調用callJS方法的時候,就會調用到對應id的instance的receiveTasks方法。

整個init流程總結如上圖。

init結束以后會設置全局方法。


for (const methodName in globalMethods) {
  global[methodName] = (...args) => {
    const ret = globalMethods[methodName](...args)
    if (ret instanceof Error) {
      console.error(ret.toString())
    }
    return ret
  }
}

圖上標的紅色的3個方法表示的是默認就有的方法。

至此,Weex JS Framework就算初始化完成。

二. Weex JS Framework 管理實例的生命周期

當Native初始化完成Component,Module,handler之后,從遠端請求到了JS Bundle,Native通過調用createInstance方法,把JS Bundle傳給JS Framework。于是接下來的這一切從createInstance開始說起。

Native通過調用createInstance,就會執行到html5/runtime/init.js里面的function createInstance (id, code, config, data)方法。


function createInstance (id, code, config, data) {
  let info = instanceMap[id]

  if (!info) {
    // 檢查版本信息
    info = checkVersion(code) || {}
    if (!frameworks[info.framework]) {
      info.framework = 'Weex'
    }

    // 初始化 instance 的 config.
    config = JSON.parse(JSON.stringify(config || {}))
    config.bundleVersion = info.version
    config.env = JSON.parse(JSON.stringify(global.WXEnvironment || {}))
    console.debug(`[JS Framework] create an ${info.framework}@${config.bundleVersion} instance from ${config.bundleVersion}`)

    const env = {
      info,
      config,
      created: Date.now(),
      framework: info.framework
    }
    env.services = createServices(id, env, runtimeConfig)
    instanceMap[id] = env

    return frameworks[info.framework].createInstance(id, code, config, data, env)
  }
  return new Error(`invalid instance id "${id}"`)
}

這個方法里面就是對版本信息,config,日期等信息進行初始化。并在Native記錄一條日志信息:


[JS Framework] create an Weex@undefined instance from undefined

上面這個createInstance方法最終還是要調用html5/framework/legacy/static/create.js里面的createInstance (id, code, options, data, info)方法。


export function createInstance (id, code, options, data, info) {
  const { services } = info || {}
  // 初始化target
  resetTarget()
  let instance = instanceMap[id]
  /* istanbul ignore else */
  options = options || {}
  let result
  /* istanbul ignore else */
  if (!instance) {
    instance = new App(id, options)
    instanceMap[id] = instance
    result = initApp(instance, code, data, services)
  }
  else {
    result = new Error(`invalid instance id "${id}"`)
  }
  return result
}


new App()方法會創建新的 App 實例對象,并且把對象放入 instanceMap 中。

App對象的定義如下:


export default function App (id, options) {
  this.id = id
  this.options = options || {}
  this.vm = null
  this.customComponentMap = {}
  this.commonModules = {}

  // document
  this.doc = new renderer.Document(
    id,
    this.options.bundleUrl,
    null,
    renderer.Listener
  )
  this.differ = new Differ(id)
}

其中有三個比較重要的屬性:

  1. id 是 JS Framework 與 Native 端通信時的唯一標識。
  2. vm 是 View Model,組件模型,包含了數據綁定相關功能。
  3. doc 是 Virtual DOM 中的根節點。

舉個例子,假設Native傳入了如下的信息進行createInstance初始化:


args:( 
      0,
       “(這里是網絡上下載的JS,由于太長了,省略)”, 
      { 
        bundleUrl = "http://192.168.31.117:8081/HelloWeex.js"; 
        debug = 1; 
      }
) 

那么instance = 0,code就是JS代碼,data對應的是下面那個字典,service = @{ }。通過這個入參傳入initApp(instance, code, data, services)方法。這個方法在html5/framework/legacy/app/ctrl/init.js里面。


export function init (app, code, data, services) {
  console.debug('[JS Framework] Intialize an instance with:\n', data)
  let result

  /* 此處省略了一些代碼*/ 

  // 初始化weexGlobalObject
  const weexGlobalObject = {
    config: app.options,
    define: bundleDefine,
    bootstrap: bundleBootstrap,
    requireModule: bundleRequireModule,
    document: bundleDocument,
    Vm: bundleVm
  }

  // 防止weexGlobalObject被修改
  Object.freeze(weexGlobalObject)
  /* 此處省略了一些代碼*/ 

  // 下面開始轉換JS Boudle的代碼
  let functionBody
  /* istanbul ignore if */
  if (typeof code === 'function') {
    // `function () {...}` -> `{...}`
    // not very strict
    functionBody = code.toString().substr(12)
  }
  /* istanbul ignore next */
  else if (code) {
    functionBody = code.toString()
  }
  // wrap IFFE and use strict mode
  functionBody = `(function(global){\n\n"use strict";\n\n ${functionBody} \n\n})(Object.create(this))`

  // run code and get result
  const globalObjects = Object.assign({
    define: bundleDefine,
    require: bundleRequire,
    bootstrap: bundleBootstrap,
    register: bundleRegister,
    render: bundleRender,
    __weex_define__: bundleDefine, // alias for define
    __weex_bootstrap__: bundleBootstrap, // alias for bootstrap
    __weex_document__: bundleDocument,
    __weex_require__: bundleRequireModule,
    __weex_viewmodel__: bundleVm,
    weex: weexGlobalObject
  }, timerAPIs, services)

  callFunction(globalObjects, functionBody)

  return result
}


上面這個方法很重要。在上面這個方法中封裝了一個globalObjects對象,里面裝了define 、require 、bootstrap 、register 、render這5個方法。

也會在Native本地記錄一條日志:


[JS Framework] Intialize an instance with: undefined

在上述5個方法中:


/**
 * @deprecated
 */
export function register (app, type, options) {
  console.warn('[JS Framework] Register is deprecated, please install lastest transformer.')
  registerCustomComponent(app, type, options)
}

其中register、render、require是已經廢棄的方法。

bundleDefine函數原型:



(...args) => defineFn(app, ...args)

bundleBootstrap函數原型:


(name, config, _data) => {
    result = bootstrap(app, name, config, _data || data)
    updateActions(app)
    app.doc.listener.createFinish()
    console.debug(`[JS Framework] After intialized an instance(${app.id})`)
  }

bundleRequire函數原型:


name => _data => {
    result = bootstrap(app, name, {}, _data)
  }

bundleRegister函數原型:


(...args) => register(app, ...args)

bundleRender函數原型:


(name, _data) => {
    result = bootstrap(app, name, {}, _data)
  }

上述5個方法封裝到globalObjects中,傳到 JS Bundle 中。


function callFunction (globalObjects, body) {
  const globalKeys = []
  const globalValues = []
  for (const key in globalObjects) {
    globalKeys.push(key)
    globalValues.push(globalObjects[key])
  }
  globalKeys.push(body)
  // 最終JS Bundle會通過new Function( )的方式被執行
  const result = new Function(...globalKeys)
  return result(...globalValues)
}


最終JS Bundle是會通過new Function( )的方式被執行。JS Bundle的代碼將會在全局環境中執行,并不能獲取到 JS Framework 執行環境中的數據,只能用globalObjects對象里面的方法。JS Bundle 本身也用了IFFE 和 嚴格模式,也并不會污染全局環境。

以上就是createInstance做的所有事情,在接收到Native的createInstance調用的時候,先會在JSFramework中新建App實例對象并保存在instanceMap 中。再把5個方法(其中3個方法已經廢棄了)傳入到new Function( )中。new Function( )會進行JSFramework最重要的事情,將 JS Bundle 轉換成 Virtual DOM 發送到原生模塊渲染。

三. Weex JS Framework 構建Virtual DOM

構建Virtual DOM的過程就是編譯執行JS Boudle的過程。

先給一個實際的JS Boudle的例子,比如如下的代碼:


// { "framework": "Weex" }
/******/ (function(modules) { // webpackBootstrap
/******/    // The module cache
/******/    var installedModules = {};

/******/    // The require function
/******/    function __webpack_require__(moduleId) {

/******/        // Check if module is in cache
/******/        if(installedModules[moduleId])
/******/            return installedModules[moduleId].exports;

/******/        // Create a new module (and put it into the cache)
/******/        var module = installedModules[moduleId] = {
/******/            exports: {},
/******/            id: moduleId,
/******/            loaded: false
/******/        };

/******/        // Execute the module function
/******/        modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);

/******/        // Flag the module as loaded
/******/        module.loaded = true;

/******/        // Return the exports of the module
/******/        return module.exports;
/******/    }


/******/    // expose the modules object (__webpack_modules__)
/******/    __webpack_require__.m = modules;

/******/    // expose the module cache
/******/    __webpack_require__.c = installedModules;

/******/    // __webpack_public_path__
/******/    __webpack_require__.p = "";

/******/    // Load entry module and return exports
/******/    return __webpack_require__(0);
/******/ })
/************************************************************************/
/******/ ([
/* 0 */
/***/ function(module, exports, __webpack_require__) {

    var __weex_template__ = __webpack_require__(1)
    var __weex_style__ = __webpack_require__(2)
    var __weex_script__ = __webpack_require__(3)

    __weex_define__('@weex-component/916f9ecb075bbff1f4ea98389a4bb514', [], function(__weex_require__, __weex_exports__, __weex_module__) {

        __weex_script__(__weex_module__, __weex_exports__, __weex_require__)
        if (__weex_exports__.__esModule && __weex_exports__.default) {
          __weex_module__.exports = __weex_exports__.default
        }

        __weex_module__.exports.template = __weex_template__

        __weex_module__.exports.style = __weex_style__

    })

    __weex_bootstrap__('@weex-component/916f9ecb075bbff1f4ea98389a4bb514',undefined,undefined)

/***/ },
/* 1 */
/***/ function(module, exports) {

    module.exports = {
      "type": "div",
      "classList": [
        "container"
      ],
      "children": [
        {
          "type": "image",
          "attr": {
            "src": "http://9.pic.paopaoche.net/up/2016-7/201671315341.png"
          },
          "classList": [
            "pic"
          ],
          "events": {
            "click": "picClick"
          }
        },
        {
          "type": "text",
          "classList": [
            "text"
          ],
          "attr": {
            "value": function () {return this.title}
          }
        }
      ]
    }

/***/ },
/* 2 */
/***/ function(module, exports) {

    module.exports = {
      "container": {
        "alignItems": "center"
      },
      "pic": {
        "width": 200,
        "height": 200
      },
      "text": {
        "fontSize": 40,
        "color": "#000000"
      }
    }

/***/ },
/* 3 */
/***/ function(module, exports) {

    module.exports = function(module, exports, __weex_require__){'use strict';

    module.exports = {
        data: function () {return {
            title: 'Hello World',
            toggle: false
        }},
        ready: function ready() {
            console.log('this.title == ' + this.title);
            this.title = 'hello Weex';
            console.log('this.title == ' + this.title);
        },
        methods: {
            picClick: function picClick() {
                this.toggle = !this.toggle;
                if (this.toggle) {
                    this.title = '圖片被點擊';
                } else {
                    this.title = 'Hello Weex';
                }
            }
        }
    };}
    /* generated by weex-loader */


/***/ }
/******/ ]);




JS Framework拿到JS Boudle以后,會先執行bundleDefine。


export const defineFn = function (app, name, ...args) {
  console.debug(`[JS Framework] define a component ${name}`)

  /*以下代碼省略*/
  /*在這個方法里面注冊自定義組件和普通的模塊*/

}


用戶自定義的組件放在app.customComponentMap中。執行完bundleDefine以后調用bundleBootstrap方法。

  1. define: 用來自定義一個復合組件
  2. bootstrap: 用來以某個復合組件為根結點渲染頁面

bundleDefine會解析代碼中的__weex_define__("@weex-component/")定義的component,包含依賴的子組件。并將component記錄到customComponentMap[name] = exports數組中,維護組件與組件代碼的對應關系。由于會依賴子組件,因此會被多次調用,直到所有的組件都被解析完全。



export function bootstrap (app, name, config, data) {
  console.debug(`[JS Framework] bootstrap for ${name}`)

  // 1. 驗證自定義的Component的名字
  let cleanName
  if (isWeexComponent(name)) {
    cleanName = removeWeexPrefix(name)
  }
  else if (isNpmModule(name)) {
    cleanName = removeJSSurfix(name)
    // 檢查是否通過老的 'define' 方法定義的
    if (!requireCustomComponent(app, cleanName)) {
      return new Error(`It's not a component: ${name}`)
    }
  }
  else {
    return new Error(`Wrong component name: ${name}`)
  }

  // 2. 驗證 configuration
  config = isPlainObject(config) ? config : {}
  // 2.1 transformer的版本檢查
  if (typeof config.transformerVersion === 'string' &&
    typeof global.transformerVersion === 'string' &&
    !semver.satisfies(config.transformerVersion,
      global.transformerVersion)) {
    return new Error(`JS Bundle version: ${config.transformerVersion} ` +
      `not compatible with ${global.transformerVersion}`)
  }
  // 2.2 降級版本檢查
  const downgradeResult = downgrade.check(config.downgrade)

  if (downgradeResult.isDowngrade) {
    app.callTasks([{
      module: 'instanceWrap',
      method: 'error',
      args: [
        downgradeResult.errorType,
        downgradeResult.code,
        downgradeResult.errorMessage
      ]
    }])
    return new Error(`Downgrade[${downgradeResult.code}]: ${downgradeResult.errorMessage}`)
  }

  // 設置 viewport
  if (config.viewport) {
    setViewport(app, config.viewport)
  }

  // 3. 新建一個新的自定義的Component組件名字和數據的viewModel
  app.vm = new Vm(cleanName, null, { _app: app }, null, data)
}


bootstrap方法會在Native本地日志記錄:


[JS Framework] bootstrap for @weex-component/677c57764d82d558f236d5241843a2a2(此處的編號是舉一個例子)

bootstrap方法的作用是校驗參數和環境信息,如果不符合當前條件,會觸發頁面降級,(也可以手動進行,比如Native出現問題了,降級到H5)。最后會根據Component新建對應的viewModel。



export default function Vm (
  type,
  options,
  parentVm,
  parentEl,
  mergedData,
  externalEvents
) {
  /*省略部分代碼*/
  // 初始化
  this._options = options
  this._methods = options.methods || {}
  this._computed = options.computed || {}
  this._css = options.style || {}
  this._ids = {}
  this._vmEvents = {}
  this._childrenVms = []
  this._type = type

  // 綁定事件和生命周期
  initEvents(this, externalEvents)

  console.debug(`[JS Framework] "init" lifecycle in 
  Vm(${this._type})`)
  this.$emit('hook:init')
  this._inited = true

  // 綁定數據到viewModel上
  this._data = typeof data === 'function' ? data() : data
  if (mergedData) {
    extend(this._data, mergedData)
  }
  initState(this)

  console.debug(`[JS Framework] "created" lifecycle in Vm(${this._type})`)
  this.$emit('hook:created')
  this._created = true

  // backward old ready entry
  if (options.methods && options.methods.ready) {
    console.warn('"exports.methods.ready" is deprecated, ' +
      'please use "exports.created" instead')
    options.methods.ready.call(this)
  }

  if (!this._app.doc) {
    return
  }

  // 如果沒有parentElement,那么就指定為documentElement
  this._parentEl = parentEl || this._app.doc.documentElement
  // 構建模板
  build(this)
}


上述代碼就是關鍵的新建viewModel的代碼,在這個函數中,如果正常運行完,會在Native記錄下兩條日志信息:


[JS Framework] "init" lifecycle in Vm(677c57764d82d558f236d5241843a2a2)  ?[;
[JS Framework] "created" lifecycle in Vm(677c57764d82d558f236d5241843a2a2)  ?[;


同時干了三件事情:

  1. initEvents 初始化事件和生命周期
  2. initState 實現數據綁定功能
  3. build模板并繪制 Native UI

1. initEvents 初始化事件和生命周期


export function initEvents (vm, externalEvents) {
  const options = vm._options || {}
  const events = options.events || {}
  for (const type1 in events) {
    vm.$on(type1, events[type1])
  }
  for (const type2 in externalEvents) {
    vm.$on(type2, externalEvents[type2])
  }
  LIFE_CYCLE_TYPES.forEach((type) => {
    vm.$on(`hook:${type}`, options[type])
  })
}



在initEvents方法里面會監聽三類事件:

  1. 組件options里面定義的事情
  2. 一些外部的事件externalEvents
  3. 還要綁定生命周期的hook鉤子

const LIFE_CYCLE_TYPES = ['init', 'created', 'ready', 'destroyed']

生命周期的鉤子包含上述4種,init,created,ready,destroyed。

$on方法是增加事件監聽者listener的。$emit方式是用來執行方法的,但是不進行dispatch和broadcast。$dispatch方法是派發事件,沿著父類往上傳遞。$broadcast方法是廣播事件,沿著子類往下傳遞。$off方法是移除事件監聽者listener。

事件object的定義如下:


function Evt (type, detail) {
  if (detail instanceof Evt) {
    return detail
  }

  this.timestamp = Date.now()
  this.detail = detail
  this.type = type

  let shouldStop = false
  this.stop = function () {
    shouldStop = true
  }
  this.hasStopped = function () {
    return shouldStop
  }
}

每個組件的事件包含事件的object,事件的監聽者,事件的emitter,生命周期的hook鉤子。

initEvents的作用就是對當前的viewModel綁定上上述三種事件的監聽者listener。

2. initState 實現數據綁定功能


export function initState (vm) {
  vm._watchers = []
  initData(vm)
  initComputed(vm)
  initMethods(vm)
}

  1. initData,設置 proxy,監聽 _data 中的屬性;然后添加 reactiveGetter & reactiveSetter 實現數據監聽。 (
  2. initComputed,初始化計算屬性,只有 getter,在 _data 中沒有對應的值。
  3. initMethods 將 _method 中的方法掛在實例上。

export function initData (vm) {
  let data = vm._data

  if (!isPlainObject(data)) {
    data = {}
  }
  // proxy data on instance
  const keys = Object.keys(data)
  let i = keys.length
  while (i--) {
    proxy(vm, keys[i])
  }
  // observe data
  observe(data, vm)
}

在initData方法里面最后一步會進行data的observe。

數據綁定的核心思想是基于 ES5 的 Object.defineProperty 方法,在 vm 實例上創建了一系列的 getter / setter,支持數組和深層對象,在設置屬性值的時候,會派發更新事件。

這塊數據綁定的思想,一部分是借鑒了Vue的實現,這塊打算以后寫篇文章專門談談。

3. build模板


export function build (vm) {
  const opt = vm._options || {}
  const template = opt.template || {}

  if (opt.replace) {
    if (template.children && template.children.length === 1) {
      compile(vm, template.children[0], vm._parentEl)
    }
    else {
      compile(vm, template.children, vm._parentEl)
    }
  }
  else {
    compile(vm, template, vm._parentEl)
  }

  console.debug(`[JS Framework] "ready" lifecycle in Vm(${vm._type})`)
  vm.$emit('hook:ready')
  vm._ready = true
}


build構建思路如下:

compile(template, parentNode)

  1. 如果 type 是 content ,就創建contentNode。
  2. 否則 如果含有 v-for 標簽, 那么就循環遍歷,創建context,繼續compile(templateWithoutFor, parentNode)
  3. 否則 如果含有 v-if 標簽,繼續compile(templateWithoutIf, parentNode)
  4. 否則如果 type 是 dynamic ,繼續compile(templateWithoutDynamicType, parentNode)
  5. 否則如果 type 是 custom ,那么調用addChildVm(vm, parentVm),build(externalDirs),遍歷子節點,然后再compile(childNode, template)
  6. 最后如果 type 是 Native ,更新(id/attr/style/class),append(template, parentNode),遍歷子節點,compile(childNode, template)

在上述一系列的compile方法中,有4個參數,

  1. vm: 待編譯的 Vm 對象。
  2. target: 待編譯的節點,是模板中的標簽經過 transformer 轉換后的結構。
  3. dest: 當前節點父節點的 Virtual DOM。
  4. meta: 元數據,在內部調用時可以用來傳遞數據。

編譯的方法也分為以下7種:

  1. compileFragment 編譯多個節點,創建 Fragment 片段。
  2. compileBlock 創建特殊的Block。
  3. compileRepeat 編譯 repeat 指令,同時會執行數據綁定,在數據變動時會觸發 DOM 節點的更新。
  4. compileShown 編譯 if 指令,也會執行數據綁定。
  5. compileType 編譯動態類型的組件。
  6. compileCustomComponent 編譯展開用戶自定義的組件,這個過程會遞歸創建子 vm,并且綁定父子關系,也會觸發子組件的生命周期函數。
  7. compileNativeComponent 編譯內置原生組件。這個方法會調用 createBody 或 createElement 與原生模塊通信并創建 Native UI。

上述7個方法里面,除了compileBlock和compileNativeComponent以外的5個方法,都會遞歸調用。

編譯好模板以后,原來的JS Boudle就都被轉變成了類似Json格式的 Virtual DOM 了。下一步開始繪制Native UI。

4. 繪制 Native UI

繪制Native UI的核心方法就是compileNativeComponent (vm, template, dest, type)。

compileNativeComponent的核心實現如下:


function compileNativeComponent (vm, template, dest, type) {
  applyNaitveComponentOptions(template)

  let element
  if (dest.ref === '_documentElement') {
    // if its parent is documentElement then it's a body
    console.debug(`[JS Framework] compile to create body for ${type}`)
    // 構建DOM根
    element = createBody(vm, type)
  }
  else {
    console.debug(`[JS Framework] compile to create element for ${type}`)
    // 添加元素
    element = createElement(vm, type)
  }

  if (!vm._rootEl) {
    vm._rootEl = element
    // bind event earlier because of lifecycle issues
    const binding = vm._externalBinding || {}
    const target = binding.template
    const parentVm = binding.parent
    if (target && target.events && parentVm && element) {
      for (const type in target.events) {
        const handler = parentVm[target.events[type]]
        if (handler) {
          element.addEvent(type, bind(handler, parentVm))
        }
      }
    }
  }

  bindElement(vm, element, template)

  if (template.attr && template.attr.append) { // backward, append prop in attr
    template.append = template.attr.append
  }

  if (template.append) { // give the append attribute for ios adaptation
    element.attr = element.attr || {}
    element.attr.append = template.append
  }

  const treeMode = template.append === 'tree'
  const app = vm._app || {}
  if (app.lastSignal !== -1 && !treeMode) {
    console.debug('[JS Framework] compile to append single node for', element)
    app.lastSignal = attachTarget(vm, element, dest)
  }
  if (app.lastSignal !== -1) {
    compileChildren(vm, template, element)
  }
  if (app.lastSignal !== -1 && treeMode) {
    console.debug('[JS Framework] compile to append whole tree for', element)
    app.lastSignal = attachTarget(vm, element, dest)
  }
}


繪制Native的UI會先繪制DOM的根,然后繪制上面的子孩子元素。子孩子需要遞歸判斷,如果還有子孩子,還需要繼續進行之前的compile的流程。

每個 Document 對象中都會包含一個 listener 屬性,它可以向 Native 端發送消息,每當創建元素或者是有更新操作時,listener 就會拼裝出制定格式的 action,并且最終調用 callNative 把 action 傳遞給原生模塊,原生模塊中也定義了相應的方法來執行 action 。

例如當某個元素執行了 element.appendChild() 時,就會調用 listener.addElement(),然后就會拼成一個類似Json格式的數據,再調用callTasks方法。



export function callTasks (app, tasks) {
  let result

  /* istanbul ignore next */
  if (typof(tasks) !== 'array') {
    tasks = [tasks]
  }

  tasks.forEach(task => {
    result = app.doc.taskCenter.send(
      'module',
      {
        module: task.module,
        method: task.method
      },
      task.args
    )
  })

  return result
}


在上述方法中會繼續調用在html5/runtime/task-center.js中的send方法。


send (type, options, args) {
    const { action, component, ref, module, method } = options

    args = args.map(arg => this.normalize(arg))

    switch (type) {
      case 'dom':
        return this[action](this.instanceId, args)
      case 'component':
        return this.componentHandler(this.instanceId, ref, method, args, { component })
      default:
        return this.moduleHandler(this.instanceId, module, method, args, {})
    }
  }


這里存在有2個handler,它們的實現是之前傳進來的sendTasks方法。


const config = {
  Document, Element, Comment, Listener,
  TaskCenter,
  sendTasks (...args) {
    return global.callNative(...args)
  }
}


sendTasks方法最終會調用callNative,調用本地原生的UI進行繪制。

四. Weex JS Framework 處理Native觸發的事件

最后來看看Weex JS Framework是如何處理Native傳遞過來的事件的。

在html5/framework/legacy/static/bridge.js里面對應的是Native的傳遞過來的事件處理方法。



const jsHandlers = {
  fireEvent: (id, ...args) => {
    return fireEvent(instanceMap[id], ...args)
  },
  callback: (id, ...args) => {
    return callback(instanceMap[id], ...args)
  }
}

/**
 * 接收來自Native的事件和回調
 */
export function receiveTasks (id, tasks) {
  const instance = instanceMap[id]
  if (instance && Array.isArray(tasks)) {
    const results = []
    tasks.forEach((task) => {
      const handler = jsHandlers[task.method]
      const args = [...task.args]
      /* istanbul ignore else */
      if (typeof handler === 'function') {
        args.unshift(id)
        results.push(handler(...args))
      }
    })
    return results
  }
  return new Error(`invalid instance id "${id}" or tasks`)
}



在Weex 每個instance實例里面都包含有一個callJS的全局方法,當本地調用了callJS這個方法以后,會調用receiveTasks方法。

關于Native會傳遞過來哪些事件,可以看這篇文章《Weex 事件傳遞的那些事兒》

在jsHandler里面封裝了fireEvent和callback方法,這兩個方法在html5/frameworks/legacy/app/ctrl/misc.js方法中。


export function fireEvent (app, ref, type, e, domChanges) {
  console.debug(`[JS Framework] Fire a "${type}" event on an element(${ref}) in instance(${app.id})`)
  if (Array.isArray(ref)) {
    ref.some((ref) => {
      return fireEvent(app, ref, type, e) !== false
    })
    return
  }
  const el = app.doc.getRef(ref)
  if (el) {
    const result = app.doc.fireEvent(el, type, e, domChanges)
    app.differ.flush()
    app.doc.taskCenter.send('dom', { action: 'updateFinish' }, [])
    return result
  }
  return new Error(`invalid element reference "${ref}"`)
}

fireEvent傳遞過來的參數包含,事件類型,事件object,是一個元素的ref。如果事件會引起DOM的變化,那么還會帶一個參數描述DOM的變化。

在htlm5/frameworks/runtime/vdom/document.js里面


  fireEvent (el, type, e, domChanges) {
    if (!el) {
      return
    }
    e = e || {}
    e.type = type
    e.target = el
    e.timestamp = Date.now()
    if (domChanges) {
      updateElement(el, domChanges)
    }
    return el.fireEvent(type, e)
  }


這里可以發現,其實對DOM的更新是單獨做的,然后接著把事件繼續往下傳,傳給element。

接著在htlm5/frameworks/runtime/vdom/element.js里面


  fireEvent (type, e) {
    const handler = this.event[type]
    if (handler) {
      return handler.call(this, e)
    }
  }

最終事件在這里通過handler的call方法進行調用。

當有數據發生變化的時候,會觸發watcher的數據監聽,當前的value和oldValue比較。先會調用watcher的update方法。


Watcher.prototype.update = function (shallow) {
  if (this.lazy) {
    this.dirty = true
  } else {
    this.run()
  }

update方法里面會調用run方法。


Watcher.prototype.run = function () {
  if (this.active) {
    const value = this.get()
    if (
      value !== this.value ||
      // Deep watchers and watchers on Object/Arrays should fire even
      // when the value is the same, because the value may
      // have mutated; but only do so if this is a
      // non-shallow update (caused by a vm digest).
      ((isObject(value) || this.deep) && !this.shallow)
    ) {
      // set new value
      const oldValue = this.value
      this.value = value
      this.cb.call(this.vm, value, oldValue)
    }
    this.queued = this.shallow = false
  }
}


run方法之后會觸發differ,dep會通知所有相關的子視圖的改變。



Dep.prototype.notify = function () {
  const subs = this.subs.slice()
  for (let i = 0, l = subs.length; i < l; i++) {
    subs[i].update()
  }
}

相關聯的子視圖也會觸發update的方法。

還有一種事件是Native通過模塊的callback回調傳遞事件。



export function callback (app, callbackId, data, ifKeepAlive) {
  console.debug(`[JS Framework] Invoke a callback(${callbackId}) with`, data,
            `in instance(${app.id})`)
  const result = app.doc.taskCenter.callback(callbackId, data, ifKeepAlive)
  updateActions(app)
  app.doc.taskCenter.send('dom', { action: 'updateFinish' }, [])
  return result
}

callback的回調比較簡單,taskCenter.callback會調用callbackManager.consume的方法。執行完callback方法以后,接著就是執行differ.flush,最后一步就是回調Native,通知updateFinish。

至此,Weex JS Framework 的三大基本功能都分析完畢了,用一張大圖做個總結,描繪它干了哪些事情:

圖片有點大,鏈接點這里

五.Weex JS Framework 未來可能做更多的事情

除了目前官方默認支持的 Vue 2.0,Rax的Framework,還可以支持其他平臺的 JS Framework 。Weex還可以支持自己自定義的 JS Framework。只要按照如下的步驟來定制,可以寫一套完整的 JS Framework。

  1. 首先你要有一套完整的 JS Framework。
  2. 了解 Weex 的 JS 引擎的特性支持情況。
  3. 適配 Weex 的 native DOM APIs。
  4. 適配 Weex 的初始化入口和多實例管理機制。
  5. 在 Weex JS runtime 的 framework 配置中加入自己的 JS Framework 然后打包。
  6. 基于該 JS Framework 撰寫 JS bundle,并加入特定的前綴注釋,以便 Weex JS runtime 能夠正確識別。

如果經過上述的步驟進行擴展以后,可以出現如下的代碼:


import * as Vue from '...'
import * as React from '...'
import * as Angular from '...'
export default { Vue, React, Angular };

這樣可以支持Vue,React,Angular。

如果在 JS Bundle 在文件開頭帶有如下格式的注釋:


// { "framework": "Vue" }
...

這樣 Weex JS 引擎就會識別出這個 JS bundle 需要用 Vue 框架來解析。并分發給 Vue 框架處理。

這樣每個 JS Framework,只要:1. 封裝了這幾個接口,2. 給自己的 JS Bundle 第一行寫好特殊格式的注釋,Weex 就可以正常的運行基于各種 JS Framework 的頁面了。

Weex 支持同時多種框架在一個移動應用中共存并各自解析基于不同框架的 JS bundle。

這一塊筆者暫時還沒有實踐各自解析不同的 JS bundle,相信這部分未來也許可以干很多有趣的事情。

最后

本篇文章把 Weex 在 Native 端的 JS Framework 的工作原理簡單的梳理了一遍,中間唯一沒有深究的點可能就是 Weex 是 如何 利用 Vue 進行數據綁定的,如何監聽數據變化的,這塊打算另外開一篇文章詳細的分析一下。到此篇為止,Weex 在 Native 端的所有源碼實現就分析完畢了。

請大家多多指點。

References:

Weex 官方文檔
Weex 框架中 JS Framework 的結構
淺析weex之vdom渲染
Native 性能穩定性極致優化


Weex 源碼解析系列文章:

Weex 是如何在 iOS 客戶端上跑起來的
由 FlexBox 算法強力驅動的 Weex 布局引擎
Weex 事件傳遞的那些事兒
Weex 中別具匠心的 JS Framework
iOS 開發者的 Weex 偽最佳實踐指北


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

推薦閱讀更多精彩內容