2020年手工webpack构建react项目,完美支持ssr,包括css和图片资源
这几天花了大量时间终于折腾出一个完美版本,并且是自己构建的webpack配置(之前失败很可能是因为react自带的webpack太复杂,构建服务端代码时有些细节没处理好)
完整代码上传到了git:https://github.com/liuxiaocong/react-self-customize-webpack-ssr
下载的话麻烦点个start,每一步的commit都有说明,下面再简单说一下:
1,基本项目结构,webpack配置
项目结构,src目录为前端开发,server目录为服务器相关,入口文件为index.js和about.js(如果是单入口站点可以忽略)
看一下package.json里面的指令设置:
"start": "cross-env NODE_ENV=development webpack-dev-server --open --mode development",
"build": "cross-env NODE_ENV=production webpack --mode production",
"server": "nodemon --exec babel-node server/index.js",
"buildServer": "NODE_ENV=development webpack --config ./server/webpack.server.config.js"
######yarn start: 前端代码开发调试.
######yarn build: 前端代码发布,配置文件为项目根目录下的webpack.config.js.
######yarn buildServer: 服务器相关代码打包,这一步是为了支持资源加载如css和image,配置文件为根目录下server目录的webpack.server.config.js
######yarn server: 服务器启动,这一步引用了yarn buildServer打包生产的ssr.js.
前端工程webpack配置,解析js,css,image,打包到根目录下的build文件夹webpack.config.js
const path = require('path');
const HtmlWebPackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
//not need to use this, MiniCssExtractPlugin already support hmr
const cssLoaderLast = process.env.NODE_ENV === 'development'?
'style-loader':
{
loader: MiniCssExtractPlugin.loader,
options: {
publicPath: '/css',
hmr: true,
},
}
module.exports = {
entry: {
main: path.resolve(__dirname, "src/index.js"),
about: path.resolve(__dirname, "src/about.js")
},
output: {
path: path.resolve(__dirname, 'build'), //出口文件輸出的路徑
filename: '[name].js' //出口文件,[name]為入口文件陣列的名稱main喔!
},
plugins: [
new HtmlWebPackPlugin({
template: './src/index.html',
filename: './index.html',
chunks: ['main'],
}),
new HtmlWebPackPlugin({
template: './src/about.html',
filename: './about.html',
chunks: ['about'],
}),
new MiniCssExtractPlugin({
// Options similar to the same options in webpackOptions.output
// both options are optional
filename: '[name].css',
chunkFilename: '[id].css',
}),
],
module: {
rules: [
{
test: /.(js|jsx)$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
},
},
{
test: /.css$/i,
use: [
{
loader: MiniCssExtractPlugin.loader,
options: {
publicPath: '/css',
hmr: true,
},
},
//'style-loader',
{
loader: 'css-loader',
options: {
modules: {
localIdentName: '[path][name]__[local]--[hash:base64:5]',
},
},
}
],
},
{
test: /.(png|jpe?g|gif|svg)$/i,
use: [
{
loader: 'file-loader',
options: {
name: '[contenthash].[ext]',
outputPath: 'images',
},
},
],
},
{
test: /.html$/,
use: [
{
loader: 'html-loader',
},
],
},
],
},
resolve: {
extensions: ['.js', '.jsx', '.json'],
},
optimization: {
splitChunks: {
chunks: 'all',
minSize: 10000,
maxSize: 0,
minChunks: 1,
maxAsyncRequests: 5,
maxInitialRequests: 3,
name: true,
cacheGroups: {
vendors: {
test: /[\/]node_modules[\/]/,
priority: -10,
},
default: {
minChunks: 1,
priority: -20,
reuseExistingChunk: true,
},
},
},
},
};
再看一下server的webpack配置,跟上面很像,改了入口和输出,保证生产的css和image一致就行。
注意下面2行代码:
target: 'node',
externals: nodeExternals(),
这是让输出的js可以在node环境运行,否则会变成引用window对象进行挂接,造成错误。
const path = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const nodeExternals = require('webpack-node-externals');
//not need to use this, MiniCssExtractPlugin already support hmr
const cssLoaderLast = process.env.NODE_ENV === 'development' ?
'style-loader' :
{
loader: MiniCssExtractPlugin.loader,
options: {
publicPath: '/css',
hmr: true,
},
};
module.exports = {
entry: {
main: path.resolve(__dirname, '../src/ssr.js'),
},
output: {
path: path.resolve(__dirname, '../buildSsr'), //出口文件輸出的路徑
filename: '[name].js', //出口文件,[name]為入口文件陣列的名稱main喔!
libraryTarget: 'commonjs2',
},
plugins: [
new MiniCssExtractPlugin({
// Options similar to the same options in webpackOptions.output
// both options are optional
filename: '[name].css',
chunkFilename: '[id].css',
}),
],
target: 'node',
externals: nodeExternals(),
module: {
rules: [
{
test: /.(js|jsx)$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
},
},
{
test: /.css$/i,
use: [
{
loader: MiniCssExtractPlugin.loader,
options: {
publicPath: '/css',
hmr: true,
},
},
//'style-loader',
{
loader: 'css-loader',
options: {
modules: {
localIdentName: '[path][name]__[local]--[hash:base64:5]',
},
},
},
],
},
{
test: /.(png|jpe?g|gif|svg)$/i,
use: [
{
loader: 'file-loader',
options: {
name: '[contenthash].[ext]',
outputPath: 'images',
},
},
],
},
{
test: /.html$/,
use: [
{
loader: 'html-loader',
},
],
},
],
},
resolve: {
extensions: ['.js', '.jsx', '.json'],
},
};
2,服务器启动代码
这部分可以看一下之前的文件https://www.jianshu.com/p/eba973875d22
入口文件是index.js
import express from 'express';
//import compression from 'compression';
import path from 'path';
import { renderToString } from 'react-dom/server';
//https://www.babeljs.cn/docs/babel-register
require('@babel/register')();
require('@babel/polyfill');
require.extensions['.less'] = () => {
return;
};
require.extensions['.css'] = () => {
return;
};
require.extensions['.svg', '.png'] = () => {
return;
};
const renderReact = require('./renderReact.js');
//const router = express.Router();
const app = express();
//app.use(compression());
renderReact(app);
app.use(express.static(path.resolve(__dirname, '../build/')));
const port = process.env.PORT || 4000;
app.listen(port, function listenHandler() {
console.info(`Running on ${ port }`);
});
用的是express,babel require是让后续的运行支持es6语法
######babel/register模块改写require命令,为它加上一个钩子。此后,每当使用require加载.js、.jsx、.es和.es6后缀名的文件,就会先用Babel进行转码:http://www.ruanyifeng.com/blog/2016/01/babel.html
renderReact.js为主要服务端路由配置,为什么要分开的原因上一篇文章也提过了babel-register
doesn't process the file it is called from, see https://stackoverflow.com/a/29425761/1795821
import React from 'react';
import fs from 'fs';
import { StaticRouter } from 'react-router-dom';
import Main from '../src/container/main';
const render = require('../buildSsr/main').default;
const reactDomServer = require('react-dom/server');
console.log('server start ....');
console.log(render);
const useServerBuildFile = true;
let buildHtml;
module.exports = function(app) {
const routerArray = ['/', '/todo', 'about'];
routerArray.forEach((item) => {
if (useServerBuildFile) {
app.get(item, render);
} else {
app.get(item, (req, res) => {
const context = {};
const appHtml = reactDomServer.renderToString(
<StaticRouter location={ req.url } context={ context }>
<Main/>
</StaticRouter>,
);
if (!buildHtml) {
buildHtml = fs.readFileSync('./build/index.html', 'utf8');
}
let result = buildHtml.replace('#body', appHtml);
res.send(result);
});
}
});
};
注意render方法的引用,来源于yarn buildServer生成的ssr.js文件,通过webpack对js和资源进行解析,然后export一个方法给服务器调用
3,前端提供给服务器的入口文件
这个就是核心,src目录下的ssr.js文件,网上其他资料基本没涉及到,很好的一个思路
import React from 'react';
import { renderToString } from 'react-dom/server';
import { StaticRouter } from 'react-router-dom';
import Main from './container/main';
import fs from "fs";
let buildHtml;
export default function render(req, res) {
const context = {};
const appString = renderToString(
<StaticRouter location={ req.url } context={ context }>
<Main/>
</StaticRouter>
);
if (!buildHtml) {
buildHtml = fs.readFileSync('./build/index.html', 'utf8');
}
let result = buildHtml.replace('#body', appString);
console.log(appString);
res.send(result);
};
服务器代码引用的就是render函数,同时资源打包和css解析跟原本的前端js一致,因为基本是同一个webpack配置打包出来的。
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- 记一次DataGuard SWITCHOVER_STATUS 状态为RESOLVABLE GAP的处理
- ABAP数据库表的元数据
- ctfshow-萌新赛
- VC++ libcurl FTP上传客户端程序
- Oracle RAC变更实验之修改11gR2+公网IP地址(网段不变)
- C# 纯控制台创建一个全屏窗口
- MySQL MHA部署添加Linux/Unix基本信息至Django中
- 记一次innobackupex导致的从库无法同步的问题
- mysqlbinlog命令详解记一次有函数的标量子查询导致的查询缓慢
- ctfshow红包题-web
- 微信支付一面(C++后台)
- 强网杯-随便注
- LeetCode 1547. Minimum Cost to Cut a Stick(动态规划)
- 管道符漏洞
- 06-STM32+ESP8266+AIR202远程升级方案-移植STM32+ESP8266实现利用http或https远程更新STM32程序到自己的项目(定时访问升级,备份升级)