自从node出现带来的前端大爆炸时代以来,前端工程化构建相关的工具层出不穷,如fis、gulp、grunt、webpack等,但如果让我说最全面的构建工具,那非webpack莫属了。
"webpack一统天下!"
最近看掘金小册中有一篇关于webpack的配置,从头撸了一遍感觉收获颇丰,所以实践、整理、记录一下整个过程相关学习。因为眼下webpack4.*
出现,所以此次整理以4.*
为主,在整理过程中会尽可能理清3.*
和4.*
的区别,以求更清晰的分析和理解。
※※※
: 新建文件
$$$
: 新依赖
安装和环境区分
一般来说我们更多的将webpack作为项目开发依赖来安装使用,并且固定版本,有利于多人协作开发。
项目init,保证项目有package.json
文件 。※※※
npm init
安装webpack
和webpack-cli
,并作为项目开发依赖(此处默认使用webpack4.8.1版本)$$$
npm install webpack webpack-cli -D
接下来,我们需要在package.json
中添加相关script。
- 此处的—mode参数,是webpack4.*不可缺少的参数,用于区分当前环境,关于环境区分后续会讲。
-
webpack-dev-server
模块需要安装,在本地开启一个简单的静态服务来进行开发。$$$
npm install webpack-dev-server -D
"scripts": {
"build": "webpack --mode production",
"start": "webpack-dev-server --mode development"
},
由于webpack 4.x 的版本可以零配置就开始进行构建,但是为了更定制化,我们还需要自己配置自己的config文件。接下来我们来新建和配置自己的webpack.config.js
文件。※※※
webpack的构建离不开一个入口文件,webpack会读取这个文件,从他开始解析依赖,打包等操作。所以我们需要新建一个入口文件entry.js
文件。※※※
webpack配置最终export一个对象或者一个函数,此处我们使用函数形式。
module.exports= {}
// 或者 webpack 4.0 暴露一个函数,可以获取到mode的环境变量
module.export = (env, argv) => {
console.log(env, argv.mode)
return {
entry: {
index: './entry.js'
}
}
}
此时我们可以试着启动项目,来查看打印出来的mode是否是对应的development和production。
npm start // => undefined development
npm run build // => undefined production
区分环境除了用webpack4.*
的—mode
参数,还可以用如下script,还可以将两者集合:
"scripts": {
"build_": "NODE_ENV=production webpack",
"start_": "NODE_ENV=development webpack-dev-server"
},
在webpack.config.js
中可以打印process.env.NODE_ENV
查看
// webpack.config.js
console.log('process.env.NODE_ENV =====>', process.env.NODE_ENV)
npm run start_ // => development
npm run build_ // => production
环境区分之后,我们就可以实现dev和prd不同的配置,以达到我们想要的效果。
常用配置
我们回到webpack的配置中来,webpack配置常用的主要字段为:
- entry 入口
- output 出口
- resolve 处理依赖模块路径的解析
- module 处理多种文件格式的loader
- plugins 除了文件格式转化由loader来处理,其他大多数由plugin来处理
- devServer 配置 webpack-dev-server
- optimization 优化,如
4.*
的chunk分离等 - ...
entry
如下代码中, index 和 vendor 是多入口名称,后续打包过程会有用!项目可以单入口也可以多入口。
vendor指向node_module中的vue组件,是为了后续chunk分离出单独的js文件,因为不常修改,可以单独打包而且可以使用chunkhash来维持相同的hash值,保证用户最大程度利用浏览器缓存机制。
{
entry: {
vendor: ["vue"],
index: './src/index.js'
}
}
output
如下代码中,[name]是根据entry中的多入口的名字。此处还可以获取[hash]、[chunkhash]等,用于相应的需求。代码中?rd=[hash] 是为了配合接下来的html-webpack-plugin
,可以将js插入html中并加上随机数,实现清缓存功能。
[chunkhash]可以保证当chunk的部分不变化情况下,不变化hash值,实现最大程度利用缓存机制。
注意:后续热更新(HMR)不能和[chunkhash]同时使用。 解决:如果是开发环境,将配置文件中的chunkhash 替换为hash
{
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js?rd=[hash:8]'
}
}
resolve
webpack 中有一个很关键的内部模块 enhanced-resolve 就是处理依赖模块路径的解析的。
modules:指定了全局的node_modules,为了避免在内部一层层查找node_module文件夹,优化性能:
extensions:指定了一个可以省略后缀名的文件类型数组,一般不推荐使用太多,有查找性能问题。
alias:指定了访问路径的别名,可以在项目中直接访问如:import "css/a.less"
resolve: {
modules: [
// 使用绝对路径指定项目 node_modules,不做过多(一层层)查询
path.resolve(__dirname, 'node_modules'),
],
extensions: ['.vue', '.js', '.json', '.jsx', '.css'],
alias: {
'css': path.resolve(__dirname, 'src/assets/css')
}
},
module
webpack 中提供一种处理多种文件格式的机制,便是使用 loader。我们可以把 loader 理解为是一个转换器,负责把某种文件格式的内容转换成 webpack 可以支持打包的模块。
这里举例常用的几个loader,更多的loader可以查看:webpack loader
rules是一个数组,包括多个loader
test: 匹配文件路径的正则表达式,通常我们都是匹配文件类型后缀
include: 指定哪些路径下的文件需要经过 loader 处理,node_module不需要处理,性能问题
loader: 单个loader可以直接用这个字段,否者可以用use字段,这些loader一般都需要安装依赖 $$$
use:指定使用的loader,use字段是一个数组,注意顺序问题,先使用的loader需要排在后面,ExtractTextPlugin是模块extract-text-webpack-plugin
用于将文件分离,这个plugin比较特殊需要loader的配合,后续讲解。
// 简单的loader
module: {
rules: [{
test: /\.(less|css)$/,
include: [
path.resolve(__dirname, 'src')
],
use: [
{ loader: 'css-loader'},
{ loader: 'style-loader'}
]
}]
}
// 如果结合ExtractTextPlugin插件分离文件和postcss-loader等其他loader
module: {
rules: [{
test: /\.(less|css)$/,
include: [
path.resolve(__dirname, 'src')
],
use: ExtractTextPlugin.extract({
use: [{
// 负责解析 CSS 代码,主要是为了处理 CSS 中的依赖,
// 例如 @import 和 url() 等引用外部文件的声明
loader: 'css-loader',
options: {
minimize: true, // 使用 css 的压缩功能
},
},
{ loader: 'postcss-loader'},
{ loader: 'less-loader' }
],
// 会将 css-loader 解析的结果转变成 JS 代码,
// 运行时【动态】插入 style 标签来让 CSS 代码生效
fallback: 'style-loader'
})
}]
}
常用的loader还有:
url-loader
,url-loader 和 file-loader 的功能类似,但前者可以转成base64 $$$
image-webpack-loader
,image-webpack-loader 的压缩是使用 imagemin 提供的一系列图片压缩类库来处理的 $$$
babel-loader
,这个不多说了,es6离不开babel。 $$$
plugins
模块代码转换的工作由 loader 来处理,除此之外的其他任何工作都可以交由 plugin 来完成,plugin分为webpack内置和第三方的。
CopyWebpackPlugin
、HtmlWebpackPlugin
、ExtractTextPlugin
都是非常重要的plugin。具体看代码中的注释:$$$
const UglifyPlugin = require('uglifyjs-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const ExtractTextPlugin = require('extract-text-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')
plugins: [
new webpack.DefinePlugin({
'process.env': {
NODE_ENV: JSON.stringify(process.env.NODE_ENV)
}
}),
// 直接将static文件复制到dist中
new CopyWebpackPlugin([
{ from: 'static/*.*', to: '', }
]),
// webpack 4.x 版本运行时,mode 为 production 即会启动压缩 JS 代码的插件,
// 屏蔽此插件,3.x可以用这个插件
new UglifyPlugin(),
// 如果我们的文件名或者路径会变化,例如使用 [hash] 来进行命名,那么最好是将 HTML 引用路径和我们的构建结果关联起来,这个时候我们可以使用 html-webpack-plugin。
// 如果需要添加多个页面关联,那么实例化多个 html-webpack-plugin, 并将它们都放到 plugins 字段数组中就可以了。
new HtmlWebpackPlugin({
filename: 'index.html', // 配置输出文件名和路径
template: './index.html', // 配置html文件模板,将js和这个关联起来
minify: { // 压缩 HTML 的配置
minifyCSS: true, // 压缩 HTML 中出现的 CSS 代码
minifyJS: true, // 压缩 HTML 中出现的 JS 代码
removeComments: true, // 移除注释
collapseWhitespace: true, // 缩去空格
removeAttributeQuotes: true // 移除属性引号
}
}),
// 使用方式特别,除了在plugins字段添加插件实例之外,还需要调整 loader 对应的配置。
// 配置输出的文件名,这里同样可以使用 [hash],多个文件会加载在一起
// name 是根据entry中的入口名字
new ExtractTextPlugin({
filename: '[name].css?rd=[hash:8]'
}),
// 在 HMR 更新的浏览器控制台中打印更易读的模块名称
new webpack.NamedModulesPlugin(),
// Hot Module Replacement 的插件
//【在这个概念出来之前,我们使用过 Hot Reloading,当代码变更时通知浏览器刷新页面】
//【HMR 可以理解为增强版的 Hot Reloading,不用整个页面刷新,而局部替换掉模块】
new webpack.HotModuleReplacementPlugin()
],
devServer
在 webpack 的配置中,可以通过 devServer 字段来配置 webpack-dev-server,如端口设置、启动 gzip 压缩等,这里简单讲解几个常用的配置。
一般来说只要使用hot: true
,来实现模块热替换。
devServer: {
// public: 'http://localhost:8080/', // public 字段用于指定静态服务的域名
// publicPath: 'static/', // 字段用于指定构建好的静态文件在浏览器中用什么路径去访问
port: '1234',
hot: true, // 模块热替换
before(app) {
// 当访问 /some/path 路径时,返回自定义的 json 数据
// 可以用于拦截部分请求返回特定内容,或者实现简单的数据 mock。
app.get('/api/test.json', function (req, res) {
res.json({ code: 200, message: 'hello world' })
})
},
/* proxy: {
'/login': {
target: 'http://127.0.0.1:8090',
changeOrigin: true,
pathRewrite: {
'^/login': '/login'
}
}
} */
},
optimization
webpack配置优化,比如webpack4.*
chunks分离就移到这里很简单的设置:
在webpack3.*中,想实现chunks分离需要用到webpack.optimize.CommonsChunkPlugin
插件:
plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor', // 使用 vendor 入口作为公共部分
filename: "vendor.js",
// minChunks: 3, // 公共的部分必须被 3 个 chunk 共享
minChunks: Infinity, // 这个配置会让webpack不再自动抽离公共模块,不管超过多少都不抽离,只抽离指定的vendor
})
]
在webpack4.*中,分离chunks移到了optimization中:
optimization: {
// webpack4.* chunks分离
splitChunks: {
// chunks: "all", // 所有的 chunks 代码公共的部分分离出来成为一个单独的文件
cacheGroups: {
vendor: {
chunks: "initial",
test: "vendor",
name: "vendor", // 使用 vendor 入口作为公共部分
enforce: true,
},
}
}
}
中间件
熟悉Node的都知道中间件,简单的说是来完成某件事情的插件。比如webpack-dev-middleware
就是在 Express
中提供 webpack-dev-server 静态服务能力的一个中间件 【dev】 $$$
npm install webpack-dev-middleware express -D
接下来我们创建一个dev.js
,然后我们用node dev.js
来执行,dev.js
代码如下:
const webpack = require('webpack')
const chalk = require('chalk') // 命令行中彩色出书
const opn = require('opn') // 自动打开浏览器
const ora = require('ora') // loading样式
const midddleware = require('webpack-dev-middleware')
const webpackOptions = require('./webpack.config.js')
const spinner = ora({
text: chalk.magenta('> 加载中...'),
spinner: 'bouncingBar'
}).start()
const port = 3000
// 本地的开发环境,可以再次全局设置mode,用于webpack配置中获取。如果script中设置了,就不需要这里设置了。
webpackOptions.mode = 'development'
const compiler = webpack(webpackOptions)
const express = require('express')
const app = express()
let hotMidddleware = midddleware(compiler, {
// webpack-dev-middleware 的配置选项
})
app.use(hotMidddleware)
// 编译完成的回调函数
hotMidddleware.waitUntilValid(() => {
spinner.stop()
console.log(chalk.magenta('> 编译完成...'))
console.log(chalk.magenta(`> 监听:http://localhost:${port}\n`))
opn(`http://localhost:${port}`)
})
app.listen(port, () => {
console.log(chalk.magenta('> 项目运行...'))
})
有了这样的中间件,我们完全可以创建一套cli,区分开发、测试、生产打包的配置。具体可以尝试实践,回头看vue-cli的配置,大体上没什么大问题了。
总结
env相关总结:
webpack 3:
package.json中 "start_": "NODE_ENV=development webpack-dev-server"
- 在webpack.config.js中可以获取 process.env.NODE_ENV
- 在runtime项目代码中【不能】获取到 process.env.NODE_ENV,默认为production。要想在项目中获取,需要webpack.DefinePlugin这个plugin设置
webpack 4:
package.json中 "start": "webpack-dev-server --mode development"
- 在webpack.config.js中【不能】获取process.env.NODE_ENV,需要把配置作为函数返回值暴露 module.exports = (env, argv) => {},其中arv.mode可以获取
- 在runtime项目代码中可以获取到 process.env.NODE_ENV。(注意不要设置webpack.DefinePlugin)
- 更加快速的增量编译构建。(hot reload比 webpack 3比较快)
我觉得可以集合两者,定义script中为"start": "NODE_ENV=development webpack-dev-server --mode development"
postcss
这里只说其中一种方法:
-
安装依赖
postcss-loader
以及其他的postcss的plugin, $$$npm install postcss-loader autoprefixer -D
-
根目录目录建
.postcssrc.js
module.exports = { "plugins": { // to edit target browsers: use "browserlist" field in package.json "autoprefixer": {} } }
-
package.json
中添加browserslist"browserslist": [ "defaults", "not ie < 8", "last 2 versions", "> 1%", "iOS 7", "last 3 iOS versions" ]
在
webpack.config.js
的匹配css
的loader中加上postcss-loader
懒加载
遵循 ES 标准的动态加载语法 dynamic-import 来编写,如果你使用了 Babel 的话,还需要 Syntax Dynamic Import 这个 Babel 插件来处理 import() 这种语法。
项目代码中:
setTimeout(() => { // 模拟异步
// 注释指定了chunk名
import(/* webpackChunkName: "lodash_" */ './lodash.js').then((res) => {
console.log(res)
})
}, 5000);
安装依赖"babel-plugin-syntax-dynamic-import"
npm install babel-plugin-syntax-dynamic-import -D
项目根目录创建.babelrc
文件
{
"presets": [["env", { "modules": false }]],
"plugins": [
"syntax-dynamic-import"
]
}
webpack.config.js
中首先要有laoder,再者output中添加chunkFilename,为了分离出来有对应的名字:
output: {
path: path.resolve(__dirname, 'dist'),
chunkFilename: '[name].js?rd=[hash:5]'
},
module: {
rules: [{
test: /\.jsx?/, // 支持 js 和 jsx;x可有可无
include: [
// src 目录下的才需要经过 babel-loader 处理
path.resolve(__dirname, 'src')
],
loader: 'babel-loader',
}]
}