本章讲了四个页面:下载页、列表页、详情页、播放页的简单实现。
- 列表页: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)()
```javascript
let 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 = {};
// 创建模板渲染context
const 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: Int
avatar: String
name: String
isTop: Boolean
content: String
publishDate: String
commentNum: Int
praiseNum: 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的mutation
schema.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或者其他操作。