基于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、电影详情
  • 四、Vuex(重点)
    • 1、vuex是什么?
    • 2、vuex的安装及配置
    • 3、vuex核心(重点)
    • 4、模块化(重点)
    • 5、改写eventBus案例
  • 五、功能实现(2)
    • 1、NodeJS接口实现
      • 1.1、接口安全
      • 1.2、用户登录接口
      • 1.3、获取用户信息接口
    • 2、登录操作
    • 3、获取用户信息
    • 4、防止
  • 六、项目上线流程(记忆)
    • 1、项目上线的要素
    • 2、Vue项目上线发布
      • 2.1、购买云服务器
      • 2.2、云服务器操作基础
      • 2.3、项目运行环境部署

一、项目概要

1、效果前瞻

仿照网站:卖座网

卖座电影

基于Vue结合Vant组件库的仿电影APP_第1张图片

说明:前端:moive: 基于Vue结合Vant组件库的仿电影APP
后台:moive-server: 基于Node.js电影项目moive的后台

2、开发流程

熟悉一个项目从0-1流程?

  • 产品立项 (需求分析、技术选型、项目人员确定),产出立项报告
    • 项目立项报告(百度文库搜)【产品经理,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/

  • 创建一个新项目

基于Vue结合Vant组件库的仿电影APP_第2张图片

  • 仓库配置

仓库名称自行决定。

基于Vue结合Vant组件库的仿电影APP_第3张图片

以gitee为例:Gitee - 企业级 DevOps 研发效能平台

  • 在主页的右上角选择“+”号,展开后,选择“新建仓库”

基于Vue结合Vant组件库的仿电影APP_第4张图片

  • 填写仓库基本的信息,只需要填写仓库名称即可

基于Vue结合Vant组件库的仿电影APP_第5张图片

  • 创建好的仓库界面

基于Vue结合Vant组件库的仿电影APP_第6张图片

2、创建项目

  • 使用vue-cli脚手架创建项目
vue create i-moive

项目名称i-moive可以根据自己的情况进行替换

  • 脚手架创建项目询问式选择回答如下

基于Vue结合Vant组件库的仿电影APP_第7张图片

基于Vue结合Vant组件库的仿电影APP_第8张图片

基于Vue结合Vant组件库的仿电影APP_第9张图片

基于Vue结合Vant组件库的仿电影APP_第10张图片

使用历史路由模式,在上线部署的时候需要做服务端的重写配置,否则项目不支持刷新,一刷新就404。

基于Vue结合Vant组件库的仿电影APP_第11张图片

基于Vue结合Vant组件库的仿电影APP_第12张图片

其实vue也支持界面化的方式管理项目(与vue create命令二选一):

vue ui

基于Vue结合Vant组件库的仿电影APP_第13张图片

  • 项目创建完毕

基于Vue结合Vant组件库的仿电影APP_第14张图片

  • 同步初始化项目到远程仓库
# 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.vuesrc/views/About.vue文件
  • 修改src/router/index.js,删除对Home.vueAbout.vue的引用
    • 删除Home.vuede 的引入
    • 删除根路由规则
    • 删除/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
        • 响应规范
          • 状态码
          • 文本信息
          • 响应体
          • ...
  • 路由守卫
    • 作用:防止用户

————————————————————————————————————————————

如果项目中所有的路由都写在入口文件中,那么将不便于编写项目和后期维护。因此路由需要进行模块化处理。

可以先行添加以下几个空的路由模块(先不拆,实现效果之后确保没有问题,则再进行拆分):

  • 电影模块
    • 正在热映(嵌套路由)
      • /films/nowPlaying
    • 即将上映(嵌套路由)
      • /films/comingSoon
    • 电影详情
      • /film/10000
  • 影院模块
    • /cinemas
  • 个人中心模块
    • 我的:/center
    • 登录:/login
  • 城市列表
    • /city

如果后续还有其他模块,届时再进行增加即可。

创建各个模块对应的视图组件文件

  • src/views目录下创建对应的文件夹与文件,同时,可以删除自带的Home.vueAbout.vue文件

  • 创建每个视图组件后在其中书写好基本内容

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/login
                pathRewrite: {
                    "^/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换成ret
        return 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");
  • 测试可用性

    • 测试代码测试完毕之后需要删除

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内置的标签名




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




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组件





b. 完成Header.vue的代码编写





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有一种奇怪的用法,代码如下:


    

在Card组件中,有一些属性例如“desc”,可能由于内容比较多或者其它原因需要使用样式(自定义),则以属性形式不好操作;vant支持我们使用“插槽”在template中去自定义展示,例如上述代码的“#tags”插槽,其实展示的内容就是Card组件的tags属性中的内容。以此类推,如果需要自定义“desc”属性,则可以这么写:

在用插槽的形式之后,要删除属性的写法。

目标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%;
}
  • 在合适的时候(特定的高度)去应用布局的样式(动态样式绑定)

目标3:实现列表的加载更多

请注意,在使用List组件的时候,务必要留意,需要使用list组件包裹我们已经实现的Card组件,不要连同官网手册中Cell组件一起复制。

==完整参考代码==





2.2、即将上映列表

