九、定制媒体播放器并改进 SEO

用户访问媒体流应用主要是为了播放媒体和探索其他相关媒体。这使得媒体播放器和呈现相关媒体细节的视图对于流式应用至关重要。

在本章中,我们将重点为在上一章中开始构建的 MERN Mediastream 应用开发播放媒体页面。我们将讨论以下主题,以增强媒体播放功能,并帮助提升媒体内容在网络上的表现力,从而接触到更多用户:

  • 自定义ReactPlayer上的控件
  • 播放相关视频列表中的下一个
  • 自动播放相关媒体的列表
  • 服务器端使用数据呈现媒体视图以改进 SEO

带有自定义媒体播放器的 MERN Mediastream

上一章中开发的 MERN Mediastream 应用实现了一个简单的媒体播放器,带有默认的浏览器控件,一次播放一个视频。在本章中,我们将使用定制的ReactPlayer和相关媒体列表更新播放媒体的视图,该列表可设置为在当前视频结束时自动播放。使用定制播放器和相关播放列表更新的视图将如此屏幕截图所示:

The code for the complete MERN Mediastream application is available on GitHub at github.com/shamahoque/mern-mediastream. You can clone this code and run the application as you go through the code explanations in the rest of this chapter. 

以下组件树图显示了构成 MERN Mediastream 前端的所有自定义组件,突出显示了本章将改进或添加的组件:

本章中添加的新组件包括MediaPlayer组件和RelatedMedia组件,前者添加了带有自定义控件的ReactPlayer,后者包含相关视频列表。

播放媒体页面

当访问者想要在 MERN Mediastream 上查看特定媒体时,他们将被带到“播放媒体”页面,该页面将包含媒体详细信息、用于播放视频的媒体播放器以及下一步可以播放的相关媒体列表。

组件结构

我们将以一种允许媒体数据从父组件向下流到内部组件的方式在播放媒体页面中组成组件结构。在这种情况下,PlayMedia组件将是父组件,包含RelatedMedia组件,以及包含嵌套MediaPlayer组件的Media组件:

当访问单个媒体链接时,PlayMedia组件将从服务器装载并检索媒体数据和相关媒体列表。然后,相关数据将作为道具传递给MediaRelatedMedia子组件。

RelatedMedia组件将链接到其他相关媒体的列表,单击每个将使用新数据重新呈现PlayMedia组件和内部组件。

我们将更新第 8 章构建流媒体应用中开发的Media组件,添加定制的媒体播放器作为子组件。这个定制的MediaPlayer组件还将利用PlayMedia传递的数据流传输当前视频,并链接到相关媒体列表中的下一个视频。

PlayMedia组件中,我们将添加一个自动播放切换,允许用户选择一个接一个地自动播放相关媒体列表中的视频。自动播放状态将通过PlayMedia组件进行管理,但此功能将要求处于状态的数据在视频结束于MediaPlayer时重新呈现,而MediaPlayer是嵌套的子组件,因此下一个视频可以在跟踪相关列表的同时自动开始播放。

为了实现这一点,PlayMedia组件需要提供一个状态更新方法作为道具,该方法将在MediaPlayer组件中用于更新这些组件之间共享和相互依赖的状态值。

考虑到这个组件结构,我们将扩展和更新 MERN Mediastream 应用,以实现功能性的播放媒体页面。

相关媒体列表

相关媒体列表将包括与给定视频属于同一类型的其他媒体记录,并按最高浏览次数排序。

相关列表 API

为了从数据库中检索相关媒体列表,我们将在服务器上设置一个 API,该 API 将在'/api/media/related/:mediaId'接收 GET 请求。

mern-mediastream/server/routes/media.routes.js

router.route('/api/media/related/:mediaId')
        .get(mediaCtrl.listRelated)

listRelated控制器方法将查询媒体集合以查找与所提供媒体具有相同类型的记录,并将此媒体记录从返回的结果中排除。返回的结果将按最大视图数排序,并限制为前四个媒体记录。返回结果中的每个media对象还将包含发布媒体的用户的姓名和 ID。

mern-mediastream/server/controllers/media.controller.js

const listRelated = (req, res) => {
  Media.find({ "_id": { "$ne": req.media },
  "genre": req.media.genre}).limit(4)
  .sort('-views')
  .populate('postedBy', '_id name')
  .exec((err, posts) => {
    if (err) {
      return res.status(400).json({
        error: errorHandler.getErrorMessage(err)
      })
    }
    res.json(posts)
  })
}

