项目学习记录 (三)

项目简介

在线音乐播放器,具有音乐搜索、播放、歌词显示、播放历史、查看歌曲评论、网易云用户歌单播放同步等功能,模仿 QQ 音乐网页版界面,采用 flexboxposition 布局;

本播放器由 茂茂 开发,项目地址: https://github.com/maomao1996/Vue-mmPlayer

项目使用了网易云音乐的第三方 API 服务

创建项目

  • 创建远程仓库 music-web,下载到本地

git clone https://codeup.aliyun.com/****/web/music-web.git
Cloning into 'music-web'...
remote: Enumerating objects: 3, done.
remote: Counting objects: 100% (3/3), done.
remote: Total 3 (delta 0), reused 0 (delta 0), pack-reused 0
Receiving objects: 100% (3/3), done.
  • 在 music-web 创建 Vue 项目

music-web % vue create music


Vue CLI v5.0.8
? Please pick a preset: Manually select features
? Check the features needed for your project: Babel, Router, Vuex, CSS 
Pre-processors
? Choose a version of Vue.js that you want to start the project with 3.x
? Use history mode for router? (Requires proper server setup for index fallback 
in production) No
? Pick a CSS pre-processor (PostCSS, Autoprefixer and CSS Modules are supported 
by default): Less
? Where do you prefer placing config for Babel, ESLint, etc.? In dedicated 
config files
? Save this as a preset for future projects? No


Vue CLI v5.0.8
✨  Creating project in /music-web/music.
⚙️  Installing CLI plugins. This might take a while...


added 843 packages, and audited 844 packages in 3m

93 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities
🚀  Invoking generators...
📦  Installing additional dependencies...


added 32 packages, and audited 876 packages in 13s

96 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities
⚓  Running completion hooks...

📄  Generating README.md...

🎉  Successfully created project music.
👉  Get started with the following commands:

 $ cd music
 $ npm run serve

  • 在 music-web 创建 Express 项目

music-web % express --view=pug server

   create : server/
   create : server/public/
   create : server/public/javascripts/
   create : server/public/images/
   create : server/public/stylesheets/
   create : server/public/stylesheets/style.css
   create : server/routes/
   create : server/routes/index.js
   create : server/routes/users.js
   create : server/views/
   create : server/views/error.pug
   create : server/views/index.pug
   create : server/views/layout.pug
   create : server/app.js
   create : server/package.json
   create : server/bin/
   create : server/bin/www

   change directory:
     $ cd server

   install dependencies:
     $ npm install

   run the app:
     $ DEBUG=server:* npm start

  • server 添加依赖

server % npm install
npm WARN deprecated core-js@2.6.12: core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js.

added 125 packages, and audited 126 packages in 52s

9 packages are looking for funding
  run `npm fund` for details

7 vulnerabilities (2 low, 5 high)

To address issues that do not require attention, run:
  npm audit fix

To address all issues, run:
  npm audit fix --force

Run `npm audit` for details.

server % npm i axios express-fileupload md5 music-metadata pac-proxy-agent qrcode safe-decode-uri-component tunnel yargs

added 103 packages, changed 3 packages, and audited 229 packages in 44s

21 packages are looking for funding
  run `npm fund` for details

7 vulnerabilities (2 low, 5 high)

To address issues that do not require attention, run:
  npm audit fix

To address all issues, run:
  npm audit fix --force

Run `npm audit` for details.

项目开发

1. 修改 server/package.json - scripts

    "serve": "nodemon ./bin/www"

2. 在 server 新建文件夹 server/module,server/plugins,server/utils

3. 在 server/module 新建模型文件 server/module/activate_init_profile.js 等

activate_init_profile.js

// 初始化名字
module.exports = (query, request) => {
  const data = {
    nickname: query.nickname,
  }
  return request(
    'POST',
    `https://music.163.com/eapi/activate/initProfile`,
    data,
    {
      crypto: 'eapi',
      cookie: query.cookie,
      proxy: query.proxy,
      realIP: query.realIP,
      url: '/api/activate/initProfile',
    },
  )
}

4. 在 server/plugins 新建文件

server/plugins/upload.js

const { default: axios } = require('axios')
module.exports = async (query, request) => {
  const data = {
    bucket: 'yyimgs',
    ext: 'jpg',
    filename: query.imgFile.name,
    local: false,
    nos_product: 0,
    return_body: `{"code":200,"size":"$(ObjectSize)"}`,
    type: 'other',
  }
  //   获取key和token
  const res = await request(
    'POST',
    `https://music.163.com/weapi/nos/token/alloc`,
    data,
    { crypto: 'weapi', cookie: query.cookie, proxy: query.proxy },
  )
  //   上传图片
  const res2 = await axios({
    method: 'post',
    url: `https://nosup-hz1.127.net/yyimgs/${res.body.result.objectKey}?offset=0&complete=true&version=1.0`,
    headers: {
      'x-nos-token': res.body.result.token,
      'Content-Type': 'image/jpeg',
    },
    data: query.imgFile.data,
  })
  //   获取裁剪后图片的id
  const imgSize = query.imgSize || 300
  const imgX = query.imgX || 0
  const imgY = query.imgY || 0
  const res3 = await request(
    'POST',
    `https://music.163.com/upload/img/op?id=${res.body.result.docId}&op=${imgX}y${imgY}y${imgSize}y${imgSize}`,
    {},
    { crypto: 'weapi', cookie: query.cookie, proxy: query.proxy },
  )

  return {
    // ...res.body.result,
    // ...res2.data,
    // ...res3.body,
    url_pre: 'https://p1.music.126.net/' + res.body.result.objectKey,
    url: res3.body.url,
    imgId: res3.body.id,
  }
}

server/plugins/songUpload.js

const { default: axios } = require('axios')
module.exports = async (query, request) => {
  let ext = 'mp3'
  if (query.songFile.name.indexOf('flac') > -1) {
    ext = 'flac'
  }
  const filename = query.songFile.name
    .replace('.' + ext, '')
    .replace(/\s/g, '')
    .replace(/\./g, '_')
  //   获取key和token
  const tokenRes = await request(
    'POST',
    `https://music.163.com/weapi/nos/token/alloc`,
    {
      bucket: 'jd-musicrep-privatecloud-audio-public',
      ext: ext,
      filename: filename,
      local: false,
      nos_product: 3,
      type: 'audio',
      md5: query.songFile.md5,
    },
    { crypto: 'weapi', cookie: query.cookie, proxy: query.proxy },
  )

  // 上传
  const objectKey = tokenRes.body.result.objectKey.replace('/', '%2F')
  try {
    await axios({
      method: 'post',
      url: `http://45.127.129.8/jd-musicrep-privatecloud-audio-public/${objectKey}?offset=0&complete=true&version=1.0`,
      headers: {
        'x-nos-token': tokenRes.body.result.token,
        'Content-MD5': query.songFile.md5,
        'Content-Type': 'audio/mpeg',
        'Content-Length': String(query.songFile.size),
      },
      data: query.songFile.data,
      maxContentLength: Infinity,
      maxBodyLength: Infinity,
    })
  } catch (error) {
    console.log('error', error.response)
    throw error.response
  }
  return {
    ...tokenRes,
  }
}

