《webpack5-指南》学习笔记

426 阅读9分钟

webpack 学习笔记

最近把webpack文档里的指南过了一遍,有一部分跟着跑了demo。看过可能会忘记,所以再整理一遍加深记忆。在我写下这篇笔记的时候,我觉得自己只能进行基础的配置,别人做的复杂的脚手架中的webpack配置还是要再去学习的。内容和官网几乎一样。

起步

注意node版本

基本安装

  • 初始化一个项目
// 初始化npm
npm init -y
//安装webpack-cli,用于在命令行中运行webpack
npm install webpack webpack-cli --save-dev
  • 调整package.json的内容保证安装包是私有的;
  • 确保安装包时私有的,移除main入口,可以防止意外发布代码。
-  "main": "index.js",
+  "private": true,

在文档的示例中,用script标签引入存在隐式的依赖关系(关系是在index.js引入前要先引入ladash)。
这种方式管理js项目会出现一些问题:

  • 无法直接体现脚本的执行依赖于外部库
  • 如果依赖不存在或引入顺序错误,应用程序将无法正常运行
  • 如果依赖被引入但并未使用,浏览器被迫下载无用代码

创建bundle

  • ./dist叫分发代码
  • ./src叫源代码
    安装package时,要打包到生产环境bundle中时使用
npm install --save

用于开发环境

npm install --save-dev
  • 执行npx webpack会将src/index.js作为入口起点,也会生成dist/main.js作为输出

配置文件

  • 文件名webpack.config.js
  • 最基础的配置
const path = require('path');

module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'main.js',
    path: path.resolve(__dirname, 'dist'),
  },
};

比起CLI这种简单的方法,配置文件具有灵活性。可以通过配置方式指定loader规则、plugin、resolve选项、以及其他许多增强功能。

npm scripts

在package.json中设置快捷方式

"scripts": {
  "build": "webpack"
},

执行npm run build即可打包文件

管理资源

  • webpack会动态的打包所有依赖(创建所谓的依赖图);现在每个模块都可以明确的表述它自身的依赖,避免打包未使用的模块。
  • webpack除了可以引入js,还可以通过loader或内置的AssetModules引入其他任何类型的文件

加载css

需要安装style-loadercss-loader

rules: [
  {
    test: /\.css$/i,
    use: ['style-loader', 'css-loader'],
  }
]
  • 模块loader可以链式调用。
  • 链中的每个loader都将对资源进行转换。
  • 链会逆序执行;第一个loader将其结果(被转换后的资源)传递给下一个loader。
  • 最后,链中最后的loader返回JavaScript。

在本例中,由于loader是链式调用的,所以style-loader和css-loader的顺序不能调换。
webpack根据正则表达式,来确定应该查找哪些文件,并将其提供给指定的loader。

加载image图像

可以使用webpack内置的AssetModules

rules: [
  {
    test: /\.(png|svg|jpg|jpeg|gif)$/i,
    type: 'asset/resource',
  }
]

图像将会被添加到output目录中

加载fonts字体

找不到woff/woff2文件,无法实践。

加载数据

可以加载的有用资源还有数据。JSON支持是内置的。
要导入CSV、TSV和XML,可以使用csv-loaderxml-loader

rules: [
  {
    test: /\.(csv|tsv)$/i,
    use: ['csv-loader'],
  }, {
    test: /\.xml$/i,
    use: ['xml-loader'],
  }
]

此外,文档还备注了一个tips: 在使用 d3 等工具实现某些数据可视化时,这个功能极其有用。可以不用在运行时再去发送一个 ajax 请求获取和解析数据,而是在构建过程中将其提前加载到模块中,以便浏览器加载模块后,直接就可以访问解析过的数据。

自定义JSON模块parser

使用自定义parser替代特定的webpack loader。俺也不知道这些后缀(.toml.yaml.json5)的文件是干嘛的。

const toml = require('toml');
const yaml = require('yamljs');
const json5 = require('json5');

  module: {
    rules: [
      {
        test: /\.toml$/i,
        type: 'json',
        parser: {
          parse: toml.parse,
        },
      }, {
        test: /\.yaml$/i,
        type: 'json',
        parser: {
          parse: yaml.parse,
        },
      }, {
        test: /\.json5$/i,
        type: 'json',
        parser: {
          parse: json5.parse,
        },
      }
    ]
  },

全局资源

上面这种方式可以将文件和资源结合起来(放在一个文件夹里,这种集中放置的方式会让所有资源紧密耦合)。要使用资源的话,可以以文件夹为单位操作。

管理输出

在文件名中使用hash并输出多个bundle

预先准备

  • 给entry添加新的入口起点,再修改output,配置文件如下。
const path = require('path');

 module.exports = {
  entry: {
    index: './src/index.js',
    print: './src/print.js',
  },
   output: {
    filename: '[name].bundle.js',
     path: path.resolve(__dirname, 'dist'),
   },
};

这时候打包好的js文件一个叫index.bundle.js一个叫print.bundle.js dist文件夹里的index.html也是自己生成的

设置 HtmlWebpackPlugin

不安装这个插件,dist文件夹下的文件很乱。每次更改入口名称或者添加新的入口,构建的时候会生成新的文件,index.html引用的还是旧文件名,还得手动改,很麻烦。实践中还发现用了这个插件之后,除第一次外,打包速度会加快。 安装这个插件会自动生成index.html的内容。

  const HtmlWebpackPlugin = require('html-webpack-plugin');
  plugins: [
    new HtmlWebpackPlugin({
      title: '管理输出',
    }),
  ],

清理./dist文件夹

每次构建前清理./dist文件夹

output: {
  filename: '[name].bundle.js',
  path: path.resolve(__dirname, 'dist'),
  clean: true,
},

manifest

不会

开发环境

source map

  • 解决很难追踪到error和waring在源代码中的原始位置的问题。
  • source map可以将编译后的代码映射回原始代码(控制台中可看) 示例中用到inline-source-map选项 在webpack.config.js中新增
module.export = {
  devtool: 'inline-source-map'
}

现在如果出现错误的话,浏览器控制台中的报错会指出发生错误的文件和内容。

选择开发工具

每次编译输入npm run build很麻烦,在代码发生变化后自动编译代码。

watch mode观察者模式

在package.json.scripts中添加

"watch": "webpack --watch"

效果:保存文件就可以看到实时编译结果
缺点是:为了看到修改效果,需要刷新浏览器。所以一般的解决方案是webpack-dev-server

webpack-dev-server

webpack-dev-server提供一个基本的web server,并且具有live loading(实时重加载)的功能。

module.export = {
  devServer: {
    static: {
      directory: path.join(__dirname, 'dist'),
    },
  },
}

以上配置告知webpack-dev-server,将dist目录下的文件serve到localhost:8080(默认)下。(serve,将资源作为server的可访问文件)
webpack-dev-server会从output.path中定义的目录为服务提供bundle文件,文件通过http://[devServer.host]:[devServer.port]/[output.publicPath]/[output.filename]进行访问

配置的时候注意node版本问题。

webpack-dev-server v4.0.0+ 要求 node >= v12.13.0、webpack >= v4.37.0(但是我们推荐使用 webpack >= v5.0.0)和 webpack-cli >= v4.7.0。

webpack-dev-middleware

webpack-dev-middleware 是一个封装器(wrapper),它可以把 webpack 处理过的文件发送到一个server。(根据名字middleware,是个中间件) 示例给出了一个与express server配合使用的示例 在webpack.config.js

output: {
  publicPath: '/',
},
// server.js
const express = require('express');
const webpack = require('webpack');
const webpackDevMiddleware = require('webpack-dev-middleware');

const app = express();
const config = require('./webpack.config.js');
const compiler = webpack(config);

// 告知 express 使用 webpack-dev-middleware,
// 以及将 webpack.config.js 配置文件作为基础配置。
app.use(
  webpackDevMiddleware(compiler, {
    publicPath: config.output.publicPath,
  })
);

// 将文件 serve 到 port 3000。
app.listen(3000, function () {
  console.log('Example app listening on port 3000!\n');
});

添加npm script

{
  "scripts": {
    "server": "node server.js",
  }
}

我的输出和官网给的例子不一样,但是控制台打印的内容一样,页面上也能正常展示。我猜测应该是配置成功了。
终于明白了。。这个东西就是把文件serve到3000端口,之前的示例都是8080端口。

代码分离

  • 代码分离这个特性,可以把代码分离到不同的bundle中,然后按需加载或并行加载这些文件。
  • 代码分离可以用于获取更小的bundle,以控制资源加载优先级,使用合理,会极大影响加载时间

常用的三种方法

  • 入口起点: 使用entry配置手动分离代码
  • 防止重复:使用EntryDependencies或者SplitChunksPlugin去重和分离chunk
  • 动态导入:通过模块的内联函数调用来分离代码

入口起点

  • 优点:简单直观

  • 缺点:手动配置,存在隐患

  • 做法: 新建文件,配置多个入口

entry: {
  index: './src/index.js',
  another: './src/another-module.js',
},

打包时的输出信息能看出index.bundle.js和another.bundle.js的体积都很大,因为重复模块都被引入了。
隐患:

  • 如果入口之间包含一些重复的模块,那些重复的模块都会被引入各个bundle中;
  • 不够灵活,不能动态地将核心应用程序逻辑中的代码拆分出来

防止重复

入口依赖

配置dependOn option选项,可以在多个chunk之间共享模块 webpack.js

entry: {
  index: {
    import: './src/index.js',
    dependOn: 'shared',
  },
  another: {
    import: './src/another-module.js',
    dependOn: 'shared',
  },
  shared: 'lodash',
},

还需要增加optimization配置项

optimization: {
  runtimeChunk: 'single',
},

这时候生成四个bundle文件,除shared.bundle.js,index.bundle.js 和 another.bundle.js 之外,还生成了一个 runtime.bundle.js

避免使用多入口的入口:`entry: { page: ['./analytics', './app'] }`,在使用async脚本标签时,会有更好的优化以及一致的执行顺序。

SplitChunksPlugin

该插件可以将公共的依赖模块提取到已有的入口chunk,或者提取到一个新生成的chunk。 把上一步的配置删除。在基础配置中增加:

optimization: {
  splitChunks: {
    chunks: 'all',
  },
},

这时候打包出来的有三个bundle,在index.bundle.js和another.bundle.js的基础上还有一个以vendors-node_modules开头的文件

社区还提供了一个对于代码分离很有帮助的plugin和loader

  • mini-css-extract-plugin: 用于将 CSS 从主应用程序中分离。

动态导入

涉及动态导入时,webpack提供了两个类似的技术:

  • 第一种:import
  • 第二种:require.ensure

不需要another.bundle.js了,修改index.js

function getComponent() {
  const element = document.createElement('div');
  return import('lodash')
    .then(({ default: _ }) => {
      const element = document.createElement('div');
      element.innerHTML = _.join(['Hello', 'webpack'], '');
      return element;
    }).catch((error => 'An error occurred while loading the component'));
};
getComponent().then((component) => {
  document.body.appendChild(component);
});

需要default的原因: webpack 4 在导入 CommonJS 模块时,将不再解析为 module.exports 的值,而是为 CommonJS 模块创建一个 artificial namespace 对象。
打包后,lodash会被分离到一个单独的bundle。 也可以和async函数一起使用

async function getComponent() {
  const element = document.createElement('div');
  const { default: _ } = await import('lodash');
  element.innerHTML = _.join(['Hello', 'webpack'], '');
  return element;
};
getComponent().then((component) => {
  document.body.appendChild(component);
});

import中还可以传入动态表达式(动态表达式是什么啊?嘤嘤嘤)

预获取/预加载模块

没看呢

bundle分析

也没看呢

缓存

webpack在打包我们模块化的引用程序时,会生成一个可部署的/dist目录,然后把打包的内容放置在此目录中。
如果我们在部署新版本中不更改资源的文件名,浏览器就会认为他没有更新,就会使用他的缓存版本。 这一章节的重点在于通过必要的配置,以确保

  • webpack编译生成的文件都够被客户端缓存
  • 在文件内容变化后,能够请求到新的文件

输出文件的文件名

  • 通过替换output.filename中的substitutions设置,来定义输出文件的名称。
  • webpack 提供了一种称为 substitution(可替换模板字符串) 的方式,通过带括号字符串来模板化文件名。
  • [contenthash] substitution 将根据资源内容创建出唯一 hash。当资源内容发生变化时,[contenthash] 也会发生变化
    修改output配置中
filename: '[name].[contenthash].js',

然后输出的文件名就是xxx.一串hash.js这样了。
这里还有一个问题,webpack在入口chunk中,包含了某些boilerplate,特别是runtime和manifest。重复构建时会导致文件名改变。

提取引导模版

  • SplitChunksPlugin 可以用于将模块分离到单独的 bundle 中
  • optimization.runtimeChunk 选项将 runtime 代码拆分为一个单独的 chunk
optimization: {
  runtimeChunk: 'single',
},

推荐将第三方库提取到单独的vendor chunk文件中。(第三方库的文件很少频繁修改)。
以上的步骤,利用client的长效缓存机制,命中缓存来取消请求,并减少向server获取资源,同时保证client代码和server代码版本一致。
可以通过使用SplitChunksPlugin 插件的 cacheGroups 选项实现。 webpack.config.js

  optimization: {
    moduleIds: 'deterministic',
    runtimeChunk: 'single',
    splitChunks: {
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          chunks: 'all',
        }
      }
    }
  }

这时候main中不包含node_modules的代码,node_modules的代码被单独打包到vendors中。(我记得上一章节用了splitchunks就会把引入的包拆分开)

模块标识符

场景:新增模块引入,只希望main的hash变化,但其实main、vendor、runtime都会变化。(main和runtime的变化都是在预期内的,vendor不该变化) 原因:每个 module.id 会默认地基于解析顺序(resolve order)进行增量。也就是说,当解析顺序发生变化,ID 也会随之改变。

  • main bundle 会随着自身的新增内容的修改,而发生变化。
  • vendor bundle 会随着自身的 module.id 的变化,而发生变化。
  • manifest runtime 会因为现在包含一个新模块的引用,而发生变化。
    main和manifest发生变化是在预期内的,vendor发生变化是不符合预期的。修复这个问题, 在optimization.moduleIds设置为deterministic
optimization: {
  moduleIds: 'deterministic',
}

这时候添加新的本地依赖,vendors都不会再变

问题

我自己实践的过程中,

  • 在输出文件名那一步,重复构建并文件名并不会改变。
  • 模块标识符,新版本好像不设置也可以。新增本地依赖,打包的输出里只打印变化的文件,没变化的文件直接不打印了。(我用的是webpack版本是5,指南应该是4)

创建Library

webpack除了可以打包应用程序,还可以用于打包js library。

创建一个library

和创建应用程序步骤一样

Expose the Library

通过 output.library 配置项暴露从入口导出的内容

output: {
  library: "webpackNumbers",
},

这样子通过script标签被引用,如果要运行在CommonJS、AMD、Node.js等环境 更新配置项为('umd'好像是个通用的配置项)

library: {
  name: 'webpackNumbers',
  type: 'umd',
}
  • tips:不推荐使用array作为库的entry

问题

创建好的库,不知道怎么引用。。。只会script的方式

外部化lodash

在一些场景中,我们更倾向于把lodash当作peerDependency。consumer(使用者)应该已经安装过ladash了。因此,放弃外部控制此外部Library,而是将控制权让给使用Library的consumer。通过externals配置来完成。 webpack.config.js新增

externals: {
  lodash: {
    commonjs: 'lodash',
    commonjs2: 'lodash',
    amd: 'lodash',
    root: '_',
  },
},

这意味着你的library需要一个名为lodash的依赖,这个依赖在consumer环境中必须存在且可用。

外部化的限制

对于想要实现从一个依赖中调用多个文件的那些Library。

import A from 'library/A';
import B from 'library/B';

无法通过在externals中指定整个library的方式,将他们从bundle中排除。而是需要逐个或使用一个正则表达式来排除他们。

modules.exports = {
  externals: [
    'library/one',
    'library/two',
    // 正则
    // /^library\/.+$/,
  ]
}

最终步骤

与生产环境指南中提到的步骤相结合,来优化输出结果。 将生成 bundle 的文件路径,添加到 package.json 中的 main 字段中。

{
  ...
  "main": "dist/webpack-numbers.js",
  ...
}

或者将其添加为标准模块

{
  ...
  "module": "src/index.js",
  ...
}

这里的 key(键) main 是参照 package.json 标准,而 module 是参照 一个提案,此提案允许 JavaScript 生态系统升级使用 ES2015 模块,而不会破坏向后兼容性。

  • 以后可以学习一下从库的创建到发布过程

环境变量

  • webpack命令行环境配置的 --env参数,允许传入任意数量的环境变量;
npx webpack --env goal=local --env production --progress
  • webpack.config.js中可以访问到这些变量。
  • 使用env变量,必须将module.exports转换成一个函数;
const path = require('path');

module.exports = (env) => {
  // Use env.<YOUR VARIABLE> here:
  console.log('Goal: ', env.goal); // 'local'
  console.log('Production: ', env.production); // true

  return {
    entry: './src/index.js',
    output: {
      filename: 'bundle.js',
      path: path.resolve(__dirname, 'dist'),
    },
  };
};
  • 如果设置 env 变量,却没有赋值,--env production 默认表示将 env.production 设置为 true

问题

翻看了客服系统的webpack配置,webpack的配置被写入文件,然后node该文件启动。配置也不是直接写的,而是new各种对象出来的。。。

构建性能

通用环境

使用最新版本(webpack、node)

将loader应用于最少数量的必要模块

通过include字段,将loader应用在实际需要的模块。

const path = require('path');
module.exports = {
  //...
  module: {
    rules: [
      {
        test: /\.js$/,
        include: path.resolve(__dirname, 'src'),
        loader: 'babel-loader',
      },
    ],
  },
};
  • 使用babel-loader的时候要安装三个模块
npm install babel-loader @babel/core @babel/preset-env --save-dev
  • 不写include构建时间 1397ms
  • 加上include构建时间 700ms

每个额外的loader/plugin都有其启动时间,尽量减少使用工具

不加loader:398ms

提高解析速度

  • 减少 resolve.modules, resolve.extensions, resolve.mainFiles, resolve.descriptionFiles 中条目数量,因为他们会增加文件系统调用的次数。(现在还没用resolve呢。。。)
  • 如果你不使用 symlinks(例如 npm link 或者 yarn link),可以设置 resolve.symlinks: false。(也没用过symlinks)
  • 如果你使用自定义 resolve plugin 规则,并且没有指定 context 上下文,可以设置 resolve.cacheWithContext: false

使用dllPlugin为改变不频繁的代码生成单独编译的结果;提高程序的编译速度,增加了构建过程的复杂度

减少编译结果的大小,提高构建性能,保持chunk体积小

  • 使用数量更少、体积更小的library
  • 多页面程序用SplitChunksPlugin
  • 多页面程序用SplitChunksPlugin,开启async模式
  • 移除未引用的代码
  • 只编译当前正在开发的那些代码

thread-loader 可以将非常消耗资源的 loader 分流给一个 worker pool。

  • 不要使用太多worker,nodejs的runtime和loader都有启动开销。最小化worker和main process之间的模块传输。进程间通讯非常消耗资源。

在webpack配置中使用cache选项,使用package.json中的postinstall清除缓存目录

好像cache: true是比cache: false在demo中快那么十几ms。。。

自定义plugin/loader

没用过自定义plugin/loader

将ProgressPlugin从webpack中删除,可以缩短构建时间.但是它也可能不会为快速构建提供太多价值。

开发环境

增量编译

  • 增量编译我的理解是根据模块依赖图,只对变化的部分进行编译,不变化的就用之前编译好的在内存里的。
  • 使用内置的watch mode;内置的watch mode会记录时间戳并将此信息传递给compilation以使缓存失效。
  • 在某些环境配置中,watch mode会回退到poll mode(轮询模式)。监听许多文件会导致CPU大量负载。可以通过watchOptions.poll开增加轮询时间。
    • watchOptions.poll通过传递 true 开启 polling,或者指定毫秒为单位进行轮询。
    • 把poll的值设置的足够大,更新内容在这段时间里就监听不到了

在内存中编译(在内存中编译、serve资源)

  • webpack-dev-server
  • webpack-hot-middleware
  • webpack-dev-middleware

stats.toJson加速

  • 避免获取stats对象的部分内容。webpack4默认使用stats.toJson()输出大量数据;webpack-dev-server在v3.1.3以后的版本,最小化每次增量构建从stats对象中获取的数据量。

devtool

  • "eval" 具有最好的性能,但并不能帮助你转译代码。
  • 使用 cheap-source-map 变体配置来提高性能
  • 使用 eval-source-map 变体配置进行增量编译
  • 最佳选择:eval-cheap-module-source-map
    使用eval-cheap-module-source-map 用了320ms/340ms,用eval-source-map用了385ms/390ms

避免在生产环境下才会用到的工具

最小化entry chunk

  • webpack只会在文件系统中输出已经更新的chunk,对于某些配置项(HMR,ouput.chunkFilename的[name]/[contenthash],[fullhash])来说,除了对已经更新的chunk无效外,对entry chunk也无效。
  • 确保生产entry chunk时,尽量减少其体积以提高性能。
  • (eg:为运行时代码创建runtime chunk)
    加上runtimeChunk: true比不加好像快了那么十几ms

webpack执行额外的算法任务,来优化输出结果的体积和加载性能,这些优化用于小型代码库,但是在大型代码库中十分消耗性能。

webpack会在输出的bundle中生成路径信息,打包数千个模块的项目中,会造成垃圾回收性能压力。

  • 在 options.output.pathinfo 设置中关闭。
  • 这个东西会在bundle中新增一点点东西
    设置为false的时候 pathinfofalse.png 设置为true的时候 pathinfotrue.png

Node.js v8.9.10 - v9.11.1中的ES2015Map和Set实现,存在性能回退。

ts-loader

  • 为ts-loader传入transpileOnly 选项,缩短ts-loader的构建时间。
  • 使用此选项会关闭类型检查,如果要再次开启类型检查,使用ForkTsCheckerWebpackPlugin插件,使用此插件会将检查过程移至单独的进程,加快ts类型检查和ESlint插入的速度。

生产环境

优化代码质量比构建性能更重要

创建多个compilation

为什么需要多个compilation?

  • parallel-wepack

source maps

相当消耗资源

工具相关问题

以下工具存在会降低构建性能

babel

减少preset/babel的数量

ts

  • 使用fork-ts-checker-webpack-plugin进行类型检查
  • 配置loader跳过类型检查
  • 使用 ts-loader 时,设置 happyPackMode: true / transpileOnly: true。

sass

  • node-sass 中有个来自 Node.js 线程池的阻塞线程的 bug。 当使用 thread-loader 时,需要设置 workerParallelJobs: 2。

内容安全策略

没学

开发-vagrant

没学

依赖管理

没学

安装

就是咋安装webpack,根据项目需要安装就行,全局安装很不方便。

模块热替换

就是那里改变更新哪里。虽然示例给的很简单,但是正式的项目中关于热替换的配置还是挺多的。 提供入口的方法始终没有走通。(直接配置项、node API都没有走通)

启用HMR

在devServer配置项中新增hot: true 在index.js中新增

if (module.hot) {
  module.hot.accept('./print.js', function() {
    console.log('Accepting the updated printMe module!');
    printMe();
  })
}

这时候修改print.js的内容,控制台中printMe的打印内容就会变

通过Node.js API

指南中给的例子好像不太行,还有个配置项拼错了。dev-server.js的内容酱紫就可以正常serve出来。

const path = require('path');
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const DevServer = require('webpack-dev-server');
const config = {
  mode: 'development',
  entry: [
    // Runtime code for hot module replacement
    'webpack/hot/dev-server.js',
    // Dev server client for web socket transport, hot and live reload logic
    // 'webpack-dev-server/client?http://localhost:8080/?hot=true&live-reload=true',
    'webpack-dev-server/client?http://localhost:8080/',
    // 'webpack-dev-server/client/index.js?hot=true&live-reload=true',
    // Your entry
    './src/index.js',
  ],
  plugins: [
    new HtmlWebpackPlugin({
      title: 'development',
    }),
    // Plugin for hot module replacement
    new webpack.HotModuleReplacementPlugin(),
  ],
  output: {
    filename: '[name].bundle.js',
    path: path.resolve(__dirname, 'dist'),
    clean: true,
  }
};
const compiler = webpack(config);
// `hot` and `client` options are disabled because we added them manually
const devServerConfig = {
  static: {
    directory: path.join(__dirname, 'dist'),
  },
  hot: true,
  client: false,
};
const server = new DevServer(devServerConfig, compiler);
server.listen(8080);

问题

刚才的情况,虽然printMe函数更新了,但是点击按钮还是打印旧的函数。这是因为onClick事件处理函数仍然绑定在旧printMe上。重新绑定一下:

let element = component();
document.body.appendChild(element);
if (module.hot) {
  module.hot.accept('./print.js', function () {
    console.log('Accepting the updated printMe module!');
    document.body.removeChild(element);
    element = component();
    document.body.appendChild(element);
  })
}

HMR加载样式

把loader配置上就行了

tree shaking

tree shaking是一个术语,用于描述移除JavaScript上下文中的未引用代码(dead-code)。它依赖于ES2015模块与法的静态结构特性,例如import和export。这个术语的概念实践是由ES2015模块打包工具rollup普及起来的。

将文件标记为side-effect-free

  • package.json的"sideEffects"属性。
  • 如果所有代码都不含负副作用,就设置为false
  • 如果确实有副作用就写进数组,数组里支持正则
  • 还可以在module.rule配置选项中设置"sideEffects"

treeShaking和sideEffects

  • sideEffects 和 usedExports(更多被认为是 tree shaking)是两种不同的优化方式。
  • sideEffects 更为有效 是因为它允许跳过整个模块/文件和整个文件子树。
  • usedExports 依赖于 terser 去检测语句中的副作用。它是一个 JavaScript 任务而且没有像 sideEffects 一样简单直接。而且它不能跳转子树/依赖由于细则中说副作用需要被评估。尽管导出函数能运作如常,但 React 框架的高阶函数(HOC)在这种情况下是会出问题的。

将函数标记为无副作用

/*#__PURE__*/

压缩输出结果

mode: 'production'

结论

用到treeShaking的优势

  • 使用ES2015模块语法(即import和export)
  • 确保没有编译器将ES2015模块语法转换为CommonJS(@@babel/preset-env的默认行为)
  • 在package.json中添加"sideEffects"属性
  • 使用mode为"production"的配置项(启用更多优化项,包括代码压缩和tree shaking)

问题

感觉自己只是知道了tree shaking是啥意思,并没有真正的理解在不同的场景下它的配置。

生产环境

配置

  • 在开发环境下,一般需要强大的source map和一个有着live reloading或hot module replacement能力的localhost server。
  • 在生产环境下,关注点在于压缩bundle、更轻量的source map、资源优化。
  • 为每个环境编写彼此独立的webpack配置,遵循不重复原则(Don't repeat yourself),保留一个common配置。

NPM scripts

修改一个scripts

指定mode

许多 library 通过与 process.env.NODE_ENV

  • NODE_ENV 是一个由 Node.js 暴露给执行脚本的系统环境变量
  • 在构建脚本webpack.config.js中process.env.NODE_ENV并没有被设置为'production'
  • 任何位于 /src 的本地代码都可以关联到 process.env.NODE_ENV 环境变量

压缩

  • 生产环境下默认使用TerserPlugin
  • 还有个选项ClosureWebpackPlugin

源码映射

  • 对于本指南,我们将在生产环境中使用 source-map 选项
  • 避免在生产中使用 inline-*** 和 eval-***,因为它们会增加 bundle 体积大小,并降低整体性能。

压缩CSS

没看呢,用的是MiniCssExtractPlugin这个插件

cli替代选项

懒加载

  • 懒加载或者按需加载。
  • 把你的代码在一些逻辑断点处分离开,然后在一些代码块中完成某些操作后,立即引用或即将引用一些新的代码块。
  • 加快了应用的初始加载速度,减轻了它的总体体积。

示例

示例好像只是换了点击事件的写法,并没有在设置上有什么变化

button.onclick = e => import(/*webpackChunkName: "print"*/'./print').then(module => {
    const print = module.default;
    print();
  });

可以实现,刚进入页面不加载print文件,点击按钮后才加载print文件的内容。但是真是的项目里,没见过谁这么用啊。

框架

俺只会react,还没看呢。应该去看一下的。。。

ECMAScript模块

ECMAScript模块(ESM)

将模块标记为ESM

在package.json设置

/*强制package.json下的文件使用ECMAScript*/
{
  "type": "module"
}
/*强制package.json下的文件使用CommonJS*/
{
  "type": "commonjs"
}
  • 文件还可以通过.mjs.cjs扩展名来设置模块类型。.mjs将它们强制置为ESM,.cjs将它们强制置为CommonJS。
  • 除了模块格式外,将模式标记为ESM还会影响解析逻辑、操作逻辑和模块中的可用符号。
  • 导入模块在ESM中更为严格,导入相对路径的模块必须包含文件名和扩展名;支持导入包。
  • non-ESM仅能导入default导出的模块,不支持命名导出的模块。 CommonJS语法不可用。
  • HMR使用import.meta.webpackHot代替module.hot

shimming预置依赖

  • 一些第三方的库会用到一些全局依赖,这些 library 也可能会创建一些需要导出的全局变量。这些不符合规范的模块就是shimming发挥作用的地方。
  • 另外一个地方:当你希望polyfill扩展浏览器能力,来支持到更多用户时。在这种情况下,将这些polyfills提供给需要修补的浏览器(也就是按需加载)。

预置变量依赖

新增plugins

new webpack.ProvidePlugin({
  _: 'lodash',
}),

在index.js就可以直接用_了。
还可以使用 ProvidePlugin 暴露出某个模块中单个导出,通过配置一个“数组路径”。

new webpack.ProvidePlugin({
  join: ['lodash', 'join'],
}),

这是在文件里直接用join就行

细粒度shimming

一些遗留模块依赖的this指向的是window对象。 在本例中,当模块运行在CommonJS上下文中时,this指向的是module.exports
使用imports-loader覆盖this指向:

  module: {
    rules: [
      {
        test: require.resolve('./src/index.js'),
        use: 'imports-loader?wrapper=window',
      },
    ],
  },

全局Exports

在src目录下新建global.js

const file = 'blah.txt';
const helpers = {
  test: function () {
    console.log('test something');
  },
  parse: function () {
    console.log('parse something');
  },
};

使用exports-loader

  module: {
    rules: [
      {
        test: require.resolve('./src/global.js'),
        use: 'exports-loader?type=commonjs&exports=file,multiple|helpers.parse|parse',
      },
    ],
  },

这样可以将一个全局变量作为一个普通模块来导出。(file导出为file,helpers.parse导出为parse)。在 entry 入口文件中(即 src/index.js),可以使用 const { file, parse } = require('./globals.js');,可以保证一切将顺利运行。

加载polyfill

  • polyfill是一块代码,用来为旧浏览器提供它没有原生支持的较新的功能。
  • polyfill基于自身执行,并且是在基础代码执行之前。
  • 这种方式优先考虑正确性,而不考虑bundle体积大小。
  • 为了安全和可靠,polyfill/shim必须运行于所有其他代码之前,而且需要同步加载。或者说,需要在所有polyfill/shim加载之后,再去加载所有应用程序代码。
  • 社区中存在误解,即现代浏览器“不需要”polyfill,或者polyfill/shim仅用于添加缺失功能。实际上,它们通常用于修复损坏实现(repair broken implementaion)。
  • 即使是最现代的浏览器中,也会出现这种情况。因此最佳实践仍旧是,不加选择地和同步地加载所有polyfill/shim,尽管这回导致bundle体积成本。

场景:fetch数据,有的老浏览器没有fetch 在index.html的head中写入

  <script>
    const modernBrowser = 'fetch' in window && 'assign' in Object;
    if (!modernBrowser) {
      const scriptElement = document.createElement('script');
      scriptElement.async = false;
      scriptElement.src = '/polyfills.bundle.js';
      document.head.appendChild(scriptElement);
    }
  </script>

entry入口文件中写入

fetch('https://jsonplaceholder.typicode.com/users')
  .then((response) => response.json())
  .then((json) => {
    console.log("We retrieved some data! AND we're confident it will work on a variety of browser distributions.");
    console.log(json);
  })
  .catch((error) =>
    console.error('Something went wrong when fetching this data: ', error)
  );

这个东西我没法验证。。。chrome浏览器是有fetch的。。。

进一步优化

babel-preset-env package 通过 browserslist 来转译那些你浏览器中不支持的特性。这个 preset 使用 useBuiltIns 选项,默认值是 false,这种方式可以将全局 babel-polyfill 导入,改进为更细粒度的 import 格式/。

Node内置

像process这种Node内置模块,能直接根据配置文件进行正确的polyfill,而不需要任何特定的loader或者plugin。

其他工具

我觉得这种情况就还是找个别的库用吧。。。

有些遗留模块没有AMD/CommonJS版本,但你也想把他们加进dist,使用noParse来标出这个模块。这样webpack可以引入这些模块,但是不进行转化(parse),以及不解析(resolve)require()和import语句。这种语法还会提高构建性能。

noParse设置

module.exports = {
  module: {
    noParse: /jquery|lodash/,
  }
}

或者

module.exports = {
  module: {
    noParse: (content) => /jquery|lodash/.test(content),
  }
}

任何需要AST的功能(ProvidePlugin)都不起作用

一些模块支持多种模块格式,例如一个混合有AMD、CommonJS和legacy的模块。在这种大多数这种模块下,会首先检查define,然后使用一些怪异代码导出一些属性。
解决方法:通过imports-loader设置additionalCode=var%20define%20=%20false;来强制CommonJS路径

Tips

shimming是一个库,他将新的API引入到一个旧的环境中,而且仅靠旧的环境中已有的手段实现。polyfill就是用在浏览器API上的shimming。 我们通常的做法是先检查当前浏览器是否支持某个API,如果不支持的话就按需加载对应的polyfill,然后旧浏览器就都可以用这个API了。

Typescript

基础配置

执行以下命令安装Typescript compiler和loader

npm install --save-dev typescript ts-loader

在项目中添加tsconfig.json

{
  "compilerOptions": {
    "outDir": "./dist/",
    "noImplicitAny": true,
    "module": "es6",
    "target": "es5",
    "jsx": "react",
    "allowJs": true,
    "moduleResolution": "node"
  }
}

修改webpack.config.js

const path = require('path');

module.exports = {
  entry: './src/index.ts',
  module: {
    rules: [
      {
        test: /\.(tsx|ts)?$/,
        use: 'ts-loader',
        exclude: /node_modules/,
      },
    ],
  },
  resolve: {
    extensions: ['.tsx', '.ts', '.js'],
  },
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist'),
  },
};

webpack会直接从./index.ts进入,通过ts-loader加载所有的.ts.tsx文件,并在当前目录输出bundle.js文件。
引入lodash文件的时候,要多安装一个类型声明文件

npm install --save-dev @types/lodash

引入的时候

import * as _ from 'lodash';

Loader

  • ts-loader依赖于tsconfig.json配置。确保没有将module设置成CommonJS模式,否则webpack将不会tree-shake你的代码。
  • 如果你已经使用bable-loader转译你的代码,你可以不使用额外的loader,使用@babel/preset-typescript然后让babel同时处理js和ts。
  • 与ts-loader,底层的@babel/plugin-transform-typescript不执行任何类型检查。

source maps

在tsconfig.json里添加

"sourceMap": true,

webpack.config.js里也要加

module.exports = {
  devtool: 'inline-source-map',
}

第三方库

从npm安装第三方库时,一定要记得同时安装此library的类型声明文件。就和刚才lodash文件一样

导入其他资源

没看呢,还不会写ts呢,学会了ts在写。

Web Worker

没看懂这是干吗呢。。。。

渐进式应用程序

现在,我们并没有运行在离线环境下

就是现在起的服务,停了之后程序就不能用了。。。
要实现的是停了服务程序还能用

添加wordbox

这一部分的具体实现还是和文档里有很大差别的 首先安装插件

npm install workbox-webpack-plugin --save-dev

在webpack.config.js中使用

const WorkboxPlugin = require('workbox-webpack-plugin');

new WorkboxPlugin.GenerateSW({
  clientsClaim: true,
      skipWaiting: true,
}),

或者

const { GenerateSW } = require('workbox-webpack-plugin');

new GenerateSW({
  clientsClaim: true,
      skipWaiting: true,
}),

然后执行npm run build输出如下

workboxoutput.png

注册Service Worker

修改index.js代码

import _ from 'lodash';
import printMe from "./print";

if ('serviceWorker' in navigator) {
  window.addEventListener('load', () => {
    navigator.serviceWorker.register('/service-worker.js').then(registration => {
      console.log('sw registered: ', registration);
    }).catch(registrationError => {
      console.log('SW registration failed: ', registrationError);
    });
  });
}

这时候先构建npm run build在启动npm run start。就可以看控制台如下 workboxconsole.png 有时候workerbox也会输出信息 swregistered.png 这样就实现了停掉server以后,再刷新页面,程序还在正常运行。
在实践的过程中还遇到了个问题:
index.js请求的路径写错了,修改了之后重新打包然后起服务,发现报错信息中路径还是错的。这时候清空缓存并硬性重新加载,就可以获取到最新的数据了。所以我觉得service worker就是把编译好的文件缓存在浏览器里,你不强制刷新他他都不会自己更新。

公共路径

这一章学的迷迷糊糊的。。。关键是还没跑通。。。

示例

  • output.publicPath 指定应用程序中所有资源的基础路径。
  • 发送到output.path目录的每个文件,都将从output.publicPath位置引用。(就是path是保持文件的路径,publicPath是引用文件的路径)

基于环境设置

import webpack from 'webpack';

// 尝试使用环境变量,否则使用根路径
const ASSET_PATH = process.env.ASSET_PATH || '/';

export default {
  output: {
    publicPath: ASSET_PATH,
  },

  plugins: [
    // 这可以帮助我们在代码中安全地使用环境变量
    new webpack.DefinePlugin({
      'process.env.ASSET_PATH': JSON.stringify(ASSET_PATH),
    }),
  ],
};
  • 加上DefinePlugin中的设置才能在src的文件中获取到process.env.ASSET_PATH。否则是获取不到的。但是这样也说不通。
  • 但是process.env.ASSET_PATH这个变量。。。应该是不需要设置就能获取到的吧。。。

在运行时设置

  • 在运营时设置publicPath.__webpack_public_path__是webpack暴露的全局变量。
__webpack_public_path__ = process.env.ASSET_PATH;
一个问题

如果在entry中用的是ES2015 module import,会在import之后进行__webpack_public_path__的赋值,这种情况下,必须将public path赋值移至一个专用的文件,然后放在entry.js的上面

// entry.js
import './public-path';
import './app';

Automatic publicPath

  • 有可能你事先不知道 publicPath 是什么,webpack 会自动根据 import.meta.urldocument.currentScriptscript.src 或者 self.location 变量设置 publicPath。你需要做的是将 output.publicPath 设为 'auto':
  • 不支持document.currentScript的浏览器引入polyfill

集成

大概就是webpack如何跟任务运行工具结合使用。

资源模块

允许使用资源文件,无需配置loader

Resource资源

module: {
  rules: [
    {
      test: /\.png/,
      type: 'asset/resource'
    }
  ]
},

配置完以后就可以import了

自定义文件输出名

默认情况下,asset/resource 模块以 [hash][ext][query] 文件名发送到输出目录。
自己写的demo里生成的是酱紫的。·ac19192cb83207a2522b.png,但这其中哪个是hash哪个是ext哪个是query我也不知道。
两种方式自定义文件生成名

  • output.assetModuleFilename修改模版字符串
output: {
  assetModuleFilename: 'images/[hash][ext][query]',
}
  • 将某些资源发送到指定目录
  module: {
    rules: [
      {
        test: /\.html/,
        type: 'asset/resource',
        generator: {
          filename: 'static/[hash][ext][query]',
        }
      }
    ]
  },

结果就是src下的.html后缀的文件都被发送到dist/static文件下。
Rule.generator.filename 与 output.assetModuleFilename 相同,并且仅适用于 asset 和 asset/resource 模块类型。

inline资源

酱紫设置

  module: {
    rules: [
      {
        test: /\.png/,
        type: 'asset/inline',
      }
    ]
  },

import的资源回变成data URIs。
data URIs由四部分组成

data:[<mediaType>][;base64],<data>

自定义data URI生成器

const svgToMiniDataURI = require('mini-svg-data-uri');

  module: {
    rules: [
      {
        test: /\.svg/,
        type: 'asset/inline',
        generator: {
          dataUrl: content => {
            content = content.toString();
            return svgToMiniDataURI(content);
          }
        }
      }
    ]
  },

source资源

module: {
    rules: [
      {
        test: /\.txt/,
        type: 'asset/source',
      }
    ]
  },

正常import就可以了

URL资源

通用类型资源

type:'asset'时,webpack按照默认条件,自动在resource和inline之间进行选择。小于8kb的文件,将会视为inline模块类型,否则被视为resource模块类型。 配置项Rule.parser.dataUrlCondition maxSize 如果一个模块源码大小小于maxSize,那么模块会被当作Base64编码的字符串注入包中,否则模块文件会被生成到输出的目标目录。

变更内联loader语法

现在建议去掉内联loader的语法。 oneof规则,只要能匹配到一个即可退出匹配。

entry高级用法

  • 不使用import样式文件的应用程序中,使用值数组结构的entry。并且传入不同类型的文件,可以实现将CSS和JS(其他)文件分离在不同bundle。
  • 在生产(prodution)模式中使用MiniCssExtractPlugin
mode.exports = {
  mode: 'production',
  entry: {
    home: ['./src/home.js', './src/home.scss'],
    account: ['./src/account.js', './src/account.scss'],
  },
  module: {
    rules: [
      {
        test: /\.scss$/,
        use: [
          // fallback to style-loader in development
          process.env.NODE_ENV !== 'production'
            ? 'style-loader'
            : MiniCssExtractPlugin.loader,
          'css-loader',
          'sass-loader',
        ],
      },
    ],
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: '[name].css',
    }),
  ],
}

这样能dist文件夹里生成四个文件

  • home.js
  • home.css
  • account.js
  • account.css