自用前端面试题(工程化)

156 阅读31分钟

0.工程化相关书籍

Webpack小书

1.做过哪些webpack的优化?

优化 Webpack 的构建速度

1.使用高版本的 Webpack (使用webpack4)

2.多线程/多实例构建:HappyPack(不维护了)、thread-loader

3. 缩小打包作用域:

  • exclude/include (确定 loader 规则范围)
  • resolve.modules 指明第三方模块的绝对路径 (减少不必要的查找)
  • resolve.extensions 尽可能减少后缀尝试的可能性
  • noParse 对完全不需要解析的库进行忽略 (不去解析但仍会打包到 bundle 中,注意被忽略掉的文件里不应该包含 import、require、define 等模块化语句)
  • IgnorePlugin (完全排除模块)
  • 合理使用alias

4.充分利用缓存提升二次构建速度:

  • babel-loader 开启缓存
  • terser-webpack-plugin 开启缓存
  • 使用 cache-loader 或者 hard-source-webpack-plugin
  • 注意:thread-loader 和 cache-loader 兩個要一起使用的話,請先放 cache-loader 接著是 thread-loader 最後才是 heavy-loader

5.DLL:

使用 DllPlugin 进行分包,使用 DllReferencePlugin(索引链接) 对 manifest.json 引用,让一些基本不会改动的代码先打包成静态资源,避免反复编译浪费时间

知识点:
noParse

  • 不去解析某个库内部的依赖关系
  • 比如jquery 这个库是独立的, 则不去解析这个库内部依赖的其他的东西
  • 在独立库的时候可以使用
module.exports = {
  module: {
    noParse: /jquery/,
    rules:[]
  }
}

IgnorePlugin

  • 忽略掉某些内容 不去解析依赖库内部引用的某些内容
  • 从moment中引用 ./locol 则忽略掉
  • 如果要用local的话 则必须在项目中必须手动引入
import 'moment/locale/zh-cn'
module.exports = {
    plugins: [
        new Webpack.IgnorePlugin(/./local/, /moment/),
    ]
}

dillPlugin

  • 不会多次打包, 优化打包时间
  • 先把依赖的不变的库打包
  • 生成 manifest.json文件
  • 然后在webpack.config中引入
  • webpack.DllPlugin Webpack.DllReferencePlugin

happypack -> thread-loader

  • 大项目的时候开启多线程打包
  • 影响前端发布速度的有两个方面,一个是构建,一个就是压缩,把这两个东西优化起来,可以减少很多发布的时间

thread-loader
thread-loader会将您的 loader 放置在一个 worker 池里面运行,以达到多线程构建。把这个 loader 放置在其他 loader 之前(如下图 example 的位置), 放置在这个 loader 之后的 loader 就会在一个单独的 worker 池(worker pool)中运行。

// webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /.js$/,
        include: path.resolve("src"),
        use: [
          "thread-loader",
          // 你的高开销的loader放置在此 (e.g babel-loader)
        ]
      }
    ]
  }
}

每个 worker 都是一个单独的有 600ms 限制的 node.js 进程。同时跨进程的数据交换也会被限制。请在高开销的loader中使用,否则效果不佳。

压缩加速——开启多线程压缩
推荐使用 terser-webpack-plugin

module.exports = {
  optimization: {
    minimizer: [new TerserPlugin(
      parallel: true   // 多线程
    )],
  },
};

优化Webpack的打包体积**

1.压缩代码

  • webpack-paralle-uglify-plugin
  • uglifyjs-webpack-plugin 开启 parallel 参数 (不支持ES6)
  • terser-webpack-plugin 开启 parallel 参数
  • 多进程并行压缩
  • 通过 mini-css-extract-plugin 提取 Chunk 中的 CSS 代码到单独文件,通过optimize-css-assets-webpack-plugin插件 开启 cssnano 压缩 CSS。

2.提取页面公共资源:

  • 使用 html-webpack-externals-plugin,将基础包通过 CDN 引入,不打入 bundle 中
  • 使用 SplitChunksPlugin 进行(公共脚本、基础包、页面公共文件)分离(Webpack4内置) ,替代了 CommonsChunkPlugin 插件
  • 基础包分离:将一些基础库放到cdn,比如vue,webpack 配置 external是的vue不打入bundle

3.Tree shaking

  • purgecss-webpack-plugin 和 mini-css-extract-plugin配合使用(建议)
  • 打包过程中检测工程中没有引用过的模块并进行标记,在资源压缩时将它们从最终的bundle中去掉(只能对ES6 Modlue生效) 开发中尽可能使用ES6 Module的模块,提高tree shaking效率
  • 禁用 babel-loader 的模块依赖解析,否则 Webpack 接收到的就都是转换过的 CommonJS 形式的模块,无法进行 tree-shaking
  • 使用 PurifyCSS(不再维护) 或者 uncss 去除无用 CSS 代码

4.Scope hoisting

  • 构建后的代码会存在大量闭包,造成体积增大,运行代码时创建的函数作用域变多,内存开销变大。Scope hoisting 将所有模块的代码按照引用顺序放在一个函数作用域里,然后适当的重命名一些变量以防止变量名冲突
  • 必须是ES6的语法,因为有很多第三方库仍采用 CommonJS 语法,为了充分发挥 Scope hoisting 的作用,需要配置 mainFields 对第三方模块优先采用 jsnext:main 中指向的ES6模块化语法

5.图片压缩

  • 使用基于 Node 库的 imagemin (很多定制选项、可以处理多种图片格式)
  • 配置 image-webpack-loader

6.动态Polyfill

  • 建议采用 polyfill-service 只给用户返回需要的polyfill,社区维护。(部分国内奇葩浏览器UA可能无法识别,但可以降级返回所需全部polyfill)
  • @babel-preset-env 中通过useBuiltIns: 'usage参数来动态加载polyfill

辅助插件

1.分析构建中的性能瓶颈

插件speed-measure-webpack-plugin,简称 SMP,可以分析出 Webpack 打包过程中 Loader 和 Plugin 的耗时,有助于找到构建过程中的性能瓶颈。

2.监控面板

插件speed-measure-webpack-plugin,在打包的时候显示出每一个loader、plugin所用的时间,来精准优化

// webpack.config.js文件
const SpeedMeasurePlugin = require('speed-measure-webpack-plugin');
const smp = new SpeedMeasurePlugin();
//............
// 用smp.warp()包裹一下合并的config
module.exports = smp.wrap(merge(_mergeConfig, webpackConfig));

3.通知面板

插件:webpack-build-notifier

// webpack.config.js文件
const WebpackBuildNotifierPlugin = require('webpack-build-notifier');
const webpackConfig= {
    plugins: [
        new WebpackBuildNotifierPlugin({
            title: '我的webpack',
            // logo: path.resolve('./img/favicon.png'),
            suppressSuccess: true
        })
    ]
}

4.开启打包进度

插件progress-bar-webpack-plugin

// webpack.config.js文件
const ProgressBarPlugin = require('progress-bar-webpack-plugin');
const webpackConfig= {
    plugins: [
        new ProgressBarPlugin(),
    ]
}

5.更清晰的开发面板

插件webpack-dashboard

// webpack.config.js文件
const DashboardPlugin = require('webpack-dashboard/plugin');
const webpackConfig= {
    plugins: [
        new DashboardPlugin()
        ]
}
// package.json文件
{
  "scripts": {
    "dev": "webpack-dashboard webpack --mode development",
  },
}

6.开启窗口的标题

插件node-bash-title

// webpack.config.js文件
const setTitle = require('node-bash-title');
setTitle('server');

7.友好的报错提示

插件friendly-errors-webpack-plugin

new FriendlyErrorsWebpackPlugin({
    compilationSuccessInfo: {
        messages: ['You application is running here http://localhost:3000'],
        notes: ['Some additionnal notes to be displayed unpon successful compilation']
    },
    onErrors: function (severity, errors) {
        // You can listen to errors transformed and prioritized by the plugin
        // severity can be 'error' or 'warning'
    },
    // should the console be cleared between each compilation?
    // default is true
    clearConsole: true,
    
    // add formatters and transformers (see below)
    additionalFormatters: [],
    additionalTransformers: []
}),

