interview2024

# 复习计划

节前完成:

  1. leetcode精选题复习
  2. react项目学习
  • 博客笔记
  • vue
  • react
  • node.js
  • 算法题
  • 面试题
  • 基础题
  • zhuawa
  • 模拟面试
  • 简历:电商业务梳理、ai项目、低代码、组件库
  • 基础题:js原理, js手写...
  • 部署/工程化:docker...

# 2024春节复习计划

  • 初二:复习链表+二叉树知识点,《top150题》算法题刷完;复习爪哇学习笔记;
  • 初三:博客【基础】部分笔记:promise原理、js原型链,js事件循环...
  • 初四:js手写, 博客【vue】部分,准备vue;
  • 初五:剩余vue面试题复习...(情人节,放松了一下~)
  • 初六:复习完剩下的博客笔记:webpack,工程化,node.js,nginx...,
  • 初七:
    1. react面试题收集;
    2. 源码部分笔记, 总结微博、boss直聘部署上线流程;
    3. 面试题收集、面试记录复习,整理;复习下my-code,test-code中的项目代码;ts知识点;
  • 初八:
    1. http部分, 简历梳理 => 电商业务、ai项目、低代码、组件库,技术点;准备自我介绍,模拟面试
    2. js手写+算法题复习,ts手写, react手写, vue/react快速新建项目

# 面试记录

  • 美团一面 2024.02.20
  1. 主要是聊了业务,聊了工作中做了什么,没怎么问技术;
  2. 有一个手写题,实现多个promise的并发请求,没做出来...大脑一片浆糊...😭
  • 字节一面 2024.02.20
  1. js为什么是单线程,js异步的原理,http缓存,etag/last-modified优先级,图片懒加载,前端性能优化,useMemo和useCallback的区别;

  2. 设计一个input输入框,需要考虑哪些问题:防抖处理,接口请求会被覆盖;

  3. js中setTimeout和setInterval都能实现定时器效果,但二者有什么区别?

  4. 手写题:将一维数组转为成树型结构...没写出来...😭,本来看到题觉得自己会写出来的..大脑一片浆糊...

  • 小米一面 2024.02.21
  1. 一道手写题:'ABABABABABAABBB', 删除'AB'和'CD',
  2. 问项目:编辑器,低代码平台通信问题(发布订阅模式,依赖收集),组件库,业务
  3. react函数组件怎么触发重新渲染,怎么避免重复渲染?
  4. css,less,scss区别,讲一下flex
  5. react项目中怎么做样式隔离?css是全局变量,怎么做样式隔离?
  • 腾讯一面 2024.02.21

上来就是两道大题:

  1. 封装一个链表类,实现:
  • insert(1)方法,在链表头部插入一个值;
  • getFirst()获取头部指针;
  • reverse() 反转链表;
let ll = new ListNode();
ll.insert(1);
ll.insert(2);
ll.insert(3);
console.log(ll.getFirst()); // {value: 3, next: ListNode(val: 2, next: ...)}
  1. 第二套题直接题目都看不懂了,一长串描述..崩溃...,大致就是获取url参数,转成obj,加密等等...
  • 猎聘一面 2024.03.01
  1. 讲一下组件库项目怎么设计的,组件库开发,有哪些性能优化?vscode插件怎么开发的?
  2. webpack打包怎么做分包的?性能优化?
  3. node.js做过什么项目?react父组件怎么调用子组件的方法?
  • 神州租车一面 2024.03.01
  1. css3新属性有哪些,html5新标签有哪些?页面的常用布局有哪些?vue有哪些hooks,react有哪些hooks?前端怎么实现预加载?

基本都是一些基础的css,html,js,webpack,vue,react基础题~

  1. 用node做过什么项目?印象深刻的项目?
  2. ts有哪些用法?interface和type的区别?ts有哪些高级用法?ts装饰器怎么用?什么是泛型?es6有哪些?map和set的区别?map和weakMap的区别?原型链?
  • 百度一面 2024.03.04
  1. 数组怎么去重?怎么随机打乱?vue2和vue3响应式处理?react性能优化?后端返回十万条数据,怎么优化?
  2. React服务端渲染遇到过哪些问题,怎么做性能优化?高阶组件?
  3. 手写题:实现一维数组转换为树形数组;
  4. 第二位面试官主要问了业务,说了组件库,编辑器,低代码,node可视化平台部署;如何保证编译的测试环境跟线上环境是一致的?
  • 快手一面 2024.03.04
  1. 问了业务:脚手架,低代码,组件库,业务开发
  2. js打印题:
// 1.
const a = {name: 'aaa'};
const b = Object.create(a); // b.prototype = a;
const c = Object.assign({}, b); // 执行浅拷贝
console.log('===c', c, b); // {} {}; 因为a是b原型上的属性


// 2.
var name = 'aaa';
var obj = {
    name: 'bbb',
    fn: function() {
        function print() {
            console.log('===print', this.name); // aaa
        }
        print();
        const print02 = () => {
            console.log('=====print02', this.name); // bbb
        }
        print02()
    }
}
obj.fn();


3.
import {reactive} from 'vue';

const obj = reactive({
    name: 'aa',
    age: 22
})
watchEffect(() => {
    const {name, age} = obj;
    console.log('===', name, age); // 执行一次: 因为 watchEffect 的回调函数会在所有依赖变化后执行一次
})
obj.name = 'bbb';
obj.age = 33;
  • 字节tiktok 一面 2024.03.11
  1. 讲一下在电商业务中主要做过些什么?
  2. 基础题:http缓存,cookie/session/localstorage, dns解析,http执行流程,闭包
  3. 三个手写题:函数柯里化,比较版本号,js执行顺序
// 1. 实现 add(1,2,3) = 9, add(1,2)(3) = 9, add(1)(2)(3) = 9
function add() {
    let args = [...arguments];
    function fn() {
        args = [...args, ...arguments];
        if (args.length >= 3) {
            let sum = args.reduce((prev, cur) => {
                return prev + cur;
            }, 0)
            return sum;
        } else {
            return fn;
        }
    }
    return fn()
}
console.log("======add", add(1,2,3), add(1)(2)(3), add(1,2)(3))

// 2. 比较版本号


// 3. 执行顺序
setTimeout(() => {
    console.log(1)
}, 0)
new Promise(resolve => {
    console.log(2);
    for(let i = 0; i < 9999; i ++) {
        if (i > 9990) {
            resolve()
        }
    }
    console.log(3);
}).then(res => {
    console.log(4);
})
console.log(5);
  • 阿里高德一面 2024.03.12
  1. 问了些基础问题..
  2. 写了个手写题:
写一个js异步方法,满足下面三个条件:
1. 每隔3秒请求一次,有结果返回结果,有报错重试发送请求;
2. 设置10s超时,超时的话返回报错结果
  • 阿里淘天一面 2024.03.14
  1. 问了最深刻的业务,做了哪些优化?
  2. 从架构的角度考虑,在做项目开发之前有哪些考虑?
  3. 对了很多b端的项目,从架构的角度考虑,有哪些思考点?微前端实现子项目之间通信隔离的原理是什么?WebView嵌入怎么做性能优化?
  4. 手写题:
// 1. 写一个高阶组件,主要是对表单进行校验;需要有onChange和value


// 2. 执行顺序题,js基础
function Foo() {
  getName = function () {
    alert(1);
  };
  return this;
}
Foo.getName = function () {
  alert(2);
};
Foo.prototype.getName = function () {
  alert(3);
};
var getName = function () {
  alert(4);
};
function getName() {
  alert(5);
}

Foo.getName(); // 2
getName(); // 4
Foo().getName(); // 1:取的是Foo里面定义的方法
getName(); // 1: 上一步把getName挂载到了全局
new Foo.getName(); // 2
new Foo().getName(); // 3: 取的是原型上的方法
new new Foo().getName(); // 3

// 3. react-loashable实现懒加载的原理?路由懒加载的原理?

// 4. 
  • 阿里高德二面 2024.03.15
  1. 在做商家端项目开发的过程中,基础架构是什么?
  2. ssr能够缩短首屏渲染时间的底层原理是什么?
  3. 封装一个虚拟滚动列表组件需要做哪些事情?哪些场景需要用防抖节流,哪些场景不需要使用?
  4. 手写题:js浅比较的代码实现
  • 同程旅行一面 2024.03.18
  1. node.js适用的场景?怎么解决高并发?
  2. 前端监控中怎么统计图片报错的情况?怎么统计页面所有图片的报错?
  • 百度一面 2024.03.21
  1. node做过哪些项目,ssr是怎么实现的?node中间件?自己设计一个koa的洋葱模型怎么设计?
  2. 基础题:重排重绘,http请求,...
  3. 手写题:
// 1. 二分查找



// 2. 全排列:给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。
function dfsSearch(arr) {
  let res = [];
  function helper(nums) {
    if (nums.length >= arr.length) {
      res.push(nums);
      return;
    }
    for(let i = 0; i < arr.length; i ++) {
      let cur = arr[i];
      if (nums.indexOf(cur) === -1) {
        nums.push(cur);
        helper([...nums]);
        nums.pop();
      }
    }
  }

  helper([]);
  return res;
}
console.log('=====dfsSearch', dfsSearch([1,2,3])) // 
  • 字节tt二面 2024.03.21
  1. 手写题:实现每次pop取出最高频次的数据
class FreqStack {
  constructor() {
    this.map = {};
  }
  push(val) {
    this.map[val] = this.map[val] ? this.map[val] + 1 : 1;
  }
  pop() {
    let max = Math.max(...Object.values(this.map));
    for(let key in this.map) {
      if (this.map[key] === max) {
        delete this.map[key];
        return key;
      }
    }
    return -1;
  }
}
const freqStack = new FreqStack();
freqStack.push(5);
freqStack.push(7);
freqStack.push(5);
freqStack.push(4);
freqStack.push(5);
freqStack.push(7);
console.log(freqStack.pop()) // 5
console.log(freqStack.pop()) // 7
  1. 问了做过哪些业务,说了微博电商,有了社区,直播大屏(响应式设计优缺点,技术方案)
  • 百度二面 2024.03.26
  1. 讲了几个项目,微博电商,监控平台,组件库,web3项目
  2. node怎么做自动部署?什么是nodejs的守护进程?node在部署过程中怎么应对客户端突然的访问量?
  3. 监控系统的插件怎么挂载到内核上?
  • 字节一面 2024.03.26
  1. 问项目,有哪些技术复杂点?
  2. 思考一个项目中自动化国际化方案,有哪些思考?
  3. 代码题:
// 1. 执行顺序:
var A = function() {this.b = 3};
var C = new A();
A.prototype.b = 9;
var b = 1;
A();
console.log(b); // 3
console.log(C.b); // 3


