seho 发布的文章

W.png

2021年前端面经

目前本人已经有了新的起点,之前在博客的求职贴也就作废了,这是我第一次写面经,写这个面经的目的希望大家能了解:

  1. 不要当作面试题背诵,因为题解仅仅是思路你会发现我的题解都比较浅甚至是有很多错误。
  2. 不要拿此题去面试其他人,因为好的面试官我认为不应该是行走的题库,应该考虑候选人是否符合团队技术栈和用人需求,不应该拘泥于业务题目,我认为多问问项目比问100道题解要有用的多。
  3. 分享出来也是记录自己的心路历程,希望大家对西安目前的招聘要求有一个大概的了解,如果你觉得我的题解有哪些错误,那就来打我吧(写评论区,我们探讨一下,我会改正的呢)
  4. 题目有些是我面试的时候必问的,有些是来自于沃趣,腾讯云,狼人杀团队的面试题,目前题主所在的公司问的题目不属于以下范畴,仅仅当作学习交流使用。
  5. 有些题目的题解我的博客也有文章(比如vue3和vite),所以大家可以去博客中看一下~

在mark一下我的博客和我现在在做的开源项目:

因卓诶-爱分享爱原创的技术博客 ~ 个人博客

最近在做的项目:

swordCodePractice/InternetQuestionBank

项目名字叫:剑指题解,业务题库app,帮助程序圈子的小伙伴们利用碎片化时间进行自我提升的小程序。

JS

事件循环模型,同异步,阻塞非阻塞,消息队列,调用栈,GUI渲染

JS虽然是一个单线程(主线程)的语言,但是它有很多工作线程,比如Ajax,定时器等等。那么同步异步在分布式系统中来讲就是一种消息通讯机制罢了,关注的是是否能立即返回结果。它不能和阻塞和非阻塞概念混为一谈。阻塞指的是等待结果返回之前影响接下来的任务,非阻塞是不影响。当JS引擎执行异步任务的时候,会把这个任务注册成一个函数,通过事件循环模型通知对应的工作线程(比如Ajax),然后主线程会继续执行当下的任务,等到工作线程完成了任务,事件循环模型会将这个异步任务的回调放到消息队列中,当主线程的调用栈为空(有空闲了)就执行消息队列最前面的消息。那么主线程从消息队列中去取任务和执行任务回调的过程就是事件循环模型的特点。

HTML到DOM是由GUI线程控制的,GUI线程和JS线程是互斥的,GUI线程会在JS的调用栈为空的情况下才会执行,因此当JS中有大量的逻辑需要处理是会影响到GUI渲染的。

闭包,作用域(词法环境)

当我们在js创建一个函数的时候,会创建一个闭包,这个闭包像是一个气泡一样,包含了创建时作用域中的变量,闭包不会随着作用域的销毁而无效,闭包保存在内存中,我们可以任何时候可以访问到闭包中的变量(除非js认定它要销毁了才会销毁),我们可以使用闭包来做很多事情,比如实现私有变量,回调函数,包括实现柯里化;关于作用域,JS中有全局作用域函数作用域以及块级作用域,其中全局作用域和函数作用域不难理解,就是直接写在window和脚本顶级位置的代码都处于全局,函数作用域就是在函数声明中的代码处于函数作用域,而块级作用域是es6新增的let const声明的代码(或者with语句以及trycatch),只能在{}这样的块级作用域中使用。

临时性锁区 TDZ

TDZ不仅仅是日常熟知的let,const情况下可能造成的问题,我们在很多地方都可能会出现TDZ。我们先来理解TDZ是什么:我们拿let和const举例子,我们在let声明之前去使用变量会得到ReferenceError这样的错误,而var不会报这样的错。因为es6规定,let声明的变量在他们的词法环境被实例化时候才会被创建,但是当它们词法绑定的时候才可以被访问(词法绑定换句话来说,没有对变量进行运算);如果在词法绑定之前就访问就会报错;那么从词法环境实例化之后到词法绑定完成这中间的过程就叫做TDZ临时锁区

变量提升是JS的变量最基本的特性,不管是let,const,var都有提升特性。let和const在词法绑定(运算)中,如果未赋值,则就是undefined;那么TDZ其他表现,会在typeof,包括函数参数预设值等场景出现。那么TDZ的作用就是让我们能够用正常的数据流去编写代码,所以我们在写代码应该要注意变量声明顺序,还有避免多个变量是同名情况。这些开发恶习是我们不提倡的。

原型链,原型

JS中的原型链是数据结构中链表的最佳体现,JS中的每个对象都有一个__proto__属性,JS中的函数中有一个原型属性prototype。

function foo(){};
foo.prototype.test = "this is test";
// a是foo的实例,a是一个对象,它有一个隐式原型指向了构造函数的原型对象
let a = new foo();
a.test; // this is test 隐式原型可以访问构造函数上的原型方法

Function也是对象,对象的隐式原型是什么呢?隐式原型指向的都是构造函数的原型,Funtion的构造函数还是Function,那么它的隐式原型就是Funtion.prototype;

Funtion.prototype中的__proto__指向了Object,Object的原型指向了Null,按照规定Null没有原型对象,所以Null是所有原型链的最终点,这个链表我们就称之为为原型链。当对象自身找不到属性的时候会去原型链上的每一个原型去找或者返回null;

总结:

1.对象有属性__proto__,指向该对象的构造函数的原型对象。

2.方法除了有属性__proto__,还有属性prototype,prototype指向该方法的原型对象。

函数

函数是JS的第一等公民,函数的大部分核心内容都在以上的章节提到过,那么这个part我们将简单说一下IIFE。

IIFE指得就是立即执行函数,这个概念js的初学者都知道,我们会通常这么写:

(function(){
    console.log("helllo world");
})()

IIFE起到了这样的作用,我们在匿名函数中声明的变量不会被外部访问即不会污染全局

脚本异步加载

在HTML5之前我们在HTML中加载script脚本的时候,是遇到脚本然后会理解执行的,它是可以阻碍下面的DOM渲染的。其次呢就是在H5加入的2个属性,async指的就是,我们异步加载了这个脚本文件并且执行了它,这个过程是不会阻塞下面的DOM渲染的。defer也是同理,但是和async在执行时间上有区别,defer会在DOM渲染完毕执行。所以总结来说,defer更适合我们常理的加载JS的方式。

Promise

在JS的远古时代,我们假设要调用一个接口或者是做一个异步的操作,我们通常会使用回调函数来resolve结果,比如这样:

request(data, (res) => {
    // 我们要通过res中的值进行下一步操作会这样写
    request(res, (res1) => {
        console.log("fuck is so hard!!")
    })
})

当我们进行多个任务嵌套,这就造成了回调地狱,这是非常不优雅的写法,那么Promise就就解决这个问题,我们通过Promise来给返回值一个“诺言”,就像下面这样:

const a1 = new Promise((resolve, reject) => {
    // resolve将会从then中获取
    // reject将会从catch中获取
});
a1.then(res => {}).catch(err => {});

那么我们需要知道,promise有几个状态:

  1. fulfilled(resolve之后)
  2. reject(reject之后)
  3. pending(还未决定,还在等)

那么Promise有哪些特性呢?比如这个状态一改变就不能在改变了,同一时间只能存在一种状态。那么async和await和promise有什么区别呢,async是es7引入的语法糖,专门处理promise的对象,它在写法上更精简了,使得异步代码彻底的变成了同步代码的视觉样子,比我们promise.then这样链式调用更容易阅读和理解。

待补充:Promise A+规范有哪些?

this的理解

在绝大多数的情况下,函数的调用方式决定了this的值,es5里面通过apply, bind, call来执行函数,此时的this就是他们的第一参数。那么后续又引入了箭头函数,箭头函数不提供this绑定,所以当前如果在箭头函数上出现,那么this指向的就是创建函数的词法环境。

Proxy

Proxy是es6的新语法,熟悉Vue2的盆友都知道,vue2的响应式是基于

