前言
通过webpack实现前端项目整体模块化的优势固然很明显,但是它同样存在一些弊端,那就是我们项目中的所有代码最终都被打包到了一起,如果我们应用非常复杂,模块非常多的话,我们的打包结果就会特别的大。而事实情况是,大多数时候,我们在应用开始工作时,并不是我们所有的模块都需要加载进来的,但是这些模块又被全部打包到一起,我们需要任何一个模块,都需要把整体加载过后才能使用,而我们的应用有一般是运行在浏览器端,那就意味着我们会浪费掉很多的流量和带宽。
那更合理的方案就是把我们的打包结果按照一定的规则去分离到多个bundle中,然后根据我们应用的运行需要,按需加载这些模块。这样我们就能大大提高应用的响应速度与运行效率。
目前webpack实现分包的方式主要有以下两种:
第一种是根据我们的业务去配置不同的打包入口,也就是我们会有多个打包入口同时打包,输出多个打包结果。
第二种就是采用ESM的动态导入功能,去实现模块的按需加载,这个时候webpack会把我们动态加载的模块单独输出到一个bundle中。
一、多入口打包
现有如下文件结构:
分为album页面内容、index.html页面内容、global.css公共样式、fetch.js是一个公共的提供请求api的模块,尝试为这个案例创建多个打包入口。
const { CleanWebpackPlugin } = require("clean-webpack-plugin")
const HtmlWebpackPlugin = require("html-webpack-plugin")
module.exports = {
mode:"none",
entry:{ // 将entry配置成一个对象,来设置多个打包入口
index:"./src/index.js",
album:"./src/album.js"
},
output:{// 修改输出文件名
filename:"[name].bundle.js" // 通过[name]这种占位符的方式动态输出文件名,[name]最终就会替换成打包入口名称
},
module:{
rules:[
{
test:/\.css$/,
use:[
"style-loader",
"css-loader"
]
}
]
},
plugins:[
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
title:"Multi Entry",
template:"./src/index.html",
filename:"index.html"
}),
new HtmlWebpackPlugin({
title:"Multi Entry",
template:"./src/album.html",
filename:"album.html"
})
]
}
打包结果:
但是生成的html中会载入两个打包后的bundle.js,我们只希望其引入自身的bundle.js
在HtmlWebpackPlugin通过chunks属性指定载入的bundle.js文件
new HtmlWebpackPlugin({
title:"Multi Entry",
template:"./src/index.html",
filename:"index.html",
chunks:['index']
}),
new HtmlWebpackPlugin({
title:"Multi Entry",
template:"./src/album.html",
filename:"album.html",
chunks:['album']
})
此时再打包,生成的html就只会载入自身需要的bundle.js了。
二、提取公共模块
在index.js和alubm.js中都有对公共模块的引入
import fetchApi from './fetch'
import './global.css'
import './index.css'
可以将这些公共模块提取出来,提取的办法也很简单,在webpack.config.js中添加如下配置:
optimization: {
splitChunks: {
chunks: 'all'
}
}
然后执行打包就会生成一个包含这些公共模块的js文件。但是我这没有生成,很头疼。找了很久,结果发现需要加上一个name属性。
optimization: {
splitChunks: {
chunks: 'all',
name: 'common'
}
}
这样就能成功将公共模块的js文件提取出来了,生成了一个common.bundle.js。
三、动态导入模块
假设我们有一个单页面应用,根据需求来载入模块。
// posts模块
export default () => {
const posts = document.createElement('div')
posts.className = 'posts'
posts.innerHTML = '<h2>Posts</h2>'
return posts
}
import posts from './posts/posts'
import album from './album/album'//结构与posts一样
const render = () => {
const hash = window.location.hash || '#posts'
const mainElement = document.querySelector('.main')
mainElement.innerHTML = ''
if (hash === '#posts') {
mainElement.appendChild(posts())
} else if (hash === '#album') {
mainElement.appendChild(album())
}
}
render()
window.addEventListener('hashchange', render)
我们在打包入口中同时导入了ablum模块和posts模块,当锚点发生变化时,根据锚点的值去决定要显示哪个组件,这里就会存在浪费的可能性,如果用户打开应用后只是访问了其中一个页面,另外一个页面所对应的模块的加载就是浪费,所以采用动态导入的方式就不会有浪费了。
动态导入就是采用ES Module的动态导入。
const render = () => {
const hash = window.location.hash || '#posts'
const mainElement = document.querySelector('.main')
mainElement.innerHTML = ''
if (hash === '#posts') {
import('./posts/posts').then(({ default: posts }) => {
mainElement.appendChild(posts())
})
} else if (hash === '#album') {
import('./album/album').then(({ default: album }) => {
mainElement.appendChild(album())
})
}
}
render()
执行打包,打包结果如下:
dist目录中多出了两个bundle.js,这两个bundle.js就是由动态导入自动分包所产生的。
整个过程我们无需配置任何地方,只需要按照ES Module动态导入的方式去导入模块就可以了,webpack内部会自动处理分包和按需加载。
四、魔法注释
默认都过动态导入产生的bundle文件,它们的名称就只是一个序号,如果我们需要给这些bundle命名的话,我们可以使用webpack所特有的魔法注释来实现。
具体使用如下:
if (hash === '#posts') {
import(/* webpackChunkName: 'components' */'./posts/posts').then(({ default: posts }) => {
mainElement.appendChild(posts())
})
} else if (hash === '#album') {
import(/* webpackChunkName: 'components' */'./album/album').then(({ default: album }) => {
mainElement.appendChild(album())
})
}
执行打包,打包结果:
如果命名一致,会打包到一个文件中,否则是多个文件。