5. 在 server/utils 新建文件

server/utils/config.json

{
    "anonymous_token": "bf8bfeabb1aa84f9c8c3906c04a04fb864322804c83f5d607e91a04eae463c9436bd1a17ec353cf780b396507a3f7464e8a60f4bbc019437993166e004087dd32d1490298caf655c2353e58daa0bc13cc7d5c198250968580b12c1b8817e3f5c807e650dd04abd3fb8130b7ae43fcc5b",
    "resourceTypeMap": {
      "0": "R_SO_4_",
      "1": "R_MV_5_",
      "2": "A_PL_0_",
      "3": "R_AL_3_",
      "4": "A_DJ_1_",
      "5": "R_VI_62_",
      "6": "A_EV_2_",
      "7": "A_DR_14_"
    }
}

server/utils/apicache.js

var url = require('url')
var MemoryCache = require('./memory-cache')

var t = {
    ms: 1,
    second: 1000,
    minute: 60000,
    hour: 3600000,
    day: 3600000 * 24,
    week: 3600000 * 24 * 7,
    month: 3600000 * 24 * 30,
  }
  
  var instances = []
  
  var matches = function (a) {
    return function (b) {
      return a === b
    }
  }
  
  var doesntMatch = function (a) {
    return function (b) {
      return !matches(a)(b)
    }
  }
  
  var logDuration = function (d, prefix) {
    var str = d > 1000 ? (d / 1000).toFixed(2) + 'sec' : d + 'ms'
    return '\x1b[33m- ' + (prefix ? prefix + ' ' : '') + str + '\x1b[0m'
  }
  
  function getSafeHeaders(res) {
    return res.getHeaders ? res.getHeaders() : res._headers
  }
  
  function ApiCache() {
    var memCache = new MemoryCache()
  
    var globalOptions = {
      debug: false,
      defaultDuration: 3600000,
      enabled: true,
      appendKey: [],
      jsonp: false,
      redisClient: false,
      headerBlacklist: [],
      statusCodes: {
        include: [],
        exclude: [],
      },
      events: {
        expire: undefined,
      },
      headers: {
        // 'cache-control':  'no-cache' // example of header overwrite
      },
      trackPerformance: false,
    }
  
    var middlewareOptions = []
    var instance = this
    var index = null
    var timers = {}
    var performanceArray = [] // for tracking cache hit rate
  
    instances.push(this)
    this.id = instances.length
  
    function debug(a, b, c, d) {
      var arr = ['\x1b[36m[apicache]\x1b[0m', a, b, c, d].filter(function (arg) {
        return arg !== undefined
      })
      var debugEnv =
        process.env.DEBUG &&
        process.env.DEBUG.split(',').indexOf('apicache') !== -1
  
      return (globalOptions.debug || debugEnv) && console.log.apply(null, arr)
    }
  
    function shouldCacheResponse(request, response, toggle) {
      var opt = globalOptions
      var codes = opt.statusCodes
  
      if (!response) return false
  
      if (toggle && !toggle(request, response)) {
        return false
      }
  
      if (
        codes.exclude &&
        codes.exclude.length &&
        codes.exclude.indexOf(response.statusCode) !== -1
      )
        return false
      if (
        codes.include &&
        codes.include.length &&
        codes.include.indexOf(response.statusCode) === -1
      )
        return false
  
      return true
    }
  
    function addIndexEntries(key, req) {
      var groupName = req.apicacheGroup
  
      if (groupName) {
        debug('group detected "' + groupName + '"')
        var group = (index.groups[groupName] = index.groups[groupName] || [])
        group.unshift(key)
      }
  
      index.all.unshift(key)
    }
  
    function filterBlacklistedHeaders(headers) {
      return Object.keys(headers)
        .filter(function (key) {
          return globalOptions.headerBlacklist.indexOf(key) === -1
        })
        .reduce(function (acc, header) {
          acc[header] = headers[header]
          return acc
        }, {})
    }
  
    function createCacheObject(status, headers, data, encoding) {
      return {
        status: status,
        headers: filterBlacklistedHeaders(headers),
        data: data,
        encoding: encoding,
        timestamp: new Date().getTime() / 1000, // seconds since epoch.  This is used to properly decrement max-age headers in cached responses.
      }
    }
  
    function cacheResponse(key, value, duration) {
      var redis = globalOptions.redisClient
      var expireCallback = globalOptions.events.expire
  
      if (redis && redis.connected) {
        try {
          redis.hset(key, 'response', JSON.stringify(value))
          redis.hset(key, 'duration', duration)
          redis.expire(key, duration / 1000, expireCallback || function () {})
        } catch (err) {
          debug('[apicache] error in redis.hset()')
        }
      } else {
        memCache.add(key, value, duration, expireCallback)
      }
  
      // add automatic cache clearing from duration, includes max limit on setTimeout
      timers[key] = setTimeout(function () {
        instance.clear(key, true)
      }, Math.min(duration, 2147483647))
    }
  
    function accumulateContent(res, content) {
      if (content) {
        if (typeof content == 'string') {
          res._apicache.content = (res._apicache.content || '') + content
        } else if (Buffer.isBuffer(content)) {
          var oldContent = res._apicache.content
  
          if (typeof oldContent === 'string') {
            oldContent = !Buffer.from
              ? new Buffer(oldContent)
              : Buffer.from(oldContent)
          }
  
          if (!oldContent) {
            oldContent = !Buffer.alloc ? new Buffer(0) : Buffer.alloc(0)
          }
  
          res._apicache.content = Buffer.concat(
            [oldContent, content],
            oldContent.length + content.length,
          )
        } else {
          res._apicache.content = content
        }
      }
    }
  
    function makeResponseCacheable(
      req,
      res,
      next,
      key,
      duration,
      strDuration,
      toggle,
    ) {
      // monkeypatch res.end to create cache object
      res._apicache = {
        write: res.write,
        writeHead: res.writeHead,
        end: res.end,
        cacheable: true,
        content: undefined,
      }
  
      // append header overwrites if applicable
      Object.keys(globalOptions.headers).forEach(function (name) {
        res.setHeader(name, globalOptions.headers[name])
      })
  
      res.writeHead = function () {
        // add cache control headers
        if (!globalOptions.headers['cache-control']) {
          if (shouldCacheResponse(req, res, toggle)) {
            res.setHeader(
              'cache-control',
              'max-age=' + (duration / 1000).toFixed(0),
            )
          } else {
            res.setHeader('cache-control', 'no-cache, no-store, must-revalidate')
          }
        }
  
        res._apicache.headers = Object.assign({}, getSafeHeaders(res))
        return res._apicache.writeHead.apply(this, arguments)
      }
  
      // patch res.write
      res.write = function (content) {
        accumulateContent(res, content)
        return res._apicache.write.apply(this, arguments)
      }
  
      // patch res.end
      res.end = function (content, encoding) {
        if (shouldCacheResponse(req, res, toggle)) {
          accumulateContent(res, content)
  
          if (res._apicache.cacheable && res._apicache.content) {
            addIndexEntries(key, req)
            var headers = res._apicache.headers || getSafeHeaders(res)
            var cacheObject = createCacheObject(
              res.statusCode,
              headers,
              res._apicache.content,
              encoding,
            )
            cacheResponse(key, cacheObject, duration)
  
            // display log entry
            var elapsed = new Date() - req.apicacheTimer
            debug(
              'adding cache entry for "' + key + '" @ ' + strDuration,
              logDuration(elapsed),
            )
            debug('_apicache.headers: ', res._apicache.headers)
            debug('res.getHeaders(): ', getSafeHeaders(res))
            debug('cacheObject: ', cacheObject)
          }
        }
  
        return res._apicache.end.apply(this, arguments)
      }
  
      next()
    }
  
    function sendCachedResponse(
      request,
      response,
      cacheObject,
      toggle,
      next,
      duration,
    ) {
      if (toggle && !toggle(request, response)) {
        return next()
      }
  
      var headers = getSafeHeaders(response)
  
      Object.assign(
        headers,
        filterBlacklistedHeaders(cacheObject.headers || {}),
        {
          // set properly-decremented max-age header.  This ensures that max-age is in sync with the cache expiration.
          'cache-control':
            'max-age=' +
            Math.max(
              0,
              (
                duration / 1000 -
                (new Date().getTime() / 1000 - cacheObject.timestamp)
              ).toFixed(0),
            ),
        },
      )
  
      // only embed apicache headers when not in production environment
  
      // unstringify buffers
      var data = cacheObject.data
      if (data && data.type === 'Buffer') {
        data =
          typeof data.data === 'number'
            ? new Buffer.alloc(data.data)
            : new Buffer.from(data.data)
      }
  
      // test Etag against If-None-Match for 304
      var cachedEtag = cacheObject.headers.etag
      var requestEtag = request.headers['if-none-match']
  
      if (requestEtag && cachedEtag === requestEtag) {
        response.writeHead(304, headers)
        return response.end()
      }
  
      response.writeHead(cacheObject.status || 200, headers)
  
      return response.end(data, cacheObject.encoding)
    }
  
    function syncOptions() {
      for (var i in middlewareOptions) {
        Object.assign(
          middlewareOptions[i].options,
          globalOptions,
          middlewareOptions[i].localOptions,
        )
      }
    }
  
    this.clear = function (target, isAutomatic) {
      var group = index.groups[target]
      var redis = globalOptions.redisClient
  
      if (group) {
        debug('clearing group "' + target + '"')
  
        group.forEach(function (key) {
          debug('clearing cached entry for "' + key + '"')
          clearTimeout(timers[key])
          delete timers[key]
          if (!globalOptions.redisClient) {
            memCache.delete(key)
          } else {
            try {
              redis.del(key)
            } catch (err) {
              console.log('[apicache] error in redis.del("' + key + '")')
            }
          }
          index.all = index.all.filter(doesntMatch(key))
        })
  
        delete index.groups[target]
      } else if (target) {
        debug(
          'clearing ' +
            (isAutomatic ? 'expired' : 'cached') +
            ' entry for "' +
            target +
            '"',
        )
        clearTimeout(timers[target])
        delete timers[target]
        // clear actual cached entry
        if (!redis) {
          memCache.delete(target)
        } else {
          try {
            redis.del(target)
          } catch (err) {
            console.log('[apicache] error in redis.del("' + target + '")')
          }
        }
  
        // remove from global index
        index.all = index.all.filter(doesntMatch(target))
  
        // remove target from each group that it may exist in
        Object.keys(index.groups).forEach(function (groupName) {
          index.groups[groupName] = index.groups[groupName].filter(
            doesntMatch(target),
          )
  
          // delete group if now empty
          if (!index.groups[groupName].length) {
            delete index.groups[groupName]
          }
        })
      } else {
        debug('clearing entire index')
  
        if (!redis) {
          memCache.clear()
        } else {
          // clear redis keys one by one from internal index to prevent clearing non-apicache entries
          index.all.forEach(function (key) {
            clearTimeout(timers[key])
            delete timers[key]
            try {
              redis.del(key)
            } catch (err) {
              console.log('[apicache] error in redis.del("' + key + '")')
            }
          })
        }
        this.resetIndex()
      }
  
      return this.getIndex()
    }
  
    function parseDuration(duration, defaultDuration) {
      if (typeof duration === 'number') return duration
  
      if (typeof duration === 'string') {
        var split = duration.match(/^([\d\.,]+)\s?(\w+)$/)
  
        if (split.length === 3) {
          var len = parseFloat(split[1])
          var unit = split[2].replace(/s$/i, '').toLowerCase()
          if (unit === 'm') {
            unit = 'ms'
          }
  
          return (len || 1) * (t[unit] || 0)
        }
      }
  
      return defaultDuration
    }
  
    this.getDuration = function (duration) {
      return parseDuration(duration, globalOptions.defaultDuration)
    }
  
    /**
     * Return cache performance statistics (hit rate).  Suitable for putting into a route:
     * <code>
     * app.get('/api/cache/performance', (req, res) => {
     *    res.json(apicache.getPerformance())
     * })
     * </code>
     */
    this.getPerformance = function () {
      return performanceArray.map(function (p) {
        return p.report()
      })
    }
  
    this.getIndex = function (group) {
      if (group) {
        return index.groups[group]
      } else {
        return index
      }
    }
  
    this.middleware = function cache(
      strDuration,
      middlewareToggle,
      localOptions,
    ) {
      var duration = instance.getDuration(strDuration)
      var opt = {}
  
      middlewareOptions.push({
        options: opt,
      })
  
      var options = function (localOptions) {
        if (localOptions) {
          middlewareOptions.find(function (middleware) {
            return middleware.options === opt
          }).localOptions = localOptions
        }
  
        syncOptions()
  
        return opt
      }
  
      options(localOptions)
  
      /**
       * A Function for non tracking performance
       */
      function NOOPCachePerformance() {
        this.report = this.hit = this.miss = function () {} // noop;
      }
  
      /**
       * A function for tracking and reporting hit rate.  These statistics are returned by the getPerformance() call above.
       */
      function CachePerformance() {
        /**
         * Tracks the hit rate for the last 100 requests.
         * If there have been fewer than 100 requests, the hit rate just considers the requests that have happened.
         */
        this.hitsLast100 = new Uint8Array(100 / 4) // each hit is 2 bits
  
        /**
         * Tracks the hit rate for the last 1000 requests.
         * If there have been fewer than 1000 requests, the hit rate just considers the requests that have happened.
         */
        this.hitsLast1000 = new Uint8Array(1000 / 4) // each hit is 2 bits
  
        /**
         * Tracks the hit rate for the last 10000 requests.
         * If there have been fewer than 10000 requests, the hit rate just considers the requests that have happened.
         */
        this.hitsLast10000 = new Uint8Array(10000 / 4) // each hit is 2 bits
  
        /**
         * Tracks the hit rate for the last 100000 requests.
         * If there have been fewer than 100000 requests, the hit rate just considers the requests that have happened.
         */
        this.hitsLast100000 = new Uint8Array(100000 / 4) // each hit is 2 bits
  
        /**
         * The number of calls that have passed through the middleware since the server started.
         */
        this.callCount = 0
  
        /**
         * The total number of hits since the server started
         */
        this.hitCount = 0
  
        /**
         * The key from the last cache hit.  This is useful in identifying which route these statistics apply to.
         */
        this.lastCacheHit = null
  
        /**
         * The key from the last cache miss.  This is useful in identifying which route these statistics apply to.
         */
        this.lastCacheMiss = null
  
        /**
         * Return performance statistics
         */
        this.report = function () {
          return {
            lastCacheHit: this.lastCacheHit,
            lastCacheMiss: this.lastCacheMiss,
            callCount: this.callCount,
            hitCount: this.hitCount,
            missCount: this.callCount - this.hitCount,
            hitRate: this.callCount == 0 ? null : this.hitCount / this.callCount,
            hitRateLast100: this.hitRate(this.hitsLast100),
            hitRateLast1000: this.hitRate(this.hitsLast1000),
            hitRateLast10000: this.hitRate(this.hitsLast10000),
            hitRateLast100000: this.hitRate(this.hitsLast100000),
          }
        }
  
        /**
         * Computes a cache hit rate from an array of hits and misses.
         * @param {Uint8Array} array An array representing hits and misses.
         * @returns a number between 0 and 1, or null if the array has no hits or misses
         */
        this.hitRate = function (array) {
          var hits = 0
          var misses = 0
          for (var i = 0; i < array.length; i++) {
            var n8 = array[i]
            for (j = 0; j < 4; j++) {
              switch (n8 & 3) {
                case 1:
                  hits++
                  break
                case 2:
                  misses++
                  break
              }
              n8 >>= 2
            }
          }
          var total = hits + misses
          if (total == 0) return null
          return hits / total
        }
  
        /**
         * Record a hit or miss in the given array.  It will be recorded at a position determined
         * by the current value of the callCount variable.
         * @param {Uint8Array} array An array representing hits and misses.
         * @param {boolean} hit true for a hit, false for a miss
         * Each element in the array is 8 bits, and encodes 4 hit/miss records.
         * Each hit or miss is encoded as to bits as follows:
         * 00 means no hit or miss has been recorded in these bits
         * 01 encodes a hit
         * 10 encodes a miss
         */
        this.recordHitInArray = function (array, hit) {
          var arrayIndex = ~~(this.callCount / 4) % array.length
          var bitOffset = (this.callCount % 4) * 2 // 2 bits per record, 4 records per uint8 array element
          var clearMask = ~(3 << bitOffset)
          var record = (hit ? 1 : 2) << bitOffset
          array[arrayIndex] = (array[arrayIndex] & clearMask) | record
        }
  
        /**
         * Records the hit or miss in the tracking arrays and increments the call count.
         * @param {boolean} hit true records a hit, false records a miss
         */
        this.recordHit = function (hit) {
          this.recordHitInArray(this.hitsLast100, hit)
          this.recordHitInArray(this.hitsLast1000, hit)
          this.recordHitInArray(this.hitsLast10000, hit)
          this.recordHitInArray(this.hitsLast100000, hit)
          if (hit) this.hitCount++
          this.callCount++
        }
  
        /**
         * Records a hit event, setting lastCacheMiss to the given key
         * @param {string} key The key that had the cache hit
         */
        this.hit = function (key) {
          this.recordHit(true)
          this.lastCacheHit = key
        }
  
        /**
         * Records a miss event, setting lastCacheMiss to the given key
         * @param {string} key The key that had the cache miss
         */
        this.miss = function (key) {
          this.recordHit(false)
          this.lastCacheMiss = key
        }
      }
  
      var perf = globalOptions.trackPerformance
        ? new CachePerformance()
        : new NOOPCachePerformance()
  
      performanceArray.push(perf)
  
      var cache = function (req, res, next) {
        function bypass() {
          debug('bypass detected, skipping cache.')
          return next()
        }
  
        // initial bypass chances
        if (!opt.enabled) return bypass()
        if (
          req.headers['x-apicache-bypass'] ||
          req.headers['x-apicache-force-fetch']
        )
          return bypass()
  
        // REMOVED IN 0.11.1 TO CORRECT MIDDLEWARE TOGGLE EXECUTE ORDER
        // if (typeof middlewareToggle === 'function') {
        //   if (!middlewareToggle(req, res)) return bypass()
        // } else if (middlewareToggle !== undefined && !middlewareToggle) {
        //   return bypass()
        // }
  
        // embed timer
        req.apicacheTimer = new Date()
  
        // In Express 4.x the url is ambigious based on where a router is mounted.  originalUrl will give the full Url
        var key = req.hostname + (req.originalUrl || req.url)
        // Remove querystring from key if jsonp option is enabled
        if (opt.jsonp) {
          key = url.parse(key).pathname
        }
  
        // add appendKey (either custom function or response path)
        if (typeof opt.appendKey === 'function') {
          key += '$$appendKey=' + opt.appendKey(req, res)
        } else if (opt.appendKey.length > 0) {
          var appendKey = req
  
          for (var i = 0; i < opt.appendKey.length; i++) {
            appendKey = appendKey[opt.appendKey[i]]
          }
          key += '$$appendKey=' + appendKey
        }
  
        // attempt cache hit
        var redis = opt.redisClient
        var cached = !redis ? memCache.getValue(key) : null
  
        // send if cache hit from memory-cache
        if (cached) {
          var elapsed = new Date() - req.apicacheTimer
          debug(
            'sending cached (memory-cache) version of',
            key,
            logDuration(elapsed),
          )
  
          perf.hit(key)
          return sendCachedResponse(
            req,
            res,
            cached,
            middlewareToggle,
            next,
            duration,
          )
        }
  
        // send if cache hit from redis
        if (redis && redis.connected) {
          try {
            redis.hgetall(key, function (err, obj) {
              if (!err && obj && obj.response) {
                var elapsed = new Date() - req.apicacheTimer
                debug(
                  'sending cached (redis) version of',
                  key,
                  logDuration(elapsed),
                )
  
                perf.hit(key)
                return sendCachedResponse(
                  req,
                  res,
                  JSON.parse(obj.response),
                  middlewareToggle,
                  next,
                  duration,
                )
              } else {
                perf.miss(key)
                return makeResponseCacheable(
                  req,
                  res,
                  next,
                  key,
                  duration,
                  strDuration,
                  middlewareToggle,
                )
              }
            })
          } catch (err) {
            // bypass redis on error
            perf.miss(key)
            return makeResponseCacheable(
              req,
              res,
              next,
              key,
              duration,
              strDuration,
              middlewareToggle,
            )
          }
        } else {
          perf.miss(key)
          return makeResponseCacheable(
            req,
            res,
            next,
            key,
            duration,
            strDuration,
            middlewareToggle,
          )
        }
      }
  
      cache.options = options
  
      return cache
    }
  
    this.options = function (options) {
      if (options) {
        Object.assign(globalOptions, options)
        syncOptions()
  
        if ('defaultDuration' in options) {
          // Convert the default duration to a number in milliseconds (if needed)
          globalOptions.defaultDuration = parseDuration(
            globalOptions.defaultDuration,
            3600000,
          )
        }
  
        if (globalOptions.trackPerformance) {
          debug(
            'WARNING: using trackPerformance flag can cause high memory usage!',
          )
        }
  
        return this
      } else {
        return globalOptions
      }
    }
  
    this.resetIndex = function () {
      index = {
        all: [],
        groups: {},
      }
    }
  
    this.newInstance = function (config) {
      var instance = new ApiCache()
  
      if (config) {
        instance.options(config)
      }
  
      return instance
    }
  
    this.clone = function () {
      return this.newInstance(this.options())
    }
  
    // initialize index
    this.resetIndex()
  }
  
  module.exports = new ApiCache()
  

