webpack4 配置笔记

前端模块打包工具

需要解决的问题:

  • 新特性代码编译
  • 模块化Javascript打包
  • 支持不同类型的资源模块

前端模块化工具

打包工具解决的是前端整体的模块化,并不单指JS模块化

  • webpack
    • 核心特性
      • 模块加载器Loader
      • 代码拆分 Code Splitting
      • 资源模块 Asset Module
  • parcel
  • rollup

webpack配置

webpack会自动地从src/index.js开始打包。

但是我们也可以通过配置,自定义入口文件 webpack.config.js

webpack工作模式

  1. 在打包指令中添加mode
shell 复制代码
yarn webpack --mode development
yarn webpack --mode production # 默认
yarn webpack --mode none // webpack以最原始的方式打包,不会做任何额外处理
  1. 在webpack配置文件中添加mode属性

    js 复制代码
    // webpack.config.js
    module.exports = {
        mode: 'production', // 默认
        // mode: 'development',
        // mode: 'none',
    }

webpack打包结果运行原理

webpack打包后的bundle.js,里面的代码是一个立即执行函数,入参为一个数组,数组每项表示一个模块函数。入口文件所对应的模块函数位于数组第一个,对应索引为0

js 复制代码
(function(modules) {
    var installModules = {};
    
    var __webpack_require__ = (moduleId) {
        // 判断是否是缓存的模块,是缓存模块就返回缓存模块
        if(installModules[moduleId]) {
            return installModules[moduleId].exports;
        }
        
        // 创建一个新模块,并把它放入缓存中
        var module = (installModules[moduleId] = {
           i: moduleId,
           l: false,
           exports: {}
        });
        
        // 执行模块函数
        modules[moduleId].call(
            module.exports,
            module, 
            module.exports, 
            __webpack_require__
        );
        
        // 标记模块已经加载
        module.l = true;
        
        // 返回模块的导出成员
        return module.exports;
   }
   // 立即执行入口文件,并返回
   return __webpack_require__(0);
})([
    function(module, exports, require) {
        var __webpack_import_foo_js__ = require(1);
        __webpack_import_foo_js__['a']();
    },
    function(module, exports, require) {
        exports['a'] = foo;
        
        var foo = function() {
            console.log('This is foo.js');
        }
    }
])

立即执行函数内部自定义实现了一个__webpack_require__函数,因为函数内不支持ESM,但支持以CJS规范引入模块,所以自定义了__webpack_require__函数以遵循CJS规范,实现了CJSrequiremodule.exports

立即执行函数体内立即执行了下标为0的入口模块函数:

js 复制代码
// 立即执行入口文件,并返回 
return __webpack_require__(0);

__webpack_require__接收到参数moduleId,内部自定了一个module对象,存放着exports属性,__webpack_require__函数内部会调用执行modules[moduleId].call(),也就是最外层数组索引对应的模块函数,返回值是module.exports

js 复制代码
var __webpack_require__ = function(moduleId) {
    var module = {
        exports: {},
    };
    
    modules[moduleId].call(
        module.exports,
        module,
        module.exports,
        __webpack_require__,
    )
    
    return module.exports;
}

来到modules[0]这里:

js 复制代码
function(module, exports, require) {
    var __webpack_import_foo_js__ = require(1);
    __webpack_import_foo_js__['a']();
},

里面调用了require(1),那也就是调用了__webpack_require__(1),即modules[1]被执行:

js 复制代码
function(module, exports, require) {
    exports['a'] = foo;

    var foo = function() {
        console.log('This is foo.js');
    }
}

回看modules[0]里面,用__webpack_import_foo_js__接收require(1)的返回值(也就是__webpack_require__(1)的返回值)module.exports,而__webpack_require__(1)被执行后,__webpack_require__(1)的返回值module.exports

js 复制代码
module: {
    exports: {}
}

变成了

js 复制代码
module: {
    exports: {
        'a': foo
    },
}

foomodules[0] 获取到并执行:

js 复制代码
__webpack_import_foo_js__['a']();

这样入口模块的依赖模块就被成功获取并执行。其他依赖模块也一样的按照这个规律去被引入执行,这就是webpack打包文件的基本运行原理。

