最新公告
  • 欢迎您光临网站无忧模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 手撸一个webpack骨架屏插件

    正文概述 掘金(一眼万年)   2021-08-18   393

    一直在做移动端开发, 用到了骨架屏, 发现公司的骨架屏是基于chrom插件生成的, 即在浏览器中运行网页地址, 然后基于插件生成(前人实现的, 具体的逻辑没有深入), 那么我们可不可以在代码打包的时候就动态的生成骨架屏呢?方案肯定是可行的, 说干就干,于是就开始了本次的倒腾之旅......其实市面上的骨架屏技术还是挺多的,本次只是一个学习过程,希望能带给大家一定的收获.

    实现思路

    • webpack插件钩子,获取编译完成之后的回调
    • 本地启一个服务器,运行npm run build之后的本次编译的代码
    • 通过puppeteer去访问生成的页面,抓取内容
    • 解析抓取的内容, 生成骨架屏元素

    经过了不断的调研和研究,总结了以上四步的实现思路,下面就来一步一步的实现吧(过程中总是磕磕绊绊, 生活就是这样,坚持了, 就能有一点收获)

    1. webpack插件钩子函数

    webpack官网提供了compiler钩子, 来监听webpack打包编译时的一系列动作,语法如下:

    compiler.hooks.someHook.tap('MyPlugin', (params) => {
      /* ... */
    });
    

    someHook是webpack官网提供的一系类hook,通过tap来注册, 例如:

    • environment 在编译器准备环境时调用,时机就在配置文件中初始化插件之后。

    • afterEnvironment 当编译器环境设置完成后,在 environment hook 后直接调用。

    • done 在 compilation 完成时执行,即webpack编译完成之后执行。(本次主要用到的hook)

    ......


    看了官网的示例,好, 那我们来手动实现一下:
    //注意: 要与注册的插件名一样
    const PLUGIN_NAME = 'SkeletonPlugin';//插件的名称
    class SkeletonPlugin {
      //代表webpack编译对象
      apply(compiler) {
        //webpack 插件钩子, 可以通过tap来注册这些钩子函数的监听
        // done 表示整个编译流程都走完了, dist目录下的文件生成了就可以触发done的回调
        compiler.hooks.done.tap(PLUGIN_NAME, async () => {
          console.log('webpack 编译结束')
        })
      }
    }
    module.exports = SkeletonPlugin;
    
    // webpack.config.js文件中注册插件
    const {SkeletonPlugin} = require('./skeleton');
    plugins:[
            new HtmlWebpackPlugin({
                template:'./src/index.html'
            }),
            new SkeletonPlugin()
        ]
    

    手撸一个webpack骨架屏插件

    学习到了这里,就已经掌握了webpack编写一个插件的基本语法了,如需更深层次的掌握,可以移步( webpack官网插件学习 )

    2. 本地起一个服务

    为什么本地要启一个服务? 我想这是很多读者的困惑,主要是结合puppeteer去抓取网页的内容, 当我们去 npm run build的时候, 启一个服务,模拟本次编译运行的url, 简单理解为npm run dev的时候打开网页,只是把这个过程放在了npm run build的时候

    3. Puppeteer去访问这个生成的页面, 抓取内容

    • Puppeteer简介: Puppeteer 是一个 Node 库,它提供了一个高级 API 来通过 DevTools 协议控制 Chromium 或 Chrome

    怎么理解? 就是我们可以通过Puppeteer去控制Chrome, 模拟用户行为, 比如说元素点击, 爬虫,抓取网页内容等等, 我们来来简单看看Puppeteer的使用:

    //安装Puppeteer
    npm i puppeteer
    // 引入
    const puppeteer = require('puppeteer');
    
    (async () => {
      //创建一个浏览器
      const browser = await puppeteer.launch();
      //创建一个网页
      const page = await browser.newPage();
      //打开网页 www.baidu.com
      await page.goto('https://www.baidu.com');
      //要在页面实例上下文中执行的方法
      await page.evaluate(x => {
        return Promise.resolve(8 * x);
      }, 7);
      //关闭浏览器
      await browser.close();
    })();
    

    手撸一个webpack骨架屏插件

    从上图我们可以看到,Puppeteer打开了百度的网页,并执行了指定的方法.

    4. 解析抓取的内容, 生成骨架屏元素

    这个实现思路主要是拿到页面上面所有的dom元素, 然后遍历, 获取元素类型tagName, 然后对每个元素类分类处理,动态添加类名和样式,核心代码实现如下:

    const styleCache = new Map()
    //转换原始元素为骨架屏元素
      //遍历整个dom树,获取每一个节点或者元素,根据元素类型进行转换
      function genSkeleton(options) {
        let rootElement = document.documentElement;
        ; (function traverse(options) {
          let { button, image } = options
          const buttons = []  //所有的按钮
          const imgs = []  //所有的图片
            ; (function preTraverse(element) {
              if (element.children && element.children.length > 0) {
                //如果有子元素, 遍历子元素
                Array.from(element.children).forEach(children => preTraverse(children))
              }
              if (element.tagName == 'BUTTON') {
                buttons.push(element)
              } else if (element.tagName == 'IMG') {
                imgs.push(element)
              }
            })(rootElement)
          buttons.forEach(item => buttonHandler(item, button))
          imgs.forEach(item => imageHandler(item, image))
        })(options)
        let rules = '';
        for (const [selector, rule] of styleCache){
          rules += `${selector} ${rule}\n`
        }
        console.log('rules ==>', rules)
        //创建style元素, 注入样式
        const styleElement = document.createElement('style');
        styleElement.innerHTML= rules;
        document.head.appendChild(styleElement);
      }
    //处理按钮
    function buttonHandler(element, options = {}) {
        const className = CLASS_NAME_PREFIX + 'button';  // sk-button
        const rule = `{
          color: ${options.color} !important;
          background:  ${options.color} !important;
          border:  none !important;
          box-shadow: none !important;
        }`
    
        addStyle(`.${className}`, rule)
        element.classList.add(className)
      }
      
    //处理图片
    function imageHandler(element, options = {}) {
        const { height, width } = element.getBoundingClientRect()
        const attrs = {
          width, height, src: SMALLEST_BASE64
        }
        setAttributes(element, attrs)
        const className = CLASS_NAME_PREFIX + 'image';  // sk-image
        const rule = `{
          background:  ${options.color} !important;
        }`
    
        addStyle(`.${className}`, rule)
        element.classList.add(className)
      }
    
    //dom上设置元素属性 height with src等
    function setAttributes(element, attrs) {
        Object.keys(attrs).forEach(key => element.setAttribute(key, attrs[key]))
      }
      
    //把对应的类名和样式 一一对应存起来
    function addStyle(selector, rule) {
        if (!styleCache.has(selector)) {
          styleCache.set(selector, rule)
        }
      }
    
    

    到这里基本上就完成了dom的操作,本次值处理了buttonimage 两种情况,后续会继续升级迭代

    插件配置文件如下:

    new SkeletonPlugin({
              staticDir: resolve(__dirname, 'dist'),
              port: '8002', //本地服务端口
              origin: 'http://localhost:8002', // Puppeteer打开地址
              device: 'iPhone 6',  //Puppeteer模拟的分辨率
              defer: 5000, //脚本执行时间
              button: { //按钮配置
                color: '#EFEFEF'
              },
              image: { //图片配置
                color: '#EFEFEF'
              }
            })
    
    

    - 疑问一: defer的作用是啥?

    上文已经提到过了dom解析成骨架屏的流程, 这里是把这段js挂在Windows上面执行的, 相当于增加了一个全局变量, 这个js文件执行是要时间的,于是就有了这个变量, 结构如下:

    //增加一个全局变量 名字叫做Skeleton
    window.Skeleton = (function () {
      function genSkeleton(options) {
        //TODO
      }
    })()
    
    async makeSkeleton(page) {
        const { defer = 5000 } = this.options
        //先读取脚本内容
        let scriptContent = await readFileSync(resolve(__dirname, 'skeletonScript.js'), 'utf8')
        //通过addScriptTag 向页面中注入这个脚本
        await page.addScriptTag({ content: scriptContent })
        //推迟时间
        await sleep(defer)
        await page.evaluate((options) => {
          //调用window上面的方法
          Skeleton.genSkeleton(options)
        }, this.options)
      }
    

    - 疑问二: 生成的骨架屏是如何替换的原有dom的?

    在原有dom上我们是有一个shell占位符的

    <div id="root"><!-- shell--></div>
    

    我们只需要把生成的dom和样式替换掉这个占位符就好了

    const skeletonHTML = await this.skeleton.genHTML(this.options.origin);
         //console.log('skeletonHTML', skeletonHTML)
          const originPath = resolve(this.options.staticDir, 'index.html');
          const originHTML = await readFileSync(originPath, 'utf8');
          const finalHTML = originHTML.replace('<!-- shell-->', skeletonHTML);
          await writeFileSync(originPath, finalHTML);
    

    实现效果, 打包之后, 发现dist/index.html文件已经有个骨架屏相关的dom和样式了

    <div id="root">
        <style>
          .sk-button {
            color: #EFEFEF !important;
            background: #EFEFEF !important;
            border: none !important;
            box-shadow: none !important;
          }
    
          .sk-image {
            background: #EFEFEF !important;
          }
        </style>
    
        <div id="root">
          <div><img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" width="359"
              height="201.984375" class="sk-image"><button class="sk-button">点我</button></div>
        </div>
      </div>
    

    以上, 一个简易版本的骨架屏就已经完成了, 当然这只是生成了两种元素,后面会继续完善.

    参考链接
    [1]webpack官网插件地址: webpack.docschina.org/api/compile…

    [2]puppeteer官网地址: github.com/puppeteer/p…


    下载网 » 手撸一个webpack骨架屏插件

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元