server/utils/crypto.js

const crypto = require('crypto')
const iv = Buffer.from('0102030405060708')
const presetKey = Buffer.from('0CoJUm6Qyw8W8jud')
const linuxapiKey = Buffer.from('rFgB&h#%2?^eDg:Q')
const base62 = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
const publicKey =
  '-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDgtQn2JZ34ZC28NWYpAUd98iZ37BUrX/aKzmFbt7clFSs6sXqHauqKWqdtLkF2KexO40H1YTX8z2lSgBBOAxLsvaklV8k4cBFK9snQXE9/DDaFt6Rr7iVZMldczhC0JNgTz+SHXT6CBHuX3e9SdB1Ua44oncaTWz7OBGLbCiK45wIDAQAB\n-----END PUBLIC KEY-----'
const eapiKey = 'e82ckenh8dichen8'

const aesEncrypt = (buffer, mode, key, iv) => {
  const cipher = crypto.createCipheriv('aes-128-' + mode, key, iv)
  return Buffer.concat([cipher.update(buffer), cipher.final()])
}

const rsaEncrypt = (buffer, key) => {
  buffer = Buffer.concat([Buffer.alloc(128 - buffer.length), buffer])
  return crypto.publicEncrypt(
    { key: key, padding: crypto.constants.RSA_NO_PADDING },
    buffer,
  )
}

