基于 Restful API 的 Content Management System 的开发

Index:

- 功能需求分析 && DB model design

- Architecture design

- Project directory architecture]

- Major module explain

- 工程化

项目Github源码

此文章只讲一些比较重要的内容 具体的实现请查看源码
个人认为这是一个写的很优雅的后端项目


<a id="title01"></a>
<p> </p>

功能需求分析 && DB model design

Paste_Image.png

diagram created using Scapple

从上图中 设计了官网的所有功能 有:

  • columns: case studies(home也整合在此column)、 careers、 news 这四个 column
  • post management
  • member management
  • User
  • API

以及 Database 的 schema

<a id="title02"></a>
<p> </p>

Architecture design

Platform:
项目基于 Node.js platform

Web Framework:
使用 Express

View:
使用 ejs (embeded javascript) 因为页面内容并不复杂 所以使用传统的服务器端渲染后端页面
使用 semantic ui 作为前端UI框架 虽然这框架体积很大 只兼容高级浏览器 但UI模块丰富 适合公司内部系统使用

Storage:
Content Database 由于 content 没有复杂的 relationship 所以使用了便于操作 性能优越的 Mongodb NoSQL

Session Database 使用读写性能很高的 Redis

<a id="title03"></a>
<p> </p>

Project directory architecture

Project directory architecture

以上是工程目录 接下来依次向下讲解核心文件夹


controllers:

controllers

此文件夹创建了许多 Data class 以 Object-Oriented Programming 的形式来操作数据对象 操作 data model

For example:

Paste_Image.png

lib

lib

此文件夹创建了许多 helper functions 方便编写逻辑 middleware/ 文件夹中创建了两个用来检测用户权限 与 读取数据的 middleware


models

models

此文件夹创建了许多 Data models 因为本项目主要使用 Mongodb 所以使用了 Mongoose Database framework 来轻松的操作 mongodb


public

public

此文件夹作为后端的 Static resources directory 用户上传的文件也会存放在此文件夹下的 uploads/ 文件夹下


routes

routes

此文件夹根据需求 创建了许多 router 文件 我们会主要使用此文件夹下的 routes.js 这个 major router 来组织其他的 router 然后提供 routes.js 给 app.js 来使用


views

views

此文件夹存放着服务器端的页面 template 文件 用来提供给 ejs 引擎去渲染成HTML页面

common/ 文件夹中放置着页面内公共的部分 以及 head foot 两个 ejs 文件 分别放置着 link 以及 scripts


<a id="title04"></a>
<p> </p>

Major module explain

接下来会讲解一些这个项目中的核心模块的思路

创建文章

创建文章一共有四个栏目 其中 home 跟 case studies 栏目合并成一个栏目

所有文章的基本结构都是相同的 标题 正文 发帖时间.. 不同栏目会有不同栏目特殊的字段 我们会为这些不同种类的文章 创建不同的 controller 使用这些 controller 去控制文章数据

因为文章的基本结构都是相同的 所以我们会创建一个 Entry.js 在 controllers/ 文件夹 来作为所有类型文章的 base class

For example:

Entry
Case
Career

<p> </p>
创建文章 的 Router 方面 由于各栏目创建文章的程序不是很复杂 所以我们将 创建文章功能 集合到了一个 submit route 中 通过统一的 endpoint 来操作数据

我们使用 formidable middleware 来获取用户 form 传输过来的 data 使用这些 data 来创建文章

创建文章 我们不使用 modelHelper module 来获取对应的 data model 因为 在创建文章时会调用 Entry instance 的 save method 在 save method 中 已经包含了对应的 data model 用来存储文章

通过 request.body 中的 entry_type field 在创建 文章 instance 的时候 来选择对应的 entry class

exports.submit = function (app) {
    return function (req, res, next) {
        //  specified upload dir
        var uploadDir = app.get('root') + '/uploads';
        if (!fs.existsSync(uploadDir)) fs.mkdirSync(uploadDir);

        //  handle incoming form data
        var form = new formidable.IncomingForm();
        form.uploadDir = uploadDir;
        form.maxFieldsSize = 10000 * 1024 * 1024;

        //  parse request body data
        form.parse(req, function (err, fields, files) {
            if (err) return next(err);

            //console.dir(fields);
            //console.dir(files);

            //  rename file
            for (var i in files) {
                //  create union file name
                if (i.indexOf('entry_') >= 0 && files[i].size) {
                    console.log(i);
                    files[i].name = new Date().getTime() + Math.random().toFixed(5)*100000 + '.' + files[i].type.split('/')[1];
                    fs.rename(files[i].path, form.uploadDir + '/' + files[i].name);
                }
            }

            var entry = {
                type: fields.entry_type,
                date: new Date(fields.entry_date),
                title: fields.entry_title,
                body: fields.entry_body
            };

            //  column match via entry_type field
            switch (fields.entry_type) {
                case 'case':
                    entry.homeThumbSrc = '/uploads/' + files.entry_home.name;
                    entry.homeThumbMobileSrc = '/uploads/' + files.entry_home_mobile.name;
                    entry.caseStudiesThumbSrc = '/uploads/' + files.entry_case.name;
                    entry.caseStudiesThumbMobileSrc = '/uploads/' + files.entry_case_mobile.name;
                    entry.homeBlockColor = fields.entry_color;
                    entry.homeBtnColor = fields.entry_btn_color;
                    entry.order = fields.entry_order;
                    entry.pushHome =false;
                    entry = new Case(entry);
                    break;
                case 'career':
                    entry = new Career(entry);
                    break;
                case 'news':
                    entry = new News(entry);
                    break;
                default:
                    return next(new Error('Invalid entry type'));
            }

            entry.save(function (err) {
                if (err) return next(err);
                console.log('entry saved');
                res.status(201);
                res.redirect('/entry?state=201');
            });
        });
    }
};

更新文章

更新文章也跟创建文章的组织方式相同 所有栏目的文章更新功能 共用一个 router 通过统一的 endpoint 来操作数据

通过 request param 的 column 值 使用(using modelHelper module)对应的 entry model 来执行更新操作


modelHelper

通过 request path 中的 column 来获取对应的 column data model

在更新文章 删除文章之类的文章操作中 会用到此 helper function

eg: 以下 command 会使用 case 的 data model 来删除指定的 id 56fde6b0bc56ea0057b6707d 此条数据

$ curl http://localhost:4000/entry/56fde6b0bc56ea0057b6707d?column=case

modelHelper module 有 history功能 它会记录你上次使用 modelHepler 的 column

如果你使用 moduleHelper 时 request param 中不包含 column 信息的话 它会使用 history中记录的 column 或者 默认使用 case column 如果 history不存在

modelHelper
exports.delete = function (req, res, next) {
    //  Is valid _id object format?
    var id = req.param('id');
    id = id ? id.match(/^[0-9a-fA-F]{24}$/) : '';

    if (!id) {
        res.status(400);
        res.send({message: 'Invalid entry id'});
        return;
    }

    //  Using model helper just passing req object to this function
    modelHelper(req, function (err, model) {
        if (err) return next(err);

        Entry.delete(model, id, function (err, entry) {
            //  remove logic
        });
    });
};

tagsHelper

标签切换 根据当前的 request path 以及 request body 中的 column 使用不同参数的标签

tagsHelper

tagsHelper
module.exports = function (path, column, context) {
    context.topic = '';
    context.tags = [
        { url: '?column=case', text: 'Case'},
        { url: '?column=career', text: 'Career'},
        { url: '?column=news', text: 'News'}
    ];

    //  fill the url via path
    context.tags.map(function (tag) {
        return tag.url = path + tag.url;
    });

    //  get context's topic
    context.tags.forEach(function (tag, index, arr) {
        if (tag.text.toLowerCase() == column) {
            return context.topic = arr.splice(index, 1)[0];
        }
    });

    return context;
};

usage:

exports.form = function (app) {
    return function (req, res, next) {
        var column = req.param('column');
        var context = {};

        //  init page tags
        context = tagsHelper(req.path, column, context);
        req.session.entryColumnHistory = column;

        res.status(200);
        res.render('page', context);
    }
};

<a id="title05"></a>
<p> </p>

工程化

Node.js 提供了基于 environment variable 来运行的系统 所以我们通过设置 NODE_ENV 这个 environment variable 来运行不同环境下的 app.js 程序

$ NODE_ENV=development node app.js
$ NODE_ENV=production node app.js

app.js 中 通过启动时设置的 NODE_ENV environment variable 来执行不同的 运行配置

if (environment == 'production') {
    app.set('path', 'admin.sisobrand.com:4000');
}

if (environment == 'development') {
    app.set('path', 'localhost:4000');
}

服务器端页面API在不同环境下的使用

通过给 页面公共部分 添加一个 input:hidden 标签 来保存当前的 API path

eg: ejs模板引擎来实现上述功能

<!-- global variables -->
   <% if (globalVariables) { %>
        <input type="hidden" name="gv_path" value="<%=globalVariables.path%>"/>
   <%}%>
<!-- end global variables -->
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,590评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,808评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,151评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,779评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,773评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,656评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,022评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,678评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,038评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,756评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,411评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,005评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,973评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,053评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,495评论 2 343

推荐阅读更多精彩内容