八、开发环境和代码抽取

1 如何区分开发环境

目前我们所有的webpack配置信息都是放到一个配置文件中的:webpack.config.js

  • 当配置越来越多时,这个文件会变得越来越不容易维护;
  • 并且某些配置是在开发环境需要使用的,某些配置是在生成环境需要使用的,当然某些配置是在开发和生成环
    境都会使用的;
  • 所以,我们最好对配置进行划分,方便我们维护和管理;

那么,在启动时如何可以区分不同的配置呢?
新建文件:


image.png

方案一:编写两个不同的配置文件,开发和生成时,分别加载不同的配置文件即可;
package.json

{
    "scripts": {
        "build": "webpack --config ./config/webpack.prod.js",
        "serve": "webpack serve --config ./config/webpack.dev.js"
  }
}

webpack.dev.js

const isProduction = false;
module.exports = {
  // ...
}

webpack.prod.js

const isProduction = true;
module.exports = {
  // ...
}

方式二:使用相同的一个入口配置文件,通过设置参数来区分它们
package.json

{
    "scripts": {
        "build": "webpack --config ./config/webpack.common.js --env production",
        "serve": "webpack serve --config ./config/webpack.common.js --env development"
  }
}

通过--env xxxx来区分开发环境
webpack.common.js

module.exports = function (env) {
  return {
    entry: "src/main.js"
  }
}

入口文件解析:
我们之前编写入口文件的规则是这样的:./src/index.js,但是如果我们的配置文件所在的位置变成了 config 目录,
我们是否应该变成 ../src/index.js呢?

  • 如果我们这样编写,会发现是报错的,依然要写成 ./src/index.js;
  • 这是因为入口文件其实是和另一个属性时有关的 context;

context的作用是用于解析入口(entry point)和加载器(loader):

  • 官方说法:默认是当前路径(但是经过我测试,默认应该是webpack的启动目录)


    image.png

2 环境配置文件分离

现在采用第一节方案二(webpack.common.js)来实现配置文件分离:
webpack.dev.js

const resolveApp = require('./paths');
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');

const isProduction = false;

console.log("加载devConfig配置文件");

module.exports = {
  mode: "development",
  devServer: {
    hot: true,
    hotOnly: true,
    compress: true,
    contentBase: resolveApp("./why"),
    watchContentBase: true,
    proxy: {
      "/why": {
        target: "http://localhost:8888",
        pathRewrite: {
          "^/why": ""
        },
        secure: false,
        changeOrigin: true
      }
    },
    historyApiFallback: {
      rewrites: [
        {from: /abc/, to: "/index.html"}
      ]
    }
  },
  plugins: [
    // 开发环境
    new ReactRefreshWebpackPlugin(),
  ]
}

webpack.prod.js

const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const isProduction = true;

module.exports = {
  mode: "production",
  plugins: [
    // 生成环境
    new CleanWebpackPlugin({}),
  ]
}

封装一个路径处理函数config/path.js

const path = require('path');

// node中的api,获取项目启动的目录
const appDir = process.cwd();
const resolveApp = (relativePath) => path.resolve(appDir, relativePath);

module.exports = resolveApp;

把公共的配置放到webpack.common.js里,并合并dev或者prod的配置

const resolveApp = require("./paths");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const VueLoaderPlugin = require("vue-loader/lib/plugin");

const { merge } = require("webpack-merge"); // webpack官方合并插件

const prodConfig = require("./webpack.prod");
const devConfig = require("./webpack.dev");

const commonConfig = {
  entry: "./src/index.js",
  output: {
    filename: "bundle.js",
    path: resolveApp("./build"),
  },
  resolve: {
    extensions: [".wasm", ".mjs", ".js", ".json", ".jsx", ".ts", ".vue"],
    alias: {
      "@": resolveApp("./src"),
      pages: resolveApp("./src/pages"),
    },
  },
  module: {
    rules: [
      {
        test: /\.jsx?$/i,
        use: "babel-loader",
      },
      {
        test: /\.vue$/i,
        use: "vue-loader",
      },
      {
        test: /\.css/i,
        use: ["style-loader", "css-loader"],
      },
    ],
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: "./index.html",
    }),
    new VueLoaderPlugin(),
  ]
};

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

  const config = isProduction ? prodConfig : devConfig;
  const mergeConfig = merge(commonConfig, config);

  return mergeConfig;
};

babel.config.js分离:

const presets = [
  ["@babel/preset-env"],
  ["@babel/preset-react"],
];
const plugins = [];
const isProduction = process.env.NODE_ENV === "production";

