React.js学习笔记(16) webpack3 (一) 服务端渲染 + ( webpack-dev-server ) + ( react-hot-loader )

(一) wepack命令行 ---- ( npm script )

在npm init 初始化的项目中 package.json文件中的"script"对象中配置

 "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "webpack --config build/webpack.config.js --progress --color --profile"
  },

// `--config` 指定 webpack 的配置文件,默认是 `webpack.config.js`




-----------------------------------------
$ webpack --help (或者-h) 列出webpack命令行所有可用的配置选项---------查看所有所有命令!!!(重要)

$ webpack --config webpack.min.js  //指定 webpack 的配置文件

$ webpack --progress //打印出编译进度的百分比值

$ webpack --color //开启/关闭控制台的颜色 [默认值: (supports-color)]

$ webpack --profile // 记录编译的性能数据,并且输出。
它会告诉你编译过程中哪些步骤耗时最长,这对于优化构建的性能很有帮助

$ webpack --display-error-details //显示异常信息

$ webpack --watch   //监听变动并自动打包
 
$ webpack -p    //压缩混淆脚本,这个非常非常重要!
 
$ webpack -d    //生成map映射文件,告知哪些模块被最终打包到哪里了

http://www.php.cn/js-tutorial-368142.html

(二) .babelrc文件

.babelrc配置babel,因为默认babel只能编译es6,需求还要用到jsx等语法,也需要是识别

.babelrc配置文件


{
    "presets": [
        ["es2015", {"loose": true}],     // 把es6编译成es5,松散模式
        "react"                          // 识别jsx语法,编译成es5
    ]
}

(三) 服务端渲染

(1) 原理:

用react生成的app在服务端( node 环境中 )进行渲染,得到完整的html内容,直接返回给浏览器可以呈现的html内容

(2) 好处:

1)便于SEO
2)让用户首屏等待时间减少

(3) 流程:
(1) 新建server-entry.js 入口文件
server-entry.js


import App from './app.jsx';   //引入
import React from 'react';   
export default <App/>          //导出






-----------------------------------------------------------------------------------
(2) 打包server-entry.js  

(webpack配置文件命名为:webpack.config.server.js)

打包原因:
server-entry.js导出的是jsx文件,不能被服务端识别(只能识别js代码),
所以需要用webpack打包成js的es5代码


webpack.config.server.js


const path = require('path');

module.exports = {
    target: 'node',     // node环境,如果是浏览器环境:target:'web'
    entry:{
        app: path.join(__dirname, '../client/server.entry.js')  // 上面的入口文件
    },
    output:{
        filename:'server-entry.js',
        path: path.join(__dirname, '../dist'),
        publicPath:'/public',  //用区分返回静态文件,还是返回服务端渲染的代码 !!!!!!!!!!
        libraryTarget:'commonjs2'  // 打包出来的js使用模块方案,umd,cmd,amd等
    },
    module:{
        rules:[
            {
                test: /.jsx$/,
                use:[
                    {loader:'babel-loader'}
                ]
            },
            {
                test:/.js$/,
                exclude:[
                    path.join(__dirname, '../node_modules')
                ],
                use:[
                    {loader: 'babel-loader'}
                ]
            }
        ]
    },

}






-----------------------------------------------------------------------------------
(3) rimraf  删除dist文件夹
因为每次打包都会新增文件,而不是覆盖原来的文件,所以引入nodejs中的:

-- ( rimraf包,用来删除文件夹 )  --
安装: cnpm install rimraf --save-dev
使用:

"scripts": {
"build:client":"webpack --config build/webpack.config.client.js --progress --color --profile",
"build:server":"webpack --config build/webpack.config.server.js --progress --color --profile",
    "clear": "rimraf dist",    //删除dist文件夹
    "build": "cnpm run clear && cnpm run build:client && cnpm run build:server",
    "start": "node server/server.js"
  },







-----------------------------------------------------------------------------------
(4) 用express启一个node服务

server.js

安装:cnpm install express -save      用于生产环境
使用:

const express = require('express');

