前端各種模塊化方案總結(jié)

大綱:
一瞎访、模塊化概述
二西乖、CommonJS規(guī)范
三、ES6 Module
四、CommonJS與ES6模塊的混編
五燥狰、Node.js中的模塊化
六揣云、循環(huán)加載
七捕儒、了解:AMD-Require.js和CMD-SeaJS
八、參考鏈接

因?yàn)閮?nèi)容太多邓夕,沒(méi)有大綱不方便閱讀刘莹,所以也可以跳轉(zhuǎn) 前端各種模塊化方案總結(jié) 附帶大綱 閱讀。

文中七成左右篇幅內(nèi)容都來(lái)自于Module的語(yǔ)法和加載實(shí)現(xiàn) — 阮一峰焚刚、徹底掌握前端模塊化 — codewhy幾篇文章点弯,結(jié)合自己之前掌握的知識(shí),按自己的記憶習(xí)慣重新進(jìn)行了梳理矿咕。

一抢肛、模塊化

1.1 什么是模塊化

那么,到底什么是模塊化開(kāi)發(fā)呢痴腌?

模塊:1雌团、在通信、計(jì)算機(jī)士聪、數(shù)據(jù)處理控制系統(tǒng)的電路中锦援,可以組合和更換的硬件單元。2剥悟、大型軟件系統(tǒng)中的一個(gè)具有獨(dú)立功能的部分灵寺。

  • 現(xiàn)實(shí)生活中模塊化的例子:模塊化計(jì)算機(jī)(cpu、內(nèi)存区岗、顯卡略板、風(fēng)扇、硬盤(pán)慈缔、光驅(qū)等等模塊)叮称、谷歌模塊化手機(jī)、模塊化房屋
  • 代碼模塊化例子:日期模塊、數(shù)學(xué)計(jì)算模塊瓤檐、日志模塊等赂韵,所有這些模塊共同組成了程序軟件系統(tǒng)

模塊化:

  • 模塊化開(kāi)發(fā)就是將程序劃分成一個(gè)個(gè)(互相依賴的)小文件/模塊來(lái)開(kāi)發(fā),然后將小模塊組合起來(lái)挠蛉;
  • 這個(gè)模塊中編寫(xiě)屬于自己的邏輯代碼祭示,有自己的作用域,不會(huì)影響到其他的結(jié)構(gòu)谴古;
  • 這個(gè)模塊可以將自己希望暴露的變量鸠删、函數(shù)经磅、對(duì)象等導(dǎo)出給其結(jié)構(gòu)使用煤杀;
  • 也可以通過(guò)某種方式垦垂,導(dǎo)入另外模塊中的變量、函數(shù)恩敌、對(duì)象等瞬测;

模塊化的好處:

  1. 防止命名沖突
  2. 代碼復(fù)用(非模塊化開(kāi)發(fā)時(shí),代碼重用時(shí)纠炮,引入 js 文件的數(shù)目可能少了或者引入的順序不對(duì),會(huì)導(dǎo)致一些問(wèn)題)
  3. 高維護(hù)性(模塊之間有高耦合低內(nèi)聚的特點(diǎn))

1.2 JavaScript設(shè)計(jì)缺陷

無(wú)論你多么喜歡JavaScript灯蝴,以及它現(xiàn)在發(fā)展的有多好恢口,我們都需要承認(rèn)在Brendan Eich用了10天寫(xiě)出JavaScript的時(shí)候,它都有很多的缺陷:

  • 比如var定義的變量作用域問(wèn)題穷躁;
  • 比如JavaScript的面向?qū)ο蟛⒉荒芟癯R?guī)面向?qū)ο笳Z(yǔ)言一樣使用class耕肩;
  • 比如JavaScript沒(méi)有模塊化的問(wèn)題;

Brendan Eich本人也多次承認(rèn)過(guò)JavaScript設(shè)計(jì)之初的缺陷问潭,但是隨著JavaScript的發(fā)展以及標(biāo)準(zhǔn)化猿诸,存在的缺陷問(wèn)題基本都得到了完善。

  • JavaScript目前已經(jīng)得到了快速的發(fā)展狡忙,無(wú)論是web梳虽、移動(dòng)端、小程序端灾茁、服務(wù)器端窜觉、桌面應(yīng)用都被廣泛的使用;

在網(wǎng)頁(yè)開(kāi)發(fā)的早期北专,Brendan Eich開(kāi)發(fā)JavaScript僅僅作為一種腳本語(yǔ)言禀挫,做一些簡(jiǎn)單的表單驗(yàn)證或動(dòng)畫(huà)實(shí)現(xiàn)等,那個(gè)時(shí)候代碼還是很少的:

  • 這個(gè)時(shí)候我們只需要講JavaScript代碼寫(xiě)到<script>標(biāo)簽中即可拓颓;
  • 并沒(méi)有必要放到多個(gè)文件中來(lái)編寫(xiě)语婴;
<button id="btn">按鈕</button>

<script>
  document.getElementById("btn").onclick = function() {
    console.log("按鈕被點(diǎn)擊了");
  }
</script>

但是隨著前端和JavaScript的快速發(fā)展,JavaScript代碼變得越來(lái)越復(fù)雜了:

  • ajax的出現(xiàn),前后端開(kāi)發(fā)分離砰左,意味著后端返回?cái)?shù)據(jù)后画拾,我們需要通過(guò)JavaScript進(jìn)行前端頁(yè)面的渲染;
  • SPA的出現(xiàn)菜职,前端頁(yè)面變得更加復(fù)雜:包括前端路由青抛、狀態(tài)管理等等一系列復(fù)雜的需求需要通過(guò)JavaScript來(lái)實(shí)現(xiàn);
  • 包括Node的實(shí)現(xiàn)酬核,JavaScript編寫(xiě)復(fù)雜的后端程序蜜另,沒(méi)有模塊化是致命的硬傷;

所以嫡意,模塊化已經(jīng)是JavaScript一個(gè)非常迫切的需求举瑰。

1.3 沒(méi)有模塊化的JavaScript

1.3.1 技術(shù)方案

演變過(guò)程:

  • 全局函數(shù)

    • ”污染”了全局變量,無(wú)法保證不與其它模塊發(fā)生變量名沖突
    • 沒(méi)有模塊的劃分蔬螟,只能人為的認(rèn)為它們屬于一個(gè)模塊此迅,但是程序并不能區(qū)分哪些函數(shù)是同一個(gè)模塊
  • 將函數(shù)封裝到對(duì)象命名空間下

    • 從代碼級(jí)別可以明顯的區(qū)分出哪些函數(shù)屬于同一個(gè)模塊
    • 從某種程度上解決了變量命名沖突的問(wèn)題,但是并不能從根本上解決命名沖突
    • 會(huì)暴露所有的模塊成員旧巾,內(nèi)部狀態(tài)可以被外部改寫(xiě)耸序,不安全
    • 命名空間越來(lái)越長(zhǎng)
  • 立即函數(shù)調(diào)用表達(dá)式(IIFE,Immediately Invoked Function Expression)

    • 將模塊封裝為立即執(zhí)行函數(shù)形式鲁猩,將公有方法坎怪,通過(guò)在函數(shù)內(nèi)部返回值的形式向外暴露

    • 會(huì)有人強(qiáng)調(diào)職責(zé)單一性,不要與程序的其它部分直接交互廓握。比如當(dāng)使用到第三方依賴時(shí)搅窿,通過(guò)向匿名函數(shù)注入依賴項(xiàng)的形式,來(lái)保證模塊的獨(dú)立性隙券,還使模塊之間的依賴關(guān)系變得明顯

      var calculator=(function(){
         var add=function(v1,v2){
          return v1+v2;
         }
         return { add:add }
      })()
      
      var calculator=(function(cal,$){
        cal.add2=function(){
            var v1=$('#v1').val();
            var v2= $('#v2').val();
           return (v1-0)+(v2-0);
        }
        return cal;
      })(window.calculator||{},window.$)
      //在這告訴我要jquery
      //依賴注入
      //很牽強(qiáng)的解決文件依賴問(wèn)題的方法
      
  • IIFE也是有很大缺陷的男应,見(jiàn)下方代碼舉例

1.3.2 問(wèn)題舉例

我們先來(lái)簡(jiǎn)單體會(huì)一下沒(méi)有模塊化代碼的問(wèn)題。

我們知道娱仔,對(duì)于一個(gè)大型的前端項(xiàng)目沐飘,通常是多人開(kāi)發(fā)的(即使一個(gè)人開(kāi)發(fā),也會(huì)將代碼劃分到多個(gè)文件夾中):

  • 我們假設(shè)有兩個(gè)人:小明和小麗同時(shí)在開(kāi)發(fā)一個(gè)項(xiàng)目拟枚,并且會(huì)將自己的JavaScript代碼放在一個(gè)單獨(dú)的js文件中薪铜。
// 小明開(kāi)發(fā)了aaa.js文件,代碼如下(當(dāng)然真實(shí)代碼會(huì)復(fù)雜的多):
var flag = true;

if (flag) {
  console.log("aaa的flag為true")
}

// 小麗開(kāi)發(fā)了bbb.js文件恩溅,代碼如下:
var flag = false;

if (!flag) {
  console.log("bbb使用了flag為false");
}

很明顯出現(xiàn)了一個(gè)問(wèn)題:

  • 大家都喜歡使用flag來(lái)存儲(chǔ)一個(gè)boolean類型的值隔箍;
  • 但是一個(gè)人賦值了true,一個(gè)人賦值了false脚乡;
  • 如果之后都不再使用蜒滩,那么也沒(méi)有關(guān)系滨达;

但是,小明又開(kāi)發(fā)了ccc.js文件:

if (flag) {
  console.log("使用了aaa的flag");
}

問(wèn)題來(lái)了:小明發(fā)現(xiàn)ccc中的flag值不對(duì)

  • 對(duì)于聰明的你俯艰,當(dāng)然一眼就看出來(lái)捡遍,是小麗將flag賦值為了false;
  • 但是如果每個(gè)文件都有上千甚至更多的代碼竹握,而且有上百個(gè)文件画株,你可以一眼看出來(lái)flag在哪個(gè)地方被修改了嗎?

備注:引用路徑如下:

<script src="./aaa.js"></script>
<script src="./bbb.js"></script>
<script src="./ccc.js"></script>

所以啦辐,沒(méi)有模塊化對(duì)于一個(gè)大型項(xiàng)目來(lái)說(shuō)是災(zāi)難性的谓传。

1.3.3 IIFE的缺陷

使用IIFE解決上面的問(wèn)題:

// aaa.js
const moduleA = (function () {
  var flag = true;

  if (flag) {
    console.log("aaa的flag為true")
  }

  return { flag: flag }
})();

// bbb.js
const moduleB = (function () {
  var flag = false;

  if (!flag) {
    console.log("bbb使用了flag為false");
  }
})();

// ccc.js
const moduleC = (function() {
  const flag = moduleA.flag;
  if (flag) {
    console.log("使用了aaa的flag");
  }
})();

命名沖突的問(wèn)題,有沒(méi)有解決呢芹关?解決了续挟。

但是,我們其實(shí)帶來(lái)了新的問(wèn)題:

  • 第一侥衬,我必須記得每一個(gè)模塊中返回對(duì)象的命名诗祸,才能在其他模塊使用過(guò)程中正確的使用;
  • 第二轴总,代碼寫(xiě)起來(lái)混亂不堪直颅,每個(gè)文件中的代碼都需要包裹在一個(gè)匿名函數(shù)中來(lái)編寫(xiě);
  • 第三肘习,在沒(méi)有合適的規(guī)范情況下际乘,每個(gè)人、每個(gè)公司都可能會(huì)任意命名漂佩、甚至出現(xiàn)模塊名稱相同的情況;

所以罪塔,我們會(huì)發(fā)現(xiàn)投蝉,雖然實(shí)現(xiàn)了模塊化,但是我們的實(shí)現(xiàn)過(guò)于簡(jiǎn)單征堪,并且是沒(méi)有規(guī)范的瘩缆。

  • 我們需要制定一定的規(guī)范來(lái)約束每個(gè)人都按照這個(gè)規(guī)范去編寫(xiě)模塊化的代碼;
  • 這個(gè)規(guī)范中應(yīng)該包括核心功能:模塊本身可以導(dǎo)出暴露的屬性佃蚜,模塊又可以導(dǎo)入自己需要的屬性庸娱;

1.4 JavaScript中模塊化方案

歷史上,JavaScript 一直沒(méi)有模塊(module)體系谐算,無(wú)法將一個(gè)大程序拆分成互相依賴的小文件熟尉,再用簡(jiǎn)單的方法拼裝起來(lái)。其他語(yǔ)言都有這項(xiàng)功能洲脂,比如 Ruby 的require斤儿、Python 的import,甚至就連 CSS 都有@import。直到ES6(2015)才推出了自己的模塊化方案往果,在此之前疆液,社區(qū)制定了一些模塊加載方案,最主要的有:

