第1章 前端环境搭建

学习目标:

  • 了解十次方需求、技术架构,理解前后端分离开发模式
  • 掌握Node.js基本使用方法,理解模块化编程
  • 掌握包资源管理器NPM的使用
  • 说出webpack的作用
  • 掌握vs code开发工具的基本使用方法
  • 掌握ES6常用的新特性语法

1 十次方需求分析与技术架构

1.1 十次方是个什么样的网站

《十次方》是程序员的专属社交平台,包括头条、问答、活动、交友、吐槽、招聘六大频道。

1、头条

文章头条、分类查看

2、问答

分标签展示、问题提问、问题回答、问题所属标签【瀑布流分页】

3、活动

活动大会信息、大会报名【瀑布流分页】

4、交友栏

根据性别展示推荐好友信息、喜欢、不喜欢、聊天操作

5、吐槽

无标签吐槽、评论

6、招聘

展示招聘信息、推荐/最新职位、查看招聘详情

7、其他功能

用户中心(个人发表的信息、浏览记录、用户设置等)

image-20220928043239890 

十次方名称的由来:2的10次方为1024,程序员都懂的。

如果你是一位技术大咖,那么赶快发布文章,增加知名度吧。

如果你是一名技术小白,那么赶快到问答频道寻求帮助的,这里高手如云哦!

如果你不想错过各种技术交流会,那么请经常关注活动频道吧~

如果你还是单身,那么赶快到交友频道找到你心仪的另一半。

如果你有太多的苦恼,那么赶快吐个槽吧~

如果你正在找工作或是想跳槽拿高薪,那么来招聘频道淘金吧~

1.2 理解前后端分离开发

​ 前后端分离已成为互联网项目开发的业界标准使用方式,通过nginx+tomcat的方式(也可以中间加一个nodejs)有效的进行解耦,并且前后端分离会为以后的大型分布式架构、弹性计算架构、微服务架构、多端化服务(多种客户端,例如:浏览器,车载终端,安卓,IOS等等)打下坚实的基础。

以前老的方式是:

1.产品经理/领导/客户提出需求

2.UI做出设计图

3.前端工程师做html页面

4.后端工程师将html页面套成jsp页面(前后端强依赖,后端必须要等前端的html做好才能套jsp。如果html发生变更,就更痛了,开发效率低)

5.集成出现问题

6.前端返工

7.后端返工

8.二次集成

9.集成成功

10.交付

新的方式是:

1.产品经理/领导/客户提出需求

2.UI做出设计图

3.前后端约定接口&数据&参数

4.前后端并行开发(无强依赖,可前后端并行开发,如果需求变更,只要接口&参数不变,就不用两边都修改代码,开发效率高)

5.前后端集成

6.前端页面调整

7.集成成功

8.交付

image-20231003233536729

1.3 前端技术架构

image-20220928043318213

架构描述:以Node.js为核心的Vue.js前端技术生态架构

  • npm:包资源管理工具,类似于maven

  • node.js:运行在服务端的 JavaScript

  • webpack:前端的组件打包编译工具

  • VScode:前端开发中使用的IDE

  • ES6:前端的常见语法规范

  • Vue.js:

    • Elment UI:现成的组件库,后台使用的技术
  • NUXT:前台使用的技术

  • Mock.js/easy Mock:生成模拟数据

2 Node.js

2.1 什么是Node.js

简单的说 Node.js 就是运行在服务端的 JavaScript。

Node.js 是一个基于Chrome JavaScript 运行时建立的一个平台。

Node.js是一个事件驱动I/O服务端JavaScript环境,基于Google的V8引擎,V8引擎执行Javascript的速度非常快,性能非常好。

2.2 Node.js安装

1、下载对应你系统的Node.js版本:

https://nodejs.org/en/download/

(我们现在使用的版本是8.9.4,资源中也已提供)
2、选安装目录进行安装

默认即可

3.测试

在命令提示符下输入命令,会显示当前node的版本

1
node -v

2.3 快速入门

2.3.1 控制台输出

我们现在做个最简单的小例子,演示如何在控制台输出,创建文本文件demo1.js,代码内容

1
2
3
var a=1;
var b=2;
console.log(a+b);

我们在命令提示符下输入命令

1
node demo1.js

image-20220928044353037

可以看到输出结果,这就是前后端分离,不需要浏览器的支持

2.3.2 使用函数

创建文本文件demo2.js

1
2
3
4
5
var c=add(100,200);
console.log(c);
function add(a,b){
return a+b;
}

命令提示符输入命令

1
node demo2.js

运行后看到输出结果为300

2.3.3 模块化编程

创建文本文件demo3_1.js

1
2
3
exports.add=function(a,b){
return a+b;
}

创建文本文件demo3_2.js

1
2
var demo= require('./demo3_1');
console.log(demo.add(400,600));

我们在命令提示符下输入命令

1
node demo3_2.js

结果为1000

image-20220928044654377

2.3.4 创建web服务器

创建文本文件demo4.js

1
2
3
4
5
6
7
8
9
10
11
var http = require('http');
http.createServer(function (request, response) {
// 发送 HTTP 头部
// HTTP 状态值: 200 : OK
// 内容类型: text/plain
response.writeHead(200, {'Content-Type': 'text/plain'});
// 发送响应数据 "Hello World"
response.end('Hello World\n');
}).listen(8888);
// 终端打印如下信息
console.log('Server running at http://127.0.0.1:8888/');

http为node内置的web模块

我们在命令提示符下输入命令

1
node demo4.js

服务启动后,我们打开浏览器,输入网址

http://localhost:8888/

即可看到网页输出结果Hello World

心情是不是很激动呢?Ctrl+c 终止运行。

2.3.5 理解服务端渲染

我们创建demo5.js  ,将上边的例子写成循环的形式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var http = require('http');
http.createServer(function (request, response) {
// 发送 HTTP 头部
// HTTP 状态值: 200 : OK
// 内容类型: text/plain
response.writeHead(200, {'Content-Type': 'text/plain'});
// 发送响应数据 "Hello World"
for(var i=0;i<10;i++){
response.write('Hello World\n');
}
response.end('');
}).listen(8888);
// 终端打印如下信息
console.log('Server running at http://127.0.0.1:8888/');

我们在命令提示符下输入命令启动服务

1
node demo5.js

浏览器地址栏输入http://127.0.0.1:8888即可看到查询结果。

我们右键“查看源代码”发现,并没有我们写的for循环语句,而是直接的10条Hello World ,这就说明这个循环是在服务端完成的,而非浏览器(客户端)来完成。这与我们原来的JSP很是相似。

2.3.6 接收参数

创建demo6.js

1
2
3
4
5
6
7
8
9
10
11
var http = require('http');
var url = require('url');
http.createServer(function(request, response){
response.writeHead(200, {'Content-Type': 'text/plain'});
// 解析 url 参数
var params = url.parse(request.url, true).query;
response.write("name:" + params.name);
response.write("\n");
response.end();
}).listen(8888);
console.log('Server running at http://127.0.0.1:8888/');

我们在命令提示符下输入命令

1
node demo6.js

在浏览器测试结果

3 包资源管理器NPM

3.1 什么是NPM

npm全称Node Package Manager,他是node包管理和分发工具。其实我们可以把NPM理解为前端的Maven .

我们通过npm 可以很方便地下载js库,管理前端工程.

最近版本的node.js已经集成了npm工具,在命令提示符输入 npm -v 可查看当前npm版本

3.2 NPM命令

3.2.1 初始化工程

init命令是工程初始化命令。

建立一个空文件夹,在命令提示符进入该文件夹  执行命令初始化

1
npm init

按照提示输入相关信息,如果是用默认值则直接回车即可。

name: 项目名称

version: 项目版本号

description: 项目描述

keywords: {Array}关键词,便于用户搜索到我们的项目

最后会生成package.json文件,这个是包的配置文件,相当于maven的pom.xml

我们之后也可以根据需要进行修改。

3.2.2 本地安装

install命令用于安装某个模块,如果我们想安装express模块(node的web框架),输出命令如下:

1
npm install express

出现黄色的是警告信息,可以忽略,请放心,你已经成功执行了该命令。

在该目录下已经出现了一个node_modules文件夹 和package-lock.json

node_modules文件夹用于存放下载的js库(相当于maven的本地仓库)

package-lock.json是当 node_modules 或 package.json 发生变化时自动生成的文件。这个文件主要功能是确定当前安装的包的依赖,以便后续重新安装的时候生成相同的依赖,而忽略项目开发过程中有些依赖已经发生的更新。

我们再打开package.json文件,发现刚才下载的express已经添加到依赖列表中了.

关于版本号定义:

1
2
3
4
5
6
7
指定版本:比如1.2.2,遵循“大版本.次要版本.小版本”的格式规定,安装时只安装指定版本。

波浪号(tilde)+指定版本:比如~1.2.2,表示安装1.2.x的最新版本(不低于1.2.2),但是不安装1.3.x,也就是说安装时不改变大版本号和次要版本号。

插入号(caret)+指定版本:比如ˆ1.2.2,表示安装1.x.x的最新版本(不低于1.2.2),但是不安装2.x.x,也就是说安装时不改变大版本号。需要注意的是,如果大版本号为0,则插入号的行为与波浪号相同,这是因为此时处于开发阶段,即使是次要版本号变动,也可能带来程序的不兼容。

latest:安装最新版本。

3.2.3 全局安装

刚才我们使用的是本地安装,会将js库安装在当前目录,而使用全局安装会将库安装到你的全局目录下。

如果你不知道你的全局目录在哪里,执行命令

1
npm root -g

我的全局目录在

C:\Users\Administrator\AppData\Roaming\npm\node_modules

比如我们全局安装jquery, 输入以下命令

1
npm install jquery -g

3.2.4 批量下载

我们从网上下载某些代码,发现只有package.json,没有node_modules文件夹,这时我们需要通过命令重新下载这些js库.

进入目录(package.json所在的目录)输入命令

1
npm install

此时,npm会自动下载package.json中依赖的js库.

3.2.5淘宝NPM镜像

有时我们使用npm下载资源会很慢,所以我们可以安装一个cnmp(淘宝镜像)来加快下载速度。

输入命令,进行全局安装淘宝镜像。

1
npm install -g cnpm --registry=https://registry.npm.taobao.org

安装后,我们可以使用以下命令来查看cnpm的版本

1
cnpm -v

使用cnpm

1
cnpm install 需要下载的js库

3.2.6 运行工程

如果我们想运行某个工程,则使用run命令

如果package.json中定义的脚本如下

dev是开发阶段测试运行

build是构建编译工程

lint 是运行js代码检测

我们现在来试一下运行dev

1
npm run dev

3.2.7 编译工程

我们接下来,测试一个代码的编译.编译后我们就可以将工程部署到nginx中啦~

编译后的代码会放在dist文件夹中,首先我们先删除dist文件夹中的文件,进入命令提示符输入命令

1
npm run build

生成后我们会发现只有个静态页面,和一个static文件夹

这种工程我们称之为单页Web应用(single page web application,SPA),就是只有一张Web页面的应用,是加载单个HTML 页面并在用户与应用程序交互时动态更新该页面的Web应用程序。

这里其实是调用了webpack来实现打包的,关于webpack我们后续的章节进行介绍

4 Webpack

4.1 什么是Webpack

​ Webpack 是一个前端资源加载/打包工具。它将根据模块的依赖关系进行静态分析,然后将这些模块按照指定的规则生成对应的静态资源。

image-20231003233553184 

​ 从图中我们可以看出,Webpack 可以将多种静态资源 js、css、less 转换成一个静态文件,减少了页面的请求。  接下来我们简单为大家介绍 Webpack 的安装与使用

4.2 Webpack安装

全局安装

1
2
npm install webpack -g
npm install webpack-cli -g

安装后查看版本号

1
webpack -v

4.3 快速入门

4.3.1 JS打包

(1)创建src文件夹,创建bar.js

1
2
3
exports.info=function(str){
document.write(str);
}

(2)src下创建logic.js

1
2
3
exports.add=function(a,b){
return a+b;
}

(3)src下创建main.js

1
2
3
var bar= require('./bar');
var logic= require('./logic');
bar.info( 'Hello world!'+ logic.add(100,200));

(4)创建配置文件webpack.config.js ,该文件与src处于同级目录

1
2
3
4
5
6
7
8
var path = require("path");
module.exports = {
entry: './src/main.js',
output: {
path: path.resolve(__dirname, './dist'),
filename: 'bundle.js'
}
};

以上代码的意思是:读取当前目录下src文件夹中的main.js(入口文件)内容,把对应的js文件打包,打包后的文件放入当前目录的dist文件夹下,打包后的js文件名为bundle.js

(5)执行编译命令

1
webpack

执行后查看bundle.js 会发现里面包含了上面两个js文件的内容

(7)创建index.html ,引用bundle.js

1
2
3
4
5
6
7
8
<!doctype html>
<html>
<head>
</head>
<body>
<script src="dist/bundle.js"></script>
</body>
</html>

测试调用index.html,会发现有内容输出

4.3.2 CSS打包

(1)安装style-loader和 css-loader

Webpack 本身只能处理 JavaScript 模块,如果要处理其他类型的文件,就需要使用 loader 进行转换。

Loader 可以理解为是模块和资源的转换器,它本身是一个函数,接受源文件作为参数,返回转换的结果。这样,我们就可以通过 require 来加载任何类型的模块或文件,比如 CoffeeScript、 JSX、 LESS 或图片。首先我们需要安装相关Loader插件,css-loader 是将 css 装载到 javascript;style-loader 是让 javascript 认识css

1
cnpm install style-loader css-loader --save-dev

(2)修改webpack.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var path = require("path");
module.exports = {
entry: './src/main.js',
output: {
path: path.resolve(__dirname, './dist'),
filename: 'bundle.js'
},
module: {
rules: [
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
}
]
}
};

(3)在src文件夹创建css文件夹,css文件夹下创建css1

1
2
3
body{
background:red;
}

(4)修改main.js ,引入css1.css

1
require('./css1.css');

(5)重新运行webpack

(6)运行index.html看看背景是不是变成红色啦?

5 开发工具VsCode

5.1 VsCode简介

​ VSCode( Visual Studio Code)是微软出的一款轻量级代码编辑器 ,重要的是它在Windows, OS X 和Linux操作系统的桌面上均可运行。Visual Studio Code内置了对JavaScript, TypeScript和Node.js语言的支持,并且为其他语言如C++, C#, Python, PHP等提供了丰富的扩展库和运行时。

5.2 VsCode安装与配置

5.2.1安装

官网下载 https://code.visualstudio.com/

默认安装即可

5.2.2插件安装

VsCode可以通过安装插件来使编辑器变的更加强大

以下为前端开发工程师常用插件

(1)HTML Snippets

超级实用且初级的 H5代码片段以及提示

(2)HTML CSS Support

让 html 标签上写class 智能提示当前项目所支持的样式 新版已经支持scss文件检索

(3)Debugger for Chrome

让 vscode 映射 chrome 的 debug功能,静态页面都可以用 vscode 来打断点调试,真666~ 

(4)vetur

vue框架所需的插件 语法高亮、智能感知、Emmet等

(5)VueHelper

snippet代码片段

5.2.3配置自动保存

点击菜单 :文件–勾选自动保存

6 ES6

6.1 什么是ES6

编程语言JavaScript是ECMAScript的实现和扩展 。ECMAScript是由ECMA(一个类似W3C的标准组织)参与进行标准化的语法规范。ECMAScript定义了:

语言语法 – 语法解析规则、关键字、语句、声明、运算符等。

类型 – 布尔型、数字、字符串、对象等。

原型和继承

内建对象和函数的标准库 – JSONMath数组方法对象自省方法等。

ECMAScript标准不定义HTML或CSS的相关功能,也不定义类似DOM(文档对象模型)的Web API,这些都在独立的标准中进行定义。ECMAScript涵盖了各种环境中JS的使用场景,无论是浏览器环境还是类似node.js的非浏览器环境。

ECMAScript标准的历史版本分别是1、2、3、5。

那么为什么没有第4版?其实,在过去确实曾计划发布提出巨量新特性的第4版,但最终却因想法太过激进而惨遭废除(这一版标准中曾经有一个极其复杂的支持泛型和类型推断的内建静态类型系统)。

ES4饱受争议,当标准委员会最终停止开发ES4时,其成员同意发布一个相对谦和的ES5版本,随后继续制定一些更具实质性的新特性。这一明确的协商协议最终命名为“Harmony”,因此,ES5规范中包含这样两句话

ECMAScript是一门充满活力的语言,并在不断进化中。

未来版本的规范中将持续进行重要的技术改进

2009年发布的改进版本ES5,引入了Object.create()Object.defineProperty()getterssetters严格模式以及JSON对象。

ECMAScript 6.0(以下简称ES6)是JavaScript语言的下一代标准,2015年6月正式发布。它的目标,是使得JavaScript语言可以用来编写复杂的大型应用程序,成为企业级开发语言。

6.2 Node.js中使用ES6

ES6+ 太棒了,但是很多高级功能node是不支持的,就需要使用babel转换成ES5

(1)babel转换配置,项目根目录添加.babelrc 文件

1
2
3
{
"presets" : ['es2015']
}

(2)安装es6转换模块

1
cnpm install babel-preset-es2015 --save-dev

(3)全局安装命令行工具

1
cnpm install babel-cli -g

(4)使用

1
babel-node js文件名

6.3 语法新特性

6.3.1 变量声明let

我们都是知道在ES6以前,var关键字声明变量。无论声明在何处,都会被视为声明在函数的最顶部(不在函数内即在全局作用域的最顶部)。这就是函数变量提升例如

1
2
3
4
5
6
7
function aa() {
if(bool) {
var test = 'hello man'
} else {
console.log(test)
}
}

以上的代码实际上是:

1
2
3
4
5
6
7
8
9
10
function aa() {
var test // 变量提升
if(bool) {
test = 'hello man'
} else {
//此处访问test 值为undefined
console.log(test)
}
//此处访问test 值为undefined
}

所以不用关心bool是否为true or false。实际上,无论如何test都会被创建声明。

接下来ES6主角登场:

我们通常用let和const来声明,let表示变量、const表示常量。let和const都是块级作用域。怎么理解这个块级作用域?在一个函数内部 ,在一个代码块内部。看以下代码

1
2
3
4
5
6
7
8
function aa() {
if(bool) {
let test = 'hello man'
} else {
//test 在此处访问不到
console.log(test)
}
}

6.3.2 常量声明

const 用于声明常量,看以下代码

1
2
const name = 'lux'
name = 'joe' //再次赋值此时会报错

6.3.3 模板字符串

es6模板字符简直是开发者的福音啊,解决了ES5在字符串功能上的痛点。

第一个用途,基本的字符串格式化。将表达式嵌入字符串中进行拼接。用${}来界定。

1
2
3
4
5
6
//es5 
var name = 'lux'
console.log('hello' + name)
//es6
const name = 'lux'
console.log(`hello ${name}`) //hello lux

第二个用途,在ES5时我们通过反斜杠()来做多行字符串或者字符串一行行拼接。ES6反引号(``)直接搞定。

1
2
3
4
5
6
7
// es5
var msg = "Hi \
man!"
// es6
const template = `<div>
<span>hello world</span>
</div>`

6.3.4 函数默认参数

ES6为参数提供了默认值。在定义函数时便初始化了这个参数,以便在参数没有被传递进去时使用。

看例子代码

1
2
3
4
5
function action(num = 200) {
console.log(num)
}
action() //200
action(300) //300

6.3.5 箭头函数

ES6很有意思的一部分就是函数的快捷写法。也就是箭头函数。

箭头函数最直观的三个特点。

1不需要function关键字来创建函数

2省略return关键字

3继承当前上下文的 this 关键字

看下面代码(ES6)

1
2
3
(response,message) => {
.......
}

相当于ES5代码

1
2
3
function(response,message){
......
}

6.3.6 对象初始化简写

ES5我们对于对象都是以键值对的形式书写,是有可能出现键值对重名的。例如

1
2
3
4
5
6
function people(name, age) {
return {
name: name,
age: age
};
}

以上代码可以简写为

1
2
3
4
5
6
function people(name, age) {
return {
name,
age
};
}

6.3.7 解构

数组和对象是JS中最常用也是最重要表示形式。为了简化提取信息,ES6新增了解构,这是将一个数据结构分解为更小的部分的过程

ES5我们提取对象中的信息形式如下

1
2
3
4
5
6
7
const people = {
name: 'lux',
age: 20
}
const name = people.name
const age = people.age
console.log(name + ' --- ' + age)

是不是觉得很熟悉,没错,在ES6之前我们就是这样获取对象信息的,一个一个获取。现在,ES6的解构能让我们从对象或者数组里取出数据存为变量,例如

1
2
3
4
5
6
7
8
9
10
11
12
//对象
const people = {
name: 'lux',
age: 20
}
const { name, age } = people
console.log(`${name} --- ${age}`)
//数组
const color = ['red', 'blue']
const [first, second] = color
console.log(first) //'red'
console.log(second) //'blue'

6.3.8 Spread Operator

ES6中另外一个好玩的特性就是Spread Operator 也是三个点儿…接下来就展示一下它的用途。 组装对象或者数组

1
2
3
4
5
6
7
8
9
//数组
const color = ['red', 'yellow']
const colorful = [...color, 'green', 'pink']
console.log(colorful) //[red, yellow, green, pink]

//对象
const alp = { fist: 'a', second: 'b'}
const alphabets = { ...alp, third: 'c' }
console.log(alphabets) //{ "fist": "a", "second": "b", "third": "c"

6.3.9 import 和 export

import导入模块、export导出模块

lib.js

1
2
3
4
let fn0=function(){
console.log('fn0...');
}
export {fn0}

demo9.js

1
2
import {fn0} from './lib'
fn0();

注意:node(v8.x)本身并不支持import关键字,所以我们需要使用babel的命令行工具来执行(配置详见6.2小节内容)

1
babel-node demo9

6.3.10 Promise (自学补充)

Promise 是异步编程的一种解决方案,比传统的解决方案-回调函数和事件– 更合理和强大。它由社区最早提出和实现,ES6将其写进了语言标准,统一了语法,原生提供了Promise.

第2章 API文档与模拟数据接口

学习目标:

  • 理解RESTful架构
  • 运用Swagger编写API文档
  • 掌握Mock.js基本语法
  • 运用easyMock实现模拟接口的编写

1 RESTful

1.1 什么是RESTful架构

​ RESTful架构,就是目前最流行的一种互联网软件架构。它结构清晰、符合标准、易于理解、扩展方便,所以正得到越来越多网站的采用。REST这个词,是Roy Thomas Fielding在他2000年的博士论文中提出的

image-20231003233607306

​ Fielding是一个非常重要的人,他是HTTP协议(1.0版和1.1版)的主要设计者、Apache服务器软件的作者之一、Apache基金会的第一任主席。所以,他的这篇论文一经发表,就引起了关注,并且立即对互联网开发产生了深远的影响。

​ Fielding将他对互联网软件的架构原则,定名为REST,即Representational State Transfer的缩写。我对这个词组的翻译是”表现层状态转化”。如果一个架构符合REST原则,就称它为RESTful架构。

1.2 理解RESTful架构

要理解RESTful架构,最好的方法就是去理解Representational State Transfer这个词组到底是什么意思,它的每一个词代表了什么涵义。

(1)资源(Resources)

REST的名称”表现层状态转化”中,省略了主语。”表现层”其实指的是”资源”(Resources)的”表现层”。

所谓”资源”,就是网络上的一个实体,或者说是网络上的一个具体信息。它可以是一段文本、一张图片、一首歌曲、一种服务,总之就是一个具体的实在。你可以用一个URI(统一资源定位符)指向它,每种资源对应一个特定的URI。要获取这个资源,访问它的URI就可以,因此URI就成了每一个资源的地址或独一无二的识别符。

所谓”上网”,就是与互联网上一系列的”资源”互动,调用它的URI。

(2)表现层(Representation)

“资源”是一种信息实体,它可以有多种外在表现形式。我们把”资源”具体呈现出来的形式,叫做它的”表现层”(Representation)。

比如,文本可以用txt格式表现,也可以用HTML格式、XML格式、JSON格式表现,甚至可以采用二进制格式;图片可以用JPG格式表现,也可以用PNG格式表现。

URI只代表资源的实体,不代表它的形式。严格地说,有些网址最后的”.html”后缀名是不必要的,因为这个后缀名表示格式,属于”表现层”范畴,而URI应该只代表”资源”的位置。它的具体表现形式,应该在HTTP请求的头信息中用Accept和Content-Type字段指定,这两个字段才是对”表现层”的描述。

(3)状态转化(State Transfer)

访问一个网站,就代表了客户端和服务器的一个互动过程。在这个过程中,势必涉及到数据和状态的变化。

互联网通信协议HTTP协议,是一个无状态协议。这意味着,所有的状态都保存在服务器端。因此,如果客户端想要操作服务器,必须通过某种手段,让服务器端发生”状态转化”(State Transfer)。而这种转化是建立在表现层之上的,所以就是”表现层状态转化”。

客户端用到的手段,只能是HTTP协议。具体来说,就是HTTP协议里面,四个表示操作方式的动词:GET、POST、PUT、DELETE。它们分别对应四种基本操作:GET用来获取资源,POST用来新建资源(也可以用于更新资源),PUT用来更新资源,DELETE用来删除资源。

综合上面的解释,我们总结一下什么是RESTful架构:

  (1)每一个URI代表一种资源;

  (2)客户端和服务器之间,传递这种资源的某种表现层;

​  (3)客户端通过四个HTTP动词,对服务器端资源进行操作,实现”表现层状态转化”。

1.3 常见错误

(1)URI包含动词

1
POST /accounts/1/transfer/500/to/2

正确的写法是把动词transfer改成名词transaction

(2)URI包含版本

1
2
3
4
5
http://www.example.com/app/1.0/foo

http://www.example.com/app/1.1/foo

http://www.example.com/app/2.0/foo

因为不同的版本,可以理解成同一种资源的不同表现形式,所以应该采用同一个URI。版本号可以在HTTP请求头信息的Accept字段中进行区分

1
2
3
4
5
Accept: vnd.example-com.foo+json; version=1.0

Accept: vnd.example-com.foo+json; version=1.1

Accept: vnd.example-com.foo+json; version=2.0

2 运用Swagger编写API文档

2.1 Swagger

2.1.1什么是Swagger

​ 随着互联网技术的发展,现在的网站架构基本都由原来的后端渲染,变成了:前端渲染、先后端分离的形态,而且前端技术和后端技术在各自的道路上越走越远。 
​ 前端和后端的唯一联系,变成了API接口;API文档变成了前后端开发人员联系的纽带,变得越来越重要,swagger就是一款让你更好的书写API文档的框架。

2.1.2 SwaggerEditor安装与启动

定位是给项目的设计人员用的,编写swagger文档

(1)下载 https://github.com/swagger-api/swagger-editor/releases/download/v2.10.4/swagger-editor.zip。我在资源中已经提供。

(2)解压swagger-editor,

(3)全局安装http-server(http-server是一个简单的零配置命令行http服务器)

1
npm install -g http-server

(4)启动swagger-editor

1
http-server swagger-editor

(5)浏览器打开: http://localhost:8080

image-20231003233612512

2.1.3 语法规则

(1)固定字段

字段名 类型 描述
swagger string 必需的。使用指定的规范版本。
info Info Object 必需的。提供元数据API。包括标题、描述、版本等
host string 主机名或ip服务API。
basePath string API的基本路径
schemes [string] API的传输协议。 值必须从列表中:”http”,”https”,”ws”,”wss”。
consumes [string] 一个MIME类型的api可以使用列表。值必须是所描述的Mime类型。
produces [string] MIME类型的api可以产生的列表。  值必须是所描述的Mime类型。
paths 路径对象 必需的。可用的路径和操作的API。
definitions 定义对象 一个对象数据类型生产和使用操作。
parameters 参数定义对象 一个对象来保存参数,可以使用在操作。 这个属性不为所有操作定义全局参数。
responses 反应定义对象 一个对象响应,可以跨操作使用。 这个属性不为所有操作定义全球响应。
externalDocs 外部文档对象 额外的外部文档。
summary string 什么操作的一个简短的总结。 最大swagger-ui可读性,这一领域应小于120个字符。
description string 详细解释操作的行为。GFM语法可用于富文本表示。
operationId string 独特的字符串用于识别操作。 id必须是唯一的在所有业务中所描述的API。 工具和库可以使用operationId来唯一地标识一个操作,因此,建议遵循通用的编程的命名约定。
deprecated boolean 声明该操作被弃用。 使用声明的操作应该没有。 默认值是false。

(2)字段类型与格式定义

普通的名字(java中的) type(swagger中的) format(有format要配置上) 说明
integer integer int32 签署了32位
long integer int64 签署了64位
float number float
double number double
string string
byte string byte base64编码的字符
binary string binary 任何的八位字节序列
boolean boolean
date string date 所定义的full-date- - - - - -RFC3339
dateTime string date-time 所定义的date-time- - - - - -RFC3339
password string password 用来提示用户界面输入需要模糊。

2.2 基础模块-城市API文档

2.2.1 新增城市

编写新增城市的API , post提交城市实体

URL: /city

Method: post

编写后的文档内容如下:

image-20231003233617457

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
swagger: '2.0'
info:
version: "1.0.0"
title: 基础模块-城市API
basePath: /base
host: api.tensquare.com
paths:
/city:
post:
summary: 新增城市
parameters:
- name: "body"
in: "body"
description: 城市实体类
required: true
schema:
$ref: '#/definitions/City'
responses:
200:
description: 成功
schema:
$ref: '#/definitions/ApiResponse'
definitions:
City:
type: object
properties:
id:
type: string
description: "ID"
name:
type: string
description: "名称"
ishot:
type: string
description: 是否热门
ApiResponse:
type: object
properties:
flag:
type: boolean
description: 是否成功
code:
type: integer
format: int32
description: 返回码
message:
type: string
description: 返回信息

编辑后可以在右侧窗口看到显示的效果

2.2.2 修改城市

URL: /city/{cityId}

Method: put

编写后的文档内容如下:

image-20231003233622767

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/city/{cityId}:
put:
summary: 修改城市
parameters:
- name: cityId
in: path
description: 城市ID
required: true
type: string
- name: body
in: body
description: 城市
schema:
$ref: '#/definitions/City'
responses:
200:
description: 成功响应
schema:
$ref: '#/definitions/ApiResponse'

2.2.3 删除城市

删除城市地址为/city/{cityId} ,与修改城市的地址相同,区别在于使用delete方法提交请求

image-20231003233626848

代码如下: (/city/{cityId} 下增加delete)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
delete:
summary: 根据ID删除
description: 返回是否成功
parameters:
- name: cityId
in: path
description: 城市ID
required: true
type: string
responses:
'200':
description: 成功
schema:
$ref: '#/definitions/ApiResponse'

2.2.4 根据ID查询城市

URL: /city/{cityId}

Method: get

返回的内容结构为: {flag:true,code:20000, message:”查询成功”,data: {…..} }

data属性返回的是city的实体类型

image-20231003233630940

代码实现如下:

(1)在definitions下定义城市对象的响应对象

1
2
3
4
5
6
7
8
9
10
11
12
ApiCityResponse:
type: "object"
properties:
code:
type: "integer"
format: "int32"
flag:
type: "boolean"
message:
type: "string"
data:
$ref: '#/definitions/City'

(2)/city/{cityId} 下新增get方法API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
get:
summary: 根据ID查询
description: 返回一个城市
parameters:
- name: cityId
in: path
description: 城市ID
required: true
type: string
responses:
'200':
description: 操作成功
schema:
$ref: '#/definitions/ApiCityResponse'

2.2.5 城市列表

URL: /city

Method: get

返回的内容结构为: {flag:true,code:20000, message:”查询成功”,data:[{…..},{…..},{…..}] }

data属性返回的是city的实体数组

image-20231003233635067

实现步骤如下:

(1)在definitions下定义城市列表对象以及相应对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
CityList:
type: "array"
items:
$ref: '#/definitions/City'
ApiCityListResponse:
type: "object"
properties:
code:
type: "integer"
format: "int32"
flag:
type: "boolean"
message:
type: "string"
data:
$ref: '#/definitions/CityList'

(2)在/city增加get

1
2
3
4
5
6
7
8
get:
summary: "城市全部列表"
description: "返回城市全部列表"
responses:
200:
description: "成功查询到数据"
schema:
$ref: '#/definitions/ApiCityListResponse'

2.2.6 根据条件查询城市列表

实现API效果如下:

image-20231003233639233

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/city/search:
post:
summary: 城市列表(条件查询)
parameters:
- name: body
in: body
description: 查询条件
required: true
schema:
$ref: "#/definitions/City"
responses:
200:
description: 查询成功
schema:
$ref: '#/definitions/ApiCityListResponse'

2.2.7 城市分页列表

实现API效果如下:

image-20231003233643072

实现如下:

(1)在definitions下定义城市分页列表响应对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ApiCityPageResponse:
type: "object"
properties:
code:
type: "integer"
format: "int32"
flag:
type: "boolean"
message:
type: "string"
data:
properties:
total:
type: "integer"
format: "int32"
rows:
$ref: '#/definitions/CityList'

(2)新增节点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/city/search/{page}/{size}:
post:
summary: 城市分页列表
parameters:
- name: page
in: path
description: 页码
required: true
type: integer
format: int32
- name: size
in: path
description: 页大小
required: true
type: integer
format: int32
- name: body
in: body
description: 查询条件
required: true
schema:
$ref: "#/definitions/City"
responses:
200:
description: 查询成功
schema:
$ref: '#/definitions/ApiCityPageResponse'

2.3 批量生成API文档

我们使用《黑马程序员代码生成器》自动生成所有表的yml文档

自动生成的文档中类型均为string ,我们这里需要再对类型进行修改即可。

步骤:

(1)执行建表脚本

(2)使用《黑马程序员代码生成器》生成脚本,有个生成swagger api文档的模板

2.4 其它模块API

请学员参见本章的扩展文档来实现部分功能。

2.5 SwaggerUI

定位是给阅读人员使用的

SwaggerUI是用来展示Swagger文档的界面,以下为安装步骤

(1)在本地安装nginx

(2)下载SwaggerUI源码 https://swagger.io/download-swagger-ui/

(3)解压,将dist文件夹下的全部文件拷贝至 nginx的html目录

(4)启动nginx

(5)浏览器打开页面 http://localhost即可看到文档页面

image-20231003233648720

(6)我们将编写好的yml文件也拷贝至nginx的html目录,这样我们就可以加载我们的swagger文档了

image-20220929172430555

3 Mock.js

3.1 什么是Mock.js

Mock.js (官网http://mockjs.com/)是一款模拟数据生成器,旨在帮助前端攻城师独立于后端进行开发,帮助编写单元测试。提供了以下模拟功能:

根据数据模板生成模拟数据

模拟 Ajax 请求,生成并返回模拟数据

基于 HTML 模板生成模拟数据

Mock.js具有以下特点:

前后端分离

让前端攻城师独立于后端进行开发。

增加单元测试的真实性

通过随机数据,模拟各种场景。

开发无侵入

不需要修改既有代码,就可以拦截 Ajax 请求,返回模拟的响应数据。

用法简单

符合直觉的接口。

数据类型丰富

支持生成随机的文本、数字、布尔值、日期、邮箱、链接、图片、颜色等。

方便扩展

支持支持扩展更多数据类型,支持自定义函数和正则。

3.2 Mock.js安装

在命令提示符下用npm安装mockjs

1
cnpm install mockjs

3.3 快速入门

需求:生成列表数据,数据条数为5条。

显示效果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{
"list": [
{
"id": 1,
"name": "测试"
},
{
"id": 1,
"name": "测试"
},
{
"id": 1,
"name": "测试"
},
{
"id": 1,
"name": "测试"
},
{
"id": 1,
"name": "测试"
}
]
}

新建demo1.js 代码如下

1
2
3
4
5
6
7
8
9
10
let Mock=require('mockjs')
let data=Mock.mock({
'list|5':[
{
'id':1,
'name':'测试'
}
]
})
console.log(JSON.stringify(data,null,2 ))

执行命令node demo1 查看运行效果。

我们在本例中产生了5条相同的数据,这些数据都是相同的,如果我们需要让这些数据是按照一定规律随机生成的,需要按照Mock.js的语法规范来定义。

Mock.js 的语法规范包括两部分:

1.数据模板定义规范(Data Template Definition,DTD)

2.数据占位符定义规范(Data Placeholder Definition,DPD)

3.4 数据模板定义规范DTD

数据模板中的每个属性由 3 部分构成:属性名、生成规则、属性值

1
2
3
4
// 属性名   name
// 生成规则 rule
// 属性值 value
'name|rule': value

属性名 和 生成规则 之间用竖线 | 分隔。

生成规则 是可选的。

生成规则 有 7 种格式:

‘name|min-max’: value

‘name|count’: value

‘name|min-max.dmin-dmax’: value

‘name|min-max.dcount’: value

‘name|count.dmin-dmax’: value

‘name|count.dcount’: value

‘name|+step’: value

生成规则的含义需要依赖属性值的类型才能确定。

属性值 中可以含有 @占位符。

属性值 还指定了最终值的初始值和类型

3.4.1 属性值是字符串

(1)’name|count’: string

通过重复 string 生成一个字符串,重复次数等于 count

1
2
3
4
5
6
7
8
9
10
11
// 使用 Mock
let Mock = require('mockjs')
let data = Mock.mock({
'list|5': [{
'id': 1,
'name':'测试',
'phone|11':'1'
}]
})
// 输出结果
console.log(JSON.stringify(data,null,2))

(2)’name|min-max’: string

通过重复 string 生成一个字符串,重复次数大于等于 min,小于等于 max

1
2
3
4
5
6
7
8
9
10
11
// 使用 Mock
let Mock = require('mockjs')
let data = Mock.mock({
'list|5': [{
'id': 1,
'name|2-4':'测试',
'phone|11':'1'
}]
})
// 输出结果
console.log(JSON.stringify(data,null,2))

3.4.2 属性值是数字number

(1)’name|+1’: number

属性值自动加 1,初始值为 number。

1
2
3
4
5
6
7
8
9
10
11
// 使用 Mock
let Mock = require('mockjs')
let data = Mock.mock({
'list|5': [{
'id|+1': 1,
'name|2-3':'测试',
'phone|11':'1'
}]
})
// 输出结果
console.log(JSON.stringify(data,null,2))

(2)’name|min-max’: number

生成一个大于等于 min、小于等于 max 的整数,属性值 number 只是用来确定类型

1
2
3
4
5
6
7
8
9
10
11
12
// 使用 Mock
let Mock = require('mockjs')
let data = Mock.mock({
'list|5': [{
'id|+1': 1,
'name|2-3':'测试',
'phone|11':'1',
'point|122-500':0
}]
})
// 输出结果
console.log(JSON.stringify(data,null,2))

(3)’name|min-max.dcount’: value 生成一个浮点数,整数部分大于等于 min、小于等于 max,小数部分为dcount位

1
2
3
4
5
6
7
8
9
10
11
12
13
// 使用 Mock
let Mock = require('mockjs')
let data = Mock.mock({
'list|5': [{
'id|+1': 1,
'name|2-3':'测试',
'phone|11':'1',
'point|122-500':0,
'money|3000-8000.2':0
}]
})
// 输出结果
console.log(JSON.stringify(data,null,2))

(4)’name|min-max.dmin-dmax’: number

生成一个浮点数,整数部分大于等于 min、小于等于 max,小数部分保留 dmin 到 dmax 位。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 使用 Mock
let Mock = require('mockjs')
let data = Mock.mock({
'list|5': [{
'id|+1': 1,
'name|2-3':'测试',
'phone|11':'1',
'point|122-500':0,
'money|3000-8000.2':0,
'money2|1000-5000.2-4':0,
}]
})
// 输出结果
console.log(JSON.stringify(data,null,2))

3.4.3 属性值是布尔

(1)’name|1’: boolean

随机生成一个布尔值,值为 true 的概率是 1/2,值为 false 的概率同样是 1/2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 使用 Mock
let Mock = require('mockjs')
let data = Mock.mock({
'list|5': [{
'id|+1': 1,
'name|2-3':'测试',
'phone|11':'1',
'point|122-500':0,
'money|3000-8000.2':0,
'status|1':true
}]
})
// 输出结果
console.log(JSON.stringify(data,null,2))

(2)’name|min-max’: value

随机生成一个布尔值,值为 value 的概率是 min / (min + max)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 使用 Mock
let Mock = require('mockjs')
let data = Mock.mock({
'list|5': [{
'id|+1': 1,
'name|2-3':'测试',
'phone|11':'1',
'point|122-500':0,
'money|3000-8000.2':0,
'status|1':true,
'default|1-3':true
}]
})
// 输出结果
console.log(JSON.stringify(data,null,2))

3.4.4 属性值是Object

(1)’name|count’: object

从属性值 object 中随机选取 count 个属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 使用 Mock
let Mock = require('mockjs')
let data = Mock.mock({
'list|5': [{
'id|+1': 1,
'name|2-3':'测试',
'phone|11':'1',
'point|122-500':0,
'money|3000-8000.2':0,
'status|1':true,
'default|1-3':true,
'detail|2':{'id':1,'date':'2005-01-01','content':'记录'}
}]
})
// 输出结果
console.log(JSON.stringify(data,null,2))

(2)’name|min-max’: object

从属性值 object 中随机选取 min 到 max 个属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 使用 Mock
let Mock = require('mockjs')
let data = Mock.mock({
'list|5': [{
'id|+1': 1,
'name|2-3':'测试',
'phone|11':'1',
'point|122-500':0,
'money|3000-8000.2':0,
'status|1':true,
'default|1-3':true,
'detail|2-3':{'id':1,'date':'2005-01-01','content':'记录'}
}]
})
// 输出结果
console.log(JSON.stringify(data,null,2))

3.4.5 属性值是数组

(1)’name|count’: array

通过重复属性值 array 生成一个新数组,重复次数为 count

(2)’name|min-max’: array

通过重复属性值 array 生成一个新数组,重复次数大于等于 min,小于等于 max。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 使用 Mock
let Mock = require('mockjs')
let data = Mock.mock({
'list|5-10': [{
'id|+1': 1,
'name|2-3':'测试',
'phone|11':'1',
'point|122-500':0,
'money|3000-8000.2':0,
'status|1':true,
'default|1-3':true,
'detail|2-3':{'id':1,'date':'2005-01-01','content':'记录'}
}]
})
// 输出结果
console.log(JSON.stringify(data,null,2))

3.5 数据占位符定义规范DPD

Mock.Random 是一个工具类,用于生成各种随机数据。

Mock.Random 的方法在数据模板中称为『占位符』,书写格式为 @占位符(参数 [, 参数]) 。

内置方法列表:

Type Method
Basic boolean, natural, integer, float, character, string, range, date, time, datetime, now
Image image, dataImage
Color color
Text paragraph, sentence, word, title, cparagraph, csentence, cword, ctitle
Name first, last, name, cfirst, clast, cname
Web url, domain, email, ip, tld
Address area, region
Helper capitalize, upper, lower, pick, shuffle
Miscellaneous guid, id

 下面我们讲解每种内置方法的使用:

3.5.1 基本方法

可以生成随机的基本数据类型

string 字符串

integer 整数

date 日期

1
2
3
4
5
6
7
8
9
10
11
12
// 使用 Mock
let Mock = require('mockjs')
let data = Mock.mock({
'list|10': [{
'id|+1': 1,
'name':'@string',
'point':'@integer',
'birthday':'@date'
}]
})
// 输出结果
console.log(JSON.stringify(data,null,2))

3.5.2 图像方法

image 随机生成图片地址

1
2
3
4
5
6
7
8
9
10
11
12
13
// 使用 Mock
let Mock = require('mockjs')
let data = Mock.mock({
'list|10': [{
'id|+1': 1,
'name':'@string',
'point':'@integer',
'birthday':'@date',
'pic':'@image'
}]
})
// 输出结果
console.log(JSON.stringify(data,null,2))

3.5.3 文本方法

@title: 标题

@cword(100) :文本内容 参数为字数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 使用 Mock
let Mock = require('mockjs')
let data = Mock.mock({
'list|10': [{
'id|+1': 1,
'name':'@string',
'point':'@integer',
'birthday':'@date',
'pic':'@image',
'title':'@title',
'content':'@cword(100)'
}]
})
// 输出结果
console.log(JSON.stringify(data,null,2))

3.5.4 名称方法

cname :中文名称

cfirst:中文姓氏

Last:英文姓氏

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 使用 Mock
let Mock = require('mockjs')
let data = Mock.mock({
'list|10': [{
// 属性 id 是一个自增数,起始值为 1,每次增 1
'id|+1': 1,
'name':'@cname',
'ename':'@last',
'cfirst':'@cfirst',
'point':'@integer',
'birthday':'@date',
'pic':'@image',
'title':'@title',
'content':'@cword(100)'
}]
})
// 输出结果
console.log(JSON.stringify(data,null,2))

3.5.5 网络方法

可以生成url ip email等网络相关信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 使用 Mock
let Mock = require('mockjs')
let data = Mock.mock({
'list|10': [{
'id|+1': 1,
'name':'@cname',
'ename':'@last',
'cfirst':'@cfirst',
'point':'@integer',
'birthday':'@date',
'pic':'@image',
'title':'@title',
'content':'@cword(100)',
'url':"@url",
'ip':"@ip",
'email':"@email"
}]
})
// 输出结果
console.log(JSON.stringify(data,null,2))

3.5.6 地址方法

@region 区域

@county 省市县

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 使用 Mock
let Mock = require('mockjs')
let data = Mock.mock({
'list|10': [{
'id|+1': 1,
'name':'@cname',
'ename':'@last',
'cfirst':'@cfirst',
'point':'@integer',
'birthday':'@date',
'pic':'@image',
'title':'@title',
'content':'@cword(100)',
'url':"@url",
'ip':"@ip",
'email':"@email",
'area':'@region',
'address':'@county(true)'
}]
})
// 输出结果
console.log(JSON.stringify(data,null,2))

4 EasyMock

4.1 什么是EasyMock

​ Easy Mock 是杭州大搜车无线团队出品的一个极其简单、高效、可视化、并且能快速生成模拟数据的在线 mock 服务。以项目管理的方式组织 Mock List,能帮助我们更好的管理 Mock 数据。

地址:https://www.easy-mock.com

在线文档:https://www.easy-mock.com/docs

这玩意可以自己部署一个在公司内网服务器给大家用

4.2 EasyMock基本入门

4.2.1初始设置

(1)登录或注册。

浏览器打开https://www.easy-mock.com 输出用户名和密码,如果不存在会自动注册。注意:请牢记密码,系统没有找回密码功能!

image-20231003233702976

登录后进入主界面

image-20231003233707041

(2)创建项目:点击右下角的加号

image-20231003233711415

填写项目名称,点击创建按钮

image-20231003233715656

创建完成后可以在列表中看到刚刚创建的项目

4.2.2接口操作

(1)创建接口。点击列表中的项目

image-20231003233720693

进入项目工作台页面

image-20231003233727214

点击“创建接口” ,左侧区域输出mock数据,右侧定义Method 、 Url 、描述等信息。

image-20231003233731746

我们可以将我们在Mock.js入门案例中的对象放入左侧的编辑窗口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
'list|10': [{
"id|+1": 1,
"name": "@cname",
"cfirst": "@cfirst",
"Last": "@Last",
"point": "@integer",
"birthday": "@date",
"pic": "@image",
"content": "@cword(30,200)",
"url": "@url",
"ip": "@ip",
"email": "@email",
"region": "@region",
"county": "@county"
}]
}

填写url Method 和描述 ,点击创建按钮

(2)克隆接口和修改接口

(3)预览接口和复制接口地址

(4)删除接口

4.3 本地部署EasyMock

4.3.1 Centos部署node.js

(1)将node官网下载的node-v8.11.1-linux-x64.tar.xz 上传至服务器

(2)解压xz文件

1
xz -d node-v8.11.1-linux-x64.tar.xz

(3)解压tar文件

1
tar -xvf node-v8.11.1-linux-x64.tar

(4)目录重命名

1
mv node-v8.11.1-linux-x64 node

(5)移动目录到/usr/local下

1
mv node /usr/local/

(6)配置环境变量

1
vi /etc/profile

填写以下内容

1
2
3
#set for nodejs  
export NODE_HOME=/usr/local/node
export PATH=$NODE_HOME/bin:$PATH

执行命令让环境变量生效

1
source /etc/profile

查看node版本看是否安装成功

1
node -v

4.3.2 MongoDB安装与启动

我们使用yum方式安装mongoDb

(1)配置yum

1
vi /etc/yum.repos.d/mongodb-org-3.2.repo

编辑以下内容:

1
2
3
4
5
6
[mongodb-org-3.2]
name=MongoDB Repository
baseurl=https://repo.mongodb.org/yum/redhat/$releasever/mongodb-org/3.2/x86_64/
gpgcheck=1
enabled=1
gpgkey=https://www.mongodb.org/static/pgp/server-3.2.asc

(2)安装MongoDB

1
yum install -y mongodb-org

(3)启动MongoD

1
systemctl start mongod

4.3.3 Redis安装与启动

(1)下载fedora的epel仓库

1
yum install epel-release

(2)下载安装redis

1
yum install redis

(3)启动redis服务

1
systemctl start redis

4.3.4 本地部署easy-mock

(1)项目下载地址: https://github.com/easy-mock/easy-mock

(2)将easy-mock-dev.zip上传至服务器

(3)安装zip 和unzip

1
yum install zip unzip

(4)解压

1
unzip easy-mock-dev.zip

(3)进入其目录,安装依赖

1
npm install

(4)执行构建

1
npm run build

(5)启动

1
npm run start

(6)打开浏览器 http://192.168.184.131:7300

image-20231003233743164

4.4 导入SwaggerAPI文档

(1)将我们的SwaggerAPI文档扩展名改为yml

(2)在easyMock中点击“设置”选项卡

(3)SwaggerDocs API 选择Upload

image-20231003233747723

(4)将SwaggerAPI文档拖动到上图的虚线区域,点击保存

(5)回到主界面后点击“同步Swagger”

第3章-使用ElementUI开发管理后台-1

学习目标:

  • 掌握elementUI提供的脚手架搭建管理后台的方法

  • 掌握elementUI的table组件的使用,实现列表功能

  • 掌握elementUI的pagination组件的使用,实现分页功能

  • 掌握elementUI的form相关组件的使用,实现条件查询功能

  • 掌握elementUI的dialog组件和$message的使用,实现弹出窗口和消息提示功能

  • 掌握elementUI的select组件的使用,实现下拉列表功能

  • 实现新增数据和修改数据的功能

  • 掌握confirm的使用,实现询问框,实现删除功能

1 管理后台搭建

我们的十次方管理后台就采用ElementUI来进行搭建.

1.1 什么是ElementUI

Element 饿了么前端出品的一套 Vue.js 后台组件库

官网: http://element.eleme.io/#/zh-CN

image-20231003233752532 

1.2 神奇的脚手架

1.2.1 快速搭建

​ 官网上提供了非常基础的脚手架,如果我们使用官网的脚手架需要自己写很多代码比如登陆界面、主界面菜单等内容。 课程已经提供了功能完整的脚手架,我们可以拿过来在此基础上开发,这样可以极大节省我们开发的时间。

(1)解压vueAdmin-template-master

(2)在命令提示符进入该目录,输入命令:

1
cnpm install

这样下载安装所有的依赖,几分钟后下载完成。

(3)输入命令:

1
npm run dev

运行后自动弹出浏览器。

1.2.2 了解工程结构

以下是主要的目录结构:

目录名称 存储内容
build 构建工程相关脚本
config 配置相关
src 工程源码
static 静态资源
src/api 访问后端API
src/utils 工具类
src/views 页面
src/router 路由

1.3 项目初始化

1.3.1 关闭语法规范性检查

修改config/index.js ,将useEslint的值改为false。

此配置作用: 是否开启语法检查,语法检查是通过ESLint 来实现的。

我们现在科普一下,什么是ESLint : ESLint 是一个语法规则和代码风格的检查工具,可以用来保证写出语法正确、风格统一的代码。如果我们开启了Eslint , 也就意味着要接受它非常苛刻的语法检查,包括空格不能少些或多些,必须单引不能双引,语句后不可以写分号等等,这些规则其实是可以设置的。我们作为前端的初学者,最好先关闭这种校验,否则会浪费很多精力在语法的规范性上。如果以后做真正的企业级开发,建议开启。

1.3.2 国际化设置

打开main.js 找到这句代码

1
import locale from 'element-ui/lib/locale/lang/en'

我们将en修改为zn-CN

1
import locale from 'element-ui/lib/locale/lang/zh-CN'

修改后组件都是按照中文的习惯展示

1.3.3 与easy-mock对接

(1)修改config下的dev.env.js中的BASE_API为easy-mock的Base URL

1
2
3
....
BASE_API: '"http://192.168.184.133:7300/mock/5af314a4c612520d0d7650c7"',
....

(2)easy-mock添加登陆认证模拟数据

地址: /user/login

提交方式:post

内容:

1
2
3
4
5
6
{
"code": 20000,
"data": {
"token": "admin"
}
}

(3)添加返回用户信息url模拟数据

地址:/user/info

提交方式:get

内容:

1
2
3
4
5
6
7
8
9
{
"code": 20000,
"data": {
"roles": ["admin"],
"role": ["admin"],
"name": "admin",
"avatar": "https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif"
}
}

1.3.4 更改标题与菜单

(1)修改index.html里的标题为”十次方后台管理系统”,修改后浏览器自动刷新。

这就是脚手架中已经为我们添加了热部署功能。

(2)修改src/router 下的index.js 中constantRouterMap的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
export const constantRouterMap = [
{ path: '/login', component: () => import('@/views/login/index'), hidden: true },
{ path: '/404', component: () => import('@/views/404'), hidden: true },
{
path: '/',
component: Layout,
redirect: '/dashboard',
name: 'Dashboard',
hidden: true,
children: [{
path: 'dashboard',
component: () => import('@/views/dashboard/index')
}]
},
{
path: '/example',
component: Layout,
redirect: '/example/table',
name: 'Example',
meta: { title: '基本信息管理', icon: 'example' },
children: [
{
path: 'table',
name: 'Table',
component: () => import('@/views/table/index'),
meta: { title: '城市管理', icon: 'table' }
},
{
path: 'tree',
name: 'Tree',
component: () => import('@/views/tree/index'),
meta: { title: '标签管理', icon: 'tree' }
}
]
},
{
path: '/form',
component: Layout,
name: 'Example2',
meta: { title: '活动管理', icon: 'example' },
children: [
{
path: 'index',
name: 'Form',
component: () => import('@/views/form/index'),
meta: { title: '活动管理', icon: 'form' }
}
]
},
{ path: '*', redirect: '/404', hidden: true }
]

2 活动管理-列表

2.1 需求分析

实现活动管理的列表页,包括分页,条件查询。

2.2 表格组件

我们在这一环节先实现一个简单的列表,如下图所示:

image-20231003233801239

准备工作:我们将swaggerAPI同步到easyMock 然后修改/gathering/gathering ( GET方法)的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"code": 20000,
"flag": true,
"message": "@string",
"data|10": [{
"id": "@string",
"name": "@cword(8,12)",
"summary": "@cword(20,40)",
"detail": "@cword(20,40)",
"sponsor": "@string",
"image": "@image",
"starttime": "@date",
"endtime": "@date",
"address": "@county(true)",
"enrolltime": "@date",
"state": "@string",
"city": "@string"
}]
}

代码实现步骤:

(1)在src/api创建gathering.js

1
2
3
4
5
6
7
8
9
import request from "@/utils/request"
export default {
getList(){
return request({
url:'/gathering/gathering',
method:'get'
})
}
}

(2)创建gathering.vue中 ,编写脚本部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<template>

</template>
<script>
import gatheringApi from '@/api/gathering'
export default {
data(){
return {
list:[]
}
},
created(){
this.fetchData();
},
methods:{
fetchData(){
gatheringApi.getList().then(response => {
this.list=response.data
});
}
}
}
</script>

(3)修改gathering.vue,编写html代码部分

1
2
3
4
5
6
7
8
9
10
<template>
<el-table :data="list" border style="width: 100%">
<el-table-column prop="id" label="活动ID" width="180"></el-table-column>
<el-table-column prop="name" label="活动名称" width="180"></el-table-column>
<el-table-column prop="sponsor" label="主办方" width="180"></el-table-column>
<el-table-column prop="address" label="活动地址" width="180"></el-table-column>
<el-table-column prop="starttime" label="开始日期" width="180"></el-table-column>
<el-table-column prop="endtime" label="结束日期" width="180"></el-table-column>
</el-table>
</template>

table组件的属性

参数 说明 类型 可选值 默认值
data 显示的数据 array

table-column组件的属性

参数 说明 类型 可选值 默认值
label 显示的标题 string
prop 对应列内容的字段名,也可以使用 property 属性 string
width 对应列的宽度 string

以上属性为我们代码中使用到的属性,其他属性请查阅官方文档.

http://element-cn.eleme.io/#/zh-CN/component/table

2.3 分页组件

我们已经通过表格组件完成了列表的展示,接下来需要使用分页组件完成分页功能

image-20231003233817335 

准备工作:修改接口/gathering/gathering/search/{page}/{size} method:POST

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
"code": 20000,
"flag": true,
"message": "@string",
"data": {
"total": "@integer(100,200)",
"rows|10": [{
"id": "@string",
"name": "@cword(8,12)",
"summary": "@cword(20,40)",
"detail": "@cword(20,40)",
"sponsor": "@string",
"image": "@image",
"starttime": "@date",
"endtime": "@date",
"address": "@county(true)",
"enrolltime": "@date",
"state": "1",
"city": "@string"
}]
}
}

代码实现:

(1)修改src/api/gathering.js,增加方法导出

1
2
3
4
5
6
7
search(page,size,searchMap) {
return request({
url: `/gathering/gathering/search/${page}/${size}`,
method: 'post',
data: searchMap
})
}

(2)修改src/views/table/gathering.vue,编写脚本部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import gatheringApi  from '@/api/gathering'
export default {
data(){
return {
list:[],
total:0,//总记录数
currentPage:1,//当前页
pageSize:10,//每页大小
searchMap: {}//查询表单绑定变量
}
},
created(){
this.fetchData()
},
methods:{
fetchData(){
gatheringApi.search(this.currentPage,this.pageSize,this.searchMap).then( response =>{
this.list=response.data.rows
this.total=response.data.total
})
}
}
}

(3)修改src/views/table/gathering.vue,增加分页栏

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<template>
<div>
<el-table :data="list" border style="width: 100%">
<el-table-column prop="id" label="活动ID" width="180"></el-table-column>
<el-table-column prop="name" label="活动名称" width="180"></el-table-column>
<el-table-column prop="sponsor" label="主办方" width="180"></el-table-column>
<el-table-column prop="address" label="活动地址" width="180"></el-table-column>
<el-table-column prop="starttime" label="开始日期" width="180"></el-table-column>
<el-table-column prop="endtime" label="结束日期" width="180"></el-table-column>
</el-table>
<el-pagination
@size-change="fetchData"
@current-change="fetchData"
:current-page="currentPage"
:page-sizes="[5, 10, 20]"
:page-size="pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="total">
</el-pagination>
</div>
</template>

currentPage为当前页 , total为总记录数

注意:template里面要求必须有唯一的跟节点,我们这里用div将表格和分页控件包起来。

pagination的常用属性:

参数 说明 类型 可选值 默认值
page-size 每页显示条目个数 Number 10
total 总条目数 Number
current-page 当前页数,支持 .sync 修饰符 Number 1
layout 组件布局,子组件名用逗号分隔 String sizesprevpagernextjumper->totalslot ‘prev, pager, next, jumper, ->, total’
page-sizes 每页显示个数选择器的选项设置 Number[] [10, 20, 30, 40, 50, 100]

pagination的常用事件:

事件名称 说明 回调参数
size-change pageSize 改变时会触发 每页条数
current-change currentPage 改变时会触发 当前页

更多属性方法事件请查看官方文档:http://element-cn.eleme.io/#/zh-CN/component/pagination

2.4 条件查询

需求:在分页列表的基础上实现条件查询功能

image-20231003233824234

代码实现:

修改src/views/table/gathering.vue,增加查询表单

1
2
3
4
5
6
7
8
9
10
11
<!--查询表单-->
<el-form :inline="true" class="demo-form-inline">
<el-form-item label="活动名称">
<el-input v-model="searchMap.name" placeholder="活动名称"></el-input>
</el-form-item>
<el-form-item label="活动日期" >
<el-date-picker type="date" placeholder="选择开始日期" v-model="searchMap.starttime_1" ></el-date-picker>
<el-date-picker type="date" placeholder="选择截止日期" v-model="searchMap.starttime_2" ></el-date-picker>
</el-form-item>
<el-button type="primary" @click="fetchData()">查询</el-button>
</el-form>

form(表单)组件属性详见官方文档:http://element-cn.eleme.io/#/zh-CN/component/form

input(文本框)组件属性详见官方文档:http://element-cn.eleme.io/#/zh-CN/component/input

date-picker(日期框)组件属性详见官方文档:http://element-cn.eleme.io/#/zh-CN/component/date-picker

3 活动管理-增加

3.1 需求分析

界面中加入”新增”按钮,点击弹出编辑窗口

image-20231003233828776

点击“修改”按钮,关闭窗口并刷新表格,弹出提示(成功或失败)

3.2 弹出窗口

(1)修改src/api/gathering.js,在template中增加对话框组件

1
2
3
<el-dialog  title="活动编辑" :visible.sync="dialogFormVisible" >

</el-dialog>

属性title为对话框标题, visible为是否显示。

(2)变量dialogFormVisible用于控制对话框的显示。我们在脚本代码中定义

1
2
3
4
5
6
data(){
return {
.....
dialogFormVisible: false //对话框是否显示
}
}

(3)template中增加按钮,用于打开对话框

1
<el-button type="primary" @click="dialogFormVisible = true">新增</el-button>

dialog属性详见官方文档:http://element-cn.eleme.io/#/zh-CN/component/dialog

3.3 编辑表单

修改src/views/table/gathering.vue,在弹出窗口添加编辑表单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<el-dialog title="编辑" :visible.sync="dialogFormVisible">
<el-form label-width="80px">
<el-form-item label="活动名称">
<el-input v-model="pojo.name" placeholder="活动名称"></el-input>
</el-form-item>
<el-form-item label="基本地址">
<el-input v-model="pojo.address" placeholder="基本地址"></el-input>
</el-form-item>
<el-form-item label="开始日期">
<el-date-picker type="date" v-model="pojo.starttime" placeholder="开始日期"></el-date-picker>
</el-form-item>
<el-form-item label="截至日期">
<el-date-picker type="date" v-model="pojo.endtime" placeholder="截至日期"></el-date-picker>
</el-form-item>
<el-form-item label="报名截止">
<el-date-picker type="date" v-model="pojo.enrolltime" placeholder="报名截止"></el-date-picker>
</el-form-item>
<el-form-item label="活动详情">
<el-input v-model="pojo.detail" placeholder="活动详情" type="textarea" :rows="2"></el-input>
</el-form-item>
<el-form-item label="是否可见">
<el-switch active-value="1" inactive-value="0" v-model="pojo.status"></el-switch>
</el-form-item>
<el-form-item>
<el-button type="primary" >保存</el-button>
<el-button @click="dialogFormVisible = false" >关闭</el-button>
</el-form-item>
</el-form>
</el-dialog>

这里我们主要要掌握多行文本编辑框与开关组件switch的使用

3.4 下拉选择框

需求:在新增窗口实现城市下拉选择框

image-20231003233834170 

我们这里需要使用elementUI提供的下拉选择框

准备工作:修改easyMock 中的/base/city (GET)

1
2
3
4
5
6
7
8
9
10
{
"flag": true,
"code": 20000,
'message': "查询成功",
'data|10': [{
"id|+1": 1,
"name": "@city",
"ishot": "1",
}]
}

代码实现

(1)创建src/api/city.js

1
2
3
4
5
6
7
8
9
import request from '@/utils/request'
export default {
getList(){
return request({
url: '/base/city',
method: 'get'
})
}
}

(2)修改src/views/table/gathering.vue的js脚本部分

为data添加属性

1
cityList: []

引入城市API

1
import cityApi from '@/api/city'

修改created,增加对城市方法的调用

1
2
3
4
5
6
created() {
this.fetchData()
cityApi.getList().then(response =>{
this.cityList = response.data
})
}

(3)修改src/views/table/gathering.vue,增加城市下拉框

1
2
3
4
5
6
7
8
9
10
<el-form-item label="城市">
<el-select v-model="pojo.city" placeholder="请选择">
<el-option
v-for="item in cityList"
:key="item.id"
:label="item.name"
:value="item.id">
</el-option>
</el-select>
</el-form-item>

3.5 表单提交

(1)修改easymock中的/gathering/gathering (增加活动 POST)

1
2
3
4
5
{
"flag": true,
"code": 20000,
'message': "执行成功"
}

(2)修改src/api/gathering.js,增加方法导出

1
2
3
4
5
6
7
save(pojo) {
return request({
url: `/gathering/gathering`,
method: 'post',
data: pojo
})
}

(3)修改src/views/table/gathering.vue的js脚本部分 增加方法执行保存

1
2
3
4
5
6
7
8
9
handleSave(){
gatheringApi.save(this.pojo).then(response=>{
alert(response.message)
if(response.flag){//如果成功
this.fetchData()//刷新列表
}
})
this.dialogFormVisible=false//关闭窗口
}

(4)修改弹出框中的“保存”按钮,调用保存方法

1
<el-button type="primary" @click="handleSave()">保存</el-button>

4 活动管理-修改

4.1 需求分析

在表格的操作列增加”修改”按钮,点击修改按钮弹出窗口并显示数据,点击保存按钮保存修改并刷新表格。

image-20231003233839366

4.2 根据ID加载数据

准备工作:修改easymock 接口 /gathering/gathering/{id} (GET)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"flag": true,
"code": 20000,
'message': "查询成功",
'data': {
"id": "1",
"name": "测试活动",
"sponsor": "主办方",
"image": "@image",
"starttime": "@date",
"endtime": "@date",
"address": "@county(true)",
"enrolltime": "@date",
"state": "1"
}
}

代码实现:

(1)修改src/api/gathering.js,增加方法定义

1
2
3
4
5
6
findById(id) {
return request({
url: `/gathering/gathering/${id}`,
method: 'get'
})
}

(2)修改src/views/table/gathering.vue的js脚本部分

新增handleEdit方法

1
2
3
4
5
6
7
8
9
handleEdit(id){
this.dialogFormVisible=true//打开窗口
//调用查询
gatheringApi.findById(id).then( response=>{
if(response.flag){
this.pojo=response.data
}
})
}

(3)在表格table中增加模板列 ,模板列中防止修改按钮,调用handleEdit方法

1
2
3
4
5
6
7
8
<el-table-column
fixed="right"
label="操作"
width="100">
<template slot-scope="scope">
<el-button @click="handleEdit(scope.row.id)" type="text" size="small">修改</el-button>
</template>
</el-table-column>

fixed=”right”的作用是定义此列为右固定列

slot-scope用于指定当前行的上下文。使用scope.row可以获取行对象

4.3 新增窗口表单清空

测试:我们在点开修改后,关闭窗口,再次新增打开窗口,会发现表单里依然有数据。这样显然是不行的。所以我们要在点击新增时清空表单。这个逻辑我们我们在handleEdit方法中实现

1
2
3
4
5
6
7
8
9
10
11
12
13
handleEdit(id){
this.dialogFormVisible=true//打开窗口
if(id!=''){
//调用查询
gatheringApi.findById(id).then( response=>{
if(response.flag){
this.pojo=response.data
}
})
}else{
this.pojo={}//清空表单
}
}

修改新增按钮,调用handleEdit方法时传递空字符串

1
<el-button type="primary" @click="handleEdit('')">新增</el-button>

4.4 保存修改

准备工作:修改easymock 接口 /gathering/gathering/{id} (PUT)

1
2
3
4
5
{
"flag": true,
"code": 20000,
'message': "修改成功"
}

代码:

(1)修改src/api/gathering.js,增加方法定义

1
2
3
4
5
6
7
update(id,pojo) {
return request({
url: `/gathering/gathering/${id}`,
method: 'put',
data: pojo
})
}

(2)修改src/views/table/gathering.vue的js脚本部分

增加属性id

1
2
3
4
5
6
data(){
return {
......
id:''//当前编辑的ID
}
}

修改handleEdit,增加

1
this.id=id

修改方法handleSave

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
handleSave(){
if(this.id!=''){//修改
gatheringApi.update(this.id,this.pojo).then(response=>{
alert(response.message)
if(response.flag){//如果成功
this.fetchData()//刷新列表
}
})
}else{//新增
gatheringApi.save(this.pojo).then(response=>{
alert(response.message)
if(response.flag){//如果成功
this.fetchData()//刷新列表
}
})
}
this.dialogFormVisible=false//关闭窗口
}

以上代码我们可以做一下优化:

修改src/api/gathering.js

1
2
3
4
5
6
7
8
9
10
update(id,pojo){
if(id == null || id == ''){
return save(pojo)
}
return request({
url: `/gathering/gathering/${id}`,
method: 'put',
data: pojo
})
}

修改src/views/table/gathering.vue的handleSave

1
2
3
4
5
6
7
8
9
handleSave(){       
gatheringApi.update(this.id,this.pojo).then(response=>{
alert(response.message)
if(response.flag){//如果成功
this.fetchData()//刷新列表
}
})
this.dialogFormVisible=false//关闭窗口
}

4.5 消息提示框

js原生的alert简直是丑爆了,有没有更漂亮的弹出框呀!当然有,用了elementUI提供了消息提示框,真是美呆了! alert(response.message)可以替换为以下代码:

1
2
3
4
this.$message({
message: response.message,
type: (response.flag?'success':'error')
});

$message详见官方文档:http://element-cn.eleme.io/#/zh-CN/component/message

你可以尝试着参照文档做出更丰富的效果哦~

5 活动管理-删除

5.1 需求分析

在表格的操作列增加”删除“按钮,点击删除按钮弹出提示框,确定后执行删除并刷新表格。

5.2 EasyMock接口

URL:gathering/:id Method: delete

1
2
3
4
5
{
"flag": true,
"code": 20000,
'message': "执行成功"
}

5.3 代码实现

(1)修改src/api/gathering.js,增加方法定义

1
2
3
4
5
6
deleteById(id){
return request({
url: `/gathering/gathering/${id}`,
method: 'delete'
})
}

(2)修改src/views/table/gathering.vue的js脚本部分

增加方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
 handleDelete(id){        
this.$confirm('确定要删除此纪录吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
gatheringApi.deleteById(id).then(response=>{
this.$message({
message: response.message,
type: (response.flag?'success':'error')
})
if(response.flag){
this.fetchData()// 刷新数据
}
})
}).catch(() => {
});
}

(3)修改src/views/table/gathering.vue ,在操作列增加删除按钮

1
<el-button @click="handleDelete(scope.row.id)" type="text" size="small">删除</el-button>

6 代码优化

我们看一下现在的API代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import request from "@/utils/request"
export default {
getList(){
return request(
{
url:'/gathering/gathering',
method:'get'
}
);
},
search(page,size,searchMap){
return request(
{
url: `/gathering/gathering/search/${page}/${size}`,
method: 'post',
data: searchMap
}
);
},
save(pojo){
return request({
url: `/gathering/gathering`,
method: 'post',
data: pojo
})
},
findById(id){
return request({
url: `/gathering/gathering/${id}`,
method: 'get'
})
},
update(id,pojo){
if(id==null || id==''){
return this.save(pojo)
}
return request({
url: `/gathering/gathering/${id}`,
method: 'put',
data: pojo
})
},
deleteById(id){
return request({
url: `/gathering/gathering/${id}`,
method: 'delete'
})
}
}

这里面的url地址都是一样的,如果以后地址发生了变化,需要逐个修改,不利于维护,所以我们这里把此字符串提取出来定义为常量,运用es6的模板字符串特性来进行拼接即可。

修改后代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import request from "@/utils/request"
const group_name='gathering'
const api_name='gathering'
export default {
getList(){
return request(
{
url:`/${group_name}/${api_name}`,
method:'get'
}
);
},
search(page,size,searchMap){
return request(
{
url: `/${group_name}/${api_name}/search/${page}/${size}`,
method: 'post',
data: searchMap
}
);
},
save(pojo){
return request({
url: `/${group_name}/${api_name}`,
method: 'post',
data: pojo
})
},
findById(id){
return request({
url: `/${group_name}/${api_name}/${id}`,
method: 'get'
})
},
update(id,pojo){
if(id==null || id==''){
return this.save(pojo)
}
return request({
url: `/${group_name}/${api_name}/${id}`,
method: 'put',
data: pojo
})
},
deleteById(id){
return request({
url: `/${group_name}/${api_name}/${id}`,
method: 'delete'
})
}
}

第4章 路由与状态管理

学习目标:

  • 理解路由在单页面工程中的作用
  • 掌握可搜索下拉框、复合型输入框等ElementUI的使用,完成招聘管理功能
  • 完成文章管理功能
  • 理解Vuex状态管理在工程中的作用

1 路由vue-router

1.1 什么是vue-router

​ vue-router就是vue官方提供的一个路由框架。使用 Vue.js ,我们已经可以通过组合组件来组成应用程序,当你要把 vue-router 添加进来,我们需要做的是,将组件(components)映射到路由(routes),然后告诉 vue-router 在哪里渲染它们。

1.2 快速入门

1.2.1 初始化工程

1
2
3
4
5
6
7
# 全局安装 vue-cli
npm install -g vue-cli
# 创建一个基于 webpack 模板的新项目
vue init webpack vue-router-demo
# 安装依赖,走你
cd vue-router-demo
npm run dev

1.2.2 路由定义

src/App.vue是我们的主界面,其中的<router-view/>标签用于显示各组件视图内容

src/router/index.js是定义路由的脚本 path是路径, name是名称 ,component是跳转的组件 。

(1)我们现在定义两个页面组件,存放在src/components下

list.vue

1
2
3
4
5
<template>
<div>
这是一个列表
</div>
</template>

about.vue

1
2
3
4
5
<template>
<div>
关于我们
</div>
</template>

(2)定义路由

修改src/router/index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import Vue from 'vue'
import Router from 'vue-router'
import HelloWorld from '@/components/HelloWorld'
import list from '@/components/list'
import about from '@/components/about'

Vue.use(Router)

export default new Router({
routes: [
{
path: '/',
name: 'HelloWorld',
component: HelloWorld
},
{
path: '/list',
name: 'List',
component: list
},
{
path: '/about',
name: 'About',
component: about
}
]
})

(3)放置跳转链接

修改src/app.vue ,添加链接

1
2
3
<router-link to="/" >首页</router-link>
<router-link to="/list">列表</router-link>
<router-link to="/about">关于</router-link>

通过router-link标签实现路由的跳转

router-link标签属性如下:

属性 类型 含义
to string | Location 表示目标路由的链接。当被点击后,内部会立刻把 to 的值传到 router.push(),所以这个值可以是一个字符串或者是描述目标位置的对象。
replace boolean 设置 replace 属性的话,当点击时,会调用 router.replace() 而不是 router.push(),于是导航后不会留下 history 记录。
append boolean 设置 append 属性后,则在当前(相对)路径前添加基路径。例如,我们从 /a 导航到一个相对路径 b,如果没有配置 append,则路径为 /b,如果配了,则为 /a/b

测试运行看是否可以跳转页面

1.3 深入了解

1.3.1 动态路由

我们经常会遇到这样的需求,有一个新闻列表,点击某一条进入新闻详细页,我们通常是传递新闻的ID给详细页,详细页根据ID进行处理。这时我们就会用到动态路由

一个『路径参数』使用冒号 : 标记。当匹配到一个路由时,参数值会被设置到 this.$route.params

看代码实现:

在src/components下创建item.vue

1
2
3
4
5
<template>
<div>
详细页 {{ $route.params.id }}
</div>
</template>

修改src/router/index.js,引入item组件

1
import item from '@/components/item'

添加路由设置

1
2
3
4
5
{
path: '/item/:id',
name: 'Item',
component: item
}

修改src/components/list.vue, 增加链接

1
2
3
4
5
6
7
8
<template>
<div>
这是一个列表
<router-link to="/item/1">新闻1</router-link>
<router-link to="/item/2">新闻2</router-link>
<router-link to="/item/3">新闻3</router-link>
</div>
</template>

1.3.2 嵌套路由

实际生活中的应用界面,通常由多层嵌套的组件组合而成。同样地,URL 中各段动态路径也按某种结构对应嵌套的各层组件,例如:

1
2
3
4
5
6
7
8
/about/address                        /about/linkman
+------------------+ +-----------------+
| About | | About |
| +--------------+ | | +-------------+ |
| | address | | +------------> | | linkman | |
| | | | | | | |
| +--------------+ | | +-------------+ |
+------------------+ +-----------------+

我们来看代码的实现

(1)在src/components下创建address.vue

1
2
3
4
5
<template>
<div>
地址:金燕龙
</div>
</template>

创建linkman.vue

1
2
3
4
5
<template>
<div>
联系人:小二黑
</div>
</template>

(2)修改src/router/index.js

引入linkman和address

1
2
import linkman from '@/components/linkman'
import address from '@/components/address'

配置嵌套路由:

1
2
3
4
5
6
7
8
9
{
path: '/about',
name: 'About',
component: about,
children: [
{path: 'linkman', component: linkman},
{path: 'address', component: address}
]
}

(3)修改src/components/about.vue

1
2
3
4
5
6
7
8
<template>
<div>
关于我们
<router-link to="/about/address" >地址</router-link>
<router-link to="/about/linkman" >联系人</router-link>
<router-view/>
</div>
</template>

1.4 十次方的路由代码

我们现在通过看提供的代码来了解

(1)src/router/index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
import Vue from 'vue'
import Router from 'vue-router'

Vue.use(Router)

/* Layout */
import Layout from '../views/layout/Layout'

export const constantRouterMap = [
{ path: '/login', component: () => import('@/views/login/index'), hidden: true },
{ path: '/404', component: () => import('@/views/404'), hidden: true },
{
path: '/',
component: Layout,
redirect: '/dashboard',
name: 'Dashboard',
hidden: true,
children: [{
path: 'dashboard',
component: () => import('@/views/dashboard/index')
}]
},
{
path: '/example',
component: Layout,
redirect: '/example/table',
name: 'Example',
meta: { title: 'Example', icon: 'example' },
children: [
{
path: 'table',
name: 'Table',
component: () => import('@/views/table/index'),
meta: { title: 'Table', icon: 'table' }
},
{
path: 'tree',
name: 'Tree',
component: () => import('@/views/tree/index'),
meta: { title: 'Tree', icon: 'tree' }
}
]
},
{
path: '/form',
component: Layout,
children: [
{
path: 'index',
name: 'Form',
component: () => import('@/views/form/index'),
meta: { title: 'Form', icon: 'form' }
}
]
},
{ path: '*', redirect: '/404', hidden: true }
]

export default new Router({
// mode: 'history', //后端支持可开
scrollBehavior: () => ({ y: 0 }),
routes: constantRouterMap
})

(2)src/main.js

1
2
3
4
5
6
7
8
9
.....
import router from './router'
.....
new Vue({
el: '#app',
router,
template: '<App/>',
components: { App }
})

2 招聘管理

2.1 准备工作

2.1.1 代码生成

(1)使用《黑马程序员代码生成器》,连接数据库tensquare_recruit

(2)将api 与vue页面拷贝到当前工程

2.1.2 路由设置

1
2
3
4
5
6
7
8
9
10
{
path: '/recruit',
component: Layout,
name: 'recruit',
meta: { title: '招聘管理', icon: 'example' },
children: [
{ path: 'enterprise', name: 'enterprise', component: () => import('@/views/table/enterprise'), meta: { title: '企业管理', icon: 'table' }},
{ path: 'recruit', name: 'recruit', component: () => import('@/views/table/recruit'), meta: { title: '招聘管理', icon: 'table' }}
]
},

2.1.3 easyMock接口导入

将swaggerAPI文档导入到easyMock中。

2.2 企业管理

2.2.1 企业简介(文本域)

修改src/views/table/enterprise.vue

1
2
3
<el-form-item label="企业简介">
<el-input v-model="pojo.summary" type="textarea" :rows="4"></el-input>
</el-form-item>

2.2.2 是否热门(开关)

修改src/views/table/enterprise.vue编辑窗口中是否热门部分

1
2
3
<el-form-item label="是否热门">
<el-switch placeholder="是否热门" on-text="" off-text="" active-value="1" inactive-value="0" v-model="pojo.ishot" ></el-switch>
</el-form-item>

2.3.3 网址输入(复合型输入框)

1
2
3
<el-input v-model="pojo.url" placeholder="请输入网址">
<template slot="prepend">http://</template>
</el-input>

参见elementUI官方文档 http://element-cn.eleme.io/#/zh-CN/component/upload (用户头像上传)实现Logo上传

(1)页面添加上传组件

1
2
3
4
5
6
7
8
9
<el-upload
class="avatar-uploader"
action="https://jsonplaceholder.typicode.com/posts/"
:show-file-list="false"
:on-success="handleAvatarSuccess"
:before-upload="beforeAvatarUpload">
<img v-if="imageUrl" :src="imageUrl" class="avatar">
<i v-else class="el-icon-plus avatar-uploader-icon"></i>
</el-upload>

action用于定义提交的服务器地址

show-file-list 是否显示已上传文件列表

before-upload 在上传之前被调用,用于判断图片类型和大小

on-success 在上传成功之后被调用,用于获取服务器上的文件名

(2) 添加样式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<style>
.avatar-uploader .el-upload {
border: 1px dashed #d9d9d9;
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
}
.avatar-uploader .el-upload:hover {
border-color: #409EFF;
}
.avatar-uploader-icon {
font-size: 28px;
color: #8c939d;
width: 100x;
height: 50px;
line-height: 50px;
text-align: center;
}
.avatar {
width: 100px;
height: 50px;
display: block;
}
</style>

(3)代码:

data添加属性

1
2
3
4
5
6
data() {
return {
......
imageUrl: ''
}
}

methods增加方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
handleAvatarSuccess(res, file) {
this.imageUrl = URL.createObjectURL(file.raw);
this.pojo.logo= this.imageUrl
},
beforeAvatarUpload(file) {
const isJPG = file.type === 'image/jpeg';
const isLt2M = file.size / 1024 / 1024 < 2;
if (!isJPG) {
this.$message.error('上传头像图片只能是 JPG 格式!');
}
if (!isLt2M) {
this.$message.error('上传头像图片大小不能超过 2MB!');
}
return isJPG && isLt2M;
}

2.3 招聘管理

2.3.1 任职方式(单选按钮)

修改src/views/table/recruit.vue

1
2
3
4
<el-form-item label="任职方式">
<el-radio v-model="pojo.type" label="1">全职</el-radio>
<el-radio v-model="pojo.type" label="2">兼职</el-radio>
</el-form-item>

2.3.2 选择企业(可搜索下拉选择框)

(1)修改src/views/table/recruit.vue 增加变量–企业列表

1
enterpriseList: []

(2)修改created()

1
2
3
4
5
6
7
8
created() {
this.fetchData()
enterprise.getList().then(response => { // 企业列表
if (response.flag === true) {
this.enterpriseList = response.data
}
})
},

(3)修改弹出窗口部分,将文本框替换为下拉框

1
2
3
4
5
6
7
8
9
10
<el-form-item label="企业ID">
<el-select v-model="pojo.eid" filterable placeholder="请选择">
<el-option
v-for="item in enterpriseList"
:key="item.id"
:label="item.name"
:value="item.id">
</el-option>
</el-select>
</el-form-item>

2.3.3 删除创建日期

创建日期是在后端自动生成的,所以要在弹出窗口中删除控件

2.3.4 状态(开关)

修改src/views/table/recruit.vue

1
2
3
<el-form-item label="状态">
<el-switch placeholder="是否热门" on-text="" off-text="" active-value="1" inactive-value="0" v-model="pojo.state" ></el-switch>
</el-form-item>

3 文章管理

3.1 准备工作

3.1.1 代码生成

(1)使用《黑马程序员代码生成器》,连接数据库tensquare_article

(2)将api 与vue页面拷贝到当前工程

3.1.2 路由设置

1
2
3
4
5
6
7
8
9
10
11
{
path: '/article',
component: Layout,
name: 'article',
meta: { title: '文章管理', icon: 'example' },
children: [
{ path: 'channel', name: 'channel', component: () => import('@/views/table/channel'), meta: { title: '频道管理', icon: 'table' }},
{ path: 'column', name: 'column', component: () => import('@/views/table/column'), meta: { title: '专栏审核', icon: 'table' }},
{ path: 'article', name: 'article', component: () => import('@/views/table/article'), meta: { title: '文章审核', icon: 'table' }}
]
}

3.1.3 easyMock接口导入

将swaggerAPI文档导入到easyMock中。

3.2 频道管理

修改频道状态为开关,代码略

3.3 专栏审核

3.3.1 修改easyMock数据

URL: article/column/search/{page}/{size}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"code": 20000,
"flag": true,
"message": "@string",
"data": {
"total": "@integer(60, 100)",
"rows|10": [{
"id": "@string",
"name": "@cword(10,20)",
"summary": "@cword(30,50)",
"userid": "@string",
"createtime": "@string",
"checktime": "@string",
"state|1": ['0', '1']
}]
}
}

3.3.2 待审核专栏列表

修改src/table/column.vue ,修改data变量的值

1
searchMap: {state:'0'},

这样在查询时就会携带状态为0的条件。

3.3.3 专栏审核

(1)修改src/api/column.js ,新增专栏审核方法

1
2
3
4
5
6
examine(id){
return request({
url: `/${group_name}/${api_name}/examine/${id}`,
method: 'put'
})
}

(2)增加方法定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
handleExamine(id){
this.$confirm('确定要审核此纪录吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
columnApi.examine(id).then(response => {
this.$message({ message: response.message, type: (response.flag ? 'success' : 'error') })
if (response.flag) {
this.fetchData() // 刷新数据
}
})
})
}

(3)审核按钮

1
<el-button @click="handleExamine(scope.row.id)" type="text" size="small">审核</el-button>

3.4 文章审核

3.4.1 修改easyMock接口

URL: /article/article/search/{page}/{size}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
{
"code": 20000,
"flag": true,
"message": "@string",
"data": {
"total": "@integer(60, 100)",
"rows|10": [{
"id": "@string",
"columnid": "@string",
"userid": "@string",
"title": "@cword(20,30)",
"content": "@string",
"image": "@string",
"createtime": "@string",
"updatetime": "@string",
"ispublic": "@string",
"istop": "@string",
"visits": "@string",
"thumbup": "@string",
"comment": "@string",
"state|1": ['1', '0'],
"channelid": "@string",
"url": "@string",
"type": "@string"
}]
}
}

3.4.2 待审核文章列表

修改src/table/article.vue ,修改data变量的值

1
searchMap: {state:'0'},

对查询表单进行精简

1
2
3
4
5
6
7
8
9
        <!--查询表单-->
<el-form :inline="true" class="demo-form-inline">
<el-form-item label="标题">
<el-input v-model="searchMap.title" placeholder="标题"></el-input></el-form-item>
<el-form-item label="文章正文">
<el-input v-model="searchMap.content" placeholder="文章正文"></el-input></el-form-item>
<el-button type="primary" @click="fetchData()">查询</el-button>
<el-button type="primary" icon="el-icon-circle-plus" @click="handleEdit('')">新增</el-button>
</el-form>

对表格列进行精简

1
2
3
4
5
6
7
8
9
10
11
12
<el-table-column prop="id" label="ID" width="80"></el-table-column>
<el-table-column prop="columnid" label="专栏ID" width="80"></el-table-column>
<el-table-column prop="userid" label="用户ID" width="80"></el-table-column>
<el-table-column prop="title" label="标题" width="80"></el-table-column>
<el-table-column prop="image" label="文章封面" width="80"></el-table-column>
<el-table-column prop="createtime" label="发表日期" width="80"></el-table-column>
<el-table-column prop="ispublic" label="是否公开" width="80"></el-table-column>
<el-table-column prop="istop" label="是否置顶" width="80"></el-table-column>
<el-table-column prop="state" label="审核状态" width="80"></el-table-column>
<el-table-column prop="channelid" label="所属频道" width="80"></el-table-column>
<el-table-column prop="url" label="URL" width="80"></el-table-column>
<el-table-column prop="type" label="类型" width="80"></el-table-column>

删除“新增”按钮

3.4.3 文章详情窗口

点击“详情”按钮打开窗口,显示标题和正文 v-html用于显示富文本内容。

1
2
3
4
5
6
<!--编辑窗口-->
<el-dialog title="详情" :visible.sync="dialogFormVisible" >
{{pojo.title}}
<hr>
<div v-html='pojo.content'></div>
</el-dialog>

3.4.4 文章审核与删除

(1)修改src/api/article.js,增加文章审核的方法

1
2
3
4
5
6
examine(id){
return request({
url: `/${group_name}/${api_name}/examine/${id}`,
method: 'put'
})
}

(2)修改src/views/table/article.vue,增加方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
handleExamine(id){
this.$confirm('确定要审核此纪录吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
articleApi.examine(id).then(response => {
this.$message({ message: response.message, type: (response.flag ? 'success' : 'error') })
if (response.flag) {
this.fetchData() // 刷新数据
}
this.dialogFormVisible = false
})
})
}

(3)新增审核和删除按钮

1
2
3
<el-button type="success" @click="handleExamine(pojo.id)" >审核通过</el-button>
<el-button type="danger" @click="handleDelete(pojo.id)" >删除</el-button>
<el-button @click="dialogFormVisible = false">关闭</el-button>

(4)删除方法添加代码

1
this.dialogFormVisible = false // 隐藏窗口

4 状态管理Vuex

我们经过测试会发现,用户登陆后可以访问其它页面的资源。未登录或退出登录后,再次访问资源会跳回到登陆页,这是如何实现的呢?长话短说,这是通过一种叫Vuex的技术来实现的。

4.1 Vuex简介

官方的解释: Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。

快速理解:每个组件都有它自己数据属性,封装在data()中,每个组件之间data是完全隔离的,是私有的。如果我们需要各个组件都能访问到数据数据,或是需要各个组件之间能互相交换数据,这就需要一个单独存储的区域存放公共属性。这就是状态管理所要解决的问题。

4.2 快速入门

4.2.1 工程搭建

1
2
3
4
5
6
# 创建一个基于 webpack 模板的新项目
vue init webpack vuexdemo
# 安装依赖,走你
cd vuexdemo
cnpm install --save vuex
npm run dev

4.2.2 读取状态值

每一个 Vuex 应用的核心就是 store(仓库)。“store”基本上就是一个容器,它包含着你的应用中大部分的**状态 (state)**。

实现步骤:

(1)在src下创建store,store下创建index.js

1
2
3
4
5
6
7
8
9
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
const store = new Vuex.Store({
state: {
count: 0
}
})
export default store

(2)修改main.js,引入和装载store

1
2
3
4
5
6
7
8
9
10
11
12
import Vue from 'vue'
import App from './App'
import router from './router'
import store from './store'
Vue.config.productionTip = false
new Vue({
el: '#app',
router,
store,
components: { App },
template: '<App/>'
})

(3)修改components\HelloWorld.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<template>
<div>
{{$store.state.count}}
<button @click="showCount">测试</button>
</div>
</template>
<script>
export default {
methods:{
showCount(){
console.log(this.$store.state.count)
}
}
}
</script>

4.2.3 改变状态值

你不能直接改变 store 中的状态。改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化,从而让我们能够实现一些工具帮助我们更好地了解我们的应用。

(1)修改store/index.js ,增加mutation定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
const store=new Vuex.Store({
state: {
count: 0
},
mutations: {
increment(state) {
state.count++
}
}
})
export default store

(2)修改components\HelloWorld.vue ,调用mutation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<template>
<div>
{{$store.state.count}}
<button @click="addCount">测试</button>
</div>
</template>
<script>
export default {
methods:{
addCount(){
this.$store.commit('increment')
}
}
}
</script>

测试: 运行工程,点击测试按钮,我们会看到控制台和页面输出递增的数字

4.2.4 状态值共享测试

如果是另外一个页面,能否读取到刚才我在HelloWorld中操作的状态值呢?我们接下来就做一个测试

(1)在components下创建show.vue

1
2
3
4
5
<template>
<div>
show: {{$store.state.count}}
</div>
</template>

(2)修改路由设置 router/index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import Vue from 'vue'
import Router from 'vue-router'
import HelloWorld from '@/components/HelloWorld'
import Show from '@/components/Show'
Vue.use(Router)
export default new Router({
routes: [
{
path: '/',
name: 'HelloWorld',
component: HelloWorld
},
{
path: '/show',
name: 'Show',
component: Show
}
]
})

测试: 在HelloWorld页面点击按钮使状态值增长,然后再进入show页面查看状态值

4.2.5 提交载荷

所谓载荷(payload)就是 向  store.commit  传入额外的参数。

(1)修改store下的index.js

1
2
3
4
5
6
7
......
mutations: {
increment (state,x) {
state.count += x
}
}
......

(2)修改HelloWorld.vue

1
2
3
4
5
6
......
addCount(){
this.$store.commit('increment',10)
console.log(this.$store.state.count)
}
......

4.2.6 Action

Action 类似于 mutation,不同在于:

  • Action 提交的是 mutation,而不是直接变更状态。
  • Action 可以包含任意异步操作。

我们现在使用 Action 来封装increment

(1)修改store/index.js

1
2
3
4
5
6
7
8
const store = new Vuex.Store({
.....
actions: {
increment (context){
context.commit('increment',10)
}
}
})

(2)修改show.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<template>
<div>
show: {{$store.state.count}}

<button @click="addCount">测试</button>
</div>
</template>
<script>
export default {
methods:{
addCount(){
this.$store.dispatch('increment')
console.log(this.$store.state.count)
}
}
}
</script>

