从头开始建立一个React App - 项目基本配置
npm init
生成package.json
文件.- 安装各种需要的依赖:
npm install --save react
- 安装React.npm install --save react-dom
安装React Dom,这个包是用来处理virtual DOM。这里提一下用React Native的话,这里就是安装react-native。npm install --save-dev webpack
- 安装Webpack, 现在最流行的模块打包工具.npm install --save-dev webpack-dev-server
- webpack官网出的一个小型express服务器,主要特性是支持热加载.npm install --save-dev babel-core
- 安装Babel, 可以把ES6转换为ES5,注意Babel最新的V6版本分为babel-cli和babel-core两个模块,这里只需要用babel-cor即可。- 安装其他的babel依赖(babel真心是一个全家桶,具体的介绍去官网看吧..我后面再总结,这里反正全装上就是了):
npm install --save babel-polyfill
- Babel includes a polyfill that includes a custom regenerator runtime and core.js. This will emulate a full ES6 environmentnpm install --save-dev babel-loader
- webpack中需要用到的loader.npm install --save babel-runtime
- Babel transform runtime 插件的依赖.npm install --save-dev babel-plugin-transform-runtime
- Externalise references to helpers and builtins, automatically polyfilling your code without polluting globals.npm install --save-dev babel-preset-es2015
- Babel preset for all es2015 plugins.npm install --save-dev babel-preset-react
- Strip flow types and transform JSX into createElement calls.npm install --save-dev babel-preset-stage-2
- All you need to use stage 2 (and greater) plugins (experimental javascript).
打开
package.json
然后添加下面的scripts:-
"scripts": {
-
"start": "webpack - dev - server - - hot - - inline - - colors - - content - base . /build" ,
-
"build": "webpack - - progress - - colors"
-
}
命令行输入
npm start
将要启动webpack dev server.命令行输入
npm build
将会进行生产环境打包.-
启动webpack
Webpack是我们的打包工具,在我们的开发环境中具体很重要的作用,具有很多非常便捷的特性,尤其是热加载hot reloading.
webpack.config.js
是如下所示的webpack的配置文件. 随着app的不断变化,配置文件也会不断的更新,这里我们就用默认的webpack.config.js
来命名这个配置文件,假如你用别的名字比如webpack.config.prod.js
那么上面的脚本build就需要相应的改变指定相应的配置文件名字:"build": "webpack webpack.config.prod.js --progress --colors"
-
module.exports = {
-
output: {
-
filename: "bundle.js"
-
module: {
-
test: /\.js$/,
-
loader: 'babel-loader',
-
plugins: ['transform-runtime'],
-
}
-
test: /\.css$/,
-
}]
-
};
- OK,我们项目的基本配置终于完成了,是时候开始写Reac代码了.
-
React 基础 - 建立你的第一个Component
在上面的项目的基本配置基础上,我们开始书写React的第一个组件来熟悉React的写法与组件思想。
首先我们在项目根目录中新建一个
index.html
文件。 在这个基础工程中, 我们使用bootstrap的样式,直接引入一个cdn即可. 然后添加一个html标签<div id="app"></div>
,我们的app就会注入到这个div中。 最后再引入<script src="bundle.js"></script>
,这是最后打包生成的js代码。以下是完整的代码:
-
-
<html lang="en">
-
<head>
-
<meta charset="UTF-8">
-
<title>Document </title>
-
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css">
-
</head>
-
<body>
-
<div id="app"> </div>
-
<script src="bundle.js"> </script>
-
</body>
-
</html>
-
- 建立一个新的文件夹
src
. 我们app的大部分代码都将放在这个文件夹里面。在src
中建立app.js
,作为React App的根组件, 其他所有的组件都会注入到这个跟组件中。 首先我们需要导入react,现在都已经用ES6的语法,
import React from 'react';
, 然后我们要引入react-dom. 这里面有react中最重要的一个虚拟dom的概念.引入代码:import ReactDOM from 'react-dom';
现在需要引入的依赖都已经完毕我们可以写第一个组件了:
-
render(){ // Every react component has a render method.
-
<div>
-
</div>
-
}
-
class ToDoApp extends React.Component {
-
return (
-
);
-
}
-
为了将这个组件注入到我们的APP中, 首先我们需要输出它。 在这个组件代码底部添加
export default ToDoApp;
。然后在
app.js
顶部我们添加import ToDoApp from '.components/ToDoApp';
导入组件用来代替Hello World
。 render中替换为新的jsx代码<ToDoApp />
半闭合类型的标签即可。然后在浏览器中你就可以看到"To Do App" 代替了原来的 "Hello World"!这样我们就完成了将第一个子组件嵌入到根组件之中了,这就是构建react app的常规模式。下面继续完善我们的组件。
返回到
ToDoApp
中来构建我们的第一个代办事项列表。首先我们使用bootstrap来构建比较方便且美观。 用下面的jsx替换当前render
方法中return
中的jsx:-
<div className="row">
-
<div className="col-md-10 col-md-offset-1">
-
<div className="panel panel-default">
-
<div className="panel-body">
-
<h1>My To Do App </h1>
-
<hr/>
-
List goes here.
-
</div>
-
</div>
-
</div>
-
</div>
-
现在打开浏览器, 你将会看到一个标题 "My To Do App" 下面跟随一个bootstrap的panel组件里面写有 "List Goes Here",我们将在这个地方构建列表。 那么我们如何将数据存储在我们的列表中呢? 答案就是使用
state
. 每一个类组件都有state
属性,可以通过this.state
在组件任何位置获取并且用this.setState({ key: "value" })
这种方法来更新状态。但是除非必要我们比较少使用state
,这里暂时先使用作为了解,后期会使用redux来管理状态。在
ToDoApp
中我们可以使用许多生命周期方法的钩子, 其中一个就是componentWillMount
。 这个方法的执行是在页面加载并且render方法之前。可以在其中获取列表数据,在我们的APP中直接用一个虚拟的数组提供。(值得注意的是componentWillMount会引起很多小问题,因此真实项目中尽量不要使用,而是应该用componentDidMount)。
在ToDoApp
中render
方法之前添加:-
this.setState({ // add an array of strings to state.
-
})
-
const List = (props) => { // we're using an arrow function and const variable type, a ES6 features
-
return (
-
I'm a list!!!
-
)
-
-
<code class="javascript" ,monospace;="" font-size:12px;="" background-color:transparent;="" padding:0px;="" border:none"="" style="outline: 0px; padding: 8px; font-family: Menlo, Monaco, Consolas, "Courier New"; word-break: break-all;"> export default List;
-
在
ToDoApp.js
引入 List用List
组件替换List goes here.
,写法为<List />
.现在在浏览器中就可以看到"I'm a list!!!"现在我们来把这个变成真实的列表,首先就需要通过props传递数据,我们把这个从state中获取的数据list通过命名为listItems的props传递,写作:
<List listItems={this.state.list} />
,现在List
已经通过props获取了ToDoApp
中的数据。然后在
List
组件中我们需要render一个列表,先用下面的jsx代码代替:-
<ul>
-
list // this is a variable we'll define next
-
</ul>
-
const list = props.listItems.map((el, i)=>(
-
// (which is stored in the state of the parent component)
-
// which returns a new array of list items. The key attribute is
-
<li key={i}><h2>el</h2></li>
-
import React from 'react';
-
const List = (props) => {
-
const list = props.listItems.map((el, i)=>(
-
));
-
return (
-
<ul>
-
list
-
</ul>
-
)
-
-
componentWillMount(){
-
list: ['thing1', 'thing2', 'thing3'],
-
})
-
onInputChange = (event) => {
-
};
6. 添加新列表事项
现在需要向列表中添加新的事项,也就是在提交后能把输入框的值存储并显示到列表中。我们需要再新建一个
onInputSubmit
的方法,参数同样是event
,函数体内首先需要写event.preventDefault()
,然后用setState
方法把新事项添加到列表数组中,但是,一定要注意我们的state应该是immutable的,这是react中必须遵循的一个准则,这样才能保证对比性与可靠性。为了实现这个功能, 需要用到
this.setState
回调函数,参数为previousState
:-
list: previousState.list.push(previousState.newToDo)
-
this.setState((previousState)=>({
-
}));
在提交添加新事项的同时,需要将
newToDo
重置为''
:-
list: [...previousState.list, previousState.newToDo ],
-
}));
7. 划掉事项
是时候添加划掉事项的功能了。为了实现这个功能需要添加一个新的属性用来标注是否需要划掉,因此需要改变原来的数组为一个对象数组,每一个事项都是一个对象,一个key为
item
表示原来的事项内容,一个key为done
用布尔值来表示是否划掉。 然后先把原来的onInputSubmit
方法修改,同样要注意immutable,使用扩展操作符如下:-
event.preventDefault();
-
list: [...previousState.list, {item: previousState.newToDo, done: false }], // notice the change here
-
}));
-
onListItemClick = (i) => { // takes the index of the element to be updated
-
list: [
-
Object.assign({}, previousState.list[i], {done: !previousState.list[i].done}), // Object.assign is a new ES6 feature that creates a new object based on the first param (in this case an empty object). Other objects can be passed in and will be added to the first object without being modified.
-
]
-
};
然后把这个方法通过props传递给
List
组件,这里就没有使用解构参数传递,用来和Input
的做对比。因为这个函数需要一个参数就是当前列表的序列号,但是肯定不能直接call这个函数否则会报错,因此使用bind方法,出入i
参数:onClick={() => props.onClick(i)}
然后在事项内容的
span
标签上添加onClick
方法,改变当前事项的done
值后,在通过判断此布尔值来进行样式的修改添加或者划掉删除线。-
style={
-
? {textDecoration: 'line-through', fontSize: '20px'}
-
}
-
>
8. 删除事项
最后我们在添加删除事项的功能,这个和划掉事项非常相似,我们只需要新增一个删除按钮,然后再新增一个方法修改列表,具体代码如下:
-
className="btn btn-danger pull-right"
-
x
-
deleteListItem = (i) => {
-
list: [
-
...previousState.list.slice(i+1) // the only diffence here is we're leaving out the clicked element
-
}))
-
npm install --save redux
-
npm install --save redux-logger
还有一些常用的中间件,比如
redux-thunk
andredux-promise
, 但是在我们的这个项目中暂时先不需要,可以自行去github了解。2. 构建
使用redux构建react应用一般都有一个标准的模板,可能不同模板形式上有区别,但是思想都是一样的,下面就先按照一种文件结构来构建。
首先我们在
src
中新建一个文件夹redux
,然后在其中新建一个文件configureStore.js
,添加以下代码:-
import createLogger from 'redux-logger';
createStore
是由redux提供的用来初始化store的函数,applyMiddleware
是用来添加我们需要的中间件的。combineReducers
用来把多个reducers
合并为一个单一实体。createLogger
就是我们这里唯一使用的一个中间件,可以console
出每一个action
后数据的详细处理过程,给调试带来了很大方便。然后添加下面代码:
-
-
const initialState = {}; //The initial state of this reducer (will be combined with the states of other reducers as your app grows)
-
export default function reducer(state = initialState, action){ // a function that has two parameters, state (which is initialized as our initialState obj), and action, which we'll cover soon.
-
default:
-
}
-
const reducer = combineReducers({
-
});
最后在底部加入下面完整的代码:
-
export default configureStore;
Cool. We're done here.
5. Connect
现在我们已经有了一个
reducer
,那么怎么和app建立联系呢?这需要两步工作。前面已经讲过类组件和函数型组件,有时候也可以称为
smart components
和dumb components
,这里我们新增一种容器组件,顾名思义,这种组件就是作为一个容器用来给组件提供actions
和state
。下面来创建第一个容器组件,首先在
/src/
下新增一个文件夹containers
,然后再其下面新建一个文件toDoAppContainer.js
。
在文件顶部首先导入connect
用来将容器和组件联系在一起,-
import ToDoApp from '../components/ToDoApp.js'
connect
这个函数被调用两次, 第一次是两个回调函数:mapStateToProps
andmapDispatchToProps
。 第二次是把state
和dispatch
传入组件的时候。这里的dispatch
又是什么呢?当我们需要在redux中发生某些行为时候,就需要调用
dispatch
函数传递一个action
然后调用reducer
这一套流程。因为我们还没有编写具体的行为,这里就暂时空白,后面再补,代码形式如下:-
return {
-
}
-
-
return {}; // here we'll soon be mapping actions to props
-
export default connect(
-
mapDispatchToProps
-
import { Provider } from 'react-redux';
-
import configureStore from './redux/configureStore';
configureStore
is the function we created that takes our combined reducers and our redux middleware and mashes them all together. Let's intialize that with the following line:<Provider store={store}> // we pass the store through to Provider with props
-
-
</Provider>
现在整个redux的基本结构已经搭建起来,下一步就可以把整个行为逻辑代码补充进去就可以了。
Redux Actions 和 Reducers
搭建起redux的基本结构后,就可以填充redux的元素了,简单来说我们只需要记住四个概念, Types
, Actions
, Action Creators
, and Reducers
。然后把这些元素用ducks
的文件组织结构组织起来就可以了。
Ducks
规则
在module中我们需要遵循下面的代码风格和命名方式:
- 须用
export default
输出名为reducer()
的函数 - 须用
export
输出 函数形式的action creators
- 须用
npm-module-or-app/reducer/ACTION_TYPE
的命名形式来命名action types
,因为到后期很多reducer,不同的人协同工作难免会出现命名重复,这样子加上app和模块的前缀的话就不会出现命名冲突的问题。 - 须用大写的蛇形方式
UPPER_SNAKE_CASE
来命名action types
。
Types
这个types
就是上面第三条中需要按照ducks
的规范命名的常量名字,将其写在文件的顶部,当action
触发时候会传递给reducer
,reducer
的switch语句会根据这个type来进行相应的数据处理。
-
const DELETE_ITEM = 'my-app/toDoApp/DELETE_ITEM';
Actions
Actions
就是一个至少包含type
的简单的js对象,同时可以包含数据以便传递给reducer
。当用户在页面上触发了某种行为,一个aciton creator
将会发送aciton
给reducer
做数据处理。
action
示例如下:
-
{ type: DELETE_ITEM, index: 1 }
-
function addItem(item){
-
type: ADD_ITEM,
-
}
-
const initialState = {
-
};
-
export default function reducer(state = initialState, action){
-
case ADD_ITEM:
-
{},
-
{ list: [...state.list, action.item]} // here we see object.assign again, and we're returning a new state built from the old state without directly manipulating it
-
default:
-
}
-
const initialState = {
-
newToDo: ''
-
-
switch (action.type){
-
return state;
-
}
现在在 ToDoApp.js
的 render()
方法中return
之前添加console.log(this.props)
会打印出下面的对象:
-
list: Array[1]
-
length: 1
-
__proto__: Object
-
<List
-
listItems={this.props.toDoApp.list}
-
/>
-
value={this.props.toDoApp.newToDo}
-
onSubmit={this.onInputSubmit}
-
const INPUT_CHANGED = 'INPUT_CHANGED';
-
export function inputChange(newToDo){
-
type: INPUT_CHANGED,
-
}
-
case INPUT_CHANGED:
-
{},
-
{newToDo: action.value}
-
import { connect } from 'react-redux';
-
import {
-
} from '../redux/modules/toDoApp'; // we added this
-
function mapStateToProps(state) {
-
toDoApp: state.toDoApp // gives our component access to state through props.toDoApp
-
}
-
function mapDispatchToProps(dispatch) {
-
inputChange: (value) => dispatch(inputChange(value)) // we added this
-
}
-
export default connect(
-
mapDispatchToProps
-
<code class="javascript" ,monospace;="" font-size:12px;="" background-color:transparent;="" padding:0px;="" border:none"="" style="outline: 0px; padding: 8px; font-family: Menlo, Monaco, Consolas, "Courier New"; word-break: break-all;">)(ToDoApp);
这样state和action都传递给了toDoApp
然后再通过props传递给子组件就可以使用了,具体都可以看项目最终代码。
4. 其他 actions
其他acitons的代码模式跟上面的基本一样,这里不在赘述。
总结
到这里一个使用webpack打包的react+redux(ducks)的基本应用模型就出来了,虽然简单但是是我们进行更复杂项目的基础,并且有了这些基础后面的路程将会顺畅多了,一起加入react的大家庭吧。