文章简介
通过研究vue-cli脚手架,构建属于自己的webpack4.0项目。了解webpack原理,部署环境,配置流程等。
学习目标
- 了解 vue-cli#2.0 的 webpack 配置
- 了解webpack原理
- 熟悉webpack部署环境
- 熟悉webpack配置流程
学习过程
一、webpack详解
什么是webpack?
webpack是一个打包工具,他的宗旨是一切静态资源即可打包。有人就会问为什么要webpack?webpack是现代前端技术的基石,常规的开发方式,比如jquery,html,css静态网页开发已经落后了。现在是MVVM的时代,数据驱动界面。webpack将现代js开发中的各种新型有用的技术,集合打包。
webpack核心概念
Webpack具有四个核心的概念,想要入门Webpack就得先好好了解这五个核心概念。它们分别是Entry(入口)、Output(输出)、loader和Plugins(插件)以及Module(模块)。接下来详细介绍这四个核心概念。
- Entry:入口,Webpack 执行构建的第一步将从 Entry 开始,可抽象成输入。 Module:模块,在 Webpack 里一切皆模块,一个模块对应着一个文件。Webpack 会从配置的 Entry 开始递归找出所有依赖的模块。
- Output:输出结果,在 Webpack 经过一系列处理并得出最终想要的代码后输出结果。
- Loader:模块转换器,用于把模块原内容按照需求转换成新内容。
- Plugin:扩展插件,在 Webpack 构建流程中的特定时机注入扩展逻辑来改变构建结果或做你想要的事情。
- Module:模块,在 Webpack 里一切皆模块,一个模块对应着一个文件。Webpack 会从配置的 Entry 开始递归找出所有依赖的模块。
webpack执行流程
webpack启动后会在entry里配置的module开始递归解析entry所依赖的所有module,每找到一个module, 就会根据配置的loader去找相应的转换规则,对module进行转换后在解析当前module所依赖的module,这些模块会以entry为分组,一个entry和所有相依赖的module也就是一个chunk,最后webpack会把所有chunk转换成文件输出,在整个流程中webpack会在恰当的时机执行plugin的逻辑。
webpack与gulp
说到webpack总是会被与gulp比较一番。说实话webpack发展到4.0,这两者之前功能上基本上都相似,该有的大家都有,只是处理机制有所不同。
gulp是工具链,构建工具,可以配合各种插件做JS压缩,CSS压缩,less编译替代手动实现自动化工作。所以它的主要作用是:
- 1.构建工具
- 2.自动化
- 3.提高效率
Gulp侧重于前端开发的 整个过程的控制管理(像是流水线),我们可以通过给gulp配置不通的task(通过Gulp中的gulp.task()方法配置,比如启动server、sass/less预编译、文件的合并压缩等等)来让gulp实现不同的功能,从而构建整个前端开发流程。
webpack是文件打包工具,可以把项目的各种js文件,css文件等打包合成一个或多个文件,主要用于模块化方案,预编译模块的方案。所以它的主要作用是: 1.打包工具 2.模块化识别 3.编译模块代码方案
webpack把开发中的所有资源(图片、js文件、css文件等)都可以看成模块,最初Webpack本身就是为前端JS代码打包而设计的,后来被扩展到其他资源的打包处理。Webpack是通过loader(加载器)和plugins(插件)对资源进行处理的。
二、分析vue-cli脚手架的webpack配置
vue-cli目录结构
├── README.md
├── build // 项目构建(webpack)相关代码
│ ├── build.js // 生产环境构建代码
│ ├── check-versions.js // 检查node、npm等版本
│ ├── dev-client.js // 热重载相关
│ ├── dev-server.js // 构建本地服务器
│ ├── utils.js // 构建工具相关
│ ├── webpack.base.conf.js // webpack基础配置
│ ├── webpack.dev.conf.js // webpack开发环境配置
│ └── webpack.prod.conf.js // webpack生产环境配置
├── config // 项目开发环境配置
│ ├── dev.env.js // 开发环境变量
│ ├── index.js // 项目一些配置变量
│ └── prod.env.js // 生产环境变量
├── index.html // 入口页面
├── package.json // 项目基本信息
├── src // 源码目录
│ ├── App.vue // 页面入口文件
│ ├── assets
│ │ └── logo.png
│ ├── components // vue公共组件
│ │ └── Hello.vue
│ └── main.js // 程序入口文件,加载各种公共组件
└── static // 静态文件,比如一些图片,json数据等
vue-cli基本功能实现
- 处理sass/scss/less/stylus转成css
- 解析后缀为css的文件,压缩提取出的css,兼容css的插件
- 压缩js文件
- 简化了HTML文件的创建,引入了外部资源
- 压缩文件,可将图片转化为base64
- 兼容vue文件
- babel转译es5
- 开发时热更新
vue-cli的webpack具体文件分析
入口
从 package.json 中我们可以看到
"scripts": {
// dev webpack-dev-server --inline 模式 --progress 显示进度 --config 指定配置文件(默认是webpack.config.js)
"dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js",
"start": "npm run dev",
// jest测试
"unit": "jest --config test/unit/jest.conf.js --coverage",
// e2e测试
"e2e": "node test/e2e/runner.js",
// 运行jest测试和e2e测试
"test": "npm run unit && npm run e2e",
// eslint --ext 指定扩展名和相应的文件
"lint": "eslint --ext .js,.vue src test/unit test/e2e/specs",
// node 执行build/build.js文件
"build": "node build/build.js"
},
Npm Script 底层实现原理是通过调用 Shell 去运行脚本命令。npm run start等同于运行npm run dev。
Npm Script 还有一个重要的功能是能运行安装到项目目录里的 node_modules 里的可执行模块。
例如在通过命令npm i -D webpack-dev-server将webpack-dev-server安装到项目后,是无法直接在项目根目录下通过命令 webpack-dev-server 去执行 webpack-dev-server 构建的,而是要通过命令 ./node_modules/.bin/webpack-dev-server 去执行。
npm run dev指定了build/webpack.dev.conf.js配置去启动服务
基本配置文件是build/webpack.base.conf.js
-
1、引入各种插件、配置等,其中引入了build/vue-loader.conf.js相关配置,
-
2、创建eslint规则配置,默认启用,
-
3、导出webpack配置对象,其中包含context,入口entry,输出output,resolve,module下的rules(处理对应文件的规则),和node相关的配置等
'use strict'
// node自带的文件路径工具
const path = require('path')
// 工具函数集合
const utils = require('./utils')
// 配置文件
const config = require('../config')
// 引入vue-loader
const vueLoaderConfig = require('./vue-loader.conf')
/**
* 获得绝对路径
* @method resolve
* @param {String} dir 相对于本文件的路径
* @return {String} 绝对路径
*/
function resolve (dir) {
return path.join(__dirname, '..', dir)
}
module.exports = {
context: path.resolve(__dirname, '../'),
entry: {
app: './src/main.js' //入口文件
},
output: {
path: config.build.assetsRoot,
filename: '[name].js', //[]占位符
//判断环境是否是生产环境,如果是打包后的文件已publicPath做前缀
publicPath: process.env.NODE_ENV === 'production'
? config.build.assetsPublicPath
: config.dev.assetsPublicPath
},
resolve: {
// 自动补全的扩展名
extensions: ['.js', '.vue', '.json'],
// 路径别名
alias: {
'vue$': 'vue/dist/vue.esm.js',
'@': resolve('src'),
'common': resolve('src/common'),
'components': resolve('src/components')
}
},
module: {
//模块规则
rules: [
{// 处理 vue文件
test: /\.vue$/,
loader: 'vue-loader',
options: vueLoaderConfig
},
{// 编译 js
test: /\.js$/,
loader: 'babel-loader',
include: [resolve('src'), resolve('test'), resolve('node_modules/webpack-dev-server/client')]
},
{// 处理图片文件
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: utils.assetsPath('img/[name].[hash:7].[ext]')
}
},
{// 处理音频,视频文件
test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: utils.assetsPath('media/[name].[hash:7].[ext]')
}
},
{// 处理字体文件
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: utils.assetsPath('fonts/[name].[hash:7].[ext]')
}
}
]
},
node: {
// prevent webpack from injecting useless setImmediate polyfill because Vue
// source contains it (although only uses it if it's native).
setImmediate: false,
// prevent webpack from injecting mocks to Node native modules
// that does not make sense for the client
dgram: 'empty',
fs: 'empty',
net: 'empty',
tls: 'empty',
child_process: 'empty'
}
}
开发环境的入口文件是 build/webpack.dev.conf.js。
-
1、引入各种依赖,同时也引入了config文件夹下的变量和配置,和一个工具函数build/utils.js,
-
2、合并build/webpack.base.conf.js配置文件,
-
3、配置开发环境一些devServer,plugin等配置,
-
4、最后导出了一个Promise,根据配置的端口,寻找可用的端口来启动服务。
'use strict'
const utils = require('./utils')
const webpack = require('webpack')
// 配置文件
const config = require('../config')
// webpack 配置合并插件
const merge = require('webpack-merge')
const path = require('path')
// webpac基本配置
const baseWebpackConfig = require('./webpack.base.conf')
//拷贝插件
const CopyWebpackPlugin = require('copy-webpack-plugin')
// 自动生成 html 并且注入到 .html 文件中的插件
const HtmlWebpackPlugin = require('html-webpack-plugin')
//识别某些类别的webpack错误,并清理,聚合和优先级
const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin')
//自动获取端口
const portfinder = require('portfinder')
//获取本地地址
const HOST = process.env.HOST
//获取端口
const PORT = process.env.PORT && Number(process.env.PORT)
// 合并基本的webpack配置
const devWebpackConfig = merge(baseWebpackConfig, {
module: {
// cssSourceMap这里配置的是true
rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap, usePostCSS: true })
},
// cheap-module-eval-source-map is faster for development
// 在开发环境是cheap-module-eval-source-map选项更快
// 这里配置的是cheap-module-eval-source-map
// 更多可以查看中文文档:https://webpack.docschina.org/configuration/devtool/#devtool
devtool: config.dev.devtool,
// these devServer options should be customized in /config/index.js
devServer: {
// 配置在客户端的日志等级,这会影响到你在浏览器开发者工具控制台里看到的日志内容。
// clientLogLevel 是枚举类型,可取如下之一的值 none | error | warning | info。
// 默认为 info 级别,即输出所有类型的日志,设置成 none 可以不输出任何日志。
clientLogLevel: 'warning',
// historyApiFallback boolean object 用于方便的开发使用了 HTML5 History API 的单页应用。
// 可以简单true 或者 任意的 404 响应可以提供为 index.html 页面。
historyApiFallback: {
rewrites: [
{ from: /.*/, to: path.posix.join(config.dev.assetsPublicPath, 'index.html') },
],
},
// 开启热更新
hot: true,
// contentBase 配置 DevServer HTTP 服务器的文件根目录。
// 默认情况下为当前执行目录,通常是项目根目录,所有一般情况下你不必设置它,除非你有额外的文件需要被 DevServer 服务
contentBase: false, // since we use CopyWebpackPlugin.
// compress 配置是否启用 gzip 压缩。boolean 为类型,默认为 false。
compress: true,
// host
// 例如你想要局域网中的其它设备访问你本地的服务,可以在启动 DevServer 时带上 --host 0.0.0.0
// 或者直接设置为 0.0.0.0
// 这里配置的是localhost
host: HOST || config.dev.host,
// 端口号 这里配置的是8080
port: PORT || config.dev.port,
// 打开浏览器,这里配置是不打开false
open: config.dev.autoOpenBrowser,
// 是否在浏览器以遮罩形式显示报错信息 这里配置的是true
overlay: config.dev.errorOverlay
? { warnings: false, errors: true }
: false,
publicPath: config.dev.assetsPublicPath,
// 代理 这里配置的是空{},有需要可以自行配置
proxy: config.dev.proxyTable,
// 启用 quiet 后,除了初始启动信息之外的任何内容都不会被打印到控制台。这也意味着来自 webpack 的错误或警告在控制台不可见。
// 开启后一般非常干净只有类似的提示 Your application is running here: http://localhost:8080
quiet: true, // necessary for FriendlyErrorsPlugin
// webpack-dev-middleware
// watch: false,
// 启用 Watch 模式。这意味着在初始构建之后,webpack 将继续监听任何已解析文件的更改。Watch 模式默认关闭。
// webpack-dev-server 和 webpack-dev-middleware 里 Watch 模式默认开启。
// Watch 模式的选项
watchOptions: {
// 或者指定毫秒为单位进行轮询。
// 这里配置为false
poll: config.dev.poll,
}
},
plugins: [
// 定义为开发环境
new webpack.DefinePlugin({
'process.env': require('../config/dev.env')
}),
// 热更新插件
new webpack.HotModuleReplacementPlugin(),
// 热更新时显示具体的模块路径
new webpack.NamedModulesPlugin(), // HMR shows correct file names in console on update.
// 在编译出现错误时,使用 NoEmitOnErrorsPlugin 来跳过输出阶段。
new webpack.NoEmitOnErrorsPlugin(),
new HtmlWebpackPlugin({
// inject 默认值 true,script标签位于html文件的 body 底部
// body 通true, header, script 标签位于 head 标签内
// false 不插入生成的 js 文件,只是单纯的生成一个 html 文件
filename: 'index.html',
template: 'index.html',
inject: true,
thymeleaf: require('../src/common/js/thymeleaf.js') || ""
}),
// copy custom static assets
// 把static资源复制到相应目录
new CopyWebpackPlugin([
{
from: path.resolve(__dirname, '../static'),
to: config.dev.assetsSubDirectory,
ignore: ['.*']
}
])
]
})
// 导出一个promise
module.exports = new Promise((resolve, reject) => {
// process.env.PORT 可以在命令行指定端口号,比如PORT=2000 npm run dev,那访问就是http://localhost:2000
// config.dev.port 这里配置是 8080
portfinder.basePort = process.env.PORT || config.dev.port
// 以配置的端口为基准,寻找可用的端口,比如:如果8080占用,那就8081,以此类推
portfinder.getPort((err, port) => {
if (err) {
reject(err)
} else {
// publish the new Port, necessary for e2e tests
process.env.PORT = port
// add port to devServer config
devWebpackConfig.devServer.port = port
// Add FriendlyErrorsPlugin
devWebpackConfig.plugins.push(new FriendlyErrorsPlugin({
compilationSuccessInfo: {
messages: [`Your application is running here: http://${devWebpackConfig.devServer.host}:${port}`],
},
onErrors: config.dev.notifyOnErrors
? utils.createNotifierCallback()
: undefined
}))
resolve(devWebpackConfig)
}
})
})
webpack生产环境配置 build/webpack.prod.conf.js
-
1、引入一些插件和配置,其中引入了build/webpack.base.conf.js webpack基本配置文件,
-
2、用DefinePlugin定义环境,
-
3、合并基本配置,定义自己的配置webpackConfig,配置了一些modules下的rules,devtools配置,output输出配置,一些处理js、提取css、压缩css、输出html插件、提取公共代码等的 plugins,
-
4、如果启用gzip,再使用相应的插件处理,
-
5、如果启用了分析打包后的插件,则用webpack-bundle-analyzer,
-
6、最后导出这份配置。
'use strict'
// 引入node路径相关
const path = require('path')
// 引入utils工具函数
const utils = require('./utils')
// 引入webpack
const webpack = require('webpack')
// 引入config/index.js配置文件
const config = require('../config')
// 合并webpack配置的插件
const merge = require('webpack-merge')
// 基本的webpack配置
const baseWebpackConfig = require('./webpack.base.conf')
// 拷贝文件和文件夹的插件
const CopyWebpackPlugin = require('copy-webpack-plugin')
// 压缩处理HTML的插件
const HtmlWebpackPlugin = require('html-webpack-plugin')
const ExtractTextPlugin = require('extract-text-webpack-plugin')
// 压缩处理css的插件
const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin')
// 压缩处理js的插件
const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
// 用DefinePlugin定义环境
const env = process.env.NODE_ENV === 'testing'
// 这里是 { NODE_ENV: '"testing"' }
? require('../config/test.env')
// 这里是 { NODE_ENV: '"production"' }
: require('../config/prod.env')
// 合并基本webpack配置
const webpackConfig = merge(baseWebpackConfig, {
module: {
// 通过styleLoaders函数生成样式的一些规则
rules: utils.styleLoaders({
// sourceMap这里是true
sourceMap: config.build.productionSourceMap,
// 是否提取css到单独的css文件
extract: true,
// 是否使用postcss
usePostCSS: true
})
},
// 配置使用sourceMap true 这里是 #source-map
devtool: config.build.productionSourceMap ? config.build.devtool : false,
output: {
// 这里是根目录下的dist
path: config.build.assetsRoot,
// 文件名称 chunkhash
filename: utils.assetsPath('js/[name].[chunkhash].js'),
// chunks名称 chunkhash
chunkFilename: utils.assetsPath('js/[id].[chunkhash].js')
},
plugins: [
// http://vuejs.github.io/vue-loader/en/workflow/production.html
// 定义具体是什么环境
new webpack.DefinePlugin({
'process.env': env
}),
// 压缩js插件
new UglifyJsPlugin({
uglifyOptions: {
compress: {
// 警告
warnings: false
// 构建后的文件 常用的配置还有这些
// 去除console.log 默认为false。 传入true会丢弃对console函数的调用。
// drop_console: true,
// 去除debugger
// drop_debugger: true,
// 默认为null. 你可以传入一个名称的数组,而UglifyJs将会假定那些函数不会产生副作用。
// pure_funcs: [ 'console.log', 'console.log.apply' ],
}
},
// 是否开启sourceMap 这里是true
sourceMap: config.build.productionSourceMap,
// 平行处理(同时处理)加快速度
parallel: true
}),
// extract css into its own file
// 提取css到单独的css文件
new ExtractTextPlugin({
// 提取到相应的文件名 使用内容hash contenthash
filename: utils.assetsPath('css/[name].[contenthash].css'),
// Setting the following option to `false` will not extract CSS from codesplit chunks.
// Their CSS will instead be inserted dynamically with style-loader when the codesplit chunk has been loaded by webpack.
// It's currently set to `true` because we are seeing that sourcemaps are included in the codesplit bundle as well when it's `false`,
// increasing file size: https://github.com/vuejs-templates/webpack/issues/1110
// allChunks 默认是false,true指提取所有chunks包括动态引入的组件。
allChunks: true,
}),
// Compress extracted CSS. We are using this plugin so that possible
// duplicated CSS from different components can be deduped.
new OptimizeCSSPlugin({
// 这里配置是true
cssProcessorOptions: config.build.productionSourceMap
? { safe: true, map: { inline: false } }
: { safe: true }
}),
// generate dist index.html with correct asset hash for caching.
// you can customize output by editing /index.html
// see https://github.com/ampedandwired/html-webpack-plugin
new HtmlWebpackPlugin({
// 输出html名称
filename: process.env.NODE_ENV === 'testing'
? 'index.html'
// 这里是 根目录下的dist/index.html
: config.build.index,
// 使用哪个模板
template: 'index.html',
// inject 默认值 true,script标签位于html文件的 body 底部
// body 通true, header, script 标签位于 head 标签内
// false 不插入生成的 js 文件,只是单纯的生成一个 html 文件
inject: true,
// 压缩
minify: {
// 删除注释
removeComments: true,
// 删除空格和换行
collapseWhitespace: true,
// 删除html标签中属性的双引号
removeAttributeQuotes: true
// 更多配置查看html-minifier插件
// more options:
// https://github.com/kangax/html-minifier#options-quick-reference
},
// necessary to consistently work with multiple chunks via CommonsChunkPlugin
// 在chunk被插入到html之前,你可以控制它们的排序。允许的值 ‘none’ | ‘auto’ | ‘dependency’ | {function} 默认为‘auto’.
// dependency 依赖(从属)
chunksSortMode: 'dependency'
}),
// keep module.id stable when vendor modules does not change
// 根据代码内容生成普通模块的id,确保源码不变,moduleID不变。
new webpack.HashedModuleIdsPlugin(),
// enable scope hoisting
// 开启作用域提升 webpack3新的特性,作用是让代码文件更小、运行的更快
new webpack.optimize.ModuleConcatenationPlugin(),
// split vendor js into its own file
// 提取公共代码
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
minChunks (module) {
// any required modules inside node_modules are extracted to vendor
return (
module.resource &&
/\.js$/.test(module.resource) &&
module.resource.indexOf(
path.join(__dirname, '../node_modules')
) === 0
)
}
}),
// extract webpack runtime and module manifest to its own file in order to
// prevent vendor hash from being updated whenever app bundle is updated
// 提取公共代码
new webpack.optimize.CommonsChunkPlugin({
// 把公共的部分放到 manifest 中
name: 'manifest',
// 传入 `Infinity` 会马上生成 公共chunk,但里面没有模块。
minChunks: Infinity
}),
// This instance extracts shared chunks from code splitted chunks and bundles them
// in a separate chunk, similar to the vendor chunk
// see: https://webpack.js.org/plugins/commons-chunk-plugin/#extra-async-commons-chunk
// 提取动态组件
new webpack.optimize.CommonsChunkPlugin({
name: 'app',
// 如果设置为 `true`,一个异步的 公共chunk 会作为 `options.name` 的子模块,和 `options.chunks` 的兄弟模块被创建。
// 它会与 `options.chunks` 并行被加载。可以通过提供想要的字符串,而不是 `true` 来对输出的文件进行更换名称。
async: 'vendor-async',
// 如果设置为 `true`,所有 公共chunk 的子模块都会被选择
children: true,
// 最小3个,包含3,chunk的时候提取
minChunks: 3
}),
// copy custom static assets
// 把static资源复制到相应目录。
new CopyWebpackPlugin([
{
from: path.resolve(__dirname, '../static'),
// 这里配置是static
to: config.build.assetsSubDirectory,
// 忽略.开头的文件。比如这里的.gitkeep,这个文件是指空文件夹也提交到git
ignore: ['.*']
}
])
]
})
// 如果开始gzip压缩,使用compression-webpack-plugin插件处理。这里配置是false
// 需要使用是需要安装 npm i compression-webpack-plugin -D
if (config.build.productionGzip) {
const CompressionWebpackPlugin = require('compression-webpack-plugin')
webpackConfig.plugins.push(
new CompressionWebpackPlugin({
// asset: 目标资源名称。 [file] 会被替换成原始资源。
// [path] 会被替换成原始资源的路径, [query] 会被替换成查询字符串。默认值是 "[path].gz[query]"。
asset: '[path].gz[query]',
// algorithm: 可以是 function(buf, callback) 或者字符串。对于字符串来说依照 zlib 的算法(或者 zopfli 的算法)。默认值是 "gzip"。
algorithm: 'gzip',
// test: 所有匹配该正则的资源都会被处理。默认值是全部资源。
// config.build.productionGzipExtensions 这里是['js', 'css']
test: new RegExp(
'\\.(' +
config.build.productionGzipExtensions.join('|') +
')$'
),
// threshold: 只有大小大于该值的资源会被处理。单位是 bytes。默认值是 0。
threshold: 10240,
// minRatio: 只有压缩率小于这个值的资源才会被处理。默认值是 0.8。
minRatio: 0.8
})
)
}
// 输出分析的插件 运行npm run build --report
// config.build.bundleAnalyzerReport这里是 process.env.npm_config_report
// build结束后会自定打开 http://127.0.0.1:8888 链接
if (config.build.bundleAnalyzerReport) {
// 更多查看链接地址:https://www.npmjs.com/package/webpack-bundle-analyzer
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
webpackConfig.plugins.push(new BundleAnalyzerPlugin())
}
// 当然也可以用官方提供的网站 http://webpack.github.io/analyse/#home
// 运行类似 webpack --profile --json > stats.json 命令
// 把生成的构建信息stats.json上传即可
// 最终导出 webpackConfig
module.exports = webpackConfig
build/utils.js 工具函数
上文build/webpack.dev.conf.js提到引入了build/utils.js工具函数。
-
1、assetsPath返回输出路径,
-
2、cssLoaders返回相应的css-loader配置,
-
3、styleLoaders返回相应的处理样式的配置,
-
4、createNotifierCallback创建启动服务时出错时提示信息回调。
'use strict'
const path = require('path')
// 引入配置文件config/index.js
const config = require('../config')
// 提取css的插件
const ExtractTextPlugin = require('extract-text-webpack-plugin')
// 引入package.json配置
const packageConfig = require('../package.json')
// 返回路径
exports.assetsPath = function (_path) {
const assetsSubDirectory = process.env.NODE_ENV === 'production'
// 二级目录 这里是 static
? config.build.assetsSubDirectory
// 二级目录 这里是 static
: config.dev.assetsSubDirectory
// 生成跨平台兼容的路径
// 更多查看Node API链接:https://nodejs.org/api/path.html#path_path_posix
return path.posix.join(assetsSubDirectory, _path)
}
exports.cssLoaders = function (options) {
// 作为参数传递进来的options对象
// {
// // sourceMap这里是true
// sourceMap: true,
// // 是否提取css到单独的css文件
// extract: true,
// // 是否使用postcss
// usePostCSS: true
// }
options = options || {}
const cssLoader = {
loader: 'css-loader',
options: {
sourceMap: options.sourceMap
}
}
const postcssLoader = {
loader: 'postcss-loader',
options: {
sourceMap: options.sourceMap
}
}
// generate loader string to be used with extract text plugin
// 创建对应的loader配置
function generateLoaders (loader, loaderOptions) {
// 是否使用usePostCSS,来决定是否采用postcssLoader
const loaders = options.usePostCSS ? [cssLoader, postcssLoader] : [cssLoader]
if (loader) {
loaders.push({
loader: loader + '-loader',
// 合并 loaderOptions 生成options
options: Object.assign({}, loaderOptions, {
sourceMap: options.sourceMap
})
})
}
// Extract CSS when that option is specified
// (which is the case during production build)
if (options.extract) {
// 如果提取使用ExtractTextPlugin插件提取
// 更多配置 看插件中文文档:https://webpack.docschina.org/plugins/extract-text-webpack-plugin/
return ExtractTextPlugin.extract({
// 指需要什么样的loader去编译文件
// loader 被用于将资源转换成一个 CSS 导出模块 (必填)
use: loaders,
// loader(例如 'style-loader')应用于当 CSS 没有被提取(也就是一个额外的 chunk,当 allChunks: false)
fallback: 'vue-style-loader'
})
} else {
return ['vue-style-loader'].concat(loaders)
}
}
return {
css: generateLoaders(),
postcss: generateLoaders(),
less: generateLoaders('less'),
// sass indentedSyntax 语法缩进,类似下方格式
// #main
// color: blue
// font-size: 0.3em
sass: generateLoaders('sass', { indentedSyntax: true }),
scss: generateLoaders('sass'),
stylus: generateLoaders('stylus'),
styl: generateLoaders('stylus')
}
}
// Generate loaders for standalone style files (outside of .vue)
// 最终会返回webpack css相关的配置
exports.styleLoaders = function (options) {
// {
// // sourceMap这里是true
// sourceMap: true,
// // 是否提取css到单独的css文件
// extract: true,
// // 是否使用postcss
// usePostCSS: true
// }
const output = []
const loaders = exports.cssLoaders(options)
for (const extension in loaders) {
const loader = loaders[extension]
output.push({
test: new RegExp('\\.' + extension + '$'),
use: loader
})
}
return output
}
// npm run dev 出错时, FriendlyErrorsPlugin插件 配置 onErrors输出错误信息
exports.createNotifierCallback = () => {
// 'node-notifier'是一个跨平台系统通知的页面,当遇到错误时,它能用系统原生的推送方式给你推送信息
const notifier = require('node-notifier')
return (severity, errors) => {
if (severity !== 'error') return
const error = errors[0]
const filename = error.file && error.file.split('!').pop()
notifier.notify({
title: packageConfig.name,
message: severity + ': ' + error.name,
subtitle: filename || '',
icon: path.join(__dirname, 'logo.png')
})
}
}
.babelrc babel相关配置
配置了一些转码规则
{
// presets指明转码的规则
"presets": [
// env项是借助插件babel-preset-env,下面这个配置说的是babel对es6,es7,es8进行转码,并且设置amd,commonjs这样的模块化文件,不进行转码
["env", {
"modules": false,
"targets": {
"browsers": ["> 1%", "last 2 versions", "not ie <= 8"]
}
}],
"stage-2"
],
// plugins 属性告诉 Babel 要使用哪些插件,插件可以控制如何转换代码。
// transform-vue-jsx 表明可以在项目中使用jsx语法,会使用这个插件转换
"plugins": ["transform-vue-jsx", "transform-runtime"],
// 在特定的环境中所执行的转码规则,当环境变量是下面的test就会覆盖上面的设置
"env": {
// test 是提前设置的环境变量,如果没有设置BABEL_ENV则使用NODE_ENV,如果都没有设置默认就是development
"test": {
"presets": ["env", "stage-2"],
"plugins": ["transform-vue-jsx", "transform-es2015-modules-commonjs", "dynamic-import-node"]
}
}
}
##前言 接着上篇文章webpack4.0从零开始构建Vue项目1—webpack基础知识和vue-cli分析
三、构建自己的webpack4.0项目
1.前端环境搭建
1.初始化配置
mkdir webpack-vuedeom
cd webpack-vuedeom
npm init -y
2.配置webpack
npm install webpack webpack-cli --save-dev
npm install webpack@x.xx webpack-cli --save-dev //安装指定版本
npm info webpacke 所有webpack版本信息
webpack -v //查看web pack版本 但是针对全局按照的webpack
npm webpack -v //查看当前项目 webpack版本
3.新建配置文件
仿照vue-cli简易的搭建了项目需要的目录,也区分开发和生产环境
├── README.md
├── build // 项目构建(webpack)相关代码
│ ├── webpack.base.conf.js // webpack基础配置
│ ├── webpack.dev.conf.js // webpack开发环境配置
│ └── webpack.prod.conf.js // webpack生产环境配置
├── index.html // 入口页面
├── package.json // 项目基本信息
├── src // 源码目录
│ ├── App.vue // 页面入口文件
│ ├── common
│ │ ├──scss
│ │ │ └── style.scss // 样式文件
│ │ ├──js
│ │ │ └── test.js // js文件
│ ├── router
│ │ └── router.js // 路由配置
│ ├── components // vue公共组件
│ │ └── Hello.vue
│ └── main.js // 程序入口文件,加载各种公共组件
└── static // 静态文件,比如一些图片,json数据等
2.webpackp配置流程
webpack.base.conf.js基本配置文件,在配置文件里进行初始化
var path = require('path');
var config = {
entry: './src/main.js',
output: {
path: path.resolve(__dirname + '/dist'),//打包生成文件地址
filename: '[name].build.js',//生成文件ming
publicPath: '/dist/'//文件输出的公共路径
}
}
module.exports = config;
在package.json配置dev指令
"scripts": {
"build": "webpack build/webpack.base.conf.js ",
"start": "npm run dev"
},
webpack默认认识js模块,不支持其它格式,需要引入loader
安装loader命令这边都不写了,npm install安装就行了
- url-loader
如果图片较多,会发很多http请求,会降低页面性能。这个问题可以通过url-loader解决。url-loader会将引入的图片编码,生成dataURl。相当于把图片数据翻译成一串字符。再把这串字符打包到文件中,最终只需要引入这个文件就能访问图片了。当然,如果图片较大,编码会消耗性能。因此url-loader提供了一个limit参数,小于limit字节的文件会被转为DataURl,大于limit的还会使用file-loader进行copy。
const path = require('path')
module.exports ={
entry:'./src/main.js',
output:{
path:path.resolve(__dirname,'dist'),
filename:"demo.js"
},
module:{
rules:[
{
test:/\.(png|gif|jpe?g)/, // ?e可有可无
use: {
loader: "url-loader",
options:{
name:"[name]_[hash].[ext]", //[hash]解决缓存,[ext]保留旧文件的文件后缀
limit:500, //是把小于500B的文件打成Base64的格式,写入JS
outputPath:"image/" //图片放到image/目录下
}
}
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
loader: 'url-loader',
options: {
// 文件大小小于limit参数,url-loader将会把文件转为DataUR
limit: 10000,
name: '[name]-[hash:5].[ext]',
output: 'fonts/'
}
}
]
}
}
- Style-loader和css-loader
module: {
[
...
{
test: /\.css$/,
loader: 'style-loader!css-loader'
}]
},
- sass-loader node-sass
{
test: /\.(sa|sc|c)ss$/,
include: path.resolve(__dirname, '../src'), // 限制打包范围,提高打包速度
//exclude: /node_modules/,
// 排除node_modules文件夹
use: [
'style-loader',
'css-loader',
'sass-loader'
]
}
-postcss-loader postcss是一个用来处理css的系统程序,它不是css语处理器,也不是css后处理后处理器,也不是新的语法,也不是优化工具。postcss本身只做两件事,将css转换成css抽象语法树,可以简单的理解为将css转换成js;postcss做的第二件事就是调用插件来处理抽象语法树,通过插件实现对css的处理。 -autoprefixer 自动添加前缀 Autoprefixer是一个后处理程序,你可以同Sass,Stylus或LESS等预处理器共通使用。它适用于普通的CSS,而你无需关心要为哪些浏览器加前缀,只需全新关注于实现,并使用W3C最新的规范。配合postcss一起使用后更好.
{
test: /\.(sa|sc|c)ss$/,
include: path.resolve(__dirname, '../src'), // 限制打包范围,提高打包速度
//exclude: /node_modules/,
// 排除node_modules文件夹
use: [
'style-loader',
'css-loader',
{
loader: "postcss-loader",
options:{
indent:"postcss",
plugins:[require('autoprefixer')],
browser:['last 10 versions']
}
},
{
loader:'sass-loader',
options:{
sourceMap: true
}
}
]
}
可以在浏览器可以看到css是通过js动态创建< style >标签来写入的
因为项目大了样式会很多,都放在js里太占体积,不能做缓存;所以一般会用到extract-text-webpack-plugin 的插件来把散落在各地的css提取出来,并生成一个main.css的文件,最终在index.html里通过< link >的形式加载它
extract-text-webpack-plugin不支持webpack4.0以上版本,用miniCssExtractPlugin替换
const miniCssExtractPlugin = require('mini-css-extract-plugin')
// 创建css实例
const cssExtractPlugin = new miniCssExtractPlugin({
filename: devMode? '[name].[chunkhash:8].css':'[name].css',
chunkFilename: '[id].css'
})
···
{
test: /\.(sa|sc|c)ss$/,
include: path.resolve(__dirname, '../src'), // 限制打包范围,提高打包速度
//exclude: /node_modules/,
// 排除node_modules文件夹
use: [
miniCssExtractPlugin.loader,
'css-loader',
{
loader: "postcss-loader",
options:{
indent:"postcss",
plugins:[require('autoprefixer')],
browser:['last 10 versions']
}
},
{
loader:'sass-loader',
options:{
sourceMap: true
}
}
]
}
···
plugins: [
cssExtractPlugin,
]
- Soucemap 源代码打包后代码的映射关系,在dev模式中,默认开启,
> devtool: "cheap-module-evel-source-map",//dev环境
//devtool: "cheap-module-source-map",//pro环境
- babel编译
虽然ES6语法已经广泛普及,但各个浏览器还不是特别兼容,为了避免出错我们需要把ES6转成ES5,使用babel进行编译
安装babel-loader、babel-core、babel-preset-env、babel-polyfill
{
test: /\.js$/,
exclude: /node_modules/,
loader: "babel-loader"
}
配置babel 规则
.babelrc文件
{
"presets": [
[
"@babel/preset-env",
{
"targets": {
"node": "8.10"
},
"corejs": "3", // 声明corejs版本
"useBuiltIns": "usage"
}
]
]
}
- html-webpack-plugin
为html文件中引入的外部资源如script、link动态添加每次compile后的hash,防止引用缓存的外部文件问题。 可以生成创建html入口文件,比如单页面可以生成一个html文件入口,配置N个html-webpack-plugin可以生成N个页面入口
const htmlWebpackPlugin = require('html-webpack-plugin')
const htmlweb = new htmlWebpackPlugin({
title: "我是首页", //用来生成页面的title元素 必须在模版页面中使用模版引擎的语法 <%= htmlWebpackPlugin.options.title %>
filename: "index.html",// 输出html文件名,默认是index.html,也可以是直接配置带有子目录 不支持[name][.ext]
template: path.resolve(__dirname, '../index.html'), // 模版文件路径,支持加载器
hash:true,// 为true将添加一个唯一的webpack编译hash到所有包含脚本和css文件,对于解除cache很有用
})
···
plugins: [
cssExtractPlugin,
htmlweb,
]
- clean-webpack-plugin
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
···
plugins: [
cssExtractPlugin,
htmlweb,
new CleanWebpackPlugin()
]
- vue-loader
const VueLoaderPlugin = require('vue-loader/lib/plugin')
···
plugins: [
cssExtractPlugin,
htmlweb,
// 将定义过的其它规则复制并应用到 .vue 文件里相应语言的块
new VueLoaderPlugin(),
new CleanWebpackPlugin()
]
- 热更新HMR&webpack-dev-server
模块热替换(HMR)的作用是,在应用运行时,无需刷新页面,便能替换、增加、删除必要的模块。 HMR 对于那些由单一状态树构成的应用非常有用。因为这些应用的组件是 “dumb” (相对于 “smart”) 的,所以在组件的代码更改后,组件的状态依然能够正确反映应用的最新状态。
热更新HMR是webpack原本带有的功能
const webpack = require('webpack')
···
devServer:{
contentBase: path.resolve(__dirname,"../dist"), //资源文件目录
open:true,//自动打开游览器
port: 8081,//服务器端口号
hot: true,//开启热更新
host:'192.168.1.4',
hotOnly:true,
// proxy:{
// '/api': {
// target: "http://localhost:9092/"
// }
// }
},
plugins: [
new webpack.HotModuleReplacementPlugin(),
new webpack.NamedModulesPlugin(), // HMR shows correct file names in console on update.
]
- webpack-merge 合并webpack配置文件
修改webpack.base.conf.js 提取公共部分
const merge = require('webpack-merge')
const devConfig = require('./webpack.dev.conf')
const proConfig = require('./webpack.pro.conf')
let devMode = process.env.http_env !== 'development'
function resolve (dir) {
return path.join(__dirname, '..', dir)
}
···
// 基于环境变量,通过注入变量自动组合
module.exports = (env) => {
if(env && env.production){
return merge(baseConfig, proConfig)
}else{
return merge(baseConfig, devConfig)
}
}
修改webpack.dev.conf.js
// 引入插件
const webpack = require('webpack')
const path = require('path')
const devConfig= {
mode: 'development', // 模式 development | production
//入口配置 string | Array | Object
devtool: "cheap-module-evel-source-map",//dev环境
devServer:{
contentBase: path.resolve(__dirname,"../dist"), //资源文件目录
open:true,//自动打开游览器
port: 8081,//服务器端口号
hot: true,//开启热更新
host:'192.168.1.4',
hotOnly:true,
// proxy:{
// '/api': {
// target: "http://localhost:9092/"
// }
// }
},
plugins: [
new webpack.HotModuleReplacementPlugin(),
new webpack.NamedModulesPlugin(), // HMR shows correct file names in console on update.
]
}
module.exports = devConfig
修改webpack.pro.conf.js
const webpack = require('webpack')
const proConfig= {
mode: 'production', // 模式 development | production
devtool: "cheap-module-source-map",//pro环境
plugins: [
new webpack.DefinePlugin({
'process.env': {
'http_env': JSON.stringify(process.env.http_env)
}
})
]
}
module.exports = proConfig
修改package.json
cross-env能跨平台地设置及使用环境变量
大多数情况下,在windows平台下使用类似于: NODE_ENV=production的命令行指令会卡住,windows平台与POSIX在使用命令行时有许多区别(例如在POSIX,使用$ENV_VAR,在windows,使用%ENV_VAR%。。。)
cross-env让这一切变得简单,不同平台使用唯一指令,无需担心跨平台问题
"scripts": {
"build": "cross-env http_env=production webpack -env.production --mode=production --config build/webpack.base.conf.js ",
"dev": "webpack-dev-server --config build/webpack.base.conf.js",
"start": "npm run dev"
},
- optimize-css-assets-webpack-plugin
修改webpack.pro.conf.js
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin')
plugins: [
new OptimizeCSSAssetsPlugin(), //压缩css
]
- uglifyjs-webpack-plugin
修改webpack.pro.conf.js
const uglifyjs = require('uglifyjs-webpack-plugin');
plugins: [
new uglifyjs(), //压缩js
]
完整代码
- webpack.base.conf.js
const path = require('path')
const miniCssExtractPlugin = require('mini-css-extract-plugin')
const htmlWebpackPlugin = require('html-webpack-plugin')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const VueLoaderPlugin = require('vue-loader/lib/plugin')
const merge = require('webpack-merge')
const devConfig = require('./webpack.dev.conf')
const proConfig = require('./webpack.pro.conf')
let devMode = process.env.http_env !== 'development'
console.log(devMode)
function resolve (dir) {
return path.join(__dirname, '..', dir)
}
// 创建多个实例
const cssExtractPlugin = new miniCssExtractPlugin({
filename: devMode? '[name].[chunkhash:8].css':'[name].css',
chunkFilename: '[id].css'
})
const htmlweb = new htmlWebpackPlugin({
title: "我是首页", //用来生成页面的title元素 必须在模版页面中使用模版引擎的语法 <%= htmlWebpackPlugin.options.title %>
filename: "index.html",// 输出html文件名,默认是index.html,也可以是直接配置带有子目录 不支持[name][.ext]
template: path.resolve(__dirname, '../index.html'), // 模版文件路径,支持加载器
hash:true,// 为true将添加一个唯一的webpack编译hash到所有包含脚本和css文件,对于解除cache很有用
})
const baseConfig = {
entry:{
index: "./src/main.js" //main 打包后的名称
},
// 出口配置, 必须是绝对路径
output:{
path: path.resolve(__dirname, "../dist"),
filename: "[name].js", //[]占位符
// publicPath: "http://www/cdn.com" //打包后的文件已publicPath做前缀
},
resolve: {
extensions: ['.js', '.vue', '.json'],
alias: {
'vue$': 'vue/dist/vue.esm.js',
'@': resolve('src'),
'common': resolve('src/common'),
'components': resolve('src/components')
}
},
optimization: {
splitChunks:{ // 代码拆分
chunks: "all", //对同步 initial 异步 async 所有的模块有效 all
// minSize: 30000, //最小尺寸
// maxSize:0,//对模块进行二次分割时使用,不推荐使用
// minChunks:1,//打包生产的chunk文件最少有几个chunk引用了这个模块 如果值为2了,就会被分割
// maxAsyncRequests: 3,//最大初始化请求书,入口文件同步请求,默认3
// automaticNameDelimiter: '-',//打包分割符号
// name:true,//打包后的名称,除了布尔值,还可以接收一个函数function
// cacheGroup:{
// vendors:{
// test:/[\\/]node_modules[\\/]/,
// name: 'vendor',//要缓存的 分割出来 chunk名字
// priority: -10 //缓存组优先级 数字越大优先级越高
// },
// other:{ //只支持同步的引用
// chunks:'initial',//必须三选一:initial | all | async默认值
// test: /vue|loadsh/, //正则规则验证,如果符合就提取chunk
// name:"other",
// minSize: 30000,
// minChunks: 1,
// },
// default:{
// minChunks: 2,
// priority: -20,
// reuseExistingChunk: true //可设置是否重用chunk
// }
// }
},
usedExports: true,
},
module:{// 处理模块
rules:[
{
// 识别.vue文件
test: /\.vue$/,
loader: 'vue-loader',
},
{
test: /\.(sa|sc|c)ss$/,
include: path.resolve(__dirname, '../src'), // 限制打包范围,提高打包速度
//exclude: /node_modules/, // 排除node_modules文件夹
use: [
devMode ? miniCssExtractPlugin.loader:'style-loader',
'css-loader',
{
loader: "postcss-loader",
options:{
indent:"postcss",
plugins:[require('autoprefixer')],
browser:['last 10 versions']
}
},
{
loader:'sass-loader',
options:{
sourceMap: true
}
}
]
},
{
test: /\.js$/,
exclude: /node_modules/,
include: path.resolve(__dirname, '../src'),
loader: "babel-loader",
},
{
test:/\.(png|gif|jpe?g)/, // ?e可有可无
use: {
loader: "url-loader",
options:{
name:"[name]_[hash].[ext]", //[hash]解决缓存,[ext]保留旧文件的文件后缀
limit:500, //是把小于500B的文件打成Base64的格式,写入JS
outputPath:"image/" //图片放到image/目录下
}
}
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
loader: 'url-loader',
options: {
// 文件大小小于limit参数,url-loader将会把文件转为DataUR
limit: 10000,
name: '[name]-[hash:5].[ext]',
output: 'fonts/'
}
}
]
},
plugins: [
cssExtractPlugin,
htmlweb,
// 将定义过的其它规则复制并应用到 .vue 文件里相应语言的块
new VueLoaderPlugin(),
new CleanWebpackPlugin()
]
}
// 基于环境变量,通过注入变量自动组合
module.exports = (env) => {
if(env && env.production){
return merge(baseConfig, proConfig)
}else{
return merge(baseConfig, devConfig)
}
}
- webpack.dev.conf.js
// 引入插件
const webpack = require('webpack')
const path = require('path')
const devConfig= {
mode: 'development', // 模式 development | production
//入口配置 string | Array | Object
devtool: "cheap-module-evel-source-map",//dev环境
devServer:{
contentBase: path.resolve(__dirname,"../dist"), //资源文件目录
open:true,//自动打开游览器
port: 8081,//服务器端口号
hot: true,//开启热更新
host:'192.168.1.4',
hotOnly:true,
// proxy:{
// '/api': {
// target: "http://localhost:9092/"
// }
// }
},
plugins: [
new webpack.HotModuleReplacementPlugin(),
new webpack.NamedModulesPlugin(), // HMR shows correct file names in console on update.
]
}
module.exports = devConfig
- webpack.pro.conf.js
const webpack = require('webpack')
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin')
const uglifyjs = require('uglifyjs-webpack-plugin');
const proConfig= {
mode: 'production', // 模式 development | production
devtool: "cheap-module-source-map",//pro环境
plugins: [
new OptimizeCSSAssetsPlugin(), //压缩css
new uglifyjs(), //压缩js
new webpack.DefinePlugin({
'process.env': {
'http_env': JSON.stringify(process.env.http_env)
}
})
]
}
module.exports = proConfig
package.json
"scripts": {
"build": "cross-env http_env=production webpack -env.production --mode=production --config build/webpack.base.conf.js ",
"dev": "webpack-dev-server --config build/webpack.base.conf.js",
"start": "npm run dev"
}
学习总结
通过这次webpack的深入学习,了解了vue-cli的webpack配置,如何通过webpack搭建vue项目。这是vue-cli最简单的项目基本配置,后续还可以优化配置