2.使用webpack4有哪些改进/新特性?

  1. V8带来的优化(for of替代forEach、Map和Set替代Object、includes替代indexOf)

  2. 默认使用更快的md4 hash算法

  3. webpacks AST可以直接从loader传递给AST,减少解析时间

  4. 使用字符串方法替代正则表达式

  5. 受 Parcel 启发,支持 0 配置启动项目,不再强制需要 webpack.config.js 配置文件,默认入口 ./src/ 目录,默认entry ./src/index.js ,默认输出 ./dist 目录,默认输出文件 ./dist/main.js

  6. 开箱即用 WebAssembly,webpack4提供了wasm的支持,现在可以引入和导出任何一个 Webassembly 的模块,也可以写一个loader来引入C++、C和Rust。(注:WebAssembly 模块只能在异步chunks中使用)

  7. 提供mode属性,设置为 development 将获得最好的开发体验,设置为 production 将专注项目编译部署,比如说开启 Scope hoisting 和 Tree-shaking 功能。

  8. 全新的插件系统,提供了针对插件和钩子的新API,变化如下:

    • 所有的 hook 由 hooks 对象统一管理,它将所有的hook作为可扩展的类属性
    • 添加插件时,你需要提供一个名字
    • 开发插件时,你可以选择插件的类型(sync/callback/promise之一)
    • 通过 this.hooks = { myHook: new SyncHook(…) } 来注册hook

3.简单描述一下 Babel 的编译过程

Babel 是一个 JavaScript 编译器,是一个工具链,主要用于将采用 ECMAScript 2015+ 语法编写的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中。

Babel 本质上就是在操作 AST 来完成代码的转译。AST是抽象语法树(Abstract Syntax Tree, AST)

如果想要了解更多,可以阅读和尝试:

Babel 的功能很纯粹,它只是一个编译器。大多数编译器的工作过程可以分为三部分:

  1. 解析(Parse) :将源代码转换成更加抽象的表示方法(例如抽象语法树)。包括词法分析和语法分析。词法分析主要把字符流源代码(Char Stream)转换成令牌流( Token Stream),语法分析主要是将令牌流转换成抽象语法树(Abstract Syntax Tree,AST)。
  2. 转换(Transform) :通过 Babel 的插件能力,对(抽象语法树)做一些特殊处理,将高版本语法的 AST 转换成支持低版本语法的 AST。让它符合编译器的期望,当然在此过程中也可以对 AST 的 Node 节点进行优化操作,比如添加、更新以及移除节点等。
  3. 生成(Generate) :将 AST 转换成字符串形式的低版本代码,同时也能创建 Source Map 映射。

经过这三个阶段,代码就被 Babel 转译成功了。

image.png image.png

说一说Git常用命令

查看分支:git branch
创建分支:git branch
切换分支:git checkout
创建+切换分支:git checkout -b
合并某分支到当前分支:git merge
删除分支:git branch -d

4.如何使用Git管理项目

我觉得可以按照实际情况来说,也可以参考下面的润色:

image.png

实际开发中,一个仓库(一般只放一个项目)主要存在两条主分支:master与develop分支。这个两个分支的生命周期是整个项目周期。

我们可能使用的不同类型的分支对项目进行管理是:

  • 功能分支 功能分支(或有时称为主题分支)用于为即将发布或遥远的未来版本开发新功能。在开始开发某个功能时,将包含该功能的目标版本在那时很可能是未知的。功能分支的本质在于,只要该功能处于开发阶段,它就存在,但最终会被合并回develop(明确将新功能添加到即将发布的版本中)或丢弃。功能分支通常只存在于开发者仓库中,而不存在于origin

  • 发布分支 发布分支支持准备新的生产版本。它们允许在最后一刻打点 i 和交叉 t。此外,它们允许修复小错误并为发布准备元数据(版本号、构建日期等)。通过在发布分支上完成所有这些工作,该develop 分支被清除以接收下一个大版本的功能。

    • develop分支拉取,且必须合并回 develop 和 master
    • 分支命名约定:release-*
  • 修补程序分支

    Hotfix 分支与发布分支非常相似,因为它们也旨在为新的生产版本做准备,尽管是计划外的。它们产生于需要立即对现场制作版本的不良状态采取行动。当必须立即解决生产版本中的关键错误时,可以从标记生产版本的主分支上的相应标记中分支出一个修补程序分支。

master:这个分支最为稳定,这个分支表明项目处于可发布的状态。
develop:做为开发的分支,平行于master分支。
Feature branches:这种分支和咱们程序员平常开发最为密切,称做功能分支。必须从develop分支建立,完成后合并回develop分支。
Release branches:这个分支用来分布新版本。从develop分支建立,完成后合并回develop与master分支。这个分支上能够作一些很是小的bug修复,固然,你也能够禁止在这个分支作任何bug的修复工做,而只作版本发布的相关操做,例如设置版本号等操做,那样的话那些发现的小bug就必须放到下一个版本修复了。若是在这个分支上发现了大bug,那么也绝对不能在这个分支上改,须要Featrue分支上改,走正常的流程。
Hotfix branches:这个分支主要为修复线上特别紧急的bug准备的。必须从master分支建立,完成后合并回develop与master分支。这个分支主要是解决线上版本的紧急bug修复的,例如忽然版本V0.1上有一个致命bug,必须修复。那么咱们就能够从master 分支上发布这个版本那个时间点 例如 tag v0.1(通常代码发布后会及时在master上打tag),来建立一个 hotfix-v0.1.1的分支,而后在这个分支上改bug,而后发布新的版本。最后将代码合并回develop与master分支。

6.实现CSS/JS代码压缩,以及代码CDN托管,图片整合

  1. CSS JS代码压缩
    可以应用gulp的gulp-uglify,gulp-minify-css模块完成;可以应用webpack的UglifyJsPlugin压缩插件完成。

  2. CDN托管
    内容分发网络,是建立在承载网基础上的虚拟分布式网络,能够将源站内容缓存到全国或全球的节点服务器上。用户就近获取内容,提高了资源的访问速度,分担源站压力

  3. 图片整合
    减少网站加载时间的最有效方式之一就是减少网站的HTTP请求数。实现这一目标的一个有效的方法就是通过CSS Sprites将多个图片整合到一个图片中,然后再用CSS来定位,缺点是可维护性差。可以使用百度的fis/webpack来自动化管理sprite。

7.如何利用webpack把代码上传服务器以及转码测试?

  1. 代码上传
    使用sftp-webpack-plugin,会把子文件夹提取出来,不优雅,可以使用gulp + webpack实现。

  2. 转码测试
    webpack应用babel来对ES6转码,开启devtool:“source-map”来进行浏览器测试,应用karma 或mocha来做单元测试。

8.项目上线流程是怎样的?

按照实际回答或者参考下面内容:
流程建议

  • 模拟线上的开发环境
    本地反向代理线上真实环境开发即可。(apache,nginx,nodejs均可实现)
  • 模拟线上的测试环境
    模拟线上的测试环境,其实是需要一台有真实数据的测试机,建议没条件搭daily的,就直接用线上数据测好了,只不过程序部分走你们的测试环境而已,有条件搭daily最好。
  • 可连调的测试环境
    可连调的测试环境,分为2种。一种是开发测试都在一个局域网段,直接绑hosts即可,不在一个网段,就每人分配一台虚拟的测试机,放在大家都可以访问到的公司内网,代码直接往上布即可。
  • 自动化的上线系统
    自动化的上线系统,可以采用Jenkins。如果没有,可以自行搭建一个简易的上线系统,原理是每次上线时都抽取最新的trunk或master,做一个tag,再打一个时间戳的标记,然后分发到cdn就行了。界面里就2个功能,打tag,回滚到某tag,部署。
  • 适合前后端的开发流程
    开发流程依据公司所用到的工具,构建,框架。原则就是分散独立开发,互相不干扰,连调时有hosts可绑即可。