webpack样式资源模块加载

  • css-loader 把css文件转成js
  • style-loader 把css-loader的结果追加到页面上

webpack文件资源加载器

file-loader 如何工作

  1. webpack在打包过程中遇到了图片文件,然后根据配置文件中的配置,匹配到对应的文件加载器,此时文件加载器开始工作。
  2. 当前这个模块的返回值返回,对于应用来说,所需要的资源就被发布出来了。同时通过模块的导出成员拿到这个资源的访问路径
js 复制代码
function (module, exports, __webpack_require__) {
    module.exports =
      __webpack_require__.p + "04cad6aa46cde16ca68207af1e03ade6.png";
},

__webpack_require__.p表示的是webpack.config.js配置文件里面配置的output.publicPath

DataURLs与url-loader

  1. dataURLs
  • 是一种当前url就可以表示文件内容的方式,不会再发送任何的http请求
  • 协议 媒体类型和编码 文件内容
  • data:[<mediatype>][;base64],<data>
  1. 这种方式适合体积比较小的资源,体积较大的话会造成打包结果体积大,从而影响运行速度

选file-loader还是url-loader

  1. 小文件使用Data URLs,减少请求次数
  2. 大文件单独提取存放,提高加载速度
  3. 对此,url-loader可以这样配置:
js 复制代码
{
  test: /.png$/,
  use: {
    loader: 'url-loader',
    options: {
      limit: 10 * 1024, // 10 KB
    }
  }
}
  • 超过10 KB文件单独提取存放,这时还是需要安装file-loader的,因为url-loader会去调用它
  • 小于10 KB文件转换为Data URLs嵌入代码中

webpack与ES2015

  • webpack默认就能处理importexport,但是webpack未能自动编译js代码,webpack只是因为模块打包需要,所以处理了importexport,未能处理ES6的其他新特性。
  • 此时需要使用babel加载器babel-loader来编译js代码。
  • babel-loader需要依赖babel其他的核心模块,所以需要安装下@babel/core,以及用于去完成具体特性转换插件的集合@babel/preset-env
  • babel-loader就会取代默认的加载器,在打包过程中就可以完成对新特性的编译转换。
js 复制代码
rules: [
  {
    test: /.js$/,
    use: {
      loader: 'babel-loader',
      options: {
        presets: ['@babel/preset-env']
      }
    }
  },
]

webpack模块的加载方式

  1. 遵循ES Modules标准的import声明
  2. 遵循CommonJS标准的require函数
  3. 部分Loader加载的非 JavaScript 也会触发资源加载
  • 例如样式代码中的@import指令和url函数
css 复制代码
@import url(reset.css);
body {
  background: #f4f8fb; 
  min-height: 100vh;
  background-image: url(icon.png);
  background-size: cover;
}
  1. HTML 代码中img标签的src属性、a标签的href属性
html 复制代码
<img src="better.png" alt="better" width="236">
<a href="better.png"></a>

webpack核心工作原理

  1. 在项目中散落的代码及资源文件,webpack会根据我们的配置,找到其中的一个文件作为打包入口,一般情况下这个文件是一个JS文件。
  2. 然后顺着我们入口文件当中的代码,根据代码中出现的import或者require之类的语句,解析推断出来这个文件所依赖的资源模块,然后分别去解析每个模块对应的依赖,最后形成了整个项目当中,所有用到的文件之间的一个依赖关系
  3. webpack会递归这个依赖树,然后找到每个节点对应的资源文件,最后根据我们配置文件当中的rules属性,找到这个模块对应的加载器,然后交给对应的加载器去加载这个模块
  4. 最后会将加载到的结果,放到bundle.js也就是我们的打包结果当中,从而实现整个项目的打包。

开发一个loader

loader专注于实现资源模块加载

js 复制代码
const marked = require('marked');

module.exports = source => {
  const html = marked(source);

  // loader需要返回js语句
  // return `module.exports = ${JSON.stringify(html)}`;

  // 也可以使用ESM的导出方式
  // return `export default ${JSON.stringify(html)}`

  // 返回 html 字符串交给下一个 loader 处理
  return html;

}

插件机制

Plugin专注于解决自动化工作

e.g.

  • 清除dist目录
  • 拷贝静态文件至输出目录
  • 压缩输出代码
    ...

