You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

异步循环嵌套异步循环是否合理?JavaScript实现优化咨询

优化异步嵌套循环的JavaScript(NightmareJS + SQL Server)实现

我有一段JavaScript代码,功能是读取.txt文件生成对象数组,遍历数组元素,再用NightmareJS请求每个元素里的链接,最后把结果写入SQL Server。代码能正常运行,但这种多层异步嵌套的写法感觉只是临时方案,而且控制台先输出完所有日志才开始发起请求,想问问有没有更规范、更优的实现方式?

现有代码

const Nightmare = require('nightmare'); 
const fs = require('fs'); 
const async = require('async'); 
const sql = require('mssql'); 

var links = recuperarLinks(); 

function recuperarLinks(){ 
  //Read the txt file and return an array 
} 

const bigFunction = () => { 
  var aparelho = ''; 
  async.eachSeries(links, async function (link) { 
    console.log('Zip Code: ' + link.zipCode); 
    async.eachSeries(link.links, async function(url){ 
      console.log('URL: ' + url); 
      try { 
        await nightmare.goto(link2) 
          .evaluate(function () { 
            //return some elements 
          }) 
          .end() 
          .then(function (result) { 
            //ajust the result 
            dadosAjustados.forEach(function (obj) { 
              //save the data 
              saveDatabase(obj, link.cep); 
            }); 
          }); 
      } catch (e) { 
        console.error(e); 
      } 
    }, function(err){ 
      console.log('Erro: '); 
      console.log(err); 
    }) 
  }, function (erro) { 
    if (erro) { 
      console.log('Erro: '); 
      console.log(erro); 
    } 
  }); 
} 

async function salvarBanco(dados, cep){ 
  const pool = new sql.ConnectionPool({ 
    user: 'sa', 
    password: 'xxx', 
    server: 'xxx', 
    database: 'xxx' 
  }); 
  pool.connect().then(function(){ 
    const request = new sql.Request(pool); 
    const insert = "some insert" 
    request.query(insert).then(function(recordset){ 
      console.log('Dado inserido'); 
      pool.close(); 
    }).catch(function(err){ 
      console.log(err); 
      pool.close(); 
    }) 
  }).catch(function(err){ 
    console.log(err); 
  }); 
} 

lerArquivo(); 

当前控制台输出

Fetching Data from cep 1 
Fetching Data from url 1 
Fetching Data from cep 2 
Fetching Data from url 2 
Fetching Data from cep 3 
Fetching Data from url 3 

输出完成后才开始发起请求。


好问题!这种多层async.eachSeries嵌套确实容易让代码变得臃肿难维护,而且你提到的日志先输出再执行请求的问题,本质是async库和async/await混用导致的时序偏差。咱们可以用原生的async/await结合for...of循环来重构,代码会更清晰易读,还能解决时序问题。

优化后的实现方案

我从几个核心点入手重构了代码,兼顾可读性、性能和时序正确性:

const Nightmare = require('nightmare'); 
const fs = require('fs'); 
const sql = require('mssql'); 

// 全局复用数据库连接池(避免频繁创建销毁连接的开销)
let sqlPool;

// 初始化数据库连接池
async function initSqlPool() {
  sqlPool = new sql.ConnectionPool({ 
    user: 'sa', 
    password: 'xxx', 
    server: 'xxx', 
    database: 'xxx' 
  });
  await sqlPool.connect();
  console.log('数据库连接池已初始化');
}

function recuperarLinks(){ 
  //Read the txt file and return an array 
} 

// 处理单个邮编组下的所有链接
async function processLinkGroup(linkGroup) {
  console.log('Zip Code: ' + linkGroup.zipCode);
  
  // 串行处理当前邮编下的每个链接
  for (const url of linkGroup.links) {
    console.log('URL: ' + url);
    try {
      // 每个请求新建Nightmare实例,避免上下文污染
      const nightmare = Nightmare();
      // 统一用async/await处理Nightmare异步操作,时序更可控
      const result = await nightmare
        .goto(url) // 修正原代码的link2笔误,应该用当前遍历的url
        .evaluate(() => {
          // 返回需要提取的页面元素,示例写法
          return document.querySelector('.target-element')?.innerText;
        })
        .end();

      // 处理并格式化结果(替换成你实际的逻辑)
      const dadosAjustados = formatResult(result);
      // 串行写入数据库
      for (const obj of dadosAjustados) {
        await salvarBanco(obj, linkGroup.zipCode);
      }
    } catch (e) {
      console.error(`处理URL ${url} 时出错:`, e);
    }
  }
}

// 数据库写入函数(复用全局连接池)
async function salvarBanco(dados, cep) {
  try {
    const request = new sql.Request(sqlPool);
    // 用参数化查询防止SQL注入,替换成你的实际表和字段
    const insert = `INSERT INTO YourTable (field1, field2, zip_code) VALUES (@f1, @f2, @zip)`;
    request.input('f1', sql.VarChar, dados.field1);
    request.input('f2', sql.Int, dados.field2);
    request.input('zip', sql.VarChar, cep);
    
    await request.query(insert);
    console.log('Dado inserido');
  } catch (err) {
    console.error('写入数据库出错:', err);
  }
}

// 主流程入口
async function main() {
  try {
    await initSqlPool();
    const links = recuperarLinks();
    
    // 串行处理每个邮编组
    for (const linkGroup of links) {
      await processLinkGroup(linkGroup);
    }
    
    console.log('所有任务处理完成');
    await sqlPool.close();
  } catch (err) {
    console.error('主流程出错:', err);
    if (sqlPool) await sqlPool.close();
  }
}

// 启动程序
main();

核心优化点说明

  1. 时序问题解决:用for...of配合await,每一步都会等待前一个操作完成才执行下一个,日志会和实际请求流程同步,不会出现先全输出日志再执行的情况。
  2. 代码可读性提升:把大函数拆分成initSqlPool、processLinkGroup等单一职责的小函数,逻辑分层清晰,更容易调试和维护。
  3. 性能优化:全局复用数据库连接池,避免频繁创建销毁连接的开销;每个请求新建Nightmare实例,防止不同请求之间的上下文污染。
  4. 安全性提升:使用参数化查询代替字符串拼接,彻底避免SQL注入风险。

内容的提问来源于stack exchange,提问作者Joao Victor

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.28 10:18:35