简单的可操作流程

  • 代码通过git管理,新需求创建新分支,分支开发,主干发布;
  • 上线走简易上线系统,参见上一节;
  • 通过gulp+webpack连到发布系统,一键集成,本地只关心原码开发;
  • 本地环境通过webpack反向代理的server;
  • 搭建基于linux的本地测试机,自动完成build+push功能;

9.前端项目工程化怎么管理的?

zhuanlan.zhihu.com/p/61233318

10.webpack 和 gulp对比

虽然都是前端自动化构建工具,但两者有诸多不同:
定位
Gulp是基于流的自动化构建工具,适合多页面开发,易于学习,易于使用,接口优雅,适合多页面应用开发,但是在单页面应用方面输出乏力,而且对流行的单页技术有些难以处理(比如 Vue 单文件组件,使用 gulp 处理就会很困难,而 webpack 一个 loader 就能轻松搞定);
Webpack是模块化打包器,可以打包一切资源,适配各种模块系统,适合单页面应用开发,但是不适合多页应用开发,灵活度高但同时配置很繁琐复杂。“打包一切” 这个优点对于 HTTP/1.1 尤其重要,因为所有资源打包在一起能明显减少浏览器访问页面时的资源请求数量,从而减少应用程序必须等待的时间。但这个优点可能会随着 HTTP/2 的流行而变得不那么突出,因为 HTTP/2 的多路复用可以有效解决客户端并行请求时的瓶颈问题

作业方式
Gulp对输入(gulp.src)的 js,ts,scss,less 等源文件依次执行打包(bundle)、编译(compile)、压缩、重命名等处理后输出(gulp.dest)到指定目录中去,为了构建而打包;
Webpack对入口文件(entry)递归解析生成依赖关系图,然后将所有依赖打包在一起,在打包之前会将所有依赖转译成可打包的 js 模块,为了打包而构建

使用方式
Gulp需要用常规 js 开发,编写一系列构建任务(task);
Webpack需要编辑各种JSON格式的配置项

11.精灵图和base64如何选择?

css精灵,用于一些小的图标不是特别多,一个的体积也稍大,比如大于10K(这个没有严格的界定);
base64,用于小图标体积较小(相对于css精灵),多少都无所谓。字体图标,用于一些别人做好的图标库(也有少数自己去做的)用起来比较方便,他的图标只能用于单色,图标用只能于一种颜色。

12.webpack怎么引入第三方的库?

拿jQuery为例:

entry: {
    page: 'path/to/page.js';
    jquery: 'node_modules/jquery/dist/jquery.min.js'
}
new HtmlWebpackPlugin({
    filename: 'index.html',
    template: 'index.html',
    inject: true,
    chunks: ['jquery', 'page'] // 按照先后顺序插入script标签  
})

13.用过Nginx吗?都用过哪些?

按照实际回答或者参考 zhuanlan.zhihu.com/p/440358562

14.模块化发展历程

模块化主要是用来抽离公共代码,隔离作用域,避免变量冲突等。

IIFE:使用自执行函数来编写模块化,特点:在一个单独的函数作用域中执行代码,避免变量冲突

(function(){  
  return {  
    data:[]  
  }  
})()

AMD:使用requireJS 来编写模块化,特点:依赖必须提前声明好

define('./index.js',function(code){  
    // code 就是index.js 返回的内容  
})

CMD:使用seaJS 来编写模块化,特点:支持动态引入依赖文件

define(function(requireexportsmodule) {    
  var indexCode = require('./index.js');  
})

CommonJS:nodejs 中自带的模块化。

var fs = require('fs');

UMD:兼容AMD,CommonJS 模块化语法。

webpack(require.ensure) :webpack 2.x 版本中的代码分割。

ES Modules:ES6 引入的模块化,支持import 来引入另一个 js。

import a from 'a';

15.为什么输入 npm install 就可以自动安装对应的模块?

npm 模块安装机制:

  1. 发出npm install命令
  2. 查询node_modules目录之中是否已经存在指定模块
    • npm 向 registry 查询模块压缩包的网址
    • 下载压缩包,存放在根目录下的.npm目录里
    • 解压压缩包到当前项目的node_modules目录
    • 若存在,不再重新安装,若不存在进行安装

npm 实现原理

输入 npm install 命令并敲下回车后,会经历如下几个阶段(以 npm 5.5.1 为例):

  1. 执行工程自身 preinstall

    当前 npm 工程如果定义了 preinstall 钩子此时会被执行。

  2. 确定首层依赖模块

    首先需要做的是确定工程中的首层依赖,也就是 dependencies 和 devDependencies 属性中直接指定的模块(假设此时没有添加 npm install 参数)。

    工程本身是整棵依赖树的根节点,每个首层依赖模块都是根节点下面的一棵子树,npm 会开启多进程从每个首层依赖模块开始逐步寻找更深层级的节点。

  3. 获取模块

    获取模块是一个递归的过程,分为以下几步:

    • 获取模块信息。在下载一个模块之前,首先要确定其版本,这是因为 package.json 中往往是 semantic version(semver,语义化版本)。此时如果版本描述文件(npm-shrinkwrap.json 或 package-lock.json)中有该模块信息直接拿即可,如果没有则从仓库获取。如 packaeg.json 中某个包的版本是 ^1.1.0,npm 就会去仓库中获取符合 1.x.x 形式的最新版本。
    • 获取模块实体。上一步会获取到模块的压缩包地址(resolved 字段),npm 会用此地址检查本地缓存,缓存中有就直接拿,如果没有则从仓库下载。
    • 查找该模块依赖,如果有依赖则回到第1步,如果没有则停止。
  4. 模块扁平化(dedupe)

    上一步获取到的是一棵完整的依赖树,其中可能包含大量重复模块。比如 A 模块依赖于 loadsh,B 模块同样依赖于 lodash。在 npm3 以前会严格按照依赖树的结构进行安装,因此会造成模块冗余。

    从 npm3 开始默认加入了一个 dedupe 的过程。它会遍历所有节点,逐个将模块放在根节点下面,也就是 node-modules 的第一层。当发现有重复模块时,则将其丢弃。

    这里需要对重复模块进行一个定义,它指的是模块名相同且 semver 兼容。每个 semver 都对应一段版本允许范围,如果两个模块的版本允许范围存在交集,那么就可以得到一个兼容版本,而不必版本号完全一致,这可以使更多冗余模块在 dedupe 过程中被去掉。

    比如 node-modules 下 foo 模块依赖 lodash@^1.0.0,bar 模块依赖 lodash@^1.1.0,则  ^1.1.0 为兼容版本。

    而当 foo 依赖 lodash@^2.0.0,bar 依赖 lodash@^1.1.0,则依据 semver 的规则,二者不存在兼容版本。会将一个版本放在 node_modules 中,另一个仍保留在依赖树里。

    举个例子,假设一个依赖树原本是这样:

    node_modules
    -- foo
    ---- lodash@version1

    -- bar
    ---- lodash@version2

    假设 version1 和 version2 是兼容版本,则经过 dedupe 会成为下面的形式:

    node_modules
    -- foo

    -- bar

    -- lodash(保留的版本为兼容版本)

    假设 version1 和 version2 为非兼容版本,则后面的版本保留在依赖树中:

    node_modules
    -- foo
    -- lodash@version1

    -- bar
    ---- lodash@version2

  5. 安装模块

    这一步将会更新工程中的 node_modules,并执行模块中的生命周期函数(按照 preinstall、install、postinstall 的顺序)。

  6. 执行工程自身生命周期

    当前 npm 工程如果定义了钩子此时会被执行(按照 install、postinstall、prepublish、prepare 的顺序)。

    最后一步是生成或更新版本描述文件,npm install 过程完成.

16.wepack中loader和plugin的区别

什么是loader?
loader是文件加载器,能够加载资源文件,并对这些文件进行一些处理,诸如编译、压缩等,最终一起打包到指定的文件中