在客户端,我们将设置相应的fetch方法,该方法将在PlayMedia组件中使用,以检索使用此 API 的相关媒体列表。

mern-mediastream/client/media/api-media.js

const listRelated = (params) => {
  return fetch('/api/media/related/'+ params.mediaId, {
    method: 'GET',
    headers: {
      'Accept': 'application/json',
      'Content-Type': 'application/json'
    }
  }).then(response => {
    return response.json() 
  }).catch((err) => console.log(err)) 
}

相关媒体组件

RelatedMedia组件将相关媒体列表作为PlayMedia组件的道具,并呈现列表中每个视频的详细信息以及视频快照。

我们使用map函数遍历媒体列表以呈现每个媒体项。

mern-mediastream/client/media/RelatedMedia.js

{this.props.media.map((item, i) => { 
    return 
      <span key={i}>... video snapshot ... | ... media details ...</span> 
  })
}

为了显示视频快照,我们将使用一个不带控件的基本ReactPlayer

mern-mediastream/client/media/RelatedMedia.js

<Link to={"/media/"+item._id}>
  <ReactPlayer url={'/api/media/video/'+item._id} width='160px'    
  height='140px'/>
</Link>

单击快照将重新呈现 PlayMedia 视图,以加载链接的媒体详细信息:

在快照旁边,我们将显示每个视频的详细信息,包括标题、流派、创建日期和视图数。

mern-mediastream/client/media/RelatedMedia.js

<Typography type="title" color="primary">{item.title}</Typography>
<Typography type="subheading"> {item.genre} </Typography>
<Typography component="p">
        {(new Date(item.created)).toDateString()}
</Typography>
<Typography type="subheading">{item.views} views</Typography>

要在视图中使用这个RelatedMedia组件,我们将把它添加到PlayMedia组件中。

PlayMedia 组件

PlayMedia组件由MediaRelatedMedia子组件以及自动播放切换组成,在视图中加载时向这些组件提供数据。为了在用户访问单个媒体链接时呈现PlayMedia组件,我们将在MainRouter中添加一个Route以在'/media/:mediaId'处挂载PlayMedia

mern-mediastream/client/MainRouter.js

<Route path="/media/:mediaId" component={PlayMedia}/>

PlayMedia组件挂载时,它将根据路由链路中的media ID参数,使用loadMedia功能从服务器获取媒体数据和相关媒体列表。

mern-mediastream/client/media/PlayMedia.js

loadMedia = (mediaId) => {
    read({mediaId: mediaId}).then((data) => {
      if (data.error) {
        this.setState({error: data.error})
      } else {
        this.setState({media: data})
          listRelated({
            mediaId: data._id}).then((data) => {
            if (data.error) {
              console.log(data.error)
            } else {
              this.setState({relatedMedia: data})
            }
          })
      }
    })
  }

loadMedia函数使用媒体 ID 和readAPIfetch方法从服务器检索媒体详细信息。然后,它使用listRelatedAPI fetch 方法从服务器检索相关媒体列表,并将值设置为 state。

当组件安装和接收道具时,使用mediaId值调用loadMedia函数。

mern-mediastream/client/media/PlayMedia.js

componentDidMount = () => {
    this.loadMedia(this.match.params.mediaId)
}
componentWillReceiveProps = (props) => {
    this.loadMedia(props.match.params.mediaId)
}

要在组件挂载时访问路由 URL 中的mediaId参数,我们需要访问组件构造函数中的 react routermatch对象。

mern-mediastream/client/media/PlayMedia.js

constructor({match}) {
    super() 
    this.state = {
      media: {postedBy: {}},
      relatedMedia: [],
      autoPlay: false,
    } 
    this.match = match 
}

组件状态中存储的媒体和相关媒体列表值用于将相关道具传递给视图中添加的子组件。例如,RelatedMedia组件仅在相关媒体列表包含任何项目时才会呈现,并作为道具传递给列表。

mern-mediastream/client/media/PlayMedia.js

{this.state.relatedMedia.length > 0 && 
      (<RelatedMedia media={this.state.relatedMedia}/>)}

