Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

82 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

node-vue-moba

node.js+vue.js 全栈开发手机端官网和管理后台

一、入门

1.项目介绍

2.工具安装和环境搭建(nodejs,npm,VUE CLI,mongodb,nodemon)

安装git

设置proxy(代理)
设置当前代理
$ git config http.proxy http://127.0.0.1:2334
取消当前代理
$ git config --unset http.proxy
取消全局代理
$ git config --global --unset http.proxy
设置socks5代理
$ git config http.proxy socks5://127.0.0.1:10809

安装nodejs

npm 的淘宝镜像
查看原本镜像
$ npm config registry //https://registry.npmjs.org
设置成淘宝镜像
  • 临时

    $ npm --registry https://registry.npm.taobao.org
  • 永久

    $ npm config set registry https://registry.npm.taobao.org
  • 配置后验证是否成功

    $ npm config get registry

    $ npm info
  • 恢复npm原镜像

    $ npm config set registry https://registry.npmjs.org

安装VUE CLI

$ npm install -g @vue/cli

安装mongodb

安装nodemon

$ npm i -g nodemon

vscode 安装

3.初始化项目

新建server文件夹

表示服务端项目,nodejs 所有的东西,包括给后台管理admin界面和前台web界面提供接口

$ mkdir server

创建web端的项目

$ vue create web

选择 默认 安装

创建后台管理界面项目

$ vue create admin

选择 默认 安装

初始化后台的服务端 server
进入server 文件夹
$ cd server
初始化 node 项目

会生成 package.json

$ npm init -y
新建入口文件 index.js
  • 自定义脚本运行文件

    在package.json 里面 的 scripts中新建

    "scripts": {
      "serve": "nodemon index.js",
      "test": "echo \"Error: no test specified\" && exit 1"
    },

二、管理后台

1.基于Element UI后台管理基础界面搭建

进入到后台admin文件夹

$ cd admin

安装 Element UI 的插件

$ vue add element

安装 提示全按 回车 即可

安装路由

$ vue add router

这里没有使用 history 的路由方式

创建首页 以及 配置路由

2.创建分类(多层级)

客户端

提交数据需安装 axios
$ npm i axios
使用 axios
创建文件引用
在main.js 中加入到原型中,在整个项目中用this来进行使用

服务端

安装express下一版本,mongoose,cors,
$ npm i express@next mongoose cors

这里express安装下一版本5.0

因为后面用 http-assert 必须要求 5.0,否则不支持async的异常处理

3.分类列表

4.修改分类

5.删除分类

6.子分类

逻辑上是父子级 的关系,实际在数据库中还是 扁平 的数据,都是平级的

只不过用一个 字段 表示 其对应关系,从而形成一个链式结构,就可实现无线层级的分类

服务端

在 models(创建的模型)下新增parent 字段
普通写法-----> 错误
parent: { type: String },
正确写法

/**

* parent: { type: String },

*

* 这里一定不是 String 类型,一定是特殊类型

*

* ref 表示关联的模型

*/

parent: { type: mongoose.SchemaTypes.ObjectId, ref: 'Category' },
  • 在 router (路由)下更改新增列表接口

    const items = await Category.find().limit(10)

      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)

// app.use('/admin/api/', router) //匹配 /admin/api 开头的路由
app.use('/admin/api/rest/:resource', router) //通用接口

记得在客户端调取接口的时候 + /rest

转类名的包

专门用来处理 单复数的转换、下划线、单词的格式转换

$ npm i inflection
使用
require('inflection').classify(req.params.resource)

app.use 中添加中间键

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)

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

$http.defaults.baseUrl

处理上传文件的包

$ npm i multer
使用
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
/**
 * 静态文件托管 express.static
 */
