Skip to content

概述

webpack通过打包commonjs(现在能识别esm)格式的js文件,可以打包出浏览器能过识别的commonjs语法的代码,本质上是模拟commonjs的加载过程。对于非commonjs的资源,需要通过loader变换成commonjs风格。

打包后的文件分析

打包成单文件

精简了部分代码。

js
// a.js
exports.a = 1;

// index.js
const a = require('./a.js').a;
exports.b = a;

(function(context) {
  const self = context;
  const wrapJsContent = (content) => {
    return (_webpack_require_, module, exports, modules) => eval(content)
  }

  const modules = {
    'a.js': wrapJsContent(`exports.a = 1`),
    'main.js': wrapJsContent(`
      const a = _webpack_require_('./a.js').a;
      exports.b = a;
    `),
  }

  const _webpack_require_ = (moduleId) => {
    if(_webpack_require_.cache[moduleId]) {
      return _webpack_require_.cache[moduleId].exports
    };
    const module = {
      exports: {},
      cache: false
    }

    _webpack_require_.cache[moduleId] = module;
    modules[moduleId](_webpack_require_, module, module.exports);

    return module.exports;
  }

  _webpack_require_.cache = {}
  _webpack_require.g = 'xx'
  _webpack_require.p = 'xxx' // 定义其他的工具函数方法等
  _webpack_require_('main.js')
})(this)

打包成多个文件

通过import() 语法可以实现打包多个文件,每个文件就是一个chunk。通过output.chunkFilename修改。在浏览器中,异步的加载chunk通过jsonp的方式执行。

js
// a.js
exports.a = 1;

// index.js
const a = import('./a.js').then(module => {console.log(module.a)});
exports.b = a;



// a.chunk.js
self[xxx].push([['a.js'], {
  'a.js': wrapJsContent(`exports.a = 1`)
}])

// main.js
const modules = {
  'main.js': wrapJsContent(`
    const a = _webpack_require_.e('./a.js').then(() => _webpack_require_('.a.js')).then(module => {console.log(module.a)});
    exports.b = a;
  `),
}

_webpack_require_.e = (id) => {
  return _webpack_require_.l(id)
}

_webpack_require_.l = (id) => {
  const src = genUrl(id);
  const result = [null, null, null]
  const result[2] = new Promise((resolve, reject) =>  [resolve, reject] = resolve);
  const s = document.createElement('script');
  s.src = src;
  appendScript(s);
  s.onload = () => {
    removeScript(s)
    result[0]();
  };
}

self[xxx].push = (chunks) => {
  modules = {
    ...modules,
    ...chunks
  }
}

打包细节

webpack不是单纯的对js文件封装,需要匹配出文件中的require、import等引用。所以如果js文件有错(使用了高阶语法或者宏形式),打包不能正常运行。对于这种情况可以采用loader的形式先编译成js能够识别的内容。

rollup打包同理,需要解决上面源码中的语法错误。

打包库

通过设置output.libraryTarget, 将打包结果添加到不同的目标上 详情

  • libraryTarget = 'var',将结果添加到library 设置的值上 ]
  • libraryTarget = 'umd', 将函数结果使用umd格式包装起来, commonjs绑定到exports[library]或者module.exports = result

对比rollup

rollup是esm文件格式的打包,rollup是简单的import,不会模拟commonjs的加载逻辑,比较轻量级。

external 和 target

target默认等于'web',在import()时采用jsonp的方式加载代码,而且不能引用内置模块,比如require('path');

可以设置 target: 'node',这样就能正确的require内置模块。 依然是采用的webpack_require的方式加载,只是在modules有点特别的写法。webpack通过这种方式可以灵活的设置加载模式,比如后面的externals。

js
"path": ((module) => {
  module.exports = require("path");
})

对于node_module时的模块,依旧当做外部模块,还是会打包到output中,对于这种,可以采用externals的方式。

js
const lodash = require('lodash')

{
  externals: {
    lodash: "commonjs lodash", // 会将 require('lodash') 变成 module.exports = require('lodash');
    lodash: "lodash", // 会将 require('lodash') 变成 module.exports = lodash;
    lodash: ['commonjs lodash', 'a'], // 会将 require('lodash') 变成 module.exports = require('lodash').a;
    lodash: ['require111('a')', 'a'], // 会将 require('lodash') 变成 module.exports = require111('lodash').a;
  }
}

这样不管是在web还是node中,均可以实现。这个相当于一个宏,可以实现一些骚操作,比如不是绑定的全局变量。

webpack中的path.resolve和path.join

