# 正文
前几节我们了解并实现了组件的直出和基本同构,完成了一个组件在服务端和浏览器端的渲染,并且实现了基本的交互-绑定事件。但这毕竟只能是算是demo,用于实际的项目开发还差的远。
所以从本节正式开始建造 react ssr 同构应用开发骨架,如同开始建造房子的骨架。
要想这个骨架能用于实际项目开发,最基本的就是要开发方便,用起来省事儿,另一个就是具备一些基础的能力,让我们只专注于业务开发。
哪些能力呢?
比如路由处理、副作用处理、seo支持、css 支持等。
现在从头开始吗?
前两节的内容呢?
当然不是,每一个小结都是层层递进的,都作为后面小结的基础,都是我们建造骨架的重要组成部分。
基于前几节的内容,同时为了方便后续的开发和调试,我们先来让这个项目的使用变得方便一些,就像是建房子的工具,不然咱们这个房子盖的太累,浪费时间和精力。
怎样才能更方便呢?
# 工程化支持第一步
大家有没有发现我们现在的 demo存在严重的体验问题。
服务的运行需要经过多次手动操作。
前端代码的构建需要手动执行编译
npm run dev
服务端代码的编译需要手动执行编译
npm run babel-node
最后手动启动服务
node app.js
后续的文件改动还要重新执行上面的步骤。
大家可以自行运行起来,体验一下这个过程。
工欲善其事,必先利其器
这种开发体验太差了,会浪费我们很多时间且严重影响心情,良好的开发体验可以让我们事半功倍,心情舒畅。
既然这样,那我们势必需要进行优化,通过程序或者工具来代替人工执行,以达到提升效率的目的。
# 确定优化目标
如何实现这个优化呢?
做之前我们先进行问题分析,然后设计出一套解决方案。
首先要明确我们想达到的一个具体的目标。
- 前后端代码可以自动编译
- 每次修改代码,
node服务可以自动重启
另外我也不想自己刷新页面,所以热更新也是必须的,不过本节不打算直接介绍热更新,在后面章节会介绍。
# 实现思路分析
前端代码构建体验优化
前端代码的构建可以使用 webpack 搞定,开启 watch 功能就可以了,更改了文件就会自动打包。
后端代码构建
后端代码这里分为两部分,一个是用来处理请求的 node 代码,只运行于服务端。
另一部分就是组件以及组件相关的代码,也需要在服务端运行来完成组件的 ssr,所以我们也需要打包一份给服务端使用。
在上一节我们使用的是babel cli来编译的,当然这是一个有效的方法,还有一个更好的办法就是使用webpack来编译。
诶?也能用webpapck?
不要以为webpack只能用于前端代码的构建,一切皆模块,只要是 js 代码都可以被编译,只是编译的目标平台不同而已。
使用 webpack 编译,并且开启watch监听,就能做到实时编译了。
node 服务自启
node 服务这里要分两部分来看待。
- (1)服务的启动
- (2)服务的自启
单纯的服务启动和自启,很简单,可以使用工具nodemon来完成,不过除了使用工具外是否还有其他的方法处理?
我们可以使用自定义进程
让一个服务运行在我们创建的进程中,然后在适当的时机杀掉进程(关闭服务),重新创建一个服务进程启动服务(重启启动)。
综合分析
上面我们分析了各个环节的实现思路,单纯的实现每一步应该没什么难度,但是多个操作如何才能统一为一个操作呢?
也就是可以通过一个命令来启动前端代码编译和监听、后端代码编译和监听、同时启动 node 服务并且能够自动重启。
好了,问题很明确了,实现方案也有了。
# 自定义进程
上面的多个操作可以看做是独立的服务,可以让每个服务运行在独立的进程中,然后利用进程间通信来达到我们要的效果。
启动阶段
在这个阶段启动各个服务。
监听阶段
webpack开启 watch选项后可以自动编译,这个不用我们来干预。
node服务的自启,可以在服务端代码构建完成的时候执行。
# 相关实现
# 项目目录
先来规定下本骨架的目录结构,可以大概的了解下都包含哪些资源。
├── dist // 生产环境打包后的资源存放目录
│ ├── static //打包的静态资源文件
│ | ├── index.js // 打包后的文件
│ ├── server //服务端文件目录
│ | ├── app.js //node server 启动入口
├── src // 源码目录
│ ├── client //前端代码
│ │ ├── app // 前端渲染入口
│ | ├── pages // 业务页面
│ │ | ├── index //默认首页
│ │ ├── router // 路由配置
│ │ ├── common // 存放通用组件和通用模块
│ ├── server // node 代码
│ │ ├── app // 服务入口
│ │ ├── middlewares //中间件
│ │ | ├── react-ssr.js //ssr 中间件
│ ├── share // 双端共享的代码可以放这里
├── webpack //构建配置
│ ├── scripts //构建脚本目录
│ │ | ├── start.js //开发环境的所有服务启动入口
│ ├── webpack.dev.config.js //前端代码的开发环境编译配置
│ ├── webpack.server.config.js 服务端代码的编译配置
# 确定执行命令
开发环境的启动入口
npm run dev // 用来启动开发环境
对应的package.json配置
"dev": "node webpack/scripts/start.js",
前端代码编译开启 watch
"scripts": {
//...
"fe:watch": "webpack --config ./webpack/webpack.dev.config.js --watch",
//...
},
服务端代码打包配置
和前端的打包配置差不多,需要配置target=node, 增加externals 选项,使用webpack-node-externals来排除不需要打包的模块,因为 node 端会自动载入这些包,可以让打包的文件更小。
// ./webpack/webpack.server.config
//webpack 配置文件
const path = require('path')
const nodeExternals = require('webpack-node-externals')
const webpack = require('webpack');
const resolvePath = (pathstr) => path.resolve(__dirname, pathstr);
process.env.BABEL_ENV = 'node';//设置 babel 的运行的环境变量
const isProd=process.env.NODE_ENV==='production';
module.exports = {
target: 'node',
entry: resolvePath('../src/server/app/index.js'),//入口文件
output: {
filename: 'app.js',
path: resolvePath('../dist/server')
},
externals: [nodeExternals()],
module: {
rules: [{
test: /\.jsx?$/,
loader: 'babel-loader',
exclude: /node_modules/
}
]
}
}
通过 api 方式启动服务端代码监听
因为我们需要监听服务端代码的构建过程,当每次编译完成时,通知主进程重启 node 服务,cli 模式下无法满足需要,所以需要调用api 来执行构建监听。
// ./webpack/scripts/svr-code-watch.js
//基于 webpack 开启对服务端代码的编译和监听
//配置文件为 webpack.server.config.js
const webpack = require('webpack');
const config = require('../webpack.server.config');
const constantCode = require('./constant');
config.mode='development';//设置编译模式
//编译对象
const compiler = webpack(config);
const watching = compiler.watch({
aggregateTimeout: 300, // 类似节流功能,聚合多个更改一起构建
ignored: /node_modules/, //排除文件
poll: 2000, //轮训的方式检查变更 单位:秒 ,如果监听没生效,可以试试这个选项.
}, (err, stats) => {
let json = stats.toJson("minimal");
if(json.errors){
json.errors.forEach(item => {
console.log(item);
});
}
if (json.warnings) {
json.warnings.forEach(item => {
console.log(item);
});
}
//定一个常量,编译完成后 通知主进程来重启node 服务,主进程通过此标志来进行判断是否重启
console.log(constantCode.SVRCODECOMPLETED);
});
compiler.hooks.done.tap('done',function (data) {
console.log('\n svr code done' ); //编译完成动作
});
//收到退出信号 退出自身进程
process.stdin.on('data', function (data) {
if (data.toString() === 'exit') {
process.exit();
}
});
ps: 文件 './constant'说明
该文件用来存放开发环境构建过程中的常量
// ./webpack/scripts/constant.js
//用于开发环境的构建过程中的常量
module.exports = {
SVRCODECOMPLETED:'SVRCODECOMPLETED',//表示服务端代码编译完成的标志
}
配置服务端代码构建执行命令
"scripts": {
//...
"svr:watch": "node ./webpack/scripts/svr-code-watch.js"
},
创建node 服务启动入口
为了方便控制,我们单独创建一个开发环境的node 服务启动入口,引入打包后的入口文件即可。
// ./webpack/svr-dev-server.js
//开发环境 node 服务启动入口
//公用配置文件,定义一些通用的数据
const proConfig = require('../../src/share/pro-config');
//node server port
const nodeServerPort = proConfig.nodeServerPort;
//启动前检查端口是否占用,杀掉占用端口的进程
require('./free-port')(nodeServerPort);
//引入打包后的入口文件,这个入口我们在 webpack 配置中已设置好
require('../../dist/server/app');
最终编译入口处理
通过创建子进程的方式,整合多个服务到统一入口。
使用const {spawn} = require('child_process');//用于创建子进程 来创建子进程,此方式为异步执行。
然后通过子进程的std进行通信,达到重启 node服务的作用。
启动入口 ./webpack/scripts/start.js
const {spawn} = require('child_process');//用于创建子进程
const constantCode = require('./constant');
const chalk = require('chalk');//为控制台输出的信息增加点色彩
const log = console.log;
const proConfig = require('../../src/share/pro-config');//双端的配置文件,配置一些基础参数,具体说明在后面
//node server port
const nodeServerPort = proConfig.nodeServerPort;
log(chalk.red('servers starting....'));
//前端代码构建 服务进程
const feCodeWatchProcess = spawn('npm', ['run', 'fe:watch'],{stdio:'inherit'});
//服务端代码监控和编译进程
const svrCodeWatchProcess = spawn('npm', ['run', 'svr:watch']);
//node 服务进程
let nodeServerProcess=null;
//启动 node 服务
const startNodeServer = () => { //重启 node 服务
nodeServerProcess && nodeServerProcess.kill();
nodeServerProcess = spawn('node',['./webpack/scripts/svr-dev-server.js']);
nodeServerProcess.stdout.on('data', print);
}
//控制台输出信息
function print(data) {
let str = data.toString();
if (str.indexOf(constantCode.SVRCODECOMPLETED) > -1) { //接收到服务端代码编译完成的通知
startNodeServer();//重启 node 服务
} else {
console.log(str);
}
}
//监听服务端代码构建服务的对外输出 stdout 事件
svrCodeWatchProcess.stdout.on('data',print);
//杀掉子进程
const killChild=()=>{
svrCodeWatchProcess && svrCodeWatchProcess.kill();
nodeServerProcess && nodeServerProcess.kill();
feCodeWatchProcess && feCodeWatchProcess.kill();
}
//主进程关闭退出子进程
process.on('close', (code) => {
console.log('main process close', code);
killChild();
});
//主进程关闭退出子进程
process.on('exit', (code) => {
console.log('main process exit', code);
killChild();
});
//非正常退出情况
process.on('SIGINT', function () {
svrCodeWatchProcess.stdin.write('exit', (error) => {
console.log('svr code watcher process exit!');
});
killChild();
});
ps:文件('../../src/share/pro-config')说明
该文件是服务端和客户端的公用基础配置文件,所以放在了share目录下
// ./src/share/pro-config.js
//双端公用的配置文件
module.exports = {
wdsPort:9002,//webpack dev server 服务的运行端口
nodeServerPort:9001,//node server 的监听端口
}
看下最终运行效果

服务端代码的构建日志信息已隐藏,只在有错误的时候才会输出,现在在编译完成时会通知主进程重启 node 服务。
到这里,我们就基本上完成了一个重要的开发体验升级,为我们后续的进展提供了便利。
# 另外一种方式
下面给大家简单介绍另外一种方式
可以使用npm-run-all工具,该工具可以同时并行多个npm 命令,只需要配置下就可以了。
举个栗子
npm-run-all --parallel fe:watch svr:watch node:server
其实本质也是通过进程来完成的。
不过对于我个人来说,还是喜欢能自己实现的尽量自己来实现,这样可以学以致用,学习的东西不实践的话永远只是纸上谈兵。
当然工具的使用也必不可少,看具体的情况,因为它确实能够帮我们大大提高效率。
# 小结
这一节我们逐步分析并实现了一个基本的工程化的搭建和配置,让我们的项目在开发和调试的时候更加便利。
对于实现方式,本文算是其中一种,也可能还有其他方法,但是思想才是最重要的。只要有了思路,相信后面的实现也只是个时间问题。
我们应用骨架,走到这里就方便多了,但是还不够,不够彻底。
现在更改了代码,还需要刷新页面来查看效果,所以后面还需要加入热更新机制,这个优化我们后面再慢慢聊。
本节完整代码:
← 初步认识同构_交互实现 双端路由同构 →