源代码css
熟悉 webpack 与 webpack4 配置。html
webpack4 相对于 3 的最主要的区别是所谓的零配置
,可是为了知足咱们的项目需求仍是要本身进行配置,不过咱们可使用一些 webpack 的预设值。同时 webpack 也拆成了两部分,webpack 和 webpack-cli,都须要本地安装。vue
咱们经过实现一个 vue 的开发模板(vue init webpack 模板,其实跟 vue 关系不太大)来进行一次体验。在配置过程当中会尽可能使用 webpack4 的相关内容。node
本文不作 webpack 配置的完整介绍,着重介绍配置过程当中须要注意的地方。查看代码注释阅读效果更佳,完整配置与详细注释可见源代码。配置位于 build 文件夹下。webpack
与版本 4 相关的章节会添加符号 ④。ios
须要注意的一点是,咱们的 webpack 代码是运行在node环境下的,这部分代码可使用 node api,可是咱们的业务代码(src下)是没法使用 node api 的。git
因为 webpack 配置中的如 context,entry(chunk入口),output(输出)和 module.rules 中 loaders 的配置在开发模式和生产模式基本都是公用的,因此咱们提取到 webpack.base.js
文件内,供复用。其中 output 部分以下:github
output: {
path: path.resolve(__dirname, '../dist/'), // 资源文件输出时写入的路径
filename: 'static/js/[name].[chunkhash].js', // 使用 chunkhash 加入文件名作文件更新和缓存处理
chunkFilename: 'static/js/[name].[chunkhash].js'
}
复制代码
须要注意的有:web
hash 是用在文件输出的名字中的,如 [name].[hash].js
,总的来讲,webpack 提供了三种 hash:vue-router
[hash]
:这次打包的全部内容的 hash。[chunkhash]
:每个 chunk 都根据自身的内容计算而来。[contenthash]
:由 css 提取插件提供,根据自身内容计算得来。三种 hash 的使用,咱们在优化部分再讲,先优先使用 [chunkhash]
。
loader 优先级须要注意两点,
{
test: /\.less$/,
use: [
'style-loader',
'css-loader',
'postcss-loader',
'less-loader'
]
}
复制代码
eslint-loader
和 babel-loader
,可是又不能配置在一个配置对象内,可以使用 enforce: 'pre' 强调优先级,由 eslint-loader
优先处理。{
test: /\.(js|vue)$/,
loader: 'eslint-loader',
enforce: 'pre',
},
{
test: /\.js$/,
loader: 'babel-loader'
}
复制代码
咱们以 less 文件的 loader 配置 ['vue-style-loader', 'css-loader', 'postcss-loader', 'less-loader']
,使用 @import url(demo.less)
为例:
vue-style-loader 功能相似 style-loader
可是因为 vue 中的单文件组件,又分为两种状况:
.vue 文件内的 style:
vue-loader
会对 .vue 单文件组件进行处理,对 .vue 单文件组件内的各类 lang="type" 咱们能够在 vue-loader
的 options 配置不一样的 loader,因为 vue-loader
内置了 postcss
对 css 进行处理,因此此处咱们不须要再配置 postcss-loader
{
test: /\.vue$/,
loader: 'vue-loader',
options: {
loaders: {
less: ['// xxx-loaders'],
scss: ['// xxx-loaders'],
}
}
}
复制代码
js 直接引入中引入样式文件:
如 main.js 中 import 'demo.less'
,这种方式引入的样式文件,在 vue-loader
处理范围置以外,因此仍然须要配置 postcss-loader
。
因为这种差别咱们将 对 css 预处理器文件的配置封装为函数,由 usePostCss
参数生成对应配置,将文件放入 utils.js
文件内,将 vue-loader
配置放在 vue-loader.js
文件内。
也就是对 css 预处理器的配置咱们须要在 vue-loader
内和 webpack
内配置两遍。
写这篇 README.md 期间 vue-loader 发布了 v15 版,须要配合插件使用,不用再进行两遍配置
postcss-loader 是一个强大的 css 处理工具,咱们将 postcss 的配置拆分出去,新建 postcss.config.js
配置文件
module.exports = {
plugins: {
// 处理 @import
'postcss-import': {},
// 处理 css 中 url
'postcss-url': {},
// 自动前缀
'autoprefixer': {
"browsers": [
"> 1%",
"last 2 versions"
]
}
}
}
复制代码
除了注释中列出的须要的功能插件,咱们还可能会用到 nextcss
(新的css语法的处理),px2rem/px-to-viewport
移动端适配相关的插件。
咱们使用 babel 编译浏览器不能识别的 js、类 js 语法,如转义 ES6+、JSX等。一样将 babel-loader 的配置拆分出去,须要建立 .babelrc
并配置:
{
"presets": [
[
/* * * babel-preset-env * 能够根据配置的目标运行环境自动启用须要的 babel 插件。 */
"env", {
"modules": false, // 关闭 babel 对 es module 的处理
"targets": { // 目标运行环境
"browsers": ["> 1%", "last 2 versions", "not ie <= 8"]
}
}
]
],
"plugins": [
"syntax-dynamic-import" // 异步加载语法编译插件
]
}
复制代码
咱们还须要对图片、视频、字体等文件进行 loader 配置,以字体文件为例子,主要用到的是 url-loader:
{
/** * 末尾 \?.* 匹配带 ? 资源路径 * 咱们引入的第三方 css 字体样式对字体的引用路径中可能带查询字符串的版本信息 */
test: /\.(woff2|woff|eot|ttf|otf)(\?.*)?$/,
/** * url-loader * 会配合 webpack 对资源引入路径进行复写,如将 css 提取成独立文件,可能出现 404 错误可查看 提取 js 中的 css 部分解决 * 会以 webpack 的输出路径为基本路径,以 name 配置进行具体输出 * limit 单位为 byte,小于这个大小的文件会编译为 base64 写进 js 或 html */
loader: 'url-loader',
options: {
limit: 10000,
name: 'static/fonts/[name].[hash:7].[ext]',
}
}
复制代码
直接引用(绝对路径)和代码执行时肯定的资源路径应该是以静态文件存在的,这些资源文件不会通过 webpack 编译处理,因此咱们将它们放在独立的文件夹(如 static)中,并在代码打包后拷贝到咱们的输出目录,咱们使用 copy-webpack-plugin 自动完成这个工做:
const CopyWebpackPlugin = require('copy-webpack-plugin')
// 在开发模式下,会将文件写入内存
new CopyWebpackPlugin([
{
from: path.resolve(__dirname, '../static'),
to: 'static',
ignore: ['.*']
}
])
复制代码
此插件在拷贝文件过多时会崩溃,不知道解决了没有。
咱们先进行生产模式的配置。
在 package.json 下添加
"scripts": {
"build": "node build/build.js"`
}
复制代码
那么使用 npm run build
命令就可执行 node build/build.js
,咱们不直接使用 webpack webpack.prod.config.js
命令去执行配置文件,而是在 build.js 中,作一些文件删除的处理,再启动 webpack。
主要是两个工做,引入 rimraf
模块删除 webpack 下以前产生的指定文件,启动 webpack,并在不一样阶段给出不一样的提示信息。
// 在第一行设置当前为 生产环境
process.env.NODE_ENV = 'production'
const webpack = require('webpack')
const rm = require('rimraf')
const webpackConfig = require('./webpack.prod')
// 删除 webpack 输出目录下的内容,也可只删除子文件如 static 等
rm(webpackConfig.output.path, err => {
// webpack 按照生产模式配置启动
webpack(webpackConfig, (err, stats) => {
// 输出一些状态信息
})
}
复制代码
更多细节见源代码注释。
新建 webpack.prod.js
文件,使用
const merge = require('webpack-merge') // 专用合并 webpack 配置的包
const webpackBaseConfig = require('./webpack.base')
module.exports = merge(webpackBaseConfig, {
// 生产模式配置
})
复制代码
合并基本配置和生产模式独有配置,而后咱们开始进行生产模式下的 webpack 的配置信息的填写。
这是 webpack4 的新 api ,有三个预设值:development
,production
,none
,咱们在生产模式选用mode: 'production'
,webpack4在此配置下默认启用了:
因此这些默认启用的内容咱们不须要再配置。
最后一点设置 process.env.NODE_ENV 的值设为 production
实际上是使用 DefinePlugin 插件:
new webpack.DefinePlugin({
"process.env.NODE_ENV": JSON.stringify("production")
})
复制代码
从而咱们能够在业务代码中经过 process.env.NODE_ENV
,如进行判断,使用开发接口仍是线上接口。若是咱们须要在 webpack 中判断当前环境,还须要单独的设置 process.env.NODE_ENV = 'production'
,这也是咱们在 build.js
中第一行作的事情。
<script src="./bundles.js"></script>
(还可能包括后面提取出来的 css 文件)到 HTML 文件。const HtmlWebpackPlugin = require('html-webpack-plugin')
plugins: [
new HtmlWebpackPlugin({
filename: path.join(__dirname, '../dist/index.html'),// 文件写入路径
template: path.join(__dirname, '../src/index.html'),// 模板文件路径
inject: true // js 等 bundles 插入 html 的位置 head/body等
})
]
复制代码
若是不对 HtmlWebpackPlugin 进行配置,则其会建立一个 HTML 文件,其中 filename
在开发模式下仍是比较重要的。
使用过 webpack3 的同窗应该对 extract-text-webpack-plugin 插件(以旧插件代称)比较熟悉,为了尝试webpack4,我并不想使用这个插件的 @next
版本,因此选择了新的替代插件 mini-css-extract-plugin(以新插件代称)。
与旧插件相同,一样须要在 webpack 的 loader 部分和 plugin 部分都进行配置,不一样的是新插件提供了单独的 loader,在 loader 部分与旧插件的配置方式不太相同。配置以下:
loader 部分
```js
const MiniCssExtractPlugin = require("mini-css-extract-plugin")
// ...
[
{
loader: MiniCssExtractPlugin.loader,
options: {
/*
* 复写 css 文件中资源路径
* webpack3.x 配置在 extract-text-webpack-plugin 插件中
* 由于 css 文件中的外链是相对与 css 的,
* 咱们抽离的 css 文件在可能会单独放在 css 文件夹内
* 引用其余如 img/a.png 会寻址错误
* 这种状况下因此单独须要配置 ../,复写其中资源的路径
*/
publicPath: '../'
},
{
loader: 'css-loader',
options: {}
},
{
loader: 'less-loader',
options: {}
}
]
```
复制代码
plugin 部分
```js
new MiniCssExtractPlugin({
// 输出到单独的 css 文件夹下
filename: "static/css/[name].[chunkhash].css"
})
```
复制代码
能够看到这个 loader 也配置在了 css 预处理器部分,在前面咱们已经把 css 预处理器的配置提取到了 utils.js 文件的函数内,因此这里也是,咱们使用 extract
参数决定是否须要提取。
回忆一下,以前使用的 style-loader
或 vue-style-loader
的做用,它们会建立标签将 css 的内容直接插入到 HTML中。而提取成独立的 css 文件以后,插入到 HTML 的工做由 html-webpack-plugin
插件完成,二者职责的这部分职责是重复的,因此咱们须要使用 extract
参数作相似以下处理:
if (options.extract) {
return [MiniCssExtractPlugin.loader, ...otherLoaders]
} else {
return ['vue-style-loader', ...otherLoaders]
}
复制代码
这是 webpack 配置中很重要的一个环节,影响到咱们使用浏览器缓存的合理性,影响页面资源的加载速度,将 js 进行合理拆分,能够有效减少咱们每次更新代码影响到的文件范围。
使用过 webpack3 的同窗必定清楚,咱们通常会提取出这么几个文件 manifest.js
(webpack 运行时,即webpack解析其余bundle的代码等)、vendor.js
(node_modules内的库)、app.js(真正的项目业务代码)。在 webpack3 中咱们使用 webpack.optimize.CommonsChunkPlugin
插件进行提取,webpack4 中咱们能够直接使用 optimization 配置项进行配置(固然仍可以使用插件配置):
/** * 优化部分包括代码拆分 * 且运行时(manifest)的代码拆分提取为了独立的 runtimeChunk 配置 */
optimization: {
splitChunks: {
chunks: "all",
cacheGroups: {
// 提取 node_modules 中代码
vendors: {
test: /[\\/]node_modules[\\/]/,
name: "vendors",
chunks: "all"
},
commons: {
// async 设置提取异步代码中的公用代码
chunks: "async"
name: 'commons-async',
/** * minSize 默认为 30000 * 想要使代码拆分真的按照咱们的设置来 * 须要减少 minSize */
minSize: 0,
// 至少为两个 chunks 的公用代码
minChunks: 2
}
}
},
/** * 对应原来的 minchunks: Infinity * 提取 webpack 运行时代码 * 直接置为 true 或设置 name */
runtimeChunk: {
name: 'manifest'
}
}
复制代码
也可将不会变的开发依赖配置到单独的entry中,如:
entry: {
app: 'index.js',
vendor2: ['vue', 'vue-router', 'axios']
}
复制代码
开发模式与生产模式的不一样是,在开发时会频繁运行代码,因此不少东西在开发模式是不推荐配置的,如css文件提取,代码压缩等。因此针对一些写入公共配置文件,可是开发模式不须要的功能,咱们须要作相似修改:process.env.NODE_ENV === 'production' ? true : false
,如 css 预处理中是否须要配置提取 loader MiniCssExtractPlugin.loader
。此外还有一些是只配置在生产模式下的,如 MiniCssExtractPlugin
和 js 代码拆分优化。
开发模式咱们须要一个开发服务,帮咱们完成实时更新、接口代理等功能。咱们使用 webpack-dev-server
。须要 npm 安装。
一样,在 package.json 下添加
"scripts": {
"dev": "webpack-dev-server --config ./build/webpack.dev.js"
}
复制代码
使用 --config
指定配置文件,因为命令直接调用 webpack-dev-server 运行,因此咱们直接写配置就好,能够不像生产模式同样去编写调用逻辑。
新建 webpack.dev.js
文件,一样使用:
// 在第一行设置当前环境为开发环境
process.env.NODE_ENV = 'development'
const merge = require('webpack-merge') // 专用合并webpack配置的包
const webpackBaseConfig = require('./webpack.base')
module.exports = merge(webpackBaseConfig, {
// 开发模式配置
})
复制代码
一样,在开发模式下咱们能够将 mode
配置为 development
,一样默认启用了一些功能:
devServer: {
clientLogLevel: 'warning',
inline: true,
// 启动热更新
hot: true,
// 在页面上全屏输出报错信息
overlay: {
warnings: true,
errors: true
},
// 显示 webpack 构建进度
progress: true,
// dev-server 服务路径
contentBase: false,
compress: true,
host: 'localhost',
port: '8080',
// 自动打开浏览器
open: true,
// 能够进行接口代理配置
proxy: xxx,
// 跟 friendly-errors-webpack-plugin 插件配合
quiet: true,
publicPath: '/'
}
复制代码
devServer 使用热更新 hot 时须要使用插件:
plugins: [
new webpack.HotModuleReplacementPlugin()
]
复制代码
优化 webpack 输出信息,须要配置:
const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin')
plugins: [
new FriendlyErrorsPlugin()
]
复制代码
[hash]
作标识,文件名变化没法热更新,因此须要将原来配置在公共配置中的 output 中的文件名配置分别写入生产和开发模式配置中,开发模式去掉 [hash]
filename: 'static/[name].js',
chunkFilename: 'static/[id].js'
复制代码
devServer
启动后的服务内容与 contentBase
有关,二者须要一致,因此咱们将 HtmlWebpackPlugin
的配置也分为 生产和开发模式,开发模式下使用:new HtmlWebpackPlugin({
filename: 'index.html', // 文件写入路径,前面的路径与 devServer 中 contentBase 对应
template: path.resolve(__dirname, '../src/index.html'),// 模板文件路径
inject: true
})
复制代码
咱们能够提取到独立的 config 文件中(本代码没作)。
在生产模式的 拆分 js 代码
部分咱们已经讲了如何拆分,那么为了更好的分析咱们的拆分是否合理,咱们能够配置一个 bundle 组成分析的插件。
const BundleAnalyzer = require('webpack-bundle-analyzer')
plugins: [
new BundleAnalyzer.BundleAnalyzerPlugin()
]
复制代码
咱们使用文件名中的 hash 变化来进行资源文件的更新,那么合理利用缓存时,就要求咱们合理的拆分文件,在内容更新时最小限度的影响文件名中的 hash。这里就用到了[hash]
,[chunkhash]
,[contenthash]
。然而 webpack 对 hash 的默认处理并不尽如人意,这一部分的优化能够参考基于 webpack 的持久化缓存方案
多页面配置代码位于 muilt-pages 分支。咱们只需作少许修改,以目前有 entry 页和 index 页为例。
将两个页面的 js 入口都配置在 webpack
的 entry
中:
entry: {
/** * 入口,chunkname: 路径 * 多入口可配置多个 */
main: './src/main.js',
entry: './src/entry.js'
}
复制代码
也能够本身设置项目结构,使用 node api 动态读取的方式获取目前的多页面入口。
需按照页面个数配置多个 HtmlWebpackPlugin
:
new HtmlWebpackPlugin({
filename: path.join(__dirname, '../dist/main.html'),// 文件写入路径
template: path.join(__dirname, '../src/index.html'),// 模板文件路径
inject: true, // 插入位置
chunks: ['manifest', 'vendors', 'common', 'main']
}),
new HtmlWebpackPlugin({
filename: path.join(__dirname, '../dist/entry.html'),// 文件写入路径
template: path.join(__dirname, '../src/index.html'),// 模板文件路径
inject: true, // 插入位置
chunks: ['manifest', 'vendors', 'common', 'entry']
}),
复制代码
其中需手动指定每一个页面的插入的 chunks(同步的),不然会将其余页面的文件也一同插入当前页面。
在单页面下,通常不存在提取非异步 js 文件的公共代码(非 node_modules)的问题,在多页面下咱们的页面间可能会公用 api、配置等文件,此时能够增长:
'common': {
// initial 设置提取同步代码中的公用代码
chunks: 'initial',
// test: 'xxxx', 也可以使用 test 选择提取哪些 chunks 里的代码
name: 'common',
minSize: 0,
minChunks: 2
}
复制代码
提取同步代码中的公用代码