前端八股文之webpack

5,686 阅读36分钟

目录

  1. webpack中source map是什么?生产环境怎么用?
  2. 说下 webpack Runtime 和 Manifest 代码的作用?
  3. webpack 如果使用了 hash 命名,是否每次都会重新生成 hash? 简单说下 webpack 的几种 hash 策略?
  4. webpack 和 gulp 的优缺点
  5. 开发环境热更新的优化方式
  6. Prerender 预渲染是什么原理?
  7. 预渲染 prerender-spa-plugin 能详细讲解么?
  8. 说一下关于tree-shaking的原理
  9. webpack 里面的插件是如何实现的?
  10. webpack做了什么?使用webpack构建是有无做了一些自定义操作?
  11. dev-server是怎么跑起来的?
  12. webpack热更新的原理
  13. 说下在项目开发,你是怎样组织 CSS 的?
  14. webpack如何用localStorage离线缓存静态资源?
  15. 如何实现 webpack 持久化缓存?
  16. webpack打包时Hash码是怎样生成的?随机值存在一样的情况,如何避免?
  17. 脚手架具体做了哪些事情,webpack 具体做了什么配置?怎样优化包的大小?
  18. webpack打包出来的体积太大,如何优化体积?
  19. 使用import时,webpack对node_modules里的依赖会做什么?
  20. Import和CommonJs在webpack打包过程中有什么不同
  21. webpack的构建流程是什么?
  22. Loader和Plugin的区别是什么?
  23. 说一下 webpack 中 css-loader 和 style-loader 的区别,file-loader 和 url-loader 的区别?
  24. 常见的loader以及作用的总结
  25. 常见的plugin以及作用的总结

webpack中source map是什么?生产环境怎么用?

source map

  • source map是将编译、打包、压缩后的代码映射回源代码的过程。打包压缩后的代码不具备良好的可读性,想要调试源码就需要 soucre map。
  • map文件只要不打开开发者工具,浏览器是不会加载的

线上环境一般有三种处理方案

  • hidden-source-map:借助第三方错误监控平台Sentry使用
  • nosources-source-map:只会显示具体行数以及查看源代码的错误栈。安全性比source map高
  • source:通过nginx设置将.map文件只对白名单开放(公司内网)

注意的是:避免在生产中使用 inline- 和 eval- ,因为它们会增加bundle体积大小,并降低整体性能。


说下 webpack Runtime 和 Manifest 代码的作用?

Runtime && Manifest

主要是管理所有模块的交互。

Runtime

Runtime 主要是指在浏览器运行时,webpack 用来连接模块化的应用程序的所有代码。runtime 包含:在模块交互时,连接模块所需的加载和解析逻辑。包括浏览器中的已加载模块的连接,以及懒加载模块的执行逻辑。

Manifest

在代码经过编译打包之后,形成如 index.html 文件、一些 bundle 和各种资源加载到浏览器中,是不是 src 目录下的文件结构现在已经不存在了,那 webpack 如何管理所有模块之间的交互呢?这就是 manifest 数据的由来

当编译器开始执行,解析和映射应用程序时,它会保留所有模块的详细要点。这个数据集合成为 manifest,当完成打包并发送到浏览器时,会在运行时通过 manifest 来解析加载模块。无论选择哪种模块语法,那些 import 或 require 语句都已经转化为__webpack_require__方法,此方法指向模块标识符。通过使用 manifest 中的数据,runtime 将能够查询模块标识符,检索背后对应的模块。

总结

runtime:根据 manifest 数据管理模块代码。主要是指模块交互时,连接模块所需的加载和解析逻辑。包括:已经加载到浏览器中的连接模块逻辑,以及尚未加载模块的延迟加载逻辑

manifest:记录在打包过程中,各个模块之间的信息以及关联关系。


webpack 如果使用了 hash 命名,是否每次都会重新生成 hash? 简单说下 webpack 的几种 hash 策略?

webpack hash 分类

输出全部使用 hash 的情况
  • 每个文件都具有相同的 hash 值,因为 hash 是基于我们使用的所有源文件生成的。
  • 如果重新运行该构建而不更改任何内容,则生成的 hash 将保持不变。
  • 如果仅仅编辑一个文件,则 hash 值将会发生变化,并且所有生成捆绑的名称中都会包含此新的 hash 值
输出的结果全部使用 chunkhash 的情况
  • chunkhash 是根据不同的入口进行依赖文件解析,构建对应的 chunk,生成对应的 hash 值
  • 在使用上来说:我们把一些公共库和程序入口文件区分开来,单独打包构建,接着可以采用 chunkhash 方式来生成 hash 值,那么只要我们不改动公共库的代码,就可以保证其 hash 值不受影响,这样也起到缓存的作用。
