去年写了一款Web音乐App,并发表了系列文章,介绍了开发的过程,当时使用create-react-app
官方脚手架搭建的项目,react-scripts
是1.x
版本,而react版本是16.2.0
,去年10月份create-react-app
已经发布了2.0
版本,react
在去年12月份升级到了16.7.0
css
前端领域的技术迭代更新实在是太快了,常常有人吐槽求不要更新、我学不动了、我学不完了html
作前端就要作好随时学习的准备,否则就会被淘汰啦⊙﹏⊙∥∣°前端
只要是作开发的都要保持一颗积极学习的心,不论是前端领域仍是后端领域,不过前端学习新技术的间隔时间要比后端长。做为Java出身的我深有体会o(╯□╰)ovue
时至今日,create-react-app
更新到了2.x的版本了,主要是升级了它所依赖的许多工具,这些工具已经发布了包含新特性和性能改进的新版本,好比babel7,webpack4,babel7
和webpack4
具体更新了哪些,优化了哪些你们能够去查阅资料。 如下列出来create-react-app
更新了的几个要点node
更多更新内容请戳这里react
由于以前使用的是react16.2,说到react16.7得从16.3提及webpack
16.3新增了几个新的生命周期函数、context API、createRef API和forwardRef API,新增的两个生命周期函数getDerivedStateFromProps
和getSnapshotBeforeUpdate
主要是替代以前的componentWillMount
, componentWillReceiveProps
和componentWillUpdate
,目的是为了支持error boundaries和即将到来的async rendering mode(异步渲染)。当使用async rendering mode时,会中断初始化渲染,错误处理的中断行为可能致使内存泄漏,而使用componentWillMount
, componentWillReceiveProps
和componentWillUpdate
会加大这类问题产生的概率git
在以前的版本,获取dom或组件时,有两种方法,一种是给一个ref,指定一个name,再用refs.name或ReactDOM.findDOMNode(name)获取,另外一种就是使用ref回调,给ref一个回调函数。在开始的时候我用的是第一种,后面改用了ref回调,如今官方不推荐使用了,推荐使用ref回调的方式,由于第一种有几个缺点,使用ref回调有些麻烦,因此官方提供了新的操做就是createRef APIgithub
当使用函数组件时如何获取dom,forwardRef API容许你使用函数组件并传递ref给子组件,这样就能方便的获取子组件中的domweb
更多内容请戳这里
这个版本的更新我仍是很喜欢的,官方终于和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标签替换成其它便可
以前说过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能减小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.codemcx.work
二维码:
以为不错请给个Star,谢谢啦~