const weapi = (object) => {
  const text = JSON.stringify(object)
  const secretKey = crypto
    .randomBytes(16)
    .map((n) => base62.charAt(n % 62).charCodeAt())
  return {
    params: aesEncrypt(
      Buffer.from(
        aesEncrypt(Buffer.from(text), 'cbc', presetKey, iv).toString('base64'),
      ),
      'cbc',
      secretKey,
      iv,
    ).toString('base64'),
    encSecKey: rsaEncrypt(secretKey.reverse(), publicKey).toString('hex'),
  }
}

const linuxapi = (object) => {
  const text = JSON.stringify(object)
  return {
    eparams: aesEncrypt(Buffer.from(text), 'ecb', linuxapiKey, '')
      .toString('hex')
      .toUpperCase(),
  }
}

const eapi = (url, object) => {
  const text = typeof object === 'object' ? JSON.stringify(object) : object
  const message = `nobody${url}use${text}md5forencrypt`
  const digest = crypto.createHash('md5').update(message).digest('hex')
  const data = `${url}-36cd479b6b5-${text}-36cd479b6b5-${digest}`
  return {
    params: aesEncrypt(Buffer.from(data), 'ecb', eapiKey, '')
      .toString('hex')
      .toUpperCase(),
  }
}

const decrypt = (cipherBuffer) => {
  const decipher = crypto.createDecipheriv('aes-128-ecb', eapiKey, '')
  return Buffer.concat([decipher.update(cipherBuffer), decipher.final()])
}

