一直在做移动端开发, 用到了骨架屏, 发现公司的骨架屏是基于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官网插件学习 )
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();
})();
从上图我们可以看到,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的操作,本次值处理了button 和 image 两种情况,后续会继续升级迭代
插件配置文件如下:
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…
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!