作业

  • 按钮提示文字修改
  • 上映时间为premiereAt,该字段是时间戳,单位是秒,注意转化

2.3、电影详情

涉及需要修改的组件:

  • src/views/Films/Detail.vue
  • src/views/Films/Films.vue

实现步骤

a. 需要在列表上给每个电影的条目添加点击事件,点完之后需要携带电影的id号去详情页面(动态路由参数);

b. 详情页面需要获取电影id号,然后根据电影的id号去查询获取电影的信息并且展示,细节有:

  • 图标播放区域,应该是用什么插件
  • 在进入详情页面之后,底部导航消失了。在离开详情页面之后底部导航又出现了

具体实现

a. 需要在列表上给每个电影的条目添加点击事件,点完之后需要携带电影的id号去详情页面(动态路由参数);

给Card组件添加点击事件:

添加事件处理程序:

// 事件处理程序:去详情页
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包,这里时间戳处理单位为毫秒

视图的展示:


返回
{{ filmInfo.name }}
{{ filmInfo.name }}
{{ filmInfo.grade }}
{{ filmInfo.category }}
{{ filmInfo.premiereAt | parseTime }}上映
{{filmInfo.nation}} | {{filmInfo.runtime}}分钟
{{filmInfo.synopsis}}
演职人员

逻辑代码中针对时间戳的格式化与返回上一页的代码:

// 导入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(() => {
   // 写需要等待异步结束之后再去做的操作 
});

视图部分的结构:

演职人员
{{ item.name }}
{{ item.role }}

逻辑部分:

// 导入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(组件通信)【事件中心的这种数据共享方式与子传父的思想是一样的,都是通过事件/事件监听来实现的,所以都有关键词emiton

基于Vue结合Vant组件库的仿电影APP_第15张图片

  • 指令:v-show

将事件中心建立在vue的原型上(修改的是main.js文件)

// 将事件中心建立好之后放到vue原型上,避免后续再使用的时候频繁去new Vue()
Vue.prototype.$eventBus = new Vue();

在App.vue中监听自定义事件toggleFooter





在Detail.vue中去通知显示和隐藏

// 获取数据
created() {
    // 通知事件中心隐藏底部导航
    this.$eventBus.$emit("toggleFooter", false);

    //....
},
beforeDestroy() {
    // 在将要离开该组件的时候通知事件中心将底部导航放出来
    this.$eventBus.$emit("toggleFooter", true);
},

四、Vuex(重点)

1、vuex是什么?

vuex是一种项目中数据共享的方式。

基于Vue结合Vant组件库的仿电影APP_第16张图片

其具有以下优势:

  • 能够在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("空间名", ["属性名",....]),

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.isShow

export 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对象的写法


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值,并返回数据

基于Vue结合Vant组件库的仿电影APP_第17张图片

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(就是一个字符串,自己定义,值是什么无所谓)

例如:

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,
}

将模拟好的数据,写入到数据库中,以便后面做登录操作:

基于Vue结合Vant组件库的仿电影APP_第18张图片

⑤配置jsonwebtoken模块需要用的secret,并在代码中读取供后续使用

在node项目目录中创建一个.env(Linux以.开头都为隐藏文件)并在此文件中写入jwt加密所需要的秘钥。同时,.env文件不要上传到Github上(.gitignore文件中声明忽略)。

基于Vue结合Vant组件库的仿电影APP_第19张图片

在代码中读取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(" ");
        // 最后一个元素即token
        let 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 {
            // 没过期
            // 判断是否马上要过期了,如果是自动给它生成新的token
            if (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、登录操作

①先给登录按钮绑定点击事件,点击之后去往登录页面

立即登录

事件处理程序

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;
            }
        });
    }
},

③在视图中展示数据

基于Vue结合Vant组件库的仿电影APP_第20张图片

针对是否登录的测试,通过浏览器的无痕模式(Ctrl+Shift+N),在该模式下,不会与普通模式共享任何会话信息。

基于Vue结合Vant组件库的仿电影APP_第21张图片

4、防止

知识点:路由守卫

含义:防止用户绕过登录页面,通过直接在地址栏中输入地址去访问原本需要登录才能访问的页面。

①例如:给个人中心的“余额”按钮绑定点击事件,点击之后去往余额组件

事件处理程序:

methods: {
    // 去账户余额页面
    goAccount() {
        this.$router.push("/account");
    },
},

②在无痕模式下直接访问余额地址也可以访问到页面内容(不合理)

基于Vue结合Vant组件库的仿电影APP_第22张图片

在实际开发的时候对于操作,前后端应该统一战线,与“行为”不共戴天。也就是说不管做前端也好,还是做后端也罢,都需要解决用户的行为。只不过以前只要后端做就可以了,现在前后端都要做。先触发前端的防;如果前端拦不住,后端再上。

③使用路由守卫

分为全局守卫和局部守卫。

修改router/index.js文件,添加全局的前置路由守卫