module.exports = { weapi, linuxapi, eapi, decrypt }

server/utils/index.js

module.exports = {
    toBoolean(val) {
      if (typeof val === 'boolean') return val
      if (val === '') return val
      return val === 'true' || val == '1'
    },
    cookieToJson(cookie) {
      if (!cookie) return {}
      let cookieArr = cookie.split(';')
      let obj = {}
      cookieArr.forEach((i) => {
        let arr = i.split('=')
        obj[arr[0]] = arr[1]
      })
      return obj
    },
    getRandom(num) {
      var random = Math.floor(
        (Math.random() + Math.floor(Math.random() * 9 + 1)) *
          Math.pow(10, num - 1),
      )
      return random
    },
  }
  

server/utils/memory-cache.js

function MemoryCache() {
    this.cache = {}
    this.size = 0
  }
  
  MemoryCache.prototype.add = function (key, value, time, timeoutCallback) {
    var old = this.cache[key]
    var instance = this
  
    var entry = {
      value: value,
      expire: time + Date.now(),
      timeout: setTimeout(function () {
        instance.delete(key)
        return (
          timeoutCallback &&
          typeof timeoutCallback === 'function' &&
          timeoutCallback(value, key)
        )
      }, time),
    }
  
    this.cache[key] = entry
    this.size = Object.keys(this.cache).length
  
    return entry
  }
  
  MemoryCache.prototype.delete = function (key) {
    var entry = this.cache[key]
  
    if (entry) {
      clearTimeout(entry.timeout)
    }
  
    delete this.cache[key]
  
    this.size = Object.keys(this.cache).length
  
    return null
  }
  
  MemoryCache.prototype.get = function (key) {
    var entry = this.cache[key]
  
    return entry
  }
  
  MemoryCache.prototype.getValue = function (key) {
    var entry = this.get(key)
  
    return entry && entry.value
  }
  
  MemoryCache.prototype.clear = function () {
    Object.keys(this.cache).forEach(function (key) {
      this.delete(key)
    }, this)
  
    return true
  }
  
  module.exports = MemoryCache
  

server/utils/request.js

const encrypt = require('./crypto')
const crypto = require('crypto')
const { default: axios } = require('axios')
const { PacProxyAgent } = require('pac-proxy-agent')
const http = require('http')
const https = require('https')
const tunnel = require('tunnel')
const { URLSearchParams, URL } = require('url')
const config = require('../utils/config.json')
// request.debug = true // 开启可看到更详细信息

