webpack
前言
webapck干什么?
依赖打包(主要),即根据资源依赖的分析后进行处理,输出结果,文件处理(次之)
为什么要对资源打包?
功能逐渐复杂推动引入大量其它类库,团队成员按照功能划分开发项目这两大现象;项目愈加需要合理组织代码,否则无法持续开发或维护;推动模块化开发,这时各种模块化规范用于规定模块独立的标准;
但多模块导致项目文件增加,导致http请求数增加;且随着模块数增加,依旧存在依赖关系逐渐复杂影响开发的问题;推动打包工具,代码在开发环境是按功能划分模块,开发体验良好;经过webpack分析模块依赖,打包输出为优化后的生产环境资源,优秀的项目;
My V-Cli
webpack4配置实践,使用webpack配置搭建一个多页面,符合实际项目开发并同时支持Vue模块,React模块的项目环境
准备
- 初始化npm
- 安装webpack与webpack-cli
- 建议webpack安装到局部,防止不同项目依赖不同版本的 Webpack 而导致冲突
- 使用webpack4,需要安装webpack-cli来执行命令
- 创建项目结构
- build/ webpack配置
- config/ 项目环境配置
- dist/ 打包输出资源
- src/ 模块资源
- pages/ 项目页面
- assets/ 项目依赖的静态资源
- static/ 已被编译过的第3方静态资源
- 配置npm脚本命令
"dev": "webpack --config ./build/webpack.dev.config.js",
基础配置
在/src/pages/index下添加用于测试的模块,然后在webpack.base.conf.js和webpack.dev.conf.js添加基本配置,成功打包输出index页面
webpack.base.config.js
- 配置入口文件与输出
entry: './src/pages/index/index.js',
output: {
path: path.resolve(__dirname,'../dist'), //要求绝对路径
filename: 'index.js'
}
webpack配置文件中,所有的相对路径配置值基于项目根目录
- 配置css模块加载
rules: [
{
test: /\.css$/,
use: ['style-loader','css-loader']
}
]
- 配置html-webpack-plugin插件
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
...
plugins: [
new HtmlWebpackPlugin({
filename: 'index.html',
template: './src/pages/index/index.html',
templateParameters:{
base: "htttp://192.168.4.43" // ejs模板语法的变量解析
},
}),
new CleanWebpackPlugin()
]
}
将指定html文件作为模板,打包时输出引入了入口文件的html页面;
-
配置clean-webpack-plugin
- 每次打包输出前清空上次打包输出目录,避免重复输出包
-
配置resolve
resolve: {
alias: {
'@assets': path.resolve(__dirname,'./src/assets'),
}
}
resolve配置项用于定义webpack如何解析部分模块;
src项目模块之间引用路径过深,定义alias别名用于webpack解析路径;
配合url-loader支持webpack解析.html/.css文件中资源url
关于配置分离
实际开发中将webpack配置分为生产环境,开发环境;配置分离两种实现:
- webpack-merge工具实现
- 判断打包环境
--- package.json srcipts ---
"scripts": {
"dev": "webpack --config ./build/webpack.dev.config.js",
"prod": "webpack --config ./build/webpack.prod.config.js --env NODE_ENV=production",
"serve": "webpack serve --open --config ./build/webpack.dev.conf.js",
}
--- webpack.dev.config.js ---
module.exports = function(env){
const isProd = Object.is(env.NODE_ENV, 'production');
return {
...
isProd ? MiniCssExtractPlugin.loader : 'style-loader'
}
}
webpack.dev.config.js
- 引入并使用webpack-merge模块
const {merge} = require('webpack-merge');
const baseconfig = require('./webpack.base.conf.js');
module.exports = merge(baseconfig,{
mode: 'development'
});
- devtool(帮助控制台调试)
const isProd = Object.is(process.env.NODE_ENV, 'production');
module.exports = {
...
devtool: isProd ? 'hidden-source-map' : 'cheap-module-eval-source-map'
}
-
开启devServer
-
安装webpack-dev-server
-
配置devServer
--- webpack.base.conf.js --- devServer: { contentBase: './dist', inline: true, // 开启热加载(刷新页面) hot: true, // 开启热替换HMR,(替换页面部分) // ...其它配置,如本地代理 proxy: { '/': { target: 'http://localhost:3000', changeOrigin: true, }, }, }
-
添加npm命令
"serve": "webpack serve --open --config ./build/webpack.dev.conf.js",
-
在开发环境下使用devServer,可以提供 HTTP 服务而不是使用本地文件预览项目;
-
监听文件的变化并自动刷新网页,做到实时预览;
-
支持 Source Map,以方便调试;(mode值自动为development)
-
hot热模块替换时,只能使用chunkhash,不能使用hash;不支持分离的模块;
-
其它loader
图片/字体模块加载
{
test: /\.html/,
use: 'html-loader'
},
{
test: /\.(png|gif|jpe?g|eot|ttf|svg|woff2?)$/,
use: {
loader: 'url-loader',
options: {
limit: 1024 * 3,
falllback: 'file-loader'
}
}
}
- 图片模块
- 加载css模块中会自动加载背景属性url引入的图片/字体文件
- 设置html模块中img标签src属性来引入图片,需配置html-loader(html代码的alias别名路径前需加上’~’才能正确解析)
- js模块中部分依赖图片模块的操作,需先引入图片模块
sass模块加载
- 安装sass(编译sass),sass-loader
{
test: /\.s[ac]ss$/,
use: ['style-loader','css-loader','sass-loader'],
}
PostCSS
作为强大的CSS 处理工具,它通过插件机制可以灵活的扩展其支持的特性,如自动加前缀,支持下一代css语法;
- 安装并配置postcss-loader
{
test: /\.css$/,
use: ['style-loader','css-loader','postcss-loader']
},
{
test: /\.s[ac]ss$/,
use: ['style-loader','css-loader','postcss-loader','sass-loader'],
}
- 全局下新建postcss.config.js
module.exports = {
plugins: [
// require('postcss-cssnext'),
require('autoprefixer')({ // 使用postcss插件前需安装插件
overrideBrowserslist:[
"defaults",
"Android 4.1",
"iOS 7.1",
"Chrome>31",
"ff>31",
"ie>=8",
"last 2 versions",
">0%"
// 'last 2 version','>1%' 根据业务选择浏览器兼容
]
})
]
}
babel转换
- 安装babel三件套:babel-loader,@babel/core,@babel/preset-env
- 配置babel-loader对js转换
{
test: /\.js$/,
exclude: /node_modules/, // 必须限制转换目录
use: 'babel-loader'
},
- 预设与ployfill配置
--- babel.config.js ---
modules.export = {
presets: [
["@babel/preset-env",
{
modules: false, // 转为es6
targets: { // 根据业务设置兼容列表
"chrome": "58",
"ie": "11"
},
useBuiltIns: "usage" // 按需引入polyfill,需安装@babel/polyfill
"corejs": 3, // 安装corejs@3,并声明
}
]
]
}
React与Vue支持
在’src/pages/’下新建分别使用了react与vue框架的两个页面目录,使用webpack配置使项目环境支持react和vue框架开发
React
- 安装React框架基础:react react-dom
- 安装并配置jsx语法转换预设:@babel/preset-react
--- babel.config.js ---
module.exports = {
presets: [
["@babel/preset-env",
{
targets: {
"chrome": "58",
"ie": "11"
},
useBuiltIns: "usage"
}
],
"@babel/preset-react"
]
}
- 编写’src/pages/reactPage’的资源模块,如html模板,入口文件
- webpack配置为多页面应用,即多个入口chunk分别对应输出的html页面
entry: {
index: './src/pages/index/index.js', // 首页入口文件
reactPage: './src/pages/reactPage/index.js' // react页面入口文件
...
plugins: [
new HtmlWebpackPlugin({
filename: 'index.html',
template: './src/pages/index/index.html',
chunks: ['index'] // 输出HTML页面及其chunks模块
}),
new HtmlWebpackPlugin({
filename: 'reactPage.html',
template: './src/pages/reactPage/index.html',
chunks: ['reactPage']
}),
]
},
output: {
path: path.resolve(__dirname,'../dist'),
filename: '[name]-[chunkhash:8].js'
},
Vue
- 安装vue框架基础:vue
- 安装并配置.vue模块loader与模板编译:vue-loader,vue-template-compiler
const VueLoaderPlugin = require('vue-loader/lib/plugin'); // 插件支持
module.exports = {
module: {
rules: [
{ // vue-loader必须在rules规则第1位
test: /\.vue$/,
loader: 'vue-loader'
}...
]
}
}
- 编写vue页面及入口文件main.js模块
- webpack配置新页面VuePage的入口文件与html页面
多页面管理
入口配置与html模板配置
实际项目的多页面应用,页面数量是未知的,将所有页面都枚举在配置里显然是不合理的。根据项目的pages目录结构,定义getEntry()方法来遍历指定文件夹获取入口文件,getTemplate获取对应的html模板并配置到plugins;
const glob = require('glob'); // glob包用于匹配文件路径
function getEntry(){
let entry = {};
glob.sync('./src/pages/*/index.js')
.forEach(filepath => {
let chunkName = filepath.match(/pages\/(.+)\/index\.js/)[1];
entry[chunkname] = filepath;
});
return entry;
}
let templates = [];
glob.sync('./src/pages/*/*.html')
.forEach(filepath => {
filepath.match(/pages\/(.+)\/index\.html/);
let name = RegExp.$1;
templates.push(new HtmlWebpackPlugin({
filename: name,
template: filepath,
chunks: [name]
}))
});
module.exports = {
entry: getEntry(),
...
plugins: [...templates]
}
控制dist输出目录结构
-
期待目录结构
- dist
- css
- img
- js
- pages
- index.html
- favicon.ico
- dist
-
配置
-
配置output.filename将chunk打包至’dist/js’
output: { path: path.resolve(__dirname,'../dist/'), filename: './js/[name]-[chunkhash:8].js' },
-
配置url-loader的name参数将图片/字体模块打包至’dist/css’
{ test: /\.(png|gif|jpe?g|eot|ttf|svg|woff2?)$/, use: { loader: 'url-loader', options: { limit: 1024 * 3, falllback: 'file-loader', name: './css/[name]-[contenthash:8].[ext]' } } }
-
配置html-webpack-plugin实例的filename,将非主页的html页面打包至’dist/pages/’
new HtmlWebpackPlugin({ filename: name == 'index.html' ? name : './pages/' + name, template: filepath, chunks: [name] })
-
webpack.prod.conf.js
分离css代码
开发环境使用style-loader对css文件进行处理后,css文件被作为模块也打包在了js文件中。实际生产环境,会对js文件和css文件分离;
- 安装并配置mini-css-extract-plugin
--- webpack.base.config.js ---
output: {
publicPath: './', // 该插件要求配置公共路径选项
...
},
--- webpack.prod.config.js ---
module.exports = merge(baseconfig,{
mode: 'production',
devtool: false,
module: {
rules: [
{
test: /\.css$/,
exclude: /node_modules/,
use: [MiniCssExtractPlugin.loader,
'css-loader','postcss-loader']
},
{
test: /\.s[ac]ss$/,
exclude: /node_modules/,
use: [MiniCssExtractPlugin.loader,
'css-loader','postcss-loader','sass-loader',
],
}
]
},
plugins: [
new MiniCssExtractPlugin({
filename: './css/[name].[contenthash:8].css'
})
]
})
优化
缩小文件搜索范围
webpack从配置的entry触发,递归解析出导入的语句
- 优化loader配置
{
test: /\.js$/,
include: /src/,
use: 'babel-loader'
},
loader配置中使用test正则,include/exclude命中文件
- 优化resolve.alias配置(以react为例)
--- webpack.dev.config.js ---
module.exports = {
resolve: {
alias: {
// 开发环境使用包含检查警告的代码
'react': path.resolve(__dirname, '../node_modules/react/dist/react.js'),
// 'react': path.resolve(__dirname, '../node_modules/react/umd/react.development.js') // react16
}
},
};
--- webpack.prod.config.js ---
module.exports = {
resolve: {
alias: {
'react': path.resolve(__dirname, '../node_modules/react/dist/react.min.js'), // react15
// 'react': path.resolve(__dirname, '../node_modules/react/umd/react.production.min.js'), // react16
}
},
};
默认从入口文件 .’/node_modules/react/react.js’ 开始递归的解析和处理依赖的几十个文件。 通过配置 resolve.alias 可以让 Webpack 在处理 React 库时,直接使用单独完整的 react.min.js 文件,从而跳过耗时的递归解析操作。
- 优化 module.noParse 配置
const path = require('path');
module.exports = {
module: {
// 忽略对 `react.xxx.min.js` 文件的递归解析处理
noParse: [/react\.\w+\.min\.js$/],
},
};
对某些独立完整文件(如上面的react.production.min.js)和为使用模块化规范的库不进行模块化编译(如jQuery和ChartJs)
Dllplugin动态链接库
HappyPack开启子线程
ParallelUglifyPlugin多线程压缩
--- webpack.prod.config.js ---
const ParallelUglifyPlugin = require('webpack-parallel-uglify-plugin');
plugins: [
...
new ParallelUglifyPlugin({
// 传递给 UglifyJS 的参数
uglifyJS: {
output: {
// 最紧凑的输出
beautify: false,
// 删除所有的注释
comments: false,
},
compress: {
// 删除所有的 `console` 语句,可以兼容ie浏览器
drop_console: true,
// 内嵌定义了但是只用到一次的变量
collapse_vars: true,
// 提取出出现多次但是没有定义成变量去引用的静态值
reduce_vars: true,
}
},
}),
]
CSS压缩
---- webpack.base.config.js ---
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
optimization: {
minimize: true,
minimizer: [
new CssMinimizerPlugin(),
],
}
区分环境
在项目中需要根据当前环境不同设置不同处理,例如请求地址;借助 DefinePlugin插件定义编译时全局变量,在代码中判断变量值进行不同处理
--- webpack.base.config.js ---
const webpack = require('webpack');
plugins: [
...
new webpack.DefinePlugin({
// 声明全局变量process.env.NODE_ENV
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
})
]
--- index.js ---
console.log(process.env.NODE_ENV);
Tree Shaking摇树
Tree Shaking自动去掉没有使用的代码,仅对ES6引入的模块检测
optimization: {
usedExports: true,
"sideEffects":false,
}
提取公共代码
-
将多个页面公共的代码抽离成单独的文件,项目公共代码分为两种:
- 基础库,如react,vue,一般不会更新(base.js)
- 业务公共业务代码,常更新(common.js)
-
提取公共代码
optimization: {
splitChunks:{
cacheGroups: {
common: {
name: 'common', // 输出名
chunks: 'initial', // 检索的chunk
priority: 2, // 规则权重
minChunks: 2, // 被引用2次以上提取
},
// 进一步提取base基础库
reactBase: {
name: 'reactBase',
test: (module) => { // chunk名称符合时再次提取
return /react|redux|prop-types/.test(module.context);
},
chunks: 'initial',
priority: 10,
}
}
}
}