babfile## 模块化开发
前面说了gulp工作流
这边现在来讲下模块化开发
模块化开发是当下最重要的开发范式
模块化只是思想。
模块化的演变过程:
- 文件划分的方式(原始方式完全靠约定)
缺点:
1.污染全局作用域
2.命名冲突
3.无法管理模块依赖关系
2.命名空间方式(放到一个对象,然后再统一使用)
3.然后就是AMD和CMD规范(社区逐渐提出的规范,历史本章不作为重点讲)
然后现在最后就是模块化规范的最佳实践:
node.js的CommonJS和eES Modules (官方推出)
Es Modules基本特性:
- 自动采用严格模式,忽略"use strict"
- 每个ESM模块都是单独的私有作用域
- ESM是通过CORS去请求外部js模块 (需要允许CORS资源请求)
- ESM的script标签会延迟执行脚本(不会像正常script标签立即执行)
CommonJS特点:
- 所有代码都运行在模块作用域,不会污染全局作用域。
- 模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。要想让模块再次运行,必须清除缓存。
- 模块加载的顺序,按照其在代码中出现的顺序。
- 属于动态加载,一导入会把其他模块导出的东西,整体导入
对module对象的描述:
1.module.exports属性
module.exports属性表示当前模块对外输出的接口,其他文件加载该模块,实际上就是读取module.exports变量。
2.exports变量
node为每一个模块提供了一个exports变量(可以说是一个对象),指向 module.exports。这相当于每个模块中都有一句这样的命令 var exports = module.exports;
这样,在对外输出时,可以在这个变量上添加方法。例如 exports.add = function (r){return Math.PI r r};注意:不能把exports直接指向一个值,这样就相当于切断了 exports 和module.exports 的关系。例如 exports=function(x){console.log(x)};
一个模块的对外接口,就是一个单一的值,不能使用exports输出,必须使用 module.exports输出。module.exports=function(x){console.log(x);};
用阮老师的话来说,这两个不好区分,那就放弃 exports,只用 module.exports 就好(手动机智)
在浏览器我们是不适合使用CommonJS规范的,动态读取时查看到这个require模块,然后发请求返回文件,这会程序会卡在那里,这对服务器端不是一个问题,因为所有的模块都存放在本地硬盘,可以同步加载完成,等待时间就是硬盘的读取时间。但是,对于浏览器,这却是一个大问题,因为模块都放在服务器端,等待时间取决于网速的快慢,可能要等很长时间,浏览器处于"假死"状态。
所以浏览器使用的是ESM,但是因为它是es6的模块,有部分浏览器还不支持
app.js的内容
基于我们现在项目不同类型文件过多,过于分散,网络请求频繁,所以我们现在设想所有的前端资源都需要模块化。
前面说的ESM和CommonJs都是对js的模块化。
我们现在需要做的是对所有资源的模块化:
- 新特性代码的编译
- 模块化javascript打包
- 支持不同类型的资源模块
下面就是介绍我们的明星级产品WebPack
webpack
本质上,_webpack_ 是一个现代 JavaScript 应用程序的_静态模块打包器(module bundler)_。当 webpack 处理应用程序时,它会递归地构建一个_依赖关系图(dependency graph)_,其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 _bundle_。
从官网看也可以看出来,它是把所有的文件打包成js文件导出
webpack的配置文件默认使用CommonJs规范
什么是 webpack 模块
对比 Node.js 模块,webpack _模块_能够以各种方式表达它们的依赖关系,几个例子如下:
- ES2015
import
语句 - CommonJS
require()
语句 - AMD
define
和require
语句 - css/sass/less 文件中的
@import
语句。 - 样式(
url(...)
)或 HTML 文件()中的图片链接(image url)
以上这些文件的载入都是会触发webpack打包机制的,检测到对应的文件类型就会触发对应的loader去进行处理成javascript代码输出。
这样它基本对我们常用的资源文件都可以做模块化转成js处理了,它提供的功能可以完美的满足我们的需求了。(webpack !=前端工程化,前端工程化是一个概念)
下面就让我们一起来了解它,先看官方说法:
入口(entry)
webpack 应该使用哪个模块,来作为构建其内部_依赖图_的开始。进入入口起点后,webpack 会找出有哪些模块和库是入口起点(直接和间接)依赖的。
每个依赖项随即被处理,最后输出到称之为 bundles 的文件中
输出(output)
output 属性告诉 webpack 在哪里输出它所创建的 _bundles_,以及如何命名这些文件,默认值为 ./dist
。基本上,整个应用程序结构,都会被编译到你指定的输出路径的文件夹中。你可以通过在配置中指定一个 output
字段
loader
loader 让 webpack 能够去处理那些非 JavaScript 文件(webpack 自身只理解 JavaScript)。loader 可以将所有类型的文件转换为 webpack 能够处理的有效模块,然后你就可以利用 webpack 的打包能力,对它们进行处理。
本质上,webpack loader 将所有类型的文件,转换为应用程序的依赖图(和最终的 bundle)可以直接引用的模块。
插件(plugins)
loader 被用于转换某些类型的模块,而插件则可以用于执行范围更广的任务。插件的范围包括,从打包优化和压缩,一直到重新定义环境中的变量。插件接口功能极其强大,可以用来处理各种各样的任务。
模式
通过选择 development
或 production
之中的一个,来设置 mode
参数,你可以启用相应模式下的 webpack 内置的优化
生产模式下,自动优化打包结果。
开发模式下,自动优化打包速度,添加一些调试过程中的辅助。
none模式下,运行最原始的打包不做任何额外处理。
下面就让我们深入的了解下webpack的工作机制(4):
首先我们来安装webpack和它的脚手架webpack-cli(此工具用于在命令行中运行 webpack,可以再node_modules的bin目录下查看它的.cmd文件中命令执行)
默认执行 webpack入口值是./src,然后根据package.json里的main属性指定的文件为入口文件。
出口输出文件默认值就是上面有写
从 webpack v4.0.0 开始,可以不用引入一个配置文件。
直接命令行执行webpack --mode=production 启动生产模式优化,这样输出到dist文件夹下,就完成了一个我们基本的打包。
下面主要介绍的是主流使用方式,也就是配置文件的方式。
使用yarn init生成一个项目下载webpack基本依赖:
然后开始写我们的webpack.config.js
const path = require('path')
module.exports = {
// 这个属性有三种取值,分别是 production、development 和 none。
// 1. 生产模式下,Webpack 会自动优化打包结果;
// 2. 开发模式下,Webpack 会自动优化打包速度,添加一些调试过程中的辅助;
// 3. None 模式下,Webpack 就是运行最原始的打包,不做任何额外处理;
mode: 'none',
entry: './src/main.js',//支持多入口,官方查看
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'dist')//需要写入绝对路径,借助node的path
}
}
mian.js的代码结构
import createHeading from './heading.js'
const heading = createHeading()
document.body.append(heading)
heading.js的代码结构
export default () => {
const element = document.createElement('h2')
element.textContent = 'Hello world'
element.addEventListener('click', () => {
alert('Hello webpack')
})
return element
}
然后执行webpack none命令打包操作,我们看一下最后weboack的生成内容。
打开bundle.js进行代码折叠
模块0开始调用了一个__webpack_require__.r工具函数,这个函数就是给我们模块添加一个标记,标明它是一个__esModule。然后就是再继续调用__webpack_require__函数传入id为1,这会加载的就是我们mian.js导入的heading.js了,然后根据heading导出方式export default来给__webpack_require__传入的导出成员对象赋值,完成之后跳回模块0代码也就是main.js,__webpack_require__会返回传入模块id的导出成员, 然后再去调用从模块1导出来的成员。
这样我们的所有模块就加载完了。
这就是一个大概的运行过程。
下面再来了解一下loader,webpack中loader的作用就是把其他类型的文件内容转成webpack能处理的js模块放入最后应用程序的依赖图js中(bundle)。
拿css-loader举例
css-loader
const path = require('path')
module.exports = {
mode: 'none',
entry: './src/main.css',
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'dist')
},
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader',
'css-loader'
]
}
]
}
}
在 webpack 的配置中 loader 有两个目标:
test
属性,用于标识出应该被对应的 loader 进行转换的某个或某些文件。use
属性,表示进行转换时,应该使用哪个 loader。
这俩也和我们最开始打包例子一样,会把我们loader转换的模块放到webpack引导函数调用的数组里,在我们的css文件中,使用了两个loader,css-loader是把我们编写的css转换成js模块,然后style-loader是用来把我们css-loader转换的结果通过style标签形式 放入界面中。
lader的执行也是从下到上执行,类似于函数组合的从右到左。
use数组里的loader类似一个管道,前一个执行的结果会传给后一个,最后一个执行的loader一定是返回一段js代码。
下面看一些常用的loader:
file-loader
通过file-loader来处理字体文件和图片文件,代码如下
const path = require('path')
module.exports = {
mode: 'none',
entry: './src/main.js',
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'dist'),
publicPath: 'dist/'
},
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader',
'css-loader'
]
},
{
test: /\.png$/,
use: 'file-loader'
}
]
}
}
项目结构是这样,默认访问index.html,输出地址是dist所以资源路径需要配置一下, publicPath: 'dist/' (publicPath可以配合CDN和hash可以查看官网).
这种file-loader的做法是物理拷贝,还有一种方式是Data Urls的方式表示文件,传统方式是服务器上有一个对应文件,我们通过请求这个地址得到这个文件,而Data Urls是当前地址可以直接表示内容的方式,也就是说这种url中的文本就已经包含了文件的内容:
使用这种url时就是发送任何的http请求,就可以根据这个url直接解析出来内容。
二进制文件会进行base64编码,使用base64来表示内容,然后在webpack中我们就借助url-loader来转换文件为Data Url的方式来展示图片。
url-loader
如下:
const path = require('path')
module.exports = {
mode: 'none',
entry: './src/main.js',
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'dist'),
publicPath: 'dist/'
},
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader',
'css-loader'
]
},
{
test: /\.png$/,
use: {
loader: 'url-loader',
options: {
limit: 10 * 1024 // 10 KB
}
}
}
]
}
}
上面这种配置,碰到图片文件就不会使用物理copy的方式了,就会使用Data Url方式转换为base64来进行展示。
然后我们都知道图片越大转化出来的base64就会越长,太大的图片就不适合放在代码中了,更适合做CDN静态资源。所以我们给url-loader添加一个配置limit配置文件大小,超过这个大小的就做base64得转换了。
所以总结就是:
- 小文件使用Data Urls,减少请求次数(url-loader)
- 大文件单独提取存放,提高加载速度(file-loader)
这样的话url-loader就必须也要下载file-loader了,因为对于超出url-loader设置大小的文件还是会调用file-loader.来进行文件copy.
webpack 的loader分类
- 编译转换类(加载到模块转换为javascript代码)
例如:css-loader - 文件操作类(把文件copy到我们的输出目录,并且把访问路径向外导出)
例如:file-loader - 代码检查类(对代码进行校验)
例如:eslint-loader
babel-loader
再说一种常用的loader,转换es6,也就是babel-loader
下载依赖 babel-loader(转换平台,配置中再具体使用特性转换插件),以及babel的核心模块@babel/core,以及所有新特性转换插件@babel/preset-env
配置:
const path = require('path')
module.exports = {
mode: 'none',
entry: './src/main.js',
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'dist'),
publicPath: 'dist/'
},
module: {
rules: [
{
test: /\.js$/,
use: {
loader: 'babel-loader',
options: {
//babel-polyfill按需引入
presets: [['@babel/preset-env', { useBuiltIns: 'usage', corejs: 3 }]]
}
}
},
{
test: /\.css$/,
use: [
'style-loader',
'css-loader'
]
},
{
test: /\.png$/,
use: {
loader: 'url-loader',
options: {
limit: 10 * 1024 // 10 KB
}
}
}
]
}
}
这样就完成了对es6的编译。
babel得优化
https://zhuanlan.zhihu.com/p/139359864
corejs3 polyfill原型方法
之前最开始说过了几种会触发webpack触发模块loader处理机制的几种情况,还有两种html中情况需要特殊处理一下,一个是图片的src一个是a标签的href需要配置html-loader
loader机制是webpack的核心
我们来实现一个自己的loader,来处理.md文件
首先创建markdown-loader.js文件来处理.md文件,结构如下
about.md中是一段文字
然后我们来编写markdown-loader.js
const marked = require('marked')
module.exports = source => {
// console.log(source)
// return 'console.log("hello ~")'
const html = marked(source)
// return `module.exports = "${html}"`
// return `export default ${JSON.stringify(html)}`
// 返回 html 字符串交给下一个 loader 处理
return html
}
第一种操作时,遵循最后一个loader中返回js代码的规则,所以我们return 'js代码',使用JSON.stringify是因为 html中特殊字符进行转义。
另一种操作就是,我们把这段转换过后html代码返回给下一个loader惊醒处理,像之前说的,loader的处理类似管道。
webpack配置如下
const path = require('path')
module.exports = {
mode: 'none',
entry: './src/main.js',
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'dist'),
publicPath: 'dist/'
},
module: {
rules: [
{
test: /\.md$/,
use: [
'html-loader',
'./markdown-loader'
]
}
]
}
}
这段配置中我们先用markdown-loader把.md文件中的文字转换成html代码,然后再把这段代码交给html-loader进行处理。
webpack插件机制(plugins)
插件目的在于解决 loader 无法实现的其他事。
由于插件可以携带参数/选项,你必须在 webpack 配置中,向 plugins
属性传入 new
实例。
我们现在记录一些常用的plugins
webpack.config.js
const path = require('path')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')
module.exports = {
mode: 'none',
entry: './src/main.js',
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'dist'),
// publicPath: 'dist/'
},
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader',
'css-loader'
]
},
{
test: /\.png$/,
use: {
loader: 'url-loader',
options: {
limit: 10 * 1024 // 10 KB
}
}
}
]
},
plugins: [
new webpack.ProgressPlugin(),
new CleanWebpackPlugin(),
// 用于生成 index.html
new HtmlWebpackPlugin({
title: 'Webpack Plugin Sample',
meta: {
viewport: 'width=device-width'
},
template: './src/index.html'
}),
new CopyWebpackPlugin([
// 'public/**'
'public'
])
]
}
webpack.ProgressPlugin用来监控各个hook执行的进度percentage,输出各个hook的名称和描述。
CleanWebpackPlugin用来每次打包清空dist文件夹。
HtmlWebpackPlugin 指定使用哪个html作为模板生成,里面可以填一些配置信息。<%= htmlWebpackPlugin.options.title %> html内部这样取值.
CopyWebpackPlugin用来copy静态资源文件(文件夹)到,指定的输出目录。
因为这会我们把html生成输出到dist同级目录中了,所以publicPath就注释去掉了。
我们对插件有所了解之后,来实现一个我们自己的插件。
webpack 插件是一个具有 apply
属性的 JavaScript 对象。apply
属性会被 webpack compiler(编译) 调用,并且 compiler 对象可在整个编译生命周期访问。
const path = require('path')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')
class MyPlugin {
apply (compiler) {
console.log('MyPlugin 启动')
compiler.hooks.emit.tap('MyPlugin', compilation => {
// compilation => 可以理解为此次打包的上下文
for (const name in compilation.assets) {
// console.log(name)
// console.log(compilation.assets[name].source())
if (name.endsWith('.js')) {//模块后缀是js
const contents = compilation.assets[name].source()//通过source方法获取内容
const withoutComments = contents.replace(/\/\*\*+\*\//g, '')
compilation.assets[name] = {
source: () => withoutComments,
size: () => withoutComments.length
}
}
}
})
}
}
module.exports = {
mode: 'none',
entry: './src/main.js',
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'dist'),
// publicPath: 'dist/'
},
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader',
'css-loader'
]
},
{
test: /\.png$/,
use: {
loader: 'url-loader',
options: {
limit: 10 * 1024 // 10 KB
}
}
}
]
},
plugins: [
new CleanWebpackPlugin(),
// 用于生成 index.html
new HtmlWebpackPlugin({
title: 'Webpack Plugin Sample',
meta: {
viewport: 'width=device-width'
},
template: './src/index.html'
}),
// 用于生成 about.html
new HtmlWebpackPlugin({
filename: 'about.html'
}),
new CopyWebpackPlugin([
// 'public/**'
'public'
]),
new MyPlugin()
]
}
我们写的apply方法会在webpack编译期调用,compiler
对象代表了完整的 webpack 环境配置(更多信息可以查看官方文档)。我们可以通过它来获取,webpack编译期间的hooks生命周期钩子函数(具体生命周期可以看管方手册),compiler hook 的 tap 方法的第一个参数,应该是驼峰式命名的插件名称。建议为此使用一个常量,以便它可以在所有 hook 中复用。
webpack钩子
我们再edit(生成资源到 output 目录之前�触发,这是一个异步串行 AsyncSeriesHook 钩子)这个钩子中注册了一个MyPlugin的插件,然后通过compilation对象(代表了一次资源版本构建,一个 compilation 对象表现了当前的模块资源、编译生成资源、变化的文件、以及被跟踪依赖的状态信息。)来获取模块的信息,如果Chunk是js,通过
compilation.assets[name].source()
获取内容加工处理后,重写当前模块的 对应source,size方法。
这样我们就实现了一个我们自己的插件(更复杂的插件可以查看官方手册,这里只做一个简单的说明)。
webpack通过 --watch 工作的话不会理解结束,会监视我们的文件变化,除非我们手动借宿。
webpack-dev-server
为你提供了一个简单的 web 服务器,并且能够自动编译,实时重新加载(live reloading),提供代理url的功能。
简单例子:
const path = require('path')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')
module.exports = {
mode: 'development',
entry: './src/main.js',
output: { //默认执行webpack打包输出目录 ./dist,
filename: 'bundle.js',
path: path.join(__dirname, 'dist')
},
devtool:'cheap-module-eval-source-map',
devServer: {
contentBase: './public',
hot: true,
// hotOnly: true // 只使用 HMR,不会 fallback 到 live reloading
proxy: {
'/api': {
// http://localhost:8080/api/users -> https://api.github.com/api/users
target: 'https://api.github.com',
// http://localhost:8080/api/users -> https://api.github.com/users
pathRewrite: {
'^/api': ''
},
// 不能使用 localhost:8080 作为请求 GitHub 的主机名
changeOrigin: true
}
}
},
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader',
'css-loader'
]
},
{
test: /\.png$/,
use: {
loader: 'url-loader',
options: {
limit: 10 * 1024 // 10 KB
}
}
}
]
},
plugins: [
//new CleanWebpackPlugin(),
// 用于生成 index.html
new HtmlWebpackPlugin({
title: 'Webpack Tutorials',
meta: {
viewport: 'width=device-width'
},
template: './src/index.html'
}),
// // 开发阶段最好不要使用这个插件
// new CopyWebpackPlugin(['public'])
new webpack.HotModuleReplacementPlugin()
]
}
contentBase
告诉服务器从哪里提供内容。只有在你想要提供静态文件时才需要。devServer.publicPath
将用于确定应该从哪里提供 bundle,并且此选项优先。publicPath
此路径下的打包文件可在浏览器中访问。
假设服务器运行在 http://localhost:8080
并且 output.filename
被设置为 bundle.js
。默认 publicPath
是 "/"
,所以你的包(bundle)可以通过 http://localhost:8080/bundle.js
访问。
hot启动热更新,注意,webpack。完全启用HMR需要使用HotModuleReplacementPlugin。如果webpack或webpack-dev-server以——hot选项启动,这个插件将自动添加,所以您可能不需要将它添加到您的webpack.config.js中,在这里是做了手动添加的。
我们在开发富文本编辑器时,js模块热更新,输入的内容也会清空掉,因为它并不能确定你的js返回了什么。
这个时候我们也可以通过module.hot来控制热更新。
例如:
import createEditor from './editor'
import background from './better.png'
import './global.css'
const editor = createEditor()
document.body.appendChild(editor)
const img = new Image()
img.src = background
document.body.appendChild(img)
// ============ 以下用于处理 HMR,与业务代码无关 ============
// console.log(createEditor)
if (module.hot) {
let lastEditor = editor
module.hot.accept('./editor', () => {
// console.log('editor 模块更新了,需要这里手动处理热替换逻辑')
// console.log(createEditor)
const value = lastEditor.innerHTML
document.body.removeChild(lastEditor)
const newEditor = createEditor()
newEditor.innerHTML = value
document.body.appendChild(newEditor)
lastEditor = newEditor
})
module.hot.accept('./better.png', () => {
img.src = background
console.log(background)
})
}
正常来说我们通过module.hto控制的模块报错后,还是会走默认的热更新,所以hotOnly配置它来阻止。
其他的一些dev-server配置信息可查看:
webpack-dev-server配置
然后我们把webpack-dev-server 命令添加到npm scripts 中就可以运行了
devtool
根据需要开发阶段个人感觉:cheap-module-eval-source-map,eval-source-map会方便点,首次构建慢,重新构建还可以接受。
接下来我们做下生产环境和开发环境的区分
首先是一个公共的配置webpack.common.js
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
entry: './src/main.js',
output: {
filename: 'js/bundle.js'
},
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader',
'css-loader'
]
},
{
test: /\.(png|jpe?g|gif)$/,
use: {
loader: 'file-loader',
options: {
outputPath: 'img',
name: '[name].[ext]'
}
}
}
]
},
plugins: [
new HtmlWebpackPlugin({
title: 'Webpack Tutorial',
template: './src/index.html'
})
]
}
webpack.dev.js 开发环境的配置
const webpack = require('webpack')
const merge = require('webpack-merge')
const common = require('./webpack.common')
module.exports = merge(common, {
mode: 'development',
devtool: 'cheap-eval-module-source-map',
devServer: {
hot: true,
contentBase: 'public'
},
plugins: [
new webpack.HotModuleReplacementPlugin()
]
})
webpack.prod.js生产环境的配置
const merge = require('webpack-merge')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const common = require('./webpack.common')
module.exports = merge(common, {
mode: 'production',
plugins: [
new CleanWebpackPlugin(),
new CopyWebpackPlugin(['public'])
]
})
最后给package.json添加scripts命令
"build": "webpack --config webpack.prod.js",
"start": "webpack-dev-server --config webpack.dev.js"
webpack优化
然后就是一些webpack优化相关的知识了。
optimization 对象
webpack不同的mode会进行不同的优化,但是这些优化也还都是可以手动配置的
webpck中mode模式的优化
module.exports = {
mode: 'none',
entry: './src/index.js',
output: {
filename: 'bundle.js'
},
optimization: {
// 模块只导出被使用的成员
usedExports: true,
// 尽可能合并每一个模块到一个函数中
concatenateModules: true,
// 压缩输出结果
// minimize: true
}
}
也就是这样配置,我们就可以(usedExports配合minimize)只导出被使用的成员,usedExports标识导出被使用成员(导出名称会被处理做单个标记字符),然后minimize把无用代码筛除,然后压缩输出,这样就形成了摇树的功能。
waring:老的webpack版本有人碰到过使用babel后摇树无效的情况。
因为摇树要使用 ES2015模块语法,所以配置babel-loader
module: {
rules: [
{
test: /\.js$/,
use: {
loader: 'babel-loader',
options: {
presets: [
// 如果 Babel 加载模块时已经转换了 ESM,则会导致 Tree Shaking 失效
// ['@babel/preset-env', { modules: 'commonjs' }]
// ['@babel/preset-env', { modules: false }]
// 也可以使用默认配置,也就是 auto,这样 babel-loader 会自动关闭 ESM 转换
['@babel/preset-env', { modules: 'auto' }]
]
}
}
}
]
}
这会我们会碰到另外一个问题
例如:
index.js中引入的css和js原型方法都没有导出,只是原型做了扩展。这样我们上面的usedExports就不会标识,最后摇树 的时候就会把他们去掉,这样和我们的需求不符。
所以我们设置optimization中的sideEffects为true,然后在package.json中设置:
规则如下:
Mark the file as side-effect-free 标记副作用文件
通过指定sideEffects为false可以告诉webpack的编译器,我们的代码是纯的,可以安全地进行摇树优化。有两种方式:
1.设置sideEffects属性为false
这里是指定那个那类文件(有副作用),就不会被摇掉,不包含里面的引用文件。
“副作用”定义为导入时执行特殊行为的代码,而不是公开一个或多个导出。比如 polyfills,它影响全局范围,通常不提供导出。
mode为production 或者设置optimize-minimize标志为true时都会进行一个摇树的优化
总结
要使用摇树优化:
- 使用 ES2015模块语法
- 确保没有编译器将 ES2015模块语法转换为 CommonJS 模块
- 在项目的 package.json 文件中添加“ sideEffects”属性
- 使用生产模式配置选项删除dead-code,达到摇树优化效果
您可以将应用程序想象为一棵树。 你实际使用的源代码和库代表了绿色的树叶,dead-code 代表着秋天里枯萎的树叶。为了摆脱枯叶,你不得不摇动树木,使它们掉下。
然后就是配置多入口和提取公共模块:
这样俩文件的公共模块就会被提取出来。
这样就会涉及到:
分离app(应用程序)和vendor(第三方库)入口
小于webpack4的版本我们 分离app和第三方代码 webpack\<4
webpack4分离公共代码
webpack4抽离公共代码方式
还有一种可以动态引入的方式就是通过es6的import()
例如:
最后一部分就是webpack生产文件的hash,js的压缩,css的压缩相关信息,如下:
最后需要了解一下runtimeChunk,属性。
设置runtimeChunk是将包含chunks 映射关系
的 list单独从 app.js里提取出来,因为每一个 chunk 的 id 基本都是基于内容 hash 出来的,所以每次改动都会影响它,如果不将它提取出来的话,等于app.js每次都会改变。缓存就失效了。设置runtimeChunk之后,webpack就会生成一个个runtime~xxx.js的文件。
然后每次更改所谓的运行时代码文件时,打包构建时app.js的hash值是不会改变的。如果每次项目更新都会更改app.js的hash值,那么用户端浏览器每次都需要重新加载变化的app.js,如果项目大切优化分包没做好的话会导致第一次加载很耗时,导致用户体验变差。现在设置了runtimeChunk,就解决了这样的问题。所以这样做的目的是避免文件的频繁变更导致浏览器缓存失效,所以其是更好的利用缓存。提升用户体验。
最后Rollup用来做框架会合适一点(了解)
parcel零配置前端应用打包器(了解,写demo可以使用)
后续下一篇讲下eslint和webpack的集成。
发现一遍webpack优化讲的很好,进一步优化可以参考
webpack4大结局:加入腾讯IM配置策略,实现前端工程化环境极致优化
本文内容借鉴于 拉钩大前端训练营