普通视图

发现新文章,点击刷新页面。
今天 — 2025年4月3日首页

vite构建工具和webpack构建工具有什么共同点和不同处

作者 随笔记
2025年4月3日 09:40

Webpack 和 Vite 都是现代前端开发中常用的构建工具,但它们在设计理念、性能和使用方式上有显著的区别。以下是它们的对比以及如何使用它们的简要说明:

1. Webpack

特点

  • 成熟稳定:Webpack 是前端生态中历史最悠久、最成熟的构建工具之一,拥有庞大的社区和丰富的插件生态。
  • 模块化打包:Webpack 的核心功能是将项目中的所有资源(如 JavaScript、CSS、图片等)视为模块,并通过依赖关系将它们打包成一个或多个文件。
  • 高度可配置:Webpack 的配置非常灵活,支持通过配置文件(如 webpack.config.js)自定义打包行为。
  • 支持多种功能:Webpack 支持代码分割、懒加载、热更新(HMR)、Tree Shaking 等高级功能。

适用场景

  • 大型项目,尤其是需要复杂配置和自定义打包逻辑的项目。
  • 需要兼容旧版浏览器或处理复杂资源加载的项目。

如何使用

  1. 安装 Webpack
npm install webpack webpack-cli --save-dev

2. 创建配置文件webpack.config.js):

const path = require('path');
module.exports = {
  entry: './src/index.js', //入口文件
  output: {
    filename: 'bundle.js', //入口文件
    path: path.resolve(__dirname,'dist'), //输出目录
  },
  module: {
    rules: [
      { 
        test: /\.css$/, //处理css文件
        use: ['style-loader', 'css-loader'],
      },
      ],
  },
};



3. 运行 Webpack

npx webpack

2. Vite

特点

  • 极速开发体验:Vite 利用现代浏览器的原生 ES 模块支持,在开发环境下无需打包,直接按需加载模块,启动速度极快。
  • 基于 ES Modules:Vite 在开发模式下使用浏览器原生的 ES Modules,生产模式下使用 Rollup 进行打包。
  • 开箱即用:Vite 提供了默认配置,支持 TypeScript、CSS 预处理器、热更新等功能,无需复杂配置。
  • 面向现代浏览器:Vite 更适合现代浏览器,对旧版浏览器的支持需要通过插件实现。

适用场景

  • 中小型项目,尤其是需要快速启动和开发的场景。
  • 使用现代前端框架(如 Vue 3、React)的项目。

如何使用

  1. 安装 Vite

按照提示选择项目模板(如 Vue、React、Vanilla JS 等)。

2. 启动开发服务器

npm run dev

3. 构建生产环境代码

npm run build

3:Webpack和Vite的主要区别

1743643881186.png

4. 如何选择?

  • 选择 Webpack
    • 项目需要高度自定义的打包配置。
    • 需要兼容旧版浏览器。
    • 项目规模较大,依赖复杂。
  • 选择 Vite
    • 追求极速的开发体验。
    • 项目基于现代前端框架(如 Vue 3、React)。
    • 项目规模较小,配置简单。

总结

  • Webpack 是前端构建工具的“老大哥”,功能强大但配置复杂,适合大型项目。
  • Vite 是新一代构建工具,以极速开发体验著称,适合中小型项目和现代前端框架。

根据项目需求选择合适的工具,可以显著提升开发效率和体验!

昨天 — 2025年4月2日首页

【万字总结】前端全方位性能优化指南(八)——Webpack 6调优、模块联邦升级、Tree Shaking突破

2025年4月1日 09:17
构建工具深度优化——从机械配置到智能工程革命 当Webpack配置项突破2000行、Node进程内存耗尽告警时,传统构建优化已触及工具链的物理极限:Babel转译耗时占比超60%、跨项目模块复用催生冗

面试看这一篇webpack

2025年4月2日 10:45

前端代码为什么要构建和打包

代码方面

  1. 体积更小(Tree-Shaking,压缩,合并)
  2. 编译高级语言或语法(TS,ES6+,模块化,scss)
  3. 兼容性和错误检查(PolyFill,postcss,eslint)

研发流程方面

  1. 统一,高效的开发环境
  2. 统一的构建流程和产出标准
  3. 集成公司构建规范(提测,上线等)
  • 提高性能
  1. 代码压缩,减小文件体积,
  2. 代码分割,按需加载提高性能
  3. 资源优化:压缩图片,移除未使用的字体,压缩字体。移除未使用的代码
  • 解决兼容性

    1. 使用 Babel 将 ES6+转化为 ES5
    2. 确保 css 在不同浏览器的兼容性
  • 优化部署

    1. 文件哈希命名,避免浏览器缓存导致的更新问题
    2. 区分环境:加载不同配置
  • 提高开发效率

    1. 减少手动操作
    2. 统一规范
    3. 错误检测(Eslint)
  • 解决复杂依赖关系

    1. 自动解析和安装依赖
    2. 将所有依赖打包到一个文件,避免重复加载

module chunk bundle 什么意思,有什么区别?

  1. module:模块,一个模块就是一个文件,一个文件就是一个模块

  2. chunk:一个 chunk 就是一个模块集合,是打包的中间产物

    • Chunk 的生成方式取决于 Webpack 的配置: 入口文件:每个入口文件会生成一个初始 chunk。 动态导入:动态 import() 会生成一个新的 chunk。 代码分割:通过 splitChunks 配置可以进一步优化 chunk 的生成。
  3. bundle:最终的输出文件,每个 bundle 对应一个 chunk,通常是经过优化和压缩后的代码文件。output 输出的文件

loader plugin 区别

  • loader
    • webpack 默认只能处理 js 文件,使用 loader 后可以处理不同类型文件
    • loader 是链式调用按照从后往前的顺序执行
  • plugin
    • 通过监听 webpack 构建生命周期中的事件,在构建过程中执行自定义逻辑。
    • 可以修改输出文件,优化资源,生成额外文件等
    • 典型:清理构建目录,压缩 js,css 文件,自动生成 html 等

webpack 如何实现懒加载

webpack 支持使用 import()动态导入,打包时会将动态导入的包打包成独立文件,会返回一个 Promise,模块加载完后会返回 resolve。只有在 import()调用才会加载

常见性能优化

优化打包构建速度

  1. 优化 babel-loader use:['babel-loader?cacheDirectory'] 开启缓存 include 和 exclude 可以选取明确需要缓存处理的文件范围 只要代码没有改就不会重新编译,使用缓存
  2. ingorePlugin
  • 避免引入无用模块
    new webpack.IngorePlugin({
      resourceRegExp: /^\.\/locale$/, //一个正则表达式,用于匹配需要忽略的模块名称(通常是模块路径的最后部分)
      contextRegExp: /moment$/  //一个正则表达式,用于匹配模块所在的上下文路径(通常是模块路径的前面部分)。
    })
    
  1. noParse
  • 跳过对模块的解析和分析依赖,间接避免重复打包、

    忽略对一些模块的解析。一些模块是独立的,不依赖其他模块。它可以是一个正则表达式、一个函数或者一个数组。

module: {
  noParse: /jquery|lodash/
}
  1. happyPack
  • 多进程打包 为一类文件配置多进程打包
 module: {
          rules: [
            {
                test: /\.js$/,
                 use: happypack/loader?id=js,
             },
          ]
  },
   plugins: [
        new HappyPack({
            id: 'js', // 与上面的 loader 中的 id 对应
            loaders: ['babel-loader?cacheDirectory=true'], // 实际使用的 loader
            threadPool: happyThreadPool, // 使用共享的线程池
            verbose: true, // 显示详细日志
        }),
   ]
  1. paralleIUglifyPlugin
  • 多进程压缩 js,项目比较小,开启多线程会增大开销
 plugins: [
        new ParallelUglifyPlugin({
            // 设置使用的 uglifyjs 版本,默认是自带版本
            uglifyJS: {
                output: {
                    beautify: false, // 是否美化输出
                    comments: false, // 是否保留注释
                },
                compress: {
                    warnings: false, // 如果为 true,则显示压缩警告
                    drop_console: true, // 删除所有的 `console` 语句
                    collapse_vars: true, // 内嵌已定义但只用到一次的变量
                    reduce_vars: true, // 提取出出现多次但是没有定义成变量去引用的静态值
                }
            },
            // 指定要并行处理的文件数量,默认为当前系统的核心数减去1
            workerCount: os.cpus().length - 1,
            // 可选参数:指定缓存目录,默认不缓存
            cacheDir: '.cache/',
        }),
    ],
  • terser-webpack-plugin webpack4 的产物,它被引入作为 UglifyJSPlugin 的替代品,因为 UglifyJS 对 ES6+ 代码的支持有限,而 Terser 提供了更好的兼容性。
  optimization: {
    minimize: true, // 启用代码压缩功能(默认在生产模式下为 true
    minimizer: [
        new TerserPlugin({
            parallel: true, // 启用多线程压缩,利用多核 CPU 提高构建速度
            terserOptions: {
                output: {
                    comments: false, // 移除所有注释,包括版权信息等,减少文件体积
                },
                compress: {
                    drop_console: true, // 删除所有的 `console` 语句(如 console.log、console.warn 等),避免在生产环境中暴露调试信息
                },
            },
        }),
    ],
  },
  1. 自动刷新(非生产环境)
  • 判断文件是否变化是通过不断的去询问系统指定文件是否变化
module.exports = {
  watch: true,// 开启监听模式,默认false
  //注意,使用了webpack-dev-server会开启自动刷新
  watchOptions: {
    ignored: /node_modules/, //忽略node_modules下的文件
    poll: 1000, // 轮询间隔,默认为 3000ms
    aggregateTimeout: 500,//监听到会等待500ms,防止编译的频繁
  }
}
  1. 热更新(非生产环境)
  • 正常刷新 整个网页全部刷新,加载慢,状态丢失
  • 热更新 新代码生效,网页不刷新,状态不会丢失
entry:[
  index: ['webpack-dev-server/client?http://localhost:5500',
            'webpack/hot/dev-server',
            path.join(__dirname, '../src', 'index.js')
        ],
]
devServer: {
  hot: true,
},
plugins: [
  new HotModuleReplacementPlugin()
]