先有規(guī)范陕贮,后有實(shí)現(xiàn):

二、CommonJS規(guī)范

2.1 CommonJS和Node

我們需要知道CommonJS是一個(gè)規(guī)范肮之,最初提出來(lái)是在瀏覽器意外的地方使用掉缺,并且當(dāng)時(shí)被命名為ServerJS,后來(lái)為了體現(xiàn)它的廣泛性局骤,修改為CommonJS攀圈,平時(shí)我們也會(huì)簡(jiǎn)稱為CJS。

  • Node是CommonJS在服務(wù)器端一個(gè)具有代表性的實(shí)現(xiàn)峦甩;
  • Browserify是CommonJS在瀏覽器中的一種實(shí)現(xiàn)赘来;
  • webpack打包工具具備對(duì)CommonJS的支持和轉(zhuǎn)換(后面會(huì)講到);

所以凯傲,Node中對(duì)CommonJS進(jìn)行了支持和實(shí)現(xiàn)犬辰,讓我們?cè)陂_(kāi)發(fā)node的過(guò)程中可以方便的進(jìn)行模塊化開(kāi)發(fā):

2.2 Node模塊化語(yǔ)法

2.2.1 模塊

// bar.js
const name = 'coderwhy';
const age = 18;
function sayHello(name) { console.log("Hello " + name); }

// main.js
console.log(name, age);
sayHello('kobe');

/*
上面的代碼會(huì)報(bào)錯(cuò):
 - 那么,就意味著別的模塊main中不能隨便訪問(wèn)另外一個(gè)模塊bar中的內(nèi)容冰单;
 - bar需要 導(dǎo)出 自己想要暴露的變量幌缝、函數(shù)、對(duì)象等诫欠;main從bar中 導(dǎo)入 自己想要使用的變量涵卵、函數(shù)、對(duì)象等數(shù)據(jù)之后荒叼,才能使用轿偎;
 */

在node中每一個(gè)文件都是一個(gè)獨(dú)立的模塊,有自己的作用域被廓。在一個(gè)模塊內(nèi)變量坏晦、函數(shù)、對(duì)象都屬于這個(gè)模塊嫁乘,對(duì)外是封閉的昆婿。

為了實(shí)現(xiàn)模塊的導(dǎo)出,Node中使用的是Module的類(提供了一個(gè)Module構(gòu)造函數(shù))蜓斧,每一個(gè)模塊都是Module的一個(gè)實(shí)例仓蛆,也就是module;

每個(gè)模塊(文件)中都包括CommonJS規(guī)范的核心變量:exports法精、module多律、require痴突;

  • module:是一個(gè)全局對(duì)象,代表當(dāng)前模塊狼荞。里面保存了模塊的信息路徑辽装、父子結(jié)構(gòu)信息、曝露出的對(duì)象信息相味。

    module.id            //帶有絕對(duì)路徑的模塊文件名
    module.filename      //模塊的文件名拾积,帶有絕對(duì)路徑
    module.loaded        //表示模塊是否已經(jīng)完成加載
    module.parent          //返回一個(gè)對(duì)象,表示調(diào)用該模塊的模塊丰涉。
    module.children      //返回一個(gè)數(shù)組拓巧,表示該模塊要用到的其他模塊瞧预。
    module.exports         //模塊對(duì)外輸出的值倔叼。需要打破模塊封裝性曝露的方法和屬性,都要掛載到module.exports上谷饿。其它文件加載該模塊,實(shí)際上就是讀取module.exports屬性
    
    // 在 /Users/computer/Desktop/ccc/lib.js 文件中 console.log(module);
    Module {
      id: '.',
      path: '/Users/computer/Desktop/ccc',
      exports: { name: 'test' },
      parent: null,
      filename: '/Users/computer/Desktop/ccc/main.js',
      loaded: false,
      children: [
        Module {...}
      ],
      paths: [ //查找路徑
        '/Users/computer/Desktop/ccc/node_modules',
        '/Users/computer/Desktop/node_modules',
        '/Users/computer/node_modules',
        '/Users/node_modules',
        '/node_modules'
      ]
    }
    
  • exports是module.exports的引用投慈。一起負(fù)責(zé)對(duì)模塊中的內(nèi)容進(jìn)行導(dǎo)出承耿;

  • require函數(shù)可以幫助我們導(dǎo)入其他模塊(自定義模塊、系統(tǒng)模塊伪煤、第三方庫(kù)模塊)中的內(nèi)容加袋;

在Node.js中,模塊分為兩類:

  • 第一類抱既,系統(tǒng)核心模塊(原生模塊)职烧,node自帶。用名稱直接可以加載防泵。

    • fs(file system):與文件系統(tǒng)交互
    • http:提供http服務(wù)器功能
    • os:提供了與操作系統(tǒng)相關(guān)的實(shí)用方法和屬性
    • path:處理文件路徑
    • querystring:解析url查詢字符串
    • url:解析url
    • util:提供一系列實(shí)用小工具
    • Buffer
    • 等等很多蚀之,見(jiàn)官方文檔
    • 核心模塊的源碼都在Node的lib子目錄中。為了提高運(yùn)行速度捷泞,它們安裝的時(shí)候都會(huì)被編譯成二進(jìn)制文件
  • 第二類恬总,文件模塊,也稱自定義模塊肚邢。用路徑加載。

    有一種特殊的文件模塊 — 包拭卿,被管理在node_modules文件夾中的包骡湖,也可以直接用名字加載。

2.2.2 exports導(dǎo)出

強(qiáng)調(diào):exports是一個(gè)對(duì)象峻厚,我們可以在這個(gè)對(duì)象中添加很多個(gè)屬性响蕴,添加的屬性會(huì)導(dǎo)出

// bar.js 導(dǎo)出內(nèi)容
exports.name = name;
exports.age = age;
exports.sayHello = sayHello;

// main.js 導(dǎo)入內(nèi)容
const bar = require('./bar');

上面這行代碼意味著什么呢?

  • 意味著main中的bar變量等于exports對(duì)象惠桃;
main中的bar = bar中的exports

所以浦夷,我可以編寫(xiě)下面的代碼:

const bar = require('./bar');

const name = bar.name;
const age = bar.age;
const sayHello = bar.sayHello;

console.log(name);
console.log(age);

sayHello('kobe');
模塊之間的引用關(guān)系

為了進(jìn)一步論證辖试,bar和exports是同一個(gè)對(duì)象:

  • 所以,bar對(duì)象是exports對(duì)象的淺拷貝劈狐;
  • 淺拷貝的本質(zhì)就是一種引用的賦值而已罐孝;
定時(shí)器修改對(duì)象

2.2.3 module.exports

但是Node中我們經(jīng)常導(dǎo)出東西的時(shí)候,又是通過(guò)module.exports導(dǎo)出的:

  • module.exports和exports有什么關(guān)系或者區(qū)別呢肥缔?

我們追根溯源莲兢,通過(guò)維基百科中對(duì)CommonJS規(guī)范的解析:

  • CommonJS中是沒(méi)有module.exports的概念的;
  • 但是為了實(shí)現(xiàn)模塊的導(dǎo)出续膳,Node中使用的是Module的類(提供了一個(gè)Module構(gòu)造函數(shù))改艇,每一個(gè)模塊都是Module的一個(gè)實(shí)例,也就是module坟岔;
  • module才是導(dǎo)出的真正實(shí)現(xiàn)者谒兄;
  • 所以在Node中真正用于導(dǎo)出的其實(shí)根本不是exports,而是module.exports社付。只是為了實(shí)現(xiàn)CommonJS的規(guī)范承疲,也為了使用方便,Node為每個(gè)模塊提供了一個(gè)exports對(duì)象瘦穆,讓其對(duì)module.exports有一個(gè)引用而已纪隙。
  • 相當(dāng)于在每個(gè)模塊頭部,有這樣一行命令:var exports = module.exports;

不能直接給exports扛或、module.exports賦值绵咱,這樣等于切斷了exports和module.exports的聯(lián)系。最終輸出的結(jié)果只會(huì)是module.exports的值熙兔。比如代碼這樣修改了:

2.2.4 require

1. require的加載原理

前面已經(jīng)說(shuō)過(guò)悲伶,CommonJS 的一個(gè)模塊,就是一個(gè)腳本文件住涉。

  • CommonJS是同步加載麸锉。模塊加載的順序,按照其在代碼中出現(xiàn)的順序

  • require命令第一次加載模塊時(shí)舆声,會(huì)執(zhí)行整個(gè)模塊(腳本文件)中的js代碼花沉,返回該模塊的module.exports接口數(shù)據(jù)。會(huì)在內(nèi)存生成一個(gè)該模塊對(duì)應(yīng)的module對(duì)象媳握。

// aaa.js
const name = 'coderwhy';
console.log("Hello aaa");

setTimeout(() => {
  console.log("setTimeout");
}, 1000);

// main.js
const aaa = require('./aaa'); // aaa.js中的代碼在引入時(shí)會(huì)被運(yùn)行一次

生成的對(duì)象:

{
  id: '...',  // 模塊名
  exports: { ... },  // 模塊輸出的各個(gè)接口
  loaded: true,   // 是一個(gè)布爾值碱屁,為false表示還沒(méi)有加載,為true表示已經(jīng)加載完畢蛾找。這是保證每個(gè)模塊只加載娩脾、運(yùn)行一次的關(guān)鍵。
  ...
}
  • 以后需要用到這個(gè)模塊的時(shí)候打毛,就會(huì)到exports屬性上面取值柿赊。
  • 模塊被多次引入時(shí)(多次執(zhí)行require命令)俩功,CommonJS 模塊只會(huì)在第一次加載時(shí)運(yùn)行一次,以后再加載碰声,會(huì)去緩存中取出第一次加載時(shí)生成的module對(duì)象并返回module.exports诡蜓。除非手動(dòng)清除系統(tǒng)緩存。
// main.js
const aaa = require('./aaa');
const bbb = require('./bbb');

// aaa.js
const ccc = require("./ccc");

// bbb.js
const ccc = require("./ccc");

// ccc.js
console.log('ccc被加載');  // ccc中的代碼只會(huì)運(yùn)行一次奥邮。
2. require的查找規(guī)則

我們現(xiàn)在已經(jīng)知道万牺,require是一個(gè)函數(shù),可以幫助我們引入一個(gè)文件(模塊)中導(dǎo)入的對(duì)象洽腺。

那么脚粟,require的查找規(guī)則是怎么樣的呢?官方文檔

這里我總結(jié)比較常見(jiàn)的查找規(guī)則: 導(dǎo)入格式如下:require(X)

  • 情況一:X是一個(gè)核心模塊蘸朋,比如path核无、http。直接返回核心模塊藕坯,并且停止查找

    • 加載核心模塊团南。傳入名字,不需要傳入路徑炼彪。因?yàn)镹ode.js已經(jīng)將核心模塊的文件代碼編譯到了二進(jìn)制的可執(zhí)行文件中了吐根。在加載的過(guò)程中,原生的核心模塊的優(yōu)先級(jí)是是最高的辐马。
  • 情況二:X是以 ./..//(根目錄)開(kāi)頭的

    • 在Linux或者M(jìn)Ac的操作系統(tǒng)中拷橘,/表示系統(tǒng)的根路徑。在Windows中喜爷,/表示當(dāng)前文件模塊所屬的根磁盤(pán)路徑
    • 第一步:將X當(dāng)做一個(gè)文件在對(duì)應(yīng)的目錄下查找冗疮;
      • 如果有后綴名,按照后綴名的格式查找對(duì)應(yīng)的文件
      • 如果沒(méi)有后綴名檩帐,會(huì)按照如下順序:
        1. 直接查找文件X
        2. 查找X.js文件:當(dāng)做JavaScript腳本文件解析
        3. 查找X.json文件:以JSON格式解析术幔。
          • 如果是加載json文件模塊,最好加上后綴.json湃密,能稍微的提高一點(diǎn)加載的速度诅挑。
          • json文件Node.js也是通過(guò)fs讀文件的形式讀取出來(lái)的,然后通過(guò)JSON.parse()轉(zhuǎn)換成一個(gè)對(duì)象
        4. 查找X.node文件:以編譯后的二進(jìn)制文件解析泛源。.node文件通常是c/c++寫(xiě)的一些擴(kuò)展模塊
    • 第二步:沒(méi)有找到對(duì)應(yīng)的文件揍障,將X作為一個(gè)目錄。查找目錄下面的index文件
      1. 查找X/index.js文件
      2. 查找X/index.json文件
      3. 查找X/index.node文件
    • 如果沒(méi)有找到俩由,那么報(bào)錯(cuò):not found
  • 情況三:直接是一個(gè)X(沒(méi)有路徑),并且X不是一個(gè)核心模塊

    • 比如在/Users/coderwhy/Desktop/Node/TestCode/04_learn_node/05_javascript-module/02_commonjs/main.js中編寫(xiě) require('why')

    • 查找順序:從當(dāng)前 package 的 node_modules 里面找癌蚁,找不到就到當(dāng)前 package 目錄上層 node_modules 里面取... 一直找到全局 node_modules 目錄幻梯。

    • 這樣找到的往往是文件夾兜畸,所以接下來(lái)就是處理一個(gè)文件目錄作為 Node 模塊的情況。如果文件目錄下有 package.json碘梢,就根據(jù)它的 main 字段找到 js 文件咬摇。如果沒(méi)有 package.json,那就默認(rèn)取文件夾下的 index.js煞躬。

      由于 webpack browsersify 等模塊打包工具是兼容 node 的模塊系統(tǒng)的肛鹏,自然也會(huì)進(jìn)行同樣的處理流程。不同的是恩沛,它們支持更靈活的配置在扰。比如在 webpack 里面,可以通過(guò) alias 和 external 字段配置雷客,實(shí)現(xiàn)對(duì)默認(rèn) import 邏輯的自定義芒珠。

    • 如果上面的路徑中都沒(méi)有找到,那么報(bào)錯(cuò):not found