输出的结果全部使用 contenthash 的情况
  • 每个生成的文件名称都有一个唯一的 hash 值,该哈希值是根据该文件的内容计算得出的。
  • 当要构建的文件内容发生改变的时候,就会生成新的 hash 值,且该文件的改变并不会影响和它同一个模块下的其他文件。

总结

所以如果使用 hash,并不是每次都会重新生成新的 hash,需要看具体使用的是哪种 hash 策略。

  • hash 是跟整个项目的构建相关,只要项目里有文件更改,整个项目构建的 hash 值都会更改,并且全部文件都共用相同的 hash 值。
  • chunkhash 是根据不同的入口进行依赖文件解析,构建对应的 chunk 生成相应的 hash 值。只有被修改过的 chunk 在重新构建之后才会生成新的 hash 值,不会影响其它的 chunk。
  • contenthash 是跟每个生成文件有关,每个文件都有一个唯一的 hash 值。当要构建的文件内容发生改变时,就会生成新的 hash 值,并且该文件的改变并不会影响和它同一模块下的其他文件。

webpack 和 gulp 的优缺点

两者同属于构建工具,但是侧重点不同

gulp 侧重于对开发流程的控制管理

优点:

  • 轻量,配置文件比较简单
  • 基于 nodeJS 强大的 stream 能力,构建速度快
  • 适合多页 web 应用以及 node 服务端应用

缺点:不太适合单页或自定义模块的开发

webpack 侧重于模块的打包

优点:

  • 任何资源都可以作为模块处理
  • 社区资源丰富,有很多插件和 loader

缺点:

  • 配置复杂
  • 不适合 node 服务端应用
  • 构建速度较慢,需要做很多性能优化

开发环境热更新的优化方式

其实就是优化开发环境打包时间。

在此之前,我们需要一个量化的指标证明我们这么做是有意义的。speed-measure-webpack-plugin可以测量各个插件和 loader 使用时间来量化指标。

  • 关闭文件名 hash 功能
  • 关闭压缩功能
  • 如果必须使用 source-map,选择 eval 或 eval-cheap-source-map 速度更快
  • 使用多线程:比如 thread-loader。另外对于有类型检查的语言,将类型检查与代码编译分到不同线程执行,虽然关闭检查可能更快,但是不建议这么做
  • 开启缓存:比如 cache-loader、hard-source-webpack-plugin 等

Prerender 预渲染是什么原理?

在 webpack 打包结束并生成文件后(after-emit hook),会启动一个 server 模拟网站的运行,用 puppeteer(google 官方的 headless 无头浏览器)访问指定的页面 route,得到相应的 html 结构,并将结果输出指定目录,过程类似爬虫。

即利用打包工具对应用进行预先渲染,让用户在首次获取到 html 文件的时候就已经能看到我们的内容,接着等待 Bundle 下载解析完成之后再进行接管。

打包构建预渲染的核心原理又是什么呢?

其实这里就要用到无头浏览器来帮助实现这项功能,他会在本地启动一个无头浏览器,并访问我们配置好的路由,接着将渲染好的页面 html 内容输出到我们的 html 文件中,并建立相关的目录

一般常用的无头浏览器 比如: phantomjs、puppeteer, 对于 prerender-spa-plugin 插件来说,它内部就是采用了 phantomjs 作为无头浏览器进行预渲染。

应用场景

  • seo 优化: 对于一些动态数据利用 renderAfterTime 也可以进行预渲染出来。当动态数据渲染出来之后,客户端代码比如 bundle.js 会马上接管 dom 操作,对于 spa 优化有便捷性
  • 骨架屏: 把骨架屏当做预渲染页面,当 ajax 获取到数据之后再把骨架屏替换掉; prerender-spa-plugin 提供了 postProcessHtml 钩子

预渲染不适用经常变化的数据,比如说股票代码网站,天气预报网站。因为此时的数据是动态的,而预渲染时已经生成好了 dom 节点。如果要兼容 seo 可以使用 SSR。

预渲染不适用大量的路由页面,比如成千上百个路由,此时打包后预渲染将会非常慢。

预渲染最好的应用场景是需要seo的活动页面

优点

  • seo: 对于搜索引擎爬虫来说(先排除高级爬虫),它不会等待你的JS执行完成之后才进行抓取,如果不进行预渲染,对于客户端渲染应用来说,HTML文件中几乎没有什么内容,故会影响你的搜索排名。采用预渲染就能保证在首次加载就能获取到相关的html内容,利于seo
  • 弱网环境:对于网络条件比较差的用户来说,你的bundle文件过大,会导致页面长时间白屏,这将使你白白流失很多用户,所以首次内容的快速呈现也是很重要的,解决白屏问题。

预渲染 prerender-spa-plugin 能详细讲解么?

直接使用该插件的时候可以配置需要预渲染的路由:

默认情况下html会在脚本执行完被捕获并输出,你也可以执行一些钩子,HTML将会在特定时机被捕获。

