CPQ

程沛权

针对document.write渲染的优化方案(适用webpack按需加载)

程沛权2018/12/15 16:34:00

本文最后更新于 5 年 11 个月前,部分内容可能不适合当前所有情况,仅供参考。

参与过 discuz 相关业务开发的同学应该都知道,dz 论坛有一套自带的 api 系统叫数据调用(后台-门户-模块管理-数据调用),对于论坛运营同学来说,可以将模块的外部调用作为广告位数据源、或者引用到专题页面去展示论坛内容,很受运营的喜爱。

但这套系统有个问题就是,所有的渲染方式都是通过document.write去抓取模板语法生成的 html 数据,而document.write我们都知道,在现代化的前端开发里是越来越排斥这个方法,因为会引起性能问题,影响网页的渲染速度,write 的越多,渲染越慢。

而且 write 方法去渲染 html,更多情况是适合手写页面的年代,现在前端开发都是通过 node+webpack 工程化打包,那么遇到必须 write 又不想 write 的情况应该怎么办?最近刚好对手里的一个项目做了渲染优化,在这里顺便做个总结,讲一下对document.write渲染 html 的一个优化思路,可以结合到 MVVM 框架(如 Vue.js)里去使用。

痛点分析

一个多元化的专题里面,会有轮播图、帖子列表、帖子排行榜等不同的模块,而传统的数据调用渲染方式,注定了一个模块只能一个调用,于是一个页面下来会有 N 个数据调用,也就是有 N 个document.write

回顾传统的方法

通过 html 模板配置数据调用,在 html 里引入数据调用,会通过 write 去渲染 html,如果页面有太多 write,渲染会卡顿严重。

相关模板如下,传统的模板就是 html 结构长什么样,模板结构就长什么样:

<ul>
  [loop]
  <li>
    <a href="{url}">
      <img src="{pic}">
      <p>{title}</p>
      <p>{dateline}</p>
    </a>
  </li>
  [/loop]
</ul>

曾经尝试的方法

如果说直接去 wirte 出 html dom 会严重卡顿,那我是不是可以 write 出 object,先通过 js 处理完数据再一次性渲染?毕竟 wirte 出来的脚本是可以运行的!

答案当然是可以!!!并且自己亲测有效!!!

相关模板如下,把模板写成 js,把数据源定义成一个 json array:

<script type="text/javascript">
var articleData = [
  [index=1]
  {
    "tid": "{id}",
    "subject": "{title}",
    "date": "{dateline}"
  }
  [/index]
  [loop]
  ,{
    "tid": "{id}",
    "subject": "{title}",
    "date": "{dateline}"
  }
  [/loop]
];
</script>

然而!!!但是!!!因为最终负责维护数据源(也就是负责推送或者抓取的人)是运营同学,他们并不清楚 js 或者 json 需要注意的问题,通过 object 格式配置数据调用模板,又会在运营过程中,因为反斜杆,引号等问题导致数据报错,整个页面渲染崩溃。

而且还有一个问题就是,现在用 node+webpack 来做开发的话,这种方式依然必须用传统的渲染方法,也就是在 entry 的 index.html 里,引入这个数据调用才行,无法通过构建打包然后按需加载!!!

优化思路

回顾了痛点,我们梳理一下我们想要的东西:

1、渲染的时候不要 write,更不要直接 write 出好多 DOM 去导致卡顿

2、开发阶段不要去修改 entry 的 index.html,想全部由 webpack 构建生成

3、最终渲染的时候可以按需加载,不要有多少数据源就在打开页面的时候全部加载完

目的理清楚了,解决方案是不是越来越清晰!!!不错,就是 ajax!

我以一个基于 Vue-CLI 的案例来直观的表达我的处理思路和方式吧!(这里有一个地方需要注意,就是通过这种方式来获取数据调用内容源的时候,不能跨域,除非你们服务端配置了允许跨域,否则专题最终都得传到论坛域名下)。

discuz 数据调用模板

这是写在数据调用的模板里的模板代码,具体语法参照模板说明,但是 html 结构无需遵循业务需要,只需要最简单的 html 标签和 className 就可以了,目的是通过 className 去获取对应标签里的文本。

这里是以抓取论坛帖子为例子,抓取了帖子 id、帖子标题、发布时间和缩略图:

<ul>
  [loop]
    <li>
      <span class="tid">{id}</span>
      <span class="subject">{title}</span>
      <span class="date">{dateline}</span>
      <img class="cover" src="{pic}">
    </li>
  [/loop]
</ul>

Vue 组件模板

写在 Vue 组件的 template 里,这是一个包含了链接、封面、标题、发布时间的文章列表。

<ul class="article-list">
  <li class="item" v-for="item in articleList">
    <router-link :to="`/article/${item.tid}`" >
      <div class="cover">
        <img :src="item.cover">
      </div>
      <div class="subject">{{ item.subject }}</div>
      <div class="date">{{ item.date }}</div>
    </router-link>
  </li>
</ul>

Vue 数据格式

在 Vue 组件 data 里的一个数据,最终文章列表的数据源是一个 JSON 数组:

articleList: [
  {
    tid: '123',
    subject: '文章的标题111',
    date: '2018-11-11',
    cover:
      'https://chengpeiquan.com/img/cover1.jpg?x-oss-process=image/interlace,1',
  },
  {
    tid: '456',
    subject: '文章的标题222',
    date: '2018-12-12',
    cover:
      'https://chengpeiquan.com/img/cover2.jpg?x-oss-process=image/interlace,1',
  },
]

Vue 数据请求方法

请求数据如我们前面说的,通过 ajax 去获取,避开document.write带来的性能问题,以下是基于 axios 的请求演示,请根据业务场景调整。

this.$ajax({
  method: 'get',
  url: '/api.php',
  params: {
    mod: 'js',
    bid: 123,
  },
})
  .then((response) => {
    // 把数据调用返回的数据进行格式化
    const DATA = response.data.slice(16, -3).replace(/\\n|\t/g, '')

    // 缓存格式化后的html代码,写入一个临时的DOM里
    const NEW_DIV = document.createElement('div')
    NEW_DIV.innerHTML = DATA

    // 提取需要的标签内容,转为JSON格式
    const RESULT = []
    const LIST = NEW_DIV.querySelectorAll('li')
    LIST.forEach((item, index) => {
      // 遍历期间都先统一缓存结果
      const RESULT_ITEM = {}
      RESULT_ITEM['tid'] = item.querySelector('.tid').innerText
      RESULT_ITEM['subject'] = item.querySelector('.subject').innerText
      RESULT_ITEM['date'] = item.querySelector('.date').innerText
      RESULT_ITEM['cover'] = item.querySelector('img').src
      RESULT.push(item)

      // 遍历结束再统一生成虚拟DOM
      if (index === LIST.length - 1) {
        this.articleList = RESULT
      }
    })
  })
  .catch((error) => {
    console.log(error)
  })