溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

Node中exports有哪些設(shè)計模式

發(fā)布時間:2021-07-21 09:25:29 來源:億速云 閱讀:174 作者:Leah 欄目:web開發(fā)

本篇文章給大家分享的是有關(guān)Node中exports有哪些設(shè)計模式,小編覺得挺實用的,因此分享給大家學(xué)習(xí),希望大家閱讀完這篇文章后可以有所收獲,話不多說,跟著小編一起來看看吧。

首先我們需要先聊點基礎(chǔ)的知識

在Node官方文件中定義了匯入一個檔案就是匯入一個模塊。

In Node.js,files and modules are in one-to-one correspondence.- Node文件

也就是所有的模塊會參考指向(Reference)一個隱式模塊物件的module.exports。當(dāng)我們使用require()時會取得的東西。同時我們也取得exports。

這個exports就是指向module.exports的參考。exports會收集該其屬性,如果module.exports沒有任何屬性就把這些數(shù)據(jù)交給module.exports,但如果module.exports已經(jīng)具備屬性的話,那么exports的所有數(shù)據(jù)都會被忽略。

為了讓您更好理解關(guān)于exports與module.exports下面的示例提供了較詳細的說明

var a = { id: 1 } var b = a console.log(a)// {id: 1} console.log(b)// {id: 1}  // b參考指向a,意味著修改b的屬性a會跟著變動 b.id = 2 console.log(a)// {id: 2} console.log(b)// {id: 2}  //但如果將一個全新的物件賦予b那么參考的關(guān)系將會中斷 b = { id: 3 } console.log(a)// {id: 2} console.log(b)// {id: 3}

另外比較具體的示例

/* person.js */ exports.name = function(){  console.log('My name is andyyou.') } … /* main.js */ var person = require('./person.js') person.name() /* person.js */ module.exports = 'Hey,andyyou' exports.name = function(){  console.log('My name is andyyou') }  /* main.js */ var person = require('./person.js') // exports的屬性被忽略了 person.name()// TypeError: Object Hey,andyyou has no method 'name'
  • exports只是指向module.exports的參考(Reference)

  • module.exports初始值為{}空物件,于是exports也會取得該空物件

  • require()回傳的是module.exports而不是exports

  • 所以您可以使用exports.property_name = something而不會使用exports = something

  • 一旦使用exports = something參考關(guān)系便會停止,也就是exports的數(shù)據(jù)都會被忽略。

本質(zhì)上我們可以理解為所有模塊都隱含實作了下面這行代碼

var exports = module.exports = {}

現(xiàn)在我們知道了,當(dāng)我們要導(dǎo)出一個function時我們得使用module.exports。

如果使用exports那個exports的內(nèi)存位置(Reference/參考)將會被修改而module.exports就不會得到其內(nèi)容。

另外,我們在許多項目看到下面的這行代碼

exports = module.exports = something

這行代碼作用就是確保exports在module.exports被我們復(fù)寫之后,仍可以指向相同的參考。

接著我們就可以透過module.exports來定義并導(dǎo)出一個function

/* function.js */ module.exports = function(){  return { name: 'andyyou' } }

使用的方式則是

var func = require('./function')

關(guān)于require一個很重要的行為就是它會緩存(Cache)module.exports的值,未來每一次require被調(diào)用時都會回傳相同的值。

它會根據(jù)匯入檔案的絕對路徑來緩存,所以當(dāng)我們想要模塊能夠回傳不同得值時,我們就需要導(dǎo)出function,如此一來每次執(zhí)行函式時就會回傳一個新值。

下面在Node REPL中簡易的示范

$ node > f1 = require('/Users/andyyou/Projects/export_this/function') [Function] > f2 = require('./function')//相同路徑 [Function] > f1 === f2 true > f1()=== f2() false

您可以觀察到require回傳了同樣的函式物件實例,但每一次調(diào)用函式回傳的物件是不同的。

更詳細的介紹可以參考官方文件,值得一讀。

現(xiàn)在,我們可以開始探討界面的設(shè)計模式(pattern)了。

導(dǎo)出命名空間

一個簡單且常用的設(shè)計模式就是導(dǎo)出一個包含數(shù)個屬性的物件,這些屬性具體的內(nèi)容主要是函式,但并不限于函式。

如此,我們就能夠透過匯入該模塊來取得這個命名空間下一系列相關(guān)的功能。

