Elpis框架 - webpack 前端工程化

69 阅读12分钟

以下是其核心原因和关键实践:

  1. 应对项目复杂度

    • 模块化开发:现代前端应用(如SPA)代码量庞大,需拆分为可复用的模块(ES Modules、组件化)。
    • 依赖管理:通过工具(如npm/yarn/pnpm)自动化处理第三方库的版本和依赖关系,避免冲突。
  2. 提升开发效率

    • 自动化工具链
      • 构建工具(Webpack、Vite)打包代码、压缩资源、转译新语法(如ES6→ES5)。
      • 热更新(HMR):实时预览代码改动,减少手动刷新。
    • 脚手架(如Create React App、Vue CLI)快速生成项目模板,统一配置。
  3. 保障代码质量

    • 代码规范:通过ESLint、Prettier强制统一风格,减少低级错误。
    • 测试工具:单元测试(Jest)、E2E测试(Cypress)确保功能稳定性。
    • TypeScript:静态类型检查,提前发现类型错误。
  4. 优化性能与体验

    • 代码分割(Code Splitting):按需加载资源,减少首屏时间。
    • Tree Shaking:剔除未使用的代码,减小打包体积。
    • SSR/SSG:服务端渲染或静态生成,提升SEO和首屏速度。
  5. 团队协作标准化

    • 统一的工作流程:Git提交规范、CI/CD流水线(如GitHub Actions)自动化部署。
    • 环境一致:通过Docker或配置锁定,确保开发、生产环境一致。
  6. 适应技术演进

    • 新语言/框架支持:工程化工具集成Less、Sass、JSX等编译能力,让开发者能用前沿技术。
    • 微前端架构:将大应用拆解为独立子项目,便于团队并行开发。

典型场景对比

传统开发

工程化开发

手动引入JS/CSS文件

按需导入+自动打包

直接修改生产代码

源码→构建→部署

无代码检查

ESLint+Git Hooks拦截错误提交

手动刷新页面

HMR实时更新

Elpis 前端工程化实现

此项目由 webpack 去配置构建

配置webpack.base.js

