计划实现功能(五大模块)
一. 博客界面(首页)
- 文章内容展示,分类导航,作者头像简单介绍
- 首屏动画(rainbow)
- 背景音乐播放器
二. 生活(分享)
- 有趣的事
- 尤克里里音乐
- 篮球视频
三. 留言(相互交流学习)
四. 关于(作者历程详细介绍)
五. 管理后台 react
- 文章发布 markdown
- 评论审核
- 账号管理
- 参考掘金
- 我的主页
六. 功能
- 文章顶置,点击回到顶部
- 加载动画
- logo
- 流量统计,字数统计,文章阅读便捷功能
- 登陆注册
- 文章搜索
- pwa应用 lavas
- 双向通信 socket.io
- ,移动端适配,rem px2rem
- 点赞,评论
- 分类与归档
- egg.js
- node 爬虫
-
安装环境
node
webpack
vue-cli
-
构建项目
vue init webpack yourName
或用 vue-cli3创建更方便
vue create yourName
(需要安装最新的vue-cli3npm i @vue/cli -g
)
- 根据 vue-cli3 从搭建到优化 配置
- 从iconfont 下载的svg图片它会自带一个fill属性,外部的css改变颜色不起作用,需要设置一个全局的css样式
path { fill: inherit !important }
或者使用 svgo 格式化下 !
- cookie的使用
- 插件 mavonEditor
- 新建api文件夹管理路由
- -api
- -index.js //主文件 api接口的统一出口
- -base.js //基础域名配置
- -user.js // 用户模块路由
- -article.js //文章模块路由
//index.js
//api接口的统一出口
//用户模块
import user from '@/api/user';
// 文章模块接口
import article from '@/api/article';
// 其他模块的接口……
// 导出接口
export default {
user,
article,
// ……
}
-----------------------------------------------
//base.js
/**
* 接口域名的管理
*/
const base = {
dev: 'http://127.0.0.1:3000',
product: ''
}
export default base;
--------------------------------------------
//user.js
import base from './base'; // 导入接口域名列表
import axios from '@/axios/request'; // 导入request中创建的axios实例
import qs from 'qs'; // 根据需求是否导入qs模块
const user = {
login (params) {
return axios.post(`${base.dev}/login`,qs.stringify(params));
},
register (params) {
return axios.post(`${base.dev}/register`,qs.stringify(params));
}
}
export default user
- main.js中导入和挂在api
import api from './api' // 导入api接口
Vue.prototype.$api = api; // 将api挂载到vue的原型上
- 使用
this.$api.user.login()
.then((data)=>{
...
})
npm i koa2-cors
(解决跨域) koa2-corsnpm i koa-bodyparser
(解析接收的参数) koa-bodyparsernpm i koa-static
(开放静态资源) koa-staticnpm i mongoose
(操作MongoDB) mongoosenpm i uuid
(生成唯一id插入数据库) uuid
const crypto = require('crypto')
const cryptoPwd = function(password,salt){
const saltPassword = password + ':' + salt
const md5 = crypto.createHash('md5')
const result = md5.update(saltPassword).digest('hex')
return result;
}
-
//生成token const getToken = function (payload= {} ) { var token = jwt.sign(payload, config.secret, { // algorithm: 'HS256', //算法 expiresIn: '2h' //Token过期时间 }); return token } //通过token获取JWT的payload,验证并解析JWT const getJWTPayload = function(token) { return jwt.verify(token, config.secret, (error, decoded) => { if(error){ console.log(error.message) return } return decoded });
-
如何避免options预检请求?(非简单请求...会出现options请求)
可设置 Access-Control-Max-Age:86400 // 缓存预检请求,一段时间内不在出现options请求;
参考文章:
路由模块管理
-
api文件夹存放接口路由
const Router = require('koa-router'); const router = new Router(); const article_controller = require('../controllers/article_controller'); router.post('/article/publish', article_controller.publish); module.exports = router;
-
controllers文件夹存放业务逻辑
const config = require('../config'); const Article_col = require('../model/article'); const uuidV1 = require('uuid/v1') //发布文章 const publish = async (ctx, next) => { const req = ctx.request.body console.log(req,'article') const id = uuidV1(); const newArticle = await Article_col.create({ id, value: req.value, title: req.title, pictureUrl: req.pictureUrl }) if (newArticle) { ctx.body = { code: 1, msg: 'success!', data: {} }; } else { ctx.body = { code: 0, msg: 'failed!' }; } } module.exports = { publish, }
-
model文件夹存放schema表
const mongoose = require('mongoose') const Schema = mongoose.Schema const articleSchema = new Schema({ id:{ type: String, required: true }, value: { type: String, required: true }, title: { type: String, required: true }, pictureUrl: { type: String }, created_time: { type: Date, default: Date.now }, eye:{ type: Number, default: 0 }, like: { type: Number, default: 0 }, comments: { type: Number, default: 0 }, }) module.exports = mongoose.model('Article', articleSchema)
-
同时在app.js中导入api中的路由,再使用路由
const Koa = require('koa') const cors = require('koa2-cors'); const bodyParser = require('koa-bodyparser'); const koaStatic = require('koa-static'); const mongoose = require('mongoose'); const config = require('./config.js'); const user_router = require('./api/user-router.js'); const article_router = require('./api/article-router.js'); const app = new Koa(); mongoose.connect(config.db, {useNewUrlParser:true}, (err) => { if (err) { console.error('Failed to connect to database'); } else { console.log('Connecting database successfully'); } }); app //跨域 .use(cors()) //开放静态资源 .use(koaStatic(__dirname + '/public')) //请求参数格式化 .use(bodyParser()) //路由 .use(user_router.routes()).use(user_router.allowedMethods()) .use(article_router.routes()).use(article_router.allowedMethods()) //启动服务监听端口 .listen(config.port,function(){ console.log('server is running ') });
-
普通用户登陆权限(留言,评论,点赞,)
-
管理员权限(发布)
-
超级管理员(对文章评论进行管理,删除)
名称 | 接口 | 参数 | 返回值 |
---|---|---|---|
文章发布 | /article/publish | value(html源码),title,pictureUrl | null |
文章保存到草稿 | /article/save | value(md源码),title,pictureUrl | null |
草稿列表 | /article/draftList | null | data([{title,mdcode}...]) |
查看草稿 | /article/lookDraft | id | data(md源码) |
文章列表 | /article/articleList | userId | data([{title,htmlcode,puctureUrl,eye, like,comments,time}...]) |
文章编辑 | /aritcle/edit | id | null |
实现思路
-
前端上传图片请求服务器,我用的时element里的上传组件
<template> <div class="publish"> <el-upload class="avatar-uploader" :action="`${baseUrl}/article/uploadCover`" :show-file-list="false" :on-success="handleAvatarSuccess" :before-upload="beforeAvatarUpload"> <img v-if="imageUrl" :src="imageUrl" class="avatar"> <i v-else class="el-icon-plus avatar-uploader-icon"></i> </el-upload> </div> </template> <script> export default { name:'publish', data () { return { baseUrl:base.dev, //服务器地址 imageUrl: '', pictureUrl: '', }; }, methods: { //文章发布 publish(){ this.$api.article.publish({ value:this.mdCode, title:this.title, pictureUrl:this.pictureUrl, }).then((data)=>{ this.$router.push({path:'/'}) }) }, //图片上传 handleAvatarSuccess(res, file) { this.imageUrl = URL.createObjectURL(file.raw); this.pictureUrl = res.pictureUrl; //返回的图片路径 }, beforeAvatarUpload(file) { const isLt2M = file.size / 1024 / 1024 < 1; if (!isLt2M) { this.$message.error('上传头像图片大小不能超过 1MB!'); } return isLt2M; } }, </script>
-
后端使用koa-multer把图片存储在public下的一个articleCover文件夹目录
- article-router.js 配置multer
const Router = require('koa-router'); const router = new Router(); const article_controller = require('../controllers/article_controller'); const multer = require('koa-multer') const path = require('path') var storage = multer.diskStorage({ //文件保存路径 destination: function (req, file, cb) { cb(null, 'public/articleCover/') //path.resolve('public/phoneManageSystem') }, //修改文件名称 filename: function (req, file, cb) { var fileFormat = (file.originalname).split("."); //以点分割成数组,数组的最后一项就是后缀名 cb(null, Date.now() + "." + fileFormat[fileFormat.length - 1]); } }) //加载配置 var upload = multer({ storage: storage, limits: { fileSize: 1024 * 1024 / 2 // 限制512KB } }); router.post('/article/uploadCover', upload.single('file'), article_controller.uploadCover); module.exports = router;
-
aritcle_controller.js 接口
//图片上传 const uploadCover = async(ctx, next)=>{ let pictureUrl = `${config.severUrl}:${config.port}/articleCover/${ctx.req.file.filename}` ctx.body = { filename: ctx.req.file.filename, //返回文件名 pictureUrl: pictureUrl //前端直接调用的地址 } }
-
必须要开放public这个文件夹作为静态资源
app.use(koaStatic(__dirname + '/public'))
-
前端调用图片
markdown中图片的上传
-
less:hover .other{}不起作用 .less{ ::before}为什么选中了全部子元素
-
vuejs 失去焦点,点击全局隐藏某些浮动元素 https://segmentfault.com/q/1010000007444595
-
时间格式化 momentjs
-
less深度选择器 .layout /deep/ .content css深度选择 >>>
-
路由懒加载
{ path: '/', name: 'home', component: resolve => require(['@/view/home/home.vue'], resolve) }
-
开启gzip
-
hiper 性能分析工具
-
v-model 实现父子组件通信,双向绑定数据
-
页面刷新时,url导航地址栏的路由和当前页面显示的不一致(注意区分route:对象和router:方法)
-
解决 vue刷新路由跳到首页的问题
// 1.根组件中通过watch监听路由地址的改变 // 2.使用钩子函数,改变路由 export default { name: 'App', data(){ return { msg:'App.vue', currentRouter:'/' } }, methods:{ }, watch:{ '$route':function(to,from){ this.currentRouter=to.path; } }, beforeMount(){ this.currentRouter=this.$route.path; } }
-
闭包应用
function sendRequest(urls, max, callback) { const len = urls.length; let idx = 0; let counter = 0; function _request() { // 有请求,有通道 while (idx < len && max > 0) { max--; // 占用通道 fetch(urls[idx++]).finally(() => { max++; // 释放通道 counter++; if (counter === len) { return callback(); } else { _request(); } }); } } _request(); }
-
前端界面实现
-
mongdb表的设计
-
时间格式化 moment
-
头像插件 vue-avatar
-
关联github 获取用户信息头像 参考 未完成
-
支持表情包功能 emoji-mart-vue
-
bug :点击其他地方不能关闭评论框
- 动画组件 引入 codepen 里的动画
- 详见文章 vue中使用d3-cloud词云
-
在
model/user.js
的schema
中添加参数对象 -
新建路由文件
notice-router.js
- 引入
koa-router
- 新建路由
- 接口挂载到路由
- 导出路由
const Router = require('koa-router'); const router = new Router(); const notice_controller = require('../controllers/notice_controller'); router.post('/notice/publishNotice', notice_controller.publishNotice); module.exports = router;
- 引入
-
新建控制器文件
notice_controller.js
- 导入需要用到的model文件
- 新建方法对象
- 导出对象
const Article_col = require('../model/article'); const User_col = require('../model/user'); //发布通知消息 const publishNotice = async (ctx, nest) => { } module.exports = { publishNotice, }
-
最后在 app.js 中 引入 notice路由模块,再挂载到 app 上
const notice_router = require('./api/notice-router.js'); app.use(notice_router.routes()).use(notice_router.allowedMethods())
-
src/api
新建 notice.js 文件// 通知消息模块api import base from './base'; // 导入接口域名列表 import axios from '@/axios/request'; // 导入request中创建的axios实例 import qs from 'qs'; // 根据需求是否导入qs模块 const notice = { publishNotice(params) { return axios.post(`${base.dev}/notice/publishNotice`,qs.stringify(params)); }, } export default notice
-
在
api/index.js
中引入和导出//通知模块 import notice from '@/api/notice'; // 导出接口 export default { notice, // …… }
-
在
Notice.vue
中使用this.$api.notice.publishNotice({userId:this.userId}) .then((data)=>{})
-
用户评论文章,通知作者;
-
回复用户通知被回复用户;
-
用户第一次登陆默认发送一个欢迎的通知,注册时插入一条数据
-
vue 中使用 vue-socket stock.io
-
新发布一片文章时给用户发送一条通知
const updateAllUser = await User_col.updateMany( {userId: {$ne: userId}}, { '$inc': { 'unreadNum': 1 }, '$push': { 'commentNotice': noticeObj } } )
-
流程设计:
用户评论文章,存储用户ID,文章作者ID,文章ID,用户评论到一个表(我直接存储到了用户信息的表里commentNotice 数组里)同时把记录通知的值加一,用户登录自动请求获取用户信息接口,如果用户信息里的commentNotice 数组里有值表示有消息通知并显示这些列表。用户查看通知把记录通知的值再赋值为0;
补充: export 和 export default 的区别
- export与export default均可用于导出常量、函数、文件、模块等
- 在一个文件或模块中,export、import可以有多个,export default仅有一个
- 通过export方式导出,在导入时要加{ },export default则不需要
- export能直接导出变量表达式,export default不行。
-
采坑: npm install vue-skeleton-webpack-plugin 报错:
Error: EPERM: operation not permitted, rename ... errno: -4048
表示没有权限,可以把vue页面的服务先关掉,然后再npm,我的是这样解决的! -
vue.config.js 配置 附上一张配置清单
const path = require('path') const SkeletonWebpackPlugin = require('vue-skeleton-webpack-plugin'); function resolve(dir) { return path.join(__dirname, './', dir) } module.exports = { chainWebpack: config => {}, configureWebpack: { plugins: [ new SkeletonWebpackPlugin({ webpackConfig: { entry: { app: resolve('src/assets/js/skeleton.js'), }, }, minimize: true, quiet: true, }), ], }, // css相关配置 css: { // 是否使用css分离插件 ExtractTextPlugin extract: true, // 开启 CSS source maps? sourceMap: false, // 启用 CSS modules for all css / pre-processor files. modules: false } }
-
配置完后需要重启服务才能生效,有时哪个地方配置错误,可能会出启动编译错误或编译到百分之几十的时候就停止了!这是需要检查配置的文件路径及代码是否规范错误!
-
如果出现错误实在不知从哪里解决,可以搭一个新的测试vue项目,再配置骨架屏!测试项目可以显示,再跟自己项目做对比找出错误!(一开始我也找不到哪里的问题一直编译错误,然后我是这么解决的!哈哈...)
-
用户管理 , 用户分析 , 流量统计 ,用户权限设置 , 用户评论数分析
-
文章管理 , 草稿 , 编辑
-
评论管理 , 审核
-
发布通知
-
标签管理
-
node 的安装
-
mongoDB 的安装 | 数据备份导入 | mongoDB 基本命令 | 配置环境 更改默认端口 ?? | 连接线上数据库 端口防火墙开放 | 开机启动?启动与关闭 conf文件启动 | 建立密码连接 |
-
pm2 生产进程管理器 | 可视化监控
-
ssl https 证书
-
nginx | nginx.conf 配置 | vue history 配置 |
#user nobody; worker_processes 1; #error_log logs/error.log; #error_log logs/error.log notice; #error_log logs/error.log info; #pid logs/nginx.pid; events { worker_connections 1024; } http { include mime.types; default_type application/octet-stream; #log_format main '$remote_addr - $remote_user [$time_local] "$request" ' # '$status $body_bytes_sent "$http_referer" ' # '"$http_user_agent" "$http_x_forwarded_for"'; #access_log logs/access.log main; sendfile on; #tcp_nopush on; #keepalive_timeout 0; keepalive_timeout 65; port_in_redirect off; #后台admin代理 server { listen 8888; server_name localhost; #charset koi8-r; #access_log logs/host.access.log main; location / { root /home/yuanhui-blog/yuanhui-admin/; index index.html index.html; try_files $uri $uri/ /index.html; autoindex on; } location @router{ rewrite ^.*$ /index.html last; } location /api/ { proxy_set_header X-Real-IP $remote_addr; proxy_pass http://106.14.174.233:8888/; } gzip on; gzip_buffers 32 4k; gzip_comp_level 6; gzip_min_length 200; gzip_types text/css text/xml application/javascript; gzip_vary on; #error_page 404 /404.html; # redirect server error pages to the static page /50x.html error_page 500 502 503 504 /50x.html; } #前台代理 server { listen 80; server_name localhost; location / { root /home/yuanhui-blog/yuanhui-live/; index index.html index.html; try_files $uri $uri/ /index.html; autoindex on; } location @router{ rewrite ^.*$ /index.html last; } location /api/ { proxy_set_header X-Real-IP $remote_addr; proxy_pass http://106.14.174.233:80/; } gzip on; gzip_buffers 32 4k; gzip_comp_level 6; gzip_min_length 200; gzip_types text/css text/xml application/javascript; gzip_vary on; error_page 500 502 503 504 /50x.html; location = /50x.html { root html; } } # proxy the PHP scripts to Apache listening on 127.0.0.1:80 #location ~ \.php$ { # proxy_pass http://127.0.0.1; #location ~ \.php$ { # proxy_pass http://127.0.0.1; #} # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000 # #location ~ \.php$ { # root html; # fastcgi_pass 127.0.0.1:9000; # fastcgi_index index.php; # fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name; # include fastcgi_params; #} # deny access to .htaccess files, if Apache's document root # concurs with nginx's one # #location ~ /\.ht { # deny all; #} # another virtual host using mix of IP-, name-, and port-based configuration # #server { # listen 8000; # listen somename:8080; # server_name somename alias another.alias; # location / { # root html; # index index.html index.htm; # } #} # HTTPS server # #server { # listen 443 ssl; # HTTPS server # #server { # listen 443 ssl; # server_name localhost; # ssl_certificate cert.pem; # ssl_certificate_key cert.key; # ssl_session_cache shared:SSL:1m; # ssl_session_timeout 5m; # ssl_ciphers HIGH:!aNULL:!MD5; # ssl_prefer_server_ciphers on; # location / { # root html; # index index.html index.htm; # } #} }
-
linux | vim 基本命令 | 防火墙 端口开放 | 路由刷新404
-
vue生产环境和开发环境配置
-
2019
-
2.27 完成了文章点赞 git上传
-
2.28 样式优化,滚动条 Vuescroll.js
-
3.01 评论模块
-
3.06 完成评论功能
-
3.11 分类,归档,标签功能
-
3.13 完成归档标签分类功能
-
3.14 添加返回顶部功能 | vuex全局加载Loading动画.
-
3.15 优化loading动画
-
3.18 文章列表接口分页处理 | 滚动加载更多
-
3.19 文章搜索, siderBar
-
3.25 分类标签词云d3 async的使用
-
3.26 消息通知系统设计
-
4.01 - 4.03 完成评论消息通知功能
-
4.04 优化通知系统
-
4.08 独立通知模块,增加发布文章通知全部用户功能
-
4.09 使用更新器
$inc $push
(原子操作) 优化 mongoDB update 语法- 左侧栏添加最热文章
- router-link路由跳转只局部渲染更改了的页面(diff算法),a标签跳转会重新渲染整个页面
- 路由的滚动
scrollBehavior
-
4.10 详情页作者介绍
-
4.11 添加作者个性化vip身份标识
-
4.15 ...
-
4.16 后台管理搭建 vue-element-admin 了解
-
4.17- 4.18 api添加token认证 |
- 如何避免options?(跨域预请求非简单请求是无法避免,可以缓存让它只请求一次) 设置 Access-Control-Max-Age:86400 // 缓存预检请求
-
4.19 notice通知模块使用vuex优化
-
4.22 添加404页面 | 路由权限
-
4.23 骨架屏搭建 | 401页面
-
4.24 骨架屏实现-
-
...work
-
5.06 骨架屏实现 (未实现,配置??)
-
5.07 首页骨架屏实现
-
5.08 添加分类category
-
5.09 后台管理搭建
-
5.10 后台 文章管理 | 发布 | 更新添加字段 | 用户管理 |
-
...work
-
5.23 后台添加发布通知 (一次通知多个用户)
-
5.24 后台评论管理
-
5.27 后台评论管理优化 | 文章筛选 | 分页 | 评论审核 | 评论接口分离
- 问题: table树的children怎么拿到整个父级的row? | mongoDB怎么更新一个数组内的一个元素?
- 时间格式化优化
-
5.28 前台获取文章评论分离使用单独接口 | 评论删除 | 评论回复
- ~~ 新评论通知 (实时跟进用户评论)~~ (待实现)
-
5.29 添加登陆
-
5.30 添加操作权限
个人中心设置(待实现)
-
6.3 服务器部署
-
6.4 服务器部署
-
6.5 服务器部署
-
6.6 服务器部署完成
-
路由动态渲染原理
-
百度统计 (PV>100)
-
首屏渲染,骨架屏优化
-
详情页大纲导航栏 markdown样式
-
图片存储方式
-
移动端适配 | 响应式 rem 转换 | vh vw | 媒体查询
-
打包部署 pm2 | webpack配置 | 单元测试 | 服务器部署环境
-
swagger | mock 数据
-
微信小程序 trao mpvue | typeScript 构建
-
云服务器
-
scss | less 语法
登陆或注销状态 cookie不能及时刷新- 定位使用vuescroll
点击其他地方不能关闭评论框- 归档:时间划线问题 github 时间选择菜单
- 菜单导航不根据 路由动态变化 跳转时去掉导航激活状态
- 详情页代码界面样式需要刷新才加载
(图片存储base64)