前言
继完成了elpis-core的内核引擎开发后,本篇主要针对前端工程化这一块进行了实践学习,这里主要使用webpack5作为构建工具,接下来就展开聊聊。
前端工程化
作为前端开发,前端工程化已经是一个老生常谈的问题了,这里还是得提一嘴什么是前端工程化,前端工程化是一个体系,是指将软件工程的方法论应用到前端开发中,通过一系列工具、规范和最佳实践,使前端开发更加标准化、自动化、高效化和可维护。它解决了随着前端项目复杂度提升而带来的各种问题。
核心内容
构建工具与自动化
- 代码转译:使用Babel等工具将现代JavaScript转译为浏览器兼容的版本。
- 模块打包:通过Webpack、Rollup等工具将模块化代码打包为浏览器可执行的文件。
- 资源压缩:压缩JS、CSS、HTML文件以减少体积。
- 代码分割:按需加载代码,优化首屏加载性能。
模块化与组件化
- 模块化开发:将代码拆分为独立的模块,如CommonJS、ES Modules。
- 组件化开发:UI组件的封装与复用,如Vue组件、React组件。
- 规范化目录结构:合理组织项目文件和代码。
规范化与标准化
- 代码规范:使用ESLint、Prettier等工具确保代码质量和一致性。
- 提交规范:如Angular Commit Message格式,使用commitizen工具辅助。
- 开发规范:制定团队遵循的开发标准和最佳实践。
持续集成与部署
- CI/CD流程:通过GitLab CI、GitHub Actions等实现自动化构建和部署。
- 自动化发布:简化版本发布流程。
- 环境管理:不同环境(开发、测试、生产)的配置管理。
性能优化
- 静态资源优化:图片压缩、字体优化等。
- 加载优化:懒加载、预加载、缓存策略。
- 构建时优化:tree-shaking、代码分割等。
前端工程化的实践意义
- 提高开发效率 :自动化工具减少重复工作。
- 保证代码质量 :规范和工具确保代码一致性和可靠性。
- 优化用户体验 :性能优化提升应用加载和运行速度。
- 便于团队协作 :标准化流程降低沟通成本。
- 降低维护成本 :良好的架构和规范使代码更容易维护和扩展。
通过这一系列工程化手段,可以使前端开发从"写代码"转变为更加系统化、规范化的软件工程实践。由于篇幅限制,这里就不涉及其他内容,就针对于webpack配置这一块做一个总结。
整体流程
首先我们需要在项目中建立好关于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,即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配置项,对于对工程化的理解也有了深一步的理解,当然,对于工程化的体系而言,这还只是冰上一角,那么,继续加油吧~~