const path = require('path');
const PrerenderSpaPlugin = require('prerender-spa-plugin');
// TODO...
{
  plugins:[
    new PrerenderSpaPlugin({
      path.resolve(__dirname,'./dist'),
      ['/home','/foo'],
      {
        // 监听到自定事件时捕获
        captureAfterDocumentEvent: "custom-post-render-event",
        // 查询到指定元素时捕获
        captureAfterElementExists: "#content",
        // 定时捕获
        captureAfterTime: 5000
      }
    })
  ]
}

这样配置完之后我们就能在我们的dist目录中找到相关路由的预渲染HTML文件了

dist
  - index.html
  - home
    - index.html
  - foo
    - index.html

路由模式改成history模式

缺点

正是因为预渲染的构建是由打包工具在打包的时候就渲染出来了,所以如果不重新构建,那么用户所看到的预渲染页面永远都是一成不变的,即便你的页面数据早早更新,但是初次渲染的时候,浏览器展示的依旧是这套老旧的数据,如果想要看到最新的数据就需要等待js下载完毕重新渲染的时候才会出现,从而造成用户感觉很突兀的感觉

由于需要借助打包工具的力量,所以我们需要增加一些配置成本,不仅如此,在进行预渲染时,也同样会拉长打包的总时间,使我们每次构建的速度大大降低,这是十分糟糕的开发体验


说一下关于tree-shaking的原理

tree-shaking 是一种通过清除多余代码方式来优化项目打包体积的技术

Tree-shaking

当前端项目到达一定的规模后,我们一般会采用按模块方式组织代码,这样可以方便代码的组织及维护。但会存在一个问题,比如我们有一个utils工具类,在另一个模块中导入它。这会在打包的时候将utils中不必要的代码也打包,从而使得打包体积变大,这时候就需要用到Tree shaking技术了

原理

  • 利用ES6模块的特点

    • 只能作为模块顶层的语句出现
    • import的模块名只能是字符串常量,不能动态引入模块
    • import 引入的模块不能再进行修改的 虽然tree-shaking的概念在1990年就提出来了,但是直到ES6的ES6-style模块出现后才真正被利用起来。这是因为tree-shaking只能在静态模块下工作。ES6模块加载是静态的,因此在ES6种使用tree-shaking是非常容易地。而且,tree-shaking不仅支持import/export级别,而且也支持声明级别

在ES6以前,我们可以使用CommonJS引入模块:require(),这种引入是动态地,也意味着我们可以基于条件来导入需要的代码:

let mainModule;
//动态导入
if(condition){
    mainModule=require('dog')
}else{
    mainModule=require('cat')
}

CommonJS的动态特性意味着tree-shaking不适用。因为它是不可能确定哪些模块实际运行之前是需要的或者是不需要的。在ES6中,进入了完全静态的导入语法:import。

//不可行
if(condition){
    mainModule=require('dog')
}else{
    mainModule=require('cat')
}

只能通过导入所有的包后再进行条件获取

import dog from 'dog';
import cat from 'cat';
if(condition){
//dog.xxx
}else{
//cat.xxx
}

ES6的import语法可以使用tree-shaking,因为可以在代码不运行的情况下就能分析出不需要的代码

如何使用?

从webpack2开始支持实现了tree-shaking特性,webpack2正式版本内置支持ES6的模块(也叫harmony模块)和未引用模块检测能力。webpack4正式版本扩展了这个检测能力,通过package.jsonsideEffects属性作为标记,向complier提供提示,表明项目中哪些文件是ES6模块,由此可以安全地删除文件中未使用地部分 如果使用的是webpack4,只需要将mode设置为production,就可以开启tree-shaking

entry: './src/index.js',
mode:'production',
output:{
    path: path.resolve(__dirname,'dist'),
    filename: 'bundle.js'
},

如果使用webpack2,可能你会发现tree-shaking不起作用。因为babel会将代码编译成CommonJS模块,而tree-shaking不支持CommonJS,所以需要配置不转义

options: {
    presets: [
        [
            'es2015',
            {
                modules: false
            }
        ]
    ]
}

关于副作用

副作用是指那些当import的时候会执行一些动作,但是不一定会有任何export。比如ployfill,ployfills不对外暴露方法给主程序使用

tree-shaking不能自动识别哪些代码属于副作用,因此手动指定这些代码显得非常重要,如果不指定可能会出现一些意想不到的问题

在webpack中,是通过package.json的sideEffects属性来实现的

"name": "tree-shaking",
"sideEffects": false

如果所有的代码都不包含副作用,我们就可以简单地将该属性标记为false来告知webpack,它可以安全地删除未用到的export导出。

如果你的代码确实有一些副作用,那么可以改为提供一个数组:

"name": "tree-shaking",
"sideEffects": [
    "./src/public/polyfill.js"
]