我们使用dispatch来调用action , Action也同样支持载荷

4.2.7 派生属性Getter

有时候我们需要从 store 中的 state 中派生出一些状态,例如我们在上例代码的基础上,我们增加一个叫 remark的属性,如果count属性值小于50则remark为加油,大于等于50小于100则remark为你真棒,大于100则remark的值为你是大神. 这时我们就需要用到getter为我们解决。

(1)修改store/index.js ,增加getters定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const store = new Vuex.Store({
......
getters: {
remark(state){
if(state.count<50){
return '加油'
}else if( state.count<100){
return '你真棒'
}else{
return '你是大神'
}
}
}
.......
})

Getter 接受 state 作为其第一个参数,也可以接受其他 getter 作为第二个参数

(2)修改HelloWorld.vue 显示派生属性的值

1
{{$store.getters.remark}}

4.3 模块化

4.3.1 Module

由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。

为了解决以上问题,Vuex 允许我们将 store 分割成模块(module)。每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块——从上至下进行同样方式的分割 .参见以下代码模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const moduleA = {
state: { ... },
mutations: { ... },
actions: { ... },
getters: { ... }
}

const moduleB = {
state: { ... },
mutations: { ... },
actions: { ... }
}

const store = new Vuex.Store({
modules: {
a: moduleA,
b: moduleB
}
})

store.state.a // -> moduleA 的状态
store.state.b // -> moduleB 的状态

我们现在就对工程按模块化进行改造

(1)修改store/index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
const moduleA ={
state: {
count: 0
},
getters: {
remark(state){
if(state.count<50){
return '加油'
}else if( state.count<100){
return '你真棒'
}else{
return '你是大神'
}
}
},
mutations: {
increment (state,x) {
state.count += x
}
},
actions: {
increment (context){
context.commit('increment',10)
}
}
}
const store = new Vuex.Store({
modules: {
a:moduleA
}
})
export default store

(2)修改HelloWorld.vue和show.vue

1
{{$store.state.a.count}}

4.3.2 标准工程结构

如果所有的状态都写在一个js中,这个js必定会很臃肿,所以Vuex建议你按以下代码结构来构建工程

1
2
3
4
5
6
7
8
9
10
11
12
13
├── index.html
├── main.js
├── api
│ └── ... # 抽取出API请求
├── components
│ ├── App.vue
│ └── ...
└── store
├── index.js # 我们组装模块并导出 store 的地方
├── getters.js
└── modules
├── a.js # A模块
└── b.js # B模块

我们现在就按照上面的结构,重新整理以下我们的代码:

(1)store下创建modules文件夹,文件夹下创建a.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export default {
state: {
count: 0
},
mutations: {
increment (state,x) {
state.count += x
}
},
actions: {
increment (context){
context.commit('increment',10)
}
}
}

(2)store下创建getters.js

1
2
3
4
5
6
7
8
9
10
11
12
export default {
remark: state => {
if(state.a.count<50){
return '加油'
}else if( state.a.count<100){
return '你真棒'
}else{
return '你是大神'
}
},
count: state=> state.a.count
}

(3)修改store/index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import Vue from 'vue'
import Vuex from 'vuex'
import a from './modules/a'
import getters from './getters'
Vue.use(Vuex)

const store = new Vuex.Store({
getters,
modules: {
a
}
})

export default store

(4)修改HelloWorld.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<template>
<div>
{{$store.getters.count}} {{$store.getters.remark}}
<button @click="addCount">测试</button>
</div>
</template>

<script>
export default {
methods:{
addCount(){
this.$store.commit('increment',10)
console.log(this.$store.getters.count)
}
}
}
</script>

4.4 十次方后台登陆(课下阅读)

脚手架已经实现了登陆部分的代码,只需学员课下阅读,不需要编写,理解实现思路即可。

4.4.1登陆

(1)src/api下创建login.js

1
2
3
4
5
6
7
8
9
10
11
12
import request from '@/utils/request'

export function login(username, password) {
return request({
url: '/user/login',
method: 'post',
data: {
username,
password
}
})
}

(2)src下建立store文件夹,store下创建modules,modules下创建user.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import { login, logout, getInfo } from '@/api/login'
import { getToken, setToken, removeToken } from '@/utils/auth'

const user = {
state: {
token: getToken(),
name: '',
avatar: '',
roles: []
},

mutations: {
SET_TOKEN: (state, token) => {
state.token = token
},
SET_NAME: (state, name) => {
state.name = name
},
SET_AVATAR: (state, avatar) => {
state.avatar = avatar
},
SET_ROLES: (state, roles) => {
state.roles = roles
}
},

actions: {
// 登录
Login({ commit }, userInfo) {
const username = userInfo.username.trim()
return new Promise((resolve, reject) => {
login(username, userInfo.password).then(response => {
const data = response.data
setToken(data.token)
commit('SET_TOKEN', data.token)
resolve()
}).catch(error => {
reject(error)
})
})
}
}
}
export default user

(3)store下创建getters.js

1
2
3
4
5
6
7
const getters = {
token: state => state.user.token,
avatar: state => state.user.avatar,
name: state => state.user.name,
roles: state => state.user.roles
}
export default getters

(4)store下创建index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import Vue from 'vue'
import Vuex from 'vuex'
import user from './modules/user'
import getters from './getters'

Vue.use(Vuex)

const store = new Vuex.Store({
modules: {
user
},
getters
})

export default store

(5)修改src下的main.js,引入store

1
2
3
4
5
6
7
8
9
import store from './store'
......
new Vue({
el: '#app',
router,
store,
template: '<App/>',
components: { App }
})

(6)构建登陆页面.在src/views/login/index.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
<template>
<div class="login-container">
<el-form autoComplete="on" :model="loginForm" :rules="loginRules" ref="loginForm" label-position="left" label-width="0px"
class="card-box login-form">
<h3 class="title">十次方管理后台</h3>
<el-form-item prop="username">
<span class="svg-container svg-container_login">
<svg-icon icon-class="user" />
</span>
<el-input name="username" type="text" v-model="loginForm.username" autoComplete="on" placeholder="username" />
</el-form-item>
<el-form-item prop="password">
<span class="svg-container">
<svg-icon icon-class="password"></svg-icon>
</span>
<el-input name="password" :type="pwdType" @keyup.enter.native="handleLogin" v-model="loginForm.password" autoComplete="on"
placeholder="password"></el-input>
<span class="show-pwd" @click="showPwd"><svg-icon icon-class="eye" /></span>
</el-form-item>
<el-form-item>
<el-button type="primary" style="width:100%;" :loading="loading" @click.native.prevent="handleLogin">
Sign in
</el-button>
</el-form-item>
<div class="tips">
<span style="margin-right:20px;">username: admin</span>
<span> password: admin</span>
</div>
</el-form>
</div>
</template>
<script>
import { isvalidUsername } from '@/utils/validate'
export default {
name: 'login',
data() {
const validateUsername = (rule, value, callback) => {
if (!isvalidUsername(value)) {
callback(new Error('请输入正确的用户名'))
} else {
callback()
}
}
const validatePass = (rule, value, callback) => {
if (value.length < 5) {
callback(new Error('密码不能小于5位'))
} else {
callback()
}
}
return {
loginForm: {
username: 'admin',
password: 'admin'
},
loginRules: {
username: [{ required: true, trigger: 'blur', validator: validateUsername }],
password: [{ required: true, trigger: 'blur', validator: validatePass }]
},
loading: false,
pwdType: 'password'
}
},
methods: {
showPwd() {
if (this.pwdType === 'password') {
this.pwdType = ''
} else {
this.pwdType = 'password'
}
},
handleLogin() {
this.$refs.loginForm.validate(valid => {
if (valid) {
this.loading = true
this.$store.dispatch('Login', this.loginForm).then(() => {
this.loading = false
this.$router.push({ path: '/' })
}).catch(() => {
this.loading = false
})
} else {
console.log('error submit!!')
return false
}
})
}
}
}
</script>
....样式略

4.4.2获取用户登陆信息

(1)修改src/api/login.js

1
2
3
4
5
6
7
export function getInfo(token) {
return request({
url: '/user/info',
method: 'get',
params: { token }
})
}

(2)修改src/store/modules/user.js,增加action方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 获取用户信息
GetInfo({ commit, state }) {
return new Promise((resolve, reject) => {
getInfo(state.token).then(response => {
const data = response.data
commit('SET_ROLES', data.roles)
commit('SET_NAME', data.name)
commit('SET_AVATAR', data.avatar)
resolve(response)
}).catch(error => {
reject(error)
})
})
},

(3)在src下创建permission.js ,实现用户信息的拉取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import router from './router'
import store from './store'
import NProgress from 'nprogress' // Progress 进度条
import 'nprogress/nprogress.css'// Progress 进度条样式
import { Message } from 'element-ui'
import { getToken } from '@/utils/auth' // 验权

const whiteList = ['/login'] // 不重定向白名单
router.beforeEach((to, from, next) => {
NProgress.start()
if (getToken()) {
if (to.path === '/login') {
next({ path: '/' })
} else {
if (store.getters.roles.length === 0) {
store.dispatch('GetInfo').then(res => { // 拉取用户信息
next()
}).catch(() => {
store.dispatch('FedLogOut').then(() => {
Message.error('验证失败,请重新登录')
next({ path: '/login' })
})
})
} else {
next()
}
}
} else {
if (whiteList.indexOf(to.path) !== -1) {
next()
} else {
next('/login')
NProgress.done()
}
}
})

router.afterEach(() => {
NProgress.done() // 结束Progress
})

(4)在顶部导航栏中实现头像的读取。

修改src\views\layout\components\Navbar.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<script>
import { mapGetters } from 'vuex'
import Breadcrumb from '@/components/Breadcrumb'
import Hamburger from '@/components/Hamburger'

export default {
components: {
Breadcrumb,
Hamburger
},
computed: {
...mapGetters([
'sidebar',
'avatar'
])
},
methods: {
toggleSideBar() {
this.$store.dispatch('ToggleSideBar')
},
logout() {
this.$store.dispatch('LogOut').then(() => {
location.reload() // 为了重新实例化vue-router对象 避免bug
})
}
}
}
</script>

读取头像

1
2
3
4
<div class="avatar-wrapper">
<img class="user-avatar" :src="avatar+'?imageView2/1/w/80/h/80'">
<i class="el-icon-caret-bottom"></i>
</div>

4.4.3退出登录

(1)修改src/api/login.js

1
2
3
4
5
6
export function logout() {
return request({
url: '/user/logout',
method: 'post'
})
}

(2)修改src/store/modules/user.js,增加action方法

1
2
3
4
5
6
7
8
9
10
11
12
13
// 登出
LogOut({ commit, state }) {
return new Promise((resolve, reject) => {
logout(state.token).then(() => {
commit('SET_TOKEN', '')
commit('SET_ROLES', [])
removeToken()
resolve()
}).catch(error => {
reject(error)
})
})
},

(3)在顶部导航栏中实现退出登录

1
2
3
4
5
logout() {
this.$store.dispatch('LogOut').then(() => {
location.reload() // 为了重新实例化vue-router对象 避免bug
})
}
1
2
3
<el-dropdown-item divided>
<span @click="logout" style="display:block;">退出登录</span>
</el-dropdown-item>

第5章 网站前台-活动与招聘

学习目标:

  • 掌握NUXT框架的基本使用方法
  • 完成十次方网站前台的搭建
  • 完成十次方网站前台活动模块的功能
  • 完成十次方网站前台招聘模块的功能

1 服务端渲染技术NUXT

1.1 什么是服务端渲染

​ 服务端渲染又称SSR (Server Side Render)是在服务端完成页面的内容,而不是在客户端通过AJAX获取数据。

​ 与传统 SPA(Single-Page Application - 单页应用程序)相比,服务器端渲染(SSR)的优势主要在于:更好的 SEO,由于搜索引擎爬虫抓取工具可以直接查看完全渲染的页面。

​ 请注意,截至目前,Google 和 Bing 可以很好对同步 JavaScript 应用程序进行索引。在这里,同步是关键。如果你的应用程序初始展示 loading 菊花图,然后通过 Ajax 获取内容,抓取工具并不会等待异步完成后再行抓取页面内容。也就是说,如果 SEO 对你的站点至关重要,而你的页面又是异步获取内容,则你可能需要服务器端渲染(SSR)解决此问题。

​ 更快的内容到达时间(time-to-content),特别是对于缓慢的网络情况或运行缓慢的设备。无需等待所有的 JavaScript 都完成下载并执行,才显示服务器渲染的标记,所以你的用户将会更快速地看到完整渲染的页面。通常可以产生更好的用户体验,并且对于那些「内容到达时间(time-to-content)与转化率直接相关」的应用程序而言,服务器端渲染(SSR)至关重要。

1.2 什么是NUXT

Nuxt.js 是一个基于 Vue.js 的轻量级应用框架,可用来创建服务端渲染 (SSR) 应用,也可充当静态站点引擎生成静态站点应用,具有优雅的代码结构分层和热加载等特性。

官网网站:

https://zh.nuxtjs.org/

1.3 NUXT环境搭建

(1)我们从网站上下载模板的压缩包  starter-template-master.zip 解压,修改template目录目录的package.json中的名称

(2)在命令提示符下进入该目录下的template目录

(3)安装依赖

1
cnpm install

(4)修改package.json

1
2
3
......
"name": "tensquare",
......

(5)修改nuxt.config.js

1
2
3
......
title: '十次方'
......

(6)测试运行

1
npm run dev

1.4 NUXT目录结构

(1)资源目录 assets

 用于组织未编译的静态资源如 LESS、SASS 或 JavaScript。

(2)组件目录 components

用于组织应用的 Vue.js 组件。Nuxt.js 不会扩展增强该目录下 Vue.js 组件,即这些组件不会像页面组件那样有 asyncData 方法的特性。

(3)布局目录 layouts

用于组织应用的布局组件。

(4)页面目录 pages

用于组织应用的路由及视图。Nuxt.js 框架读取该目录下所有的 .vue 文件并自动生成对应的路由配置。

(5)插件目录 plugins

用于组织那些需要在 根vue.js应用 实例化之前需要运行的 Javascript 插件。

(6)nuxt.config.js 文件

nuxt.config.js 文件用于组织Nuxt.js 应用的个性化配置,以便覆盖默认配置。

1.5 NUXT快速入门

1.5.1 定义布局

我们通常的网站头部和尾部都是相同的,我们可以把头部为尾部提取出来,形成布局页

修改layouts目录下default.vue

1
2
3
4
5
6
7
<template>
<div>
<header>NUXT入门小Demo</header>
<nuxt/>
<footer>--黑马程序员--</footer>
</div>
</template>

<**nuxt**/>为内容的区域

1.5.2 页面路由

在page目录创建文件夹

image-20231003234048437 

recruit目录创建index.vue

1
2
3
4
5
<template>
<div>
招聘列表
</div>
</template>

gathering目录创建index.vue

1
2
3
4
5
<template>
<div>
活动列表
</div>
</template>

NUXT的路由是根据目录自动生成的,无需手写。

修改default.vue,header中添加导航链接

1
2
3
<router-link to="/">首页</router-link>
<router-link to="/recruit">招聘</router-link>
<router-link to="/gathering">活动</router-link>

点击导航链接,测试路由效果

1.5.3 数据渲染

(1)安装axios,用于异步获取数据

1
cnpm install axios --save

(2)修改gathering目录的index.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<template>
  <div>
    活动列表
<div v-for="(item,index) in items" :key="index" >{{item.name}}</div>
  </div>
</template>
<script>
import axios from 'axios'
export default {
asyncData () {
return axios.get('http://192.168.184.133:7300/mock/5af314a4c612520d0d7650c7/gathering/gathering')
.then( res => {
return { items: res.data.data }
})
}
}
</script>

asyncData是用于异步加载数据的方法

1.5.4 动态路由

如果我们需要根据ID查询活动详情,就需要使用动态路由。NUXT的动态路由是以下划线开头的vue文件,参数名为下划线后边的文件名

创建pages/gathering/item/_id.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<template>
  <div>
    活动详情
{{item.id}}
<hr>
{{item.name}}

  </div>
</template>
<script>
import axios from 'axios'
export default {
asyncData( {params} ){
//params.id
return axios.get(`http://192.168.184.133:7300/mock/5af314a4c612520d0d7650c7/gathering/gathering/${params.id}`).then(
res =>{
return {item: res.data.data}
}
)
}
}
</script>

我们在地址栏输入 http://localhost:3000/gathering/item/1 即可看到运行结果

在活动列表页点击链接进入详情页

1
2
3
4
    活动列表
<div v-for="(item,index) in items" :key="index">
<nuxt-link :to="'/gathering/item/'+item.id">{{item.name}}</nuxt-link>
</div>

目前 nuxt-link 的作用和 router-link 一致 ,都是进行路由的跳转。

2 十次方网站前台搭建

2.1 网站整体布局

(1)拷贝静态资源:将静态原型中的css、img、plugins目录拷贝至assets目录下 。

(2)我们参照静态原型中的activiti-index.html页面来编写网站的通用布局,即网站的头部和尾部

修改layouts下的default.vue,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
<template>
<div>
<header>
<div class="activity-head">
<div class="wrapper">
<div class="sui-navbar">
<div class="navbar-inner">
<a href="index-logined.html" class="sui-brand"><img src="~/assets/img/asset-logo-black.png" alt="社交"/></a>
<ul class="sui-nav">
<li class="active"><a href="~/assets/headline-logined.html">头条</a></li>
<li><a href="~/assets/qa-logined.html" >问答</a></li>
<li><a href="~/assets/activity-index.html" >活动</a></li>
<li><a href="~/assets/makeFriends-index.html" >交友</a></li>
<li><a href="~/assets/spit-index.html" >吐槽</a></li>
<li><a href="~/assets/recruit-index.html" >招聘</a></li>
</ul>
<form class="sui-form sui-form pull-left">
<input type="text" placeholder="输入关键词...">
<span class="btn-search fa fa-search"></span>
</form>
<div class="sui-nav pull-right info">
<li><a href="~/assets/other-notice.html" target="_blank" class="notice">通知</a></li>
<li class="hover">
<span class="fa fa-plus "></span>
<ul class="hoverinfo">
<li><i class="fa fa-share-alt" aria-hidden="true"></i> <a href="~/assets/headline-submit.html">去分享</a></li>
<li><i class="fa fa-question-circle" aria-hidden="true"></i> <a href="~/assets/qa-submit.html" target="_blank">提问题</a></li>
<li><i class="fa fa-comments" aria-hidden="true"></i><a href="~/assets/spit-submit.html" target="_blank">去吐槽</a></li>
<li><i class="fa fa-heartbeat" aria-hidden="true"></i> <a href="~/assets/makeFriends-submit.html" target="_blank">发约会</a></li>
</ul>
</li>
<li><a href="~/assets/person-homepage.html" target="_blank" class="homego"><img src="~/assets/img/widget-photo.png" alt="用户头像"></a></li>
</div>
</div>
</div>
</div>
</div>
</header>
<nuxt/>
<footer>
<!--底部版权-->
<div class="footer">
<div class="wrapper">
<div class="footer-bottom">
<div class="link">
<dl>
<dt>网站相关</dt>
<dd>关于我们</dd>
<dd>服务条款</dd>
<dd>帮助中心</dd>
<dd>编辑器语法</dd>
</dl>
<dl>
<dt>常用链接</dt>
<dd>传智播客</dd>
<dd>传智论坛</dd>
</dl>
<dl>
<dt>联系我们</dt>
<dd>联系我们</dd>
<dd>加入我们</dd>
<dd>建议反馈</dd>
</dl>
<dl>
<dt>关注我们</dt>
<dd>微博</dd>
<dd>twitter</dd>
</dl>

<div class="xuke">
<h3>内容许可</h3>
<p>除特别说明外,用户内容均采用知识共享署名-非商业性使用-禁止演绎4.0 国际许可协议 (CC BY-NC-ND 4.0) 进行许可</p>
<p>本站由 传智研究院 提供更新服务</p>
</div>
</div>


<p class="Copyright">Copyright © 2017 传智问答社区 当前版本 0.0.1</p>
</div>
</div>
</div>

</footer>
</div>
</template>
<script>
import '~/assets/plugins/normalize-css/normalize.css'
import '~/assets/plugins/yui/cssgrids-min.css'
import '~/assets/plugins/sui/sui.min.css'
import '~/assets/plugins/sui/sui-append.min.css'
import '~/assets/plugins/font-awesome/css/font-awesome.min.css'
import '~/assets/css/widget-base.css'
import '~/assets/css/widget-head-foot.css'
export default {
}
</script>

2.2 头条页面

修改pages/index.vue ,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
<template>
<div class="sui-container wrapper">
<div class="sj-content">
<div class="left-nav">
<div class="float-nav" id="float-nav">
<ul class="sui-nav nav-tabs nav-xlarge tab-navbar tab-vertical">
<li class="active"><a>热门</a></li>
<li><a href="#">牛人专区</a></li>
<li><a href="#">机器学习</a></li>
<li><a href="#">后端开发</a></li>
<li><a href="#">人工智能</a></li>
<li><a href="#">虚拟现实</a></li>
<li><a href="#">商业预测</a></li>
<li><a href="#">前端开发</a></li>
</ul>
</div>
</div>
<div class="right-content">
<div class="fl middle">
<div class="carousel">
<div id="myCarousel" data-ride="carousel" data-interval="4000" class="sui-carousel slide">
<ol class="carousel-indicators">
<li data-target="#myCarousel" data-slide-to="0" class="active"></li>
<li data-target="#myCarousel" data-slide-to="1"></li>
<li data-target="#myCarousel" data-slide-to="2"></li>
</ol>
<div class="carousel-inner">
<div class="active item">
<img src="~/assets/img/widget-banner01.png" />
</div>
<div class="item">
<img src="~/assets/img/widget-banner02.png" />
</div>
<div class="item">
<img src="~/assets/img/widget-banner01.png" />
</div>
</div>
</div>
</div>
<div class="data-list">
<ul class="headline fixed" id="headline">
<li class="headline-item">
<div class="fl indexImg">
<img src="" />
</div>
<div class="words">
<h3>Drive.ai融资5000万吴恩达加入董事会 <span><img src="" class="vip" /></span></h3>
<h5 class="author">
<div class="fl">
<span class="authorName"> <img src="~/assets/img/widget-photo.png" alt="" /> 玻璃通 </span>
<span>6月22日 12:01</span>
</div>
<div class="fr attention">
<span class="attentionText">关注</span>
<span class="beforeclose"> <i class="fa fa-times delete" aria-hidden="true"></i> <i class="nolike">不感兴趣</i> </span>
</div>
<div class="clearfix"></div> </h5>
</div> <p class="content">滴滴与360都开源了各自的插件化框架,VirtualAPK与RePlugin,作为一个插件化方面的狂热研究者,在周末就迫不及待的下载了Virtualapk框架来进行研究,本篇博客带的……</p> </li>
<li class="headline-item">
<div class="fl indexImg">
<img src="" />
</div>
<div class="words">
<h3>Drive.ai融资5000万吴恩达加入董事会 <span><img src="~/assets/img/widget-vip.png" class="vip" /></span></h3>
<h5 class="author">
<div class="fl">
<span class="authorName"> <img src="~/assets/img/widget-photo.png" alt="" /> 玻璃通 </span>
<span>6月12日 13:34</span>
</div>
<div class="fr attention">
<span class="attentionText">关注</span>
<span class="beforeclose"> <i class="fa fa-times delete" aria-hidden="true"></i> <i class="nolike">不感兴趣</i> </span>
</div>
<div class="clearfix"></div> </h5>
</div> <p class="content">滴滴与360都开源了各自的插件化框架,VirtualAPK与RePlugin,作为一个插件化方面的狂热研究者,在周末就迫不及待的下载了Virtualapk框架来进行研究,本篇博客带的……</p> </li>
<li class="headline-item">
<div class="fl indexImg">
<img src="~/assets/img/widget-list01.png" />
</div>
<div class="words">
<h3>Drive.ai融资5000万吴恩达加入董事会 <span><img src="~/assets/img/widget-vip.png" class="vip" /></span></h3>
<h5 class="author">
<div class="fl">
<span class="authorName"> <img src="~/assets/img/widget-photo.png" alt="" /> 玻璃通 </span>
<span>6月12日 13:34</span>
</div>
<div class="fr attention">
<span class="attentionText">关注</span>
<span class="beforeclose"> <i class="fa fa-times delete" aria-hidden="true"></i> <i class="nolike">不感兴趣</i> </span>
</div>
<div class="clearfix"></div> </h5>
</div> <p class="content"></p> </li>
<li class="headline-item">
<div class="fl indexImg">
<img src="" />
</div>
<div class="words">
<h3>Drive.ai融资5000万吴恩达加入董事会 <span><img src="~/assets/img/widget-vip.png" class="vip" /></span></h3>
<h5 class="author">
<div class="fl">
<span class="authorName"> <img src="~/assets/img/widget-photo.png" alt="" /> 玻璃通 </span>
<span>6月7日 10:34</span>
</div>
<div class="fr attention">
<span class="attentionText">关注</span>
<span class="beforeclose"> <i class="fa fa-times delete" aria-hidden="true"></i> <i class="nolike">不感兴趣</i> </span>
</div>
<div class="clearfix"></div> </h5>
</div> <p class="content">滴滴与360都开源了各自的插件化框架,VirtualAPK与RePlugin,作为一个插件化方面的狂热研究者,在周末就迫不及待的下载了Virtualapk框架来进行研究,本篇博客带的……</p> </li>
<li class="headline-item">
<div class="fl indexImg">
<img src="~/assets/img/widget-list02.png" />
</div>
<div class="words">
<h3>Drive.ai融资5000万吴恩达加入董事会 <span><img src="~/assets/img/widget-vip.png" class="vip" /></span></h3>
<h5 class="author">
<div class="fl">
<span class="authorName"> <img src="~/assets/img/widget-photo.png" alt="" /> 玻璃通 </span>
<span>6月12日 13:34</span>
</div>
<div class="fr attention">
<span class="attentionText">关注</span>
<span class="beforeclose"> <i class="fa fa-times delete" aria-hidden="true"></i> <i class="nolike">不感兴趣</i> </span>
</div>
<div class="clearfix"></div> </h5>
</div> <p class="content"></p> </li>
<li class="headline-item">
<div class="fl indexImg">
<img src="" />
</div>
<div class="words">
<h3>Drive.ai融资5000万吴恩达加入董事会 <span><img src="~/assets/img/widget-vip.png" class="vip" /></span></h3>
<h5 class="author">
<div class="fl">
<span class="authorName"> <img src="~/assets/img/widget-photo.png" alt="" /> 玻璃通 </span>
<span>6月7日 10:34</span>
</div>
<div class="fr attention">
<span class="attentionText">关注</span>
<span class="beforeclose"> <i class="fa fa-times delete" aria-hidden="true"></i> <i class="nolike">不感兴趣</i> </span>
</div>
<div class="clearfix"></div> </h5>
</div> <p class="content">滴滴与360都开源了各自的插件化框架,VirtualAPK与RePlugin,作为一个插件化方面的狂热研究者,在周末就迫不及待的下载了Virtualapk框架来进行研究,本篇博客带的……</p> </li>
</ul>
<div class="stop">
<a href="javascript:;">32分钟前看到这里,点击刷新 <i class="fa fa-refresh" aria-hidden="true"></i></a>
</div>
<ul id="data-list-down" class="headline loading">
<li class="headline-item">
<div class="fl indexImg">
<img src="" />
</div>
<div class="words">
<h3>Drive.ai融资5000万吴恩达加入董事会 <span><img src="" class="vip" /></span></h3>
<h5 class="author">
<div class="fl">
<span class="authorName"> <img src="~/assets/img/widget-photo.png" alt="" /> 玻璃通 </span>
<span>6月22日 12:01</span>
</div>
<div class="fr attention">
<span class="attentionText">关注</span>
<span class="beforeclose"> <i class="fa fa-times delete" aria-hidden="true"></i> <i class="nolike">不感兴趣</i> </span>
</div>
<div class="clearfix"></div> </h5>
</div> <p class="content">滴滴与360都开源了各自的插件化框架,VirtualAPK与RePlugin,作为一个插件化方面的狂热研究者,在周末就迫不及待的下载了Virtualapk框架来进行研究,本篇博客带的……</p> </li>
<li class="headline-item">
<div class="fl indexImg">
<img src="" />
</div>
<div class="words">
<h3>Drive.ai融资5000万吴恩达加入董事会 <span><img src="~/assets/img/widget-vip.png" class="vip" /></span></h3>
<h5 class="author">
<div class="fl">
<span class="authorName"> <img src="~/assets/img/widget-photo.png" alt="" /> 玻璃通 </span>
<span>6月12日 13:34</span>
</div>
<div class="fr attention">
<span class="attentionText">关注</span>
<span class="beforeclose"> <i class="fa fa-times delete" aria-hidden="true"></i> <i class="nolike">不感兴趣</i> </span>
</div>
<div class="clearfix"></div> </h5>
</div> <p class="content">滴滴与360都开源了各自的插件化框架,VirtualAPK与RePlugin,作为一个插件化方面的狂热研究者,在周末就迫不及待的下载了Virtualapk框架来进行研究,本篇博客带的……</p> </li>
<li class="headline-item">
<div class="fl indexImg">
<img src="~/assets/img/widget-list01.png" />
</div>
<div class="words">
<h3>Drive.ai融资5000万吴恩达加入董事会 <span><img src="~/assets/img/widget-vip.png" class="vip" /></span></h3>
<h5 class="author">
<div class="fl">
<span class="authorName"> <img src="~/assets/img/widget-photo.png" alt="" /> 玻璃通 </span>
<span>6月12日 13:34</span>
</div>
<div class="fr attention">
<span class="attentionText">关注</span>
<span class="beforeclose"> <i class="fa fa-times delete" aria-hidden="true"></i> <i class="nolike">不感兴趣</i> </span>
</div>
<div class="clearfix"></div> </h5>
</div> <p class="content"></p> </li>
<li class="headline-item">
<div class="fl indexImg">
<img src="" />
</div>
<div class="words">
<h3>Drive.ai融资5000万吴恩达加入董事会 <span><img src="~/assets/img/widget-vip.png" class="vip" /></span></h3>
<h5 class="author">
<div class="fl">
<span class="authorName"> <img src="~/assets/img/widget-photo.png" alt="" /> 玻璃通 </span>
<span>6月7日 10:34</span>
</div>
<div class="fr attention">
<span class="attentionText">关注</span>
<span class="beforeclose"> <i class="fa fa-times delete" aria-hidden="true"></i> <i class="nolike">不感兴趣</i> </span>
</div>
<div class="clearfix"></div> </h5>
</div> <p class="content">滴滴与360都开源了各自的插件化框架,VirtualAPK与RePlugin,作为一个插件化方面的狂热研究者,在周末就迫不及待的下载了Virtualapk框架来进行研究,本篇博客带的……</p> </li>
<li class="headline-item">
<div class="fl indexImg">
<img src="~/assets/img/widget-list02.png" />
</div>
<div class="words">
<h3>Drive.ai融资5000万吴恩达加入董事会 <span><img src="~/assets/img/widget-vip.png" class="vip" /></span></h3>
<h5 class="author">
<div class="fl">
<span class="authorName"> <img src="~/assets/img/widget-photo.png" alt="" /> 玻璃通 </span>
<span>6月12日 13:34</span>
</div>
<div class="fr attention">
<span class="attentionText">关注</span>
<span class="beforeclose"> <i class="fa fa-times delete" aria-hidden="true"></i> <i class="nolike">不感兴趣</i> </span>
</div>
<div class="clearfix"></div> </h5>
</div> <p class="content"></p> </li>
<li class="headline-item">
<div class="fl indexImg">
<img src="" />
</div>
<div class="words">
<h3>Drive.ai融资5000万吴恩达加入董事会 <span><img src="~/assets/img/widget-vip.png" class="vip" /></span></h3>
<h5 class="author">
<div class="fl">
<span class="authorName"> <img src="~/assets/img/widget-photo.png" alt="" /> 玻璃通 </span>
<span>6月7日 10:34</span>
</div>
<div class="fr attention">
<span class="attentionText">关注</span>
<span class="beforeclose"> <i class="fa fa-times delete" aria-hidden="true"></i> <i class="nolike">不感兴趣</i> </span>
</div>
<div class="clearfix"></div> </h5>
</div> <p class="content">滴滴与360都开源了各自的插件化框架,VirtualAPK与RePlugin,作为一个插件化方面的狂热研究者,在周末就迫不及待的下载了Virtualapk框架来进行研究,本篇博客带的……</p> </li>
</ul>
<ul id="loaded" class="headline">
</ul>
</div>
<!-- <script src="~/assets/data-list.js" type="text/javascript"></script> -->
</div>
<div class="fl right">
<div class="activity">
<div class="acti">
<img src="~/assets/img/widget-activity01.png" alt="活动一" />
</div>
<div class="acti">
<img src="~/assets/img/widget-activity02.png" alt="活动一" />
</div>
</div>
<div class="block-btn">
<p>今天,有什么好东西要和大家分享么?</p>
<a class="sui-btn btn-block btn-share" href="~/assets/headline-submit.html" target="_blank">发布分享</a>
</div>
<div class="question-list">
<h3 class="title">热门回答</h3>
<div class="lists">
<ul>
<li class="list-item"> <p class="list-title">关于系统问答你都应该都应该都应该注意些什么吗?</p> <p class="authorInfo"> <span class="authorName"><img src="~/assets/img/widget-photo.png" alt="" />玻璃筒</span> <span>6月22日 12:01</span> </p> </li>
<li class="list-item"> <p class="list-title">关于系统问答你都应该注意吗?</p> <p class="authorInfo"> <span class="authorName"><img src="~/assets/img/widget-photo.png" alt="" />玻璃筒</span> <span>6月22日 12:01</span> </p> </li>
<li class="list-item"> <p class="list-title">关于系统问答你都应该注意吗?</p> <p class="authorInfo"> <span class="authorName"><img src="~/assets/img/widget-photo.png" alt="" />玻璃筒</span> <span>6月22日 12:01</span> </p> </li>
<li class="list-item"> <p class="list-title">关于系统问答你都应该注意吗?</p> <p class="authorInfo"> <span class="authorName"><img src="~/assets/img/widget-photo.png" alt="" />玻璃筒</span> <span>6月22日 12:01</span> </p> </li>
<li class="list-item"> <p class="list-title">关于系统问答你都应该注意吗?</p> <p class="authorInfo"> <span class="authorName"><img src="~/assets/img/widget-photo.png" alt="" />玻璃筒</span> <span>6月22日 12:01</span> </p> </li>
</ul>
<a class="sui-btn btn-block btn-bordered btn-more">查看更多</a>
</div>
</div>
<div class="card-list">
<div class="head">
<h3 class="title">遇见TA</h3>
</div>
<div class="list">
<ul>
<li class="card-item">
<div class="attention">
<span>关注匹配度:<i class="degree">83%</i></span>
<span class="fr"><i class="fa fa-heart-o" aria-hidden="true"></i><i class="fa fa-times close" aria-hidden="true"></i></span>
</div>
<div class="img">
<img src="~/assets/img/widget-t01be3f1015cf52e1f3.png" alt="" />
</div>
<div class="info">
<div class="fl photo">
<img src="~/assets/img/widget-photo.png" alt="" />
</div>
<div class="fl intro">
<p>【馨儿】发布了雕刻时光约会邀请</p>
<p class="name">玻璃通 <span class="date">6月22日 12:01</span></p>
</div>
<div class="clearfix"></div>
</div> </li>
<li class="card-item">
<div class="attention">
<span>关注匹配度:<i class="degree">86%</i></span>
<span class="fr"><i class="fa fa-heart-o" aria-hidden="true"></i><i class="fa fa-times close" aria-hidden="true"></i></span>
</div>
<div class="img">
<img src="~/assets/img/widget-MOG88A60E7ZI.png" alt="" />
</div>
<div class="info">
<div class="fl photo">
<img src="~/assets/img/widget-photo.png" alt="" />
</div>
<div class="fl intro">
<p>【馨儿】发布了雕刻时光约会邀请</p>
<p class="name">玻璃通 <span class="date">6月22日 12:01</span></p>
</div>
<div class="clearfix"></div>
</div> </li>
<li class="card-item">
<div class="attention">
<span>关注匹配度:<i class="degree">78%</i></span>
<span class="fr"><i class="fa fa-heart-o" aria-hidden="true"></i><i class="fa fa-times close" aria-hidden="true"></i></span>
</div>
<div class="img">
<img src="~/assets/img/widget-t019e2d84e53580b099.png" alt="" />
</div>
<div class="info">
<div class="fl photo">
<img src="~/assets/img/widget-photo.png" alt="" />
</div>
<div class="fl intro">
<p>【馨儿】发布了雕刻时光约会邀请</p>
<p class="name">玻璃通 <span class="date">6月22日 12:01</span></p>
</div>
<div class="clearfix"></div>
</div> </li>
</ul>
<a class="sui-btn btn-block btn-bordered btn-more">查看更多</a>
</div>
</div>
<div class="activity-list">
<h3 class="title">活动日历</h3>
<div class="list">
<ul>
<li class="list-item"> <p class="list-time"> 2017/06/30 北京</p>
<div class="list-content clearfix">
<div class="fl img">
<img src="~/assets/img/widget-simple.png" alt="" />
</div>
<div>
<p>在线峰会 | 前端开发重难点技术剖析与创新实践</p>
</div>
</div> </li>
<li class="list-item"> <p class="list-time"> 2017/06/30 北京</p>
<div class="list-content clearfix">
<div class="fl img">
<img src="~/assets/img/widget-simple.png" alt="" />
</div>
<div>
<p>在线峰会 | 前端开发重难点技术剖析与创新实践</p>
</div>
</div> </li>
<li class="list-item"> <p class="list-time"> 2017/06/30 北京</p>
<div class="list-content clearfix">
<div class="fl img">
<img src="~/assets/img/widget-simple.png" alt="" />
</div>
<div>
<p>在线峰会 | 前端开发重难点技术剖析与创新实践</p>
</div>
</div> </li>
<li class="list-item"> <p class="list-time"> 2017/06/30 北京</p>
<div class="list-content clearfix">
<div class="fl img">
<img src="~/assets/img/widget-simple.png" alt="" />
</div>
<div>
<p>在线峰会 | 前端开发重难点技术剖析与创新实践</p>
</div>
</div> </li>
</ul>
<a class="sui-btn btn-block btn-bordered btn-more">查看更多</a>
</div>
</div>
<div class="ad-carousel">
<div class="carousel">
<div id="myCarousel" data-ride="carousel" data-interval="4000" class="sui-carousel slide">
<ol class="carousel-indicators">
<li data-target="#myCarousel" data-slide-to="0" class="active"></li>
<li data-target="#myCarousel" data-slide-to="1"></li>
<li data-target="#myCarousel" data-slide-to="2"></li>
</ol>
<div class="carousel-inner">
<div class="active item">
<img src="~/assets/img/widget-ad01.png" />
</div>
<div class="item">
<img src="~/assets/img/widget-ad01.png" />
</div>
<div class="item">
<img src="~/assets/img/widget-ad01.png" />
</div>
</div>
<span class="adname">广告</span>
</div>
</div>
</div>
<!-- <link rel="import" href=".~/assets/.~/assets/modules/ui-modules/footer/footer.html?__inline"> -->
</div>
</div>
</div>
</div>
</template>

