第1章 前端环境搭建 学习目标:
了解十次方需求、技术架构,理解前后端分离开发模式
掌握Node.js基本使用方法,理解模块化编程
掌握包资源管理器NPM的使用
说出webpack的作用
掌握vs code开发工具的基本使用方法
掌握ES6常用的新特性语法
1 十次方需求分析与技术架构 1.1 十次方是个什么样的网站 《十次方》是程序员的专属社交平台,包括头条、问答、活动、交友、吐槽、招聘六大频道。
1、头条
文章头条、分类查看
2、问答
分标签展示、问题提问、问题回答、问题所属标签【瀑布流分页】
3、活动
活动大会信息、大会报名【瀑布流分页】
4、交友栏
根据性别展示推荐好友信息、喜欢、不喜欢、聊天操作
5、吐槽
无标签吐槽、评论
6、招聘
展示招聘信息、推荐/最新职位、查看招聘详情
7、其他功能
用户中心(个人发表的信息、浏览记录、用户设置等)
十次方名称的由来: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.交付
1.3 前端技术架构
架构描述:以Node.js为核心的Vue.js前端技术生态架构
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的版本
2.3 快速入门 2.3.1 控制台输出 我们现在做个最简单的小例子,演示如何在控制台输出,创建文本文件demo1.js,代码内容
1 2 3 var a=1 ;var b=2 ;console .log (a+b);
我们在命令提示符下输入命令
可以看到输出结果,这就是前后端分离,不需要浏览器的支持
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; }
命令提示符输入命令
运行后看到输出结果为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 ));
我们在命令提示符下输入命令
结果为1000
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 ) { response.writeHead (200 , {'Content-Type' : 'text/plain' }); response.end ('Hello World\n' ); }).listen (8888 ); console .log ('Server running at http://127.0.0.1:8888/' );
http为node内置的web模块
我们在命令提示符下输入命令
服务启动后,我们打开浏览器,输入网址
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 ) { response.writeHead (200 , {'Content-Type' : 'text/plain' }); 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/' );
我们在命令提示符下输入命令启动服务
浏览器地址栏输入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' }); 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/' );
我们在命令提示符下输入命令
在浏览器测试结果
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命令是工程初始化命令。
建立一个空文件夹,在命令提示符进入该文件夹 执行命令初始化
按照提示输入相关信息,如果是用默认值则直接回车即可。
name: 项目名称
version: 项目版本号
description: 项目描述
keywords: {Array}关键词,便于用户搜索到我们的项目
最后会生成package.json文件,这个是包的配置文件,相当于maven的pom.xml
我们之后也可以根据需要进行修改。
3.2.2 本地安装 install命令用于安装某个模块,如果我们想安装express模块(node的web框架),输出命令如下:
出现黄色的是警告信息,可以忽略,请放心,你已经成功执行了该命令。
在该目录下已经出现了一个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库安装在当前目录,而使用全局安装会将库安装到你的全局目录下。
如果你不知道你的全局目录在哪里,执行命令
我的全局目录在
C:\Users\Administrator\AppData\Roaming\npm\node_modules
比如我们全局安装jquery, 输入以下命令
3.2.4 批量下载 我们从网上下载某些代码,发现只有package.json,没有node_modules文件夹,这时我们需要通过命令重新下载这些js库.
进入目录(package.json所在的目录)输入命令
此时,npm会自动下载package.json中依赖的js库.
3.2.5淘宝NPM镜像 有时我们使用npm下载资源会很慢,所以我们可以安装一个cnmp(淘宝镜像)来加快下载速度。
输入命令,进行全局安装淘宝镜像。
1 npm install -g cnpm --registry=https://registry.npm.taobao.org
安装后,我们可以使用以下命令来查看cnpm的版本
使用cnpm
3.2.6 运行工程 如果我们想运行某个工程,则使用run命令
如果package.json中定义的脚本如下
dev是开发阶段测试运行
build是构建编译工程
lint 是运行js代码检测
我们现在来试一下运行dev
3.2.7 编译工程 我们接下来,测试一个代码的编译.编译后我们就可以将工程部署到nginx中啦~
编译后的代码会放在dist文件夹中,首先我们先删除dist文件夹中的文件,进入命令提示符输入命令
生成后我们会发现只有个静态页面,和一个static文件夹
这种工程我们称之为单页Web应用(single page web application,SPA),就是只有一张Web页面的应用,是加载单个HTML 页面并在用户与应用程序交互时动态更新该页面的Web应用程序。
这里其实是调用了webpack来实现打包的,关于webpack我们后续的章节进行介绍
4 Webpack 4.1 什么是Webpack Webpack 是一个前端资源加载/打包工具。它将根据模块的依赖关系进行静态分析,然后将这些模块按照指定的规则生成对应的静态资源。
从图中我们可以看出,Webpack 可以将多种静态资源 js、css、less 转换成一个静态文件,减少了页面的请求。 接下来我们简单为大家介绍 Webpack 的安装与使用
4.2 Webpack安装 全局安装
1 2 npm install webpack -g npm install webpack-cli -g
安装后查看版本号
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)执行编译命令
执行后查看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
(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定义了:
语言语法 – 语法解析规则、关键字、语句、声明、运算符等。
类型 – 布尔型、数字、字符串、对象等。
原型和继承
内建对象和函数的标准库 – JSON 、Math 、数组方法 、对象自省方法 等。
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() 、getters 和setters 、严格模式 以及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)使用
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 { console .log (test) } }
所以不用关心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 { 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 var name = 'lux' console .log ('hello' + name)const name = 'lux' console .log (`hello ${name} ` )
第二个用途,在ES5时我们通过反斜杠()来做多行字符串或者字符串一行行拼接。ES6反引号(``)直接搞定。
1 2 3 4 5 6 7 var msg = "Hi \ man!" 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 () action (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) console .log (second)
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) const alp = { fist : 'a' , second : 'b' }const alphabets = { ...alp, third : 'c' }console .log (alphabets)
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小节内容)
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年的博士论文 中提出的
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
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
编写后的文档内容如下:
代码如下:
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
编写后的文档内容如下:
代码如下:
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方法提交请求
代码如下: (/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的实体类型
代码实现如下:
(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的实体数组
实现步骤如下:
(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效果如下:
代码如下:
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效果如下:
实现如下:
(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即可看到文档页面
(6)我们将编写好的yml文件也拷贝至nginx的html目录,这样我们就可以加载我们的swagger文档了
3 Mock.js 3.1 什么是Mock.js Mock.js (官网http://mockjs.com/)是一款模拟数据生成器,旨在帮助前端攻城师独立于后端进行开发,帮助编写单元测试。提供了以下模拟功能:
根据数据模板生成模拟数据
模拟 Ajax 请求,生成并返回模拟数据
基于 HTML 模板生成模拟数据
Mock.js具有以下特点:
前后端分离
让前端攻城师独立于后端进行开发。
增加单元测试的真实性
通过随机数据,模拟各种场景。
开发无侵入
不需要修改既有代码,就可以拦截 Ajax 请求,返回模拟的响应数据。
用法简单
符合直觉的接口。
数据类型丰富
支持生成随机的文本、数字、布尔值、日期、邮箱、链接、图片、颜色等。
方便扩展
支持支持扩展更多数据类型,支持自定义函数和正则。
3.2 Mock.js安装 在命令提示符下用npm安装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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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)' }] }) 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 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 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 输出用户名和密码,如果不存在会自动注册。注意:请牢记密码,系统没有找回密码功能!
登录后进入主界面
(2)创建项目:点击右下角的加号
填写项目名称,点击创建按钮
创建完成后可以在列表中看到刚刚创建的项目
4.2.2接口操作 (1)创建接口。点击列表中的项目
进入项目工作台页面
点击“创建接口” ,左侧区域输出mock数据,右侧定义Method 、 Url 、描述等信息。
我们可以将我们在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下
(6)配置环境变量
填写以下内容
1 2 3 #set for nodejs export NODE_HOME=/usr/local/node export PATH=$NODE_HOME/bin:$PATH
执行命令让环境变量生效
查看node版本看是否安装成功
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
4.3.3 Redis安装与启动 (1)下载fedora的epel仓库
1 yum install epel-release
(2)下载安装redis
(3)启动redis服务
4.3.4 本地部署easy-mock (1)项目下载地址: https://github.com/easy-mock/easy-mock
(2)将easy-mock-dev.zip上传至服务器
(3)安装zip 和unzip
(4)解压
(3)进入其目录,安装依赖
(4)执行构建
(5)启动
(6)打开浏览器 http://192.168.184.131:7300
4.4 导入SwaggerAPI文档 (1)将我们的SwaggerAPI文档扩展名改为yml
(2)在easyMock中点击“设置”选项卡
(3)SwaggerDocs API 选择Upload
(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
1.2 神奇的脚手架 1.2.1 快速搭建 官网上提供了非常基础的脚手架,如果我们使用官网的脚手架需要自己写很多代码比如登陆界面、主界面菜单等内容。 课程已经提供了功能完整的脚手架,我们可以拿过来在此基础上开发,这样可以极大节省我们开发的时间。
(1)解压vueAdmin-template-master
(2)在命令提示符进入该目录,输入命令:
这样下载安装所有的依赖,几分钟后下载完成。
(3)输入命令:
运行后自动弹出浏览器。
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 表格组件 我们在这一环节先实现一个简单的列表,如下图所示:
准备工作:我们将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 分页组件 我们已经通过表格组件完成了列表的展示,接下来需要使用分页组件完成分页功能
准备工作:修改接口/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
sizes
, prev
, pager
, next
, jumper
, ->
, total
, slot
‘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 条件查询 需求:在分页列表的基础上实现条件查询功能
代码实现:
修改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 需求分析 界面中加入”新增”按钮,点击弹出编辑窗口
点击“修改”按钮,关闭窗口并刷新表格,弹出提示(成功或失败)
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 下拉选择框 需求:在新增窗口实现城市下拉选择框
我们这里需要使用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添加属性
引入城市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 需求分析 在表格的操作列增加”修改”按钮,点击修改按钮弹出窗口并显示数据,点击保存按钮保存修改并刷新表格。
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 :'' } }
修改handleEdit,增加
修改方法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 npm install -g vue-cli vue init webpack vue-router-demo cd vue-router-demonpm 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 )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 ({ 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 >
2.2.4 上传Logo 参见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 : 100 x; 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 增加变量–企业列表
(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变量的值
这样在查询时就会携带状态为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 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 vue init webpack vuexdemo cd vuexdemocnpm 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 store.state .b
我们现在就对工程按模块化进行改造
(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' import 'nprogress/nprogress.css' 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 () })
(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 () }) } } } </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 () }) }
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)安装依赖
(4)修改package.json
1 2 3 ...... "name": "tensquare", ......
(5)修改nuxt.config.js
1 2 3 ...... title: '十次方' ......
(6)测试运行
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目录创建文件夹
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} ){ 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 > </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 > </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' const service = axios.create ({ baseURL : 'http://192.168.184.133:7300/mock/5af314a4c612520d0d7650c7' , 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 ="#" > 更多职位推荐 <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" > 百度 · 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 ="#" > 更多职位推荐 <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" > 百度 · 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}} · 有赞 </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/ 首先进行账号的注册
(2)开发者资质认证
(3)创建网站应用
填写应用的相关信息
应用通过审核后,会得到AppID和 AppSecret,后边进行编码中会使用。
AppID : wx3bdb1192c22883f3 AppSecret : db9d6b88821df403e5ff11742e799105
3.2 微信第三方登陆流程 1. 第三方发起微信授权登录请求,微信用户允许授权第三方应用后,微信会拉起应用或重定向到第三方网站,并且带上授权临时票据code参数; 2. 通过code参数加上AppID和AppSecret等,通过API换取access_token; 3. 通过access_token进行接口调用,获取用户基本数据资源或帮助用户实现基本操作。
获取access_token时序图:
3.3 获取code (1)修改login.vue,在登陆表单下方添加一个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 浏览器显示
我们打开手机用微信扫二维码, 会出现以下提示
点击确认登陆按钮,浏览器会自动跳转到
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 ){ 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请求方式: GEThttps://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 ) { 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 ){ 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 > · <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 > · <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 > · <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 > · <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 > · <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" > × </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" > × </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' const service = axios.create ({ baseURL : 'http://192.168.184.133:7300/mock/5af314a4c612520d0d7650c7' , 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 } ], 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 : { 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 ) }, 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 : { 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 } })) }, 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/Po4slv0AABviSURBVHja7F0JfBRF1u/puXJxCqIiiID4LawIIq6KgooICh7ITRIIN6ui6KcsiiugCPrJp6KuohwJhFtQDhUPRFg5FJRDkfUAOUQFTAhKyDFX7/tnXieVSs9Mz6TD4W8ev6Knq6urqqvqX++oVxWbUkWUNmRoPbvD/ojd7minBQJHfT5vevbMmX8ocYrTn4QcVmaWOnhwEoHlCZfLNVC1qc5iT/EiT3Fx53mz4qCJ05+PbFZk0n/Q4EtcLuf0xISkG7x+b15xUfHY7JkzZsabN05x8IQGTX3iMkuSEhOv9fn9xYWFhRMJNFPCvXNr97ucdLmCQmsKzSk0pFCPQqKQ7CiFwxR+ofA1hc9Xv718T7y74nTWg6dfRobqsDveSEpOHmRTVbWooGC9x+fttmD27PwQgKlFl54U7qRwkwQUs5RL4SMKyymsIDAVxbsvTmcVePplDGqTkJDwYYI7obbP7/MVFBQMnj97VnYI0LSiy4MU+lJwWVjv3ynMpvAigehgvBvjdMaDJ3Xw4PFJSSnj7apq83g9OSSmXb0wK3OvAWguo8tTzGmqkrwUZlEYTyA6Gu/OOJ2R4EkbMvSd5KTkrorNphQVFX7n8RS3XJiV5ZFAk0KXyRTuoWA/hd9xnMIjBKC4kSJOZw54+gwcqLqcri8TE5NaIXFhceFuj8dz2aKsrIAEnL/RZSGFiytRnzUUbg7xrIBCFgMzFK2gkEEgOh7v2jhVNamREriczq8SExJbKZqmFBYV7vEUGwJnCF0+ZeD4TZRrlOZlCotDpN9IAfpTgwj5QkzcQvVpEu/aOJ1W8JCOs8adkNhCUzSFdJzjxHFaL5pTATj/pAvEJZigv2AdRCZYxuZT6M4gyJOez2HDwl+keFjY/k6hPYfbTXzTJRQ2s94VpzidevD0yxg0lUS1joQbJUBUVFR0PQEnXwLOGLo8ybf7KHRGtJAEOhHWfRqQKJVGAWbm8RTq8HMfhbEUBlEAKO/geFjTJlJoQu9Mp+tVFP4VxXfVhQhI9bs43sVxOqU6T58BA29LSU5516YGH58sODlxYWbmBAk4feiySIi6m8JOCrr17TMK6eLiJr1zJV228u0HrOR/LeQ3jrnYbIrP53hwkLUC4KKh7yi0pbxORPMSlYm1qIYWtnM+1WGpQTkwsPS0IP+DlP/aCN9k4za8kEJjTEzMpffwxBRtPXZTmVuirSjV4yW6DBCi3qd8+sby0ZTXXSzJVJaWUx12cJ6YqJtLDADfeTmlWcZpsE7Zp4JvW+/0AUkJbvcSWNXAdYo9xQcMgIMZfYYQtV8JLl62F8Sw4VSYR8oeFXsGoKNnO6Vn/6a4xVI50QAHouDrFK6mcAPHXUrhDTDSKBvzfsVaM/sBCksN4vFdmRbkn8XtJA+uJBalUc45ISSNg6xrRluPaTyoFIM+C2dp3UZhlDjk6B2Ml1/DvFNIY+M7g3iAZ6AF7Yfxu8NgDMxk8FyHsYVxT/WA10tXTPAVwGN32Fc6HM5kTdOCsPN6+hjMYPjYakL0h5SpRs+8/GwQ7uW8Ke7VULWnZ79K5TSjy3oKtcJ8dA6nwcBcSQEWv39IafpSXm9S/m/9iSWIX0K0aQFzt7ph3gWHbWphXWA4qhGl9PNehDQ7LeIwEYm56RbmagtguaXfGRjjFHqwYQu69zZHea6T3jbBndBR/6Li4qIdi+fM+VxWhyhcL8XtEj5yuBFwYviI75nDtebOxewJL4UjFA6xuPG9XhalbcpGCSNRdBo9X01pC2OoyhzW6+ZRgHf43TwojwuDBDPSfoqbSr9bsg7XycRs/gfP4NESxN92wv1PYdJCFzWyUsLVaTXakMJv0rPxzJFkGsvc/EwkjAuz4l9TSXIyQwD4ndTHc1k/zy8HHofTudBG4hqsazb65/X5hkrcABa1SSFEJgz4kxbPAjAcrOMQTva9kjnP+SGSQM7H+tD/x2hUQd4t+H4VlXerQR2G0eV/+fYbCi+Z+L5jdBlN797MooFOi+nZf8J873MSeH4OU0waG2Zyddmd4zGrZnF+jaR3Vuo6gFRuRpTg2RJBHDNL+0ykKaI6rzOpK8WyDoiJF14so1nSecQhcJ2Obqe7CaQ1qDsej+fw4rlzvjSQMY0sWI7TMdVQIzhYTHtCiew79xCUVWpgb5TFBJhdb6JQWwk6tg6V0rSlMF24f5HlaLPUR8rz4wjpZU7ycxiA/iS0l9n63MF+iTKdJ/wuNpHPFLawngpqGAUo7AZ9bIZgIJvFom4ZeBwOx8vBX1qJ5OP1eecZvBxqdb/GaQCOjcWiNJOvXMCKXtSdCWWVyuvHivUoup9H95OFJFt58L8CmZieT+BZ2ixdKN3/ZBV4qB5Zkn6j01B6phtWXpFem2iizodM9hPK+KQSXT0R7WlSd4p1HG43iMO6o26lXSmI2ddhAqbvGloCnp5pafVdTtf/6NgJEPsJBPxTpEaA0tkhxMx8yj2b2UAxgHUSmMm7KJFdg/rHAJ4uVM46QXzAoANQUqQZqYgH/dWc/rwoyqgv/NYiiGH6RKCTl9riSJi0oaxR7QTRLyvKNsHywtwzQM85yHp2rAQxbIa+XCKNr32SeK3TXo7bUwIeu2p/WoGyw+TzeY8tyc4+JuXX1UAZBxJ7U0YfnI6WY2PBGg4AeE0l6KVwIRsY3NLM9DBEPXrPF0Ux9TiEo78Jvy8NYS0KR6Ku9ms40ZI5bn2T+o5ZymEjRySCWxXM7puiaMN84ftd3D+HWAeTCf13kVmRiurwBIvsp4WC4LHbuwZ1naDI5vf7vwoxU8lKXNdwiq1IvdLSMZBT3pyXnVuFYILMu9lgwGGmXsgzNqxh2yJk9bmJ4roKuh4sVwUGaVAfrE+8FgYMbqX8OlZditsfwYDhFLmWlH4ptcPDwn1rvkKMfECyqOniyLFIRhlZzGT96SgsjxH6BOtMrSg9RKoVDJ4UFg1nCdbSdLo8LwEV9/8Xot3gT3m7hcPnpUgLzRXA0zM1LcVhd9Qp03ZK3HE2G6RtI1lRAJycKMpKhVJNIBpIAFp4ivSi6nS5l8KjStm61BXhwEPvQJlczSEcdRRENxgIfgmTthHlu9tg0VgW2RQGxkVRfKac3ikN3h3MrboYiTws/mYpsS02djcQg50h0gLEjQQOA1NxOpUN6y3cvG6WxMLBDLxQhMVYKxeyo9aFHSStDQ1KbJqiabYS7qNpgY8M0ooLae9GCRxdL4Di9xoBaAUBqKAKQXOjEnQB6U0hSXocydRaLYQCGY7eNZHm4hAWuPMt/nwjke9Og+9+HJZD1t+sJLG9fxdAvI7Kgl79MJcNjguPlA+F9JhcnmYrnVc5w8mhqrYbFZHt4KemfGkwg1eTFK2oCGAh0ABAj7Do9FkVflcCGxGSTsFgrayouVGpxEEsBtYs2XkXouVkg1ebcADXepNFTJGuFnQ5WJ1mG+SxVypLXi6oLln7dPrCQA3QdUP43c2QzOoHWb8JRdD7rouy6apX0tgAzqOWs1AFNE2jgS6fsya7yPwnxvJ0blNcxQNyNTV+Z5bj3QZKaTg6yeJIJJovgHOEiQnlqDDI8N4tFnwqdLPEEG2sk7jVA3qEvMaB2R+W1DaiBwbVcYIAnmP0bLSJ+sgWRluU4mBbDkagCgceHzw8JCBfJHH6e0X3MDYuKZUEj62u7lEA9kP6jtFGNXFmxMDfGmN56Ch4I+w+BTP6ZyxPPyU3dJhZvGkUM5hNmsU8EdLDAXIL1Qvffi6Fty34TCOQe4TvqSF8/z7mSvo+p2eV4NoU6o6Fv9ek2V5cJK1jwEG20be8ZCCaipSrnD6SOdtWqwsAeJJL7zRTcvQnkSwsIaxtWCOCiHgPcTbLOU/XHndfS8DvzMrmeqoj/LVeZquSWQ8IACcWL+fnTKZ7sAomDnlhUNQzfmdLHPSdmawD6vStEtw/lciDfGqYMpINOAhm7nDgCfCADbf2JrrdTGPDi8L99anAyd6PUpR1sBFCpyLF2BpaOfDo1pEy7mNI4ppPZgzAgWy9gG//SffrCUCWDqJatWrflpubM05owK0sp3/FFjaxIU83oSNXWJAP/MYuCcV5dHWTQfuGBB6AaxO3lVUey1cJv3/ks/X2hxnk4u1xXfRi74zzhP56IQrgOHmMXi7pwLvoGcbCQh6LeZLOcywWzlN+VtYMEaQjNy9akx4BBYrnW2xVAfdpROE9im9FALLsoI6kxKSmeapdEaTOUPJzONeXpawn2dmCBo/j6QK3wCx9G0844kZAiELpPEPqawXYPzNcCboP9eeOOsaDFvrPXVZ8Nw2IsVKUvPEPC9hYGM4J5dvGTqA2Kd8JzLVBByhNIxPVEc3NO2L8HpdQLuj1EB4U4PZZ4oTBxhPEXxki+5Yc4D2zkft2ITsgx8R5NOzdKfWmVm1GG5nuZOV4dIi1ilDAGcDseDAB5W26h+EBTo+tlbIjqiwhqnsNt9utFBZG5M77wuhJ0AnyqRPuZ1EH4XZWnOvxrJXMotFrLBa9ziBR+LsWcGe+r5SZh2F+vdLsKaeUZwelbHEzFDlZzLxDiv9Ruv+MRdlQZWHh2MipVlSoHQae17qifojzaSNxwY0RvvGaUMq/ErSUYh3tBtbNjPrqZ8rjJHOYDvT7bonb6AYVSCMYh30k45HunvQCvfsWc6u1lG/ANHgCAc2nqmULWxDdeqamNVg6f544Qz/OM2dmFMCBzAmbficCTsliF13zKB4fgf0/Q+n3UxRnhcs6XIwamgTP9gid2pQHu05Y08LmKHhUq4KegYNJnuE20cGDdx+g9M8yAHW3pRbcho9HYQh4IIZm2EVl75IGmU/UgwzoPYNBJ1P9EJPOAaVs4XOM9GyV1K6zGRS/s3YtO8Pmc30D3EfbjfQwyqcn9885HIwowPruWJ6wPqb3HmQQDVfKHzSTwJIBwmqWLEyRSlynZDbUd46WAMhm6yBUtguLJQPM+jMRKDBIHsPsoQOnVACfl/0Dz+AAbFerOI/d7qjndES0C8AU+2UERRN1SxHSP8CdCpFrjpAcfnLJsOpJ+ssYWLkoHmJqthRflQeSYBLqp5w++lSwZG6k799rwAUw6cCz+yKlvMncK4MtDEGcrhYCOB7uP5w3MFrk9Og/CjieuTlztBWSiQy/J0UltmlaII9wU0t3C4XoptpsyFzfkgD7PlZ8vzIJHMzIsMl3IaCEEhdWsvXmCit67eHH/mnz+/21VXvEQ0o/DSd2YnKgAf4oNyJEtcekQQBrENZ0vmDdD2IARIeJrM9hy0KWIEOPYd0GOtME0VM3AmGX7HqTYvdhTjs3Rtl9vhKdX5uhIYnKfoXaDv29hHUKxQA8cv38bNCZFOKMAqM+KqRyxrMBRGGDxGbWqfEHAPJM5IH2Ws9SBsZ3BvfbpmjBg8Ibl2NHqirKo03YYmMGOJBTsTfkQQJOOCc7fZG1rhXgycs7dmutWrVtJLpFSrrcRMOWeGmz3vGp9Az7eurSNVeK307x9WRg0v1hHIhB1wPRfA8GolJxj40VJG6l+JbLes6qzDEo6XtbGLlusVGipkVFZbIa8X1lToflk53u47MHo3YHcgQ0bYMS3B2p6IYDu8PRTEgD14+nqYCl4WY2Ak5dVpbfY3nTxIylWfIX41S7OpzFzXDJ0DjLohkIIeJzQ8R7QsQfUM4QiuBoaVUZOaegDIiHWyzMLy+mcacFAnODTqHlOI+DwNCfb7PZ0vRUhLxe4FkNi6CRDgCpJiiclSa3O6FTCRQDYYtdEf9LCnGyktRlCxbsJX2h9OAO3XBgt9t1X6INLFfeQ9yntQHHOZcCTpfBloOnCTg/mSi3Getqn1T2AzJGjuzrdrlLfMz8gbDHZD8f7+44WQoe/Ofz+zYEgVP2wOl0XtorLa01b1Z6na0jWwlAgwXg3K2UHXb4imJ6JVi7lpXijZX9gMSExKlBzqlhB2yoZHDX2Xw2dtCy5pfeR2GKFHe+8LsDhRcp1LeoPOTVPw4Nc9YaJeD3j6EB2FlfZNZ1H4fdiVX0S2ngPUOgWcCWpVK9h7gMFpeiOkyQAIejSHEazYQ3580LVKbyGSNGDiTwlA4ajyekIe0fZ3EfYV0D7jOP8uCGPxkWR3VLJaQBmNOzFGu2ZCOvOUqZO1WcwnEeEt2+8nq9FfQPp8vZrHda+ihWqrCnIjfK/f9GXKcDW3oWVA44I9zJySkl25uDDBN/AsVwAT+T6vz5WdxH3ZTyO0XbKyb+NEwlCF4g98ShYZLzlIhuPu8oEtVWBneTllPGn++ZmrZy6fx5VlmNYMlLJa5TqVNFnU7Xv10uVyKzSoieRmLb4Wi4Ds3qMNnDM+ClHru/+5HuIarClLuB7t/iNDgoA5ZF6HYXU/xkjse6EBYpp1LcIbpH3aawWAsXn/co/mNOi9ndSfdTddGLLl3o/tEQnAdOks9Qugn8W+HfordxAsXB6tiO55MSx1jKs1D4Phh04IMH3y+srGPBei6+VcgH6x4wKy+n9OB4WKd6la8Qt6FYwpy/iEE9jK/getmU1y6hPHgRwKMEC5PQS4+zDr2Y0vki9MUNPHHge2HoWUvvvCM8T+L2hu8kXKawrXwRpdkhpBnN+jq+byDnhfvpXF+835G/Cda7zEj1qsB5mPus8ng9u2TDgU21Odwu186eaWkpViCHQPMGhUq5hw8a+fcXUpJTrsIQ0RF4It/wD3Fn8NYEswSwYZG3B99jkMGtYwx3BqYVgAVrX7AYTqI4/fCOISzy6KeJ3sj3hSxmjeI83Ayqp3kAKAzYUL5sGUrwmFtQIyXoh+bi39WFdG/zwAf7bcA66FquM8qFnx0Wa59h/fUwD+zd9KyXkM94pcxptRXfb+Q6Qi5uyVIDwLONgYH4kRiAPAGhPDiJfs/tCXEfZzzAU2WeUt5TQwaNjcJcNia14fewfXsVxU/nNGj/b/gbEzgN2n07PXtCmghe4PpDvIea8BADBQv18ILxcr2w6Do1arFNJ7/P1y1ACpAm8QSHw1nD5XDtIQBVP92skvScSdWrVeddjWUVPXGiwpLRlGiPxKJZp4AbWncb0nd7tqVOqcF6BmYv5IsVbQxM3ReqE19vFsStXOYASN+JgQPOkMgAuI7iXPzu+ybql8Ez7EH8piDu/8eC7mUUN4JCR9aBMCvrxwRjySGF0+BdHA0MnzasqmcKk4ARYba+nN4ZzhMB1nKwvaE3xXWnACPSCP4u/aAR5H+M37sf5VHAu+uU8H/OpDtzR3DwGymMoXAbG61GUD3/qgQXSdEP19CzdAoPc9/Aw2Micy1FmHA6UZq+SKsEzx0/j0HXEu3FHPUHnkxiAw9xnwOkdD8hcx+2vtVzu9wHeqWlnbY/WUgcJ4uAM67EA1yvm4a/5OBV8k+W4zxwERkXYzEYxO2oA2ryoP6c2+kGFr/A1tdQo+/nmfU2SnsJdxLS3sSzPQD4EaULMNCSWF8BuI6w2HAzx1VTotzwZaTbUVmirV63Ltah+qBu2J4BP7zGGFw8wK7lOkPs6Rwm7/l63nQt5oF2jH6vEdJ8y9dzOd2tFC6kcJLKuoDCtRQAsnOU8Ecj6yfiyFzgIa4n2g5/aGCFKKJx/Z4URF2dfqBn4nkZunfLEuGbAmz9TYxJ59GJdJvJvdMHdHa53O1Ffzd4Wzscjpqqqn7XOz195JLs7FP2l6fThw471+12rU1KSmlR0YdAU/Lyyi36g90PrMRfasBAhxs8zPDXsNhWh2Vj3G+mxtbZ3Ac8S25kuRk+cas4Dg6Q+kSEzvuDAXU9z77FnCcG0gHK89tKNpO8ACyaHvXjeW9iwMj0uyQCynTcIO8TEXSWv3A7duSJo5BFraQI31EiXlF7HDGQCpCvfoqT0XqifvxX3TDtYtQ+RvfRcZ5S8c3vu9Hr9R40GKeKqtrtSUnJM/pnDN5JIGpY1cAZMGz42GopKT8lJSa3kDkirl6fV8k7nicCp5vZfTMhaCfPbhN5clnHoQfrQO9LQKvJOhFcXz7iQTJZAJfCSiiMBb1Yt/mY69qa46w4cTXcCrHuMjOD6lJTDDzQatHv16zqMzaWfMJAHcE6YjKV0Zb1JN14EaquKnN+Mc96FPAnRPTtM0bHGdczAIyvqsamIXhIfAv4fN4WNDCPlA7UUv0i6MpDnKAlDej9/TIGres9YEDjKuA29w27b1ROjeo1ppDO5VKkWuh05OgRHUwQ1brEcr6CNMNpDIoLuSO/4cF+gSDWiWD1ctp1LNJs4tlzJ90fFtJ+wHnY+L11/Btxq4VBUo1CJN2yWpSfBa4G7/BUyruhqJwrwa3qHvrdzsLua8wDeTW1wTxY89CubH3TdUJ3GLEZJJ8nB8MDthvUYAB2F7iQTvpJqWuVU0AhN8AsWzA/v0f/VChnu11OV12R+yg2vhAluBM60O+9qUOG7PP7/JmBQGDakuy5MTl8pg0Z2tZut49zu923UJmJZefIBRdty58tpyl/nPhDyc8/EWAu8ZQVf1RL4CgQvdZzp+tuRLDcbReABnl+A1vW1nH0OhZVVhvkCfqZ3tvDg/dHFu/WSop/TaVsk5lMWDK4g979MJzVSp4QKD1MtXDa3Ua/Z7Mx4xYW5aAvbbRwXEEXxBaDPlTWUb5vwpZDjI1aLArnsjcDTOHdqA4b2BoHo8Fz9Ew/3RUcui/rXmtZd1rD1r2Z3C9d+FtmKDGc/mkpeBhAOT1SUxvTQP3S7XI3Ew8JEc+2xrim5xfbXLYnKe2T6cOG5wb8/l0BLbBFC2hbKclOeuHgojlzSsSp/oMG0eCwNSdAtCYd6moCTGun09XU6XC4S0Ci4wSmaJvxwSRFxUXK0aNHsAEMfzT4Y4vb5QMemG/y4DtMnQST537mTCL9iy1gGwSTcVPmhOIA3kd5TFPKb5OGUnyeoEOVKOeS4rpUKX/+NkzG2Bd0PoswAPM0peJfd9vN8Ye4/I1UfnNel2nDuhbyuZOerRTeE//WqJ6HvLENdaotxf3GaT+j/LxUVns2FUPc/isDqBub0gcrZWcBHuYJ57gu4tK73RksXfkdpElj8zjS7KQ0yBfWv6vYpI78J9Ez0V9ytlLxYI+9XE/5AJpVSpTnEZo+qZL0m1fd7oSRKv5Eti34qmoTsrAFQYSHGF0qgyD4yMaF2YLp9PTBS7k8VN1KYdPTK6Xg0YEFN5xffv15Aell91ZmP0ec4lQZiuqY156pqS2JQ7xDIlWDMrCUjvSSwV1uoOsl6EBh8CgCaGzl0gr3+nMJPMXFxfk5x3L6LZs//51498XprAGPTr3S0oc6nc4pBKQ6pdxHGuzluU95EMTCfYjLFBUUFLyYk/vbYxbqNnGK06kFTymI0tMHkZ4yzuV0N9G5RQXuo5cgcR9bGfupwH108JDOpBGn+cHr82YF/IFn58+eFYh3WZz+FOAp40RptVXV/ojdrnZ2OJzNnA5ncnkOY477wAjh9flO+HzebwIB/3J/IPDygtmzC+LdFKc/LXgMjAspNpvanrjJNaqqtlBVWwP6XcOmqtV0GGlYTNS0E5qmHdUCga8DmraJfn+0MCszbgCI01lB/xVgAOX9ozl3Z9A5AAAAAElFTkSuQmCC" >
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 : (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管理控制台界面。
(3)创建存储空间
新建Bucket,命名为tensquare ,读写权限为公共读
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 /> </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; }
(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)页面增加文本框
(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呢?我们这里需要想到有两个问题
sockJS和StompJS如何引用
跨域如何解决(因为是前后端分离)
我们现在来看实现步骤:
(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; }
(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聊天前台页面