模块化
什么是模块化开发,为什么要使用它?
模块化开发,字面意思就是分模块进行开发。
javascript
// example-one.js
var age = 18;
// example-two.js
var age = 20
// index.html
<script src="example-one.js"></script>
<script src="example-two.js"></script>
<script>
console.log(age);
</script>
在没有模块化开发的年代,多人开发时,同名变量会受到干扰,所以就有了模块化开发AMD、UMD。
当然AMD和UMD不重点讲述,主要聊的是常用的 CommonJS 与ES Module
聊聊 CommonJS 和 ES Module
CommonJS:
- 语法 :使用
require()
导入模块,module.exports
或exports
导出模块。 - 场景:Node.js 默认模块系统,适用于服务端开发(同步加载)。
- 示例:
ini
// math.js 导出模块
module.exports = { add: (a, b) => a + b };
// index.js 导入模块
const math = require('./math.js');
math.add(2, 3);
ES Module
- 语法 :使用
import
导入模块,export
导出模块。 - 场景:浏览器原生支持及现代前端工程化项目(异步加载)。
- 示例:
csharp
// math.js 导出模块
export const add = (a, b) => a + b;
// index.js 导入模块
import { add } from './math.js';
add(2, 3);
区别:
CommonJS 将文件内的变量值拷贝给 module.exports 导出一个对象,使用者可以按需加载该文件,它是一个同步操作,后续内容需要等待它完成。它是有缓存的,其缓存是根据 loaded 属性是否为 true 判断。
ESmodule 先进行分析文件,在文件头部根据 import 解析相关内容,进行fetch获取,形成模块记录,然后模块实例化分配空间,将导入和导出的内存地址相连接,最后运行代码,将值填充到内存中。
构建与打包
什么是构建与打包?为什么需要它们?
- 构建:对代码进行编译、转换、优化等操作(如将TypeScript转成JavaScript、Sass转成CSS)。
- 打包:将多个分散的代码文件合并为少数几个文件(或按需分块),解决依赖关系并优化资源加载。
常用的构建打包工具有哪些?具体使用?
常见工具与场景
工具 | 特点 | 适用场景 |
---|---|---|
Webpack | 高度可配置,插件生态丰富 | 复杂SPA、需深度定制化 |
Rollup | 面向库的打包,Tree Shaking效率高 | 开发开源库、组件库 |
Vite | 基于ESM的按需编译,开发模式极速热更新 | 现代框架(Vue/React)项目 |
聊下webpack 的内容以及它的原理和使用?
一、Webpack 的定义与定位
Webpack 是一个静态模块打包工具,它将项目中的所有资源(JS、CSS、图片等)视为模块,通过依赖关系构建依赖图(Dependency Graph),最终生成优化后的静态资源包。
核心目标:
- 模块化支持:处理 CommonJS、ES Modules、AMD 等模块规范
- 代码转换:通过 Loader 编译非 JS 资源(如 TypeScript → JS,Sass → CSS)
- 性能优化:代码压缩、Tree Shaking、代码分割
- 生态扩展:通过插件系统实现功能增强(如 HTML 生成、环境变量注入)
二、原理和使用
- 项目中的拆分
在项目中,我们通常分为三个文件:
基础配置(webpack.base.js)
开发配置(webpack.dev.js)
生产配置(webpack.prod.js)
- 入口(Entry)
入口主要是打包的起点文件,Webpack 从入口开始递归分析依赖。
java
// webpack.base.js
module.exports = {
entry: './src/index.js' // 单入口
// 多入口:entry: { app: './src/app.js', admin: './src/admin.js' }
};
- 输出(Output)
指定打包后的文件存储位置和命名规则。
arduino
// webpack.dev.js 开发环境的 output 配置 产物输出路径,因为开发和生产环境输出不一致,所以在各自环境中自行配置
// devServer 配置
const DEV_SERVER_CONFIG = {
HOST: '127.0.0.1',
PORT: 9002,
HMR_PATH: '__webpack_hmr', // 官方规定
TIMEOUT: 20000
}
module.exports = {
output: {
filename: 'js/[name]_[chunkhash:8].bundle.js',
path: path.join(process.cwd(), './app/public/dist/dev/'), // 输出文件存储路径(自定义)
publicPath: `http://${DEV_SERVER_CONFIG.HOST}:${DEV_SERVER_CONFIG.PORT}/public/dist/dev/`,
// 自建服务器,外部资源公共路径(主要用于热更新)
globalObject: 'this'
}
}
// webpack.prod.js 生产环境的 output 配置
module.exports = {
output: {
filename: 'js/[name]_[chunkhash:8].bundle.js',
path: path.join(process.cwd(), './app/public/dist/prod'), // 输出文件存储路径(自定义)
publicPath: '/dist/prod',
crossOriginLoading: 'anonymous'
}
}
- Loader
处理非 JS 文件的转换器,类似"翻译官"。
javascript
// webpack.base.js
module.exports = {
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'
}]
}
}
// webpack.prod.js
module.exports = {
module: {
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主要用于多进程打包,提高打包效率
- 插件(Plugins)
扩展 Webpack 功能,处理 Loader 无法实现的任务(如资源优化、环境注入)。插件可以在webpack 的任意生命周期进行注入,提高了拓展性。
php
// webpack.base.js
// 配置 webpack 插件
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { VueLoaderPlugin } = require('vue-loader');
const htmlWebpackPluginList = [];
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 ]
})
)
// 如果是多入口打包,对所有目标文件遍历进行以上操作即可
module.exports = {
plugins: [
// 处理 .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', // 禁用生产环境显示"水合" 信息
}),
// 构造最终渲染的页面模版
...htmlWebpackPluginList
]
}
// webpack.dev.js
const webpack = require('webpack');
module.exports = {
// 开发阶段插件
plugins: [
// HotModuleReplacementPlugin 用于实现热模块替换 ( Hot Module Replacement 简称 HMR)
// 模块热替换允许在应用程序运行时替换模块
// 极大的提升开发效率,因为能让应用程序一直保持运行状态
new webpack.HotModuleReplacementPlugin({
multiStep: false
})
]
}
// webpack.prod.js
const os = require('os');
const HappyPack = require('happypack');
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');
// 多线程 build 设置
const happypackCommonConfig = {
debug: false,
threadPool: HappyPack.ThreadPool({ size: os.cpus().length })
}
module.exports = {
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'
})
]
}
- 分包(Chunk)与其他优化
根据入口和代码分割原则,将模块分组为chunk。丑化压缩代码,减少没必要的损耗。
javascript
// webpack.base.js
// 配置打包输出优化(配置代码分割,模块合并,缓存,TreeShaking,压缩等优化策略)
module.exports = {
optimization: {
/**
* 把 js 文件打包成3种类型
* 1. vendor: 第三方 lib库,基本不会改的,除非依赖版本升级
* 2. common: 业务组件代码的公共部分抽取出来,改动较少
* 3. entry.{page}: 不用页面 entry 里的业务组件代码的差异部分,会经常改动
* 目的:把改动和引用频率不一样的 js 区分出来,以达到更好利用浏览器缓存的效果
*/
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, // 最小分割文件的大小(1 byte)
priority: 10, // 优先级
reuseExistingChunk: true, // 复用已有的公共 chunk
}
}
}
}
}
// webpack.prod.js
const TerserWebpackPlugin = require('terser-webpack-plugin');
module.exports = {
optimization: {
// 使用 TerserPlugin 的并发和缓存,提升压缩阶段的性能
// 清除 console.log
minimize: true,
minimizer: [
new TerserWebpackPlugin({
cache: true, // 启动缓存来加载构建过程
parallel: true, // 利用多核 CPU 的优势来加快压缩速度
terserOptions: {
compress: {
drop_console: true, // 去掉 console.log 内容
}
}
})
]
}
}
- 热更新(HMR)
热模块替换(Hot Module Replacement, HMR) 是 Webpack 提供的一项功能,允许在应用运行时动态替换、添加或删除模块,无需完全刷新页面。它能保留应用状态(如表单输入、路由位置),显著提升开发体验。
javascript
// 本地开发启动 devServer
const express = require('express');
const path = require('path');
const consoler = require('consoler');
const webpack = require('webpack');
const devMiddleware = require('webpack-dev-middleware');
const hotMiddleware = require('webpack-hot-middleware');
// 从 webpack.dev.js 获取 webpack配置 和 devServer 配置
const {
// webpack 配置
webpackConfig,
// devServer 配置,暴露给dev.js 使用
DEV_SERVER_CONFIG
} = require('./config/webpack.dev');
const app = 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: () => {}
}));
consoler.info('请等待webpack初次构建完成提示...')
// 启动 devServer
const port = DEV_SERVER_CONFIG.PORT;
app.listen(port, () => {
console.log(`app listening on port ${port}`)
});
从上述图与代码可以描述出,我们搭建一套express服务器 devServer,当代码发生变化时,我们将通过解析引擎重新进行解析。
devServer服务器通过 devMiddleware,监听到打包的文件发生变化。而后devServer服务器通过 hotMiddleware 通知客户端,代码发生了变化。最后客户端重新从devServer服务器获取最新代码。
总结:以上是关于工程化的部分理解,还有开发规范、CI/CD(持续集成/持续部署)、前端性能优化、监控与质量保障、微前端架构等,不在本文过多赘述。工程化给开发者带来了便捷,同时合理的使用工程化,会让项目稳固进行下去。