app.use('/uploads', express.static(__dirname + '/uploads'))
router\admin\index.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
  • 普通写法

    this.model.icon = res.url
  • 正确写法

    /**

    ​ * 当给对象加属性时,console.log 可以打印出来,但是没有更新到视图上

    ​ *

    ​ * this.$set(target, key, value) 方法 -----> 响应式对象

    ​ * 要更改的数据源(可以是对象或者数组),要更改的具体数据,重新赋的值

    ​ */

    this.$set(this.model, 'icon', res.url)

10.英雄管理

11.编辑英雄

浅拷贝和深拷贝

  • 简单点来说,就是假设B复制了A,当修改A时,看B是否会发生变化,如果B也跟着变了,说明这是浅拷贝,拿人手短,如果B没变,那就是深拷贝,自食其力。

    /**

    ​ * 方法用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target)。

    ​ * Object.assign(target, source1,source2)

    ​ * 第一个参数是目标对象,后面的参数都是源对象

    ​ */

    this.model = Object.assign({}, this.model, res.data)

12.装备的多选

13.技能的编辑

14.文章管理

15.富文本编辑器(quill)

关于vue的富文本编辑器

  • 推荐vue2Editor / vue-quill-editor

  • 使用(vue2Editor)

    • <script>
      import { VueEditor } from "vue2-editor";
      
      export default {
        components: { VueEditor }
      };
      </script>
    • <!-- 
        -- useCustomImageHandler:开启自定义图像上传处理 
        -- image-added: 上传处理事件
      -->
      <vue-editor
        v-model="model.body"
        useCustomImageHandler
        @image-added="handleImageAdded"
      ></vue-editor>
    • /**
       * 需要传递四个参数:
       * 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

$ npm i bcrypt
  • 使密码散列话

使用

password: {
  type: String,
  select: false, // 不能被查询
  set(val) {
    return require('bcrypt').hashSync(val, 10)
  }
},

hashSync 同步方法 ----> 设置的值 和 密码散列话程度(10-12 为最好,太高或太低都不好)

18.登录接口

安装 jsonwebtoken

$ npm i jsonwebtoken

服务端

登陆接口
  • 在 /router/admin 中完成**“登陆接口” **

    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 中 定义全局变量

    //可放在全局变量环境里
    app.set('secret', 'i26rtfx4e456b')

客户端

axios 接受拦截器 ----> 捕获错误
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
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信息
http.interceptors.request.use(config => {
  // 请求头的授权信息 Authorization
  config.headers.Authorization = 'Bearer ' + localStorage.token
  return config
}, err => {
  return Promise.reject(err)
})

服务端

资源详情接口 加 中间键 --------> 校验用户是否登陆
//加一个中间键
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

$ npm i http-assert

使用 http-assert

assert(username === 'fjodor', 401, 'authentication failed')

参数1:需要满足的条件,

参数2: 如果不满足抛出http代码错误,

参数3:文字提示信息

if (!user) {

return res.status(422).send({

message: '用户不存在'

})

}

assert(user, 422, '用户不存在')

21.客户端路由限制(beforeEach,meta)


三、移动端网站

1.“工具样式”概念和SASS(SCSS)

工具样式

设置全局class 进行直接引用

安装sass

$ npm i npm i -D sass sass-loader

2.整体样式

样式重置

根据不同的设备配置不同的样式

sass 语法

定义变量
  • Css 定义变量

     --color-text: #666
  • Less 定义变量

    @width: 10px;
  • Sass 定义变量

    // $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 使用变量

    color: var(--color-text)
  • Less 使用变量

    width: @width;
  • Sass 使用变量

    font-size: $base-font-size
sass 的循环

@each $var in list ----> 循环

Class 名称变量 为 #{$var}

Css 样式变量 为 $var

@each $var in (left,center,right)
  .text-#{$var}
    text-align:$var

可参考标准样式网站 ------> bootstrap

官网 https://v3.bootcss.com/

3.首页顶部轮播图(swiper)

安装vue-awesome-swiper组件

//直接安装版本3即可,自动会选择3.1.3版本
$ npm i vue-awesome-swiper@3 -S

使用vue-awesome-swiper

全局引用-------> main.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 } */)
局部引用
import { Swiper, SwiperSlide, directive } from 'vue-awesome-swiper'
import 'swiper/css/swiper.css'
 
