乾貨!NPM私服 + 自定義NFS

路從今夜白丶發表於2019-03-04

Cnpm,官方解釋為Company npm。

由於團隊需求,現在需要搭建一個npm私服,用來更方便地管理團隊的元件庫,並且更快速更穩定地提供服務,我踏上了搭建npm私服的道路。

Clone cnpmjs.org專案程式碼

git clone https://github.com/cnpm/cnpmjs.org.git

下載完程式碼後,我們們先來大概瞄一眼專案目錄

+-- bin/                            ---一些命令指令碼
|   --- nodejsctl                        ---npm start啟動的指令碼
|   --- ...    
+-- common/                         ---公共目錄,存放日誌配置、郵件配置等
+-- config/                         
|   --- index.js                    ---主要配置檔案
+-- controllers/                                 
|   --- registry/                   ---7001埠的controller層
|   --- web/                        ---7002埠的controller層
|   --- sync_module_worker.js       ---sync的主程式檔案        
|   --- ...                         
+-- docs/                                 
|   --- db.sql                      ---資料庫建表sql
|   --- ...                         
+-- lib/           
+-- middleware/       
+-- models/                         ---資料庫操作目錄
+-- public/   
+-- routes/                                 
|   --- registry.js                 ---7001埠的路由檔案
|   --- web.js                      ---7002埠的路由檔案
|   --- ...                         
+-- servers/                                 
|   --- registry.js                 ---7001埠的伺服器入口檔案
|   --- web.js                      ---7002埠的伺服器入口檔案
|   --- ...                         
+-- services/
+-- sync/                                 
|   --- sync_all.js                 ---sync模式選擇all時執行的檔案
|   --- sync_exist.js               ---sync模式選擇exist時執行的檔案
|   --- ...    
+-- test/
+-- tools/
+-- view/
--- dispatch.js                     ---啟動npm服務的主要檔案,bin/nodejsctl中執行的就是這個檔案
--- package.json   

複製程式碼

我們可以發現,cnpm使用的是koa框架,結構是經典的route->controller->services->model

同步模組的具體流程是在controllers/sync_module_worker.js檔案中的

1. 根據設定的sync模式,從上游源中下載模組到一個臨時路徑/root/.cnpmjs.org/downloads/xxxxx.tgz
2. 呼叫nfs.upload方法將臨時路徑儲存的tgz上傳到指定儲存位置
3. 無論是否上傳成功,都刪除剛剛下載的臨時檔案
複製程式碼

看到這裡,喜歡思考的同學或許會說了,我到底應該怎麼搭建自己的npm私服?你說了半天,我還是啥都不知道,比如吧:

  1. sync模式怎麼選擇呢?
  2. 上游源是什麼呢?
  3. 臨時路徑為什麼是root/.cnpmjs.org/downloads?我能隨意修改嗎?
  4. nfs.upload是什麼呢,到底是將tgz上傳到哪裡呢?
  5. 需要資料庫嗎,資料庫配置又在哪裡呢????
  6. 7001埠是什麼,7002埠又是什麼??
  7. 搭建完成後,我應該怎麼使用我的私服?

配置config/index.js檔案~

var config = {
  version: version,
  dataDir: dataDir,

  /**
   * Cluster mode
   */
  enableCluster: true,
  numCPUs: os.cpus().length,

  /*
   * server configure
   */

  registryPort: 7001,
  webPort: 7002,
  bindingHost: `0.0.0.0`, // only binding on 127.0.0.1 for local access

  // debug mode
  // if in debug mode, some middleware like limit wont load
  // logger module will print to stdout
  debug: process.env.NODE_ENV === `development`,
  
  // page mode, enable on development env
  pagemock: process.env.NODE_ENV === `development`,
  
  // session secret
  sessionSecret: `cnpmjs.org test session secret`,
  
  // max request json body size
  jsonLimit: `10mb`,
  
  // log dir name
  logdir: path.join(dataDir, `logs`),
  
  // update file template dir
  uploadDir: path.join(dataDir, `downloads`),
  
  // web page viewCache
  viewCache: false,

  // config for koa-limit middleware
  // for limit download rates
  limit: {
    enable: false,
    token: `koa-limit:download`,
    limit: 1000,
    interval: 1000 * 60 * 60 * 24,
    whiteList: [],
    blackList: [],
    message: `request frequency limited, any question, please contact fengmk2@gmail.com`,
  },

  enableCompress: true, // enable gzip response or not

  // default system admins
  admins: {
    // name: email
    sunxiuguo: `sunxiuguo@my.com`,
  },

  // email notification for errors
  // check https://github.com/andris9/Nodemailer for more informations
  mail: {
    enable: false,
    appname: `cnpmjs.org`,
    from: `cnpmjs.org mail sender <adderss@gmail.com>`,
    service: `gmail`,
    auth: {
      user: `address@gmail.com`,
      pass: `your password`
    }
  },

  logoURL: `https://os.alipayobjects.com/rmsportal/oygxuIUkkrRccUz.jpg`, // cnpm logo image url
  adBanner: ``,
  customReadmeFile: ``, // you can use your custom readme file instead the cnpm one
  customFooter: ``, // you can add copyright and site total script html here
  npmClientName: `cnpm`, // use `${name} install package`
  packagePageContributorSearch: true, // package page contributor link to search, default is true

  // max handle number of package.json `dependencies` property
  maxDependencies: 200,
  
  // backup filepath prefix
  backupFilePrefix: `/cnpm/backup/`,

  /**
   * database config
   */

  database: {
    db: `******`,  // 庫名
    username: `*********`, // 資料庫使用者名稱
    password: `************`, // 資料庫密碼

    // the sql dialect of the database
    // - currently supported: `mysql`, `sqlite`, `postgres`, `mariadb`
    dialect: `mysql`,

    // the Docker container network hostname defined at docker-compose.yml
    host: `**************`,  // 資料庫域名

    // custom port; default: 3306
    port: 3318,  // 資料庫埠號

    // use pooling in order to reduce db connection overload and to increase speed
    // currently only for mysql and postgresql (since v1.5.0)
    pool: {
      maxConnections: 10,
      minConnections: 0,
      maxIdleTime: 30000
    },

    dialectOptions: {
      // if your server run on full cpu load, please set trace to false
      trace: true,
    },

    // the storage engine for `sqlite`
    // default store into ~/.cnpmjs.org/data.sqlite
    storage: path.join(dataDir, `data.sqlite`),

    logging: !!process.env.SQL_DEBUG,
  },

  // package tarball store in local filesystem by default
  nfs: aws.create({
    accessKeyId: `*************`,  // s3 accessKeyId
    secretAccessKey: `****************`, // s3 secretAccessKey
    // change to your endpoint
    endpoint: `*****************`, // https://docs.aws.amazon.com/zh_cn/general/latest/gr/rande.html
    bucket: `npm-online`, // s3 bucket名稱
    signatureVersion: `v4`, // s3 api版本
    mode: `private`, // public: 通過url下載tar包; private: 通過key下載tar包
  }),
  
  // if set true, will 302 redirect to `nfs.url(dist.key)`
  downloadRedirectToNFS: false,

  // registry url name
  registryHost: `registry.npm.my.com`,

  /**
   * registry mode config
   */

  // enable private mode or not
  // private mode: only admins can publish, other users just can sync package from source npm
  // public mode: all users can publish
  enablePrivate: false,

  // registry scopes, if dont set, means do not support scopes
  scopes: [ `@cnpm`, `@sunxiuguo`, `@companyName` ],

  // some registry already have some private packages in global scope
  // but we want to treat them as scoped private packages,
  // so you can use this white list.
  privatePackages: [],

  /**
   * sync configs
   */

  // the official npm registry
  // cnpm wont directly sync from this one
  // but sometimes will request it for some package infomations
  // please dont change it if not necessary
  officialNpmRegistry: `https://registry.npmjs.com`,
  officialNpmReplicate: `https://replicate.npmjs.com`,

  // sync source, upstream registry
  // If you want to directly sync from official npm registry
  // please drop them an email first
  sourceNpmRegistry: `https://registry.npm.taobao.org`,
  sourceNpmWeb: `https://npm.taobao.org`,

  // upstream registry is base on cnpm/cnpmjs.org or not
  // if your upstream is official npm registry, please turn it off
  sourceNpmRegistryIsCNpm: true,

  // if install return 404, try to sync from source registry
  syncByInstall: true,

  // sync mode select
  // none: do not sync any module, proxy all public modules from sourceNpmRegistry
  // exist: only sync exist modules
  // all: sync all modules
  syncModel: `exist`, // `none`, `all`, `exist`

  syncConcurrency: 1,
  // sync interval, default is 10 minutes
  syncInterval: `10m`,

  // sync polular modules, default to false
  // because cnpm can not auto sync tag change for now
  // so we want to sync popular modules to ensure their tags
  syncPopular: false,
  syncPopularInterval: `1h`,
  // top 100
  topPopular: 100,

  // sync devDependencies or not, default is false
  syncDevDependencies: false,
  // try to remove all deleted versions from original registry
  syncDeletedVersions: true,

  // changes streaming sync
  syncChangesStream: false,
  handleSyncRegistry: `http://127.0.0.1:7001`,

  // default badge subject
  badgeSubject: `cnpm`,
  // defautl use https://badgen.net/
  badgeService: {
    url: function(subject, status, options) {
      options = options || {};
      let url = `https://badgen.net/badge/${utility.encodeURIComponent(subject)}/${utility.encodeURIComponent(status)}`;
      if (options.color) {
        url += `/${utility.encodeURIComponent(options.color)}`;
      }
      if (options.icon) {
        url += `?icon=${utility.encodeURIComponent(options.icon)}`;
      }
      return url;
    },
  },

  packagephobiaURL: `https://packagephobia.now.sh`,
  packagephobiaSupportPrivatePackage: false,

  // custom user service, @see https://github.com/cnpm/cnpmjs.org/wiki/Use-Your-Own-User-Authorization
  // when you not intend to ingegrate with your company  user system, then use null, it would
  // use the default cnpm user system
  userService: null,

  // always-auth https://docs.npmjs.com/misc/config#always-auth
  // Force npm to always require authentication when accessing the registry, even for GET requests.
  alwaysAuth: false,

  // if you are behind firewall, need to request through http proxy, please set this
  // e.g.: `httpProxy: `http://proxy.mycompany.com:8080``
  // httpProxy: `http://gfw.guazi-corp.com`,
  httpProxy: null,


  // snyk.io root url
  snykUrl: `https://snyk.io`,

  // https://github.com/cnpm/cnpmjs.org/issues/1149
  // if enable this option, must create module_abbreviated and package_readme table in database
  enableAbbreviatedMetadata: true,

  // global hook function: function* (envelope) {}
  // envelope format please see https://github.com/npm/registry/blob/master/docs/hooks/hooks-payload.md#payload
  globalHook: null,

  opensearch: {
    host: ``,
  },
};


