基于 React 同构直出的 Sonic 使用示例。React 同构直出使用 Redux/Next.js/Koa2 实现。

licensePRs Welcomewiki

目录- 技术栈- 快速开始 - 安装 - 启动 - 脚本- 项目架构 - 目录结构 - 原理- 技术支持- License

§ 技术栈


§ 快速开始

推荐升级到 node 8.x + npm 5.x 环境。

⊙ 安装

  1. git clone https://github.com/Tencent/VasSonic.git <my-project-name>
  2. cd <my-project-name>/sonic-react
  3. npm install # 安装项目依赖

⊙ 启动

  1. npm run build
  2. npm start

后端接入指引-React版本 - 图4

手机端安装 Android 或 iOS 测试用应用程序。(下载

然后将手机与服务器连接在同一局域网下,查看服务器 ip 配置手机代理,并设置测试链接地址为 http://服务器ip:3000/demo

1:设置手机代理2:设置测试链接
设置手机代理设置测试链接
3:访问 demo4:效果演示
访问demodemo

⊙ 脚本

npm run <script>描述
start启动服务(生产环境,需先执行 npm run build 命令)
dev启动服务(开发环境,无需执行 npm run build 命令)
build打包构建到目录 .next

§ 项目架构

⊙ 目录结构

  1. .
  2. ├── components # demo 页面视图组件
  3. ├── containers # demo 页面容器组件
  4. ├── pages # Next.js 用于存放每个页面入口组件的目录
  5. └── demo.js # demo 页面入口 js
  6. ├── redux # Redux 相关模块
  7. └── duck.js # ducks 模式组织 redux 模块
  8. ├── static # Next.js 用于存放静态资源的目录
  9. └── server.js # 服务入口 js

⊙ 原理

我们不去深究 React 直出以及示例中拼图游戏逻辑的实现,主要来说明下示例中是如何在 React 项目中使用 Sonic 的,流程图如下所示:

后端接入指引-React版本 - 图9

  • 服务端拦截 React 渲染出来的HTML字符串,添加 HTML 注释标签来帮助 Sonic 区分模板和数据块。数据块需要通过 <!— sonicdiff-moduleName —> <!— sonicdiff-moduleName-end —> 来标记,剩下的部分称为模版。示例中代码实现如下:
  1. /**
  2. * 添加 Sonic 所需的 HTML 注释标签
  3. *
  4. * 举例:
  5. * <!DOCTYPE html> <!DOCTYPE html>
  6. * <html> <html>
  7. * <head></head> <head></head>
  8. * <body> <body>
  9. * … … … …
  10. * <div id="root" data-sonicdiff="firstScreenHtml"> => <!-- sonicdiff-firstScreenHtml -->
  11. * … … <div id="root" data-sonicdiff="firstScreenHtml">
  12. * </div> … …
  13. * … … </div>
  14. * <script> <!-- sonicdiff-firstScreenHtml-end -->
  15. * __NEXT_DATA__=xxx … …
  16. * </script> <!-- sonicdiff-initState -->
  17. * </body> <script>
  18. * __NEXT_DATA__=xxx
  19. * </script>
  20. * <!-- sonicdiff-initState-end -->
  21. * </body>
  22. *
  23. * @param html {string} 原始 HTML 字符串
  24. * @returns {string} 添加注释标签后的 HTML 字符串
  25. */
  26. function formatHtml(html) {
  27. const $ = cheerio.load(html);
  28. $('*[data-sonicdiff]').each(function(index, element) {
  29. let tagName = $(this).data('sonicdiff');
  30. return $(this).replaceWith('<!--sonicdiff-' + tagName + '-->' + $(this).clone() + '<!--sonicdiff-' + tagName + '-end-->');
  31. });
  32. html = $.html();
  33. html = html.replace(/<script\s*>\s*__NEXT_DATA__\s*=([\s\S]+?)<\/script>/ig, function(data1) {
  34. return '<!--sonicdiff-initState-->' + data1 + '<!--sonicdiff-initState-end-->';
  35. });
  36. return html;
  37. }
  • 服务端使用 sonic_differ 模块对数据进行处理后输出给浏览器。
  1. server.use(async (ctx, next) => {
  2. await next();
  3.  
  4. // 只拦截 html 请求
  5. if (!ctx.response.is('html')) {
  6. return;
  7. }
  8.  
  9. // 非 sonic 模式不做特殊处理
  10. if (!ctx.request.header['accept-diff']) {
  11. ctx.body = ctx.state.resHtml;
  12. return;
  13. }
  14.  
  15. // 使用 sonic_differ 模块对数据进行处理
  16. let sonicData = sonicDiff(ctx, formatHtml(ctx.state.resHtml));
  17.  
  18. if (sonicData.cache) {
  19. // sonic 模式:完全缓存
  20. ctx.body = '';
  21. } else {
  22. // 其它 sonic 状态
  23. ctx.body = sonicData.data;
  24. }
  25. });
  • 前端在执行到 componentDidMount() 阶段时,通过 js 调用终端接口来获取 sonic 状态和数据,根据终端返回的不同状态,来决定如何渲染页面。
  1. componentDidMount() {
  2. // 获取客户端返回的 sonic 状态和数据,根据终端返回数据做出相应的处理
  3. this.getSonicData((status, sonicUpdateData) => {
  4. switch (status) {
  5. // sonic 状态:数据更新
  6. case 3:
  7. // 使用客户端返回的数据更新页面 Store
  8. let initState = sonicUpdateData['{initState}'] || '';
  9. initState.replace(/<!--sonicdiff-initState-->\s*<script>\s*__NEXT_DATA__\s*=([\s\S]+?)module=/ig, function(matched, $1) {
  10. window.__NEXT_DATA__ = JSON.parse($1);
  11. });
  12. this.props.initImgArr(window.__NEXT_DATA__.props.initialState.gameArea);
  13. break;
  14. default:
  15. break
  16. }
  17. // 展示 sonic 状态
  18. this.props.setSonicStatus(status);
  19. });
  20. }
  21.  
  22. getSonicData(callback) {
  23. let sonicHadExecute = 0; // 判断回调是否触发过的标识
  24. const timeout = 3000; // 终端接口 3s 内没有响应,触发超时逻辑
  25.  
  26. // 调用终端接口通知客户端进行 sonic 处理逻辑
  27. window.sonic && window.sonic.getDiffData();
  28.  
  29. function sonicCallback(data) {
  30. if (sonicHadExecute === 0) {
  31. sonicHadExecute = 1;
  32. callback(data['sonicStatus'], data['sonicUpdateData']);
  33. }
  34. }
  35.  
  36. setTimeout(function() {
  37. if (sonicHadExecute === 0) {
  38. sonicHadExecute = 1;
  39. callback(0, {});
  40. }
  41. }, timeout);
  42.  
  43. // 终端调用 getDiffDataCallback 方法将数据传递给页面
  44. window['getDiffDataCallback'] = function (sonicData) {
  45. /**
  46. * Sonic 状态:
  47. * 0: 异常
  48. * 1: 首次加载(首次和正常页面逻辑一样,前端无需特殊处理)
  49. * 2: 模板更新(当模版发生变化时,终端会自动刷新当前页面,前端也无需特殊处理)
  50. * 3: 数据更新(sonic页面模版没有变化,只有数据块发生变化,终端会返回变化的数据块名称和内容,前端只需要把变化的内容替换到页面即可)
  51. * 4: 完全缓存(sonic页面模版和数据都没有变化,页面无需任何处理)
  52. */
  53. let sonicStatus = 0;
  54. let sonicUpdateData = {}; // 数据更新时终端返回的数据
  55. sonicData = JSON.parse(sonicData);
  56. switch (parseInt(sonicData['srcCode'], 10)) {
  57. case 1000:
  58. sonicStatus = 1;
  59. break;
  60. case 2000:
  61. sonicStatus = 2;
  62. break;
  63. case 200:
  64. sonicStatus = 3;
  65. sonicUpdateData = JSON.parse(sonicData['result'] || '{}');
  66. break;
  67. case 304:
  68. sonicStatus = 4;
  69. break;
  70. }
  71. sonicCallback({ sonicStatus: sonicStatus, sonicUpdateData: sonicUpdateData });
  72. };
  73. }

§ 技术支持

遇到其他问题,可以:

  • 通过demo来理解 sample
  • 联系我们。

§ License

VasSonic is under the BSD license. See the LICENSE file for details.