环境分离与代码分离

环境分离

我们在webpack.config.js中做了很多配置,但是某些配置需要在开发环境中使用、某些需要在生产环境中使用、也有一些配置在开发环境与生产环境都需要使用。
所以我们最好做一个环境分离,对配置做一个划分,方便我们维护和管理。

启动时如何区分不同的配置呢?

方案一:

编写两个不同的配置文件,开发和生产时,分别加载不同的配置文件即可。

  1. //package.json
  2. "scripts": {
  3. "build": "webpack --config ./config/webpack.prod.js",
  4. "serve": "webpack serve --config ./config/webpack.dev.js",
  5. },

方案二:(推荐)

使用相同的一个入口配置文件,通过设置参数来区分它们;(推荐)
目录结构如下:
wps7-16539739852361.jpg

启动命令配置:

  1. //package.json
  2. "scripts": {
  3. "build": "webpack --config ./config/webpack.common.js --env production",
  4. "serve": "webpack serve --config ./config/webpack.common.js --env development"
  5. },

使用webpack.common.js作为入口,入口文件相同。然后根据传入的env参数,区分开不同的环境。

拼接config配置
有了env参数区分环境,就可以根据不同的环境,对当前环境的config配置与公共config配置进行拼接。

安装webpack-merge合并config:

  1. npm install webpack-merge -D

使用:

//webpack.common.js   
const { merge } = require("webpack-merge");  

module.exports = function(env) {  
  const isProduction = env.production;  
  process.env.NODE_ENV = isProduction ? "production": "development";  

  //prodConfig为生产环境的config,devConfig为开发环境的config,commonConfig为公共的config  
  const config = isProduction ? prodConfig : devConfig;  
  const mergeConfig = merge(commonConfig, config);  

  return mergeConfig;  
};

我们通过以上配置就实现了环境的分离,以及根据当前环境进行配置的合并。

babel的环境分离

但是有时候在babel.config.js里,也需要根据当前环境判断是否需要某些配置,我们如何判断呢?

//bable.config.js   
if (process.env.NODE_ENV === 'development') {  
   config.plugins.push(require.resolve('react-refresh/babel'))  
}

使用process.env.NODE_ENV就可以从进程里拿到当前环境。

要想拿到process.env.NODE_ENV,需要我们在上边的webpack.common.js中通过拿到的当前环境env参数后对process.env.NODE_ENV进行手动赋值。

或者设置webpack的mode属性,也是可以拿到process.env.NODE_ENV。
我们在之前学习mode属性时,也了解到了mode的值会自动给process.env.NODE_ENV设置值。
wps8.jpg

代码分离(Code Splitting)

为什么需要用到代码分离?
我们试想一下,如果把通过编译打包后,所有的js文件都打包到了一个bundle.js中(包含业务代码、第三方依赖、暂时没用到的模块)。
那浏览器在加载首页时需要下载解析这个bundle.js,那就太影响首页的加载速度了,很影响用户体验。
如果有了代码分离,我们的代码给分离到不同的bundle.js中,我们就可以按需加载或者并行加载这些js文件了。

通过我们的代码分离配置,在真实开发中一般会分离出这几个js文件

  • 同步导入:main.bundle.js(业务代码)
  • vendor_chunks.js(所有第三方库)
  • common_chunks.js(在多入口中多次引用的代码,比较少见)
  • runtime.js;
  • 异步导入的话每一个模块的异步导入都对应一个js文件

接下来我们看看代码分离的实现方式。
webpack中常用的代码分离的三种方式:

  • 入口起点:使用entry配置多入口,手动实现代码分离;(现在项目大多都为单入口)(不推荐)
  • SplitChunks Plugin:SplitChunksPlugin去重和分离代码;(主要用于分离第三方库或在多入口中多次导入的代码)
  • 动态导入:通过模块的内联函数调用分离代码。(比如路由懒加载)

方式一:入口起点

实际上,就是在entry配置多个入口,手动实现代码分离。每个入口都会分离出一个bundle文件。

//webpack.config.js  
entry: {  
   main: "./src/main.js",  
   index: "./src/index.js"  
 },  
 output: {  
   path: path.resolve(__dirname, "./build"),  
   //每个文件都有对应的name  
   filename: "[name].bundle.js",  
 },

缺点:假如我们在两个入口文件,都依赖了同一个第三方库(比如lodash),那打包出来的两个bundle文件里都各自有一份lodash代码。这就很影响性能了。

解决方案:使用Entry Dependencies(入口依赖)。
Entry Dependencies(入口依赖):

//webpack.config.js  
 entry: {  
   // main: "./src/main.js",  
   // index: "./src/index.js"  
   // 更改为这种写法,dependOn传入共同的第三方依赖  
   main: { import: "./src/main.js", dependOn: "shared" },  
   index: { import: "./src/index.js", dependOn: "shared" },  
   //自定义依赖名称,作为dependOn的value。也可以是数组  
   // lodash: "lodash",  
   // dayjs: "dayjs"  
   shared: ["lodash", "dayjs"]  
 }

入口依赖其实还是需要手动配置每一个共有的第三方依赖。这里我们了解下即可。(“放弃抢救”,换一种代码分离方式。下一个更乖~)

方式二. SplitChunks Plugin

无需手动安装:该插件webpack已经默认安装和集成。

splitChunks插件会根据规则帮我们进行代码分离。
我们直接在splitChunks下配置我们需要的规则即可。

//webpack.config.js  
 optimization: {  
   splitChunks: {  
     // chunks代码分离规则:async对异步导入进行分离(默认)、initial只对同步导入、all 异步/同步导入都进行分离  
     chunks: "all",  
     // 最小尺寸: 如果拆分出来一个, 那么拆分出来的这个包的大小最小为minSize。20000约为20kb  
     minSize: 20000,  
     // 将大于maxSize的包, 拆分成不小于minSize的包  
     maxSize: 20000,  
     // minChunks表示引入的包, 至少在多个entry入口以其依赖被导入了几次(默认 1 )  
     //  满足条件就打包到 common_chunks里  
     minChunks: 1,  
     //缓存组,其中的每一项缓存组都可以继承/覆盖之前提到的 splitChunks 参数值。  
     //缓存组匹配到的所有第三方依赖都统一打包到一起  
     cacheGroups: {  
       vendors: {  
         //test:如果在node_modules文件夹里能找到(默认为所有modules)  
         test: /[\\/]node_modules[\\/]/,  
         filename: "[id]_vendors.js",  
         //name与filename作用相同,区别在于filename可以添加[placeholder]  
         // name: "vendor-chunks.js",  
         //priority:优先级,大的优先  
         priority: -10  
       },  
       default: {  
         //minChunks:在多个入口中,被导入两次就分割(默认为 2 )  
         minChunks: 2,  
         filename: "common_[id].js",  
         priority: -20,  
         //reuseExistingChunk:使用已执行打包的代码,不再重复打包(默认true)  
         reuseExistingChunk: true  
       }  
     }  
   },  
 },

在实际的开发中,常见的配置其实只有chunks属性与cacheGroups属性。其它配置采用webpack的默认值即可(这么多配置了解下干嘛用的即可)

我们也可以一起看一下React脚手架和vue3脚手架都做了哪些splitChunks配置。
React脚手架关于splitChunks的配置:
wps9.jpg

Vue3脚手架关于splitChunks的配置:
wps10.jpg

看完我们发现,脚手架里边只配置了chunks或cacheGroups。(其他都是采用默认的)。
确实跟刚才说的那样,不用配置太多属性,使用默认即可。最多配置一下chunks与cacheGroups。

方式三:动态导入

使用动态导入的模块,webpack都会在打包时分离出单独的js文件。

为什么需要用到动态导入?
有时候,我们并不确定某个模块的代码一定会用到(只有在符合条件的情况下才用到),所以就把这部分模块拆分成一个独立的js文件。
这样就可以保证不会用到该模块时,浏览器不需要加载和处理该模块js代码。提高性能。

webpack提供了两种实现动态导入的方式:
第一种:使用ECMAScript中的 import() 语法来完成,也是目前推荐的方式;

import(/* webpackChunkName: "foo" */"./foo").then(res => {  
  console.log(res);  
});

第二种,使用webpack遗留的 require.ensure,目前已经不推荐使用;

动态导入的文件命名

因为动态导入通常是一定会打包成独立的文件的,所以并不会再cacheGroups中进行配置;
默认情况下,动态导入的文件命名规则是根据output里配置的fileName的。

但是这不符合我们的需要,我们想要的是动态导入的文件命名规则跟fileName的规则不同,这样我们才能很好区分出哪些文件是动态导入的。

使用chunkFilename给动态导入分离出来的文件命名:

//webpack.config.js  
output: {  
   path: resolveApp("./build"),  
   filename: "[name].bundle.js",  
   //chunkFilename:配置动态导入分割的文件命名规则  
   chunkFilename: "[name].chunk.js"  
 },

[name]是每一个文件对应的name,不配置的话是一个数字(chunkIds的默认规则)。
我们可以使用optimization.chunkIds配置name的规则。(这里了解就行,实例开发中使用默认值即可,一般不会手动配置chunkIds,)