複製程式碼

是不是成功開啟檔案了~恭喜你!你成功的邁出了第二步!
什麼?你問第一步是什麼?第一步是clone程式碼啊

配置檔案解惑

  • sync模式是什麼?怎麼選擇?

    syncModel屬性控制sync模式,分為none,exist,all三種情況。

    • none:永不同步,只管理私有使用者上傳的包,其它源包會直接從源站獲取;
    • exist:定時同步已經存在於資料庫的包;
    • all:定時同步所有源站的包;
  • 上游源是什麼?怎麼設定?

    上游源就是你同步包的地址,比如你的上游源是淘寶源,那麼你的npm私服就會從淘寶源進行包的同步。

    • sourceNpmRegistry屬性控制上游源地址的設定,預設為registry.npm.taobao.org
    • sourceNpmRegistryIsCNpm屬性表示上游源是否是cnpm,如果你的上游源是淘寶,此屬性設定為true;如果你的上游源為官方Npm源,那麼此屬性設定為false
    • syncByInstall屬性為true時,表示如果從你的私服源install時找不到包,那麼程式會自動從上游源進行同步。
  • 臨時路徑在哪?我能隨意修改嗎?

    uploadDir屬性設定同步的模組存放的臨時路徑,預設為path.join(dataDir, `downloads`),即root/.cnpmjs.org/downloads

  • nfs.upload是什麼?要將tgz上傳到哪裡?

    nfs屬性控制包儲存,包括上傳,下載等等。nfs的意思是network file system

    • nfs預設使用的是fs-cnpm這個外掛,可以看到裡面定義了好多方法,比如upload,download,remove等等;
    • 可以看到配置檔案中nfs傳入了一個dir屬性,預設為path.join(dataDir, `nfs`),也就是root/.cnpmjs.org/nfs,即同步的包檔案預設存放在這個目錄下;
    • 當然這只是一種檔案儲存方案,我們現在是接入的amazon s3的物件儲存系統來儲存包,這個後面會詳細說一下。
  • 需要資料庫嗎?資料庫配置在哪裡?
    • npm是需要資料庫的,docs/db.sql就是建表sql,資料庫儲存的資訊主要是包資訊,使用者資訊,包和使用者的關聯資訊,也會儲存npm伺服器各種包的下載資訊等。
    • 當從上游成功同步了一個包到npm伺服器時,資料庫中就會記錄下這個包的相關資訊,包檔案則會儲存在nfs中。
    • database屬性就是設定資料庫資訊的,包括庫名,使用者名稱,密碼,埠號,資料庫地址等等。
    • 只要把建表sql匯入資料庫中,建立好所有的表即可
  • 7001和7002埠分別是什麼服務?

    registryPort屬性預設為7001,webPort屬性預設為7002.
    registry服務主要是用來提供給使用者源相關操作,比如設定npm源
    web服務主要是提供給使用者的一個圖形化管理介面,比如在介面上查詢某個模組

  • 除了上面這些,還需要什麼配置?
    • bindingHost: 設定為0.0.0.0,開放給外部使用
    • admins: 可以新增幾個管理員使用者 name: email的格式
    • registryHost:設定為npm伺服器7001埠的域名,比如我搭建npm的伺服器7001埠的域名為 registry.npm.my.com,就設定為這個域名
    • scopes:可以新增幾個字首,以後釋出包的時候帶有這些字首的,就代表是私有包

