所有源代码、文档和图片都在 github 的仓库里,点击进入仓库

相关阅读

1. 客户端渲染与服务端渲染

1.1 客户端渲染

  • 客户端渲染,实际上就是客户端向服务端请求页面,服务端返回的是一个非常简单的 HTML 页面,在这个页面里,只有很少的一些 HTML 标签
  • 客户端渲染时,页面中有两个比较重要的点,第一个是 script 标签,可能会有好几个 script 标签,这个标签是打包后的 js 代码,用来生成 DOM 元素,发送请求,事件绑定等。但是,生成的 DOM 元素需要有一个在页面上展示的容器,所以另外一个点就是容器,一般是一个 id 名为 root 或 app 的 div 标签,类似于这样 <div id="root"></div><div id="app"></div>
  • 客户端渲染的特点

    • 客户端加载所有的 js 资源,渲染 DOM 元素
    • 在浏览器页面上的所有资源,都由客户端主动去获取,服务端只负责静态资源的提供和 API 接口,不再负责页面的渲染,如果采用 CDN 的话,服务端仅仅需要提供 API 接口
    • 优点: 前后端分离,前端专注页面的开发,后端专注接口的开发
    • 缺点: 首屏加载资源多,首屏加载时响应慢。页面上没有 DOM 元素,不利于 SEO 优化

1.2 服务端渲染

  • 服务端渲染,就是客户端页面上的 HTML 元素,都要由服务端负责渲染。服务端利用模板引擎,把数据填充到模板中,生成 HTML 字符串,最终把 HTML 字符串返回到浏览器,浏览器接收到 HTML 字符串,通过 HTML 引擎解析,最终生成 DOM 元素,显示在页面上
  • 比如 Node.js 可以渲染的模板引擎有 ejs,nunjucks,pug 等。Java 最常见的是 JSP 模板引擎。
  • 服务端渲染的特点

    • 优点: 页面资源大多由服务端负责处理,所以页面加载速度快,缩短首屏加载时间。有利于 SEO 优化。无需占用客户端资源
    • 缺点: 不利于前后端分离,开发效率低。占用服务器资源

1.3 区分与选择

  • 客户端渲染和服务端渲染本质的区别就是,是谁负责 HTML 页面的拼接,那么就是谁渲染的页面
  • 如果对首屏加载时间有非常高的需求,或者是需要 SEO 优化,那么就选择服务端渲染
  • 如果对首屏加载时间没有要求,也不需要做 SEO 优化,类似于做后台管理系列的业务,那么就可以选择客户端渲染
  • 具体选择客户端渲染还是服务端渲染,没有强制的要求,具体要根据项目的需求来区分选择

2. 项目基础架构搭建

  • 我们现在搭建的项目架构,是一个整体的项目架构,现在创建的一些目录,可能暂时会用不上,但是为了了解每一个目录每一个模块具体都是做什么的,我们提前先知道,方便以后使用
  • 我们把项目叫做 react-ssr-webpack

2.1 创建项目

  • 新建一个 react-ssr-webpack 的文件夹,这个文件夹存放我们所有的源代码,也就是根目录。
  • 使用 npm init 初始化项目
  • 下载 webpack 的依赖包 npm i webpack webpack-cli -D

    • webpack 和 webpack-cli 是 wepack 打包的主要模块
  • 下载 Babel 的依赖包 npm i @babel/core @babel/preset-env @babel/preset-react babel-loader @babel/plugin-proposal-class-properties -D

    • 我们采用的是 babel@7 版本,不再采用 babel@6 版本,因为 babel@7 使用起来更加的方便,简单
    • @babel/core,Babel 编译的核心库
    • @babel/preset-env,Babel 官方预置的一个库,一系列插件的集合
    • @babel/preset-react,主要是用来编译 jsx 语法
    • babel-loader,webpack 处理 js 文件所需要的加载器
    • @babel/plugin-proposal-class-properties,编译 class 的一些新的特性
  • 下载 React 的依赖包 npm i react react-dom react-router-dom -S
  • 下载 express,npm i express -S,我们采用 express 做后端服务,也可以采用 koa,hapi,egg 等
  • 根目录的文件目录结构
├── node_modules/   第三方依赖包
|── build/  服务端打包后生成的代码
|   └── server.js
├── public/      客户端打包后生成的代码
│   └── client.js
├── src
│   ├── client/        客户端源代码
│   ├── components/        React 组件
│   ├── containers/        React 容器组件
│   ├── server/        服务端源代码
│   ├── store/        redux
|      └── routes.js     路由
├── .babelrc        babel 编译
├── .gitignore        git 忽略文件
├── package.json
├── webpack.base.js        webpack 基础配置
├── webpack.client.js        webpack 客户端配置
└── webpack.server.js        webpack 服务端配置

2.2 配置服务端的 wbpack.server.js

  • 配置服务端的 webpack,是因为我们要在服务端使用 jsx 语法,需要借助 babel 编译
  • 既然服务端使用了 babel ,那么服务端也可以使用新的 ES6/7/8 语法
  • 还有一点需要注意的是,在 webpack.server.js 进行编译的时候,仅仅是将 jsx 和 ES6/7/8 高级语法转为 ES5 语法,生成一个 build/server.js 文件。所以我们还需要用 build/server.js 创建一个服务,这个服务就是我们最终访问的服务
  • webpack.server.js
