Chrome 浏览器插件从 Manifest V2 升级到 V3 版本所需要修改的点

一、Manifest V2 支持时间表

Chrome 浏览器官方已经给出确定的时间来弃用 V2 版本的插件了。

最早从 2024 年 6 月Chrome 127 开始,我们将开始停用 Chrome 的不稳定版本(开发者版、Canary 版和 Beta 版)中的 Manifest V2 扩展程序。受此变化影响的用户会在浏览器中看到 Manifest V2 扩展程序自动停用,并且无法再从 Chrome 应用商店安装 Manifest V2 扩展程序。此外,Manifest V2 扩展程序在 Chrome 应用商店中将不再拥有“精选”徽章(如果目前已有该徽章)。

如果企业如果使用 ExtensionManifestV2Availability 政策确保其组织中的 Manifest V2 扩展程序能持续正常运行,则其组织中还有一年的时间(即在 2025 年 6 月之前)迁移 Manifest V2 扩展程序。在此之前,已启用此政策的浏览器不会受到弃用安排的影响。

如果扩展程序发布商目前仍在发布 Manifest V2 扩展程序,我们强烈建议您在 2024 年 6 月之前完成向 Manifest V3 的迁移。

官方时间线

1、2022 年 6 月:Chrome 应用商店 - 不再有新的专用扩展程序

Chrome 应用商店不再接受公开范围设为“不公开”的新 Manifest V2 扩展程序。

2、2024 年 6 月:在稳定发布前弃用 Chrome MV2

3、2024 年 6 月 + 1-X 个月:弃用 Chrome MV2 并稳定发布

4、2025 年 6 月:Chrome MV2 弃用(企业版)

二、Manifest V3 迁移核对列表

1、Manifest.json 文件

1. 更新 manifest_version 版本号

manifest_version 字段的值从 2 更改为 3。

{
  "manifest_version": 3
}

2. 更新 permissionshost_permissions 字段

Manifest V3 中的主机权限是一个单独的字段;

不需要在 permissionsoptional_permissions 中指定这些权限;

有单独的字段 host_permissions 来表示。

2.1. V2 版本

{
  "permissions": [
    "tabs",
    "bookmarks",
    "https://www.blogger.com/",
  ],
  "optional_permissions": [
    "unlimitedStorage",
    "*://*/*",
  ]
}

2.2. V3 版本

{
  "permissions": [
    "tabs",
    "bookmarks"
  ],
  "optional_permissions": [
    "unlimitedStorage"
  ],
  "host_permissions": [
    "https://www.blogger.com/",
  ],
  "optional_host_permissions": [
    "*://*/*",
  ]
}

3. 更新 web_accessible_resources 字段

Manifest V3 会限制哪些网站和扩展程序可以访问扩展程序中的资源,以此来限制数据公开范围;

Manifest V2 中,默认情况下,指定资源可供所有网站访问;

在下面的 Manifest V3 示例中,这些资源仅可供匹配的网站使用,而只有某些图片可供所有网站使用。

3.1. V2 版本

{
  "web_accessible_resources": [
    "images/*",
    "style/extension.css",
    "script/extension.js"
  ],
}

3.2. V3 版本

{
    "web_accessible_resources": [
    {
      "resources": [
        "images/*"
      ],
      "matches": [
        "*://*/*"
      ]
    },
    {
      "resources": [
        "style/extension.css",
        "script/extension.js"
      ],
      "matches": [
        "https://example.com/*"
      ]
    }
  ],
}

2、迁移到 Service Worker

使用 service worker 替换 backgroundevent pages,以确保后台代码远离主线程,这样可以让扩展程序仅在需要时运行,从而节省资源。

1. BackgroundService Worker 之间的区别

1.1. Service Workerbackground 之间的差异

  • 在主线程以外运行,这意味着不会干扰扩展程序内容;
  • 具有特殊功能,例如拦截扩展程序来源上的提取事件,例如拦截工具栏弹出式窗口中的提取事件;
  • 可以通过客户端界面与其他上下文进行通信和交互。

1.2. 改动点

  • 由于它们无法访问 DOMwindow 接口,因此需要将此类调用移至其他 API 或移至屏幕外文档中;
  • 不应注册事件监听器来响应返回的 promise 或在事件回调内部;
  • 不向后兼容 XMLHttpRequest(),因此需要将接口的调用替换为 fetch()
  • 由于它们在不使用时终止,因此需要保留应用状态,而不是依赖于全局变量;
  • 终止 Service Worker 还可以在计时器完成之前结束计时器。需要将其替换为 alarms

2. 更新 manifest.json 中的 background 字段

