最懒的前端多语言策略(三)

之前在umi项目下弄了个umi-plugin-gtj,原理是在umi的生命周期onDevCompileDone下执行输出json逻辑,但发现有非常多的地方可以优化
回顾:
最懒的前端多语言策略(一)
最懒的前端多语言策略(二)

可见的不足

发现每次编译都会执行run(逻辑入口) -> fileDisplay(递归目录)
很容易发现只要有小小改动,都会走递归逻辑,这样肯定不行

旧逻辑的不足

尝试解决

1.于是我想在umi本身提供的onDevCompileDone里头看看有没返回,还真实看不出哪个是,刚提了个issue,估计也不会回的,先放弃在onDevCompileDone里做的想法


onDevCompileDone回调参数

2.于是我打算在onStart里搞,onStart是只执行一次,onDevCompileDone思路又先放弃,那能在里面写自定义监听逻辑了

fs.watch

简单的watch逻辑

fs.watch(entry, {
  recursive: true, // 递归
}, (event, filename) => {
  console.log(`${filename}文件发生更新`)
 // 执行逻辑
 run(`${entry}/${filename}`)
})

众所周知,watch回调是非常敏感的,即使只修改一个字母,可能也会触发4次回调,这样肯定不行,我们可以判断只在event为'change'时做逻辑

