在前两篇文章中,我们已经介绍了如何使用 Webpack 构建 React 后台管理系统的基本步骤和常用配置。本文将进一步深入探讨 Webpack 的高级配置和优化技巧,以及如何使用 React Router 和 Redux 管理应用状态。
Webpack 高级配置
1. 处理 CSS
在前面的文章中,我们已经介绍了如何使用 css-loader 和 style-loader 处理 CSS 文件。但是,当我们的 CSS 文件很多时,这种方式就不太适用了。因为每个 CSS 文件都会被打包成一个 <style> 标签,这样会导致页面加载速度变慢。
为了解决这个问题,我们可以使用 MiniCssExtractPlugin 插件将 CSS 文件提取为独立的文件,这样不仅可以减少页面加载时间,还可以将样式文件与 JavaScript 文件分开缓存。
// www.javascriptcn.com code example
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
// ...
module: {
rules: [
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
],
},
],
},
plugins: [
new MiniCssExtractPlugin({
filename: 'css/[name].[hash:8].css',
chunkFilename: 'css/[name].[hash:8].chunk.css',
}),
],
};2. 配置文件别名
在开发过程中,我们经常需要引用一些公共的组件或工具函数。如果每次都使用相对路径来引用这些文件,会非常麻烦。为了解决这个问题,我们可以使用 Webpack 的 resolve.alias 配置项,给常用的文件夹或文件设置别名。
// www.javascriptcn.com code example
module.exports = {
// ...
resolve: {
alias: {
'@': path.resolve(__dirname, 'src'),
'components': path.resolve(__dirname, 'src/components'),
'utils': path.resolve(__dirname, 'src/utils'),
},
},
};这样,在代码中就可以使用别名来引用文件了。
import Button from 'components/Button';
import { formatDate } from 'utils/date';3. 代码分割
随着应用规模的增大,JavaScript 文件也会变得越来越庞大。为了提高页面加载速度,我们可以使用 Webpack 的代码分割功能,将代码分割成多个小文件,按需加载。
Webpack 4 默认支持动态导入语法(Dynamic Import),可以将异步加载的模块打包成一个独立的 chunk。
import('./components/Button').then(Button => {
// ...
});我们也可以使用 react-loadable 库来实现代码分割。
import Loadable from 'react-loadable';
const Loading = () => <div>Loading...</div>;
const Button = Loadable({
loader: () => import(/* webpackChunkName: "Button" */ './components/Button'),
loading: Loading,
});4. 优化打包体积
在前面的文章中,我们已经介绍了如何使用 UglifyJSPlugin 插件压缩 JavaScript 代码。除此之外,还可以使用以下优化方式来减小打包体积。
4.1 Tree Shaking
Tree Shaking 是指通过静态分析,将没有使用的代码从打包结果中剔除。Webpack 4 默认支持 Tree Shaking,只需要在配置文件中将 mode 设置为 'production',即可开启该功能。
module.exports = {
mode: 'production',
// ...
};4.2 Scope Hoisting
Scope Hoisting 是指将模块的作用域尽可能提升到最高层,减少模块间的闭包调用,从而减小打包体积。Webpack 3 可以使用 ModuleConcatenationPlugin 插件开启该功能。
const webpack = require('webpack');
module.exports = {
// ...
plugins: [
new webpack.optimize.ModuleConcatenationPlugin(),
],
};Webpack 4 默认开启了 Scope Hoisting,不需要额外配置。
5. 处理图片和字体
在前面的文章中,我们已经介绍了如何使用 file-loader 处理图片和字体文件。但是,当图片和字体文件很多时,这种方式也不太适用了。因为每个文件都会被打包成一个单独的文件,这样会导致 HTTP 请求过多,影响页面加载速度。
为了解决这个问题,我们可以使用 url-loader,将小于指定大小的文件转换成 Data URL,减少 HTTP 请求。同时,使用 image-webpack-loader 压缩图片文件。
// www.javascriptcn.com code example
module.exports = {
// ...
module: {
rules: [
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
use: [
{
loader: 'url-loader',
options: {
limit: 8192,
name: 'img/[name].[hash:8].[ext]',
},
},
{
loader: 'image-webpack-loader',
options: {
mozjpeg: {
progressive: true,
quality: 65,
},
optipng: {
enabled: false,
},
pngquant: {
quality: '65-90',
speed: 4,
},
gifsicle: {
interlaced: false,
},
webp: {
quality: 75,
},
},
},
],
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
use: [
{
loader: 'url-loader',
options: {
limit: 8192,
name: 'fonts/[name].[hash:8].[ext]',
},
},
],
},
],
},
};使用 React Router 管理路由
React Router 是 React 官方推荐的路由库,可以帮助我们实现前端路由和页面跳转。使用 React Router,我们可以将页面拆分成多个组件,按需加载。
安装 React Router
npm install react-router-dom --save
基本用法
// www.javascriptcn.com code example
import React from 'react';
import { BrowserRouter as Router, Route, Link } from 'react-router-dom';
const Home = () => <h2>Home</h2>;
const About = () => <h2>About</h2>;
const Topics = ({ match }) => (
<div>
<h2>Topics</h2>
<ul>
<li>
<Link to={`${match.url}/components`}>Components</Link>
</li>
<li>
<Link to={`${match.url}/props-v-state`}>Props v. State</Link>
</li>
</ul>
<Route path={`${match.path}/:topicId`} component={Topic} />
<Route
exact
path={match.path}
render={() => <h3>Please select a topic.</h3>}
/>
</div>
);
const Topic = ({ match }) => <h3>{match.params.topicId}</h3>;
const App = () => (
<Router>
<div>
<nav>
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/about">About</Link>
</li>
<li>
<Link to="/topics">Topics</Link>
</li>
</ul>
</nav>
<Route exact path="/" component={Home} />
<Route path="/about" component={About} />
<Route path="/topics" component={Topics} />
</div>
</Router>
);
export default App;嵌套路由
// www.javascriptcn.com code example
import React from 'react';
import { BrowserRouter as Router, Route, Link } from 'react-router-dom';
const Home = () => <h2>Home</h2>;
const About = () => <h2>About</h2>;
const Topics = ({ match }) => (
<div>
<h2>Topics</h2>
<ul>
<li>
<Link to={`${match.url}/components`}>Components</Link>
</li>
<li>
<Link to={`${match.url}/props-v-state`}>Props v. State</Link>
</li>
</ul>
<Route path={`${match.path}/:topicId`} component={Topic} />
<Route
exact
path={match.path}
render={() => <h3>Please select a topic.</h3>}
/>
</div>
);
const Topic = ({ match }) => (
<div>
<h3>{match.params.topicId}</h3>
<ul>
<li>
<Link to={`${match.url}/subtopic-1`}>Subtopic 1</Link>
</li>
<li>
<Link to={`${match.url}/subtopic-2`}>Subtopic 2</Link>
</li>
</ul>
<Route path={`${match.path}/:subtopicId`} component={Subtopic} />
<Route
exact
path={match.path}
render={() => <h4>Please select a subtopic.</h4>}
/>
</div>
);
const Subtopic = ({ match }) => <h4>{match.params.subtopicId}</h4>;
const App = () => (
<Router>
<div>
<nav>
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/about">About</Link>
</li>
<li>
<Link to="/topics">Topics</Link>
</li>
</ul>
</nav>
<Route exact path="/" component={Home} />
<Route path="/about" component={About} />
<Route path="/topics" component={Topics} />
</div>
</Router>
);
export default App;使用 Redux 管理状态
Redux 是一个状态管理库,可以帮助我们管理应用的状态。使用 Redux,我们可以将应用的状态保存在一个全局的 Store 中,方便组件间共享和管理。
安装 Redux
npm install redux react-redux --save
基本用法
// www.javascriptcn.com code example
import React from 'react';
import { connect, Provider } from 'react-redux';
import { createStore } from 'redux';
// Action Types
const INCREMENT = 'INCREMENT';
const DECREMENT = 'DECREMENT';
// Action Creators
const increment = () => ({ type: INCREMENT });
const decrement = () => ({ type: DECREMENT });
// Reducer
const counterReducer = (state = 0, action) => {
switch (action.type) {
case INCREMENT:
return state + 1;
case DECREMENT:
return state - 1;
default:
return state;
}
};
// Store
const store = createStore(counterReducer);
// Counter Component
const Counter = ({ count, increment, decrement }) => (
<div>
<h2>Count: {count}</h2>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
</div>
);
const mapStateToProps = state => ({
count: state,
});
const mapDispatchToProps = {
increment,
decrement,
};
const ConnectedCounter = connect(
mapStateToProps,
mapDispatchToProps,
)(Counter);
const App = () => (
<Provider store={store}>
<ConnectedCounter />
</Provider>
);
export default App;使用中间件
Redux 中间件可以在 Action 发出后,到达 Reducer 前,对 Action 进行拦截和处理。常用的中间件有 redux-thunk 和 redux-saga。
redux-thunk
redux-thunk 允许 Action Creator 返回一个函数,而不是一个普通的 Action 对象。这个函数可以在内部执行异步操作,并在操作完成后再 dispatch 一个 Action。
// www.javascriptcn.com code example
import { applyMiddleware, createStore } from 'redux';
import thunk from 'redux-thunk';
// Action Types
const FETCH_USERS_REQUEST = 'FETCH_USERS_REQUEST';
const FETCH_USERS_SUCCESS = 'FETCH_USERS_SUCCESS';
const FETCH_USERS_FAILURE = 'FETCH_USERS_FAILURE';
// Action Creators
const fetchUsersRequest = () => ({ type: FETCH_USERS_REQUEST });
const fetchUsersSuccess = users => ({ type: FETCH_USERS_SUCCESS, users });
const fetchUsersFailure = error => ({ type: FETCH_USERS_FAILURE, error });
const fetchUsers = () => {
return dispatch => {
dispatch(fetchUsersRequest());
fetch('/api/users')
.then(response => response.json())
.then(users => dispatch(fetchUsersSuccess(users)))
.catch(error => dispatch(fetchUsersFailure(error)));
};
};
// Reducer
const usersReducer = (state = { loading: false, data: [], error: null }, action) => {
switch (action.type) {
case FETCH_USERS_REQUEST:
return { ...state, loading: true, error: null };
case FETCH_USERS_SUCCESS:
return { ...state, loading: false, data: action.users };
case FETCH_USERS_FAILURE:
return { ...state, loading: false, error: action.error };
default:
return state;
}
};
// Store
const store = createStore(usersReducer, applyMiddleware(thunk));redux-saga
redux-saga 使用 Generator 函数实现异步操作的流程控制。使用 redux-saga,我们可以将异步操作放在 Saga 函数中,然后在监听器中调用。
// www.javascriptcn.com code example
import { applyMiddleware, createStore } from 'redux';
import createSagaMiddleware from 'redux-saga';
import { call, put, takeEvery } from 'redux-saga/effects';
// Action Types
const FETCH_USERS_REQUEST = 'FETCH_USERS_REQUEST';
const FETCH_USERS_SUCCESS = 'FETCH_USERS_SUCCESS';
const FETCH_USERS_FAILURE = 'FETCH_USERS_FAILURE';
// Action Creators
const fetchUsersRequest = () => ({ type: FETCH_USERS_REQUEST });
const fetchUsersSuccess = users => ({ type: FETCH_USERS_SUCCESS, users });
const fetchUsersFailure = error => ({ type: FETCH_USERS_FAILURE, error });
// Saga
function* fetchUsersSaga() {
try {
yield put(fetchUsersRequest());
const users = yield call(() => fetch('/api/users').then(response => response.json()));
yield put(fetchUsersSuccess(users));
} catch (error) {
yield put(fetchUsersFailure(error));
}
}
// Reducer
const usersReducer = (state = { loading: false, data: [], error: null }, action) => {
switch (action.type) {
case FETCH_USERS_REQUEST:
return { ...state, loading: true, error: null };
case FETCH_USERS_SUCCESS:
return { ...state, loading: false, data: action.users };
case FETCH_USERS_FAILURE:
return { ...state, loading: false, error: action.error };
default:
return state;
}
};
// Store
const sagaMiddleware = createSagaMiddleware();
const store = createStore(usersReducer, applyMiddleware(sagaMiddleware));
sagaMiddleware.run(function* () {
yield takeEvery(FETCH_USERS_REQUEST, fetchUsersSaga);
});示例代码
完整示例代码可以在 GitHub 上查看。
来源:JavaScript中文网 ,转载请注明来源 https://www.javascriptcn.com/post/67d0ef23a941bf71341fe68b