1.1需求描述
采用vue.js开发搜索界面则SEO不友好,需要解决SEO的问题。
1.2了解SEO
总结:seo是网站为了提高自已的网站排名,获得更多的流量,对网站的结构及内容进行调整优化,以便搜索引擎
(百度,google等)更好抓取到更优质的网站的内容。
下图是搜索引擎爬取网站页面的大概流程图:
(搜索引擎的工作流程很复杂,下图只是简单概括)
从上图可以看到SEO是网站自己为了方便spider抓取网页而作出的网页内容优化,常见的SEO方法比如:
1)对url链接的规范化,多用restful风格的url,多用静态资源url;
2) 注意title、keywords的设置。
3)由于spider对javascript支持不好,对于网页跳转用href标签。
。。。
1.3服务端渲染和客户端渲染
采用什么技术有利于SEO?要解答这个问题需要理解服务端渲染和客户端渲染。什么是服务端渲染?
我们用传统的servlet开发来举例:浏览器请求servlet,servlet在服务端生成html响应给浏览器,浏览器展示html 的内容,这个过程就是服务端渲染,如下图:
服务端渲染的特点:
1)在服务端生成html网页的dom元素。
2)客户端(浏览器)只负责显示dom元素内容。
当初随着web2.0的到来,A JAX技术兴起,出现了客户端渲染:客户端(浏览器) 使用A JAX向服务端发起http请求,获取到了想要的数据,客户端拿着数据开始渲染html网页,生成Dom元素,并最终将网页内容展示给用户, 如下图:
S
客户端渲染的特点:
1)在服务端只是给客户端响应的了数据,而不是html网页
2)客户端(浏览器)负责获取服务端的数据生成Dom元素。
两种方式各有什么优缺点? 客户端渲染:
1)缺点
不利于网站进行SEO,因为网站大量使用javascript技术,不利于spider抓取网页。
2)优点
客户端负责渲染,用户体验性好,服务端只提供数据不用关心用户界面的内容,有利于提高服务端的开发效率。
3)适用场景
对SEO没有要求的系统,比如后台管理类的系统,如电商后台管理,用户管理等。
服务端渲染:
1)优点
有利于SEO,网站通过href的url将spider直接引到服务端,服务端提供优质的网页内容给spider。
2)缺点
服务端完成一部分客户端的工作,通常完成一个需求需要修改客户端和服务端的代码,开发效率低,不利于系统的 稳定性。
3)适用场景
对SEO有要求的系统,比如:门户首页、商品详情页面等。
2.1Nuxt.js介绍
移动互联网的兴起促进了web前后端分离开发模式的发展,服务端只专注业务,前端只专注用户体验,前端大量运 用的前端渲染技术,比如流行的vue.js、react框架都实现了功能强大的前端渲染。
但是,对于有SEO需求的网页如果使用前端渲染技术去开发就不利于SEO了,有没有一种即使用vue.js、react的前 端技术也实现服务端渲染的技术呢?其实,对于服务端渲染的需求,vue.js、react这样流行的前端框架提供了服务端渲染的解决方案。
从上图可以看到:
react框架提供next.js实现服务端渲染。
vue.js框架提供Nuxt.js实现服务端渲染。
2.2Nuxt.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
自动代码分层服务端渲染
强大的路由功能,支持异步数据静态文件服务
ES6/ES7 语法支持
打包和压缩 JS 和 CSS HTML头部标签管理本地开发支持热加载集成ESLint
支持各种样式预处理器: SASS、LESS、 Stylus等等
3.1创建Nuxt工程
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版本,并加入了今后开发中所使用的依赖包,直接解压本工程即 可使用。
3.2目录结构
本工程的目录结构如下:
‐资源目录 资源目录 assets 用于组织未编译的静态资源如 LESS、SASS 或 JavaScript。 ‐组件目录 组件目录 components 用于组织应用的 Vue.js 组件。Nuxt.js 不会扩展增强该目录下 Vue.js 组件,即这些组件不会像页面组件那样有 asyncData 方法的特性。 ‐布局目录 布局目录 layouts 用于组织应用的布局组件。该目录名为Nuxt.js保留的,不可更改。 ‐中间件目录 middleware 目录用于存放应用的中间件。 ‐页面目录 页面目录 pages 用于组织应用的路由及视图。Nuxt.js 框架读取该目录下所有的 .vue 文件并自动生成对应的路由配置。 该目录名为Nuxt.js保留的,不可更改。 ‐插件目录 插件目录 plugins 用于组织那些需要在 根vue.js应用 实例化之前需要运行的 Javascript 插件。 ‐静态文件目录 静态文件目录 static 用于存放应用的静态文件,此类文件不会被 Nuxt.js 调用 Webpack 进行构建编译处理。 服务器启动的时候,该目录下的文件会映射至应用的根路径 / 下。 举个例子: /static/logo.png 映射至 /logo.png 该目录名为Nuxt.js保留的,不可更改。 ‐Store 目 录 store 目录用于组织应用的 Vuex 状态树 文件。 Nuxt.js 框架集成了 Vuex 状态树 的相关功能配置,在 store 目录下创建一个 index.js 文件可激活这些配置。 该目录名为Nuxt.js保留的,不可更改。 ‐nuxt.config.js 文 件 nuxt.config.js 文件用于组织Nuxt.js 应用的个性化配置,以便覆盖默认配置。该文件名为Nuxt.js保留的,不可更改。 ‐package.json 文 件 package.json 文件用于描述应用的依赖关系和对外暴露的脚本接口。 该文件名为Nuxt.js保留的,不可更改。
nuxt.js 提供了目录的别名,方便在程序中引用:
3.3页面布局
页面布局就是页面内容的整体结构,通过在layouts目录下添加布局文件来实现。在layouts 根目录下的所有文件都属于个性化布局文件,可以在页面组件中利用 layout 属性来引用。
一个例子:
1、定义:layouts/test.vue布局文件,如下:
注意:布局文件中一定要加 <nuxt/> 组件用于显示页面内容。
<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,如果如下:
这里是头测试页面这里是尾
3.4路由
3.4.1基础路由
Nuxt.js 依据目录结构自动生成 vue-router 模块的路由配置。
Nuxt.js根据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
3.4.2嵌套路由
你可以通过 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'">修改</nuxt‐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
点击修改:
3.6获取数据
3.6.1asyncData 方法
Nuxt.js 扩展了 Vue.js,增加了一个叫 asyncData 的方法,
方法会在组件(限于页面组件)每次加载之前被调用。它可以在服务端或路由更新之前被调用。 在这个方法被调用的时候,第一个参数被设定为当前页面的上下文对象,你可以利用返回的数据一并返回给当前组件。方法来获取数据,Nuxt.js 会将返回的数据融合组件方法
注意:由于对象。
例子:
方法是在组件 初始化 前被调用的,所以在方法内是没有办法通过来引用组件的实例
在上边例子中的user/_id.vue中添加,页面代码如下:
<template> <div> 修改用户信息{{id}},名称:{{name}} </div> </template> <script> export default{ layout:'test', //根据id查询用户信息 asyncData(){ console.log("async方法") return { name:'黑马程序员' } }, data(){ return { id:'' } }, mounted(){ this.id=this.$route.params.id; } } </script> <style> </style>
此方法在服务端被执行,观察服务端控制台打印输出“async方法”。
此方法返回data模型数据,在服务端被渲染,最后响应给前端,刷新此页面查看页面源代码可以看到name模型数据已在页面源代码中显示。
3.6.2async /await方法
使用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) }) }
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:'黑马程序员' } },
观察服务端控制台发现是按照a、b方法的调用顺序输出1、2,实现了使用async/await完成同步调用。
3.1搜索页面
3.1.1需求分析
观察服务端控制台发现是按照a、b方法的调用顺序输出1、2,实现了使用async/await完成同步调用。
3.1搜索页面
3.1.1需求分析
<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>
3.1.3Nginx代理配置
搜索页面中以/static开头的静态资源通过nginx解析,如下:
/static/plugins:指向门户目录下的plugins目录。
/static/css:指向门户目录下的的css目录
修改Nginx中www.xuecheng.com虚拟主机的配置:
#静态资源,包括系统所需要的图片,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/; } #分类信息 location /static/category/ { proxy_pass http://static_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; }
其它配置:
#开发环境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/; }
在静态虚拟主机中添加:
#学成网静态资源server { listen 91; server_name localhost; #分类信息 location /static/category/ { alias F:/develop/xuecheng/static/category/; } ...
3.1.4搜索页面
创建搜索页面如下:
3.1.4搜索页面
创建搜索页面如下:
//配置文件
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>
3.1.5测试
重启Nginx,请求:http://www.xuecheng.com/course/search,页面效果如下:
3.2查询全部
3.2.1需求分析
初次进入页面,没有输入任何查询条件,默认查询全部课程,分页显示。
3.2.2API方法
在api目录创建本工程所用的api方法类,api方法类使用了public.js等一些抽取类:
/api/public.js抽取axios 的基础方法
/api/util.js工具类
/con?g/sysCon?g.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); }
3.2.3搜索方法
实现思路如下:
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 } } }
3.2.5 页面
在页面中展示课程列表。
<div class="content‐list"> <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> <!‐‐ <em> ? </em>‐‐> <!‐‐<em>1125人在学习</em>‐‐></li> </ul> </nuxt‐link> </div> <li class="clearfix"></li> </div>
3.3分页查询
3.3.1服务端代码
... //分页 //当前页码 if(page<=0){ page=1; } //起始记录下标 int from=(page ‐1) * size; searchSourceBuilder.from(from); searchSourceBuilder.size(size); ...
3.3.2前端代码
使用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; } ...
3.4按分类搜索
3.4.1需求分析
1、通过一级分类搜索
2、选择一级分类后将显示下属的二级分类
3、选择二分类进行搜索
4、选择一级分类的全部则表示没有按照分类搜索
5、选择一级分类的全部时二级分类不显示
3.4.2API方法
课程分类将通过页面静态化的方式写入静态资源下,通过/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"); }
3.4.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) //查询分类 let category_data=await courseApi.sysres_category() if (course_data && course_data.queryResult ) { //全部分类 let category=category_data.category//分部分类 let first_category=category[0].children//一级分类let second_category=[]//二级分类 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 } //遍历一级分类 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,//课程列表first_category: first_category, second_category: second_category, 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 } } }
3.3.4页面
在页面显示一级分类及二级分类,需要根据当前是否选择一级分类、是否选择二分类显示页面内容。
<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> <!‐‐<ol> <li>数据分析</li> <li>机器学习工程</li> <li>前端开发工程</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> <!‐‐ <li>大数据</li> <li>云计算</li>‐‐> </ol> <!‐‐<a href="#" class="more">更多 ∨</a>‐‐> </ul>
当用户点击分类时立即执行搜索,实现思路如下:
1)点击分类立即更改路由。
2)通过监听路由,路由更改则刷新页面。
1)创建搜索方法
search(){ //刷新当前页面window.location.reload(); }
2)定义watch
通过vue.js的watch可以实现监视某个变量,当变量值出现变化时执行某个方法。 实现思路是:
1、点击分类页面路由更改
2、通过watch监视路由,路由更改触发search方法与methods并行定义watch:
watch:{//路由发生变化立即搜索search表示search方法'$route':'search' },
3.5按难度等级搜索
3.5.1需求分析
用户选择不同的课程难度等级去搜索课程。
3.5.2API方法
使用 search_course方法完成搜索。
3.5.3页面
按难度等级搜索思路如下:
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>
3.6高亮显示
3.6.1服务端代码
修改service的搜索方法,添加高亮设置:
... //定义高亮 HighlightBuilder highlightBuilder=new HighlightBuilder(); highlightBuilder.preTags("<font class='eslight'>"); highlightBuilder.postTags("</font>"); 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); ....
3.6.2前端代码
在search/index.vue中定义eslight样式:
<style> .eslight{ color: red; } ...
4.1需求分析
本次集成测试的目的如下:
1、测试课程发布与CMS接口是否正常。
2、测试课程发布与ES接口是否正常。
3、测试课程从创建到发布的整个过程。
4.2准备环境
1、启动MySQL、MongoDB
2、启动ElasticSearch、RabbitMQ 3、启动Eureka Server
4、启动CMS、课程管理服务、搜索服务。
5、启动Nginx、系统管理前端、教学管理前端、Nuxt.js。
试中常被问到的问题,此问题包含web开发中从前端到后端到运维的绝大多数知识,主要考察面试者知识的广度。本文会根据作者了解的程度增加不断更新,不足之处欢迎评论区补充。
首先了解一下URL的组成:
http://www.baidu.com:8080/news/index.asp?boardID=5&ID=24618&page=1#name
从上面的URL可以看出,一个完整的URL包括以下几部分:
1、协议部分:该URL的协议部分为“http:”,这代表网页使用的是HTTP协议。在Internet中可以使用多种协议,如HTTP,FTP等等本例中使用的是HTTP协议。在"HTTP"后面的“//”为分隔符
2、域名部分:该URL的域名部分为“www.baidu.com”。一个URL中,也可以使用IP地址作为域名使用
3、端口部分:跟在域名后面的是端口,域名和端口之间使用“:”作为分隔符。端口不是一个URL必须的部分,如果省略端口部分,将采用默认端口80
4、虚拟目录部分:从域名后的第一个“/”开始到最后一个“/”为止,是虚拟目录部分。虚拟目录也不是一个URL必须的部分。本例中的虚拟目录是“/news/”
5、文件名部分:从域名后的最后一个“/”开始到“?”为止,是文件名部分,如果没有“?”,则是从域名后的最后一个“/”开始到“#”为止,是文件部分,如果没有“?”和“#”,那么从域名后的最后一个“/”开始到结束,都是文件名部分。本例中的文件名是“index.asp”。文件名部分也不是一个URL必须的部分,如果省略该部分,则使用默认的文件名
6、锚部分:从“#”开始到最后,都是锚部分。本例中的锚部分是“name”。锚部分也不是一个URL必须的部分
7、参数部分:从“?”开始到“#”为止之间的部分为参数部分,又称搜索部分、查询部分。本例中的参数部分为“boardID=5&ID=24618&page=1”。参数可以允许有多个参数,参数与参数之间用“&”作为分隔符。
很多大公司面试喜欢问这样一道面试题,输入URL到看见页面发生了什么?,今天我们来总结一下。 简单来说,共有以下几个过程
下面我们来看看具体的细节
发送至 DNS 服务器并获得域名对应的 WEB 服务器的 ip 地址。
DNS 解析首先会从你的浏览器的缓存中去寻找是否有这个网址对应的 IP 地址,如果没有就向OS系统的 DNS 缓存中寻找,如果没有就是路由器的 DNS 缓存, 如果没有就是 ISP 的DNS 缓存中寻找。 所以,缓存的寻找过程就是: 浏览器 -> 系统 -> 路由器 -> ISP。 如果在某一个缓存中找到的话,就直接跳到下一步。 如果都没有找到的话,就会向 ISP 或者公共的域名解析服务发起 DNS 查找请求。这个查找的过程还是一个递归查询的过程。
输入www.google.com网址后,首先在本地的域名服务器中查找,没找到去根域名服务器查找,没有再去com顶级域名服务器查找,,如此的类推下去,直到找到IP地址,然后把它记录在本地,供下次使用。大致过程就是. -> .com -> google.com. -> www.google.com.。 (你可能觉得我多写 .,并木有,这个.对应的就是根域名服务器,默认情况下所有的网址的最后一位都是.,既然是默认情况下,为了方便用户,通常都会省略,浏览器在请求DNS的时候会自动加上)
既然已经懂得了解析的具体过程,我们可以看到上述一共经过了N个过程,每个过程有一定的消耗和时间的等待,因此我们得想办法解决一下这个问题!
DNS存在着多级缓存,从离浏览器的距离排序的话,有以下几种: 浏览器缓存,系统缓存,路由器缓存,IPS服务器缓存,根域名服务器缓存,顶级域名服务器缓存,主域名服务器缓存。
在你的chrome浏览器中输入:chrome://dns/,你可以看到chrome浏览器的DNS缓存。
系统缓存主要存在/etc/hosts(Linux系统)中
检查浏览器是否有缓存
通过Cache-Control和Expires来检查是否命中强缓存,命中则直接取本地磁盘的html(状态码为200 from disk(or memory) cache,内存or磁盘);
如果没有命中强缓存,则会向服务器发起请求(先进行下一步的TCP连接),服务器通过Etag和Last-Modify来与服务器确认返回的响应是否被更改(协商缓存),若无更改则返回状态码(304 Not Modified),浏览器取本地缓存;
若强缓存和协商缓存都没有命中则返回请求结果。
不知道你们有没有注意这样一件事,你访问http://baidu.com的时候,每次响应的并非是同一个服务器(IP地址不同),一般大公司都有成百上千台服务器来支撑访问,假设只有一个服务器,那它的性能和存储量要多大才能支撑这样大量的访问呢?DNS可以返回一个合适的机器的IP给用户,例如可以根据每台机器的负载量,该机器离用户地理位置的距离等等,这种过程就是DNS负载均衡
TCP 协议通过三次握手建立连接。
翻译成大白话就是:
为什么是3次?:避免历史连接,确认客户端发来的请求是这次通信的人。
为什么不是4次?:3次够了第四次浪费
建立连接的过程是利用客户服务器模式,假设主机A为客户端,主机B为服务器端。
采用三次握手是为了防止失效的连接请求报文段突然又传送到主机B,因而产生错误。失效的连接请求报文段是指:主机A发出的连接请求没有收到主机B的确认,于是经过一段时间后,主机A又重新向主机B发送连接请求,且建立成功,顺序完成数据传输。考虑这样一种特殊情况,主机A第一次发送的连接请求并没有丢失,而是因为网络节点导致延迟达到主机B,主机B以为是主机A又发起的新连接,于是主机B同意连接,并向主机A发回确认,但是此时主机A根本不会理会,主机B就一直在等待主机A发送数据,导致主机B的资源浪费。
采用两次握手不行,原因就是上面说的失效的连接请求的特殊情况。而在三次握手中, client和server都有一个发syn和收ack的过程, 双方都是发后能收, 表明通信则准备工作OK.
为什么不是四次握手呢? 大家应该知道通信中著名的蓝军红军约定, 这个例子说明, 通信不可能100%可靠, 而上面的三次握手已经做好了通信的准备工作, 再增加握手, 并不能显著提高可靠性, 而且也没有必要。
第一次握手:
客户端发送syn包(Seq=x)到服务器,并进入SYN_SEND状态,等待服务器确认;
第二次握手:
服务器收到syn包,必须确认客户的SYN(ack=x+1),同时自己也发送一个SYN包(Seq=y),即SYN+ACK包,此时服务器进入SYN_RECV状态;
第三次握手:
客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=y+1),此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手。
握手过程中传送的包里不包含数据,三次握手完毕后,客户端与服务器才正式开始传送数据。理想状态下,TCP连接一旦建立,在通信双方中的任何一方主动关闭连接之前,TCP 连接都将被一直保持下去。
HTTPS=HTTP + 加密 + 认证 + 完整性保护
要先申请CA证书,并安装在服务器上(一个文件,配置nginx支持监听443端口开启ssl并设置证书路径)
浏览器发送请求;
网站从浏览器发过来的加密规则中选一组自身也支持的加密算法和hash算法,并向浏览器发送带有公钥的证书,当然证书还包含了很多信息,如网站地址、证书的颁发机构、过期时间等。
浏览器解析证书。
验证证书的合法性。如颁发机构是否合法、证书中的网站地址是否与访问的地址一致,若不合法,则浏览器提示证书不受信任,若合法,浏览器会显示一个小锁头。
若合法,或用户接受了不合法的证书,浏览器会生成一串随机数的密码(即密钥),并用证书中提供的公钥加密。
使用约定好的hash计算握手消息,并使用生成的随机数(即密钥)对消息进行加密,最后将之前生成的所有消息一并发送给网站服务器。
网站服务器解析消息。用已有的私钥将密钥解密出来,然后用密钥解密发过来的握手消息,并验证是否跟浏览器传过来的一致。然后再用密钥加密一段握手消息,发送给浏览器。
浏览器解密并计算握手消息的HASH,如果与服务端发来的HASH一致,此时握手过程结束,之后所有的通信数据将由之前浏览器生成的随机密码并利用对称加密算法进行加密。这里浏览器与网站互相发送加密的握手消息并验证,目的是为了保证双方都获得了一致的密码,并且可以正常的加密解密数据,为后续真正数据的传输做一次测试。
发送HTTP请求
首先科补一个小知识,HTTP的端口为80/8080,而HTTPS的端口为443
发送HTTP请求的过程就是构建HTTP请求报文并通过TCP协议中发送到服务器指定端口 请求报文由请求行,请求抱头,请求正文组成。
请求行
请求行的格式为Method Request-URL HTTP-Version CRLF eg: GET index.html HTTP/1.1 常用的方法有: GET, POST, PUT, DELETE, OPTIONS, HEAD。
常见的请求方法区别
这里主要展示POST和GET的区别
常见的区别
注意一点你也可以在GET里面藏body,POST里面带参数
重点区别
GET会产生一个TCP数据包,而POST会产生两个TCP数据包。
详细的说就是:
注意一点,并不是所有的浏览器都会发送两次数据包,Firefox就发送一次
请求报头
请求报头允许客户端向服务器传递请求的附加信息和客户端自身的信息。
从图中可以看出,请求报头中使用了Accept, Accept-Encoding, Accept-Language, Cache-Control, Connection, Cookie等字段。Accept用于指定客户端用于接受哪些类型的信息,Accept-Encoding与Accept类似,它用于指定接受的编码方式。Connection设置为Keep-alive用于告诉客户端本次HTTP请求结束之后并不需要关闭TCP连接,这样可以使下次HTTP请求使用相同的TCP通道,节省TCP连接建立的时间。
请求正文
当使用POST, PUT等方法时,通常需要客户端向服务器传递数据。这些数据就储存在请求正文中。在请求包头中有一些与请求正文相关的信息,例如: 现在的Web应用通常采用Rest架构,请求的数据格式一般为json。这时就需要设置Content-Type: application/json。
更重要的事情-HTTP缓存
HTTP属于客户端缓存,我们常认为浏览器有一个缓存数据库,用来保存一些静态文件,下面我们分为以下几个方面来简单介绍HTTP缓存
缓存的规则
缓存规则分为强制缓存和协商缓存
强制缓存
当缓存数据库中有客户端需要的数据,客户端直接将数据从其中拿出来使用(如果数据未失效),当缓存服务器没有需要的数据时,客户端才会向服务端请求。
又称对比缓存。客户端会先从缓存数据库拿到一个缓存的标识,然后向服务端验证标识是否失效,如果没有失效服务端会返回304,这样客户端可以直接去缓存数据库拿出数据,如果失效,服务端会返回新的数据
强制缓存
对于强制缓存,服务器响应的header中会用两个字段来表明——Expires和Cache-Control。
Expires
Exprires的值为服务端返回的数据到期时间。当再次请求时的请求时间小于返回的此时间,则直接使用缓存数据。但由于服务端时间和客户端时间可能有误差,这也将导致缓存命中的误差,另一方面,Expires是HTTP1.0的产物,故现在大多数使用Cache-Control替代。
Cache-Control
Cache-Control有很多属性,不同的属性代表的意义也不同。
协商缓存
协商缓存需要进行对比判断是否可以使用缓存。浏览器第一次请求数据时,服务器会将缓存标识与数据一起响应给客户端,客户端将它们备份至缓存中。再次请求时,客户端会将缓存中的标识发送给服务器,服务器根据此标识判断。若未失效,返回304状态码,浏览器拿到此状态码就可以直接使用缓存数据了。
对于协商缓存来说,缓存标识我们需要着重理解一下,下面我们将着重介绍它的两种缓存方案。
Last-Modified
Last-Modified:服务器在响应请求时,会告诉浏览器资源的最后修改时间。
从字面上看,就是说:从某个时间节点算起,是否文件被修改了
这两个的区别是一个是修改了才下载一个是没修改才下载。
Last-Modified 说好却也不是特别好,因为如果在服务器上,一个资源被修改了,但其实际内容根本没发生改变,会因为Last-Modified时间匹配不上而返回了整个实体给客户端(即使客户端缓存里有个一模一样的资源)。为了解决这个问题,HTTP1.1推出了Etag。
Etag
Etag:服务器响应请求时,通过此字段告诉浏览器当前资源在服务器生成的唯一标识(生成规则由服务器决定)
但是实际应用中由于Etag的计算是使用算法来得出的,而算法会占用服务端计算的资源,所有服务端的资源都是宝贵的,所以就很少使用Etag了。
缓存的优点
不同刷新的请求执行过程
浏览器地址栏中写入URL,回车
F5
Ctrl+F5
服务器处理请求并返回HTTP报文
它会对TCP连接进行处理,对HTTP协议进行解析,并按照报文格式进一步封装成HTTP Request对象,供上层使用。这一部分工作一般是由Web服务器去进行,我使用过的Web服务器有Tomcat, Nginx和Apache等等 HTTP报文也分成三份,状态码 ,响应报头和响应报文
状态码
状态码是由3位数组成,第一个数字定义了响应的类别,且有五种可能取值:
平时遇到比较常见的状态码有:200, 204, 301, 302, 304, 400, 401, 403, 404, 422, 500
常见状态码区别
200 成功
请求成功,通常服务器提供了需要的资源。
204 无内容
服务器成功处理了请求,但没有返回任何内容。
301 永久移动
请求的网页已永久移动到新位置。 服务器返回此响应(对 GET 或 HEAD 请求的响应)时,会自动将请求者转到新位置。
302 临时移动
服务器目前从不同位置的网页响应请求,但请求者应继续使用原有位置来进行以后的请求。
304 未修改
自从上次请求后,请求的网页未修改过。 服务器返回此响应时,不会返回网页内容。
400 错误请求
服务器不理解请求的语法。
401 未授权
请求要求身份验证。 对于需要登录的网页,服务器可能返回此响应。
403 禁止
服务器拒绝请求。
404 未找到
服务器找不到请求的网页。
422 无法处理
请求格式正确,但是由于含有语义错误,无法响应
500 服务器内部错误
服务器遇到错误,无法完成请求。
响应报头
常见的响应报头字段有: Server, Connection...。
响应报文
你从服务器请求的HTML,CSS,JS文件就放在这里面
就是Webkit解析渲染页面的过程。
这个过程涉及两个比较重要的概念回流和重绘,DOM结点都是以盒模型形式存在,需要浏览器去计算位置和宽度等,这个过程就是回流。等到页面的宽高,大小,颜色等属性确定下来后,浏览器开始绘制内容,这个过程叫做重绘。浏览器刚打开页面一定要经过这两个过程的,但是这个过程非常非常非常消耗性能,所以我们应该尽量减少页面的回流和重绘
这个过程中可能会有dom操作、ajax发起的http网络请求等。
web-socket、ajax等,这个过程通常是为了获取数据
setTimeout、setInterval、Promise等宏任务、微任务队列
当Render Tree中部分或全部元素的尺寸、结构、或某些属性发生改变时,浏览器重新渲染部分或全部文档的过程称为回流。
会导致回流的操作:
一些常用且会导致回流的属性和方法:
当页面中元素样式的改变并不影响它在文档流中的位置时(例如:color、background-color、visibility等),浏览器会将新样式赋予给元素并重新绘制它,这个过程称为重绘。
JS的解析是由浏览器的JS引擎完成的。由于JavaScript是单线程运行,也就是说一个时间只能干一件事,干这件事情时其他事情都有排队,但是有些人物比较耗时(例如IO操作),所以将任务分为同步任务和异步任务,所有的同步任务放在主线程上执行,形成执行栈,而异步任务等待,当执行栈被清空时才去看看异步任务有没有东西要搞,有再提取到主线程执行,这样往复循环(冤冤相报何时了,阿弥陀佛),就形成了Event Loop事件循环,下面来看看大人物
先看一段代码
setTimeout(function(){
console.log('定时器开始啦')
});
new Promise(function(resolve){
console.log('马上执行for循环啦');
for(var i=0; i < 10000; i++){
i==99 && resolve();
}
}).then(function(){
console.log('执行then函数啦')
});
console.log('代码执行结束');
结果我想大家都应该知道。主要来介绍JavaScript的解析,至于Promise等下一节再说
JavaScript是一门单线程语言,尽管H5中提出了Web-Worker,能够模拟实现多线程,但本质上还是单线程,说它是多线程就是扯淡。
既然是单线程,每个事件的执行就要有顺序,比如你去银行取钱,前面的人在进行,后面的就得等待,要是前面的人弄个一两个小时,估计后面的人都疯了,因此,浏览器的JS引擎处理JavaScript时分为同步任务和异步任务
这张图我们可以清楚看到
js引擎存在monitoring process进程,会持续不断的检查主线程执行栈是否为空,一旦为空,就会去Event Queue那里检查是否有等待被调用的函数。 估计看完这些你对事件循环有一定的了解,但是事实上我们看对的没这么简单,通常我们会看到Promise,setTimeout,process.nextTick(),这个时候你和我就懵逼。
除了同步任务和异步任务,我们还分为宏任务和微任务,常见的有以下几种
不同任务会进入不同的任务队列来执行。 JS引擎开始工作后,先在宏任务中开始第一次循环(script里面先执行,不过我喜欢把它拎出来,直接称其进入执行栈),当主线程执行栈全部任务被清空后去微任务看看,如果有等待执行的任务,执行全部的微任务(其实将其回调函数推入执行栈来执行),再去宏任务找最先进入队列的任务执行,执行这个任务后再去主线程执行任务(例如执行```console.log("hello world")这种任务),执行栈被清空后再去微任务,这样往复循环(冤冤相报何时了)
Tip:微任务会全部执行,而宏任务会一个一个来执行
下面来看一段代码
setTimeout(function() {
console.log('setTimeout');
})
new Promise(function(resolve) {
console.log('promise');
resolve();
}).then(function() {
console.log('then');
})
console.log('console');
我们看看它的执行情况
具体的执行过程大致就是这样。
文本标记语言(HTML)是用于在 Internet 上显示 Web 页面的主要标记语言。换句话说,网页由 HTML 组成,用于通过 Web 浏览器显示文本,图像或其他资源。所有 HTML 都是纯文本,这意味着它不是编译的,可以由人类阅读。HTML 文件的文件扩展名为.htm 或.html。
自1990年以来,HTML就一直被用作WWW的信息表示语言,使用HTML语言描述的文件需要通过WWW浏览器显示出效果。HTML是一种建立网页文件的语言,通过标记式的指令(Tag),将影像、声音、图片、文字动画、影视等内容显示出来。事实上,每一个HTML文档都是一种静态的网页文件,这个文件里面包含了HTML指令代码,这些指令代码并不是一种程序语言,只是一种排版网页中资料显示位置的标记结构语言,易学易懂,非常简单。
HTML的普遍应用就是带来了超文本的技术―通过单击鼠标从一个主题跳转到另一个主题,从一个页面跳转到另一个页面,与世界各地主机的文件链接超文本传输协议规定了浏览器在运行HTML文档时所遵循的规则和进行的操作。HTTP协议的制定使浏览器在运行超文本时有了统一的规则和标准。
万维网(world wide web)上的一个超媒体文档称之为一个页面(外语:page)。作为一个组织或者个人在万维网上放置开始点的页面称为主页(外语:Homepage)或首页,主页中通常包括有指向其他相关页面或其他节点的指针(超级链接),所谓超级链接,就是一种统一资源定位器(Uniform Resource Locator,外语缩写:URL)指针,通过激活(点击)它,可使浏览器方便地获取新的网页。这也是HTML获得广泛应用的最重要的原因之一。在逻辑上将视为一个整体的一系列页面的有机集合称为网站(Website或Site)。超级文本标记语言(英文缩写:HTML)是为“网页创建和其它可在网页浏览器中看到的信息”设计的一种标记语言。
当 Web 开发人员构建应用程序时,工作在服务器上执行,原始 HTML 将发送给用户。使用 AJAX 等技术,服务器端开发和客户端开发之间的界限很模糊。
HTML 从未被设计用于当今存在的 Web,因为它只是一种在控制和设计方面具有严重限制的标记语言。已经使用了许多技术来解决这个问题 – 最重要的是层叠样式表(CSS)。
长期解决方案是(或希望是)HTML5,它是下一代 HTML,允许更多的控制和交互性。与 Web 上的任何开发一样,迁移到标准是一个缓慢而艰巨的过程,Web 开发人员和设计人员必须使用当前和支持的技术,这意味着基本 HTML 将继续使用一段时间。
*请认真填写需求信息,我们会在24小时内与您取得联系。