Object.defineProperty()

而vue3的响应式是用proxy的,但是并不是ref,computed都是用proxy实现的。那么proxy主要的作用有什么呢?proxy可以创建一个对象的代理,实现查找,拦截以及自定义的其他功能。

const target = {};
const proxy = new Proxy(target, {
   get (target, key, receiver) {
       return 567567;
   }
});
console.log(proxy.getData);

proxy可以代理所有ProxyHandler定义的型位,也可以代理13种对象行为;

proxy和object.defineProperty的区别:

  1. 对于深层嵌套的对象,defineProperty需要遍历让每一层代理,这也就是vue2为什么watch的钩子中有一个deep的选项,开启deep就遍历。
  2. 对于数组,defineProperty不能监听关于它的操作,所以vue中.push一个数据到数组中,template不会变就是这个原因,我们必须要concat返回一个新的数组才能被访问到。
  3. defineProperty比proxy兼容性好。

数据类型/引用类型

基本数据类型有7种:number, string, obejct, null, undefined, boolean, symbol

引用数据类型:统称为object:

let a1 = {};
let a2 = {};
console.log(a1 == a2);
console.log(a1 === a2);

引用数据类型的比较是比较在堆内存中的引用;引用数据类型的真实数据是存在堆中,而基础数据类型存在于栈中。

那么这块就是老生常谈的就是==,===的区别,这个说了太多次了,==有隐式转换不严谨,我们在开发中应当使用===进行严格的判断,能够区分不同的数据类型;

我们需要注意的事情是,在堆中的对象是可变的,在栈中的变量是不可变的,我们如果更改了栈中的变量会重新在栈中划分一块区域。

深拷贝和浅拷贝

根据我们上面提到的数据类型相关知识,我们这里的深拷贝和浅拷贝只针对引用数据类型即object。我们知道引用数据类型虽然在栈中有自己的一个区域存储变量,但是同时又在堆中存储了栈中变量的引用,由于其特殊性,我们在对“对象”和“对象”进行浅拷贝操作的时候,仅仅会复制对象的指针,也就是他们还是指向同一个堆内存;但是深拷贝就不一样了,他会从内而外的复制一个新的对象,指向一个新的堆内存,2个对象互不干扰。这就是深拷贝和浅拷贝的区别。

那么浅拷贝和直接赋值有哪些区别呢?

首先赋值操作,肯定就是复制了栈中的对象,赋值之后此刻就是联动状态,a变b也变;浅拷贝唯一的区别就是第一层是否是基本数据类型。浅拷贝不会改变基础数据类型,但是如果对象其中有子对象(object,array)这个时候子对象变,那么浅拷贝之后的对象也会变。

常用的深拷贝和浅拷贝的操作?

浅拷贝就concat, splice, object.assign(这个深拷贝也可以用)等等

深拷贝就最简单的json.stringify或者写一个递归;

柯里化

柯里化的意思就是在计算机科学中,可以把一个函数多个参数的写法转换一系列一个参数的技术。

var curry = fn =>
    judge = (...args) =>
        args.length === fn.length
            ? fn(...args)
            : (arg) => judge(...args, arg)

上面这个是高颜值写法,柯里化通常用于函数式编程,意指提高代码复用,更容易阅读;

模块化

ES6的模块化解决了很多以前JS的依赖管理和开发全局变量污染的情况,这个part将简述几个模块化标准的特点以及区别。

commonJS:

const m = require("./m.js");
m(); // m.***();
// 屈居于模块中的module的export对象导出的是什么

commonjs会遇到require语句会执行并且运行这个脚本,而这个脚本中的内容都会被缓存到内存中,模块可以被多次加载,但是只有第一次加载才会被缓存;NodeJS就是参照了CommonJS的规范,但是NodeJS自己加了一点东西,就是把module.exports指向了一个变量exports;

特点:

  1. 模块中的代码不会污染其他变量,是在模块作用域中。
  2. 模块加载会被缓存,使用模块中的内容会直接从缓存中取,如果需要获取再次获取模块,需要清除缓存。
  3. 模块加载顺序就是代码执行的顺序

AMD

commonjs非常好,但是它require之后需要立即执行这个特性不适合浏览器,所以AMD和CMD就是适合浏览器运行的模块解决方案,AMD是异步加载,加载完模块依赖的包之后,会有一个回调函数给开发者,我们在回调函数中才可以正常的使用模块中的方法,例如下面:

define({
  add: function(x, y){
    return x + y;
  }
});
// 使用它
require("math", (a) => {
    a.add(1, 2)
});
// 定义AMD模块其实有很多参数,我们只列举了最简单的写法

CMD

CMD的API和AMD很像,它支持AMD的所有API,但是CMD推荐的是:

define((require, exports, module) => {
// do something
})

CMD与AMD的区别就是:

  1. AMD提倡依赖前置,即依赖提前执行,CMD推崇依赖就近,依赖是延迟执行

UMD

那么既然CMD和AMD以及CommonJS这么好,我们可以灵活地使用这些模块,就需要UMD了,UMD是一个通用的模块标准,在UMD的实现中,通过判断环境来决定使用哪种模块化,如果环境中AMD的不存在(define)就寻找commonjs,如果commonjs都不支持,那么会把模块暴漏在window或者global中。

ESModule

我们最熟悉的ESmodule来了,其实在esmodule之前,那些规范都是社区定义的,但是esmodule是真正的官方语言层面提出的模块解决方案,那么esmodule对比commonjs有什么优势呢?commonjs和AMD它们一样,都是在运行时才能确定依赖,但是esmodule能做到编译时就能确定模块依赖。

ESmodule是使用,export 和 import这两个关键语句来进行模块引入和导出的,官方定义的模块就是不一样,支持别名,默认引入等等,写法也非常精简,所以esmodule是我们目前开发web浏览器端的代码时最常用的模块标准。

Vue

Vue2核心理念和Vue3核心理念

  1. Vue3的VDOM进行了重写,首先是体积更小了。
  2. 响应式系统的升级,用proxy来劫持数据,使得Vue2响应式系统的一些坑得到了完善(具体什么坑可以看defineProperty的缺点)
  3. 组件内不需要根节点了
  4. DIFF算法更快,标记了所有的静态节点,只需要DIFF动态的内容
  5. 使用组合API区别于传统的OptionAPI,原来的option将逻辑分散到各个地方不宜与维护和阅读,而组合API将同样逻辑的代码组合在一块,高内聚低耦合,阅读维护更加的方便。

style中的scoped原理

每个组件都会在postcss编译中产生一个Hash,在style中写scoped属性之后,在页面的表现就是使用css的选择器选择对应组件的hash。在这个选择器(组件)下的css是不会影响到外部的css的。

Vdom

VD本质就是一个JS对象,我们熟知的React和Vue都是使用VD进行页面渲染的,那么VD有哪些好处呢?我们都知道页面出现都要经过js运算,生成渲染树,绘制这几个阶段,那么组件中如果出现大量的变化是要经过重排/重绘的,为了避免这个情况,我们使用VDOM来表述DOM,它和DOM是一一对应的,然后经过在内存中我们Vdom和dom的比对,然后分批次的更新页面上的dom。那么页面渲染性能就可以得到很好的提升。

而且VD可以跨平台渲染,因为本质就是一个对象,不仅可以把对象编译成DOM,还可以编译成QQ,微信,ios,原生安卓。缺点也是显而易见的,我们需要创建额外的函数去做这个事情,但是也可以依赖工具比如vue-loader等。

domdiff是对比新旧虚拟dom一种算法,分两种情况:

  • Components diff算法
  • Element diff 算法

组件的diff则是查看组件的类型,类型不相同就新替换旧的,类型相同就更新组件属性,然后进入组件递归上述操作。

标签的diff则是查看标签名,如果标签名不同就直接替换,相同就只更新属性,之后进入标签内部递归上述操作。

MVVM(本来不想写)

如果要讨论MVVM,那么就要谈到MVC和MVP了,就简单的概述一下三种架构模式吧。

MVC

MVC是非常流行的架构模式,它分为view,model,controller层,它们都是单向的通信,由view去触发controller,controller再去触发model做数据处理,然后反方向返回给view层,在这个架构中controller层是最薄的,它只做了逻辑分发的作用,一个桥梁衔接。熟知的框架就是SpringMVC。

MVP

MVP是MVC的衍生的一种架构模式,它把controller变成了Presenter,各个部分的通信都是双向的,而这里的视图view则是被动的,在这个架构中,Presenter做了绝大部分工作,而view是最薄的。

MVVM

MVVM也是衍生的一种架构模式,它没有controller,新增了一个viewModel,它保证了view和数据的双向绑定,其中有一个绑定器去做这样的事情,view的变动会反应到viewModel,反之亦然。

谈谈Vite,为什么这么快,主要解决什么问题?

天下武功,唯快不破;Vite作为超现代的本地开发服务器而且是基于ESM的。那么现在很多开发服务器都是基于ESM的,比如SnowPack。Vite本地开发快主要使用ESM,对于TS和HMR使用的是ESbuild的插件,Esbuild是用GO语言原生编译的,所以Vite在TS和HMR都有出色的表现。vite在生产环境使用的是Rollup(和Vue-cli一样)。

Vite还有以下的特点:

  1. 开箱即用,TS,jSX,CSS等
  2. MPA应用或者库模式都是非常简单配置
  3. SSR服务端渲染

想要具体了解vite是如何运作的,可以了解我以前写的一篇浅析vite的文章:

从0开始理解Vite的主要新特性(一)

Vue如何做依赖收集

我们需要事先了解观察者模式,观察者模式是一个观察者和目标组成的,观察者直接订阅目标,当目标做出改变,观察者也要立即做出回应。那么发布订阅模式和观察者模式有些许不一样,发布者只需要做好发布的任务,不关心订阅者有没有订阅。

在Vue的组件中有一个watch去监听set值,监听到set值之后会去调用render从而实现视图上的变化。那么Vue如何知道哪些变量使用了呢?我们在template中访问了这个变量,势必会被getter监听,在getter我们就可以对这个变量进行标识,从而实现变量被使用之后才能被组件中的watcher监听到。这个步骤就是Vue的依赖收集简单原理。

那么上述就是,一个一对多的例子,一个数据变更了,多个用到这个数据的地方都要做出改变,这就是观察者模式要做到的东西。那么在这个里面目标是data数据,观察者就是计算属性,侦听器,视图等。

  • Keep-alive怎么做?原理是?

这个地方我们描述的是vue中内置的持久化状态的组件,而不是http里面的keep-alive,这个需要注意。如果熟悉vue的同学肯定写过这样的代码:

<keep-alive>
    <App/>
</keep-alive>

(keep-alive下文简称ka), ka 包裹了app组件,就代表了app组件中所有状态都会被缓存,它们不会重走生命周期函数,即create和mounted只执行一次,但是请注意vue并不会销毁被缓存的组件,ka本身也不会去渲染任何元素,它是一个抽象组件。业务需求中我们也需要对缓存组件做一些额外初始化处理,但是生命周期只执行一次就很难受,vue给我们提供了activated这样的钩子,当缓存的组件再次渲染之后我们依然可以做一些初始化的工作。

mounted(){
    // 第一次渲染被缓存的组件
},
activated(){
    // 当缓存再次渲染后触发的钩子,但是此时不会触发mounted
},
deactivated(){
    // 当缓存的组件被销毁,触发钩子
}

我们刚刚提到了ka是抽象组件的问题,vue内部是如何不渲染这个ka的呢,是因为在vue组件在初始化生命周期父子组件建立关系时,如果遇到abstract为true的组件则不渲染,而ka就是true;那vue内部实现中,keepalive是如何有缓存功能的?这一块我摘抄一篇文章提到的一段源码。

// src/core/vdom/patch.js
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
    let i = vnode.data
    if (isDef(i)) {
      const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
      if (isDef(i = i.hook) && isDef(i = i.init)) {
        i(vnode, false /* hydrating */)
      }

      if (isDef(vnode.componentInstance)) {
        initComponent(vnode, insertedVnodeQueue)
        insert(parentElm, vnode.elm, refElm) // 将缓存的DOM(vnode.elm)插入父元素中
        if (isTrue(isReactivated)) {
          reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
        }
        return true
      }
    }
  }

作者:wilton
链接:https://juejin.cn/post/6844903837770203144
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

ka组件实例componentInstance是undefined但是keepAlive是true(所有被缓存过的组件keepAlive都是true),所以只执行到i(vnode, false / hydrating /)这一段代码,下一步的if没有执行。但是再次访问被缓存的组件的时候,componentInstance是上一次初始化的,所以我们能顺利的走完下一个if即insert(parentElm, vnode.elm, refElm);将缓存的内容插入到dom中。

我们之前提到的ka只会执行一次完整的生命周期,这一块的逻辑和上述一样,组件实例componentInstance不为undefined且keepAlive为true就不会执行mounted。

这就是keep-alive组件的源码实现,我这个题解描述的非常片面,在写这部分的时候,我自身对于ka的理解没有达到源码级别,所以看了这篇文章之后才有一个全新的认识,我把文章po一下:

彻底揭秘keep-alive原理

nextTick原理?怎么用

nextTick是Vue官方提供的一组API,用于获取在vue中下一次更新DOM结束的回调。由于Vue的DOM更新是异步的,所以我们更改DOM之后需要使用这个API来获取最新的DOM。

原理待补充

router原理?

路由这个概念其实是web层面是服务端的概念,但是前端为什么引入了路由概念,是由于spa应用的流行,很多库都有自己的router实现,那么前端路由中的URL和视图有哪些特性呢?

  1. 监听URL变化改变视图
  2. 不会刷新页面

我们使用hash和history来简单构建一下router基于我们的上面两个特性:

  1. hash指的就是URL中的锚点#之后的内容,锚点在HTML中常用于导航,在这个模式下,我们改变hash是不会刷新页面的。
window.addEventListener('hashchange', onHashChange);
function onHashChange(){
    // 通过判断hash来对页面进行变换
}
  1. history模式在外表下和普通URL无异,但是在这个模式下要比hash做更多一层:popstate我们使用这个API来监听URL的变化,但是当直接改变URL或者a标签跳转是不会走popstate的,所以我们需要监听URL变化和a标签跳转:
window.addEventListener('popstate', onPopState)
// 我们通过拦截a标签的事件,自己绑定一个click事件,去写下面的代码
history.pushState(null, '', el.getAttribute('href'));
// 这样手动的更新URL,不会刷新页面
// 判断URL进行刷新试图
...

React

惭愧,react我只学了1-2天,而且直接接触的是hook,react这块只达到了能写的实战水平,但是水平完全够不到vue,因为知识有限,所以这块罗列的题目很少。

useState如何回调?

待补充

类组件 vs 函数组件

待补充

更新state是同步还是异步的?

待补充

hooks带来了什么?

待补充

Webpack/Vite/SnowPack/Babel

简单讲一下webpack的构建过程

  1. 寻找入口文件
  2. 通过入口文件,逐层识别模块依赖,commonjs,amd,esmodule都会被分析,然后获取代码中的依赖。
  3. 然后webpack就要做分析代码 转换代码 编译代码 输出代码这几个重要阶段,在这几个阶段中webpack中的loader和plugin都会起作用。
  4. 输出代码

常用的loader知道有哪些么?css-loader和style-loader是干嘛的?

loader就简单的用css-loader或者scss语法loader,那么题中的css-loader和style-loader都是出现率极高的loader,css-loader主要是分析语法中的@import语句的,还用于处理css和css文件的关联,style-loader会把css挂载到html中的style里面去。

常见的plugins有哪些知道么?

