OnceOA模块开发实例:用OnceIO和OnceDB搭建开源技术论坛

OnceOA OnceIO OnceDB 二次开发 by wx_15926 on 1575305390567


OurJS原有架构博客是基于文件系统的。优点是不需要配置数据库。所有文章在初始化时都会加载到内存中,能够支承大流量的访问。

但随着文章数量的增加,初始化启动时,硬盘IO读写会有一定的压力,再加上一些云服务器限制了IO频率。造成启动时间较慢。

这篇文章将介绍如何将OurJS博客移到到OnceOA架构。

相关源码:ourjs 网站模块的源码 https://github.com/oncedoc/onceoa-modules

OnceOA 优点

OnceOA是基于模块化Web框架和 OnceIOOnceDB 内存数据库构建。OnceDB在Redis基于上添加了全文搜索、关系查询、索引、聚合计算等指令,结合oncedb的node.js驱动,能在模块中动态定义数据库表、字段。OnceOA可以让开发者在不更改原系统的情况下,在新模块中对系统整体进行深度更改和定制。

  1. 数据库和业务逻辑分离,网站数据、Session保存在OnceDB中,重启升级只需几秒,用户几乎感觉不到系统升级。
  2. OnceOA已经积累了丰富的扩展模块,支持Markdown、图床、文件管理、短信。可与多个网站、应用共用微信认证公众号,微信扫码、支付等事件可通过messengerdb独立分发给各个应用网站,实现独立的扫码登录,支付等功能。
  3. 对于一些技术类文件可直接编辑Html或Markdown嵌入博客,实现一些复杂的动态效果。并支持将模块目录映射到 OnceDoc 开发环境中,直接在线编辑源码,重启进程后立即生效。
  4. 各个功能的前端、后端、数据库扩展定义在一个模块扩展包中。各功能彻底隔离。功能扩展不需要改更原系统代码。不加载模块就不会增加系统和数据库的复杂性,影响原系统的使用。
  5. 通过模板映射复写、路由映射重定向。在扩展模块中就可深度定制原系统的图标、标题、网站风格,不影响系统升级。禁掉扩展模块原系统则会恢复正常。

OnceIO 模块化Web框架

下面以OurJS模块为例,说明如何创建一个OnceIO模块。

创建OurJS模块

首先在 /onceai/oncedoc/mod 目录下创建一个新目录 ourjs

package.json

与node.js的module一样。在模块目录是创建 package.json 描述模块名称和主程序入口,如下图所示。

ourjs_module.png

package.json 内容如下:

{
  "description": "Our JavaScript 网站迁移", 
  "version": "2.1.0",
  "icon": "/ourjs/img/ourjs.png",
  "main": "./svr/ourjs.main"
}

main描述加载该模块的入口程序,此项必填。

  • main: 是主程序的入口 [必须]
  • description: 是对该模块的描述 [可选]
  • icon: 在应用中心显示的图标 [可选]

ourjs下面有3个目录

  • web: 模块前端和静态资源目录,存放javascript/css/图片和模板文件
  • svr: 模块后端代码
  • data: 是 ourjs 原始文章数据,这个目录是从旧版ourjs复制而来,如果不需要从ourjs迁移旧文章数据的可忽略。

这里的main 是 ourjs/svr/ourjs.main.js 文件,省略了扩展名,

定义模块和静态资源目录: app.mod

ourjs.main.js 文件中:

模块定义如下::

app.mod('ourjs', '../web')  

模块名称是ourjs,建议使用跟模块目录一样的名称。静态资源目录因为相当于当前 svr/ourjs.main.js 的上一级,因此使用 ../web

定义之后就能像 package.json 中的 icon 那样。使用 /ourjs/img/ourjs.png 相对地址来访问 ourjs/web/img/ourjs.png 的静态资源图片。

模板嵌套和模板预加载: app.pre

在 OnceIO 中模板嵌套采用 include 语法,如

<!--#include="/blog/blog.navbar.part"-->

OnceIO 默认使用 doT 模板引擎,并添加自定义的模板文件引用规则:

  • 绝对地址引用:/blog/blog.navbar.part 以绝对地址 / 开头,代表引用 blog 模块静态资源目录下的 blog.navbar.part 模板文件,不管在哪一个模块中引用都会从blog模块下读取。不同模块间的模板可相互引用。
  • 相对地址引用:OnceIO 通过当前访问网址判断访问的是哪一个模块,比如访问 127.0.0.1:8064/blog/home 时,访问的就是blog模块下载的模板文件。如模板引用中不加其实模块地址前辍如 <!--#include="blog.navbar.part"-->,或渲染中使用 res.render('blog.home.part', {}),则会从当前访问的 blog 模块中读取这些模板文件。

app.pre 可以预加载模块中的模板。

include文件的加载跟 Express 框架采用阻塞代码读取模板内容不同。OnceIO是全缓存的,如果是第一次加载,会直接返回缓存中的空内容,然后再在异步读取模板内容到缓存。因此第一次刷新时嵌套的模板会显示空。

