前端工程化:webpack

554 阅读7分钟

webpack四个核心概念:

入口(entry),输出 (output),loader,插件(plugins)

1.webpack工作流程

  • 参数解析
  • 找到入口文件
  • 调用Loader编译文件
  • 遍历AST,收集依赖
  • 生成Chunk
  • 输出文件

2.loader的配置和使用

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

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

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

当use是通过数组形式声明Loader时,Loader的执行顺序是从右到左,从下到上,

potscss-loader -> css-loader -> style-loader 

styleLoader(cssLoader(postcssLoader(content)))

2.1内联引入

可以在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")

2.2loader类型

同步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)
    })
}

piching loader 

{
  test: /\.js$/,
  use: [
    { loader: 'aa-loader' },
    { loader: 'bb-loader' },
    { loader: 'cc-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方法,再按照从右向左的顺序执行其实际方法

2.3 raw loader

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

export const raw = true 

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

2.4 loader几个重要的api

所谓loader,也只是一个符合commonjs规范的node模块,它会导出一个可执行函数。loader runner会调用这个函数,将文件的内容或者上一个loader处理的结果传递进去。同时,webpack还为loader提供了一个上下文this

2.4.1 this.callback()

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

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

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

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

2.4.2 this.async()

同上异步loader

2.4.3 this.cacheable()

3. webpack常用的三种JS压缩插件

UglifyJSwebpack-parallel-uglify-plugin,terser-webpack-plugin

3.1 UglifyJS

支持: babel、present2015、webpack3

缺点:它使用的是单线程压缩代码,也就是说多个js文件需要被压缩,它需要一个个文件进行压缩。所以在正式环境打包压缩代码速度非常慢(因为压缩JS代码需要先把代码解析成用Object抽象表示的AST语法树,再去应用各种规则分析和处理AST,导致这个过程耗时非常大)

优点:老项目支持(兼容IOS10)

3.2 webpack-parallel-uglify-plugin

支持: babel7、webpack4

缺点:老项目不支持(不兼容IOS10)

优点:parallelUglifyPlugin插件则会开启多个子进程,把多个文件压缩的工作分给多个子进程去完成,但是每个子进程还是通过UglifyJS去压缩代码。无非就是变成了并行处理该压缩了,并行处理多个子任务,效率会更加的高。

3.3 terser-webpack-plugin

支持:babel7、webpack4

缺点:老项目不支持(不兼容IOS10)

有点:和ParallelUglifyPlugin一样,并行处理多个子任务,效率会更高。webpack4官方推荐,有人维护。

4. webpack面试题

  1. 说一说 loader和 plugin 的区别
  2. webpack 构建流程是怎样的
  3. 编写 webpack loader 的思路
  4. 编写 webpack plugin 的思路

webpack打包原理:

1. 识别入口文件

2. 通过逐层识别模块依赖(Commonjs、amd或者es6的import,webpack都会对其进行分析,来获取代码的依赖)

3. webpack做的就是分析代码,转换代码,编译代码,输出代码

4. 最终形成打包后的代码

什么是loader:

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

1. 处理一个文件可以使用多个loader,loader的执行顺序和配置中的顺序是相反的,即最后一个loader最先执行,第一个loader最后执行

2. 第一个执行的loader接受源文件内容作为参数,其他loader接受前一个执行的loader的返回值作为参数,最后执行的loader会返回此模块的JavaScript源码

什么是plugin

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

loader和plugin的区别

对于loader,它是一个转换器,将A文件进行编译形成B文件,这里操作的是文件,比如将A.less转换为A.css,单纯的将文件转换过程

plugin是一个扩展器,它丰富了webpack本身,针对是loader结束后,webpack打包的整个过程,它并不直接操作文件,而是基于事件机制工作,会监听webpack打包过程中的某些节点,执行广泛的任务。

如何自定义webpack plugins:

1. javascript 命名函数

2. 在插件函数prototype上定义一个apply方法

3. 定义一个绑定到webpack自身的hook

4. 处理webpack内部特定数据

5. 功能完成后调用webpack提供的回调

5.webpack优化

5.1缓存

大部分loader都提供了cache配置项,可以通过设置cacheDirectory来开启缓存或者使用cache-loader

module.exports = {
    module:{
        rules:[
            {
                test:/\.ext$/,
                use:['cache-loader',...loaders],
                include:path.resolve('src')
            },
            {
                test:/\.js$/,
                loader:require.resolve('bable-loader'),
                options:{
                    cacheDirectory:true,
                }
            }
        ]
    }
}

5.2多核

happypack 开启多核线程

const HappyPack = require('happypack')
const os = require('os')
// 开辟一个线程池
// 拿到系统CPU的最大核数,happypack 将编译工作灌满所有线程
const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length })

module.exports = {
  module: {
    rules: [
      {
        test: /\.(js|jsx)$/,
        exclude: /node_modules/,
        use: 'happypack/loader?id=js',
      },
    ],
  },
  plugins: [
    new HappyPack({
      id: 'js',
      threadPool: happyThreadPool,
      loaders: [
        {
          loader: 'babel-loader',
        },
      ],
    }),
  ],
}

5.3抽离

常用的静态依赖,或者工具库,使用Externals的方式使用CDN的方式引用他们

module.exports = {
  ...,
  externals: {
    // key是我们 import 的包名,value 是CDN为我们提供的全局变量名
    // 所以最后 webpack 会把一个静态资源编译成:module.export.react = window.React
    "react": "React",
    "react-dom": "ReactDOM",
    "redux": "Redux",
    "react-router-dom": "ReactRouterDOM"
  }
}

6. tree-shaking

tree-shaking的技术是因为ES6Module是一种可以做静态分析的模块机制,当前主流的tree-shaking技术依赖于ES6中的Import和export模块机制,打包器会检测代码中的模块时候被导出、导入,且被JavaScript文件使用。

// 导入并赋值给 JavaScript 对象,然后在下面的代码中被用到
// 这会被看作“活”代码,不会做 tree-shaking
import Stuff from './stuff';
doSomething(Stuff);
// 导入并赋值给 JavaScript 对象,但在接下来的代码里没有用到
// 这就会被当做“死”代码,会被 tree-shaking
import Stuff from './stuff';
doSomething();
// 导入但没有赋值给 JavaScript 对象,也没有在代码里用到
// 这会被当做“死”代码,会被 tree-shaking
import './stuff';
doSomething();
//// 导入整个库,但是没有赋值给 JavaScript 对象,也没有在代码里用到
//// 非常奇怪,这竟然被当做“活”代码,因为 Webpack 对库的导入和本地代码导入的处理方式不同。
import 'my-lib';
doSomething();

用支持tree-shaking的方式写import

在编写支持tree-shaking的代码时,导入方式非常重要,避免将整个库导入到单个JavaScript对象中,当这样做时,webpack会认为你需要整个库,这个时候,webpack就不会摇它

比如:

// 全部导入 (不支持 tree-shaking)
import _ from 'lodash';

// 具名导入(支持 tree-shaking)
import { debounce } from 'lodash';

// 直接导入具体的模块 (支持 tree-shaking)
import debounce from 'lodash/lib/debounce';

基本的webpack配置

使用webpack进行tree-shaking第一步是编写webpack配置文件,

1.处于生产模式,webpack只有在压缩代码时才会tree-shaking

2.usedExports设置为true,

3.支持删除死代码的压缩器 terser-webpack-plugin,terserPlugin

// Base Webpack Config for Tree Shaking
const config = {
 mode: 'production',
 optimization: {
  usedExports: true,
  minimizer: [
   new TerserPlugin({...})
  ]
 }
};

副作用

全局样式表,或者全局配置的JavaScript文件,webpack认为这样的文件有副作用,具有副作用的文件不应该被tree-shaking。

但是我们可以配置我们的项目,告诉webpack它是没有副作用的,可以进行tree-shaking

如何告诉webpack你的代码无副作用

package.json有一个特殊的属性sideEffects,就是为此而存在的,它有三个值:

1.true是默认值,这意味着所有的文件都有副作用,也就是没有一个可以tree-shaking

2.false,所有的文件都可以进行tree-shaking

3.[...]文件路径数组,告诉webpack除了数组中的文件,其他的文件都可以进行tree-shaking

全局CSS与副作用

全局CSS直接导入到JavaScript文件中的样式表

import './myStyleSheet.css'

因此,如果你做了副作用的修改,那么在运行webpack构建时,导入的样式将会被删除。

解决方案:

webpack使用它的模块规则系统来控制各种类型文件的加载。每种文件类型的每个规则都有自己的sideEffects标志。这会覆盖之前为匹配规则的文件设置的所有sideEffects标志

// 全局 CSS 副作用规则相关的 Webpack 配置
const config = {
 module: {
  rules: [
   {
    test: /regex/,
    use: [loaders],
    sideEffects: true
   }
  ]
 } 
};

webpack所有模块规则上都有这个属性,处理全局样式表的规则必须用上它。

什么是模块,模块为什么重要

多年来,javaScript已经发展出在文件之间以模块的方式有效导入/到处代码的能力。

// Commonjs
const stuff = require('./stuff');
module.exports = stuff;

// es2015 
import stuff from './stuff';
export default stuff;

默认情况下,babel假定我们使用的es2015模块编写代码,并转换成commonJS模块,这样做是为了与服务器端JavaScript库的广泛兼容,JavaScript库通常构建在nodejs之上,但是webpack不支持使用commonJS模块来完成tree-shaking

为了进行tree-shaking我们需要将代码编译到es2015模块

es2015模块Babel配置

// es2015 模块的基本 Babel 配置
const config = {
 presets: [
  [
   '[@babel/preset-env](http://twitter.com/babel/preset-env)',
   {
    modules: false
   }
  ]
 ]
};

把modules设置为false,就是告诉Babel不要编译模块代码,这会让Babel保留我们现有的es215 import/export语句

**划重点:**所有可需要 tree-shaking 的代码必须以这种方式编译。因此,如果你有要导入的库,则必须将这些库编译为 es2015 模块以便进行 tree-shaking 。如果它们被编译为 commonjs,那么它们就不能做 tree-shaking ,并且将会被打包进你的应用程序中。许多库支持部分导入,lodash 就是一个很好的例子,它本身是 commonjs 模块,但是它有一个 lodash-es 版本,用的是 es2015模块。

此外,如果你在应用程序中使用内部库,也必须使用 es2015 模块编译。为了减少应用程序包的大小,必须将所有这些内部库修改为以这种方式编译。

参考

7.code spliting

主要是将逻辑代码和第三方代码进行拆分,

自动化分离vendor需要引入minChunks,在配置中我们就可以对所有node_module下所引用的模块进行打包,

new webpack.optimize.CommonsChunkPlugin({
    name:"vendor",
    minChunks:({resource})=>(
        resource && resource.indexOf('node_modules')>=0 && resource.match(/\.js$/)
    )
})

// 使用async异步加载的就再加上这个优化
new webpack.optimize.CommonsChunkPlugin({
      async: 'used-twice',
      minChunks: (module, count) => (
        count >= 2
      ),
    }),

如果第三方库使用的是异步加载,就还是会导致重复加载第三方库

8.babel编译原理

  • babylon 将ES6/ES7代码解析成AST
  • babel-traverse对AST进行遍历转译,得到新的AST
  • 新的AST通过babel-generator转换成ES5

9.webpack loader为什么是从右至左运行

js函数组合是函数式编程中非常重要的思想,有两种函数组合的实现方式,一种是pipe,一种是compose,前者从左向右组合函数,后者方向反之。

例如:let compose = (f,g)=>(...args)=>f(g(...args));

在webpack loader中就运用了compose的方式运行的。

10. css,js处理

css 处理 4步走

css less/scss...代码处理=> css => css兼容 => 代码提取到单独的css文件=> css文件压缩

module.exports = {
    module:{
        rules:[
            {
                test:/\.css$/,
                use:[
                        MiniCssExtractPlugin.loader, // 将js文件中css抽出打包到一个css文件中
                        'css-loader',
                        {
                            loader:'postcss-loader',  // css 兼容性处理在package.json对浏览器版本browserslist做配置
                            options:{
                                ident:'postcss',
                                plugins:()=>[
                                    require('postcss-preset-env')()
                                ]
                            }
                        },
                        'less-loader'// "scss-loader"
                    ]
            }
        ]
    },
    plugins:[
        new MiniCssExtractPlugin({ // 将js文件中的css提取出到单独的css文件中
            filename:"css/build.css"
        }),
        new OptimizeCssAssetsWebpackPlugin() // css代码压缩
    ]
}

js 

开发中的代码通过eslint-loader做规范处理,错误预检,然后通过babel-loader做兼容性,将第三方代码和业务代码分开code-split,代码压缩 

module.exports = {
 // 一般在开发模式下做兼容处理
    /* 
        js兼容性处理:babel-loader @babel-core
        1.基本兼容性处理  @babel/preset-env
        问题只能转换基本语法,promise不能处理
        2.全部兼容性处理  @babel/polyfill
        问题:只需要部分兼容就好了,但是将所有的兼容都引入了
        3.按需加载做兼容性处理:core-js
    */
    module:{
        rules:[
            {
                test:/\.js$/,
                exclude:/node_modules/,
                loader:'babel-loader',
                options:{
                    presets:[
                        [
                            '@babel/preset-env',
                            {
                                useBuiltIns: 'usage',
                                corejs:{version:3},
                                targets:{
                                    chrome: '60',
                                    firefox:"60",
                                    ie:"9",
                                    safari:'10',
                                    edge:'17'
                                }
                            }
                        ]
                    ]
                }
            },
/*
    语法检查 : eslint-loader eslint
    
*/
            {
                test:/\.js$/,
                exclude:/node_modules/,
                enforce:'pre', // 一个文件同时只能进行一个loader的处理,加这个属性,就会使得语法检查最先执行
                loader:"eslint-loader",
                options:{
                    // 自动修复格式问题
                    fix:true
                }
            }
        ]
    }
}

11.如何提高webpack的打包速度

  1. 利用缓存:利用Webpack的持久缓存功能,避免重复构建没有变化的代码
  2. 使用多进程/多线程构建 :使用thread-loader、happypack等插件可以将构建过程分解为多个进程或线程
  3. 使用DllPlugin和HardSourceWebpackPlugin: DllPlugin可以将第三方库预先打包成单独的文件,减少构建时间。HardSourceWebpackPlugin可以缓存中间文件,加速后续构建过程
  4. 使用Tree Shaking: 配置Webpack的Tree Shaking机制,去除未使用的代码,减小生成的文件体积
  5. 移除不必要的插件: 移除不必要的插件和配置,避免不必要的复杂性和性能开销

12. 如何减少打包的体积

  1. 代码分割(Code Splitting):将应用程序的代码划分为多个代码块,按需加载
  2. Tree Shaking:配置Webpack的Tree Shaking机制,去除未使用的代码
  3. 压缩代码:使用工具如UglifyJS或Terser来压缩JavaScript代码
  4. 使用生产模式:在Webpack中使用生产模式,通过设置mode: 'production'来启用优化
  5. 使用压缩工具:使用现代的压缩工具,如Brotli和Gzip,来对静态资源进行压缩
  6. 利用CDN加速:将项目中引用的静态资源路径修改为CDN上的路径,减少图片、字体等静态资源等打包

  // 提高打包速度

// 1. 合理使用loader, exclude,include

// 2. 使用treeshaking, 删除无用代码

// 3. 开启多进程打包, thread-loader

// 4. 利用缓存,cache

// 5. DllPlugin可以将第三方库预先打包成单独的文件,减少构建时间

// 减少打包体积 将一个大的包拆分成多个小包,并提取公共代码

// 1. 代码压缩,js terser-webpack-plugin css css-minimizer-webpack-plugin,optimization, minimize:true

// 2. three-shaking 删除引用但是没用的代码 ,optimization usedExports:true

// 3. 组件可以采用 按需引用,babel-plugin-import 设置

// 4. dllPlugin 将不会改动的三方代码打包到一个js文件中