比如说:

  1. htmlwebpackplugins:生成一个html并且自动引入入口文件
  2. HMR plugins:热模块更新的插件

loader和plugins的区别?

loader主要是针对特定文件的分析和编译,而plugins是webpack生态的最重要支柱,它可以干loader干不了的事情,比如说在webpack执行中会广播很多事件,而plugin可以监听这些事件并且处理和改变输出结果。

简单讲一下sourceMap

“简单“讲一下,我也只能简单讲一下了,难的我也不会。首先我们通常前端在部署的时候,会将js,css这类文件进行压缩,通常这部分工作是打包器为我们做的。但是我们想要在生产环境中查看代码错误,而浏览器会将错误定位到我们压缩编译过后的js中,这对我们debug是不利的。所以我们可以使用sourcemap去解决这个问题,它本质是一个map文件,存储了编译过后的代码的位置信息,我们在编译过后的js中引入map文件即可,引入之后浏览器会根据map存储的位置进行反馈给控制台,我们就可以很快进行debug。那么在webpack中,sourcemap有几种的常见模式:

(省略7种常见的.....)我如果真的遇到问我哪7种模式的面试官我就会很无语,webpack成千上万的配置项我咋可能都能背出来,开发中就直接查文档就好了。eval构建速度很快,而且我们sourcemap不用存储很细致的位置信息,所以使用cheap就可以了,综合来看,一般都是下面这个配置:

开发环境:cheap-module-eval-source-map
生产环境: cheap-module-source-map

如何提升webpack打包速度呢?

Webpack: 从9个方面对打包速度提升(和Node.js的美好碰撞)

webpack的热模块更新怎么做的?

这个问题我说实话,就算不会webpack,也都知道热更新,就算不知道热更新这个词语也都知道我们通常在使用一些脚手架快速开发的时候,更改文件能在页面即时同步技术。那么webpack是如何做热更新的呢,webpack中有一个官方维护的plugin:

HotModuleReplacementPlugin | webpack

hmr之前有很多库比如我经常在vscode中用到的live server,能够监听文件的变化通知浏览器进行刷新,之所以hmr的出现风靡技术圈,原因就是以前的livereload机制太扯了。比如一个提交表单场景,你输了很多表单值,你准备进行测试,发现代码中有一个地方有错,你更改之后发现页面刷新,你之前输入的表单值全部没了。我对hmr工作原理理解非常浅,如果你要深入了解这个内容,我不妨推荐给你们一篇文章,我认为全网就这篇文章把hmr讲透彻了。

Webpack HMR 原理解析

简单说一下我的理解,我就是那种喜欢纸上谈兵的(狗头):

  1. 启动webpack时候有watch选项,即文件更改→重新打包→编译到内存中。
  2. webpack-dev-server会监听静态文件变化,然后通知浏览器livereload(重新加载非hmr)
  3. webpack-dev-server会和客户端建立websocket连接,推送给客户端不同类型的消息,比如上一步的静态文件信息,还有变化了的文件更改,还有新模块的hash,后续会根据hash值去和客户端本地文件做比对,达到热模块更新效果。
  4. HotModuleReplacement拿到上述从服务端给的hash之后,hmr插件通过一个runtime模块去请求所有要更新的文件hash列表,再发起请求拿到要更新的所有文件
  5. hmr插件将比对新旧模块区别,然后判断要不要替换,如果选择替换的话,将分析相关文件依赖,是要一起更新的,如果hmr热更新失败,那就触发livereload,刷新浏览器获取最新的文件。

我原本面试主要说的是建立websokect连接,balalalalala...,但是看了上面的分析文章之后,自己简单的概括了一下,收获非常大,hmr的难点应该是在于如何替换,其他倒是很容易理解。

webpack模块打包原理?

暂补充

webpack文件监听怎么做的?

—watch,webpack-dev-server是默认开启的,脱离webpack之后可以用nodemon做监听。

webpack代码分割本质,有什么意义?

应用越来越大,vendor也会变大,某个功能可能只依赖了vendor一点,但是也要把整个包加载,使用代码分割,可以将业务和业务隔离开,按需加载。避免了浏览器加载页面白屏或者加载功能模块性能的问题。

简单聊聊Babel原理吧

我对babel原理一窍不通,惭愧。babel我的理解就是js的翻译官,能够高效的把高级/实验性语法进行低版本兼容。

CSS

WebComponents

WC是现代组件化的一种技术,WC的核心理念就是:

  1. 自定义元素Custom Element
  2. ShadowDom
  3. 模板Template

我们可以用这些技术构建出一个自定义组件,而这个组件的css和js逻辑都是独立于组件本身。

在这以下几种情况,容器不能承载ShadowDom:

  1. 自身已经承载了Input,TextArea
  2. 承载了Img标签的容器

如何让页面元素消失?

这个是我面试必考题,能够考验候选人的CSS方面的想象力,下面简单列举一下我的答案:

  1. 宽高为0
  2. 背景为白色,让用户看不到
  3. 用posotion transform margin 一系列移动的方法把元素移出去看不到
  4. css缩小,旋转可以把元素处理到看不见
  5. 透明度和css可见,透明度可以用opacity和filter实现
  6. 把字体设置为白色让用户看不到

重排和重绘?如何写组件优化重绘重排带来的开销?

重排和重绘是浏览器页面渲染的必经之路,我们在开发中的代码操作可能引出重排和重绘,那么关于重排和重绘,我们只需要记住一句话:我们页面的布局发生了改变就会引起重排,而重新排会影响到重新绘制,更改颜色文字大小等会引起元素重新绘制但是不会进行重排。在我们开发中,浏览器已经做了很多渲染优化,我们只能在特殊的场景能够发现重排和重绘优化带来的红利,但是我们仍然需要关注渲染性能的优化。比如在我们开发组件的时候,可以使用css contain content,让组件变化仅仅组件内部进行重排重绘,而不是影响整个页面。

transform中的translate会引起重排么?

不会的,GUI线程会开启一个新的图层,不会影响普通文档的图层,那么让线程开启一个新的图层的操作也有很多,比如说透明度,过渡动画,3D等等。定位虽然脱离了普通文档流,但是它还是默认图层,所以还是会影响到普通文档流。创建新的图层是要内存消耗的,所以好的东西不要贪杯。

如何更改组件内部样式?(深度选择器)

这个我就暂且默认为Vue的组件吧,我们也在上面提到了Vue组件如果做样式隔离的原理。

  1. <<<选择器(记忆中,scss和less不支持)
  2. 曲线救国,再写一个style,把组件内部的样式写到没scoped的style中
  3. /deep/ 我一般用这个

css中如何获取html元素上的属性

这个题很简单啊,但是在开发中确实非常少用到,下面给大家上代码演示一下:

content:attr(href); // 必须在伪元素中写

有了解过calc么?

和上面那个题是一个智商的题,问出来的人其实我觉得水平不高,但是奈何我们的题解中预判它可能存在,就简单说一下:

width: calc(100px + 40vh - 100px); // 简单举个例子,计算各个属性的css值,运算符之间必须要空格隔开,要不然无效

CSS沙盒

css sandbox其实也是主要解决样式隔离的问题,那么样式隔离的问题,我们有以下几个方法去解决/设计:

  1. namespace在acticle下面用,但是这个场景一般用在H5制作器上,多个组件移动到同一个区域中展示。
  2. Iframe这个是真正意义上的沙盒,但是很多局限性,高度不能自适应等等,因为iframe是上古时代的产物,所以这里不太了解。
  3. Webcomponent的实现之一ShadowDOM可以解决样式隔离的问题,但是如果care兼容性的话还是用第二种iframe的办法吧。

简单聊聊BFC?如何解决BFC造成的问题?

BFC(块级格式化上下文),指的就是在一个如果一个盒子是BFC盒子的话,那么在盒子里面的任何css都不会影响外部,它更像是一个css"作用域",所以我们在css开发中遇到一些问题,包括BFC内的样式重叠等等,我们都可以用BFC把元素独立开,去解决这个问题。

