前言
基于Node.js的Web应用框架很多,包括Express、Koa、Sails以及Egg等(后两者是前两者的增强)。虽然Express和Koa本身提供的能力非常简单,但是如果对于项目的开发有特殊需求,完全可以进行灵活的扩展,从而写出各种千奇百怪的MVC模式(如果对服务端MVC不是很清晰可以阅读服务端MVC之Model2的衍生)。
本文简单介绍以前设计的几种基于Express扩展的技术选型方案,恰好涵盖了React、Angular以及Vue这三个Web前端框架。
本文使用的示例项目都相对简单,是真实项目的简化,希望对刚入门Express小白们有所启示。
React技术选型方案
2016年7月到10月,从零开始学习React并使用React设计了服务端渲染的Express应用(同年10月25日诞生了Next.js),大致的技术选型如下:
- Bootstrap
- React
- Mongoose
- Webpack
- Karma/Chai
由于对React不是很熟悉,首先实现了单页应用,然后实现了服务端渲染应用。
实现React单页应用(SPA)
Web前端的学习和设计过程
当时对React的学习过程大致如下(React发展很快,部分学习过程现在不适用):
- 学习React语法
- 学习ES6/ES7语法
- 学习babel/webpack,打包代码支持ES6/ES7/JSX语法
- 学习webpack-dev-server/Hot Module Replacement,启动开发环境的Express服务,实现热加载功能
- 学习flux/react-redux
- 学习react-router
- 学习mocha/karma
以上学习过程记录在react-demo和react-start-kit里(只有参考价值,没有学习价值),此时只是Web前端对于React单页应用设计的实现过程。大致结构如下:
在前后端分离的开发模式中,如果Web前端实现的是SPA(单页应用),服务端可以选用不同的设计语言,例如Node.js、Java或者Golang等。Web前端可以通过Express渲染服务器请求转发的形式去获取后端的相应数据。如果想要前端先行,可以使用Easy Mock或者自己设定的JSON数据模拟后端提供的数据格式。
服务端的设计过程
服务端的设计选用Node.js的Express框架,大致实现步骤如下:
- 搭建服务端Express,设置服务端MVC目录结构
- 设置Express的静态资源目录,将Web前端的Webpack构建目录设置成Express的静态资源目录
- 设置单页应用的路由和路由服务
- 启动服务查看页面是否可以渲染成功
以上实现过程记录在一个简单的示例rewatch里(非项目代码),入口文件是app.js
,此时前后端分离,可以同时启动服务端Express服务和启动开发态React调试页面服务(webpack-dev-server),并使用开发态页面向Express服务发送请求获取显示数据(当时使用JQuery的$.ajax
发送请求)。设计完成后将开发态页面使用Webpack打包构建,放入服务端Express的静态资源目录。首屏渲染的工作交给Ejs模板引擎(事实上不需要使用模板引擎而应该直接使用HTML字符串渲染)进行处理。大致结构如下:
实现React服务端渲染(SSR)
单页应用在路由跳转时不需要额外的请求静态资源,可以提升用户的体验,但是如果应用较大,首次请求静态资源和进行页面动态渲染的过程中会产生以下问题:
为了解决以上客户端渲染的问题,需要实现React服务端渲染。由于当时还没出现成熟的服务端渲染应用框架,因此只能自己摸索构建React服务端渲染方案:
- 为了实现前后端代码同构,需要对服务端代码进行Webpack打包配置
- 使用script标签以及全局变量的形式实现前后端
react-redux
数据store
的统一(这个印象深刻,当时想了好久)
使用了服务端渲染方案后,可以去除之前的Ejs模板引擎,当时设计的大致结构如下:
当页面发送路由请求时,Express服务端使用react-router
匹配相应路由对应的React组件实例并调用renderToString
方法进行服务端页面渲染(页面的局部刷新)。当页面渲染完成后,由React打包后的静态资源对页面进行hydrate处理。此时的React代码是同构的,因此需要注意哪些会运行在服务端,哪些会运行在客户端。同时服务端需要对同构代码进行Webpack打包处理。
以上实现记录在示例rewatch中,入口文件是server.js
,由于文件比较混乱(把客户端渲染和服务端渲染的示例放在了同一个文件项目中),这里给出另外一个非常简单的示例rewatch-server-render,项目目录结构如下:
.
├── public # 静态资源目录
│ └── js
│ ├── bundle.js # react目录打包文件
│ ├── common.js # react目录打包公共文件
│ ├── react-dom.min.js # react库文件
│ └── react.min.js # react库文件
├── react # react同构代码目录(没有react-router,可以查看rewatch示例)
│ ├── actions
│ ├── components
│ ├── containers
│ ├── reducers
│ ├── store
│ └── index.js
├── server # 服务端
│ └── routes # 服务端路由(没有使用react-router同构,可以查看rewatch示例)
├── server.js # 开发态服务入口文件
├── server.bundle.js # 生产态服务入口文件
├── webpack.browser.config.js # 静态资源打包的Webpack配置(目标文件bundle.js、common.js)
└── webpack.node.config.js # 服务端打包的Webpack配置(目标文件server.bundle.js)
Angular技术选型方案
2016年10月到2017年3月,使用Angular设计了一个Express应用,大致的技术选型如下:
- Ejs
- Bootstrap
- Angular-Chart
- Mongoose
- Redis
- Sokect.io
这是一个简单的服务端多页应用示例,使用Ejs模板引擎进行页面渲染,渲染完成后交由Anguar进行页面的响应操作(发送请求使用Angular内置的$http
服务)。该示例不需要额外的Webpack配置,只需要启动Express服务本身渲染设计即可。目录结构如下:
.
├── client # 静态资源目录
│ ├── css/ # 样式
│ ├── imgs/ # 图片
│ ├── js/ # 脚本
│ │ ├── angular/ # angular应用
│ │ │ ├── controllers/ # angular控制器
│ │ │ ├── services/ # angular服务
│ │ │ └── webapp.js/ # angular自动引导应用程序
│ │ └── sockets/ # sockets应用
│ └── lib # 插件(包括angualr、bootstrap/bootstrap-table、chart等)
├── config # 配置(包括Redis、Mongoose配置)
│ ├── config.js # 参数配置
│ └── index.config.js # 导出配置
├── server # 服务端
│ ├── constants/ # 常量
│ ├── controllers/ # 控制器
│ ├── events/ # 事件
│ ├── models/ # 模型
│ ├── routes/ # 路由
│ ├── sockets/ # socket.io
│ ├── pubs/ # Redis发布
│ └── subs/ # Redis订阅
├── views # 视图(使用Ejs模板引擎)
└── app.js # 服务入口文件
Vue技术选型方案
2018年6月,使用Vue设计了服务端渲染的Express应用,大致技术选型如下:
- MongoDB
- Nuxt
- Vue
- lokka
- Muse-UI
- 客户端和服务端同构代码的Webpack配置由Nuxt封装
- 服务端Backpack配置
项目选型特点
选型详细说明
为了支持Graphql查询语言,服务端选择使用支持Express中间件扩展的graphql-yoga。
客户端的HTTP请求需要符合Graphql请求格式,一种方式是使用axios等模拟Graphql的请求格式,另外一种方式是选用支持Graphql请求格式的API库,这里选用lokka作为Graphql客户端请求API。
为了快速设计页面,选用了基于Vue 2.0 的 Material Design UI 组件库Muse-UI。
选用了Nuxt作为服务端渲染的中间件(基于Vue.js的通用应用框架,预设了服务端渲染应用所需要的各种配置。)
为了支持客户端TypeScript语法,需要扩展Nuxt的默认Webpack配置,利用Nuxt的模块/注册自定义loaders配置ts-loader,配合nuxt-property-decorator实现客户端TypeScript语法。
在Nuxt的目录结构中,服务端引入的同构代码放在.nuxt
目录中,是Webpack打包后的代码文件,因此如果服务端不使用特殊的语法,完全不需要Backpack配置。此项目为了支持TypeScript语法,使用Backpack对服务端代码进行构建(不影响同构部分代码的构建,同构代码在Nuxt里是通过读取文件的方式获取)。
项目目录结构
.
├── .nuxt # Nuxt构建目录(Nuxt预设目录)
├── assets # 资源目录(Nuxt预设目录)
│ ├── img # 图片
│ ├── icon # 图标
│ └── style # 样式
├── build # 配置(包括Redis、Mongoose配置)
│ └── main.js # 服务端Backpack构建的目标启动入口文件
├── common # 前后端通用
│ ├── constants/ # 常量
│ └── types/ # TypeScript接口
├── components # 组件目录(Nuxt预设目录)
├── constants # 前端常量目录
├── views # 视图(使用Ejs模板引擎)
├── docs # 文档目录(渲染.md文件)
├── graphql # 前端Graphql请求接口
├── layouts # 布局目录(Nuxt预设目录)
├── middleware # 中间件目录(Nuxt预设目录)
├── mixins # 全局mixins
├── modules # Nuxt模块(TypeScrpt的Webpack配置扩展)
├── pages # 页面目录(Nuxt预设目录)
├── plugins # 插件目录(Nuxt预设目录)
├── server # 服务端目录
│ ├── constants/ # 常量
│ ├── database/ # 数据库模型
│ ├── express/ # 服务对外的公共API接口
│ │ ├── controllers/ # 控制器
│ │ ├── routes/ # 路由
│ │ └── services/ # 服务
│ ├── graphql/ # 服务内部的Graphql查询接口
│ │ ├── middlewares/ # Graphql中间件
│ │ ├── resolvers/ # Graphql Resolver
│ │ ├── schemas/ # Graphql Schema
│ │ └── index.ts # graphql接口入口文件
│ ├── types/ # TypeScript接口
│ ├── utils/ # 工具方法
│ └── index.ts # 服务端入口文件(Backpack构建入口地址)
├── static # 静态文件目录(Nuxt预设目录)
├── store # Vuex目录(Nuxt预设目录)
├── utils # 客户端工具方法
├── .cz-config.js # cz提交配置文件
├── .env # 环境变量
├── .gitignore # Git忽视文件
├── .huskyrc # Git钩子配置文件
├── .vcmrc # cz校验配置
├── app.html # html文件
├── backpack.config.js # Backpack配置文件
├── CHANGELOG.md # 升级日志
├── ecosystem.config.js # PM2启动配置文件
├── index.d.ts # TypeScript声明文件
├── nuxt.config.js # Nuxt配置文件
├── package.json # 项目描述文件
├── README.md # 说明
├── tag.bat # 项目打Tag脚本
└── tsconfig_node.json # TypeScript配置文件
运行脚本设计
在package.json
中的配置脚本如下:
"build": "cross-env NODE_ENV=production nuxt build && backpack build",
"pm2": "pm2 start ecosystem.config.js",
"pm2:stop": "pm2 stop ecosystem.config.js",
"dev:client": "cross-env NODE_ENV=development DEV_TYPE=nuxt ts-node --compiler ntypescript --project tsconfig_node.json ./server",
"dev:server": "cross-env NODE_ENV=development DEV_TYPE=server ts-node-dev --compiler ntypescript --project tsconfig_node.json ./server"
build
:使用Webpack构建Nuxt资源包以及使用Backpack构建服务端入口文件(转义TypeScript)
pm2
:以生产模式启动一个进程守护的Web服务器
pm2:stop
:停止运行Web服务器
dev:client
:启动开发态热部署前端渲染服务
dev:server
:启动开发态热启动服务端服务
虽然是服务端渲染框架(理论上可以一个人开发项目,启动一个热加载的服务端命令即可),但是在开发的过程中考虑到多人协作以及开发的便利性仍然将客户端和服务端进行分离。
在服务端配置Nuxt的Builder
会导致服务端热加载过慢,因此将服务端Nuxt的Builder
过滤掉,使用ts-node-dev做服务端热启动。在客户端使用ts-node启动服务,通过识别DEV_TYPE
环境变量加载Nuxt的Builder
,实现Web前端的热加载功能。需要注意客户端向服务端发送请求是跨域的,因此在服务端的开发态环境需要配置允许跨域。
技术选型方案总结
设计了以上三个方案后,发现从零开始构建一个Express应用时至少需要考虑以下几个方面:
- 数据库(MongoDB/MySql等)选型
- 是否需要模板引擎以及模板引擎(Ejs/Jade等)选型
- 前端框架(JQuery/Angular/React/Vue等)选型
- HTTP请求(axios/request/superagent等)选型
- 是否需要UI组件库以及UI组件库选型
- 客户端是否需要Webpack构建
- 服务端是否需要Webpack/Backpack构建
- 其他(session、redis、socket.io等)
- 性能、监控等
简单的起手式
- MongoDB
- Ejs模板引擎
- JQuery
- JQuery内置的
$.ajax
- Bootstrap(可选)
- 客户端和服务端都不需要Webpack配置
对于Express新手而言,可以先尝试多页应用 + MongoDB + 模板引擎 + JQuery的选型方案:使用Ejs模板引擎需要额外了解Ejs语法,但是语法相对简单,学习成本低。使用JQuery不需要考虑HTTP请求API选型,JQuery内置了HTTP请求的API。如果对于页面布局以及样式设计不熟悉,可以考虑选用Bootstrap前端框架。最后该选型方案不需要深入了解ES6/ES7/JSX等语法,因此不需要学习和使用Webpack配置。最重要的一点,使用Ejs模板引擎进行渲染的Express应用,是天然的服务端渲染应用。
主流框架的应用设计
- MongoDB
- 无需模板引擎
- React/Vue等
- axios/request/superagent等
- Ant Design/Ant Design Vue/Element/Muse-UI等
- 客户端Webpack配置
- 服务端是否需要Webpack/Backpack配置依据情况而定
如果前端框架选型是React、Vue或Angular(通常是单页应用设计),并且需要使用ES6/ES7/JSX以及Vue的SFC格式等语法,那么Web前端势必要设计Webpack的构建配置,此时可以使用类似于webpack-dev-server
的Express开发态渲染服务器设计和调试开发态前端页面。当然目前的Web前端开发针对不同的前端框架都有自己设计的脚手架,因此可以直接使用脚手架进行开发设计和静态资源构建。
如果框架中没有内置HTTP请求API,可以自己封装或者使用一些成熟的HTTP库,例如axios、request以及superagent等。
如果需要使用UI组件库进行页面设计,可以根据使用的框架进行UI组件库选型,例如React的Ant Design、Vue的Element。
Express服务端的设计由于使用了主流框架的动态渲染能力,因此可以去除模板引擎渲染功能。如果想额外支持ES6/ES7/TypeScript语法等,那么需要Backpack进行服务端构建。
主流框架的应用设计和简单的起手式不同,前后端开发可以完全分离,这样的应用设计大大解放了前端的生产力(前端不再受限于服务端的模板引擎)。例如目前的主流框架设计的一些脚手架,可以优雅的将Webpack配置,开发态渲染服务器以及请求代理结合在一起,做到开箱即用,提升用户的开发体验。