Manifest V3 中,background 页面被 Service Worker 所取代:

  • manifest.json 中的 "background.scripts" 替换为 "background.service_worker"
  • "service_worker" 字段接受字符串,而不是字符串数组;
  • manifest.json 中移除 "background.persistent"

2.1. V2 版本

{
  "background": {
    "scripts": [
      "backgroundContextMenus.js",
      "backgroundOauth.js"
    ],
    "persistent": false
  },
}

2.2. V3 版本

{
  "background": {
    "service_worker": "service_worker.js",
    "type": "module"
  }
}

"service_worker" 字段接受单个字符串。只有使用 ES module (使用 import 关键字)时,才需要 "type" 字段。其值将始终为 "module"

3. 将 DOMwindow 调用移至屏幕外文档

某些扩展程序需要访问 DOMwindow 对象,无需打开新的窗口或标签页。Offscreen API 支持这类使用情形,因为这类 API 可以打开和关闭与扩展程序打包在一起的未显示文档,而不会干扰用户体验。除了消息传递之外,屏幕外文档不会与其他扩展程序上下文共享 API,而是起到完整网页的作用,供扩展程序进行互动。

如需使用 Offscreen API,请通过 Service Worker 创建屏幕外文档。

chrome.offscreen.createDocument({
  url: chrome.runtime.getURL('offscreen.html'),
  reasons: ['CLIPBOARD'],
  justification: 'testing the offscreen API',
});

4. 将 localStorage 转换为其他类型

Web 平台的 Storage 接口(可从 window.localStorage 访问)无法在 Service Worker 中使用。

请使用 chrome.storage.local

5. 同步注册监听器

异步注册监听器(例如在 promisecallback 中)注册并不一定能在 Manifest V3 中有效。

5.1. V2 版本

chrome.storage.local.get(["badgeText"], ({ badgeText }) => {
  chrome.browserAction.setBadgeText({ text: badgeText });
  chrome.browserAction.onClicked.addListener(handleActionClick);
});

Manifest V3 中,系统会在分派事件时重新初始化 Service Worker。这意味着当事件触发时,系统不会注册监听器(因为它们是异步添加的),系统还会错过事件。

5.2. V3 版本

改为将事件监听器注册移至脚本的顶层。这样可以确保 Chrome 能够立即找到并调用操作的点击处理程序,即使扩展程序尚未执行其启动逻辑也是如此。

chrome.action.onClicked.addListener(handleActionClick);

chrome.storage.local.get(["badgeText"], ({ badgeText }) => {
  chrome.action.setBadgeText({ text: badgeText });
});

6. 将 XMLHttpRequest() 替换为全局 fetch()

无法从 Service Worker、扩展程序或其他方法调用 XMLHttpRequest()。将后台脚本对 XMLHttpRequest() 的调用替换为对fetch() 的调用。

const response = await fetch('https://www.example.com/greeting.json');
console.log(response.statusText);

7. 保存状态

Service Worker 是临时的,这意味着它们可能会在用户的浏览器会话期间反复启动、运行和终止。这也意味着,自之前的上下文销毁后,数据并非立即在全局变量中可用。如需解决此问题,请使用存储 API

对于 Manifest V3 来说,要将全局变量替换为对 Storage API 的调用。

chrome.runtime.onMessage.addListener(({ type, name }) => {
  if (type === "set-name") {
    chrome.storage.local.set({ name });
  }
});

chrome.action.onClicked.addListener(async (tab) => {
  const { name } = await chrome.storage.local.get(["name"]);
  chrome.tabs.sendMessage(tab.id, { name });
});

8. 把计时器/定时器替换为 alarms

setTimeout()setInterval()Web 开发中比较常见。不过,在 Service Worker 中可能会失败,因为每当 Service Worker 终止时,计时器就会取消。

8.1. V2 版本

// 3 minutes in milliseconds
const TIMEOUT = 3 * 60 * 1000;
setTimeout(() => {
  chrome.action.setIcon({
    path: getRandomIconPath(),
  });
}, TIMEOUT);

8.2. V3 版本

async function startAlarm(name, duration) {
  await chrome.alarms.create(name, { delayInMinutes: 3 });
}

chrome.alarms.onAlarm.addListener(() => {
  chrome.action.setIcon({
    path: getRandomIconPath(),
  });
});

3、API 调用

1. 将 tab.executeScript() 替换为 scripting.executeScript()

Manifest V3 中,executeScript()tabs API 移至 scripting API。这就需要在实际的代码更改的基础上更改 manifest.json 文件中的权限。

