This commit is contained in:
周伟
2022-05-11 19:04:14 +08:00
parent 9440ac7291
commit d9c5ffd627
826 changed files with 0 additions and 481675 deletions

View File

@@ -1,978 +1,489 @@
<!DOCTYPE html>
<!-- saved from url=(0046)https://kaiiiz.github.io/hexo-theme-book-demo/ -->
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no">
<link rel="icon" href="/static/favicon.png">
<title>03 构建提速:如何正确使用 SourceMap.md.html</title>
<!-- Spectre.css framework -->
<link rel="stylesheet" href="/static/index.css">
<!-- theme css & js -->
<meta name="generator" content="Hexo 4.2.0">
</head>
<body>
<div class="book-container">
<div class="book-sidebar">
<div class="book-brand">
<a href="/">
<img src="/static/favicon.png">
<span>技术文章摘抄</span>
</a>
</div>
<div class="book-menu uncollapsible">
<ul class="uncollapsible">
<li><a href="/" class="current-tab">首页</a></li>
</ul>
<ul class="uncollapsible">
<li><a href="../">上一级</a></li>
</ul>
<ul class="uncollapsible">
<li>
<a href="/专栏/前端工程化精讲-完/00 开篇词 建立上帝视角,全面系统掌握前端效率工程化.md.html">00 开篇词 建立上帝视角,全面系统掌握前端效率工程化.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/01 项目基石:前端脚手架工具探秘.md.html">01 项目基石:前端脚手架工具探秘.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/02 界面调试:热更新技术如何开着飞机修引擎?.md.html">02 界面调试:热更新技术如何开着飞机修引擎?.md.html</a>
</li>
<li>
<a class="current-tab" href="/专栏/前端工程化精讲-完/03 构建提速:如何正确使用 SourceMap.md.html">03 构建提速:如何正确使用 SourceMap.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/04 接口调试Mock 工具如何快速进行接口调试?.md.html">04 接口调试Mock 工具如何快速进行接口调试?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/05 编码效率:如何提高编写代码的效率?.md.html">05 编码效率:如何提高编写代码的效率?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/06 团队工具:如何利用云开发提升团队开发效率?.md.html">06 团队工具:如何利用云开发提升团队开发效率?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/07 低代码工具:如何用更少的代码实现更灵活的需求.md.html">07 低代码工具:如何用更少的代码实现更灵活的需求.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/08 无代码工具:如何做到不写代码就能高效交付?.md.html">08 无代码工具:如何做到不写代码就能高效交付?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/09 构建总览:前端构建工具的演进.md.html">09 构建总览:前端构建工具的演进.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/10 流程分解Webpack 的完整构建流程.md.html">10 流程分解Webpack 的完整构建流程.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/11 编译提效:如何为 Webpack 编译阶段提速?.md.html">11 编译提效:如何为 Webpack 编译阶段提速?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/12 打包提效:如何为 Webpack 打包阶段提速?.md.html">12 打包提效:如何为 Webpack 打包阶段提速?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/13 缓存优化:那些基于缓存的优化方案.md.html">13 缓存优化:那些基于缓存的优化方案.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/14 增量构建Webpack 中的增量构建.md.html">14 增量构建Webpack 中的增量构建.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/15 版本特性Webpack 5 中的优化细节.md.html">15 版本特性Webpack 5 中的优化细节.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/16 无包构建:盘点那些 No-bundle 的构建方案.md.html">16 无包构建:盘点那些 No-bundle 的构建方案.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/17 部署初探:为什么一般不在开发环境下部署代码?.md.html">17 部署初探:为什么一般不在开发环境下部署代码?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/18 工具盘点:掌握那些流行的代码部署工具.md.html">18 工具盘点:掌握那些流行的代码部署工具.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/19 安装提效:部署流程中的依赖安装效率优化.md.html">19 安装提效:部署流程中的依赖安装效率优化.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/20 流程优化:部署流程中的构建流程策略优化.md.html">20 流程优化:部署流程中的构建流程策略优化.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/21 容器方案:从构建到部署,容器化方案的优势有哪些?.md.html">21 容器方案:从构建到部署,容器化方案的优势有哪些?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/22 案例分析:搭建基本的前端高效部署系统.md.html">22 案例分析:搭建基本的前端高效部署系统.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/23 结束语 前端效率工程化的未来展望.md.html">23 结束语 前端效率工程化的未来展望.md.html</a>
</li>
</ul>
</div>
</div>
<div class="sidebar-toggle" onclick="sidebar_toggle()" onmouseover="add_inner()" onmouseleave="remove_inner()">
<div class="sidebar-toggle-inner"></div>
</div>
<script>
function add_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.add('show')
}
function remove_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.remove('show')
}
function sidebar_toggle() {
let sidebar_toggle = document.querySelector('.sidebar-toggle')
let sidebar = document.querySelector('.book-sidebar')
let content = document.querySelector('.off-canvas-content')
if (sidebar_toggle.classList.contains('extend')) { // show
sidebar_toggle.classList.remove('extend')
sidebar.classList.remove('hide')
content.classList.remove('extend')
} else { // hide
sidebar_toggle.classList.add('extend')
sidebar.classList.add('hide')
content.classList.add('extend')
}
}
function open_sidebar() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.add('show')
overlay.classList.add('show')
}
function hide_canvas() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.remove('show')
overlay.classList.remove('show')
}
</script>
<div class="off-canvas-content">
<div class="columns">
<div class="column col-12 col-lg-12">
<div class="book-navbar">
<!-- For Responsive Layout -->
<header class="navbar">
<section class="navbar-section">
<a onclick="open_sidebar()">
<i class="icon icon-menu"></i>
</a>
</section>
</header>
</div>
<div class="book-content" style="max-width: 960px; margin: 0 auto;
overflow-x: auto;
overflow-y: hidden;">
<div class="book-post">
<p id="tip" align="center"></p>
<div><h1>03 构建提速:如何正确使用 SourceMap</h1>
<p>在上一课时中我们聊了开发时的热更新机制和其中的技术细节,热更新能帮助我们在开发时快速预览代码效果,免去了手动执行编译和刷新浏览器的操作。课后留的思考题是找一个实现了 HMR 的 Loader查看其中用到哪些热替换的 API希望通过这个题目能让你加深对相关知识点的印象。</p>
<p>那么除了热更新以外,项目的开发环境还有哪些在影响着我们的开发效率呢?在过去的工作中,公司同事就曾问过我一个问题:为什么我的项目在开发环境下每次构建还是很卡?每次保存完代码都要过 1~2 秒才能看到效果,这是怎么回事呢?其实这里面的原因主要是这位同事在开发时选择的 Source Map 设定不对。今天我们就来具体讨论下这个问题。首先,什么是 <strong>Source Map</strong> 呢?</p>
<h3>什么是 Source Map</h3>
<p>在前端开发过程中,通常我们编写的源代码会经过多重处理(编译、封装、压缩等),最后形成产物代码。于是在浏览器中调试产物代码时,我们往往会发现代码变得面目全非,例如:</p>
<p><img src="assets/Ciqc1F85_FmAA4UeAABWNiHqsWQ595.png" alt="Drawing 0.png" /></p>
<p>因此,我们需要一种在调试时将产物代码显示回源代码的功能,<strong>source map</strong> 就是实现这一目标的工具。</p>
<p>source-map 的基本原理是,在编译处理的过程中,在生成产物代码的同时生成产物代码中被转换的部分与源代码中相应部分的映射关系表。有了这样一张完整的映射表,我们就可以通过 Chrome 控制台中的&quot;Enable Javascript source map&quot;来实现调试时的显示与定位源代码功能。</p>
<p>对于同一个源文件,根据不同的目标,可以生成不同效果的 source map。它们在<strong>构建速度</strong><strong>质量</strong>(反解代码与源代码的接近程度以及调试时行号列号等辅助信息的对应情况)、<strong>访问方式</strong>(在产物文件中或是单独生成 source map 文件)和<strong>文件大小</strong>等方面各不相同。在开发环境和生产环境下,我们对于 source map 功能的期望也有所不同:</p>
<ul>
<li><strong>在开发环境中</strong>,通常我们关注的是构建速度快,质量高,以便于提升开发效率,而不关注生成文件的大小和访问方式。</li>
<li><strong>在生产环境中</strong>,通常我们更关注是否需要提供线上 source map , 生成的文件大小和访问方式是否会对页面性能造成影响等,其次才是质量和构建速度。</li>
</ul>
<h3>Webpack 中的 source map 预设</h3>
<p>在 Webpack 中,通过设置 devtool 来选择 source map 的预设类型,文档中共有 <a href="https://webpack.js.org/configuration/devtool/#devtool">20 余种</a> source map 的预设(注意:其中部分预设实际效果与其他预设相同,即页面表格中空白行条目)可供选择,这些预设通常包含了 &quot;eval&quot; &quot;cheap&quot; &quot;module&quot; &quot;inline&quot; &quot;hidden&quot; &quot;nosource&quot; &quot;source-map&quot; 等关键字的组合,这些关键字的具体逻辑如下:</p>
<pre><code>webpack/lib/WebpackOptionsApply.js:232
if (options.devtool.includes(&quot;source-map&quot;)) {
const hidden = options.devtool.includes(&quot;hidden&quot;);
const inline = options.devtool.includes(&quot;inline&quot;);
const evalWrapped = options.devtool.includes(&quot;eval&quot;);
const cheap = options.devtool.includes(&quot;cheap&quot;);
const moduleMaps = options.devtool.includes(&quot;module&quot;);
const noSources = options.devtool.includes(&quot;nosources&quot;);
const Plugin = evalWrapped
? require(&quot;./EvalSourceMapDevToolPlugin&quot;)
: require(&quot;./SourceMapDevToolPlugin&quot;);
new Plugin({
filename: inline ? null : options.output.sourceMapFilename,
moduleFilenameTemplate: options.output.devtoolModuleFilenameTemplate,
fallbackModuleFilenameTemplate:
options.output.devtoolFallbackModuleFilenameTemplate,
append: hidden ? false : undefined,
module: moduleMaps ? true : cheap ? false : true,
columns: cheap ? false : true,
noSources: noSources,
namespace: options.output.devtoolNamespace
}).apply(compiler);
} else if (options.devtool.includes(&quot;eval&quot;)) {
const EvalDevToolModulePlugin = require(&quot;./EvalDevToolModulePlugin&quot;);
new EvalDevToolModulePlugin({
moduleFilenameTemplate: options.output.devtoolModuleFilenameTemplate,
namespace: options.output.devtoolNamespace
}).apply(compiler);
}
</code></pre>
<p>如上面的代码所示, devtool 的值匹配并非精确匹配,某个关键字只要包含在赋值中即可获得匹配,例如:'foo-eval-bar' 等同于 'eval''cheapfoo-source-map' 等同于 'cheap-source-map'。</p>
<h4>Source Map 名称关键字</h4>
<p>各字段的作用各不相同,为了便于记忆,我们在这里简单整理下这些关键字的作用:</p>
<ul>
<li><strong>false</strong>:即不开启 source map 功能,其他不符合上述规则的赋值也等价于 false。</li>
<li><strong>eval</strong>:是指在编译器中使用 EvalDevToolModulePlugin 作为 source map 的处理插件。</li>
<li><strong>[xxx-...]source-map</strong>:根据 devtool 对应值中是否有 <strong>eval</strong> 关键字来决定使用 EvalSourceMapDevToolPlugin 或 SourceMapDevToolPlugin 作为 source map 的处理插件,其余关键字则决定传入到插件的相关字段赋值。</li>
<li><strong>inline</strong>:决定是否传入插件的 filename 参数,作用是决定单独生成 source map 文件还是在行内显示,<strong>该参数在 eval- 参数存在时无效</strong></li>
<li><strong>hidden</strong>:决定传入插件 append 的赋值,作用是判断是否添加 SourceMappingURL 的注释,<strong>该参数在 eval- 参数存在时无效</strong></li>
<li><strong>module</strong>:为 true 时传入插件的 module 为 true 作用是为加载器Loaders生成 source map。</li>
<li><strong>cheap</strong>:这个关键字有两处作用。首先,当 module 为 false 时,它决定插件 module 参数的最终取值,最终取值与 cheap 相反。其次,它决定插件 columns 参数的取值,作用是决定生成的 source map 中是否包含列信息,在不包含列信息的情况下,调试时只能定位到指定代码所在的行而定位不到所在的列。</li>
<li><strong>nosource</strong>nosource 决定了插件中 noSource 变量的取值,作用是决定生成的 source map 中是否包含源代码信息,不包含源码情况下只能显示调用堆栈信息。</li>
</ul>
<h4>Source Map 处理插件</h4>
<p>从上面的规则中我们还可以看到,根据不同规则,实际上 Webpack 是从三种插件中选择其一作为 source map 的处理插件。</p>
<ul>
<li><a href="https://github.com/webpack/webpack/blob/master/lib/EvalDevToolModulePlugin.js">EvalDevToolModulePlugin</a>:模块代码后添加 sourceURL=webpack:///+ 模块引用路径,不生成 source map 内容,模块产物代码通过 eval() 封装。</li>
<li><a href="https://github.com/webpack/webpack/blob/master/lib/EvalSourceMapDevToolPlugin.js">EvalSourceMapDevToolPlugin</a>:生成 base64 格式的 source map 并附加在模块代码之后, source map 后添加 sourceURL=webpack:///+ 模块引用路径,不单独生成文件,模块产物代码通过 eval() 封装。</li>
<li><a href="https://github.com/webpack/webpack/blob/master/lib/SourceMapDevToolPlugin.js">SourceMapDevToolPlugin</a>:生成单独的 .map 文件,模块产物代码不通过 eval 封装。</li>
</ul>
<p>通过上面的代码分析,我们了解了不同参数在 Webpack 运行时起到的作用。那么这些不同参数组合下的各种预设对我们的 source map 生成又各自会产生什么样的效果呢?下面我们通过示例来看一下。</p>
<h3>不同预设的示例结果对比</h3>
<p>下面,以课程示例代码 <a href="https://github.com/fe-efficiency/lessons_fe_efficiency/tree/master/03_develop_environment">03_develop_environment</a> 为例,我们来对比下几种常用预设的差异(为了使时间差异更明显,示例中引入了几个大的类库文件):</p>
<p><img src="assets/Ciqc1F87kyiAZvHdAAIGvohk2F4144.png" alt="12.png" /></p>
<blockquote>
<p>*注1“/”前后分别表示产物 js 大小和对应 .map 大小。
*注2“/”前后分别表示初次构建时间和开启 watch 模式下 rebuild 时间。对应统计的都是 development 模式下的笔者机器环境下几次构建时间的平均值,只作为相对快慢与量级的比较。</p>
</blockquote>
<h4>不同预设的效果总结</h4>
<p>从上图的数据中我们不难发现,选择不同的 devtool 类型在以下几个方面会产生不同的效果。</p>
<ul>
<li>质量:生成的 source map 的质量分为 5 个级别,对应的调试便捷性依次降低:源代码 &gt; 缺少列信息的源代码 &gt; loader 转换后的代码 &gt; 生成后的产物代码 &gt; 无法显示代码(具体参见下面的<strong>不同质量的源码示例</strong>小节)。对应对质量产生影响的预设关键字优先级为 souce-map = eval-source-map &gt; cheap-module- &gt; cheap- &gt; eval = none &gt; nosource-。</li>
<li>构建的速度:再次构建速度都要显著快于初次构建速度。不同环境下关注的速度也不同:
<ul>
<li>在开发环境下:一直开着 devServer再次构建的速度对我们的效率影响远大于初次构建的速度。从结果中可以看到eval- 对应的 EvalSourceMapDevToolPlugin 整体要快于不带 eval- 的 SourceMapDevToolPlugin。尤其在质量最佳的配置下eval-source-map 的再次构建速度要远快于其他几种。而同样插件配置下,不同质量配置与构建速度成反比,但差异程度有限,更多是看具体项目的大小而定。</li>
<li>在生产环境下:通常不会开启再次构建,因此相比再次构建,初次构建的速度更值得关注,甚至对构建速度以外因素的考虑要优先于对构建速度的考虑,这一部分我们在之后的构建优化的课程里会再次讨论到。</li>
</ul>
</li>
<li>包的大小和生成方式:在开发环境下我们并不需要关注这些因素,正如在开发环境下也通常不考虑使用分包等优化方式。我们需要关注速度和质量来保证我们的高效开发体验,而其他的部分则是在生产环境下需要考虑的问题。</li>
</ul>
<h4>不同质量的源码示例</h4>
<ul>
<li>源码且包含列信息</li>
</ul>
<p><img src="assets/CgqCHl85_KuANSVfAADSE8VO7Qg572.png" alt="Drawing 1.png" /></p>
<ul>
<li>源码不包含列信息</li>
</ul>
<p><img src="assets/Ciqc1F85_LCAMTlgAADhqpZ4v9o628.png" alt="Drawing 2.png" /></p>
<ul>
<li>Loader转换后代码</li>
</ul>
<p><img src="assets/CgqCHl85_LqAPrYzAADfmUwS_JE006.png" alt="Drawing 3.png" /></p>
<ul>
<li>生成后的产物代码</li>
</ul>
<p><img src="assets/CgqCHl85_MGAHhmMAAKGwvDeXIM418.png" alt="Drawing 4.png" /></p>
<h4>开发环境下 Source Map 推荐预设</h4>
<p>在这里我们对开发环境下使用的推荐预设做一个总结(生产环境的预设我们将在之后的构建效率篇中再具体分析):</p>
<ul>
<li>通常来说,开发环境首选哪一种预设取决于 source map 对于我们的帮助程度。</li>
<li>如果对项目代码了如指掌,查看产物代码也可以无障碍地了解对应源代码的部分,那就可以关闭 devtool 或使用 eval 来获得最快构建速度。</li>
<li>反之如果在调试时,需要通过 source map 来快速定位到源代码,则优先考虑使用 <strong>eval-cheap-modulesource-map</strong>,它的质量与初次/再次构建速度都属于次优级,以牺牲定位到列的功能为代价换取更快的构建速度通常也是值得的。</li>
<li>在其他情况下,根据对质量要求更高或是对速度要求更高的不同情况,可以分别考虑使用 <strong>eval-source-map</strong><strong>eval-cheap-source-map</strong></li>
</ul>
<p>了解了开发环境下如何选择 source map 预设后,我们再来补充几种工具和脚手架中的默认预设:</p>
<ul>
<li>Webpack 配置中,如果不设定 devtool则使用默认值 eval即速度与 devtool:false 几乎相同、但模块代码后多了 sourceURL 以帮助定位模块的文件名称。</li>
<li><a href="https://github.com/facebook/create-react-app/blob/fa648daca1dedd97aec4fa3bae8752c4dcf37e6f/packages/react-scripts/config/webpack.config.js">create-react-app 中</a>,在生产环境下,根据 shouldUseSourceMap 参数决定使用source-map或 false在开发环境下使用cheap-module-source-map不包含列信息的源代码但更快</li>
<li><a href="https://github.com/vuejs/vue-cli/blob/36f961e43dc76705878659247b563e2af83138ce/packages/%40vue/cli-service/lib/commands/serve.js">vue-cli-service 中</a>,与 creat-react-app 中相同。</li>
</ul>
<p>除了上面讨论的这些简单的预设外Webpack 还允许开发者直接使用对应插件来进行更精细化的 source map 控制,在开发环境下我们首选的还是 EvalSourceMapDevToolPlugin。下面我们再来看看如何直接使用这个插件进行优化。</p>
<h4>EvalSourceMapDevToolPlugin 的使用</h4>
<p>在 EvalSourceMapDevToolPlugin 的 <a href="https://webpack.js.org/plugins/eval-source-map-dev-tool-plugin/">传入参数</a>中,除了上面和预设相关的 filename、append、module、columns 外,还有影响注释内容的 moduleFilenameTemplate 和 protocol以及影响处理范围的 test、include、exclude。这里重点看处理范围的参数因为通常我们需要调试的是开发的业务代码部分而非依赖的第三方模块部分。因此在生成 source map 的时候如果可以排除第三方模块的部分而只生成业务代码的 source map无疑能进一步提升构建的速度例如示例</p>
<pre><code>webpack.config.js
...
//devtool: 'eval-source-map',
devtool: false,
plugins: [
new webpack.EvalSourceMapDevToolPlugin({
exclude: /node_modules/,
module: true,
columns: false
})
],
...
</code></pre>
<p>在上面的示例中,我们将 devtool 设为 false而直接使用 EvalSourceMapDevToolPlugin通过传入 module: true 和 column:false达到和预设 eval-cheap-module-source-map 一样的质量,同时传入 exclude 参数,排除第三方依赖包的 source map 生成。保存设定后通过运行可以看到,在文件体积减小(尽管开发环境并不关注文件大小)的同时,再次构建的速度相比上面表格中的速度提升了将近一倍,达到了最快一级。</p>
<p><img src="assets/CgqCHl85_N2AUkcpAAEqvMKhgVQ549.png" alt="Drawing 5.png" /></p>
<p>类似这样的优化可以帮助我们在一些大型项目中,通过自定义设置来获取比预设更好的开发体验。</p>
<h3>总结</h3>
<p>在今天这一课时中我们主要了解了提升开发效率的另一个重要工具——source map 的用途和使用方法。我们分析了 Webpack 中 devtool 的各种参数预设的组合规则、使用效果及其背后的原理。对于开发环境,我们根据一组示例对比分析来了解通常情况下的最佳选择,也知道了如何直接使用插件来达到更细致的优化。</p>
<p>限于篇幅原因,关于 source map 这一课时还有两个与提效无关的小细节没有提到,一个是生成的 source map 的内容,即浏览器工具是如何将 source map 内容映射回源文件的,如果你感兴趣可以通过这个<a href="https://www.ruanyifeng.com/blog/2013/01/javascript_source_map.html">链接</a>进一步了解;另一个是我们在控制台的网络面板中通常看不到 source map 文件的请求,其原因是出于安全考虑 Chrome 隐藏了 source map 的请求,需要通过 <a href="chrome://net-export/">net-log</a> 来查询。</p>
<p><strong>最后还是留一个小作业</strong>:不知道你有没有留意过自己项目里的 source map 使用的是哪一种生成方式吗?可以根据这一课时的内容对它进行调整和观察效果,也欢迎你在课后留言区讨论项目里对 source map 的优化方案。</p>
</div>
</div>
<div>
<div style="float: left">
<a href="/专栏/前端工程化精讲-完/02 界面调试:热更新技术如何开着飞机修引擎?.md.html">上一页</a>
</div>
<div style="float: right">
<a href="/专栏/前端工程化精讲-完/04 接口调试Mock 工具如何快速进行接口调试?.md.html">下一页</a>
</div>
</div>
</div>
</div>
</div>
</div>
<a class="off-canvas-overlay" onclick="hide_canvas()"></a>
</div>
<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v652eace1692a40cfa3763df669d7439c1639079717194" integrity="sha512-Gi7xpJR8tSkrpF7aordPZQlW2DLtzUlZcumS8dMQjwDHEnw9I7ZLyiOj/6tZStRBGtGgN6ceN6cMH8z7etPGlw==" data-cf-beacon='{"rayId":"7099774c19e23cfa","version":"2021.12.0","r":1,"token":"1f5d475227ce4f0089a7cff1ab17c0f5","si":100}' crossorigin="anonymous"></script>
</body>
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-NPSEEVD756"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', 'G-NPSEEVD756');
var path = window.location.pathname
var cookie = getCookie("lastPath");
console.log(path)
if (path.replace("/", "") === "") {
if (cookie.replace("/", "") !== "") {
console.log(cookie)
document.getElementById("tip").innerHTML = "<a href='" + cookie + "'>跳转到上次进度</a>"
}
} else {
setCookie("lastPath", path)
}
function setCookie(cname, cvalue) {
var d = new Date();
d.setTime(d.getTime() + (180 * 24 * 60 * 60 * 1000));
var expires = "expires=" + d.toGMTString();
document.cookie = cname + "=" + cvalue + "; " + expires + ";path = /";
}
function getCookie(cname) {
var name = cname + "=";
var ca = document.cookie.split(';');
for (var i = 0; i < ca.length; i++) {
var c = ca[i].trim();
if (c.indexOf(name) === 0) return c.substring(name.length, c.length);
}
return "";
}
</script>
</html>

View File

@@ -1,936 +1,468 @@
<!DOCTYPE html>
<!-- saved from url=(0046)https://kaiiiz.github.io/hexo-theme-book-demo/ -->
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no">
<link rel="icon" href="/static/favicon.png">
<title>06 团队工具:如何利用云开发提升团队开发效率?.md.html</title>
<!-- Spectre.css framework -->
<link rel="stylesheet" href="/static/index.css">
<!-- theme css & js -->
<meta name="generator" content="Hexo 4.2.0">
</head>
<body>
<div class="book-container">
<div class="book-sidebar">
<div class="book-brand">
<a href="/">
<img src="/static/favicon.png">
<span>技术文章摘抄</span>
</a>
</div>
<div class="book-menu uncollapsible">
<ul class="uncollapsible">
<li><a href="/" class="current-tab">首页</a></li>
</ul>
<ul class="uncollapsible">
<li><a href="../">上一级</a></li>
</ul>
<ul class="uncollapsible">
<li>
<a href="/专栏/前端工程化精讲-完/00 开篇词 建立上帝视角,全面系统掌握前端效率工程化.md.html">00 开篇词 建立上帝视角,全面系统掌握前端效率工程化.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/01 项目基石:前端脚手架工具探秘.md.html">01 项目基石:前端脚手架工具探秘.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/02 界面调试:热更新技术如何开着飞机修引擎?.md.html">02 界面调试:热更新技术如何开着飞机修引擎?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/03 构建提速:如何正确使用 SourceMap.md.html">03 构建提速:如何正确使用 SourceMap.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/04 接口调试Mock 工具如何快速进行接口调试?.md.html">04 接口调试Mock 工具如何快速进行接口调试?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/05 编码效率:如何提高编写代码的效率?.md.html">05 编码效率:如何提高编写代码的效率?.md.html</a>
</li>
<li>
<a class="current-tab" href="/专栏/前端工程化精讲-完/06 团队工具:如何利用云开发提升团队开发效率?.md.html">06 团队工具:如何利用云开发提升团队开发效率?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/07 低代码工具:如何用更少的代码实现更灵活的需求.md.html">07 低代码工具:如何用更少的代码实现更灵活的需求.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/08 无代码工具:如何做到不写代码就能高效交付?.md.html">08 无代码工具:如何做到不写代码就能高效交付?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/09 构建总览:前端构建工具的演进.md.html">09 构建总览:前端构建工具的演进.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/10 流程分解Webpack 的完整构建流程.md.html">10 流程分解Webpack 的完整构建流程.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/11 编译提效:如何为 Webpack 编译阶段提速?.md.html">11 编译提效:如何为 Webpack 编译阶段提速?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/12 打包提效:如何为 Webpack 打包阶段提速?.md.html">12 打包提效:如何为 Webpack 打包阶段提速?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/13 缓存优化:那些基于缓存的优化方案.md.html">13 缓存优化:那些基于缓存的优化方案.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/14 增量构建Webpack 中的增量构建.md.html">14 增量构建Webpack 中的增量构建.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/15 版本特性Webpack 5 中的优化细节.md.html">15 版本特性Webpack 5 中的优化细节.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/16 无包构建:盘点那些 No-bundle 的构建方案.md.html">16 无包构建:盘点那些 No-bundle 的构建方案.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/17 部署初探:为什么一般不在开发环境下部署代码?.md.html">17 部署初探:为什么一般不在开发环境下部署代码?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/18 工具盘点:掌握那些流行的代码部署工具.md.html">18 工具盘点:掌握那些流行的代码部署工具.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/19 安装提效:部署流程中的依赖安装效率优化.md.html">19 安装提效:部署流程中的依赖安装效率优化.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/20 流程优化:部署流程中的构建流程策略优化.md.html">20 流程优化:部署流程中的构建流程策略优化.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/21 容器方案:从构建到部署,容器化方案的优势有哪些?.md.html">21 容器方案:从构建到部署,容器化方案的优势有哪些?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/22 案例分析:搭建基本的前端高效部署系统.md.html">22 案例分析:搭建基本的前端高效部署系统.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/23 结束语 前端效率工程化的未来展望.md.html">23 结束语 前端效率工程化的未来展望.md.html</a>
</li>
</ul>
</div>
</div>
<div class="sidebar-toggle" onclick="sidebar_toggle()" onmouseover="add_inner()" onmouseleave="remove_inner()">
<div class="sidebar-toggle-inner"></div>
</div>
<script>
function add_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.add('show')
}
function remove_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.remove('show')
}
function sidebar_toggle() {
let sidebar_toggle = document.querySelector('.sidebar-toggle')
let sidebar = document.querySelector('.book-sidebar')
let content = document.querySelector('.off-canvas-content')
if (sidebar_toggle.classList.contains('extend')) { // show
sidebar_toggle.classList.remove('extend')
sidebar.classList.remove('hide')
content.classList.remove('extend')
} else { // hide
sidebar_toggle.classList.add('extend')
sidebar.classList.add('hide')
content.classList.add('extend')
}
}
function open_sidebar() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.add('show')
overlay.classList.add('show')
}
function hide_canvas() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.remove('show')
overlay.classList.remove('show')
}
</script>
<div class="off-canvas-content">
<div class="columns">
<div class="column col-12 col-lg-12">
<div class="book-navbar">
<!-- For Responsive Layout -->
<header class="navbar">
<section class="navbar-section">
<a onclick="open_sidebar()">
<i class="icon icon-menu"></i>
</a>
</section>
</header>
</div>
<div class="book-content" style="max-width: 960px; margin: 0 auto;
overflow-x: auto;
overflow-y: hidden;">
<div class="book-post">
<p id="tip" align="center"></p>
<div><h1>06 团队工具:如何利用云开发提升团队开发效率?</h1>
<p>上一讲我们讨论了在开发时提升编码效率的相关工具。在项目开发过程中,我们可以编写自定义的代码片段缩写规则来提升个人的编码效率。那么,如果想要把这些规则分享给团队内的其他成员或自己的其他电脑设备时又该怎么做呢?带着这个问题,我们就进入到今天要聊的主题:<strong>云开发</strong>Cloud Development</p>
<p>和之前介绍的适用于个人的开发提效工具不同,云开发的优势主要是在团队效率的提升方面。在这一讲里,我会介绍云开发和普通开发模式的区别,目前典型的云开发产品,以及云开发模式的应用场景。下面我就进入到第一个问题:什么是云开发。</p>
<h3>软件开发环境的对比</h3>
<p>在具体介绍什么是云开发之前,让我们先来看下普通的日常开发流程是什么样的。</p>
<h4>个人电脑开发环境</h4>
<p>通常我们都使用个人电脑作为日常的开发环境,对应的开发流程通常是:</p>
<ol>
<li><strong>基础环境准备</strong>:在个人电脑中准备开发环境所需设施,下载安装开发所需各种<strong>应用程序</strong>以前端为例IDE、Node、Git、Yarn......),调试各种<strong>配置文件</strong>.bashrc、npmrc......),安装必要<strong>IDE 插件</strong>并调试<strong>IDE 配置项</strong>UI、编码格式、Snippets......)等。</li>
<li>下载代码:将项目源代码从代码仓库(例如 Git Repo中下载到个人电脑的开发目录下。</li>
<li>安装项目依赖。</li>
<li>运行开发服务。</li>
<li>编码调试。</li>
<li>执行任务Lint 检查、格式化检查、单元测试等)。</li>
</ol>
<p>上述开发流程的流畅度一定程度上依赖于所使用电脑的硬件配置,因此程序员往往需要高性能配置的个人电脑。</p>
<h4>远程开发</h4>
<p>远程开发是将开发环境部署到远程服务器,然后通过个人电脑的 IDEIntegrated Development Environment ,集成开发环境) 进行远程连接来进行开发的方式(例如通过 VS Code 中的 <a href="https://code.visualstudio.com/docs/remote/remote-overview">Remote SSH 插件</a>)。和传统的个人电脑开发环境相比,远程开发模式的优点主要在于:</p>
<ol>
<li>由远程的开发服务器来承载项目数据存储和运行计算的需求,从而解放了对个人电脑资源的占用和对性能的要求。</li>
<li>由于个人电脑只提供了用户操作界面和远程连接的能力,因此大大减少了访问设备变更对于项目开发的影响,例如在更换新电脑或在家使用不同电脑设备的情况下,只需要安装 IDE 和少量配置,就能快速获得一致的开发体验,而无须再重复进行基础环境的准备。</li>
</ol>
<p>远程开发的主要问题在于:</p>
<ol>
<li>需要申请单独的开发机资源。</li>
<li>新申请的开发机也需要和个人电脑环境一样,人工进行基础环境的准备工作。</li>
<li>将开发机单独用于远程开发,在资源分配上可能存在资源利用不充分的问题。</li>
</ol>
<h4>云开发</h4>
<p>云开发模式是在上述远程开发模式的基础之上发展而来的,将开发环境托管,由远程开发服务器变更为云服务。个人电脑通过 IDE 或云服务提供的浏览器界面访问云端工作区进行开发。云开发模式在继承远程开发模式优点的基础上,更能提升效率的原因在于:</p>
<ol>
<li>通过容器化技术将开发环境所需基础设施应用程序、配置文件、IDE 插件、IDE 设定项等)集成到基础镜像中,大大<strong>提升开发环境准备的效率</strong>。同时,同样的基础环境也避免了相同项目不同开发集成环境导致的环境差异类问题。</li>
<li>通过服务化的云开发平台,<strong>简化使用流程</strong>,解决个人使用远程开发时可能遇到的技术困难,使得刚入职的新人也能够快速上手。</li>
<li>对于团队而言,能够<strong>提升团队协作效率</strong>。云开发模式有利于流程规范的统一,有利于团队成员共享开发工具,同时支持多人访问相同开发环境,有助于结对编程等协作流程。</li>
<li>对于公司而言,使用弹性化的云端容器环境有利于资源利用率的提升和硬件资产成本的降低。</li>
</ol>
<h3>典型云开发产品介绍</h3>
<p>以下表格是一些已经推出的云开发产品,感兴趣的话,你可以根据自己所接触过的云厂商来进一步了解。</p>
<table>
<thead>
<tr>
<th>产品</th>
<th>厂商</th>
<th>基础 IDE</th>
<th>IDE 类型</th>
<th>代码托管方式</th>
</tr>
</thead>
<tbody>
<tr>
<td>VS Codespace</td>
<td>微软</td>
<td>VS Code</td>
<td>Web/VS/VSC</td>
<td>云端Asure/自维护</td>
</tr>
<tr>
<td>Gitpod</td>
<td>Eclipse</td>
<td>Theia</td>
<td>Web/Desktop</td>
<td>云端/自维护(限制用户数量)</td>
</tr>
<tr>
<td>CloudIDE</td>
<td>阿里云</td>
<td>KAITIAN IDE</td>
<td>Web</td>
<td>云端</td>
</tr>
<tr>
<td>Cloud Studio</td>
<td>Coding.net (腾讯云)</td>
<td>VS Code</td>
<td>Web</td>
<td>云端5 个工作空间)</td>
</tr>
<tr>
<td>Cloud9</td>
<td>AWS</td>
<td>Cloud9</td>
<td>Web</td>
<td>云端AWS</td>
</tr>
</tbody>
</table>
<p>本课时重点介绍的是以 VS Codespace 为代表的云开发产品,以及以 Theia 为代表的 WebIDE 框架。</p>
<h4>微软Visual Studio Codespace</h4>
<p><a href="https://visualstudio.microsoft.com/zh-hans/services/visual-studio-codespaces/">Visual Studio Codespace</a>(以下简称<strong>Codespace</strong>),是微软 VS Code 团队 2019 年推出的云开发环境产品,该产品的特点是:</p>
<ul>
<li>支持三种访问客户端VS CodeVisual Studio IDE以及 Web。</li>
<li>提供收费的云托管Azure环境与免费的自维护环境两种服务方式仍需要注册 Azure 账号来访问)。</li>
<li>内置多人协作工具 Live Share 和 AI 智能代码提示功能 InteliCode。</li>
<li>自定义环境基础配置,可<a href="https://docs.microsoft.com/zh-cn/visualstudio/codespaces/reference/configuring">定制化</a>开发环境基础设施,例如 Linux 版本、工具、端口、变量、 IDE 插件等。</li>
<li>自定义<a href="https://docs.microsoft.com/zh-cn/visualstudio/codespaces/reference/personalizing">个性化配置</a>,定制环境中各类配置文件,例如 .bashrc.editorconfig 等。</li>
</ul>
<h4>Eclipse: Theia</h4>
<p><a href="https://theia-ide.org/">Eclipse Theia</a><strong>(以下简称 Theia</strong>,是 Eclipse 基金会推出的 VS Code 的替代产品,它的定位是以 NodeJS 和 TS 为技术栈开发的云端和桌面端的 IDE 基础框架,于 2017 年启动, 2018 年发布了对应的 Web 端 IDE 产品 <a href="https://github.com/gitpod-io/gitpod">Gitpod</a></p>
<p>Theia 和 VS Code 的技术相同点有:</p>
<ul>
<li>编辑器核心都基于 <a href="https://microsoft.github.io/monaco-editor/">Monaco Editor</a></li>
<li>都支持 Language Server ProtocolLSP</li>
<li>都支持 Debug Adepter ProtocolDAP</li>
<li>都支持 VS Code 的插件体系。</li>
</ul>
<p>官方总结,与 VS Code 相比Theia 的不同之处在于:</p>
<ul>
<li>架构上更模块化,更易于自定义。</li>
<li>从一开始就被设计成同时运行于桌面和云端。</li>
<li>由厂商中立的开源基金会开发维护。</li>
<li>开发独立的 WebIDE 是云开发产品的<strong>首选</strong>,目前 VS Code 并未开源功能完整的 Web 版本(目前开源的 Web 版本仅可用于预览功能),但 Thiea 有开源可定制化的版本。</li>
</ul>
<h3>云开发模式的技术要素</h3>
<p>一般来说,云开发模式依赖的技术要素主要有三个方面:<strong>WebIDE</strong><strong>容器化</strong>,以及能够<strong>对接其他云服务</strong></p>
<h4>WebIDE</h4>
<p>继 VS Code 2019 年发布 Codespace 后, Eclipse 基金会于 2020 年初也发布了 Theia 1.0 版本。 WebIDE 在功能体验上已达到和桌面 IDE 相同的水平(尽管在初始化阶段会有不同程度的额外耗时)。同时, WebIDE 还具有以下优点:</p>
<ul>
<li>便于<strong>平台化定制</strong>:在团队使用时可通过定制 WebIDE 来实现<strong>通用的功能扩展和升级</strong>,而无须变更团队成员的桌面 IDE例如使用微信开发者工具软件的同学在工具发布新版本时需要各自处理升级而 Web 版则无须如此)。</li>
<li><strong>流程体验上更平滑</strong>:虽然基本使用仍然是打开一个包含源代码的工作空间容器进行开发,但是通过和代码仓库以及 CI/CD 工具的对接,可以在很多流程节点上做到平滑的体验(例如,测试环境下修复 Bug可以通过工具在查找到对应的提交版本后点击进入到 IDE 界面进行修复、测试和提交,相比于原先需要线下操作的流程而言,效率会上升一个台阶)。</li>
</ul>
<h4>容器化</h4>
<p>容器化以往在服务部署中应用较多。在云开发中的用途主要有:</p>
<ul>
<li>为每个用户的每个项目创建<strong>独立的工作空间。</strong></li>
<li>基于容器化的分层结构,可以方便地在基础环境、项目、用户等维度做镜像继承,便于团队成员维护相同项目时<strong>提升环境创建效率。</strong></li>
<li>相比个人虚拟机,有利于<strong>提升资源利用率</strong>,同时环境搭建更便捷。</li>
</ul>
<h4>云服务对接</h4>
<p>在一些云厂商的云开发产品中,除了容器化工作空间和 WebIDE 之外,也包含了与其他上下游服务的对接。例如在阿里云的 CloudIDE 产品中,就包含了一键部署等功能。而在自研的体系内,也可通过类似的方式将各个环节的工作流程进行串联,从而形成整体工作流程的效率提升。</p>
<h3>云开发的效率提升应用场景</h3>
<p>当我们以团队的方式来实践云开发时,能够找到一些效率优化的切入点,下面仅列举一些代表性的应用场景。</p>
<h4>项目篇</h4>
<ol>
<li><strong>加速创建新项目</strong>:在云开发模式下,可以将包含依赖安装的项目模板存储为镜像,开发者选择镜像并创建容器后即可直接预览效果或进入开发,免去下载模板与安装依赖的时间。</li>
<li><strong>项目依赖版本统一</strong>npm 依赖包在不同环境下安装时,版本自动升级的问题常常对开发测试造成影响(尽管可以通过 “npm ci” 等命令锁定版本,但在实际业务中普及率并不高,这个问题在部署效率篇中会再次谈到)。而在云开发模式下,可以将 node_modules 依赖目录(或 Yarn 的 .pnp 目录)与依赖版本做关联,存储为独立镜像,供开发、测试、部署使用。在相应的流程中就可以免去安装依赖,以达到各环境下依赖版本的统一管理,同时也提升了各环境的处理效率。</li>
</ol>
<h4>工具篇</h4>
<ol>
<li><strong>开箱即用的开发环境</strong>:在开发环境维度上,通过云开发模式,可以将开发所需的不同基础环境以及各种应用程序制作成开发环境镜像,供开发者自由选择。刚入职的新人无须花费大量时间去学习和安装调试项目的开发环境,真正达到开箱即用的效果。</li>
<li><strong>自定义辅助工具的快速共享和共建</strong>:前端工具的共享不再局限于各自安装 npm 包的方式通用的配置、公共的依赖、针对特定项目类型的代码片段、emmet 缩写等,一切能想到的辅助工具,都可以在云平台的模式下快速落地,集成到各开发者的工作空间中。同时,对于工具的作者来说,在云平台的方式下也更容易获取到安装使用量等数据反馈,让优秀的工具得以呈现和传播。</li>
</ol>
<h4>流程篇</h4>
<ol>
<li><strong>连接代码仓库与开发环境</strong>:云开发的模式可以缩短代码仓库与开发环境的距离,通过项目与开发环境的配置关联,可以从代码仓库的任意 commit 直连创建云端工作空间或进入已有工作空间。</li>
<li><strong>连接 Pipeline 与开发环境</strong>:在构建部署的过程中,遇到构建问题或其他测试流程问题的情况时,可以通过对应的提交信息,直连创建临时修复用途的项目工作空间,以便快速调试和修复部署。</li>
</ol>
<h3>使用云开发的注意点</h3>
<p>尽管随着 WebIDE 的兴起,越来越多的云开发产品开始呈现,但是作为一种新兴的工作模式,在尝试规模化使用前还是需要考虑到可能出现的一些问题:</p>
<ol>
<li><strong>代码安全问题</strong>:代码安全是首先需要考虑的问题。通常在代码仓库中我们会设置具体项目的<strong>访问权限</strong>,云开发模式下的镜像与空间访问设计上也应当注意对这部分权限的验证。此外,对于公司内部的项目,在使用云开发模式时应当首选<strong>支持内部部署</strong>的云服务或<strong>搭建自维护</strong>的云服务,而非将代码上传到外部云空间中。</li>
<li><strong>服务搭建与维护</strong>:要在团队内使用云开发的功能,需要考虑服务搭建的方式和成本。对于大厂而言,云服务资源和技术建设比较丰富,搭建自维护的云开发服务可以提供更多灵活的功能;而对于中小规模的技术团队而言,购买使用一些支持内部部署的现有云开发服务是更好的选择。</li>
<li><strong>服务降级与备份</strong>:由于云开发模式下将开发环境与工作代码都存储于云端,需要考虑当云端服务异常时的降级策略。例如是否有独立的环境镜像可供下载后离线使用,以及工作空间内的暂存代码是否有备份,可供独立下载使用。</li>
</ol>
<h3>总结</h3>
<p>这一课时我们先介绍了云开发的概念,以及相比于现在的开发方式,它能解决哪些方面的问题。然后一起了解了几款有代表性的云产品,其中需要重点关注的是 VS Code 系的 Codespace 产品。此外,如果你对定制 WebIDE 感兴趣,从 Theia 入手会是比较好的选择。</p>
<p>在介绍完产品后,我们又讨论了云开发这种模式的一般技术要素,以及使用它所能带来的几个比较明确的效率提升场景。最后还有几个新技术对应的风险点,在真正尝试选择云开发方案前需要被考虑到。</p>
<p>今天的<strong>课后思考题</strong>是:希望你能实际体验课中讲到的一些产品,可以在课后讨论中分享你使用后的感受。</p>
</div>
</div>
<div>
<div style="float: left">
<a href="/专栏/前端工程化精讲-完/05 编码效率:如何提高编写代码的效率?.md.html">上一页</a>
</div>
<div style="float: right">
<a href="/专栏/前端工程化精讲-完/07 低代码工具:如何用更少的代码实现更灵活的需求.md.html">下一页</a>
</div>
</div>
</div>
</div>
</div>
</div>
<a class="off-canvas-overlay" onclick="hide_canvas()"></a>
</div>
<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v652eace1692a40cfa3763df669d7439c1639079717194" integrity="sha512-Gi7xpJR8tSkrpF7aordPZQlW2DLtzUlZcumS8dMQjwDHEnw9I7ZLyiOj/6tZStRBGtGgN6ceN6cMH8z7etPGlw==" data-cf-beacon='{"rayId":"7099775369673cfa","version":"2021.12.0","r":1,"token":"1f5d475227ce4f0089a7cff1ab17c0f5","si":100}' crossorigin="anonymous"></script>
</body>
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-NPSEEVD756"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', 'G-NPSEEVD756');
var path = window.location.pathname
var cookie = getCookie("lastPath");
console.log(path)
if (path.replace("/", "") === "") {
if (cookie.replace("/", "") !== "") {
console.log(cookie)
document.getElementById("tip").innerHTML = "<a href='" + cookie + "'>跳转到上次进度</a>"
}
} else {
setCookie("lastPath", path)
}
function setCookie(cname, cvalue) {
var d = new Date();
d.setTime(d.getTime() + (180 * 24 * 60 * 60 * 1000));
var expires = "expires=" + d.toGMTString();
document.cookie = cname + "=" + cvalue + "; " + expires + ";path = /";
}
function getCookie(cname) {
var name = cname + "=";
var ca = document.cookie.split(';');
for (var i = 0; i < ca.length; i++) {
var c = ca[i].trim();
if (c.indexOf(name) === 0) return c.substring(name.length, c.length);
}
return "";
}
</script>
</html>