如何触发BFC呢?(没见过没用过我不会写)

  1. body元素就是天然BFC的盒子
  2. overflow 除了auto其他的值都可以解决
  3. 定位 固定定位 绝对定位
  4. display inline-block flex
  5. 浮动 float

清除浮动

清除浮动这种题,我感觉没有脑血栓10年问不出来。我的面试风格就是挑有用的问,浮动都是很老的题目了,清除浮动这样的题以前很常见,但是2021我觉得没人会问了(校招笔试题可能会看到):

  1. overflow: hidden;
  2. 相邻的元素是clear:both
  3. 或者父子都浮动(尽管脑血栓20年才会这么干)
  4. 伪元素加clear:both(推荐)

HTML

有了解过PWA规范么?

pwa是一系列的技术/规范,是由谷歌提出的,我们都称之为渐进式应用(你也可以理解为国外小程序,个人觉得比我们国内小程序功能更实用,但是没有生态没有用户粘度),使用现代webAPI去构建原生一样的跨平台应用。那么这一块的内容有点多的,所以我们就简单讲解用的比较常见的几个:

  1. servicewoker 离线使用
  2. 像APP一样具体推送通知的功能
  3. 像APP一样添加到主屏幕

这一块其实有蛮多内容的,感兴趣的可以自己搜一下。

简单说一下可替换元素吧

在HTML中有一部分元素就叫做可替换元素,它们不受CSS引擎控制,我们往往看到可替换元素的时候,这个元素就本身自己具备了一些普通元素没有特性,比如Img,Input,textArea等等;

就拿Img来说,img是行内元素,行内元素的定义就是不独立一行且无法设置宽高,但是img作为行内可替换元素是可以设置宽高的。

单页应用和多页应用(spa和mpa)的区别?

这两个概念,尤其是spa这个概念是最近几年非常流行的。spa对应的就是mpa,即多页应用;我们传统的前端页面,都是由一个一个html组成的,通过各种link来连接一个一个页面,那么意味着我们每请求一次都要去请求一个新的html进行响应。这个方案在pc端还尚可,但是在移动端中,用户每切换一次页面都是一次巨大的成本,所以spa应用而生,spa也促成了很多前端路由方案,通过url和页面进行绑定,然后由JS去控制显示的视图,这样做的好处就是,切换速度很快,用户体验感好。

那么spa也有一些缺点,比如说是SEO搜索收录不太好,因为搜索爬虫爬取mpa页面会记录每一个HTML的路径,而爬虫遇到spa页面的时候将会大大受限。mpa在首次加载的速度又优于spa,但是切换速度就不如spa了。所以综合来讲,我们web常见的官网,论坛是适合用mpa来做;移动端适合用spa来做;

SSR服务端渲染是什么?

在了解SSR需要了解浏览器部分的页面解析过程(下文就有),SSR对应的是CSR,CSR即客户端渲染,在我们现在前端中,通常都是把HTML渲染,然后在渲染过程中去加载css和js,此时的HTML因为css和js可能没加载完会出现白屏,此时的HTML组装方式是在客户端。那SSR服务端渲染目前的模式是由服务端将css和js进行处理,然后纯粹的返回给客户端一个完整的html模板,此时HTML组装方式是在服务端。那么SSR和CSR各有什么优缺点呢?在我们了解spa的时候就讲到了SEO的例子,那么这边在延申一下,爬虫也分为高级和低级,目前百度谷歌等爬虫库里面存在着很多低级爬虫,低级爬虫只能爬解析url之后的HTML页面,而客户端渲染的spa应用返回的htm都是空的,所以只有高级爬虫才会对js和css文件进行加载然后爬取,所以SEO收录是spa的一个痛点。如果我们开启服务端渲染,那SEO的问题虽然会迎刃而解,但是要在服务端进行HTML的处理势必会造成大量的计算,开销/投入产出比都是我们应该考虑的。

等待补充,同构(即SSR和CSR共存)

浏览器和工程化

HTTP2带来了什么对于前端?

等待补充

输入一个URL到页面呈现中间经历了什么?

  1. 解析URL
  2. 查看浏览器中的缓存
  3. DNS解析
  4. 建立TCP/IP连接
  5. 发送HTTP请求
  6. 服务端处理请求返回报文
  7. 浏览器解析HTML成DOM树
  8. 异步解析css外链成css规则树
  9. 遇到script标签,同步执行(分async和defer进行异步加载)
  10. 将DOM树和css规则树构造成render树
  11. 渲染render树

你如何去架构一个新的前端项目?

等待补充

浏览器如何优化重排和重绘的?

等待补充

如何在浏览器中进行标签页面的通信呢?

postmessage,等待补充

跨域(其实这题应该不会问了)是怎么造成的

等待补充

有开发过浏览器插件么?

我答的很好,但是之后会补充这一块

有了解过微前端么?

我答的很好,但是之后会补充这一块

有了解过serverless么?

我答的很好,但是之后会补充这一块

微信截图_20210411191642.png

JS

  • 事件循环模型,同异步,阻塞非阻塞
  • 闭包
  • 作用域链和原型链,原型和继承...
  • 调用栈
  • 复习函数
  • Promise【new】
  • Proxy【new】
  • 数据类型/引用类型
  • 深拷贝和浅拷贝,实现方式的优缺点
  • 柯里化
  • 模块化

Vue

  • Vue2核心理念和Vue3核心理念(比较异同)
  • css中的scoped原理
  • Vdom(这个题react也会问)
  • MVVM(本来不想写)但是确实肯定会考到wi1
  • Vue3带来了什么?有什么新特性
  • 谈谈Vite,为什么这么快,主要解决什么问题?

React

  • useState如何回调?
  • 类组件 vs 函数组件
  • 更新state是同步还是异步的?
  • hooks带来了什么?

Webpack/Vite/SnowPack/Babel

  • 简单讲一下webpack的构建过程
  • webpack有哪些常见的优化手段
  • 常用的loader知道有哪些么?css-loader和style-loader是干嘛的?
  • 常见的plugins有哪些知道么?
  • loader和plugins的区别?
  • 简单讲一下sourceMap
  • 如何提升webpack打包速度呢?
  • webpack的热模块更新怎么做的?
  • webpack模块打包原理?
  • webpack文件监听怎么做的?
  • webpack代码分割本质,有什么意义?
  • 简单聊聊Babel原理吧
  • Vite为什么开发中打包速度那么快?
  • Vite(或者esbuild)为什么ts编译的速度这么快?

CSS

  • Webcomponents规范(那三个核心理念讲出来就好)
  • 如何让页面元素消失?
  • 重排和重绘?如何写组件优化重绘重排带来的开销?(只要答出来contain属性就行了),transform是否引起重排?
  • 如何更改组件内部样式?(深度选择器)
  • css中如何获取html元素上的属性
  • 有了解过calc么?
  • 简单聊聊BFC?如何解决BFC造成的问题?

HTML

  • 有了解过PWA规范么?
  • 简单说一下自定义元素吧(和Webcomponents一起说会更好)
  • 简单说一下可替换元素吧

浏览器和工程化

  • HTTP2带来了什么对于前端?
  • 你如何去架构一个新的前端项目?
  • 浏览器如何优化重排和重绘的?
  • 如何在浏览器中进行标签页面的通信呢?
  • 跨域(其实这题应该不会问了)是怎么造成的
  • 有开发过浏览器插件么?
  • 有了解过微前端么?
  • 有了解过serverless么?
  • 未完待续...

微信截图_20210409201630.png

写在前面

博主要在西安找一个好工作,前端/全栈岗位,中级前端岗位,希望目标公司双休,做自研产品的最好,技术氛围好的优先,水友们有岗位或者HR朋友看到我这个文章请与我联系,po一下简历吧:
沈昊.pdf
联系方式微信:npm_install_s

正文

