学习抖音哲玄《大前端全栈实践》课程之 webpack前端工程化
前言
本文是根据抖音哲玄《大前端全栈实践》课程学习的笔记,重点讨论基于webpack实现前端工程化的过程。该内容是课程的里程碑2,在完成里程碑1(后端搭建)后,我们已能够从浏览器访问一个页面,并成功渲染数据。
然而,之前我们编写的页面是一个简单的HTML页面,它包括三部分:HTML结构、CSS样式和JavaScript行为。随着前端技术的不断发展,构建页面框架已经不再仅仅是编写HTML语法。为了提高开发效率和代码复用性,很多组件库应运而生,如Element Plus,专为后台类项目设计。
在样式部分,编写CSS不再局限于原生CSS,开发者更倾向于使用预处理器,如Less和Sass(SCSS)。这些工具不仅使样式更加灵活和可维护,也扩展了CSS的功能。
在行为部分,传统的JavaScript也逐渐被ES6+等现代语法所替代,这带来了模块化开发的优势。使用ES6语法时,我们需要通过export和import进行模块的导入和导出。然而,浏览器对这些新语法的兼容性较差,特别是ES6模块化语法,浏览器本身无法直接识别和执行这些代码。
此外,当我们使用Less、Sass、Vue等框架或预处理器时,文件的后缀分别是.less、.scss和.vue,这些文件格式同样不被浏览器直接识别。因此,我们需要将这些包含依赖关系的文件转化为浏览器能够理解的语法和格式。这个转化过程,正是前端工程化所需要解决的核心问题。
在实际开发中,Webpack作为一种打包工具,能够帮助我们自动化地处理这些不同类型的资源,优化开发流程,提升开发效率。通过配置Webpack,我们可以将各种模块化的代码、预处理过的样式文件、Vue组件等资源,打包成浏览器能够直接加载的文件,进而实现高效的前端工程化开发。
webpack工程化配置目录
|--app
|--pages
|--page1
|--entry.page1.js
|--page1.css
|--page1.vue
|--page2
|--entry.page2.js
|--page2.css
|--page2.vue
|--public
|--dist
|--webpack
|--config
|--webpack.base.js
|--webpack.dev.js
|--webpack.prod.js
|--dev.js
|--prod.js
我们知道,浏览器访问的页面是经过webpack打包处理过的,一般规定打包后的文件存在public/dist文件下,那么怎么让webpack去干活呢?
我们规定开发者写页面时,统一在pages文件下写,
如图中的pages/page1.vue和pages/page2.vue,
pages/page1.vue页面引用了pages/entry.page1.js和pages/page1.css,
pages/page2.vue页面引用了pages/entry.page2.js和pages/page2.css
最后我们需要webpack转化为public/dist/entry.page1.tpl和public/dist/entry.page2.tpl,
后端规定访问/view/:page时,如访问view/page1则去拿到打包后的public/dist/entry.page1,
若访问view/page2则去拿到public/dist/entry.page2
要完成这一过程,则需要去配置webpack,首先要告诉webpack要转化的文件入口,
在elpise项目入口文件是{ 'entry.page1': './app/pages/page1/entry.page1.js', 'entry.page2': './app/pages/page2/entry.page2.js' }
然后告诉webpack打包后的文件放在哪,当然了,一般规定public/dist,
区分开发环境生产环境则可以这样配 public/dist/dev public/dist/prod
那么webpack怎么处理.vue,.less,.scss文件呢?那就需要各种各样的loader去处理,具体配置见下文
//以下是prod.js文件
const webpack = require("webpack");
const webpackProdConfig = require("./config/webpack.prod.js");
webpack(webpackProdConfig, (err, stats) => {
if (err) {
console.log(err);
return;
}
process.stdout.write(
`${stats.toString({
colors: true, //在控制台输出色彩信息
modules: false, //不显示每个模块的打包信息
children: false, //不显示子编译任务的信息
chunks: false, //不显示每个代码块的信息
chunkModules: false, //显示代码块中模块信息
})}\n`
);
});
//以下是webpack.base.js文件
const glob = require("glob");
const webpack = require("webpack");
const path = require("path");
const { VueLoaderPlugin } = require("vue-loader");
const HtmlWebpackPlugin = require("html-webpack-plugin");
//动态构造pageEntries htmlWebpackPluginList
const pageEntries = {};
const htmlWebpackPluginList = [];
//获取app/pages 目录下所有入口文件 (entry.page1.js, entry.page2.js,entry.xxx.js)
const entryList = path.resolve(process.cwd(), "./app/pages/**/entry.*.js");
glob.sync(entryList).forEach((entry) => {
const entryName = path.basename(entry, ".js");
//构造entry
pageEntries[entryName] = entry;
//构造最佳渲染的页面文件
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$/,
include: [
//只对业务代码进行babel,加快webpack打包速度
path.resolve(process.cwd(), "./app/pages"),
],
use: {
loader: "babel-loader",
},
},
{
test: /.(png|jpe?g|gif)(?=.*)?$/,
use: {
loader: "asset",
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"],
},
{
test: /.scss$/,
use: ["style-loader", "css-loader", "sass-loader"],
},
],
},
//产物输出配置,因为开发和生产环境输出不一致,所以在各自环境中自行配置
output: {},
//配置模块解析的具体行为(定义webpack再打包时,如何找到并解析具体模块的路径)
resolve: {
extensions: [".js", ".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插件
plugins: [
//处理.vue文件,这个插件是必须的,它只能将你定义过的其他规则复制并应用到.vue文件里
//例如,如果有一条匹配规则/.js$/的规则,那么它会将这个规则应用到.vue文件里的<script>部分
new VueLoaderPlugin(),
//第三方库暴露到window context下
new webpack.ProvidePlugin({
Vue: "vue",
axios: "axios",
_: "lodash",
}),
//定义全局常量
new webpack.DefinePlugin({
//支持Vue解析optionsApi
_VUE_OPTIONS_API_: true,
//是否支持devtools,禁用Vue调试工具
_VUE_PROD_DEVTOOLS_: false,
//禁用生产环境显示'水合'信息
_VUE_PROD_HYDRATION_MISMATCH_DETAILS: false,
}),
...htmlWebpackPluginList,
],
//配置打包输出优化(代码分割,模块合并,缓存,TreeShaking,压缩等优化策略)
optimization: {
/**
* 把js文件打包成三种类型
* 1.vendor:第三方lib库,基本不会改动,除非依赖版本升级
* 2.common:业务组件代码的公共部分抽取出来,基本不会改动
* 3.entry.(page1,page2,xxx):不用entry里的业务组件代码的差异部分,会经常改动
* 目的:把改动和引用频率不一样的js区分出来,以达到更好利用浏览器缓存的效果
*/
splitChunks: {
chunks: "all", //对同步和异步模块都进行分割
maxAsyncRequests: 10, //按需加载时的最大并行请求数
maxInitialRequests: 10, //入口点的最大并行请求数
cacheGroups: {
vendor: {
//第三方依赖库
test: /[\/]node_modules[\/]/, //打包node_modules下的文件
name: "vendor", //模块文件名
priority: 20, //优先级,数字越大优先级越高
enforce: true, //是否强制将模块拆分到缓存组中,
reuseExistingChunk: true, ////如果该模块已经被打包过了,则直接从缓存组中引用该模块,而不是重新打包
},
common: {
//公共模块
name: "common", //模块名称
minChunks: 2, //模块被引用次数大于等于2次,就会被归为公共部分
minSize: 1, //最小分割文件大小(1 byte)
priority: 10, //优先级
reuseExistingChunk: true, //复用已有的公共chunk
},
},
},
//将webpack运行时生成的代码打包到runtime.js中
runtimeChunk: true,
},
};
//以下是webpack.prod.js文件
const path = require("path");
const merge = require("webpack-merge");
const os = require("os");
const HappyPack = require("happypack");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const CleanWebpackPlugin = require("clean-webpack-plugin");
const CSSMinizisePlugin = require("css-minimizer-webpack-plugin");
const HtmlWebpackInjectAttributesPlugin = require("html-webpack-inject-attributes-plugin");
const TerserWebpackPlugin = require("terser-webpack-plugin");
//多线程build配置
const happypackCommonConfig = {
debug: false,
threadPool: HappyPack.ThreadPool({
size: os.cpus().length,
}),
};
//基类配置
const baseConfig = require("./webpack.base.js");
//生产环境webpack配置
const webpackConfig = merge.smart(baseConfig, {
//指定生产环境
mode: "production",
//生产环境output配置
output: {
filename: "js/[name]_[chunkhash:8].bundle.js",
path: path.join(process.cwd(), "./app/public/dist/prod"),
publicPath: "/dist/prod",
crossOriginLoading: "anonymous",
},
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"],
},
],
},
//webpack不会有大量hints信息,默认为warning
performance: {
hints: false,
},
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]_[chunkhash:8].bundle.css",
}),
//优化并压缩css资源
new CSSMinizisePlugin(),
//多线程打包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",
}),
],
optimization: {
//使用TerserWebpackPlugin的并发和缓存,提升压缩阶段的性能
//清除console.log
minimize: true,
minimizer: [
new TerserWebpackPlugin({
cache: true, //启用缓存来加速构建过程
parallel: true, //利用多线CPU的优势来加快压缩速度
terserOptions: {
//配置terser的参数
compress: {
drop_console: true, //去掉console.log内容
},
},
}),
],
},
});
module.exports = webpackConfig;
以上代码用到了打包优化配置optimization,在elpise项目中主要进行了三种打包优化 1.vendor:第三方lib库,基本不会改动,除非依赖版本升级 2.common:业务组件代码的公共部分抽取出来,基本不会改动 3.entry.(page1,page2,xxx):不用entry里的业务组件代码的差异部分,会经常改动 目的:把改动和引用频率不一样的js区分出来,以达到更好利用浏览器缓存的效果
课程中讲到了开发阶段需要启用热更新,因为用户在修改文件时,修改好以后需要重新打包浏览器页面才会改变,这样开发的时候显得很麻烦,为了解决这个问题,则需要重新开启一个服务器,起到在中间进行消息传递的作用
//以下是dev.js文件配置
//本地开的启动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 { webpackConfig, DEV_SERVER_CONFIG } = require("./config/webpack.dev.js");
const app = express();
const compiler = webpack(webpackConfig);
//指定静态文件目录
app.use(express.static(path.join(__dirname, "../public/dist")));
//引用divMiddleware中间件(监控文件改动)
app.use(
devMiddleware(compiler, {
//落地文件
writeToDisk: (filePath) => {
return 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}`);
});
//以下是webpack.dev.js
const path = require("path");
const merge = require("webpack-merge");
const webpack = require("webpack");
//基类配置
const baseConfig = require("./webpack.base.js");
//devSerer配置
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) => {
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`,
];
}
});
//开发环境webpack配置
const webpackConfig = merge.smart(baseConfig, {
//指定开发环境
mode: "development",
//source-map开发工具,呈现代码的映射关系,便于在开发过程中调试代码
devtool: "eval-cheap-module-source-map",
//开发环境的output配置
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",
},
plugins: [
//HotModuleReplacementPlugin用于实现热模块替换(Hot Module Replacement 简称hmr)
//模块热替换允许在应用程序运行时替换模块
//极大地提升开发效率,因为能让应用程序一直保持运行状态
new webpack.HotModuleReplacementPlugin({
multiStep: true,
}),
],
});
module.exports = {
// webpack配置
webpackConfig,
//开发环境devServer配置,暴露给dev.js使用
DEV_SERVER_CONFIG,
};