NFS個性化配置

因為我們的npm私服是放在docker裡,包檔案不可能使用fs-cnpm儲存在docker裡,所以我們接入了amazon s3的物件儲存服務。
官方提供了接入npm的協議NFS-Guide

Can download the uploaded file through http request. like qn-cnpm.
uploadBuffer: use options.key to customize the filename, then callback {url: `http://test.com/xxx.tgz`}.
url: accept a key and respond download url.
remove: remove file by key

Can not download by http request. like sfs-client or oss-cnpm.
uploadBuffer: upload the file, and must callback {key: `xxx`}, so cnpmjs.org can record the key, and use this key to download or remove.
download: need provide download api to download the file by key.
createDownloadStream: streaming download file by key
remove: remove file by key
複製程式碼

如果儲存系統支援通過http請求下載包檔案,就提供uploadBuffer,url,remove方法
如果儲存系統不支援通過http請求下載包檔案,就需要提供uploadBuffer,download,createDownloadStream,remove方法。
並且所有方法都需要是async的,或者是generatord的。

NFS接入S3物件儲存

因為我們使用的bucket,首先要提供一個create的方法來例項化一個s3物件。

  • create
exports.create = function (options) {
    return new AwsWrapper(options);
};

function AwsWrapper(options) {
    this.client = new S3(options);
    this.mode = options.mode;
    this.bucket = options.bucket;
    var params = {
        Bucket: options.bucket,
        CreateBucketConfiguration: {
            LocationConstraint: ":npm"//桶所在服務區
        }
    };
    this.client.createBucket(params, function (err, data) {
        if (err) {
            // an error occurred
            logger.syncInfo(err);
        } else {
            // successful response
            console.log(data.Location);
        }
    });
}
複製程式碼

然後按照協議提供對應的方法

  • uploadBuffer

    呼叫路徑在controllers/registry/package/save.js,當publish包時會進入這個方法,入參為fileBuffer和options;
    這個方法很簡單,只需呼叫對應儲存系統提供的api,把buffer上傳即可。

    const key = trimKey(options.key);
      logger.syncInfo(`enter aws->uploadBuffer key=${key}`);
      let result = {
          key,
      };
    
      let uploadParams = {
          Bucket: this.bucket,
          Key: key,
          Body: fileBuffer
      };
    
      this.client.upload (uploadParams, function (err, data) {
          if (err) {
              logger.syncInfo(err);
          }
      });
    複製程式碼
  • upload

    呼叫路徑在controllers/sync_module_worker.js,當從上游同步包的時候會進入這個方法,入參為filePath和options
    upload和uploadBuffer不同的是,upload是讀取傳入的filePath的檔案作為body上傳,uploadBuffer是直接把傳入的buffer物件作為body上傳。

