本文由 VicSusi 在众成翻译平台翻译。
你是不是对 React 充满好奇,并且还没有机会学习它?或者你已经尝试过,却挣扎于核心概念的掌握?又或者你已经学习过基础,但是想巩固一下知识?无论你是以上哪种类型,这篇文章都适合你。
我们要基于以下的全新 React 概念构建一个简单的 React 音乐播放器。
下面是我们要了解的内容:
以上是构建和维护一个 React 应用你需要了解的知识。但我们打算按部分来介绍。
现在情况是这样:你要完成一个小小的任务。他们创建了用户上传音乐的页面,并用醒目的颜色让页面更加直观。但他们需要你来完成困难的部分-就是指让这个页面正常工作。
开始前,创建一个新的项目目录,添加 以下的三个文件 .
app.css
- body {
- <pre> background: #f9f9f9;
- font-family: 'Open Sans', sans-serif;
- text-align: center;
- }
- #container {
- position: relative;
- z-index: 2;
- padding-top: 100px;
- }
- .play {
- display: block;
- width: 0;
- height: 0;
- border-top: 50px solid transparent;
- border-bottom: 50px solid transparent;
- border-left: 60px solid #2c3e50;
- margin: 100px auto 50px auto;
- position: relative;
- z-index: 1;
- transition: all 0.3s;
- -webkit-transition: all 0.3s;
- -moz-transition: all 0.3s;
- left: 10px;
- }
- .play:before {
- content: '';
- position: absolute;
- top: -75px;
- left: -115px;
- bottom: -75px;
- right: -35px;
- border-radius: 50%;
- border: 10px solid #2c3e50;
- z-index: 2;
- transition: all 0.3s;
- -webkit-transition: all 0.3s;
- -moz-transition: all 0.3s;
- }
- .play:after {
- content: '';
- opacity: 0;
- transition: opacity 0.6s;
- -webkit-transition: opacity 0.6s;
- -moz-transition: opacity 0.6s;
- }
- .play:hover:before, .play:focus:before {
- transform: scale(1.1);
- -webkit-transform: scale(1.1);
- -moz-transform: scale(1.1);
- }
- .play.active {
- border-color: transparent;
- }
- .play.active:after {
- content: '';
- opacity: 1;
- width: 25px;
- height: 80px;
- position: absolute;
- right: 8px;
- top: -40px;
- border-right: 20px solid #2c3e50;
- border-left: 20px solid #2c3e50;
- }
- h1 {
- text-transform: uppercase;
- color: #34495e;
- letter-spacing: 2px;
- font-size: 2em;
- margin-bottom: 0;
- }
- canvas {
- position: fixed;
- left: 0;
- top: 0;
- width: 100%;
- height: 100%;
- }
- audio {
- position: fixed;
- left: 10px;
- bottom: 10px;
- width: calc(100% - 20px);
- }
- </pre>
- <div>```</div>
- <div>app.js</div>
- <div>```</div>
- <div>
- <pre>var ALPHA,
- AudioAnalyser,
- COLORS,
- MP3_PATH,
- NUM_BANDS,
- NUM_PARTICLES,
- Particle,
- SCALE,
- SIZE,
- SMOOTHING,
- SPEED,
- SPIN,
- TIMES_CALLED,
- ANALYSER;
- NUM_PARTICLES = 150;
- NUM_BANDS = 128;
- TIMES_CALLED = 0;
- SMOOTHING = 0.5;
- MP3_PATH = 'music.mp3';
- SCALE = {
- MIN: 5.0,
- MAX: 80.0
- };
- SPEED = {
- MIN: 0.2,
- MAX: 1.0
- };
- ALPHA = {
- MIN: 0.8,
- MAX: 0.9
- };
- SPIN = {
- MIN: 0.001,
- MAX: 0.005
- };
- SIZE = {
- MIN: 0.5,
- MAX: 1.25
- };
- COLORS = [
- '#69D2E7',
- '#1B676B',
- '#BEF202',
- '#EBE54D',
- '#00CDAC',
- '#1693A5',
- '#F9D423',
- '#FF4E50',
- '#E7204E',
- '#0CCABA',
- '#FF006F'
- ];
- function getAnimation(file) {
- AudioAnalyser = (function() {
- AudioAnalyser.AudioContext = self.AudioContext || self.webkitAudioContext;
- AudioAnalyser.enabled = AudioAnalyser.AudioContext != null;
- function AudioAnalyser(audio, numBands, smoothing) {
- var src;
- this.audio = audio != null ? audio : new Audio();
- this.numBands = numBands != null ? numBands : 256;
- this.smoothing = smoothing != null ? smoothing : 0.3;
- this.audio = document.getElementById('audio');
- if (!this.audio) {
- return;
- }
- try {
- this.audio.src = window.URL.createObjectURL(file);
- } catch (err) {
- console.log(err);
- }
- this.context = new AudioAnalyser.AudioContext();
- this.jsNode = this.context.createScriptProcessor(2048, 1, 1);
- this.analyser = this.context.createAnalyser();
- this.analyser.smoothingTimeConstant = this.smoothing;
- this.analyser.fftSize = this.numBands * 2;
- this.bands = new Uint8Array(this.analyser.frequencyBinCount);
- this.audio.addEventListener(
- 'play',
- (function(_this) {
- return function() {
- if (TIMES_CALLED === 1) {
- return;
- }
- ANALYSER.start();
- TIMES_CALLED++;
- _this.source = _this.context.createMediaElementSource(_this.audio);
- _this.source.connect(_this.analyser);
- _this.analyser.connect(_this.jsNode);
- _this.jsNode.connect(_this.context.destination);
- _this.source.connect(_this.context.destination);
- return (_this.jsNode.onaudioprocess = function() {
- _this.analyser.getByteFrequencyData(_this.bands);
- if (!_this.audio.paused) {
- return typeof _this.onUpdate === 'function'
- ? _this.onUpdate(_this.bands)
- : void 0;
- }
- });
- };
- })(this)
- );
- }
- AudioAnalyser.prototype.start = function() {
- return this.audio.play();
- };
- AudioAnalyser.prototype.stop = function() {
- return this.audio.pause();
- };
- return AudioAnalyser;
- })();
- Particle = (function() {
- function Particle(x1, y1) {
- this.x = x1 != null ? x1 : 0;
- this.y = y1 != null ? y1 : 0;
- this.reset();
- }
- Particle.prototype.reset = function() {
- this.level = 1 + floor(random(4));
- this.scale = random(SCALE.MIN, SCALE.MAX);
- this.alpha = random(ALPHA.MIN, ALPHA.MAX);
- this.speed = random(SPEED.MIN, SPEED.MAX);
- this.color = random(COLORS);
- this.size = random(SIZE.MIN, SIZE.MAX);
- this.spin = random(SPIN.MAX, SPIN.MAX);
- this.band = floor(random(NUM_BANDS));
- if (random() < 0.5) {
- this.spin = -this.spin;
- }
- this.smoothedScale = 0.0;
- this.smoothedAlpha = 0.0;
- this.decayScale = 0.0;
- this.decayAlpha = 0.0;
- this.rotation = random(TWO_PI);
- return (this.energy = 0.0);
- };
- Particle.prototype.move = function() {
- this.rotation += this.spin;
- return (this.y -= this.speed * this.level);
- };
- Particle.prototype.draw = function(ctx) {
- var alpha, power, scale;
- power = exp(this.energy);
- scale = this.scale * power;
- alpha = this.alpha * this.energy * 1.5;
- this.decayScale = max(this.decayScale, scale);
- this.decayAlpha = max(this.decayAlpha, alpha);
- this.smoothedScale += (this.decayScale - this.smoothedScale) * 0.3;
- this.smoothedAlpha += (this.decayAlpha - this.smoothedAlpha) * 0.3;
- this.decayScale *= 0.985;
- this.decayAlpha *= 0.975;
- ctx.save();
- ctx.beginPath();
- ctx.translate(this.x + cos(this.rotation * this.speed) * 250, this.y);
- ctx.rotate(this.rotation);
- ctx.scale(
- this.smoothedScale * this.level,
- this.smoothedScale * this.level
- );
- ctx.moveTo(this.size * 0.5, 0);
- ctx.lineTo(this.size * -0.5, 0);
- ctx.lineWidth = 1;
- ctx.lineCap = 'round';
- ctx.globalAlpha = this.smoothedAlpha / this.level;
- ctx.strokeStyle = this.color;
- ctx.stroke();
- return ctx.restore();
- };
- return Particle;
- })();
- Sketch.create({
- particles: [],
- setup: function() {
- var analyser, error, i, intro, j, particle, ref, warning, x, y;
- for (i = j = 0, ref = NUM_PARTICLES - 1; j <= ref; i = j += 1) {
- x = random(this.width);
- y = random(this.height * 2);
- particle = new Particle(x, y);
- particle.energy = random(particle.band / 256);
- this.particles.push(particle);
- }
- if (AudioAnalyser.enabled) {
- try {
- analyser = new AudioAnalyser(MP3_PATH, NUM_BANDS, SMOOTHING);
- analyser.onUpdate = (function(_this) {
- return function(bands) {
- var k, len, ref1, results;
- ref1 = _this.particles;
- results = [];
- for (k = 0, len = ref1.length; k < len; k++) {
- particle = ref1[k];
- results.push((particle.energy = bands[particle.band] / 256));
- }
- return results;
- };
- })(this);
- analyser.audio = window.audio;
- ANALYSER = analyser;
- intro = document.getElementById('intro');
- intro.style.display = 'none';
- if (
- /Safari/.test(navigator.userAgent) &&
- !/Chrome/.test(navigator.userAgent)
- ) {
- warning = document.getElementById('warning2');
- return (warning.style.display = 'block');
- }
- } catch (_error) {
- error = _error;
- }
- } else {
- warning = document.getElementById('warning1');
- return (warning.style.display = 'block');
- }
- },
- draw: function() {
- var j, len, particle, ref, results;
- this.globalCompositeOperation = 'lighter';
- ref = this.particles;
- results = [];
- for (j = 0, len = ref.length; j < len; j++) {
- particle = ref[j];
- if (particle.y < -particle.size * particle.level * particle.scale * 2) {
- particle.reset();
- particle.x = random(this.width);
- particle.y =
- this.height + particle.size * particle.scale * particle.level;
- }
- particle.move();
- results.push(particle.draw(this));
- }
- return results;
- }
- });
- }
- function handleFileSelect(evt) {
- var files = evt.target.files;
- getAnimation(files[0]);
- }
- getAnimation(null);
- document
- .getElementById('files')
- .addEventListener('change', handleFileSelect, false);</pre>
- </div>
- <div>```</div>
- <div>index.html</div>
- <div>
- <link rel="stylesheet" href="app.css">
- <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Open+Sans:400,800">
- Play Music
- <input type="file" id="files" name="files[]" multiple />
- `<script crossorigin src="https://unpkg.com/react@15/dist/react.js">`</script>
- `<script crossorigin src="https://unpkg.com/react-dom@15/dist/react-dom.js">`</script>
- `<script src="https://npmcdn.com/babel-core@5.8.38/browser.min.js">`</script>
- `<script src="https://soulwire.github.io/sketch.js/js/sketch.min.js">`</script>
- `<script src="app.js">`</script>
- `<script type="text/babel">`
- // React code goes here.
- </script>
```
确定你使用了最新版 chrome ,不然代码中的动画效果无法呈现
谢谢 Steven Fabre 提供的播放按钮 CSS 代码,还有 Justin Windle 提供的可视化代码 ( 你可以在这里查看代码 )
在代码编辑器里打开 index.html,让我们开始吧!
React 是构建用户界面的一种方式。它只和你在前端所见的相关。React 通过把每个页面切分为小块让用户界面变得易于构建。我们把这些小块叫做组件。
这里有一个把页面切分为组件的例子:
每个高亮的部分都被视为一个组件。可这对于开发者意味着什么呢
一个 React 组件就是一个代表页面某部分的代码块。每个组件都是一个 JS 函数,这个函数返回的代码表示一个 web 页面的一部分。
为了构建一个页面,我们将按一定顺序调用这些函数,然后把结果放在一起展示给用户。
让我们在 index.html 中写一个 type="text/babel" 的组件:
- `<script type="text/babel">`
- function OurFirstComponent() {
- return (
- // Code that represents the UI element goes here
- );
- }
- </script>
当我们调用 OurFirstcomponent() 函数,我们会得到页面的一个部分。
你可以像下面这样编写函数:
- const OurFirstComponent = () = > {
- return (
- // Stuff to make this component goes here
- );
- }
React 使用了 JSX 语言,它看起来像 HTML 但在 JS 里运行,HTML 可不经常这么做。
你可以为这部分添加无样式的 HTML,这样就可以在 UI 上显示了:
- ` & lt;
- script type = "text/babel" & gt;`
- function OurFirstComponent() {
- return ( < h1 > Hello, I am a React Component ! </h1>
- );
- }
- </script & gt;
When we call the OurFirstComponent() function, we get back a bit of JSX. We can use something called ReactDOM to put it on the page.
当我们调用 OurFirstComponent 函数,我们得到一些 JSX 代码。我们可以使用 ReactDOM 把它们放在页面上。
- ` & lt;
- script type = "text/babel" & gt;`
- function OurFirstComponent() {
- return ( < h1 > Hello, I am a React Component ! </h1>
- );
- }/
- * *const placeWeWantToPutComponent = document.getElementById('hook');
- ReactDOM.render(OurFirstComponent(), placeWeWantToPutComponent); * *<
- /script>/
现在通过 ID 我们会将标签放入元素中。当你刷新浏览器时它看起来应该是这样:
我们可以像这样用 JSX 编写组件:
- ReactDOM.render( & lt; OurFirstComponent / >, placeWeWantToPutComponent);
这是标准-调用组件就像编写 HTML,效果是一样的。
我们可以把 React 组件放在其他组件里。
- ` & lt;
- script type = "text/babel" & gt;`
- function OurFirstComponent() {
- return ( * *<h1 > I am the child ! </h1>**
- );
- }/
- **function Container() {
- return (
- <div>
- <h1>I am the parent!</h1>
- <OurFirstComponent />
- </div>
- );
- }**
- const placeWeWantToPutComponent = document.getElementById('hook');
- ReactDOM.render( * *< Container / > * *, placeWeWantToPutComponent); & lt;
- /script>/
我们就是这样通过 React 组件构造页面的-通过组件嵌套组件实现。
到目前为止,我们一直在像编写函数一样编写组件。它们被称为函数式组件.
但你也可以用另一种方式编写组件,比如 JavaScript 类。这样编写的组件被称作类组件。
- class Container extends React.Component {
- render() {
- return (
- <div>
- <h1>I am the parent!</h1>
- <OurFirstComponent />
- </div>
- );
- }
- }
- const placeWeWantToPutComponent = document.getElementById('hook');
- ReactDOM.render( & lt; Container / >, placeWeWantToPutComponent);
类组件一定要有一个 render() 函数. 这个渲染函数返回组件的 JSX 代码。它们和函数化组件的使用方法相同。
你应该更多地使用函数化组件而不是类组件,因为前者更容易阅读,除非你需要组件状态(稍后介绍)
你可以把 JavaScript 变量这样放进你的 JSX 中:
- class Container extends React.Component {
- render() {
- **const greeting = 'I am a string!';**
- return (
- <div>
- **<h1>{ greeting }</h1>**
- <OurFirstComponent />
- </div>
- );
- }
- }
现在 I am a string 会在 h1 标签内出现。
你也可以做点更难的,比如调用一个函数:
- class Container extends React.Component {
- render() {
- **const addNumbers = (num1, num2) => {
- return num1 + num2;
- };**
- return (
- <div>
- **<h1>The sum is: { addNumbers(1, 2) }</h1>**
- <OurFirstComponent />
- </div>
- );
- }
- }
将 OurFirstComponent() 更名为 PlayButton。我们想要它返回下面的内容:
- <a href="#" title="Play video" />
可现在有个问题:class 在 JavaScript 中是关键字,所以我们无法使用它。那我们该如何创建一个示例?
用一个名字叫 className 的属性代替:
- `<script type="text/babel">`
- **function PlayButton() {
- return <a href="#" title="Play video" />;
- }**
- class Container extends React.Component {
- render() {
- return (
- <div>
- **<PlayButton />**
- </div>
- );
- }
- }
- const placeWeWantToPutComponent = document.getElementById('hook');
- ReactDOM.render( & lt; Container / >, placeWeWantToPutComponent); & lt;
- /script>/
类组件可以存储关于它们最近状态的信息。这个信息被称为状态,储存在一个 JavaScript 对象中。
下面的代码里,有一个对象代表我们组件的状态。对象有一个键名叫 isMusicPlaying,值为 false。这个对象被以建造者模式分配给 this.state,当这个类首次使用时 this.state 被调用。
- class Container extends React.Component {
- **constructor(props) {
- super(props);
- this.state = { isMusicPlaying: false };
- }**
- render() {
- return (
- <div>
- <PlayButton />
- </div>
- );
- }
- }
一个 React 组件的建造者模式总是需要在最开始就调用 super(props)。
好,那我们要状态干什么呢?为什么它会存在?
可以基于事件的状态来更新我们的 UI。
在本文中,我们会基于用户点击播放按钮的操作,利用状态实现播放按钮从停止到播放的改变。
当用户点击按钮,状态就会更新,然后 UI 就会更新。
我们按下面的步骤开始。我们可以利用 this,state 监视组件状态。下面的代码中,我们持续监视状态并根据它来决定展示给用户什么样的内容。
- class Container extends React.Component {
- constructor(props) {
- super(props);
- this.state = { isMusicPlaying: false };
- }
- render() {
- **const status = this.state.isMusicPlaying ? 'Playing' : 'Not playing';**
- return (
- <div>
- **<h1>{ status }</h1>**
- <PlayButton />
- </div>
- );
- }
- }
在这个渲染函数中,this 经常表示它所在的组件。
但这并没有什么用处,除非我们有方法可以改变 this.state.isMusicPlaying。
用户可以通过点击播放按钮与我们的组件实现交互。我们想对这些事件做出反应 (ha...ha...)。
我们通过与事件相关的函数实现交互操作。我们称之为事件句柄。
- class Container extends React.Component {
- constructor(props) {
- super(props);
- this.state = { isMusicPlaying: false };
- }
- * *handleClick(event) {
- // Do something about the click
- }; * *
- render() {
- let status = this.state.isMusicPlaying
- ? 'Playing :)'
- : 'Not playing :(';
- return (
- <div>
- <h1>{ status }</h1>
- <PlayButton />
- </div>
- );
- }
- }
当用户点击 h1 时,我们的组件会运行 handleClick 函数。这个函数将得到的时间对象作为参数,这意味着它可以随心所欲地使用该对象。
我们在 handleClick 上使用. bind 方法,以确保 this 特指整个组件,而不只是 h1。
当我们改变了组件的状态,它会再次调用渲染函数。
我们可以随 this.setState() 改变状态,只要我们给它一个表示新状态的新对象。
我们页面中的组件会始终展示它们的最新状态。这是 React 替我们做的。
- handleClick() {
- **if (this.state.isMusicPlaying) {
- this.setState({ isMusicPlaying: false });
- } else {
- this.setState({ isMusicPlaying: true });
- }**
- };
可是点击一个 h1 还是不如点击播放按钮方便。让我们动手实现一下吧。
你的组件可以互相通信。让我们试一下。
我们可以用属性告知播放按钮音乐是否在播放。属性是一个父组件分享给子组件的信息。
JSX 中的属性看起来和 HTML 属性差不多。
我们给播放按钮设置一个名为 isMusicPlaying 的属性,它和 this.state 里的 isMusicPlaying 一样。
- class Container extends React.Component {
- constructor(props) {
- super(props);
- this.state = { isMusicPlaying: false };
- }
- handleClick() {
- if (this.state.isMusicPlaying) {
- this.setState({ isMusicPlaying: false });
- } else {
- this.setState({ isMusicPlaying: true });
- }
- };
- render() {
- return ( < div > **< PlayButton isMusicPlaying = {
- this.state.isMusicPlaying
- }
- />** </div > );
- }
- }
当 Container 的状态改变,PlayButton 属性也会改变,PlayButton 函数也会被再次调用。这意味着我们的组件会在屏幕上更新。
在 PlayButton 里,我们可以对改变做出反应,因为 PlayButton 会将得到的属性作为参数。
- function PlayButton( * *props * *) { * *const className = props.isMusicPlaying ? 'play active': 'play'; * *
- return < a href = "#"title = "Play video" / >;
- }
如果我们改变状态为 this.state = {isMusicPlaying:true};,然后刷新页面,你应该能看到暂停按钮。
你的属性没有必要必须为信息。它们可以是函数。
- function PlayButton(props) {
- const className = props.isMusicPlaying ? 'play active': 'play';
- return < a href = "#"title = "Play video" / >;
- }
- class Container extends React.Component {
- constructor(props) {
- super(props);
- this.state = { isMusicPlaying: false };
- }
- handleClick() {
- if (this.state.isMusicPlaying) {
- this.setState({ isMusicPlaying: false });
- } else {
- this.setState({ isMusicPlaying: true });
- }
- };
- render() {
- return (
- <div>
- <PlayButton
- **onClick={this.handleClick.bind(this)}**
- isMusicPlaying={this.state.isMusicPlaying}
- />
- </div>
- );
- }
- }
现在,当我们点击播放按钮,Container 的状态会改变,这会改变播放按钮的属性,导致按钮在页面上刷新。
setState 很糟糕,因为它不会立即响应。React 会等待一会以便查看是否有更多改变要做,之后才会改变状态。
这意味着你不确定当你调用 setState 时你的状态会变成什么。
所以你不应该这样做:
- handleClick() {
- this.setState({ isMusicPlaying: !this.state.isMusicPlaying });
- };
如果你是基于旧状态来改变新状态,你需要做点不一样的工作。
你需要赋给 getState 一个函数而不是对象。这个函数获得旧状态作为一个参数,返回一个新对象,这个对象即为新状态。
它看起来像是这样:
- handleClick() {
- this.setState(prevState = > {
- return {
- isMusicPlaying: !prevState.isMusicPlaying
- };
- });
- };
这更困难,但只有当你使用旧状态来获得新状态时才需要。如果不是,你可以只赋给 setState 一个对象。
让我们试试播放音乐。
- class Container extends React.Component {
- constructor(props) {
- super(props);
- this.state = { isMusicPlaying: false };
- }
- handleClick() {
- this.setState(prevState = > {
- return {
- isMusicPlaying: !prevState.isMusicPlaying
- };
- });
- };
- render() {
- return ( < div > < PlayButton onClick = {
- this.handleClick.bind(this)
- }
- isMusicPlaying = {
- this.state.isMusicPlaying
- }
- />
- **<audio / > **</div>
- );
- }
- }/
- <audio> { this.audio = audioTag }}** />
- handleClick() { * *
- if (this.state.isMusicPlaying) {
- this.audio.pause();
- } else {
- this.audio.play();
- } * *this.setState(prevState = > {
- return {
- isMusicPlaying: !prevState.isMusicPlaying
- };
- });
- };
来源: https://sdk.cn/news/7876