里程碑2: 基于webpack5完成前端工程化建设

23 阅读6分钟

工程化思想

业务代码 -> 解析引擎 -> 产物文件

解析引擎:将业务文件进行处理生成产物文件

  • 解析编译:编译文件,如.vue => vue-loader 以及其他loader 输出xx.js xx.css, xx.tpl
  • 压缩优化:生产环境中压缩混合js,压缩css,输出文件产物;开发环境中,资源注入模版中,热更新
  • 模块分包:所有的文件在编辑后打包到一个bundle文件中太大,不利于复用,所以需要将打包内容按照内容或者其他规则进行分别打包

webpack配置

在webpack配置文件中,按照生产和开发环境进行分别配置,先提取出公共配置,再分别配置。 配置项:

  • entry:由于有多个入口,所以需要动态构造入口路径
  • module:各种lodaer,模块解析配置,决定了要加载哪些模块,以及用什么方式去加载;loader的运行顺序是从后向前,如[style-loader, css-loader]先运行css-loader再运行style-loader
  • output:打包后的产物要放在那个路径下,生产和开发环境配置不同
  • resolve:配置模块解析的具体行为,可以定义扩展名列表,别名配置,给某个相对路径起个别名,方便后续引入时使用
  • plugins:插件,在打包编译的过程中可以执行任务优化等
  • optimization:配置打包输出优化的配置,代码分割,缓存,treeshaking,压缩等优化
wepack公共配置文件 webpack.base.js
const path = require('path');
const webpack = require('webpack');
const { VueLoaderPlugin } = require('vue-loader');
const HtmlWebpackPlugin = require('html-webpack-plugin');

// 动态构造 pageEntries htmlWebpackPluginList
const pageEntries = {};
const htmlWebpackPluginList = [];
// 获取app/pages目录下所有入口文件(entry.xx.js)
const entryList = path.resolve(process.cwd(), './app/pages/**/entry.*.js');
glob.sync(entryList).forEach((file) => {
  const entryName = path.basename(file, '.js');
  // 构造entry
  pageEntries[entryName] = file;
  // 构造最终渲染的页面文件
  htmlWebpackPluginList.push(
    // htmlWebpackPlugin 辅助注入打包后的 bundle 文件到tpl文件中
    new HtmlWebpackPlugin({
      // 产物(最终模版)输出路径
      filename: path.resolve(
        process.cwd(),
        './app/public/dist/',
        `${entryName}.tpl`
      ),
      // 指定要使用的模版文件
      template: path.resolve(process.cwd(), './app/view/entry.tpl'),
      // 要注入的代码块
      chunks: [entryName],
    })
  );
});

/**
 * webpack基础配置
 */
module.exports = {
  // 入口配置
  entry: pageEntries,
  // 模块解析配置(决定了要加载解析哪些模块,以及用什么方式去解析)
  // loader
  module: {
    rules: [
      {
        test: /\.vue$/,
        use: { loader: 'vue-loader' },
      },
      {
        test: /\.js$/,
        include: [
          // 只对业务代码进行babel,加快webpack打包速度
          path.resolve(process.cwd(), './app/pages'),
        ],
        use: { loader: 'babel-loader' },
      },
      {
        test: /\.(png|jpe?g]|gif)(\?.+)?$/,
        use: {
          loader: 'url-loader',
          options: {
            limit: 300,
            esModule: false,
          },
        },
      },
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader'],
      },
      {
        test: /\.less$/,
        use: ['style-loader', 'css-loader', 'less-loader'],
      },
      {
        test: /\.(eot|svg|ttf|woff|woff2)(\?\S*)?$/,
        use: 'file-loader',
      },
    ],
  },
  // 产物输出路径,因为开发和生产环境输出不一致,应在各自环境配置中配置
  output: {},
  // 配置模块解析的具体行为(定义 webpack 在打包时,如何找到并解析具体模块的路径)
  resolve: {
    // 如在import时,不写后缀 import page from '/page' 按照配置的后缀顺序去找文件
    extensions: ['.js', '.vue', ' .less', '.css'],
    alias: {
      $pages: path.resolve(process.cwd(), './app/pages'),
      $common: path.resolve(process.cwd(), './app/pages/common'),
      $widgets: path.resolve(process.cwd(), './app/pages/widgets'),
      $store: path.resolve(process.cwd(), './app/pages/store'),
    },
  },
  // 配置webpack插件
  plugins: [
    // 处理.vue文件,这个插件是必须的
    // 它的功能是将你定义过的其他规则复制并应用到.vue文件里
    // 例如,如果有一条规则 /.\js$/的规则,那么他会应用到.vue文件中的<script>板块中
    new VueLoaderPlugin(),
    // 把第三方库暴露到window context下,如window.vue
    new webpack.ProvidePlugin({
      Vue: 'vue',
      axios: 'axios',
      lodash: 'lodash',
    }),
    // 定义全局常量 允许创建一个在编译时可配置的全局常量
    // 例如vue有几个全局变量,打包时将常量设置一下
    new webpack.DefinePlugin({
      __VUE_OPTIONS_API__: 'true', // 支持vue 解析 option API
      __VUE_PROD_DEVTOOLS__: 'false', // 禁用 Vue 调试工具
      __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: 'false', // 禁用生产环境显示‘水合’信息
    }),
    // 构造最终渲染的页面模版
    ...htmlWebpackPluginList,
  ],
  // 配置打包输出优化(配置代码分割,模块合并,缓存,TreeShaking,压缩等优化策略}
  optimization: {
    /**
     * 把 js 文件打包成三种类型
     * 1、vendor:第三方lib库,基本不会改动,除非依赖升级
     * 2、common:业务组件代码的公共部分抽取出来,改动较少
     * 3、entry.{page};不同页面 entry 里的业务组件代码的差异部分,会经常改动
     * 目的:把改动和引用频率不一样的js区分出来,以达到更好利用浏览器缓存的效果
     */
    splitChunks: {
      chunks: 'all', // 对同步和异步代码都进行分割
      maxAsyncRequests: 10, // 每次异步架子啊的最大并行请求数
      maxInitialRequests: 10, // 入口点的最大并行请求数
      //具体打包的规则
      cacheGroups: {
        vendor: {
          //第三方库
          test: /[\\/]node_modules[\\/]/,
          name: 'vendor', // 模块名称
          priority: 20, // 优先级,数字越大,优先级越高,默认为0(一个有满足这个有满足下边的规则,按照设定的优先级来处理)
          enforce: true, // 强制执行
          reuseExistingChunk: true, // 复用已有的公共chunk
        },
        common: {
          // 公共模块
          name: 'common', // 模块名称
          minChunks: 2, // 被两处引用即被归为公共模块
          minSize: 1, // 最小分割文件大小(1byte)文件超过这个值就被分割
          priority: 10, // 优先级
          reuseExistingChunk: true, // 复用已有的公共chunk
        },
      },
    },
    // 将webpack运行时生成的代码打包到runtime.js
    runtimeChunk: true,
  },
};

