TMS web 是怎么建起来的 - NodeJS 建站简述

TMS (tech manage system) 其实是技术内部工具集合的平台。其中最主要一个内容是包含了项目发布系统,发布系统是利用 Fabric 可以比较轻松的通过编程的方式在目标机器上执行 shell 命令特性。我在其中主要是设计和开发它的用户界面,方便开发人员通过图形化的界面操作系统及观察命令运行情况。也是第一次用nodejs开发后台应用。

技术选型

  • NodeJS
  • Express
  • xtemplate
  • bootstrap
  • xterm
  • pm2

目录结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
.
├── libs
│   ├── db.js
│   ├── fab.js
│   └── project.js
├── public
│   └── assets
├── routes
│   ├── autotest.js
│   ├── deploy.js
│   └── index.js
├── views
│   ├── autotest_start_up.xtpl
│   ├── deploy_projects.xtpl
│   ├── home.xtpl
│   ├── layout.xtpl
│   ├── menu.xtpl
│   ├── project_build.xtpl
│   ├── project_building.xtpl
│   ├── project_detail.xtpl
│   └── project_history.xtpl
├── app.js
├── config.js
├── package.json
└── start-web.sh

安装运行

1
2
3
npm insatll
export TMS_ENV="local"
node app.js

开发相关

路由的设计 - 中间件

应用级中间件,方便模块化路由

1
2
3
4
5
6
7
var index = require('./routes/index')
var deploy = require('./routes/deploy')
var autotest = require('./routes/autotest')

app.use('/', index)
app.use('/deploy', deploy)
app.use('/autotest', autotest)

创建路由级中间件

1
2
3
4
5
6
7
8
9
10
11
12
13
router.get('/:projectName/detail', function (req, res) {

var projectName = req.projectName

var targets = project.getTargetByProjectName(projectName)

res.render('project_detail', {
project: projectJson[projectName],
projectName: projectName,
targets: targets,
buildCount: req.buildCount
})
})

使用 param 中间件给所有带 projectName 的路径挂载同一数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
router.param('projectName', function (req, res, next, projectName) {

var getRunningBuild = db.getBuilding(projectName)

getRunningBuild.done(function (buildList) {

buildList.map(function (item, index) {
formatHistoryData(item)
})

req.projectName = projectName
req.building = buildList
req.buildCount = buildList.length
next()
})

});

异步管理 - promise 对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

exports.getBuilding = function (projectName) {

var sqlDefer = Q.defer()

pool.getConnection(function (err, connection) {

connection.query(sql, function (err, rows, fields) {

if (!err) {
sqlDefer.resolve(rows)
} else {
sqlDefer.reject(err)
}

connection.release()
})
})

return sqlDefer.promise
}

var getBuild = getBuilding(projectName);

getBuild.done(function (builds) {

})

面向前端的模版 - xtemplate

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{{extend ("./layout")}}

{{#block("page_class")}} page_project_build {{/block}}


{{#block ("menu")}}

{{parse ("./menu", current="deploy")}}

{{/block}}

{{#block ("body")}}

.....

{{/block}}

调用 shell 命令 - child_process

1
2
3
4
5
6
7
var cp = require('child_process')
cp.exec(commondLine, {
cwd: path.join(__dirname, '../../tms-belt'),
timeout: 3000
}, function (error, stdout, stderr) {

})

调试工具 - node-inspector

1
2
3
4
5
6
node-inspector
node app.js --debug

或者

node-debug app.js

进程管理 - pm2

1
2
3
4
5
6
7
pm2 start app.js --node-args="--debug" --watch
pm2 list
pm2 show 0
pm2 logs
pm2 restart
pm2 kill 0
pm2 monit

日志的实时显示

以为web页面中显示出的日志文件也是需要类似代码格式化类的工具,其实是需要web版的终端来实现的。我采用的方案是 xtrm 特别当我们需要实时数据时,代码格式化的性能是跟不上的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
function websocket(server){

const socket = io.listen(server)

socket.on('connection', (client) => {


client.on('cmd_stop', (projectName, targetKey)=>{

var fab_param = 'do:' + projectName + ',target_key=' + targetKey + ',cmd=stop'

var fab = cp.spawn('fab', [fab_param], {
cwd: path.join(__dirname, '../../tms-belt'),
timeout: 3000
})

fab.stdout.on('data', function(buffer){
client.emit('cmd_stop_data', buffer.toString())
})

fab.stderr.on('data', (buffer) => {
client.emit('socket_error', `${buffer}`)
});

fab.on('exit', function(){
client.emit('cmd_exit')
})

client.on('disconnect', () => {
fab.kill()
})

})

....

})

});

}

其它

NodeJS 版本问题

现在 NodeJS 版本已经到了 5.X 以上了,对 ES6 的特性支持的很好,但是模块的依赖关系复杂,目前还是用在 0.X 的比较安全

如何处理好环境配置信息

  • 使用环境变量 env/export
  • 使用项目外文件管理 config.json/confiy.yml
  • 使用 config 模块