SEO 是网站为了获得更多的流量,对网站的结构及内容进行调整和优化,以便搜索引擎 (百度,google等)更好抓取到网站的内容,提高自已的网站排名。
注意:spider对javascript支持不好,ajax获取的JSON数据无法被spider爬取
服务端渲染又称SSR (Server Side Render):是在服务端完成页面的内容渲染。
浏览器请求页面URL,然后服务器接收到请求之后,到数据库查询数据,将数据丢到后端的页面模板(jsp、Thymeleaf 等)中,并组装成完整的HTML页面,最后返回给浏览器。
特点:
优点:
有利于SEO
缺点:
开发效率低
适用场景:
对SEO有要求的系统,比如:门户首页、商品详情页面等。
客户端渲染又称CSR (Client Side Render):是在客户端(浏览器)完成页面的内容渲染。这是一个前后端分离的渲染模式
浏览器请求URL,前端服务器直接返回一个静态HTML文件,这个HTML文件中加载了很多渲染页面需要的 JavaScript 脚本和 CSS 样式表,浏览器拿到 HTML 文件后开始加载脚本和样式表,并且执行脚本,这个时候脚本请求后端服务提供的API,获取数据,获取完成后将数据通过JavaScript脚本动态的渲染到页面中,完成页面显示。
特点:
缺点:
不利于网站进行SEO,因为网站大量使用javascript技术,不利于搜索引擎抓取网页。
优点:
客户端负责渲染,用户体验性好,服务端只提供数据,不用关心用户界面的内容,有利于提高服务端的开发效率。
适用场景:
对SEO没有要求的系统,比如后台管理类的系统。
首先是浏览器请求URL,前端服务器接收到URL请求之后,根据不同的URL,前端服务器向后端服务器请求数据,请求完成后,前端服务器会组装一个携带了具体数据的HTML文本,并且返回给浏览器,浏览器得到HTML之后开始渲染页面,同时,浏览器加载并执行 JavaScript 脚本,给页面上的元素绑定事件,让页面变得可交互,当用户与浏览器页面进行交互,如跳转到下一个页面时,浏览器会执行 JavaScript 脚本,向后端服务器请求数据,获取完数据之后再次执行 JavaScript 代码动态渲染页面。
移动互联网的兴起促进了web前后端分离开发模式
的发展,服务端只专注业务,前端只专注用户体验,比如流行的vue.js实现了功能强大的前端渲染。 但是,对于有SEO需求的网页
如果使用前端渲染技术去开发就不利于SEO了,有没有一种即使用vue.js 的前端技术也实现服务端渲染的技术
呢?
Nuxt.js 是一个基于 Vue.js 的轻量级应用框架,可以用来创建服务端渲染 (SSR) 应用。
官网网站:https://zh.nuxtjs.org/
解压 guigu-syt-site.zip
资料:资料>用户系统前端>guigu-syt-site.zip
项目默认3000端口启动,如果想要修改Nuxt.js的启动端口,则可以在package.json文件中添加如下配置
"config": { "nuxt": { "host": "127.0.0.1", "port": "3333" } }
npm cache clear --force npm install
npm run dev
layouts目录下default.vue
pages/index.vue,默认使用layouts目录下default.vue布局文件
index.vue中的页面内容会被自动嵌入到模板文件的
在service-util的Knife4jConfig类中添加如下@Bean配置:在Swagger配置中添加front路径相关配置
@Bean public Docket docketFront() { //指定使用Swagger2规范 Docket docket = new Docket(DocumentationType.SWAGGER_2) .apiInfo(new ApiInfoBuilder() .description("尚医通 APIs") .description("本文档描述了尚医通网站系统接口") .contact("admin@atguigu.com") .version("1.0") .build()) //分组名称 .groupName("尚医通网站") .select() .paths(PathSelectors.regex("/front/.*")) .build(); return docket; }
修改spring-security的WebSecurityConfig类,配置授权校验排除front路径
/** * 配置哪些请求不拦截 * 排除swagger相关请求 * @param web * @throws Exception */ @Override public void configure(WebSecurity web) throws Exception { web.ignoring().antMatchers("/api/**","/inner/**","/front/**","/favicon.ico","/swagger-resources/**", "/webjars/**", "/v2/**", "/swagger-ui.html/**", "/doc.html"); }
在service-hosp中创建front包,创建FrontHospitalController
package com.atguigu.syt.hosp.controller.front; @Api(tags = "医院接口") @RestController @RequestMapping("/front/hosp/hospital") public class FrontHospitalController { @Resource private HospitalService hospitalService; @ApiOperation(value = "根据医院名称、级别和区域查询医院列表") @ApiImplicitParams({ @ApiImplicitParam(name = "hosname",value = "医院名称"), @ApiImplicitParam(name = "hostype",value = "医院类型"), @ApiImplicitParam(name = "districtCode",value = "医院地区")}) @GetMapping("/list") public Result<List<Hospital>> list(String hosname, String hostype, String districtCode) { List<Hospital> list = hospitalService.selectList(hosname, hostype, districtCode); return Result.ok(list); } }
接口:HospitalService
/** * 医院列表查询 * @param hosname * @param hostype * @param districtCode * @return */ List<Hospital> selectList(String hosname, String hostype, String districtCode);
实现:HospitalServiceImpl
/** * 根据条件查询医院列表 * @param hosname 医院名称:模糊匹配 * @param hostype:医院级别:精确匹配 * @param districtCode:医院地区:精确匹配 * @return */ @Override public List<Hospital> selectList(String hosname, String hostype, String districtCode) { Sort sort = Sort.by(Sort.Direction.ASC, "hoscode"); //List<Hospital> list = hospitalRepository.findByHosnameLikeAndHostypeAndDistrictCodeAndStatus(hosname, hostype, districtCode, 1, sort); //创建条件匹配器 ExampleMatcher matcher = ExampleMatcher.matching() //构建对象 .withMatcher("hosname", ExampleMatcher.GenericPropertyMatchers.contains()) //模糊查询 .withMatcher("hostype", ExampleMatcher.GenericPropertyMatchers.exact()) //精确查询 .withMatcher("districtCode", ExampleMatcher.GenericPropertyMatchers.exact()); //精确查询 //创建查询对象 Hospital hospital = new Hospital(); hospital.setHosname(hosname); hospital.setHostype(hostype); hospital.setDistrictCode(districtCode); hospital.setStatus(1); //已上线 Example<Hospital> example = Example.of(hospital, matcher); //执行查询 List<Hospital> list = hospitalRepository.findAll(example, sort); //封装医院等级数据 list.forEach(this::packHospital); return list; }
在service-cmn中创建front包,创建FrontDictController
package com.atguigu.syt.cmn.controller.front; @Api(tags = "数据字典接口") @RestController @RequestMapping("/front/cmn/dict") public class FrontDictController { @Resource private DictService dictService; @ApiOperation(value = "根据数据字典类型id获取数据列表") @ApiImplicitParam(name = "dictTypeId", value = "类型id", required = true) @GetMapping(value = "/findDictList/{dictTypeId}") public Result<List<Dict>> findDictList(@PathVariable Long dictTypeId) { List<Dict> list = dictService.findDictListByDictTypeId(dictTypeId); return Result.ok(list); } }
接口:DictService
/** * 根据数据字典类别和字典值获取字典列表 * @param dictTypeId * @return */ List<Dict> findDictListByDictTypeId(Long dictTypeId);
实现:DictServiceImpl
@Override public List<Dict> findDictListByDictTypeId(Long dictTypeId) { LambdaQueryWrapper<Dict> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(Dict::getDictTypeId, dictTypeId); return this.list(queryWrapper); }
在service-cmn,创建FrontRegionController
package com.atguigu.syt.cmn.controller.front; @Api(tags = "地区接口") @RestController @RequestMapping("/front/cmn/region") public class FrontRegionController { @Resource private RegionService regionService; @ApiOperation(value = "根据上级code获取子节点数据列表") @ApiImplicitParam(name = "parentCode", value = "上级节点code", required = true) @GetMapping(value = "/findRegionList/{parentCode}") public Result<List<Region>> findRegionList(@PathVariable String parentCode) { List<Region> list = regionService.findRegionListByParentCode(parentCode); return Result.ok(list); } }
使用之前的service
在api目录中创建hosp.js
import request from '~/utils/request' export default { hospList(searchObj) { return request({ url: `/front/hosp/hospital/list`, method: 'get', params: searchObj }) }, }
在api目录中创建cmn.js
import request from '~/utils/request' export default { dictList(dictTypeId) { return request({ url: `/front/cmn/dict/findDictList/${dictTypeId}`, method: 'get' }) }, regionList(parentCode) { return request({ url: `/front/cmn/region/findRegionList/${parentCode}`, method: 'get' }) } }
pages/index.vue
脚本
<!-- 客户端渲染实例 --> <script> import cmnApi from '~/api/cmn' export default { data() { return { searchObj:{}, //查询对象 hostypeList: [], //医院类型列表 } }, created() { cmnApi.dictList(1).then((response) => { this.hostypeList = response.data }) }, } </script>
页面渲染
<span class="item v-link clickable" v-for="item in hostypeList" :key="item.id"> {{item.name}} </span>
在浏览器中测试首页医院等级列表的显示:浏览器控制台显示如下错误
原因:
浏览器从一个域名的网页去请求另一个域名的资源时,域名、端口、协议任一不同,都是跨域 。
前后端分离开发中,需要考虑ajax跨域的问题。
这里我们可以从服务端解决这个问题:在相关的Controller类上添加注解
@CrossOrigin //跨域
也可以在server-gateway中创建配置文件CorsConfig
package com.atguigu.syt.gateway.config; @Configuration public class CorsConfig { @Bean public CorsWebFilter corsFilter() { CorsConfiguration config = new CorsConfiguration(); config.addAllowedOrigin("*"); //所有源服务器 config.addAllowedHeader("*"); //所有请求头 config.addAllowedMethod("*"); //所有方法:GET、POST、PUT、DElETE config.setAllowCredentials(true);//cookie可跨域 UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(new PathPatternParser()); source.registerCorsConfiguration("/**", config); //所有路径 // cors过滤器 return new CorsWebFilter(source); } }
目前我们已经在网关做了跨域处理,那么service服务就不需要再做跨域处理了,将之前在controller类上添加过@CrossOrigin
标签去掉,并重启对应的微服务
pages/index.vue
脚本
<!-- 服务端渲染实例 --> <script> import cmnApi from '~/api/cmn' export default { asyncData() { return cmnApi.dictList(1).then((response) => { const hostypeList = response.data return { hostypeList } }) }, data() { return { searchObj:{}, //查询对象 } }, } </script>
上面异步调用方式不能同时调用后端的两个接口(例如同时获取级别和地区列表),因此这里我们使用同步调用写法。
使用 async 和 await 关键字
<!-- 服务器端渲染,同步数据获取实例 --> <script> import hospApi from '~/api/hosp' import cmnApi from '~/api/cmn' export default { async asyncData() { //获取等级 const hostypeResponse = await cmnApi.dictList(1) //获取地区 const areaResponse = await cmnApi.regionList('110100') //获取医院 const hospitalResponse = await hospApi.hospList() return { hostypeList: hostypeResponse.data, areaList: areaResponse.data, hospitalList: hospitalResponse.data } }, data() { return { searchObj:{}, //查询对象 } }, } </script>
<template> <div class="home page-component"> <el-carousel indicator-position="outside"> <el-carousel-item> <img src="~assets/images/web-banner1.png" alt="" /> </el-carousel-item> </el-carousel> <!-- 搜索 --> <div class="search-container"> <div class="search-wrapper"> <div class="hospital-search"> <el-input class="search-input" v-model="searchObj.hosname" prefix-icon="el-icon-search" placeholder="输入医院名称" > <span @click="fetchData()" slot="suffix" class="search-btn v-link highlight clickable selected" >搜索 </span> </el-input> </div> </div> </div> <!-- bottom --> <div class="bottom"> <div class="left"> <div class="home-filter-wrapper"> <div class="title">医院</div> <div> <div class="filter-wrapper"> <span class="label">等级:</span> <div class="condition-wrapper"> <span class="item v-link clickable" :class="hostypeActiveIndex == -1 ? 'selected' : ''" @click="hostypeSelect(undefined, -1)" > 全部 </span> <span class="item v-link clickable" :class="hostypeActiveIndex == index ? 'selected' : ''" v-for="(item, index) in hostypeList" :key="item.id" @click="hostypeSelect(item.value, index)" > {{ item.name }} </span> </div> </div> <div class="filter-wrapper"> <span class="label">地区:</span> <div class="condition-wrapper"> <span class="item v-link clickable" :class="areaActiveIndex == -1 ? 'selected' : ''" @click="areaSelect(undefined, -1)" > 全部 </span ><span class="item v-link clickable" :class="areaActiveIndex == index ? 'selected' : ''" v-for="(item, index) in areaList" :key="item.id" @click="areaSelect(item.code, index)" > {{ item.name }} </span> </div> </div> </div> </div> <div class="v-scroll-list hospital-list"> <div v-for="item in hospitalList" :key="item.hoscode" class="v-card clickable list-item" > <div class=""> <div @click="show(item.hoscode)" class="hospital-list-item hos-item" index="0" > <div class="wrapper"> <div class="hospital-title">{{ item.hosname }}</div> <div class="bottom-container"> <div class="icon-wrapper"> <span class="iconfont"></span> {{ item.param.hostypeString }} </div> <div class="icon-wrapper"> <span class="iconfont"></span> 每天{{ item.bookingRule.releaseTime }}放号 </div> </div> </div> <img :src="'data:image/jpeg;base64,' + item.logoData" :alt="item.hosname" class="hospital-img" /> </div> </div> </div> </div> </div> <div class="right"> <div class="common-dept"> <div class="header-wrapper"> <div class="title">常见科室</div> <div class="all-wrapper"> <span>全部</span> <span class="iconfont icon"></span> </div> </div> <div class="content-wrapper"> <span class="item v-link clickable dark">神经内科 </span> <span class="item v-link clickable dark">消化内科 </span> <span class="item v-link clickable dark">呼吸内科 </span> <span class="item v-link clickable dark">内科 </span> <span class="item v-link clickable dark">神经外科 </span> <span class="item v-link clickable dark">妇科 </span> <span class="item v-link clickable dark"> 产科 </span> <span class="item v-link clickable dark">儿科 </span> </div> </div> <div class="space"> <div class="header-wrapper"> <div class="title-wrapper"> <div class="icon-wrapper"> <span class="iconfont title-icon"></span> </div> <span class="title">平台公告</span> </div> <div class="all-wrapper"> <span>全部</span> <span class="iconfont icon"></span> </div> </div> <div class="content-wrapper"> <div class="notice-wrapper"> <div class="point"></div> <span class="notice v-link clickable dark" >关于延长北京大学国际医院放假的通知 </span> </div> <div class="notice-wrapper"> <div class="point"></div> <span class="notice v-link clickable dark" >北京中医药大学东方医院部分科室医生门诊医 </span> </div> <div class="notice-wrapper"> <div class="point"></div> <span class="notice v-link clickable dark"> 武警总医院号源暂停更新通知 </span> </div> </div> </div> <div class="suspend-notice-list space"> <div class="header-wrapper"> <div class="title-wrapper"> <div class="icon-wrapper"> <span class="iconfont title-icon"></span> </div> <span class="title">停诊公告</span> </div> <div class="all-wrapper"> <span>全部</span> <span class="iconfont icon"></span> </div> </div> <div class="content-wrapper"> <div class="notice-wrapper"> <div class="point"></div> <span class="notice v-link clickable dark"> 中国人民解放军总医院第六医学中心(原海军总医院)呼吸内科门诊停诊公告 </span> </div> <div class="notice-wrapper"> <div class="point"></div> <span class="notice v-link clickable dark"> 首都医科大学附属北京潞河医院老年医学科门诊停诊公告 </span> </div> <div class="notice-wrapper"> <div class="point"></div> <span class="notice v-link clickable dark" >中日友好医院中西医结合心内科门诊停诊公告 </span> </div> </div> </div> </div> </div> </div> </template> <!-- 服务器端渲染,异步数据获取实例 --> <script> import hospApi from '~/api/hosp' import cmnApi from '~/api/cmn' export default { async asyncData() { //获取等级 const hostypeResponse = await cmnApi.dictList(1) //获取地区 const areaResponse = await cmnApi.regionList('110100') //获取医院 const hospitalResponse = await hospApi.hospList() return { hostypeList: hostypeResponse.data, areaList: areaResponse.data, hospitalList: hospitalResponse.data } }, data() { return { hostypeActiveIndex: -1, //类别高亮索引 areaActiveIndex: -1, //地区高亮索引 searchObj: {}, //查询条件 } }, methods: { //根据医院等级查询 hostypeSelect(hostype, index) { this.hostypeActiveIndex = index this.searchObj.hostype = hostype this.fetchData() }, //根据地区查询 areaSelect(districtCode, index) { this.areaActiveIndex = index this.searchObj.districtCode = districtCode this.fetchData() }, //查询医院数据 fetchData() { hospApi.hospList(this.searchObj).then((response) => { this.hospitalList = response.data }) }, //点击某个医院名称,跳转到详情页面中 show(hoscode) { window.location.href = '/hospital/' + hoscode }, }, } </script>
在service-hosp的FrontHospitalController添加方法
@ApiOperation(value = "医院预约挂号详情") @GetMapping("/show/{hoscode}") public Result<Hospital> show(@PathVariable String hoscode) { Hospital hospital = hospitalService.show(hoscode); return Result.ok(hospital); }
使用之前的service
在service-hosp中创建FrontDepartmentController
package com.atguigu.syt.hosp.controller.front; @Api(tags = "科室管理") @RestController @RequestMapping("/front/hosp/department") public class FrontDepartmentController { @Resource private DepartmentService departmentService; @ApiOperation(value = "查询医院所有科室列表") @ApiImplicitParam(name = "hoscode",value = "医院编码", required = true) @GetMapping("/getDeptList/{hoscode}") public Result<List<DepartmentVo>> getDeptList(@PathVariable String hoscode) { List<DepartmentVo> list = departmentService.findDeptTree(hoscode); return Result.ok(list); } }
使用之前的service
创建 /pages/hospital/_hoscode.vue组件:
<template> <div> 详情 {{$route.params.hoscode}} </div> </template>
动态路由:
如果我们需要根据id查询一条记录,就需要使用动态路由。NUXT的动态路由是以下划线开头的vue文件,参数名为下划线后的文件名
在浏览器中输入http://localhost:3000/hospital/10000,可以看到医院编码被动态的显示在了页面中
hosp.js中添加方法
//根据医院编号显示医院详情 show(hoscode) { return request({ url: `/front/hosp/hospital/show/${hoscode}`, method: 'get' }) }, //根据医院编号显示所有科室 getDeptList(hoscode) { return request({ url: `/front/hosp/department/getDeptList/${hoscode}`, method: 'get' }) }
<template> <div class="nav-container page-component"> <!--左侧导航 #start --> <div class="nav left-nav"> <div class="nav-item selected"> <span class="v-link selected dark">预约挂号</span> </div> <div class="nav-item"> <span class="v-link clickable dark">医院详情</span> </div> <div class="nav-item"> <span class="v-link clickable dark">预约须知</span> </div> <div class="nav-item"> <span class="v-link clickable dark">停诊信息</span> </div> <div class="nav-item"> <span class="v-link clickable dark">查询/取消</span> </div> </div> <!-- 左侧导航 #end --> <!-- 右侧内容 #start --> <div class="page-container"> <div class="hospital-home"> <div class="common-header"> <div class="title-wrapper"> <span class="hospital-title">{{ hospital.hosname }}</span> <div class="icon-wrapper"> <span class="iconfont"></span>{{ hospital.param.hostypeString }} </div> </div> </div> <div class="info-wrapper"> <img class="hospital-img" :src="'data:image/jpeg;base64,' + hospital.logoData" :alt="hospital.hosname" /> <div class="content-wrapper"> <div>挂号规则</div> <div class="line"> <div> <span class="label">预约周期:</span ><span>{{ bookingRule.cycle }}天</span> </div> <div class="space"> <span class="label">放号时间:</span ><span>{{ bookingRule.releaseTime }}</span> </div> <div class="space"> <span class="label">停挂时间:</span ><span>{{ bookingRule.stopTime }}</span> </div> </div> <div class="line"> <span class="label">退号时间:</span> <span v-if="bookingRule.quitDay == -1" >就诊前一工作日{{ bookingRule.quitTime }}前取消</span > <span v-if="bookingRule.quitDay == 0" >就诊前当天{{ bookingRule.quitTime }}前取消</span > </div> <div style="margin-top: 20px">医院预约规则</div> <div class="rule-wrapper"> <ol> <li v-for="item in bookingRule.rule" :key="item"> {{ item }} </li> </ol> </div> </div> </div> <div class="title select-title">选择科室</div> <div class="select-dept-wrapper"> <div class="department-wrapper"> <div class="hospital-department"> <div class="dept-list-wrapper el-scrollbar" style="height: 100%"> <div class="dept-list el-scrollbar__wrap" style="margin-bottom: -17px; margin-right: -17px" > <div class="el-scrollbar__view"> <div class="sub-item" v-for="(item, index) in departmentList" :key="item.id" :class="index == activeIndex ? 'selected' : ''" @click="move(index)" > {{ item.depname }} </div> </div> </div> </div> </div> </div> <div class="sub-dept-container"> <div v-for="(item, index) in departmentList" :key="item.id" v-show="index == activeIndex" class="sub-dept-wrapper" :id="item.depcode" > <div class="sub-title"> <div class="block selected"></div> {{ item.depname }} </div> <div class="sub-item-wrapper"> <div v-for="it in item.children" :key="it.id" class="sub-item" @click="schedule(it.depcode)" > <span class="v-link clickable">{{ it.depname }} </span> </div> </div> </div> </div> </div> </div> </div> <!-- 右侧内容 #end --> </div> </template> <script> import '~/assets/css/hospital_personal.css' import '~/assets/css/hospital.css' import hospApi from '~/api/hosp' export default { async asyncData(page) { const hoscode = page.route.params.hoscode //获取医院详情 const hospitalResponse = await hospApi.show(hoscode) //获取所有科室 const departmentResponse = await hospApi.getDeptList(hoscode) return { hospital: hospitalResponse.data, bookingRule: hospitalResponse.data.bookingRule, departmentList: departmentResponse.data, } }, data () { return { activeIndex: 0 } }, methods: { move(index){ this.activeIndex = index }, schedule(depcode) { window.location.href = '/hospital/schedule?hoscode=' + this.$route.params.hoscode + "&depcode="+ depcode } } } </script>
源码下载地址:https://gitee.com/dengyaojava/guigu-syt-parent