关于WebComponents的文章其实过年就想写的,但是自身的理解都太片面,所以最近才去边学边写。webCompoents下文中简称WC。从毕业开始如果有面试任务我都会尝试地问一下候选人是否了解过WC,很遗憾面试了半百的人,我连了解过ShadowCompoents的人都没遇到过,可能是面试地薪资要求太低,亦或者是有2-3年开发经验的工程师没有留意过类似的规范,今天我们就来好好梳理一下WC是什么,为什么WC影响了我们现在开发前端的方式吧!

是什么

WC是一套技术,允许开发者创建一套可以定制的元素(组件),相关逻辑和样式都会封装在元素中,并且你可以直接使用它。WC的出现解决了以往前端领域中,对多个具有相似性的功能只能复制粘贴从而造成代码臃肿的问题;WC的出现也推动了模块化/组件化的发展,让更多开发者享受封装组件带来的便利。


WC有3个要素:

  1. Custom Element 自定义元素
  2. ShadowDom 影子盒模型
  3. HTML模板

在HTML中有大多数的标签已经是运用到WC的技术了,比如熟悉的input,video,audio,select等等,我们从现在开始从0实现一个Button组件。要掌握WC的运用,需从实践开始。

影子DOM

我们在写HTML的时候,使用一些标签就可以表达一个具有形式和结构的页面,我们人类去编写这样的代码会很容易,但是机器却不会了,机器需要将HTML转换为真正的文档,而页面结构将会被解析成数据模型(对象/节点),浏览器通过创建这样的节点树(DOM)来确定用户写的HTML的层次结构,DOM最主要的特性是实时的,我们可以通过程序去操控它:

const title = document.createElement("div");
title.textContent = "hello"
document.body.appendChild(title);

这就是为什么我们可以直接通过js来操作页面上的效果就是因为DOM的存在;
那么影子DOM的意思其实已经在字面上了,“隐藏在影子中你看不到”的DOM,举一个简单的例子,我们写一个video标签:

<video>
   <source src="movie.mp4" type="video/mp4">
</video>

控制台显示如下内容:

微信截图_20210408113550.png

在video这样的WC下,有许多DOM都隐藏在其中,这些隐藏DOM都是内置好的功能和样式,在shadowDom中所有的样式和逻辑都是与外部隔绝,不会出现css冲突的问题。

创建一个简单的shadowDom:

const title = document.getElementById("title");
const shadowRoot = title.attachShadow({mode: 'open'});
shadowRoot.innerHTML = "<div>this is shadowDOM</div>"

此shadowRoot存在于title的节点树之下被称之为阴影树,而title就是阴影根,shadowRoot独立于title,shadowRoot中的内容样式逻辑都皆在组件本地,所以这就是shadowRoot不会造成css冲突的原因。

但是需要注意,并不是所有元素都可以承载shadowDOM,以下几种情况shadowDOM将无效/报错:

  1. 已经承载了shadowDOM的元素比如input,textarea等
  2. 元素承载了shadowDOM是img标签

除了我们可以定义”open“的shadowDOM之外,我们还可以定义闭合的shadowDOM:

const shadowRoot = title.attachShadow({mode: 'closed'});

HTML内部的Video标签就是一个闭合的shadowDOM,它的意义主要在于外部的JS是无法访问这个shadowDOM的,无论你使用assignedSlot/composedPath等等都是无效的。请记住闭合的shadowDOM不是很有用处,大可不必使用它,它不是我们理解的“安全的ShadowDOM”。

创建Custom Element

我们创建一个自定义的元素,结合使用shadowDOM,来完成我们开头说的button组件。

customElements.define(
  "i-button",
  class extends HTMLElement {
    constructor() {
      super();
      const shadowRoot = this.attachShadow({ mode: "open" });
      shadowRoot.innerHTML = `
        <div class="button">
            <slot name="icon"></slot>
            <slot></slot>
        </div>
      `;
    }
  }
);

我们通过customElements对象创建一个自定义组件,组件接收一个类,此时i-button的影子DOM就是我们在构造函数中定义的,影子DOM内容是标签,关于插槽稍后再讲述,我们先尝试使用i-button。

<i-button>提交</i-button>

页面正常渲染我们的button组件,如果不出意外的话,你能在控制台看到我们刚刚编写的自定义元素以及元素下的shadowDOM。

组合和插槽

组合在shadowDOM中是一个很重要的概念,我们在HTML中使用各式各样的标签完成页面,页面的构成是各种标签的组合,而组件也是一样,例如video标签,我们通过video的子级source来定义媒介资源地址,但是它却不会渲染。

别着急,我们先来梳理下几个术语概念:

Light DOM

指的是用户编写的内容,比如在上文中,我们使用了i-button组件,在这个组件中我们写下了字 “提交”那么此时“提交”就是Light DOM,此时“提交”是实际子元素,它是真实存在的,而不像是shadowDOM。

Shadow DOM

具体的意义上文提到过,补充一下ShadowDOM对组件而言是本地的,它还可以定义一些“标记”或者说是“插槽

使用过vue的水友们,应该更能体会插槽带来的便利,插槽是组件内部的占位符,方便使用者编写的LightDOM按照指定的方式和组件一起呈现出来,那么这个指定的方式也就是插槽的类型了:

具名插槽

在组件内部定义的之后,我们使用组件的时候可以这么使用:

<i-button>
    <img slot="icon" src="icon.png"></img>
    提交
</i-button>

默认插槽

默认插槽就是我们组件中写的内容,比如就是上文中的提交二字,它没有被slot标记,就会默认放在组件中的位置渲染。如果用户不在组件提供LigntDOM,那么我们可以定义一个后备插槽以便备用:


/* 默认渲染的位置 */
<slot></slot>
<slot>如果用户没传递内容,那么将会显示我</slot>

当用户编写的LightDOM被组件定义的插槽使用了,那么此时,这个元素并不是被插槽移动了位置,插槽没有移动位置的功能,其实就是浏览器把LightDOM元素渲染到了shadowDOM的位置上了而已。

理解了插槽之后,我们就可以使用更多的标签将其组合在一起,构成一个较为完整且实用的组件了,当然还有样式!

样式

ShadowDOM最有趣的特型就是作用域CSS了,外部的css选择器不会影响到shadowDOM,内部的也不会影响外部的,css的作用域为阴影根
我们来定义一下Button组件的样式:

#shadow-root
<style>
 .button{
   background: red;
 }
</style>
<div class="button">
  <slot name="icon"></slot>
  <slot></slot>
</div>

oh, no,尽管它定义成了红色很丑,但是不妨碍我们研究它的作用域CSS;

<link rel="stylesheet" href="styles.css">
<div></div>

还可以加入link标签引入一个css,这个css也是带有作用域的。
我们在写WC的时候,不仅会需要组件自身维护自己的样式,也需要外部组件可以通过一种方法改变组件内部样式,这样既保证了封装性又有灵活性,那么:host这个伪类是需要了解的。

:host是一个选择宿主的伪类选择器,我们可以使用:host来匹配宿主或者宿主下的元素:

<style>
    :host{
        color: #fff;
    }
</style>

也可以匹配阴影根下的元素:

:host(.button){
    background: blue;
}

也可以定义插槽样式:

::slotted(.icon){
   width: 2px;
   height: 2px;
}

从外部定义自定义组件的样式,直接使用元素名进行设置:

i-button:hover{
    opacity: 0.8;
}

当外部设置了样式之后,优先级会大于内部的css规则,比如:

:host{
   opacity: 0.1;
}

通常开发者编写自己的组件的时候,会使用CSS自定义属性,而使用者可以修改其CSS自定义属性:

<style>
    i-button{
        /*我很喜欢红色*/
        --diy-bg: red; 
    }
</style>
<i-button background></i-button>

影子dom这样写:

:host([background]){
    background: var(--diy-bg, black);
}

