GItHub地址 https://github.com/a130888599/WordCount
注意
- 请将所有测试文件放进test文件
- 为了规避各种各样的bug和控制文件大小,没有使用各种框架
- 由于原生node.js写GUI过于复杂,改用网页显示
PSP表格
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 20 | 20 |
· Estimate | · 估计这个任务需要多少时间 | 10 | 20 |
Development | 开发 | 42460(4天) | 32460(3天) |
· Analysis | · 需求分析 (包括学习新技术) | 2*60 | 6*60 |
· Design Spec | · 生成设计文档 | 4*60 | 3*60 |
· Design Review | · 设计复审 (和同事审核设计文档) | 3*60 | 2*60 |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 5*60 | 6*60 |
· Design | · 具体设计 | 7*60 | 7*60 |
· Coding | · 具体编码 | 4*60 | 6*60 |
· Code Review | · 代码复审 | 2*60 | 3*60 |
· Test | · 测试(自我测试,修改代码,提交修改) | 2*60 | 7*60 |
Reporting | 报告 | 3*60 | 3*60 |
· Test Report | · 测试报告 | 2*60 | 2*60 |
· Size Measurement | · 计算工作量 | 15 | 30 |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 30 | 60 |
合计 | 3 * 24 * 60(3天) | 4 * 24 * 60(4天) |
题目要求
• 实现一个简单完整的软件工具
• 进行单元测试,回归测试,效能测试
• 记录自己在每个环节花费的时间
软件功能
能够统计文本文件的字符数,单词数,行数等,最好具备其他扩展功能,能够快速处理多个文件
命令行格式:wc.exe [parameter] [file_name]
功能:
• -c:返回字符数
• -w:返回词数
• -l:返回行数
• -s:递归处理目录下符合条件的所有文件
• -a:返回代码行数,空行数,注释行数
• -x:打开图形化界面
解题思路
分析了一下需求和自己的技术栈,决定使用node.js进行开发
难点
项目的难点主要有
- 如何支持通配符(*,?)
- 多参数之间的关系,比如 wc.exe -s -a -l test.c
- 读取文件是异步操作,怎么保证程序的顺序执行
- GUI如何实现
方法
- 由于通配符只有两个,可以在获取到文件名时,先判断是否含有通配符,因此就有三种处理情况:*,?,普通文件,分别设置判断函数就行。
- 多参数之间,基本是没有相互影响的,比如-c, -l,但有一种情况是相互影响的,就是含有-s的,因此需要先判断是否有-s参数。
- 异步操作,可以使用Promise保证其顺序执行
- 改用网页呈现
代码实现
index.js
// 判断参数中是否有-x,有则打开server.js
function isOpenGUI(params) {
for (let param of params) {
if (param == '-x') {
c.fork(__dirname + '/server.js')
console.log(__dirname + '/server.js');
}
}
}
isOpenGUI(params)
// 判断是不是全选文件
function isSearchAllFiles(fileName) {
// 如果选择的是全部文件
if (fileName.includes('*')) {
const fileType = fileName.substr(fileName.indexOf('.'))
const files = fs.readdirSync('./test')
return files.filter(item => item.includes(fileType))
} else {
return [fileName]
}
}
const isIncludeS = includeS(params)
newParams = params.filter(item => item != '-s')
const fileArr = isIncludeS ? fs.readdirSync('./test') : isSearchAllFiles(fileName)
// 遍历每一个符合要求的文件,分别传入参数
fileArr.map((file) => {
let filePath = `${__dirname}/test/${file}`
newParams.map(async (item) => {
await fileDetail(item, filePath, file)
})
})
各功能实现
// -c
function getFileCharsNum(data) {
const str = removeEscapeChar(data).join('') // 获得处理掉转义符后的字符串
console.log(`${fileName} 的字符数为:${str.length}`);
return {
res: str.length
}
}
// -l
function getFileLinesNum(data) {
const arr = removeEscapeChar(data)
// console.log(arr);
console.log(`${fileName} 的行数为:${arr.length}`);
return {
res: arr.length
}
}
// -w
function getFileWordsNum(data) {
const arr = removeEscapeChar(data) // 获取处理掉转义符后的数组,每个元素代表一行
let res = 0;
arr.map((item) => { // 通过空格判断单词
let isSpace = true
for (let i = 0; i < item.length; i++) {
if (item[i] == ' ')
isSpace = true
else {
if (isSpace)
res++
isSpace = false
}
}
})
console.log(`${fileName} 的单词数为:${res}`);
return {
res
}
}
// -a
function getFileAllNum(data) {
let [spaceLines, commentLines, codeLines] = [0, 0, 0]
const arr = removeEscapeChar(data)
arr.map((item) => {
if (isNullLine(item))
spaceLines++
else if (isCommentLine(item))
commentLines++
else
codeLines++
})
console.log(`${fileName} 的空行数为:${spaceLines}`);
console.log(`${fileName} 的注释行数为:${commentLines}`);
console.log(`${fileName} 的代码行数为:${codeLines}`);
return {
res: {
spaceLines: spaceLines,
commentLines: commentLines,
codeLines: codeLines
}
}
}
// 判断是否为空行
function isNullLine(str) {
if (str == '')
return true
return false
}
// 处理字符串,把转义符去除,返回每行的数据
function removeEscapeChar(str) {
let [arr, newArr] = [str.split('\r'), []] // 将\r去除
arr.map((item) => {
newArr.push(item.trim()) // 将\n去除
})
return newArr
}
// 判断是否为代码行,只能判断//,不能判断/**/,/** */和某些编程语言的特定语法
function isCommentLine(str) {
if (str.includes('//')) // 未找到
return true
return false
}
// 设置文件名
function setFileName(name) {
fileName = name
}
// 获取文件内容
async function getFileDetail(parameters, filePath, file) {
try {
await fs.readFile(filePath, 'utf-8', async (error, data) => {
if (error) {
console.log(error);
return
}
setFileName(file)
switch (parameters) {
case '-c': {
getFileCharsNum(data)
return
}
case '-w': {
getFileWordsNum(data)
return
}
case '-l': {
getFileLinesNum(data)
return
}
case '-a': {
getFileAllNum(data)
return
}
default:
console.log('无此命令,请重新输入');
}
})
return new Promise(res => {
res(true)
})
} catch (error) {
console.log("ERROR: ", error);
}
}
module.exports = {
getFileCharsNum,
getFileLinesNum,
getFileWordsNum,
getFileAllNum,
setFileName
}
服务器代码
const server = http.createServer((req, res) => {
let url_obj = url.parse(req.url, true);
//进入路由判断并返回相应文件
route(url_obj, req, res);
})
server.listen(8081, () => {
console.log('服务器已开启8081')
c.exec('start http://localhost:8081/');
})
function route(url_obj, req, res) {
let pathname = url_obj.pathname;
switch (pathname) {
case '/': {
openHTML(pathname, res)
return
}
case '/getAllFile': {
console.log('hhhhhh');
getAllFileDetail(res)
return
}
default: {
let data = fs.readFileSync(`./web${req.url}`);
res.end(data);
return
}
}
}
// 读取html文件
function openHTML(filmName, res) {
if (filmName == '/') { //如果是主页
data = fs.readFileSync(`./web/index.html`, 'utf-8');
} else {
data = fs.readFileSync(`./web${filmName}`, 'utf-8'); //异步读取会导致undefined
}
res.end(data);
}
// web返回:全部数据
function getAllFileDetail(res) {
const filesArr = fs.readdirSync('./test')
let resArr = []
filesArr.map(item => {
console.log('item :', item);
setFileName(item)
let data = fs.readFileSync(`./test/${item}`, 'utf-8')
let resValue = {
fileName: item,
value: {
charsNum: getFileCharsNum(data),
LinesNum: getFileLinesNum(data),
WordsNum: getFileWordsNum(data),
AllNum: getFileAllNum(data)
}
}
resArr.push(resValue)
})
res.end(JSON.stringify(resArr))
}
测试运行
项目小结
- 字符串识别和处理的时候,使用正则表达式更合适,可惜还没学会
- 代码有冗余重复,没有整理完成,可以再优化
- PSP预计时间不准确,有待锻炼
- 没有设计好文件分类,难以扩展功能,用户体验较差,页面设计过于简单