const chooseUserAgent = (ua = false) => {
    // 伪造这个头显然是用来欺骗服务器,让它认为这个请求是来自浏览器,而不是同样也来自服务端。
  const userAgentList = {
    mobile: [
      // iOS 13.5.1 14.0 beta with safari
      'Mozilla/5.0 (iPhone; CPU iPhone OS 13_5_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.1.1 Mobile/15E148 Safari/604.1',
      'Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0 Mobile/15E148 Safari/604.',
      // iOS with qq micromsg
      'Mozilla/5.0 (iPhone; CPU iPhone OS 13_5_1 like Mac OS X) AppleWebKit/602.1.50 (KHTML like Gecko) Mobile/14A456 QQ/6.5.7.408 V1_IPH_SQ_6.5.7_1_APP_A Pixel/750 Core/UIWebView NetType/4G Mem/103',
      'Mozilla/5.0 (iPhone; CPU iPhone OS 13_5_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 MicroMessenger/7.0.15(0x17000f27) NetType/WIFI Language/zh',
      // Android -> Huawei Xiaomi
      'Mozilla/5.0 (Linux; Android 9; PCT-AL10) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.64 HuaweiBrowser/10.0.3.311 Mobile Safari/537.36',
      'Mozilla/5.0 (Linux; U; Android 9; zh-cn; Redmi Note 8 Build/PKQ1.190616.001) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/71.0.3578.141 Mobile Safari/537.36 XiaoMi/MiuiBrowser/12.5.22',
      // Android + qq micromsg
      'Mozilla/5.0 (Linux; Android 10; YAL-AL00 Build/HUAWEIYAL-AL00; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/78.0.3904.62 XWEB/2581 MMWEBSDK/200801 Mobile Safari/537.36 MMWEBID/3027 MicroMessenger/7.0.18.1740(0x27001235) Process/toolsmp WeChat/arm64 NetType/WIFI Language/zh_CN ABI/arm64',
      'Mozilla/5.0 (Linux; U; Android 8.1.0; zh-cn; BKK-AL10 Build/HONORBKK-AL10) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/66.0.3359.126 MQQBrowser/10.6 Mobile Safari/537.36',
    ],
    pc: [
      // macOS 10.15.6  Firefox / Chrome / Safari
      'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:80.0) Gecko/20100101 Firefox/80.0',
      'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.30 Safari/537.36',
      'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.1.2 Safari/605.1.15',
      // Windows 10 Firefox / Chrome / Edge
      'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:80.0) Gecko/20100101 Firefox/80.0',
      'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.30 Safari/537.36',
      'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36 Edge/13.10586',
      // Linux 就算了
    ],
  }
  let realUserAgentList =
    userAgentList[ua] || userAgentList.mobile.concat(userAgentList.pc)
  return ['mobile', 'pc', false].indexOf(ua) > -1
    ? realUserAgentList[Math.floor(Math.random() * realUserAgentList.length)]
    : ua
}
const createRequest = (method, url, data = {}, options) => {
  return new Promise((resolve, reject) => {
    let headers = { 'User-Agent': chooseUserAgent(options.ua) }
    if (method.toUpperCase() === 'POST')
      headers['Content-Type'] = 'application/x-www-form-urlencoded'// 如果是post请求,修改编码格式
    if (url.includes('music.163.com'))
      headers['Referer'] = 'https://music.163.com'// 伪造Referer头
    let ip = options.realIP || options.ip || ''// 设置ip头部
    // console.log(ip) 
    if (ip) {
      headers['X-Real-IP'] = ip
      headers['X-Forwarded-For'] = ip
    }
    // headers['X-Real-IP'] = '118.88.88.88'// 设置cookie
    if (typeof options.cookie === 'object') {
      options.cookie = {
        ...options.cookie,
        __remember_me: true,
        NMTID: crypto.randomBytes(16).toString('hex'),
        _ntes_nuid: crypto.randomBytes(16).toString('hex'),
      }
      if (!options.cookie.MUSIC_U) {
        // 游客
        if (!options.cookie.MUSIC_A) {
          options.cookie.MUSIC_A = config.anonymous_token
        }
      }
      headers['Cookie'] = Object.keys(options.cookie)
        .map(
          (key) =>
            encodeURIComponent(key) +
            '=' +
            encodeURIComponent(options.cookie[key]),
        )
        .join('; ')
    } else if (options.cookie) {
      headers['Cookie'] = options.cookie
    } else {
      headers['Cookie'] = '__remember_me=true; NMTID=xxx'
    }
    // console.log(options.cookie, headers['Cookie'])
    if (options.crypto === 'weapi') {
      let csrfToken = (headers['Cookie'] || '').match(/_csrf=([^(;|$)]+)/)
      data.csrf_token = csrfToken ? csrfToken[1] : ''
      data = encrypt.weapi(data)
      url = url.replace(/\w*api/, 'weapi')
    } else if (options.crypto === 'linuxapi') {
      data = encrypt.linuxapi({
        method: method,
        url: url.replace(/\w*api/, 'api'),
        params: data,
      })
      headers['User-Agent'] =
        'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36'
      url = 'https://music.163.com/api/linux/forward'
    } else if (options.crypto === 'eapi') {
      const cookie = options.cookie || {}
      const csrfToken = cookie['__csrf'] || ''
      const header = {
        osver: cookie.osver, //系统版本
        deviceId: cookie.deviceId, //encrypt.base64.encode(imei + '\t02:00:00:00:00:00\t5106025eb79a5247\t70ffbaac7')
        appver: cookie.appver || '8.9.70', // app版本
        versioncode: cookie.versioncode || '140', //版本号
        mobilename: cookie.mobilename, //设备model
        buildver: cookie.buildver || Date.now().toString().substr(0, 10),
        resolution: cookie.resolution || '1920x1080', //设备分辨率
        __csrf: csrfToken,
        os: cookie.os || 'android',
        channel: cookie.channel,
        requestId: `${Date.now()}_${Math.floor(Math.random() * 1000)
          .toString()
          .padStart(4, '0')}`,
      }
      if (cookie.MUSIC_U) header['MUSIC_U'] = cookie.MUSIC_U
      if (cookie.MUSIC_A) header['MUSIC_A'] = cookie.MUSIC_A
      headers['Cookie'] = Object.keys(header)
        .map(
          (key) =>
            encodeURIComponent(key) + '=' + encodeURIComponent(header[key]),
        )
        .join('; ')
      data.header = header
      data = encrypt.eapi(options.url, data)
      url = url.replace(/\w*api/, 'eapi')
    }
    const answer = { status: 500, body: {}, cookie: [] }
    let settings = {
      method: method,
      url: url,
      headers: headers,
      data: new URLSearchParams(data).toString(),
      httpAgent: new http.Agent({ keepAlive: true }),
      httpsAgent: new https.Agent({ keepAlive: true }),
    }

    if (options.crypto === 'eapi') settings.encoding = null

    if (options.proxy) {
      if (options.proxy.indexOf('pac') > -1) {
        settings.httpAgent = new PacProxyAgent(options.proxy)
        settings.httpsAgent = new PacProxyAgent(options.proxy)
      } else {
        const purl = new URL(options.proxy)
        if (purl.hostname) {
          const agent = tunnel.httpsOverHttp({
            proxy: {
              host: purl.hostname,
              port: purl.port || 80,
            },
          })
          settings.httpsAgent = agent
          settings.httpAgent = agent
          settings.proxy = false
        } else {
          console.error('代理配置无效,不使用代理')
        }
      }
    } else {
      settings.proxy = false
    }
    if (options.crypto === 'eapi') {
      settings = {
        ...settings,
        responseType: 'arraybuffer',
      }
    }
    axios(settings)
      .then((res) => {
        const body = res.data
        answer.cookie = (res.headers['set-cookie'] || []).map((x) =>
          x.replace(/\s*Domain=[^(;|$)]+;*/, ''),
        )
        try {
          if (options.crypto === 'eapi') {
            answer.body = JSON.parse(encrypt.decrypt(body).toString())
          } else {
            answer.body = body
          }

          answer.status = answer.body.code || res.status
          if (
            [201, 302, 400, 502, 800, 801, 802, 803].indexOf(answer.body.code) >
            -1
          ) {
            // 特殊状态码
            answer.status = 200
          }
        } catch (e) {
          // console.log(e)
          try {
            answer.body = JSON.parse(body.toString())
          } catch (err) {
            // console.log(err)
            // can't decrypt and can't parse directly
            answer.body = body
          }
          answer.status = res.status
        }

        answer.status =
          100 < answer.status && answer.status < 600 ? answer.status : 400
        if (answer.status === 200) resolve(answer)
        else reject(answer)
      })
      .catch((err) => {
        answer.status = 502
        answer.body = { code: 502, msg: err }
        reject(answer)
      })
  })
}