对于 executeScript() 方法:

  • "scripting" 权限;
  • host permissions 权限或 "activeTab" 权限。

scripting.executeScript() 方法与其使用 tabs.executeScript() 的方式类似。但还是有一些区别。

  • 旧方法只能接受一个文件,而新方法可以接受一组文件;
  • 还将传递 ScriptInjection 对象,而不是 InjectDetails

1.1. V2 版本

async function getCurrentTab() {/* ... */}
let tab = await getCurrentTab();

chrome.tabs.executeScript(
  tab.id,
  {
    file: 'content-script.js'
  }
);

代码在 background 脚本中

1.2. V3 版本

async function getCurrentTab()
let tab = await getCurrentTab();

chrome.scripting.executeScript({
  target: {tabId: tab.id},
  files: ['content-script.js']
});

代码在 service worker 中

2. 将 tab.insertCSS()tab.removeCSS() 替换为 scripting.insertCSS()scripting.removeCSS()

Manifest V3 中,insertCSS()removeCSS() 已从 tabs API 移至 scriptingAPI。不仅需要更改代码,还需要对 manifest.json 文件中的权限进行更改:

  • "scripting" 权限;
  • host permissions 权限或 "activeTab" 权限。

scripting API 上的函数与 tabs 上的函数类似。但还是有一些区别。

  • 调用这些方法时,需要传递 CSSInjection 对象,而不是 InjectDetails
  • tabId 作为 CSSInjection.target 的成员(而不是方法参数)进行传递。

2.1. V2 版本

chrome.tabs.insertCSS(tabId, injectDetails, () => {
  // callback code
});

代码在 background 脚本中

2.2. V3 版本

const insertPromise = await chrome.scripting.insertCSS({
  files: ["style.css"],
  target: { tabId: tab.id }
});
// Remaining code. 

代码在 service worker

3. 将 Browser ActionsPage Actions 替换为 Actions

Manifest V2 中,Browser ActionsPage Actions 是两个单独的概念。虽然一开始他们扮演的角色不同,但随着时间的推移,它们之间的差异越来越小。在 Manifest V3 中,这些概念已整合到 Action API 中。这需要更改 manifest.json 和扩展代码。

Manifest V3 中的操作与浏览器操作最为相似;不过,action API 不像 pageAction 那样提供 hide()show()。如果仍需要页面操作,可以使用声明性内容模拟这些操作,也可以使用标签页 ID 调用 enable()disable()

3.1. 将 "browser_action""page_action" 替换为 "action"

manifest.json 中,将 "browser_action""page_action" 字段替换为 "action" 字段。如需了解 "action" 字段的相关信息,请参阅参考文档。

3.1.1. V2 版本
{
  "page_action": { ... },
  "browser_action": {
    "default_popup": "popup.html"
   }

}
3.1.2. V3 版本
{
  "action": {
    "default_popup": "popup.html"
  }
}

3.2. 将 BrowserAction APIpageAction API 替换为 Action API

如果 Manifest V2 使用 browserActionpageAction API,现在应使用 action API

3.2.1. V2 版本
chrome.browserAction.onClicked.addListener(tab => { ... });
chrome.pageAction.onClicked.addListener(tab => { ... });
3.2.2. V3 版本
chrome.action.onClicked.addListener(tab => { ... });

4. 将 callback 替换为 promise

Manifest V3 中,许多扩展程序 API 方法都会返回 promise

为了实现向后兼容性,许多方法在添加 promise 支持后会继续支持回调。需要注意的是,不能在同一函数调用中同时使用这两者。如果传递回调,则函数不会返回 promise;如果希望返回 promise,则也不要传递回调。某些 API 功能(例如事件监听器)将继续需要回调。

如需从回调转换为 promise,请移除回调并处理返回的 promise

4.1. Callback

chrome.permissions.request(newPerms, (granted) => {
  if (granted) {
    console.log('granted');
  } else {
    console.log('not granted');
  }
});

4.2. Promise

const newPerms = { permissions: ['topSites'] };
chrome.permissions.request(newPerms)
.then((granted) => {
  if (granted) {
    console.log('granted');
  } else {
    console.log('not granted');
  }
});

5. 替换需要 Manifest V2 background 上下文的函数

其他扩展程序上下文只能使用消息传递与扩展程序 Service Worker 交互。因此,需要替换需要后台上下文的调用,具体而言:

  • chrome.runtime.getBackgroundPage()
  • chrome.extension.getBackgroundPage()
  • chrome.extension.getExtensionTabs()