在本章后面的自动播放相关媒体部分中,只有当相关媒体列表的长度大于零时,我们才会在RelatedMedia组件上方添加自动播放切换组件。我们还将讨论将作为道具传递给Media组件的handleAutoPlay方法的实现,以及媒体详细信息对象,以及相关媒体列表中第一个媒体的视频 URL 作为下一个要播放的 URL。

mern-mediastream/client/media/PlayMedia.js

const nextUrl = this.state.relatedMedia.length > 0
          ? `/media/${this.state.relatedMedia[0]._id}` : ''
<Media media={this.state.media} 
       nextUrl={nextUrl} 
       handleAutoplay={this.handleAutoplay}/>

Media组件呈现媒体细节,同时也是一个媒体播放器,允许观众控制视频流。

媒体播放器

我们将自定义ReactPlayer上的播放器控件,以自定义外观和功能替换默认浏览器控件,如此屏幕截图所示:

这些控件将添加到视频下方,包括进度搜索栏、播放、暂停、下一步、音量、循环和全屏选项,以及显示播放的持续时间。

更新媒体组件

我们将创建一个新的MediaPlayer组件,该组件将包含定制的ReactPlayer。在Media组件中,我们将用新的MediaPlayer组件替换之前使用的ReactPlayer,并传递视频源 URL、下一个视频的 URL 和handleAutoPlay方法,它们作为propsPlayMedia组件接收。

mern-mediastream/client/media/Media.js

const mediaUrl = this.props.media._id
          ? `/api/media/video/${this.props.media._id}`
          : null
...
<MediaPlayer srcUrl={mediaUrl} 
             nextUrl={this.props.nextUrl} 
             handleAutoplay={this.props.handleAutoplay}/>

正在初始化媒体播放器

MediaPlayer组件将包含ReactPlayer组件,在添加自定义控件和处理代码之前,从初始控件值开始。

首先,我们将初始控制值设置为state

mern-mediastream/client/media/MediaPlayer.js

state = {
      playing: true,
      volume: 0.8,
      muted: false,
      played: 0,
      loaded: 0,
      duration: 0,
      ended:false,
      playbackRate: 1.0,
      loop: false,
      fullscreen: false,
      videoError: false
} 

在视图中,我们将使用从Media组件发送的道具,使用控制值和源 URL 添加ReactPlayer

mern-mediastream/client/media/MediaPlayer.js

const { playing, ended, volume, muted, loop, played, loaded, duration, playbackRate, fullscreen, videoError } = this.state
...
  <ReactPlayer
     ref={this.ref}
     width={fullscreen ? '100%':'inherit'}
     height={fullscreen ? '100%':'inherit'}
     style={fullscreen ? {position:'relative'} : {maxHeight: '500px'}}
     config={{ attributes: { style: { height: '100%', width: '100%'} } }}
     url={this.props.srcUrl}
     playing={playing}
     loop={loop}
     playbackRate={playbackRate}
     volume={volume}
     muted={muted}
     onEnded={this.onEnded}
     onError={this.videoError}
     onProgress={this.onProgress}
     onDuration={this.onDuration}/>

我们将获得该播放器的一个引用,因此可以在自定义控件的更改处理代码中使用它。

mern-mediastream/client/media/MediaPlayer.js

ref = player => {
      this.player = player
}

如果无法加载源视频,我们将捕获错误。

mern-mediastream/client/media/MediaPlayer.js

videoError = e => {
  this.setState({videoError: true}) 
}

然后,我们将有条件地在视图中显示一条错误消息。

mern-mediastream/client/media/MediaPlayer.js

{videoError && <p className={classes.videoError}>Video Error. Try again later.</p>}

自定义媒体控件

我们将在视频下方添加自定义播放器控制元素,并使用ReactPlayerAPI 提供的选项和事件操作它们的功能

播放、暂停和重播

用户将能够播放、暂停和重放当前视频,我们将使用绑定到ReactPlayer属性和事件的Material-UI组件实现这三个选项:

为了实现播放、暂停和重播功能,我们将根据视频是正在播放、暂停还是已结束,有条件地添加播放、暂停或重播图标按钮。

mern-mediastream/client/media/MediaPlayer.js

<IconButton color="primary" onClick={this.playPause}>
    <Icon>{playing ? 'pause': (ended ? 'replay' : 'play_arrow')}</Icon>
</IconButton>

当用户点击按钮时,我们将更新状态中的播放值,从而更新ReactPlayer