流程圖:

  • Node.js會(huì)通過(guò)同步阻塞的方式看這個(gè)路徑是否存在搅裙。依次嘗試皱卓,直到找到為止,如果找不到部逮,報(bào)錯(cuò)

  • 優(yōu)先從緩存加載:common.js規(guī)范:載后娜汁,再次加載時(shí),去緩存中取module.exports 參考文獻(xiàn)

3. require的加載順序

如果有多個(gè)模塊的引入兄朋,那么加載順序是什么掐禁?

如果出現(xiàn)下面模塊的引用關(guān)系,那么加載順序是什么呢蜈漓?

  • 這個(gè)其實(shí)是一種數(shù)據(jù)結(jié)構(gòu):圖結(jié)構(gòu)穆桂;
  • 圖結(jié)構(gòu)在遍歷的過(guò)程中,有深度優(yōu)先搜索(DFS, depth first search)和廣度優(yōu)先搜索(BFS, breadth first search)融虽;
  • Node采用的是深度優(yōu)先算法:main -> aaa -> ccc -> ddd -> eee ->bbb

多個(gè)模塊的引入關(guān)系:

2.3 Node的源碼解析

Module類

Module.prototype.require函數(shù)

Module._load函數(shù)

三享完、ES6 Module

4.1 認(rèn)識(shí)ES6 Module

4.1.1 ES6 Module的優(yōu)勢(shì)

ES6 在語(yǔ)言標(biāo)準(zhǔn)的層面上,實(shí)現(xiàn)了模塊功能有额,而且實(shí)現(xiàn)得相當(dāng)簡(jiǎn)單般又,完全可以取代 CommonJS 和 AMD 規(guī)范,成為瀏覽器和服務(wù)器通用的模塊解決方案巍佑。

ES6 模塊的設(shè)計(jì)思想是盡量的靜態(tài)化茴迁,使得編譯時(shí)就能確定模塊的依賴關(guān)系,以及輸入和輸出的變量萤衰。CommonJS 和 AMD 模塊堕义,都只能在運(yùn)行時(shí)確定這些東西,導(dǎo)致完全沒(méi)辦法在編譯時(shí)做“靜態(tài)優(yōu)化”脆栋。

由于 ES6 模塊是編譯時(shí)加載:

  • 可以在編譯時(shí)就完成模塊加載倦卖,效率要比 CommonJS 模塊的加載方式高

  • 使得靜態(tài)分析成為可能洒擦。有了它,就能進(jìn)一步拓寬 JavaScript 的語(yǔ)法怕膛,比如引入宏(macro)和類型檢驗(yàn)(type system)這些只能靠靜態(tài)分析實(shí)現(xiàn)的功能熟嫩。

除了靜態(tài)加載帶來(lái)的各種好處,ES6 模塊還有以下好處褐捻。

  • 不再需要UMD模塊格式了掸茅,將來(lái)服務(wù)器和瀏覽器都會(huì)支持 ES6 模塊格式。目前柠逞,通過(guò)各種工具庫(kù)昧狮,其實(shí)已經(jīng)做到了這一點(diǎn)。
  • 將來(lái)瀏覽器的新 API 就能用模塊格式提供边苹,不再必須做成全局變量或者navigator對(duì)象的屬性陵且。
  • 不再需要對(duì)象作為命名空間(比如Math對(duì)象),未來(lái)這些功能可以通過(guò)模塊提供个束。

4.1.2 自動(dòng)啟動(dòng)嚴(yán)格模式

ES6 的模塊自動(dòng)采用嚴(yán)格模式慕购,不管你有沒(méi)有在模塊頭部加上"use strict";

4.1.3 瀏覽器中加載ES6 Module

1. 加載普通js文件

HTML 網(wǎng)頁(yè)中移斩,瀏覽器通過(guò)<script>標(biāo)簽加載 JavaScript 腳本。

<!-- 頁(yè)面內(nèi)嵌的腳本 -->
<script type="application/javascript"> // code </script>

<!-- 外部腳本 -->
<script type="application/javascript" src="path/to/myModule.js"> //code... </script>
  • 上面代碼中藻糖,由于瀏覽器腳本的默認(rèn)語(yǔ)言是 JavaScript江咳,因此type="application/javascript"可以省略峭火。

  • 默認(rèn)情況下爱致,瀏覽器是同步加載 JavaScript 腳本烤送,即渲染引擎遇到<script>標(biāo)簽就會(huì)停下來(lái),等到執(zhí)行完腳本糠悯,再繼續(xù)向下渲染帮坚。如果是外部腳本,還必須加入腳本下載的時(shí)間互艾。

    如果腳本體積很大试和,下載和執(zhí)行的時(shí)間就會(huì)很長(zhǎng),因此造成瀏覽器堵塞纫普,用戶會(huì)感覺(jué)到瀏覽器“卡死”了阅悍,沒(méi)有任何響應(yīng)。這顯然是很不好的體驗(yàn),所以瀏覽器允許腳本異步加載溉箕。

下面就是兩種異步加載的語(yǔ)法晦墙。

<script src="path/to/myModule.js" defer></script>
<script src="path/to/myModule.js" async></script>

上面代碼中,<script>標(biāo)簽打開(kāi)deferasync屬性肴茄,腳本就會(huì)異步加載。渲染引擎遇到這一行命令但指,就會(huì)開(kāi)始下載外部腳本寡痰,但不會(huì)等它下載和執(zhí)行,而是直接執(zhí)行后面的命令棋凳。

deferasync的區(qū)別是:

  • defer要等到整個(gè)頁(yè)面在內(nèi)存中正常渲染結(jié)束(DOM 結(jié)構(gòu)完全生成拦坠,以及其他腳本執(zhí)行完成),才會(huì)執(zhí)行剩岳;
  • async一旦下載完贞滨,渲染引擎就會(huì)中斷渲染,執(zhí)行這個(gè)腳本以后拍棕,再繼續(xù)渲染晓铆。
  • 一句話,defer是“渲染完再執(zhí)行”绰播,async是“下載完就執(zhí)行”骄噪。
  • 另外,如果有多個(gè)defer腳本蠢箩,會(huì)按照它們?cè)陧?yè)面出現(xiàn)的順序加載链蕊,而多個(gè)async腳本是不能保證加載順序的。
2. 加載ES6 Module

瀏覽器內(nèi)嵌谬泌、外鏈 ES6 模塊代碼滔韵,也使用<script>標(biāo)簽,但是都要加入type="module"屬性掌实。

type屬性設(shè)為module陪蜻,所以瀏覽器知道這是一個(gè) ES6 模塊。瀏覽器對(duì)于帶有type="module"<script>潮峦,都是異步加載囱皿,不會(huì)造成堵塞瀏覽器,即等到整個(gè)頁(yè)面渲染完忱嘹,再執(zhí)行模塊腳本嘱腥,等同于打開(kāi)了<script>標(biāo)簽的defer屬性。

<script type="module" src="./foo.js"></script>

<!-- 等同于下面代碼拘悦。如果網(wǎng)頁(yè)有多個(gè) <script type="module">齿兔,它們會(huì)按照在頁(yè)面出現(xiàn)的順序依次執(zhí)行。 -->
<script type="module" src="./foo.js" defer></script>

<!-- 
<script>標(biāo)簽的async屬性也可以打開(kāi):
    這時(shí)只要加載完成,渲染引擎就會(huì)中斷渲染立即執(zhí)行分苇。執(zhí)行完成后添诉,再恢復(fù)渲染。 
    同樣的:一旦使用了此屬性医寿,<script type="module">就不會(huì)按照在頁(yè)面出現(xiàn)的順序執(zhí)行栏赴,而是只要該模塊加載完成,就執(zhí)行該模塊靖秩。
-->
<script type="module" src="./foo.js" async></script>

ES6 模塊也允許內(nèi)嵌在網(wǎng)頁(yè)中须眷,語(yǔ)法行為與加載外部腳本完全一致。

<script type="module">
  import utils from "./utils.js";

  // other code
</script>

對(duì)于外部的模塊腳本(上例是foo.js)沟突,有幾點(diǎn)需要注意花颗。

  • 代碼是在模塊作用域之中運(yùn)行,而不是在全局作用域運(yùn)行惠拭。模塊內(nèi)部的頂層變量扩劝,外部不可見(jiàn)。
  • 模塊腳本自動(dòng)采用嚴(yán)格模式职辅,不管有沒(méi)有聲明use strict棒呛。
  • 模塊之中,可以使用import命令加載其他模塊(.js后綴不可省略罐农,需要提供絕對(duì) URL 或相對(duì) URL)条霜,也可以使用export命令輸出對(duì)外接口。
  • 模塊之中涵亏,頂層的this關(guān)鍵字返回undefined宰睡,而不是指向window。也就是說(shuō)气筋,在模塊頂層使用this關(guān)鍵字拆内,是無(wú)意義的。
  • 同一個(gè)模塊如果加載多次宠默,將只執(zhí)行一次麸恍。

下面是一個(gè)示例模塊。

import utils from 'https://example.com/js/utils.js';

const x = 1;

console.log(x === window.x); //false
console.log(this === undefined); // true

利用頂層的this等于undefined這個(gè)語(yǔ)法點(diǎn)搀矫,可以偵測(cè)當(dāng)前代碼是否在 ES6 模塊之中抹沪。

const isNotModuleScript = this !== undefined;

4.1.4 本地瀏覽的報(bào)錯(cuò)

代碼結(jié)構(gòu)如下(個(gè)人習(xí)慣)

├── index.html
├── main.js
└── modules
    └── foo.js

index.html中引入兩個(gè)js文件作為模塊:

<script src="./modules/foo.js" type="module"></script>
<script src="main.js" type="module"></script>

如果直接在瀏覽器中運(yùn)行代碼,會(huì)報(bào)如下錯(cuò)誤:

