通过学习如何使用webpack提高后端开发效率(一),我们已经可以通过webpack的loader和piugin机制来处理各种文件资源了。细心的小伙伴发现缺少对字体文件和HTML的支持
// 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
my-webpack webpack大法好!!前端大法好!!
复制代码
/* src/index.scss */ /* 添加以下样式 */ @font-face { font-family: 'myFont'; src: url('./font/ZaoZiGongFangQiaoPinTi-2.ttf'); } h1 { font-family: 'myFont'; } 复制代码
之前每次重新打包的时候都要删除dist文件夹,确实很麻烦。 现在我们可以使用clean-webpack-plugin,它也可以在每次打包时删除指定的文件夹。 我们在命令行执行npmiclean-webpack-plugin-D
更改 webpack.config.js
// webpack.config.js // 新增以下引入 const CleanWebpackPlugin = require('clean-webpack-plugin'); // 新增以下插件 plugins: [ new CleanWebpackPlugin(['dist']) ], 复制代码
然后在命令行执行npmrunbuild,我们的dist文件夹就会被手动删除,并输出如下结果。 可以看到,虽然我们成功打包了字体文件,但是字体文件太大了,甚至webpack发出了警告 [big] 。
这里我们通常有以下几种解决方案:
CDN font-spider
我们来练习第三种方案,这也是我推荐的方案。 在命令行中依次执行。
npm i font-spider -D font-spider ./dist/index.html 复制代码
可以看到近4MB的字体文件大小被压缩到了6KB以下! ! ! 页面效果和之前一模一样。
对于 HTML 文档
对于标签的引入,我们需要使用html-loader,它可以将HTML文档中的img.src解析为require,这样就可以引入图片了。 废话不多说,我们直接看一下功效。 在命令行执行npmihtml-loader -D
更改以下文件
// webpack.config.js // 新增对html的loader { test: /.html$/, use: { loader: 'html-loader', options: { attrs: ['img:src'] // img代表解析标签,src代表要解析的值,以key:value形式存在于attrs数组中 } } } 复制代码
+ 复制代码
在命令行执行npmrunbuild并检查dist/index.html。 看来已经成功了。
想象一下,如果我们的入口文件很大(包括所有业务逻辑代码),就会导致首屏加载缓慢,用户体验下降。 这里我们从两个方面来解决:
// dynamic.js export default () => { console.log('Im dynamically loaded.'); } 复制代码
更改以下文件
+ 复制代码
// 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(); }) } 复制代码
执行npmrunbuild可以看到
如果 /*webpackChunkName:"dynamic"*/ 未设置
,那么就是
可以得出的推论是,必须将ChunkName设置为“dynamic”,否则该包将是一个以手动分配的id命名的JS文件,可读性很差。 并且没有 ChunkNames
标记。
现在我们打开dist/index.html,此时
当我点击按钮时
控制台复制出来
至此,我们就成功实现了动态加载。
回头看看我们的webpack.config.js,我们不知不觉写了这么多代码。 考虑到我们在开发实际项目时,有两种工作模式:开发和生产,各司其职。 我们不妨打断一下,把配置分开。 。
命令行执行 npmiwebpack-mergecross-env -D
webpack-merge 可以合并 webpack 配置项,cross-env 可以设置和使用环境变量。
添加了 webpack.base.js 以提供基本的 webpackloaderplugin 配置
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,直接插入到标签中 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使用哈希值作为文件名的一部分,可以有效地利用缓存。 当文件发生更改或者重新打包时,哈希值会发生变化,导致缓存失效,HTTP请求重新拉取资源。
Webpack 有三种哈希处理策略,分别是:
散列
属于项目级别,即任何一个文件每次发生变化,所有文件名的哈希值都会发生变化。 所以一旦任何一个文件发生改变,整个项目的文件缓存就会失效。 例如,如果将整个项目的文件名的命名策略改为name.[hash:7](:7表示截取完整哈希值的前七位),我们可以看到该文件的哈希值打包后的文件是一样的,所以对于没有改变的模块来说,hash也被更新了,导致缓存失效。
块哈希
Chunkhash根据不同的入口文件(Entry)解析依赖文件,构建对应的chunk,并生成对应的hash值。 例如,如果将整个项目文件名的命名策略改为name.[chunkhash:7],我们可以看到ChunkNames为“index”的文件具有相同的hash值webpack提示效率,但不同的chunk具有不同的hash值。 这也防止了更改某个文件会改变整个项目的哈希值的情况。
内容哈希
但问题突然出现了。 index.scss作为模块导出到index.js,其chunkhash值一致。 只要其中之一发生变化,其关联文件的 chunkhash 值也会发生变化。 这个时候就要用到contenthash了。 根据文件内容估计。 如果文件内容发生变化,contenthash 值也会发生变化。 我们将css文件的命名策略更改为name.[contenthash:7],并更改了src/index.jswebpack提示效率,其他文件没有更改。 我们再次打包发现:
字面意思就是把树上的叶子抖落下来,使数字的重量减轻。 类比程序,就像从我们的应用程序中删除无用的代码,从而减少体积。 得益于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') }, 复制代码
在命令行执行npmrunbuild,查看打包好的shake.js,发现square方法没有打包,说明tree-shaking起作用了。 而这一切都是由webpack在生产环境中手动为我们实现的。
分割块
字面意思就是分割代码块。 默认情况下,它只会影响按需加载的代码块,因为更改初始化的代码块将影响运行项目时需要包含的 HTML 中的脚本标签。 请记住,我们在 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, // 缓存优先级权重 }, } } }, 复制代码
命令行执行 npmilodash -S
更改 src/index.js
// 新增以下内容 import _ from 'lodash'; 复制代码
执行npmrunbuild,可以看到优化前lodash被打包到index.js中,优化后lodash被打包到vendor.js中。
通常在 CSS 代码中,有很多样式是我们不使用的。 它们是多余的。 我们需要删除它们并压缩剩余的 CSS 样式以减小 CSS 文件的大小。
在命令行执行 npmigloboptimize-css-assets-webpack-pluginpurifycss-webpackpurify-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 ] } 复制代码
执行npmrunbuild
正如您所看到的,不仅删除了多余的 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。
首先在命令行执行npmi@babel/core@babel/preset-envbabel-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/ }, 复制代码
然后在命令行执行npmiuglifyjs-webpack-plugin -D
更改 webpack.prod.js
// 新增以下引入 const UglifyJsPlugin = require("uglifyjs-webpack-plugin"); // 新增以下优化 optimization: { minimizer: [ + new UglifyJsPlugin({ // 压缩JS cache: true, parallel: true, sourceMap: true }) ] } 复制代码
执行npmrunbuild,可以看到打包后的文件大小大大减小。 大功告成,JS也被压缩了。
以index.html为例,我们可以打开Chrome的开发者工具,选择Moretools,点击Coverage面板,可以看到JS、CSS等文件的使用情况,可以通过我们自定义的webpack配置进行优化。
多页
有时,我们需要同时创建多个页面。 使用html-webpack-plugin,我们只需要在plugins中添加新页面的配置项即可。
添加了 src/main.html
main page I am Main Page
复制代码
更改 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 }), ], 复制代码
执行npmrunbuild并成功创建index.html和main.html。
结论
至此,我们摆脱了第三方脚手架的束缚,逐步构建了自己的后端流程工具,达到了即用型、功能齐全、快捷方便、可复用性高的特点。 希望男伴能自己动手,不要总是纸上谈兵的webpack,一定要了解它的建立和优化原理,并融入到自己的工程项目中,拒绝使用以前复杂的、不规范的开发流程,不做“CV工程师”,创建自己的知识体系和工作流程,提高后端开发效率。
最后,这个项目的源码已经部署在Github上,额外做了很多优化(少支持、ESLint检查、图片格式压缩……),方便大家直接下载体验,辅助项目开发。 以后会持续维护,希望男伙伴们互相学习,提出建议。