<!-- 在程序的入口文件里引入以下代码 -->
if (module.hot) {
  //需要监听的文件,数组,目录
  //module.hot.accept(); // 监听所有模块的变化
    module.hot.accept('./App', () => {
      //监听到后的回调函数
        const NextApp = require('./App').default;
        render(NextApp);
    });
}
  1. Dllplugin(非生产环境) 将不常更新的第三方库打包成动态链接库 Dll
  • 需要额外配置 dll 的 webpack 配置文件进行打包,再将文件引入启动的 webpack 文件

  • Dllplugin 预打包出 dll 文件和描述模块索引的 json 文件,DllReferencePlugin 使用 dll 文件

//在启动配置下
module:{
  rules: [
    exclude: /node_modules/,可以忽略,已经把一些模块(react)打包了
  ]
}
 plugins: [
        new webpack.DllReferencePlugin({
            manifest: require('./dist/vendor-manifest.json'), // 引用之前生成的 manifest的json 文件
        }),
    ],

优化产出代码

  1. 小图片 base64 编码

  2. 文件名使用 bundle 加哈希

  3. 懒加载

  4. 提取公共代码

  5. IngorePlugin

  6. 使用 cdn,http://cdn...,配置 publicPath 为 cdn

  7. 使用 production

    • 自动压缩代码
    • vue React 会自动删掉调试代码(如开发时的警告)
    • 启用 Tree-Shaking
  8. 使用 Scope Hosting

    • 将多个模块合并到同一个函数作用域内,从而避免了每个模块被包裹在单独的立即执行函数表达式(IIFE)中,减少了函数调用的开销,并提高了代码压缩的效果

HtmlWebpackPlugin

  1. template 自定义 html 的模板路径
  2. chunks: 当存在多个路口文件时,指定需要在 html 中引入的 chunks(包)

entry: {
index: './src/index.js',
print: './src/print.js',
},
plugins: [
new HtmlWebpackPlugin({
title: '管理输出',
chunks: ['index', 'print']
})
],

  1. inject:控制 chunks 注入到 html 的哪个标签中,'head' or ‘body’ or false
  2. minify:在生产环境中,你可能希望最小化生成的 HTML 文件。可以通过设置这个选项为 true 或者传递一个对象来自定义最小化选项。

minify: {
collapseWhitespace: true, // 折叠空白区域
removeComments: true, // 移除注释
removeRedundantAttributes: true, // 删除多余的属性
}

source map

更容易地追踪错误与警告在源代码中的原始位置

devtool: 'inline-source-map'

自动编译工具

  1. 观察模式:script 添加 "watch": "webpack --watch",npm run watch 启动。保存文件后会自动重新编译修改后的文件。 缺点:需要手动刷新浏览器才能看到修改后效果

  2. webpack-dev-server:

    • 优点:自动刷新浏览器,不需要手动刷新浏览器

    • webpack 配置添加

      devServer: {
          static: './dist'    //多个可以是数组
          open: true,         //自动打开浏览器,boolean or 浏览器名称
          port: 8080,         //监听的端口号,默认8080
          hot: true,    //启用 Hot Module Replacement (HMR),提高开发效率。
          proxy: {
            '/api': {}配置代理
          }//将我们本地前端 http://localhost:5137/api 代理到服务器地址 http://localhost:3000
      },
      
    • script 添加 "start": "webpack serve --open",

    • 它会将在 output.path 中定义的目录中的 bundle 文件作为可访问资源部署在 server 中,简而言之就是可以直接浏览器访问该路径。

    • 如果页面希望在不同路径中找到 bundle 文件,可以修改 dev server 配置中的 devMiddleware.publicPath 选项。

  3. webpack-dev-middleware

    • 包装器:它可以把 webpack 处理过的文件发送到 server,这是 webpack-dev-server 内部的原理,但是它也可以作为一个单独的包使用

optimization

  • runtimeChunk:

    • 'single', 表示创建一个单独的运行时文件,该文件将在所有 chunk 之间共享。这有助于缓存优化,因为如果应用的入口点不变,则运行时文件也不变,从而允许浏览器使用缓存版本,而不是每次都重新下载。
  • 每页有多个入口点是否可以使用多个入口点而无需重复模块 (如果你的应用程序每页有多个入口点,并且你希望这些入口点之间共享模块而不重复打包相同的依赖)

多个 chunks 只需要实例化一次 多个 chunks 所依赖的模块可以提取到多个页面使用的公共包中,但是有些模块在很少的页面使用,打包器可能会将他们内联到每个 chunks 中而不需要提取到共享包。 无论提取还是内联,esmodule 和 commonJS 都规定模块只能在每个 js 上下文实例化一次,保证模块的顶级范围是全局并在该模块的所有用法之间共享 实例化多次会为正确的代码引入错误或者效率低下

基本配置