// 2. 二叉树的锯齿层序遍历
  • 车好多一面 2024.04.02
  1. 开发商详页的时候,有哪些思考?(主要是sku的设计)
  2. b端商品管理配置sku的流程?
  3. c端展示商品装修有哪些处理?c端有哪些缓存处理?keep-alive原理是什么?react怎么实现keep-alive?
  4. c端首屏渲染优化有哪些方案?虚拟滚动原理?虚拟滚动怎么处理item不定高度的情况?
  5. vue怎么实现一个函数式的toast组件?以及它对于时间的处理?
  • 车好多二面 2024.04.07
  1. 做过哪些项目给公司带来了哪些收益?
  2. 对于项目秒开率的优化,会从哪些方面分析和优化?
  3. react的源码设计中为什么会把setState设计为异步的,v18后为什么又会把原生方法设计为同步的?
  4. 从架构层面分析,vue2和vue3的差异?
  5. 手写题:
/**
示例 2:

输入:nums1 = [0], nums2 = [0], nums3 = [0], nums4 = [0]
输出:1

提示:
n == nums1.length
n == nums2.length
n == nums3.length
n == nums4.length
1 <= n <= 200
-228 <= nums1[i], nums2[i], nums3[i], nums4[i] <= 228

返回实现数组和为0的个数
 *  /
  • 阿里书旗一面 2024.04.09
  1. 基础问题:css的响应式布局,css预处理器,怎么解决0.5px,
  2. js基础题:闭包,js的垃圾回收机制(为什么要分为标记清除和引用计数,在什么时候执行,区别),原型链,作用域,js怎么确定this指向,怎么实现原型链继承,事件委托...
  3. vue的用法,vue项目怎么做性能优化,v-bind和v-model的区别...
// 写一个防抖函数


// 使用原型链实现继承
function SuperType(age) {
  this.name = 'a';
  this.age = age;
}
SuperType.prototype.sayName = function() {
  console.log(this.name);
}

function SubType(name) {
  this.name = name;
  this.age = 18;
}
SubType.prototype = new SuperType(10);
const instance = new SubType('b');
instance.sayName() // b


// 实现一个数组合并方法
  • 51talk一面 2024.04.11
  1. https的流程,怎么进行加密,验证的?
  2. 口述算法题:怎么获取两个数组的交集?
  3. 了解nginx吗?什么是k8s?
  • 阿里书旗二面 2024.04.14
  1. 出了一个html加载流程问题:

  2. 手写题:用两个栈实现一个队列


  1. 如果你第一天去公司,产品让修改一个文案,怎么修改?具体点?
  • 快手一面 2024.04.15
  1. 问了项目,怎么做性能优化,怎么缩短首屏渲染时间,.vue编译成.js过程
  2. 手写题:
// 执行顺序
async function async1() {
  console.log('async start'); // 2
  await new Promise(resolve => {
    console.log('promise'); // 3
    return 'promise end';
  })
  console.log('async end'); // 因为上面promise没有resolve,所以不会执行这里
}
console.log('script start');  // 1
async1().then(res => console.log(res));
console.log('script end'); // 4

// 爬楼梯
function dpFn(n) {
  let arr = new Array(n+1).fill(0);
  arr[0] = 0;
  arr[1] = 1;
  for(let i = 2; i <= n; i ++) {
    arr[i] = arr[i-1] + arr[i-2];
  }
  return arr[n];
}
  • 白龙马一面 2024.04.15
  1. 业务方向:介绍下最近做过的项目,做过哪些性能优化,怎么缩短首屏渲染时间,静态资源抽离cdn的形式能缩短首屏渲染时间吗?
  2. 技术方向:
  • 简单请求和普通请求有什么区别?
  • v8的垃圾回收机制了解吗?
  • js异步执行顺序题和手写题:
// 执行顺序:

/**
 * 
 * 输入一组版本号,输出从大到小的排序?
['2.1.0.1', '0.402.1', '10.2.1','10.2.1.9', '1.0.4.5', '1.0.4.3']
 */
function versionSort(versons) {
    return (versons || []).sort((a,b) => {
        let v1 = a.split('.');
        let v2 = b.split('.');
        let len = Math.max(v1.length, v2.length);
        for(let i = 0; i < len; i ++) {
            let cur1 = v1[i] ? Number(v1[i]) : -1;
            let cur2 = v2[i] ? Number(v2[i]) : -1;
            if (cur1 !== cur2) return cur2 - cur1;
        }
    })
}
console.log('=====versionSort', versionSort(['2.1.0.1', '0.402.1', '10.2.1','10.2.1.9', '1.0.4.5', '1.0.4.3']))
// ['10.2.1.9', '10.2.1', '2.1.0.1', '1.0.4.5', '1.0.4.3', '0.402.1']
  • 快手二面 2024.04.16
  1. node开发自动化部署,怎么实现测试环境的隔离?
  2. 讲一下react的fiber架构?怎么实现<1px?
  3. 手写题:实现一个限制最大并发请求的方法
  • 白龙马二面 2024.04.17(线下)
  1. 问最近的项目,怎么实现性能优化?...
  2. 异步流程题,口述编码题
  • 51talk二面 2024.04.18
  1. 问了几个印象深刻的项目:订单升级、有了社区、...,有没有压力最大的时候?遇到的技术难点最大的是?
  2. 自己实现一个无痕埋点会怎么实现?
  • 京东一面 2024.04.18
  1. 问了业务:组件库,ai助手,
  2. 如何解决 Chrome Cookie Same-Site 跨域问题
  3. vue2和vue3的区别,defineProperty和proxy的区别,响应式处理原理,react的fiber架构是什么
  4. 执行顺序:
var foo = {
    a: 1,
    fn: function(b) {
        b = b || this.a;
        return function(c) {
            console.log(this.a, b, c);
        }
    }
}
var a = 2;
var obj2 = {a: 3};
foo.fn(a).call(obj2, 1); // 3 2 1
foo.fn.call(obj2)(1) // 2 3 1
  • 360一面(线下) 2024.04.23
  1. 基本就是照着简历把重要的项目都说一遍,做过哪些业务,没怎么问技术

二面也就是简单问下业务~

  • 京东超市一面 2024.04.24
  1. 基本都是基础题,es6的class怎么实现原型继承,super()的作用?讲一下js中的严格模式?
  2. fiber架构的遍历规则是什么(父节点,子节点,兄弟节点)

fiber的遍历顺序是由根节点开始的,优先级 child > sibling > return父节点

  1. 服务端渲染原理?react ssr渲染注水和脱水?组件库开发的架构设计?
  • 百度萝卜快跑一面 2024.04.25
  1. 基本问了一些基础知识点,问了项目,没做题
  • 百度二面 2024.04.29
  1. 前端实现登录怎么设计?低代码实现逻辑?
  2. 手写一个快排,sort的时间复杂度
  3. ssr项目原理,node怎么解决重启的问题?
  4. 写过哪些动画,怎么解决实现动画效果卡顿的情况?
  • 百度三面 2024.05.08
  1. 问了项目,具体应用场景,node开发项目等等
  2. 手写题:
// 生成 数值大小位于(2, 32] 之间的整数,长度为n的随机数组

// 口述实现:`i'am a good man.` => `.man good a am'i`

# 软件问题整理

  • 脚手架怎么打包?脚手架开发遇到哪些问题?
  • 数据埋点怎么做?怎么统计首屏时间,首屏加载时间这些数据?
  • 对于前端项目做过哪些技术上的优化?有没有什么前端技术指标?

# 公司工程化流程

# vipkid

本地项目开发,打包部署,通过jenkins选择分支,环境,执行脚本部署;

后端接口测试环境需要在前端通过_gateway.config.js指定

# boss

前端开发完成,push到gitlab仓库;jenkins构建:选分支,环境

线上:先上线静态资源,再上线前端模板

# weibo

后端环境通过 whistle 代理

  • 电商业务:

gmv: Gross Merchandise Volume,是电商领域中的一个重要概念,中文意为“总交易额”或“总销售额”

UV(Unique Visitors):独立访客数,指访问电商网站或应用的不同用户的数量。这个指标可以反映电商平台的吸引力和用户规模。

PV(Page Views):页面访问量,指用户访问电商网站或应用时浏览的页面数量。这个指标可以反映用户对电商平台的兴趣和粘性。

CR(Conversion Rate):转化率,指用户从访问电商平台到完成购买行为的比例。这个指标可以反映电商平台的销售能力和用户体验。

电商的用户规模指的是在特定电商平台或应用上注册并使用其服务的用户总数。它代表了电商平台的市场覆盖范围和潜在用户基础。用户规模的大小可以反映电商平台的影响力和受欢迎程度。

日活(日活跃用户数,DAU)则是指一天之内活跃在平台上的用户数。它反映了电商平台的日常活跃度和用户粘性。日活是评估电商平台运营状况和用户参与度的重要指标之一。一个高的日活意味着平台能够吸引并保持用户的日常参与,有利于提升销售和用户留存率。

买量指的是通过购买互联网流量来增加用户数量或提高用户活跃度。在电商和互联网行业中,买量通常是通过广告投放、搜索引擎优化、社交媒体推广等方式来实现的,目的是为了吸引更多的潜在用户,提高品牌知名度和用户粘性。

留存则是指用户在使用产品或服务后,能够持续保持活跃状态并继续使用的能力。留存率是衡量产品或服务用户粘性和满意度的重要指标之一。留存率越高,说明产品或服务的用户体验越好,用户对其的认可度和忠诚度也越高。在电商领域中,客户留存尤其重要,因为它直接关系到销售额、市场份额以及企业的可持续发展。

# 自我介绍

面试官你好,我的名字叫周元,我从大学毕业先后在vipkid,boss直聘,新浪微博做过前端开发,做前端开发这几年做过的业务方向主要是用户增长,社区,电商相关的需求;

除了业务方向的工作,我之前也参与过不少b端项目的开发,比如公司的后台管理系统,访谈管理系统,还有部门动态智能表单低代码平台,创作者平台等等;

部门基建上也主要负责开发了部门组件库和一些vscode编辑器小工具之类的

在平时的工作我也愿意花时间探索一些新的领域,进行技术沉淀,以上就是我的自我介绍~

# 离职原因

  • 个人发展到了一定瓶颈期,希望寻求一些新的机会,以拓展我的技能和经验。同时我渴望面对新的挑战,并愿意在一个更具发展潜力的环境中贡献我的专业知识。

  • 一方面是为了有更多的时间准备面试,一方面是最近身体出点问题,恰好最近一段时间公司业务比较忙,就想好好休息一下,也顺便好好准备面试

# 业务

# 组件库

  • 组件库开发的技术架构设计?
  1. 调研方案:
  • 项目管理:如ElementUI,每一个项目都不只是单一的组件库,他通常包含组件库目录+测试项目+组件库文档+工具库+cli脚手架;使用Monorepo架构风格对项目进行管理

