IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> JavaScript知识库 -> 前端脚手架开发工具包 -> 正文阅读

[JavaScript知识库]前端脚手架开发工具包

文件操作

fs-extra

fs-extra是fs的一个扩展,提供了非常多的便利API,并且继承了fs所有方法和为fs方法添加了promise的支持。

npm install --save-dev fs-extra

应该总是fs-extra代替fs使用,所有fs方法都附在fs-extra,fs如果未传递回调,则所有方法都将返回promise。

大多数方法默认为异步,如果未传递回调,则所有异步方法将返回一个promise。

const fs = require('fs-extra')

// 异步方法,返回promise
fs.copy('/tmp/myfile', '/tmp/mynewfile')
  .then(() => console.log('success!'))
  .catch(err => console.error(err))

// 异步方法,回调函数
fs.copy('/tmp/myfile', '/tmp/mynewfile', err => {
  if (err) return console.error(err)
  console.log('success!')
})

// 同步方法,注意必须使用try catch包裹着才能捕获错误
try {
  fs.copySync('/tmp/myfile', '/tmp/mynewfile')
  console.log('success!')
} catch (err) {
  console.error(err)
}

// Async/Await:
async function copyFiles () {
  try {
    await fs.copy('/tmp/myfile', '/tmp/mynewfile')
    console.log('success!')
  } catch (err) {
    console.error(err)
  }
}

copyFiles()

readdirp

fs.readdir 的递归版本,对外暴露 stream api

var readdirp = require('readdirp'),
  path = require('path'),
  es = require('event-stream');

// print out all JavaScript files along with their size
var stream = readdirp({ root: path.join(__dirname), fileFilter: '*.js' });

stream
  .on('warn', function (err) {
    console.error('non-fatal error', err);

    // optionally call stream.destroy() here in order to abort and cause 'close' to be emitted
  })
  .on('error', function (err) {
    console.error('fatal error', err);
  })
  .pipe(
    es.mapSync(function (entry) {
      return { path: entry.path, size: entry.stat.size };
    })
  )
  .pipe(es.stringify())
  .pipe(process.stdout);

filefilter 函数过滤

  fileFilter(entry) {
    return /\.html?$/.test(entry.basename)
  }

entry 结果

  parentDir     :  'test/bed/root_dir1',
  fullParentDir :  '/User/dev/readdirp/test/bed/root_dir1',
  name          :  'root_dir1_subdir1',
  path          :  'test/bed/root_dir1/root_dir1_subdir1',
  fullPath      :  '/User/dev/readdirp/test/bed/root_dir1/root_dir1_subdir1',
  stat          :  [ ... ]

event-stream

Streams是 node 最好的也是最容易被误解的流处理工具,EventStream 是一个工具包,可以让创建和使用流变得容易。

所有event-stream函数都返回Stream.

//pretty.js
if (!module.parent) {
  var es = require('event-stream');
  var inspect = require('util').inspect;

  process.stdin //connect streams together with `pipe`
    .pipe(es.split()) //split stream to break on newlines
    .pipe(
      es.map(function (data, cb) {
        //turn this async function into a stream
        cb(null, inspect(JSON.parse(data))); //render it nicely
      })
    )
    .pipe(process.stdout); // pipe it to stdout !
}

文件匹配

glob

npm i glob
var glob = require("glob")

// options is optional
glob("**/*.js", options, function (er, files) {
  // files is an array of filenames.
  // If the `nonull` option is set, and nothing
  // was found, then files is ["**/*.js"]
  // er is an error object or null.
})

远程下载模板代码

download-git-repo

Download and extract a git repository (GitHub, GitLab, Bitbucket) from node.

$ npm install download-git-repo
download('gitlab:mygitlab.com:flippidippi/download-git-repo-fixture#my-branch', 'test/tmp', { headers: { 'PRIVATE-TOKEN': '1234' } } function (err) {
	console.log(err ? 'Error' : 'Success')
})

也可以直接使用 git clone 的方式