自动清除输出目录插件

clean-webpack-plugin

自动生成使用bundle.js的HTML

html-webpack-plugin

通过这个插件,自动生成dist目录中的index.html文件,包括对bundle.js的引入都是自动化的无需手动去修改。

默认生成的是index.html,如果有多个html文件,可以配置多个

js 复制代码
plugins: [
    new HtmlWebpackPlugin({ filename: 'xxx.html' }),
    // 如果有多个html文件,可以配置多个 HtmlWebpackPlugin
    new HtmlWebpackPlugin({ filename: 'xxx.html' })
]

直接拷贝项目根目录文件到输出文件夹

copy-webpack-plugin

像可以把整个根目录下public的资源完整copy到dist目录,那么就可以:

js 复制代码
new CopyWebpackPlugin([
  'public'
])

开发一个 webpack 插件

  • plugin 通过钩子机制实现
  • 插件必须是一个函数或者是一个包含 apply 方法的对象
  • 插件通过在生命周期的钩子中挂载函数实现扩展
js 复制代码
// 实现一个插件,去除打包文件中的内容开头的/****/
class MyPlugin {
  apply(compiler) {
    console.log('MyPlugin启动');

    compiler.hooks.emit.tap('MyPlugin', compilation => {
      // compilation => 可以理解为此次打包的上下文
      // compilation.assets
      for(const name in compilation.assets) {
        if(name.endsWith('.js')) {
          const contents = compilation.assets[name].source();
          const withoutComments = contents.replace(/\/\*\*+\//g, '');
          compilation.assets[name] = {
            source: () => withoutComments,
            size: () => withoutComments.length,
          }
        }
      }
    })
  }
}

webpack开发体验问题

需要解决的问题:

  1. 以http Server 运行,更接近生产环境、可以请求ajax数据
  2. 自动编译、自动刷新,大大减少开发过程中重复的操作
  3. 提供 Source Map 支持,运行过程中一旦出现错误,就可以根据错误的推断信息快速定位到源代码当中的对应位置,便于调试应用

自动编译

  1. watch 工作模式
shell 复制代码
yarn webpack --watch

监听文件变化,自动重新打包

  1. 希望编译过后自动刷新浏览器

webpack开发工具-webpack-dev-server

  • 提供用于开发的 HTTP Server,集成自动编译自动刷新浏览器等功能

  • webpack-dev-server为了提高工作效率,所以并没有将打包结果写入到磁盘中, 而是将打包结果暂存到内存当中,而内部的http-server会从内存当中把文件读取出来发送给浏览器,这样一来会将少很多不必要的读写操作,从而大大提高构建速率。

  • --oepn参数可以自动唤起浏览器,打开运行地址

  • 默认会将构建结果输出的文件,全部作为开发服务器的文件,也就是说,只要是webpack输出的文件,都可以直接被访问。如果有其他静态资源文件也需要serve,就需要额外的告诉webpack-dev-server,所以需要在配置文件中配置下:

    js 复制代码
      devServer: {
        contentBase: './public' // 多个用数组。这样就可以在浏览器中访问 public 下的文件了,e.g. http://localhost:8080/better.png
      },
  • 代理API服务。本地代码运行在http://localhost:8080上面,而最终上线过后应用和API会部署到同源地址上面。那这样就会有一个非常常见的问题,在实际生产环境中,我们可以直接去访问API,但是开发环境当中就会产生跨域请求问题,并不是所有API都应该支持CORS(跨域资源共享), 如果是前后端同源部署的话,不需要开启CORS,所以就会导致开发阶段接口的跨域问题。最好的办法就是在开发服务器当中配置代理服务,也就是把API接口服务代理到本地的这个开发服务器地址