export default {
  components: {
    Swiper,
    SwiperSlide
  },
  directives: {
    swiper: directive
  }
}
页面使用
<template>
  <swiper ref="mySwiper" :options="swiperOptions">
    <swiper-slide>Slide 1</swiper-slide>
    <swiper-slide>Slide 2</swiper-slide>
    <swiper-slide>Slide 3</swiper-slide>
    <swiper-slide>Slide 4</swiper-slide>
    <swiper-slide>Slide 5</swiper-slide>
    <div class="swiper-pagination" slot="pagination"></div>
  </swiper>
</template>
 
<script>
  export default {
    name: 'carrousel',
    data() {
      return {
        swiperOptions: {
          pagination: {
            el: '.swiper-pagination'
          },
          // Some Swiper option/callback...
        }
      }
    },
    computed: {
      swiper() {
        return this.$refs.mySwiper.$swiper
      }
    },
    mounted() {
      console.log('Current Swiper instance object', this.swiper)
      this.swiper.slideTo(3, 1000, false)
    }
  }
</script> 

更多使用方式见

官网 https://www.npmjs.com/package/vue-awesome-swiper

4.使用精灵图片(spirte)

精灵图片辅助网站 spritecow

官网 http://www.spritecow.com/

5.使用字体图标(iconfont)

6.考虑通用组件(SFC)

7.卡片组件

8.菜单组件

9.首页新闻资讯

虚拟字段(virtual)设置以及其弊端

不会保存到数据库,但是可以根据这个字段查找数据库中的数据

使用
  • 模型中定义虚拟字段

    schema.virtual('children', { //定义虚拟字段
      localField: '_id', //内键 ---> 主表关联从表parentId
      foreignField: 'parent', //外键 主键 ----> 主表id
      justOne: false, //只查询一条数据
      ref: 'Category' //关联模型
    })
  • 路由中关联虚拟字段

    populate 关联

    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 语句),它通过包含一个聚合函数(如 Sum 或 Avg )来汇总来自多个行的信息。

聚合管道

聚合查询里面的查询叫 聚合管道,以流水线的方式查询

作用:

  1. 对文档“过滤” -----> 进行筛选
  2. 对文档“变换” ------> 改变输出形式

详情 :https://www.cnblogs.com/shanyou/p/3494854.html

使用
  1. 达到 虚拟字段设置同样效果

    // { $match: { parent: parent._id } } 写法也可以写成where 条件查询形式
    const cats = await Category.find().where({
      parent: parent
    }).lean()
  2. 实现 当前功能需求

    /**
      * 聚合查询 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 查询元素 $$ -------> 返回的是一个数组

获取导航栏数据
$$('.hero-nav > li').map((li,i) => {
	return { 
		categoryName:li.innerText,heroes
	}
})
获取英雄列表

数据需要转成 json JSON.stringify() ----> 进行转换

$$('.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

12.英雄详情页


四、发布和部署(阿里云)

1.生产环境编译

全局安装serve

$ npm i -g serve

客户端运行打包文件(打包名字叫dist)

$ serve dist

客户端建立开发环境

baseURL更换成环境变量
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文件中新增
app.use('/admin', express.static(__dirname + '/admin'))

处理报错

因为打包后的html引入的都是根(“/”)

在admin文件中修改
app.use('/', express.static(__dirname + '/admin'))

静态文件加上文件夹路径

在客户端 vu e.config.js新增
module.exports = {
  .......
  // outputDir 指生成的的目录名
  outputDir: __dirname + '/../server/admin',
  // publicPath 指生成的静态文件路径
  publicPath: process.env.NODE_ENV === 'production'
    ? '/admin/'
    : '/'
};
在admin文件中修改
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云存储存放上传文件

About

node.js+vue.js 全栈开发手机端官网和管理后台

Topics

Resources

Stars

3 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages