1.Vue错误监听
window.onerror
- 可以监听当前页面所有的 JS 报错,jQuery 时代经常用,全局绑定一次即可
// try-catch捕获后无法被 window.onerror 捕获
window.onerror = function(msg, source, line, column, error) {
console.log('window.onerror---------', msg, source, line, column, error)
}
// 注意,如果用 window.addEventListener('error', event => {}) 参数不一样!!!
复制代码
errorCaptured 生命周期
- 会监听所有下级组件的错误,可以返回 false 阻止向上传播,因为可能会有多个上级节点都监听错误
errorCaptured(error, instance, info) {
console.log('errorCaptured--------', error, instance, info)
}
复制代码
errorHandler 全局的错误监听
- 所有组件的报错都会汇总到这里来,如果 errorCaptured 返回 false 则不会到这里
const app = createApp(App)
app.config.errorHandler = (error, instance, info) => {
console.log('errorHandler--------', error, instance, info)
}
复制代码
注意:errorHandler 和 warnHandler 会阻止错误走向 window.onerror
异步错误
- 组件内的异步错误 errorHandler 监听不到,还是需要 window.onerror
mounted() {
setTimeout(() => {
throw new Error('setTimeout 报错')
}, 1000)
},
复制代码
总结
方式
errorCaptured
监听下级组件的错误,可返回false
阻止向上传播errorHandler
监听 Vue 全局错误window.onerror
监听其他的 JS 错误,如异步
建议:结合使用
- 一些重要的、复杂的、有运行风险的组件,可使用
errorCaptured
重点监听 - 然后用
errorHandler
window.onerror
候补全局监听,避免意外情况
2.React错误监听
ErrorBoundary
- 可以监听所有下级组件报错 ,不监听事件和异步错误,建议应用到最顶层,监听全局错误,需要打包后可以看到报错 UI 效果
- 错误边界涉及到两个生命周期函数,分别为 getDerivedStateFromError 和 componentDidCatch
- getDerivedStateFromError 为静态方法,方法中需要返回一个对象,该对象会和state对象进行合并用于更改应用程序状态
- componentDidCatch 方法用于记录应用程序错误信息,该方法的参数就是错误对象
// index.js 入口文件
ReactDOM.render(
<React.StrictMode>
<ErrorBoundary>
<App />
</ErrorBoundary>
</React.StrictMode>,
document.getElementById('root')
);
复制代码
函数组件中也可以使用:
function App(props) {
return <ErrorBoundary>
{props.children}
</ErrorBoundary>
}
复制代码
// ErrorBoundaries.js
import React from "react";
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = {
error: null, // 存储当前的报错信息
};
}
static getDerivedStateFromError(error) {
// 更新 state 使下一次渲染能够显示降级后的 UI
console.info("getDerivedStateFromError...", error);
return { error };
}
componentDidCatch(error, errorInfo) {
// 统计上报错误信息
console.info("componentDidCatch...", error, errorInfo);
}
render() {
if (this.state.error) {
// 提示错误
return <h1>报错了</h1>;
}
// 没有错误,就渲染子组件
return this.props.children;
}
}
export default ErrorBoundary;
复制代码
异步错误
ErrorBoundary 无法捕捉到事件和异步报错,可使用 window.onerror
来监听,也可 try-catch
window.onerror = function(msg, source, line, column, error) {
console.log('window.onerror---------', msg, source, line, column, error)
}
// 注意,如果用 window.addEventListener('error', event => {}) 参数不一样!!!
复制代码
总结
ErrorBoundary
监听渲染时报错try-catch
和window.onerror
捕获其他错误
扩展
Promise 监听报错要使用 window.onunhandledrejection
前端拿到错误监听之后,需要传递给服务端,进行错误收集和分析,然后修复 bug
3.Vue性能优化和遇到的坑
- 优化方面可以看我这篇文章:Vue性能优化 – 掘金 (juejin.cn)
坑:
- 全局事件,定时器没有即使清除,会有内存泄漏风险
- Vue2.x 中,无法监听 data 属性的新增和删除,以及数组的部分修改,所以有 Vue.set、Vue.delete
- 路由切换时,页面自动会 scroll 到顶部,缓存数据和
scrollTop
,返回当前页使用缓存渲染页面到之前的位置
4.React性能优化和遇到的坑
- 循环使用 key
- 修改 css 模拟
v-show
- 使用 Fragment 减少层级
- JSX 中不要定义函数
- 在构造函数 bind this
- 使用 shouldComponentUpdate 控制组件渲染
- React.memo 缓存函数组件
- useMemo 缓存数据
- 异步组件
- 路由懒加载
- SSR
更详细的可以看我这篇文章:React 性能优化最佳实践 – 掘金 (juejin.cn)
坑:
- 自定义组件命名,开头字母要大写,html 标签开头字母小写
- JSX 中 属性
for
写成htmlFor
,class
写成className
- state 作为不可变数据,不可直接修改,使用纯函数
- JSX 中,属性要通过
{}
区分 JS 表达式和字符串 - setState 是异步更新的,要在第二个参数 callback 中拿到最新的 state 值
- React Hooks 有很多限制,例如,
useEffect
依赖项(即第二个参数)里有对象、数组,就会出现死循环
5.排查性能问题
- 利用现有的性能分析工具,如 Lighthouse和 Chrome devtools
- 根据各项性能指标进行评估,分析问题
- 提出对应加载和渲染的解决方案
- 后面还要持续监控、分析、解决、测试(可使用第三方统计服务)
更多可看我这篇文章:浏览器等运行环境面试知识体系汇总 – 掘金 (juejin.cn)
6.for vs forEach
- for循环会更快
- 因为 forEach 每次循环都要新创建一个函数增加开销,所以耗时更久
7.js-bridge 原理
- 微信中的 h5 通过 jssdk 提供的 API 可以调用微信 app 的某些功能
- JS 无法直接调用 app 的 API ,需要通过一种方式 —— 通称 js-bridge ,它也是一些 JS 代码
- 当然,前提是 app 得开发支持,控制权在 app 端。就像跨域,server 不开放支持,客户端再折腾也没用
方式1 – 注入 API
- 客户端为 webview 做定制开发,在 window 增加一些 API ,供前端调用
- 例如增加一个
window.getVersion
API ,前端 JS 即可调用它来获取 app 版本号
const v = window.getVersion()
// 但这种方式一般都是同步的
// 因为你即便你传入了一个 callback 函数,app 也无法执行。app 只能执行一段全局的 JS 代码(像 `eval`)
复制代码
方式2 – 劫持 url scheme
- 一个 iframe 请求 url ,返回的是一个网页,天然支持异步
const iframe1 = document.getElementById('iframe1')
iframe1.onload = () => {
console.log(iframe1.contentWindow.document.body.innerHTML)
}
iframe1.src = 'http://127.0.0.1:8881/size-unit.html'
复制代码
上述 url 使用的是标准的 http 协议,如果改成'my-app-name://api/getVersion'
默认会报错,因为该协议是未识别的
既然未识别的协议,那就可以为我所用,app 监听所有的网络请求,遇到 my-app-name:
协议,就分析 path ,并返回响应的内容
const iframe1 = document.getElementById('iframe1')
iframe1.onload = () => {
console.log(iframe1.contentWindow.document.body.innerHTML) // '{ version: '1.0.1' }'
}
iframe1.src = 'my-app-name://api/getVersion'
复制代码
这种自定义协议的方式,就叫做“url scheme”。微信的 scheme 以 'weixin://'
开头,可搜索“微信 scheme”
chrome 也有自己的 scheme
chrome://version
查看版本信息chrome://dino
恐龙小游戏- 其他可参考 mp.weixin.qq.com/s/T1Qkt8DTZ…
封装SDK
scheme 的调用方式非常复杂,不能每个 API 都写重复的代码,所以一般要封装 sdk ,就像微信提供的 jssdk
const sdk = {
invoke(url, data, success, err) {
const iframe = document.createElement('iframe')
iframe.style.display = 'none'
document.body.appendChild(iframe)
iframe.onload = () => {
const content = iframe.contentWindow.document.body.innerHTML
success(JSON.parse(content))
iframe.remove()
}
iframe.onerror = () => {
err()
iframe.remove()
}
iframe.src = `my-app-name://${url}?data=${JSON.string(data)}`
}
fn1(data, success, err) {
invoke('api/fn1', data, success, err)
}
fn2(data, success, err) {
invoke('api/fn2', data, success, err)
}
}
// 使用
sdk.fn1(
{a: 10},
(data) => { console.log('success', data) },
() => { console.log('err') }
)
复制代码
8.requestIdleCallback 和 requestAnimationFrame
requestIdleCallback
因为React 16 内部使用 Fiber ,即组件渲染过程可以暂停,先去执行高优任务,核心就是requestIdleCallback
requestIdleCallback 会在网页渲染完成后,CPU 空闲时执行,不一定每一帧都执行
requestIdleCallback 不适合执行 DOM 操作,因为修改了 DOM 之后下一帧不一定会触发修改
requestAnimationFrame
每次渲染都执行,高优
页面的渲染是一帧一帧进行的,至少每秒 60 次(即 16.6ms 一次)才能肉眼感觉流畅,所以,网页动画也要这个帧率才能流畅
用 JS 来控制时间是不靠谱的,因为 JS 执行本身还需要时间,而且 JS 和 DOM 渲染线程互斥。所以 ms 级别的时间会出现误差
requestAnimationFrame
就解决了这个问题,浏览器每次渲染都会执行,不用自己计算时间
两者比较
- requestAnimationFrame 和 requestIdleCallback 都是宏任务,它们比 setTimeout 更晚触发
- requestAnimationFrame 可用于网页动画
- requestIdleCallback 可用于一些低优先级的场景,以代替 setTimeout,例如发送统计数据
9.移动端1px边框、click 300ms 延迟、touch点击穿透
详情请看这篇文章:移动端Web常见问题 – 掘金 (juejin.cn)
10.渲染 10w 条数据
- 一般这种场景应该是后端分页给出
虚拟列表
基本原理
- 只渲染可视区域 DOM
- 其他隐藏区域不渲染,只用一个
<div>
撑开高度 - 监听容器滚动,随时创建和销毁 DOM
虚拟列表实现比较复杂,特别是在结合异步 ajax 加载,明白实现原理,实际项目可用第三方 lib
自定义中间层
- 自定义nodejs 中间层,获取并拆分这 10w 条数据
- 前端对接 nodejs 中间层,而不是服务端
- 成本比较高
11.设计模式
- 设计原则是设计模式的基础,开放封闭原则是最重要的:对扩展开发,对修改封闭。
工厂模式
用一个工厂函数,创建一个实例,封装创建的过程:
class Foo { ... }
function factory(): Foo {
// 封装创建过程,这其中可能有很多业务逻辑
return new Foo(...arguments)
}
复制代码
应用场景
- jQuery
$('div')
创建一个 jQuery 实例 - React 的
React.createElement()
和 Vue 的h()
创建一个 vnode
单例模式
提供全局唯一的对象,无论获取多少次:
class SingleTon {
private static instance: SingleTon | null = null
private constructor() {}
public static getInstance(): SingleTon {
if(this.instance == null) {
this.instance = new SingleTon()
}
return this.instance
}
fn1() {}
fn2() {}
}
// const s1 = new SingleTon() // Error: constructor of 'singleton' is private
const s2 = SingleTon.getInstance()
s2.fn1()
s2.fn2()
const s3 = SingleTon.getInstance()
s2 === s3 // true
复制代码
应用场景
- Vuex Redux 的 store ,全局唯一的
- 全局唯一的 dialog modal
代理模式
- 使用者不能直接访问真实数据,而是通过一个代理层来访问
- ES Proxy 本身就是代理模式,Vue3 基于它来实现响应式
观察者模式
- 即常说的绑定事件。一个主题,一个观察者,主题变化之后触发观察者执行
// 一个主题,一个观察者,主题变化之后触发观察者执行
btn.addEventListener('click', () => { ... })
复制代码
发布订阅模式
即常说的自定义事件,一个 event
对象,可以绑定事件,可以触发事件
// 绑定
event.on('event-key', () => {
// 事件1
})
event.on('event-key', () => {
// 事件2
})
// 触发执行
event.emit('event-key')
// 解绑事件,一般组件销毁前进行
event.off('event-key', fn1)
event.off('event-key', fn2)
复制代码
装饰器模式
- ES 和 TS 的 Decorator 语法就是装饰器模式。可以为 class 和 method 增加新的功能
- 以下代码可以在 ts playground 中运行
// class 装饰器
function logDec(target) {
target.flag = true
}
@logDec
class Log {
// ...
}
console.log(Log.flag) // true
复制代码
// method 装饰器
// 每次 buy 都要发送统计日志,可以抽离到一个 decorator 中
function log(target, name, descriptor) {
// console.log(descriptor.value) // buy 函数
const oldValue = descriptor.value // 暂存 buy 函数
// “装饰” buy 函数
descriptor.value = function(param) {
console.log(`Calling ${name} with`, param) // 打印日志
return oldValue.call(this, param) // 执行原来的 buy 函数
};
return descriptor
}
class Seller {
@log
public buy(num) {
console.log('do buy', num)
}
}
const s = new Seller()
s.buy(100)
复制代码
ngular nest.js 都已广泛使用装饰器。这种编程模式叫做AOP 面向切面编程:关注业务逻辑,抽离工具功能
import { Controller, Get, Post } from '@nestjs/common';
@Controller('cats')
export class CatsController {
@Post()
create(): string {
return 'This action adds a new cat';
}
@Get()
findAll(): string {
return 'This action returns all cats';
}
}
复制代码
观察者模式 VS 发布订阅模式
观察者模式
- Subject 和 Observer 直接绑定,中间无媒介
- 如
addEventListener
绑定事件
发布订阅模式
- Publisher 和 Observer 相互不认识,中间有媒介
- 如
eventBus
自定义事件
MVC VS MVVM
MVC 原理
- View 传送指令到 Controller
- Controller 完成业务逻辑后,要求 Model 改变状态
- Model 将新的数据发送到 View,用户得到反馈
MVVM 直接对标 Vue 即可
- View 即 Vue template
- Model 即 Vue data
- VM 即 Vue 其他核心功能,负责 View 和 Model 通讯
12.处理项目冲突
不影响需求和工期的冲突,如技术方案问题,尽量私下沟通解决
需求变更和时间延期一定要开会解决,保证决策领导知晓在场
当产生结果之后,最常见就是发邮件抄送给各位负责人
13.数组转树和树转数组
定义一个 convert
函数,将以下数组转换为树结构:
const arr = [
{ id: 1, name: '部门A', parentId: 0 }, // 0 代表顶级节点,无父节点
{ id: 2, name: '部门B', parentId: 1 },
{ id: 3, name: '部门C', parentId: 1 },
{ id: 4, name: '部门D', parentId: 2 },
{ id: 5, name: '部门E', parentId: 2 },
{ id: 6, name: '部门F', parentId: 3 },
]
复制代码
遍历数组,针对每个元素
- 生成 tree node
- 找到 parentNode 并加入到它的
children
- 找 parentNode 时,需要根据
id
能尽快找到 tree node - 需要一个 map ,这样时间复杂度是
O(1)
。否则就需要遍历查找,时间复杂度高
interface IArrayItem {
id: number;
name: string;
parentId: number;
}
interface ITreeNode {
id: number;
name: string;
children?: ITreeNode[];
}
function convert(arr: IArrayItem[]): ITreeNode | null {
// 用于 id 和 treeNode 的映射 key为number类型 value为ITreeNode类型
const idToTreeNode: Map<number, ITreeNode> = new Map();
let root = null;
arr.forEach((item) => {
const { id, name, parentId } = item;
// 定义 tree node 并加入 map
const treeNode: ITreeNode = { id, name };
idToTreeNode.set(id, treeNode);
// 找到 parentNode 并加入到它的 children
const parentNode = idToTreeNode.get(parentId);
if (parentNode) {
if (parentNode.children == null) parentNode.children = [];
parentNode.children.push(treeNode);
}
// 找到根节点
if (parentId === 0) root = treeNode;
});
return root;
}
const arr = [
{ id: 1, name: "部门A", parentId: 0 }, // 0 代表顶级节点,无父节点
{ id: 2, name: "部门B", parentId: 1 },
{ id: 3, name: "部门C", parentId: 1 },
{ id: 4, name: "部门D", parentId: 2 },
{ id: 5, name: "部门E", parentId: 2 },
{ id: 6, name: "部门F", parentId: 3 },
];
const tree = convert(arr);
console.info(tree);
复制代码
定义一个 convert
函数,将以下对象转换为数组
const obj = {
id: 1,
name: '部门A',
children: [
{
id: 2,
name: '部门B',
children: [
{ id: 4, name: '部门D' },
{ id: 5, name: '部门E' }
]
},
{
id: 3,
name: '部门C',
children: [{ id: 6, name: '部门F' }]
}
]
}
复制代码
[
{ id: 1, name: '部门A', parentId: 0 }, // 0 代表顶级节点,无父节点
{ id: 2, name: '部门B', parentId: 1 },
{ id: 3, name: '部门C', parentId: 1 },
{ id: 4, name: '部门D', parentId: 2 },
{ id: 5, name: '部门E', parentId: 2 },
{ id: 6, name: '部门F', parentId: 3 },
]
复制代码
- 根据顺组的顺序,需要广度优先遍历树
- 要快速获取
parentId
需要存储nodeToParent
map 结构
interface IArrayItem {
id: number;
name: string;
parentId: number;
}
interface ITreeNode {
id: number;
name: string;
children?: ITreeNode[];
}
function convert1(root: ITreeNode): IArrayItem[] {
// Map
const nodeToParent: Map<ITreeNode, ITreeNode> = new Map();
const arr: IArrayItem[] = [];
// 广度优先遍历,queue
const queue: ITreeNode[] = [];
queue.unshift(root); // 根节点 入队
while (queue.length > 0) {
const curNode = queue.pop(); // 出队
if (curNode == null) break;
const { id, name, children = [] } = curNode;
// 创建数组 item 并 push
const parentNode = nodeToParent.get(curNode);
const parentId = parentNode?.id || 0;
const item = { id, name, parentId };
arr.push(item);
// 子节点入队
children.forEach((child) => {
// 映射 parent
nodeToParent.set(child, curNode);
// 入队
queue.unshift(child);
});
}
return arr;
}
const obj = {
id: 1,
name: "部门A",
children: [
{
id: 2,
name: "部门B",
children: [
{ id: 4, name: "部门D" },
{ id: 5, name: "部门E" },
],
},
{
id: 3,
name: "部门C",
children: [{ id: 6, name: "部门F" }],
},
],
};
const arr1 = convert1(obj);
console.info(arr1);
复制代码
14.map parseInt
['1', '2', '3'].map(parseInt)
输出什么?parseInt(string, radix)
解析一个字符串并返回指定基数的十进制整数string
要解析的字符串radix
可选参数,数字基数(即进制),范围为 2-36,如果string
以0x
开头,则按照 16 进制处理。其他情况,按 10 进制处理
题目代码可以拆解为:
const arr = ['1', '2', '3']
const res = arr.map((s, index) => {
console.log(`s is ${s}, index is ${index}`)
return parseInt(s, index)
})
console.log(res)
复制代码
执行过程:
parseInt('1', 0) // 1 ,radix === 0 按 10 进制处理
parseInt('2', 1) // NaN ,radix === 1 非法(不在 2-36 之内)
parseInt('3', 2) // NaN ,2 进制中没有 3
复制代码
所以答案:
['1', '2', '3'].map(parseInt) // [1, NaN, NaN]
复制代码
15.前端统计 sdk
分析
前端统计的范围
- 访问量 PV
- 自定义事件(如统计一个按钮被点击了多少次)
- 性能
- 错误
统计数据的流程 (只做前端 SDK ,但是要了解全局)
- 前端发送统计数据给服务端
- 服务端接受,并处理统计数据
- 查看统计结果
代码结构
SDK 要用于多个不同的产品,所以初始化要传入 productId
class MyStatistic {
private productId: number
constructor(productId: number = 0) {
if (productId <= 0) {
throw new Error('productId is invalid')
}
this.productId = productId // 产品 id (SDK 会被用于多个产品)
this.initPerformance() // 性能统计
this.initError() // 监听错误
}
private send(url: string, paramObj: object = {}) {
// TODO 发送统计数据
}
private initPerformance() {
// TODO 性能统计
}
private initError() {
// TODO 监听全局错误(有些错误需要主动传递过来,如 Vue React try-catch 的)
}
pv() {
// TODO 访问量 PV 统计
}
event(key: string, value: string) {
// TODO 自定义事件
}
error(key: string, info: object = {}) {
// TODO 错误统计
}
}
复制代码
用户使用:
const myStatistic = new MyStatistic('abc')
复制代码
发送数据
发送统计数据,用 <img>
—— 浏览器兼容性好,没有跨域限制
private send(url: string, paramObj: object = {}) {
// 追加 productId
paramObj.productId = this.productId
// params 参数拼接为字符串
const paramArr = []
for (let key in paramObj) {
const value = paramObj[key]
paramArr.push(`${key}=${value}`)
}
const img = document.createElement('img')
img.src = `${url}?${paramArr.join('&')}`
}
复制代码
如果再精细一点的优化,send
中可以使用 requestIdleCallback
(兼容使用 setTimeout
)
自定义事件统计
event(key: string, value: string) {
const url = 'xxx' // 接受自定义事件的 API
this.send(url, { key, value }) // 发送
}
复制代码
用户使用:
// 如需要统计“同意” “不同意” “取消” 三个按钮的点击量,即可使用自定义事件统计
$agreeBtn.click(() => {
// ...业务逻辑...
myStatistic.event('some-button', 'agree') // 其他不同的按钮,传递不同的 value (如 'refuse' 'cancel')
})
复制代码
访问量 PV
PV 可以通过自定义事件的方式。但是为了避免用户重复发送,需要加一个判断
// 定义一个全局的 Set ,记录已经发送 pv 的 url
const PV_URL_SET = new Set()
复制代码
pv() {
const href = location.href
if (PV_URL_SET.has(href)) return
this.event('pv', '') // 发送 pv
PV_URL_SET.add(href)
}
复制代码
用户使用:
myStatistic.pv()
复制代码
【注意】PV 统计需要让用户自己发送吗,能不能在 DOMContentLoaded 时自动发送?—— 最好让用户发送,因为 SPA 中切换路由也可能发送 PV
性能统计
通过 console.table( performance.timing )
可以看到网页的各个性能
private initPerformance() {
const url = 'yyy' // 接受性能统计的 API
this.send(url, performance.timing) // 全部传给服务端,让服务端去计算结果 —— 统计尽量要最原始数据,不要加工处理
}
复制代码
PS:想要得到全面的性能数据,要在网页加载完成之后( DOMContentLoaded 或 onload )去初始化 myStatistic
错误统计
监听全局操作
private initError() {
// 全局操作
window.addEventListener('error', event => {
const { error, lineno, colno } = event
this.error(error, { lineno, colno })
})
// Promise 未 catch 的报错 ( 参考 unhandledrejection.html )
window.addEventListener("unhandledrejection", event => {
this.error(event.reason)
})
}
复制代码
被开发这主动收集的错误,需要调用 API 来统计
error(error: Error, info: object = {}) {
// error 结构 { message, stack }
// info 是附加信息
const url = 'zzz' // 接受错误统计的 API
this.send(url, Object.assign(error, info))
}
复制代码
用户使用:
// try catch
try {
100()
} catch (e) {
myStatistic.error(e)
}
// Vue 错误监听
app.config.errorHandler = (error, instance, info) => {
myStatistic.error(error, { info })
}
// React 错误监听
componentDidCatch(error, errorInfo) {
myStatistic.error(error, { info: errorInfo })
}
复制代码
总结
- 自定义事件(包括 PV)
- 性能统计
- 报错统计
PS:以上是一个统计 SDK 的基本估计,可以应对面试,实际工作中还可能需要进一步完善很多细节。
16.sourcemap
- 遇到 JS 报错的问题,就离不开 sourcemap
- JS 上线之前要合并、混淆和压缩,压缩之后,一旦线上有报错,通过行、列根本找不到源代码的位置,不好定位错误
- sourcemap 就是用于解决这个问题
配置
- sourcemap 是在打包、压缩 js 时生成,通过 webpack 的打包工具即可配置
- webpack 通过
devtool
来配置 sourcemap ,有多种选择 webpack.docschina.org/configurati…- 不用
devtool
– 正常打包,不会生成 sourcemap 文件 eval
– 所有代码都放在eval(...)
中执行,不生成 sourcemap 文件source-map
– 生成单独的 sourcemap 文件,并在 js 文件最后指定eval-source-map
– 代码都放在eval(...)
中执行,sourcemap 内嵌到 js 代码中,不生成独立的文件inline-source-map
– sourcemap 以 base64 格式插入到 js 末尾,不生成单独的文件cheap-source-map
– sourcemap 只包含行信息,没有列信息(文件体积更小,生成更快)eval-cheap-source-map
– 同上,但是所有代码都放在eval(...)
中执行
- 不用
推荐
- 开发和测试
eval
eval-source-map
eval-cheap-source-map
—— 追求效率 - 生产环境
source-map
或者不产出 sourcemap —— 看个人需求
17.H5编辑器
- 低代码,现在流行
这是一个 H5 编辑器,用 vue + vuex 来实现,几个问题:
- 在点“保存”按钮的时候,往服务端传递的数据结构是什么样子的?
- 如何保证画布和属性面板是同步更新的?
- 如果在扩展一个“图层”面板,数据结构该怎么设计?
设计思路
vuex store
{
// 作品
work: {
title: '作品标题',
setting: { /* 一些可能的配置项,用不到就先预留 */ },
props: { /* 页面 body 的一些设置,如背景色 */ },
components: [
// components 要用数组,有序结构
// 单个 node 要符合常见的 vnode 格式
{
id: 'xxx', // 每个组件都有 id ,不重复
name: '文本1',
tag: 'text',
attrs: { fontSize: '20px' },
children: [
'文本1' // 文本内容,有时候放在 children ,有时候放在 attrs 或者 props ,没有标准,看实际情况来确定
]
},
{
id: 'yyy',
name: '图片1',
tag: 'image',
attrs: { src: 'xxx.png', width: '100px' },
children: null
},
]
},
// 画布当前选中的组件,记录 id 即可
activeComponentId: 'xxx'
}
复制代码
vuex getter
const getters = {
layers() {
store.work.components.map(c => {
return {
id: c.id,
name: c.name
}
})
}
}
复制代码
总之,基本思路就是:
- 每个组件尽量符合 vnode 规范
- 用数组来组织数据,有序
- 尽量使用引用关系,不要冗余
18.权限管理
例如,一个博客管理后台,可以添加很多用户,分配不同的角色,不同角色具有不同权限
- 普通用户:查看博客,审核通过博客,下架博客
- 管理员:修改博客,删除博客 + 普通用户的权限
- 超级管理员:添加用户,删除用户,绑定用户和角色 + 管理员的权限
RBAC 模型
RBAC – Role-based access control 基于角色的访问控制。它可以满足我们绝大部分管理系统的管理权限控制。
- 三个模型
- 用户
- 角色
- 权限
- 两个关系(以角色为“轴”)
- 角色 – 用户
- 角色 – 权限
功能
用户管理
- 增删改查
- 绑定角色
角色管理
- 增删改查
- 绑定权限
权限管理
- 增删改查
19.H5 抽奖页
你作为前端负责人,来开发一个 h5 页,某个抽奖功能的运营活动
你要从 0 开发这个页面,你会要求 server 端给你哪些接口和能力?
答案
- 获取用户信息(同时判断是否登录)
- 如果登录,判断该用户是否已经抽奖,以判断他是否还能继续抽奖
- 抽奖接口
- 可能还需要调用登录接口
- 当然也可以直接输入手机号抽奖,需明确需求
- 埋点统计
- pv
- 自定义事件
- 微信分享
20.技术选型
- 考虑社区成熟度
- 考虑公司的技术积累
- 考虑团队成员的学习成本
- 考虑它的价值是否真的被利用
21.图片懒加载
第一,<img>
要使用 data-src
(或其他属性)记录 src 。还有,loading.gif 要自定义,要配合实际图片的尺寸
<img src="./img/loading.gif" data-src="./img/animal1.jpeg"/>
复制代码
第二,可以使用 Element.getBoundingClientRect()
来判断当前元素的位置
第三,页面滚动实时计算,注意节流
代码可看我这篇文章:移动端性能优化 – 掘金 (juejin.cn)