# node-vue-moba
**Repository Path**: my-study-vue/node-vue-moba
## Basic Information
- **Project Name**: node-vue-moba
- **Description**: node.js+vue.js 全栈开发手机端官网和管理后台
- **Primary Language**: Unknown
- **License**: MIT
- **Default Branch**: main
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 2
- **Forks**: 0
- **Created**: 2021-07-21
- **Last Updated**: 2023-05-15
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# node-vue-moba
> node.js+vue.js 全栈开发手机端官网和管理后台
## 一、入门
### 1.项目介绍
### 2.工具安装和环境搭建(nodejs,npm,VUE CLI,mongodb,nodemon)
#### 安装git
- 官网 https://git-scm.com/downloads 下载最新版
##### 设置proxy(代理)
###### 设置当前代理
```sh
$ git config http.proxy http://127.0.0.1:2334
```
###### 取消当前代理
```sh
$ git config --unset http.proxy
```
###### 取消全局代理
```sh
$ git config --global --unset http.proxy
```
###### 设置socks5代理
```sh
$ git config http.proxy socks5://127.0.0.1:10809
```
#### 安装nodejs
- 官网 http://nodejs.cn/ 下载最新版
##### npm 的淘宝镜像
###### 查看原本镜像
```sh
$ npm config registry //https://registry.npmjs.org
```
###### 设置成淘宝镜像
- 临时
```sh
$ npm --registry https://registry.npm.taobao.org
```
- 永久
```sh
$ npm config set registry https://registry.npm.taobao.org
```
- 配置后验证是否成功
```sh
$ npm config get registry
```
或
```sh
$ npm info
```
- 恢复npm原镜像
```sh
$ npm config set registry https://registry.npmjs.org
```
#### 安装VUE CLI
```sh
$ npm install -g @vue/cli
```
#### 安装mongodb
- 下载地址https://www.mongodb.com/try/download/community 下载最新版
#### 安装nodemon
```sh
$ npm i -g nodemon
```
#### vscode 安装
### 3.初始化项目
#### 新建server文件夹
>表示服务端项目,nodejs 所有的东西,包括给后台管理admin界面和前台web界面提供接口
```sh
$ mkdir server
```
#### 创建web端的项目
```sh
$ vue create web
```
>选择 默认 安装
#### 创建后台管理界面项目
```sh
$ vue create admin
```
> 选择 默认 安装
##### 初始化后台的服务端 server
###### 进入server 文件夹
```sh
$ cd server
```
###### 初始化 node 项目
> 会生成 package.json
```sh
$ npm init -y
```
###### 新建入口文件 index.js
- 自定义脚本运行文件
> 在package.json 里面 的 scripts中新建
```json
"scripts": {
"serve": "nodemon index.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
```
----------
## 二、管理后台
### 1.基于Element UI后台管理基础界面搭建
#### 进入到后台admin文件夹
```sh
$ cd admin
```
#### 安装 Element UI 的插件
```sh
$ vue add element
```
>安装 提示全按 回车 即可
#### 安装路由
```sh
$ vue add router
```
>这里没有使用 history 的路由方式
#### 创建首页 以及 配置路由
### 2.创建分类(多层级)
#### 客户端
##### 提交数据需安装 axios
```sh
$ npm i axios
```
##### 使用 axios
###### 创建文件引用
###### 在main.js 中加入到原型中,在整个项目中用this来进行使用
#### 服务端
##### 安装express下一版本,mongoose,cors,
```sh
$ npm i express@next mongoose cors
```
>这里express安装下一版本5.0
>
>因为后面用 http-assert 必须要求 5.0,否则不支持async的异常处理
### 3.分类列表
### 4.修改分类
### 5.删除分类
### 6.子分类
>逻辑上是父子级 的关系,实际在数据库中还是 扁平 的数据,都是平级的
>
>只不过用一个 字段 表示 其对应关系,从而形成一个链式结构,就可实现无线层级的分类
#### 服务端
##### 在 models(创建的模型)下新增`parent` 字段
###### ~~普通写法-----> 错误~~
```js
parent: { type: String },
```
###### 正确写法
>/**
>
>\* parent: { type: String },
>
>\*
>
>\* 这里一定不是 String 类型,一定是特殊类型
>
>\*
>
>\* ref 表示关联的模型
>
>*/
```js
parent: { type: mongoose.SchemaTypes.ObjectId, ref: 'Category' },
```
- 在 router (路由)下更改`新增列表`接口
~~const items = await Category.find().limit(10)~~
```js
router.get('/categories', async (req, res) => {
// 显示新增列表
// const items = await Category.find().limit(10)
const items = await Category.find().populate('parent').limit(10)
/**
* populate 表示关联字段 取出/查出
* 把 那个关联字段的 相关数据 展示出来
*/
res.send(items)
})
```
### 7.通用CRUD接口(服务端)
>CRUD 是crate(增)、read(查)、update(改)、delete(删)的缩写
>
>简而言之 就是增删改查的一个公用写法接口抽离出来
#### 更改成动态路由
~~app.use('/admin/api/', router)~~
```js
// app.use('/admin/api/', router) //匹配 /admin/api 开头的路由
app.use('/admin/api/rest/:resource', router) //通用接口
```
>记得在客户端调取接口的时候 + /rest
#### 转类名的包
>专门用来处理 单复数的转换、下划线、单词的格式转换
```sh
$ npm i inflection
```
##### 使用
```js
require('inflection').classify(req.params.resource)
```
#### app.use 中添加中间键
```js
app.use('/admin/api/rest/:resource', async (req, res, next) => {
const modelName = require('inflection').classify(req.params.resource)
req.Mondel = require(`../../models/${modelName}`)
next()
}, router) //通用接口
```
#### 修改 关联字段
~~const items = await req.Mondel.find().populate('parent').limit(10)~~
```js
const queryOptions = {}
if (req.Mondel.modelName === 'Category') {
queryOptions.populate = 'parent'
}
const items = await req.Mondel.find().setOptions(queryOptions).limit(10)
```
### 8.装备管理
### 9.图片上传(multer)和文件管理
#### 封装的axios取baseURL
```js
$http.defaults.baseUrl
```
#### 处理上传文件的包
```sh
$ npm i multer
```
##### 使用
```js
const multer = require('multer')
// 上传中间键
const upload = multer({
/**
* dest 目标地址在哪里
*
* __dirname 绝对地址 (必须加)
*
* upload.single() 表示单个文件的上传
*
* file 表示 传入的参数字段(Form Data 里的)
*/
dest: __dirname + '/../../uploads'
})
// 有了上传中间键req 上才会有file
app.post('/upload', upload.single('file'), async (req, res) => {
const file = req.file
res.send(file)
}) // 上传文件接口,不使用路由
```
#### 静态文件管理
##### 服务端
###### index.js
```js
/**
* 静态文件托管 express.static
*/
app.use('/uploads', express.static(__dirname + '/uploads'))
```
###### router\admin\index.js
```js
// 有了上传中间键req 上才会有file
app.post('/admin/api/upload', upload.single('file'), async (req, res) => {
const file = req.file
/**
* 需要定义静态路由来访问静态文件进行文件托管
*/
file.url = `http://localhost:3000/uploads/${file.filename}`
res.send(file)
}) // 上传文件接口,不使用路由
```
##### 客户端
###### src/views/item/itemEdit.Vue
- ~~普通写法~~
```vue
this.model.icon = res.url
```
- 正确写法
> /**
>
> \* 当给对象加属性时,console.log 可以打印出来,但是没有更新到视图上
>
> \*
>
> \* this.$set(target, key, value) 方法 -----> 响应式对象
>
> \* 要更改的数据源(可以是对象或者数组),要更改的具体数据,重新赋的值
>
> */
```js
this.$set(this.model, 'icon', res.url)
```
### 10.英雄管理
### 11.编辑英雄
#### 浅拷贝和深拷贝
- ##### 简单点来说,就是假设B复制了A,当修改A时,看B是否会发生变化,如果B也跟着变了,说明这是浅拷贝,拿人手短,如果B没变,那就是深拷贝,自食其力。
> /**
>
> \* 方法用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target)。
>
> \* Object.assign(target, source1,source2)
>
> \* 第一个参数是目标对象,后面的参数都是源对象
>
> */
```js
this.model = Object.assign({}, this.model, res.data)
```
### 12.装备的多选
### 13.技能的编辑
### 14.文章管理
### 15.富文本编辑器(quill)
#### 关于vue的富文本编辑器
- 推荐vue2Editor / vue-quill-editor
- 使用(vue2Editor)
- ```vue
```
- ```vue
```
- ```
/**
* 需要传递四个参数:
* 1.处理的文件
* 2.编辑器实例
* 3.上传时的光标(可以成功插入到正确位置)
* 4.重置上传的方法
*/
async handleImageAdded (file, Editor, cursorLocation, resetUploader) {
const formData = new FormData();
formData.append("file", file);
const res = await this.$http.post('upload', formData)
Editor.insertEmbed(cursorLocation, "image", res.data.url);
resetUploader();
},
```
### 16.首页广告管理
### 17.管理员账号管理(bcrypt)
#### 安装bcrypt
```sh
$ npm i bcrypt
```
- 使密码散列话
#### 使用
```js
password: {
type: String,
select: false, // 不能被查询
set(val) {
return require('bcrypt').hashSync(val, 10)
}
},
```
>hashSync 同步方法 ----> 设置的值 和 密码散列话程度(10-12 为最好,太高或太低都不好)
### 18.登录接口
#### 安装 jsonwebtoken
```sh
$ npm i jsonwebtoken
```
#### 服务端
##### 登陆接口
- 在 /router/admin 中完成**“登陆接口” **
```js
app.post('/admin/api/login', async (req, res) => {
const { userName, password } = req.body
/**
* 1.根据用户名寻找用户
*
* 引入用户登陆模型
* 寻找一条与输入用户名匹配的数据
* 判断用户是否存在
*/
const AdminUser = require('../../models/AdminUser')
// const user = await AdminUser.findOne({ userName })
// select('+password')表示 select: false的可以被取出来
const user = await AdminUser.findOne({ userName }).select('+password')
if (!user) {
return res.status(422).send({
message: '用户不存在'
})
}
// 2.校验密码
const isValid = require('bcrypt').compareSync(password, user.password)
if (!isValid) {
return res.status(422).send({
message: '密码错误'
})
}
// 3.返回token
const jwt = require('jsonwebtoken')
/**
* sign(payload: string | object | Buffer, secretOrPrivateKey: jwt.Secret, options?: jwt.SignOptions): string
*
* payload 承载的数据,secretOrPrivateKey 密钥 ---> 全局的
*/
const token = jwt.sign({
id: user._id,
// userName: user.userName, //一般大多数不需要用户名
}, app.get('secret'))
res.send({ token })
})
```
- 在 index.js 中 定义全局变量
```js
//可放在全局变量环境里
app.set('secret', 'i26rtfx4e456b')
```
#### 客户端
##### axios 接受拦截器 ----> 捕获错误
```js
import Vue from 'vue'
http.interceptors.response.use(res => {
return res.data
}, err => {
if (err.response.data.message) {
Vue.prototype.$message({
type: 'error',
message: err.response.data.message,
});
}
return Promise.reject(err)
})
```
##### login 界面保存token
```js
async login() {
const res = await this.$http.post('login', this.model)
// 表示当前浏览器关闭后依然保存着
localStorage.token = res.data.token
// 表示当前浏览器关闭之后就没了
// sessionStorage.token = res.data.token
this.$router.push('/')
this.$message({
type: 'success',
message: '登陆成功'
})
}
```
### 19.登录接口(jwt,jsonwebtoken)
> 必须有token才能进行访问
#### 客户端
##### axios 请求拦截器 ----> 请求头里保存 token信息
```js
http.interceptors.request.use(config => {
// 请求头的授权信息 Authorization
config.headers.Authorization = 'Bearer ' + localStorage.token
return config
}, err => {
return Promise.reject(err)
})
```
#### 服务端
##### 资源详情接口 加 中间键 --------> 校验用户是否登陆
```js
//加一个中间键
router.get('/', async (req, res, next) => {
/**
* 校验用户是否登陆
*
* 前端 Authorization 大写
* 后端 authorization 小写
*/
const token = String(req.headers.authorization || '').split(' ').pop()
/**
* verify(token: string, secretOrPublicKey: jwt.Secret, options: jwt.VerifyOptions & { complete: true; }): string | jwt.Jwt
*
* 校验方法
*
* decode 只是解密没有校验性
*/
const { id } = jwt.verify(token, app.get('secret'))
req.user = await AdminUser.findById(id)
await next()
}, async (req, res) => {
...
})
```
### 20.服务端登录校验(http-assert)
>判断用户不存在
#### 安装 http-assert
```sh
$ npm i http-assert
```
#### 使用 http-assert
>assert(username === 'fjodor', 401, 'authentication failed')
>
>参数1:需要满足的条件,
>
>参数2: 如果不满足抛出http代码错误,
>
>参数3:文字提示信息
~~if (!user) {~~
~~return res.status(422).send({~~
~~message: '用户不存在'~~
~~})~~
~~}~~
```js
assert(user, 422, '用户不存在')
```
### 21.客户端路由限制(beforeEach,meta)
-------
## 三、移动端网站
### 1.“工具样式”概念和SASS(SCSS)
#### 工具样式
>设置全局class 进行直接引用
#### 安装sass
```sh
$ npm i npm i -D sass sass-loader
```
### 2.整体样式
#### 样式重置
>根据不同的设备配置不同的样式
#### sass 语法
##### 定义变量
- Css 定义变量
```css
--color-text: #666
```
- Less 定义变量
```less
@width: 10px;
```
- Sass 定义变量
```scss
// $colors:(a,b,c) // list
// $colors:(a:1,b:2,c:3) //map
$colors:("primary": #db9e3f,"white": #fff,"light": #f9f9f9,"grey": #999,"dark-1": #343440,"dark": #222,"black": #000,)
$base-font-size:1rem
```
##### 使用变量
- Css 使用变量
```css
color: var(--color-text)
```
- Less 使用变量
```less
width: @width;
```
- Sass 使用变量
```scss
font-size: $base-font-size
```
##### sass 的循环
>@each $var in list ----> 循环
>
>
>
>Class 名称变量 为 **#{$var}**
>
>Css 样式变量 为 **$var**
```scss
@each $var in (left,center,right)
.text-#{$var}
text-align:$var
```
#### 可参考标准样式网站 ------> **bootstrap**
> 官网 https://v3.bootcss.com/
### 3.首页顶部轮播图(swiper)
#### 安装**vue-awesome-swiper**组件
```sh
//直接安装版本3即可,自动会选择3.1.3版本
$ npm i vue-awesome-swiper@3 -S
```
#### 使用vue-awesome-swiper
##### 全局引用-------> main.js
```js
import Vue from 'vue'
import VueAwesomeSwiper from 'vue-awesome-swiper'
// import style
import 'swiper/css/swiper.css'
Vue.use(VueAwesomeSwiper, /* { default options with global component } */)
```
##### 局部引用
```js
import { Swiper, SwiperSlide, directive } from 'vue-awesome-swiper'
import 'swiper/css/swiper.css'
export default {
components: {
Swiper,
SwiperSlide
},
directives: {
swiper: directive
}
}
```
##### 页面使用
```vue
Slide 1
Slide 2
Slide 3
Slide 4
Slide 5
```
> 更多使用方式见
>
> 官网 https://www.npmjs.com/package/vue-awesome-swiper
### 4.使用精灵图片(spirte)
#### 精灵图片辅助网站 spritecow
> 官网 http://www.spritecow.com/
### 5.使用字体图标(iconfont)
### 6.考虑通用组件(SFC)
### 7.卡片组件
### 8.菜单组件
### 9.首页新闻资讯
#### 虚拟字段(virtual)设置以及其弊端
>不会保存到数据库,但是可以根据这个字段查找数据库中的数据
##### 使用
- 模型中定义虚拟字段
```js
schema.virtual('children', { //定义虚拟字段
localField: '_id', //内键 ---> 主表关联从表parentId
foreignField: 'parent', //外键 主键 ----> 主表id
justOne: false, //只查询一条数据
ref: 'Category' //关联模型
})
```
- 路由中关联虚拟字段
>populate 关联
```js
const parent = await Category.findOne({
name: '新闻分类'
}).populate({
/**
* populate 关联
*
* path 需要关联的子分类
*
* <弊端: populate不能控制每个分类下面的每一个新闻下显示很多条,只能控制总数>
*/
path: 'children',
populate: {
path: 'newsList'
}
}).lean()
//lean 属性 转换为JS Object格式
```
##### 弊端
>populate不能控制chilren下面的每一个newList下显示很多条,只能控制chilren 下面的总数
#### 聚合查询 aggregate
>一种查询([SQL](https://baike.baidu.com/item/SQL) 语句),它通过包含一个[聚合函数](https://baike.baidu.com/item/聚合函数)(如 Sum 或 Avg )来汇总来自多个行的信息。
##### 聚合管道
>聚合查询里面的查询叫 聚合管道,以流水线的方式查询
>
>
>
>作用:
>
>1. 对文档“过滤” -----> 进行筛选
>2. 对文档“变换” ------> 改变输出形式
>
>详情 :https://www.cnblogs.com/shanyou/p/3494854.html
##### 使用
1. 达到 **虚拟字段设置**同样效果
```js
// { $match: { parent: parent._id } } 写法也可以写成where 条件查询形式
const cats = await Category.find().where({
parent: parent
}).lean()
```
2. 实现 当前功能需求
```js
/**
* 聚合查询 aggregate
*
* 聚合查询里面的查询叫 聚合管道
*/
const parent = await Category.findOne({
name: '新闻分类'
})
const cats = await Category.aggregate([
{ $match: { parent: parent._id } },// 查询到相关数据 需要关联主表id
{
$lookup: { //外链接
from: 'articles',// 关联的表的集合
localField: '_id',
foreignField: 'categories',
as: 'newsList',// 取名
}
},// 达到populate 嵌套的效果
{
/**
* addFields 本意添加字段,
* 也可用来修改字段
*/
$addFields: {
newsList: {
$slice: ['$newsList', 5], // 需要筛选的字段 和 筛选的个数
},
}
}
])
const subCats = cats.map(v => v._id)
cats.unshift({
name: '热门',
newsList: await Article.find().where({
categories: { $in: subCats } //$in表示 筛选出字段值等于制定数组中的所有值
}).populate('categories').limit(5).lean()
})//unshift 往当前增加数据
cats.forEach(cat => {
cat.newsList.forEach(news => {
news.categoryName = cat.name === '热门' ? news.categories[0].name : cat.name
})
})
res.send(cats)
```
### 10.首页英雄列表
#### 利用浏览器console 爬取数据
>根据dom 查询元素 $$ -------> 返回的是一个数组
##### 获取导航栏数据
```js
$$('.hero-nav > li').map((li,i) => {
return {
categoryName:li.innerText,heroes
}
})
```
##### 获取英雄列表
>数据需要转成 json JSON.stringify() ----> 进行转换
```js
$$('.hero-nav > li').map((li,i) => {
return {
categoryName:li.innerText,
heroes:$$('li',$$('.hero-list')[i]).map(el => ({
name:$$('h3',el)[0].innerHTML,
avatar:$$('img',el)[0].src
}))
}
})
```
### 11.新闻详情页
#### 时间处理的包 ---- dayjs
- 中文官网 https://dayjs.fenxianglu.cn/
### 12.英雄详情页
--------
## 四、发布和部署(阿里云)
### 1.生产环境编译
#### 全局安装serve
```sh
$ npm i -g serve
```
#### 客户端运行打包文件(打包名字叫dist)
```sh
$ serve dist
```
#### 客户端建立开发环境
##### baseURL更换成环境变量
```js
const http = axios.create({
baseURL: process.env.VUE_APP_API_URL || '/admin/api',
// baseURL: 'http://localhost:3000/admin/api',
//$http.defaults.baseUrl defaults 表示默认参数
timeout: 5000
})
```
##### 在客户端 根目录下新建 .env.development 文件
```
VUE_APP_API_URL=http://localhost:3000/admin/api
```
#### 把客户端dist 文件复制给后端根目录下,改名为admin
##### 后端上传打包(静态文件托管)
##### 在admin文件中新增
```js
app.use('/admin', express.static(__dirname + '/admin'))
```
#### 处理报错
>因为打包后的html引入的都是根(“/”)
##### 在admin文件中修改
```js
app.use('/', express.static(__dirname + '/admin'))
```
#### 静态文件加上文件夹路径
##### 在客户端 vu e.config.js新增
```js
module.exports = {
.......
// outputDir 指生成的的目录名
outputDir: __dirname + '/../server/admin',
// publicPath 指生成的静态文件路径
publicPath: process.env.NODE_ENV === 'production'
? '/admin/'
: '/'
};
```
##### 在admin文件中修改
```js
app.use('/admin', express.static(__dirname + '/admin'))
```
### 2.购买域名和服务器
#### 域名
>挑选国内的域名需要进行备案,国外包括香港不用备案但是速度会慢一些,以及会有一些限制。
### 3.域名解析
### 4.Nginx 安装和配置
### 5.MongoDB数据库的安装和配置
### 6.git 安装、配置ssh-key
### 7.Node.js安装、配置淘宝路径
### 8.拉去代码,安装pm2并启动项目
### 9.配置Nginx 的反向代理
-------
## 五、进阶
### 1.使用免费SSL证书启用HTTPS安全连接
### 2.使用阿里云OSS云存储存放上传文件