命令行参数解析

minimist轻量级的命令行参数解析引擎

node.js的命令行参数解析工具有很多,比如:argparse、optimist、yars、commander。optimist和yargs内部使用的解析引擎正是minimist,如果你喜欢轻量级的技术,那么minimist足够简单好用,代码量也很少(只有几百行),非常适合研读。

minimist的特性比较全面:

  • short options
  • long options
  • Boolean 和 Number类型的自动转化
  • option alias
// test.js
var args = require('minimist')(process.argv.slice(2));

console.log(args.hello);
$ node test.js --hello=world
// world
$ node test.js --hello world
// world
$ node test.js --hello
// true 注意:不是空字符串而是true
?  node git:(main) ? node ./minimist.js start        
args:  { _: [ 'start' ] }
?  node git:(main) ? node ./minimist.js --start
args:  { _: [], start: true }
?  node git:(main) ? 

Commander

可以自动解析命令和参数,用于处理用户输入的命令,合并多选项,处理短参等等

npm install commander
const { program } = require('commander');

program
  .option('-d, --debug', 'output extra debugging')
  .option('-s, --small', 'small pizza size')
  .option('-p, --pizza-type <type>', 'flavour of pizza');

program.parse(process.argv);

const options = program.opts();
if (options.debug) console.log(options);
console.log('pizza details:');
if (options.small) console.log('- small pizza size');
if (options.pizzaType) console.log(`- ${options.pizzaType}`);
$ pizza-options -p
error: option '-p, --pizza-type <type>' argument missing
$ pizza-options -d -s -p vegetarian
{ debug: true, small: true, pizzaType: 'vegetarian' }
pizza details:
- small pizza size
- vegetarian
$ pizza-options --pizza-type=cheese
pizza details:
- cheese

通过 program.parse(arguments) 方法处理参数,没有被使用的选项会存放在 program.args 数组中。该方法的参数是可选的,默认值为 process.argv

CLI工具更新通知

update-notifier

控制台展示升级提醒

$ npm install update-notifier

Simple

const updateNotifier = require('update-notifier');
const pkg = require('./package.json');

updateNotifier({pkg}).notify();

Comprehensive

const updateNotifier = require('update-notifier');
const pkg = require('./package.json');

// Checks for available update and returns an instance
const notifier = updateNotifier({pkg});

// Notify using the built-in convenience method
notifier.notify();

// `notifier.update` contains some useful info about the update
console.log(notifier.update);
/*
{
 latest: '1.0.1',
 current: '1.0.0',
 type: 'patch', // Possible values: latest, major, minor, patch, prerelease, build
 name: 'pageres'
}
*/

日志打印

debug

一个模仿 Node.js 核心调试技术的小型 JavaScript 调试实用程序。适用于 Node.js 和 Web 浏览器。

$ npm install -D debug

使用案例:

var debug = require('debug')('http')
  , http = require('http')
  , name = 'My App';

// fake app

debug('booting %o', name);

http.createServer(function(req, res){
  debug(req.method + ' ' + req.url);
  res.end('hello\n');
}).listen(3000, function(){
  debug('listening');
});

运行结果:
运行效果图

chalk-终端字符串区分展示颜色

Highlights

npm install chalk

IMPORTANT: Chalk 5 is ESM. If you want to use Chalk with TypeScript or a build tool, you will probably want to use Chalk 4 for now. Read more.

import chalk from 'chalk';

console.log(chalk.blue('Hello world!'));

用户交互

inquirer

通用的命令行用户界面集合,用于和用户进行交互

npm install inquirer
var inquirer = require('inquirer');

inquirer
  .prompt([
    /* Pass your questions in here */
  ])
  .then((answers) => {
    // Use user feedback for... whatever!!
  })
  .catch((error) => {
    if (error.isTtyError) {
      // Prompt couldn't be rendered in the current environment
    } else {
      // Something else went wrong
    }
  });

脚本执行

execa

