1 react 基本js文件:
import React from 'react'; import ReactDOM from 'react-dom'; import App from './App.jsx'; ReactDOM.render(<App/>,document.getElementById('root')) // 把app组件传给 reactCreateElement 作为的参数 要引入 react
基本 webpack.config 配置:
const path = require('path'); module.exports = { entry:[ app: path.join(__dirname,'../clinet/app.js'); //使用path,设置绝对路径,注意 dirname前面是两个下划线 ], output:[ filename:'[name].[hash].js', //依赖的文件发生变化的时候,hash值变化。 path:path.join(__dirname,'../dist'),//输出的绝对路径 publicPath:'/public' //设置引用文件的公共路径,用于设置CDN的路径 ], module:{ rules:[ { test:/.jsx$/, loader:'babel-loader'//编译jsx,es6,es7等语法 //这里要安装 babel: npm i babel-loader -D //babel-loader只是一个webpack的插件,不包括babel的核心代码,所以还要安装: //babel-core: npm i bable-core -D //此外,bable-core 默认是编译 es6的,为了还编译jsx,所以要进行另外的配置:.babelrc 文件 } ] } }
对应的 babelrc文件
// .babelrc 文件 { "presets":[ //指定babel编译哪些类型 ["es2015",{"loose":true}],//指定 es2015是 松散的不是严格的 "react" //在这里加上react,这个支持之后,babel才会编译react代码 ] } //安装上述依赖文件: // npm i babel-preset-es2015 babel-preset-es2015-loose babel-preset-react -D
为了打开html文件:
安装: 1 npm i html-webpack-plugin -D
webpack.config.js 文件
const path = require('path'); const HTMLPlugin =require('html-webpack-plugin') module.exports = { entry:[ app: path.join(__dirname,'../clinet/app.js'); //使用path,设置绝对路径,注意 dirname前面是两个下划线 ], output:[ filename:'[name].[hash].js', //依赖的文件发生变化的时候,hash值变化。 path:path.join(__dirname,'../dist'),//输出的绝对路径 publicPath:'/public/' //设置引用文件的公共路径,用于设置CDN的路径 ], module:{ rules:[ { test:/.jsx$/, loader:'babel-loader'//编译jsx,es6,es7等语法 //这里要安装 babel: npm i babel-loader -D //babel-loader只是一个webpack的插件,不包括babel的核心代码,所以还要安装: //babel-core: npm i bable-core -D //此外,bable-core 默认是编译 es6的,为了还编译jsx,所以要进行另外的配置:.babelrc 文件 }, { test:/.js$/, loader:'babel-loader', exclude:[ path.join(__dirname,'../node_modules') ] } ] }, plugins:[ new HTMLPlugin(); //有两个作用:1 生成html文件 ,2:根据output的配置把entry 文件打包 ] }
2 服务端渲染的配置
单页面存在的问题:
1 SEO不友好
2 首次请求等待时间过长,体验不友好。
首先看一下客户端的入口文件 app.js
客户端的入口文件 import React from 'react'; import ReactDOM from 'react-dom'; import App from './App.jsx'; ReactDOM.render(<App/>,document.getElementById('root'))
对比服务端渲染的入口文件 server-entry.js 文件:都是为了引入 最外层组件 <App/> 但是服务端没有dom元素,所以只能抛出 <App/> 组件,而不能挂载到 root的div节点上。
// 在服务端运行的文件 import React from 'react'; import App from './App.jsx'; export default <App/>
服务端和客户端的入口文件都是把<App> 组件提供出来,不同的是客户端可以直接把组件挂载到 root上。而服务器端的入口文件是提供 <App/>组件,之后在服务器端的server.js中,将其挂载到root中;且客户端和服务器端用的是同一个html模板。因为入口文件不同,所以两个的webpack配置文件也不同:
新建一个服务器端的webpack打包的文件:
const path = require('path'); module.exports = { target:'node', //这里是新的配置,web/node 规定在哪里使用 entry:[ app: path.join(__dirname,'../clinet/serve-entry.js'); ], output:[ filename:'server-entry.js', //node.js 去 improt 这个js文件,且在服务器端没有缓存,不需要hash path:path.join(__dirname,'../dist'),//输出的绝对路径 publicPath:'/public/', libraryTarget: 'commonjs2' //打包出来的js 使用的模块方案, 包括 AMD CMD CommonJS等规范,这里使用的是commonjs2,适用于node端 ], module:{ rules:[ { test:/.jsx$/, loader:'babel-loader'//编译jsx,es6,es7等语法 //这里要安装 babel: npm i babel-loader -D //babel-loader只是一个webpack的插件,不包括babel的核心代码,所以还要安装: //babel-core: npm i bable-core -D //此外,bable-core 默认是编译 es6的,为了还编译jsx,所以要进行另外的配置:.babelrc 文件 }, { test:/.js$/, loader:'babel-loader', exclude:[ path.join(__dirname,'../node_modules') ] } ] } }
修改配置项:
//修改 package.json文件 { "script":{ "build:clinet": "webpack --config build/webpack.config.client.js", "build:server": "webpack --config build/webpack.config.server.js", "clear": "rimraf dist" //rimraf 是node的一个包,专门用来删除文件夹 "build": "npm run clear && npm run build:client && npm run build:server", } }
//安装rimraf包。 npm i rimraf -D
然后每次打包完,会有三个文件 客户端的js 服务器端的js 还有一个html。
上面介绍的是 客户端和服务器端的 webpack 配置文件;
===================
下面编写 express 服务端:
首先安装 npm i express -S
新建 server文件夹 建立 server.js文件:
说明: 1. dist/server-entry文件是打包生成的js文件;res.send()渲染的就是生成的js文件;
2. app.get 获取到浏览器所有的请求,返回 渲染的 js文件;监听端口在3333
3. 由于html中 使用的是es6的语法: export default <App/>;而node中又不能使用 impront { app } from './app' 这种结构赋值的语法,所以这里要用 require().default 获取默认值;
然后在配置json文件中,设置服务端启动命令:
{ "script":{ "start": "node server/server.js " } }
运行服务端代码之后,服务端返回的只是打包后入口文件中写的代码 字符串:‘<div></div>’,我们需要返回的是整个html。
步骤一:先在client文件夹下新建 模板文件 template.html: 其中关键代码:
<!doctype html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title> </head> <body> <div id="root"><!--app--></div> </body> </html>
把生成的文件覆盖掉 <!--app-->;
然后修改客户端的配置文件。webpack.config.client.js中的模板插件:
{ plugins: [ new HTMLPlugin({ template: path.join(__dirname, '../client/template.html') }) ] }
注意这里是改动的客户端的配置文件,客户端打包生成的html文件 ,然后服务端 server.js 去引用:
const express = require('express') const ReactSSR = require('react-dom/server'); const fs = require('fs') const path = require('path') const serverEntry = require('../dist/server-entry').default;//引入的是服务端的配置打包后的js文件 const template = fs.readFileSync(path.join(__dirname, '../dist/index.html'), 'utf8')//同步引入客户端打包生成的 html 文件,如果不使用 utf8 则是buffer文件 const app = express(); 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>,然后发送的是模板文件 }) app.listen(3333, function () { console.log('server is listening on 3333') })
注意的是: 客户端和服务端的 output配置中的 publicPath 要写为“public”
output:[ filename:'[name].[hash].js', //依赖的文件发生变化的时候,hash值变化。 path:path.join(__dirname,'../dist'),//输出的绝对路径 publicPath:'/public' //设置引用文件的公共路径,用于设置CDN的路径 ]
这样生成的html中引用的静态文件js和css以及图片等路径前缀都是在public下,所以 上面的服务端 server.js 的
app.use('/public', express.static(path.join(__dirname, '../dist')));
给访问的 public 目录下的文件返回 dist文件夹下的文件,其他的 * 返回 模板文件
- 问题1 为啥还要在 服务端的配置文件中 设置 public
- 问题2 服务端的 server-entry.js 和 客户端的 app.js 有啥区别?
客户端的入口文件 app.js 可以对浏览器进行操作,比如
import React from 'react'; import ReactDOM from 'react-dom'; import App from './App.jsx'; ReactDOM.render(<App/>,document.getElementById('root'))
而服务器端的入口文件 server-entry 不可以:
import React from 'react'; import App from './App.jsx'; exprot defalut <App/>
所以后续,对应的webpack.config配置文件也需要设置两个,分别对应客户端和服务器端;
他们两个的入口和出口配置不一样
客户端的配置文件:
{ entry:[ app: path.join(__dirname,'../clinet/app.js'); //使用path,设置绝对路径,注意 dirname前面是两个下划线 ], output:[ filename:'[name].[hash].js', //依赖的文件发生变化的时候,hash值变化。 path:path.join(__dirname,'../dist'),//输出的绝对路径 publicPath:'/public' //设置引用文件的公共路径,用于设置CDN的路径 ], }
服务器端的配置文件:
{ target:'node', //这里是新的配置,web/node 规定在哪里使用 entry:[ app: path.join(__dirname,'../clinet/serve-entry.js'); ], output:[ filename:'server-entry.js', //node.js 去 improt 这个js文件,且在服务器端没有缓存,不需要hash path:path.join(__dirname,'../dist'),//输出的绝对路径 publicPath:'/public/', libraryTarget: 'commonjs2' //打包出来的js 使用的模块方案, 包括 AMD CMD CommonJS等规范,这里使用的是commonjs2,适用于node端 ], }
因此,服务器端和客户端是单独的入口js文件和输出文件。
客户端: client/app.js------> dist/app.hash.js 文件,如下图所示,其提供的组件<App/>挂载到 root下;
服务器端: client/server-entry.js------> dist/server.entry.js 文件,在服务器端 server/server.js的作用下,将其生成的<App/>组件挂载到 root 下。
也就是说,客户端的app.js文件,生成的组件挂载到 root上;服务器端把server-entry生成的组件直接挂载到 root 上。
根据模板文件生成:
<body> <div id="root"><!-- app --></div> <script type="text/javascript" src="/public/app.9337b7bd3bfeb9af5f86.js"></script> </body>
两个是独立的,所以两个都要加 /public,这样服务器就会根据两个位于 public 文件夹下 而返回对应的app.hash.js 和 server.entry.js文件,而不是 模板代码。