掘金从上线到现在,网站的前端重构了 3 次,后端也陆陆续续修改了整个网站的结构 2 次,但是随着业务不断推演复杂,团队人手增加,有需要一波进一步的优化!
这周,我们会根据当下掘金的情况和接下里的主要业务,整理代码。
掘金技术整理系列文章:
- 掘金技术整理(一)掘金的后端架构
- 掘金技术整理(二)掘金前端 Vue 结构及最佳实践 - 正在写
后端架构梳理
在架构开发之前,我们首先要梳理现有的网站状态和需求,然后再做优化
- 后端语言、框架、功能架构状态
- 主要业务
- 代码结构
- 一些最佳实践
- 下一步的展望
后端语言、框架、功能架构状态
后端语言 Node.js
由于网站的早期开发是我来负责的,虽然我曾经写过 PHP
、Ruby on Rails
和 Python
的后台,但因为自己同时写很多前端代码因此对 JavaScript
最熟。与此同时,我们选择了 LeanCloud 作为我们的云存储、推送、托管平台,因而就继续使用了 Node.js
为开发框架。
- 当前版本:
v4.4.5
框架 Express.js
因为选择了 LeanCloud 的缘故,其后端你托管要用 Express
加上本身对这个框架的使用还算熟练,便沿用了下来。
- 当前版本:
4.13.3
功能架构状态
上面已经讲了我们选择了 LeanCloud,具体来讲我们使用了如下功能:
LeanStorage
:数据存储LeanMessage
:移动应用推送Leanengine
:云引擎- Web 服务器
- 网页渲染
- 简单的 API 接口
- 云函数
- 数据绑定脚本
- 定时脚本
- Web 服务器
LeanAnalytics
:数据统计工具- 当前版本:
1.0.0-beta
由于掘金网站的主页面是一个纯前端应用,其部分业务会与后端数据接口直接关联。下图主要展示了我们后端的整体状态:
其中黑色的箭头表示业务需求,而黄色箭头代表数据更新需求。
主要业务模块
Web Server
网站服务器是整个应用最基础的部分,它处理网页端的页面、API 请求,外联用户信息和 LeanStorage 数据库。
config.js // 后端配置文件
server.js // 服务器启动脚本
app.js // 后端业务,被 server.js 引用
cloud.js // 云函数定义,被 app.js 引用
webpack.config.js // webpack 打包配置文件
根据不同的环境(生产环境、前端开发、后端开发),config.js
定义了各个开发环境需要的配置文件信息。例如在 package.json
的 npm scripts
里会定义:
"dev": "cross-env NODE_ENV=devFrontend supervisor -i vue,node_modules server"
这样,npm run dev
就会设置当下的 NODE_ENV
是 devFrontend
,也就是前端开发环境。
接下来 server.js
就会根据当下环境读取的配置文件开启服务端业务,如 app.js
定义的后端业务代码或 cloud.js
里的云函数。当然,在不同环境下 webpack
也会做不同的操作。
app.js
app.js
援引了配置文件后,执行 Express.js
框架,开启业务代码。除了基本的 middleware(页面 template engine jade
, cookie, session 等等),其最主要的业务包含:
- 页面:
routes
- 用户:
auth
- 错误处理
由于网站大量使用前端 router,因而在 routes
定义中也要格外小心,掘金的开发方式是这样的。例如 Styleguide 页面,可能是这样的定义的:
// app.js 文件
// routes/page.js 里定义了网页相关的路由及后端渲染
var page = require('./routes/page');
// app 绑定了 styleguide 页面的路由,及几个前端路由统一
app.get('/styleguide', page.styleguide);
app.get('/styleguide/base', page.styleguide);
app.get('/styleguide/components', page.styleguide);
cloud.js
云函数
LeanCloud 很好地支持了云函数,可以帮助你完成后端的数据触发性 Hook 脚本及定时脚本。
例如每一条评论存储在 Comment
Table 里,那么对于这条评论,我们可以捕捉到
beforeSave // 存储之前
afterSave // 存储之后
beforeUpdate // 更新之前
afterUpdate // 更新之后
beforeDelete // 删除之前
afterDelete // 删除之后
加入,我们想要实现一个功能,就是增加一个 comment
后,更新相应的文章的评论数加一,而删除后则减一。
// cloud.js 文件
// cloud/comment.js 定义了关于 Comment 数据的 Hook 函数
var comment = require('./cloud/comment');
AV.Cloud.afterSave('Comment', comment.afterSave);
AV.Cloud.afterDelete('Comment', comment.afterDelete);
// cloud/comment.js
exports.afterSave = function(request, response) {
... // update 相关文章的数据
if (ok) {
response.success();
} else {
response.error('...');
}
})
webpack.config.js
webpack 是一个模块打包工具,随着它的插件、业务越来越强大,它也像是之前的 grunt
和 gulp
一样分摊了一部分脚本自动化的功能。
webpack.config.base.js
:基本的打包配置文件,主要用于开发环境热更新webpack.config.prod.js
:生产环境的配置文件,引用了base
,定义打包需求,生成 build 好的文件
这里我就不展开关于 webpack 本身的配置优化的部分。
代码结构
除了上面说的最基本的服务器开启文件,整个项目的代码结构如下:
config.js
server.js
app.js
/routes // 各个路由的后端业务逻辑
/views // 网页渲染的 jade 文件
/vue // 各个页面的 vue 业务逻辑
/redis // 缓存定义
/public // 外部访问的静态文件
/assets // 后端静态文件
/data // 后端静态数据
/scss // SCSS 样式文件
cloud.js
/cloud // 云函数相关定义文件
webpack.config.js // webpack 打包配置文件
webpack.config.base.js
webpack.config.prod.js
当我们要增加一个页面的时候
我们再以 Styleguide 为例,如果我们要添加这样的一个网页代码:
- 我们确认它应该在
app.js
的路由的哪个模块下,/styleguide
是一个独立页面,因而它应该被定义在/routes/page.js
里,并定义到:// page.js 文件 exports.styleguide = function(req, res) { res.render('styleguide', { title: '掘金前端 Style Guide' }) }
- 路由绑定:
// app.js app.get('/styleguide', page.styleguide);
- 由于它是是一个网页,因而我们还要在
/views
里面定义/views/styleguide.jade
- 这个时候我们会看这个页面是否会是一个前端网页:
- 是:在
/vue
里定义,并要在webpack.config.base.js
里定义打包逻辑 - 否:则在
page.js
里后端渲染页面是传入数据
- 是:在
- 基于网页的复杂度来测试是否需要独立的样式,则需要定义
/assets/scss/styleguide.scss
(更多 CSS 结构我们会在另外一篇文章中详细描述)
这样,整个 Styleguide 页面会影响到的后端代码是:
app.js // 路由绑定到 /styleguide
/routes
page.js // 定义了 styleguide 后端业务
/views
styleguide.jade
/vue
/styleguide // styleguide 相关 vue 前端业务
main.js
app.vue
/assets
/scss
/pages/styleguide // [optional] styleguide 内的复杂组件样式
__style.scss
layout.scss
...
styleguide.scss // styleguide 相关独立样式
webpack.config.base.js // 定义新的 styleguide 相对应的 entry
当我们要增加 Hook 函数的时候
举例,我们要开发一个数据的 Hook 函数到 LeanCloud,比如说每当一个新的 Comment 生成的时候,我们要更新对应文章的评论数及最新的评论:
cloud.js // AV.Cloud.afterSave('Comment', comment.afterSave)
/cloud
comment.js // exports.afterSave = function(request, response) {}
当我们要增加一个定时脚本的时候
- 定义脚本在
/cloud/____.js
文件里 - 更新
cloud.js
文件注册脚本,如:AV.Cloud.define('cloudFunctionName', functionName)
- 部署后,在 LeanCloud 的定时脚本控制台定义运行的周期及时间
一些最佳实践
Node.js
- 能用环境变量却别开的数据,都放到
config.js
里,不要用if
和else
语句区分 - 善用
npm scripts
绑定运行函数,如:npm run dev // 开发,测试数据 npm run dev-backend // 后端开发,测试数据 npm run dev-build // 测试数据,打包 npm run prod // 开发,生产数据 npm run prod-backend // 后端开发,生产数据 npm run prod-build // 生产数据,部署前的打包 npm run test // 测试 npm start // 开启服务器
- 函数名竟可能简单易懂,类似于
getPopEntries
可以明确到getPopularEntries
- 每当安装、删除库的时候,记得用
npm install/uninstall PACKAGE --save/--save-dev
,随时更新库 - 命名规范
- 常数:
I_LOVE_YOU
用下划线加大写字母 - 变量:
iLoveYou
驼峰,无论是普通变量还是函数名 - Class:
ILoveYou
首字母大写的驼峰,包括 LeanCloud 自己的数据 Table 名 - 当内嵌的回调函数用到了类似变量名则,使用
_iLoveYou
加前置下划线
- 常数:
LeanCloud
- 不要重复 LeanCloud 定义好的数据类名,如
AV.User
,不要使用'_User'
- 善用
Promise
,将复杂的业务改写为清晰的异步处理流:start() .then(step1) .then(step2) .then(step3) .then(step4) .catch(errorHandler) .finally(callback)
- 但小心,LeanCloud 修改了几个 keyword 函数名,如:
always
替换了原有的finally
AV.Promise.error
替换了Promise.reject
AV.Promise.when([promise1, promise2, promise3])
可以在三个 promise 都完成的情况下做异步操作
- 但小心,LeanCloud 修改了几个 keyword 函数名,如:
- LeanStorage 数据库查询的技巧:
- 多用自带的一些语句,如
exists
,startsWith
,matches
等 - 利用
select
拿去部分数据 - 查询一个数据时,善用
query.first()
和query.get(id)
,但是注意:first()
后then(function(obj) {})
中的obj
可能是null
get()
后如果得不到数据会直接引发error
- 使用
AV.Object.createWithoutData(TABLE_NAME, ID)
来实现指针查询,无需取一遍数据 - 关联查询:
query.include('reply.user')
,一个文章查询可以用这类语句直接查出来一个评论的回复的用户数据,也就是说拿出来的一个comment
可以访问到comment.get('reply')
和comment.get('reply').get('user')
。 - 内嵌查询:
matchesQuery
- 多用自带的一些语句,如
Git 管理
origin/
master // 线上版本
|- hotfix-login // 热修复,如登录异常
release // 最新的要部署的版本
develop // 开发分支
|- feature-homepage-v2 // 正在开发的业务,如第二版的首页
|- feature-timeline-api // 正在开发的业务,如 Timeline 的 API
developer-ming
master
release
develop
|- feature-timeline-api // 我正在开发这个 feature,不断和 origin 同步
新的业务
- 任何的一个新的业务开发都要在本地从
develop
fork 出来一个新的 branchfeature-name
- 业务开发完成后,提交 Pull Request,
feature-name -> develop
,记得打 label 到feature
- Code Review,如果有错误,在
feature-name
里修复 - 相关负责人 Merge Pull Request,假删除这个分支
部署新的业务
develop
上不断 merge 新的 review 过的业务功能- 部署前,发 Pull Request 到
develop -> release
- 相关负责人 Code Review,合并代码
npm run build
打包业务代码,准备部署- 部署前的 commit,打 label 到
publish
- 发 PR 到
release -> master
,标注版本号 - 部署,如果出错,回滚或者新建
hotfix
分支
小技巧
develop
和release
的同步,用git rebase
develop
及feature
分支不做build
操作- 多人负责一个 feature 的时候,可以就一个功能再分拆到各个 branches
下一步的展望
根据产品接下来的发展路径,有几个重要的功能需要优化。
- 后端渲染页面,SEO 优化
- 文章页面的收敛,利用 Browser Agent
- 分享文章:Web Desktop 的详情页
- 分享文章:Web Mobile 的阅读页
- 原创文章:Web Desktop/Mobile 的阅读页
- 沸点活动:Web Desktop/Mobile 的详情页
- 所有文章:App 内的阅读页 / JSBridge
- 不同页面的前后端打包,根据不同页面的需求,加载相应的程序组件
- 脱离手动打包、配合部署
- 组件如:
- 用户
- Vue 通用样式组件
- 页面本身的业务代码
- 网页间的跳转逻辑
- API 服务器独立,并配合移动端也无需求,通用 API 实现
- 推送逻辑重构