什么是plugin?
在webpack运行的生命周期中会广播出许多事件,plugin可以监听这些事件,在合适的时机通过webpack提供的API改变输出结果。

区别:

  • 对于loader,它是一个转换器,将A文件进行编译形成B文件,这里操作的是文件,比如将A.scss转换为A.css,单纯的文件转换过程
  • plugin是一个扩展器,它丰富了webpack本身,针对是loader结束后,webpack打包的整个过程,它并不直接操作文件,而是基于事件机制工作,会监听webpack打包过程中的某些节点,执行广泛的任务。

17.简述 webpack 工作流程

关于 webpack 的工作流程,简单来说可以概括为以下几步:

  1. 参数解析
  2. 找到入口文件
  3. 调用 Loader 编译文件
  4. 遍历 AST,收集依赖
  5. 生成 Chunk
  6. 输出文件

18.webpack的loader有什么作用?如何使用?

作用

Loader 的作用很简单,就是处理任意类型的文件,并且将它们转换成一个让 webpack 可以处理的有效模块。

配置和使用

在 config 里配置
Loader 可以在 webpack.config.js里配置,这也是推荐的做法,定义在 module.rules 里:

// webpack.config.js
module.exports = {
  module: {
    rules: [
      { test: /\.js$/, use: 'babel-loader' },
      {
        test: /\.css$/,
        use: [
          { loader: 'style-loader' },
          { loader: 'css-loader' },
          { loader: 'postcss-loader' },
        ]
      }
    ]
  }
};

每一条 rule 会包含两个属性:test 和 use,比如 { test: /.js$/, use: 'babel-loader' } 意思就是:当 webpack 遇到扩展名为 js 的文件时,先用 babel-loader 处理一下,然后再打包它。

use 的类型:string|array|object|function:

string: 只有一个 Loader 时,直接声明 Loader,比如 babel-loader。 array: 声明多个 Loader 时,使用数组形式声明,比如上文声明 .css 的 Loader。 object: 只有一个 Loader 时,需要有额外的配置项时。 function: use 也支持回调函数的形式。

内联 可以在 import 等语句里指定 Loader,使用 ! 来将 Loader分开:

import style from 'style-loader!css-loader?modules!./styles.css';

内联时,通过 query 来传递参数,例如 ?key=value。

一般来说,推荐使用统一 config 的形式来配置 Loader,内联形式多出现于 Loader 内部,比如 style-loader 会在自身代码里引入 css-loader:

require("!!../../node_modules/css-loader/dist/cjs.js!./styles.css");

19.有哪些loader类型?

同步loader

module.exports = function(source) {
  const result = someSyncOperation(source); // 同步逻辑
  return result;
}

一般来说,Loader 都是同步的,通过 return 或者 this.callback 来同步地返回 source转换后的结果。

异步 Loader

有的时候,我们需要在 Loader 里做一些异步的事情,比如说需要发送网络请求。如果同步地等着,网络请求就会阻塞整个构建过程,这个时候我们就需要进行异步 Loader,可以这样做:

module.exports = function(source) {
  // 告诉 webpack 这次转换是异步的
  const callback = this.async();
  // 异步逻辑
  someAsyncOperation(content, function(err, result) {
    if (err) return callback(err);
    // 通过 callback 来返回异步处理的结果
    callback(null, result, map, meta);
  });
};

Pitching Loader

{
  test: /\.js$/,
  use: [
    { loader: 'aa-loader' },
    { loader: 'bb-loader' },
    { loader: 'cc-loader' },
  ]
}

我们知道,Loader 总是从右到左被调用。上面配置的 Loader,就会按照以下顺序执行:

cc-loader -> bb-loader -> aa-loader

每个 Loader 都支持一个 pitch 属性,通过 module.exports.pitch 声明。如果该 Loader 声明了 pitch,则该方法会优先于 Loader 的实际方法先执行,官方也给出了执行顺序:

|- aa-loader `pitch`
  |- bb-loader `pitch`
    |- cc-loader `pitch`
      |- requested module is picked up as a dependency
    |- cc-loader normal execution
  |- bb-loader normal execution
|- aa-loader normal execution

也就是会先从左向右执行一次每个 Loader 的 pitch 方法,再按照从右向左的顺序执行其实际方法。

Raw Loader

我们在 url-loader 里和 file-loader 最后都见过这样一句代码:

export const raw = true;

默认情况下,webpack 会把文件进行 UTF-8 编码,然后传给 Loader。通过设置 rawLoader 就可以接受到原始的 Buffer 数据。

20.用过哪些loader的api?

官方文档: webpack.js.org/api/loaders…
所谓 Loader,也只是一个符合 commonjs 规范的 node 模块,它会导出一个可执行函数。loader runner 会调用这个函数,将文件的内容或者上一个 Loader 处理的结果传递进去。同时,webpack 还为 Loader 提供了一个上下文 this,其中有很多有用的 api

this.callback()

在 Loader 中,通常使用 return 来返回一个字符串或者 Buffer。如果需要返回多个结果值时,就需要使用 this.callback

this.callback(
  // 无法转换时返回 Error,其余情况都返回 null
  err: Error | null,
  // 转换结果
  content: string | Buffer,
  // source map,方便调试用的
  sourceMap?: SourceMap,
  // 可以是任何东西。比如 ast
  meta?: any
);

一般来说如果调用该函数的话,应该手动 return,告诉 webpack 返回的结果在 this.callback 中,以避免含糊不清的结果:

module.exports = function(source) {
  this.callback(null, source, sourceMaps);
  return;
};

this.async()

异步loader

module.exports = function(source) {
  // 告诉 webpack 这次转换是异步的
  const callback = this.async();
  // 异步逻辑
  someAsyncOperation(content, function(err, result) {
    if (err) return callback(err);
    // 通过 callback 来返回异步处理的结果
    callback(null, result, map, meta);
  });
};

this.cacheable()

有些情况下,有些操作需要耗费大量时间,每一次调用 Loader 转换时都会执行这些费时的操作。

在处理这类费时的操作时, webapck 会默认缓存所有 Loader 的处理结果,只有当被处理的文件发生变化时,才会重新调用 Loader 去执行转换操作。

webpack 是默认可缓存的,可以执行 this.cacheable(false) 手动关闭缓存。

this.resource

当前处理文件的完整请求路径,包括 query,比如 /src/App.vue?type=templpate

this.resourcePath

当前处理文件的路径,不包括 query,比如 /src/App.vue

this.resourceQuery

当前处理文件的 query 字符串,比如 ?type=template。我们在 vue-loader 里有见过如何使用它:

const qs = require('querystring');

const { resourceQuery } = this;
const rawQuery = resourceQuery.slice(1); // 删除前面的 ?
const incomingQuery = qs.parse(rawQuery); // 解析字符串成对象
// 取 query
if (incomingQuery.type) {}

this.emitFile

让 webpack 在输出目录新建一个文件,

if (typeof options.emitFile === 'undefined' || options.emitFile) {
  this.emitFile(outputPath, content);
}

20.loader特点总结

  • Loader 是一个 node 模块;
  • Loader 可以处理任意类型的文件,转换成 webpack 可以处理的模块;
  • Loader 可以在 webpack.config.js 里配置,也可以在 require 语句里内联;
  • Loader 可以根据配置从右向左链式执行;
  • Loader 接受源文件内容字符串或者 Buffer
  • Loader 分为多种类型:同步、异步和 pitching,他们的执行流程不一样;
  • webpack 为 Loader 提供了一个上下文,有一些 api 可以使用

21.loader的工作流程是怎样的?

webpack编译流程非常复杂,但其中涉及loader的部分主要包括了:

  • loader(webpack)的默认配置
  • 使用loaderResolver解析loader模块路径
  • 根据rule.modules创建RulesSet规则集
  • 使用loader-runner运行loader

其对应的大致流程如下:

image.png

  1. 首先,在Compiler.js中会为将用户配置与默认配置合并,其中就包括了loader部分。

  2. 然后,webpack就会根据配置创建两个关键的对象——NormalModuleFactoryContextModuleFactory。它们相当于是两个类工厂,通过其可以创建相应的NormalModuleContextModule。其中NormalModule类是这篇文章主要关注的,webpack会为源码中的模块文件对应生成一个NormalModule实例。

  3. 在工厂创建NormalModule实例之前还有一些必要步骤,其中与loader最相关的就是通过loader的resolver来解析loader路径。

  4. NormalModule实例创建之后,则会通过其.build()方法来进行模块的构建。构建模块的第一步就是使用loader来加载并处理模块内容。而loader-runner这个库就是webpack中loader的运行器。

  5. 最后,将loader处理完的模块内容输出,进入后续的编译流程。

22.如何编写一个loader?

blog.csdn.net/Niall_Tonsh…

23.vue-loader详解

mp.weixin.qq.com/s/NO5jZfoHZ…

24.style-loader详解

mp.weixin.qq.com/s/alIKsKkGR…

25.webpack编译流程详解

mp.weixin.qq.com/s?__biz=MzI…

26.实现一个简易的webpack

juejin.cn/post/684490…

27.什么是webpack?有什么作用?

webpack是一个模块化打包器,分析项目结构,处理模块化依赖,转换成为浏览器可运行的代码

  • 代码转换: TypeScript 编译成 JavaScript、SCSS,LESS 编译成 CSS.

  • 文件优化:压缩 JavaScript、CSS、HTML 代码,压缩合并图片。

  • 代码分割:提取多个页面的公共代码、提取首屏不需要执行部分的代码让其异步加载。

  • 模块合并:在采用模块化的项目里会有很多个模块和文件,需要构建功能把模块分类合并成一个文件。

  • 自动刷新:监听本地源代码的变化,自动重新构建、刷新浏览器。

构建把一系列前端代码自动化去处理复杂的流程,解放生产力

28.用过webpack plugin么?

webpack整个构建流程有许多钩子,开发者可以在指定的阶段加入自己的行为到webpack构建流程中。插件由以下构成:

  • 一个 JavaScript 命名函数。
  • 在插件函数的 prototype 上定义一个 apply 方法。
  • 指定一个绑定到 webpack 自身的事件钩子。
  • 处理 webpack 内部实例的特定数据。
  • 功能完成后调用 webpack 提供的回调。

整个webpack流程由compiler和compilation构成,compiler只会创建一次,compilation如果开起了watch文件变化,那么会多次生成compilation。

29.WebPack和Grunt以及Gulp相比有什么特性?

其实Webpack和另外两个并没有太多的可比性,Gulp/Grunt是一种能够优化前端的开发流程的工具,而WebPack是一种模块化的解决方案,不过Webpack的优点使得Webpack在很多场景下可以替代Gulp/Grunt类的工具。

  1. Entry:入口,Webpack 执行构建的第一步将从 Entry 开始,可抽象成输入。
  2. Module:模块,在 Webpack 里一切皆模块,一个模块对应着一个文件。Webpack 会从配置的 Entry 开始递归找出所有依赖的模块。
  3. Chunk:代码块,一个 Chunk 由多个模块组合而成,用于代码合并与分割。
  4. Loader:模块转换器,用于把模块原内容按照需求转换成新内容。
  5. Plugin:扩展插件,在 Webpack 构建流程中的特定时机注入扩展逻辑来改变构建结果或做你想要的事情。
  6. Output:输出结果,在 Webpack 经过一系列处理并得出最终想要的代码后输出结果。

30.webpack从0开始使用

juejin.cn/post/684490…

31.手写webpack loader

juejin.cn/post/684490…

32.webpack 如何通过作用域分析消除无用代码

diverse.space/2018/05/bet…

33.webpack默认配置是在哪处理的,loader有什么默认配置么?

webpack的入口文件是lib/webpack.js,会根据配置文件,设置编译时的配置options

options = new WebpackOptionsDefaulter().process(options);
compiler = new Compiler(options.context);
compiler.options = options;

由此可见,默认配置是放在WebpackOptionsDefaulter里的。例如,在module.rules这部分的默认值为[];但是此外还有一个module.defaultRules配置项,虽然不开放给开发者使用,但是包含了loader的默认配置。

34.webpack中有一个resolver的概念,用于解析模块文件的真实绝对路径,那么loader和normal module的resolver使用的是同一个么?

NormalModuleFactory中,创建出NormalModule实例之前会涉及到四个钩子:

  • beforeResolve
  • resolve
  • factory
  • afterResolve

其中较为重要的有两个:

  • resolve部分负责解析loader模块的路径(例如css-loader这个loader的模块路径是什么);
  • factory负责来基于resolve钩子的返回值来创建NormalModule实例。

resolve钩子上注册的方法较长,其中还包括了模块资源本身的路径解析。resolver有两种,分别是loaderResolver和normalResolver。

const loaderResolver = this.getResolver("loader");
const normalResolver = this.getResolver("normal", data.resolveOptions);

由于除了config文件中可以配置loader外,还有inline loader的写法,因此,对loader文件的路径解析也分为两种:inline loader和config文件中的loader。resolver钩子中会先处理inline loader。

35.什么是inline loader?

import Styles from 'style-loader!css-loader?modules!./styles.css';

上面是一个inline loader的例子。其中的request为style-loader!css-loader?modules!./styles.css

首先webpack会从request中解析出所需的loader:

let elements = requestWithoutMatchResource
    .replace(/^-?!+/, "")
    .replace(/!!+/g, "!")
    .split("!");

因此,从style-loader!css-loader?modules!./styles.css中可以取出两个loader:style-loadercss-loader

然后会将“解析模块的loader数组”与“解析模块本身”一起并行执行。 解析返回的结果格式大致如下:

[ 
    // 第一个元素是一个loader数组
    [ { 
        loader:
            '/workspace/basic-demo/home/node_modules/html-webpack-plugin/lib/loader.js',
        options: undefined
    } ],
    // 第二个元素是模块本身的一些信息
    {
        resourceResolveData: {
            context: [Object],
            path: '/workspace/basic-demo/home/public/index.html',
            request: undefined,
            query: '',
            module: false,
            file: false,
            descriptionFilePath: '/workspace/basic-demo/home/package.json',
            descriptionFileData: [Object],
            descriptionFileRoot: '/workspace/basic-demo/home',
            relativePath: './public/index.html',
            __innerRequest_request: undefined,
            __innerRequest_relativePath: './public/index.html',
            __innerRequest: './public/index.html'
        },
	resource: '/workspace/basic-demo/home/public/index.html'
    }
]

其中第一个元素就是该模块被引用时所涉及的所有inline loader,包含loader文件的绝对路径和配置项。

36.inline loader和normal config loader执行的先后顺序是什么?

module.rules部分的配置如下:

const result = this.ruleSet.exec({
    resource: resourcePath,
    realResource:
        matchResource !== undefined
            ? resource.replace(/\?.*/, "")
            : resourcePath,
    resourceQuery,
    issuer: contextInfo.issuer,
    compiler: contextInfo.compiler
});

NormalModuleFactory中有一个ruleSet的属性,这里你可以简单理解为:它可以根据模块路径名,匹配出模块所需的loader。RuleSet细节此处先按下不表,其具体内容我会在下一节介绍。

这里向this.ruleSet.exec()中传入源码模块路径,返回的result就是当前模块匹配出的config中的loader。如果你熟悉webpack配置,会知道module.rules中有一个enforce字段。基于该字段,webpack会将loader分为preLoader、postLoader和loader三种:

for (const r of result) {
    if (r.type === "use") {
        // post类型
        if (r.enforce === "post" && !noPrePostAutoLoaders) {
            useLoadersPost.push(r.value);
        // pre类型
        } else if (
            r.enforce === "pre" &&
            !noPreAutoLoaders &&
            !noPrePostAutoLoaders
        ) {
            useLoadersPre.push(r.value);
        } else if (
            !r.enforce &&
            !noAutoLoaders &&
            !noPrePostAutoLoaders
        ) {
            useLoaders.push(r.value);
        }
    }
    // ……
}

