webpack 完成工程化建设
引言 -- 我们为什么需要 webpack 等打包工具来进行工程化
我们在实现一个网页项目的时候,通常都会有很多的 js,css,其他的一些资源文件。如果我们直接丢给浏览器的话,他将会面临两个大问题: 1.要请求加载这些文件,速度很慢,会影响加载的效率。 2.浏览器对于某些文件无法识别,例如第三方node_module, ts,less 代码等文件。
这涉及到开发过程和运行过程,这些问题都是我们的痛点所在。开发过程我们希望改动文件后编辑加载的速度能更快,让我们更好的调试代码。运行上线的时候我们则是希望能够加载的更加顺畅。这就是我们的打包工具所要解决的问题,我们通过使用打包工具,将这些文件进行加工,将他们整合成尽可能少的文件数量,以及浏览器能够识别的文件来进行编译。
核心概念 => webpack
webpack
webpack 是一个用于前端开发的静态模块打包工具,能够将项目中的各种资源(如javaScript,css,图片等)视为模块,通过分析依赖关系打包成优化后的静态文件来提升开发效率和项目可维护性。
关键组成部分
入口:Entry:指定打包的入口文件 出口:Output:定义打包后的文件的输出路径和名称。 加载器:Loader:负责将各类非 JavaSript 资源转化成模块。(帮助浏览器将有可能无法识别的文件转化成能够识别的文件) 插件:Plugin:用于执行复杂的自定义任务,扩展 webpack 功能。 文件别名配置:resolve
module.exports = {
// 入口配置
entry: './src/main.js',
// 产物输出路径
output: {},
// 配置模块解析的具体行为,(定义 webpack 在打包的时候,是怎么找到并解析具体模块)
resolve: {
extensions: [".js", ".vue", ".less", "css"] // 自动解析这些文件
alias: { $xxxx: xxxx } // 可以将对应的路径文件指向一个具体的替代
},
// 配置插件
plugins: [
// 处理vue文件,将我们设定的规则都能够在vue文件中运行
new VueLoaderPlugin(),
// 把第三方库暴露到全局 window 上下文, 可以在文件中直接使用,例如 vue, axios等库,无需再单独导入
new webpack.ProvidePlugin({
Vue: "vue",
axios: "axios",
_: "lodash",
}),
// 定义全局常量 关于vue的一些设置
new webpack.DefinePlugin({
_VUE_OPTIONS_API_: true, // 支持vue解析optionsApi
_VUE_PROD_DEVTOOLS_: false, // 禁用vue调试工具
_VUE_PROD_HYDRATIO_MISMATCH_DETAILS_: false, // 禁用生产环境显示 '水合' 信息
}),
// 渲染我们指定的模板代码
new HtmlWebpackPlugin({
// 产物(最终模板)输出路径
filename: path.resolve(process.cwd(), "../app/public/dist/entry.tpl"),
// 指定要使用的模板文件
template: path.resolve(__dirname, "../../view/entry.tpl"),
// 要注入的代码块
chunks: [`${enrtyName}`],
})
],
// 打包输出优化(配置代码分割,缓存,Treeshaing,压缩优化行为)
optimization: {
// 将 js 文件打包成三种类型
// 1. vendor: 第三方库代码
// 2. common:业务组件代码的公共部分
// 3. 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: {
//公共模块
test: /[\\/]common|widgets[\\/]/,
name: "common", //模块名称
minChunks: 2, //最少被两个模块引用才打包
minSize: 1, //最小分割文件大小
priority: 10, // 优先级
reuseExistingChunk: true, //可复用已存在的模块
},
},
}
}
}
基础的配置如上述代码所示,这样子我们就完成了一个基础的 webpack 配置。
工程化设计
多环境差异化配置
我们可以划分成为开发环境和生产环境,每个环境对应的配置有共同性,也有差异性,开发环境更注重调试和开发效率,而生产环境则更重视代码压缩和资源优化,所以我们将其配置文件划分成为三块
- webpack.base.js: 做多入口文件配置,模板生成,还有一些共同性的配置,然后暴露出去为baseConfig,用于其他环境配置对他进行扩展。
- webpack.dev.js: 热更新HMR,devtool开发工具,热更新插件配置
const merge = require('webpack-merge');
const path = require('path')
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路径
`${require.resolve('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 webpackDevConfig = merge.smart(baseConfig, {
//指定开发环境
mode: 'development',
//source-map 开发工具,呈现代码映射关系,以便在开发过程中调试代码
devtool: 'eval-cheap-module-source-map',
//开发阶段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'
},
//开发阶段插件
plugins: [
//用于实现热模块替换()
//允许在运用程序运行时替换模块
//极大的提升开发效率,因为能让应用程序一直保持运行状态
new webpack.HotModuleReplacementPlugin({
multiStep: false,
})
]
})
module.exports = {
// webpack 配置(导出为 `webpackDevConfig` 以匹配使用处)
webpackDevConfig,
// devServer 配置 暴露给 dev.js 使用
DEV_SERVER_CONFIG
};
- webpack.prod.js: 重视代码资源,会做一些代码压缩操作,tree-shaking 剔除无用代码,以及happypack多线程打包
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 CSSMinimizerPlugin = require('css-minimizer-webpack-plugin');
const HtmlWebpackInjectAttributesPlugin = require('html-webpack-inject-attributes-plugin');
const TerserPlugin = require('terser-webpack-plugin');
// 基类配置
const baseConfig = require('./webpack.base.js');
//多线程build配置
const happypackCommonConfig = {
debug: false,
threadPool: HappyPack.ThreadPool({ size: os.cpus().length }),
}
//生产环境 webpack 配置
const webpackProdConfig = merge.smart(baseConfig, {
mode: 'production', //指定生产环境
output: baseConfig.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, `${require.resolve('happypack/loader')}?id=css`]
}, {
test: /\.js$/,
include: [
path.resolve(__dirname, '../../pages'),
// 处理业务目录下的
path.resolve(process.cwd(), "./app/pages"),
],
use: [`${require.resolve('happypack/loader')}?id=js`]
}]
},
// webpack 不会有大量 hints 信息, 默认为 warning
performance: {
hints: false
},
plugins: [
//每次 build 前清空public/dist目录
new CleanWEbpackPlugin(['public/dist/prod'], {
root: path.resolve(process.cwd(), './app/'),
exclued: [],
verbose: true,
dry: false
}),
//提取css的公共部分,有效利用缓存,非公共的css使用inline
new MiniCssExtractPlugin({
chunkFilename: 'css/[name]_[contenthash:8].bundle.css',
}),
//优化并压缩css资源
new CSSMinimizerPlugin(),
//多线程打包js,加快打包速度
new HappyPack({
...happypackCommonConfig,
id: 'js',
loaders: [`${require.resolve('babel-loader')}?${JSON.stringify({
presets: [require.resolve('@babel/preset-env')],
plugins: [
require.resolve('@babel/plugin-transform-runtime'),
]
})}`]
}),
//多线程打包css,加快打包速度
new HappyPack({
...happypackCommonConfig,
id: 'css',
loaders: [{
path: require.resolve('css-loader'),
options: {
importLoaders: 1
}
}]
}),
//浏览器在请求资源的时候,不发送用户的身份凭证
new HtmlWebpackInjectAttributesPlugin({
corssorigin: 'anonymous'
})
],
optimization: {
//使用 TerserPlugin 的并发和缓存,提升压缩阶段的性能
//清除console.log
minimize: true,
minimizer: [
new TerserPlugin({
cache: true, //启用缓存来加速构建过程
parallel: true, //利用多核cpu的优势加快压缩速度
terserOptions: {
compress: {
drop_console: true //去掉console.log
}
}
})
]
}
})
module.exports = webpackProdConfig;
多入口配置,只要按照约定好的文件格式创建文件,即可自动引入为入口文件,并且生成对应的HTML模板,无需手动一个个配置入口文件
// 动态构建 pageEntryies htmlWebpackPluginList
const elpisPageEntries = {};
const elpisHtmlWebpackPPluginList = [];
// 获取 app/pages 下的所有入口文件
const elpisEntryList = path.resolve(__dirname, "../../pages/**/entry.*.js");
glob.sync(elpisEntryList).forEach((file) => {
handleEntry(file, elpisPageEntries, elpisHtmlWebpackPPluginList);
});
// 动态构建 业务页面入口
const businessEntries = {};
const businessHtmlWebpackPPluginList = [];
const businessEntryList = path.resolve(
process.cwd(),
"./app/pages/**/entry.*.js"
);
glob.sync(businessEntryList).forEach((file) => {
handleEntry(file, businessEntries, businessHtmlWebpackPPluginList);
});
function handleEntry(file, entries = {}, htmlWebpackPPluginList = []) {
const enrtyName = path.basename(file, ".js");
//构造entry
entries[enrtyName] = file;
//构造最终渲染的页面文件
htmlWebpackPPluginList.push(
new HtmlWebpackPlugin({
filename: path.resolve(
process.cwd(),
"./app/public/dist/",
`${enrtyName}.tpl`
), //产物(最终模板)输出路径
template: path.resolve(__dirname, "../../view/entry.tpl"), //指定要使用的模板文件
chunks: [`${enrtyName}`], //要注入的代码块
})
);
}
// 加载 业务 webpack 配置
let businessWebpackConfig = {};
try {
businessWebpackConfig = require(`${process.cwd()}/app/webpack.config.js`);
} catch (e) {}
如何实现热更新?
热更新的流程可以概括为:监控文件改动 -> 识别模块变化 -> 通知浏览器 -> 浏览器局部更新。 热更新可以划分成为两个重点,如何监听文件修改?,如何告知浏览器? webpack其实提供了两个核心的中间件来帮助我们实现热更新,devMiddleware(文件监控和编译),hotMiddleware(与浏览器通信)
app.use(
devMiddleware(compiler, {
//落地文件
writeToDisk: (filePath) => {
return filePath.endsWith(".tpl");
},
//资源路径
publicPath: webpackDevConfig.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: {
color: true,
},
})
);
//引用hotMiddleware中间件,实现热更新
app.use(
hotMiddleware(compiler, {
path: `/${DEV_SERVER_CONFIG.HMR_PATH}`,
log: () => {},
})
);