module.exports = createRequest

6. 在 server 新建文件 server/server.js

const fs = require('fs')
const path = require('path')
const exec = require('child_process').exec //子进程
const decode = require('safe-decode-uri-component')

const packageJSON = require('./package.json')

const { cookieToJson } = require('./utils/index')

const request = require('./utils/request')

/**
 * 版本检查结果。
 * @readonly
 * @enum {number}
 */
const VERSION_CHECK_RESULT = {
    FAILED: -1,
    NOT_LATEST: 0,
    LATEST: 1,
}

/**
 * @typedef {{
*   identifier?: string,
*   route: string,
*   module: any
* }} ModuleDefinition
*/

/**
* @typedef {{
*   port?: number,
*   host?: string,
*   checkVersion?: boolean,
*   moduleDefs?: ModuleDefinition[]
* }} NcmApiOptions
*/

/**
* @typedef {{
*   status: VERSION_CHECK_RESULT,
*   ourVersion?: string,
*   npmVersion?: string,
* }} VersionCheckResult
*/

/**
* @typedef {{
*  server?: import('http').Server,
* }} ExpressExtension
*/

/**
 * 动态获取模块定义。
 *
 * @param {string} modulesPath 模块的路径 (JS).
 * @param {Record<string, string>} [specificRoute] 特定模块的特定路径。
 * @param {boolean} [doRequire] If true, require() the module directly. 如果为真,直接引用模块。
 * Otherwise, print out the module path. Default to true. 否则,打印模块路径。默认为 true
 * @returns {Promise<ModuleDefinition[]>} The module definitions. 模块定义。
 *
 * @example getModuleDefinitions("./module", {"album_new.js": "/album/create"})
 */
async function getModulesDefinitions(
    modulesPath,
    specificRoute,
    doRequire = true,
  ) {
    const files = await fs.promises.readdir(modulesPath)//返回一个Promise对象,异步读取目录中的文件和子目录名称
    const parseRoute = (/** @type {string} */ fileName) =>
      specificRoute && fileName in specificRoute
        ? specificRoute[fileName]
        : `/${fileName.replace(/\.js$/i, '').replace(/_/g, '/')}`
  
    // 遍历目录下的所有文件
    const modules = files
      .reverse()// 反转列表中的元素
      .filter((file) => file.endsWith('.js'))// 过滤出js文件
      .map((file) => {//map()和forEach()方法非常相似。 两种方法都遍历数组并将函数应用于每个元素。 主要区别在于map()返回一个新数组,而forEach()不返回任何内容。
        const identifier = file.split('.').shift()// 模块标识 shift() 方法用于把数组的第一个元素从其中删除,并返回第一个元素的值。
        const route = parseRoute(file)// 模块对应的路由
        const modulePath = path.join(modulesPath, file)// 模块路径
        const module = doRequire ? require(modulePath) : modulePath// 加载模块
  
        return { identifier, route, module }
      })
  
    return modules
}


/**
 * 检查此API的版本是否为最新版本。
 *
 * @returns {Promise<VersionCheckResult>} If true, this API is up-to-date; 如果为真,API 已更新到最新版本。
 * otherwise, this API should be upgraded and you would 否则,API 需要更新,并提醒用户手动更新。
 * need to notify users to upgrade it manually.
 */
async function checkVersion() {
    return new Promise((resolve) => {
      exec('npm info NeteaseCloudMusicApi version', (err, stdout) => {// 生成一个子shell,能够在shell中执行命令。
        if (!err) {
          let version = stdout.trim()// 4.9.2
  
          /**
           * @param {VERSION_CHECK_RESULT} status
           */
          const resolveStatus = (status) =>
            resolve({// 成功
              status,
              ourVersion: packageJSON.version,
              npmVersion: version,
            })
  
          resolveStatus(
            packageJSON.version < version
              ? VERSION_CHECK_RESULT.NOT_LATEST
              : VERSION_CHECK_RESULT.LATEST,
          )
        }
      })
  
      resolve({
        status: VERSION_CHECK_RESULT.FAILED,// 失败
      })
    })
  }
  

  /**
 * Construct the server of NCM API. 构建NCM API服务器。
 *
 * @param {ModuleDefinition[]} [moduleDefs] Customized module definitions [advanced] 自定义模块定义
 * @returns {Promise<import("express").Express>} The server instance. 服务器实例。
 */
