webpack 的核心价值就是前端源码的打包,即将前端源码中每一个文件(无论任何类型)都当做一个 pack ,然后分析依赖,将其最终打包出线上运行的代码。webpack 的四个核心部分
entry 规定入口文件,一个或者多个
output 规定输出文件的位置
loader 各个类型的转换工具
plugin 打包过程中各种自定义功能的插件
webpack 如今已经进入 v4.x 版本,v5.x 估计也会很快发布。不过看 v5 的变化相比于 v4 ,常用的配置没有变,这是一个好消息,说明基本稳定。
前端工程师需要了解的 webpack
前端工程化是近几年前端发展迅速的主要推手之一,webpack 无疑是前端工程化的核心工具。目前前端工程化工具还没有到一键生成,或者重度继承到某个 IDE 中(虽然有些 cli 工具可以直接创建),还是需要开发人员手动做一些配置。
因此,作为前端开发人员,熟练应用 webpack 的常用配置、常用优化方案是必备的技能 ―― 这也正是本文的内容。另外,webpack 的实现原理算是一个加分项,不要求所有开发人员掌握,本文也没有涉及。
基础配置
初始化环境
npm init -y 初始化 npm 环境,然后安装 webpack npm i webpack webpack-cli -D
新建 src 目录并在其中新建 index.js ,随便写点 console.log('index js') 。然后根目录创建 webpack.config.js ,内容如下
const path = require('path')
module.exports = {
// mode 可选 development 或 production ,默认为后者
// production 会默认压缩代码并进行其他优化(如 tree shaking)
mode: 'development',
entry: path.join(__dirname, 'src', 'index'),
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'dist')
}
}
然后增加 package.json 的 scripts
"scripts": {
"build": "webpack"
},
然后运行 npm run build 即可打包文件到 dist 目录。
区分 dev 和 build
使用 webpack 需要两个最基本的功能:第一,开发的代码运行一下看看是否有效;第二,开发完毕了将代码打包出来。这两个操作的需求、配置都是完全不一样的。例如,运行代码时不需要压缩以便 debug ,而打包代码时就需要压缩以减少文件体积。因此,这里我们还是先把两者分开,方便接下来各个步骤的讲解。
首先,安装 npm i webpack-merge -D ,然后根目录新建 build 目录,其中新建如下三个文件。
// webpack.common.js 公共的配置
const path = require('path')
const srcPath = path.join(__dirname, '..', 'src')
const distPath = path.join(__dirname, '..', 'dist')
module.exports = {
entry: path.join(srcPath, 'index')
}
// webpack.dev.js 运行代码的配置(该文件暂时用不到,先创建了,下文会用到)
const path = require('path')
const webpackCommonConf = require('./webpack.common.js')
const { smart } = require('webpack-merge')
const srcPath = path.join(__dirname, '..', 'src')
const distPath = path.join(__dirname, '..', 'dist')
module.exports = smart(webpackCommonConf, {
mode: 'development'
})
// webpack.prod.js 打包代码的配置
const path = require('path')
const webpackCommonConf = require('./webpack.common.js')
const { smart } = require('webpack-merge')
const srcPath = path.join(__dirname, '..', 'src')
const distPath = path.join(__dirname, '..', 'dist')
module.exports = smart(webpackCommonConf, {
mode: 'production',
output: {
filename: 'bundle.[contentHash:8].js', // 打包代码时,加上 hash 戳
path: distPath,
// publicPath: 'http://cdn.abc.com' // 修改所有静态文件 url 的前缀(如 cdn 域名),这里暂时用不到
}
})
修改 package.json 中的 scripts
"scripts": {
"build": "webpack --config build/webpack.prod.js"
},
重新运行 npm run build 即可看到打包出来的代码。最后,别忘了将根目录下的 webpack.config.js 删除。
这将引发一个新的问题:js 代码中将如何判断是什么环境呢?需要借助 webpack.DefinedPlugin 插件来定义全局变量。可以在 webpack.dev.js 和 webpack.prod.js 中做如下配置:
// 引入 webpack
const webpack = require('webpack')
// 增加 webpack 配置
plugins: [
new webpack.DefinePlugin({
// 注意:此处 webpack.dev.js 中写 'development' ,webpack.prod.js 中写 'production'
ENV: JSON.stringify('development')
})
最后,修改 src/index.js 只需加入一行 console.log(ENV) ,然后重启 npm run dev 即可看到效果。
JS 模块化
webpack 默认支持 js 各种模块化,如常见的 commonJS 和 ES6 Module 。但是推荐使用 ES6 Module ,因为 production 模式下,ES6 Module 会默认触发 tree shaking ,而 commonJS 则没有这个福利。究其原因,ES6 Module 是静态引用,在编译时即可确定依赖关系,而 commonJS 是动态引用。
不过使用 ES6 Module 时,ES6 的解构赋值语法这里有一个坑,例如 index.js 中有一行 import {fn, name} from './a.js' ,此时 a.js 中有以下几种写法,大家要注意!
// 正确写法一
export function fn() {
console.log('fn')
}
export const name = 'b'
// 正确写法二
function fn() {
console.log('fn')
}
const name = 'b'
export {
fn,
name
}
// 错误写法
function fn() {
console.log('fn')
}
export default {
fn,
name: 'b'
}
该现象的具体原因可参考https://www.jb51.net/article/162079.htm 。下文马上要讲解启动本地服务,读者可以马上写一个 demo 自己验证一下这个现象。
启动本地服务
上文创建的 webpack.dev.js 一直没使用,下面就要用起来。
使用 html
启动本地服务,肯定需要一个 html 页面作为载体,新建一个 src/index.html 并初始化内容
this is index html
要使用这个 html 文件,还需要安装 npm i html-webpack-plugin -D ,然后配置 build/webpack.common.js ,因为无论 dev 还是 prod 都需要打包 html 文件。
plugins: [
new HtmlWebpackPlugin({
template: path.join(srcPath, 'index.html'),
filename: 'index.html'
})
]
重新运行 npm run build 会发现打包出来了 dist/index.html ,且内部已经自动插入了打包的 js 文件。
webpack-dev-server
有了 html 和 js 文件,就可以启动服务了。首先安装 npm i webpack-dev-server -D ,然后打开 build/webpack.dev.js配置。只有运行代码才需要本地 server ,打包代码时不需要。
devServer: {
port: 3000,
progress: true, // 显示打包的进度条
contentBase: distPath, // 根目录
open: true, // 自动打开浏览器
compress: true // 启动 gzip 压缩
}
打开 package.json 修改 scripts ,增加 "dev": "webpack-dev-server --config build/webpack.dev.js", 。然后运行 npm run dev ,打开浏览器访问 localhost:3000 即可看到效果。
解决跨域
实际开发中,server 端提供的端口地址和前端可能不同,导致 ajax 收到跨域限制。使用 webpack-dev-server 可配置代理,解决跨域问题。如有需要,在 build/webpack.dev.js 中增加如下配置。
devServer: {
// 设置代理
proxy: {
// 将本地 /api/xxx 代理到 localhost:3000/api/xxx
'/api': 'http://localhost:3000',
// 将本地 /api2/xxx 代理到 localhost:3000/xxx
'/api2': {
target: 'http://localhost:3000',
pathRewrite: {
'/api2': ''
}
}
}
处理 ES6
使用 babel
由于现在浏览器还不能保证完全支持 ES6 ,将 ES6 编译为 ES5 ,需要借助 babel 这个神器。安装 babel npm i babel-loader @babel/core @babel/preset-env -D ,然后修改 build/webpack.common.js 配置
module: {
rules: [
{
test: /\.js$/,
loader: ['babel-loader'],
include: srcPath,
exclude: /node_modules/
},
]
},
还要根目录下新建一个 .babelrc json 文件,内容下
{
"presets": ["@babel/preset-env"],
"plugins": []
}
在 src/index.js 中加入一行 ES6 代码,如箭头函数 const fn = () => { console.log('this is fn') } 。然后重新运行 npm run dev,可以看到浏览器中加载的 js 中,这个函数已经被编译为 function 形式。
使用高级特性
babel 可以解析 ES6 大部分语法特性,但是无法解析 class 、静态属性、块级作用域,还有很多大于 ES6 版本的语法特性,如装饰器。因此,想要把日常开发中的 ES6 代码全部转换为 ES5 ,还需要借助很多 babel 插件。
安装 npm i @babel/plugin-proposal-class-properties @babel/plugin-transform-block-scoping @babel/plugin-transform-classes -D ,然后配置 .babelrc
{
"presets": ["@babel/preset-env"],
"plugins": [
"@babel/plugin-proposal-class-properties",
"@babel/plugin-transform-block-scoping",
"@babel/plugin-transform-classes"
]
}
在 src/index.js 中新增一段 class 代码,然后重新运行 npm run build ,打包出来的代码会将 class 转换为 function 形式。
source map
source map 用于反解析压缩代码中错误的行列信息,dev 时代码没有压缩,用不到 source map ,因此要配置 build/webpack.prod.js
// webpack 中 source map 的可选项,是情况选择一种:
// devtool: 'source-map' // 1. 生成独立的 source map 文件
// devtool: 'eval-source-map' // 2. 同 1 ,但不会产生独立的文件,集成到打包出来的 js 文件中
// devtool: 'cheap-module-source-map' // 3. 生成单独的 source map 文件,但没有列信息(因此文件体积较小)
devtool: 'cheap-module-eval-source-map' // 4. 同 3 ,但不会产生独立的文件,集成到打包出来的 js 文件中
生产环境下推荐使用 1 或者 3 ,即生成独立的 map 文件。修改之后,重新运行 npm run build ,会看到打包出来了 map 文件。
处理样式
在 webpack 看来,不仅仅是 js ,其他的文件也是一个一个的模块,通过相应的 loader 进行解析并最终产出。
处理 css
安装必要插件 npm i style-loader css-loader -D ,然后配置 build/webpack.common.js
module: {
rules: [
{ /* js loader */ },
{
test: /\.css$/,
loader: ['style-loader', 'css-loader'] // loader 的执行顺序是:从后往前
}
]
},
新建一个 css 文件,然后引入到 src/index.js 中 import './css/index.css' ,重新运行 npm run dev 即可看到效果。
处理 less
less sass 都是常用 css 预处理语言,以 less 为例讲解。安装必要插件 npm i less less-loader -D ,然后配置 build/webpack.common.js
{
test: /\.less$/,
loader: ['style-loader', 'css-loader', 'less-loader'] // 增加 'less-loader' ,注意顺序
}
新建一个 less 文件,然后引入到 src/index.js 中 import './css/index.less' ,重新运行 npm run dev 即可看到效果。
自动添加前缀
一些 css3 的语法,例如 transform: rotate(45deg); 为了浏览器兼容性需要加一些前缀,如 webkit- ,可以通过 webpack 来自动添加。安装 npm i postcss-loader autoprefixer -D ,然后配置
{
test: /\.css$/,
loader: ['style-loader', 'css-loader', 'postcss-loader'] // 增加 'postcss-loader' , 注意顺序
}
还要新建一个 postcss.config.js 文件,内容是
module.exports = {
plugins: [require('autoprefixer')]
}
重新运行 npm run dev 即可看到效果,自动增加了必要的前缀。
抽离 css 文件
默认情况下,webpack 会将 css 代码全部写入到 html 的