Multirepo痛点:每一个项目都是独立的,也就意味着从环境配置,代码复用、版本管理、项目基建、公共类库等等方面在每一次新开项目的过程中都需要统一管理,这样导致每个项目之间可能存在差异

  • 包管理工具,项目间的依赖管理: Pnpm workspace => 轻量,磁盘空间利用非常高效、包安装速度极快 (lerna配置复杂,舍弃)
  • 开发构建工具:vite+vue3+vitepress (webpack配置复杂,编译时间慢,舍弃)
  • 代码规范:eslint,prettier
  • 测试工具:vitest
  • 代码检测:husky, lint-staged
  1. 开发落地:
  • 组件源码预览:

  • 组件开发,组件库按需引入,打包多种js规范的输出包,

  • 编写文档

  • 组件库开发遇到了哪些难点?

  1. 展示组件源码:写一个简单的vite插件,可以约定一个语法规则,在模块加载的时候通过正则匹配拿到demo组件的路径和名称,同时也可以拿到demo源码;之后就可以通过改写模块属性,把源码内容转成字符串添加到模块中;之后在vpDemo组件中拿到源码字符串sourceCode,就可以进行展示了~

  2. 商品卡片组件封装:通过设计组件属性来动态渲染不同类型的商品卡片

  3. 瀑布流组件封装:先同步渲染两屏的列表,再异步渲染剩下的列表

  • 懒加载: IntersectionObserver 监听图片元素,出现在视图当中开始从瀑布流数据队列的列头中取出一个数据并渲染到当前瀑布流的最低列,如此循环往复实现瀑布流的懒加载。

  • 商品加载重复问题:

    • 在瀑布流渲染过程中,可能出现 DOM 在没有加载完成的情况下,用户再次滑动到底部会再次加载新的一页数据;导致渲染会再次被执行,相当于会存在多个渲染流程同时在执行,可能出现数据重复的情况。
    • 每次渲染可以当做一个异步的请求, 维护一个异步渲染任务队列,在每次执行异步任务时会从队列头部中 shift ,利用 promise.then 并且递归调用该方法,实现有序并且自动执行任务。
  • 定位性能优化:用transform替换绝对定位的left,top,避免触发重排重绘

  • 组件库开发中,设计比如buttton这种基础组件时,有哪些考虑?

  1. 语义清晰:按钮组件应该具有明确的语义,即它应该传达出它的功能或用途。
  2. 可定制性:允许用户通过props(属性)自定义按钮的样式、大小、形状等。提供主题支持,使用户能够根据需要切换按钮的外观。允许用户传入自定义内容,如图标、文本等。
  3. 交互细节:为按钮添加鼠标悬停、点击等状态的样式变化,以提供即时的视觉反馈。对于加载中的按钮,可以添加禁用状态并显示加载指示器。提供声音或震动反馈,以支持视觉障碍用户。
  4. 性能优化:尽量减少按钮的渲染开销,避免不必要的重绘和重排。避免在按钮内部使用复杂的JavaScript逻辑,以保持组件的轻量级。使用CSS的will-change属性来优化可能变化的属性,如变换和动画。
  5. 兼容性:确保按钮组件在不同浏览器和设备上都能正常显示和工作。使用Autoprefixer等工具来确保CSS属性的兼容性。对旧版浏览器提供降级方案或polyfill
  6. 文档和示例:提供详细的文档,说明按钮组件的使用方法、属性和事件。提供丰富的示例,展示如何组合使用不同的属性和内容来自定义按钮。
  7. 测试:编写单元测试来验证按钮组件的基本功能。进行集成测试,确保按钮与其他组件的交互符合预期。进行跨浏览器和设备的测试,确保兼容性和性能
  8. 扩展性:设计按钮组件时,要考虑未来的扩展性。例如,是否支持更多类型的按钮(如开关按钮、下拉菜单按钮等)。使用组合优于继承的原则来设计组件,以便在需要时能够轻松扩展或修改组件。
  • 为什么需要二次封装组件库?
  1. 统一风格:在一个大的项目或者多个相关的项目中,保持一致的界面风格和交互方式是非常重要的。通过二次封装,我们可以定义统一的样式和行为,减少不一致性。
  2. 降低维护成本:当底层的组件库更新时,我们可能需要在项目的多个地方进行修改。但是如果我们有了自己的封装,只需要在封装层面进行更新即可,这大大降低了维护成本。
  3. 增加定制功能:有些时候,我们需要在原有组件库的基础上增加一些特定的功能,如特定的验证、错误处理等。二次封装提供了这样的可能。
  4. 提高开发效率:在一些常用的功能(如表单验证、全局提示等)上,二次封装可以提供更方便的API,提高开发效率。
  • 组件库开发需要考虑哪些因素?
  1. 规划和目标:确定组件库的目标、受众和用途。它将用于解决哪些问题?它将主要服务于哪些用户?
  2. 组件选择:评估需要哪些组件,例如按钮、输入框、导航菜单等。考虑通用的组件以及特定于你的应用程序需求的组件。
  3. 设计原则:组件库应该遵循一致性、可扩展性和易于维护的设计原则。
  4. UI设计:根据产品的需求和品牌设计语言,为组件制定统一的UI风格和设计规范
  5. 设计模式:确定常用的设计模式,如表单提交、编辑等。将这些模式融入组件设计,确保一致性和可用性。
  6. 代码规范:编写组件代码时遵循好的编程实践和代码规范。这可以提高代码质量、减少维护成本并方便其他开发人员使用。
  7. 组件接口:为组件提供清晰、简洁和易于理解的API接口。这将方便其他开发人员更容易地使用和自定义这些组件。
  8. 文档:编写详细的使用说明和文档,提供组件的简介、使用范例、可配置选项等信息。
  9. 测试:使用单元测试、集成测试和端对端测试确保组件的质量、组件在不同浏览器和设备下的兼容性。
  10. 版本控制:使用版本控制系统(如Git)跟踪组件库的开发进度,便于多人协作和跟踪更改历史。
  11. 持续集成和发布:设置自动构建和部署流程,确保组件库的最新版本可以快速、轻松地推送给用户。
  12. 反馈和支持:提供渠道让用户提出问题和反馈,以便持续改进组件库和修复潜在问题。
  • 如何对一个组件库进行测试?

组件库的测试大致可以分为两类:一类是针对组件本身的功能和性能的测试(例如,单元测试、性能测试),另一类是针对组件在集成环境下的行为和性能的测试(例如,集成测试、系统测试)。

  1. 功能测试(单元测试): 通常来说,组件的功能测试可以通过单元测试来完成。单元测试的目的是验证组件的单个功能是否按照预期工作。这通常可以通过编写测试用例来完成,每个测试用例针对一个特定的功能。
  2. 边界测试: 边界测试是一种特殊的功能测试,用于检查组件在输入或输出达到极限或边界条件时的行为。
  3. 响应测试: 响应测试通常涉及到 UI 组件在不同的设备或屏幕尺寸下的行为。这可能需要使用端到端(E2E)测试工具,如 Puppeteer、Cypress 等。
  4. 交互测试: 交互测试也可以通过端到端(E2E)测试工具来完成。
  5. 异常测试: 异常测试用于验证组件在遇到错误或非法输入时能否正确处理。这通常可以通过在测试用例中模拟错误条件来完成。
  6. 性能测试: 性能测试用于验证组件的性能,例如,加载速度、内存消耗等。
  7. 自动化测试: 单元测试、集成测试和系统测试都可以通过自动化测试工具进行。例如,Jest 和 Mocha 可以用于自动化运行 JavaScript 单元测试,Puppeteer 和 Selenium 可以用于自动化运行端到端测试。
  • 组件库如何实现在线主题定制的?
  1. 使用 CSS变量 定义样式: 将组件的样式使用 CSS 变量定义,这样可以通过改变 CSS 变量的值来修改样式。
:root {
  --primary-color: #1890ff;
}
.btn {
  background: var(--primary-color); 
}

/* theme.js */
export default {
  '--primary-color': '#409eff'
}
  1. 提供主题文件进行配置: 让用户可以通过导入自定义的主题文件来覆盖默认样式。
  2. 在线主题编辑器: 提供一个在线工具,用户可以在工具中配置主题,生成主题文件。工具会提交主题配置,服务器端接收后动态编译生成新的样式,并返回给前端。
  3. 前端应用新样式: 前端通过加载服务器返回的 CSS 文件来应用新的主题样式,实现样式更新而无需重新打包。
  4. 持久化主题配置: 将用户主题配置持久化本地存储,这样每次访问都可以应用上次选定的主题。
  • 组件库的渐进升级策略应该怎么设计?

组件库的渐进升级策略通常会涉及到版本控制、向下兼容性、废弃通知以及旧版本的兼容性等多个方面。这种策略的主要目的是在保持库的稳定性和功能性的同时,尽可能地减少对用户的影响。

  1. 版本控制策略: 主版本号.次版本号.补丁版本号
  2. 向下兼容处理: 向下兼容性是指在升级组件库时,保证新版本不会破坏旧版本的功能。
  3. 功能被废弃怎么通知用户升级: 当一个功能或者组件被废弃时,应在库的文档、更新日志以及相关的 API 文档中明确注明。可以在代码中添加警告或者错误信息来提醒用户:
  4. 兼容旧版本的方案: 兼容旧版本的策略取决于特定的需求和资源。一种常见的策略是在主版本升级后,继续维护旧版本的一个分支,以便在必要时进行修复和改进。
  • 组件库的按需加载实现中存在哪些潜在问题,如何解决?

按需加载(也称为代码拆分)是现代前端开发中常见的一种优化手段,可以有效地减少应用的初始加载时间。对于组件库来说,它使用户只加载和使用他们真正需要的组件,而不是加载整个库。

  • babel-plugin-import: 使用如 babel-plugin-import 的 Babel 插件可以在编译时将导入整个库的语句转换为仅导入使用的组件。
  • tree-shaking: Webpack、Rollup 等工具都已经支持了 Tree shaking。在项目的配置中开启 Tree shaking,然后使用 ES Modules 的导入导出语法,即可实现按需加载。

在使用 Tree shaking 的时候,存在副作用(side effects)问题:

比如打包后组件代码中包含了:import "./xxx.css", 这样会使得构建工具无法知道这段代码是否有副作用(也就是会不会用到其它引入的文件中的代码),所以构建的时候就会全量打包代码从而失去 esmodule 的自动按需引入功能。

  • 解决方案:pkg中配置:sideEffects: ['./xxx.css']

  • 组件库中css解决方案有哪些?

  1. 样式和逻辑分离组件的CSS和JS在代码层面上是分离的,开发时写在不同的文件里。在打包时生成独立的逻辑文件和样式文件。
  • 优点:可以直接提供源码,便于主题定制; 适用面广,可以支持不同的框架和技术栈; 支持SSR,样式处理留给使用者。
  • 缺点:使用时需要分别引入逻辑和样式,按需加载实现复杂; 样式文件打包可能存在冗余。
  1. 样式和逻辑结合: 这种方案将CSS和JS打包在一起,输出单一的JS文件; style-components
  • CSS in JS:样式以对象或字符串形式存在在JS中。将CSS打包进JS:通过构建工具,将CSS文件内容注入到JS中。

  • 优点:使用简单,只需要引入JS即可。天然支持按需加载。

  • 缺点:需要额外的runtime,可能影响性能。难以利用浏览器缓存。SSR需要框架额外支持。

  • 设计一个组件库的 CI/CD 和发布流程

  1. 分支管理:开发者在开发新特性或修复 bug 时,应该在新的分支(通常称为 feature 分支)上进行开发。完成开发后,提交一个 pull request 到 main 或 master 分支,并进行代码审查。
  2. 代码检查:使用如 ESLint、Stylelint 等工具进行代码检查,使用 Jest 等工具进行单元测试和覆盖率检查。这些步骤可以在提交代码时或者 pull request 的过程中自动进行。
  3. 版本管理:在合并代码并发布新版本前,需要确认新的版本号,并生成相应的 changelog。可以使用如 standard-version 这样的工具自动化这个过程。
  4. 构建:使用如 Webpack、Rollup 等工具进行构建,生成可以在不同环境(如浏览器、Node.js)下使用的代码。
  5. 发布:将构建好的代码发布到 npm,同时更新文档网站。
  6. 部署:部署到github pages或者自建服务
  • Tree组件如何实现高性能大数据渲染?

