内容简介:通过接上篇文章,我们的目录结构,如图所示:如何我们从网上随意下载了一种字体,放置于
通过 如何利用webpack来提升前端开发效率(一) 的学习,我们已经能够通过 webpack
的 loader
和 piugin
机制来处理各种文件资源。细心的小伙伴们发现了缺少了对字体文件和 HTML
中 <img>
标签的资源处理,那让我们先来解决这个问题。
接上篇文章,我们的目录结构,如图所示:
首先是对字体文件的处理,修改webpack.config.js
// webpack.config.js // 新增对字体的loader { test: /\.(eot|woff|woff2|ttf)$/, use: [{ loader: 'url-loader', options: { name: '[name].[hash:7].[ext]', limit: 8192, outputPath: 'font', // 打包到 dist/font 目录下 } }] }, 复制代码
如何我们从网上随意下载了一种字体,放置于 src
文件夹下,并修改 src/index.html
<!-- src/index.html --> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>my-webpack</title> </head> <body> <h1>webpack大法好!!前端大法好!!</h1> </body> </html> 复制代码
在 index.scss
中引入字体
/* src/index.scss */ /* 添加以下样式 */ @font-face { font-family: 'myFont'; src: url('./font/ZaoZiGongFangQiaoPinTi-2.ttf'); } h1 { font-family: 'myFont'; } 复制代码
在此之前,每次重新打包都要删除 dist
文件夹,实在是麻烦,现在我们可以借助 clean-webpack-plugin
,它能够在每次打包时删除指定的文件夹,我们在命令行执行 npm i clean-webpack-plugin -D
修改 webpack.config.js
// webpack.config.js // 新增以下引入 const CleanWebpackPlugin = require('clean-webpack-plugin'); // 新增以下插件 plugins: [ new CleanWebpackPlugin(['dist']) ], 复制代码
随后在命令行执行 npm run build
,我们的 dist
文件夹会被自动删除,并输出以下结果,可以看到我们虽然成功打包了字体文件,但字体文件是在太大,连 webpack
都发出了警告 [big]
。
这里我们一般有以下解决方案:
CDN font-spider
我们实践一下第三种方案,也是我推荐的方案 在命令行依次执行
npm i font-spider -D font-spider ./dist/index.html 复制代码
可以看到将近 4MB 的字体文件体积瞬间压缩至不足 6KB !!!而页面效果和之前一模一样。
而对于HTML
文档中
<img>标签的引入问题
,我们需要借助
html-loader
,它能将
HTML
文档中
img.src
解析成
require
,从而实现引入图片,话不多说,我们直接看效果。在命令行执行
npm i html-loader -D
修改以下文件
// webpack.config.js // 新增对html的loader { test: /\.html$/, use: { loader: 'html-loader', options: { attrs: ['img:src'] // img代表解析标签,src代表要解析的值,以key:value形式存在于attrs数组中 } } } 复制代码
<!-- src/index.html --> <body> + <img src="./leaf.png" alt=""> </body> 复制代码
在命令行执行 npm run build
,查看 dist/index.html
,看来已经成功啦
动态加载
设想如果我们的入口文件很大(包含了所有的业务逻辑代码),就会造成首屏加载变慢,用户体验感下降。 这里我们从两个方面解决:
- 模块解耦,将入口文件解耦,将基础模块(UI,工具类)和业务模块分离,即能方便代码维护拓展,也能减少入口文件的体积。
- 动态加载,用户不可能一开始就用到所有的功能,这时候我们可以将次要的,需要事件触发的模块,在之后的交互过程中,动态引入。 在
src
目录下新增dynamic.js
// dynamic.js export default () => { console.log('Im dynamically loaded.'); } 复制代码
修改以下文件
<!-- src/index.html --> <body> + <button id="btn">点击我,动态加载dynamic.js</button> </body> 复制代码
// src/index.js // 新增以下内容 const btn = document.getElementById('btn'); // 点击按钮,动态加载dynamic.js btn.onclick = () => { import(/* webpackChunkName: "dynamic" */ './dynamic.js').then(function (module) { const fn = module.default; fn(); }) } 复制代码
执行 npm run build
,可以看到
/* webpackChunkName: "dynamic" */
,则是
可以得出得结论是:设置ChunkName
为
"dynamic"
是必要的,否则打包完成会是以自动分配的、可读性很差的
id
命名的
JS
文件。且没有
Chunk Names
标识。
现在我们打开 dist/index.html
,此时
当我点击该按钮时
控制台打印出
Network网络请求显示,动态加载了dynamic.js
至此,我们成功实现了动态加载。
分离开发环境和生产环境
回头看我们的 webpack.config.js
,不知不觉就写了这么多代码,鉴于我们在开发实际项目时,是开发和生产两套工作模式,各司其职,我们不如做个了断,分离配置。
命令行执行 npm i webpack-merge cross-env -D
webpack-merge
可以合并webpack配置项, cross-env
可以设置及使用环境变量。
新增 webpack.base.js
,提供基本的 webpack loader plugin
配置
const path = require('path'); const htmlWebpackPlugin = require('html-webpack-plugin'); const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const pathResolve = (targetPath) => path.resolve(__dirname, targetPath); const devMode = process.env.NODE_ENV !== 'production'; // 在node中,有全局变量process表示的是当前的node进程。 // process.env包含着关于系统环境的信息。 // NODE_ENV是用户一个自定义的变量,在webpack中它的用途是来判断当前是生产环境或开发环境。 // 我们可以通过 cross-env 将 NODE_ENV=development 写入 npm run dev的指令中,从而注入NODE_ENV变量。 module.exports = { entry: { index: pathResolve('js/index.js') }, output: { path: pathResolve('dist'), }, module: { rules: [ { test: /\.html$/, use: { loader: 'html-loader', options: { attrs: ['img:src'] }, }, }, { test: /\.(eot|woff|woff2|ttf)$/, use: [{ loader: 'url-loader', options: { name: '[name].[hash:7].[ext]', limit: 8192, outputPath: 'font', }, }], }, { test: /\.(sa|sc|c)ss$/, use: [ devMode ? 'style-loader' : { // 如果处于开发模式,则无需再外链CSS,直接插入到<style>标签中 loader: MiniCssExtractPlugin.loader, options: { publicPath: '../' } }, 'css-loader', 'postcss-loader', 'sass-loader', ], }, { test: /\.(png|jpg|jpeg|svg|gif)$/, use: [{ loader: 'url-loader', options: { limit: 8192, name: '[name].[hash:7].[ext]', outputPath: 'img', }, }], }, ], }, plugins: [ new htmlWebpackPlugin({ minify: { collapseWhitespace: true, // 移除空格 removeAttributeQuotes: true, // 移除引号 removeComments: true // 移除注释 }, filename: pathResolve('dist/index.html'), template: pathResolve('src/index.html'), }) ] }; 复制代码
新增 webpack.dev.js
,服务于开发模式下
const path = require('path'); const webpack = require('webpack'); const base = require('./webpack.base.js'); const { smart } = require('webpack-merge'); const pathResolve = (targetPath) => path.resolve(__dirname, targetPath); module.exports = smart(base, { mode: 'development', output: { filename: 'js/[name].[hash:7].js' }, devServer: { contentBase: pathResolve('dist'), port: '8080', inline: true, historyApiFallback: true, hot: true }, plugins: [ new webpack.HotModuleReplacementPlugin(), new webpack.NamedModulesPlugin() ] }) 复制代码
新增 webpack.prod.js
,服务于生产模式下
const path = require('path'); const base = require('./webpack.base.js'); const { smart } = require('webpack-merge'); const CleanWebpackPlugin = require('clean-webpack-plugin'); const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const pathResolve = (targetPath) => path.resolve(__dirname, targetPath); module.exports = smart(base, { mode: 'production', devtool: 'source-map', // 会生成对于调试的完整的.map文件,但同时也会减慢打包速度,适用于打包后的代码查错 output: { filename: 'js/[name].[chunkhash:7].js', chunkFilename: 'js/[name].[chunkhash:7].js', }, plugins: [ new CleanWebpackPlugin(['dist']), new MiniCssExtractPlugin({ filename: 'css/[name].[contenthash:7].css', }), ], }); 复制代码
相应的, package.json
也需要修改
// 新增以下两条命令 // cross-env 决定运行环境 --config 决定运行哪个配置文件 "dev": "cross-env NODE_ENV=development webpack-dev-server --config webpack.dev.js", "build": "cross-env NODE_ENV=production webpack --config webpack.prod.js " 复制代码
缓存之争
缓存在前端的地位毋庸置疑,正确的利用缓存就能极大地提高应用的加载速度和性能。 webpack
利用了 hash
值作为文件名的组成部分,能有效利用缓存。当修改文件,重新打包时,hash值就会改变,导致缓存失效,HTTP请求重新拉取资源。
而 webpack
有三种hash处理策略,分别是:
hash
属于项目工程级别的,即每次修改任何一个文件,所有文件名的 hash
值都将改变。所以一旦修改了任何一个文件,整个项目的文件缓存都将失效。如将整个项目的 filename
的命名策略改为 name.[hash:7]
(:7的意思是从完整hash值中截取前七位),我们可以看到,打包后的文件hash值是一样的,所以对于没有改变的模块而言,hash也被更新了,导致缓存失效了。
chunkhash
chunkhash
根据不同的入口文件( Entry
)进行依赖文件解析、构建对应的 chunk
,生成对应的哈希值。如将整个项目 filename
的命名策略改为 name.[chunkhash:7]
,我们可以看到 Chunk Names
为 "index"
的文件hash值一致,而不同 chunk
的 hash
值不同。这也就避免了修改某个文件,整个工程hash值都将改变的情况。
contenthash
但问题随之而来, index.scss
是作为模块导入到 index.js
中的,其 chunkhash
值是一致的,只要其中之一改变,与其关联的文件 chunkhash
值也会改变。这时候就要用到 contenthash
,它是根据文件的内容计算,该文件的内容改变了,contenthash值才会改变。我们将css文件的命名策略改为 name.[contenthash:7]
,并修改 src/index.js
,不改动其他文件,再次打包,发现:
生产环境的配置优化
tree-shaking
字面意思理解为从一棵树上把叶子摇晃下来,这样数的重量就减轻了,类比程序,就如同从我们的应用上删除没用的代码,从而减少体积。借于 ES6
的模块引入是静态分析的,故而 webpack
可以在编译时正确判断到底加载了什么代码,即没有被引用的模块不会被打包进来,减少我们的包大小,缩小应用的加载时间,呈现给用户更佳的体验。那么怎么使用呢?
新建 src/utils.js
// src/utils.js const square = (num) => num ** 2; const cube = num => num * num * num; // 导出了两个方法 export { square, cube } 复制代码
新建 src/shake.js
// src/shake.js import { cube } from './utils.js'; // 只使用了cube方法 console.log('cube(3) is' + cube(3)); 复制代码
在 webpack.base.js
中新增入口文件 shake.js
entry: { + shake: pathResolve('src/shake.js') }, 复制代码
命令行执行 npm run build
,查看打包后的 shake.js
,并没有发现 square
方法没有被打包进来,说明 tree-shaking
起作用了。 而这一切都是 webpack
在 production
环境下自动为我们实现的。
splitChunks
字面意思为拆分代码块,默认情况下它将只会影响按需加载的代码块,因为改变初始化的代码块将会影响 HTML
中运行项目需要包含的 script
标签。还记得我们在 src/index.js
中动态引入了 src/dynamic.js
吗,最终 dynamic.js
被独立打包,就是归功于 splitChunks
。
在实际生产中,我们经常会引入第三方库( JQuery
, Lodash
),往往这些第三方库体积高达几十KB掺杂在业务代码中,并且不会像业务代码一样经常更新,这时候我们就需要将他们拆分出来,既能保持第三方库持久缓存,又能缩减业务代码的体积。
修改 webpack.prod.js
// 在module.exports中新增如下内容 optimization: { runtimeChunk: { name: 'manifest', // 被注入了webpackJsonp的定义及异步加载相关的定义,单独打包模块信息清单,利于缓存 }, splitChunks: { cacheGroups: { // 缓存组,默认将所有来源于node_modules的模块分配到叫做'venders'的缓存组,所有引用超过两次的模块分配到'default'缓存组. vendor: { chunks: "all", // all, async, initial 三选一, 插件作用的chunks范围,推荐all test: /[\\/]node_modules[\\/]/, // 缓存组所选择的的模块范围 name: "vendor", // Chunk Names及打包出来的文件名 minChunks: 1, // 引用次数>=1 maxInitialRequests: 5, // 页面初始化时加载代码块的请求数量应该<=5 minSize: 0, // 代码块的最小尺寸 priority: 100, // 缓存优先级权重 }, } } }, 复制代码
命令行执行 npm i lodash -S
修改 src/index.js
// 新增以下内容 import _ from 'lodash'; 复制代码
执行 npm run build
,可以看到优化前 lodash
被打包进 index.js
,优化后 lodash
被打包进 vendor.js
。
压缩代码,去除冗余
往往在CSS代码中,存在很多我们没有用到的样式,它们是冗余的,我们需要将它们剔除,并压缩剩余的CSS样式,以减少CSS文件体积。
在命令行执行 npm i glob optimize-css-assets-webpack-plugin purifycss-webpack purify-css -D
修改 webpack.prod.js
// 新增以下引入 const glob = require('glob'); // 匹配所需文件 const PurifyCssWebpack = require('purifycss-webpack'); // 去除冗余CSS const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin"); // 压缩CSS // 新增以下插件 new PurifyCssWebpack({ paths: glob.sync(pathResolve('src/*.html')) // 同步扫描所有html文件中所引用的css,并去除冗余样式 }) // 新增以下优化 optimization: { minimizer: [ new OptimizeCSSAssetsPlugin({}) // 压缩CSS ] } 复制代码
执行 npm run build
CSS
,并压缩至一行。接下来,我们需要压缩
JS
代码。 由于我们使用的是
uglifyjs-webpack-plugin
,它需要ES6的支持,所以我们先让工程支持ES6的语法。 Babel 是一个 JavaScript 编译器。它能把下一代 JavaScript 语法转译成ES5,以适配多种运行环境。
@babel/core
提供了babel的转译API,如 babel.transform
等,用于对代码进行转译。像 webpack
的 babel-loader
就是调用这些API来完成转译过程的。
@babel/preset-env
可以根据配置的目标浏览器或者运行环境来自动将 ES2015+
的代码转换为 ES5
。
先在命令行执行 npm i @babel/core @babel/preset-env babel-loader @babel/plugin-syntax-dynamic-import -D
新建 .babelrc
文件
{ "presets": [ // 配置预设环境 ["@babel/preset-env", { "modules": false }] ], "plugins": [ "@babel/plugin-syntax-dynamic-import" // 处理src/index.js中动态加载 ] } 复制代码
修改 webpack.base.js
// 新增js的解析规则 { test: /\.(js|jsx)$/, use: 'babel-loader', exclude: /node_modules/ }, 复制代码
然后命令行执行 npm i uglifyjs-webpack-plugin -D
修改 webpack.prod.js
// 新增以下引入 const UglifyJsPlugin = require("uglifyjs-webpack-plugin"); // 新增以下优化 optimization: { minimizer: [ + new UglifyJsPlugin({ // 压缩JS cache: true, parallel: true, sourceMap: true }) ] } 复制代码
执行 npm run build
,可以看到打包的文件体积大大减少,大功告成, JS
也被压缩了。
以 index.html
为例,我们可以打开Chrome的开发者工具,选择More tools,点击Coverage面板,可以看到JS、CSS等文件的使用率,配合我们定制的webpack配置进行极致优化。
多页面
有时候,我们需要同时构建多个页面,借助 html-webpack-plugin
,只需在 plugins
中添加新页面的配置项。
新增 src/main.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>main page</title> </head> <body> <h1>I am Main Page</h1> </body> </html> 复制代码
修改 webpack.base.js
// 修改以下内容 plugins: [ new htmlWebpackPlugin({ // 配置index.html minify: { collapseWhitespace: true, removeAttributeQuotes: true, removeComments: true }, filename: pathResolve('dist/index.html'), template: pathResolve('src/index.html'), chunks: ['manifest', 'vendor', 'index', ] // 配置index.html需要用的chunk块,即加载哪些JS文件,manifest模块管理的核心,必须第一个进行加载,不然会报错 }), new htmlWebpackPlugin({ // 配置main.html minify: { collapseWhitespace: true, removeAttributeQuotes: true, removeComments: true }, filename: pathResolve('dist/main.html'), template: pathResolve('src/main.html'), chunks: ['manifest', 'shake'] // 配置index.html需要用的chunk块,加载manifest.js,shake.js }), ], 复制代码
执行 npm run build
,成功构建了 index.html
, main.html
。
结语
至此,我们摆脱了第三方脚手架的的禁锢,循序渐进的搭建了属于自己的前端流程工具,做到了即改即用,功能俱全,快速便捷,复用性强的特点。希望小伙伴能亲自动手,别老是纸上谈 webpack
,要理解它的构建、优化原理,得心应手得融入到自己的工程项目中,拒绝再用以前繁琐,不规范的开发流程,不做“CV工程师”,创建属于自己的知识体系、工作流程,提高前端的开发效率。
最后,本项目源码已部署在 Github
上,并增加了许多额外优化( less
的支持, ESLint
检测,针对图片格式的压缩...),让大家可以直接下载体验,并协助项目开发,日后也会持续维护,希望小伙伴们可以互相学习,提提建议。
easy-frontend 一个快速,简单,易用的前端开发效率提升工具
Star该项目,就是你们对我最大的的鼓励!!
前端路上,不忘初心,祝大家早日发财!!
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- 提升前端开发效率的工具
- 六个提升前端开发效率的工具
- babel在提升前端效率的实践
- 如何利用webpack来提升前端开发效率(一)?
- 小技巧:SpringBoot项目如何让前端开发提高效率?
- Spring Boot 项目如何让前端开发提高效率?
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。