app 模块位于 lib/units/app/路径下,app 模块的作用是提供一个完整的 http 服务器,这里的『完整』的意思是指包括 html、js、css、image 等所有的 web 静态内容。如果你深入了解过 stf 前面的实现原理,就会知道 stf 是一个完全的前后端分离式设计,简单来说,动态的数据完全是从接口来获取的,页面渲染完全由 js 来完成,app 模块的作用就是把前端的所有东西返回给浏览器,返回完以后,前端面就和 app 模块没有任何交互了!!!如果你不相信,可以在打开 stf 首页以后把 app 模块停掉(需要用 docker 等分离式部署方式),完全不会影响 stf 的正常使用,除非你刷新页面。
app 模块从原理上讲非常简单,它就是一个普通的 http 服务器,使用 express 来实现,而它所需要返回的东西已经由 webpack 打包好了。
这里多说一句,如果你在开发 stf 的时候,用 local 方式启动,只是对前端做了修改,只需要执行 npm install 一下然后刷新页面就可以了,不用重新启动整个 stf。
启动方式可以参考文档中给出的 docker 启动命令,下面只摘出主要的部分:
stf app --port 3000 \
--auth-url https://stf.example.org/auth/mock/ \
--websocket-url wss://stf.example.org/
也就是说,只要指定三个参数 port、授权--auth-url、websocket 地址--websocket-url 即可,auth 和 websocket 的参数会在启动对应的模块时指定。这里的 auth 参数的作用是在 app 模块中授权失败的时候自动跳转么授权(登录)页面,而 websocket 的 url 似乎只是在 GLOBAL_APPSTATE 中用到,没有用来建立连接。
这里面的"stf"这个命令可以执行是因为我们把 bin/stf 这个文件加入到了系统环境变量,打开 bin/stf 这个文件可以看出只有一行../lib/cli/please,然后我们看看这个 please 是什么,发现它其实指向了../lib/cli/index.js,因此,你如果没有把 bin/stf 加入到系统环境变量,直接用 node ../lib/cli/index.js 也可以,或者直接 node ../lib/cli,例如 local 启动可以直接
node ../lib/cli local --public-ip xxx
或者这样启动 app
node ../lib/cli app --port 3000 \
--auth-url https://stf.example.org/auth/mock/ \
--websocket-url wss://stf.example.org/
首先看 index.js 文件,这是 app 模块的入口。在 app 模块中引入了不少其他的模块,下面简单说一下,其实如果你了解过 express 框架,这些都很简单了。
app.set 是设备服务器的一些参数:
app.use(路径,function(){}) 就是处理对应路径的一些方法了,比如说/static/wiki、/static/app/build/entry、/static/app/data 等,假如用户请求的是/static/logo 这个路径,那么 express 会把请求交给 serveStatic 这个方法来处理。详情可以查询一下 express 的中间件相关知识。
看下面这一段:
if (fs.existsSync(pathutil.resource('build'))) {
log.info('Using pre-built resources')
app.use(compression())
app.use('/static/app/build/entry',
serveStatic(pathutil.resource('build/entry')))
app.use('/static/app/build', serveStatic(pathutil.resource('build'), {
maxAge: '10d'
}))
}
else {
log.info('Using webpack')
// Keep webpack-related requires here, as our prebuilt package won't
// have them at all.
var webpackServerConfig = require('./../../../webpack.config').webpackServer
app.use('/static/app/build',
require('./middleware/webpack')(webpackServerConfig))
}
这段话的意思是如果存在 build 文件夹(已经用 webpack build 过),那么就使用 build 文件夹中的内容,否则就要使用 webpack 热生成了。关于 webpack 中间件下文再做介绍。
app.use(cookieSession({
name: options.ssid
, keys: [options.secret]
}))
app.use(auth({
secret: options.secret
, authUrl: options.authUrl
}))
cookieSession 需要设置 name 和 keys 两个参数,key 是用来对 cookies 进行签名和验证用的。
auth 方法也需要传入两个参数 secret 和 authUrl,authUrl 是指授权 url,在授权模块中会指定。
下面看一下 middleware 文件夹中的 auth 文件,在这里定义了 auth 方法。先看第一段话:
if (req.query.jwt) {
// Coming from auth client
var data = jwtutil.decode(req.query.jwt, options.secret)
var redir = urlutil.removeParam(req.url, 'jwt')
if (data) {
// Redirect once to get rid of the token
dbapi.saveUserAfterLogin({
name: data.name
, email: data.email
, ip: req.ip
})
.then(function() {
req.session.jwt = data
res.redirect(redir)
})
.catch(next)
}
else {
// Invalid token, forward to auth client
res.redirect(options.authUrl)
}
}
if (req.query.jwt) 是指 query 中包含 jwt 的字段,query 中的 jwt 字段是指 url 中直接包含 jwt=xxxx 等内容,在用户每一次用 mock 方式登录的时候会出现这种情况。下面就是把 jwt 中的内容解析为明文信息 data,然后是解析 jwt 中的重定向 url 到 redir。如果发现 jwt 解析成功,就把对应的用户存储在数据库中,如果解析不成功,就重定向到授权的 url。
总结一下,这段代码其实是处理用户第一次登录的时候的验证问题,用户第一次登录的时候由于 cookies 没有 jwt token,只能由 auth 模块在 url 的后面加入 jwt 参数来授权,然后由 app 模块解析。
else if (req.session && req.session.jwt) {
dbapi.loadUser(req.session.jwt.email)
.then(function(user) {
if (user) {
// Continue existing session
req.user = user
next()
}
else {
// We no longer have the user in the database
res.redirect(options.authUrl)
}
})
.catch(next)
}
在 else 语句中,是从 session 中解析了用户信息。一个典型的 jwt 串如下:
{ jwt: 'eyJhbGciOiJIUzI1NiIsImV4cCI6MTUwMTQxMTAzNzg5NH0.eyJlbWFpbCI6InRlc3R1c2VyQHRlc3QuY29tIiwibmFtZSI6InRlc3R1c2VyIn0.W5zYDcA4wu6kB1GWR9BLOKdGtyDwRO9IQaA2LqW7CrY' }
下面这段代码:
app.all('/app/api/v1/dummy', function(req, res) {
res.send('OK')
})
我也没搞清楚是干什么用的,估计是测试用的。
下面的 bodyParser、csrf、validator 可以参数对应的中间件。res.cookie 是把 XSRF-TOKEN 写入 cookies,然后在请求的时候带上,防止伪造请求。
app.get('/', function(req, res) {
res.render('index')
})
这段就是用户访问 stf 的根目录时处理的代码了,这里返回了 index,是指 res/app/views/index.pug 这个文件。
app.get('/app/api/v1/state.js', function(req, res) {
...xxxx
})
这段是提供 app 状态的代码,它的后缀有点儿奇怪,是.js,当别人访问它的时候,会认为他是一个 js 文件,但是不真的是 js 文件,而且在 response 里设置 type 为'application/javascript',应该是为了动态设置前端某个静态变量用的,等我研究透了 STF 前端再详细介绍。在 res/app/views/index.pug 有用到这个路径。
app 模块的访问路径就是 STF 网站的根目录。下面是 app 模块的 nginx 配置:
location / {
proxy_pass http://stf_app;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Real-IP $http_x_real_ip;
}
可以看出 app 的访问路径是/。
当我们执行 stf app xxx 的时候,yargs 这个命令行工具会执行 lib/units/app/index.js 这个文件,然后会启动一个 express 服务器。如果用户访问 stf 网站的根目录,express 服务器会返回 webpack 打包好的 html、css、js、img 等文件,其中 js 会建立 websocket 或者请求 api 与后端交互。在 app 中也包含了 cookies 和授权等内容。
app 本身其实并不复杂,而它向浏览器传输的内容 -- 前端框架则是 STF 中非常复杂的一部分。