最后,使用neo-aysnc来并行解析三类loader数组:

asyncLib.parallel(
    [
        this.resolveRequestArray.bind(
            this,
            contextInfo,
            this.context,
            useLoadersPost, // postLoader
            loaderResolver
        ),
        this.resolveRequestArray.bind(
            this,
            contextInfo,
            this.context,
            useLoaders, // loader
            loaderResolver
        ),
        this.resolveRequestArray.bind(
            this,
            contextInfo,
            this.context,
            useLoadersPre, // preLoader
            loaderResolver
        )
    ]
    // ……
}
loaders = results[0].concat(loaders, results[1], results[2]);

其中results[0]results[1]results[2]loader分别是postLoader、loader(normal config loader)、preLoader和inlineLoader。因此合并后的loader顺序是:post、inline、normal和pre。

然而loader是从右至左执行的,真实的loader执行顺序是倒过来的,因此inlineLoader是整体后于config中normal loader执行的。

37.配置中的module.rules在webpack中是如何生效与实现的?

webpack使用RuleSet对象来匹配模块所需的loader。RuleSet相当于一个规则过滤器,会将resourcePath应用于所有的module.rules规则,从而筛选出所需的loader。其中最重要的两个方法是:

  • 类静态方法.normalizeRule()
  • 实例方法.exec()

webpack编译会根据用户配置与默认配置,实例化一个RuleSet。首先,通过其上的静态方法.normalizeRule()将配置值转换为标准化的test对象;其上还会存储一个this.references属性,是一个map类型的存储,key是loader在配置中的类型和位置,例如,ref-2表示loader配置数组中的第三个。 实例化后的RuleSet就可以用于为每个模块获取对应的loader。这个实例化的RuleSet就是我们上面提到的NormalModuleFactory实例上的this.ruleSet属性。工厂每次创建一个新的NormalModule时都会调用RuleSet实例的.exec()方法,只有当通过了各类测试条件,才会将该loader push到结果数组中。

38.webpack编译流程中loader是如何以及在何时发挥作用的?

loader的绝对路径解析完毕后,在NormalModuleFactoryfactory钩子中会创建当前模块的NormalModule对象。到目前为止,loader的前序工作已经差不多结束了,下面就是真正去运行各个loader。

我们都知道,运行loader读取与处理模块是webpack模块处理的第一步。但如果说到详细的运行时机,就涉及到webpack编译中compilation这个非常重要的对象。

webpack是以入口维度进行编译的,compilation中有一个重要方法——.addEntry(),会基于入口进行模块构建。.addEntry()方法中调用的._addModuleChain()会执行一系列的模块方法:

this.semaphore.acquire(() => {
    moduleFactory.create(
        {
            // ……
        },
        (err, module) => {
            if (err) {
                this.semaphore.release();
                return errorAndCallback(new EntryModuleNotFoundError(err));
            }
            // ……
            if (addModuleResult.build) {
                // 模块构建
                this.buildModule(module, false, null, null, err => {
                    if (err) {
                        this.semaphore.release();
                        return errorAndCallback(err);
                    }

                    if (currentProfile) {
                        const afterBuilding = Date.now();
                        currentProfile.building = afterBuilding - afterFactory;
                    }

                    this.semaphore.release();
                    afterBuild();
                });
            }
        }
    )
}

其中,对于未build过的模块,最终会调用到NormalModule对象的.doBuild()方法。而构建模块.doBuild()的第一步就是运行所有的loader。

39.loader为什么是自右向左执行的?

webpack将loader的运行工具剥离出来,独立成了loader-runner库,loader-runner分为了两个部分:loadLoader.js与LoaderRunner.js。

loadLoader.js是一个兼容性的模块加载器,可以加载例如cjs、esm或SystemJS这种的模块定义。而LoaderRunner.js则是loader模块运行的核心部分。其中暴露出来的.runLoaders()方法则是loader运行的启动方法。

每个loader模块都支持一个.pitch属性,上面的方法会优先于loader的实际方法执行。实际上,webpack官方也给出了pitch与loader本身方法的执行顺序图:

|- a-loader `pitch`
  |- b-loader `pitch`
    |- c-loader `pitch`
      |- requested module is picked up as a dependency
    |- c-loader normal execution
  |- b-loader normal execution
|- a-loader normal execution

这两个阶段(pitch和normal)就是loader-runner中对应的iteratePitchingLoaders()iterateNormalLoaders()两个方法。

iteratePitchingLoaders()会递归执行,并记录loader的pitch状态与当前执行到的loaderIndexloaderIndex++)。当达到最大的loader序号时,才会处理实际的module:

if(loaderContext.loaderIndex >= loaderContext.loaders.length)
    return processResource(options, loaderContext, callback);

loaderContext.loaderIndex值达到整体loader数组长度时,表明所有pitch都被执行完毕(执行到了最后的loader),这时会调用processResource()来处理模块资源。主要包括:添加该模块为依赖和读取模块内容。然后会递归执行iterateNormalLoaders()并进行loaderIndex--操作,因此loader会“反向”执行。

40.如果在某个pitch中返回值,具体会发生什么?

据官方说明:在pitch中返回值会跳过余下的loader。
只有当loaderIndex达到最大数组长度,即pitch过所有loader后,才会执行processResource()

if(loaderContext.loaderIndex >= loaderContext.loaders.length)
    return processResource(options, loaderContext, callback);

因此,在pitch中返回值除了跳过余下loader外,不仅会使.addDependency()不触发(不将该模块资源添加进依赖),而且无法读取模块的文件内容。loader会将pitch返回的值作为“文件内容”来处理,并返回给webpack。

41.如果你写过loader,那么可能在loader function中用到了this,这里的this究竟是什么,是webpack实例么?

其实这里的this既不是webpack实例,也不是compiler、compilation、normalModule等这些实例。而是一个loaderContext的loader-runner特有对象

每次调用runLoaders()方法时,如果不显式传入context,则会默认创建一个新的loaderContext。所以在官网上提到的各种loader API(callback、data、loaderIndex、addContextDependency等)都是该对象上的属性。

42.loader function中的this.data是如何实现的?

loader中的this其实是一个叫loaderContext的对象,那么this.data的实现其实就是loaderContext.data的实现。

Object.defineProperty(loaderContext, "data", {
    enumerable: true,
    get: function() {
        return loaderContext.loaders[loaderContext.loaderIndex].data;
    }
});

这里定义了一个.data的(存)取器。可以看出,调用this.data时,不同的normal loader由于loaderIndex不同,会得到不同的值;而pitch方法的形参data也是不同的loader下的data。

runSyncOrAsync(
    fn,
    loaderContext,
    [loaderContext.remainingRequest, loaderContext.previousRequest, currentLoaderObject.data = {}],
    function(err) {
        // ……
    }
);

runSyncOrAsync()中的数组[loaderContext.remainingRequest, loaderContext.previousRequest, currentLoaderObject.data = {}]就是pitch方法的入参,而currentLoaderObject就是当前loaderIndex所指的loader对象。

43.如何写一个异步loader,webpack又是如何实现loader的异步化的?

pitch与normal loader的实际执行,都是在runSyncOrAsync()这个方法中。

根据webpack文档,当我们调用this.async()时,会将loader变为一个异步的loader,并返回一个异步回调。

在具体实现上,runSyncOrAsync()内部有一个isSync变量,默认为true;当我们调用this.async()时,它会被置为false,并返回一个innerCallback作为异步执行完后的回调通知:

context.async = function async() {
    if(isDone) {
        if(reportedError) return; // ignore
        throw new Error("async(): The callback was already called.");
    }
    isSync = false;
    return innerCallback;
};

一般都使用this.async()返回的callback来通知异步完成,但实际上,执行this.callback()也是一样的效果:

var innerCallback = context.callback = function() {
    // ……
}