扩展程序脚本应使用消息传递在 Service Worker 和扩展程序的其他部分之间进行通信。目前,这需要使用 sendMessage(),并在扩展 Service Worker 中实现 chrome.runtime.onMessage。从长远来看,应计划将这些调用替换为 postMessage()Service Worker消息事件处理程序

6. 替换不受支持的 API

需要在 Manifest V3 中更改下列方法和属性。

Manifest V2 方法或属性 替换为 Manifest V3 方法或属性
chrome.extension.connect() chrome.runtime.connect()
chrome.extension.connectNative() chrome.runtime.connectNative()
chrome.extension.getExtensionTabs() chrome.extension.getViews()
chrome.extension.getURL() chrome.runtime.getURL()
chrome.extension.lastError 如果方法返回 promise,请使用 promise.catch()
chrome.extension.onConnect chrome.runtime.onConnect
chrome.extension.onConnectExternal chrome.runtime.onConnectExternal
chrome.extension.onMessage chrome.runtime.onMessage
chrome.extension.onRequest chrome.runtime.onRequest
chrome.extension.onRequestExternal chrome.runtime.onMessageExternal
chrome.extension.sendMessage() chrome.runtime.sendMessage()
chrome.extension.sendNativeMessage() chrome.runtime.sendNativeMessage()
chrome.extension.sendRequest() chrome.runtime.sendMessage()
chrome.runtime.onSuspendbackground 脚本) 在扩展 Service Worker 中不受支持。请改用 beforeunload 文档事件。
chrome.tabs.getAllInWindow() chrome.tabs.query()
chrome.tabs.getSelected() chrome.tabs.query()
chrome.tabs.onActiveChanged chrome.tabs.onActivated
chrome.tabs.onHighlightChanged chrome.tabs.onHighlighted
chrome.tabs.onSelectionChanged chrome.tabs.onActivated
chrome.tabs.sendRequest() chrome.runtime.sendMessage()
chrome.tabs.Tab.selected chrome.tabs.Tab.highlighted

4、替换屏蔽 Web 请求监听器

Manifest V3 更改了扩展程序处理网络请求修改的方式。扩展程序会指定规则来描述在满足一组给定条件时要执行的操作,而不是拦截网络请求并在运行时使用 chrome.webRequest 更改请求。

Web Request API 和声明式网络请求 API 有很大的区别。需要根据用例重新编写代码,而不是将一个函数调用替换为另一个函数调用。

1. 更新 permissions

manifest.json 中的 "permissions" 字段进行以下更改。

  • 如果不再需要观察网络请求,请移除 "webRequest" 权限;
  • 将匹配模式从 "permissions" 移至 "host_permissions"

需要根据使用场景添加其他权限。这些权限通过其支持的用例进行描述。

2. 创建声明性网络请求规则

如需创建声明性 net 请求规则,需要向 manifest.json 添加 "declarative_net_request" 对象。"declarative_net_request" 代码块包含指向规则文件的 "rule_resource" 对象数组。规则文件包含一组对象,用于指定操作以及调用这些操作的条件。

3. 常见使用场景

3.1. 屏蔽单个网址

Manifest V2 中的一个常见用例是在后台脚本中使用 onBeforeRequest 事件来屏蔽网络请求。

3.1.1. Background 脚本改为 V3 规则文件
3.1.1.1. 规则文件
  1. rule.json
[
  {
    "id": 1,
    "priority": 1,
    "action": { "type": "block" },
    "condition": {
      "urlFilter": "||example.com",
      "resourceTypes": ["main_frame"]
    }
  }
]
  1. 示例

[图片上传失败...(image-67c199-1705057590208)]

  1. Manifest.json 文件引入
{
  "name": "URL Blocker",
  "version": "0.1",
  "manifest_version": 3,
  "description": "Uses the chrome.declarativeNetRequest API to block requests.",
  "background": {
    "service_worker": "service_worker.js"
  },
  "declarative_net_request": {
    "rule_resources": [
      {
        "id": "ruleset_1",
        "enabled": true,
        "path": "rules_1.json"
      }
    ]
  },
  "permissions": ["declarativeNetRequest", "declarativeNetRequestFeedback"]
}
3.1.1.2. V2 版本
chrome.webRequest.onBeforeRequest.addListener((e) => {
    return { cancel: true };
}, { urls: ["https://www.example.com/*"] }, ["blocking"]);
3.1.1.3. V3 版本

对于 Manifest V3,请使用 "block" 操作类型创建新的 declarativeNetRequest 规则。请注意示例规则中的 "condition" 对象。其 "urlFilter" 取代了传递给 webRequest 监听器的 urls 选项。"resourceTypes" 数组指定要屏蔽的资源的类别。

