如果你对webpack
不是很了解,请你关注我之前的文章,都是百星以上star
的高质量文
- 9102年:手写一个React完美版移动端脚手架
- 前端性能优化不完全手册
- GIT仓库地址
- 欢迎你关注我的
《前端进阶》
专栏 一起突破学习 文章内容都会不定期更新 记得一定要收藏
- 本文书写于2019年5月17日 未经作者允许不得转载 使用最新版4.31版本
webpack
-
webpack
用了会上瘾,它也是突破你技术瓶颈的好方向,现在基本上任何东西都离不开webpack
,webpack
用得好,什么next nuxt
随便上手(本人体会很深),本人参考了Vue
脚手架,京东的webpack
优化方案,以及本人的其他方面优化,着重在生产模式
下的构建速度优化提升非常明显(当然开发环境下也是~),性能提升很明显哦~ - 本配置完成功能:
- 识别
.Vue
文件和template模板
-
tree shaking
摇树优化 删除掉无用代码 - 引入
babel polifill
并且按需加载,识别一切代码 - 识别
async / await
和 箭头函数 -
PWA
功能,热刷新,安装后立即接管浏览器 离线后仍让可以访问网站 还可以在手机上添加网站到桌面使用 -
preload
预加载资源prefetch
按需请求资源 ,这里除了dns
预解析外,建议其他的使用按需加载组件,顺便代码分割,这也是京东的优化方案 - 配置
nginx
,拦截非预期请求(京东的方案) -
CSS
模块化,不怕命名冲突 - 小图片的
base64
处理 - 文件后缀省掉j
sx js json
等 - 实现
VueRouter
路由懒加载,按需加载 , 代码分割 指定多个路由同个chunkName
并且打包到同个chunk
中 实现代码精确分割 - 支持
less sass stylus
等预处理 -
code spliting
优化首屏加载时间 不让一个文件体积过大 - 提取公共代码,打包成一个chunk
- 每个chunk有对应的
chunkhash
,每个文件有对应的contenthash
,方便浏览器区别缓存 - 图片压缩
-
CSS
压缩 - 增加
CSS
前缀 兼容各种浏览器 - 对于各种不同文件打包输出指定文件夹下
- 缓存babel的编译结果,加快编译速度
- 每个入口文件,对应一个chunk,打包出来后对应一个文件 也是
code spliting
- 删除HTML文件的注释等无用内容
- 每次编译删除旧的打包代码
- 将
CSS
文件单独抽取出来 - 让babel不仅缓存编译结果,还在第一次编译后开启多线程编译,极大加快构建速度
性能优化没有尽头,本人仅表达自己目前掌握的知识点,士别三日,刮目相看:每隔三天,技术就会进步一次
正式开始吧,假设你已经懂什么是
entry output loader plugin
,如果不懂,看我上面的文章哦~
webpack常见配置
代码语言:javascript复制// 入口文件
entry: {
app: './src/js/index.js',
},
// 输出文件
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
publicPath: '/' //确保文件资源能够在 http://localhost:3000 下正确访问
},
// 开发者工具 source-map
devtool: 'inline-source-map',
// 创建开发者服务器
devServer: {
contentBase: './dist',
hot: true // 热更新
},
plugins: [
// 删除dist目录
new CleanWebpackPlugin(['dist']),
// 重新穿件html文件
new HtmlWebpackPlugin({
title: 'Output Management'
}),
// 以便更容易查看要修补(patch)的依赖
new webpack.NamedModulesPlugin(),
// 热更新模块
new webpack.HotModuleReplacementPlugin()
],
// 环境
mode: "development",
// loader配置
module: {
rules: [
{
test: /.css$/,
use: [
'style-loader',
'css-loader'
]
},
{
test: /.(png|svg|jpg|gif)$/,
use: [
'file-loader'
]
}
]
}
这里面我们重点关注
module
和plugins
属性,因为今天的重点是编写loader
和plugin
,需要配置这两个属性。
-
webpack
启动后,在读取配置的过程中会先执行new MyPlugin(options)
初始化一个MyPlugin
获得其实例。在初始化compiler
对象后,再调用myPlugin.apply(compiler)
给插件实例传入compiler
对象。
插件实例在获取到 compiler
对象后,就可以通过 compiler.plugin
(事件名称, 回调函数) 监听到 Webpack
广播出来的事件。
并且可以通过 compiler 对象去操作 webpack。
-
Compiler
对象包含了Webpack
环境所有的的配置信息,包含options,loaders,plugins
这些信息,这个对象在Webpack
启动时候被实例化,它是全局唯一的,可以简单地把它理解为Webpack
实例;-
Compilation
对象包含了当前的模块资源、编译生成资源、变化的文件等。当 Webpack 以开发模式运行时,每当检测到一个文件变化,一次新的
Compilation将被创建。
Compilation对象也提供了很多事件回调供插件做扩展。通过
Compilation也能读取到
Compiler` 对象。 -
Compiler 和 Compilation
的区别在于: -
Compiler
代表了整个Webpack
从启动到关闭的生命周期,而Compilation
只是代表了一次新的编译。 - 事件流
-
webpack
通过Tapable
来组织这条复杂的生产线。 -
webpack
的事件流机制保证了插件的有序性,使得整个系统扩展性很好。 -
webpack
的事件流机制应用了观察者模式,和Node.js 中的 EventEmitter
非常相似。
-
1.2 打包原理
- 识别入口文件
- 通过逐层识别模块依赖。(
Commonjs、amd
或者es6
的import,webpack
都会对其进行分析。来获取代码的依赖) -
webpack
做的就是分析代码。转换代码,编译代码,输出代码 - 最终形成打包后的代码
- 这些都是
webpack
的一些基础知识,对于理解webpack的工作机制很有帮助。
脚手架一般都是遵循了commonjs
模块化方案,如果你不是很懂,那么看起来很费劲,我写的脚手架,就不使用模块化方案了,简单粗
暴
- 开始开发环境配置
- 包管理器 使用
yarn
不解释 就用yarn
- 配置
webpack.dev.js
开发模式下的配置 yarn init -y
-
yarn add webpack webpack-cli
(yarn
会自动添加依赖是线上依赖还是开发环境的依赖)
配置入口
代码语言:javascript复制entry: path.resolve(__dirname, '../src/main.js')}
配置输出目录
代码语言:javascript复制output: {
filename: 'js/[name].[hash:5].js',
path: path.resolve(__dirname, '../dist'),
},
引入Vue
脚手架里基本配置的loader
,后面的loader
都是往rules
数组里加就行了~
代码语言:javascript复制module: {
rules: [
{
test: /.(png|jpe?g|gif|svg)(?.*)?$/,
use: [{
loader: 'url-loader',
options: {
limit: 10000,
name: 'img/[name]-[hash:5].[ext]',
}
}
]
},
{
test: /.(woff2?|eot|ttf|otf)(?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: 'fonts/[name]-[hash:5].[ext]',
}
},
{
test: /.(mp4|webm|ogg|mp3|wav|flac|aac)(?.*)?$/,
use: [
{
loader: 'url-loader',
options: {
limit: 4096,
name: 'media/[name]-[hash:5].[ext]',
}
}
]
}
]
},
有人会问 这么多我怎么看啊 别急 第一个
url-loader
是处理base64
图片的,让低于limit
大小的文件以base64
形式使用,后面两个一样的套路,只是换了文件类型而已 ,不会的话,先复制过去跑一把?
配置识别.vue
文件和tempalte
模板 , yarn add vue vue-loader vue-template-compiler
代码语言:javascript复制加入loader
{
test:/.vue$/,
loader:"vue-loader"
}
加入plugin
const vueplugin = require('vue-loader/lib/plugin')
在webpack的plugin中
new vueplugin()即可
入口指定babel-polifill
,vendor
代码分割公共模块,打包后这些代码都会在一个公共模块
代码语言:javascript复制 app: ['babel-polyfill', './src/index.js', './src/pages/home/index.js', './src/pages/home/categorys/index.jsx'],
vendor: ['vuex', 'better-scroll', 'mint-ui', 'element-ui']
指定 html
文件为模板打包输出,自动引入打包后的js
文件
代码语言:javascript复制const HtmlWebpackPlugin = require('html-webpack-plugin');
plugins: [
new HtmlWebpackPlugin({
template: path.resolve(__dirname,'../index.html'),
filename: 'index.html'
}),
]
省掉.vue
的后缀 ,直接配置在module.exports
对象中,跟entry
同级
代码语言:javascript复制 resolve: {
extensions: ['.js','.json','.vue'],
}
加入识别html
文件的loader
代码语言:javascript复制 {
test: /.(html)$/,
loader: 'html-loader'
}
开启多线程编译
代码语言:javascript复制const os = require('os')
{
loader: 'thread-loader',
options: {
workers: os.cpus().length
}
}
加入babel-loader
加入 babel-loader 还有 解析JSX ES6语法的 babel preset
代码语言:javascript复制 @babel/preset-env解析es6语法
@babel/plugin-syntax-dynamic-import解析vue的 import按需加载,附带code spliting功能
{
loader: 'babel-loader',
options: { //jsx语法
presets: ["@babel/preset-react",
//tree shaking 按需加载babel-polifill
["@babel/preset-env", { "modules": false, "useBuiltIns": "false", "corejs": 2 }]],
plugins: [
//支持import 懒加载
"@babel/plugin-syntax-dynamic-import",
//andt-mobile按需加载 true是less,如果不用less style的值可以写'css'
["import", { libraryName: "antd-mobile", style: true }],
//识别class组件
["@babel/plugin-proposal-class-properties", { "loose": true }],
],
cacheDirectory: true
},
}
在使用上面的babel
配置后 我们躺着就可以用vueRouter
的路由懒加载了
路由懒加载
- 当打包构建应用时,JavaScript 包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就更加高效了。
- 结合 Vue 的异步组件和 Webpack 的代码分割功能,轻松实现路由组件的懒加载。
- 首先,可以将异步组件定义为返回一个 Promise 的工厂函数 (该函数返回的 Promise 应该 resolve 组件本身):
- const Foo = () => Promise.resolve({ / 组件定义对象 / })
- 第二,在 Webpack 中,我们可以使用动态 import语法来定义代码分块点 (split point):
import('./Foo.vue') // 返回 Promise
注意
- 如果您使用的是 Babel,你将需要添加 syntax-dynamic-import 插件,才能使 Babel 可以正确地解析语法。
- 结合这两者,这就是如何定义一个能够被 Webpack 自动代码分割的异步组件。
const Foo = () => import('./Foo.vue')
在路由配置中什么都不需要改变,只需要像往常一样使用 Foo:
const router = new VueRouter({
routes: [
{ path: '/foo', component: Foo }
]
})
# 把组件按组分块
有时候我们想把某个路由下的所有组件都打包在同个异步块 (chunk) 中。只需要使用 命名 chunk,一个特殊的注释语法来提供 chunk name (需要 Webpack > 2.4)。
const Foo = () => import(/* webpackChunkName: "group-foo" */ './Foo.vue')
const Bar = () => import(/* webpackChunkName: "group-foo" */ './Bar.vue')
const Baz = () => import(/* webpackChunkName: "group-foo" */ './Baz.vue')
Webpack 会将任何一个异步模块与相同的块名称组合到相同的异步块中。
加入插件 热更新plugin和html-webpack-plugin
代码语言:javascript复制 const HtmlWebpackPlugin = require('html-webpack-plugin')
const webpack = require('webpack')
new HtmlWebpackPlugin({
template: './src/index.html'
}),
new webpack.HotModuleReplacementPlugin(),
devServer: {
contentBase: '../build',
open: true,
port: 5000,
hot: true
},
加入less-css
识别的模块
代码语言:javascript复制 {
test: /.(less|css)$/,
use: [
{ loader: 'style-loader' },
{
loader: 'css-loader'
, options: {
modules: false, //不建议开启css模块化,某些ui组件库可能会按需加载失败
localIdentName: '[local]--[hash:base64:5]'
}
},
{
loader: 'less-loader',
options: { javascriptEnabled: true }
}
]
},
下面正式开始生产环境
踩坑是好事 为什么这次不放完整的源码 因为不去踩坑 永远提升不了技术
html
杀掉无效的代码
代码语言:javascript复制 new HtmlWebpackPlugin({
template: './src/index.html',
minify: {
removeComments: true,
collapseWhitespace: true,
removeRedundantAttributes: true,
useShortDoctype: true,
removeEmptyAttributes: true,
removeStyleLinkTypeAttributes: true,
keepClosingSlash: true,
minifyJS: true,
minifyCSS: true,
minifyURLs: true,
}
}),
加入图片压缩 性能优化很大
代码语言:javascript复制{
test: /.(jpg|jpeg|bmp|svg|png|webp|gif)$/,
use:[
{loader: 'url-loader',
options: {
limit: 8 * 1024,
name: '[name].[hash:8].[ext]',
outputPath:'/img'
}},
{
loader: 'img-loader',
options: {
plugins: [
require('imagemin-gifsicle')({
interlaced: false
}),
require('imagemin-mozjpeg')({
progressive: true,
arithmetic: false
}),
require('imagemin-pngquant')({
floyd: 0.5,
speed: 2
}),
require('imagemin-svgo')({
plugins: [
{ removeTitle: true },
{ convertPathData: false }
]
})
]
}
}
]
}
加入file-loader 把一些文件打包输出到固定的目录下
代码语言:javascript复制{
exclude: /.(js|json|less|css|jsx)$/,
loader: 'file-loader',
options: {
outputPath: 'media/',
name: '[name].[contenthash:8].[ext]'
}
}
加入压缩css的插件
代码语言:javascript复制 const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin')
new OptimizeCssAssetsWebpackPlugin({
cssProcessPluginOptions:{
preset:['default',{discardComments: {removeAll:true} }]
}
}),
加入code spliting代码分割 vue
脚手架是同步异步分开割,我是直接一起割
代码语言:javascript复制 optimization: {
runtimeChunk:true, //设置为 true, 一个chunk打包后就是一个文件,一个chunk对应`一些js css 图片`等
splitChunks: {
chunks: 'all' // 默认 entry 的 chunk 不会被拆分, 配置成 all, 就可以了拆分了,一个入口`JS`,
//打包后就生成一个单独的文件
}
}
加入 WorkboxPlugin , PWA的插件
代码语言:javascript复制pwa这个技术其实要想真正用好,还是需要下点功夫,它有它的生命周期,以及它在浏览器中热更新带来的副作用等,需要认真研究。可以参考百度的lavas框架发展历史~
const WorkboxPlugin = require('workbox-webpack-plugin')
new WorkboxPlugin.GenerateSW({
clientsClaim: true, //让浏览器立即servece worker被接管
skipWaiting: true, // 更新sw文件后,立即插队到最前面
importWorkboxFrom: 'local',
include: [/.js$/, /.css$/, /.html$/,/.jpg/,/.jpeg/,/.svg/,/.webp/,/.png/],
}),
单页面应用的优化核心 :
- 最重要的是路由懒加载 代码分割
- 部分渲染在服务端完成 极大加快首屏渲染速度
VUE
首选nuxt
框架,也可以使用它的脚手架 - 图片压缩和图片懒加载是对页面层次最大的优化之一
- 后面继续书写
next nuxt
和pwa
的使用~