同时,在runSyncOrAsync()中,只有isSync标识为true时,才会在loader function执行完毕后立即(同步)回调callback来继续loader-runner。

if(isSync) {
    isDone = true;
    if(result === undefined)
        return callback();
    if(result && typeof result === "object" && typeof result.then === "function") {
        return result.catch(callback).then(function(r) {
            callback(null, r);
        });
    }
    return callback(null, result);
}

代码里有一处会判断返回值是否是Promise(typeof result.then === "function"),如果是Promise则会异步调用callback。因此,想要获得一个异步的loader,除了webpack文档里提到的this.async()方法,还可以直接返回一个Promise。

44.聊一聊webpack-dev-server和其中socket,HMR的实现

这个问题感觉很偏,可以了解一下:github.com/879479119/8…

45.如何通过Webpack实现Code Splitting

zhuanlan.zhihu.com/p/26710831

46.说一下webpack的热更新(HMR)原理

什么是HMR

HMR 全称 Hot Module Replacement,中文语境通常翻译为模块热更新,它能够在保持页面状态的情况下动态替换资源模块,提供丝滑顺畅的 Web 页面开发体验.

启用HMR

1. 配置 devServer.hot 属性为 true

devServer:{
        hot:true, //支持热更新
        port:8080,
        //contentBase:path.resolve(__dirname,'static') //指定(额外的)静态文件目录, // 如果使用 CopyWebpackPlugin ,设置为false
        static:path.resolve(__dirname,'static')  // webpack5
    },

2. 调用 module.hot.accept 接口

调用 module.hot.accept 接口,声明如何将模块安全地替换为最新代码,如

import component from"./component";
 
let demoComponent = component();
 
document.body.appendChild(demoComponent);
 
// HMR interface
 
if (module.hot) { 
 
 // Capture hot update
 
 module.hot.accept("./component", () => {
     const nextComponent = component();
     // Replace old content with the hot loaded one 
     document.body.replaceChild(nextComponent, demoComponent); 
     demoComponent = nextComponent;
 
 });
 
}

不同模块的具体替换逻辑不同也很复杂,但是业界许多 Webpack Loader 已经提供了针对不同资源的 HMR 功能,例如:

  • style-loader 内置 Css 模块热更
  • vue-loader 内置 Vue 模块热更
  • react-hot-reload 内置 React 模块热更接口

实现原理

HMR的核心流程:

  1. 使用 webpack-dev-server (后面简称 WDS)托管静态资源,同时以 Runtime 方式注入 HMR 客户端代码;
  2. 浏览器加载页面后,与 WDS 建立 WebSocket 连接;
  3. Webpack 监听到文件变化后,增量构建发生变更的模块,并通过 WebSocket 发送 hash 事件;
  4. 浏览器接收到 hash 事件后,请求 manifest 资源文件,确认增量变更范围;
  5. 浏览器加载发生变更的增量模块;
  6. Webpack 运行时触发变更模块的 module.hot.accept 回调,执行代码变更逻辑。

HMR的关键实现

1. 注入 HMR 客户端运行时

执行 npx webpack serve 命令后,WDS 调用 HotModuleReplacementPlugin 插件向应用的主 Chunk 注入一系列 HMR Runtime,包括但不限于:

  • 用于建立 WebSocket 连接,处理 hash 等消息的运行时代码;
  • 用于加载热更新资源的 RuntimeGlobals.hmrDownloadManifest 与 RuntimeGlobals.hmrDownloadUpdateHandlers 接口;
  • 用于处理模块更新策略的 module.hot.accept 接口 经过 HotModuleReplacementPlugin 处理后,构建产物中即包含了所有运行 HMR 所需的客户端运行时与接口。这些 HMR 运行时会在浏览器执行一套基于 WebSocket 消息的时序框架,如图:

image.png

2. 增量构建

除注入客户端代码外,HotModuleReplacementPlugin 插件还会借助 Webpack 的 watch 能力,在代码文件发生变化后执行增量构建,生成:

manifest 文件:JSON 格式文件,包含所有发生变更的模块列表,命名为 [hash].hot-update.json; 模块变更文件:js 格式,包含编译后的模块代码,命名为 [hash].hot-update.js; 增量构建完毕后,Webpack 将触发 compilation.hooks.done 钩子,并传递本次构建的统计信息对象 stats。WDS 则监听 done 钩子,在回调中通过 WebSocket 发送模块更新消息。

3. 加载更新

客户端接受到 hash 消息后,首先发出 manifest 请求获取本轮热更新涉及的 chunk,manifest 请求完成后,客户端 HMR 运行时开始下载发生变化的 chunk 文件,将最新模块代码加载到本地。

注意,在 Webpack 4 及之前,热更新文件以模块为单位,即所有发生变化的模块都会生成对应的热更新文件; Webpack 5 之后热更新文件以 chunk 为单位,如上例中,main chunk 下任意文件的变化都只会生成 main.[hash].hot-update.js 更新文件。

4. module.hot.accept回调

经过上述步骤,浏览器加载完最新模块代码后,HMR 运行时会继续触发 module.hot.accept 回调,将最新代码替换到运行环境中。

module.hot.accept 是 HMR 运行时暴露给用户代码的重要接口之一,它在 Webpack HMR 体系中开了一个口子,让用户能够自定义模块热替换的逻辑。module.hot.accept 接口签名如下:

module.hot.accept(path?: string, callback?: function);它接受两个参数:

path:指定需要拦截变更行为的模块路径 callback:模块更新后,将最新模块代码应用到运行环境的函数。

5. 失败兜底

module.hot.accept 函数只接受具体路径的 path 参数,也就是说我们无法通过 glob 或类似风格的方式批量注册热更新回调。

一旦某个模块没有注册对应的 module.hot.accept 函数后,HMR 运行时会执行兜底策略,通常是刷新页面,确保页面上运行的始终是最新的代码。

6. 更新事件冒泡

在 Webpack HMR 框架中,module.hot.accept 函数只能捕获当前模块对应子孙模块的更新事件,例如对于下面的模块依赖树:

image.png

示例中,更新事件会沿着模块依赖树自底向上逐级传递,从 foo 到 index ,从 bar-1 到 bar 再到 index,但不支持反向或跨子树传递,也就是说:

在 foo.js 中无法捕获 bar.js 及其子模块的变更事件; 在 bar-1.js 中无法捕获 bar.js 的变更事件 这一特性与 DOM 事件规范中的冒泡过程极为相似,使用时如果摸不准模块的依赖关系,建议直接在应用的入口文件中编写热更新函数。

7. 无参数调用

module.hot.accept 函数还支持无参数调用风格,作用是捕获当前文件的变更事件,并从模块第一行开始重新运行该模块的代码。

47.webpack的Plugin是什么?有什么作用?工作原理是怎样的?

是什么?

plugin是插件的意思,通常用于对现有的架构进行扩展。webpack中的插件,就是对webpack现有功能的各种扩展,比如打包优化,文件压缩等等。

作用

按官方说法:可以通过插件,扩展 webpack,加入自定义的构建行为,使 webpack 可以执行更广泛的任务,拥有更强的构建能力。

工作原理

webpack 就像一条生产线,要经过一系列处理流程后才能将源文件转换成输出结果。 这条生产线上的每个处理流程的职责都是单一的,多个流程之间有存在依赖关系,只有完成当前处理后才能交给下一个流程去处理。

webpack 在编译过代码程中,会触发一系列 Tapable 钩子事件,插件所做的,就是找到相应的钩子,往上面挂上自己的任务,也就是注册事件,这样,当 webpack 构建的时候,插件注册的事件就会随着钩子的触发而执行了。

相关重要知识点

什么是钩子?

钩子的本质就是:事件。为了方便我们直接介入和控制编译过程,webpack 把编译过程中触发的各类关键事件封装成事件接口暴露了出来,这些接口被很形象地称做:hooks(钩子)。开发插件,离不开这些钩子。

Tapable

