原理

监听打包完成事件
当webpack编译完成后会将打包好的文件输出到我们打包的目录中,我们可以启动一个服务,通过puppeteer 自动化访问生成的页面,生成快照,抓取骨架内容,将页面中的元素通过css 替换成我们想要的效果,比如背景色生成浅灰,页面中的所有button按钮生成深灰色,以及图片、文字等等都有对应的css替换。最后将我们在 html 中的占位替换成骨架屏内容就可以。
image.png

image.png

创建插件

  1. const PLUGIN_NAME = 'SkeletonPlugin'
  2. const Skeleton = require('./Skeleton')
  3. const Server = require('./server')
  4. class SkeletonPlugin{
  5. constructor(options){
  6. this.options = options
  7. }
  8. // compiler 代表了整个webpack配置
  9. apply(compiler){
  10. // tap :以同步方式触发钩子;
  11. compiler.hooks.done.tap(PLUGIN_NAME, async () =>{
  12. // 启动服务,预览项目dist,拿到dom结构,转成骨架屏页面
  13. await this.startServer()
  14. // 用puptier 抓取内容
  15. // 生成骨架屏内容
  16. this.skeleton = new Skeleton(this.options)
  17. // 启动一个chrome 无头浏览器
  18. await this.skeleton.initialize()
  19. // 生成骨架屏html和style
  20. const skeletonHtml = await this.skeleton.genHtml(this.options.origin)
  21. console.log(skeletonHtml)
  22. // 销毁无头浏览器
  23. // await this.skeleton.destroy()
  24. // 关闭服务
  25. // await this.server.close()
  26. })
  27. }
  28. async startServer(){
  29. // 创建服务
  30. this.server = new Server(this.options)
  31. // 启动这个服务
  32. await this.server.listen()
  33. }
  34. }
  35. module.exports = SkeletonPlugin;

创建服务,打开浏览器

  1. const puppeteer = require('puppeteer')
  2. class Skeleton{
  3. constructor(options){
  4. this.options = options
  5. }
  6. // 打开一个浏览器
  7. async initialize(){
  8. this.browser = await puppeteer.launch({headless: false})
  9. }
  10. // 打开新的页签
  11. async newPage(){
  12. let { device } = this.options
  13. // 打开一个新的页签
  14. let page = await this.browser.newPage()
  15. // 选择一个适配设备
  16. await page.emulate(puppeteer.devices[device])
  17. return page
  18. }
  19. async genHtml(url){
  20. let page = await this.newPage()
  21. let res = await page.goto(url, { waitUntil: "networkidle2" })
  22. if(res && !res.ok()){
  23. // 访问失败 抛出异常
  24. throw new Error(`${res.status} on ${url}`)
  25. }
  26. return 'html'
  27. }
  28. async destory(){
  29. if(this.browser){
  30. await this.browser.close()
  31. this.browser = null
  32. }
  33. }
  34. }
  35. module.exports = Skeleton

生成骨架屏dom结构

  1. async makeSkeleton(page){
  2. const {defer = 5000} = this.options
  3. // 读取要生成骨架屏DoM 结构的脚本
  4. const scriptContent = await readFileSync(resolve(__dirname, '../script/index.js'), 'utf8')
  5. // 向页面中注入脚本
  6. await page.addScriptTag({content: scriptContent})
  7. // 执行脚本需要花时间
  8. await sleep(defer)
  9. // 脚本执行完就创建骨架屏DOM结构 在puppteer页面中调用全局方法 evaluate
  10. await page.evaluate((options) =>{
  11. Skeleton.genSkeleton(options)
  12. }, this.options)
  13. }
  14. async genHtml(url){
  15. let page = await this.newPage()
  16. let res = await page.goto(url, { waitUntil: "networkidle2" })
  17. if(res && !res.ok()){
  18. // 访问失败 抛出异常
  19. throw new Error(`${res.status} on ${url}`)
  20. }
  21. // 创建骨架屏dom结构
  22. await this.makeSkeleton(page)
  23. return 'html'
  24. }

处理元素的脚本

  1. // 创建全局变量 用var 或者 window.Skeleton
  2. var Skeleton = (function(){
  3. // 转换原始元素为骨架dom元素
  4. function genSkeleton(){
  5. console.log('骨架')
  6. }
  7. // 获取附加dom 元素的html 和style
  8. function genHtmlAndStyle(){
  9. }
  10. return {genSkeleton, genHtmlAndStyle}
  11. })()

京东骨架屏方案 https://zhuanlan.zhihu.com/p/74403911