當(dāng)您匯入一個命名空間類型的模塊時,我們通常會將模塊指定到某一個變數(shù),然后透過它的成員(物件屬性)來存取使用這些功能。

甚至我們也可以將這些變數(shù)成員直接指定到區(qū)域變數(shù)。

var fs = require('fs') var readFile = fs.readFile var ReadStream = fs.ReadStream  readFile('./file.txt',function(err,data){  console.log('readFile contents: %s',data) })

這便是fs核心模塊的做法

var fs = exports

首先將隱式exports物件設(shè)定一個區(qū)域變數(shù)(即上面提過的exports)到fs,然后透過fs的屬性使用各個功能,例如:fs.Stats =  binding.Stats。

由于fs參考exports并且它是一個物件,所以當(dāng)我們require('fs')時,我們就能夠透過屬性使用那些功能。

fs.readFile = function(path,options,callback_){  //… }

其他東西也是一樣的作法,例如導(dǎo)出構(gòu)造函數(shù)

fs.ReadStream = ReadStream function ReadStream(path,options){  //… } ReadStream.prototype.open = function(){  //… }

當(dāng)導(dǎo)出命名空間時,您可以指定屬性到exports,就像fs的作法,又或者可以建立一個新的物件指派給module.exports

/* exports作法*/ exports.verstion = '1.0'  /*或者module.exports作法*/ module.exports = {  version: '1.0',  doYourTasks: function(){  //…  } }

一個常見的作法就是透過一個根模塊(root)來匯整并導(dǎo)出其他模塊,如此一來只需要一個require便可以使用所有的模塊。

原文作者在Good  Eggs工作時,會將數(shù)據(jù)模型(Model)拆分成個別的模塊,并使用導(dǎo)出構(gòu)造函數(shù)的方式導(dǎo)出(請參考下文介紹),然后透過一個index檔案來集合該目錄下所有的數(shù)據(jù)模型并一起導(dǎo)出,如此一來在models命名空間下的所有數(shù)據(jù)模型都可以使用

var models = require('./models') var User = models.User var Product = models.Product

在ES2015和CoffeeScript中我們甚至還可以使用解構(gòu)指派來匯入我們需要的功能

/* CoffeeScript */ {User,Product} = require './models'  /* ES2015 */ import {User,Product} from './models'

而剛剛提到的index.js大概就如下

exports.User = require('./User') exports.Person = require('./person')

實際上這樣分開的寫法還有更精簡的寫法,我們可以透過一個小小的函式庫來匯入在同一階層中所有檔案并搭配CamelCase的命名規(guī)則導(dǎo)出。

于是在我們的index.js中看起來就會如下

module.exports = require('../lib/require_siblings')(__filename)

導(dǎo)出函式

另外一個設(shè)計模式是導(dǎo)出函式當(dāng)作該模塊的界面。常見的作法是導(dǎo)出一個工廠函式(Factory function),然后呼叫并回傳一個物件。

在使用Express.js的時候便是這種作法

var express = require('express') var app = express()  app.get('/hello',function(req,res,next){  res.send('Hi there!We are using Express v' + express.version) })

Express導(dǎo)出該函式,讓我們可以用來建立一個新的express應(yīng)用程序。

在使用這種模式時,通常我們會使用factory function搭配參數(shù)讓我們可以設(shè)定并回傳初始化后的物件。

想要導(dǎo)出function,我們就一定要使用module.exports,Express便是這么做

exports = module.exports = createApplication  … function createApplication(){ … }

上面指派了createApplication函式到module.exports然后再指給exports確保參考一致。

同時Express也使用下面這種方式將導(dǎo)出函式當(dāng)作命名空間的作法使用。

exports.version = '3.1.1'

這邊要大略解釋一下由于Javascript原生并沒有支持命名空間的機制,于是大部分在JS中提到的namespace指的就是透過物件封裝的方式來達到namespace的效果,也就是***種設(shè)計模式。

注意!并沒有任何方式可以阻止我們將導(dǎo)出的函式作為命名空間物件使用,我們可以用其來引用其他的function,構(gòu)造函數(shù),物件。

Express 3.3.2 / 2013-07-03之后已經(jīng)將exports.version移除了

另外在導(dǎo)出函式的時候***為其命名,如此一來當(dāng)出錯的時候我們比較容易從錯誤堆疊信息中找到問題點。

下面是兩個簡單的例子:

/* bomb1.js */ module.exports = function(){  throw new Error('boom') } module.exports = function bomb(){  throw new Error('boom') } $ node > bomb = require('./bomb1'); [Function] > bomb() Error: boom  at module.exports(/Users/andyyou/Projects/export_this/bomb1.js:2:9)  at repl:1:2 … > bomb = require('./bomb2'); [Function: bomb] > bomb() Error: boom   at bomb(/Users/andyyou/Projects/export_this/bomb2.js:2:9)  at repl:1:2 …

導(dǎo)出函式還有些比較特別的案例,值得用另外的名稱以區(qū)分它們的不同。

導(dǎo)出高階函式

一個高階函式或functor基本上就是一個函式可以接受一個或多個函式為其輸入或輸出。而這邊我們要談?wù)摰暮笳?一個函式回傳函式

當(dāng)我們想要模塊能夠根據(jù)輸入控制回傳函式的行為時,導(dǎo)出一個高階函式就是一種非常實用的設(shè)計模式。

補充:functor & monad

舉例來說Connect就提供了許多可掛載的功能給網(wǎng)頁框架。

這里的middleware我們先理解成一個有三個參數(shù)(req,res,next)的function。

Express從v4.x版之后不再相依于connect

connect middleware慣例就是導(dǎo)出的function執(zhí)行后,要回傳一個middleware function。

在處理request的過程中這個回傳的middleware function就可以接手使用剛剛提到的三個參數(shù),用來在過程中做一些處理或設(shè)定。

同時因為閉包的特性這些設(shè)定在整個中間件的處理流程中都是有效的。

舉例來說,compression這個middleware就可以在處理responsive過程中協(xié)助壓縮

var connect = require('connect') var app = connect()  // gzip outgoing responses var compression = require('compression') app.use(compression())

而它的原始碼看起來就如下

module.exports = compression … function compression(options){ …  return function compression(req,res,next){ …  next()  } }

于是每一個request都會經(jīng)過compression middleware處理,而代入的options也因為閉包的關(guān)系會被保留下來

這是一種***彈性的模塊作法,也可能在您的開發(fā)項目上幫上許多忙。

middleware在這里您可以大略想成串連執(zhí)行一系列的function,自然其Function Signature要一致

導(dǎo)出構(gòu)造函數(shù)

在一般面向?qū)ο笳Z言中,constructor構(gòu)造函數(shù)指的是一小段代碼協(xié)助我們從類別Class建立一個物件。

// C# class Car {  // c#構(gòu)造函數(shù)  // constructor即class中用來初始化物件的method。  public Car(name){  name = name;  } } var car = new Car('BMW');

由于在ES2015之前Javascript并不支持類別,某種程度上在Javascript之中我們可以把任何一個function當(dāng)作類別,或者說一個function可以當(dāng)作function執(zhí)行或者搭配new關(guān)鍵字當(dāng)作constructor來使用。如果想知道更詳細的介紹可以閱讀MDN教學(xué)。

欲導(dǎo)出構(gòu)造函數(shù),我們需要透過構(gòu)造函式來定義類別,然后透過new來建立物件實例。

function Person(name){  this.name = name }  Person.prototype.greet = function(){  return 'Hi,I am ' + this.name }  var person = new Person('andyyou') console.log(person.greet())// Hi,I am andyyou

在這種設(shè)計模式底下,我們通常會將每個檔案設(shè)計成一個類別,然后導(dǎo)出構(gòu)造函數(shù)。這使得我們的項目構(gòu)架更加清楚。

var Person = require('./person') var person = new Person()

整個檔案看起來會如下

/* person.js */ function Person(name){  this.name = name }  Person.prototype.greet = function(){  return 'Hi,I am ' + this.name }  exports = module.exports = Person

導(dǎo)出單一物件實例Signleton

當(dāng)我們需要所有的模塊使用者共享物件的狀態(tài)與行為時,就需要導(dǎo)出單一物件實例。

Mongoose是一個ODM(Object-Document Mapper)函式庫,讓我們可以使用程序中的Model物件去操作MongoDB。

var mongoose = require('mongoose') mongoose.connect('mongodb://localhost/test')  var Cat = mongoose.model('Cat',{name: String})  var kitty = new Cat({name: 'Zildjian'}) kitty.save(function(err){  if(err)  throw Error('save failed')  console.log('meow') })

那我們require取得的mongoose物件是什么東西呢?事實上mongoose模塊的內(nèi)部是這么處理的

function Mongoose(){ … }  module.exports = exports = new Mongoose()

因為require的緩存了module.exports的值,于是所有reqire('mongoose')將會回傳相同的物件實例,之后在整個應(yīng)用程序之中使用的都會是同一個物件。

Mongoose使用面向?qū)ο蟮脑O(shè)計模式來封裝,解耦(分離功能之間的相依性),維護狀態(tài)使整體具備可讀性,同時透過導(dǎo)出一個Mongoose  Class的物件給使用者,讓我們可以簡單的存取使用。

如果我們有需要,它也可以建立其他的物件實例來作為命名空間使用。實際上Mongoose內(nèi)部提供了存取構(gòu)造函數(shù)的方法

Mongoose.prototype.Mongoose = Mongoose

因此我們可以這么做

var mongoose = require('mongoose')  var Mongoose = mongoose.Mongoose  var anotherMongoose = new Mongoose()  anotherMongoose.connect('mongodb://localhost/test')

擴展全局物件

一個被匯入的模塊不只限于單純?nèi)〉闷鋵?dǎo)出的數(shù)據(jù)。它也可以用來修改全局物件或回傳全局物件,自然也能定義新的全局物件。而在這邊的全局物件(Global  objects)或稱為標(biāo)準(zhǔn)內(nèi)置物件像是Object,F(xiàn)unction,Array指的是在全局能存取到的物件們,而不是當(dāng)Javascript開始執(zhí)行時所產(chǎn)生代表global  scope的global object。

當(dāng)我們需要擴增或修改全局物件預(yù)設(shè)行為時就需要使用這種設(shè)計模式。當(dāng)然這樣的方式是有爭議,您必須謹(jǐn)慎使用,特別是在開放原始碼的項目上。

例如:Should.js是一個常被用在單元測試中用來判斷分析值是否正確的函式庫。

require('should')  var user = {  name: 'andyyou' }  user.name.should.equal('andyyou')

這樣您是否比較清楚了,should.js增加了底層的Object的功能,加入了一個非列舉型的屬性  should,讓我們可以用簡潔的語法來撰寫單元測試。

而在內(nèi)部should.js做了這樣的事情

var should = function(obj){  return new Assertion(util.isWrapperType(obj)?obj.valueOf():obj) } … exports = module.exports = should  Object.defineProperty(Object.prototype,'should',{  set: function(){},  get: function(){  return should(this);  },  configurable: true });

就算看到這邊您肯定跟我一樣有滿滿的疑惑,全局物件擴展定義跟exprots有啥關(guān)聯(lián)呢?

事實上

/* whoami.js */ exports = module.exports = {  name: 'andyyou' }  Object.defineProperty(Object.prototype,'whoami',{  set: function(){},  get: function(){  return 'I am ' + this.name  } })  /* app.js */ var whoami = require('whoami') console.log(whoami)// { name: 'andyyou' }  var obj = { name: 'lena' } console.log(obj.whoami)// I am lena