const ReactSSR = require('react-dom/server'); 
 // ReactDOMServer 对象使你能够将组件渲染为静态标记。 通常,它在 Node 服务器上
 // ReactDOMServer 对象允许您在服务器上渲染组件。
 // renderToString()
 // ReactDOMServer.renderToString(element)
 // 将 React 元素渲染到其初始 HTML 中。 该函数应该只在服务器上使用。 React 将返回一个 HTML 字符串。 
 // 您可以使用renderToString()此方法在服务器上生成 HTML ,
 // 并在初始请求时发送标记,以加快网页加载速度,并允许搜索引擎抓取你的网页以实现 SEO 目的。

const serverEntry = require('../dist/server-entry').default;

const fs = require('fs');
 // 引入node的fs模块
 // fs模块用于对系统文件及目录进行读写操作。
 // 1、异步读取
 //  fs.readFile( url , code , callback); 
 // 2、同步读取
 //  fs.readFileSync( url , code );

const path = require('path');
const app = express();

const template =  fs.readFileSync(path.join(__dirname,'../dist/index.html'),'utf8');
// fs.readFileSync()同步读取
// utf8格式读取,才是一个string字符串

app.use('/public', express.static(path.join(__dirname, '../dist')))  
// 对静态文件指定对应的请求返回
// 对静态文件不返回html形式,而是本来的js形式
// 作用:对于/public文件下的文件都是返回静态文件,( 不返回无端端渲染的文件 )

app.get('*',function(req, res) {   // 所有的url请求
  const appString = ReactSSR.renderToString(serverEntry);//在服务器上把打包后的组件生成HTML字符串
  const templeteA = template.replace('<app></app>', appString)  //字符串替换操作
  res.send(templeteA);   // 返回html代码
}) 
app.listen(3333,function(){   // 启用3333端口
   console.log('server is listening on 3333')
})

/*
template.html文件

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Webpack App</title>
  </head>
  <body>
      <div id="root"><app></app></div>
  </body>
</html>

*/

(五) 浏览器渲染

webpack.config.client.js




const path = require('path');
const HTMLWEBPACk = require('html-webpack-plugin');   // 插件

module.exports = {
    entry:{
        app: path.join(__dirname, '../client/app.js')   // 对应下面注释部分的入口文件app.js
    },
    output:{
        filename:'[name].[hash].js',
        path: path.join(__dirname, '../dist'),
        publicPath:'/public'
    },
    module:{
        rules:[
            {
                test: /.jsx$/,
                use:[
                    {loader:'babel-loader'}
                ]
            },
            {
                test:/.js$/,
                exclude:[
                    path.join(__dirname, '../node_modules')
                ],
                use:[
                    {loader: 'babel-loader'}
                ]
            }
        ]
    },
    plugins:[
        new HTMLWEBPACk({
            template: path.join(__dirname, '../client/template.html')   // 使用的html模板
        })
    ]
}


/*
app.js入口文件

import React from 'react';
import ReactDom from 'react-dom';
import App from './App.jsx';

ReactDom.hydrate(<App/>, document.getElementById('root'));  // 插入template.html的id=root的节点

*/



/*
template.html文件

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Webpack App</title>
  </head>
  <body>
      <div id="root"><app></app></div>
  </body>
</html>
*/

服务端渲染和浏览器渲染总结:

  • rimraf 包:
    node包,删除文件夹

  • html-webpack-plugin插件:
    自动根据模板生成html,和相关依赖

  • babel相关:
    babel-core,babel-loader,babel-preset-es2015,babel-preset-react,babel-preset-es2015-loose松散模式

  • webpack.config.js注意点:
    -------target:执行环境 ( target: 'node' 或者 target: 'web')
    -------libraryTarget:打包出来的js使用的模块方案(commonjs2),umd,cmd,amd等

  • react-dom/server
    -------在服务器中,渲染组件(react生成的文件)
    -------ReactDOMServer.renderToString(element)此方法在服务器上生成 HTML

  • export default xxx 如果用require方式引用export default的内容,需要加上default
    const a = require('....').default

  • 用express起一个node服务
    -------app.use(),app.get(),app.listen()
    -------fs模块,读取写文件(node中的模块)
    1、异步读取
      fs.readFile( url , code , callback);
    2、同步读取
      fs.readFileSync( url , code ); --- !!!!!!!注意:用 utf8 模式生成字符串!!!!!!
    const template = fs.readFileSync(path.join(__dirname, '../dist/index.html'),'utf8');

  • express.static() 托管静态文件,例如图片、CSS、JavaScript 文件等。
    app.use('/public', express.static(path.join(__dirname, '../dist')))