为了防止在重启后第一次加载时页面会显示不完整,可在启动时使用 app.pre 预加载所有的模板到缓存,

app.pre('ourjs', '.part')
app.pre('ourjs', '.tmpl')

*.part 扩展名,在 onceoa 中一般代表一段模板并无特殊含意,而 *.tmpl 是完整的 html 文件。您可以自己的扩展名定义模板。

也可不加 app.pre,仅会影响重启后第一位用户的访问。

模板映射: res.template

OnceOA中的模板文件都是不加密的,可以根据需要进行二次开发和定制。但为了可维护性,OnceIO设计了一种模板文件映射的方法,您可以将您需要修改的模块文件复制到一个新的模块目录中,并通过 res.template 将原模板引用到新的模板文件。这样系统升级也不会对您定制的功能造成影响,并且只要禁用您扩展的模块,系统原功能就能恢复正常。

比如,我们可以采用模板映射的方法,将 OnceOA 系统的 site.header.part,site.footer.part 模板映射到 ourjs 模块下,修改原网站的整体风格,添加ourjs自己的网站图标、标题。res.template通过filter(middleware)来设置,如。

app.use('/', function(req, res) {
  res.template({
      // '/ask/ask.nav.part'         : '/ourjs/ourjs.nav.part'
    //, '/ask/ask.list.tmpl'        : '/ourjs/ourjs.list.tmpl'
      'ask.list.tmpl'             : '/ourjs/ourjs.list.tmpl'
    , '/pay/pay.header.part'      : '/ourjs/ourjs.nav.part'
    , '/site.header.part'         : '/ourjs/ourjs.nav.part'
    , '/site.footer.part'         : '/ourjs/ourjs.footer.part'
    , '/blog/blog.navbar.part'    : '/ourjs/ourjs.nav.part'
    , '/blog/blog.header.part'    : '/ourjs/ourjs.header.part'
    , '/blog/blog.footer.part'    : '/ourjs/ourjs.footer.part'
    , '/blog/blog.home.tmpl'      : '/ourjs/ourjs.home.tmpl'
    , '/blog/blog.rss.tmpl'       : '/ourjs/ourjs.rss.tmpl'
    , '/blog/blog.view.tmpl'      : '/ourjs/ourjs.view.tmpl'
    , '/analytics.tmpl'           : '/ourjs/ourjs.analytics.tmpl'
    , '/site.script.tmpl'         : '/ourjs/ourjs.script.tmpl'
  })

  //判断是否为微信,模板文件可通过 it.isWechat 来判断
  if ((req.headers['user-agent'] || '').indexOf('MicroMessenger') > 0) {
    res.model('isWechat', true)
  }

  req.filter.next()

})

这里匹配的是所有路径 '/',当匹配所有请求时此参数可省略。这样设置以后,访问 http://127.0.0.1:8064/blog/home 就会使用使用自定义的模板文件来渲染。

模板数据: res.model

上面的模板映射中间件还添加了 isWechat 属性,在微信客户端访问时值为true。如果您想在映射的模板中显示自定义的Model数据,也可通过filter(middleware)来添加,比如在访问 /blog/home 时添加突发新闻,则就这样设置:

app.use('/blog/home', function(req, res) {
   var breakingNews = []
   //异步填充breakingNews,省略。
   res.model('breakingNews', breakingNews)
   req.filter.next()
})

然后在 /ourjs/ourjs.home.tmpl 模板文件中可通过 it.breakingNews 访问这些数据。

网址映射:确保原网址可用性

OurJS 网站已经有一段时间的历史,保证原有网址可用性非常重要,能确保搜索引擎的历史流量不流失。

例如如下访问地址,就要保证移植后网页也用类似形式访问:

/detail/551b9b0529c8d81960000007
/detail/593658adf1239006149616c1
/bbs/
/userinfo/ourjs
/bbs/JavaScript

在OnceOA博客系统中,博客主页地址是 /blog/home,文章的访问地址是 /blog/view/:article,router路由形式就要做如下映射了。

/blog/view/:articleid => /detail/:articleid
/blog/user/:username => /userinfo/:username

路由映射 app.map

OnceIO 提供直接路由映射的方法,

路由映射需要其它模块均加载好才能找到匹配的路由路径,因此需要放在 OnceDoc.on('ready', callback) 中,等待所有模块都加载好才能映射。还有一种方法是添加一点延时(0秒也可)。

OnceDoc.on('ready', function() {
  .... 
  app.map({
      '/blog/home/:keyword'   : '/home/:keyword'
    // , '/blog/view/:id'        : '/detail/:id'
    , '/blog/rss/:keyword'    : '/rss/:keyword'
    , '/ask/key/:key'         : '/bbs/:key/:pager'
    , '/blog/user/:poster'    : '/userinfo/:poster'
  })
})

打印所有路由表达示:

路由映射的使用前提是知道原有的路由表达式,可通过下面的方式获取,注意此语句最好也放到 OnceDoc.on('ready', callback) 中执行,否则在此ourjs模块之后加载的模块可能不在 app.handlers 列表中。

console.log(app.handlers)

