最近为了能够写一份值得参考的webpack文档,特意的去查了好多相应的书籍,博客。距离上次写的那篇文章好想也过去将近一周的时间了。我想是时候要准备下一篇文章了。不然就食言而肥了。 算了,技术类文章就直接从技术类开始说起吧。首先,学习webpack呢?是因为我在开发vue和react的时候遇到了这个工具,然后最近在看人家的招聘要求的时候,总是会带上这个。
然后我就趁着下班时间研究了一下这个东西。还是和之前的说法一样,如果有任何的疑惑,请在留言区留言,如果我能看见一定会及时的向您反馈。 新弄的github代码地址 ###1、新语言的诞生背景 ![]()
近些年来web应用的功能需求的完善和所设计到领域越发宽广导致前端萌生了很多新的思想和框架。我简单的介绍一下: 首先行业的领导着提出 模块化 的思想,他们认为将一个复杂的系统分为多个模块来开发会大大减少开发难度和提升开发效率。同时考虑到css只能用静态的语法描述元素的样式,无法像写javascript那样增加逻辑判断与共享变量。于是在这种大环境下,诞生了 es6 、 typescript 、 scss 等新的语言。但是考虑到例如es6无法在浏览器中直接运行,需要将es6转换成es5之后浏览器才能识别。于是新的构建工具就出现了。 构建工具的功能主要是:进行代码转换、文件压缩、代码分割、模块合并、自动刷新、代码检验、自动发布等功能。构建其实是工程化、自动化思想在前端开发的体验,我们要做的其实是用代码去让前端项目自动化的执行这一系列化复杂的流程罢了。 ![]()
说到构建工具,我看百度上有很多。例如npm script、grunt、gulp、fis3、webpack等等。因为本文主要是讲webpack,如果有机会能接触到以上的几个工具,我会另外详细的描述。 ###2、 webpack的优势 ![]()
从我这些天无论是自己的实践还是书上写的来说,我认为webpack本身就是特别符合模块化开发的一种工具。在webpack里面一切文件都是模块,并通过loader转换文件,通过plugin注入钩子,最后输出由多模块组合成的文件。其优点主要是: 1、 专注于处理模块化开发的项目,能做到开箱即用,一步到位 2、 能通过plugin扩展 3、 应用各种领域 4、 社区庞大 5、 良好的开发体验 但谈及为什么要选用webpack,我看书上主要有以下几个看法: 1、大多数团队在开发新项目的时候都会采用紧跟时代的技术,这些技术基本都会采用“模块化+新语言+新框架”,webpack可以为这些新项目提供一站式的解决方案 2、webpack有良好的生态链和维护团队,能提供一定的开发体验并保证质量 3、webpack被全世界大量的web开发者使用和验证,能找到各个层次面所需的教程和经验分享 (反正,综上所述 嗯 你再不学webpack就out了😂) ###3、 webpack的安装(需要node环境滴) ![]()
-
初始化项目
npm init 复制代码
-
全局安装
// 最新版本好像变成了webpack-cli注意一下 npm install -g webpack 复制代码
-
项目内安装
npm install webpack --save-dev 复制代码
在这里扯一句闲话,可能很多教程谈到安装webpack都会让你选择直接-g,但是我并不推荐你这么做,我总感觉-g之后就成了全局变量,但是我并不是每个项目内都能用到这个所谓的webpack,将webpack的作用域设置为项目内,功能与全局没有差别。 ###4、webpack的基础使用 如下图所示,在上面新建的项目下面新建index.js.。然后敲一些简单的js代码:
// 打包代码
webpack-cli index.js --output build/bundle.js --mode development
复制代码
打包成功之后就会发现目录下面多了一个build目录,里面有bundle.js文件,最后新建index,html,并引用该js,就会看到如下的效果
cnpm install webpack-dev-server --save
复制代码
// 两步
webpack-cli
webpack-dev-server
复制代码
cnpm install html-webpack-plugin --save
复制代码
然后加入配置项
- index.js
'use strict' // 严格模式
// Template version: 1.2.7
// see http://vuejs-templates.github.io/webpack for documentation.
const config = require('./config') // 导入config文件
const path = require('path') //使用Node自带的文件路径插件
module.exports = {
// 开发环境
dev: {
// Paths
assetsSubDirectory: 'static', // 编译输出的二级目录
assetsPublicPath: '/', // 编译发布上线路径的根目录,可配置为资源服务器域名或 CDN 域名
proxyTable: {
}, // 需要 proxyTable 代理的接口(可跨域),详情请看之前的文章
// Various Dev Server settings
host: '0.0.0.0', // host,如果设置成0.0.0.0可以通过统一局域网其他设备通过ip访问该网页
port: 8080, // 网页默认端口号,如果端口被占用会自动分配一个随即未被占有的端口
autoOpenBrowser: false, // 是否自动打开浏览器
errorOverlay: true, // 在浏览器是否展示错误蒙层
notifyOnErrors: true, // 是否展示错误的通知
// 这个是webpack-dev-servr的watchOptions的一个选项,指定webpack检查文件的方式
// 因为webpack使用文件系统去获取文件改变的通知。在有些情况下,这个可能不起作用。例如,当使用NFC的时候,
// vagrant也会在这方面存在很多问题,在这些情况下,使用poll选项(以轮询的方式去检查文件是否改变)可以设定为true
// 或者具体的数值,指定文件查询的具体周期。
poll: false,
// Use Eslint Loader?
useEslint: true,// eslint代码检查
showEslintErrorsInOverlay: false, // 如果设置为true,在浏览器中,eslint的错误和警告会以蒙层的方式展现。
/**
* Source Maps
*/
// https://webpack.js.org/configuration/devtool/#development
devtool: 'eval-source-map', // 调试工具
cacheBusting: true, // 指定是否通过在文件名称后面添加一个查询字符串来创建source map的缓存
cssSourceMap: false, // 是否开启 cssSourceMap
},
// 正式环境
build: {
// Template for index.html
index: path.resolve(__dirname, '../dist/index.html'), // 编译注入的 index.html 文件,必须是本地的绝对路径
// Paths
assetsRoot: path.resolve(__dirname, '../dist'), // 编译输出的静态资源根路径
assetsSubDirectory: 'static', // 编译输出的二级目录
assetsPublicPath: config.getOutPutPath(), // 编译发布上线路径的根目录,可配置为资源服务器域名或 CDN 域名
// assetsPublicPath: './',
/**
* Source Maps
*/
productionSourceMap: true, //生成用于生产构建的源映射
devtool: '#source-map', // 调试代码的模式,共有7种,这里是生成source-map文件
productionGzip: false, // 是否开启 gzip
productionGzipExtensions: ['js', 'css'], // 需要使用 gzip 压缩的文件扩展名
// 一个实用工具,用于分析项目的依赖关系
// 如果这个选项是true的话,那么则会在build后,会在浏览器中生成一份bundler报告
bundleAnalyzerReport: process.env.npm_config_report
}
}
复制代码
- utils.js
const path = require('path') // 引入nodejs的path模块,用于操作路径
const config = require('../config') // 引入模板的配置文件,下面就需要去这个文件中看看有什么基本的配置
const ExtractTextPlugin = require('extract-text-webpack-plugin') // 提取特定文件的插件,比如把css文件提取到一个文件中去
const packageConfig = require('../package.json') // 加载package.json文件
// 生成编译输出的二级目录
exports.assetsPath = function (_path) {
const assetsSubDirectory = process.env.NODE_ENV === 'production'
? config.build.assetsSubDirectory
: config.dev.assetsSubDirectory
// path.posix是path模块跨平台的实现(不同平台的路径表示是不一样的)
return path.posix.join(assetsSubDirectory, _path)
}
// 为不同的css预处理器提供一个统一的生成方式,也就是统一处理各种css类型的打包问题。
// 这个是为在vue文件中的style中使用的css类型
exports.cssLoaders = function (options) {
options = options || {}
// 打包css模块
const cssLoader = {
loader: 'css-loader',
options: {
sourceMap: options.sourceMap
}
}
// 编译postcss模块
const postcssLoader = {
// 使用postcss-loader来打包postcss模块
loader: 'postcss-loader',
// 配置source map
options: {
sourceMap: options.sourceMap
}
}
// 创建loader加载器字符串,结合extract text插件使用
/**
*
* loader:loader的名称
* loaderOptions:loader对应的options配置对象
*/
function generateLoaders (loader, loaderOptions) {
// 通过usePostCSS 来标明是否使用了postcss
const loaders = options.usePostCSS ? [cssLoader, postcssLoader] : [cssLoader]
// 如果指定了具体的loader的名称
if (loader) {
// 向loaders的数组中添加该loader对应的加载器
// 一个很重要的地方就是,一个数组中的loader加载器,是从右向左执行的。
loaders.push({
// loader加载器的名称
loader: loader + '-loader',
// 对应的加载器的配置对象
options: Object.assign({}, loaderOptions, {
sourceMap: options.sourceMap
})
})
}
// 如果明确指定了需要提取静态文件,则使用
// ExtractTextPlugin.extract({})来包裹我们的各种css处理器。
if (options.extract) {
return ExtractTextPlugin.extract({
use: loaders,
// fallback这个选项我们可以这样理解
// webpack默认会按照loaders中的加载器从右向左调用编译各种css类型文件。如果一切顺利,在loaders中的
// 各个加载器运行结束之后就会把css文件导入到规定的文件中去,如果不顺利,则继续使用vue-style-loader来处理
// css文件
fallback: 'vue-style-loader'
})
} else {
// 如果没有提取行为,则最后再使用vue-style-loader处理css
return ['vue-style-loader'].concat(loaders)
}
}
// https://vue-loader.vuejs.org/en/configurations/extract-css.html
return {
css: generateLoaders(),
postcss: generateLoaders(),
less: generateLoaders('less'),
sass: generateLoaders('sass', { indentedSyntax: true }),
scss: generateLoaders('sass'),
stylus: generateLoaders('stylus'),
styl: generateLoaders('stylus')
}
}
// 使用这个函数,为那些独立的style文件创建加载器配置。
exports.styleLoaders = function (options) {
// 保存加载器配置的变量
const output = []
// 获取所有css文件类型的loaders
const loaders = exports.cssLoaders(options)
for (const extension in loaders) {
const loader = loaders[extension]
// 生成对应的loader配置
output.push({
test: new RegExp('\\.' + extension + '$'),
use: loader
})
}
return output
}
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
icon: path.join(__dirname, 'logo.png')
})
}
}
复制代码
- vue.loader.config.js
const utils = require('./utils')
const config = require('../config')
// 设置是不是生产环境
const isProduction = process.env.NODE_ENV === 'production'
// 根据不同的环境,引入不同的source map配置文件
const sourceMapEnabled = isProduction
? config.build.productionSourceMap
: config.dev.cssSourceMap
module.exports = {
// vue文件中的css loader配置
loaders: utils.cssLoaders({
sourceMap: sourceMapEnabled,
// 生产环境下就会把css文件抽取到一个独立的文件中
extract: isProduction
}),
// css source map文件的配置
cssSourceMap: sourceMapEnabled,
// css source map文件缓存控制变量
cacheBusting: config.dev.cacheBusting,
transformToRequire: {
video: ['src', 'poster'],
source: 'src',
img: 'src',
image: 'xlink:href'
}
}
复制代码
- build/webpack.base.conf.js
const path = require('path') // 使用 NodeJS 自带的文件路径插件
const utils = require('./utils') //封装了一些方法的工具
const config = require('../config') //使用 config/index.js
const vueLoaderConfig = require('./vue-loader.conf') //使用vue-loader.conf
function resolve (dir) {
return path.join(__dirname, '..', dir) // 拼接我们的工作区路径为一个绝对路径
}
// eslint的规则
const createLintingRule = () => ({
// 对.js和.vue结尾的文件进行eslint检查
test: /\.(js|vue)$/,
// 使用eslint-loader
loader: 'eslint-loader',
// enforce的值可能是pre和post。其中pre有点和webpack@1中的preLoader配置含义相似。
// post和v1中的postLoader配置含义相似。表示loader的调用时机
// 这里表示在调用其他loader之前需要先调用这个规则进行代码风格的检查
enforce: 'pre',
// 需要进行eslint检查的文件的目录存在的地方
include: [resolve('src'), resolve('test')],
// eslint-loader配置过程中需要指定的选项
options: {
// 文件风格的检查的格式化程序,这里使用的是第三方的eslint-friendly-formatter
formatter: require('eslint-friendly-formatter'),
// 是否需要eslint输出警告信息
emitWarning: !config.dev.showEslintErrorsInOverlay
}
})
var webpack = require('webpack')
module.exports = {
// webpack在寻找寻找相对路径的文件时候会以context作为根目录。
// context默认为执行启动webpack时所在的当前工作目录
context: path.resolve(__dirname, '../'),
// entry表示入口,webpack构建的第一步从entry开始
// 类型可以是string,object,array
entry: {
app: './src/main.js'
},
output: {
// 输出文件存放的目录,必须是string类型的绝对目录
path: config.build.assetsRoot,
// 通过entry不同生成不同的文件名字,详情请看文章
filename: '[name].js',
// 发布到线上所有资源的url前缀,为string类型
publicPath: process.env.NODE_ENV === 'production'
? config.build.assetsPublicPath
: config.dev.assetsPublicPath,
// 导出库的名称,为string类型,不填写的时候,默认输出格式是匿名的立即执行函数
// library: "KlivitamLibrary"
// 导出库的类型,为枚举类型,默认是var
// 可选值: umd umd2 commonjs2,commonjs,amd,this,var,assign,window,global,jsonp
// libraryTarget: "jsonp"
// 是否包含游泳的文件信息到生成的代码里
// pathinfo: true
// 附加chunk的文件名称
// chunkFilename: "[id].js"
// chunkFilename: "[chunkhash].js"
// jsonp异步加载资源时的回调函数名
// jsonpFunction: "webpackJsonP"
// 生成source map文件的名称
// sourceMapFilename: "[file].map"
//浏览器开发者工具里显示的远吗模块名称
// devtoolModuleFilenameTemplate: "webpack:///[resource-path]"
// 异步加载跨域的资源时使用的方式
// crossOriginLoading: "use-credentials",
// crossOriginLoading: "anonymous",
// crossOriginLoading: false,
},
// 配置模块解析时候的一些选项
resolve: {
// 自动补全的扩展名,能够使用户在引入模块时不带扩展
extensions: ['.js', '.vue', '.json'],
// 默认路径代理,例如 import Vue from 'vue$',会自动到 'vue/dist/vue.esm.js'中寻找
alias: {
'vue$': 'vue/dist/vue.esm.js',
'@': resolve('src'),
'components': resolve('src/components'),
// 可以在引入文件的时候使用pages符号引入src/pages文件夹中的文件
'pages': resolve('src/pages'),
'http': resolve('src/http'),
'public': resolve('src/public'),
'jquery': 'jquery'
}
},
// 下面是针对具体的模块进行的具体的配置
// 下面的配置语法采用的是version >= @2的版本
module: {
noParse: [/videojs-contrib-hls/], // 不用解析和处理的模块
// rules是一个数组,其中的每一个元素都是一个对象,这个对象是针对具体类型的文件进行的配置。
rules: [
...(config.dev.useEslint ? [createLintingRule()] : []),
{
test: /\.vue$/, // 正则匹配loader名字
loader: 'vue-loader', // loader名字
// 针对此加载器的具体配置
// 针对前面的分析,这个配置对象中包含了各种css类型文件的配置,css source map的配置 以及一些transform的配置
options: vueLoaderConfig
},
{
test: /\.js$/,
// js文件的处理主要使用的是babel-loader。在这里没有指定具体的编译规则,babel-loader会自动
// 读取根目录下面的.babelrc中的babel配置用于编译js文件
loader: 'babel-loader',
// 指定需要进行编译的文件的路径
// 这里表示只对src和test文件夹中的文件进行编译
include: [resolve('src'), resolve('test'), resolve('config/myapi')] //规则所包含的文件夹
},
{
// 对图片资源进行编译的配置
// 指定文件的类型
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
// 使用url-loader进行文件资源的编译
loader: 'url-loader',
// url-loader的配置选项
options: {
// 文件的大小小于10000字节(10kb)的时候会返回一个dataUrl
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]')
}
}
]
},
// 这些选项用于配置polyfill或mock某些node.js全局变量和模块。
// 这可以使最初为nodejs编写的代码可以在浏览器端运行
node: {
// 这个配置是一个对象,其中的每个属性都是nodejs全局变量或模块的名称
setImmediate: false,
// prevent webpack from injecting mocks to Node native modules
// that does not make sense for the client
// 设置成empty则表示提供一个空对象
dgram: 'empty',
fs: 'empty',
net: 'empty',
tls: 'empty',
child_process: 'empty'
}
}
复制代码
+build/webpack.dev.conf.js
'use strict'
// 首先引入的是一些工具方法,下面我们就需要去util文件种看一下有哪些对应的工具方法
const utils = require('./utils')
// 引入webpack模块
const webpack = require('webpack')
// 引入配置文件
// 这个配置文件中包含了一些dev和production环境的基本配置
const config = require('../config')
// 引入webpack-merge模块。这个模块用于把多个webpack配置合并成一个配置,后面的配置会覆盖前面的配置。
const merge = require('webpack-merge')
// 引入webpack的基本设置,这个设置文件包含了开发环境和生产环境的一些公共配置
const baseWebpackConfig = require('./webpack.base.conf')
// 用于生成html文件的插件
const HtmlWebpackPlugin = require('html-webpack-plugin')
// 这个插件能够更好的在终端看到webpack运行时的错误和警告等信息。可以提升开发体验
const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin')
// 查找一个未使用的端口
const portfinder = require('portfinder')
// 获取host环境变量,用于配置开发环境域名
const HOST = process.env.HOST
// 获取post环境变量,用于配置开发环境时候的端口号
const PORT = process.env.PORT && Number(process.env.PORT)
// 开发环境的完整的配置文件
const devWebpackConfig = merge(baseWebpackConfig, {
module: {
// 为那些独立的css类型文件添加loader配置(没有写在vue文件的style标签中的样式)
rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap, usePostCSS: true })
},
// 开发环境使用'eval-source-map'模式的source map
// 因为速度快
devtool: config.dev.devtool,
// 下面是对webpack-dev-server选项的基本配置,这些配置信息,我们可以在/config/index.js
// 文件中进行自定义配置。
devServer: {
// 用于配置在开发工具的控制台中显示的日志级别
// 注意这个不是对bundle的错误和警告的配置,而是对它生成之前的消息的配置
clientLogLevel: 'warning',
// 表示当使用html5的history api的时候,任意的404响应都需要被替代为index.html
historyApiFallback: true,
// 启用webpack的热替换特性
hot: true,
// 一切服务都需要使用gzip压缩
// 可以在js,css等文件的response header中发现有Content-Encoding:gzip响应头
compress: true,
// 指定使用一个 host。默认是 localhost
// 如果希望服务器外部可以访问(通过我们电脑的ip地址和端口号访问我们的应用)
// 可以指定0.0.0.0,使用这个可以使得同一局域网内所有设备都能访问你的本地网页
host: HOST || config.dev.host,
// 指定要监听请求的端口号
port: PORT || config.dev.port,
// 是否自动打开浏览器
open: config.dev.autoOpenBrowser,
// 当编译出现错误的时候,是否希望在浏览器中展示一个全屏的蒙层来展示错误信息
overlay: config.dev.errorOverlay
// 表示只显示错误信息而不显示警告信息
// 如果两者都希望显示,则把这两项都设置为true
? { warnings: false, errors: true }
// 设置为false则表示啥都不显示
: false,
// 指定webpack-dev-server的根目录,这个目录下的所有的文件都是能直接通过浏览器访问的
// 推荐和output.publicPath设置为一致
publicPath: config.dev.assetsPublicPath,
// 配置代理,这样我们就可以跨域访问某些接口
// 我们访问的接口,如果符合这个选项的配置,就会通过代理服务器转发我们的请求
proxy: config.dev.proxyTable,
// 启用 quiet 后,除了初始启动信息之外的任何内容都不会被打印到控制台。这也意味着来自 webpack 的错误或警告在控制台不可见。
quiet: true, // necessary for FriendlyErrorsPlugin
// 与监视文件相关的控制选项
watchOptions: {
// 如果这个选项为true,会以轮询的方式检查我们的文件的变动,效率不好
poll: config.dev.poll,
}
},
plugins: [
// 创建一个在编译时可以配置的全局变量
new webpack.DefinePlugin({
'process.env': require('../config/dev.env')
}),
// 启用热替换模块
// 记住,我们永远不要再生产环境中使用hmr
new webpack.HotModuleReplacementPlugin(),
// 这个插件的主要作用就是在热加载的时候直接返回更新文件的名称,而不是文件的id
new webpack.NamedModulesPlugin(), // HMR shows correct file names in console on update.
// 使用这个插件可以在编译出错的时候来跳过输出阶段,这样可以确保输出资源不会包含错误。
new webpack.NoEmitOnErrorsPlugin(),
// 这个插件主要是生成一个html文件
new HtmlWebpackPlugin({
// 生成的html文件的名称
filename: 'index.html',
// 使用的模板的名称
template: 'index.html',
// 将所有的静态文件都插入到body文件的末尾
inject: true
}),
]
})
module.exports = new Promise((resolve, reject) => {
portfinder.basePort = process.env.PORT || config.dev.port
// 这种获取port的方式会返回一个promise
portfinder.getPort((err, port) => {
if (err) {
reject(err)
} else {
// 把获取到的端口号设置为环境变量PORT的值
process.env.PORT = port
// 重新设置webpack-dev-server的端口的值
devWebpackConfig.devServer.port = port
// 将FriendlyErrorsPlugin添加到webpack的配置文件中
devWebpackConfig.plugins.push(new FriendlyErrorsPlugin({
// 编译成功时候的输出信息
compilationSuccessInfo: {
messages: [`Your application is running here: http://${devWebpackConfig.devServer.host}:${port}`],
},
// 当编译出错的时候,根据config.dev.notifyOnErrors来确定是否需要在桌面右上角显示错误通知框
onErrors: config.dev.notifyOnErrors
? utils.createNotifierCallback()
: undefined
}))
// resolve我们的配置文件
resolve(devWebpackConfig)
}
})
})
复制代码
- webpack.prod.conf.js
// 引入path模块
const path = require('path')
// 引入工具方法
const utils = require('./utils')
// 引入webpack模块
const webpack = require('webpack')
// 引入基本的配置
const config = require('../config')
// 引入webpack-merge模块
const merge = require('webpack-merge')
// 引入开发环境和生产环境公共的配置
const baseWebpackConfig = require('./webpack.base.conf')
// 这个模块主要用于在webpack中拷贝文件和文件夹
const CopyWebpackPlugin = require('copy-webpack-plugin')
// 这个插件主要是用于基于模版生成html文件的
const HtmlWebpackPlugin = require('html-webpack-plugin')
// 这个插件主要是用于将入口中所有的chunk,移到独立的分离的css文件中
const ExtractTextPlugin = require('extract-text-webpack-plugin')
// 这个插件主要是用于压缩css模块的
const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin'