美文网首页
基于 Restful API 的 Content Managem

基于 Restful API 的 Content Managem

作者: 爱喝百岁山的狼 | 来源:发表于2016-04-19 17:27 被阅读0次

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 -->

相关文章

网友评论

      本文标题:基于 Restful API 的 Content Managem

      本文链接:https://www.haomeiwen.com/subject/othzlttx.html