const key = trimKey(options.key);
    logger.syncInfo(`進入aws->upload key=${key} filePath=${filePath}`);
    let result = {
        key,
    };
    let fileStream = fs.createReadStream(filePath);
    fileStream.on(`error`, function(err) {
        logger.syncInfo(err);
    });

    let uploadParams = {
        Bucket: this.bucket,
        Key: key,
        Body: fileStream
    };

    await this.client.upload (uploadParams, function (err, data) {
        if (err) {
            logger.syncInfo(err);
        }
    });
    return result;
複製程式碼
  • url

    呼叫路徑在controllers/registry/package/download.js,當下載包的時候會進入這個方法,入參為key和options,用於獲取包的存放的url地址

const params = { Bucket: this.bucket, Key: trimKey(key) };
    logger.syncInfo(`進入aws->url key=${key} trimKey=${trimKey(key)}`);
    return this.client.getSignedUrl(`getObject`, params);
複製程式碼
  • remove

    呼叫路徑在controllers/registry/package/remove.js controllers/registry/package/remove_version.js 和 controllers/sync_module_worker.js,當刪除包或者刪除版本的時候會進入這個方法,入參為key和options

const params = { Bucket: this.bucket, Key: trimKey(key) };
    logger.syncInfo(`進入aws->remove key=${key} trimKey=${trimKey(key)}`);
    await this.client.deleteObject(params);
複製程式碼
  • createDownloadStream

    呼叫路徑在controllers/utils.js,當下載包的時候會進入這個方法,入參為key和options,把可讀流作為使用者下載請求的response的body
    utils.js中是唯一呼叫download和createDownloadStream的地方,然而我們仔細看原始碼,可以發現如果定義了createDownloadStream方法,就會直接返回createDownloadStream的結果,而不會繼續進行下面的download操作。
    也就是說,我們只需要定義createDownloadStream方法即可

const params = { Bucket: this.bucket, Key: trimKey(key) };
    logger.syncInfo(`進入aws->createDownloadStream key=${key} trimKey=${trimKey(key)}`);
    return this.client.getObject(params).createReadStream();
複製程式碼

編寫NPM測試指令碼

設定npm源為剛搭建的私有源
npm config set registry http://registry.npm.my.com 

檢視當前的registry地址
npm get registry

清理npm快取
npm cache clean --force 

隨便選一個專案 刪除node_modules包
rm -rf node_modules

安裝
npm install

手動同步一個包,比如react(可以在web介面上的/sync/路徑下輸入包名進行同步)
npm sync react
複製程式碼

只是手動安裝一個專案的依賴包可能無法說明什麼,我們來寫一個簡單的自動測試指令碼

require(`shelljs/global`)

const logger = require(`./log`).logger;
const fs = require(`fs`);
const MODULE_DIR = `/node_modules`;
const PARENT_PATH = `/Users/sunxiuguo/project/`;
const projectName = [
    `test1`,
    `test2`,
    `test3`,
    `test4`,
]
const absolutePath = projectName.map(item => {
    return {
        modulesPath: PARENT_PATH + item + MODULE_DIR,
        parentPath: PARENT_PATH + item,
    }
});

const startTime = new Date(`2018/11/06 21:00:000`).getTime();
const endTime = new Date(`2018/11/08 10:00:000`).getTime();

/**
 * 讀取路徑
 * @param path
 */
function getStat(path){
    if (exec(`cd ${path}`).code == 0) {
        return true;
    }
    return false;
}

async function npmCachecleanAndInstall(projectPath) {
    cd(projectPath);
    logger.info(`cd ${projectPath}`);
    exec(`pwd`);
    await execAndLogAsync(`npm cache clean --force`);
    await execAndLogAsync(`npm install --registry=http://registry.npm.my.com`)
}

async function execAndLogAsync(command) {
    logger.info(command);
    let result = await exec(command);
    if (result.stderr) {
        logger.error(result.stderr);
    }
}

async function install(path) {
    logger.info(`install: path = ${JSON.stringify(path)}`);
    let isExists = getStat(path.modulesPath);
    if (!isExists) {
        // 如果不存在 npm install
        logger.info(`install: 不存在${path.modulesPath}目錄,開始npm install`)
        await npmCachecleanAndInstall(path.parentPath);
    } else {
        // 如果存在,刪除 && npm install
        logger.info(`install: 存在${path.modulesPath}目錄,開始刪除`)
        await execAndLogAsync(`rm -rf ${path.modulesPath}`);
        logger.info(`install: 刪除${path.modulesPath}成功,開始npm install`)
        await npmCachecleanAndInstall(path.parentPath);
    }
}

