是不是路由定义了params
,但是跳转没带上
没有足够的数据
LIXINCHAO 回答了问题 · 1月13日
是不是路由定义了params
,但是跳转没带上
是不是路由定义了params,但是跳转没带上
关注 3 回答 2
LIXINCHAO 收藏了文章 · 1月11日
一般阿里社招都是招3-5年的P6+工程师,但在某boss上有个阿里的技术专家私聊了我要我简历,我说我只有两年经历,但是这boss说,没关系,他喜欢基础好的,让我试一试,于是我也抱着试一试的心态发了简历。
简历发过去之后,boss就给我打了电话,让我简单的介绍一下自己,我就噼里啪啦说了一些,还说了一些题外话。然后boss就开始问我问题。
由于面了四轮,所以最开始的面试记忆有点模糊了。
1.说一下你了解CSS盒模型。
2.说一下box-sizing的应用场景。
3.说一下你了解的弹性FLEX布局.
4.说一下一个未知宽高元素怎么上下左右垂直居中。
5.说一下原型链,对象,构造函数之间的一些联系。
6.DOM事件的绑定的几种方式
7.说一下你项目中用到的技术栈,以及觉得得意和出色的点,以及让你头疼的点,怎么解决的。
8.有没有了解http2.0,websocket,https,说一下你的理解以及你所了解的特性。
第一轮电面大约面了50分钟,就记起来这么多,还有一些细节问题可能淡忘了,总体来说,面的都是以基础为主,然后boss说把我简历推荐给内部,进行正式的社招流程。
这次就直接省略自我介绍了。
1.webpack的入口文件怎么配置,多个入口怎么分割啥的,我也没太听清楚。
2.我看到你的项目用到了Babel的一个插件:transform-runtime以及stage-2,你说一下他们的作用。
3.我看到你的webpack配置用到webpack.optimize.UglifyJsPlugin这个插件,有没有觉得压缩速度很慢,有什么办法提升速度。
4.简历上看见你了解http协议。说一下200和304的理解和区别
5.DOM事件中target和currentTarget的区别
6.说一下你平时怎么解决跨域的。以及后续JSONP的原理和实现以及cors怎么设置。
7.说一下深拷贝的实现原理。
8.说一下项目中觉得可以改进的地方以及做的很优秀的地方?
最后问了有什么需要问的地方,面试到这里基本就结束了,大约面了一个多钟头,还是蛮累的。总体来说,回答的广度和深度以及细节都还算OK,觉得这轮面试基本没什么悬念。
过了几天,接到阿里另一个面试官的电话,上一轮面试通过了,这次是二轮技术面,说估计一个钟头。这次依然跳过自我介绍之类的,直奔主题。
1.有没有自己写过webpack的loader,他的原理以及啥的,记得也不太清楚。
2.有没有去研究webpack的一些原理和机制,怎么实现的。
3.babel把ES6转成ES5或者ES3之类的原理是什么,有没有去研究。
4.git大型项目的团队合作,以及持续集成啥的。
5.什么是函数柯里化?以及说一下JS的API有哪些应用到了函数柯里化的实现?
6.ES6的箭头函数this问题,以及拓展运算符。
7.JS模块化Commonjs,UMD,CMD规范的了解,以及ES6的模块化跟其他几种的区别,以及出现的意义。
8.说一下Vue实现双向数据绑定的原理,以及vue.js和react.js异同点,如果让你选框架,你怎么怎么权衡这两个框架,分析一下。
9.我看你也写博客,说一下草稿的交互细节以及实现原理。
最后面试官问我有什么想问的吗,面试到这里基本就结束了,差不多面了一个小时。
上一轮发挥感觉没前两轮发挥好,所以还是有点不自信的,没想到第三天后,就来电话了,通知我去阿里园区面试。
1.先自我介绍一下,说一下项目的技术栈,以及项目中遇到的一些问题啥的。
2.一个业务场景,面对产品不断迭代,以及需求的变动该怎么应对,具体技术方案实现。
具体业务场景,我就不一一描述,Boss在白板上画了一个大致的模块图,然后做了一些需求描述。
然后需求一层一层的改变,然后往下挖,主要是考察应对产品能力,以及对代码的可维护性和可拓展性这些考察,开放性问题,我觉得还考察一些沟通交流方面的能力,因为有些地方面试官故意说得很含糊,反正就是一个综合能力,以及对产品的理解,中间谈到怎么实现,也问到了一些具体的点,记得问到的有一下几个。
① 怎么获取一个元素到视图顶部的距离。
② getBoundingClientRect获取的top和offsetTop获取的top区别
③事件委托
1.业务场景:比如说百度的一个服务不想让阿里使用,如果识别到是阿里的请求,然后跳转到404或者拒绝服务之类的?
2.二分查找的时间复杂度怎么求,是多少
3.XSS是什么,攻击原理,怎么预防。
4.线性顺序存储结构和链式存储结构有什么区别?以及优缺点。
5.分析一下移动端日历,PC端日历以及桌面日历的一些不同和需要注意的地方。
6.白板写代码,用最简洁的代码实现数组去重。
7.怎么实现草稿,多终端同步,以及冲突问题?
一面的时候其实我自己感觉答得不是特别好,当时面下来感觉要凉了,很幸运的时候还是给我过了。
我准备面试之前对我自己的要求就是,我会的尽量不会很快就被问倒,所以我重点复习了我擅长的知识,并且到网上刷了很多阿里面试题,做了一个整理,现在分享给大家,算是一个感恩回馈吧。
......
一般阿里社招都是招3-5年的P6+工程师,但在某boss上有个阿里的技术专家私聊了我要我简历,我说我只有两年经历,但是这boss说,没关系,他喜欢基础好的,让我试一试,于是我也抱着试一试的心态发了简历。
LIXINCHAO 收藏了文章 · 2020-12-30
本文主要对以下技术要点进行分析:
为了让本文更加容易理解,我将以上技术要点结合在一起写了一个可视化拖拽组件库 DEMO:
建议结合源码一起阅读,效果更好(这个 DEMO 使用的是 Vue 技术栈)。
先来看一下页面的整体结构。
这一节要讲的编辑器其实就是中间的画布。它的作用是:当从左边组件列表拖拽出一个组件放到画布中时,画布要把这个组件渲染出来。
这个编辑器的实现思路是:
componentData
维护编辑器中的数据。push()
方法将新的组件数据添加到 componentData
。v-for
指令遍历 componentData
,将每个组件逐个渲染到画布(也可以使用 JSX 语法结合 render()
方法代替)。编辑器渲染的核心代码如下所示:
<component
v-for="item in componentData"
:key="item.id"
:is="item.component"
:style="item.style"
:propValue="item.propValue"
/>
每个组件数据大概是这样:
{
component: 'v-text', // 组件名称,需要提前注册到 Vue
label: '文字', // 左侧组件列表中显示的名字
propValue: '文字', // 组件所使用的值
icon: 'el-icon-edit', // 左侧组件列表中显示的名字
animations: [], // 动画列表
events: {}, // 事件列表
style: { // 组件样式
width: 200,
height: 33,
fontSize: 14,
fontWeight: 500,
lineHeight: '',
letterSpacing: 0,
textAlign: '',
color: '',
},
}
在遍历 componentData
组件数据时,主要靠 is
属性来识别出真正要渲染的是哪个组件。
例如要渲染的组件数据是 { component: 'v-text' }
,则 <component :is="item.component" />
会被转换为 <v-text />
。当然,你这个组件也要提前注册到 Vue 中。
如果你想了解更多 is
属性的资料,请查看官方文档。
原则上使用第三方组件也是可以的,但建议你最好封装一下。不管是第三方组件还是自定义组件,每个组件所需的属性可能都不一样,所以每个组件数据可以暴露出一个属性 propValue
用于传递值。
例如 a 组件只需要一个属性,你的 propValue
可以这样写:propValue: 'aaa'
。如果需要多个属性,propValue
则可以是一个对象:
propValue: {
a: 1,
b: 'text'
}
在这个 DEMO 组件库中我定义了三个组件。
图片组件 Picture
:
<template>
<div style="overflow: hidden">
<img :data-original="propValue">
</div>
</template>
<script>
export default {
props: {
propValue: {
type: String,
require: true,
},
},
}
</script>
按钮组件 VButton
:
<template>
<button class="v-button">{{ propValue }}</button>
</template>
<script>
export default {
props: {
propValue: {
type: String,
default: '',
},
},
}
</script>
文本组件 VText
:
<template>
<textarea
v-if="editMode == 'edit'"
:value="propValue"
class="text textarea"
@input="handleInput"
ref="v-text"
></textarea>
<div v-else class="text disabled">
<div v-for="(text, index) in propValue.split('\n')" :key="index">{{ text }}</div>
</div>
</template>
<script>
import { mapState } from 'vuex'
export default {
props: {
propValue: {
type: String,
},
element: {
type: Object,
},
},
computed: mapState([
'editMode',
]),
methods: {
handleInput(e) {
this.$emit('input', this.element, e.target.value)
},
},
}
</script>
一个元素如果要设为可拖拽,必须给它添加一个 draggable
属性。另外,在将组件列表中的组件拖拽到画布中,还有两个事件是起到关键作用的:
dragstart
事件,在拖拽刚开始时触发。它主要用于将拖拽的组件信息传递给画布。drop
事件,在拖拽结束时触发。主要用于接收拖拽的组件信息。先来看一下左侧组件列表的代码:
<div @dragstart="handleDragStart" class="component-list">
<div v-for="(item, index) in componentList" :key="index" class="list" draggable :data-index="index">
<i :class="item.icon"></i>
<span>{{ item.label }}</span>
</div>
</div>
handleDragStart(e) {
e.dataTransfer.setData('index', e.target.dataset.index)
}
可以看到给列表中的每一个组件都设置了 draggable
属性。另外,在触发 dragstart
事件时,使用 dataTransfer.setData()
传输数据。再来看一下接收数据的代码:
<div class="content" @drop="handleDrop" @dragover="handleDragOver" @click="deselectCurComponent">
<Editor />
</div>
handleDrop(e) {
e.preventDefault()
e.stopPropagation()
const component = deepCopy(componentList[e.dataTransfer.getData('index')])
this.$store.commit('addComponent', component)
}
触发 drop
事件时,使用 dataTransfer.getData()
接收传输过来的索引数据,然后根据索引找到对应的组件数据,再添加到画布,从而渲染组件。
首先需要将画布设为相对定位 position: relative
,然后将每个组件设为绝对定位 position: absolute
。除了这一点外,还要通过监听三个事件来进行移动:
mousedown
事件,在组件上按下鼠标时,记录组件当前的位置,即 xy 坐标(为了方便讲解,这里使用的坐标轴,实际上 xy 对应的是 css 中的 left
和 top
。mousemove
事件,每次鼠标移动时,都用当前最新的 xy 坐标减去最开始的 xy 坐标,从而计算出移动距离,再改变组件位置。mouseup
事件,鼠标抬起时结束移动。handleMouseDown(e) {
e.stopPropagation()
this.$store.commit('setCurComponent', { component: this.element, zIndex: this.zIndex })
const pos = { ...this.defaultStyle }
const startY = e.clientY
const startX = e.clientX
// 如果直接修改属性,值的类型会变为字符串,所以要转为数值型
const startTop = Number(pos.top)
const startLeft = Number(pos.left)
const move = (moveEvent) => {
const currX = moveEvent.clientX
const currY = moveEvent.clientY
pos.top = currY - startY + startTop
pos.left = currX - startX + startLeft
// 修改当前组件样式
this.$store.commit('setShapeStyle', pos)
}
const up = () => {
document.removeEventListener('mousemove', move)
document.removeEventListener('mouseup', up)
}
document.addEventListener('mousemove', move)
document.addEventListener('mouseup', up)
}
由于拖拽组件到画布中是有先后顺序的,所以可以按照数据顺序来分配图层层级。
例如画布新增了五个组件 abcde,那它们在画布数据中的顺序为 [a, b, c, d, e]
,图层层级和索引一一对应,即它们的 z-index
属性值是 01234(后来居上)。用代码表示如下:
<div v-for="(item, index) in componentData" :zIndex="index"></div>
如果不了解 z-index
属性的,请看一下 MDN 文档。
理解了这一点之后,改变图层层级就很容易做到了。改变图层层级,即是改变组件数据在 componentData
数组中的顺序。例如有 [a, b, c]
三个组件,它们的图层层级从低到高顺序为 abc(索引越大,层级越高)。
如果要将 b 组件上移,只需将它和 c 调换顺序即可:
const temp = componentData[1]
componentData[1] = componentData[2]
componentData[2] = temp
同理,置顶置底也是一样,例如我要将 a 组件置顶,只需将 a 和最后一个组件调换顺序即可:
const temp = componentData[0]
componentData[0] = componentData[componentData.lenght - 1]
componentData[componentData.lenght - 1] = temp
删除组件非常简单,一行代码搞定:componentData.splice(index, 1)
。
细心的网友可能会发现,点击画布上的组件时,组件上会出现 8 个小圆点。这 8 个小圆点就是用来放大缩小用的。实现原理如下:
Shape
组件,Shape
组件里包含 8 个小圆点和一个 <slot>
插槽,用于放置组件。<!--页面组件列表展示-->
<Shape v-for="(item, index) in componentData"
:defaultStyle="item.style"
:style="getShapeStyle(item.style, index)"
:key="item.id"
:active="item === curComponent"
:element="item"
:zIndex="index"
>
<component
class="component"
:is="item.component"
:style="getComponentStyle(item.style)"
:propValue="item.propValue"
/>
</Shape>
Shape
组件内部结构:
<template>
<div class="shape" :class="{ active: this.active }" @click="selectCurComponent" @mousedown="handleMouseDown"
@contextmenu="handleContextMenu">
<div
class="shape-point"
v-for="(item, index) in (active? pointList : [])"
@mousedown="handleMouseDownOnPoint(item)"
:key="index"
:style="getPointStyle(item)">
</div>
<slot></slot>
</div>
</template>
起作用的是这行代码 :active="item === curComponent"
。
先来看一下计算小圆点位置的代码:
const pointList = ['t', 'r', 'b', 'l', 'lt', 'rt', 'lb', 'rb']
getPointStyle(point) {
const { width, height } = this.defaultStyle
const hasT = /t/.test(point)
const hasB = /b/.test(point)
const hasL = /l/.test(point)
const hasR = /r/.test(point)
let newLeft = 0
let newTop = 0
// 四个角的点
if (point.length === 2) {
newLeft = hasL? 0 : width
newTop = hasT? 0 : height
} else {
// 上下两点的点,宽度居中
if (hasT || hasB) {
newLeft = width / 2
newTop = hasT? 0 : height
}
// 左右两边的点,高度居中
if (hasL || hasR) {
newLeft = hasL? 0 : width
newTop = Math.floor(height / 2)
}
}
const style = {
marginLeft: hasR? '-4px' : '-3px',
marginTop: '-3px',
left: `${newLeft}px`,
top: `${newTop}px`,
cursor: point.split('').reverse().map(m => this.directionKey[m]).join('') + '-resize',
}
return style
}
计算小圆点的位置需要获取一些信息:
height
、宽度 width
注意,小圆点也是绝对定位的,相对于 Shape
组件。所以有四个小圆点的位置很好确定:
left: 0, top: 0
left: width, top: 0
left: 0, top: height
left: width, top: height
另外的四个小圆点需要通过计算间接算出来。例如左边中间的小圆点,计算公式为 left: 0, top: height / 2
,其他小圆点同理。
handleMouseDownOnPoint(point) {
const downEvent = window.event
downEvent.stopPropagation()
downEvent.preventDefault()
const pos = { ...this.defaultStyle }
const height = Number(pos.height)
const width = Number(pos.width)
const top = Number(pos.top)
const left = Number(pos.left)
const startX = downEvent.clientX
const startY = downEvent.clientY
// 是否需要保存快照
let needSave = false
const move = (moveEvent) => {
needSave = true
const currX = moveEvent.clientX
const currY = moveEvent.clientY
const disY = currY - startY
const disX = currX - startX
const hasT = /t/.test(point)
const hasB = /b/.test(point)
const hasL = /l/.test(point)
const hasR = /r/.test(point)
const newHeight = height + (hasT? -disY : hasB? disY : 0)
const newWidth = width + (hasL? -disX : hasR? disX : 0)
pos.height = newHeight > 0? newHeight : 0
pos.width = newWidth > 0? newWidth : 0
pos.left = left + (hasL? disX : 0)
pos.top = top + (hasT? disY : 0)
this.$store.commit('setShapeStyle', pos)
}
const up = () => {
document.removeEventListener('mousemove', move)
document.removeEventListener('mouseup', up)
needSave && this.$store.commit('recordSnapshot')
}
document.addEventListener('mousemove', move)
document.addEventListener('mouseup', up)
}
它的原理是这样的:
撤销重做的实现原理其实挺简单的,先看一下代码:
snapshotData: [], // 编辑器快照数据
snapshotIndex: -1, // 快照索引
undo(state) {
if (state.snapshotIndex >= 0) {
state.snapshotIndex--
store.commit('setComponentData', deepCopy(state.snapshotData[state.snapshotIndex]))
}
},
redo(state) {
if (state.snapshotIndex < state.snapshotData.length - 1) {
state.snapshotIndex++
store.commit('setComponentData', deepCopy(state.snapshotData[state.snapshotIndex]))
}
},
setComponentData(state, componentData = []) {
Vue.set(state, 'componentData', componentData)
},
recordSnapshot(state) {
// 添加新的快照
state.snapshotData[++state.snapshotIndex] = deepCopy(state.componentData)
// 在 undo 过程中,添加新的快照时,要将它后面的快照清理掉
if (state.snapshotIndex < state.snapshotData.length - 1) {
state.snapshotData = state.snapshotData.slice(0, state.snapshotIndex + 1)
}
},
用一个数组来保存编辑器的快照数据。保存快照就是不停地执行 push()
操作,将当前的编辑器数据推入 snapshotData
数组,并增加快照索引 snapshotIndex
。目前以下几个动作会触发保存快照操作:
...
假设现在 snapshotData
保存了 4 个快照。即 [a, b, c, d]
,对应的快照索引为 3。如果这时进行了撤销操作,我们需要将快照索引减 1,然后将对应的快照数据赋值给画布。
例如当前画布数据是 d,进行撤销后,索引 -1,现在画布的数据是 c。
明白了撤销,那重做就很好理解了,就是将快照索引加 1,然后将对应的快照数据赋值给画布。
不过还有一点要注意,就是在撤销操作中进行了新的操作,要怎么办呢?有两种解决方案:
[a, b, c, d]
举例,假设现在进行了两次撤销操作,快照索引变为 1,对应的快照数据为 b,如果这时进行了新的操作,对应的快照数据为 e。那 e 会把 cd 顶掉,现在的快照数据为 [a, b, e]
。[a, b, e, c, d]
。我采用的是第一种方案。
什么是吸附?就是在拖拽组件时,如果它和另一个组件的距离比较接近,就会自动吸附在一起。
吸附的代码大概在 300 行左右,建议自己打开源码文件看(文件路径:src\\components\\Editor\\MarkLine.vue
)。这里不贴代码了,主要说说原理是怎么实现的。
在页面上创建 6 条线,分别是三横三竖。这 6 条线的作用是对齐,它们什么时候会出现呢?
具体的计算公式主要是根据每个组件的 xy 坐标和宽度高度进行计算的。例如要判断 ab 两个组件的左边是否对齐,则要知道它们每个组件的 x 坐标;如果要知道它们右边是否对齐,除了要知道 x 坐标,还要知道它们各自的宽度。
// 左对齐的条件
a.x == b.x
// 右对齐的条件
a.x + a.width == b.x + b.width
在对齐的时候,显示标线。
另外还要判断 ab 两个组件是否“足够”近。如果足够近,就吸附在一起。是否足够近要靠一个变量来判断:
diff: 3, // 相距 dff 像素将自动吸附
小于等于 diff
像素则自动吸附。
吸附效果是怎么实现的呢?
假设现在有 ab 组件,a 组件坐标 xy 都是 0,宽高都是 100。现在假设 a 组件不动,我们正在拖拽 b 组件。当把 b 组件拖到坐标为 x: 0, y: 103
时,由于 103 - 100 <= 3(diff)
,所以可以判定它们已经接近得足够近。这时需要手动将 b 组件的 y 坐标值设为 100,这样就将 ab 组件吸附在一起了。
在拖拽时如果 6 条标线都显示出来会不太美观。所以我们可以做一下优化,在纵横方向上最多只同时显示一条线。实现原理如下:
可以发现,关键的地方是我们要知道两个组件的方向。即 ab 两个组件靠近,我们要知道到底 b 是在 a 的左边还是右边。
这一点可以通过鼠标移动事件来判断,之前在讲解拖拽的时候说过,mousedown
事件触发时会记录起点坐标。所以每次触发 mousemove
事件时,用当前坐标减去原来的坐标,就可以判断组件方向。例如 x 方向上,如果 b.x - a.x
的差值为正,说明是 b 在 a 右边,否则为左边。
// 触发元素移动事件,用于显示标线、吸附功能
// 后面两个参数代表鼠标移动方向
// currY - startY > 0 true 表示向下移动 false 表示向上移动
// currX - startX > 0 true 表示向右移动 false 表示向左移动
eventBus.$emit('move', this.$el, currY - startY > 0, currX - startX > 0)
每个组件都有一些通用属性和独有的属性,我们需要提供一个能显示和修改属性的地方。
// 每个组件数据大概是这样
{
component: 'v-text', // 组件名称,需要提前注册到 Vue
label: '文字', // 左侧组件列表中显示的名字
propValue: '文字', // 组件所使用的值
icon: 'el-icon-edit', // 左侧组件列表中显示的名字
animations: [], // 动画列表
events: {}, // 事件列表
style: { // 组件样式
width: 200,
height: 33,
fontSize: 14,
fontWeight: 500,
lineHeight: '',
letterSpacing: 0,
textAlign: '',
color: '',
},
}
我定义了一个 AttrList
组件,用于显示每个组件的属性。
<template>
<div class="attr-list">
<el-form>
<el-form-item v-for="(key, index) in styleKeys" :key="index" :label="map[key]">
<el-color-picker v-if="key == 'borderColor'" v-model="curComponent.style[key]"></el-color-picker>
<el-color-picker v-else-if="key == 'color'" v-model="curComponent.style[key]"></el-color-picker>
<el-color-picker v-else-if="key == 'backgroundColor'" v-model="curComponent.style[key]"></el-color-picker>
<el-select v-else-if="key == 'textAlign'" v-model="curComponent.style[key]">
<el-option
v-for="item in options"
:key="item.value"
:label="item.label"
:value="item.value"
></el-option>
</el-select>
<el-input type="number" v-else v-model="curComponent.style[key]" />
</el-form-item>
<el-form-item label="内容" v-if="curComponent && curComponent.propValue && !excludes.includes(curComponent.component)">
<el-input type="textarea" v-model="curComponent.propValue" />
</el-form-item>
</el-form>
</div>
</template>
代码逻辑很简单,就是遍历组件的 style
对象,将每一个属性遍历出来。并且需要根据具体的属性用不同的组件显示出来,例如颜色属性,需要用颜色选择器显示;数值类的属性需要用 type=number
的 input 组件显示等等。
为了方便用户修改属性值,我使用 v-model
将组件和值绑定在一起。
预览和编辑的渲染原理是一样的,区别是不需要编辑功能。所以只需要将原先渲染组件的代码稍微改一下就可以了。
<!--页面组件列表展示-->
<Shape v-for="(item, index) in componentData"
:defaultStyle="item.style"
:style="getShapeStyle(item.style, index)"
:key="item.id"
:active="item === curComponent"
:element="item"
:zIndex="index"
>
<component
class="component"
:is="item.component"
:style="getComponentStyle(item.style)"
:propValue="item.propValue"
/>
</Shape>
经过刚才的介绍,我们知道 Shape
组件具备了拖拽、放大缩小的功能。现在只需要将 Shape
组件去掉,外面改成套一个普通的 DIV 就可以了(其实不用这个 DIV 也行,但为了绑定事件这个功能,所以需要加上)。
<!--页面组件列表展示-->
<div v-for="(item, index) in componentData" :key="item.id">
<component
class="component"
:is="item.component"
:style="getComponentStyle(item.style)"
:propValue="item.propValue"
/>
</div>
保存代码的功能也特别简单,只需要保存画布上的数据 componentData
即可。保存有两种选择:
在 DEMO 上我使用的 localStorage
保存在本地。
每个组件有一个 events
对象,用于存储绑定的事件。目前我只定义了两个事件:
// 编辑器自定义事件
const events = {
redirect(url) {
if (url) {
window.location.href = url
}
},
alert(msg) {
if (msg) {
alert(msg)
}
},
}
const mixins = {
methods: events,
}
const eventList = [
{
key: 'redirect',
label: '跳转事件',
event: events.redirect,
param: '',
},
{
key: 'alert',
label: 'alert 事件',
event: events.alert,
param: '',
},
]
export {
mixins,
events,
eventList,
}
不过不能在编辑的时候触发,可以在预览的时候触发。
通过 v-for
指令将事件列表渲染出来:
<el-tabs v-model="eventActiveName">
<el-tab-pane v-for="item in eventList" :key="item.key" :label="item.label" :name="item.key" style="padding: 0 20px">
<el-input v-if="item.key == 'redirect'" v-model="item.param" type="textarea" placeholder="请输入完整的 URL" />
<el-input v-if="item.key == 'alert'" v-model="item.param" type="textarea" placeholder="请输入要 alert 的内容" />
<el-button style="margin-top: 20px;" @click="addEvent(item.key, item.param)">确定</el-button>
</el-tab-pane>
</el-tabs>
选中事件时将事件添加到组件的 events
对象。
预览或真正渲染页面时,也需要在每个组件外面套一层 DIV,这样就可以在 DIV 上绑定一个点击事件,点击时触发我们刚才添加的事件。
<template>
<div @click="handleClick">
<component
class="conponent"
:is="config.component"
:style="getStyle(config.style)"
:propValue="config.propValue"
/>
</div>
</template>
handleClick() {
const events = this.config.events
// 循环触发绑定的事件
Object.keys(events).forEach(event => {
this[event](events[event])
})
}
动画和事件的原理是一样的,先将所有的动画通过 v-for
指令渲染出来,然后点击动画将对应的动画添加到组件的 animations
数组里。同事件一样,执行的时候也是遍历组件所有的动画并执行。
为了方便,我们使用了 animate.css 动画库。
// main.js
import '@/styles/animate.css'
现在我们提前定义好所有的动画数据:
export default [
{
label: '进入',
children: [
{ label: '渐显', value: 'fadeIn' },
{ label: '向右进入', value: 'fadeInLeft' },
{ label: '向左进入', value: 'fadeInRight' },
{ label: '向上进入', value: 'fadeInUp' },
{ label: '向下进入', value: 'fadeInDown' },
{ label: '向右长距进入', value: 'fadeInLeftBig' },
{ label: '向左长距进入', value: 'fadeInRightBig' },
{ label: '向上长距进入', value: 'fadeInUpBig' },
{ label: '向下长距进入', value: 'fadeInDownBig' },
{ label: '旋转进入', value: 'rotateIn' },
{ label: '左顺时针旋转', value: 'rotateInDownLeft' },
{ label: '右逆时针旋转', value: 'rotateInDownRight' },
{ label: '左逆时针旋转', value: 'rotateInUpLeft' },
{ label: '右逆时针旋转', value: 'rotateInUpRight' },
{ label: '弹入', value: 'bounceIn' },
{ label: '向右弹入', value: 'bounceInLeft' },
{ label: '向左弹入', value: 'bounceInRight' },
{ label: '向上弹入', value: 'bounceInUp' },
{ label: '向下弹入', value: 'bounceInDown' },
{ label: '光速从右进入', value: 'lightSpeedInRight' },
{ label: '光速从左进入', value: 'lightSpeedInLeft' },
{ label: '光速从右退出', value: 'lightSpeedOutRight' },
{ label: '光速从左退出', value: 'lightSpeedOutLeft' },
{ label: 'Y轴旋转', value: 'flip' },
{ label: '中心X轴旋转', value: 'flipInX' },
{ label: '中心Y轴旋转', value: 'flipInY' },
{ label: '左长半径旋转', value: 'rollIn' },
{ label: '由小变大进入', value: 'zoomIn' },
{ label: '左变大进入', value: 'zoomInLeft' },
{ label: '右变大进入', value: 'zoomInRight' },
{ label: '向上变大进入', value: 'zoomInUp' },
{ label: '向下变大进入', value: 'zoomInDown' },
{ label: '向右滑动展开', value: 'slideInLeft' },
{ label: '向左滑动展开', value: 'slideInRight' },
{ label: '向上滑动展开', value: 'slideInUp' },
{ label: '向下滑动展开', value: 'slideInDown' },
],
},
{
label: '强调',
children: [
{ label: '弹跳', value: 'bounce' },
{ label: '闪烁', value: 'flash' },
{ label: '放大缩小', value: 'pulse' },
{ label: '放大缩小弹簧', value: 'rubberBand' },
{ label: '左右晃动', value: 'headShake' },
{ label: '左右扇形摇摆', value: 'swing' },
{ label: '放大晃动缩小', value: 'tada' },
{ label: '扇形摇摆', value: 'wobble' },
{ label: '左右上下晃动', value: 'jello' },
{ label: 'Y轴旋转', value: 'flip' },
],
},
{
label: '退出',
children: [
{ label: '渐隐', value: 'fadeOut' },
{ label: '向左退出', value: 'fadeOutLeft' },
{ label: '向右退出', value: 'fadeOutRight' },
{ label: '向上退出', value: 'fadeOutUp' },
{ label: '向下退出', value: 'fadeOutDown' },
{ label: '向左长距退出', value: 'fadeOutLeftBig' },
{ label: '向右长距退出', value: 'fadeOutRightBig' },
{ label: '向上长距退出', value: 'fadeOutUpBig' },
{ label: '向下长距退出', value: 'fadeOutDownBig' },
{ label: '旋转退出', value: 'rotateOut' },
{ label: '左顺时针旋转', value: 'rotateOutDownLeft' },
{ label: '右逆时针旋转', value: 'rotateOutDownRight' },
{ label: '左逆时针旋转', value: 'rotateOutUpLeft' },
{ label: '右逆时针旋转', value: 'rotateOutUpRight' },
{ label: '弹出', value: 'bounceOut' },
{ label: '向左弹出', value: 'bounceOutLeft' },
{ label: '向右弹出', value: 'bounceOutRight' },
{ label: '向上弹出', value: 'bounceOutUp' },
{ label: '向下弹出', value: 'bounceOutDown' },
{ label: '中心X轴旋转', value: 'flipOutX' },
{ label: '中心Y轴旋转', value: 'flipOutY' },
{ label: '左长半径旋转', value: 'rollOut' },
{ label: '由小变大退出', value: 'zoomOut' },
{ label: '左变大退出', value: 'zoomOutLeft' },
{ label: '右变大退出', value: 'zoomOutRight' },
{ label: '向上变大退出', value: 'zoomOutUp' },
{ label: '向下变大退出', value: 'zoomOutDown' },
{ label: '向左滑动收起', value: 'slideOutLeft' },
{ label: '向右滑动收起', value: 'slideOutRight' },
{ label: '向上滑动收起', value: 'slideOutUp' },
{ label: '向下滑动收起', value: 'slideOutDown' },
],
},
]
然后用 v-for
指令渲染出来动画列表。
<el-tabs v-model="animationActiveName">
<el-tab-pane v-for="item in animationClassData" :key="item.label" :label="item.label" :name="item.label">
<el-scrollbar class="animate-container">
<div
class="animate"
v-for="(animate, index) in item.children"
:key="index"
@mouseover="hoverPreviewAnimate = animate.value"
@click="addAnimation(animate)"
>
<div :class="[hoverPreviewAnimate === animate.value && animate.value + ' animated']">
{{ animate.label }}
</div>
</div>
</el-scrollbar>
</el-tab-pane>
</el-tabs>
点击动画将调用 addAnimation(animate)
将动画添加到组件的 animations
数组。
运行动画的代码:
export default async function runAnimation($el, animations = []) {
const play = (animation) => new Promise(resolve => {
$el.classList.add(animation.value, 'animated')
const removeAnimation = () => {
$el.removeEventListener('animationend', removeAnimation)
$el.removeEventListener('animationcancel', removeAnimation)
$el.classList.remove(animation.value, 'animated')
resolve()
}
$el.addEventListener('animationend', removeAnimation)
$el.addEventListener('animationcancel', removeAnimation)
})
for (let i = 0, len = animations.length; i < len; i++) {
await play(animations[i])
}
}
运行动画需要两个参数:组件对应的 DOM 元素(在组件使用 this.$el
获取)和它的动画数据 animations
。并且需要监听 animationend
事件和 animationcancel
事件:一个是动画结束时触发,一个是动画意外终止时触发。
利用这一点再配合 Promise
一起使用,就可以逐个运行组件的每个动画了。
由于时间关系,这个功能我还没做。现在简单的描述一下怎么做这个功能。那就是使用 psd.js 库,它可以解析 PSD 文件。
使用 psd
库解析 PSD 文件得出的数据如下:
{ children:
[ { type: 'group',
visible: false,
opacity: 1,
blendingMode: 'normal',
name: 'Version D',
left: 0,
right: 900,
top: 0,
bottom: 600,
height: 600,
width: 900,
children:
[ { type: 'layer',
visible: true,
opacity: 1,
blendingMode: 'normal',
name: 'Make a change and save.',
left: 275,
right: 636,
top: 435,
bottom: 466,
height: 31,
width: 361,
mask: {},
text:
{ value: 'Make a change and save.',
font:
{ name: 'HelveticaNeue-Light',
sizes: [ 33 ],
colors: [ [ 85, 96, 110, 255 ] ],
alignment: [ 'center' ] },
left: 0,
top: 0,
right: 0,
bottom: 0,
transform: { xx: 1, xy: 0, yx: 0, yy: 1, tx: 456, ty: 459 } },
image: {} } ] } ],
document:
{ width: 900,
height: 600,
resources:
{ layerComps:
[ { id: 692243163, name: 'Version A', capturedInfo: 1 },
{ id: 725235304, name: 'Version B', capturedInfo: 1 },
{ id: 730932877, name: 'Version C', capturedInfo: 1 } ],
guides: [],
slices: [] } } }
从以上代码可以发现,这些数据和 css 非常像。根据这一点,只需要写一个转换函数,将这些数据转换成我们组件所需的数据,就能实现 PSD 文件转成渲染组件的功能。目前 quark-h5 和 luban-h5 都是这样实现的 PSD 转换功能。
由于画布是可以调整大小的,我们可以使用 iphone6 的分辨率来开发手机页面。
这样开发出来的页面也可以在手机下正常浏览,但可能会有样式偏差。因为我自定义的三个组件是没有做适配的,如果你需要开发手机页面,那自定义组件必须使用移动端的 UI 组件库。或者自己开发移动端专用的自定义组件。
由于 DEMO 的代码比较多,所以在讲解每一个功能点时,我只把关键代码贴上来。所以大家会发现 DEMO 的源码和我贴上来的代码会有些区别,请不必在意。
另外,DEMO 的样式也比较简陋,主要是最近事情比较多,没太多时间写好看点,请见谅。
本文主要对以下技术要点进行分析:编辑器自定义组件拖拽删除组件、调整图层层级放大缩小撤消、重做组件属性设置吸附预览、保存代码绑定事件绑定动画导入 PSD手机模式为了让本文更加容易理解,我将以上技术要点结合在一起写了一个可视化拖拽组件库 DEMO:github 项目...
LIXINCHAO 回答了问题 · 2020-12-23
activated
生命周期中重新查询一遍列表吧
activated生命周期中重新查询一遍列表吧
关注 3 回答 2
LIXINCHAO 关注了用户 · 2020-12-23
Z 是政采云拼音首字母,oo 是无穷的符号,结合 Zoo 有生物圈的含义。寄望我们的前端 ZooTeam 团队,不论是人才梯队,还是技术体系,都能各面兼备,成长为一个生态,卓越且持续卓越。
政采云前端团队(ZooTeam),一个年轻富有激情和创造力的前端团队,隶属于政采云产品研发部,Base 在风景如画的杭州。团队现有 50 余个前端小伙伴,平均年龄 27 岁,近 3 成是全栈工程师,妥妥的青年风暴团。成员构成既有来自于阿里、网易的“老”兵,也有浙大、中科大、杭电等校的应届新人。团队在日常的业务对接之外,还在物料体系、工程平台、搭建平台、性能体验、云端应用、数据分析及可视化等方向进行技术探索和实战,推动并落地了一系列的内部技术产品,持续探索前端技术体系的新边界。
Z 是政采云拼音首字母,oo 是无穷的符号,结合 Zoo 有生物圈的含义。寄望我们的前端 ZooTeam 团队,不论是人才梯队,还是技术体系,都能各面兼备,成长为一个生态,卓越且持续卓越。政采云前端团队(ZooTeam),一个年轻富有激情和创造力的前端团队,隶属于政采云产...
关注 1487
LIXINCHAO 收藏了文章 · 2020-12-22
大家好,我是你们的 猫哥,一个不喜欢吃鱼、又不喜欢喵 的超级猫 ~
猫哥是一个常年混迹在 GitHub 上的猫星人,所以发现了不少好的前端开源项目、常用技巧,在此分享给大家。
公众号:前端GitHub,专注于挖掘 GitHub 上优秀的前端开源项目,并以专题的形式推荐,每专题大概 10 个好项目,每周会有一到三篇精华文章推送,与时俱进版前端资源教程。
已经推荐了 面试项目
、css 奇技淫巧项目
、代码规范项目
、数据结构与算法项目
、JavaScript 奇技淫巧项目
、前端必备在线工具
等专题的近 100 个优秀项目了。
平时如何发现好的开源项目,可以看看这篇文章:GitHub 上能挖矿的神仙技巧 - 如何发现优秀开源项目
以下为【前端GitHub】的第 13 期精华内容。
程序员简历模板系列。
包括 PHP 程序员简历模板、iOS 程序员简历模板、Android 程序员简历模板、Web 前端程序员简历模板、Java 程序员简历模板、C/C++ 程序员简历模板、NodeJS 程序员简历模板、架构师简历模板以及通用程序员简历模板。
https://github.com/geekcompan...
用 markdown 语法来写的简历模版,非常简结通用。
还介绍了导出 PDF 的方法。
https://github.com/CyC2018/Ma...
项目包括程序员简历例句,程序员简历范例,Java 简历模版,Python 简历模版,C++ 简历模版。
比如前端:
https://github.com/resumejob/...
好用的 MarkDown 在线简历工具,可在线预览、编辑、设置访问密码和生成 PDF。
https://github.com/geekcompan...
支持 Markdown 和富文本的在线简历排版工具,支持切换 4 种模版、自定义和导出功能。
简历的制作过程考验了一个人的两个能力,逻辑能力和细节能力。
https://github.com/mdnice/mar...
是一个帮助你快速生成漂亮简历的工具,它基于 Vue 和 LESS,生成的简历可导出为 PDF 格式。
https://github.com/salomonell...
适合中文的简历模板收集。
该项目收集了好多个中文的简历开源项目模版,比如上图那个。
https://github.com/dyweb/awes...
一份优雅简约的简历
https://github.com/gwuhaolin/...
前端程序员模版,这个模版就是结合前面的几个简历模版,结合笔者自己的工作经验,写的简历,也就是笔者上一年跳槽时的简历 😂,其中部分内容做了处理。
这个是在 VsCode 里面用 markdown 语法写好,然后导出来的,非常简单,而且自由度非常高,随自己怎么修改。
其中装的 VsCode 插件是 markdown-all-in-one 和 markdown-pdf,支持预览 md 文件以及导入各种格式的 md 文件。
支持导出的格式:
上面简历的原文件地址:
https://github.com/biaochenxu...
这个项目是在你的 README 中获取动态生成的 GitHub 统计信息!
不过这种简历的方式要对自己的 GitHub 贡献很有信心才好哦。
比如笔者的如下,也不好做为简历来写 😂
不过只要是 readme 文件,放在 自己的用户名的仓库
里面也是可以的,也能达到第 9 个项目中简历的效果。
https://github.com/anuraghazr...
不知不觉,已经写到第 13 期了呢,已经分享了 100 个好的前端项目了呢,往期精文请看下方宝藏仓库,点击很危险,请慎入!
[前端GitHub]:https://github.com/FrontEndGitHub/FrontEndGitHub
平时如何发现好的开源项目,可以看看这篇文章:如何在 GitHub 上发现优秀开源项目 和 如何使用 GitHub 进行精准搜索的神仙技巧。
可以加超级猫的 wx:CB834301747 ,一起闲聊前端。
微信搜 “前端GitHub”,回复 “电子书” 即可以获得 160 本前端精华书籍哦。
往期精文
喜欢就收藏,顺便点个赞吧,你的支持是超级猫最大的鼓励!
查看原文公众号:前端GitHub,专注于挖掘 GitHub 上优秀的前端开源项目,并以专题的形式推荐,每专题大概 10 个好项目,每周会有一到三篇精华文章推送,与时俱进版前端资源教程。
LIXINCHAO 赞了回答 · 2020-12-10
我觉得你的数据应该不是完整的数据,顶层应该是一个数组对象,每个省是一个数据对象,即完整的应该是:
var AllData=[{...,'children':[{...,'children':[{},{}...]},{}...]},{}...]
即从顶层起,实质结构是类似的,可以分阶段遍历比较查询
你的查询用3层遍历是唯一可行的,不过你的代码没有注意安全性,没有考虑某些层没有匹配的情况。
如果不考虑空间占用,其实可以先对这些数据进行特殊化处理,比如转换成特殊的键值对数据集,就可以更快的查询,大致的意思是你可以重新把数据生成为键值对,比如
{"北京市":"110000",
"北京市-北京市":"110100",
"北京市-北京市-东城区":"110101",
....
"天津市":"120000",
"天津市-天津市":"120100",
"天津市-天津市-和平区":"120101",
...
}
这样扁平化后查询就会很迅速。
而且这个数据其实也可以先由其他方法格式化好,查询起来更方便。
当然,这个只是一般性探讨,其实这样查询不一定有分层查询速度快的,可能只是感觉代码少一些而已。
此外,你可以利用lodash之类的库函数,它对这些查找有封装出来的工具,可以更方便的关注具体逻辑。比如引入Lodash后,相应的代码可能是:
let findXX=function( ArrLists,FindLists){
let rt=[];
let M=ArrLists;
for(let i=0;i<FindLists.length;i++){
let obj = _.find(M, {'label': FindLists[i] });
if(obj==undefined) break;
rt.push(obj['value'])
M=(obj['children']?obj['children']:undefined)
if(M==undefined) break;
}
return rt;
}
console.log(findXX(AllData,["北京市","北京市","da西城区"]))
console.log(findXX(AllData,["北京市","北京市","西城区"]))
console.log(findXX(AllData,["北京市"]))
console.log(findXX(AllData,["天津市","天津市"]))
你的查询用3层遍历是唯一可行的,不过你的代码没有注意安全性,没有考虑某些层没有匹配的情况。如果不考虑空间占用,其实可以先对这些数据进行特殊化处理,比如转换成特殊的键值对数据集,就可以更快的查询,大致的意思是你可以重新把数据生成为键值对,比如
关注 3 回答 2
LIXINCHAO 收藏了文章 · 2020-12-09
最近在写公众号的时候,常常会自己做首图,并且慢慢地发现沉迷于制作首图,感觉扁平化的设计的真好好看。慢慢地萌生了一个做一个属于自己的首图生成器的想法。
制作呢,当然也不是拍拍脑袋就开始,在开始之前,就去研究了一下某在线设计网站(如果有人不知道的话,可以说一下,这是一个在线制作海报之类的网站 T T 像我们这种内容创作者用的比较多),毕竟人家已经做了很久了,我只是想做个方便个人使用的。毕竟以上用 PS 做着还是有一些废时间,由于组成的元素都很简单,做一个自动化生成的完全可以。
但是研究着研究着,就看到了某在线设计网站的水印,像这种技术支持的网站,最重要的防御措施就是水印了,水印能够很好的保护知识产权。
慢慢地路就走偏了,开始对它的水印感兴趣了。不禁发现之前只是大概知道水印的生成方法,但是从来没有仔细研究过,本文将以以下的路线进行讲解。以下所有代码示例均在
https://github.com/hua1995116/node-demo/tree/master/watermark
水印(watermark)是一种容易识别、被夹于纸内,能够透过光线穿过从而显现出各种不同阴影的技术。
水印的类型有很多,有一些是整图覆盖在图层上的水印,还有一些是在角落。
那么这个水印怎么实现呢?熟悉 PS 的朋友,都知道 PS 有个叫做图层的概念。
网页也是如此。我们可以通过绝对定位,来将水印覆盖到我们的页面之上。
最终变成了这个样子。
等等,但是发现少了点什么。直接覆盖上去,就好像是一个蒙层,我都知道这样是无法触发底下图层的事件的,此时就要介绍一个css属性pointer-events
。
pointer-events
CSS 属性指定在什么情况下 (如果有) 某个特定的图形元素可以成为鼠标事件的 target。
当它的被设置为 none
的时候,能让元素实体虚化,虽然存在这个元素,但是该元素不会触发鼠标事件。详情可以查看 CSS3 pointer-events:none应用举例及扩展 « 张鑫旭-鑫空间-鑫生活 。
这下理清了实现原理,等于成功了一半了!
明水印的生成方式主要可以归为两类,一种是 纯 html 元素(纯div),另一种则为背景图(canvas/svg)。
下面我分别来介绍一下,两种方式。
我们首先来讲比较简单的 div 生成的方式。就按照我们刚才说的。
// 文本内容
<div class="app">
<h1>秋风</h1>
<p>hello</p>
</div>
首先我们来生成一个水印块,就是上面的 一个个秋风的笔记
。这里主要有一点就是设置一个透明度(为了让水印看起来不是那么明显,从而不遮挡我们的主要页面),另一个就是一个旋转,如果是正的水平会显得不是那么好看,最后一点就是使用 userSelect
属性,让此时的文字无法被选中。
userSelect
CSS 属性 user-select
控制用户能否选中文本。除了文本框内,它对被载入为 chrome 的内容没有影响。
function cssHelper(el, prototype) {
for (let i in prototype) {
el.style[i] = prototype[i]
}
}
const item = document.createElement('div')
item.innerHTML = '秋风的笔记'
cssHelper(item, {
position: 'absolute',
top: `50px`,
left: `50px`,
fontSize: `16px`,
color: '#000',
lineHeight: 1.5,
opacity: 0.1,
transform: `rotate(-15deg)`,
transformOrigin: '0 0',
userSelect: 'none',
whiteSpace: 'nowrap',
overflow: 'hidden',
})
有了一个水印片,我们就可以通过计算屏幕的宽高,以及水印的大小来计算我们需要生成的水印个数。
const waterHeight = 100;
const waterWidth = 180;
const { clientWidth, clientHeight } = document.documentElement || document.body;
const column = Math.ceil(clientWidth / waterWidth);
const rows = Math.ceil(clientHeight / waterHeight);
for (let i = 0; i < column * rows; i++) {
const wrap = document.createElement('div');
cssHelper(wrap, Object.create({
position: 'relative',
width: `${waterWidth}px`,
height: `${waterHeight}px`,
flex: `0 0 ${waterWidth}px`,
overflow: 'hidden',
}));
wrap.appendChild(createItem());
waterWrapper.appendChild(wrap)
}
document.body.appendChild(waterWrapper)
这样子我们就完美地实现了上面我们给出的思路的样子啦。
canvas
的实现很简单,主要是利用canvas
绘制一个水印,然后将它转化为 base64 的图片,通过canvas.toDataURL()
来拿到文件流的 url ,关于文件流相关转化可以参考我之前写的文章一文带你层层解锁「文件下载」的奥秘, 然后将获取的 url 填充在一个元素的背景中,然后我们设置背景图片的属性为重复。
.watermark {
position: fixed;
top: 0px;
right: 0px;
bottom: 0px;
left: 0px;
pointer-events: none;
background-repeat: repeat;
}
function createWaterMark() {
const angle = -20;
const txt = '秋风的笔记'
const canvas = document.createElement('canvas');
canvas.width = 180;
canvas.height = 100;
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, 180, 100);
ctx.fillStyle = '#000';
ctx.globalAlpha = 0.1;
ctx.font = `16px serif`
ctx.rotate(Math.PI / 180 * angle);
ctx.fillText(txt, 0, 50);
return canvas.toDataURL();
}
const watermakr = document.createElement('div');
watermakr.className = 'watermark';
watermakr.style.backgroundImage = `url(${createWaterMark()})`
document.body.appendChild(watermakr);
svg 和 canvas 类似,主要还是生成背景图片。
function createWaterMark() {
const svgStr =
`<svg xmlns="http://www.w3.org/2000/svg" width="180px" height="100px">
<text x="0px" y="30px" dy="16px"
text-anchor="start"
stroke="#000"
stroke-opacity="0.1"
fill="none"
transform="rotate(-20)"
font-weight="100"
font-size="16"
>
秋风的笔记
</text>
</svg>`;
return `data:image/svg+xml;base64,${window.btoa(unescape(encodeURIComponent(svgStr)))}`;
}
const watermakr = document.createElement('div');
watermakr.className = 'watermark';
watermakr.style.backgroundImage = `url(${createWaterMark()})`
document.body.appendChild(watermakr);
以上就很快实现了水印的几种方案。但是对于有心之人来说,肯定会想着破解,以上破解也很简单。
打开了 Chrome Devtools
找到对应的元素,直接按 delete
即可删除。
这样子的水印对于大概知道控制台操作的小白就可以轻松破解,那么有什么办法能防御住这样的操作呢?
答案是肯定的,js 有一个方法叫做 MutationObserver
,能够监控元素的改动。
MutationObserver 对现代浏览的兼容性还是不错的,MutationObserver是元素观察器,字面上就可以理解这是用来观察Node(节点)变化的。MutationObserver是在DOM4规范中定义的,它的前身是MutationEvent事件,最低支持版本为 ie9 ,目前已经被弃用。
在这里我们主要观察的有三点
来通过 MDN 查看该方法的使用示例。
const targetNode = document.getElementById('some-id');
// 观察器的配置(需要观察什么变动)
const config = { attributes: true, childList: true, subtree: true };
// 当观察到变动时执行的回调函数
const callback = function(mutationsList, observer) {
// Use traditional 'for loops' for IE 11
for(let mutation of mutationsList) {
if (mutation.type === 'childList') {
console.log('A child node has been added or removed.');
}
else if (mutation.type === 'attributes') {
console.log('The ' + mutation.attributeName + ' attribute was modified.');
}
}
};
// 创建一个观察器实例并传入回调函数
const observer = new MutationObserver(callback);
// 以上述配置开始观察目标节点
observer.observe(targetNode, config);
而MutationObserver
主要是监听子元素的改动,因此我们的监听对象为 document.body
, 一旦监听到我们的水印元素被删除,或者属性修改,我们就重新生成一个。通过以上示例,加上我们的思路,很快我们就写一个监听删除元素的示例。(监听属性修改也是类似就不一一展示了)
// 观察器的配置(需要观察什么变动)
const config = { attributes: true, childList: true, subtree: true };
// 当观察到变动时执行的回调函数
const callback = function (mutationsList, observer) {
// Use traditional 'for loops' for IE 11
for (let mutation of mutationsList) {
mutation.removedNodes.forEach(function (item) {
if (item === watermakr) {
document.body.appendChild(watermakr);
}
});
}
};
// 监听元素
const targetNode = document.body;
// 创建一个观察器实例并传入回调函数
const observer = new MutationObserver(callback);
// 以上述配置开始观察目标节点
observer.observe(targetNode, config);
我们打开控制台来检验一下。
这回完美了,能够完美抵御一些开发小白了。
那么这样就万无一失了吗?显然,道高一尺魔高一丈,毕竟前端的一切都是不安全的。
在一个高级前端工程师面前,一切都是纸老虎。接下来我就随便介绍三种破解的方式。
打开 Chrome Devtools
,点击设置 - Debugger - Disabled JavaScript .
然后再打开页面,delete
我们的水印元素。
复制一个 body 元素,然后将原来 body 元素的删除。
打开一个代理工具,例如 charles
,将生成水印相关的代码删除。
接下来我们实战一下,通过预先分析,我们看到某在线设计网站的内容是以 div 的方式实现的,所以可以利用这种方案。 打开 https://www.gaoding.com/design?id=33931419&simple=1 (仅供举例学习)
打开控制台,Ctrl + F
搜索 watermark
相关字眼。(这一步是作为一个程序员的直觉,基本上你要找什么,搜索对应的英文就可以 ~)
很快我们就找到了水印图。发现直接删除,没有办法删除水印元素,根据我们刚才学习的,肯定是利用了MutationObserver
方法。我们使用我们的第一个破解方法,将 JavaScript 禁用,再将元素删除。
水印已经消失了。
但是这样真的就万事大吉了吗?
不知道你有没有听过一种东西,看不见摸不着,但是它却真实存在,他的名字叫做暗水印,我们将时间倒流到 16 年间的月饼门事件,因为有员工将内网站点截图了,但是很快被定位出是谁截图了。
虽然你将一些可见的水印去除了,但是还会存在一些不可见的保护版权的水印。(这就是防止一些坏人拿去作另外的用途)
暗水印是一种肉眼不可见的水印方式,可以保持图片美观的同时,保护你的资源版权。
暗水印的生成方式有很多,常见的为通过修改RGB 分量值的小量变动、DWT、DCT 和 FFT 等等方法。
通过介绍前端实现 RGB 分量值的小量变动 来揭秘其中的奥秘,主要参考 不能说的秘密——前端也能玩的图片隐写术 | AlloyTeam。
我们都知道图片都是有一个个像素点构成的,每个像素点都是由 RGB 三种元素构成。当我们把其中的一个分量修改,人的肉眼是很难看出其中的变化,甚至是像素眼的设计师也很难分辨出。
你能看出其中的差别吗?根据这个原理,我们就来实践吧。(女孩子可以掌握方法后可以拿以下图片进行试验测试)
首先拿到以上图片,我们先来讲解解码方式,解码其实很简单,我们需要创建一个规律,再通过我们的规律去解码。现在假设的规律为,我们将所有像素的 R 通道的值为奇数的时候我们创建的通道密码,举个简单的例子。
例如我们把以上当做是一个图形,加入他要和一个中文的 "一" 放进图像,例如我们将 "一" 放入第二行。按照我们的算法,我们的图像会变成这个样子。
解码的时候,我们拿到所有的奇数像素将它渲染出来,例如这里的 '5779' 是不是正好是一个 "一",下面就转化为实践。
首先创建一个 canvas
标签。
<canvas id="canvas" width="256" height="256"></canvas>
var ctx = document.getElementById('canvas').getContext('2d');
var img = new Image();
var originalData;
img.onload = function () {
// canvas像素信息
ctx.drawImage(img, 0, 0);
originalData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
console.log()
processData(ctx, originalData)
};
img.src = 'qiufeng-super.png';
我们打印出这个数组,会有一个非常大的数组,一共有 256 256 4 = 262144 个值。因为每个像素除了 RGB 外还有一个 alpha 通道,也就是我们常用的透明度。
上面也说了,我们的 R 通道为奇数的时候 ,就我们的解密密码。因此我们只需要所有的像素点的 R 通道为奇数的时候,将它填填充,不为奇数的时候就不填充,很快我们就能得到我们的隐藏图像。
var processData = function (ctx, originalData) {
var data = originalData.data;
for (var i = 0; i < data.length; i++) {
if (i % 4 == 0) {
// R分量
if (data[i] % 2 == 0) {
data[i] = 0;
} else {
data[i] = 255;
}
} else if (i % 4 == 3) {
// alpha通道不做处理
continue;
} else {
// 关闭其他分量,不关闭也不影响答案
data[i] = 0;
}
}
// 将结果绘制到画布
ctx.putImageData(originalData, 0, 0);
}
processData(ctx, originalData)
解密完会出现类似于以下这个样子。
那我们如何加密的,那就相反的方式就可以啦。(这里都用了 不能说的秘密——前端也能玩的图片隐写术中的例子,= = 我也能写出一个例子,但是觉得没必要,别人已经写得很好了,我们只是讲述这个方法,需要代码来举例而已)
加密呢,首先我们需要获取加密的图像信息。
var textData;
var ctx = document.getElementById('canvas').getContext('2d');
ctx.font = '30px Microsoft Yahei';
ctx.fillText('秋风的笔记', 60, 130);
textData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height).data;
然后提取加密信息在待加密的图片上进行处理。
var mergeData = function (ctx, newData, color, originalData) {
var oData = originalData.data;
var bit, offset; // offset的作用是找到alpha通道值,这里需要大家自己动动脑筋
switch (color) {
case 'R':
bit = 0;
offset = 3;
break;
case 'G':
bit = 1;
offset = 2;
break;
case 'B':
bit = 2;
offset = 1;
break;
}
for (var i = 0; i < oData.length; i++) {
if (i % 4 == bit) {
// 只处理目标通道
if (newData[i + offset] === 0 && (oData[i] % 2 === 1)) {
// 没有信息的像素,该通道最低位置0,但不要越界
if (oData[i] === 255) {
oData[i]--;
} else {
oData[i]++;
}
} else if (newData[i + offset] !== 0 && (oData[i] % 2 === 0)) {
// // 有信息的像素,该通道最低位置1,可以想想上面的斑点效果是怎么实现的
oData[i]++;
}
}
}
ctx.putImageData(originalData, 0, 0);
}
主要的思路还是我一开始所讲的,在有像素信息的点,将 R 偶数的通道+1。在没有像素点的地方将 R 通道转化成偶数,最后在 img.onload
调用 processData(ctx, originalData)
。
img.onload = function () {
// 获取指定区域的canvas像素信息
ctx.drawImage(img, 0, 0);
originalData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
console.log(originalData)
processData(ctx, originalData)
};
以上方法就是一种比较简单的加密方式。以上代码都放到了仓库 watermark/demo/canvas-dark-watermark.html
路径下,方法都封装好了~。
但是实际过程需要更专业的加密方式,例如利用傅里叶变化公式,来进行频域制定数字盲水印,这里就不详细展开讲了,以后研究完再详细讲~
听完上述的介绍,那么某在线设计网站是不是很有可能使用了暗水印呢?
当然啦,通过我对某在线设计网站的分析,我分析了以下几种情况,我们一一来进行测试。
我们先通过免费下载的图片来进行分析。打开 https://www.gaoding.com/design?id=13964513159025728&mode=user
通过实验(实验主要是去分析他各个场景下触发的请求),发现在下载免费图片的时候,发现它都会去向阿里云发送一个 POST 请求,这熟悉的请求域名以及熟悉的数据封装方式,这不就是 阿里云 OSS 客户端上传方式嘛。这就好办了,我们去查询一下阿里云是否有生成暗水印的相关方式,从而来看看某在线设计网站是否含有暗水印。很快我们就从官方文档搜索到了相关的文档,且对于低 QPS 是免费的。(这就是最好理解的连带效应,例如我们觉得耐克阿迪啥卖运动类服饰,你买了他的鞋子,可能还会想买他的衣服)
const { RPCClient } = require("@alicloud/pop-core");
var client = new RPCClient({
endpoint: "http://imm.cn-shenzhen.aliyuncs.com",
accessKeyId: 'xxx',
accessKeySecret: 'xxx',
apiVersion: "2017-09-06",
});
(async () => {
try {
var params = {
Project: "test-project",
ImageUri: "oss://watermark-shenzheng/source/20201009-182331-fd5a.png",
TargetUri: "oss://watermark-shenzheng/dist/20201009-182331-fd5a-out.jpg",
Model: "DWT"
};
var result = await client.request("DecodeBlindWatermark", params);
console.log(result);
} catch (err) {
console.log(err);
}
})()
我们写了一个demo进行了测试。由于阿里云含有多种暗水印加密方式,为啥我使用了 DWT
呢?因为其他几种都需要原图,而我们刚才的测试,他上传只会上传一个文件到 OSS ,因此大致上排除了需要原图的方案。
但是我们的结果却没有发现任何加密的迹象。
为什么我们会去猜想阿里云的图片暗水印的方式?因为从上传的角度来考虑,我们上传的图片 key 的地址即是我们下载的图片,也就是现在存在两种情况,一就是通过阿里云的盲水印方案,另一种就是上传前进行了水印的植入。现在看来不是阿里云水印的方案,那么只是能是上传前就有了水印。
这个过程就有两种情况,一是生成的过程中加入的水印,前端加入的水印。二是物料图含有水印。
对于第一种情况,我们可以通过 dom-to-image
这个库,在前端直接进行下载,或者使用截图的方式。目前通过直接下载和通过站点内生成,发现元素略有不同。
第一个为我通过 dom-to-image
的方式下载,第二种为站点内下载,明显大了一些。(有点怀疑他在图片生成中可能做了什么手脚)
但是感觉前端加密的方式比较容易破解,最坏的情况想到了对素材进行了加密,但是这样的话就无从破解了(但是查阅了一些资料,由于某在线设计网站站点素材大多是透明背景的,这种加密效果可能会弱一些,以后牛逼了再来补充)。目前这一块暂时还不清楚,探究止步于此了。
那如果一张图经过暗水印加密,他的抵抗攻击性又是如何呢?
这是一张通过阿里云 DWT
暗水印进行的加密,解密后的样子为"秋风"字样,我们分别来测试一下。
结果: 识别效果不错
结果: 识别效果不错
结果:识别效果不错
结果: 直接就拉胯了。
可见,暗水印的抵抗攻击性还是蛮强的,是一种比较好的抵御攻击的方式~
以上仅仅为技术交流~ 大家不要在实际的场景盲目使用,商业项目违规使用后果自负 ~ 或者期待一下我接下来想搞的这个个人免费首图生成器~ 喜欢文章的小伙伴可以点个赞哦 ~ 欢迎关注公众号 秋风的笔记 ,学习前端不迷路。
https://help.aliyun.com/document_detail/138800.html?spm=a2c4g.11186623.6.656.3bd46bb4oglhEr
https://oss.console.aliyun.com/bucket/oss-cn-shenzhen/watermark-shenzheng/object?path=dist%2F
https://juejin.cn/post/6844903650054111246
https://www.zhihu.com/question/50677827/answer/122388524
https://www.zhihu.com/question/50735753
如果我的文章有帮助到你,希望你也能帮助我,欢迎关注我的微信公众号 秋风的笔记
,回复好友
二次,可加微信并且加入交流群,秋风的笔记
将一直陪伴你的左右。
最近在写公众号的时候,常常会自己做首图,并且慢慢地发现沉迷于制作首图,感觉扁平化的设计的真好好看。慢慢地萌生了一个做一个属于自己的首图生成器的想法。
LIXINCHAO 赞了文章 · 2020-12-04
本文永久地址: 大厂面经大全,将会持续更新和补充
欢迎关注公众号【互联网大厂招聘】,作者将在公众号里持续推送各个大厂的招聘职位及要求,并与大厂面试官以及招聘负责人直通,感兴趣的可以直接与负责人交流。
另外,作者也将持续推送优质的大厂面试经验,各种大厂独家面试题以及优秀文章分享,不限于前端,后端,运维和系统设计。
扫码添加我的微信,一定要备注面试,进入面试群,或者与我交流。
本文永久地址: 大厂面经大全,将会持续更新和补充 计算机基础题总结 前端面试题总结 各大厂 “寒冬”三年经验前端面试总结(含头条、百度、饿了么、滴滴等) 金三银四魔都两年半前端面经 (拼多读、猫眼、B站) 面试分享:两年工作经验成功面试阿里P6总结 (阿里、滴滴) ...
赞 144 收藏 112 评论 0
查看全部 个人动态 →
(゚∀゚ )
暂时没有
注册于 2018-11-21
个人主页被 445 人浏览
推荐关注