    js 复制代码
      proxy: {
        // 每一个属性是一个代理规则的配置
        // key-需要被代理的请求路径前缀,值-为这个前缀所匹配到的代理规则配置
        '/api': {
          // target-代理目标
          // http://localhost:8080/api/users -> https://api.github.com/api/users
          target: 'https://api.github.com', 
          // 重写规则
          // https://api.github.com/api/users -> https://api.github.com/users
          pathRewrite: {
            '^/api': '',
          },
          // true-不能使用localhost:8080作为GitHub的主机名,因为 localhost:8080对于GitHub来说是不认识的
          changeOrigin: true
        }
      }

changOrigin设置为true,这是因为默认代理服务器会以我们实际在浏览器当中请求的主机名,也就是localhost:8080作为代理请求的主机名,也就是我们在浏览器端对我们代理过后的这个地址发起请求,那这个请求背后肯定还需要请求到GitHub的服务器,请求的过程当中会带一个主机名,那这个主机名默认情况下使用的是我们用户在浏览器端发起请求的这个主机名,也就是localhost:8080,而一般情况下服务器那边需要根据主机名去判断,这个请求是属于哪个网站,从而把这个请求指派到对应的网站。localhost:8080对GitHub服务器来说肯定是不认识的,所以这里需要修改。changOrigin等于true的情况下,就会以实际我们代理请求这次发生的过程中的主机名去请求。

SourceMap

  • .map为文件名结尾
  • SourceMap翻译过来就是源代码地图,指的是源代码和运行代码之间的映射关系,解决了源代码与运行代码不一致所产生的问题
  • 例如在文件jquery-3.4.1.min.js结尾添加:// #sourceMappingURL=jquery-3.4.1.min.map,打开浏览器调试的时候,就可以看源代码

webpack配置SourceMap

js 复制代码
devtool: 'source-map',
  • webpack有多种SourceMap的配置方式。
  • 每种方式的效率和效果各不相同,效果好的效率低,效果差的效率高

不同 devtool 之间的差异

  1. eval
  • 把模块代码放到eval函数中执行,并通过sourceUrl标注模块文件的路径
  • 没有生成对应的SourceMap,只能定位是哪个文件出了错误
  1. eval-source-map
  • 同样也是用eval函数执行模块代码
  • 不仅可以定位模块文件路径,还可以定位到出错的行和列
  • 相比于eval,生成了SourceMap
  1. cheap-eval-source-map
  • 与eval-source-map一样,不同的是不能定位出错位置的 列 信息
  • 生成速度比较快
  1. cheap-module-eval-source-map
  • 与eval-source-map一样, 不同的是模块代码没有经过es6转换
  1. inline-source-map
  • 跟普通的source-map效果一样,

  • 不同的是,普通的source-map是以物理文件bundle.js的形式存在

    js 复制代码
    bundle.js
    bundle.js.map
    
    // 在bundle.js结尾添加 sourceMappingURL=bundle.js.map
  • 而inline-source-map的源代码以base64的形式内嵌在map文件中存在

    js 复制代码
    bundle.js.map
    
    // 在bundle.js结尾添加 sourceMapUR=data:application/json;xxxx

    这种方式一般不会被使用,因为把SourceMap放到源代码过后体积会变大

  1. hidden-source-map
  • 在浏览器开发工具中看不到SourceMap的效果,但是SourceMap文件确实有生成
  • 这是因为在代码当中没有引入这个SourceMap文件(也就是没有在代码结尾添加sourceMappingURL=xxx)
  • 这种方式在开发一些第三方包的时候会比较有用,比如 jquery
  1. nosources-source-map
  • 能看到错误出现的行列位置信息,但是不能看到源代码
  • 这种是为了在生产环境中保护源代码不被暴露

SourceMap的选择

  • 开发环境:cheap-module-eval-source-map 原因:

    1. 代码每行不会超过80个字符
    2. 代码经过Loader转换过后的差异较大
    3. 虽然首次打包速度慢,但是重写打包相对较快
  • 生产环境: none/nosource-source-map 原因:

    1. SourceMap会暴露源代码
    2. 调试是开发阶段的事情
  • 记忆技巧:

    1. eval 是否使用eval执行模块代码、只定位出错文件路径
    2. cheap 没有列信息
    3. module 不做es语法编译转换

自动刷新HMR

  • 模块热替换,指的是应用运行过程中实时替换某个模块,应用运行状态不受影响。
  • 如果没有模块热替换,自动刷新会导致整个页面状态的丢失
  • 热替换只将修改的模块实时替换至应用中

如何使用HMR

