题目加答案,每日更新5~10题
目录
已完结
- 搜索请求如何处理?搜索请求中文如何处理?
- 你是如何组织js代码的?(可以从模块、组件、模式、编程思想等方面回答)
- 说出前端框架设计模式(MVVM 或 MVP 或 MVC)的含义以及原理
- 页面埋点怎么实现?
- 说一下Taro的编译原理
- 封装公共组件需要注意什么?
- 组件库设计有什么原则呢?
- 设计搜索组件(具有自动补全功能组件),你需要考虑的问题是什么?
- 说一下对 ESLint 的了解? 如何使用? 它的工作原理?
- 对 to B 和 to C 的业务的理解?
- 是否了解 glob 库,glob 是如何处理文件的?是否有别的方法?
- 前端怎样做单元检测?
- 项目如何管理模块?
- 微服务和单体应用的区别是什么?
- 什么是微服务?
- 用微服务有什么好处?
- 说一下面向对象的理解,面向对象有什么好处?
- 面向对象的三要素是什么,分别是什么意思?
- 了解函数式编程中的compose么?
- 讲一下你所了解的函数式编程
- 你所理解的前端路由是什么?
- 如何实现按需加载?
- 请说明JS进行压缩、合并、打包实现的原理是什么?为什么需要压缩、合并、打包?
- PWA 是什么,对 PWA 有什么了解?
- 简单介绍使用图片base64编码的优点和缺点
- 说一下base64的编码方式
- 扫描二维码登录网页是什么原理,前后两个事件是如何联系的?
- 网页验证码是干嘛的,是为了解决什么安全问题?
- 实现单点登录的原理
- utf-8 和 asc 码有什么区别?
- 什么是死锁?
- 简单描述静态链接和动态链接的区别,并举例说明。
- 说一下对 URL 进行编码/解码的实现方式?
搜索请求如何处理?搜索请求中文如何处理?
- 防抖节流
- 编码
搜索请求处理
搜索过程中,快速的字符改变输入/更变,会导致过于频繁的请求,浪费请求支援,一般考虑防抖(debounce)进行优化;
防抖:
持续触发事件时,一定时间内没有在触发事件,函数执行依次;如果设定的时间之前又触发了事件,就重新开始延时
防抖实现
function debounce(fn, wait) {
var timeout = null;
return function () {
if (timeout !== null) {
clearTimeout(timeout);
}
timeout = setTimeout(fn, wait);
};
}
搜索请求中文如何处理
一般是通过encodeURL或encodeURIComponent方法将对应的中文进行编码
encodeURI与 decodeURI
调用js方法encodeURI(url),将url编码,然后请求,应该是最常用的
百度搜索就是get请求,比如空格就是%20,多打几个空格搜索看看。
而且chrome+tomcat测试服务器接收自动转回来。
转移序列概念
修改服务器编码集(一劳永逸)
Tomcat为例
打开 server.xml
<Connector port=”80″ protocol=”HTTP/1.1″ connectionTimeout=”20000″ redirectPort=”8443″ URIEncoding=”utf-8″/>
比较常用的方法
服务器转码(不推荐)
String param = new String(param.getBytes(“iso-8859-1″),”utf-8”)
Base64 加密
使用base64.js之类的脚本进行加密,服务端接收数据后解密。
你是如何组织js代码的?(可以从模块、组件、模式、编程思想等方面回答)
组织JavaScript代码
精心设计的代码更易于维护,优化和扩展,能使开发者更高效。这意味着更多的注意力和精力可以花在构建伟大的事情上,每个人(用户,开发者和利益相关者)都能够进行便捷式的获取
比较宽松的语言,特别是JavaScript,需要一些规矩才能写好
JavaScript环境非常宽松,随处扔些代码片段,就可能起作用。早点确立系统架构(然后遵守它!)对你的代码库提供制约,确保自始至终的一致性。
有三个高级的点,跟语言无关的点,对代码设计很重要
- 系统架构
代码库的基础设计。控制各种组件的规则,例如模块(models)、视图(views)和控制器(controllers)以及之间的相互作用
- 可维护性
如何更好的改进和扩展代码?
- 复用性
应用组件如何复用?每个组件的实例如何简便地个性定制?
模块模式
模块模式
是一个简单的结构基础,它可以让你的代码保持干净和条例清晰。一个"模块"就是个标准的包含方法和属性的对象字面量,简单是这个模式的最大亮点:甚至一些不熟悉传统的软件设计模式的人,一眼就能立刻明白代码是如何工作的。
用此模式的应用,每个组件有它独立的模块。例如,创建自动完成功能,你要写个模块用于文本区域,一个模块用于结果列表。两个模块相互工作,但是文本区域代码不会触及结果列表代码,反之亦然。
模块解耦
是模块模式非常适用于构建可靠的系统架构的原因。应用间的关系是明确定义的;任何关系到文本区域的事情被文本区域模块管理,并不是散落在代码库中 --- 代码整洁
模块化的组织的另外一个好处是固有的可维护性。模块可以独立的改进和优化,不会影响应用的任何其他部分。
说出前端框架设计模式(MVVM 或 MVP 或 MVC)的含义以及原理
MVC、MVP 和 MVVM 都是常见的软件架构设计模式,它通过分离关注点来改进代码的组织方式。不同于设计模式,只是为了解决一类问题而总结出的抽象方法,一种架构模式往往使用了多种设计模式
了解 MVC、MVP 和 MVVM,就要知道它们的相同点和不同点,不同部分是 C(Controller)、P(Presenter)、VM(View-Model),而相同的部分则是 MV(Model-View)
Model&View
- Model
Model 层用于封装和应用程序的业务逻辑相关的数据以及对数据的处理方法
- View
View 作为视图层,只要负责数据的展示,但是对一个应用程序,这远远是不够的,我们还需要响应用户的操作、同步更新 View 和 Model。于是在 MVC 中引入了控制器 Controller,让它来定义用户界面面对用户输入的响应方式,它连接模型和视图,用于控制应用程序的流程,处理用户的行为和数据上的改变
MVC
MVC 这种架构模式,极大地降低了 GUI 应用程序的管理难度,而后被大量用于构建桌面和服务器端应用程序。MVC 允许在不改变视图的情况下改变视图对用户输入的响应方式,用户对 View 的操作交给了 Controller 处理,在 Controller 中响应 View 的事件调用 Model 的接口对数据进行操作,一旦 Model 发生变化便通知相关视图进行更新
- Model
Model 层用来存储业务的数据,一旦数据发生变化,模型将通知有关的使用 Model 和 View 之间使用观察者模式,View 事先在此 Model 上注册,进而观察 Model,以便更新在 Model 上发生改变的数据
- View
View 和 controller 之间使用了策略模式,View 引入 Controller 的实例来实现特定的响应策略
- Controller
控制器是模型和视图之间的纽带,MVC 将响应机制封装在 controller 对象中
,当用户和你的应用产生交互时,控制器中的事件触发器就开始工作了。
MVC 模式的业务模式主要集中在 Controller,而前端的 View 其实已经具备了独立处理用户事件的能力,当每个事件都流经 Controller 时,这层会变得十分臃肿。而且 MVC 中的 View 和 Controller 一般是一一对应的,捆绑起来表示一个组件,视图与控制器间的过于紧密的连接让 Controller 的复用性成了问题,如果想多个 View 公用一个 Controller 时该怎么办呢?就是下文介绍的 MVP
MVP
MVP 是 MVC 模式的改良,和 MVC 的相同之处在于: Controller/Presenter 负责业务逻辑,Model 管理数据,View 负责显示
虽然在 MVC 里,View 是可以直接访问 Model 的,但 MVP 中的 View 并不能直接使用 Model,而是通过为 Presenter 提供接口,让 Presenter 去更新 Model,在通过观察者模式更新 View
与 MVC 相比。MVP 模式通过解耦 View 和 Model,完全分离视图和模型使职责划分更加清晰
;由于 View 不依赖 Model,可以将 View 抽离出来做组件,它只需要提供一系列接口提供给上层操作。
- Model
Model 层依然是主要与业务相关的数据和对应处理数据的方法。
- View
MVP 定义了 Presenter 和 View 之间的接口,用户对 View 的操作都转移到了 Presenter。View 非常薄弱,不部署任何业务逻辑,成为"被动视图",即没有任何主动性,而 Presenter 非常重要,所有的逻辑都部署在这里
- Presenter
Presenter 作为 View 和 Model 之间的"中间人",除了基本的业务逻辑之外,还有大量代码需要对从 View 到 Model 和从 Model 到 View 的数据进行"手动同步",这样 Presenter 显得很重要,维护起来比较困难,而且由于没有数据绑定,如果 Presenter 对视图渲染的需求增多,它不得不过多关注特定的视图,一旦视图需求发生改变,Presenter 也需要改动
。
MVVM
MVVM 最早由微软提出,ViewModel 指"Model of View" --- 视图的模型。这个概念曾在一段时间内被前端圈热炒,以至于很多初学者拿 JQ 和 Vue 做对比
MVVM 把 View 和 Model 的同步逻辑自动化
。以前 Presenter 负责的 View 和 Model 同步不再手动地进行操作,而是交给框架所提供地数据绑定功能进行负责,只需要告诉它 View 显式的数据对应的 Model 哪一部分即可
基本上与 MVP 模式完全一致,唯一的区别使:它采用双向绑定,View 的变动,自动反映在 ViewModel,反之亦然。Angular 是采用这种模式
- Model
在 MVVM 中,我们可以把 Model 成为数据层,因为它仅仅关注数据本身,不关心任何行为
- View
和 MVC/MVP 不同的是,MVVM 中的 View 通过使用模板语法来声明式的将数据渲染进 DOM,当 ViewModel 对 Model 进行更新的时候,会通过数据绑定更新到 View
- ViewModel
ViewModel 大致上及时 MVC 的 Controller 和 MVP 的 Presenter 了,也是整个模式的重点,业务逻辑也主要集中在这里,其中的一大核心点就是数据绑定。与MVP不同的是,没有View为Presenter提供的接口,之前由Presenter负责的View和Model之间的数据同步交给了ViewModel中的数据绑定进行处理
,当Model发生变化,ViewModel就会自动更新;ViewModel变化,Model也会更新
整体来看,比MVP/MVC精简很多,不仅仅简化了业务与界面的依赖,还解决了数据频繁更新的问题。因为在MVVM中,View不知道Model的存在,ViewModel和Model也察觉不到View,这种低耦合模式使开发过程更加容易,提高应用的可重用性
总结
MV* 的目的是把应用程序的数据、业务逻辑和界面这三块解耦,分离关注点,不仅利于团队协作和测试,更有利于维护和管理。业务逻辑不再关心底层数据的读写,而这些数据又以对象的形式呈现给业务逻辑层。从MVC---MVP---MVVM,就像一个打怪升级的过程,它们都是在MVC基础上随着时代和应用环境的发展衍变而来的。与其我们在纠结使用什么框架或者架构模式的时候,不如先了解它们。静下来思考业务场景和开发需求,不同需求下会有最合适的解决方法。我们使用这个框架就代表认同它的思想,相信它能够提升开发效率解决当前的问题,而不仅仅是因为潮流趋势而追捧
页面埋点怎么实现?
现有的埋点类型
-
手动代码埋点
: 在需要采集数据的地方调用埋点的方法。在任意地点任意场景进行数据采集 -
可视化埋点
: 元素都带有唯一标识。通过埋点配置后台,将元素与要采集事件关联起来,可以自动生成埋点代码嵌入到页面中。 -
无埋点
: 前端自动采集全部事件,上报埋点数据,由后端来过滤和计算出有用的数据,
基本实现的方案
约定通用的埋点采集接口规范:
如header(标识X-Device-Id, X-Source-Url,X-Current-Url,X-User-Id等信息)
body(标识PageSessionID, Event, PageTitle,CurrentTime, ExtraInfo);
指定调用采集脚本的方式:
单页面应用 => 对history路径的变化保持监听, 路径变化时触发埋点收集;
页面加载离开绑定对应的onload,unload事件,页面元素上绑定相关的交互事件(click, event等)
示意伪代码:
var collect = {
deviceUrl:'',
eventUrl:'',
isuploadUrl:'',
parmas:{},
device:{}
};
//获取埋点配置
collect.setParames = function(){}
//更新访问路径及页面信息
collect.updatePageInfo = function(){}
//获取事件参数
collect.getParames = function(){}
//获取设备信息
collect.getDevice = function(){}
//事件采集
collect.send = function(){}
//设备采集
collect.sendDevice = function(){}
//判断是否采集,埋点采集的开关
collect.isupload = function(){
/*
1. 判断是否采集,不采集就注销事件监听(项目中区分游客身份和用户身份的采集情况,这个方法会被判断两次)
2. 采集则判断是否已经采集过
a.已经采集过不做任何操作
b.没有采集过添加事件监听
3. 判断是 混合应用还是纯 web 应用
a.如果是web 应用,调用 collect.setIframe 设置 iframe
b.如果是混合应用 将开始加载和加载完成事件传输给 app
*/
}
//点击事件处理函数
collect.clickHandler = function(){}
//离开页面的事件处理函数
collect.beforeUnloadHandler = function(){}
//页面回退事件处理函数
collect.onPopStateHandler = function(){}
//系统事件初始化,注册离开事件,浏览器后退事件
collect.event = function(){}
//获取记录开始加载数据信息
collect.getBeforeload = function(){}
//存储加载完成,获取设备类型,记录加载完成信息
collect.onload = function(){
/*
1. 判断cookie是否有存设备类型信息,有表示混合应用
2. 采集加载完成时间等信息
3. 调用 collect.isupload 判断是否进行采集
*/
}
//web 应用,通过嵌入 iframe 进行跨域 cookie 通讯,设置设备id
collect.setIframe = function(){}
//app 与 h5 混合应用,直接将数信息发给 app,判断设备类型做原生方法适配器
collect.saveEvent = function(){}
//采集自定义事件类型
collect.dispatch = function(){}
//将参数 userId 存入sessionStorage
collect.storeUserId = function(){}
//采集H5信息,如果是混合应用,将采集到的信息发送给 app 端
collect.saveEventInfo = function(){}
//页面初始化调用方法
collect.init = function(){
/*
1. 获取开始加载的采集信息
2. 获取 SDK 配置信息,设备信息
3. 改写 history 两个方法,单页面应用页面跳转前调用我们自己的方法
4. 页面加载完成,调用 collect.onload 方法
*/
}
collect.init(); // 初始化
//暴露给业务方调用的方法
return {
dispatch: collect.dispatch,
storeUserId: collect.storeUserId,
}
说一下Taro的编译原理
命令
taro build:xxx
是执行编译的命令,根据不同的参数,把代码编译成h5、小程序、React Native等等
编译流程
ast
抽象语法树(AST)或者语法树(syntax tree),是源代码的抽象语法结构和树状表现形式,这里特指编程语言的源代码
parse tree
具体语法树,通常称作分析树。一般的,在源代码的翻译和编译过程中,语法分析器创建出分析树。一旦AST被创建出来,在后续的处理过程中,比如语义分析阶段,会添加一些信息。
编译
无论是h5、小程序、还是RN,都有一个共同的部分:都可以将源代码作为纯文本解析为抽象语法树(AST)的数据结构。
当源代码解析成AST后,根据不同的平台的语法树规则,产生对应平台的语法树,然后产生代码。
这样就可以做到一套代码,不同平台执行。
taro编译步骤
- 先parse,将代码解析(Parse)成抽象语法树(AST)
- 然后对AST,进行遍历(traverse)和替换(replace)
- 最后是生成,根据最新的AST生成编译后的代码
封装公共组件需要注意什么?
前端组件化发展之路
- 静态页面的公众资源封装----比如公用的js文件、css文件在多个页面复用(模块化思想 交互少)
- 早期动态页面 把具有同一类的业务逻辑的页面单独组建起来,在页面动态引入 在页面的公用页头、页尾等(页面级别封装)
- 以JQUI、easyUI、mini_UI以及ExtJS为代表的DOM封装---表现酷炫,但是艰难维护和修改
- 前端
MV***
时期到来的Angular、React、Vue 【组件思想 一切皆组件】
封装组件的好处
- 降低系统各个功能的耦合性
- 提高了功能内部的聚合性
- 前端功能化
- 代码维护难度降低
- 开发效率以及开发成本的提升
组件封装的难点
- 怎么做到最大程度的铜鼓但是又能支持相对的个性化(个性和共性的关系)
- 组件之间的数据共享
- 组件之间的事件传递
- 开发人员的抽象能力,编码能力,使用规范问题
组件抽象的基本原则
单一性
单一性的要求一个组件具有高内聚、低耦合的特征,它只负责一件事情,不要耦合一些没必要的逻辑,并且尽量不要和其它组件有着过多的双向交互和互相依赖的关系。单一性并不代表着不可以引用其他组件,当前组件可能是外层的容器组件。里面包含一些子组件,这样的设计是没有问题的
复用性/通用性
在设计组件的时候,一定要考虑组件的复用性或者说是通用性。这样是说:当组件封装好之后,可以在类似的场景中直接使用或者调用。这就要求我们在设计的时候考虑下组件功能的通用性,以及考虑组件入参的合理性
这时候有两种情况:
一种是很多不同的项目之间,可能存在类似的使用场景,因此会提炼出一个公共的组件是为了复用。一般我们称之为基础组件或业务组件、公共组件
另一种是在项目内部,仅在当前场景下作为一个独立的木块可以抽取出来作为一个组件,暂且称之为项目组件
公共组件和项目组件在设计上的侧重也有所不同,公共组件要更多的考虑通用性,通过一个组件满足不同项目中相似的使用场景。而项目组件更多的是处理当前业务中的特殊场景,可能是页面拆解后的不同模块,特可能是不同操作的弹框,往往这种组件不适合直接嫁接其他项目中
所以对于一个组件来说,个人以为也不能一味追求通用性饰器变得难以维护
分离处理
抽离组件的时候最好做一下业务层和视图层的分离处理,其中视图层主要负责页面展示样式和交互,业务层主要负责处理业务逻辑,比如接口调用,数据结构调整等。这样做的好处除了职责分离,还有有效提高组件性能,比如新增和编辑两个操作需要分别调用不同接口时,业务层和视图层的分离处理可以避免组件中耦合对新增或编辑的判断,它们可以共有一个视图,并在各自的业务层实现不同的业务逻辑
组件库设计有什么原则呢?
标准性
- 任何一个组件都应该遵守一套标准,可以使得不同区域的开发人员据此标准开发出一套标准统一的组件
独立性
- 描述组件的细粒度,遵循单一职责原则,保持组件的纯粹性
- 属性配置等 API 对外开放,组件内部对外封闭,尽可能地少与业务耦合
复用和易用
- UI 差异,消化在组件内部(并不是只写 if/else)
- 输入输出友好,易用
使用 SPOT 法则
Single Point Of Truth ,尽量不要重复代码
避免暴露组件内部实现
避免直接操作 dom,避免使用 ref
- 使用父组件的 state 控制子组件的状态而不是直接通过 ref 操作子组件
入口处检查参数的有效性,出口处检查返回的正确性
无环依赖原则(ADP)
- 无环依赖原则(ADP)为我们解决包之间的关系耦合问题
稳定抽象原则(SAP)
- 组件的抽象程度与其稳定长度成正比
- 一个稳定的组件应该是抽象的(逻辑无关)
- 一个不稳定的组件应该是具体的(逻辑相关的)
- 为降低组件之间的耦合度,我们要针对抽象组件编程,而不是针对业务实现编程
避免冗余状态
- 如果一个数据可以由另外一个state变换得到,那么这个数据就不是一个state,只需要写一个变换的处理函数,在Vue中可以使用计算属性
- 如果一个数据是固定的,不会变化的常量,那么这个数据就如同HTML固定的站点标题一样,写死或者作为全局配置属性等,不属于state
- 如果兄弟组件用于相同的state,那么这个state应该放到更高的层级,使用props传递到两个组件中
合理的依赖关系
- 父组件不依赖子组件,删除某个子组件不会造成功能异常
扁平化参数
- 除了数据,避免复杂的对象,尽量只接收原始类型的值
良好的接口设计
- 把组件内部可以完成的工作做到极致,虽然提倡拥抱变化,但接口不是越多越好
- 如果常量变为props能应对更多的场景,那么就可以作为props,原有的常量可以作为默认值。
- 如果需要为了某一调用者编写大量特定需求的代码,那么可以烤炉通过扩展等方式构建一个新的组件
- 保证组件的属性和事件足够给大多数的组件使用
api尽量和已知概念保持一致
设计搜索组件(具有自动补全功能组件),你需要考虑的问题是什么?
基本实现原理
首先我们需要明确一下需求,我们要做的是一个自动补全组件,类似与Google的搜索框。主要是根据用户的输入,来联想一些用户可能会搜索的词,然后提示用户来提升用户体验。
通用组件
我们设计组件的原则,首先看的组件的通用性、可移植性和扩展性,组件的粒度要小,其次是要保证安全性。
考虑大概设计
- 这个组件由简单的一个文本和搜索按钮还有右侧扩展区组成
- 文本框监听有focus、blur、change等事件
focus、blur由组件自身处理显示隐藏
change主要吹props的题词搜索事件
search处理props的搜索事件
右侧的扩展区通过{props.children} react
或者slot vue
来处理
change 和 search 也可以通过emit事件来进行处理
安全性
基本的处理就是上述的内容,但是作为一个搜索组件,我们更要防止其他人通过这个组件来攻击服务器。所以我们要处理用户的每一次输入,进行转义后提交,后端人员也必须做相应的字符处理流程。
性能问题
用户如果一直在不停的输入/删除,我们需要不停的进行服务器请求,有没有一个方式可以有效的限制用户的请求数,又不影响用户体验的方法呢?当然有的,那就是利用防抖/节流,这里还是建使用防抖来进行限制。
其他方向
- 在客户端做一些敏感词的提示会比较好
- 保存用户的一些输入/搜索记录,后端在做联想搜索时会更加方便
- 增加一些用户体验方面的功能,比如一些展示效果,一键清空内容之类的功能
注意事项
- 不要无限制的扩充组件的功能,防止臃肿
- 要尽可能解耦,用多组件组合的方式,或者使用HOC来协助
- 在必要的情况下,可以定义一些配置项,但也不要过于复杂化
注重业务需求和用户体验是最重要的,不要为了实现而实现,在保证用户体验的前提下,多思考下对其他组件和其他协作开发者的影响。
说一下对 ESLint 的了解? 如何使用? 它的工作原理?
Lint 工具简史
在计算机科学中,lint 是一种工具的别称,它用来标记代码中,某些可疑的、不具结构性(可能会造成 bug)的语句。它是一种静态程序分析工具,最早使用 C 语言,在 UNIX 平台上开发出来。后来它成为通用术语,可用于描述在任何一种编程语言中,用来标记代码中有疑义语句的工具。
在 JS 20 多年的发展历程中,也出现过许许多多的 lint 工具,JSLint、JSHint、ESLint 等。
JSLint 是最早的 lint 工具,但是它的 lint 规则是不可以自定义的。JSHint 继承自 JSLint,增强了可配置性。随后又出现了 ESLint,有可自定义的 rules、提供了完善的插件机制、可定位到具体的 rules。
ESLint 由 Nicholas C.Zakas 《JavaScript 高级程序设计》作者,于 2013 年 6 月创建,它的出现因为 Zakas 想使用 JSHint 添加一条自定义的规则,但是发现 JSHint 不支持,于是自己开发了一个。
ESLint 号称下一代的 JS Linter 工具,它的灵感来源于 PHP Linter,将源代码解析成 AST,然后检测 AST 来判断代码是否符合规则
。ESLint 默认使用 Espree 将源代码解析成 AST,然后你就可以使用任意规则来检测 AST 是否符合预期,这也是 ESLint 高扩展性的原因。
但是,那个时候 ESLint 并没有大火,因为需要将源代码转成 AST,运行速度上输给了 JSHint,并且 JSHint 当时已经有完善的生态(编辑器的支持)。真正让 ESLint 大火是因为 ES6 的出现
。
ES6 发布后,因为新增了很多的语法,JSHint 短期内无法提供支持,而 ESLint 只需要有合适的解析器就能够进行 lint 检查。这是 babel 为 ESLint 提供了支持,开发了 babel-eslint,让 ESLint 成为最快支持 ES6 语法的 lint 工具
。
总结:
JS 的 linter 工具发展历史其实也不算短,ESLint 之所以能够后来者居上,
主要原因还是 JSLint 和 JSHint 采用自顶向下的方式来解析代码
,并且早期 JS 的语法万年不更新,用这种方式能够以较快的速度来解析代码,找到可能存在的语法错误和不规范的代码。但是 ES6 发布之后,JS 语法发生了很多的改动,比如:箭头函数、模板字符串、扩展运算符……,这些语法的发布,导致 JSHint 和 JSLint 如果不更新解析器就无法检测 ES6 的代码。而 ESLint 另辟蹊径,采用 AST 的方式对代码进行静态分析,并保留了强大的可扩展性和灵活的配置能力
。这也告诉我们,在日常的编码过程中,一定要考虑到后续的扩展能力。正是因为这个强大扩展能力,让业界的很多 JS 编码规范能够在各个团队进行快速的落地,并且团队自己定制的代码规范也可以对外共享。
为什么要用 ESLint?
ESLint 其实早在 2013 年 7 月就发布了,ESLint 对于保持代码风格的一致性,增强代码可读性非常不错,另外也方便团队进行合作。
保持一致性就意味着要对我们编写的代码增加一定的约束,ESLint 就是这么一个通过各种规则(rule)对我们的代码添加约束的工具。JS 作为一种动态语言,写起来可以随心所欲,bug 遍地都是,但是通过合适的规则来约束,能让我们的代码更加健壮,工程更可靠。
现实生活中,我们也不自觉中遵守和构建着各种不同的规则。新的规则被构建是因为我们在某些方面有了更多的经验总结,将其转变为规则可能是希望以后少踩坑,也能共享一套最佳实践,提高工作效率。同样,ESLint 的核心就是包含各种各样的规则,这些规则大多为总舵开发者经验的总结:
- 有的可以帮我们避免错误
- 有的可以帮我们写出最佳实践的代码
- 有的可以帮我们规范变量的使用方式
- 有的可以帮我们规范代码格式
- 有的可以帮我们更合适的使用新语法
总得来说:ESLint 允许我们通过自由扩展,组合的一套代码应当遵循的规则,可以让我们的代码更为健壮,其功能不仅在于帮我们的代码风格保持统一,还能帮我们用上社区的最佳实践,减少错误。
工作原理
ESLint 默认使用 Espree 解析器解析 JS 生成抽象语法树。解析器是将代码解析成 AST 的工具,ES6、react、vue 都开发了对应的解析器,所以 ESLint 能检测它们,ESLint 也是因此一统前端 Lint 工具的。
常用的解析器还包括以下几种:
- Esprima: Espree 就是基于 Esprima 改良的
- Babel-eslint:一个对 Babel 解析器的包装,当项目中使用了 babel,babel 解析器就会把你的 code 转换成 AST,然后该解析器会将转换其转换为 ESLint 能懂的 ESTree。这个目前用的也比较多,目前不在维护和更新,现在升级为
@babel/eslint-parser
- @typescript-eslint/parser:将 TypeScript 转换为 ESTree 兼容的形式来在 ESLint 中使用。
无论使用哪种解析器,本质是为了将 code 转换成 ESLint 能够懂得 ESTree。
- 深度遍历 AST,监听匹配过程,ESLint 会使用 AST 去分析代码中的模式,以"从上至下"在"从下至上"的顺序遍历每个选择器两次
- 触发监听选择器的 rule 回调,在深度遍历的过程中,生效的每条规则都会对应其中的某一个或者多个选择器进行监听,每当匹配到选择器,监听该选择器的 rule,都会触发对应的回调。
使用方式
安装: npm install -g eslint
生成配置文件: 在项目根目录执行init,生成 .eslintrc 文件。在init时,要求在根目录讯在package.json。当然也可以直接复制个现成的 .eslintrc.js 文件。
eslint --init
自定义配置项:根据规则文档,编辑 .eslintrc.js 文件内容。
module.exports = {
env: {
node: true,
},
rules: {
// 强制使用一致的缩进
indent: ["warn", "tab"],
// 禁止空格和 tab 的混合缩进
"no-mixed-spaces-and-tabs": 1,
// 禁用 debugger
"no-debugger": 1,
// 禁止不必要的布尔转换
"no-extra-boolean-cast": 1,
// 强制所有控制语句使用一致的括号风格
curly: 1,
// 禁止使用多个空格
"no-multi-spaces": 1,
// 要求在函数标识符和其调用之间有空格
"func-call-spacing": 1,
// 强制在函数括号内使用一致的换行
"function-paren-newline": ["warn", "never"],
// 强制隐式返回的箭头函数体的位置
"implicit-arrow-linebreak": 1,
// 强制在对象字面量的属性中键和值之间使用一致的间距
"key-spacing": 1,
// 强制在关键字前后使用一致的空格
"keyword-spacing": 1,
// 要求调用无参构造函数时有圆括号
"new-parens": 1,
// 禁止出现多行空行
"no-multiple-empty-lines": 1,
// 要求使用分号代替 ASI
semi: ["warn", "always"],
// 要求操作符周围有空格
"space-infix-ops": 1,
},
};
对 to B 和 to C 的业务的理解?
B 和 C 的含义
B 其实是 Bussiness 的缩写,直译的意思就是商业,大家普遍把它作为机构客户的代名词。C 其实是 Consumer 的缩写,直译的意思是消费者、用户或者顾客,因为日常生活中接触的绝大部分直接消费者都是个人,所以大家普遍把 C 作为个人客户的代名词。
- To B 就是 To business, 面向企业或者特定用户群体的面商类产品
- To C 就是 To customer, 产品面向消费者
- To C 产品是你去挖掘用户需求,是创造,从无到有,更注重的是用户体验
- To B 产品是公司战略或相关方给你提出要求,产品经理将这类「线下已有的需求」系统化,达到提高现有流程的效率的目的。产品更注重的是功能价值以及系统性
两者的区别以及需要注意的点
- 1.
业务形态不同
。ToC 的需求更多的是围绕衣食住行来展开的,ToB 的需求更多的是围绕机构所处的某个行业或者某个领域来展开的,场景更为复杂多样。总之,ToC 是生活,是因点生点
; ToB 是面向生产,是因面生点
。 - 2.
产品需求不同
。ToC 作为独立的个体所存在,面向的用户是更加大众的: 90 后的叔叔阿姨,00 后,10 后,对产品的需求是是功能的外部化,体验和视觉效果要好。ToB 更多的考虑的是产品的功能时效化以及专业化,要求产品能够实实在在的价值和效用。 - 3.
产品单体消费量不同
。ToC 的消费能力一般情况下要远小于 ToB 的消费能力.所以一般 ToC 需要一定的客户群体才能支撑起一个业务的发展。 - 4.
性价比要求不同
。ToC 对于性价比的实际要求要远低于理论需求,因为 ToC 大多数无法具有准确分析性的专业能力,所以产生了贵的就是好的的理论,相比而言 ToB 具有相对专业判断能力,而且需要考虑到投入/产出比,同时具有需要实现性价比的意愿和能力。 - 5.
市场声誉影响力不同
。个人更容易受外部环境的影响而做出不适合自己的决策。具有很好市场声誉(市场曝光度比较高)的企业,往往对 ToC 更具有吸引力。这个就是从众心理的“妙用”。反之 ToB 除了会考虑市场声誉(这个可能是进入门槛),更多的会关注产品本身的质量、效能,然后综合评估产品的可购买性。一句话总结,ToC 感性,ToB 理性。 - 6.
售后要求不同
。ToC 对产品的售后要求一般比较低,商家也多是标准化服务,其中缘由一是产品的生命周期偏短,很多属于”阅后即焚”类型的产品;二是因为产品的技术含量并不十分复杂,大家可以 DIY 维修。反观 ToB ,其对产品的售后要求普遍偏高,不仅因为量大,而且因为复杂。所以 ToB 的行业一般都建立完备的客户服务系统,包括产品维修、咨询、投诉等等。 - 7.
决策体系不同
。ToC 的决策相对简单,对于是否购买,有时候只需要一个人就能决定,ToB 的决策流程相对复杂从部门门的相互合作,经过 boss 等等,所以需要的时间也相对较长。 - 8.
可拓展性不同
。所谓的可拓展性,指的就是除了一种商品之外,是否还有向其出售更多商品的可行性。这个在流量经济时代尤为重要。基于以上所述的种种差别和特殊属性来看,ToC 的可拓展性较强,可以实现以点带面,一件极具诱惑力的产品就可能把客户牢牢抓住,比如外卖。这个也是为什么现在很多人看好美团的原因。ToB 的可拓展性偏弱,只能实现以点带点,面上的横向拓展难度很大,纵向产业链还有些可能。
陷阱
个人客户是 ToC 的范畴,但是机构客户就一定属于 ToB 么?其实未必。很多你所谓的 B 端客户,其实只能算作是大 C 客户! (包括一部分中小型企业)
是否了解 glob 库,glob 是如何处理文件的?是否有别的方法?
程序需要对磁盘文件进行管理,就需要读取磁盘上的文件列表,然后可能会需要判断文件夹或文件名,还可能需要递归扫描子目录。glob 库专门用来扫描磁盘文件,并且返回我们需要的文件类型
。
glob 工具是基于 JS,使用 minimatch 库来进行匹配。node 的 glob 模块允许你使用 * 等符号,来写一个 glob 规则,想在 shell 里面一样获取匹配对应规则的文件
const glob = require("glob");
glob("**/*.js", function (error, files) {
// files 就是我们得到的文件的列表
});
上述代码中,我们会递归查找当前目录下的所有.js 文件,因为我们使用了 */.js
通配符做查找条件。
glob 支持的通配符模式
glob 支持强大的匹配规则,但是要注意 glob 的匹配规则并不是正则表达式,详细支持如下:
* 匹配0到多个字符
? 匹配一个字符
[...] 匹配一个字符列表,类似正则表达式的字符列表
!(pattern|pattern|pattern) 反向匹配括号内的模式
?(pattern|pattern|pattern) 匹配0或1个括号内的模式
+(pattern|pattern|pattern) 匹配至少1个括号内的模式
*(pattern|pattern|pattern) 匹配0到多个括号内的模式
@(pattern|pattern|pattern) 精确匹配括号内的模式
** 匹配0到多个子目录,递归匹配子目录
其它特性
除上下文中的异步接口,glob 还支持 glob.sync() 同步接口,另外,glob 还支持大量的参数选项,比如 cwd、root 等等。
node 进行文件处理的几种方式
在使用 node 开发过程中很多时候会遇到对文件系统做各种处理操作
文件处理开发中常用的内置模块
path: 处理文件路径
fs: 操作文件系统
child_process: 新建子进程
process: 进程
比较好用的第三方模块
glob: 使用 shell 命令的模式匹配文件
trash: 文件放到回收站
前端怎样做单元检测?
在单元检测的时候,常用的方法论是:TDD和BDD.
TDD(Test-driven development):
其基本思路是通过测试推动开发的进行。从调用者的角度触发,尝试函数逻辑的各种可能性 ,进而辅助性增强代码质量。
BDD(Behavior-driven development):
其基本思路是通过预期行为逐步构建功能块。通过与客户讨论,对预期结果有初步的认知,尝试达到预期效果
目前前端测试框架有 Mocha、jasmine、jest 等,它们配合断言库来进行单元测试。断言库包括assert(nodejs自带的断言库)、chai等
- 普通测试
import { expect } from 'chai';
import add from '../src/common/add';
describe('加法函数测试', () => {
it('应该返回两个数之和', () => {
expect(add(4, 5)).to.be.equal(9);
});
});
/*
加法函数测试
√ 应该返回两个数之和
1 passing
*/
- 异步函数测试
import { expect } from 'chai';
import asyncFn from('../src/common/asyncFn');
describe('异步函数测试', () => {
it('async with done', async () => {
const res = await asyncFn();
expect(res).to.have.deep.property('status', 200);
});
});
/*
异步函数测试
√ async with done (176ms)
1 passing
*/
项目如何管理模块?
在一个项目内,当有多个开发者一起协作开发时,或者功能越来越多、项目越来越庞大时,保证项目井然有序的进行是相当重要的。一般会从下面几点来考证一个项目是否管理得很好:
- 可扩展性: 能够很方便、清晰得扩展一个页面、组件、模块
- 组件化: 多个页面之间共用的大块代码可以独立成组件、多个页面、组件之间共用的小块代码可以独立成公共模块
- 可阅读性: 阅读性良好(包括目录文件结构、代码结构),能够很快捷的找到某个页面、组件的文件,也能快捷的看出项目有哪些页面、组件
- 可移植性: 能够轻松的对项目架构进行升级,或移植某些页面、组件、模块到其他项目
- 可重构性: 对某个页面、组件、模块进行重构时,能够保证在重构之后功能不变、不会产生新bug
- 开发友好: 开发者在开发某一个功能时,能够有比较好的体验(举反例:多个文件相隔很远)
- 协作性:多人协作时,很少产生代码冲突、文件覆盖等问题
- 可交接性: 当有人离开这个项目的时候,交接给其他人也是可以的
多个项目之间,如何管理好项目之间联系,比如公用组件、公共模块等,保证快捷高效开发、不重复造轮子、也是很重要的。一般会从下面几点来考证多个项目之间是否管理的很好:
- 组件化:多个项目公用的代码应当独立出来,成为一个单独的组件项目
- 版本化:组件项目与应用项目都应当版本化管理,特别是组件项目的版本都应当符合semver语义化版本规范
- 统一性:多个项目之间应当使用相同的技术选型、UI框架、脚手架、开发工具、构建工具、测试库、目录规范、代码规范等,相同功能都应指定使用固定某一个库
- 文档化:组件项目一定需要相关的文档,应用项目在必要的时候也要形成相应的文档
微服务和单体应用的区别是什么?
单体应用: 传统架构,集所有功能于一身构建一个项目,不可分开部署
单体架构所有的模块都公用一个数据库,存储方式比较单一,微服务每个模块都可以使用不同的存储方式(比如有的是用redis,有的是用mysql等等),数据库也是单个模块对应着自己的数据库
单体架构所有的模块开发所使用的技术一样,微服务每个模块都可以使用不同的开发技术,开发模式更灵活
单体应用是将所有功能模块放在一个单体进程中,并且通过在不同的服务器上面复制这个单体进行扩展
什么是微服务?
所谓的微服务是SOA架构下的最终产物,该架构的设计目标是为了肢解业务,使得服务能够独立运行。
微服务可以按照业务划分,将一组特定的业务划分成一个服务,每个服务都有自己对的数据库,独立部署,服务直接通过Rest API进行通讯。每一个独立运行的服务组成整合系统
总结下,微服务是,由单一应用程序构成的小服务,拥有自己的进程与轻量化处理,服务依业务功能设计,以全自动的方式部署,与其他服务使用HTTP api通讯。同时,服务会使用最小的规模的集中管理(例如docker)技术,服务可以用不同的编程语言与数据库等。微服务架构是将复杂臃肿的单体应用进行细粒度的服务化拆分,每个拆分出来的服务各自独立打包部署,并交由小团队进行开发和运维,从而极大地提高了应用交付地效率
微服务设计的原则: 各司其职 服务高于可用性和可扩展性
用微服务有什么好处?
-
微服务应用的一个最大的优点是: 它们往往
比传统的应用程序更有效的利用计算资源
。这是因为它们通过扩展组件来处理性能瓶颈问题。这样一来开发人员只需要为额外的组件部署计算资源,而不需要部署一个完整的应用程序的全新迭代。最终的结果是有更多的资源可以提供给其他的任务。 -
微服务应用的另一个好处是:
它们更快并且更容易更新
。当开发者对一个传统的单体应用程序进行时,他们必须做详细的QA测试,以确保变更不会影响其他特性或功能。但有了微服务,开发者可以更新应用程序的单个组件,而不影响其他的部分。测试微服务应用程序仍然是必需的,但它更容易识别和隔离问题,从而加快开发速度并支持DevOps和持续应用程序开发 -
微服务应用的第三个好处:
微服务架构有助于新兴的云服务
,比如事件驱动计算,类似AWS Lambda这样的功能让开发人员能够编写代码 处于休眠状态,直到应用程序事件触发。事件处理时才需要使用计算资源,而企业只需要为每次事件而不是固定数目的计算实例支付
通俗解释
易于开发和维护:
因为一个服务只专注一个特定的业务,业务就变得比较清晰,同时维护起来也是比较方便
单个服务启动比较快:
单个服务代码质量不会很多,启动起来就会很快
便于伸缩:
如果系统中有三个服务ABC,服务B的访问量比较大,我们可以将服务B集群部署
单体应用中,如果需要改动功能,那么则需要重新部署整个单体应用,而微服务不需要,只需要重新部署修改的功能模块的微服务。每一个功能模块都可以替换和独立维护的单元,完全体现了高度复用性、高度维护性,高度扩展性。
说一下面向对象的理解,面向对象有什么好处?
可以理解为在做一件事时是: "该让谁来做” 。那个谁就是对象,他要怎么做是他自己的事,最后就是一群对象合力把事情做好。
相比较于面向过程
的"步骤化”分析问题,面向对象
则是“功能化”分析问题,其优点体现在:
- 将数据和方法
封装
在一起,以接口的方式提供给调用者,调用者无需关注问题的解决过程; - 对象之间通过
继承
,减少代码的冗余,提高程序的复用性; - 通过
重载/重写
方法来拓展对象的功能;
以上的优点来源于面向对象的三大特征: 封装、继承和多态。
面向对象的三要素是什么,分别是什么意思?
封装
把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏
继承
使用现有类的所有功能并在无需重新编写原来的类的情况下对这些功能进行扩展
多态
一个类实例的相同方法在不同情形下有不同表现形式。多态机制使具有不同内部结构的对象可以共享相同的外部接口
了解函数式编程中的compose么?
compose是函数式编程中一个非常重要的函数,compose的函数作用就是组合函数的,将函数串联起来执行。将多个函数组合起来,一个函数的输出结果是另一个函数的输入函数,一旦第一个函数开始执行,就会像多米诺骨牌一样推导执行
简单小栗子
const greeting = (name) => `hello ${name}`;
const toUpper = (str) => str.toUpperCase();
const fn = compose(toUpper, greeting);
console.log(fn("sunny"));
// HELLO SUNNY
输入sunny
之后变成大写,最后输出 HELLO SUNNY
,使用函数组合就可以达到这个效果,需要greeting和toUpper两个函数
这就是compose的使用,主要有以下几点:
- compose参数就是函数,返回也是一个函数
除了第一个函数接收参数,其他函数接受的都是上一个函数的返回值,所以初始函数的参数是多元的,而其他函数的接收值是一元的
- compose函数可以接收任意的参数,所有的参数都是函数,且执行方向是自右向左的,初始函数一定到参数的最右面
知道这些之后,上面的例子,compose执行过程就很容易分析了。
compose还有一个好处是:如果在想加一个处理函数,不需要修改fn,再去执行一个compose即可
compose(fn1,fn)
手动实现
- 简单实现:递归实现
const compose = function (...funcs) {
let len = funcs.length,
count = len - 1,
result = null;
// 首先compse 返回的是一个函数
return function fn(...args) {
// 函数体里就是不断执行args函数,将上一个函数的执行结果作为下一个执行函数的输入参数,需要一个count来记录args函数列表的执行情况
result = funcs[count].apply(this, args);
// 递归退出条件
if (count <= 0) {
count = len - 1;
return result;
} else {
count--;
return fn.call(null, result);
}
};
};
// 测试
const greeting = (name) => `hello ${name}`;
const toUpper = (str) => str.toUpperCase();
const fn = compose(toUpper, greeting);
console.log(fn("sunny"));
- 迭代实现
function compose(...fns) {
let isFirst = true;
return (...args) => {
return fns.reduceRight((result, fn) => {
if (!isFirst) return fn(result);
isFirst = false;
return fn(...result);
}, args);
};
}
- lodash.js库的实现
var flow = function (funcs) {
var length = funcs.length;
var index = length;
while (index--) {
if (typeof funcs[index] !== "function") {
throw new TypeError("Expected a function");
}
}
return function (...args) {
var index = 0;
var result = length ? funcs[index].apply(this, args) : args[0];
while (++index < length) {
result = funcs[index].call(this, result);
}
return result;
};
};
var flowRight = function (funcs) {
return flow(funcs.reverse());
};
const greeting = (name) => `hello ${name}`;
const toUpper = (str) => str.toUpperCase();
const fn = flowRight([toUpper, greeting]);
console.log(fn("sunny"));
lodash的实现是从左到右实现的,但是也提供了从右到左的flowRight,还多了一层函数的校验,而且接收到的是数组而不是参数序列
讲一下你所了解的函数式编程
函数式编程是一种编程范式,我们常见的编程范式有命令式编程、逻辑式编程,常见的面向对象编程也是一种命令式编程。
-
如果说面向对象编程的思维方式: 把现实世界中的事物抽象成程序世界中的类和对象,通过
封装、继承和多态
来演示事物事件的联系,那么函数式编程的思维方式是:把现实世界的事物和事物之间的联系抽象到程序世界(对运算过程进行抽象
)。函数编程中的函数,不是指计算机中的函数,而是指数学中的函数。也就是说一个函数的值仅决定于函数参数的值,不依赖其他状态。并且相同的值输入得到相同的结果,纯函数
。 -
在函数式语言中,函数作为一等公民,可以在任何地方定义,在函数内或函数外,可以作为函数的参数和返回值,可以对函数进行组合。简单的理解就是:函数式编程用来描述数据(函数)直接的映射
优点
函数式编程的优点是由它的不可变性带来的,以下几点进行描述:
- 函数式编程可以抛弃this
- 打包过程中可以更好的利用tree shaking过滤无用代码
- 函数不依赖外部的状态,也不修改外部的状态,函数调用的结果不依赖函数调用的时间和调用的位置,这样的代码容易进行推理且不易出错,还可以把运行的结果进行缓存。同时,方便进行单元测试,方便进行处理
- 由于函数式语言是面向数学的抽象,更接近人的语言,而不是机器的语言,代码比较简洁,也更容易理解
风险
函数式编程的不可变性如果掺入可变性就带来了风险。如果一个纯函数掺入可变性就不再是纯函数啦,也就是如果函数依赖外部的状态接没有办法保证输出相同,就会带来副作用
。
const num=9;
function com(val){
return val===num
}
这个函数依赖外部的是num,如果num变成别的数字或者内容,那么的话相同的输入就变成不同的输出啦,这个副作用是不可避免的。一般副作用的来源:配置文件、数据库、获取用户的输入…
所有的外部交互都有可能带出副作用,副作用会给程序带来安全隐患,给程序带来不确定性,同时副作用不可能完全禁止,只能尽力控制它们在可控的范围内发生
你所理解的前端路由是什么?
基于hash
展示页面也就是切换#
后面的内容,呈现给用户不同的页面。现在越来越多的单页面应用基本都是基于hash实现的
特性:
url中hash值的变化并不会重新加载页面,hash值的改变,都会在浏览器的访问历史中增加一个记录,也就是能通过浏览器的回退、前进按钮控制hash的切换
我们可以通过hashchange事件,监听到hash值的变化,从而响应不同路径的逻辑处理
基于history
基于history新API(history.pushState()+popState事件)
window.history.pushState(null,null,"www.baidu.com")
这两个API的相同之处都会操作浏览器的历史记录,而不会引起页面的刷新。
不同之处在于:pushState会增加一条新的历史记录,而replace则会替换当前的历史记录
如何实现按需加载?
实现按需加载的方法
ES2020动态导入
import("./dynamic-module").then((module) => {
// do something
});
// 也支持await关键字
const module = await import("./dynamic-module");
vue中通过router配置
vue 中通过 router 配置,实现组件的按需加载。在一些单个组件文件较大的时候,采用按虚加载能够减少 build.js 的体积,优化加载速度(如果体积较小,那么采用按虚加载会增加额外的 http 请求,反倒增加了加载时间)
import Vue from "vue";
import App from "./App.vue";
import VueRouter from "vue-router";
Vue.use(VueRouter);
//AMD规范的异步载入
const ComA = (resolve) => require(["./components/A.vue"], resolve);
const ComB = (resolve) => require(["./components/B.vue"], resolve);
//CMD风格的异步加载
const ComA = (resolve) =>
require.ensure([], () => resolve(require("./components/A.vue")));
const ComB = (resolve) =>
require.ensure([], () => resolve(require("./components/B.vue")));
const router = new VueRouter({
routes: [
{
name: "component-A",
path: "/a",
component: ComA,
},
{
name: "component-B",
path: "/b",
component: ComB,
}
],
});
new Vue({
el: "#app",
router: router,
render: (h) => h(App),
});
vue 或者 webpack 配置
//webpack.config.js
output: {
path: path.resolve(__dirname, './dist'),
publicPath: '/dist/',
filename: 'build.js',
//添加chundkFilename
chunkFilename: '[name].[chunkhash:5].chunk.js'
}
webpack打包模块工具实现
在大型项目中,build.js 可能过大,导致页面加载时间过长。这个时候就需要 code splitting ,将文件分割成块,我们可以定义一些分割点,根据这些分割点对文件进行分块,并实现按虚加载。
请说明JS进行压缩、合并、打包实现的原理是什么?为什么需要压缩、合并、打包?
压缩
-
- 去掉注释代码
-
- 去掉换行符、空格等
-
- 规范的变量名、函数名对于代码阅读有很大的帮助、但是对于机器来说,没有太多的意义,所以缩短变量名长度,可以有效减少代码文件的体积
合并
合并即多个JS代码后在输出,nginx有个concat模块可以多个资源合并再输出;
另外可以自建Combo服务,从URL中获取对应需要获取的静态资源名,再从对应文件中获取后合并输出。
目前前端项目合并一般会在打包过程中已经处理完
打包
模块打包,一般至少有一个入口文件,从入口文件代码中,根据代码中出现的import或者require之类语法,解析推断出这个文件所依赖的资源模块,然后再去分别解析每个资源模块的依赖,最终形成整个项目所有用到的文件自己的依赖关系树
。之后根据这个依赖树对每个资源文件处理,其中过程可能会有代码编译、优化等,结果会被打包到目标文件中
为什么需要压缩、合并、打包
-
- 压缩可以减少JS代码体积大小,加快HTTP请求速度
-
- 因为浏览器对同一个域名有并发请求限制,过多请求会导致HTTP请求队头阻塞等问题,合并可以减少HTTP请求数量
-
- 对于项目代码的模块化,一些规律性的重复工作,甚至是整个前端项目的工程化,提升效率打包工具必不可少
分别列出一个常用工具或插件?
- 压缩: Uglifyis WebpackPlugin
- 合并:Combo
- 打包:Webpack
PWA 是什么,对 PWA 有什么了解?
PWA(Progressive web apps,渐进式 Web 应用)运用现代的 web api 以及传统的渐进式增强策略来创建跨平台 web 应用程序。
什么是 PWA 应用?
PWA 应用是指那些使用指定技术和标准模式来开发的 web 应用,这将同时赋予它们 web 应用和原生应用的特性。
举例:PWA 既能像网站那么快捷,又能向 qq、微信一样离线在本地运行。
什么使应用成为 PWA?
总体依据:当应用程序满足某些要求时,可以将其视为 PWA,或者实现一组给定的功能:离线工作、可安装、易于同步,可以发送推送通知。
辨别工具:还有一些工具可以按百分比衡量应用的完整性。(Lightouse 目前是最受欢迎的工具)通过实施各种技术优势,我们可以使应用程序更加渐进式,从而最终获得更高的 Lighthouse 得分。但这只是一个粗略的指标。
辨别原则:这里有一些关键的原则来辨别一个 web 应用是否是一个 PWA 应用,它应该具有以下特点:
- Discoverable,内容可以通过搜索引擎发现
- Installable,可以出现在设备的主屏幕
- Linkable,可以简单地通过一个 URL 来分享它
- Network independent,它可以在离线状态或者是在网速很差的情况下运行
- Progressive,在老版本的浏览器仍旧可以使用,在新版本的浏览器上可以使用全部功能。
- Re-engageable,无论何时有新的内容都可以发送通知。
- Responsive,它在任何具有屏幕和浏览器的设备上可以正常使用 -- 包括手机、平板电脑、笔记本、电视、冰箱等等。
- Safe,在个人和应用之间的连接是安全的,可以阻止第三发访问个人的敏感数据
PWA 优势
我们需要在设计网站时时刻记住 PWA 的优势。app shell 允许网站:
- 可访问:即使看起来像个本地应用,请记住它仍然是个网站 --- 可以点击页面中的连接并分享给你的朋友
- 渐进式:先从"好用的,旧式的网站"出发,一步步渐进式的增加新特性,记住要随时侦测浏览器是否可用这些新增加的特性,同时注意处理任何由于浏览器不支持而导致的 error。例如,service workers 可以让离线工作成为可能,同时提高网站的体验,但是记住就算没有 service worker 网站也应该能运行良好。
- 响应式:响应式页面设计也适用于渐进式 web 应用,主要是针对移动端设备。有许多不同的设备配置有浏览器--你需要让网站支持不同的屏幕尺寸,视窗(viewport)或者是不同的像素密度(pixel density),常用的技术有 viewport meta tag,CSS media queries,Flexbox。
PWA 存在的问题
- 支持率不高:现在 ios 手机端不支持 PWA,IE 也暂时不支持
- Chrome 在中国桌面版占有率还是不错的,安卓移动端上的占有率却很低
- 各大厂商还未明确支持 pwa
- 依赖的 GCM 服务在国内无法使用
- 微信小程序的竞争
PWA 关键技术
Manifest 实现添加到主屏幕
<head>
<title>Minimal PWA</title>
<meta name="viewport" content="width=device-width, user-scalable=no" />
<link rel="manifest" href="manifest.json" />
<link rel="stylesheet" type="text/css" href="main.css" />
<link rel="icon" href="/e.png" type="image/png" />
</head>
// manifest.json
{
"name": "Minimal PWA", // 必填 显示的插件名称
"short_name": "PWA Demo", // 可选 在APP launcher和新的tab页显示,如果没有设置,则使用name
"description": "The app that helps you understand PWA", //用于描述应用
"display": "standalone", // 定义开发人员对Web应用程序的首选显示模式。standalone模式会有单独的
"start_url": "/", // 应用启动时的url
"theme_color": "#313131", // 桌面图标的背景色
"background_color": "#313131", // 为web应用程序预定义的背景颜色。在启动web应用程序和加载应用程序的内容之间创建了一个平滑的过渡。
"icons": [
// 桌面图标,是一个数组
{
"src": "icon/lowres.webp",
"sizes": "48x48", // 以空格分隔的图片尺寸
"type": "image/webp" // 帮助userAgent快速排除不支持的类型
},
{
"src": "icon/lowres",
"sizes": "48x48"
},
{
"src": "icon/hd_hi.ico",
"sizes": "72x72 96x96 128x128 256x256"
},
{
"src": "icon/hd_hi.svg",
"sizes": "72x72"
}
]
}
service worker实现离线缓存
Service Worker 是 Chrome 团队提出和力推的一个 WEB API ,用于给 web 应用提供高级的可持续性的后台处理能力。
Service Workers就像介于服务器和网页之间的拦截器,能够拦截进出的http请求,从而完全控制你的网站。
最主要的特点:
- 在页面中注册并安装成功后,运行于浏览器后台,不受页面刷新的影响,可以监听和拦截作用域范围内所有页面的HTTP请求。
网站必须使用HTTPS
。除了使用本地开发环境调用时(如域名使用localhost)- 运行于浏览器后台,可以控制打开的作用域范围下所有的页面要求
- 单独的作用域范围,单独的运行环境和执行线程
- 不能操作页面DOM,但可以通过事件机制来处理
- 事件驱动型服务线程
简单介绍使用图片base64编码的优点和缺点
一般一些网站的小图标可以使用base64图片
来引入。
base64 编码是一种图片处理格式,通过特定的算法将图片编码成一长串字符串,在页面上显示的时候,可以用该字符串来代替图片的url 属性。
优点
减少图片的http请求数
缺点
- 编码后的大小会比原文件大1/3
如果把大图片编码到html/css 中,不仅会造成文件体积的增加,影响文件的加载速度,还会增加浏览器对 html 或 css 文件解析渲染的时间。
- 无法缓存
使用 base64 无法直接缓存,要缓存只能缓存包含 base64 的文件,比如 HTML 或者 CSS,这相比域直接缓存图片的效果要差很多。
- 兼容性的问题,ie8 以前的浏览器不支持。
说一下base64的编码方式
Base64是一种编码方式
。
选用大小写字母、数字0-9、+和/的64个可打印字符来表示二进制数据。 26 + 26 + 10 + 2 = 64
将二进制数据每三个字节为一组。一共是3*8=24bit(位),划分为四组,每一组为6bit(位)。如果要编码的二进制不是3的倍数,会用00
在末尾补足,然后在编码的末尾加上1-2个=
号,表示补了多少字节,解码的时候会去掉。将3个字节的二进制数据编码为4字节的文本,解码的时候会去掉。将3字节的二进制数据编码为4字节的文本,是可以让数据在邮件正文、网页等直接显示的
base64编码通俗解释
Base64是传输8bit字节码的编码方式,Base64可以将ASCII字符串或者二进制编码只包含A-Z、a-z、0-9、+、/这64个字符(26个大写字母,26个小写字母,10个数字,一个+,一个/ 刚好组成64个字符);这64个字符用6个bit就可以全部表示出来,一个字节有8个bit位,那么还剩下两个bit位,那这两个bit位用来补充,转换完空的结果是用=
来补位,总之要保证最后编码出来的字节数是4的倍数
字符串“Xu”经过Base64编码后变为“WHU=”。
字符串“X”经过Base64编码后变为“WA==”
注意事项
因为标准的Base64会有+
和/
在URL中不能直接做参数,于是出现了一种"url safe"的Base64,将+
和/
转换为-
和_
。因为=
用在URL和cookie会有歧义,所以很多Base64会把=
去掉。由于Base的长度永远是4的倍数,所以只要加上=
把长度变为4的倍数,就可以解码了。
扫描二维码登录网页是什么原理,前后两个事件是如何联系的?
核心过程应该是:
-
浏览器获得一个
临时id
-
通过
长链接
等待客户端扫描带有此 id 的二维码后 -
从长链接中获得客户端上报给 server 的
帐号信息
进行展示 -
并在客户端点击确认后,获得
服务器授信的令牌
,进行随后的信息交互过程 -
在超时、网络断开、其他设备上登录后,此前获得的令牌或丢失、或失效,对授权过程形成有效的安全防护
个人理解二维码登录网页的基本原理是,用户进入登录网页后,服务器生成一个 uid 来标识一个用户。对应的二维码对应了一个对应 uid 的链接,任何能够识别二维码的应用都可以获得这个链接,但是它们没有办法和对应登录的服务器响应。比如微信的二维码登录,只有用微信识这个二维码才有效。当微信客户端打开这个链接时,对应的登录服务器就获得了用户的相关信息。这个时候登录网页根据先前的长连接获取到服务器传过来的用户信息进行显示。然后提前
预加载
一些登录后可能用到的信息。当客户端点击确认授权登陆后,服务器生成一个权限令牌给网页,网页之后使用这个令牌进行信息的交互过程。由于整个授权的过程都是在手机端进行的,因此能够很好的防止 PC 上泛滥的病毒。并且在超时、网络断开、其他设备上登录后,此前获得的令牌或丢失、或失效,对授权过程能够形成有效的安全防护。
网页验证码是干嘛的,是为了解决什么安全问题?
-
区分用户是计算机还是人的公共全自动程序。可以防止恶意破解密码、刷票、论坛灌水
-
有效防止黑客对某一个特定注册用户用特定程序暴力破解方式进行不断的登陆尝试
实现单点登录的原理
什么是单点登录?
单点登录sso(single sign on)是一个多系统共存的环境下,用户在一处登录后,就不用再其他系统中登录,也就是用户的一次登录得到其他所有系统的信任
比如现有业务系统A、B、C以及sso系统,第一次访问A系统时发现没有登录,引导用户到sso系统上登录,根据用户的登录信息,生成唯一的一个token凭证返回给用户。后期用户访问B、C系统的时候,携带上对应的凭证到sso系统去校验,校验通过后就可以单点登录
单点登录在大型网站中使用的频率比较高,例如,阿里旗下有天猫、淘宝、支付宝等网站,其背后的成百上千的子系统,用户操作一次或者交易可能涉及到很多子系统,每个子系统都需要验证,所以提出来用户登录一次就可以访问相互信任的应用系统
单点登录有一个独立的认证中心,只有认证中心才能接受用户的用户名和密码等信息的认证,其他系统不提供登录入口,只接受认证中心的间接授权。间接授权通过令牌实现,当用户提供的用户名和密码通过认证中心认证之后,认证中心会创建授权令牌,在接下来的跳转过程中,授权令牌作为参数发送给各个子系统,子系统拿到令牌即得到了授权,然后创建局部会话
单点登录原理
单点登录有同域和跨域两种场景:
同域
适用场景: 都是企业自己的系统,所有系统都使用同一个一级域名通过不同的二级域名来区分
比如:企业有一个一级域名为zf.com,我们有三个系统分别是门户系统(sso.zf.com),应用1(app1.zf.com)和应用2(app2.zf.com),需要实现系统之间的单点登录,实现架构如下: 核心原理
-
门户系统设置的
cookie的domain为一级域名也是zf.com
,这样就可以共享门户的cookie给所有的使用该域名xxx.zf.com的系统 -
使用session等技术让所有系统
共享session
-
这样只要门户系统登录之后无论跳转应用1或是应用2,都能通过门户cookie中的sessionId读取到session中登录信息实现单点登录
跨域
单点登录之间的系统域名不一样,例如第三方系统。由于域名不一样不能共享cookie了,需要一个独立的授权系统,即一个独立的认证中心(passport),子系统本身将不参与登录操作,当一个系统登录成功之后,passport将会颁发一个令牌给子系统,子系统可以拿着令牌去获取各自的资源,为了减少频繁认证,各个子系统在被passport授权之后,会建立一个局部会话,在一定时间内无法再次向passport发起认证
-
用户第一次访问应用系统的时候,因为没有登录会被引导到
认证系统
中进行登录 -
根据用户提供的登录信息,认证系统进行身份校验,如果通过,返回给用户一个认证凭据----令牌
-
用户再次访问别的应用的时候,带上令牌作为认证凭证
-
应用系统收到请求后会把令牌送到认证服务器进行校验,如果通过,用户就可以在不用登录的情况下访问其他信任的业务服务器
utf-8 和 asc 码有什么区别?
ASCII => GB2312 => GBK => GB18030 => Unicode => utf-8
字节
- 计算机内部,所有信息最终都是一个二进制值
- 每一个二进制位(bit)有0和1两种状态,因此八个二进制位就可以组合出256种状态,这被称为一个字节(byte)
单位
- 8位 = 1字节
- 1024字节 = 1K
- 1024K = 1M
- 1024M = 1G
- 1024G = 1T
进制表示
let a = 0b10100;//二进制
let b = 0o24;//八进制
let c = 20;//十进制
let d = 0x14;//十六进制
console.log(a == b);
console.log(b == c);
console.log(c == d);
进制转换
- 10进制转任意进制 10进制数.toString(目标进制)
console.log(c.toString(2));
- 任意进制转十进制 parseInt('任意进制字符串', 原始进制);
console.log(parseInt('10100', 2));
ASCII
最开始计算机只在美国用,八位的字节可以组合出256种不同状态。0-32种状态规定了特殊用途,一旦终端、打印机遇上约定好的这些字节被传过来时,就要做一些约定的动作,如:
- 遇上0×10, 终端就换行;
- 遇上0×07, 终端就向人们嘟嘟叫;
又把所有的空格、标点符号、数字、大小写字母分别用连续的字节状态表示,一直编到了第 127 号,这样计算机就可以用不同字节来存储英语的文字了
这128个符号(包括32个不能打印出来的控制符号),只占用了一个字节的后面7位,最前面的一位统一规定为0
这个方案叫做 ASCII 编码
American Standard Code for Information Interchange:美国信息互换标准代码
GB2312
后来西欧一些国家用的不是英文,它们的字母在ASCII里没有为了可以保存他们的文字,他们使用127号这后的空位来保存新的字母,一直编到了最后一位255。比如法语中的é的编码为130。当然了不同国家表示的符号也不一样,比如,130在法语编码中代表了é,在希伯来语编码中却代表了字母Gimel (ג)。
从128 到 255 这一页的字符集被称为扩展字符集。
中国为了表示汉字,把127号之后的符号取消了,规定
- 一个小于127的字符的意义与原来相同,但
两个大于 127 的字符
连在一起时,就表示一个汉字; - 前面的一个字节(他称之为高字节)从 0xA1 用到 0xF7,后面一个字节(低字节)从 0xA1 到 0xFE;
- 这样我们就可以组合出大约
7000
多个(247-161)*(254-161)=(7998)简体汉字了。 - 还把数学符号、日文假名和ASCII里原来就有的数字、标点和字母都重新编成两个字长的编码。这就是
全角字符
,127以下那些就叫半角字符
。 - 把这种汉字方案叫做 GB2312。GB2312 是对 ASCII 的
中文扩展
GBK
后来还是不够用,于是干脆不再要求低字节一定是 127 号之后的内码,只要第一个字节是大于 127 就固定表示这是一个汉字的开始,又增加了近 20000 个新的汉字(包括繁体字)和符号。
GB18030 / DBCS
又加了几千个新的少数民族的字,GBK
扩成了GB18030
通称他们叫做 DBCS
Double Byte Character Set:双字节字符集。
在 DBCS 系列标准里,最大的特点是两字节长的汉字字符和一字节长的英文字符并存于同一套编码方案里
各个国家都像中国这样搞出一套自己的编码标准,结果互相之间谁也不懂谁的编码,谁也不支持别人的编码
Unicode
ISO 的国际组织废了所有的地区性编码方案,重新搞一个包括了地球上所有文化、所有字母和符 的编码! Unicode 当然是一个很大的集合,现在的规模可以容纳100多万个符号。
- International Organization for Standardization:国际标准化组织。
- Universal Multiple-Octet Coded Character Set,简称 UCS,俗称 Unicode
ISO 就直接规定必须用两个字节,也就是 16 位来统一表示所有的字符,对于 ASCII 里的那些 半角字符,Unicode 保持其原编码不变,只是将其长度由原来的 8 位扩展为16 位
,而其他文化和语言的字符则全部重新统一编码。
从 Unicode 开始,无论是半角的英文字母,还是全角的汉字,它们都是统一的一个字符!同时,也都是统一的 两个字节
- 字节是一个8位的物理存贮单元,
- 而字符则是一个文化相关的符号。
UTF-8
Unicode 在很长一段时间内无法推广,直到互联网的出现,为解决 Unicode 如何在网络上传输的问题,于是面向传输的众多 UTF 标准出现了,
Universal Character Set(UCS)Transfer Format:UTF编码
- UTF-8 就是在互联网上使用最广的一种 Unicode 的实现方式
- UTF-8就是每次以8个位为单位传输数据
- 而UTF-16就是每次 16 个位
- UTF-8 最大的一个特点,就是它是一种变长的编码方式
Unicode 一个中文字符占 2 个字节,而 UTF-8 一个中文字符占 3 个字节
UTF-8 是 Unicode 的实现方式之一
什么是死锁?
如果一组进程中每一个进程都在等待仅由该组进程中的其他进程才能引发的事件,那么该组进程是死锁的。换言之:死锁就是两个线程同时占用两个资源,但又在彼此等待对方释放锁
。
举例来说:有两个进程 A 和 B,A 持有资源 a 等待 b 资源,B 持有资源 b 等待 a 资源,两个进程都在等待另一个资源的同时不释放资源,就形成死锁。
死锁产生的四个必要条件
- 互斥条件:一个资源每次只能被一个进程使用。
- 请求和保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放
- 不剥夺条件:进程已获得的资源,在未使用完之前,不能强行剥夺
- 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系
死锁处理
预防死锁:
破坏四个必要条件中的一个或多个来预防死锁
避免死锁:
在资源动态分配的过程中,用某种方式防止系统进入不安全的状态
检测死锁:
运行时产生死锁,及时发现死锁,将程序解脱出来。
解除死锁:
发生死锁时,撤销进程,回收资源,分配给正在阻塞状态的进程。
1.预防死锁的方法:
破坏请求和保持条件:一次性的申请所有资源,之后不在申请资源,如果不满足资源条件则得不到资源分配。只获得初期资源运行,之后将运行完的资源释放,请求新的资源
破坏不可抢占条件:当一个进程获得某种不可抢占资源,提出新的资源申请,若不能满足,则释放所有资源,以后需要的时候则再次重新申请。
破坏循环等待条件:对资源进行排号,按照序号递增的顺序请求资源。若进程获得序号高的资源想要获取序号低的资源,则需要先释放序号高的资源。
2.死锁的解除方法:
抢占资源。从一个或多个进程中抢占足够数量的资源,分配给死锁进程,以解除死锁状态
终止(撤销)进程:将一个或多个死锁进程终止(撤销),直至打破循环环路,使系统从死锁状态解脱
简单描述静态链接和动态链接的区别,并举例说明
- 静态链接就是在程序执行前,把要链接的内容链接到可执行的文件中,生成一个目标文件
- 动态链接则是没有将内容拷贝到可执行文件中,而是先加入地址或者引用指针,在执行的过程中再去找要链接的内容。
动态链接有两种方式:
装在时动态链接
在编译前确认链接信息,编译时只保留重要的链接信息,执行时在内存中将其链接如调用程序的执行空间中。目的是便于代码共享
运行时动态链接
在编译前不知道链接信息,在执行时才会加载到内存中,并标识内存地址。目的是只存一份
这个和JS中模块引入一样,如果当前的模块写在当前文件中则是静态链接,而用import引入,则是动态链接。
说一下对 URL 进行编码/解码的实现方式?
escape 和 unescape
Javascript 语言用于编码的函数,一共有三个,最古老的一个就是 escape() 。虽然这个函数现在已经不提倡使用了,但是由于历史原因,很多地方还在使用它,所以有必要先从它讲起。
- 不能直接用于 URL 编码,它的真正作用是
返回一个字符的 Unicode 编码值
- 它的具体规则是,除了 AscII 字母、数字、标点符号
"@* _+- ./"
以外,对其他所有字符进行编码。在 u0000 到 u00ff 之间的符号被转成 %xx 的形式,其余符号被转成 %uxxxx 的形式。对应的解码函数是 unescape() - 无论网页的原始编码是什么,一旦被 Javascript 编码,就都变为 unicode 字符。也就是说,Javascipt 函数的输入和输出,默认都是 Unicode 字符。这—点对下面两个函数也适用。
- 网页在提交表单的时候,如果有空格,则会被转化为+字符。服务器处理数据的时候,会把+号处理成空格。所以,使用的时候要小心。
简单来说,escape 是对字符串(string)进行编码,而另外两种是对 URL
,作用是让它们在所有电脑上可读。编码之后的效果是%xx 或者号 uxxxx 这种形式。
最关键的是,当你需要对 URL 编码时,请忘记这个方法,这个方法是针对字符串使用的,不适用于 URL。
encodeURI 和 decodeURI
- encodeURI()是 Javascript 中真正用来对 URL 编码的函数
- 它用于对 URL 的组成部分进行个别编码,除了常见的符号以外,对其他一些在网址中有特殊含义的符号
"; /? : & = + $ ,#"
,也不进行编码。编码后,它输出符号的 utf-8 形式,并且在每个字节前加上% - 它对应的解码函数是 decodeURI()
- 需要注意的是,它不对单引号
'
编码。
encodeURICoponent 和 decodeURIComponent
- 与 encodeURI()的区别是,它用于对整个 URL 进行编码。
"; / ?:G &=+$,#"
,这些在 encodeURI()中不被编码的符号,在 encodeURIComponent()中统统会被编码。 - 它对应的解码函数是 decodeURIComponent().
使用场景
-
- 如果只是编码字符串,不和 URL 有半毛钱关系,那么用 escape。
-
- 如果你需要编码整个 URL,然后需要使用这个 URL,那么用 encodeURI。
-
- 当你需要编码 URL 中的参数的时候,那么 encodeURIComponent 是最好方法。