去年寫了一款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,謝謝啦~