mern-mediastream/client/media/MediaPlayer.js

playPause = () => {
     this.setState({ playing: !this.state.playing })
}

下一场比赛

用户可以使用“下一步”按钮播放相关媒体列表中的下一个视频:

如果相关列表不包含任何介质,则“下一步”按钮将被禁用。“播放下一个”图标基本上会链接到作为道具从PlayMedia传入的下一个 URL 值。

mern-mediastream/client/media/MediaPlayer.js

<IconButton disabled={!this.props.nextUrl} color="primary">
    <Link to={this.props.nextUrl}>
       <Icon>skip_next</Icon>
    </Link>
</IconButton>

点击此next按钮将重新加载包含新媒体详细信息的PlayMedia组件,并开始播放视频。

循环结束

用户还可以使用“循环”按钮将当前视频设置为在循环中继续播放:

我们将设置一个循环图标按钮,该按钮将以不同的颜色呈现,以指示它是已设置还是未设置。

mern-mediastream/client/media/MediaPlayer.js

<IconButton color={loop? 'primary' : 'default'} 
            onClick={this.onLoop}>
    <Icon>loop</Icon>
</IconButton>

点击循环图标按钮时,更新状态中的loop值。

mern-mediastream/client/media/MediaPlayer.js

onLoop = () => {
   this.setState({ loop: !this.state.loop })
}

我们需要捕获onEnded事件,检查loop是否已设置为 true,以便playing值可以相应地更新。

mern-mediastream/client/media/MediaPlayer.js

onEnded = () => {
    if(this.state.loop){
      this.setState({ playing: true})
    }else{
      this.setState({ ended: true, playing: false })
    }
}

因此,如果loop设置为 true,当视频结束时,它将再次开始播放,否则它将停止播放并呈现 replay 按钮。

音量控制

为了控制正在播放的视频的音量,用户可以选择增大或减小音量,以及静音或取消静音。渲染的卷控件将根据用户操作和卷的当前值进行更新:

  • 如果音量增大,则会呈现音量增大图标:

  • 如果用户将音量减小到零,将呈现音量关闭图标:

  • 如果用户单击图标使音量静音,将显示音量静音图标按钮:

为了实现这一点,我们将根据volumemutedvolume_upvolume_off值有条件地在IconButton中呈现不同的图标:

<IconButton color="primary" onClick={this.toggleMuted}>
    <Icon> {volume > 0 && !muted && 'volume_up' || 
            muted && 'volume_off' || 
               volume==0 && 'volume_mute'} </Icon>
</IconButton>

单击此音量按钮时,它将使音量静音或取消静音。

mern-mediastream/client/media/MediaPlayer.js

toggleMuted = () => {
    this.setState({ muted: !this.state.muted })
}

为了允许用户增加或减少音量,我们将添加一个input range,允许用户在01之间设置音量值。

mern-mediastream/client/media/MediaPlayer.js

<input type="range" 
       min={0} 
       max={1} 
       step='any' 
       value={muted? 0 : volume} 
       onChange={this.setVolume}/>

更改输入范围上的value将相应地设置volume值。

mern-mediastream/client/media/MediaPlayer.js

  setVolume = e => {
    this.setState({ volume: parseFloat(e.target.value) })
  }

进度控制

我们将使用 Material UILinearProgress组件来指示缓冲了多少视频以及播放了多少视频。然后,我们将此组件与range input相结合,使用户能够将时间滑块移动到视频的不同部分并从那里播放:

LinearProgress组件将采用playedloaded值以不同的颜色显示:

<LinearProgress color="primary" variant="buffer" 
                value={played*100} valueBuffer={loaded*100} 
                style={{width: '100%'}} 
                classes={{ colorPrimary: classes.primaryColor,
                           dashedColorPrimary: classes.primaryDashed,
                           dashed: {animation: 'none'} }}
/>

要在视频播放或加载时更新LinearProgress组件,我们将使用onProgress事件侦听器设置playedloaded的当前值。

mern-mediastream/client/media/MediaPlayer.js

onProgress = progress => {
    if (!this.state.seeking) {
      this.setState({played: progress.played, loaded: progress.loaded})
    }
}

对于时间滑动控制,我们将添加range input元素,并使用 CSS 样式将其放置在LinearProgress组件上。范围的当前值将随着played值的变化而更新,因此范围值似乎随着视频的进展而移动。