Tapable 为 webpack 提供了统一的插件接口(钩子)类型定义,它是 webpack 的核心功能库。webpack 中目前有十种 hooks,在 Tapable 源码中可以看到:

// https://github.com/webpack/tapable/blob/master/lib/index.js

exports.SyncHook = require("./SyncHook");
exports.SyncBailHook = require("./SyncBailHook");
exports.SyncWaterfallHook = require("./SyncWaterfallHook");
exports.SyncLoopHook = require("./SyncLoopHook");
exports.AsyncParallelHook = require("./AsyncParallelHook");
exports.AsyncParallelBailHook = require("./AsyncParallelBailHook");
exports.AsyncSeriesHook = require("./AsyncSeriesHook");
exports.AsyncSeriesBailHook = require("./AsyncSeriesBailHook");
exports.AsyncSeriesLoopHook = require("./AsyncSeriesLoopHook");
exports.AsyncSeriesWaterfallHook = require("./AsyncSeriesWaterfallHook");

Tapable 还统一暴露了三个方法给插件,用于注入不同类型的自定义构建行为:

  • tap:可以注册同步钩子和异步钩子。
  • tapAsync:回调方式注册异步钩子。
  • tapPromise:Promise方式注册异步钩子。

webpack 里的几个非常重要的对象,CompilerCompilation 和 JavascriptParser 都继承了 Tapable 类,它们身上挂着丰富的钩子,基于这些钩子,可以按需开发出各种功能的插件,应用于编译的各个阶段。

48.开发一个最简的webpack 插件

一个 webpack plugin 由如下部分组成:

  1. 一个命名的 Javascript 方法或者 JavaScript 类。
  2. 它的原型上需要定义一个叫做 apply 的方法。
  3. 注册一个事件钩子。
  4. 操作webpack内部实例特定数据。
  5. 功能完成后,调用webpack提供的回调。

一个基本的 plugin 代码结构大致长这个样子:

// plugins/MyPlugin.js

class MyPlugin {
  apply(compiler) {
    compiler.hooks.done.tap('My Plugin', (stats) => {
      console.log('Bravo!');
    });
  }
}

module.exports = MyPlugin;

49.项目中做过哪些基础架构工作有哪些?

按照实际回答即可,不然容易引入自己不掌握的知识点

50.CDN的工作原理是什么?

  1. 用户向浏览器提供要访问网站的域名,域名解析的请求被发往本地用户使用的DNS服务器,本地DNS服务器将解析请求转发至网站的DNS服务器(NS);

  2. 由于网站的DNS服务器(NS)对此域名的解析设置了CNAME,请求最终被指向到CDN网络中的GLB系统;

  3. GLB系统对域名进行智能解析,将响应速度最快的节点IP返回给用户;

  4. 浏览器在得到实际的IP地址以后,向CDN节点发出访问请求;

  5. 由于是第一次访问,CDN节点将回到源站获得用户请求的数据并发给用户,同时CDN节点根据缓存策略对该数据进行缓存;

  6. 当有其他用户再次访问同样内容时,CDN节点直接将数据返回给客户,完成请求/服务过程

51.如何做技术选型?

  • 了解当前业务需求,总结需求的核心诉求,是追求性能、体验、迭代效率还是快速上线?
  • 了解业务中长期规划,归纳中长期迭代运维所依赖的技术能力;
  • 汇总本组研发人员各技术栈的分布和人力分布;
  • 了解各技术方案的基建完善程度和运维水平;
  • 结合以上要素,对待选技术方案进行多维度评估,其中明显不满足核心诉求的技术方案可以直接排除
  • 对评估结果进行总结,选择匹配度最高的技术方案

52.介绍一下webpack搭建项目

参考答案:blog.csdn.net/lxl743p/art…

53.讲一下webpack的构建过程

  1. 初始化参数:解析webpack配置参数,合并shell传入和webpack.config.js文件配置的参数,形成最后的配置结果。
  2. 开始编译:上一步得到的参数初始化compiler对象,注册所有配置的插件,插件监听webpack构建生命周期的事件节点,做出相应的反应,执行对象的 run 方法开始执行编译。
  3. 确定入口:从配置的entry入口,开始解析文件构建AST语法树,找出依赖,递归下去。
  4. 编译模块:递归中根据文件类型和loader配置,调用所有配置的loader对文件进行转换,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理。
  5. 完成模块编译:在经过第4步使⽤ Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系;
  6. 输出资源:根据⼊⼝和模块之间的依赖关系,组装成⼀个个包含多个模块的 Chunk,再把每个 Chunk 转换成⼀个单独的⽂件加⼊到输出列表,这步是可以修改输出内容的最后机会;
  7. 输出完成:在确定好输出内容后,根据配置确定输出的路径和⽂件名,把⽂件内容写⼊到⽂件系统。

54.webpack的chunkGroup分包规则

cloud.tencent.com/developer/a…

55.webpack异步加载实现原理是什么?

利用webpack的splitChunks,可以把构建好的代码分chunk,然后根据用户显示声明的异步加载,去使用异步加载的方式来加载chunk的文件。
详解:zhuanlan.zhihu.com/p/100459699

56.自己怎么实现post2css loader?(TODO)

57.cli工具,如何考虑给用户提供自定义的选项,并根据用户选项自动生成相应的文件?

cli开发参考:juejin.cn/post/712721…

58.express中间件原理

blog.csdn.net/chen__cheng…

59.webpack打包后的代码实际是怎么运行起来的?

segmentfault.com/a/119000002…

60.webpack中loader是怎样的执行顺序?

见 39.loader为什么是自右向左执行的

62.webpack拆包时,chunk、hash的区别

Chunk 则是输出产物的基本组织单位;
hash则是每个chunk生成的唯一标识。

63.讲一下项目开发流程

64.讲一下组件设计原则

组件聚合指导我们应该将哪些类组合成一个组件,要考虑三个原则:复用/发布等同原则共同闭包原则共同复用原则
组件耦合帮助我们确定组件之间的相互依赖关系,要考虑三个原则:无依赖环原则稳定依赖原则稳定抽象原则

详解:www.jianshu.com/p/42f8e6e62…

65.介绍下oauth

OAuth 是一种开放协议,允许通过桌面和 Web 应用程序以简单和标准的方法进行 API 身份验证。

OAuth 协议为用户资源的授权提供了一个安全的、开放而又简易的标准。与以往的授权方式不同之处是 OAUTH 的授权不会使第三方触及到用户的帐号信息(如用户名与密 码),即第三方无需使用用户的用户名与密码就可以申请获得该用户资源的授权,因此 OAUTH 是安全的。同时,任何第三方都可以使用 OAUTH 认证服务,任 何服务提供商都可以实现自身的 OAUTH 认证服务,因而 OAUTH 是开放的。业界提供了 OAUTH 的多种实现如 PHP,JavaScript,Java,Ruby 等各种语言开发包,大大节约了程序员的时间,因而 OAUTH 是简易的。

66.webpack作用是什么?

  • 代码转换
  • 文件优化
  • 代码分割
  • 模块合并
  • 自动刷新
  • 代码校验
  • 自动发布

67.webpack的打包流程/原理

  1. 初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数;
  2. 开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,通过执 行对象的 run 方法开始执行编译;
  3. 确定入口:根据配置中的 entry 找出所有入口文件;
  4. 编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块 依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理;
  5. 完成模块编译:在经过第 4 步使用 Loader 翻译完所有模块后,得到了每个模块被翻译 后的最终内容及它们之间的依赖关系;
  6. 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再 将每个 Chunk 转换成一个单独的文件加入输出列表中,这是可以修改输出内容的最后机会;
  7. 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,将文件的内容写 入文件系统中

68.说一下happypack的原理

webpack中最耗时的就是loader的转换过程,转换的流程很⻓。happypack的原理就是把这部分的任务拆解成多个子进程去并行处理,减少构建时间。

通过new HappyPack()实例化,然后把任务交由实例统一调度分配。核心调度器会将一个个 任务分配给空闲的子进程。处理完毕后发送给核心调度器。

69.