這個(gè)在MDN上面有給出解釋:

  • https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Modules
  • 你需要注意本地測(cè)試 — 如果你通過(guò)本地加載Html 文件 (比如一個(gè) file:// 路徑的文件), 你將會(huì)遇到 CORS 錯(cuò)誤瓤球,因?yàn)镴avascript 模塊安全性需要融欧。
  • 你需要通過(guò)一個(gè)服務(wù)器來(lái)測(cè)試。

我這里使用的VSCode卦羡,VSCode中有一個(gè)插件:Live Server

  • 通過(guò)插件運(yùn)行噪馏,可以將我們的代碼運(yùn)行在一個(gè)本地服務(wù)中麦到;

4.2 ES6 Module的語(yǔ)法

模塊功能主要由兩個(gè)命令構(gòu)成:exportimport

  • export命令用于規(guī)定模塊的對(duì)外接口
  • import命令用于輸入其他模塊提供的功能。

4.2.1 模塊與CommonJS模塊的區(qū)別

1. 相同點(diǎn)

與CommonJS的相同點(diǎn):一個(gè)模塊就是一個(gè)獨(dú)立的文件欠肾。該文件內(nèi)部的所有變量瓶颠,外部無(wú)法獲取。如果你希望外部能夠讀取模塊內(nèi)部的某個(gè)變量刺桃,就必須使用export關(guān)鍵字輸出該變量粹淋。

2. 導(dǎo)出的不同

CommonJS通過(guò)module.exports導(dǎo)出的是一個(gè)對(duì)象,是module.exports屬性淺拷貝后導(dǎo)出:

  • 該對(duì)象只有在腳本運(yùn)行完才會(huì)生成瑟慈。

  • 導(dǎo)出的是一個(gè)對(duì)象意味著可以將這個(gè)對(duì)象的引用在導(dǎo)入模塊中賦值給其他變量廓啊;但是最終他們指向的都是同一個(gè)對(duì)象,那么一個(gè)變量修改了對(duì)象的屬性封豪,所有的地方都會(huì)被修改;

// 導(dǎo)出
var counter = 3;
var obj = {count: 3}
function incCounter() {
    counter++;
    obj.count++;
}
module.exports = {
  counter: counter,
  incCounter: incCounter,
  obj: obj
};

// 導(dǎo)入
var mod = require('./lib');

console.log(mod.counter, mod.obj.count); // 3  3
mod.incCounter();
console.log(mod.counter, mod.obj.count); // 3  4

ES Module通過(guò)export導(dǎo)出的不是對(duì)象炒瘟,是一個(gè)個(gè)導(dǎo)出變量/函數(shù)/類本身的引用:

說(shuō)法1:

  • 它的對(duì)外接口只是一種靜態(tài)定義吹埠,在代碼靜態(tài)解析階段就會(huì)生成。JS 引擎對(duì)腳本靜態(tài)分析的時(shí)候疮装,遇到模塊加載命令import缘琅,就會(huì)生成一個(gè)只讀引用。等到腳本真正執(zhí)行時(shí)廓推,再根據(jù)這個(gè)只讀引用刷袍,到被加載的那個(gè)模塊里面去取值。
  • 換句話說(shuō)樊展,ES6 的import有點(diǎn)像 Unix 系統(tǒng)的“符號(hào)連接”呻纹,原始值變了,import加載的值也會(huì)跟著變专缠。(由于 ES6 輸入的模塊變量雷酪,只是一個(gè)“符號(hào)連接”,所以這個(gè)變量是只讀的涝婉,對(duì)它進(jìn)行重新賦值會(huì)報(bào)錯(cuò))
  • 所以哥力,import命令叫做“連接” binding 其實(shí)更合適。

說(shuō)法2:

  • export在導(dǎo)出一個(gè)變量時(shí)墩弯,js引擎會(huì)解析這個(gè)語(yǔ)法吩跋,并且創(chuàng)建模塊環(huán)境記錄(module environment record);

  • 模塊環(huán)境記錄會(huì)和變量進(jìn)行 綁定(binding)渔工,并且這個(gè)綁定是實(shí)時(shí)的锌钮;

  • 而在導(dǎo)入的地方,我們是可以實(shí)時(shí)的獲取到綁定的最新值的涨缚;

export和import綁定的過(guò)程:

還是舉上面的例子轧粟。

// lib.js
export let counter = 3;
export function incCounter() {
  counter++;
}

// main.js
import { counter, incCounter } from './lib';
console.log(counter); // 3
incCounter();
console.log(counter); // 4

上面代碼說(shuō)明策治,ES6 模塊輸入的變量counter是活的,完全反應(yīng)其所在模塊lib.js內(nèi)部的變化兰吟。

3. 導(dǎo)入的不同
// CommonJS模塊
let { stat, exists, readfile } = require('fs');

// 等同于
let _fs = require('fs');
let stat = _fs.stat;
let exists = _fs.exists;
let readfile = _fs.readfile;

上面代碼實(shí)質(zhì)會(huì)整體加載fs模塊(即加載fs的所有方法)通惫,生成一個(gè)對(duì)象(_fs),然后再?gòu)倪@個(gè)對(duì)象上面讀取 3 個(gè)方法混蔼。

// ES6模塊
import { stat, exists, readFile } from 'fs';

上面代碼實(shí)質(zhì)只是從fs模塊加載 3 個(gè)方法履腋,其他方法不加載。

4.2.2 export

export關(guān)鍵字將一個(gè)模塊中的變量惭嚣、函數(shù)遵湖、類等導(dǎo)出;

1. export <decl>

方式一:分別導(dǎo)出晚吞。在語(yǔ)句聲明的前面直接加上export關(guān)鍵字:

export const name = 'coderwhy';
export const age = 18;
export let message = "my name is why";

export function sayHello(name) {
  console.log("Hello " + name);
}

// export需要指定對(duì)外暴露的接口延旧,所以不能直接輸出一個(gè)值
// export 40; //error
2. export {}

方式二:統(tǒng)一導(dǎo)出。將所有需要導(dǎo)出的標(biāo)識(shí)符槽地,放到export后面的 {}中迁沫。它與上一種寫(xiě)法是等價(jià)的,但是應(yīng)該優(yōu)先考慮使用這種寫(xiě)法捌蚊。因?yàn)檫@樣就可以在腳本尾部集畅,一眼看清楚輸出了哪些數(shù)據(jù)。

  • 注意:這里的 {}里面不是ES6的對(duì)象字面量的增強(qiáng)寫(xiě)法缅糟,{}也不是表示一個(gè)對(duì)象的挺智;
  • 所以:export {name: name},是錯(cuò)誤的寫(xiě)法窗宦;
const name = 'coderwhy';
const age = 18;

function sayHello(name) {
  console.log("Hello " + name);
}

export {
  name,
  age,
  sayHello
}
3. export {<> as <>}

方式三:通常情況下赦颇,export輸出的變量就是本來(lái)的名字,但是可以使用as關(guān)鍵字在導(dǎo)出時(shí)給標(biāo)識(shí)符起一個(gè)別名:export {<> as <>}

export {
  name as fName,
  age as fAge,
  sayHello as fSayHello1,
  sayHello as fSayHello2, // 重命名后迫摔,sayHello可以用不同的名字輸出兩次沐扳。
}
4. export導(dǎo)出的是標(biāo)識(shí)符的地址

export語(yǔ)句輸出的接口,與其對(duì)應(yīng)的值是動(dòng)態(tài)綁定關(guān)系句占,即通過(guò)該接口沪摄,可以取到模塊內(nèi)部實(shí)時(shí)的值。

export var foo = 'bar';
setTimeout(() => foo = 'baz', 500);

上面代碼輸出變量foo纱烘,值為bar杨拐,500 毫秒之后變成baz

這一點(diǎn)與 CommonJS 規(guī)范完全不同擂啥。CommonJS 模塊輸出的是值的緩存哄陶,不存在動(dòng)態(tài)更新。

5. export導(dǎo)出同一個(gè)實(shí)例
function C() {
  this.sum = 0;
}

export let c = new C();

不同的模塊中哺壶,加載這個(gè)模塊屋吨,得到的都是同一個(gè)實(shí)例蜒谤。對(duì)c修改,其他模塊導(dǎo)入的數(shù)據(jù)也會(huì)改變

6. export書(shū)寫(xiě)位置

export命令可以出現(xiàn)在模塊的任何位置至扰,只要處于模塊頂層就可以敲长。如果處于塊級(jí)作用域內(nèi)灵疮,就會(huì)報(bào)錯(cuò)叶组,import命令也是如此糙俗。這是因?yàn)樘幱跅l件代碼塊之中,就沒(méi)法做靜態(tài)優(yōu)化了直秆,違背了 ES6 模塊的設(shè)計(jì)初衷濒募。

function foo() {
  export default 'bar' // SyntaxError
}
foo()
7. export書(shū)寫(xiě)次數(shù)

一個(gè)模塊中:export <decl>export {}圾结、export {<> as <>}都是可以出現(xiàn)0-n次的

4.2.3 import

import關(guān)鍵字負(fù)責(zé)從另外一個(gè)模塊中導(dǎo)入內(nèi)容瑰剃。

import語(yǔ)句會(huì)執(zhí)行所加載的模塊。如果同一個(gè)模塊被加載多次筝野,那么模塊里的代碼只執(zhí)行一次培他。

導(dǎo)入內(nèi)容的方式也有多種:

1. import {} from ''

方式一:選擇導(dǎo)入。import {標(biāo)識(shí)符列表} from '模塊'遗座;

注意:

  • 大括號(hào)里面的變量名,必須與被導(dǎo)入模塊對(duì)外接口的名稱相同俊扳。
  • 這里的{}也不是一個(gè)對(duì)象途蒋,里面只是存放導(dǎo)入的標(biāo)識(shí)符列表內(nèi)容;
import { name, age, sayHello } from './modules/foo.js';

console.log(name)
console.log(age);
sayHello("Kobe");
import { name } from './modules/foo.js';
import { age } from './modules/foo.js';
// 等同于
import { name, age } from './modules/foo.js';

上面代碼中馋记,雖然nameage在兩個(gè)語(yǔ)句中加載号坡,但是它們對(duì)應(yīng)的是同一個(gè)foo.js模塊。也就是說(shuō)梯醒,import語(yǔ)句是 Singleton 模式宽堆。

1. import ''的含義

import語(yǔ)句會(huì)執(zhí)行所加載的模塊,因此可以有下面的寫(xiě)法茸习。

import 'lodash'; 

上面代碼僅僅執(zhí)行lodash模塊畜隶,但是不導(dǎo)入任何值。

同樣的号胚,如果多次重復(fù)執(zhí)行同一句import語(yǔ)句籽慢,那么只會(huì)執(zhí)行一次,而不會(huì)執(zhí)行多次猫胁。

import 'lodash';
import 'lodash'; // 代碼加載了兩次`lodash`箱亿,但是只會(huì)執(zhí)行一次。
2. import {<> as <>} from ''

方式二:導(dǎo)入時(shí)給標(biāo)識(shí)符起別名: import {<> as <>} from ''

import { name as wName, age as wAge, sayHello as wSayHello } from './modules/foo.js';
3. import * as <> from ''

方式三:整體導(dǎo)入弃秆。將模塊功能放到一個(gè)模塊功能對(duì)象(a module object)上届惋,用*指定: import * as <> from ''

import * as foo from './modules/foo.js';

console.log(foo.name);
console.log(foo.age);
foo.sayHello("Kobe");

// foo.n = "add"; // Type Error: object is not extensible
// foo.f = function () {}; 

注意髓帽,模塊整體加載所在的那個(gè)對(duì)象,應(yīng)該是可以靜態(tài)分析的脑豹,所以不允許運(yùn)行時(shí)改變郑藏。上面的寫(xiě)法是不允許的。

4. import導(dǎo)入為只讀
import { name } from './modules/foo.js';
name = "mod"; // Syntax Error : 'name' is read-only;

name是只讀的晨缴。但是译秦,如果name是一個(gè)對(duì)象,改寫(xiě)其屬性是允許的击碗,并且其他模塊也可以讀到改寫(xiě)后的值筑悴。不過(guò),這種寫(xiě)法很難查錯(cuò)稍途,建議凡是輸入的變量阁吝,都當(dāng)作完全只讀,不要輕易改變它的屬性械拍。

5. import from后的路徑

import后面的from指定模塊文件的位置突勇,可以是相對(duì)路徑,也可以是絕對(duì)路徑坷虑,<font color=red>后綴名不能省略</font>甲馋。

如果不帶有路徑,只是一個(gè)模塊名迄损,那么必須有配置文件定躏,告訴 JavaScript 引擎該模塊的位置。

import { myMethod } from 'util';

上面代碼中芹敌,util是模塊文件名痊远,由于不帶有路徑,必須通過(guò)配置氏捞,告訴引擎怎么取到這個(gè)模塊碧聪。

6. import命令的提升

注意,import命令具有提升效果液茎,會(huì)提升到整個(gè)模塊的頭部逞姿,首先執(zhí)行。

foo();
import { foo } from 'my_module';

上面的代碼不會(huì)報(bào)錯(cuò)捆等,因?yàn)?code>import的執(zhí)行早于foo的調(diào)用哼凯。這種行為的本質(zhì)是,import命令是編譯階段執(zhí)行的楚里,在代碼運(yùn)行之前断部。

目前階段,通過(guò) Babel 轉(zhuǎn)碼班缎,CommonJS 模塊的require命令和 ES6 模塊的import命令蝴光,可以寫(xiě)在同一個(gè)模塊里面她渴,但是最好不要這樣做。因?yàn)?code>import在靜態(tài)解析階段執(zhí)行蔑祟,所以它是一個(gè)模塊之中最早執(zhí)行的趁耗。下面的代碼可能不會(huì)得到預(yù)期結(jié)果。

require('core-js/modules/es6.symbol');
require('core-js/modules/es6.promise');
import React from 'React';
7. import中不能使用表達(dá)式和變量

由于import是靜態(tài)執(zhí)行疆虚,所以不能使用表達(dá)式和變量苛败,這些只有在運(yùn)行時(shí)才能得到結(jié)果的語(yǔ)法結(jié)構(gòu)。

// 報(bào)錯(cuò)
import { 'f' + 'oo' } from 'my_module';

// 報(bào)錯(cuò)
let module = 'my_module';
import { foo } from module;

// 報(bào)錯(cuò)
if (x === 1) {
  import { foo } from 'module1';
} else {
  import { foo } from 'module2';
}

上面三種寫(xiě)法都會(huì)報(bào)錯(cuò)径簿,因?yàn)樗鼈冇玫搅吮磉_(dá)式罢屈、變量和if結(jié)構(gòu)。在靜態(tài)分析階段篇亭,這些語(yǔ)法都是沒(méi)法得到值的缠捌。

4.2.4 export default