mern-mediastream/client/media/MediaPlayer.js

<input type="range" min={0} max={1}
       value={played} step='any'
       onMouseDown={this.onSeekMouseDown}
       onChange={this.onSeekChange}
       onMouseUp={this.onSeekMouseUp}
       style={{ position: 'absolute',
                width: '100%',
                top: '-7px',
                zIndex: '999',
                '-webkit-appearance': 'none',
                backgroundColor: 'rgba(0,0,0,0)' }}
/>

如果用户自己拖动并设置范围选择器,我们将添加代码来处理onMouseDownonMouseUponChange事件,以便从所需位置启动视频。

当用户按住鼠标开始拖动时,我们会将 seeking 设置为 true,这样进度值就不会设置为playedloaded

mern-mediastream/client/media/MediaPlayer.js

onSeekMouseDown = e => {
    this.setState({ seeking: true })
}

当范围值发生变化时,在检查用户是否将时间滑块拖到视频末尾后,我们将设置played值和ended值。

mern-mediastream/client/media/MediaPlayer.js

onSeekChange = e => {
  this.setState({ played: parseFloat(e.target.value), 
                    ended: parseFloat(e.target.value) >= 1 })
}

当用户完成拖动并抬起鼠标时,我们将seeking设置为false,并将播放器的seekTo值设置为range input中的当前值。

mern-mediastream/client/media/MediaPlayer.js

onSeekMouseUp = e => {
  this.setState({ seeking: false })
  this.player.seekTo(parseFloat(e.target.value))
}

这样,用户将能够选择视频的任何部分,还可以获得视频流传输时间进度的视觉信息。

全屏

用户可以通过单击控件中的全屏按钮全屏查看视频:

为了实现视频的全屏选项,我们将使用screenfullnpm 模块跟踪视图何时处于全屏状态,并使用react-dom中的findDOMNode指定哪个 DOM 元素将与screenfull一起全屏显示。

要设置fullscreen代码,我们首先安装screenfull

npm install screenfull --save

然后将screenfullfindDOMNode导入MediaPlayer组件。

mern-mediastream/client/media/MediaPlayer.js

import screenfull from 'screenfull'
import { findDOMNode } from 'react-dom'

MediaPlayer组件挂载时,我们将添加一个screenfull更改事件监听器,该监听器将更新状态中的fullscreen值,以指示屏幕是否处于全屏状态。

mern-mediastream/client/media/MediaPlayer.js

componentDidMount = () => {
  if (screenfull.enabled) {
     screenfull.on('change', () => {
         let fullscreen = screenfull.isFullscreen ? true : false 
         this.setState({fullscreen: fullscreen}) 
     }) 
  }
}

在视图中,我们将为fullscreen添加一个icon按钮和其他控制按钮。

mern-mediastream/client/media/MediaPlayer.js

<IconButton color="primary" onClick={this.onClickFullscreen}>
  <Icon>fullscreen</Icon>
</IconButton>

当用户点击此按钮时,我们将使用screenfullfindDOMNode使视频播放器全屏显示。

mern-mediastream/client/media/MediaPlayer.js

onClickFullscreen = () => {
   screenfull.request(findDOMNode(this.player))
}

然后,用户可以全屏观看视频,随时按Esc退出全屏并返回 PlayMedia 视图。

播放时长

在媒体播放器的“自定义媒体控制”部分中,我们希望以可读的时间格式显示已经过去的时间以及视频的总持续时间:

为了显示时间,我们可以使用 HTMLtime元素。

mern-mediastream/client/media/MediaPlayer.js

<time dateTime={`P${Math.round(duration * played)}S`}>
      {this.format(duration * played)}
</time> / 
<time dateTime={`P${Math.round(duration)}S`}>
    {this.format(duration)}
</time>

我们将使用onDuration事件获取视频的duration值,然后将其设置为状态,这样就可以在时间元素中渲染视频。

mern-mediastream/client/media/MediaPlayer.js

onDuration = (duration) => {
    this.setState({ duration })
}

为了使持续时间值可读,我们将使用以下format函数。

mern-mediastream/client/media/MediaPlayer.js