path.join是从左往右依次执行,上一次的结果成为下一次的相对路径或者绝对路径的开头,是一个简单的拼接。不一定能得出相对路径。 path.resolve是完成一个绝对路径,每一次碰到绝对路径重新计算路径,最后能获得一个绝对路径。

js
const path = require('path');

const path1 = path.join('/a', 'b') // /a/b
const path2 = path.join(__dirname, '/a', 'b') // D://xx/xx/a/b 

const path2 = path.resolve(__dirname, '/a', 'b') // D:/a/b  第二个开始会独立计算,
const path2 = path.join('a', 'b') // D://xx/xx/a/b

module,chunk和bundle的理解

moudle很好理解,一个资源就是就是一个module
chunk包括入口chunk和子chunk,webpack内部执行时将moudule转换成chunk bundle是多个chunk的合并,最终打包的产物,默认情况下每个入门文件和代码分离(import)的部分会生成bundle,其余的可以通过插件来设置。

如何降低入口文件的大小

  • externals
  • 通过splitChunk调整

TODO: 调整bundle文件大小

配置文件

可以采用函数、promise和数组的形式配置,数组的形式会独立的执行多个打包程序。promise的场景可以用在统一管理配置服务的地方。

  • context可以指定entry和loader的相对路径,本身必须是相对路径。

entry

  • entry 支持多入口,webpack会独立的从多个入口打包,如果遇到多个入口共享的chunk,只会打包一次。
  • 单个入口支持数组的形式,相当于新建一个文件文件里面是依次rerquire 数组,同时数组最后一个require的内容作为导出
js
{
  entry: {
    main: ['./a', '.b']
  }
}

// 相当于新建一个文件temp.js
require('./a');
module.exports = require('./b');
{
  entry: {
    main: './temp.js'
  }
}
  • 最完整的配置方式是通过对象,output.filename中的name是entry的key(默认是main),可以通过filename来修改。
js
{
  entry: {
    'main': {
      import: ['./index.js'],
      filename: 'test'
    }
  }
}
  • 可以通过dependOn来指明依赖,实现公共chunk的提取。
js
{
  entry: {
    'lodash11': 'lodash'
    'main': {
      import: ['./index.js'],
      filename: 'test',
      dependOn: ['lodash11']
    }
  }
}

最终生成bundle中 main实际的代码是 self['xxx'].push(['main.js', modules]),相当于import()分离的代码。lodash包含了主要代码。实际上不推荐这样做,lodash没办法复用。

output

  • path必须是绝对路径,使用path.resolve或者path.join(__dirname, xxx)
  • filename 用于配置入口chunk打包后的bundle, chunkFilename是其他chunk打包的bundle

contenthash、fullhash和chunkhash

TODO:

chunkFormat 和 chunkLoading

chunkFormat用于制定chunk的格式,'array-push' | 'commonjs' | 'module'

chunkLoading 表示入口main如何加载chunk module_require.f.xxx 'jsonp' | 'import-scripts' | 'require' | 'async-node' | 'import'

默认情况下和target有关,可以用于跨项目共享chunk上面,比如chunkFormat只要和chunkloading能匹配,项目中就可以引用其他项目的chunk

目前支持在每个entry中配置output.library

js
{
  entry: {
    main: {
      import: '.index.js',
      library: {
        name: 'xxx',
        type: 'umd',
      }
    }
  }
}

asset和publicPath以及base问题。

  • 通过copy-webpack-plugin实现静态资源到dist目录的拷贝,所有的静态资源必须使用相对路劲。
  • publicPath用于chunk和部分其他资源的路劲前缀,请总是以/结尾
  • base 定义了页面中所有资源相对路劲,如果未设置默认是当前文档(路径的最后一个/前的目录)。同时也支持相对路径,相对时当前文档

支持动态publicPath,_webpack_public__path 代码中设置即可。这些webpack内置变量都是宏,会在编译阶段替换。

部署子目录时需要涉及上面的问题,一般按照下面流程调整。

  • 修改publicPath调整webpack资源,最终的请求地址是简单拼接,所以一定/结尾,调整成./
  • 静态资源全部修改成相对路径,使用copy-webpack-plugin 拷贝到dist目录下
  • 修改index.html 中的base,调整成子目录绝对路劲(history模式下) ,如果是hash模式不需要调整,只需要在浏览器路径后补齐/

针对开发环境asset异常的问题按照下面解决 TODO:

module.noParse对于externals的补充

对于不需要打包的外部模块可以采用noParse的方式,webpack会忽略对当前文件的引用。这个行为发生在externals之前,用在node环境比较多