// React HMR -> 模块的热替换 必然是在开发时才有效果
if (!isProduction) {
  plugins.push(["react-refresh/babel"]);
} else {

}
module.exports = {
  presets,
  plugins
}

3 代码分离

代码分离(Code Splitting)是webpack一个非常重要的特性:

  • 它主要的目的是将代码分离到不同的bundle中,之后我们可以按需加载,或者并行加载这些文件;
  • 比如默认情况下,所有的JavaScript代码(业务代码、第三方依赖、暂时没有用到的模块)在首页全部都加载,就会影响首页的加载速度;
  • 代码分离可以分出出更小的bundle,以及控制资源加载优先级,提供代码的加载性能;

Webpack中常用的代码分离有三种:

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

3.1 多入口起点

入口起点的含义非常简单,就是配置多入口。
比如有两个文件src/main.jssrc/index.js,在webpack.common.js里配置:

 entry: {
    main: "./src/main.js",
    index: "./src/index.js"
 },
 output: {
    path: resolveApp("./build"),
    filename: "[name].bundle.js"
 }

3.2 Entry Dependencies(入口依赖)

假如我们的index.js和main.js都依赖两个库:lodash、dayjs

  • 如果我们单纯的进行入口分离,那么打包后的两个bunlde都有会有一份lodash和dayjs;
  • 事实上我们可以对他们进行共享;
    webpack.common.js
entry: {
    main: { import: "./src/main.js", dependOn: "shared" },
    index: { import: "./src/index.js", dependOn: "shared" },
    lodash: "lodash",
    // dayjs: "dayjs"
    shared: ["lodash", "dayjs"]
}

3.3 SplitChunks

另外一种分包的模式是splitChunk,它是使用SplitChunksPlugin来实现的,用于import文件或者第三方包的时候:

  • 因为该插件webpack已经默认安装和集成,所以我们并不需要单独安装和直接使用该插件;
  • 只需要提供SplitChunksPlugin相关的配置信息即可;

Webpack提供了SplitChunksPlugin默认的配置,我们也可以手动来修改它的配置:

  • 比如默认配置中,chunks仅仅针对于异步(async)请求,我们可以设置为initial(同步)或者all;
  optimization: {
    splitChunks: {
      // async异步导入
      // initial同步导入
      // all 异步/同步导入
      chunks: "all",
    }
  }

3.3.1 SplitChunks自定义配置

Chunks:

  • 默认值是async
  • 另一个值是initial,表示对同步的代码进行处理
  • all表示对同步和异步代码都进行处理

minSize:

  • 拆分包的大小, 至少为minSize;
  • 如果一个包拆分出来达不到minSize,那么这个包就不会拆分;

maxSize:

  • 将大于maxSize的包,拆分为不小于minSize的包;

minChunks:

  • 至少被引入的次数,默认是1
  • 如果我们写一个2,但是引入了一次,那么不会被单独拆分;

name:设置拆包的名称

  • 可以设置一个名称,也可以设置为false;
  • 设置为false后,需要在cacheGroups中设置名称;

cacheGroups:

  • 用于对拆分的包就行分组,比如一个lodash在拆分之后,并不会立即打包,而是会等到有没有其他符合规则的包一起来打包;
  • test属性:匹配符合规则的包;
  • name属性:拆分包的name属性;
  • filename属性:拆分包的名称,可以自己使用placeholder属性;
  optimization: {
    // natural: 使用自然数(不推荐),
    // named: 使用包所在目录作为name(在开发环境推荐)
    // deterministic: 生成id, 针对相同文件生成的id是不变
    // chunkIds: "deterministic",
    splitChunks: {
      // async异步导入
      // initial同步导入
      // all 异步/同步导入
      chunks: "all",
      // 最小尺寸: 如果拆分出来一个, 那么拆分出来的这个包的大小最小为minSize
      minSize: 20000,
      // 将大于maxSize的包, 拆分成不小于minSize的包
      maxSize: 20000,
      // minChunks表示引入的包, 至少被导入了几次
      minChunks: 1,
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          filename: "[id]_vendors.js",
          // name: "vendor-chunks.js",
          priority: -10 //优先级
        },
        // bar: {
        //   test: /bar_/,
        //   filename: "[id]_bar.js"
        // }
        default: {
          minChunks: 2,
          filename: "common_[id].js",
          priority: -20
        }
      }
    },
    // true/multiple
    // single
    // object: name
    runtimeChunk: {
      name: function(entrypoint) {
        return `why-${entrypoint.name}`
      }
    }
 }

3.4 动态导入(dynamic import)

