webpack的工作原理
1. Tapable
- webpack是基于事件流机制,他的工作原理就是将一个一个的插件串联起来,这一功能的实现就是基于Tapable
- webpack在编译过程中负责编译的Compiler以及负责创建bundle的Compilation都是tapable的实例。
+ webpack中的插件机制
+ webpack的插件机制大概分为三个阶段
1. 创建: webpack在内部创建各种钩子
2. 注册: 插件将自己的方法注册到对应的钩子上,交给webpack
3. 调用: webpack在编译的过程中,在适当的时机调用钩子函数,同时也执行了插件内部的方法
工作原理
流程
- 初始化阶段
- 根据yargs解析命令行的参数,结合配置文件,初始化Compiler实例,负责监文件以及启动编译
- 依次调用插件的apply方法,便于插件可以监听后续的事件,同时将Compiler对象传入插件中,方便插件在适当的实际触发对应的事件。
- 读取配置文件的entry,为给一个入口实例化一个entryPlugin,为后续的递归解析做准备
- 编译阶段
- 调用Compiler的run方法,启动编译
- 当检测到有新的文件编译时会创建一个新的compilation,一个 Compilation 对象包含了当前的模块资源、编译生成资源、变化的文件等。
- make,一个新的compilation创建完成开始真正的编译
- 找到对应的入口文件,创建模块,使用所有配置的loader编译模块(在此操作中,会将代码解析成AST,进行相关的操作后,再将AST生成相应的代码),递归编译依赖的模块,得到入口和模块的依赖关系
- 结束阶段
- 封装,根据入口以及模块间的依赖关系,组合成一个一个的chunk,并添加在输出列表中
- emit,确定好输出列表, 根据output配置的路径和文件名生成对应的文件
- done
实现
let path = require('path');
// // config配置文件
let config = require(path.resolve('webpack.config.js'));
let Compiler = require('../lib/Compiler.js')
let compiler = new Compiler(config);
compiler.hooks.entryOption.call();
// 标识运行编译
compiler.run();
// Compiler类
let fs = require('fs');
let path = require('path');
let babylon = require('babylon');
let t = require('@babel/types');
let traverse = require('@babel/traverse').default;
let generator = require('@babel/generator').default;
let ejs = require('ejs');
let {SyncHook} = require('tapable')
// babylon 主要就是把源码 转换成ast
// @babel/traverse
// @babel/types
// @babel/generator
class Compiler {
constructor(config) {
// entry output
this.config = config;
// 需要保存入口文件的路径
this.entryId; // './src/index.js'
// 需要保存所有的模块依赖
this.modules = {};
this.entry = config.entry; // 入口路径
// 工作路径
this.root = process.cwd();
this.hooks = {
entryOption:new SyncHook(),
compile:new SyncHook(),
afterCompile:new SyncHook(),
afterPulgins:new SyncHook(),
run:new SyncHook(),
emit:new SyncHook(),
done:new SyncHook()
}
// 如果传递了plugins参数
let plugins = this.config.plugins;
if(Array.isArray(plugins)){
plugins.forEach(plugin => {
plugin.apply(this);
});
}
this.hooks.afterPulgins.call();
}
getSource(modulePath) { // ./index.less
let rules = this.config.module.rules;
let content = fs.readFileSync(modulePath, 'utf8');
// 拿到每个规则来处理
for (let i = 0; i < rules.length; i++) {
let rule = rules[i];
let { test, use } = rule;
let len = use.length - 1;
if (test.test(modulePath)) { // 这个模块需要通过loader来转化
// loader获取对应的loader函数
function normalLoader() {
let loader = require(use[len--]);
// 递归调用loader 实现转化功能
content = loader(content);
if(len>=0){
normalLoader();
}
}
normalLoader();
}
}
return content
}
// 解析源码
parse(source, parentPath) { // AST解析语法树
let ast = babylon.parse(source);
let dependencies = [];// 依赖的数组
traverse(ast, {
CallExpression(p) { // a() require()
let node = p.node; // 对应的节点
if (node.callee.name === 'require') {
node.callee.name = '__webpack_require__';
let moduleName = node.arguments[0].value; // 取到的就是模块的引用名字
moduleName = moduleName + (path.extname(moduleName) ? '' : '.js');
moduleName = './' + path.join(parentPath, moduleName); 'src/a.js'
dependencies.push(moduleName);
node.arguments = [t.stringLiteral(moduleName)];
}
}
});
let sourceCode = generator(ast).code;
return { sourceCode, dependencies }
}
// 构建模块
buildModule(modulePath, isEntry) {
// 拿到模块的内容
let source = this.getSource(modulePath);
// 模块id modulePath = modulePath- this.root src/index.js
let moduleName = './' + path.relative(this.root, modulePath);
if (isEntry) {
this.entryId = moduleName; // 保存入口的名字
}
// 解析需要把source源码进行改造 返回一个依赖列表
let { sourceCode, dependencies } = this.parse(source, path.dirname(moduleName)); // ./src
// 把相对路径和模块中的内容 对应起来
this.modules[moduleName] = sourceCode;
dependencies.forEach(dep => { // 附模块的加载 递归加载
this.buildModule(path.join(this.root, dep), false);
});
}
emitFile() { // 发射文件
// 用数据 渲染我们的
// 拿到输出到哪个目录下 输出路径
let main = path.join(this.config.output.path, this.config.output.filename);
// 模板的理解
let templateStr = this.getSource(path.join(__dirname, 'main.ejs'));
let code = ejs.render(templateStr, { entryId: this.entryId, modules: this.modules });
this.assets = {}
// 资源中 路径对应的代码
this.assets[main] = code;
fs.writeFileSync(main, this.assets[main]);
}
run() {
this.hooks.run.call();
// 执行 并且创建模块的依赖关系
this.hooks.compile.call();
this.buildModule(path.resolve(this.root, this.entry), true);
this.hooks.afterCompile.call();
// 发射一个文件 打包后的文件
this.emitFile();
this.hooks.emit.call();
this.hooks.done.call();
}
}
module.exports = Compiler
tree-shaking的原理
- tree-shaking关注与无用模块的消除,消除那些引用了但并没有被使用的模块, 或许会对 bundle 产生显著的体积优化。
使用tree-shaking优化,需要:
+ 必须基于es6语法,即(import和export)
+ 在package.json中配置sideEffects入口
+ 引入一个能够删除未引用代码(dead code)的压缩工具(minifier)(例如 UglifyJSPlugin)
- 借用官网的一个🌰:
可以看出,webpack在打包过程中,会在未被导入的模块中加一行注释'unused harmony export
square',标志代码为'dead code',但这一过程也仅仅只是标记,因此再不做处理的情况下,未使用的模块仍然在打包的bundle中。
+ 为了解决这一问题,需要配合开启压缩,将这些无用的代码去除
+ 切换到production模式,mode: production
+ 在打包的命令行中加参数 --optimize-minimize
+ 手动使用UglifyJSPlugin插件
- ES6模块依赖关系是确定的,和运行时的状态无关,可以进行可靠的静态分析
- webpack从第2版本就开始支持Tree-shaking的功能。但如果我们模块中含有具有副作用的模块,webpack的Tree-shaking就不那么完美了,此时就需要在package.json中配置sideEffects入口来解决这个问题。
SplitChunks
为什么要进行代码分割
- 相同的资源被重复的加载,浪费用户的流量和服务器的成本;
- 每个页面需要加载的资源太大,导致网页首屏加载缓慢,影响用户体验。
- 如果能把公共代码抽离成单独文件进行加载能进行优化,可以减少网络传输流量,降低服务器成本
分割的方式
-
- 直接在配置参数entry分离
-
- 懒加载,比如当用户进行了某个操作后,动态的使用import导入
-
- webpack的配置参数 原理: 解析模块的依赖关系,根据optimation中splitChunks的配置把那些匹配到配置规则的模块单独打包成一个文件,通过jsonp加载。
optimization: {
splitChunks: {
chunks: "all",//默认作用于异步chunk,值为all/initial/async
minSize: 30000, //默认值是30kb,代码块的最小尺寸
minChunks: 1, //被多少模块共享,在分割之前模块的被引用次数
maxAsyncRequests: 5, //按需加载最大并行请求数量
maxInitialRequests: 3, //一个入口的最大并行请求数量
name: true, //打包后的名称,默认是chunk的名字通过分隔符(默认是~)分隔开,如vendor~
automaticNameDelimiter: '~',//默认webpack将会使用入口名和代码块的名称生成命名,比如 'vendors~main.js'
cacheGroups: { //设置缓存组用来抽取满足不同规则的chunk,下面以生成common为例
vendors: {
chunks: "initial",
test: /node_modules/,//条件
priority: -10 ///优先级,一个chunk很可能满足多个缓存组,会被抽取到优先级高的缓存组中,为了能够让自定义缓存组有更高的优先级(默认0),默认缓存组的priority属性为负值.
},
commons: {
chunks: "initial",
minSize: 0,//最小提取字节数
minChunks: 2, //最少被几个chunk引用
priority: -20,
reuseExistingChunk: true// 如果该chunk中引用了已经被抽取的chunk,直接引用该chunk,不会重复打包代码
}
}
},
}
Scope Hoisting
为什么这么做
- 对于功能不复杂,相对比较小的模块,在运行时,模块越多创建的函数作用域就越多,大量作用域包裹代码会导致体积增大,内存开销跟着加大,因此可以使用scope Hoisting打包出更小、运行的更快的代码。
原理
scope hoisting的原理: 分析出模块之间的依赖关系,根据optimization中splitChunks的配置,
+ 这个功能在mode为production下默认开启,开发环境要用 webpack.optimize.ModuleConcatenationPlugin插件
配置:
module.exports = {
resolve: {
// 针对 Npm 中的第三方模块优先采用 jsnext:main 中指向的 ES6 模块化语法的文件
// 考虑到 Scope Hoisting 依赖源码需采用 ES6 模块化语法,还需要配置 mainFields。
mainFields: ['jsnext:main', 'browser', 'main']
},
plugins: [
// 开启 Scope Hoisting
new webpack.optimize.ModuleConcatenationPlugin(),
],
};
+ 对于采用了'非 ES6' 模块化语法的代码,Webpack 会降级处理不使用 Scope Hoisting 优化,为了知道 Webpack 对哪些代码做了降级处理,
在启动 Webpack 时带上 '--display-optimization-bailout' 参数
hmr原理
- 模块热替换的原理和自动刷新原理类似,都需要往要开发的网页中注入一个代理客户端用于连接 DevServer 和网页
1. webpack对文件系统watch打包到内存中
+ webpack-dev-middleware调用webpack的api对文件系统进行watch,当文件发生变化时,webpack会重新进行打
包编译,存放到内存中,
+ 这一功能的实现基于memory-fs,memory-fs 是 webpack-dev-middleware 的一个依赖库,
webpack-dev-middleware 将 webpack 原本的 outputFileSystem 替换成了MemoryFileSystem 实例,这样代码就将输出到内存中。
2. devServer 通知浏览器端文件发生改变
+ 当启动devServer时,服务器与浏览器建立了一个web-socket长链接,
+ webpack-dev-server调用webpack的api并监听打包编译的各个阶段
+ 在监听到compiler的done事件执行完后,webpack-dev-server通过——sendStatus方法将新打包模块的hash值发送给浏览器
compiler.plugin('done', (stats) => {
// stats.hash 是最新打包文件的 hash 值
this._sendStats(this.sockets, stats.toJson(clientStats));
this._stats = stats;
});
...
Server.prototype._sendStats = function (sockets, stats, force) {
if (!force && stats &&
(!stats.errors || stats.errors.length === 0) && stats.assets &&
stats.assets.every(asset => !asset.emitted)
) { return this.sockWrite(sockets, 'still-ok'); }
// 调用 sockWrite 方法将 hash 值通过 websocket 发送到浏览器端
this.sockWrite(sockets, 'hash', stats.hash);
if (stats.errors.length > 0) { this.sockWrite(sockets, 'errors', stats.errors); }
else if (stats.warnings.length > 0) { this.sockWrite(sockets, 'warnings', stats.warnings); } else { this.sockWrite(sockets, 'ok'); }
};
3 注入的代理客户端接收到服务器的消息并作出相应
+ webpack-dev-server 修改了webpack 配置中的 entry 属性,在里面添加了 webpack-dev-client
的代码,这样在最后的 bundle.js 文件中就会接收 websocket 消息的代码了。
+ 在接收到hash事件后,将传入的hash暂存,等待接收到ok事件,会根据webpack的配置文件中关于hot的配置,
决定是刷新当前页面还是热更新,如果需要热更新,客户端将会向服务端发送webpackHotUpdate消息
4. webpack 接收到最新 hash 值验证并请求模块代码
+ devServer监听客户端传来的webpackHotUpdate事件,调用调用webpack/lib/HotModuleReplacement.runtime
中的 check 方法,检测是否有新的更新。
+ 在 check 过程中会调用hotDownloadManifest 和 hotDownloadUpdateChunk两个方法
+ hotDownloadManifest: 发送ajax,返回发生变化的模块以及最新的hash
+ hotDownloadUpdateChunk: 使用JSONP的方式请求最新的代码
5. HotModuleReplacement.runtime 的hotApply对模块进行热更新
function hotApply() {
// ...
var idx;
var queue = outdatedModules.slice();
while(queue.length > 0) {
moduleId = queue.pop();
module = installedModules[moduleId];
// ...
// remove module from cache
delete installedModules[moduleId];
// when disposing there is no need to call dispose handler
delete outdatedDependencies[moduleId];
// remove "parents" references from all children
for(j = 0; j < module.children.length; j++) {
var child = installedModules[module.children[j]];
if(!child) continue;
idx = child.parents.indexOf(moduleId);
if(idx >= 0) {
child.parents.splice(idx, 1);
}
}
}
// ...
// insert new code
for(moduleId in appliedUpdate) {
if(Object.prototype.hasOwnProperty.call(appliedUpdate, moduleId)) {
modules[moduleId] = appliedUpdate[moduleId];
}
}
// ...
}
使用方法
// webpack.config.js
const webpack = require('webpack');
module.exports = {
entry:{
main:'./src/index.js',
},
plugins: [
// 该插件的作用就是实现模块热替换,实际上当启动时带上 `--hot` 参数,会注入该插件,生成 .hot-update.json 文件。
new webpack.NamedModulesPlugin(), // 用于启动 HMR 时可以显示模块的相对路径
new webpack.HotModuleReplacementPlugin(), // Hot Module Replacement 的插件
],
devServer:{
// 告诉 DevServer 要开启模块热替换模式
hot: true,
}
// index.js
if (module.hot) {
// accept 函数的第一个参数指出当前文件接受哪些子模块的替换,这里表示只接受 ./AppComponent 这个子模块
// 第2个参数用于在新的子模块加载完毕后需要执行的逻辑
module.hot.accept([${依赖的文件列表}], () => {
...操作
});
}
基础用法
entry:配置入口文件的地址
output:配置出口文件的地址
module:配置模块,主要用来配置不同文件的加载器
plugins:配置插件
devServer:配置开发服务器
webpack-dev-server
devServer:{
contentBase:path.resolve(__dirname,'dist'), // 配置开发服务运行时的文件根目录
host:'localhost',
compress:true,
port:8080
}
"scripts": {
"build": "webpack",
"dev": "webpack-dev-server --open "
}
// 关于服务器代理实现mock数据
+ before(app){
app.get('/api/users', function(req, res) {
res.json([{id:1,name:'zfpx1'}])
})
}
+ 2 webpack-dev-middleware 是 Express 中提供 webpack-dev-server 静态服务能力的一个中间件
const express = require('express');
const app = express();
const webpack = require('webpack');
const webpackDevMiddleware = require('webpack-dev-middleware');
const webpackOptions = require('./webpack.config');
webpackOptions.mode = 'development';
const compiler = webpack(webpackOptions);
app.use(webpackDevMiddleware(compiler, {}));
app.listen(3000);
resolve
+ const bootstrap = path.resolve(__dirname,'node_modules/_bootstrap@3.3.7@bootstrap/dist/css/bootstrap.css');
+ resolve: {
extensions: [".js",".jsx",".json",".css"] // 指定extension之后可以不用在require或是import的时候加文件扩展名,会依次尝试添加扩展名进行匹配,
mainFields: ['browser', 'module', 'main'],// 默认情况下package.json 文件则按照文件中 main
字段的文件名来查找文件, 配置 target === "web" 或者 target === "webworker" 时 mainFields
默认值,target 的值为其他时,mainFields 默认值为: mainFields: ["module", "main"],如果没有package.json文件,mainFiles: ['index'],默认找index文件,也添加其他默认使用的文件名
alias:{
"bootstrap":bootstrap //每当引入bootstrap模块的时候,它会直接引入bootstrap,而不需要从node_modules文件夹中按模块的查找规则查找,提升模块查找的速度
}
}
+
module
modules: {
noParse(content) {
return /jquery|lodash/.test(content) // 第三方模块不需要解析,提高整体的构建速度
},
}
watch
- webpack定时获取文件的更新时间,并跟上次保存的时间进行比对,不一致就表示发生了变化,poll就用来配置每秒问多少次
- 当检测文件不再发生变化,会先缓存起来,等待一段时间后之后再通知监听者,这个等待时间通过aggregateTimeout配置
- webpack只会监听entry依赖的文件
- 我们需要尽可能减少需要监听的文件数量和检查频率,当然频率的降低会导致灵敏度下降
watch:true,
//只有开启监听模式时,watchOptions才有意义
watchOptions:{
//默认为空,不监听的文件或者文件夹,支持正则匹配
ignored:/node_modules/,
//监听到变化发生后会等300ms再去执行,默认300ms
aggregateTimeout:300,
//判断文件是否发生变化是通过不停的询问文件系统指定议是有变化实现的,默认每秒问1000次
poll:1000
}
loader的写法
test:匹配处理文件的扩展名的正则表达式
use:loader名称,就是你要使用模块的名称
include/exclude:手动指定必须处理的文件夹或屏蔽不需要处理的文件夹
query:为loaders提供额外的设置选项
1. loader或者use
module: {
rules: [
{
test: /\.css/,
loader:['style-loader','css-loader']
// use:['style-loader','css-loader']
}
]
}
2. use+loader
module: {
rules: [
{
test: /\.css/,
include: path.resolve(__dirname,'src'),
exclude: /node_modules/,
use: [{
loader: 'style-loader',
options: {
insert:'top'
}
},'css-loader']
}
]
}
webpack常用功能的总结
clean-webpack-plugin
new CleanWebpackPlugin() //默认删除dist目录
mini-css-extract-plugin
- 图片相关
- file-loader 解决CSS等文件中的引入图片路径问题
- url-loader 当图片小于limit的时候会把图片BASE64编码,大于limit参数的时候还是使用file-loader 进行拷贝
- 因为CSS的下载和JS可以并行,当一个HTML文件很大的时候,我们可以把CSS单独提取出来加载
// 分离css
plugins: [
//参数类似于webpackOptions.output
new MiniCssExtractPlugin({
filename: '[name].css',
chunkFilename:'[id].css'
}),
]
// modules
{
test: /\.css/,
include: path.resolve(__dirname,'src'),
exclude: /node_modules/,
use: [{
loader: MiniCssExtractPlugin.loader
},'css-loader']
}
px 自动转成rem
- 使用px2rem-loader
- 页面渲染时计算根元素的font-size值
1. 页面根元素
let root = document.documentElement;
function setRemUnit () {
root.style.fontSize = root.clientWidth / 10 + 'px';
}
window.addEventListener('resize', setRemUnit);
2. px2rem-loader
{
loader:'px2rem-loader',
options:{
remUnit:75,
remPrecesion:8
}
}
html-webpack-plugin
- 可以根据你设置的模板,在每次运行后生成的对应的模板文件
cnpm i html-webpack-plugin -D
plugins: [
new HtmlWebpackPlugin({
minify: true, //压缩
removeAttrubuteQuotes: true, // 去掉属性双引号
template:'./src/index.html',//指定模板文件
filename:'index.html',//产出后的文件名
inject:false,
hash:true,//为了避免缓存,可以在产出的资源后面添加hash值
chunks:['common','index'],
chunksSortMode:'manual'//对引入代码块进行排序的模式
}),
)]
多入口
const path=require('path');
const HtmlWebpackPlugin=require('html-webpack-plugin');
const htmlWebpackPlugins=[];
const glob = require('glob');
const entry={};
const entryFiles = glob.sync('./src/**/index.js');
entryFiles.forEach((entryFile,index)=>{
let entryName = path.dirname(entryFile).split('/').pop();
entry[entryName]=entryFile;
htmlWebpackPlugins.push(new HtmlWebpackPlugin({
template:`./src/${entryName}/index.html`,
filename:`${entryName}/index.html`,
chunks:[entryName],
inject:true,
minify:{
html5:true,
collapseWhitespace:true,
preserveLineBreaks:false,
minifyCSS:true,
minifyJS:true,
removeComments:false
}
}));
});
module.exports={
entry,
plugins: [
//other plugins
...htmlWebpackPlugins
]
}
处理CSS3属性前缀
- Trident内核:主要代表为IE浏览器, 前缀为-ms
- Gecko内核:主要代表为Firefox, 前缀为-moz
- Presto内核:主要代表为Opera, 前缀为-o
- Webkit内核:产要代表为Chrome和Safari, 前缀为-webkit
// npm i postcss-loader autoprefixer -D
postcss.config.js
module.exports={
plugins:[require('autoprefixer')]
}
// webpack.config.js
{
test:/\.css$/,
use:[MiniCssExtractPlugin.loader,'css-loader','postcss-loader'],
include:path.join(__dirname,'./src'),
exclude:/node_modules/
}
引入第三方库
1. new webpack.ProvidePlugin({
_:'lodash'
})
2. externals: {
jquery: 'jQuery'//如果要在浏览器中运行,那么不用添加什么前缀,默认设置就是global
}
3. expose-loader
+ require("expose-loader?libraryName!./file.js");
// 通过属性名 "libraryName" 暴露 file.js 的 exports 到全局上下文。
// 在浏览器中,就将可以使用 window.libraryName 访问。
例如:require("expose-loader?$!jquery");// window.$ 就可以在浏览器控制台中使用
+ webpack配置
+ 1. module: {
loaders: [
{ test: require.resolve("jquery"), loader: "expose-loader?$!expose-loader?jQuery" },
]
}
+webpack v2 用法
// 除了暴露为 window. $ 之外,假设你还想把它暴露为 window.jQuery
2. module: {
rules: [{
test: require.resolve('jquery'),
use: [{
loader: 'expose-loader',
options: 'jQuery'
},{
loader: 'expose-loader',
options: '$'
}]
}]
}
拷贝静态文件copy-webpack-plugin
new CopyWebpackPlugin([{
from: path.resolve(__dirname,'src/assets'),//静态资源目录源地址
to:path.resolve(__dirname,'dist/assets') //目标地址,相对于output的path目录
}])
html-webpack-externals-plugin(外链CDN)
new htmlWebpackExternalsPlugin({
externals:[
{
module:'react',
entry:'https://cdn.bootcss.com/react/15.6.1/react.js',
global:'React'
},
{
module:'react-dom',
entry:'https://cdn.bootcss.com/react/15.6.1/react-dom.js',
global:'ReactDOM'
}
]
})
配置的全局常量 DefinePlugin
// 可以配置的全局常量
+ 如果这个值是一个字符串,它会被当作一个代码片段来使用。
+ 如果这个值不是字符串,它会被转化为字符串(包括函数)。
+ 如果这个值是一个对象,它所有的 key 会被同样的方式定义。
+ 如果在一个 key 前面加了 typeof,它会被定义为 typeof 调用。
new webpack.DefinePlugin({
PRODUCTION: JSON.stringify(true),
VERSION: JSON.stringify("5fa3b9"),
BROWSER_SUPPORTS_HTML5: true,
TWO: "1+1",
"typeof window": JSON.stringify("object")
})
//
console.log("Running App version " + VERSION);
if(!BROWSER_SUPPORTS_HTML5) require("html5shiv");
IgnorePlugin忽略某些特定的模块,让 webpack 不把这些指定的模块打包进去
//
+ 第一个是匹配引入模块路径的正则表达式
+ 第二个是匹配模块的对应上下文,即所在目录名
new webpack.IgnorePlugin(/^\.\/locale/,/moment$/)
图片进行压缩和优化Image-webpack-loader
test: /\.(gif|png|jpe?g|svg)$/i,
use: [
'file-loader',
{
loader: 'image-webpack-loader',
options: {
mozjpeg: {
progressive: true,
quality: 65
},
// optipng.enabled: false will disable optipng
optipng: {
enabled: false,
},
pngquant: {
quality: [0.65, 0.90],
speed: 4
},
gifsicle: {
interlaced: false,
},
// the webp option will enable WEBP
webp: {
quality: 75
}
}
},
],
}]
+ mozjpeg — Compress JPEG images
+ optipng — Compress PNG images
+ pngquant — Compress PNG images
+ svgo — Compress SVG images
+ gifsicle — Compress GIF images
+ webp — Compress JPG & PNG images into WEBP
prerender-spa-plugin:vue预渲染插件
- 在构建时 (build time) 简单地生成针对特定路由的静态 HTML,有利于seo优化。
const path = require('path')
const PrerenderSPAPlugin = require('prerender-spa-plugin')
module.exports = {
plugins: [
...
new PrerenderSPAPlugin({
// Required - The path to the webpack-outputted app to prerender.
staticDir: path.join(__dirname, 'dist'),
// Required - Routes to render.
routes: [ '/', '/about', '/some/deep/nested/route' ], // 在构建时,针对配置的路由生成对应的html文件,便于爬虫爬去,对seo友好
})
]
}
webpack常见优化手段
- tree-shaking
- scope-Hoisting
- splitChunk
- cdn
CDN 又叫内容分发网络,通过把资源部署到世界各地,用户在访问时按照就近原则从离用户最近的服务器获取资源,从而加速资源的获取速度。
+ 缺点:由于 CDN 服务一般都会给资源开启很长时间的缓存,导致用户在很长一段时间内还是运行的之前的版本,这会新的导致发布不能立即生效
+ 解决办法:
+ HTML文件不缓存,放在自己的服务器上,关闭自己服务器的缓存,静态资源的URL变成指向CDN服务器的地址
+ 静态的JavaScript、CSS、图片等文件开启CDN和缓存,并且文件名带上HASH
+ hash一般是结合CDN缓存来使用,通过webpack构建之后,生成对应文件名自动带上对应的MD5值。如
果文件内容改变的话,那么对应文件哈希值也会改变,对应的HTML引用的URL地址也会改变,触发CDN服
务器从源服务器上拉取对应数据,进而更新本地缓存。
+ 为了并行加载不阻塞,把不同的静态资源分配到不同的CDN服务器上
+ 由于同一时刻针对同一个域名的资源并行请求是有限制, 可以通过在 HTML HEAD 标签中 加入<link rel="dns-prefetch" href="http://xxx">去预解析域名,以降低域名解析带来的延迟
+ webpack的处理:
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name]_[hash:8].js',
publicPath: 'cdn地址'
},
- 优化css,mini-mini-css-extract-plugin 未完