router.beforeEach((to, from, next) => {
    // 需要定义一组数据(路由),这组路由可能是需要登录才能访问的(当然也可能是不需要登录就能访问的),当我们获取到目标路由的时候,可以去数组中进行判断,符合特定条件就继续,否则不允许访问。
    // 例如:下面的数组需要登录才能访问
    let needLogin = ["/account", "/order", "/settings"];
    if (needLogin.includes(to.path)) {
        // 判断是否有token
        let 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、购买云服务器

基于Vue结合Vant组件库的仿电影APP_第23张图片

基于Vue结合Vant组件库的仿电影APP_第24张图片

设置安全组/防火墙:

基于Vue结合Vant组件库的仿电影APP_第25张图片

基于Vue结合Vant组件库的仿电影APP_第26张图片

2.2、云服务器操作基础

①使用cmder等终端工具连接远程服务器

ssh root@服务器公网IP地址

在首次连接时会询问是否连接,输入yes按下回车。随后输入密码,在输入密码的时候没有任何提示,确认正确输入后按下回车。

基于Vue结合Vant组件库的仿电影APP_第27张图片

退出的方式有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

基于Vue结合Vant组件库的仿电影APP_第28张图片

2.3、项目运行环境部署

后续操作会用到不少相对路径,为了保证大家的操作正确,此处统一先切换当前工作路径:

cd /usr/local/src
# 该地址是已经存在的,不需要自己创建

①安装mongoDB

下载地址:Download MongoDB Community Server | MongoDB

基于Vue结合Vant组件库的仿电影APP_第29张图片

可以选择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目录。

基于Vue结合Vant组件库的仿电影APP_第30张图片

.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的解压目录:

基于Vue结合Vant组件库的仿电影APP_第31张图片

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/

基于Vue结合Vant组件库的仿电影APP_第32张图片

随后,就可以通过以下命令去启动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连接工具进行测试,如果有以下输出则一切正常:

基于Vue结合Vant组件库的仿电影APP_第33张图片

此时可以在其中创建好maizuo数据库,以及往库中写入users表中的数据了。

基于Vue结合Vant组件库的仿电影APP_第34张图片

②安装nodejs

文档地址:https://github.com/nodesource/distributions/blob/master/README.md

基于Vue结合Vant组件库的仿电影APP_第35张图片

复制好对应的指令后在终端中去执行(这个命令会在我们服务器上安装一个nodejs的镜像源以告诉包管理工具去哪里下载nodejs):

curl -sL https://rpm.nodesource.com/setup_14.x | bash -

随后运行以下命令安装nodejs:

yum类似于npm的感觉,是用于管理centos下的软件包的。

sudo yum install -y nodejs

使用sudo开头的命令可能会提示让输入密码,如果有则输入当前用户的密码即可。

基于Vue结合Vant组件库的仿电影APP_第36张图片

安装好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):

基于Vue结合Vant组件库的仿电影APP_第37张图片

接下来进入node代码的目录/usr/local/src/http,运行安装所需模块的指令:

npm install

此时即便运行了node服务器,也会出现无法访问的情况,需进入阿里云的控制台添加允许3000端口通过。(针对专有网络只有“入方向”需要配置,针对经典网络只有“公网入方向”需要配置)

基于Vue结合Vant组件库的仿电影APP_第38张图片

最后,让node在后台执行http.js文件(根据需要换成自己的文件名),此处需要用到前面安装的pm2工具:

# 先进入项目目录
pm2 start index.js

## 重启
pm2 restart index.js
## 停止
pm2 stop index.js

如果成功,则会看到如下效果:

基于Vue结合Vant组件库的仿电影APP_第39张图片

③安装nginx

Nginx是一款轻量级服务器/反向代理服务器及电子邮件(IMAP/POP3)代理服务器,其特点是占有内存少,并发能力强,事实上nginx的并发能力在同类型的网页服务器中表现较好,中国大陆使用nginx网站用户有:百度、京东、新浪、网易、腾讯、淘宝等。

软件官网:nginx news

傻瓜式包管理工具安装方式说明参考地址:nginx: Linux packages

基于Vue结合Vant组件库的仿电影APP_第40张图片

按照上述提示,在服务器上指定的位置/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上进行,写完毕之后再通过文件传输工具,将文件上传到指定的位置即可。

基于Vue结合Vant组件库的仿电影APP_第41张图片

随后运行nginx的安装命令:

sudo yum install -y nginx

基于Vue结合Vant组件库的仿电影APP_第42张图片

在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备案才可以。

以阿里云为例,先进入域名控制台,在需要使用的域名后面点击解析按钮进入解析页面,随后点击添加记录按钮并按照自身需求填写解析信息:

基于Vue结合Vant组件库的仿电影APP_第43张图片

设置完成后一般1分钟内即可生效,可以在本机windows上通过ping命令进行测试:

# 以刚才设置的域名为例
ping sh2008.lynnn.cn

基于Vue结合Vant组件库的仿电影APP_第44张图片

⑤项目代码部署

a. 修改项目中的所有请求地址,将其都改成线上模式的地址,随后打包。将打包好的vue代码上传到Nginx默认的站点下,目录地址为/usr/share/nginx/html

vue项目打包命令:

npm run build

基于Vue结合Vant组件库的仿电影APP_第45张图片

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.js,javascript,html,css,ajax,npm)