View File

@@ -1,768 +1,384 @@
<!DOCTYPE html>
<!-- saved from url=(0046)https://kaiiiz.github.io/hexo-theme-book-demo/ -->
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no">
<link rel="icon" href="/static/favicon.png">
<title>07 低代码工具:如何用更少的代码实现更灵活的需求.md.html</title>
<!-- Spectre.css framework -->
<link rel="stylesheet" href="/static/index.css">
<!-- theme css & js -->
<meta name="generator" content="Hexo 4.2.0">
</head>
<body>
<div class="book-container">
<div class="book-sidebar">
<div class="book-brand">
<a href="/">
<img src="/static/favicon.png">
<span>技术文章摘抄</span>
</a>
</div>
<div class="book-menu uncollapsible">
<ul class="uncollapsible">
<li><a href="/" class="current-tab">首页</a></li>
</ul>
<ul class="uncollapsible">
<li><a href="../">上一级</a></li>
</ul>
<ul class="uncollapsible">
<li>
<a href="/专栏/前端工程化精讲-完/00 开篇词 建立上帝视角,全面系统掌握前端效率工程化.md.html">00 开篇词 建立上帝视角,全面系统掌握前端效率工程化.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/01 项目基石:前端脚手架工具探秘.md.html">01 项目基石:前端脚手架工具探秘.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/02 界面调试:热更新技术如何开着飞机修引擎?.md.html">02 界面调试:热更新技术如何开着飞机修引擎?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/03 构建提速:如何正确使用 SourceMap.md.html">03 构建提速:如何正确使用 SourceMap.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/04 接口调试Mock 工具如何快速进行接口调试?.md.html">04 接口调试Mock 工具如何快速进行接口调试?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/05 编码效率:如何提高编写代码的效率?.md.html">05 编码效率:如何提高编写代码的效率?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/06 团队工具:如何利用云开发提升团队开发效率?.md.html">06 团队工具:如何利用云开发提升团队开发效率?.md.html</a>
</li>
<li>
<a class="current-tab" href="/专栏/前端工程化精讲-完/07 低代码工具:如何用更少的代码实现更灵活的需求.md.html">07 低代码工具:如何用更少的代码实现更灵活的需求.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/08 无代码工具:如何做到不写代码就能高效交付?.md.html">08 无代码工具:如何做到不写代码就能高效交付?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/09 构建总览:前端构建工具的演进.md.html">09 构建总览:前端构建工具的演进.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/10 流程分解Webpack 的完整构建流程.md.html">10 流程分解Webpack 的完整构建流程.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/11 编译提效:如何为 Webpack 编译阶段提速?.md.html">11 编译提效:如何为 Webpack 编译阶段提速?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/12 打包提效:如何为 Webpack 打包阶段提速?.md.html">12 打包提效:如何为 Webpack 打包阶段提速?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/13 缓存优化:那些基于缓存的优化方案.md.html">13 缓存优化:那些基于缓存的优化方案.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/14 增量构建Webpack 中的增量构建.md.html">14 增量构建Webpack 中的增量构建.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/15 版本特性Webpack 5 中的优化细节.md.html">15 版本特性Webpack 5 中的优化细节.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/16 无包构建:盘点那些 No-bundle 的构建方案.md.html">16 无包构建:盘点那些 No-bundle 的构建方案.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/17 部署初探:为什么一般不在开发环境下部署代码?.md.html">17 部署初探:为什么一般不在开发环境下部署代码?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/18 工具盘点:掌握那些流行的代码部署工具.md.html">18 工具盘点:掌握那些流行的代码部署工具.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/19 安装提效:部署流程中的依赖安装效率优化.md.html">19 安装提效:部署流程中的依赖安装效率优化.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/20 流程优化:部署流程中的构建流程策略优化.md.html">20 流程优化:部署流程中的构建流程策略优化.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/21 容器方案:从构建到部署,容器化方案的优势有哪些?.md.html">21 容器方案:从构建到部署,容器化方案的优势有哪些?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/22 案例分析:搭建基本的前端高效部署系统.md.html">22 案例分析:搭建基本的前端高效部署系统.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/23 结束语 前端效率工程化的未来展望.md.html">23 结束语 前端效率工程化的未来展望.md.html</a>
</li>
</ul>
</div>
</div>
<div class="sidebar-toggle" onclick="sidebar_toggle()" onmouseover="add_inner()" onmouseleave="remove_inner()">
<div class="sidebar-toggle-inner"></div>
</div>
<script>
function add_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.add('show')
}
function remove_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.remove('show')
}
function sidebar_toggle() {
let sidebar_toggle = document.querySelector('.sidebar-toggle')
let sidebar = document.querySelector('.book-sidebar')
let content = document.querySelector('.off-canvas-content')
if (sidebar_toggle.classList.contains('extend')) { // show
sidebar_toggle.classList.remove('extend')
sidebar.classList.remove('hide')
content.classList.remove('extend')
} else { // hide
sidebar_toggle.classList.add('extend')
sidebar.classList.add('hide')
content.classList.add('extend')
}
}
function open_sidebar() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.add('show')
overlay.classList.add('show')
}
function hide_canvas() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.remove('show')
overlay.classList.remove('show')
}
</script>
<div class="off-canvas-content">
<div class="columns">
<div class="column col-12 col-lg-12">
<div class="book-navbar">
<!-- For Responsive Layout -->
<header class="navbar">
<section class="navbar-section">
<a onclick="open_sidebar()">
<i class="icon icon-menu"></i>
</a>
</section>
</header>
</div>
<div class="book-content" style="max-width: 960px; margin: 0 auto;
overflow-x: auto;
overflow-y: hidden;">
<div class="book-post">
<p id="tip" align="center"></p>
<div><h1>07 低代码工具:如何用更少的代码实现更灵活的需求</h1>
<p>在进入到这一课的内容之前先让我们来回顾下,通过脚手架工具生成初始化代码,以及通过 Snippet 工具生成代码片段的方式。两种方案的相同在于,通过简单的输入和选择就能获得预设后的项目代码。这种转化方式对于效率的提升效果是清晰可见的。于是有人就想到,能不能更进一步,将我们日常开发的手写代码过程,都转变为通过使用工具来快速生成呢?于是就有了开发效率篇的最后两节内容:低代码开发和无代码开发。这节课我们先来谈低代码开发。</p>
<h3>什么是低代码开发</h3>
<p><strong>低代码开发</strong>Low-Code Development<strong>LCD</strong>),是一种<a href="https://en.wikipedia.org/wiki/Low-code_development_platform">很早被提出2011</a>的开发模式,开发者<strong>主要通过图形化用户界面和配置</strong>来创建应用软件,而不是像传统模式那样<strong>主要依靠手写代码</strong>。对应的,提供给开发者的这类低代码开发功能实现的软件,称为<strong>低代码开发平台</strong>Low-Code Development Platform <strong>LCDP</strong>)。低代码开发模式的开发者,通常不需要具备非常专业的编码技能,或者不需要某一专门领域的编码技能,而是可以通过平台的功能和约束来实现专业代码的产出。</p>
<p>从定义中我们可以看到,低代码开发的工作方式主要依赖操作图形化的用户界面,包括拖拽控件,以及修改其中可被编辑区域的配置。这种可视化的开发方式,可以追溯到更早的 Dreamwaver 时期。而随着前端项目的日趋复杂,这种方式已不再适应现代项目的需求,于是渐渐被更专业的工程化的开发模式所取代。</p>
<p>但是,快速生成项目代码的诉求从未消失。人们也慢慢找到了实现这个目的的两种路径:</p>
<ul>
<li>一种是在高度定制化的场景中,基于经验总结,找到那些相对固定的产品形态,例如公司介绍、产品列表、活动页面等,开放少量的编辑入口,让非专业开发者也能使用。下一课介绍的<strong>无代码开发</strong>,主要就是面向这样的场景需求。</li>
<li>另一类则相反,顺着早期可视化开发的思路,尝试以组件化和数据绑定为基础,通过抽象语法或 IDE 来实现自由度更高、交互复杂度上限更高的页面搭建流程。这种项目开发方式通常需要一定的开发经验与编码能力,只是和普通编码开发方式相比,更多通过操作可视化工具的方式来达到整体效率的提升,因此被称为<strong>低代码开发。</strong></li>
</ul>
<p>在实际场景中,尤其是商用的低代码平台产品,往往提供的是上面两种开发方式的结合。</p>
<h4>低代码开发的典型应用场景</h4>
<p>低代码开发的一类典型应用场景是在 PC 端中后台系统的开发流程中,原因如下:</p>
<ol>
<li>尽管中后台系统的具体页面布局并不固定,但整体 UI 风格较统一,可以基于统一的 UI 组件库来实现搭建,通过组件拖拽组合即可灵活组织成不同形态功能的页面,因此适用于低代码类型的开发模式。</li>
<li>中后台系统涉及数据的增删改查,需要有一定的编码调试能力,无法直接通过 UI 交互完成,因此不适用无代码开发模式。</li>
</ol>
<p>以中后台系统的开发为目标,低代码开发的方式还可以<strong>细分为以下两种</strong>:基于编写 JSON 的开发方式,和基于可视化操作平台的开发方式,下面我们来依次介绍一下。</p>
<h3>基于编写 JSON 的低代码开发</h3>
<p>当我们去审视一个项目前端部分的最终呈现时,可以发现:</p>
<ol>
<li>一个项目的前端部分本质上呈现的是通过路由连接的不同页面。而前端开发的目标就是<strong>最终输出页面的展示与交互功能</strong></li>
<li>如果学过浏览器基本原理,你会知道:每一个页面的内容在浏览器中,最终都归结为<strong>DOM 语法树DOM Tree+ 样式Style+ 动态交互逻辑Dynamic Logic</strong></li>
<li>在组件化开发的今天,一个规范定义的组件包含了特定功能的 DOM 子树和样式风格。因此页面的内容又可以定义为:<strong>组件树Component Tree+ 动态交互逻辑Dynamic Logic</strong></li>
</ol>
<p>而基于 JSON-Schema 的低代码开发的切入逻辑是:</p>
<ol>
<li>在特定场景下,例如开发中后台增删改查页面时,大部分前端手动编写的代码是模式化的。</li>
<li>页面组件结构模板和相应数据模型的代码组织,可以替换为<strong>更高效</strong>的 JSON 语法树描述。</li>
<li>通过制定用于编写的<strong>JSON 语法图式JSON Schema</strong>,以及封装能够渲染对应 JSON 语法树的运行时工具集,就可以提升开发效率,降低开发技术要求。</li>
</ol>
<p>下图中的代码就是组件语法树示例(完整的示例代码参见 <a href="https://github.com/fe-efficiency/lessons_fe_efficiency/tree/master/07_low_code">07_low_code</a>),我们通过编写一个简单的 JSON 语法树以及对应的编译器,来展示低代码开发的模式。</p>
<p><img src="assets/CgqCHl9MyGOAKUrZAAFHKI-ma8o592.png" alt="Drawing 0.png" /></p>
<h4>编写 JSON 开发的高效性</h4>
<p>编写 JSON 语法树开发的高效性体现在:</p>
<ol>
<li>由于只用编写 JSON ,而隐藏了前端开发所需的大量技术细节(构建、框架等),因此降低了对开发人员的编码要求,即使是非专业的开发人员,也可以根据示例和文档完成相应页面的开发。</li>
<li>由于只用编写 JSON ,大量的辅助代码集成在工具内部,整体上减少了需要生成的代码量。</li>
<li>可以对中后台系统所使用的常用业务组件进行抽象,然后以示例页面或示例组件的方式,供用户选择。</li>
</ol>
<h4>编写 JSON 开发的缺点</h4>
<p>但另一方面,这种方式也存在着一些不足:</p>
<ol>
<li><strong>输入效率</strong>:单从组件结构的描述而言,使用 JSON 描述的代码量要多于同等结构的 JSX 语法(参见示例代码 <a href="https://github.com/fe-efficiency/lessons_fe_efficiency/tree/master/07_low_code">07_low_code</a>),对于有经验的前端开发者而言,通常无法第一时间感受到效率的提升。</li>
<li><strong>学习记忆成本</strong>:由于引入了新的 JSON 语法图式,无论对于前端开发者、后端开发者还是非专业的人员来说,上手的学习成本都不可避免。此外,不同组件存在不同属性,要在实际编写过程中灵活运用,对记忆量也是一个考验。而反复查阅文档又会造成效率的下降(对于这个问题,有个优化方案是利用 IDE Snippets 的选项功能生成对应的语法提示)。</li>
<li><strong>复用性和可维护性</strong>:对于多页面存在可复用业务组件的情况,在 JSON 编写的模式下往往需要手动复制到各页面 JSON 中,牺牲了复用组件的可维护性。此外,对于功能复杂的页面,对应的 JSON 长度也会让维护体验变得不太美好。</li>
<li><strong>问题排查难度增加</strong>:这个问题涉及面向人群,如果是非专业的人员从事 JSON 的开发过程,当遇到问题时,在如何排查上可能造成阻碍,因此通常需要配备额外的专业人员来提供技术支持。</li>
</ol>
<p>针对编写 JSON 过程中的输入效率、记忆成本和可维护性等问题,许多低代码工具进一步提供了可视化操作平台的工作方式。下面再让我们来了解下,这种方式是怎么解决上述问题的。</p>
<h3>基于可视化操作平台的低代码开发</h3>
<p>可视化的低代码操作平台把编写 JSON 的过程变成了拖拽组件和调试属性配置,如下图所示,这样的交互方式对用户来说更直观友好,开发效率也会更高。</p>
<p><img src="assets/CgqCHl9MyHmAdpfQAAdLANM4tuQ134.png" alt="Drawing 1.png" /></p>
<h4>可视化操作平台的基本使用方式</h4>
<p>绝大部分的可视化操作平台都将界面布局分为三个区域:左侧的<strong>组件选择区</strong>,中部的<strong>预览交互区</strong>以及右侧的<strong>属性编辑区</strong>。这三个区域的排布所对应的,也是用户<strong>生成页面的操作流程</strong></p>
<ul>
<li>首先,在左侧面板中选择组件。</li>
<li>然后,拖入中间预览区域,并放置到合适的容器块内。</li>
<li>最后,调试右侧面板中新移入的组件属性。</li>
<li>调试完成后,进行下一个组件的循环操作直到整个页面搭建完成。</li>
</ul>
<h4>可视化操作平台的生产效率影响因素</h4>
<p>通常来说,在组件数量不变的情况下,编写 JSON 的产出效率更大程度上取决于编写页面的开发者的技术熟练度。但在使用可视化操作平台时却并非如此:我们会看到,平台本身的很多方面也会直接影响使用者的产出:</p>
<ul>
<li>首先,平台的<strong>功能完备性</strong>直接决定了用户<strong>产出的上限</strong>:开发者不可能在平台里使用组件区没有显示的组件,也不可能创建编辑区不存在的属性。这就迫使平台开发者需要尽可能完整地陈列所有类型的组件,以及通过定义组件类型描述,来获取所有可以被编辑的属性和方法。包括用户交互和数据对组件的影响,这些都需要平台以合适的使用方式提供给用户。</li>
<li>其次,平台的<strong>逻辑自洽性</strong>决定了用户产出的<strong>质量</strong>:在代码的组织上,不同组件之间不可以任意组合,错误的组合可能导致显示与功能的异常。如果平台只是简单罗列所有组件,而对其中的规则不加以限制,就可能导致用户在使用过程中出现意料外的产出结果。所以,平台开发者需要有一套完整的组件关联关系表,并反映到交互呈现中。</li>
<li>最后,平台提供的<strong>交互易用性</strong>决定了用户的产出<strong>效率</strong>:尽管大部分低代码平台都提供了相似的区域操作逻辑,但真正影响用户使用效率的往往是很多细节的控制。例如,与单纯依靠光标选取组件相比,在侧边栏提供节点树的方式可以更大程度减少误选;与简单陈列所有组件相比,合适的分类,以及当选择特定组件时筛选出可添加的部分组件,更能减少用户搜索的时间,同时减少可能的出错;一些平台提供了操作栈回放的功能,能减少用户误操作后的修复成本,等等。</li>
</ul>
<h3>低代码开发的产品</h3>
<p>低代码开发的产品有很多,其中既包括商用的产品,例如 Kony、OutSystems、Mendix、Appian、iVX国内也包括开源类的产品例如阿里飞冰、百度 Amis、贝壳河图、Vvvebjs、react-visual-editor 等。这里就不一一介绍了,感兴趣的话,你可以进一步搜索了解。</p>
<h3>总结</h3>
<p>这节课我们介绍了低代码开发的概念和它的基本应用场景,也了解了低代码开发的两种基本开发模式:基于编写 JSON 的方式和基于可视化操作平台的方式。</p>
<p>前者对普通的项目开发流程做了抽象,将编写不同功能模块的代码变为只编写组件语法树描述信息,这种方式在一定程度上降低了使用者的技术要求,提升了开发的效率,但是在一些方面仍然不甚理想。而平台化的开发模式相对而言解决了编写 JSON 模式下的一些问题,但是要搭建一个功能完备、使用逻辑自洽和交互性良好的平台也并非易事。</p>
<p>通过这节课的学习,希望能为你提供一种新的项目技术方案,在合适的应用场景下,可以考虑使用低代码工具来提升开发效率。今天的课后思考题是:这节课里讲到的低代码工具主要面向什么样的用户群体?</p>
</div>
</div>
<div>
<div style="float: left">
<a href="/专栏/前端工程化精讲-完/06 团队工具:如何利用云开发提升团队开发效率?.md.html">上一页</a>
</div>
<div style="float: right">
<a href="/专栏/前端工程化精讲-完/08 无代码工具:如何做到不写代码就能高效交付?.md.html">下一页</a>
</div>
</div>
</div>
</div>
</div>
</div>
<a class="off-canvas-overlay" onclick="hide_canvas()"></a>
</div>
<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v652eace1692a40cfa3763df669d7439c1639079717194" integrity="sha512-Gi7xpJR8tSkrpF7aordPZQlW2DLtzUlZcumS8dMQjwDHEnw9I7ZLyiOj/6tZStRBGtGgN6ceN6cMH8z7etPGlw==" data-cf-beacon='{"rayId":"70997755ce8d3cfa","version":"2021.12.0","r":1,"token":"1f5d475227ce4f0089a7cff1ab17c0f5","si":100}' crossorigin="anonymous"></script>
</body>
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-NPSEEVD756"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', 'G-NPSEEVD756');
var path = window.location.pathname
var cookie = getCookie("lastPath");
console.log(path)
if (path.replace("/", "") === "") {
if (cookie.replace("/", "") !== "") {
console.log(cookie)
document.getElementById("tip").innerHTML = "<a href='" + cookie + "'>跳转到上次进度</a>"
}
} else {
setCookie("lastPath", path)
}
function setCookie(cname, cvalue) {
var d = new Date();
d.setTime(d.getTime() + (180 * 24 * 60 * 60 * 1000));
var expires = "expires=" + d.toGMTString();
document.cookie = cname + "=" + cvalue + "; " + expires + ";path = /";
}
function getCookie(cname) {
var name = cname + "=";
var ca = document.cookie.split(';');
for (var i = 0; i < ca.length; i++) {
var c = ca[i].trim();
if (c.indexOf(name) === 0) return c.substring(name.length, c.length);
}
return "";
}
</script>
</html>

View File

@@ -1,890 +1,445 @@
<!DOCTYPE html>
<!-- saved from url=(0046)https://kaiiiz.github.io/hexo-theme-book-demo/ -->
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no">
<link rel="icon" href="/static/favicon.png">
<title>08 无代码工具:如何做到不写代码就能高效交付?.md.html</title>
<!-- Spectre.css framework -->
<link rel="stylesheet" href="/static/index.css">
<!-- theme css & js -->
<meta name="generator" content="Hexo 4.2.0">
</head>
<body>
<div class="book-container">
<div class="book-sidebar">
<div class="book-brand">
<a href="/">
<img src="/static/favicon.png">
<span>技术文章摘抄</span>
</a>
</div>
<div class="book-menu uncollapsible">
<ul class="uncollapsible">
<li><a href="/" class="current-tab">首页</a></li>
</ul>
<ul class="uncollapsible">
<li><a href="../">上一级</a></li>
</ul>
<ul class="uncollapsible">
<li>
<a href="/专栏/前端工程化精讲-完/00 开篇词 建立上帝视角,全面系统掌握前端效率工程化.md.html">00 开篇词 建立上帝视角,全面系统掌握前端效率工程化.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/01 项目基石:前端脚手架工具探秘.md.html">01 项目基石:前端脚手架工具探秘.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/02 界面调试:热更新技术如何开着飞机修引擎?.md.html">02 界面调试:热更新技术如何开着飞机修引擎?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/03 构建提速:如何正确使用 SourceMap.md.html">03 构建提速:如何正确使用 SourceMap.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/04 接口调试Mock 工具如何快速进行接口调试?.md.html">04 接口调试Mock 工具如何快速进行接口调试?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/05 编码效率:如何提高编写代码的效率?.md.html">05 编码效率:如何提高编写代码的效率?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/06 团队工具:如何利用云开发提升团队开发效率?.md.html">06 团队工具:如何利用云开发提升团队开发效率?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/07 低代码工具:如何用更少的代码实现更灵活的需求.md.html">07 低代码工具:如何用更少的代码实现更灵活的需求.md.html</a>
</li>
<li>
<a class="current-tab" href="/专栏/前端工程化精讲-完/08 无代码工具:如何做到不写代码就能高效交付?.md.html">08 无代码工具:如何做到不写代码就能高效交付?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/09 构建总览:前端构建工具的演进.md.html">09 构建总览:前端构建工具的演进.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/10 流程分解Webpack 的完整构建流程.md.html">10 流程分解Webpack 的完整构建流程.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/11 编译提效:如何为 Webpack 编译阶段提速?.md.html">11 编译提效:如何为 Webpack 编译阶段提速?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/12 打包提效:如何为 Webpack 打包阶段提速?.md.html">12 打包提效:如何为 Webpack 打包阶段提速?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/13 缓存优化:那些基于缓存的优化方案.md.html">13 缓存优化:那些基于缓存的优化方案.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/14 增量构建Webpack 中的增量构建.md.html">14 增量构建Webpack 中的增量构建.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/15 版本特性Webpack 5 中的优化细节.md.html">15 版本特性Webpack 5 中的优化细节.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/16 无包构建:盘点那些 No-bundle 的构建方案.md.html">16 无包构建:盘点那些 No-bundle 的构建方案.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/17 部署初探:为什么一般不在开发环境下部署代码?.md.html">17 部署初探:为什么一般不在开发环境下部署代码?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/18 工具盘点:掌握那些流行的代码部署工具.md.html">18 工具盘点:掌握那些流行的代码部署工具.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/19 安装提效:部署流程中的依赖安装效率优化.md.html">19 安装提效:部署流程中的依赖安装效率优化.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/20 流程优化:部署流程中的构建流程策略优化.md.html">20 流程优化:部署流程中的构建流程策略优化.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/21 容器方案:从构建到部署,容器化方案的优势有哪些?.md.html">21 容器方案:从构建到部署,容器化方案的优势有哪些?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/22 案例分析:搭建基本的前端高效部署系统.md.html">22 案例分析:搭建基本的前端高效部署系统.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/23 结束语 前端效率工程化的未来展望.md.html">23 结束语 前端效率工程化的未来展望.md.html</a>
</li>
</ul>
</div>
</div>
<div class="sidebar-toggle" onclick="sidebar_toggle()" onmouseover="add_inner()" onmouseleave="remove_inner()">
<div class="sidebar-toggle-inner"></div>
</div>
<script>
function add_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.add('show')
}
function remove_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.remove('show')
}
function sidebar_toggle() {
let sidebar_toggle = document.querySelector('.sidebar-toggle')
let sidebar = document.querySelector('.book-sidebar')
let content = document.querySelector('.off-canvas-content')
if (sidebar_toggle.classList.contains('extend')) { // show
sidebar_toggle.classList.remove('extend')
sidebar.classList.remove('hide')
content.classList.remove('extend')
} else { // hide
sidebar_toggle.classList.add('extend')
sidebar.classList.add('hide')
content.classList.add('extend')
}
}
function open_sidebar() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.add('show')
overlay.classList.add('show')
}
function hide_canvas() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.remove('show')
overlay.classList.remove('show')
}
</script>
<div class="off-canvas-content">
<div class="columns">
<div class="column col-12 col-lg-12">
<div class="book-navbar">
<!-- For Responsive Layout -->
<header class="navbar">
<section class="navbar-section">
<a onclick="open_sidebar()">
<i class="icon icon-menu"></i>
</a>
</section>
</header>
</div>
<div class="book-content" style="max-width: 960px; margin: 0 auto;
overflow-x: auto;
overflow-y: hidden;">
<div class="book-post">
<p id="tip" align="center"></p>
<div><h1>08 无代码工具:如何做到不写代码就能高效交付?</h1>
<p>在开始今天的课程前,我们先来简单回顾下上节课的思考题:低代码工具主要面向什么样的用户群体呢?低代码工具本质上是对组件化开发流程的简化,但在开发过程中,仍然可能进行编码调试。因此,它面向的用户群体应该是具有一定技术基础的开发人员,专业的后端开发也可以使用这类工具来快速开发项目中的前端功能。</p>
<p>在介绍低代码开发的时候,经常伴随出现另一个名词:<strong>无代码开发</strong>。今天我们就来近距离地了解这种开发模式的相关知识。</p>
<h3>无代码开发模式的出现</h3>
<p>在讨论无代码开发之前,我们先来看下这种开发模式出现的原因:</p>
<ol>
<li><strong>有需求量大且更新频率快的小型项目</strong>:例如不同主题内容的运营活动页面、招聘页面等。</li>
<li><strong>这些项目流程模式基本相同但又具有一定的定制性</strong>:例如一个优惠券活动,需要投放到不同城市,因此文案内容、图片背景和优惠券金额等都可能不同。</li>
<li><strong>开发人员成本昂贵,供不应求</strong>:尽管开发人员可以通过代码复用等方式来提升开发类似代码的效率,但是总体而言这类简单项目从定制开发到上线的流程,仍然以天为单位,在面对大量雷同的项目时,开发效率仍不能满足用户方的需求。</li>
<li><strong>非互联网企业缺少技术资源</strong>:许多非互联网中小型企业内部缺少完整的技术团队,无法通过自身技术资源解决日常的互联网开发需求。</li>
</ol>
<p>无代码开发模式正是为这些问题而量身定制的。</p>
<h3>无代码开发介绍</h3>
<p><strong>无代码开发</strong><strong>No-Code Development / Codeless Development</strong>)是指通过非手写代码工具来产出代码的方式。这类工具被称为:<a href="https://en.wikipedia.org/wiki/No-code_development_platform">无代码开发平台</a><strong>No-Code Development PlatformNCDP</strong></p>
<h4>无代码开发和低代码开发的区别</h4>
<p>从下面的表格中可以看到无代码开发和低代码开发的主要区别,包括目标人群、目标产品、开发模式等 7 个维度。</p>
<table>
<thead>
<tr>
<th><strong>区别维度</strong></th>
<th><strong>低代码开发</strong></th>
<th><strong>无代码开发(面向非开发)</strong></th>
<th><strong>无代码开发(面向准开发)</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>目标人群</strong></td>
<td>主要面向有一定技术基础的开发人员</td>
<td>主要面向非开发岗位人员(例如运营人员,设计人员)</td>
<td>主要面向准开发人员(对开发思维的需求随项目难度递增)</td>
</tr>
<tr>
<td><strong>目标产品</strong></td>
<td>主要为 B 端中后台</td>
<td>主要为 C 端活动或 H5</td>
<td>结合前两者</td>
</tr>
<tr>
<td><strong>开发模式</strong></td>
<td>编写 JSON/操作图形化交互平台(偏重前端)</td>
<td>操作图形化交互平台(偏重前端)</td>
<td>操作图形化交互平台(前端到后端)</td>
</tr>
<tr>
<td><strong>基础设施</strong></td>
<td>通用的组件库与渲染流程</td>
<td>典型的页面/项目模板,以及与视觉呈现相关的组件</td>
<td>前后端组件</td>
</tr>
<tr>
<td><strong>可自由定制的内容</strong></td>
<td>组件的选择、布局、属性、数据、交互</td>
<td>可视化数据(文本、媒体、动画等)的编辑</td>
<td>前端可视化数据,后端数据与逻辑功能等</td>
</tr>
<tr>
<td><strong>数据接口</strong></td>
<td>通常由独立后端单独开发提供</td>
<td>无数据接口,或通常由平台方提供标准化的接口</td>
<td>基于云基础设施的数据功能</td>
</tr>
<tr>
<td><strong>部署</strong></td>
<td>可单独部署</td>
<td>通常由平台方提供云服务部署</td>
<td>通常由平台方提供云服务部署</td>
</tr>
</tbody>
</table>
<p>其中,可以把市面上的<strong>无代码开发模式</strong>进一步<strong>细分为两类</strong></p>
<ul>
<li><strong>一类完全面向非开发人员</strong>,如百度 H5对应开发的目标产品主要是模式化的 C 端活动与其他 H5 页面类型(例如招聘页面,测试小游戏等);</li>
<li><strong>另一类面向准开发人员</strong>,即缺少代码经验且希望开发全栈产品的个人或团队,在目标产品和定制功能上更全面,但是相应的学习使用成本也更高,这类产品以 iVX 为代表。</li>
</ul>
<p>下面,我们就来进一步了解下这两种开发模式的异同。</p>
<h3>典型产品分析</h3>
<h4>面向非开发人员的无代码开发产品</h4>
<p>这类产品的设计目标就是将一些固定类型的项目生产流程,由代码开发转变为操作图形化交互工具。</p>
<p><strong>企业内部的定制化搭建平台</strong></p>
<p>例如面向 C 端的企业经常会有推广拉新的开发需求,以红包活动为例,如果按照普通的代码开发流程,需要经历以下 6 个流程才能最终上线:</p>
<ol>
<li>产品确定活动流程,交付产品文档与原型。</li>
<li>设计师设计页面,交付设计稿。</li>
<li>前端工程师开发活动的前端代码。</li>
<li>后端工程师开发活动的后端代码。</li>
<li>前后端联调后交付测试。</li>
<li>测试通过后部署上线。</li>
</ol>
<p>这类活动通常是可复用的,然而针对不同时间段,或者同一时间段的不同推广渠道和推广地区,通常需要提供模式相同但外观与数据(例如红包金额)不同的活动产品,于是当需要复用时,会发现除了产品交付的流程不用变更外,后续所有开发部署流程都难以避免。这导致<strong>开发资源被低效地占用,生产效率也无法进一步提升</strong></p>
<p>而无代码开发产品可以完美地解决这一类问题,针对同一类型的活动项目,前后端工程师可以开发出对应的可视化活动搭建平台,提供:</p>
<ol>
<li>选择活动类型并预览效果的功能。</li>
<li>文本、图片、活动金额、上下线时间等元素替换功能。</li>
<li>数据统计等辅助模块。</li>
</ol>
<p>这种方法通过一次开发,即可让运营人员长期重复使用,解放了后续的开发资源,并且从流程上将普通项目开发的 6 个环节简化为两个环节:设计师设计页面,以及运营人员无代码地编辑内容。这将原先以天为单位的开发部署时间,缩短为以分钟为单位的编辑生成时间。</p>
<p>以上便是<strong>企业内部无代码开发</strong>的一类应用场景。</p>
<p><strong>外部无代码搭建平台</strong></p>
<p>另一类面向非开发人员的无代码开发产品,针对的是缺乏开发资源的企业和部门。对于一些常见的小型项目需求,例如招聘页面、报名页面等,它们往往需要借助<strong>外部提供的无代码开发平台</strong>。这类无代码开发平台包括百度 H5、MAKA、易企秀等。</p>
<p><img src="assets/Ciqc1F9R0u-ABNY5ABlh1mN5IXo611.png" alt="image" /></p>
<p>百度 H5 编辑界面</p>
<p>这类产品的特点是:</p>
<ul>
<li><strong>场景类型固定</strong>:通常提供一些企业常见类型页面生成(招聘介绍、报名表单、宣传活动、答题测试等)。</li>
<li><strong>设计模板丰富</strong>:通常都提供了大量经过设计的页面模板供用户选择,部分平台还提供了第三方设计师设计与发布设计模板的功能。</li>
<li><strong>定制化功能多样</strong>:除了常见的文本和图片类型外,这类产品的 IDE 中通常还包含了媒体、表单、动画等多维度编辑功能。</li>
<li><strong>后端功能较少</strong>:产品形态大多是纯前端的,即使涉及后端数据,例如表单提交,也只提供了基于云平台的上报数据统计,在 IDE 中没有自定义后端接口和数据字段的部分,这也和使用人群的定位一致。</li>
<li><strong>部署在云端</strong>:通常都提供免费或收费的云端部署方案,以降低用户运维难度和操作成本。</li>
<li><strong>使用人群细化</strong>:使用人群进一步分化为设计师与普通使用者。对于普通使用者而言,除了开发资源外,通常也不具备设计资源。于是设计师就可以使用平台提供的 IDE 工具,发布付费设计模板,供普通使用者选择。</li>
</ul>
<h4>面向准开发人员的无代码开发产品</h4>
<p>而面向准开发人员的无代码开发产品,则有以下几点不同:</p>
<ul>
<li><strong>更为多样化的应用场景</strong>:同上述面向非开发人员的产品相比,这类产品最主要的功能是提供了描述性的后端的数据与功能模块,因此能够实现的应用场景也更为多样化和通用化。以 iVX 为例,可实现的应用场景从上面的 C 端产品扩展到了 B 端产品包括小程序、小游戏、H5、营销活动BPM、OA、CRM、ERP企业中台BI、大屏幕等。</li>
</ul>
<p><img src="assets/CgqCHl9R0v-ASMv2AAQk8slb6Xc485.png" alt="image" /></p>
<p>iVX 编辑器中后端逻辑描述面板</p>
<ul>
<li><strong>目标人群的变化</strong>:应用场景扩展对应的是 IDE 功能的复杂化和操作学习成本的增加,于是目标人群也多少有些不同:
<ul>
<li>从一方面来看,这种功能增强型的无代码模式能够吸引更多有产品思维但缺少实际开发经验的个人或缺少开发资源的团队尝试使用,例如外包团队或早期创业团队。</li>
<li>但从另一方面看,要开发一个具有一定复杂度的项目,对开发人员的要求不只体现在代码能力方面,还需要开发人员对产品、全栈架构与交互逻辑层面有一定的认识和理解。无代码平台主要解决的是前一个层面的问题,对于后者,则仍然受到使用者的技术经验限制。这也在一定程度上也会造成无形的使用壁垒。</li>
<li>此外,使用者对这类全新的开发工具和流程的开发经验的掌握,很难迁移到其他开发工具和流程中,这也可能使使用者产生是否值得学习这类工具的疑虑。也许直到这类产品在市场中得到充分的验证,培养出足够多的使用者和需求方后,才能真正解决担忧。</li>
</ul>
</li>
</ul>
<h3>总结</h3>
<p>这节课我们介绍了和低代码开发对应的另一种开发模式:无代码开发。首先我们对比了两种开发模式不同维度的区别,又进一步介绍了无代码开发的两种不同方向:面向非开发人员的产品与面向准开发人员的产品。</p>
<p>面向非开发人员的无代码开发在企业内部与外部都有相应的应用场景:在企业内部我们可以将一些频率高的常用简易开发流程,固化为无代码开发产品,供运营或其他岗位人员使用;而在企业外部,也有不少免费或收费的无代码平台,将开发工具提供给缺乏技术资源的企业与个人;同时,设计师也可以在这类平台上制作自己的设计模板提供给用户。</p>
<p>面向准开发人员的无代码产品相比之下具有更广泛的使用场景,通过提供后端数据与逻辑的描述功能,用户可以通过 IDE 开发出具备前后端数据交互的复杂应用,进一步减少与普通代码开发的功能边界的差距。但相对的,要利用工具实现复杂应用,对用户的学习成本和思维培养也是一种新的挑战。究竟这种模式是否能被更多企业所接受,成为代码开发模式的替代呢?让我们拭目以待。</p>
<p>通过这一课,希望作为前端工程师的你能够在工作中考虑无代码开发的第一种应用场景,在合适的场景下开发相应的工具,来解放团队的开发资源,提升效率。而对于外部的无代码开发平台,如果感兴趣,希望你进一步了解,或许能对普通代码开发的模式有所借鉴。</p>
<p>最后,随着这一课的结束,我们就完成了开发效率篇的所有课程。今天的思考题是回顾本模块的内容,学会在工作中运用,如有疑问,欢迎你写在留言区。</p>
<p>下节课我们将进入第二个模块:构建效率篇。一起深入了解你所使用的构建工具中有哪些影响效率的知识点。</p>
</div>
</div>
<div>
<div style="float: left">
<a href="/专栏/前端工程化精讲-完/07 低代码工具:如何用更少的代码实现更灵活的需求.md.html">上一页</a>
</div>
<div style="float: right">
<a href="/专栏/前端工程化精讲-完/09 构建总览:前端构建工具的演进.md.html">下一页</a>
</div>
</div>
</div>
</div>
</div>
</div>
<a class="off-canvas-overlay" onclick="hide_canvas()"></a>
</div>
<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v652eace1692a40cfa3763df669d7439c1639079717194" integrity="sha512-Gi7xpJR8tSkrpF7aordPZQlW2DLtzUlZcumS8dMQjwDHEnw9I7ZLyiOj/6tZStRBGtGgN6ceN6cMH8z7etPGlw==" data-cf-beacon='{"rayId":"709977581bc53cfa","version":"2021.12.0","r":1,"token":"1f5d475227ce4f0089a7cff1ab17c0f5","si":100}' crossorigin="anonymous"></script>
</body>
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-NPSEEVD756"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', 'G-NPSEEVD756');
var path = window.location.pathname
var cookie = getCookie("lastPath");
console.log(path)
if (path.replace("/", "") === "") {
if (cookie.replace("/", "") !== "") {
console.log(cookie)
document.getElementById("tip").innerHTML = "<a href='" + cookie + "'>跳转到上次进度</a>"
}
} else {
setCookie("lastPath", path)
}
function setCookie(cname, cvalue) {
var d = new Date();
d.setTime(d.getTime() + (180 * 24 * 60 * 60 * 1000));
var expires = "expires=" + d.toGMTString();
document.cookie = cname + "=" + cvalue + "; " + expires + ";path = /";
}
function getCookie(cname) {
var name = cname + "=";
var ca = document.cookie.split(';');
for (var i = 0; i < ca.length; i++) {
var c = ca[i].trim();
if (c.indexOf(name) === 0) return c.substring(name.length, c.length);
}
return "";
}
</script>
</html>

View File

