Skip to content

Instantly share code, notes, and snippets.

@wanming
Created July 6, 2015 07:27
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save wanming/61365afc17b6cfed5487 to your computer and use it in GitHub Desktop.
Save wanming/61365afc17b6cfed5487 to your computer and use it in GitHub Desktop.
石墨架构概览

###石墨整体架构一览 ####前端篇

#####框架 石墨的前端使用的框架很传统: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: 压缩我们的后端模板文件

#####实现效果

开发环境下:

dev_source

dev_chrome

生产环境下:

pro_source

pro_chrome

#####流程

先列下我们的项目文件结构,真实情况下还有很多别的文件夹,这里就先不显示了。

file_tree

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即可,特别赞。

前端代码里,写起来一些正常,不必操心生产环境的事,比如: tpl_html coffee和sass转换:使用grunt里的3个库grunt-contrib-watch & grunt-contrib-coffee & grunt-contrib-sass即可实现.

#####生产环境下

在部署生产环境的时候,执行一条grunt任务就可以了,虽然只有一条命令,看起来很简单,但是却做了好多事情:

  1. 转换coffee & sass, 使用上面所讲的两个grunt插件grunt-contrib-coffee/sass

  2. 清理public/dist目录,这不是必须的,但是可以避免一些垃圾文件或者潜在的问题

  3. 复制front里的样式、图片、字体,注意,这里木有js,js是单独处理的

  4. 复制app/views里的模板到app/dist-views/中,因为这里要替换掉一些静态资源的路径和tag,而且不能影响到开发环境下的开发。

  5. 替换掉后端模板app/dist-views/里的标签

    可能你注意到了,上面的一堆script脚本是被一个类似于build:js:libs /static/dist/assets/scripts/libs.jsendbuild的标签包括起来的,这两个标签是用来供正则匹配的:

    1. build:js纯粹是个标记,这个标记借鉴了grunt-usemin,libs是要生成的uglify任务名,后面的路径是要替换掉这一堆script元素的script标签的路径
    2. 匹配到中间的一堆script标签,然后取出来这些js的路径
    3. 再把这些路径转化成相对路径,供grunt config使用
    4. 通过代码和这些路径,生成一个grunt uglify任务供打包js开源库,这里会生成一个叫libs的grunt-uglify任务
    5. 生成的文件会放到public/dist/assets/scripts/libs.js里,这是一个压缩的文件,它打包了所有我们需要用到的开源库
    6. 再把这一堆的script标签替换为<script src='/static/dist/assets/scripts/libs.js'></script>
    7. ok,现在生产环境下的模板里的开源库的一堆的script不见了,只剩一个/blah/libs.js
    8. 如果任务名叫做“remove”的话,该段落完全被删除,因为生产环境下,通过打包和一系列操作,我们可以去掉requireJS了,requireJS的config也不要了,所以要删掉,但是开发环境下要用,所以有必要酱紫
    9. 开发环境下,我们通过requireJS启动模块需要用到require(['mudoleName'])的方式,这在生产环境下同样需要去掉,因为最后我们打包出来的js只有一个moduleName.js了,直接使用script标签引入就可以,所以,如果任务名叫做replace,那么不生成任何uglify任务,仅替换为响应的script标签即可

    相关代码较为复杂,这里就不贴出来了,yoeman出品的grunt-usemin实现了类似的功能,我曾尝试过,感觉还是不够灵活,我觉得在前端代码中路径统一使用绝对路径会比较方便好维护,grunt-usemin似乎无法达到我要的绝对路径的效果,不如自己手动写代码来的方便,所以就有了以上复杂到蛋疼的逻辑

  6. 接下来就要跑上面生成好的libs任务了,grunt-uglify会自动打包所有的js并且压缩成var a,b,c,d这种效果,最后输出到libs.js这个文件

  7. 接下来是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体积也小了一点

  8. 重命名所有的public/dist下的所有静态资源,这个是为了解决缓存问题的,由于我们的静态资源是nginx处理的,过期时间设置的无限,这样就会导致这样的问题:服务器这边修改了a.js,但是客户那边a.js有缓存的,不会去请求这个新的a.js,这个时候就会出问题;还有,如果服务器上要存多个版本的静态文件,用传统的在文件后加?v=1.2.1这样的方式也很难解决。我们的做法是,使用grunt-filerev这个插件,通过计算文件内容的md5,加在文件名(不是扩展名后)后,变成类似于a.234ab3df.js这样的格式,如果在一次部署中,这个文件改变了,那么在filerev后这个带md5后缀的名字也会改,那么对于客户端来说这是一个新的静态资源,浏览器里会从服务器获取它;如果某个js文件没有修改,那么生成的带md5的这个文件名也还是老样子,浏览器就会依然使用缓存。

  9. 所有的静态文件都被重命名了,那么我们的所有静态资源引用就全部失效了,那些<script src='blah/libs.js'></script>就全部404了,这个怎么破?上面的任务在跑完后会生成一个对象叫做grunt.filerev.summary,里面存放的是重命名前和重命名后文件名的对应关系,通过这个关系,我们通过正则表达式来替换掉所有js、css文件以及app/dist-views/里模板里的引用标签,这样就不会有404的错误了,另外,如果用上了CDN的话,这里会直接替换成对应的CDN的路径。

  10. 最后,通过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的数据都是放内存的,那些读取概率极小的老数据占内存是没必要的。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment