webpack

参考文档

webpack是一个打包工具,通过各种loaderplugin,从入口文件开始,将整个依赖链上的所有资源(js,ts,jpg|png|jpeg|svg,html,fonts,css,json,txt等等)都当作模块来处理,不同的资源类型由不同的loader来处理,配合插件,最终打包合成一个或多个bundles。每个依赖链打包过程可以看作一个chunk

bundle:结果,chunk: 过程

常用概念

  • entry: 入口,有多种写法。

    单入口:

    module.exports = {
      entry: './path/to/my/entry/file.js',
    };

    单入口带依赖:

    module.exports = {
      entry: ['./src/file_1.js', './src/file_2.js'], // 实际就是file_2中引入了file_1,入口文件是file_2
      output: {
        filename: 'bundle.js',
      },
    };

    多入口:

    module.exports = {
      entry: {
        main: './src/app.js',
        vendor: './src/vendor.js',
      },
    };
  • output: 出口,bundle的位置

    module.exports = { 
      output: {
        path: path.resolve(__dirname, 'dist'), // 使用Node的path组件,相当于当前文件的位置下的dist文件夹
        filename: '[name].[contenthash:8].js', // 基于entry的output,输出的文件名使用占位符来动态生成文件名
        chunkFilename: '[name].[chunkhash:8].js' // 某些情况下webpack会额外启用一些chunk用来打包对应的文件,比如lazy load,此时输出的文件就会用这个命名规则
        publicPath: '/', // 公共路径,打包后所有的资源url都会有这个前缀
        clean: true,
      },
    }

    对于lazy load,比如:import('./App.tsx'),此时[name]的表现类似hash,可以使用import(/* webpackChunkName:"app" */ './App.tsx')的方式来命名chunk从而可以使用这个名称。

  • module:这就是用来处理不同类型的资源(模块)的,默认webpack只会处理jsjson,其他类型的都需要为他们指定loader

    module.exports = { 
      module: {
        rules: [
          { test: /\.tsx?$/, use: 'babel-loader', exclude: /node_modules/ },
          {
            test: /\.(png|jpg|jpeg|gif)$/,
            type: 'asset',
            generator: {
              filename: 'images/[hash][ext][query]',
            },
            parser: {
              dataUrlCondition: { maxSize: 4 * 1024 },
            },
          },
          {
            test: /\.less$/,
            use: [devMode ? 'style-loader' : MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader', 'less-loader'],
          },
          {
            test: /\.html$/,
            loader: 'html-loader',
          },
        ],
      },
    }

    通过test匹配对应的资源,通过use来指定loader,有很多用法,数组语法,可以一次指定多个loader,从右至左的顺序生效;对象语法,可以为loader添加选项。

    test后面使用正则,常见用法: /\.后缀$/,如果不想区分后缀大小写,可以使用/\.后缀$/i

    webpack5中对于file-loader,url-loader的使用有了改变,通过type来使用:

    • asset/resource 发送一个单独的文件并导出 URL。之前通过使用 file-loader 实现。

    • asset/inline 导出一个资源的 data URI(比如对于小图像可以使用这种格式来代替引用,默认使用base64算法)。之前通过使用 url-loader 实现。

      常见的比如svg如果很小的话,完全可以都用data URI,还可以自行指定 data URI的生成算法,官网例子

    • asset/source 导出资源的源代码(比如.txt文件的内容)。之前通过使用 raw-loader 实现。

    • asset 在导出一个 data URI 和发送一个单独的文件之间自动选择。之前通过使用 url-loader,并且配置资源体积限制实现。

  • resolve:模块解析相关,设置后引用时可以省略扩展名。

    注意:这只是用于解析自己的源码,对于第三方库,按照本身设置的逻辑解析,比如typescript中,就是使用tsconfig中的配置moduleResolution

    module.exports = { 
     resolve: {
        extensions: ['.tsx', '.ts', '.js', '...'], // '...'表示还包含webpack默认的
      },   
    }
  • devServer: 开发服务器,用于测试。

    module.exports = { 
      devServer: {
        static: {
          directory: path.join(__dirname, 'dist'), // 注意这里是join,相当于绝对路径
          publicPath: '/app/', // 公共访问路径,dev server run的时候,打包的内容是放在内存中的,这个publicPath就是访问内存中的资源的公共路径。而output中的publicPath,是打包后的资源都会加上这个公共路径。可以理解为dev server的publicPath就是nginx设置的虚拟路径,而output的publicPath会修改src等资源的引用路径。2者配合才有效果。
        },
        open: true,
        port: 8080,
        hot: true,
      },
    }
  • mode: 值可以是none,development,production,不同的模式打包后不一样,比如production自动会进行压缩、丑化和tree shaking。

  • target: 值可以是webnode,不同的环境打包编译也有不同。

  • optimization: 优化配置,一般用来剥离chunk,比如被多个chunk共同引用的内容,可以单独设置一个chunk,第三方包也可以单独设置一个chunk。

    还有个runtimeChunk配置,runtimeChunk,表示运行时chunk,为什么有这个东西?

    这里面有2个东西:

    • webpack在编译后的代码中运行时的依赖,主要就是模块加载和解析

    • 模块的映射关系。比如A模块引用B模块,如果B模块变了,contenthash变化,此时A模块中的引用也要变,导致A模块的content变化,从而A模块的contenthash也会变,此时A模块的缓存就失效了,又要重新请求,但是实际上此时只有B模块变了,只需要重新加载B模块就行了,为了解决这个问题,就需要使用runtimeChunk,让A模块不直接维护和B模块的引用关系。

      效果: B模块变了,B模块的contenthash重新计算,runtimeChunk对应的bundle内容跟着改变,也会重新计算contenthash,此时就只需要重新加载B模块和runtimeChunk对应的bundle,虽然也是2个,但是runtimeChunk体积非常小。

    module.exports = { 
      optimization: {
        splitChunks: {
          cacheGroups: {
            // 打包业务中公共代码
            common: {
              name: 'common',
              chunks: 'all',
              minSize: 0, // 大于某个大小时才会单独chunk
              priority: 0,
              minChunks: 2, // 最少引用次数
            },
            // 打包第三方库的文件
            vendor: {
              name: 'vendor',
              test: /[\\/]node_modules[\\/]/,
              chunks: 'all',
              minSize: 0,
              priority: 10,
              minChunks: 2,
            },
          },
        },
        runtimeChunk: { name: 'manifest' }, // 运行时代码
      }, 
    }
  • plugins: 插件,用于扩展webpack的功能。

    最常用的有HtmlWebpackPlugin,可以生成html文件,并自动关联js,css文件,特别对于单页面应用,基本不用配置,开箱即用,还可以指定模板。

    MiniCssExtractPlugin,可以用于单独打包css,而不是放在style中(style-loader的作用)。

  • devtool: source map相关配置,注意生产环境下的配置,不要轻易暴露源码。

常用占位符

一般用于bundle的filename。

  • [fullhash]:根据整个应用计算。[fullhash: 8]可以指定长度
  • [contenthash]:根据文件内容计算
  • [chunkhash]:基于chunk计算
  • [name]: 文件名,一般用在entry是对象形式时
  • [id]: chunk id,
  • [ext]: 扩展名
  • [query]: 模块的 query,例如,文件名 ? 后面的字符串

配置文件语言

建议使用typescript来写。

即: webpack.config.ts

需要额外安装ts-node。也就是配置文件实际是通过ts-node来编译的。

注意: webpack.config.ts是运行在node环境下,而node环境下只支持commonjs模块。所以tsconfig的compilerOptions必须要是commonjs,如果你想使用es模块,那必须要给ts-node单独配置compilerOptions:

// tsconfig.json
{
  "compilerOptions": {
    "module": "ESNext",
  },
  "ts-node": {
    "compilerOptions": {
      "module": "CommonJS"
    }
  }
}

常见模板:

import * as path from 'path';
import * as webpack from 'webpack';
// in case you run into any typescript error when configuring `devServer`
import 'webpack-dev-server';
 
const config: webpack.Configuration = {
  mode: 'production',
  entry: './foo.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'foo.bundle.js',
  },
};
 
export default config;

使用typescript

如何在webpack中直接使用typescript

可以直接使用ts-loader来编译(就是使用tsc),但是更好的方式是使用babel-loader

因为直接编译后生成的js,在没有经过babel的转换时,可能无法在目标环境中运行,有些目标环境不支持es6以后的语法和API。

babel-loader可以通过配置,将typescript直接编译成目标环境适配的js

但是:

babel只能编译,无法进行类型检查,也就是babel只是代替了tsc的编译功能。

这里也能看出babel只是进行编译,并且它是按照自己的配置文件来编译,并不会读取tsconfig.json中的配置。

所以:

最好的选择是2者配合使用:

参考文档

  • babel编译,读取.babelrc.js
  • tsc类型检查,读取tsconfig.json。此时需要配置关闭生成js文件,只是进行类型检查。

所以有些配置项需要写2次。

build时还要先输入tsc来检查,很麻烦,可以利用插件fork-ts-checker-webpack-plugin完成。

babel

参考资料

babel作用:

  • 语法转换:语法降级

    比如箭头函数,转换为常规的声明式或表达式

  • ployfill: API补充

    比如有些环境是不支持Promise,Async/Await,Array.flat等等功能的,需要”填平”,也就是引入ployfill,来支持这些功能。

babel针对不同的内容,都提供了对应的转换插件,非常多,一个个配置是很难的,幸运的是官方提供了preset,可以看作是插件的集合。

常用的有:

  • @babel/preset-env:基本包含了所有的转换降级规则
  • @babel/preset-typescript: 用于编译typescript

react,vue也有对应的预设。

上面的预设只是进行了语法的转换,ployfill呢?

其实@babel/preset-env,可以自动引入ployfill,通过useBuiltIns来配置,当然需要先安装@babel/polyfill或者core-js和regenerator-runtime。但是这种方式会污染全局API,所以建议使用插件的方式,插件不会污染全局API,插件会另外再生成一个API,然后让你的源码改为调用这个API。

通过插件@babel/plugin-transform-runtime来完成。

只需要配置插件的corejs就行了,可以取boolean,2,3,取值false表示不进行ployfill,取值2表示使用@babel/runtime-corejs2来ployfill,取值3表示使用@babel/runtime-corejs3来ployfill,建议使用3,当然前提是安装了这个包(生产环境下需要)。

@babel/plugin-transform-runtime默认开启了对Generator/async的转换,不需要配置。

// .babelrc.js 可被babel-loader读取
module.exports = {
  presets: [['@babel/env', { corejs: false}], '@babel/typescript'], // 多个preset,从右至左的顺序生效
  plugins: [['@babel/plugin-transform-runtime', { helpers: true, corejs: 3 }]],
};
 

注意:使用插件来plofyfill,那@babel/env中的就要关闭。

@babel/plugin-transform-runtime还有个作用,就是抽离公共辅助函数,类似tsconfig中的importHelpers,这个功能依赖@babel/runtime-corejs2|@babel/runtime-corejs3|@babel/runtime

browserslist

babel可以根据目标环境自行选择怎么语法降级和ployfill,这个目标环境就是browserslist。

可以在package.json中配置:

{
  "browserslist": [
    "defaults",
    "not IE 11",
    "maintained node versions"
  ]
}

可以通过npx browserslist命令(需要安装这个包),查看这个配置对应的浏览器及版本。

browserslist可以被用于很多地方,比如postcss,eslint等等。