前端工程化实践(一):老项目工程化升级改造
前端功能化概念
前端工程化解决的问题
前端工程化解决解决的问题包含前端开发效率,开发规范,访问性能等。
传统前端开发会碰到的问题以及解决方案
- js全局作用域冲突问题,解决:模块化 npm webpack
- 编码规范问题,解决:eslint
- 资源合并和压缩问题,解决:webpack
- 高版本js预发降级,解决:es6降级es5使用babel等工具
前端工程化应用场景
企业中存在的前端问题:
- 解决研发流程中的问题
- 工程化核心目标是效率、规范、性能
- 对研发流程中的痛点进行诊断优化
- 项目量级增加:几千行代码=>几万行代码,解决:模块化
- 项目数量扩大:几个=> 几千个,解决:研发脚手架,研发平台
- 项目复杂度高:web项目=>H5/PC/小程序/脚手架...,解决:工程脚手架
如何解决这些问题:
- 项目研发模式升级,模块化
- 工程脚手架(vue、react等等)
- 研发脚手架(创建、发布)
- 项目性能优化
模块化
什么是前端模块化
- 将复杂的程序根据需求拆分成若干个模块,一个模块包括输入和输出
- 模块内部是私有的,对外暴露接口和其他模块通信
- 一个html页面可以引用的script包括:脚本和模块
CommonJS
CommonJS介绍
- Node.js默认模块化规范, 每个文件就是一个模块,有自己的作用域
- Node中CJS模块加载采用同步加载方式
- 通过require加载模块,通过exports或module.exports输出模块
CommonJS规范特点
- 所有代码都运行在模块作用域,不会污染全局作用域。
- 模块可以多次加载,第一次加载时会运行模块,模块输出结果会被向缓存,再次加载时,会从缓存结果中直接读取模块输出结果。
- 模块加载的顺序按照其在代码中出现的顺序。
AMD
- AMD规范采用非同步加载模块,允许指定回调函数
- Node模块通常都位于本地,加载速度快,所以适用于同步加载
- 浏览器环境下,模块需要请求获取,所以适用于异步加载
- require.js是AMD的一个具体实现库
CMD
- CMD整合了CommonJS和AMD的优点,模块加载是异步的
- CMD专门用于浏览器端,sea.js是CMD规范的一 个实现
- AMD和CMD最大的问题是没有通过语法升级解决模块化
ESModule
ESModule规范介绍
- ESModule设计理念是希望在编译时就确定模块依赖关系及输入输出
- CommonJS和AMD必须在运行时才能确定依赖和输入、输出
- ESM通过import加载模块,通过export输出模块
ESModule和CommonJS对比
- CommonJS模块输出的是值的拷贝,ES6模块输出的是值的引用
- CommonJS模块是运行时加载,ES6模块是编译时输出接口
- CommonJs是单个值导出,ES6 Module可以导出多个
实战前言
如果对webpack不熟悉的同学可以提前预习一下我之前的文章 构建webpack5知识体系【近万字总结】 本篇文章带你从0完成对一个老项目的工程化改造,学习本篇文章你将会掌握以下技能:
- 工程化基本概念
- webpack的熟练配置和优化技巧
- 对老项目工程化升级
本文章采用开源项目 ZBestPC 来改造。
项目源码地址:github.com/NewCoder798…
项目架构改造难点:
- 资源合并和拆分
- 拆分粒度如何
- 用webpack如何控制
问题分析
原始项目中存在的问题:
- css和js资源多,且全部采用同步加载,渲染时需多次请求和加载,降低页面加载性能
- css和js源码未压缩
- 开发模式陈旧,需要同时维护html、js和css, 代码逻辑和结构不清晰,迭代困难
- 项目不支持source-map,调试困难,手动维护source-map成本大
项目需求
- 第一阶段:项目webpack改造,使原生js项目能够支持模块开发及打包
- 第二阶段: Vue SPA (单页应用)改造,使项目能够使用Vue进行单页应用开发
- 第三阶段: Vue MPA (多页应用)改造,使项目能够使用Vue进行多页应用开发
项目webpack改造
项目初始化
目录规划
创建npm项目安装webpack依赖
npm init -y
npm install webpack webpack-cli
创建js入口文件
创建webpack配置文件
// webpack.config.js
const path = require("path");
module.exports = {
mode: "development",
entry: "./src/index.js",
output: {
filename: "bundle.js",
path: path.resolve(__dirname, "./dist"),
},
};
配置package.json命令
// package.json
{
"name": "pc_update",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"build": "webpack"
},
"keywords": [],
"author": "",
"license": "ISC"
}
执行 npm run build 生成结果
npm run build
添加html页面,引入打包后的js文件。
出现打印信息则表示成功。
本小结源码地址
首页移植
资源文件拷贝
资源文件拷贝,将源码中html/js/ css/img文件拷贝至新项目中:
- 将index.html拷贝至src/index.html
- 将js目录拷贝至src/js
- 将css目录拷贝至src/css
- 将img目录拷贝至src/img
删除引用
删除src/index.html 中所有link 和script的引用。
需要注意的是下面这一段不是外链引入的不能删除。
安装插件配置信息
安装html-webpack-plugin插件
npm install html-webpack-plugin --save-dev
在webpack.config.js添加html-webpack-plugin配置信息
// webpack.config.js
const HtmlWebpackPlugin = require("html-webpack-plugin");
const path = require("path");
module.exports = {
mode: "development",
// 新修改entry
entry: {
bundle: "./src/index.js",
},
// 新修改output
output: {
filename: "[name].js",
path: path.resolve(__dirname, "./dist"),
},
// 添加插件信息
plugins: [
new HtmlWebpackPlugin({
filename: "index.html",
template: "./src/index.html",
}),
],
};
重新执行 npm run build 会在dist目录下生成两个文件。
index.html文件中会异步引入bundle.js文件。
添加css引用
在src/index.js中添加 css 引用
import './css/public.css'
import './css/index.css'
安装css-loader、style-loader
npm install css-loader style-loader -D
添加css-loader、style-loader配置
// webpack.config.js
module.exports = {
// ...省略其他配置...
module: {
rules: [
{
test: /\.css$/i,
use: ["style-loader", "css-loader"],
},
],
},
};
添加图片处理
图片处理采用webpack内置 asset 模块,添加文件处理配置 parser 和 generator。
module: {
// ...省略其他配置...
rules: [
// ...省略其他配置...
// 图片处理
{
test: /\.(png|jpg|svg|jpeg|gif)$/i,
type: "asset",
parser: {
// 如果一个模块源码大小小于 maxSize,那么模块会被作为一个 Base64 编码的字符串注入到包中, 否则模块文件会被生成到输出的目标目录中。
dataUrlCondition: {
maxSize: 8 * 1024,
},
},
// 输出目录
generator: {
filename: "images/[name].[hash:6][ext]",
},
},
],
},
执行npm run build命令,发现有两处报错。
这里需要注意的是:它源码包中没有这两个图片,所以我们需要注释掉这两断引入图片的css代码。
重新执行npm run build,打包成功。
引用js文件
在src/index.js中添加js引用
import './js/jquery-1.12.4.min'
import './js/public'
import './js/nav'
import './js/jquery.flexslider-min'
执行npm run build,打包好后我们在浏览器打开dist/html文件,发现jquery的$没定义。
我们来解决一下jquery的$没定义的这个问题。
安装jquery和flexslider。
npm install -S jquery flexslider -D
在webpack.config.js插件中使用 ProvidePlugin 配置jquery。
plugins: [
// ...省略...
new webpack.ProvidePlugin({
$: 'jquery',
jQuery: 'jquery',
}),
],
修改src/index.js模块引入。
// src/index.js
import './css/public.css'
import './css/index.css'
import 'jquery'
import './js/public'
import './js/nav'
在 "删除引用" 的小章节中,我们src/index.html中留下一段js代码。
我们将这段代码移入到src/js/nav.js中,并且引入flexslider。
// src/js/nav.js
// 引入flexslider
import 'flexslider'
// 导航固定顶部
$(function () {
$(window).scroll(function () {
var ws = $(window).scrollTop();
if (ws > 60) {
$(".head")
.addClass("ding")
.css({ background: "rgba(255,255,255," + ws / 300 + ")" });
} else {
$(".head").removeClass("ding").css({ background: "#fff" });
}
});
});
$(function () {
$("#home_slider").flexslider({
animation: "slide",
controlNav: true,
directionNav: true,
animationLoop: true,
slideshow: true,
slideshowSpeed: 2000,
useCSS: false,
});
});
重新打包npm run build,来看一下控制台已经不报$没定义的错误了,不过还存在着大量图片找不到,这是因为我们src/index.html的图片地址路径不正确,我们来替换一下。
替换完成后重新打包,在浏览器中打开dist/html页面,会看到报错全部消失了,页面也完美呈现了。
源码地址
登录页移植
通过对首页的移植,已经大致明白了移植的流程。这里只简单写一下。
插件配置
plugins: [
// ...省略...
new HtmlWebpackPlugin({
filename: "login.html",
template: "./src/login.html",
}),
],
引入login页面link文件
大家可以参考源代码自己移植一下。
源码地址
项目优化
js分离
存在的问题
目前index.html和login.html同时引用了bundle.js,bundle.js对应src/index.js, 该文件同时引用了index.html
和login.html的依赖资源,这样会导致src/index.js随项目规模扩大越来越臃肿。
此时的bundle.js大小为466kb。
要解决这个问题需要指定index.html和login.html分别引用不同的js文件,这就需要涉及webpack多入口配置。
多入口配置
创建login.js,将login相关内容写入,同时删除index.js中login相关内容。
配置webpack文件为多入口,同时配置chunks。
执行npm run build,发现文件从原来的466kb减小到53.8kb。
增加开发模式
dev-server
目前我们改造的项目每次修改内容都需要重新打包,所以这阶段我们需要做的是支持开发模式提升效率。 安装 webpack-dev-server。
npm i -D webpack-dev-server
添加webpack-dev-server配置。
module.exports = {
/** ...省略其他配置... **/
devServer: {
static: {
directory: path.resolve(__dirname, '../dist'),
},
compress: true,
port: 8000,
hot: true,
},
/** ...省略其他配置... **/
}
添加启动命令。
执行npm run start,打开localhost:8000,发现图片路径丢失,那么我们接下来我们来解决这个问题。
copy-plugin
我们从上一节的截图可以看到页面的图片路径不正确,所以图片加载不出来,我们安装 CopyWebpackPlugin 插件将src里面的图片都copy到dist里面。 安装copy-webpack-plugin插件
npm i -D copy-webpack-plugin
添加webpack-dev-server配置。
const HtmlWebpackPlugin = require("html-webpack-plugin");
const path = require("path");
const webpack = require("webpack");
const CopyPlugin = require("copy-webpack-plugin");
module.exports = {
/** ...省略其他配置... **/
plugins: [
/** ...省略其他配置... **/
new CopyPlugin({
patterns: [
{
from: path.resolve(__dirname, "./src/img"),
to: path.resolve(__dirname, "./dist/img"),
},
],
}),
],
};
替换图片路径。
重新执行npm run start,打开页面,发现已经没问题了。
本小节源码地址
剥离css文件
安装插件 MiniCssExtractPlugin。
npm install --save-dev mini-css-extract-plugin
修改mini-css-extract-plugin配置。
const HtmlWebpackPlugin = require("html-webpack-plugin");
const path = require("path");
const webpack = require("webpack");
const CopyPlugin = require("copy-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
module.exports = {
/** ...省略其他配置... **/
module: {
rules: [
{
test: /\.css$/i,
use: [
MiniCssExtractPlugin.loader,
{ loader: 'css-loader' },
],
},
],
},
plugins: [
new MiniCssExtractPlugin({
filename: "css/[name].css",
chunkFilename: "css/[name].chunk.css",
}),
],
};
重新打包,css文件已经抽离出来了,当然js文件大小也会减少,可以自己体验一下。
文件压缩
js和css文件压缩
安装插件 UglifyJsPlugin 和 OptimizeCssAssetsPlugin
npm install --save-dev uglifyjs-webpack-plugin
npm install --save-dev css-minimizer-webpack-plugin
添加webpack压缩配置。
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
const config = {
/** ...省略其他配置... **/
optimization: {
minimize: true, //代码压缩
minimizer: [
new UglifyJsPlugin(),
new CssMinimizerPlugin(),
],
},
}
本小节源码地址:github.com/linhexs/old…
treeshaking触发条件
- 通过解构的方式获取方法, 可以触发treesiaking
- 调用的npm包必须使用ESM
- 同一文件的treeshaking有触发条件,条件就是mode=production
- 一定要注意使用解构来加载模块
代码分割
这里我们在src/index.js中加入了一个lodash。
分割前效果:
利用 splitChunks 来进行拆分。
const HtmlWebpackPlugin = require("html-webpack-plugin");
const path = require("path");
const webpack = require("webpack");
const CopyPlugin = require("copy-webpack-plugin");
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
module.exports = {
/** ...省略其他配置... **/
optimization: {
minimize: true, //代码压缩
minimizer: [
new UglifyJsPlugin(),
new CssMinimizerPlugin(),
],
// 分包
splitChunks: {
minSize: 5 * 1024,
chunks: 'all',
name: 'common',
automaticNameDelimiter: '_',
cacheGroups: {
jquery: {
name: 'jquery',
chunks: 'all',
test: /jquery\.js/,
},
'lodash': {
name: 'lodash',
chunks: 'all',
test: /lodash/,
}
},
},
},
};
拆分后效果,只看红框内标出的即可:
可以看出包单个包明显体积减少了。
本小节源码地址:github.com/linhexs/old…
公共文件提取
添加ejs抽取header和footer部分。
在html页面引入ejs。
添加ejs-loader解析。
npm install ejs-loader -D
module: {
/** ...省略其他配置... **/
rules: [
{
test: /\.ejs/,
loader: "ejs-loader",
options: {
esModule: false,
},
},
],
},
清空dist目录
使用CleanWebpackPlugin插件对dist目录清理。
npm install clean-webpack-plugin -D
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
module.exports = {
/** ...省略其他配置... **/
plugins: [
new CleanWebpackPlugin(),
],
};
webpack配置完整代码
const HtmlWebpackPlugin = require("html-webpack-plugin");
const path = require("path");
const webpack = require("webpack");
const CopyPlugin = require("copy-webpack-plugin");
const UglifyJsPlugin = require("uglifyjs-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
module.exports = {
mode: "development",
entry: {
index: "./src/index.js",
login: "./src/login.js",
},
devServer: {
static: {
directory: path.resolve(__dirname, "../dist"),
},
compress: true,
port: 8000,
hot: true,
},
output: {
filename: "js/[name].js",
path: path.resolve(__dirname, "./dist"),
},
module: {
rules: [
{
test: /\.css$/i,
use: [MiniCssExtractPlugin.loader, { loader: "css-loader" }],
},
{
test: /\.(png|jpg|svg|jpeg|gif)$/i,
type: "asset",
parser: {
// 如果一个模块源码大小小于 maxSize,那么模块会被作为一个 Base64 编码的字符串注入到包中, 否则模块文件会被生成到输出的目标目录中。
dataUrlCondition: {
maxSize: 8 * 1024,
},
},
// 输出目录
generator: {
filename: "images/[name].[hash:6][ext]",
},
},
{
test: /\.ejs/,
loader: "ejs-loader",
options: {
esModule: false,
},
},
],
},
optimization: {
minimize: true, //代码压缩
minimizer: [new UglifyJsPlugin(), new CssMinimizerPlugin()],
splitChunks: {
minSize: 5 * 1024,
chunks: "all",
name: "common",
automaticNameDelimiter: "_",
cacheGroups: {
jquery: {
name: "jquery",
chunks: "all",
test: /jquery\.js/,
},
lodash: {
name: "lodash",
chunks: "all",
test: /lodash/,
},
},
},
},
plugins: [
new MiniCssExtractPlugin({
filename: "css/[name].css",
chunkFilename: "css/[name].chunk.css",
}),
new HtmlWebpackPlugin({
filename: "index.html",
template: "./src/index.html",
chunks: ["index"],
}),
new HtmlWebpackPlugin({
filename: "login.html",
template: "./src/login.html",
chunks: ["login"],
}),
new webpack.ProvidePlugin({
$: "jquery",
jQuery: "jquery",
}),
new CopyPlugin({
patterns: [
{
from: path.resolve(__dirname, "./src/img"),
to: path.resolve(__dirname, "./dist/img"),
},
],
}),
new CleanWebpackPlugin(),
],
};
Vue SPA应用改造
转移webpack文件
我们新建一个build文件,将webpack配置移入进去。
对应要修改我们里面的输出路径,这里我就将源码都粘贴出来,大家对应修改即可。
const HtmlWebpackPlugin = require("html-webpack-plugin");
const path = require("path");
const webpack = require("webpack");
const CopyPlugin = require("copy-webpack-plugin");
const UglifyJsPlugin = require("uglifyjs-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
module.exports = {
mode: "development",
entry: {
index: path.resolve(__dirname, '../src/index.js'),
login: path.resolve(__dirname, '../src/login.js'),
},
devServer: {
static: {
directory: path.resolve(__dirname, "../dist"),
},
compress: true,
port: 8000,
hot: true,
},
output: {
filename: "js/[name].js",
path: path.resolve(__dirname, "../dist"),
},
module: {
rules: [
{
test: /\.css$/i,
use: [MiniCssExtractPlugin.loader, { loader: "css-loader" }],
},
{
test: /\.(png|jpg|svg|jpeg|gif)$/i,
type: "asset",
parser: {
// 如果一个模块源码大小小于 maxSize,那么模块会被作为一个 Base64 编码的字符串注入到包中, 否则模块文件会被生成到输出的目标目录中。
dataUrlCondition: {
maxSize: 8 * 1024,
},
},
// 输出目录
generator: {
filename: "images/[name].[hash:6][ext]",
},
},
{
test: /\.ejs/,
loader: "ejs-loader",
options: {
esModule: false,
},
},
],
},
optimization: {
minimize: true, //代码压缩
minimizer: [new UglifyJsPlugin(), new CssMinimizerPlugin()],
splitChunks: {
minSize: 5 * 1024,
chunks: "all",
name: "common",
automaticNameDelimiter: "_",
cacheGroups: {
jquery: {
name: "jquery",
chunks: "all",
test: /jquery\.js/,
},
lodash: {
name: "lodash",
chunks: "all",
test: /lodash/,
},
},
},
},
plugins: [
new MiniCssExtractPlugin({
filename: "css/[name].css",
chunkFilename: "css/[name].chunk.css",
}),
new HtmlWebpackPlugin({
filename: "index.html",
template: "./src/index.html",
chunks: ["index"],
}),
new HtmlWebpackPlugin({
filename: "login.html",
template: "./src/login.html",
chunks: ["login"],
}),
new webpack.ProvidePlugin({
$: "jquery",
jQuery: "jquery",
}),
new CopyPlugin({
patterns: [
{
from: path.resolve(__dirname, "../src/img"),
to: path.resolve(__dirname, "../dist/img"),
},
],
}),
new CleanWebpackPlugin(),
],
};
修改package.json中的scripts命令。
"scripts": {
"start": "webpack-dev-server --config build/webpack.config.js",
"build": "webpack --config build/webpack.config.js"
},
接入vue
安装Vue相关的依赖
这里直接升级为vue3。
npm install vue -D
npm install @vue/compiler-sfc vue-template-compiler vue-loader -D
创建vue相关代码
创建src/main.js文件
// src/main.js
import { createApp } from "vue";
import App from "./App.vue";
createApp(App).mount('#app')
创建src/App.vue文件
// src/App.vue
<template>
<div id="app">{{ message }}</div>
</template>
<script>
export default {
data() {
return {
message: "Hello vue",
};
},
};
</script>
<style></style>
创建模版文件
创建public/index.html文件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>vue</title>
</head>
<body>
<div id="app"></div>
</body>
</html>
创建vue配置命令
创建build/webpack.config.vue.js文件,并写入配置。
const HtmlWebpackPlugin = require("html-webpack-plugin");
const path = require("path");
const webpack = require("webpack");
const CopyPlugin = require("copy-webpack-plugin");
const UglifyJsPlugin = require("uglifyjs-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const { VueLoaderPlugin } = require("vue-loader");
module.exports = {
mode: "development",
entry: path.resolve(__dirname, "../src/main.js"),
devServer: {
static: {
directory: path.resolve(__dirname, "../dist"),
},
compress: true,
port: 8001,
hot: true,
},
output: {
filename: "js/[name].js",
path: path.resolve(__dirname, "../dist"),
},
module: {
rules: [
{
test: /\.vue$/,
loader: "vue-loader",
},
{
test: /\.css$/i,
use: [MiniCssExtractPlugin.loader, { loader: "css-loader" }],
},
{
test: /\.(png|jpg|svg|jpeg|gif)$/i,
type: "asset",
parser: {
// 如果一个模块源码大小小于 maxSize,那么模块会被作为一个 Base64 编码的字符串注入到包中, 否则模块文件会被生成到输出的目标目录中。
dataUrlCondition: {
maxSize: 8 * 1024,
},
},
// 输出目录
generator: {
filename: "images/[name].[hash:6][ext]",
},
},
{
test: /\.ejs/,
loader: "ejs-loader",
options: {
esModule: false,
},
},
],
},
optimization: {
minimize: true, //代码压缩
minimizer: [new UglifyJsPlugin(), new CssMinimizerPlugin()],
splitChunks: {
minSize: 5 * 1024,
chunks: "all",
name: "common",
automaticNameDelimiter: "_",
cacheGroups: {
jquery: {
name: "jquery",
chunks: "all",
test: /jquery\.js/,
},
lodash: {
name: "lodash",
chunks: "all",
test: /lodash/,
},
},
},
},
plugins: [
new MiniCssExtractPlugin({
filename: "css/[name].css",
chunkFilename: "css/[name].chunk.css",
}),
new HtmlWebpackPlugin({
filename: "index.html",
template: path.resolve(__dirname, "../public/index.html"),
}),
new webpack.ProvidePlugin({
$: "jquery",
jQuery: "jquery",
}),
new CopyPlugin({
patterns: [
{
from: path.resolve(__dirname, "../src/img"),
to: path.resolve(__dirname, "../dist/img"),
},
],
}),
new CleanWebpackPlugin(),
new VueLoaderPlugin(),
],
};
添加vue打包命令
"scripts": {
"start:vue": "webpack-dev-server --config build/webpack.vue.config.js",
"build:vue": "webpack --config build/webpack.vue.config.js"
},
执行npm run start:vue
页面出现效果则成功。
接入路由
安装依赖
安装 vue-router 依赖。
npm install vue-router@4 -D
添加配置
添加 src/router.js。
import { createRouter, createWebHashHistory } from 'vue-router';
import Home from './Home.vue';
import Login from './Login.vue';
const router = createRouter({
history: createWebHashHistory(),
routes: [
{
path: '/',
redirect: '/home',
},
{
path: '/home',
name: 'Home',
component: Home,
},
{
path: '/login',
name: 'Login',
component: Login,
},
],
});
export default router;
添加vue-router相关内容
修改 src/main.js,添加 vue-router 相关内容。
// src/main.js
import { createApp } from "vue";
import App from "./App.vue";
import router from './router.js';
createApp(App).use(router).mount('#app')
修改App.vue。
// src/app.vue
<template>
<router-view />
</template>
<script></script>
<style>
#app {
width: 100%;
height: 100%;
}
</style>
添加组件
添加Login组件和Home组件,将原来的登录页和首页移植进去,看文章源码,这里就不仔细讲了。
源码地址
Vue MPA应用改造
这里先留个悬念,过几天再更新,其实就是配置一个多入口即刻,大家可以自己尝试一下,大家也可以根据现有知识完成对项目React的升级改造和把其他页面移植过来。
webpack优化相关知识点补充
webpack打包优化方向
- 打包速度:优化打包速度,主要提升开发效率,更快的打包构建过程。
- 打包体积:优化打包体积,主要是提升产品的使用体验,降低服务器资源成本,更快的页面加载,同时也可以让打包更快
webpack打包速度优化
webpack 进行打包速度优化有七种常用手段
优化 loader 搜索范围
对于 loader 来说,影响打包效率首当其冲必属 Babel 了。因为 Babel 会将代码转为字符串生成 AST,然后对 AST 继续进行转变 最后再生成新的代码,项目越大,转换代码越多,效率就越低。优化正则匹配、使用 include 和 exclude 指定需要处理的文件,忽略不需要处理的文件。
rules: [{
// 优化正则匹配
test: /\.js$/,
// 指定需要处理的目录
include: path.resolve(__dirname, 'src')
// 理论上只有include就够了,但是某些情况需要排除文件的时候可以用这个,排除不需要处理文件 // exclude: []
}]
多进程/多线程
受限于 node 是单线程运行的,所以 webpack 在打包的过程中也是单线程的,特别是在执行 loader 的时候,长时间编译的任务很多,这样就会导致等待的情况。我们可以使用一些方法将 loader 的同步执行转换为并行,这样就能充分利用系统资源来提高打包速度了。
{
test: /\.js?$/,
exclude: /node_modules/,
use: [
{
loader: "thread-loader",
options: {
workers: 3, // 进程 3 个 }
},
},
],
};
分包
在使用 webpack 进行打包时候,对于依赖的第三方库,比如 vue,vuex 等这些不会修改的依赖,我们可以让它和我们自己编写的代码分开打包,这样做的好处是每次更改我本地代码的文件的时候,webpack 只需要打包我项目本身的文件代码,而不会再 去编译第三方库,那么第三方库在第一次打包的时候只打包一次,以后只要我们不升级第三方包的时候,那么 webpack 就不会 对这些库去打包,这样可以快速提高打包的速度。因此为了解决这个问题,DllPlugin 和 DllReferencePlugin 插件就产生了。这种方式可以极大的减少打包类库的次数,只有当类库更新版本才需要重新打包,并且也实现了将公共代码抽离成单独文件的优化方案。
// webpack.dll.conf.js
const path = require("path");
const webpack = require("webpack");
const UglifyJsPlugin = require("uglifyjs-webpack-plugin");
module.exports = {
mode: "production",
devtool: false,
entry: {
vue: ["vue", "vue-router", "iscroll", "vuex"],
},
output: {
path: path.join(__dirname, "../dist"),
filename: "lib/[name]_[hash:4].dll.js",
library: "[name]_[hash:4]",
},
performance: {
hints: false,
maxAssetSize: 300000, //单文件超过300k,命令行告警 maxEntrypointSize: 300000, //首次加载文件总和超过300k,命令行告警
},
optimization: {
minimizer: [
new UglifyJsPlugin({
parallel: true, // 开启多线程并行
}),
],
},
plugins: [
new webpack.DllPlugin({
context: __dirname,
path: path.join(__dirname, "../dist/lib", "[name]-manifest.json"),
name: "[name]_[hash:4]",
}),
],
};
// webpack.prod.cong.js
plugins: [
new webpack.DllReferencePlugin({
context: __dirname,
manifest: require("../dist/lib/vue-manifest.json"),
}),
];
开启缓存
当设置 cache.type: “filesystem” 时,webpack 会在内部以分层方式启用文件系统缓存和内存缓存,将处理结果结存放到内存中。
cache: {
type: "filesystem"
// cacheDirectory 默认路径是 node_modules/.cache/webpack
// cacheDirectory: path.resolve(__dirname, '.temp_cache')
},
打包分析工具
显示测量打包过程中各个插件和 loader 每一步所消耗的时间,然后让我们可以有针对的分析项目中耗时的模块对其进行处理。
npm install speed-measure-webpack-plugin -D
// webpack.prod.config.js
const SpeedMeatureWebpackPlugin = require("speed-measure-webpack-plugin");
const smp = new SpeedMeatureWebpackPlugin();
var webpackConfig = merge(baseWebpackConfig,{})
--> 修改为下面格式
var webpackConfig = {...}
module.exports = webpackConfig
--> 修改为下面格式
module.exports = smp.wrap(merge(baseWebpackConfig, webpackConfig));
ignorePlugin
这是 webpack 内置插件, 它的作用是忽略第三方包指定目录,让这些指定目录不要被打包进去,防止在 import 或 require 调用时,生成以下正则表达式匹配的模块.
- requestRegExp 匹配( test )资源请求路径的正则表达式。
- contextRegExp( 可选 )匹配( test )资源上下文( 目录 )的正则表达式。
new webpack.IgnorePlugin({
resourceRegExp: /^\.\/test$/,
contextRegExp: /test$/,
})
优化文件路径
- alias:省下搜索文件的时间,让 webpack 更快找到路径
- mainFiles:解析目录时要使用的文件名
- extensions:指定需要检查的扩展名,配置之后可以不用在 require 或是 import 的时候加文件扩展名,会依次尝试添加扩展名进行匹配
resolve: {
extensions: ['.js', '.vue'],
mainFiles: ['index'],
alias: {
'@': resolve('src'),
}
}
webpack打包体积优化
webpack打包体积优化有11种常用优化手段
构建体积分析
npm run build 构建,会默认打开: http://127.0.0.1:8888/,可以看到各个包的体积,分析项目各模块的大小,可以按需优化。
npm install webpack-bundle-analyzer -D
const BundleAnalyzerPlugin = require("webpack-bundle-analyzer").BundleAnalyzerPlugin;
plugins:[
new BundleAnalyzerPlugin()
]
项目图片资源优化压缩处理
{
test: /\.(gif|png|jpe?g|svg|webp)$/i,
type: "asset/resource",
parser: {
dataUrlCondition: { maxSize: 8 * 1024 },
},
generator: {
filename: "images/[name].[hash:6][ext]",
},
use: [
{
loader: "image-webpack-loader",
options: {
mozjpeg: { progressive: true, quality: 65 },
optipng: {
enabled: false,
},
pngquant: {
quality: [0.5, 0.65],
speed: 4,
},
gifsicle: {
interlaced: false,
},
webp: {
quality: 75,
},
},
},
],
};
删除无用的 css 样式
有时候一些项目中可能会存在一些 css 样式被迭代废弃,需要将其删除,可以使用 purgecss-webpack-plugin 插件,该插件可以去除未使用的 css。
npm install purgecss-webpack-plugin -D
// webpack.prod.conf.js
const PurgeCSSPlugin = require("purgecss-webpack-plugin");
const glob = require("glob");
const PATHS = {
src: path.join(__dirname, "src"),
};
// plugins
new PurgeCSSPlugin({
paths: glob.sync(`${PATHS.src}/**/*`, { nodir: true }),
safelist: ["body"],
});
代码压缩
optimization: {
minimize: true, //代码压缩
minimizer: [
new UglifyJsPlugin(),
new CssMinimizerPlugin(),
],
splitChunks: {
minSize: 5 * 1024,
chunks: 'all',
name: 'common',
automaticNameDelimiter: '_',
cacheGroups: {
jquery: {
name: 'jquery',
chunks: 'all',
test: /jquery\.js/,
},
'lodash': {
name: 'lodash',
chunks: 'all',
test: /lodash/,
}
},
},
},
开启 Scope Hoisting
Scope Hoisting 又译作“作用域提升”。只需在配置文件中添加一个新的插件,就可以让 webpack 打包出来的代码文件更小、运行 的更快, Scope Hoisting 会分析出模块之间的依赖关系,尽可能的把打包出来的模块合并到一个函数中去,然后适当地重命名一些变量以防止命名冲突。
new webpack.optimize.ModuleConcatenationPlugin();
提取公共代码
将项目中的公共模块提取出来,可以减少代码的冗余度,提高代码的运行效率和页面的加载速度。
new webpack.optimize.CommonsChunkPlugin(options);
代码分离
代码分离能够将工程代码分离到各个文件中,然后按需加载或并行加载这些文件,也用于获取更小的 bundle,以及控制资源加载优先级,在配置文件中配置多入口,输出多个 chunk。
// 多入口配置 最终输出两个chunk
module.exports = {
entry: {
index: 'index.js',
login: 'login.js'
},
output: {
//对于多入口配置需要指定[name]否则会出现重名问题 filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist')
}
};
Tree-shaking
tree shaking 是一个术语,通常用于描述移除 JavaScript 上下文中的未引用代码( dead-code )。它依赖于 ES2015 模块语法的静态结构 特性,例如 import 和 export。
CDN 加速
CDN是构建在网络之上的内容分发网络,依靠部署在各地的边缘服务器,通过中心平台的负载均衡、内容分发、调度等功能模块,使用户就近获取所需内容,降低网络拥塞,提高用户访问响应速度和命中率。CDN 的关键技术主要有内容存储和分发技术。在项目中以 CDN 的方式加载资源,项目中不需要对资源进行打包,大大减少打包后的文件体积 。
生产环境关闭 sourceMap
sourceMap 本质上是一种映射关系,打包出来的 js 文件中的代码可以映射到代码文件的具体位置,这种映射关系会帮助我们直接找到在源代码中的错误。但这样会使项目打包速度减慢,项目体积变大,可以在生产环境关闭 sourceMap 。
按需加载
在开发项目的时候,项目中都会存在十几甚至更多的路由页面。如果我们将这些页面全部打包进一个文件的话,虽然将多个请求合并了,但是同样也加载了很多并不需要的代码,耗费了更长的时间。那么为了页面能更快地呈现给用户,我们肯定是希望页面能加载的文件体积越小越好,这时候我们就可以使用按需加载,将每个路由页面单独打包为一个文件。以下是常见的按需加载的场景。
- 路由组件按需加载
- 按需加载需引入第三方组件
- 对于一些插件,如果只是在个别组件中用的到,也可以不要在 main.js 里面引入,而是在组件中按需引入
转载自:https://juejin.cn/post/7220463381492514853