简述
其实一个简单的博客系统实际上很简单,无非就是发文章,显示文章内容。因此在这里,核心功能也是如此,通过系统后台进行文章发布,然年由于前台使用了Angular那么为了保障SEO,使用了Anuglar的SSR功能来进行页面的显示与控制。
博客系统目录结构
├── app // be source code│ ├── controllers│ ├── models│ ├── routes│ │ ├── api│ │ ├── page│ │ └── web│ └── services├── bin├── config // project config├── front-end // fe source code│ ├── blog-admin // blog admin manager system│ └── blog-index // blog index├── logs├── middleware // koa middleware├── public // fe code build in there.│ ├── blog-admin│ ├── blog-index // use ng6 ssr.│ │ ├── browser│ │ └── server│ ├── error // error page style│ │ └── css│ ├── images│ └── images-bg├── test├── uploads├── utils└── views // error page.└── error
技术栈
- Koa2
- Angular
- MySql
后端实现
使用了Koa2框架实现的,使用了传统的MVC架构,数据库使用MySql。其实整体上来看,后端比较简单,都是对一些表的CRUD操作,最多就是一些复杂的查询。而且表的数量也不多,实际只有7张表。下文会列出这7张表的结构,我们使用JavaScript去描述,这里使用了nodejs的第三方模块sequelize
数据表
- 文章数据表
const Article = mysql.define('blog_article', {id: {type: Sequelize.BIGINT, primaryKey: true, autoIncrement: true},title: {type: Sequelize.STRING(512), allowNull: false},type: {type: Sequelize.STRING(256), allowNull: false},tag: {type: Sequelize.STRING(256), allowNull: false},abstract: {type: Sequelize.TEXT, allowNull: false},date: {type: Sequelize.BIGINT, allowNull: false},articleHtml: {type: Sequelize.TEXT('long'), allowNull: false},articleMd: {type: Sequelize.TEXT('long'), allowNull: false},coverImg: {type: Sequelize.INTEGER, allowNull: false},status: {type: Sequelize.STRING(10), allowNull: false},readCount: {type: Sequelize.INTEGER, allowNull: false, defaultValue: 0},commentCount: {type: Sequelize.INTEGER, allowNull: false, defaultValue: 0},seo: {type: Sequelize.STRING(256),allowNull: true}}, {tableName: 'blog_article',timestamps: false,freezeTableName: true});
- 分类标签数据表
const CategoryTag = mysql.define('blog_cat_tag', {id: {type: Sequelize.BIGINT, primaryKey: true,autoIncrement: true},type: {type: Sequelize.STRING(100), allowNull:false},name: {type: Sequelize.STRING(256), allowNull:false},date: {type: Sequelize.BIGINT, allowNull:false},count: {type: Sequelize.INTEGER, allowNull:false, defaultValue: 0}}, {tableName: 'blog_cat_tag',timestamps: false,freezeTableName: true});
- 前端错误收集信息表
const mysql = require('./db');const Sequelize = require('sequelize');const Cgi = mysql.define('blog_cgi', {id: {type: Sequelize.BIGINT, primaryKey: true, autoIncrement: true},content: {type: Sequelize.TEXT, allowNull: false},type: {type: Sequelize.STRING(256), allowNull: false},ip: {type: Sequelize.STRING(256), allowNull: false},date: {type: Sequelize.BIGINT, allowNull: false}}, {tableName: 'blog_cgi',timestamps: false,freezeTableName: true});module.exports = Cgi;
- 评论信息表
const mysql = require('./db');const Sequelize = require('sequelize');const Comment = mysql.define('blog_comment', {id: {type: Sequelize.BIGINT, primaryKey: true, autoIncrement: true},article: {type: Sequelize.BIGINT, allowNull: false},content: {type: Sequelize.TEXT, allowNull: false},date: {type: Sequelize.BIGINT, allowNull: false},visitor: {type: Sequelize.STRING(256), allowNull: false},quotes: {type: Sequelize.TEXT}}, {tableName: 'blog_comment',timestamps: false,freezeTableName: true});module.exports = Comment;
- 图片信息表
const mysql = require('./db');const Sequelize = require('sequelize');const Image = mysql.define('blog_image', {id: {type: Sequelize.BIGINT, primaryKey: true, autoIncrement: true},name: {type: Sequelize.STRING, allowNull: false},ext: {type: Sequelize.STRING, allowNull: false},mime: {type: Sequelize.STRING, allowNull: false},path: {type: Sequelize.STRING, allowNull: false},size: {type: Sequelize.INTEGER, allowNull: false},date: {type: Sequelize.BIGINT, allowNull: false}}, {tableName: 'blog_image',timestamps: false,freezeTableName: true});module.exports = Image;
- 用户信息表
const mysql = require('./db');const Sequelize = require('sequelize');const User = mysql.define('blog_user', {id: {type: Sequelize.BIGINT, primaryKey: true, autoIncrement: true},account: {type: Sequelize.STRING(256), allowNull: false},name: {type: Sequelize.STRING(256), allowNull: false},position: {type: Sequelize.STRING(256), allowNull: false},signature: {type: Sequelize.STRING(256), allowNull: false},label: {type: Sequelize.STRING(256), allowNull: false},introduce: {type: Sequelize.TEXT, allowNull: false},password: {type: Sequelize.TEXT, allowNull: false},headImg: {type: Sequelize.INTEGER, allowNull: false}}, {tableName: 'blog_user',timestamps: false,freezeTableName: true});module.exports = User;
其实如果要简单的的话,只需要发布文章这一个功能的话,上面有一些表基本都可以不用,仅保留文章信息表就可以,不过这样搭建起来博客系统,只能用于发文章。而没有额外的一些功能。
路由设计
我们后端路由设计也比较简单,使用koa router即可,具体使用方法可以看文档,这里不赘述。这里koa router设计是为了配合后台管理以及前台的AngularRouter实现。
- /admin/xxx 访问管理端
- /xxx 访问博客端
至于Angular Router,具体使用见前文Angular入坑(五):路由基础。
业务逻辑
后端业务逻辑分为6个模块:
- 文章管理
- 标签和类别管理
- 素材库
- 评论管理
- 前端监控
- 个人信息管理
这里主要是涉及到数据库的操作,包括基础数据的CRUD操作等,这里就不贴代码了,可以参考后文贴出的github仓库地址查看源代码。
这里需要讲解的有几点:
- 数据库连接,使用了sequelize模块,为了避免硬编码,这里将配置抽出到一个json文件中,在json配置数据相关的配置项。
const Config = require('../../config/database.config');const Sequelize = require('sequelize');const mysql = new Sequelize(Config.dbname,Config.username,Config.password, Config.options);module.exports = mysql;
- 就像数据库连接配置一样,日志的配置也是如此。
- 各个数据表模块分离,将对于的数据操放到一个Service类中进行,然后在Controller中调用Service的方法,获取到结果后做进一步处理,如数据封装,修改等。
- 统一系统的数据结构,这样有利于数据维护,可以新建一个工具类,进行数据转化,传入不同的数据格式的值,传出成统一的数据格式。
- 异常处理,koa2中由于使用了async-await的形式,因此需要注意好try…catcha…后处理对应的异常。
- 权限处理方面,则是自定义一个koa2的中间件
- 其余的包括csrf,session,file upload方面的处理则看你对应的文档即可知悉。
博客端实现
博客端实际上就是个单纯的Anuglar应用。前面一系列文章已经讲了一些博客端的内容了,如SSR,PWA的实现。这里也不赘述了,详细可以看之前的内容。
SEO处理
主要是使用了SSR,而使用了SSR之后还需要对meta标签处理,这里实现的方式是:@angular/platform-browser下的Meta, Title类。基本的流程是,当在服务端获取到数据的时候,通过事件总线,通知所有注册了update-meta事件的模块,进行更新当前页面的meta标签。
this.eventBus.on('update-meta', (data: Event) => {const meta: MyMeta = data.data;this.title.setTitle(meta.title);Object.keys(meta).forEach((val: string) => {this.meta.updateTag({name: val, content: meta[val]});this.meta.updateTag({name: `og:${val}`, content: meta[val]});});});
这样最终出来的html代码中,就会包含所需的meta,title的内容。
评论功能
这里评论功能实现也比较简单,采用单纯评论-引用评论形式实现。应用评论时使用自定义的,标签将一些关键数据包装一起来,当提交之后对这些数据进行解析提取后进行下一步的后端方面呢的处理。
前端日志上报
这里分错误日志和访问日志;基本原理是在onload事件和onerror事件分别创建一张图片,链接地址为api地址,而后在图片的onload或onnerror事件中把图片移除了。
function error(msg, url, line) {// 收集上报数据的信息const REPORT_URL = `${environment.apiURL.cgiError}?type=cli-error&report=`;// 收集错误信息,发生错误的脚本文件网络地址,用户代理信息,时间const m: any = {url,line,agent: navigator.userAgent,time: +new Date};if (msg instanceof ErrorEvent) {m.error = {msg: msg.message,lineno: msg.lineno,filename: msg.filename,type: msg.type};} else {m.error = msg;}// 组装错误上报信息内容URLurl = REPORT_URL + JSON.stringify(m);let img = new Image;img.onload = img.onerror = function () {img = null;};// 发送数据到后台cgiimg.src = url;}// 监听错误上报window.onerror = function (msg, url, line) {error(msg, url, line);};window.onload = () => {setTimeout(() => {(function _performanc1e() {const REPORT_URL = `${environment.apiURL.cgiTime}?type=cli-time&report=`;const perf = window.performance;const points = ['navigationStart', 'unloadEventStart', 'unloadEventEnd','redirectStart', 'redirectEnd', 'fetchStart', 'domainLookupStart','connectStart', 'requestStart', 'responseStart', 'responseEnd','domLoading', 'domInteractive', 'domContentLoadedEventEnd','domComplete', 'loadEventStart', 'loadEventEnd'];const timing = perf.timing;if (perf && timing) {const arr = [];const navigationStart = timing[points[0]];for (let i = 0, l = points.length; i < l; i++) {arr[i] = timing[points[i]] - navigationStart;}const obj: any = {};points.forEach((val, idx) => {obj[val] = arr[idx];});const url = REPORT_URL + JSON.stringify(obj);let img = new Image;img.onload = img.onerror = function () {img = null;};img.src = url;}})();});};
总结
其实整体来说,博客的功能很简单,也就是文章,分类标签,素材等各种CRUD操作。而后利用Angular实现界面,获取数据,显示数据就完是了。因此这里简单的讲了下我自己博客系统的实现细节。有兴趣的同学,可以看看我博客系统的具体实现。