路由映射的原理非常简单,OnceIO的路由表达式支持字符串、正则和数组。路由映射相当前将原来的字符串变成成了路组。只要匹配其中任意一个,就会执行定义的handler。

在 blog\svr\blog.article.js,查看文章和用户文章列表的 handler 是这样定义的:

app.url('/blog/user/:poster', function(req, res) { ... })

'/blog/user/:poster' : '/userinfo/:poster' 映射以后,在 app.handlers中看到的是

...
{ expression: [ '/blog/user/:poster', '/userinfo/:poster' ],
    handler: [Function],
    file: 'mod\\blog\\svr\\blog.article.js',
    loose: true },
...

打印当前路由表达示:

在handler中,可通过 req.router 来判断当前匹配的是哪一个表达示:

app.url(['/blog/home/:keyword', '/blog/rss/:keyword'], function( req, res ) {
    console.log(req.router)
    console.log(this)
})

handler中的this则是当前路由的mapper设置

> /blog/home/:keyword
> { expression:
     [ '/blog/home/:keyword',
       '/blog/rss/:keyword',
       '/home/:keyword',
       '/rss/:keyword' ],
    handler: [Function: showListHandler],
    file: 'mod\\blog\\svr\\blog.article.js',
    loose: true }

自定义handler

ourjs中有些文章还能通过urlSlug的方式访问。

/detail/node-js编码规范指南教程-教你优雅地写javascript代码

这个设计是有缺陷的,因为标题的改变会影响访问地址,目前已经改成 /detail/:id/:title 的方法,并且 :title 可省略,

http://ourjs.com/detail/546c4b3fbc3f9b154e00004a/node-js编码规范指南教程-教你优雅地写javascript代码

但是原有访问地址已经被搜索引擎收录,也会带来很多流量,因此这里对 /detail/:显示文章内容的handler做了定制。您可在 ourjs.main.js 中看到相应的代码。

OnceDB扩展

OurJS系统中有一些额外的字段,比如用户的 company 和 urlSlug

company是一个显示字段,直接对 user 进行 extend 即可。

  oncedb.extend('user', {
      company : ''
  })

url_slug 比较复杂,这里定义了一个 unique 属性:

oncedb.extend('article', {
    //已知问题,注意反斜杠,因为在字符串里面需要2个进行转义
    title   : 'unique("article_url_slug", (this.title || "").toLowerCase().replace(/[^\\w\\u4e00-\\u9fa5]+/g, "-"))'
})

unique的key是article_url_slug,值是将标题去除特殊字段的结果。可在Redis客户端中连接OnceDB,查看最终结果:

自定义首页

通过设置 defaultUrl/ afterLoginUrl 自定义默认和登录后的首页

  MAIN_CONFIG.defaultUrl      = '/home'
  MAIN_CONFIG.afterLoginUrl   = '/home'

自定义翻译

OnceOA通过 it.local 显示翻译文件,因此可直接更改翻译字符串,而无需映射更改模板文件。

  LOCAL.BRAND             = 'Our<b>JS</b>'
  LOCAL.BRAND_TEXT        = 'Our<b>JS</b>'
  LOCAL.SITE_BRIEF_TITLE  = 'OurJS'
  LOCAL.ASK_TITLE         = 'OurJS 爱我技术 我们的技术-IT文摘 JavaScript社区 Node.JS社区 前端社区 全端论坛 MongoDB html5 CSS3 开源社区'
  LOCAL.BLOG_TITLE        = LOCAL.ASK_TITLE

  LOCAL.FOLLOW_US_NAME    = 'OnceJS'
  LOCAL.FOLLOW_US_HTML    = '<img src="http://onceoa.com/ask/img/oncedoc.jpg" alt="OnceOA" width="160" height="160" style="border: solid 1px #666;">'

OurJS 文件夹映射到 OnceDoc 开发环境

OnceOA 支持将模块目录映射到 OnceDoc 中,直接在线编辑源码,只需要一行代码,注意这里的路径是绝对路径,这与 app.map / res.template 的映射不同。

OnceDoc.Folder.addMapping({ text: 'OurJS', path: path.join(__dirname, '../') })

登录后访问 /oncedoc,可看到左侧目录树多了一个 ourjs 团队文件夹,可直接打开代码文件,在线编辑,效果如下。

ourjs_ide.png

编辑好后,在 /onceos 中重启进程即可生效。

启用 OurJS 模块

模块下载后,可访问 /onceos 点击应用中心,在已禁用中找到 ourjs,并点击启用,即可。

ourjs_enable.jpg

数据迁移

ourjs/svr/ourjs.migrate.js 文件是将 OurJS 原有文章迁移到 OnceOA的自动化脚本,直接访问 /ourjs/migrate 即可启动移到,此时会自动读取 ourjs\data\models 下的数据文件添加到 OnceOA。

app.get('/ourjs/migrate', function(req, res) {
  setUsers()
  setArticle()

  res.send('Start migration')
})

您也可以只迁移文章,只需将 setUsers() 注释掉即可。


上一篇: OnceIO中间件