module.rules

配置webpack对资源的处理,通过use一个一个loader,返回一个webpack能够识别的js文件。

resolve 别名和编辑器快速引用

resolve.alias配置别名,需要配置绝对路径,使用别名相当于path.resolve(@xxx, 'somepath'); 搭配上jsconfig.json 或则 tsconfig.json 中的compilerOptions

json
{
  "compilerOptions": {
    "module": "commonjs esnext", // 使用import还是require实现自动补全
    "baseUrl": "",  // 指定后面的路径的基路径
    "paths": {
      "@": ["./src"]
    }
  }
}

TODO: vscode中自动补全时,通过配置xx总是补全绝对路劲

webpack如何查找module

  • resolve.extensions: array 省略后缀
  • resolve.mainFiles: 'index' 针对目录默认加载的文件
  • resolve.modules: ["node_modules"] 对于无路径的引用,到node_modules下查找目录。
  • 查找目录后根据resolve.mainFields: array 配置,查询package.json对应字段中的入口文件。默认情况下, target: web是[browser, module, main]。target:node [module, main],如果没有配置默认当做目录处理。 可以查看lodash的包

package.json 中的main browser和module

main 对应 commonjs。 module对应 esm。 browser 对应 umd 规范。

优化篇

webpack的重点

webpack 优化选项配置

optimization.chunkIds

webpack生成的chunkId,在output.chunkFilename 替换ID选项, 开发环境默认是named,生产环境默认 deterministic,如果都不是,设置为natural natural: 从0开始顺序编号 named: 方便调试的id,一般是文件名 deterministic: 在多次构建不会变化的短数字id,方便生产环境缓存 size: 让初始化下载包更小的数字id,比如某个包按natural编码是11,为了在降低初始包大小,把这个包改成2(两位数变以为数),刚好降低 total-size: 让总包变得更小的数字id

optimization.moduleIds

控制台__webpack_require.m 中module的名字。参数同chunkIds。

js
__webpack_require.m = {
  './src/main.ts':eval(xxx),  // 把'./src/main.ts' 替换成更短的数字,减少包大小
  './src/index.ts':eval(xxx),
}

optimization.mangleExports

允许控制导出处理 size: 最小下载 deterministic: 最小缓存

optimization.concatenateModules

让webpack查找那些包是可以安全的合并到某一个模块,生成会默认开启

optimiaztion.emitOnErrors

默认是false,表示如果编译报错会将错误的资源发送出来到生成的代码中。场景是用来在页面上提示编译报错的信息,正常情况下,引用main.js会渲染界面,开启这个选项后,编译报错后,引入main.js 执行后控制台就会报错(真是个小垃圾功能)

optimization.flagIncludedChunks

告诉webpack不要加载子chunk

optimization.mergeDuplicateChunks

是否需要合并相同的引用,比如A引用了B,C引用B,正常情况会打包两次B,开启了只会打包一次。生产环境会默认开启。

optimization.minimizer 和 minimize

webpack5默认内置了minimizer,生产默认开启 也能通过自定义TerserPlugin来实现。 [new TerserPlugin(), '...'] '...'表示默认的压缩组件

optimization.runtimeChunk

true: 为每个入口开启单独一个runtime文件 false: 在入口文件中直接嵌入 { name: (entry: string) => string } 为每个入口开启一个runtime,并自定义 独立出runtime后,入口文件的加载方式会和chunkload相同

optimization.splitchunk

前面的优化项都是对chunk的生成做优化,chunk生成好后,通过splitchunk来再次对chunk进行处理 默认的splitchunk配置如下

js
module.exports = {
  //...
  optimization: {
    splitChunks: {
      chunks: 'async',  // 只处理异步导入的chunk   选择落入cacheGroupdechunk那些chunk会被处理
      minSize: 20000,   // 单chunk最小20000b
      minRemainingSize: 0,  //  拆分后最小chunk大小
      minChunks: 1, // 如果一个模块被引用了1次就会被拆封,即使是同步应用。
      maxAsyncRequests: 30,
      maxInitialRequests: 30,
      enforceSizeThreshold: 50000,
      cacheGroups: {
        defaultVendors: {
          test: /[\\/]node_modules[\\/]/,
          priority: -10,
          reuseExistingChunk: true,
        },
        default: {
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true,
        },
      },
    },
  },
};

拆分包是多条件判断,多个条件同时满足才会拆分。比如需要同时满足minSize和minChunks

cacheGroup表示先将chunk划分到cache组中,每个异步包也是一个cache,按照cacheGroup来处理再按照cache组规则优先级进行分包。

