从零搭建react全家桶脚手架
init项目
- 创建文件夹并进入
mkdir react-family && cd react-family
- 初始化项目
npm init
按照提示输入信息
webpack
- 安装webpack
npm install webpack --save-dev
--save-dev
只在开发环境中依赖的东西--save
发布之后还依赖的东西 - 配置webpack.dev.config.js文件
const path = require('path') module.exports = { //入口文件,刚才初始化项目填写信息时有写,因人而异 entry:path.join(__dirname, '/src/index.js'), //输出到dist文件夹,输出的名字叫bundle.js output:{ path:path.join(__dirname, './dist'), filename:'bundle.js' } }
- 学会使用webpack编译文件
新建入口文件
mkdir src && touch ./src/index.js
在index.js中添加内容document.getElementById("app").innerHTML = "webpack works"
现在执行命令webpack --config webpack.dev.config.js
可以看到在项目根目录下面生产了dist文件以及bundle.js文件 - 测试
dist
文件夹下面新建一个index.html文件touch ./dist/index.html
并在文件里面写入以下内容
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>wentworth</title>
</head>
<body>
<div id="app"></div>
<script src="./bundle.js"></script>
</body>
</html>
优化命令
修改package.json
里面的script
,增加start
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev-build": "webpack --config webpack.dev.config.js"
}
babel
Babel把最新标准编写的Javascript代码向下编译成可以在今天随处可用的版本。这一过程叫做“源码到源码”的编译,也被称为转换编译 通俗地讲,就是我们可以用ES6,ES7来编写代码,Babel会把他们统统转为ES5
- babel-core 调用Babel的API进行转码
- babel-loader 用来解析文件
- babel-preset-es2015 用于解析ES6
- babel-preset-react 用来解析JSX
- babel-preset-stage-0 用于解析ES7的提案
npm install --save-dev babel-core babel-loader babel-preset-es2015 babel-preset-react babel-preset-stage-0
- 新建
babel
配置文件.babelrc
touch .babelrc
{ "presets": [ "es2015", "react", "stage-0" ], "plugins": [] }
- 修改
webpack.dev.config.js
,增加babel-loader
//src下面以.js结尾的文件,要是用babel解析 //cacheDirectory=true用来缓存编译结果,下次编译加速 module:{ rules:[{ test:/\.js$/, use:['babel-loader?cacheDirectory=true'], include:path.join(__dirname, 'src') }] }
- 修改
src/index.js
const func = str => { document.getElementById('app').innerHTML = str; }; func('我现在在使用Babel!');
- 执行命令
npm run dev-build
react
npm install --save react react-dom
- 修改
src/index.js
,使用reactimport React from 'react' import ReactDom from 'react-dom' ReactDom.render( <div>Hello React</div>, document.getElementById("app") ) export default Home
执行命令
npm run dev-build
查看结果 - 实现组件化
cd src mkdir component cd component mkdir Home cd Home touch Home.js
- 按照React语法,写一个Home组件
import React, {Component} from 'react' class Home extends Component { render() { return( <div className="wwj-home"> Hello React </div> ) } } export default Home
- 修改
src/index.js
引用Home组件import React from 'react' import ReactDom from 'react-dom' import Home from './component/Home/home' ReactDom.render( <Home />, document.getElementById("app") )
- 执行命令
npm run dev-build
查看效果
React-Router
npm install --save react-router-dom
- 新建
module
和app.js
文件 避免在src/index.js
文件中引入大量的组件,可用一个App容器来引入项目中所有的组件,App.js可放在 /src/module
文件夹下面cd src mkdir module cd module touch app.js
import React from 'react' import { BrowserRouter as Router, Route, Switch, } from 'react-router-dom' import Home from '../module/home/home' import Music from '../module/music/music' import Video from '../module/video/video' const App = () => ( <Router> <Switch> <Route path="/" exact component={Home} /> <Route path="/music" exact component={Music} /> <Router path="/video" exact component={Video} /> </Switch> </Router> ) export default App
- 新建对应的组件文件夹和文件
cd src/module mkdir music cd music && touch music.js mkdir home cd home && touch home.js mkdir video cd video && touch video.js
home.js
import React, {Component} from 'react' class Home extends Component { render() { return( <div className="wwj-home"> Hello React </div> ) } } export default Home
music.js
import React, {Component} from 'react' class Music extends Component { render() { return( <div className="wwj-music"> Hello Music </div> ) } } export default Music
video.js
import React, {Component} from 'react' class Video extends Component { render() { return( <div className="wwj-video"> Hello Video </div> ) } } export default Video
- 执行命令
npm run dev-build
打开index.html
文件时没有反应,这是正常的,我们之前用的是绝对路径访问index.html
,不是我们想象中的http://localhost:8080
,那么,这时我们需要一个简单的WEB服务器,指向index.html
webpack-dev-server
简单来说,webpack-dev-server就是一个小型的静态文件服务器,使用它,可以为
webpack
打包生成的资源文件提供Web服务
npm install --save-dev webpack-dev-server -g
需要全局安装
修改webpack.dev.config.js
,增加devServer
配置
devServer:{
contentBase: path.join(__dirname, './dist'),//url根目录,默认指向项目根目录
historyApiFallback: true,//当使用HTML5 History API 时,任意的404响应都可能需要被替代为 index.html
open:true//自动打开浏览器
port:8080//在指定的端口打开
}
webpack-dev-server的其他配置
- color console中打印彩色日志
- proxy代理 比如说在
localhost:3000
端口上有服务的话,可以这么写proxy: { "/api": "http://localhost:3000" }
- progress 将编译进度输出到控制台。
根据这几个配置,
package.json
文件中的scripts
"start": "webpack-dev-server --config webpack.dev.config.js --color --progress"
执行
npm run start
(可能会报错, 无报错此步可以略过) 这个时候需要在webpack.dev.config.js
的配置中增加mode: "development",
,代码如下module.exports = { entry:path.join(__dirname, 'src/index.js'), mode: "development", output:{ path:path.join(__dirname, './dist'), filename:'bundle.js' } }
下一次启动就没问题了
模块热替换(Hot Module Replacement)
到目前为止,当我们修改代码的时候,浏览器会自动刷新,但是,我们并不希望每次修改了代码浏览器都会自动刷新,而是只刷新修改的那部分
接下来我们要这么修改package.json
增加--hot
"start": "webpack-dev-server --config webpack.dev.config.js --color --progress --hot"
src/index.js
增加module.hot.accept()
,模块更新的时候,通知index.js
文件
import React from 'react'
import ReactDom from 'react-dom'
import App from './module/app'
if(module.hot) {
module.hot.accept()
}
const rootNode = document.getElementById('app')
ReactDom.render(
<App />,
rootNode
)
现在启动npm run start
,修改home.js
,打开浏览器,会看到浏览器在没有刷新的情况下页面内容也更新了
HRM配置其实有两种方式,一种是CLI,也就是我们现在用的这种,还有一种是Node.js API方式,功能上都是一样的
But,上面的配置对react不是很友好哦
例如下面的demo
,当模块热替换的时候,state
会重置,这不是我么想要的
修改home.js
,增加计数state
src/module/home/home.js
import React, {Component} from 'react'
class Home extends Component {
state = {
count:0
}
_handleClick = () => {
this.setState({
count:++this.state.count
})
}
render() {
return(
<div className="ww-home">
{this.state.count}
<button onClick={this._handleClick}>自增</button>
</div>
)
}
}
export default Home
文件路径优化
在我们之前写的代码中,我们引用文件的时候用的都是相对路径
比如在src/module/app.js
中引用home.js
时,我们用的就是
import Home from '../module/home/home'
webpack提供了一个别名配置,无论文件在什么位置下,都可以这么引用
import Home from 'mudule/home/home'
修改webpck.dev.config.js
文件,增加别名配置
resolve: {
alias: {
module: path.join(__dirname, 'src/module'),
component: path.join(__dirname, 'src/component'),
}
},
修改src/module/app.js
import Home from 'module/home/home'
import Music from 'module/music/music'
import Video from 'module/video/video'
Redux
接下来我么要集成讲redux
了,需要对redux
有个比较全面的认识,可以去看中文文档
开始撸代码,我们就做一个比较简单的例子,实现计数的自增、自减和重置
npm install --save redux
- 在
src/module/home/
文件夹下面新建文件home.action-creator.js
和home.action-type.js
home.action-type.js
//每个action3种都有3状态,REQUEST、SUCCESS、ERROR export const createActionSet = actionName => ({ REQUEST: `${actionName}_REQUEST`, SUCCESS: `${actionName}_SUCCESS`, ERROR: `${actionName}_ERROR` }) export const INCREMENT = createActionSet('INCREMENT') export const DECREMENT = createActionSet('DECREMENT') export const RESET = createActionSet('RESET')
home.action-creator.js
import * as type from './home.action-type' export const homeActionCreator = type => err => { return { type, payload:err } } export const increMentReq = () => { return { type: type.INCREMENT.REQUEST, } } export const increMentSuccess = () => { return { type: type.INCREMENT.SUCCESS, } } export const increMentError = homeActionCreator(type.INCREMENT.ERROR) export const decreMentReq = () => { return { type: type.DECREMENT.REQUEST, } } export const decreMentSuccess = () => { return { type: type.DECREMENT.SUCCESS, } } export const decreMentError = homeActionCreator(type.DECREMENT.ERROR) export const resetReq = () => { return { type: type.RESET.REQUEST, } } export const resetSuccess = () => { return { type: type.RESET.SUCCESS, } } export const resetError = homeActionCreator(type.RESET.ERROR)
home.reducer.js
import * as type from './home.action-type' const initState = { error: [], isloading: false, count: 0 } const home = (state = initState, action) => { switch (action.type) { case type.INCREMENT.REQUEST: return { ...state, isloading: true, } case type.INCREMENT.SUCCESS: return { ...state, isloading: false, count: state.count+1 } case type.INCREMENT.ERROR: return { ...state, isloading: false, error: state.error.concat(action.payload) } case type.DECREMENT.REQUEST: return { ...state, isloading: true, } case type.DECREMENT.SUCCESS: return { ...state, isloading: false, count: state.count - 1 } case type.DECREMENT.ERROR: return { ...state, isloading: false, error: state.error.concat(action.payload) } case type.RESET.REQUEST: return { ...state, isloading: true, } case type.RESET.SUCCESS: return { ...state, isloading: false, count: 0 } case type.RESET.ERROR: return { ...state, isloading: false, error: state.error.concat(action.payload) } default: return state } } export default home
src/reducer.index.js
import { combineReducers } from 'redux' import { home } from 'module/home/home.reducer' const rootReducer = combineReducers({ home }) export default rootReducer
到这里,我们必须理解下面一句话
reducer
就是纯函数,接收state
和action
,然后返回一个新的state
接下来我们要创建一个store
store
是用来干什么的? 我们用action
来描述发生了什么,用reducer
来根据action
更新state
那么我们如何提交action
,提交的时候如何触发reducer
呢store
就是把他们联系在一起的对象,store
有以下职责:- 维持应用的
state
- 提供
getState()
方法获取state
- 提供
dispatch
方法触发reducer
更新state
- 通过
subscribe(listener)
注册监听器
- 维持应用的
在src
文件夹下面新建store.index.js
src/store.index.js
import {createStore} from 'redux'
import rootReducer from './reducer.index'
const store = createStore(
rootReducer
)
export default store
React-Redux
另外我们还需要安装一个东西react-redux
react-redux
将react
和redux
联系起来
- 安装
react-redux
npm install --save react-redux
- 修改
src/module/home/home.js
import React, {Component} from 'react' import {connect} from 'react-redux' import * as homeAction from './home.action-creator' class Home extends Component { _increMent = () => { const {dispatch} = this.props dispatch(homeAction.increMentReq()) dispatch(homeAction.increMentSuccess()) } _decreMent = () => { const {dispatch} = this.props dispatch(homeAction.decreMentReq()) dispatch(homeAction.decreMentSuccess()) } _reset = () => { const {dispatch} = this.props dispatch(homeAction.resetReq()) dispatch(homeAction.resetSuccess()) } render() { const {home:{count}} = this.props return( <div className="ww-home"> {count} <button onClick={this._increMent}>自增</button> <button onClick={this._decreMent}>自减</button> <button onClick={this._reset}>重置</button> </div> ) } } const mapStateToProps = ({home}) => { return { home } } export default connect(mapStateToProps)(Home)
Redux-Saga
发起异步action
- 安装
redux-saga
npm install --save redux-saga
- 在
src/module/home
文件夹下新建home.saga.js
import {take, put, fork, select, cancel, all} from 'redux-saga/effects' import * as type from './home.action-type' import * as action from './home.action-creator' function* handleIncrement() { while(true) { try { yield take(type.INCREMENT.REQUEST) yield put(action.increMentSuccess()) } catch (error) { console.log(error) } } } function* handleDecrement() { while(true) { try { yield take(type.DECREMENT.REQUEST) yield put(action.decreMentSuccess()) } catch (error) { console.log(error) } } } function* handleReset() { while(true) { try { yield take(type.RESET.REQUEST) yield put(action.resetSuccess()) } catch (error) { console.log(error) } } } export default function* homeSaga() { yield all([ fork(handleIncrement), fork(handleDecrement), fork(handleReset) ]) }
- 在
src
文件夹下新建saga.index.js
import {all} from 'redux-saga/effects' import homeSaga from 'module/home/home.saga' export default function* rootSaga() { yield all([ homeSaga() ]) }
- 修改
store.index.js
文件import { createStore, applyMiddleware } from 'redux' import createSagaMiddleware from 'redux-saga' import rootReducer from './reducer.index' import rootSaga from './saga.index' const sagaMiddleware = createSagaMiddleware() const store = createStore( rootReducer, applyMiddleware( sagaMiddleware ) ) sagaMiddleware.run(rootSaga) export default store
- 修改
scr/module/home/home.js
import React, {Component} from 'react' import {connect} from 'react-redux' import * as homeAction from './home.action-creator' class Home extends Component { _increMent = () => { const {dispatch} = this.props dispatch(homeAction.increMentReq()) dispatch(homeAction.increMentSuccess()) } _decreMent = () => { const {dispatch} = this.props dispatch(homeAction.decreMentReq()) } _reset = () => { const {dispatch} = this.props dispatch(homeAction.resetReq()) } render() { const {home:{count}} = this.props return( <div className="ww-home"> {count} <button onClick={this._increMent}>自增</button> <button onClick={this._decreMent}>自减</button> <button onClick={this._reset}>重置</button> </div> ) } } const mapStateToProps = ({home}) => { return { home } } export default connect(mapStateToProps)(Home)
- 启动项目
npm run start
这个时候可能在控制台会出现下图错误 这个时候是因为你还没有安装babel-polyfill
,安装babel-ployfill
npm install --save babel-polyfill
,并在src/index.js
引入import 'babel-polyfill'
重新刷页面错误解决
devtool优化
我们发现一个问题,不管代码错在哪里,浏览器只是报错误在bundle.js
第几行
这时需要在webpack.dev.config.js
增加配置
devtool: 'inline-source-map'
编译CSS
npm install --save-dev css-loader style-loader
webpack.dev.config.js
增加rules
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
}
编译图片
npm install --save-dev url-loader file-loader
webpack.dev.config.js
增加rules
{
test: /\.(png|jpg|gif)$/,
use: [{
loader: 'url-loader',
options: {
limit: 8192
}
}]
}
options limit 8192
意思是,小于等于8K
的图片会被转成base64
编码,直接插入HTML中,减少HTTP请求。
按需加载
为什么要实现按需加载?
我们现在看到,打包完后,所有页面只生成了一个build.js,当我们首屏加载的时候,就会很慢。因为他也下载了别的页面的js了哦。
如果每个页面都打包了自己单独的JS
,在进入自己页面的时候才加载对应的js
,那首屏加载就会快很多哦。
npm install --save-dev bundle-loader
- 在
src/component
文件夹下新建asyncRoute.js
import React, { Component } from 'react' class AsyncRoute extends Component { state = { mod: null } componentWillMount() { this.load(this.props) } componentWillReceiveProps(nextProps) { if (nextProps.load !== this.props.load) { this.load(nextProps) } } load(props) { this.setState({ mod: null }) props.load((mod) => { this.setState({ // handle both es imports and cjs mod: mod.default ? mod.default : mod }) }) } render() { if (!this.state.mod) return false return this.props.children(this.state.mod) } } export default AsyncRoute
- 修改
src/module/app.js
文件import React, { Component } from 'react' import { BrowserRouter as Router, Route, Switch, } from 'react-router-dom' import Home from 'bundle-loader?lazy&name=home!module/home/home' import Music from 'bundle-loader?lazy&name=music!module/music/music' import Video from 'bundle-loader?lazy&name=video!module/video/video' import AsyncRoute from 'component/asyncRoute' const Loading = () => { return <div>Loading...</div> } const createComponent = (component) => (props) => ( <AsyncRoute load={component}> { (Component) => Component ? <Component {...props} /> : <Loading /> } </AsyncRoute> ); const App = () => ( <Router> <Switch> <Route path="/" exact component={createComponent(Home)} /> <Route path="/music" exact component={createComponent(Music)} /> <Router path="/video" exact component={createComponent(Video)} /> </Switch> </Router> ) export default App
缓存
修改webpack.dev.config.js
output: {
path: path.join(__dirname, './dist'),
filename: '[name].[hash].js',
chunkFilename: '[name].[chunkhash].js'
}
HtmlWebpackPlugin
npm install html-webpack-plugin --save-dev
修改webpack.dev.config.js
var HtmlWebpackPlugin = require('html-webpack-plugin');
plugins: [new HtmlWebpackPlugin({
filename: 'index.html',
template: path.join(__dirname, 'src/index.html')
})],
}