内容简介:去年写了一款Web音乐App,并发表了系列文章,介绍了开发的过程,当时使用前端领域的技术迭代更新实在是太快了,经常有人吐槽做前端就要做好随时学习的准备,不然就会被淘汰啦⊙﹏⊙∥∣°
去年写了一款Web音乐App,并发表了系列文章,介绍了开发的过程,当时使用 create-react-app 官方脚手架搭建的项目, react-scripts 是 1.x 版本,而react版本是 16.2.0 ,去年10月份 create-react-app 已经发布了 2.0 版本, react 在去年12月份升级到了 16.7.0
前端领域的技术迭代更新实在是太快了,经常有人吐槽 求不要更新 、 我学不动了 、 我学不完了
做前端就要做好随时学习的准备,不然就会被淘汰啦⊙﹏⊙∥∣°
只要是做开发的都要保持一颗积极学习的心,不管是前端领域还是后端领域,不过前端学习新技术的间隔时间要比后端长。作为 Java 出身的我深有体会o(╯□╰)o
更新介绍
create-react-app
时至今日, create-react-app 更新到了2.x的版本了,主要是升级了它所依赖的许多工具,这些 工具 已经发布了包含新特性和性能改进的新版本,比如 babel7 , webpack4 , babel7 和 webpack4 具体更新了哪些,优化了哪些大家可以去查阅资料。 以下列出来 create-react-app 更新了的几个要点
- 新增Sass预处理器,CSS模块化支持
- 更新到Babel7
- 更新到webpack4
- 新增 preset-env
更多更新内容请戳这里
react16.3
因为之前使用的是react16.2,说到react16.7得从16.3说起
16.3新增了几个新的生命周期函数、 context API 、 createRef API 和forwardRef API,新增的两个生命周期函数 getDerivedStateFromProps 和 getSnapshotBeforeUpdate 主要是替代之前的 componentWillMount , componentWillReceiveProps 和 componentWillUpdate ,目的是为了支持error boundaries和即将到来的 async rendering mode (异步渲染)。当使用 async rendering mode 时,会中断初始化渲染,错误处理的中断行为可能导致内存泄漏,而使用 componentWillMount , componentWillReceiveProps 和 componentWillUpdate 会加大这类问题产生的几率
在之前的版本,获取dom或组件时,有两种方法,一种是给一个ref,指定一个name,再用refs.name或ReactDOM.findDOMNode(name)获取,另一种就是使用ref回调,给ref一个回调函数。在开始的时候我用的是第一种,后面改用了ref回调,现在官方不推荐使用了,推荐使用ref回调的方式,因为第一种有几个 缺点 ,使用ref回调有些麻烦,所以官方提供了新的操作就是createRef API
当使用函数组件时如何获取dom,forwardRef API允许你使用函数组件并传递ref给子组件,这样就能方便的获取子组件中的dom
更多内容请戳这里
react16.6
这个版本的更新我还是很喜欢的,官方终于和vue一样支持 Code Splitting 了
在React中使用 Code Splitting ,麻烦点自己写一个懒加载组件,简单点使用第三方库。现在官方新增React.lazy和 Suspense 用来支持 Code Splitting
import React, {lazy, Suspense} from 'react';
const LazyComponent = lazy(() => import('./LazyComponent'));
function MyComponent() {
return (
<Suspense fallback={<div>Loading...</div>}>
<LazyComponent />
</Suspense>
);
}
复制代码
注意:React.lazy and Suspense目前不支持服务端渲染,服务端渲染官方推荐使用 Loadable Components
类组件中有个生命周期函数 shouldComponentUpdate 用来告诉组件是否进行render,继承 React.component ,可以自己重新这个方法来判断决定该怎样进行render,继承 React.PureComponent ,默认已经实现了 shouldComponentUpdate ,它会把props和state进行浅比较,不相等才进行render,不能自己重写 shouldComponentUpdate 。对于函数组件,它没有这样的功能,在这个版本中新增了 React.memo ,使函数组件具有和 React.PureComponent 一样的功能
16.3中新增了context API,当使用context时你需要使用 Consumer 像下面这样
const ThemeContext = React.createContext('light');
...
class MyComponent extends React.Component {
render() {
return (
<ThemeContext.Consumer>
{theme => /* 使用context */}
</ThemeContext.Consumer>
);
}
}
复制代码
现在可以使用更方便的static contextType
const ThemeContext = React.createContext('light');
...
class MyComponent extends React.Component {
render() {
let value = this.context;
/* 使用context */
}
}
MyComponent.contextType = ThemeContext;
复制代码
更多内容请戳这里
升级
此次升级基于此 源码
在开始之前,先把组件目录做一下调整,使用约定俗成的目录名称来存放对应的组件,新建views目录,把components目录下的组件移到views目录下,然后把common目录下的组件移到components目录
修改配置
现在开始升级,将 react-scripts 升级到 2.1.3 , react 升级到 16.7.0
npm install --save --save-exact react-scripts@2.1.3 复制代码
npm install react@16.7.0 react-dom@16.7.0 复制代码
稍等片刻
运行 npm run start
发现报错了,之前是基于 react-scripts 1.x的版本自定义了脚本, react-scripts 2.x中配置变化了很多,导致原来自定义的脚本不能用了。另外寻找修改配置的方法太费时间,如果你熟悉webpack配置运行自带的 eject 将配置文件提取出来,或者寻找第三方 customize-cra ,这样的话就要多学习一下配置方法,如果作者不维护了,react-scripts发生大的更新,也不能及时适配新的版本,这里我选择暴力,将配置文件提取出来
let's do it
运行 npm run eject
scripts 目录已经在项目中存在了(之前自定义配置写的脚本),删了它,再次运行,稍等片刻,执行完后在package.json中添加了很多依赖,还有一些postcss、babel和eslint配置
wait
package.json中 scripts 的脚本并未更新,参考了其它 npm run eject 后的 scripts ,然后将其修改如下
"scripts": {
"start": "npm run dev",
"dev": "node scripts/start.js",
"build": "node scripts/build.js"
}
复制代码
eject后,开发相关依赖都到 dependencies 中去了,然后将开发相关依赖放到 devDependencies 并且去掉jest相关依赖
运行 npm run dev
提示是否添加browserslist配置,输入Y回车,然后会出现如下报错,页面样式错乱
Module not found: Can't resolve '@/api/config' 复制代码
此时还没配置别名 @ 和 stylus
打开config目录下面的webpack.config.js,找到配置 resolve 节点下的 alias ,增加别名
config/webpack.config.js
module.exports = function(webpackEnv) {
...
return {
...
resolve: {
...
alias: {
// Support React Native Web
// https://www.smashingmagazine.com/2016/08/a-glimpse-into-the-future-with-react-native-for-web/
'react-native': 'react-native-web',
'@': path.join(__dirname, '..', "src")
},
}
}
...
}
复制代码
关于 alias ,使用 alias 可以减少webpack打包的时间,但是对ide或工具不友好,无法进行跳转,查看代码时非常不方便。如果你能忍受,就配置,不能忍受import时就写相对路径吧,这里使用 alias 做演示,最终的源码没有使用 alias
接着就是stylus,官方居然只支持sass,可能是sass使用的人多,你好歹都多支持几个吧≡(▔﹏▔)≡
之前用原始的方式使用css,存在很严重的问题,就是会出现css冲突的问题,这类问题有很多解决方案如 styled-compoents 、 styled-jsx 和 css modules ,前面两个简直是另类, css modules 没有颠覆原始的css,同时还支持css处理器,不依赖框架,不仅在react中还可以在vue中使用。在webpack中启用css modules只需要给 css-loader 一个 modules 选项即可,在项目中有时候css文件会用到css modules而有些并不需要,对于这种需求, resct-scripts 是这么配的
config/webpack.config.js
...
// style files regexes
const cssRegex = /\.css$/;
const cssModuleRegex = /\.module\.css$/;
const sassRegex = /\.(scss|sass)$/;
const sassModuleRegex = /\.module\.(scss|sass)$/;
// This is the production and development configuration.
// It is focused on developer experience, fast rebuilds, and a minimal bundle.
module.exports = function(webpackEnv) {
...
return {
...
module: {
strictExportPresence: true,
rules: [
...,
{
test: cssRegex,
exclude: cssModuleRegex,
use: getStyleLoaders({
importLoaders: 1,
sourceMap: isEnvProduction && shouldUseSourceMap,
}),
sideEffects: true,
},
// Adds support for CSS Modules (https://github.com/css-modules/css-modules)
// using the extension .module.css
{
test: cssModuleRegex,
use: getStyleLoaders({
importLoaders: 1,
sourceMap: isEnvProduction && shouldUseSourceMap,
modules: true,
getLocalIdent: getCSSModuleLocalIdent,
}),
},
// Opt-in support for SASS (using .scss or .sass extensions).
// By default we support SASS Modules with the
// extensions .module.scss or .module.sass
{
test: sassRegex,
exclude: sassModuleRegex,
use: getStyleLoaders(
{
importLoaders: 2,
sourceMap: isEnvProduction && shouldUseSourceMap,
},
'sass-loader'
),
sideEffects: true,
},
// Adds support for CSS Modules, but using SASS
// using the extension .module.scss or .module.sass
{
test: sassModuleRegex,
use: getStyleLoaders(
{
importLoaders: 2,
sourceMap: isEnvProduction && shouldUseSourceMap,
modules: true,
getLocalIdent: getCSSModuleLocalIdent,
},
'sass-loader'
),
},
...
]
}
}
}
复制代码
上述配置中, getStyleLoaders 是一个返回样式loader配置的函数,根据传入的参数返回不同的配置,在rules中,以 .css 或 .(scss|sass) 结尾就使用常规的loader,以 .moduels.css 或 .module.(scss|sass) 结尾就启用css moduels。当需要使用css modules时,就在文件名后面后缀前面加一个.module,react中样式文件命名约定和组件文件名一致,并且组件和样式放到同一个目录,如果有一个名为RecommendList.js文件,那么样式文件命名为recommend-list.module.css,放到一起时,就成了下面这样
怎么会有这么长的尾巴
如何去掉这个长尾巴而不影响使用css modules,我们使用webpack配置中的Rule.oneOf和 Rule.resourceQuery
在 webpack.config.js 中增加stylus配置
config/webpack.config.js
...
// style files regexes
const cssRegex = /\.css$/;
const cssModuleRegex = /\.module\.css$/;
const sassRegex = /\.(scss|sass)$/;
const sassModuleRegex = /\.module\.(scss|sass)$/;
const stylusRegex = /\.(styl|stylus)$/;
// This is the production and development configuration.
// It is focused on developer experience, fast rebuilds, and a minimal bundle.
module.exports = function(webpackEnv) {
...
return {
...
module: {
strictExportPresence: true,
rules: [
...,
// Adds support for CSS Modules, but using SASS
// using the extension .module.scss or .module.sass
{
test: sassModuleRegex,
use: getStyleLoaders(
{
importLoaders: 2,
sourceMap: isEnvProduction && shouldUseSourceMap,
modules: true,
getLocalIdent: getCSSModuleLocalIdent,
},
'sass-loader'
),
},
{
test: stylusRegex,
oneOf: [
{
// Match *.styl?module
resourceQuery: /module/,
use: getStyleLoaders(
{
camelCase: true,
importLoaders: 2,
sourceMap: isEnvProduction && shouldUseSourceMap,
modules: true,
getLocalIdent: getCSSModuleLocalIdent,
},
'stylus-loader'
)
},
{
use: getStyleLoaders(
{
importLoaders: 2,
sourceMap: isEnvProduction && shouldUseSourceMap,
},
'stylus-loader'
)
}
]
},
...
]
}
}
}
复制代码
oneOf用来取其中一个最先匹配到的规则, resourceQuery 用来匹配 import style from 'xxx.styl?module' ,这样需要使用css module就在后面加 ?module ,不需要就直接 import 'xxx.styl' , camelCase: true 是css-loader中的一个配置选项,表示启用驼峰命名,使用css moduels需要通过对象.属性获取编译后样式名称,样式名使用短横线分割,就需要使用属性选择器如style['css-name'],启用驼峰命名后,就可以style.cssName
至此,页面样式就正常了,不过还并未使用到css modules,接着就需要把所有的css改成css modules,这是一个繁琐的过程,就拿Recommend组件来举例
先import样式
import style from "./recommend.styl?module" 复制代码
再通过style对象获取样式
class Recommend extends React.Component {
...
render() {
return (
<div className="music-recommend">
<Scroll refresh={this.state.refreshScroll}
onScroll={(e) => {
/* 检查懒加载组件是否出现在视图中,如果出现就加载组件 */
forceCheck();
}}>
<div>
<div className="slider-container">
<div className="swiper-wrapper">
{
this.state.sliderList.map(slider => {
return (
<div className="swiper-slide" key={slider.id}>
<div className="slider-nav" onClick={this.toLink(slider.linkUrl)}>
<img src={slider.picUrl} width="100%" height="100%" alt="推荐" />
</div>
</div>
);
})
}
</div>
<div className="swiper-pagination"></div>
</div>
<div className={style.albumContainer} style={this.state.loading === true ? { display: "none" } : {}}>
<h1 className={`${style.title} skin-recommend-title`}>最新专辑</h1>
<div className={style.albumList}>
{albums}
</div>
</div>
</div>
</Scroll>
...
</div>
);
}
}
复制代码
有些是插件固定的样名,有些是用来做皮肤切换固定的样名,这些都不能使用css modules,这个时候就需要使用 :global() ,表示全局样式,css-loader就不会处理样式名,如
:global(.music-recommend)
width: 100%
height: 100%
:global(.slider-container)
height: 160px
position: relative
:global(.slider-nav)
display: block
width: 100%
height: 100%
:global(.swiper-pagination-bullet-active)
background-color: #DDDDDD
复制代码
因为加入了eslint,出现了以下警告
./src/components/recommend/Recommend.js Line 131: The href attribute is required for an anchor to be keyboard accessible. Provide a valid, navigable address as the href value. If you cannot provide an href, but still need the element to resemble a link, use a button and change it with appropriate styles. Learn more: https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/anchor-is-valid.md jsx-a11y/anchor-is-valid ./src/components/singer/SingerList.js Line 153: The href attribute is required for an anchor to be keyboard accessible. Provide a valid, navigable address as the href value. If you cannot provide an href, but still need the element to resemble a link, use a button and change it with appropriate styles. Learn more: https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/anchor-is-valid.md jsx-a11y/anchor-is-valid Line 159: The href attribute is required for an anchor to be keyboard accessible. Provide a valid, navigable address as the href value. If you cannot provide an href, but still need the element to resemble a link, use a button and change it with appropriate styles. Learn more: https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/anchor-is-valid.md jsx-a11y/anchor-is-valid 复制代码
这个规则规定a标签必须指定有效的href,把a标签替换成其它即可
ref
之前说过react16.3新增了createRef API,那么就用这个新的API替换ref回调。以Album组件为例
在 constructor 中使用 React.createRef() 初始化
src/views/album/Album.js
class Album extends React.Component {
constructor(props) {
super(props);
// React 16.3 or higher
this.albumBgRef = React.createRef();
this.albumContainerRef = React.createRef();
this.albumFixedBgRef = React.createRef();
this.playButtonWrapperRef = React.createRef();
this.musicalNoteRef = React.createRef();
}
...
}
复制代码
使用ref指定初始化的值
render() {
...
return (
<CSSTransition in={this.state.show} timeout={300} classNames="translate">
<div className="music-album">
<Header title={album.name}></Header>
<div style={{ position: "relative" }}>
<div ref={this.albumBgRef} className={style.albumImg} style={imgStyle}>
<div className={style.filter}></div>
</div>
<div ref={this.albumFixedBgRef} className={style.albumImg + " " + style.fixed} style={imgStyle}>
<div className={style.filter}></div>
</div>
<div className={style.playWrapper} ref={this.playButtonWrapperRef}>
<div className={style.playButton} onClick={this.playAll}>
<i className="icon-play"></i>
<span>播放全部</span>
</div>
</div>
</div>
<div ref={this.albumContainerRef} className={style.albumContainer}>
<div className={style.albumScroll} style={this.state.loading === true ? { display: "none" } : {}}>
<Scroll refresh={this.state.refreshScroll} onScroll={this.scroll}>
<div className={`${style.albumWrapper} skin-detail-wrapper`}>
...
</div>
</Scroll>
</div>
<Loading title="正在加载..." show={this.state.loading} />
</div>
<MusicalNote ref={this.musicalNoteRef}/>
</div>
</CSSTransition>
);
}
复制代码
通过 current 属性获取dom或组件实例,
scroll = ({ y }) => {
let albumBgDOM = this.albumBgRef.current;
let albumFixedBgDOM = this.albumFixedBgRef.current;
let playButtonWrapperDOM = this.playButtonWrapperRef.current;
if (y < 0) {
if (Math.abs(y) + 55 > albumBgDOM.offsetHeight) {
albumFixedBgDOM.style.display = "block";
} else {
albumFixedBgDOM.style.display = "none";
}
} else {
let transform = `scale(${1 + y * 0.004}, ${1 + y * 0.004})`;
albumBgDOM.style.webkitTransform = transform;
albumBgDOM.style.transform = transform;
playButtonWrapperDOM.style.marginTop = `${y}px`;
}
}
复制代码
selectSong(song) {
return (e) => {
this.props.setSongs([song]);
this.props.changeCurrentSong(song);
this.musicalNoteRef.current.startAnimation({
x: e.nativeEvent.clientX,
y: e.nativeEvent.clientY
});
};
}
复制代码
当ref使用在html标签上时,current就是dom元素的引用,当ref使用在组件上时,current就是组件挂载后的实例。组件挂载后current就会指向dom元素或组件实例,组件卸载就会赋值为null,组件更新前会更新ref
Code Splitting
Code Splitting能减少js文件体积,加快文件传输速度,做到按需加载,现在react官方提供了 React.lazy 和 Suspense 来支持Code Splitting,关于它们的详细内容请戳这里
在之前,路由都是直接写在组件中的,现在将路由拆开,在配置文件中统一配置路由,便于集中管理
在src目录下新增router目录,然后新建 router.js
import React, { lazy, Suspense } from "react"
let RecommendComponent = lazy(() => import("../views/recommend/Recommend"));
const Recommend = (props) => {
return (
<Suspense fallback={null}>
<RecommendComponent {...props} />
</Suspense>
)
}
let AlbumComponent = lazy(() => import("../containers/Album"));
const Album = (props) => {
return (
<Suspense fallback={null}>
<AlbumComponent {...props} />
</Suspense>
)
}
...
const router = [
{
path: "/recommend",
component: Recommend,
routes: [
{
path: "/recommend/:id",
component: Album
}
]
},
...
];
export default router
复制代码
在使用 lazy 方法包裹后的组件外层需要用 Suspense 包裹,并指定 fallback , fallback 在组件对应的资源下载时渲染,这里不渲染任何东西,指定 null 。官方示例中,在 Route 外层只用了一个 Suspense ,见此,这里会有子路由,如果在最外层使用一个 Suspense ,子路由懒加载时渲染fallback会把父路由视图组件内容替换,导致父组件页面内容丢失,子路由视图组件渲染完成后,才出现完整内容,中间有一个闪烁的过程,所以最好在每个路由视图组件上都用 Suspense 包裹。你需要将 props 手动传给懒加载组件,这样就能获取react-router中的 match , history 等
上诉使用 Suspense 的部分存在重复代码,我们用高阶组件改造一下
const withSuspense = (Component) => {
return (props) => (
<Suspense fallback={null}>
<Component {...props} />
</Suspense>
);
}
const Recommend = withSuspense(lazy(() => import("../views/recommend/Recommend")));
const Album = withSuspense(lazy(() => import("../containers/Album")));
const router = [
{
path: "/recommend",
component: Recommend,
routes: [
{
path: "/recommend/:id",
component: Album
}
]
},
...
];
复制代码
接下来,使用这些配置
先将一级路由,放到 App 组件中,常规操作就是这样 <Route path="/recommend" component={Recommend} /> ,借助react-router-config,不需要手动写,只需要调用 renderRoutes 方法,传入路由配置即可
注意:路由配置必须使用固定的几个属性,大部分和 Route 组件的props相同
安装react-router-config,这里react-router版本较低,react-router-config也是用了低版本
npm install react-router-config@1.0.0-beta.4 复制代码
src/views/App.js
import { renderRoutes } from "react-router-config"
import router from "../router"
class App extends React.Component {
...
render() {
return (
<Router>
...
<div className={style.musicView}>
{/*
Switch组件用来选择最近的一个路由,否则没有指定path的路由也会显示
Redirect重定向到列表页
*/}
<Switch>
<Redirect from="/" to="/recommend" exact />
{/* 渲染 Route */}
{ renderRoutes(router) }
</Switch>
</div>
</Router>
);
}
}
复制代码
Redirect用来做重定向,需要放到最前面,否则不生效。 renderRoutes 会根据配置生成Route组件类似 <Route path="/recommend" component={Recommend} />
接着在Recommend组件中使用子路由配置
src/views/recommend/Recommend.js
import { renderRoutes } from "react-router-config"
class Recommend extends React.Component {
render() {
let { route } = this.props;
return (
<div className="music-recommend">
...
<Loading title="正在加载..." show={this.state.loading} />
{ renderRoutes(route.routes) }
</div>
);
}
}
复制代码
调用 renderRoutes 后,会把当前层级的路由配置传递给 route ,然后通过 route.routes 获取子路由配置,以此类推子级、子子级都是这样做
renderRoutes源码 见此
还有其它组件路由需要改造,都使用这种方式即可
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- music-dl:从网易云音乐、QQ 音乐、酷狗音乐、虾米音乐等搜索和下载歌曲(Python)
- 中央音乐学院首招音乐人工智能博士!研究 AI + 音乐,他们是认真的
- Python 创作音乐: 计算机创作,计算音乐
- Android开源在线音乐播放器——波尼音乐
- OpenAI发布音乐生成神经网络 MuseNet,可创作4分钟音乐,刚刚还办了场音乐会
- Python 播放音乐:使用 mido 编写,播放多声轨 MIDI 文件音乐
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
The Web Designer's Idea Book, Vol. 2
Patrick McNeil / How / 2010-9-19 / USD 30.00
Web Design Inspiration at a Glance Volume 2 of The Web Designer's Idea Book includes more than 650 new websites arranged thematically, so you can easily find inspiration for your work. Auth......一起来看看 《The Web Designer's Idea Book, Vol. 2》 这本书的介绍吧!