├── client ├── static │ └── .gitkeep ├── .eslintignore ├── build │ ├── logo.png │ ├── vue-loader.conf.js │ ├── build.js │ ├── check-versions.js │ ├── webpack.base.conf.js │ ├── utils.js │ ├── webpack.dev.conf.js │ └── webpack.prod.conf.js ├── config │ ├── prod.env.js │ ├── dev.env.js │ └── index.js ├── src │ ├── assets │ │ └── img │ │ │ └── avatar.jpg │ ├── main.js │ ├── plugins │ │ └── utils.js │ ├── router │ │ └── index.js │ ├── store │ │ ├── module │ │ │ └── userInfo.js │ │ └── index.js │ ├── App.vue │ ├── axios │ │ └── index.js │ ├── views │ │ ├── Home.vue │ │ ├── Login.vue │ │ └── Register.vue │ └── components │ │ ├── Loading.vue │ │ ├── Paginator.vue │ │ ├── CommentItem.vue │ │ └── CommentBox.vue ├── .editorconfig ├── .gitignore ├── .babelrc ├── .postcssrc.js ├── index.html ├── README.md ├── .eslintrc.js └── package.json ├── .eslintignore ├── .editorconfig ├── server ├── controller │ ├── index.js │ ├── other.js │ ├── leave.js │ └── user.js ├── utils │ ├── config.js │ └── token.js ├── routes │ └── index.js ├── package.json ├── app.js ├── bin │ └── www.js └── db │ └── index.js ├── .gitignore ├── .postcssrc.js ├── .babelrc ├── .eslintrc.js └── README.md /client/static/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /build/ 2 | /config/ 3 | /dist/ 4 | /*.js 5 | /test/unit/coverage/ 6 | -------------------------------------------------------------------------------- /client/.eslintignore: -------------------------------------------------------------------------------- 1 | /build/ 2 | /config/ 3 | /dist/ 4 | /*.js 5 | /test/unit/coverage/ 6 | -------------------------------------------------------------------------------- /client/build/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiChangyi/demo1/HEAD/client/build/logo.png -------------------------------------------------------------------------------- /client/config/prod.env.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | module.exports = { 3 | NODE_ENV: '"production"' 4 | } 5 | -------------------------------------------------------------------------------- /client/src/assets/img/avatar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiChangyi/demo1/HEAD/client/src/assets/img/avatar.jpg -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /client/config/dev.env.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const merge = require('webpack-merge') 3 | const prodEnv = require('./prod.env') 4 | 5 | module.exports = merge(prodEnv, { 6 | NODE_ENV: '"development"' 7 | }) 8 | -------------------------------------------------------------------------------- /client/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /server/controller/index.js: -------------------------------------------------------------------------------- 1 | // API 分发器 2 | const user = require("./user"); 3 | const other = require('./other'); 4 | const leave = require('./leave'); 5 | 6 | module.exports = { 7 | user, 8 | other, 9 | leave 10 | } -------------------------------------------------------------------------------- /client/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | /dist/ 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Editor directories and files 9 | .idea 10 | .vscode 11 | *.suo 12 | *.ntvs* 13 | *.njsproj 14 | *.sln 15 | -------------------------------------------------------------------------------- /server/utils/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // 用户密码加密字符串 3 | PWD_ENCODE_STR: "pawn_user_encode_str", 4 | // token 加密字符串, 5 | TOKEN_ENCODE_STR: "pawn_token_encode_str", 6 | // 添加非get请求通过的连接 7 | URL_YES_PASS: ['/api/user/login', '/api/user'] 8 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | dist/ 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | /test/unit/coverage/ 8 | /test/e2e/reports/ 9 | selenium-debug.log 10 | 11 | # Editor directories and files 12 | .idea 13 | .vscode 14 | *.suo 15 | *.ntvs* 16 | *.njsproj 17 | *.sln 18 | -------------------------------------------------------------------------------- /client/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { 4 | "modules": false, 5 | "targets": { 6 | "browsers": ["> 1%", "last 2 versions", "not ie <= 8"] 7 | } 8 | }], 9 | "stage-2" 10 | ], 11 | "plugins": ["transform-vue-jsx", "transform-runtime"] 12 | } 13 | -------------------------------------------------------------------------------- /.postcssrc.js: -------------------------------------------------------------------------------- 1 | // https://github.com/michael-ciniawsky/postcss-load-config 2 | 3 | module.exports = { 4 | "plugins": { 5 | "postcss-import": {}, 6 | "postcss-url": {}, 7 | // to edit target browsers: use "browserslist" field in package.json 8 | "autoprefixer": {} 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /client/.postcssrc.js: -------------------------------------------------------------------------------- 1 | // https://github.com/michael-ciniawsky/postcss-load-config 2 | 3 | module.exports = { 4 | "plugins": { 5 | "postcss-import": {}, 6 | "postcss-url": {}, 7 | // to edit target browsers: use "browserslist" field in package.json 8 | "autoprefixer": {} 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | client 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { 4 | "modules": false, 5 | "targets": { 6 | "browsers": ["> 1%", "last 2 versions", "not ie <= 8"] 7 | } 8 | }], 9 | "stage-2" 10 | ], 11 | "plugins": ["transform-vue-jsx", "transform-runtime"], 12 | "env": { 13 | "test": { 14 | "presets": ["env", "stage-2"], 15 | "plugins": ["transform-vue-jsx", "transform-es2015-modules-commonjs", "dynamic-import-node"] 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /client/src/main.js: -------------------------------------------------------------------------------- 1 | // The Vue build version to load with the `import` command 2 | // (runtime-only or standalone) has been set in webpack.base.conf with an alias. 3 | import Vue from 'vue' 4 | import App from './App' 5 | import router from './router' 6 | import axios from './axios' 7 | import store from './store' 8 | 9 | Vue.config.productionTip = false; 10 | Vue.prototype.$http = axios; 11 | 12 | /* eslint-disable no-new */ 13 | new Vue({ 14 | el: '#app', 15 | router, 16 | store, 17 | components: { App }, 18 | template: '' 19 | }) 20 | -------------------------------------------------------------------------------- /client/README.md: -------------------------------------------------------------------------------- 1 | # client 2 | 3 | > A Vue.js project 4 | 5 | ## Build Setup 6 | 7 | ``` bash 8 | # install dependencies 9 | npm install 10 | 11 | # serve with hot reload at localhost:8080 12 | npm run dev 13 | 14 | # build for production with minification 15 | npm run build 16 | 17 | # build for production and view the bundle analyzer report 18 | npm run build --report 19 | ``` 20 | 21 | For a detailed explanation on how things work, check out the [guide](http://vuejs-templates.github.io/webpack/) and [docs for vue-loader](http://vuejs.github.io/vue-loader). 22 | -------------------------------------------------------------------------------- /client/build/vue-loader.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const utils = require('./utils') 3 | const config = require('../config') 4 | const isProduction = process.env.NODE_ENV === 'production' 5 | const sourceMapEnabled = isProduction 6 | ? config.build.productionSourceMap 7 | : config.dev.cssSourceMap 8 | 9 | module.exports = { 10 | loaders: utils.cssLoaders({ 11 | sourceMap: sourceMapEnabled, 12 | extract: isProduction 13 | }), 14 | cssSourceMap: sourceMapEnabled, 15 | cacheBusting: config.dev.cacheBusting, 16 | transformToRequire: { 17 | video: ['src', 'poster'], 18 | source: 'src', 19 | img: 'src', 20 | image: 'xlink:href' 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /server/routes/index.js: -------------------------------------------------------------------------------- 1 | const router = require('koa-router')() 2 | const controller = require('../controller') 3 | 4 | router.get('/', async (ctx, next) => { 5 | ctx.body = "hello world! create by pawn! my blog => http://blog.lcylove.cn" 6 | }) 7 | .post("/api/user", controller.user.register) // 用户注册 8 | .post("/api/user/login", controller.user.login) // 用户登录 9 | .get('/api/user',controller.user.query) // 根据用户_id查询用户 10 | .get('/api/other/checkcode', controller.other.checkcode)// 验证码获取 11 | .post("/api/leave", controller.leave.addLeaver)// 添加留言 12 | .get("/api/leave", controller.leave.getLeaves)// 留言获取 13 | .delete("/api/leave/:id", controller.leave.deleteLeaver) // 删除留言 14 | 15 | module.exports = router 16 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue_koa_mongodb", 3 | "version": "1.0.0", 4 | "description": "vue+koa+mongodb 小练习。登录注册,留言", 5 | "main": "app.js", 6 | "scripts": { 7 | "start": "node bin/www", 8 | "prd": "pm2 start bin/www", 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "dependencies": { 12 | "debug": "^3.1.0", 13 | "gd-bmp": "^1.2.4", 14 | "jsonwebtoken": "^8.3.0", 15 | "koa": "^2.5.2", 16 | "koa-bodyparser": "^4.2.1", 17 | "koa-convert": "^1.2.0", 18 | "koa-json": "^2.0.2", 19 | "koa-logger": "^3.2.0", 20 | "koa-onerror": "^4.1.0", 21 | "koa-router": "^7.4.0", 22 | "koa-static": "^5.0.0", 23 | "mongoose": "^5.2.14", 24 | "sha1": "^1.1.1", 25 | "xss": "^1.0.3" 26 | }, 27 | "devDependencies": { 28 | "nodemon": "^1.18.4" 29 | }, 30 | "author": "", 31 | "license": "ISC" 32 | } 33 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | // https://eslint.org/docs/user-guide/configuring 2 | 3 | module.exports = { 4 | root: true, 5 | parserOptions: { 6 | parser: 'babel-eslint' 7 | }, 8 | env: { 9 | browser: true, 10 | }, 11 | extends: [ 12 | // https://github.com/vuejs/eslint-plugin-vue#priority-a-essential-error-prevention 13 | // consider switching to `plugin:vue/strongly-recommended` or `plugin:vue/recommended` for stricter rules. 14 | 'plugin:vue/essential', 15 | // https://github.com/standard/standard/blob/master/docs/RULES-en.md 16 | 'standard' 17 | ], 18 | // required to lint *.vue files 19 | plugins: [ 20 | 'vue' 21 | ], 22 | // add your custom rules here 23 | rules: { 24 | // allow async-await 25 | 'generator-star-spacing': 'off', 26 | // allow debugger during development 27 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off' 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /client/.eslintrc.js: -------------------------------------------------------------------------------- 1 | // https://eslint.org/docs/user-guide/configuring 2 | 3 | module.exports = { 4 | root: true, 5 | parserOptions: { 6 | parser: 'babel-eslint' 7 | }, 8 | env: { 9 | browser: true, 10 | }, 11 | extends: [ 12 | // https://github.com/vuejs/eslint-plugin-vue#priority-a-essential-error-prevention 13 | // consider switching to `plugin:vue/strongly-recommended` or `plugin:vue/recommended` for stricter rules. 14 | 'plugin:vue/essential', 15 | // https://github.com/standard/standard/blob/master/docs/RULES-en.md 16 | 'standard' 17 | ], 18 | // required to lint *.vue files 19 | plugins: [ 20 | 'vue' 21 | ], 22 | // add your custom rules here 23 | rules: { 24 | // allow async-await 25 | 'generator-star-spacing': 'off', 26 | // allow debugger during development 27 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off' 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /client/src/plugins/utils.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // 时间格式化 3 | format_date(num=0,format = 'YY-MM-DD hh:mm:ss'){ 4 | let date = new Date(Number(num)); 5 | let year = date.getFullYear(), 6 | month = date.getMonth()+1,//月份是从0开始的 7 | day = date.getDate(), 8 | hour = date.getHours(), 9 | min = date.getMinutes(), 10 | sec = date.getSeconds(); 11 | 12 | let preArr = Array.apply(null,Array(10)).map(function(elem, index) { 13 | return '0'+index; 14 | });////开个长度为10的数组 格式为 00 01 02 03 15 | 16 | let newTime = format.replace(/YY/g,year) 17 | .replace(/MM/g,preArr[month]||month) 18 | .replace(/DD/g,preArr[day]||day) 19 | .replace(/hh/g,preArr[hour]||hour) 20 | .replace(/mm/g,preArr[min]||min) 21 | .replace(/ss/g,preArr[sec]||sec); 22 | 23 | return newTime; 24 | } 25 | } -------------------------------------------------------------------------------- /server/app.js: -------------------------------------------------------------------------------- 1 | const Koa = require('koa') 2 | const app = new Koa() 3 | const json = require('koa-json') 4 | const onerror = require('koa-onerror') 5 | const bodyparser = require('koa-bodyparser') 6 | const logger = require('koa-logger') 7 | const {check_token} = require('./utils/token') 8 | 9 | // error handler 10 | onerror(app) 11 | 12 | // middlewares 13 | app.use(bodyparser({ 14 | enableTypes:['json', 'form', 'text'] 15 | })) 16 | app.use(json()) 17 | app.use(logger()) 18 | app.use(require('koa-static')(__dirname + '/public')) 19 | 20 | // 添加token 验证中间件 21 | app.use(check_token); 22 | 23 | // logger 24 | app.use(async (ctx, next) => { 25 | const start = new Date() 26 | await next() 27 | const ms = new Date() - start 28 | console.log(`${ctx.method} ${ctx.url} - ${ms}ms`) 29 | }) 30 | 31 | // routes 32 | const index = require('./routes/index') 33 | app.use(index.routes(), index.allowedMethods()) 34 | 35 | // error-handling 36 | app.on('error', (err, ctx) => { 37 | console.error('server error', err, ctx) 38 | }); 39 | 40 | module.exports = app -------------------------------------------------------------------------------- /client/src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | import store from '../store' 4 | 5 | Vue.use(Router) 6 | 7 | const router = new Router({ 8 | routes: [{ 9 | path: '/home', 10 | redirect: '/' 11 | }, { 12 | path: '/', 13 | name: 'home', 14 | component: resolve => require(['@/views/Home'], resolve), 15 | meta:{ 16 | requiresAuth:true 17 | }, 18 | }, { 19 | path: '/login', 20 | name: '/login', 21 | component: resolve => require(['@/views/Login'], resolve) 22 | }, { 23 | path: '/register', 24 | name: 'register', 25 | component: resolve => require(['@/views/Register'], resolve) 26 | }] 27 | }) 28 | 29 | //注册全局钩子用来拦截导航 30 | router.beforeEach((to, from, next) => { 31 | //获取store里面的token 32 | let token = store.state.user.token; 33 | //判断要去的路由有没有requiresAuth 34 | if(to.meta.requiresAuth){ 35 | if(token){ 36 | next(); 37 | }else{ 38 | next({ 39 | path: '/login', 40 | query: { redirect: to.fullPath } // 将刚刚要去的路由path(却无权限)作为参数,方便登录成功后直接跳转到该路由 41 | }); 42 | } 43 | }else{ 44 | next(); 45 | } 46 | }); 47 | 48 | export default router; -------------------------------------------------------------------------------- /client/src/store/module/userInfo.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Pawn 3 | * @Date: 2018-08-29 09:41:24 4 | * @Last Modified by: Pawn 5 | * @Last Modified time: 2018-09-20 16:06:15 6 | */ 7 | import api from '@/axios' 8 | 9 | const state = { 10 | data: [] 11 | } 12 | 13 | const actions = { 14 | async get_userinfo({state,commit},_id){ 15 | let index = state.data.findIndex((item) => { 16 | return item._id == _id; 17 | }) 18 | if(index == -1){ 19 | state.data.push({ 20 | _id, 21 | avatar: '', 22 | user_name: '' 23 | }) 24 | // 没有获取 25 | let res = await api.api_get_user(_id); 26 | let {code,msg,data} = res.data; 27 | if(code == 200){ 28 | await commit("save_userinfo",data); 29 | } 30 | } 31 | } 32 | } 33 | 34 | const mutations = { 35 | save_userinfo(state,data){ 36 | let {_id = '',avatar = '',user_name = ''} = data; 37 | state.data.forEach( item => { 38 | if(item._id == _id){ 39 | item.avatar = avatar; 40 | item.user_name = user_name; 41 | return; 42 | } 43 | }) 44 | state.data.push(data); 45 | } 46 | } 47 | 48 | export default { 49 | state, 50 | actions, 51 | mutations 52 | } -------------------------------------------------------------------------------- /server/bin/www.js: -------------------------------------------------------------------------------- 1 | const app = require('../app'); 2 | const debug = require('debug')('demo:server'); 3 | const http = require('http'); 4 | 5 | // 设置端口号7778 6 | const port = normalizePort( process.env.PORT || "7778"); 7 | const server = http.createServer(app.callback()); 8 | 9 | server.listen(port); 10 | server.on('error', onError); 11 | server.on('listening', onListening); 12 | 13 | function normalizePort(val){ 14 | let port = parseInt(val,10); 15 | if(isNaN(port)) return port; 16 | if(port >= 0) return port; 17 | return false; 18 | } 19 | 20 | function onError(error){ 21 | if( error.syscall !== 'listen') throw error; 22 | 23 | let bind = typeof port == 'string'?'Pipe ' + port: 'port ' + port; 24 | 25 | switch(error.code){ 26 | case 'EACCES': 27 | console.error(bind + ' requires elevated privileges'); 28 | process.exit(1); 29 | break; 30 | case 'EADDRINUSE': 31 | console.error(bind + ' is already in use'); 32 | process.exit(1); 33 | break; 34 | default: 35 | throw error; 36 | } 37 | } 38 | 39 | function onListening() { 40 | let addr = server.address(); 41 | let bind = typeof addr === 'string' 42 | ? 'pipe ' + addr 43 | : 'port ' + addr.port; 44 | debug('Listening on ' + bind); 45 | } -------------------------------------------------------------------------------- /server/db/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Pawn 3 | * @Date: 2018-09-10 21:52:04 4 | * @Last Modified by: Pawn 5 | * @Last Modified time: 2018-09-11 20:35:44 6 | */ 7 | const mongoose = require('mongoose'); 8 | const db = mongoose.connect("mongodb://localhost:27017/test", {useNewUrlParser:true}, function(err){ 9 | if(err){ 10 | console.log(err) 11 | }else{ 12 | console.log("Connection success!") 13 | } 14 | }) 15 | const Schema = mongoose.Schema; 16 | 17 | // 用户 18 | let userSchema = new Schema({ 19 | user_name: String, 20 | user_id: String, 21 | user_pwd: String, 22 | avatar: { 23 | type: String, 24 | default: "" 25 | }, 26 | token: { 27 | type: String, 28 | default: "" 29 | } 30 | }) 31 | 32 | // 留言 33 | let commentSchema = new Schema({ 34 | user_id: { 35 | type: mongoose.Schema.ObjectId, 36 | ref: 'User' 37 | }, 38 | content: String, 39 | create_time: { 40 | type: String, 41 | default: Date.now 42 | } 43 | }) 44 | 45 | // 验证码 46 | let checkcodeSchema = new Schema({ 47 | token: String, 48 | code: String 49 | }) 50 | 51 | exports.User = mongoose.model('User', userSchema); 52 | exports.Comment = mongoose.model('Comment', commentSchema); 53 | exports.Checkcode = mongoose.model('Checkcode', checkcodeSchema); -------------------------------------------------------------------------------- /client/build/build.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | require('./check-versions')() 3 | 4 | process.env.NODE_ENV = 'production' 5 | 6 | const ora = require('ora') 7 | const rm = require('rimraf') 8 | const path = require('path') 9 | const chalk = require('chalk') 10 | const webpack = require('webpack') 11 | const config = require('../config') 12 | const webpackConfig = require('./webpack.prod.conf') 13 | 14 | const spinner = ora('building for production...') 15 | spinner.start() 16 | 17 | rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => { 18 | if (err) throw err 19 | webpack(webpackConfig, (err, stats) => { 20 | spinner.stop() 21 | if (err) throw err 22 | process.stdout.write(stats.toString({ 23 | colors: true, 24 | modules: false, 25 | children: false, // If you are using ts-loader, setting this to true will make TypeScript errors show up during build. 26 | chunks: false, 27 | chunkModules: false 28 | }) + '\n\n') 29 | 30 | if (stats.hasErrors()) { 31 | console.log(chalk.red(' Build failed with errors.\n')) 32 | process.exit(1) 33 | } 34 | 35 | console.log(chalk.cyan(' Build complete.\n')) 36 | console.log(chalk.yellow( 37 | ' Tip: built files are meant to be served over an HTTP server.\n' + 38 | ' Opening index.html over file:// won\'t work.\n' 39 | )) 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /client/src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | import userInfo from './module/userInfo' 4 | 5 | Vue.use(Vuex); 6 | 7 | const state = { 8 | user: { 9 | _id: window.sessionStorage.getItem('_id') || '', 10 | user_name: window.sessionStorage.getItem('user_name') || '', 11 | token: window.sessionStorage.getItem('token') || '', 12 | avatar: window.sessionStorage.getItem('avatar') || '' 13 | } 14 | } 15 | 16 | const mutations = { 17 | save: (state,data) => { 18 | state.user._id = data._id; 19 | state.user.token = data.token; 20 | state.user.user_name = data.user_name; 21 | state.user.avatar = data.avatar; 22 | 23 | window.sessionStorage.setItem('_id', data._id); 24 | window.sessionStorage.setItem('token', data.token); 25 | window.sessionStorage.setItem('user_name', data.user_name); 26 | window.sessionStorage.setItem('avatar', data.avatar); 27 | }, 28 | remove: (state) => { 29 | state.user.token = ''; 30 | state.user.user_name = ''; 31 | state.user.avatar = ''; 32 | state.user._id = ''; 33 | 34 | window.sessionStorage.removeItem('_id'); 35 | window.sessionStorage.removeItem('token'); 36 | window.sessionStorage.removeItem('user_name'); 37 | window.sessionStorage.removeItem('avatar'); 38 | } 39 | } 40 | 41 | export default new Vuex.Store({ 42 | modules: { 43 | userInfo 44 | }, 45 | state, 46 | mutations 47 | }); -------------------------------------------------------------------------------- /client/build/check-versions.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const chalk = require('chalk') 3 | const semver = require('semver') 4 | const packageConfig = require('../package.json') 5 | const shell = require('shelljs') 6 | 7 | function exec (cmd) { 8 | return require('child_process').execSync(cmd).toString().trim() 9 | } 10 | 11 | const versionRequirements = [ 12 | { 13 | name: 'node', 14 | currentVersion: semver.clean(process.version), 15 | versionRequirement: packageConfig.engines.node 16 | } 17 | ] 18 | 19 | if (shell.which('npm')) { 20 | versionRequirements.push({ 21 | name: 'npm', 22 | currentVersion: exec('npm --version'), 23 | versionRequirement: packageConfig.engines.npm 24 | }) 25 | } 26 | 27 | module.exports = function () { 28 | const warnings = [] 29 | 30 | for (let i = 0; i < versionRequirements.length; i++) { 31 | const mod = versionRequirements[i] 32 | 33 | if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) { 34 | warnings.push(mod.name + ': ' + 35 | chalk.red(mod.currentVersion) + ' should be ' + 36 | chalk.green(mod.versionRequirement) 37 | ) 38 | } 39 | } 40 | 41 | if (warnings.length) { 42 | console.log('') 43 | console.log(chalk.yellow('To use this template, you must update following to modules:')) 44 | console.log() 45 | 46 | for (let i = 0; i < warnings.length; i++) { 47 | const warning = warnings[i] 48 | console.log(' ' + warning) 49 | } 50 | 51 | console.log() 52 | process.exit(1) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /client/src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | 13 | 85 | -------------------------------------------------------------------------------- /server/utils/token.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Pawn 3 | * @Date: 2018-09-11 21:08:33 4 | * @Last Modified by: Pawn 5 | * @Last Modified time: 2018-09-19 20:44:43 6 | */ 7 | 8 | const jwt = require('jsonwebtoken'); 9 | const {TOKEN_ENCODE_STR, URL_YES_PASS} = require('./config'); 10 | const Checkcode = require('../db').Checkcode; 11 | const User = require('../db').User 12 | 13 | module.exports = { 14 | // 生成登录 token 15 | create_token(str){ 16 | return jwt.sign({str},TOKEN_ENCODE_STR, {expiresIn: '1h'}); 17 | }, 18 | /* 19 | 验证登录 token 是否正确 => 写成中间件 20 | get 请求与设置的请求不拦截验证,其余均需登录 21 | */ 22 | async check_token(ctx, next){ 23 | let url = ctx.url; 24 | if(ctx.method != 'GET' && !URL_YES_PASS.includes(url)){ 25 | let token = ctx.get("Authorization"); 26 | if(token == ''){ 27 | // 直接抛出错误 28 | ctx.response.status = 401; 29 | ctx.response.body = "你还没有登录,快去登录吧!"; 30 | return; 31 | } 32 | try { 33 | // 验证token是否过期 34 | let {str = ""} = await jwt.verify(token, TOKEN_ENCODE_STR); 35 | // 验证token与账号是否匹配 36 | let res = await User.find({user_id:str,token}); 37 | if(res.length == 0){ 38 | ctx.response.status = 401; 39 | ctx.response.body = "登录过期,请重新登录!"; 40 | return; 41 | } 42 | // 保存用户的_id,便于操作 43 | ctx._id = res[0]._id; 44 | }catch (e) { 45 | ctx.response.status = 401; 46 | ctx.response.body = "登录已过期请重新登录!"; 47 | return; 48 | } 49 | } 50 | await next(); 51 | }, 52 | // 验证 验证码 token 与 code 是否正确 53 | async check_token_code({token,code}){ 54 | try { 55 | // 验证码转大写 56 | code = code.toUpperCase(); 57 | await jwt.verify(token, TOKEN_ENCODE_STR); 58 | // 读数据库,删除验证码 59 | let res = await Checkcode.findOneAndDelete({token,code}); 60 | if(res == null){ 61 | return false; 62 | } 63 | }catch (e) { 64 | return false; 65 | } 66 | return true; 67 | } 68 | } -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "version": "1.0.0", 4 | "description": "A Vue.js project", 5 | "author": "LiChangyi <1052069088@qq.com>", 6 | "private": true, 7 | "scripts": { 8 | "dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js", 9 | "start": "npm run dev", 10 | "build": "node build/build.js" 11 | }, 12 | "dependencies": { 13 | "axios": "^0.18.0", 14 | "vue": "^2.5.2", 15 | "vue-cropper": "^0.3.6", 16 | "vue-router": "^3.0.1", 17 | "vuex": "^3.0.1" 18 | }, 19 | "devDependencies": { 20 | "autoprefixer": "^7.1.2", 21 | "babel-core": "^6.22.1", 22 | "babel-helper-vue-jsx-merge-props": "^2.0.3", 23 | "babel-loader": "^7.1.1", 24 | "babel-plugin-syntax-jsx": "^6.18.0", 25 | "babel-plugin-transform-runtime": "^6.22.0", 26 | "babel-plugin-transform-vue-jsx": "^3.5.0", 27 | "babel-preset-env": "^1.3.2", 28 | "babel-preset-stage-2": "^6.22.0", 29 | "chalk": "^2.0.1", 30 | "copy-webpack-plugin": "^4.0.1", 31 | "css-loader": "^0.28.0", 32 | "extract-text-webpack-plugin": "^3.0.0", 33 | "file-loader": "^1.1.4", 34 | "friendly-errors-webpack-plugin": "^1.6.1", 35 | "html-webpack-plugin": "^2.30.1", 36 | "node-notifier": "^5.1.2", 37 | "node-sass": "^4.9.3", 38 | "optimize-css-assets-webpack-plugin": "^3.2.0", 39 | "ora": "^1.2.0", 40 | "portfinder": "^1.0.13", 41 | "postcss-import": "^11.0.0", 42 | "postcss-loader": "^2.0.8", 43 | "postcss-url": "^7.2.1", 44 | "rimraf": "^2.6.0", 45 | "sass-loader": "^7.1.0", 46 | "semver": "^5.3.0", 47 | "shelljs": "^0.7.6", 48 | "uglifyjs-webpack-plugin": "^1.1.1", 49 | "url-loader": "^0.5.8", 50 | "vue-loader": "^13.3.0", 51 | "vue-style-loader": "^3.0.1", 52 | "vue-template-compiler": "^2.5.2", 53 | "webpack": "^3.6.0", 54 | "webpack-bundle-analyzer": "^2.9.0", 55 | "webpack-dev-server": "^2.9.1", 56 | "webpack-merge": "^4.1.0" 57 | }, 58 | "engines": { 59 | "node": ">= 6.0.0", 60 | "npm": ">= 3.0.0" 61 | }, 62 | "browserslist": [ 63 | "> 1%", 64 | "last 2 versions", 65 | "not ie <= 8" 66 | ] 67 | } 68 | -------------------------------------------------------------------------------- /client/config/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | // Template version: 1.3.1 3 | // see http://vuejs-templates.github.io/webpack for documentation. 4 | 5 | const path = require('path') 6 | 7 | module.exports = { 8 | dev: { 9 | 10 | // Paths 11 | assetsSubDirectory: 'static', 12 | assetsPublicPath: '/', 13 | proxyTable: { 14 | '/api':{ 15 | target: "http://localhost:7778", 16 | changeOrigin: true 17 | } 18 | }, 19 | 20 | // Various Dev Server settings 21 | host: 'localhost', // can be overwritten by process.env.HOST 22 | port: 8888, // can be overwritten by process.env.PORT, if port is in use, a free one will be determined 23 | autoOpenBrowser: false, 24 | errorOverlay: true, 25 | notifyOnErrors: true, 26 | poll: false, // https://webpack.js.org/configuration/dev-server/#devserver-watchoptions- 27 | 28 | 29 | /** 30 | * Source Maps 31 | */ 32 | 33 | // https://webpack.js.org/configuration/devtool/#development 34 | devtool: 'cheap-module-eval-source-map', 35 | 36 | // If you have problems debugging vue-files in devtools, 37 | // set this to false - it *may* help 38 | // https://vue-loader.vuejs.org/en/options.html#cachebusting 39 | cacheBusting: true, 40 | 41 | cssSourceMap: true 42 | }, 43 | 44 | build: { 45 | // Template for index.html 46 | index: path.resolve(__dirname, '../dist/index.html'), 47 | 48 | // Paths 49 | assetsRoot: path.resolve(__dirname, '../dist'), 50 | assetsSubDirectory: 'static', 51 | assetsPublicPath: '', 52 | 53 | /** 54 | * Source Maps 55 | */ 56 | 57 | productionSourceMap: true, 58 | // https://webpack.js.org/configuration/devtool/#production 59 | devtool: '#source-map', 60 | 61 | // Gzip off by default as many popular static hosts such as 62 | // Surge or Netlify already gzip all static assets for you. 63 | // Before setting to `true`, make sure to: 64 | // npm install --save-dev compression-webpack-plugin 65 | productionGzip: false, 66 | productionGzipExtensions: ['js', 'css'], 67 | 68 | // Run the build command with an extra argument to 69 | // View the bundle analyzer report after build finishes: 70 | // `npm run build --report` 71 | // Set to `true` or `false` to always turn it on or off 72 | bundleAnalyzerReport: process.env.npm_config_report 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /server/controller/other.js: -------------------------------------------------------------------------------- 1 | const BMP24 = require('gd-bmp').BMP24; 2 | const {create_token} = require('../utils/token'); 3 | const Checkcode = require('../db').Checkcode; 4 | 5 | module.exports = { 6 | // 验证码获取 7 | async checkcode(ctx, next){ 8 | try { 9 | let {code,img} = makeCapcha(); 10 | let token = create_token(code); 11 | await new Checkcode({token,code}).save(); 12 | 13 | ctx.body = { 14 | code: 200, 15 | msg: '获取验证码成功!', 16 | data: { 17 | token, 18 | img: "data:image/bmp;base64," + img.getFileData().toString('base64') 19 | } 20 | } 21 | } catch(e){ 22 | console.log(e); 23 | ctx.body = { 24 | code: 500, 25 | msg: '获取验证码失败!' 26 | } 27 | } 28 | } 29 | } 30 | 31 | function rand(min, max) { 32 | return Math.random()*(max-min+1) + min | 0; //特殊的技巧,|0可以强制转换为整数 33 | } 34 | 35 | function makeCapcha() { 36 | var img = new BMP24(100, 40); 37 | img.drawCircle(rand(0, 100), rand(0, 40), rand(10 , 40), rand(0, 0xffffff)); 38 | //边框 39 | img.drawRect(0, 0, img.w-1, img.h-1, rand(0, 0xffffff)); 40 | img.fillRect(0, 0, 100, 40, 0x252632); 41 | // img.fillRect(rand(0, 100), rand(0, 40), rand(10, 35), rand(10, 35), rand(0, 0xffffff)); 42 | img.drawLine(rand(0, 100), rand(0, 40), rand(0, 100), rand(0, 40), rand(0, 0xffffff)); 43 | //return img; 44 | //画曲线 45 | var w=img.w/2; 46 | var h=img.h; 47 | var color = rand(0, 0xffffff); 48 | var y1=rand(-5,5); //Y轴位置调整 49 | var w2=rand(10,15); //数值越小频率越高 50 | var h3=rand(4,6); //数值越小幅度越大 51 | var bl = rand(1,5); 52 | for(var i=-w; i { 18 | // 每次发送请求,检查 vuex 中是否有token,如果有放在headers中 19 | if(store.state.user.token){ 20 | config.headers.Authorization = store.state.user.token; 21 | } 22 | return config; 23 | }, 24 | err => { 25 | return Promise.reject(err); 26 | } 27 | ) 28 | 29 | // respone拦截器 30 | instance.interceptors.response.use( 31 | response => { 32 | return response; 33 | }, 34 | // 除了200以外的请求到这里来,,这里的200不是我们设置的那个code200,,我这里是,没有登录才会不返回200 35 | error => { 36 | let { response } = error; 37 | if(response != null){ 38 | // 这里为什么处理401错误,详见,server/untils/token check_token这个函数 39 | if(response.status == 401) { 40 | let msg = response.data || '请重新登录!'; 41 | alert(msg); 42 | store.commit("remove") // token过期,清除 43 | router.replace({ //跳转到登录页面 44 | path: '/login', 45 | // 添加一个重定向后缀,等登录以后再到这里来 46 | query: { redirect: router.currentRoute.fullPath } 47 | }); 48 | return Promise.reject(error.response); 49 | } 50 | }else { 51 | console.log(error) 52 | } 53 | } 54 | ) 55 | 56 | // 添加API请求 57 | // 这里填写的url见 server/routes/index.js 记住一定要配置代理,不然会返回404错误 58 | export default { 59 | // 获取验证码 60 | api_get_checkcode(){ 61 | return instance.get('/api/other/checkcode'); 62 | }, 63 | // 用户注册 64 | api_add_user(data){ 65 | return instance.post('/api/user', data); 66 | }, 67 | // 用户登录 68 | api_login_user(data){ 69 | return instance.post('/api/user/login', data); 70 | }, 71 | // 添加留言 72 | api_add_leave(data){ 73 | return instance.post('/api/leave', data); 74 | }, 75 | // 获取留言 76 | api_get_leave(data){ 77 | let {page = 1, size = 10} = data; 78 | return instance.get(`/api/leave?page=${page}&size=${size}`); 79 | }, 80 | // 删除留言 81 | api_del_leave(id){ 82 | return instance.delete(`/api/leave/${id}`); 83 | }, 84 | // 根据用户id获取用户信息 85 | api_get_user(id){ 86 | return instance.get(`/api/user?_id=${id}`); 87 | } 88 | } -------------------------------------------------------------------------------- /client/src/views/Home.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 76 | 77 | 96 | -------------------------------------------------------------------------------- /client/src/components/Loading.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 112 | -------------------------------------------------------------------------------- /client/src/views/Login.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /client/src/components/Paginator.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 71 | 72 | 73 | 107 | 108 | -------------------------------------------------------------------------------- /client/build/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const path = require('path') 3 | const config = require('../config') 4 | const ExtractTextPlugin = require('extract-text-webpack-plugin') 5 | const packageConfig = require('../package.json') 6 | 7 | exports.assetsPath = function (_path) { 8 | const assetsSubDirectory = process.env.NODE_ENV === 'production' 9 | ? config.build.assetsSubDirectory 10 | : config.dev.assetsSubDirectory 11 | 12 | return path.posix.join(assetsSubDirectory, _path) 13 | } 14 | 15 | exports.cssLoaders = function (options) { 16 | options = options || {} 17 | 18 | const cssLoader = { 19 | loader: 'css-loader', 20 | options: { 21 | sourceMap: options.sourceMap 22 | } 23 | } 24 | 25 | const postcssLoader = { 26 | loader: 'postcss-loader', 27 | options: { 28 | sourceMap: options.sourceMap 29 | } 30 | } 31 | 32 | // generate loader string to be used with extract text plugin 33 | function generateLoaders (loader, loaderOptions) { 34 | const loaders = options.usePostCSS ? [cssLoader, postcssLoader] : [cssLoader] 35 | 36 | if (loader) { 37 | loaders.push({ 38 | loader: loader + '-loader', 39 | options: Object.assign({}, loaderOptions, { 40 | sourceMap: options.sourceMap 41 | }) 42 | }) 43 | } 44 | 45 | // Extract CSS when that option is specified 46 | // (which is the case during production build) 47 | if (options.extract) { 48 | return ExtractTextPlugin.extract({ 49 | use: loaders, 50 | fallback: 'vue-style-loader' 51 | }) 52 | } else { 53 | return ['vue-style-loader'].concat(loaders) 54 | } 55 | } 56 | 57 | // https://vue-loader.vuejs.org/en/configurations/extract-css.html 58 | return { 59 | css: generateLoaders(), 60 | postcss: generateLoaders(), 61 | less: generateLoaders('less'), 62 | sass: generateLoaders('sass', { indentedSyntax: true }), 63 | scss: generateLoaders('sass'), 64 | stylus: generateLoaders('stylus'), 65 | styl: generateLoaders('stylus') 66 | } 67 | } 68 | 69 | // Generate loaders for standalone style files (outside of .vue) 70 | exports.styleLoaders = function (options) { 71 | const output = [] 72 | const loaders = exports.cssLoaders(options) 73 | 74 | for (const extension in loaders) { 75 | const loader = loaders[extension] 76 | output.push({ 77 | test: new RegExp('\\.' + extension + '$'), 78 | use: loader 79 | }) 80 | } 81 | 82 | return output 83 | } 84 | 85 | exports.createNotifierCallback = () => { 86 | const notifier = require('node-notifier') 87 | 88 | return (severity, errors) => { 89 | if (severity !== 'error') return 90 | 91 | const error = errors[0] 92 | const filename = error.file && error.file.split('!').pop() 93 | 94 | notifier.notify({ 95 | title: packageConfig.name, 96 | message: severity + ': ' + error.name, 97 | subtitle: filename || '', 98 | icon: path.join(__dirname, 'logo.png') 99 | }) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /client/src/components/CommentItem.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 54 | 55 | 56 | 125 | -------------------------------------------------------------------------------- /server/controller/leave.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Pawn 3 | * @Date: 2018-09-13 18:29:26 4 | * @Last Modified by: Pawn 5 | * @Last Modified time: 2018-09-19 20:41:42 6 | */ 7 | const Comment = require('../db').Comment 8 | const xss = require("xss"); 9 | 10 | module.exports = { 11 | /* 12 | @name: 获取留言 13 | @param: size => 返回最大个数 默认:10 14 | @param: page => 当前页数 默认:1 15 | */ 16 | async getLeaves(ctx, next) { 17 | let {size = 10, page = 1} = ctx.query; 18 | /* 19 | 这里分页查询有2种方法: 20 | 1. 第一种方法,就是我这里用到的,用skip跳过多少个,然后limit取到多少个 21 | 2. 第二种方法,在客服端把当前的最后一个的_id给传过来,然后用$lt来找到比当前_id小的。就吧page换成最后一个元素的_id 22 | 在效率上来讲,第二种方法效率更高,但是如果你想放回的内容的不是按_id排序的,那还是用第一种方法吧 23 | */ 24 | try { 25 | let options = { 26 | skip: Number((page-1)*size), 27 | limit: Number(size), 28 | sort:{"create_time":"-1"}, 29 | // populate: { 30 | // path: 'user_id', 31 | // select: "_id user_name avatar" 32 | // } 33 | } 34 | let res = await Comment.find({},null,options); 35 | let total = await Comment.countDocuments(); 36 | ctx.body = { 37 | code: 200, 38 | msg: '获取留言成功', 39 | data: { 40 | list: res, 41 | pagination: { 42 | total, 43 | page : Number(page), 44 | size : Number(size) 45 | } 46 | } 47 | } 48 | }catch(e) { 49 | console.log(e); 50 | ctx.body = { 51 | code: 500, 52 | msg: "获取留言失败,服务器异常,请稍后再试!" 53 | } 54 | } 55 | 56 | }, 57 | // 添加留言 58 | async addLeaver(ctx, next) { 59 | let { content = '' } = ctx.request.body; 60 | if(content == ''){ 61 | ctx.body = { 62 | code: 401, 63 | msg: "留言失败,请写点什么吧!" 64 | } 65 | return; 66 | } 67 | if(content.length >= 150){ 68 | ctx.body = { 69 | code: 401, 70 | msg: "你说的太多了,最多只能输入150个字符哦。" 71 | } 72 | return; 73 | } 74 | // 转义,防止xss攻击 75 | content = xss(content); 76 | try { 77 | let comment = new Comment({ 78 | user_id: ctx._id, 79 | content 80 | }); 81 | let res = await comment.save(); 82 | if(res._id != null){ 83 | ctx.body = { 84 | code: 200, 85 | msg: '留言成功!', 86 | data: res 87 | } 88 | }else{ 89 | ctx.body = { 90 | code: 500, 91 | msg: '留言失败,服务器异常,请稍后再试!' 92 | } 93 | } 94 | } catch (e){ 95 | console.log(e); 96 | ctx.body = { 97 | code: 500, 98 | msg: '留言失败,服务器异常,请稍后再试!' 99 | } 100 | } 101 | }, 102 | // 删除留言 103 | async deleteLeaver(ctx, next){ 104 | let _id = ctx.params.id; 105 | try { 106 | let res = await Comment.findOneAndDelete({_id,user_id: ctx._id}); 107 | if(res == null){ 108 | ctx.body = { 109 | code: 500, 110 | msg: '删除留言失败,服务器异常!' 111 | } 112 | }else { 113 | ctx.body = { 114 | code: 200, 115 | msg: '删除留言成功!' 116 | } 117 | } 118 | } catch(e){ 119 | console.log(e); 120 | ctx.body = { 121 | code: 500, 122 | msg: '删除留言失败,服务器异常!' 123 | } 124 | } 125 | } 126 | } -------------------------------------------------------------------------------- /client/build/webpack.dev.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const utils = require('./utils') 3 | const webpack = require('webpack') 4 | const config = require('../config') 5 | const merge = require('webpack-merge') 6 | const path = require('path') 7 | const baseWebpackConfig = require('./webpack.base.conf') 8 | const CopyWebpackPlugin = require('copy-webpack-plugin') 9 | const HtmlWebpackPlugin = require('html-webpack-plugin') 10 | const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin') 11 | const portfinder = require('portfinder') 12 | 13 | const HOST = process.env.HOST 14 | const PORT = process.env.PORT && Number(process.env.PORT) 15 | 16 | const devWebpackConfig = merge(baseWebpackConfig, { 17 | module: { 18 | rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap, usePostCSS: true }) 19 | }, 20 | // cheap-module-eval-source-map is faster for development 21 | devtool: config.dev.devtool, 22 | 23 | // these devServer options should be customized in /config/index.js 24 | devServer: { 25 | clientLogLevel: 'warning', 26 | historyApiFallback: { 27 | rewrites: [ 28 | { from: /.*/, to: path.posix.join(config.dev.assetsPublicPath, 'index.html') }, 29 | ], 30 | }, 31 | hot: true, 32 | contentBase: false, // since we use CopyWebpackPlugin. 33 | compress: true, 34 | host: HOST || config.dev.host, 35 | port: PORT || config.dev.port, 36 | open: config.dev.autoOpenBrowser, 37 | overlay: config.dev.errorOverlay 38 | ? { warnings: false, errors: true } 39 | : false, 40 | publicPath: config.dev.assetsPublicPath, 41 | proxy: config.dev.proxyTable, 42 | quiet: true, // necessary for FriendlyErrorsPlugin 43 | watchOptions: { 44 | poll: config.dev.poll, 45 | } 46 | }, 47 | plugins: [ 48 | new webpack.DefinePlugin({ 49 | 'process.env': require('../config/dev.env') 50 | }), 51 | new webpack.HotModuleReplacementPlugin(), 52 | new webpack.NamedModulesPlugin(), // HMR shows correct file names in console on update. 53 | new webpack.NoEmitOnErrorsPlugin(), 54 | // https://github.com/ampedandwired/html-webpack-plugin 55 | new HtmlWebpackPlugin({ 56 | filename: 'index.html', 57 | template: 'index.html', 58 | inject: true 59 | }), 60 | // copy custom static assets 61 | new CopyWebpackPlugin([ 62 | { 63 | from: path.resolve(__dirname, '../static'), 64 | to: config.dev.assetsSubDirectory, 65 | ignore: ['.*'] 66 | } 67 | ]) 68 | ] 69 | }) 70 | 71 | module.exports = new Promise((resolve, reject) => { 72 | portfinder.basePort = process.env.PORT || config.dev.port 73 | portfinder.getPort((err, port) => { 74 | if (err) { 75 | reject(err) 76 | } else { 77 | // publish the new Port, necessary for e2e tests 78 | process.env.PORT = port 79 | // add port to devServer config 80 | devWebpackConfig.devServer.port = port 81 | 82 | // Add FriendlyErrorsPlugin 83 | devWebpackConfig.plugins.push(new FriendlyErrorsPlugin({ 84 | compilationSuccessInfo: { 85 | messages: [`Your application is running here: http://${devWebpackConfig.devServer.host}:${port}`], 86 | }, 87 | onErrors: config.dev.notifyOnErrors 88 | ? utils.createNotifierCallback() 89 | : undefined 90 | })) 91 | 92 | resolve(devWebpackConfig) 93 | } 94 | }) 95 | }) 96 | -------------------------------------------------------------------------------- /client/src/components/CommentBox.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 66 | 67 | 68 | 153 | -------------------------------------------------------------------------------- /server/controller/user.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Pawn 3 | * @Date: 2018-09-11 19:49:02 4 | * @Last Modified by: Pawn 5 | * @Last Modified time: 2018-09-19 19:07:36 6 | */ 7 | const User = require('../db').User; 8 | const sha1 = require('sha1'); 9 | const { PWD_ENCODE_STR} = require('../utils/config'); 10 | const {create_token, check_token_code} = require('../utils/token'); 11 | const xss = require('xss') 12 | 13 | module.exports = { 14 | // 用户注册 15 | async register(ctx, next){ 16 | let {user_name = '', user_id = '', user_pwd = '', re_user_pwd = '', avatar = "", code = '', code_token = ''} = ctx.request.body; 17 | try { 18 | if(user_name == '' || user_id == "" || user_pwd == ""){ 19 | ctx.body = { 20 | code: 401, 21 | msg: "注册失败,请填写完整表单!" 22 | } 23 | return; 24 | } 25 | if(avatar == ''){ 26 | ctx.body = { 27 | code: 401, 28 | msg: "注册失败,请上传头像!" 29 | } 30 | return; 31 | } 32 | if(user_pwd.length < 5){ 33 | ctx.body = { 34 | code: 401, 35 | msg: '注册失败,密码最少为5位!' 36 | } 37 | return; 38 | } 39 | if(user_pwd != re_user_pwd){ 40 | ctx.body = { 41 | code: 401, 42 | msg: "注册失败,2次密码输入不一致!" 43 | } 44 | return; 45 | } 46 | 47 | // 验证码判断 48 | let mark = await check_token_code({token:code_token,code}); 49 | if(!mark){ 50 | ctx.body = { 51 | code: 401, 52 | msg: '登录失败,验证码错误!' 53 | } 54 | return; 55 | } 56 | 57 | // 判断 user_id 是否重复 58 | let res = await User.find({user_id}); 59 | if(res.length != 0){ 60 | ctx.body = { 61 | code: 409, 62 | msg: '注册失败,登录账号重复了,换一个吧!' 63 | } 64 | return; 65 | } 66 | user_pwd = sha1(sha1(user_pwd + PWD_ENCODE_STR)); 67 | // 防止xss攻击, 转义 68 | user_name = xss(user_name); 69 | let token = create_token(user_id); 70 | let user = new User({user_id,user_name,user_pwd,avatar,token}); 71 | res = await user.save(); 72 | if(res._id != null){ 73 | ctx.body = { 74 | code: 200, 75 | msg: "注册成功!", 76 | data: { 77 | _id: res._id, 78 | user_name, 79 | avatar, 80 | token, 81 | } 82 | } 83 | }else{ 84 | ctx.body = { 85 | code: 500, 86 | msg: "注册失败,服务器异常!" 87 | } 88 | } 89 | }catch (e){ 90 | console.log(e); 91 | ctx.body = { 92 | code: 500, 93 | msg: "注册失败,服务器异常!" 94 | } 95 | } 96 | }, 97 | // 用户登录 98 | async login(ctx, next){ 99 | let {user_id = '', user_pwd = '' , code = "" , code_token = ''} = ctx.request.body; 100 | try { 101 | if(user_id == '' || user_pwd == ''){ 102 | ctx.body = { 103 | code: 401, 104 | msg: "登录失败,请输入登录账号或密码!" 105 | } 106 | return; 107 | } 108 | // 验证码判断 109 | let mark = await check_token_code({token:code_token,code}); 110 | if(!mark){ 111 | ctx.body = { 112 | code: 401, 113 | msg: '登录失败,验证码错误!' 114 | } 115 | return; 116 | } 117 | user_pwd = sha1(sha1(user_pwd + PWD_ENCODE_STR)); 118 | let res = await User.find({user_id,user_pwd}); 119 | if(res.length == 0){ 120 | ctx.body = { 121 | code: 401, 122 | msg: '登录失败,用户名或者密码错误!' 123 | } 124 | return; 125 | } 126 | let token = create_token(user_id); 127 | res[0].token = token; 128 | res[0].save(); 129 | ctx.body = { 130 | code: 200, 131 | msg: "登录成功!", 132 | data: { 133 | _id: res[0]._id, 134 | user_name: res[0].user_name, 135 | avatar: res[0].avatar, 136 | token 137 | } 138 | } 139 | } catch(e){ 140 | console.log(e); 141 | ctx.body = { 142 | code: 500, 143 | msg: '登录失败,服务器异常!' 144 | } 145 | } 146 | }, 147 | // 通过_id 获取用户信息 148 | async query(ctx, next){ 149 | let _id = ctx.query._id; 150 | if(_id.length != 24){ 151 | ctx.body = { 152 | code: 401, 153 | msg: '查询失败,_id错误!' 154 | } 155 | return; 156 | } 157 | try { 158 | let res = await User.findOne({_id},{avatar:true,_id: true,user_name:true}); 159 | ctx.body = { 160 | code: 200, 161 | msg: '查询成功!', 162 | data: res 163 | } 164 | }catch(e){ 165 | console.log(e); 166 | ctx.body = { 167 | code: 500, 168 | msg: '查询失败,服务器异常,请稍后再试!' 169 | } 170 | } 171 | } 172 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vue+Koa+Mongodb 小练习 2 | 3 | > 作者: Pawn 4 | > 5 | > 本文首发: [Pawn博客](http://blog.lcylove.cn) 6 | > 7 | > 功能: 基于vue koa mongodb进行登录,注册,留言的简单网站。 8 | > 9 | > 体验地址: [http://demo.lcylove.cn](http://demo.lcylove.cn) 10 | > 11 | > github: [https://github.com/LiChangyi/demo1](https://github.com/LiChangyi/demo1) 12 | 13 | ## 前面的话 14 | 15 | ### 原因 16 | 17 | 1. 前段时间用vue+koa+mongodb搭建了一个个人博客,因为是第一次写前后交互,发现有很多地方不是特别的完善,同时代码对于新学者来说可读性也不是很大。所以这个小练习,从一个简单的方面入手,希望能给踩过同样多坑的同路人一点启发。 18 | 2. 在我今年年初在学习vue以及koa的时候,网上对这方面的内容,都是一个完整的项目,文件太多,观看难度太大,其次是,都对图片的上传都没怎么涉及。 19 | 3. 我在学习部署koa和vue的项目的时候,网上的知识很零碎,这里我会归纳一下。 20 | 21 | ### 涉及知识点 22 | 23 | 1. vue全家桶的使用 24 | 2. 在vue中使用axios,并配置它 25 | 3. koa与mongoose的基本使用 26 | 4. jsonwebtoken的使用以及前后台鉴定登录 27 | 28 | **注: 本文面对刚学vue或者koa不久或者想了解一个简单的前后台交互的问题的同学,涉及基础。** 29 | 30 | ### 预览 31 | 32 | ![首页预览](http://image.lcylove.cn/165eb2bbfc3_57e8bb55) 33 | 34 | ![登录预览](http://image.lcylove.cn/165eb2bd2fc_da979ffb) 35 | 36 | ![注册预览](http://image.lcylove.cn/165eb2bdd1c_34336d24) 37 | 38 | ## 感兴趣的问题 39 | 40 | > 我在代码里面写了很多的注释方便阅读,这里简单说一下我个人当初学习的时候比较疑惑为问题 41 | 42 | ### 一些小变动 43 | 44 | 前端直接采用vue-cli进行一个基础的项目骨架。然后由于是一个简单的项目,所以页面就随便写一下,主要是实现功能。 45 | 46 | 因为我们在服务器上面采用的是二级域名的形式,所以需要在 `config/index.js` 下面的 `build` 项里面将 `assetsPublicPath` 设置成相对路径。 47 | 48 | ```javascript 49 | assetsPublicPath: '' 50 | ``` 51 | 52 | 我们在本地开发的时候需要进行调试,需要用到代理,不然就只有设置后台允许跨域。所以在 `config/index.js` 下面的 `dev` 对象里面添加: 53 | 54 | ``` javascript 55 | proxyTable: { 56 | '/api':{ // 只代理 /api url下的请求 57 | target: "http://localhost:7778", // 后台服务器的地址 58 | changeOrigin: true 59 | } 60 | } 61 | ``` 62 | 63 | ### 如何让服务器端记住你(jsonwebtoken) 64 | 65 | HTTP请求是无状态的,意思是他记不住你这个人是谁,他只知道你要什么资源,然后给你什么。但是实际问题是当用户给我们寻求资源的时候,我们应该要考虑应该给他这个资源。对这个人的身份做一个判别,然后在做决定给他什么样的资源。 66 | 67 | 所以针对每个用户我们需要用一个唯一的标识来确定他,这就是为什么需要登录才能操作,登录的目的就是让服务端产生一个认识你的标识,以后你的每次请求都要带上去。 68 | 69 | 在前后台不分离的时候,服务器端往往会在客服端放一个`SessionId` 或者一个`cookie`的东西。但是现在前后端分离以后,我们登录成功,服务器端应该也会给我们这样一个唯一标识身份的字符串。然后我们在每次请求数据的时候带上它。这里我服务器端采用的是`jsonwebtoken` 来制造这个唯一标识,代码详情 => `server/utils/token.js` 然后我写了一个中间件`check_token` 来判断如果这个资源需要登录,就会去检查他的token如果token不对那么就直接抛出错误。 70 | 71 | 前端拿到服务端的token后我们需要把他存放起来这里大概会有2种方式: 72 | 73 | 1. 存在vuex 里面, 这种方式有一个弊端就是,网页一刷新vuex里面的数据就清空了。就意味着要重新登录。 74 | 2. 存在sessionStorage里面,采用浏览器的会话存储,只有当浏览器关闭的时候才会清空数据。 75 | 76 | 这里我把2种方法结合起来,得到token的时候把他同时存放在vuex和sessionStorage里面,存放在vuex里面是为了操作方便,存放在sessionStorage是为了保持刷新页面的时候数据不丢失。在前端每次向后台请求数据的时候,带上这个token,详见代码 => `client/src/axios/index.js` 77 | 78 | 关于一些网上的争论: 79 | 80 | Q :有人说,让客服端存放token不安全,或者说用sessionStorage方法来存放不安全,因为存在着csrf问题 81 | 82 | A :没有绝对的安全,我个人了解到就是用以前的`cookie`或者`SessionId` 也存在着这样的问题。想要解决这个问题就尽量的吧网页升级成https,或者,采用服务器中转的方式,在2者之间在加入一个服务器端,把真实的token存放在中转,然后客服端与中转进行通信。 83 | 84 | ### 验证码的识别 85 | 86 | 验证码的生成我采用了`gd-bmp` 包具体用法,看`server/controller/other.js` 同样根据上面的介绍,http是没有状态的,我们要验证验证码的正确性,应该对每个验证码增加一个唯一的标识,然后存放在数据,当用户登录或者用户注册用到验证码的时候,把验证码和相应的验证码标识一起发往后台,然后判断验证码的正确与否。对于验证码及标识的存储,我这里为了方便就是采用mongodb来存储,但是网上很多人推荐用redis来存储。 87 | 88 | ### 本地图片的上传 89 | 90 | 这个问题从很久以前就很迷惑,一直不知道如何上传图片到服务器。即使h5出现了``但是解决这个问题也是很麻烦。我个人觉得上传图片应该有2种方式: 91 | 92 | 1. 直接用过input的onchang事件获取到的文件,来上传二进制文件。 93 | 2. 将图片转换成base64来进行上传 94 | 95 | 我这里采用的是第二种,用base64上传图片,然后自己吧base64字符串保存进数据库,因为操作比较方便。当然你也可以在服务器端吧base64转换成二进制文件存放在服务器里面,然后把文件地址保存在数据库里面。也可以在本地直接上传二进制文件,如果你采用这个方式,那么你应该在koa里面在加入一个处理file请求的中间件。 96 | 97 | 也可以借助第三方的存储,比如我在我的博客里面写了一个接口就是直接在客服端上传文件到七牛云,然后七牛云返回给我链接。当时之所以采用这个操作是因为,小水管服务器太慢了,借助第三方加载图片会快很多。 98 | 99 | ### 关于项目的服务器部署 100 | 101 | 因为vue的简单,很多都只知道`npm install` 和 `npm run dev` 所以有很多人会有疑问,那就是我这个vue项目如何部署在服务器上面?难道是把代码上传到服务器上面来执行上面2条命令吗? 102 | 103 | 其实这个问题是由于大家只会机械式操作留下的,因为vue-cli的简单方便已经mvvm框架的厉害,我们忘记了我们写的东西本子上还是网页。所以我们需要用`webpack` 将我们的项目打包一下在命令行里面执行`npm run build` 将我们写的vue和js代码以及其他的资源文件,打包`/dist`里面。这里面的文件就是我们写的网页,,我们只需要吧这里面的文件上传到服务器下就可以运行了。 104 | 105 | 这里关于把打包出来的文件往往会有2方式运行: 106 | 107 | 1. 将文件丢到`server/public` 文件夹下面,因为我们在`server/app.js`下面配置了静态文件目录,然后我们启动服务端。就可以在`127.0.0.1:7778/index.html`(假设服务器端口号是7778)看到我们的网页。 108 | 2. 用nginx服务器代理 ,静态文件用nginx托管,然后设置转发的方式来获取api请求数据。 109 | 110 | 其实第一种的话也是借助与nodejs会自动启动一个服务器,进行静态文件的托管。我个人比较喜欢第二种方法,下面我们就进行这种文件的配置。 111 | 112 | 开始之前,你应该检查你的服务器是否安装有`nginx`与`pm2` 113 | 114 | ```bash 115 | $ pm2 -v 116 | $ nginx -v 117 | ``` 118 | 119 | 如果正确出现版本号,那么就已经安装了,如果没有的话,请谷歌安装。`pm2`的作用是进行进程守护,当你的nodejs意外的停止的时候,进行重启。 120 | 121 | 如果我们有域名的话,我们现在域名商哪里添加一个二级域名解析。这里添加完解析以后会要几分钟的等待时间 122 | 123 | ![添加域名解析](http://image.lcylove.cn/165eb6463fe_6d6fc408) 124 | 125 | 然后,我们找到nginx的配置文件`nginx.conf` 在里面加入: 126 | 127 | ``` 128 | server { 129 | listen 80; 130 | server_name demo.lcylove.cn; 131 | root /data/www/demo; 132 | index index.html index.htm index.php; 133 | 134 | location /api/ { 135 | proxy_set_header X-Real-IP $remote_addr; 136 | proxy_set_header Host $http_host; 137 | proxy_pass http://127.0.0.1:7778; 138 | } 139 | } 140 | ``` 141 | 142 | 注意: `location /api/` 这里说明只有api/*的请求才会进行转发。 143 | 144 | 然后进行nginx服务器的重启:`nginx -s reload` 145 | 146 | 我们把server的代码放在服务器下,通过命令行移到相应位置执行命令: 147 | 148 | ``` bash 149 | $ npm install && cnpm i 150 | $ pm2 start --name demo1 npm -- run start 151 | ``` 152 | 153 | 启动我们的nodejs服务器。然后我们就可以打开网站 [demo.lcylove.cn](http://demo.lcylove.cn) 查看效果 154 | 155 | ## 最后 156 | 157 | 由于本人才疏学浅,如果有任何问题的欢迎下面留言讨论! -------------------------------------------------------------------------------- /client/build/webpack.prod.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const path = require('path') 3 | const utils = require('./utils') 4 | const webpack = require('webpack') 5 | const config = require('../config') 6 | const merge = require('webpack-merge') 7 | const baseWebpackConfig = require('./webpack.base.conf') 8 | const CopyWebpackPlugin = require('copy-webpack-plugin') 9 | const HtmlWebpackPlugin = require('html-webpack-plugin') 10 | const ExtractTextPlugin = require('extract-text-webpack-plugin') 11 | const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin') 12 | const UglifyJsPlugin = require('uglifyjs-webpack-plugin') 13 | 14 | const env = require('../config/prod.env') 15 | 16 | const webpackConfig = merge(baseWebpackConfig, { 17 | module: { 18 | rules: utils.styleLoaders({ 19 | sourceMap: config.build.productionSourceMap, 20 | extract: true, 21 | usePostCSS: true 22 | }) 23 | }, 24 | devtool: config.build.productionSourceMap ? config.build.devtool : false, 25 | output: { 26 | path: config.build.assetsRoot, 27 | filename: utils.assetsPath('js/[name].[chunkhash].js'), 28 | chunkFilename: utils.assetsPath('js/[id].[chunkhash].js') 29 | }, 30 | plugins: [ 31 | // http://vuejs.github.io/vue-loader/en/workflow/production.html 32 | new webpack.DefinePlugin({ 33 | 'process.env': env 34 | }), 35 | new UglifyJsPlugin({ 36 | uglifyOptions: { 37 | compress: { 38 | warnings: false 39 | } 40 | }, 41 | sourceMap: config.build.productionSourceMap, 42 | parallel: true 43 | }), 44 | // extract css into its own file 45 | new ExtractTextPlugin({ 46 | filename: utils.assetsPath('css/[name].[contenthash].css'), 47 | // Setting the following option to `false` will not extract CSS from codesplit chunks. 48 | // Their CSS will instead be inserted dynamically with style-loader when the codesplit chunk has been loaded by webpack. 49 | // It's currently set to `true` because we are seeing that sourcemaps are included in the codesplit bundle as well when it's `false`, 50 | // increasing file size: https://github.com/vuejs-templates/webpack/issues/1110 51 | allChunks: true, 52 | }), 53 | // Compress extracted CSS. We are using this plugin so that possible 54 | // duplicated CSS from different components can be deduped. 55 | new OptimizeCSSPlugin({ 56 | cssProcessorOptions: config.build.productionSourceMap 57 | ? { safe: true, map: { inline: false } } 58 | : { safe: true } 59 | }), 60 | // generate dist index.html with correct asset hash for caching. 61 | // you can customize output by editing /index.html 62 | // see https://github.com/ampedandwired/html-webpack-plugin 63 | new HtmlWebpackPlugin({ 64 | filename: config.build.index, 65 | template: 'index.html', 66 | inject: true, 67 | minify: { 68 | removeComments: true, 69 | collapseWhitespace: true, 70 | removeAttributeQuotes: true 71 | // more options: 72 | // https://github.com/kangax/html-minifier#options-quick-reference 73 | }, 74 | // necessary to consistently work with multiple chunks via CommonsChunkPlugin 75 | chunksSortMode: 'dependency' 76 | }), 77 | // keep module.id stable when vendor modules does not change 78 | new webpack.HashedModuleIdsPlugin(), 79 | // enable scope hoisting 80 | new webpack.optimize.ModuleConcatenationPlugin(), 81 | // split vendor js into its own file 82 | new webpack.optimize.CommonsChunkPlugin({ 83 | name: 'vendor', 84 | minChunks (module) { 85 | // any required modules inside node_modules are extracted to vendor 86 | return ( 87 | module.resource && 88 | /\.js$/.test(module.resource) && 89 | module.resource.indexOf( 90 | path.join(__dirname, '../node_modules') 91 | ) === 0 92 | ) 93 | } 94 | }), 95 | // extract webpack runtime and module manifest to its own file in order to 96 | // prevent vendor hash from being updated whenever app bundle is updated 97 | new webpack.optimize.CommonsChunkPlugin({ 98 | name: 'manifest', 99 | minChunks: Infinity 100 | }), 101 | // This instance extracts shared chunks from code splitted chunks and bundles them 102 | // in a separate chunk, similar to the vendor chunk 103 | // see: https://webpack.js.org/plugins/commons-chunk-plugin/#extra-async-commons-chunk 104 | new webpack.optimize.CommonsChunkPlugin({ 105 | name: 'app', 106 | async: 'vendor-async', 107 | children: true, 108 | minChunks: 3 109 | }), 110 | 111 | // copy custom static assets 112 | new CopyWebpackPlugin([ 113 | { 114 | from: path.resolve(__dirname, '../static'), 115 | to: config.build.assetsSubDirectory, 116 | ignore: ['.*'] 117 | } 118 | ]) 119 | ] 120 | }) 121 | 122 | if (config.build.productionGzip) { 123 | const CompressionWebpackPlugin = require('compression-webpack-plugin') 124 | 125 | webpackConfig.plugins.push( 126 | new CompressionWebpackPlugin({ 127 | asset: '[path].gz[query]', 128 | algorithm: 'gzip', 129 | test: new RegExp( 130 | '\\.(' + 131 | config.build.productionGzipExtensions.join('|') + 132 | ')$' 133 | ), 134 | threshold: 10240, 135 | minRatio: 0.8 136 | }) 137 | ) 138 | } 139 | 140 | if (config.build.bundleAnalyzerReport) { 141 | const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin 142 | webpackConfig.plugins.push(new BundleAnalyzerPlugin()) 143 | } 144 | 145 | module.exports = webpackConfig 146 | -------------------------------------------------------------------------------- /client/src/views/Register.vue: -------------------------------------------------------------------------------- 1 | 58 | 59 | 162 | 163 | 164 | 221 | 222 | --------------------------------------------------------------------------------