众所周知我们项目的环境有分为 开发环境、生产环境,所以我们的 webpack 配置在不同环境下是不同的,base 文件下的配置属于两环境的共同配置。webpack配置项有入口entry、出口output、编译loader、插件plugin、优化optimization等配置,这些配置无论在生产环境还是开发环境中都会存在相同的部分,重点在优化optimization配置项,此配置会去配置分包策略来达到打包速度优化等等的效果。

  1. 入口 entry(项目为多页面应用框架,所以存在多个入口):

    // 获取 app/pages 目录下所有入口文件const pageEntries = {};const htmlWebpackPlugins = [];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;  // 构造最终渲染的页面  htmlWebpackPlugins.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],    })  );});
    
  2. 编译 loader:

    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',      },    ],
    
  3. 插件 plugin:

        /**     * 处理 .vue 文件,这个插件是必须的     * 它的职能是将你定义过的其他规则复制并应用到 .vue 文件里     * 例如,如果有一条匹配规则 /\.js$/ 的规则,那么它会应用到 .vue 文件中的 script 板块中     */    new VueLoaderPlugin(),    // 把第三方库暴露到 window context 下    new webpack.ProvidePlugin({      Vue: 'vue',      axios: 'axios',      _: 'lodash',    }),    // 定义全局常量    new webpack.DefinePlugin({      __VUE_OPTIONS_API__: 'true', // 支持 vue 解析 optionsApi      __VUE_PROD_DEVTOOLS__: 'false', // 禁用 Vue 调试工具      __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: 'false', // 禁用生产环境显示 "水合"信息    }),    // 构造最终渲染的页面模板,在处理入口文件处处理的    ...htmlWebpackPlugins,
    
  4. 优化 optimization(重点):

    optimization: {    splitChunks: {      chunks: 'all', // 对同步和异步模块都进行分割      maxAsyncRequests: 10, // 每次异步加载的最大并行请求数      maxInitialRequests: 10, // 入口点的最大并行请求数      cacheGroups: {        vendor: {          test: /[\\/]node_modules[\\/]/, // 打包 node_modules 中的文件          name: 'vendor',          priority: 20, // 优先级,数字越大优先级越高          enforce: true, // 强制执行          reuseExistingChunk: true, // 复用已有的公共 chunk        },        common: {          name: 'common',          minChunks: 2, // 被两处引用即被归为公共模块          minSize: 1, // 最小分割文件大小(1byte)          priority: 10, // 优先级          reuseExistingChunk: true, // 复用已有的公共 chunk        },      },    },    //  webpack 运行时生成的代码打包到 runtime.js    runtimeChunk: true,  },
    

1. 分包策略,optimization配置splitChunks,把 js 文件打包成3中类型

  1. vendor:第三方 lib 库,基本不会改动,除非依赖版本升级
  2. conmmon:业务组件代码的公共部分抽取出来,改动较少
  3. entry.{page}:不用页面 entry 里的业务组件代码的差异部分,会经常改动

      目的:把改动和引用频率不一样的 js 区分出来,以达到更好利用浏览器缓存的效果。

2. Elpis作为多页面应用框架,那么多页面应用和单页面应用有什么区别?

 1. 项目规模和复杂性 

  • 单页面应用(SPA):适用于用户交互较多,界面需要频繁切换,且应用本身相对独立的项目。例如,社交媒体平台、电子商务网站、管理后台等,用户需要在同一页面内快速切换视图和加载不同的内容。SPA 通过动态加载和局部更新,可以减少页面加载次数,提升用户体验。 
  •  多页面应用(MPA):适用于大型、内容丰富的站点,或者具有多个独立功能模块的项目。例如,新闻网站、企业官网、大型电商平台等,页面间的内容较为独立,页面间的跳转比较频繁。MPA 更加适合页面内容独立性强、信息量大、SEO需求高的项目。 

 2. SEO(搜索引擎优化) 

  • 单页面应用(SPA):传统的 SPA 由于其内容是动态加载的,搜索引擎抓取不便,SEO 做得不好。虽然现代框架(如 Vue、React)提供了 SSR(服务端渲染)支持,解决了部分问题,但如果对 SEO 要求非常高的项目,仍需额外的配置和优化。 
  •  多页面应用(MPA):每个页面都是独立的、完整的 HTML 页面,搜索引擎可以轻松抓取到每个页面的内容,因此对于 SEO 更加友好。如果 SEO 是项目成功的关键因素,那么 MPA 会是更合适的选择。 

 3. 加载性能 

  • 单页面应用(SPA):SPA 在首次加载时,可能会加载较大的 JavaScript 文件,导致首次加载时间较长。但后续的页面切换不需要重新加载页面,用户的交互响应速度非常快。适合需要快速切换视图并且后续用户体验重于首次加载速度的项目。 
  •  多页面应用(MPA):每次跳转到新页面时,都会重新加载完整的页面资源,因此每个页面的加载时间相对独立,但通常不会有 SPA 那样的延迟。适用于对首次加载速度要求较高,且页面内容较为独立的项目。 

 4. 开发与维护 

  • 单页面应用(SPA):开发时需要更强的前端技术栈支持,特别是 JavaScript 及前端路由等。代码结构通常会被划分为多个组件,前端开发者需要考虑更多的前端状态管理和路由控制。适合长期迭代、功能变化频繁的项目,维护起来相对集中。 
  •  多页面应用(MPA):每个页面都作为独立的 HTML 页面进行开发,前后端耦合度高,开发过程中会涉及到更多的页面跳转和后台请求,容易使得项目的维护变得分散。适用于那些页面结构简单且功能明确,且不需要频繁变动的项目。 

 5. 用户体验 

  • 单页面应用(SPA):SPA 提供更加流畅的用户体验,减少了页面刷新,交互式操作更加迅速。适用于需要频繁切换页面且需要实时更新数据的项目。 
  •  多页面应用(MPA):每次跳转页面时,用户会感受到一定的页面刷新,虽然不如 SPA 流畅,但页面切换更明确,适合传统的页面布局和内容展示。 

 6. 技术栈与团队经验 

  • 单页面应用(SPA):需要前端开发者掌握现代前端框架(如 Vue、React、Angular)、路由、状态管理、构建工具等技术。如果你的团队在现代 JavaScript 框架和前端工程化方面有经验,SPA 可能会是一个更好的选择。 

  •  多页面应用(MPA):相对简单的前后端分离结构,不需要过多的前端技术栈依赖,适用于传统的开发方式。对于没有前端框架经验的团队,MPA 会比较容易实现。

 7. 更新频率 

  • 单页面应用(SPA):适合内容更新较为频繁且涉及大量交互和动态更新的应用。适用于 SaaS 平台、企业管理后台等需要高度交互的应用。 
  •  多页面应用(MPA):适合内容相对静态且更新频率较低的应用,比如内容展示类的企业官网、博客类平台等。 

 总结: 

  • 选择 SPA:适用于交互性强、需要快速响应的应用,且对 SEO 要求不高,或者可以通过 SSR(服务端渲染)解决 SEO 问题的项目。 

  • 选择 MPA:适用于信息量大、内容较为独立、对 SEO 有较高要求的项目,尤其是在多功能、多页面展示的场景下。

配置webpack.dev.js

配置好了地基base文件,那么开始来配置开发环境下的webpack,在开发环境下,最注重的是方便调试代码,准确定位错误信息,各类方便开发的配置(热更新等)。

  1. 配置source-map:

    devtool: 'eval-cheap-module-source-map'
    
  2. 出口output:

    output: {    filename: 'js/[name]_[chunkhash: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',  }
    
  3. 热更新HMR:

    // devServer 配置const DEV_SERVER_CONFIG = {  HOST: '127.0.0.1',  PORT: '9002',  HMR_PATH: '__webpack_hmr', // 官方指定  TIMEOUT: 20000,};// 开发阶段的 entry 配置需要添加 hmrObject.keys(baseConfig.entry).forEach((key) => {  // 第三方包不作为 hmr 的入口  if (key !== 'vendor') {    baseConfig.entry[key] = [      // 主入口文件      baseConfig.entry[key],      // 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',    ];  }});
    
    // 插件中应用  plugins: [    // 热更新    new webpack.HotModuleReplacementPlugin({      multiStep: false, // 关闭多步热更新      fullBuildTimeout: DEV_SERVER_CONFIG.TIMEOUT, // 热更新超时时间    }),  ],
    

source-map配置

webpack 的 sourcemap 配置是 evalcheapnosourcesinlinesource-map 等基础配置的组合。正则校验:^(inline-|hidden-|eval-)?(nosources-)?(cheap-(module-)?)?source-map$

  1. eval:浏览器 devtool 支持通过 sourceURL来把 eval 的内容单独生成文件,还可以进一步通过 sourceMappingURL 来映射回源码,webpack 利用这个特性来简化了 sourcemap 的处理,可以直接从模块开始映射,不用从 bundle 级别。
  2. cheap:只映射到源代码的某一行,不精确到列,可以提升 sourcemap 生成速度
  3. nosources:不生成 sourceContent 内容,可以减小 sourcemap 文件的大小
  4. module:sourcemap 生成时会关联每一步 loader 生成的 sourcemap,配合 sourcemap-loader 可以映射回最初的源码
  5. source-map:生成 sourcemap 文件,可以配置 inline,会以 dataURL 的方式内联,可以配置 hidden,只生成 sourcemap,不和生成的文件关联

热更新HMR

一种在不重启或重新部署整个应用的情况下,动态更新代码、资源或配置的技术,能够显著提升开发效率。

1.原理如下:

  1. 客户端检测:客户端通过特定的技术(如websocket、长轮询等)检测服务器端的内容是否发生变化。
  2. 服务器端处理:当服务器端内容发生变化时,服务器将新的内容发送给客户端。
  3. 客户端接收和解析:客户端接收到服务器端发送的新内容后,对其进行解析和处理。
  4. 应用加载和渲染:客户端将新内容加载到应用中并进行渲染,从而实现应用内容的更新。

2. 热更新实现方式

  • 文件监听:Webpack通过内置的文件系统监听器,实时监测项目文件的变动。
  • 构建处理:当文件发生变动时,Webpack将重新构建该文件,并生成新的模块ID。
  • 对比差异:Webpack比较新旧模块的差异,只更新变更的部分,避免全量更新。
  • 动态替换:Webpack将更新的模块动态替换到页面中,实现无缝热更新。

3. 工作流程详解

  • 开发过程中,开发者修改了代码,Webpack监听到文件变动,触发构建。
  • 构建过程中,Webpack对比新旧模块,找出差异部分。
  • 更新过程中,Webpack将差异部分动态替换到页面中,实现实时预览。
  • 整个过程无需重新加载整个页面,大大提升了开发效率。

4. 热更新优势

  • 提高开发效率:开发者可以实时预览代码变更,无需频繁刷新页面。

  • 减少资源浪费:对比差异更新,减少网络资源消耗。

  • 缩短上线时间:热更新无需重新部署整个项目,减少上线时间。

配置webpack.prod.js

接下来就是生产环境下的webpack配置,生产环境下注重的是打包速度优化。

  1. 配置source-map:

     devtool: 'nosources-source-map',
    
  2. 出口output:

    output: {  filename: 'js/[name]_[chunkhash:8].bundle.js',  path: path.join(process.cwd(), './app/public/dist/prod'),  publicPath: '/dist/prod',  crossOriginLoading: 'anonymous', }
    
  3. 插件plugins:

    plugins: [  // 每次 build 前,清空 public/dist 目录  new CleanWebpackPlugin(['public.dist'], {   root: path.resolve(process.cwd(), './app/'),   exclude: [],   verbose: true,   dry: false,  }),  // 提取 css 的公共部分,有效利用缓存  new MiniCssExtractPlugin({   chunkFilename: 'css/[name]_[contenthash:8].bundle.css',  }),  // 优化并压缩 css 资源  new CSSMinimizerPlugin(),  // 多线程打包 js ,加快打包速度  new HappyPack({   ...happypackCommonConfig,   id: 'js',   loaders: [    `babel-loader?${JSON.stringify({     presets: ['@babel/preset-env'],     plugins: ['@babel/plugin-transform-runtime'],    })}`,   ],  }),  // 多线程打包 css ,加快打包速度  new HappyPack({   ...happypackCommonConfig,   id: 'css',   loaders: [    {     path: 'css-loader',     options: {      importLoaders: 1,     },    },   ],  }),  // 浏览器在请求资源时不发送用户的身份凭证  new HtmlWebpackInjectAttributesPlugin({   crossorigin: 'anonymous',  }), ],
    
  4. 优化optimization:

    optimization: {  // 使用 terserPlugin 的并发和缓存,提升压缩阶段的性能  minimize: true,  minimizer: [   new TerserWebpackPlugin({    cache: true, // 启用缓存来加速构建过程    parallel: true, // 利用多核 cpu 的优势来加快压缩速度    terserOptions: {     compress: {      drop_console: true, // 清除 console.log     },    },   }),  ], },
    
  5. 编译loader:

    rules: [   {    test: /\.css$/,    use: [MiniCssExtractPlugin.loader, 'happypack/loader?id=css'],   },   {    test: /\.js$/,    include: [     // 只对业务代码进行 babel,加快 webpack 打包速度     path.resolve(process.cwd(), './app/pages'),    ],    use: ['happypack/loader?id=js'],   },  ],
    

这里是利用happypack多线程打包,那么是否有其他更好的加速打包工具呢?

(1)使用 thread-loader(官方推荐)thread-loader可以将耗时的 Loader 操作分配到多个子进程中并行处理,从而提高构建速度。例如,在处理大量 JavaScript 文件时,可以将thread-loader放在babel-loader等之前,如:

module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        use: [
          'thread-loader',
          'babel-loader'
        ]
      }
    ]
  }
};

// 注意:请仅在耗时的 loader 上使用。

(2)使用cache-loader:cache-loader 和 thread-loader 一样,使用起来也很简单,仅仅需要在一些性能开销较大的 loader 之前添加此 loader,以将结果缓存到磁盘里,显著提升二次构建速度。

module.exports = {
  module: {
    rules: [
      {
        test: /\.ext$/,
        use: ['cache-loader', ...loaders],
        include: path.resolve('src'),
      },
    ],
  },
};

// 注意:请对性能开销大的 loader 使用

(3)使用插件HardSourceWebpackPlugin:

  • 第一次构建将花费正常的时间

  • 第二次构建将显着加快(大概提升90%的构建速度)。

    const HardSourceWebpackPlugin = require('hard-source-webpack-plugin')
    const clientWebpackConfig = {
      // ...
      plugins: [
        new HardSourceWebpackPlugin({
          // cacheDirectory是在高速缓存写入。默认情况下,将缓存存储在node_modules下的目录中
          // 'node_modules/.cache/hard-source/[confighash]'
          cacheDirectory: path.join(__dirname, './lib/.cache/hard-source/[confighash]'),
          // configHash在启动webpack实例时转换webpack配置,
          // 并用于cacheDirectory为不同的webpack配置构建不同的缓存
          configHash: function(webpackConfig) {
            // node-object-hash on npm can be used to build this.
            return require('node-object-hash')({sort: false}).hash(webpackConfig);
          },
          // 当加载器、插件、其他构建时脚本或其他动态依赖项发生更改时,
          // hard-source需要替换缓存以确保输出正确。
          // environmentHash被用来确定这一点。如果散列与先前的构建不同,则将使用新的缓存
          environmentHash: {
            root: process.cwd(),
            directories: [],
            files: ['package-lock.json', 'yarn.lock'],
          },
          // An object. 控制来源
          info: {
            // 'none' or 'test'.
            mode: 'none',
            // 'debug', 'log', 'info', 'warn', or 'error'.
            level: 'debug',
          },
          // Clean up large, old caches automatically.
          cachePrune: {
            // Caches younger than `maxAge` are not considered for deletion. They must
            // be at least this (default: 2 days) old in milliseconds.
            maxAge: 2 * 24 * 60 * 60 * 1000,
            // All caches together must be larger than `sizeThreshold` before any
            // caches will be deleted. Together they must be at least this
            // (default: 50 MB) big in bytes.
            sizeThreshold: 50 * 1024 * 1024
          },
        }),
        new HardSourceWebpackPlugin.ExcludeModulePlugin([
          {
            test: /.*\.DS_Store/
          }
        ]),
      ]
    }
    

总结

       前端工程化‌是指在前端开发中引入一系列标准化和自动化的工具和流程,以提高开发效率、代码质量和项目的可维护性。它涵盖了代码组织、开发工具、构建和打包、版本控制、测试等多个方面,通过采用模块化、自动化测试、代码规范等手段,实现前端的“4个现代化”:模块化、组件化、规范化和自动化。前端工程化的核心目标是为了提高前端开发过程的效率和可维护性,确保快速交付高质量的应用程序‌。  ‌

  • 选择合适的构建工具‌:根据项目需求选择合适的构建工具和插件,以确保高效的资源管理和代码优化。 ‌
  • 模块化‌:将代码分割为小模块,提高代码复用性和可维护性 ‌
  • 自动化测试‌:编写和运行自动化测试,确保代码质量。 ‌
  • 代码规范‌:采用一致的代码风格和规范,使用Linting工具检查和修复代码,提高代码质量以及团队合作效率。 ‌
  • 持续集成/持续交付(CI/CD)‌:自动化构建和部署,确保快速交付高质量的应用程序。 

       通过这些方法和工具的应用,前端工程化旨在提升开发效率、提高前端应用质量、降低开发难度和企业成本。狭义上,前端工程化涉及从代码发布到生产环境的整个流程,包括构建、分支管理、自动化测试、部署等。广义上,它还包括从编码开始到发布、运行和维护的整个阶段‌25。 总之,前端工程化是一种综合性的方法,通过标准化和自动化的手段,优化前端开发的流程和工具,从而提高开发效率、代码质量和项目的可维护性,确保快速交付高质量的应用程序‌。

参考文档:
抖音“哲玄前端”《大前端全栈实践》blog.csdn.net/siweisibian…
blog.csdn.net/Pentoncos/a…
developer.aliyun.com/article/151…
www.cnblogs.com/songfengyan…
blog.csdn.net/sunyctf/art…