总结

  • tree-shaking不会支持动态导入(如CommonJS的require()语法),只纯静态的导入(ES6的import/export)
  • webpack中可以在项目package.json文件中,添加一个"sideEffects"属性,手动指定副作用的脚本

webpack 里面的插件是如何实现的?

实现分析

  • webpack本质是一个事件流机制,核心模块:tapable(Sync + Async)Hooks 构造出 Compiler(编译) + Compilation(创建bundles)
  • compiler对象代表了完整的webpack环境配置。这个对象在启动webpack时被一次性建立,并配置好所有可操作的设置,包括options、loader和plugin。当在webpack环境中应用一个插件时,插件将收到此compiler对象的引用。可以使用它来访问webpack的主环境
  • compilation对象代表了一次资源版本构建。当运行webpack开发环境中间件时,每当检测到一个文件变化,就会创建一个新的compilation,从而生成一个新的编译资源。一个compilation对象表现了当前的模块资源、编译生成资源、变化的文件、以及被跟踪依赖的状态的信息。compilation对象也提供了很多关键时机的回调,以供插件做自定义处理时选择使用
  • 创建一个插件函数,在其prototype上定义apply方法,指定一个webpack自身的事件钩子
  • 函数内部处理webpack内部实例的特定数据
  • 处理完成后,调用webpack提供的回调函数
function MyWebpackPlugin(){
    //
};
// prototype上定义apply方法
MyWebpackPlugin.prototype.apply = function(){
    // 指定一个事件函数挂载到webpack
    compiler.plugin("webpacksEventHook", funcion (compiler){
        console.log("这是一个插件");
        //功能完成调用后webpack提供的回调函数
        callback()
    })
}

webpack做了什么?使用webpack构建是有无做了一些自定义操作?

webpack做了什么?

  1. webpack本质上只是一个js引用程序的静态打包器,它能够基于文件的依赖,递归的构建一个文件依赖关系图,最终将文件打包称为一个或多个bundle;
  2. webpack基于entry识别哪个/哪些模块是构建打包的入口
  3. webpack基于output,将构建打包的文件输出到指定的目录
  4. 从入口文件出发,调用所有配置的Loader对模块进行翻译,再找出该模块依赖的模块,再递归本步骤知道所有入口依赖的文件都经过了本步骤的处理
  5. 经过Loader翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系。根据入口和模块之间的依赖关系,组装成一个个包含多个模块的chunk,再把每个chunk转换成一个单独的文件加入到输出列表

webpack构建时有无做一些自定义操作

  1. alias:指定别名,能在一定程度上降低开发对文件路径的输入难度,缓存路径能提升些打包速度
  2. module对loader处理添加fallback,能在loader处理中,依次进行指定的预处理或后处理,自定义loader组件也可以在此进行特殊替换
  3. optimization-splitChunks-cacheGroups自定义打包中的性能优化部分,对共用模块的拆分、识别以及提取后的指定部分
  4. 自定义的plugins配置
    • CopyWebpackPlugin对静态文件的拷贝
    • ProgressBarPlugin打包进度的监控
    • HappyPack多线程打包等等
  5. stats调整打包过程中控制台的输出,详细到每个文件的大小、耗时及打包状态等各种显示优化
  6. devServer-before: 添加打包前的优化,可以实现较为简洁的mock数据

dev-server是怎么跑起来的?

dev-server运行配置

  • 安装webpack-dev-server的npm包
  • 在webpack.config.js进行配置

devServer中常用的配置对象属性如下

  1. contentBase: "./" 本地服务器在哪个目录搭建页面,一般在当前目录即可
  2. historyApiFallback:true 搭建spa应用时会用到。它使用的时HTML5 History Api,任意的跳转或404响应可以指向index.html页面
  3. inline:true 用来支持dev-server自动刷新的配置,webpack有两种模式支持自动刷新,一种是iframe模式,一种是inline模式;使用iframe模式是不需要在devServer进行配置的。只需使用特定的URL格式访问即可;不过我们一般还是常用inline模式,在devServer中对inline设置为true后,当启动webpack-dev-server时仍需要配置inline才能生效
  4. hot: true 启动webpack热模块替换特性
  5. port 端口号(默认8080)

怎么跑起来的

  1. 启动HTTP服务
  2. webpack构建时输出Bundle到内存,HTTP服务从内存中读取Bundle文件
  3. 监听文件变化,重新执行第二个步骤

dev-server 实际上是一个HTTP服务器,所以还可以做静态资源的访问和API的Proxy代码

静态资源访问

{
    devServer:{
        contentBase:'public'
    }
}

Proxy代理

{
    devServer:{
        proxy:{
            '/api':{
                target:'http://api.target.com'
            }
        }
    }
}

webpack热更新的原理

基础概念

  1. webpack compiler:将js编译成Bundle

  2. Bundle Server:提供文件在浏览器的访问,实际上就是一个服务器

  3. HMR Server:将热更新的文件输出给HMR Runtime

  4. HMR Runtime:会注入到bundle.js中,与HRM Server通过webSocket链接,接收文件变化,并更新对应文件

  5. bundle.js:构建输出的文件

