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)) }) }) }

 

posted @ 2019-06-09 10:18  小猪冒泡  阅读(1171)  评论(0编辑  收藏  举报