为了方便后续回顾该项目时能够清晰的知道本章节讲了哪些内容,并且能够从该章节的笔记中得到一些帮助,所以在完成本章节的学习后在此对本章节所涉及到的知识点进行总结概述。
本章节为【学成在线】项目的 day12
的内容
知识点结合实战应用会更有意义,所以这里我就不再对单个知识点进行拆分成单个笔记,内容会比较多,这里我们可以根据目录进行按需阅读。
采用 vue.js
开发搜索界面则 SEO
不友好,需要解决 SEO
的问题。
我们先开一下百度百科是如何描述的
总结:seo
是网站为了提高自已的网站排名,获得更多的流量,对网站的结构及内容进行调整优化,以便搜索引擎(百度,google等)更好抓取到更优质的网站的内容。
下图是搜索引擎爬取网站页面的大概流程图:
搜索引擎的工作流程很复杂,下图只是简单概括
从上图可以看到 SEO
是网站自己为了方便 spider
(爬虫) 抓取网页而作出的网页内容优化,常见的 SEO
方法比如:
对 url
链接的规范化,多用 restful
风格的 url
,多用静态资源 url
;
注意 title
、keywords
的设置。
由于 spider
对 javascript
支持不好,对于网页跳转用 href
标签。
采用什么技术有利于 SEO
?要解答这个问题需要理解服务端渲染和客户端渲染。
那么什么是服务端渲染?
我们用传统的 servlet
开发来举例:浏览器请求 servlet
,servlet
在服务端生成 html
响应给浏览器,浏览器展示html
的内容,这个过程就是服务端渲染,如下图:
服务端渲染的特点:
1)在服务端生成 html
网页的 dom
元素。
2)客户端(浏览器)只负责显示 dom
元素内容。
当初随着 web2.0
的到来,AJAX
技术兴起,出现了客户端渲染:客户端(浏览器) 使用 AJAX
向服务端发起http
请求,获取到了想要的数据,客户端拿着数据开始渲染 html
网页,生成 Dom
元素,并最终将网页内容展示给用户
客户端渲染如下图:
客户端渲染的特点:
1)在服务端只是给客户端响应的了数据,而不是 html
网页
2)客户端(浏览器)负责获取服务端的数据生成 Dom
元素。
两种方式各有什么优缺点?
客户端渲染:
不利于网站进行 SEO
,因为网站大量使用 javascript
技术,不利于 spider
抓取网页。
客户端负责渲染,用户体验性好,服务端只提供数据不用关心用户界面的内容,有利于提高服务端的开发效率。
3)适用场景
对SEO没有要求的系统,比如后台管理类的系统,如电商后台管理,用户管理等。
服务端渲染:
有利于SEO,网站通过 href
的 url
将 spider
直接引到服务端,服务端提供优质的网页内容给 spider
。
服务端完成一部分客户端的工作,通常完成一个需求需要修改客户端和服务端的代码,开发效率低,不利于系统的
稳定性。
3)适用场景
对 SEO
有要求的系统,比如:门户首页、商品详情页面等。
移动互联网的兴起促进了 web
前后端分离开发模式的发展,服务端只专注业务,前端只专注用户体验,前端大量运用的前端渲染技术,比如流行的 vue.js
、react
框架都实现了功能强大的前端渲染。
但是,对于有 SEO
需求的网页如果使用前端渲染技术去开发就不利于 SEO
了,有没有一种即使用 vue.js
、react
的前端技术也实现服务端渲染的技术呢?其实,对于服务端渲染的需求,vue.js
、react
这样流行的前端框架提供了服务端渲染的解决方案。
从上图可以看到:
react
框架提供 next.js
实现服务端渲染。
vue.js
框架提供 Nuxt.js
实现服务端渲染。
基本原理
下图展示了从客户端请求到 Nuxt.js
进行服务端渲染的整体的工作流程:
1、用户打开浏览器,输入网址请求到 Node.js
2、部署在 Node.js
的应用 Nuxt.js
接收浏览器请求,并请求服务端获取数据
3、Nuxt.js 获取到数据后进行服务端渲染
4、Nuxt.js 将 html
网页响应给浏览器
Nuxt.js 使用了哪些技术?
Nuxt.js 使用 Vue.js
+ webpack
+ Babel
三大技术框架/组件,如下图:
Babel
是一个 js
的转码器,负责将 ES6
的代码转成浏览器识别的 ES5
代码。
Webpack
是一个前端工程打包工具。
Vue.js
是一个优秀的前端框架。
那么 Nuxt.js 的特性有哪些?
Vue.js
JS
和 CSS
HTML
头部标签管理ESLint
SASS
、LESS
、 Stylus
等等nuxt.js
有标准的目录结构,官方提供了模板工程,可以模板工程快速创建 nuxt
项目。
模板工程地址:https://github.com/nuxt-community/starter-template/archive/master.zip
本项目提供基于 Nuxt.js
的封装工程,基于此封装工程开发搜索前端,见 资料/xc-ui-pc-portal.zip
,解压 xc-ui-pc-portal.zip
到本项目前端工程目录下。
本前端工程属于门户的一部分,将承载一部分考虑 SEO
的非静态化页面。
本工程基于 Nuxt.js
模板工程构建,Nuxt.js
使用 1.3
版本,并加入了今后开发中所使用的依赖包,直接解压本工程即可使用。
目录结构如下
名称 | 描述信息 |
---|---|
assets | 资源目录 assets 用于组织未编译的静态资源如 LESS 、SASS 或 JavaScript |
components | 组件目录 components 用于组织应用的 Vue.js 组件。Nuxt.js 不会扩展增强该目录下 Vue.js 组件,即这些组件不会像页面组件那样有 asyncData 方法的特性。 |
layouts | 布局目录 layouts 用于组织应用的布局组件。该目录名为 Nuxt.js 保留的,不可更改。 |
middleware | middleware 目录用于存放应用的中间件 |
pages | 页面目录 pages 用于组织应用的路由及视图。Nuxt.js 框架读取该目录下所有的 .vue 文件并自动生成对应的路由配置。该目录名为 Nuxt.js 保留的,不可更改。 |
plugins | 插件目录 plugins 用于组织那些需要在 根vue.js 应用 实例化之前需要运行的 Javascript 插件 |
static | 静态文件目录 static 用于存放应用的静态文件,此类文件不会被 Nuxt.js 调用 Webpack 进行构建编译处理。 服务器启动的时候,该目录下的文件会映射至应用的根路径 / 下。例如: /static/logo.png 映射至 /logo.png ,该目录名为Nuxt.js 保留的,不可更改。 |
Store | store 目录用于组织应用的 Vuex 状态树 文件。 Nuxt.js 框架集成了 Vuex 状态树 的相关功能配置,在 store 目录下创建一个 index.js 文件可激活这些配置。 |
nuxt.config.js | nuxt.config.js 文件用于组织 Nuxt.js 应用的个性化配置,以便覆盖默认配置。该文件名为Nuxt.js 保留的,不可更改。 |
package.json | 文件用于描述应用的依赖关系和对外暴露的脚本接口。该文件名为 Nuxt.js 保留的,不可更改。 |
nuxt.js 提供了目录的别名,方便在程序中引用:
官方文档: https://zh.nuxtjs.org/guide/installation
页面布局就是页面内容的整体结构,通过在 layouts
目录下添加布局文件来实现。在 layouts
根目录下的所有文件都属于个性化布局文件,可以在页面组件中利用 layout
属性来引用。
一个例子:
1、定义:layouts/test.vue
布局文件,如下:
注意:布局文件中一定要加
组件用于显示页面内容。
<template>
<div>
<div>这里是头div>
<nuxt/>
<div>这里是尾div>
div>
template>
<script>
export default {
}
script>
<style>
style>
2、在 pages
目录创建 user
目录,并创建 index.vue
页面
在 pages/user/index.vue
页面里, 可以指定页面组件使用 test
布局,代码如下:
<template>
<div>
测试页面
div>
template>
<script>
export default{
layout:'test'
}
script>
<style>
style>
3、测试
请求:http://localhost:10000/user,效果如下:
Nuxt.js 依据 pages
目录结构自动生成 vue-router
模块的路由配置。
Nuxt.js 根据 pages
的目录结构及页面名称定义规范来生成路由,下边是一个基础路由的例子
假设 pages
的目录结构如下:
pages/
--| user/
-----| index.vue
-----| one.vue
那么,Nuxt.js
自动生成的路由配置如下:
router: {
routes: [
{
name: 'user',
path: '/user',
component: 'pages/user/index.vue'
},
{
name: 'user-one',
path: '/user/one',
component: 'pages/user/one.vue'
}
]
}
index.vue 代码如下:
<template>
<div>
用户管理首页
div>
template>
<script>
export default{
layout:"test"
}
script>
<style>
style>
one.vue 代码如下:
<template>
<div>
one页面
div>
template>
<script>
export default{
layout:"test"
}
script>
<style>
style>
分别访问如下链接进行测试:
http://localhost:10000/user
http://localhost:10000/user/one
你可以通过 vue-router
的子路由创建 Nuxt.js
应用的嵌套路由。
创建内嵌子路由,你需要添加一个 Vue
文件,同时添加一个与该文件同名的目录用来存放子视图组件。
别忘了在父级 Vue
文件内增加
用于显示子视图内容。
假设文件结构如:
pages/
--| user/
-----| _id.vue
-----| index.vue
--| user.vue
Nuxt.js 自动生成的路由配置如下:
router: {
routes: [
{
path: '/user',
component: 'pages/user.vue',
children: [
{
path: '',
component: 'pages/user/index.vue',
name: 'user'
},
{
path: ':id',
component: 'pages/user/_id.vue',
name: 'user-id'
}
]
}
]
}
将 user.vue
文件创建到与 user
目录的父目录下,即和 user
目录保持平级 。
<template>
<div>
用户管理导航
<nuxt-link :to="'/user/101'">点击修改IDnuxt-link>
<nuxt-child/>
div>
template>
<script>
export default{
layout:"test"
}
script>
<style>
style>
_id.vue
页面实现了向页面传入 id
参数,页面内容如下:
<template>
<div>
修改用户信息{{id}}
div>
template>
<script>
export default {
layout:"test",
data(){
return{
id:""
}
},
mounted(){
this.id = this.$route.params.id;
console.log(this.id)
}
}
script>
<style>
style>
测试:http://localhost:10000/user
点击修改
Nuxt.js 扩展了 Vue.js
,增加了一个叫 asyncData
的方法, asyncData
方法会在组件(限于页面组件)每次加载之前被调用。
它可以在服务端或路由更新之前被调用。 在这个方法被调用的时候,第一个参数被设定为当前页面的上下文对象,你可以利用 asyncData
方法来获取数据,Nuxt.js
会将 asyncData
返回的数据融合组件 data
方法返回的数据一并返回给当前组件,从而实现服务端渲染页面的效果。
注意:由于 asyncData
方法是在组件 初始化 前被调用的,所以在方法内是没有办法通过 this
来引用组件的实例对象。
例子:在上边例子中的 user/_id.vue
中添加,页面代码如下:
<template>
<div>
修改用户信息{{id}},名称:{{name}},课程名称:{{course}}
div>
template>
<script>
export default{
layout:'test',
//根据id查询用户信息
asyncData(){
console.log("async方法")
return {
name:'黑马程序员'
}
},
data(){
return {
id:'',
course: ""
}
},
methods:{
getCourse:function(){
this.course = "spring实战666"
}
},
mounted(){
this.id = this.$route.params.id;
this.getCourse();
}
}
script>
<style>
style>
此方法在服务端被执行,观察服务端控制台打印输出 “async方法”。
此方法返回 data
模型数据,在服务端被渲染,最后响应给前端,刷新此页面查看页面源代码可以看到 name
模型数据已在页面源代码中显示,而 course
变量是在 mounted
钩子函数中调用了 getCourse
方法对 course
进行赋值,属于客户端使用 JS
进行渲染,所以在页面源代码中没有看到 course
变量的值,如下图所示
使用 async
和 await
配合 promise
也可以实现同步调用,nuxt.js
中使用 async/await
实现同步调用效果。
1、先测试异步调用,增加a、b两个方法,并在 mounted
中调用
methods:{
a(){
return new Promise(function(resolve,reject){
setTimeout(function () {
resolve(1)
},2000)
})
},
b(){
return new Promise(function(resolve,reject){
setTimeout(function () {
resolve(2)
},1000)
})
}
},
mounted(){
this.a().then(res=>{
alert(res)
console.log(res)
})
this.b().then(res=>{
alert(res)
console.log(res)
})
}
从上述代码中,a
方法使用 setTimeout
延迟了2秒执行,b
方法延迟了1秒,如果按同步顺序进行执行,应该还是先输出 a
方法的内容再输出 b
方法。
观察客户端,并没有按照方法执行的顺序输出,使用 Promise
实现了异步调用,执行结果如下图
2、使用 async/await
完成同步调用
async asyncData({ store, route }) {
console.log("async方法")
var a = await new Promise(function (resolve, reject) {
setTimeout(function () {
console.log("1")
resolve(1)
},2000)
});
var a = await new Promise(function (resolve, reject) {
setTimeout(function () {
console.log("2")
resolve(2)
},1000)
});
return {
name:'黑马程序员'
}
},
这里我们在 asyncData
方法前面增加了 async
关键字,在调用 Promise
前也增加了 await
, 观察服务端控制台发现是按照 a、b
方法的调用顺序输出 1、2,实现了使用 async/await
完成同步调用。
上图是课程搜索前端的界面,用户通过前端向服务端发起搜索请求,搜索功能包括:
1、界面默认查询所有课程,并分页显示
2、通过一级分类和二分类搜索课程,选择一级分类后将显示下属的二级分类
3、通过关键字搜索课程
4、通过课程等级搜索课程
nuxt.js
将 /layout/default.vue
作为所有页面的默认布局,通常布局包括:页头、内容区、页尾
default.vue
内容如下:
<template>
<div>
<Header />
<nuxt/>
<Footer />
div>
template>
<script>
import Footer from '../components/Footer.vue'
import Header from '../components/Header.vue'
export default {
components: {
Header,
Footer
}
}
script>
<style>
style>
搜索页面中以 /static
开头的静态资源通过 nginx
解析,如下:
/static/plugins
:指向门户目录下的 plugins
目录。
/static/css
:指向门户目录下的的 css
目录
修改 Nginx
中 www.xuecheng.com 虚拟主机的配置:
在之前的章节当中如果已经配置了静态资源虚拟主机,可以忽略这个步骤
#静态资源,包括系统所需要的图片,js、css等静态资源
location /static/img/ {
alias F:/develop/xc_portal_static/img/;
#静态资源,包括系统所需要的图片,js、css等静态资源
location /static/img/ {
alias F:/develop/xc_portal_static/img/;
}
location /static/css/ {
alias F:/develop/xc_portal_static/css/;
}
location /static/js/ {
alias F:/develop/xc_portal_static/js/;
}
location /static/plugins/ {
alias F:/develop/xc_portal_static/plugins/;
add_header Access-Control-Allow-Origin http://ucenter.xuecheng.com;
add_header Access-Control-Allow-Credentials true;
add_header Access-Control-Allow-Methods GET;
}
配置搜索 Url
,下图是 Nginx
搜索转发流程图:
用户请求 /course/search
时 Nginx
将请求转发到 nuxt.js
服务,nginx
在转发时根据每台 nuxt
服务的负载情况进行转发,实现负载均衡。
本教程开发环境 Nuxt.js
服务和 www.xuecheng.com 虚拟机主在同一台计算机,使用同一个 nginx
,配置如下:
#前端门户课程搜索
location ^~ /course/search {
proxy_pass http://dynamic_portal_server_pool;
}
#后端搜索服务
location /openapi/search/ {
proxy_pass http://search_server_pool/search/;
}
dynamic_portal_server_pool
配置如下 :
#前端动态门户
upstream dynamic_portal_server_pool{
server 127.0.0.1:10000 weight=10;
}
#后台搜索服务(公开api)
upstream search_server_pool{
server 127.0.0.1:40100 weight=10;
}
其它配置:
nuxt.js 会自动请求这些一些内置的api,如果不配置的话前端会报错,所以还是给它整上,暂时不需要去追究这些接口是何作用
#开发环境webpack定时加载此文件
location ^~ /__webpack_hmr {
proxy_pass http://dynamic_portal_server_pool/__webpack_hmr;
}
#开发环境 nuxt 访问 _nuxt
location ^~ /_nuxt/ {
proxy_pass http://dynamic_portal_server_pool/_nuxt/;
}
在静态虚拟主机中添加:
#分类信息
location /static/category/ {
alias E:/Project/XueChengOnline/xcEduUI01/xuecheng/static/category/;
}
创建搜索页面如下:
页面文件参考:资料/search/index_1.vue
,重要代码如下:
nuxt.js
支持定义 header
,本页面我们在 header
中引入 css
样式并定义头部信息。
//配置文件
let config = require('~/config/sysConfig')
import querystring from 'querystring'
import * as courseApi from '~/api/course'
export default {
head() {
return {
title: '传智播客-一样的教育,不一样的品质',
meta: [
{charset: 'utf-8'},
{name: 'description', content: '传智播客专注IT培训,Java培训,Android培训,安卓培训,PHP培训,C++培训,网页设计培训,平面设计培训,UI设计培训,移动开发培训,网络营销培训,web前端培训,云计算大数据培训,全栈工程师培训,产品经理培训。'},
{name: 'keywords', content: this.keywords}
],
link: [
{rel: 'stylesheet', href: '/static/plugins/normalize-css/normalize.css'},
{rel: 'stylesheet', href: '/static/plugins/bootstrap/dist/css/bootstrap.css'},
{rel: 'stylesheet', href: '/static/css/page-learing-list.css'}
]
}
},
其它数据模型及方法:
<script>
//配置文件
let config = require('~/config/sysConfig')
import querystring from 'querystring'
import * as courseApi from '~/api/course'
export default {
head() {
return {
title: '传智播客-一样的教育,不一样的品质',
meta: [
{charset: 'utf-8'},
{name: 'description', content: '传智播客专注IT培训,Java培训,Android培训,安卓培训,PHP培
训,C++培训,网页设计培训,平面设计培训,UI设计培训,移动开发培训,网络营销培训,web前端培训,云计算大数据培训,
全栈工程师培训,产品经理培训。'},
{name: 'keywords', content: this.keywords}
],
link: [
{rel: 'stylesheet', href: '/static/plugins/normalize-css/normalize.css'},
{rel: 'stylesheet', href: '/static/plugins/bootstrap/dist/css/bootstrap.css'},
{rel: 'stylesheet', href: '/static/css/page-learing-list.css'}
]
}
},
async asyncData({ store, route }) {
return {
courselist: {},
first_category:{},
second_category:{},
mt:'',
st:'',
grade:'',
keyword:'',
total:0,
imgUrl:config.imgUrl
}
},
data() {
return {
courselist: {},
first_category:{},
second_category:{},
mt:'',
st:'',
grade:'',
keyword:'',
imgUrl:config.imgUrl,
total:0,//总记录数
page:1,//页码
page_size:12//每页显示个数
}
},
watch:{//路由发生变化立即搜索search表示search方法
'$route':'search'
},
methods: {
//分页触发
handleCurrentChange(page) {
},
//搜索方法
search(){
//刷新当前页面
window.location.reload();
}
}
}
script>
重启Nginx,请求:http://www.xuecheng.com/course/search,页面效果如下:
初次进入页面,没有输入任何查询条件,默认查询全部课程,分页显示
在api目录创建本工程所用的api方法类,api方法类使用了public.js等一些抽取类:
/api/public.js-------------抽取axios 的基础方法
/api/util.js-----------------工具类
/config/sysConfig.js----系统配置类,配置了系统参数变量
创建 course.js
,作为课程相关业务模块的 api
方法类。
import http from './public'
import qs from 'qs'
let config = require('~/config/sysConfig')
let apiURL = config.apiURL
let staticURL = config.staticURL
if (typeof window === 'undefined') {
apiURL = config.backApiURL
staticURL = config.backStaticURL
}
/*搜索*/
export const search_course = (page,size,params) => {
let querys = qs.stringify(params);
return http.requestQuickGet(apiURL+"/search/course/list/"+page+"/"+size+"?"+querys);
}
实现思路如下:
1、用户请求本页面到达 node.js
2、在 asyncData
方法中向服务端请求查询课程
3、asyncData
方法执行完成开始服务端渲染在 asyncData
中执行搜索,代码如下:
async asyncData({ store, route }) {//服务端调用方法
//搜索课程
let page = route.query.page;
if(!page){
page = 1;
}else{
page = Number.parseInt(page)
}
console.log(page);
//请求搜索服务,搜索服务
let course_data = await courseApi.search_course(page,2,route.query);
console.log(course_data)
//拿到数据
if (course_data && course_data.queryResult ) {
let keywords = ''
let mt=''
let st=''
let grade=''
let keyword=''
let total = course_data.queryResult.total
if( route.query.mt){
mt = route.query.mt
}
if( route.query.st){
st = route.query.st
}
if( route.query.grade){
grade = route.query.grade
}
if( route.query.keyword){
keyword = route.query.keyword
}
return {
courselist: course_data.queryResult.list,//课程列表
keywords:keywords,
mt:mt,
st:st,
grade:grade,
keyword:keyword,
page:page,
total:total,
imgUrl:config.imgUrl
}
}else{
//未拿到数据,返回空值对象到前端
return {
courselist: {},
first_category:{},
second_category:{},
mt:'',
st:'',
grade:'',
keyword:'',
page:page,
total:0,
imgUrl:config.imgUrl
}
}
}
在页面中展示课程列表。
<div class="recom-item" v-for="(course, index) in courselist">
<nuxt-link :to="'/course/detail/'+course.id+'.html'" target="_blank">
<div v-if="course.pic">
<p>
<img :src="imgUrl+'/'+course.pic" width="100%" alt />
p>
div>
<div v-else>
<p>
<img src="/img/widget-demo1.png" width="100%" alt />
p>
div>
<ul>
<li class="course_title">
<span v-html="course.name">span>
li>
<li style="float: left">
<span v-if="course.charge == '203001'">免费span>
<span v-if="course.charge == '203002'">¥{{course.price | money}}span>
li>
ul>
nuxt-link>
div>
添加在 index.vue
页面的 content-list
节点下,具体代码参考 资料/index_2.vue
文件
访问搜索页面,nuxt.js
会在页面渲染之前请求查询接口拿到数据,并在 node.js 上完成页面的渲染
效果预览
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pK9ajaIO-1595566122416)(https://qnoss.codeyee.com/20200704_12/image21)]
服务端实现代码已在 day11
的内容中完成,搜索服务核心代码如下
...
//分页
//当前页码
if(page<=0){
page = 1;
} /
/起始记录下标
int from = (page -1) * size;
searchSourceBuilder.from(from);
searchSourceBuilder.size(size);
...
使用 Element ui
的 el-pagination
分页插件:
<div style="text-align: center">
<el-pagination
background
layout="prev, pager, next"
@current-change="handleCurrentChange"
:total="total"
:page-size="page_size"
:current-page="page"
prev-text="上一页"
next-text="下一页">
el-pagination>
div>
定义分页触发方法:
methods:{
//分页触发
handleCurrentChange(page) {
this.page = page
this.$route.query.page = page
let querys = querystring.stringify(this.$route.query)
window.location = '/course/search?'+querys;
} .
..
1)通过一级分类搜索
2)选择一级分类后将显示下属的二级分类
3)选择二分类进行搜索
4)选择一级分类的全部则表示没有按照分类搜索
5)选择一级分类的全部时二级分类不显示
课程分类将通过页面静态化的方式写入静态资源下,通过 /category/category.json
可访问,
通过 www.xuecheng.com/static/category/category.json 即可访问。
category.json
的内容如下
我们需要定义 api
方法获取所有的分类
在 /api/course.js
中添加:
/*获取分类*/
export const sysres_category = () => {
return http.requestQuickGet(staticURL+"/static/category/category.json");
}
进入搜索页面将默认显示所有一级分类,当前如果已选择一级分类则要显示所有一级分类及该一级分类下属的二级
分类。在 asyncData
方法中实现上边的需求,代码如下:
async asyncData({ store, route }) {
//服务端调用方法
//搜索课程
let page = route.query.page;
if (!page) {
page = 1;
} else {
page = Number.parseInt(page);
}
console.log(page);
//请求搜索服务,搜索服务
let course_data = await courseApi.search_course(page, 2, route.query);
console.log(course_data);
let category_data = await courseApi.sysres_category();
console.log(category_data)
if (course_data && course_data.queryResult) {
let keywords = "";
let mt = "";
let st = "";
let grade = "";
let keyword = "";
let total = course_data.queryResult.total;
if (route.query.mt) {
mt = route.query.mt;
}
if (route.query.st) {
st = route.query.st;
}
if (route.query.grade) {
grade = route.query.grade;
}
if (route.query.keyword) {
keyword = route.query.keyword;
}
//全部分类
let category = category_data.category; //分部分类
let first_category = category[0].children; //一级分类
let second_category = []; //二级分类
//遍历一级分类
for (var i in first_category) {
keywords += first_category[i].name + " ";
if (mt != "" && mt == first_category[i].id) {
//取出二级分类
second_category = first_category[i].children;
// console.log(second_category)
break;
}
}
return {
courselist: course_data.queryResult.list, //课程列表
keywords: keywords,
first_category: first_category,
second_category: second_category,
mt: mt,
st: st,
grade: grade,
keyword: keyword,
page: page,
total: total,
imgUrl: config.imgUrl
};
} else {
return {
courselist: {},
first_category: {},
second_category: {},
mt: "",
st: "",
grade: "",
keyword: "",
page: page,
total: 0,
imgUrl: config.imgUrl
};
}
},
在页面显示一级分类及二级分类,需要根据当前是否选择一级分类、是否选择二分类显示页面内容。
<ul>
<li>一级分类:li>
<li v-if="mt!=''"><nuxt-link class="title-link" :to="'/course/search?
keyword='+keyword+'&grade='+grade">全部nuxt-link>li>
<li class="all" v-else>全部li>
<ol>
<li v-for="category_v in first_category">
<nuxt-link class="title-link all" :to="'/course/search?keyword='+keyword+'&mt=' +
category_v.id" v-if="category_v.id == mt">{{category_v.name}}nuxt-link>
<nuxt-link class="title-link" :to="'/course/search?keyword='+keyword+'&mt=' +
category_v.id" v-else>{{category_v.name}}nuxt-link>
li>
ol>
ul>
<ul>
<li>二级分类:li>
<li v-if="st!=''"><nuxt-link class="title-link" :to="'/course/search?
keyword='+keyword+'&mt='+mt+'&grade='+grade">全部nuxt-link>li>
<li class="all" v-else>全部li>
<ol v-if="second_category.length>0">
<li v-for="category_v in second_category">
<nuxt-link class="title-link all" :to="'/course/search?keyword='+keyword+'&mt='+mt+'&st='
+ category_v.id" v-if="category_v.id == st">{{category_v.name}}nuxt-link>
<nuxt-link class="title-link" :to="'/course/search?keyword='+keyword+'&mt='+mt+'&st=' +
category_v.id" v-else>{{category_v.name}}nuxt-link>
li>
ol>
ul>
当用户点击分类时立即执行搜索,实现思路如下:
点击分类立即更改路由。
通过监听路由,路由更改则刷新页面。
1)创建搜索方法
search(){
//刷新当前页面
window.location.reload();
}
2)定义watch
通过 vue.js
的 watch
可以实现监视某个变量,当变量值出现变化时执行某个方法。
实现思路是:
1、点击分类页面路由更改
2、通过 watch
监视路由,路由更改触发 search
方法与 methods
并行定义 watch
:
watch: {
//路由发生变化立即搜索search表示search方法
$route: "search"
},
用户选择不同的课程难度等级去搜索课程。
使用 search_course
方法完成搜索。
按难度等级搜索思路如下:
1)点击难度等级立即更改路由。
2)通过监听路由,路由更改则立即执行 search
搜索方法
按难度等级搜索页面代码如下:
<ul>
<li>难度等级:li>
<li v-if="grade!=''">
<nuxt-link class="title-link" :to="'/course/search?keyword='+keyword+'&mt=' +
mt+'&st='+st+'&grade='">全部
nuxt-link>
li>
<li class="all" v-else>全部li>
<ol>
<li v-if="grade=='200001'" class="all">初级li>
<li v-else><nuxt-link class="title-link" :to="'/course/search?keyword='+keyword+'&mt=' +
mt+'&st='+st+'&grade=200001'">初级nuxt-link>li>
<li v-if="grade=='200002'" class="all">中级li>
<li v-else><nuxt-link class="title-link" :to="'/course/search?keyword='+keyword+'&mt=' +
mt+'&st='+st+'&grade=200002'">中级nuxt-link>li>
<li v-if="grade=='200003'" class="all">高级li>
<li v-else><nuxt-link class="title-link" :to="'/course/search?keyword='+keyword+'&mt=' +
mt+'&st='+st+'&grade=200003'">高级nuxt-link>li>
ol>
ul>
高亮的核心代码
...
//定义高亮
HighlightBuilder highlightBuilder = new HighlightBuilder();
highlightBuilder.preTags("");
highlightBuilder.postTags("");
highlightBuilder.fields().add(new HighlightBuilder.Field("name"));
searchSourceBuilder.highlighter(highlightBuilder);
...
//添加数据
for(SearchHit hit:searchHits){
CoursePub coursePub = new CoursePub();
//源文档
Map<String, Object> sourceAsMap = hit.getSourceAsMap();
//课程id
String id = (String) sourceAsMap.get("id");
coursePub.setId(id);
//取出name
String name = (String) sourceAsMap.get("name");
//取出高亮字段
Map<String, HighlightField> highlightFields = hit.getHighlightFields();
if(highlightFields.get("name")!=null){
HighlightField highlightField = highlightFields.get("name");
Text[] fragments = highlightField.fragments();
StringBuffer stringBuffer = new StringBuffer();
for(Text text:fragments){
stringBuffer.append(text);
}
name = stringBuffer.toString();
}
coursePub.setName(name);
....
}
核心的代码主要是设置 HighlightBuilder
对象的高亮属性,然后在遍历添加数据的循环中,在map中取出name
属性后,再取出高亮字段,并且设置到 name
属性中。
以下是搜索服务的全部代码
package com.xuecheng.search.service;
import com.xuecheng.framework.domain.course.CoursePub;
import com.xuecheng.framework.domain.search.CourseSearchParam;
import com.xuecheng.framework.model.response.CommonCode;
import com.xuecheng.framework.model.response.QueryResponseResult;
import com.xuecheng.framework.model.response.QueryResult;
import org.apache.commons.lang3.StringUtils;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.text.Text;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.MultiMatchQueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightField;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import javax.naming.directory.SearchResult;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Map;
@Service
public class EsCourseService {
private static final Logger LOGGER = LoggerFactory.getLogger(EsCourseService.class);
@Value("${xuecheng.elasticsearch.course.index}")
private String es_index;
@Value("${xuecheng.elasticsearch.course.type}")
private String es_type;
@Value("${xuecheng.elasticsearch.course.source_field}")
private String source_field;
@Autowired
RestHighLevelClient restHighLevelClient;
/**
* 课程列表搜索
* @param page 页码
* @param size 每页数量
* @param courseSearchParam 搜索参数
* @return
* @throws IOException
*/
public QueryResponseResult<CoursePub> findList(int page, int size, CourseSearchParam courseSearchParam) throws IOException{
//设置索引
SearchRequest searchRequest = new SearchRequest(es_index);
//设置类型
searchRequest.types(es_type);
//创建搜索源对象
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
//创建布尔查询对象
BoolQueryBuilder boolQueryBuilder = new BoolQueryBuilder();
//源字段过滤
String[] fieldArr = source_field.split(",");
searchSourceBuilder.fetchSource(fieldArr,new String[]{});
//根据关键字进行查询
if(StringUtils.isNotEmpty(courseSearchParam.getKeyword())){
//匹配关键词
MultiMatchQueryBuilder multiMatchQueryBuilder = QueryBuilders.multiMatchQuery(courseSearchParam.getKeyword(), "name", "teachplan", "description");
//设置匹配占比
multiMatchQueryBuilder.minimumShouldMatch("70%");
//提升字段的权重值
multiMatchQueryBuilder.field("name",10);
boolQueryBuilder.must(multiMatchQueryBuilder);
}
//根据难度进行过滤
if(StringUtils.isNotEmpty(courseSearchParam.getMt())){
boolQueryBuilder.filter(QueryBuilders.termQuery("mt",courseSearchParam.getMt()));
}
if(StringUtils.isNotEmpty(courseSearchParam.getSt())){
boolQueryBuilder.filter(QueryBuilders.termQuery("st",courseSearchParam.getSt()));
}
//根据等级进行过滤
if(StringUtils.isNotEmpty(courseSearchParam.getGrade())){
boolQueryBuilder.filter(QueryBuilders.termQuery("grade",courseSearchParam.getGrade()));
}
//设置分页参数
if(page<=0){
page = 1;
}
if(size<=0){
size = 20;
}
//计算搜索起始位置
int start = (page-1) * size;
searchSourceBuilder.from(start);
searchSourceBuilder.size(size);
//将布尔查询对象添加到搜索源内
searchSourceBuilder.query(boolQueryBuilder);
//配置高亮信息
HighlightBuilder highlightBuilder = new HighlightBuilder();
highlightBuilder.preTags("");
highlightBuilder.postTags("");
//设置高亮字段
highlightBuilder.fields().add(new HighlightBuilder.Field("name"));
searchSourceBuilder.highlighter(highlightBuilder);
//请求搜索
searchRequest.source(searchSourceBuilder);
SearchResponse searchResponse = null;
try{
searchResponse = restHighLevelClient.search(searchRequest);
}catch (Exception e){
//搜索异常
e.printStackTrace();
LOGGER.error("search error ...{}",e.getMessage());
return new QueryResponseResult<>(CommonCode.FAIL,null);
}
//结果收集处理
SearchHits hits = searchResponse.getHits();
//获取匹配度高的结果
SearchHit[] searchHits = hits.getHits();
//总记录数
long totalHits = hits.getTotalHits();
//数据列表
ArrayList<CoursePub> list = new ArrayList<>();
//添加数据
for (SearchHit hit: searchHits){
CoursePub coursePub = new CoursePub();
Map<String, Object> sourceAsMap = hit.getSourceAsMap();
//取出id
String id = (String) sourceAsMap.get("id");
coursePub.setId(id);
//取出名称
String name = (String) sourceAsMap.get("name");
//取出高亮字段
Map<String, HighlightField> highlightFields = hit.getHighlightFields();
if(highlightFields.get("name")!=null){
HighlightField highlightField = highlightFields.get("name");
Text[] fragments = highlightField.fragments();
StringBuffer stringBuffer = new StringBuffer();
for(Text text:fragments){
stringBuffer.append(text);
}
name = stringBuffer.toString();
}
coursePub.setName(name);
//图片
String pic = (String) sourceAsMap.get("pic");
coursePub.setPic(pic);
//优惠后的价格
Float price = null;
try {
if(sourceAsMap.get("price") !=null){
price = Float.parseFloat(String.format("%.3f",sourceAsMap.get("price")));
}
}catch (Exception e){
e.printStackTrace();
}
coursePub.setPrice(price);
//优惠前的价格
Float priceOld = null;
try {
if(sourceAsMap.get("price_old") !=null){
priceOld = Float.parseFloat(String.format("%.3f",sourceAsMap.get("price_old")));
}
}catch (Exception e){
e.printStackTrace();
}
coursePub.setPrice_old(priceOld);
list.add(coursePub);
}
//返回响应结果
QueryResult<CoursePub> queryResult = new QueryResult<>();
queryResult.setList(list);
queryResult.setTotal(totalHits);
return new QueryResponseResult<>(CommonCode.SUCCESS,queryResult);
}
}
在后端的代码中,我们在添加高亮标签时候引用了 eslight
的样式,代码如下
highlightBuilder.preTags("<font class='eslight'>");
所以我们在 search/index.vue
中定义 eslight
样式,实现多高亮字段的样式控制。