原理

1.启动阶段

  • webpack Compiler将对应文件打包成bundle.js(包含注入的HMR Server),发送给Bundler Server
  • 浏览器即可访问服务器的方式去获取bundle.js

2.更新阶段(文件发生变化)

  • webpack compiler重新编译,发送给HMR Server
  • HMR Server可以知道有哪些资源、哪些模块发生了变化,通知HMR Runtime
  • HRM Runtime更新代码

HMR原理详解

使用webpack-dev-server去启动本地服务,内部实现只要使用了webpack、express、websocket

  • 使用express启动本地服务,当浏览器访问资源时对此响应

  • 服务端和客户端使用websocket实现长连接

  • webpack监听源文件的变化,即当开发者保存文件时触发webpack的重新编译

    • 每次编译都会生成hash值,已改动模块的json文件、已改动模块代码的js文件
    • 编译完成后通过socket向客户端推送当前编译的hash戳
  • 客户端的websocket监听到有文件改动推送过来的hash戳,会和上一次对比

    • 一致就走缓存
    • 不一致就通过ajax和jsonp向服务端获取最新资源
  • 使用内存文件系统去替换有修改的内容实现局部刷新

server端

  • 启动webpack-dev-server服务器
  • 创建webpack实例
  • 创建server服务器
  • 添加webpack的done事件回调
  • 编译完成向客户端发送消息
  • 创建express应用app
  • 设置文件系统为内存文件系统
  • 添加webpack-dev-middleware中间件
  • 中间件负责返回生成的文件
  • 启动webpack编译
  • 创建http服务器并启动服务
  • 使用sockjs在浏览器端和服务端之间建立一个websocket长连接
  • 创建socket服务器

client端

  • webpack-dev-server/client端会监听到此hash消息
  • 客户端收到ok消息后会执行reloadApp方法进行更新
  • 在reloadApp中会进行判断,是否支持热更新,如果支持的话发生webpackHotUpdate事件,如果不支持就直接刷新浏览器
  • 在webpack/hot/dev-server.js会监听webpackHotUpdate事件
  • 在check方法里会调用module.hot.check方法
  • HotModuleReplacement.runtime请求Manifest
  • 通过调用JsonpMainTemplate.runtime的hotDownloadManifest方法
  • 调用JsonpMainTemplate.runtime的hotDownloadUpdateChunk方法通过JSONP请求获取最新的模块代码
  • 补丁js取回来或会调用JsonpMainTemplate.runtime.js的webpackHotUpdate方法
  • 然后会调用HotModuleReplacement.runtime.js的hotAddUpdateChunk方法动态更新模块代码
  • 然后调用hotApply方法进行热更新

说下在项目开发,你是怎样组织 CSS 的?

在我们日常使用的 css 组织来说,css-loader、style-loader 是必不可少的。那一般如何组织 css 呢?

cssModule

css-loader 中配置 module 可以开启模块化,防止类名污染,实现 css 局域化 缺点:生成的 className 语义化降低

postCss

兼容性强以及写法前置,集成了 css-next 以及 autoprefix、css in js、css module 等众多特点,可选配置,人性化

less\sass\stylus

css 预处理器 增加编程的特性,无需考虑兼容的问题,可以使用变量、函数等,不过 postCss 都支持。


webpack如何用localStorage离线缓存静态资源?

  1. 在配置webpack时,我们可以使用html-webpack-plugin来注入到和html一段脚本来实现将第三方或者共用资源进行静态化存储在html中注入一段标识,例如<% HtmlWebpackPlugin.options.loading.html %>,在html-webpack-plugin中即可通过配置html属性,将script注入进去

  2. 利用webpack-manifest-plugin并通过配置webpack-manifest-plugin,生成manifest.json文件,用来对比js资源的差异,做到是否替换,当然,也要写缓存script

  3. 在我们做CI以及CD的时候,也可以通过编辑文件流来实现静态化脚本的注入,来降低服务器的压力,提高性能

  4. 可以通过自定义plugin或者html-webpack-plugin等周期函数,动态注入前端静态化存储script


如何实现 webpack 持久化缓存?

1、强缓存

服务端设置HTTP缓存头(Cache-Control等)

2、依赖和运行时单独打包

打包依赖(dependencies)和运行时(runtime)到不同chunk(在webpack中,编译后的单独文件称为chunk),即作splitChunk,因为它们几乎是不变的

3、分包加载(延迟加载)

使用import()方式,可以动态加载的文件分到独立的chunk,以得到自己的chunkhash

4、保证hash值稳定

编译过程和文件内容的更改尽量不影响其他文件hash的计算。对于低版本webpack生成的增量数字ID不稳定问题,可用HashedModuleldsPlugin基于文件路径生成解决


