实现企业级全栈应用服务框架-Elpis(二)

68 阅读5分钟

前言

继完成了elpis-core的内核引擎开发后,本篇主要针对前端工程化这一块进行了实践学习,这里主要使用webpack5作为构建工具,接下来就展开聊聊。

前端工程化

作为前端开发,前端工程化已经是一个老生常谈的问题了,这里还是得提一嘴什么是前端工程化,前端工程化是一个体系,是指将软件工程的方法论应用到前端开发中,通过一系列工具、规范和最佳实践,使前端开发更加标准化、自动化、高效化和可维护。它解决了随着前端项目复杂度提升而带来的各种问题。

核心内容

构建工具与自动化
  1. 代码转译:使用Babel等工具将现代JavaScript转译为浏览器兼容的版本。
  2. 模块打包:通过Webpack、Rollup等工具将模块化代码打包为浏览器可执行的文件。
  3. 资源压缩:压缩JS、CSS、HTML文件以减少体积。
  4. 代码分割:按需加载代码,优化首屏加载性能。
模块化与组件化
  1. 模块化开发:将代码拆分为独立的模块,如CommonJS、ES Modules。
  2. 组件化开发:UI组件的封装与复用,如Vue组件、React组件。
  3. 规范化目录结构:合理组织项目文件和代码。
规范化与标准化
  1. 代码规范:使用ESLint、Prettier等工具确保代码质量和一致性。
  2. 提交规范:如Angular Commit Message格式,使用commitizen工具辅助。
  3. 开发规范:制定团队遵循的开发标准和最佳实践。
持续集成与部署
  1. CI/CD流程:通过GitLab CI、GitHub Actions等实现自动化构建和部署。
  2. 自动化发布:简化版本发布流程。
  3. 环境管理:不同环境(开发、测试、生产)的配置管理。
性能优化
  1. 静态资源优化:图片压缩、字体优化等。
  2. 加载优化:懒加载、预加载、缓存策略。
  3. 构建时优化:tree-shaking、代码分割等。
前端工程化的实践意义
  1. 提高开发效率 :自动化工具减少重复工作。
  2. 保证代码质量 :规范和工具确保代码一致性和可靠性。
  3. 优化用户体验 :性能优化提升应用加载和运行速度。
  4. 便于团队协作 :标准化流程降低沟通成本。
  5. 降低维护成本 :良好的架构和规范使代码更容易维护和扩展。

通过这一系列工程化手段,可以使前端开发从"写代码"转变为更加系统化、规范化的软件工程实践。由于篇幅限制,这里就不涉及其他内容,就针对于webpack配置这一块做一个总结。

整体流程

build.png 首先我们需要在项目中建立好关于webpack的配置文件, 文件结构如下:

   webpack
     | config
     |  | webpack.base.js
     |  | webpack.dev.js
     |  | webpack.prod.js
     | dev.js
     | prod.js

这里是对配置环境做了分流处理,通用部分的配置统一收拢于webpack.base.js,其大体配置如下:

const glob = require('glob');
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目录下所有入口文件
const entryList = path.resolve(process.cwd(), './app/pages/**/entry.*.js');
glob.sync(entryList).forEach((file) => {
  const entryName = path.basename(file, '.js');
  // 构造pageEntries
  pageEntries[entryName] = file;
  htmlWebpackPluginList.push(
    // html-webpack-plugin辅助注入打包后的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,
  // 模块解析配置(加载哪些模块,以及用什么方式去解析)
  module: {
    rules: [
      {
        test: /\.vue$/,
        use: {
          loader: 'vue-loader',
        },
      },
      {
        test: /\.(js|jsx)$/,
        include: [
          // 仅对业务代码进行babel转换,加快打包速度
          path.resolve(process.cwd(), './app/pages'),
        ],
        use: {
          loader: 'babel-loader',
        },
      },
      {
        test: /\.(css)$/,
        use: ['style-loader', 'css-loader',],
      },
      {
        test: /\.(less)$/,
        use: ['style-loader', 'css-loader', 'less-loader'],
      },
      {
        test: /\.(png|jpe?g|gif|svg)(\?.+)?$/,
        use: {
          loader: 'url-loader',
          options: {
            limit: 300,
            esModule: false,
          },
        },
      },
      {
        test: /\.(ttf|woff2?|eot|otf)(\?\S*)?$/,
        use: 'file-loader',
      },
    ],
  },
  // 产物输出路径
  output: {
    filename: 'js/[name]_[contenthash:8].bundle.js',
    path: path.join(process.cwd(), './app/public/dist/prod'),
    publicPath: '/dist/prod/',
    crossOriginLoading: 'anonymous',
  },
  // 配置模块解析的具体行为(定义webpack在打包时,如何找到并解析具体模块的路径)
  resolve: {
    extensions: ['.js', '.jsx', '.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插件(在webpack打包过程中,执行额外的任务)
  plugins: [
    // 处理.vue文件的插件,将定义过的其他规则复制应用到.vue文件中
    new VueLoaderPlugin(),
    // 把第三方库暴露到window.context对象中
    new webpack.ProvidePlugin({
      Vue: 'vue',
      axios: 'axios',
      _: 'lodash',
      
    }),
    // 定义全局常量
    new webpack.DefinePlugin({
      // 支持vue解析options api
      __VUE_OPTIONS_API__: true,
      // 关闭vue生产环境下的devtools
      __VUE_PROD_DEVTOOLS__: false,
      // 关闭vue生产环境下的水合信息
      __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: false,
    }),
    ...htmlWebpackPluginList,
  ],
  // 配置webpack的优化项(优化打包结果,提升打包速度)
  optimization: {
    /**
     * 把js文件打包分3种类型
     * 1. vendor: 第三方lib库,会被提取到vendor chunk中
     * 2. common: 业务公共代码,会被提取到common chunk中
     * 3. entry.{page}: 不同entry里的业务代码的差异部分,会经常变动
     * 把改动和引用频率不同的js文件区分出来,已达到浏览器更好的缓存利用
     */
    splitChunks: {
      // 提取所有模块都进行分割
      chunks: 'all',
      // 提取的公共代码的最大并发请求数
      maxAsyncRequests: 10,
      // 提取的公共代码的最大初始化请求数
      maxInitialRequests: 10,
      // 提取的公共代码的缓存组
      cacheGroups: {
        // 提取所有node_modules中的代码
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendor',
          priority: 20, // 优先级大小
          enforce: true, // 强制执行
          reuseExistingChunk: true, // 复用已存在的chunk
        },
        // 公共模块
        common: {
          name: 'common',
          minChunks: 2, // 最小引用次数
          minSize: 1, // 最小分割文件大小
          priority: 10, // 优先级大小
          reuseExistingChunk: true, // 复用已存在的chunk
        },
      },
    },
    // 将webpack运行时生成的代码打包到runtime中
    runtimeChunk: true
  },

};

由于项目是多页面应用,对entry的处理,渲染模板都是动态的。在module中引入了各种文件对应的loader负责解析对应后缀名的文件,optimization中对打包的文件做了分包处理,这也是分包策略配置的核心。plugins也是可扩展的,如果需要其他插件配置,请自行研究。至于其他配置选项都可查阅webpackd官方文档,这里不再赘述。

webpack.dev.js

webpack.dev.js是开发配置文件,大体配置如下:

const path = require('path');
const merge = require('webpack-merge');
const webpack = require('webpack');
// 基类配置
const baseConfig = require('./webpack.base.js');
const DEV_SERVER_CONFIG  = {
  HOST:'127.0.0.1',
  PORT:9002,
  HMR_PATH:'__webpack_hmr',
  TIMEOUT:20000,
};
// 开发阶段entry配置需要加入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}&reload=true`
    ]
  }
});
const webpackConfig = merge.smart(baseConfig, {
  mode: 'development',
  devtool: 'eval-cheap-module-source-map',
  output: {
    filename: 'js/[name]_[contenthash:8].bundle.js',
    path: path.resolve(process.cwd(), './app/public/dist/dev'), // 输出文件存储路径
    publicPath: `http://${DEV_SERVER_CONFIG.HOST}:${DEV_SERVER_CONFIG.PORT}/public/dist/dev/`, // 外部资源路径
    globalObject: 'this',
  },
  // 配置开发阶段插件
  plugins: [
    // HotModuleReplacementPlugin 用于实现模块热替换(Hot Module Replacement)
    // 模块热替换允许在应用程序运行时可替换模块,而无需刷新整个页面,极大提升开发效率
    new webpack.HotModuleReplacementPlugin({
      multiStep: false,
    }),
  ],
});
module.exports = {
  webpackConfig,
  DEV_SERVER_CONFIG,
};
dev.js
const express = require('express');
const path = require('path');
const webpack = require('webpack');
const consoler = require('consoler');
const devMiddleware = require('webpack-dev-middleware');
const hotMiddleware = require('webpack-hot-middleware');
const { webpackConfig, DEV_SERVER_CONFIG } = require('./config/webpack.dev.js');
const app = new express();
const compiler = webpack(webpackConfig);
// 指定静态文件目录
app.use(express.static(path.join(__dirname, '../public/dist')));
// 引用devMiddleware 中间件,监控文件改动
app.use(
  devMiddleware(compiler, {
    // 落地文件
    writeToDisk: (filePath) => filePath.endsWith('.tpl'),
    // 资源路径
    publicPath: webpackConfig.output.publicPath,
    // headers 配置
    headers: {
      'Access-Control-Allow-Origin': '*',
      'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS',
      'Access-Control-Allow-Headers': 'X-Requested-With, content-type, Authorization',
    },
    stats: {
      colors: true,
    },
  })
);
// 引用hotMiddleware 中间件,实现热更新
app.use(
  hotMiddleware(compiler, {
    path: `/${DEV_SERVER_CONFIG.HMR_PATH}`,
    log: false,
  })
);
consoler.info('webpack 构建中...');
// 启动服务
const port = DEV_SERVER_CONFIG.PORT;
// 启动DEV SERVER
app.listen(port, () => {
  console.log(`webpack dev server 启动成功,监听端口 ${port}`);
});

以上配置是为了解决开发环境中webpack构建和资源服务的问题,引用了express,如果这里不进行相关配置,对开发体验不友好,无法自动监控文件变化并重新构建,还会产生跨域限制,影响开发效率。

HMR

HMR.png HMR,即Hot Module Replacement,即在开发阶段,每次业务文件发生变化时,界面会自动更新,HMR就是起到这样一个作用,它允许在应用程序运行时可替换模块,而无需刷新整个页面,极大提升开发效率。市面上其他的构建工具,也基本是按照这个思路来的。

webpack.prod.js
const path = require('path');
const merge = require('webpack-merge');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
const HtmlWebpackInjectAttributesPlugin = require('html-webpack-inject-attributes-plugin');
const TerserWebpackPlugin = require('terser-webpack-plugin');

// 基类配置
const baseConfig = require('./webpack.base.js');
const webpackConfig = merge.smart(baseConfig, {
  mode: 'production',
  output: {
    filename: 'js/[name]_[contenthash:8].bundle.js',
    path: path.join(process.cwd(), './app/public/dist/prod'),
    publicPath: '/dist/prod/',
    crossOriginLoading: 'anonymous',
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        // 替换happypack为直接使用loader链
        use: [
          MiniCssExtractPlugin.loader,
          'thread-loader', // 添加多线程loader
          {
            loader: 'css-loader',
            options: {
              importLoaders: 1,
            }
          }
        ],
        exclude: /node_modules/,
      },
      {
        test: /\.js$/,
        include: path.resolve(process.cwd(), './app/pages'),
        // 替换happypack为直接使用loader链
        use: [
          'thread-loader', // 添加多线程loader
          {
            loader: 'babel-loader',
            options: {
              presets: ['@babel/preset-env'],
              plugins: ['@babel/plugin-transform-runtime'],
              cacheDirectory: true, // 启用babel-loader缓存
            }
          }
        ],
        exclude: /node_modules/,
      },
    ],
  },
  // webpack不会有大量hints信息,默认为warning提示,设为false关闭
  performance: {
    hints: false,
  },
  // 配置webpack插件(在webpack打包过程中,执行额外的任务)
  plugins: [
    // build时清空public/dist目录
    new CleanWebpackPlugin(['public/dist'], {
      root: path.resolve(process.cwd(), './app/'),
      exclude: [],
      verbose: true,
      dry: false,
    }),
    // 提取css的公共部分,有效利用浏览器缓存,其余部分inline
    new MiniCssExtractPlugin({
      chunkFilename: 'css/[name]_[contenthash:8].bundle.css',
    }),
    // 优化并压缩css
    new CssMinimizerPlugin({
      minimizerOptions: {
        preset: [
          'default',
          {
            discardComments: { removeAll: true },
          },
        ],
      },
    }),
    // 浏览器在请求资源时不发送用户身份凭证
    new HtmlWebpackInjectAttributesPlugin({
      crossorigin: 'anonymous',
    }),
  ],
  optimization: {
    minimize: true,
    minimizer: [
      new TerserWebpackPlugin({
        cache: true, // 开启缓存
        parallel: true, // 多线程打包,加快打包速度
        terserOptions: {
          compress: {
            drop_console: true, // 移除console.log
          },
        },
      }),
    ],
  },
  // 添加持久化缓存配置
  cache: {
    type: 'filesystem',
    buildDependencies: {
      config: [__filename], // 当配置文件变化时,重新构建缓存
    },
  },
});
module.exports = webpackConfig;
prod.js
const webpack = require('webpack');
const webpackProdConfig = require('./config/webpack.prod.js');
console.log('building...');
webpack(webpackProdConfig, (err, stats) => {
  if (err) { console.error(err); }
  process.stdout.write(stats.toString({
    colors: true,  // 开启颜色输出
    modules: false, // 不显示模块打包信息
    children: false, // 不显示子模块信息
    chunks: false, // 不显示每个chunk的信息
    chunkModules: true, // 显示代码块中模块信息
  }) + '\n');
});

以上是生产环境的配置,跟开发环境相比,生产环境更侧重于优化构建产物,代码压缩、资源分离、缓存优化和性能优化,提升用户体验,开发环境针对开发效率,热模块替换、更快的构建速度、更好的源码映射和调试体验。

总结

至此,项目工程化的配置初步完成了,这里只是通过学习配置了不同环境下的webpack配置项,对于对工程化的理解也有了深一步的理解,当然,对于工程化的体系而言,这还只是冰上一角,那么,继续加油吧~~