commonjs 和 es6 模块的区别是什么?
CMJ 是社区标准,ESM 是官方标准
CMJ 是使用 API 实现的模块化,ESM 是使用新语法实现的模块化
CMJ 仅在 node 环境中支持,ESM 各种环境均支持
CMJ 是动态的依赖,ESM 既支持动态,也支持静态
CMJ 只是普通函数调用和赋值,ESM 导入时有符号绑定
解释一下 npm 模块安装机制是什么?
- npm 会检查本地的 node_modules 目录中是否已经安装过该模块,如果已经安装,则不再重新安装
- npm 检查缓存中是否有相同的模块,如果有,直接从缓存中读取安装
- 如果本地和缓存中均不存在,npm 会从 registry 指定的地址下载安装包,然后将其写入到本地的 node_modules 目录中,同时缓存起来。
ES6 中如何实现模块化的异步加载?
使用动态导入即可,导入后,得到的是一个 Promise,完成后,得到一个模块对象,其中包含了所有的导出结果。
说一下你对前端工程化,模块化,组件化的理解?
这三者中,模块化是基础,没有模块化,就没有组件化和工程化
模块化的出现,解决了困扰前端的两大难题:全局污染问题和依赖混乱问题,从而让精细的拆分前端工程成为了可能。
工程化的出现,解决了前端开发环境和生产环境要求不一致的矛盾。在开发环境中,我们希望代码使用尽可能的细分,代码格式尽可能的统一和规范,而在生产环境中,我们希望代码尽可能的被压缩、混淆,尽可能的优化体积。工程化的出现,就是为了解决这一矛盾,它可以让我们舒服的在开发环境中书写代码,然后经过打包,生成最合适的生产环境代码,这样就解放了开发者的精力,让开发者把更多的注意力集中在开发环境上即可。
组件化开发是一些前端框架带来的概念,它把一个网页,或者一个站点,甚至一个完整的产品线,划分为多个小的组件,组件是一个可以复用的单元,它包含了一个某个区域的完整功能。这样一来,前端便具备了开发复杂应用的能力。
webpack 和 gulp 的区别是什么?
webpack 是基于模块化的构建工具,gulp 是基于工作流的构建工具。
webpack 是一个打包器,它以一个入口为起点,构建出整个项目的依赖关系图,然后进行打包合并,生成打包结果。
gulp 是一个过程管理器,每一步做什么完全看开发人员如何配置,把每一个步骤连接起来形成一个完整的构建流水线。
这两者并不矛盾,完全可以在一个工程中同时使用 webpack 和 gulp,将 webpack 作为 gulp 流水线中的一环。
描述一下 webpack 的构建流程?
- 初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数
- 开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的
run
方法开始执行编译 - 确定入口:根据配置中的
entry
找出所有的入口文件 - 编译模块:从入口文件出发,调用所有配置的
loader
对模块进行翻译,再把翻译后的内容转换成 AST,通过对 AST 的分析找出该模块依赖的模块,再递归
本步骤直到所有入口依赖的文件都经过了本步骤的处理 - 完成模块编译:在经过第 4 步使用
loader
翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系图
- 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的
Chunk
,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会 - 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统
webpack 的核心概念都有哪些?
- loader:加载器,主要用于代码转换,比如 JS 代码降级,CSS 预编译、模块化等
- plugin:插件,webpack 打包流程中每个环节都提供了钩子函数,可以利用这些钩子函数参与到打包生命周期中,更改或增加 webpack 的某些功能,比如生成页面和 css 文件、压缩打包结果等
- module:模块。webpack 将所有依赖均视为模块,无论是 js、css、html、图片,统统都是模块
- entry:入口。打包过程中的概念,webpack 以一个或多个文件作为入口点,分析整个依赖关系。
- chunk:打包过程中的概念,一个 chunk 是一个相对独立的打包过程,以一个或多个文件为入口,分析整个依赖关系,最终完成打包合并
- bundle:webpack 打包结果
- tree shaking:树摇优化。在打包结果中,去掉没有用到的代码。
- HMR:热更新。是指在运行期间,遇到代码更改后,无须重启整个项目,只更新变动的那一部分代码。
- dev server:开发服务器。在开发环境中搭建的临时服务器,用于承载对打包结果的访问
webpack 热更新原理是什么?
当开启热更新后,页面中会植入一段 websocket 脚本,同时,开发服务器也会和客户端建立 websocket 通信,当源码发生变动时,webpack 会进行以下处理:
- webpack 重新打包
- webpack-dev-server 检测到模块的变化,于是通过 websocket 告知客户端变化已经发生
- 客户端收到消息后,通过 ajax 发送请求到开发服务器,以过去打包的 hash 值请求服务器的一个 json 文件
- 服务器告诉客户端哪些模块发生了变动,同时告诉客户端这次打包产生的新 hash 值
- 客户端再次用过去的 hash 值,以 JSONP 的方式请求变动的模块
- 服务器响应一个函数调用,用于更新模块的代码
- 此时,模块代码已经完成更新。客户端按照之前的监听配置,执行相应模块变动后的回调函数。
说一下 webpack 中的几种 hash 的实现原理是什么?
- hash:hash 是跟整个项目的构建相关,只要项目里有文件更改,整个项目构建的 hash 值都会更改,并且全部文件都共用相同的 hash 值
- chunkhash:每个打包过程单独的 hash 值,如果一个项目有多个 entry,则每个 entry 维护自己的 chunkhash。
- contenthash:每个文件内容单独的 hash 值,它和打包结果文件内容有关,只要文件内容不变,contenthash 不变。
webpack 如果使用了 hash 命名,那是每次都会重新生成 hash 吗?
不会。它跟关联的内容是否有变化有关系,如果没有变化,hash 就不会变。具体来说,conten thash 和具体的打包文件内容有关,chunk hash 和某一 entry 为起点的打包过程中涉及的内容有关,hash 和整个工程所有模块内容有关。
webpack 中的 loader 属性和 plugins 属性的区别是什么?
它们都是 webpack 功能的扩展点。
loader 是加载器,主要用于代码转换,比如 JS 代码降级,CSS 预编译、模块化等
plugins 是插件,webpack 打包流程中每个环节都提供了钩子函数,可以利用这些钩子函数参与到打包生命周期中,更改或增加 webpack 的某些功能,比如生成页面和 css 文件、压缩打包结果等
说一下 webpack loader 的作用是什么?
用于转换代码。有时是因为 webpack 无法识别某些内容,比如图片、css 等,需要由 loader 将其转换为 JS 代码。有时是因为某些代码需要被特殊处理,比如 JS 兼容性的处理,需要由 loader 将其进一步转换。不管是什么情况,loader 的作用只有一个,就是转换代码。
常用的 webpack Loader 都有哪些?
- css-loader:编译 css 代码为 js
- sass-loader:将 sass 代码转换成 css 代码
- postcss-loader:将 css 代码使用 postcss 进行编译
- file-loader:保存文件到输出目录,将文件内容转换成文件路径
- url-loader:将文件内容转换成 dataurl
- vue-loader:编译单文件组件
- babel-loader:对 JS 代码进行降级处理
解释一下 webpack 插件的实现原理?
本质上,webpack 的插件是一个带有apply
函数的对象。当 webpack 创建好 compiler 对象后,会执行注册插件的 apply 函数,同时将 compiler 对象作为参数传入。
在 apply 函数中,开发者可以通过 compiler 对象监听多个钩子函数的执行,不同的钩子函数对应 webpack 编译的不同阶段。当 webpack 进行到一定阶段后,会调用这些监听函数,同时将 compilation 对象传入。开发者可以使用 compilation 对象获取和改变 webpack 的各种信息,从而影响构建过程。
有用过哪些插件做项目的分析吗?
用过 webpack-bundle-analyzer 分析过打包结果,主要用于优化项目打包体积
什么是 babel,有什么作用?
babel 是一个 JS 编译器,主要用于将下一代的 JS 语言代码编译成兼容性更好的代码。
它其实本身做的事情并不多,它负责将 JS 代码编译成为 AST,然后依托其生态中的各种插件对 AST 中的语法和 API 进行处理
说一下 webpack 常用插件都有哪些?
- html-webpack-plugin:生成 HTML 文件
- clean-webpack-plugin:清除输出目录
- copy-webpack-plugin:复制文件到输出目录
- mini-css-extract-plugin:将 css 打包成单独文件的插件
- terser-plugin:混淆代码
- webpack-bundle-analyzer:分析打包结果
使用 babel-loader 会有哪些问题,可以怎样优化?
- 如果不做特殊处理,babel-loader 会对所有匹配的模块进行降级,这对于那些已经处理好兼容性问题的第三方库显得多此一举,因此可以使用 exclude 配置排除掉这些第三方库
- 在旧版本的 babel-loader 中,默认开启了对 ESM 的转换,这样会导致 webpack 的 tree shaking 失效,因为 tree shaking 是需要保留 ESM 语法的,所以需要关闭 babel-loader 的 ESM 转换,在其新版本中已经默认关闭了。
解释一下 babel-polyfill 的作用是什么?
babel-polyfill 已经是一个非常古老的项目了,babel 从 7.4 版本开始已不再支持它,转而使用更加强大的 core-js,此题也适用于问「core-js 的作用是什么」
参考答案:
默认情况下,babel 本身只转换新的语法,而不对新的 API 进行处理。由于旧的环境中无法支持新的 API,比如 IE6 无法支持 fetch api,这就需要一个补丁,用旧语言的特性实现一遍新的 API,babel-polyfill 就是用来做这件事的。
webpack 中是如何处理图片的?
webpack 本身不处理图片,它会把图片内容仍然当做 JS 代码来解析,结果就是报错,打包失败。如果要处理图片,需要通过 loader 来处理。其中,url-loader 会把图片转换为 base64 编码,然后得到一个 dataurl,file-loader 则会将图片生成到打包目录中,然后得到一个资源路径。但无论是哪一种 loader,它们的核心功能,都是把图片内容转换成 JS 代码,因为只有转换成 JS 代码,webpack 才能识别。
html-webpack-plugin 打包出来的 html 为什么 style 放在头部 script 放在底部?
浏览器在解析 HTML 时是从上到下进行解析的,当遇到样式和 JS 时,都会停止 HTML 解析,转而解析样式和执行 JS。而我们往往希望,页面的样式解析完成后再解析 HTML,这样可以避免页面闪烁,基于此,样式应该放到顶部;而相反的,我们希望在解析完 HTML 后再执行 JS,这样可以让用户尽快的看到页面,同时也让 JS 执行时能够拿到完整的 DOM 树,基于此,JS 代码应该放到底部。
不过,在 HTML5 中出现了 async 和 defer 属性,使用该属性可以更好的解决 JS 的问题,我们可以把 script 放到顶部,让浏览器尽快下载,但延迟执行。实际上,在新版本的 html-webpack-plugin 中,它已经这样做了。
webpack 配置如何实现开发环境不使用 cdn、生产环境使用 cdn?
要配置 CDN,有两个步骤:
- 在 html 模板中直接加入 cdn 引用
- 在 webpack 配置中,加入
externals
配置,告诉 webpack 不要打包其中的模块,转而使用全局变量
若要在开发环境中不使用 CDN,只需根据环境变量判断不同的环境,进行不同的打包处理即可。
- 在 html 模板中使用 ejs 模板语法进行判断,只有在生产环境中引入 CDN
- 在 webpack 配置中,可以根据
process.env
中的环境变量进行判断是否使用externals
配置 - 在
package.json
脚本中设置不同的环境变量完成打包或开发启动。
介绍一下 webpack 中的 tree-shaking 的工作流程
tree-shaking 仅支持 ESM 的静态导入语法,对于 CMJ 或者 ESM 中的动态导入不支持 tree shaking。
具体流程主要分为两步:标记和删除
- 标记 webpack 在分析依赖时,会使用注释的方式对导入和导出进行标记,对于模块中没有被其他模块用到的导出标记为 unused harmony export
- 删除之后在 Uglifyjs (或者其他类似的工具) 步骤进行代码精简,把标记为无用的代码删除。
如何优化 webpack 的打包速度?
- noParse 很多第三方库本身就是已经打包好的代码,对于这种代码无须再进行解析,可以使用 noParse 配置排除掉这些第三方库
- externals 对于一些知名的第三方库可以使用 CDN,这部分库可以通过 externals 配置不进行打包
- 限制 loader 的范围在使用 loader 的时候,可以通过 exclude 排除掉一些不必要的编译,比如 babel-loader 对于那些已经完成打包的第三方库没有必要再降级一次,可以排除掉
- 开启 loader 缓存可以利用
cache-loader
缓存 loader 的编译结果,避免在源码没有变动时反复编译 - 开启多线程编译可以利用
thread-loader
开启多线程编译,提升编译效率
webpack 如何实现动态导入?
当遇到代码中包含动态导入语句时,webpack 会将导入的模块及其依赖分配到单独的一个 chunk 中进行打包,形成单独的打包结果。而动态导入的语句会被编译成一个普通的函数调用,该函数在执行时,会使用 JSONP 的方式动态的把分离出去的包加载到模块集合中。
在前端工程化中,可以进行哪些方面的优化?
- 对传输性能的优化
- 压缩和混淆使用 Uglifyjs 或其他类似工具对打包结果进行压缩、混淆,可以有效的减少包体积
- tree shaking 项目中尽量使用 ESM,可以有效利用 tree shaking 优化,降低包体积
- 抽离公共模块将一些公共代码单独打包,这样可以充分利用浏览器缓存,其他代码变动后,不影响公共代码,浏览器可以直接从缓存中找到公共代码。具体方式有多种,比如 dll、splitChunks
- 异步加载对一些可以延迟执行的模块可以使用动态导入的方式异步加载它们,这样在打包结果中,它们会形成单独的包,同时,在页面一开始解析时并不需要加载它们,而是页面解析完成后,执行 JS 的过程中去加载它们。这样可以显著提高页面的响应速度,在单页应用中尤其有用。
- CDN 对一些知名的库使用 CDN,不仅可以节省打包时间,还可以显著提升库的加载速度
- gzip 目前浏览器普遍支持 gzip 格式,因此可以将静态文件均使用 gzip 进行压缩
- 环境适配有些打包结果中包含了大量兼容性处理的代码,但在新版本浏览器中这些代码毫无意义。因此,可以把浏览器分为多个层次,为不同层次的浏览器给予不同的打包结果。
- 对打包过程的优化
- noParse 很多第三方库本身就是已经打包好的代码,对于这种代码无须再进行解析,可以使用 noParse 配置排除掉这些第三方库
- externals 对于一些知名的第三方库可以使用 CDN,这部分库可以通过 externals 配置不进行打包
- 限制 loader 的范围在使用 loader 的时候,可以通过 exclude 排除掉一些不必要的编译,比如 babel-loader 对于那些已经完成打包的第三方库没有必要再降级一次,可以排除掉
- 开启 loader 缓存可以利用
cache-loader
缓存 loader 的编译结果,避免在源码没有变动时反复编译 - 开启多线程编译可以利用
thread-loader
开启多线程编译,提升编译效率 - 动态链接库对于某些需要打包的第三方库,可以使用 dll 的方式单独对其打包,然后 DLLPlugin 将其整合到当前项目中,这样就避免了在开发中频繁去打包这些库
- 对开发体验的优化
- lint 使用 eslint、stylelint 等工具保证团队代码风格一致
- HMR 使用热替换避免页面刷新导致的状态丢失,提升开发体验
如果有一个工程打包特别大-如何进行优化?
- CDN 如果工程中使用了一些知名的第三方库,可以考虑使用 CDN,而不进行打包
- 抽离公共模块如果工程中用到了一些大的公共库,可以考虑将其分割出来单独打包
- 异步加载对于那些不需要在一开始就执行的模块,可以考虑使用动态导入的方式异步加载它们,以尽量减少主包的体积
- 压缩、混淆
- tree shaking 尽量使用 ESM 语法进行导入导出,充分利用 tree shaking 去除无用代码
- gzip 开启 gzip 压缩,进一步减少包体积
- 环境适配有些打包结果中包含了大量兼容性处理的代码,但在新版本浏览器中这些代码毫无意义。因此,可以把浏览器分为多个层次,为不同层次的浏览器给予不同的打包结果。
介绍一下 webpack scope hoisting?
scope hoisting 是 webpack 的内置优化,它是针对模块的优化,在生产环境打包时会自动开启。
在未开启 scope hoisting 时,webpack 会将每个模块的代码放置在一个独立的函数环境中,这样是为了保证模块的作用域互不干扰。
而 scope hoisting 的作用恰恰相反,是把多个模块的代码合并到一个函数环境中执行。在这一过程中,webpack 会按照顺序正确的合并模块代码,同时对涉及的标识符做适当处理以避免重名。
这样做的好处是减少了函数调用,对运行效率有一定提升,同时也降低了打包体积。
但 scope hoisting 的启用是有前提的,如果遇到某些模块多次被其他模块引用,或者使用了动态导入的模块,或者是非 ESM 的模块,都不会有 scope hoisting。
具体说一下 split-chunks-plugin 的使用场景及使用方法
- 公共模块比如某些多页应用会有多个入口,从而形成多个 chunk,而这些 chunk 中用到了一些公共模块,为了减少整体的包体积,可以使用 splitchunksplugin 将公共模块分离出来。可以配置 minChunks 来指定被多少个 chunk 引用时进行分包
- 并行下载由于 HTML5 支持 defer 和 async,因此可以同时下载多个 JS 文件以充分利用带宽。如果打包结果是一个很大的文件,就无法利用到这一点。可以利用 splitchunks 插件将文件进行拆分,通过配置 maxSize 属性指定包体积达到多大时进行拆分