webpack打包时Hash码是怎样生成的?随机值存在一样的情况,如何避免?

  • hash 代表每次webpack编译中生成的hash值,所有使用这种方式的文件hash都相同。每次构建都会使webpack计算新的hash。 

  • chunkhash基于入口文件及其关联的chunk生成,某个文件的改动只会影响与它有关联的chunk的hash值,不会影响其他文件 

  • contenthash根据文件内容创建。当文件内容发生变化时,contenthash发生变化

避免相同随机值

webpack在计算hash后分割chunk。

产生相同随机值可能是因为这些文件属于同一个chunk,可以将某一个文件提到独立的chunk(如放入entry)


脚手架具体做了哪些事情,webpack 具体做了什么配置?怎样优化包的大小?

脚手架具体做的事情

  • 创建 vue 项目 demo
  • 搭建 webpack 基本配置
  • 搭建 babelrc 基本配置
  • 通过设置决定是否帮助搭建 vue-router(路由)
  • 是否帮助配置 eslint
  • 是否帮助配置 css 预处理器
  • 是否帮助配置单元测试工具 Karma+Mocha
  • 是否帮助安装 e2e 来进行用户行为模拟测试

相当于帮助你完成项目的基础搭建

webpack 具体做了哪些配置?

  • 热更新
  • 代理
  • 转译
  • 压缩打包
  • 自动上传

开启多核压缩 terser-webpack-plugin

const TerserPlugin = require("terser-webpack-plugin");
module.exports = {
  optimization: {
    minimizer: [
      new TerserPlugin({
        parallel: true,
        terserOptions: {
          ecma: 6,
        },
      }),
    ],
  },
};

监控面板 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));

开启一个通知面板 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,
    }),
  ],
};

开启打包进度 progress-bar-webpack-plugin

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

开发面板更清晰 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",
  },
}

开启窗口的标题 node-bash-title

这个包 Mac 的 item 应用有效果,window 暂时没有效果

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

配置打印的信息 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: []
}),

webpack打包出来的体积太大,如何优化体积?

借助工具分析性能瓶颈

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

按需加载

  • 路由组件按需加载
  • 第三方组件和插件。按需加载需引入第三方组件
  • 对于一些插件,如果只是在个别组件中用的到,也可以不要在 main.js 里面引入,而是在组件中按需引入

生产环境关闭 sourceMap

代码压缩

  • UglifyJS: vue-cli 默认使用的压缩代码方式,它使用的是单线程压缩代码,打包时间较慢
  • ParallelUglifyPlugin: 开启多个子进程,把对多个文件压缩的工作分别给多个子进程去完成

CDN 优化

  • 随着项目越做越大,依赖的第三方 npm 包越来越多,构建之后的文件也会越来越大。
  • 再加上又是单页应用,这就会导致在网速较慢或者服务器带宽有限的情况出现长时间的白屏。

压缩代码

webpack-paralle-uglify-plugin

uglifyjs-webpack-plugin 开启parallel参数(不支持ES6)

terser-webpack-plugin 开启paraller参数

多进程并行压缩

通过 mini-css-extract-plugin 提取Chunk中的CSS代码到单独文件

通过 optimize-css-assets-webpack-plugin 插件,开启cssnano 压缩 css

提取页面公共资源

使用 html-webpack-externals-plugin,将基础包通过CDN引入,不打入bundle中

使用SplitChunksPlugin进行(公共脚本、基础包、页面公共文件)分离(webpack4内置) 替代了 CommonsChunkPlugin 插件

基础包分离:将一些基础库放到cdn,比如vue、webpack配置 external 是vue的不打入bundle

Tree shaking

禁用babel-loader的模块依赖解析,否则 webpack 接收到的就是转换过的commonJS形式的模块,无法进行tree shaking

purgecss-webpack-plugin 和 mini-css-extract-plugin 配合使用(仅仅是建议)

打包过程中检测工程中没有引用过的模块并进行标记,在资源压缩时将它们从最终的bundle中去掉(只能对ES6 Module生效)开发中尽可能使用 ES6 Module 的模块,提高tree shaking的效率

使用 PurifyCSS(不在维护)或者uncss去除无用css代码

Scope hoisting

构建后的代码会存在大量闭包,造成体积增大,运行代码时创建的函数作用域变多,内存开销变大。

Scope hoisting 将所有模块的代码按照引用顺序放在一个函数作用域里,然后适当的重命名一些变量以防止变量名冲突

必须是ES6的语法,因为有很多第三方库仍采用CommonJS 语法,为了充分发挥Scope hoisting 的作用,需要配置 mainFields 对第三方模块优先采用 jsnext:main 中指向的ES6模块化语法

图片压缩

使用基于node库的imagemin(很多定制选项、可以处理多种图片格式)

配置 image-webpack-loader

动态Polyfill

建议采用 polyfill-service 只给用户返回需要的polyfill,社区维护。

