基于Vue结合Vant组件库的仿电影APP
Vue综合案例
- Vue综合案例
- 一、项目概要
- 1、效果前瞻
- 2、开发流程
- 3、开发环境
- 二、初始化及必要知识点
- 1、初始化远程仓库
- 2、创建项目
- 3、路由规划
- 4、反向代理配置
- 5、网络请求封装
- 6、vant组件配置
- 三、功能实现(1)
- 1、导航实现
- 1.1、底部导航
- 1.2、顶部导航
- 2、电影模块
- 2.1、正在热映列表
- 2.2、即将上映列表
- 2.3、电影详情
- 1、导航实现
- 四、Vuex(重点)
- 1、vuex是什么?
- 2、vuex的安装及配置
- 3、vuex核心(重点)
- 4、模块化(重点)
- 5、改写eventBus案例
- 五、功能实现(2)
- 1、NodeJS接口实现
- 1.1、接口安全
- 1.2、用户登录接口
- 1.3、获取用户信息接口
- 2、登录操作
- 3、获取用户信息
- 4、防止翻墙
- 1、NodeJS接口实现
- 六、项目上线流程(记忆)
- 1、项目上线的要素
- 2、Vue项目上线发布
- 2.1、购买云服务器
- 2.2、云服务器操作基础
- 2.3、项目运行环境部署
一、项目概要
1、效果前瞻
仿照网站:卖座网
卖座电影
说明:前端:moive: 基于Vue结合Vant组件库的仿电影APP
后台:moive-server: 基于Node.js电影项目moive的后台
2、开发流程
熟悉一个项目从0-1流程?
- 产品立项 (需求分析、技术选型、项目人员确定),产出立项报告
- 项目立项报告(百度文库搜)【产品经理,PM】
- 当前背景
- 项目需求
- 人员安排
- 功能介绍
- 市场需求
- 项目风险
- 项目立项报告(百度文库搜)【产品经理,PM】
- 产品原型 (设计产品原型图)【产品经理】
- 产品原型图(通过简单的黑白线条勾勒出项目的初始界面效果)
- 进行ui设计(效果图)
- 依据用户的视觉体验给界面加上了颜色
- 项目开发 (前端 与 后端)【周期最长的一步】 周期最长的
- 设计(UI):设计图和切图【UI人员】
- 前端:出一版静态页(模板)
- 以前:html+css+js+其他库文件
- 现在:
- v
- r
- a
- 后端:服务器端
- 写接口
- 搭服务器
- 写业务逻辑
- 前后端整合
- 耦合式开发(也还算行,模版引擎)
- 前后端分离式(幸福,只需要看前端代码即可)
- 产出v1.0的代码
- 项目测试
- 测试部门:QA人员(质量保障)
- 测试
- 内测
- 公测(大公司的产品)
- 项目上线
- 运维&后端&前端
面试会问的问题:之前的团队的人员配置。
答:这个回答需要取决公司的规模,比方说,以美团为例,研发岗170个人。前端:40个,后端:50个,运维:30个,设计:30个,产品经理:10个,测试:10。
如果是小规模,则例如20个人总共,前端可以为5个,后端,5个人,运维2,测试2个,产品2-3个人,UI:3-4人。
3、开发环境
开发工具:vs code并安装vue开发插件:Vetur
开发环境:windows / mac (mac重度患者优先考虑)
项目运行环境:node v10+
Vue脚手架: vue-cli 4+
代码版本工具:Git/Svn(乌龟SVN),Gitee/GitLab/Github,乌龟Git(基于可视化界面的git工具)
二、初始化及必要知识点
1、初始化远程仓库
以Github为例:https://github.com/
- 创建一个新项目
- 仓库配置
仓库名称自行决定。
以gitee为例:Gitee - 企业级 DevOps 研发效能平台
- 在主页的右上角选择“+”号,展开后,选择“新建仓库”
- 填写仓库基本的信息,只需要填写仓库名称即可
- 创建好的仓库界面
2、创建项目
- 使用
vue-cli
脚手架创建项目
vue create i-moive
项目名称
i-moive
可以根据自己的情况进行替换
- 脚手架创建项目询问式选择回答如下
使用历史路由模式,在上线部署的时候需要做服务端的重写配置,否则项目不支持刷新,一刷新就404。
其实vue也支持界面化的方式管理项目(与vue create
命令二选一):
vue ui
- 项目创建完毕
- 同步初始化项目到远程仓库
# i-moive,根据自己的项目名称进行替换
cd i-moive
git remote add origin 远程仓库地址
# 将本地当前的分支代码上传到远程的master分支中
git push -u origin master
- 创建开发分支
dev
(用于做测试的分支,等测试代码没问题之后再往主分支上合并)
以后实际工作是master分支为最终稳定运行的版本的代码,而在开发期间提交的代码一般会提交给开发分支,待后期测试没问题,再与master分支进行合并(pull request)。
git branch dev
git checkout dev
git push -u origin dev
后续操作开发就在dev分支上开发,等全部代码编写完毕,再与master分支合并。
- (==可选操作==)为当前项目设置记住密码/SSH免密登录
如果每次提交都提示输入帐号密码,则可以做此步骤。
修改当前项目中的
.git/config
文件
将配置:
[remote "origin"]url = https://github.com/......
修改为:
[remote "origin"]url = https://用户名:密码@github.com/......
后续每天工作使用Git指令是什么??
# 将远程仓库的代码拉取到本地
git pull# 写代码的环节# 写好代码
git add .
git commit -m "注释"# 下班
git push
# 打卡下班
3、路由规划
在后续开始之前先对项目进行一个清理,删除不需要的东西:
- 删除
src/assets/logo.png
文件 - 删除
src/components/HelloWorld.vue
文件 - 删除
src/views/Home.vue
和src/views/About.vue
文件 - 修改
src/router/index.js
,删除对Home.vue
和About.vue
的引用- 删除
Home.vue
de 的引入 - 删除根路由规则
- 删除
/about
路由规则
- 删除
- 修改
src/App.vue
文件- 删除
id="nav"
的div元素 - 清除
style
标签之间的所有的样式
- 删除
最终清理完毕的标志:页面是白的,控制台没有任何报错。
————————————————————————————————————————————
回顾:路由知识点
a. 路由的概念:最早接触该概念在nodejs处,正常的说,请求与响应构成了项目开发的一个闭环。在请求时,服务器知道响应我们什么的东西,它是通过请求地址知道的,这个请求地址就是路由中的一部分。专业的来讲,路由就是一种对应关系:请求的地址与响应的资源的一种对应关系。
b. 在vue中,要使用路由需要借助路由管理器:vue-router
c. 在这里(路由)可能会用到的知识点:
- 路由基本定义方式
- 默认情况下路由在路由配置文件中定义:
src/router/index.js
文件中的routes
数组中定义 - 在定义路由规则的时候有2个常用的对象属性
- path:请求的地址(路径)【路由规则中,path属性必须得有】
- component:响应的组件【路由规则中,component属性可以没有】
- 默认情况下路由在路由配置文件中定义:
- 路由的重定向方式
- 定义:重新确定指向,例如:我们要去楼下买luckin,但是其门口贴了张公告,说不好意思,我们搬家了,搬到了电影院边上,那我们如果还需要买,则就得移步到新的地址去买,这个行为就是重定向。
- 对应的俩个路由规则属性是:
- path:原本需要请求的地址
- redirect:实际请求的地址
- 路由嵌套的方式
- 定义:犹如嵌套循环的概念,所谓嵌套路由,实际就是路由里套着路由(套娃式路由)
- 使用场景:在项目中,至少存在俩个相同路由前缀的路由的时候,就可以使用嵌套路由;使用该方式可以省去重复写相同前缀的操作;
- /admin/user/add
- /admin/user/del
- /admin/user/select
- /admin/user/mod
- 对应的规则属性:
- children:指定被嵌套的子路由们
- 注意点:==嵌套路由在使用的时候,一定要在父路由组件中添加路由渲染容器标签
router-view
==
- 动态路由匹配(路由参数)
- 作用:使用restful规范给目标路由地址传递参数
- 补充restful规范:
- 其是一种接口开发规范
- 规范≠标准,可以不遵守,但是一般情况下,我们还是很听话的
- 常用的restful规范:
- 请求类型不再固定是get和post,在restful中对应着四个请求类型:
- get:查询
- post:增加
- put:修改
- delete:删除
- 对于请求地址的规范(路由参数,不要使用以前的“?”形式传值),以用户操作为例:
- get
- 查单个:/user/100
- 查全部:/user
- post
- 新增:/user
- put
- 修改:/user/100
- delete
- 删除:/user/100
- get
- 响应规范
- 状态码
- 文本信息
- 响应体
- ...
- 请求类型不再固定是get和post,在restful中对应着四个请求类型:
- 路由守卫
- 作用:防止用户翻墙
————————————————————————————————————————————
如果项目中所有的路由都写在入口文件中,那么将不便于编写项目和后期维护。因此路由需要进行模块化处理。
可以先行添加以下几个空的路由模块(先不拆,实现效果之后确保没有问题,则再进行拆分):
- 电影模块
- 正在热映(嵌套路由)
- /films/nowPlaying
- 即将上映(嵌套路由)
- /films/comingSoon
- 电影详情
- /film/10000
- 正在热映(嵌套路由)
- 影院模块
- /cinemas
- 个人中心模块
- 我的:/center
- 登录:/login
- 城市列表
- /city
如果后续还有其他模块,届时再进行增加即可。
创建各个模块对应的视图组件文件
在
src/views
目录下创建对应的文件夹与文件,同时,可以删除自带的Home.vue
与About.vue
文件创建每个视图组件后在其中书写好基本内容
<template><div><h1>XXXX</h1></div> </template>
src/views ├─Center (个人中心) │ └─Index.vue │ ├─Cinemas (电影院) │ └─Index.vue │ ├─News (电影院) │ └─Index.vue │ └─Films (电影)│ Index.vue│ NowPlaying.vue└─ComingSoon.vue
创建模块化的目录及路由文件
在每个路由模块文件中注册好对应的路由及各自所使用的视图组件
src/router├─index.js│└─modules│ center.js│ cinemas.js│ city.js| common.js└─films.js
在剔除router/index.js
中无用代码后,示例代码如下
import Vue from "vue";
import VueRouter from "vue-router";// 导入需要的路由模块
import commonRoutes from "./modules/common";
import filmsRoutes from "./modules/films";
import cinemasRoutes from "./modules/cinemas";
import centerRoutes from "./modules/center";
import cityRoutes from "./modules/city";Vue.use(VueRouter);const routes = [// 路由:请求地址与响应资源的对应关系// 实现步骤:// a. 创建地址对应的组件,组件暂时不考虑布局等因素,输出不同内容即可;// b. 导入组件// c. 书写路由规则(不拆分)// d. 拆分成模块化的形式// 通用模块、电影模块、影院模块、城市模块、个人中心模块// 展开路由模块的数组,将所有的对象元素放在routes数组中...commonRoutes,// 电影模块的路由...filmsRoutes,// 电影院模块...cinemasRoutes,// 我的模块...centerRoutes,// 城市列表...cityRoutes,
];const router = new VueRouter({mode: "history",base: process.env.BASE_URL,routes,
});export default router;
4、反向代理配置
回顾:网络请求
a. 什么是跨域?(什么是同源策略?)
- 同协议
- 同域名
- 同端口
b. 为什么存在跨域问题?
浏览器为了安全考虑,所以存在这样的限制
c. 如何解决跨域问题?
- cors:通过后端设置响应头来实现的
- jsonp:通过后端设置响应callback来实现的
- 代理:
- 通过服务器软件进行代理(不占优势)
- 通过后端语言进行代理(原因:后端不存在跨域问题)
在vue中,脚手架为了方便让我们解决跨域问题,其已经集成了nodejs代理解决跨域操作,我们需要做的是,设置代理的相关配置信息。
————————————————————————————————————————————
为什么需要配置:接口有很多都是存在跨域问题的,但是cors、jsonp的方式都是依赖后端去解决支持问题,所以只能考虑代理操作。
配置vue的代理操作需要注意以下几点:
- 后续使用的配置是基于node的,不是vue的
- 代理只在本地的开发环境下生效,打包上线后就没了
- 上线时可能需要更改实际请求的地址(cors)
- 目的是为了在开发阶段,因为前后端进度不一致,而临时应急
- 针对vue项目的代理配置需要在项目的根目录下创建文件“vue.config.js”,切勿写错名字
- 以下配置不要去背(webpack的配置),认识即可
- 这个文件修改之后需要重启项目才能生效(对于vue根目录下的其他配置文件也适用)
module.exports = {// 开发服务器设置devServer: {open: true,// 设置 npm run serve 启动后的端口号port: 3000,// 如果你开始了eslint,不要让eslint在页面中遮罩,它错误会在console.log控制台打印overlay: false,// vue项目代理请求proxy: {// 规则// axios中相对地址开头的字符串 匹配请求uri中的前几位"/api": {// 把相对地址中的域名 映射到 目标地址中// localhost:3000 => https://api.iynn.cn/film/api/v1/target: "https://api.iynn.cn/film/api/v1",// 修改host请求的域名为目标域名// changeOrigin: false,changeOrigin: true,// 请求uri和目标uri有一个对应关系// 请求/api/login ==> 目标 /v1/api/loginpathRewrite: {"^/api": "",},},},},
};
5、网络请求封装
回顾:网络请求
- xhr
- jQuery
- fetch
- axios
- 老尤推荐的
- axios支持promise
- axios支持拦截器
- axios支持全局配置
- axios既支持前端又支持后端(nodejs)
- ...
语法:
// 下载axios.js文件
// get语法
axios.get(url).then(ret => {// ret是响应的一个聚合对象,有这样一些属性// headers、config、request、status、statusText、data// data属性:响应的内容
});// post语法
// 发送普通表单提交的
// 请求头content-type: application/x-www-form-urlencoded
// 请求体是form data
axios.post(url,"查询字符串").then(ret => {});// 发送的是json格式的数据
// 请求头content-type: application/json
// 请求体是request payload
axios.post(url,普通的对象).then(ret => {});// 复杂语法:类似于$.ajax()
axios({url,method,timeout,....
});
在封装前请先安装axios
npm i -S axios
步骤:
请求地址文件配置
- 路径:config/uri.js
- 好处:后期接口地址如果发生了变化,我们可以统一去管理和修改
// 作用:对请求地址的配置,简化原本很长的地址写法 let prefix = "/api/";export default {// 声明各个请求地址// 城市信息getCities: prefix + "getCitiesInfo",// 正在热映getNowList: prefix + "getNowPlayingFilmList",// 即将上映getSoonList: prefix + "getComingSoonFilmList",// 。。。 };
封装请求文件
- 路径:api/http.js
import axios from "axios";// 可以对axios进行封装 // 以往在学习使用axios的时候每次取获取数据的结果都是从ret.data中获取 // 这种写法很是不方便,我们可以在此处对axios进行改造,让返回的ret就等同于以前的ret.data // 拦截器:此处是对返回结果其实就是响应进行处理,所以得使用响应拦截器 // var a = 'index.php?' // a + 'username=zhangsan' axios.interceptors.response.use((ret) => {// 将ret.data换成retreturn ret.data || ret;// if (ret.data) {// return ret.data;// } else {// return ret;// } });// 请求拦截器 // axios.interceptors.request.use();export default axios;
注册axios到vue实例上
- 好处:后续操作简单,因为每个组件中都有vue实例
this
,注册到实例上后续可以直接通过this调用,而不再需要每次都importhttp.js
- 修改文件:main.js
import Vue from "vue"; import App from "./App.vue"; import router from "./router"; import store from "./store"; // 导入axios import axios from "./api/http"; // 将axios注册到vue实例上(原型上) Vue.prototype.$http = axios; Vue.config.productionTip = false;new Vue({router,store,render: (h) => h(App), }).$mount("#app");
- 好处:后续操作简单,因为每个组件中都有vue实例
测试可用性
- 测试代码测试完毕之后需要删除
<script> import uri from "@/config/uri"; export default {async created() {let ret = await this.$http.get(uri.getCity);console.log(ret);}, }; </script>
6、vant组件配置
官网:Vant 4 - A lightweight, customizable Vue UI library for mobile web apps.
Vant 是有赞前端团队开源的移动端组件库,于 2017 年开源,已持续维护 4 年时间。Vant 对内承载了有赞所有核心业务,对外服务十多万开发者,是业界主流的移动端组件库之一。
配置使用步骤
安装
npm i -S vant
引入组件
npm i -D babel-plugin-import
// 对于使用 babel7 的用户,可以在 /babel.config.js 中配置 module.exports = {// 原先自带的presets: ["@vue/cli-plugin-babel/preset"],// 从vant文档中复制的plugins: [["import",{libraryName: "vant",libraryDirectory: "es",style: true,},"vant",],], };
导入&使用UI组件
- import ... from ...
- Vue.use( ... )
三、功能实现(1)
1、导航实现
1.1、底部导航
该功能实现需要是的vant组件是:Vant 4 - A lightweight, customizable Vue UI library for mobile web apps.
底部导航组件名称:Footer.vue
导航文件位置:src/components/Nav/Footer.vue
实现步骤
a. 将Footer.vue组件设置为全局使用的组件(将Footer.vue在App.vue中去使用)
b. 将vant的导航组件tabBar在Footer.vue中去使用(参考vant文档来使用)
- 图标问题
- 图标如果需要解决,可以使用阿里矢量图iconfont-阿里巴巴矢量图标库
- 导航跳转的问题
- 解决细小的bug
具体实现
a. 将Footer.vue在App.vue中进行使用
注意点:
- 要对组件进行注册
- 在注册组件的时候,不要将组件命名成html内置的标签名
<template><div id="app"><router-view /><!-- 使用Footer组件 --><Footer></Footer></div>
</template><script>
// 导入Footer组件
import Footer from "@/components/Nav/Footer"
export default {// 注册组件components: {Footer}
}
</script><style lang="scss"></style>
b. 将vant的导航组件tabBar在Footer.vue中去使用(参考vant文档来使用)
解决字体图标问题
从阿里矢量图上下载合适的图标
将解压缩的文件夹放到src/assets目录下,为了方便并命名为
iconfont
在Footer.vue组件中去使用iconfont.css
import "@/assets/iconfont/iconfont.css"
在Footer.vue中去使用阿里矢量图(难)
- 需要使用icon组件:Vant 4 - A lightweight, customizable Vue UI library for mobile web apps.
导航跳转的问题
- 知识点
- 编程式导航
- 语法:this.$router.push(url)
- 思路:在做的时候,导航地址有几种固定的情况,此处虽然可以使用分支语句,但是后期维护比较麻烦,建议变通考虑,可以使用地址数组,让数组的地址与下标index一一对应,后续取到了下标实则就获取到了地址。
- 知识点
细小bug需要解决
- bug1:在刷新页面的时候,tabBar的索引会被重置为0
- 解决思路:在页面加载的时候重新设置正确的active值即可
- bug2:如果用户在即将上映页面刷新的话,会匹配不到对应的地址索引
- 解决思路:在原有的基础之上,增加对地址的判断,判断是否是即将上映,如果是则active为0,否则按原计划赋值active
- bug1:在刷新页面的时候,tabBar的索引会被重置为0
<template><div><van-tabbar v-model="active" active-color="#ff5f16" inactive-color="#000" @change="changeTab"><van-tabbar-item><!-- 使用vant的icon组件 --><van-icon class="iconfont icon-dianying" slot="icon" size="20"></van-icon><span>电影</span></van-tabbar-item><van-tabbar-item><!-- 使用vant的icon组件 --><van-icon class="iconfont icon-yingyuan" slot="icon" size="20"></van-icon><span>影院</span></van-tabbar-item><van-tabbar-item><!-- 使用vant的icon组件 --><van-icon class="iconfont icon-wode" slot="icon" size="20"></van-icon><span>我的</span></van-tabbar-item></van-tabbar></div>
</template><script>
// 引入需要使用的vant组件
import Vue from "vue";
import { Tabbar, TabbarItem, Icon } from "vant";
Vue.use(Tabbar);
Vue.use(TabbarItem);
Vue.use(Icon);// 引入需要的字体样式
import "@/assets/iconfont/iconfont.css";export default {// data选项必须要求是一个函数,函数必须返回一个普通的对象data: function() {return {// active表示默认选中哪个菜单,0表示第一个菜单active: 0,// 定义好数组地址url: ["/films/nowPlaying", "/cinemas", "/center"],};},methods: {// 改变菜单后出发,形参是索引下标changeTab(index) {// 此处可以使用分支进行判断跳转地址,但是代码量比较多,不建议使用if与switch语句// 优化方案:提前定义好地址数组,让下标对应上this.$router.push(this.url[index]);},},// 在created生命周期中去解决地址因为刷新而导致的active归零的情况created() {// 获取地址栏中的路径// console.log(this.$route);let path = this.$route.path;let index = this.url.indexOf(path);// 纠正索引this.active = path === "/films/comingSoon" ? 0 : index;},
};
</script><style scoped></style>
1.2、顶部导航
使用的vant组件是:Vant 4 - A lightweight, customizable Vue UI library for mobile web apps.
底部导航组件名称:Header.vue
导航文件位置:src/components/Nav/Header.vue
实现步骤
a. 在Films.vue组件中去使用Header.vue组件
b. 完成Header.vue的代码编写
具体实现
a. 在Films.vue组件中去使用Header.vue组件
<template><!-- 视图部分 --><div><!-- 3. 使用header组件 --><Header></Header><router-view></router-view></div>
</template><script>
// 1. 导入Header.vue
import Header from "@/components/Nav/Header"
// 逻辑部分
export default {// 2. 注册组件components: {Header}
};
</script><style scoped>
/* 样式部分 */
</style>
b. 完成Header.vue的代码编写
<template><div><van-tabs v-model="active" @click="onClick" line-width="60" line-height="2" title-active-color="#ff5f16" title-inactive-color="#191a1b"><van-tab title="正在热映"></van-tab><van-tab title="即将上映"></van-tab></van-tabs></div>
</template><script>
// 1. 引入vant的tab和tabs组件
import Vue from "vue";
import { Tab, Tabs } from "vant";
Vue.use(Tab);
Vue.use(Tabs);export default {data: function() {return {// 默认tab索引active: 0,// 数组地址url: ["/films/nowPlaying", "/films/comingSoon"],};},methods: {/*** index:索引* title:tab的名称(文字内容)*/onClick(index, title) {// 编程式导航this.$router.push(this.url[index]);},},// 在生命周期中解决刷新导致active归零的情况created() {this.active = this.url.indexOf(this.$route.path);},
};
</script><style scoped></style>
2、电影模块
2.1、正在热映列表
需求点
- 列表的基本的展示
- 顶部导航的吸顶效果
- 往下滚动/往上滑动实现加载更多
基本信息
涉及的组件:src/views/Films/NowPlaying.vue
涉及的vant组件:
- 卡片组件:Vant 4 - A lightweight, customizable Vue UI library for mobile web apps.
- 加载更多的列表组件:Vant 4 - A lightweight, customizable Vue UI library for mobile web apps.
目标1:实现列表的基本展示
a. 先获取接口的数据
b. 循环展示列表
在做列表展示的时候需要注意,卡片组件Card有一种奇怪的用法,代码如下:
<van-card num="2" price="2.00" desc="描述信息" title="商品标题" thumb="https://img01.yzcdn.cn/vant/ipad.jpeg"><template #tags><van-tag plain type="danger">标签</van-tag></template> </van-card>
在Card组件中,有一些属性例如“desc”,可能由于内容比较多或者其它原因需要使用样式(自定义),则以属性形式不好操作;vant支持我们使用“插槽”在
template
中去自定义展示,例如上述代码的“#tags”插槽,其实展示的内容就是Card组件的tags属性中的内容。以此类推,如果需要自定义“desc”属性,则可以这么写:<template #desc>自己定义的desc属性中的内容 </template>
在用插槽的形式之后,要删除属性的写法。
目标2:实现顶部导航的吸顶效果
吸顶效果与滚动条距离顶端的高度有关,肯定是需要获取滚动条的位置的。
修改的组件:src/components/Nav/Header.vue
小步骤:
- 获取滚动条的位置
// mounted周期中获取滚动条高度
mounted() {// 监听滚动条的滚动事件addEventListener("scroll", () => {// 获取滚动条距离顶部的高度let scrollTop = document.documentElement.scrollTop;if (scrollTop >= 300) {// 具备吸顶效果this.isSticky = true;} else {// 不具备吸顶效果this.isSticky = false;}});
},
- 写粘性布局的样式
/* 吸顶样式 */
.sticky {position: fixed;z-index: 999;width: 100%;
}
- 在合适的时候(特定的高度)去应用布局的样式(动态样式绑定)
<!-- 需要在data中定义isSticky数据,默认值为false -->
<div :class="{ sticky: isSticky }"><van-tabs v-model="active" @click="onClick" line-width="60" line-height="2" title-active-color="#ff5f16" title-inactive-color="#191a1b"><van-tab title="正在热映"></van-tab><van-tab title="即将上映"></van-tab></van-tabs>
</div>
目标3:实现列表的加载更多
请注意,在使用List组件的时候,务必要留意,需要使用list组件包裹我们已经实现的Card组件,不要连同官网手册中Cell组件一起复制。
==完整参考代码==
<template><!-- 视图部分 --><div class="main"><van-list v-model="loading" :finished="finished" finished-text="——————————我是有底线的——————————" @load="onLoad"><!-- 展示正在热映列表 --><van-card v-for="item in list" :key="item.filmId"><!-- 电影描述:主演、时长、国籍等信息 --><template #desc><div class="desc"><div>观众评分:<span class="grade">{{ item.grade }}</span></div><div>主演:{{ item.actors | parseActors }}</div><div>{{ item.nation }} | {{ item.runtime }}分钟</div></div></template><!-- 电影名称 --><template #title><div class="title">{{ item.name }}</div></template><!-- 电影缩略图 --><template #thumb><img :src="item.poster" width="66" /></template><!-- 购票按钮 --><template #tags><van-tag plain class="buyTicket" size="large" type="danger">购票</van-tag></template></van-card></van-list></div>
</template><script>
// 引入vant的组件
import Vue from "vue";
import { Card, Tag, List } from "vant";
Vue.use(Card);
Vue.use(Tag);
Vue.use(List);// 导入地址模块
import uri from "@/config/uri";
export default {data() {return {// 默认的页码pageNum: 1,// 电影列表list: [],// 确定加载状态的,是否处于加载中。在每次加载完毕后需要设置为falseloading: false,// 确定是否全部请求完毕的,当所有的数据全部获取到后,需要将该值设置为truefinished: false,};},methods: {// 触发数据加载的// 请注意,该onLoad方法并不是首次先加载created周期中的,后续再使用onLonad的,而是每次都会使用onLoadonLoad() {this.$http.get(uri.getNowPlaying + "?pageNum=" + this.pageNum).then((ret) => {// 获取下最大的页码let maxPageNum = Math.ceil(ret.data.total / 10);// console.log(maxPageNum);// console.log(ret);// 将原先的数据与现在本次的数据做合并this.list = [...this.list,...ret.data.films];// 本次请求已经完成this.loading = false;if (this.pageNum < maxPageNum) {// 页码+1this.pageNum++;} else {// 说明已经全部加载完毕了this.finished = true;}});},},// created发送请求// created() {// this.$http.get(uri.getNowPlaying + "?pageNum=" + this.pageNum).then((ret) => {// // console.log(ret);// this.list = ret.data.films;// });// },// 过滤器// 作用:处理数据格式filters: {// 解析主演parseActors(actors) {let str = "";// 动画片可能没有演职人员if (actors) {actors.forEach((el) => {str += el.name + " ";});} else {str = "暂无主演";}// 注意长度的截取return str.length > 17 ? str.substr(0, 17) + "..." : str;},},
};
</script><style scoped>
/* 样式部分 */
/* 解决图片自带的圆角 */
.van-card__thumb img {border-radius: 0px;
}
/* 重写图片右侧的边距 */
.van-card__thumb {width: 70px;
}
/* 电影名称 */
.title {font-size: 16px;
}
/* 描述样式 */
.desc {font-size: 13px;color: #797d82;
}
/* 评分 */
.grade {color: orange;
}
/* 购票按钮 */
.buyTicket {position: relative;float: right;top: -35px;
}
/* 防止最后一个被挡住 */
.main {margin-bottom: 50px;
}
</style>
2.2、即将上映列表
作业
- 按钮提示文字修改
- 上映时间为
premiereAt
,该字段是时间戳,单位是秒,注意转化
2.3、电影详情
涉及需要修改的组件:
- src/views/Films/Detail.vue
- src/views/Films/Films.vue
实现步骤
a. 需要在列表上给每个电影的条目添加点击事件,点完之后需要携带电影的id号去详情页面(动态路由参数);
b. 详情页面需要获取电影id号,然后根据电影的id号去查询获取电影的信息并且展示,细节有:
- 图标播放区域,应该是用什么插件
- 在进入详情页面之后,底部导航消失了。在离开详情页面之后底部导航又出现了
具体实现
a. 需要在列表上给每个电影的条目添加点击事件,点完之后需要携带电影的id号去详情页面(动态路由参数);
给Card组件添加点击事件:
<van-card v-for="item in list" @click="goDetail(item.filmId)" :key="item.filmId">
添加事件处理程序:
// 事件处理程序:去详情页
goDetail(filmId) {// 编程式导航this.$router.push("/film/" + filmId);
},
b. 详情页面需要获取电影id号,然后根据电影的id号去查询获取电影的信息并且展示,细节有:
步骤1:获取id,根据id获取电影的基本信息
// 逻辑部分
import uri from "@/config/uri";export default {data() {return {filmInfo: {},};},// 获取数据created() {// 获取电影的id号(获取时参数名称要与路由规则中声明的一致)let filmId = this.$route.params.film_id;this.$http.get(uri.getDetail + "?filmId=" + filmId).then((ret) => {// console.log(ret.data.film);this.filmInfo = ret.data.film;});},
};
步骤2:需要将刚才步骤1获取到的数据在组件视图部分展示出来(不考虑图片滑动区域、底部导航的隐藏)
关于项目中时间戳的格式化:
- 自己去使用Date对象去做格式化
- 使用三方的模块去处理格式化(推荐)
- moment包
- 使用方法
- 安装:
npm i -S moment
- 官网手册:Moment.js 中文网
- 语法:
moment(可选的时间戳).format(指定输出的格式);
- moment()方法的参数如果没有,则解析当前时间戳,如果有指定那就解析指定的时间戳。
- format()表示想要输出的格式,例如如果我们想输出“2021-05-20 10:00:00”,则可以通过以下形式指定:
YYYY-MM-DD HH:mm:ss
- moment作为js包,这里时间戳处理单位为毫秒
视图的展示:
<!-- 视图部分 -->
<div><div class="header"><!-- 头部 --><div style="display:flex;"><!-- 返回功能 --><div style="width:10%;" @click="goBack">返回</div><div style="width:90%;text-align:center;">{{ filmInfo.name }}</div></div><div><img :src="filmInfo.poster" width="100%" height="300" alt="" /></div></div><div class="detail"><div style="display:flex;"><div>{{ filmInfo.name }}</div><div>{{ filmInfo.grade }}</div></div><div>{{ filmInfo.category }}</div><div>{{ filmInfo.premiereAt | parseTime }}上映</div><div>{{filmInfo.nation}} | {{filmInfo.runtime}}分钟</div><div>{{filmInfo.synopsis}}</div></div><div class="photos1"><!-- 图片区域(演职人员) --><div>演职人员</div><div><!-- 在这里输出可以滑动的图片信息 --></div></div><div class="photos2"><!-- 图片区域 --></div>
</div>
逻辑代码中针对时间戳的格式化与返回上一页的代码:
// 导入moment
import moment from "moment";export default {methods: {// 返回上一页goBack() {this.$router.go(-1);},},// 过滤器filters: {// 修饰上映时间parseTime(timestamp) {// moment作为js包,这里时间戳处理单位为毫秒return moment(timestamp * 1000).format("YYYY-MM-DD");},},
};
步骤3:实现图片的滑动显示
使用插件:Swiper
官网:Swiper中文网-轮播图幻灯片js插件,H5页面前端开发
确定使用的案例是:Swiper demo
安装Swiper:
npm i -S swiper
使用步骤:
- 引入css和js
- 定义容器
- 实例化
在vue中,存在数据是异步请求,但是后面的渲染需要用到数据的情况,这个时候我们往往想到的是定时器去解决这个问题,这个不是最优的方法。vue提供了一个异步渲染的方法:$nextTick,语法如下:
this.$nextTick(() => {// 写需要等待异步结束之后再去做的操作 });
视图部分的结构:
<div class="photos1"><!-- 图片区域(演职人员) --><div>演职人员</div><div><!-- 在这里输出可以滑动的图片信息 --><div class="swiper-container"><div class="swiper-wrapper"><div class="swiper-slide" v-for="item in filmInfo.actors" :key="item.name"><img :src="item.avatarAddress" width="85" /><div>{{ item.name }}</div><div>{{ item.role }}</div></div></div></div></div>
</div>
逻辑部分:
// 导入swiper相关的外部文件
import Swiper from "swiper";
import "swiper/swiper-bundle.min.css";// 获取数据
export default {created() {// 获取电影的id号(获取时参数名称要与路由规则中声明的一致)let filmId = this.$route.params.film_id;this.$http.get(uri.getDetail + "?filmId=" + filmId).then((ret) => {// console.log(ret.data.film);this.filmInfo = ret.data.film;// 产生滑动图片组this.$nextTick(() => {new Swiper(".swiper-container", {slidesPerView: 4,spaceBetween: 30,});});});},
}
步骤4:实现底部导航适时的隐藏和展示(eventBus)
实现思路:进入到详情组件的时候会通知底部导航隐藏,离开详情组件的时候会通知底部导航显示。
涉及到的知识点:
- 生命周期
- created(进入)
- beforeDestroy(离开)
- eventBus(组件通信)【事件中心的这种数据共享方式与子传父的思想是一样的,都是通过事件/事件监听来实现的,所以都有关键词
emit
和on
】
- 指令:v-show
将事件中心建立在vue的原型上(修改的是main.js文件)
// 将事件中心建立好之后放到vue原型上,避免后续再使用的时候频繁去new Vue()
Vue.prototype.$eventBus = new Vue();
在App.vue中监听自定义事件toggleFooter
<template><div id="app"><router-view /><!-- 使用Footer组件 --><Footer v-show="isShow"></Footer></div>
</template><script>
// 导入Footer组件
import Footer from "@/components/Nav/Footer";
export default {data() {return {isShow: true,};},// 注册组件components: {Footer,},// 接收通知(监听事件)created() {// 开始监听底部导航的改变事件this.$eventBus.$on("toggleFooter", (val) => {// val就是实际需要使用的值this.isShow = val;});},
};
</script><style lang="scss"></style>
在Detail.vue中去通知显示和隐藏
// 获取数据
created() {// 通知事件中心隐藏底部导航this.$eventBus.$emit("toggleFooter", false);//....
},
beforeDestroy() {// 在将要离开该组件的时候通知事件中心将底部导航放出来this.$eventBus.$emit("toggleFooter", true);
},
四、Vuex(重点)
1、vuex是什么?
vuex是一种项目中数据共享的方式。
其具有以下优势:
- 能够在vuex中集中管理共享的数据,便于开发和后期进行维护
- 能够高效的实现组件之间的数据共享,提高开发效率(代码量)
- 存储在vuex中的数据是响应式的,当数据发生改变时,页面中的数据也会同步更新
什么样的数据适合存储在Vuex中?
一般情况下,只有组件之间共享的数据才有必要存储到vuex中,对于组件中私有的数据依旧存储在组件自身的data中即可。
2、vuex的安装及配置
vuex不是脚手架在安装项目的时候自带的,是一个选配的功能,默认是不被安装的(需要自己根据需要选择)。因此其安装和配置存在两种情况:
情况1:在通过vue脚手架vue create xxxx
的命令的时候,手动选择安装vuex【极力推荐】。好处在于不需要自己手动创建store
目录及目录下的index.js
文件。
情况2:在通过vue脚手架vue create xxxx
的命令的时候,可能没有选择安装vuex,则这个时候我们有两种选择:
- 删了重来,再建立项目的时候选择安装vuex
- 当然也可以通过命令来补救安装,但是通过命令后续安装的vuex,需要自己创建
store
目录和其下的index.js
文件- npm i -S vuex
3、vuex核心(重点)
- state:提供唯一公共数据源,所有的共享数据都要统一放到state中进行存储
// 在组件中访问state数据的第一种方式(单个)
this.$store.state.全局数据名称
// 在组件中访问state数据的第二种方式(批量)
// 按需导入mapState函数
import {mapState} from 'vuex'
// 将全局函数映射为当前组件的计算属性
computed: {...mapState(['count'])
}
第二种方式映射过来的情况,其数据的使用方式如同在当前组件中使用自身的data数据一样(下同)。
- 在视图中,就直接插值表达式
- 在js中就
this.xxxx
- mutation(s):用于变更store中的数据(修改)
- 在Vuex中只能通过mutation变更store中的数据,不可以直接操作store中的数据
- 通过这种方式操作起来稍微繁琐一些,但是可以集中监控所有数据的变化
// 定义mutations
const sotre = new Vuex.Store({state: {count: 0},mutations: {add(state[,arg]){// 变更状态state.count++}}
})
// 组件中触发mutation的第一种方式
methods:{handle(){this.$store.commit('add'[,arg])}
}
// 组件中触发mutation的第二种方式
import {mapMutations} from 'vuex'
methods:{...mapMutations(['add','reduce']),handle1(){this.add()},handle2(){this.reduce([arg])}
}
==不要在mutation中写异步的代码==
在mutation中混合异步调用会导致你的程序很难调试。每个mutation执行完成后都会对应到一个新的状态变更,这样devtools就可以打个快照存下来,然后就可以实现 time-travel 了。如果mutation支持异步操作,就没有办法知道状态是何时更新的,无法很好的进行状态的追踪,给调试带来困难。
- action(s):用于处理==异步==操作任务
// 声明action
const store = new Vuex.Store({// 省略其他代码mutations: {add(state){state.count++}},actions: {addAsync(context[,arg]){setTimeout(() => {context.commit('add'[,arg])},1000)}}
})
// 组件中触发action
methods: {handle(){this.$store.dispatch('addAsync'[,arg])}
}
action也是支持如同state、mutation一样的按需导入mapActions方式进行触发。
- getter(s):对store中已有的数据加工处理形成新的数据
- 对已有的数据进行加工除了,类似于Vue的计算属性
- store数据发生变化,则getter中的数据也会跟着变化
// 定义getter
....
getters: {showNum: state => {return '当前最新的数量是【' + state.count + '】'}
}
// 在组件中访问getters数据的第一种方式
this.$store.getters.全局数据名称
// 在组件中访问getters数据的第二种方式
// 按需导入mapGetters函数
import {mapGetters} from 'vuex'
// 将全局函数映射为当前组件的计算属性
computed: {...mapGetters(['showNum'])
}
4、模块化(重点)
为什么有状态的模块化?
- 主要是因为项目是多人协作开发的,如果都去修改一个文件,则经常会出现代码冲突,而解决冲突比较费事费力。
使用步骤
- 建立src/store/modules文件夹(名称随意)
- 在modules文件夹中建立需要的模块文件(命名以功能为导向,记得导出一下)
注意点1(了解):
- 在模块的时候,因为多人合作,不能的开发者之前并不清楚其他怎么给方法和数据源进行命名,这样的话就有一个问题:万一名称重名怎么办?如果冲突了,会执行以下合并策略:
- state数据源肯定不会冲突,它以模块进行保存
- mutations、actions的方法不会以模块为单位进行保存,如果出现同名则可能会冲突。vuex会先将这些同名的方法,整合到一起,都去执行。会先执行index.js中的,再去执行其他的。
- getters如果出现冲突,不给解决,直接报错。
- 因为多人合作可能出现命名的冲突,特别针对getters,vuex模块化的时候支持使用
命名空间
- 默认是没有给模块开启命名空间的
- 如果需要请自己开启,通过模块对象的属性“namespaced”,将其值设置为true
- 命名空间的名称,是模块的名字(模块里面属性的名字)
- 在模块的时候,因为多人合作,不能的开发者之前并不清楚其他怎么给方法和数据源进行命名,这样的话就有一个问题:万一名称重名怎么办?如果冲突了,会执行以下合并策略:
注意点2:由于模块使用了命名空间,所以之前没有模块化的使用方式(this、map系列)在模块化之后都要发生对应的变化
- state
- this形式:this.$store.state.空间名.xxxx
- map系列:...mapSate(空间名,[xxxx,yyyy,zzzz...])
- mutations
- this形式:this.$store.commit("空间名/方法名", "参数");
- map系列:...mapMutations("空间名",["方法名",...]),
- actions
- this形式:this.$store.dispatch("空间名/方法名", "参数");
- map系列:...mapActions("空间名",["方法名",...]),
- getters
- this形式:this.$store.getters["空间名/属性名"]
- map形式:...mapActions("空间名", ["属性名",....]),
- state
5、改写eventBus案例
a. 先要定义默认的数据源(store/modules/common.js)
// 拆分后的模块,基本保留了原先的核心属性(除了modules)// 该模块由张三负责// 请注意:在实际开发项目的时候,每个人可能负责不同的模块,鉴于在写代码的时候他们并不可能每次都坐在一起商量vuex中的变量、方法的命名,则可能会出现名称冲突的情况。对于冲突情况vuex会帮我们自动解决,它有自己的合并策略,but对于getters没有合并策略,遇到重名的getters就会报错。这个问题需要解决。// 解决方案:采用模块命名空间(思想来自于后端)
// 只要给导出的成员加上namespaced属性,值设置为true即可
// 原理:在往index.js中合并vuex各个模块的时候,会先产生一个以模块名称为名的属性,然后才会把模块中的变量和方法放进去。// 空间名是指在index.js的modules对象中的属性名// Object.common.isShow
// Object.user.isShowexport default {// 开启命名空间,防止命名冲突namespaced: true,// 默认的数据源state: {// 是否限时底部导航isShow: true,},// 同步修改数据的方法集合mutations: {setIsShow(state, arg) {state.isShow = arg;},},// 异步修改数据的方法集合actions: {setIsShowAsync(context, arg) {setTimeout(() => {context.commit("setIsShow", arg);}, 500);},},// 数据修饰处理方法集合getters: {getIsShow(state) {return state.isShow ? "显示" : "隐藏";},},
};
b. 去除在main.js中对eventBus的原先挂载操作
c. 去除App.vue中的事件监听,换成直接使用store对象的写法
<!-- 使用Footer组件 -->
<Footer v-show="$store.state.common.isShow"></Footer>
d. 在详情组件进入和离开的时候使用vuex修改数据源
进入组件(created生命周期):
this.$store.commit("common/setIsShow", false);
离开组件(beforeDestroy生命周期):
this.$store.commit("common/setIsShow", true);
五、功能实现(2)
1、NodeJS接口实现
1.1、接口安全
前后端分离式开发需要进行数据交互,传输的数据被偷窥、被抓包、被伪造时有发生,那么如何设计一套比较安全的API接口方案呢?
并不是所有的接口都需要考虑安全的,有些接口是公开的,任何人只要知道地址都可以调用,对于一些项目中需要用户登录才能访问的接口才需要考虑安全问题。
一般解决的方案有以下几类:
- token令牌认证(json web token:jwt)
- 不用服务端负责存储
- 基于json格式(跨语言性好)
- 允许我们携带一些业务逻辑需要但非秘密的数据
- AK(app key)&SK(secret key)【用户名&密码】
- 时间戳超时验证+签名算法字符串
- 付款场景
- 数据脱敏(防范数据库数据泄露)
- HTTPS
- 数字证书(防运营商)
- IP黑/白名单(服务器层面的限制,apache、nginx)
- oAuth2.0
关于
JWT
:
Json web token(JWT),是基于token的鉴权机制,类似于http协议也是无状态的,它不需要在服务端去保留用户的认证信息或者会话信息,为应用的扩展提供了便利。JWT具备以下几个优点:
因json的通用性,所以JWT是可以进行跨语言
JWT可以在自身存储一些其他业务逻辑所必要的非敏感信息
便于传输,jwt的构成非常简单,字节占用很小,所以它是非常便于传输的
它不需要在服务端保存会话信息,所以它非常适合应用在前后端分离的项目上
使用JWT进行鉴权的工作流程如下(重点):
- 用户使用用户名密码来请求服务器
- 服务器进行验证用户的信息(查数据库)
- 服务器通过验证发送给用户一个token(令牌)
- 客户端存储token(Vuex+localStorage),并在每次请求时附送上这个token值
- 服务端验证token值,并返回数据
JWT是由三段信息构成的(头部、载荷、签名),将这三部分使用.
连接在一起就组成了JWT字符串,形如:(“头部.载荷.签名”)
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImp0aSI6IjNmMmc1N2E5MmFhIn0.eyJpYXQiOjE1NTk1Mjk1MjksImlzcyI6Imh0dHA6XC9cL3d3dy5weWcuY29tIiwiYXVkIjoiaHR0cDpcL1wvd3d3LnB5Zy5jb20iLCJuYmYiOjE1NTk1Mjk1MjgsImV4cCI6MTU1OTUzMzEyOSwianRpIjoiM2YyZzU3YTkyYWEiLCJ1c2VyX2lkIjoxfQ.4BaThL6_TbIMBGLIWZgpnoDQ-JlAjzbiK3y3BcvNiGI
其中:
- 头部(header),包含了两(可以更多)部分信息,分别是类型的声明和所使用的加密算法。
一个完整的头部就像下面的JSON:
{'typ': 'JWT','alg': 'HS256'
}
然后将头部进行base64加密/编码(该加密是可以对称解密的),这就得到了jwt的第一部分。
- 载荷(payload)(body),载荷就是存放有效信息的地方。这些有效信息包含三个部分
- 标准中约定声明(建议但不强制)
- 签发人
- 使用者
- 签发时间
- 有效期
- ....
- 公共的声明
- 私有的声明
- 标准中约定声明(建议但不强制)
定义一个payload:
{"sub": "1234567890","name": "John Doe","admin": true
}
依旧进行base64加密,这就得到了jwt的第二部分。
- 签名(signature),这个签证信息由三部分组成:
- 经过base64编码后的
- header
- payload
- secret(就是一个字符串,自己定义,值是什么无所谓)
- 经过base64编码后的
例如:
var encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload);
var signature = HMACSHA256(encodedString, 'secret');
这样就得到了jwt的第三部分。
var jwt = encodedString + '.' + base64UrlEncode(signature);
最终将三部分信息通过.
进行连接就得到了最终的jwt字符串。后续不需要自己去写jwt怎么生成的。因此,此流程理解即可。
需要注意的是
- secret是保存在服务器端的
- jwt的签发生成也是在服务器端的
- secret是用来进行jwt的签发和jwt的验证
所以,secret它就是服务端的私钥,在任何场景都不应该泄露出去。一旦其他人(包括客户端的用户)得知这个secret,那就意味着他们可以自我签发jwt,接口就没有安全性可言了。
1.2、用户登录接口
①新建一个空文件夹,在其中初始化NodeJS项目
npm init -y
npm i -S express md5 mongoose jsonwebtoken body-parser cors
②新建http.js
文件,创建一个express服务器
const express = require("express");
const app = express();
const port = 3000;
const path = require("path");
const fs = require("fs");
const md5 = require("md5");
const bodyParser = require("body-parser");
const jwt = require("jsonwebtoken");
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json())
app.get("/", (req, res) => res.send("Hello World!"));app.listen(port, () => console.log(`Server is running at http://127.0.0.1:${port}!`));
③使用md5
模块来编写一个对密码加密的中间件
// 密码加密中间件
const passwdCrypt = (req, res, next) => {// 明文密码字段为password// 加盐加密或加料加密const passwd = md5(req.body.password + md5(req.body.password).substr(9, 17));// 将加密的密码挂载到req的body上(覆盖原密码)req.body.password = passwd;// 继续next();
};
app.use(passwdCrypt);
④调用写好的密码加密中间件生成一个用户的初始密码用于后面做登录使用
// 接口3:获取初始的数据库中用户的密码加密结果(一次性接口)
app.post("/api/v1/user/passwdInit", (req, res) => {res.send("您的初始密码为123456,加密结果为:" + req.body.password);
});
POST形式访问
/init
获得密码后就得到了一个完整的用户数据,此时可以将数据写入到MongoDB中。如果我们自己的加密方式与讲义的代码不一样,请根据自己加密得到的密码来实际替换下面的password字段的值。
{userId: 31167509,mobile: '18512345678',password: '66b044ec6d334ad42eca2a4c164bde17',headIcon: 'https://mall.s.maizuo.com/4f0b29878f62f5e298a89a4654f0e8f0.jpg',gender: 0,
}
将模拟好的数据,写入到数据库中,以便后面做登录操作:
⑤配置jsonwebtoken
模块需要用的secret
,并在代码中读取供后续使用
在node项目目录中创建一个.env(Linux以.开头都为隐藏文件)并在此文件中写入jwt加密所需要的秘钥。同时,.env文件不要上传到Github上(.gitignore文件中声明忽略)。
在代码中读取secret
// 读取secret
const secret = fs.readFileSync(path.join(__dirname,"../",".env"),"utf-8");
⑥引入mongoose
// 引入mongoose
const mongoose = require("mongoose");
mongoose.connect("mongodb://localhost:27017/maizuo", {useNewUrlParser: true,useUnifiedTopology: true,
});
const UserSchema = new mongoose.Schema({userId: {type: Number,required: true,},mobile: {type: String,required: true,},password: {type: String,required: true,},headIcon: String,gender: Number,
});
const Model = mongoose.model("User", UserSchema, "users");
⑦创建登录路由/api/v1/user/login
实现用户名密码校验,并判断校验结果做出响应
// 登录验证接口
app.post("/api/v1/user/login", (req, res) => {// 获取手机号与加密之后的密码let data = req.body;// 去数据库中去查询上是否存在对应的记录// 注意:mongoose提供的方法都是异步的Model.findOne(data).then((ret) => {// findOne:查不到返回null,查到就返回对应的文档(建议)// find:查不到返回空数组,查到了返回包括文档的数组if (ret) {// 查到了,签发令牌// 语法:jsonwebtoken.sign(载荷对象,secret)let _token = jsonwebtoken.sign({userId: ret.userId,},secret);res.send({error: 0,msg: "登录成功!",_token,});} else {// 没查到res.send({error: 1,msg: "手机号或密码错误。",});}});
});
最终输出
登录成功则输出:
{"error": 0,"msg": "登录成功!","_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjMxMTY3NTA5LCJpYXQiOjE2MjA3MDI0MDh9.KA0ANNgUvZoZQmftacFvsUxia0_q0Ofx3ZRL9TJhdaE"
}
登录失败则输出:
{"error": 1,"msg": "手机号或密码错误。"
}
1.3、获取用户信息接口
个人中心的信息是用户登录成功后才能进行的页面展示,在请求数据时,后台接口一定要判断当前请求是否有token,且token解密后一定是一个合法数据。
**接口需求:**依据客户端传递给服务端的用户编号userId
,在验证通过jwt
后输出对应用户信息
注意点:
有些企业提供的接口jwt所返回的token格式可能会在原有token之前拼接一个
持有者(空格)
的信息,例如用户zhangsan
获取到的token:
zhangsan eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjMxMTY3NTA5LCJtb2JpbGUiOiIxODUxMjM0NTY3OCIsImlhdCI6MTYwMjY0OTg2NX0.tVByVZYu4s5dgzLZwR00HHW7QZ0gkYpVXaVNhCdawbU
如果是上述在接收token的时候需要注意,别获取错误了。注意,验证是否合法只用token,前面的持有者不用。
// 接口2:获取用户信息
app.get("/api/v1/user/getUserInfo", (req, res) => {// 1. 认证token(在认证的过程中,如果认证失败,程序会抛异常)try {let tokenStr = req.headers["authorization"];let arr = tokenStr.split(" ");// 最后一个元素即tokenlet token = arr[arr.length - 1];// 开始验证令牌// 语法:jsonwebtoken.verify(令牌字符串,secret)// 验证成功返回载荷,验证失败抛异常const ret = jsonwebtoken.verify(token, secret);// (可选)验证是否过期【约定过期时间2小时,7200s】if (Date.now() / 1000 - ret.iat > 7200) {// 过期了res.send({error: 3,msg: "token令牌已经过期。",token: "",});} else {// 没过期// 判断是否马上要过期了,如果是自动给它生成新的tokenif (Date.now() / 1000 - ret.iat > 5400) {token = jsonwebtoken.sign({userId: ret.userId,},secret);}// 获取数据Model.findOne({ userId: ret.userId }).then((ret) => {// 2. 返回(失败或成功)if (ret) {// 取到信息了,则返回res.send({error: 0,msg: "用户信息获取成功!",token: token,data: {userId: ret.userId,mobile: ret.mobile,headIcon: ret.headIcon,gender: ret.gender,},});} else {// 账号已经已经没了res.send({error: 4,msg: "你号没了。",token: "",});}});}} catch (error) {// 抛异常了res.status(500).send({error: 2,msg: "token令牌验证失败。",});}
});
2、登录操作
①先给登录按钮绑定点击事件,点击之后去往登录页面
<div class="nick-name" @click="goLogin">立即登录
</div>
事件处理程序
methods: {goLogin() {// 编程式导航this.$router.push("/login");},
},
②需要在登录组件Login.vue中设置底部导航的适时显示和隐藏(vuex)
created() {// 进入该组件隐藏底部导航this.$store.commit("common/setIsShow", false);
},
beforeDestroy() {// 离开该组件显示底部导航this.$store.commit("common/setIsShow", true);
},
③在Login.vue中发起登录请求
// 表单提交按钮触发的处理程序
onSubmit(values) {// 发起网络请求请求nodejs// 如果参数直接就是values,则该请求为post形式json请求this.$http.post("http://127.0.0.1:8000/api/v1/user/login", values).then((ret) => {if (ret.error > 0) {// 出错了Toast.fail(ret.msg);} else {Toast.success(ret.msg);setTimeout(() => {this.$router.push("/center");}, 2000);}});
},
④在模块化的vuex中(common.js)设置针对token的配置项
state: {isShow: true,_token: "",
},
mutations: {setIsShow(state, arg) {state.isShow = arg;},// 设置token的值setToken(state, arg) {state._token = arg;// 存储到localStorage中localStorage.setItem("_token", arg);},
}
⑤在响应拦截中统一保存token(http.js)
// 在非vue文件中无法使用`this.$store`对象
// 导入store获取store对象
import store from "@/store/index"// 响应拦截器
// 使用场景:用于对响应结果进行加工处理再返回
axios.interceptors.response.use((ret) => {// 为了方便,在拦截器中判断是否有token,如果有则直接存储(复用)if (ret.data._token) {// 存储到vuex中store.commit("common/setToken", ret.data._token);}// 再简写(短路运算)return ret.data || ret;
});
这个时候会出现一个问题,当页面刷新的时候vuex中的数据就会被重新初始化。但是localStorage中的数据依旧还在,所以需要将数据再次反向同步一下。时机:在页面加载的时候。
⑥在入口main.js文件中去做本地存储与vuex的同步
// 在这里统一从localStorage中获取数据赋值给vuex
let _token = localStorage.getItem("_token");
if (_token) {store.commit("common/setToken", _token);
}
3、获取用户信息
流程:在进入到个人中心组件后应先判断下当前用户是否登录(是否有token),如果有尝试使用token去调用接口获取个人信息,取到了再展示。
①在请求拦截器中添加token的请求
// 导入store获取store对象
import store from "@/store/index";// 请求拦截器:
// 使用场景:需要在请求的时候设置全局的超时时间、设置比较统一的请求头等
axios.interceptors.request.use((config) => {// 拦截下来// 在这里增加上额外需要的处理,比如加头信息、加全局配置。。。// 追加头信息let _token = store.state.common._token;if (_token) {// 加请求头config.headers["Authorization"] = _token;}// 放行return config;
});
②在个人中心组件发送请求获取个人信息
data() {return {// 初始化个人信息对象userInfo: {},};
},
created() {let _token = this.$store.state.common._token;if (_token) {this.$http.get("http://127.0.0.1:8000/api/v1/user/getUserInfo").then((ret) => {if (ret.error == 0) {// 赋值给初始可变数据this.userInfo = ret.data;}});}
},
③在视图中展示数据
针对是否登录的测试,通过浏览器的无痕模式(Ctrl+Shift+N),在该模式下,不会与普通模式共享任何会话信息。
4、防止翻墙
知识点:路由守卫
含义:防止用户绕过登录页面,通过直接在地址栏中输入地址去访问原本需要登录才能访问的页面。
①例如:给个人中心的“余额”按钮绑定点击事件,点击之后去往余额组件
<div @click="goAccount" class="margin-set my-balance" data-enter-time="1608645908" data-click-fun="track_f_475218">
事件处理程序:
methods: {// 去账户余额页面goAccount() {this.$router.push("/account");},
},
②在无痕模式下直接访问余额地址也可以访问到页面内容(不合理)
在实际开发的时候对于翻墙操作,前后端应该统一战线,与“翻墙行为”不共戴天。也就是说不管做前端也好,还是做后端也罢,都需要解决用户翻墙的行为。只不过以前只要后端做就可以了,现在前后端都要做。先触发前端的防翻墙;如果前端拦不住,后端再上。
③使用路由守卫
分为全局守卫和局部守卫。
修改router/index.js文件,添加全局的前置路由守卫
router.beforeEach((to, from, next) => {// 需要定义一组数据(路由),这组路由可能是需要登录才能访问的(当然也可能是不需要登录就能访问的),当我们获取到目标路由的时候,可以去数组中进行判断,符合特定条件就继续,否则不允许访问。// 例如:下面的数组需要登录才能访问let needLogin = ["/account", "/order", "/settings"];if (needLogin.includes(to.path)) {// 判断是否有tokenlet token = store.state.common._token;if (token) {// 登录了next();} else {// 没登录,去登录页面router.push("/login?goto=" + to.path);}} else {// 继续next();}
});
④可以设置指定返回的地址
当用户登录成功之后,设置跳转到指定的地址(在登录的vue组件中):
// 表单提交按钮触发的处理程序
onSubmit(values) {// 发起网络请求请求nodejs// 如果参数直接就是values,则该请求为post形式json请求this.$http.post("http://127.0.0.1:8000/api/v1/user/login", values).then((ret) => {if (ret.error > 0) {// 出错了Toast.fail(ret.msg);} else {Toast.success(ret.msg);setTimeout(() => {// 判断是否有来源地址if (this.$route.query.goto) {this.$router.push(this.$route.query.goto);} else {this.$router.push("/center");}}, 2000);}});
},
六、项目上线流程(记忆)
1、项目上线的要素
- 辅助软件
- 连接服务器的软件
- 文件传输工具(FileZilla或其他替代方案)/ Git
- 服务器(购买)
- 选型:Linux(性能好,安全性高)
- 配置环境(难度大,命令行)
- 域名(可选)
- 好记
- 在大陆地区使用大陆的服务器,需要对域名进行备案(15天)+ 公安备案
- 代码
2、Vue项目上线发布
2.1、购买云服务器
设置安全组/防火墙:
2.2、云服务器操作基础
①使用cmder等终端工具连接远程服务器
ssh root@服务器公网IP地址
在首次连接时会询问是否连接,输入yes
按下回车。随后输入密码,在输入密码的时候没有任何提示,确认正确输入后按下回车。
退出的方式有2种:
- 简单粗暴关闭连接工具
- Ctrl + D
服务器旨在长期稳定的给用户提供服务,因此没有特殊需求,一般是不用关机的。因此,上述2个退出操作并不会让服务器关机。
附:基本操作命令
pwd:(print working directory)输出当前命令行所在的工作路径
cd:(change directory)更改当前命令行所在的工作路径
- cd 路径
- 路径支持相对路径与绝对路径,需要注意Linux系统没有盘符的概念
ls:(list,列出)列出指定(默认为当前)路径下的文档结构
- ls 选项 路径
- 选项:
- -a:列出所有(包含隐藏文档)的文档
- -l:(list,列表)以列表的形式列出详细信息
- 选项可以合在一起写(仅支持单个字母的选项),让多个选项公用一个“-”
mkdir:(make directory)创建文件夹
- mkdir 文件夹路径
- 选项:
- -p:(parent)在创建文件夹的时候连同其父级文件夹一起创建
touch:创建普通文件
- touch 文件路径
- 路径要求目录必须存在(touch没有mkdir类似的流氓
-p
选项)
cp:(copy)复制文档
- cp 选项 需要复制的文档路径 复制到的位置
- 选项:
- -r:递归(如果是复制的是文件夹,则一定要递归)
mv:(move)移动/剪切&重命名文档
- mv 需要操作的文档路径 保存的文档路径
rm:(remove)删除文档
- rm 选项 文档路径
- 选项:
- -r:递归
- -f:强制(不提示是否删除,静默模式)
②本地⇋服务器
的文件传输
文件传输可以借助可视化的辅助工具,如:FileZilla
2.3、项目运行环境部署
后续操作会用到不少相对路径,为了保证大家的操作正确,此处统一先切换当前工作路径:
cd /usr/local/src # 该地址是已经存在的,不需要自己创建
①安装mongoDB
下载地址:Download MongoDB Community Server | MongoDB
可以选择Copy Link
随后去服务器中对应的目录执行命令下载:
wget https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-rhel70-4.4.4.tgz
# 或
curl -O https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-rhel70-4.4.4.tgz
也可以直接用今日docs
目录下已经下载好的压缩包,使用FileZilla进行上传到服务器的/usr/local/src
目录。
.tgz
格式文件是压缩包文件格式的一种,需要使用其中的内容得先解压,解压命令为:
tar -zxvf mongodb-linux-x86_64-rhel70-4.4.4.tgz
# 或
tar -xvf mongodb-linux-x86_64-rhel70-4.4.4.tgz# -z:表示指定解压缩所使用的方式,表示使用gz格式进行解压
# 如果不指定使用什么方式解压,则tar会自己判断
解压后会得到mongoDB的解压目录:
Linux系统下对于第三方软件的安装一般存放在/usr/local
下,此处建议将解压后得到的目录中的bin
目录进行转移,转移的同时需要创建mongoDB的数据文件夹和日志文件夹,命令如下:
mkdir -p /usr/local/mongodb/data
mkdir /usr/local/mongodb/log
cp -r /usr/local/src/mongodb-linux-x86_64-rhel70-4.4.4/bin /usr/local/mongodb/
# 建立mongodb需要使用的日志文件
touch /usr/local/mongodb/log/logfile
上述指令执行完毕后可以通过ls
进行列出检查,查看是否有以下文档结构:
ls /usr/local/mongodb/
随后,就可以通过以下命令去启动mongoDB了:
需要注意,此种方式的mongoDB为绿色软件,默认不会开机自动启动,不再需要使用的时候直接删除
/usr/local/mongodb
目录即可卸载软件。
/usr/local/mongodb/bin/mongod --dbpath=/usr/local/mongodb/data --logpath=/usr/local/mongodb/log/logfile --bind_ip=127.0.0.1 --fork
# --dbpath:指定数据库文件夹位置
# --logpath:指定日志文件位置
# --bind_ip:绑定监听的网卡ip地址
# --fork:以后台服务的形式运行
注意:
logpath
配置项的值一定是一个文件(可以不存在),不能是文件夹。
至此,mongoDB已经可以使用了,可以通过运行mongoDB连接工具进行测试,如果有以下输出则一切正常:
此时可以在其中创建好maizuo
数据库,以及往库中写入users
表中的数据了。
②安装nodejs
文档地址:https://github.com/nodesource/distributions/blob/master/README.md
复制好对应的指令后在终端中去执行(这个命令会在我们服务器上安装一个nodejs的镜像源以告诉包管理工具去哪里下载nodejs):
curl -sL https://rpm.nodesource.com/setup_14.x | bash -
随后运行以下命令安装nodejs:
yum类似于npm的感觉,是用于管理centos下的软件包的。
sudo yum install -y nodejs
使用
sudo
开头的命令可能会提示让输入密码,如果有则输入当前用户的密码即可。
安装好nodejs后,可以通过命令测试是否安装成功nodejs:
node -v
最后,可以继续安装一些可选的全局包以方便后面使用:
# 安装nrm,并切换npm镜像源为淘宝
npm i -g nrm
nrm use taobao# 安装nodemon
npm i -g nodemon# 安装pm2(让node在后端运行的工具,这样可以在配置完毕之后关闭终端窗口)
npm i -g pm2
到此,nodejs环境安装完毕!
上传node服务端的代码到远程服务器,位置可以随意(因为代码是node运行的,不是nginx):
接下来进入node代码的目录/usr/local/src/http
,运行安装所需模块的指令:
npm install
此时即便运行了node服务器,也会出现无法访问的情况,需进入阿里云的控制台添加允许3000端口通过。(针对专有网络只有“入方向”需要配置,针对经典网络只有“公网入方向”需要配置)
最后,让node在后台执行http.js文件(根据需要换成自己的文件名),此处需要用到前面安装的pm2工具:
# 先进入项目目录
pm2 start index.js## 重启
pm2 restart index.js
## 停止
pm2 stop index.js
如果成功,则会看到如下效果:
③安装nginx
Nginx是一款轻量级服务器/反向代理服务器及电子邮件(IMAP/POP3)代理服务器,其特点是占有内存少,并发能力强,事实上nginx的并发能力在同类型的网页服务器中表现较好,中国大陆使用nginx网站用户有:百度、京东、新浪、网易、腾讯、淘宝等。
软件官网:nginx news
傻瓜式包管理工具安装方式说明参考地址:nginx: Linux packages
按照上述提示,在服务器上指定的位置/etc/yum.repos.d/nginx.repo
新建一个文件,文件内容如下:
[nginx-stable]
name=nginx stable repo
baseurl=http://nginx.org/packages/centos/$releasever/$basearch/
gpgcheck=1
enabled=1
gpgkey=https://nginx.org/keys/nginx_signing.key
module_hotfixes=true[nginx-mainline]
name=nginx mainline repo
baseurl=http://nginx.org/packages/mainline/centos/$releasever/$basearch/
gpgcheck=1
enabled=0
gpgkey=https://nginx.org/keys/nginx_signing.key
module_hotfixes=true
对于文件的创建和修改,可以考虑咋windows上进行,写完毕之后再通过文件传输工具,将文件上传到指定的位置即可。
随后运行nginx的安装命令:
sudo yum install -y nginx
在nginx完成安装后,可以通过以下几个命令来管理nginx服务:
# 启动nginx
systemctl start nginx# 停止nginx
systemctl stop nginx# 重启nginx
systemctl restart nginx# 设置nginx开机自启动
systemctl enable nginx# 设置nginx开机不自启动
systemctl disable nginx
接下来启动nginx:
systemctl start nginx
请注意,后续每次修改了nginx的配置文件都需要对nginx服务进行重启,否则新的配置不会生效。
nginx相关的目录位置:
- 配置文件
- 主配置文件nginx.conf:/etc/nginx/nginx.conf
- 从配置文件‘*.conf’:可以是任意位置,以主配置文件声明为准,比较常用针对站点的从配置文件在
/etc/nginx/conf.d/
目录下- 默认站点目录
- /usr/share/nginx/html(等于PHPstudy中的WWW目录,回头代码得放到这个里面去)
④域名解析(如果有域名的话)
如果是大陆服务器使用,则域名一定要通过了ICP备案才可以。
以阿里云为例,先进入域名控制台,在需要使用的域名后面点击解析
按钮进入解析页面,随后点击添加记录
按钮并按照自身需求填写解析信息:
设置完成后一般1分钟内即可生效,可以在本机windows
上通过ping
命令进行测试:
# 以刚才设置的域名为例
ping sh2008.lynnn.cn
⑤项目代码部署
a. 修改项目中的所有请求地址,将其都改成线上模式的地址,随后打包。将打包好的vue代码上传到Nginx默认的站点下,目录地址为/usr/share/nginx/html
vue项目打包命令:
npm run build
b. 解决nginx下,vue路由模式history
失效的问题
方案1:不使用history
模式的路由
不使用
istory
模式,则得用hash模式,该模式下地址栏会有#
方案2:配置nginx,让nginx支持history
模式的路由
try_files $uri $uri/ /index.html;
将上述的代码放到/etc/nginx/conf.d/default.conf
中
location / {root /usr/share/nginx/html;index index.html index.htm;# 以下是新增的一行try_files $uri $uri/ /index.html;
}
随后重启nginx:
systemctl restart nginx
Q.E.D.
基于Vue结合Vant组件库的仿电影APP相关推荐
- 如何开发一个基于 Vue 的 ui 组件库
如何开发一个基于 Vue 的 ui 组件库 开发模式 预览 demo 在开发一个 ui 组件库时,肯定需要一边预览 demo,一边修改代码. 常见的解决方案是像开发一般项目一样使用 webpack-d ...
- vue 字典_【开源】基于Vue的前端组件库HeyUI
说道vue组件库,目前主流的基本就是iview和element.今天又发现一个很不错的.HeyUI. 组件也很丰富,入门比较简单. 反正开源框架我们有不嫌多,多多益善啊.感兴趣的可以看看. 关于Hey ...
- npm 编译打包vue_从零到一教你基于vue开发一个组件库
前言 Vue是一套用于构建用户界面的渐进式框架,目前有越来越多的开发者在学习和使用.在笔者写完 徐小夕:如何从0到1教你搭建前端团队的组件系统zhuanlan.zhihu.com 之后很多朋友希望了 ...
- cli3解决 ie11语法错误 vue_从零到一教你基于vue开发一个组件库高性能前端架构解决方案...
Vue是一套用于构建用户界面的渐进式框架,目前有越来越多的开发者在学习和使用.虽然笔者有近2年没有从事vue的开发了,但平时一直在关注vue的更新和发展,笔者一直认为技术团队的组件化之路重点在于基础架 ...
- 基于Vue和Element-ui组件库搭建的后台管理系统
1. 电商管理后台 API 接口文档 说明 前端:https://gitee.com/Cola163/system 后台: https://gitee.com/Cola163/system-serve ...
- vant组件做表格_有赞Vant组件库上线墨刀!以后轻松做出电商原型
继上周新上线了简历模板之后,本周墨刀的原型模板库又欢喜地增添一名新成员! 有赞Vant组件库 (做电商的宝宝要捂嘴笑了) Vant 组件库是有赞前端团队开源的一套基于Vue的UI组件库,目前版本收录了 ...
- 墨刀联合有赞Vant组件库,让你轻松设计出电商原型
继上周新上线了简历模板之后,本周墨刀的原型模板库又欢喜地增添一名新成员! 有赞Vant组件库 (做电商的宝宝要捂嘴笑了) Vant 组件库是有赞前端团队开源的一套基于Vue的UI组件库,目前版本收录了 ...
- 【Vue知识点- No8.】网易云音乐案例(vant组件库的使用)
No8.网易云音乐案例 知识点自测 知道reset.css和flexible.js的作用. 什么是组件库-例如bootstrap的作用. yarn命令的使用. 组件名字用name属性方式注册. 如何自 ...
- 【Vue知识点- No7.】路由、vant组件库的使用
No7.路由.vant组件库的使用 学习目标 1.能够了解单页面应用概念和优缺点 2.能够掌握vue-router路由系统使用 3.能够掌握链接导航和编程式导航用法 4.能够掌握路由嵌套和路由守卫 5 ...
最新文章
- 详解pytorch中的常见的Tensor数据类型以及类型转换
- pringboot 单元测试 空指针_单元测试中的 FIRST 原则
- SQL Server之存储过程基础知识
- C#中的委托,匿名方法和Lambda表达式
- TIM怎么显示每条信息的时间
- 收藏 | 一文读懂机器学习中的正则化
- 简单的控制台五子小游戏棋程序(Java)
- Glib2之无法添加符号: DSO missing from command line(十九)
- Python 遗传算法 Genetic Algorithm
- springboot webService调用
- 进程间的通信方式有哪些?
- CAD中打开CAD图纸看不到内容怎么办?
- 【51单片机】 火焰传感器用法及代码
- 操作系统的主要功能(3)
- 说话人识别的特征选取
- 云原生|kubernetes|kubeadm部署的集群的100年证书
- 【第66篇】行人属性识别研究综述(一)
- HBuilder X 初体验
- MATLAB LSB图像信息隐藏 最低位平面验证 以及PSNR SSIM评价
- HTML报错 Malformed markup: Attribute “xxx“ appears more than once in element
热门文章
- 打包成apk,生成apk文件,上传到网站服务器提供链接下载
- 质量管理(新旧)七种工具
- 45个有用的JavaScript技巧,值得你学习
- PowerBuilder常用函数功能和用法解析
- Android使用百度地图定位并显示手机位置后使用前置摄像头“偷拍”
- 计算机教育部第四次学科评估结果,【重磅!】全国高校第四轮学科评估结果出炉(附完整名单)...
- 华硕ezflash3找不到u盘_华硕主板如何通过ASUS EZ Flash 3更新BIOS?
- hw11————玩转 Docker 容器技术
- centos8 yum 安装mysql8
- Java集合面试总结