loader 的执行顺序是从后往前的

  • 拆分、merge公共的配置

    使用 const {smart} = require('webpack-merge'); module.exports = smart(baseConfig, {})// baseConfig 为公共配置

  • 处理 ES6

    • 使用 loader:babel-loader,还需要配置.babeirc 文件

      
          "presets": ["@babel/preset-env"]
          "plugins": []
      
      ```
      
  • 处理样式 loader:css-loader,style-loader... 使用 postcss-loader 需要配置 postcss-config.js

  • 处理图片

  • 使用 loader:url-loader,图片小可以直接使用 base64,减少请求

  • output


output: {
filename: '[name].[contenthash:8].js',//动态名称,为入口名称
//使用 8 位的哈希字符串设置文件,文件内容没有更改哈希就不变,请求会命中缓存,加载更快
},

高级配置

  1. 生成多入口 html 并引入指定包:

entry:{
index: './src/index.js',
print: './src/print.js',
}
output:{
filename: '[name].[contentHash:8].js',//动态名称,为入口名称
}
//多个 html 需要 new 多个 HtmlWebpackPlugin
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html',//自定义引用的 html 的模板路径
filename: 'index.html',//自定义 html 的文件名
chunks: ['index', 'print']//指定需要在 html 中引入的 chunks(包),不指定会把入口的 js 文件全部引入
})
]

  1. 抽离压缩 css 文件
  • 使用 loader:mini-css-extract-plugin 在 module 的 rule 里面使用,在 plugins 里面使用。 压缩需要配置

module: {
    rules: [
    {
        test: /\.css$/,
        use: [
            MiniCssExtractPlugin.loader,
            'css-loader',
            'postcss-loader'
            ]
    }]
},
plugins: [
new MiniCssExtractPlugin({
filename: '[name].bundle.css',
})
]

optimization: {
runtimeChunk: 'single',//生成共享文件
minimizer: [
// 默认情况下会包含 TerserPlugin 来压缩 JS 文件
`...`,
new CssMinimizerPlugin(), // 添加 CSS 压缩插件
],
},

  1. css 文件并没有在入口文件中引入,而是直接在 html 引入,所以需要手动引入,使用'copy-webpack-plugin'

new CopyWebpackPlugin({
patterns: [
{ from: 'src/index.css', to: 'assets' },//to 默认指向打包路径这里会在 dist 下创建 assets 文件夹存储
],
})

  1. 抽离公共代码 抽离公共代码,使得文件修改后不会重新请求,而是使用缓存

optimization: {
//分割代码
    splitChunks: {
        chunks: 'all',
        /\*\*
        _ all: 所有的 chunks
        _ initial: 直接引入了的入口 chunks,不处理异步文件
        _ async: 只针对所有的异步 chunks
        _ function: 自定义函数,返回 true  false
        \*/
        cacheGroups: {
            //第三方模块
            vendor: {
            name: 'vendor',//chunk 名称
            priority: 1,//优先级,越大优先级越高
            test: /node_modules/,
            minSize: 0,//最小尺寸,默认 0
            minChunks: 1,//最少被几个 chunk 引用
        },
        // 公共模块
        common: {
            name: 'common',//chunk 名称
            priority: 0,//优先级
            minSize: 0,//最小尺寸,默认 0
            minChunks: 2,//最少被几个 chunk 引用
        }
      }
    }


  1. 懒加载 使用 import('文件路径').then(res => {}),webpack 会打包出一个单独的 js 文件,然后异步加载,不会阻塞页面的渲染

  2. 处理 jsx 在.babelrc 里面使用 使用'@babel/preset-react' 在 module 的 rules 会使用 loader 处理 jsx,匹配 jsx 文件,他会自动处理 jsx

  3. vue vue loader 在 module 的 rules 会使用 loader 处理 vue 的规则,匹配 vue 文件,使用 vue loader

ESModule 和 CommonJS

ESModule 静态引入,编译时引入 CommonJS 动态引入,执行时引入 静态分析才能实现 Tree-Shaking,执行时引入无法分析

babel

  • 只解析语法,将箭头函数,解构,类等转换为向后兼容的语法。不会自动处理新的内置对象或方法,如 Promise,Array.from 等

  • 用于将 ES6+代码转换为兼容向下得代码

  • 删除类型注释,不做类型检查

  1. 环境搭建
  2. .babelrc 配置
  3. presets 和 plugins

babel-runtime 和 babel-polyfill 的区别

babel-polyfill

旨在填补不同环境之间的兼容性差距

  • core-js 和 regenerator-runtime 的集合

说明:从 7.4 开始被弃用。

  • 它通过修改原生对象(Object.prototype)实现向后兼容,可能与其他库产生冲突。 Array.prototype.includes = function () {}//重新定义方法
  • 会引入整个 core-js 的 polyfills,无法按需引入,增大打包体积

如何按需引入

"presets": [
    [
      "@babel/preset-env",
      //添加如下代码
      {
        "useBuiltIns": "usage",
        "corejs": 3 //版本
      }
    ]
  ],

babel-runtime

用于支持模块化和非全局污染的 polyfill 和 helper 函数。它与 @babel/polyfill 不同,后者会全局修改原生对象(如 Array.prototype),而 babel-runtime 通过模块化的方式引入 polyfills 和 helpers,避免了全局污染。

核心概念

  1. 核心概念 (1) Helper Functions Babel 在编译代码时会生成一些辅助函数(helper functions),例如 _classCallCheck、_defineProperty 等。这些函数通常用于实现类、继承等特性。

    默认情况下,这些辅助函数会被重复嵌入到每个文件中。这会导致打包体积增大,尤其是在大型项目中。

(2) Polyfills 为了支持新的 JavaScript API(如 Promise、Map 等),需要引入 polyfills。babel-runtime 提供了一种方式,通过模块化的方式引入这些 polyfills,而不是全局污染。

如何产出一个 lib

配置 webpack 文件,output 的 library 可以定义库的全局名称。 使用 npm publish 发布到 npm。更新需要更新版本号 npm version patch # 更新补丁版本

  • 支持 ts 需要 webpack 去处理 ts 文件

  • 支持多平台 为了让库能够同时支持浏览器和 Node.js 环境,建议:

    设置 libraryTarget: 'umd'。 使用 globalObject: 'this' 来兼容不同环境的全局对象。

Webpack在项目中的配置与工作原理解析

作者 雾散尽时
2025年4月1日 21:05

Webpack配置项逐项解析

Entry(入口配置)

在Webpack中,entry指定了应用程序的入口文件或文件组,是构建依赖图的起点。本项目的Webpack配置采用了动态多入口的方式:通过 glob 工具搜索 app/pages 目录下所有匹配 entry.*.js 的文件。也就是说,每个页面子目录里如果存在形如 entry.页面名.js 的文件,就会被自动收集为一个入口。Webpack将为每个找到的入口文件创建一个命名的入口点,入口名通常取决于文件名(例如 entry.page1.js 会对应入口名 entry.page1 或简化为 page1)。

这个配置的作用:允许项目按页面模块化管理,不同页面有各自的打包入口。每个入口文件及其依赖会打包成独立的输出文件。这种配置让各页面的代码彼此隔离,减少了单次加载的体积,同时也方便按需加载和代码分割。对于每个找到的入口,配置里还自动生成了一个对应的 HTML 模板插件实例(使用 HtmlWebpackPlugin),用于生成该页面的 HTML 文件(本项目中是 .tpl 模板文件)。这样,每个页面入口都会有自己的输出 HTML,里面自动注入打包后的对应脚本。

Output(输出配置)

output 决定Webpack打包后文件的存放路径、文件名等。本项目对开发环境和生产环境分别设置了不同的输出配置:

  • 输出路径:在开发环境中(webpack.dev.js),输出被指定到 app/public/dist/dev/ 目录下,而生产环境(webpack.prod.js)输出到 app/public/dist/prod/ 目录。这样可以将开发构建和生产构建的文件分开存储,避免互相干扰。
  • 文件名格式:采用了占位符来确保文件名具有意义和缓存友好。比如 filename: 'js/[name]_[chunkhash:8].bundle.js'表示输出的文件名前缀为入口名称([name]),加上8位长度的chunkhash用于区分文件版本,再加上 .bundle.js 后缀,存放在 js/ 子目录中。chunkhash根据文件内容生成,能有效利用浏览器缓存(内容不变则hash不变)。生产模式下CSS也有类似命名,如 css/[name]_[contenthash:8].bundle.css
  • publicPath:指定了发布后静态资源的基础访问路径。例如开发环境下 publicPath: http://127.0.0.1:9002/public/dist/dev/,表示页面引用打包资源时,会以该URL为基础。这在开发使用webpack-dev-middleware时很重要,因为资源实际上由开发服务器(Express)通过该路径提供。在生产环境,publicPath被设为 '/dist/prod',表示静态文件部署在服务器相对路径 /dist/prod 下,供客户端访问。
  • 其他输出选项:例如设置了 crossOriginLoading: 'anonymous'来指定,意味着Webpack会将运行时代码拆分到独立的chunk(通常命名为runtime~*.js),从而实现更好的缓存与模块热替换管理。

Module(模块规则与Loader)

module.rules 定义了文件类型与对应处理 Loader 之间的映射。Webpack遇到不同类型的模块文件时,会根据这里的规则进行预处理。本项目的配置为各种常见资源设置了相应的loader:

  • Vue单文件组件 (.vue) :使用 vue-loader 处理。Vue Loader可以解析.vue文件,将其中的模板、脚本和样式部分拆解并分别交由其他loader或Vue编译器处理,使得Vue单文件组件可以被Webpack理解并打包。
  • JavaScript (.js) :使用 babel-loader 转译。规则中限定了 include: app/pages 路径,表示只对本项目源码部分(pages目录)应用Babel转换,跳过诸如node_modules的库以加快构建。Babel-loader会调用Babel将ES6/ES7等新语法转换为更广泛兼容的ES5语法,确保代码能在大多数浏览器运行。
  • 图片资源 (.png, .jpg, .gif等) :使用 url-loader。它会将较小的图片文件转为Base64编码内嵌到打包内容中(通过配置的 limit: 300 字节阈值),以减少HTTP请求数。超过限制的图片则自动调用file-loader方式输出为单独文件,并返回其路径。配置里还设置了 esModule: false以确保url-loader导出的模块采用CommonJS规范(这样可以和Vue等环境更好兼容,避免默认导出对象的问题)。
  • CSS 文件 (.css) :在开发环境中使用 style-loadercss-loadercss-loader解析CSS文件中的 @importurl() 等引用,处理依赖关系;而style-loader则将解析后的CSS通过动态创建标签插入到网页中,使样式生效。在生产环境中,会改用 MiniCssExtractPlugin.loader 取代 style-loader,以将CSS提取成独立文件,避免样式通过JS注入(提高性能并便于缓存)。
  • Less 文件 (.less) :规则为 less-loader + css-loader + style-loaderless-loader将Less预处理语言编译成CSS,然后再交由css-loader和style-loader处理。这与CSS类似,只是多了一步将Less转换为CSS。
  • 字体和SVG等资源 (.eot, .ttf, .woff2, .svg) :使用 file-loader。它会直接复制文件到输出目录,并把引用替换为最终的文件URL。这样在CSS/JS中引用字体和SVG时,最终构建输出对应的文件,并能正确加载。
  • 线程优化加载器:在生产配置中,引入了 thread-loader 来加速繁重的转换过程。例如对JS使用了线程池:先用thread-loader开多个工作线程,然后再交由babel-loader执行实际转译(在配置中,通过 HappyPack 或 thread-loader 实现)。thread-loader可以让耗时的Babel转译并行处理,充分利用多核CPU,加快打包速度。类似地,CSS处理也用了线程loader。需要注意,thread-loader要放在实际工作loader(如babel-loader)之前引入。项目中还曾配置HappyPack插件实现多线程,功能类似,也是将特定loader任务分发到子进程并行处理。总之,这些配置都是为了提高构建性能,对用户透明。

Resolve(模块解析配置)

resolve 配置影响Webpack如何寻找模块,以及对模块路径的简化。本项目的resolve设置主要包括两个部分:

  • extensions(自动扩展后缀) :设置为 ['.js', '.vue', '.less', '.css']。这意味着当你在代码中导入模块时,如果没有写文件扩展名,Webpack会按照这个顺序尝试添加这些扩展去寻找文件。例如,import Foo from '@/common/utils',Webpack会尝试补全为 utils.js, utils.vue, utils.less, utils.css 等,直到找到存在的文件。这减少了书写文件名后缀的麻烦。

  • alias(路径别名) :定义了一些快捷路径映射。比如:

    • $pages 被映射为项目的 app/pages 目录,
    • $common$widgets 都映射为 app/pages/common (可能是不同语义的同一路径),
    • $store 映射为 app/pages/store

    有了这些别名,在源码中可以使用诸如 import stuff from '$common/utils' 这种写法,Webpack会将其解析为实际的绝对路径app/pages/common/utils.js。这样做提高了可读性,也避免了出现很多相对路径(例如 ../../../common/utils)的情况。

Plugins(插件配置)

Webpack插件可以在构建生命周期的不同阶段介入,扩展Webpack功能。本项目使用了多种插件来实现特定功能:

  • VueLoaderPlugin:这是处理.vue文件所必需的插件。Vue Loader本身在处理Vue单文件组件时需要该插件配合,以正确解析组件内的模板和样式部分并生成相应模块。这插件无需配置选项,只需在plugins数组中实例化一次即可。

  • HtmlWebpackPlugin:用于为打包输出生成HTML文件的插件。本项目对每个入口都push了一个HtmlWebpackPlugin实例。配置里指定了模板文件 app/view/entry.tpl 作为基础模板,以及输出文件名为 app/public/dist/${entryName}.tpl。插件会在Webpack构建完成后自动往模板中插入对应入口的

  • webpack.ProvidePlugin:提供全局变量的插件。在配置中,将 Vueaxios_(lodash的变量名)映射到相应的模块。这意味着在项目源码中,可以直接使用 Vueaxios_ 这些变量而不用每次import Vue from 'vue'等。Webpack遇到这些未定义的全局变量时,会自动帮你引入对应的库模块。这对使用第三方库(比如在很多文件里都用到lodash或Vue实例)非常方便,避免重复引用。

  • webpack.DefinePlugin:定义编译时常量的插件。在配置中,用它定义了一些全局常量,例如:

    • _VUE_OPTION_API_: 'true',启用Vue的选项式API支持,
    • _VUE_PROD_DEVTOOLS_: 'false',禁用Vue生产模式下的DevTools支持,
    • _VUE_PROD_HYDRATION_MISMATCH_DETTAILS_: 'false',禁用Vue生产环境下关于服务端渲染水合不匹配的详细提示。

    这些常量通常是给框架或库读取的,用于按环境开启/关闭某些功能。DefinePlugin会在编译阶段直接替换代码中出现的对应标识为给定的值(注意这里是直接文本替换,如果是字符串需要再加引号)。此外,常用的 process.env.NODE_ENV 也可通过DefinePlugin设置为 "production""development",以便前端代码中能根据环境执行不同逻辑。

  • webpack.HotModuleReplacementPlugin:热模块替换插件,仅在开发环境使用。它用于启用Webpack的HMR特性,使应用运行中可以实时替换更新过的模块而无需整页刷新。配置项 multiStep: false 表示不使用多步热更新(一般保持默认即可)。有了它,开发时修改代码,浏览器中只会更新变动的模块部分,状态不丢失,提升开发效率。需要注意,HMR插件需要配合HMR客户端代码和服务器端middleware共同工作(后续详述)。

  • CleanWebpackPlugin:清理输出目录的插件,在生产构建中使用。每次执行生产构建前,它会删除上一次构建留下的旧文件。配置中指定清理 app/public/dist 目录下的内容。这样可避免旧的无用文件堆积,并确保每次部署只包含最新的资源。

  • MiniCssExtractPlugin:CSS提取插件,用于生产环境。它会将原本由style-loader内嵌的CSS提取成单独的 .css 文件。结合它的loader(MiniCssExtractPlugin.loader),在打包时CSS不再通过JS插入页面,而是作为静态文件链接。在配置中指定了输出的CSS文件名格式 css/[name]_[contenthash:8].bundle.css,并会为按需加载的CSS生成 chunkFilename(同样使用contenthash)。这样CSS也可以独立缓存,并行加载,减少页面渲染阻塞。

  • CSSMinimizerPlugin:CSS压缩插件(基于cssnano),用于生产环境。Webpack5+默认会对JS压缩,而CSS需要单独配置这个插件。它会在构建优化阶段对生成的CSS文件进行代码压缩、去除空白和注释等优化,减小文件体积。

  • TerserWebpackPlugin:JS压缩混淆插件(Webpack默认的压缩器),在生产优化中启用。配置中设置了 drop_console: true,意味着会移除所有console.*语句,以减少不必要的日志输出并优化代码体积。同时开启了并行(parallel:true)和缓存(cache:true),充分利用多核CPU加快压缩速度,并缓存结果避免重复压缩相同内容。

  • HtmlWebpackInjectAttributesPlugin:一个用于给HtmlWebpackPlugin生成的标签添加属性的插件。本项目将其用于在输出的HTML模板中为所有 属性。这样浏览器加载这些静态资源时不会附带用户cookies等凭证,也便于错误追踪(例如配合Sentry可以获取跨域脚本的具体报错)。这个插件在HtmlWebpackPlugin生成HTML之后、写入文件之前执行,自动遍历标签进行属性注入,无需手工修改模板。

  • HappyPack:虽然在最终配置中并未通过module.rules直接使用HappyPack的loader(相关规则被注释掉了),但仍然初始化了两个HappyPack插件实例用于示例。一份用于多线程处理JS(配置了babel-loader及preset),一份用于多线程处理CSS(配置了css-loader)。HappyPack的作用与thread-loader类似,都是为了并行处理来加速打包。在本项目中可能是出于演示或兼容目的保留,但主要的并行处理已经通过thread-loader实现。了解HappyPack有助于理解Webpack构建提速的原理:它通过建立worker池,让多个文件的转换同时进行,而不是单线程依次执行。

DevServer(开发服务器配置)

Webpack通常可以通过devServer字段配置开发服务器(webpack-dev-server),如指定端口、启用HMR、设置代理等。然而本项目并未使用webpack-dev-server自带的devServer配置项,而是选择了自定义的Express服务器结合中间件的方式来实现相同功能。这种方式下,devServer字段在Webpack配置中实际上是不存在的,取而代之的是手写的服务器脚本。

在webpack.dev.js中,可以看到定义了一个DEV_SERVER_CONFIG对象,包含开发服务的HOST、端口、HMR路径等信息。随后,在 app/webpack/dev.js 脚本中,使用Express启动了一个服务器并结合 webpack-dev-middlewarewebpack-hot-middleware 来提供开发服务。简要来说:

  • Express服务器监听在配置的HOST和PORT上(默认127.0.0.1:9002)。
  • 使用 webpack-dev-middleware 将Webpack编译器挂载到Express上。它会实时监听文件变动、执行Webpack编译,并将打包后的文件暂存于内存中供访问。配置了 publicPath 来对应Webpack输出的publicPath,确保请求路径匹配。还设置了 writeToDisk: (filePath) => filePath.endsWith('.tpl'),表示只有 .tpl 结尾的文件会写入磁盘。这样做的原因是:我们的页面模板需要实际存在文件(供Koa去渲染),而JS/CSS等资源可以仅存在于内存提高构建速度。
  • 使用 webpack-hot-middleware 实现HMR的实时通讯。它通过配置的HMR路径(如/__webpack_hmr)建立与浏览器的长连接,当有模块更新时通知客户端进行热更新。项目中将其log设为空函数关闭了默认日志输出,使控制台更清爽。

虽然这里没有直接使用devServer字段,但作用是等价的:设定开发服务器的主机和端口热更新等。典型webpack-dev-server配置中,如果用了devServer, 可能会有:

devServer: {
  contentBase: path.join(__dirname, 'dist'),
  port: 9002,
  hot: true,
  open: true,
  proxy: { ... }
}

而本项目采用手动方式,所有这些配置通过代码实现。例如HMR这里就是通过在每个入口里注入 webpack-hot-middleware/client脚本并启用HotModuleReplacementPlugin实现的。对于学习者来说,devServer的核心工作无非是启动一个本地服务、实时重新编译、推送更新到浏览器。本项目展示了如何不用webpack-dev-server也能达到同样效果,这对于定制更复杂的开发流程是有益的。

Webpack整体执行流程

Webpack的工作可以分为开发模式下的实时编译流程和生产模式下的一次性打包流程。下面我们从运行npm run dev(开发)和npm run build:prod(生产构建)两个场景,描述Webpack从启动到生成输出的全过程。

开发环境下的流程(npm run dev

  1. 启动命令:开发时使用 npm run dev。根据package.json,这会设置环境变量_ENV=local并启动nodemon ./index.jsnodemon用于监听文件变化自动重启,但更重要的是它启动了项目的Koa服务器。与此同时,开发者还需要在另一个终端运行npm run build:dev 来启动Webpack的编译服务(或在项目中修改为在Koa启动时自动引入Webpack中间件)。

  2. 初始化Koa服务器index.js 调用自定义框架的 ElpisCore.start() 方法启动Koa应用。Koa加载各种中间件和路由(controllers等),其中包括渲染页面的控制器。此时Koa本身并不会构建前端资源,但它已经准备好在有人访问时提供接口数据或渲染模板。

  3. 启动Webpack开发服务:另一边,运行npm run build:dev 实际执行的是 node ./app/webpack/dev.js。这个脚本通过Express启动了Webpack开发服务器,并使用Webpack Dev Middleware编译前端代码。脚本内做了如下事情:

    • 加载Webpack配置(webpack.dev.js合并了webpack.base.js)并调用 webpack(webpackConfig) 创建编译器。
    • 为每个入口注入HMR客户端脚本,使浏览器能够接收热更新通知。
    • 使用 devMiddleware(compiler, {...}) 启动编译器的监听,指定输出publicPath和写入规则等。Webpack此时开始根据配置 编译构建 项目:解析入口、递归解析依赖模块、应用loader转换代码、打包模块为bundle、拆分代码块、等等。首次编译完成后,所有打包产物(包括各页面的JS、CSS和对应生成的.tpl模板)会存放在内存的文件系统中。由于配置了writeToDisk规则,.tpl文件会同步写到实际磁盘上供Koa使用。
    • 使用 hotMiddleware(compiler, {...}) 建立HMR连接。这样,当Webpack侦测到源代码改动并重新编译出增量更新时,会通过hot-middleware将更新消息推送给客户端浏览器。
    • Express服务器开始监听9002端口,等待浏览器请求静态资源。
  4. 浏览器访问与文件提供:当用户在浏览器访问应用时,例如访问 http://localhost:7001/page1(假设Koa监听在7001端口并路由到ViewController的render逻辑),Koa服务器会调用 ctx.render('dist/entry.page1', data)去渲染对应的页面模板。由于在开发编译时已经生成了最新的 entry.page1.tpl 并写入了 app/public/dist/entry.page1.tpl,Koa的视图引擎能找到这个模板文件,并插入相应数据后返回HTML给浏览器。

  5. 加载打包资源:浏览器收到HTML后,其中的引用脚本会指向类似 http://127.0.0.1:9002/public/dist/dev/js/page1_<hash>.bundle.js 这样的路径(因为HtmlWebpackPlugin模板里用的是publicPath指向9002)。于是浏览器向Webpack的开发服务器请求这些资源。Express的静态和devMiddleware会截获这些请求,从内存中返回对应的JS/CSS文件内容。由于使用了source-map,在开发工具中还能看到映射到源代码的调试信息,方便排错(devtool配置为eval-cheap-module-source-map)。

  6. 热更新循环:此后,如果开发者修改了前端代码(JS/Vue等文件),Webpack的devMiddleware会检测到变更,触发一次增量编译。Webpack根据改动的模块重新构建相应的模块和受影响的chunk,生成热更新补丁文件。在编译完成后,hotMiddleware通过长连接向浏览器推送更新信号。浏览器端的HMR客户端(此前插入的webpack-hot-middleware/client脚本)接收到通知,利用HotModuleReplacementPlugin的API动态获取更新模块并替换掉旧的模块。页面无需刷新,即可实时更新内容。这一过程对开发者是透明的,只会看到应用迅速地反映出代码修改结果。

整个开发流程可以概括为:启动两个服务器(Koa应用服务器 + Webpack构建服务器)协同工作。Koa负责业务逻辑和渲染模板,Webpack服务器负责实时编译和提供静态资源及热更新。最终效果是在开发时,用户访问得到的是最新编译的前端代码,而且可以在不刷新页面的情况下看到代码改动。

生产环境下的流程(npm run build:prod

  1. 启动构建:执行 npm run build:prod 时,脚本会运行 node ./app/webpack/prod.js。不像开发模式有持续的server,这个过程是一次性地运行Webpack进行构建。控制台会打印 “building...” 提示,表示开始构建。

  2. 加载配置并编译:prod.js 脚本载入了生产模式的Webpack配置(webpack.prod.js,内部已merge基础配置)并调用 webpack(webpackConfig, callback) 来执行编译。一旦开始:

    • CleanWebpackPlugin 首先清空之前的 app/public/dist 输出目录内容,确保旧文件不影响本次构建结果。
    • Webpack根据入口配置收集所有入口文件及其依赖模块。
    • 针对每个模块文件类型,按module.rules应用相应的loader进行转换,例如.vue -> 先经vue-loader处理, .js -> babel-loader转译, .less -> 编译成CSS等。因为是生产模式,CSS会被提取、JS会做压缩,所以此阶段会协调各插件一起工作:MiniCssExtractPlugin的loader提取CSS文本、HappyPack/thread-loader让Babel等转换多线程执行、VueLoaderPlugin处理.vue输出等等。
    • Webpack将处理后的模块根据入口和代码分割策略进行打包,生成若干chunk。比如每个页面入口会生成一个对应的chunk,另外还有可能把公用依赖拆分出的 vendorscommon chunk,以及运行时的 runtime chunk。
    • 进入优化阶段:Webpack会并行调用TerserPlugin压缩JS和CSSMinimizerPlugin压缩CSS。Terser根据配置丢掉了console语句并混淆压缩代码,CSSMinimizer则去除冗余优化CSS。
    • HtmlWebpackPlugin在所有chunk确定后,根据模板为每个入口生成最终的HTML文件(.tpl)。HtmlWebpackInjectAttributesPlugin随后给这些HTML里的资源标签加上必要的属性,如crossorigin。
  3. 输出文件:当编译完成并经过各种优化后,Webpack将把各个输出文件写入到 app/public/dist/prod/ 目录下。包括:

    • 每个入口对应的 .bundle.js 主文件,以及按需分出的 vendors.bundle.jscommon.bundle.js 等公共chunk文件,和一个 runtime~*.js 运行时文件(如果有启用runtimeChunk)。
    • 提取出来的 .bundle.css 样式文件(如果有CSS)。
    • 为每个页面生成的 .tpl 模板文件(例如 entry.page1.tpl),其中已经引用了以上输出的JS/CSS文件。
  4. 构建结果验证:prod.js 脚本在回调中输出了本次构建的统计信息stats。这包括打包生成了哪些文件、文件大小、耗时等概要(配置中设定了不显示过多模块细节,只关注总览)。如果有错误也会在这里体现。一切正常则提示构建成功。

  5. 部署与运行:生产构建完成后,一般就可以将 app/public/dist/prod 目录部署为静态资源目录。项目的服务器(通过 npm run prod 启动Koa,设置_ENV=production)会上线运行。Koa在生产模式下不会启用webpack-dev-middleware,而是直接使用打包好的模板和文件。用户访问某页面时,Koa的控制器会渲染 dist/entry.xxx.tpl 模板,返回包含版本化资源引用的HTML。浏览器再去请求这些JS/CSS静态文件(通常由静态服务器或Koa的静态中间件提供),最终呈现出页面。因为文件都已经压缩和带有hash,用户加载速度和缓存效率都显著提升。

总结来看,Webpack生产构建是一次性、严格优化输出的过程,从清理旧文件到生成新文件,全程自动化完成。开发构建则是持续监听、快速反馈的过程,为开发提供便利。两者使用了同一个基础配置,但通过不同的mode和插件组合,实现了截然不同的运行机制。

项目中Webpack的实际应用分析

以上我们解析了配置项和流程,下面结合本项目的细节,深入说明Webpack在此项目中的一些关键应用点:代码分包策略、热更新机制、所用Loaders和Plugins各自的作用原理。

代码分包与SplitChunks实现

本项目非常关注对**代码分割(Code Splitting)**的处理,以实现更高效的加载。Webpack提供了optimization.splitChunks配置用于自动分割chunk,本项目利用了这一功能:

  • 拆分策略:配置中设置 chunks: 'all',意味着无论同步或异步加载的模块,只要满足条件都进行分割。这保证即使初始加载时依赖的包,或动态import的包,都可能被提取到独立chunk。

  • 缓存组:定义了两个主要的缓存组(cacheGroups)用于分包:

    • vendors组:匹配所有来自node_modules的第三方库代码的一个chunk。通过这样做,所有页面共享的第三方库只需加载一次,而且这些库变动频率低,可以长时间缓存。priority: -10略高于默认值,enforce: true确保即便模块较小也强制分离。
    • common组:匹配项目中被多次引用的通用模块。条件设置为 minChunks: 2(至少被两个入口引用)且 minSize: 1(大小至少1字节,实际上任意非空模块都行),符合即提取。这样那些在不同页面入口间共享的业务代码(比如工具函数、公共组件)会打包到一个独立的“common”chunk。reuseExistingChunk: true允许重用已有的chunk,避免重复打包。
  • 效果:经过上述策略,打包输出时会多出文件如 vendors~*.jscommon~*.js(具体名称可能带hash)。页面在引用时,通过HtmlWebpackPlugin已经自动加上了这些公共chunk的让各页面只需加载各自独有的代码 + 公共依赖,大大减少了总体冗余。

  • runtimeChunk:此外,optimization.runtimeChunk: true也开启了将Webpack运行时拆分。Webpack运行时代码包括模块加载逻辑、HMR管理等,通常很小但每次构建可能会变化。如果将其内联或放在主bundle,会导致主文件hash频繁变动,不利于缓存。分离runtime可以使主代码更纯粹,只有真正业务代码变动才会影响其hash。此外在HMR场景,runtimeChunk也能更好地管理模块更新记录。启用后,输出一个runtime.js(名称视Webpack版本而定),由HTML引用。这样runtime本身也能被浏览器缓存,并隔离更新影响。

通过SplitChunks,本项目实现了按来源和复用频率进行模块拆分:第三方库、业务公共模块各自成包,最大程度实现复用与缓存优化。这对非单页的多入口应用尤其重要,每个页面初始加载更轻量,而公共部分后台统筹。

热模块替换(HMR)的启用与配置原理

在开发阶段,本项目启用了**热模块替换(Hot Module Replacement, HMR)**功能,以提升开发体验。虽然没有使用webpack-dev-server自带的简易配置,但通过组合中间件手动实现了HMR,其原理如下:

  • HMR客户端注入:在Webpack开发配置合并时,代码遍历了所有入口并为每个入口数组添加了 webpack-hot-middleware/client。这个特殊入口会在浏览器端运行,建立与开发服务器的连接(通过EventSource长连接或WebSocket),监听服务器推送的更新通知。加入 ?path=http://127.0.0.1:9002/__webpack_hmr&reload=true 参数表明客户端从指定的HMR路径订阅更新,并在无法热替换时回退到自动刷新页面。
  • 启用HMR插件:Webpack配置中包含了 HotModuleReplacementPlugin。没有这个插件,即使注入了HMR客户端也无法真正应用更新。HMR插件会在编译过程中为可以热更新的模块添加特殊标记,并拦截模块变化事件,从而在运行时执行替换逻辑。它还会影响bundle的输出,加入HMR所需的元数据(如每个模块的ID、依赖关系,以便动态查找更新模块)。
  • Dev Middleware 与 Hot Middleware:Express服务器一侧,webpack-dev-middleware负责监听文件改动并触发Webpack增量编译。当编译产生了新的模块代码块(hot update chunk)时,不会像正常刷新那样输出完整文件,而是生成补丁包。随后,webpack-hot-middleware感知到编译完成,通过前面提到的长连接向浏览器发送更新信号。它指定了路径 /__webpack_hmr 供客户端监听,并将变更信息以流的形式推送。
  • 模块接收更新:浏览器端,注入的HMR客户端脚本接收到通知后,会进一步调用Webpack HMR API。具体来说,对于发生变化的模块,Webpack会尝试调用该模块内部的module.hot.accept钩子(如果模块代码中有定义,通常框架会帮我们隐藏处理,例如Vue单文件组件由vue-loader自动接管HMR),或者向上冒泡到父模块。如果模块能安全替换,Webpack将把新的模块代码直接替换旧模块并运行新模块导出的内容,而应用状态(例如页面中已经渲染的部分)保持不变。比如修改了Vue组件的模板,HMR会直接更新组件的渲染函数,Vue会只重新渲染变化的部分。
  • 无法热更新的处理:如果某个改动模块无法安全地进行HMR(比如模块没有做HMR处理,或是意外错误),由于我们在URL参数中指定了reload=true,hot-middleware客户端会在HMR失败时执行一次整页刷新,以保证应用不会处于不一致状态。这相当于回退方案,确保开发调试至少能拿到最新代码。

小结:通过上述机制,开发时的每次保存代码文件,都触发Webpack编译并增量地把更新发送到前端应用,做到界面实时刷新而又保留先前状态。对于样式修改,style-loader本身支持HMR,能直接替换标签内容;对于Vue组件,vue-loader编译出的模块也支持HMR接口。所以开发者会感受到修改样式、修改组件模板/脚本,页面立即局部更新,非常高效。需要注意HMR仅用于开发,生产环境下没有注入相关代码,也不会建立这样的连接。

构建中使用的Loaders及作用

本项目用了多种Loader,它们各司其职地在Webpack构建流程中转换源码。在整个构建过程中,Webpack遇到特定类型的文件就会按规则链式调用相应loader,对文件内容进行编译或处理。以下是项目中用到的主要Loader及它们的作用,用通俗的话来说:

  • Babel Loader (babel-loader) :把高级的JavaScript语法转换为向后兼容的版本。开发者可以用ES6+/ES7等新特性写代码,Babel-loader会调用Babel编译器,根据预设(preset-env)将其转成大多数浏览器能识别的旧语法(比如箭头函数变普通函数,Promise变成基于Polyfill的实现等)。简单讲,它是代码的“翻译官”,翻译成“所有浏览器都看得懂”的语言。
  • Vue Loader (vue-loader) :专门处理.vue单文件组件。Vue单文件里可能写了模板、脚本、样式,格式特殊,浏览器不直接支持。vue-loader接管这些.vue文件,把里面的内容拆分出来:模板部分类似转换成渲染函数的JS代码,部分交给相应的CSS预处理loader,部分当普通JS对待。最终输出一个标准的JS模块,导出Vue组件选项对象。可以说,vue-loader让Webpack“认识”Vue组件文件,使开发者能够以单文件组件形式组织代码。
  • CSS Loader (css-loader) :让Webpack能够解析CSS文件中的内容。当遇到@import "other.css"url('image.png')这样的语法时,css-loader会帮忙处理这些依赖,把它们当作模块看待。它输出的其实是处理后的CSS字符串,供后续loader或插件使用。通俗地说,它把CSS变成了一段可以被JavaScript使用的模块,并解析了其中引用的其他资源路径。
  • Style Loader (style-loader) :在开发模式下,style-loader接上css-loader的输出,会动态地往HTML文档里插入一个<style>标签,把CSS字符串塞进去,让样式生效。也就是说,它把CSS“挂”到网页上去。这种方式适合开发调试,样式热更新迅速。但是在生产环境我们会改用MiniCssExtractPlugin把CSS拆出来,因为大量标签会降低性能且无法缓存。
  • Less Loader (less-loader) :把Less代码编译成普通CSS。Less是一种CSS预处理语言,支持变量、嵌套等特性。less-loader就像一个翻译,将Less文件转换为了CSS文本,然后交由css-loader处理。开发者因此可以用更简洁的Less语法写样式,最终仍旧得到浏览器可理解的CSS。
  • URL Loader (url-loader) :处理图片和字体等二进制文件的小帮手。它的策略是对于小文件直接读入并转成Base64编码的字符串,嵌入到打包内容里;对于超过设定大小的文件,则交给file-loader处理成单独文件。这样页面上很多小图标、背景图等可以直接内联,减少请求数,而大的图片仍独立加载以免主bundle过大。项目中配置的limit=300字节,说明非常小的图片才内联,大部分稍大的图片还是会独立成文件。无论哪种情况,url-loader都会返回一个可以在代码中使用的资源路径:对小图是一个data URI,对大图是发布后文件的路径。
  • File Loader (file-loader) :几乎所有非代码资源都可以用file-loader处理,包括图片、字体、媒体等。它的作用很直接:“接过”该文件,然后输出到指定的输出目录,并给出一个路径。这路径通常是根据文件内容hash或名称生成的,确保引用正确。url-loader在超过大小时实际上就是调用file-loader来完成工作的。在项目中,字体文件和SVG就是用file-loader输出的。简单说,如果把Webpack比作打包工厂,file-loader就是库管+搬运工,把源文件搬到输出仓库,并告诉其他模块“你需要的东西在那里,去拿吧”。
  • Thread Loader (thread-loader) :这是一个帮助其他loader提速的“多线程助手”。单个loader(例如Babel)处理大量文件时可能很慢,thread-loader会在它前面启动多个工作线程,把即将处理的文件分摊到不同线程中并行执行后续loader。项目在生产配置中就在babel-loader和css-loader前用了thread-loader,并指定了线程数量(通常等于CPU核心数)等参数。打个比方,本来100份活儿一个人干,现在开4个线程4个人一起干,每人25份,效率就上去了。线程池会管理这些工作,空闲超时还会自动回收线程以免资源浪费。对开发者而言,这一切都是幕后进行,只是构建速度变快了。
  • HappyPack Loader:本质上作用类似于thread-loader,只不过配置和使用方式略有不同。HappyPack需要把实际的loader配置写在它的loaders选项里,并在rules中用happypack/loader?id=...代替原本的loader。项目曾经配置过HappyPack用于JS和CSS的处理(id为'js'和'css'),指定了babel-loader和css-loader等。但是在最终rules里并没有启用这些HappyPack loader(被注释掉了),可能开发者改用了thread-loader直接简化处理。不管怎样,了解HappyPack有助于理解:它通过子进程池并行执行loader任务,实现和thread-loader类似的效果。两者都是为了解决Webpack构建瓶颈,让多核CPU充分运转起来。

总体来说,Loaders就像Webpack的“翻译和处理工” ,把各种类型的源文件转换为可以被Webpack捆绑的模块。其中有的翻译代码(Babel把高级JS翻译成低级JS,vue-loader把.vue翻译成JS模块),有的处理资源(url/file-loader搬运文件,style-loader把CSS塞进页面)。它们串联起来,使Webpack可以把不同格式的内容都统一处理打包。这些Loader大部分只在构建时运行,对最终产出的代码体积和运行性能没有直接影响(除了babel会影响代码形式),但是对开发体验、代码组织非常有帮助,让开发者可以自由使用高级语法和模块化方式,而无需手动转换。

构建中使用的Plugins及其作用与触发时机

Webpack插件体系为构建流程提供了高度的可扩展性。插件可以在Webpack运行过程的不同阶段介入,执行特定的任务或修改输出。本项目用到的插件我们在前文已罗列,这里从它们在构建生命周期中的作用和时机来做个通俗总结:

  • HtmlWebpackPlugin:在Webpack完成所有模块和chunk的处理后,进入生成文件(asset)阶段时运行。它为每个入口根据指定的模板生成了HTML文件(在本项目中是.tpl)并自动插入对应的 等标签,然后输出文件。在插件的生命周期钩子上,这是在emit阶段(即将写入输出目录前)完成的。对开发者而言,它省去了手动维护HTML引用的麻烦,保证引用准确无遗漏。
  • HtmlWebpackInjectAttributesPlugin:这个插件紧随HtmlWebpackPlugin之后工作。当HTML内容已经生成但尚未写出时,它介入遍历所有标签节点,加上我们需要的属性如 crossorigin="anonymous"。触发时机也是在emit阶段,但优先级在HtmlWebpackPlugin生成内容之后。这样保证属性正确地出现在最终写入的HTML文件里。它的作用对最终用户来说是静默的,但对安全和跨域请求有影响:通过anonymous属性,浏览器在请求这些静态资源时不会附带cookies,也允许JS跨域错误捕获(如结合CORS headers)。换句话说,它帮我们最后润色了一下输出的HTML文件。
  • ProvidePlugin:这个插件实质在编译阶段发挥作用。当Webpack解析每个模块的源码时,ProvidePlugin会检查其中用到的全局变量标识符(如Vue、axios等),如果发现对应配置了ProvidePlugin,它就自动在模块头部插入require('vue')等代码。比如某个文件中直接用了Vue.component(...)却没有import,ProvidePlugin会确保Webpack不报错并自动把Vue模块提供给它。这个过程发生在模块编译的解析阶段,属于加载前的准备工作。对最终打包输出,它不额外生成文件,只是影响模块内容。所以可以说ProvidePlugin是在幕后默默地“提供变量”,让我们在源码里少写很多import声明,属于构建时优化开发体验的手段。
  • DefinePlugin:DefinePlugin也是在编译阶段执行,它通过文本替换的方式注入常量。Webpack打包每个模块时,会查找代码中出现的特定标识符并用定义的值替换。例如代码里有 if (_VUE_OPTION_API_) { ... },在编译后就直接变成了 if (true) { ... }(因为配置中_define了_VUE_OPTION_API_为'true')。这些替换在源码转成AST语法树时进行,甚至可以结合Uglify/Terser在后续优化中删掉永远不会执行的分支。DefinePlugin的触发时机可以认为是每个模块源码处理的时候。它影响的是输出代码本身(嵌入不同的值),典型用例还有设置process.env.NODE_ENV。总之,这是一个编译期的“全局开关”插件,打包结果中不会保留原来的标识符,而是替换成具体的值。
  • HotModuleReplacementPlugin:HMR插件有点特殊,它既在编译阶段影响输出,又在运行时参与热更新流程。编译时,它为每个模块注入HMR相关的钩子代码(如检查模块的module.hot.accept调用,添加HMR标记等),并确保Webpack输出热更新所需的manifest和补丁chunk。当启用HMR插件时,Webpack会生成额外的 Hot Update 文件(.hot-update.json和.hot-update.js),这些都是HMR插件促成的结果。运行时,当dev服务器检测到文件变更,它也协助协调客户端的模块替换。所以可以认为HMR插件贯穿了编译->运行的周期:编译时为HMR做好准备,运行时真正执行模块热替换。如果没有它,webpack-dev-middleware侦测变化后只能回退到整页刷新。
  • CleanWebpackPlugin:此插件最先执行,基本在编译开始前就运行。当我们启动webpack( )开始构建,还未读入新的模块时,CleanWebpackPlugin按照配置把目标文件夹(如dist目录)内容删除。这是一个构建前置步骤,确保接下来输出时目录是干净的。它通过Node文件操作同步地清理指定路径,所以触发时机就是在Webpack准备输出文件前的hooks上。在实际运行中,你会发现每次构建开始控制台首先输出clean的操作日志(如果verbose:true的话),然后才继续编译模块。
  • MiniCssExtractPlugin:这个插件在编译阶段和输出阶段都各有动作。编译阶段,它配合其loader捕获到所有模块中的CSS代码段,将它们从模块的JS中抽离出来,暂存成独立的CSS文件块。输出阶段,它根据chunk把收集的CSS内容生成最终的.css文件并写入输出目录。可以说,它接管了CSS模块的输出。当Webpack处理到CSS模块时,原本style-loader会把CSS变成JS字符串注入,这时被MiniCssExtractPlugin拦截改为提取流程。所以在生成文件时就会出现.css文件。这插件典型触发点是在optimize assets过程中,将CSS作为asset输出。对于开发者,它的存在是感觉不到的,只是最终多出了CSS文件,但对用户来说好处是CSS以形式加载,避免JS长任务,并可以并行加载和缓存。
  • CSSMinimizerPlugin:这个插件在**优化优化阶段(Optimize Chunk Assets)**运行,具体在Webpack完成模块组装、开始优化输出文件的时候。它遍历所有产出的CSS文件,对每一个应用CSS nano等优化算法进行压缩。触发时机通常是在Webpack内部的 optimizeAssets 钩子。在此之后,CSS文件就变成压缩过的版本再写出。对开发者来说,这发生在构建末尾,看不到,但输出的CSS体积已经明显减小了。这种压缩不改变功能,只是去掉空格、注释、缩短颜色代码等,对浏览器透明。
  • TerserWebpackPlugin:类似地,TerserPlugin在优化阶段针对JS文件执行。Webpack在production模式默认会使用TerserPlugin,对每个JS chunk文件进行AST解析、变量名混淆、删除多余代码等压缩动作。项目里特别配置了parallel和drop_console,这些在插件初始化时设定,一旦进入压缩阶段,每个JS文件的压缩都会并行处理并执行移除console的额外步骤。它的运行顺序通常在loader处理、代码分割结束后开始,对最终要输出的JS代码进行处理。因此可以理解为最后一步深加工。执行完毕,JS代码就定型了,Webpack随后把它们写入文件系统。
  • HtmlWebpackPluginList (...HtmlWebpackPlugin) :这个并不是单一插件,而是根据每个页面入口push的一组HtmlWebpackPlugin实例。它们在构建流程中的触发顺序可能彼此并行,但总的来说,每当一个编译完成(emit前),各HtmlWebpackPlugin依次执行生成各自页面。这些插件实例并没有彼此依赖,但要确保在assets确定后才能正确插入引用。Webpack会在compilation优化完调用HtmlWebpackPlugin,为每个入口生成HTML。所以触发时机是编译接近尾声,在优化和chunk生成都结束后。由于项目有多个页面入口,实际上会生成多个模板文件(entry.page1.tpl, entry.page2.tpl等),但HtmlWebpackPlugin让这一切自动完成。每个实例的生命周期包括:读取模板 -> 插入资源列表 -> 输出文件。这一系列发生在构建流程的尾部,但在写入磁盘前完成。
  • HappyPack:HappyPack插件本身在构建一开始就启动,创建其内部的线程池(根据配置的cpu数量)。如果有使用HappyPack loader的规则,它会在loader执行阶段拦截,让任务交给它的线程去做。但由于本项目最终没有启用对应的happypack/loader(规则被注释),HappyPack插件实际上没有派上用场。一般而言,HappyPack在compile阶段监控特定类型文件的解析,当有文件匹配时,就把文件内容和loader发送到子线程处理。等子线程处理完,再将结果返回主线程,Webpack继续打包。这种插件运行时机算是与loader并行,但它本身是个管理者。因为这里没真正用,所以构建过程中它只是初始化了并未实际处理模块。
  • webpack-dev-middleware & webpack-hot-middleware(非Webpack自带插件) :虽然不是通过Webpack配置中的plugins字段引入的,但值得一提它们在开发流程的作用。这两个中间件不是Webpack内部插件,而是外部Express中使用的库。不过它们通过Compiler API与Webpack深度交互:dev-middleware在每次编译后接管输出,将文件存在内存并向Express提供;hot-middleware监听Compiler的done事件以获取更新信息,然后触发HMR流程。可以把它们看成是在Webpack和开发服务器之间架起桥梁的“插件”。它们的触发显然是在开发模式下,每次rebuild完成的时候,各自完成相应职责。

通过以上分析可以看出,每个插件都有特定的“介入点”:

  • 有的在编译前后(如CleanWebpackPlugin前置清理,HtmlWebpackPlugin尾部生成);
  • 有的贯穿编译全过程(如HMR、DefinePlugin始终影响模块处理);
  • 有的纯粹在输出环节(如压缩类插件,属性注入插件)。

它们共同辅助Webpack将源代码转化为最终产品:既提高了构建效率(多线程、自动刷新)、又优化了构建结果(分包、压缩、提取CSS)、还方便了开发使用(全局变量提供、自动生成HTML)。对于非专业人士,可以将Webpack想象成一个流水线工厂:配置里面的Loader是不同工序的机器,Plugin则是工厂里的“智能管家”和“助手”,在关键节点上协助或改变生产流程。经过这一系列流水线作业,我们的原始源码材料被加工成适合发布的成果。这就是Webpack在本项目中的工作原理和配置方式,全程自动、高效,并且通过合理的配置让开发与部署变得更加容易。

昨天以前首页

Webpack Loader 执行机制

作者 ak啊
2025年4月1日 17:28

一、Loader 链式调用机制

Loader 的执行分为 Pitch 阶段Normal 阶段,两者共同构成链式调用逻辑。


1. Pitch 阶段
  • 执行顺序:从左到右(与 Normal 阶段相反)。
  • 核心作用:拦截机制。如果某个 Loader 的 pitch 方法返回非 undefined 值,直接跳过后续 Loader,进入 Normal 阶段的逆向执行。
  • 伪代码逻辑
    const result = loaderA.pitch(remainingRequest, previousRequest, data);
    if (result !== undefined) {
      // 跳过后续 Loader,进入 Normal 阶段逆向执行
    }
    
2. Normal 阶段
  • 执行顺序:从右到左。
  • 核心作用:实际处理文件内容,上一个 Loader 的输出是下一个 Loader 的输入。

二、源码转换流程(runLoaders 核心逻辑)

Webpack 使用 loader-runner 模块处理 Loader 链。以下是简化后的源码分析:

关键源码:runLoaders 函数(简化版)
function runLoaders(resource, loaders, context, callback) {
  const loaderContext = context || {};
  let loaderIndex = 0; // 当前执行的 Loader 索引
  let processOptions = {
    resourceBuffer: null,
    readResource: fs.readFile.bind(fs)
  };

  // 迭代执行 Pitch 阶段
  iteratePitchingLoaders(processOptions, loaderContext, (err, result) => {
    if (err) return callback(err);
    callback(null, ...result);
  });

  function iteratePitchingLoaders(options, loaderContext, callback) {
    if (loaderIndex >= loaders.length) {
      // 所有 Pitch 执行完毕,读取资源
      return processResource(options, loaderContext, callback);
    }

    const currentLoader = loaders[loaderIndex];
    const pitchFn = currentLoader.pitch;

    loaderIndex++; // 移动到下一个 Loader

    if (!pitchFn) {
      // 没有 pitch 方法,继续下一个 Loader
      return iteratePitchingLoaders(options, loaderContext, callback);
    }

    // 执行当前 Loader 的 pitch 方法
    pitchFn.call(
      loaderContext,
      loaderContext.remainingRequest,
      loaderContext.previousRequest,
      (currentLoader.data = {})
    ), (err, ...args) => {
      if (args.length > 0) {
        const hasResult = args.some(arg => arg !== undefined);
        if (hasResult) {
          // Pitch 返回结果,跳过后续 Loader,逆向执行 Normal
          loaderIndex--;
          iterateNormalLoaders(options, loaderContext, args, callback);
          return;
        }
      }
      // 继续下一个 Pitch
      iteratePitchingLoaders(options, loaderContext, callback);
    });
  }

  function processResource(options, loaderContext, callback) {
    // 读取原始资源内容
    options.readResource(loaderContext.resource, (err, buffer) => {
      const resourceBuffer = buffer;
      iterateNormalLoaders(options, loaderContext, [resourceBuffer], callback);
    });
  }

  function iterateNormalLoaders(options, loaderContext, args, callback) {
    if (loaderIndex < 0) {
      // 所有 Normal 阶段完成
      return callback(null, args);
    }

    const currentLoader = loaders[loaderIndex];
    const normalFn = currentLoader.normal || currentLoader;

    loaderIndex--; // 逆向执行

    // 执行当前 Loader 的 Normal 方法
    normalFn.call(loaderContext, args[0], (err, ...returnArgs) => {
      if (err) return callback(err);
      iterateNormalLoaders(options, loaderContext, returnArgs, callback);
    });
  }
}

三、执行流程详解

  1. Pitch 阶段从左到右执行

    • 依次调用每个 Loader 的 pitch 方法。
    • 若某个 pitch 返回结果,跳过后续 Loader,直接进入 Normal 阶段。
  2. 读取资源文件

    • 若所有 pitch 均未拦截,读取原始文件内容。
  3. Normal 阶段从右到左执行

    • 将资源内容传递给最后一个 Loader 处理,结果逆向传递。

四、典型使用案例

案例:自定义 Loader 链观察执行顺序

Loader 配置

// webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.txt$/,
        use: [
          './loaders/loaderA.js',
          './loaders/loaderB.js',
          './loaders/loaderC.js'
        ]
      }
    ]
  }
};

Loader 实现

// loaderA.js
module.exports = function(source) {
  console.log('[Normal A]');
  return source + '-A';
};
module.exports.pitch = function() {
  console.log('[Pitch A]');
};

// loaderB.js
module.exports = function(source) {
  console.log('[Normal B]');
  return source + '-B';
};
module.exports.pitch = function() {
  console.log('[Pitch B]');
  // 返回非 undefined 值,拦截后续 Loader
  return '拦截内容';
};

// loaderC.js
module.exports = function(source) {
  console.log('[Normal C]');
  return source + '-C';
};
module.exports.pitch = function() {
  console.log('[Pitch C]');
};

执行结果

[Pitch A]
[Pitch B]  // B 的 pitch 返回拦截内容,跳过后续 Pitch
[Normal B] // 进入 Normal 阶段,从 B 开始逆向执行
[Normal A]
最终结果: "拦截内容-B-A"

五、关键总结

  1. Pitch 拦截:通过 pitch 方法提前返回结果,优化构建流程。
  2. 执行方向
    • Pitch:从左到右。
    • Normal:从右到左(若未拦截)。
  3. 资源处理runLoaders 通过 iteratePitchingLoadersiterateNormalLoaders 实现链式调用。

基于vite官方开源脚手架预设,实现一个 npm create template-vue3-ts-preset(2):分析入口文件

作者 大专哥
2025年3月31日 17:58

上一篇文章中我们寻找到了create-vite项目中,脚手架要运行的index入口文件,本篇主要讲解在入口文件,脚手架主要做了什么

最终实现效果:通过 pnpm create template-vue3-ts-preset 安装我们自己的项目

源码:这里

声明:本项目的核心源码也是基于开源vite修改而来,本质是想让大家明白创建一个脚手架并发布到npm上走完流程。

前置知识:了解npm、node 以及 《如何创建一个本地的脚手架》《基于vite官方开源脚手架预设,实现一个 npm create template-vue3-ts-preset(1):寻找 create-vite入口》

  1. 首先我们先观察代码最后一段,发现这样一句

image.png 此时我们可以看到有一个init方法执行了,那么经常编码的同学都知道这大概是一个程序的初始化功能,那么我们去文中寻找init这个方法

1. 首先第一句话为:

image.png 我们可以通过这句话拿到几个关键要素,argv以及formatTargetDir
2. 在全局搜索 这两个方法

image.png 首先这句话是采用了mri的方法 那么mri是啥,通过ai搜索我们可知,这个是一个命令参数解析器,我们先不管他的ts类型,先从这个方法的第一个参数看起

  • process.argv.slice(2): process是一个全局的node对象, argv是node中的一个方法,然后用slice(2)来截取,通过询问或查询资料可以得到:process.argv 是获取命令行参数数组的第三个值。
  • 第二个参数
      {
  alias: { h: 'help', t: 'template' },
  boolean: ['help', 'overwrite'],
  string: ['template'],
} 

这是一个对象 具体作用未知,然后我们查阅node文档可知 mri 方法的作用:就是用于解析命令行参数,并返回一个对象,接受两个参数,第一个参数是node中输入的命令,第二个参数是一些配置选项 所以这个方法其实是拿到我们cmd中输入的一些命令,例如:

    import mri from 'mri'
const argv = mri<{
    template?: string
    help?: boolean
    overwrite?: boolean
  }>(process.argv.slice(2), {
    alias: { h: 'help', t: 'template' },
    boolean: ['help', 'overwrite'],
    string: ['template'],
  })


const init = async () => {
    console.log(argv)
     const s=(argv._[0])
     console.log(s)
}
init()
运行:node index.js  1234
输出:{ _: [ '1234' ] }   1234


image.png 这个方法就比较简单了,接受一个参数 类型为string 去掉参数前后的空格以及末尾多余的“/”

所以:image.png 的作用是:获取用户输入内容的第三个字符串 首先判断它是否为空 如果为空则为undefined,如果不为空则去掉前后空格以及多余的“/”

接着我们继续往下看:

image.png 前三局句代码我们比较容易理解,他就是argv方法的第二个参数的前两个对象当中的,实际上我们获取到的:

  1. argTemplate 就是获取我们输入内容-t后的别名, string类型
  2. argOverwrite 则是判断目录是否要求被覆盖 boolean类型
  3. help 则是判断用户是否输入了 --help或者--h。boolean类型

下面的判断则是首先判断你是否输入了 --help或者--h 如果输入了就返回 帮助命令,然后推出 下面是 helpMessage: image.png 这些我们之后再研究,其实就是输出了一些文本命令,来帮助你需要vite做什么

接着我们继续往下看

image.png

第一句调用了pkgFromUserAgent 并传入了process.env.npm_config_user_agent

我们一步一步来分析:

  1. 首先pkgFromUserAgent 方法:
    function pkgFromUserAgent(userAgent: string | undefined): PkgInfo | undefined {
  if (!userAgent) return undefined
  const pkgSpec = userAgent.split(' ')[0]
  const pkgSpecArr = pkgSpec.split('/')
  return {
    name: pkgSpecArr[0],
    version: pkgSpecArr[1],
  }
}

很显然,这个方法就是将参数userAgent用空格进行切割,然后取第0位 然后将第0位在用/进行分割 将分割出来的数据第一个返回name的value 第二个为version的value

  1. 紧接着我们分析 process.env.npm_config_user_agent,通过经验我们可以分析到,这个似乎是node的某个信息,我们尝试运行 node process.env.npm_config_user_agent 发现打印出来为undefined 经过查询可知 这句话实际上是需要通过npm、pnpm 等包管理器运行才能拿到,因为是需要获取包管理器的信息的,(尝试:将这句话写到某个node脚本中,然后通过package的script 运行)所以我们大致可以得到这样一个结论:这个方法的意思是解析出我们当前使用的具体是哪一个包管理器(npm/pnpm/yarn....)和版本信息包括获取node版本信息等等 例如:

image.png

image.png

image.png

第二句,则是调用了prompts.cancel 的方法传入了“peration cancelled”。

我们接着分析prompts.cancel

  1. 首先我们通过全局搜索prompts 发现它来源于一个第三方库:

image.png 这是一个CLI交互库,其主要作用是在终端中与用户进行交互式对话。比如,我们在创建项目时,需要一步一步输入项目名称,选择模版等等,大概分为一下使用场景:

image.png 由此我们可知:prompts.cancel('Operation cancelled')实际上就是推出或者取消操作 并输出'Operation cancelled'

接着我们继续往下看

image.png 我们会发现其实这里的注释大致已经讲清楚了他的作用,主要是用来获取项目名称和目标目录的。 这里我会逐步进行分析:

  1. 首先:let targetDir = argTargetDir 在前面提到 argTargetDir是用来获取我们输入的用空格隔开的第三个内容的,其实就是我们输入命令之后的内容。
  2. 紧接着这里判断我们的输入是否存在,如果不存在的话就执行下面的内容,如果存在的话,首先是异步执行promps.text 方法,这个方法在上面的使用场景可知,他是获取文本输入的,其中message主要是提示用户的文字,defaultValue是默认选项,如果用户直接回车,则返回这个,placeHolder则是灰色的提示文字。
  3. 紧接着会用 prompts.isCancel来判断用户的输入,因为这里具有默认值,如果rojectName 为空只有一种可能:那就是用户进行了推出或者取消操作,那么此时我们直接推出就好了
  4. 接着是将我们输入的文字进行格式化,去掉前后空格之类的,这个方法我们前面已经说过了

接着我们继续往下看:

image.png 通过注释我们可知,这段代码的主要是在目录不为空的情况下进行的逻辑处理

  1. 首先这个判断中fs.existsSync(targetDir) fs是node当中查询本地文件的一个模块,这里主要是判断是否存在我们输入的文件夹名称,后半部分是判断 这个文件夹是否为空然后取反,结合起来就是:判断是否存在当前文件夹并且不为空。
  2. 紧接着就是判断目标文件夹不为空是否覆盖,如果覆盖则overwrite为yes,否则通过prompts,给用户提供了三个选择:推出 / 删除目录下的所有文件然后继续(等于清空文件夹)/ 不管里面有没有东西,都在里面创建项目
  3. 接着判断overwrite 为取消,如果是的话,就推出
  4. 这里的switch判断主要是对两种选择做出了操作,一个是yes,(清空文件夹),一个是no(推出程序)

接着我们继续往下看:

image.png

  1. 接着packageName是获取当前文件夹的绝对路径,作用是在创建我们的项目时指定路径,fs在对文件操作时需要绝对路径
  2. 紧接着使用isValidPackageName判断路径是否规范,如果中间有空格的话可能就会不合法
  3. 在路径不合法情况下为用户弹出一个输入内容,toValidPackageName(packageName)方法会给你一个合法的名称,如果你仍然坚持不合法的名称就会触发 输出 Invalid package.json name 但是此时程序并没有推出,但是也无法进行下一步,直到你输入一个正确的名称或者推出
  4. 这里判断你有没有推出,如果为true就推出
  5. 接着将这个绝对路径的名称赋值给packageName

接着我们继续往下看:

// Choose a framework and variant 内容中会有这一句注释(选择框架或者变体)这里就是需要我们执行create vite 输入名称之后 需要后续执行的操作

image.png

  1. 首先将我们在 create vite --t xxx 之后输入的内容赋值给 template ,然后将hasInvalidArgTemplate先设fasle
  2. 紧接着判断argTemplate 是否存在并且是否不在我们的模版名称之内:TEMPLATES的定义如下:

image.png 可以看到的是将 FRAMEWORKS中提取所有的variant.name 并扁平化一个数组,后续调用的时候用了includes方法来判断是否匹配

搜索FRAMEWORKS 我们大致可以看到这是一个数组,里面的内容其实就和我们外面的模版基本匹配,然后提取了一些要素, image.png

  1. 接着我们回到原文,当存在名称并不在模版中时候,将 template为空并将hasInvalidArgTemplate 修改为true

  2. 接着判断template是否存在,由上面可知,当输入--t后的名称存在并且不在预设模版中时 template就为undefined,此时就会执行这个if当中的代码,否则就跳过,(验证触发: pnpm create vite --t demo 输入这条命令 node就会执行 "${argTemplate}" isn't a valid template. Please choose from below: 当输入 pnpm create vite --t template-solid-ts 等时候,则会跳过这段代码

  3. 我们接着来看 判断中首先会触发一个互动,满足4的条件之后,option 展示FRAMEWORKS的所有选项,前文我们已经提到了,这里会展示第一层所有选项,最后将这个选项返回给framework

接着我们继续往下

image.png

  1. 这里首先会再次判断是否为空,为空直接推出
  2. 接着再次出发点一个互动,让选择以那种变体开发,也就是我们在创建项目时选择ts js...等一些操作
  3. 这里的逻辑操作基本和上面一样,所需要注意的只有getFullCustomCommand方法

image.png 这个方法实际上就是将 预设中的的包管理器(npm、pnpm等) 转换成我们使用的包管理器。 接下来我们一步一步分析这个方法

  1. 首先这个方法接受两个参数:customCommand:string以及pkgInfo?: PkgInfo 类型PkgInfo=interface PkgInfo { name: string version: string }
  2. 第一句的意思是判断当前是否使用了包管理器 有的话就用当前的,没有的话就用npm
  3. 第二句的意思是判断当前是不是yarn 如果是的话 版本必须为1.xxx 否则返回false这是一个boolean类型
  4. 接着返回customCommand 也就是我们输入的命令 首先用正则匹配npm 在满足条件的情况下,判断是否为 bun 、pnpm 满足的情况下返回对应的 包命令, 最后如果有其他情况则保留原有的格式
  5. 当不满足npm正则时候则匹配 isYarn 变量 如果满足就返回空,如果不满足的话就就去掉@latest
  6. 最后再用正则匹配下 pm exec 如果满足则将npm exec 替换成运行临时包的命令

好了,让我们在回到之前的代码当中, 然后判断下是否为空,为空推出。 最终将这个值赋值给template #我们接着来分析

image.png

  1. 首先我们将 向path.jion方法中传入全局变量中的cwd(也就是当前工作目录),在传入 我们targetDir(我们 在vite 后跟的第一个字符串),生成完整的绝对路径返回给root
  2. 紧接着通过 fs.mkdirSync 来创建新的文件夹
  3. 接着创建一个变量isReactSwc 默认为false
  4. 再然后我们拿到template 也就是包名,判断一下是否包含-swc这个字段,如果包含就把template设置为true,然后把名称中的swc删除掉,这一步其实是在针对react 因为这里reactswc模式要比babel-loader要快
  5. 再然后将获得的pkginfo做一下判断如果有就用输入的,如果没有默认用npm
  6. 再然后就用FRAMEWORKS循环对比判断其中的variants对象中的name是否与tamplate匹配 如果匹配就返回其中的customCommand解构给customCommand

我们接着往下

image.png

  1. 当结构完成之后,首先判断结构的值存不存在,如果存在的话,就调用getFullCustomCommand方法将名称以及包管理器名称作为参数传入 我们之前已经分析过getFullCustomCommand最后返回 成我们命令行中对应的包名+模版名称
  2. 然后通过结构拿到我们对应的包管理器名称,
  3. 再然后把命令参数中的TARGET_DIR 替换成我们所需的项目目录名称
  4. 接着同步执行这个命令,并传入管理器名称,目录名称 等,
  5. 当代码执行完毕以后,使用process.exit进行推出,
  6. 然后打印Scaffolding project in ${root}...

    我们接着往下

image.png

  1. 这里定义了一个templateDir 这个主要是用来获取模版的绝对路径的因为template之前源于variant variant本身并没有完整的模版名,所以这里需要拼接写 模版名,最终得到一个完整的外部的模版名称,
  2. 再然后就比较简单了,创建write利用fs模块 来创建文件了,首先先判断是文件还是文件夹,有如果是文件夹就创建文件夹没否则就拷贝文件(单个文件)
  3. 再然后就获取目前模板的绝对路径,赋值给files
  4. 然后就是设置package.json的一些关键信息
  5. 再然后我们需要写入自己的packge.json文件,因为package.json文件本身包含一些文件信息,所以不能直接拷贝
  6. 接下来的if (isReactSwc) 则是针对react-swc的操作,如果选择了这个那么就得是ts

然后接下来的就主要是输入到log中的内容了。

  1. 首先设置一个doneMessage为空字符串
  2. 然后获取我们创建的项目和当前目录之间的差异路径
  3. 将差异路径赋值给donemessage
  4. 然后判断创建目录的路径是否等于当前目录的路径,如果相等的话 ,就将其中的正则匹配通过三元表达式赋值给doneMessage 5.因为之前我们已经哪都pkgManager 他就包管理器名称,这里主要是针对yarn进行区分,然后拼接对应的字符串,最后通过 prompts.outro(doneMessage) 输出到页面

到此整个npm crete vite的功能基本已经实现了:接下来我们总结下这个主要干了些什么事情:

总结:

1. 首先是获取项目名称以及目标目录,
2. 对目标目录进行处理
3. 再然后就是获取我们想创建什么样的项目
4. 执行创建命令创建对应的项目。









 




❌
❌