另外一个代码拆分的方式是动态导入时,webpack提供了两种实现动态导入的方式:

  • 第一种,使用ECMAScript中的 import() 语法来完成,也是目前推荐的方式;
  • 第二种,使用webpack遗留的 require.ensure,目前已经不推荐使用;

比如我们有一个模块 bar.js:

  • 该模块我们希望在代码运行过程中来加载它(比如判断一个条件成立时加载);
  • 因为我们并不确定这个模块中的代码一定会用到,所以最好拆分成一个独立的js文件;
  • 这样可以保证不用到该内容时,浏览器不需要加载和处理该文件的js代码;
  • 这个时候我们就可以使用动态导入

注意:

  • 在webpack中,通过动态导入获取到一个对象;
  • 真正导出的内容,在改对象的default属性中,所以我们需要做一个简单的解构;
import('xxx').then(({ default }) => {})

3.4.1 optimization.chunkIds配置

optimization.chunkIds配置用于告知webpack模块的id采用什么算法生成。
有三个比较常见的值:

  • natural:按照数字的顺序使用id;
  • named:development下的默认值,一个可读的名称的id;
  • deterministic:确定性的,在不同的编译中不变的短数字id
    • 在webpack4中是没有这个值的;
    • 那个时候如果使用natural,那么在一些编译发生变化时,就会有问题;

最佳实践:

  • 开发过程中,我们推荐使用named;
  • 打包过程中,我们推荐使用deterministic;

3.4.2 动态导入的文件命名

optimization.chunkIds值为deterministic
动态导入的文件命名:

  • 因为动态导入通常是一定会打包成独立的文件的,所以并不会再cacheGroups中进行配置;
  • 那么它的命名我们通常会在output中,通过 chunkFilename 属性来命名;
output: {
    chunkFilename: "[name].[hash:6].chunk.js"
},

但是,你会发现默认情况下我们获取到的 [name] 是和id的名称保持一致的,如果我们希望修改name的值,可以通过magic comments(魔法注释)的方式;

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

3.4.3 optimization. runtimeChunk配置

配置runtime相关的代码是否抽取到一个单独的chunk中:

  • runtime相关的代码指的是在运行环境中,对模块进行解析、加载、模块信息相关的代码;
  • 比如我们的component、bar两个通过import函数相关的代码加载,就是通过runtime代码完成的;

抽离出来后,有利于浏览器缓存的策略:

  • 比如我们修改了业务代码(main),那么runtime和component、bar的chunk是不需要重新加载的;
  • 比如我们修改了component、bar的代码,那么main中的代码是不需要重新加载的;

设置的值:

  • true/multiple:针对每个入口打包一个runtime文件;
  • single:打包一个runtime文件;
  • 对象:name属性决定runtimeChunk的名称;
runtimeChunk: {
      name: function(entrypoint) {
        return `why-${entrypoint.name}`
      }
}

3.5 代码的懒加载

动态import使用最多的一个场景是懒加载(比如路由懒加载):

  • 新建一个element.js文件
const element = document.createElement('div');

element.innerHTML = "Hello Element";

export default element;
  • 我们可以在一个按钮点击时,加载这个对象;
    main.js
const button = document.createElement("button");
button.innerHTML = "加载元素";
button.addEventListener("click", () => {
  // prefetch -> 魔法注释(magic comments)
    /* webpackPrefetch: true */
    /* webpackPreload: true */
  import(
    /* webpackChunkName: 'element' */
    /* webpackPrefetch: true */
    "./element"
  ).then(({default: element}) => {
    document.body.appendChild(element);
  })
});
document.body.appendChild(button);

3.5.1 Prefetch和Preload

webpack v4.6.0+ 增加了对预获取和预加载的支持。
在声明 import 时,使用下面这些内置指令,来告知浏览器:

  • prefetch(预获取):将来某些导航下可能需要的资源
  • preload(预加载):当前导航下可能需要资源

与 prefetch 指令相比,preload 指令有许多不同之处:

  • preload chunk 会在父 chunk 加载时,以并行方式开始加载。prefetch chunk 会在父 chunk 加载结束后开始加载。
  • preload chunk 具有中等优先级,并立即下载。prefetch chunk 在浏览器闲置时下载。
  • preload chunk 会在父 chunk 中立即请求,用于当下时刻。prefetch chunk 会用于未来的某个时刻。
import(
    /* webpackChunkName: 'element' */
    /* webpackPrefetch: true */
    /*  webpackPreload: true */
    "./element"
  ).then(({default: element}) => {
    document.body.appendChild(element);
  })

你可能感兴趣的:(八、开发环境和代码抽取)