框架(Vue & React)高频实践方法论汇总

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-catchwindow.onerror 捕获其他错误

扩展

Promise 监听报错要使用 window.onunhandledrejection

前端拿到错误监听之后,需要传递给服务端,进行错误收集和分析,然后修复 bug

3.Vue性能优化和遇到的坑

坑:

  • 全局事件,定时器没有即使清除,会有内存泄漏风险
  • 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 写成 htmlForclass 写成 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 不开放支持,客户端再折腾也没用

image-20220310011558684

方式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

封装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,如果 string0x 开头,则按照 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 ) 可以看到网页的各个性能

image-20220315020429641

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.技术选型

  1. 考虑社区成熟度
  2. 考虑公司的技术积累
  3. 考虑团队成员的学习成本
  4. 考虑它的价值是否真的被利用

21.图片懒加载

第一,<img> 要使用 data-src(或其他属性)记录 src 。还有,loading.gif 要自定义,要配合实际图片的尺寸

<img src="./img/loading.gif" data-src="./img/animal1.jpeg"/>
复制代码

第二,可以使用 Element.getBoundingClientRect() 来判断当前元素的位置

第三,页面滚动实时计算,注意节流

代码可看我这篇文章:移动端性能优化 – 掘金 (juejin.cn)

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享