if(event !== ‘change')return

然而我发现我只在文件复制粘贴,什么内容没变都会触发watch,我觉得是修改时间也会影响,于是得直接判断文件内容,上md5

const md5 = require('md5')
let preveMd5 = null
...
fs.watch(entry, {
  recursive: true, // 递归
}, (event, filename) => {
  if (event !== 'change') return
  if(!filename) return
  let currentMd5 = md5(fs.readFileSync(`${entry}/${filename}`))
  if (currentMd5 == preveMd5) {
     return
  }
  preveMd5 = currentMd5
  console.log(`${filename}文件发生更新`)
  // 执行逻辑
  run(`${entry}/${filename}`)
})

完成,只变动一次,但对于打代码极快的我,几秒操作可能执行很多次run,于是上debounce

let timer = null
fs.watch(entry, {
  recursive: true, // 递归
}, (event, filename) => {
  if (event !== 'change') return
  if(!filename) return
  if(timer) return
  timer = setTimoue(() => timer = null, 1000)
  let currentMd5 = md5(fs.readFileSync(`${entry}/${filename}`))
  if (currentMd5 == preveMd5) {
     return
  }
  preveMd5 = currentMd5
  console.log(`${filename}文件发生更新`)
  // 执行逻辑
  run(`${entry}/${filename}`)
})

自定义watch逻辑完成!

生成逻辑改动

因为逻辑从onDevCompileDone转移到onStart,同时我想将其单独
封装,于是改主体逻辑

const createCode = ({
   entry,
   output,
   increment,
   word,
}) = > {
  ...
  run()  // 第一次执行时调用
  return {
    run // 将run返回,方便多次调用
  }
}

在onStart里加上

api.onStart(() => {
  ...
  const { run } = createCode({
    entry,
    output,
    increment,
    word,
  })
  ...
  fs.watch...
})

细节改动

因为watch已经告诉我们是什么文件变化了,我们不用执行fileDisplay(递归文件夹),只需直接走readFileToObj(写入逻辑)就好了,run和readFileToObj也需要改动

// 开始逻辑, 有filename参数直接去写入json逻辑
function run(filename) {
  ...
  then(value => {
    if(filename){
       readFileToObj(filename, value, null, true)
    }
    // 要不就走递归,全部文件访问
    fileDisplay(filePath, value, function (value) {
      console.log('finish:', Date.now() - startTime)
    })
  })
}
// 写入逻辑,多了个alone参数,直接写入
function readFileToObj(fReadName, value, callback, alone = false) {
    ...
     objReadline.on('close', () => {
      // 文件都读过了,写进生成文件
      if (--readNum === 0 || alone) {
        let result = JSON.stringify(obj, null, 2)
        fs.writeFile(output, result, err => {
          if (err) {
            console.warn(err)
          }
        })
        callback && callback()
      }
    })   
}

完成!基本没毛病,贴上完整代码

const fs = require('fs')
const md5 = require('md5')
const readline = require('readline')
const path = require('path')
let preveMd5 = null

const createCode = ({
  entry,
  output,
  increment,
  word
}) => {
  const obj = {}
  const separator = `${word}[` // 分隔符
  const suffix = ['.js', '.jsx'] // 后缀白名单
  let readNum = 0 // 计数还剩未读的文件数
  console.log('-----start-----')

  // 写入逻辑,多了个alone参数,直接写入
  function readFileToObj(fReadName, value, callback, alone = false) {
    var fRead = fs.createReadStream(fReadName)
    var objReadline = readline.createInterface({
      input: fRead,
    });
    objReadline.on('line', line => {
      // 注释的忽略
      if (line.includes('//') || line.includes('*')) {
        return
      }
      if (line) {
        const arr = line.split(separator)
        if (arr.length > 1) {
          const bb = arr.slice(1)
          for (let i in bb) {
            const v0 = bb[i].split(']')[0]
            const v = v0.substr(1, v0.length - 2)
            if (!v) {
              // 空输出提示
              console.warn(`空行为:${line}`)
              continue
            }
            // 增量就不覆盖了
            if (increment && value && value[v]) {
              obj[v] = value[v]
            } else {
              obj[v] = v
            }
  
          }
        }
      }
    })
    objReadline.on('close', () => {
      // 文件都读过了,写进生成文件
      if (--readNum === 0 || alone) {
        let result = JSON.stringify(obj, null, 2)
        fs.writeFile(output, result, err => {
          if (err) {
            console.warn(err)
          }
        })
        callback && callback()
      }
    })
  }
  
  
  const filePath = path.resolve(entry)
  
  // 递归执行,直到判断是文件就执行readFileToObj
  function fileDisplay(filePath, value, callback) {
    fs.readdir(filePath, (err, files) => {
      let count = 0
      function checkEnd() {
        if (++count === files.length && callback) {
          callback()
        }
      }
      if (err) {
        console.warn(err)
      } else {
        files.forEach(filename => {
          var fileDir = path.join(filePath, filename)
          fs.stat(fileDir, (err2, status) => {
            if (err2) {
              console.warn(err2)
            } else {
              if (status.isDirectory()) {
                return fileDisplay(fileDir, value, checkEnd)
              }
              else if (status.isFile()) {
                // 后缀不符合的跳过,并计数加一
                if (suffix.includes(path.extname(fileDir))) {               
                  readNum++
                  readFileToObj(fileDir, value)
                }
              }
              checkEnd()
            }
          })
        })
      }
    })
  }
  
  
  // 开始逻辑, 有filename参数直接去写入json逻辑
  function run(filename) {
    new Promise((resolve, reject) => {
      fs.exists(output, exists => {
        // 存在且增量生成
        if (exists && increment) {
          console.log('增量更新')
          fs.readFile(output, 'utf-8', (err, data) => {
            if (err) {
              console.warn(err)
            } else {
              try {
                // 旧文件已存在的json
                const json = JSON.parse(data)
                resolve(json)
              } catch (e) {
                // 翻车
                console.warn(e)
              }
            }
          })
        } else {
          console.log('全量更新')
          resolve()
        }
      })
    }).then(value => {
      let startTime = Date.now()
      if(filename) {
        readFileToObj(filename, value, null, true)
        return
      }
      // 要不就走递归,全部文件访问
      fileDisplay(filePath, value, function (value) {
        console.log('finish:', Date.now() - startTime)
      })
    })
  }
  run()
  return {
    run
  }
}



export default (api, {
  entry = './src',
  output = './lang.json',
  increment = true,
  word = 'lang'
} = {}) => {
  api.onStart(() => {
    if (!output) {
      throw new Error('output必填')
    }
    const { run } = createCode({
      entry,
      output,
      increment,
      word
    })
    let timer = null
    fs.watch(entry, {
      recursive: true
    }, (event, filename) => {
      if (event !== 'change') return
      if(!filename) return
      if(filename.indexOf('.umi') > -1) return
      if(timer) return
      timer = setTimeout(() => timer = null, 1000)
      let currentMd5 = md5(fs.readFileSync(`${entry}/${filename}`))
      if (currentMd5 == preveMd5) {
          return
      }
      preveMd5 = currentMd5
      console.log(`${filename}文件发生更新`)
      run(`${entry}/${filename}`)
    });
   
  })
 
  api.onDevCompileDone(({ stats }) => {
    
  });
};

改进后对比

递归只在onStart执行了一次,后续开发只根据变动文件做增量更新,大大减少了没必要的访问成本

思考

原本是想将watch逻辑放在外面脚本,在onStart利用child_process启动脚本,无奈并没触发到理想的效果,希望有知道的哥们指点一下

你可能感兴趣的:(最懒的前端多语言策略(三))