<script>
import '~/assets/css/page-sj-headline-login.css'
</script>

参考headline-login.html页面构建,拷贝代码后将./批量替换为~/assets/

2.4 其它频道页

按照上述方法构建以下页面

(1)建立pages/qa/index.vue (问答首页)

(2)建立pages/gathering/index.vue(活动首页)

(3)建立pages/friends/index.vue(交友首页)

(4)建立pages/spit/index.vue (吐槽首页)

(5)建立pages/recruit/index.vue (招聘首页)

2.5 网站导航

修改layouts/default.vue

1
2
3
4
5
6
7
<ul class="sui-nav">
<router-link to="/" tag="li" active-class="active" exact><a>头条</a></router-link>
<router-link to="/qa" tag="li" active-class="active"><a>问答</a></router-link>
<router-link to="/gathering" tag="li" active-class="active"><a>活动</a></router-link>
<router-link to="/friends" tag="li" active-class="active"><a>交友</a></router-link>
<router-link to="/spit" tag="li" active-class="active"><a>吐槽</a></router-link>
<router-link to="/recruit" tag="li" active-class="active"><a>招聘</a></router-link> </ul>

详见官方文档:

https://router.vuejs.org/zh-cn/api/router-link.html

3 活动模块

3.1 活动列表页

3.1.1 数据渲染

我们这一步将读取模拟的动态数据来完成服务端数据渲染部分

(1)创建utils文件夹,utils下创建request.js ,用于封装axios

1
2
3
4
5
6
7
import axios from 'axios'
// 创建axios实例
const service = axios.create({
baseURL: 'http://192.168.184.133:7300/mock/5af314a4c612520d0d7650c7', // api的base_url
timeout: 30000 // 请求超时时间
})
export default service

(2)创建api文件夹,将管理后台工程的api/gathering.js 拷贝到api文件夹

(3)修改pages/gathering/index.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<template>
<div class="wrapper activities">
<div class="activity-card-list">
<div class="top-title">
<h4 class="latest">最新活动</h4>
<div class="clearfix"></div>
</div>
<div class="activity-list">
<ul class="activity">
<li class="activity-item" v-for="(item,index) in items" :key="index">
<div class="activity-inner">
<a href="http://"></a>
<div class="img">
<a :href="'/gathering/item/'+item.id" target="_blank"><img :src="item.image" alt="" /></a>
</div>
<div class="text">
<p class="title">{{item.name}}</p>
<div class="fl goin">
<p>时间:{{item.starttime}}</p>
<p>城市:{{item.city}}</p>
</div>
<div class="fr btn">
<span class="sui-btn btn-bao">立即报名</span>
</div>
<div class="clearfix"></div>
</div>
</div> </li>
</ul>
</div>
</div>
</div>
</template>
<script>
import '~/assets/css/page-sj-activity-index.css'
import gatheringApi from '@/api/gathering'
export default {
asyncData(){
return gatheringApi.search(1,12,{state:'1'}).then( res => {
return {items: res.data.data.rows }
})
}
}
</script>

(4)为了实现完美的测试效果,我们修改easyMock接口

URL:/gathering/gathering/search/{page}/{size} (post)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
"flag": true,
"code": 20000,
"message": "@string",
"data": {
"total": "@integer(60, 100)",
"rows|12": [{
"id": "@string",
"name": "@cword(8,12)",
"summary": "@cword(20,40)",
"detail": "@cword(20,40)",
"sponsor": "@string",
"image": "https://img-ads.csdn.net/2018/201805171739161420.jpg",
"starttime": "@date",
"endtime": "@date",
"address": "@county(true)",
"enrolltime": "@date",
"state": "1",
"city": "@city"
}]
}
}

3.1.2 瀑布流组件

我们这里使用的瀑布流组件vue-infinite-scroll,安装:

1
cnpm install vue-infinite-scroll --save

代码实现

(1)plugins下创建vue-infinite-scroll.js

1
2
3
import Vue from 'vue'
import infiniteScroll from 'vue-infinite-scroll'
Vue.use(infiniteScroll)

(2)修改nuxt.config.js

1
2
3
4
plugins: [
......
{ src: '~plugins/vue-infinite-scroll', ssr: false }
],

(3)修改页面pages/gathering/index.vue

1
<div class="activity-list" v-infinite-scroll="loadMore" >

添加pageNo用于记录页码

1
2
3
4
5
data() {
return {
pageNo: 1
}
},

编写方法loadMore

1
2
3
4
5
6
7
8
methods: {
loadMore(){
this.pageNo++
gatheringApi.search(this.pageNo,12,{state:'1'}).then( res => {
this.items = this.items.concat( res.data.data.rows )
})
}
}

3.2 活动详情页

3.2.1 活动详情页构建

修改pages/gathering/item/_id.vue 内容根据静态原型页面activity-detail.html构建 ,代码略

3.2.2 数据渲染

修改pages/gathering/item/_id.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
<template>
<div class="wrapper activities">
<h1>{{item.name}}</h1>
<div class="img-text">
<div class="left-img">
<img :src="item.image" alt="" />
</div>
<div class="right-txt">
<p>开始时间: {{item.starttime}}</p>
<p>结束时间: {{item.endtime}}</p>
<p>举办地点: {{item.address}}</p>
<p>主办方:{{item.sponsor}}</p>
<p>报名截止:{{item.enrolltime}}
<div class="join">
<button class="sui-btn btn-danger">立即报名</button>
<span class="will">报名即将开始</span>
</div>
</div>
</div>
<div class="simple-text">
<div class="left-content">
<div class="content-item">
<div class="tit">
<span>大会介绍</span>
</div>
<div class="text">
<h4></h4>
<p>{{item.summary}}</p>
</div>
</div>
<div class="content-item">
<div class="tit">
<span>议题简介</span>
</div>
<div class="text">
<h4></h4>
<p>{{item.detail}}</p>
</div>
</div>
</div>
<div class="right-intro">
<div class="content-item">
<div class="tit">
<span>活动组织者</span>
</div>
<div class="text">
<p>主办方: {{item.sponsor}}</p>
</div>
</div>
<div class="content-item">
<div class="tit">
<span>相关链接</span>
</div>
<div class="text">
<p>活动官网: infoQ.com</p>
</div>
</div>
<div class="content-item">
<div class="tit">
<span>分享扩散</span>
</div>
<div class="text">
<p><img src="~/assets/img/widget-weibo.png" alt="" width="30" /><img src="~/assets/img/widget-weixin.png" alt="" width="30" /></p>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import "~/assets/css/page-sj-activity-detail.css"
import gatheringApi from '@/api/gathering'
export default {
asyncData( {params} ){
return gatheringApi.findById(params.id).then(res=>{
return {item: res.data.data}
})
}
}
</script>

3.2.3 分享组件

Share.js是一款一键转发工具,它可以一键分享到新浪微博、微信、QQ空间、QQ好友、腾讯微博、豆瓣、Facebook组件、Twitter、Linkedin、Google+、点点等社交网站,使用字体图标。

以下步骤可以实现微博和微信分享

(1)修改pages/gathering/item/_id.vue的脚本部分。以下代码用于引入外部的js .我们这里的js采用cdn方式引入 地址为:

https://cdn.bootcss.com/social-share.js/1.0.16/js/social-share.min.js

所需要的样式: https://cdn.bootcss.com/social-share.js/1.0.16/css/share.min.css

1
2
3
4
5
6
7
8
head: {
script: [
{ src: 'https://cdn.bootcss.com/social-share.js/1.0.16/js/social-share.min.js' }
],
link: [
{ rel: 'stylesheet', href: 'https://cdn.bootcss.com/social-share.js/1.0.16/css/share.min.css' }
]
}

(2)修改pages/gathering/_id.vue的页面部分,在合适的位置添加分享按钮

1
2
3
4
5
<div class="social-share"  
data-sites="weibo,wechat"
data-url="http://www.itheima.com"
:data-title="item.name">
</div>

选项:

1
2
3
4
5
6
7
8
9
url                 : '', // 网址,默认使用 window.location.href
source              : '', // 来源(QQ空间会用到), 默认读取head标签:<meta name="site" content="http://overtrue" />
title               : '', // 标题,默认读取 document.title 或者 <meta name="title" content="share.js" />
description         : '', // 描述, 默认读取head标签:<meta name="description" content="PHP弱类型的实现原理分析" />
image               : '', // 图片, 默认取网页中第一个img标签
sites               : ['qzone', 'qq', 'weibo','wechat', 'douban'], // 启用的站点
disabled            : ['google', 'facebook', 'twitter'], // 禁用的站点
wechatQrcodeTitle   : '微信扫一扫:分享', // 微信二维码提示文字
wechatQrcodeHelper  : '<p>微信里点“发现”,扫一下</p><p>二维码便可将本文分享至朋友圈。</p>'

以上选项均可通过标签 data-xxx 来设置

4 招聘模块

4.1 招聘列表页

4.1.1 编写API方法

(1)将管理后台的api/recruit.js和api/enterprise.js 拷贝到当前工程的api文件夹下

(2)修改api/recruit.js,增加方法

1
2
3
4
5
6
7
8
9
10
11
12
recommend() {   
return request({
url: `/${api_group}/${api_name}/search/recommend`,
method: 'get'
})
},
newlist() {
return request({
url: `/${api_group}/${api_name}/search/newlist`,
method: 'get'
})
}

(3)修改api/enterprise.js,增加方法

1
2
3
4
5
6
hotlist() {   
return request({
url: `/${api_group}/${api_name}/search/hotlist`,
method: 'get'
})
}

4.1.2 招聘列表页数据渲染

修改pages/recruit/index.vue axios.all可以并发多个异步请求,axios.spread负责获取多个异步请求的返回结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import '~/assets/css/page-sj-recruit-index.css'
import recruitApi from '@/api/recruit'
import enterpriseApi from '@/api/enterprise'
import axios from 'axios'
export default {
asyncData(){
return axios.all([recruitApi.recommend(), recruitApi.newlist(),enterpriseApi.hotlist() ]).then(
axios.spread( function( recommendList,newList ,hostList ){
return {
recommendList: recommendList.data.data,
newList: newList.data.data,
hostList: hostList.data.data
}
})
)
}
}

模板部分代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
<template>
<div class="wrapper tag-item">
<div class="fl left-list">

<div class="job-position">
<div class="job-type reco-job">
<div class="head">
<h4 class="title pull-left">推荐职位</h4>
<span class="more pull-right"><a href="#">更多职位推荐&nbsp;&nbsp;<i class="fa fa-angle-right" aria-hidden="true"></i></a></span>
<div class="clearfix"></div>
</div>
<ul class="yui3-g job-list" style="display:block;">
<li class="yui3-u-1-2 job-item" v-for="(item,index) in recommendList" :key="index">
<p><span class="name">
<a href="~/assets/recruit-detail.html" target="_blank">{{item.jobname}}</a>
</span><span class="city"><i class="fa fa-map-marker"></i> 北京</span></p> <p class="need"><span class="money">{{item.salary}}</span>/{{item.condition}}/{{item.education}}/{{item.type}}</p> <p><span class="company">百度 &middot; 6天前</span></p>
</li>

</ul>
</div>
<div class="job-type latest-job">
<div class="head">
<h4 class="title pull-left">最新职位</h4>
<span class="more pull-right"><a href="#">更多职位推荐&nbsp;&nbsp;<i class="fa fa-angle-right" aria-hidden="true"></i></a></span>
<div class="clearfix"></div>
</div>
<ul class="yui3-g job-list" style="display:block;">
<li class="yui3-u-1-2 job-item" v-for="(item,index) in newList" :key="index"> <p>
<span class="name"> <a href="~/assets/recruit-jobDetail.html" target="_blank">{{item.jobname}}</a></span><span class="city"><i class="fa fa-map-marker"></i> 北京</span></p> <p class="need"><span class="money">{{item.salary}}</span>/{{item.condition}}/{{item.education}}/{{item.type}}</p> <p><span class="company">百度 &middot; 6天前</span></p> </li>

</ul>
</div>
</div>
</div>
<div class="fl right-tag">
<div class="hot-company">
<p class="mail">提交收录请发邮件至ccccccc@qq.com</p>
<div class="company">
<div class="head">
<h4>热门企业</h4>
</div>
<ul class="yui3-g company" style="display:block;">
<li class="yui3-u-1-3 company-item" v-for="(item,index) in hostList" :key="index">
<p><img :src="item.logo" alt="" /></p>
<p class="title">{{item.name}}</p> <p class="position">
<a href="~/assets/recruit-company.html" target="_blank">{{item.jobcount}}个职位</a></p> </li>

</ul>
</div>
</div>
</div>
<div class="clearfix"></div>
</div>
</template>

4.2 招聘详情页

4.2.1 构建招聘详情页

(1)构建招聘详细页 pages/recruit/item/_id.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
<template>
<!--两列布局-->
<div class="wrapper tag-item">
<div class="job-intro">
<div class="left-img">
<img src="~/assets/img/widget-company.png" alt="" />
</div>
<div class="middle-intro">
<div class="name">Python开发工程师 · 有赞</div>
<div class="intro">15K-25K / 经验3-5年 / 本科及以上 / 全职</div>
<div class="tag">
<li>Python</li><li>Python</li><li>O2O</li><li>Python</li><li>Python</li>
</div>
</div>
<div class="right-tool">
<p class="throw"><button class="sui-btn btn-throw">投简历</button></p>
<button class="sui-btn btn-collect">收藏</button>
<span>100收藏</span>
<span>291浏览</span>
</div>
<div style="clear:both"></div>
</div>
<div class="fl left-list ">
<div class="tit">
<span>职位描述</span>
</div>
<div class="content">
<p>我们提供:</p>
<p>富有市场竞争力的薪水;</p>
<p>一套程序猿的顶级梦幻装备, 一台顶级CPU+大内存+SSD台式机+护眼显示屏,一把舒适的人体工程学座椅;</p>
<p>一群聪明欢乐的小伙伴们;</p>
<p>顶级IT创业公司提供的大平台,丰富的晋升机制;</p>
<p>提供独立的单身公寓,空调热水。</p>
<p>提供五险,双休和法定假日休假</p>
</div>
<div class="tit">
<span>职位要求</span>
</div>
<div class="content">
<p>对新技术有好奇心, 学习能力强, 良好的英文资料阅读能力</p>
<p>熟悉面向对象编程, 有良好的编程⻛格和习惯.</p>
<p>良好的团队合作精神和沟通能力,勤奋上进</p>
<p>熟练使用PHP, 熟悉symfony/laravel优先.</p>
<p>熟悉HTML/CSS/JS, 懂vuejs优先.</p>
<p>熟悉 Linux/Mac 开发环境</p>
<p>熟悉 MySQL 数据库,掌握 MongoDB, Redis 等 NoSQL</p>
<p>熟悉git协作</p>
<p>*我们是正域团队,,我们正在做一件改变行业的事情,</p>

<p>如果你激情四射、胆大有料、敢想敢干、协作一流, 立刻加入我们!</p>
</div>
<div class="time">发布于1小时前</div>
</div>
<div class="fl right-tag">
<div class="company-job">
<div class="intro">
<img src="~/assets/img/widget-company.png" alt="" />
<div class="title">有赞App</div>
<div class="content">“有赞”基于云服务模式向商户提供免费、强大的微商城系统和完整的微电商行业解决方案,并致力于通过粉丝营销、交易创新、消保体系为广大商户、消费者搭建移动购物平台。</div>
</div>
<div class="tag">
<li>电子商务</li>
<li>移动互联网</li>
<li>O2O</li>
<li>2012年成立</li>
<li>11-50名雇员</li>
</div>
<div class="btns">
<a class="sui-btn btn-home" href="~/assets/recruit-company.html" target="_blank">企业主页</a>
<a class="sui-btn btn-position" href="~/assets/recruit-job.html" target="_blank">1个职位</a>
</div>
</div>
</div>
</div>
</template>
<script>
import '~/assets/css/page-sj-recruit-detail.css'
export default {

}
</script>

(2)修改pages/recruit/index.vue 链接

1
<nuxt-link :to="'/recruit/item/'+item.id">{{item.jobname}}</nuxt-link>

4.2.2 招聘详情页-渲染招聘信息

修改 pages/recruit/item/_id.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
<template>
<div class="wrapper tag-item">
<div class="job-intro">
<div class="left-img">
<img src="~/assets/img/widget-company.png" alt="" />
</div>
<div class="middle-intro">
<div class="name">
{{item.jobname}} &middot; 有赞
</div>
<div class="intro">
{{item.salary}} / {{item.condition}} / {{item.education}} / {{item.type}}
</div>
<div class="tag">
<li>Python</li>
<li>Python</li>
<li>O2O</li>
<li>Python</li>
<li>Python</li>
</div>
</div>
<div class="right-tool">
<p class="throw"><button class="sui-btn btn-throw">投简历</button></p>
<button class="sui-btn btn-collect">收藏</button>
<span>100收藏</span>
<span>291浏览</span>
</div>
<div style="clear:both"></div>
</div>
<div class="fl left-list ">
<div class="tit">
<span>职位描述</span>
</div>
<div class="content">
<p>{{item.content1}}</p>
</div>
<div class="tit">
<span>职位要求</span>
</div>
<div class="content">
<p>{{item.content2}}</p>
</div>
<div class="time">
发布于1小时前
</div>
</div>
<div class="fl right-tag">
<div class="company-job">
<div class="intro">
<img src="~/assets/img/widget-company.png" alt="" />
<div class="title">
有赞App
</div>
<div class="content">
“有赞”基于云服务模式向商户提供免费、强大的微商城系统和完整的微电商行业解决方案,并致力于通过粉丝营销、交易创新、消保体系为广大商户、消费者搭建移动购物平台。
</div>
</div>
<div class="tag">
<li>电子商务</li>
<li>移动互联网</li>
<li>O2O</li>
<li>2012年成立</li>
<li>11-50名雇员</li>
</div>
<div class="btns">
<a class="sui-btn btn-home" href="~/assets/recruit-company.html" target="_blank">企业主页</a>
<a class="sui-btn btn-position" href="~/assets/recruit-job.html" target="_blank">1个职位</a>
</div>
</div>
</div>
</div>
</template>
<script>
import '~/assets/css/page-sj-recruit-detail.css'
import recruitApi from '@/api/recruit'
export default {
asyncData({params}){
return recruitApi.findById(params.id ).then( res =>{
return {item:res.data.data}
})
}
}
</script>

4.2.3 招聘详情页-渲染企业信息

修改 pages/recruit/item/_id.vue,以嵌套方式加载企业信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import '~/assets/css/page-sj-recruit-detail.css'
import recruitApi from '@/api/recruit'
import enterpriseApi from '@/api/enterprise'
export default {
asyncData({params}){
return recruitApi.findById(params.id ).then( res =>{
return enterpriseApi.findById( res.data.data.eid ).then(
res2=>{
return {
enterprise_item: res2.data.data,
item:res.data.data
}
}
)
})
}
}

修改 pages/recruit/item/_id.vue页面模板部分

LOGO

1
<img :src="enterprise_item.logo" alt="" /> 

企业名称:

1
{{enterprise_item.name}}

企业简介:

1
{{enterprise_item.summary}}

企业标签:

1
2
3
<div class="tag"> 
<li v-for="(label,index) in enterprise_item.labels.split(',')" :key="index">{{label}}</li>
</div>

企业主页和企业职位数:

1
2
3
4
<div class="btns"> 
<a class="sui-btn btn-home" :href="enterprise_item.url" target="_blank">企业主页</a>
<a class="sui-btn btn-position" href="~/assets/recruit-job.html" target="_blank">{{enterprise_item.jobcount}}个职位</a>
</div>

为了达到良好的测试效果,建议修改以下easyMock的数据

/recruit/enterprise/{enterpriseId} (GET)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"code": "20000",
"flag": true,
"message": "@string",
"data": {
"id": "@string",
"name": "传智播客",
"summary": "传智播客是一家高端IT教育培训机构",
"address": "@string",
"labels": "IT教育培训,高端,新三板上市企业",
"coordinate": "@string",
"ishot": "@string",
"logo": "http://www.itcast.cn/2018czgw/images/logo.png",
"jobcount": "132",
"url": "http://www.itcast.cn"
}
}

第6章 网站前台-登陆与用户中心

学习目标:

  • 完成用户注册功能
  • 完成用户登陆功能,掌握js-cookie的使用
  • 完成微信扫码登陆的功能
  • 完成用户中心嵌套布局,掌握nuxt嵌套路由的使用

1 用户注册

1.1 页面构建

创建pages/login.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
<template>
<div class="wrapper loginsign">
<div class="item signup">
<div class="form">
<h3 class="loginsign-title">注册新账号</h3>
<form class="sui-form">
<div class="control-group">
<label for="inputname" class="control-label">名字</label>
<div class="controls">
<input type="text" id="inputname" placeholder="真实姓名或常用昵称" class="input-xlarge" data-rules="required" />
</div>
</div>
<div class="different">
<div class="radio-content">
<div id="a1" class="phone">
<div class="control-group number">
<input type="text" placeholder="仅支持大陆手机号" class="input-xlarge" data-rules="required|mobile" />
</div>
<div class="control-group code">
<div class="input-append">
<input id="appendedInputButton" type="text" placeholder="短信验证" class="span2 input-large msg-input" />
<button type="button" class="sui-btn msg-btn">获取验证码</button>
</div>
</div>
<div class="control-group">
<label for="inputpassword" class="control-label">密码</label>
<div class="controls">
<input type="text" id="inputpassword" placeholder="请输入6-16位密码" class="input-xlarge" />
</div>
</div>
</div>
<div id="a2" class="email">
<div class="control-group inputemail">
<input type="text" placeholder="输入手机号" class="input-xlarge" />
</div>
<div class="control-group">
<label for="inputpassword" class="control-label">密码:</label>
<div class="controls">
<input type="text" id="inputpassword" placeholder="请输入6-16位字符" class="input-xlarge" />
</div>
</div>
</div>
</div>
</div>
<div class="control-group btn-signup">
<label class="control-label"></label>
<div class="controls">
<label> <input type="checkbox" /><span class="type-text" style="font-size:12px;">同意协议并接受《服务条款》</span> </label>
<button type="submit" class="sui-btn btn-danger btn-yes">注 册</button>
</div>
</div>
</form>
</div>
</div>
<div class="item">
<div class="form">
<h3 class="loginsign-title">用户登录</h3>
<form class="sui-form login-form">
<div class="control-group">
<label for="inputname" class="control-label">手机号或Email:</label>
<div class="controls">
<input type="text" id="inputname" placeholder="11位手机号或Email" class="input-xlarge" data-rules="required" />
</div>
</div>
<div class="control-group">
<label for="inputpassword" class="control-label">密码:</label>
<div class="controls">
<input type="text" id="inputpassword" placeholder="输入登录密码" class="input-xlarge" />
</div>
</div>
<div class="controls">
<label> <input type="checkbox" name="remember-me" /><span class="type-text" style="font-size:12px;">记住登录状态</span> </label>
<button type="submit" class="sui-btn btn-danger btn-yes">登 录</button>
</div>
<div class="other-methods">
</div>
</form>
</div>
</div>
</div>
</template>
<script>
import '~/assets/css/page-sj-person-loginsign.css'
export default {

}
</script>

其中的内容参见person-loginsign.html

1.2 获取验证码

1.2.1 模拟数据与API

(1)将user.yml 导入easymock

(2)修改easy-mock 数据

url: /user/user/sendsms/{mobile}

method:put

1
2
3
4
5
{
"code": 20000,
"flag": true,
"message": "验证码发送成功"
}

(3)编写API 创建api/user.js