現(xiàn)在我們明白了上面說的修改全局物件的意思了。should.js導(dǎo)出了一個should函式但是它主要的使用方式則是把should加到Object屬性上,透過物件本身來呼叫。

套用猴子補丁(Monkey Patch)

在這邊所謂的猴子補丁特別指的是在執(zhí)行時期動態(tài)修改一個類別或者模塊,通常會這么做是希望補強某的第三方套件的bug或功能。

假設(shè)某個模塊沒有提供您客制化功能的界面,而您又需要這個功能的時候,我們就會實作一個模塊來補強既有的模塊。

這個設(shè)計模式有點類似擴展全局物件,但并非修改全局物件,而是依靠Node模塊系統(tǒng)的緩存機制,當(dāng)其他代碼匯入該模塊時去補強該模塊的實例物件。

預(yù)設(shè)來說Mongoose會使用小寫以及復(fù)數(shù)的慣例替數(shù)據(jù)模型命名。例如一個數(shù)據(jù)模型叫做CreditCard最終我們會得到collection的名稱是creditcards。假如我們希望可以換成credit_cards并且其他地方也遵循一樣的用法。

下面是我們試著使用猴子補丁的方式來替既有的模塊增加功能

var pluralize = require('pluralize')//處理復(fù)數(shù)單字的函式庫 var mongoose = require('mongoose') var Mongoose = mongoose.Mongoose  mongoose.Promise = global.Promise // v4.1+ http://mongoosejs.com/docs/promises.html var model = Mongoose.prototype.model  //補丁 var fn = function(name,schema,collection,skipInit){  collection = collection || pluralize.plural(name.replace(/([a-z\d])([A-Z])/g,'$1_$2').toLowerCase())  return model.call(this,name,schema,collection,skipInit) } Mongoose.prototype.model = fn  /*實際測試*/ mongoose.connect('mongodb://localhost/test') var CreditCardSchema = new mongoose.Schema({number: String}) var CreditCardModel = mongoose.model('CreditCard',CreditCardSchema);  var card = new CreditCardModel({number: '5555444433332222'}); card.save(function(err){  if(err){  console.log(err)  }  console.log('success') })