(六) webpack-dev-server

webpack-dev-server需要安装

cnpm install webpack-dev-server --save-dev

简写形式:cnpm i webpack-dev-server -D
(1) process.env

process.env属性返回一个包含用户环境信息的对象


process.env.NODE_ENV === 'development';



const isDev = process.env.NODE_ENV === 'development';
// process.env返回对象的 NODE_ENV属性 是否和 'development'相等


// 如果isDev 是true,是开发环境,执行
if(isDev) {
    config.devServer = {  // 存在就配置config中的devServer 对象
        host:'0.0.0.0',   // 这样就能用ip,或者localhost等方式访问
        port:'8888',
        contentBase: path.join(__dirname, '../dist'), // 静态文静
        hot: true,    // 启动hot-module-replacement
        overlay: {    // webpack编译出错,在网页显示出错信息
            errors: true    // 只显示错误信息,不现实警告等信息
        },
        publicPath:'/public',    // webpack-dev-sever的publicPath 对比output中的publicPath
        historyApiFallback: {     // 让所有404的请求全部返回下面的url
            index: '/public/index.html'
        }
    }
}

(2) cross-env

cross-env能跨平台地设置及使用环境变量

  • 安装:cnpm install cross-env --save-dev
  • 使用:
package.json


{
  "scripts": {
    "dev": "cross-env NODE_ENV=development webpack --config build/webpack.config.js",
   "build": "cross-env NODE_ENV=production webpack --config build/webpack.config.js"
  }
}                           // 生产环境和执行环境

cross-env NODE_ENV = development 用来设置环境变量
(3) react如何做判断开发环境,还是生产环境

链接:https://segmentfault.com/q/1010000007782377

(4)完整代码
webpack.config.client.js文件

const path = require('path');
const htmlPlugin = require('html-webpack-plugin');

const isDev = process.env.NODE_ENV === 'development';

const config = {
    entry: {
        app:path.join(__dirname, '../client/app.js')
    },
    output:{
        filename: '[name].[hash].js',
        path: path.join(__dirname, '../dist'),
        publicPath:'/public'
    },
    resolve: {       // resolve是分解的意思,extensions:是扩展的意思(扩展名)
        extensions:['.js','.jsx']     // 不需要写.js和.jsx文件的后缀名
    },
    module: {
        rules:[
            {
                test:/.js$/,
                exclude: [
                    path.join(__dirname, '../node_modules')
                ],
                use:[
                    {
                        loader: 'babel-loader'
                    }
                ]
            },
            {
                test:/.jsx$/,
                use:[ 
                    {
                        loader: 'babel-loader'
                    }
                ]
            }
        ]
    },
    plugins:[
        new htmlPlugin({
            template: path.join(__dirname, '../client/template.html')
        })
    ]

}

if(isDev) {
    config.devServer = {     
        host:'0.0.0.0',
        port:'8888',
        contentBase: path.join(__dirname, '../dist'),
        // hot: true,
        overlay: {
            errors: true
        },
        publicPath:'/public',
        historyApiFallback: {
            index: '/public/index.html'
        }
    }
}

module.exports = config;

-------------------------------------------------------------------------------

package.json文件


{
  "name": "new",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "clear": "rimraf dist",
    "build:client": "webpack --config build/webpack.config.client.js --color --progress --profile",
    "build:server": "webpack --config build/webpack.config.server.js --color --progress --profile",
    "build": "cnpm run clear && cnpm run build:client && cnpm run build:server",
    "start": "node server/server.js",
    "dev:client": "cross-env NODE_ENV=development webpack-dev-server --config build/webpack.config.client.js"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "babel-core": "^6.26.0",
    "babel-loader": "^7.1.2",
    "babel-preset-es2015": "^6.24.1",
    "babel-preset-es2015-loose": "^8.0.0",
    "babel-preset-react": "^6.24.1",
    "cross-env": "^5.1.3",
    "express": "^4.16.2",
    "html-webpack-plugin": "^2.30.1",
    "react": "^16.2.0",
    "react-dom": "^16.2.0",
    "rimraf": "^2.6.2",
    "webpack": "^3.10.0",
    "webpack-dev-server": "^2.11.0"
  },
  "dependencies": {
    "express": "^4.16.2",
    "react": "^16.2.0",
    "react-dom": "^16.2.0"
  }
}










