用于 Gatsby / React SSR 构建的 JS 对象的条件解构
Conditional Destructuring of a JS Object for Gatsby / React SSR Build
以下组件 AudioPlayer
- 基于 react-media-player
在 Gatsby/React 开发环境中运行良好。但是将它构建到 React SSR 中真是太难了。
AudioPlayer
依赖 window
对象进行实例化,Node.js 不可用。因此,我不得不使用 Gatsby 的自定义 Webpack 配置来检测媒体播放器并将空加载器放入混合中。效果很好:
exports.onCreateWebpackConfig = ({ stage, loaders, actions }) => {
if (stage === "build-html") {
actions.setWebpackConfig({
module: {
rules: [
{
test: /react-media-player/,
use: loaders.null(),
},
],
},
})
}
}
但是当你尝试通过 Webpack 构建时,我得到了错误:
WebpackError: TypeError: Cannot destructure property CurrentTime
of
'undefined' or 'null'.
这是有道理的,因为我们刚刚删除了包含 CurrentTime
的 react-media-player
。那么如何确保 Webpack 不会尝试解构此组件中的 controls
对象? (ES6中的对我来说没有意义,所以慢慢解释):
import React, { Component } from 'react'
import { Media, Player, controls } from 'react-media-player'
const { CurrentTime, SeekBar, Duration, Volume, PlayPause, MuteUnmute } = controls
let panner = null
class AudioPlayer extends Component {
componentDidMount() {
const audioContext = new (window.AudioContext || window.webkitAudioContext)()
panner = audioContext.createPanner()
panner.setPosition(0, 0, 1)
panner.panningModel = 'equalpower'
panner.connect(audioContext.destination)
const source = audioContext.createMediaElementSource(this._player.instance)
source.connect(panner)
panner.connect(audioContext.destination)
}
_handlePannerChange = ({ target }) => {
const x = +target.value
const y = 0
const z = 1 - Math.abs(x)
panner.setPosition(x, y, z)
}
render() {
return (
<div>
{ typeof window !== 'undefined' && Media &&
<Media>
<div>
<Player
ref={c => this._player = c}
src={this.props.src}
useAudioObject
/>
<section className="media-controls">
<div className="media-title-box">
<PlayPause className="media-control media-control--play-pause"/>
<div className="media-title-content">
<div className="media-title">{ this.props.mediaTitle }</div>
<div className="media-subtitle">{ this.props.mediaSubtitle }</div>
</div>
</div>
<div className="media-controls-container">
<CurrentTime className="media-control media-control--current-time"/>
<SeekBar className="media-control media-control--volume-range"/>
<Duration className="media-control media-control--duration"/>
</div>
<div className="media-sound-controls">
<MuteUnmute className="media-control media-control--mute-unmute"/>
<Volume className="media-control media-control--volume"/>
</div>
</section>
</div>
</Media>
}
</div>
)
}
}
export default AudioPlayer
对于未来的 google 员工/那些感兴趣的人来说,这个问题的最终解决方案需要在我这边跳过很多障碍。我将介绍我完成的所有事情,希望这对其他人有帮助:
AudioPlayer
组件问题
我认为 react-media-player
使用 SSR 特别困难的原因是因为它是组件的集合,而不仅仅是您放入的组件。对于我的构建,我正在创建一个音频播放器,因此包含组件:Media
、Player
、CurrentTime
、SeekBar
、Duration
、Volume
来自 [=16] =] 库和其他组件 PlayPause
和 MuteUnmute
。
我的自定义 AudioPlayer
组件需要三个 { typeof window !== 'undefined'}
检查才能使我的构建工作并将 panner
和 audioContext
变量重构为 componentDidMount()
.请注意,第一个 window
检查查找 media
而不是 react-media-player
,因为 react-media-player
永远不会在渲染函数中被调用。
AudioPlayer.js
import React, { Component } from 'react'
import { Media, Player, controls } from 'react-media-player'
import PlayPause from '../../components/AudioPlayerPlayPause'
import MuteUnmute from '../../components/AudioPlayerMuteUnmute'
const { CurrentTime, SeekBar, Duration, Volume } = controls
let panner = null
class AudioPlayer extends Component {
componentDidMount() {
const audioContext = new (window.AudioContext || window.webkitAudioContext)()
panner = audioContext.createPanner()
panner.setPosition(0, 0, 1)
panner.panningModel = 'equalpower'
panner.connect(audioContext.destination)
const source = audioContext.createMediaElementSource(this._player.instance)
source.connect(panner)
panner.connect(audioContext.destination)
}
_handlePannerChange = ({ target }) => {
const x = +target.value
const y = 0
const z = 1 - Math.abs(x)
panner.setPosition(x, y, z)
}
render() {
return (
<div>
{ typeof window !== 'undefined' && Media &&
<Media>
<div>
<Player
ref={c => this._player = c}
src={this.props.src}
useAudioObject
/>
<section className="media-controls">
<div className="media-title-box">
{ typeof window !== 'undefined' && PlayPause &&
<PlayPause className="media-control media-control--play-pause"/>
}
<div className="media-title-content">
<div className="media-title">{ this.props.mediaTitle }</div>
<div className="media-subtitle">{ this.props.mediaSubtitle }</div>
</div>
</div>
<div className="media-controls-container">
<CurrentTime className="media-control media-control--current-time"/>
<SeekBar className="media-control media-control--volume-range"/>
<Duration className="media-control media-control--duration"/>
</div>
<div className="media-sound-controls">
{ typeof window !== 'undefined' && MuteUnmute &&
<MuteUnmute className="media-control media-control--mute-unmute"/>
}
<Volume className="media-control media-control--volume"/>
</div>
</section>
</div>
</Media>
}
</div>
)
}
}
export default AudioPlayer
PlayPause
+ MuteUnmute
组件问题
来自 react-media-player
示例库的这些组件中有名为 scale
和 scaleX
的子组件,它们是 <transition>
的非常简单的包装器但最终搞砸了构建。我基本上只是将它们重构为 PlayPause 和 MuteUnmute 组件本身,这解决了我在那里遇到的问题。
withMediaProps()
包装这些模块导出让 Webpack 适合。我不得不使用 Gatsby null loader 来解决这个问题,所以在构建期间 Webpack 不会寻找 react-media-player
.
MuteUnmute.js:
import React, { Component } from 'react'
import { withMediaProps } from 'react-media-player'
import Transition from 'react-motion-ui-pack'
class MuteUnmute extends Component {
_handleMuteUnmute = () => {
this.props.media.muteUnmute()
}
render() {
const { media: { volume }, className } = this.props
return (
<svg width="36px" height="36px" viewBox="0 0 36 36" className={className} onClick={this._handleMuteUnmute}>
<circle fill="#eaebec" cx="18" cy="18" r="18"/>
<polygon fill="#3b3b3b" points="11,14.844 11,21.442 14.202,21.442 17.656,25 17.656,11 14.074,14.844"/>
<Transition
component="g"
enter={{ scale: 1 }}
leave={{ scale: 0 }}
>
{ volume >= 0.5 &&
<path key="first-bar" fill="#3b3b3b" d="M24.024,14.443c-0.607-1.028-1.441-1.807-2.236-2.326c-0.405-0.252-0.796-0.448-1.153-0.597c-0.362-0.139-0.682-0.245-0.954-0.305c-0.058-0.018-0.104-0.023-0.158-0.035v1.202c0.2,0.052,0.421,0.124,0.672,0.22c0.298,0.125,0.622,0.289,0.961,0.497c0.662,0.434,1.359,1.084,1.864,1.94c0.26,0.424,0.448,0.904,0.599,1.401c0.139,0.538,0.193,0.903,0.216,1.616c-0.017,0.421-0.075,1.029-0.216,1.506c-0.151,0.497-0.339,0.977-0.599,1.401c-0.505,0.856-1.202,1.507-1.864,1.94c-0.339,0.209-0.663,0.373-0.961,0.497c-0.268,0.102-0.489,0.174-0.672,0.221v1.201c0.054-0.012,0.1-0.018,0.158-0.035c0.272-0.06,0.592-0.166,0.954-0.305c0.358-0.149,0.748-0.346,1.153-0.597c0.795-0.519,1.629-1.298,2.236-2.326C24.639,20.534,24.994,19.273,25,18C24.994,16.727,24.639,15.466,24.024,14.443z"/>
}
</Transition>
<Transition
component="g"
enter={{ scale: 1 }}
leave={{ scale: 0 }}
>
{ volume > 0 &&
<path key="second-bar" fill="#3b3b3b" d="M21.733,18c0-1.518-0.91-2.819-2.211-3.402v6.804C20.824,20.818,21.733,19.518,21.733,18z"/>
}
</Transition>
<Transition
component="g"
enter={{ scale: 1 }}
leave={{ scale: 0 }}
>
{ volume === 0 &&
<polygon key="mute" fill="#3b3b3b" points="24.839,15.955 23.778,14.895 21.733,16.94 19.688,14.895 18.628,15.955 20.673,18 18.628,20.045 19.688,21.106 21.733,19.061 23.778,21.106 24.839,20.045 22.794,18 "/>
}
</Transition>
</svg>
)
}
}
export default withMediaProps(MuteUnmute)
PlayPause.js:
import React, { Component } from 'react'
import { withMediaProps } from 'react-media-player'
import Transition from 'react-motion-ui-pack'
class PlayPause extends Component {
_handlePlayPause = () => {
this.props.media.playPause()
}
render() {
const { media: { isPlaying }, className } = this.props
return (
<svg
role="button"
width="36px"
height="36px"
viewBox="0 0 36 36"
className={className}
onClick={this._handlePlayPause}
>
<circle fill="#eaebec" cx="18" cy="18" r="18"/>
<Transition
component="g"
enter={{ scaleX: 1 }}
leave={{ scaleX: 0 }}
>
{ isPlaying &&
<g key="pause" style={{ transformOrigin: '0% 50%' }}>
<rect x="12" y="11" fill="#3b3b3b" width="4" height="14"/>
<rect x="20" y="11" fill="#3b3b3b" width="4" height="14"/>
</g>
}
</Transition>
<Transition
component="g"
enter={{ scaleX: 1 }}
leave={{ scaleX: 0 }}
>
{ !isPlaying &&
<polygon
key="play"
fill="#3b3b3b"
points="14,11 26,18 14,25"
style={{ transformOrigin: '100% 50%' }}
/>
}
</Transition>
</svg>
)
}
}
export default withMediaProps(PlayPause)
盖茨比-node.js
/**
* Implement Gatsby's Node APIs in this file.
*
* See: https://www.gatsbyjs.org/docs/node-apis/
*/
exports.onCreateWebpackConfig = ({ stage, loaders, actions }) => {
if (stage === "build-html") {
actions.setWebpackConfig({
module: {
rules: [
{
test: /AudioPlayer|PlayPause|MuteUnmute/,
use: loaders.null(),
},
],
},
})
}
}
打包/构建问题
*我使用的是 Node v10.12,出于某种原因需要升级 node-gyp
。在梳理了网络之后,我最好的猜测是我的全局 node-gyp
包与最新的 Node 不同步,所以它崩溃了。 npm install -g node-gyp
然后 node-gyp -v
应该产生 3.8.0
。那个版本让我的构建开始了。
package.json
我的结局是这样的:
"dependencies": {
"gatsby": "^2.0.12",
"gatsby-cli": "^2.4.3",
"gatsby-link": "^2.0.2",
"gatsby-plugin-manifest": "^2.0.4",
"gatsby-plugin-react-helmet": "^3.0.0",
"gatsby-plugin-sass": "^2.0.1",
"node-sass": "^4.9.3",
"prop-types": "^15.6.2",
"react": "^16.4.2",
"react-anchor-link-smooth-scroll": "^1.0.11",
"react-dom": "^16.4.2",
"react-helmet": "^5.2.0",
"react-hot-loader": "^4.3.11",
"react-media-player": "^0.7.1",
"react-modal": "^3.5.1",
"react-motion-ui-pack": "^0.10.3",
"react-tabs": "^2.2.2",
"react-transition-group": "^2.4.0",
"react-waypoint": "^8.0.3"
}
最后,感谢@pieh + @KyleAMathews 和 Gatsby 团队的其他成员 - Gatsby 生态系统和支持仍然是 OSS 社区中最好的。我会继续尽我所能。谢谢你给我们的一切。
以下组件 AudioPlayer
- 基于 react-media-player
在 Gatsby/React 开发环境中运行良好。但是将它构建到 React SSR 中真是太难了。
AudioPlayer
依赖 window
对象进行实例化,Node.js 不可用。因此,我不得不使用 Gatsby 的自定义 Webpack 配置来检测媒体播放器并将空加载器放入混合中。效果很好:
exports.onCreateWebpackConfig = ({ stage, loaders, actions }) => {
if (stage === "build-html") {
actions.setWebpackConfig({
module: {
rules: [
{
test: /react-media-player/,
use: loaders.null(),
},
],
},
})
}
}
但是当你尝试通过 Webpack 构建时,我得到了错误:
WebpackError: TypeError: Cannot destructure property
CurrentTime
of 'undefined' or 'null'.
这是有道理的,因为我们刚刚删除了包含 CurrentTime
的 react-media-player
。那么如何确保 Webpack 不会尝试解构此组件中的 controls
对象? (ES6中的
import React, { Component } from 'react'
import { Media, Player, controls } from 'react-media-player'
const { CurrentTime, SeekBar, Duration, Volume, PlayPause, MuteUnmute } = controls
let panner = null
class AudioPlayer extends Component {
componentDidMount() {
const audioContext = new (window.AudioContext || window.webkitAudioContext)()
panner = audioContext.createPanner()
panner.setPosition(0, 0, 1)
panner.panningModel = 'equalpower'
panner.connect(audioContext.destination)
const source = audioContext.createMediaElementSource(this._player.instance)
source.connect(panner)
panner.connect(audioContext.destination)
}
_handlePannerChange = ({ target }) => {
const x = +target.value
const y = 0
const z = 1 - Math.abs(x)
panner.setPosition(x, y, z)
}
render() {
return (
<div>
{ typeof window !== 'undefined' && Media &&
<Media>
<div>
<Player
ref={c => this._player = c}
src={this.props.src}
useAudioObject
/>
<section className="media-controls">
<div className="media-title-box">
<PlayPause className="media-control media-control--play-pause"/>
<div className="media-title-content">
<div className="media-title">{ this.props.mediaTitle }</div>
<div className="media-subtitle">{ this.props.mediaSubtitle }</div>
</div>
</div>
<div className="media-controls-container">
<CurrentTime className="media-control media-control--current-time"/>
<SeekBar className="media-control media-control--volume-range"/>
<Duration className="media-control media-control--duration"/>
</div>
<div className="media-sound-controls">
<MuteUnmute className="media-control media-control--mute-unmute"/>
<Volume className="media-control media-control--volume"/>
</div>
</section>
</div>
</Media>
}
</div>
)
}
}
export default AudioPlayer
对于未来的 google 员工/那些感兴趣的人来说,这个问题的最终解决方案需要在我这边跳过很多障碍。我将介绍我完成的所有事情,希望这对其他人有帮助:
AudioPlayer
组件问题
我认为
react-media-player
使用 SSR 特别困难的原因是因为它是组件的集合,而不仅仅是您放入的组件。对于我的构建,我正在创建一个音频播放器,因此包含组件:Media
、Player
、CurrentTime
、SeekBar
、Duration
、Volume
来自 [=16] =] 库和其他组件PlayPause
和MuteUnmute
。我的自定义
AudioPlayer
组件需要三个{ typeof window !== 'undefined'}
检查才能使我的构建工作并将panner
和audioContext
变量重构为componentDidMount()
.请注意,第一个window
检查查找media
而不是react-media-player
,因为react-media-player
永远不会在渲染函数中被调用。
AudioPlayer.js
import React, { Component } from 'react'
import { Media, Player, controls } from 'react-media-player'
import PlayPause from '../../components/AudioPlayerPlayPause'
import MuteUnmute from '../../components/AudioPlayerMuteUnmute'
const { CurrentTime, SeekBar, Duration, Volume } = controls
let panner = null
class AudioPlayer extends Component {
componentDidMount() {
const audioContext = new (window.AudioContext || window.webkitAudioContext)()
panner = audioContext.createPanner()
panner.setPosition(0, 0, 1)
panner.panningModel = 'equalpower'
panner.connect(audioContext.destination)
const source = audioContext.createMediaElementSource(this._player.instance)
source.connect(panner)
panner.connect(audioContext.destination)
}
_handlePannerChange = ({ target }) => {
const x = +target.value
const y = 0
const z = 1 - Math.abs(x)
panner.setPosition(x, y, z)
}
render() {
return (
<div>
{ typeof window !== 'undefined' && Media &&
<Media>
<div>
<Player
ref={c => this._player = c}
src={this.props.src}
useAudioObject
/>
<section className="media-controls">
<div className="media-title-box">
{ typeof window !== 'undefined' && PlayPause &&
<PlayPause className="media-control media-control--play-pause"/>
}
<div className="media-title-content">
<div className="media-title">{ this.props.mediaTitle }</div>
<div className="media-subtitle">{ this.props.mediaSubtitle }</div>
</div>
</div>
<div className="media-controls-container">
<CurrentTime className="media-control media-control--current-time"/>
<SeekBar className="media-control media-control--volume-range"/>
<Duration className="media-control media-control--duration"/>
</div>
<div className="media-sound-controls">
{ typeof window !== 'undefined' && MuteUnmute &&
<MuteUnmute className="media-control media-control--mute-unmute"/>
}
<Volume className="media-control media-control--volume"/>
</div>
</section>
</div>
</Media>
}
</div>
)
}
}
export default AudioPlayer
PlayPause
+ MuteUnmute
组件问题
来自
react-media-player
示例库的这些组件中有名为scale
和scaleX
的子组件,它们是<transition>
的非常简单的包装器但最终搞砸了构建。我基本上只是将它们重构为 PlayPause 和 MuteUnmute 组件本身,这解决了我在那里遇到的问题。withMediaProps()
包装这些模块导出让 Webpack 适合。我不得不使用 Gatsby null loader 来解决这个问题,所以在构建期间 Webpack 不会寻找react-media-player
.
MuteUnmute.js:
import React, { Component } from 'react'
import { withMediaProps } from 'react-media-player'
import Transition from 'react-motion-ui-pack'
class MuteUnmute extends Component {
_handleMuteUnmute = () => {
this.props.media.muteUnmute()
}
render() {
const { media: { volume }, className } = this.props
return (
<svg width="36px" height="36px" viewBox="0 0 36 36" className={className} onClick={this._handleMuteUnmute}>
<circle fill="#eaebec" cx="18" cy="18" r="18"/>
<polygon fill="#3b3b3b" points="11,14.844 11,21.442 14.202,21.442 17.656,25 17.656,11 14.074,14.844"/>
<Transition
component="g"
enter={{ scale: 1 }}
leave={{ scale: 0 }}
>
{ volume >= 0.5 &&
<path key="first-bar" fill="#3b3b3b" d="M24.024,14.443c-0.607-1.028-1.441-1.807-2.236-2.326c-0.405-0.252-0.796-0.448-1.153-0.597c-0.362-0.139-0.682-0.245-0.954-0.305c-0.058-0.018-0.104-0.023-0.158-0.035v1.202c0.2,0.052,0.421,0.124,0.672,0.22c0.298,0.125,0.622,0.289,0.961,0.497c0.662,0.434,1.359,1.084,1.864,1.94c0.26,0.424,0.448,0.904,0.599,1.401c0.139,0.538,0.193,0.903,0.216,1.616c-0.017,0.421-0.075,1.029-0.216,1.506c-0.151,0.497-0.339,0.977-0.599,1.401c-0.505,0.856-1.202,1.507-1.864,1.94c-0.339,0.209-0.663,0.373-0.961,0.497c-0.268,0.102-0.489,0.174-0.672,0.221v1.201c0.054-0.012,0.1-0.018,0.158-0.035c0.272-0.06,0.592-0.166,0.954-0.305c0.358-0.149,0.748-0.346,1.153-0.597c0.795-0.519,1.629-1.298,2.236-2.326C24.639,20.534,24.994,19.273,25,18C24.994,16.727,24.639,15.466,24.024,14.443z"/>
}
</Transition>
<Transition
component="g"
enter={{ scale: 1 }}
leave={{ scale: 0 }}
>
{ volume > 0 &&
<path key="second-bar" fill="#3b3b3b" d="M21.733,18c0-1.518-0.91-2.819-2.211-3.402v6.804C20.824,20.818,21.733,19.518,21.733,18z"/>
}
</Transition>
<Transition
component="g"
enter={{ scale: 1 }}
leave={{ scale: 0 }}
>
{ volume === 0 &&
<polygon key="mute" fill="#3b3b3b" points="24.839,15.955 23.778,14.895 21.733,16.94 19.688,14.895 18.628,15.955 20.673,18 18.628,20.045 19.688,21.106 21.733,19.061 23.778,21.106 24.839,20.045 22.794,18 "/>
}
</Transition>
</svg>
)
}
}
export default withMediaProps(MuteUnmute)
PlayPause.js:
import React, { Component } from 'react'
import { withMediaProps } from 'react-media-player'
import Transition from 'react-motion-ui-pack'
class PlayPause extends Component {
_handlePlayPause = () => {
this.props.media.playPause()
}
render() {
const { media: { isPlaying }, className } = this.props
return (
<svg
role="button"
width="36px"
height="36px"
viewBox="0 0 36 36"
className={className}
onClick={this._handlePlayPause}
>
<circle fill="#eaebec" cx="18" cy="18" r="18"/>
<Transition
component="g"
enter={{ scaleX: 1 }}
leave={{ scaleX: 0 }}
>
{ isPlaying &&
<g key="pause" style={{ transformOrigin: '0% 50%' }}>
<rect x="12" y="11" fill="#3b3b3b" width="4" height="14"/>
<rect x="20" y="11" fill="#3b3b3b" width="4" height="14"/>
</g>
}
</Transition>
<Transition
component="g"
enter={{ scaleX: 1 }}
leave={{ scaleX: 0 }}
>
{ !isPlaying &&
<polygon
key="play"
fill="#3b3b3b"
points="14,11 26,18 14,25"
style={{ transformOrigin: '100% 50%' }}
/>
}
</Transition>
</svg>
)
}
}
export default withMediaProps(PlayPause)
盖茨比-node.js
/**
* Implement Gatsby's Node APIs in this file.
*
* See: https://www.gatsbyjs.org/docs/node-apis/
*/
exports.onCreateWebpackConfig = ({ stage, loaders, actions }) => {
if (stage === "build-html") {
actions.setWebpackConfig({
module: {
rules: [
{
test: /AudioPlayer|PlayPause|MuteUnmute/,
use: loaders.null(),
},
],
},
})
}
}
打包/构建问题
*我使用的是 Node v10.12,出于某种原因需要升级 node-gyp
。在梳理了网络之后,我最好的猜测是我的全局 node-gyp
包与最新的 Node 不同步,所以它崩溃了。 npm install -g node-gyp
然后 node-gyp -v
应该产生 3.8.0
。那个版本让我的构建开始了。
package.json
我的结局是这样的:
"dependencies": {
"gatsby": "^2.0.12",
"gatsby-cli": "^2.4.3",
"gatsby-link": "^2.0.2",
"gatsby-plugin-manifest": "^2.0.4",
"gatsby-plugin-react-helmet": "^3.0.0",
"gatsby-plugin-sass": "^2.0.1",
"node-sass": "^4.9.3",
"prop-types": "^15.6.2",
"react": "^16.4.2",
"react-anchor-link-smooth-scroll": "^1.0.11",
"react-dom": "^16.4.2",
"react-helmet": "^5.2.0",
"react-hot-loader": "^4.3.11",
"react-media-player": "^0.7.1",
"react-modal": "^3.5.1",
"react-motion-ui-pack": "^0.10.3",
"react-tabs": "^2.2.2",
"react-transition-group": "^2.4.0",
"react-waypoint": "^8.0.3"
}
最后,感谢@pieh + @KyleAMathews 和 Gatsby 团队的其他成员 - Gatsby 生态系统和支持仍然是 OSS 社区中最好的。我会继续尽我所能。谢谢你给我们的一切。