减少webpack打包时间
1.优化Loader
对于Loader来说,影响打包效率首当齐冲必属Babel了。因为babel会将代码转为字符串生成AST,然后对AST继续进行转变最后生成新的代码,项目越大,转化代码越多,效率就越低。当然了,我们是有办法优化的。
module.exports={
module:[
rules:[
{
//js文件才使用babel
test:'\.js$',
loader:'babel-loader',
//只在src文件夹下查找
include:[resolve('src')],
//不会去查找路径
exclude:/node_modules/
}
]
}
}
对于Babel来说,我们肯定是希望只作用在JS代码上的,然后node_modules中使用的到吗都是编译过的,所以我们也完全没有必要再去处理一遍…
当然这样做还不够,我们还可以将Babel编译过的文件缓存起来,下次只需要编译更改过的代码文件即可,这样我们可以大幅度加快打包时间。
loader:'babel-loader?cacheDirectory=true'
HappyPack
受限于Node是单线程运行的,所以Webpack在打包的过程中也是单线程的,特别是在执行Loader的时候,长时间编译的任务很多,这样就会导致等待的情况。
HappyPack可以将Loader的同步执行转换为并行的,这样就能充分利用系统资源来加快打包效率了。
module:{
loaders:[
{
test:/\.js$/,
include:[resolve('src')],
exclude:/node_modules/,
//id后面的内容下面
loader:'happypack/loader?id=happybabel'
}
]
},
plugins:[
new HappyPack({
id:'Happybabel',
loaders:['babel-loader?cacheDirectory'],
//开启4个线程
threads:4
})
]
DllPlugin
DllPlugin可以将特定的类库提前打包然后引入。这种方式可以极大的减少打包类库的次数,只有当类库更新版本才有需要重新打包,并且也实现了将公共代码抽离成单独文件的优化方案。
//单独配置在一个文件中
const path=require('path');
const webpack=require('webpack');
module.export={
entry:{
//统一打包类库
vendor:['react'];
},
output:{
path:path.join(__dirname,'dist'),
filename:'[name].dll.js',
library:'[name]-[hash]'
},
plugins:[
new webpack.Dllplugin({
name:'[name]-[hash]',
context:__dirname,
path:path.join(__dirname,'dist','[name]-manifest.json')
})
]
}
然后我们需要指向这个配置文件生成依赖文件,接下来我们需要使用DllReferencePlugin将依赖文件引入项目中
//webpack.conf.js
module.exports={
plugins:[
new webpack.DllRefercePlugin({
context:__dirname,
manifest:require('./dist/vendor-manifest.json'),
})
]
}
代码压缩
在webpack3中,我们一般使用UglifyJS来压缩代码,但是这个是单线程运行的,为了加快效率,我们可以使用webpack-parallel-uglify-plugin来运行UglifyJS,从而提交效率。
在webpack4中,我们就不需要以上操作了,只需要将mode设置为production就可以默认开启以上功能。代码压缩也是我们必做的性能优化方案,当然我们不知可以压缩JS代码,还可以压缩HTML、CSS代码,并且在压缩JS代码的过程中,我们还可以通过配置实现比如删除console.log这类代码的功能。
一些小的优化点
reslove.extensions:用来表明文件后缀名列表,默认查找顺序是[’.js’,’.json’],如果你的导入文件没有添加后缀就会按照这个顺序查找文件。我们应该尽可能减少后缀列表长度,然后将出现频率高的后缀排在前面。
resolve.alias:可以通过别名的方式来映射一个路径,能让webpack更快找到路径
module.noParse:如果你确定一个文件下没有其他依赖,就可以使用该属性让webpack不扫描该文件,这种方式碎玉大型类库很有帮助
减少webpack打包后的文件体积
按需加载
相比大家在开发SPA项目的时候,项目中都会在十几甚至更多的路由页面。如果我们将这些页面全部打包进入一个JS文件的话,虽然将多个请求合并了,但是同样也加载了很多并不需要的代码,耗费了更长的时间。那么为了首页能更快地呈现给用户,我们肯定希望首页能加载文件体积越小越好,这时候我们就可以使用按序加载,将每个路由页面单独为一个文件,当然不仅仅路由可以按需加载,对于loadash这种大型类库同样可以使用这个功能。按需加载的底层机制都是当使用的时候再去下载的对应文件,返回一个Promise,当Promise成功以后去执行回调。
Scope Hoisting
Scope Hoisting会分析出模块之间的依赖关系,尽可能的把打包出来的模块合并到一个函数中去。
比如我们希望打包两个文件
//test.js
export const a=1
//index.js
import {a} from './text.js'
对于这种情况,我们打包出来的代码会类似这样
[
/*0*/
function(module,exports,require){
//...
}
/*1*/
function (module,exports,require){
//...
}
]
但是如果我们使用Scope Hoisting的话,代码就会尽可能的合并到一个函数中去,也就变成了这样的类似代码
[
/*0*/
function (module,exports,require){
//...
}
]
这样的打包方式生成的代码明显比之前少很多,如果在webpack4中你希望开启这个功能,只需要启用optimization.concatenateModules就可以了。
module.exports={
optimaization:{
concatenateModules:true
}
}
Tree Shaking
Tree Shaking可以实现删除项目中未被引用的代码,比如:
//test.js
export const a=1
export const b=2
//index.js
import {a} from './test.js'
对于以上情况,test文件中的变量b如果没有在项目中使用到的话,就不会被打包到文件中。如果你使用webpack4的话,开启生产环境就会自动启动这个优化功能。
打包工具的核心原理:
1.找出入口文件所有的依赖关系
2.然后通过构建CommonJS代码来获取exports导出的内容
可以实现以下两个功能的打包工具
1.将ES6转化为ES5
2.支持在JS文件中import CSS文件
首先安装一些Babel先关的工具
yarn add babylon babel-traverse babel-core babel-preset-env
接下来将这些工具引入文件中
const fs=require('fs');
const path=requir('path');
const babylon=require('babylon');
const traverse= require('babel-traverse').default
const {transformFromAst}=require('babel-core');
首先先来实现如何使用Babel转化代码
function readCode(filePath){
//读取文件内容
const content=fs.readFileSync(filePath,'utf-8');
//生成AST,抽象语法树,它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。
const ast=babylon.parse(content,{
sourceType:'module'
})
//寻找当前文件的依赖关系
const dependencies=[];
traverse(ast,{
ImportDeclaration:({node})=>{
dependencies.push(node.socure.value)
}
})
//通过AST将代码转为ES5
const {code}=transformFromAst(ast,null,{
presets:['env']
})
return {
filePath,
dependencies,
code
}
}
接下来需要实现一个函数,这个函数的功能有以下几点:
调用readCode函数,传入入口文件
分析入口文件的依赖
识别JS和CSS文件
function getDependencies(entry){
//读取入口文件
const entryObject=readCode(entry);
const dependencies=[entryObject];
//遍历所有文件依赖关系
for(const asset of dependencies){
//获取文件目录
const dirname=path.dirname(asset.filePath);
asset.dependencies.forEach(relativePath=>{
//获得绝对定位
const absolutePath=path.join(dirname,relativePath);
if(/\.css$/.test(absolutePath)){
const content=fs.readFileSync(absolutePath,'utf-8');
const code=`
const style=document.createElement('style');
style.innerText=${JSON.stringify(content).replace(/\\r\\n/g,'')};
document.head.appendChild(style);
`;
dependencies.push({
filePath:absolutePath,
relativePath,
dependencies;[],
code
})
}else{
//JS代码需要继续查找是否有依赖关系
const child=readCode(absolutePath);
child.relativePath=relativePath;
dependencies.push(child)
}
})
}
return dependencies
}
首先我们读取入口文件,然后创建一个数组,该数组的目的是存储代码中涉及到的文件
接下来我们遍历这个数组,一开始这个数组中只有入口文件,在遍历的过程中,如果入口文件有依赖其他的文件,那么就会被push到这个数组中
在遍历的过程中,我们先获得该文件对应的目录,然后遍历当前文件的依赖关系
在边路文件依赖过程中,首先生成依赖文件的绝对路径,然后判断当前文件CSS文件还是JS文件
如果是CSS文件的话,我们不用Babel去编译了,只需要读取CSS文件中的代码,然后创建一个style标签,将代码插入进标签并且放入head中即可
如果是js文件的话,我们还需要分析JS文件是否还有别的依赖关系
最后将读取文件后的对象push进数组中
现在我们已经获取到了所有的依赖文件,接下来就是实现打包的功能
function bundle(dependencies,entry){
let modules='';
dependencies.forEach(dep=>{
const filePath=dep.relativePath||entry;
modules+=`'${filePath}':(
function (module,exports,require){${dep.code}}
),`
})
//构建require函数,目的是为了获取模块暴露出来的内容
const result=`
(function (modules){
function require(id){
const module={exports:{}}
modules[id](module,module.exports.require)
return module.exports
}
})({${modules}})
`
//当生成的内容写入到文件中
fs.writeFileSync('./bundle.js',result)
}
这段代码结合Babel转化后的代码来看,这样大家就会能理解为什么需要这样写了
//entry.js
var _a=require('./a.js')
var _a2=_interopRequireDefault(_a);
function _interopRequireDefault(obj){
return obj&&obj.__esModule?obj:{default:obj}
}
console.log(_a2.default);
//a.js
Object.defineProperty(exports,'__esModule',{
value:true
})
var a=1;
exports.default=a;
首先遍历所有依赖文件,构建出一个函数参数对象
对象的属性就是当前文件的相对路径,属性值是一个函数,函数体是当前文件下的代码,函数接受三个参数module、exports、require
module参数对应CommonJS中的module
exports参数对应CommonJS中的module.export
require参数对应我们自己创建的require函数
接下来就是构造一个使用参数的函数,函数做的事情很简单,就是内部创建一个require函数,然后调用require(entry),也就是require(’./entry.js’),这样就会从函数中找到./entry.js对应的函数并执行,最后将导出的内容通过module.export的方式让外部获取到
最后再将打包出来的内容写入到单独的文件中
如果你对上面的代码的实现还有疑问的话,可以阅读下打包后的部分简化代码
;(function(module){
function require(id){
//构造一个CommonJS导出代码
const module={exports:{}}
//去参数获取文件对应的函数并执行
modules[id](module,module,exports,require)
return module.exports
}
require('./entry.js')
})({
'./entry.js': function(module, exports, require) {
// 这里继续通过构造的 require 去找到 a.js 文件对应的函数
var _a = require('./a.js')
console.log(_a2.default)
},
'./a.js': function(module, exports, require) {
var a = 1
// 将 require 函数中的变量 module 变成了这样的结构
// module.exports = 1
// 这样就能在外部取到导出的内容了
exports.default = a
}
// 省略
})
转载链接:https://juejin.im/book/5bdc715fe51d454e755f75ef/section/5bdc775a6fb9a049f818b879