  1. HMR集成在 webpack-dev-server中
  2. 开启方式:
  • 在运行webpack-dev-server这个命令时加--hot参数
  • 配置文件开启
js 复制代码
devServer: {
  hot: true,
},
plugins: [
  new webpack.HotModuleReplacementPlugin()
]
  1. webpack的HMR并不可以开箱即用。webpack中的HMR需要手动处理模块热替换逻辑
  2. 之所以样式文件可以实现热替换,是因为样式使用的了loader,样式改变后webpack可以及时地识别替换,而js的导出内容无规律,webpack很难去判断识别。
  3. 框架的脚手架可以实现js的热替换,是因为框架本身就是有规律可循的,而不是非框架的js那样无规律,这样实现HMR会较为容易

HMR API

js 复制代码
// main.js
module.hot.accept('./heading.js', () => {
  console.log('heading 模块更新了,需要这里手动处理热替换逻辑');
});

HMR 需要注意的坑

  1. 处理HMR的代码报错会导致自动刷新,报错信息会被清除,导致无法知道哪里出错。解决,使用hotOnly
js 复制代码
devServer: {
  hotOnly: true,
}
  1. 没启用 HMR 的情况下,HMR API 报错
js 复制代码
// 使用的时候,做一个判断是否存在
if(module.hot) {
  ...
}
  1. 使用 HMR API,代码中是否多了一些与业务无关的代码? 在生产环境中不会存在 HMR API 相关的代码
js 复制代码
// 在 yan build 之后的代码:
// 在生产环境中,代码压缩之后这个条件也不会被打包到最终文件中
if(false) {}

生产环境优化

  1. 开发环境侧重提高开发体验,提供了SourceMap调试代码、HMR热替换模块功能等功能,自动往打包结果中添加了一些额外代码。这些代码对于生产环境来说是冗余的。
  2. 生产环境更注重运行效率,开发环境更注重开发效率
  3. 为了解决这些问题,webpack4推出了mode用法,为不同模式提供一些预设配置
  4. 同时webpack也建议我们为不同的工作环境创建不同的配置

不同环境下的配置

  1. 配置文件根据环境的不同导出不同配置
js 复制代码
// webpack 配置文件还支持导出一个函数,这个函数接收两个参数,返回我们需要的配置对象
// env - 通过cli传递的环境参数
// argv - 运行cli过程中所传递的所有参数
module.exports = (env, argv) => {
  const config = {
    ...
  }

  if(env === 'production') {
    config.mode = 'production';
    config.devtool = false;
    config.plugins = [
      ...config.plugins,
      new CleanWebpackPlugin(),
      // 开发阶段最好不要使用这个插件,用 devServer的 contentBase
      new CopyWebpackPlugin([
        'public'
      ]),
    ]
  }

  return config;
}
  1. 一个环境对应一个配置文件

    webpack.common.js
    webpack.prod.js
    webpack.dev.js

js 复制代码
// webpack.prod.js
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const common = require('./webpack.common.js');
const merge = require('webpack-merge');

module.exports = merge({}, common, {
  mode: 'production',
  plugins: [
    new CleanWebpackPlugin(),
    new CopyWebpackPlugin(['public']),
  ]
});
  • 打包命令
shell 复制代码
# 指明使用哪个配置文件打包
yarn webpack --config webpack.prod.js
  • 也可以修改打包命令:
json 复制代码
"scripts": {
  "build": "webpack --config webpack.prod.js",
  "dev": "webpack-dev-server"
}

DefinePlugin

  • 为代码注入全局成员
  • 在production模式下,默认这个插件会启用,往代码中注入了process.env.NODE_ENV常量,很多第三方工具都是通过这个常量去判断当前环境从而决定是否执行某些任务
js 复制代码
// webpack.config.js
module.exports = {
  mode: 'none',
  entry: './src/main.js',
  output: {
    filename: 'bundle.js',
  },
  plugins: [
    new webpack.DefinePlugin([
      API_BASE_URL: JSON.stringify('"https://api.example.com"')
    ])
  ]
}

TreeShaking

  • 摇掉未引用的代码
  • 在生产模式下会自动开启
  • 不是某个配置选项,而是一组功能搭配使用后的优化效果

TreeShaking 的使用

js 复制代码
// 集中配置webpack内部的优化功能
optimization: {
  useExports: true, // 只导出外部使用到的成员
  minimize: true, // 开启代码压缩功能
},

webpack合并模块

