美文网首页
八、开发环境和代码抽取

八、开发环境和代码抽取

作者: AShuiCoder | 来源:发表于2021-05-05 22:17 被阅读0次

    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);
      })
    

    相关文章

      网友评论

          本文标题:八、开发环境和代码抽取

          本文链接:https://www.haomeiwen.com/subject/vuytdltx.html