webpack构建React工程(第一季)
发布于 3 个月前 作者 zhb333 828 次浏览 来自 分享

webpack构建React工程(第一季)

点击进入码云获取源代码

说明:阅读本文需要一定的前置知识:如 ES6的基本语法、Node+npm的基本用法、使用过webpck、对React的基本掌握等,不然很有可能看不懂该文!

一、webpack构建React开发环境基本用法(小白篇)

  1. 创建一个空文件夹,我这里命名为"react-build-with-webpack", cd进入该文件夹,使用npm初始化该文件夹:
    mkdir react-build-webpack
    
    cd react-build-webpack
    
    npm init
    

执行完上面的命令后,会产生一个package.json文件,这是npm的配置文件,相信了解过node.js的你,肯定知道它的作用

  1. 下载安装必要的模块(我使用的是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"
    }
    
  2. 接下了我们来创建基本的目录结构 + 文件 如下图:
    目录结构+文件

    • 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

dist文件夹

浏览器打开该文件夹下的index.html

浏览器查看

通过webpack打包的react应用,便可以在浏览器正常显示了

二、配合webpack-dev-server + hot-module-replacement (进阶篇)

一、webpack-dev-server的配置及使用

  1. 首先删除(小白篇)生成的dist目录,因为使用webpack-dev-server时,会优先使用磁盘中的dist目录,但是每次webpack-dev-server更新的打包文件版本号不一样,否则会出错

  2. 下载 webpack-dev-server 模块:

cnpm i webpack-dev-server -D
  1. 以及下载可以在执行package.json脚本时给node.js传入环境变量的模块 cross-env
cnpm i cross-env -D
  1. 在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"
},
  1. 编写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;
  1. 完成上面的几步之后,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的使用及配置

  1. 需配合react的babel插件react-hot-loader,所以需要先安装一下
cnpm i react-hot-loader -D
  1. react-hot-loader配置三部曲,这里我贴上官网的介绍: 点击进入官网查看 react-hot-loader

  2. 根据什么的三部曲进行配置:

    1. 编辑.babelrc 文件
    {
        "presets": [
            "env",
            "react"
        ],
        "plugins": ["react-hot-loader/babel"]
    }
    
    1. 编辑client/App.jsx
    import React from 'react';
    import { hot } from 'react-hot-loader';
    
    // 一个简单的function组件
    const App = () => <h1>佛曰: 我执,是痛苦的根源!</h1>
    
    export default hot(module)(App)
    
    1. 更改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服务端渲染(大师篇)

一、首先,我先解释一下,为什么需要服务端渲染:

  1. 体验不好,使用React编写的代码,需要先下载到客户端,然后通过js的渲染,才能显示到页面,在渲染完成以前,客户端得到的只是一个空白;
    空白html
  2. seo不友好,因为是一个空白的html文档,浏览器的搜索引擎爬虫根本不能获取到网页的内容

二、开始我们的服务端渲染之旅

  1. 下载编写服务端代码必要的安装包
cnpm i ejs express -S
cnpm i rimraf ejs-compiled-loader -D
  1. 创建server目录 + server.js,用于我们编写服务端代码:
    server目录

  2. 需要编写一个服务端渲染入口client/server-app.js,以及服务端入口的webpack配置文件build/webpack.server.js, 服务端渲染需要用到的ejs模板文件client/server.ejs
    服务端文件

    1. server-app.js
    import React from 'react';
    import App from './App.jsx';
    
    export default <App />
    
    1. 打包用于服务端渲染的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端运行
    1. 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:

    1. 配置生成服务端渲染需要用到的模板: build/webpack.config.js, 在plugins中新增如下:
    plugins: [
        ...  
        // 服务端渲染模板
        new HTMLPlugin({
            template: '!!ejs-compiled-loader!' + path.resolve(__dirname, '../client/server.ejs'),
            filename: 'server.ejs'
        })
    ]
    
    1. 完成以上步骤之后,我们还有配置一下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"
    },
    
    1. 接下来,我们就可以打包服务端渲染需要的代码了:
    npm run build
    

    执行之后,生成dist目录:
    dist目录

    1. 好了,下面才是我们进行服务端渲染的代码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基础的同学,应该很容易理解上面的代码

    1. 在package.json中再编写一条启动命令:
    "start": "node server/server.js"
    
    1. 执行上面的脚本,便可开启express服务,访问: localhost:3000, 及返回服务端渲染好的html代码了
      start
      localhost:3000
      源代码

    2. 不过,高兴还太早了,因为报错了!
      报错
      因为,我们的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 没有报错:
      没有报错
      正确引用静态资源文件
      静态资源文件app.js

至此,简单的服务端渲染就讲完了,更复杂的服务端渲染,会在以后的新教程中讲解! 接下了还需要构建在开发环境下的服务端渲染,请接着继续往下看

四、开发环境的服务端渲染(超神篇)

如果我们想看服务端渲染的效果,根据上面的介绍,那么就必须使用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'));

至此,简单的开发环境渲染就讲完了,以后的教程还会继续讲解更复杂的开发环境服务端渲染

回到顶部