1. 概述

前面我們學(xué)習(xí)的導(dǎo)出功能都是有名字的導(dǎo)出(named exports):

  • 在導(dǎo)出export時(shí)指定了名字;
  • 在導(dǎo)入import時(shí)需要知道具體的名字译蒂;

還有一種導(dǎo)出叫做默認(rèn)導(dǎo)出(default export)

  • 默認(rèn)導(dǎo)出export時(shí)可以不需要指定名字曼月;
  • 在導(dǎo)入時(shí)不需要使用 {},并且可以自己來(lái)指定名字柔昼;
  • 它也方便我們和現(xiàn)有的CommonJS等規(guī)范相互操作哑芹;
2. 導(dǎo)出與導(dǎo)入格式

也是可以導(dǎo)出變量、函數(shù)捕透、類的绩衷。

// 導(dǎo)出格式1
export default function sub(num1, num2) {
  return num1 - num2;
}

// 導(dǎo)出格式2:用在非匿名函數(shù)前
export default function() {}

// 導(dǎo)出格式3:用在函數(shù)變量前
function sub() { console.log('sub'); }
export default sub;

// 函數(shù)名`sub`,在模塊外部是無(wú)效的激率。加載的時(shí)候,視同匿名函數(shù)加載勿决。


// 導(dǎo)入格式1:常用及推薦
import sub from './modules/foo.js';
console.log(sub(20, 30));

// 導(dǎo)入格式2
import * as m from './modules/foo.js';
console.log(m.default.sub(20, 30));

// 導(dǎo)入格式3
import {default as m} from './modules/foo.js';
console.log(m.sub(20, 30));
3. export default的本質(zhì)

本質(zhì)上乒躺,export default就是輸出一個(gè)叫做default的變量或方法,然后系統(tǒng)允許你為它取任意名字低缩。所以嘉冒,下面的寫(xiě)法是有效的。

// modules.js
function add(x, y) {
  return x * y;
}
export {add as default};  // 等同于 export default add;

// app.js
import { default as foo } from 'modules'; // 等同于 import foo from 'modules';

正是因?yàn)?code>export default命令其實(shí)只是輸出一個(gè)叫做default的變量咆繁,所以它后面不能跟變量聲明語(yǔ)句讳推。

// 正確
export var a = 1;

// 正確
var a = 1;
export default a; // 含義是將變量`a`的值賦給變量`default`。所以玩般,最后一種寫(xiě)法會(huì)報(bào)錯(cuò)银觅。

// 錯(cuò)誤
// export default var a = 1;

// 同樣地,因?yàn)閌export default`命令的本質(zhì)是將后面的值坏为,賦給`default`變量究驴,所以可以直接將一個(gè)值寫(xiě)在`export default`之后镊绪。
// 正確
export default 42;
// 報(bào)錯(cuò)
// export 42; // export后面得跟聲明,或者{標(biāo)識(shí)符}
4. export default與export

注意:在一個(gè)模塊中洒忧,export default是可以與export同時(shí)使用的:

  • export default用于指定模塊的默認(rèn)輸出蝴韭。顯然,一個(gè)模塊只能有一個(gè)默認(rèn)輸出熙侍,因此export default命令只能使用一次榄鉴。
  • export是沒(méi)有限制的。export <decl>蛉抓、export {}庆尘、export {<> as <>}都是可以出現(xiàn)0-n次的
// 導(dǎo)出
export default function sub(num1, num2) {
  return num1 - num2;
}
export var name = "module1";

// 導(dǎo)入 在一條`import`語(yǔ)句中,同時(shí)輸入默認(rèn)接口和其他接口
import m, {name} from './modules/foo.js'; //m.sub芝雪、name
import * as m from './modules/foo.js'; // m.default.sub减余、m.name
import {default as m, name} from './modules/foo.js'; // m.sub、name

4.2.5 export和import結(jié)合

// bar.js 導(dǎo)出一個(gè)sum函數(shù)
export const sum = function(num1, num2) {
  return num1 + num2;
}

// foo.js做一個(gè)中轉(zhuǎn)

// main.js直接從foo中導(dǎo)入
import { sum } from './modules/foo.js';
console.log(sum(20, 30));

如果從一個(gè)模塊中導(dǎo)入的內(nèi)容惩系,我們希望再直接導(dǎo)出出去位岔,這個(gè)時(shí)候可以使用export和import的結(jié)合,寫(xiě)成一行堡牡。

// foo.js 導(dǎo)入抒抬,但是只是做一個(gè)中轉(zhuǎn)
export { sum } from './bar.js';

// 接口改名
export { sum as barSum } from './bar.js'; // 甚至在foo.js中導(dǎo)出時(shí),我們可以變化它的名字

// 整體導(dǎo)入和導(dǎo)出
export * from './bar.js';
    // 相當(dāng)于實(shí)現(xiàn)了模塊之間的繼承晤柄。注意擦剑,`export *`命令會(huì)忽略后面模塊的`default`接口。

// 默認(rèn)接口
export { default } from 'foo';

// 具名接口改為默認(rèn)接口的寫(xiě)法如下:
export { es6 as default } from './someModule';
        // 等同于
        import { es6 } from './someModule';
        export default es6;

// 默認(rèn)接口也可以改名為具名接口:
export { default as es6 } from './someModule';

// ES2020 之前芥颈,有一種`import`語(yǔ)句惠勒,沒(méi)有對(duì)應(yīng)的復(fù)合寫(xiě)法。[ES2020](https://github.com/tc39/proposal-export-ns-from)補(bǔ)上了這個(gè)寫(xiě)法爬坑。
export * as ns from "mod";
        // 等同于
        import * as ns from "mod";
        export {ns};

// 需要注意的是纠屋,寫(xiě)成一行以后,`sum`實(shí)際上并沒(méi)有被導(dǎo)入當(dāng)前模塊盾计,只是相當(dāng)于對(duì)外轉(zhuǎn)發(fā)了這個(gè)接口售担,導(dǎo)致當(dāng)前模塊不能直接使用`sum`。

為什么要這樣做呢署辉?

  • 在開(kāi)發(fā)和封裝一個(gè)功能庫(kù)時(shí)族铆,通常我們希望將暴露的所有接口放到一個(gè)文件中;
  • 這樣方便指定統(tǒng)一的接口規(guī)范哭尝,也方便閱讀哥攘;
  • 這個(gè)時(shí)候,我們就可以使用export和import結(jié)合使用;

4.2.6 import()

1. import()的背景

前面介紹過(guò)献丑,import命令會(huì)被 JavaScript 引擎靜態(tài)分析末捣,先于模塊內(nèi)的其他語(yǔ)句執(zhí)行。所以创橄,importexport命令只能在模塊的頂層箩做,是不可以在其放到邏輯代碼中(比如在if代碼塊之中,或在函數(shù)之中)的妥畏。下面的代碼會(huì)報(bào)錯(cuò):

if (true) {
  import sub from './modules/foo.js';
}

引擎處理import語(yǔ)句是在編譯時(shí)邦邦,這時(shí)不會(huì)去分析或執(zhí)行if語(yǔ)句,所以import語(yǔ)句放在if代碼塊之中毫無(wú)意義醉蚁,因此會(huì)報(bào)句法錯(cuò)誤燃辖,而不是執(zhí)行時(shí)錯(cuò)誤。

這樣的設(shè)計(jì)网棍,固然有利于編譯器提高效率黔龟,但也導(dǎo)致無(wú)法在運(yùn)行時(shí)加載模塊。在語(yǔ)法上滥玷,條件加載就不可能實(shí)現(xiàn)氏身。如果import命令要取代 Node 的require方法,這就形成了一個(gè)障礙惑畴。因?yàn)?code>require是運(yùn)行時(shí)加載模塊蛋欣,import命令無(wú)法取代require的動(dòng)態(tài)加載功能。

const path = './' + fileName;
const myModual = require(path); 
// 上面的語(yǔ)句就是動(dòng)態(tài)加載如贷,`require`到底加載哪一個(gè)模塊陷虎,只有運(yùn)行時(shí)才知道。`import`命令做不到這一點(diǎn)杠袱。

ES2020提案 引入import()函數(shù)尚猿,支持動(dòng)態(tài)加載模塊。

import(specifier)

上面代碼中楣富,import函數(shù)的參數(shù)specifier凿掂,指定所要加載的模塊的位置。import命令能夠接受什么參數(shù)菩彬,import()函數(shù)就能接受什么參數(shù),兩者區(qū)別主要是后者為動(dòng)態(tài)加載潮梯。

2. 語(yǔ)法

import()返回一個(gè) Promise 對(duì)象骗灶。下面是一個(gè)例子。

const main = document.querySelector('main');

import(`./section-modules/${someVariable}.js`)
  .then(module => {     // 加載模塊成功以后秉馏,這個(gè)模塊會(huì)作為一個(gè)對(duì)象耙旦,當(dāng)作`then`方法的參數(shù).
//.then({export1, export2} => {     // 可以使用對(duì)象解構(gòu)賦值的語(yǔ)法,獲取輸出接口萝究。
//.then({default: theDefault} => {  // 如果是default,那么需要解構(gòu)重命名
    
    module.loadPageInto(main); // module.default來(lái)使用默認(rèn)導(dǎo)出
  })
  .catch(err => {
    main.textContent = err.message;
  });

// 如果想同時(shí)加載多個(gè)模塊,可以采用下面的寫(xiě)法狰晚。
Promise.all([
  import('./module1.js'),
  import('./module2.js'),
  import('./module3.js'),
])
.then(([module1, module2, module3]) => {
   ···
});

// 返回值是Promise對(duì)象芜抒,所以也可以用在async函數(shù)中
async function main() {
  const myModule = await import('./myModule.js');
  const {export1, export2} = await import('./myModule.js');
  const [module1, module2, module3] =
    await Promise.all([
      import('./module1.js'),
      import('./module2.js'),
      import('./module3.js'),
    ]);
}
main();

import()函數(shù)可以用在任何地方,不僅僅是模塊渊跋,非模塊的腳本也可以使用。它是運(yùn)行時(shí)執(zhí)行,也就是說(shuō)侨舆,什么時(shí)候運(yùn)行到這一句,就會(huì)加載指定的模塊绢陌。另外挨下,import()函數(shù)與所加載的模塊沒(méi)有靜態(tài)連接關(guān)系,這點(diǎn)也是與import語(yǔ)句不相同脐湾。import()類似于 Node 的require方法臭笆,區(qū)別主要是前者是異步加載,后者是同步加載秤掌。

3. 適用場(chǎng)合
  • 按需加載愁铺。

    import()可以在需要的時(shí)候,再加載某個(gè)模塊机杜。比如放在click事件的監(jiān)聽(tīng)函數(shù)之中帜讲,只有用戶點(diǎn)擊了按鈕,才會(huì)加載這個(gè)模塊椒拗。

  • 條件加載

    import()可以放在if代碼塊似将,根據(jù)不同的情況,加載不同的模塊蚀苛。

  • 動(dòng)態(tài)的模塊路徑

    import()允許模塊路徑動(dòng)態(tài)生成在验。

    import(f()).then(...);  // 根據(jù)函數(shù)`f`的返回結(jié)果,加載不同的模塊堵未。
    

4.2.7 應(yīng)用: 公共頭文件

介紹const命令的時(shí)候說(shuō)過(guò)腋舌,const聲明的常量只在當(dāng)前代碼塊有效。如果想設(shè)置跨模塊的常量(即跨多個(gè)文件)渗蟹,或者說(shuō)一個(gè)值要被多個(gè)模塊共享块饺,可以采用下面的寫(xiě)法。

// constants.js 模塊
export const A = 1;
export const B = 3;
export const C = 4;

// test1.js 模塊
import * as constants from './constants';
console.log(constants.A); // 1
console.log(constants.B); // 3

// test2.js 模塊
import {A, B} from './constants';
console.log(A); // 1
console.log(B); // 3

如果要使用的常量非常多雌芽,可以建一個(gè)專門(mén)的constants目錄授艰,將各種常量寫(xiě)在不同的文件里面,保存在該目錄下世落。

// constants/db.js
export const db = {
  url: 'http://my.couchdbserver.local:5984',
  admin_username: 'admin',
  admin_password: 'admin password'
};

// constants/user.js
export const users = ['root', 'admin', 'staff', 'ceo', 'chief', 'moderator'];

然后淮腾,將這些文件輸出的常量,合并在index.js里面。

// constants/index.js
export {db} from './db';
export {users} from './users';

使用的時(shí)候谷朝,直接加載index.js就可以了洲押。

// script.js
import {db, users} from './constants/index.js';

