代码工具:文档生成器与模块打包器
文档生成器概述
文档生成器在软件开发中扮演着重要角色,它能帮助开发者自动生成代码的文档,提高代码的可维护性和可读性。以
fill-in-headers-input.js
为例,它展示了文档生成器的基本功能。
主要组件及功能
-
main:作为主驱动程序,负责协调文档生成的整个流程。 -
parseArgs:用于解析命令行参数,接收两个参数: -
args (string[]):待解析的参数数组。 -
defaults (Object):默认值对象。
最终返回程序配置对象。 -
BaseProcessor:默认的处理类,包含以下方法: -
constructor:构建基础处理器。 -
run:将输入传递到输出,接收两个参数:-
input (stream):读取输入的流。 -
output (stream):写入输出的流。
-
代码即数据
代码本质上也是一种数据,我们可以像处理其他数据一样对其进行处理。例如,解析代码生成抽象语法树(AST)与解析 HTML 生成文档对象模型(DOM)类似,都是将易于人类编写的文本表示转换为便于程序操作的数据结构。将代码视为数据,能让我们通过单个命令完成常规编程任务,从而有更多时间思考那些尚未能自动化的任务。
文档生成器练习
为了进一步完善文档生成器,可进行以下练习:
1.
构建索引
:修改文档生成器,生成所有类和方法的字母索引,索引条目应为对应文档的超链接。
2.
异常文档化
:扩展文档生成器,允许开发者记录函数抛出的异常。
3.
弃用警告
:添加功能,允许作者将函数和方法标记为弃用,提示其不建议使用。
4.
使用示例
:增强文档生成器,若文档注释中出现水平规则
---
,则将后续文本排版为使用示例。
5.
单元测试
:使用 Mocha 为文档生成器编写单元测试。
6.
函数总结
:修改文档生成器,将函数内部使用
//*
的行注释格式化为该函数文档中的项目列表。
7.
交叉引用
:使文档生成器支持在一个类或函数的文档中包含指向其他类或函数的 Markdown 链接。
8.
数据类型定义
:允许作者像 JSDoc 一样定义新的数据类型。
9.
内联参数文档
:处理参数文档与参数在同一行的情况,例如:
/**
* Transform data.
*/
function process(
input, /*- {stream} where to read */
output, /*- {stream} where to write */
op /*- {Operation} what to do */
) {
// body would go here
}
- 测试即文档 :编写工具,提取函数开头块注释中的代码和输出,并转换为断言。例如,对于以下输入:
const findIncreasing = (values) => {
/**
* > findIncreasing ([])
* []
* > findIncreasing ([1])
* [1]
* > findIncreasing ([1, 2])
* [1, 2]
* > findIncreasing ([2, 1])
* [2]
*/
}
工具将生成:
assert.deepStrictEqual(findIncreasing ([]), [])
assert.deepStrictEqual(findIncreasing ([1]), [1])
assert.deepStrictEqual(findIncreasing ([1, 2]), [1, 2])
assert.deepStrictEqual(findIncreasing ([2, 1]), [2])
模块打包器概述
由于 JavaScript 最初设计时未考虑大型程序的需求,缺乏将多个源文件合并为单个文件的功能。模块打包器应运而生,它能找到应用程序依赖的所有文件,并将它们合并为一个可加载的文件,提高加载效率,同时测试依赖是否能正确解析。
测试用例
- 单文件测试 :一个不依赖其他文件的单文件,代码如下:
const main = () => {
console.log('in main ')
}
module.exports = main
预期输出为
in main
。
-
两文件测试
:
main.js
依赖
other.js
,代码如下:
// main.js
const other = require ('./other ')
const main = () => {
console.log(other('main '))
}
module.exports = main
// other.js
const other = (caller) => {
return `other called from ${caller}`
}
module.exports = other
预期输出为
other called from main
。
-
多文件多目录测试
:
main.js
依赖多个文件,各文件之间存在复杂的依赖关系,代码如下:
// main.js
const topLeft = require ('./top -left ')
const topRight = require ('./top -right ')
const bottomLeft = require ('./ subdir/bottom -left ')
const bottomRight = require ('./ subdir/bottom -right ')
const main = () => {
const functions = [topLeft, topRight, bottomLeft, bottomRight]
functions.forEach(func => {
console.log(`${func('main ')} `)
})
}
module.exports = main
预期输出为:
topLeft from main
topRight from main with topLeft from topRight and bottomRight from topRight
bottomLeft from main with topLeft from bottomLeft and bottomRight from bottomLeft
bottomRight from main
查找依赖
单个文件依赖查找
要获取单个源文件的所有依赖,可解析该文件并提取所有
require
调用。以下是实现代码:
import acorn from 'acorn '
import fs from 'fs '
import walk from 'acorn -walk '
const getRequires = (filename) => {
const entryPointFile = filename
const text = fs.readFileSync(entryPointFile, 'utf -8')
const ast = acorn.parse(text)
const requires = []
walk.simple(ast, {
CallExpression: (node, state) => {
if ((node.callee.type === 'Identifier') && (node.callee.name === 'require')) {
state.push(node.arguments[0].value)
}
}
}, null, requires)
return requires
}
export default getRequires
使用示例:
import getRequires from './get -requires.js '
const result = getRequires(process.argv[2])
console.log(result)
依赖查找的问题
上述依赖查找方法对于合理的 JavaScript 程序能给出正确结果,但对于一些复杂情况,如创建
require
的别名或使用
eval
动态加载模块,可能无法准确找到所有依赖。
查找所有依赖(传递闭包)
为了获取打包所需的所有依赖,需要找到入口点依赖的传递闭包。以下是实现代码:
import path from 'path '
import getRequires from './get -requires.js '
const transitiveClosure = (entryPointPath) => {
const pending = [path.resolve(entryPointPath)]
const filenames = new Set()
while (pending.length > 0) {
const candidate = path.resolve(pending.pop())
if (filenames.has(candidate)) {
continue
}
filenames.add(candidate)
const candidateDir = path.dirname(candidate)
getRequires(candidate)
.map(raw => path.resolve(path.join(candidateDir, `${raw }.js `)))
.filter(cooked => !filenames.has(cooked))
.forEach(cooked => pending.push(cooked))
}
return [...filenames]
}
export default transitiveClosure
为了跟踪文件内所需名称与绝对路径的映射,可对传递闭包代码进行修改:
import path from 'path '
import getRequires from './get -requires.js '
const transitiveClosure = (entryPointPath) => {
const mapping = {}
const pending = [path.resolve(entryPointPath)]
const filenames = new Set()
while (pending.length > 0) {
const candidate = path.resolve(pending.pop())
if (filenames.has(candidate)) {
continue
}
filenames.add(candidate)
mapping[candidate] = {}
const candidateDir = path.dirname(candidate)
getRequires(candidate)
.map(raw => {
mapping[candidate][raw] = path.resolve(path.join(candidateDir, `${raw }.js `))
return mapping[candidate][raw]
})
.filter(cooked => cooked!== null)
.forEach(cooked => pending.push(cooked))
}
return mapping
}
export default transitiveClosure
安全合并文件
为了将找到的文件合并为一个文件,同时保持每个文件在自己的命名空间中,可使用立即执行函数表达式(IIFE)将源代码包装起来,并为其提供一个
module
对象和
require
实现来解决包内的依赖关系。以下是合并文件的代码:
import fs from 'fs '
import path from 'path '
const HEAD = `const initialize = (creators) => {`
const TAIL = `}`
const combineFiles = (allFilenames) => {
const body = allFilenames
.map(filename => {
const key = path.resolve(filename)
const source = fs.readFileSync(filename, 'utf -8')
const func = `(module, require) => {${source }}`
const entry = `creators.set('${key}',\n${func})`
return `// ${key }\n${entry }\n`
})
.join('\n')
const func = `${HEAD }\n${body }\n${TAIL}`
return func
}
export default combineFiles
文件相互访问
为了使文件能够相互访问,需要创建一个查找表,将绝对文件名映射到创建模块导出的函数,并实现一个
require
函数来解决依赖关系。以下是实现代码:
import fs from 'fs '
import path from 'path '
import transitiveClosure from './ transitive -closure.js '
const HEAD = `const creators = new Map ()
const cache = new Map ()
const makeRequire = (absPath) => {
return (localPath) => {
const actualKey = translate[absPath ][ localPath]
if (! cache.has(actualKey )) {
const m = {}
creators.get(actualKey )(m)
cache.set(actualKey , m.exports)
}
return cache.get(actualKey)
}
}
const initialize = (creators) => {`
const TAIL = `}
initialize(creators)`
const makeProof = (entryPoint) => `
const start = creators.get('${entryPoint }')
const m = {}
start(m)
m.exports ()`
const createBundle = (entryPoint) => {
entryPoint = path.resolve(entryPoint)
const table = transitiveClosure(entryPoint)
const translate = `const translate = ${JSON.stringify(table, null, 2)}`
const creators = Object.keys(table).map(filename => makeCreator(filename))
const proof = makeProof(entryPoint)
return [
translate,
HEAD,
...creators,
TAIL,
proof
].join('\n')
}
const makeCreator = (filename) => {
const key = path.resolve(filename)
const source = fs.readFileSync(filename, 'utf -8')
const func = `(module, require = makeRequire ('${key }')) =>\n{${source }}`
const entry = `creators.set('${key}',\n${func})`
return `// ${key }\n${entry }\n`
}
export default createBundle
通过以上步骤,我们可以实现一个基本的模块打包器,将多个文件合并为一个可加载的文件,提高应用程序的加载效率。
模块打包器实现流程总结
为了更清晰地展示模块打包器的实现过程,我们可以用以下 mermaid 流程图来概括:
graph TD;
A[选择入口文件] --> B[查找依赖(传递闭包)];
B --> C[合并文件];
C --> D[实现文件相互访问];
D --> E[生成打包文件];
模块打包器各步骤详细分析
| 步骤 | 描述 | 代码示例 |
|---|---|---|
| 查找依赖(传递闭包) | 从入口文件开始,递归查找所有依赖文件,并记录文件路径映射 |
javascript<br>import path from 'path '<br>import getRequires from './get -requires.js '<br><br>const transitiveClosure = (entryPointPath) => {<br> const mapping = {}<br> const pending = [path.resolve(entryPointPath)]<br> const filenames = new Set()<br> while (pending.length > 0) {<br> const candidate = path.resolve(pending.pop())<br> if (filenames.has(candidate)) {<br> continue<br> }<br> filenames.add(candidate)<br> mapping[candidate] = {}<br> const candidateDir = path.dirname(candidate)<br> getRequires(candidate)<br> .map(raw => {<br> mapping[candidate][raw] = path.resolve(path.join(candidateDir, `${raw }.js `))<br> return mapping[candidate][raw]<br> })<br> .filter(cooked => cooked!== null)<br> .forEach(cooked => pending.push(cooked))<br> }<br> return mapping<br>}<br><br>export default transitiveClosure<br>
|
| 合并文件 | 将找到的所有依赖文件包装在 IIFE 中,并存储在查找表中 |
javascript<br>import fs from 'fs '<br>import path from 'path '<br><br>const HEAD = `const initialize = (creators) => {`<br>const TAIL = `}`<br><br>const combineFiles = (allFilenames) => {<br> const body = allFilenames<br> .map(filename => {<br> const key = path.resolve(filename)<br> const source = fs.readFileSync(filename, 'utf -8')<br> const func = `(module, require) => {${source }}`<br> const entry = `creators.set('${key}',\n${func})`<br> return `// ${key }\n${entry }\n`<br> })<br> .join('\n')<br> const func = `${HEAD }\n${body }\n${TAIL}`<br> return func<br>}<br><br>export default combineFiles<br>
|
| 实现文件相互访问 |
创建查找表和
require
函数,解决文件间的依赖关系
|
javascript<br>import fs from 'fs '<br>import path from 'path '<br>import transitiveClosure from './ transitive -closure.js '<br><br>const HEAD = `const creators = new Map ()<br>const cache = new Map ()<br>const makeRequire = (absPath) => {<br> return (localPath) => {<br> const actualKey = translate[absPath ][ localPath]<br> if (! cache.has(actualKey )) {<br> const m = {}<br> creators.get(actualKey )(m)<br> cache.set(actualKey , m.exports)<br> }<br> return cache.get(actualKey)<br> }<br>}<br>const initialize = (creators) => {`<br><br>const TAIL = `}<br>initialize(creators)`<br><br>const makeProof = (entryPoint) => `<br>const start = creators.get('${entryPoint }')<br>const m = {}<br>start(m)<br>m.exports ()`<br><br>const createBundle = (entryPoint) => {<br> entryPoint = path.resolve(entryPoint)<br> const table = transitiveClosure(entryPoint)<br> const translate = `const translate = ${JSON.stringify(table, null, 2)}`<br> const creators = Object.keys(table).map(filename => makeCreator(filename))<br> const proof = makeProof(entryPoint)<br> return [<br> translate,<br> HEAD,<br> ...creators,<br> TAIL,<br> proof<br> ].join('\n')<br>}<br><br>const makeCreator = (filename) => {<br> const key = path.resolve(filename)<br> const source = fs.readFileSync(filename, 'utf -8')<br> const func = `(module, require = makeRequire ('${key }')) =>\n{${source }}`<br> const entry = `creators.set('${key}',\n${func})`<br> return `// ${key }\n${entry }\n`<br>}<br><br>export default createBundle<br>
|
总结与展望
通过上述内容,我们详细介绍了文档生成器和模块打包器的相关知识。文档生成器可以帮助开发者更好地管理代码文档,提高代码的可维护性;而模块打包器则能解决 JavaScript 中多文件加载的问题,提高应用程序的加载效率。
在实际开发中,我们可以根据具体需求对文档生成器和模块打包器进行扩展和优化。例如,对于文档生成器,可以进一步完善其功能,使其支持更多的文档格式和注释规范;对于模块打包器,可以考虑处理更多复杂的依赖关系,如循环依赖等。
同时,我们也应该认识到,在实现这些工具的过程中,会遇到一些复杂的问题,如依赖查找的准确性、代码的可读性和可维护性等。因此,在开发过程中,我们需要不断地进行测试和优化,以确保工具的稳定性和可靠性。
希望本文能为开发者在使用和开发文档生成器及模块打包器方面提供一些帮助和启示。在未来的软件开发中,合理使用这些工具将有助于提高开发效率和代码质量。
实践建议
如果你想亲自实践这些工具,可以按照以下步骤进行:
1.
文档生成器实践
:
- 选择一个合适的代码项目,使用现有的文档生成器工具进行文档生成。
- 根据前面提到的练习内容,逐步对文档生成器进行扩展和优化。
- 编写单元测试,确保文档生成器的功能正确性。
2.
模块打包器实践
:
- 创建一个简单的 JavaScript 项目,包含多个文件和依赖关系。
- 按照本文介绍的步骤,实现一个基本的模块打包器。
- 对不同的测试用例进行测试,验证模块打包器的功能。
- 尝试对模块打包器进行优化,如处理更复杂的依赖关系、提高打包效率等。
通过实践,你将更深入地理解文档生成器和模块打包器的原理和实现方法,从而在实际开发中更好地应用它们。
超级会员免费看

被折叠的 条评论
为什么被折叠?