我们在使用组件的时候给自定义元素设置了一个值为red,然后在自定义元素中加了一个“background”的属性,然后在其shadowDOM中匹配了元素如果有属性background的话:就设置背景为外部传入的“red”,如果外部没有传入,则就是默认的“black“。

当然开发组件的时候,我们需要告知使用者一些内置的css自定义属性。

技巧

使用css containment

我们可以使用“css遏制”来优化web组件重排重绘的性能,当web组件内部进行了UI/位置变更,势必会引起页面的重排和重绘,使用css遏制之后告诉浏览器,组件这一块是一块独立的DOM,浏览器就不会造成整个页面的重排重绘了,只会在组件内部进行重排和重绘。

:host {
  display: block;
  contain: content; /* Boom. CSS containment FTW. */
}

使用Template

使用Template代替innerHTML,使用template之后你会发现,vue的组件就是使用这种方式来呈现组件的,它们都是一样的!使用template会更清晰地看到组件的DOM结构。

<template id="i-button">
    <div class="button">
      <slot name="icon"></slot>
      <slot></slot>
    </div>
</template>
<script>
    customElements.define(
      "i-button",
      class extends HTMLElement {
        constructor() {
          super();
          var template = document
            .getElementById('i-button')
            .content;
          const shadowRoot = this.attachShadow({ mode: "open" }).appendChild(template.cloneNode(true));
          `;
        }
      }
    );
  </script>
    // 使用Button组件
    <i-button></i-button>
    <style>
         // 增加一些Style样式
    </style>

剑指题解


互联网人题解神器,神器在手,offer我有

随手刷题 | 无广告绿色免费 | 个性推荐 | 每日自动推送 | 多维度丰富领域知识

项目的起源是一个 github 仓库,名为前端知识每日 3+1,正是这个优秀的题库项目才有了我们团队开发聚合题库的想法。在我们国内程序员圈子,大多数小伙伴为了更好的技术发展和 offer,都通过训练题来提高自己的眼界,其中不乏包括算法/业务/软技能的题目,但是因为我们的环境和侧重点不一样,所以我们接触的项目和题目都不一样,而市面上很少有一个项目能做到,涵盖不同圈子的聚合题库。 《剑指题解》项目就是一个优秀的互联网行业聚合题库神器。


开源免费不易,请关注我们的公众号以及开源仓库

沈昊Seho/InternetQuestionBank<br/>
Github 点 star, 拉取代码时请选择最新分支
Dcloud 插件市场支持我们

目录

特点

  1. 汇聚优秀的开源题目,以技术社区为主线展开的刷题 APP
  2. 根据用户的喜好,去推送相关的最新题目,不会再刷到 “牛头不对马嘴”无用的题目
  3. 我们绑定了服务号,每日定时推送给用户最新的题目,让你能在每天清晨就开启活力新一天
  4. 刷题 APP 无广告,无硬性推广,一切盈利皆在赞助/官网流量
  5. 我们还有很多有意思的待开发 feature,比如大佬带萌新模式,还将开启校园/企业的通道,让每一个学生/员工都能每时每刻提高自己
  6. 剑指题解开源项目,从原型图到小程序,UI 图,UML,API 文档,第三方 API 对接说明书,使用说明都将免费开源,欢迎各位同僚二开,让这个行业变得更好,我们将用心地辅助你们解决在技术上的难题。

技术栈

关于技术栈的选型可以参考我们的语雀文档
周边物料的开源,还需要感谢 mockPlus 以及语雀:

<img class="imgIcon" src="http://static.yinzhuoei.com/typecho/2021/01/17/69752764374919.png" width="10%">


<img class="imgIcon" src="http://static.yinzhuoei.com/typecho/2021/01/17/697832282748067.jpg" width="20%"></img>

UML

由于此部分在开发新 feature 的工作中可能会进行变更或者本就有实质性的错误,欢迎向我们反馈错误,我们将感激不尽。

类图

拉取任务

更多

更多相关资料请移步
out/doc或者语雀-UML

设计概览



设计源文件请移步:点击查看-为了您的体验,请务必下载最新版本的设计图
原型图在线预览: 点击查看, 如果失效请联系我们

API

《剑指题解》团队在 API 层面,开源的内容非常有限制,因为涉及到多位合作伙伴的题库授权,所以如果您要使用聚合题库 API 请联系我们商议,如果您是学习者,那么在语雀中的 API 文档会帮助到您,在这里我们将简单介绍。

uniapp 的云函数开发,我们是第一次接触,所以在此之前我们做了很多调研,在下文也提到了关于 explain.js 这个框架的作用,我们也是非常感谢 uniapp 开源社区有这样创作质量的第一批的 unicloud 框架开发者,在咨询了官方人员之后,我们了解到如果要使用 ts 来开发,需要把 ts 编译成 js 来放在云函数目录,所以我们选择了以下的技术栈,开发者可以很爽的使用 ts 来开发 unicloud 云函数:

  • esbuild-node-tsc
  • nodemon

esbuild-node-tsc 这个工具是基于 esbuild 的,利用其特性,我们可以比 ts 官方提出的 tsc 编译工具更快,快几十倍到上百倍,这也是得益于 esbuild 的使用 go 语言直接编译的特性。

nodemon 可以帮助我们监听目录文件,一旦改变就执行打包 ts 命令直接将编译好的 js 放在指定目录
所以我们把 explain.js 的配置也相应的改变,我们在 services 目录中存储的是我们 ts 文件,在 dist 中存储的是我们编译好的 js 文件,而 dist 目录就是 explain 需要的:

config.init({
  baseDir: __dirname,
  serviceDir: "/dist/",
});

需要注意的事情是:
由于云函数大小限制,我们需要把相关依赖全部安装到全局,包括 typescript

读写职责分离

读写职责分离模式(CQRS)是一种把查询(Queries) 数据和和更新(Commands) 数据通过使用各自独立的接口分开的模式。
Uniapp 的 Unicloud 很好,其中的 jql 也是大开眼界,确实统一了前端开发操作数据库的体验,让我们前端开发操作数据库非常友好,但是我们使用 CQRS 将这些原本封装的很好的 API 不予以使用。

  • 前端使用 jql 去读取想要的数据
  • 写入操作就交给云函数去执行

如图,前端读取一个列表数据

const db = uniCloud.database();
const databaseName = "testInit";
// 获取测试数据列表
export function getTestList() {
  return new Promise((resolve) => {
    db.collection(databaseName)
      .get()
      .then((res) => {
        // res 为数据库查询结果
        resolve(res);
      })
      .catch((err) => {
        // err.message 错误信息
        // err.code 错误码
      });
  });
}

我们将这样的文件抽出了一个一个查询模块,每一个模块对应了一个数据表(这里是 testInit)封装在了 API 这个文件夹中。

Vue 页面将这样去调用,这样我们在 API 查询层将做好数据的处理,确保 Vue 拿到的是可以直接渲染的干净数据。

import { getTestList } from "../../api/test";
const data = getTestList();

我们写入操作将通过云函数直接调用,唯一设计不同的地方在于,我们将云函数这个概念改变了,应该是模块化,我们把一个一个函数变成了模块。

在我们的初步技术调研过程中,由于 Uni 官方并没有提供给用户云函数开发框架,但是我们在插件市场中找到了一款名为 explain 的开发框架,它可以迅速的帮助我们实现 restapi 风格的单路由云函数,这款框架我们不多做介绍,文档在这里:explain.js 快速开发 uni 云函数的框架

// 注册用户根据手机号
  addUserByPhone() {
    return handleMustRequireParam(
        [{
            key: "username",
            value: "用户名",
          },
          {
            key: "password",
            value: "密码",
          },
        ],
        this.event.params
      )
      .then(async () => {
        const {
          username,
          password
        } = this.event.params;
        if (!/^1[3456789]\d{9}$/.test(username)) {
          return appErrorMessage("用户名格式不正确");
        } else if (password === "" || password.length < 6) {
          return appErrorMessage("密码格式不正确");
        } else {
          // 校验手机号
          return await uniID.register({
            username,
            password,
          });
        }
      })
      .catch((err) => {
        return err;
      });
  }

那么我们如果要调用 testPrint 这个模块中的增加操作

uniCloud.callFunction({
  name: "application",
  data: {
    route: "api/user",
    method: "POST",
    params: {
      username: "18291563764",
      password: "sas",
    },
  },
});

贡献者

感谢为《剑指题解》这个优秀项目贡献自己一份力的小伙伴们:

如果您想加入到我们的贡献者队列中,请联系我们,这里还有相关贡献者的介绍,希望对您有帮助

核心维护者

相关开源物料

题库合作伙伴

QQ 群和钉钉群欢迎大家加入


许可

Apache License © 剑指题解
如果您在其他有关我们的文档见到了与之不符合的协议内容,请联系我们,这可能是我们的历史遗留的代码问题。

Nginx是一款高性能服务器,最近这几年非常火,以轻量且高并发,高性能著称,那么此笔记将不会从0开始讲解API,而是会从各种问题入手,通过问题学习nginx。


特点:

  1. IO多路复用
  2. 高性能
  3. 高并发
  4. 占用系统资源少

Untitled.png

Nginx作为一个WEB服务器,有着大好的未来,市场份额非常给力,同时也是份额上升速度最快的web服务器。

Untitled.png

Nginx作为前端来说,需要学习什么?我们只需要学习Nginx在应用部署,反向代理,处理资源的进程,亦或者是搭建网站的基础知识,如果你还没有一个blog,那么就从现在开始学习nginx并且搭建你的第一个网站吧。


反向代理与正向代理

Untitled.png

我们在平时上外网的时候,比如谷歌,youtube,twitter,Ins等,如果使用我们内地网络,是访问不成功的,只有在香港台湾或者境外才能访问到类似的外网。那我们需要通过内地网络去访问外网只能通过一个proxy代理去做一个请求的转发,我们的内地网络请求在到达外网地址之前,会经过一层代理,这个代理会去请求外网,请求成功之后会把页面呈现给我们的客户端。

在这个过程中,外网服务器不知道我们的内地网络是谁,只知道代理地址,所以对于外网服务器来说,请求的真实客户端是看不到的。那么这个过程就叫做 正向代理,proxy代理的是客户端。

Untitled.png

反向代理是相反的,代理的是服务端,对于客户端而言,访问的服务器仅仅是多个真实服务器的一个代理而已,所以对于客户端用户而言,真实服务器的信息是不可见的。这样的过程也就是反向代理,proxy代理的是服务端

Nginx如何去做反向代理?

server{
        listen 80;
        server_name nginx.yinzhuoei.com;
        location / {
               proxy_pass http://yinzhuoei.com;
        }
}

其他的proxy配置:

proxy_set_header :在将客户端请求发送给后端服务器之前,更改来自客户端的请求头信息。
proxy_connect_timeout:配置Nginx与后端代理服务器尝试建立连接的超时时间。
proxy_read_timeout : 配置Nginx向后端服务器组发出read请求后,等待相应的超时时间。
proxy_send_timeout:配置Nginx向后端服务器组发出write请求后,等待相应的超时时间。
proxy_redirect :用于修改后端服务器返回的响应头中的Location和Refresh。

解决跨域

通过反向代理解决跨域:

server
{
   listen 3003;
   server_name localhost;
      ##  = /表示精确匹配路径为/的url
   location = / {
       proxy_pass http://localhost:5500;
   }
   ##  若 proxy_pass最后为/ 如http://localhost:3000/;匹配/no/son,则真实匹配为http://localhost:3000/son
   location /no {
       proxy_pass http://localhost:3000;
   }
   ##  /ok/表示精确匹配以ok开头的url,/ok2是匹配不到的,/ok/son则可以
   location /ok/ {
       proxy_pass http://localhost:3000;
   }
}

加header头允许跨域:

server
{
    listen 3002;
    server_name localhost;
    location /ok {
        proxy_pass http://localhost:3000;

        #   指定允许跨域的方法,*代表所有
        add_header Access-Control-Allow-Methods *;

        #   预检命令的缓存,如果不缓存每次会发送两次请求
        add_header Access-Control-Max-Age 3600;
        #   带cookie请求需要加上这个字段,并设置为true
        add_header Access-Control-Allow-Credentials true;

        #   表示允许这个域跨域调用(客户端发送请求的域名和端口) 
        #   $http_origin动态获取请求客户端请求的域   不用*的原因是带cookie的请求不支持*号
        add_header Access-Control-Allow-Origin $http_origin;

        #   表示请求头的字段 动态获取
        add_header Access-Control-Allow-Headers 
        $http_access_control_request_headers;

        #   OPTIONS预检命令,预检命令通过时才发送请求
        #   检查请求的类型是不是预检命令
        if ($request_method = OPTIONS){
            return 200;
        }
    }
}

Master&Woker模式

Nginx启动之后,启动了80端口进行服务监听,那么进程中就存在一个Mater主进程和多个Woker进程;

Untitled.png

Master进程的作用就是:读取&验证nginx.conf配置文件并且管理多个woker进程;接受外部信号;监控Woker,如果Woker挂掉,将自动重启Woker;

Woker进程的作用就是:多个Woker会拦截所有的请求并做出处理;每一个woker进程维护一个线程;woker的个数和CPU有关,从nginx.conf配置woker个数,配置几个就是几个,但是要避免配置过多,要充分利用CPU;

一个请求到响应的流程:

  1. Nginx启动,Matster进程根据nginx.conf初始化;初始化监听socket;fork出多个woker进程;
  2. 发起请求
  3. woker进程们一起竞争,胜出者通过三次握手,建立socket连接,处理请求。

如何做热部署呢?

热部署就和前端热部署一样的性质,即修改配置文件,不需要重启服务器就可以使用最新的配置。

nginx -s reload

通过这样的一个命令即可热部署,无需重启,随时改随时用。

一般情况下,我们做热部署可以有几个方案,比如前端,webpack的本地开发工具,webpack-dev-server,即本地启动一个服务,开启一个websocket,当我们的文件改动,就重新加载这个css/js。

而nginx也是同样的方式么?我们的主进程master去发布一个修改请求,然后woker去订阅这个消息,实现类似这样的热部署?

其实不然,nginx使用的是如下的方案,当master监听到配置文件的更改,会创建一批新的woker去执行新的请求,老的woker进程会在任务处理完毕之后,再由master杀掉进程。


如何做到高并发?

Nginx采用多进程+异步非阻塞方式(IO多路复用):

关于异步和同步,我需要做一些概念上的整理;

同步和异步指的是消息的通信机制,我们做web开发是最能理解同步异步的区别的,因为我们天天和接口打交道;

1)所谓同步指的就是发起一个请求/调用,在没有得到结果之前就不会返回,一旦得到结果就立即返回;

2)所谓异步指的就是发起一个请求/调用,调用者不会主动去care被调用者,而被调用者拿到结果之后会通知调用者

而阻塞非阻塞指的是程序在等待调用结果时的状态

1)阻塞调用指的就是,结果返回之前当前线程被挂起,调用线程在返回之后才返回;那么挂起的这个线程是会被阻塞的;

2)非阻塞调用指的就是,不能立刻得到结果之前,线程是不会被挂起的,仍然可以做其他事情;那么非阻塞调用如何知道得到结果了呢,需要定时去check的;

关于阻塞IO和非阻塞IO等我总结完了再说哈,还有关于Nginx的IO多路复用Epoll模型,这个是延申知识了,我也需要学习整理哈,现在还不清楚这一块的东西。

Nginx后续章节过段时间发,中间要发几篇shadowDom和剑指题解的文章,大家耐心等待...


学习资料如下:

nginx如何做到高并发?
8分钟带你深入浅出搞懂Nginx
理解同步异步&阻塞非阻塞

微信截图_20210331210548.png