format = (seconds) => {
  const date = new Date(seconds * 1000)
  const hh = date.getUTCHours()
  let mm = date.getUTCMinutes()
  const ss = ('0' + date.getUTCSeconds()).slice(-2)
  if (hh) {
    mm = ('0' + date.getUTCMinutes()).slice(-2) 
    return `${hh}:${mm}:${ss}`
  }
  return `${mm}:${ss}`
}

format函数以秒为单位获取持续时间值,并将其转换为hh/mm/ss格式。

添加到自定义媒体播放器中的控件主要基于ReactPlayer模块中的一些可用功能,以及作为文档提供的示例。有更多选项可用于进一步的定制和扩展,这可能会根据特定的功能需求进行更多的探索。

自动播放相关媒体

我们将通过在PlayMedia中添加切换来完成前面讨论的自动播放功能,并在MediaPlayer组件中实现handleAutoplay方法,视频结束时需要调用该方法。

切换自动播放

除了允许用户设置自动播放外,切换还将指示当前是否设置了自动播放:

对于自动播放切换,我们将使用Material-UI``Switch组件和FormControlLabel组件,并将其添加到RelatedMedia组件上方的PlayMedia组件中,仅当相关媒体列表中有媒体时,才会进行渲染。

mern-mediastream/client/media/PlayMedia.js

<FormControlLabel 
    control={
            <Switch
              checked={this.state.autoPlay}
              onChange={this.handleChange}
              color="primary"
            />
          }
    label={this.state.autoPlay? 'Autoplay ON':'Autoplay OFF'}
/>

为了处理对切换的更改并将其反映在状态的autoplay值中,我们将使用以下onChange处理函数。

mern-mediastream/client/media/PlayMedia.js

handleChange = (event) => {
   this.setState({ autoPlay: event.target.checked }) 
} 

处理跨组件的自动播放

PlayMediahandleAutoPlay方法传递给Media组件,作为视频结束时MediaPlayer组件使用的道具。

此处所需的功能是,当视频结束时,如果 autoplay 设置为 true 且当前相关媒体列表不为空,PlayMedia应加载相关列表中第一个视频的媒体详细信息。反过来,MediaMediaPlayer组件应使用新媒体详细信息进行更新,开始播放新视频,并适当渲染播放器上的控件。RelatedMedia组件中的列表也应该随着从列表中删除的当前媒体而更新,因此只有剩余的播放列表项可见。

mern-mediastream/client/media/PlayMedia.js

handleAutoplay = (updateMediaControls) => {
    let playList = this.state.relatedMedia
    let playMedia = playList[0]

    if(!this.state.autoPlay || playList.length == 0 )
      return updateMediaControls()

    if(playList.length > 1){
      playList.shift()
      this.setState({media: playMedia, relatedMedia:playList})
    }else{
      listRelated({
          mediaId: playMedia._id}).then((data) => {
            if (data.error) {
             console.log(data.error)
            } else {
             this.setState({media: playMedia, relatedMedia: data})
            }
         })
    }
  }

当视频在MediaPlayer组件中结束时,handleAutoplay方法处理以下事项:

  • 它从MediaPlayer组件中的onEnded事件侦听器获取回调函数。如果未设置自动播放或相关媒体列表为空,则将执行此回调,以便呈现MediaPlayer上的控件以显示视频已结束。
  • 如果设置了自动播放,并且列表中有多个相关媒体,则:

    • 相关媒体列表中的第一项被设置为处于状态的当前媒体对象,以便可以对其进行渲染
    • 删除第一个现在将开始在视图中播放的项目,即可更新相关媒体列表
  • 如果设置了自动播放且相关媒体列表中只有一个项目,则最后一个项目将设置为“媒体”,以便开始播放,并调用listRelatedfetch 方法以使用最后一个项目的相关媒体重新填充 RelatedMedia 视图。

在 MediaPlayer 中视频结束时更新状态

MediaPlayerPlayMedia接收handleAutoplay方法作为道具。只有当当前视频的loop设置为false时,我们才会更新onEnded事件的侦听器代码以执行此方法。

mern-mediastream/client/media/MediaPlayer.js

onEnded = () => {
  if(this.state.loop){
    this.setState({ playing: true})
  }else{
    this.props.handleAutoplay(() => {
                              this.setState({ ended: true, 
                                                playing: false })
                            }) 
    }
}

PlayMedia中确定未设置自动播放或相关媒体列表为空后,向handleAutoplay方法传递回调函数,以将播放设置为 false,并呈现回放图标按钮,而不是播放或暂停图标按钮。