除了splitChunks会做分包外,异步导入的模块webpack总是会对其进行分包。

汇总一下分包的逻辑,首先是webpack的分包,webpack会区分为入口chunk、异步chunk还有入口依赖(dependOn依赖的包)作为分包。 分包完后,进入splitChunk分包,上面分入的包会按照cacheGroup进入缓存组,在缓存组中按照配置的规则再次进行分包。一个chunk只会落入一个cacheGroup,通过优先级来判断落入那个。

DDL优化

这是一种落后的配置,通过DDL又称动态运行库,webpack5中不需要DDL技术(splitChunks太强大),不过也可以实现。通过dependOn声明DDL库

js
export default {
  entry: {
    main: {
      deependOn: 'ddl',
      import: './src/index.ts'
    },
    ddl: ['lodash', 'vue']
  }
}

会把ddl打包到单独ddl.js包含了ddl代码和webpack runtime。再通过优化配置将runtime剔除掉。会有单独的ddl.js 这个ddl即可每次打包不再更新。

指定部分模块单独成chunk

利用splitChunks的分包特性很容易做到,webpack默认只会有3种包,dependOn,init和异步导入的chunk,还有runtime分离包。

ts
export default {
  optimization: {
    splitChunks: {
      lodash: {   // 将lodash从包里拆分出
        chunks: 'all', 
        minSize: 0, // 分包最小大小 
        test: /[/\\]node_modules[/\\]lodash[/\\]?/
      },
    }
  }
}

优化检查点

  1. reslove 优化,提高查找效率速度
    • extensions
    • alias
    • modules
    • mainFields
    • mainFiles
    • resloveLoader
  2. noParse 跳过部分模块中的解析(模块本身还是会解析)
  3. 资源cdn化,在cdn过程提前查询dns域名。
  4. external分离第三方资源,通过htmlWepbackPlugin引入
  5. 动态导入,分离模块。
  6. bebael-loader使用缓存
  7. 配置webpack缓存
  8. oneof loader提高loader的查找性能

optimization 常用字段

js
{
  optimization: {
    chunkIds: 'named deterministic' // 调整的是chunkFilename中[name]字段 named 使用文件名 deterministic 使用有缓存的数字编码,比如刚开始是0.js 1.js 2.js 如果删除了1.js ,下次打包会生成0.js 2.js,不会生成1.js。利于长期缓存
    moduleIds: 'natural size total-size', //生成bundle中modules moduleId。    同上 natural 从自然数开始 size尽可能让初始包小 total-size 尽可能让总包小。 
    flagIncludedChunks: boolean, // prod 默认开启,表示是否不重复加载子chunk,如果a,b 都同时引用了c c会打包两次
    mergeDuplicateChunks: true, // 是否合并相同的chunk,在多个入口同时引用了相同的文件就会被合并。
    minimize: true, // 是否开启内置压缩,生产默认开启,开发默认关闭
    minimizer: xxx // 自定义压缩工具
  }
}

minimizer TerserPlugin 插件使用

TODO:

dev server

js
{
  devServer: {
      client: {
        overlay: true,  { errors: true, warnings: false }, // 是否需要在浏览器全屏展示错误警告信息
        progress: true,  // 是否展示编译的进度条信息
        reconnect: true number // 是否自动重连
        compress:  true, // 启用gzip压缩

      },
      historyApiFallback: true // 启用history api
      headers: {  // 添加自定义响应头,可用于直接鉴权
        "X-xx-x": 'something' 
      },

      host: '0.0.0.0'   // 配置本地监听的host,一般配置local-ip
      port: 8088 // 本地监听端口号
      hot: true  // 是否启动热刷新,开启后会默认打开插件
      open: ['/xxx'] true // 自动打开新页面,详情参考open库
      open: {  // open 包
        target: ['first.html', 'http://localhost:8080/second.html'],
        app: {
          name: 'google-chrome',
          arguments: ['--incognito', '--new-window'],
        },
      }

      proxy: { // http-proxy-middleware 
        '/api': {
          target: 'xcxx:xxxx',
          pathRewrite: {
            '^/api': ''
          },
          secure: false, // 关闭https的安全校验,用于后端的https自签名情况
          bypass: () => boolean  // 判断是否转发
        }
      },

      // 开发环境配置静态目录
      static: {
        directory: ['./src/assets'],
        publicPath: './xxx'
      },
      devMiddleware: { // webpack-dev-middleware 用于配置devServer在哪儿查找资源

      }
  }
}