您不該輕易使用上面這種方式補丁,這邊只是為了說明猴子補丁這種方式,mongoose已經(jīng)有提供官方的方式設(shè)定名稱

var schema = new Schema({..},{ collection: 'your_collection_name' })

當(dāng)這個模塊***次被匯入的時候便會讓mongoose重新定義Mongoose.prototype.model并將其設(shè)回原本的model的實作。

如此一來所有Mongoose的實例物件都具備新的行為了。注意到這邊并沒有修改exports所以當(dāng)我們require的時候得到的是預(yù)設(shè)的物件

另外如果您想使用上面這種補丁的方式時,記得閱讀原始碼并注意是否產(chǎn)生沖突。

請善用導(dǎo)出的功能

Node模塊系統(tǒng)提供了一個簡單的機制來封裝功能,使我們能夠建立了清楚的界面。希望掌握這七種設(shè)計模式提供不同的優(yōu)缺點能對您有所幫助。

在這邊作者并沒有徹底的調(diào)查所有的方式,一定有其他選項可供選擇,這邊只有描述幾個最常見且不錯的方法。

小結(jié)

  • namespace:導(dǎo)出一個物件包含需要的功能

root module的方式,使用一個根模塊導(dǎo)出其他模塊

  • function:直接將module.exports設(shè)為function

Function物件也可以拿來當(dāng)作命名空間使用

為其命名方便調(diào)試

exports = module.exports = something的作法是為了確保參考(Reference)一致

  • high-order function:可以透過代入?yún)?shù)控制并回傳function。

可協(xié)助實作middleware的設(shè)計模式

換句話說middleware即一系列相同signature的function串連。一個接一個執(zhí)行

  • constructor:導(dǎo)出類別(function),使用時再new,具備OOP的優(yōu)點

  • singleton:導(dǎo)出單一物件實例,重點在各個檔案可以共享物件狀態(tài)

  • global objects:在全局物件作的修改也會一起被導(dǎo)出

  • monkey patch:執(zhí)行時期,利用Node緩存機制在instance加上補丁

筆記

  • 一個javascript檔案可視為一個模塊

  • 解決特定問題或需求,功能完整由單一或多個模塊組合而成的整體稱為套件(package)

  • require匯入的模塊具有自己的scope

  • exports只是module.exports的參考,exports會記錄收集屬性如果module.exports沒有任何屬性就把其數(shù)據(jù)交給module.exports,但如果module.exports已經(jīng)具備屬性的話,那么exports的所有數(shù)據(jù)都會被忽略。

  • 就算exports置于后方仍會被忽略

  • Node初始化的順序

Native Module -> Module

StartNodeInstance()-> CreateEnvironment()-> LoadEnvironment()->  Cached

  • Native Module加載機制

檢查是否有緩存

->有;直接回傳this.exports

->沒有;new一個模塊物件

cache()

compile()->  NativeModule.wrap()將原始碼包進function字串->runInThisContext()建立函式

return NativeModule.exports

  • Node的require會cache,也就是說:如果希望模塊產(chǎn)生不同的instance時應(yīng)使用function

以上就是Node中exports有哪些設(shè)計模式,小編相信有部分知識點可能是我們?nèi)粘9ぷ鲿姷交蛴玫降摹OM隳芡ㄟ^這篇文章學(xué)到更多知識。更多詳情敬請關(guān)注億速云行業(yè)資訊頻道。

向AI問一下細節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI