前言: 在看本文前, 建议你看下, 下面这两篇文章 顺便给个赞和 GitHub 的赞哦~
如果你对 webpack 不是很了解, 请你关注我之前的文章, 都是百星以上 star 的高质量文
9102 年: 手写一个 React 完美版移动端脚手架
前端性能优化不完全手册
Git 仓库地址 https://github.com/JinJieTan/React-webpack
欢迎你关注我的《前端进阶》专栏 一起突破学习
文章内容都会不定期更新 记得一定要收藏
本文书写于 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 处理
文件后缀省掉 jsx 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 常见配置
- // 入口文件
- 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 会自动添加依赖是线上依赖还是开发环境的依赖)
配置入口
entry: path.resolve(__dirname, '../src/main.js')}
配置输出目录
- output: {
- filename: 'js/[name].[hash:5].js',
- path: path.resolve(__dirname, '../dist'),
- },
引入 Vue 脚手架里基本配置的 loader , 后面的 loader 都是往 rules 数组里加就行了~
- 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
加入 loader
- {
- test:/\.vue$/,
- loader:"vue-loader"
- }
加入 plugin
const vueplugin = require('vue-loader/lib/plugin')
在 webpack 的 plugin 中
new vueplugin()即可
入口指定 babel-polifill ,vendor 代码分割公共模块, 打包后这些代码都会在一个公共模块
- 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 文件
- const HtmlWebpackPlugin = require('html-webpack-plugin');
- plugins: [
- new HtmlWebpackPlugin({
- template: path.resolve(__dirname,'../index.html'),
- filename: 'index.html'
- }),
- ]
省掉. vue 的后缀 , 直接配置在 module.exports 对象中, 跟 entry 同级
- resolve: {
- extensions: ['.js','.json','.vue'],
- }
加入识别 HTML 文件的 loader
- {
- test: /\.(HTML)$/,
- loader: 'html-loader'
- }
开启多线程编译
- const os = require('os')
- {
- loader: 'thread-loader',
- options: {
- workers: os.cpus().length
- }
- }
加入 babel-loader 加入 babel-loader 还有 解析 JSX ES6 语法的 babel preset
@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
- 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 识别的模块
- {
- 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 杀掉无效的代码
- 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,
- }
- }),
加入图片压缩 性能优化很大
- {
- 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 把一些文件打包输出到固定的目录下
- {
- exclude: /\.(JS|JSON|Less|CSS|jsx)$/,
- loader: 'file-loader',
- options: {
- outputPath: 'media/',
- name: '[name].[contenthash:8].[ext]'
- }
- }
加入压缩 CSS 的插件
- const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin')
- new OptimizeCssAssetsWebpackPlugin({
- cssProcessPluginOptions:{
- preset:['default',{discardComments: {removeAll:true} }]
- }
- }),
加入 code spliting 代码分割 vue 脚手架是同步异步分开割, 我是直接一起割
- optimization: {
- runtimeChunk:true, // 设置为 true, 一个 chunk 打包后就是一个文件, 一个 chunk 对应 ` 一些 js css 图片 ` 等
- splitChunks: {
- chunks: 'all' // 默认 entry 的 chunk 不会被拆分, 配置成 all, 就可以了拆分了, 一个入口 `JS`,
- // 打包后就生成一个单独的文件
- }
- }
加入 WorkboxPlugin , PWA 的插件
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 的使用~
脚手架的搭建过程很多坑, 但是却能大大提升你的技术天花板, 跟着作者一起踩坑吧, 别忘了来我的 GitHub 点赞哦~ 仓库源码地址~ 欢迎 star https://github.com/JinJieTan/React-webpack
来源: https://segmentfault.com/a/1190000019207033