Webpack 4 实战:样式处理、性能优化与服务端渲染
1. 样式处理
1.1 样式渲染原理
当我们检查页面时,会发现动态注入了一个带有临时 URL 的
<link>
标签,其中包含编译后的 CSS。类名呈现为 “Home_Home_2kP…” 这种形式,这是因为配置了
localIdentName: '[name]_[local]_[hash:base64]'
,这样可以创建隔离的样式,避免使用相同类名时相互影响。
1.2 实现 CSS 预处理器
若要将 CSS 代码提取到
style.css
文件并在生产模式下压缩代码,可按以下步骤操作:
1.
安装插件
:
npm install extract-text-webpack-plugin@v4.0.0-beta.0
- 添加到 Webpack 插件 :
import HtmlWebPackPlugin from 'html-webpack-plugin';
import ExtractTextPlugin from 'extract-text-webpack-plugin';
const isProduction = process.env.NODE_ENV === 'production';
const plugins = [
new HtmlWebPackPlugin({
title: 'Codejobs',
template: './src/index.html',
filename: './index.html'
})
];
if (isProduction) {
plugins.push(
new ExtractTextPlugin({
allChunks: true,
filename: './css/[name].css'
})
);
}
export default plugins;
-
在
package.json中添加脚本 :
"scripts": {
"start": "webpack-dev-server --mode development --open",
"start-production": "NODE_ENV=production webpack-dev-server --mode production",
"build-development": "webpack --mode development",
"build": "webpack --mode production"
}
- 在终端运行生产模式 :
npm run start-production
-
添加规则到
module节点 :
import ExtractTextPlugin from 'extract-text-webpack-plugin';
const isProduction = process.env.NODE_ENV === 'production';
const rules = [
{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
use: 'babel-loader'
}
];
if (isProduction) {
rules.push({
test: /\.scss/,
use: ExtractTextPlugin.extract({
fallback: 'style-loader',
use: [
'css-loader?minimize=true&modules=true&localIdentName=[name]_[local]_[hash:base64]',
'sass-loader'
]
})
});
} else {
rules.push({
test: /\.scss$/, // .scss - .styl - .less
use: [
{
loader: 'style-loader'
},
{
loader: 'css-loader',
options: {
modules: true,
importLoaders: 1,
localIdentName: '[name]_[local]_[hash:base64]',
sourceMap: true,
minimize: true
}
},
{
loader: 'sass-loader' // sass-loader, stylus-loader or less-loader
}
]
});
}
export default {
rules
};
2. Webpack 4 优化 - 拆分捆绑包
2.1 准备工作
安装以下包:
npm install webpack-bundle-analyzer webpack-notifier
2.2 操作步骤
-
添加源映射
:
创建webpack/configuration/devtool.js文件:
const isProduction = process.env.NODE_ENV === 'production';
export default !isProduction ? 'cheap-module-source-map' : 'eval';
-
拆分捆绑包
:
创建optimization.js文件:
export default {
splitChunks: {
cacheGroups: {
default: false,
commons: {
test: /node_modules/,
name: 'vendor',
chunks: 'all'
}
}
}
}
-
将新文件导入
index.js:
// Configuration
import devtool from './devtool';
import module from './module';
import optimization from './optimization';
import plugins from './plugins';
import resolve from './resolve';
export {
devtool,
module,
optimization,
plugins,
resolve
};
-
将节点添加到
webpack.config.babel.js:
import {
devtool,
module,
optimization,
plugins,
resolve
} from './webpack/configuration';
export default {
devtool,
module,
plugins,
optimization,
resolve
};
2.3 测试效果
-
运行
npm start,查看 HTML 会自动注入vendor.js和main.js捆绑包。 -
运行
npm run start-production,会发现捆绑包变小,优化后捆绑包大小减少 40%。
2.4 使用插件
- BundleAnalyzerPlugin :可查看所有包和组件的大小。
- WebpackNotifierPlugin :每次 Webpack 构建时显示通知。
import HtmlWebPackPlugin from 'html-webpack-plugin';
import ExtractTextPlugin from 'extract-text-webpack-plugin';
import WebpackNotifierPlugin from 'webpack-notifier';
import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer';
const isProduction = process.env.NODE_ENV === 'production';
const plugins = [
new HtmlWebPackPlugin({
title: 'Codejobs',
template: './src/index.html',
filename: './index.html'
})
];
if (isProduction) {
plugins.push(
new ExtractTextPlugin({
allChunks: true,
filename: './css/[name].css'
})
);
} else {
plugins.push(
new BundleAnalyzerPlugin(),
new WebpackNotifierPlugin({
title: 'CodeJobs'
})
);
}
export default plugins;
3. 结合 Node.js、React/Redux 和 Webpack 4
3.1 准备工作
安装以下包:
npm install babel-cli express nodemon react-hot-loader react-router-dom webpack-hot-middleware compression-webpack-plugin react-redux redux
3.2 实现步骤
-
在
.babelrc中添加react-hot-loader插件 :
{
"presets": ["env", "react"],
"env": {
"development": {
"plugins": [
"react-hot-loader/babel"
]
}
}
}
-
创建 Express 服务器
:
创建src/server/index.js文件:
// Dependencies
import express from 'express';
import path from 'path';
import webpackDevMiddleware from 'webpack-dev-middleware';
import webpackHotMiddleware from 'webpack-hot-middleware';
import webpack from 'webpack';
// Webpack Configuration
import webpackConfig from '../../webpack.config.babel';
// Client Render
import clientRender from './render/clientRender';
// Utils
import { isMobile } from '../shared/utils/device';
// Environment
const isProduction = process.env.NODE_ENV === 'production';
// Express Application
const app = express();
// Webpack Compiler
const compiler = webpack(webpackConfig);
// Webpack Middleware
if (!isProduction) {
// Hot Module Replacement
app.use(webpackDevMiddleware(compiler));
app.use(webpackHotMiddleware(compiler));
} else {
// Public directory
app.use(express.static(path.join(__dirname, '../../public')));
// GZip Compression just for Production
app.get('*.js', (req, res, next) => {
req.url = `${req.url}.gz`;
res.set('Content-Encoding', 'gzip');
next();
});
}
// Device Detection
app.use((req, res, next) => {
req.isMobile = isMobile(req.headers['user-agent']);
next();
});
// Client Side Rendering
app.use(clientRender());
// Disabling x-powered-by
app.disable('x-powered-by');
// Listen Port 3000...
app.listen(3000);
- 创建设备检测工具文件 :
export function getCurrentDevice(ua) {
return /mobile/i.test(ua) ? 'mobile' : 'desktop';
}
export function isDesktop(ua) {
return !/mobile/i.test(ua);
}
export function isMobile(ua) {
return /mobile/i.test(ua);
}
- 创建设备 reducer :
export default function deviceReducer(state = {}) {
return state;
}
- 合并 reducers :
// Dependencies
import { combineReducers } from 'redux';
// Shared Reducers
import device from './deviceReducer';
const rootReducer = combineReducers({
device
});
export default rootReducer;
- 创建初始状态文件 :
export default req => ({
device: {
isMobile: req.isMobile
}
});
- 配置 Redux 存储 :
// Dependencies
import { createStore } from 'redux';
// Root Reducer
import rootReducer from '../reducers';
export default function configureStore(initialState) {
return createStore(
rootReducer,
initialState
);
}
- 创建 HTML 渲染文件 :
// Dependencies
import serialize from 'serialize-javascript';
// Environment
const isProduction = process.env.NODE_ENV === 'production';
export default function html(options) {
const { title, initialState } = options;
let path = '/';
let link = '';
if (isProduction) {
path = '/app/';
link = `<link rel="stylesheet" href="${path}css/main.css" />`;
}
return `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>${title}</title>
${link}
</head>
<body>
<div id="root"></div>
<script>
window.initialState = ${serialize(initialState)};
</script>
<script src="${path}vendor.js"></script>
<script src="${path}main.js"></script>
</body>
</html>
`;
}
- 创建 HTML 渲染函数 :
// HTML
import html from './html';
// Initial State
import initialState from './initialState';
export default function clientRender() {
return (req, res) => res.send(html({
title: 'Codejobs',
initialState: initialState(req)
}));
}
- 创建客户端主入口文件 :
// Dependencies
import React from 'react';
import { render } from 'react-dom';
import { Provider } from 'react-redux';
import { AppContainer } from 'react-hot-loader';
// Redux Store
import configureStore from './shared/redux/configureStore';
// Components
import App from './client/App';
// Configuring Redux Store
const store = configureStore(window.initialState);
// Root element
const rootElement = document.querySelector('#root');
// App Wrapper
const renderApp = Component => {
render(
<AppContainer>
<Provider store={store}>
<Component />
</Provider>
</AppContainer>,
rootElement
);
};
// Rendering app
renderApp(App);
// Hot Module Replacement
if (module.hot) {
module.hot.accept('./client/App', () => {
renderApp(require('./client/App').default);
});
}
- 创建客户端路由文件 :
// Dependencies
import React from 'react';
import { BrowserRouter, Switch, Route } from 'react-router-dom';
// Components
import About from './components/About';
import Home from './components/Home';
const App = () => (
<BrowserRouter>
<Switch>
<Route exact path="/" component={Home} />
<Route exact path="/about" component={About} />
</Switch>
</BrowserRouter>
);
export default App;
- 创建 About 组件 :
import React from 'react';
import { bool } from 'prop-types';
import { connect } from 'react-redux';
import styles from './About.scss';
const About = ({ isMobile }) => (
<h1 className={styles.About}>About - {isMobile ? 'mobile' : 'desktop'}</h1>
);
About.propTypes = {
isMobile: bool
};
export default connect(({ device }) => ({
isMobile: device.isMobile
}))(About);
- 添加 About 组件样式 :
$color: green;
.About {
color: $color;
}
- 添加 React Hot Loader 入口 :
const isProduction = process.env.NODE_ENV === 'production';
const entry = [];
if (!isProduction) {
entry.push(
'webpack-hot-middleware/client?path=http://localhost:3000/__webpack_hmr&reload=true',
'react-hot-loader/patch',
'./src/index.jsx'
);
} else {
entry.push('./src/index.jsx');
}
export default entry;
- 创建输出文件配置 :
// Dependencies
import path from 'path';
export default {
filename: '[name].js',
path: path.resolve(__dirname, '../../public/app'),
publicPath: '/'
};
-
将文件导入
index.js:
// Configuration
import devtool from './devtool';
import entry from './entry';
import mode from './mode';
import module from './module';
import optimization from './optimization';
import output from './output';
import plugins from './plugins';
import resolve from './resolve';
export {
devtool,
entry,
mode,
module,
optimization,
output,
plugins,
resolve
};
- 创建模式配置文件 :
const isProduction = process.env.NODE_ENV === 'production';
export default !isProduction ? 'development' : 'production';
-
添加插件到
plugins.js:
import ExtractTextPlugin from 'extract-text-webpack-plugin';
import WebpackNotifierPlugin from 'webpack-notifier';
import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer';
import CompressionPlugin from 'compression-webpack-plugin';
import webpack from 'webpack';
const isProduction = process.env.NODE_ENV === 'production';
const plugins = [];
if (isProduction) {
plugins.push(
new ExtractTextPlugin({
allChunks: true,
filename: './css/[name].css'
}),
new CompressionPlugin({
asset: '[path].gz[query]',
algorithm: 'gzip',
test: /\.js$/,
threshold: 10240,
minRatio: 0.8
})
);
} else {
plugins.push(
new webpack.HotModuleReplacementPlugin(),
new BundleAnalyzerPlugin(),
new WebpackNotifierPlugin({
title: 'CodeJobs'
})
);
}
export default plugins;
-
更新
package.json脚本 :
"scripts": {
"build": "NODE_ENV=production webpack",
"clean": "rm -rf public/app",
"start": "npm run clean && NODE_ENV=development nodemon src/server --watch src/server --exec babel-node --presets es2015",
"start-production": "npm run clean && npm run build && NODE_ENV=production babel-node src/server --presets es2015"
}
3.3 运行效果
-
运行
npm start,可看到页面,打开浏览器控制台会发现 HMR 已连接,修改Home组件内容可实现无刷新更新。 -
运行
npm run start-production,可在生产模式下运行应用,捆绑包会变小。
3.4 使用别名简化导入路径
-
安装
babel-plugin-module-resolver包 :
npm install babel-plugin-module-resolver
-
在
.babelrc中添加别名 :
{
"presets": ["env", "react"],
"env": {
"development": {
"plugins": [
"react-hot-loader/babel"
]
}
},
"plugins": [
["module-resolver", {
"root": ["./"],
"alias": {
"@App": "./src/client/App.jsx",
"@client": "./src/client/",
"@components": "./src/client/components",
"@configureStore": "./src/shared/redux/configureStore.js",
"@reducers": "./src/shared/reducers",
"@server": "./src/server/",
"@utils": "./src/shared/utils",
"@webpack": "./webpack.config.babel.js"
}
}]
]
}
流程图
graph TD;
A[开始] --> B[安装依赖];
B --> C[配置 Webpack];
C --> D[创建服务器];
D --> E[配置 Redux];
E --> F[创建组件和路由];
F --> G[添加样式和插件];
G --> H[运行开发或生产模式];
H --> I[测试应用];
总结
通过以上步骤,我们实现了 Webpack 4 的样式处理、性能优化以及结合 Node.js、React/Redux 的应用开发。掌握这些技巧可以帮助我们构建更高效、更强大的 Web 应用。在实际开发中,可根据项目需求灵活调整配置和代码。
4. 实现服务器端渲染
4.1 服务器端渲染原理
传统的 React 通常使用客户端渲染(CSR),即动态地将 HTML 代码注入到目标 div 中。而服务器端渲染(SSR)则是在服务器端生成完整的 HTML 页面,然后将其发送到客户端。这样做的好处包括更好的搜索引擎优化(SEO)、更快的首屏加载时间等。
4.2 实现步骤
-
安装必要的依赖
:
- 确保已经安装了express、react、react - dom、react - redux、redux等相关依赖。 -
创建服务器端渲染逻辑
:
- 在服务器端代码中,使用ReactDOMServer.renderToString方法将 React 组件渲染为字符串。
- 示例代码如下:
import React from'react';
import { renderToString } from'react - dom/server';
import { Provider } from'react - redux';
import { createStore } from'redux';
import rootReducer from './reducers';
import App from './client/App';
// 创建 Redux 存储
const store = createStore(rootReducer);
// 渲染 React 组件为字符串
const html = renderToString(
<Provider store={store}>
<App />
</Provider>
);
// 返回包含渲染结果的 HTML 页面
const fullHtml = `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf - 8">
<title>Server - Side Rendered App</title>
</head>
<body>
<div id="root">${html}</div>
<script>
window.__INITIAL_STATE__ = ${JSON.stringify(store.getState())};
</script>
<script src="/bundle.js"></script>
</body>
</html>
`;
-
在 Express 服务器中使用服务器端渲染
:
- 在 Express 服务器代码中,处理客户端请求并返回服务器端渲染的 HTML 页面。
- 示例代码如下:
import express from 'express';
import { getFullHtml } from './render'; // 假设 getFullHtml 是上面生成 fullHtml 的函数
const app = express();
app.get('/', (req, res) => {
const html = getFullHtml();
res.send(html);
});
app.listen(3000, () => {
console.log('Server is running on port 3000');
});
-
在客户端激活 React 应用
:
- 在客户端代码中,使用ReactDOM.hydrate方法将服务器端渲染的 HTML 激活为 React 应用。
- 示例代码如下:
import React from'react';
import { hydrate } from'react - dom';
import { Provider } from'react - redux';
import { createStore } from'redux';
import rootReducer from './reducers';
import App from './client/App';
// 获取服务器端传递的初始状态
const initialState = window.__INITIAL_STATE__;
// 创建 Redux 存储
const store = createStore(rootReducer, initialState);
// 激活 React 应用
hydrate(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
4.3 服务器端渲染的优势和挑战
| 优势 | 挑战 |
|---|---|
| 更好的 SEO,搜索引擎可以直接抓取完整的 HTML 页面 | 服务器端负载增加,需要更多的服务器资源 |
| 更快的首屏加载时间,用户可以更快地看到页面内容 | 开发和调试复杂度增加,需要处理服务器端和客户端的不同环境 |
| 支持不支持 JavaScript 的设备 | 需要处理服务器端和客户端状态同步的问题 |
5. 实现带有服务器端渲染的 Promise
5.1 需求背景
在服务器端渲染中,有时候组件可能依赖于异步数据,例如从 API 获取数据。这就需要处理 Promise,确保在数据获取完成后再进行渲染。
5.2 实现步骤
-
在组件中添加异步数据获取逻辑
:
- 例如,在一个UserList组件中,从 API 获取用户列表。
import React, { useEffect, useState } from'react';
const UserList = () => {
const [users, setUsers] = useState([]);
useEffect(() => {
const fetchUsers = async () => {
const response = await fetch('https://api.example.com/users');
const data = await response.json();
setUsers(data);
};
fetchUsers();
}, []);
return (
<div>
<h2>User List</h2>
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</div>
);
};
export default UserList;
-
在服务器端等待 Promise 完成
:
- 在服务器端渲染时,需要等待组件中的 Promise 完成后再进行渲染。
- 可以使用Promise.all来等待多个 Promise。
import React from'react';
import { renderToString } from'react - dom/server';
import { Provider } from'react - redux';
import { createStore } from'redux';
import rootReducer from './reducers';
import App from './client/App';
// 假设 App 组件包含需要等待的异步操作
const fetchData = () => {
// 这里可以处理组件中的异步操作
return Promise.resolve();
};
const renderApp = async () => {
await fetchData();
const store = createStore(rootReducer);
const html = renderToString(
<Provider store={store}>
<App />
</Provider>
);
return html;
};
const getFullHtml = async () => {
const appHtml = await renderApp();
return `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf - 8">
<title>Server - Side Rendered App with Promise</title>
</head>
<body>
<div id="root">${appHtml}</div>
<script>
window.__INITIAL_STATE__ = ${JSON.stringify(store.getState())};
</script>
<script src="/bundle.js"></script>
</body>
</html>
`;
};
export { getFullHtml };
6. 使用 Next.js 进行服务器端渲染
6.1 Next.js 简介
Next.js 是一个基于 React 的服务器端渲染框架,它简化了服务器端渲染的开发过程,提供了很多内置的功能,如自动代码分割、静态生成等。
6.2 快速开始
-
创建 Next.js 项目
:
- 使用npx create - next - app my - next - app创建一个新的 Next.js 项目。 -
创建页面
:
- 在pages目录下创建页面组件,Next.js 会自动将这些组件映射为路由。
- 例如,创建pages/index.js:
import React from'react';
const HomePage = () => {
return (
<div>
<h1>Welcome to Next.js</h1>
</div>
);
};
export default HomePage;
-
运行开发服务器
:
- 在项目根目录下运行npm run dev,启动开发服务器。
- 打开浏览器访问http://localhost:3000即可看到页面。
6.3 Next.js 的高级功能
- 静态生成(SSG) :在构建时生成静态 HTML 页面,适合内容更新不频繁的页面。
- 增量静态再生(ISR) :在构建时生成静态页面,并且可以在运行时更新这些页面。
- API 路由 :在 Next.js 中可以轻松创建 API 路由,处理服务器端逻辑。
流程图
graph TD;
A[开始服务器端渲染] --> B[安装依赖];
B --> C[创建服务器端渲染逻辑];
C --> D[处理异步数据(Promise)];
D --> E[使用 Next.js 简化开发];
E --> F[部署应用];
F --> G[测试和优化];
总结
服务器端渲染是提升 Web 应用性能和 SEO 的重要手段。通过实现服务器端渲染、处理 Promise 以及使用 Next.js 等框架,我们可以构建出更高效、更友好的 Web 应用。在实际开发中,需要根据项目的需求和特点选择合适的技术和方案,同时要注意处理好服务器端和客户端的不同环境以及状态同步等问题。
超级会员免费看
1761

被折叠的 条评论
为什么被折叠?



