前言
一开始使用WordPress,觉得有些臃肿,PHP会的不多改起来有点麻烦。后面换成Hexo,支持Markdown静态优秀,不过动态功能像评论、文件管理得自己实现,不如自个写一个。
业余时间快速开发,计划主体功能2-3个月内完成,实际上因为拖延症陆陆续续做了大半年。
1、技术选型
1.1、Node.js框架
Express、Koa 都是非常优秀的框架,简单且扩展性强,非常适合做个人项目,但框架本身缺少约定,基础设施需要寻找或自己搭建。而我的初衷是快速搭建,所以我把目光转向阿里巴巴团队的Egg.js和360团队的Thinkjs,两者中文文档详细容易扩展,社区资源丰富。
Egg.js 选择了 Koa 作为其基础框架,在它的模型基础上,进一步对它进行了一些增强。
Thinkjs,框架底层也是基于 Koa 2.x 实现,相对于Egg.js来说,相对于Egg.js它处于更上层,封装了很多常用的功能,符合快速搭建一个博客的初衷,所以我选用了Thinkjs。
2 年前入坑 thinkjs ,现在有没有必要改换 eggjs ?
更新于2021-10-08:18年选用Thinkjs的时候,它和Egg.js差不多,其实两个都能满足博客应用的需求,虽然它不更新了,但是已经相对稳定,所以暂时不会闲着更换成Egg.js,后续的新项目再考虑Egg.js
1.2、后端技术
- Node.js
- Mysql:由于博客的数据间存在一定的关系,且数据量并不大,所以选用Mysql而不是MongoDB,而且还有个原因就是Thinkjs对Mysql更友好。
- file-cache:我的网站流量并不大,所以简单使用文件做缓存管理,后续更换redis也很简单
- session-cookies:内容管理系统就只有我一个用户登录,所以简单使用传统的session-cookies,而不是jwt,且如果用jwt图片验证码服务端存储还得用另外的
- RESTful:内容管理系统api使用RESTful规范
- Nunjucks:模板引擎,主要用于后端渲染sitemap、xml
1.3、前端技术
- Vue2、Vue-router、Vuex
- Nuxt.js:Vue服务端渲染
- Element-ui:组件库
- Eslint:JS代码规范控制
- Stylelint:CSS代码规范控制
- Axios:网络请求
2、功能与特性
2.1、内容管理
- SPA单页应用:内容管理系统不需要SEO,首页渲染时间也没有要求
- 栏目管理:文章分类,单页面如关于我们页面
- 文章管理:编写、修改文章
- 文件管理:文件上传,预览与选择,图片自动裁剪格式化
- 友链管理
- 评论管理:原本打算使用Valine、Waline、gittalk这样的第三方评论系统,但是不好管理,所以不如自己实现
- 系统配置
- 用户管理:单用户,所以没有权限控制
2.2、博客前台
- SSR同构渲染:需要考虑搜索引擎的收录,所以需要SEO优化
- 状态数据管理:菜单、系统配置公共数据
- 响应式:element-ui的栅格布局
- 网络请求:全局拦截器
- 暗黑模式:浅色、暗黑模式切换
3、项目结构
因为前后端都是使用javascript
开发,前台(nuxt-ssr模式)、后台(nuxt-spa模式)都是用element-ui+vue,所以考虑放到同一个项目,使用一个node_modules。
client 前台
├── admin 内容管理平台
| ├── api 整合管理所有的api接口
| ├── layouts 外部容器
| ├── middleware
│ | └── permission.js 登录鉴权
| ├── plugins
│ | ├── api.js 使上下文能使用api请求
│ | ├── axios.js axios全局拦截器
│ | ├── element-ui element-ui按需引入
│ | └── svg-icon 注册svg icon组件
| ├── store vuex
| ├── styles 公共样式
| ├── utils 工具集
| ├── views 页面组件
| └── router.js @nuxtjs/router路由配置
├── common 内容管理平台和博客前台功能模块
├── front 博客前台
| ├── layouts 外部容器
│ | ├── app.vue 工具页面使用的外部容器,没有菜单
│ | └── default 默认容器,有菜单
| ├── pages nuxt页面
| ├── plugins
│ | ├── axios.js axios全局拦截器
│ | ├── element-ui element-ui按需引入
│ | └── filters 注册全局过滤器
| ├── store vuex
│ | └── index.js nuxtServerInit获取菜单及系统配置
| ├── vendor 第三方库
│ | ├── aplayer 音乐播放器
│ | ├── live2d Live2D Cubism SDK v2看板娘
│ └── └── spine-ts spine骨骼动画
├── config 配置
| ├── adapter.model.js 数据库配置
│ ├── nuxt.admin.js 内容管理平台nuxt配置
│ └── nuxt.front.js 博客前台nuxt配置
├── logs 日志文件
├── runtime
| ├── cache 缓存
| ├── session 会话信息
├── src 后台
| ├── config 配置
│ | ├── adapter 数据库、缓存、日志等配置
│ | ├── middleware 中间件配置
│ | | └── nuxt.js nuxt中间件
│ | ├── extend.js thinkjs扩展配置
│ | └── router.js thinkjs路由
│ ├── controller 控制器
│ | ├── admin 内容管理平台api
│ | └── front 博客前台api
│ ├── logic 校验层
│ ├── model 模型,数据库操作
│ ├── service 服务,可复用,抽离controller逻辑
├── views
│ ├── rss.xml rss模板
│ └── sitemap.xml 网站地图模板
├── www 静态资源,nuxt打包目标目录
│ └── upload 上传目录
├── development.js thinkjs开发环境入口
├── nuxt.config.js
├── package.json
├── production.js thinkjs生产环境入口
4、系统设计
4.1、中间件
nuxt作为node中间件使用Renderer处理和服务所有 SSR 和资源请求,开发环境先Builder然后再渲染,生产环境直接使用nuxt build构建好的资源进行渲染
const { Nuxt, Builder } = require('nuxt');
module.exports = options => {
const config = require('config/nuxt.front.js'));
config.dev = options.isDev;
const nuxt = new Nuxt(config);
if (options.isDev) {
new Builder(nuxt).build();
}
const middleware = async(ctx, next) => {
// Default 404
ctx.status = options.status || 200;
ctx.req.session = await ctx.session();
await nuxt.render(ctx.req, ctx.res);
let err = null;
return next().catch(e => {
err = e;
}).then(() => {
if (err) {
return Promise.reject(err);
}
// 如果后续执行逻辑有错误,则将错误返回
return new Promise((resolve, reject) => {
return { resolve, reject };
});
});
};
return middleware;
};
front、admin都用nuxt,根据命令里面是否包含--admin,获取对应的配置
nuxt.config.js
const adminConfig = require('./config/nuxt.admin.js')
const frontConfig = require('./config/nuxt.front.js')
const isAdmin = process.argv.includes('--admin')
module.exports = isAdmin ? adminConfig : frontConfig
admin不需要SEO,所以使用nuxt的spa模式
config/nuxt.admin.js
const path = require('path')
const isPro = process.env.NODE_ENV === 'production'
const srcDir = 'client/admin/'
function resolve(dir) {
return path.join(__dirname, '../', srcDir, dir)
}
module.exports = {
alias: {
'#': resolve('../common')
},
axios: {
proxy: true,
prefix: '/admin'
},
build: {
babel: {
plugins: [
[
'component',
{
'libraryName': 'element-ui',
'styleLibraryName': 'theme-chalk'
}
]
]
},
extractCSS: true,
extend(config, ctx) {
// set svg-sprite-loader
const svgRule = config.module.rules.find(rule => rule.test.test('.svg'))
svgRule.exclude = [resolve('assets/icons/svg')]
// Includes /icons/svg for svg-sprite-loader
config.module.rules.push({
test: /\.svg$/,
include: [resolve('assets/icons/svg')],
loader: 'svg-sprite-loader',
options: {
symbolId: 'icon-[name]'
}
})
},
publicPath: '//cdn.timelessq.com/nuxt-admin/' // 只需将www/admin上传cdn
},
buildDir: 'www/nuxt-admin',
buildModules: [
'@nuxtjs/router'
],
css: [
'@/styles/index.scss'
],
plugins: [
'@/plugins/axios',
'@/plugins/api',
'@/plugins/element-ui',
'@/plugins/svg-icon'
],
generate: {
dir: 'www/nuxt-admin',
fallback: 'index.html'
},
head: {
meta: [
{ charset: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
{ hid: 'description', name: 'description', content: '' }
]
},
modern: isPro, // 现代模式
modules: [
'@nuxtjs/axios',
'@nuxtjs/proxy'
],
proxy: {
'/admin': {
target: 'http://127.0.0.1:8360', // 目标接口域名
changeOrigin: true // 表示是否跨域
}
},
render: {
compressor: false // 禁用中间件压缩
// resourceHints: false // 添加prefetch并preload链接以加快初始页面加载时间。
},
router: {
middleware: ['permission']
},
server: {
port: 9528
},
srcDir,
ssr: false,
target: 'static',
telemetry: false // 关闭收集遥测数据
}
front前台就需要开启SSR
config/nuxt.front.js
const path = require('path')
const CompressionPlugin = require('compression-webpack-plugin')
const isPro = process.env.NODE_ENV === 'production'
const srcDir = 'client/front/'
function resolve(dir) {
return path.join(__dirname, '../', srcDir, dir)
}
module.exports = {
alias: {
'#': resolve('../common')
},
buildModules: [
'@nuxtjs/color-mode',
'@nuxtjs/style-resources'
],
build: {
babel: {
plugins: [
[
'component',
{
'libraryName': 'element-ui',
'styleLibraryName': '../packages/theme-chalk/src',
'ext': '.scss'
}
],
[
'prismjs',
{
'languages': ['markup', 'css', 'javascript', 'json', 'less', 'scss', 'shell', 'typescript'],
'plugins': ['show-language', 'highlight-keywords', 'toolbar'],
'css': false
}
]
]
},
extractCSS: true,
plugins: [
new CompressionPlugin({
test: /.(js|css|woff|ttf)$/, // 匹配需要压缩的文件后缀 看需求
threshold: 10240 // 大于10kb的会压缩,默认为0
})
],
publicPath: '//cdn.timelessq.com/nuxt-front/dist/client' // 只需将www/front上传cdn
},
buildDir: 'www/nuxt-front',
css: [
'@/styles/global.scss'
],
head: {
meta: [
{ charset: 'utf-8' },
{ name: 'renderer', content: 'webkit' },
{ name: 'viewport', content: 'width=device-width,initial-scale=1,shrink-to-fit=no' }
]
},
modern: isPro, // 现代模式
modules: [
'@nuxtjs/axios'
],
plugins: [
'@/plugins/element-ui',
'@/plugins/axios',
'@/plugins/filters'
],
render: {
compressor: false // 禁用中间件压缩
},
srcDir,
styleResources: {
scss: resolve('/styles/element-variables.scss')
},
telemetry: false // 关闭收集遥测数据
}
4.1、文件管理
- 文件上传本地www/upload目录,开发环境用Thinkjs提供静态资源服务,生产环境www作为网站根目录,使用nginx管理,除静态资源外反向代理到Thinkjs的服务
- 图片访问时自动裁剪生成指定尺寸、格式的缩略图(后续文章会提到)
- 生产环境静态资源使用腾讯云对象存储的CDN服务,自动回源拉取文件,不用开发上传腾讯云对象存储功能
还没有评论,快来抢第一吧