@babel-preset-env中通过useBuiltIns:"usage" 参数来动态加载polyfill


使用import时,webpack对node_modules里的依赖会做什么?

ES6 Module

不同于CommonJS和AMD的模块加载方法,ES6在JS语言层面上实现了模块功能。

它的设计思想是:尽量的静态化,使得编译时就能确定模块的依赖关系。

在遇到模块加载命令import时,不会去执行模块,而是只生成一个引用。等到真的需要用到的时候,再到模块里面去取值。这和CommonJS模块规范的最大不同。

webpack会对node_modules里的依赖做什么

webpack会根据定义的引入方式判断模块类型,再进行相关编译转化。

当使用import引入时,babel默认会把ES6的模块转化成CommonJS规范,然后会将node_module里的依赖打包成自执行函数的样式

  • 判断引入方式(import 或 require)
  • 编译转化
(function(modules){
    //执行逻辑
})([模块数组])

在模块数组中将模块传入,在函数体中经过一系列操作最终将模块通过 module.exports 导出


Import和CommonJs在webpack打包过程中有什么不同

1.es6模块调用commonjs模块

可以直接使用commonjs模块,commonjs模块将不会被webpack的模块系统编译而是原样输出,并且commonjs模块没有default属性

2.es6模块调用es6模块

被调用的es6模块不会添加{esModule:true},只有调用者才会添加{ esModule:true},并且可以进行tree-shaking操作,如果被调用的es6模块只是import进来,但是并没有被用到,那么被调用的es6模块将会被标记为/* unused harmony default export */,在压缩时此模块将被删除(如果被调用的es6模块里有立即执行语句,那么这些语句将会被保留)

3.commonjs模块引用es6模块

es6模块编译后会添加{__esModule:true}。如果被调用的es6模块中恰好有export default语句,那么编译后的es6模块将会添加default属性

4.commonjs模块调用commonjs模块

commonjs模块会原样输出


webpack的构建流程是什么?

简洁版本

webpack的运行流程是一个串行的过程,从启动到结束会依次执行以下流程

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

> 在以上系统中,webpack会在特定的时间点广播出特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用webpack提供的API改变webpack的运行结果

webpack是一个打包模块化javascript的工具,它将一切文件都看作是模块。通过loader编译转换文件,通过plugin注入钩子,最后输出的资源模块组合成文件。它的主要配置有entry,output,modules,plugins,loader.

基本概念:

  1. compiler:webpack的运行入口,实例化时定义webpack构建主要流程,同时创建时使用的核心对象compilation;

  2. compilation:由compiler实例化,存储构建过程中使用的数据,用户监控这些数据的变化,每次构建创建一个compilation实例;

  3. chunk: 一般一个入口对应一个chunk;

  4. Module: 用于标示代码模块的类型,有很多子类用于处理不同情况的模块,模块相关信息都可以从Module实例中获取,例如dependiencies记录模块的依赖信息;

  5. Parser: 基于acorn来分析AST语法树,解析出代码模块的依赖

  6. Dependency: 解析用于保存代码块对应的依赖使用对象

  7. template: 生成最终代码要用到的代码模块

基本流程:

  1. 创建complier实例,用于控制构建流程,complier实例包含了webpack基本环境信息

  2. 根据配置项(webpack.config.js + shell脚本参数)转换成对应的内部插件,并初始化options配置项

  3. 执行compiler.run

  4. 创建complitation实例,每次构建都会创建一个compilation实例,包含了这次构建的基本信息;

  5. 从entery开始递归分析依赖,对每个模块进行buildmodule,通过loader将不同的类型的模块转化成webpack模块

  6. 调用parser.parse将上面的结构转化成AST树,

  7. 遍历整个AST树,搜集依赖dependency,并保存在compilation的实例中

  8. 生成chunks,不同的entry,生成不同的chunks,动态导入也会生成自己的chunks,待到生成chunks后再继续优化

  9. 使用template基于compilation的数据生成结果代码

编译过程:

  • 第一步先初始化参数,通过args将webpack.config.js和shell脚本的配置信息合并,并且初始化。

  • 第二步利用初始化的参数创建complier对象,complier可以视作为一个webpack的实例。存在于webpack从启动到结束的整个过程,它包含了webpack的module,plugin等参数信息,然后调用complier.run方法开始编译。

  • 第三步根据entry配置信息找到入口文件,创建compilation对象,可以理解为webpack的一次构建编译过程。包含了当前编译环境的所有资源。包括编译后的资源。

  • 第四步通过配置信息,调用loader进行模块编译,使用acorn将模块转化为AST树,当遇到require模块依赖时,创建依赖并加入到依赖数组,再找出依赖的依赖,递归异步的处理所有的依赖项。

  • 第五步得到所有模块的依赖关系和模块翻译之后的文件后,然后调用compilation.seal方法,对这些模块和根据模块依赖关系创建chunks进行整理,将所有资源进行合并拆分等操作,最后一次性修改输出内容的地方。

  • 第六步根据配置信息中的output配置进行最后模块文件的输出,指定输出文件名和文件路径。