1
2
3
4
5
6
7
8
9
10
11
import request from '@/utils/request'
const api_group = 'user'
const api_name = 'user'
export default {
sendsms(mobile) {
return request({
url: `/${api_group}/${api_name}/sendsms/${mobile}`,
method: 'put'
})
}
}

1.2.2 调用API

(1)修改pages/login.vue脚本部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<script>
import '~/assets/css/page-sj-person-loginsign.css'
import userApi from '@/api/user'
export default {
data(){
return {
pojo: {}
}
},
methods: {
sendsms(){
userApi.sendsms( this.pojo.mobile ).then(res => {
alert(res.data.message)
})
}
}
}
</script>

(2)修改pages/login.vue 绑定变量

1
<input type="text" v-model="pojo.mobile" placeholder="仅支持大陆手机号" class="input-xlarge" /> 

(3)修改pages/login.vue 按钮绑定方法

1
<button type="button" class="sui-btn msg-btn" @click="sendsms" >获取验证码</button> 

1.2.3 使用ElementUI 的弹出框

(1)安装element-ui

1
cnpm install element-ui --save

(2)plugins文件夹下创建element-ui.js

1
2
3
import Vue from 'vue'  
import ElementUI from 'element-ui'
Vue.use(ElementUI)

(3)修改nuxt.config.js,加入插件与样式

1
2
3
4
5
6
7
8
9
.......
plugins: [
.....
{ src: '~plugins/element-ui.js', ssr: false }
],
css: [
'element-ui/lib/theme-chalk/index.css'
],
.........

(4)修改pages/login.vue的脚本部分,将alert替换为以下代码

1
2
3
4
this.$message({
message: res.data.message,
type: (res.data.flag?'success':'error')
})

1.3 提交注册

(1)在easy-mock 增加数据

URL: /user/user/register/{code}

Method: post

1
2
3
4
5
{
"flag": true,
"code": 20000,
'message': "执行成功"
}

(2)修改api/user.js,增加方法

1
2
3
4
5
6
7
register(user,code) {
return request({
url: `/${api_group}/${api_name}/register/${code}`,
method: 'post',
data:user
})
},

(3)修改pages/login/index.vue脚本部分 增加属性

1
2
3
4
5
6
data(){
return {
pojo: {},
code:''
}
},

新增注册的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
register () {
userApi.register(this.pojo).then( res =>{
if(res.data.flag){
this.$message({
message: '注册成功',
type: 'success'
})
this.pojo={}
}else{
this.$message({
message: '注册出错',
type: 'error'
})
}
})
}

(4)修改pages/login/index.vue页面部分

绑定表单输入框

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<form class="sui-form"> 
<div class="control-group">
<label for="inputname" class="control-label">登录名</label>
<div class="controls">
<input type="text" id="inputname" v-model="pojo.loginname" placeholder="登录名" class="input-xlarge" />
</div>
</div>
<div class="control-group">
<label for="inputname" class="control-label">昵称</label>
<div class="controls">
<input type="text" id="inputname" v-model="pojo.nickname" placeholder="真实姓名或常用昵称" class="input-xlarge" />
</div>
</div>
<div class="different">
<div class="radio-content">
<div id="a1" class="phone">
<div class="control-group number">
<input type="text" v-model="pojo.mobile" placeholder="仅支持大陆手机号" class="input-xlarge" data-rules="required|mobile" />
</div>
<div class="control-group code">
<div class="input-append">
<input id="appendedInputButton" v-model="code" type="text" placeholder="短信验证" class="span2 input-large msg-input" />
<button type="button" class="sui-btn msg-btn" @click="sendsms" >获取验证码</button>
</div>
</div>
<div class="control-group">
<label for="inputpassword" class="control-label">密码</label>
<div class="controls">
<input type="text" id="inputpassword" v-model="pojo.password" placeholder="请输入6-16位密码" class="input-xlarge" />
</div>
</div>
</div>
</div>
</div>
<div class="control-group btn-signup">
<label class="control-label"></label>
<div class="controls">
<label> <input type="checkbox" /><span class="type-text" style="font-size:12px;">同意协议并接受《服务条款》</span> </label>
<button type="button" class="sui-btn btn-danger btn-yes" @click="register">注 册</button>
</div>
</div>
</form>

绑定方法

1
<button type="button" class="sui-btn btn-danger btn-yes" @click="register">注 册</button> 

1.4 输入校验

以下功能由学员实现:

(1)校验昵称必须填写

(2)校验手机号的合法性 ,可以使用正则表达式进行验证。

1
^((13[0-9])|(15[^4])|(18[0,2,3,5-9])|(17[0-8])|(147))\\d{8}$

(3)密码长度校验

(4)判断是否勾选同意条款

2 用户登陆

2.1 登陆验证

(1)mock模拟数据

url: /user/user/login

method: post

1
2
3
4
5
6
7
8
9
{
"code": 20000,
"flag": true,
"data": {
"token": "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI5ODcyNTUxMDQ1NDM1MjY5MTIiLCJpYXQiOjE1MjQyMTQ5NDgsInJvbGVzIjoidXNlciIsImV4cCI6MTUyNDIxNTMwOH0.icFRMKfaHlPn224hU3Gm_LOHflaONj9IfWIVj8gSbbM",
"name": "小白",
"avatar": 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif'
}
}

(2)API编写。 修改api/user.js,新增方法

1
2
3
4
5
6
7
8
9
10
login(mobile, password) {
return request({
url: `/${api_group}/${api_name}/login`,
method: 'post',
data: {
mobile,
password
}
})
}

(3)修改pages/login/index.vue ,增加属性:用户名和密码

1
2
3
4
5
6
7
data(){
return {
....
mobile: '',
password: ''
}
},

(4)修改pages/login/index.vue ,增加登陆的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
login () {    
userApi.login(this.mobile,this.password).then( res =>{
if(res.data.flag){
location.href='/manager'
}else{
this.$message({
message: res.data.message,
type: 'error'
})
this.mobile=''
this.password=''
}
})
}

(5)绑定页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<form class="sui-form login-form"> 
<div class="control-group">
<label for="inputname" class="control-label">手机号:</label>
<div class="controls">
<input type="text" id="inputname" v-model="loginname" placeholder="11位手机号" class="input-xlarge" />
</div>
</div>
<div class="control-group">
<label for="inputpassword" class="control-label">密码:</label>
<div class="controls">
<input type="password" id="inputpassword" v-model="password" placeholder="输入登录密码" class="input-xlarge" />
</div>
</div>
<div class="controls">
<label> <input type="checkbox" name="remember-me" /><span class="type-text" style="font-size:12px;">记住登录状态</span> </label>
<button type="button" @click="login" class="sui-btn btn-danger btn-yes">登 录</button>
</div>
<div class="other-methods">
</div>
</form>

测试效果,登陆后跳转到首页

2.2 登录用户信息存储

(1)安装js-cookie

1
cnpm install js-cookie --save

(2)创建utils/auth.js

1
2
3
4
5
6
7
8
9
10
11
import Cookies from 'js-cookie'

const TokenKey = 'User-Token'
const NameKey = 'User-Name'
const AvatarKey = 'User-Avatar'

export function setUser(token,name,avatar) {
Cookies.set(NameKey, name)
Cookies.set(AvatarKey, avatar)
Cookies.set(TokenKey, token)
}

(3)修改pages/login/index.vue 导入auth.js

1
import { setUser } from '@/utils/auth'

修改登陆方法,调用auth实现cookie的数据的保存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
login(){
userApi.login(this.mobile,this.password ).then( res=> {
if(res.data.flag){
//保存用户信息
setUser(res.data.data.token, res.data.data.name, res.data.data.avatar)
location.href='/manager' //用户中心
}else{
this.$message( {
message: res.data.message,
type: "error"
})
this.mobile=''
this.password=''
}
})
}

2.3 登陆状态显示用户信息

修改utils/auth.js

1
2
3
4
5
6
7
export function getUser() {
return {
token:Cookies.get(TokenKey),
name:Cookies.get(NameKey),
avatar:Cookies.get(AvatarKey)
}
}

修改layouts/default.vue 代码部分

1
2
3
4
5
6
7
8
9
10
11
import { getUser } from '@/utils/auth'
export default {
data() {
return {
user:{}
}
},
created() {
this.user= getUser()
}
}

判断在当前已经登陆的情况下显示当前登录用户名称和头像

1
2
3
4
<div class="sui-nav pull-right info" v-if="user.name!==undefined"> 
<li><a href="~/assets/other-notice.html" class="notice">{{user.name}}</a></li>
<li><a href="#" class="homego"><img :src="user.avatar" width="50px" height="50px" :alt="user.name"></a></li>
</div>

2.4 未登录状态显示登陆链接

修改layouts/default.vue 页面部分

1
2
3
<div class="sui-nav pull-right info" v-if="user.name===undefined"> 
<router-link to="/login">登陆</router-link>
</div>

2.5 退出登录

修改utils/auth.js

1
2
3
4
5
export function removeUser() {
Cookies.remove(TokenKey)
Cookies.remove(NameKey)
Cookies.remove(AvatarKey)
}

修改layouts/default.vue 导入removeUser方法

1
2
import { getUser,removeUser } from '@/utils/auth'
import userApi from '@/api/user'

增加退出登录的方法

1
2
3
4
5
6
methods:{
logout(){
removeUser()//清除登陆用户信息
location.href='/'
}
}

增加退出登录的链接

1
<li><a @click="logout" class="notice">退出登录</a></li>

3 微信扫码登陆

3.1 账户申请

(1)打开微信开放平台: https://open.weixin.qq.com/ 首先进行账号的注册

image-20231003233929358

(2)开发者资质认证

image-20231003233934438

(3)创建网站应用

image-20231003233939500

填写应用的相关信息

image-20231003233943826

应用通过审核后,会得到AppID和 AppSecret,后边进行编码中会使用。

AppID : wx3bdb1192c22883f3
AppSecret : db9d6b88821df403e5ff11742e799105

3.2 微信第三方登陆流程

1. 第三方发起微信授权登录请求,微信用户允许授权第三方应用后,微信会拉起应用或重定向到第三方网站,并且带上授权临时票据code参数;
2. 通过code参数加上AppID和AppSecret等,通过API换取access_token;
3. 通过access_token进行接口调用,获取用户基本数据资源或帮助用户实现基本操作。

获取access_token时序图:

image-20231003233949116

3.3 获取code

(1)修改login.vue,在登陆表单下方添加一个div, 用于显示微信登陆二维码

1
<div id="weixin"></div> 

(2)修改login.vue ,引入微信登陆二维码js

1
2
3
4
5
6
7
8
9
10
11
12
13
mounted(){
var obj = new WxLogin({
id: "weixin",
appid: "wx3bdb1192c22883f3",
scope: "snsapi_login",
redirect_uri: "http://note.java.itcast.cn/weixinlogin"
});
},
head:{
script:[
{src:'http://res.wx.qq.com/connect/zh_CN/htmledition/js/wxLogin.js'}
]
}

appid: 应用唯一标识

scope:应用授权作用于

redirect_uri:回调地址,是微信登陆成功后要跳转到的页面

(3)测试:http://localhost:3000/login 浏览器显示

image-20231003233959525

我们打开手机用微信扫二维码, 会出现以下提示

image-20231003234004629

点击确认登陆按钮,浏览器会自动跳转到

http://note.java.itcast.cn/weixinlogin?code=02147Yff12Yhgz0ArCef1qabgf147Yf0&state=undefined

这个code是微信发给用户的临时令牌。我们可以根据code再次请求微信第三方登陆接口得到access_token(正式令牌)

3.4 获取access_token

3.4.1 API

通过code获取access_token

接口说明

通过code获取access_token的接口。

请求说明

1
2
3
http请求方式: GET
https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code

参数说明

参数 是否必须 说明
appid 应用唯一标识,在微信开放平台提交应用审核通过后获得
secret 应用密钥AppSecret,在微信开放平台提交应用审核通过后获得
code 填写第一步获取的code参数
grant_type 填authorization_code

返回说明

正确的返回:

1
2
3
4
5
6
7
{
"access_token":"ACCESS_TOKEN",
"expires_in":7200,
"refresh_token":"REFRESH_TOKEN","openid":"OPENID",
"scope":"SCOPE"
}

参数 说明
access_token 接口调用凭证
expires_in access_token接口调用凭证超时时间,单位(秒)
refresh_token 用户刷新access_token
openid 授权用户唯一标识
scope 用户授权的作用域,使用逗号(,)分隔

3.4.2 编写node服务

创建新的node工程 weixinlogin 用于调用微信第三方登陆接口 工程下创建server.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var http = require('http');  
var https = require('https');
var url = require('url');
http.createServer(function(request,response){
var params=url.parse(request.url, true).query;
var appid='wx3bdb1192c22883f3';
var secret='db9d6b88821df403e5ff11742e799105';
if(params.operation==='token'){
https.get(`https://api.weixin.qq.com/sns/oauth2/access_token?appid=${appid}&secret=${secret}&code=${params.code}&grant_type=authorization_code`, function (res) {
res.on('data', function (chunk) {
response.writeHead(200,{'Content-Type':'application/json;charset=utf-8' ,"Access-Control-Allow-Origin": "*" });
response.end(chunk);
});
})
}
}).listen(8888);
// 终端打印如下信息
console.log('Server running at http://127.0.0.1:8888/');

在控制台输入 node server 运行服务

地址栏测试:http://localhost:8888/?code=02147Yff12Yhgz0ArCef1qabgf147Yf0&operation=token

结果如下:

1
2
3
4
5
6
7
8
{
"access_token": "10_zSHADX2JGMivKFfa4nMbZV3ECzY21UY3qrF5ADyjpr_iiLUifo-nlN0GaRnUEN9T7BagiwSC07awplRFIO1Ghw",
"expires_in": 7200,
"refresh_token": "10__zl8gcJz0RXVDKtksbNTQJZ2uK1HiLJZ3I5PcSkA2VB3b6WXi2CR3R_htW6B8kKOmj-91p08SJMfVKkL84vP1w",
"openid": "oypcC1u9r-mxVsRGSLFqE65lysVI",
"scope": "snsapi_login",
"unionid": "o6JuL1gaIwnVsZC5BpRYImTHKTm8"
}

3.4.3 调用node服务

node服务编写完成后,我们在十次方前台工程中调用node服务

(1)编写API ,创建api/weixin.js

1
2
3
4
5
6
import axios from 'axios'
export default {
getAccessToken(code){
return axios.get(`http://localhost:8888?operation=token&code=${code}`)
}
}

(2)创建utils/param.js (用于获取浏览器地址栏参数)

1
2
3
4
5
6
export function getUrlParam(name) {
  var reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)");
  var r = window.location.search.substr(1).match(reg);
  if(r != null) return unescape(r[2]);
  return null;
}

(3)创建pages/weixinlogin.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<template>
<div></div>
</template>
<script>
import {getUrlParam} from '@/utils/param'
import weixin from '@/api/weixin'
import {setUser} from '@/utils/auth'
export default {
mounted(){
let code=getUrlParam('code')
if(code!==null){//如果是微信登陆
//根据code获取access_token
weixin.getAccessToken(code).then( res=>{
let access_token= res.data.access_token
let openid= res.data.openid
console.log('access_token:'+access_token+ 'openid:'+openid)
})
}
}
}
</script>

3.5 获取用户昵称与头像

3.5.1 API

http请求方式: GET
https://api.weixin.qq.com/sns/userinfo?access_token=ACCESS_TOKEN&openid=OPENID

参数 是否必须 说明
access_token 调用凭证
openid 普通用户的标识,对当前开发者帐号唯一
lang 国家地区语言版本,zh_CN 简体,zh_TW 繁体,en 英语,默认为zh-CN

返回说明

正确的Json返回结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"openid":"OPENID",
"nickname":"NICKNAME",
"sex":1,
"province":"PROVINCE",
"city":"CITY",
"country":"COUNTRY",
"headimgurl": "http://wx.qlogo.cn/mmopen/g3MonUZtNHkdmzicIlibx6iaFqAc56vxLSUfpb6n5WKSYVY0ChQKkiaJSgQ1dZuTOgvLLrhJbERQQ4eMsv84eavHiaiceqxibJxCfHe/0",
"privilege":[
"PRIVILEGE1",
"PRIVILEGE2"
],
"unionid": " o6_bmasdasdsad6_2sgVt7hMZOPfL"

}
参数 说明
openid 普通用户的标识,对当前开发者帐号唯一
nickname 普通用户昵称
sex 普通用户性别,1为男性,2为女性
province 普通用户个人资料填写的省份
city 普通用户个人资料填写的城市
country 国家,如中国为CN
headimgurl 用户头像,最后一个数值代表正方形头像大小(有0、46、64、96、132数值可选,0代表640*640正方形头像),用户没有头像时该项为空
privilege 用户特权信息,json数组,如微信沃卡用户为(chinaunicom)
unionid 用户统一标识。针对一个微信开放平台帐号下的应用,同一用户的unionid是唯一的。

3.5.2 编写node服务

修改node工程 weixinlogin的server.js, 新增代码

1
2
3
4
5
6
7
8
9
10
11
.....
if(params.operation==='userinfo'){
https.get(`https://api.weixin.qq.com/sns/userinfo?access_token=${params.access_token}&openid=${params.openid}`, function (res) {
res.on('data', function (chunk) {
// 发送响应数据 "Hello World"
response.writeHead(200,{'Content-Type':'application/json;charset=utf-8' ,"Access-Control-Allow-Origin": "*" });
response.end(chunk);
});
})
}
.....

3.5.3 调用node服务

(1)编写API ,修改api/weixin.js 新增方法 用于根据access_token和openid获取用户信息

1
2
3
getUserinfo(access_token,openid){
return axios.get(`http://localhost:8888?operation=userinfo&access_token=${access_token}&openid=${openid}`)
}

(2)修改pages/weixinlogin.vue

1
import {setUser} from '@/utils/auth'

在获取access_token和openid后,再次请求接口,获取昵称和头像,保存到cookie中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
mounted(){
let code=getUrlParam('code')
if(code!==null){//如果是微信登陆
//根据code获取access_token
weixin.getAccessToken(code).then( res=>{
let access_token= res.data.access_token
let openid= res.data.openid
weixin.getUserinfo( access_token, openid ).then( res => {
//提取用户昵称和头像 **********************
let nickname= res.data.nickname
let headimgurl= res.data.headimgurl
setUser(access_token,nickname,headimgurl)
location.href='/'
})
})
}
}

3.6 域名与端口设置

我们刚才都是要通过手动更改url才能完成测试,主要是因为回调地址是域名而我们的工程是本地地址。其实我们要想实现一气呵成的效果也不难,只要通过域名和端口设置即可。

3.6.1 域名指向

我们可以通过SwitchHosts 配置域名指向

1
127.0.0.1 note.java.itcast.cn

这样我们的工程就可以通过 http://note.java.itcast.cn:3000来访问了

3.6.2 NUXT端口设置

修改package.json ,添加配置

1
2
3
4
5
"config": {
"nuxt": {
"port": "80"
}
},

重新启动工程,就可以通过http://note.java.itcast.cn 来访问了。

通过以上修改后,我们再次测试微信扫码登陆,就可以看到和生产环境一样的运行效果。

4 用户中心嵌套布局

4.1 子布局页

(1)创建pages/manager.vue ,这个是用户中心的布局页

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
<template>
<div>
<div class="myhome-personinfo" style="background-image: url('~/assets/img/widget-homebg.png');">
<div class="wrapper">
<div class="person-baseinfo">
<!--头像信息-->
<div class="photo">
<img src="~/assets/img/widget-myphoto.jpg" alt="" class="person" />
<div class="share">
<span><img src="~/assets/img/asset-QQ.png" alt="" width="34" height="28" /></span>
<span><img src="~/assets/img/asset-weixin.png" alt="" width="28" height="28" /></span>
<span><img src="~/assets/img/asset-weibo.png" alt="" width="28" height="28" /></span>
</div>
</div>
<!--文字信息-->
<div class="info">
<h1>Web爱好者<span class="allinfo"><a href="~/assets/person-myfile.html" target="_blank">查看完整档案</a></span></h1>
<ul class="fill">
<li> <i class="fa fa-map-marker" aria-hidden="true"></i> <span class="edit-item"> 填写现居城市</span>
<form action="" class="sui-form form-inline">
<input type="text" placeholder="现居城市" />
<button class="sui-btn btn-danger save-btn">保存</button>
</form> </li>
<li> <i class="fa fa-graduation-cap" aria-hidden="true"></i> <span class="edit-item"> 填写毕业院校</span>
<form action="" class="sui-form form-inline">
<input type="text" placeholder="院校名称" />
<input type="text" placeholder="所学专业" />
<button class="sui-btn btn-danger save-btn">保存</button>
</form> </li>
<li> <i class="fa fa-shopping-bag" aria-hidden="true"></i> <span class="edit-item"> 填写所在公司/组织</span>
<form action="" class="sui-form form-inline">
<input type="text" placeholder="公司/组织名称" />
<input type="text" placeholder="职位头衔" />
<button class="sui-btn btn-danger save-btn">保存</button>
</form> </li>
<li> <i class="fa fa-link" aria-hidden="true"></i> <span class="edit-item"> 填写个人网站</span>
<form action="" class="sui-form form-inline">
<input type="text" placeholder="个人网站" />
<button class="sui-btn btn-danger save-btn">保存</button>
</form> </li>
</ul>
</div>
</div>
<!--右侧编辑-->
<div class="edit-info">
<h4>个人简介<span class="addedit"><img src="~/assets/img/widget-edit.png" width="12" height="12" />编辑</span></h4>
<div class="info-box">
<div class="edit-intro">
暂时没有个人简介
</div>
</div>
</div>
<div class="clearfix"></div>
</div>
</div>
<!--两列布局-->
<div class="wrapper myhome">
<div class="left-list">
<div class="myhome-list">
<ul class="home-list">
<li class="active"><a href="~/assets/person-homepage.html">我的主页</a></li>
<li><a href="~/assets/person-myanswer.html">我的回答</a></li>
<li><a href="~/assets/person-myquestion.html">我的提问</a></li>
<li><a href="~/assets/person-myshare.html">我的分享</a></li>
</ul>
<ul class="home-list bottom">
<li><a href="~/assets/person-dynamic.html">个人动态</a></li>
<li><a href="~/assets/person-myfocus.html">我的关注</a></li>
<li><a href="~/assets/person-mycollect.html">我的收藏</a></li>
<li><a href="~/assets/person-myreaded.html">浏览记录</a></li>
<li><a href="~/assets/person-account.html">账户设置</a></li>
</ul>
</div>
</div>
<div class="right-content">
<nuxt-child/>
</div>
<div class="clearfix"></div>
</div>
</div>
</template>
<script>
import '~/assets/css/page-sj-person-homepage.css'
</script>

注意:我们使用 <nuxt-child/>标签

(2)在pages下创建manager文件夹,manager文件夹下创建index.vue(用户中心的默认首页)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
<template>
<div class="home-content">
<ul class="sui-nav nav-tabs nav-large">
<li class="active"><a href="#one" data-toggle="tab">我的提问</a></li>
<li><a href="#two" data-toggle="tab">我的回答</a></li>
</ul>
<div class="tab-content tab-wraped">
<div id="one" class="tab-pane active">
<ul class="question-list">
<li> <span class="fl good"><span class="num">12</span> 有用</span> <span class="title"><a href="#">有关PHP初级进阶的问题</a></span> <span class="fr date">4月6日</span> <span class="clearfix"></span> </li>
<li> <span class="fl good"><span class="num">12</span> 有用</span> <span class="title"><a href="#">有关JAVA初级进阶的问题</a></span> <span class="fr date">4月6日</span> <span class="clearfix"></span> </li>
<li> <span class="fl good"><span class="num">12</span> 有用</span> <span class="title"><a href="#">有关HTML5初级进阶的问题</a></span> <span class="fr date">4月6日</span> <span class="clearfix"></span> </li>
<li> <span class="fl good"><span class="num">12</span> 有用</span> <span class="title"><a href="#">有关C++初级进阶的问题</a></span> <span class="fr date">4月6日</span> <span class="clearfix"></span> </li>
<li> <span class="fl good"><span class="num">12</span> 有用</span> <span class="title"><a href="#">有关python初级进阶的问题</a></span> <span class="fr date">4月6日</span> <span class="clearfix"></span> </li>
</ul>
</div>
<div id="two" class="tab-pane">
<ul class="question-list">
<li> <span class="fl good"> <span class="num">8</span> 有用</span> <span class="title"><a href="#">有关PHP初级进阶的问题</a></span> <span class="fr date">2017-07-05 15:08</span> <span class="clearfix"></span> </li>
<li> <span class="fl good"> <span class="num">7</span> 有用</span> <span class="title"><a href="#">有关JAVA初级进阶的问题</a></span> <span class="fr date">2017-07-05 15:08</span> <span class="clearfix"></span> </li>
<li> <span class="fl good"> <span class="num">6</span> 有用</span> <span class="title"><a href="#">有关HTML5初级进阶的问题</a></span> <span class="fr date">2017-07-05 15:08</span> <span class="clearfix"></span> </li>
<li> <span class="fl good"> <span class="num">12</span> 有用</span> <span class="title"><a href="#">有关C++初级进阶的问题</a></span> <span class="fr date">2017-07-05 15:08</span> <span class="clearfix"></span> </li>
<li> <span class="fl good"> <span class="num">12</span> 有用</span> <span class="title"><a href="#">有关python初级进阶的问题</a></span> <span class="fr date">2017-07-05 15:08</span> <span class="clearfix"></span> </li>
</ul>
</div>
</div>
<div class="activities">
<h4 class="tit"><span>我的动态</span></h4>
<ul class="activities-content">
<li>
<div class="index-title">
<span class="author">本杰明</span>
<span class="operate">关注了标签</span> &middot;
<span class="time">3小时前</span>
</div>
<div class="guanzhuname">
<span class="tag">php</span>
<span class="tagnum">100</span> 关注
</div>
<div class="intro">
PHP,是英文超文本预处理语言 Hypertext Preprocessor 的缩写。PHP 是一种开源的通用计算机脚本语言,尤其适用于网络开发并可嵌入HTML中使用。PHP 的语法借鉴吸收C语言、Java和Perl等流行计算机语言的特点,易于一般程序员学习。
</div> </li>
<li>
<div class="index-title">
<span class="author">本杰明</span>
<span class="operate">回答了问题</span> &middot;
<span class="time">3小时前</span>
</div>
<div class="question">
<p class="title">网页链接如何直接打开微信,并进入公众号关注页面</p>
<p class="content">现在针对这个微信是屏蔽的,你可以选择通过连接到一个其他的公众号文章中进行关注。</p>
</div>
<div class="qa-num">
<span>关注<i>1</i></span>
<span>回答<i>2</i></span>
</div> </li>
<li>
<div class="index-title">
<span class="author">本杰明</span>
<span class="operate">收藏了文章</span> &middot;
<span class="time">3小时前</span>
</div>
<div class="question">
<p class="title">网页链接如何直接打开微信,并进入公众号关注页面</p>
</div>
<div class="qa-num">
<span><a href="#">http://baidu.com</a></span>
</div> </li>
<li>
<div class="index-title">
<span class="author">本杰明</span>
<span class="operate">收藏了文章</span> &middot;
<span class="time">3小时前</span>
</div>
<div class="question">
<p class="title">网页链接如何直接打开微信,并进入公众号关注页面</p>
</div>
<div class="qa-num">
<span><a href="#">http://baidu.com</a></span>
</div> </li>
<li>
<div class="index-title">
<span class="author">本杰明</span>
<span class="operate">回答了问题</span> &middot;
<span class="time">3小时前</span>
</div>
<div class="question">
<p class="title">网页链接如何直接打开微信,并进入公众号关注页面</p>
<p class="content">现在针对这个微信是屏蔽的,你可以选择通过连接到一个其他的公众号文章中进行关注。</p>
</div>
<div class="qa-num">
<span>关注<i>1</i></span>
<span>回答<i>2</i></span>
</div> </li>
</ul>
</div>
</div>
</template>

4.2 用户中心各子页面

(1)创建pages/manager/myanswer.vue(我的问答)

(2)创建pages/manager/myquestion.vue(我的提问)

(3)创建pages/manager/myshare.vue(我的分享)

(4)创建pages/manager/dynamic.vue(个人动态)

(5)创建pages/manager/myfocus.vue(我的关注)

(6)创建pages/manager/mycollect.vue(我的收藏)

(7)创建pages/manager/myreaded.vue (浏览记录)

(8)创建pages/manager/account.vue(账户设置)

4.3 菜单样式处理

修改pages/manager.vue中的链接地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<div class="myhome-list"> 
<ul class="home-list">
<router-link to="/manager" active-class="active" tag="li" exact ><a>我的主页</a></router-link>
<router-link to="/manager/myanswer" active-class="active" tag="li" exact ><a>我的回答</a></router-link>
<router-link to="/manager/myquestion" active-class="active" tag="li" exact ><a>我的提问</a></router-link>
<router-link to="/manager/myshare" active-class="active" tag="li" exact ><a>我的分享</a></router-link>
</ul>
<ul class="home-list bottom">
<router-link to="/manager/dynamic" active-class="active" tag="li" exact ><a>个人动态</a></router-link>
<router-link to="/manager/myfocus" active-class="active" tag="li" exact ><a>我的关注</a></router-link>
<router-link to="/manager/mycollect" active-class="active" tag="li" exact ><a>我的收藏</a></router-link>
<router-link to="/manager/myreaded" active-class="active" tag="li" exact ><a>浏览记录</a></router-link>
<router-link to="/manager/account" active-class="active" tag="li" exact ><a>账户设置</a></router-link>
</ul>
</div>

4.4 用户中心鉴权

修改pages/manager.vue代码部分

1
2
3
4
5
6
7
8
9
import '~/assets/css/page-sj-person-homepage.css'
import {getUser} from '@/utils/auth'
export default {
created(){
if(getUser().name===undefined){
this.$router.push('/login')
}
}
}

测试:在未登录的情况下在地址栏输入http://localhost:3000/manager 会自动跳转到登录页

修改layouts/default.vue的用户名与头像,修改链接到用户中心

1
2
3
<li><a href="/manager" class="notice">{{user.name}}</a></li>     
...
<li><a href="/manager" class="homego"><img :src="user.avatar" width="50px" height="50px" :alt="user.name" /></a></li>

第7章 网站前台-吐槽与问答

学习目标:

  • 完成吐槽列表与详细页
  • 完成发吐槽与评论功能,掌握富文本编辑器的使用
  • 完成问答频道功能
  • 掌握DataURL和阿里云OSS

1 吐槽列表与详细页

1.1 吐槽列表页

1.1.1 吐槽列表页数据渲染

吐槽列表页已经构建,我们现在来实现数据的渲染

(1)easyMock模拟数据

URL: spit/spit/search/{page}/{size}

Method: post

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
"code": 20000,
"flag": true,
"message": "查询成功",
"data": {
"total": "@integer(60, 100)",
"rows|10": [{
"id": "@string",
"content": "@cword(100,300)",
"publishtime": "@datetime",
"userid": "@string",
"nickname": "小雅",
"visits": "@integer(60, 100)",
"thumbup": "@integer(60, 100)",
"share": "@integer(60, 100)",
"comment": "@integer(60, 100)",
"state": "@string",
"parentid": "@string"
}]
}
}

(2)api目录下创建spit.js