@@ -1,968 +1,484 @@
<!DOCTYPE html>
<!-- saved from url=(0046)https://kaiiiz.github.io/hexo-theme-book-demo/ -->
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no">
<link rel="icon" href="/static/favicon.png">
<title>12 打包提效:如何为 Webpack 打包阶段提速?.md.html</title>
<!-- Spectre.css framework -->
<link rel="stylesheet" href="/static/index.css">
<!-- theme css & js -->
<meta name="generator" content="Hexo 4.2.0">
</head>
<body>
<div class="book-container">
<div class="book-sidebar">
<div class="book-brand">
<a href="/">
<img src="/static/favicon.png">
<span>技术文章摘抄</span>
</a>
</div>
<div class="book-menu uncollapsible">
<ul class="uncollapsible">
<li><a href="/" class="current-tab">首页</a></li>
</ul>
<ul class="uncollapsible">
<li><a href="../">上一级</a></li>
</ul>
<ul class="uncollapsible">
<li>
<a href="/专栏/前端工程化精讲-完/00 开篇词 建立上帝视角,全面系统掌握前端效率工程化.md.html">00 开篇词 建立上帝视角,全面系统掌握前端效率工程化.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/01 项目基石:前端脚手架工具探秘.md.html">01 项目基石:前端脚手架工具探秘.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/02 界面调试:热更新技术如何开着飞机修引擎?.md.html">02 界面调试:热更新技术如何开着飞机修引擎?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/03 构建提速:如何正确使用 SourceMap.md.html">03 构建提速:如何正确使用 SourceMap.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/04 接口调试Mock 工具如何快速进行接口调试?.md.html">04 接口调试Mock 工具如何快速进行接口调试?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/05 编码效率:如何提高编写代码的效率?.md.html">05 编码效率:如何提高编写代码的效率?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/06 团队工具:如何利用云开发提升团队开发效率?.md.html">06 团队工具:如何利用云开发提升团队开发效率?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/07 低代码工具:如何用更少的代码实现更灵活的需求.md.html">07 低代码工具:如何用更少的代码实现更灵活的需求.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/08 无代码工具:如何做到不写代码就能高效交付?.md.html">08 无代码工具:如何做到不写代码就能高效交付?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/09 构建总览:前端构建工具的演进.md.html">09 构建总览:前端构建工具的演进.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/10 流程分解Webpack 的完整构建流程.md.html">10 流程分解Webpack 的完整构建流程.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/11 编译提效:如何为 Webpack 编译阶段提速?.md.html">11 编译提效:如何为 Webpack 编译阶段提速?.md.html</a>
</li>
<li>
<a class="current-tab" href="/专栏/前端工程化精讲-完/12 打包提效:如何为 Webpack 打包阶段提速?.md.html">12 打包提效:如何为 Webpack 打包阶段提速?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/13 缓存优化:那些基于缓存的优化方案.md.html">13 缓存优化:那些基于缓存的优化方案.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/14 增量构建Webpack 中的增量构建.md.html">14 增量构建Webpack 中的增量构建.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/15 版本特性Webpack 5 中的优化细节.md.html">15 版本特性Webpack 5 中的优化细节.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/16 无包构建:盘点那些 No-bundle 的构建方案.md.html">16 无包构建:盘点那些 No-bundle 的构建方案.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/17 部署初探:为什么一般不在开发环境下部署代码?.md.html">17 部署初探:为什么一般不在开发环境下部署代码?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/18 工具盘点:掌握那些流行的代码部署工具.md.html">18 工具盘点:掌握那些流行的代码部署工具.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/19 安装提效:部署流程中的依赖安装效率优化.md.html">19 安装提效:部署流程中的依赖安装效率优化.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/20 流程优化:部署流程中的构建流程策略优化.md.html">20 流程优化:部署流程中的构建流程策略优化.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/21 容器方案:从构建到部署,容器化方案的优势有哪些?.md.html">21 容器方案:从构建到部署,容器化方案的优势有哪些?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/22 案例分析:搭建基本的前端高效部署系统.md.html">22 案例分析:搭建基本的前端高效部署系统.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/23 结束语 前端效率工程化的未来展望.md.html">23 结束语 前端效率工程化的未来展望.md.html</a>
</li>
</ul>
</div>
</div>
<div class="sidebar-toggle" onclick="sidebar_toggle()" onmouseover="add_inner()" onmouseleave="remove_inner()">
<div class="sidebar-toggle-inner"></div>
</div>
<script>
function add_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.add('show')
}
function remove_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.remove('show')
}
function sidebar_toggle() {
let sidebar_toggle = document.querySelector('.sidebar-toggle')
let sidebar = document.querySelector('.book-sidebar')
let content = document.querySelector('.off-canvas-content')
if (sidebar_toggle.classList.contains('extend')) { // show
sidebar_toggle.classList.remove('extend')
sidebar.classList.remove('hide')
content.classList.remove('extend')
} else { // hide
sidebar_toggle.classList.add('extend')
sidebar.classList.add('hide')
content.classList.add('extend')
}
}
function open_sidebar() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.add('show')
overlay.classList.add('show')
}
function hide_canvas() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.remove('show')
overlay.classList.remove('show')
}
</script>
<div class="off-canvas-content">
<div class="columns">
<div class="column col-12 col-lg-12">
<div class="book-navbar">
<!-- For Responsive Layout -->
<header class="navbar">
<section class="navbar-section">
<a onclick="open_sidebar()">
<i class="icon icon-menu"></i>
</a>
</section>
</header>
</div>
<div class="book-content" style="max-width: 960px; margin: 0 auto;
overflow-x: auto;
overflow-y: hidden;">
<div class="book-post">
<p id="tip" align="center"></p>
<div><h1>12 打包提效:如何为 Webpack 打包阶段提速?</h1>
<p>上节课我们聊了 Webpack 构建流程中第一阶段,也就是编译模块阶段的提效方案,这些方案可以归为三个不同的优化方向。不知道大家课后有没有对照分析自己在项目里用到了其中的哪些方案呢?</p>
<p>今天我们就来继续聊聊 Webpack 构建流程中的第二个阶段,也就是从代码优化到生成产物阶段的效率提升问题(这节课的示例代码参照 [<a href="https://github.com/fe-efficiency/lessons_fe_efficiency/tree/master/12_optimize_efficiency">12_optimize_efficiency]</a>)。</p>
<h3>准备分析工具</h3>
<p>同上节课一样,在分析优化阶段的提效方案之前,我们还是需要先来准备一个分析统计时间的工具。但不同的是,在优化阶段对应的生命周期 Hooks 有很多(参照第 10 讲中的内容)。因此在编写统计插件时,我们要将需要统计的 Hooks 划分为不同区间,如下面的代码所示:</p>
<pre><code>WebpackTimingPlugin.js:
...
const lifeHooks = [
{
name: 'optimizeDependencies',
start: 'optimizeDependencies',
end: 'afterOptimizeDependencies',
},
{ name: 'createChunks', start: 'beforeChunks', end: 'afterChunks' },
...
];
...
let startTime
compilation.hooks[start].tap(PluginName, () =&gt; {
startTime = Date.now()
})
compilation.hooks[end].tap(PluginName, () =&gt; {
const cost = Date.now() - startTime
console.log(`[Step ${name}] costs: ${chalk.red(cost)}ms`)
})
...
</code></pre>
<p>使用后的效果如下图所示:</p>
<p><img src="assets/CgqCHl9kV6KAd5qDAACDxSy2vds191.png" alt="Drawing 0.png" /></p>
<p>通过这样的插件,我们可以分析目前项目中的效率瓶颈,从而进一步为选取优化方案及评估方案效果提供依据。</p>
<h3>优化阶段效率提升的整体分析</h3>
<p>在“第 10 课时|流程分解Webpack 的完整构建流程”中,我们提到了下面的这张图。如图所示,整个优化阶段可以细分为 12 个子任务,每个任务依次对数据进行一定的处理,并将结果传递给下一任务:</p>
<p><img src="assets/CgqCHl9kV6qAUBvfAABnYGwsyYs441.png" alt="Drawing 2.png" /></p>
<p>因此,这一阶段的优化也可以分为两个不同的方向:</p>
<ol>
<li>针对某些任务,使用效率更高的工具或配置项,从而<strong>提升当前任务的工作效率</strong></li>
<li>提升特定任务的优化效果,以减少传递给下一任务的数据量,从而<strong>提升后续环节的工作效率</strong></li>
</ol>
<h3>以提升当前任务工作效率为目标的方案</h3>
<p>一般在项目的优化阶段,主要耗时的任务有两个:一个是生成 ChunkAssets即根据 Chunk 信息<strong>生成 Chunk 的产物代码</strong>;另一个是优化 Assets<strong>压缩 Chunk 产物代码</strong></p>
<p>第一个任务主要在 Webpack 引擎内部的模块中处理,相对而言优化手段较少,主要集中在利用缓存方面,具体将在下节课中讨论。而在压缩 Chunk 产物代码的过程中会用到一些第三方插件,选择不同的插件,以及插件中的不同配置都可能会对其中的效率产生影响。</p>
<p>这节课我们重点来看压缩代码的优化方案。</p>
<h4>面向 JS 的压缩工具</h4>
<p>Webpack 4 中内置了 <a href="https://www.npmjs.com/package/terser-webpack-plugin">TerserWebpackPlugin</a> 作为默认的 JS 压缩工具,之前的版本则需要在项目配置中单独引入,早期主要使用的是 <a href="https://www.npmjs.com/package/uglifyjs-webpack-plugin">UglifyJSWebpackPlugin</a>。这两个 Webpack 插件内部的压缩功能分别基于 <a href="https://www.npmjs.com/package/terser">Terser</a><a href="https://github.com/mishoo/UglifyJS2">UglifyJS</a></p>
<p><a href="https://github.com/babel/minify#benchmarks">第三方的测试结果</a>看,两者在压缩效率与质量方面差别不大,但 Terser 整体上略胜一筹。</p>
<p>从本节课示例代码的运行结果npm run build:jscomp来看如下面的表格所示在不带任何优化配置的情况下3 个测试文件的构建结果都是 Terser 效果更好。</p>
<p><img src="assets/Ciqc1F9kbd6AZL4AAAA8akSVxH8499.png" alt="Lark20200918-161929.png" /></p>
<p><strong>Terser 和 UglifyJS 插件中的效率优化</strong></p>
<p><a href="https://github.com/terser/terser">Terser</a> 原本是 Fork 自 uglify-es 的项目Fork 指从开源项目的某一版本分离出来成为独立的项目),其绝大部分的 API 和参数都与 uglify-es 和 <a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="87f2e0ebeee1feaaedf4c7b4">[email&#160;protected]</a> 兼容。因此,两者对应参数的作用与优化方式也基本相同,这里就以 Terser 为例来分析其中的优化方向。</p>
<p>在作为 Webpack 插件的 TerserWebpackPlugin 中,对执行效率产生影响的配置主要分为 3 个方面:</p>
<ol>
<li><strong>Cache 选项</strong>:默认开启,使用缓存能够极大程度上提升再次构建时的工作效率,这方面的细节我们将在下节课中展开讨论。</li>
<li><strong>Parallel 选项</strong>:默认开启,并发选项在大多数情况下能够提升该插件的工作效率,但具体提升的程度则因项目而异。在小型项目中,多进程通信的额外消耗可能会抵消其带来的益处。</li>
<li><strong>terserOptions 选项</strong>:即 Terser 工具中的 <a href="https://github.com/terser/terser#minify-options">minify 选项集合</a>。这些选项是对具体压缩处理过程产生影响的配置项。我们主要来看其中的<strong>compress</strong><strong>mangle</strong>选项,不同选项的压缩结果如下面的代码所示:</li>
</ol>
<pre><code>//源代码./src/example-terser-opts.js
function HelloWorld() {
const foo = '1234'
console.log(HelloWorld, foo)
}
HelloWorld()
//默认配置项compress={}, mangle=true的压缩后代码
function(e,t){!function e(){console.log(e,&quot;1234&quot;)}()}});
//compress=false的压缩后代码
function(e,r){function t(){var e=&quot;1234&quot;;console.log(t,e)}t()}});
//mangle=false的压缩代码
function(module,exports){!function HelloWorld(){console.log(HelloWorld,&quot;1234&quot;)}()}});
//compress=falsemangle=false的压缩后代码
function(module,exports){function HelloWorld(){var foo=&quot;1234&quot;;console.log(HelloWorld,foo)}HelloWorld()}});
</code></pre>
<p>从上面的例子中可以看到:</p>
<ol>
<li><strong>compress 参数的作用</strong>是执行特定的压缩策略,例如省略变量赋值的语句,从而将变量的值直接替换到引入变量的位置上,减小代码体积。而当 compress 参数为 false 时,这类压缩策略不再生效,示例代码压缩后的体积从 1.16KB 增加到 1.2KB,对压缩质量的影响有限。</li>
<li><strong>mangle 参数的作用</strong>是对源代码中的变量与函数名称进行压缩,当参数为 false 时,示例代码压缩后的体积从 1.16KB 增加到 1.84KB,对代码压缩的效果影响非常大。</li>
</ol>
<p>在了解了两个参数对压缩质量的影响之后,我们再来看下它们对效率的影响。以上面表格中的 example-antd 为例,我制作了下面的表格进行对比:</p>
<p><img src="assets/Ciqc1F9kbdCALcuwAABCdtCwxuY965.png" alt="Lark20200918-161934.png" /></p>
<p>从结果中可以看到,当<strong>compress</strong>参数为 false 时,压缩阶段的效率有明显提升,同时对压缩的质量影响较小。在需要对压缩阶段的效率进行优化的情况下,<strong>可以优先选择设置该参数</strong></p>
<h4>面向 CSS 的压缩工具</h4>
<p>CSS 同样有几种压缩工具可供选择:<a href="https://www.npmjs.com/package/optimize-css-assets-webpack-plugin">OptimizeCSSAssetsPlugin</a>(在 Create-React-App 中使用)、<a href="https://www.npmjs.com/package/@intervolga/optimize-cssnano-plugin">OptimizeCSSNanoPlugin</a>(在 VUE-CLI 中使用),以及<a href="https://www.npmjs.com/package/css-minimizer-webpack-plugin">CSSMinimizerWebpackPlugin</a>2020 年 Webpack 社区新发布的 CSS 压缩插件)。</p>
<p>这三个插件在压缩 CSS 代码功能方面,都默认基于 <a href="https://cssnano.co/">cssnano</a> 实现,因此在压缩质量方面没有什么差别。</p>
<p>在压缩效率方面,首先值得一提的是最新发布的 CSSMinimizerWebpackPlugin<strong>支持缓存和多进程</strong>,这是另外两个工具不具备的。而在非缓存的普通压缩过程方面,整体上 3 个工具相差不大,不同的参数结果略有不同,如下面的表格所示(下面结果为示例代码中 example-css 的执行构建结果)。</p>
<p><img src="assets/CgqCHl9kbb6AI7F5AABRRdbprbU989.png" alt="Lark20200918-161938.png" /></p>
<blockquote>
<p>CSSMinimizerWebpackPlugin 中默认开启多进程选项 parallel但是在测试示例较小的情况下多进程的通信时间反而可能导致效率的降低。测试中关闭多进程选项后构建时间明显缩短。</p>
</blockquote>
<p>从上面的表格中可以看到,三个插件的构建时间基本相近,在开启 sourceMap 的情况下 CSSMinimizerWebpackPlugin 的构建时间相对较长。但考虑到<strong>只有这一新发布的插件支持缓存和多进程</strong>等对项目构建效率影响明显的功能,即使在压缩 CSS 的时间较长的情况下,还是<strong>推荐使用它</strong></p>
<h3>以提升后续环节工作效率为目标的方案</h3>
<p>优化阶段的另一类优化方向是通过对本环节的处理减少后续环节处理内容以便提升后续环节的工作效率。我们列举两个案例Split Chunks分包 和 Tree Shaking摇树</p>
<h4>Split Chunks</h4>
<p><a href="https://webpack.js.org/guides/code-splitting/">Split Chunks分包</a>是指在 Chunk 生成之后,将原先以入口点来划分的 Chunks 根据一定的规则(例如异步引入或分离公共依赖等原则),分离出子 Chunk 的过程。</p>
<p>Split Chunks 有诸多优点例如有利于缓存命中下节课中会提到、有利于运行时的持久化文件缓存等。其中有一类情况能提升后续环节的工作效率即通过分包来抽离多个入口点引用的公共依赖。我们通过下面的代码示例npm run build:split来看一下。</p>
<pre><code>./src/example-split1.js
import { slice } from 'lodash'
console.log('slice', slice([1]))
./src/example-split2.js
import { join } from 'lodash'
console.log('join', join([1], [2]))
./webpack.split.config.js
...
optimization: {
...
splitChunks: {
chunks: 'all'
}
}
...
</code></pre>
<p>在这个示例中,有两个入口文件引入了相同的依赖包 lodash在没有额外设置分包的情况下 lodash 被同时打入到两个产物文件中,在后续的压缩代码阶段耗时 1740ms。<strong>而在设置分包规则为 chunks:'all' 的情况下</strong>,通过分离公共依赖到单独的 Chunk使得在后续压缩代码阶段只需要压缩一次 lodash 的依赖包代码,从而减少了压缩时长,总耗时为 1036ms。通过下面两张图片也可以看出这样的变化。</p>
<p><img src="assets/Ciqc1F9kWAWANNLZAAGM4v1icLA197.png" alt="Drawing 3.png" />
<img src="assets/CgqCHl9kWAqAELXZAAG5xisRryc225.png" alt="Drawing 4.png" /></p>
<p>这里起作用的是 Webpack 4 中内置的 SplitChunksPlugin该插件在 production 模式下默认启用。其默认的分包规则为 chunks: '<strong>async</strong>',作用是分离动态引入的模块 (import('...')),在处理动态引入的模块时能够自动分离其中的公共依赖。</p>
<p>但是对于示例中多入口静态引用相同依赖包的情况,则<strong>不会处理分包。而设置为 chunks: 'all'则能够将所有的依赖情况都进行分包处理从而减少了重复引入相同模块代码的情况。SplitChunksPlugin 的工作阶段是在optimizeChunks</strong>阶段Webpack 4 中是在 optimizeChunksAdvanced在 Webpack 5 中去掉了 basic 和 advanced合并为 optimizeChunks而压缩代码是在 optimizeChunkAssets 阶段,从而起到提升后续环节工作效率的作用。</p>
<h4>Tree Shaking</h4>
<p><a href="https://webpack.js.org/guides/tree-shaking/">Tree Shaking摇树</a>是指在构建打包过程中移除那些引入但未被使用的无效代码Dead-code elimination。这种优化手段最早应用于在 Rollup 工具中,而在 Webpack 2 之后的版本中, Webpack 开始内置这一功能。下面我们先来看一下 Tree Shaking 的例子,如下面的表格所示:</p>
<p><img src="assets/Ciqc1F9kbaqAUkjGAACmMR1PvL4711.png" alt="Lark20200918-161943.png" /></p>
<p>可以看到引入不同的依赖包lodash vs lodash-es、不同的引入方式以及是否使用 babel 等,都会对 Tree Shaking 的效果产生影响。下面我们就来分析具体原因。</p>
<ol>
<li><strong>ES6 模块</strong> 首先,只有 ES6 类型的模块才能进行Tree Shaking。因为 ES6 模块的依赖关系是确定的,因此可以进行不依赖运行时的<strong>静态分析</strong>,而 CommonJS 类型的模块则不能。因此CommonJS 类型的模块 lodash在无论哪种引入方式下都不能实现 Tree Shaking而需要依赖第三方提供的插件例如 babel-plugin-lodash 等)才能实现动态删除无效代码。而 ES6 风格的模块 lodash-es则可以进行 Tree Shaking 优化。</li>
<li><strong>引入方式</strong>:以 default 方式引入的模块,无法被 Tree Shaking而引入单个导出对象的方式无论是使用 import * as xxx 的语法,还是 import {xxx} 的语法,都可以进行 Tree Shaking。</li>
<li><strong>sideEffects</strong>:在 Webpack 4 中,会根据依赖模块 package.json 中的 sideEffects 属性来确认对应的依赖包代码是否会产生副作用。只有 sideEffects 为 false 的依赖包(或不在 sideEffects 对应数组中的文件),才可以实现安全移除未使用代码的功能。在上面的例子中,如果我们查看 lodash-es 的 package.json 文件,可以看到其中包含了 &quot;sideEffects&quot;:false 的描述。此外,在 Webpack 配置的加载器规则和优化配置项中,分别有 rule.sideEffects默认为 false和 optimization.sideEffects默认为 true选项前者指代在要处理的模块中是否有副作用后者指代在优化过程中是否遵循依赖模块的副作用描述。尤其前者常用于对 CSS 文件模块开启副作用模式,以防止被移除。</li>
<li><strong>Babel</strong>:在 Babel 7 之前的<strong>babel-preset-env</strong>modules 的默认选项为 '<strong>commonjs</strong>',因此在使用 babel 处理模块时,即使模块本身是 ES6 风格的,也会在转换过程中,因为被转换而导致无法在后续优化阶段应用 Tree Shaking。而在 Babel 7 之后的 @babel/preset-env 中modules 选项默认为 <strong>auto</strong>’,它的含义是对 ES6 风格的模块不做转换(等同于 modules: false而将其他类型的模块默认转换为 CommonJS 风格。因此我们会看到,后者即使经过 babel 处理,也能应用 Tree Shaking。</li>
</ol>
<h3>总结</h3>
<p>这节课我们主要讨论了代码优化阶段效率提升的方向和方法。这一阶段的优化方向大致可分为两类:一类是以提升当前任务工作效率为目标的方案,这部分我们讨论了压缩 JS 时选择合适的压缩工具与配置优化项,以及压缩 CSS 时对优化工具的选择。另一类是以提升后续环节工作效率为目标的方案,这部分我们讨论了 splitChunks 的作用和配置项,以及应用 Tree Shaking 的一些注意事项。希望通过本节课的学习,帮助你加深对这一阶段 Webpack 处理逻辑的理解,也能够对其中的一些优化方式有更清晰的理解。</p>
<p>今天的<strong>课后思考题是</strong>:回忆 Tree Shaking 的触发条件有哪些?在自己所在的项目里观察试验一下,看看哪些依赖代码在构建时应用了 Tree Shaking 优化,是否存在应该生效但在打包结果中没有被正确移除的代码?</p>
</div>
</div>
<div>
<div style="float: left">
<a href="/专栏/前端工程化精讲-完/11 编译提效:如何为 Webpack 编译阶段提速?.md.html">上一页</a>
</div>
<div style="float: right">
<a href="/专栏/前端工程化精讲-完/13 缓存优化:那些基于缓存的优化方案.md.html">下一页</a>
</div>
</div>
</div>
</div>
</div>
</div>
<a class="off-canvas-overlay" onclick="hide_canvas()"></a>
</div>
<script data-cfasync="false" src="/cdn-cgi/scripts/5c5dd728/cloudflare-static/email-decode.min.js"></script><script defer src="https://static.cloudflareinsights.com/beacon.min.js/v652eace1692a40cfa3763df669d7439c1639079717194" integrity="sha512-Gi7xpJR8tSkrpF7aordPZQlW2DLtzUlZcumS8dMQjwDHEnw9I7ZLyiOj/6tZStRBGtGgN6ceN6cMH8z7etPGlw==" data-cf-beacon='{"rayId":"7099776178c43cfa","version":"2021.12.0","r":1,"token":"1f5d475227ce4f0089a7cff1ab17c0f5","si":100}' crossorigin="anonymous"></script>
</body>
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-NPSEEVD756"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', 'G-NPSEEVD756');
var path = window.location.pathname
var cookie = getCookie("lastPath");
console.log(path)
if (path.replace("/", "") === "") {
if (cookie.replace("/", "") !== "") {
console.log(cookie)
document.getElementById("tip").innerHTML = "<a href='" + cookie + "'>跳转到上次进度</a>"
}
} else {
setCookie("lastPath", path)
}
function setCookie(cname, cvalue) {
var d = new Date();
d.setTime(d.getTime() + (180 * 24 * 60 * 60 * 1000));
var expires = "expires=" + d.toGMTString();
document.cookie = cname + "=" + cvalue + "; " + expires + ";path = /";
}
function getCookie(cname) {
var name = cname + "=";
var ca = document.cookie.split(';');
for (var i = 0; i < ca.length; i++) {
var c = ca[i].trim();
if (c.indexOf(name) === 0) return c.substring(name.length, c.length);
}
return "";
}
</script>
</html>

View File

@@ -1,932 +1,466 @@
<!DOCTYPE html>
<!-- saved from url=(0046)https://kaiiiz.github.io/hexo-theme-book-demo/ -->
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no">
<link rel="icon" href="/static/favicon.png">
<title>13 缓存优化:那些基于缓存的优化方案.md.html</title>
<!-- Spectre.css framework -->
<link rel="stylesheet" href="/static/index.css">
<!-- theme css & js -->
<meta name="generator" content="Hexo 4.2.0">
</head>
<body>
<div class="book-container">
<div class="book-sidebar">
<div class="book-brand">
<a href="/">
<img src="/static/favicon.png">
<span>技术文章摘抄</span>
</a>
</div>
<div class="book-menu uncollapsible">
<ul class="uncollapsible">
<li><a href="/" class="current-tab">首页</a></li>
</ul>
<ul class="uncollapsible">
<li><a href="../">上一级</a></li>
</ul>
<ul class="uncollapsible">
<li>
<a href="/专栏/前端工程化精讲-完/00 开篇词 建立上帝视角,全面系统掌握前端效率工程化.md.html">00 开篇词 建立上帝视角,全面系统掌握前端效率工程化.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/01 项目基石:前端脚手架工具探秘.md.html">01 项目基石:前端脚手架工具探秘.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/02 界面调试:热更新技术如何开着飞机修引擎?.md.html">02 界面调试:热更新技术如何开着飞机修引擎?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/03 构建提速:如何正确使用 SourceMap.md.html">03 构建提速:如何正确使用 SourceMap.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/04 接口调试Mock 工具如何快速进行接口调试?.md.html">04 接口调试Mock 工具如何快速进行接口调试?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/05 编码效率:如何提高编写代码的效率?.md.html">05 编码效率:如何提高编写代码的效率?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/06 团队工具:如何利用云开发提升团队开发效率?.md.html">06 团队工具:如何利用云开发提升团队开发效率?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/07 低代码工具:如何用更少的代码实现更灵活的需求.md.html">07 低代码工具:如何用更少的代码实现更灵活的需求.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/08 无代码工具:如何做到不写代码就能高效交付?.md.html">08 无代码工具:如何做到不写代码就能高效交付?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/09 构建总览:前端构建工具的演进.md.html">09 构建总览:前端构建工具的演进.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/10 流程分解Webpack 的完整构建流程.md.html">10 流程分解Webpack 的完整构建流程.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/11 编译提效:如何为 Webpack 编译阶段提速?.md.html">11 编译提效:如何为 Webpack 编译阶段提速?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/12 打包提效:如何为 Webpack 打包阶段提速?.md.html">12 打包提效:如何为 Webpack 打包阶段提速?.md.html</a>
</li>
<li>
<a class="current-tab" href="/专栏/前端工程化精讲-完/13 缓存优化:那些基于缓存的优化方案.md.html">13 缓存优化:那些基于缓存的优化方案.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/14 增量构建Webpack 中的增量构建.md.html">14 增量构建Webpack 中的增量构建.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/15 版本特性Webpack 5 中的优化细节.md.html">15 版本特性Webpack 5 中的优化细节.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/16 无包构建:盘点那些 No-bundle 的构建方案.md.html">16 无包构建:盘点那些 No-bundle 的构建方案.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/17 部署初探:为什么一般不在开发环境下部署代码?.md.html">17 部署初探:为什么一般不在开发环境下部署代码?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/18 工具盘点:掌握那些流行的代码部署工具.md.html">18 工具盘点:掌握那些流行的代码部署工具.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/19 安装提效:部署流程中的依赖安装效率优化.md.html">19 安装提效:部署流程中的依赖安装效率优化.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/20 流程优化:部署流程中的构建流程策略优化.md.html">20 流程优化:部署流程中的构建流程策略优化.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/21 容器方案:从构建到部署,容器化方案的优势有哪些?.md.html">21 容器方案:从构建到部署,容器化方案的优势有哪些?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/22 案例分析:搭建基本的前端高效部署系统.md.html">22 案例分析:搭建基本的前端高效部署系统.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/23 结束语 前端效率工程化的未来展望.md.html">23 结束语 前端效率工程化的未来展望.md.html</a>
</li>
</ul>
</div>
</div>
<div class="sidebar-toggle" onclick="sidebar_toggle()" onmouseover="add_inner()" onmouseleave="remove_inner()">
<div class="sidebar-toggle-inner"></div>
</div>
<script>
function add_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.add('show')
}
function remove_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.remove('show')
}
function sidebar_toggle() {
let sidebar_toggle = document.querySelector('.sidebar-toggle')
let sidebar = document.querySelector('.book-sidebar')
let content = document.querySelector('.off-canvas-content')
if (sidebar_toggle.classList.contains('extend')) { // show
sidebar_toggle.classList.remove('extend')
sidebar.classList.remove('hide')
content.classList.remove('extend')
} else { // hide
sidebar_toggle.classList.add('extend')
sidebar.classList.add('hide')
content.classList.add('extend')
}
}
function open_sidebar() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.add('show')
overlay.classList.add('show')
}
function hide_canvas() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.remove('show')
overlay.classList.remove('show')
}
</script>
<div class="off-canvas-content">
<div class="columns">
<div class="column col-12 col-lg-12">
<div class="book-navbar">
<!-- For Responsive Layout -->
<header class="navbar">
<section class="navbar-section">
<a onclick="open_sidebar()">
<i class="icon icon-menu"></i>
</a>
</section>
</header>
</div>
<div class="book-content" style="max-width: 960px; margin: 0 auto;
overflow-x: auto;
overflow-y: hidden;">
<div class="book-post">
<p id="tip" align="center"></p>
<div><h1>13 缓存优化:那些基于缓存的优化方案</h1>
<p>上节课的思考题是 Webpack 4 中 Tree Shaking 的触发条件有哪些?我们一起来回忆一下,要让引入的模块支持 Tree Shaking一般有 4 点需要注意:</p>
<ol>
<li>引入的模块需要是 ES6 类型的CommonJS 类型的则不支持。</li>
<li>引入方式不能使用 default。</li>
<li>引用第三方依赖包的情况下,对应的 package.json 需要设置 sideEffects:false 来表明无副作用。</li>
<li>使用 Babel 的情况下,需要注意不同版本 Babel 对于模块化的预设不同。</li>
</ol>
<p>在前面的两节课中,我们讨论了 Webpack 在编译和优化打包阶段的提效方向,以及各自对应的实践方法。除了这些针对具体处理过程的优化方法外,还有一个特定类型的优化方法没有聊到,就是利用缓存数据来加速构建过程的处理。这节课我们就将介绍它。</p>
<h3>缓存优化的基本原理</h3>
<p>在讲缓存优化的原理之前我们先来看下面的例子,如下面的代码和图片所示(本节课的完整示例代码参见 <a href="https://github.com/fe-efficiency/lessons_fe_efficiency/tree/master/13_cache">13_cache</a></p>
<pre><code>./src/example-basic.js
import _ from 'lodash'
</code></pre>
<p><img src="assets/Ciqc1F9kXKSADBZcAAE6sFADuy0456.png" alt="Drawing 0.png" />
<img src="assets/CgqCHl9kXKmAUFg3AAErEUA8F2E549.png" alt="Drawing 1.png" /></p>
<p>可以看到,在没有增加任何优化设置的情况下,初次构建时在 optimizeChunkAssets 阶段的耗时是 1000ms 左右,而再次构建时的耗时直接降到了 18ms几乎可以忽略不计。</p>
<p>这里的原因就在于Webpack 4 内置了压缩插件 TerserWebpackPlugin且默认开启了<a href="https://webpack.js.org/plugins/terser-webpack-plugin/#cache">缓存</a>参数。在初次构建的压缩代码过程中就将这一阶段的结果写入了缓存目录node_modules/.cache/terser-webpack-plugin/)中,当再次构建进行到压缩代码阶段时,即可对比读取已有缓存,如下面的代码所示(相关的代码逻辑在插件的<a href="https://github.com/webpack-contrib/terser-webpack-plugin/blob/master/src/index.js">源代码</a>中可以看到)。</p>
<pre><code>terser-webpack-plugin/src/index.js:
...
if (cache.isEnabled()) {
let taskResult;
try {
taskResult = await cache.get(task); //读取缓存
} catch (ignoreError) {
return enqueue(task); //缓存未命中情况下执行任务
}
task.callback(taskResult); //缓存命中情况下返回缓存结果
...
const enqueue = async (task) =&gt; {
let taskResult;
if (cache.isEnabled() &amp;&amp; !taskResult.error) {
await cache.store(task, taskResult); //写入缓存
}
}
}
</code></pre>
<p>以上就是 TerserWebpackPlugin 插件中利用缓存的基本原理。事实上,在 Webpack 构建流程中还有许多处理过程支持使用缓存,下面我们就来梳理编译和优化打包阶段分别有哪些任务环境可以用到缓存。</p>
<h3>编译阶段的缓存优化</h3>
<p>编译过程的耗时点主要在使用不同加载器Loader来编译模块的过程。下面我们来分别看下几个典型 Loader 中的缓存处理:</p>
<h4>Babel-loader</h4>
<p><a href="https://webpack.js.org/loaders/babel-loader/">Babel-loader</a> 是绝大部分项目中会使用到的 JS/JSX/TS 编译器。在 Babel-loader 中,与缓存相关的设置主要有:</p>
<ul>
<li><strong>cacheDirectory</strong>:默认为 false即不开启缓存。当值为 true 时开启缓存并使用默认缓存目录(./node_modules/.cache/babel-loader/),也可以指定其他路径值作为缓存目录。</li>
<li><strong>cacheIdentifier</strong>:用于计算缓存标识符。默认使用 Babel 相关依赖包的版本、babelrc 配置文件的内容,以及环境变量等与模块内容一起参与计算缓存标识符。如果上述内容发生变化,即使模块内容不变,也不能命中缓存。</li>
<li><strong>cacheCompression</strong>:默认为 true将缓存内容压缩为 gz 包以减小缓存目录的体积。在设为 false 的情况下将跳过压缩和解压的过程,从而提升这一阶段的速度。</li>
</ul>
<p>开启缓存选项前后的构建时长效果如图所示(示例中运行 npm run build:babel可以看到由于开启了 Babel 的缓存,再次构建的速度比初次构建时要快了许多。</p>
<p><img src="assets/CgqCHl9kXL6AIfhNAAB0Ns1VzFM141.png" alt="Drawing 2.png" />
<img src="assets/CgqCHl9kXMOAND8YAACPsQpEG1k472.png" alt="Drawing 3.png" /></p>
<h4>Cache-loader</h4>
<p>在编译过程中利用缓存的第二种方式是使用<a href="https://webpack.js.org/loaders/cache-loader/"> Cache-loader</a>。在使用时,需要将 cache-loader 添加到对构建效率影响较大的 Loader如 babel-loader 等)之前,如下面的代码所示:</p>
<pre><code>./webpack.cache.config.js
...
module: {
rules: [
{
test: /\.js$/,
use: ['cache-loader', 'babel-loader'],
},
],
}
...
</code></pre>
<p>执行两次构建后可以发现,使用 cache-loader 后,比使用 babel-loader 的开启缓存选项后的构建时间更短,如下图所示:</p>
<p><img src="assets/CgqCHl9kXOqAGqBaAAB8XJNiH2c187.png" alt="Drawing 4.png" />
<img src="assets/Ciqc1F9kXO-Ae1fcAABt0doSQD0218.png" alt="Drawing 5.png" /></p>
<p>主要原因是 babel-loader 中的<strong>缓存信息较少</strong>,而 cache-loader 中存储的<strong>Buffer 形式的数据处理效率更高</strong>。下面的示例代码,是 babel-loader 和 cache-loader 入口模块的缓存信息对比:</p>
<pre><code>//babel-loader中的缓存数据
{&quot;ast&quot;:null,&quot;code&quot;:&quot;import _ from 'lodash';&quot;,&quot;map&quot;:null,&quot;metadata&quot;:{},&quot;sourceType&quot;:&quot;module&quot;}
//cache-loader中的缓存数据
{&quot;remainingRequest&quot;:&quot;...lessons_fe_efficiency/13_cache/node_modules/babel-loader/lib/index.js!.../lessons_fe_efficiency/13_cache/src/example-basic.js&quot;,&quot;dependencies&quot;:[{&quot;path&quot;:&quot;...lessons_fe_efficiency/13_cache/src/example-basic.js&quot;,&quot;mtime&quot;:1599191174705},{&quot;path&quot;:&quot;...lessons_fe_efficiency/13_cache/node_modules/cache-loader/dist/cjs.js&quot;,&quot;mtime&quot;:499162500000},{&quot;path&quot;:&quot;.../lessons_fe_efficiency/13_cache/node_modules/babel-loader/lib/index.js&quot;,&quot;mtime&quot;:499162500000}],&quot;contextDependencies&quot;:[],&quot;result&quot;:[{&quot;type&quot;:&quot;Buffer&quot;,&quot;data&quot;:&quot;base64:aW1wb3J0IF8gZnJvbSAnbG9kYXNoJzs=&quot;},null]}
</code></pre>
<h3>优化打包阶段的缓存优化</h3>
<h4>生成 ChunkAsset 时的缓存优化</h4>
<p>在 Webpack 4 中,生成 ChunkAsset 过程中的缓存优化是受限制的:只有在 watch 模式下,且配置中开启 <a href="https://v4.webpack.js.org/configuration/other-options/#cache">cache</a>development 模式下自动开启)才能在这一阶段执行缓存的逻辑。这是因为,在 Webpack 4 中,缓存插件是基于内存的,只有在 watch 模式下才能在内存中获取到相应的缓存数据对象。而在 Webpack 5 中这一问题得到解决,具体的我们会在后续课程中再次展开。</p>
<h4>代码压缩时的缓存优化</h4>
<p>在上一课时中曾提到,在代码压缩阶段,对于 JS 的压缩TerserWebpackPlugin 和 UglifyJSPlugin 都是支持缓存设置的。而对于 CSS 的压缩,目前最新发布的 CSSMinimizerWebpackPlugin 支持且默认开启缓存,其他的插件如 OptimizeCSSAssetsPlugin 和 OptimizeCSSNanoPlugin 目前还不支持使用缓存。</p>
<p>TerserWebpackPlugin 插件的效果在本节课的开头部分我们已经演示过了,这里再来看一下 CSSMinimizerWebpackPlugin 的缓存效果对比,如下面的图片所示,开启该插件的缓存后,再次构建的时长降低到了初次构建的 1/4。</p>
<p><img src="assets/Ciqc1F9kXQGAWkf8AACuXoea9dE075.png" alt="Drawing 6.png" />
<img src="assets/CgqCHl9kXQaAEle1AACx-Pmol-Q677.png" alt="Drawing 7.png" /></p>
<p>以上就是 Webpack 4 中编译与优化打包阶段可用的几种缓存方案。接下来我们再来看下在构建过程中使用缓存的一些注意点。</p>
<h3>缓存的失效</h3>
<p>尽管上面示例所显示的再次构建时间要比初次构建时间快很多,但前提是两次构建没有任何代码发生变化,也就是说,最佳效果是在缓存完全命中的情况下。而现实中,通常需要重新构建的原因是代码发生了变化。因此<strong>如何最大程度地让缓存命中</strong>,成为我们选择缓存方案后首先要考虑的事情。</p>
<h4>缓存标识符发生变化导致的缓存失效</h4>
<p>在上面介绍的支持缓存的 Loader 和插件中,会根据一些固定字段的值加上所处理的模块或 Chunk 的数据 hash 值来生成对应缓存的标识符,例如特定依赖包的版本、对应插件的配置项信息、环境变量等。一旦其中的值发生变化,对应缓存标识符就会发生改变。这也意味着对应工具中,<strong>所有之前的缓存都将失效</strong>。因此,通常情况下我们需要尽可能少地变更会影响到缓存标识符生成的字段。</p>
<p>其中<strong>尤其需要注意的是</strong>,在许多项目的集成构建环境中,特定依赖包由于安装时所生成的语义化版本,导致构建版本时常自动更新,并造成缓存失效。因此,建议大家还是在使用缓存时根据项目的构建使用场景来合理设置对应缓存标识符的计算属性,从而尽可能地减少因为标识符变化而导致缓存失效的情况。</p>
<h4>编译阶段的缓存失效</h4>
<p>编译阶段的执行时间由每个模块的编译时间相加而成。在开启缓存的情况下,代码发生变化的模块将被重新编译,但不影响它所依赖的及依赖它的其他模块,其他模块将继续使用缓存。因此,这一阶段不需要考虑缓存失效扩大化的问题。</p>
<h4>优化打包阶段的缓存失效</h4>
<p>优化打包阶段的缓存失效问题则需要引起注意。还是以课程开头的 example-basic 为例,在使用缓存快速构建后,当我们任意修改入口文件的代码后会发现,代码压缩阶段的时间再次变为和初次构建时相近,也就是说,这一 Chunk 的 Terser 插件的缓存完全失效了,如下面的图片所示。</p>
<p><img src="assets/Ciqc1F9kXRGAUqisAAEy5POkOmg344.png" alt="Drawing 8.png" />
<img src="assets/CgqCHl9kXRaAH-yTAAEq_mHfoPo473.png" alt="Drawing 9.png" /></p>
<p>之所以会出现这样的结果,是因为,尽管在模块编译阶段每个模块是单独执行编译的,但是当进入到代码压缩环节时,各模块已经被组织到了相关联的 Chunk 中。如上面的示例4 个模块最后只生成了一个 Chunk任何一个模块发生变化都会导致整个 Chunk 的内容发生变化,而使之前保存的缓存失效。</p>
<p>在知道了失效原因后,<strong>对应的优化思路也就显而易见了</strong>:尽可能地把那些不变的处理成本高昂的模块打入单独的 Chunk 中。这就涉及了 Webpack 中的分包配置——<a href="https://webpack.js.org/configuration/optimization/#optimizationsplitchunks">splitChunks</a></p>
<h4>使用 splitChunks 优化缓存利用率</h4>
<p>构建分包的好处有许多比如合并通用依赖、提升构建缓存利用率、提升资源访问的缓存利用率、资源懒加载等我们只讨论其中提升构建缓存利用率的部分。在上面示例的基础上只要对设定稍加更改webpack.cache-miss.config.js就会看到即使变更了入口模块的代码也不会对压缩阶段的时间产生多少影响因为主要的依赖包已经分离为独立的 Chunk如下面的代码和图片所示</p>
<pre><code>./webpack.cache-miss.config.js
...
optimization: {
splitChunks: {
chunks: 'all',
},
},
...
</code></pre>
<p><img src="assets/CgqCHl9kXSeAOtCSAAEiD0YND3g006.png" alt="Drawing 10.png" /></p>
<h3>其他使用缓存的注意事项</h3>
<h4>CI/CD 中的缓存目录问题</h4>
<p>在许多自动化集成的系统中项目的构建空间会在每次构建执行完毕后立即回收清理。在这种情况下默认的项目构建缓存目录node_mo dules/.cache将无法留存导致即使项目中开启了缓存设置也无法享受缓存的便利性反而因为需要写入缓存文件而浪费额外的时间。因此在集成化的平台中构建部署的项目如果需要使用缓存则需要根据对应平台的规范将缓存设置到公共缓存目录下。这类问题我们会在第三模块部署优化中再次展开。</p>
<h4>缓存的清理</h4>
<p>缓存的便利性本质在于用磁盘空间换取构建时间。对于一个大量使用缓存的项目,随着时间的流逝,缓存空间会不断增大。这在只有少数项目的个人电脑中还不是非常大的问题,但对于上述多项目的集成环境而言,则需要考虑对缓存区域的定期清理。</p>
<h4>与产物的持久化缓存相区别</h4>
<p>这节课我们没有谈到浏览器端加载资源的缓存问题,以及相对应的如何在 Webpack 中生成产物的持久化缓存方法(即那些你可能比较熟悉的 hash、chunkhash、contenthash因为这一部分知识所影响的是项目访问的性能而对构建的效率没有影响。希望你在学习时清楚地区分这两者的区别。</p>
<h3>总结</h3>
<p>今天我们聊了 Webpack 常规构建效率优化的第三个方面缓存优化主题的内容。Webpack 的构建缓存优化分为两个阶段:编译阶段的针对 Loader 的缓存优化,以及优化打包阶段的针对压缩代码过程的缓存优化。除了了解这些优化的工具和设置外,在使用缓存时还需要额外注意如何减少缓存的失效。此外,针对不同的构建环境,还需要考虑到缓存目录的留存与清理等问题。</p>
<p>这节课的<strong>课后思考题是</strong>:课程中介绍的几种支持缓存的工具在设定上有哪些相似的功能选项?建议你在课后对它们做一个整理对比,以便加深印象。</p>
</div>
</div>
<div>
<div style="float: left">
<a href="/专栏/前端工程化精讲-完/12 打包提效:如何为 Webpack 打包阶段提速?.md.html">上一页</a>
</div>
<div style="float: right">
<a href="/专栏/前端工程化精讲-完/14 增量构建Webpack 中的增量构建.md.html">下一页</a>
</div>
</div>
</div>
</div>
</div>
</div>
<a class="off-canvas-overlay" onclick="hide_canvas()"></a>
</div>
<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v652eace1692a40cfa3763df669d7439c1639079717194" integrity="sha512-Gi7xpJR8tSkrpF7aordPZQlW2DLtzUlZcumS8dMQjwDHEnw9I7ZLyiOj/6tZStRBGtGgN6ceN6cMH8z7etPGlw==" data-cf-beacon='{"rayId":"70997763ff303cfa","version":"2021.12.0","r":1,"token":"1f5d475227ce4f0089a7cff1ab17c0f5","si":100}' crossorigin="anonymous"></script>
</body>
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-NPSEEVD756"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', 'G-NPSEEVD756');
var path = window.location.pathname
var cookie = getCookie("lastPath");
console.log(path)
if (path.replace("/", "") === "") {
if (cookie.replace("/", "") !== "") {
console.log(cookie)
document.getElementById("tip").innerHTML = "<a href='" + cookie + "'>跳转到上次进度</a>"
}
} else {
setCookie("lastPath", path)
}
function setCookie(cname, cvalue) {
var d = new Date();
d.setTime(d.getTime() + (180 * 24 * 60 * 60 * 1000));
var expires = "expires=" + d.toGMTString();
document.cookie = cname + "=" + cvalue + "; " + expires + ";path = /";
}
function getCookie(cname) {
var name = cname + "=";
var ca = document.cookie.split(';');
for (var i = 0; i < ca.length; i++) {
var c = ca[i].trim();
if (c.indexOf(name) === 0) return c.substring(name.length, c.length);
}
return "";
}
</script>
</html>

View File

@@ -1,966 +1,483 @@
<!DOCTYPE html>
<!-- saved from url=(0046)https://kaiiiz.github.io/hexo-theme-book-demo/ -->
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no">
<link rel="icon" href="/static/favicon.png">
<title>14 增量构建Webpack 中的增量构建.md.html</title>
<!-- Spectre.css framework -->
<link rel="stylesheet" href="/static/index.css">
<!-- theme css & js -->
<meta name="generator" content="Hexo 4.2.0">
</head>
<body>
<div class="book-container">
<div class="book-sidebar">
<div class="book-brand">
<a href="/">
<img src="/static/favicon.png">
<span>技术文章摘抄</span>
</a>
</div>
<div class="book-menu uncollapsible">
<ul class="uncollapsible">
<li><a href="/" class="current-tab">首页</a></li>
</ul>
<ul class="uncollapsible">
<li><a href="../">上一级</a></li>
</ul>
<ul class="uncollapsible">
<li>
<a href="/专栏/前端工程化精讲-完/00 开篇词 建立上帝视角,全面系统掌握前端效率工程化.md.html">00 开篇词 建立上帝视角,全面系统掌握前端效率工程化.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/01 项目基石:前端脚手架工具探秘.md.html">01 项目基石:前端脚手架工具探秘.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/02 界面调试:热更新技术如何开着飞机修引擎?.md.html">02 界面调试:热更新技术如何开着飞机修引擎?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/03 构建提速:如何正确使用 SourceMap.md.html">03 构建提速:如何正确使用 SourceMap.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/04 接口调试Mock 工具如何快速进行接口调试?.md.html">04 接口调试Mock 工具如何快速进行接口调试?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/05 编码效率:如何提高编写代码的效率?.md.html">05 编码效率:如何提高编写代码的效率?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/06 团队工具:如何利用云开发提升团队开发效率?.md.html">06 团队工具:如何利用云开发提升团队开发效率?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/07 低代码工具:如何用更少的代码实现更灵活的需求.md.html">07 低代码工具:如何用更少的代码实现更灵活的需求.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/08 无代码工具:如何做到不写代码就能高效交付?.md.html">08 无代码工具:如何做到不写代码就能高效交付?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/09 构建总览:前端构建工具的演进.md.html">09 构建总览:前端构建工具的演进.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/10 流程分解Webpack 的完整构建流程.md.html">10 流程分解Webpack 的完整构建流程.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/11 编译提效:如何为 Webpack 编译阶段提速?.md.html">11 编译提效:如何为 Webpack 编译阶段提速?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/12 打包提效:如何为 Webpack 打包阶段提速?.md.html">12 打包提效:如何为 Webpack 打包阶段提速?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/13 缓存优化:那些基于缓存的优化方案.md.html">13 缓存优化:那些基于缓存的优化方案.md.html</a>
</li>
<li>
<a class="current-tab" href="/专栏/前端工程化精讲-完/14 增量构建Webpack 中的增量构建.md.html">14 增量构建Webpack 中的增量构建.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/15 版本特性Webpack 5 中的优化细节.md.html">15 版本特性Webpack 5 中的优化细节.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/16 无包构建:盘点那些 No-bundle 的构建方案.md.html">16 无包构建:盘点那些 No-bundle 的构建方案.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/17 部署初探:为什么一般不在开发环境下部署代码?.md.html">17 部署初探:为什么一般不在开发环境下部署代码?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/18 工具盘点:掌握那些流行的代码部署工具.md.html">18 工具盘点:掌握那些流行的代码部署工具.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/19 安装提效:部署流程中的依赖安装效率优化.md.html">19 安装提效:部署流程中的依赖安装效率优化.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/20 流程优化:部署流程中的构建流程策略优化.md.html">20 流程优化:部署流程中的构建流程策略优化.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/21 容器方案:从构建到部署,容器化方案的优势有哪些?.md.html">21 容器方案:从构建到部署,容器化方案的优势有哪些?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/22 案例分析:搭建基本的前端高效部署系统.md.html">22 案例分析:搭建基本的前端高效部署系统.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/23 结束语 前端效率工程化的未来展望.md.html">23 结束语 前端效率工程化的未来展望.md.html</a>
</li>
</ul>
</div>
</div>
<div class="sidebar-toggle" onclick="sidebar_toggle()" onmouseover="add_inner()" onmouseleave="remove_inner()">
<div class="sidebar-toggle-inner"></div>
</div>
<script>
function add_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.add('show')
}
function remove_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.remove('show')
}
function sidebar_toggle() {
let sidebar_toggle = document.querySelector('.sidebar-toggle')
let sidebar = document.querySelector('.book-sidebar')
let content = document.querySelector('.off-canvas-content')
if (sidebar_toggle.classList.contains('extend')) { // show
sidebar_toggle.classList.remove('extend')
sidebar.classList.remove('hide')
content.classList.remove('extend')
} else { // hide
sidebar_toggle.classList.add('extend')
sidebar.classList.add('hide')
content.classList.add('extend')
}
}
function open_sidebar() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.add('show')
overlay.classList.add('show')
}
function hide_canvas() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.remove('show')
overlay.classList.remove('show')
}
</script>
<div class="off-canvas-content">
<div class="columns">
<div class="column col-12 col-lg-12">
<div class="book-navbar">
<!-- For Responsive Layout -->
<header class="navbar">
<section class="navbar-section">
<a onclick="open_sidebar()">
<i class="icon icon-menu"></i>
</a>
</section>
</header>
</div>
<div class="book-content" style="max-width: 960px; margin: 0 auto;
overflow-x: auto;
overflow-y: hidden;">
<div class="book-post">
<p id="tip" align="center"></p>
<div><h1>14 增量构建Webpack 中的增量构建</h1>
<p>开始课程前我先来解答上一节课的思考题课程中介绍的几种支持缓存的插件TerserWebpackPluginCSSMinimizerWebpackPlugin和 Loaderbabel-loadercache-loader在缓存方面有哪些相同的配置项呢</p>
<p>通过对比不难发现,这些工具通常至少包含两个配置项:第一项用于指定是否开启缓存,以及指定缓存目录(值为 true 时使用默认目录,指定目录时也表示开启),配置名称通常是 cache 或 cacheDirectory第二项用于指定缓存标识符的计算参数通常默认值是一个包含多维度参数的对象例如这个工具模块的版本号、配置项对象、文件路径和内容等。这个配置项是为了确保缓存使用的安全性防止当源代码不变但相关构建参数发生变化时对旧缓存的误用。</p>
<p>下面开始本节课的学习。曾经有同事问我一个问题:为什么我只改了一行代码,却需要花 5 分钟才能构建完成?</p>
<p>你可能也有同样的疑问,但经过前面几节关于 Webpack 构建原理和优化的课程后,相信已经可以解答。尽管只改动了一行代码,但是在执行构建时,要完整执行所有模块的编译、优化和生成产物的处理过程,而不是只需要处理所改动的文件。大多数情况下,我们能做的是像前面几节课中讨论的那样,通过各种优化方案提升整体构建的效率。</p>
<p>但是只编译打包所改动的文件真的不能实现吗?这节课我们就来讨论这个话题(课程里完整的示例代码参见 <a href="https://github.com/fe-efficiency/lessons_fe_efficiency/tree/master/14_incremental_build">14_incremental_build</a>)。</p>
<h3>Webpack 中的增量构建</h3>
<p>上述只构建改动文件的处理过程在 Webpack 中是实际存在的,你可能也很熟悉,那就是在<strong>开启 devServer</strong>的时候,当我们执行 webpack-dev-server 命令后Webpack 会进行一次初始化的构建构建完成后启动服务并进入到等待更新的状态。当本地文件有变更时Webpack 几乎瞬间将变更的文件进行编译,并将编译后的代码内容推送到浏览器端。你会发现,这个文件变更后的处理过程就符合上面所说的只编译打包改动的文件的操作,这就称为“<strong>增量构建”</strong>。我们通过示例代码进行验证(<em>npm run dev</em>),如下面的图片:</p>
<p><img src="assets/CgqCHl9sTsWAbetxAAGoldlDrIw704.png" alt="Drawing 0.png" />
<img src="assets/Ciqc1F9sTsmAJc8YAADz9x_Zsvo780.png" alt="Drawing 1.png" /></p>
<p>可以看到,在开发服务模式下,初次构建编译了 47 个模块,完整的构建时间为 3306ms。当我们改动其中一个源码文件后日志显示 Webpack 只再次构建了这一个模块因此再次构建的时间非常短24ms。那么为什么在开发服务模式下可以实现增量构建的效果而在生产环境下不行呢下面我们来分析影响结果的因素。</p>
<h3>增量构建的影响因素</h3>
<h4>watch 配置</h4>
<p>在上面的增量构建过程中,第一个想到的就是<strong>需要监控文件的变化</strong>。显然,只有得知变更的是哪个文件后,才能进行后续的针对性处理。要实现这一点也很简单,在“第 2 课时|界面调试:热更新技术如何开着飞机修引擎?”中已经介绍过,在 Webpack 中<strong>启用 watch 配置</strong>即可,此外在使用 devServer 的情况下,该选项会<strong>默认开启</strong>。那么,如果在生产模式下开启 watch 配置,是不是再次构建时,就会按增量的方式执行呢?我们仍然通过示例验证(<em>npm run build:watch</em>),如下面的图片所示:</p>
<p><img src="assets/CgqCHl9sTtOAPzPRAAHMQJnGHlo474.png" alt="Drawing 2.png" />
<img src="assets/CgqCHl9sTtiAB2seAAG0v0B0ORQ594.png" alt="Drawing 3.png" /></p>
<p>从结果中可以发现,在生产模式下开启 watch 配置后,相比初次构建,再次构建所编译的模块数量并未减少,即使只改动了一个文件,也仍然会对所有模块进行编译。因此可以得出结论,在生产环境下只开启 watch 配置后的再次构建<strong>并不能</strong>实现增量构建。</p>
<h4>cache 配置</h4>
<p>仔细查阅 Webpack 的配置项文档,会在菜单最下方的“其他选项”一栏中找到 <a href="https://v4.webpack.js.org/configuration/other-options/#cache">cache</a> 选项(需要注意的是我们查阅的是 <strong>Webpack 4 版本的文档</strong>Webpack 5 中这一选项会有大的改变,会在下一节课中展开讨论)。这一选项的值有两种类型:布尔值和对象类型。一般情况下默认为<strong>false</strong>,即不使用缓存,但在开发模式开启 watch 配置的情况下cache 的默认值变更为<strong>true</strong>。此外,如果 cache 传值为对象类型,则表示使用该对象来作为缓存对象,这往往用于多个编译器 compiler 的调用情况。</p>
<p>下面我们就来看一下在生产模式下如果watch 和 cache 都为 true结果会如何npm run build:watch-cache如下面的图片所示</p>
<p><img src="assets/CgqCHl9sTuuAc0_4AAHBe2Lt3do732.png" alt="Drawing 4.png" />
<img src="assets/Ciqc1F9sTvCAY2NvAAEtJYxCA_8121.png" alt="Drawing 5.png" /></p>
<p>正如我们所期望的,再次构建时,在编译模块阶段只对有变化的文件进行了重新编译,实现了<strong>增量编译</strong>的效果。</p>
<p>但是美中不足的是,在优化阶段压缩代码时仍然耗费了较多的时间。这一点很容易理解:</p>
<p>体积最大的 react、react-dom 等模块和入口模块打入了同一个 Chunk 中,即使修改的模块是单独分离的 bar.js但它的产物名称的变化仍然需要反映在入口 Chunk 的 runtime 模块中。因此入口 Chunk 也需要跟着重新压缩而无法复用压缩缓存数据。根据前面几节课的知识点,我们对配置再做一些优化,将 vendor 分离后再来看看效果,如下面的图片所示:</p>
<p><img src="assets/CgqCHl9sTvqAP1oIAAG2kbb-DGY688.png" alt="Drawing 6.png" />
<img src="assets/CgqCHl9sTv6AYxTKAAFAsmUEZMg953.png" alt="Drawing 7.png" /></p>
<p>可以看到,通过上面这一系列的配置后(<strong>watch + cache</strong>),在生产模式下,最终呈现出了我们期望的<strong>增量构建</strong>效果:有文件发生变化时会自动编译变更的模块,并只对该模块影响到的少量 Chunk 进行优化并更新产物文件版本,而其他产物文件则保持之前的版本。如此,整个构建过程的速度大大提升。</p>
<h3>增量构建的实现原理</h3>
<p>为什么在配置项中需要同时启用 watch 和 cache 配置才能获得增量构建的效果呢?接下来我们从源码层面分析。</p>
<h4>watch 配置的作用</h4>
<p>watch 配置的具体逻辑在 Webpack 的 <a href="https://github.com/webpack/webpack/blob/webpack-4/lib/Watching.js">Watching.js</a> 中。查看源码可以看到,在它构建相关的 _go 方法中,执行的依然是 compiler实例的 compile 方法,这一点与普通构建流程并无区别。真正的区别在于,在 watch 模式下,构建完成后并不自动退出,因此构建上下文的对象(包括前一次构建后的缓存数据对象)都可以保留在内存中,并在 rebuild 时重复使用,如下面的代码所示:</p>
<pre><code>lib/Watching.js
...
_go() {
...
this.compiler.hooks.watchRun.callAsync(this.compiler, err =&gt; {
const onCompiled = (err, compilation) =&gt; {
...
}
this.compiler.compile(onCompiled);
}
}
</code></pre>
<h4>cache 配置的作用</h4>
<p>cache 配置的源码逻辑主要涉及两个文件:<a href="https://github.com/webpack/webpack/blob/webpack-4/lib/CachePlugin.js">CachePlugin.js</a><a href="https://github.com/webpack/webpack/blob/webpack-4/lib/Compilation.js">Compilation.js</a>。其中 CachePlugin.js 的核心作用是将该插件实例的 cache 属性传入 compilation 实例中,如下面的代码所示:</p>
<pre><code>lib/CachePlugin.js
...
compiler.hooks.thisCompilation.tap(&quot;CachePlugin&quot;, compilation =&gt; {
compilation.cache = cache;
...
}
</code></pre>
<p>而在 Compilation.js 中,运用 cache 的地方有两处:</p>
<ol>
<li><strong>编译阶段添加模块时</strong>,若命中缓存<strong>module</strong>,则直接跳过该模块的编译过程(与 cache-loader 等作用于加载器的缓存不同,此处的缓存可直接跳过 Webpack 内置的编译阶段)。</li>
<li>在创建 Chunk 产物代码阶段,若命中缓存<strong>Chunk</strong>,则直接跳过该 Chunk 的产物代码生成过程。</li>
</ol>
<p>如下面的代码所示:</p>
<pre><code>lib/Compilation.js
...
addModule(module, cacheGroup) {
...
if (this.cache &amp;&amp; this.cache[cacheName]) {
const cacheModule = this.cache[cacheName];
...
//缓存模块存在情况下判断是否需要rebuild
rebuild = ...
if (!rebuild) {
...
//无须rebuild情况下返回cacheModule并标记build:false
return {
module: cacheModule,
issuer: true,
build: false,
dependencies: true
}
}
...
}
if (this.cache) {
this.cache[cacheName] = module;
}
...
//无缓存或需要rebuild情况下返回module并标记build:true
return {
module: module,
issuer: true,
build: true,
dependencies: true
};
}
...
createChunkAssets() {
...
if ( this.cache &amp;&amp; this.cache[cacheName] &amp;&amp; this.cache[cacheName].hash === usedHash ) {
source = this.cache[cacheName].source;
} else {
source = fileManifest.render();
...
}
}
</code></pre>
<p>以上就是 Webpack 4 中 watch 和 cache 配置的作用原理。通过 Webpack 内置的 cache 插件,将整个构建中相对耗时的两个内部处理环节——编译模块和生成产物,进行缓存的读写处理,从而实现增量构建处理。那么我们是不是就可以在生产环境下直接使用这个方案呢?</p>
<h3>生产环境下使用增量构建的阻碍</h3>
<p>增量构建之所以快是因为将构建所需的数据项目文件、node_modules 中的文件数据、历史构建后的缓存数据等)都<strong>保留在内存中</strong>。在 watch 模式下保留着构建使用的 Node 进程,使得下一次构建时可以直接读取内存中的数据。</p>
<p>而生产环境下的构建通常在集成部署系统中进行。对于管理多项目的构建系统而言,构建过程是任务式的:任务结束后即结束进程并回收系统资源。对于这样的系统而言,增量构建所需的保留进程与长时间占用内存,通常都是<strong>不可接受的</strong></p>
<p>因此,基于内存的缓存数据注定无法运用到生产环境中。要想在生产环境下提升构建速度,<strong>首要条件是将缓存写入到文件系统中</strong>。只有将文件系统中的缓存数据持久化,才能脱离对保持进程的依赖,你只需要在每次构建时将缓存数据读取到内存中进行处理即可。事实上,这也是上一课时中讲到的那些 Loader 与插件中的缓存数据的存储方式。</p>
<p>遗憾的是Webpack 4 中的 cache 配置<strong>只支持基于内存的缓存</strong>,并不支持文件系统的缓存。因此,我们只能通过上节课讲到的一些支持缓存的第三方处理插件将局部的构建环节应用“<strong>增量处理</strong>”。</p>
<p>不过好消息是 Webpack 5 中<strong>正式支持基于文件系统的持久化缓存</strong>Persistent Cache。我们会在下一课时详细讨论包括这一特性在内的 Webpack 5 中的优化点。</p>
<h3>总结</h3>
<p>这节课我们主要讨论了构建处理的一种理想情况:增量构建。增量构建在每次执行构建时,只编译处理内容有修改的少量文件,从而极大地提升构建效率。</p>
<p>在 Webpack 4 中有两个配置项与增量构建相关watch 和 cache。当我们启用开发服务器时这两个选项都是默认启用的因此可以在开发模式下体验到增量构建带来的速度提升。</p>
<p>从内部原理的角度分析watch 的作用是保留进程,使得初次构建后的数据对象能够在再次构建时复用。而 cache 的作用则体现在构建过程中,在添加模块与生成产物代码时可以利用 cache 对象进行相应阶段结果数据的读写。显然,这种基于内存的缓存方式无法在生产环境下广泛使用。</p>
<p>今天的<strong>课后思考题</strong>是:在启用增量构建的情况下有时候可能还会遇到 rebuild 很慢的情况,试着分析原因。</p>
</div>
</div>
<div>
<div style="float: left">
<a href="/专栏/前端工程化精讲-完/13 缓存优化:那些基于缓存的优化方案.md.html">上一页</a>
</div>
<div style="float: right">
<a href="/专栏/前端工程化精讲-完/15 版本特性Webpack 5 中的优化细节.md.html">下一页</a>
</div>
</div>
</div>
</div>
</div>
</div>
<a class="off-canvas-overlay" onclick="hide_canvas()"></a>
</div>
<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v652eace1692a40cfa3763df669d7439c1639079717194" integrity="sha512-Gi7xpJR8tSkrpF7aordPZQlW2DLtzUlZcumS8dMQjwDHEnw9I7ZLyiOj/6tZStRBGtGgN6ceN6cMH8z7etPGlw==" data-cf-beacon='{"rayId":"709977664bfe3cfa","version":"2021.12.0","r":1,"token":"1f5d475227ce4f0089a7cff1ab17c0f5","si":100}' crossorigin="anonymous"></script>
</body>
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-NPSEEVD756"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', 'G-NPSEEVD756');
var path = window.location.pathname
var cookie = getCookie("lastPath");
console.log(path)
if (path.replace("/", "") === "") {
if (cookie.replace("/", "") !== "") {
console.log(cookie)
document.getElementById("tip").innerHTML = "<a href='" + cookie + "'>跳转到上次进度</a>"
}
} else {
setCookie("lastPath", path)
}
function setCookie(cname, cvalue) {
var d = new Date();
d.setTime(d.getTime() + (180 * 24 * 60 * 60 * 1000));
var expires = "expires=" + d.toGMTString();
document.cookie = cname + "=" + cvalue + "; " + expires + ";path = /";
}
function getCookie(cname) {
var name = cname + "=";
var ca = document.cookie.split(';');
for (var i = 0; i < ca.length; i++) {
var c = ca[i].trim();
if (c.indexOf(name) === 0) return c.substring(name.length, c.length);
}
return "";
}
</script>
</html>

View File

@@ -1,978 +1,489 @@
<!DOCTYPE html>
<!-- saved from url=(0046)https://kaiiiz.github.io/hexo-theme-book-demo/ -->
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no">
<link rel="icon" href="/static/favicon.png">
<title>15 版本特性Webpack 5 中的优化细节.md.html</title>
<!-- Spectre.css framework -->
<link rel="stylesheet" href="/static/index.css">
<!-- theme css & js -->
<meta name="generator" content="Hexo 4.2.0">
</head>
<body>
<div class="book-container">
<div class="book-sidebar">
<div class="book-brand">
<a href="/">
<img src="/static/favicon.png">
<span>技术文章摘抄</span>
</a>
</div>
<div class="book-menu uncollapsible">
<ul class="uncollapsible">
<li><a href="/" class="current-tab">首页</a></li>
</ul>
<ul class="uncollapsible">
<li><a href="../">上一级</a></li>
</ul>
<ul class="uncollapsible">
<li>
<a href="/专栏/前端工程化精讲-完/00 开篇词 建立上帝视角,全面系统掌握前端效率工程化.md.html">00 开篇词 建立上帝视角,全面系统掌握前端效率工程化.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/01 项目基石:前端脚手架工具探秘.md.html">01 项目基石:前端脚手架工具探秘.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/02 界面调试:热更新技术如何开着飞机修引擎?.md.html">02 界面调试:热更新技术如何开着飞机修引擎?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/03 构建提速:如何正确使用 SourceMap.md.html">03 构建提速:如何正确使用 SourceMap.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/04 接口调试Mock 工具如何快速进行接口调试?.md.html">04 接口调试Mock 工具如何快速进行接口调试?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/05 编码效率:如何提高编写代码的效率?.md.html">05 编码效率:如何提高编写代码的效率?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/06 团队工具:如何利用云开发提升团队开发效率?.md.html">06 团队工具:如何利用云开发提升团队开发效率?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/07 低代码工具:如何用更少的代码实现更灵活的需求.md.html">07 低代码工具:如何用更少的代码实现更灵活的需求.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/08 无代码工具:如何做到不写代码就能高效交付?.md.html">08 无代码工具:如何做到不写代码就能高效交付?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/09 构建总览:前端构建工具的演进.md.html">09 构建总览:前端构建工具的演进.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/10 流程分解Webpack 的完整构建流程.md.html">10 流程分解Webpack 的完整构建流程.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/11 编译提效:如何为 Webpack 编译阶段提速?.md.html">11 编译提效:如何为 Webpack 编译阶段提速?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/12 打包提效:如何为 Webpack 打包阶段提速?.md.html">12 打包提效:如何为 Webpack 打包阶段提速?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/13 缓存优化:那些基于缓存的优化方案.md.html">13 缓存优化:那些基于缓存的优化方案.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/14 增量构建Webpack 中的增量构建.md.html">14 增量构建Webpack 中的增量构建.md.html</a>
</li>
<li>
<a class="current-tab" href="/专栏/前端工程化精讲-完/15 版本特性Webpack 5 中的优化细节.md.html">15 版本特性Webpack 5 中的优化细节.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/16 无包构建:盘点那些 No-bundle 的构建方案.md.html">16 无包构建:盘点那些 No-bundle 的构建方案.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/17 部署初探:为什么一般不在开发环境下部署代码?.md.html">17 部署初探:为什么一般不在开发环境下部署代码?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/18 工具盘点:掌握那些流行的代码部署工具.md.html">18 工具盘点:掌握那些流行的代码部署工具.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/19 安装提效:部署流程中的依赖安装效率优化.md.html">19 安装提效:部署流程中的依赖安装效率优化.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/20 流程优化:部署流程中的构建流程策略优化.md.html">20 流程优化:部署流程中的构建流程策略优化.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/21 容器方案:从构建到部署,容器化方案的优势有哪些?.md.html">21 容器方案:从构建到部署,容器化方案的优势有哪些?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/22 案例分析:搭建基本的前端高效部署系统.md.html">22 案例分析:搭建基本的前端高效部署系统.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/23 结束语 前端效率工程化的未来展望.md.html">23 结束语 前端效率工程化的未来展望.md.html</a>
</li>
</ul>
</div>
</div>
<div class="sidebar-toggle" onclick="sidebar_toggle()" onmouseover="add_inner()" onmouseleave="remove_inner()">
<div class="sidebar-toggle-inner"></div>
</div>
<script>
function add_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.add('show')
}
function remove_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.remove('show')
}
function sidebar_toggle() {
let sidebar_toggle = document.querySelector('.sidebar-toggle')
let sidebar = document.querySelector('.book-sidebar')
let content = document.querySelector('.off-canvas-content')
if (sidebar_toggle.classList.contains('extend')) { // show
sidebar_toggle.classList.remove('extend')
sidebar.classList.remove('hide')
content.classList.remove('extend')
} else { // hide
sidebar_toggle.classList.add('extend')
sidebar.classList.add('hide')
content.classList.add('extend')
}
}
function open_sidebar() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.add('show')
overlay.classList.add('show')
}
function hide_canvas() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.remove('show')
overlay.classList.remove('show')
}
</script>
<div class="off-canvas-content">
<div class="columns">
<div class="column col-12 col-lg-12">
<div class="book-navbar">
<!-- For Responsive Layout -->
<header class="navbar">
<section class="navbar-section">
<a onclick="open_sidebar()">
<i class="icon icon-menu"></i>
</a>
</section>
</header>
</div>
<div class="book-content" style="max-width: 960px; margin: 0 auto;
overflow-x: auto;
overflow-y: hidden;">
<div class="book-post">
<p id="tip" align="center"></p>
<div><h1>15 版本特性Webpack 5 中的优化细节</h1>
<p>开始课程前,我们先来解答上一节课的思考题:为什么在开启增量构建后,有时候 rebuild 还是会很慢呢我们可以从两方面来找原因。首先Webpack 4 中的增量构建只运用到了新增模块与生成 Chunk 产物阶段,其他处理过程(如代码压缩)仍需要通过其他方式进行优化,例如分包和压缩插件的缓存。其次,过程中的一些处理会额外增加构建时间,例如生成 Source Map 等。因此还是需要通过统计各阶段的执行时间来具体问题具体分析。</p>
<p>然后开始这节课的学习。在上节课里,我们讨论了 Webpack 4 中增量构建的原理,也分析了为什么在生产环境下难以使用增量构建,其中最主要的一点是 Webpack 4 中没有基于文件系统的持久化缓存。这个问题在 Webpack 5 中得到了解决,这节课我们就来看看 Webpack 5 有哪些新的功能特性。</p>
<h3>Webpack 5 中的效率优化点</h3>
<p>Webpack 5 中的变化有很多,完整的功能变更清单参见<a href="https://github.com/webpack/changelog-v5">官方文档</a>,这里我们介绍其中与构建效率相关的几个主要功能点:</p>
<ul>
<li>Persistent Caching</li>
<li>Tree Shaking</li>
<li>Logs</li>
</ul>
<h3>Persistent Caching</h3>
<p>首先我们通过示例来看下 Webpack 5 中缓存方面的变化。</p>
<h4>持久化缓存的示例</h4>
<p>如下面的代码和图片所示:</p>
<pre><code>./webpack.cache.config.js
...
module.exports = {
...
cache: {
type: 'filesystem',
cacheLocation: path.resolve(__dirname, '.appcache'),
buildDependencies: {
config: [__filename],
},
},
...
}
</code></pre>
<p><img src="assets/Ciqc1F9sT2WAI_vnAAGUeALmmZo570.png" alt="Drawing 0.png" />
<img src="assets/CgqCHl9sT2qAexnjAADgsW9ijYU168.png" alt="Drawing 1.png" />
<img src="assets/Ciqc1F9sT26AIkKYAAEVjcm9aeY144.png" alt="Drawing 2.png" /></p>
<p>可以看到,初次构建完整花费了 3282ms而在不修改代码进行再次构建的情况下只花费了不到原先时间的 1/10。在修改代码文件的新情况下也只花费了 628ms多花费的时间体现在构建被修改的文件的编译上这就实现了上一课时所寻求的<strong>生产环境下的增量构建</strong></p>
<h4>Cache 基本配置</h4>
<p>在 Webpack 4 中cache 只是单个属性的配置,所对应的赋值为 true 或 false用来代表是否启用缓存或者赋值为对象来表示在构建中使用的缓存对象。而在 Webpack 5 中,<a href="https://webpack.js.org/configuration/other-options/#cache">cache</a> 配置除了原本的 true 和 false 外,还增加了许多子配置项,例如:</p>
<ul>
<li>cache.<strong>type</strong>:缓存类型。值为 'memory'或filesystem分别代表基于内存的临时缓存以及基于文件系统的持久化缓存。在选择 filesystem 的情况下,下面介绍的其他属性生效。</li>
<li>cache.<strong>cacheDirectory</strong>:缓存目录。默认目录为 node_modules/.cache/webpack。</li>
<li>cache.<strong>name</strong>:缓存名称。同时也是 cacheDirectory 中的子目录命名,默认值为 Webpack 的 ${config.name}-${config.mode}。</li>
<li>cache.<strong>cacheLocation</strong>缓存真正的存放地址。默认使用的是上述两个属性的组合path.resolve(cache.cacheDirectory, cache.name)。该属性在赋值情况下将忽略上面的 cacheDirectory 和 name 属性。</li>
</ul>
<h4>单个模块的缓存失效</h4>
<p>Webpack 5 会跟踪每个模块的依赖项fileDependencies、contextDependencies、missingDependencies。当模块本身或其依赖项发生变更时Webpack 能找到所有受影响的模块,并重新进行构建处理。</p>
<p>这里需要注意的是,对于 node_modules 中的第三方依赖包中的模块出于性能考虑Webpack 不会跟踪具体模块文件的内容和修改时间,而是依据依赖包里<strong>package.json 的 name 和 version 字段</strong>来判断模块是否发生变更。因此,单纯修改 node_modules 中的模块内容,在构建时不会触发缓存的失效。</p>
<h4>全局的缓存失效</h4>
<p>当模块代码没有发生变化,但是构建处理过程本身发生变化时(例如升级了 Webpack 版本、修改了配置文件、改变了环境变量等),也可能对构建后的产物代码产生影响。因此在这种情况下不能复用之前缓存的数据,而需要让全局缓存失效,重新构建并生成新的缓存。在 Webpack 5 中共提供了 3 种不同维度的全局缓存失效配置。</p>
<p><strong>buildDependencies</strong></p>
<p>第一种配置是cache.buildDependencies用于指定可能对构建过程产生影响的依赖项。</p>
<p>它的默认选项是{defaultWebpack: [&quot;webpack/lib&quot;]}。这一选项的含义是,当 node_modules 中的 Webpack 或 Webpack 的依赖项(例如 watchpack 等)发生变化时,当前的构建缓存即失效。</p>
<p>上述选项是默认内置的,无须写在项目配置文件中。配置文件中的 buildDenpendencies 还支持增加另一种选项 {config: [__filename]},它的作用是当配置文件内容或配置文件依赖的模块文件发生变化时,当前的构建缓存即失效。</p>
<p><strong>version</strong></p>
<p>第二种配置是 cache.version。当配置文件和代码都没有发生变化但是构建的外部依赖如环境变量发生变化时预期的构建产物代码也可能不同。这时就可以使用 version 配置来防止在外部依赖不同的情况下混用了相同的缓存。例如,可以传入 cache: {version: process.env.NODE_ENV},达到当不同环境切换时彼此不共用缓存的效果。</p>
<p><strong>name</strong></p>
<p>缓存的名称除了作为默认的缓存目录下的子目录名称外,也起到区分缓存数据的作用。例如,可以传入 cache: {name: process.env.NODE_ENV}。这里有两点需要补充说明:</p>
<ul>
<li><strong>name 的特殊性</strong>:与 version 或 buildDependencies 等配置不同name 在默认情况下是作为缓存的子目录名称存在的,因此可以利用 name<strong>保留多套缓存</strong>。在 name 切换时,若已存在同名称的缓存,则可以<strong>复用之前的缓存</strong>。与之相比,当其他全局配置发生变化时,会直接将之前的缓存失效,即使切换回之前已缓存过的设置,也会当作无缓存处理。</li>
<li>当 cacheLocation 配置存在时,将忽略 name 的缓存目录功能,上述多套缓存复用的功能<strong>也将失效</strong></li>
</ul>
<h4>其他</h4>
<p>除了上述介绍的配置项外cache 还支持其他属性managedPath、hashAlgorithm、store、idleTimeout 等,具体功能可以通过<a href="https://webpack.js.org/configuration/other-options/#cache">官方文档</a>进行查询。</p>
<p>此外,在 Webpack 4 中,部分插件是默认启用缓存功能的(例如压缩代码的 Terser 插件等),项目在生产环境下构建时,可能无意识地享受缓存带来的效率提升,但是在 Webpack 5 中则不行。无论是否设置 cache 配置Webpack 5 都将忽略各插件的缓存设置(例如 <a href="https://webpack.js.org/plugins/terser-webpack-plugin/#cache">TerserWebpackPlugin</a>),而由引擎自身提供构建各环节的缓存读写逻辑。<strong>因此,项目在迁移到 Webpack 5 时都需要通过上面介绍的 cache 属性来单独配置缓存。</strong></p>
<h3>Tree Shaking</h3>
<p>Webpack 5 中的另一项优化体现在 Tree Shaking 功能方面。在之前课程中介绍过Webpack 4 中的 Tree Shaking 功能在使用上存在限制:只支持 ES6 类型的模块代码分析,且需要相应的依赖包或需要函数声明为无副作用等。这使得在实际项目构建过程中 Tree Shaking 的优化效果往往不尽如人意。而这一问题在 Webpack 5 中得到了不少改善。</p>
<h4>Nested Tree Shaking</h4>
<p>Webpack 5 增加了对嵌套模块的导出跟踪功能,能够找到那些嵌套在最内层而未被使用的模块属性。例如下面的示例代码,在构建后的结果代码中只包含了引用的内部模块的一个属性,而忽略了不被引用的内部模块和中间模块的其他属性:</p>
<pre><code>//./src/inner-module.js
export const a = 'inner_a'
export const b = 'inner_b'
//.src/nested-module.js
import * as inner from './inner-module'
const nested = 'nested'
export { inner, nested }
//./src/example-tree-nested.js
import * as nested from './nested-module'
console.log(nested.inner.a)
//./dist/tree-nest.js
(()=&gt;{&quot;use strict&quot;;console.log(&quot;inner_a&quot;)})();
</code></pre>
<h4>Inner Module Tree Shaking</h4>
<p>除了上面对嵌套引用模块的依赖分析优化外Webpack 5 中还增加了分析模块中导出项与导入项的依赖关系的功能。通过 optimization.innerGraph生产环境下默认开启选项Webpack 5 可以分析特定类型导出项中对导入项的依赖关系,从而找到更多未被使用的导入模块并加以移除。例如下面的示例代码:</p>
<pre><code>//./src/inner-module.js
export const a = 'inner_a'
export const b = 'inner_b'
export const c = 'inner_c'
//./src/example-tree-nested.js 同上面示例
//.src/nested-module.js
...
const useB = function () {
return inner.b
}
export const usingB = function () {
return useB()
}
//./dist/tree-nest.js (默认optimization.innerGraph = true)
... const t=&quot;inner_a&quot;,n=&quot;inner_b&quot;} ...
//./dist/tree-nest.js (optimization.innerGraph = false)
... const t=&quot;inner_a&quot;} ...
</code></pre>
<p>在 nested-module.js 中新增了导出项 usingB该导出项间接依赖导入项 inner.b而这一导出项在入口模块中并未使用。在默认情况下构建完成后只保留真正被使用的 inner.a。但是如果将优化项 innerGraph 关闭(且需要同时设置 concatenateModules:false构建后会发现间接引用的导出项没有被移除该导出项间接引用的 inner.b 也被保留到了产物代码中。</p>
<h4>CommonJS Tree Shaking</h4>
<p>Webpack 5 中增加了对一些 CommonJS 风格模块代码的静态分析功功能:</p>
<ul>
<li>支持 exports.xxx、this.exports.xxx、module.exports.xxx 语法的导出分析。</li>
<li>支持 object.defineProperty(exports, &quot;xxxx&quot;, ...) 语法的导出分析。</li>
<li>支持 require('xxxx').xxx 语法的导入分析。</li>
</ul>
<p>例如下面的代码:</p>
<pre><code>//./src/commonjs-module.js
exports.a = 11
this.exports.b = 22
module.exports.c = 33
console.log('module')
//./src/example-tree-commonjs.js
const a = require('./commonjs-module').a
console.log(a)
//./dist/tree-commonjs.js
()=&gt;{var o={263:function(o,r){r.a=11,console.log(&quot;module&quot;)}}...
</code></pre>
<p>可以看到产物代码中只有被引入的属性 a 和 console 语句,而其他两个导出属性 b 和 c 已经在产物中被排除了。</p>
<h3>Logs</h3>
<p>第三个要提到的 Webpack 5 的效率优化点是,它增加了许多内部处理过程的日志,可以通过 stats.logging 来访问。下面两张图是使用相同配置*stats: {logging: &quot;verbose&quot;}*的情况下Webpack 4 和 Webpack 5 构建输出的日志:</p>
<p><img src="assets/CgqCHl9sT6WAWzGiAACp4k0mjjw366.png" alt="Drawing 3.png" />
<img src="assets/Ciqc1F9sT6qAeQs4AAMWioPCn4s820.png" alt="Drawing 4.png" /></p>
<p>可以看到Webpack 5 构建输出的日志要丰富完整得多。通过这些日志能够很好地反映构建各阶段的处理过程、耗费时间,以及缓存使用的情况。在大多数情况下,它已经能够代替之前人工编写的统计插件功能了。</p>
<h3>其他功能优化项</h3>
<p>除了上面介绍的和构建效率相关的几项变化外Webpack 5 中还有许多大大小小的功能变化,例如新增了改变微前端构建运行流程的 <a href="https://webpack.js.org/concepts/module-federation/">Module Federation</a> 和对产物代码进行优化处理的 Runtime Modules优化了处理模块的工作队列在生命周期 Hooks 中增加了 stage 选项等。感兴趣的话,你可以通过文章顶部的文档链接或官方网站来进一步了解。</p>
<h3>总结</h3>
<p>在本节课上线后不久,<a href="https://github.com/webpack/webpack/issues/11406">Webpack 5 的稳定版本</a>将对外发布2020 年 10 月 10 日)。希望这节课能让你对它有一个初步的印象。</p>
<p>本节课我们主要了解了 Webpack 最新版本与构建效率相关的几个优化功能点,其中最重要的就是 Webpack 5 中引入的持久化缓存的特性。在这个部分我们讨论了如何开启和定制持久化缓存以及通过哪些方式可以让缓存主动失效以确保在项目里可以安全地享受缓存带来的效率提升。此外Webpack 5 中对于 Tree Shaking 的优化也能帮助我们更好地优化项目依赖,减小构建产物的体积。</p>
<p>本节课的<strong>课后思考题是</strong>:结合今天所讲的持久化缓存和日志统计,分析一下 Webpack 5 中都有哪些阶段使用到了缓存?</p>
<p>Webpack 构建效率优化的系列至此就告一段落了,下节课我们来介绍构建效率篇的最后一个主题:无包构建。</p>
</div>
</div>
<div>
<div style="float: left">
<a href="/专栏/前端工程化精讲-完/14 增量构建Webpack 中的增量构建.md.html">上一页</a>
</div>
<div style="float: right">
<a href="/专栏/前端工程化精讲-完/16 无包构建:盘点那些 No-bundle 的构建方案.md.html">下一页</a>
</div>
</div>
</div>
</div>
</div>
</div>
<a class="off-canvas-overlay" onclick="hide_canvas()"></a>
</div>
<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v652eace1692a40cfa3763df669d7439c1639079717194" integrity="sha512-Gi7xpJR8tSkrpF7aordPZQlW2DLtzUlZcumS8dMQjwDHEnw9I7ZLyiOj/6tZStRBGtGgN6ceN6cMH8z7etPGlw==" data-cf-beacon='{"rayId":"70997768a90a3cfa","version":"2021.12.0","r":1,"token":"1f5d475227ce4f0089a7cff1ab17c0f5","si":100}' crossorigin="anonymous"></script>
</body>
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-NPSEEVD756"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', 'G-NPSEEVD756');
var path = window.location.pathname
var cookie = getCookie("lastPath");
console.log(path)
if (path.replace("/", "") === "") {
if (cookie.replace("/", "") !== "") {
console.log(cookie)
document.getElementById("tip").innerHTML = "<a href='" + cookie + "'>跳转到上次进度</a>"
}
} else {
setCookie("lastPath", path)
}
function setCookie(cname, cvalue) {
var d = new Date();
d.setTime(d.getTime() + (180 * 24 * 60 * 60 * 1000));
var expires = "expires=" + d.toGMTString();
document.cookie = cname + "=" + cvalue + "; " + expires + ";path = /";
}
function getCookie(cname) {
var name = cname + "=";
var ca = document.cookie.split(';');
for (var i = 0; i < ca.length; i++) {
var c = ca[i].trim();
if (c.indexOf(name) === 0) return c.substring(name.length, c.length);
}
return "";
}
</script>
</html>

View File

@@ -1,920 +1,460 @@
<!DOCTYPE html>
<!-- saved from url=(0046)https://kaiiiz.github.io/hexo-theme-book-demo/ -->
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no">
<link rel="icon" href="/static/favicon.png">
<title>16 无包构建:盘点那些 No-bundle 的构建方案.md.html</title>
<!-- Spectre.css framework -->
<link rel="stylesheet" href="/static/index.css">
<!-- theme css & js -->
<meta name="generator" content="Hexo 4.2.0">
</head>
<body>
<div class="book-container">
<div class="book-sidebar">
<div class="book-brand">
<a href="/">
<img src="/static/favicon.png">
<span>技术文章摘抄</span>
</a>
</div>
<div class="book-menu uncollapsible">
<ul class="uncollapsible">
<li><a href="/" class="current-tab">首页</a></li>
</ul>
<ul class="uncollapsible">
<li><a href="../">上一级</a></li>
</ul>
<ul class="uncollapsible">
<li>
<a href="/专栏/前端工程化精讲-完/00 开篇词 建立上帝视角,全面系统掌握前端效率工程化.md.html">00 开篇词 建立上帝视角,全面系统掌握前端效率工程化.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/01 项目基石:前端脚手架工具探秘.md.html">01 项目基石:前端脚手架工具探秘.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/02 界面调试:热更新技术如何开着飞机修引擎?.md.html">02 界面调试:热更新技术如何开着飞机修引擎?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/03 构建提速:如何正确使用 SourceMap.md.html">03 构建提速:如何正确使用 SourceMap.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/04 接口调试Mock 工具如何快速进行接口调试?.md.html">04 接口调试Mock 工具如何快速进行接口调试?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/05 编码效率:如何提高编写代码的效率?.md.html">05 编码效率:如何提高编写代码的效率?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/06 团队工具:如何利用云开发提升团队开发效率?.md.html">06 团队工具:如何利用云开发提升团队开发效率?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/07 低代码工具:如何用更少的代码实现更灵活的需求.md.html">07 低代码工具:如何用更少的代码实现更灵活的需求.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/08 无代码工具:如何做到不写代码就能高效交付?.md.html">08 无代码工具:如何做到不写代码就能高效交付?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/09 构建总览:前端构建工具的演进.md.html">09 构建总览:前端构建工具的演进.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/10 流程分解Webpack 的完整构建流程.md.html">10 流程分解Webpack 的完整构建流程.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/11 编译提效:如何为 Webpack 编译阶段提速?.md.html">11 编译提效:如何为 Webpack 编译阶段提速?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/12 打包提效:如何为 Webpack 打包阶段提速?.md.html">12 打包提效:如何为 Webpack 打包阶段提速?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/13 缓存优化:那些基于缓存的优化方案.md.html">13 缓存优化:那些基于缓存的优化方案.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/14 增量构建Webpack 中的增量构建.md.html">14 增量构建Webpack 中的增量构建.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/15 版本特性Webpack 5 中的优化细节.md.html">15 版本特性Webpack 5 中的优化细节.md.html</a>
</li>
<li>
<a class="current-tab" href="/专栏/前端工程化精讲-完/16 无包构建:盘点那些 No-bundle 的构建方案.md.html">16 无包构建:盘点那些 No-bundle 的构建方案.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/17 部署初探:为什么一般不在开发环境下部署代码?.md.html">17 部署初探:为什么一般不在开发环境下部署代码?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/18 工具盘点:掌握那些流行的代码部署工具.md.html">18 工具盘点:掌握那些流行的代码部署工具.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/19 安装提效:部署流程中的依赖安装效率优化.md.html">19 安装提效:部署流程中的依赖安装效率优化.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/20 流程优化:部署流程中的构建流程策略优化.md.html">20 流程优化:部署流程中的构建流程策略优化.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/21 容器方案:从构建到部署,容器化方案的优势有哪些?.md.html">21 容器方案:从构建到部署,容器化方案的优势有哪些?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/22 案例分析:搭建基本的前端高效部署系统.md.html">22 案例分析:搭建基本的前端高效部署系统.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/23 结束语 前端效率工程化的未来展望.md.html">23 结束语 前端效率工程化的未来展望.md.html</a>
</li>
</ul>
</div>
</div>
<div class="sidebar-toggle" onclick="sidebar_toggle()" onmouseover="add_inner()" onmouseleave="remove_inner()">
<div class="sidebar-toggle-inner"></div>
</div>
<script>
function add_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.add('show')
}
function remove_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.remove('show')
}
function sidebar_toggle() {
let sidebar_toggle = document.querySelector('.sidebar-toggle')
let sidebar = document.querySelector('.book-sidebar')
let content = document.querySelector('.off-canvas-content')
if (sidebar_toggle.classList.contains('extend')) { // show
sidebar_toggle.classList.remove('extend')
sidebar.classList.remove('hide')
content.classList.remove('extend')
} else { // hide
sidebar_toggle.classList.add('extend')
sidebar.classList.add('hide')
content.classList.add('extend')
}
}
function open_sidebar() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.add('show')
overlay.classList.add('show')
}
function hide_canvas() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.remove('show')
overlay.classList.remove('show')
}
</script>
<div class="off-canvas-content">
<div class="columns">
<div class="column col-12 col-lg-12">
<div class="book-navbar">
<!-- For Responsive Layout -->
<header class="navbar">
<section class="navbar-section">
<a onclick="open_sidebar()">
<i class="icon icon-menu"></i>
</a>
</section>
</header>
</div>
<div class="book-content" style="max-width: 960px; margin: 0 auto;
overflow-x: auto;
overflow-y: hidden;">
<div class="book-post">
<p id="tip" align="center"></p>
<div><h1>16 无包构建:盘点那些 No-bundle 的构建方案</h1>
<p>上节课我们讨论了 Webpack 的最新版本 Webpack 5 所带来的提效新功能。思考题是 Webpack 5 中的持久化缓存究竟会影响哪些构建环节呢?</p>
<p>通过对 compiler.cache.hook.get 的追踪不难发现:持久化缓存一共影响下面这些环节与内置的插件:</p>
<ul>
<li>编译模块ResolverCachePlugin、Compilation/modules。</li>
<li>优化模块FlagDependencyExportsPlugin、ModuleConcatenationPlugin。</li>
<li>生成代码Compilation/codeGeneration、Compilation/assets。</li>
<li>优化产物TerserWebpackPlugin、RealContentHashPlugin。</li>
</ul>
<p>正是通过这样多环节的缓存读写控制,才打造出 Webpack 5 高效的持久化缓存功能。</p>
<p>在之前的课程里我们详细分解了 Webpack 构建工具的效率优化方案这节课我们来聊一聊今年比较火的另一种构建工具思路无包构建No-Bundle/Unbundle</p>
<h3>什么是无包构建</h3>
<p>什么是无包构建呢?这是一个与基于模块化打包的构建方案相对的概念。</p>
<p>在“ 第 9 课时|构建总览:前端构建工具的演进”中谈到过,目前主流的构建工具,例如 Webpack、Rollup 等都是基于一个或多个入口点模块,通过依赖分析将有依赖关系的模块<strong>打包到一起</strong>,最后形成少数几个产物代码包,因此这些工具也被称为<strong>打包工具</strong>。只不过,这些工具的构建过程除了打包外,还包括了模块编译和代码优化等,因此称为打包式构建工具或许更恰当。</p>
<p><strong>无包构建</strong>是指这样一类构建方式:在构建时只需处理模块的编译而<strong>无须打包</strong>,把模块间的**依赖关系完全交给浏览器来处理。**浏览器会加载入口模块,分析依赖后,再通过网络请求加载被依赖的模块。通过这样的方式简化构建时的处理过程,提升构建效率。</p>
<p>这种通过浏览器原生的模块进行解析的方式又称为 Native-ESMNative ES Module。下面我们就通过一个简单示例来展示这种基于浏览器的模块加载过程<a href="https://github.com/fe-efficiency/lessons_fe_efficiency/tree/master/16_nobundle">16_nobundle</a>/simple-esm如下面的代码和图片所示</p>
<pre><code>//./src/index.html
...
&lt;script type=&quot;module&quot; src=&quot;./modules/foo.js&quot;&gt;&lt;/script&gt;
...
//.src/modules/foo.js
import { bar } from './bar.js'
import { appendHTML } from './common.js'
...
import('https://cdn.jsdelivr.net/npm/<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="aac6c5cecbd9c287cfd9ea9e849b9d849b9f">[email&#160;protected]</a>/slice.js').then((module) =&gt; {...})
</code></pre>
<p><img src="assets/CgqCHl9yo46AYuszAANDvM6jRMk647.png" alt="Drawing 0.png" /></p>
<p>从示例中可以看到,在没有任何构建工具处理的情况下,在页面中引入带有 type=&quot;module&quot; 属性的 script浏览器就会在加载入口模块时依次加载了所有被依赖的模块。下面我们就来深入了解一下这种基于浏览器加载 JS 模块的技术的细节。</p>
<h3>基于浏览器的 JS 模块加载功能</h3>
<p>从 caniuse 网站中可以看到,目前大部分主流的浏览器都已支持 JavaScript modules 这一特性,如下图所示:</p>
<p><img src="assets/Ciqc1F9yo5aADhYKAAMTR4GJTG8708.png" alt="Drawing 1.png" /></p>
<p>[图片来源:<a href="https://caniuse.com/es6-module">https://caniuse.com/es6-module</a>]</p>
<p>我们来总结这种加载方式的注意点。</p>
<h4>HTML 中的 Script 引用</h4>
<ul>
<li>入口模块文件在页面中引用时需要带上**type=&quot;module&quot;**属性。对应的,存在 type=&quot;nomodule&quot;,即支持 ES Module 的现代浏览器,它会忽略 type=&quot;nomodule&quot; 属性的 script因此可以用作旧浏览器中的降级方案。</li>
<li>带有 type=&quot;module&quot; 属性的 script在浏览器中<strong>通过 defer 的方式异步执行</strong>(异步下载,不阻塞 HTML顺次执行即使是行内的 script 代码也遵循这一原则(而普通的行内 script 代码则忽略 defer 属性)。</li>
<li>带有 type=&quot;module&quot; 属性且带有<strong>async</strong>属性的 script在浏览器中<strong>通过 async 的方式异步执行</strong>(异步下载,不阻塞 HTML按该<strong>模块和所依赖的模块</strong>下载完成的先后顺序执行,无视 DOM 中的加载顺序),即使是行内的 script 代码,也遵循这一原则(而普通的行内 script 代码则忽略 async 属性)。</li>
<li>即使多次加载相同模块,也只会执行一次。</li>
</ul>
<h4>模块内依赖的引用</h4>
<ul>
<li>只能使用 import ... from '...' 的 ES6 风格的模块导入方式,或者使用 import(...).then(...) 的 ES6 动态导入方式,不支持其他模块化规范的引用方式(例如 require、define 等)。</li>
<li>导入的模块只支持使用相对路径('/xxx', './xxx', '../xxx')和 URL 方式('https://xxx', 'http://xxx')进行引用,不支持直接使用包名开头的方式('xxxx', 'xxx/xxx')。</li>
<li>只支持引用MIME Type为 text/javascript 方式的模块,不支持其他类型文件的加载(例如 CSS 等)。</li>
</ul>
<h4>为什么需要构建工具</h4>
<p>从上面的技术细节中我们会发现,对于一个普通的项目而言,要使用这种加载方案仍然有几个主要问题:</p>
<ol>
<li>许多其他类型的文件需要编译处理为 ES6 模块才能被浏览器正常加载JSX、Vue、TS、CSS、Image 等)。</li>
<li>许多第三方依赖包在通过第三方 URL 引用时,不仅过程烦琐,而且往往难以进行灵活的版本控制与更新,因此需要合适的方式来解决引用路径的问题。</li>
<li>对于现实中的项目开发而言,一些便利的辅助开发技术,例如热更新等还是需要由构建工具来提供。</li>
</ol>
<p>下面,我们分析 Vite 和 Snowpack 这两个有代表性的构建工具是如何解决上面的问题的。</p>
<h3>Vite</h3>
<p><a href="https://github.com/vitejs/vite">Vite</a> 是 Vue 框架的作者尤雨溪最新推出的基于 Native-ESM 的 Web 构建工具。它在开发环境下基于 Native-ESM 处理构建过程,只编译不打包,在生产环境下则基于 Rollup 打包。我们还是先通过 Vite 的官方示例来观察它的使用效果,如下面的代码和图片所示(示例代码参见 <a href="https://github.com/fe-efficiency/lessons_fe_efficiency/tree/master/16_nobundle/example-vite">example-vite</a></p>
<pre><code>npm init vite-app example-vite
cd example-vite
npm install
npm run dev
</code></pre>
<p><img src="assets/CgqCHl9yo-mAWrIzAAOaSZguuaM643.png" alt="Drawing 2.png" /></p>
<p>可以看到,运行示例代码后,在浏览器中只引入了 src/main.js 这一个入口模块,但是在网络面板中却依次加载了若干依赖模块,包括外部模块 vue 和 css。依赖图如下</p>
<p><img src="assets/Ciqc1F9yo_GAWATTAACYUvrJKL4148.png" alt="Drawing 4.png" /></p>
<p>可以看到,经过 Vite 处理后,浏览器中加载的模块与源代码中导入的模块相比发生了变化,这些变化包括对外部依赖包的处理,对 vue 文件的处理,对 css 文件的处理等。下面我们就来逐个分析其中的变化。</p>
<h4>对导入模块的解析</h4>
<p><strong>对 HTML 文件的预处理</strong></p>
<p>当启动 Vite 时,会通过 <a href="https://github.com/vitejs/vite/blob/master/src/node/server/serverPluginHtml.ts">serverPluginHtml</a>.ts 注入 <a href="https://github.com/vitejs/vite/blob/master/src/client/client.ts">/vite/client</a> 运行时的依赖模块,该模块用于处理热更新,以及提供更新 CSS 的方法 updateStyle。</p>
<p><strong>对外部依赖包的解析</strong></p>
<p>首先是对不带路径前缀的外部依赖包(也称为<strong>Bare Modules</strong>)的解析,例如上图中在示例源代码中导入了 'vue' 模块,但是在浏览器的网络请求中变为了请求 /@module/vue。</p>
<p>这个解析过程在 Vite 中主要通过三个文件来处理:</p>
<ul>
<li><a href="https://github.com/vitejs/vite/blob/master/src/node/resolver.ts#L464">resolver</a>.ts 负责找到对应在 node_modules 中的真实依赖包代码Vite 会在启动服务时对项目 package.json 中的 dependencies 做预处理读取并存入缓存目录 node_modules/.vite_opt_cache 中)。</li>
<li><a href="https://github.com/vitejs/vite/blob/master/src/node/server/serverPluginModuleRewrite.ts">serverPluginModuleRewrite</a>.ts 负责把源码中的 bare modules 加上 /@module/ 前缀。</li>
<li><a href="https://github.com/vitejs/vite/blob/master/src/node/server/serverPluginModuleResolve.ts">serverPluginModuleResolve</a>.ts 负责解析加上前缀后的模块。</li>
</ul>
<p><strong>对 Vue文件的解析</strong></p>
<p>对 Vue 文件的解析是通过 <a href="https://github.com/vitejs/vite/blob/master/src/node/server/serverPluginVue.ts">serverPluginVue</a>.ts 处理的,分离出 Vue 代码中的 script/template/style 代码片段,并分别转换为 JS 模块,然后将 template/style 模块的 import写到script 模块代码的头部。因此在浏览器访问时,一个 Vue 源代码文件会分裂为 2~3 的关联请求(例如上面的 /src/App.vue 和 /src/App.vue?type=template如果 App.vue 中包含<code> &lt;style&gt;</code> 则会产生第 3 个请求 /src/App.vue?type=style</p>
<p><strong>对 CSS 文件的解析</strong></p>
<p>对 CSS 文件的解析是通过 <a href="https://github.com/vitejs/vite/blob/master/src/node/server/serverPluginCss.ts">serverPluginCSS</a>.ts 处理的,解析过程主要是将 CSS 文件的内容转换为下面的 JS 代码模块,其中的 updateStyle 由注入 HTML 中的 /vite/client 模块提供,如下面的代码所示:</p>
<pre><code>import { updateStyle } from &quot;/vite/client&quot;
const css = &quot;...&quot;
updateStyle(&quot;\&quot;...\&quot;&quot;, css) // id, cssContent
export default css
</code></pre>
<p>以上就是示例代码中主要文件类型的基本解析逻辑可以看到Vite 正是通过这些解析器来解决不同类型文件以 JS 模块的方式在浏览器中加载的问题。在 Vite 源码中还包含了其他更多文件类型的解析器,例如 JSON、TS、SASS 等,这里就不一一列举了,感兴趣的话,你可以进一步查阅<a href="https://github.com/vitejs/vite">官方文档</a></p>
<h4>Vite 中的其他辅助功能</h4>
<p>除了提供这些解析器的能力外Vite 还提供了其他便捷的构建功能,大致整理如下:</p>
<ul>
<li><strong>多框架</strong>:除了在默认的 Vue 中使用外,还支持在 React 和 Preact 项目中使用。工具默认提供了 Vue、React 和 Preact 对应的脚手架模板。</li>
<li><strong>热更新HMR</strong>:默认提供的 3 种框架的脚手架模板中都内置了 HMR 功能,同时也提供了 HMR 的 API 供第三方插件或项目代码使用。</li>
<li><strong>自定义配置文件</strong>:支持使用自定义配置文件来细化构建配置,配置项功能参考 <a href="https://github.com/vuejs/vite/blob/master/src/node/config.ts">config.ts</a></li>
<li><strong>HTTPS 与 HTTP/2</strong>:支持使用 --https 启动参数来开启使用 HTTPS 和 HTTP/2 协议的开发服务器。</li>
<li><strong>服务代理</strong>:在自定义配置中支持配置代理,将部分请求代理到第三方服务。</li>
<li><strong>模式与环境变量</strong>:支持通过 mode 来指定构建模式为 development 或 production。相应模式下自动读取 dotenv 类型的环境变量配置文件(例如 .env.production.local</li>
<li><strong>生产环境打包</strong>:生产环境使用 Rollup 进行打包,支持传入自定义配置,配置项功能参考 <a href="https://github.com/vitejs/vite/blob/master/src/node/build/index.ts">build/index.ts</a></li>
</ul>
<h4>Vite 的使用限制</h4>
<p>Vite 的使用限制如下:</p>
<ul>
<li>面向支持 ES6 的现代浏览器,在生产环境下,编译目标参数 esBuildTarget 的默认值为 es2019最低支持版本为 es2015因为内部会使用 <a href="https://github.com/vitejs/vite/blob/master/src/node/esbuildService.ts">esbuild</a> 处理编译压缩,用来获得<a href="https://github.com/evanw/esbuild#why-is-it-fast">最快的构建速度</a>)。</li>
<li>对 Vue 框架的支持目前仅限于最新的 Vue 3 版本,不兼容更低版本。</li>
</ul>
<h3>Snowpack</h3>
<p>Snowpack 是另一个比较知名的无包构建工具,从整体功能来说和上述 Vite工具提供的功能大致相同主要差异点在 Snowpack 在生产环境下默认使用无包构建而非打包模式(可以通过引入打包插件例如 @snowpack/plugin-webpack 来实现打包模式),而 Vite 仅在开发模式下使用。示例代码参见 <a href="https://github.com/fe-efficiency/lessons_fe_efficiency/tree/master/16_nobundle/example-vite">example-snow</a>。下面我们简单整理下两者的异同。</p>
<h4>与 Vite 相同的功能点</h4>
<p>两者都支持各种代码转换加载器、热更新、环境变量(需要安装 dotenv 插件、服务代理、HTTPS 与 HTTP/2 等。</p>
<h4>与 Vite 的差异点</h4>
<ul>
<li><strong>相同的功能,实现细节不同</strong>:例如对 Bare Module 的处理除了转换后前缀名称不同外Vite 使用 /@module/ 前缀,而 Snowpack 使用 /web_modules/ 前缀)Vite 支持类似 &quot;AAA/BBB&quot; 类型的子模块引用方式,而 Snowpack 目前尚不支持。</li>
<li><strong>工具稳定性</strong>截止写稿的时间点2020 年 9 月 21 日Vite 的最新版本为 v1.0.0-rc4仍未发布第一个稳定版本。而 Snowpack 自年初发布第一个稳定版本以来,已经更新到了 v2.11.1 版本。</li>
<li><strong>插件体系</strong>除了版本差异外Snowpack 提供了较完善的插件体系,支持用户和社区发布自定义插件,而 Vite 虽然也内置了许多插件,但目前并没有提供自定义插件的相关文档。</li>
<li><strong>打包工具</strong>在生产环境下Vite 使用 Rollup 作为打包工具,而 Snowpack 则需要引入插件来实现打包功能,官方支持的打包插件有 @snowpack/plugin-webpack 和 @snowpack/plugin-parcel暂未提供 Rollup 对应的插件。</li>
<li><strong>特殊优化</strong>Vite 中内置了对 Vue 的大量构建优化,因此对 Vue 项目而言,选择 Vite 通常可以获得更好的开发体验。</li>
</ul>
<h3>无包构建与打包构建</h3>
<p>通过上面的 Vite 等无包构建工具的功能介绍可以发现,同 Webpack 等主流打包构建工具相比,无包构建流程的优缺点都十分明显。</p>
<h4>无包构建的优点</h4>
<p>无包构建的最大优势在于<strong>构建速度快</strong>,尤其是启动服务的<strong>初次构建速度</strong>要比目前主流的打包构建工具要快很多,原因如下:</p>
<ul>
<li><strong>初次构建启动快</strong>:打包构建流程在初次启动时需要进行一系列的模块依赖分析与编译,而在无包构建流程中,这些工作都是<strong>在浏览器渲染页面时异步处理的</strong>,启动服务时只需要做少量的优化处理即可(例如缓存项目依赖的 Bare Modules所以启动非常快。</li>
<li><strong>按需编译</strong>:在打包构建流程中,启动服务时即需要完整编译打包所有模块,而无包构建流程是在浏览器渲染时,根据入口模块分析加载所需模块,编译过程按需处理,因此相比之下处理内容更少,速度也会更快</li>
<li><strong>增量构建速度快</strong>:在修改代码后的 rebuild 过程中,主流的打包构建中仍然包含编译被修改的模块和打包产物这两个主要流程,因此相比之下,只需处理编译单个模块的无包构建在速度上也会更胜一筹(尽管在打包构建工具中,也可以通过分包等方式尽可能地减少两者的差距)。</li>
</ul>
<h4>无包构建的缺点</h4>
<ul>
<li><strong>浏览器网络请求数量剧增</strong>:无包构建最主要面对的问题是,它的运行模式决定了在一般项目里,渲染页面所需发起的请求数远比打包构建要多得多,使得打开页面会产生瀑布式的大量网络请求,将对页面的渲染造成延迟。这对于服务稳定性和访问性能要求更高的生产环境而言,通常是不太能接受的,尤其对不支持 HTTP/2 的服务器而言,这种处理更是灾难性的。因此,一般是在开发环境下才使用无包构建,在生产环境下则仍旧使用打包构建。</li>
<li><strong>浏览器的兼容性</strong>:无包构建要求浏览器支持 JavaScript module 特性,尽管目前的主流浏览器已大多支持,但是对于需要兼容旧浏览器的项目而言,仍然不可能在生产环境下使用。而在开发环境下则通常没有这种顾虑。</li>
</ul>
<h3>总结</h3>
<p>这节课我们主要讨论了今年比较热门的无包构建。</p>
<p>无包构建产生的基础是浏览器对 JS 模块加载的支持,这样才可能把构建过程中分析模块依赖关系并打包的过程变为在浏览器中逐个加载引用的模块。但是这种加载模块的方式在实际项目应用场景下还存在一些阻碍,于是有了无包构建工具。</p>
<p>在这些工具里,我们主要介绍了 Vite 和 Snowpack希望通过介绍他们的开发模式的基本工作流程和差异点让你对这类工具的功能特点有一个基本的了解。</p>
<p>今天的课后思考题是,为什么 Vite/Snowpack 这样的无包构建工具要比 Webpack 这样的打包构建工具速度更快呢?</p>
<p>随着这节课的结束,构建优化模块也就告一段落了。下节课开始我们将进入部署优化模块。</p>
</div>
</div>
<div>
<div style="float: left">
<a href="/专栏/前端工程化精讲-完/15 版本特性Webpack 5 中的优化细节.md.html">上一页</a>
</div>
<div style="float: right">
<a href="/专栏/前端工程化精讲-完/17 部署初探:为什么一般不在开发环境下部署代码?.md.html">下一页</a>
</div>
</div>
</div>
</div>
</div>
</div>
<a class="off-canvas-overlay" onclick="hide_canvas()"></a>
</div>
<script data-cfasync="false" src="/cdn-cgi/scripts/5c5dd728/cloudflare-static/email-decode.min.js"></script><script defer src="https://static.cloudflareinsights.com/beacon.min.js/v652eace1692a40cfa3763df669d7439c1639079717194" integrity="sha512-Gi7xpJR8tSkrpF7aordPZQlW2DLtzUlZcumS8dMQjwDHEnw9I7ZLyiOj/6tZStRBGtGgN6ceN6cMH8z7etPGlw==" data-cf-beacon='{"rayId":"7099776b0e6c3cfa","version":"2021.12.0","r":1,"token":"1f5d475227ce4f0089a7cff1ab17c0f5","si":100}' crossorigin="anonymous"></script>
</body>
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-NPSEEVD756"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', 'G-NPSEEVD756');
var path = window.location.pathname
var cookie = getCookie("lastPath");
console.log(path)
if (path.replace("/", "") === "") {
if (cookie.replace("/", "") !== "") {
console.log(cookie)
document.getElementById("tip").innerHTML = "<a href='" + cookie + "'>跳转到上次进度</a>"
}
} else {
setCookie("lastPath", path)
}
function setCookie(cname, cvalue) {
var d = new Date();
d.setTime(d.getTime() + (180 * 24 * 60 * 60 * 1000));
var expires = "expires=" + d.toGMTString();
document.cookie = cname + "=" + cvalue + "; " + expires + ";path = /";
}
function getCookie(cname) {
var name = cname + "=";
var ca = document.cookie.split(';');
for (var i = 0; i < ca.length; i++) {
var c = ca[i].trim();
if (c.indexOf(name) === 0) return c.substring(name.length, c.length);
}
return "";
}
</script>
</html>

View File

@@ -1,744 +1,372 @@
<!DOCTYPE html>
<!-- saved from url=(0046)https://kaiiiz.github.io/hexo-theme-book-demo/ -->
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no">
<link rel="icon" href="/static/favicon.png">
<title>17 部署初探:为什么一般不在开发环境下部署代码?.md.html</title>
<!-- Spectre.css framework -->
<link rel="stylesheet" href="/static/index.css">
<!-- theme css & js -->
<meta name="generator" content="Hexo 4.2.0">
</head>
<body>
<div class="book-container">
<div class="book-sidebar">
<div class="book-brand">
<a href="/">
<img src="/static/favicon.png">
<span>技术文章摘抄</span>
</a>
</div>
<div class="book-menu uncollapsible">
<ul class="uncollapsible">
<li><a href="/" class="current-tab">首页</a></li>
</ul>
<ul class="uncollapsible">
<li><a href="../">上一级</a></li>
</ul>
<ul class="uncollapsible">
<li>
<a href="/专栏/前端工程化精讲-完/00 开篇词 建立上帝视角,全面系统掌握前端效率工程化.md.html">00 开篇词 建立上帝视角,全面系统掌握前端效率工程化.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/01 项目基石:前端脚手架工具探秘.md.html">01 项目基石:前端脚手架工具探秘.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/02 界面调试:热更新技术如何开着飞机修引擎?.md.html">02 界面调试:热更新技术如何开着飞机修引擎?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/03 构建提速:如何正确使用 SourceMap.md.html">03 构建提速:如何正确使用 SourceMap.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/04 接口调试Mock 工具如何快速进行接口调试?.md.html">04 接口调试Mock 工具如何快速进行接口调试?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/05 编码效率:如何提高编写代码的效率?.md.html">05 编码效率:如何提高编写代码的效率?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/06 团队工具:如何利用云开发提升团队开发效率?.md.html">06 团队工具:如何利用云开发提升团队开发效率?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/07 低代码工具:如何用更少的代码实现更灵活的需求.md.html">07 低代码工具:如何用更少的代码实现更灵活的需求.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/08 无代码工具:如何做到不写代码就能高效交付?.md.html">08 无代码工具:如何做到不写代码就能高效交付?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/09 构建总览:前端构建工具的演进.md.html">09 构建总览:前端构建工具的演进.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/10 流程分解Webpack 的完整构建流程.md.html">10 流程分解Webpack 的完整构建流程.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/11 编译提效:如何为 Webpack 编译阶段提速?.md.html">11 编译提效:如何为 Webpack 编译阶段提速?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/12 打包提效:如何为 Webpack 打包阶段提速?.md.html">12 打包提效:如何为 Webpack 打包阶段提速?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/13 缓存优化:那些基于缓存的优化方案.md.html">13 缓存优化:那些基于缓存的优化方案.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/14 增量构建Webpack 中的增量构建.md.html">14 增量构建Webpack 中的增量构建.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/15 版本特性Webpack 5 中的优化细节.md.html">15 版本特性Webpack 5 中的优化细节.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/16 无包构建:盘点那些 No-bundle 的构建方案.md.html">16 无包构建:盘点那些 No-bundle 的构建方案.md.html</a>
</li>
<li>
<a class="current-tab" href="/专栏/前端工程化精讲-完/17 部署初探:为什么一般不在开发环境下部署代码?.md.html">17 部署初探:为什么一般不在开发环境下部署代码?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/18 工具盘点:掌握那些流行的代码部署工具.md.html">18 工具盘点:掌握那些流行的代码部署工具.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/19 安装提效:部署流程中的依赖安装效率优化.md.html">19 安装提效:部署流程中的依赖安装效率优化.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/20 流程优化:部署流程中的构建流程策略优化.md.html">20 流程优化:部署流程中的构建流程策略优化.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/21 容器方案:从构建到部署,容器化方案的优势有哪些?.md.html">21 容器方案:从构建到部署,容器化方案的优势有哪些?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/22 案例分析:搭建基本的前端高效部署系统.md.html">22 案例分析:搭建基本的前端高效部署系统.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/23 结束语 前端效率工程化的未来展望.md.html">23 结束语 前端效率工程化的未来展望.md.html</a>
</li>
</ul>
</div>
</div>
<div class="sidebar-toggle" onclick="sidebar_toggle()" onmouseover="add_inner()" onmouseleave="remove_inner()">
<div class="sidebar-toggle-inner"></div>
</div>
<script>
function add_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.add('show')
}
function remove_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.remove('show')
}
function sidebar_toggle() {
let sidebar_toggle = document.querySelector('.sidebar-toggle')
let sidebar = document.querySelector('.book-sidebar')
let content = document.querySelector('.off-canvas-content')
if (sidebar_toggle.classList.contains('extend')) { // show
sidebar_toggle.classList.remove('extend')
sidebar.classList.remove('hide')
content.classList.remove('extend')
} else { // hide
sidebar_toggle.classList.add('extend')
sidebar.classList.add('hide')
content.classList.add('extend')
}
}
function open_sidebar() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.add('show')
overlay.classList.add('show')
}
function hide_canvas() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.remove('show')
overlay.classList.remove('show')
}
</script>
<div class="off-canvas-content">
<div class="columns">
<div class="column col-12 col-lg-12">
<div class="book-navbar">
<!-- For Responsive Layout -->
<header class="navbar">
<section class="navbar-section">
<a onclick="open_sidebar()">
<i class="icon icon-menu"></i>
</a>
</section>
</header>
</div>
<div class="book-content" style="max-width: 960px; margin: 0 auto;
overflow-x: auto;
overflow-y: hidden;">
<div class="book-post">
<p id="tip" align="center"></p>
<div><h1>17 部署初探:为什么一般不在开发环境下部署代码?</h1>
<p>这节课我们将进入前端效率工程化的第三个模块——部署效率篇。本模块主要讨论两个方面的问题:第一个是在前端项目的构建部署流程里,除了使用构建工具执行构建之外,还有哪些因素会影响整个部署流程的工作效率?第二个是在部署系统中进行项目构建时,又会面临哪些和环境相关的问题和优化方案?</p>
<p>这节课我们先来讨论为什么要使用部署系统,而不是在本地环境下部署代码?</p>
<p>在分析这个问题之前,我们先对前端项目的部署流程进行界定。</p>
<h3>前端项目的一般部署流程</h3>
<p>在前端项目中,通常可以把在一个全新环境下的代码部署过程分为以下几个环节:</p>
<ol>
<li><strong>获取代码</strong>:从代码仓库获取项目代码,并切换到待部署的分支或版本。</li>
<li><strong>安装依赖</strong>:安装项目构建所需要的依赖包。</li>
<li><strong>源码构建</strong>:使用构建工具对项目源代码进行构建,生成产物代码。</li>
<li><strong>产物打包</strong>:将部署所需的代码(通常指的是构建后的产物代码,如果是部署 Node 服务则还需要其他目录与文件)打成压缩包。</li>
<li><strong>推送代码</strong>:将待部署的文件或压缩包推送至目标服务器的特定目录下,如果是推送压缩包的情况,还需执行解压。</li>
<li><strong>重启服务</strong>:在部署 Node 服务的情况下,在代码推送后需要进行服务重启。</li>
</ol>
<h3>本地部署相比部署系统的优势</h3>
<p>对于使用部署系统的项目而言,除了重启服务这一步骤在普通静态服务部署中不需要执行外,上述其他环节通常是每次构建都需要经历的。</p>
<p>而如果使用本地开发环境进行部署,则可以根据情况<strong>对前两个环节进行简化</strong></p>
<ol>
<li>在获取代码的环节中,本地开发环境已经包含了项目的本地代码,同拉取完整的代码仓库相比,直接获取更新内容并切换分支或版本的处理要更快一些。</li>
<li>在安装依赖的环节中,本地开发环境通常已包含了构建所需的最新依赖包,即使切换到待部署版本后发现依赖版本有变更,更新依赖包的时间也比在空目录下完整安装依赖包的时间更短。</li>
</ol>
<p>此外,本地部署还有另外两点优势是使用部署系统所不具备的:</p>
<ol>
<li>增量构建:我们之前分析过增量构建的实现原理。在构建配置与项目依赖不发生变化的情况下,理论上,本地部署可以让构建进程长时间地驻留,以达到增量构建的效果。</li>
<li>快速调试:本地部署时,构建过程会直接在本地进行,因此有任何构建问题时可以第一时间发现并处理。相比之下,远程的部署系统则需要将一定的时间消耗在链路反馈和本地环境切换上。</li>
</ol>
<p>因此,如果单从上面的部署环节来看,本地部署的效率一般优于部署系统,那么为什么在企业中通常不建议这样做呢?</p>
<h3>本地部署的劣势</h3>
<p>同远程部署系统相比,不管从安全性还是人员效率上看,本地部署都存在诸多问题:</p>
<h4>流程安全风险</h4>
<p><strong>环境一致性</strong></p>
<p>本地部署的第一个问题在于无法保证环境的一致性:</p>
<ul>
<li>同一个项目不同开发人员的本地环境操作系统、NodeJS 版本等)都可能存在差异。</li>
<li>由于 NodeJS <a href="https://docs.npmjs.com/about-semantic-versioning">语义化版本</a>Semantic Version在安装时自动升级的问题不同开发人员的本地 node_modules 中的依赖包版本也可能存在差异。</li>
<li>开发人员的本地环境和部署代码的目标服务器环境之间也可能存在差异。</li>
</ul>
<p>这些差异会导致项目代码的稳定性无法得到保障。例如对于一个 Node 项目而言,在一个 NodeJS 低版本环境下构建的产物,在 Node 高版本环境下就有可能启动异常。</p>
<p>因此,如果项目都由开发人员各自在本地部署,无疑会降低项目的稳定性,增加部署风险。</p>
<p>而使用远程统一的部署系统,一方面避免了不同开发人员的本地环境差异性,另一方面,部署系统的工作环境也可以与线上服务环境保持一致,从而降低环境不一致的风险。</p>
<p><strong>过程一致性</strong>
同环境一致性的问题相似,本地部署的第二个问题是无法保证部署过程的一致性。所谓过程的一致性,就是尽可能地让每次部署的流程顺序、各环节的处理过程都保持一致,从而打造规范化的部署流程。本地部署依赖人工操作,这就可能因为操作中的疏漏,导致过程一致性无法得到保障。尽管可以通过将部署流程写入脚本等方式减少人工误操作的风险,但是这和通过部署系统将完整处理过程写入代码的方式相比,仍然不够安全可靠。同时,系统可以记录每次部署操作的细节日志,便于当出现问题时快速解决。</p>
<h4>工作效率问题</h4>
<p><strong>可回溯性</strong>
可回溯性的问题可以从日志和产物两方面来看。</p>
<ul>
<li><strong>日志</strong>:在部署过程中我们可能遇到各种问题,例如构建失败、单元测试执行失败、推送代码失败、部署后启动服务失败等。遇到这些问题时,需要有相应的日志来帮助定位。尽管在本地部署执行时也会输出日志,但是这些日志是临时的,查阅不便,且本地部署的日志至多只能保留当前一次的处理日志,如果希望对历史部署过程进行查看分析,更不能使用这种方式。</li>
<li><strong>产物</strong>:通常,部署系统中会留存最近几次部署的构建产物包,以便当部署后的代码存在问题时能够快速回滚发布。而本地部署在项目的开发目录下执行,因此通常只会保留最近一次的构建产物,这就阻碍了上述快速回滚的实现。</li>
</ul>
<p>相对的,一个规范化的部署系统,则可以记录和留存每一次部署操作的细节日志,以及保留最近若干次的部署代码包,因此在可回溯性上又胜一筹。</p>
<p><strong>人员分工</strong>
工作效率的第二个问题是人员分工问题,这个问题又可以从以下几个侧面来分析:</p>
<p>首先部署过程需要耗费时间。在本地部署当前项目的某一个分支时,无法同时对该项目进行继续开发,往往只能中断当前的工作,等待部署完成。</p>
<p>在这个前提下,一个项目中的多名开发人员如果各自在电脑中<strong>进行部署</strong>,无疑增大了上述流程安全的风险系数。但反过来,如果一个项目里只有<strong>个别开发者的本地环境拥有部署权限</strong>,则所有人的部署需求都会堆积到一起,大大增加对有权限的开发者的工作时间的占用。如果不能及时响应处理,也会延误其他人的后续工作。</p>
<p>此外由于分工角色的不同,在许多情况下,部署流程会主动由测试人员而非开发人员发起。当部署在开发人员的本地环境中进行时,会像上面多人开发集中部署那样彼此影响,也增加了相应的沟通成本。</p>
<p><strong>CI/CD</strong>
<a href="https://en.wikipedia.org/wiki/Continuous_integration">持续集成</a>Continuous IntegrationCI<a href="https://en.wikipedia.org/wiki/Continuous_delivery">持续交付</a>Continuous DeliveryCD是软件生产领域提升迭代效率的一种工作方式开发人员提交代码后由 CI/CD 系统自动化地执行合并、构建、测试和部署等一系列管道化Pipeline的流程从而尽早发现和反馈代码问题以小步快跑的方式加速软件的版本迭代过程。</p>
<p>这个过程通常是各系统(版本管理系统、构建系统、部署系统等)以自动化的方式协同完成的。而本地部署依赖人工操作,所以并不支持这种自动化的处理过程。</p>
<h3>总结</h3>
<p>作为部署优化的开篇,这节课我们主要讨论了相比远程部署系统,本地部署的优缺点:尽管本地部署有着流程简化、快速调试等优点,但是相对应的也带来了流程安全风险和人员效率下降等问题。因此一般在规范化的企业技术研发流程中,通常都不使用本地人工操作这样的部署方式。</p>
<p>本节课的思考题是:回顾一下本地部署同远程部署系统相比有哪些优势呢?关于这两者的差异,还可以从其他维度进行分析吗?如果有的话,你可以在留言区回复,期待你的想法。</p>
</div>
</div>
<div>
<div style="float: left">
<a href="/专栏/前端工程化精讲-完/16 无包构建:盘点那些 No-bundle 的构建方案.md.html">上一页</a>
</div>
<div style="float: right">
<a href="/专栏/前端工程化精讲-完/18 工具盘点:掌握那些流行的代码部署工具.md.html">下一页</a>
</div>
</div>
</div>
</div>
</div>
</div>
<a class="off-canvas-overlay" onclick="hide_canvas()"></a>
</div>
<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v652eace1692a40cfa3763df669d7439c1639079717194" integrity="sha512-Gi7xpJR8tSkrpF7aordPZQlW2DLtzUlZcumS8dMQjwDHEnw9I7ZLyiOj/6tZStRBGtGgN6ceN6cMH8z7etPGlw==" data-cf-beacon='{"rayId":"7099776d5bba3cfa","version":"2021.12.0","r":1,"token":"1f5d475227ce4f0089a7cff1ab17c0f5","si":100}' crossorigin="anonymous"></script>
</body>
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-NPSEEVD756"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', 'G-NPSEEVD756');
var path = window.location.pathname
var cookie = getCookie("lastPath");
console.log(path)
if (path.replace("/", "") === "") {
if (cookie.replace("/", "") !== "") {
console.log(cookie)
document.getElementById("tip").innerHTML = "<a href='" + cookie + "'>跳转到上次进度</a>"
}
} else {
setCookie("lastPath", path)
}
function setCookie(cname, cvalue) {
var d = new Date();
d.setTime(d.getTime() + (180 * 24 * 60 * 60 * 1000));
var expires = "expires=" + d.toGMTString();
document.cookie = cname + "=" + cvalue + "; " + expires + ";path = /";
}
function getCookie(cname) {
var name = cname + "=";
var ca = document.cookie.split(';');
for (var i = 0; i < ca.length; i++) {
var c = ca[i].trim();
if (c.indexOf(name) === 0) return c.substring(name.length, c.length);
}
return "";
}
</script>
</html>