async function consturctServer(moduleDefs, app) {

    /**
     * Special Routers 特殊路由
     */
    const special = {
      'daily_signin.js': '/daily_signin',
      'fm_trash.js': '/fm_trash',
      'personal_fm.js': '/personal_fm',
    }
  
    /**
     * Load every modules in this directory 加载 /module 目录下的所有模块,每个模块对应一个接口
     */
    const moduleDefinitions =
      moduleDefs ||
      (await getModulesDefinitions(path.join(__dirname, 'module'), special))
  
    for (const moduleDef of moduleDefinitions) {
      // Register the route. 注册路由
      app.use(moduleDef.route, async (req, res) => {
        // cookie也可以从查询参数、请求体上传来
        [req.query, req.body].forEach((item) => {
          if (typeof item.cookie === 'string') {
            // 将cookie字符串转换成json类型
            item.cookie = cookieToJson(decode(item.cookie))
          }
        })
  
         // 把cookie、查询参数、请求头、文件都整合到一起,作为参数传给每个模块
        let query = Object.assign(
          {},
          { cookie: req.cookies },
          req.query,
          req.body,
          req.files,
        )
  
        try {
           // 执行模块方法,即发起对网易云音乐接口的请求
          const moduleResponse = await moduleDef.module(query, (...params) => {
            // 参数注入客户端IP
            const obj = [...params]
            let ip = req.ip
            // 处理ip,为了实现IPv4-IPv6互通,IPv4地址前会增加::ffff:
            if (ip.substr(0, 7) == '::ffff:') {
              ip = ip.substr(7)
            }
            // console.log(ip)
            obj[3] = {
              ...obj[3],
              ip,
            }
            return request(...obj)
          })
          console.log('[OK]', decode(req.originalUrl))
          // 请求成功后,获取响应中的cookie,并且通过Set-Cookie响应头来将这个cookie设置到前端浏览器上
          const cookies = moduleResponse.cookie
          if (!query.noCookie) {
            if (Array.isArray(cookies) && cookies.length > 0) {
              if (req.protocol === 'https') {
                // Try to fix CORS SameSite Problem
                // 去掉跨域请求cookie的SameSite限制,这个属性用来限制第三方Cookie,从而减少安全风险
                res.append(
                  'Set-Cookie',
                  cookies.map((cookie) => {
                    return cookie + '; SameSite=None; Secure'
                  }),
                )
              } else {
                res.append('Set-Cookie', cookies)
              }
            }
          }
          // 回复请求
          res.status(moduleResponse.status).send(moduleResponse.body)
        } catch (/** @type {*} */ moduleResponse) {
          console.log('[ERR]', decode(req.originalUrl), {
            status: moduleResponse.status,
            body: moduleResponse.body,
          })
          // 请求失败处理
          // 没有响应体,返回404
          if (!moduleResponse.body) {
            res.status(404).send({
              code: 404,
              data: null,
              msg: 'Not Found',
            })
            return
          }
          // 301代表调用了需要登录的接口,但是并没有登录
          if (moduleResponse.body.code == '301')
            moduleResponse.body.msg = '需要登录'
          if (!query.noCookie) {
            res.append('Set-Cookie', moduleResponse.cookie)
          }
  
          res.status(moduleResponse.status).send(moduleResponse.body)
        }
      })
    }
  
  }
  
  /**
   * Serve the NCM API.
   * @param {NcmApiOptions} options
   * @returns {Promise<import('express').Express & ExpressExtension>}
   */
  async function serveNcmApi(options, app) {
    // const port = Number(options.port || process.env.PORT || '3000')
    // const host = options.host || process.env.HOST || ''
  
    const checkVersionSubmission =
      options.checkVersion &&
      checkVersion().then(({ npmVersion, ourVersion, status }) => {// 检查版本
        if (status == VERSION_CHECK_RESULT.NOT_LATEST) {
          console.log(
            `最新版本: ${npmVersion}, 当前版本: ${ourVersion}, 请及时更新`,
          )
        }
      })

    const constructServerSubmission = consturctServer(options.moduleDefs, app)
  
    await Promise.all([// 当数组里的P1,P2都执行完成时,页面才显示。
      checkVersionSubmission,
      constructServerSubmission,
    ])
  
    // const appExt = app
    // appExt.server = app.listen(port, host, () => {
    //   console.log(`server running @ http://${host ? host : 'localhost'}:${port}`)
    // })
  
    // return appExt
  }
  
  module.exports = {serveNcmApi, getModulesDefinitions}

7. 修改 server/app.js

var createError = require('http-errors');//路由错误处理
var express = require('express');
var path = require('path');
var cookieParser = require('cookie-parser');
var logger = require('morgan');//错误输出日志

const fileUpload = require('express-fileupload')
const cache = require('./utils/apicache').middleware


var indexRouter = require('./routes/index');
var usersRouter = require('./routes/users');

var app = express();

// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'pug');

app.use(logger('dev'));//开发环境设置
app.use(express.json());//解析json
app.use(express.urlencoded({ extended: false }));//post参数设置
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));//静态资源设置

// app.use('/', indexRouter);
// app.use('/users', usersRouter);

// // catch 404 and forward to error handler
// // 当于所有路径都不匹配时,报404,自定义
// app.use(function(req, res, next) {
//   next(createError(404));
// });

// // error handler
// app.use(function(err, req, res, next) {
//   // set locals, only providing error in development
//   res.locals.message = err.message;
//   res.locals.error = req.app.get('env') === 'development' ? err : {};

//   // render the error page
//   res.status(err.status || 500);
//   res.render('error');
// });

const { CORS_ALLOW_ORIGIN } = process.env// es6的写法 const { xxx } = this.state; 其实就相当于:const xxx = this.state.xxx
app.set('trust proxy', true)// 设置为true,则客户端的IP地址被理解为X-Forwarded-*报头中最左边的条目

/**
     * CORS & Preflight request 预检请求
     */
app.use((req, res, next) => {
  if (req.path !== '/' && !req.path.includes('.')) {
    res.set({
      'Access-Control-Allow-Credentials': true,// 跨域情况下,允许客户端携带验证信息,比如cookie,同时,前端发送请求时也需要设置 withCredentials: true
      'Access-Control-Allow-Origin':
        CORS_ALLOW_ORIGIN || req.headers.origin || '*',// 允许跨域请求的域名,设置为*代表允许所有域名
      'Access-Control-Allow-Headers': 'X-Requested-With,Content-Type',// 用于给预检请求(options)列出服务端允许的自定义标头,如果前端发送的请求中包含自定义的请求标头,且该标头不包含在Access-Control-Allow-Headers中,那么该请求无法成功发起
      'Access-Control-Allow-Methods': 'PUT,POST,GET,DELETE,OPTIONS',// 设置跨域请求允许的请求方法
      'Content-Type': 'application/json; charset=utf-8',// 设置响应数据的类型及编码
    })
  }
  // OPTIONS为预检请求,复杂请求会在发送真正的请求前先发送一个预检请求,获取服务器支持的Access-Control-Allow-xxx相关信息,判断后续是否有必要再发送真正的请求,返回状态码204代表请求成功,但是没有内容
  req.method === 'OPTIONS' ? res.status(204).end() : next()
})
    

 /**
     * Cookie Parser 解析Cookie
     */
 app.use((req, _, next) => {
  req.cookies = {}
  //;(req.headers.cookie || '').split(/\s*;\s*/).forEach((pair) => { //  Polynomial regular expression //
  // 从请求头中读取cookie,cookie格式为:name=value;name2=value2...,所以先根据;切割为数组
  ;(req.headers.cookie || '').split(/;\s+|(?<!\s)\s+$/g).forEach((pair) => {
    let crack = pair.indexOf('=')
    // 没有值的直接跳过
    if (crack < 1 || crack == pair.length - 1) return
    // 将cookie保存到cookies对象上
    req.cookies[decode(pair.slice(0, crack)).trim()] = decode(
      pair.slice(crack + 1),
    ).trim()
  })
  next()
})

/**
 * 文件上传处理
 */

app.use(fileUpload())

/**
 * Cache
 * 缓存请求,两分钟内同样的请求会从缓存里读取数据,不会向网易云音乐服务器发送请求
 */
app.use(cache('2 minutes', (_, res) => res.statusCode === 200))

async function start() {
  // 如果需要手动修改anonymous_token,需要注释generateConfig调用
  require('./server').serveNcmApi({
    checkVersion: true,
  }, app)
}
start()

module.exports = app;

8. 显示结果

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值