热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

从eggbin聊到commandlineinterfaceTool

最近正在看一些关于egg方面的东西,其中对于egg的运行方式是基于egg-bin来处理的,正好可以借此机会通过egg-bin来了解egg的运行过程以及e
最近正在看一些关于 egg 方面的东西,其中对于 egg 的运行方式是基于 egg-bin 来处理的,正好可以借此机会通过 egg-bin 来了解 egg 的运行过程以及 egg-bin 在其他场景下的作用。而 egg-bin 是基于 common-bin(封装的 cli 开发工具)开发的,其中对于 node cli 工具的开发方式也颇有启发,一并进行下相关方面的学习。

关于 node 的命令行程序已经屡见不鲜了,譬如经常使用到的 npmwebpackcreate-react-appyarn 等等,虽然都作为辅助工具使用,但对于各种使用场景都可以说不可或缺,也大大提高了开发中的效率。众所周知其实我们在这些程序中跑的每个指令不过就是一个封装好功能的脚本罢了,其原理其实没有什么好提的,但如果想要开发一个也已应用于指定场景的 cli 工具还是有一些方面需要注意的,本文选用了egg-bin 来进行具体分析,其中 egg-bin 是一个便捷开发者在本地开发、调试、测试 egg 的命令行开发工具,集成了本地调试、单元测试和代码覆盖率等功能,最后会指出一些在开发 cli 工具的一些常用操作。

概览

egg-bin 基于抽象命令行工具 common-bin ,一个抽象封装了诸如 yargs、co 模块,并提供对于 async/generator 特性的支持,内置了 helper、subcommand 等实用功能,也算是五脏俱全了,凭借这些封装可以以及对于 cli 文件结构的约定,可以大大简化一个 node 工具的开发流程。

  1. 基于 common-bin (在 yargs 上抽象封装的 node 命令行工具,支持 async/generator 特性)
  2. 包含 CovCommand 代码覆盖率命令、DebugCommand 本地调试命令、DevCommand 本地开发命令、PkgfilesCommand package.json 文件编辑、TestCommand 测试命令

其文件结构如下:

├── bin
│ └── egg-bin.js
├── lib
│ ├── cmd
│ │ ├── cov.js
│ │ ├── debug.js
│ │ ├── dev.js
│ │ ├── pkgfiles.js
│ │ └── test.js
│ ├── command.js
│ ├── mocha-clean.js
│ └── start-cluster
├── index.js
└── package.json

在入口 index.js 文件中构造了 EggBin 对象,并将 cmd 文件夹下的命令自动挂载到实例对象下面

class EggBin extends Command {constructor(rawArgv) {super(rawArgv);this.usage = 'Usage: egg-bin [command] [options]';// load directorythis.load(path.join(__dirname, 'lib/cmd'));}
}

接着通过执行 Command-binstart() 方法,完成构造函数的内容,实际上则是启动 yargs 实例进程,并检查 load 的子命令,将所有命令统一生成一个 map 集合,并在 yargs 上注册,先看构造阶段都做了些什么事:

  • load 子命令配置文件,自动注册所有该文件夹下的子命令

load(fullPath) {// load entire directoryconst files = fs.readdirSync(fullPath);const names = [];for (const file of files) {if (path.extname(file) === '.js') {const name = path.basename(file).replace(/\.js$/, '');names.push(name);this.add(name, path.join(fullPath, file));}}}

找到的所有 files 有 'autod.js', 'cov.js', 'debug.js', 'dev.js', 'pkgfiles.js', 'test.js', 通过遍历所有的 files ,并进行 addCommand 操作,

add(name, target) {assert(name, `${name} is required`);if (!(target.prototype instanceof CommonBin)) {assert(fs.existsSync(target) && fs.statSync(target).isFile(), `${target} is not a file.`);target = require(target);assert(target.prototype instanceof CommonBin,'command class should be sub class of common-bin');}this[COMMANDS].set(name, target);}

最后可以看到生成了实例化后的 Command 集合

在完成了构造阶段的所有工作后,才开始执行 start() 内的内容,start()里面主要是使用co包了一个generator函数,并且在generator函数中执行了this[DISPATCH],实际上的工作都是在这其中完成的。

* [DISPATCH]() {// 执行 yargs 中的方法this.yargs.completion().help().version().wrap(120).alias('h', 'help').alias('v', 'version').group([ 'help', 'version' ], 'Global Options:');// 检查是否存在该子命令, 存在递归判断是否存在子命令if (this[COMMANDS].has(commandName)) {const Command = this[COMMANDS].get(commandName);const rawArgv = this.rawArgv.slice();rawArgv.splice(rawArgv.indexOf(commandName), 1);const command = new Command(rawArgv);yield command[DISPATCH]();return;}// 不存在指令, 则默认显示所有命令帮助信息for (const [ name, Command ] of this[COMMANDS].entries()) {this.yargs.command(name, Command.prototype.description || '');}const context = this.context;// print completion for bashif (context.argv.AUTO_COMPLETIONS) {// slice to remove `--AUTO_COMPLETIONS=` which we appendthis.yargs.getCompletion(this.rawArgv.slice(1), completions => {// console.log('%s', completions)completions.forEach(x => console.log(x));});} else {// handle by selfyield this.helper.callFn(this.run, [ context ], this);}}

首先会去执行yargs中一些方法,这里common-bin只是保留了yargs中一些对自己有用的方法,比如completion()、wrap()、alias()等. 接着会对获取到的命令进行校验,如果存在this[COMMAND]对象中就递归判断是否存在子命令。在当前例子中也就是去执行DevCommand, 而由于DevCommand最终也是继承于common-bin的,然后执行 yield command[DISPATCH](); 接着开始递归执行this[DISPATCH]了,直到所有的子命令递归完毕,才会去使用helper(common-bin中支持异步的关键所在)类继续执行指定 command 文件中的* run()函数 ,执行脚本操作( 自动注入了 context 实例对象 { cwd, env, argv, rawArgv } 包含了当前操作路径、操作环境信息、处理前后的参数)。

主要功能概览

DEV 多 cluster 服务的启动过程

首先我们打开 DEBUG 信息并启动一个 port 为 7003,cluster 数为3个的 egg 服务, 看启用服务的实际执行路径:

$ DEBUG=egg-bin ./node_modules/.bin/egg-bin dev -p 7003 -c 3->
egg-bin detect available port +0msegg-bin use available port 7001 +18msegg-bin /Users/nickj/Desktop/Project/node/egg/egg-example/node_modules/egg-bin/lib/start-cluster ["{\"baseDir\":\"/Users/nickj/Desktop/Project/node/egg/egg-example\",\"workers\":1,\"framework\":\"/Users/nickj/Desktop/Project/node/egg/egg-example/node_modules/egg\"}"] [], "development" +1ms

注意到实际是执行 egg/bin/lib/start-cluster 脚本启动服务的。

通过 $ pstree -p 82541 查看启动服务占用的实际进程:

可以看到 egg-bin 已经顺利通过 egg-cluster 启动了一个 agent 进程和 三个 app_worker 子进程,通过结果我们也借此机会看看 egg-cluster 内部做了什么,以及 egg 运行时都做了什么。

  • egg-bin/lib/cmd/dev.js dev bin 发起点

yield this.helper.forkNode(this.serverBin, devArgs, options);-> this.serverBin = path.join(__dirname, '../start-cluster');


  • egg-bin/lib/start-cluster

#!/usr/bin/env node'use strict';const debug = require('debug')('egg-bin:start-cluster');
const options = JSON.parse(process.argv[2]);
debug('start cluster options: %j', options);
require(options.framework).startCluster(options);

startCluster 启动传入 baseDir 和 framework,Master 进程启动


这里我们用跟着代码执行的顺序,一步步来看服务启动内部的具体执行,已简化。

egg-cluster/index.js

/*** start egg app* @method Egg#startCluster* @param {Object} options {@link Master}* @param {Function} callback start success callback*/
exports.startCluster = function(options, callback) {new Master(options).ready(callback);
};

Master 会先 fork Agent Worker 守护进程

-> Master 得到 Agent Worker 启动成功的消息(IPC),使用 cluster fork 多个 App Worker 进程

  • App Worker 有多个进程,所以这几个进程是并行启动的,但执行逻辑是一致的
  • 单个 App Worker 和 Agent 类似,通过 framework 找到框架目录,实例化该框架的 Application 类
  • Application 找到 AppWorkerLoader,开始进行加载,顺序也是类似的,会异步等待,完成后通知 Master 启动完成

  • egg-cluster/lib/master.js

// Master 会先 fork Agent Worker 守护进程
detectPort((err, port) => {...this.options.clusterPort = port;this.forkAgentWorker();
});-> ./lib/agent_worker.js
agent.ready(err => {// don't send started message to master when start errorif (err) return;agent.removeListener('error', startErrorHandler);process.send({ action: 'agent-start', to: 'master' });
});// Master 得到 Agent Worker 启动成功的消息,使用 cluster fork App Worker 进程
this.once('agent-start', this.forkAppWorkers.bind(this));-> (forkAppWorkers)
cfork({exec: this.getAppWorkerFile(),args,silent: false,count: this.options.workers,// don't refork in local envrefork: this.isProduction,
});-> (getAppWorkerFile())
getAppWorkerFile() {return path.join(__dirname, 'app_worker.js');
}

  • egg-cluster/lib/app_worker.js

app.ready(startServer);->
function startServer(err) {...let server;if (options.https) {const httpsOptions = Object.assign({}, options.https, {key: fs.readFileSync(options.https.key),cert: fs.readFileSync(options.https.cert),});server = require('https').createServer(httpsOptions, app.callback());} else {server = require('http').createServer(app.callback());}// emit `server` event in appapp.emit('server', server);// sticky 模式:Master 负责统一监听对外端口,然后根据用户 ip 转发到固定的 Worker 子进程上,每个 Worker 自己启动了一个新的本地服务if (options.sticky) {server.listen(0, '127.0.0.1');// Listen to messages sent from the master. Ignore everything else.process.on('message', (message, connection) => {if (message !== 'sticky-session:connection') {return;}// Emulate a connection event on the server by emitting the// event with the connection the master sent us.server.emit('connection', connection);connection.resume();});} else { // 非 sticky 模式:每个 Worker 都统一启动服务监听外部端口if (listenConfig.path) {server.listen(listenConfig.path);} else {if (typeof port !== 'number') {consoleLogger.error('[app_worker] port should be number, but got %s(%s)', port, typeof port);exitProcess();return;}const args = [ port ];if (listenConfig.hostname) args.push(listenConfig.hostname);debug('listen options %s', args);server.listen(...args);}}
}

其中在每个 worker 中还实例化了 Application, 这里也算是 egg 服务启动时的实际入口配置文件了,
在实例化 application(options) 时,agent_worker 和多个 app_worker 进程就会执行 egg 模块下面的 load 逻辑,依次加载我们应用中 Plugin 插件、 extends 扩展内置对象、app 实例、service 服务层、中间件、controller 控制层、router 路由等,具体加载过程就不深入了。

const Application = require(options.framework).Application;
const app = new Application(options);

启动相关联节点

this.on('agent-start', this.onAgentStart.bind(this));-> this.logger.info('[master] agent_worker#%s:%s started (%sms)',this.agentWorker.id, this.agentWorker.pid, Date.now() - this.agentStartTime);this.ready(() => {this.logger.info('[master] %s started on %s (%sms)%s',frameworkPkg.name, this[APP_ADDRESS], Date.now() - startTime, stickyMsg);
}

Master 等待多个 App Worker 的成功消息后启动完成,能对外提供服务。

Debug

DebugCommand继承于 DevCommand,所以同样会启动 egg 服务,并通过实例化 InspectorProxy 进行 debug 操作。

* run(context) {const proxyPort = context.argv.proxy;context.argv.proxy = undefined;const eggArgs = yield this.formatArgs(context);...// start eggconst child = cp.fork(this.serverBin, eggArgs, options);// start debug proxyconst proxy = new InspectorProxy({ port: proxyPort });// proxy to new workerchild.on('message', msg => {if (msg && msg.action === 'debug' && msg.from === 'app') {const { debugPort, pid } = msg.data;debug(`recieve new worker#${pid} debugPort: ${debugPort}`);proxy.start({ debugPort }).then(() => {console.log(chalk.yellow(`Debug Proxy online, now you could attach to ${proxyPort} without worry about reload.`));if (newDebugger) console.log(chalk.yellow(`DevTools → ${proxy.url}`));});}});child.on('exit', () => proxy.end());}

关于 inspectProxy 主要任务就是会持续的监听调试进程上返回的 json 文件信息,监听间隔时间为 1000 ms。

watchingInspect(delay = 0) {clearTimeout(this.timeout);this.timeout = setTimeout(() => {urllib.request(`http://127.0.0.1:${this.debugPort}/json`, {dataType: 'json',}).then(({ data }) => {this.attach(data && data[0]);}).catch(e => {this.detach(e);});}, delay);}attach(data) {if (!this.attached) {this.log(`${this.debugPort} opened`);debug(`attached ${this.debugPort}: %O`, data);}this.attached = true;this.emit('attached', (this.inspectInfo = data));this.watchingInspect(1000);}

egg-bin 会智能选择调试协议,在 8.x 之后版本使用 Inspector Protocol 协议,低版本使用 Legacy Protocol.

Test

这个命令会自动执行 test 目录下的以 .test.js 结尾的文件,通过 mocha 跑编写的测试用例, egg-bin 会自动将内置的 Mocha、co-mocha、power-assert,nyc 等模块组合引入到测试脚本中,可以让我们聚焦精力在编写测试代码上,而不是纠结选择那些测试周边工具和模块。

* run(context) {const opt = {env: Object.assign({NODE_ENV: 'test',}, context.env),execArgv: context.execArgv,};const mochaFile = require.resolve('mocha/bin/_mocha');const testArgs = yield this.formatTestArgs(context);if (!testArgs) return;yield this.helper.forkNode(mochaFile, testArgs, opt);}

其中主要逻辑在 formatTestArgs 其中,会通过指令接收的条件动态将测试需要使用的库 push 到 requireArr 中:

formatTestArgs({ argv, debug }) {//省略// collect requirelet requireArr = testArgv.require || testArgv.r || [];/* istanbul ignore next */if (!Array.isArray(requireArr)) requireArr = [ requireArr ];// 清理 mocha 测试堆栈跟踪,堆栈跟踪充斥着各种帧, 你不想看到的, 像是从模块和 mocha 内部代码if (!testArgv.fullTrace) requireArr.unshift(require.resolve('../mocha-clean'));// 增加 mocha 对于 generator 的支持requireArr.push(require.resolve('co-mocha'));// 断言库if (requireArr.includes('intelli-espower-loader')) {console.warn('[egg-bin] don\'t need to manually require `intelli-espower-loader` anymore');} else {requireArr.push(require.resolve('intelli-espower-loader'));}testArgv.require = requireArr;// collect test fileslet files = testArgv._.slice();if (!files.length) {files = [ process.env.TESTS || 'test/**/*.test.js' ];}// 加载egg项目中除掉node_modules和fixtures里面的test文件files = globby.sync(files.concat('!test/**/{fixtures, node_modules}/**/*.test.js'));// auto add setup file as the first test file 进行测试前的初始化工作const setupFile = path.join(process.cwd(), 'test/.setup.js');if (fs.existsSync(setupFile)) {files.unshift(setupFile);}testArgv._ = files;// remove aliastestArgv.$0 = undefined;testArgv.r = undefined;testArgv.t = undefined;testArgv.g = undefined;return this.helper.unparseArgv(testArgv);}

Cov

CovCommand 命令继承于 TestCommand, 用来测试代码的测试覆盖率,内置了 nyc 来支持单元测试自动生成代码覆盖率报告。

* run(context) {const { cwd, argv, execArgv, env } = context;if (argv.prerequire) {env.EGG_BIN_PREREQUIRE = 'true';}delete argv.prerequire;// ignore coverageif (argv.x) {if (Array.isArray(argv.x)) {for (const exclude of argv.x) {this.addExclude(exclude);}} else {this.addExclude(argv.x);}argv.x = undefined;}const excludes = (process.env.COV_EXCLUDES && process.env.COV_EXCLUDES.split(',')) || [];for (const exclude of excludes) {this.addExclude(exclude);}const nycCli = require.resolve('nyc/bin/nyc.js');const coverageDir = path.join(cwd, 'coverage');yield rimraf(coverageDir);const outputDir = path.join(cwd, 'node_modules/.nyc_output');yield rimraf(outputDir);const opt = {cwd,execArgv,env: Object.assign({NODE_ENV: 'test',EGG_TYPESCRIPT: context.argv.typescript,}, env),};// save coverage-xxxx.json to $PWD/coverageconst covArgs = yield this.getCovArgs(context);if (!covArgs) return;debug('covArgs: %j', covArgs);yield this.helper.forkNode(nycCli, covArgs, opt);}

命令行常用操作

最后再总结一些常见的命令行开发操作,主要为获取用户输入的参数,文件路径判断,以及 fork 子进程执行命令等,比如如果要实现如下的的非常简单命令行功能。

$ cli # 结构
$ cli --name "CLI" # 示例

  • 全局化应用指令

在 npm 包中,我们可以通过 -g 指定咋全局安装一个模块,以 unix 环境为例,实际上就是将模块中指定在 package.json 中的 bin 内的脚本又在 usr/local/bin 创建了一份并与全局中 usr/local/lib/node_modules//bin/index.js 之间创建了一个连接,这样我们可以在全局任何位置下调用指定的 npm 包,具体方式只需要在 package.json 中定义将在可执行名称和目标执行文件 ,比如:

// package.json
"bin": {"cli": "index.js"
}

npm 中将 bin 指令与 node_modules 创建连接的相关代码:

var me = folder || npm.prefix
var target = path.resolve(npm.globalDir, d.name)
symlink(me, target, false, true, function (er) {if (er) return cb(er)log.verbose('link', 'build target', target)// also install missing dependencies.npm.commands.install(me, [], function (er) {if (er) return cb(er)// build the global stuff. Don't run *any* scripts, because// install command already will have done that.build([target], true, build._noLC, true, function (er) {if (er) return cb(er)resultPrinter(path.basename(me), me, target, cb)})})
})

只需要使用 #!/usr/bin/env node 告诉npm 该 js 文件是一个 node.js 可执行文件,Linux会自动使用node来运行该脚本,在本地下我们可以在根目录下执行 $ npm link,将模块安装在全局并生成连接:

#!/usr/bin/env node// index.js
var argv = require('yargs').option('name', {alias: 'n',demand: true,default: 'tom',describe: 'your name',type: 'string'}).help('h').usage('Usage: hello [options]').example('hello -n tom', 'say hello to Tom').argv;console.log(`say hello to ${argv.name}`);

$ cli -n Nick -> say hello to Nick

获取命令行参数

node 中原生支持的 process.argv 表示执行的脚本时同时传入的参数数组。而如果需要指定参数名或 alias,则需要通过第三方库实现,我们以 common-bin 封装的 yargs 进行分析。

通过 argv._ 可以获取到所有的非 options 的参数。所有 options 参数则挂载在 argv 对象下面。

当然强大还有一些强大第三方处理交互的包可以让我们处理更多不同的参数处理,提供了诸如选择器、autoComplate 输入、表单输入以及输入的校验等等,赋予 cli 工具更多的可能。

比如 enquirer 中的 autoComplete Promot

推荐,node cli 用户交互库

  • Enquirer 原生提供对于 async/await 的支持
  • Inquirer.js

子进程

有时候我们需要在程序中调用其他的 shell 命令,可以通过node 原生的 child_process 衍生子进程去执行,比如 common-bin 的应用方式,包括两种一个是 forkNode, 一个是 spawn ,主要区别就是前者将会衍生子进程执行路径指定文件,后者则是一个 shell 命令。

const cp = require('child_process');
exports.forkNode = (modulePath, args = [], options = {}) => {options.stdio = options.stdio || 'inherit';const proc = cp.fork(modulePath, args, options);gracefull(proc);return new Promise((resolve, reject) => {proc.once('exit', code => {childs.delete(proc);if (code !== 0) {const err = new Error(modulePath + ' ' + args + ' exit with code ' + code);err.code = code;reject(err);} else {resolve();}});});
};exports.spawn = (cmd, args = [], options = {}) => {options.stdio = options.stdio || 'inherit';return new Promise((resolve, reject) => {const proc = cp.spawn(cmd, args, options);gracefull(proc);proc.once('error', err => {/* istanbul ignore next */reject(err);});proc.once('exit', code => {childs.delete(proc);if (code !== 0) {return reject(new Error(`spawn ${cmd} ${args.join(' ')} fail, exit code: ${code}`));}resolve();});});
};

child_process.fork(): 衍生一个新的 Node.js 进程,并通过 IPC 通讯通道来调用指定的模块,该通道允许父进程与子进程之间相互发送信息。

一些文件操作

当我们使用CLI工具时,我们常常还需要一些对文件进行操作,需要注意的就是对于 cli 内部模块路径以及cli 被调用的路径的区分:

  • 获得 cli 内部文件所在路径 __dirname

获取 cli 内部文件所在路径,以处理 cli 内部文件操作。

  • 获得当前 cli 工具的调用路径 process.cwd()

获取当前 cli 工具被调用的路径,已处理一些对外的附加文件操作。

常见的做法是将 cwd 作为可选参数,默认指定当前位置为工作目录,所以我们可以从任何路径调用我们的 cli 工具,并将其设置为当前的工作目录。

const { join, resolve } = require('path')const cwd = resolve(yargs.argv.cwd || process.cwd())
process.chdir(cwd);yargs.help().options({ cwd: { desc: 'Change the current working directory' } }).demand(1).argv

参考

  • 结合源码解密 egg 运行原理

常用第三方包

  • osenv 方便的获取不同系统的环境和目录配置
  • figlet 命令行炫酷的Logo生成器
  • meow 命令行帮助命令封装
  • inquire 强大的用户交互
  • chalk 让命令行的output带有颜色
  • shelljs Node.js执行shell命令
  • clui 进度条
  • ora 加载状态



推荐阅读
  • 利用Node.js实现PSD文件的高效切图
    本文介绍了如何通过Node.js及其psd2json模块,快速实现PSD文件的自动化切图过程,以适应项目中频繁的界面更新需求。此方法不仅提高了工作效率,还简化了从设计稿到实际应用的转换流程。 ... [详细]
  • 本打算教一步步实现koa-router,因为要解释的太多了,所以先简化成mini版本,从实现部分功能到阅读源码,希望能让你好理解一些。希望你之前有读过koa源码,没有的话,给你链接 ... [详细]
  • 本文探讨了如何通过优化 DOM 操作来提升 JavaScript 的性能,包括使用 `createElement` 函数、动画元素、理解重绘事件及处理鼠标滚动事件等关键主题。 ... [详细]
  • 小编给大家分享一下Vue3中如何提高开发效率,相信大部分人都还不怎么了解,因此分享这篇文章给大家参考一下,希望大家阅读完这篇文章后大有收获, ... [详细]
  • 实践指南:使用Express、Create React App与MongoDB搭建React开发环境
    本文详细介绍了如何利用Express、Create React App和MongoDB构建一个高效的React应用开发环境,旨在为开发者提供一套完整的解决方案,包括环境搭建、数据模拟及前后端交互。 ... [详细]
  • Flutter 核心技术与混合开发模式深入解析
    本文深入探讨了 Flutter 的核心技术,特别是其混合开发模式,包括统一管理模式和三端分离模式,以及混合栈原理。通过对比不同模式的优缺点,帮助开发者选择最适合项目的混合开发策略。 ... [详细]
  • 在Java开发中,保护代码安全是一个重要的课题。由于Java字节码容易被反编译,因此使用代码混淆工具如ProGuard变得尤为重要。本文将详细介绍如何使用ProGuard进行代码混淆,以及其基本原理和常见问题。 ... [详细]
  • Django与Python及其他Web框架的对比
    本文详细介绍了Django与其他Python Web框架(如Flask和Tornado)的区别,并探讨了Django的基本使用方法及与其他语言(如PHP)的比较。 ... [详细]
  • ABP框架是ASP.NET Boilerplate的简称,它不仅是一个开源且文档丰富的应用程序框架,还提供了一套基于领域驱动设计(DDD)的最佳实践架构模型。本文将详细介绍ABP框架的特点、项目结构及其在Web API优先架构中的应用。 ... [详细]
  • 本文详细介绍了 Node.js 中 OS 模块的 arch 方法,包括其功能、语法、参数以及返回值,并提供了具体的使用示例。 ... [详细]
  • ArcBlock 发布 ABT 节点 1.0.31 版本更新
    2020年11月9日,ArcBlock 区块链基础平台发布了 ABT 节点开发平台的1.0.31版本更新,此次更新带来了多项功能增强与性能优化。 ... [详细]
  • 黑客松获奖名单出炉、NFT艺术周圆满落幕 |Oasis周报 ... [详细]
  • 本文探讨了异步编程的发展历程,从最初的AJAX异步回调到现代的Promise、Generator+Co以及Async/Await等技术。文章详细分析了Promise的工作原理及其源码实现,帮助开发者更好地理解和使用这一重要工具。 ... [详细]
  • 如何将955万数据表的17秒SQL查询优化至300毫秒
    本文详细介绍了通过优化SQL查询策略,成功将一张包含955万条记录的财务流水表的查询时间从17秒缩短至300毫秒的方法。文章不仅提供了具体的SQL优化技巧,还深入探讨了背后的数据库原理。 ... [详细]
  • 入门指南:使用FastRPC技术连接Qualcomm Hexagon DSP
    本文旨在为初学者提供关于如何使用FastRPC技术连接Qualcomm Hexagon DSP的基础知识。FastRPC技术允许开发者在本地客户端实现远程调用,从而简化Hexagon DSP的开发和调试过程。 ... [详细]
author-avatar
偶们滴小圈子6868
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有