1
2
3
4
5
6
7
8
9
10
11
12
import request from '@/utils/request'
const group_name = 'spit'
const api_name = 'spit'
export default {
search(page, size, searchMap) {
return request({
url: `/${group_name}/${api_name}/search/${page}/${size}`,
method: 'post',
data: searchMap
})
}
}

(3)修改pages/spit/index.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
<template>
<div>
<div class="wrapper tag-item">
<div class="fl left-list">
<div class="tc-data-list">
<div class="tc-list">
<ul class="detail-list">
<li class="qa-item" v-for="(item,index) in items" :key="index">
<div class="fl record">
<div class="number">
<div class="border useful">
<p class="usenum"><a href="#" class="zan"><i class="fa fa-thumbs-up " aria-hidden="true"></i></a></p>
<p class="zannum"> {{item.thumbup}} </p>
</div>
<div class="border answer">
<a href="#" class="star"><i class="fa fa-star-o" aria-hidden="true"></i></a>
</div>
</div>
</div>
<div class="info">
<p class="text"> <a href="~/assets/spit-detail.html" target="_blank"> {{item.content}} </a> </p>
<div class="other">
<div class="fl date">
<span>{{item.publishtime}}</span>
</div>
<div class="fr remark">
<a href="#" data-toggle="modal" data-target="#shareModal" class="share"><i class="fa fa-share-alt" aria-hidden="true"></i> 分享</a>
<a href="#" data-toggle="modal" data-target="#remarkModal" class="comment"><i class="fa fa-commenting" aria-hidden="true"></i> 回复</a>
</div>
</div>
</div>
<div class="clearfix"></div> </li>
</ul>
<div class="modal fade" id="shareModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title" id="myModalLabel">分享到</h4>
</div>
<div class="modal-body" style="overflow:hidden">
<div class="jiathis_style_32x32">
<a class="jiathis_button_qzone"></a>
<a class="jiathis_button_tsina"></a>
<a class="jiathis_button_weixin"></a>
<a class="jiathis_button_cqq"></a>
</div>
</div>
</div>
</div>
</div>
<div class="modal fade" id="remarkModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title" id="myModalLabel">发表评论</h4>
</div>
<div class="modal-body">
<div class="comment">
<span class="who"><img src="~/assets/img/asset-photo.png" />匿名评论</span>: 今天入职腾讯,产品岗,普通非985211大学,上家是不到五十人创业小公司!16年毕业!找内推腾讯给的面试机会,五轮面试!可能是我把运气攒一起了!
</div>
<div class="form">
<textarea class="form-control" rows="5" placeholder="匿名评论"></textarea>
<div class="remark">
<button class="sui-btn btn-info">发表</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="fl right-tag">
<div class="block-btn">
<p>来个匿名吐槽,发泄一下你心中的怒火吧!</p>
<a class="sui-btn btn-block btn-share" href="~/assets/spit-submit.html" target="_blank">发吐槽</a>
</div>
</div>
<div class="clearfix"></div>
</div>
</div>
</template>
<script>
import '~/assets/css/page-sj-spit-index.css'
import spitApi from '@/api/spit'
export default {
asyncData(){
return spitApi.search(1,10, {state:'1'} ).then( res=> {
return {items:res.data.data.rows}
})
}
}
</script>

1.1.2 吐槽列表瀑布流

修改页面pages/spit/index.vue

1
<div class="tc-list" v-infinite-scroll="loadMore" >

修改pages/spit/index.vue ,添加pageNo用于记录页码 增加方法

1
2
3
4
5
6
7
8
9
10
11
12
13
data(){
return {
pageNo: 1
}
},
methods:{
loadMore(){
this.pageNo++
spitApi.search( this.pageNo,10,{state:'1'} ).then( res=>{
this.items=this.items.concat( res.data.data.rows )
})
}
}

1.2 吐槽详情页

1.2.1 构建吐槽详情页

(1)根据spit-detail.html创建pages/spit/_id.vue ,内容略

(2)修改pages/spit/index.vue 页面链接

1
<router-link :to="'/spit/'+item.id"> {{item.content}} </router-link>

1.2.2 吐槽详情页数据渲染

(1)easyMock模拟数据

URL: spit/spit/{id}

Methos: GET

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"code": 20000,
"flag": true,
"message": "@string",
"data": {
"id": "@string",
"nikename": "小雅",
"content": "@cword(100,300)",
"publishtime": "@datetime",
"thumbup": "@integer(60, 100)",
"share": "@integer(60, 100)",
"comment": "@integer(60, 100)"
}
}

URL: spit/spit/commentlist/{id}

Method: GET

1
2
3
4
5
6
7
8
9
10
11
12
{
"code": 20000,
"flag": true,
"message": "@string",
"data|10": [{
"id": "@string",
"content": "我要评论我要评论我要评论我要评论我要评论我要评论",
"nikename": "小雅",
"publishtime": "@datetime",
"thumbup": "@integer(60, 100)"
}]
}

(2)修改api/spit.js

1
2
3
4
5
6
7
8
9
10
11
12
findById(id){
return request({
url: `/${api_group}/${api_name}/${id}`,
method: 'get'
})
},
commentlist(id){
return request({
url: `/${api_group}/${api_name}/commentlist/${id}`,
method: 'get'
})
}

(3)修改pages/spit/_id.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
<template>
<div class="wrapper tc-detail">
<div class="fl left-list">
<div class="tc-detail">
<!-- 标题区 -->
<div class="detail-tit">
<div class="detail-author">
<a href="javascript:;">{{pojo.nickname}}</a> 发布
</div>
<div class="detail-content">
<p>{{pojo.content}}</p>

</div>
<div class="detail-tool">
<ul>
<li><span class="star"><i class="fa fa-thumbs-o-up" aria-hidden="true"></i> {{pojo.thumbup}}</span></li>
<li><a href="#" data-toggle="modal" data-target="#shareModal"><i class="fa fa-share-alt" aria-hidden="true"></i> {{pojo.share}}</a></li>
<li><a data-toggle="modal" data-target="#remarkModal"><i class="fa fa-commenting" aria-hidden="true"></i> {{pojo.comment}}</a></li>
</ul>
</div>
</div>
<!-- 评论区 -->
<div class="comment-area">
<div class="comment-tit">
<span>评论</span>
</div>
<ul class="comment-list">
<li v-for="(item,index) in commentlist" :key="index">
<div class="item-photo">
<img src="~/assets/img/widget-widget-photo.png" alt="" />
</div>
<div class="item-content">
<p class="author"><a href="javascript:;">{{item.nickname}}</a> 发布</p>
<p class="content">{{item.content}}</p>
</div>
<div class="item-thumb">
<div>
<i class="fa fa-thumbs-o-up" aria-hidden="true"></i> {{item.thumbup}}
</div>
</div> </li>

</ul>
</div>
</div>
</div>
<div class="fl right-tag">
<div class="block-btn">
<p>来个匿名吐槽,发泄一下你心中的怒火吧!</p>
<a class="sui-btn btn-block btn-share" href="~/assets/spit-submit.html" target="_blank">发吐槽</a>
</div>
</div>
<div class="clearfix"></div>
</div>
</template>
<script>
import '~/assets/css/page-sj-spit-detail.css'
import spitApi from '@/api/spit'
import axios from 'axios'
export default {
asyncData({params}){
return axios.all( [ spitApi.findById(params.id),spitApi.commentlist(params.id) ] ).then(
axios.spread( function( pojo,commentlist ){
return {
pojo: pojo.data.data,
commentlist: commentlist.data.data
}
})
)
}
}
</script>

1.3 吐槽点赞

1.3.1 基本功能

(1)easyMock模拟数据

URL: spit/spit/thumbup/{id}

Method: put

1
2
3
4
5
{
"code": 20000,
"flag": true,
"message": "执行成功"
}

(2)api/spit.js 增加方法

1
2
3
4
5
6
thumbup(id) {
return request({
url: `/${api_group}/${api_name}/thumbup/${id}`,
method: 'put'
})
}

(3)修改pages/spit/index.vue

1
2
3
4
5
6
7
8
9
methods: {
thumbup(index){
spitApi.thumbup(this.items[index].id).then( res=>{
if(res.data.flag){
this.items[index].thumbup++
}
})
}
}

(4)点赞调用

1
<p class="usenum" @click="thumbup(index)"><a href="#" class="zan"><i class="fa fa-thumbs-up " aria-hidden="true"></i></a></p>    

1.3.2 样式处理

实现点赞后,大拇指图标变色。样式表已经写好,在li 的样式上加上color 即可

(1)修改pages/spit/index.vue 的代码部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import '~/assets/css/page-sj-spit-index.css'
import spitApi from '@/api/spit'
export default {
asyncData(){
return spitApi.search(1,10, {state:'1'} ).then( res=> {
let tmp= res.data.data.rows.map( item=>{
return {
...item,
zan: ''
}
})
return {items:tmp}
})
},
data(){
return {
pageNo: 1
}
},
methods:{
loadMore(){
this.pageNo++
spitApi.search( this.pageNo,10,{state:'1'} ).then( res=>{
let tmp= res.data.data.rows.map( item=>{
return {
...item,
zan: ''
}
})
this.items=this.items.concat( tmp )
})
},
thumbup(index){
this.items[index].zan='color'
spitApi.thumbup(this.items[index].id).then( res=>{
if(res.data.flag){
this.items[index].thumbup++
}
})
}
}
}

(2)修改pages/spit/index.vue的html部分

1
2
3
<a href="#" class="zan">
<i :class="'fa fa-thumbs-up '+item.zan" aria-hidden="true"></i>
</a>

1.3.3 判断是否登陆

要求:点赞必须要在用户登陆的情况下执行,非登陆状态下不能点赞。并且不可重复点赞

导入getUser

1
import {getUser} from '@/utils/auth'

修改thumbup(点赞)方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
thumbup(index){
if(getUser().name===undefined){
this.$message({
message:'必须登陆才可以点赞哦~',
type:"warning"
})
return
}
if( this.items[index].zan==='color'){
this.$message({
message:'不可以重复点赞哦~',
type:"warning"
})
return
}
this.items[index].zan='color'
spitApi.thumbup(this.items[index].id).then( res=>{
if(res.data.flag){
this.items[index].thumbup++
}
})
}

1.3.4 提交token

修改utils/request.js ,每次请求将token添加到header里

1
2
3
4
5
6
7
8
9
import axios from 'axios'
import {getUser} from '@/utils/auth'
// 创建axios实例
const service = axios.create({
baseURL: 'http://192.168.184.133:7300/mock/5af314a4c612520d0d7650c7', // api的base_url
timeout: 30000, // 请求超时时间
headers: { 'Authorization': 'Bearer '+getUser().token }
})
export default service

2 发吐槽与吐槽评论

2.1 发吐槽

2.1.1 构建页面

我们这里用到VUE常用的富文本编辑器vue-quill-editor

详见文档: https://www.npmjs.com/package/vue-quill-editor

(1)安装vue-quill-editor

1
cnpm install vue-quill-editor --save

(2)plugins下创建nuxt-quill-plugin.js

1
2
3
import Vue from 'vue'
import VueQuillEditor from 'vue-quill-editor/dist/ssr'
Vue.use(VueQuillEditor)

(3)修改nuxt.config.js ,配置插件和样式

1
2
3
4
5
6
7
8
9
plugins: [
{ src: '~plugins/nuxt-quill-plugin.js', ssr: false }
],
// some nuxt config...
css: [
'quill/dist/quill.snow.css',
'quill/dist/quill.bubble.css',
'quill/dist/quill.core.css'
],

(4)pages/spit/submit.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
<template>
<div class="wrapper release-tc">
<div class="release-box">
<h3>发布吐槽</h3>
<div class="editor">
<div class="quill-editor"
:content="content"
@change="onEditorChange($event)"
@blur="onEditorBlur($event)"
@focus="onEditorFocus($event)"
@ready="onEditorReady($event)"
v-quill:myQuillEditor="editorOption">
</div>

<div class="btns">
<button class="sui-btn btn-danger btn-release">发布</button>
</div>
<div class="clearfix"></div>
</div>
</div>
<div class="clearfix"></div>
</div>

</template>
<script>
import '~/assets/css/page-sj-spit-submit.css'
export default {
data () {
return {
content: '',
editorOption: {
// some quill options
modules: {
toolbar: [
[{ 'size': ['small', false, 'large'] }],
['bold', 'italic'],
[{ 'list': 'ordered'}, { 'list': 'bullet' }],
['link', 'image']
]
}
}
}
},
mounted() {
console.log('app init, my quill insrance object is:', this.myQuillEditor)
/*setTimeout(() => {
this.content = 'i am changed'
}, 3000)*/
},
methods: {
onEditorBlur(editor) {
console.log('editor blur!', editor)
},
onEditorFocus(editor) {
console.log('editor focus!', editor)
},
onEditorReady(editor) {
console.log('editor ready!', editor)
},
onEditorChange({ editor, html, text }) {
console.log('editor change!', editor, html, text)
this.content = html
}
}
}
</script>
<style>
.quill-editor {
min-height: 200px;
max-height: 400px;
overflow-y: auto;
}

</style>

(5)修改pages/spit/index.vue 链接到此页面

1
<router-link class="sui-btn btn-block btn-share" to="/spit/submit">发吐槽</router-link>

2.1.2 提交吐槽

(1)easyMock模拟数据

URL:spit/spit

Method: post

1
2
3
4
5
{
"code": 20000,
"flag": true,
"message": "执行成功"
}

(2)修改api/spit.js ,增加提交吐槽的方法

1
2
3
4
5
6
7
save(pojo) {
return request({
url: `/${group_name}/${api_name}`,
method: 'post',
data: pojo
})
}

(2)修改pages/spit/submit.vue 引入API

1
import spitApi from '@/api/spit'

在methods增加方法

1
2
3
4
5
6
7
8
9
10
11
save(){
spitApi.save({ content:this.content } ).then(res=>{
this.$message({
message: res.data.message,
type: (res.data.flag?'success':'error')
})
if(res.data.flag){
this.$router.push('/spit')
}
})
}

$router.push()的作用是路由跳转。

(3)发布按钮调用方法

1
<button class="sui-btn btn-danger btn-release" @click="save">发布</button> 

2.2 吐槽评论

2.2.1 评论弹出框

我们这里的评论弹出框使用elementUI的弹出框来实现

(1)修改pages/spit/_id.vue ,添加弹出框, 弹出框中放置富文本编辑器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<el-dialog
title="评论"
:visible.sync="dialogVisible"
width="40%" >
<div class="quill-editor"
:content="content"
@change="onEditorChange($event)"
v-quill:myQuillEditor="editorOption">
</div>
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">取 消</el-button>
<el-button type="primary" @click="dialogVisible = false">确 定</el-button>
</span>
</el-dialog>

为富文本编辑框添加样式:

1
2
3
4
5
6
7
<style>
.quill-editor {
min-height: 200px;
max-height: 400px;
overflow-y: auto;
}
</style>

(2)修改pages/spit/index.vue代码部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
data(){
return {
dialogVisible: false,
content: '',
editorOption: {
// some quill options
modules: {
toolbar: [
[{ 'size': ['small', false, 'large'] }],
['bold', 'italic'],
[{ 'list': 'ordered'}, { 'list': 'bullet' }],
['link', 'image']
]
}
}
}
},
methods:{
onEditorChange({ editor, html, text }) {
console.log('editor change!', editor, html, text)
this.content = html
}
}

(3)修改pages/spit/_id.vue 中的回复链接

1
<a @click="dialogVisible=true;content=''"><i  class="fa fa-commenting" aria-hidden="true"></i> {{pojo.comment}}</a>

2.2.2 提交评论

修改pages/spit/_id.vue ,增加提交回复的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
save(){
spitApi.save({ content:this.content,parentid: this.pojo.id } ).then(res=>{
this.$message({
message: res.data.message,
type: (res.data.flag?'success':'error')
})
if(res.data.flag){
this.dialogVisible=false
spitApi.commentlist(this.pojo.id).then(res=>{
this.commentlist=res.data.data
})
}
})
}

编辑提交按钮

1
<el-button type="primary" @click="save">提交</el-button>

3 问答频道

3.1 嵌套布局与标签导航

3.1.1 嵌套布局

(1)创建pages/qa.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<template>
<div>
<div class="tab-nav ">
<div class="wrapper">
<ul class="fl sui-nav nav-tabs navbar-dark">
<li class="active"><a href="#index" data-toggle="tab">首页</a></li>
<li><a href="#php" data-toggle="tab">Php</a></li>
<li><a href="#js" data-toggle="tab">Javascript </a></li>
<li><a href="#python" data-toggle="tab">Python</a></li>
<li><a href="#java" data-toggle="tab">Java</a></li>
</ul>
<span class="fr more"><a href="./qa-allTag.html" target="_blank">更多</a></span>
<div class="clearfix"></div>
</div>
</div>
<nuxt-child/>
</div>
</template>
<script>
import '~/assets/css/page-sj-qa-logined.css'
export default {

}
</script>

(2)创建pages/qa/label/_label.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
<template>
<div class="wrapper qa-content">
<div class="fl left-list">
<div class="tab-content">
<div id="index" class="tab-pane active">
<div class="tab-bottom-line">
<ul class="sui-nav nav-tabs">
<li class="active"><a href="#new" data-toggle="tab">最新回答</a></li>
<li><a href="#hot" data-toggle="tab">热门回答</a></li>
<li><a href="#wait" data-toggle="tab">等待回答</a></li>
</ul>
<div class="qa-list">
<div class="tab-content">
<div id="new" class="tab-pane active">
<ul class="detail-list">
<li class="qa-item">
<div class="fl record">
<div class="number">
<div class="border useful">
<p class="usenum">12</p>
<p>有用</p>
</div>
<div class="border answer">
<p class="ansnum">9</p>
<p>回答</p>
</div>
</div>
</div>
<div class="fl info">
<div class="question">
<p class="author"><span class="name">luckness</span><span>3</span>分钟前回答</p>
<p class="title"><a href="./qa-detail.html" target="_blank">有关PHP初级进阶的问题?</a></p>
</div>
<div class="other">
<ul class="fl sui-tag">
<li>Php</li>
<li>Javascript</li>
</ul>
<div class="fr brower">
<p>浏览量 50 | 2017-07-05 15:09 来自 <a href="#">毕鹏 </a></p>
</div>
</div>
</div>
<div class="clearfix"></div> </li>
<li class="qa-item">
<div class="fl record">
<div class="number">
<div class="border useful">
<p class="usenum">12</p>
<p>有用</p>
</div>
<div class="border answer">
<p class="ansnum">9</p>
<p>回答</p>
</div>
</div>
</div>
<div class="fl info">
<div class="question">
<p class="author"><span class="name">牛奶糖</span><span>3</span>分钟前回答</p>
<p class="title"><a href="./qa-detail.html" target="_blank">springMVC的controller接收json数据失败</a></p>
</div>
<div class="other">
<ul class="fl sui-tag">
<li>Php</li>
<li>Javascript</li>
</ul>
<div class="fr brower">
<p>浏览量 50 | 2017-07-05 15:09 来自 <a href="#">毕鹏 </a></p>
</div>
</div>
</div>
<div class="clearfix"></div> </li>
<li class="qa-item">
<div class="fl record">
<div class="number">
<div class="border useful">
<p class="usenum">12</p>
<p>有用</p>
</div>
<div class="border answer">
<p class="ansnum">9</p>
<p>回答</p>
</div>
</div>
</div>
<div class="fl info">
<div class="question">
<p class="author"><span class="name">大白兔</span><span>3</span>分钟前回答</p>
<p class="title"><a href="./qa-detail.html" target="_blank">监听器中timer查询数据库</a></p>
</div>
<div class="other">
<ul class="fl sui-tag">
<li>Php</li>
<li>Javascript</li>
</ul>
<div class="fr brower">
<p>浏览量 50 | 2017-07-05 15:09 来自 <a href="#">毕鹏 </a></p>
</div>
</div>
</div>
<div class="clearfix"></div> </li>
<li class="qa-item">
<div class="fl record">
<div class="number">
<div class="border useful">
<p class="usenum">34</p>
<p>有用</p>
</div>
<div class="border answer">
<p class="ansnum">9</p>
<p>回答</p>
</div>
</div>
</div>
<div class="fl info">
<div class="question">
<p class="author"><span class="name">luckness</span><span>3</span>分钟前回答</p>
<p class="title"><a href="./qa-detail.html" target="_blank">服务器上安装了一个考试系统ASP.NET,安装完成后访问不了,求助</a></p>
</div>
<div class="other">
<ul class="fl sui-tag">
<li>Php</li>
<li>Javascript</li>
</ul>
<div class="fr brower">
<p>浏览量 50 | 2017-07-05 15:09 来自 <a href="#">毕鹏 </a></p>
</div>
</div>
</div>
<div class="clearfix"></div> </li>
<li class="qa-item">
<div class="fl record">
<div class="number">
<div class="border useful">
<p class="usenum">12</p>
<p>有用</p>
</div>
<div class="border answer">
<p class="ansnum">9</p>
<p>回答</p>
</div>
</div>
</div>
<div class="fl info">
<div class="question">
<p class="author"><span class="name">牛奶糖</span><span>3</span>分钟前回答</p>
<p class="title"><a href="./qa-detail.html" target="_blank">springMVC的controller接收json数据失败</a></p>
</div>
<div class="other">
<ul class="fl sui-tag">
<li>Php</li>
<li>Javascript</li>
</ul>
<div class="fr brower">
<p>浏览量 50 | 2017-07-05 15:09 来自 <a href="#">毕鹏 </a></p>
</div>
</div>
</div>
<div class="clearfix"></div> </li>
<li class="qa-item">
<div class="fl record">
<div class="number">
<div class="border useful">
<p class="usenum">12</p>
<p>有用</p>
</div>
<div class="border answer">
<p class="ansnum">9</p>
<p>回答</p>
</div>
</div>
</div>
<div class="fl info">
<div class="question">
<p class="author"><span class="name">大白兔</span><span>3</span>分钟前回答</p>
<p class="title"><a href="./qa-detail.html" target="_blank">监听器中timer查询数据库</a></p>
</div>
<div class="other">
<ul class="fl sui-tag">
<li>Php</li>
<li>Javascript</li>
</ul>
<div class="fr brower">
<p>浏览量 50 | 2017-07-05 15:09 来自 <a href="#">毕鹏 </a></p>
</div>
</div>
</div>
<div class="clearfix"></div> </li>
</ul>
</div>
<div id="hot" class="tab-pane">
<p>热门回答</p>
</div>
<div id="wait" class="tab-pane">
<p>等待回答</p>
</div>
</div>
</div>
</div>
</div>
<div id="php" class="tab-pane">
php
</div>
<div id="js" class="tab-pane">
Javascript
</div>
<div id="python" class="tab-pane">
python
</div>
<div id="java" class="tab-pane">
java
</div>
</div>
</div>
<div class="fl right-tag">
<div class="block-btn">
<p>今天,有什么好东西要和大家分享么?</p>
<a class="sui-btn btn-block btn-share" href="./qa-submit.html" target="_blank">发布问题</a>
</div>
<div class="hot-tags">
<div class="head">
<h3 class="title">热门标签</h3>
</div>
<div class="tags">
<ul class="sui-tag">
<li>Php</li>
<li>Javascript</li>
<li>Gif</li>
<li>Java</li>
<li>C#</li>
<li>iOS</li>
<li>C++</li>
</ul>
</div>
</div>
</div>
<div class="clearfix"></div>
</div>
</template>

(3)创建pages/qa/index.vue

1
2
3
4
5
6
7
8
9
10
<template>
<div></div>
</template>
<script>
export default {
created(){
this.$router.push('/qa/label/0')
}
}
</script>

3.1.2 标签导航

(1)easyMock模拟数据

URL: base/label/toplist

Method: GET

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
"flag": true,
"code": 20000,
"data": [{
"id": "1",
"labelname": "JAVA"
},
{
"id": "2",
"labelname": "PHP"
},
{
"id": "3",
"labelname": "前端"
},
{
"id": "4",
"labelname": "Python"
}
]
}

(2)编写标签API 创建api/label.js

1
2
3
4
5
6
7
8
9
10
11
12
import request from '@/utils/request'
import {getUser} from '@/utils/auth'
const api_group = 'base'
const api_name = 'label'
export default {
toplist() {
return request({
url: `/${api_group}/${api_name}/toplist`,
method: 'get'
})
}
}

(3)修改pages/qa.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<template>
<div>
<div class="tab-nav ">
<div class="wrapper">
<ul class="fl sui-nav nav-tabs navbar-dark">
<router-link tag="li" to="/qa" active-class="active" exact ><a> 首页</a></router-link>
<router-link tag="li" :to="'/qa/label/'+item.id" active-class="active" v-for="(item,index) in labelList" :key="index" >
<a>{{item.labelname}} </a>
</router-link>
</ul>
<span class="fr more"><a href="./qa-allTag.html" target="_blank">更多</a></span>
<div class="clearfix"></div>
</div>
</div>
<nuxt-child/>
</div>
</template>
<script>
import labelApi from '@/api/label'
export default {
asyncData ({ params, error}) {
return labelApi.toplist().then((res) => {
return {labelList: res.data.data }
})
}
}
</script>

(4)创建pages/qa/index.vue

1
2
3
4
5
<template>
<div>
这里是问答列表
</div>
</template>

3.2 问答列表

3.2.1 最新问答列表

(1)easy-mock模拟数据

URL:/qa/problem/newlist/{label}/{page}/{size}

Method:GET

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
"code": "@integer(60, 100)",
"flag": "@boolean",
"message": "@string",
"data": {
"total": "@integer(60, 100)",
"rows|10": [{
"id": "@integer(1, 1000)",
"title": "@cword(20,30)",
"content": "@string",
"createtime": "@datetime",
"updatetime": "@datetime",
"userid": "@integer(1, 1000)",
"nickname": "小马",
"visits": "@integer(60, 100)",
"thumbup": "@integer(60, 100)",
"reply": "@integer(60, 100)",
"solve": "@string",
"replyname": "小牛",
"replytime": "@datetime"
}]
}
}

(2)API编写 创建api/problem.js

1
2
3
4
5
6
7
8
9
10
11
import request from '@/utils/request'
const group_name = 'qa'
const api_name = 'problem'
export default {
list(type,label,page,size){
return request({
url:`/${group_name}/${api_name}/${type}/${label}/${page}/${size}`,
method: 'get'
})
}
}

(3)修改pages/qa/label/_label.vue 脚本部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import problemApi from '@/api/problem'
import axios from 'axios'
export default {
asyncData({params}){
return axios.all([problemApi.list('newlist',params.label,1,10),
problemApi.list('hotlist',params.label,1,10),
problemApi.list('waitlist',params.label,1,10) ] ).then( axios.spread(function(newlist,hotlist,waitlist ){
return {
newlist:newlist.data.data.rows,
hotlist:hotlist.data.data.rows,
waitlist:waitlist.data.data.rows
}
}))
}
}

(4)修改pages/qa/label/_label.vue 模板部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<ul class="detail-list"> 
<li class="qa-item" v-for="(item,index) in newlist" :key="index">
<div class="fl record">
<div class="number">
<div class="border useful">
<p class="usenum">{{item.thumbup}}</p>
<p>有用</p>
</div>
<div class="border answer">
<p class="ansnum">{{item.reply}}</p>
<p>回答</p>
</div>
</div>
</div>
<div class="fl info">
<div class="question">
<p class="author"><span class="name">{{item.replyname}}</span><span>{{item.replytime}}</span>回答</p>
<p class="title"><a href="./qa-detail.html" target="_blank">{{item.title}}</a></p>
</div>
<div class="other">
<div class="fr brower">
<p>浏览量 {{item.visits}} | {{item.createtime}} 来自 <a href="#">{{item.nickname}} </a></p>
</div>
</div>
</div>
<div class="clearfix"></div> </li>
</ul>

3.2.2 热门回答和等待回答列表

(1)定义属性type ,默认值为new

1
2
3
4
5
data(){
return {
type:'new'
}
}

(2)修改选项卡

1
2
3
4
5
<ul class="sui-nav nav-tabs"> 
<li :class="type=='new'?'active':''"><a @click="type='new'">最新回答</a></li>
<li :class="type=='hot'?'active':''"><a @click="type='hot'">热门回答</a></li>
<li :class="type=='wait'?'active':''"><a @click="type='wait'">等待回答</a></li>
</ul>

(3)修改div的样式为动态获取

最新回答列表

1
2
3
<div id="new" :class="'tab-pane '+(type=='new'?'active':'')">
.....
</div>

热门回答列表

1
<div id="hot" :class="'tab-pane '+(type=='hot'?'active':'')"> 

等待回答列表

1
<div id="wait" :class="'tab-pane '+(type=='wait'?'active':'')"> 

(4)参照最新问答列表编写热门回答列表与等待回答列表内容

1
2
3
<li class="qa-item" v-for="(item,index) in hotlist" :key="index"> 
.....
<li class="qa-item" v-for="(item,index) in waitlist" :key="index">

3.2.3 问答列表瀑布流

(1)修改pages/qa/label/_label.vue模板部分

1
2
<div class="qa-list"  v-infinite-scroll="loadMore"> 
....

(2)修改pages/qa/label/_label.vue脚本部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import problemApi from '@/api/problem'
import axios from 'axios'
export default {
asyncData({params}){
return axios.all([problemApi.list('newlist',params.label,1,10),
problemApi.list('hotlist',params.label,1,10),
problemApi.list('waitlist',params.label,1,10) ] ).then( axios.spread(function(newlist,hotlist,waitlist ){
return {
newlist:newlist.data.data.rows,
hotlist:hotlist.data.data.rows,
waitlist:waitlist.data.data.rows,
label:params.label //标签ID,我们需要记录下来
}
}))
},
data(){
return {
type:'new',
page_new: 1,//记录最新问题列表的页码
page_hot: 1,//记录热门问题列表的页码
page_wait: 1//记录等待回答列表的页码
}
},
methods:{
loadMore(){
if(this.type==='new'){
this.page_new++
problemApi.list('newlist',this.label,this.page_new,10).then( res=>{
this.newlist=this.newlist.concat( res.data.data.rows )
})
}
if(this.type==='hot'){
this.page_hot++
problemApi.list('hotlist',this.label,this.page_hot,10).then( res=>{
this.hotlist=this.hotlist.concat( res.data.data.rows )
})
}
if(this.type==='wait'){
this.page_wait++
problemApi.list('waitlist',this.label,this.page_wait,10).then( res=>{
this.waitlist=this.waitlist.concat( res.data.data.rows )
})
}
}
}
}

3.3 问答详细页

学员实现

3.4 发布问题页

学员实现。使用富文本编辑器(参见吐槽模块的实现)

3.5 标签列表与关注标签

学员实现

4 图片上传

4.1 Data URL

Data URL给了我们一种很巧妙的将图片“嵌入”到HTML中的方法。跟传统的用img标记将服务器上的图片引用到页面中的方式不一样,在Data URL协议中,图片被转换成base64编码的字符串形式,并存储在URL中,冠以mime-type。

传统方式:

1
<img src="images/myimg.gif ">

这种方式中,img标记的src属性指定了一个远程服务器上的资源。当网页加载到浏览器中 时,浏览器会针对每个外部资源都向服务器发送一次拉取资源请求,占用网络资源。大多数的浏览器都有一个并发请求数不能超过4个的限制。这意味着,如果一个 网页里嵌入了过多的外部资源,这些请求会导致整个页面的加载延迟。而使用Data URL技术,图片数据以base64字符串格式嵌入到了页面中,与HTML成为一体,它的形式如下