devServer中的publicPath 和 output中的publicPath的区别

devServer中会先将代码打包到内存中,和打包到dist一样,会按照output.publicPath组织chunk和资源的加载。

output.publicPath 仅用于chunk和资源加载的,是一个简单的拼接,不会影响入口chunk。 比如设置了output.publicPath: './xx',入口chunk的地址还是localhost:3000/main.js,但是子chunk的加载地址会变成./xx/chunk.js

devServer 中的publicPath是用于生成子目录服务器,会影响所有资源的加载。包括入口chunk。 默认情况下这个值和output中的一致,所有output中不要设置相对路劲。 output.publicPath: ./a devMiddleware.publicPath: /b 入口chunk的地址是host:3000/b/main.js 加载的资源地址变成了 host:3000/b/a/chunk.js,显而易见的main.js 和chunk.js 不在同济目录下。 这个时候有种方法,是把子chunk打包到目录下。

在CICD中如何利用缓存加快打包进度

TODO:

监听文件变动watch 和 watchOption

在开发服务器中默认开启,build默认是关闭。 watchOption控制监听的方式、重新构建间隔和目录。会监听已经解析的资源变更。如果新增了一个文件,只需要引用一次就可以实现监听

模块联邦

模块联邦提供了加载其他模块的能力,可以用于跨项目共享chunk。

  • 本地模块指的是入口文件构建的模块,远程模块是MF定义出的。相当于共享配置的两个入口文件。
  • 每个构建都是一个容器,容器可以加载远程模块。
  • 远程模块需要使用异步的方式加载。
  • 远程模块也可以作为容器加载其他远程模块。
  • 远程模块中,因为publicPath是简单的拼接,如果设置了publicPath会出现加载宿主域的问题。需要将publicPath设置为auto或则动态设置。
  • 远程模块至少会生成runtime、remote和module 3种chunk文件
js
const MF = webpack.container.ModuleFederationPlugin;

module.exports = {
  plugins: [
    // 打包逻辑 runtime -> remote.js -> ./componentA
    new MF({
      name: 'remote',  // 远程模块名称
      filename: 'remote.js', // 生成的远程模块文件,用于宿主容器加载
      runtime: 'remote.runtime.js', // 默认情况下runtime文件生成在filename 定义的文件中,可以单独抽离出来。
      exposes: [{
        './componentA': './src/componentA.vue'  // 需要使用./前缀,host 通过 import('remote/componentA')
        '.': 'src/componentA/index.js'  // import('remote')
      }]
    })
  ]
}

动态设置publicPath

因为runtime.js 需要加载remote.js和module.js 所以需要提前设置publicPath。MF在打包时如果container chunk和入口chunk同名,会合并。所以需要将name设置为一个入口name相同。

js
module.exports = {
  entry: {
    main: './index.js',
    remote: './public-path.js' // 在入口文件中配置动态的js。
  },
  plugins: [
    new MF({
      name: 'remote',  // 远程模块名称
      filename: 'remote.js', // 生成的远程模块文件,用于宿主容器加载
      runtime: 'remote.runtime.js', // 默认情况下runtime文件生成在filename 定义的文件中,可以单独抽离出来。
      exposes: [{
        './componentA': './src/componentA.vue'
        '.': 'src/componentA/index.js'
      }]
    })
  ]
}

动态加载远程模块

正常情况下远程模块是在webpack配置中制定的,可以手动实现远程模块的加载。

js
const loadModule = async (url, containerName, module) => {
  await loadScript(url);   // 加载remoteEntry js 脚本。
  const container = window[containerName];  // 默认情况下remoteEntry 按照var 导出。 通过window containerName 可以获取到container。
  const factory = await container.get(module)  // get方法是一个异步的方法,获取的是modules里的value。
  return factory()  // 执行一次相当于执行了module文件。
}

上面代码不够完整,缺少共享模块的加载以及重复引用下module不是单例问题。

js
const remoteModuleCache = new set();
let loadModule = async (url, containerName, module) => {
  if(hasCache(container, module)) {
    return getCache()
  }
  await loadScript(url);   // 加载remoteEntry js 脚本
  await __webpack_init_sharing__('default')  // 全局初始化共享模块
  const container = window[containerName];  // 默认情况下remoteEntry library 按照var 导出。 通过window containerName 可以获取到container。
  container.init(__webpack_share_scoped__.default)
  const factory = await container.get(module)  // get方法是一个异步的方法,获取的是modules里的value。
  return factory()  // 执行一次相当于执行了module文件。
}

远程模块加载流程

  1. webpack_modules 声明了remoteEntry入口文件。
  2. 通过__webpack_require__.e 调用 __webpack__require.f.remote 中间件加载入口js文件。
  3. 入口js文件加载好后,在全局暴露remote变量
  4. host中加载远程模块的module会固定编译成 一个chunkid,
  5. host 会调用remote去获取这个chunkId对应的js文件,
  6. 加载完成后回调在remote中,然后host执行这个remote中的回调函数得到module。

远程模块踩坑

  1. 远程模块的异步chunk加载方式务必不要和host应用雷同,会出现覆盖host modules问题。大坑,使用 output.chunkLoadingGlobal、output.uniquename修改
  2. 目前自动加载远程模块的方式仅默认方式能生效,即把远程模块绑定到全局变量上。其他的方式均需要自行加载远程模块

webpack宏变量

webpack_xxx_xx 格式的都是webpack的宏,会在编译阶段替换成内置的命令,可以嚣张点直接写内置的命令或者使用DefinePlugin 插件来定义。

loader

loader用于将webpack不能直接识别的内容转换,一般来说需要返回一个具有导出对象的文件。可以用处理图片、代码宏等操作。

style-loader、css-loader、postcss-loader、sass-loader和less-loader

样式处理按照如下loader顺序执行

  1. vue-loader 根据style中lang将style的内容单独使用对应的loader处理。
  2. sass-loader和less-loader调用sass或者less编译成css。
  3. postcss-loader对编译后的css进行处理,添加autofixprefix、修复部分错误等
  4. css-loader解决css文件中的@import和url()
  5. style-loader实现一个js,将css代码添加到style标签中。

重点关注的地方:各种样式、资源引用的处理细节。

  • vue-loader不会对资源进行任何处理,@import '../style/a.scss'。相对的是vue文件本身,简单理解vue-loader会将vue文件拆分成两个文件,一个js文件,一个scss文件。

  • sass-loader扩展了@import功能(同js require一致的查找规则),添加~前缀可以从node_module中查找(可以不添加,默认相对路径,相对路径无法查询自动使用node_modules)。在js中被直接引用的scss文件视为入口文件,scss中@import文件的资源相对路径都参考这个(见后文例子)。 除此之外sass-loader不会处理代码中的url()。sass处理@import时,不会处理.css后缀、http开头、@import url() 格式。针对这个问题,可以使用resolve-url-loader 前置处理。

  • less-loader 大致同上。less也实现了webpackImport功能。less还扩展了一些,比如只引用不输出等。具体参考文档。不过less-loader 处理url,不需要使用resolve-url-loader

  • css-loader中@import始终应该被处理,对于url() 按照webpackImport方式处理,不过别名使用的是~,node_modules使用的~moudle,配置上options.url,可以动态的判断是否需要处理url资源

scss
// src/style/test/test.scss
@import 'a';  // ./a.scss => node_modules/a.scss
@import '@src/a';  // /src/a.scss
.test {
  background: url('xx.jpeg');  // src/style/test/xx.jpeg
  background: url('~@src/assets/img/xx.jpeg');  // src/assets/img/xx.jpegjpeg
  background: url('./public/xx.jpeg'); // ./public/xx.jpeg   // 这个不会经过webpack处理,其余的都会
  background: url('/src/xx.jpeg'); // /src/xx.jpeg'
}

图片、字体等资源的处理。

webpack5中使用module.rules.type取代了file-loader, url-loader 和 raw-loader。

  • asset/resource 代替 raw-loader
  • asset/inline 代替 url-loader
  • asset/source 代替 file-loader
  • asset 代替了url-loader 并自动选择data-url还是url

默认情况下总是会使用asset/source,并且以8KB为分界线,通过module.rules.parser.dataUrlCondition调整。 在output.assetModuleFilename定义输出的文件名称。

默认情况下webapck总是会对资源asset处理, 如果需要自己配置loader,需要加上type: 'javascript/auto',否则资源会被打包两次。

loader的加载方式和加载顺序

use: ['style-loader', 'css-loader']时,我们直到loader总是从后往前执行,css-loader 传递给style-loader的总是js代码,style-loader如何处理js代码将css提取出。

有如下两种require('xxx.css') 和 require('style-loader!css-loader!xxx.css')。前面是根据在rules中的规则进行解析,后者是直接在require时指明loader的处理方式,再和和rules指定的,以!作为间隔符。

loader分为下面四种加载,通过enforce可以制定loader应用时机。

  • pre 前置加载
  • normal 默认值,正常require(使用当前流程)
  • post 后置加载
  • inline 上文的行内形式