[
  {
    "id" : 1,
    "priority": 1,
    "action" : { "type" : "block" },
    "condition" : {
      "urlFilter" : "||example.com",
      "resourceTypes" : ["main_frame"]
    }
  }
]
3.1.2. 需要更新该扩展程序的权限。

manifest.json 中,将 "webRequestBlocking" 权限替换为 "declarativeNetRequest" 权限。请注意,由于屏蔽内容不需要主机权限,因此该网址已从 "permissions" 字段中移除。

3.1.2.1. V2 版本
  "permissions": [
    "webRequestBlocking",
    "https://*.example.com/*"
  ]
3.1.2.2. V3 版本
"permissions": [
  "declarativeNetRequest",
]

3.2. 重定向多个网址

Manifest V2 中的另一个常见用例是使用 BeforeRequest 事件重定向网络请求。

3.2.1. Background 脚本改为 V3 规则文件
3.2.1.1. V2 版本
chrome.webRequest.onBeforeRequest.addListener((e) => {
    console.log(e);
    return { redirectUrl: "https://developer.chrome.com/docs/extensions/mv3/intro/" };
  }, { 
    urls: [
      "https://developer.chrome.com/docs/extensions/mv2/"
    ]
  }, 
  ["blocking"]
);
3.2.1.2. V3 版本

对于 Manifest V3,请使用 "redirect" 操作类型。与之前一样,"urlFilter" 会替换传递给 webRequest 监听器的 url 选项。请注意,在此示例中,规则文件的 "action" 对象包含一个 "redirect" 字段,其中包含要返回的网址,而不是要过滤的网址。

[
  {
    "id" : 1,
    "priority": 1,
    "action": {
      "type": "redirect",
      "redirect": { "url": "https://developer.chrome.com/docs/extensions/mv3/intro/" }
    },
    "condition": {
      "urlFilter": "https://developer.chrome.com/docs/extensions/mv2/",
      "resourceTypes": ["main_frame"]
    }
  }
3.2.2. 需要更改扩展程序的权限

"webRequestBlocking" 权限替换为 "declarativeNetRequest" 权限。系统再次将这些网址从 manifest.json 移到了规则文件中。请注意,除了主机权限之外,重定向还需要 "declarativeNetRequestWithHostAccess" 权限。

3.2.2.1. V2 版本
  "permissions": [
    "webRequestBlocking",
    "https://developer.chrome.com/docs/extensions/*",
    "https://developer.chrome.com/docs/extensions/reference"
  ]
3.2.2.2. V3 版本
  "permissions": [
    "declarativeNetRequestWithHostAccess"
  ],
  "host_permissions": [
    "https://developer.chrome.com/*"
  ]

3.3. 屏蔽 Cookie

Manifest V2 中,要屏蔽 Cookie,需要先拦截网络请求标头,然后再发送这些标头并移除特定的 Cookie

3.3.1. Background 脚本改为 V3 规则文件
3.3.1.1. V2 版本
chrome.webRequest.onBeforeSendHeaders.addListener(
  function(details) {
    removeHeader(details.requestHeaders, 'cookie');
    return {requestHeaders: details.requestHeaders};
  },
  // filters
  {urls: ['https://*/*', 'http://*/*']},
  // extraInfoSpec
  ['blocking', 'requestHeaders', 'extraHeaders']);
3.3.1.2. V3 版本

Manifest V3 也通过规则文件中的规则实现这一点。这次的操作类型为 "modifyHeaders"。该文件接受 "requestHeaders" 对象数组,用于指定要修改的标头以及如何修改这些标头。请注意,"condition" 对象仅包含 "resourceTypes" 数组。它支持的值与前面的示例相同。

[
  {
    "id": 1,
    "priority": 1,
    "action": {
      "type": "modifyHeaders",
      "requestHeaders": [
        { "header": "cookie", "operation": "remove" }
      ]
    },
    "condition": {
      "urlFilter": "|*?no-cookies=1",
      "resourceTypes": ["main_frame"]
    }
  }
]
3.3.2. 需要更新该扩展程序的权限。

"webRequestBlocking" 权限替换为 "declarativeNetRequest" 权限。

3.3.2.1. V2 版本
  "permissions": [
    "webRequestBlocking",
    "https://developer.chrome.com/docs/extensions/*",
    "https://developer.chrome.com/docs/extensions/reference"
  ]
3.3.2.2. V3 版本
  "permissions": [
    "declarativeNetRequestWithHostAccess"
  ],
  "host_permissions": [
    "https://developer.chrome.com/*"
  ]

引用

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

推荐阅读更多精彩内容