|
源码地址:https://github.com/brickspert/react-family 因为本教程写于2017年9月,然而前端技术发展太快了。有些库的版本一直在升级,所以你如果碰到奇怪的问题,请先检查下安装的库版本是否和我源码中的一样。please~大家阅读的时候,照着目录来阅读哦,有些章节不在文章里面。要点链接的~目录
写在前面 当我第一次跟着项目做 做项目,总是要解决各种问题的,所以每个地方都需要去了解,但是对整个框架没有一个整体的了解,实在是不行。 期间,我也跟着别人的搭建框架的教程一步一步的走,但是经常因为自己太菜,走不下去。在经过各种蹂躏之后,对整个框架也有一个大概的了解, 我的这个教程,从新建根文件夹开始,到成型的框架,每个文件为什么要建立?建立了干什么?每个依赖都是干什么的?一步一步写下来,供大家学习。 当然,这个框架我以后会一直维护的,也希望大家能一起来完善这个框架,如果您有任何建议,欢迎在这里留言,欢迎 我基于该框架 说明
cd src/pages mkdir Home
│ .babelrc #babel配置文件
│ package-lock.json
│ package.json
│ README.MD
│ webpack.config.js #webpack生产配置文件
│ webpack.dev.config.js #webpack开发配置文件
│
├─dist
├─public #公共资源文件
└─src #项目源码
│ index.html #index.html模板
│ index.js #入口文件
│
├─component #组建库
│ └─Hello
│ Hello.js
│
├─pages #页面目录
│ ├─Counter
│ │ Counter.js
│ │
│ ├─Home
│ │ Home.js
│ │
│ ├─Page1
│ │ │ Page1.css #页面样式
│ │ │ Page1.js
│ │ │
│ │ └─images #页面图片
│ │ brickpsert.jpg
│ │
│ └─UserInfo
│ UserInfo.js
│
├─redux
│ │ reducers.js
│ │ store.js
│ │
│ ├─actions
│ │ counter.js
│ │ userInfo.js
│ │
│ ├─middleware
│ │ promiseMiddleware.js
│ │
│ └─reducers
│ counter.js
│ userInfo.js
│
└─router #路由文件
Bundle.js
router.js
init项目
webpack
babel
通俗的说,就是我们可以用ES6, ES7等来编写代码,Babel会把他们统统转为ES5。
{
"presets": [
"es2015",
"react",
"stage-0"
],
"plugins": []
}
修改 /*src文件夹下面的以.js结尾的文件,要使用babel解析*/
/*cacheDirectory是用来缓存编译结果,下次编译加速*/
module: {
rules: [{
test: /\.js$/,
use: ['babel-loader?cacheDirectory=true'],
include: path.join(__dirname, 'src')
}]
}
现在我们简单测试下,是否能正确转义ES6~ /*使用es6的箭头函数*/
var func = str => {
document.getElementById('app').innerHTML = str;
};
func('我现在在使用Babel!');
执行打包命令 浏览器打开 然后我们打开打包后的 Q: A: 每一级包含上一级的功能,比如 参考地址: react
修改 import React from 'react';
import ReactDom from 'react-dom';
ReactDom.render(
<div>Hello React!</div>, document.getElementById('app'));
执行打包命令 打开 我们简单做下改进,把 cd src mkdir component cd component mkdir Hello cd Hello touch Hello.js 按照React语法,写一个Hello组件 import React, {Component} from 'react';
export default class Hello extends Component {
render() {
return (
<div>
Hello,React!
</div>
)
}
}
然后让我们修改
import React from 'react';
import ReactDom from 'react-dom';
import Hello from './component/Hello/Hello';
ReactDom.render(
<Hello/>, document.getElementById('app'));
在根目录执行打包命令
打开 命令优化 Q:每次打包都得在根目录执行这么一长串命令 A:修改
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev-build": "webpack --config webpack.dev.config.js"
}
现在我们打包只需要执行 参考地址: http://www.ruanyifeng.com/blog/2016/10/npm_scripts.html react-router
新建
按照
import React from 'react';
import {BrowserRouter as Router, Route, Switch, Link} from 'react-router-dom';
import Home from '../pages/Home/Home';
import Page1 from '../pages/Page1/Page1';
const getRouter = () => (
<Router>
<div>
<ul>
<li><Link to="/">首页</Link></li>
<li><Link to="/page1">Page1</Link></li>
</ul>
<Switch>
<Route exact path="/" component={Home}/>
<Route path="/page1" component={Page1}/>
</Switch>
</div>
</Router>
);
export default getRouter;
新建页面文件夹
新建两个页面
填充内容:
import React, {Component} from 'react';
export default class Home extends Component {
render() {
return (
<div>
this is home~
</div>
)
}
}
Page1.js import React, {Component} from 'react';
export default class Page1 extends Component {
render() {
return (
<div>
this is Page1~
</div>
)
}
}
现在路由和页面建好了,我们在入口文件 修改 import React from 'react';
import ReactDom from 'react-dom';
import getRouter from './router/router';
ReactDom.render(
getRouter(), document.getElementById('app'));
现在执行打包命令 那么问题来了~我们发现点击‘首页’和‘Page1’没有反应。不要惊慌,这是正常的。 我们之前一直用这个路径访问
下一节,我们来使用第二种方法启动服务器。这一节的DEMO,先放这里。 参考地址 webpack-dev-server 简单来说,
修改
devServer: {
contentBase: path.join(__dirname, './dist')
}
现在执行
浏览器打开http://localhost:8080,OK,现在我们可以点击 Q: A: 重要提示:webpack-dev-server编译后的文件,都存储在内存中,我们并不能看见的。你可以删除之前遗留的文件 每次执行 "scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev-build": "webpack --config webpack.dev.config.js",
"start": "webpack-dev-server --config webpack.dev.config.js"
}
下次执行 既然用到了
proxy: {
"/api": "http://localhost:3000"
}
根据这几个配置,修改下我们的
devServer: {
port: 8080,
contentBase: path.join(__dirname, './dist'),
historyApiFallback: true,
host: '0.0.0.0'
}
"start": "webpack-dev-server --config webpack.dev.config.js --color --progress" 现在我们执行 参考地址:
模块热替换(Hot Module Replacement) 到目前,当我们修改代码的时候,浏览器会自动刷新,不信你可以去试试。(如果你的不会刷新,看看这个调整文本编辑器) 我相信看这个教程的人,应该用过别人的框架。我们在修改代码的时候,浏览器不会刷新,只会更新自己修改的那一块。我们也要实现这个效果。 我们看下webpack模块热替换教程。 我们接下来要这么修改
"start": "webpack-dev-server --config webpack.dev.config.js --color --progress --hot"
import React from 'react';
import ReactDom from 'react-dom';
import getRouter from './router/router';
if (module.hot) {
module.hot.accept();
}
ReactDom.render(
getRouter(), document.getElementById('app'));
现在我们执行 做模块热替换,我们只改了几行代码,非常简单的。纸老虎一个~ 现在我需要说明下我们命令行使用的 const webpack = require('webpack');
devServer: {
hot: true
}
plugins:[
new webpack.HotModuleReplacementPlugin()
]
你以为模块热替换到这里就结束了?nonono~ 上面的配置对 例如下面的 修改
import React, {Component} from 'react';
export default class Home extends Component {
constructor(props) {
super(props);
this.state = {
count: 0
}
}
_handleClick() {
this.setState({
count: ++this.state.count
});
}
render() {
return (
<div>
this is home~<br/>
当前计数:{this.state.count}<br/>
<button onClick={() => this._handleClick()}>自增</button>
</div>
)
}
}
你可以测试一下,当我们修改代码的时候, 为了在 Q: 请问 A: 区别在于 下面我们来加入 安装依赖
根据文档,
{
"presets": [
"es2015",
"react",
"stage-0"
],
"plugins": [
"react-hot-loader/babel"
]
}
entry: [
'react-hot-loader/patch',
path.join(__dirname, 'src/index.js')
]
import React from 'react';
import ReactDom from 'react-dom';
import {AppContainer} from 'react-hot-loader';
import getRouter from './router/router';
/*初始化*/
renderWithHotReload(getRouter());
/*热更新*/
if (module.hot) {
module.hot.accept('./router/router', () => {
const getRouter = require('./router/router').default;
renderWithHotReload(getRouter());
});
}
function renderWithHotReload(RootElement) {
ReactDom.render(
<AppContainer>
{RootElement}
</AppContainer>,
document.getElementById('app')
)
}
现在,执行 参考文章: 文件路径优化 做到这里,我们简单休息下。做下优化~ 在之前写的代码中,我们引用组件,或者页面时候,写的是相对路径~ 比如 import Home from '../pages/Home/Home'; webpack提供了一个别名配置,就是我们无论在哪个路径下,引用都可以这样 import Home from 'pages/Home/Home'; 下面我们来配置下,修改
resolve: {
alias: {
pages: path.join(__dirname, 'src/pages'),
component: path.join(__dirname, 'src/component'),
router: path.join(__dirname, 'src/router')
}
}
然后我们把之前使用的绝对路径统统改掉。
import Home from 'pages/Home/Home'; import Page1 from 'pages/Page1/Page1';
import getRouter from 'router/router'; 我们这里约定,下面,我们会默认配置需要的别名路径,不再做重复的讲述哦。 redux 接下来,我们就要就要就要集成 要对 如果要对 不要被各种关于 reducers, middleware, store 的演讲所蒙蔽 ---- Redux 实际是非常简单的。 当然,我这篇文章是写给新手的,如果看不懂上面的文章,或者不想看,没关系。先会用,多用用就知道原理了。 开始整代码!我们就做一个最简单的计数器。自增,自减,重置。 先安装 初始化目录结构 cd src mkdir redux cd redux mkdir actions mkdir reducers touch reducers.js touch store.js touch actions/counter.js touch reducers/counter.js 先来写 /*action*/
export const INCREMENT = "counter/INCREMENT";
export const DECREMENT = "counter/DECREMENT";
export const RESET = "counter/RESET";
export function increment() {
return {type: INCREMENT}
}
export function decrement() {
return {type: DECREMENT}
}
export function reset() {
return {type: RESET}
}
再来写
import {INCREMENT, DECREMENT, RESET} from '../actions/counter';
/*
* 初始化state
*/
const initState = {
count: 0
};
/*
* reducer
*/
export default function reducer(state = initState, action) {
switch (action.type) {
case INCREMENT:
return {
count: state.count + 1
};
case DECREMENT:
return {
count: state.count - 1
};
case RESET:
return {count: 0};
default:
return state
}
}
一个项目有很多的
import counter from './reducers/counter';
export default function combineReducers(state = {}, action) {
return {
counter: counter(state.counter, action)
}
}
到这里,我们必须再理解下一句话。
看看上面的代码,无论是 接下来,我们要创建一个 前面我们可以使用 还可以使用 那我们如何提交
import {createStore} from 'redux';
import combineReducers from './reducers.js';
let store = createStore(combineReducers);
export default store;
到现在为止,我们已经可以使用 下面我们就简单的测试下 cd src cd redux touch testRedux.js
import {increment, decrement, reset} from './actions/counter';
import store from './store';
// 打印初始状态
console.log(store.getState());
// 每次 state 更新时,打印日志
// 注意 subscribe() 返回一个函数用来注销监听器
let unsubscribe = store.subscribe(() =>
console.log(store.getState())
);
// 发起一系列 action
store.dispatch(increment());
store.dispatch(decrement());
store.dispatch(reset());
// 停止监听 state 更新
unsubscribe();
当前文件夹执行命令
是不是看到输出了
做这个测试,就是为了告诉大家, 到这里,我建议你再理下
就是酱紫~~ 这会
alias: {
...
actions: path.join(__dirname, 'src/redux/actions'),
reducers: path.join(__dirname, 'src/redux/reducers'),
redux: path.join(__dirname, 'src/redux')
}
把前面的相对路径都改改。 下面我们开始搭配 写一个
import React, {Component} from 'react';
export default class Counter extends Component {
render() {
return (
<div>
<div>当前计数为(显示redux计数)</div>
<button onClick={() => {
console.log('调用自增函数');
}}>自增
</button>
<button onClick={() => {
console.log('调用自减函数');
}}>自减
</button>
<button onClick={() => {
console.log('调用重置函数');
}}>重置
</button>
</div>
)
}
}
修改路由,增加
import React from 'react';
import {BrowserRouter as Router, Route, Switch, Link} from 'react-router-dom';
import Home from 'pages/Home/Home';
import Page1 from 'pages/Page1/Page1';
import Counter from 'pages/Counter/Counter';
const getRouter = () => (
<Router>
<div>
<ul>
<li><Link to="/">首页</Link></li>
<li><Link to="/page1">Page1</Link></li>
<li><Link to="/counter">Counter</Link></li>
</ul>
<Switch>
<Route exact path="/" component={Home}/>
<Route path="/page1" component={Page1}/>
<Route path="/counter" component={Counter}/>
</Switch>
</div>
</Router>
);
export default getRouter;
下一步,我们让 当然我们可以使用刚才测试
先来安装
import React, {Component} from 'react';
import {increment, decrement, reset} from 'actions/counter';
import {connect} from 'react-redux';
class Counter extends Component {
render() {
return (
<div>
<div>当前计数为{this.props.counter.count}</div>
<button onClick={() => this.props.increment()}>自增
</button>
<button onClick={() => this.props.decrement()}>自减
</button>
<button onClick={() => this.props.reset()}>重置
</button>
</div>
)
}
}
const mapStateToProps = (state) => {
return {
counter: state.counter
}
};
const mapDispatchToProps = (dispatch) => {
return {
increment: () => {
dispatch(increment())
},
decrement: () => {
dispatch(decrement())
},
reset: () => {
dispatch(reset())
}
}
};
export default connect(mapStateToProps, mapDispatchToProps)(Counter);
下面我们要传入
import React from 'react';
import ReactDom from 'react-dom';
import {AppContainer} from 'react-hot-loader';
import {Provider} from 'react-redux';
import store from './redux/store';
import getRouter from 'router/router';
/*初始化*/
renderWithHotReload(getRouter());
/*热更新*/
if (module.hot) {
module.hot.accept('./router/router', () => {
const getRouter = require('router/router').default;
renderWithHotReload(getRouter());
});
}
function renderWithHotReload(RootElement) {
ReactDom.render(
<AppContainer>
<Provider store={store}>
{RootElement}
</Provider>
</AppContainer>,
document.getElementById('app')
)
}
到这里我们就可以执行 但是你发现 ERROR in ./node_modules/react-redux/es/connect/mapDispatchToProps.js Module not found: Error: Can't resolve 'redux' in 'F:\Project\react\react-family\node_modules\react-redux\es\connect' ERROR in ./src/redux/store.js Module not found: Error: Can't resolve 'redux' in 'F:\Project\react\react-family\src\redux' WTF?这个错误困扰了半天。我说下为什么造成这个错误。我们引用
然而,我们在 resolve: {
alias: {
...
redux: path.join(__dirname, 'src/redux')
}
}
然后 现在你可以 这里我们再缕下(可以读React 实践心得:react-redux 之 connect 方法详解)
接下来,我们要说异步 参考地址: http://cn.redux.js.org/docs/advanced/AsyncActions.html 想象一下我们调用一个异步
下面,我们以向后台请求用户基本信息为例。
cd dist mkdir api cd api touch user.json
{
"name": "brickspert",
"intro": "please give me a star"
}
cd src/redux/actions touch userInfo.js
export const GET_USER_INFO_REQUEST = "userInfo/GET_USER_INFO_REQUEST";
export const GET_USER_INFO_SUCCESS = "userInfo/GET_USER_INFO_SUCCESS";
export const GET_USER_INFO_FAIL = "userInfo/GET_USER_INFO_FAIL";
function getUserInfoRequest() {
return {
type: GET_USER_INFO_REQUEST
}
}
function getUserInfoSuccess(userInfo) {
return {
type: GET_USER_INFO_SUCCESS,
userInfo: userInfo
}
}
function getUserInfoFail() {
return {
type: GET_USER_INFO_FAIL
}
}
我们创建了请求中,请求成功,请求失败三个
再强调下, cd src/redux/reducers touch userInfo.js
import {GET_USER_INFO_REQUEST, GET_USER_INFO_SUCCESS, GET_USER_INFO_FAIL} from 'actions/userInfo';
const initState = {
isLoading: false,
userInfo: {},
errorMsg: ''
};
export default function reducer(state = initState, action) {
switch (action.type) {
case GET_USER_INFO_REQUEST:
return {
...state,
isLoading: true,
userInfo: {},
errorMsg: ''
};
case GET_USER_INFO_SUCCESS:
return {
...state,
isLoading: false,
userInfo: action.userInfo,
errorMsg: ''
};
case GET_USER_INFO_FAIL:
return {
...state,
isLoading: false,
userInfo: {},
errorMsg: '请求错误'
};
default:
return state;
}
}
这里的 组合
import counter from 'reducers/counter';
import userInfo from 'reducers/userInfo';
export default function combineReducers(state = {}, action) {
return {
counter: counter(state.counter, action),
userInfo: userInfo(state.userInfo, action)
}
}
export function getUserInfo() {
return function (dispatch) {
dispatch(getUserInfoRequest());
return fetch('http://localhost:8080/api/user.json')
.then((response => {
return response.json()
}))
.then((json) => {
dispatch(getUserInfoSuccess(json))
}
).catch(
() => {
dispatch(getUserInfoFail());
}
)
}
}
我们这里发现,别的 {type: xxxx}
但是我们现在的这个 为了让
这里涉及到 简单的说,中间件就是 我们来引入
import {createStore, applyMiddleware} from 'redux';
import thunkMiddleware from 'redux-thunk';
import combineReducers from './reducers.js';
let store = createStore(combineReducers, applyMiddleware(thunkMiddleware));
export default store;
到这里, cd src/pages mkdir UserInfo cd UserInfo touch UserInfo.js
import React, {Component} from 'react';
import {connect} from 'react-redux';
import {getUserInfo} from "actions/userInfo";
class UserInfo extends Component {
render() {
const {userInfo, isLoading, errorMsg} = this.props.userInfo;
return (
<div>
{
isLoading ? '请求信息中......' :
(
errorMsg ? errorMsg :
<div>
<p>用户信息:</p>
<p>用户名:{userInfo.name}</p>
<p>介绍:{userInfo.intro}</p>
</div>
)
}
<button onClick={() => this.props.getUserInfo()}>请求用户信息</button>
</div>
)
}
}
export default connect((state) => ({userInfo: state.userInfo}), {getUserInfo})(UserInfo);
这里你可能发现 增加路由 import React from 'react';
import {BrowserRouter as Router, Route, Switch, Link} from 'react-router-dom';
import Home from 'pages/Home/Home';
import Page1 from 'pages/Page1/Page1';
import Counter from 'pages/Counter/Counter';
import UserInfo from 'pages/UserInfo/UserInfo';
const getRouter = () => (
<Router>
<div>
<ul>
<li><Link to="/">首页</Link></li>
<li><Link to="/page1">Page1</Link></li>
<li><Link to="/counter">Counter</Link></li>
<li><Link to="/userinfo">UserInfo</Link></li>
</ul>
<Switch>
<Route exact path="/" component={Home}/>
<Route path="/page1" component={Page1}/>
<Route path="/counter" component={Counter}/>
<Route path="/userinfo" component={UserInfo}/>
</Switch>
</div>
</Router>
);
export default getRouter;
现在你可以执行 到这里 combinReducers优化
import {combineReducers} from "redux";
import counter from 'reducers/counter';
import userInfo from 'reducers/userInfo';
export default combineReducers({
counter,
userInfo
});
devtool优化 现在我们发现一个问题,代码哪里写错了,浏览器报错只报在 这让我们分析错误无从下手。看这里。 我们增加
devtool: 'inline-source-map' 这次看错误信息是不是提示的很详细了? 同时,我们在 编译css 先说这里为什么不用
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
}
我们用 cd src/pages/Page1 touch Page1.css
.page-box {
border: 1px solid red;
}
import React, {Component} from 'react';
import './Page1.css';
export default class Page1 extends Component {
render() {
return (
<div className="page-box">
this is page1~
</div>
)
}
}
好了,现在 编译图片
{
test: /\.(png|jpg|gif)$/,
use: [{
loader: 'url-loader',
options: {
limit: 8192
}
}]
}
我们来用 cd src/pages/Page1 mkdir images 给 修改代码,引用图片
import React, {Component} from 'react';
import './Page1.css';
import image from './images/brickpsert.jpg';
export default class Page1 extends Component {
render() {
return (
<div className="page-box">
this is page1~
<img src={image}/>
</div>
)
}
}
可以去看看效果啦。 按需加载 为什么要实现按需加载? 我们现在看到,打包完后,所有页面只生成了一个 如果每个页面都打包了自己单独的JS,在进入自己页面的时候才加载对应的js,那首屏加载就会快很多哦。 在 在4.0版本,官方放弃了这种处理按需加载的方式,选择了一个更加简洁的处理方式。 根据官方示例,我们开搞
cd src/router touch Bundle.js
import React, {Component} from 'react'
class Bundle extends Component {
state = {
// short for "module" but that's a keyword in js, so "mod"
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() {
return this.props.children(this.state.mod)
}
}
export default Bundle;
import React from 'react';
import {BrowserRouter as Router, Route, Switch, Link} from 'react-router-dom';
import Bundle from './Bundle';
import Home from 'bundle-loader?lazy&name=home!pages/Home/Home';
import Page1 from 'bundle-loader?lazy&name=page1!pages/Page1/Page1';
import Counter from 'bundle-loader?lazy&name=counter!pages/Counter/Counter';
import UserInfo from 'bundle-loader?lazy&name=userInfo!pages/UserInfo/UserInfo';
const Loading = function () {
return <div>Loading...</div>
};
const createComponent = (component) => (props) => (
<Bundle load={component}>
{
(Component) => Component ? <Component {...props} /> : <Loading/>
}
</Bundle>
);
const getRouter = () => (
<Router>
<div>
<ul>
<li><Link to="/">首页</Link></li>
<li><Link to="/page1">Page1</Link></li>
<li><Link to="/counter">Counter</Link></li>
<li><Link to="/userinfo">UserInfo</Link></li>
</ul>
<Switch>
<Route exact path="/" component={createComponent(Home)}/>
<Route path="/page1" component={createComponent(Page1)}/>
<Route path="/counter" component={createComponent(Counter)}/>
<Route path="/userinfo" component={createComponent(UserInfo)}/>
</Switch>
</div>
</Router>
);
export default getRouter;
现在你可以 但是你可能发现,名字都是 我们修改下 output: {
path: path.join(__dirname, './dist'),
filename: 'bundle.js',
chunkFilename: '[name].js'
}
现在你运行发现名字变成 那么问题来了 其实在这里我们定义了,
看到没。这里有个 参考地址:
缓存 想象一下这个场景~ 我们网站上线了,用户第一次访问首页,下载了 这肯定不行呀,所以我们一般都会做一个缓存,用户下载一次 有一天,我们更新了 怎么解决?每次代码更新后,打包生成的名字不一样。比如第一次叫 文档看这里 我们照着文档来
output: {
path: path.join(__dirname, './dist'),
filename: '[name].[hash].js',
chunkFilename: '[name].[chunkhash].js'
}
每次打包都用增加 现在我们试试,是不是修改了文件,打包后相应的文件名字就变啦? 但是你可能发现了,网页打开报错了~因为你 啊~那岂不是我每次编译打包,都得去改一下js名字?欲知后事如何,且看下节分享。 HtmlWebpackPlugin 这个插件,每次会自动把js插入到你的模板
新建模板 cd src touch index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<div id="app"></div>
</body>
</html>
修改 var HtmlWebpackPlugin = require('html-webpack-plugin');
plugins: [new HtmlWebpackPlugin({
filename: 'index.html',
template: path.join(__dirname, 'src/index.html')
})],
说明一下: 提取公共代码 想象一下,我们的主文件,原来的 我们把
var webpack = require('webpack');
entry: {
app: [
'react-hot-loader/patch',
path.join(__dirname, 'src/index.js')
],
vendor: ['react', 'react-router-dom', 'redux', 'react-dom', 'react-redux']
}
/*plugins*/
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor'
})
把 但是你现在可能发现编译生成的文件 output: {
path: path.join(__dirname, './dist'),
filename: '[name].[hash].js', //这里应该用chunkhash替换hash
chunkFilename: '[name].[chunkhash].js'
}
但是无奈,如果用 现在我们在配置开发版配置文件,就向 生产坏境构建
文档看这里 我们要开始做了~ touch webpack.config.js 在
const path = require('path');
var HtmlWebpackPlugin = require('html-webpack-plugin');
var webpack = require('webpack');
module.exports = {
devtool: 'cheap-module-source-map',
entry: {
app: [
path.join(__dirname, 'src/index.js')
],
vendor: ['react', 'react-router-dom', 'redux', 'react-dom', 'react-redux']
},
output: {
path: path.join(__dirname, './dist'),
filename: '[name].[chunkhash].js',
chunkFilename: '[name].[chunkhash].js'
},
module: {
rules: [{
test: /\.js$/,
use: ['babel-loader'],
include: path.join(__dirname, 'src')
}, {
test: /\.css$/,
use: ['style-loader', 'css-loader']
}, {
test: /\.(png|jpg|gif)$/,
use: [{
loader: 'url-loader',
options: {
limit: 8192
}
}]
}]
},
plugins: [
new HtmlWebpackPlugin({
filename: 'index.html',
template: path.join(__dirname, 'src/index.html')
}),
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor'
})
],
resolve: {
alias: {
pages: path.join(__dirname, 'src/pages'),
component: path.join(__dirname, 'src/component'),
router: path.join(__dirname, 'src/router'),
actions: path.join(__dirname, 'src/redux/actions'),
reducers: path.join(__dirname, 'src/redux/reducers')
}
}
};
在
然后执行 接下来我们还是要优化正式版配置文件~ 文件压缩
const UglifyJSPlugin = require('uglifyjs-webpack-plugin')
module.exports = {
plugins: [
new UglifyJSPlugin()
]
}
指定环境
module.exports = {
plugins: [
new webpack.DefinePlugin({
'process.env': {
'NODE_ENV': JSON.stringify('production')
}
})
]
}
优化缓存 刚才我们把 但是现在又有一个问题了。 你随便修改代码一处,例如 官方文档推荐了一个插件HashedModuleIdsPlugin plugins: [
new webpack.HashedModuleIdsPlugin()
]
现在你打包,修改代码再试试,是不是名字不变啦?错了,现在打包,我发现名字还是变了,经过比对文档,我发现还要加一个 new webpack.optimize.CommonsChunkPlugin({
name: 'runtime'
})
加上这句话就好了~为什么呢?看下解释。 注意,引入顺序在这里很重要。CommonsChunkPlugin 的 'vendor' 实例,必须在 'runtime' 实例之前引入。 public path 想象一个场景,我们的静态文件放在了单独的静态服务器上去了,那我们打包的时候,如何让静态文件的链接定位到静态服务器呢? 看文档Public Path
output: {
publicPath : '/'
}
打包优化 你现在打开
const CleanWebpackPlugin = require('clean-webpack-plugin');
plugins: [
new CleanWebpackPlugin(['dist'])
]
现在 抽取css 目前我们的 我们使用extract-text-webpack-plugin来实现。
const ExtractTextPlugin = require("extract-text-webpack-plugin");
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: ExtractTextPlugin.extract({
fallback: "style-loader",
use: "css-loader"
})
}
]
},
plugins: [
new ExtractTextPlugin({
filename: '[name].[contenthash:5].css',
allChunks: true
})
]
}
使用 先安装下axios
我们之前项目的一次API请求是这样写的哦~
export function getUserInfo() {
return {
types: [GET_USER_INFO_REQUEST, GET_USER_INFO_SUCCESS, GET_USER_INFO_FAIL],
promise: client => client.get(`http://localhost:8080/api/user.json`)
afterSuccess:(dispatch,getState,response)=>{
/*请求成功后执行的函数*/
},
otherData:otherData
}
}
然后在dispatch(getUserInfo())后,通过 中间件的教程看这里 我们想想中间件的逻辑
来写一个 cd src/redux mkdir middleware cd middleware touch promiseMiddleware.js
import axios from 'axios';
export default store => next => action => {
const {dispatch, getState} = store;
/*如果dispatch来的是一个function,此处不做处理,直接进入下一级*/
if (typeof action === 'function') {
action(dispatch, getState);
return;
}
/*解析action*/
const {
promise,
types,
afterSuccess,
...rest
} = action;
/*没有promise,证明不是想要发送ajax请求的,就直接进入下一步啦!*/
if (!action.promise) {
return next(action);
}
/*解析types*/
const [REQUEST,
SUCCESS,
FAILURE] = types;
/*开始请求的时候,发一个action*/
next({
...rest,
type: REQUEST
});
/*定义请求成功时的方法*/
const onFulfilled = result => {
next({
...rest,
result,
type: SUCCESS
});
if (afterSuccess) {
afterSuccess(dispatch, getState, result);
}
};
/*定义请求失败时的方法*/
const onRejected = error => {
next({
...rest,
error,
type: FAILURE
});
};
return promise(axios).then(onFulfilled, onRejected).catch(error => {
console.error('MIDDLEWARE ERROR:', error);
onRejected(error)
})
}
修改 import {createStore, applyMiddleware} from 'redux';
import combineReducers from './reducers.js';
import promiseMiddleware from './middleware/promiseMiddleware'
let store = createStore(combineReducers, applyMiddleware(promiseMiddleware));
export default store;
修改 export const GET_USER_INFO_REQUEST = "userInfo/GET_USER_INFO_REQUEST";
export const GET_USER_INFO_SUCCESS = "userInfo/GET_USER_INFO_SUCCESS";
export const GET_USER_INFO_FAIL = "userInfo/GET_USER_INFO_FAIL";
export function getUserInfo() {
return {
types: [GET_USER_INFO_REQUEST, GET_USER_INFO_SUCCESS, GET_USER_INFO_FAIL],
promise: client => client.get(`http://localhost:8080/api/user.json`)
}
}
是不是简单清新很多啦? 修改 case GET_USER_INFO_SUCCESS:
return {
...state,
isLoading: false,
userInfo: action.result.data,
errorMsg: ''
};
调整文本编辑器 使用自动编译代码时,可能会在保存文件时遇到一些问题。某些编辑器具有“安全写入”功能,可能会影响重新编译。 要在一些常见的编辑器中禁用此功能,请查看以下列表:
合并提取 想象一个场景,现在我想给 这肯定不行啊。所以我们要把公共的配置文件提取出来。提取到
这里我们需要用到webpack-merge来合并公共配置和单独的配置。 这样说一下,应该看代码就能看懂了。下次公共配置直接就写在
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const webpack = require('webpack');
commonConfig = {
entry: {
app: [
path.join(__dirname, 'src/index.js')
],
vendor: ['react', 'react-router-dom', 'redux', 'react-dom', 'react-redux']
},
output: {
path: path.join(__dirname, './dist'),
filename: '[name].[chunkhash].js',
chunkFilename: '[name].[chunkhash].js',
publicPath: "/"
},
module: {
rules: [{
test: /\.js$/,
use: ['babel-loader?cacheDirectory=true'],
include: path.join(__dirname, 'src')
}, {
test: /\.(png|jpg|gif)$/,
use: [{
loader: 'url-loader',
options: {
limit: 8192
}
}]
}]
},
plugins: [
new HtmlWebpackPlugin({
filename: 'index.html',
template: path.join(__dirname, 'src/index.html')
}),
new webpack.HashedModuleIdsPlugin(),
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor'
}),
new webpack.optimize.CommonsChunkPlugin({
name: 'runtime'
})
],
resolve: {
alias: {
pages: path.join(__dirname, 'src/pages'),
components: path.join(__dirname, 'src/components'),
router: path.join(__dirname, 'src/router'),
actions: path.join(__dirname, 'src/redux/actions'),
reducers: path.join(__dirname, 'src/redux/reducers')
}
}
};
module.exports = commonConfig;
const merge = require('webpack-merge');
const path = require('path');
const commonConfig = require('./webpack.common.config.js');
const devConfig = {
devtool: 'inline-source-map',
entry: {
app: [
'react-hot-loader/patch',
path.join(__dirname, 'src/index.js')
]
},
output: {
/*这里本来应该是[chunkhash]的,但是由于[chunkhash]和react-hot-loader不兼容。只能妥协*/
filename: '[name].[hash].js'
},
module: {
rules: [{
test: /\.css$/,
use: ["style-loader", "css-loader"]
}]
},
devServer: {
contentBase: path.join(__dirname, './dist'),
historyApiFallback: true,
host: '0.0.0.0',
}
};
module.exports = merge({
customizeArray(a, b, key) {
/*entry.app不合并,全替换*/
if (key === 'entry.app') {
return b;
}
return undefined;
}
})(commonConfig, devConfig);
const merge = require('webpack-merge');
const webpack = require('webpack');
const UglifyJSPlugin = require('uglifyjs-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const ExtractTextPlugin = require("extract-text-webpack-plugin");
const commonConfig = require('./webpack.common.config.js');
const publicConfig = {
devtool: 'cheap-module-source-map',
module: {
rules: [{
test: /\.css$/,
use: ExtractTextPlugin.extract({
fallback: "style-loader",
use: "css-loader"
})
}]
},
plugins: [
new CleanWebpackPlugin(['dist/*.*']),
new UglifyJSPlugin(),
new webpack.DefinePlugin({
'process.env': {
'NODE_ENV': JSON.stringify('production')
}
}),
new ExtractTextPlugin({
filename: '[name].[contenthash:5].css',
allChunks: true
})
]
};
module.exports = merge(commonConfig, publicConfig);
优化目录结构并增加404页面 现在我们优化下目录结构,把
import React, {Component} from 'react';
import Nav from 'components/Nav/Nav';
import getRouter from 'router/router';
export default class App extends Component {
render() {
return (
<div>
<Nav/>
{getRouter()}
</div>
)
}
}
import React from 'react';
import ReactDom from 'react-dom';
import {AppContainer} from 'react-hot-loader';
import {Provider} from 'react-redux';
import store from './redux/store';
import {BrowserRouter as Router} from 'react-router-dom';
import App from 'components/App/App';
renderWithHotReload(App);
if (module.hot) {
module.hot.accept('components/App/App', () => {
const NextApp = require('components/App/App').default;
renderWithHotReload(NextApp);
});
}
function renderWithHotReload(RootElement) {
ReactDom.render(
<AppContainer>
<Provider store={store}>
<Router>
<RootElement/>
</Router>
</Provider>
</AppContainer>,
document.getElementById('app')
)
}
import NotFound from 'bundle-loader?lazy&name=notFound!pages/NotFound/NotFound';
<Route component={createComponent(NotFound)}/>
加入 babel-plugin-transform-runtime 和 babel-polyfill
参考地址:
集成PostCSS Q: 这是啥?为什么要用它? 他有很多很多的插件,我们举几个例子~ Autoprefixer这个插件,可以自动给css属性加浏览器前缀。 /*编译前*/
.container{
display: flex;
}
/*编译后*/
.container{
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
}
postcss-cssnext 允许你使用未来的 CSS 特性(包括 autoprefixer) 当然,它有很多很多的插件可以用,你可以去官网详细了解。我们今天只用 npm install --save-dev postcss-loader npm install --save-dev postcss-cssnext 修改 webpack.dev.config.js rules: [{
test: /\.(css|scss)$/,
use: ["style-loader", "css-loader", "postcss-loader"]
}]
webpack.config.js rules: [{
test: /\.css$/,
use: ExtractTextPlugin.extract({
fallback: "style-loader",
use: ["css-loader", "postcss-loader"]
})
}]
根目录增加 touch postcss.config.js
module.exports = {
plugins: {
'postcss-cssnext': {}
}
};
现在你运行代码,然后写个css,去浏览器审查元素,看看,属性是不是生成了浏览器前缀? redux 模块热替换配置 今天突然发现,当修改reducer代码的时候,页面会整个刷新,而不是局部刷新唉。 这不行,就去查了webpack文档,果然是要配置的。看这里 代码修改起来也简单,增加一段监听reducers变化,并替换的代码。
if (module.hot) {
module.hot.accept("./reducers", () => {
const nextCombineReducers = require("./reducers").default;
store.replaceReducer(nextCombineReducers);
});
}
哦了~ 模拟AJAX数据之Mock.js 每个改进都是为了解决问题。 现在我在开发中碰到了问题,我先描述下问题: 我们现在做前后端完全分离的应用,前端写前端的,后端写后端的,他们通过API接口连接。 前端同学心理路程:"后端同学接口写的好慢,我都没法调试了。" 是不是有这个问题呢?一般我们怎么解决? 第一种:自己这边随便造点数据,等后端接口写好了之后,再小修改,再调试。 第二种:想想我们之前获得用户信息的 并且,后端接口一般都不带 好了,下面介绍下今天的主角Mock.js。 他会做一件事情:拦截AJAX请求,返回需要的数据! 我们写AJAX请求的时候,正常写,Mock.js会自动拦截的。 Mock.js提供各种随机生成数据。具体可以去官网看~ 下面我们就在项目中集成咯:
到这里还没完,我们还要配置:只有在开发坏境下,才引入 跟着我做: 先给
然后修改
这样,就只会在 哦了,到这里就结束了~回头缕下: 我们定义了mock,在index.js引入。 mock的工作就是,拦截AJAX请求,返回模拟数据。 参考文章: http://www.jianshu.com/p/dd23a6547114 https://segmentfault.com/a/1190000005793320 使用 CSS Modules 关于什么是 可以去看阮一峰的文章CSS Modules 用法教程 修改以下几个地方:
enjoy it! 使用 json-server 代替 Mock.js json-server和
我们用
let Mock = require('mockjs');
var Random = Mock.Random;
module.exports = function () {
var data = {};
data.user = {
'name': Random.cname(),
'intro': Random.word(20)
};
return data;
};
devServer: {
...
proxy: {
"/api/*": "http://localhost:8090/$1"
}
}
哦了,你可以 问题: 问题修复 1. react热模块加载无效举例:在首页中,当我们计数加上去,然后修改代码,计数又恢复成0了。也就是热模块加载的时候,重置了 如果我们不使用 解决问题参考这里:https://github.com/gaearon/react-hot-loader/tree/next#code-splitting 解决步骤:
import {hot} from 'react-hot-loader';
...
export default hot(module)(Home);
其他模块如果需要,可以自己同理修改哦。 |
???? 51???? 1???? 6???? 17
Owner
brickspert commented on 4 Sep 2017 •
edited
|
合并提取 想象一个场景,现在我想给 这肯定不行啊。所以我们要把公共的配置文件提取出来。提取到
这里我们需要用到webpack-merge来合并公共配置和单独的配置。 这样说一下,应该看代码就能看懂了。下次公共配置直接就写在
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const webpack = require('webpack');
commonConfig = {
entry: {
app: [
path.join(__dirname, 'src/index.js')
],
vendor: ['react', 'react-router-dom', 'redux', 'react-dom', 'react-redux']
},
output: {
path: path.join(__dirname, './dist'),
filename: '[name].[chunkhash].js',
chunkFilename: '[name].[chunkhash].js',
publicPath: "/"
},
module: {
rules: [{
test: /\.js$/,
use: ['babel-loader?cacheDirectory=true'],
include: path.join(__dirname, 'src')
}, {
test: /\.(png|jpg|gif)$/,
use: [{
loader: 'url-loader',
options: {
limit: 8192
}
}]
}]
},
plugins: [
new HtmlWebpackPlugin({
filename: 'index.html',
template: path.join(__dirname, 'src/index.html')
}),
new webpack.HashedModuleIdsPlugin(),
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor'
}),
new webpack.optimize.CommonsChunkPlugin({
name: 'runtime'
})
],
resolve: {
alias: {
pages: path.join(__dirname, 'src/pages'),
components: path.join(__dirname, 'src/components'),
router: path.join(__dirname, 'src/router'),
actions: path.join(__dirname, 'src/redux/actions'),
reducers: path.join(__dirname, 'src/redux/reducers')
}
}
};
module.exports = commonConfig;
const merge = require('webpack-merge');
const path = require('path');
const commonConfig = require('./webpack.common.config.js');
const devConfig = {
devtool: 'inline-source-map',
entry: {
app: [
'react-hot-loader/patch',
path.join(__dirname, 'src/index.js')
]
},
output: {
/*这里本来应该是[chunkhash]的,但是由于[chunkhash]和react-hot-loader不兼容。只能妥协*/
filename: '[name].[hash].js'
},
module: {
rules: [{
test: /\.css$/,
use: ["style-loader", "css-loader"]
}]
},
devServer: {
contentBase: path.join(__dirname, './dist'),
historyApiFallback: true,
host: '0.0.0.0',
}
};
module.exports = merge({
customizeArray(a, b, key) {
/*entry.app不合并,全替换*/
if (key === 'entry.app') {
return b;
}
return undefined;
}
})(commonConfig, devConfig);
const merge = require('webpack-merge');
const webpack = require('webpack');
const UglifyJSPlugin = require('uglifyjs-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const ExtractTextPlugin = require("extract-text-webpack-plugin");
const commonConfig = require('./webpack.common.config.js');
const publicConfig = {
devtool: 'cheap-module-source-map',
module: {
rules: [{
test: /\.css$/,
use: ExtractTextPlugin.extract({
fallback: "style-loader",
use: "css-loader"
})
}]
},
plugins: [
new CleanWebpackPlugin(['dist/*.*']),
new UglifyJSPlugin(),
new webpack.DefinePlugin({
'process.env': {
'NODE_ENV': JSON.stringify('production')
}
}),
new ExtractTextPlugin({
filename: '[name].[contenthash:5].css',
allChunks: true
})
]
};
module.exports = merge(commonConfig, publicConfig);
|
原文https://github.com/brickspert/blog/issues/1#webpack