logger.info(`beginning!`)

if (new Date().getTime() < startTime) {
    logger.info(`未到開始時間, 開始時間為2018/11/06 22:00:000`)
    exit(1);
}

for(let path of absolutePath) {
    (async function(){
        while (new Date().getTime() < endTime) {
            await install(path);
        }
    })()
}
複製程式碼

來釋出一個包吧!

首先新增一個使用者,新增後會預設以這個使用者登入
npm adduser
username:sunxiuguo
password:sunxiuguo
email:sunxiuguo1@qq.com

進入要釋出的目錄
npm publish

檢視剛才釋出的包資訊(也可以在web介面上查詢)
npm view moduleName

這時如果其他小夥伴也要釋出這個包,就會報錯了,因為其他小夥伴不是這個包的maintainer
我們們來檢視一下這個包的owner都有誰
npm owner ls moduleName

然後新增wangwang為這個包的owner
npm owner add wangwang moduleName

什麼?!!又報錯了?!
不要慌,那是因為根本沒有wangwang這個使用者,需要執行npm adduser新增一下
npm adduser
username:wangwang
password:1231131313
email:wangwang@guazi.com

再次新增owner
npm owner add wangwang moduleName

成功了!從此wangwang也可以釋出這個包了

以後如果想登入,直接Login即可
npm login
username:wangwang
password:1231131313
email:wangwang@guazi.com
複製程式碼

如果想撤銷釋出一個包怎麼辦?

強調一下,撤銷釋出包是很危險的一件事情,如果有其他同學用了你的包,然後你心血澎湃地把這個包撤銷了??其他同學肯定一臉問號
npm unpublish moduleName
複製程式碼
  1. 根據規範,只有在發包的24小時內才允許撤銷釋出的包( unpublish is only allowed with versions published in the last 24 hours)
  2. 即使你撤銷了釋出的包,發包的時候也不能再和被撤銷的包的名稱和版本重複了(即不能名稱相同,版本相同,因為這兩者構成的唯一標識已經被“佔用”了)
如果你不再維護你釋出的moduleA了,可以使用下面這個命令
這個命令並不會撤銷已釋出的包,只是會在其他人用的你的包時收到警告
npm deprecate moduleA
複製程式碼

NPM包的版本應該怎麼維護?

版本格式:主版號.次版號.修訂號,版號遞增規則如下:
主版號:當你做了不相容的API 修改,
次版號:當你做了向下相容的功能性新增,
修訂號:當你做了向下相容的問題修正,比如修復了一個bug。

改變當前package的版本號,update_type為patch, minor, or major其中之一,分別表示修訂號,次版號,主版號
npm version <update_type>

比如當前版本號為0.1.0
npm version patch
0.1.1
npm version minor
0.2.0
npm version major
1.0.0
複製程式碼

我都踩過哪些坑?

  • docker中npm install -g報錯
Error: could not get uid/gid
[ `nobody`, 0 ]
    at /usr/lib/node_modules/npm/node_modules/uid-number/uid-number.js:37:16
    at ChildProcess.exithandler (child_process.js:205:5)
    at emitTwo (events.js:106:13)
    at ChildProcess.emit (events.js:191:7)
    at maybeClose (internal/child_process.js:891:16)
    at Socket.<anonymous> (internal/child_process.js:342:11)
    at emitOne (events.js:96:13)
    at Socket.emit (events.js:188:7)
    at Pipe._handle.close [as _onclose] (net.js:497:12)
    
    在全域性安裝前執行下面這條命令即可
    npm config set unsafe-perm true

複製程式碼
  • nodejs.ErrorException: Error: stream.push() after EOF
清一下快取
npm cache clean --force
複製程式碼
  • publish成功了,但是install失敗,報錯shanumNotMatch,unexpected end of file
這個問題我自己的情況是,在controllers/utils.js裡,呼叫nfs.download方法,writeStream還沒有寫完,就開始了readStream並且清理了臨時路徑,導致檔案被截斷了,所以一定要注意非同步的問題,並且除錯的時候儘量寫好try catch和日誌,方便以後定位問題。  
當然也可以直接定義一個createDownloadStream方法,直接返回可讀流給body。
複製程式碼

期間還踩過好多好多坑,遺憾的是忘記記錄下來了….

以上是在下關於npm私服搭建的一點拙見,如有不足,望諸位客官多多指正。

相關文章