1、服务端渲染技术Nuxt
什么是服务端渲染
服务端渲染又称SSR(Server Side Render) 是在服务端完成页面的内容,而不是在客户通过AJAX获取数据,vue就是通过ajax获取数据,不利于SEO。
SSR的优势主要在与SEO,因为搜索引擎爬虫抓取工具可以直接查看完全渲染的页面。
如果你的应用初始展示loading,然后通过ajax获取数据内容,抓取工具不会等待异步抓取完成后再进行页面内容的抓取。所以,如果SEO对你的站点至关重要,而你的页面又是异步获取内容,则你可能需要服务端渲染SSR解决此问题。
另外,使用SSR,我们可以获取更快的内容到达时间(time to content),无需等待所有的Javascript下载完成并执行,这样会产生更好的用户体验。对于那些time to content 与转化率直接相关的应用程序来说,SSR至关重要。
什么是Nuxt
Nuxt.js是一个基于Vue.js的轻量级应用框架,可用来创建服务端渲染(SSR)应用,也可以充当静态站点引擎生成静态站点应用,具有优雅的代码结构分层和热加载等特性。
Nuxt环境初始化
1、下载压缩包
https://github.com/nuxt-community/starter-template/archive/master.zip
2、解压,将template复制到项目中,修改为edu-web作为前台项目
3、vscode打开项目,修改package.json中的name、description、author
"name": "jude-edu-web",
"version": "1.0.0",
"description": "艾编程前台",
"author": "jude<747463168@qq.com>",
4、配置ESLint,将web-admin项目下的eslintrc.js复制到当前项目下替换掉,保持一致使用vue-element-admin的esint配置
5、修改nuxt.config.js,如下
head: {
title: '艾编程,为每个互联网人提供高质量终身学习平台',
meta: [
{ charset: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
{ hid:'keywords', name: 'keywords',content: '艾编程'},
{ hid: 'description', name: 'description', content: '艾编程是在大数据人工智能高速发展的今天成立的一家以提供各行业商业项目研发解决方案为核心的在线教育学习平台' }
],
link: [
{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }
]
},
6、安装依赖
npm install
运行
npm run dev
Nuxt目录结构
1、资源目录assets
静态资源如less, css,sass,javascritp
2、组件目录components
3、布局目录layouts
4、页面目录pages
用于组织应用的路由及视图,Nuxt.js框架会读取该目录下的所有.vue文件并自动生成对应的路由配置。
5、插件目录plugins
用于组织那些需要在根据vue.js应用实例化之前需要运行的javascript插件
6、nuxt.config.js文件
用于组织Nuxt.js应用的个性化配置,以便覆盖默认配置,下面举一些常用的配置
-
head
可以在这个配置项中配置全局的
head
,如定义网站的标题、meta
,引入第三方的 CSS、JavaScript 文件等head: { title: '百姓店铺', meta: [ { charset: 'utf-8' }, { name: 'viewport', content: 'width=device-width, initial-scale=1' }, { name: 'applicable-device', content: 'pc,mobile' }, ], link: [ { rel: 'icon', type: 'image/x-icon', href: '/logoicon.ico' }//地址栏小图标的引入 { rel: 'stylesheet', type: 'text/css', href: 'https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css'}, ], script: [ {src: 'https://code.jquery.com/jquery-3.1.1.min.js'}, {src: 'https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js'} ] },
-
build
这个配置项用来配置 Nuxt.js 项目的构建规则,即 Webpack 的构建配置,如通过 vendor 字段引入第三方模块,通过 plugin 字段配置 Webpack 插件,通过 loaders 字段自定义 Webpack 加载器等。通常我们会在 build 的 vendor 字段中引入 axios 模块,从而在项目中进行 HTTP 请求( axios 也是 Vue.js 官方推荐的 HTTP 请求框架)
build: { vendor: ['core-js', 'axios'], loaders: [ { test: /\.(scss|sass)$/, use: [{ loader: "style-loader" }, { loader: "css-loader" }, { loader: "sass-loader" }] }, { test: /\.(png|jpe?g|gif|svg)$/, loader: 'url-loader', query: { limit: 1000, name: 'img/[name].[hash:7].[ext]' } }, { test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, loader: 'url-loader', query: { limit: 1000, name: 'fonts/[name].[hash:7].[ext]' } } ] }
-
css
在这个配置项中,引入全局的 CSS 文件,之后每个页面都会被引入。
css: [ //该配置项用于定义应用的全局(所有页面均需引用的)样式文件、模块或第三方库。 'element-ui/lib/theme-chalk/index.css',//在创建项目的时候安装了elememt插件,这里自动引入插件的默认样式 '@/assets/css/reset.css', //引入assets下的reset.css全局标签重置样式 '@/assets/main.css' //引入全局的动画样式 ],
-
router
可以在此配置路由的基本规则,以及进行中间件的配置。例如,你可以创建一个用来获取
User-Agent
的中间件,并在此加载。 -
loading
Nuxt.js 提供了一套页面内加载进度指示组件,可以在此配置颜色,禁用,或是配置自定义的加载组件。
-
env: 可以在此配置用来在服务端和客户端共享的全局变量。
目录即路由
Nuxt.js 在 vue-router
之上定义了一套自动化的生成规则,例如,我们有以下目录结构:
这个目录下含有一个基础路由(无参数)以及两个动态路由(带参数),Nuxt.js 会生成如下的路由配置表(可以在 .nuxt
目录下的 router.js
文件中找到):
routes: [
{
path: "/",
component: _abe13a78,
name: "index"
},
{
path: "/article/:id?",
component: _48f202f2,
name: "article-id"
},
{
path: "/:page",
component: _5ccbb43a,
name: "page"
}
]
参考: https://blog.csdn.net/muzidigbig/article/details/84955246
EDU项目整合nuxtjs
1) 页面布局 layouts
2) 2页面 pages
3) 路由 pages 下面的路径,所有的页面使用index.vue ,路由映射 / pages/index.vue /teacher pages/teacher/index.vue
4) 使用插件 ,将插件导入,编写到 plugins 下面,最终导入到 nuxt.config.js (核心配置文件)
5) 幻灯片使用 (1、导入页面 2、编写vue对应的值)
6) 后台数据(编写前端的controller)
7) 前端页面使用axios获取后端数据,前端页面渲染!
8) 后端编写请求 根据id查询讲师信息
9) 前端格式处理 (teacher/id) nuxt 中,动态的路由,都使用下划线开头的 vue文件, 参数渲染 teacher/id pages/teacher/_id.vue
10) 完整具体的页面样式
11) 课程页面编写 ( 逻辑同上!)
12) 扩展布局(layout) 编辑 布局页面,放到 layouts 目录下 编写组件,使用自己编写的 layouts布局
2、首页和路由
页面布局
1、复制静态资源
将静态原型中的css、img、photo复制到assets目录下
将网站的logo 图标 favicon.ico复制到static目录下
2、定义布局
修改layouts目录下的default.vue,添加公共头部header与底部footer
<template>
<div>
<!-- 公共头 -->
<!-- 内容区域 -->
<nuxt/>
<!-- 公共底部 -->
</div>
</template>
导航栏
<!-- 导航栏 -->
<ul class="nav">
<!-- router-link = a连接 to href -->
<!--约定 默认会解析page下的页面 / pages/index.vue 自动映射的-->
<router-link to="/" tag="li" active-class="current" exact>
<a>首页</a>
</router-link>
<!-- 默认会解析page下的页面 / pages/course/index.vue -->
<router-link to="/course" tag="li" active-class="current">
<a>课程</a>
</router-link>
<router-link to="/teacher" tag="li" active-class="current">
<a>名师</a>
</router-link>
<router-link to="/article" tag="li" active-class="current">
<a>文章</a>
</router-link>
<router-link to="/qa" tag="li" active-class="current">
<a>问答</a>
</router-link>
</ul>
定义首页面
安装幻灯片插件
npm install vue-awesome-swiper@3.1.3 --save
官网:https://www.npmjs.com/package/vue-awesome-swiper
最新版本4.1.1,我们使用3.1.3
install完后,在plugins文件夹下新建next-swiper-plugin.js,内容如下
import Vue from 'vue'
import VueAwesomeSwiper from 'vue-awesome-swiper/dist/ssr'
// 使用插件
Vue.use(VueAwesomeSwiper)
nuxt.config.js文件配置插件
module.exports = {
build: {
...
},
plugins: [
{src: '~/plugins/nuxt-swiper-plugin.js',ssr: false}
],
css: [
'swiper/dist/css/swiper.css'
]
}
首页内容
<template>
<div>
<!-- 轮播图 -->
<!-- 课程信息 -->
<!-- 讲师信息 -->
</div>
</template>
添加轮播图代码
<div v-swiper:mySwiper="swiperOption">
<div class="swiper-wrapper">
<!-- 图片信息 -->
<div class="swiper-slide" style="background: #040B1B;">
<a target="_blank" href="/">
<img src="~/assets/photo/banner/1525939573202.jpg" alt="首页banner">
</a>
</div>
<div class="swiper-slide" style="background: #040B1B;">
<a target="_blank" href="/">
<img src="~/assets/photo/banner/1.jpg" alt="首页banner">
</a>
</div>
</div>
<div slot="pagination" class="swiper-pagination swiper-pagination-white "/>
...
<script>
export default {
data() {
return {
swiperOption: {
// 配置分页
pagination: {
el: '.swiper-pagination',
clickable: true
},
autoplay: {
delay: 5000
},
loop: true
},
</script>
测试:
3、讲师列表页
安装axios
npm install axios
新建utils/request.js对axios进行简单的封装
import axios from 'axios'
const service = axios.create({
baseURL: 'http://localhost:8210', // api 的 base_url
timeout: 20000 // 请求超时时间
})
export default service
在pages创建teacher/index.vue
<template>
<div>
讲师列表
<div v-for="item in data.items" :key = "item.id">
<a :href="'/teacher/' + item.id"></a>
</div>
</div>
</template>
<script>
import teacher from 'api/teacher'
export default {
// asyncData 渲染组件前获取数据
asyncData({ params, error }) {
return teacher.getPageList(1,8).then(response => {
console.log(response.data.data)
// 返回数据
return { data: response.data.data }
})
}
}
</script>
前端api/teacher.js
import request from '@/utils/request'
const api_name = '/edu/teacher'
export default {
getPageList(page, limit) {
return request({
url: `${api_name}/${page}/${limit}`,
method: 'get'
})
},
getById(id) {
return request({
url: `${api_name}/${id}`,
method: 'get'
})
}
}
后端api返回数据的代码在此不细说,主要是理解使用Nuxt渲染前端页面的步骤。
最终效果
4、讲师详情页
我们根据ID查询单个老师的信息,需要使用动态路由,Nuxt的动态路由是以下划线开头的vue文件,参数名为下划后边的文件名。
在pages/teacher目录下新建_id.vue,那么id就是要传递的参数,在vue页面里使用params.id获取
// 讲师介绍
<template>
<div>
讲师介绍
</br>
</br>
</br>
</div>
</template>
<script>
import teacher from 'api/teacher'
// asyncData 渲染组件前获取数据
asyncData({ params, error }) {
return teacher.getById(params.id).then(response => {
// 返回数据
return { item: response.data.data.teacher }
})
}
</script>
web层接口定义TeacherController
@ApiOperation(value = "根据id查询讲师")
@GetMapping("{id}")
public R getById(@ApiParam(name = "id",value = "讲师id",required = true) @PathVariable String id){
Teacher teacher = teacherService.getById(id);
// 查询老师的课程信息
List<Course> courseList = courseService.list(new QueryWrapper<Course>()
.eq("teacher_id",id).orderByDesc("gmt_modified"));
return R.ok().data("teacher",teacher).data("courseList",courseList);
}
最终效果
5、课程列表页
参考讲师列表页
6、课程详情页
后端API
定义展示对象
@ApiModel(value = "课程信息",description = "网站课程详情页需要的相关字段")
@Data
public class CourseWebVo implements Serializable {
private static final long serialVersionUID = -5122459757537929907L;
private String id;
@ApiModelProperty(value = "课程标题")
private String title;
@ApiModelProperty(value = "课程销售价格,设置为0则可免费观看")
private BigDecimal price;
@ApiModelProperty(value = "总课时")
private Integer lessonNum;
@ApiModelProperty(value = "课程封面图片路径")
private String cover;
@ApiModelProperty(value = "销售数量")
private Long buyCount;
@ApiModelProperty(value = "浏览数量")
private Long viewCount;
@ApiModelProperty(value = "课程简介")
private String description;
@ApiModelProperty(value = "课程讲师ID")
private String teacherId;
@ApiModelProperty(value = "讲师姓名")
private String teacherName;
@ApiModelProperty(value = "讲师资历,一句话说明讲师")
private String intro;
@ApiModelProperty(value = "讲师头像")
private String avatar;
@ApiModelProperty(value = "课程类别ID")
private String subjectLevelOneId;
@ApiModelProperty(value = "类别名称")
private String subjectLevelOne;
@ApiModelProperty(value = "课程类别ID")
private String subjectLevelTwoId;
@ApiModelProperty(value = "类别名称")
private String subjectLevelTwo;
}
字段涉及到多张表:课程表、讲师表、课程类别表,需要自定义sql查询语句
在CourseMapper.xml定义sql:
<select id="getCourseWebById" resultType="com.jude.edu.vo.CourseWebVo">
select
c.id,
c.title,
c.cover,
CONVERT(c.price, DECIMAL(8,2)) as price,
c.lesson_num as lessonNum,
c.buy_count as buyCount,
c.view_count as viewCount,
cd.description,
t.id as teacherId,
t.name as teacherName,
t.intro,
t.avatar,
s1.id as subjectLevelOneId,
s1.title as subjectLevelOne,
s2.id as subjectLevelTwoId,
s2.title as subjectLevelTwo
from
edu_course c
left join edu_course_description cd on c.id = cd.id
left join edu_teacher t on c.teacher_id = t.id
left join edu_subject s1 on c.subject_parent_id =s1.id
left join edu_subject s2 on c.subject_id = s2.id
where
c.id = #{courseId}
</select>
数据层接口CourseMapper
CourseWebVo getCourseWebById(String courseId);
业务层接口CourseService
CourseWebVo getCourseWebById(String courseId);
接口实现类 CourseServiceImpl.java
@Override
public CourseWebVo getCourseWebById(String courseId) {
// 课程浏览量 + 1
Course course = this.getById(courseId);
course.setViewCount(course.getViewCount() + 1);
this.updateById(course);
// redis (先走缓存再走数据!)
return baseMapper.getCourseWebById(courseId);
}
web层接口的定义 CourseController.java
@ApiOperation(value = "根据id查询课程")
@GetMapping(value = "{courseId}")
public R getById(
@ApiParam(name = "courseId",value = "课程id",required = true)
@PathVariable String courseId){
// 课程和讲师等信息
CourseWebVo courseWebVo = courseService.getCourseWebById(courseId);
// 课程对应的章节视频信息
List<ChapterVo> chapterVoList = chapterService.nestedList(courseId);
return R.ok().data("course",courseWebVo).data("chapterVoList",chapterVoList);
}
前台页面
api/course.js
import request from '@/utils/request'
const api_name = '/edu/course'
export default {
getPageList(page, limit) {
return request({
url: `${api_name}/${page}/${limit}`,
method: 'get'
})
},
getById(courseId) {
return request({
url: `${api_name}/${courseId}`,
method: 'get'
})
}
}
在pages/teacher目录下新建_id.vue
<template>
<section class="container">
<!-- 课程所属分类-->
<section class="path-wrap txtOf hLh30">
...
</section>
<!-- 课程基本信息 -->
<div>
...
</div>
<!-- 详情 -->
<div class="mt20 c-infor-box">
<!-- 课程大纲章节 -->
<article class="fl col-7">
...
</article>
<!-- 主讲讲师-->
<aside class="fl col-3">
<div class="i-box">
...
</div>
</aside>
</div>
</section>
</template>
测试效果
7、文章
参考艾编程官网
列表页
详情页
这个可以做为作业,后续自己实现
8、视频点播集成到前端
后端获取播放凭证
业务层接口VideoService
// 获取播放凭证
String getVideoPlayAuth(String videoId);
接口实现类 VideoServiceImpl.java
@Override
public String getVideoPlayAuth(String videoId) {
DefaultAcsClient client = null;
try{
client = AliyunVodSDKUtils.initVodClient(ConstantPropertiesUtils.ACCESS_KEY_ID,ConstantPropertiesUtils.ACCESS_KEY_SECRET);
// 请求
GetVideoPlayAuthRequest request = new GetVideoPlayAuthRequest();
// 响应
GetVideoPlayAuthResponse response = new GetVideoPlayAuthResponse();
// 设置请求参数,视频ID
request.setVideoId(videoId);
// 获取请求的响应
response = client.getAcsResponse(request);
return response.getPlayAuth();
}catch (ClientException e){
throw new JudeException(ResultCodeEnum.FETCH_PLAYAUTH_ERROR);
}
}
web层接口的定义 VideoController.java
@GetMapping("get-play-auth/{videoId}")
public R getVideoPlayAuth(@PathVariable("videoId") String videoId) {
///得到播放凭证,最重要的
String playAuth = videoService.getVideoPlayAuth(videoId);
//返回结果
return R.ok().message("获取凭证成功").data("playAuth", playAuth);
}
}
前端播放器整合
1、点击播放超链接
修改课时目录超链接
<li v-for="video in chapter.children" :key="video.id" class="lh-menu-second ml30">
<!-- 最终跳转到视频播放页面 -->
<a
:href="'/player/'+video.videoSourceId"
:title="video.title"
target="_blank">
<span v-if="video.free === true" class="fr">
<i class="free-icon vam mr10">免费试听</i>
</span>
<em class="lh-menu-i-2 icon16 mr5"> </em>
</a>
</li>
因为播放器的布局和其他页面的基本布局不一致,因此创建新的布局容器layouts/video.vue
<template>
<div class="coding-player">
<div class="head">
<a href="#" title="艾编程">
<img class="logo" src="~/assets/img/logo.png" lt="艾编程">
</a></div>
<div class="body">
<div class="content">
<!-- 内容 -->
<nuxt/>
</div>
</div>
</div>
</template>
<script>
export default {}
</script>
<style>
html,body{
height:100%;
}
</style>
<style scoped>
.head {
height: 50px;
position: absolute;
top: 0;
left: 0;
width: 100%;
}
.head .logo{
height: 50px;
margin-left: 10px;
}
.body {
position: absolute;
top: 50px;
left: 0;
right: 0;
bottom: 0;
overflow: hidden;
}
</style>
3、创建api模块api/vod.js,从后端获取播放凭证
import request from '@/utils/request'
const api_name = '/vod/video'
export default {
getPlayAuth(videoId) {
return request({
url: `${api_name}/get-play-auth/${videoId}`,
method: 'get'
})
}
}
4、创建播放页面
在pages/player目录下新建_vid.vue,cans 名就是vid
引入播放器js库和css
<template>
<div>
<!-- 阿里云视频播放器样式 -->
<link rel="stylesheet" href="https://g.alicdn.com/de/prismplayer/2.8.7/skins/default/aliplayer-min.css" >
<!-- 启用私有加密的防调式:生产环境使用 -->
<script src="https://g.alicdn.com/de/prismplayer/2.8.0/hls/aliplayer-vod-anti-min.js" />
<!-- 阿里云视频播放器脚本 -->
<script type="text/javascript" charset="utf-8" src="https://g.alicdn.com/de/prismplayer/2.8.7/aliplayer-min.js" />
<!-- 阿里云视频播放器组件 -->
<script type="text/javascript" charset="utf-8" src="https://player.alicdn.com/aliplayer/presentation/js/aliplayercomponents.min.js"/>
<!-- 播放的div -->
<!-- 定义播放器dom -->
<div id="J_prismPlayer" class="prism-player" />
</div>
</template>
<script>
import vod from '@/api/vod'
// eslint-disable-next-line no-unused-vars
var videoAdClose = function(videoAd) {
/* 调用视频广告组件的暂停事件来暂停广告 */
videoAd.pauseVideoAd()
var result = confirm('确定开通会员关闭广告吗?')
if (result) {
/* 调用视频广告组件关闭事件来关闭广告! */
videoAd.closeVideoAd()
} else {
/* 调用视频广告组件的播放事件来播放广告 */
videoAd.playVideoAd()
}
}
// eslint-disable-next-line no-unused-vars
var danmukuList = [
{ mode: 1, text: '哈哈', stime: 1000, size: 25, color: 0xffffff },
{ mode: 1, text: '前方高能', stime: 2000, size: 25, color: 0xffffff },
{ mode: 1, text: '灵魂歌手', stime: 30000, size: 25, color: 0xffffff },
{ mode: 1, text: '这是弹幕2', stime: 1000, size: 25, color: 0x00c1de },
{ mode: 1, text: '神测试', stime: 1000, size: 25, color: 0x00c1de },
{ mode: 1, text: '顺手一划', stime: 1000, size: 25, color: 0x00c1de },
{ mode: 1, text: '哈哈', stime: 1000, size: 25, color: 0xffffff },
{ mode: 1, text: '哈哈', stime: 1000, size: 25, color: 0xffffff },
{ mode: 1, text: '哈哈', stime: 1000, size: 25, color: 0xffffff },
{ mode: 1, text: '哈哈', stime: 1000, size: 25, color: 0xffffff }
]
export default {
/* 关闭广告的自定义事件, 可自行修改代码从而满足不同的功能, 参数为视频广告组件本身 */
layout: 'video', // 应用video布局
// 1、获取播放凭证
asyncData({ params, error }) {
return vod.getPlayAuth(params.vid).then(response => {
// console.log(response.data.data)
return {
// 数据是固定的!拿到视频信息 和 播放凭证,就可以播放视频了!
vid: params.vid,
playAuth: response.data.data.playAuth
}
})
},
/**
* 页面渲染完成时:此时js脚本已加载,Aliplayer已定义,可以使用
* 如果在created生命周期函数中使用,Aliplayer is not defined错误
*/
mounted() {
/* eslint-disable no-undef */
// 2、创建播放器
const player = new Aliplayer({
id: 'J_prismPlayer',
vid: this.vid, // 视频id
playauth: this.playAuth, // 播放凭证
encryptType: '1', // 如果播放加密视频,则需设置encryptType=1,非加密视频无需设置此项
width: '100%',
height: '500px',
'extraInfo': {
'crossOrigin': 'anonymous'
},
'skinLayout': [
{ 'name': 'bigPlayButton', 'align': 'blabs', 'x': 30, 'y': 80 },
{ 'name': 'H5Loading', 'align': 'cc' },
{ 'name': 'errorDisplay', 'align': 'tlabs', 'x': 0, 'y': 0 },
{ 'name': 'infoDisplay' },
{ 'name': 'tooltip', 'align': 'blabs', 'x': 0, 'y': 56 },
{ 'name': 'thumbnail' },
{
'name': 'controlBar', 'align': 'blabs', 'x': 0, 'y': 0,
'children': [
{ 'name': 'progress', 'align': 'blabs', 'x': 0, 'y': 44 },
{ 'name': 'playButton', 'align': 'tl', 'x': 15, 'y': 12 },
{ 'name': 'timeDisplay', 'align': 'tl', 'x': 10, 'y': 7 },
{ 'name': 'fullScreenButton', 'align': 'tr', 'x': 10, 'y': 12 },
{ 'name': 'subtitle', 'align': 'tr', 'x': 15, 'y': 12 },
{ 'name': 'setting', 'align': 'tr', 'x': 15, 'y': 12 },
{ 'name': 'volume', 'align': 'tr', 'x': 5, 'y': 10 },
{ 'name': 'snapshot', 'align': 'tr', 'x': 10, 'y': 12 }
]
}
],
components: [{
name: 'AliplayerDanmuComponent', // 弹幕组件
type: AliPlayerComponent.AliplayerDanmuComponent,
args: [danmukuList]
}]
}, function(player) {
console.log('播放器创建成功')
})
/* h5截图按钮, 截图成功回调 */
player.on('snapshoted', function(data) {
var pictureData = data.paramData.base64
var downloadElement = document.createElement('a')
downloadElement.setAttribute('href', pictureData)
var fileName = 'Aliplayer' + Date.now() + '.png'
downloadElement.setAttribute('download', fileName)
downloadElement.click()
pictureData = null
})
}
}
</script>