View File

@@ -1,758 +1,379 @@
<!DOCTYPE html>
<!-- saved from url=(0046)https://kaiiiz.github.io/hexo-theme-book-demo/ -->
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no">
<link rel="icon" href="/static/favicon.png">
<title>18 工具盘点:掌握那些流行的代码部署工具.md.html</title>
<!-- Spectre.css framework -->
<link rel="stylesheet" href="/static/index.css">
<!-- theme css & js -->
<meta name="generator" content="Hexo 4.2.0">
</head>
<body>
<div class="book-container">
<div class="book-sidebar">
<div class="book-brand">
<a href="/">
<img src="/static/favicon.png">
<span>技术文章摘抄</span>
</a>
</div>
<div class="book-menu uncollapsible">
<ul class="uncollapsible">
<li><a href="/" class="current-tab">首页</a></li>
</ul>
<ul class="uncollapsible">
<li><a href="../">上一级</a></li>
</ul>
<ul class="uncollapsible">
<li>
<a href="/专栏/前端工程化精讲-完/00 开篇词 建立上帝视角,全面系统掌握前端效率工程化.md.html">00 开篇词 建立上帝视角,全面系统掌握前端效率工程化.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/01 项目基石:前端脚手架工具探秘.md.html">01 项目基石:前端脚手架工具探秘.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/02 界面调试:热更新技术如何开着飞机修引擎?.md.html">02 界面调试:热更新技术如何开着飞机修引擎?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/03 构建提速:如何正确使用 SourceMap.md.html">03 构建提速:如何正确使用 SourceMap.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/04 接口调试Mock 工具如何快速进行接口调试?.md.html">04 接口调试Mock 工具如何快速进行接口调试?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/05 编码效率:如何提高编写代码的效率?.md.html">05 编码效率:如何提高编写代码的效率?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/06 团队工具:如何利用云开发提升团队开发效率?.md.html">06 团队工具:如何利用云开发提升团队开发效率?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/07 低代码工具:如何用更少的代码实现更灵活的需求.md.html">07 低代码工具:如何用更少的代码实现更灵活的需求.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/08 无代码工具:如何做到不写代码就能高效交付?.md.html">08 无代码工具:如何做到不写代码就能高效交付?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/09 构建总览:前端构建工具的演进.md.html">09 构建总览:前端构建工具的演进.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/10 流程分解Webpack 的完整构建流程.md.html">10 流程分解Webpack 的完整构建流程.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/11 编译提效:如何为 Webpack 编译阶段提速?.md.html">11 编译提效:如何为 Webpack 编译阶段提速?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/12 打包提效:如何为 Webpack 打包阶段提速?.md.html">12 打包提效:如何为 Webpack 打包阶段提速?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/13 缓存优化:那些基于缓存的优化方案.md.html">13 缓存优化:那些基于缓存的优化方案.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/14 增量构建Webpack 中的增量构建.md.html">14 增量构建Webpack 中的增量构建.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/15 版本特性Webpack 5 中的优化细节.md.html">15 版本特性Webpack 5 中的优化细节.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/16 无包构建:盘点那些 No-bundle 的构建方案.md.html">16 无包构建:盘点那些 No-bundle 的构建方案.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/17 部署初探:为什么一般不在开发环境下部署代码?.md.html">17 部署初探:为什么一般不在开发环境下部署代码?.md.html</a>
</li>
<li>
<a class="current-tab" href="/专栏/前端工程化精讲-完/18 工具盘点:掌握那些流行的代码部署工具.md.html">18 工具盘点:掌握那些流行的代码部署工具.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/19 安装提效:部署流程中的依赖安装效率优化.md.html">19 安装提效:部署流程中的依赖安装效率优化.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/20 流程优化:部署流程中的构建流程策略优化.md.html">20 流程优化:部署流程中的构建流程策略优化.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/21 容器方案:从构建到部署,容器化方案的优势有哪些?.md.html">21 容器方案:从构建到部署,容器化方案的优势有哪些?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/22 案例分析:搭建基本的前端高效部署系统.md.html">22 案例分析:搭建基本的前端高效部署系统.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/23 结束语 前端效率工程化的未来展望.md.html">23 结束语 前端效率工程化的未来展望.md.html</a>
</li>
</ul>
</div>
</div>
<div class="sidebar-toggle" onclick="sidebar_toggle()" onmouseover="add_inner()" onmouseleave="remove_inner()">
<div class="sidebar-toggle-inner"></div>
</div>
<script>
function add_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.add('show')
}
function remove_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.remove('show')
}
function sidebar_toggle() {
let sidebar_toggle = document.querySelector('.sidebar-toggle')
let sidebar = document.querySelector('.book-sidebar')
let content = document.querySelector('.off-canvas-content')
if (sidebar_toggle.classList.contains('extend')) { // show
sidebar_toggle.classList.remove('extend')
sidebar.classList.remove('hide')
content.classList.remove('extend')
} else { // hide
sidebar_toggle.classList.add('extend')
sidebar.classList.add('hide')
content.classList.add('extend')
}
}
function open_sidebar() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.add('show')
overlay.classList.add('show')
}
function hide_canvas() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.remove('show')
overlay.classList.remove('show')
}
</script>
<div class="off-canvas-content">
<div class="columns">
<div class="column col-12 col-lg-12">
<div class="book-navbar">
<!-- For Responsive Layout -->
<header class="navbar">
<section class="navbar-section">
<a onclick="open_sidebar()">
<i class="icon icon-menu"></i>
</a>
</section>
</header>
</div>
<div class="book-content" style="max-width: 960px; margin: 0 auto;
overflow-x: auto;
overflow-y: hidden;">
<div class="book-post">
<p id="tip" align="center"></p>
<div><h1>18 工具盘点:掌握那些流行的代码部署工具</h1>
<p>上节课我们通过分析“为什么不在本地环境进行部署”这个问题,来对比部署系统的重要性:一个优秀的部署系统,能够自动化地完整部署流程的各环节,无须占用开发人员的时间与精力,同时又能保证环境与过程的一致性,增强流程的稳定性,降低外部因素导致的风险。此外,部署系统还可以提供过程日志、历史版本构建包、通知邮件等各类辅助功能模块,来打造更完善的部署工作流程。</p>
<p>这节课我就来为你介绍在企业项目和开源项目中被广泛使用的几个典型部署工具,包括 Jenkins、CircleCI、Github Actions、Gitlab CI。</p>
<h3>Jenkins</h3>
<p><img src="assets/Ciqc1F-AFmqADoTDAADSwJG8TWA117.png" alt="Drawing 0.png" /></p>
<p>Jenkins Logo</p>
<p><a href="https://www.jenkins.io/">Jenkins</a> 是诞生较早且使用广泛的开源持续集成工具。早在 2004 年Sun 公司就推出了它的前身 Husdon它在 2011 年更名为 Jenkins。下面介绍它的功能特点。</p>
<h4>功能特点</h4>
<ul>
<li><strong>搭建方式</strong>Jenkins 是一款基于 Java 的应用程序,官方提供了 Linux、Mac 和 Windows 等各系统下的搭建方式,同时也提供了基于 Docker 的容器化搭建方式。此外Jenkins 支持分布式的服务方式,各任务可以在不同的节点服务器上运行。</li>
<li><strong>收费方式</strong>Jenkins 是完全免费的开源产品。</li>
<li><strong>多类型 Job</strong>Job 是 Jenkins 中的基本工作单元。它可以是一个项目的构建部署流程也可以是其他类型例如流水线Pipeline。在 Jenkins 中支持各种类型的 Job自定义项目、流水线、文件夹、多配置项目、Github 组织等。</li>
<li><strong>插件系统</strong>Jenkins 架构中内置的插件系统为它提供了极强的功能扩展性。目前 Jenkins 社区中共有超过<a href="https://plugins.jenkins.io/">1500 个插件</a>,功能涵盖了继续继承和部署的各个环节。</li>
<li><strong>Job 配置</strong>:得益于其插件系统,在 Jenkins 的 Job 配置中可以灵活定制各种复杂的构建与部署选项,例如构建远程触发、构建参数化选项、关联 Jira、执行 Windows 批处理、邮件通知等。</li>
<li><strong>API 调用</strong>Jenkins 提供了 Restful 的 API 接口,可用于外部调用控制节点、任务、配置、构建等处理过程。</li>
</ul>
<p><img src="assets/Ciqc1F-AFoiAbc1YAAIiZzD3poU694.png" alt="Drawing 1.png" /></p>
<p>Jenkins 中 Job 的基本配置界面</p>
<h3>CircleCI</h3>
<p><img src="assets/Ciqc1F-AFpGAfFjTAAAXBcXm7AQ619.png" alt="Drawing 2.png" /></p>
<p>CircleCI Logo</p>
<p><a href="https://circleci.com/product/">CircleCI</a> 是一款基于云端的持续集成服务,下面介绍它的功能特点。</p>
<h4>功能特点</h4>
<ul>
<li><strong>云端服务</strong>:由于 CircleCI 是一款基于云端的持续集成服务,因此无须搭建和管理即可直接使用。同时也提供了收费的本地化搭建服务方式。</li>
<li><strong>收费方式</strong>CircleCI 的云端服务分为免费与收费两种,免费版本一个账号只能同时运行一个 Job同时对使用数据量、构建环境等有一定限制。而收费版本则提供了更多的并发构建数、更多的环境、更快的性能等。此外如第一点所述企业内部使用的本地化搭建服务方式也是收费的。</li>
<li><strong>缓存优化</strong>CircleCI 的任务构建是基于容器化的,因此能够缓存依赖安装的数据,从而加速构建流程。</li>
<li><strong>SSH 调试</strong>:它提供了基于 SSH 访问构建容器的功能,便于在构建错误时快速地进入容器内进行调试。</li>
<li><strong>配置简化</strong>:在 CircleCI 中提供了开箱即用的用户体验,只需要少量配置即可快速开始构建项目。</li>
<li><strong>API 调用</strong>CircleCI 中也提供了 Restfull 的 API 接口,可用于访问项目、构建和产物。</li>
</ul>
<p><img src="assets/Ciqc1F-AFqKAU29WAARwRnLOcKU376.png" alt="Drawing 3.png" /></p>
<p>CircleCI 项目流水线示例界面</p>
<h3>Github Actions</h3>
<p><img src="assets/Ciqc1F-AFquAK06qAAATdguATCs007.png" alt="Drawing 4.png" /></p>
<p>Github Actions Logo</p>
<p><a href="https://github.com/features/actions">Github Actions</a>GHA是 Github 官方提供的 CI/CD 流程工具,用于为 Github 中的开源项目提供简单易用的持续集成工作流能力。</p>
<h4>功能特点</h4>
<ul>
<li><strong>多系统</strong>:提供 Linux、Mac、Windows 等各主流操作系统环境下的运行能力,同时也支持在容器中运行。</li>
<li><strong>矩阵运行</strong>:支持同时在多个操作系统或不同环境下(例如不同 NodeJS 版本的环境中)运行构建和测试流程。</li>
<li><strong>多语言</strong>:支持 NodeJS、JAVA、PHP、Python、Go、Rust 等各种编程语言的工作流程。</li>
<li><strong>多容器测试</strong>:支持直接使用 Docker-Compose 进行多容器关联的测试(而 CircleCI 中则需要先执行安装才能使用)。</li>
<li><strong>社区支持</strong>Github 社区中提供了众多工作流的模板可供选择使用,例如构建并发布 npm 包、构建并提交到 Docker Hub 等。</li>
<li><strong>费用情况</strong>Github Action 对于公开的仓库,以及在自运维执行器的情况下是免费的。而对于私有仓库则提供一定额度的免费执行时间和免费存储空间,超出部分则需要收费。</li>
</ul>
<p><img src="assets/CgqCHl-AFrqAZddtAAJj5zKVbrY255.png" alt="Drawing 5.png" /></p>
<p>Github Actions 的工作流模板</p>
<p><img src="assets/Ciqc1F-AFsGAVzEOAAF9PCfkPQQ795.png" alt="Drawing 6.png" /></p>
<p>Github Actions 中的矩阵执行示例</p>
<h3>Gitlab CI</h3>
<p>Gitlab 是由 Gitlab Inc. 开发的基于 Git 的版本管理与软件开发平台。除了作为代码仓库外它还具有在线编辑、Wiki、CI/CD 等功能。在费用方面它提供了免费的社区版本Community EditionCE和免费或收费的商用版本Enterprise EditionEE。其中社区版本和免费的商用版本的区别主要体现在升级到付费商用版本时的操作成本。另一方面即使是免费的社区版本其功能也能够满足企业内的一般使用场景因此常作为企业内部版本管理系统的主要选择之一下面我们就来了解 Gitlab 内置的 CI/CD 功能。</p>
<h4>功能特点</h4>
<ul>
<li>与前面两款产品相似的是Gitlab CI 也使用 yml 文件作为 CI/CD 工作流程的配置文件,在 Gitlab 中,默认的配置文件名为 .gitlab-ci.yml。在配置文件中涵盖了任务流水线Pipeline的处理过程细节例如在配置文件中可以定义一到多个任务Job每个任务可以指定一个任务运行的阶段Stage和一到多个执行脚本Script等。完整的 .gitlab-ci.yml 配置项可参考<a href="https://docs.gitlab.com/ee/ci/yaml/README.html">官方文档</a></li>
<li><strong>独立安装执行器</strong>与前面两款产品不同的是Gitlab 中需要单独安装执行器。Gitlab 中的执行器 Gitlab Runner 是一个独立运行的开源程序,它的作用是执行任务,并将结果反馈到 Gitlab 中。开发者可以在独立的服务器上<a href="https://docs.gitlab.com/runner/install/index.html">安装</a>Gitlab Runner 工具,然后依次执行<strong>gitlab-runner register</strong>注册特定配置的 Runner最后执行<strong>gitlab-runner start</strong>启动相应服务。此外,项目中除了注册独立的 Runner 外,也可以使用共享的或组内通用的 Runner。</li>
</ul>
<p>当项目根目录中存在.gitlab-ci.yml 文件时,用户提交代码到 Git 仓库时,在 Gitlab 的 CI/CD 面板中即可看到相应的任务记录,当成功设置 gitlab-runner 时这些任务就会在相应的 Runner 中执行并反馈日志和结果。如下图所示:</p>
<p><img src="assets/CgqCHl-AFtSAdFLVAAJ3DhDFMt0140.png" alt="Drawing 7.png" /></p>
<p>Gitlab CI/CD 的任务列表示例界面</p>
<h3>总结</h3>
<p>最后我们来做一个总结。在今天的课程里,我们一起了解了 4 个典型 CI/CD 工具Jenkins、CircleCI、Github Actions 和 Gitlab CI。</p>
<ul>
<li>在这四款工具中Jenkins 是诞生最早也最广为人知的,它的优点在于插件功能丰富且完全开源免费,因此在企业中应用较多。但缺点在于缺少特定语言环境工作流的配置模板,使用成本相对较高。此外,它的服务器需要独立部署和运维。</li>
<li>CircleCI 和 Github Actions 都提供了基于容器化的云端服务的能力提供不同的收费策略以满足普通小型开源项目和大型私有项目的各类需求。两者相比CircleCI 胜在除了对接 Github 中的项目外,还支持 BitBucket、Heroku 等平台的流程对接。而 Github Actions 则是 Github 项目中内置的 CI/CD 工具,使用成本最低,且提供了矩阵运行、多容器测试、多工作流模板等特色功能。</li>
<li>Gitlab CI 则是企业中较受欢迎的版本管理工具。Gitlab 中内置 CI/CD 工具,和 CircleCI 与 Github Actions 相同的是Gitlab CI 也使用 yml 格式的配置文件,不同之处主要在于需要独立安装与配置 Runner。</li>
</ul>
<p>本节课的课后思考题是:如果你所在的企业需要选择一款 CI/CD 工具,你选择的主要依据有哪些呢?以今天谈到的几款工具为例,谈谈你的选择和想法吧。</p>
</div>
</div>
<div>
<div style="float: left">
<a href="/专栏/前端工程化精讲-完/17 部署初探:为什么一般不在开发环境下部署代码?.md.html">上一页</a>
</div>
<div style="float: right">
<a href="/专栏/前端工程化精讲-完/19 安装提效:部署流程中的依赖安装效率优化.md.html">下一页</a>
</div>
</div>
</div>
</div>
</div>
</div>
<a class="off-canvas-overlay" onclick="hide_canvas()"></a>
</div>
<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v652eace1692a40cfa3763df669d7439c1639079717194" integrity="sha512-Gi7xpJR8tSkrpF7aordPZQlW2DLtzUlZcumS8dMQjwDHEnw9I7ZLyiOj/6tZStRBGtGgN6ceN6cMH8z7etPGlw==" data-cf-beacon='{"rayId":"7099776fa8b23cfa","version":"2021.12.0","r":1,"token":"1f5d475227ce4f0089a7cff1ab17c0f5","si":100}' crossorigin="anonymous"></script>
</body>
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-NPSEEVD756"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', 'G-NPSEEVD756');
var path = window.location.pathname
var cookie = getCookie("lastPath");
console.log(path)
if (path.replace("/", "") === "") {
if (cookie.replace("/", "") !== "") {
console.log(cookie)
document.getElementById("tip").innerHTML = "<a href='" + cookie + "'>跳转到上次进度</a>"
}
} else {
setCookie("lastPath", path)
}
function setCookie(cname, cvalue) {
var d = new Date();
d.setTime(d.getTime() + (180 * 24 * 60 * 60 * 1000));
var expires = "expires=" + d.toGMTString();
document.cookie = cname + "=" + cvalue + "; " + expires + ";path = /";
}
function getCookie(cname) {
var name = cname + "=";
var ca = document.cookie.split(';');
for (var i = 0; i < ca.length; i++) {
var c = ca[i].trim();
if (c.indexOf(name) === 0) return c.substring(name.length, c.length);
}
return "";
}
</script>
</html>