加载流程 pre -> inline -> normal -> post

js
module.exports = {
  module: {
    rules: [
      {
        test: /\.less/,
        use: [
          {
            loader: 'pre-loader',
            enforce: 'pre'
          },
          {
            loader: ['post-loader'],
            enforce: 'post'
          },
          {
            loader: ['normal-loaderA', 'normal-loaderB'],
            // enforce: 'normal'
          }
        ]
      }
    ]
  }
}

require('inline-loader!xxx.less')  //  use: ['pre-loader', 'inline-loader', 'normal-loaderA', 'normal-loaderB', 'post-loader']

在inline前面添加不同修饰符可以定义不同的行为

  • ! 禁用normal loader
  • !! 禁用所有loader pre normal post
  • -! 禁用pre normal
js
require('!inline-loader!xxx.less')  //  use: ['pre-loader', 'inline-loader', 'post-loader']
require('-!inline-loader!xxx.less')  //  use: ['inline-loader', 'normal-loaderA', 'normal-loaderB',  'post-loader']

在确定好顺序后开始加载loader loader区分为pitch阶段和normal,pitch阶段相当于时间捕获,从前到后执行,具有break功能。pitch中能够获取后续loader链。pitch 如果有返回会直接跳到前一个loader的normal阶段。

js
require('inline-loader!xxx.less')  'pre-loader', 'inline-loader', 'normal-loaderA', 'normal-loaderB', 'post-loader'   => 'post-loader', 'normal-loaderB', 'normal-loaderA', 'inline-loader', 'pre-loader', 

require('-!inline-loader!xxx.less') 'inline-loader'  =>  'inline-loader'

了解加载顺序后,分析一下style-loader 和 css-loader的执行流程。 less-loader对less文件处理后会返回css内容,非js模块 css-loader处理css中的url和import 返回js模块(为什么),js导出一个对象,执行toString可以得到原始的css文件。 style-loader 只接受css内容 ,不支持js模块包裹的css。

style-loader 通过pitch的方式在运行时获取了css-loader 返回的moudle,通过隐式调用toString拿到原始的css文本添加到style中。

js
// css-loader
module.exports = {
  toString() {
    return `
      .test {
        color: red
      }
    `
  }
}

正常情况下css-loader 返回给下一级是包含module.exports语句的文本,需要动态执行才能获取到原始的文本,可以实现一个css-loader和style-loader的适配器

js
// css-style-loader.js
module.exports = (content) => {
  const evalFn = () => {
    const module = { exports: {}};
    eval(content);
    return module
  }
  const cssText = evalFn(content).toString();
  const style = document.createElement('style')
  style.innerText = cssText;
  document.body.appendChild(style)
}

publicPath那些事儿

publicPath是webpack中最重要的属性,大部分不熟悉的同学都会碰到各种奇怪的404。有二个进阶的使用办法,第一个是设置为auto。编译的后代码如下