(七) react-hot-loader

webpack-dev-server 的热加载是开发人员修改了代码,代码经过打包,重新刷新了整个页面。而 react-hot-loader 不会刷新整个页面,它只替换了修改的代码,做到了页面的局部刷新。但它需要依赖 webpack 的 HotModuleReplacement 热加载插件。

(1) 安装
cnpm install react-hot-loader --save-dev
(2) 使用
.babelrc文件


{
    "presets":[
        ["es2015", {"lose": true}],
        "react"
    ],
    "plugins":["react-hot-loader/babel"]    // 配置babel
}

app.js入口文件



import React from 'react';
import ReactDom from 'react-dom';
import App from './App.jsx';
import {AppContainer} from 'react-hot-loader';    // 引入AppContainer组件



const root = document.getElementById('root');

const render = Component => {    // 自建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)
    })
}
webpack.config.client.js文件


const webpack = require('webpack');
// 因为HotModuleReplacementPlugin()是在 webpack中,所以引入webpack

entry : {
        app: [
            'react-hot-loader/patch',
            path.join(__dirname, '../client/app.js')
        ]
    }

plugins:[
  new webpack.HotModuleReplacementPlugin()    // 使用webpack的HotModuleReplacementPlugin插件
]

--------------------------------------------------------------------------------------

完整写法:


const path = require('path');
const HTMLWEBPACk = require('html-webpack-plugin');
const isDev = process.env.NODE_ENV === 'development';
const webpack = require('webpack');

const config = {
    entry:{
        app: path.join(__dirname, '../client/app.js')
    },
    output:{
        filename:'[name].[hash].js',
        path: path.join(__dirname, '../dist'),
        publicPath:'/public/'
    }, // 注意:保证把 output.publicPath 属性设置成 "/"。以保证 hot reloading 会在嵌套的路由有效。
    module:{
        rules:[
            {
                test: /.jsx$/,
                use:[
                    {loader:'babel-loader'}
                ]
            },
            {
                test:/.js$/,
                exclude:[
                    path.join(__dirname, '../node_modules')
                ],
                use:[
                    {loader: 'babel-loader'}
                ]
            }
        ]
    },
    plugins:[
        new HTMLWEBPACk({
            template: path.join(__dirname, '../client/template.html')
        })
    ]
}

if (isDev) {    // 如果是开发环境,在package.json中的script对象中指定
    config.entry = {
        app: [
            'react-hot-loader/patch',   // 入口文件中添加react-hot-loader/patch
            path.join(__dirname, '../client/app.js')
        ]
    }
    config.devServer = {
        host: '0.0.0.0',
        port:'8888',
        contentBase: path.join(__dirname, '../dist'),
        hot: true,
        overlay: {
            errors: true
        },
        publicPath:'/public',
        historyApiFallback: {
            index:'/public/index.html'
        }
    }
    config.plugins.push(new webpack.HotModuleReplacementPlugin())

    // 向config的plugin数组配置中push一个webpack中的 HotModuleReplacementPlugin()插件
}

module.exports = config;

https://www.jianshu.com/p/b7accbae3a1c
http://blog.csdn.net/huangpb123/article/details/78556652


2018-1-27更

webpack.config.js

module.exports = {
  resolve: {     // resolve是分解的意思, extensions是扩展的意思(扩展名)
      extensions: ['.js', '.jsx']    // 不用写.js和.jsx文件的后缀名
  }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,905评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,140评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,791评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,483评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,476评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,516评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,905评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,560评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,778评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,557评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,635评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,338评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,925评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,898评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,142评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,818评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,347评论 2 342

推荐阅读更多精彩内容