4.2.8 與CommonJS模塊化的差異

  • CommonJS 模塊輸出的是一個(gè)值的拷貝(module.exports的淺拷貝),ES6 模塊輸出的是值的引用圆凰。
  • CommonJS 模塊是運(yùn)行時(shí)加載杈帐,ES6 模塊是編譯(解析)時(shí)加載。
    • 運(yùn)行時(shí)加載意味著是js引擎在 執(zhí)行js代碼的過(guò)程中 加載模塊送朱;所以require可以與變量娘荡、表達(dá)式等運(yùn)行時(shí)代碼結(jié)合使用

    • 編譯時(shí)(解析)時(shí)加載,意味著import不能和運(yùn)行時(shí)相關(guān)的內(nèi)容放在一起使用:

      • 比如from后面的路徑需要?jiǎng)討B(tài)獲仁徽印炮沐;
      • 比如不能將import放到if等語(yǔ)句的代碼塊中;
      • 所以我們有時(shí)候也稱ES Module是靜態(tài)解析的回怜,而不是動(dòng)態(tài)或者運(yùn)行時(shí)解析的大年;
  • CommonJS 模塊的require()是同步加載模塊,ES6 模塊的import命令是異步加載玉雾,有一個(gè)獨(dú)立的模塊依賴的解析階段翔试。
    • 同步的就意味著一個(gè)文件沒(méi)有加載結(jié)束之前,后面的代碼都不會(huì)執(zhí)行复旬;
    • 異步的意味著:不會(huì)阻塞主線程繼續(xù)執(zhí)行垦缅;
      • JS引擎在遇到import時(shí)會(huì)去獲取這個(gè)js文件的過(guò)程是異步的
      • 設(shè)置了 type=module 的script標(biāo)簽,相當(dāng)于加上了 async 屬性驹碍;
      • 如果我們后面有普通的script標(biāo)簽以及對(duì)應(yīng)的代碼壁涎,那么ES Module對(duì)應(yīng)的js文件和代碼不會(huì)阻塞它們的執(zhí)行;

CommonJS代碼:

console.log("main代碼執(zhí)行");

const flag = true;
if (flag) {
  // 同步加載foo文件志秃,并且執(zhí)行一次內(nèi)部的代碼
  const foo = require('./foo');
  console.log("if語(yǔ)句繼續(xù)執(zhí)行");
}

ES Module代碼:

<script src="main.js" type="module"></script>
<!-- 這個(gè)js文件的代碼不會(huì)被阻塞執(zhí)行 -->
<script src="index.js"></script>

四怔球、CommonJS模塊與ES6模塊的混編

4.3 CommonJS模塊加載ES6模塊

通常情況下,CommonJS不能加載ES Module

  • 因?yàn)镃ommonJS是同步加載的浮还,但是ES Module必須經(jīng)過(guò)靜態(tài)分析等竟坛,無(wú)法在這個(gè)時(shí)候執(zhí)行JavaScript代碼;
  • 但是這個(gè)并非絕對(duì)的钧舌,某些平臺(tái)在實(shí)現(xiàn)的時(shí)候可以對(duì)代碼進(jìn)行針對(duì)性的解析担汤,也可能會(huì)支持;

可以使用import()這個(gè)方法加載

(async () => {
  await import('./my-app.mjs');
})();

上面代碼可以在 CommonJS 模塊中運(yùn)行洼冻。

require()不支持 ES6 模塊的一個(gè)原因是崭歧,它是同步加載,而 ES6 模塊內(nèi)部可以使用頂層await命令碘赖,導(dǎo)致無(wú)法被同步加載驾荣。

4.2 ES6模塊加載CommonJS模塊

多數(shù)情況下,ES Module可以加載CommonJS普泡,但是只能整體加載播掷,不能只加載單一的輸出項(xiàng)。

  • ES Module在加載CommonJS時(shí)撼班,會(huì)將其module.exports導(dǎo)出的內(nèi)容作為default導(dǎo)出方式來(lái)使用歧匈;
  • 這個(gè)依然需要看具體的實(shí)現(xiàn),比如webpack中是支持的砰嘁、Node最新的Current(v14.13.1)版本也是支持的件炉;
// foo.js
const address = 'foo的address';

module.exports = {
  address
}

// main.js
import foo from './modules/foo.js';
console.log(foo.address);

還有一種變通的加載方法,就是使用 Node.js 內(nèi)置的module.createRequire()方法矮湘。

// cjs.cjs
module.exports = 'cjs';

// esm.mjs
import { createRequire } from 'module';

const require = createRequire(import.meta.url);

const cjs = require('./cjs.cjs');
cjs === 'cjs'; // true

上面代碼中斟冕,ES6 模塊通過(guò)module.createRequire()方法可以加載 CommonJS 模塊。但是缅阳,這種寫(xiě)法等于將 ES6 和 CommonJS 混在一起了磕蛇,所以不建議使用。

4.3 使模塊同時(shí)支持兩種模塊化導(dǎo)入

一個(gè)模塊同時(shí)要支持 CommonJS 和 ES6 兩種格式十办,也很容易秀撇。

如果原始模塊是 ES6 格式,那么需要給出一個(gè)整體輸出接口向族,比如export default obj呵燕,使得 CommonJS 可以用import()進(jìn)行加載。

如果原始模塊是 CommonJS 格式件相,那么可以加一個(gè)包裝層再扭。

import cjsModule from '../index.js';
export const foo = cjsModule.foo;

上面代碼先整體輸入 CommonJS 模塊,然后再根據(jù)需要輸出具名接口适肠。

你可以把這個(gè)文件的后綴名改為.mjs霍衫,或者將它放在一個(gè)子目錄,再在這個(gè)子目錄里面放一個(gè)單獨(dú)的package.json文件侯养,指明{ type: "module" }敦跌。

如果是Node.js中,還有一種做法是在package.json文件的exports字段逛揩,指明兩種格式模塊各自的加載入口柠傍。

"exports":{
  "require": "./index.js",
  "import": "./esm/wrapper.js"
}

上面代碼指定require()import辩稽,加載該模塊會(huì)自動(dòng)切換到不一樣的入口文件惧笛。

五、Node.js中的模塊化

5.1 Node中支持 ES6 Module

JavaScript 現(xiàn)在常用的有兩種模塊逞泄。

  • ES6 模塊患整,簡(jiǎn)稱 ESM拜效;
  • CommonJS 模塊,簡(jiǎn)稱 CJS各谚。

CommonJS 模塊是 Node.js 專用的紧憾,與 ES6 模塊不兼容。語(yǔ)法上面昌渤,兩者最明顯的差異是赴穗,CommonJS 模塊使用require()module.exports,ES6 模塊使用importexport膀息。

從 Node.js v13.2 版本開(kāi)始般眉,Node.js 已經(jīng)默認(rèn)打開(kāi)了 ES6 模塊支持,需要進(jìn)行以下操作:

  • 方式一:文件以 .mjs 結(jié)尾潜支,表示使用的是ES Module甸赃;
  • 方式二:在package.json中配置字段 type: module,一旦設(shè)置了以后冗酿,該目錄里面的 JS 腳本辑奈,就被解釋用 ES6 模塊。
    • 如果這時(shí)還要使用 CommonJS 模塊已烤,那么需要將 CommonJS 腳本的后綴名都改成.cjs鸠窗。
  • 如果沒(méi)有type字段,或者type字段為commonjs胯究,則.js腳本會(huì)被解釋成 CommonJS 模塊稍计。

在之前的版本(比如v12.19.0)中,也是可以正常運(yùn)行的裕循,但是輸出臺(tái)會(huì)報(bào)一個(gè)警告:The ESM Module loader is experimental

Node.js 遇到 ES6 模塊臣嚣,默認(rèn)啟用嚴(yán)格模式,不必在每個(gè)模塊文件頂部指定"use strict"剥哑。

總結(jié)為一句話:

  • .mjs文件總是以 ES6 模塊加載
  • .cjs文件總是以 CommonJS 模塊加載
  • .js文件的加載取決于package.json里面type字段的設(shè)置硅则。

注意,ES6 模塊與 CommonJS 模塊盡量不要混用株婴。require命令不能加載.mjs文件怎虫,會(huì)報(bào)錯(cuò),只有import命令才可以加載.mjs文件困介。反過(guò)來(lái)大审,.mjs文件里面也不能使用require命令,必須使用import座哩。

5.2 Node.js包模塊的入口文件設(shè)置

5.2.1 package.json 的 main 字段

package.json文件有兩個(gè)字段可以指定模塊的入口文件:mainexports徒扶。比較簡(jiǎn)單的模塊,可以只使用main字段根穷,指定模塊加載的入口文件姜骡。

舉例:指定入口文件导坟,格式為ESM
// ./node_modules/es-module-package/package.json
{
  "type": "module",
  "main": "./src/index.js"
}

上面代碼指定項(xiàng)目的入口腳本為./src/index.js,它的格式為 ES6 模塊圈澈。如果沒(méi)有type字段乍迄,index.js就會(huì)被解釋為 CommonJS 模塊。

然后士败,import命令就可以加載這個(gè)模塊。

// ./my-app.mjs

import { something } from 'es-module-package';
// 實(shí)際加載的是 ./node_modules/es-module-package/src/index.js

上面代碼中褥伴,運(yùn)行該腳本以后谅将,Node.js 就會(huì)到./node_modules目錄下面,尋找es-module-package模塊重慢,然后根據(jù)該模塊package.jsonmain字段去執(zhí)行入口文件饥臂。

這時(shí),如果用 CommonJS 模塊的require()命令去加載es-module-package模塊會(huì)報(bào)錯(cuò)似踱,因?yàn)?CommonJS 模塊不能處理export命令隅熙。

5.2.2 package.json 的 exports 字段

exports字段的優(yōu)先級(jí)高于main字段。它有多種用法核芽。

1. 給腳本或子目錄起別名

package.json文件的exports字段可以指定腳本或子目錄的別名囚戚。

// ./node_modules/es-module-package/package.json
{
  "exports": {
    "./submodule": "./src/submodule.js",  //給腳本文件 src/submodule.js 起別名
    "./features/": "./src/features/",// 給子目錄 ./src/features/ 起別名
  }
}

通過(guò)別名加載:

import submodule from 'es-module-package/submodule';
// 加載 ./node_modules/es-module-package/src/submodule.js

import feature from 'es-module-package/features/x.js';
// 加載 ./node_modules/es-module-package/src/features/x.js

如果沒(méi)有指定別名轧简,就不能用“模塊+腳本名”這種形式加載腳本驰坊。

// 報(bào)錯(cuò)
import submodule from 'es-module-package/private-module.js';

// 不報(bào)錯(cuò)
import submodule from './node_modules/es-module-package/private-module.js';
2. main 的別名.

exports字段的別名如果是. 就代表了是模塊的主入口,優(yōu)先級(jí)高于main字段哮独,并且可以直接簡(jiǎn)寫(xiě)成exports字段的值拳芙。

{
  "exports": {
    ".": "./main.js"
  }
}

// 等同于
{
  "exports": "./main.js"
}

由于exports字段只有支持 ES6 的 Node.js 才認(rèn)識(shí),所以可以用來(lái)兼容舊版本的 Node.js皮璧。

{
  "main": "./main-legacy.cjs",
  "exports": {
    ".": "./main-modern.cjs"
  }
}

上面代碼中舟扎,老版本的 Node.js (不支持 ES6 模塊)的入口文件是main-legacy.cjs,新版本的 Node.js 的入口文件是main-modern.cjs悴务。

3. 條件加載

利用.這個(gè)別名睹限,可以為 ES6 模塊和 CommonJS 指定不同的入口。目前讯檐,這個(gè)功能需要在 Node.js 運(yùn)行的時(shí)候邦泄,打開(kāi)--experimental-conditional-exports標(biāo)志。

{
  "type": "module",
  "exports": {
    ".": {
      "require": "./main.cjs", // 別名`.`的`require`條件指定`require()`命令的入口文件(即 CommonJS 的入口)
      "default": "./main.js" // 別名`.`的`default`條件指定其他情況的入口(即 ES6 的入口)裂垦。
    }
  }
}

上面的寫(xiě)法可以簡(jiǎn)寫(xiě)如下

{
  "exports": {
    "require": "./main.cjs",
    "default": "./main.js"
  }
}

注意顺囊,如果同時(shí)還有其他別名,就不能采用簡(jiǎn)寫(xiě)蕉拢,否則或報(bào)錯(cuò)特碳。

{
  // 報(bào)錯(cuò)
  "exports": {
    "./feature": "./lib/feature.js",
    "require": "./main.cjs",
    "default": "./main.js"
  }
}

5.3 Node.js原生模塊完全支持ES6 Module

Node.js 的內(nèi)置模塊可以整體加載诚亚,也可以加載指定的輸出項(xiàng)。

// 整體加載
import EventEmitter from 'events';
const e = new EventEmitter();

// 加載指定的輸出項(xiàng)
import { readFile } from 'fs';
readFile('./foo.txt', (err, source) => {
  if (err) {
    console.error(err);
  } else {
    console.log(source);
  }
});

5.4 加載路徑

