脚手架创建应用
当今web开发其实比较复杂,开发一个项目时通常要引入多个技术栈(UI:react, 状态管理:redux,路由:react/router,js特性翻译:babel,打包:webpack,语法检查:eslint),每个技术栈背后有很多package并且涉及很多配置,过程复杂且可重用,脚手架工具可以简单且快速地生成一个空的项目来开发。最常用的脚手架为create-react-app。另外还有一些脚手架(rekkit:集成了redux和react-router,codeSandbox:在线打包编译热加载)可以尝试。
打包部署
最常用的打包工具webpack,具体执行流程优化,源码分析包括热重载如何实现等涵盖内容较多后面争取补充。
为什么需要打包?
- 编译ES6语法特性,编译JSX
- 整合资源(图片、Less/Sass)
- 优化代码体积
注意事项
- 设置nodejs环境为production(这里因为有些js library不同环境运行不一样,比如react会在开发环境检查传参类型)
设置环境变量在build.js中:process.env.BABEL_ENV = 'production';
process.env.NODE_ENV = 'production';
- 禁用开发专用代码,比如logger
- 设置根路径(在package.json的“homepage”属性中)
执行打包:npm run build
redux状态管理
redux是基于flux的设计模式提出的状态管理框架,react将state转换为DOM,redux将这个过程放到组件之外的store中,当store中信息变化,组件也会进行相应的更新,也就是担当了管理全局状态和让组件通信更容易(传统一般是父组件通过props传参给子组件,子组件暴露一个事件回传参数给父组件)的职责。redux通过action来描述发生了什么(充当指示器),reducer用来串联state和action。
三大原则
- Single source of truth
传统MVC的model和view互相调用,错从复杂,redux的所有数据来源都是store - State is read-only
state + action = new state,必须action来生成而不是改变原有的state - Changes are made with pure funcitons
pure function的函数输出结果完全取决于输入参数,不依赖于任何外部参数或者外部资源
function visibilityFilter(state = 'SHOW_ALL', action) {
switch (action.type) {
case 'SET_VISIBILITY_FILTER':
return action.filter
default:
return state
}
}
function todos(state = [], action) {
switch (action.type) {
case 'ADD_TODO':
return [
...state,
{
text: action.text,
completed: false
}
]
case 'COMPLETE_TODO':
//通过state.map生成新的state对象而不是修改原有state对象
return state.map((todo, index) => {
if (index === action.index) {
return Object.assign({}, todo, {
completed: true
})
}
return todo
})
//如果不是ADD或者COMPLETE类型的action直接返回state
default:
return state
}
}
//combineReducers返回一个封装函数,将reducer合并
import { combineReducers, createStore } from 'redux'
const reducer = combineReducers({ visibilityFilter, todos })
const store = createStore(reducer)
理解Store
const store = createStore(reducer)
- getState()
- dispatch(action)
- subscribe(listener)
Dispatcher用来派发action操作,State是数据对象,Reducer更新state,(state, action) => new state,state的任何变化都一定是由一个action引起的,一旦发现state有问题可以追踪action是什么方便定位问题。redux利用中间件来截获和发出action,可以通过log打印出来追踪action变化。redux中的action可以通过组合多个 同步action的方式来实现异步action,比如:
export function saveFile(filePath, content) {
return (dispatch) => { // optionally you can have getState as the second argument
dispatch({
type: HOME_SAVE_FILE_BEGIN,
});
//通过返回一个promise来控制UI flow而不是通过state
//下面场景中,提交form后如果成功跳转到另外的页面,失败则显示错误信息,如果用state来控制是比较困难的,但是通 过一个promise就比较容易达成目的
const promise = new Promise((resolve, reject) => {
const doRequest = axios.post('/rekit/api/save-file', {
file: filePath,
content,
});
doRequest.then(
(res) => {
dispatch({
type: HOME_SAVE_FILE_SUCCESS,
data: {
file: filePath,
content,
},
});
resolve(res);
},
// Use rejectHandler as the second argument so that render errors won't be caught.
(err) => {
dispatch({
type: HOME_SAVE_FILE_FAILURE,
data: { error: err },
});
reject(err);
},
);
});
return promise;
};
}
不同功能的action和reducer一般存放在独立文件中再统一引入到一个index.js中,这种文件管理方式基本和vue-store的风格一致。
reudx的运行基础:不可变数据(immutable data),即store必须经过深拷贝或浅拷贝才可修改,不可再原数据基础上做修改,这样做的目的是:
- 性能优化:不需要比较前后的值来判断是否有更新,而是直接比较引用是否发生改变
- 易于调试和跟踪:store变化时由于记录了old state和new state的值,可以进行diff比较
- 易于推测:可以通过action值推测执行过程
如何操作不可变数据:
- 原生:{...}, Object.assign
const state = {filter: 'completed', todos: ['Learn React']};
//写法1
const newState = {...state, todos: [
...state.todos,
'Learn Redux'
]};
//写法2
const newState2 = Object.assign({}, state, todos: [
...state.todos,
'Learn Redux'
]);
- immutability-helper工具类
import udpate from 'immutability-helper';
const state = {filter: 'completed', todos: [
'Learn React'
]};
const newState = udpate(state, {todos: {$push: ['Learn Redux']}});
- immer(性能稍差)
import produce from 'immer';
const state = {filter: 'completed', todos: [
'Learn React'
]};
//draftState相当于是一个代理state,其实依然没有在原state上修改,但是操作起来比较像
const newState = produce(state, draftState => {
draftState.todos.push('Learn Redux');
});
react-router
非官方出品,但是已经成为公认的路由管理标准。
- 单页应用需要页面切换
- 通过URL可以定位到页面
- 更有语义的组织资源
路由 -----> Router -----> 组件容器,
特性:1、声明式理由 2、动态路由
实现:1、URL路径(BroswerRouter) 2、hash路由(HashRouter,当浏览器不支持路由切换页面不刷新的时候使用) 3、内存路由(MemoryRouter,一般用在服务端渲染)
import React from "react";
import { HashRouter as Router, Route, Link } from "react-router-dom";
import { MemoryRouter } from "react-router";
const Home = () => <h1>Home</h1>;
const Hello = () => <h1>Hello</h1>;
const About = () => <h1>About Us</h1>;
export default class RouterSample extends React.PureComponent {
render() {
// react-router其实是组件的一部分
return (
<Router>
<div>
<ul id="menu">
<li>
<Link to="/home">Home</Link>
</li>
<li>
<Link to="/hello">Hello</Link>
</li>
<li>
<Link to="/about">About</Link>
</li>
</ul>
<div id="page-container">
<Route path="/home" component={Home} />
<Route path="/hello" component={Hello} />
<Route path="/about" component={About} />
</div>
</div>
</Router>
);
}
}
react-router API介绍:https://reactrouter.com/web/a...
路由参数:
//传参
<Route path="/topic/:id" component={Topic} />
//获取参数
const Topic = ({ match }) => (
<h1>Topic {match.params.id}</h1>
);
嵌套路由
场景:除了导航栏可能还有多级子导航栏,嵌套路由是一个前端特有的概念。只要match path,组件就会render出来。
//假设浏览器输入一个url为: xxxx/category/3/sub/2
import React from "react";
import {
BrowserRouter as Router,
Route,
Link
} from "react-router-dom";
const Category = ({ match }) => (
<h1>Sub Category {match.params.subId}</h1>
);
const SubCategory = ({ match }) => (
<div>
<h1>Category {match.params.id}</h1>
<ul id="menu">
<li>
<Link to={`/category/${match.params.id}/sub/1`}>
Sub Category 1
</Link>
</li>
<li>
<Link to={`/category/${match.params.id}/sub/2`}>
Sub Category 2
</Link>
</li>
<li>
<Link to={`/category/${match.params.id}/sub/3`}>
Sub Category 3
</Link>
</li>
</ul>
<div id="page-container-2">
<!--xxxx/category/3/sub/2 (path完全匹配) -->
<Route
path="/category/:id/sub/:subId"
component={Category}
/>
</div>
</div>
);
export default class NestedRoute extends React.PureComponent {
render() {
return (
<Router>
<div>
<ul id="menu">
<li>
<Link to="/category/1">Category 1</Link>
</li>
<li>
<Link to="/category/2">Category 2</Link>
</li>
<li>
<Link to="/category/3">Category 3</Link>
</li>
</ul>
<div id="page-container">
<!--xxxx/category/3/sub/2 会首先匹配到顶层容器的路由(path部分匹配) -->
<Route
path="/category/:id"
component={SubCategory}
/>
</div>
</div>
</Router>
);
}
}