View File

@@ -1,858 +1,429 @@
<!DOCTYPE html>
<!-- saved from url=(0046)https://kaiiiz.github.io/hexo-theme-book-demo/ -->
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no">
<link rel="icon" href="/static/favicon.png">
<title>19 安装提效:部署流程中的依赖安装效率优化.md.html</title>
<!-- Spectre.css framework -->
<link rel="stylesheet" href="/static/index.css">
<!-- theme css & js -->
<meta name="generator" content="Hexo 4.2.0">
</head>
<body>
<div class="book-container">
<div class="book-sidebar">
<div class="book-brand">
<a href="/">
<img src="/static/favicon.png">
<span>技术文章摘抄</span>
</a>
</div>
<div class="book-menu uncollapsible">
<ul class="uncollapsible">
<li><a href="/" class="current-tab">首页</a></li>
</ul>
<ul class="uncollapsible">
<li><a href="../">上一级</a></li>
</ul>
<ul class="uncollapsible">
<li>
<a href="/专栏/前端工程化精讲-完/00 开篇词 建立上帝视角,全面系统掌握前端效率工程化.md.html">00 开篇词 建立上帝视角,全面系统掌握前端效率工程化.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/01 项目基石:前端脚手架工具探秘.md.html">01 项目基石:前端脚手架工具探秘.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/02 界面调试:热更新技术如何开着飞机修引擎?.md.html">02 界面调试:热更新技术如何开着飞机修引擎?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/03 构建提速:如何正确使用 SourceMap.md.html">03 构建提速:如何正确使用 SourceMap.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/04 接口调试Mock 工具如何快速进行接口调试?.md.html">04 接口调试Mock 工具如何快速进行接口调试?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/05 编码效率:如何提高编写代码的效率?.md.html">05 编码效率:如何提高编写代码的效率?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/06 团队工具:如何利用云开发提升团队开发效率?.md.html">06 团队工具:如何利用云开发提升团队开发效率?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/07 低代码工具:如何用更少的代码实现更灵活的需求.md.html">07 低代码工具:如何用更少的代码实现更灵活的需求.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/08 无代码工具:如何做到不写代码就能高效交付?.md.html">08 无代码工具:如何做到不写代码就能高效交付?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/09 构建总览:前端构建工具的演进.md.html">09 构建总览:前端构建工具的演进.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/10 流程分解Webpack 的完整构建流程.md.html">10 流程分解Webpack 的完整构建流程.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/11 编译提效:如何为 Webpack 编译阶段提速?.md.html">11 编译提效:如何为 Webpack 编译阶段提速?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/12 打包提效:如何为 Webpack 打包阶段提速?.md.html">12 打包提效:如何为 Webpack 打包阶段提速?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/13 缓存优化:那些基于缓存的优化方案.md.html">13 缓存优化:那些基于缓存的优化方案.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/14 增量构建Webpack 中的增量构建.md.html">14 增量构建Webpack 中的增量构建.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/15 版本特性Webpack 5 中的优化细节.md.html">15 版本特性Webpack 5 中的优化细节.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/16 无包构建:盘点那些 No-bundle 的构建方案.md.html">16 无包构建:盘点那些 No-bundle 的构建方案.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/17 部署初探:为什么一般不在开发环境下部署代码?.md.html">17 部署初探:为什么一般不在开发环境下部署代码?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/18 工具盘点:掌握那些流行的代码部署工具.md.html">18 工具盘点:掌握那些流行的代码部署工具.md.html</a>
</li>
<li>
<a class="current-tab" href="/专栏/前端工程化精讲-完/19 安装提效:部署流程中的依赖安装效率优化.md.html">19 安装提效:部署流程中的依赖安装效率优化.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/20 流程优化:部署流程中的构建流程策略优化.md.html">20 流程优化:部署流程中的构建流程策略优化.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/21 容器方案:从构建到部署,容器化方案的优势有哪些?.md.html">21 容器方案:从构建到部署,容器化方案的优势有哪些?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/22 案例分析:搭建基本的前端高效部署系统.md.html">22 案例分析:搭建基本的前端高效部署系统.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/23 结束语 前端效率工程化的未来展望.md.html">23 结束语 前端效率工程化的未来展望.md.html</a>
</li>
</ul>
</div>
</div>
<div class="sidebar-toggle" onclick="sidebar_toggle()" onmouseover="add_inner()" onmouseleave="remove_inner()">
<div class="sidebar-toggle-inner"></div>
</div>
<script>
function add_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.add('show')
}
function remove_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.remove('show')
}
function sidebar_toggle() {
let sidebar_toggle = document.querySelector('.sidebar-toggle')
let sidebar = document.querySelector('.book-sidebar')
let content = document.querySelector('.off-canvas-content')
if (sidebar_toggle.classList.contains('extend')) { // show
sidebar_toggle.classList.remove('extend')
sidebar.classList.remove('hide')
content.classList.remove('extend')
} else { // hide
sidebar_toggle.classList.add('extend')
sidebar.classList.add('hide')
content.classList.add('extend')
}
}
function open_sidebar() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.add('show')
overlay.classList.add('show')
}
function hide_canvas() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.remove('show')
overlay.classList.remove('show')
}
</script>
<div class="off-canvas-content">
<div class="columns">
<div class="column col-12 col-lg-12">
<div class="book-navbar">
<!-- For Responsive Layout -->
<header class="navbar">
<section class="navbar-section">
<a onclick="open_sidebar()">
<i class="icon icon-menu"></i>
</a>
</section>
</header>
</div>
<div class="book-content" style="max-width: 960px; margin: 0 auto;
overflow-x: auto;
overflow-y: hidden;">
<div class="book-post">
<p id="tip" align="center"></p>
<div><h1>19 安装提效:部署流程中的依赖安装效率优化</h1>
<p>上节课我们主要介绍了几个典型的部署工具及其特点。课后的思考题是如果所在企业需要选择一款 CI/CD 工具,你选择的依据有哪些?如果是我会从这几个方面思考:选择付费系统还是免费系统,选择云服务还是自运维,所选的方案是否便于对接上下游系统流程,使用配置是否便捷,对用户而言是否有学习成本......希望这些能为你提供参考。</p>
<p>下面开始本节课的学习。我们在之前的课程中介绍过,前端项目的部署流程包含了代码获取、依赖安装、执行构建、产物打包等阶段,每个阶段的执行过程都有值得关注的效率提升点。在系列课程的第二模块构建效率篇中,我们已经讨论了执行构建阶段的主要优化点,而今天的课程则将详细分析在执行构建的前一个环节——依赖安装阶段中,又有哪些因素会对执行效率产生影响。</p>
<h3>分析前的准备工作</h3>
<h4>五种前端依赖的安装方式</h4>
<p>我们先来对比 5 种不同的前端依赖安装方式:</p>
<ul>
<li><strong>npm</strong>npm 是 NodeJS 自带的包管理工具,也是使用最广泛的工具之一。在测试时我们使用它的默认安装命令 npm install不带额外参数示例代码参照 <a href="https://github.com/fe-efficiency/lessons_fe_efficiency/tree/master/19_install/with_npm">19_install/with_npm</a><em></em></li>
<li><strong>Yarn</strong>Yarn 是 Facebook 于 2016 年发布的包管理工具,和 npm 5 之前的版本相比Yarn 在依赖版本稳定性和安装效率方面通常更优(在 Github 中Yarn 的 star 数量是 npm 的两倍多,可见其受欢迎程度)。在测试时我们同样使用默认安装命令 Yarn, 不带额外参数,示例代码参照 <a href="https://github.com/fe-efficiency/lessons_fe_efficiency/tree/master/19_install/with_yarn">19_install/with_yarn</a></li>
<li><strong>Yarn with PnP</strong>Yarn 自 1.12 版本开始支持 PnP 功能,旨在抛弃作为包管理目录的 node_modules而使用软链接到本地缓存目录的方式来提升安装和模块解析的效率。在测试时我们使用 yarn --pnp不带额外参数。示例代码参照 <a href="https://github.com/fe-efficiency/lessons_fe_efficiency/tree/master/19_install/with_yarn_pnp1">19_install/with_yarn_p</a><a href="https://github.com/fe-efficiency/lessons_fe_efficiency/tree/master/19_install/with_yarn">np1</a></li>
<li><strong>Yarn v2</strong>Yarn 在 <a href="https://dev.to/arcanis/introducing-yarn-2-4eh1">2020 年初</a>发布了 v2 版本,它和 v1 版本相比有许多重大改变,包括默认使用优化后的 PnP 等。v2 版本目前通过 Set Version 的方式安装在项目内部,而非全局安装。测试时我们使用安装命令 Yarn不带额外参数。示例代码参照 <a href="https://github.com/fe-efficiency/lessons_fe_efficiency/tree/master/19_install/with_yarn_v2">19_install/with_yarn_v2</a></li>
<li><strong>pnpm</strong>pnpm 是于 2017 年发布的包管理工具,和 Yarn 相同,它也支持依赖版本的确定性安装特性,同时使用硬连接与符号连接缓存目录的方式,这种方式相比于非 PnP 模式下的 Yarn 安装而言磁盘存储效率更高。测试时我们使用安装命令 pnpm install不带额外参数。示例代码参照 <a href="https://github.com/fe-efficiency/lessons_fe_efficiency/tree/master/19_install/with_pnpm">19_install/with_pnpm</a></li>
</ul>
<h4>依赖安装的基本流程</h4>
<p>在对影响效率的问题进行分析之前,我们需要先了解一下前端依赖安装的基本流程阶段划分,这有助于分析不同场景下执行时间的快慢因素,排除各工具的细节差异。前端项目中依赖包安装的主要执行阶段如下:</p>
<ol>
<li><strong>解析依赖关系阶段</strong>:这个阶段的主要功能是分析项目中各依赖包的依赖关系和版本信息。</li>
<li><strong>下载阶段</strong>:这个阶段的主要功能是下载依赖包。</li>
<li><strong>链接阶段</strong>:这个阶段的主要功能是处理项目依赖目录和缓存之间的硬链接和符号连接。</li>
</ol>
<p>那么如何获取执行时间呢?</p>
<h4>如何获取执行时间</h4>
<p>上面的几种安装方式中npm 和 Yarn 在执行完成后的输出日志中会包含执行时间,而 pnpm 的输出日志中则没有。不过我们还是可以使用系统提供的 time 命令来获取,方法如下所示:</p>
<pre><code>time npm i
time yarn
time pnpm i
</code></pre>
<h4></h4>
<h4>如何获取执行日志</h4>
<p>除了获取安装过程的执行时间外,如果需要进一步分析造成时间差异的原因,就需要从安装过程日志中获取更详细的执行细节,从中寻找答案。不同工具显示详细日志的方式也不同:</p>
<ul>
<li><strong>npm</strong>:使用 npm 安装时需要在执行命令后增加***--verbose***来显示完整日志。</li>
<li><strong>Yarn v1</strong>Yarn v1 版本(包括 Yarn --PnP的实现方法和 npm 一样,即通过增加 <strong>--verbose</strong> 来显示完整日志。</li>
<li><strong>Yarn v2</strong>Yarn v2 版本默认显示完整日志,可通过 --json 参数变换日志格式,这里使用默认设置即可。</li>
<li><strong>pnpm</strong>pnpm 安装时需要在执行命令后增加 <strong>--reporter ndjson</strong> 来显示完整日志。</li>
</ul>
<h4>环境状态的五个分析维度</h4>
<p>在确定了安装工具和分析方式后,我们还需要对执行过程进行划分,下面我一共区分了 5 种项目执行安装时可能遇到的场景:</p>
<p><img src="assets/Ciqc1F-EFtSATNnGAABjLC1FXMQ343.png" alt="Drawing 0.png" /></p>
<blockquote>
<p>注 1除了第一种纯净环境外后面的环境中都存在 Lock 文件。因为 Lock 文件对于提供稳定依赖版本至关重要。出于现实场景考虑,这里不再单独对比没有 Lock 文件但存在历史安装目录的场景。
注 2 为了屏蔽网络对解析下载依赖包的影响,所有目录下均使用相同注册表网址 registry.npm.taobao.org。
注 3以下时间统计的默认设备为 MacOS网速约为 20Mbit/s。</p>
</blockquote>
<h3>不同维度对安装效率的影响分析</h3>
<h4>纯净环境</h4>
<p>首先来对纯净环境进行分析,不同安装方式的执行耗时统计如下:</p>
<p><img src="assets/Ciqc1F-EFuOAf51jAABmxP3DGmM839.png" alt="Drawing 1.png" /></p>
<blockquote>
<p>注 1总安装时间为执行后显示的时间。而各阶段的细分时间在日志中分析获取。
注 2在 pnpm 的执行过程中并未对各阶段进行完全分隔,而是针对不同依赖包递归执行各阶段,这种情况在<strong>纯净环境中尤其明显</strong>,因此阶段时间上不便做单独划分。</p>
</blockquote>
<p>对结果的分析如下:</p>
<ul>
<li>在总体执行时间上npm &lt; pnpm &lt; Yarn v1 --PnP &lt; Yarn v1 &lt; Yarn v2根据<a href="https://github.com/pnpm/benchmarks-of-javascript-package-managers">其他统计</a>,纯净环境下的执行时间是 pnpm &lt; Yarn v1 --PnP &lt; Yarn v1 &lt; npm推测主要是设备带宽等环境区别造成</li>
<li>在解析依赖阶段和下载阶段npm 略快于 Yarn v1主要原因是 Yarn 将网络并发数设置为 <strong>8</strong>(源码中搜索 NETWORK_CONCURRENCY 可见),而 npm 中的并发数则是 <strong>10</strong>(源码中搜索 npm.limit 可见)。而 Yarn v2 在这两个阶段中性能尤其缓慢,原因可能是其在设计上的重大变更——在相应阶段中引入了更复杂的依赖分析算法和对下载包的额外处理。</li>
<li>在链接阶段npm 耗时小于 Yarn v1 而大于 Yarn v1 --PnP原因在于 npm 在下载阶段中处理了文件写入安装目录的过程Yarn v1 则在链接阶段进行写入,而 Yarn v1 --PnP 则直接使用软硬链接而非复制文件因此效率更高。Yarn v2 一方面因为同样采用软硬链接的方式,另一方面链接的数据为 Zip 压缩包而非二进制缓存,因此效率中等。</li>
</ul>
<h4>Lock 环境</h4>
<p>然后我们来考察 Lock 文件对于安装效率的影响。和第一种最纯净的情况相比,带有 Lock 文件的情况通常更符合现实中项目在部署环境中的初始状态(因为 Lock 文件可以在一定程度上保证项目依赖版本的稳定性,因此通常都会把 Lock 文件也保留在代码仓库中)。引入 Lock 文件后,不同安装工具执行安装的耗时情况如下:</p>
<p><img src="assets/CgqCHl-EFwyAUyc0AABWRUrdf70141.png" alt="Drawing 2.png" /></p>
<blockquote>
<p>注 1 Yarn 解析依赖阶段日志未显示耗时,因此标记为 0 秒。</p>
</blockquote>
<p>对结果的分析如下:</p>
<ul>
<li>从总的执行时间上看Lock 文件的存在对于各安装工具而言都起到了积极的作用,安装时间都有一定程度的缩短。</li>
<li>进一步细分安装各阶段可以发现,<strong>Lock 文件主要优化的是依赖解析阶段的时间</strong>,即在 Lock 文件存在的情况下,项目在安装时直接通过 Lock 文件获取项目中的具体依赖关系与依赖包的版本信息,因此这一阶段的耗时大多趋近于零。</li>
</ul>
<h4>缓存环境</h4>
<p>缓存环境是在部署服务中可能遇到的一种情形。项目在部署过程中依赖安装时产生了本地缓存,部署结束后项目工作目录被删除,因此再次部署开始时工作目录内有 Lock 文件,也有本地缓存,但是不存在安装目录。这种情形下的耗时统计如下:</p>
<p><img src="assets/CgqCHl-EFxqARvOCAABWYHKz7lk118.png" alt="Drawing 3.png" /></p>
<p>对结果的分析如下:</p>
<ul>
<li>从执行时间上看,各类型的安装方式的耗时都明显下降。</li>
<li>从细分阶段的耗时情况可以发现,<strong>本地缓存主要优化的是下载依赖包阶段的耗时</strong>,即在本地缓存命中的情况下免去了通过网络请求下载依赖包数据的过程。对于使用 npm 的项目而言,这一阶段还剩下解压缓存写入安装目录的耗时。对于使用 Yarn 的项目而言,这一阶段没有其他处理,直接略过。对于 pnpm 的项目而言,这一阶段的处理中还剩下从缓存硬链接到项目安装目录的过程。</li>
</ul>
<h4>无缓存的重复安装环境</h4>
<p>无缓存的重复安装环境在本地环境下部署时可能遇到,即当本地已存在安装目录,但人工清理缓存后再次执行安装时可能遇到。这种情况的耗时如下:</p>
<p><img src="assets/Ciqc1F-EFyqAc4dbAABWpKpPTt4305.png" alt="Drawing 4.png" /></p>
<p>对结果的分析如下:</p>
<ul>
<li>从上面的表格中可以看到,存在安装目录这一条件首先<strong>对链接阶段能起到优化的作用</strong>。对于下载阶段,除了使用 PnP 的两种安装方式外,当项目中已存在安装目录时,下载阶段耗时也趋近于零。其中 Yarn v1 表现最好,各主要阶段都直接略过,而 npm 和 pnpm 则多少还有一些处理过程。</li>
<li>而使用 PnP 的两种安装方式因为内部机制依赖缓存,本身不存在 node_modules因此在清除本地缓存的情况下仍需完整经历远程下载过程只不过由于其他安装后文件的存在例如 .pnp.js使得链接阶段的耗时相比 Lock 环境而言有所下降。</li>
</ul>
<h4>有缓存的重复安装环境</h4>
<p>最后是安装目录与本地缓存都存在的情况,耗时如下:</p>
<p><img src="assets/CgqCHl-EFzKAOLpCAABS3Wdu4Zw636.png" alt="Drawing 5.png" /></p>
<p>对结果的分析如下:</p>
<ul>
<li>无论对于哪种安装方式而言,这种情况都是最理想的。可以看到,各安装工具的耗时都趋近于零。其中尤其对于 Yarn v1 而言效率最高,而 pnpm 次之npm 相对最慢。</li>
</ul>
<h3>对不同安装工具和不同安装条件的效率总结</h3>
<h4>不同安装条件</h4>
<p>下面我们进行一个总结,首先来看不同安装条件下的效率情况:</p>
<ol>
<li>对于项目的依赖安装过程来说,效率最高的 3 个条件是:存在 Lock 文件存在,存在本地缓存存在和存在安装记录。</li>
<li>这 3 个条件中Lock 文件的留存是<strong>最容易做到的,也是最可能被忽略的</strong>。大部分项目也都会保留在代码仓库中。不过据我观察也存在不提交 Lock 文件的情况,即在一些多人维护的项目中,因为害怕处理冲突而主动忽略了 Lock 文件,这是应该尽量避免的。</li>
<li>本地缓存则是当安装记录不存在时<strong>最重要的优化手段。</strong> 对于大部分部署系统而言,本地缓存通常也是会默认保留在部署服务器上的。因此需要注意的更多是磁盘空间与效率的平衡。此外需要注意在部署服务的个别项目中,执行清除缓存的操作也会影响其他项目。</li>
<li>本地安装记录对于部署系统而言需要占据较多的磁盘空间,因此在多数情况下,部署系统默认不保留安装目录。项目中如需考虑这一优化点,建议确认所使用的部署系统是否支持相关设定。</li>
<li>在安装条件方面,其实还有一些额外的不容易量化的条件,例如网速、磁盘 I/O 速度等,对于部署服务而言,这些硬件条件的好坏也会直接影响用户的使用效率。这里不再展开。</li>
</ol>
<h4>不同安装工具</h4>
<p>然后我们对不同安装工具的效率情况进行总结:</p>
<p>1.单从效率而言,各工具在不同安装条件下的优劣各有不同:</p>
<ul>
<li>如果考虑各种场景下的综合表现pnpm 是最稳定高效的,其他工具在不同场景下多少都有表现不佳的情况。</li>
<li>如果考虑现实情况中,在部署环境下大多可以支持 Lock 文件和本地缓存的留存,并且部分系统中也会保留安装目录,则 Yarn v1 是更好的选择。</li>
<li>如果考虑只有 Lock 文件的情况,则 npm 的表现要优于 Yarn。</li>
<li>在无安装目录的情况下Yarn v1 的 PnP 模式效率要高于普通模式。</li>
<li>尽管 Yarn v2 在无缓存的情况都表现不佳,但是它有一点优势是其他方式无法替代的,即支持针对单个项目清除缓存而不影响全局。</li>
</ul>
<p>2.不同的安装工具除了对安装过程的效率会有影响外对构建过程也会产生影响。Yarn v1 普通模式可以作为 npm 的直接替代,不对构建产生影响。而剩下的 PnP 模式、Yarn v2 和 pnpm 则因为依赖存储的方式不同,在构建解析时多少会有差异。因此在项目中选择工具时需要综合考虑。</p>
<h3>总结</h3>
<p>这节课主要讨论了部署流程中的依赖安装环节的执行细节问题。依赖安装常常是一个容易被忽视的环节,但是其中又有很多对执行效率产生影响的因素,不同的安装方式和安装环境条件都可能对执行效率产生影响。</p>
<p>希望通过今天的课程,你可以掌握不同条件下的执行效率区别,并应用到实际项目中。今天的课后思考题是:如果项目中使用的是 npm在最佳条件下是否可以像 Yarn 那样耗时更趋近于零呢?试着从部署系统的角度考虑看看有没有解决方案。</p>
</div>
</div>
<div>
<div style="float: left">
<a href="/专栏/前端工程化精讲-完/18 工具盘点:掌握那些流行的代码部署工具.md.html">上一页</a>
</div>
<div style="float: right">
<a href="/专栏/前端工程化精讲-完/20 流程优化:部署流程中的构建流程策略优化.md.html">下一页</a>
</div>
</div>
</div>
</div>
</div>
</div>
<a class="off-canvas-overlay" onclick="hide_canvas()"></a>
</div>
<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v652eace1692a40cfa3763df669d7439c1639079717194" integrity="sha512-Gi7xpJR8tSkrpF7aordPZQlW2DLtzUlZcumS8dMQjwDHEnw9I7ZLyiOj/6tZStRBGtGgN6ceN6cMH8z7etPGlw==" data-cf-beacon='{"rayId":"709977720dc13cfa","version":"2021.12.0","r":1,"token":"1f5d475227ce4f0089a7cff1ab17c0f5","si":100}' crossorigin="anonymous"></script>
</body>
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-NPSEEVD756"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', 'G-NPSEEVD756');
var path = window.location.pathname
var cookie = getCookie("lastPath");
console.log(path)
if (path.replace("/", "") === "") {
if (cookie.replace("/", "") !== "") {
console.log(cookie)
document.getElementById("tip").innerHTML = "<a href='" + cookie + "'>跳转到上次进度</a>"
}
} else {
setCookie("lastPath", path)
}
function setCookie(cname, cvalue) {
var d = new Date();
d.setTime(d.getTime() + (180 * 24 * 60 * 60 * 1000));
var expires = "expires=" + d.toGMTString();
document.cookie = cname + "=" + cvalue + "; " + expires + ";path = /";
}
function getCookie(cname) {
var name = cname + "=";
var ca = document.cookie.split(';');
for (var i = 0; i < ca.length; i++) {
var c = ca[i].trim();
if (c.indexOf(name) === 0) return c.substring(name.length, c.length);
}
return "";
}
</script>
</html>

View File

@@ -1,780 +1,390 @@
<!DOCTYPE html>
<!-- saved from url=(0046)https://kaiiiz.github.io/hexo-theme-book-demo/ -->
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no">
<link rel="icon" href="/static/favicon.png">
<title>21 容器方案:从构建到部署,容器化方案的优势有哪些?.md.html</title>
<!-- Spectre.css framework -->
<link rel="stylesheet" href="/static/index.css">
<!-- theme css & js -->
<meta name="generator" content="Hexo 4.2.0">
</head>
<body>
<div class="book-container">
<div class="book-sidebar">
<div class="book-brand">
<a href="/">
<img src="/static/favicon.png">
<span>技术文章摘抄</span>
</a>
</div>
<div class="book-menu uncollapsible">
<ul class="uncollapsible">
<li><a href="/" class="current-tab">首页</a></li>
</ul>
<ul class="uncollapsible">
<li><a href="../">上一级</a></li>
</ul>
<ul class="uncollapsible">
<li>
<a href="/专栏/前端工程化精讲-完/00 开篇词 建立上帝视角,全面系统掌握前端效率工程化.md.html">00 开篇词 建立上帝视角,全面系统掌握前端效率工程化.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/01 项目基石:前端脚手架工具探秘.md.html">01 项目基石:前端脚手架工具探秘.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/02 界面调试:热更新技术如何开着飞机修引擎?.md.html">02 界面调试:热更新技术如何开着飞机修引擎?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/03 构建提速:如何正确使用 SourceMap.md.html">03 构建提速:如何正确使用 SourceMap.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/04 接口调试Mock 工具如何快速进行接口调试?.md.html">04 接口调试Mock 工具如何快速进行接口调试?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/05 编码效率:如何提高编写代码的效率?.md.html">05 编码效率:如何提高编写代码的效率?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/06 团队工具:如何利用云开发提升团队开发效率?.md.html">06 团队工具:如何利用云开发提升团队开发效率?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/07 低代码工具:如何用更少的代码实现更灵活的需求.md.html">07 低代码工具:如何用更少的代码实现更灵活的需求.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/08 无代码工具:如何做到不写代码就能高效交付?.md.html">08 无代码工具:如何做到不写代码就能高效交付?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/09 构建总览:前端构建工具的演进.md.html">09 构建总览:前端构建工具的演进.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/10 流程分解Webpack 的完整构建流程.md.html">10 流程分解Webpack 的完整构建流程.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/11 编译提效:如何为 Webpack 编译阶段提速?.md.html">11 编译提效:如何为 Webpack 编译阶段提速?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/12 打包提效:如何为 Webpack 打包阶段提速?.md.html">12 打包提效:如何为 Webpack 打包阶段提速?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/13 缓存优化:那些基于缓存的优化方案.md.html">13 缓存优化:那些基于缓存的优化方案.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/14 增量构建Webpack 中的增量构建.md.html">14 增量构建Webpack 中的增量构建.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/15 版本特性Webpack 5 中的优化细节.md.html">15 版本特性Webpack 5 中的优化细节.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/16 无包构建:盘点那些 No-bundle 的构建方案.md.html">16 无包构建:盘点那些 No-bundle 的构建方案.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/17 部署初探:为什么一般不在开发环境下部署代码?.md.html">17 部署初探:为什么一般不在开发环境下部署代码?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/18 工具盘点:掌握那些流行的代码部署工具.md.html">18 工具盘点:掌握那些流行的代码部署工具.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/19 安装提效:部署流程中的依赖安装效率优化.md.html">19 安装提效:部署流程中的依赖安装效率优化.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/20 流程优化:部署流程中的构建流程策略优化.md.html">20 流程优化:部署流程中的构建流程策略优化.md.html</a>
</li>
<li>
<a class="current-tab" href="/专栏/前端工程化精讲-完/21 容器方案:从构建到部署,容器化方案的优势有哪些?.md.html">21 容器方案:从构建到部署,容器化方案的优势有哪些?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/22 案例分析:搭建基本的前端高效部署系统.md.html">22 案例分析:搭建基本的前端高效部署系统.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/23 结束语 前端效率工程化的未来展望.md.html">23 结束语 前端效率工程化的未来展望.md.html</a>
</li>
</ul>
</div>
</div>
<div class="sidebar-toggle" onclick="sidebar_toggle()" onmouseover="add_inner()" onmouseleave="remove_inner()">
<div class="sidebar-toggle-inner"></div>
</div>
<script>
function add_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.add('show')
}
function remove_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.remove('show')
}
function sidebar_toggle() {
let sidebar_toggle = document.querySelector('.sidebar-toggle')
let sidebar = document.querySelector('.book-sidebar')
let content = document.querySelector('.off-canvas-content')
if (sidebar_toggle.classList.contains('extend')) { // show
sidebar_toggle.classList.remove('extend')
sidebar.classList.remove('hide')
content.classList.remove('extend')
} else { // hide
sidebar_toggle.classList.add('extend')
sidebar.classList.add('hide')
content.classList.add('extend')
}
}
function open_sidebar() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.add('show')
overlay.classList.add('show')
}
function hide_canvas() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.remove('show')
overlay.classList.remove('show')
}
</script>
<div class="off-canvas-content">
<div class="columns">
<div class="column col-12 col-lg-12">
<div class="book-navbar">
<!-- For Responsive Layout -->
<header class="navbar">
<section class="navbar-section">
<a onclick="open_sidebar()">
<i class="icon icon-menu"></i>
</a>
</section>
</header>
</div>
<div class="book-content" style="max-width: 960px; margin: 0 auto;
overflow-x: auto;
overflow-y: hidden;">
<div class="book-post">
<p id="tip" align="center"></p>
<div><h1>21 容器方案:从构建到部署,容器化方案的优势有哪些?</h1>
<p>上节课我们主要介绍了部署系统中各耗时环节的一些常用优化方案。课后思考题是:课程中提到了几种利用缓存的优化方案呢?如果你认真学习了课程内容,不难发现我一共提到了三种基于缓存的优化方案,它们分别是:多项目共用依赖缓存、依赖安装目录的缓存以及构建过程的持久化缓存备份。这些缓存方案不仅可以运用到传统的部署方式中,在今天介绍的容器化部署方案中也有各自的用武之地。</p>
<p>下面我就来介绍本节课的第一个话题:什么是容器化呢?</p>
<h3>容器化的基本概念</h3>
<h4>什么是容器化</h4>
<p><a href="https://en.wikipedia.org/wiki/OS-level_virtualization">容器化Containerization</a>通常是指以 <a href="https://www.docker.com/">Docker</a> 技术为代表,将操作系统内核虚拟化的技术。和传统的虚拟机相比,容器化具有占用空间更小、性能开销更低、启动更快、支持弹性伸缩以及支持容器间互联等优势。</p>
<p>下面介绍 Docker 的几个基本概念。</p>
<h4>Docker</h4>
<p>通常我们提到的 Docker 是指运行在 Linux/Windows/macOS 中开源的虚拟化引擎用于创建、管理和编排容器此外Docker 也是发布和维护该开源项目的公司名称)。</p>
<h4>镜像</h4>
<p>Docker 中的镜像Image是指用于创建容器实例的基础虚拟化模板。对于开发人员来说可以把镜像理解为编程语言中的类通过一个镜像可以创建多个容器实例镜像之间也存在继承关系。通过 Docker 引擎可以构建、删除镜像,还可以将本地镜像 push 到远程仓库或者从远程拉取。例如一个基于 <a href="https://github.com/nodejs/docker-node/blob/bcd3607a38cdf5529cef9a6ed1163d7e06ddea6d/14/stretch/Dockerfile">node:14</a> 的镜像,在创建时不光包含了运行 node14 版本所需的 Linux 系统环境,还包含了额外打入到镜像内的 Yarn 程序。</p>
<h4>容器</h4>
<p>容器Container是 Docker 中的核心功能单元。通常一个容器内包含了一个或多个应用程序以及运行它们所需要的完整相关环境依赖。例如,基于上面例子中的 node:14 镜像,就可以创建出对应版本 NodeJS 的独立运行环境。通过 Docker 引擎可以对容器进行创建、删除、停止、恢复、与容器交互等操作。</p>
<h4>数据挂载与数据卷</h4>
<p>通常情况下容器内的基础数据来自创建容器的镜像。创建容器后,在容器内执行的命令如果导致容器内的数据产生变化,这些变更的数据会在容器删除的同时被清除,无法持久化保留。如果要解决持久化保留数据,可以采取两种方式:挂载容器的宿主环境(即执行启动容器命令所在的服务器)的目录或使用数据卷。数据卷可以理解为通过 Docker 引擎创建的宿主环境下的独立磁盘空间,用于在容器内读写数据,生命周期独立,不受容器生命周期的影响。</p>
<h4>网络</h4>
<p>Docker 容器的网络有多种驱动类型,例如 bridge、host、overlay 等。其中默认的 bridge 类型类似网桥,用于点对点访问容器间端口或者将容器端口映射到宿主环境下。而 host 则是直接使用宿主环境的网络。更多网络模型介绍可参照<a href="https://docs.docker.com/network/">官方说明文档</a></p>
<h3>容器化的构建部署</h3>
<p>在了解了容器化的基本概念后,我们再来看看什么是容器化的构建部署。</p>
<p>顾名思义,容器化的构建部署是把原先在部署服务器中执行的项目部署流程的各个环节,改为使用容器化的技术来完成,具体可以划分为操作镜像和操作容器两个阶段。</p>
<h4>镜像阶段</h4>
<p>镜像阶段的主要目标是创建一个用于部署代码的容器环境的工作镜像。以前端项目为例,工作镜像中一般会包含:特定版本的 node 环境、git、项目构建所需的其他依赖库等。有了这样的环境就可以在对应的容器中执行各部署环节。</p>
<p>构建镜像的具体内容写在 Dockerfile 文件中,例如下面的代码:</p>
<pre><code># 通过FROM指定父镜像
FROM node:12-slim
# 通过RUN命令依次在镜像中安装gitmake和curl程序
RUN apt-get update
RUN apt-get -y install git
RUN apt-get install -y build-essential
RUN apt-get install -y curl
</code></pre>
<p>这是一个基本的包含 node、git 等依赖程序的部署工作环境的镜像内容。</p>
<p>然后在 Dockerfile 所在目录下执行构建命令,即可创建相应镜像,如下所示:</p>
<pre><code>docker build --network host --tag foo:bar .
</code></pre>
<h4>容器阶段</h4>
<p>容器阶段的主要目标就是基于项目的工作镜像创建执行部署过程的容器,并操作容器执行相应的各部署环节:获取代码、安装依赖、执行构建、产物打包、推送产物等。操作分为两个部分,创建容器与执行命令,如下面的代码所示:</p>
<pre><code># 创建容器
docker run -dit --name container_1 foo:bar bash
# 容器内执行命令
docker exec -it container_1 xxxx
</code></pre>
<h3>容器化部署过程的优势</h3>
<p>与传统的在单台服务器上以目录区分不同部署项目的方式相比,容器化的构建部署过程有以下优点:</p>
<h4>环境隔离</h4>
<p>每个项目在独立的容器内执行构建部署过程,保证容器与宿主环境之间,容器与容器之间的环境隔离,防止原先共用一台服务器时可能产生的互相影响(例如项目脚本中修改了全局配置或环境变量等)。同时,环境隔离还可以保证每个项目都可以自由定制专属的环境依赖,而不必担心对其他项目产生影响。</p>
<h4>多环境构建</h4>
<p>使用容器化部署,可以方便地针对同一个项目生成多套不同的构建环境,达到类似 Github Actions 中的矩阵构建效果,使项目可以同时检测多套环境下的集成过程。</p>
<h4>便于调试</h4>
<p>用户可通过 Xterm+SSH 的方式,通过浏览器访问部署系统中的容器环境。同传统的部署方式相比,用容器化的方式可以在部署遇到问题时让用户第一时间进入容器环境中进行现场调试,效率和便捷性大大提升。之前介绍过的部署工具 CircleCI 中就提供了这一调试功能。</p>
<h4>环境一致性与迁移效率</h4>
<p>由于部署过程的工作环境以 Docker 镜像的方式独立制作和存储,因此可以在支持 Docker 引擎的任意服务器中使用。使用时提供一致的工作环境,无须考虑不同服务器操作系统的差异。在迁移时也可以做到一键迁移,无须重复安装环境依赖。</p>
<h3>容器化部署过程的挑战及建议</h3>
<p>尽管容器化的部署方式有上述优势,但也面临一些问题,例如缓存方面和性能方面的问题等。</p>
<h4>缓存问题</h4>
<p>项目在独立容器中构建部署时,首先面临的就是缓存方面的问题:</p>
<ul>
<li><strong>依赖缓存</strong>:默认情况下,容器内的依赖缓存目录与宿主环境缓存目录不互通,这就导致每次依赖安装时,都无法享受宿主环境缓存带来的效率提升。同时,每次部署流程都在新容器中进行,这也导致在依赖安装的过程中,也无法读取历史依赖安装后的缓存数据。要解决这类问题,可以从两方面入手:生成容器时挂载宿主环境依赖缓存目录,以及上节课中提到的安装目录缓存。</li>
<li><strong>构建缓存</strong>:在传统的服务器中执行部署时,可以通过留存历史构建目录的方式来保留构建缓存数据。而在容器化的情况下,每次部署过程都会基于新容器环境重新执行各部署环节,构建过程的缓存数据也会随着部署结束、容器移除而消失。因此在这种部署方式中,尤其需要重视持久化缓存数据的留存问题。你同样可以考虑从两个方向解决:在宿主环境中创建构建缓存目录并挂载到容器中,并在项目构建配置中将缓存目录设置为该目录,这样就可以将缓存直接写入宿主服务器目录中。或者按照上节课提到的持久化缓存的备份与还原方案,将缓存备份到宿主服务器或远程存储服务器中,之后在新部署流程中进行还原使用。</li>
</ul>
<h4>性能问题</h4>
<p>通常情况下,与传统的服务器部署相比,容器化部署并没有明显的性能差异,但是在实践中也存在一些性能方面的特殊情况:</p>
<ul>
<li><strong>容器资源限制</strong>:在创建容器时可以通过参数来限制容器使用的 CPU 核心数和内存大小。和在普通服务器中执行部署时,完整使用所有系统资源的方式相比,限制系统资源的方式会在一定程度上导致执行过程性能的降低。在多项目使用的容器化集群方案中,这种限制通常是必要的,只是对于性能降低明显的项目而言,可以考虑在设置中分配更多的资源以提升执行效率。</li>
<li><strong>copy-to-write</strong>:性能问题的另一方面则体现在它独特的数据存储方式上。和传统的磁盘读写方式不同,容器中的数据是分层的,环境的数据来自镜像层,而新增的数据则来自写入容器层。在读取数据时,数据来自镜像层还是容器层并没有差别,但当修改数据时,如果修改或删除的是镜像层的数据,容器会先将数据从镜像层复制到容器层,然后进行相应操作。这种先复制后写入的模式称为 copy-to-write。因此如果在容器的部署流程中涉及对镜像层数据的修改时执行起来会比在普通服务器中的操作耗费更多时间。举个极端的例子如果我们把项目的依赖安装过程写入构建镜像中然后在容器内移动 node_modules 目录,会发现这个操作耗费的时间几乎等同于复制完整目录的时间。而同样的移动操作在普通服务器中几乎是瞬间完成的。因此,在使用容器化部署时,需要尽量避免将可变数据写入镜像中。</li>
</ul>
<h3>总结</h3>
<p>这节课我们首先了解了以 Docker 为代表的容器化技术的基本概念:镜像、容器、数据挂载和网络等。然后讨论了容器化的构建部署需要经历的流程,先创建镜像,然后根据镜像创建容器,最后在容器内执行相关部署环节。接着分析了容器化部署的优势和劣势:容器化部署具有隔离性高、支持多环境矩阵执行、易于调试和环境标准化等优势,同时在使用时也需要额外注意对应的缓存和性能问题。</p>
<p>本节课的思考题是:容器化技术不仅可以应用在部署过程中,还更广泛地被应用在部署后的项目服务运行中。试比较这两种场景下对容器化技术需求的差异性。</p>
<p>下节课我们将进入部署效率模块的最后一节课:如何搭建基本的前端高效部署系统。</p>
</div>
</div>
<div>
<div style="float: left">
<a href="/专栏/前端工程化精讲-完/20 流程优化:部署流程中的构建流程策略优化.md.html">上一页</a>
</div>
<div style="float: right">
<a href="/专栏/前端工程化精讲-完/22 案例分析:搭建基本的前端高效部署系统.md.html">下一页</a>
</div>
</div>
</div>
</div>
</div>
</div>
<a class="off-canvas-overlay" onclick="hide_canvas()"></a>
</div>
<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v652eace1692a40cfa3763df669d7439c1639079717194" integrity="sha512-Gi7xpJR8tSkrpF7aordPZQlW2DLtzUlZcumS8dMQjwDHEnw9I7ZLyiOj/6tZStRBGtGgN6ceN6cMH8z7etPGlw==" data-cf-beacon='{"rayId":"709977768f363cfa","version":"2021.12.0","r":1,"token":"1f5d475227ce4f0089a7cff1ab17c0f5","si":100}' crossorigin="anonymous"></script>
</body>
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-NPSEEVD756"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', 'G-NPSEEVD756');
var path = window.location.pathname
var cookie = getCookie("lastPath");
console.log(path)
if (path.replace("/", "") === "") {
if (cookie.replace("/", "") !== "") {
console.log(cookie)
document.getElementById("tip").innerHTML = "<a href='" + cookie + "'>跳转到上次进度</a>"
}
} else {
setCookie("lastPath", path)
}
function setCookie(cname, cvalue) {
var d = new Date();
d.setTime(d.getTime() + (180 * 24 * 60 * 60 * 1000));
var expires = "expires=" + d.toGMTString();
document.cookie = cname + "=" + cvalue + "; " + expires + ";path = /";
}
function getCookie(cname) {
var name = cname + "=";
var ca = document.cookie.split(';');
for (var i = 0; i < ca.length; i++) {
var c = ca[i].trim();
if (c.indexOf(name) === 0) return c.substring(name.length, c.length);
}
return "";
}
</script>
</html>