Tree组件的核心思路是将原始的嵌套children数据结构平铺成一维数组,然后通过计算每个节点的深度(deep)、层级关系等信息,在渲染时动态计算缩进宽度、连接线等,从而实现树形结构的可视化。

  1. 将原始树形数据平铺为一维数组,便于后续计算
  2. 计算出实际需要渲染的节点数据,过滤隐藏的节点
  3. 利用虚拟列表技术只渲染可视区域的数据,实现大数据量的高效渲染
  4. 使用Web Workers来处理数据处理或计算密集型任务
  • 组件库开发的时候做了哪些性能优化?
  1. 组件的轻量化设计精简组件的DOM结构,避免不必要的嵌套和冗余元素。减少组件的依赖,确保每个组件尽可能独立,减少因为依赖导致的性能损耗。
  2. 按需加载:设计组件库时,考虑提供按需加载的功能,让用户只加载他们实际需要的组件,而不是整个组件库。使用工具或配置,将组件库拆分成多个小模块,并支持动态导入
  3. 组件的复用性:提高组件的复用性,减少重复编写和渲染相同的组件逻辑。
  4. 优化组件的初始化与销毁:优化组件的初始化过程,减少在组件挂载时执行的耗时操作。在组件卸载时,及时清理定时器、事件监听器等资源,避免内存泄漏。
  5. 使用性能分析工具:利用Chrome DevTools、React DevTools等性能分析工具,对组件库进行性能分析,找出性能瓶颈并进行优化。监控组件的渲染时间、内存占用等指标,确保组件性能达到最佳状态。
  6. 优化组件的渲染性能:利用shouldComponentUpdate、React.memo等机制,避免不必要的渲染。对于列表渲染等高频场景,使用虚拟化(virtualization)技术,只渲染视口内的组件。
  7. 优化事件处理:避免在组件内部使用过多的内联事件处理器,可以考虑使用事件委托技术。对于频繁触发的事件,使用防抖(debounce)和节流(throttle)技术,减少事件处理函数的执行频率。
  8. 使用TypeScript:引入TypeScript可以为组件库提供更好的类型检查,减少运行时错误,提高代码质量。TypeScript的类型定义还可以作为文档使用,帮助使用者更好地理解组件的属性和方法。
  9. 提供主题定制功能:设计组件库时,考虑提供主题定制功能,让用户能够根据自己的需求调整组件的样式。通过CSS变量或主题对象等方式,实现主题的动态切换和定制。
  10. 文档与示例的完善:提供详细的文档和示例,帮助使用者更好地理解和使用组件库。文档中应包含组件的性能最佳实践和建议,指导用户在使用组件时避免性能问题。

作为面试官,为什么我推荐组件库作为前端面试的亮点? (opens new window)

# 有了社区

  • 架构:后台管理系统 + 创作者平台 + SEO推广首页 + H5页面 + 社区

  • 不同模块:社区精选,职场问答,求职百科,精选好文

  • 类型:文章,视频,动态,提问,回答

  • 状态:已发布,审核中,未通过

# 创作者平台编辑器

  • MutationObserver监听编辑器容器变化,自动保存;正则动态匹配用户输入内容,样式转换:h1/h2, 列表,引用,代码块,加粗,斜体

  • 自定义编辑器工具栏,图片,投票,格式刷,链接

  • 图片自动拖拽改变大小,添加图片描述

  • 数据保存:md原格式,html格式,纯文本格式

  • 粘贴操作

    • 图片:自动掉接口上传
    • html: 过滤内容,清除粘贴过来的内联样式;
      • 粘贴图片处理:img外链url转内链url, 或者base64 => file文件 => 上传接口获取内链url,最后给img添加点击事件(点击呼起)

base64转换为blob文件window.atob将base64编码解码为byte; new Blob转换成文件,添加文件的type,name,lastModifiedDate属性

Q: 粘贴后部分图片顺序错乱问题,因为原文章的存在 span 标签中嵌套 img 等情况,在编辑器渲染时会造成乱序

拿到粘贴的html文本,遍历,拿到img;如果其父元素不符合规则,更新其父元素dom

格式刷实现流程

  1. 点击格式刷,如果有选中区域且有样式,则保存其样式,按键状态改为选中。
  2. 监听选区变化,如果选中范围且有保存样式,则粘贴样式,并初始化

# 能不能从技术的角度讲一下你工作中负责业务的复杂度?

举两个技术难点:

  1. 编辑器中对于粘贴文本内容的处理:图片,xss攻击,dom顺序错乱,
  2. 编辑器对用户内容的监听,格式转换,dom渲染性能问题(web worker)
  3. react项目重构为vue项目:用户订单系统

# 低代码-访谈表单