  • 除了useExports以外,还可以使用concatenateModules继续优化输出

  • 普通的打包结果是把每个模块放到单独的一个函数当中,如果模块很多,就会有很多这样的模块函数

    js 复制代码
    // 集中配置webpack内部的优化功能
    optimization: {
      // useExports: true, // 只导出外部使用到的成员
      // minimize: true, // 开启代码压缩功能
      concatenateModules: true, // 尽可能合并每一个模块到一个函数中
    },
  • concatenateModules作用是尽可能将所有模块合并输出到一个函数中,提升运行效率,减少代码体积

  • 这个特性又被称之为Scope Hoisting,即作用域提升,这是webpack3添加的一个特性,配合minimize使用,代码体积又会进一步减少

Tree Shaking 与 Babel

  • Tree Shaking 的前提是使用ESM来组织代码,也就是由 webpack 打包的代码必须使用ESM

  • 为了转换代码中的ESM新特性,会使用到babel-loader,有可能会把ESM转成CJS,这取决于有没有使用到ESM转为CJS的插件,比如@babel/preset-env,那么这时 Tree Shaking会失效,但是,在最新的babel-loader中自动关闭了ESM转为CJS的插件,所以 Tree Shaking 还是有效的

  • 如果要强行转成CJS,也是可以通过配置实现的:

    js 复制代码
    module: {
      rules: {
        test: /\.js$/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: [
              ['@babel/preset-env', { modules: 'commonjs' }]
            ]
          }
        }
      }
    }

webpack 与 sideEffects

  • sideEffects,即副作用
  • 通过配置的方式,去标识我们的代码是否有副作用,从而为 Tree Shaking 提供更大的压缩空间
  • 副作用是指模块执行时,除了导出成员之外所作的事情
  • sideEffects一般在NPM包中用到,用来标记是否有副作用
    1. 开启副作用,webpack打包时会先检查当前代码所属的package.json当中有没有sideEffects这样一个标识,以此来判断这个模块是否有副作用
    2. 如果这个模块没有副作用,"sideEffects": false,那这些没有副作用的模块就不会被打包
  • 生产环境中,webpack 默认会开启 sideEffects
  • sideEffects涉及到两个地方,webpack.config.js中的sideEffects: true表示开启这个功能,package.json中的sideEffects: false标识有没有副作用
json 复制代码
// package.json
{
  "sideEffects": false
}
js 复制代码
// webpack.config.js
optimization: {
  sideEffects: true
}

sideEffects使用注意

  1. 使用前提是,确保你的代码真的没有副作用,否则webpack会误删掉有副作用的代码
  2. 样式文件.css也是会有副作用的
  3. 为了标识有副作用的文件,可以这样:
json 复制代码
// package.json
{
  "sideEffects": [
    "./src/extend.js",
    "*.css"
  ]
}
  1. 那么除了这些有副作用的,其他没有副作用的文件将会被webpack做sideEffects处理

Code Splitting

  • 分包/代码分割
  • 如果没有代码分割,所有代码都会被打包到一起,bundle体积过大
  • 实际上,在应用开始工作时,并不是每个模块在启动的时候都是必要的,但是所有的模块都打包到一起,那么我们需要任何一个模块的时候都必须把整体加载下来过后才能使用,而我们的应用一般都运行浏览器端,就意味着我们会浪费掉很多的流量和带宽
  • 更为合理的方案,就是把我们的打包结果按一定的规则分离到多个bundle中,然后根据应用的运行需要,按需加载这些模块,也就是分包
  • 这样就可以大大提高应用的响应速度以及运行效率

