webpack构建React工程(第一季)
说明:阅读本文需要一定的前置知识:如 ES6的基本语法、Node+npm的基本用法、使用过webpck、对React的基本掌握等,不然很有可能看不懂该文!
一、webpack构建React开发环境基本用法(小白篇)
- 创建一个空文件夹,我这里命名为"react-build-with-webpack",
cd
进入该文件夹,使用npm
初始化该文件夹:mkdir react-build-webpack cd react-build-webpack npm init
执行完上面的命令后,会产生一个package.json
文件,这是npm的配置文件,相信了解过node.js
的你,肯定知道它的作用
-
下载安装必要的模块(我使用的是
cnpm
, 而且下载的模块都是最新的版本)
react:cnpm i react react-dom -S
webpack:
cnpm i webpack webpack-cli -D
babel:
cnpm i babel-loader babel-core babel-preset-env \ babel-preset-react -D
webpack plugin:
cnpm i html-webpack-plugin -D
必须安装好上面的模块,才能正常使用webpack管理和打包React代码哟!
查看package.json,可以看到我们安装的模块:
"dependencies": { "react": "^16.3.2", "react-dom": "^16.3.2" }, "devDependencies": { "babel-core": "^6.26.3", "babel-loader": "^7.1.4", "babel-preset-env": "^1.7.0", "babel-preset-react": "^6.24.1", "html-webpack-plugin": "^3.2.0", "webpack": "^4.8.3", "webpack-cli": "^2.1.3" }
-
接下了我们来创建基本的目录结构 + 文件 如下图:
-
build目录放置webpack的配置文件: webpack.config.js
const path = require('path'); const HTMLPlugin = require('html-webpack-plugin') const config = { mode: 'development', //开发模式 entry: { app: path.resolve(__dirname, '../client/app.js') //入口文件 }, output: { path: path.resolve(__dirname, '../dist/'), // 输出路径 filename: '[name].[hash:8].js', // 输出的文件名(带版本号) }, // 模块管理 module: { // 规则匹配,并使用loader处理 rules: [ // 使用babel-loader来处理js文件,及jsx文件 { test: /\.(js|jsx)$/i, loader: 'babel-loader', exclude: path.join(__dirname, '../node_modules') } ] }, // webpack插件 plugins: [ // 引入模板文件插件 new HTMLPlugin({ template: path.resolve(__dirname, '../client/index.html') }) ] }; module.exports = config;
-
client目录放置客户端代码:
- 入口文件:app.js
import React from 'react'; import ReactDOM from 'react-dom'; import App from './App.jsx'; // 将App组件渲染到html页面 ReactDOM.render(<App />, document.getElementById('root'));
- App组件: app.jsx
import React from 'react'; // 一个简单的function组件 export default () => { return ( <h1>世界,你好!</h1> ) }
- 模板文件 index.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>webpack-with-react</title> </head> <body> <div id="root"></div> </body> </html>
-
babel配置文件: .babelrc
{ "presets": [ "env", "react" ] }
综上,需要你创建的目录及文件大概就是这样了,不过,我们还需要在package.json中编写一条脚本命令,这样才能使用webpack进行打包
- 打开package.json, 在scripts中添加build命令:
"scripts": { "test": "echo \"Error: no test specified\" && exit 1", "build": "webpack --config build/webpack.config.js" },
-
好了, 万事俱备! 在react-build-with-webpack目录下,执行下面命令,便会在当前目录下生成dist文件夹:
npm run build
浏览器打开该文件夹下的index.html
通过webpack打包的react应用,便可以在浏览器正常显示了
二、配合webpack-dev-server + hot-module-replacement (进阶篇)
一、webpack-dev-server的配置及使用
-
首先删除(小白篇)生成的dist目录,因为使用webpack-dev-server时,会优先使用磁盘中的dist目录,但是每次webpack-dev-server更新的打包文件版本号不一样,否则会出错
-
下载 webpack-dev-server 模块:
cnpm i webpack-dev-server -D
- 以及下载可以在执行package.json脚本时给node.js传入环境变量的模块 cross-env
cnpm i cross-env -D
- 在package.json执行脚本中加入一条命令 devc(开启客户端调试的命令):
"scripts": {
"build": "webpack --config build/webpack.config.js",
"devc": "cross-env NODE_ENV=development webpack-dev-server --config build/webpack.config.js"
},
- 编写build/webpack.config.js文件:
const path = require('path');
const HTMLPlugin = require('html-webpack-plugin')
// 是否为开发环境
const isDev = process.env.NODE_ENV === 'development'
const config = {
mode: isDev ? 'development' : 'production', //开发模式
entry: {
app: path.resolve(__dirname, '../client/app.js') //入口文件
},
output: {
path: path.resolve(__dirname, '../dist/'), // 输出路径
filename: '[name].[hash:8].js' // 输出的文件名(带版本号)
},
// 模块管理
module: {
// 规则匹配,并使用loader处理
rules: [
// 使用babel-loader来处理js文件,及jsx文件
{
test: /\.(js|jsx)$/i,
loader: 'babel-loader',
exclude: path.join(__dirname, '../node_modules')
}
]
},
// webpack插件
plugins: [
// 引入模板文件插件
new HTMLPlugin({
template: path.resolve(__dirname, '../client/index.html')
})
]
};
if (isDev) {
// webpack-dev-server配置
config.devServer = {
host: '0.0.0.0', // 域名
port: 8000, // 端口
contentBase: path.resolve(__dirname, '../dist/'), //静态文件路径
overlay: true // 开启错误调试
}
}
module.exports = config;
- 完成上面的几步之后,webpack-dev-server基本配置完毕,接下来运行客户端调试环境:
npm run devc
运行成功后,访问 localhost:8000 就可以看到我们的react调试页面了!
接着你可以把client/App.jsx中的“世界,你好!”改成别的文字,然后保存, 你会发现,localhost:8000页面会自动刷新,显示你更改后的效果;这就是我们使用webpack-dev-server的真正原因
当然, 光使用webpack-dev-server还不够,因为每次更新代码都会刷新页面,浏览器会重新渲染页面,缺点不言而喻。我们应该利用react的虚拟DOM机制,局部刷新我们更改的部分,而不是整个页面进行刷新,接下来我们使用webpack的hot-module-replacement解决这个问题
二、hot-module-replacement的使用及配置
- 需配合react的babel插件
react-hot-loader
,所以需要先安装一下
cnpm i react-hot-loader -D
-
react-hot-loader配置三部曲,这里我贴上官网的介绍: 点击进入官网查看
-
根据什么的三部曲进行配置:
- 编辑.babelrc 文件
{ "presets": [ "env", "react" ], "plugins": ["react-hot-loader/babel"] }
- 编辑client/App.jsx
import React from 'react'; import { hot } from 'react-hot-loader'; // 一个简单的function组件 const App = () => <h1>佛曰: 我执,是痛苦的根源!</h1> export default hot(module)(App)
- 更改build/webpack.config.js
const path = require('path'); const HTMLPlugin = require('html-webpack-plugin') // 是否为开发环境 const isDev = process.env.NODE_ENV === 'development' const Webpack = require('webpack') const config = { mode: isDev ? 'development' : 'production', //开发模式 entry: { app: path.resolve(__dirname, '../client/app.js') //入口文件 }, output: { path: path.resolve(__dirname, '../dist/'), // 输出路径 filename: '[name].[hash:8].js' // 输出的文件名(带版本号) }, // 模块管理 module: { // 规则匹配,并使用loader处理 rules: [ // 使用babel-loader来处理js文件,及jsx文件 { test: /\.(js|jsx)$/i, loader: 'babel-loader', exclude: path.join(__dirname, '../node_modules') } ] }, // webpack插件 plugins: [ // 引入模板文件插件 new HTMLPlugin({ template: path.resolve(__dirname, '../client/index.html') }) ] }; if (isDev) { // webpack-dev-server配置 config.devServer = { host: '0.0.0.0', // 域名 port: 8000, // 端口 contentBase: path.resolve(__dirname, '../dist/'), //静态文件路径 overlay: true, // 开启错误调试 hot: true //是否开启hot-module-replacement }; // 配置hot-module-replacement config.plugins.push(new Webpack.HotModuleReplacementPlugin()) } module.exports = config;
完成上面的配置后,再次运行:
npm run devc
访问:localhost:8000, 然后你再去更改client/App.jsx文件,你会发现,页面没有刷新,但你的更改却可以实时显示,这是一件很神奇的事,不是么!
三、线上环境的React服务端渲染(大师篇)
一、首先,我先解释一下,为什么需要服务端渲染:
- 体验不好,使用React编写的代码,需要先下载到客户端,然后通过js的渲染,才能显示到页面,在渲染完成以前,客户端得到的只是一个空白;
- seo不友好,因为是一个空白的html文档,浏览器的搜索引擎爬虫根本不能获取到网页的内容
二、开始我们的服务端渲染之旅
- 下载编写服务端代码必要的安装包
cnpm i ejs express -S
cnpm i rimraf ejs-compiled-loader -D
-
创建server目录 + server.js,用于我们编写服务端代码:
-
需要编写一个服务端渲染入口client/server-app.js,以及服务端入口的webpack配置文件build/webpack.server.js, 服务端渲染需要用到的ejs模板文件client/server.ejs
- server-app.js
import React from 'react'; import App from './App.jsx'; export default <App />
- 打包用于服务端渲染的webpack配置:build/webpack.server.js
const path = require('path'); // 是否为开发环境 const isDev = process.env.NODE_ENV === 'development' const config = { mode: isDev ? 'development' : 'production', //开发模式 target: 'node', // node运行环境 entry: { app: path.resolve(__dirname, '../client/server-app.js') //入口文件 }, output: { path: path.resolve(__dirname, '../dist/'), // 输出路径 filename: 'server-app.js', // 输出的文件名 libraryTarget: 'commonjs2' // 使用最新commonjs模块化方案 }, // 模块管理 module: { // 规则匹配,并使用loader处理 rules: [ // 使用babel-loader来处理js文件,及jsx文件 { test: /\.(js|jsx)$/i, loader: 'babel-loader', exclude: path.join(__dirname, '../node_modules') } ] } }; module.exports = config;
注意:什么的配置:
- target: “node”
- libaryTarget: "commonjs2"
这两个配置是关键,这样打包处理的代码才能遵循commonjs规范,才能在node.js端运行
- client/server.ejs
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>webpack-with-react</title> </head> <body> <div id="root"><%%- appString %></div> </body> </html>
你可能已经发现,什么的ejs语法很奇怪,解释一下,因为webpack在打包时,ejs会执行,会了防止报错,必须使用什么的写法即:
<%%- appString %>
, 在webpack编译时,需要相应的loader进行解析,下面我们就来配置该loader:ejs-compiled-loader
:- 配置生成服务端渲染需要用到的模板: build/webpack.config.js, 在plugins中新增如下:
plugins: [ ... // 服务端渲染模板 new HTMLPlugin({ template: '!!ejs-compiled-loader!' + path.resolve(__dirname, '../client/server.ejs'), filename: 'server.ejs' }) ]
- 完成以上步骤之后,我们还有配置一下package.json的执行脚本部分,修改后为:
"scripts": { "test": "echo \"Error: no test specified\" && exit 1", "buildc": "webpack --config build/webpack.config.js", "builds": "webpack --config build/webpack.server.js", "build": "rimraf dist && npm run buildc && npm run builds", "devc": "cross-env NODE_ENV=development webpack-dev-server --config build/webpack.config.js" },
- 接下来,我们就可以打包服务端渲染需要的代码了:
npm run build
执行之后,生成dist目录:
- 好了,下面才是我们进行服务端渲染的代码server/server.js
const express = require('express'); const ejs = require('ejs'); const path = require('path'); const fs = require('fs'); const ReactSSR = require('react-dom/server'); const serverApp = require('../dist/server-app').default; const app = express(); const template = fs.readFileSync(path.join(__dirname, '../dist/server.ejs'), 'utf-8'); app.get('*', (req, res) => { const appString = ReactSSR.renderToString(serverApp); const html = ejs.render(template, {appString}); res.send(html); }); app.listen(3000, () => { console.log('server is listening on 3000'); })
熟悉node.js以及express基础的同学,应该很容易理解上面的代码
- 在package.json中再编写一条启动命令:
"start": "node server/server.js"
-
执行上面的脚本,便可开启express服务,访问: localhost:3000, 及返回服务端渲染好的html代码了
-
不过,高兴还太早了,因为报错了!
因为,我们的server端代码响应的任意请求都是渲染出来的html代码:
app.get('*', (req, res) => { var appString = ReactSSR.renderToString(serverApp); var html = ejs.render(template, {appString}); res.send(html); });
这是不行的,应该有所区分,所以还需要更改一下配置,和修改一下代码:
- build/webpack.config.js的output部分:
output: { path: path.resolve(__dirname, '../dist/'), // 输出路径 filename: '[name].[hash:8].js', // 输出的文件名(带版本号) publicPath: '/public/' },
- build/webpack.config.js的devServer部分:
// webpack-dev-server配置 config.devServer = { host: '0.0.0.0', // 域名 port: 8000, // 端口 contentBase: path.resolve(__dirname, '../dist/'), //静态文件路径 overlay: true, // 开启错误调试 hot: true, //是否开启hot-module-replacement publicPath: '/public/', historyApiFallback: { // 404默认返回 index: '/public/index.html' } };
- server/server.js 需要引导一下静态资源目录:
const app = express(); app.use('/public', express.static(path.join(__dirname, '../dist/')));
- 重新打包,并开启服务端渲染
npm run build
npm start
- 启动成功后,再次访问localhost: 3000
没有报错:
正确引用静态资源文件
至此,简单的服务端渲染就讲完了,更复杂的服务端渲染,会在以后的新教程中讲解! 接下了还需要构建在开发环境下的服务端渲染,请接着继续往下看
四、开发环境的服务端渲染(超神篇)
如果我们想看服务端渲染的效果,根据上面的介绍,那么就必须使用webpack打包,然后启动服务器,重新修改代码,又要重新打包,重启服务器,这样也太浪费时间了,打包是很费cpu和内存,也很费时费力的,接下来,我们就需要来构建一个开发环境下,实时更新的服务端渲染;
一、安装必要的依赖:
cnpm i http-proxy-middleware memory-fs nodemon -D
cnpm i axios -S
二、修改server/server.js,对开发环境和线上环境进行区分
server/server.js:
const express = require('express');
const ejs = require('ejs');
const path = require('path');
const fs = require('fs');
const ReactSSR = require('react-dom/server');
var isDev = process.env.NODE_ENV === 'development';
const app = express();
if (!isDev) { //线上环境
const serverApp = require('../dist/server-app').default;
app.use('/public', express.static(path.join(__dirname, '../dist/')));
const template = fs.readFileSync(path.join(__dirname, '../dist/server.ejs'), 'utf-8');
app.get('*', (req, res) => {
var appString = ReactSSR.renderToString(serverApp);
var html = ejs.render(template, {appString});
res.send(html);
});
} else { // 开发环境
const devServer = require('./utils/dev-server')
devServer(app);
}
app.listen(3000, () => {
console.log('server is listening on 3000');
})
三、 开发环境的服务端代码: server/utils/dev-server.js
const proxy = require('http-proxy-middleware'); // 服务端代理插件
const webpack = require('webpack');
const config = require('../../build/webpack.server');
const axios = require('axios'); // 异步请求插件
const MemoryFS = require('memory-fs'); // 存取内存数据流插件
const mfs = new MemoryFS();
const path = require('path');
const ReactSSR = require('react-dom/server');
const ejs = require('ejs');
// 获取模板,在开发环境,没有打包好的dist,所以模板的获取要到,webpack-dev-server服务获取
const getTemplate = () => {
return new Promise((resolve, reject) => {
axios.get('http://localhost:8000/public/server.ejs')
.then((response) => {
resolve(response.data)
})
.catch(reject);
});
}
// commonjs模块
const NativeModule = require('module');
// 虚拟机
const vm = require('vm');
// 把模块字符串,转化为可运行的模块
const getModuleFromString = (bundleStr, filename) => {
// 设置一个假模块
const m = {exports: {}};
// 把模块字符串包装为commonjs调用形式
const wrapper = NativeModule.wrap(bundleStr, filename);
// 把字符串变成可执行脚本
const script = new vm.Script(wrapper, {
displayErrors: true,
filename
});
const result = script.runInThisContext();
result.call(m.exports, m.exports, require, m);
return m;
}
// 编译webpack
const compiler = webpack(config);
// 把webpack磁盘形式的存取操作,改为内存形式的存取操作
compiler.outputFileSystem = mfs;
// 需要进行服务端渲染的App入口
let serverApp;
// webpack监听入口文件,以及入口文件引用的其他模块的变化
compiler.watch({}, (err, stats) => {
if (err) throw err;
stats = stats.toJson();
// 打印webpack监听过程的报错
stats.errors.forEach(err => console.error(err));
// 打印webpack监听过程的警告
stats.warnings.forEach(err => console.warn(err));
// 内存中入口App路径
const bundlePath = path.join(config.output.path, config.output.filename);
const bundleStr = mfs.readFileSync(bundlePath, 'utf-8');
const m = getModuleFromString(bundle, config.output.filename);
serverApp = m.exports.default;
})
module.exports = (app) => {
// /public开头的path,代理到webpack-dev-server服务
app.use('/public', proxy({
target: 'http://localhost:8000'
}));
app.get('*', (req, res, next) => {
getTemplate()
.then((template) => {
if (!serverApp) {
return res.send('serverApp还没编译完成,请稍后刷新!');
}
const appString = ReactSSR.renderToString(serverApp);
const html = ejs.render(template, {appString});
res.send(html);
})
.catch(next);
})
}
四、 使用nodemon启动开发环境服务,这样服务端代码有更改,服务端会自动刷新重启,编写nodemon.json配置文件:
{
"restartable": "rs",
"ignore": [
".git/",
"dist/",
"node_modules/",
"client/",
"build/"
],
"ext": "js",
"verbose": true,
"env": {
"NODE_ENV": "development"
}
}
五、还需要在package.json中,添加一条脚本来启动开发环境的服务端渲染:
"devs": "nodemon server/server.js",
做完上面五步,执行以下步骤,就能够启动开发环境的服务端渲染了:
如果有dist,将其删除
rm -rf dist
启动webpack-dev-server
npm run devc
启动开发环境服务端渲染
npm run devs
访问:localhost:8000
访问:localhost:3000
修改client/App.jsx, 你会发现上面打开的两个网页,同时自动显示修改后的结果;
不过,在localhost:3000的页面,打开控制台,你会发现有一个警告:
react-dom.development.js:5585 Warning: render(): Calling ReactDOM.render() to hydrate server-rendered markup will stop working in React v17. Replace the ReactDOM.render() call with ReactDOM.hydrate() if you want React to attach to the server HTML.
意思是,如果使用服务端渲染需要用ReactDOM.hydrate()
,我的思路是根据端口号来判断,在客户端渲染的端口号为:8000, 在服务服务端渲染的端口号为: 3000
因此修改client/app.js为:
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App.jsx';
var reactDOMRender = window.location.port == '8000' ? ReactDOM.render :ReactDOM.hydrate;
// 将App组件渲染到html页面
reactDOMRender(<App />, document.getElementById('root'));
至此,简单的开发环境渲染就讲完了,以后的教程还会继续讲解更复杂的开发环境服务端渲染
m