// 老版本 execa
const execa = require('execa')

module.exports = async function(command, options = {}) {
  if (typeof options === 'string') {
    options = {
      cwd: options
    }
  }
  if (/^c?npm outdated .*$/.test(command)) {
    let result
    try {
      result = execa.shellSync(command, {
        cwd: process.cwd(),
        stdio: 'inherit',
        ...options
      })
    } catch (e) {
      result = e
    }
    return Promise.resolve(result)
  } else {
	// 新版本 execa.commandSync API 
    return await execa.shellSync(command, {
      cwd: process.cwd(),
      stdio: 'inherit',
      ...options
    })
  }
}

这个包改进了child_process方法:

  • Promise接口
  • 从输出中删除最后的换行符,这样您就不必执行stdout.trim()
  • 支持跨平台的shebang二进制文件
  • 改进Windows支持。
  • 更高的最大缓冲区。100mb而不是200kb。
  • 按名称执行本地安装的二进制文件。
  • 在父进程终止时清除派生的进程。
  • stdoutstderr获得交错输出,类似于在终端上打印的输出。(异步)
  • 可以指定文件和参数作为一个单一的字符串没有外壳
  • 更具描述性的错误。
npm install execa
import {execa} from 'execa';

const {stdout} = await execa('echo', ['unicorns']);
console.log(stdout);
//=> 'unicorns'

基本api及用法:

  1. execa(file, arguments?, options?)
const { stdout } = await execa('git', ['status']);
// 指定执行目录同样是传入cwd或者process.chdir();
const { stdout } = await execa('git', ['status'], {cwd: resolve('../demo')});
复制代码
  1. execa.sync(file, arguments?, options?):同步执行文件
  2. execa.command(command, options?): 与execa()相同,只是文件和参数都在单个命令字符串中指定
execa.command('git status');
  1. execa.commandSync(command, options?):与 execa.command()相同,但是是同步的。

shelljs

$ npm install  shelljs -D
let shell = require('shelljs')
let name = process.argv[2] || 'Auto-commit';
let exec = shell.exec

if (exec('git add .').code !== 0) {
    echo('Error: Git add failed')
    exit(1)
}
if (exec(`git commit -am "${name}"`).code !== 0) {
    echo('Error: Git commit failed')
    exit(1)
}
if (exec('git push').code !== 0) {
    echo('Error: Git commit failed')
    exit(1)
}

持久化存储

configstore

轻松加载和持久化配置,无需考虑存储位置和方式

$ npm install configstore
import Configstore from 'configstore';

const packageJson = JSON.parse(fs.readFileSync('./package.json', 'utf8'));

// Create a Configstore instance.
const config = new Configstore(packageJson.name, {foo: 'bar'});

console.log(config.get('foo'));
//=> 'bar'

config.set('awesome', true);
console.log(config.get('awesome'));
//=> true

// Use dot-notation to access nested properties.
config.set('bar.baz', true);
console.log(config.get('bar'));
//=> {baz: true}

config.delete('awesome');
console.log(config.get('awesome'));
//=> undefined

conf

允许我们在用户的机器上保存持久的信息

$ npm install conf
const Conf = require('conf');

const config = new Conf();

config.set('unicorn', '🦄');
console.log(config.get('unicorn'));
//=> '🦄'

// Use dot-notation to access nested properties
config.set('foo.bar', true);
console.log(config.get('foo'));
//=> {bar: true}

config.delete('unicorn');
console.log(config.get('unicorn'));
//=> undefined

自行简易实现

const path = require('path')
const fs = require('fs-extra')
const cacheFileName = 'index.json'
const cacheFilePath = path.join(process.cwd(), 'node_modules/.cache/test')
const filePath = path.join(cacheFilePath, cacheFileName)

function save(data={}) {
  fs.ensureDirSync(cacheFilePath)
  fs.writeJsonSync(filePath, data)
}

function load() {
  try {
    return require(filePath)
  } catch(e) {
    save({})
    return {}
  }
}