分包方式

  1. 多入口打包,输出多个打包结果
  • 适用于传统的多页应用程序

  • 一个页面对应一个打包入口

  • 公共部分单独提取

  • 配置,

    1. entry是一个对象(注意不是数组,数组的话会把多个页面打包到一起)
    2. output输出文件名filename
    3. 输出html的插件------HtmlWebpackPlugin会输出一个自动注入所有打包结果的html文件, 但是这里需要指定输出的html文件所使用的bundle,就需要用到chunks属性,这样每个打包入口就会形成一个独立的chunk
    js 复制代码
    entry: {
      // key - 入口的名称,值 - 入口所对应的文件路径
      index: './src/index.js',
      about: './src/about.js'
    },
    output: {
      filename: '[name].bundle.js'
    },
    plugins: [
      new HtmlWebpackPlugin({
        title: 'Webpack Plugin  Sample',
        meta: {
          viewport: 'width=device-width'
        },
        template: './src/index.html',
        chunks: ['index']
      }),
      // 用于生成 about.html
      new HtmlWebpackPlugin({
        filename: 'about.html',
        chunks: ['about']
      }),
    ]
  • 提取公共模块 Split Chunks

    1. 多入口文件会存在一个问题,就是不同入口中肯定会有公共模块
    2. 所以需要将公共模块提取到单独的chunk中
    3. webpack实现公共模块提取的方式,通过Split Chunks实现:
    js 复制代码
    optimization: {
      splitChunks: {
        chunks: 'all' // all 表示把所有的公共模块都提取到一个单独的chunk中
      }
    }
  1. 动态导入,ESM实现模块按需加载,webpack会把按需加载的模块单独输出到一个bundle当中
  • 按需加载,指的是需要用到某个模块的时候,再加载这个模块

  • 所有动态导入的模块会被自动分包

  • webpack内部会自动处理分包和按需加载,无需我们手动配置

    js 复制代码
    // 静态导入
    // 打包入口文件index.js
    import posts from './posts/posts'
    import album from './album/album'
    
    const render = () => {
      const hash = window.location.hash || '#posts';
    
      const mainElement = document.querySelector('.main');
    
      mainElement.innerHTML = '';
    
      if(hash === '#posts') {
        mainElement.appendChild(posts());
      } else if(hash === '#alnum') {
        mainElement.appendChild(album());
      }
    }
    
    render();
    
    window.addEventListener('hashchange', render);
    js 复制代码
    // 动态导入
    // 打包入口文件index.js
    const render = () => {
      const hash = window.location.hash || '#posts';
    
      const mainElement = document.querySelector('.main');
    
      mainElement.innerHTML = '';
    
      if(hash === '#posts') {
        // import函数实现动态导入
        import('./posts/posts').then(({ default: posts }) => {
          mainElement.appendChild(posts());
        });
      } else if(hash === '#alnum') {
        // import函数实现动态导入
        import('./album/album').then(({ default: album }) => {
          mainElement.appendChild(album());
        })
      }
    }
    
    render();
    
    window.addEventListener('hashchange', render);

魔法注释 Magic Comments

  • 默认按需加载产生的bundle文件,名称只是一个序号,如果需要给这些bundle命名的话,可以使用魔法注释

    js 复制代码
    import(/* webpackChunkName: 'posts' */'./posts/posts').then(({ default: posts }) => {
      mainElement.appendChild(posts());
    });

MiniCssExtractPlugin

  • 提取CSS文件

  • 如果样式不是很多(少于150kb),不建议用,不然效果反而适得其反

  • MiniCssExtractPlugin会自动提取代码当中的CSS到一个单独的css文件当中

  • 所以不再需要style-loader将样式通过style标签注入,而是替换为使用MiniCssExtractPlugin.loader通过link的方式引入

  • 配置:

    js 复制代码
    module.exports = {
        module: {
            rules: [
                {
                    test: /.css$/,
                    use: [
                      // 'style-loader', // 把样式通过 style 标签注入,使用 MiniCssExtractPlugin 就不用这个
                      MiniCssExtractPlugin.loader, // 使用 MiniCssExtractPlugin 就用这个,把样式 link 到 html
                      'css-loader'
                    ]
                }
            ]
        },
        plugins: [
          // MiniCssExtractPlugin会自动提取代码当中的CSS到一个单独的文件当中
          // 所以不再需要style-loader将样式通过style标签注入
          // 而是替换为使用MiniCssExtractPlugin.loader通过link的方式引入
          new MiniCssExtractPlugin(), // 如果样式不是很多(少于150kb),不建议用,不然效果反而适得其反
        ]
    }

