Node.js 模塊系統(tǒng)源碼探微

Node.js 的出現(xiàn)使得前端工程師可以跨端工作在服務(wù)器上嫌术,當(dāng)然献烦,一個(gè)新的運(yùn)行環(huán)境的誕生亦會帶來新的模塊、功能与帆、抑或是思想上的革新了赌,本文將帶領(lǐng)讀者領(lǐng)略 Node.js (以下簡稱 Node) 的模塊設(shè)計(jì)思想以及剖析部分核心源碼實(shí)現(xiàn)。

CommonJS 規(guī)范

Node 最初遵循 CommonJS 規(guī)范來實(shí)現(xiàn)自己的模塊系統(tǒng)玄糟,同時(shí)做了一部分區(qū)別于規(guī)范的定制勿她。CommonJS 規(guī)范是為了解決 JavaScript 的作用域問題而定義的模塊形式,它可以使每個(gè)模塊在它自身的命名空間中執(zhí)行阵翎。

該規(guī)范強(qiáng)調(diào)模塊必須通過module.exports導(dǎo)出對外的變量或函數(shù)逢并,通過require()來導(dǎo)入其他模塊的輸出到當(dāng)前模塊作用域中之剧,同時(shí),遵循以下約定:

在模塊中砍聊,必須暴露一個(gè) require 變量背稼,它是一個(gè)函數(shù),require 函數(shù)接受一個(gè)模塊標(biāo)識符玻蝌,require 返回外部模塊的導(dǎo)出的 API蟹肘。如果要求的模塊不能被返回則 require 必須拋出一個(gè)錯(cuò)誤。

在模塊中俯树,必須有一個(gè)自由變量叫做 exports帘腹,它是一個(gè)對象,模塊在執(zhí)行時(shí)可以在 exports 上掛載模塊的屬性许饿。模塊必須使用 exports 對象作為唯一的導(dǎo)出方式阳欲。

在模塊中,必須有一個(gè)自由變量 module米辐,它也是一個(gè)對象胸完。module 對象必須有一個(gè) id 屬性,它是這個(gè)模塊的頂層 id翘贮。id 屬性必須是這樣的赊窥,require(module.id)會從源出module.id的那個(gè)模塊返回 exports 對象(就是說 module.id 可以被傳遞到另一個(gè)模塊,而且在要求它時(shí)必須返回最初的模塊)狸页。

Node 對 CommonJS 規(guī)范的實(shí)現(xiàn)

定義了模塊內(nèi)部的 module.require 函數(shù)和全局的 require 函數(shù)锨能,用來加載模塊。

在 Node 模塊系統(tǒng)中芍耘,每個(gè)文件都被視為一個(gè)獨(dú)立的模塊址遇。模塊被加載時(shí),都會初始化為 Module 對象的實(shí)例斋竞,Module 對象的基本實(shí)現(xiàn)和屬性如下所示:

function Module(id ="", parent) {// 模塊 id,通常為模塊的絕對路徑this.id = id;this.path = path.dirname(id);this.exports = {};// 當(dāng)前模塊調(diào)用者this.parent = parent;? updateChildren(parent,this,false);this.filename =null;// 模塊是否加載完成 this.loaded =false;// 當(dāng)前模塊所引用的模塊this.children = [];}

每一個(gè)模塊都對外暴露自己的 exports 屬性作為使用接口倔约。

模塊導(dǎo)出以及引用

在 Node 中,可使用 module.exports 對象整體導(dǎo)出一個(gè)變量或者函數(shù)坝初,也可將需要導(dǎo)出的變量或函數(shù)掛載到 exports 對象的屬性上浸剩,代碼如下所示:

// 1. 使用 exports: 筆者習(xí)慣通常用作對工具庫函數(shù)或常量的導(dǎo)出exports.name ='xiaoxiang';exports.add = (a, b) => a + b;// 2. 使用 module.exports:導(dǎo)出一整個(gè)對象或者單一函數(shù)...module.exports= {? add,? minus}

通過全局 require 函數(shù)引用模塊,可傳入模塊名稱鳄袍、相對路徑或者絕對路徑绢要,當(dāng)模塊文件后綴為 js / json / node 時(shí),可省略后綴拗小,如下代碼所示:

// 引用模塊const{ add, minus } =require('./module');consta =require('/usr/app/module');consthttp =require('http');

注意事項(xiàng):

exports變量是在模塊的文件級作用域內(nèi)可用的重罪,且在模塊執(zhí)行之前賦值給module.exports。

exports.name ='test';console.log(module.exports.name);// testmodule.export.name ='test';console.log(exports.name);// test

如果為exports賦予了新值,則它將不再綁定到module.exports剿配,反之亦然:

exports= { name:'test'};console.log(module.exports.name,exports.name);// undefined, test

當(dāng)module.exports屬性被新對象完全替換時(shí)搅幅,通常也需要重新賦值exports:

module.exports=exports= { name:'test'};console.log(module.exports.name,exports.name)// test, test

模塊系統(tǒng)實(shí)現(xiàn)分析

模塊定位

以下是require函數(shù)的代碼實(shí)現(xiàn):

// require 入口函數(shù)Module.prototype.require =function(id){//...requireDepth++;try{returnModule._load(id,this,/* isMain */false);// 加載模塊}finally{? ? requireDepth--;? }};

上述代碼接收給定的模塊路徑,其中的 requireDepth 用來記載模塊加載的深度呼胚。其中 Module 的類方法_load實(shí)現(xiàn)了 Node 加載模塊的主要邏輯盏筐,下面我們來解析Module._load函數(shù)的源碼實(shí)現(xiàn),為了方便大家理解砸讳,我把注釋加在了文中。

Module._load =function(request, parent, isMain){// 步驟一:解析出模塊的全路徑constfilename = Module._resolveFilename(request, parent, isMain);// 步驟二:加載模塊界牡,具體分三種情況處理// 情況一:存在緩存的模塊簿寂,直接返回模塊的 exports 屬性constcachedModule = Module._cache[filename];if(cachedModule !==undefined)returncachedModule.exports;// 情況二:加載內(nèi)建模塊constmod = loadNativeModule(filename, request);if(mod && mod.canBeRequiredByUsers)returnmod.exports;// 情況三:構(gòu)建模塊加載constmodule=newModule(filename, parent);// 加載過之后就進(jìn)行模塊實(shí)例緩存Module._cache[filename] =module;// 步驟三:加載模塊文件module.load(filename);// 步驟四:返回導(dǎo)出對象returnmodule.exports;};

加載策略

上面的代碼信息量比較大,我們主要看以下幾個(gè)問題:

模塊的緩存策略是什么宿亡?

分析上述代碼我們可以看到常遂,?_load加載函數(shù)針對三種情況給出了不同的加載策略,分別是:

情況一:緩存命中挽荠,直接返回克胳。

情況二:內(nèi)建模塊,返回暴露出來的 exports 屬性圈匆,也就是 module.exports 的別名漠另。

情況三:使用文件或第三方代碼生成模塊,最后返回跃赚,并且緩存笆搓,這樣下次同樣的訪問就會去使用緩存而不是重新加載。


Module._resolveFilename(request, parent, isMain) 是怎么解析出文件名稱的纬傲?

我們看如下定義的類方法:

Module._resolveFilename =function(request, parent, isMain, options){if(NativeModule.canBeRequiredByUsers(request)) {// 優(yōu)先加載內(nèi)建模塊returnrequest; }letpaths;// node require.resolve 函數(shù)使用的 options满败,options.paths 用于指定查找路徑if(typeofoptions ==="object"&& options !==null) {if(ArrayIsArray(options.paths)) {constisRelative =? ? ? request.startsWith("./") ||? ? ? request.startsWith("../") ||? ? ? (isWindows && request.startsWith(".\\")) ||? ? ? request.startsWith("..\\");if(isRelative) {? ? ? paths = options.paths;? ? }else{constfakeParent =newModule("",null);? ? ? paths = [];for(leti =0; i < options.paths.length; i++) {constpath = options.paths[i];? ? ? ? fakeParent.paths = Module._nodeModulePaths(path);constlookupPaths = Module._resolveLookupPaths(request, fakeParent);for(letj =0; j < lookupPaths.length; j++) {if(!paths.includes(lookupPaths[j])) paths.push(lookupPaths[j]);? ? ? ? }? ? ? }? ? }? }elseif(options.paths ===undefined) {? ? paths = Module._resolveLookupPaths(request, parent);? }else{//...} }else{// 查找模塊存在路徑paths = Module._resolveLookupPaths(request, parent); }// 依據(jù)給出的模塊和遍歷地址數(shù)組,以及是否為入口模塊來查找模塊路徑constfilename = Module._findPath(request, paths, isMain);if(!filename) {constrequireStack = [];for(letcursor = parent; cursor; cursor = cursor.parent) {? ? requireStack.push(cursor.filename || cursor.id);? }// 未找到模塊叹括,拋出異常(是不是很熟悉的錯(cuò)誤)letmessage =`Cannot find module '${request}'`;if(requireStack.length >0) {? ? message = message +"\nRequire stack:\n- "+ requireStack.join("\n- ");? }consterr =newError(message);? err.code ="MODULE_NOT_FOUND";? err.requireStack = requireStack;throwerr; }// 最終返回包含文件名的完整路徑returnfilename;};

上面的代碼中比較突出的是使用了_resolveLookupPaths和_findPath兩個(gè)方法算墨。

_resolveLookupPaths: 通過接受模塊名稱和模塊調(diào)用者,返回提供_findPath使用的遍歷范圍數(shù)組汁雷。

// 模塊文件尋址的地址數(shù)組方法Module._resolveLookupPaths =function(request, parent){if(NativeModule.canBeRequiredByUsers(request)) {? ? ? debug("looking for %j in []", request);returnnull;? ? }// 如果不是相對路徑if(? ? ? request.charAt(0) !=="."||? ? ? (request.length >1&&? ? ? ? request.charAt(1) !=="."&&? ? ? ? request.charAt(1) !=="/"&&? ? ? ? (!isWindows || request.charAt(1) !=="\\"))? ? ) {/**

? ? ? * 檢查 node_modules 文件夾

? ? ? * modulePaths 為用戶目錄净嘀,node_path 環(huán)境變量指定目錄、全局 node 安裝目錄

? ? ? */let paths = modulePaths;if(parent!=null&&parent.paths &&parent.paths.length) {// 父模塊的 modulePath 也要加到子模塊的 modulePath 里面摔竿,往上回溯查找paths =parent.paths.concat(paths);? ? ? }returnpaths.length >0? paths :null;? ? }// 使用 repl 交互時(shí)面粮,依次查找 ./ ./node_modules 以及 modulePathsif(!parent|| !parent.id || !parent.filename) {constmainPaths = ["."].concat(Module._nodeModulePaths("."), modulePaths);returnmainPaths;? ? }// 如果是相對路徑引入,則將父級文件夾路徑加入查找路徑constparentDir = [path.dirname(parent.filename)];returnparentDir;? };

_findPath: 依據(jù)目標(biāo)模塊和上述函數(shù)查找到的范圍继低,找到對應(yīng)的 filename 并返回熬苍。

// 依據(jù)給出的模塊和遍歷地址數(shù)組,以及是否頂層模塊來尋找模塊真實(shí)路徑Module._findPath =function(request, paths, isMain){constabsoluteRequest = path.isAbsolute(request);if(absoluteRequest) {// 絕對路徑,直接定位到具體模塊paths = [""]; }elseif(!paths || paths.length ===0) {returnfalse; }constcacheKey =? request +"\x00"+ (paths.length ===1? paths[0] : paths.join("\x00"));// 緩存路徑constentry = Module._pathCache[cacheKey];if(entry)returnentry;letexts;lettrailingSlash =? request.length >0&&? request.charCodeAt(request.length -1) === CHAR_FORWARD_SLASH;// '/'if(!trailingSlash) {? trailingSlash =/(?:^|\/)\.?\.$/.test(request); }// For each pathfor(leti =0; i < paths.length; i++) {constcurPath = paths[i];if(curPath && stat(curPath) <1)continue;constbasePath = resolveExports(curPath, request, absoluteRequest);letfilename;constrc = stat(basePath);if(!trailingSlash) {if(rc ===0) {// stat 狀態(tài)返回 0柴底,則為文件// File.if(!isMain) {if(preserveSymlinks) {// 當(dāng)解析和緩存模塊時(shí)婿脸,命令模塊加載器保持符號連接。filename = path.resolve(basePath);? ? ? ? }else{// 不保持符號鏈接filename = toRealPath(basePath);? ? ? ? }? ? ? }elseif(preserveSymlinksMain) {? ? ? ? filename = path.resolve(basePath);? ? ? }else{? ? ? ? filename = toRealPath(basePath);? ? ? }? ? }if(!filename) {if(exts ===undefined) exts = ObjectKeys(Module._extensions);// 解析后綴名filename = tryExtensions(basePath, exts, isMain);? ? }? }if(!filename && rc ===1) {/**

? ? ? *? stat 狀態(tài)返回 1 且文件名不存在柄驻,則認(rèn)為是文件夾

? ? ? * 如果文件后綴不存在狐树,則嘗試加載該目錄下的 package.json 中 main 入口指定的文件

? ? ? * 如果不存在,然后嘗試 index[.js, .node, .json] 文件

? ? */if(exts ===undefined) exts = ObjectKeys(Module._extensions);? ? filename = tryPackage(basePath, exts, isMain, request);? }if(filename) {// 如果存在該文件鸿脓,將文件名則加入緩存Module._pathCache[cacheKey] = filename;returnfilename;? } }constselfFilename = trySelf(paths, exts, isMain, trailingSlash, request);if(selfFilename) {// 設(shè)置路徑的緩存Module._pathCache[cacheKey] = selfFilename;returnselfFilename; }returnfalse;};

模塊加載

標(biāo)準(zhǔn)模塊處理

閱讀完上面的代碼抑钟,我們發(fā)現(xiàn),當(dāng)遇到模塊是一個(gè)文件夾的時(shí)候會執(zhí)行tryPackage函數(shù)的邏輯野哭,下面簡要分析一下具體實(shí)現(xiàn)在塔。

// 嘗試加載標(biāo)準(zhǔn)模塊functiontryPackage(requestPath, exts, isMain, originalPath){constpkg = readPackageMain(requestPath);if(!pkg) {// 如果沒有 package.json 這直接使用 index 作為默認(rèn)入口文件returntryExtensions(path.resolve(requestPath,"index"), exts, isMain);? }constfilename = path.resolve(requestPath, pkg);letactual =? ? tryFile(filename, isMain) ||? ? tryExtensions(filename, exts, isMain) ||? ? tryExtensions(path.resolve(filename,"index"), exts, isMain);//...returnactual;}// 讀取 package.json 中的 main 字段functionreadPackageMain(requestPath){constpkg = readPackage(requestPath);returnpkg ? pkg.main :undefined;}

readPackage 函數(shù)負(fù)責(zé)讀取和解析 package.json 文件中的內(nèi)容,具體描述如下:

function readPackage(requestPath) {constjsonPath = path.resolve(requestPath,"package.json");constexisting = packageJsonCache.get(jsonPath);if(existing !== undefined)returnexisting;// 調(diào)用 libuv uv_fs_open 的執(zhí)行邏輯拨黔,讀取 package.json 文件蛔溃,并且緩存constjson = internalModuleReadJSON(path.toNamespacedPath(jsonPath));if(json === undefined) {// 接著緩存文件packageJsonCache.set(jsonPath,false);returnfalse;? }//...try{constparsed = JSONParse(json);constfiltered = {? ? ? name: parsed.name,? ? ? main: parsed.main,? ? ? exports: parsed.exports,? ? ? type: parsed.type? ? };? ? packageJsonCache.set(jsonPath, filtered);returnfiltered;? }catch(e) {//...}}

上面的兩段代碼完美地解釋 package.json 文件的作用,模塊的配置入口( package.json 中的 main 字段)以及模塊的默認(rèn)文件為什么是 index篱蝇,具體流程如下圖所示:

模塊文件處理

定位到對應(yīng)模塊之后贺待,該如何加載和解析呢?以下是具體代碼分析:

Module.prototype.load = function(filename) {// 保證模塊沒有加載過assert(!this.loaded);this.filename = filename;// 找到當(dāng)前文件夾的 node_modulesthis.paths = Module._nodeModulePaths(path.dirname(filename));constextension = findLongestRegisteredExtension(filename);//...// 執(zhí)行特定文件后綴名解析函數(shù) 如 js / json / nodeModule._extensions[extension](this, filename);// 表示該模塊加載成功this.loaded =true;// ... 省略 esm 模塊的支持};

后綴處理

可以看出零截,針對不同的文件后綴麸塞,Node.js 的加載方式是不同的,一下針對.js, .json, .node簡單進(jìn)行分析瞻润。

.js 后綴 js 文件讀取主要通過 Node 內(nèi)置 APIfs.readFileSync實(shí)現(xiàn)喘垂。

Module._extensions[".js"] =function(module, filename){// 讀取文件內(nèi)容constcontent = fs.readFileSync(filename,"utf8");// 編譯執(zhí)行代碼module._compile(content, filename);};

.json 后綴 JSON 文件的處理邏輯比較簡單,讀取文件內(nèi)容后執(zhí)行JSONParse即可拿到結(jié)果绍撞。

Module._extensions[".json"] =function(module, filename){// 直接按照 utf-8 格式加載文件constcontent = fs.readFileSync(filename,"utf8");//...try{// 以 JSON 對象格式導(dǎo)出文件內(nèi)容module.exports = JSONParse(stripBOM(content));? }catch(err) {//...}};

.node 后綴 .node 文件是一種由 C / C++ 實(shí)現(xiàn)的原生模塊正勒,通過 process.dlopen 函數(shù)讀取,而 process.dlopen 函數(shù)實(shí)際上調(diào)用了 C++ 代碼中的 DLOpen 函數(shù)傻铣,而 DLOpen 中又調(diào)用了 uv_dlopen, 后者加載 .node 文件章贞,類似 OS 加載系統(tǒng)類庫文件。

Module._extensions[".node"] =function(module, filename){//...returnprocess.dlopen(module, path.toNamespacedPath(filename));};

從上面的三段源碼非洲,我們看出來并且可以理解鸭限,只有 JS 后綴最后會執(zhí)行實(shí)例方法?_compile,我們?nèi)コ恍?shí)驗(yàn)特性和調(diào)試相關(guān)的邏輯來簡要的分析一下這段代碼。

編譯執(zhí)行

模塊加載完成后两踏,Node 使用 V8 引擎提供的方法構(gòu)建運(yùn)行沙箱败京,并執(zhí)行函數(shù)代碼,代碼如下所示:

Module.prototype._compile =function(content, filename){letmoduleURL;letredirects;// 向模塊內(nèi)部注入公共變量 __dirname / __filename / module / exports / require梦染,并且編譯函數(shù)constcompiledWrapper = wrapSafe(filename, content,this);constdirname = path.dirname(filename);constrequire= makeRequireFunction(this, redirects);letresult;constexports =this.exports;constthisValue = exports;constmodule=this;if(requireDepth ===0) statCache =newMap();//...// 執(zhí)行模塊中的函數(shù)result = compiledWrapper.call(? ? ? thisValue,? ? ? exports,require,module,? ? ? filename,? ? ? dirname? ? );? hasLoadedAnyUserCJSModule =true;if(requireDepth ===0) statCache =null;returnresult;};// 注入變量的核心邏輯functionwrapSafe(filename, content, cjsModuleInstance){if(patched) {constwrapper = Module.wrap(content);// vm 沙箱運(yùn)行 赡麦,直接返回運(yùn)行結(jié)果朴皆,env -> SetProtoMethod(script_tmpl, "runInThisContext", RunInThisContext);returnvm.runInThisContext(wrapper, {? ? ? filename,lineOffset:0,displayErrors:true,// 動態(tài)加載importModuleDynamically:asyncspecifier => {constloader = asyncESM.ESMLoader;returnloader.import(specifier, normalizeReferrerURL(filename));? ? ? }? ? });? }letcompiled;try{? ? compiled = compileFunction(? ? ? content,? ? ? filename,0,0,undefined,false,undefined,? ? ? [],? ? ? ["exports","require","module","__filename","__dirname"]? ? );? }catch(err) {//...}const{ callbackMap } = internalBinding("module_wrap");? callbackMap.set(compiled.cacheKey, {importModuleDynamically:asyncspecifier => {constloader = asyncESM.ESMLoader;returnloader.import(specifier, normalizeReferrerURL(filename));? ? }? });returncompiled.function;}

上述代碼中,我們可以看到在?_compile函數(shù)中調(diào)用了wrapwrapSafe函數(shù)泛粹,執(zhí)行了__dirname / __filename / module / exports / require公共變量的注入遂铡,并且調(diào)用了 C++ 的 runInThisContext 方法(位于 src/node_contextify.cc 文件)構(gòu)建了模塊代碼運(yùn)行的沙箱環(huán)境,并返回了compiledWrapper對象晶姊,最終通過compiledWrapper.call方法運(yùn)行模塊扒接。

結(jié)語

至此,Node.js 的模塊系統(tǒng)分析告一段落们衙,Node.js 世界的精彩和絕妙無窮無盡钾怔,學(xué)習(xí)的路上和諸君共勉。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末蒙挑,一起剝皮案震驚了整個(gè)濱河市蒂教,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌脆荷,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,681評論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件懊悯,死亡現(xiàn)場離奇詭異蜓谋,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)炭分,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,205評論 3 399
  • 文/潘曉璐 我一進(jìn)店門桃焕,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人捧毛,你說我怎么就攤上這事观堂。” “怎么了呀忧?”我有些...
    開封第一講書人閱讀 169,421評論 0 362
  • 文/不壞的土叔 我叫張陵师痕,是天一觀的道長。 經(jīng)常有香客問我而账,道長胰坟,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 60,114評論 1 300
  • 正文 為了忘掉前任泞辐,我火速辦了婚禮笔横,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘咐吼。我一直安慰自己吹缔,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,116評論 6 398
  • 文/花漫 我一把揭開白布锯茄。 她就那樣靜靜地躺著厢塘,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上俗冻,一...
    開封第一講書人閱讀 52,713評論 1 312
  • 那天礁叔,我揣著相機(jī)與錄音,去河邊找鬼迄薄。 笑死琅关,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的讥蔽。 我是一名探鬼主播涣易,決...
    沈念sama閱讀 41,170評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼冶伞!你這毒婦竟也來了新症?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 40,116評論 0 277
  • 序言:老撾萬榮一對情侶失蹤响禽,失蹤者是張志新(化名)和其女友劉穎徒爹,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體芋类,經(jīng)...
    沈念sama閱讀 46,651評論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡隆嗅,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,714評論 3 342
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了侯繁。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片胖喳。...
    茶點(diǎn)故事閱讀 40,865評論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖贮竟,靈堂內(nèi)的尸體忽然破棺而出丽焊,到底是詐尸還是另有隱情,我是刑警寧澤咕别,帶...
    沈念sama閱讀 36,527評論 5 351
  • 正文 年R本政府宣布技健,位于F島的核電站,受9級特大地震影響惰拱,放射性物質(zhì)發(fā)生泄漏凫乖。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,211評論 3 336
  • 文/蒙蒙 一弓颈、第九天 我趴在偏房一處隱蔽的房頂上張望帽芽。 院中可真熱鬧,春花似錦翔冀、人聲如沸导街。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,699評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽搬瑰。三九已至款票,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間泽论,已是汗流浹背艾少。 一陣腳步聲響...
    開封第一講書人閱讀 33,814評論 1 274
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留翼悴,地道東北人缚够。 一個(gè)月前我還...
    沈念sama閱讀 49,299評論 3 379
  • 正文 我出身青樓,卻偏偏與公主長得像鹦赎,于是被迫代替她去往敵國和親谍椅。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,870評論 2 361

推薦閱讀更多精彩內(nèi)容