//webpack.config.js 
optimization: { 
  // natural: 使用自然数(不推荐), 
  // named: 使用包所在目录作为name(在开发环境下的默认值) 
  // deterministic: 生成id, 针对相同文件生成的id是不变(在生产环境的默认值) 
  chunkIds: "deterministic", 
}

对于动态导入的文件,按照上边说的,name默认跟id一致。但是我们其实还可以用另一种方式修改name,而且其优先级是比chunkIds高的。
使用magic comments(魔法注释)的方式:

// magic comments  
import(/* webpackChunkName: "foo" */"./foo").then(res => {  
  console.log(res);  
});

这个magic comments怎么长得这么眼熟呢?因为在之前学习vue-router中用到过。
下边是在vue-router官方网站看到的:
wps11.jpg

现在才发现,原来在之前学习vue-router的时候就接触过动态导入跟magic comments了。
vue-router的路由懒加载的原理,其实就是我们的代码分离的动态导入方式。

动态导入优化:Prefetch和Preload

上边我们学习到了,异步导入后webpack自动进行的代码分离,其实就是懒加载的原理。
只有用到了异步导入那个模块,浏览器才会去下载、执行当前异步导入模块的js文件。

但是我们的懒加载还是有优化的空间。
假如说我们要进行懒加载的模块,如果逻辑很复杂文件很大,那我们每次使用的时候才去下载并执行,岂不是也有可能花费很多时间。影响用户体验。
如果可以在浏览器空闲的时候,就对我们懒加载的模块的js文件进行下载,等到使用到该模块直接拿来执行就好了。

Webpack考虑到了这些给出了解决方案:使用魔法注释的prefetch或preload。
webpackPrefetch(预获取):告诉浏览器有空闲时先下载我这个异步导入的js文件
webpackPreload(预加载):告诉浏览器在下载当前父chunk时并行下载我这个异步导入的js文件

//魔法注释之webpackPrefetch:(预获取)告诉浏览器有空闲时先下载我这个异步导入的js文件  
import(  
  /* webpackChunkName: 'element' */  
  /* webpackPrefetch: true */  
  "./element"  
).then((res) => {  
  console.log(res);  
})  

//魔法注释之webpackPreload:(预加载)告诉浏览器在下载当前父chunk时并行下载我这个异步导入的js文件  
import(  
  /* webpackChunkName: 'element' */  
  /* webpackPreload: true */  
  "./element"  
).then((res) => {  
  console.log(res);  
})

二选一即可,一般使用Prefetch。
因为preload在下载父chunk时就并行下载懒加载的js文件,有些违背我们使用懒加载的初衷(减少首页的加载速度)。虽然preload是并行下载,但是也会给浏览器增加负担。

runtime分离

什么是runtime?
在运行环境中,对模块进行解析、加载、模块信息等都会生成相关的代码;这些代码就是runtime代码。

默认情况下,runtime代码会被打包到main.bundle.js。
如果我们要对runtime进行额外的代码分离处理,就配置下Optimization.runtimeChunk即可。

//webpack.config.js  
optimization{  
 //runtimeChunk:默认把runtime代码都打包到main.bundle.js里(每个入口分离出来的主文件里)  
//multiple(多入口情况下,每个入口对应的runtime各自分离到各自的runtime文件下)  
//single(多入口的情况下,所有runtime也是分离到同一个runtime文件)  
//object(接受一个对象,可以在对象里设置name)  
 runtimeChunk: {  
  //name:可以是字符串,也可以是函数(入口文件作为参数)  
  //name:'runtime'  
  name: function(entrypoint) {  
    return `${entrypoint.name}`  
  }  
}

CSS分离

在最开始的loader的学习中,我们知道了怎么去使用style-loader与css-loader去处理我们项目里的css代码。
这里我们稍微回顾一下,css-loader负责将css代码转化为js代码,而style-loader负责将转化后的代码绑定到js文件上。在js文件里将css代码写在style标签里。

那我们要是不想要把css文件全都放到打包出来的js文件里,有什么好办法吗?
MiniCssExtractPlugin可以帮助我们将css提取到一个独立的css文件中。
(该插件需要在webpack4+才可以使用。)

安装:

npm install mini-css-extract-plugin -D

使用:

//webpack.config.js  
const MiniCssExtractPlugin = require('mini-css-extract-plugin');、  

module: {  
  rules: [  
    {  
      test: /\.css/i,  
      // MiniCssExtractPlugin.loader替代style-lodader  
      use: [  
        MiniCssExtractPlugin.loader,  
        "css-loader"  
      ],  
    },  
  ],  
},  
plugins: [  
  new MiniCssExtractPlugin({  
    fileName: "css/[name].css"  
  })  
],

注意点:使用 MiniCssExtractPlugin.loader替代style-loader