原理:

webpack打包输出后的文件其实就是一个闭包,传入的参数是一个对象,键值为所有输出文件的路径,内容为eval包裹的文件内容。闭包内重写了模块的加载方式,自己定义了webpack_require方法,来实现commonjs规范模块的加载机制。

webpack实际上是基于时间流,通过一系列的插件来运行,webpack利用tapable库提供的各种钩子来实现对于整个构建流程各个步骤的控制。


Loader和Plugin的区别是什么?

1. 作用不同:

  • Loader直译为"加载器"。Webpack将一切文件视为模块,但是webpack原生是只能解析js文件,如果想将其他文件也打包的话,就会用到loader。 所以Loader的作用是让webpack拥有了加载和解析非JavaScript文件的能力。

  • Plugin直译为"插件"。Plugin可以扩展webpack的功能,让webpack具有更多的灵活性。 在 Webpack 运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果。

2. 用法不同:

  • Loader在module.rules中配置,也就是说作为模块的解析规则而存在。类型为数组,每一项都是一个Object,里面描述了对于什么类型的文件(test),使用什么加载(loader)和使用的参数(options)
  • Plugin在plugins中单独配置。类型为数组,每一项是一个plugin的实例,参数都通过构造函数传入。

说一下 webpack 中 css-loader 和 style-loader 的区别,file-loader 和 url-loader 的区别?

  • css-loader:处理 css 文件
  • style-loader:把 js 中 import 导入的样式文件代码,打包到 js 文件中,运行 js 文件时,将样式自动插入到<style></style>标签中
  • file-loader:返回的是图片的 url
  • url-loader:可以通过 limit 属性对图片分情况处理,当图片小于 limit(单位为 byte)大小时转 base64,大于 limit 时调用 file-loader 对图片进行处理

url-loader 封装了 file-loader,但 url-loader 并不依赖 file-loader


常见的loader以及作用的总结

  • raw-loader:加载文件原始内容(utf-8)
  • file-loader:把文件输出到一个文件夹中,在代码中通过相对URL去引用输出的文件
  • url-loader: 和file-loader类似,但是能在文件很小的情况下以base64的方式把文件内容注入到代码中
  • source-map-loader: 加载额外的Source Map文件,以方便断点调试
  • svg-inline-loader:将压缩后的 SVG 内容注入代码中
  • image-loader:加载并且压缩图片文件
  • json-loader 加载 JSON 文件(默认包含)
  • handlebars-loader: 将 Handlebars 模版编译成函数并返回
  • babel-loader:把ES6转化成ES5
  • ts-loader: 将 TypeScript 转换成 JavaScript
  • awesome-typescript-loader:将 TypeScript 转换成 JavaScript,性能优于 ts-loader
  • css-loader:加载css,支持模块化、压缩、文件导入等特性
  • style-loader:把css代码注入到js中,通过DOM操作去加载css
  • eslint-loader:通过ESLint检查JS代码
  • tslint-loader:通过 TSLint检查 TypeScript 代码
  • postcss-loader:扩展 CSS 语法,使用下一代 CSS,可以配合 autoprefixer 插件自动补齐 CSS3 前缀
  • vue-loader:加载 Vue.js 单文件组件
  • cache-loader: 可以在一些性能开销较大的 Loader 之前添加,目的是将结果缓存到磁盘里

常见的plugin以及作用的总结

  • define-plugin:定义环境变量(Webpack4 之后指定 mode 会自动配置)

  • ignore-plugin:忽略部分文件

  • commons-chunk-plugin:提取公共代码

  • html-webpack-plugin:简化 HTML 文件创建 (依赖于 html-loader)

  • web-webpack-plugin:可方便地为单页应用输出 HTML,比 html-webpack-plugin 好用

  • uglifyjs-webpack-plugin:不支持 ES6 压缩 (Webpack4 以前)

  • terser-webpack-plugin: 支持压缩 ES6 (Webpack4)

  • mini-css-extract-plugin: 分离样式文件,CSS 提取为独立文件,支持按需加载 (替代extract-text-webpack-plugin)

  • webpack-parallel-uglify-plugin: 多进程执行代码压缩,提升构建速度

  • serviceworker-webpack-plugin:为网页应用增加离线缓存功能

  • clean-webpack-plugin: 目录清理

  • ModuleConcatenationPlugin: 开启 Scope Hoisting

  • speed-measure-webpack-plugin: 可以看到每个 Loader 和 Plugin 执行耗时 (整个打包耗时、每个 Plugin 和 Loader 耗时)

  • webpack-bundle-analyzer: 可视化 Webpack 输出文件的体积 (业务组件、依赖第三方模块)