OptimizeCssAssetsWebpackPlugin

  • 压缩 css

  • 在生产环境,不配置任何压缩插件时,默认会压缩js,但是css文件不会被压缩

  • 在 plugins 中配置压缩css压缩插件OptimizeCssAssetsWebpackPlugin,表示在任何情况下都有效,包括开发环境

  • optimization 的 minimizer 配置 css 压缩插件会覆盖原有的默认值,导致 js 的压缩功能会丢失,所以需要安装压缩js的插件TerserWebpackPlugin来对js进行压缩

    js 复制代码
    // 在 plugins 中配置压缩插件,表示在任何情况下都有效
    plugins: [
      new OptimizeCssAssetsWebpackPlugin(), // 压缩打包后的css
      new TerserWebpackPlugin(),
    ]
    
    // 或者在 optimization 的 `minimizer` 属性中按需配置需要用到的压缩插件
    // 注意这里配置后会覆盖原有的默认值,所以除了配置 css 压缩插件,还需要补充 js 压缩插件,不然 js 的压缩功能会丢失
    optimization: {
      minimizer: [
        new OptimizeCssAssetsWebpackPlugin(),
    
      ]
    }

输出文件名 hash

  • 部署前端项目时,都会启用服务器的静态资源缓存,这样对于用户的浏览器而言,就可以缓存前端的静态资源,后续就不再需要请求服务器得到这些静态资源,这样整体应用的响应速度就可以得到提升

  • 不过开启静态资源的客户端缓存,也会有一些小问题。如果在缓存策略当中,缓存失效时间设置过短,效果不是特别明显,缓存失效时间过长,一旦应用发生了更新,重新部署过后,又没有办法及时更新到客户端

  • 为了解决这个问题,在生产环境下,需要给输出的文件名添加hash值。这样一旦资源文件发生改变,文件名称也可以跟着变。对于客户端而言,全新的文件名就是全新的请求,也就没有缓存的问题,这样也就可以把服务端的缓存策略当中的时间设置得比较长,也就不用担心文件更新过后的问题

  • webpack配置filename支持的hash

    1. [hash],整个项目级别,也就是项目当中有任何一个地方改动,那么这次打包过程中的所有hash值都会发生变化
    2. [chunkhash],chunk级别,也就是在打包过程中,只要是同一路的打包,chunkhash都会发生变化
    3. [contenthash],文件级别,根据输出文件的内容生成的hash值,也就是不同的文件就有不同的hash值,只有文件发生了变化,才会更新hash
    js 复制代码
    output: {
      // 输出文件的名称
      // filename: '[name]-[hash]bundle.js',
      // filename: '[name]-[chunkhash]bundle.js',
      filename: '[name]-[contenthash]bundle.js',
    },
  • 指定hash长度

  • e.g. [contenthash:8]

js 复制代码
output: {
  // 输出文件的名称
  // filename: '[name]-[hash]bundle.js',
  // filename: '[name]-[chunkhash]bundle.js',
  filename: '[name]-[contenthash:8]bundle.js',
},

以上是关于webpack4的一些核心配置知识,后面有时间会写一篇webpack5的。感谢大家用心的阅读。

参考

[1] 「前端工程化」之 Webpack 原理与实践(彻底搞懂吃透 Webpack)汪磊原版-b站

相关推荐
理想不理想v25 分钟前
vue经典前端面试题
前端·javascript·vue.js
不收藏找不到我26 分钟前
浏览器交互事件汇总
前端·交互
YBN娜40 分钟前
Vue实现登录功能
前端·javascript·vue.js
阳光开朗大男孩 = ̄ω ̄=40 分钟前
CSS——选择器、PxCook软件、盒子模型
前端·javascript·css
minDuck1 小时前
ruoyi-vue集成tianai-captcha验证码
java·前端·vue.js
小政爱学习!1 小时前
封装axios、环境变量、api解耦、解决跨域、全局组件注入
开发语言·前端·javascript
魏大帅。1 小时前
Axios 的 responseType 属性详解及 Blob 与 ArrayBuffer 解析
前端·javascript·ajax
花花鱼1 小时前
vue3 基于element-plus进行的一个可拖动改变导航与内容区域大小的简单方法
前端·javascript·elementui
k09331 小时前
sourceTree回滚版本到某次提交
开发语言·前端·javascript
EricWang13582 小时前
[OS] 项目三-2-proc.c: exit(int status)
服务器·c语言·前端