webpack4.0
(一) webpack安装使用
1.简介
webpack是个打包工具,它默认处理js文件,同时也能借助loaders实现对其他类型文件的处理,同时还能用插件来简化我们的开发流程。
2.配置环境
先要安装一下准备环境,node,因为webpcak是基于node的打包工具
其次要安装webpack和webpack-cli
npm init -y
npm install webpack webpack-cli -D
3.命令行打包
安装好了以后,我们可以通过命令行直接来进行打包,可以先新建一个index.js文件,然后在命令行:
npx webpack index.js
打包完成后会有一个默认的打包文件,我们要想看看效果可以新建一个index.html来引入默认的打包文件,然后看效果。
4.脚本打包
在此之前,我们先来修改一下目录,让结构更加清晰,我们新建一个src目录,将index.js放在里面,然后新建一个dist目录,将index.html放在里面
4.1 配置webpack.config.js
接着在根目录下新建一个webpack.config.js的文件,然后在里面配置一些打包的参数,
const path = require('path');
module.exports = {
entry:{
'main':'./src/index.js'
},
output:{
filename:'bundle.js',
path:path.resolve(__dirname,'dist')
}
}
4.2 配置package.json
配置好了以后,我们再来配置一下package.json文件,实现脚本打包的功能,
{
"scripts": {
"bundle": "webpack"
},
}
此时在命令行中直接使用 npm run bundle,等待打包成功后,手动打开我们的index.html文件看看效果,至此我们已经完成了webpack的安装和使用。
(二)webpack打包资源(loader)
前面我们安装并使用了webpack,我们处理的内容是js文件,那它如何处理其他类型的文件呢?比如说图片,css,字体等等,这就需要用到loaders,也就是说,webpack能认得js文件,但他不认识其他文件,所以需要一个中间人来告诉他该怎么处理.
1.处理图片资源
首先先来看一下图片文件。首先我们先来截屏一张图片,然后把它放在src/dog.png,然后要把它挂载在index.html上,在index.js中这样来写;
import dog from './dog.png';
var img = new Image();
img.src = dog;
var demo = ducument.getElementById('demo');
demo.append(img);
写好以后,我们直接来打包肯定会失败,因为webpack不认识这种文件,所以我们还需要来写一下配置文件,在webpack.config,js中这样来写;
module.exports = {
module:{
rules:[
{
test:/\.(png|jpg|gif)$/,
use:{
loader:'file-loader'
}
}
]
}
}
1.1 file-loader
写好了以后我们先来安装一下file-loader,npm install file-loader -D,然后再来打包,npm run bundle,打包完成以后,再来看看index.html的效果,就会发现图片已经能够加载在页面了,我们成功的处理了图片这种资源,此时打包出来的图片的名字一堆乱码,太难看,所以我们可以修改一下
module:{
rules:[
{
test:/\.(jpg|png|gif)$/,
use:{
loader:"file-loader",
options:{
name:"[name].[ext]"
}
}
}
]
}
1.2 url-loader
除了可以使用file-loader,我们还可以使用另一种loader来处理图片,那就是url-loader,我们来npm install url-loader -D,然后把图片处理规则改成url-loader再来看看效果,先把dist里面的打包文件删除,然后再打包,完成后,会发现并没有像file-loader那样把图片单独打包出来,我们在浏览器看一下它的img的src就会发现它被打包成了base64文件,这是它和file-loader的区别,为什么会有file-loader和url-loader这两种处理方式呢?这是因为图片的大小是不确定的,如果非常大,那就单独处理,这样的话页面的加载就不用等这个图片加完了以后再显示,如果非常小,那就直接包含在打包文件内,这样就会使得减少发送http的请求。
其实url-loader也可以通过配置来实现分类打包,利用limit来设置,如果小于某kb就打包在bundle.js中,否则的话就单独打包在img中然后引用,配置如下:
options:{
name:"[name].[ext]",
outputPath:"img/",
limit:20400
}
就会发现大于20kb的图片就会直接被分开打包,小于20kb就以base64位的方式打包在bundle.js中。
2.处理css资源
前面我们已经能处理js和图片资源了,接下来我们来处理一下css,scss等文件,
其实思路大概都差不多,都是使用对应的loader来进行处理。
首先我们先新建style.css和style.scss文件,随便写点样式,
//style.css
body{
background:green
}
//style.scss
body{
transform:translate(0,0)
}
然后在index.js中引入并使用
import './style.css';
1.1 css/style loader
引入好了以后,我们需要去配置一下loader,在webpack.config.js中
{
test:/\.css$/,
use:{
loader:["css-loader","style-loader"]
}
}
1.2 scss-loader
这些lodaer加载时是从后向前加载的,css-loader 是将多个css文件合并成一个,style-loader是将合并完成的css文件挂载在html的head,首先我们先来安装一下,npm install css-loader style-loader -D,然后npm run bundle进行打包,然后打开index,html就会发现页面已经变成了蓝色,说明css这种类型的文件已经能被webpack识别并打包了,我们再来试试scss文件,同样在webpack.config.js中进行配置
{
test:/\.scss$/,
use:[
'style-loader',
'css-loader',
'sass-loader'
]
}
此时我们再把刚才写好的style.scss文件在index.js中引用
import './style.scss';
然后再来安装一下npm install sass-loader -D,后npm run bundle,会发现有报错,这是因为我们缺少了node—sass这个包,安装一下,npm install node-sass -D,然后继续npm run build打包完成后在html中打开,就会发现页面变成了绿色,说明sass文件也能被webpack进行处理了
1.3 postcss
前面是最基础的处理css文件的方法,那除了这样来处理文件我们有时候还需要给不同的css属性添加厂商前缀,这个我们通过postcss-loader来实现,首先我们先来在style.css文件中使用一些css3的属性
body{
transform: translate(0,0);
}
此时我们来打包的时候是不会有厂商前缀的,这就需要我们来写一些规则,
{
test:/\.css$/,
use:['style-loader','css-loader','postcss-loader']
}
配置好了以后,我们还需要新建一个postcss.config.js来对postcss来进行配置,
//postcss.config.js
module.exports = {
plugins:[require('autoprefixer')]
}
先来安装postcss-loader,npm install postcss-loader autoprefixer -D,然后npm run build此时再来看看,就会发现里面自动加了前缀
3.处理字体文件
至此我们已经可以处理js,css,图片文件,接下来我们处理字体文件,我们平常使用的字体都是默认字体,有时候我们希望使用一些特殊的字体,那webpack是不认识这些文件的,我们就来看看它是如何处理这些文件的,首先我们先下载一种字体,我已经准备好了,大家下载来用就好了,接着我们在样式文件中使用一下
//style.css
font-face {
font-family: 'fontnameRegular';
src: url('fontname.ttf');
}
div{
font-family: fontnameRegular
}
然后在js中引用一下
import './style.css';
var demo = document.getElementById('demo');
var div = document.createElement('div');
div.innerHTML = '我要自学网';
demo.append(div);
如果此时进行打包,肯定会失败,试一下,果然失败了,那怎么处理呢?
1.1 file-loader
配置一下loader
{
test:/\.(ttf)$/,
use:{
loader:'file-loader'
}
}
然后打包试试,npm run bundle,在浏览器查看效果,字体就变化了,这样就成功处理了字体文件
(三)webpack实现自动化(plugins)
1.构建流程
上面我们说的都是关于处理资源的内容,接下来我们来看一下我们是如何通过plugins简化操作流程的,先想一下,我们到目前为止对于打包编译过程里手工做了什么?
源码-清除-新建-打包-浏览-刷新-报错-定位-转码-切换
- 0.新建dist———–output里的path会自动新建dist目录
- 1.新建index.html—————html(模板)
- 2.引入打包后的文件引用到index.html——–html
- 3.修改src源代码后清空上次打包的文件———–clean
- 4.再次npm run bundle进行打包——————–npm run start
- 5.将index.html在浏览器打开—————open:true
- 6.下次有变更刷新页面————-contentBase:’./dist’
- 7.代码开发完成后切换到线上环境重新配置相关参数—————webpack merge
- 8.配置babel使得支持es6语法,react语法
- 9.配置eslint使得支持智能报错
这些都可以自动化实现,
2.新建模板引入资源
我们首先来处理1和2自动的新建index.html并引入打包资源,这需要借助插件html-webpack-plugin来实现,先来安装一下,
npm install html-webpack-plugin -D
安装好了以后,需要在webpack.config.js里配一下,
const HtmlWebpackPlugin = require('html-webpack-plugin');
plugins:[new HtmlWebpackPlugin({
template:'./src/idex.html'
})]
//src/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>html模板</title>
</head>
<body>
<div id="demo"></div>
</body>
</html>
它是一个插件,所以写在plugins里面,它的作用是在每次打包文件的时候读取模板文件然后插入到新生成的index.html然后把打包好的资源引入文件中。此时我们呢npm run bundle发现它果然实现了新建HTML文件并引入打包资源。
3.清空打包目录
接着我们要解决3.自动清空dist目录,你可能会想说,我以前并没有手动的清除过dist目录,在我重新bundle以后还是直接能用啊,那为什么还要来清除呢?我们假想这样的场景,你在打包的时候,把出口的文件名修改了,在打包的时候会生成不同的打包文件,多修改几次就会使得dist目录非常的臃肿,所以我们需要清除。
清除同样是靠插件clean-webpack-plugin同样来安装一下
npm install clean-webpack-plugin -D
然后配一下
plugins:[
new CleanWebpackPlugin({
root:path.resolve(__dirname,'dist')
})
]
这个插件会在打包之前被调用然后去清除dist目录下的内容.实现了我们的目的。
4.持续监控(devserver)
接下来我们4.输入一次命令持续监控文件变化,而不是每次修改源码就重复的打包,这个可以借助webpackdevserver来实现,首先先来下载
npm install webpack-dev-server -D
接着来配一下,
devServer:{
contentBase:'./dist',
open:true
}
修改一下启动脚本,
{
"scripts":{
"dev":"webpack-dev-server"
}
}
此时再来打包一下,npm run dev,就会发现它打包完成后自动打开了浏览器,然后当我们源码有修改的时候会自动刷新页面.
为什么要用这个服务器呢?使用server它的协议就会变成http,这有助于我们在开发时避免跨域问题。
5.环境切换
那我们如何来区分这两种环境呢?难道每次切换环境的时候都来修改对应的配置么?那太麻烦了,我们可以把开发环境和线上环境分别写在一个文件里,然后把共有的逻辑写在另一个文件,在切换环境的时候直接调用环境配置文件即可,
首先是开发环境
//webpack.dev.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const webpack = require('webpack');
module.exports = {
mode:"development",
devtool:"source-map",
entry:{
"main":"./src/index.js"
},
devServer:{
contentBase:'./dist',
open: true,
port:8080,
hot:true
},
module:{
rules:[
{
test:/\.js$/,
exclude:"/node_modules/",
loader:"babel-loader"
},
{
test:/\.(jpg|png|gif)$/,
use:{
loader:'url-loader',
options:{
name:'[name].[ext]',
outputPath:'images/',
limit:1024
}
}
},
{
test:/\.css$/,
use:['style-loader','css-loader']
},
{
test:/\.scss$/,
use:['style-loader',
{
loader:'css-loader',
options:{
modules:true
}
},
'sass-loader']
},
{
test:/\.(ttf)$/,
use:{
loader:'file-loader'
}
}
]
},
plugins:[
new HtmlWebpackPlugin({
template:'./src/index.html'
}),
new CleanWebpackPlugin({
root:path.resolve(__dirname,'dist')
}),
new webpack.HotModuleReplacementPlugin()
],
optimization:{
usedExports: true
},
output:{
filename:'bundle.js',
path:path.resolve(__dirname,'dist')
}
}
接着是线上环境
//webpack.prod.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const webpack = require('webpack');
module.exports = {
mode:"production",
devtool:"cheap-module-source-map",
entry:{
"main":"./src/index.js"
},
module:{
rules:[
{
test:/\.js$/,
exclude:"/node_modules/",
loader:"babel-loader"
},
{
test:/\.(jpg|png|gif)$/,
use:{
loader:'url-loader',
options:{
name:'[name].[ext]',
outputPath:'images/',
limit:1024
}
}
},
{
test:/\.css$/,
use:['style-loader','css-loader']
},
{
test:/\.scss$/,
use:['style-loader',
{
loader:'css-loader',
options:{
modules:true
}
},
'sass-loader']
},
{
test:/\.(ttf)$/,
use:{
loader:'file-loader'
}
}
]
},
plugins:[
new HtmlWebpackPlugin({
template:'./src/index.html'
}),
new CleanWebpackPlugin({
root:path.resolve(__dirname,'dist')
})
],
output:{
filename:'bundle.js',
path:path.resolve(__dirname,'dist')
}
}
然后我们来看,这两个里面有太多的重复代码,我们可以把他抽离出来放在单独的一个文件中,然后在dev和prod中分别来引用common
//wenpack.common.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
module.exports = {
entry:{
"main":"./src/index.js"
},
module:{
rules:[
{
test:/\.js$/,
exclude:"/node_modules/",
loader:"babel-loader"
},
{
test:/\.(jpg|png|gif)$/,
use:{
loader:'url-loader',
options:{
name:'[name].[ext]',
outputPath:'images/',
limit:1024
}
}
},
{
test:/\.css$/,
use:['style-loader','css-loader']
},
{
test:/\.scss$/,
use:['style-loader',
{
loader:'css-loader',
options:{
modules:true
}
},
'sass-loader']
},
{
test:/\.(ttf)$/,
use:{
loader:'file-loader'
}
}
]
},
plugins:[
new HtmlWebpackPlugin({
template:'./src/index.html'
}),
new CleanWebpackPlugin({
root:path.resolve(__dirname,'dist')
})
],
output:{
filename:'bundle.js',
path:path.resolve(__dirname,'dist')
}
}
抽离好了common,接下来就是引用了,这里我们需要使用一个新的插件来完成合并webpack-merge,先来安装一下,
npm install webpack-merge -D
//dev
const webpack = require('webpack');
const merge = require('webpack-merge');
const commonConfig = require('./webpack.common.js');
const devConfig = {
mode:"development",
devtool:"source-map",
devServer:{
contentBase:'./dist',
open: true,
port:8080,
hot:true
},
plugins:[
new webpack.HotModuleReplacementPlugin()
],
optimization:{
usedExports: true
}
}
module.exports = merge(commonConfig,devConfig)
//prod
const merge = require('webpack-merge');
const commonConfig = require('./webpack.common.js')
const prodConfig = {
mode:"production",
devtool:"cheap-module-source-map"
}
module.exports = merge(commonConfig,prodConfig)
此时根目录下有一堆的配置文件,太杂乱了,所以把他们放在build文件夹下,然后修改脚本路径
"scripts": {
"bundle": "webpack",
"dev": "webpack-dev-server --config ./build/webpack.dev.js",
"build": "webpack --config ./build/webpack.prod.js"
},
6.配置babel
6.1支持ES6语法
使用es6代码书写业务,这个需要使用babel来进行转码,我们来写一下
首先,要使用babel就要先来安装
npm install -D babel-loader @babel/core
然后,配置loader
module.exports = {
module:{
rules:[
{
test:/\.js$/,
exclude:"/node_modules/",
loader:"babel-loader",
options:{
presets:['@babel/preset-env']
}
}
]
}
}
配置好了loader以后,我们还需要安装一个模块,因为loader只是把babel和webpack联系在一起,具体的转换工作还是由下面这个/preset-env 模块来做的,转换出来的内容有时候一些低版本的浏览器同样也不支持,就需要对他进行在此的兼容使用/polyfill 这个模块
npm isntall @babel/preset-env -D
npm install @babel/polyfill --save
写好了以后,我们来写点es6的代码来试试
//index.js
import '@babel/polyfill'
const arr = [
new Promise(() => {}),
new Promise(() => {})
];
arr.map(item => {
console.log(item);
});
然后就会看到,代码被打包成了可以被低版本浏览器识别的es5代码,但是我们再来考虑一下,我们只用到了仅有的几个es6的语法,但是他在识别的时候把所有的特性都转换了,这根本没必要,另外,有的浏览器默认就支持es6的代码,不需要进行转换就能用 ,根本不用转换就能用啊,这两个点怎么解决呢?我们可以给他添加一些配置来实现,
options:{
presets:[['@babel/preset-env',{
targets:{
chrome:"67"
},
useBuiltIns:"usage"
}]]
}
总结一下:1.babel-loader @babel/core:使用babel的必要环境,loader负责将babel和webpack联系起来
2.@babel/preset-env:实际进行代码转换的模块
3.@babel/polyfill:转化出来的代码在一些低版本浏览器同样不能识别,用这个模块来兼容
4.targets:{chrome:"67"} :浏览器支持就不用再转换了
5.useBuiltIns:"usage":只转换使用到的es6代码特性
至此我们就可以写任意的es6代码了,但是babel本身的配置非常多,如果都写在webpack.config.js中,那会非常的臃肿,所以我们可以把他单独拿出来写在.babelrc中
//.babelrc
{
"presets":[
[
"@babel/preset-env",
{
"targets":{
"chrome":"67"
},
"useBuiltIns":"usage"
}
]
]
}
我们再来打包试一下,npm run bundle 发现没有问题
6.2支持react语法
最后一个内容就是,如何使用框架来写源码?
首先,我们先安装框架
npm install react react-dom -D
接着写配置,
//.babelrc
{
"presets":[
[
"@babel/preset-env",
{
"targets":{
"chrome":"67"
},
"useBuiltIns":"usage"
}
],
"@babel/preset-react"
]
}
写点react代码
import React ,{ Component }from 'react';
import ReactDom from 'react-dom';
class App extends Component{
render() {
return <div>hello world</div>
}
}
ReactDom.render(<App />,document.getElementById('demo'));
再来运行npm run dev就能看到react代码了。
6.3支持vue语法
同样先来安装框架
npm install vue
接着安装loader
npm install -D vue-loader vue-template-compiler
配制一下loader
module:{
rules:[
{
test: /\.vue$/,
loader: 'vue-loader'
}
]
},
plugons:[
new VueLoaderPlugin()
]
写点vue代码
//index.vue
<template>
<div class="rest">{{info}}</div>
</template>
<script>
module.exports = {
data(){
return {
info:'hello'
}
}
}
</script>
<style lang="stylus" scoped>
.test
color:red
</style>
挂载在页面上
//index.js
import Vue from 'vue';
import App from './index.vue';
const root = document.getElementById('root');
new Vue({
render:(h) => h(App)
}).$mount(root)
7.配置Eslint
在项目中,为了统一编码规范,我们可以用一些工具来帮我们约束,eslint是最常用的规范工具,我们来使用一下
npm instll eslint -D
npm install babel-eslint -D
npm install eslint-loader -D
npx eslint --init
现在已经有了一个配置文件.eslint.js,但是对于一些错误没法直接在sublimt中显示,我们就可以用eslint-loader来进行配置,接着我们来配置一下
//.eslintrc
module.exports = {
"parser":"babel-eslint"
}
rules:{}
//dev
devServer:{
overlay:true
}
//common
module:{
rules:[
{
test:/\.js$/,
use:["babel-loader","eslint-loader"]
}
]
}
此时,只要有错误就会在浏览器上进行弹出。
至此我们看一下,所有手工操作的内容我们现在都变成了自动操作,大大简化了开发的繁琐,
(四)webpack提升性能
1.性能分析
接下来我们要考虑的问题就是性能问题,我们在上面的开发过程中有哪些性能可以提升呢?
- 1.源码有修改时不要整页刷新只刷新修改的部分即可(HML)
- 2.源码出错时快速定位到出错的位置,不必精确到字符,只要精确到哪一行即可。(sourcemap)
- 3.
2.HMR(热更替)
2.1 cssHMR
首先是1.局部刷新更替的代码,这个可以使用HML来进行实现,它的名字叫热替换,当我们开启了HML就会只刷新更改的代码,我们举例来看一下
//index.js
import './style.css'
var div = document.createElement('div');
div.innerHTML = "我要自学网";
var p = document.createElement('p');
p.innerHTML = "51zxw";
var demo = document.getElementById('demo');
demo.append(div);
demo.append(p);
//style.css
div{
color:green;
}
p{
color:red
}
此时我们npm run dev开启服务器,会发现页面上渲染好了内容,当我们去修改div的颜色的时候,页面会整页刷新,但是实际上p的颜色并没有变啊,它也被重新加载了一遍,如果只有这两个样式那还好说,样式一旦增多,这绝对是极大的资源浪费啊,所以,就需要只刷新div的样式而不去刷新整体的样式,我们需要配一下
const webpack = require('webpack');
devServer:{
contentBase:'./dist',
open:true,
port:8080,
hot:true
},
plugins:[
new HtmlWebpackPlugin({
template:'./src/index.html'
}),
new CleanWebpackPlugin({
root:path.rasolve(__dirnam,'dist')
}),
new webpack.HotModuleReplacement()
]
此时我们再来重启服务器,然后在修改div颜色的时候,就会发现它不会再整页刷新而是只替换了修改的代码。
2.2jsHMR
上面是针对css代码的热替换,其实js代码同样也可以,我们举例说明一下
//counter.js
function counter(){
var div = document.createElement('div');
div.setAttribute('id','counter');
div.innerHTML = 1;
div.onclick = function(){
div.innerHTML = parseInt(div.innerHTML,10)+1;
}
document.body.appendChild(div)
}
export default counter;
//number.js
function number(){
var div = document.createElement('div');
div.setAttribute('id','number');
div.innerHTML = 3;
document.body.appendChild(div);
}
export default number;
//index.js
import counter from './counter.js';
import number from './number.js';
counter()
number()
在未开启HML的时候,页面第一次加载,显示p和div,当修改div里的内容时,p里的内容没有改变,但也被刷新了
在开启HML的时候,我们可以指定只刷新a的内容
//index.js
import createDiv from './a.js';
import createP from './b.js';
createDiv();
createP();
if(module.hot){
module.hot.accept('./a.js',function(){
var div = document.getElementById('d');
document.body.remove(div)
createDiv()
})
}
至此就实现了js代码的HML热更替
3.sourceMap(源码映射)
接着我们需要定位出错代码的位置,这个要用soucemap来进行定位,但是开发环境下它是默认开启的,所以我们先把他关掉
//webpack.config.js
module.exports = {
devtool:"none"
}
然后启动服务器,这时候我们故意改错代码
console.lo('51zxw')
这是后当我们点击错误的代码的链接的时候,我们直接跳到了打包文件里去了,而不是定位到源码里,所以我们希望能定位到源码里,我们再来修改一下配置
module.exports = {
devtool:"source-map"
}
然后重启服务器,再次点击的时候就会定位到具体的出错源码,此时我们会发现,dist目录不见了,其实是webpack为了加快打包速度,把dist目录直接放在了内存中,我们重新npm run bundle来学习一下,就会发现此时dist目录又出现了,但也多了一个map文件,这个就是soucre-map的对应文件,他会把打包资源和源码关系醉成对应放在这个文件里。知道了怎么用,我们接下来说说devtool有哪些参数,各代表什么意思。inline-source-map:直接把对应文件map放在打包文件里
cheap-source-map:只定位到错误行而不是具体错误字符,同时也不处理引入的第三方包带来的错误
module-source-map:既处理业务代码又处理第三方包的错误
eval-source-map:速度最快但错误信息不全面
在开发环境中一般要使用:devtool:cheap-module-eval-source-map
在生产环境中一般要使用:devtool:cheap-module-source-map
4.TreeShaking
我们在前面已经学会了打包不同的资源和自动化开发流程,同时也做了一点性能优化,但还有很多的地方可以优化,比如,我们在前面配置过”useBuiltIns”:”usage”,这个是为了使得babel在转码的时候只转码我们用到的es6特性而不是把所有的es6特性都转码,那在我们自己写的js代码中,如果我们引入了一个模块,但是只是用了其中导出的一个内容,那其他的内容也会被打包,那有没有办法使得我们用哪些东西就打包哪些,不用的就全去除掉呢?这个就是TreeShaking,es模块未引用的导出内容全部不加载我们来实现一下,
export const add = function(a,b){
console.log(a+b)
}
export const minus = function(a,b){
console.log(a-b)
}
//index.js
import { add } from './math.js';
add(1,2)
此时我们打包的时候,这两个方法都会被打包进bundle.js,我们来配置一下,
optimization:{
usedExports:true
}
{
"sideEffects":false
}
在线上环境基本上默认开启treeshaking,开发环境使用optionzation来进行触发。
5.CodeSplitting(代码分割)
接着我们来看一下代码分割,有时候我们的代码里既包括自己写的业务逻辑,又有引入的第三方包,那在用户加载的时候,如果把两个文件打包在一个文件中,下载的速度会非常的慢,所以就需要把代码分割开来,加载两个较小的文件比加载一个较大的文件速度更快。那如何来实现代码分割呢?
首先我们先来使用一个外来的库loadsh
npm install loadsh -D
//index.js
import _ from 'loadsh';
console.log(_.join(['a','b'],'***'))
然后配置common来实现代码分割
optimization:{
splitChunks:{
chunks:"all"
}
},
此时,loadsh和业务代码就分开打包了
6.LazyLoading(懒加载)
再来看一下懒加载,通过import异步加载模块,也就是只有需要使用这个模块的时候才来加载,举例说明一下
function getComponent(){
return import('loadsh');
var element = document.createElement('div');
element.innerHTML = _.join(['a','b'],'***');
rerun element;
}
document.addEventListener('click',()=>{
getComponent().then(element => {
document.body.appendChild(element)
})
})
7.PreFech(预请求)
再来看一下prefech,异步代码在网络空闲的时候自动下载,举例说明一下
//index.js
document.addEventListener('click',()=>{
import(/*webpackPrefetch:true*/'/.click.js').then(
({default:func}) => {
func()
})
})
//click.js
function handleClick(){
const ele = document.createElement('div')
ele.innerHTML = "hello";
document.body.append(ele)
}
export default handleClick;
8.caching(缓存)
caching,在进行打包的时候,如果有的包内容没有发生改变,那么前后两次打包的名称就相同,这使得浏览器在请求的时候,如果看到相同的文件就不会去重新发请求,所以可以用contenthash来时的每次修改生成一个新的文件名来避免。
output:{
filename:'[name].[contenthash].js'
}
9.shimming(垫片)
6.shimming:垫片,一种兼容方案,有很多的行为都叫垫片,它主要是弥补一些不足,例如将变量全局化
const webpack = require('webpack');
plugins:[
new webpack.ProvidePlugin({
$:'jquery'
})
]
全局变量
module.exports = (env) => {
if(env && env.production){
return merge(commonConfig,prodConfig);
}else{
return merge(commonConfig,devConfig);
}
}
10.DllPlugins
在打包的时候,我们难免会引入第三方的包,重复的打包时这些没有改变的模块也会重新进行打包,为了加快打包的时间,我们可以在第一次打包的时候就把它保存下来,在以后打包的时候直接调用就行了,这个要借助Dllplugin来实现。
首先我们先来引入一些内容来看一下
import React, { Component } from 'react';
import ReactDom from 'react-dom';
class App extends Component{
render() {
return <div>hello world</div>
}
}
ReactDom.render(<App />,document.getElementById('demo'));
然后新建一个webpack.dll.js
const path = require('path');
const webpack = require('webpack');
module.exports = {
mode:'development',
entry:{
vendors:['react','react-dom']
},
output:{
filename:'[name].dll.js',
path:path.resolve(__dirname,'../dll'),
library:'[name]'
},
plugins:[
new webpack.DllPlugin({
name:'[name]',
path:path.resolve(__dirname,'../dll/[name].manifest.json'),
})
]
}
先来打包一次,生成模块对应的打包文件和json对应文件
{
"scripts":{
"build:dll": "webpack --config ./build/webpack.dll.js"
}
}
接下来我们需要在index.html中引入这个文件,
//common.js
const AddAssetHtmlWebpackPlugin = require('add-asset-html-webpack-plugin');
plugins:[
new AddAssetHtmlWebpackPlugin({
filepath:path.resolve(__dirname,'../dll/vendors.dll.js')
})
]
引入成功后,当打包时在commonjs中来检查是不是有vendors.manifest.json中已经打包的内容。如果有就直接在全局变量中使用,没有就去node_modules寻找后再打包,
//common.js
plugins:[
new webpack.DllReferencePlugin({
manifest:path.resolve(__dirname,'../dll/vendors.manifest.json')
})
]
就会发现打包的速度确实加快了,若果在大型项目里,我们需要引入多个模块,需要重复的配太麻烦,所以坐下自动插入
//common
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const webpack = require('webpack');
const devConfig = require('./webpack.dev.js');
const prodConfig = require('./webpack.prod.js');
const merge = require('webpack-merge');
const AddAssetHtmlWebpackPlugin = require('add-asset-html-webpack-plugin');
const fs = require('fs');
const plugins = [
new HtmlWebpackPlugin({
template:"./src/index.html"
}),
new CleanWebpackPlugin({
root:path.resolve(__dirname,'../dist')
}),
new webpack.ProvidePlugin({
$:'jquery'
})
]
const files = fs.readdirSync(path.resolve(__dirname,'../dll'));
files.forEach(file => {
if(/.*\.dll.js/.test(file)){
plugins.push(new AddAssetHtmlWebpackPlugin({
filepath:path.resolve(__dirname,'../dll',file)
}))
}
if(/.*\.manifest.json/.test(file)){
plugins.push(new webpack.DllReferencePlugin({
manifest:path.resolve(__dirname,'../dll',file)
}))
}
})
// module.exports = {
const commonConfig = {
entry:{
"main":"./src/index.js"
},
module:{
rules:[
{
test:/\.js$/,
exclude: /node_modules/,
use:[
"babel-loader",
// "eslint-loader",
// {
// loader:"imports-loader?this=>window"
// }
]
},
{
test:/\.(png|jpg|gif)$/,
use:{
loader:"url-loader"
}
},
{
test:/\.(ttf|eot|svg)$/,
use:{
loader:"file-loader"
}
}
]
},
plugins,
optimization:{
usedExports:true,
splitChunks:false
},
output:{
path:path.resolve(__dirname,'../dist')
}
}
module.exports = (env) => {
if(env && env.production){
return merge(commonConfig,prodConfig);
}else{
return merge(commonConfig,devConfig);
}
}
1.采用最新版本的node
2.loader和plugin作用在有需求的模块上,像第三方模块上尽量就不检查了
3.resolve
resolve:{
extends:['js','jsx'],
alias:{
child:path.resolve(__diename,'/')
}
}
4.DllPlugins
5.控制包的大小
6.合理使用sourcemap
7.结合stats.json进行分析
8.开发环境内存编译
9.开发环境剔除不用的插件
总结一下提升性能:
1.treeshaking:摇掉引入模块中未使用的内容
//dev
optimization:{
usedExports:true
}
//package
{
"sideEffects":false
}
2.codesplitting:分块打包代码
optimization:{
splitChunks:{
chunks:"all",
minSize:30000,
minChunks:1,
name:true,
cacheGroup:{
vendors:{
vendors:{
test:/[\\/]node_modules[\\/]/,
priority:-10,
filename:'vendors.js'
}
},
default:{
priority:-20,
reuseExistingChunk:true
filename:"common.js"
}
}
}
},
npm install mini-css-extract-plugin -D//拆分
npm install optimize-css-assets-webpack-plugin -D//压缩
//prod
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module:{
rules:[
{
test:/\.css$/,
use:[
MiniCssExtractPlugin.loader,
"css-loader",
"postcss-loader"
]
},
{
test:/\.scss$/,
use:[
MiniCssExtractPlugin.loader,
"css-loader",
"sass-loader"
]
}
]
},
optimization:{
minimizer:[new OptimizeCssAssetsWebpackPlugin({})]
},
plugins:[
new MiniCssExtractPlugin({
filename:'[name].css',
chunkFilename:'[name].chunk.css'
})
]
//注意避免和optimization冲突,并且把common中的css处理摘出来后把prod中也配上
3.lazy-loading:懒加载,只有在调用的时候才去加载模块
function getComponent(){
return import('loadsh');
var element = document.createElement('div');
element.innerHTML = _.join(['a','b'],'***');
rerun element;
}
document.addEventListener('click',()=>{
getComponent().then(element => {
document.body.appendChild(element)
})
})
4.preFech:异步代码在网络空闲的时候自动下载
document.addEventListener('click',()=>{
import(/*webpackPrefetch:true*/'/.click.js').then(
({default:func}) => {
func()
})
})
5.caching:huancun
output:{
filename:'[name].[contenthash].js'
}
6.shimming:垫片,一种兼容方案,将变量全局化
const webpack = require('webpack');
plugins:[
new webpack.ProvidePlugin({
$:'jquery'
})
]
全局变量
module.exports = (env) => {
if(env && env.production){
return merge(commonConfig,prodConfig);
}else{
return merge(commonConfig,devConfig);
}
}
(五)webpack底层探索
1.自己实现loader
loader是用来处理各种资源的,它本质上是一个函数,我们来写一下,
//webpack.config.js
const path = require('path');
module.exports = {
mode:'development',
entry:{
main:"./src/index.js"
},
resolveLoader:{
modules:['node_modules','./loaders']
},
module:{
rules:[
{
test:/\.js$/,
use:{
loader:'my-loader.js',
options:{
name:'kk'
}
}
}
]
},
output:{
filename:'[name].js',
path:path.resolve(__dirname,'dist')
}
}
//index.js
console.log('hello world');
//my-loader.js
//npm install loader-utils -D
const loaderUtils = require('loader-utils');
module.exports = function(source){
//1.直接处理参数
const options = loaderUtils.getOptions(this);
// return source.replace('lee',options.name);
//2.callback处理参数
// const result = source.replace('lee',options.name);
// this.callback(null,result);
//3.写异步代码
const callback = this.async();
setTimeout(()=>{
const result = source.replace('dell',options.name);
callback(null,result)
},1000)
}
2.自己实现plugin
plugin是用来帮助我们简化一些操作流程,它实质上是一个类,我们来写一下
//webpack.config.js
const path = require('path');
const CopyrightWebpackPlugin = require('./plugins/copyright-webpack-plugin.js');
module.exports = {
mode:'development',
entry:{
main:"./src/index.js"
},
resolveLoader:{
modules:['node_modules','./loaders']
},
plugins:[
new CopyrightWebpackPlugin({
name:'dell'
})
],
output:{
filename:'[name].js',
path:path.resolve(__dirname,'dist')
}
}
//plugins/copyright-webpack-plugin.js
class CopyrightWebpackPlugin{
constructor(options){
cosnole.log(options)
}
apply(compiler){
compiler.hooks.emit.tapAsync('CopyrightWebpackPlugin',(compiltion,cb)=>{
compiltions.assets['kk.txt'] = {
source:function(){
return 'kk'
},
size:function(){
return 2;
}
};
cb();
})
}
}
module.exports = CopyrightWebpackPlugin;
3.自己实现bundller
bundller实质上是要寻找层层引用,然后转化代码使之执行,我们来写一下
const fs = require('fs');
const path = require('path');
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const babel = require('@babel/core');
//分析一个模块
const moduleAnalyser = (filename) => {
//1.读取文件
const content = fs.readFileSync(filename,'utf-8');
//2.获得抽象语法树
const ast = parser.parse(content,{
sourceType:'module'
});
//3.找出第一个模块中引入模块的相对/绝对地址,转化后的代码
const dependencies = {};
traverse(ast,{
ImportDeclaration({ node }){
const dirname = path.dirname(filename);
const newFile = './' + path.join(dirname,node.source.value);
dependencies[node.source.value] = newFile;
}
});
const { code } = babel.transformFromAst(ast,null,{
presets:['@babel/preset-env']
});
return {
filename,
dependencies,
code
}
}
//4.遍历所有的模块
const makeDenpendencesGraph = (entry) => {
const entryModule = moduleAnalyser(entry);
const graphArray = [ entryModule ];
for(let i = 0; i<graphArray.length; i++){
const item = graphArray[i];
const { dependencies } = item;
if(dependencies){
for(let j in dependencies){
graphArray.push(
moduleAnalyser(dependencies[j])
);
}
}
}
const graph = {};
graphArray.forEach(item => {
graph[item.filename] = {
dependencies:item.dependencies,
code:item.code
}
});
return graph;
}
//5.生成打包后的代码
const generateCode = (entry) => {
const graph = JSON.stringify(makeDenpendencesGraph(entry))
return `
(function(graph){
function require(module){
function localRequire(relativePath){
return require(graph[module].dependencies[relativePath]);
}
var exports = {};
(function(require,exports,code){
eval(code);
})(localRequire,exports,graph[module].code)
return exports;
};
require('${entry}')
})('${graph}')
`;
}
const code = generateCode('./src/index.js');
console.log(code)
终于写完了,参考教程放在下面:
https://coding.imooc.com/clas…