js
/******/ 	(() => {
/******/ 		var scriptUrl;
/******/ 		if (__webpack_require__.g.importScripts) scriptUrl = __webpack_require__.g.location + "";//  __webpack_require__.g 就是window全局变量
/******/ 		var document = __webpack_require__.g.document;    // window.document
/******/ 		if (!scriptUrl && document) {
/******/ 			if (document.currentScript)
/******/ 				scriptUrl = document.currentScript.src   // currentScript会指向当前这个代码所在的js文件
/******/ 			if (!scriptUrl) {   // 获取第一个js文件所在的标签地址
/******/ 				var scripts = document.getElementsByTagName("script");
/******/ 				if(scripts.length) scriptUrl = scripts[scripts.length - 1].src
/******/ 			}
/******/ 		}
/******/ 		// When supporting browsers where an automatic publicPath is not supported you must specify an output.publicPath manually via configuration
/******/ 		// or pass an empty string ("") and set the __webpack_public_path__ variable from your code to use your own logic.
/******/ 		if (!scriptUrl) throw new Error("Automatic publicPath is not supported in this browser");
/******/ 		scriptUrl = scriptUrl.replace(/#.*$/, "").replace(/\?.*$/, "").replace(/\/[^\/]+$/, "/");  // 获取地址的相对路径
/******/ 		__webpack_require__.p = scriptUrl;
/******/ 	})();

第二个是动态设置通过设置__webpack_public_path__ = 'xxxxx'运行时设置,需要注意的是,webpack_public_path__是宏命令,在编译时会转换成__webpack_require.p,所以务必需要在入口module中定义。而且如果是esmodule,还必须在在单独的文件中定义,使用import 导入。

ts
__webpack_public_path__ = 'xxxxx'  // 这种方式不行,因为import总会提到最上级执行,import xx from 'xxx' 就在 __webpack_public_path__ = 'xxxxx' 前面
import 'public-path.ts'  // 这种方式才行
import xxx from 'xxx'

代码压缩

webpack默认带了开箱即用的terserWebpackPlugin配置,如果需要自行添加压缩,需要重新安装依赖 terser-webpack-plugin。并在optimization.minimizer 新增。同时还需要保留原来的配置['...', new terserWebpackPlugin()]。terserWebpackPlugin压缩文件时只针对source-map 生效,其他的都无法生成source-map。

除了对js压缩外,webpack通过CssMinimizerPlugin 对css进行压缩

contenthash、fullhash和chunkHash

contentHash: 针对产物的内容(原始内容)进行hash fullHash: 整个项目内容hash chunkHash: 针对chunk的内容进行hash hash有一个边界的效应,异步chunk变化时主chunk也会变,原因是主chunk需要引用异步chunk,异步chunk名字变化,主应用也必须要变化,就没法满足contentHash,这点在入口chunk中最为明显。

webpack-runtime 和 manifest

打包后代码大量包含webpack内置的加载方法,这部分代码理论不会有太大变化,通过optimization.runtimeChunk分离出来。但是每次带hash打包会,wepback runtime文件中总是会包含对异步模块的映射(manifest)就是那堆__webpack_require__.u ,导致runtime总是会变化。通过html-webpack-inline-source-plugin插件直接将代码内联到html中,随着html更新。为什么不能将代码webpack.u固定到其他位置。后来找到了原因,整个runtime文件也就才2Kb,没必要再过度拆分了。社区也有方案(chunk-manifest-webpack-plugin),这个方案也会没用(如果是runtime引用这个文件,那么runtime的hash势必会变(参考hash的边界效应))没得意义。

异步模块的骚操作

在vue中路由模块中组件一般是通过异步组件导入的。一个异步组件就会生成一个chunk,开发者可以制定chunk,将多个异步组件合并

js
export default [
  {
    name: 'home',
    path: '/',
    component: Home
  },
  {
    name: 'foo',
    path: '/foo',
    component: () => import(/* webpackChunkName: "foo" */'./src/page/foo')
  },
  {  // 如果boo文件比较小,不想拆封成两个文件,可以这样做
    name: 'boo',
    path: '/boo',
    component: () => import(/* webpackChunkName: "foo" */'./src/page/boo')
  }
]

loader开发

plugin开发

杂项提效

scss中如何在组件使用scss变量

  1. 全局变量定义为引用文件,防止引入时重复编译
  2. scss-loader中对每一个sass入口文件添加对全局变量的引用。

sass中全局变量、extend、mixin都应该使用引用并导出。

css
$colors: (  blue: #007dc6,  blue-hover: #3da1e0 ); @mixin colorSet($colorName) {  color: map-get($colors, $colorName);  &:hover {  color: map-get($colors, $colorName#{-hover});  } } a {  @include colorSet(blue); }产出如下:a { color:#007dc6 } a:hover { color:#3da1e0 }动态创建:@function addColorSet($colorName, $colorValue, $colorHoverValue: null) {  $colorHoverValue: if($colorHoverValue == null, darken( $colorValue, 10% ), $colorHoverValue);  $colors: map-merge($colors, (  $colorName: $colorValue,  $colorName#{-hover}: $colorHoverValue  ));  @return $colors; } @each $color in blue, red {  @if not map-has-key($colors, $color) {  $colors: addColorSet($color, $color);  }  a {  &.#{$color} { @include colorSet($color); }   } }产出如下:a.blue { color: #007dc6; } a.blue:hover { color: #3da1e0; } a.red { color: red; } a.red:hover { color: #cc0000; }

contextModule

如果request含有表达式,webpack会自动生成一个context module 打包所有符合条件的文件。同时开发者也能自定义context module。通过require.context 获取一个context module返回一个函数,通过函数的keys 获取所有的request的

js
require('./template' + name + '.js')   // 生成一个contextModule = { directory: './template' ,regexp: /\.js&/ }
import('./template' + name + '.js')    // 异步打包的方式, 通过内联注释的方式可以自定义context module 详解见webpack import() 文档

const m = require.context('./template', false, /\.js&/, 'sync')   // 通过第四个参数可以定义的打包的方式,同步异步还是promise,是否分包等特性
m.keys.forEach(m)

自行实现一下require.context 异步的场景