###石墨整体架构一览 ####前端篇
#####框架 石墨的前端使用的框架很传统:jQuery + RequireJS,我们使用RequireJS的text模块引入模板,手动用代码渲染数据,没有使用AngularJS之类的MV*框架的原因是自己编写的js更灵活,拓展性更好。框架们的功能都很强大,但是当遇到一些复杂依赖或者性能问题时,坑往往会很深。
#####环境结构
在生产环境下
- 请求的静态资源数量尽可能少,每个文件的体积尽可能小
- 所有的静态文件放在CDN上,而且都是永久缓存
- 每次部署后,客户端都会去请求修改过的文件,不会因为本地缓存导致不更新的问题
- 优化到最后一个空格
在开发环境下
- 所有的JS/CSS都是独立的,debug版本,便于调试
- 所有的公共JS库都有版本控制,而且可以随时无痛升降级
- coffee或者scss,会实时转换成最新的js/css
- AMD/CMD形式的开发,解决繁琐的js依赖问题
怎么实现的
- requireJS
- 解决js模块依赖问题
- requireJS optimizer打包使用require的js
- grunt
- watch: 监控sass文件和coffee文件,实时转换为css和js
- uglify: 合并多个js文件,并且压缩之
- filerev: 重命名文件,为其加上md5码
- htmlmin: 压缩我们的后端模板文件
#####实现效果
开发环境下:
生产环境下:
#####流程
先列下我们的项目文件结构,真实情况下还有很多别的文件夹,这里就先不显示了。
app/views/: 后端的模板 app/dist-views/: 生产环境下的后端模板,后端模板经过处理后会被放到这里
front/: 这里放置所有的前端代码,fonts放置字体,scripts放置js,styles放置样式,images放置图片,其中,scripts里有三大块:1.业务代码,cow目录中;2.开源js库,verdor目录中;3.require_config.js,这个是requireJS的config文件,用于开发环境下的前端和生产环境打包。PS:这里的代码不能通过路由访问到,这也考虑到了源码的安全问题。
grunt/: 这里放置一些grunt的任务,由于grunt的任务太多了,分散下好管理一点
public/: 这里放置一些生产环境下打包或者处理过的资源
######开发环境下
前端开源JS库统一使用bower管理,通过在项目根目录下配置.bowerrc,可以设置bower的库的安装位置:
{
"directory" : "front/assets/scripts/vendor/"
}
然后,就可以通过类似于bower install jquery --save 来安装相应的库了,当然,bower install后也可以直接跟github的git,比如https://github.com/lodash/lodash.git
。具体使用,请参考bower, 安装过的库都在bower.json里,内容很简单,就不贴出来了。
BTW,front/scripts/vendor这个目录应该加到.gitignore里去,因为这里面文件很多,但是又是可以从网上直接下载,所以没必要加入版本控制。pull下代码后,只需要在项目根目录跑一下bower install 就OK。切换版本的话,只需要在bower.json里修改下版本号,然后再跑bower install即可,特别赞。
前端代码里,写起来一些正常,不必操心生产环境的事,比如:
coffee和sass转换:使用grunt里的3个库grunt-contrib-watch & grunt-contrib-coffee & grunt-contrib-sass即可实现.
#####生产环境下
在部署生产环境的时候,执行一条grunt任务就可以了,虽然只有一条命令,看起来很简单,但是却做了好多事情:
-
转换coffee & sass, 使用上面所讲的两个grunt插件grunt-contrib-coffee/sass
-
清理public/dist目录,这不是必须的,但是可以避免一些垃圾文件或者潜在的问题
-
复制front里的样式、图片、字体,注意,这里木有js,js是单独处理的
-
复制app/views里的模板到app/dist-views/中,因为这里要替换掉一些静态资源的路径和tag,而且不能影响到开发环境下的开发。
-
替换掉后端模板app/dist-views/里的标签
可能你注意到了,上面的一堆script脚本是被一个类似于
build:js:libs /static/dist/assets/scripts/libs.js
和endbuild
的标签包括起来的,这两个标签是用来供正则匹配的:- build:js纯粹是个标记,这个标记借鉴了grunt-usemin,libs是要生成的uglify任务名,后面的路径是要替换掉这一堆script元素的script标签的路径
- 匹配到中间的一堆script标签,然后取出来这些js的路径
- 再把这些路径转化成相对路径,供grunt config使用
- 通过代码和这些路径,生成一个grunt uglify任务供打包js开源库,这里会生成一个叫libs的grunt-uglify任务
- 生成的文件会放到
public/dist/assets/scripts/libs.js
里,这是一个压缩的文件,它打包了所有我们需要用到的开源库 - 再把这一堆的script标签替换为
<script src='/static/dist/assets/scripts/libs.js'></script>
- ok,现在生产环境下的模板里的开源库的一堆的script不见了,只剩一个
/blah/libs.js
了 - 如果任务名叫做“remove”的话,该段落完全被删除,因为生产环境下,通过打包和一系列操作,我们可以去掉requireJS了,requireJS的config也不要了,所以要删掉,但是开发环境下要用,所以有必要酱紫
- 开发环境下,我们通过requireJS启动模块需要用到
require(['mudoleName'])
的方式,这在生产环境下同样需要去掉,因为最后我们打包出来的js只有一个moduleName.js
了,直接使用script标签引入就可以,所以,如果任务名叫做replace
,那么不生成任何uglify任务,仅替换为响应的script标签即可
相关代码较为复杂,这里就不贴出来了,yoeman出品的grunt-usemin实现了类似的功能,我曾尝试过,感觉还是不够灵活,我觉得在前端代码中路径统一使用绝对路径会比较方便好维护,grunt-usemin似乎无法达到我要的绝对路径的效果,不如自己手动写代码来的方便,所以就有了以上复杂到蛋疼的逻辑
-
接下来就要跑上面生成好的libs任务了,grunt-uglify会自动打包所有的js并且压缩成
var a,b,c,d
这种效果,最后输出到libs.js
这个文件 -
接下来是
requirejs
来接手,r.js会把需要启动的模块按照依赖关系全部打包到一个js中去,实在比seajs的打包工具spm要方便很多,这里的打包逻辑参考了jquery源码中的打包逻辑,贴个r.js的配置:baseConfig = { baseUrl: "front/assets/scripts/" mainConfigFile: "front/assets/scripts/require_config.js" optimize: "uglify2" preserveLicenseComments: false findNestedDependencies: true skipSemiColonInsertion: true optimizeAllPluginResources: true rawText: {} onModuleBundleComplete: (data) -> amdclean = module.require('amdclean') outputFile = data.path cleanedCode = amdclean.clean({ 'filePath': outputFile }) fs.writeFileSync(outputFile, cleanedCode); }
grunt生成任务的代码:
grunt.registerMultiTask( "build", "build list.js account.js pad.js with requirejs optimizer.", -> done = this.async() dest = this.data.dest name = this.data.name config = _.assign({}, baseConfig, { name: name, out: dest }) config.wrap = { start: '', end: ";require(['#{this.data.name}'])" } requirejs.optimize( config, (response) -> grunt.log.ok( "File '" + dest + "' created." ) done() (err) -> grunt.log.error('err') done( err ) ) )
再来一个grunt config代码,要打包什么文件,直接加这里就行,其他地方都不用动
build: { acount: name: 'cow/account/account' dest: 'public/dist/assets/scripts/account.js' personal: name: 'cow/account/personal' dest: 'public/dist/assets/scripts/personal.js' list: name: 'cow/list/list' dest: 'public/dist/assets/scripts/list.js' pad: name: 'cow/pad/boot' dest: 'public/dist/assets/scripts/pad.js' }
注意:打包后生成的js是没有启动代码的,所以需要在配置里加一个
wrap
:;require(['#{this.data.name}'])"
,这样就可以启动了此外:这里用到了一个可以清除requireJS痕迹的库amdclean,打包完成后,会清除掉requirejs代码,这样生产环境下就不再需要requirejs了,js体积也小了一点
-
重命名所有的public/dist下的所有静态资源,这个是为了解决缓存问题的,由于我们的静态资源是nginx处理的,过期时间设置的无限,这样就会导致这样的问题:服务器这边修改了a.js,但是客户那边a.js有缓存的,不会去请求这个新的a.js,这个时候就会出问题;还有,如果服务器上要存多个版本的静态文件,用传统的在文件后加
?v=1.2.1
这样的方式也很难解决。我们的做法是,使用grunt-filerev这个插件,通过计算文件内容的md5,加在文件名(不是扩展名后)后,变成类似于a.234ab3df.js这样的格式,如果在一次部署中,这个文件改变了,那么在filerev后这个带md5后缀的名字也会改,那么对于客户端来说这是一个新的静态资源,浏览器里会从服务器获取它;如果某个js文件没有修改,那么生成的带md5的这个文件名也还是老样子,浏览器就会依然使用缓存。 -
所有的静态文件都被重命名了,那么我们的所有静态资源引用就全部失效了,那些
<script src='blah/libs.js'></script>
就全部404了,这个怎么破?上面的任务在跑完后会生成一个对象叫做grunt.filerev.summary
,里面存放的是重命名前和重命名后文件名的对应关系,通过这个关系,我们通过正则表达式来替换掉所有js、css文件以及app/dist-views/里模板里的引用标签,这样就不会有404的错误了,另外,如果用上了CDN的话,这里会直接替换成对应的CDN的路径。 -
最后,通过grunt-htmlmin来压缩下app/dist-views/里的模板,去掉一些空格和换行,使源码看起来更专业一点。贴个配置出来:
htmlmin: { views: { options: { removeComments: true collapseWhitespace: true minifyJS: true minifyCSS: true conservativeCollapse: true preserveLineBreaks: true }, files: [ { cwd: 'app/dist-views/' expand: true src: [ '**/*.html' ] dest: 'app/dist-views/' filter: 'isFile' } ] } }
至此,在grunt配置中定好这一系列顺序,创建一个新的任务,我这里叫pro,每次部署生产环境的时候跑下grunt pro就可以了,开发环境下一切照旧,不存在环境切换的问题。
####后端篇
石墨后端使用的技术是NodeJS + MySQL + Redis。
#####NodeJS 使用NodeJS的原因是(希望)它能承载更高的并发,而且对websocket的支持非常好。石墨前端文件夹和文档页面使用的是单页面技术,文档内容的实时同步、标题的重命名、通知等消息都是通过socket.io实时推送。
#####MySQL 石墨的小伙伴们对传统关系型数据库比较熟悉,而且多层的文件夹结构似乎更适合关系型数据库,所以数据库选择上比较保守的选择了MySQL。
#####Redis Redis是用来做缓存加持久层用的。在多人同时编写一个文档时,会产生大量的文档改动记录,这些记录不但会用来做一些协同工作,也会保存起来生成文档的历史改动信息。所以有必要持久化。但是这些数据数量上十分巨大,如果直接写入到数据库中,会对数据库产生大量的写入操作,所以我们这里的写入策略是先写入到Redis中,等数据库空闲的时候再把较老的文档改动记录写回到数据库中。写回数据库的原因是Redis的数据都是放内存的,那些读取概率极小的老数据占内存是没必要的。