// data {}
exports.set = function(key, data) {
  let localData = load()
  localData[key] = data
  save(localData)
}

exports.get = function(key) {
  let data = load()
  return data[key]
}

环境变量配置

手动配置

// Do this as the first thing so that any code reading it knows the right env.
process.env.BABEL_ENV = 'production';
process.env.NODE_ENV = 'production';

简易封装

const enmu = {
  source: 'source',
  test: 'test',
  development: 'development',
  production: 'production',
}

// 在 env 下挂载一个 mode 变量,使用该变量作为全局环境的唯一表示
module.exports = {
  setProcessMode(name) {
    let result = enmu[name]
    if (result) {
      let arr = process.env.mode ? process.env.mode.split(',') : []
      arr.push(name)
      process.env.mode = [...new Set(arr)]
    } else {
      throw new Error('没有预置此字段')
    }
  },
  isTest() {
    return process.env.mode ? process.env.mode.split(',').includes('test') : false
  },
  isPrd() {
    return process.env.mode ? process.env.mode.split(',').includes('production') : false
  },
  isDev() {
    return process.env.mode ? process.env.mode.split(',').includes('development') : false
  }
}

接入第三方 dotenv

Dotenv 是一个零依赖模块,它将环境变量从.env文件加载到process.env

npm install dotenv --save

在项目的根目录创建一个 .env 文件

S3_BUCKET="YOURS3BUCKET"
SECRET_KEY="YOURSECRETKEYGOESHERE"

在项目中尽可能早的配置dotenv

require('dotenv').config()
console.log(process.env) // remove this after you've confirmed it working

扩展环境变量 dotenv-expand

dotenv-expand 在 dotenv之上添加变量扩展,扩展计算机上已经存在的环境变量

# Install locally (recommended)
npm install dotenv-expand --save
const appDirectory = fs.realpathSync(process.cwd());
const resolveApp = relativePath => path.resolve(appDirectory, relativePath);

const dotenv = resolveApp('.env'),

// https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use
const dotenvFiles = [
  `${paths.dotenv}.${NODE_ENV}.local`,
  // Don't include `.env.local` for `test` environment
  // since normally you expect tests to produce the same
  // results for everyone
  NODE_ENV !== 'test' && `${paths.dotenv}.local`,
  `${paths.dotenv}.${NODE_ENV}`,
  paths.dotenv,
].filter(Boolean);

// Load environment variables from .env* files. Suppress warnings using silent
// if this file is missing. dotenv will never modify any environment variables
// that have already been set.  Variable expansion is supported in .env files.
// https://github.com/motdotla/dotenv
// https://github.com/motdotla/dotenv-expand
dotenvFiles.forEach(dotenvFile => {
  if (fs.existsSync(dotenvFile)) {
    require('dotenv-expand')(
      require('dotenv').config({
        path: dotenvFile,
      })
    );
  }
});

端口检测

detect-port-alt

使用npm包detect-port-alt

$ npm i detect-port --save

使用案例:

const detect = require('detect-port');

/**
 * callback usage
 */

detect(port, (err, _port) => {
  if (err) {
    console.log(err);
  }

  if (port == _port) {
    console.log(`port: ${port} was not occupied`);
  } else {
    console.log(`port: ${port} was occupied, try port: ${_port}`);
  }
});

/**
 * for a yield syntax instead of callback function implement
 */

const co = require('co');

co(function* () {
  const _port = yield detect(port);

  if (port == _port) {
    console.log(`port: ${port} was not occupied`);
  } else {
    console.log(`port: ${port} was occupied, try port: ${_port}`);
  }
});

/**
 * use as a promise
 */

detect(port)
  .then((_port) => {
    if (port == _port) {
      console.log(`port: ${port} was not occupied`);
    } else {
      console.log(`port: ${port} was occupied, try port: ${_port}`);
    }
  })
  .catch((err) => {
    console.log(err);
  });

选择可用端口方法