Happypack 可开启多线程打包,目前可以使用thread-loader替代happypack进行多线程打包,要注意使用方法。

常用插件:
  • CleanWebpackPlugin:每次build前清空指定目录
  • MiniCssExtractPlugin:提取css的公共部分,有效利用缓存(打包到一个css单独文件,在tpl中以link形式引入)
  • CssMinizerPlugin:优化并压缩css资源
  • TerserWebpackPlugin:使用该插件的并发和缓存,提升压缩阶段的性能
  • VueLoaderPlugin:处理.vue文件,这个插件式vue开发必须的,功能是将定义过的功能复制并应用到.vue文件里,如js相关规则会应用到vue文件的script模块中
  • HtmlWebpackPlugin:将打包后的buldle模块注入到相应模版中
  • Webpack.HotModuleReplacementPlugin:开发模式下使用,HMR用于实现热模块替换,热模块允许在应用程序运行时替换模块,极大提高开发效率,因为能让应用程序一直保持运行状态
开发环境实现热更新

开发环境打包:为了方便开发,生产环境需要配置热更新模块。开发环境除了最终产物(即koa服务要渲染的tpl文件,其他的bundle包都会以代码片段的形式存储在express服务的内存中)

image.png

  • 使用express服务和webpack-dev-middleware 以及webpack-hot-middleware两个模块来实现模块热更新。
  • devMiddleware用来监控业务文件的改动,用户改动保存后,有改动后就重新编译构建相应的代码到这个express服务的内存中。
  • hotMiddleware用eventsource的方式来通知客户端代码有改动,客户端即浏览器就会再次从内存中请求资源,进而刷新页面。 开发阶段的ertry配置需要加入hmr更新入口
Object.keys(baseConfig.entry).forEach((v) => {
  // 第三方包不作为 hmr 入口
  if (v !== 'vendor') {
    baseConfig.entry[v] = [
      // 主入口文件
      baseConfig.entry[v],
      // hmr 更新入口,官方指定的hmr路径
      `webpack-hot-middleware/client?path=http://${DEV_SERVER_CONFIG.HOST}:${DEV_SERVER_CONFIG.PORT}/${DEV_SERVER_CONFIG.HMR_PATH}&timeout=${DEV_SERVER_CONFIG.TIMEOUT}`,
    ];
  }
});

提取boot.js文件,将vue应用注册挂载动作提到文件中:

因为会有多个页面入口,入口文件挂载vue操作可提出到一个文件中,公共的引入可以放入里边,引入需要的ElementUI、pinia、路由相关注册等,注意,需要将这些都引入后,再挂载到root根元素下,否则这些应用在页面刷新后,不能正常应用。

封装curl方法,使用axios进行请求:

构造请求时需要用到的参数; 请求后得到请求结果的处理,catch错误处理,业务错误处理,成功处理,都是返回一个Promise。