很多时候看到网页上的某些dom结构和其样式,想要将其一起复制下来,这样以后想要什么样式的时候就不用自己去写了 。直接copy非常的方便。但苦于没有找到合适的工具,只能自己尝试写代码去实现。
方法1
第一次尝试的方法是使用getComputedStyle这个api,遍历dom,获取每个节点的样式,拼接出字符串,代码如下
const tags = ['br', 'hr', 'img', 'input', 'param', 'meta', 'link'];
let dom = document.createElement('div')
document.body.appendChild(dom)
let obj = window.getComputedStyle(dom)
let attrsListCamel = Object.keys(obj).filter(key=>!/\d/.test(key)&&!/webkit/.test(key))
let defaultValue = {}
let lineMap = {}
attrsListCamel.forEach(key => {
defaultValue[key] = obj[key]
lineMap[key] = key.replace(/[A-Z]/g,s=>'-'+s.toLowerCase())
})
/* 分隔符转驼峰命名法 */
function separatorToCamelNaming(name) {
const nameArr = name.split(/-/g);
let newName = '';
for (let i = 0, j = nameArr.length; i < j; i++) {
const item = nameArr[i];
if (i === 0) {
newName += item;
} else {
newName += `${item[0].toLocaleUpperCase()}${item.substr(1)}`;
}
}
return newName;
}
/* 将style转换成字符串 */
function style2String(node, styleNames) {
const css = window.getComputedStyle(node);
const styleArr = [];
for (const name of styleNames) {
const fName = separatorToCamelNaming(name);
let value = css[fName];
if (value !== defaultValue[fName]) {
value = value.replace(/"/g, '');
styleArr.push(`${lineMap[fName]}: ${value};`);
}
}
return styleArr.join(' ');
}
let id = 1
/* dom转html */
function dom2html(node,obj,mode = 1) {
let htmltxt = '';
if (node instanceof Comment) return ''
else if (node.nodeName !== '#text') {
const nodeName = node.nodeName.toLowerCase()
const styleStr = style2String(node, attrsListCamel);
if(mode == 1){
if (node instanceof HTMLImageElement) {
htmltxt += `<${nodeName} src="${node.src}" style="${styleStr}">`;
} else {
htmltxt += `<${nodeName} style="${styleStr}">`;
}
}else if(mode == 2){
obj.csstxt += `#id${id}{${styleStr}}`
if (node instanceof HTMLImageElement) {
htmltxt += `<${nodeName} id="id${id++}" src="${node.src}">`;
} else {
htmltxt += `<${nodeName} id="id${id++}">`;
}
}
//处理子节点
if (!tags.includes(nodeName)) {
// 子节点
const childNodes = node.childNodes;
for (let i = 0, j = childNodes.length; i < j; i++) {
htmltxt += dom2html(childNodes[i],obj,mode);
}
htmltxt += `</${nodeName}>`;
}
} else {
htmltxt += node.data;
}
return htmltxt
}
function styleToNumber(value) {
return Number(value.replace('px', ''));
}
function getHtml(node,mode){
let obj = {
csstxt:''
}
let htmltxt = dom2html(node,obj,mode)
return{
csstxt:obj.csstxt,
htmltxt
}
}
使用getHtml方法,第一个参数是dom,第二个参数为1标签内联style,为2提取单独的css
这个方法有很大缺陷
1.上下级dom的很多样式会有重复,因为有的样式可以继承
所有元素可继承:visibility和cursor。
内联元素可继承:letter-spacing(字符间距)、word-spacing(增加或减少单词间的空白(即字间隔))、white-space、line-height、color、font、font-family、font-size、font-style、font-variant、font-weight、text-decoration、text-transform、direction。
终端块状元素可继承:text-indent和text-align。
列表元素可继承:list-style、list-style-type、list-style-position、list-style-image
2.获取的width,height单位都是px,而它可能原本是百分比的,这样dom就不会随着页面的resize而发送改变
方法2
使用document.styleSheets,这个对象的每个属性都是CSSStyleSheet类型的对象
CSSStyleSheet类型的对象中的cssRules和rules是同一个对象,包含了一条一条的样式规则,选择器以及样式
首先把dom结构打平成一层的数组flatDoms,把所有的cssRule收集到一个数组中,遍历这个数组,用document.querySelectorAll获取该条规则对应的dom数组,若该数组和flatDoms有交集,则把该条规则记录下来。
但是有的styleSheet是有跨域的限制,不能读取rules
对此可以尝试通过fetch(sheet.href)去获取该样式表,创建style标签插入到页面中,之后再进行获取dom和样式的操作
完整代码如下
function getStyleRules(){
let obj = {},arr = []
for(sheet of document.styleSheets ) {
try{
sheet.cssRules
for(rule of sheet.cssRules ) {
obj[rule.selectorText] = rule.cssText
arr.push(rule.selectorText)
}
}catch(err){
console.log(sheet,"不能读取");
}
}
return {
obj,arr
}
}
function getFlatArray(root){
let arr = []
function flatdom(dom){
dom.childNodes.forEach(dom=>{
arr.push(dom)
flatdom(dom)
})
}
arr.push(root)
flatdom(root)
return arr
}
function hasTheSameDom(flatDoms,selectedDoms){
for(dom of selectedDoms){
if(flatDoms.includes(dom)){
return true
}
}
return false
}
function getUsedCss(dom){
let flatDoms = getFlatArray(dom),cssArr = [],fakeCssArr=[]
let {arr:selectors,obj:ruleMap} = getStyleRules()
selectors.forEach(selector=>{
if(selector&&selector.includes(':')){
let newselector = selector.split(":")[0]
if(!newselector){//直接以:开头的选择器
fakeCssArr.unshift(ruleMap[selector])
}else{
let selectedDoms = document.querySelectorAll(newselector)
if(hasTheSameDom(flatDoms,selectedDoms)){
fakeCssArr.push(ruleMap[selector])
}
}
}else{
let selectedDoms = document.querySelectorAll(selector)
if(hasTheSameDom(flatDoms,selectedDoms)){
cssArr.push(ruleMap[selector])
}
}
})
return {
cssArr:cssArr.join(''),
fakeCssArr:fakeCssArr.join('')
}
}
function getHtml(dom){
let html = dom.outerHTML
html = html.replace(/<style.*?>(.|\n)*?<\/style>/g,'')
.replace(/<script.*?>(.|\n)*?<\/script>/g,'')
return html
}
function getHtmlAndCss(dom,mode){
if(mode == 1){
let promiseArr = []
for(let sheet of document.styleSheets ) {
try{
sheet.cssRules
}catch(err){
let p = fetch(sheet.href)
.then(response => response.text())
.then(response => {
const st = document.createElement('style');
st.textContent = response;
document.body.append(st);
sheet.ownerNode.parentNode.removeChild(sheet.ownerNode)
});
promiseArr.push(p)
}
}
Promise.all(promiseArr).then(()=>{
console.log({
html:getHtml(dom),
...getUsedCss(dom)
});
}).catch(err=>{
console.log(err)
})
}else{
return{
html:getHtml(dom),
...getUsedCss(dom)
}
}
}
使用getHtmlAndCss方法,第一个参数是dom,第二个参数,如果设置1,则会先尝试去获取所有的rule,如果因为跨域而出现获取不到的情况,则通过fetch实现