function choosePort(host, defaultPort) {
  return detect(defaultPort, host).then(
    port =>
      new Promise(resolve => {
        if (port === defaultPort) {
          return resolve(port);
        }
        const message =
          process.platform !== 'win32' && defaultPort < 1024 && !isRoot()
            ? `Admin permissions are required to run a server on a port below 1024.`
            : `Something is already running on port ${defaultPort}.`;
        // const isInteractive = process.stdout.isTTY;
        if (isInteractive) {
          clearConsole();
          const existingProcess = getProcessForPort(defaultPort);
          const question = {
            type: 'confirm',
            name: 'shouldChangePort',
            message:
              chalk.yellow(
                message +
                  `${existingProcess ? ` Probably:\n  ${existingProcess}` : ''}`
              ) + '\n\nWould you like to run the app on another port instead?',
            initial: true,
          };
          prompts(question).then(answer => {
            if (answer.shouldChangePort) {
              resolve(port);
            } else {
              resolve(null);
            }
          });
        } else {
          console.log(chalk.red(message));
          resolve(null);
        }
      }),
    err => {
      throw new Error(
        chalk.red(`Could not find an open port at ${chalk.bold(host)}.`) +
          '\n' +
          ('Network error message: ' + err.message || err) +
          '\n'
      );
    }
  );
}

detect-port-alt 实现原理

[[debug包打印指定模块的日志信息]]

'use strict';

// 打印 detect-port 相关的日志信息
const debug = require('debug')('detect-port');
// 基于 net 做监听以检测端口是否可用
const net = require('net');
// 用户获取 IP 地址
const address = require('address');

module.exports = (port, host, callback) => {
  if (typeof port === 'function') {
    callback = port;
    port = null;
  } else if (typeof host === 'function') {
    callback = host;
    host = null;
  }
  port = parseInt(port) || 0;
  let maxPort = port + 10;
  if (maxPort > 65535) {
    maxPort = 65535;
  }
  debug('detect free port between [%s, %s)', port, maxPort);
  if (typeof callback === 'function') {
    return tryListen(host, port, maxPort, callback);
  }
  // promise
  return new Promise((resolve, reject) => {
    tryListen(host, port, maxPort, (error, realPort) => {
      if (error) {
        reject(error);
      } else {
        resolve(realPort);
      }
    });
  });
};

function tryListen(host, port, maxPort, callback) {
  function handleError() {
    port++;
    if (port >= maxPort) {
      debug(
        'port: %s >= maxPort: %s, give up and use random port',
        port,
        maxPort
      );
      port = 0;
      maxPort = 0;
    }
    tryListen(host, port, maxPort, callback);
  }

  // 1. check specified host (or null)
  listen(port, host, (err, realPort) => {
    // ignore random listening
    if (port === 0) {
      return callback(err, realPort);
    }

    if (err) {
      return handleError(err);
    }

    // 2. check default host
    listen(port, null, err => {
      if (err) {
        return handleError(err);
      }

      // 3. check localhost
      listen(port, 'localhost', err => {
        if (err) {
          return handleError(err);
        }

        // 4. check current ip
        let ip;
        try {
          ip = address.ip();
        } catch (err) {
          // Skip the `ip` check if `address.ip()` fails
          return callback(null, realPort);
        }

        listen(port, ip, (err, realPort) => {
          if (err) {
            return handleError(err);
          }

          callback(null, realPort);
        });
      });
    });
  });
}

function listen(port, hostname, callback) {
  const server = new net.Server();

  server.on('error', err => {
    debug('listen %s:%s error: %s', hostname, port, err);
    server.close();
    if (err.code === 'ENOTFOUND') {
      debug('ignore dns ENOTFOUND error, get free %s:%s', hostname, port);
      return callback(null, port);
    }
    return callback(err);
  });

  server.listen(port, hostname, () => {
    port = server.address().port;
    server.close();
    debug('get free %s:%s', hostname, port);
    return callback(null, port);
  });
}

portfinder

