zoukankan      html  css  js  c++  java
  • 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)) }) }) }
  • 相关阅读:
    [LeetCode] 582. Kill Process
    [LeetCode] 686. Repeated String Match
    [LeetCode] 341. Flatten Nested List Iterator
    [LeetCode] 404. Sum of Left Leaves
    [LeetCode] 366. Find Leaves of Binary Tree
    [LeetCode] 1485. Clone Binary Tree With Random Pointer
    [LeetCode] 459. Repeated Substring Pattern
    [LeetCode] 565. Array Nesting
    [LeetCode] 679. 24 Game
    [LeetCode] 364. Nested List Weight Sum II
  • 原文地址:https://www.cnblogs.com/xiaozhumaopao/p/10992743.html
Copyright © 2011-2022 走看看