1
<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAM8AAAA4CAYAAABaFqz+AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6OUQyRTkxNThFRDEyMTFFNjhENUY5QjM0OUZGQzEwN0IiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6OUQyRTkxNTlFRDEyMTFFNjhENUY5QjM0OUZGQzEwN0IiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo5RDJFOTE1NkVEMTIxMUU2OEQ1RjlCMzQ5RkZDMTA3QiIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo5RDJFOTE1N0VEMTIxMUU2OEQ1RjlCMzQ5RkZDMTA3QiIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/Po4slv0AABviSURBVHja7F0JfBRF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">

vue-quill-editor的图片上传默认采用Data URL方式。

4.2 辅助插件vue-quill-editor-upload

如果你不想使用Data URL方式存储图片,我们可以通过一个辅助插件vue-quill-editor-upload 来让vue-quill-editor实现传统方式的上传。

(1)安装:

1
cnpm install vue-quill-editor-upload --save

(2)修改submit.vue 引入插件

1
import {quillRedefine} from 'vue-quill-editor-upload'

(3)将editorOption的值改为{}

1
2
3
4
5
6
data () {
return {
content: '',
editorOption:{}//修改此处!
}
}

(4)新增created 钩子函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
created () {
this.editorOption = quillRedefine(
{
// 图片上传的设置
uploadConfig: {
action: 'http://localhost:3000/upload', // 必填参数 图片上传地址
// 必选参数 res是一个函数,函数接收的response为上传成功时服务器返回的数据
// 你必须把返回的数据中所包含的图片地址 return 回去
res: (respnse) => {
return respnse.info
},
name: 'img' // 图片上传参数名
}
}
)
}

4.3 Multer(了解)

课程中提供了上传图片的服务端代码 upload-server ,我们可以先测试运行后,对照在线文档阅读并理解代码(课程不要求学员独立编写此段代码)

1
2
cnpm install 
npm run start

这段代码主要应用两项技术:

(1)Express –node.js的web框架 在线文档: http://www.expressjs.com.cn/4x/api.html

(2)Multer –Express官方推出的,用于multipart/form-data请求数据处理的中间件 在线文档: https://github.com/expressjs/multer/blob/master/doc/README-zh-cn.md

4.4 云存储解决方案-阿里云OSS

为了能够解决海量数据存储与弹性扩容,我们在十次方项目中采用云存储的解决方案- 阿里云OSS。

4.4.1 准备工作

(1)申请阿里云账号并完成实名认证: 可以使用我们之前发短信用的阿里云账号。

(2)开通OSS: 登录阿里云官网。将鼠标移至产品找到并单击对象存储OSS打开OSS产品详情页面。在OSS产品详情页中的单击立即开通。开通服务后,在OSS产品详情页面单击管理控制台直接进入OSS管理控制台界面。您也可以单击位于官网首页右上方菜单栏的控制台,进入阿里云管理控制台首页,然后单击左侧的对象存储OSS菜单进入OSS管理控制台界面。

image-20231003234119836

(3)创建存储空间

新建Bucket,命名为tensquare ,读写权限为公共读

image-20231003234100261

4.4.2 代码编写

(1)安装ali-oss

1
2
cnpm install ali-oss --save
cnpm install co --save

(2)修改file-upload-demo-master的server.js

1
2
3
4
5
6
7
8
var co = require('co');
var OSS = require('ali-oss');
var client = new OSS({
accessKeyId: 'LTAIWaEERTRWSD2',
accessKeySecret: 'PznrHXxYvTcADAFFDDDJnoAokJ0NSWEWF',
endpoint: 'oss-cn-beijing.aliyuncs.com',
bucket: 'tensquare'
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
app.post('/upload', upload.single('img'), (req, res) => {	

// 没有附带文件
if (!req.file) {
res.json({ ok: false });
return;
}

co(function* () {
var stream = fs.createReadStream(req.file.path);
var result = yield client.putStream(req.file.originalname, stream);
console.log("result:"+result);

res.json({ ok: true , info: result.url})
}).catch(function (err) {
console.log(err);
});

});

co :已同步的方式调用异步的代码 配合yield关键字使用,将异步结果直接返回。

十次方前端系统开发-第8章

网站前台-交友与聊天

学习目标:

1.推荐好友列表

1.1数据渲染

(1)easyMock模拟数据

URL: friend/friend/list

Method: GET

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
{
"flag": true,
"code": 20000,
"data": [{
"nickname": "小雅",
"pic": "https://ss3.bdstatic.com/70cFv8Sh_Q1YnxGkpoWK1HF6hhy/it/u=1872448934,405071047&fm=27&gp=0.jpg",
"age": 24,
"constellation": "金牛座",
"activity":"喝咖啡"
},
{
"nickname": "婷婷",
"pic": "https://ss1.bdstatic.com/70cFuXSh_Q1YnxGkpoWK1HF6hhy/it/u=2989004090,2118875929&fm=27&gp=0.jpg",
"age": 23,
"constellation": "双子座",
"activity":"看电影"
},
{
"nickname": "慕冉",
"pic": "https://ss2.bdstatic.com/70cFvnSh_Q1YnxGkpoWK1HF6hhy/it/u=4253788808,3515932570&fm=27&gp=0.jpg",
"age": 27,
"constellation": "处女座",
"activity":"逛街"
},
{
"nickname": "茉莉莉",
"pic": "https://ss0.bdstatic.com/70cFvHSh_Q1YnxGkpoWK1HF6hhy/it/u=1819234630,314130062&fm=27&gp=0.jpg",
"age": 20,
"constellation": "射手座",
"activity":"玩游戏"
}
]
}

(2)API编写

创建api/friend.js

1
2
3
4
5
6
7
8
9
10
11
import request from '@/utils/request'
const api_group = 'friend'
const api_name = 'friend'
export default {
list() {
return request({
url: `/${api_group}/${api_name}/list`,
method: 'get'
})
}
}

(3)页面组件数据渲染

修改pages/friend/index.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
<template>
<div>
<div class="banner">
<div class="wrapper">
<img src="~/assets/img/page-banner.png" alt="" />
</div>
</div>
<!--两列布局-->
<div class="wrapper tag-item">
<div class="fl left-list">
<p class="full-info">为了获取更好的体验 请 <a href="makeFriends-edit.html" target="_blank">完善兴趣信息</a><span class="sui-icon icon-tb-close close"></span></p>
<div class="friend-dating-list">
<ul class="friends">
<li class="friend-item" v-for="(item,index) in friendList" :key="index" >
<div class="fl photo">
<span class="cafe-more"></span>
<div class="img">
<img :src="item.pic" alt="" height="148" width="239"/>
</div>
<div class="tag">
<span class="tag-cafe cafe"><i class="fa fa-coffee" aria-hidden="true"></i> {{item.activity}}</span>
</div>
</div>
<div class="fl content">
<p class="title"> <span class="name">{{item.nickname}}</span> 邀你一起 <span class="cafe">喝咖啡</span> <b class="bold">匹配度 96%</b> </p>
<p class="aa"> <span class="money"> {{item.age}}岁 | {{item.constellation}} | 教育 | 软件工程师</span> </p>
<p class="point"> 他刚刚分享了XXX文章</p>
<p class="desc"> 推荐理由:你们共同关注PHP、Python 等 5 个标签,都关注 XXX 活动。</p>
</div>
<div class="fr xy">
<ul>
<li><i class="like sui-icon icon-tb-like"></i></li>
<li><i class="close sui-icon icon-tb-roundclose"></i></li>
<li><i class="message sui-icon icon-tb-comment"></i></li>
</ul>
</div>
<div class="clearfix"></div>
</li>
</ul>
</div>
</div>
<div class="fl right-tag">
<div class="friend-info-card">
<div class="card">
<div class="photo-name">
<img src="~/assets/img/widget-photo.png" alt="" />
<div class="name-edit">
<p><span class="name">用户名</span> <span class="edit">编辑兴趣资料</span></p>
<p>认证</p>
</div>
<div class="clearfix"></div>
</div>
<div class="like">
<span class="like1">喜欢 <i class="num">400</i></span>
<span>被喜欢 <i class="num">5</i></span>
</div>
</div>
</div>
<div class="block-btn">
<p>约一约有趣的人,会一会好的时光!</p>
<a class="sui-btn btn-block btn-share" href="~/assets/makeFriends-submit.html" target="_blank">发布约会</a>
</div>
<div class="rank">
<div class="head">
<h3 class="title">排行榜</h3>
</div>
<div class="rank-list">
<ul class="rank">
<li> <span class="fl dating">喝咖啡</span> <span class="fr cafe">102258</span> </li>
<li> <span class="fl dating">吃饭</span> <span class="fr eat">9878</span> </li>
<li> <span class="fl dating">看电影</span> <span class="fr movie">2564</span> </li>
<li> <span class="fl dating">旅行</span> <span class="fr travel">897</span> </li>
</ul>
</div>
</div>
<div class="friend-line-card">
<div class="card">
<p>找个合适的参加一场线下活动</p>
</div>
</div>
<div class="friend-line-card">
<div class="card">
<p>找个合适的参加一场线下活动</p>
</div>
</div>
</div>
<div class="clearfix"></div>
</div>
</div>
</template>
<script>
import '~/assets/css/page-sj-makeFriends-index.css'
import friendApi from '@/api/friend'
export default {
asyncData ({ params, error }) {
return friendApi.list().then( res=>{
return {friendList: res.data.data }
})
}
}
</script>

1.2心动配对

(1)easyMock模拟数据

URL:friend/friend/like/{id}/{type}

Method:put

1
2
3
4
5
6
7
8
9
10
11
{
"flag":true,
"code":20000,
"data": {
"nickname": "悦悦",
"pic": "https://ss2.bdstatic.com/70cFvnSh_Q1YnxGkpoWK1HF6hhy/it/u=3814696406,295322094&fm=27&gp=0.jpg",
"age": 21,
"constellation": "狮子座",
"activity":"聊人生"
}
}

(2)编写API。 修改

1
2
3
4
5
6
like(id,type) {
return request({
url: `/${api_group}/${api_name}/like/${id}/${type}`,
method: 'put'
})
}

(3)页面调用 修改pages/friends/index.vue ,代码部分增加方法

1
2
3
4
5
6
7
8
9
10
11
12
methods:{
like(id,type){
friendApi.like(id,type).then( res=>{
for(let i=0;i<this.friendList.length;i++){
if(this.friendList[i].id===id){
this.friendList.splice(i,1)
}
}
this.friendList.push(res.data.data)
})
}
}

页面部分:

1
2
<li><i class="like sui-icon icon-tb-like" @click="like(item.id,1)"></i></li> 
<li><i class="close sui-icon icon-tb-roundclose" @click="like(item.id,0)"></i></li>

1.3样式处理

(1)修改代码部分

1
2
3
4
5
6
7
8
9
10
11
12
changeActive($event){
$event.currentTarget.className="like sui-icon icon-tb-likefill";
},
removeActive($event){
$event.currentTarget.className="like sui-icon icon-tb-like";
},
changeClose($event){
$event.currentTarget.className="close sui-icon icon-tb-roundclosefill";
},
removeClose($event){
$event.currentTarget.className="close sui-icon icon-tb-roundclose";
}

(2)修改页面部分

1
2
<li><i class="like sui-icon icon-tb-like" @click="like(item.id,1)"  @mouseover="changeActive($event)" @mouseout="removeActive($event)"></i></li> 
<li><i class="close sui-icon icon-tb-roundclose" @click="like(item.id,0)" @mouseover="changeClose($event)" @mouseout="removeClose($event)"></i></li>

2.消息中心

2.1构建页面

创建pages/friends/friendList.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
<template>
<div>
<div class="banner">
<div class="wrapper">
<img src="~/assets/img/page-banner.png" alt="" />
</div>
</div>
<!--两列布局-->
<div class="wrapper tag-item">
<div class="fl left-list">
<div class="friend-list">
<h4>消息中心</h4>
<ul>
<li class="friend-item">
<div class="tip">
<span class="num">2</span>
<img src="~/assets/img/widget-photo.png" alt="" />
</div>
<div class="msg">
<p><span class="name">毕鹏</span><span class="date">2017-10-23</span></p>
<p class="msg-content">Hi 你在干什么呢?</p>
</div> </li>
<li class="friend-item">
<div class="tip">
<span class="num">2</span>
<img src="~/assets/img/widget-photo.png" alt="" />
</div>
<div class="msg">
<p><span class="name">毕鹏</span><span class="date">2017-10-23</span></p>
<p class="msg-content">Hi 你在干什么呢?</p>
</div> </li>
<li class="friend-item">
<div class="tip">
<span class="num">2</span>
<img src="~/assets/img/widget-photo.png" alt="" />
</div>
<div class="msg">
<p><span class="name">毕鹏</span><span class="date">2017-10-23</span></p>
<p class="msg-content">Hi 你在干什么呢?</p>
</div> </li>
<li class="friend-item">
<div class="tip">
<span class="num">2</span>
<img src="~/assets/img/widget-photo.png" alt="" />
</div>
<div class="msg">
<p><span class="name">毕鹏</span><span class="date">2017-10-23</span></p>
<p class="msg-content">Hi 你在干什么呢?</p>
</div> </li>
<li class="friend-item">
<div class="tip">
<span class="num">2</span>
<img src="~/assets/img/widget-photo.png" alt="" />
</div>
<div class="msg">
<p><span class="name">毕鹏</span><span class="date">2017-10-23</span></p>
<p class="msg-content">Hi 你在干什么呢?</p>
</div> </li>
</ul>
</div>
</div>
<div class="fl right-tag">
<div class="friend-info-card">
<div class="card">
<div class="photo-name">
<img src="~/assets/img/widget-photo.png" alt="" />
<div class="name-edit">
<p><span class="name">用户名</span> <span class="edit">编辑兴趣资料</span></p>
<p>认证</p>
</div>
<div class="clearfix"></div>
</div>
<div class="like">
<span class="like1">喜欢 <i class="num">400</i></span>
<span>被喜欢 <i class="num">5</i></span>
</div>
</div>
</div>
<div class="block-btn">
<p>约一约有趣的人,会一会好的时光!</p>
<a class="sui-btn btn-block btn-share" href="~/assets/makeFriends-submit.html" target="_blank">发布约会</a>
</div>
<div class="rank">
<div class="head">
<h3 class="title">排行榜</h3>
</div>
<div class="rank-list">
<ul class="rank">
<li> <span class="fl dating">喝咖啡</span> <span class="fr cafe">102258</span> </li>
<li> <span class="fl dating">吃饭</span> <span class="fr eat">9878</span> </li>
<li> <span class="fl dating">看电影</span> <span class="fr movie">2564</span> </li>
<li> <span class="fl dating">旅行</span> <span class="fr travel">897</span> </li>
</ul>
</div>
</div>
<div class="friend-line-card">
<div class="card">
<p>找个合适的参加一场线下活动</p>
</div>
</div>
<div class="friend-line-card">
<div class="card">
<p>找个合适的参加一场线下活动</p>
</div>
</div>
</div>
<div class="clearfix"></div>
</div>
</div>
</template>
<script>
import '~/assets/css/page-sj-makeFriends-list.css'
export default {
}
</script>

2.2数据渲染

(1)easyMock模拟数据

URL: friend/friend/mylist

Method:GET

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
{
"flag": true,
"code": 20000,
"data":[
{
"id":"1",
"nickname": "悦悦",
"avatar": "https://ss1.bdstatic.com/70cFvXSh_Q1YnxGkpoWK1HF6hhy/it/u=3016890200,2017113207&fm=27&gp=0.jpg",
"unread": 3,
"message": "在吗?",
"lasttime":"2018-4-1"
},
{
"id":"2",
"nickname": "婉儿",
"avatar": "https://ss3.bdstatic.com/70cFv8Sh_Q1YnxGkpoWK1HF6hhy/it/u=394607255,721510492&fm=27&gp=0.jpg",
"unread": 10,
"message": "怎么不理我:(",
"lasttime":"2018-4-2"
},
{
"id":"3",
"nickname": "雨柯",
"avatar": "https://ss0.bdstatic.com/70cFvHSh_Q1YnxGkpoWK1HF6hhy/it/u=359604599,305372113&fm=27&gp=0.jpg",
"unread": 1,
"message": "化妆品诚招代理,微信:abcd1234",
"lasttime":"2018-4-3"
}
]
}

(2)API编写。修改api/friend.js

1
2
3
4
5
6
mylist() {   
return request({
url: `/${api_group}/${api_name}/mylist`,
method: 'get'
})
}

(3)页面渲染。修改pages/friends/friendList.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
<template>
<div>
<div class="banner">
<div class="wrapper">
<img src="~/assets/img/page-banner.png" alt="" />
</div>
</div>
<!--两列布局-->
<div class="wrapper tag-item">
<div class="fl left-list">
<div class="friend-list">
<h4>消息中心</h4>
<ul>
<li class="friend-item" v-for="(item,index) in myFriendList" :key="index">
<div class="tip">
<span class="num">{{item.unread}}</span>
<img :src="item.avatar" alt="" height="50px" width="50px"/>
</div>
<div class="msg">
<p><span class="name">{{item.nickname}}</span><span class="date">{{item.lasttime}}</span></p>
<p class="msg-content">{{item.message}}</p>
</div>
</li>
</ul>
</div>
</div>
<div class="fl right-tag">
<div class="friend-info-card">
<div class="card">
<div class="photo-name">
<img src="~/assets/img/widget-photo.png" alt="" />
<div class="name-edit">
<p><span class="name">用户名</span> <span class="edit">编辑兴趣资料</span></p>
<p>认证</p>
</div>
<div class="clearfix"></div>
</div>
<div class="like">
<span class="like1">喜欢 <i class="num">400</i></span>
<span>被喜欢 <i class="num">5</i></span>
</div>
</div>
</div>
<div class="block-btn">
<p>约一约有趣的人,会一会好的时光!</p>
<a class="sui-btn btn-block btn-share" href="~/assets/makeFriends-submit.html" target="_blank">发布约会</a>
</div>
<div class="rank">
<div class="head">
<h3 class="title">排行榜</h3>
</div>
<div class="rank-list">
<ul class="rank">
<li> <span class="fl dating">喝咖啡</span> <span class="fr cafe">102258</span> </li>
<li> <span class="fl dating">吃饭</span> <span class="fr eat">9878</span> </li>
<li> <span class="fl dating">看电影</span> <span class="fr movie">2564</span> </li>
<li> <span class="fl dating">旅行</span> <span class="fr travel">897</span> </li>
</ul>
</div>
</div>
<div class="friend-line-card">
<div class="card">
<p>找个合适的参加一场线下活动</p>
</div>
</div>
<div class="friend-line-card">
<div class="card">
<p>找个合适的参加一场线下活动</p>
</div>
</div>
</div>
<div class="clearfix"></div>
</div>
</div>
</template>
<script>
import '~/assets/css/page-sj-makeFriends-list.css'
import friendApi from '@/api/friend'
export default {
asyncData ({ params, error }) {
return friendApi.mylist().then( res=>{
return {myFriendList: res.data.data }
})
}
}
</script>

3.WebSocket快速入门

3.1WebSocket简介

3.3.1 WebSocket

​ Websocket是html5提出的一个协议规范,参考rfc6455。

​ websocket约定了一个通信的规范,通过一个握手的机制,客户端(浏览器)和服务器(webserver)之间能建立一个类似tcp的连接,从而方便c-s之间的通信。在websocket出现之前,web交互一般是基于http协议的短连接或者长连接。

​ WebSocket是为解决客户端与服务端实时通信而产生的技术。websocket协议本质上是一个基于tcp的协议,是先通过HTTP/HTTPS协议发起一条特殊的http请求进行握手后创建一个用于交换数据的TCP连接,此后服务端与客户端通过此TCP连接进行实时通信。

​ 以前web server实现推送技术或者即时通讯,用的都是轮询(polling),在特点的时间间隔(比如1秒钟)由浏览器自动发出请求,将服务器的消息主动的拉回来,在这种情况下,我们需要不断的向服务器发送请求,然而HTTP request 的header是非常长的,里面包含的数据可能只是一个很小的值,这样会占用很多的带宽和服务器资源。

​ 而最比较新的技术去做轮询的效果是Comet – 用了AJAX。但这种技术虽然可达到全双工通信,但依然需要发出请求(reuqest)。

​ WebSocket API最伟大之处在于服务器和客户端可以在给定的时间范围内的任意时刻,相互推送信息。 浏览器和服务器只需要要做一个握手的动作,在建立连接之后,服务器可以主动传送数据给客户端,客户端也可以随时向服务器发送数据。 此外,服务器与客户端之间交换的标头信息很小。

​ WebSocket并不限于以Ajax(或XHR)方式通信,因为Ajax技术需要客户端发起请求,而WebSocket服务器和客户端可以彼此相互推送信息

3.3.2 SockJS

​ SockJS是一个浏览器JavaScript库(对WebSocket原生API进行了封装),它提供了一个类似于网络的对象。SockJS提供了一个连贯的、跨浏览器的Javascript API,它在浏览器和web服务器之间创建了一个低延迟、全双工、跨域通信通道。

​ 一些浏览器中缺少对WebSocket的支持,因此,回退选项是必要的,而Spring框架提供了基于SockJS协议的透明的回退选项。

​ SockJS的一大好处在于提供了浏览器兼容性。优先使用原生WebSocket,如果在不支持websocket的浏览器中,会自动降为轮询的方式。 除此之外,spring也对socketJS提供了支持。

​ 如果java代码中添加了withSockJS() ,服务器也会自动降级为轮询。

1
registry.addEndpoint("/coordination").withSockJS();

​ SockJS的目标是让应用程序使用WebSocket API,但在运行时需要在必要时返回到非WebSocket替代,即无需更改应用程序代码。

3.3.3 STOMP

​ STOMP即Simple (or Streaming) Text Orientated Messaging Protocol,简单(流)文本定向消息协议,它提供了一个可互操作的连接格式,允许STOMP客户端与任意STOMP消息代理(Broker)进行交互。STOMP协议由于设计简单,易于开发客户端,因此在多种语言和多种平台上得到广泛地应用。

​ STOMP协议的前身是TTMP协议(一个简单的基于文本的协议),专为消息中间件设计。

​ STOMP是一个非常简单和容易实现的协议,其设计灵感源自于HTTP的简单性。尽管STOMP协议在服务器端的实现可能有一定的难度,但客户端的实现却很容易。例如,可以使用Telnet登录到任何的STOMP代理,并与STOMP代理进行交互。

3.3.4 STOMP.js

stomp.js(Stomp Over WebSocket)是使用H5 Web Socket API实现的Stomp客户端,可以实现消息实时推送.

3.2入门demo之匿名聊天室

3.2.1服务端代码

(1)创建spring boot工程 pom.xml引入起步依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.0.RELEASE</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
</dependencies>

(2)创建启动类

1
2
3
4
5
6
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}

(3)创建配置类WebSocketConfig

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic");
config.setApplicationDestinationPrefixes("/app");
}

@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/my-websocket").withSockJS();
}
}

这里配置了以“/app”开头的websocket请求url。和名为“my-websocket”的endpoint(端点)

(4)创建消息实体类SocketMessage

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class SocketMessage {

private String message;

private String date;

public String getMessage() {
return message;
}

public void setMessage(String message) {
this.message = message;
}

public String getDate() {
return date;
}

public void setDate(String date) {
this.date = date;
}
}

(5)创建ChatController,用于接受和处理聊天请求

1
2
3
4
5
6
7
8
9
10
11
@Controller
public class ChatController {

@MessageMapping("/send")
@SendTo("/topic/send")
public SocketMessage send(SocketMessage message) throws Exception {
DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
message.setDate(df.format(new Date()));
return message;
}
}

3.2.2客户端代码

在上面的SpringBoot工程的resources目录下创建static目录,static目录下创建index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<!DOCTYPE html>
<html>
<head>
<title>websocket</title>
<meta charset="UTF-8">
<script src="https://cdn.bootcss.com/sockjs-client/1.1.4/sockjs.min.js"></script>
<script src="https://cdn.bootcss.com/stomp.js/2.3.3/stomp.min.js"></script>
<script type="text/javascript">
var stompClient=null;
function connect(){
var socket = new SockJS('http://localhost:8080/my-websocket');
stompClient = Stomp.over(socket);
stompClient.connect({}, function(frame) {
// 注册发送消息
stompClient.subscribe('/topic/send', function(msg) {
var body= JSON.parse(msg.body) ;
document.getElementById('message').innerHTML += "<br>"+body.date+" "+ body.message;
});
});
}
connect();//连接
//发送
function send() {
stompClient.send("/app/send", {}, JSON.stringify({
'message' : document.getElementById('content').value
}));
}
</script>
</head>
<body>
<div>
<input type="text"
id="content" placeholder="请输入内容..." />
<button onclick="send()" type="button">发送</button>
<br/> 消息列表: <br/>
<div id="message"></div>
</div>
</body>
</html>

3.3入门demo之服务端推送

我们刚才的例子是由客户端发起请求后,服务端给于响应,那如果由服务端主动发起如何实现呢?我们在上边的例子基础上再次添加新的功能,服务端每间隔一秒将服务端时间推送到客户端。

3.3.1服务端代码

创建任务类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Component
@EnableScheduling
public class Task {

@Autowired
private SimpMessagingTemplate messagingTemplate;

@Scheduled(fixedRate = 1000)
public void callback() throws Exception {
// 发现消息
DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
messagingTemplate.convertAndSend("/topic/callback", df.format(new Date()));
}
}

3.3.2客户端代码

修改index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
function connect(){
var socket = new SockJS('http://localhost:8080/my-websocket');
stompClient = Stomp.over(socket);
stompClient.connect({}, function(frame) {
// 注册发送消息
//.......

// 注册推送时间回调
stompClient.subscribe('/topic/callback', function(r) {
document.getElementById("date").innerHTML='当前服务器时间:' + r.body;
});
});
}

3.4 入门demo之STOMP监听类

(1)创建监听类,此类的方法onApplicationEvent在用户连接时执行,可以获取用户的sessionID以及从前端传递过来的其它信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Component
public class STOMPConnectEventListener implements ApplicationListener<SessionConnectEvent> {

@Autowired
private SimpMessagingTemplate messagingTemplate;

@Override
public void onApplicationEvent(SessionConnectEvent event) {
StompHeaderAccessor sha = StompHeaderAccessor.wrap(event.getMessage());
String userId = sha.getNativeHeader("login").get(0);
String sessionId = sha.getSessionId();
System.out.println("STOMPConnectEventListener........"+userId+"-"+sessionId);
}
}

(2)修改页面index.html ,增加我的名字文本框,去掉connect()方法的自动调用

1
2
我的名字:<input id="username">
<button onclick="connect()" type="button">进入聊天室</button>

修改connect()方法 ,将当前登录用户名传递给后端

1
2
3
4
stompClient.connect({"login": document.getElementById("username").value },      function(frame) {
// 注册发送消息
// .......
});

测试一下,看看能不能再控制台打印出当前用户名和sessionId

3.5入门demo 之点对点发送

当前用户名可以获取了,我们接下来就可以实现点对点发送了,开启私聊模式 :)

(1)首先我们应该写个静态变量保存当前登录的用户列表,修改Application ,增加属性

1
2
//当前用户列表
public static Map<String,String> sessionMap=new HashMap<>();

我们以用户名作为key,sessionId为值存到hashMap中

(2)修改STOMPConnectEventListener的onApplicationEvent方法,将获取到的用户信息和sessionID保存到静态变量中

1
Application.sessionMap.put(agentId,sessionId);

(3)修改消息实体类SocketMessage

1
2
3
4
5
6
7
public class SocketMessage {
private String fromUser;
private String toUser;
private String message;
private String date;
//getter and setter ....
}

(4)修改ChatController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Controller
public class ChatController {

@Autowired
private SimpMessagingTemplate messagingTemplate;

@MessageMapping("/send")
public void send(SocketMessage message) throws Exception {
DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
message.setDate(df.format(new Date()));
String sessionId= Application.sessionMap.get(message.getToUser());
System.out.println("sessionId:"+sessionId); messagingTemplate.convertAndSendToUser(sessionId,"/topic/send",message,createHeaders(sessionId));
}

private MessageHeaders createHeaders(String sessionId) {
SimpMessageHeaderAccessor headerAccessor = SimpMessageHeaderAccessor.create(SimpMessageType.MESSAGE);
headerAccessor.setSessionId(sessionId);
headerAccessor.setLeaveMutable(true);
return headerAccessor.getMessageHeaders();
}
}

(5)页面增加文本框

1
发送给:<input id="toUser">

(6)修改页面index.html 中的JS代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var stompClient=null;
function connect(){
var socket = new SockJS('http://localhost:8080/my-websocket');
stompClient = Stomp.over(socket);
stompClient.connect({"login": document.getElementById("username").value }, function(frame) {
// 注册发送消息
stompClient.subscribe('/user/topic/send', function(msg) {
var body= JSON.parse(msg.body) ;
document.getElementById('message').innerHTML += "<br>"+body.date+" "+ body.message;
});
});
}

//发送
function send() {
stompClient.send("/app/send", {}, JSON.stringify({
'toUser' :document.getElementById('toUser').value,
'message': document.getElementById('content').value
}));
}

3.6 Vue中使用SockJS和StompJS

我们刚才所做的例子都是基于原生方式的写法,那么如何在我们的项目中使用sockJS和StompJS呢?我们这里需要想到有两个问题

  1. sockJS和StompJS如何引用
  2. 跨域如何解决(因为是前后端分离)

我们现在来看实现步骤:

(1)修改服务端代码,解决跨域问题。修改WebSocketConfig的registerStompEndpoints方法

1
2
3
4
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/my-websocket").setAllowedOrigins("*").withSockJS();
}

setAllowedOrigins(“*”)用于解决跨域问题

(2)安装sockJS和StompJS

1
2
3
cnpm install sockjs-client --save
cnpm install stompjs --save
cnpm install net --save

(3)在前端工程增加聊天测试页面 chattest.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<template>
<div>
昵称:<input v-model="name">
<button @click="connect">登录聊天室</button>
发送给:
<input v-model="message.toUser">
消息:<input v-model="message.message">
<button @click="send">发送</button>
接受到消息:
<div v-html="info">
</div>
</div>
</template>
<script>
import SockJS from "sockjs-client"
import Stomp from "stompjs"
export default {
data(){
return {name:"",stompClient:null ,message:{toUser:'',message:''},info:""}
},
methods:{
connect(){
let socket=new SockJS('http://localhost:8080/my-websocket')
this.stompClient = Stomp.over(socket)
this.stompClient.connect({"login": this.name }, this.onConnected);
},
onConnected(frame) {
this.stompClient.subscribe('/user/topic/send', this.callback)
},
callback(msg){
let body= JSON.parse(msg.body)
this.info += "<br>"+body.date+" "+ body.message
},
send(){
this.stompClient.send("/app/send", {}, JSON.stringify(this.message));
}
}
}
</script>

4.十次方-好友私聊

4.1聊天微服务

(1)创建十次方聊天服务模块 tensquare_chat

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

(2)创建com.tensquare.chat包 ,包下创建启动类

1
2
3
4
5
6
7
@SpringBootApplication
public class Application {

public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}

(3)创建消息实体类

1
2
3
4
5
6
7
8
9
10
11
12
public class SocketMessage {

private String fromUser;

private String toUser;

private String message;

private String date;

// getter and setter ....
}

(4)创建配置类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic");
config.setApplicationDestinationPrefixes("/app");
}

@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/tensquare").setAllowedOrigins("*").withSockJS();
}
}

4.2聊天前台页面