项目结构
├─ config
│ ├─ webpack.common.config.js //webpack基础配置
│ ├─ webpack.dev.config.js //webpack开发配置
│ └─ webpack.prod.config.js //webpack生产配置
├─ src
│ ├─ css //css文件夹
│ │ ├─ counter.css
│ │ └─ user.css
│ ├─ counter // 页面counter文件夹
│ │ ├─ action.js
│ │ ├─ reducer.js
│ │ └─ view.js
│ ├─ user // 页面user文件夹
│ │ ├─ action.js
│ │ ├─ reducer.js
│ │ └─ view.js
│ ├─ index.html //html模板
│ ├─ index.js //入口js
│ ├─ router.js //路由js
│ └─ store.js //redux js
├─ .babelrc //babel配置
├─ package.json //package.json
React Router例子
import React, {Component} from 'react'
import ReactDom from 'react-dom';
import {
BrowserRouter as Router,
Route,
NavLink,
} from 'react-router-dom'
function UrlParam() {
return (
<Router>
<div>
<ul>
<li>
<NavLink exact to="/" activeClassName="hurray">Home</NavLink>
</li>
<li>
<NavLink to="/about/123" activeClassName="hurray">About</NavLink>
</li>
</ul>
<p></p>
<Route exact path="/" component={Home}/>
<Route path="/about/:name" component={About}/>
</div>
</Router>
)
}
class About extends Component {
render() {
let { match } = this.props
return (
<h1>{`${match.params.name} About`}</h1>
)
}
}
class Home extends Component {
render() {
return (
<div>
<h1>Home</h1>
</div>
)
}
}
ReactDom.render(
<UrlParam />, document.getElementById('root'));
Redux例子
/* action */
export const INCREMENT = "counter/INCREMENT"
export const DECREMENT = "counter/DECREMENT"
export const RESET = "counter/RESET"
// action创建函数
export function increment() {
return {type: INCREMENT}
}
export function decrement() {
return {type: DECREMENT}
}
export function reset() {
return {type: RESET}
}
/* reducer */
const initState = {
count: 0
}
export function counter(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
}
}
/* store */
import {createStore} from 'redux'
import {combineReducers} from "redux"
const store = createStore(combineReducers({
counter,
}))
/* test */
// 打印初始状态
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()
在当前文件夹执行webpack testRedux.js命令 在dist文件夹下,打开index.html可看到console记录
{ counter: { count: 0 } }
{ counter: { count: 1 } }
{ counter: { count: 0 } }
{ counter: { count: 0 } }
Action:描述当前发生了什么的普通对象,是改变state的唯一方法。其中type属性是必须的,表示Action的名称。
Action创建函数: 就是生成Action的方法。
Reducer: 是一个纯函数,接受Action和当前State作为参数,描述了应用如何更新state。
combineReducers函数 处理整棵树, reducer处理树的某一个点。
Store: 是保存数据的地方,可以把它看成一个容器。整个应用只能有一个Store。
-
提供getState()方法获取State
-
提供dispatch(action)方法出发reducers方法更新State
-
通过subscribe(listener)注册监听器
-
通过subscribe(listener)返回的注销监听器的函数,注销监听器
redux的数据流:
- 调用store.dispatch(action),提交action
- store调用创建时传入的reducer函数,把当前的state和action传进去获取新的state。
- reducer把多个子reducer的输出合并成一个单一的state树
- stoer保存根reducer返回的完整state树,并调用监听器
集成Redux、React Router
创建action
src/counter/action.js
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
}
}
创建reducer
src/counter/reducer.js
import { INCREMENT, DECREMENT, RESET } from './action'
const initState = {
count: 0
}
/**
* 接收旧的state和action, 生成新的state
*/
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
}
创建view
src/counter/view.js
import React, {Component} from 'react'
import {connect} from 'react-redux'
import {increment, decrement, reset } from './action'
class Counter extends Component {
render() {
return (
<div>
<div>当前计数为{this.props.counter.count}</div>
<button
onClick={() => {
console.log('调用自增函数')
this.props.increment()
}}
>
自增
</button>
<button
onClick={() => {
console.log('调用自减函数')
this.props.decrement()
}}
>
自减
</button>
<button
onClick={() => {
console.log('调用重置函数')
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())
}
}
}
/**
* 利用connect方法生成容器组件。
* 容器组件就是使用store.subscribe()从Redux State树种读取部分数据,并通过props来把这些数据提供给要渲染的组件。
* connect接受两个参数,mapStateToProps把redux的state转为组件的props属性字段,mapDispatchToProps把发射actions的方法转为props属性函数。
*/
export default connect(mapStateToProps, mapDispatchToProps)(Counter)
connect函数作用是从redux state树种读取部分数据,并通过props来把这些数据提供给要渲染的组件,也传递dispatch(action)函数到props。
添加counter子reducer并创建store
src/store.js
import { createStore, applyMiddleware } from 'redux'
import {combineReducers} from 'redux'
import counter from './counter/reducer'
// redux提供了一个combineReducers函数来合并reducer
const reducer = combineReducers({
counter,
})
// 创建store
const store = createStore(reducer)
export default store
创建router
src/router.js
import React from 'react'
import {
BrowserRouter as Router,
Route,
Switch,
NavLink,
} from 'react-router-dom'
import index from './css/index.css'
import Counter from './counter/view'
export default function getRouter() {
return (
<Router>
<div className={index.header_nav}>
<ul>
<li>
<NavLink exact to="/" activeClassName={index.cur_nav}>
Counter
</NavLink>
</li>
</ul>
<Switch>
<Route exact path="/" component={Counter}/>
<Route render={() => <div>please usd user or counter url!</div>}/>
</Switch>
</div>
</Router>
)
}
组件访问store
src/index.js
import React from 'react'
import ReactDOM from 'react-dom'
import {Provider} from 'react-redux'
import Store from './store'
import Router from './router'
ReactDOM.render((
<Provider store={Store}>
<Router />
</Provider>
), document.getElementById('root'))
Provider组件是让所有的组件可以访问到store。不用手动去传。也不用手动去监听。
按需加载
每个页面都打包加载自己单独的js。
bundle-loader 实现
- 使用require()来进行相应chunk的加载,该方法会返回一个function,这个function接收一个回调函数作为参数。
var waitForChunk = require("bundle-loader!./file.js");
// To wait until the chunk is available (and get the exports)
// you need to async wait for it.
waitForChunk(function(file) {
// use file like it was required with
// var file = require("./file.js");
});
- 创建异步加载的包装组件bundle.js。Bundle的主要功能就是接收一个组件异步加载的方法,并返回相应的react组件。
import React, { Component } from 'react';
class LazyLoader extends Component {
constructor(props) {
super(props);
this.state = {
component: null,
props: null,
};
this._isMounted = false; // 这个需要考虑
}
componentWillMount() {
this._load();
}
componentDidMount() {
this._isMounted = true;
}
componentWillReceiveProps(next) {
if (next.component === this.props.component) {
return null;
}
this._load();
}
componentWillUnmount() {
this._isMounted = false;
}
_load() {
this.props.component( com => {
this.setState({
component: com.default || com, // 兼容 es6 => commonjs
});
});
}
render() {
const LazyComponent = this.state.component;
const props = Object.assign({}, this.props); // clone props
delete props.component; // 去掉 component
return LazyComponent ? (
<LazyComponent {...props}/>
) : null;
}
}
export default LazyLoader;
- 创建需要懒加载的组件, lazyComponent.js
import React, { Component } from 'react';
class LazyComponent extends Component {
render() {
return (
<div>
this is a lazy Component!!!;
<br/>
name: {this.props.name} ===== tips: {this.props.tips}
</div>
);
}
}
export default LazyComponent;
- 对组件进行懒加载
引入了需要加载的组件 LazyComponent 和相应的加载器 LazyLoader,通过它来对 LazyComponent 进行按需加载。
import React from 'react';
import ReactDOM from 'react-dom'
import LazyComponent from 'bundle-loader?lazy&name=lazy.[name]!./lazyComponent';
import Bundle from './bundle';
ReactDOM.render((
<Bundle component={LazyComponent} name={'lazyname...'} tips={'lazytips...'}/>
), document.getElementById('root'))
import()会返回一个Promise对象
import('./lazyComponent').then(mod => {
someOperate(mod);
}).catch(err => {
console.log('failed');
});
require.ensure()
require.ensure(
// 当前require进来的模块的依赖
[],
// 对调函数,参数必须是require,用于动态引入其他模块
require => {
let LazyComponent = require('./lazyComponent');
someOperate(LazyComponent);
},
// 处理error的回
error => {
console.log('failed');
},
// 打包的chunk名称
'lazyComponent');
打包
配置HTML模版
html-webpack-plugin简化了HTML文件的创建,生成一个 HTML5文件, 其中css在head中的标签中引入,webpack打包生成的js在body中的script标签中引入。
Babel
Babel将ES6代码转成ES5的代码。
- 尽可能少的编译文件,是有test或者exclude或者include。设置cacheDirectory为true将转义的结果缓存到文件系统中。
- 在转换 ES2015 语法为 ECMAScript 5 的语法时,babel 会需要一些辅助函数,例如 _extend。babel 默认会将这些辅助函数内联到每一个 js 文件里,这样文件多的时候,项目就会很大。为避免重复引用,babel提供了transform-runtime来将这些函数放到一个单独的模块babel-runtime中。
编译css
- css内联在html的style中
module: {
rules: [
{
test: /\.css$/,
loader: "style-loader!css-loader?modules"
},
]
}
- 拆分css,html页面以link的方式引入
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
module: {
rules: [
{
test: /\.css$/,
/* 抽取css */
use: [
{
loader: MiniCssExtractPlugin.loader,
options: {
publicPath: path.join(__dirname2, 'dist'),
hmr: process.env.NODE_ENV === 'development',
}
},
{
loader: 'css-loader',
options: {
modules: true,
}
}
],
},
]
},
plugins: [
new MiniCssExtractPlugin({
filename: '[name].[hash].css',
chunkFilename: '[id].[hash].css',
}),
]
编译图片
img标引用的图片地址也需要url-loader来帮我们处理。
提取公共代码
optimization中的splitChunks。
文件压缩
new UglifyJSPlugin()
打包前自动清理dist文件夹
打包优化 每次打包前自动清理下dist文件
node模块安装
打包相关
- npm init
- npm install --save-dev webpack@4 webpack-cli@3 webpack-dev-server webpack-merge
- npm install --save-dev html-webpack-plugin clean-webpack-plugin
- 编译JS: npm install --save-dev babel-core@6 babel-loader@7 babel-preset-env babel-preset-react babel-polyfill
- 编译css: npm install --save-dev css-loader style-loader mini-css-extract-plugin
- 编译图片: npm install --save-dev url-loader file-loader
- 按需加载: npm install --save-dev bundle-loader
- 压缩: npm install --save-dev uglifyjs-webpack-plugin
内容相关
- npm install --save react react-dom
- npm install --save redux redux-thunk
- npm install --save react-redux
- npm install --save react-router-dom
webpack配置
webpack.common.config.js
const path = require('path')
const __dirname2 = path.join(__dirname, '../')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const commonConfig = {
/* 入口 */
entry: path.join(__dirname2, 'src/index.js'),
/* 输出 */
output: {
path: path.join(__dirname2, './dist'),
filename: '[name].[chunkhash].js', // 每个输出 bundle 的名称
chunkFilename: '[name].[chunkhash].js', // 非入口chunk文件的名称
publicPath: "/"
},
module: {
rules: [
/* webpack会自动调用.babelrc里的babel配置选项 */
{
test: /\.js$/,
/*cacheDirectory是用来缓存编译结果,下次编译加速*/
use: ['babel-loader?cacheDirectory=true'],
/* src文件夹下面的以.js结尾的文件,要使用babel解析 */
include: path.join(__dirname2, 'src')
},
/* 编译图片 */
{
test: /\.(png|jpg|gif)$/,
use: [{
loader: 'url-loader',
options: {
limit: 8192
}
}]
}
]
},
optimization: {
/* 提取代码 */
splitChunks: {
chunks: 'all',
cacheGroups: {
/* node modules */
vendor: {
test: /[\\/]node_modules[\\/]/,
chunks: 'all',
name: "vendor",
},
/* 自定义的共享的utils方法 */
utils: {
name: "utils",
chunks: "initial",
minSize: 0 // 只要超出0字节就生成一个新包
}
}
},
//提取webpack运行时的代码,名字是manifest
runtimeChunk: {
name: 'manifest'
}
},
plugins: [
new HtmlWebpackPlugin({
filename: 'index.html',
template: path.join(__dirname2, 'src/index.html')
}),
],
};
module.exports = commonConfig
webpack.dev.config.js
const path = require('path')
const merge = require('webpack-merge')
const __dirname2 = path.join(__dirname, '../')
const commonConfig = require('./webpack.common.config.js')
const devConfig = {
mode: 'development',
devtool: 'cheap-module-source-map',
/* 输出到dist文件夹,每次打包生成的名字后面增加chunkhash */
devServer: {
port: 8080,
inline: false,
contentBase: path.join(__dirname2, 'dist'),
historyApiFallback: true,
},
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
modules: true,
//localIdentName: "[name]__[local]__[hash:base64:5]"
}
}
]
},
]
}
}
module.exports = merge(commonConfig, devConfig)
webpack.prod.config.js
const path = require('path');
const merge = require('webpack-merge');
const webpack = require('webpack')
const UglifyJSPlugin = require('uglifyjs-webpack-plugin')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const commonConfig = require('./webpack.common.config.js');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const __dirname2 = path.join(__dirname, '../')
const publicConfig = {
/* webpack 使用生产环境的内置优化 */
mode: 'production',
devtool: 'none',
module: {
rules: [
{
test: /\.css$/,
/* 抽取css */
use: [
{
loader: MiniCssExtractPlugin.loader,
options: {
publicPath: path.join(__dirname2, 'dist'),
hmr: process.env.NODE_ENV === 'development',
}
},
{
loader: 'css-loader',
options: {
modules: true,
//localIdentName: "[name]__[local]__[hash:base64:5]"
}
}
],
},
]
},
plugins: [
/* 文件压缩 */
new UglifyJSPlugin(),
/* 指定环境 process.env.NODE_ENV 决定library中应该引用哪些内容 */
new webpack.DefinePlugin({
'process.env': {
'NODE_ENV': JSON.stringify('production')
}
}),
/* 打包优化 每次打包前自动清理下dist文件夹 */
new CleanWebpackPlugin(),
new MiniCssExtractPlugin({
filename: '[name].[hash].css',
chunkFilename: '[id].[hash].css',
}),
]
}
module.exports = merge(commonConfig, publicConfig)
babel配置
{
"presets": ["react", "env"],
}