2020年工作总结
小程序
我的小程序常见bug就是code失效的问题,做法是code使用过后重新生成新的code,其实就是刷新sessionKey,所以后端维护session就变得很难,因此就直接传入code就好了。但是新生成的code容易失效(大概3分钟左右),在使用之前就已失效,所以是我的常见bug。
大亲家红娘系列
很简单的一系列小程序
三分钟约会
主要功能:实现在线匹配和在线实时聊天,以及聊天倒计时和结束时状态判断。
难点:IOS和Android的息屏和亮屏状态不一致,对于websocket连接需要进行区别处理,以及匹配动画和聊天。
总结在另外一篇:微信小程序踩坑记-1
朋友星球
主要功能:实现六角星的动画效果。
难点:需要头像点击,感觉没有什么好的想法,就只有CSS3样式 + JS控制了。
优化思想:海报生成一次之后就缓存起来,然后点击再次生成的时候就可以用之前生成过的了。但是根据需求来。将两个海报生成函数封装起来了,每次根据参数的不同而生成不同的海报效果。
import wepy from '@wepy/core'
import { mapGetters } from '@wepy/x'
import store from '../store'
import apis from '../utils/apis'
import { showToast, timeout2Promise } from '../utils/util'
const POSITIONS = [ 'topRight', 'topLeft', 'centerLeft', 'bottomLeft', 'bottomRight' ]
wepy.component({
store,
props: {
userInfo: {
type: Object,
required: true
}
},
data: {
totalPages: 1,
page: 1,
isInit: false, // 避免每次修改userinfo触发获取朋友列表
members: [],
friends: {},
allSmallFriends: {},
smallFriends: {},
clickFriend: '',
lineTransition: '',
hexagonSmallPosition: {},
friendChangeTransform: '',
friendChangeImgStyle: '',
friendStyle: {},
centerAvatar: '',
showCircleAni: false,
showInviteTip: true,
inviteTipAni: 'fade-in',
showSmallFriend: false,
smallMyPos: '',
clicking: false // 记录点击状态
},
computed: {
...mapGetters([ 'appId' ]),
},
watch: {
userInfo(newVal) {
if (newVal.is_has_union_id === 1 && !!newVal.wx_avatar_url) {
if (!this.isInit) {
this.isInit = true
this.centerAvatar = newVal.wx_avatar_url
this.getGroupFriends()
}
}
}
},
created() {
this.initSmallPosition()
},
methods: {
initSmallPosition() {
this.showCircleAni = false // 隐藏中心背景动效
const y1 = 246
const y2 = 300
const x1 = 216
const x2 = 176
const x3 = 106
const smallPosition = {}
smallPosition.topRight = [
`transform: translate(${x1}rpx, -${y1}rpx)`,
`transform: translate(${x2}rpx, -${y2}rpx)`,
`transform: translate(${x3}rpx, -${y2}rpx)`
]
smallPosition.topLeft = [
`transform: translate(-${x3}rpx, -${y2}rpx)`,
`transform: translate(-${x2}rpx, -${y2}rpx)`,
`transform: translate(-${x1}rpx, -${y1}rpx)`
]
smallPosition.centerLeft = [
`transform: translate(-320rpx, -60rpx)`,
`transform: translate(-350rpx)`,
`transform: translate(-320rpx, 60rpx)`
]
smallPosition.centerRight = [
`transform: translate(320rpx, -60rpx)`,
`transform: translate(350rpx)`,
`transform: translate(320rpx, 60rpx)`
]
smallPosition.bottomRight = [
`transform: translate(${x3}rpx, ${y2}rpx)`,
`transform: translate(${x1}rpx, ${y1}rpx)`,
`transform: translate(${x2}rpx, ${y2}rpx)`
]
smallPosition.bottomLeft = [
`transform: translate(-${x1}rpx, ${y1}rpx)`,
`transform: translate(-${x2}rpx, ${y2}rpx)`,
`transform: translate(-${x3}rpx, ${y2}rpx)`
]
this.hexagonSmallPosition = smallPosition
},
getGroupFriends(page = 1) {
apis.getGroupFriends({
page
}).then(res => {
const { total, members } = res.data
this.page = page
this.totalPages = total
let friends = {}
let smallFriends = {}
for (let i = 0, len = members.length; i < len; i++) {
friends[POSITIONS[i]] = members[i].member_avatar_url
smallFriends[POSITIONS[i]] = members[i].members.slice(0, 3)
}
if (page < total || total > 1) { // 显示换一批
friends.bottomRight = 'nextPage'
}
this.members = members
this.friends = friends
this.allSmallFriends = smallFriends
this.smallFriends = smallFriends
this.$emit('load')
this.clicking = false
}).catch(() => {
this.clicking = false
})
},
onNextPage() {
if (this.clicking) return
if (this.page < this.totalPages) {
this.getGroupFriends(this.page + 1)
} else {
showToast({
title: '已无更多单身团成员,请邀请单身好友加入'
})
this.getGroupFriends(1)
}
},
onClickSmallImg() {
if (this.showSmallFriend) return
showToast({
title: '直接联系朋友的朋友的功能暂未开放'
})
},
onClickFriendImg(pos) {
if (this.clicking) return
this.clicking = true
if (this.showSmallFriend) {
if (pos === this.smallMyPos) {
// 在朋友的朋友状态下 点击我的头像
this.showChangeAnimation(pos, 'my')
} else {
showToast({
title: '直接联系朋友的朋友的功能暂未开放'
})
this.clicking = false
return
}
} else {
this.inviteTipAni = 'fade-out'
timeout2Promise(100).then(() => {
this.showInviteTip = false
})
this.showChangeAnimation(pos, 'friend')
}
},
showChangeAnimation(pos, type) {
this.clickFriend = pos
const index = POSITIONS.indexOf(pos)
timeout2Promise(300).then(() => {
// 收缩虚线
this.lineTransition = 'line-shrink'
return timeout2Promise(300)
}).then(() => {
this.hideSmallPosition()
return timeout2Promise(300)
}).then(() => {
this.changeCenterImg(pos)
return timeout2Promise(300)
}).then(() => {
// 收缩朋友图片
const style = 'transform: translate(0, 0)'
this.friendStyle = {
centerRight: style,
topRight: style,
topLeft: style,
centerLeft: style,
bottomLeft: style,
bottomRight: style
}
return timeout2Promise(300)
}).then(() => {
this.handleChangeAnimate(type, index)
return timeout2Promise(300)
}).then(() => {
// 放射虚线
this.lineTransition = ''
return timeout2Promise(400)
}).then(() => {
this.friendStyle = {
centerRight: '',
topRight: 'transition-delay: 50ms',
topLeft: 'transition-delay: 100ms',
centerLeft: 'transition-delay: 150ms',
bottomLeft: 'transition-delay: 200ms',
bottomRight: 'transition-delay: 250ms',
}
if (type === 'my') {
this.smallFriends = this.allSmallFriends
timeout2Promise(500).then(() => {
this.initSmallPosition()
return timeout2Promise(300)
}).then(() => {
this.inviteTipAni = 'fade-in'
this.showInviteTip = true
})
} else {
const smallFriends = {}
smallFriends[this.smallMyPos] = this.members.slice(0, 3)
this.smallFriends = smallFriends
timeout2Promise(500).then(() => {
this.initSmallPosition()
})
}
this.clicking = false
})
},
handleChangeAnimate(type, index) {
// 更换为动态背景图
this.showCircleAni = true
// 初始化更换的效果
this.clickFriend = ''
this.friendChangeTransform = ''
this.friendChangeImgStyle = ''
if (type === 'friend') {
const supFriend = this.members[index]
const subFriend = supFriend.members
// 更换中心图片
this.centerAvatar = supFriend.member_avatar_url
// 展示朋友的朋友的图片 更换朋友图片
let friends = {}
const len = subFriend.length > 5 ? 5 : subFriend.length // 确保即使朋友的朋友返回的是大于4个的 最后一个位置还是我的
const position = ['centerRight', ...POSITIONS]
for (let i = 0; i < len; i++) {
friends[position[i]] = subFriend[i].member_avatar_url
}
const myPos = position[len] // ? len : 1 避免第一个centerRight
friends[myPos] = this.userInfo.wx_avatar_url // 自己的头像 附在最后一个位置上
this.showSmallFriend = true
this.friends = friends
this.smallMyPos = myPos
this.$emit('small', true)
} else {
// 更换中心图片
this.centerAvatar = this.userInfo.wx_avatar_url
// 展示朋友的图片
let friends = {}
const len = this.members.length
for (let i = 0; i < len; i++) {
friends[POSITIONS[i]] = this.members[i].member_avatar_url
}
if (this.page < this.totalPages || this.totalPages > 1) { // 显示换一批
friends.bottomRight = 'nextPage'
}
this.friends = friends
this.smallMyPos = ''
this.showSmallFriend = false
this.$emit('small', false)
}
},
hideSmallPosition() {
const top = -212
const bottom = 206
const left = -126
const right = 136
const topRight = `transform: translate(${right}rpx, ${top}rpx); opacity: 0`
const topLeft = `transform: translate(${left}rpx, ${top}rpx); opacity: 0`
const centerLeft = `transform: translate(-240rpx); opacity: 0`
const centerRight = `transform: translate(240rpx); opacity: 0`
const bottomRight = `transform: translate(${right}rpx, ${bottom}rpx); opacity: 0`
const bottomLeft = `transform: translate(${left}rpx, ${bottom}rpx); opacity: 0`
const smallPosition = {
topRight: new Array(3).fill(topRight),
topLeft: new Array(3).fill(topLeft),
centerLeft: new Array(3).fill(centerLeft),
centerRight: new Array(3).fill(centerRight),
bottomRight: new Array(3).fill(bottomRight),
bottomLeft: new Array(3).fill(bottomLeft)
}
this.hexagonSmallPosition = smallPosition
},
changeCenterImg(pos) {
this.friendChangeImgStyle = 'width: 136rpx; height: 136rpx; border: none;'
this.friendChangeTransform = pos
},
onShare() {
if (this.clicking) {
return
}
this.$emit('share')
}
}
})
</script>
VUE 全家桶
CRM
主要职责:搭建基础组件框架,项目从无到有,从 0 到 1。
负责部分:
- 基础组件框架包括页头、菜单、面包屑、弹窗等。
- 权限管理部分,axios拦截和路由前置。
- 文档管理,创建并编写公共部分配置文档。
步骤:
- 确定技术选型
- 搭建项目结构(组织框架)
- 搭建样式结构,样式标准化,确定主题颜色、字体等全局元素,定义常量文件。
- 封装基础内容,如网络请求、路由模块、store模块、权限校验、常量数据(角色常量、错误码)
- 搭建基础框架,构建通用组件。大致布局包括左侧菜单栏、顶部信息展示、底部信息,留出中间部分作为内容模块
- 构建基础页面,如登录页面、404页面等
- 每个步骤都需要文档编写,后续根据UI和业务需求,合理划分组件,公用组件和业务模块内部组件
- 前期可能会避免过度设计,根据业务需求需要持续精益划分
权限管理
页面级权限管理
- 在 vue-router 的 meta 路由元信息属性中配置自定义属性 premise ,设置权限数组。
{
path: 'clue',
name: 'Clue',
component: () => import('@/views/saleOcean/Qcard'),
meta: {
title: '相亲名片',
type: 'tab',
// keepAlive: true,
permission: [ADMIN, OPERATOR, STOREMANAGER, EXPERIENCE]
}
},
路由挂载,虽然是按照权限分配,但是所有页面路由都会挂在到前端。
用户登录后获取的用户权限信息存储在store中,根据用户权限信息通过路由元信息中的promise属性过滤路由数组生成权限路由。
import { asyncRouterMap, constantRouterMap } from '@/router'
import {
ADMIN,
OPERATOR,
STOREMANAGER,
SALER,
EXPERIENCE
} from '@/configs/enum'
/**
* 单页面多角色时,过滤页面
*
* @param role
* @param route
* @returns {*}
*/
function hasPermission(role, route) {
if (route.meta && route.meta.permission) {
return route.meta.permission.includes(role)
} else {
return true
}
}
function filterAsyncRouter(routerMap, role, filterPageName) {
const pageList = []
let list = []
const accessedRouters = routerMap.filter(route => {
if (hasPermission(role, route) && !filterPageName.includes(route.name)) {
if (route.children && route.children.length) {
[route.children, list] = filterAsyncRouter(route.children, role, filterPageName)
pageList.push(...list)
}
pageList.push(route.name)
return true
}
return false
})
return [accessedRouters, pageList]
}
const permission = {
state: {
routers: constantRouterMap,
addRouters: [],
pageList: [],
isAdmin: false,
isOperator: false,
isStoreManager: false,
isSaler: false,
isExperience: false,
isAdminOrOperator: false,
isStoreOrSaler: false
},
mutations: {
SET_ROUTERS: (state, routers) => {
state.addRouters = routers
state.routers = constantRouterMap.concat(routers)
},
SET_PAGELIST: (state, pages) => {
state.pageList = pages
},
SET_ROLE: (state, role) => {
switch (Number(role)) {
case ADMIN:
state.isAdmin = true
state.isAdminOrOperator = true
break
case OPERATOR:
state.isOperator = true
state.isAdminOrOperator = true
break
case STOREMANAGER:
state.isStoreManager = true
state.isStoreOrSaler = true
break
case SALER:
state.isSaler = true
state.isStoreOrSaler = true
break
case EXPERIENCE:
state.isExperience = true
break
default:
break
}
}
},
actions: {
GenerateRoutes({ commit, getters: { userInfo } }, role) {
return new Promise((resolve, reject) => {
if (role) {
const filterPageName = ![ADMIN, OPERATOR].includes(role) && userInfo.virtual_number_status !== 1 ? ['Call'] : []
const [accessedRouters, pageList] = filterAsyncRouter(asyncRouterMap, role, filterPageName)
commit('SET_ROUTERS', accessedRouters)
commit('SET_PAGELIST', pageList)
commit('SET_ROLE', role)
resolve([accessedRouters, pageList])
} else {
reject()
}
})
}
}
}
export default permission
设置路由全局前置守卫 beforeEach,可以改变导航本身,守卫是异步解析执行,此时导航在所有守卫 resolve 完之前一直处于 等待中。
所以在此时校验当前用户的权限,从store中获取权限路由,进行判断当前路由页面是否存在权限路由中,如果不存在则返回登录界面或404界面。
import router from './router'
import store from './store'
import NProgress from 'nprogress' // progress bar
import 'nprogress/nprogress.css' // progress bar style
import notification from 'ant-design-vue/es/notification'
import {
getUserInfoByStorage
} from '@/store/storage'
NProgress.configure({ showSpinner: false }) // NProgress Configuration
const whiteList = ['login']
const relogin = (next) => {
notification.error({
message: '错误',
description: '请重新登录'
})
next({ path: '/login' })
}
router.beforeEach((to, from, next) => {
NProgress.start() // start progress bar
if (to.path === '/login' || whiteList.includes(to.name)) { // 在免登录白名单,直接进入
NProgress.done()
next()
} else {
let { addRouters, userInfo, pageList } = store.getters
if (Object.keys(userInfo).length === 0) { // 避免刷新页面时vuex重置后userinfo为空, 刷新后从storage中获取userInfo
userInfo = getUserInfoByStorage()
userInfo.token && store.commit('SET_ACCESS_TOKEN', userInfo.token)
store.commit('SET_USERINFO', userInfo)
}
if (addRouters.length === 0) {
if (userInfo && userInfo.role) { // storage中没有userinfo
store.dispatch('GenerateRoutes', userInfo.role).then(([, list]) => {
if (list.includes(to.name)) {
next()
} else { // 没有下一个页面的权限
notification.error({
message: '错误',
description: '没有权限, 请重新登录'
})
relogin(next)
}
}).catch(() => {
// role is undefined
relogin(next)
})
} else {
next({ path: '/login' })
}
} else {
// 在已登录情况下跳转到不属于该用户的页面,返回到登录页面
pageList.includes(to.name) ? next() : relogin(next)
}
}
})
router.afterEach(() => {
NProgress.done() // finish progress bar
})
后台管理系统
权限可交互树状图
主要加了D3库进行展示树形结构。涉及到简单的svg
。
SVG:
- 所有 SVG 显示属性都可以作为 CSS 属性来使用。
- SVG 有自己的事件属性,可以在元素上绑定事件进行事件响应。实现SVG可交互动画效果。
- SVG 有相关的 DOM api,可以进行一系列元素选择和操作。
<template>
<div class="tree-container" :id="id">
<svg class="d3-tree">
<g class="container"></g>
</svg>
</div>
</template>
<script>
import * as d3 from 'd3'
export default {
name: 'MindMap',
props: {
dataset: {
type: Object,
default() {
return {}
}
}
},
data() {
return {
id: '',
zoom: null,
index: 0,
duration: 750,
root: null,
nodes: [],
links: [],
dTreeData: null,
transform: null,
margin: {
top: 20,
right: 90,
bottom: 30,
left: 90
}
}
},
created() {
this.id = this.uuid()
},
mounted() {
//创建svg画布
this.width = document.getElementById(this.id).clientWidth
this.height = document.getElementById(this.id).clientHeight
// $el提供一个在页面上已存在的 DOM 元素作为 Vue 实例的挂载目标
const svg = d3.select(this.$el).select('svg.d3-tree')
.attr('width', this.width) // 设置属性
.attr('height', this.height)
const transform = d3.zoomIdentity.translate(this.margin.left, this.margin.top).scale(1) // 恒等变换
const container = svg.select('g.container')
// init zoom behavior, which is both an object and function
this.zoom = d3.zoom() // 移动缩放
.scaleExtent([1 / 2, 8]) // 可缩放范围
.on('zoom', this.zoomed) // 绑定缩放动作监听
container.transition().duration(this.duration).call(this.zoom.transform, transform) // 平滑的重置缩放(750ms)
svg.call(this.zoom) // 重置缩放变换
this.root = this.getRoot()
this.update(this.root)
},
computed: {
treemap() {
// 创建一个新的整齐(同深度节点对齐)的树布局
return d3.tree()
.size([this.height, this.width])
}
},
methods: {
uuid() {
function s4() {
return Math.floor((1 + Math.random()) * 0x10000)
.toString(16)
.substring(1)
}
return (
s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4()
)
},
/**
* @description 获取构造根节点,先遍历数据赋值每个节点属性返回一整棵树
*/
getRoot() {
let root = d3.hierarchy(this.dataset, d => { // 根据指定的层次结构数据构造一个根节点,设置value、children、parent、height、depth、data
return d.children // 子元素
})
const multiple = Math.floor(root.leaves().length / 20) || 1
this.height = this.height * multiple
root.x0 = this.height / 2 // 容器中心点,x为纵向
root.y0 = 0
return root
},
/**
* @description 点击节点进行展开、收缩
*/
clickNode(d) {
if (!d._children && !d.children)
return
if (d.children) {
this.$set(d, '_children', d.children)
d.children = null
} else {
this.$set(d, 'children', d._children)
d._children = null
}
this.$nextTick(
() => {
this.update(d)
}
)
},
diagonal(s, d) {
return `M ${s.y} ${s.x}
C ${(s.y + d.y) / 2} ${s.x},
${(s.y + d.y) / 2} ${d.x},
${d.y} ${d.x}`
},
/**
* @description 获取构造的node数据和link数据, 构造每个节点的x,y值
*/
getNodesAndLinks() {
// treemap generate new x、y coordinate according to root node,
// so can‘t use computed propter of vue
this.dTreeData = this.treemap(this.root)
this.nodes = this.dTreeData.descendants() // 返回后代节点数组,第一个节点为自身,然后依次为所有子节点的拓扑排序。
this.links = this.dTreeData.descendants().slice(1)
},
/**
* @description 数据与Dom进行绑定
*/
update(source) {
this.getNodesAndLinks()
this.nodes.forEach(d => { // 遍历每个点,重定义y值=深度(depth)*180, 即横向偏移量
d.y = d.depth * 180
})
// *************************** Nodes section *************************** //
// Update the nodes...
const svg = d3.select(this.$el).select('svg.d3-tree')
const container = svg.select('g.container')
let node = container.selectAll('g.node') // 选择所有与指定的 selector 匹配的元素。
.data(this.nodes, d => { // 将数据集传递给子元素,设置子元素的id
return d.id || (d.id = ++this.index)
})
// Enter any new sources at the parent's previous position.
let nodeEnter = node.enter().append('g') // 添加子节点
.attr('class', 'node')
.on('click', this.clickNode)
.attr('transform', d => {
return 'translate(' + source.y0 + ',' + source.x0 + ')'
})
nodeEnter.append('circle') // 每个子节点中包含的元素
.attr('r', 8)
.style('fill', function (d) {
let color = d.data.color || '#fff'
return d.children || d._children ? '#40A9FF' : color;
});
nodeEnter.append('text')
.attr('x', function (d) {
return d.children || d._children ? -15 : 15;
})
.attr('dy', '.35em') // 文字的偏移量
.attr('text-anchor', function (d) {
return d.children || d._children ? 'end' : 'start';
})
.text(function (d) {
return d.data.name
})
.style('fill-opacity', 0);
// Transition nodes to their new position.
// 这个方法通常用来在 data-join 操作之后合并 enter 和 update。在单独修改输入和更新元素之后对其进行合并执行其他的操作而不需要额外的代码。
let nodeUpdate = nodeEnter.merge(node) // 返回一个将当前选择集和指定的 other 选择集合并之后的新的选择集
.transition()
.duration(this.duration)
.attr('transform', function (d) {
return 'translate(' + d.y + ',' + d.x + ')';
});
nodeUpdate.select('circle')
.attr('r', 8)
.attr('stroke', '#40A9FF')
.attr('stroke-width', 1)
.style('fill', function (d) {
let color = d.data.color || '#fff'
return d.children || d._children ? '#40A9FF' : color;
});
nodeUpdate.select('text')
.style('fill-opacity', 1);
// Transition exiting nodes to the parent's new position.
let nodeExit = node.exit()
.transition()
.duration(this.duration)
.attr('transform', function (d) {
return 'translate(' + source.y + ',' + source.x + ')';
})
.remove();
nodeExit.select('circle')
.attr('r', 0);
nodeExit.select('text')
.style('fill-opacity', 0);
// *************************** Links section *************************** //
// Update the links…
let link = container.selectAll('path.link')
.data(this.links, d => {
return d.id
})
// Enter any new links at the parent's previous position.
let linkEnter = link.enter().insert('path', 'g')
.attr("class", "link")
.attr('d', d => {
let o = {
x: source.x0,
y: source.y0
};
return this.diagonal(o, o)
})
.attr('fill', 'none')
.attr('stroke-width', 1)
.attr('stroke', '#999')
// Transition links to their new position.
let linkUpdate = linkEnter.merge(link)
linkUpdate.transition()
.duration(this.duration)
.attr('d', d => {
return this.diagonal(d, d.parent) // link的两端
})
// Transition exiting nodes to the parent's new position.
link.exit().transition()
.duration(this.duration)
.attr('d', d => {
let o = {
x: source.x,
y: source.y
};
return this.diagonal(o, o)
})
.remove();
// Stash the old positions for transition.
this.nodes.forEach(d => {
d.x0 = d.x
d.y0 = d.y
})
},
/**
* @description control the canvas zoom to up or down
*/
zoomed() {
d3.select(this.$el).select('g.container').attr('transform', d3.event.transform)
}
}
}
</script>
<style lang='less' scoped>
.tree-container {
width: 100%;
height: 600px;
}
.d3-tree {
.node {
cursor: pointer;
}
.node text {
font: 18px;
}
}
</style>
权限控制
按钮级别的权限控制。每个用户拥有自己角色的权限同时拥有自己特有的权限。
后端存储角色按钮控制权限和用户的按钮控制权限。
每个按钮都有唯一的key值。前端保存页面和按钮的映射表。
用户登录后从后端获取的权限列表,从按钮key值通过映射表倒推到页面路由,获取权限路由数组。
这里会比单纯的页面级权限控制要复杂一些。
前端会挂载所有权限的路由,但是通过全局路由守卫 beforeEnter 控制导航,每次导航都需要检测是否在权限路由数组中。
如果权限不足,则跳转到登录页面或当前权限路由的第一页。
用户手动刷新页面时会导致store数据清空,则需要部分 store 数据做加密持久化存储。
后台管理系统优化方向
- 组件懒加载
- 图片懒加载
- 使用
v-once
加载静态组件 - webpack开启tree-shaking、使用DII方式打包antd(全包导入未使用懒加载)相关包
官网
大亲家系列网站
前期使用html + css + jQuery改写其他公司官网,直接 down 的其他公司的网站修改的。所以需要快速开发出来,就暂时在旧的页面上使用。
后期使用webpack + pug进行修改。可以设置 babel 的 polyfill 和 使用 less 以及模板。将页面按照功能模块分为页头、侧边栏、弹窗、固定页尾、页尾。
后期修改优势:
可以减少对低兼容的忌讳,使用 babel 进行补充polyfill。
使用less,对 css 进行合理的拆分和通用样式。
降低维护成本,如需要改动关键字,第一版传统写法则需要搜索出所有的文档进行改动,使用模板引擎能够减少改动部分,方便维护。
使用 tree-shaking 可以减少代码冗余。