// webpack.server.js
const path = require('path');
// webpack-node-externals 模块是为了不打包 node 的模块,比如 path, fs 等。因为我们的 Node 已经内置了这些模块,所以没有必要打包
const WebpackNodeExternals = require('webpack-node-externals');

module.exports = {
  target: 'node',
  // 服务端的入口文件,是 src/server/index.js
  mode: 'development',
  entry: './src/server/index.js',
  output: {
    // 打包后生成的文件的路径与文件名
    filename: 'server.js',
    path: path.resolve(__dirname, 'build/')
  },
  externals: [nodeExternals()],
  module: {
    rules: [
      {
        test: /\.js?$/,
        // 可以在这里通过 option 配置 Babel,也可以使用 .babelrc 文件配置 Babel
        loader: 'babel-loader',
        exclude: /node_modules/
      }
    ]
  }
};

2.3 配置 package.json

  • 前边我们说了,我们一共需要做两件事,一件事是使用 webpack 编译服务端的代码,另一件事是把服务端编译后生成的代码作为一个服务启动
  • 所以我们先下载两个全局的包,npm-run-all 和 nodemon

    • npm i npm-run-all -g,这个工具可以在一个 Terminal 里同时启动多个服务,便于我们开发
    • npm i nodemon -g,热重启 Node 服务,nodemon 的使用方法和 Node 是一样的,nodemon 监听到 build/server.js 文件的变动,就会自动重启服务
  • 我们在 scripts 里添加两条命令
  • 命令1 dev:build:server ,这个命令是调用 webpack.server.js 进行打包

    • "dev:build:server": "webpack --config webpack.server.js --watch"
    • --config 参数的意思是指定 webpack.server.js 为配置文件,如果没有 --config 参数,那么 webpack 会默认调用 webpack.config.js 配置文件,如果没有指定配置文件,也没有 webpack.config.js 文件,那么会报错
    • --watch 参数的意思是开启监听,每次修改代码,都会自动打包
  • 命令2 dev:start ,这个命令是使用打包后代码开启服务

    • "dev:start": "nodemon build/server.js"
    • 每次 dev:build:server 打包后,都会生成新的 build/server.js 文件
    • nodemon 监测到文件的改动,就会重新开启服务
  • 我们使用 npm-run-all 再添加一条命令,用来启动这两个服务

    • "dev": "npm-run-all --parallel dev:**"
    • dev 命令是使用 npm-run-all 工具开启多个服务,--parallel 参数的意思是并行开启服务,dev:** 的意思是 npm-run-all 会启动 scripts 里所有的以 dev: 开头的命令。 dev 只是一个代称,也可以使用其他的字符
  • 建议使用 npm-run-all 和 nodemon,这样我们可以在一个 Terminal 里进行所有的操作,也可以开启多个 Terminal,每个 Terminal 开启不同的服务
{
    "dev": "npm-run-all --parallel dev:**",
    "dev:build:server": "webpack --config webpack.server.js --watch",
    "dev:start": "nodemon build/server.js"
}
  • 预览 package.json
{
  "name": "react-ssr-docs",
  "version": "1.0.0",
  "description": "完全解读 react 服务端渲染",
  "main": "index.js",
  "scripts": {
    "dev": "npm-run-all --parallel dev:**",
    "dev:build:server": "webpack --config webpack.server.js --watch",
    "dev:start": "nodemon build/server.js"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/dawnight/react-ssr-docs.git"
  },
  "keywords": [
    "react",
    "redux",
    "react-ssr"
  ],
  "author": "dawnight",
  "license": "MIT",
  "bugs": {
    "url": "https://github.com/dawnight/react-ssr-docs/issues"
  },
  "homepage": "https://github.com/dawnight/react-ssr-docs#readme",
  "devDependencies": {
    "@babel/core": "^7.4.3",
    "@babel/plugin-proposal-class-properties": "^7.4.0",
    "@babel/preset-env": "^7.4.3",
    "@babel/preset-react": "^7.0.0",
    "babel-loader": "^8.0.5",
    "webpack": "^4.30.0",
    "webpack-cli": "^3.3.1",
    "webpack-node-externals": "^1.7.2"
  },
  "dependencies": {
    "express": "^4.16.4",
    "react": "^16.8.6",
    "react-dom": "^16.8.6",
    "react-redux": "^7.0.2",
    "react-router-dom": "^5.0.0",
    "redux": "^4.0.1",
    "redux-logger": "^3.0.6",
    "redux-thunk": "^2.3.0"
  }
}

2.4 配置 .babelrc

  • @babel/core, @babel/preset-env 和 @babel/preset-react 库是必须要有的
  • @babel/plugin-proposal-class-properties 库并不是必须的,因为在后边我们使用了 class 的新语法特性,所以需要使用这个库,如果不安装这个库,也可以使用传统的 class 的属性方法语法,效果是一样的。
{
  "presets": [
    "@babel/preset-env",
    "@babel/preset-react"
  ],
  "plugins": [
    "@babel/plugin-proposal-class-properties"
  ]
}

相关阅读

03-05 20:04