内容简介:去年写了一款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 文件音乐
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。