自动寻找 800065535内可用端口号

Loading展示

ora

终端加载器

npm install ora
import ora from 'ora';

const spinner = ora('Loading unicorns').start();

setTimeout(() => {
	spinner.color = 'yellow';
	spinner.text = 'Loading rainbows';
}, 1000);

打开本地App

open

打开诸如 URL、文件、可执行文件之类的东西,跨平台。

$ npm install open
const open = require('open');

// Opens the image in the default image viewer and waits for the opened app to quit.
await open('unicorn.png', {wait: true});
console.log('The image viewer app quit');

// Opens the URL in the default browser.
await open('https://sindresorhus.com');

// Opens the URL in a specified browser.
await open('https://sindresorhus.com', {app: {name: 'firefox'}});

// Specify app arguments.
await open('https://sindresorhus.com', {app: {name: 'google chrome', arguments: ['--incognito']}});

// Open an app
await open.openApp('xcode');

// Open an app with arguments
await open.openApp(open.apps.chrome, {arguments: ['--incognito']});

better-opn

根据匹配的url,重用浏览器的tab页面

$ npm install better-opn
const opn = require('better-opn');

opn('http://localhost:3000');

压缩包

zip-dir

创建zip压缩包

$ npm install zip-dir
var zipdir = require('zip-dir');

// `buffer` is the buffer of the zipped file

var buffer = await zipdir('/path/to/be/zipped');

zipdir('/path/to/be/zipped', function (err, buffer) {

 // `buffer` is the buffer of the zipped file

});

zipdir('/path/to/be/zipped', { saveTo: '~/myzip.zip' }, function (err, buffer) {

 // `buffer` is the buffer of the zipped file

 // And the buffer was saved to `~/myzip.zip`

});

// Use a filter option to prevent zipping other zip files!

// Keep in mind you have to allow a directory to descend into!

zipdir('/path/to/be/zipped', { filter: (path, stat) => !/\.zip$/.test(path) }, function (err, buffer) {

});

// Use an `each` option to call a function everytime a file is added, and receives the path

zipdir('/path/to/be/zipped', { each: path => console.log(p, "added!"), function (err, buffer) {

});

工具方法

清空控制台

/**
 * Copyright (c) 2015-present, Facebook, Inc.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

'use strict';

function clearConsole() {
  process.stdout.write(
    process.platform === 'win32' ? '\x1B[2J\x1B[0f' : '\x1B[2J\x1B[3J\x1B[H'
  );
}

module.exports = clearConsole;

配置对象合并

function overwrite(obj1, obj2) {
  const func = (o1={}, o2, key) => {
    if(!key){
      Object.keys(o2).forEach((v) => func(o1, o2[v], v))      
    } else if (toString.call(o2) === '[object Object]') {
      // 对象进行递归处理
      o1[key] = o1[key] || {}
      Object.keys(o2).forEach((v) => {
        func(o1[key], o2[v], v)
      })      
    } else if (typeof o2 === 'function') {
	  // 函数接受初始值,处理后返回新的值
      o1[key] = o2(o1)
    } else if (Array.isArray(o2)) {
	  // 数组进行合并
      o1[key] = o1[key] || []
      o1[key] = [...new Set(o1[key].concat(o2))]    
    } else {
      // 值直接替换
      o1[key] = o2
    }
    return o1
  }
  return func(obj1, obj2)
}

也可以借助

  1. deepmerge
  2. webpack-merge
  3. extend
  JavaScript知识库 最新文章
ES6的相关知识点
react 函数式组件 & react其他一些总结
Vue基础超详细
前端JS也可以连点成线(Vue中运用 AntVG6)
Vue事件处理的基本使用
Vue后台项目的记录 (一)
前后端分离vue跨域,devServer配置proxy代理
TypeScript
初识vuex
vue项目安装包指令收集
上一篇文章      下一篇文章      查看所有文章
加:2022-06-06 17:16:21  更:2022-06-06 17:17:48 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/23 16:55:01-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码