webpack 配置react脚手架(二):热更新
下面继续配置 webpack dev server hot module replacement:
首先配置dev-server 安装 npm i webpack-dev-server -D
const isDev = process.env.NODE_ENV === 'development' const config = { entry:{}, output:{}, plugins:{} } if(isDev){ config.devServer = { host: '0.0.0.0', //设置 0.0.0.0 的host 可以访问 localhost,127.0.0.1,本季ip来访问 contentBase: path.join(__dirname, '../dist'), //因为,devserver是服务打包后的文件,所以和output的path 保持一致即可 port: '8888', hot: true, overlay: { errors: true //server有任何的错误,则在网页中 蒙层加提示 } } } module.exports = config;
修改json文件: "dev:client": "cross-env NODE_ENV=development webpack-dev-server --config build/webpack.config.client.js",
其中 cross-env 是兼容了win mac linux的NODE_ENV,也是一个安装包: npm i cross-env -D
然后 npm run dev:cient 即可以启动服务 localhost:8888;
发现 app.js是无法获取到的,其路径为: http://localhost:8888/public/app.js 可以看出是多了一层 public;
根据server的配置项:contentBase 是把dev-server放在了dist目录下,开启的服务器。则 locahost:8888 相当于 dist目录,而之前output配置的输出文件前面是有路径 public的,所以 dev-server也需要增加这个配置:
if(isDev){ config.devServer = { host: '0.0.0.0', //设置 0.0.0.0 的host 可以访问 localhost,127.0.0.1,本季ip来访问 contentBase: path.join(__dirname, '../dist'), //因为,devserver是服务打包后的文件,所以和output的path 保持一致即可 port: '8888', hot: true, overlay: { errors: true //server有任何的错误,则在网页中 蒙层加提示 }, publicPath: '/public/', //增加公共路径,对应着output的 publicPath historyApiFallback: { index: '/public/index.html' //这里是给本地服务器增加功能:因为是单页面应用,如果刷新页面,或者访问不到路由,则跳转到首页 }, } }
注意:一定要把打包生成的dist目录删除掉,在执行 npm run dev:client 这时因为,服务器会先检测本地磁盘是否有dist目录,如果有就会调取这里面的文件!!
===================华丽的分割线
接下来配置 热更新 hot,
Q:webpack-dev-server 已经是热加载,为何还要在 react 项目还要安装 react-hot-loader 呢?
A:其实这两者的更新是有区别的,webpack-dev-server 的热加载是开发人员修改了代码,代码经过打包,重新刷新了整个页面。而 react-hot-loader 不会刷新整个页面,它只替换了修改的代码,做到了页面的局部刷新。但它需要依赖 webpack 的 HotModuleReplacement 热加载插件 (参考文章: react使用react-hot-loader实现局部热更新)
首先在 .babelrc 文件中增加 对react hot更新的配置:
{ "presets": [ ["es2015", { "loose": true }], "react" ], "plugins": [ "react-hot-loader/babel"] //使用babel的情况下,添加 react-hot-loader,支持react 热更新 }
安装包: npm i react-loader@next -D //教程中这里是最新的版本,尚未正式版,开发的时候注意版本
修改app.js 入口文件:
import React from 'react' import ReactDOM from 'react-dom' import App from './App.jsx' ReactDOM.hydrate(<App />,document.getElementById('root')); if (module.hot) { module.hot.accept('./App.jsx', () => { const NextApp = require('./App.jsx').default ReactDOM.hydrate(<NextApp />,document.getElementById('root')) }) } // module.hot 监听到 app.jsx发生变化之后,重新获取 app.jsx 为NextApp 然后重新渲染;
修改package.js文件:
const webpack = require('webpack'); //因为用到了webpack下的包 HotModuleReplacementPlugin const config ={ } if(isDev){ config.entry=[ 'react-hot-loader/patch', //入口文件中要把 hot 打包进去 path.join(__dirname,'../client/app.js') ], config.devServer = { host: '0.0.0.0', contentBase: path.join(__dirname, '../dist'), port: '8888', hot: true, //打开这里 overlay: { errors: true }, publicPath: '/public/', historyApiFallback: { index: '/public/index.html' } } config.plugins.push(new webpack.HotModuleReplacementPlugin) //增加了这里 }
最后还要返回来在 app.js 入口文件中配置:
import React from 'react' import ReactDOM from 'react-dom' import { AppContainer } from 'react-hot-loader' import App from './App.jsx' ReactDOM.hydrate(<App />,document.getElementById('root')); const root = document.getElementById('root'); const render = Component => { ReactDOM.hydrate( <AppContainer> <Component/> </AppContainer>, root ) } render(App); if (module.hot) { module.hot.accept('./App.jsx', () => { const NextApp = require('./App.jsx').default render(NextApp); }) }
这样才能热更新!
=================================服务端更新配置
上面书写了客户端的热更新,并且热更新的文件都存在内存中,所以服务端不能再从 dist文件夹下获取依赖的 js和 html文件,因此,服务端的js文件也需要区分是否是dev模式:
const express = require('express') const ReactSSR = require('react-dom/server'); const fs = require('fs') const path = require('path') const app = express(); const isDev = process.env.NODE_ENV === 'development'; //在这里定义 if(!isDev){ const serverEntry = require('../dist/server-entry').default;//引入的是服务端的配置打包后的js文件 const template = fs.readFileSync(path.join(__dirname, '../dist/index.html'), 'utf8')//同步引入客户端打包生成的 html 文件,如果不使用 utf8 则是buffer文件 app.use('/public', express.static(path.join(__dirname, '../dist'))); //给静态文件指定返回内容,这里给piblic文件夹下的内容返回的是静态文件的dist文件夹 app.get('*', function (req, res) { const appString = ReactSSR.renderToString(serverEntry); res.send(template.replace('<!--app-->',appString)) //用返回的js文件替换掉模板中的<app>,然后发送的是模板文件 }) }else{ //util 文件夹下的 dev.static.js const devStatic = require('./util/dev.static.js');
devStatic(app); //之所以这里把 app 传递进去,是因为app是 express(),我们可以在新建的文件中继续使用 app.get、app.send 等函数 } app.listen(3333, function () { console.log('server is listening on 3333') })
根据以上代码可知,把原来的从dist目录下获取文件的代码放在了 不是 dev模式下了,而dev模式下我们放在了 util/dev.static.js 文件下。
根据if else可以看出,在文件 dev.static.js 文件中我们要做的事情是:把静态文件js和模版从内存中提取出来,交给app.get请求 然后 send 出去。
接下来编辑 dev.static.js 文件,首先安装 npm i axios -S
步骤一: 获取内存中的模板html文件
const axios = require('axios');// 在浏览器端和服务器端都可以使用 axios /*在这里从内存中获取模版html,因为每次dev-server启动的是本地的服务,url是固定的; 这样可以根据 dev-server 实时的拿到最新的 模板文件 */ const getTemplate = () => { return new Promise((resolve,reject)=>{ axios.get('http://localhost:8888/public/index.html') .then(res => { resolve(res.data); //返回的内容放在了 data中 }) .catch(reject) }) } module.exports = function (app) { app.get("*",function(req,res){ }) }
步骤二:获取。server-entry.js等bundle文件
const axios = require('axios'); //从内存中获取 js等bundle文件,启动webpack,通过webpack打包的结果,获取bundle文件。 const webpack = require('webpack'); //通过 config.server.js 文件 获取 输出文件路径等信息 const serverConfig = require('../../build/webpack.config.server.js'); const getTemplate = () => { return new Promise((resolve,reject)=>{ axios.get('http://localhost:8888/public/index.html') .then(res => { resolve(res.data); }) .catch(reject) }) } // 通过webpack的watch方法,监听配置文件中的 entry 入口文件(及其依赖的文件)是否发生变化,一旦变化,就会重新打包(类似于热更新) const serverCompiler = webpack(serverConfig); serverCompiler.watch({},(err,status)=>{//status 在终端上显示的信息 if(err) throw; let stats = status.toJson(); stats.error.forEach(err => console.log(err)); stats.waring.forEach(warn => console.warn(warn)); const bundlePath = path.join( serverConfig.output.path, serverConfig.output.filename );// 获取输出文件的路径 }) module.exports = function (app) { app.get("*",function(req,res){ }) }
获取到 生成的 文件名字之后需要在 内存中读取 文件:
要使用 memory-fs,所以要安装 npm i memory-fs -D;
const axios = require('axios'); const path = require('path'); const webpack = require('webpack'); const serverConfig = require('../../build/webpack.config.server.js'); // 要使用 memory-fs,所以要安装 npm i memory-fs -D; // 在内存中读写文件,这样就可以从内存中读取 获取到的文件 const MemoryFs = require('memory-fs'); //最后要把得到的js文件,渲染到dom上去,所以要用到 const ReactDomServer = require('react-dom/server'); const getTemplate = () => { return new Promise((resolve,reject)=>{ axios.get('http://localhost:8888/public/index.html') .then(res => { resolve(res.data); }) .catch(reject) }) } //通过module的 constructor 构造方法去创建一个新的 module const Module = module.constructior let serverBundle; const mfs = new MemoryFs;//new 一个 对象; const serverCompiler = webpack(serverConfig); serverCompiler.outputFileSystem = mfs; //webpack 提供的配置项,其输出通过mfs内存读写,这里如果写错名字就会写到硬盘中 serverCompiler.watch({},(err,status)=>{ if(err) throw; let stats = status.toJson(); stats.error.forEach(err => console.log(err)); stats.waring.forEach(warn => console.warn(warn)); const bundlePath = path.join( serverConfig.output.path, serverConfig.output.filename );// 获取输出文件的路径 //通过 mfs 读取文件的路径,就可以得到文件,是 string 类型的文件,无法直接使用 const bundle = mfs.readFileSync(bundlePath,'utf-8'); //需要传入 编码格式 const m = new Module(); //动态编译成一个文件,需要给这个文件指定文件名字,否则无法在缓存中进行缓存,下次则拿不到该文件 m._compile(bundle,'server-entry.js');//使用module的_compile方法将String的文件,生成一个新的 模块,转换成了真正可以读取的文件 /*为了在后面的 app.get方法中使用。将生成的文件赋值给全局变量; 此外,因为是在 watch中执行的,每次依赖的文件更新,输出的文件也会更新*/ serverBundle = m.exports.default; }) module.exports = function (app) { app.get("*",function(req,res){ getTemplate().then(template => { const content = ReactDomServer.renderToString(serverBundle); res.send(template.replace('<!--app-->',content)) }) }) }
最后在package.json 中定义命令:
{ "script":{ "dev:server":"cross-env NODE_ENV = development node server/sever.js" } }
启动客户端和服务器端:npm run dev:client npm run dev:server;
发现无论是js还是html都返回的一样,所以就想之前 对静态文件的 public中做的区分,但是由于这个是在内存中,所以不同:
安装: npm i http-proxy-middleware -D 做代理的中间件
const axios = require('axios'); const path = require('path'); const webpack = require('webpack'); const serverConfig = require('../../build/webpack.config.server.js'); const MemoryFs = require('memory-fs'); const ReactDomServer = require('react-dom/server'); //引入中间件 const proxy = require('http-proxy-middleware'); const getTemplate = () => { return new Promise((resolve,reject)=>{ axios.get('http://localhost:8888/public/index.html') .then(res => { resolve(res.data); }) .catch(reject) }) } const Module = module.constructior let serverBundle; const mfs = new MemoryFs; const serverCompiler = webpack(serverConfig); serverCompiler.outputFileSystem = mfs; serverCompiler.watch({},(err,status)=>{ if(err) throw; let stats = status.toJson(); stats.error.forEach(err => console.log(err)); stats.waring.forEach(warn => console.warn(warn)); const bundlePath = path.join( serverConfig.output.path, serverConfig.output.filename ); const bundle = mfs.readFileSync(bundlePath,'utf-8'); const m = new Module(); m._compile(bundle,'server-entry.js'); serverBundle = m.exports.default; }) module.exports = function (app) {
//服务器端端口是 3333;客户端的端口是 8888;
//这里做的代理是,访问当前3333端口的public文件时,代理去请求客户端的 8888端口文件
app.use('/public',proxy({ target:'http://localhost:8888' })) app.get("*",function(req,res){ getTemplate().then(template => { const content = ReactDomServer.renderToString(serverBundle); res.send(template.replace('<!--app-->',content)) }) }) }