View File

@@ -1,666 +1,333 @@
<!DOCTYPE html>
<!-- saved from url=(0046)https://kaiiiz.github.io/hexo-theme-book-demo/ -->
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no">
<link rel="icon" href="/static/favicon.png">
<title>23 结束语 前端效率工程化的未来展望.md.html</title>
<!-- Spectre.css framework -->
<link rel="stylesheet" href="/static/index.css">
<!-- theme css & js -->
<meta name="generator" content="Hexo 4.2.0">
</head>
<body>
<div class="book-container">
<div class="book-sidebar">
<div class="book-brand">
<a href="/">
<img src="/static/favicon.png">
<span>技术文章摘抄</span>
</a>
</div>
<div class="book-menu uncollapsible">
<ul class="uncollapsible">
<li><a href="/" class="current-tab">首页</a></li>
</ul>
<ul class="uncollapsible">
<li><a href="../">上一级</a></li>
</ul>
<ul class="uncollapsible">
<li>
<a href="/专栏/前端工程化精讲-完/00 开篇词 建立上帝视角,全面系统掌握前端效率工程化.md.html">00 开篇词 建立上帝视角,全面系统掌握前端效率工程化.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/01 项目基石:前端脚手架工具探秘.md.html">01 项目基石:前端脚手架工具探秘.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/02 界面调试:热更新技术如何开着飞机修引擎?.md.html">02 界面调试:热更新技术如何开着飞机修引擎?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/03 构建提速:如何正确使用 SourceMap.md.html">03 构建提速:如何正确使用 SourceMap.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/04 接口调试Mock 工具如何快速进行接口调试?.md.html">04 接口调试Mock 工具如何快速进行接口调试?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/05 编码效率:如何提高编写代码的效率?.md.html">05 编码效率:如何提高编写代码的效率?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/06 团队工具:如何利用云开发提升团队开发效率?.md.html">06 团队工具:如何利用云开发提升团队开发效率?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/07 低代码工具:如何用更少的代码实现更灵活的需求.md.html">07 低代码工具:如何用更少的代码实现更灵活的需求.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/08 无代码工具:如何做到不写代码就能高效交付?.md.html">08 无代码工具:如何做到不写代码就能高效交付?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/09 构建总览:前端构建工具的演进.md.html">09 构建总览:前端构建工具的演进.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/10 流程分解Webpack 的完整构建流程.md.html">10 流程分解Webpack 的完整构建流程.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/11 编译提效:如何为 Webpack 编译阶段提速?.md.html">11 编译提效:如何为 Webpack 编译阶段提速?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/12 打包提效:如何为 Webpack 打包阶段提速?.md.html">12 打包提效:如何为 Webpack 打包阶段提速?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/13 缓存优化:那些基于缓存的优化方案.md.html">13 缓存优化:那些基于缓存的优化方案.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/14 增量构建Webpack 中的增量构建.md.html">14 增量构建Webpack 中的增量构建.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/15 版本特性Webpack 5 中的优化细节.md.html">15 版本特性Webpack 5 中的优化细节.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/16 无包构建:盘点那些 No-bundle 的构建方案.md.html">16 无包构建:盘点那些 No-bundle 的构建方案.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/17 部署初探:为什么一般不在开发环境下部署代码?.md.html">17 部署初探:为什么一般不在开发环境下部署代码?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/18 工具盘点:掌握那些流行的代码部署工具.md.html">18 工具盘点:掌握那些流行的代码部署工具.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/19 安装提效:部署流程中的依赖安装效率优化.md.html">19 安装提效:部署流程中的依赖安装效率优化.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/20 流程优化:部署流程中的构建流程策略优化.md.html">20 流程优化:部署流程中的构建流程策略优化.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/21 容器方案:从构建到部署,容器化方案的优势有哪些?.md.html">21 容器方案:从构建到部署,容器化方案的优势有哪些?.md.html</a>
</li>
<li>
<a href="/专栏/前端工程化精讲-完/22 案例分析:搭建基本的前端高效部署系统.md.html">22 案例分析:搭建基本的前端高效部署系统.md.html</a>
</li>
<li>
<a class="current-tab" href="/专栏/前端工程化精讲-完/23 结束语 前端效率工程化的未来展望.md.html">23 结束语 前端效率工程化的未来展望.md.html</a>
</li>
</ul>
</div>
</div>
<div class="sidebar-toggle" onclick="sidebar_toggle()" onmouseover="add_inner()" onmouseleave="remove_inner()">
<div class="sidebar-toggle-inner"></div>
</div>
<script>
function add_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.add('show')
}
function remove_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.remove('show')
}
function sidebar_toggle() {
let sidebar_toggle = document.querySelector('.sidebar-toggle')
let sidebar = document.querySelector('.book-sidebar')
let content = document.querySelector('.off-canvas-content')
if (sidebar_toggle.classList.contains('extend')) { // show
sidebar_toggle.classList.remove('extend')
sidebar.classList.remove('hide')
content.classList.remove('extend')
} else { // hide
sidebar_toggle.classList.add('extend')
sidebar.classList.add('hide')
content.classList.add('extend')
}
}
function open_sidebar() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.add('show')
overlay.classList.add('show')
}
function hide_canvas() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.remove('show')
overlay.classList.remove('show')
}
</script>
<div class="off-canvas-content">
<div class="columns">
<div class="column col-12 col-lg-12">
<div class="book-navbar">
<!-- For Responsive Layout -->
<header class="navbar">
<section class="navbar-section">
<a onclick="open_sidebar()">
<i class="icon icon-menu"></i>
</a>
</section>
</header>
</div>
<div class="book-content" style="max-width: 960px; margin: 0 auto;
overflow-x: auto;
overflow-y: hidden;">
<div class="book-post">
<p id="tip" align="center"></p>
<div><h1>23 结束语 前端效率工程化的未来展望</h1>
<p>你好,我是李思嘉。</p>
<p>本专栏的内容到这里就结束了。我们先来简单回顾一下整个课程的主要内容,如下图:</p>
<p><img src="assets/Ciqc1F-WhHaAcAYHAAFR7grnZ_s239.png" alt="Lark20201026-160921.png" /></p>
<p>在这个专栏中,我主要介绍且梳理了前端工程化中效率提升方向的知识,内容涵盖开发效率、构建效率和部署效率三个方面。希望你通过这个系列课程的学习,能建立起前端效率工程化方面相对完整的知识体系,同时在前端开发日常流程中的效率工程类问题方面,能找到分析和解决的新方向。</p>
<p>当然,这些方向实际涵盖的概念与技术点非常广泛,并不容易完全掌握,除了已有的概念和技术之外,新的技术和方向也在不断涌现。下面我会对前端效率工程化相关的技术做一些展望。</p>
<h3>云工作流</h3>
<p>开发效率方面,由 Web IDE 发展而来的云开发工具,目前正逐渐成为几个大型厂商探索的方向之一。从开发到部署的完整云端工作流方式在未来可能成为标准。</p>
<h3>AI 生成页面</h3>
<p>和目前依赖设计资源与开发基建的低代码或无代码开发相比,基于 AI 的生成页面工具(例如微软的 <a href="https://www.microsoft.com/en-us/ai/ai-lab-sketch2code">Sketch2Code</a>可以进一步解放生产力。目前无论是基于设计稿Sketch/PSD的精准生成方式还是基于草图乃至描述语句的 AI 匹配生成方式,仍有各自的局限性。但是随着 AI 技术的发展,这类产品可能会替代目前前端开发过程中的一些基础重复性工作。</p>
<h3>Go/Rust</h3>
<p>在构建效率方面Webpack 5 带来了更完整的缓存策略和代码优化策略,但是从底层性能上,构建工具本身的性能仍然受到 NodeJS 自身语言的限制。从 esbuild 工具的思路出发,基于 Go、Rust 等高性能语言的编译工具在未来或许能成为性能突破点之一。</p>
<h3>No bundle &amp; HTTP/3</h3>
<p>构建效率另一个方向的发展来自无包构建。尽管无包构建工具在生产环境下仍然采用打包构建的方式,但随着网络技术(例如 HTTP/3的发展或许最终可以在生产环境下同样采用无包构建。此外渐进式的使用方式例如 vendor 部分打包而源码部分无包使用)也可能很快成为可实现的方向。</p>
<h3>总结</h3>
<p>希望我的专栏内容和对未来趋势的展望能对你有所帮助。学习时,你需要在日常工作中不断实践,结合具体的场景尝试可行的优化方案。</p>
<p>由于自身在团队中的职责不同,每个开发人员索要学习和侧重的点也不同:</p>
<ul>
<li>如果你目前主要做的是具体项目的开发维护工作,那么分析现有项目的构建工具、构建配置就是一个很好的入手点。通过构建效率模块的相关课程,相信你可以比之前更全面地进行分析和优化。</li>
<li>如果你目前承担着多个项目的选型与架构工作,希望开发效率模块的一些视角可以为你带来思路。</li>
<li>如果你目前从事的是前端基础建设的相关工作,希望系列课程中提到的一些新的开发、构建和部署工具也能为你提供一些着手方向。</li>
</ul>
<p>前端工程化是一个系统工程,不同的开发人员都可以在团队中找到适合的位置。通过不断地实战开发和经验积累,相信你可以逐步提升对技术的认识,增强技术实力。</p>
<p>如果你觉得课程不错,不要忘了推荐给身边的朋友。前路漫漫,一起加油!</p>
</div>
</div>
<div>
<div style="float: left">
<a href="/专栏/前端工程化精讲-完/22 案例分析:搭建基本的前端高效部署系统.md.html">上一页</a>
</div>
</div>
</div>
</div>
</div>
</div>
<a class="off-canvas-overlay" onclick="hide_canvas()"></a>
</div>
<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v652eace1692a40cfa3763df669d7439c1639079717194" integrity="sha512-Gi7xpJR8tSkrpF7aordPZQlW2DLtzUlZcumS8dMQjwDHEnw9I7ZLyiOj/6tZStRBGtGgN6ceN6cMH8z7etPGlw==" data-cf-beacon='{"rayId":"7099777b286a3cfa","version":"2021.12.0","r":1,"token":"1f5d475227ce4f0089a7cff1ab17c0f5","si":100}' crossorigin="anonymous"></script>
</body>
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-NPSEEVD756"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', 'G-NPSEEVD756');
var path = window.location.pathname
var cookie = getCookie("lastPath");
console.log(path)
if (path.replace("/", "") === "") {
if (cookie.replace("/", "") !== "") {
console.log(cookie)
document.getElementById("tip").innerHTML = "<a href='" + cookie + "'>跳转到上次进度</a>"
}
} else {
setCookie("lastPath", path)
}
function setCookie(cname, cvalue) {
var d = new Date();
d.setTime(d.getTime() + (180 * 24 * 60 * 60 * 1000));
var expires = "expires=" + d.toGMTString();
document.cookie = cname + "=" + cvalue + "; " + expires + ";path = /";
}
function getCookie(cname) {
var name = cname + "=";
var ca = document.cookie.split(';');
for (var i = 0; i < ca.length; i++) {
var c = ca[i].trim();
if (c.indexOf(name) === 0) return c.substring(name.length, c.length);
}
return "";
}
</script>
</html>