低代码平台是一种软件开发工具,旨在帮助开发者快速构建应用程序而无需编写大量的传统代码。它们通常提供了可视化的用户界面,通过拖放、配置和组合预先构建的组件或模块,使开发者能够以更快的速度开发应用程序。

  • 视图渲染模板:数组结构,保存组件id,和页面结构;
{ code: 'field_10001_001', children: [{code: 'field_20001_001' // 系统字段}]},
  • 组件配置模板:{comp_id: {...}}

Q: 表单中存在一些需要显隐判断的字段,怎么在配置模型中设计?

观察者模式,依赖收集;在当前组件配置中添加自身的显隐条件,初始化的时候对依赖组件进行监听,当依赖组件value变化时,通知当前组件进行状态变化

Q: 低代码项目组件存在树状结构,拖拽怎么准确定位?

  1. 组件的唯一标识:每个组件在树状结构中应该有一个唯一的标识(如ID)。这样,在拖拽操作时,可以通过这个标识来跟踪和定位组件。
  2. 拖拽事件的监听:监听拖拽开始(dragstart)、拖拽中(dragover 或 drag)和拖拽结束(drop)的事件。在拖拽开始时,记录被拖拽组件的标识和位置。在拖拽过程中,实时更新组件的位置信息。
  • dragStart:event.dataTransfer.setData('text/plain', id)注入拖拽组件id信息;
  • drop: 当被拖动的元素放置在可放置目标上时触发,通常在目标元素上使用; e.dataTransfer.getData(), 获取本次拖拽信息
  1. 树状结构的更新:当组件被拖拽到新的位置时,需要更新树状结构以反映这一变化。这可能涉及到修改组件的父节点、子节点关系,或者调整组件在兄弟节点中的顺序。
  2. 性能优化:对于大型树状结构,拖拽操作可能会引发大量的计算和渲染工作。因此,需要考虑性能优化措施,如使用虚拟滚动、懒加载等技术来减少不必要的计算和渲染。
  3. 错误处理和边界检查:在拖拽过程中,需要进行错误处理和边界检查,以确保组件不会被拖拽到无效的位置或导致结构错误。例如,可以限制组件只能在特定的区域内拖拽,或者禁止将组件拖拽到不合适的父节点下

低代码开发中遇到过哪些性能问题?做过哪些性能优化?

  1. 主线程耗时操作:主线程中如果存在耗时操作,如复杂的计算、大量的数据处理或网络请求等,会导致界面卡顿,用户体验下降,甚至可能引发程序崩溃。因此,需要避免在主线程中执行耗时操作,或将这些操作移至后台线程执行。
  2. 渲染性能问题:在低代码应用中,组件的渲染性能直接影响到用户体验。如果组件渲染速度慢,或者存在过多的重绘和重排,会导致界面响应迟缓。优化渲染性能的方法包括减少不必要的渲染、使用高效的渲染算法和技术,以及避免频繁的DOM操作。
  3. 数据处理效率:低代码应用通常需要处理大量的数据,包括数据的获取、存储、计算和传输等。如果数据处理效率低下,会导致应用性能下降。因此,需要优化数据处理的流程和方法,如使用合适的数据结构、算法和缓存机制,减少不必要的数据传输和计算。

Q: 低代码中怎么做版本管理?

保存组件的每个版本配置信息

Q: 模块之间如何进行解耦?

把每个模块都写成独立的项目, 通过全局的发布订阅模式来通信; 通过全局数据来共享,而不是通过直接相互传递数据

Q: 怎么支持跨平台?

低代码平台搭建好后最终得到的是一个 json,而这个 json 本身就是以一种通用的语言来描述页面结构、表现和行为的,它和平台无关。要想跨平台、跨框架,只需要做一个适配层去解析这些 json即可。

举个例子来说,我们在解析的过程中肯定会需要创建元素,而 vue 和 react 都有各自的 createElement 方法,那到时候只需要把创建元素的方法换成各框架各自的方法就行了。思路听起来很简单,但是每一种适配起来都很繁琐.

渲染引擎

渲染引擎,简单地说,就是一个工具,它输入是页面信息(json/代码),输出就是用户看到的页面了。每个低代码软件都有自己的渲染引擎。可视化编辑器通过拖拽和配置,生成页面信息,然后使用引擎把页面信息渲染成用户看到的页面,这就是低代码开发应用的大致流程。渲染引擎内置丰富的组件,并支持自定义组件。百度开源的amis引擎,用json来描述页面,例如这个页面对应的信息。

低代码架构分层

协议栈 => 低代码引擎 => 引擎生态 => 低代码平台

画布样式隔离:

  1. iframe:把设计器组件树渲染在 iframe 内,iframe会隔离js跟css,并且iframe尺寸的变化也会触发 @media 查询,是非常理想的实现方式
  2. web component沙箱方式,用 shadow dom 作为画布,把设计器组件树渲染在 shadow dom 内。这样的实现方式,性能跟div方式差不多,还可以有效隔离js上下文跟css样式;但不能模拟浏览器大小,它的大小改变也不能触发无法触发@media 查询。

# 用户增长

高并发:在同时或者极短时间内,有大量的请求到达服务端,每个请求都需要服务端消耗资源去进行处理。

怎么处理秒杀页面的瞬时高并发?

  • 页面静态化:用户浏览商品等常规操作,并不会请求到服务端。只有到了秒杀时间点,并且用户主动点了秒杀按钮才允许访问服务端。这样能过滤大部分无效请求。

  • CDN加速: 让用户就近获取所需内容,降低网络拥塞,提高用户访问响应速度和命中率。让用户最快访问到活动页面。

  • 使用浏览器的缓存功能,减少访问服务器,js、css、图片

  • 压缩文件传输,减少网络流量

  • 异步请求,分批获取数据

  • 给秒杀按钮的点击事件做节流处理

  • 后端高并发:redis缓存 > mysql数据库

  • 后端限流:为了防止某个用户,请求接口次数过于频繁,可以只针对该用户做限制。对同一ip限流。

# 微博电商

  • 商家侧需求:微博用户 => 申请成为商家 => 创建小店 => 小店配置 => 创建商品 => 商品上线

  • 用户侧需求:浏览商品 => 下单 => 购买 => 售后

# 互爱町web3

一个web3办公室小游戏:让用户从NFT的创建,售卖中了解web3

web3是去中心化生态系统网络,此系统会包含一系列开源协议,能够为应用程序的开发者提供构建模块,而基于区块链技术构建的Web3.0平台和应用程序不由传统的中心化企业所有,网络所赋予的权利和资产归开发者和用户所有。

区块链是去中心化的分布式记账系统。

智能合约就是运行在以太坊区块链网络上简单程序,智能合约是用 Solidity 语言编写的。

互爱町:

  1. 用户信息模块:去中心化登录、离线钱包生成、虚拟货币交换(燃料费充值,互助币,互通币)
  2. NFT市集模块:NFT的创建,展示,售卖
  3. 办公室模块:社区发帖,评论,办公区道具摆放
  4. 世界杯竞猜:输入密码 => 查询是否有燃料费 => 查询互通币余额 => 查询授权是否充足 => 不足则授权 => 投注

货币流程逻辑:

  1. 注册成功,赠送3000互助币;互助币可以充值燃料费,可以兑换互通币;
  2. 互助币主要用于中心化交易,如道具购买;互通币主要用于链上交易,如NFT铸造,购买;链上每次交易需要燃料费;
  3. 可以通过售卖NFT,赚取互通币;可以通过打卡,发表帖子,评论,赚取互助币;

NFT铸造流程

  1. 设置NFT属性:名称,类型,价格,份数等
  2. 查询燃料费是否充足;
  3. 查询互助币是否充足;
  4. NFT商品信息上传云端备份;
  5. 输入密码,生成钱包;
  6. 调链上合约方法:授权,铸造NFT

nft购买流程:查询是否有燃料费 => 查询HTB余额 => 查看已拥有的nft份数 => 代币授权 => nft购买

  • 技术难点:
  1. 本地离线钱包和去中心化登录的实现
  • 用户生成助记词,输入密码后,把助记词和用户地址加密,保存在本地。
  • 通过加密私钥 + 密码生成钱包,创建基础合约;
  • 连接区块链网络,通过传入合约地址和合约ABI生成合约实例,调用已经部署的合约方法
  1. 智能合约的自动化部署:
  • 创建智能合约
  • 选择网络,输入助记词,合约入参,点击部署
  • 合约编译:服务端拿到合约后,用solc.js进行编译,拿到合约的abi、bytecode等信息
  • 合约部署:web3.js连接区块链网络(助记词+网络地址),传入上一步得到的合约编译信息,调用合约实例的deploy方法,开始部署;
  • websocket监听部署情况,及时反馈部署进度给前端;部署成功后,会返回一个合约地址;
  • 之后前端通过生成的合约地址,就可以调用该合约了

# 家长端订单项目的重构

vue重构react项目

原因:

  1. 之前的代码多人开发,不规范,业务逻辑混乱,
  2. vue上手成本低,团队推行vue,
  • 首屏优化:ssr, 图片懒加载,虚拟滚动,骨架屏,异步子线程,路由懒加载减少入口js体积

  • 打包优化:多进程打包,静态资源cdn, 图片压缩,js压缩,js拆包抽离,

  • 项目优化:模块化,组件封装,引入监控,

# 脚手架开发会遇到哪些问题?

  1. 配置复杂性:脚手架通常需要配置大量的参数和选项,以满足不同项目的需求。这些配置可能涉及构建、测试、部署等多个环节,配置不当可能导致项目出错或性能不佳。
  2. 版本兼容性:不同版本的依赖库和工具可能存在兼容性问题,导致脚手架无法正常工作。开发者需要关注相关依赖的版本变化,并及时更新或调整配置。
  3. 性能优化:打包后的文件大小和加载速度对项目的性能至关重要。开发者需要关注代码拆分、懒加载、压缩优化等技巧,以提高项目的性能。
  4. 可维护性:随着项目的迭代和更新,脚手架也需要不断维护和升级。如何保持脚手架的稳定性和可扩展性是一个挑战。
  5. 社区支持和文档:如果使用的脚手架没有良好的社区支持和详细的文档,那么在遇到问题时可能会难以找到解决方案。

不用打包,require es5语法,纯粹的node.js代码,直接发布

  • child_process.exec('git clone ...') 直接clone远程模板

# 自研前端监控的架构设计?

一篇讲透自研的前端错误监控 (opens new window)

为什么选择自研:成本、安全、扩展性

  1. 分析需要实现功能:错误收集 => 错误上报 => 采集聚合 => 可视分析 => 监控报警
  2. 系统设计:
  • 搜集上报端(SDK):输入是所有错误,输出是捕获上报错误;核心是处理不同类型错误的搜集工作

    • 错误类型: 常见JS执行错误(SyntaxError, TypeError, ReferenceError, promise错误,...)、异步接口请求错误, vue错误,react错误
    • 搜集错误: try/catch, window.onerror, window.addEventListener, unhandledrejection,Vue.config.errorHandler
    • 行为搜集:UI行为:点击、滚动、聚焦/失焦、长按;浏览器行为:请求、前进/后退、跳转、新开页面、关闭;控制台行为:log、warn、error
    • 上报:使用new Image进行接口上报, 选用了1x1的透明GIF:​可以进行跨域、不会携带cookie、不需要等待服务器返回数据;异步加载sdk,异步上报
  • 采集聚合端:输入是接口接收到的错误记录,输出是有效的数据入库。核心功能需要对数据进行清洗,顺带解决了过多的服务压力。

    • 错误标识:错误条目id,定位同个错误事件,设备信息,uid
    • 错误过滤: 域名过滤、重复的错误避免上报的次数超过阈值
    • 错误接收:错误记录(对字段进行check)、削峰机制(每秒设置2000的阈值)、采样处理​(只采集 20%)
    • 错误存储:存储方案:阿里云日志服务 - Log Service(SLS); 日志延时 / 查询延时 / 查询能力:关键词查询、条件组合查询、模糊查询、数值比较、上下文查询 / 扩展性:快速应对百倍流量上涨 / 成本:每 GB 费用。
  • 可视分析端(可视化平台)

    • 首页图表,可选1天、4小时、1小时等等,聚合错误数,根据1天切分24份来聚合。
    • 首页列表,聚合选中时间内的数据,展示错误文件、错误key、事件数、错误类型、时间、错误信息。
    • 错误详情,事件列表、基本信息、设备信息、设备占比图表(见上面事件列表的图)。
    • 错误作者排行榜, 通过钉钉日报来提醒对应人员处理: webpack打包通过git命令把作者和作者邮箱、时间打包在头部; 在可视化服务中,去请求对应的报错url匹配到对应作者,返回给展示端。
    • SourceMap
  • 错误报警

    • 每条业务线设置自己的阈值、错误时间跨度,报警轮询间隔
    • 通过钉钉hook报警到对应的群
    • 通过日报形式报出错误作者排行榜

# 前端监控平台/监控SDK的架构设计?

说说前端监控平台/监控SDK的架构设计和难点亮点? (opens new window)

整体架构设计

  • 应用层:Core, Web, SDK-Core, SDK-Plugin
  • 接入层:在接入层经过 削峰限流 、数据清洗 和 数据加工后,将原始日志存储于 ES 中,再经过数据聚合 后,将聚合的数据持久化存储 于 MySQL ,最后提供 RESTful API 提供给监控平台调用;
  • 平台服务:监控查询,日志查询,报警
  • 监控平台:数据详情,性能瓶颈分析,异常分析,可视化报表

应用层的SDK架构设计

为支持多平台、可拓展、可插拔的特点,整体SDK的架构设计是 内核+插件 的插件式设计;每个 SDK 首先继承于平台无关的 Core 层代码。然后在自身SDK中,初始化内核实例和插件;

- packages/
    - core/ // 无关平台的公共方法,生命周期管理...
    - web/ // web平台sdk
        - core/ // 平台相关公共方法: 参数初始化、数据格式化、上报
        - plugins/ // 插件:性能采集、错误捕获、...
    - wx/ // 微信平台sdk..
    ...

Q: SDK 如何设计成多平台支持?

在前端监控的领域里,我们可能不仅仅只是监控一个 web环境 下的数据,包括 Nodejs、微信小程序、Electron 等各种其余的环境都是有监控的业务需求在的;

解决方案:通过插件化对代码进行组织

  1. Core 来管理 SDK 内与平台无关的一些代码,比如一些公共方法(生成mid方法、格式化);
  2. 然后每个平台单独一个 SDK;去继承 core 的类;SDK 内自己管理SDK特有的核心方法逻辑,比如上报、参数初始化
  3. 最后就是 Plugins 插件,每个SDK都是由 内核+插件 组成的,我们将所有的插件功能,比如性能监控、错误监控都抽离成插件
  • 优点:每个平台分开打包,每个包的体积会大大缩小;代码的逻辑更加清晰自恰
  • 打包:打包上线时,我们通过修改 build 的脚本,对 packages 文件夹下的每个平台都单独打一个包,并且分开上传到 npm 平台;

Q: SDK 如何方便的进行业务拓展和定制?

SDK 内部的一个架构设计:内核+插件 的设计:

  1. 内核里是SDK内的公共逻辑或者基础逻辑;比如数据格式化和数据上报是底下插件都要用到的公共逻辑;而配置初始化是SDK运行的一个基础逻辑;
  2. 插件里是SDK的上层拓展业务,比如说监听js错误、监听promise错误,每一个小功能都是一个插件;内核和插件一起组成了 SDK实例 Instance,最后暴露给客户端使用;

我们需要拓展业务,只需要在内核的基础上,不断的往上叠加 Monitor 插件的数量就可以了;

Q: SDK 在拓展新业务的时候,如何保证原有业务的正确性?

可以引入单元测试,并对每一个插件,每一个内核方法,都单独编写测试用例,在覆盖率达标的情况下,只要每次代码上传都测试通过,就可以保证原有业务的一个稳定性。

Q: SDK 如何实现异常隔离以及上报?

异常隔离机制:我们对每一个插件模块,都单独的用trycatch包裹起来,然后当抛出错误的时候,进行数据的封装、上报;

它实现了:

  1. 当SDK产生异常时不会影响主业务的流程;
  2. 当SDK产生异常时进行数据的封装、上报;
  3. 出现异常后,中止 SDK 的运行,并移除所有的监听;

Q: SDK 如何实现服务端时间的校对?

我们通过 JS 调用 new Date() 获取的时间,是我们的机器时间;也就是说:这个时间是一个随时都有可能不准确的时间;

比如说 API全链路监控, 需要准确的时间

  • 方案1: 可以在初始化SDK的时候,发送一个简单的请求给上报服务器,获取返回的 Date 值后计算 Diff差值 存在本地;这样子就可以提供一个 公共API,来提供一个时间校对的服务,让本地的时间 比较趋近于 服务端的真实时间;(但还会有一个单程传输耗时的误差)

  • 方案2:我们可以让后端服务在返回时, 在响应头里带上后端服务执行完毕所消耗的时间server-timing;我们取到数据后,将 ttfb耗时 减去返回的 server-timing 再除以 2;就是单程传输的耗时;那这样我们上文的计算中差的 单程传输耗时的误差 就可以补上了;

TTFB是Time to First Byte的缩写, 首字节时间,它测量的是从用户或客户端发出HTTP请求到客户端的浏览器接收到页面的第一个字节的持续时间

Q: SDK 如何实现会话级别的错误上报去重?

在用户的一次会话中,如果产生了同一个错误,那么将这同一个错误上报多次是没有意义的

我们传入的参数,是 错误信息、错误行号、错误列号、错误文件等可能的关键信息的一个集合,这样保证了产生在同一个地方的错误,生成的 错误mid 都是相等的;这样子,我们才能在错误上报的入口函数里,做上报去重

Q: SDK 如何生成错误唯一 ID?

错误ID,它的作用分两种:

  1. 客户端用以实现会话级别的上报去重
  2. 服务端用以实现相同错误的数据聚合

根据错误堆栈,解析出了 错误文件名、错误函数名、错误行号、错误列号等信息;我们再利用上述的所有信息,最终生成一个 hash值,这个值就是能够完全的描述这个错误的唯一性的 ID

Q: SDK 采用什么样的上报策略?

上述的几个上报方式:

  1. 信标(Beacon API): Beacon API 用于发送异步和非阻塞请求到服务器。这类请求不需要响应。与 XMLHttpRequest 或 Fetch API 请求不同,浏览器会保证在页面卸载前,将信标请求初始化并运行完成。Beacon API 主要的使用场景是将分析数据发送给服务器
  2. Ajax(XMLHttpRequest 和 fetch)
  3. Image(GIF、PNG): 我们可以以向服务端请求图片资源的形式,像服务端传输少量数据,这种方式不会造成跨域;

上报方案:可采用 sendBeacon + xmlHttpRequest 降级上报的方式,当浏览器不支持 sendBeacon 或者 传输的数据量超过了 sendBeacon 的限制,我们就降级采用 xmlHttpRequest 进行上报数据;

  • 为什么不用 Image?那跨域怎么办呀?

Image 是以GET方式请求图片资源的方式,将上报数据附在 URL 上携带到服务端,而URL地址的长度是有一定限制的。浏览器、服务器、代理服务器都对 URL 长度有要求。有的浏览器要求URL中path部分不超过 2048,这就导致有些请求会发送不完全。至于跨域问题,作为接受数据上报的服务端,允许跨域是理所应当的;

上报时机:

  1. PV、错误、用户自定义行为 都是触发后立即就进行上报;
  2. 性能数据 需要等待页面加载完成、数据采集完毕后进行上报;
  3. API请求数据 会进行本地暂存,在数据量达到10条(自拟)时触发一次上报,并且在页面可见性变化、以及页面关闭之前进行上报;
  4. 如果你还要上报 点击行为 等其余的数据,跟 API请求数据 一样的上报时机;

上报优化:

  1. 启用 HTTP2,在 HTTP1 中,每次日志上报请求头都携带了大量的重复数据导致性能浪费。HTTP2头部压缩能有效减少请求头的大小;
  2. 服务端可以返回 204 状态码,省去响应体;
  3. 使用 requestIdleCallback ,这是一个较新的 API,它可以插入一个函数,这个函数将在浏览器空闲时期被调用。这使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件,如果不支持的话,就使用 settimeout

Q: 平台数据如何进行 削峰限流?

假设某一个时间点突然间流量爆炸,无数的数据向服务器访问过来,这时如果没有一个削峰限流的策略,很可能会导致机器Down掉。

现在做流量整形的方法很多,最常见的就是三种:

  1. 计数器算法:计数器算法就是单位时间内入库数量固定,后面的数据全部丢弃;缺点是无法应对恶意用户;
  2. 漏桶算法:漏桶算法就是系统以固有的速率处理请求,当请求太多超过了桶的容量时,请求就会被丢弃;缺点是漏桶算法对于骤增的流量来说缺乏效率;
  3. 令牌桶算法:令牌桶算法就是系统会以恒定的速度往固定容量的桶里放入令牌,当请求需要被处理时就会从桶里取一个令牌,当没有令牌可取的时候就会据拒绝服务;

方案

计数器 + 令牌桶 => 计数器能够削峰,限制最大并发数以保证服务高可用; 令牌桶实现流量均匀入库,保证下游服务健康

  1. 首先从外部来的流量是我们无法预估的,假设如上图我们有三个 服务器Pod ,如果总流量来的非常大,那么这时我们通过计数器算法,给它设置一个很大的最大值
  2. 这样经过总流量的计数器削峰后,再到中心化的令牌桶限流:通过 redis 来实现,我们先做一个定时器每分钟都去令牌桶里写令牌,然后单机的流量每个进来后,都去 redis 里取令牌,有令牌就处理入库;没有令牌就把流量抛弃;
  3. 这样子我们就实现了一个 单机的削峰 + 中心化的限流,两者一结合,就是解决了小流量应用限流后没流量的问题,以及控制了入库的数量均匀且稳定;

Q: 平台数据为什么需要 数据加工?

当我们的数据上报之后,因为 IP地址 是在服务端获取的嘛,所以服务端就需要有一个服务,去统一给请求数据中加上 IP地址 以及 IP地址 解析后的归属地、运营商等信息;根据业务需要,还可以加上服务端服务的版本号 等其余信息,方便后续做追踪;

Q: 平台数据为什么需要 数据清洗、聚合?

  • 数据清洗是为了经由白名单、黑名单过滤等的业务需要,还有避免已关闭的应用数据继续入库;
  • 数据聚合是为了将相同信息的数据进行抽象聚合,以便查询和追踪;

还可以将抽象聚合出来的 issue ,关联于公司的 缺陷平台(类bug管理平台) ,实现 issue追踪直接自动贴bug到负责人头上 等业务功能;

Q: 平台数据如何进行 多维度追踪?

会对每一个用户(user),会去生成一个 用户id(uid) ;并对每一次会话(session),生成一个 会话id(sid)

uid 和 sid 都是28位的随机ID,sid 和 uid 都在初始化时生成,不同的是,因为 sid 的生命周期只在一次会话之中(关闭页签之前),所以 sid 我们存放在 sessionStorage 中,而 uid 我们存放在 cookie 里,过期时间设置六个月;

每次SDK初始化时,都先去 cookie 和 sessionStorage 里取 uid 和 sid,如果取不到就重新生成一份;并且在每次数据上报时,都将这些 id 附带上去;

Q: 代码错误如何进行 源码映射?

通过解析错误堆栈,得到了错误的文件、行列号等信息,可以通过对 sourcemap 可以对源码进行映射,定位错误源码的位置;

注意:在生产环境我们是不可以将 sourcemap 文件发布上线的,我们可以通过手动上传到监控平台的形式去进行错误的分析定位

Q: 如何设计监控告警的维度?

  • 宏观告警 更加关注的是:一段时间区间内,新增异常的数量、比率是否超过了阈值;如果超过了那就进行告警;
  • 微观告警 更加关注的是:是否有新增的、且未解决的异常

只要出现的新异常,它的 uid 是当前已激活的异常中全新的一个;那么就进行告警,通知大群、通知负责人、在缺陷平台上新建 bug 指派给负责人;

Q: 监控告警如何指派给代码提交者?

如上文提到,我们当发现新 bug 产生时,我们可以将这个 bug 指派给负责人;这里其实还可以做的更细致一点,我们可以做一个 处理人自动分配 的机制;

处理人自动分配,分配给谁呢?捕获错误时上报了错误的位置,也就是源码所在;那么我们只需要找到最近一次提交这行代码的人就可以了;

Git Blame: 那么找出 出错行author 的原理其实就是 Git Blame:

// git blame -L <n,m> <file>
// n是起始行,m是结束行,file是指定文件
// eg:
git blame -L 2,2 LICENSE
// 返回:commitID (代码提交作者 提交时间 代码位于文件中的行数) 实际代码

这样子,我们就可以获取到具体的提交记录是哪次,并且提交者是谁;可以利用 Gitlab Open-api 在服务端集成

# SSR原理

彻底理解服务端渲染 - SSR原理 (opens new window)

所谓同构,通俗的讲,就是一套代码在服务器上运行一遍,到达浏览器又运行一遍。服务端渲染完成页面结构,浏览器端渲染完成事件绑定

SEO(Search Engine Optimization),指的是利用搜索引擎的规则提高网站在有关搜索引擎内的自然排名。

以vue项目为例~

  • 构建服务端代码:服务端代码构建结束后,需要将构建结果运行在nodejs服务器上。
    1. 不需要编译CSS
    2. 构建的目标的运行环境是commonjs
    3. 不需要代码切割,nodejs将所有代码一次性加载到内存中更有利于运行效率

require('vue-server-renderer/server-plugin')

  • 构建客户端代码: 在服务端渲染中,HTML是由服务端渲染的,也就是说,我们要加载那些JavaScript脚本,是服务端决定的,因为HTML中的script标签是由服务端拼接的,所以在客户端代码构建的时候,我们需要使用插件,生成一个构建结果清单,这个清单是用来告诉服务端,当前页面需要加载哪些JS脚本和CSS样式表
    1. 使用vue-server-renderer提供的client-server,主要作用是生成构建加过清单vue-ssr-client-manifest.json,服务端在渲染页面时,根据这个清单来渲染HTML中的script标签(JavaScript)和link标签(CSS)。
    2. 会去除所有关于客户端生成的html配置,因为已经交给服务端生成

require('vue-server-renderer/client-plugin')

  • 模板组件共享: 为了实现模板组件共享,我们需要将获取 Vue 渲染实例写成通用代码

  • http服务:所有东西的准备好之后,我们需要修改nodejs的HTTP服务器的启动文件。首先,加载服务端代码server-entry.js的webpack构建结果:

// 服务端打包生成清单
const serverBundle = path.resolve(process.cwd(), 'serverDist', 'vue-ssr-server-bundle.json');
// 客户端打包生成清单:包含了所有需要在客户端运行的js脚本和css静态资源
const clientManifestPath = path.resolve(process.cwd(), 'dist', 'vue-ssr-client-manifest.json');
const clientManifest = JSON.parse(fs.readFileSync(clientManifestPath, 'utf-8'));
const template = fs.readFileSync(path.resolve(__dirname, 'index.html'), 'utf-8'); // 入口模板
// 生成html渲染内容
const renderer = createBundleRenderer(serverBundle, {
    template, // 使用HTML模板
    clientManifest // 将客户端的构建结果清单传入
});

在HTML模板中,通过传入的客户端渲染结果clientManifest,将自动注入所有link样式表标签,而占位符将会被替换成模板组件被渲染后的具体的HTML片段和script脚本标签。

HTML准备完成后,我们在server中挂起所有路由请求; 同时把客户端的构建结果文件挂载到服务器的静态资源目录;

由于服务器已经渲染好了 HTML,我们显然无需将其丢弃再重新创建所有的 DOM 元素。相反,我们需要"激活"这些静态的 HTML,然后使他们成为动态的(能够响应后续的数据变化)。

  • 路由的共享和同步:router准备好了之后,修改server-entry.js,将请求的URL传递给router,使得在创建app的时候可以根据URL匹配到对应的路由,进而可知道需要渲染哪些组件

  • 数据模型的共享与状态同步:我们在访问页面的时候,还需要获取需要渲染的数据,并且渲染成HTML,也就是说,在渲染HTML之前,我们需要将所有数据都准备好,然后传递给renderer。

    1. 创建STORE实例: 与createApp类似,创建一个createStore.js,用来实例化store,同时提供给客户端和服务端使用;
    2. STORE连接组件:需要在路由的组件中放置数据预取逻辑函数。在组件中自定义一个静态函数asyncData,由于此函数会在组件实例化之前调用,所以它无法访问 this。需要将 store 和路由信息作为参数传递进去
    3. 服务端获取数据:在服务器的入口文件server-entry.js中,我们通过URL路由匹配 router.getMatchedComponents()得到了需要渲染的组件,这个时候我们可以调用组件内部的asyncData方法,将所需要的所有数据都获取完后,传递给渲染器renderer上下文。
    4. 将state存入context后,在服务端渲染HTML时候,也就是渲染template的时候,context.state会被序列化到window.__INITIAL_STATE__中,方便客户端激活数据
    5. 客户端激活状态数据: 服务端预请求数据之后,通过将数据注入到组件中,渲染组件并转化成HTML,然后吐给客户端,那么客户端为了激活后端返回的HTML被解析后的DOM节点,需要将后端渲染组件时用的store的state也同步到浏览器的store中,保证在页面渲染的时候保持与服务器渲染时的数据是一致的,才能完成DOM的激活

    在服务端的渲染中,state已经被序列化到了window.__INITIAL_STATE__, 我们需要做的就是将这个window.__INITIAL_STATE__在客户端渲染之前,同步到客户端的store中,通过使用store的replaceState函数,将window.__INITIAL_STATE__同步到store内部,完成数据模型的状态同步。

总结:

  1. 当浏览器访问服务端渲染项目时,服务端将URL传给到预选构建好的VUE应用渲染器,渲染器匹配到对应的路由的组件之后,执行我们预先在组件内定义的asyncData方法获取数据,并将获取完的数据传递给渲染器的上下文,利用template组装成HTML,并将HTML和状态state一并吐给前端浏览器;
  2. 浏览器加载了构建好的客户端VUE应用后,将state数据同步到前端的store中,并根据数据激活后端返回的被浏览器解析为DOM元素的HTML文本,完成了数据状态、路由、组件的同步,同时使得页面得到直出,减少了白屏时间,有了更好的加载体验,同时更有利于SEO。

Q: 异步数据的服务端渲染方案(数据注水与脱水)?

客户端和服务端的运行流程,当浏览器发送请求时,服务器接受到请求,这时候服务器和客户端的store都是空的,紧接着客户端执行componentDidMount生命周期中的函数,获取到数据并渲染到页面,然而服务器端始终不会执行componentDidMount,因此不会拿到数据,这也导致服务器端的store始终是空的。换而言之,关于异步数据的操作始终只是客户端渲染。

组件配置了一个loadData参数,这个参数代表了服务端获取数据的函数。每次渲染一个组件获取异步数据时,都会调用相应组件的这个函数。

让两个store的数据同步变化: 在服务端获取获取之后,在返回的html代码中加入window.context = {state: ${JSON.stringify(store.getState())}}; 这叫做数据的注水操作,即把服务端的store数据注入到window全局环境中。

接下来是脱水处理,换句话说也就是把window上绑定的数据给到客户端的store,可以在客户端store产生的源头进行:

// store/index.js

//客户端的store创建函数
export const getClientStore = () => {
  const defaultState = window.context ? window.context.state : {};
  return createStore(reducer, defaultState, applyMiddleware(thunk));
}

// 组件中处理
componentDidMount() {
  //判断当前的数据是否已经从服务端获取
  //要知道,如果是首次渲染的时候就渲染了这个组件,则不会重复发请求
  //若首次渲染页面的时候未将这个组件渲染出来,则一定要执行异步请求的代码
  //这两种情况对于同一组件是都是有可能发生的
  if (!this.props.list.length) {
    this.props.getHomeList()
  }
}

# vue-ssr项目SSR是如何运作的?

Vue SSR深度剖析 (opens new window)

  • 编译阶段

vue-ssr是同构框架,即我们开发的同一份代码会被运行在服务端和客户端两个环境中, vue-ssr提供的方式是配置两个入口文件(entry-client.js、entry-server.js),通过webpack把你的代码编译成两个bundle。

两个入口的编译方式可以很方便的做两个环境的差异化代码抹平:

  1. 客户端入口vue实例化之后执行挂载dom的代码,服务端入口的vue则只需要生成vue对象即可 。
  2. 一些不兼容ssr的第三方库或者代码片段,我们可以只在客户端入口中加载 。
  3. 即使通用代码我们也可以通过打包工具做到两个运行环境的差异化。比如最常见的在应用中发起请求时,在客户端我们经常使用axios来发起请求,在服务端虽然也兼容axios,但是服务端发起的请求并不需要和客户端一样走外网请求,服务端的接口网关或者鉴权方式和客户端也不一定相同。这种情况我们可以通过webpack的resolve.alias配置实现两个环境引用不同模块。
  4. 在服务端的代码我们不需要做code split,甚至我们项目中所有引入的依赖库,也并不需要打包到bundle中。因为在node运行环境中,我们的依赖库都可以通过require在运行时加载进来。
// Server Bundle 
// vue-ssr-server-bundle.json:
{ 
  "entry": "static/js/app.80f0e94fe005dfb1b2d7.js", 
  "files": { 
    "static/js/app.80f0e94fe005dfb1b2d7.js": "module.exports=function(t...", 
    "static/js/xxx.29dba471385af57c280c.js": "module.exports=function(t..." 
  } 
} 


// Client Bundle
// 许多静态资源...
// vue-ssr-client-manifest.json:
{ 
  "publicPath": "//cdn.xxx.cn/xxx/", 
  "all": [ 
    "static/js/app.80f0e94fe005dfb1b2d7.js", 
    "static/css/app.d3f8a9a55d0c0be68be0.css"
  ], 
  "initial": [ 
    "static/js/app.80f0e94fe005dfb1b2d7.js",
    "static/css/app.d3f8a9a55d0c0be68be0.css"
  ], 
  "async": [ 
    "static/js/xxx.29dba471385af57c280c.js" 
  ], 
  "modules": { 
    "00f0587d": [ 0, 1 ] 
    ... 
    } 
} 

Server Bundle中包含了所有要在服务端运行的代码列表,和一个入口文件名。

Client Bundle包含了所有需要在客户端运行的脚本和静态资源,如:js、css图片、字体等。还有一份clientManifest文件清单,清单中initial数组中的js将会在ssr输出时插入到html字符串中作为preload和script脚本引用。async和modules将配合检索出异步组件和异步依赖库的js文件的引入,在输出阶段我们会详细解读。

  • 初始化阶段

ssr应用会在node启动时初始化一个renderer单例对象,renderer对象由vue-server-renderer库的createBundleRenderer函数创建,函数接受两个参数,serverBundle内容和options配置

// 生成html渲染内容
const renderer = createBundleRenderer(serverBundle, {
    template, // 使用HTML模板
    clientManifest // 将客户端的构建结果清单传入
});
// 静态资源
app.use(express.static(path.resolve(process.cwd(), 'dist')));
// 路由
app.get('*', function(req, res) {
    const context = {
        url: req.url
    };
    // 转成html字符串
    renderer.renderToString(context, (err, html) => {
        if (err) {
            console.log(err);
            res.send('500 server error');
            return;
        }
        res.send(html);
    })
});

初始化完成,当用户发起请求时,renderer.renderToString将完成vue组件到html的过程

  • 渲染阶段

当用户请求达到node端时,调用renderer.renderToString函数并传入用户上下文context,context对象可以包含一些服务端的信息,比如:url、ua等等,也可以包含一些用户信息

context对象内容(除了context.state和模板中的占位字段)并不会被输出到前端

在初始化阶段第一步创建的templateRenderer,它负责html的组装,其中renderStyles、renderResourceHints、renderState、renderScripts分别是生成页面需要加载的样式、preloadprefetch资源、页面state(比如vuex的状态state,需要在服务端给context.state赋值才能输出)、脚本文件引用的内容。

renderer对象负责把vue对象递归转为vnode,并把vnode根据不同node类型调用不同渲染函数最终组装为html。

  • 内容输出阶段

在上一个阶段我们已经拿到了vue组件渲染结果,它是一个html字符串,在浏览器中展示页面我们还需要css、js等依赖资源的引入标签和我们在服务端的渲染数据,这些最终组装成一个完整的html报文输出到浏览器中。

页面所需要的脚本依赖我们通过用户上下文contextrenderStyles、renderResourceHints、renderState、renderScripts这些函数分别获得, 接下来可以用我们自己熟悉的模板引擎来渲染出最终的html报文

  • 客户端阶段

当客户端发起了请求,服务端返回渲染结果和css加载完毕后,用户就已经可以看到页面渲染结果了,不用等待js加载和执行。服务端输出的数据有两种,一个是服务端渲染的页面结果,还有一个在服务端需要输出到浏览器的数据状态

这里的数据状态可能是组件创建过程中产生的数据,这些数据需要同步给浏览器,否则会造成两端组件状态不一致。我们一般会使用vuex来存储这些数据状态,并在渲染完成后把vuex的state复制给用户上下文的context.state

// 在组装html阶段可以通过renderState生成输出内容:
<script>window.__INITIAL_STATE__={"data": 'xxx'}</script>

// 当客户端开始执行js时,我们可以通过window全局变量读取到这里的数据状态,并替换到自己的数据状态:
store.replaceState(window.__INITIAL_STATE__);

之后在我们调用$mount挂载vue对象时,vue会判断mount的dom是否含有data-server-rendered属性,如果有表示该组件已经经过服务端渲染了,并会跳过客户端的渲染阶段,开始执行之后的组件生命周期钩子函数。

之后所有的交互和vue-router不同页面之间的跳转将全部在浏览器端运行。

# SSR的几点优化

  1. 合理利用缓存: vue-ssr很快,但它毕竟不是常规的渲染引擎拼接字符串或者静态页面的输出。所以ssr的页面在访问流量比较大时要好好利用缓存
  2. 建议在服务端只渲染首屏的内容,尽量减少不必要的运算。比如列表的场景,我们一页的内容可能是十条,但是用户在一屏的大小中最多只能看到五条,那我们在服务端只渲染五条内容,剩下的内容可以在浏览器端异步渲染。
  3. 不要让ssr在服务端执行一些密集cpu的运算,这条同样适用于任何nodejs应用,任何密集cpu的运算都会拖慢整个应用的响应速度。
  4. 在服务端调用后端接口或者查询数据库时,尽量把请求超时时间控制在一个合理的范围,因为一旦后端服务大量出现超时异常,减少我们请求的超时时间,及时断开请求将避免服务资源被快速沾满。
  5. 合理利用dns-prefetch、preload和prefetch加速页面资源下载速度,preload和prefetch在我们配置了template和inject时vue会帮我们自动插入。
  • preload:告知浏览器该资源会在当前页面用到,浏览器会在解析dom树的同时非阻塞的下载该资源,在页面解析完成请求该资源时立即返回,并且通过标签的as属性浏览器会给不同类型的资源标识不同的加载优先级,比如css相关的资源会比js和图片的优先级更高。
  • prefetch:告知浏览器该资源可能会在页面上用到,浏览器会在空闲时机预下载,并不保证一定能预下载。
  • dns-prefetch:告知浏览器这些域名请帮我们开始dns的解析工作,待页面解析完成加载这些域名的资源时不用再执行dns解析。
  1. 非阻塞式的脚本加载: 这个在我们配置了template和inject后vue也会自动帮我们的script加载脚本加上defer属性,script有两种属性defer和async
  2. 合理定义组件边界: 不要定义不必要的组件,组件的粒度要把控好,太细粒度的组件定义没有意义。

# AI

AI大模型是指具有巨大参数量的深度学习模型,通常包含数十亿甚至数万亿个参数

AI大模型(如深度学习模型)的原理是基于神经网络和大量数据的训练。这些模型通过模拟人脑的神经元结构,对输入数据进行多层抽象和处理,从而实现对复杂任务的学习和预测

人工智能模型的参数就是它们的大脑神经元,它们存储了模型从数据中学习到的知识和经验,也决定了模型的智能和性能。参数越多,神经元越多,模型就越复杂,也越强大。

大模型”应该是基于具有超级大规模的、甚至可以称之为“超参数”的模型,需要大量的计算资源、更强的计算能力以及更优秀的算法优化方法进行训练和优化。大型模型:1亿 – 10亿个参数; 极大型模型:≥ 10亿个参数

AI大模型上下文长度是指AI模型在生成预测或生成文本时,所考虑的输入文本的大小范围。更长的上下文长度可以让模型看到更多的信息,从而做出更准确、流畅、创造性的预测或生成

LLaMA2-70b中70b是什么?

70b代表的是模型参数大小。这个b是指十亿的意思,LLaMA2-70b就是说模型有700亿个参数。M:百万,K:千

  1. 参数量大就意味着大模型训练和推理的时间变慢,也意味着需要更多的显卡和电力花费
  2. 人脑有不到1000亿个神经元,每个神经元平均有1000个连接,估计人脑有100万亿个参数; 现在GPT-4已经达到人脑1%的水平~

AI大模型的训练主要步骤有数据预处理、模型构建、模型训练、模型评估

  1. 数据预处理:首先,需要对原始数据进行清洗、整理和标注,以便为模型提供合适的输入
  2. 构建神经网络:接下来,根据任务需求,设计并搭建一个神经网络。神经网络通常由多个层次组成,每个层次包含若干个神经元。神经元之间通过权重连接,用于表示输入数据与输出数据之间的关系。
  3. 前向传播将经过预处理的数据输入到神经网络中,按照权重计算得出各层神经元的输出。这个过程称为前向传播。
  4. 激活函数:在神经网络的每一层之后,通常会使用激活函数(如ReLU、Sigmoid或Tanh等)对输出进行非线性变换,以增加模型的表达能力
  5. 损失函数:为了衡量模型预测结果与真实目标之间的差距,需要定义一个损失函数。损失函数会计算预测误差,并将其作为优化目标。常见的损失函数有均方误差(MSE)、交叉熵损失(Cross-Entropy Loss)等。
  6. 优化算法根据损失函数,选择合适的优化算法(如梯度下降、随机梯度下降、Adam等)来更新神经网络中的权重和偏置,以减小损失函数的值。这个过程称为反向传播。
  7. 训练与验证:重复执行上述步骤,直到模型在训练集上达到满意的性能。为了防止过拟合,还需要在验证集上评估模型的泛化能力。如果发现模型在验证集上的表现不佳,可以调整网络结构、超参数或训练策略等。
  8. 部署与使用:当模型在训练集和验证集上表现良好时,可以将数据模型进行部署和使用。
  • AI大模型需要更多的计算资源,如多台GPU和分布式计算等,高昂的成本阻碍了普及和应用。
  • AI大模型需要大量的标注数据,以便训练和优化模型。但实际场景中的数据通常是不完整、不一致和缺乏标注的。

# 前端架构

好的架构,不仅要清晰有逻辑,还要简单灵活好扩展;能切实解决问题,也能支撑业务快速稳定发展,并不断演进。

前端架构师的目标也离不开高性能、高可用、易扩展以及解决系统复杂度

  • 举个解决首屏加载速度慢问题的例子:
  1. 了解业务:全面调研当前业务和竞品的现状,充分理解当前渲染链路和节点,确认当前存在的问题
  2. 寻找方案:预估未来发展的方向,尽可能多的了解相关解决方案或创新自己的方案,比如:SSR,ER,预渲染,预加载,静态化等
  3. 评估方案:和相关同学讨论或开会,评估所有可行的方案及其合适度、复杂度、前瞻性和 ROI。选出至少一个候选方案,比如:SSR
  4. Demo 开发:基于现有开发能力为所有候选方案开发对应 Demo,提前探路并验证风险和可行性,帮助产出更合适的方案设计
  5. 方案设计:梳理清楚 SSR 完整链路上相关节点和合作方,多写、多画、多思考、多讨论相关架构和设计,深入细节产出 RFC 文档
  6. RFC 评审:充分评审设计、实现和产物细节,可多次评审直至所有成员达成共识。确定相关开发和团队分工,保证方案完善可执行
  7. 落到实处:推进项目开发,多与开发团队沟通,并至少参与一部分编码工作,打通所有相关开发和运维链路,保障产物简单好用
  8. 沉淀传承:沉淀文档,通过会议、分享或文章帮助其他人理解 SSR 方案和架构,用好 SSR。做好答疑,并推动方案实施
  9. 不断演进:关注 SSR 的发展,演进已有链路,比如,个性化的 SSR,结合 ER 的 SSR 等

架构师不同于高级开发可以只追求技术的深度,还需要有一定的技术广度。

前端架构师进行前端技术建设的核心目的,是为了提高开发效率,保证开发质量,为保障项目高质量按时交付

  1. 基础架构设计:主要目的是从架构层面出发,通过流程化设计,规避常见问题,提高开发效率;
  • 代码版本控制,公共静态资源缓存
  1. 工程化设计:与代码强相关,主要目的是提高代码质量,增强代码的长期可维护性,降低开发时间和成本;
  • 开发脚手架,模块抽离
  1. 团队管理类:通过合理有效的团队管理,提高团队人效比,为未来项目研发、技术发展,进行人才储备、技术研发;
  • 权限控制

  • 什么是架构?

  1. 组织业务:拆分业务生命周期,确立业务边界,构建出了一套解决特定业务问题的领域模型,并且确认模型之间、领域之间的关系与协作方式,完成了对业务领域内的要素的组织工作。

  2. 组织技术:技术选型, 架构师需要选用合适的框架、中间件、编程语言、网络协议等技术工具依据之前设计方案组织起来形成一套软件系统方案。

  3. 组织人员;

  4. 组织全局,对外输出:架构师的首要目标是解决业务问题,推动业务增长。架构师需要关注运行过程中产生的数据比如业务成功率,系统运行资源占用数据、用户反馈信息、业务增长情况等,这些信息将会帮助架构师制定下一步架构目标和方向。

  5. 架构:所谓架构能力,简单地说就是将不同的模块、组件、系统组装起来,联动发挥作用,解决业务或技术需求的一个过程

  • 作为架构师,首先要对需求的把握非常清晰,一个是需求要落实的功能点,另一个是要考虑一些特性,譬如性能,未来的扩展性等等。
  • 根据需求特性、指标数据、团队熟悉度,做好技术选型
  • 根据经验、方法论、指标数据,不断丰富与完善自己的架构方案与套件
  • 不断学习,没有捷径
  1. 管理:在理念上,我认为要让大家高效工作、快乐工作,在实施上,要想尽办法给团队、给成员赋能
  • 个人层面上,本质就是希望个人的能力不断提升,大家能够找到自己发展的目标,技能上做到一专多长,并且最终达成为一位自带“体系”的技术人。
  • 团队层面上,做好模块划分、流程优化、技术规划与梯队建设
    • 所谓模块划分,就是大家在相对稳定的模块中工作,当你比较熟悉业务逻辑的话,工作都相对容易
    • 流程优化,减少工作中的流程对研发人员带来的学习成本。
    • 技术规划,主要就是引领整个团队的技术方向,并努力将之落地-
    • 梯队建设的好坏,能决定你之前的技术规划能否顺利落地; 不同职级与经验的人的比例要均衡,还需要在各个技术方向有技术储备。

架构设计是理论设计,需要做哪些事情。工程化是落地实现,怎么去做那些事情。

前端架构的设计,应是用于解决已存在或者未来可能发生的技术问题,增加前端项目的可管理性、稳定性、可扩展性。

# 架构技术设计

基础层偏基础设施建设,与业务相关性较低。应用层更贴近用户,用于解决某一个问题。

基础层

  • 版本管理:Gitlab
  • CI/CD: jenkins,灰度发布
  • 统一脚手架:mock数据
  • 前端埋点系统:监控和报警
  • 安全管理:XSS攻击,https, CSRF,定期备份
  • 代码规范:eslint

应用层

  • SPA/MPA,SSR
  • 技术栈统一
  • 浏览器兼容
  • 内容平台建设
  • 权限管理平台
  • 登录系统设计:单点登录
  • CDN
  • 负载均衡

# 做过的项目中有什么技术难点?

# 面试总结

  1. 做题时一定要冷静,思路清晰!!!
  2. 回答问题时不要语速太快,有条理的说,不卑不亢
  3. 眼神不要闪烁,表达清晰,不要着急回答问题
上次更新: 5/20/2024, 8:29:56 PM
最近更新
01
taro开发实操笔记
09-29
02
前端跨端技术调研报告
07-28
03
Flutter学习笔记
07-15
更多文章>