ES6 模塊的加載路徑必須給出腳本的完整路徑午乓,不能省略腳本的后綴名站宗。import命令和package.json文件的main字段如果省略腳本的后綴名,會(huì)報(bào)錯(cuò)益愈。

// ES6 模塊中將報(bào)錯(cuò)
import { something } from './index';

為了與瀏覽器的import加載規(guī)則相同梢灭,Node.js 的.mjs文件支持 URL 路徑。

import './foo.mjs?query=1'; // 加載 ./foo 傳入?yún)?shù) ?query=1

上面代碼中蒸其,腳本路徑帶有參數(shù)?query=1敏释,Node 會(huì)按 URL 規(guī)則解讀。同一個(gè)腳本只要參數(shù)不同摸袁,就會(huì)被加載多次钥顽,并且保存成不同的緩存。由于這個(gè)原因靠汁,只要文件名中含有:蜂大、%#蝶怔、?等特殊字符奶浦,最好對(duì)這些字符進(jìn)行轉(zhuǎn)義。

目前踢星,Node.js 的import命令只支持加載本地模塊(file:協(xié)議)和data:協(xié)議财喳,不支持加載遠(yuǎn)程模塊。另外斩狱,腳本路徑只支持相對(duì)路徑耳高,不支持絕對(duì)路徑(即以///開(kāi)頭的路徑)。

5.5 內(nèi)部變量

ES6 模塊應(yīng)該是通用的所踊,同一個(gè)模塊不用修改泌枪,就可以用在瀏覽器環(huán)境和服務(wù)器環(huán)境。為了達(dá)到這個(gè)目標(biāo)秕岛,Node.js 規(guī)定 ES6 模塊之中不能使用 CommonJS 模塊的特有的一些內(nèi)部變量碌燕。

首先,就是this關(guān)鍵字继薛。ES6 模塊之中修壕,頂層的this指向undefined;CommonJS 模塊的頂層this指向當(dāng)前模塊遏考,這是兩者的一個(gè)重大差異慈鸠。

其次,以下這些頂層變量在 ES6 模塊之中都是不存在的灌具。

  • arguments
  • require
  • module
  • exports
  • __filename
  • __dirname

六、循環(huán)加載

“循環(huán)加載”(circular dependency)指的是,a腳本的執(zhí)行依賴b腳本漫贞,而b腳本的執(zhí)行又依賴a腳本。

// a.js
var b = require('b');

// b.js
var a = require('a');

通常芦昔,“循環(huán)加載”表示存在強(qiáng)耦合,如果處理不好咕缎,還可能導(dǎo)致遞歸加載,使得程序無(wú)法執(zhí)行料扰,因此應(yīng)該避免出現(xiàn)凭豪。

但是實(shí)際上,這是很難避免的记罚,尤其是依賴關(guān)系復(fù)雜的大項(xiàng)目,很容易出現(xiàn)a依賴b壳嚎,b依賴c桐智,c又依賴a這樣的情況。這意味著烟馅,模塊加載機(jī)制必須考慮“循環(huán)加載”的情況说庭。

對(duì)于 JavaScript 語(yǔ)言來(lái)說(shuō),目前最常見(jiàn)的兩種模塊格式 CommonJS 和 ES6郑趁,處理“循環(huán)加載”的方法是不一樣的刊驴,返回的結(jié)果也不一樣。

6.1 CommonJS 模塊的循環(huán)加載

CommonJS 模塊的重要特性是加載時(shí)執(zhí)行寡润,即腳本代碼在require的時(shí)候捆憎,就會(huì)全部執(zhí)行。一旦出現(xiàn)某個(gè)模塊被"循環(huán)加載"梭纹,就只輸出已經(jīng)執(zhí)行的部分躲惰,還未執(zhí)行的部分不會(huì)輸出。

讓我們來(lái)看变抽,Node 官方文檔里面的例子础拨。

// a.js
exports.done = false;   // 先輸出一個(gè)`done`變量

var b = require('./b.js'); // 然后加載另一個(gè)腳本文件b.js。注意绍载,此時(shí)代碼就停在這里诡宗,等待`b.js`執(zhí)行完畢,再往下執(zhí)行击儡。

console.log('在 a.js 之中塔沃,b.done = %j', b.done); // b.js執(zhí)行完畢,返回來(lái)a.js接著往下執(zhí)行阳谍,直到執(zhí)行完畢芳悲。
exports.done = true;
console.log('a.js 執(zhí)行完畢');

// b.js
exports.done = false;

/*
 執(zhí)行到這一行立肘,會(huì)去加載a.js,這時(shí)名扛,就發(fā)生了“循環(huán)加載”谅年。系統(tǒng)會(huì)去a.js模塊對(duì)應(yīng)對(duì)象的exports屬性取值,可是因?yàn)閍.js還沒(méi)有執(zhí)行完肮韧,從exports屬性只能取回已經(jīng)執(zhí)行的部分融蹂,而不是最后的值。
 此時(shí):a.js已經(jīng)執(zhí)行的部分弄企,只有一行:exports.done = false; 即對(duì)于b.js來(lái)說(shuō)超燃,它從a.js只輸入一個(gè)變量done=false 。
 */
var a = require('./a.js'); 

console.log('在 b.js 之中拘领,a.done = %j', a.done);
exports.done = true;
console.log('b.js 執(zhí)行完畢');
// b.js接著往下執(zhí)行意乓,等到全部執(zhí)行完畢,再把執(zhí)行權(quán)交還給a.js约素。

我們寫(xiě)一個(gè)腳本main.js届良,驗(yàn)證這個(gè)過(guò)程。

var a = require('./a.js');
var b = require('./b.js');
console.log('在 main.js 之中, a.done=%j, b.done=%j', a.done, b.done);

執(zhí)行main.js圣猎,運(yùn)行結(jié)果如下:

$ node main.js

在 b.js 之中士葫,a.done = false
b.js 執(zhí)行完畢
在 a.js 之中,b.done = true
a.js 執(zhí)行完畢
在 main.js 之中, a.done=true, b.done=true

上面的代碼證明了兩件事:

  1. b.js之中送悔,a.js沒(méi)有執(zhí)行完畢慢显,只執(zhí)行了第一行。
  2. main.js執(zhí)行到第二行時(shí)欠啤,不會(huì)再次執(zhí)行b.js荚藻,而是輸出緩存的b.js的執(zhí)行結(jié)果,即它的第四行exports.done = true;

總之洁段,CommonJS 輸入的是被輸出值的拷貝鞋喇,不是引用。

另外眉撵,由于 CommonJS 模塊遇到循環(huán)加載時(shí)侦香,返回的是當(dāng)前已經(jīng)執(zhí)行的部分的值,而不是代碼全部執(zhí)行后的值纽疟,兩者可能會(huì)有差異罐韩。所以,輸入變量的時(shí)候污朽,必須非常小心散吵。

var a = require('a'); // 安全的寫(xiě)法
var foo = require('a').foo; // 危險(xiǎn)的寫(xiě)法

exports.good = function (arg) {
  return a.foo('good', arg); // 使用的是 a.foo 的最新值
};

exports.bad = function (arg) {
  return foo('bad', arg); // 使用的是一個(gè)部分加載時(shí)的值
};

上面代碼中,如果發(fā)生循環(huán)加載,require('a').foo的值很可能后面會(huì)被改寫(xiě)矾睦,改用require('a')會(huì)更保險(xiǎn)一點(diǎn)晦款。

6.2 ES6 模塊的循環(huán)加載

ES6 處理“循環(huán)加載”與 CommonJS 有本質(zhì)的不同。ES6 模塊是動(dòng)態(tài)引用枚冗,如果使用import從一個(gè)模塊加載變量(即import foo from 'foo')缓溅,那些變量不會(huì)被緩存,而是成為一個(gè)指向被加載模塊的引用赁温,需要開(kāi)發(fā)者自己保證坛怪,真正取值的時(shí)候能夠取到值。

請(qǐng)看下面這個(gè)例子股囊。

// a.mjs
import {bar} from './b';
console.log('a.mjs');
console.log(bar);
export let foo = 'foo';

// b.mjs
import {foo} from './a';
console.log('b.mjs');
console.log(foo);
export let bar = 'bar';

上面代碼中袜匿,a.mjs加載b.mjsb.mjs又加載a.mjs稚疹,構(gòu)成循環(huán)加載居灯。執(zhí)行a.mjs,結(jié)果如下内狗。

$ node --experimental-modules a.mjs
b.mjs
ReferenceError: foo is not defined

上面代碼中怪嫌,執(zhí)行a.mjs以后會(huì)報(bào)錯(cuò),foo變量未定義其屏,這是為什么喇勋?

讓我們一行行來(lái)看缨该,ES6 循環(huán)加載是怎么處理的:

  • 首先偎行,執(zhí)行a.mjs以后,引擎發(fā)現(xiàn)它加載了b.mjs贰拿,因此會(huì)優(yōu)先執(zhí)行b.mjs蛤袒,然后再執(zhí)行a.mjs
  • 接著膨更,執(zhí)行b.mjs的時(shí)候妙真,已知它從a.mjs輸入了foo接口,這時(shí)不會(huì)去執(zhí)行a.mjs荚守,而是認(rèn)為這個(gè)接口已經(jīng)存在了珍德,繼續(xù)往下執(zhí)行。
  • 執(zhí)行到第三行console.log(foo)的時(shí)候矗漾,才發(fā)現(xiàn)這個(gè)接口根本沒(méi)定義锈候,因此報(bào)錯(cuò)。

解決這個(gè)問(wèn)題的方法敞贡,就是讓b.mjs運(yùn)行的時(shí)候泵琳,foo已經(jīng)有定義了。這可以通過(guò)將foo寫(xiě)成函數(shù)來(lái)解決。

// a.mjs
import {bar} from './b';
console.log('a.mjs');
console.log(bar());
function foo() { return 'foo' }   // const foo = () => 'foo'; 仍然會(huì)執(zhí)行報(bào)錯(cuò)获列。函數(shù)表達(dá)式谷市,就不具有提升作用
export {foo};

// b.mjs
import {foo} from './a';
console.log('b.mjs');
console.log(foo());
function bar() { return 'bar' }
export {bar};

這時(shí)再執(zhí)行a.mjs就可以得到預(yù)期結(jié)果。

$ node --experimental-modules a.mjs
b.mjs
foo
a.mjs
bar

這是因?yàn)?strong>函數(shù)具有提升作用击孩,在執(zhí)行import {bar} from './b'時(shí)迫悠,函數(shù)foo就已經(jīng)有定義了,所以b.mjs加載的時(shí)候不會(huì)報(bào)錯(cuò)溯壶。

這也意味著及皂,如果把函數(shù)foo改寫(xiě)成函數(shù)表達(dá)式,也會(huì)報(bào)錯(cuò)且改。

6.3 代碼示例

我們?cè)賮?lái)看 ES6 模塊加載器SystemJS給出的一個(gè)例子验烧。

// even.js
import { odd } from './odd'
export var counter = 0;
export function even(n) {
  counter++;
  return n === 0 || odd(n - 1);
}

// odd.js
import { even } from './even';
export function odd(n) {
  return n !== 0 && even(n - 1);
}

上面代碼中,even.js里面的函數(shù)even有一個(gè)參數(shù)n又跛,只要不等于 0碍拆,就會(huì)減去 1,傳入加載的odd()慨蓝。odd.js也會(huì)做類似操作感混。

運(yùn)行上面這段代碼,結(jié)果如下礼烈。

$ babel-node
> import * as m from './even.js';
> m.even(10);
true
> m.counter
6
> m.even(20)
true
> m.counter
17

上面代碼中弧满,參數(shù)n從 10 變?yōu)?0 的過(guò)程中,even()一共會(huì)執(zhí)行 6 次此熬,所以變量counter等于 6庭呜。第二次調(diào)用even()時(shí),參數(shù)n從 20 變?yōu)?0犀忱,even()一共會(huì)執(zhí)行 11 次募谎,加上前面的 6 次,所以變量counter等于 17阴汇。

這個(gè)例子要是改寫(xiě)成 CommonJS数冬,就根本無(wú)法執(zhí)行,會(huì)報(bào)錯(cuò)搀庶。

// even.js
var odd = require('./odd');
var counter = 0;
exports.counter = counter;
exports.even = function (n) {
  counter++;
  return n == 0 || odd(n - 1);
}

// odd.js
var even = require('./even').even;
module.exports = function (n) {
  return n != 0 && even(n - 1);
}

上面代碼中拐纱,even.js加載odd.js,而odd.js又去加載even.js哥倔,形成“循環(huán)加載”秸架。這時(shí),執(zhí)行引擎就會(huì)輸出even.js已經(jīng)執(zhí)行的部分(不存在任何結(jié)果)未斑,所以在odd.js之中咕宿,變量even等于undefined币绩,等到后面調(diào)用even(n - 1)就會(huì)報(bào)錯(cuò)。

$ node
> var m = require('./even');
> m.even(10)
TypeError: even is not a function

