热门标签 | 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 加载状态



推荐阅读
  • 深入浅出 webpack 系列(二):实现 PostCSS 代码的编译与优化
    在前一篇文章中,我们探讨了如何通过基础配置使 Webpack 完成 ES6 代码的编译。本文将深入讲解如何利用 Webpack 实现 PostCSS 代码的编译与优化,包括配置相关插件和加载器,以提升开发效率和代码质量。我们将详细介绍每个步骤,并提供实用示例,帮助读者更好地理解和应用这些技术。 ... [详细]
  • WinMain 函数详解及示例
    本文详细介绍了 WinMain 函数的参数及其用途,并提供了一个具体的示例代码来解析 WinMain 函数的实现。 ... [详细]
  • MicrosoftDeploymentToolkit2010部署培训实验手册V1.0目录实验环境说明3实验环境虚拟机使用信息3注意:4实验手册正文说 ... [详细]
  • 本文详细介绍了 PHP 中对象的生命周期、内存管理和魔术方法的使用,包括对象的自动销毁、析构函数的作用以及各种魔术方法的具体应用场景。 ... [详细]
  • 在Delphi7下要制作系统托盘,只能制作一个比较简单的系统托盘,因为ShellAPI文件定义的TNotifyIconData结构体是比较早的版本。定义如下:1234 ... [详细]
  • 在JavaWeb开发中,文件上传是一个常见的需求。无论是通过表单还是其他方式上传文件,都必须使用POST请求。前端部分通常采用HTML表单来实现文件选择和提交功能。后端则利用Apache Commons FileUpload库来处理上传的文件,该库提供了强大的文件解析和存储能力,能够高效地处理各种文件类型。此外,为了提高系统的安全性和稳定性,还需要对上传文件的大小、格式等进行严格的校验和限制。 ... [详细]
  • 本文介绍了如何使用 Node.js 和 Express(4.x 及以上版本)构建高效的文件上传功能。通过引入 `multer` 中间件,可以轻松实现文件上传。首先,需要通过 `npm install multer` 安装该中间件。接着,在 Express 应用中配置 `multer`,以处理多部分表单数据。本文详细讲解了 `multer` 的基本用法和高级配置,帮助开发者快速搭建稳定可靠的文件上传服务。 ... [详细]
  • Composer 无法加载本地第三方库?如何解决这一常见问题 ... [详细]
  • 本文详细介绍了在MySQL中如何高效利用EXPLAIN命令进行查询优化。通过实例解析和步骤说明,文章旨在帮助读者深入理解EXPLAIN命令的工作原理及其在性能调优中的应用,内容通俗易懂且结构清晰,适合各水平的数据库管理员和技术人员参考学习。 ... [详细]
  • 本文详细解析了 Android 系统启动过程中的核心文件 `init.c`,探讨了其在系统初始化阶段的关键作用。通过对 `init.c` 的源代码进行深入分析,揭示了其如何管理进程、解析配置文件以及执行系统启动脚本。此外,文章还介绍了 `init` 进程的生命周期及其与内核的交互方式,为开发者提供了深入了解 Android 启动机制的宝贵资料。 ... [详细]
  • 在Android平台中,播放音频的采样率通常固定为44.1kHz,而录音的采样率则固定为8kHz。为了确保音频设备的正常工作,底层驱动必须预先设定这些固定的采样率。当上层应用提供的采样率与这些预设值不匹配时,需要通过重采样(resample)技术来调整采样率,以保证音频数据的正确处理和传输。本文将详细探讨FFMpeg在音频处理中的基础理论及重采样技术的应用。 ... [详细]
  • Web开发框架概览:Java与JavaScript技术及框架综述
    Web开发涉及服务器端和客户端的协同工作。在服务器端,Java是一种优秀的编程语言,适用于构建各种功能模块,如通过Servlet实现特定服务。客户端则主要依赖HTML进行内容展示,同时借助JavaScript增强交互性和动态效果。此外,现代Web开发还广泛使用各种框架和库,如Spring Boot、React和Vue.js,以提高开发效率和应用性能。 ... [详细]
  • PHP预处理常量详解:如何定义与使用常量 ... [详细]
  • 字符串学习时间:1.5W(“W”周,下同)知识点checkliststrlen()函数的返回值是什么类型的?字 ... [详细]
  • 本文介绍了如何利用 `matplotlib` 库中的 `FuncAnimation` 类将 Python 中的动态图像保存为视频文件。通过详细解释 `FuncAnimation` 类的参数和方法,文章提供了多种实用技巧,帮助用户高效地生成高质量的动态图像视频。此外,还探讨了不同视频编码器的选择及其对输出文件质量的影响,为读者提供了全面的技术指导。 ... [详细]
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社区 版权所有