谈谈对前端工程化的理解?(面试题)
使用软件工程的技术和方法来进行前端的开发流程、技术、工具、经验等规范化、标准化,其主要目的是为了提高效率和降低成本,即提高开发过程中的开发效率,减少不必要的重复工作时间
主要是模块化、组件化、规范化、自动化
1.模块化: js的模块 commonjs es mdoule css的less sass等预编译 css module 资源的模块化webpack打包
2.组件化 比如react的一切皆组件的思想
3.规范化 比如编码规范 git commit规范 前后端接口规范(restful api)
4.自动化 比如自动化构建 自动化部署 CI/CD 自动化测试等
一、Git
2. 经常使用的 git 命令?
git init // 新建 git 代码库
git add // 添加指定文件到暂存区
git commit -m [message] // 提交暂存区到仓库区
git branch // 列出所有分支
git checkout -b [branch] // 新建一个分支,并切换到该分支
git status // 显示有变更文件的状态
git remote [add] [url] // 添加远程仓库
git pull [remotename] [localbranchname] //拉取远程仓库代码
git push [remotename] [localbranchname] // 推送代码到远程仓库
git reset HEAD^ // 回退代码提交
3. git pull 和 git fetch 的区别
- git fetch 只是将远程仓库的变化下载下来,并没有和本地分支合并。
- git pull 会将远程仓库的变化下载下来,并和当前分支合并。
4. git rebase 和 git merge 的区别
git merge 和 git rebase 都是用于分支合并,关键在 commit 记录的处理上不同:
- git merge 会新建一个新的 commit 对象,然后两个分支以前的 commit 记录都指向这个新 commit 记录。这种方法会保留之前每个分支的 commit 历史。
- git rebase 会先找到两个分支的第一个共同的 commit 祖先记录,然后将提取当前分支这之后的所有 commit 记录,然后将这个 commit 记录添加到目标分支的最新提交后面。经过这个合并后,两个分支合并后的 commit 记录就变为了线性的记录了。
二、Webpack(所有都是面试常考点)
webpack如何实现代码分离
入口起点
:使用entry
配置手动地分离代码。防止重复
:使用CommonsChunkPlugin
去重和分离chunk
。动态导入
:通过模块的内联函数调用来分离代码。
你知道webpack的作用是什么吗?
从官网上的描述我们其实不难理解,webpack
的作用其实有以下几点:
- 模块打包。可以将不同模块的文件打包整合在一起,并且保证它们之间的引用正确,执行有序。利用打包我们就可以在开发的时候根据我们自己的业务自由划分文件模块,保证项目结构的清晰和可读性。
- 编译兼容。在前端的“上古时期”,手写一堆浏览器兼容代码一直是令前端工程师头皮发麻的事情,而在今天这个问题被大大的弱化了,通过
webpack
的Loader
机制,不仅仅可以帮助我们对代码做polyfill
,还可以编译转换诸如.less, .vue, .jsx
这类在浏览器无法识别的格式文件,让我们在开发的时候可以使用新特性和新语法做开发,提高开发效率。 - 能力扩展。通过
webpack
的Plugin
机制,我们在实现模块化打包和编译兼容的基础上,可以进一步实现诸如按需加载,代码压缩等一系列功能,帮助我们进一步提高自动化程度,工程效率以及打包输出的质量。
说一下模块打包运行原理?
如果面试官问你Webpack
是如何把这些模块合并到一起,并且保证其正常工作的,你是否了解呢?
首先我们应该简单了解一下webpack
的整个打包流程:
- 1、读取
webpack
的配置参数; - 2、启动
webpack
,创建Compiler
对象并开始解析项目; - 3、从入口文件(
entry
)开始解析,并且找到其导入的依赖模块,递归遍历分析,形成依赖关系树; - 4、对不同文件类型的依赖模块文件使用对应的
Loader
进行编译,最终转为Javascript
文件; - 5、整个过程中
webpack
会通过发布订阅模式,向外抛出一些hooks
,而webpack
的插件即可通过监听这些关键的事件节点,执行插件任务进而达到干预输出结果的目的。
你知道sourceMap是什么吗?
sourceMap
是一项将编译、打包、压缩后的代码映射回源代码的技术,由于打包压缩后的代码并没有阅读性可言,一旦在开发中报错或者遇到问题,直接在混淆代码中debug
问题会带来非常糟糕的体验,sourceMap
可以帮助我们快速定位到源代码的位置,提高我们的开发效率。sourceMap
其实并不是Webpack
特有的功能,而是Webpack
支持sourceMap
,像JQuery
也支持souceMap
。
是否写过Loader?简单描述一下编写loader的思路?
从上面的打包代码我们其实可以知道,Webpack
最后打包出来的成果是一份Javascript
代码,实际上在Webpack
内部默认也只能够处理JS
模块代码,在打包过程中,会默认把所有遇到的文件都当作 JavaScript
代码进行解析,因此当项目存在非JS
类型文件时,我们需要先对其进行必要的转换,才能继续执行打包任务,这也是Loader
机制存在的意义。
是否写过Plugin?简单描述一下编写plugin的思路?
如果说Loader
负责文件转换,那么Plugin
便是负责功能扩展。Loader
和Plugin
作为Webpack
的两个重要组成部分,承担着两部分不同的职责。
上文已经说过,webpack
基于发布订阅模式,在运行的生命周期中会广播出许多事件,插件通过监听这些事件,就可以在特定的阶段执行自己的插件任务,从而实现自己想要的功能。
有哪些常见的Loader?你用过哪些Loader?
source-map-loader
:加载额外的 Source Map 文件,以方便断点调试image-loader
:加载并且压缩图片文件-sass-loader
:将SCSS/SASS代码转换成CSScss-loader
:加载 CSS,支持模块化、压缩、文件导入等特性ts-loader
: 将 TypeScript 转换成 JavaScript
webpack loader的执行顺序
?从左到右?从上到下?
在Webpack中,loader的执行顺序是从右向左执行的。 至于为什么是从右到左执行而不是从左到右? 因为webpack选择了compose这样的函数式编程方式,而 gulp 却选择应用从左到右的pipe 管道式编程。
1.有哪些常见的Plugin?你用过哪些Plugin?
html-webpack-plugin
:简化 HTML 文件创建 (依赖于 html-loader)
2.那你再说一说Loader和Plugin的区别?
Loader
本质就是一个函数,在该函数中对接收到的内容进行转换,返回转换后的结果。 因为 Webpack 只认识 JavaScript,所以 Loader 就成了翻译官,对其他类型的资源进行转译的预处理工作。
Plugin
就是插件,基于事件流框架 Tapable
,插件可以扩展 Webpack 的功能,在 Webpack 运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果。
Loader
在 module.rules 中配置,作为模块的解析规则,类型为数组。每一项都是一个 Object,内部包含了 test(类型文件)、loader、options (参数)等属性。
Plugin
在 plugins 中单独配置,类型为数组,每一项是一个 Plugin 的实例,参数都通过构造函数传入。
8.说一下 Webpack 的热更新原理吧
(敲黑板,这道题必考)
Webpack
的热更新又称热替换(Hot Module Replacement
),缩写为 HMR
。 这个机制可以做到不用刷新浏览器而将新变更的模块替换掉旧的模块。
HMR的核心就是客户端从服务端拉去更新后的文件,准确的说是 chunk diff (chunk 需要更新的部分),实际上 WDS 与浏览器之间维护了一个 Websocket
,当本地资源发生变化时,WDS 会向浏览器推送更新,并带上构建时的 hash,让客户端与上一次资源进行对比。客户端对比出差异后会向 WDS 发起 Ajax
请求来获取更改内容(文件列表、hash),这样客户端就可以再借助这些信息继续向 WDS 发起 jsonp
请求获取该chunk的增量更新。
后续的部分(拿到增量更新之后如何处理?哪些状态该保留?哪些又需要更新?)由 HotModulePlugin
来完成,提供了相关 API 以供开发者针对自身场景进行处理,像react-hot-loader
和 vue-loader
都是借助这些 API 实现 HMR。
9.webpack优化
优秀参考文章:webpack进阶之性能优化 juejin.cn/post/724481…
一:构建时间优化
首先就是构建时间的优化了
thread-loader
多进程打包,可以大大提高构建的速度,使用方法是将thread-loader
放在比较费时间的loader之前,比如babel-loader
由于启动项目和打包项目都需要加速,所以配置在
webpack.base.js
npm i thread-loader -D
// webpack.base.js
{
test: /.js$/,
use: [
'thread-loader',
'babel-loader'
],
}
}
cache-loader
缓存资源,提高二次构建的速度,使用方法是将cache-loader
放在比较费时间的loader之前,比如babel-loader
由于启动项目和打包项目都需要加速,所以配置在
webpack.base.js
npm i cache-loader -D
// webpack.base.js
{
test: /.js$/,
use: [
'cache-loader',
'thread-loader',
'babel-loader'
],
},
开启热更新
比如你修改了项目中某一个文件,会导致整个项目刷新,这非常耗时间。如果只刷新修改的这个模块,其他保持原状,那将大大提高修改代码的重新构建时间
只用于开发中,所以配置在
webpack.dev.js
// webpack.dev.js
//引入webpack
const webpack = require('webpack');
//使用webpack提供的热更新插件
plugins: [
new webpack.HotModuleReplacementPlugin()
],
//最后需要在我们的devserver中配置
devServer: {
+ hot: true
},
exclude & include
exclude
:不需要处理的文件include
:需要处理的文件
合理设置这两个属性,可以大大提高构建速度
在
webpack.base.js
中配置
// webpack.base.js
{
test: /.js$/,
//使用include来指定编译文件夹
include: path.resolve(__dirname, '../src'),
//使用exclude排除指定文件夹
exclude: /node_modules/,
use: [
'babel-loader'
]
},
构建区分环境
区分环境去构建是非常重要的,我们要明确知道,开发环境时我们需要哪些配置,不需要哪些配置;而最终打包生产环境时又需要哪些配置,不需要哪些配置:
开发环境
:去除代码压缩、gzip、体积分析等优化的配置,大大提高构建速度生产环境
:需要代码压缩、gzip、体积分析等优化的配置,大大降低最终项目打包体积
上篇文章已经带大家进行了环境区分
提升webpack版本
webpack版本越新,打包的效果肯定更好
二:打包体积优化
主要是打包后项目整体体积的优化,有利于项目上线后的页面加载速度提升
本项目已经是webpack最新版本
压缩代码
我们都知道,在浏览器中,运行 JS 代码是需要先将代码文件从浏览器通过服务器下载下来后再进行解析执行。那么在相同的网络环境下文件的大小会直接影响到网页加载的时长。那么,对代码进行压缩就是最简单高效的操作。
压缩 html
压缩 html
使用的还是 html-webpack-plugin
插件。该插件支持配置一个 minify 对象,用来配置压缩 html
。
如上配置后,我们的html
代码就会移除空格和注释。可以看到,重新构建后代码变成了一行。
压缩 css
对于webpack4
及以下 使用的是 optimize-css-assets-webpack-plugin插件来压缩css
。
压缩 js
在 webpack
中,我们可以使用 uglifyjs-webpack-plugin 和 terser-webpack-plugin 插件来优化 JS 资源。
压缩 image
一般来说在打包之后,一些图片文件的大小是远远要比 js
或者 css
文件要来的大,所以我们首先要做的就是对于图片的优化,我们可以手动的去通过线上的图片压缩工具,如 tiny png 帮我们来压缩图片。
但是这个比较繁琐,在项目中我们希望能够更加自动化一点,自动帮我们做好图片压缩,这个时候我们就可以借助 image-webpack-loader 帮助我们来实现。它是基于 imagemin 这个 Node 库来实现图片压缩的。
使用很简单,我们只要在 file-loader
之后加入 image-webpack-loader
即可:
按需加载
很多时候我们不需要一次性加载所有的JS
文件,而应该在不同阶段去加载所需要的代码。webpack
内置了强大的分割代码的功能可以实现按需加载。
比如,我们在点击了某个按钮之后,才需要使用使用对应的JS
文件中的代码,我们可以使用 import()
语法按需引入
提前加载(prefetch 和 preload)
上面说的代码懒加载在使用的时候才去加载是会提升页面性能,但是如果懒加载的模块比较大,当我们点击的时候再去加载的话无疑会让用户等待时间加长。
如果可以利用浏览器空闲时候去加载这些切分出来的模块那就好了?
诶,还真有,那就是prefetch 和 preload
prefetch和preload的概念
prefetch
(预取):将来可能需要一些模块资源,在核心代码加载完成之后带宽空闲的时候再去加载需要用到的模块代码。
preload
(预加载):当前核心代码加载期间可能需要模块资源,其是和核心代码文件一起去加载的。
prefetch
我们将上面的例子稍微改下,加个注释/* webpackPrefetch: true */
js
复制代码
// index.js
document.getElementById("btn1").onclick = async () => {
const imp = await import(/* webpackPrefetch: true */ "./impModule.js");
imp.default();
};
上面的代码的意思是当我们主要的核心代码加载完成,浏览器有空闲的时候,浏览器就会帮我们自动的去下载impModule.js
preload
/* webpackPreload: true */
使用方式类似。
prefetch 与 preload 的区别
preload chunk
会在父chunk
加载时,以并行方式开始加载。prefetch chunk
会在父chunk
加载结束后开始加载。preload chunk
具有中等优先级,并立即下载。prefetch chunk
在浏览器闲置时下载。preload chunk
会在父chunk
中立即请求,用于当下时刻。prefetch chunk
会用于未来的某个时刻。- 浏览器支持程度不同,需要注意。
Code Splitting (代码分割 也就是常说的分包)
分包优化的原理:
-
减少冗余代码:在大型前端应用中,通常会有许多模块和库,其中可能包含一些共同的依赖模块(例如React、Vue等)。如果每个页面都包含所有这些依赖,那么会导致构建物体积增加,因为这些依赖会被重复打包。SplitChunks 可以将这些共同的依赖模块提取到一个单独的包中,避免了冗余代码。
-
并行加载:将代码分剥离为多个包后,浏览器能够并行加载这些包,从而加速页面加载时间。这是因为浏览器通常限制同时下载的资源数量,如果你的应用只有一个大包,它必须等待下载完成,然后才能开始执行 JavaScript 代码。通过拆分为多个小包,可以并行下载,提高加载速度。
-
缓存优化:当你使用 SplitChunks 时,每个包都会拥有自己的文件名,并且这些文件名通常基于内容生成,这意味着只有在包内容发生更改时,浏览器才会重新下载这些包。这有助于利用浏览器缓存,减少了重复下载的开销。
-
动态导入:SplitChunks 还允许你使用动态导入,即在运行时根据需要加载代码。这对于按需加载模块或页面非常有用,因为它不会在初始加载时增加构建物体积,只有在需要时才会加载相应的包。
SplitChunksPlugin
下面笔者来看看取代 CommonChunkPlugin
的 SplitChunksPlugin
默认情况下,它只会影响到按需加载的 chunks。
既然js
支持代码分割,那css
是不是也支持呢?
MiniCssExtractPlugin
我们知道,style-loader
会把css
打包进js
文件里面,这样js
在运行的时候才会在html
文件动态生成style
标签将样式插入,这无疑加大了我们js
文件的体积。其次还不利于css代码的复用。
我们可以利用 mini-css-extract-plugin 插件,将我们的css
代码分离出来。
Tree Shaking (摇钱树)
Tree Shaking
又称为摇钱树,主要用来清除没有使用到的js
代码。
JS tree shaking
Gzip
前端除了在打包的时候将无用的代码或者 console
、注释剔除之外。我们还可以使用 Gzip
对资源进行进一步压缩。Gzip
原本是 UNIX
系统的文件压缩,后来逐步成为 web
领域主流的压缩工具。那么浏览器和服务端是如何通信来支持 Gzip
呢?
- 当用户访问 web 站点的时候,会在
request header
中设置accept-encoding:gzip
,表明浏览器是否支持Gzip
。 - 服务器在收到请求后,判断如果需要返回
Gzip
压缩后的文件那么服务器就会先将我们的JS\CSS
等其他资源文件进行Gzip
压缩后再传输到客户端,同时将response headers
设置content-encoding:gzip
。反之,则返回源文件。 - 浏览器在接收到服务器返回的文件后,判断服务端返回的内容是否为压缩过的内容,是的话则进行解压操作。
一般情况下我们并不会让服务器实时 Gzip
压缩,而是利用webpack
提前将静态资源进行Gzip
压缩,然后将Gzip
资源放到服务器,当请求需要的时候直接将Gzip
资源发送给客户端。
我们只需要安装 compression-webpack-plugin
并在plugins
配置就可以了
配置好我们再来构建,可以发现生成了资源的.gz
文件
八、模块化
模块化的好处
- 避免命名冲突(减少命名空间污染)
- 更好的分离, 按需加载
- 更高复用性
- 高可维护性
ES6 模块与 CommonJS 模块的差异(面试题)
它们有两个重大差异:
① CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
② CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
第二个差异是因为 CommonJS 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。
此外还有AMD CMD
AMD(面试题)
AMD
是运行在浏览器环境的一个异步模块定义规范 ,是RequireJS
在推广过程中对模块定义的规范化产出。
AMD规范
AMD
推崇依赖前置,在定义模块的时候就要声明其依赖的模块
优点
用户体验好,因为没有延迟,依赖模块提前执行了。
CMD(面试题)
CMD
是一个通用模块定义规范;是SeaJs推广过程中对模块定义的规范化产出
CMD规范
CMD
推崇依赖就近,只有在用到某个模块的时候才会去require
优点
性能好,因为只有用户需要的时候才执行。
13. 怎么配置单⻚应⽤?怎么配置多⻚应⽤?
单⻚应⽤可以理解为webpack的标准模式,直接在 entry 中指定单⻚应⽤的⼊⼝即可,这⾥不再赘述多⻚应⽤的话,可以使⽤webpack的 AutoWebPlugin 来完成简单⾃动化的构建,但是前提是项⽬的⽬录结构必须遵守他预设的规范。 多⻚应⽤中要注意的是:
- 每个⻚⾯都有公共的代码,可以将这些代码抽离出来,避免重复的加载。⽐如,每个⻚⾯都引⽤了同⼀套css样式表
- 随着业务的不断扩展,⻚⾯可能会不断的追加,所以⼀定要让⼊⼝的配置⾜够灵活,避免每次添加新⻚⾯还需要修改构建配置
14 Babel的原理是什么
Babel主要是用于将es6语法转化编译为es5语法
babel 的转译过程也分为三个阶段,这三步具体是:
- 解析 Parse: 将代码解析⽣成抽象语法树(AST),即词法分析与语法分析的过程;
- 转换 Transform: 对于 AST 进⾏变换⼀系列的操作,babel 接受得到 AST 并通过 babel-traverse 对其进⾏遍历,在此过程中进⾏添加、更新及移除等操作;
- ⽣成 Generate: 将变换后的 AST 再转换为 JS 代码, 使⽤到的模块是 babel-generator。
AST 是什么
抽象语法树 (Abstract Syntax Tree),简称 AST,它是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构,由词法分析和语法分析生成
词法分析
词法分析,也称之为扫描(scanner),简单来说就是调用 next() 方法,一个一个字母的来读取字符,然后与定义好的 JavaScript 关键字符做比较,生成对应的Token。Token 是一个不可分割的最小单元
例如 var 这三个字符,它只能作为一个整体,语义上不能再被分解,因此它是一个 Token。
词法分析器里,每个关键字是一个 Token ,每个标识符是一个 Token,每个操作符是一个 Token,每个标点符号也都是一个 Token。除此之外,还会过滤掉源程序中的注释和空白字符(换行符、空格、制表符等。
最终,整个代码将被分割进一个tokens列表(或者说一维数组)。
语法分析
语法分析会将词法分析出来的 Token 转化成有语法含义的抽象语法树结构。同时,验证语法,语法如果有错的话,抛出语法错误。
说了这么多我们来看下 javaScript 代码片段转成 AST 之后是什么样的我们拿一行简单的代码来展示
🌰例子 1
const fn = a => a;
复制代码
如图从这个 AST 语法树我们就能够很清楚的看出一个代码他的具体含义,并且使用的是什么语法,方法等。
用人话翻译这个图就是:用类型 const 声明变量 fn 指向一个箭头函数表达式,它的参数是 a 函数体也是 a。
前端错误监控以及上报
-
前端错误分类:
- 即时运行错误:代码错误
- 资源加载错误
- 对于跨域的代码运行错误会显示 Script error. 对于这种情况我们需要给 script 标签添加 crossorigin 属性,并且服务器添加Access-Control-Allow-Origin ,script标签中的crossorigin有三种值anonymous(表示对该元素的cors请求不设置凭证),use-credentials(表示cors请求设置凭证),''(空字符串和anonymous一样都是不设置凭证)
-
即时运行错误捕获
(1)try ....catch (2) window.onerror 或者 window.addEventListener 记住事件捕获阶段获得,不是冒泡阶段
- 资源加载错误
(1)object.onerror,如img.onerror, 当某一项资源加载失败,加载资源的元素会触发定义的onerror事件(2)performance.getEntries (getEntries api返回一个资源加载完成数组,假设为img,再查询页面中一共有多少个img,二者的差就是没有加载上的资源) (3)Error事件捕获
-
错误如何上报
(1)ajax
(2)image的src上报
(new Image()).src = '错误上报的请求地址'
一般来说,大厂都是采用利用image对象的方式上报错误的;使用图片发送get请求,上报信息,由于浏览器对图片有缓存,同样的请求,图片只会发送一次,避免重复上报