node.js+vue.js 全栈开发手机端官网和管理后台
- 官网 https://git-scm.com/downloads 下载最新版
$ git config http.proxy http://127.0.0.1:2334$ git config --unset http.proxy$ git config --global --unset http.proxy$ git config http.proxy socks5://127.0.0.1:10809- 官网 http://nodejs.cn/ 下载最新版
$ 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
$ npm install -g @vue/cli$ npm i -g nodemon表示服务端项目,nodejs 所有的东西,包括给后台管理admin界面和前台web界面提供接口
$ mkdir server$ vue create web选择 默认 安装
$ vue create admin选择 默认 安装
$ cd server会生成 package.json
$ npm init -y-
自定义脚本运行文件
在package.json 里面 的 scripts中新建
"scripts": { "serve": "nodemon index.js", "test": "echo \"Error: no test specified\" && exit 1" },
$ cd admin$ vue add element安装 提示全按 回车 即可
$ vue add router这里没有使用 history 的路由方式
$ npm i axios$ npm i express@next mongoose cors这里express安装下一版本5.0
因为后面用 http-assert 必须要求 5.0,否则不支持async的异常处理
逻辑上是父子级 的关系,实际在数据库中还是 扁平 的数据,都是平级的
只不过用一个 字段 表示 其对应关系,从而形成一个链式结构,就可实现无线层级的分类
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) })
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 inflectionrequire('inflection').classify(req.params.resource)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)$http.defaults.baseUrl$ npm i multerconst 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)
}) // 上传文件接口,不使用路由/**
* 静态文件托管 express.static
*/
app.use('/uploads', express.static(__dirname + '/uploads'))// 有了上传中间键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)
}) // 上传文件接口,不使用路由-
普通写法this.model.icon = res.url
-
正确写法
/**
* 当给对象加属性时,console.log 可以打印出来,但是没有更新到视图上
*
* this.$set(target, key, value) 方法 -----> 响应式对象
* 要更改的数据源(可以是对象或者数组),要更改的具体数据,重新赋的值
*/
this.$set(this.model, 'icon', res.url)
-
/**
* 方法用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target)。
* Object.assign(target, source1,source2)
* 第一个参数是目标对象,后面的参数都是源对象
*/
this.model = Object.assign({}, this.model, res.data)
-
推荐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(); },
-
$ npm i bcrypt- 使密码散列话
password: {
type: String,
select: false, // 不能被查询
set(val) {
return require('bcrypt').hashSync(val, 10)
}
},hashSync 同步方法 ----> 设置的值 和 密码散列话程度(10-12 为最好,太高或太低都不好)
$ 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')
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)
})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: '登陆成功'
})
}必须有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) => {
...
})判断用户不存在
$ npm i http-assertassert(username === 'fjodor', 401, 'authentication failed')
参数1:需要满足的条件,
参数2: 如果不满足抛出http代码错误,
参数3:文字提示信息
if (!user) {
return res.status(422).send({
message: '用户不存在'
})
}
assert(user, 422, '用户不存在')设置全局class 进行直接引用
$ npm i npm i -D sass sass-loader根据不同的设备配置不同的样式
-
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
@each $var in list ----> 循环
Class 名称变量 为 #{$var}
Css 样式变量 为 $var
@each $var in (left,center,right)
.text-#{$var}
text-align:$var//直接安装版本3即可,自动会选择3.1.3版本
$ npm i vue-awesome-swiper@3 -Simport 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> 更多使用方式见
不会保存到数据库,但是可以根据这个字段查找数据库中的数据
-
模型中定义虚拟字段
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 下面的总数
聚合查询里面的查询叫 聚合管道,以流水线的方式查询
作用:
- 对文档“过滤” -----> 进行筛选
- 对文档“变换” ------> 改变输出形式
-
达到 虚拟字段设置同样效果
// { $match: { parent: parent._id } } 写法也可以写成where 条件查询形式 const cats = await Category.find().where({ parent: parent }).lean()
-
实现 当前功能需求
/** * 聚合查询 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)
根据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
}))
}
})$ npm i -g serve$ serve distconst http = axios.create({
baseURL: process.env.VUE_APP_API_URL || '/admin/api',
// baseURL: 'http://localhost:3000/admin/api',
//$http.defaults.baseUrl defaults 表示默认参数
timeout: 5000
})VUE_APP_API_URL=http://localhost:3000/admin/api
app.use('/admin', express.static(__dirname + '/admin'))因为打包后的html引入的都是根(“/”)
app.use('/', express.static(__dirname + '/admin'))module.exports = {
.......
// outputDir 指生成的的目录名
outputDir: __dirname + '/../server/admin',
// publicPath 指生成的静态文件路径
publicPath: process.env.NODE_ENV === 'production'
? '/admin/'
: '/'
};app.use('/admin', express.static(__dirname + '/admin'))挑选国内的域名需要进行备案,国外包括香港不用备案但是速度会慢一些,以及会有一些限制。