本章讲了四个页面:下载页、列表页、详情页、播放页的简单实现。
- 列表页:SEO价值、前后端通信
- 详情页:相对简单
- 播放页:不看播放器,主要就是评论列表
- 下载页:静态页面基本上
网站架构

上面的图解释下:
- 下载页:浏览器请求到达BFF之后,路由中间件判断是下载页,返回页面html,以及其他静态资源 ;
- 详情页:浏览器请求路由匹配后,BFF拿到id,在通过ID去请求后台server拿到数据(RPC通信),然后在渲染模版(模版引擎)并返回;
- 播放页:比详情页多了Ajax请求(页面上有点赞操作);
- 列表页:这里简单实现了前后端同构的技术方案;
APP下载页
APP下载页其实就是路由匹配后返回静态文件,代码如下: ```javascript
const koa = require(‘koa’); const fs = require(‘fs’); const mount = require(‘koa-mount’); const static = require(‘koa-static’);
const app = new koa();
app.use( static(__dirname + ‘/source/‘) );
app.use( mount(‘/‘, async (ctx) => { ctx.body = fs.readFileSync(__dirname + ‘/source/index.htm’, ‘utf-8’) }) );
// app.listen(4000); module.exports = app;
这里的静态文件用的是koa-static,请求到了之后先返回了/source/index.htm,在/source/index.htm中有一些link标签,他们需要的文件因为又了koa-static制定的目录,其目录下面的文件都能按照路径返回。<br />路由用中间件的是koa-mount。<a name="fbxnK"></a>### 详情页面详情页面的技术点:模版引擎、RPC通信<a name="uHrB0"></a>#### 模版引擎模版引擎要点- include 子模板- xss 过滤、模板 helper 函数课程使用了es6 模版字符串,借用了vm模块的runInNewContext方法,实现了一个类似ejs的基本模版引擎。1. 如何把字符串作为代码- eval- new Function(codeStr)()```javascriptlet code = `var a = 10; let b = 20; console.log('b is:', b)`new Function(code)()
- node的vm模块 ```javascript const vm = require(‘vm’) global.a = 100;
// 运行在当前环境中[当前作用域] vm.runInThisContext(‘console.log(a)’); // 100
// 运行在新的环境中[其他作用域] vm.runInNewContext(‘console.log(a)’); // a is not defined
vm.runInThisContext、vm.runInNewContext其不同点就是代码的作用域不同,vm.runInNewContext需要给一个参数,作为作用域对象,所以大致假设一个字符串:```javascript`<div>${user.name}</div>`
那么利用vm模块我们把user指定为其作用域
const user = { name: 'yxnne' }vm.runInNewContext('`<div>${user.name}</div>`', { user })
- 转义
不过需要转义一下,因为user可能包含script标签
比如:
const user = {name : '<script>alert()</script>'}
所以增加一个转义函数
const user = {name : '<script>alert()</script>'}// 调用转义函数 _vm.runInNewContext('`<div>${ _(user.name) }</div>`', {user,_: function () {if (!markup) return '';return String(markup).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/'/g, ''').replace(/"/g, '"')}}})
- include实现
include可以在当前模版中包含其他模版的代码,这里的思路还是定义一个include函数,inclue函数可以接受模版名称,返回模版代码:
const context = {include(name) {return templateMap[name]()},_: function(markup) { /* ..略.. */ }}
我们把所有模版用对象作成k-v的形式,这样就能根据name对应上了:
const templateMap = {templateA: '`<h2>${include("templateB")}</h2>`',templateB: '`<p>hahahaha</p>`'}
把模版对象里面统一变成函数,这样:
Object.keys(templateMap).forEach(key=> {const temp = templateMap[key];templateMap[key] = vm.runInNewContext(`(function() {return ${ temp }})`, context);})console.log(templateMap['templateA']());
以上只是思路。
按照思路,封装一个简单的,完整的:
const fs = require('fs');const vm = require('vm');const templateCache = {};// 创建模板渲染contextconst templateContext = vm.createContext({include: function (name, data) {const template = templateCache[name] || createTemplate(name)return template(data);}});/**** @param {*} templatePath 模板路径* @returns 渲染好的html字符串*/function createTemplate(templatePath) {templateCache[templatePath] = vm.runInContext(`(function (data) {with (data) {return \`${fs.readFileSync(templatePath, 'utf-8')}\`}})`,templateContext);return templateCache[templatePath]}module.exports = createTemplate
RPC通信
还是采用tcp建立全双工通信,协议使用protocol-buffers的方案。
整体代码部分在这里。
详情页实现
播放页面
这一页的整体流程是,用户请求到达BFF,BFF请求server拿到相关数据后,render模版,用户侧呈现页面。不过这页存在交互行为,所以需要发起Ajax请求。
API的方案
- RESTful
- 简单易懂
- 可以快速搭建
- 在数据的聚合方面有很大劣势(涉及多个资源,可能需要多个请求,而且存在大量的无用属性)
- GraphQL
BFF接受到query=hello后:
const { graphql, buildSchema } = require('graphql');// graphql schema协议const schema = buildSchema(`type Query {hello: String}`);// 获得数据的方式root = {hello: () => {return 'Hello world!';},};// 路由把hello参数取出然后调用这里function(query) {// 依照graphql的规则去取数据return graphql(schema, query, root).then((response) => {return response;});}
koa- graphql是现成的koa中间件(),举个例子:
const app = new (require('koa'));const graphqlHTTP = require('koa-graphql');app.use(graphqlHTTP({schema: require('./schema')}))app.listen(3000);
const { graphql, buildSchema } = require('graphql');// 定义数据// type Query定义了查询的方法// 请求comment,返回Array<Comment>const schema = buildSchema(`type Comment {id: Intavatar: Stringname: StringisTop: Booleancontent: StringpublishDate: StringcommentNum: IntpraiseNum: Int}type Query {comment: [Comment]}`)// 定义了饭回数据的方法schema.getQueryType().getFields().comment.resolve = () => {// 真实情况就return 真实取到的数据return [{id: 1001,avatar: "https://static001.geekbang.org/account/avatar/00/0f/52/62/1b3ebed5.jpg",name: "abc",isTop: true,content: "哈哈哈哈",publishDate: "今天",commentNum: 10,praiseNum: 5}]}module.exports = schema;
type Query定义了查询的方法请求comment,返回Array
{"data": {"comment" : [{ "name": "abc"}]}}
播放页实现
整体流程是,用户请求到达BFF,BFF请求server拿到相关数据后,render模版,用户侧呈现页面。不过这页存在交互行为,所以需要发起Ajax请求,前端Ajax请求到达后端后,后端使用GraphQL的Mutation的方式处理。Mutation就是对数据要有所改变,比如点赞评论,点赞之后点赞数要增加,这就是一个Mutation。
const schema = buildSchema(`// ...type Mutation {praise(id: Int): Int}`)// 处理mutation// 处理name是praise的mutationschema.getMutationType().getFields().praise.resolve = (args0, { id }) => {mockDatabase[id].praiseNum++;return mockDatabase[id].praiseNum}
全部代码在这里:
code: 播放页面
列表页
前后端同构
这里讲解的技术方案是同构。
后端需要渲染列表页,基于一下几个点:
- 首屏加速
- SEO
- 前端也需要渲染列表
- 无刷新过滤、排序
啥是前前后端同构 ?
同一个模板/组件,可在浏览器渲染,也可在 Node.js 渲染
- ReactDomServer.renderToString()
- VueServerRenderer.renderToString()
举个列子:node端使用ReactDomServer.renderToString() 输出渲染后的组件,
假设现在有个React组件:
const React = require('react');class App extends React.Component {render() {return (<p></p>)}}module.exports = <App />
借助babel进行语法转化,然后借助ReactDomServer.renderToString() 可以转化成html:
require("@babel/register")({presets: ['@babel/preset-react']})const ReactDOMServer = require('react-dom/server');console.log(ReactDOMServer.renderToString(require('./index.jsx')))
但是如果使用了redux或者vuex之类的,会相对麻烦点,不过推荐Next.js(),这是个成熟的同构方案。
axios也是前后端同构的请求方案,是HTTP请求器。
前后端同构的关键是职责分离,那一些事处理环境的,哪些事处理数据的,等等…
列表页实现
同构流程是这样:
- 当浏览器请求到服务端后,服务端先得到页面数据,用数据渲染react组件,然后转成html string,插入到待返回的html中,然后将html返回给浏览器;
- 在返回给浏览器的html中存在一段“同构代码”,就是react组件用webpack打包之后的js文件。
- 当浏览器获得server返回的html之后,解析html呈现页面,可以快速的显示出html内容(执行到同构代码的时候,会再次渲染一次,但是用户感知不到,react有机制可以将开销最小化)
- 当页面出现交互行为,发请求其实走的就是浏览器端执行的代码了,可以去请求API或者其他操作。