七府阀、了解:AMD和CMD規(guī)范

7.1. CommonJS規(guī)范缺點(diǎn)

CommonJS加載模塊是同步的:

  • 同步的意味著只有等到對(duì)應(yīng)的模塊加載完畢缆镣,當(dāng)前模塊中的內(nèi)容才能被運(yùn)行;
  • 這個(gè)在服務(wù)器不會(huì)有什么問(wèn)題试浙,因?yàn)榉?wù)器加載的js文件都是本地文件董瞻,加載速度非常快田巴;

如果將它應(yīng)用于瀏覽器呢钠糊?

  • 瀏覽器加載js文件需要先從服務(wù)器將文件下載下來(lái),之后在加載運(yùn)行壹哺;
  • 那么采用同步的就意味著后續(xù)的js代碼都無(wú)法正常運(yùn)行抄伍,即使是一些簡(jiǎn)單的DOM操作;

所以在瀏覽器中管宵,我們通常不使用CommonJS規(guī)范:

  • 當(dāng)然在webpack中使用CommonJS是另外一回事截珍;
  • 因?yàn)樗鼤?huì)將我們的代碼轉(zhuǎn)成瀏覽器可以直接執(zhí)行的代碼;

在早期為了可以在瀏覽器中使用模塊化箩朴,通常會(huì)采用AMD或CMD:

  • 但是目前一方面現(xiàn)代的瀏覽器已經(jīng)支持ES Modules岗喉,另一方面借助于webpack等工具可以實(shí)現(xiàn)對(duì)CommonJS或者ES Module代碼的轉(zhuǎn)換;
  • AMD和CMD已經(jīng)使用非常少了炸庞,所以這里我們進(jìn)行簡(jiǎn)單的演練钱床;

7.2. AMD規(guī)范

7.2.1 AMD與Require.js

AMD主要是應(yīng)用于瀏覽器的一種模塊化規(guī)范:

  • AMD是Asynchronous Module Definition(異步模塊定義)的縮寫(xiě);
  • 它采用的是異步加載模塊埠居;
  • 事實(shí)上AMD的規(guī)范還要早于CommonJS查牌,但是CommonJS目前依然在被使用,而AMD使用的較少了拐格;

我們提到過(guò)僧免,規(guī)范只是定義代碼的應(yīng)該如何去編寫(xiě)刑赶,只有有了具體的實(shí)現(xiàn)才能被應(yīng)用:

  • AMD實(shí)現(xiàn)的比較常用的庫(kù)是require.js和curl.js捏浊;

7.2.2 Require.js的使用

第一步:下載require.js

第二步:定義HTML的script標(biāo)簽引入require.js和定義入口文件:

  • data-main屬性的作用是在加載完src的文件后會(huì)加載執(zhí)行該文件
<script src="./lib/require.js" data-main="./index.js"></script>

第三步:編寫(xiě)如下目錄和代碼(個(gè)人習(xí)慣)

├── index.html
├── index.js
├── lib
│   └── require.js
└── modules
    ├── bar.js
    └── foo.js

index.js

(function() {
  require.config({
    baseUrl: '',
    paths: {
      foo: './modules/foo',
      bar: './modules/bar'
    }
  })
 
  // 開(kāi)始加載執(zhí)行foo模塊的代碼
  require(['foo'], function(foo) {

  })
})();

modules/bar.js

  • 如果一個(gè)模塊不依賴其他撞叨,那么直接使用define(function)即可
define(function() {
  const name = "coderwhy";
  const age = 18;
  const sayHello = function(name) {
    console.log("Hello " + name);
  }

  return {
    name,
    age, 
    sayHello
  }
})

modules/foo.js

define(['bar'], function(bar) {
  console.log(bar.name);
  console.log(bar.age);
  bar.sayHello('kobe');
})

7.3 CMD規(guī)范

7.3.1 CMD與SeaJS

CMD規(guī)范也是應(yīng)用于瀏覽器的一種模塊化規(guī)范:

  • CMD 是Common Module Definition(通用模塊定義)的縮寫(xiě)金踪;
  • 它也采用了異步加載模塊,但是它將CommonJS的優(yōu)點(diǎn)吸收了過(guò)來(lái)牵敷;
  • 但是目前CMD使用也非常少了胡岔;

CMD也有自己比較優(yōu)秀的實(shí)現(xiàn)方案:

  • SeaJS

7.3.2 SeaJS的使用

1. 下載SeaJS
2. 引入sea.js和啟動(dòng)模塊
  • seajs是指定主入口文件的,也稱為啟動(dòng)模塊
<script src="./lib/sea.js"></script> <!--在調(diào)用 seajs 之前枷餐,必須先引入 sea.js 文件-->
<script>
  seajs.use('./index.js');  
  /*
   通過(guò) seajs.use() 函數(shù)可以啟動(dòng)模塊
        - ('模塊id' [,callback])  加載一個(gè)模塊靶瘸,并執(zhí)行回調(diào)函數(shù)
        - (['模塊1', '模塊2'] [,callback])  加載多個(gè)模塊,并執(zhí)行回調(diào)函數(shù)
        - callback 參數(shù)是可選的。格式為:function( 模塊對(duì)象 ){ 業(yè)務(wù)代碼 };
        
     - seajs.use 理論上只用于加載啟動(dòng)怨咪,不應(yīng)該出現(xiàn)在 define 中的模塊代碼里
     - seajs.use 和 DOM ready 事件沒(méi)有任何關(guān)系屋剑。要想保證 文檔結(jié)構(gòu)加載完畢再執(zhí)行你的 js 代碼,一定要在seajs.use內(nèi)部通過(guò) window.onload 或者 $(function(){})
   */
</script>
3. 編寫(xiě)如下目錄和代碼(個(gè)人習(xí)慣)
├── index.html
├── index.js
├── lib
│   └── sea.js
└── modules
    ├── bar.js
    └── foo.js
4. 定義模塊define
  • 在CMD規(guī)范中诗眨,一個(gè)模塊就是一個(gè)js文件

module是一個(gè)對(duì)象唉匾,存儲(chǔ)了模塊的元信息,具體如下:

  • module.id——模塊的ID匠楚。

  • module.dependencies——一個(gè)數(shù)組巍膘,存儲(chǔ)了此模塊依賴的所有模塊的ID列表。

  • module.exports——與exports指向同一個(gè)對(duì)象芋簿。

  • module.uri

define 是一個(gè)全局函數(shù)峡懈,用來(lái)定義模塊:define( factory )

  • 對(duì)象{}:這種方式,外部會(huì)直接獲取到該對(duì)象
  • 字符串"": 同上
  • 函數(shù):define(function(require, exports, module){ 模塊代碼 }); 為了減少出錯(cuò)与斤,定義函數(shù)的時(shí)候直接把這三個(gè)參數(shù)寫(xiě)上
5. 導(dǎo)出接口exports和module.exports
  • 功能:通過(guò)給 exports或module.exports動(dòng)態(tài)的掛載變量逮诲、函數(shù)或?qū)ο螅獠繒?huì)獲取到該接口

  • exports 等價(jià)于 module.exports幽告。exports能做什么梅鹦,module.exports就能做什么

  • 可以通過(guò)多次給exports 掛載屬性向外暴露

  • 不能直接給 exports 賦值

  • 如果想暴露單個(gè)變量、函數(shù)或?qū)ο罂梢酝ㄟ^(guò)直接給 module.exports 賦值 即可

6. 依賴模塊require
/*
 模塊標(biāo)識(shí)/模塊id
    - 模塊標(biāo)識(shí)就是一個(gè)`字符串`冗锁,用來(lái)`標(biāo)識(shí)模塊`
    - 模塊標(biāo)識(shí) 可以不包含后綴名.js
    - 以 ./或 ../ 開(kāi)頭的相對(duì)路徑模塊齐唆,相對(duì)于 require 所在模塊的路徑
    - 不以 ./ 或 ../ 開(kāi)頭的頂級(jí)標(biāo)識(shí),會(huì)相對(duì)于模塊的基礎(chǔ)路徑解析(配置項(xiàng)中的base)
    - 絕對(duì)路徑如http://127.0.0.1:8080/js/a.js冻河、/js/a.js
 */
requeire('模塊id')
/*
 1.用于根據(jù)一個(gè)模塊id加載/依賴該模塊
 2.參數(shù)必須是一個(gè)字符串
 3.該方法會(huì)得到 要加載的模塊中的 module.exports 對(duì)象
 */
  • 只能在模塊環(huán)境define中使用箍邮,define(factory)的構(gòu)造方法第一個(gè)參數(shù)必須命名為 require
  • 不要重命名require函數(shù)或者在任何作用域中給 require 重新賦值
  • 在一個(gè)模塊系統(tǒng)中,require 加載過(guò)的模塊會(huì)被緩存
  • 默認(rèn) require 是同步加載模塊的

require.async

SeaJS會(huì)在html頁(yè)面打開(kāi)時(shí)通過(guò)靜態(tài)分析一次性記載所有需要的js文件叨叙,如果想要某個(gè)js文件在用到時(shí)才下載锭弊,可以使用require.async:

require.async('/path/to/module/file', function(m) {
    //code of callback...
});

這樣只有在用到這個(gè)模塊時(shí),對(duì)應(yīng)的js文件才會(huì)被下載擂错,也就實(shí)現(xiàn)了JavaScript代碼的按需加載味滞。

SeaJS高級(jí)配置
  • alias:別名配置
  • paths:路徑配置
  • vars:變量配置
  • map:映射配置
  • preload:預(yù)加載項(xiàng)
  • debug:調(diào)試模式
  • base:基礎(chǔ)路徑
  • charset:文件編碼
代碼示例

index.js

define(function(require, exports, module) {
  const foo = require('./modules/foo');
})

bar.js

define(function(require, exports, module) {
  const name = 'lilei';
  const age = 20;
  const sayHello = function(name) {
    console.log("你好 " + name);
  }

  module.exports = {
    name,
    age,
    sayHello
  }
})

foo.js

define(function(require, exports, module) {
  const bar = require('./bar');

  console.log(bar.name);
  console.log(bar.age);
  bar.sayHello("韓梅梅");
})

八、參考鏈接

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末钮呀,一起剝皮案震驚了整個(gè)濱河市剑鞍,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌爽醋,老刑警劉巖蚁署,帶你破解...
    沈念sama閱讀 206,126評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異蚂四,居然都是意外死亡光戈,警方通過(guò)查閱死者的電腦和手機(jī)哪痰,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)久妆,“玉大人妒御,你說(shuō)我怎么就攤上這事≌蚪龋” “怎么了乎莉?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,445評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)奸笤。 經(jīng)常有香客問(wèn)我惋啃,道長(zhǎng),這世上最難降的妖魔是什么监右? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,185評(píng)論 1 278
  • 正文 為了忘掉前任边灭,我火速辦了婚禮,結(jié)果婚禮上健盒,老公的妹妹穿的比我還像新娘绒瘦。我一直安慰自己,他們只是感情好扣癣,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,178評(píng)論 5 371
  • 文/花漫 我一把揭開(kāi)白布惰帽。 她就那樣靜靜地躺著,像睡著了一般父虑。 火紅的嫁衣襯著肌膚如雪材部。 梳的紋絲不亂的頭發(fā)上舆逃,一...
    開(kāi)封第一講書(shū)人閱讀 48,970評(píng)論 1 284
  • 那天晓勇,我揣著相機(jī)與錄音师倔,去河邊找鬼。 笑死莱衩,一個(gè)胖子當(dāng)著我的面吹牛爵嗅,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播笨蚁,決...
    沈念sama閱讀 38,276評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼睹晒,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了赚窃?” 一聲冷哼從身側(cè)響起册招,我...
    開(kāi)封第一講書(shū)人閱讀 36,927評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤岔激,失蹤者是張志新(化名)和其女友劉穎勒极,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體虑鼎,經(jīng)...
    沈念sama閱讀 43,400評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡辱匿,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,883評(píng)論 2 323
  • 正文 我和宋清朗相戀三年键痛,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片匾七。...
    茶點(diǎn)故事閱讀 37,997評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡絮短,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出昨忆,到底是詐尸還是另有隱情丁频,我是刑警寧澤,帶...
    沈念sama閱讀 33,646評(píng)論 4 322
  • 正文 年R本政府宣布邑贴,位于F島的核電站席里,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏拢驾。R本人自食惡果不足惜奖磁,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,213評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望繁疤。 院中可真熱鬧咖为,春花似錦、人聲如沸稠腊。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,204評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)架忌。三九已至褐啡,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間鳖昌,已是汗流浹背备畦。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,423評(píng)論 1 260
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留许昨,地道東北人懂盐。 一個(gè)月前我還...
    沈念sama閱讀 45,423評(píng)論 2 352
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像糕档,于是被迫代替她去往敵國(guó)和親莉恼。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,722評(píng)論 2 345

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