1. webpack是什么?
一个现代的javascript应用程序的静态模块打包器。当webpack处理应用程序时,会根据入口文件等引入的地方形成一个依赖关系图,其中包括应用程序需要的每个模块,然后将这些模块打包成一个或多个bundle。
官方图形象的描述了不同的格式资源通过webpack最终打包成常用的浏览器可识别的资源。
2. webpack的作用?
2.1 模块化能简单引入并使用某个被依赖的模块的功能
2.2 处理打包javascript的文件
2.3 可以将ES6转化为浏览器可识别和兼容的语法
2.4 通过loader转化后可打包处理非javascript资源文件
2.5 能提取共用的模块化功能打包后产生独立的一个或多个文件
2.6 搭建开发环境开启本地服务器、监视文件改动,热替换
2.7 合理的长效缓存等...
3. webpack主要的配置?
3.1 entry
用来告诉webpack应该使用哪个模块,来作为构建依赖图的起点
module.exports = {
// 单个入口文件时,如果未命名则默认打包后名为main.js
entry: './src/index.js'
// 打包生成 app.js
entry:{
app : './src/index.js'
}
// 多入口路径文件 打包后会生成app.js和main.js
entry:{
app: './src/index.js',
main: './src/main.js'
}
}
3.2 output
用于指示webpack打包后文件存放的路径和文件命名,设置静态资源前缀路径等
const path = require('path');
module.exports = {
entry:{
app:'./src/index.js'
},
output:{
path: path.resolve(__dirname,'dist'),
filename: 'js/[name].[contenthash:8].js',
/*
* 可以理解为配置的基础路径
* 打包后之前的源码资源会根据这个配置生成新的访问路径
* 比如'/app.js' => https://cfile.xiaoman.cn/app.js
*/
publicPath:'https://cfile.xiaoman.cn'
}
/* 这个module部分会描述**
module: {
rules: [
{
test: /\.vue$/,
use: [{
loader:'vue-loader'
}]
}
]
}
示例demo的目录结构和其他文件
根据上面配置现在所有的资源全部打包在app.js这个文件中,详情如下图
随着不停的迭代项目会越来越大,如果将全部文件打包到同一个文件意味着用户仅浏览一个页面要下载所有页面的资源(针对单页面应用),这种方式大大的降低首屏渲染速度。那么减少首次资源加载和不必要资源的加载是必不可少的。
则动态按需加载或者共用部分不要重复打包到每个使用到的文件中,通过将公用部分资源分离似乎可以解决这个问题。
const path = require('path');
module.exports = {
entry:{
app:'./src/index.js'
},
output:{
path: path.resolve(__dirname,'dist'),
filename: 'js/[name].[contenthash:8].js',
chunkFilename: 'js/[name].[contenthash:8].js',
publicPath:'https://cfile.xiaoman.cn'
},
/* 这个module部分会描述**/
module: {
rules: [
{
test: /\.vue$/,
use: [{
loader:'vue-loader'
}]
}
]
}
根据上面的配置,在其他文件异步加载某个文件,打包情况如下,将异步加载的文件单独形成一个文件。这时候app.js的字节数降低了,并且只有用户需要加载这个资源才会立即被加载
1.非命名式动态加载文件
以上配置即可完成
更改后的index.js文件(将路由改为动态加载)
可以看到打包后单独生成了0.2711cda5.js文件
2.命名式动态加载文件
这种比较清晰知道文件的来源,但是每个文件这样命名较为麻烦
/*
* 在map.json文件中我随便导出了一些json数据,仅供打包演示
* 以下是util.js文件的代码
*/
export const getMap = ()=> import(
/* webpackChunkName: "async-database-map"*/
'./map.json').then(module => { return module.default
})
这是目录结构和Add.vue文件的代码
打包之后就多了一个async-database-map.3a0b70bc.js文件
3.3 module
现在在Add.vue中加上样式。开始打包...
发现webpack会报错,大概意思说它不认识css,需要使用loader转化一下从而让wepback可以处理css
那么我们知道了module主要是把每个需要转化的类型的模块使用loader进行特有的处理。比如webpack本身不支持处理非javacript类型的文件。则可以通过vue-loader、css-loader转化为webpack可以处理的文件。这样就可以写vue、css文件。当然字体、图片等都可以,需要在moudle中进行另外配置即可。
/*
* webpack.config.js 文件,其他配置同上
* 仅展示module配置
*/
module.exports = {
module: {
// 不需要解析的文件名或路径
noParse: \[/iconfont.js/\],
// 解析文件的规则
rules:[
{
test: /\.vue$/,
use: ['vue-loader']
},
{
test:'/\.js$/', // 被解析的文件类型
ues: [
/*
* 将es6语法可以转话成浏览器可识别的es5语法
* 在根目录下添加 babel.config.js并配置如何转换即可
* 如果用了babel-loader webpack默认会使用
* babel.config.js作为babel-loader的配置选项
* 当然还有其他方式比如.babelrc,详见[babel官网](https://www.babeljs.cn/docs/configuration)
*/
'babel-loader',
'thread-loader'
],
include: [
/* 需包括的非当前webpack配置所在的根目录下的路径
* 并非一个大项目的根目录,因为一个项目下可能有多个
* 子项目
*/
resolve('common/js')
],
// 不包括的需解析转换的目录或文件
exclude: [
/node_modules\/(?!amumu)/
]
},
{
test: /\.css$/,
use: ['style-loader','css-loader']
}
]
}
}
配置之后打包成功,并且es6也被babel-loader转成浏览器可识别的es5语法
3.4 resolve
模块解析,主要用于帮忙找到文件的依赖模块的绝对路径,使其能够被正确解析。
/*
* webpack.config.js 文件,其他配置同上
* 仅展示resovle配置
*/
module.exports = {
resolve:{
/*
* 表示js、vue、json后缀的文件再被引入到其他文件时可省略后缀
* 名也可以找到,如不写在引入时不能省略后缀名
*/
extensions:['.js', '.vue', '.json'],
/*
* 别名,import中的@crm在打包时将自动换成配置的别名路径
*/
alias: {
'@root':resolveRoot('./'),
'@crm':resolveRoot('crm/src'),
}
}
}
3.5 externals
由于某些包不需要打包到bundle中,而是在运行时再去从外部获取这些扩展的资源。比如通过cdn路径获取jQuery,就无需将jQuery打包到bundle中了。
// 在index.html中 通过cdn去引入jQuery
<script
src="https://code.jquery.com/jquery-3.1.0.js"
crossorigin="anonymous">
</script>
// webpack.config.js
module.configs = {
exterals: {
jquery: jQuery
}
}
上面的则表示在模块通过import依赖jquery时,jQuery会被当做全局变量去替换被使用到的地方。
当然在我们现有的系统中,并未通过cdn的形式去直接引入exterals中配置的外部依赖。而是通过gulp将某些配置的基础依赖包打包在vendor.base.js(见gulpfile.js)中,并直接在index.html中引入。
3.6 optimization
webpack 4新的项目打包优化配置属性,有些属性是用来替代webpack.
optimize.xx下的某些插件。
/*
* webpack.config.js 文件,其他配置同上
* 仅展示optimization配置
*/
module.exports = {
optimization: {
/*
* 告诉webpack使用TerserPlugin或者在minimizer属性中的插件
* 来最小化 要输出的bundle
* 默认只处理 js 文件
*/
minimize: false
/* 配置通过那个插件和如何最小化bundle */
minimizer:[new TerserPlugin()]
/*
* 将运行时每个模块之间的映射关系单独分离成一个文件
* 主要记录了在运行时模块之间的运行和映射关系
* [关于runtime与manifest官方解析](https://www.webpackjs.com/concepts/manifest/)
*/
runtimeChunk:{
name: 'runtime'
},
/*
* 分离打包模块
* 将满足test的文件单独分离成一个文件或多个文件,不与入口文件
* app.js 打包在一起
*/
splitChunks: {
cacheGroups: {
vendor: {
chunks:'initial',
name:'vendor',
priority:0,
test: /\/node\_modules\//,
enforce:true,
},
styles:{
name: 'styles',
test: /\.css$/,
chunks: 'all',
enforce: true,
}
}
}
这里就清晰的记录了,node_modules模块和styles与app.js分离,自身单独生成文件,又大大的减少了app.js的字节数,加快首屏渲染时间。
如果没有分离,则样式更新或者vue中的其他部分更新都会使得整个文件的缓存失效。分离后css和逻辑js部分完全独立,更改css不会使js部分失效,同理亦然。这样更好的利用了缓存机制,减少不必要未更新部分的下载。
3.7 plugins
用于自定义webpack的构建过程。webpack有内置插件,可以通过webpack.[plugin-name]使用这些插件。当然npm社区有更多的插件。
// webpack.config.js 文件,其他配置同上
module.exports = {
plguins: [
/*
* 在全局中定义某些变量,使得在项目任意处可访问自定义的变量
*/
new webpack.DefinePlugin({
'process.env':{
PROJECT_NAME:'"crm"',
NODE_ENV:'"production"',
ENV:'"production"'
},
}),
]
}
3.8 webpack-dev-server
开发时创建本地服务器,可通过配置的网址和端口号进行访问。通过webpack-dev-server生成的bundle运行于项目根目录,这个文件并没有存到物理磁盘上,而是在内存中。当我们更改某些文件内容时会自动编译出最新的结果,能够更快的看到效果,加快开发速率。
安装 webpack-dev-server 依赖, 在package.json中配置start脚本
// webpack.config.js 文件,其他配置同上
module.exports = {
devServer:{
host: 'http://localhost:8081',
port: 8001,
// 开启热更新
hot: true,
/*
* 将dist目录的文件作为可访问文件。一般想要提供静态文件时才需要
*/
contentBase: './dist',
/*
* 绑定特定的基础目录
* 比如访问某个bundle.js
* 其完整路径应是 http://localhost:8081/assets/bundle.js
*/
publicPath: '/assets',
}
}
运行后输出如下信息,直接在浏览器即可访问到
4 常见的插件的作用
1.VueLoaderPlugin
专门用于对vue文件的打包,虽然在module中对vue文件使用了vue-loader, 还需要手动在插件处调用才行。可以自己定义参数,但最新的参数中已经废弃了在vue插件中定义css样式的加载器。
module.exports = {
plguins: [
new webpack.DefinePlugin({'同上省略'}),
new VueLoaderPlugin(),
]
}
2.AssetsPlugin
生成webpack-assets.json文件,记录用到css和js的文件名,用来做缓存破坏。
可以自定义配置生成路径、文件名或只记录入口文件css和js文件名、元信息等
module.exports = {
plugins:[
new AssetsPlugin({
/* 是否输入到配置的编译路径处 */
useCompilerPath:true,
/* 输出文件中只包含入口文件及其关联块用到的css和js文件名 */
entrypoints:true,
/* 为true时只在webpack-dev-server运行时在内存中生成,不写入磁盘 */
keepInMemory:process.env.NODE_ENV!=='production',
metadata: {
build: {
buildTime:format(newDate(), 'yyyy-MM-dd HH:mm:ss'),
},
},
})
]
}
3.webpack.DefinePlugin
上面描述了(详见3.4),用来定义全局变量,使得在项目中任意文件可使用这个变量名
4.webpack.NamedModulesPlugin
给入口文件和已命名的异步打包文件之外的chunk进行命名。比如路由中动态加载的文件。
constroutes = [
{ path:'add', component: ()=>import('./Add.vue')},
]
如果不使用这个插件webpack会自动命名为 [id].[contenthash:8]
(根据上面的chunkFilename的配置会生成hash后缀).
另外根据自己的需求命名文件
module.exports = {
plugins: [
new webpack.NamedChunksPlugin(chunk => {
if (chunk.name) {
returnchunk.name
}
consthash = require('hash-sum')
const joinedHash=hash(Array.from(chunk.modulesIterable, m =>m.id).join('_'))
return `chunk-`+joinedHash
})
]
}
可与3.2中生成的bunlde进行对比可发现以0.2711cda5.js命名的文件变为
chunk-226123c6.eef091ca.js了
5.HtmlWebpackPlugin
因为项目都有一个入口的html文件,这个文件会负责引入并加载js/img等等静态资源从而显示到浏览器。但我们打包的js名是由文件名+hash组成。如果在html中引入的js名能自动根据编译结果进行变化就省事不少。这个插件正好解决此问题。
module.exports = {
plugins: [
/* 默认会输出到配置的output路径下 */
new HtmlWebpackPlugin({
// 生成的文件名
filename:'index.html',
// 以哪个文件为模板来进入js等打包文件的修改和注入
template:'index.html',
})
]
}
打包的bundle截图
生成的index.html自动引入了webpack打包的入口js文件,无需每次打包完成后手动去引入
6.inlineManifestWebpackPlugin
这个插件的作用是把单独打包的关于记录manifest信息的js文件(默认名:runtime.js)的内容以内联方式放在入口html(默认名:index.html)中。因为index.html每次打包都会变动,记录manifest的文件每次也会变动则这个文件的缓存是没用的。如果记录manifest的文件的内容内联到index.html就可以减少一个请求。更好的进行长效缓存。
module.exports = {
optimization:{
runtimeChunk:{
name:'runtime',
}
},
plugins:[
new HtmlWebpackPlugin({
filename:'index.html',
template:'index.html',
}),
/*
* 1.首先要把记录manifest信息的文件单独打包
* 2. 搭配HtmlWebpackPlugin一起使用,并在后面配置
* 3. 传入的名字与 optimization中配置的分离manifest文件名一致,默认是runtime
*/
new InlineManifestWebpackPlugin('runtime')
]
}
7.CopyWebpackPlugin
顾名思义,拷贝文件。 将某些文件从一个地方拷贝到另外的地方。
module.exports = {
plguins: [
new webpack.DefinePlugin({'同上省略'}),
new VueLoaderPlugin(),
new CopyWebpackPlugin([
{
/* 具体根据自己的项目目录 */
from:'../static',
to:'./dist/static',
ignore: ['.*'],
},
]),
]
}
8.MiniCssExtractPlugin
将样式从内嵌style的文件中分离出来并形成单独的文件。比如xx.vue文件中有些样式,就能把这部分样式抽出来。这样做,如果xx.vue中的非样式部分修改,不会使得样式文件的缓存实现。各自独立,不会相互影响,可减少请求http资源的数量。
这里配置遇到一个坑,在mode设置为'development'分离完全没问题,改为'production',分离之后的css文件就不见了。经过调查该插件有把css分离出来,只是webpack在tree sharking时认为无副作用就将生成的css摇了。
因为最新生成的package.json中包含了 sideEffects: false这个属性,即认为是无副作用的。
生产环境webpack默认sideEffects:true 读取packages.json的标识
来判断模块或包有/无副作用
则webpack在打包检查被分离css文件时并未找到以import和export导入导出的形式,认为无副作用直接摇掉。
webpack关于tree sharking部分定义如下:
9.OptimizeCSSAssetsWebpackPlugin
压缩css,可以跟上比较,字节从63 bytes -> 44 bytes,明显减少。
10.FriendlyErrorsWebpackPlugin
可以识别某些类的网页包错误,并对它们进行清理、聚合和排序,以提供更好的开发人员体验。
在未配置前打包时会输入某些包时的输入信息,大多是无用的信息,我们希望控制台能输入有效且干净的信息。
module.exports = {
devServer:{
contentBase:'./dist',
hot:true, // 是否启用热更新
/*
* 启动之后,只会在控制台打印初始信息,不会打印其他信息。意味关闭了错误和其他提示
* 千万要配置这个属性,否则FriendlyErrorsPlugin不起作用
*/
quiet: true,
},
plugins:{
/*
* 所以需要使用这个插件在编辑成功/报错是进行提示
*/
new FriendlyErrorsPlugin({
compilationSuccessInfo: {
messages: [`good job compile succ`],
},
onErrors:function(){
console.log(`compile faild, check you project`)
},
clearConsole:true,
}),
}
}
编译成功时清空了控制台,运行compilationSuccessInfo,执行自定义内容。
编译失败时明确告诉是个文件哪一行编译错误,方便快速定位。
插件的世界多而广,只简单介绍几种项目中用到的比较多的插件。可根据项目的需要在npm社区中寻找合适的插件。
5 结语
这里描述了一个基本的webpack使用以及webpack基本属性。实际项目远远不止这些配置,也不会如此简单。可能涉及到长效缓存、本地开发优化、首屏加载速度、文件名变更等等一系列的问题。加油吧!
6 参考
webpack官方文档
手摸手,带你用合理的姿势使用webpack4(下)
带你用合理的姿势使用webpack4(上)
处理打包文件体积过大的问题
webpack4---生产环境css样式丢失问题
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。