前言
什么是全栈?
一般前端提全栈,大都会遭受一顿质疑:为什么要全栈?行业类的知识都没精通反而跨行业,是否算不务正业?
其实全栈 ≠ 全干,某网友观点:“如果软件开发是一个职业的话,全栈是必然的目标!”,在全栈的过程的中,不仅要学习更多知识还要转变思维去思考,其目标导向就是锻炼自己的整个综合实力。
早些年在 JSP、PHP时代,前端只是做单一的交互处理,被”亲切“称为“页面仔”。随着前端技术的爆发增长,Node 的出现使前端大展手脚,前后端分离也成为主流开发模式。
目前依托 Node 的全栈开发如火如荼,比如一些TOB或内部运营业务,这些业务的特点是:不用复杂的架构设计,低流量周期短,这时候Node的优势就出来了:开发快、组件全,有成熟的完整企业级框架,不用团队协作成本等等,基于统一的JS开发语言就可以快速将整个业务搭建起来。
概要
以下图片列举了前端全栈可能会用到的一些后端知识体系,基本所有的技术及框架在 Node 层都有对应实现。
应用场景
全栈应用场景广泛,其主要目的是为了解决某些问题,而不是为了全栈而全栈,所以在场景上不要盲目实现,一定要事先评估可行性。目前市场上比较常用的有以下几种:
- 业务系统
企业网站、CMS/CRM内容管理系统等,主要是前端数据展示及内容的更新,整个数据层结构模型也比较清晰,不会出现过多的复杂设计,所以比较适合全栈开发,可以大大减少沟通成本。
- 接口系统 BFF(Backend For Frontend)层 API 接口实现,可以对接后端服务层的远程RPC协议,然后对控制层二次封装,提供数据给移动端、PC端等多平台调用。由于免编译、热发布等特性,在处理数据增删查改、对接支付、发送短信邮件等服务上优势性明显。 常见的以 REST API 为例,风格简洁统一备受欢迎:
GET /article //查询 POST /article //新增 PUT /article/:id //修改 DELETE /article/:id //删除
- SSR Server Side Rendering 服务端渲染,相比于传统的模板类语言 (handlebars、ejs),这里多指 React、Vue等单页应用组件在服务端字符串化后以 html 形式返回给浏览器渲染 ,同时还可以在服务端对数据二次封装处理。主要适用于对C端产品首页性能及SEO优化有需求的场景。 主流框架:NextJS、NuxtJS
- 工具 一些依赖后端服务的工具,比如报表处理、图片处理、视频处理等等。
- ServerLess 函数调用等云服务,配合微服务架构可以集成完整的公用基础平台。相比于常规的应用系统,无需专人维护,按模块多组别维护,即用即销,灵活度更高。 主流框架:各云厂商、OpenFaaS
框架
借助于框架可以加速开发及提高可维护性,如工具、SSR等较轻量的场景可使用基础库实现,而大型的业务系统就需要比较成熟的企业级框架,包括鉴权、拦截、数据模型、微服务等等框架都已内置,仅使用少量代码就可以快速实现业务。
- Express/Koa/Fastify 基础库,对底层 API 进行封装,简化了代码处理方式。同时具有强扩展性,可以通过集成插件模块使业务的实现上更加清晰,比如简化路由等常用功能:
原生语法示例:
const http = require('http'); const url = require('url'); http .createServer((req, res) => { const router = url.parse(req.url, true); if (router.pathname === '/' && req.method === 'GET') { res.writeHead(200, { 'Content-Type': 'text/html' }); res.end(`${router?.query?.name} hello world`); } }) .listen(9090);
express语法示例:
const express = require('express'); const app = express(); app.get('/', (req, res, next) => { const { name } = req.query; res.send(`${name} hello world`); }); app.listen(9090);
- EggJS 阿里开源,国内主流的Node企业级框架,开箱即用上手快,遵循MVC模型对控制层、服务层高度封装,可以快速搭建服务接口。 由于整个框架是高度集成的,所以官方宣称的有点也可能成为缺点“『约定优于配置』,按照一套统一的约定进行应用开发”,正是约定大于配置,所以必须按照约定的目录结构来开发,过于局限性。插件机制依赖ctx,会污染整个请求链路,不利于排查问题。
语法示例:
//article.controller.js const { Controller } = require('egg'); class ArticleController extends Controller { async list() { const { ctx } = this; const id = ctx.params.id; const info = await ctx.service.article.getName(id); ctx.body = info; } } module.exports = ArticleController; // router.js module.exports = app => { const { router, controller } = app; router.get('/list', controller.article.list); };
- NestJS/Midway 遵循依赖注入思想,偏后端思维(类似Java Spring),社区生态丰富,支持微服务,完美支持 TS 语法。
语法示例:
// article.module.ts import { Module } from '@nestjs/common' import { TypeOrmModule } from '@nestjs/typeorm' import { ArticleController } from './article.controller' import { ArticleService } from './article.service' import { Article } from './article.entity' @Module({ imports: [TypeOrmModule.forFeature([Article])], controllers: [ArticleController], providers: [ArticleService], exports: [ArticleService], }) export class ArticleModule {}
// article.controller.ts import { Inject, Controller, Get, Query, Post, Body } from '@nestjs/common'; import { ArticleService } from "./article.service.ts" @Controller('/article') export class ArticleController { constructor(@Inject(ArticleService) private readonly service: ArticleService) {} @Get() async list(@Query('title') title:string) { return await this.service.findAll(title); } }
数据库
- 关系型数据库:以行列为维度储存的二维表格模型,每张表为一个实体,实体可以关联。
- 非关系型数据库:非结构化存储,灵活度高无需统一数据结构,表与表之间没有关联,可扩展性强。相对轻量符合前端思维。 以下为常用数据库:
- Mysql 主流的关系型数据库,互联网应用广泛,以二维表的形式存储数据。
语法示例:
const mysql = require('mysql2') const connection = mysql.createConnection({ host: 'localhost', user: 'root', password: '', database: 'demo' }) connection.query('SELECT * FROM article WHERE name = "hello"', (err, results, fields) => { console.log(results) })
场景:WEB网站、内容管理、高事务安全等系统
- MongoDB 文档储存的非关系型数据库,面向集合存储,易存储对象类型数据,易扩展性,语法对前端开发友好,特别适合模型不确定的业务场景。
语法示例:
collection('article').find({ name: "hello" }).toArray() collection('article').updateMany({ name: "hello" }, { $set: { nickname: "world" } }) collection('article').deleteMany({ name: "hello" })
场景:WEB网站、文本查询、云服务
- SqlLite 以文件形式存储在磁盘中,零配置管理,轻量快。由于其以单文件的形式存放,不依赖服务器,所以可以集成在客户端软件中,做一些本地数据存储。 场景:移动端、桌面软件
- ORM 原生 SQL 语句相对复杂,尤其是在关联拼接时,不仅要确定SQL语法正确性,还要关注子语句和父语句之间的联系, 同时要对入参转义,避免 SQL 注入等安全问题。 ORM 是对 SQL 语句的一种映射封装,可以快速组装需要的执行语句,调用方式一般采用链式结构,清晰明了,其主要目的就是让开发者中底层操作中抽离出来,去专心做业务逻辑。 目前主流的 ORM 框架有 Sequelize、TypeORM、Prisma,不过当遇到复杂查询时还是需要搭配原生 SQL,两者相辅相成。
SQL 语法示例:
SELECT `user`.`id` AS `user_id`, `user`.`name` AS `user_name`, `user`.`enable` AS `user_enable`, `user`.`super` AS `user_super`, `user`.`last_login_ip` AS `user_last_login_ip`, `user`.`last_login_at` AS `user_last_login_at`, `user`.`created_at` AS `user_created_at`, `user`.`updated_at` AS `user_updated_at`, `role`.`id` AS `role_id`, `role`.`name` AS `role_name` FROM `rs_user` `user` LEFT JOIN `rs_user_role_relation` `user_role` ON `user_role`.`user_id`=`user`.`id` LEFT JOIN `rs_role` `role` ON `role`.`id`=`user_role`.`role_id`
TypeORM 语法示例:
await getRepository(User) .createQueryBuilder('user') .select(['user', 'role.id', 'role.name']) .leftJoin('user.roles', 'role') .getMany()
中间件
中间件作用主要是解耦合,提升开发效率。一般非大型应用并不适合使用中间件,因为体量不够,引入中间件后还多一层运维成本,增加的业务的难度。
- Redis 一种非关系型数据库,以key-value 形式存储数据,优先保存内存中,读取数据快,适用非持久化存储场景,支持发布订阅,key具有效性过期自动删除。 简易流程:
语法示例:
const Redis = require('ioredis') const redis = new Redis({ port: 6379, host: '127.0.0.1' }) redis.set('demo-key', 'hello world') redis.get('demo-key', (err, result) => { console.log(result) })
- 消息订阅 消息队列机制,主要应用于消息异步传递机制,用于不同系统间的数据共享调用,解决数据库频繁连接、高频读写效率等问题,常见的场景如:日志上报、异步处理等等 特点:
- 解耦:按需订阅,各服务减少业务耦合,提高维护性
- 异步:同步操作转化为异步,降低处理等待时间
- 可靠:未处理的消息会缓存系统中,避免应该崩溃数据丢失
如注册流程:
主流框架:Kafka 、MQTT 、RabbitMQ,均有对应的Node组件实现
kafka-node
、mqtt.js
、amqplib
日志
健壮的系统必须要有日志可以对整个调用链路追踪定位,还可以对日志分析发现潜在问题进行优化:
主流的日志库有:Log4j、Winston、Pino,基本都具备以下特性:
级别分类:按等级对日志归类,如access、info、error等等,分别存放在不同文件,方便查找
文件分割:支持按日期、文件大小切割,按场景细分
格式统一:打印的日志具有统一格式,如时间、等级、IP、路径、入参等等必要信息
Pino风格:
安全
业务实现的同时安全问题也是重中之重,一些常规手段可以从编码上避免安全问题的出现:
- 入参校验:数据处理前验证入参是否合法,避免注入等情况。
- cookie、jwt、oauth2.0:鉴权认证,涉及写入操作严格走拦截器权限校验。
- 接口防刷:增加节流模式,短时间频繁触发直接拦截加入频控黑名单。
- CROS跨域:增加请求站点白名单
总结
全栈是个庞大的知识体系,前端从事全栈开发也不会导致“不务正业”,因为在实践的过程中,我们扩展了眼界,锻炼了思维,提升了对整个业务的架构能力,知己知彼,这样才能更好理解业务的运作,让前端更深入业务。