自动播放功能将通过此实现一个接一个地继续播放相关视频。此实现演示了当值相互依赖时跨组件更新状态的另一种方法

使用数据进行服务器端渲染

搜索引擎优化对于任何向用户交付内容并希望使内容易于查找的 web 应用都很重要。一般来说,任何网页上的内容都有更好的机会获得更多的观众,如果这些内容很容易被搜索引擎阅读的话。当搜索引擎机器人访问 web URL 时,它将获得服务器端呈现的输出。因此,要使内容可发现,内容应该是服务器端呈现输出的一部分。

在 MERN Mediastream 中,我们将使用使媒体详细信息在搜索引擎结果中流行的案例来演示如何将数据注入 MERN 应用中的服务器端渲染视图。我们将着重于通过为返回到'/media/:mediaId'路径的PlayMedia组件注入数据来实现服务器端渲染。这里概述的一般步骤可用于使用其他视图的数据实现 SSR。

路由配置

为了在服务器上呈现 React 视图时加载这些视图的数据,我们将使用 React 路由配置 npm 模块,该模块为 React 路由提供静态路由配置帮助:

npm install react-router-config --save

我们将创建一个路由配置文件,用于将路由与服务器上的传入请求 URL 相匹配,以检查在服务器返回呈现的标记之前是否必须注入数据。

对于 MERN Mediastream 中的路由配置,我们只列出呈现PlayMedia组件的路由。

mern-mediastream/client/routeConfig.js

import PlayMedia from './media/PlayMedia' 
import { read } from './media/api-media.js' 
const routes = [
  {
    path: '/media/:mediaId',
    component: PlayMedia,
    loadData: (params) => read(params)
  }
]
export default routes 

对于该路由和组件,我们将从api-media.js指定read获取方法作为负载数据方法。然后,当服务器生成标记时,它将用于检索数据并将数据注入 PlayMedia 视图。

正在更新 Express 服务器的 SSR 代码

我们将更新server/express.js中现有的基本服务器端渲染代码,为将渲染服务器端的 React 视图添加数据加载功能。

使用路由配置加载数据

我们将定义loadBranchData来使用react-router-config中的matchRoutes,以及路由配置文件中定义的路由来查找与传入请求 URL 匹配的路由。

mern-mediastream/server/express.js

import { matchRoutes } from 'react-router-config' 
import routes from './../client/routeConfig' 
const loadBranchData = (location) => {
  const branch = matchRoutes(routes, location) 
  const promises = branch.map(({ route, match }) => {
    return route.loadData
      ? route.loadData(branch[0].match.params)
      : Promise.resolve(null)
  })
  return Promise.all(promises)
}

如果找到匹配的路由,则将执行任何相关的loadData方法,以返回包含获取数据的Promise,如果没有loadData方法,则返回null

每当服务器收到请求时,这里定义的loadBranchData都需要调用,因此如果找到匹配的路由,我们可以在呈现服务器端时获取相关数据并将其注入 React 组件。

同构提取

我们还将在express.js中导入同构 fetch,以便readfetch 方法或我们为客户机定义的任何其他 fetch 现在可以在服务器上使用。

mern-mediastream/server/express.js

import 'isomorphic-fetch'

绝对网址

使用isomorphic-fetch的一个问题是,它当前要求获取 URL 是绝对的。因此,我们需要将api-media.js中定义的read获取方法中使用的 URL 更新为绝对 URL。

我们将在config.js中设置一个config变量,而不是在代码中硬编码服务器地址。

mern-mediastream/config/config.js

serverUrl: process.env.serverUrl || 'http://localhost:3000'

然后我们将更新api-media.js中的read方法,使其使用绝对 URL 调用服务器上的读取 API。

mern-mediastream/client/media/api-media.js:

import config from '../../config/config'
const read = (params) => {
  return fetch(config.serverUrl +'/api/media/' + params.mediaId, {
    method: 'GET'
  }).then((response) => { ... })

这将使readfetch 调用与isomorphic-fetch兼容,因此它可以在服务器上正常使用。

将数据注入 React 应用

在后端现有的服务器端渲染代码中,我们使用ReactDOMServer将 React 应用转换为标记。我们将在express.js中更新此代码,在使用loadBranchData方法获取MainRouter后,将数据作为道具注入MainRouter

mern-mediastream/server/express.js

...
loadBranchData(req.url).then(data => {
    const markup = ReactDOMServer.renderToString(
      <StaticRouter location={req.url} context={context}>
        <JssProvider registry={sheetsRegistry}
      generateClassName={generateClassName}>
      <MuiThemeProvider theme={theme} sheetsManager={new Map()}>
        < MainRouter data={data}/>
      </MuiThemeProvider>
    </JssProvider>
      </StaticRouter>
    ) 
...
}).catch(err => {
 res.status(500).send("Data could not load") 
 }) 
...

当服务器生成标记时,要将损坏的数据添加到渲染的 To.T0:Up 组件中,我们需要更新客户端代码来考虑服务器注入的数据。

在客户端代码中应用服务器注入的数据

在客户端,我们将访问从服务器传递的数据,并将其添加到 PlayMedia 视图中。

将数据道具从主路由传递到 PlayMedia

在使用ReactDOMServer.renderToString生成标记时,我们将预加载的数据作为道具传递给MainRouter。我们可以在MainRouter的构造函数中访问该数据属性。

mern-mediastream/client/MainRouter.js

  constructor({data}) {
    super() 
      this.data = data 
  }

为了允许PlayMedia访问此数据,我们将更改PlayMediaRoute组件,以将此数据作为道具传递。

mern-mediastream/client/MainRouter.js

<Route path="/media/:mediaId" 
       render={(props) => (
          <PlayMedia {...props} data={this.data} />
        )} />

在 PlayMedia 中呈现接收到的数据

PlayMedia组件中,我们将检查从服务器传递的数据,并将值设置为 state,以便在视图中呈现媒体详细信息。

mern-mediastream/client/media/PlayMedia.js

...
render() {
    if (this.props.data && this.props.data[0] != null) {
      this.state.media = this.props.data[0] 
      this.state.relatedMedia = [] 
    }
...
}

这将使用 PlayMedia 视图中注入的媒体数据生成服务器生成的标记。

用数据检查 SSR 的实施

对于 MERN Mediastream,呈现 PlayMedia 的任何链接现在都应该在服务器端生成标记,并预加载媒体详细信息。我们可以通过在关闭 JavaScript 的浏览器中打开应用 URL 来验证服务器端呈现数据的实现是否正常工作。我们将研究如何在 Chrome 浏览器中实现这一点,以及生成的视图应该向用户和搜索引擎显示什么。

铬试验

在 Chrome 中测试这个实现只需要更新 Chrome 设置,并在 JS 被阻止的选项卡中加载应用。

加载启用 JS 的页面

首先,在 Chrome 中打开应用,然后浏览到任何媒体链接,让它在启用 JavaScript 的情况下正常呈现。这将显示已实现的 PlayMedia 视图,其中包含功能正常的媒体播放器和相关媒体列表。

从设置中禁用 JS

接下来,在 Chrome 上禁用 JavaScript。为此,您可以转到chrome://settings/content/javascript的高级设置,并使用切换来阻止 JavaScript:

现在,刷新 MERN Mediastream 选项卡中的媒体链接,地址 URL 旁边会有一个图标,显示 JavaScript 确实被禁用:

阻止 JS 的播放媒体视图

PlayMedia 视图的渲染应与下图类似,仅填充媒体详细信息。但用户界面不再是交互式的,因为 JavaScript 被阻止,只有默认的浏览器控件可操作:

这是搜索引擎机器人将读取的媒体内容,以及当浏览器上没有加载 JavaScript 时用户将看到的内容。

MERN Mediastream 现在拥有完全可操作的媒体播放工具,允许用户轻松浏览和播放视频。此外,由于使用预加载数据进行服务器端渲染,显示单个媒体内容的媒体视图现在已通过搜索引擎优化。

总结

在本章中,我们通过使用ReactPlayer提供的选项添加自定义媒体播放器控件,完全升级了 MERN Mediastream 上的播放媒体页面从数据库检索相关媒体后,启用相关媒体播放列表的自动播放功能,并在服务器上呈现视图时,通过从服务器注入数据使媒体详细信息搜索引擎可读。

现在,我们已经利用 MERN stack 技术探索了流媒体和 SEO 等高级功能,在接下来的章节中,我们将通过将虚拟现实元素合并到 web 应用中,进一步测试该堆栈的潜力。