SegmentFault 全栈爱好者最新的文章
2020-06-14T18:31:26+08:00
https://segmentfault.com/feeds/blogs
https://creativecommons.org/licenses/by-nc-nd/4.0/
Deno
https://segmentfault.com/a/1190000022928927
2020-06-14T18:31:26+08:00
2020-06-14T18:31:26+08:00
champyin
https://segmentfault.com/u/champyin
8
<h2>前言</h2>
<p>Deno 已经被前端圈子提及有很长一段时间了,上个月 Deno 发布了 1.0 版本,又掀起了一小股 Deno 热。Deno 到底是什么?它可以用来做什么呢?它好用吗?带着一直以来的好奇心,趁着周末的时间,认真地接触了一次 Deno。</p>
<h2>一、什么是Deno?</h2>
<p>Deno 是一个更安全的 JavaScript 和 TypeScript 运行时,作者 Ryan Dahl 同时也是 Node.js 的创建者。</p>
<h3>什么是运行时?</h3>
<p>运行时是一个运行环境,也叫运行平台,开发者可以使用指定的语言,基于这个环境开发应用。可以认为运行时就是一个舞台,能做什么事情,取决于舞台能提供什么道具。比如浏览器就是一个运行时,我们可以在浏览器上通过 JS 调用浏览器提供的 API 操作 DOM。</p>
<h3>Deno 的作用</h3>
<p>Deno 的作用,是让开发者可以使用 JavaScript 语言开发后端服务。</p>
<h2>二、为什么会有Deno?</h2>
<p>我们知道 Node.js 也是一个让开发者可以使用 JavaScript 语言开发后端服务的 JavaScript 运行时。那既然已经有一个 Node.js,而且已经非常成功,为什么还要造另一个 JavaScript 运行时 Deno ?</p>
<p>两年前(2018年6月),Ryan Dahl 在德国柏林召开的 JSConf EU 会议上发表了名为 “10 Things I Regret About Node.js” 的演讲,有兴趣可以到这里下载 <a href="https://link.segmentfault.com/?enc=KQ2WMzMIQq3JCE%2BQ%2BkDSWw%3D%3D.JX3Tt9f%2FQtSDtzHMKFlmbJm6lLtlv5xEKXBmsVcQMBhaJrbOkOpcNvfywWqZr4rlIvosS0CXICMrx%2FwBaP3yGVIurerMdMPmbLiWsFym4MhGj%2BaAcZvXFLRCz0SgpJa0" rel="nofollow">PPT</a> 。</p>
<p><img src="/img/bVbIm0E" alt="deno-Ryan-Dahl.jpg" title="deno-Ryan-Dahl.jpg"></p>
<p>在分享中,Ryan 回顾了在他看来当初开发 Node.js 时留下的10大遗憾。但由于Node.js 现在已经广泛应用于各个领域,为了保证兼容性,对 Node.js 底层进行大规模改造已经不现实。会上,Ryan 宣布了他决定开发一个全新的 JavaScript Runtime 以解决当初的种种缺陷,这个项目就是 Deno。</p>
<p><img src="/img/bVbIm0F" alt="deno-logo.png" title="deno-logo.png"></p>
<p>Deno 的命名很有意思,就是把 node(no de) 倒过来 deno(de no),颇有颠覆 Node 的意味。</p>
<p>BTW,上个月(2020年5月15日),Deno 发布了1.0版本。</p>
<h2>三、走近 Deno</h2>
<h3>Deno 的开发语言</h3>
<p>相比 Node.js 使用 C++ 开发,Deno 起初使用的开发语言是 GoLang,后来改为了 Rust。并且随后把 C++ 写的 libdeno 换成了 Rust 编写的 V8 绑定:denoland/rusty_8。</p>
<p>Deno 目前是建立在 V8 引擎、Rust 、Tokio、TypeScript 的基础之上。</p>
<ul>
<li>V8 是 chrome 浏览器内的 JavaScript 运行时。</li>
<li>Rust 是一门系统编程语言,专注于安全,尤其是并发安全。它的性能和标准C++ 不相上下。</li>
<li>Tokio 是一个给 Rust 语言使用的异步运行时,提供 event loop 和具体的 I/O 类型。</li>
<li>TypeScript 是 JavaScript 的超集。</li>
</ul>
<h3>Deno 的特性</h3>
<ul>
<li>默认支持 ES Modules</li>
<li>默认支持 TypeScript</li>
<li>尽可能兼容 Web 标准 APIs</li>
<li>默认采用沙箱模式运行代码,更安全</li>
<li>去中心化第三方模块机制</li>
<li>提供标准库</li>
</ul>
<h3>与 Node.js 的比较</h3>
<ul>
<li>使用 ES 模块,不支持 require()</li>
<li>Deno 不使用 package.json</li>
<li>Deno 不使用 npm</li>
<li>Deno 中的所有异步操作返回 promise,因此 Deno 提供与 Node 不同的 API</li>
<li>Deno 需要显示指定文件、网络和环境权限</li>
<li>第三方模块通过 URL 或者文件路径导入</li>
<li>当未捕获的错误发生时,Deno 总是会异常退出</li>
<li>兼容 Web 的运行时 APIs,更利于前后端的代码同构。</li>
</ul>
<h2>四、如何使用 Deno</h2>
<p>Deno 能够在 macOS、Linux 和 Windows 上运行。Deno 是一个单独的可执行文件,它没有额外的依赖。</p>
<h3>1.安装</h3>
<p>在 macOS 下可以通过Shell命令安装:</p>
<pre><code>curl -fsSL https://deno.land/x/install/install.sh | sh</code></pre>
<p>这个方式在国内安装会很慢,慢到下不下来。。。so,不推荐。</p>
<p>也可以通过HomeBrew 安装:</p>
<pre><code>brew install deno</code></pre>
<p><img src="/img/bVbIm0G" alt="deno-install-brew.jpg" title="deno-install-brew.jpg"></p>
<p>这个方式可以安装下来,但是,安装的版本是 v0.20.0,很低的版本:</p>
<p><img src="/img/bVbIm0H" alt="deno-version-low.jpg" title="deno-version-low.jpg"></p>
<p>并且这个版本不带 upgrade 命令,升级 deno 的时候很不方便。so,也不推荐。</p>
<p>安利通过国内加速器(镜像源 <a href="https://link.segmentfault.com/?enc=VGMrgKGfXcOzJ1Dw%2BloWgg%3D%3D.9LHwnz%2FygloBF204BB%2F52IUAgRdoSW4X2HvKGTYw9PA%3D" rel="nofollow">https://x.deno.js.cn</a> )来安装:</p>
<pre><code>curl -fsSL https://x.deno.js.cn/install.sh | sh</code></pre>
<p>也可以指定版本:</p>
<pre><code>curl -fsSL https://x.deno.js.cn/install.sh | sh -s v1.0.0 </code></pre>
<p><img src="/img/bVbIm0K" alt="deno-install-x.jpg" title="deno-install-x.jpg"></p>
<p>首次安装,可以看到提示,需要手动配置一下环境变量,配置语句也已经给出:</p>
<pre><code>$ touch ~/.bash_profile # 创建用户环境变量文件
$ vim ~/.bash_profile # 打开文件,将刚才命令行提示给出的配置语句粘贴进去,保存退出。</code></pre>
<p>让配置立即生效:</p>
<pre><code>$ source ~/.bash_profile </code></pre>
<p>环境变量就设置好了,现在在任何一个新打开的命令行里面都可以使用 deno 命令了。</p>
<p>注意:如果之前使用 brew 安装过低版本的 deno,请使用 brew uninstall deno 卸载 deno 之后,再使用加速器安装高版本,不卸载直接安装高版本不会生效。(别问我为什么知道。。。都是泪。</p>
<p>其他操作系统环境的安装可参考 <a href="https://link.segmentfault.com/?enc=G2TOtTHDOQH3LhpCfSPIxg%3D%3D.NaLEVKneS7RSFt6dT4UWvLLjyNX5DRWeZmLOvnEVqS4Y4VJbInCKmTlYBlIbPX42" rel="nofollow">https://github.com/denoland/d...</a>。</p>
<p>如果要升级本地的 Deno,可以运行</p>
<pre><code>deno upgrade</code></pre>
<p>还可以安装指定的版本:</p>
<pre><code>deno upgrade --version 1.1.0</code></pre>
<p>这个命令会从 github.com/denoland/deno/releases 获取最新的发布版本(一个可执行的二进制文件 zip 压缩包),然后解压并替换现有的版本。而 github release 的文件使用的是 aws,在国内访问不稳定。</p>
<p>So,升级也推荐使用国内加速器安装指定版本来达到目的。</p>
<h3>2.测试安装</h3>
<pre><code>deno --version</code></pre>
<p><img src="/img/bVbIm0O" alt="deno-version-high.jpg" title="deno-version-high.jpg"></p>
<p>如果打印出 Deno 版本,说明安装成功。</p>
<p>到这里,我们就安装好 Deno ,并且可以基于 Deno 进行开发了。</p>
<h3>3.运行一个远程的项目</h3>
<p>跑一个远程项目(官方的demo)</p>
<pre><code>deno run https://deno.land/std/examples/welcome.ts</code></pre>
<p>可以看到在控制台输出"Welcome to Deno 🦕”。</p>
<p><img src="/img/bVbIm0Q" alt="deno-run-remote.jpg" title="deno-run-remote.jpg"></p>
<h3>4.运行一个本地的项目</h3>
<p>起一个最简单的本地服务</p>
<pre><code>// http.js
import { serve } from "https://deno.land/std@0.57.0/http/server.ts";
const s = serve({ port: 8000 });
console.log("http://localhost:8000/");
for await (const req of s) {
req.respond({ body: "Hello World\n" });
}</code></pre>
<p>可以看到 Deno 在引用第三方模块的方式为 ES6 的 import 语法,并且直接通过 URL 来引入,版本号也被锁定在了 URL 中。</p>
<p>另外,Deno 支持 顶层的 await 语法,不用与 async 语法配对使用了。</p>
<p>运行:</p>
<pre><code>deno run http.js</code></pre>
<p><img src="/img/bVbIm0R" alt="deno-run-local.jpg" title="deno-run-local.jpg"></p>
<p>首次引入第三方包,Deno 会去下载这个包和它的依赖,这些包会被缓存到全局,下次再引入的时候,将直接读取缓存。</p>
<p>这里报了一个缺少网络权限的错,这是因为 Deno 采用沙箱模式运行代码,网络权限必须通过手动添加 flag (--allow-net)来开启。</p>
<p>带上网络权限运行:</p>
<pre><code>deno run --allow-net http.js</code></pre>
<p><img src="/img/bVbIm0S" alt="deno-run-with-net.jpg" title="deno-run-with-net.jpg"></p>
<p>打开localhost://8000</p>
<p><img src="/img/bVbIm0T" alt="deno-server.jpg" title="deno-server.jpg"></p>
<p>可以看到一个简单的本地服务就跑起来了。</p>
<h3>5.其他相关配置</h3>
<p>如果我们要高效地使用 Deno,最好还需要设置一些开发环境,比如环境变量、命令行自动补全、编辑器等。</p>
<ul>
<li>环境变量<p>DENO_DIR:<br>这是 Deno 在本地存放生成的代码和缓存下载的模块的路径,默认为 $HOME/Library/Caches/deno。</p>
<p>NO_COLOR:<br>这个会关闭输出的文字颜色。</p>
<p>HTTP_PROXY 和 HTTPS_PROXY :<br>这两个变量用来设置 HTTP 和 HTTPS 的代理地址。</p>
</li>
<li>命令自动补全<br>通过 deno completions <shell> 命令可以生成补全脚本。他会输出到 stdout,应该将它重定向到适当的文件。<p>Deno 支持的 shell 有 zsh、bash、fish、powershell、elvish。</p>
</li>
<li>编辑器插件</li>
</ul>
<p>我们可以给 VS Code 配置 Deno 的插件: <a href="https://link.segmentfault.com/?enc=hzUcifk3JHRKS6LMJkId%2Fg%3D%3D.BjUR66aKsIuL5yvbKYEDvkR32LShJSWsCVLTg6DLdJ5M8o6rAoZtfHv61heP3IX1" rel="nofollow">vscode_deno</a></p>
<p>如果你是其他编辑器/IDE,可以参考官网<a href="https://link.segmentfault.com/?enc=NCkVYsMzjRfbuo818Uc0nQ%3D%3D.oHleAlzVkekgBu0GU9rXt%2F8ASkFy4N4y2%2BUzhzkiilfkWaM9XCn%2BmPkSiBUzAj8beXXAG%2B9NarnkpkUs0E3etg%3D%3D" rel="nofollow">推荐的插件</a></p>
<h2>Deno 将来会取代 Node.js 吗?</h2>
<p>这也是很多前端者关心的话题,网络上两种声音都有,我的看法是:会共存,但不会取代。</p>
<p>首先,Node.js 的作者之所以开发 Deno 只是为了兑现他心目中对 JavaScript Runtime 的一个理想实现,并不是为了取代 Node.js;</p>
<p>其次,Node.js 经过十多年的发展,已经很成熟了(虽然在 Ryan 的眼里不那么完美),并且已经被广泛应用。个人认为,将来 Deno 要做的事情,Node.js 都能做,如果没有特别的因素(比如潜在的安全隐患等),已经使用了 Node.js 的应用,不大会改用 Deno 重构。</p>
<p>所以,以我目前的认知,我认为 Deno 如果能发展起来,应该会与 Node.js 共生,而不会取代 Node.js。</p>
<p>不管怎样,我很钦佩 Ryan,在 Node.js 获得如此成功之后,仍然怀揣对作品的理想追求,大胆分享自己在 Node.js 中犯的“错误”,开始 Deno 的征程,并且现在 Deno 正在以飞快的速度在迭代。就在昨天,Deno 又发布了 V1.1.0。</p>
<p><img src="/img/bVbIm00" alt="deno-release-v1.1.0.jpg" title="deno-release-v1.1.0.jpg"></p>
<h2>结语</h2>
<p>以上是我对 Deno 的一个初探,解答了什么是 Deno,它有什么作用,有哪些特点,与 Node.js 有什么不同,以及如何使用 Deno(虽然只浅浅地跑了最简单的程序,但足以让我感觉到 Deno 与 Node.js 在使用上的不同)。现在,总算对 Deno 的有了一个比较清晰的了解。</p>
<p>有兴趣交流的小伙伴可以在这里留言讨论:<a href="https://link.segmentfault.com/?enc=GgnBV4jIzGIht8vZaAR1Rg%3D%3D.yIFqLk0zUMKLUQjZ58pCfPGC0UMxKjvPLzd7VQql66KX88cVKJBJeX%2FjdkOYsASnB3hQQn3DrVoF3E2y2eN0RA%3D%3D" rel="nofollow">https://github.com/yc111/yc11...</a><br>Deno 交流QQ群:698469316</p>
<p><br></p>
<p>参考<br>Deno Manual:<a href="https://link.segmentfault.com/?enc=6ogI4KnRq%2FTRfUEK4mkQ2A%3D%3D.m3vwxkx8nQTtKQ9vnk3JctBPHsT%2FmNIGOCDxr8jNhPw%3D" rel="nofollow">https://deno.land/manual</a><br>Deno Doc:<a href="https://link.segmentfault.com/?enc=PkDXgDtPjiLQsmUIaQMnvA%3D%3D.XlCMlZNVkn2dcOEAsAEeaNameMvjHeKhpoeOuYkEM4wCZ1pQUCIiFpmKN4CNCxwIfL7LBCqJrtGvSjyJ501spV5fRhL3Xq0VOSgA9%2FbFY03hYRHThWQ9WUnvjp8TRtEm" rel="nofollow">https://doc.deno.land/https/g...</a><br>Deno中文社区:<a href="https://link.segmentfault.com/?enc=2CJsmy%2FfCdqqlsS4aYKguQ%3D%3D.xrMBwEBd5VBjg4cAR76eXlURTttLJTgPwoSJLVSxMAI%3D" rel="nofollow">https://denocn.org/</a><br>Deno中文开发者社区:<a href="https://link.segmentfault.com/?enc=kNwepp91si1Lr7uN6SRvkw%3D%3D.O%2FhjnVdf3NJ9EY44Zh%2F9lQ1vJGDP6rI44bhEl%2BDfGFg%3D" rel="nofollow">https://deno.js.cn/</a><br>Deno中文手册:<a href="https://link.segmentfault.com/?enc=fqFQT2ahTCS6TmFQci823g%3D%3D.Hs88WClPGMfuwv7woakoHMklEVjJRepM88ieQHTznwNbTLap5Dk%2FPFDeZjs3DEs3" rel="nofollow">https://nugine.github.io/deno...</a><br>Futures 和 Tokio 项目的前世今生:<a href="https://link.segmentfault.com/?enc=3TN4JER8XgGxt9VZM%2F0THA%3D%3D.ojhKGSBAsvYYTw6W8O%2BQdu%2FB6EjxEuT6VPFK3KJmfh9j19XAKz7LjlLEeGlesOHPyVE27NJFwPF4hjjwggnp0dQqADUoe8oyj2TJ56usKYc%3D" rel="nofollow">https://rustcc.cn/article?id=...</a></p>
<p><br></p>
<p>文章首发于于公众号「前端手札」,喜欢的话可以关注一下哦。</p>
<p><img src="/img/remote/1460000021959062" alt="qianduanshouzha-gzh.png" title="qianduanshouzha-gzh.png"></p>
<p><br></p>
<blockquote>本文作者:ChampYin <br>转载请注明出处:<a href="https://link.segmentfault.com/?enc=G3JvTU%2FtcdMpVh%2Fzu9WH3w%3D%3D.y8qUOHosuOMgyiKau0Luw2fso4KVWHZAdeGYc3BCfKIVYQ5MU25MpCRkeIpT%2Fp%2Br0iZ6iE2HvYUKGZOSkq76Ww%3D%3D" rel="nofollow">https://champyin.com/2020/06/14/Deno-初探/</a>
</blockquote>
彻底弄懂GMT、UTC、时区和夏令时
https://segmentfault.com/a/1190000022456305
2020-04-24T15:33:25+08:00
2020-04-24T15:33:25+08:00
champyin
https://segmentfault.com/u/champyin
12
<h2>前言</h2>
<p>格林威治时间、世界时、祖鲁时间、GMT、UTC、跨时区、夏令时,这些眼花缭乱的时间术语,我们可能都不陌生,但是真正遇到问题,可能又不那么确定,不得不再去查一查,处理完可能过段时间又忘记。今天,我们彻底来梳理一下它们。</p>
<h2>一、GMT</h2>
<h3>什么是GMT</h3>
<p>GMT(Greenwich Mean Time), 格林威治平时(也称格林威治时间)。</p>
<p>它规定太阳每天经过位于英国伦敦郊区的皇家格林威治天文台的时间为中午12点。</p>
<h3>GMT的历史</h3>
<p>格林威治皇家天文台为了海上霸权的扩张计划,在十七世纪就开始进行天体观测。为了天文观测,选择了穿过英国伦敦格林威治天文台子午仪中心的一条经线作为零度参考线,这条线,简称格林威治子午线。</p>
<p>1884年10月在美国华盛顿召开了一个国际子午线会议,该会议将格林威治子午线设定为本初子午线,并将格林威治平时 (GMT, Greenwich Mean Time) 作为世界时间标准(UT, Universal Time)。由此也确定了全球24小时自然时区的划分,所有时区都以和 GMT 之间的偏移量做为参考。</p>
<p>1972年之前,格林威治时间(GMT)一直是世界时间的标准。1972年之后,GMT 不再是一个时间标准了。</p>
<h2>二、UTC</h2>
<h3>什么是UTC</h3>
<p>UTC(Coodinated Universal Time),协调世界时,又称世界统一时间、世界标准时间、国际协调时间。由于英文(CUT)和法文(TUC)的缩写不同,作为妥协,简称UTC。</p>
<p>UTC 是现在全球通用的时间标准,全球各地都同意将各自的时间进行同步协调。UTC 时间是经过平均太阳时(以格林威治时间GMT为准)、地轴运动修正后的新时标以及以秒为单位的国际原子时所综合精算而成。</p>
<p>在军事中,协调世界时会使用“Z”来表示。又由于Z在无线电联络中使用“Zulu”作代称,协调世界时也会被称为"Zulu time"。</p>
<h3>UTC 由两部分构成:</h3>
<ul>
<li>原子时间(TAI, International Atomic Time): <br>结合了全球400个所有的原子钟而得到的时间,它决定了我们每个人的钟表中,时间流动的速度。</li>
<li>世界时间(UT, Universal Time): <br>也称天文时间,或太阳时,他的依据是地球的自转,我们用它来确定多少原子时,对应于一个地球日的时间长度。</li>
</ul>
<h3>UTC的历史</h3>
<p>1960年,国际无线电咨询委员会规范统一了 UTC 的概念,并在次年投入实际使用。</p>
<p>“Coordinated Universal Time”这个名字则在1967年才被正式采纳。</p>
<p>1967年以前, UTC被数次调整过,原因是要使用闰秒(leap second)来将 UTC 与地球自转时间进行统一。</p>
<h2>三、GMT vs UTC</h2>
<p>GMT是前世界标准时,UTC是现世界标准时。<br>UTC 比 GMT更精准,以原子时计时,适应现代社会的精确计时。<br>但在不需要精确到秒的情况下,二者可以视为等同。<br>每年格林尼治天文台会发调时信息,基于UTC。</p>
<h2>四、时区</h2>
<p>随着火车铁路与其他交通和通讯工具的发展,以及全球化贸易的推动,各地使用各自的当地太阳时间带来了时间不统一的问题,在19世纪催生了统一时间标准的需求,时区由此诞生。</p>
<h3>时区是如何定义的</h3>
<p>从格林威治本初子午线起,经度每向东或者向西间隔15°,就划分一个时区,在这个区域内,大家使用同样的标准时间。</p>
<p>但实际上,为了照顾到行政上的方便,常将1个国家或1个省份划在一起。所以时区并不严格按南北直线来划分,而是按自然条件来划分。另外:由于目前,国际上并没有一个批准各国更改时区的机构。一些国家会由于特定原因改变自己的时区。</p>
<p>全球共分为24个标准时区,相邻时区的时间相差一个小时。</p>
<p><img src="/img/remote/1460000022456308" alt="time-timezone.png" title="time-timezone.png"></p>
<p>在不同地区,同一个时区往往会有很多个不同的时区名称,因为名称中通常会包含该国该地区的地理信息。在夏令时期间,当地的时区名称及字母缩写会有所变化(通常会包含“daylight”或“summer”字样)。</p>
<p>例如美国东部标准时间叫:EST,Estern Standard Time;而东部夏令时间叫:EDT,Estern Daylight Time。</p>
<blockquote>想查看世界所有时区的名字可以访问这个网站:<br><a href="https://link.segmentfault.com/?enc=93B7ebDk2Kuy38kVtHz0xQ%3D%3D.pa2kS7zFbqSfhylJ6vOdInP6mn04NHLEB2cd10RcZROQgLbiEi3%2F%2Fm3RhIThLhPh" rel="nofollow">https://www.timeanddate.com/t...</a>
</blockquote>
<h2>四、夏令时</h2>
<h3>什么是夏令时</h3>
<p>DST(Daylight Saving Time),夏令时又称夏季时间,或者夏时制。</p>
<p>它是为节约能源而人为规定地方时间的制度。一般在天亮早的夏季人为将时间提前一小时,可以使人早起早睡,减少照明量,以充分利用光照资源,从而节约照明用电。</p>
<p>全球约40%的国家在夏季使用夏令时,其他国家则全年只使用标准时间。标准时间在有的国家也因此被相应地称为冬季时间。</p>
<p>在施行夏令时的国家,一年里面有一天只有23小时(夏令时开始那一天),有一天有25小时(夏令时结束那一天),其他时间每天都是24小时。</p>
<p><img src="/img/remote/1460000022456309" alt="time-daylight-time.jpg" title="time-daylight-time.jpg"></p>
<p>绿色部分为2019年统计的在全球施行冬夏令时的国家和地区。</p>
<h3>夏令时的历史</h3>
<p>1784年,美国驻法国大使本杰明·富兰克林(Benjamin Franklin)提出“日光节约时间制”。1908年,英国建筑师威廉·维莱特(William Willett)再次提出,但当时该提案并未被采纳。</p>
<p>1916年,处于一战时期的德国政府下令将时钟推至一个小时后,通过获得额外一小时的日光来节省战争所需的煤炭,成为第一个实行夏时制的国家。随后,英法俄美四个一战参战国纷纷效仿。</p>
<p>美国在一战结束后于1919年取消夏时制,但在1942年二战时,美国重新启动夏令时制,1966年正式立法确定永久使用。1973至1975年石油危机爆发期间,美国连续两年延长夏令时制,以节省石油。</p>
<p>欧洲大部分国家则是从1976年——第四次中东战争导致首次石油危机(1973年)的3年后才开始施行夏令时制。</p>
<p>1986年4月,中国国务院办公厅发出《在全国范围内实行夏时制的通知》,要求全民早睡早起节约能源:每年4月中旬的第一个星期日2时,将时钟拨快一小时;10月中旬第一个星期日的2时,再将时钟拨慢一小时。但此夏令时只实行了6年,在1992年停止施行,主因是中国东西地域广阔却只奉行一个北京时间,实时夏令时制带来很多不切实际的反效果。</p>
<h3>夏令时的争议</h3>
<p>从过去的100多年来看,夏令时往往是在国家发生严重危机(如战争和能源短缺)的情况下才会受到青睐。而在相对和平的近10年里,这种时间制度则变得越来越不受欢迎。</p>
<p>它会使得人们的生物钟被扰乱,常常陷入睡眠不足的情况,不仅对人体健康有害、导致车祸,还会对旅游、航空领域造成极大的混乱。</p>
<p>另外,冬、夏令时究竟能否起到节能的作用,也仍有待商榷。美国一项截至2014年3月的研究表明,这种时间转换制度最多能在3、4月帮助美国减少1%的用电量,而美国国家标准局则认为,夏令时对用电量没有丝毫影响。 </p>
<p>在俄罗斯,此前的一份报告也显示,夏令时帮助俄罗斯每年节约的电量,仅相当于两三个火力发电厂的发电量,十分的“鸡肋”。</p>
<p>去年(2019年)3月26日,作为全世界第一个提出并实行夏令时的国家,德国,在欧洲议会上以410比192的赞成票通过了取消冬、夏令时转换制提案,拟定于2021年4月起,所有欧盟国家不再实行冬、夏令时转换。待各成员国形成最终法案后,将选择永久使用夏令时时间或是冬令时时间。</p>
<h2>五、本地时间</h2>
<p>在日常生活中所使用的时间我们通常称之为本地时间。这个时间等于我们所在(或者所使用)时区内的当地时间,它由与世界标准时间(UTC)之间的偏移量来定义。这个偏移量可以表示为 UTC- 或 UTC+,后面接上偏移的小时和分钟数。</p>
<h2>六、JavaScript中的Date</h2>
<p>得到本地时间,在不同时区打印 new Date() ,输出的结果将会不一样:</p>
<pre><code class="javascript">new Date();</code></pre>
<p>得到本地时间距 1970年1月1日午夜(GMT时间)之间的毫秒数:</p>
<pre><code class="javascript">new Date().getTime();</code></pre>
<p>返回本地时间与 GMT 时间之间的时间差,以分钟为单位:</p>
<pre><code class="javascript">new Date().getTimezoneOffset();</code></pre>
<p>如何在任何地方都能正确显示当地时间(只要知道该地的timezone):</p>
<pre><code class="javascript">//目标表时间,东八区
let timezone = 8;
//获取本地时间与格林威治时间的时间差(注意是分钟,记得转换)
const diff = new Date().getTimezoneOffset();
//根据本地时间和时间差获得格林威治时间
const absTime = new Date().getTime() + diff * 60 * 1000;
//根据格林威治时间和各地时区,得到各地时区的时间
let localTime = new Date(absTime + timeZone * 60 * 60 * 1000);
//处理夏令时(isDST为自己封装的处理方法)
if(isDST(localTime, country)) {
localTime = new Date(absTime + (timeZone + 1) * 60 * 60 * 1000);
}
return localTime;</code></pre>
<h2>结语</h2>
<p>以上分别从定义、来源等维度解释和扩展说明了GMT、UTC、时区和夏令时的概念、历史、意义,并在最后列举了这些概念在JS项目中的一个非常实用的应用。</p>
<p>简单地讲, GMT 是以前的世界时间标准;UTC 是现在在使用的世界时间标准;时区是基于格林威治子午线来偏移的,往东为正,往西为负;夏令时是地方时间制度,施行夏令时的地方,每年有2天很特殊(一天只有23个小时,另一天有25个小时)。</p>
<p>从源头上彻底了解了这些概念,将会让我们在处理与时间相关的问题时如虎添翼。</p>
<p><br></p>
<p>文章同时发表于公众号「前端手札」,喜欢的话可以关注一下哦。</p>
<p><img src="/img/remote/1460000021959062" alt="qianduanshouzha-gzh.png" title="qianduanshouzha-gzh.png"></p>
<blockquote>本文作者:ChampYin <br>转载请注明出处:<a href="https://link.segmentfault.com/?enc=oLE0wsVAI7Ln6PbXTcQKSw%3D%3D.cpaSUxeP5kCIE%2F6ypsj5%2BxcICApQ8Udh8Afn93WeyBPxw1z3reMbHyLWgxREDo52ER%2FRumsqm5G3qBwOnTYD%2Fe7OXloz6TB3BpqBBJ56x%2FxC37pvjFj7%2FlfdMkSvCv3BWgSIemlNaCGRxdv14S15TaIr88VjZscplgXGMvsFeuvMidn9%2BzHxjDzNnriS%2Bzyld8NiZYZVf%2BlJKfIZwnUnXw%3D%3D" rel="nofollow">https://champyin.com/2020/04/24/彻底弄懂GMT、UTC、时区和夏令时</a>
</blockquote>
开发一个时间小程序
https://segmentfault.com/a/1190000022393294
2020-04-16T15:26:40+08:00
2020-04-16T15:26:40+08:00
champyin
https://segmentfault.com/u/champyin
16
<h2>前言</h2>
<p>跟异国他乡的朋友们微信聊天的时候,经常面临时差的问题。我每次想要确定对方现在是几点,总是要口算一下,有时忘记具体时差,或者涉及跨天,还得打开浏览器查一下,很不方便。有什么方法可以把朋友们所在城市的时间集中起来随时供自己查看呢?于是想到了微信小程序。找了找市面上的时间小程序,不是功能太杂就是小广告太多,不满意。</p>
<p>为什么不自己动手量身打造一个呢?</p>
<p>行动起来。</p>
<h2>首先快速明确需求</h2>
<p>很简单:</p>
<ol>
<li>需要展示时间的城市初定:加州、纽约,再加北京做对比</li>
<li>需要显示具体的时分秒,和年月日</li>
<li>需要实时变化</li>
<li>在其他国家也能正确展示时间</li>
</ol>
<h2>然后创建项目开撸</h2>
<blockquote>怎么创建和前期的准备就不在这里展开了,相信不少人都熟悉。如果不熟悉小程序开发的可以参考<a href="https://link.segmentfault.com/?enc=%2F7OLjiNceB4lsqMcwzHGcA%3D%3D.HKf9tgcosxW8HRJD6M8h8wwyEJE34ilNyZwXbhSWd%2BXjgESo9l%2Bv28KNwBixQAfOoo31cQVZrPLtQpoA9hhBrA%3D%3D" rel="nofollow">官网</a> 或者我的另一篇文章<a href="https://link.segmentfault.com/?enc=gT6emmWa0H8U1Jg%2BmCTB2w%3D%3D.uiXz%2B2%2BGYWyQwPVaR6ZhSyxJb3FdJ1Zb2kfwDe%2B65ekOksJ0E3VWwPS%2BavORfmN5ZOv695Pl4WSEJXyN6gciuYAtqdtFIdGjlYfAXXb4IEbRSD9dQwY0e8jsCYcaOMdJRPwOFTOLeIzD1y2nghn8z2rAd23fA5p%2BXK256ZXqbLs%3D" rel="nofollow">如何开发微信小程序</a> ,上面有对如何开发小程序的简明扼要的的介绍。</blockquote>
<h3>关键逻辑</h3>
<p>这个小程序的核心是时间的处理。如何得到其他地区的时刻信息?</p>
<p>这还不简单?<br>先获取本地时刻,然后加上或者减去另外一个地点与国内(北京时间)的时差(小时),最多再处理一下跨天的情况,不就得到其他地点的时刻了?</p>
<p>我一开始也是这么想的,做完觉得还挺美,准备提交的时候,突然意识到问题:我时差全是基于北京时间计算的,换在其他国家访问,获取的本地时间已经不是北京时间了,时差应该变才对,写死了时差可还行?!发布一个只能在国内使用的鸡肋时间工具,可不是我的风格!</p>
<p>捣鼓一阵,新方案出炉:</p>
<ol>
<li>想办法获得零时区的时间</li>
<li>获取不同地区与零时区的时差(时区)</li>
<li>用零时区的时间加减与零时区的时差(时区),得到各地的绝对时间</li>
</ol>
<h4>1. 获得零时区的时间</h4>
<p>零时区,也叫中时区,位于英国格林威治本初子午线上。该时区的地方时,叫做格林威治时间,也叫世界时。</p>
<p>我们不能直接获得格林威治时间,但是我们可以获得本地与格林威治的时间差:</p>
<pre><code class="javascript">const diff = new Date().getTimezoneOffset() // 单位为分钟</code></pre>
<p>然后根据本地时间和时间差获得格林威治时间:</p>
<pre><code class="javascript">const absTime = new Date().getTime() + diff * 60 * 1000;</code></pre>
<h4>2. 查询各地时区</h4>
<p>格林威治本初子午线将地球划分为东西两个半球,格林威治本初子午线为零时区,往西依次为西一区到西十一区,往东依次为东一区到东十一区,西十二区和东十二区重合成为东西十二区,一共划分了24个时区,每个时区相差正好是1个小时。</p>
<p>北京是东八区,纽约是西五区,加州是西八区。</p>
<p>完整时区地图:</p>
<p><img src="/img/remote/1460000022393299" alt="timezone-map.jpg" title="timezone-map.jpg"></p>
<h4>3. 计算各地的绝对时间</h4>
<p>东时区的时刻比零时区快,西时区的时刻比零时区慢,所以东时区为正,西时区为负,所有时间计算记得转换为毫秒。</p>
<pre><code class="javascript">let localTime = new Date(absTime + timeZone * 60 * 60 * 1000);</code></pre>
<p>获取任何时区的绝对时间的完整核心代码:</p>
<pre><code class="javascript">/**
* timeZone: 东n区为正,西n区为负, 单位为小时
*/
const getFullTimeInfo = (timeZone, country, spliter) => {
//获取本地时间与格林威治时间的时间差(注意是分钟,记得转换)
const diff = new Date().getTimezoneOffset();
//根据本地时间和时间差获得格林威治时间
const absTime = new Date().getTime() + diff * 60 * 1000;
//根据格林威治时间和各地时区,得到各地时区的时间
let localTime = new Date(absTime + timeZone * 60 * 60 * 1000)
return {
time: formatTime(localTime, spliter)
};
}</code></pre>
<h2>发布</h2>
<p>很快,第一版就完成了。</p>
<p><img src="/img/remote/1460000022393297" alt="world-time-v1.0.0" title="world-time-v1.0.0"></p>
<p>刚开始这个样子略丑,有点裸奔的赶脚。不过第一版最主要是核心功能,简陋的界面只是暂时的。</p>
<p>给当地的朋友检验确定时间展示正确后,提交代码、提交审核,2天后收到审核通过的通知(吐槽腾讯的审核效率?),然后在小程序管理平台点击发布,哦了。</p>
<p>扫描二维码,打开小程序,然后收藏。以后要看时间了,微信主界面向下一拉,打开我的时间工具,一眼就看到想要知道的时间信息,确实比之前便捷多了。功能虽然简单,界面虽然简陋,但是妥妥滴满足我的需求。</p>
<h2>迭代</h2>
<p>用了一阵子,觉得样式啥的还是得丰富丰富,于是花了一些时间做了一次改版,实时时间以时钟效果展示,并且修改了布局,顺便重构了一下代码,便于新增地区。</p>
<p><img src="/img/remote/1460000022393298" alt="world-time-v2.0.0" title="world-time-v2.0.0"></p>
<p>嗯,效果似乎还行~</p>
<h2>改BUG</h2>
<p>前几天跟澳洲的朋友聊天,聊着聊着居然发现了我的程序的一个潜在BUG。</p>
<p>那天是4月4日的早晨(北京时间),我跟朋友吐槽我的一个疑惑:查询悉尼时区为东十区(即与北京相差2小时),但是为啥查询悉尼时间却与北京相差3小时(所以我当时程序中是把悉尼作为东十一区来计算的)。朋友说:是的没错,我们这里现在在使用夏令时,等夏令时结束就恢复2个小时时差了。然后一查,今年澳洲夏令时将在4月5号凌晨3点结束。。。</p>
<p>也就是说,距离这个BUG发作还有不到一天的时间。。。</p>
<p>马上打开电脑,改BUG。。。</p>
<p>根据资料,获得美国和澳大利亚的夏令时规则:</p>
<ul><li>美国</li></ul>
<p>每年的3月第二个星期日02:00:00,时钟向前调整1小时,变为03:00:00,开始夏令时。<br>每年的11月第一个星期日02:00:00,时钟向后调整1小时,变为01:00:00,结束夏令时。</p>
<ul><li>澳大利亚</li></ul>
<p>每年的10月第一个星期日02:00:00,时钟向前调整1小时,变为03:00:00,开始夏令时。<br>每年的4月第一个星期日03:00:00,时钟向后调整1小时,变为02:00:00,结束夏令时。</p>
<blockquote>关于夏令时,也挺有意思,有空我会另开一个篇幅来专门讲述。</blockquote>
<p>将夏令时的判断逻辑加上:</p>
<pre><code class="diff">/**
* timeZone: 东n区为正,西n区为负, 单位为小时
*/
const getFullTimeInfo = (timeZone, country, spliter) => {
//获取本地时间与格林威治时间的时间差(注意是分钟,记得转换)
const diff = new Date().getTimezoneOffset();
//根据本地时间和时间差获得格林威治时间
const absTime = new Date().getTime() + diff * 60 * 1000;
//根据格林威治时间和各地时区,得到各地时区的时间
let localTime = new Date(absTime + timeZone * 60 * 60 * 1000)
+ // 考虑夏令时
+ // judgeDST是我封装好的一个判断夏令时的方法
+ const isDST = judgeDST(localTime, country);
+ if (isDST) {
+ localTime = new Date(absTime + (timeZone + 1) * 60 * 60 * 1000)
+ }
return {
time: formatTime(localTime, spliter).split(':').slice(0,2).join(':'),
isDST
};
}
</code></pre>
<p>有了现在的版本:</p>
<p><img src="/img/remote/1460000022393300" alt="world-time-v2.1.0" title="world-time-v2.1.0"></p>
<p>以后对这个小工具我还会不断优化,会越来越灵活,比如支持地区选择,这样每个人都可以定制自己的时差表了。可以期待一下哦~</p>
<p>最后附上小程序二维码,扫一扫即可体验。</p>
<p><img src="/img/remote/1460000022393301" alt="world-time-qr-code.jpg" title="world-time-qr-code.jpg"></p>
<p>-- <br>还是毛爷爷说得好:自己动手丰衣足食。</p>
<p>Happy coding :)</p>
<p><br></p>
<p>文章同时发表于公众号「前端手札」,喜欢的话可以关注一下哦。</p>
<p><img src="/img/remote/1460000021959062" alt="qianduanshouzha-gzh.png" title="qianduanshouzha-gzh.png"></p>
<blockquote>本文作者:ChampYin <br>转载请注明出处:<a href="https://link.segmentfault.com/?enc=ba4QVcKdn%2FbEFjkwHIT2WQ%3D%3D.Uul1tS%2BdFXsyPfYOJ2FPWXcaf0vPKwGRAAeiz%2BsVh8V6QI9IdMwxL1%2Fax8QEWtvmGETc7PLm7uM37XVKnJZByiittFt4uN1PQ2tgTyiN7cZkecnWKP9QuPlRrXSKeaMw%2Bb6LY09uEWXkz4yHe7tF%2BpZndSiOUKNw6QdDKyxoAFM%3D" rel="nofollow">http://champyin.com/2020/04/08/开发一个时间小程序/</a>
</blockquote>
开发一个Vue插件
https://segmentfault.com/a/1190000021959058
2020-03-09T15:50:30+08:00
2020-03-09T15:50:30+08:00
champyin
https://segmentfault.com/u/champyin
38
<h2>前言</h2>
<p>Vue 项目开发过程中,经常用到插件,比如原生插件 <code>vue-router</code>、<code>vuex</code>,还有 <code>element-ui</code> 提供的 <code>notify</code>、<code>message</code> 等等。这些插件让我们的开发变得更简单更高效。那么 Vue 插件是怎么开发的呢?如何自己开发一个 Vue 插件然后打包发布到npm?</p>
<p>本文涉及技术点:</p>
<ol>
<li>Vue 插件的本质</li>
<li>
<code>Vue.extend()</code> 全局方法</li>
<li>如何手动挂载 <code>Vue</code> 实例</li>
<li>
<code>Vue.use()</code> 的原理</li>
<li>如何打包成 <code>umd</code> 格式</li>
<li>发布前如何测试 <code>npm</code> 包</li>
</ol>
<h2>一、定义</h2>
<p>什么是Vue插件,它和Vue组件有什么区别?来看一下官网的解释:</p>
<blockquote>“插件通常用来为 Vue 添加全局功能。”<br>“组件是可复用的 Vue 实例,且带有一个名字。”<br>—— Vue.js 官网</blockquote>
<p>Emmmm,似乎好像有种朦胧美。。。</p>
<p>我来尝试解释一下,其实, <code>Vue 插件</code> 和 <code>Vue组件</code> 只是在 <code>Vue.js</code> 中包装的两个概念而已,不管是插件还是组件,最终目的都是为了实现逻辑复用。它们的本质都是对代码逻辑的封装,只是封装方式不同而已。在必要时,组件也可以封装成插件,插件也可以改写成组件,就看实际哪种封装更方便使用了。</p>
<p>除此之外,插件是全局的,组件可以全局注册也可以局部注册。</p>
<p>我们今天只聚焦 Vue 插件。</p>
<blockquote>
<p>插件一般有下面几种:</p>
<ul>
<li>添加全局方法或者属性。如: vue-custom-element</li>
<li>添加全局资源:指令/过滤器/过渡等。如 vue-touch</li>
<li>通过全局混入来添加一些组件选项。如 vue-router</li>
<li>添加 Vue 实例方法,通过把它们添加到 Vue.prototype 上实现。</li>
<li>一个库,提供自己的 API,同时提供上面提到的一个或多个功能。如 vue-router</li>
</ul>
<p>—— Vue.js 官网</p>
</blockquote>
<h2>二、插件的使用</h2>
<p>插件需要通过 <code>Vue.use()</code> 方法注册到全局,并且需要在调用 <code>new Vue()</code> 启动应用之前完成。之后在其他 Vue 实例里面就可以通过 <code>this.$xxx</code> 来调用插件中提供的 API 了。</p>
<p>下面以实现一个简易的提示框插件 toast 为例,给大家介绍怎么一步一步开发和发布一个 Vue 插件。</p>
<p>希望达到的效果:<br>在 main.js 中 use:</p>
<pre><code class="javascript">// src/main.js
import Vue from 'vue'
import toast from '@champyin/toast'
Vue.use(toast)</code></pre>
<p>在 App.vue 的生命周期 mounted 方法里调用 this.$toast():</p>
<pre><code class="vue">// src/App.vue
<template>
<div>
<button @click='handleClick'>Toast</button>
</div>
</template>
<script>
export default {
name: 'demo',
methods: {
handleClick() {
this.$toast({
type: 'success',
msg: '成功',
duration: 3
})
}
}
}
</script></code></pre>
<p>运行后在页面上点击按钮,弹出 <code>成功</code> 的提示,然后3秒后消失。</p>
<p><img src="/img/remote/1460000021959061" alt="how-to-write-a-vue-plugin01.jpg" title="how-to-write-a-vue-plugin01.jpg"></p>
<p>在线体验地址:<a href="https://link.segmentfault.com/?enc=uXZATYQgKtqUdDxMwscY%2Fg%3D%3D.ceW3473f04itq5hj%2FA4DYxwWVWjxXg2Gsj6u8RYrIn4%3D" rel="nofollow">http://champyin.com/toast/</a></p>
<h2>三、插件开发</h2>
<h3>1. 编写 toast 的本体。</h3>
<p>在 Vue 项目(你可以使用 Vue-cli 快速生成一个 Vue 项目,也可以自己用 webpack 搭建一个)的 src 目录下创建 components/Toast/index.vue 文件。</p>
<pre><code class="vue">// src/components/Toast/index.vue
<template>
<transition name='fade'>
<div class='uco-toast' v-if='isShow'>
<span :class='iconStyle'></span>
<span>{{msg}}</span>
</div>
</transition>
</template>
<script>
export default {
data() {
return {
isShow: false,
type: 'success',
msg: '成功',
duration: 1,
};
},
computed: {
iconStyle() {
return `tfont icon-${this.type} toast-icon`;
},
},
mounted() {
this.isShow = true;
setTimeout(() => {
this.isShow = false;
}, this.duration * 1000);
},
};
</script>
<style lang='less' scoped>
// 样式略
</style></code></pre>
<p>现在 toast 本体完成了,但是它里面的数据目前没法改变,因为我没有给它定义 props 属性。这不是 bug,而是,插件并不是通过 pops 来传值的。</p>
<h3>2. 手动挂载 toast 实例的 dom</h3>
<p>为了给插件传值,可以利用基础 Vue 构造器 <code>Vue.extend()</code> 创建一个“子类”。这个子类相当于一个继承了 Vue 的 Toast 构造器。然后在 new 这个构造函数的时候,给 Toast 的 data 属性传值,然后手动调用这个实例的 <code>$mount()</code> 方法手动挂载,最后使用原生JS的 appendChild 将真实 DOM (通过实例上的 <code>$el</code> 属性获取)添加到 body 上。</p>
<p>在 src 目录下新建 components/Toast/index.js 文件:</p>
<pre><code class="javascript">// src/components/Toast/index.js
import Vue from 'vue';
import Toast from './index.vue';
// 使用 Vue.extend() 创建 Toast 的构造器
const ToastConstructor = Vue.extend(Toast);
const toast = function(options = {}) {
// 创建 Toast 实例,通过构造函数传参,
// 并调用 Vue 实例上的 $mount() 手动挂载
const toastInstance = new ToastConstructor({
data: options
}).$mount();
// 手动把真实 dom 挂到 html 的 body 上
document.body.appendChild(toastInstance.$el);
return toastInstance;
};
// 导出包装好的 toast 方法
export default toast;</code></pre>
<h3>3. 暴露 install 方法给 Vue.use() 使用。</h3>
<blockquote>为了支持 Vue.use(),Vue.js 的插件应该暴露一个 install 方法。这个方法的第一个参数是 Vue 构造器,第二个参数是一个可选的选项对象。<br>—— Vue.js 官网</blockquote>
<p>通过 <a href="https://link.segmentfault.com/?enc=SmeJQ28PkvxBVAOcu65Qgg%3D%3D.wCw9xa4Jb4pWzORWSAN%2BcwDM%2F7BD0tH0LkiapqUT%2FpfNH5L9gDdUuBDfKI%2F7FNMTwqWdrT4BSP6P%2BhAtKnWepUcnW2lY%2FlvnY24%2BKJFLrMo%3D" rel="nofollow">Vue.js 源码</a>也可以看出,Vue.use() 方法所做的事情就是调用插件或者组件的 install 方法,然后把全局 Vue 传进去供插件和组件使用。</p>
<pre><code class="javascript">// https://github.com/vuejs/vue/blob/dev/src/core/global-api/use.js
/* @flow */
import { toArray } from '../util/index'
export function initUse (Vue: GlobalAPI) {
Vue.use = function (plugin: Function | Object) {
const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))
if (installedPlugins.indexOf(plugin) > -1) {
return this
}
// additional parameters
const args = toArray(arguments, 1)
args.unshift(this)
if (typeof plugin.install === 'function') {
plugin.install.apply(plugin, args)
} else if (typeof plugin === 'function') {
plugin.apply(null, args)
}
installedPlugins.push(plugin)
return this
}
}</code></pre>
<p>在 src 目录下新建 components/index.js 文件,定义一个 install 方法,在里面将 toast 实例放到 Vue.prototype 上作为 Vue 实例的方法暴露到全局。</p>
<pre><code class="javascript">// src/components/index.js
import toast from './Toast/index';
import '../icon/iconfont.css';
// 准备好 install 方法 给 Vue.use() 使用
const install = function (Vue) {
if (install.installed) return;
install.installed = true;
// 将包装好的 toast 挂到Vue的原型上,作为 Vue 实例上的方法
Vue.prototype.$toast = toast;
}
// 默认导出 install
export default {
install,
};</code></pre>
<p>现在插件就开发完成了,可以在当前项目中本地引用这个插件了。</p>
<pre><code class="javascript">//在 main.js 中
import toast from src/components/index.js;
Vue.use(toast);
//在 App.vue 中
handleClick(){
this.$toast();
}</code></pre>
<h2>四、发布到npm</h2>
<p>为了方便其他人也可以使用到这个插件,我们可以把它发布到 npm 上去。发布的步骤很简单,但是发布之前,需要有一些小配置和一些注意的地方。</p>
<h3>1. 打包配置</h3>
<p>首先我们要把它打包成可以给浏览器解析的 UMD 格式的的模块,并且去掉对 Vue.js 的打包,这样别人在 Vue 项目中使用这个插件的时候就不会有两份 Vue 或者出现 Vue 版本冲突的问题,以保证可以更好被独立引用。</p>
<p>如果你是用 Vue-cli 生成的项目,那只需要在你的 npm 脚本中配置一下库的打包命令:</p>
<pre><code class="json">// package.json
"build:lib": "vue-cli-service build --target lib --name toast --dest lib src/components/index.js"</code></pre>
<p>命令说明:</p>
<pre><code>--target:构建的目标
targetType 有三个选项:lib | wc | wc-async
lib:库
wc:web component
wc-async:异步的 web component
--name:库或组件的名字
当入口为单一文件时,name为库或组件的文件名
当入口为global表达式时,name为每个库或组件文件名字的前缀
[entry]:打包入口
可以是.vue文件,也可以是.js文件
当注册多个web component时,入口可以是一个global表达式,如 components/*.vue
--dest:输出目录
默认为dist目录,也可以修改为自定义的目录</code></pre>
<p>然后运行 <code>npm run build:lib</code>,即可在 lib 目录下生成如下文件:</p>
<pre><code>toast.umd.js 一个直接给浏览器或者AMD loader 使用的 UMD 包
toast.umd.min.js 一个压缩版 UMD 构建版本
toast.common.js 一个给打包器用的CommonJS包</code></pre>
<p>如果你是用 webpack 搭建的 Vue 项目,那就需要在 webpck 中配置一下 output.libraryTarget 等属性:</p>
<pre><code class="javascript">// build/webpack.lib.conf.js
const path = require('path');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
module.exports = {
mode: 'production',
entry: './src/components/index.js',
output: {
path: path.resolve(__dirname, '../lib'),
filename: 'toast.js',
library: 'toast',
libraryTarget: 'umd',
libraryExport: 'default',
umdNamedDefine: true,
globalObject: 'typeof self !== \'undefined\' ? self : this',
},
externals: {
vue: {
root: 'Vue',
commonjs: 'vue',
commonjs2: 'vue',
amd: 'vue',
},
},
plugins: [
new CleanWebpackPlugin(),
],
};</code></pre>
<p>然后运行 <code>npm run build:lib</code>,即可在 lib 目录下生成如下文件:</p>
<pre><code>toast.js 直接给浏览器或者AMD loader 使用的 UMD 包</code></pre>
<h3>2. 发布前的测试</h3>
<p>发布前,我们需要配置一下 package.json 里的 <code>name</code> 和 <code>main</code> 字段:<br><code>name</code> 的值是最终包的名字,<code>install</code> 和 <code>import</code> 的就是这个名字(请确保全网唯一)。<br><code>main</code> 的值是包的入口文件路径(相对当前文件的路径),一定要填写正确,否则包无法被引用。</p>
<pre><code class="json">"name": "@champyin/toast",
"main": "lib/toast.js",</code></pre>
<p>为了确保包的配置没有问题,我们可以利用 <code>npm link</code> 命令在本地测试一下包的使用情况。使用npm link测试包的使用估计很多人都会,就不赘述了。如果有需要可以看我的另一篇中文章<a href="https://link.segmentfault.com/?enc=7jogXMq32n4hPKGKxatmRw%3D%3D.5YjMaL%2FQyf039JxqdgEFnjN%2B2cHcJ1OTw%2BjEV18Y7O7viMnDAcxyJntosR4TY30E8T%2B%2FI4mNnrkFY%2FWyAvAjNA%3D%3D" rel="nofollow">npm link详解</a>。</p>
<p>这个时候,我们其实就可以发布了,但是为了防止把不必要的文件发布出去(比如测试用例和demo)浪费人家下载的流量,我们最好是建一个 <code>.npmigore</code> 文件,语法跟 <code>.gitignore</code> 相同。</p>
<h3>3. 发布</h3>
<p>发布的方法很简单(不过首先你要有个 npm 账号),在 package.json 所在的目录下执行这两句就可以了:</p>
<pre><code>npm add user
npm publish</code></pre>
<p>关于更详细的发布教程,我在另一篇文章有专门细说:<a href="https://link.segmentfault.com/?enc=UuRkT15ZtV%2Fi%2BEQD%2FkXL%2Bw%3D%3D.d9%2FqjRGJdkmek5kbWFFxfCvI%2FSTDKOGVmgpUpDzlGThb8o%2F1oSn%2FabKeekjBzN88aozLAe7i4i24%2FLRVDvw6PGqNWjlddSbxxQG8tIFxtk00uhawl7JRlQM6p8QsyaGZXPBgepvQBVgDd68efNg%2BBQ%3D%3D" rel="nofollow">如何发布一个npm模块</a>。</p>
<h3>4. 安装测试</h3>
<p>其实到了这一步一99.99%是不会出错了,安装一遍只是为了那 0.01% 的万一。</p>
<p>在另一个 Vue 项目里(注意不能在开发toast的项目里哈),从 npm 安装自己刚才发布的包:</p>
<pre><code>npm i -D @champyin/toast</code></pre>
<p>然后在项目中使用一下自己的插件,点击按钮就会弹出 toast 小提示了。</p>
<pre><code>//在 main.js 中
import toast from '@champyin/toast';
Vue.use(toast);
//在 App.vue 中
handleClick(){
this.$toast();
}</code></pre>
<p>项目体验地址:<a href="https://link.segmentfault.com/?enc=AcbGHukmLzSt%2B6OkzhvPIw%3D%3D.IHxJvGsv%2Bu9YkE%2F1e16qjj5%2FvF3Jy1zNrwjuhra7edY%3D" rel="nofollow">http://champyin.com/toast/</a> <br>npm 地址:<a href="https://link.segmentfault.com/?enc=XKtH%2B0J6hwt5EuN5dLvXNQ%3D%3D.mlvANQKDB33DXXPq2nGu89KVEHBBv%2BvO2Mb2e7ZlfqD0sraBaOX8oPqI3211F78b" rel="nofollow">https://www.npmjs.com/package/@champyin/toast</a> <br>欢迎给我提 issue:<a href="https://link.segmentfault.com/?enc=GgtgnnK11nkMPcVDX43u%2BQ%3D%3D.58HAqZyMhDRJLx7zliheNHI%2BQRVw%2F2oZfYY90gxJ1k3fS2QZwoawSmVCZhtHGFGC" rel="nofollow">https://github.com/yc111/toast/issues</a> </p>
<p>Happy coding :)</p>
<p>文章同时发表于公众号「前端手札」,喜欢的话可以关注一下哦。</p>
<p><img src="/img/remote/1460000021959062" alt="qianduanshouzha-gzh.png" title="qianduanshouzha-gzh.png"></p>
<p>更多参考: <br><a href="https://link.segmentfault.com/?enc=hqhOhlanC7y4uzKb4N7HNw%3D%3D.Mw55Lev2iDFweT17CAbwVqyPHKYaaSrW1hyEfRfovOgCZemuoWfxfzOcsldXt7dz" rel="nofollow">https://cn.vuejs.org/v2/guide...</a> <br><a href="https://link.segmentfault.com/?enc=04klpn8q2mNOAQWDH5FvAA%3D%3D.AugAprR37BX%2B8EuG4tOx4OdvvMNhvujC%2BCWfDTc81zntdMCB1cefeKK48ryErB3E" rel="nofollow">https://cn.vuejs.org/v2/api/#...</a> <br><a href="https://link.segmentfault.com/?enc=4fLTahND0Ez4dHQhDYf%2BoQ%3D%3D.ACjDPm93BhCYmBIySf%2F7g%2FHpuebVKRMPuOUdoj9FzpzFBKhy%2Bn7Exk96t6EUowI4dGYNO4UhcQrvzgOz3bHMJw%3D%3D" rel="nofollow">https://cli.vuejs.org/zh/guid...</a> <br><a href="https://link.segmentfault.com/?enc=Lvsh1bGWdye6RUPbfsvX7g%3D%3D.5WSSipQ8y4XzhVkYSh72usumAHJaVwtwZPKEQFHbJB80nLFIJI6ZB0nOPt5yrcG5" rel="nofollow">https://webpack.js.org/guides...</a> <br><a href="https://link.segmentfault.com/?enc=4iL96lnW4MkEvpuvpgkmGQ%3D%3D.xj4jf9sHUTIULCZoymj2Lr5CsvpdCD%2BukSIiEXiDdrA1vm6diyidxt9VNmEU94jf" rel="nofollow">https://docs.npmjs.com/cli-co...</a> <br><a href="https://link.segmentfault.com/?enc=L6xxiuc1lpuC1%2BYExFAVZQ%3D%3D.LN59EATbnxe8x%2BiCtwaIniBDdZShSbsgX2zoshc9%2BFegnF6sxaWVGz9x3b6uHXm3" rel="nofollow">https://docs.npmjs.com/cli-co...</a> <br><a href="https://link.segmentfault.com/?enc=t7h9ofW8OQO2Nn4ZiOrb8Q%3D%3D.2LbIje5qehXf68xCkH7qIc65LuqE1QUQJD2h7MaZyaMLHtbIq3dkW1fTP6pIjGjH" rel="nofollow">https://www.npmjs.com/package...</a> </p>
<p><br></p>
<blockquote>本文作者:ChampYin <br>转载请注明出处:<a href="https://link.segmentfault.com/?enc=4oa5xy83jiy1nNi7wbJTSw%3D%3D.h1ebaYtbsbvP2Qpu%2BNcZVgV1yuRVdRuNVk6FX%2FzBW29CA5G7HGhcnNXvgOkW%2FdBuXWdKHYCQPsgwP0OPOus2bJL0yhSC%2BVEh8yR7WothtRGr1becnfI21DG%2F6ohGnMvH" rel="nofollow">http://champyin.com/2020/03/05/开发一个Vue插件/</a>
</blockquote>
揭秘webpack loader
https://segmentfault.com/a/1190000021657031
2020-01-29T14:31:52+08:00
2020-01-29T14:31:52+08:00
champyin
https://segmentfault.com/u/champyin
7
<h2>前言</h2>
<p>Loader(加载器) 是 webpack 的核心之一。它用于将不同类型的文件转换为 webpack 可识别的模块。本文将尝试深入探索 webpack 中的 loader,揭秘它的工作原理,以及如何开发一个 loader。</p>
<h2>一、Loader 工作原理</h2>
<p>webpack 只能直接处理 javascript 格式的代码。任何非 js 文件都必须被预先处理转换为 js 代码,才可以参与打包。loader(加载器)就是这样一个代码转换器。它由 webpack 的 <code>loader runner</code> 执行调用,接收原始资源数据作为参数(当多个加载器联合使用时,上一个loader的结果会传入下一个loader),最终输出 javascript 代码(和可选的 source map)给 webpack 做进一步编译。</p>
<h2>二、 Loader 执行顺序</h2>
<h3>1. 分类</h3>
<ul>
<li>pre: 前置loader</li>
<li>normal: 普通loader</li>
<li>inline: 内联loader</li>
<li>post: 后置loader</li>
</ul>
<h3>2. 执行优先级</h3>
<ul>
<li>4类 loader 的执行优级为:<code>pre > normal > inline > post</code> 。</li>
<li>相同优先级的 loader 执行顺序为:<code>从右到左,从下到上</code>。</li>
</ul>
<h3>3. 前缀的作用</h3>
<p>内联 loader 可以通过添加不同前缀,跳过其他类型 loader。</p>
<ul>
<li>
<code>!</code> 跳过 normal loader。</li>
<li>
<code>-!</code> 跳过 pre 和 normal loader。</li>
<li>
<code>!!</code> 跳过 pre、 normal 和 post loader。</li>
</ul>
<p>这些前缀在很多场景下非常有用。</p>
<h2>三、如何开发一个loader</h2>
<p>loader 是一个导出一个函数的 node 模块。</p>
<h3>1. 最简单的 loader</h3>
<p>当只有一个 loader 应用于资源文件时,它接收源码作为参数,输出转换后的 js 代码。</p>
<pre><code class="javascript">// loaders/simple-loader.js
module.exports = function loader (source) {
console.log('simple-loader is working');
return source;
}</code></pre>
<p>这就是一个最简单的 loader 了,这个 loader 啥也没干,就是接收源码,然后原样返回,为了证明这个loader被调用了,我在里面打印了一句话‘simple-loader is working’。</p>
<p>测试这个 loader:<br><strong>需要先配置 loader 路径</strong><br>若是使用 npm 安装的第三方 loader,直接写 loader 的名字就可以了。但是现在用的是自己开发的本地 loader,需要我们手动配置路径,告诉 webpack 这些 loader 在哪里。</p>
<pre><code class="javascript">// webpack.config.js
const path = require('path');
module.exports = {
entry: {...},
output: {...},
module: {
rules: [
{
test: /\.js$/,
// 直接指明 loader 的绝对路径
use: path.resolve(__dirname, 'loaders/simple-loader')
}
]
}
}</code></pre>
<blockquote>如果觉得这样配置本地 loader 并不优雅,可以在 <a href="https://link.segmentfault.com/?enc=0zrVaCWWvbOvWM%2ByqBj8IA%3D%3D.K2SbgfAuo5FZZUOks6DvfuKzX03YO49eRNvbBVwgFZhKI6h80GDSQd1q1YSnFRh5tb2rWtnvTL%2FTI3njvhjOB%2B83BuVA4s1l%2BkMve7Ngd%2Fh0b2V7GzfgC4yZUDGr4v4%2FX7CalgWSKLm2aPswtbIiIy9iUXaqdbGvgQKUOVR4hOY%3D" rel="nofollow">webpack配置本地loader的四种方法</a> 中挑一个你喜欢的。</blockquote>
<p><strong>执行webpack编译</strong><br>可以看到,控制台输出 ‘simple-loader is working’。说明 loader 成功被调用。</p>
<p><img src="/img/remote/1460000021657035" alt="webpack-loader1.jpg" title="webpack-loader1.jpg"></p>
<h3>2. 带 pitch 的 loader</h3>
<p><code>pitch</code> 是 loader 上的一个方法,它的作用是阻断 loader 链。</p>
<pre><code class="javascript">// loaders/simple-loader-with-pitch.js
module.exports = function (source) {
console.log('normal excution');
return source;
}
// loader上的pitch方法,非必须
module.exports.pitch = function() {
console.log('pitching graph');
// todo
}</code></pre>
<p>pitch 方法不是必须的。如果有 pitch,loader 的执行则会分为两个阶段:<code>pitch</code> 阶段 和 <code>normal execution</code> 阶段。webpack 会先从左到右执行 loader 链中的每个 loader 上的 pitch 方法(如果有),然后再从右到左执行 loader 链中的每个 loader 上的普通 loader 方法。</p>
<p>假如配置了如下 loader 链:</p>
<pre><code class="javascript">use: ['loader1', 'loader2', 'loader3']</code></pre>
<p>真实的 loader 执行过程是:</p>
<p><img src="/img/remote/1460000021657034" alt="webpack-loader-flow-with-pitch.png" title="webpack-loader-flow-with-pitch.png"></p>
<p>在这个过程中如果任何 pitch 有返回值,则 loader 链被阻断。webpack 会跳过后面所有的的 pitch 和 loader,直接进入上一个 loader 的 <code>normal execution</code>。</p>
<p>假设在 loader2 的 pitch 中返回了一个字符串,此时 loader 链发生阻断:</p>
<p><img src="/img/remote/1460000021657038" alt="webpack-loader-flow-with-pitch2.png" title="webpack-loader-flow-with-pitch2.png"></p>
<h3>3. 写一个简版的 style-loader</h3>
<p>style-loader 通常不会独自使用,而是跟 css-loader 连用。css-loader 的返回值是一个 js 模块,大致长这样:</p>
<pre><code class="javascript">// 打印 css-loader 的返回值
// Imports
var ___CSS_LOADER_API_IMPORT___ = require("../node_modules/css-loader/dist/runtime/api.js");
exports = ___CSS_LOADER_API_IMPORT___(false);
// Module
exports.push([module.id, "\nbody {\n background: yellow;\n}\n", ""]);
// Exports
module.exports = exports;</code></pre>
<p>这个模块在运行时上下文中执行后返回 <code>css</code> 代码 <code>"\nbody {\n background: yellow;\n}\n"</code>。</p>
<p>style-loader 的作用就是将这段 <code>css</code> 代码转成 <code>style</code> 标签插入到 <code>html</code> 的 <code>head</code> 中。</p>
<h4>设计思路</h4>
<ol>
<li>style-loader 最终需返回一个 <code>js</code> 脚本:在脚本中创建一个 <code>style</code> 标签,将 <code>css</code> 代码赋给 <code>style</code> 标签,再将这个 <code>style</code> 标签插入 <code>html</code> 的 <code>head</code> 中。</li>
<li>难点是获取 <code>css</code> 代码,因为 css-loader 的返回值只能在运行时的上下文中执行,而执行 loader 是在编译阶段。换句话说,css-loader 的返回值在 style-loader 里派不上用场。</li>
<li>曲线救国方案:使用获取 <code>css</code> 代码的表达式,在运行时再获取 css (类似 <code>require('css-loader!index.css')</code>)。</li>
<li>在处理 css 的 loader 中又去调用 <code>inline loader</code> require <code>css</code> 文件,会产生循环执行 loader 的问题,所以我们需要利用 <code>pitch</code> 方法,让 style-loader 在 <code>pitch</code> 阶段返回脚本,跳过剩下的 loader,同时还需要内联前缀 <code>!!</code> 的加持。</li>
</ol>
<blockquote>注:pitch 方法有3个参数:</blockquote>
<ul>
<li>remainingRequest:loader链中排在自己后面的 loader 以及资源文件的绝对路径以<code>!</code>作为连接符组成的字符串。</li>
<li>precedingRequest:loader链中排在自己前面的 loader 的绝对路径以<code>!</code>作为连接符组成的字符串。</li>
<li>data:每个 loader 中存放在上下文中的固定字段,可用于 pitch 给 loader 传递数据。</li>
</ul>
<blockquote>可以利用 <code>remainingRequest</code> 参数获取 loader 链的剩余部分。</blockquote>
<h4>实现</h4>
<pre><code class="javascript">// loaders/simple-style-loader.js
const loaderUtils = require('loader-utils');
module.exports = function(source) {
// do nothing
}
module.exports.pitch = function(remainingRequest) {
console.log('simple-style-loader is working');
// 在 pitch 阶段返回脚本
return (
`
// 创建 style 标签
let style = document.createElement('style');
/**
* 利用 remainingRequest 参数获取 loader 链的剩余部分
* 利用 ‘!!’ 前缀跳过其他 loader
* 利用 loaderUtils 的 stringifyRequest 方法将模块的绝对路径转为相对路径
* 将获取 css 的 require 表达式赋给 style 标签
*/
style.innerHTML = require(${loaderUtils.stringifyRequest(this, '!!' + remainingRequest)});
// 将 style 标签插入 head
document.head.appendChild(style);
`
)
}</code></pre>
<p>一个简易的 style-loader 就完成了。</p>
<h4>试用</h4>
<p>webpack 配置</p>
<pre><code class="javascript">// webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: {...},
output: {...},
// 手动配置 loader 路径
resolveLoader: {
modules: [path.resolve(__dirname, 'loaders'), 'node_modules']
},
module: {
rules: [
{
// 配置处理 css 的 loader
test: /\.css$/,
use: ['simple-style-loader', 'css-loader']
}
]
},
plugins: [
// 渲染首页
new HtmlWebpackPlugin({
template: './src/index.html'
})
]
}</code></pre>
<p>在 index.js 中引入一个 css 样式文件</p>
<pre><code class="javascript">// src/index.js
require('./index.css');
console.log('Brovo!');</code></pre>
<p>样式文件中将 body 的背景色设置为黄色</p>
<pre><code class="css">// src/index.css
body {
background-color: yellow;
}</code></pre>
<p>执行webpack</p>
<pre><code class="shell">npm run build</code></pre>
<p>可以看到命令行控制台打印了 'simple-style-loader is working',说明 webpack 成功调用了我们编写的 loader。</p>
<p><img src="/img/remote/1460000021657036" alt="webpack-loader3.jpg" title="webpack-loader3.jpg"></p>
<p>在浏览器打开 dist 下的 index.html 页面,可以看到样式生效,而且成功插入到了页面头部!</p>
<p><img src="/img/remote/1460000021657037" alt="webpack-loader2.jpg" title="webpack-loader2.jpg"></p>
<p>说明我们编写的 loader 发挥作用了。</p>
<p>成功!</p>
<h2>三、一些 tips</h2>
<h3>推荐2个工具包</h3>
<p>开发 loader 必备:</p>
<p><strong>1. <a href="https://link.segmentfault.com/?enc=DbK82UnqKTVfzqYRGPgsgQ%3D%3D.GS6Uz7mdpDn4go5ELS4q4bjMA50x9ZfhWFteMJuoevWAuzhfxCaRko69q7oIDkAN" rel="nofollow">loader-utils</a></strong><br>这个模块中常用的几个方法:</p>
<ul>
<li>getOptions 获取 loader 的配置项。</li>
<li>interpolateName 处理生成文件的名字。</li>
<li>stringifyRequest 把绝对路径处理成相对根目录的相对路径。</li>
</ul>
<p><strong>2. <a href="https://link.segmentfault.com/?enc=l35MuCUMmxnfUJRS3CVyXA%3D%3D.G%2Fdxdp9X6tLPKfyjTtxF4koLm%2B%2BvsFECJiW%2BwZUAas5B3qLZB2AEG0wufzhisgq4" rel="nofollow">schema-utils</a></strong><br>这个模块可以帮你验证 loader option 配置的合法性。<br>用法:</p>
<pre><code class="javascript">// loaders/simple-loader-with-validate.js
const loaderUtils = require('loader-utils');
const validate = require('schema-utils');
module.exports = function(source) {
// 获取 loader 配置项
let options = loaderUtils.getOptions(this) || {};
// 定义配置项结构和类型
let schema = {
type: 'object',
properties: {
name: {
type: 'string'
}
}
}
// 验证配置项是否符合要求
validate(schema, options);
return source;
}</code></pre>
<p>当配置项不符合要求,编译就会中断并在控制台打印错误信息:</p>
<p><img src="/img/remote/1460000021657039" alt="webpack-loader4.jpg" title="webpack-loader4.jpg"></p>
<h3>开发异步 loader</h3>
<p>异步 loader 的开发(例如里面有一些需要读取文件的操作的时候),需要通过 this.async() 获取异步回调,然后手动调用它。<br>用法:</p>
<pre><code class="javascript">// loaders/simple-async-loader.js
module.exports = function(source) {
console.log('async loader');
let cb = this.async();
setTimeout(() => {
console.log('ok');
// 在异步回调中手动调用 cb 返回处理结果
cb(null, source);
}, 3000);
}</code></pre>
<blockquote>注: 异步回调 cb() 的第一个参数是 <code>error</code>,要返回的结果放在第二个参数。</blockquote>
<h3>raw loader</h3>
<p>如果是处理图片、字体等资源的 loader,需要将 loader 上的 raw 属性设置为 true,让 loader 支持二进制格式资源(webpack默认是以 <code>utf-8</code> 的格式读取文件内容给 loader)。<br>用法:</p>
<pre><code class="javascript">// loaders/simple-raw-loader.js
module.exports = function(source) {
// 将输出 buffer 类型的二进制数据
console.log(source);
// todo handle source
let result = 'results of processing source'
return `
module.exports = '${result}'
`;
}
// 告诉 wepack 这个 loader 需要接收的是二进制格式的数据
module.exports.raw = true;</code></pre>
<p><img src="/img/remote/1460000021657040" alt="webpack-loader5.jpg" title="webpack-loader5.jpg"></p>
<blockquote>注:通常 raw 属性会在有文件输出需求的 loader 中使用。</blockquote>
<h3>输出文件</h3>
<p>在开发一些处理资源文件(比如图片、字体等)的 loader 中,需要拷贝或者生成新的文件,可以使用内部的 <code>this.emitFile()</code> 方法.<br>用法:</p>
<pre><code class="javascript">// loaders/simple-file-loader.js
const loaderUtils = require('loader-utils');
module.exports = function(source) {
// 获取 loader 的配置项
let options = loaderUtils.getOptions(this) || {};
// 获取用户设置的文件名或者制作新的文件名
// 注意第三个参数,是计算 contenthash 的依据
let url = loaderUtils.interpolateName(this, options.filename || '[contenthash].[ext]', {content: source});
// 输出文件
this.emitFile(url, source);
// 返回导出文件地址的模块脚本
return `module.exports = '${JSON.stringify(url)}'`;
}
module.exports.raw = true;</code></pre>
<p><img src="/img/remote/1460000021657041" alt="webpack-loader6.jpg" title="webpack-loader6.jpg"></p>
<blockquote>在这个例子中,loader 读取图片内容(buffer),将其重命名,然后调用 <code>this.emitFile()</code> 输出到指定目录,最后返回一个模块,这个模块导出重命名后的图片地址。于是当 <code>require</code> 图片的时候,就相当于 require 了一个模块,从而得到最终的图片路径。(这就是 file-loader 的基本原理)</blockquote>
<h3>开发约定</h3>
<p>为了让我们的 loader 具有更高的质量和复用性,记得保持简单。也就是尽量保持让一个 loader 专注一件事情,如果发现你写的 loader 比较庞大,可以试着将其拆成几个 loader 。</p>
<p>在 webpack 社区,有一份 loader 开发准则,我们可以去参考它来指导我们的 loader 设计:</p>
<ul>
<li>保持简单。</li>
<li>利用多个loader链。</li>
<li>模块化输出。</li>
<li>确保loader是无状态的。</li>
<li>使用 loader-utils 包。</li>
<li>标记加载程序依赖项。</li>
<li>解析模块依赖关系。</li>
<li>提取公共代码。</li>
<li>避免绝对路径。</li>
<li>使用 peerDependency 对等依赖项。</li>
</ul>
<h2>四、总结</h2>
<ol>
<li>loader 的本质是一个 node 模块,这个模块导出一个函数,这个函数上可能还有一个 pitch 方法。</li>
<li>了解了 loader 的本质和 loader 链的执行机制,其实就已经具备了 loader 开发基础了。</li>
<li>开发 loader 不难上手,但是要开发一款高质量的 loader,仍需不断实践。</li>
<li>尝试自己开发维护一个小 loader 吧~ 没准以后可以通过自己编写 loader 来解决项目中的一些实际问题。</li>
</ol>
<p>文章源码获取:<a href="https://link.segmentfault.com/?enc=o8QotHDWQHIk08oZ5ps7sg%3D%3D.GggNbnWRhPcAs3ETue6%2ByVCV2slDgscgv3pORRGtakRpRfmgU044nLuSDu9hJWTx" rel="nofollow">https://github.com/yc111/webp...</a></p>
<p>欢迎交流~</p>
<p>Happy New Year!</p>
<p>--</p>
<p>参考 <br><a href="https://link.segmentfault.com/?enc=moppkMkG8fluwEIPoEGG1A%3D%3D.5X3FgljpyzdRSvFxdti6y1Wizylv2a9NHTpinfZ4bjQUqw604DoTfDbF19KJ635Q" rel="nofollow">https://webpack.js.org/concep...</a> <br><a href="https://link.segmentfault.com/?enc=l6ROL4H%2FDXiHUtSWw7XUyA%3D%3D.PTNfuFQFMfQ%2FpiSi19FJ1Uvj%2FC7JFBchH4BYVIEmbuddnEJPTo6gs8qvsXaWHJY3" rel="nofollow">https://webpack.js.org/api/lo...</a> <br><a href="https://link.segmentfault.com/?enc=aeHQbw888Gw%2B9P1QzVoyhw%3D%3D.NXTt3juDdp3LskfSy9%2ByjxqlhPSrte9zobrA3EXgYKs2MTAzMmy4JR37dTXLxskp5lEqE4bx6raDrKboLCqh6w%3D%3D" rel="nofollow">https://webpack.js.org/contri...</a> <br><a href="https://link.segmentfault.com/?enc=pNTqFZ6DaD%2BYPDsHYAQaSw%3D%3D.aNGpVOaqTxP54dYLpBtjahWcWrRgzbkfVvQgJXmes6m1yEyeBaFJDx1mqASZw9Ywaek8Tf6Bv3b2sVlLlZh6v0sZLNHc4yni47R6GkdvEXY%3D" rel="nofollow">https://github.com/webpack/we...</a> <br><a href="https://link.segmentfault.com/?enc=ZLR%2Fr3IUjvM%2F4gSyKzBnFg%3D%3D.Ra2ac7bV%2FpTYtlsT7lkCjV3kYPgTH%2FBCLodSPDkHO3Hi0L9s4so5sZWIZsgCaJztL2xHWbke7ymhTgGX5VGl00tB41dFsOiWYu4Pv0Hbzpc%3D" rel="nofollow">https://github.com/webpack-co...</a> <br><a href="https://link.segmentfault.com/?enc=qjA2aaxk9a2sornLiF%2BBtQ%3D%3D.IZa526GIdnU84h%2BVUHFxBP1ro1WzjmXZvJlqx08Rkyc91KA4DlaBavr58aM2hREC" rel="nofollow">https://www.npmjs.com/package...</a> <br><a href="https://link.segmentfault.com/?enc=dUmMAMpaz8MzedXQPup3pA%3D%3D.BjFpSAE4VZGDuWS9cdsiojLullml1XvGsHnxv3ddxi8UoD03qYAY%2FHMEmlax4kpo" rel="nofollow">https://www.npmjs.com/package...</a> </p>
<p>欢迎转载,转载请注明出处: <br><a href="https://link.segmentfault.com/?enc=Ss5YJUlJKyQJcvRAeM2m%2Fg%3D%3D.9vWfMbDJl6XyMcIh%2B1DXfirUDR7XhSS6SJ7%2F1AFd%2BYLej2zrfJ2qSi1rxx20237%2FHyvBEaPnZfsicAgFaoYxHubcRotFrkbqXjoSpkWnaDE%3D" rel="nofollow">https://champyin.com/2020/01/...</a></p>
揭秘webpack plugin
https://segmentfault.com/a/1190000021593923
2020-01-15T19:41:38+08:00
2020-01-15T19:41:38+08:00
champyin
https://segmentfault.com/u/champyin
2
<blockquote>作者:champyin<br>原文:<a href="https://link.segmentfault.com/?enc=abEkh6UCk%2BuFvkoPBNd%2BxA%3D%3D.284P%2BAid2gXfkxkQAqMEfdBO0orEwQi9lU3PMg%2Bk7GUpVhEaHpw3AFo2j0wA6XAPZGTDS871SN3Vhwyh68BXKYUCaTaKFB8rXnsyHpPqFcc%3D" rel="nofollow">http://champyin.com/2020/01/1...</a><br>转载请注明出处</blockquote>
<h2>前言</h2>
<p>Plugin(插件) 是 webpack 生态的的一个关键部分。它为社区提供了一种强大的方法来扩展 webpack 和开发 webpack 的编译过程。本文将尝试探索 webpack plugin,揭秘它的工作原理,以及如何开发一个 plugin。</p>
<h2>一、Plugin 的作用</h2>
<p>关于 Plugin 的作用,引用一下 webpack 官方的介绍:</p>
<blockquote>Plugins expose the full potential of the webpack engine to third-party developers. Using staged build callbacks, developers can introduce their own behaviors into the webpack build process.</blockquote>
<p>我把它通俗翻译了下:<br>通过插件我们可以扩展 webpack,加入自定义的构建行为,使 webpack 可以执行更广泛的任务,拥有更强的构建能力。</p>
<h2>二、Plugin 工作原理</h2>
<blockquote>webpack 就像一条生产线,要经过一系列处理流程后才能将源文件转换成输出结果。 这条生产线上的每个处理流程的职责都是单一的,多个流程之间有存在依赖关系,只有完成当前处理后才能交给下一个流程去处理。 <br>插件就像是一个插入到生产线中的一个功能,在特定的时机对生产线上的资源做处理。webpack 通过 Tapable 来组织这条复杂的生产线。 webpack 在运行过程中会广播事件,插件只需要监听它所关心的事件,就能加入到这条生产线中,去改变生产线的运作。 <br>webpack 的事件流机制保证了插件的有序性,使得整个系统扩展性很好。 <br>——「深入浅出 Webpack」</blockquote>
<p>站在代码逻辑的角度就是:webpack 在编译代码过程中,会触发一系列 Tapable 钩子事件,插件所做的,就是找到相应的钩子,往上面挂上自己的任务,也就是注册事件,这样,当 webpack 构建的时候,插件注册的事件就会随着钩子的触发而执行了。</p>
<h2>三、webpack 的一些底层逻辑</h2>
<p>开发一个 plugin 比开发一个 loader 更高级一些(关于 loader 的开发,可以看我的另一篇文章<a href="https://link.segmentfault.com/?enc=xJJ0phHHar%2FlSTRoyLdTSg%3D%3D.8Qg%2BFwUCAacQD8RRObjFmSyhKNnixDdpwcJyVbPORlUpKZtTXh2xd9qi7cUCmxPVes5upxP6QXlB3RMOP7jv6yIObPCM%2BxWjXxM5oJIyY2Y%3D" rel="nofollow">「揭秘webpack loader」</a>),因为我们会用到一些 webpack 比较底层的内部组件。因此我们需要了解一些 webpack 的底层逻辑。</p>
<h3>webpack 内部执行流程</h3>
<p>一次完整的 webpack 打包大致是这样的过程:</p>
<ul>
<li>将命令行参数与 <code>webpack 配置文件</code> 合并、解析得到参数对象。</li>
<li>参数对象传给 webpack 执行得到 <code>Compiler</code> 对象。</li>
<li>执行 <code>Compiler</code> 的 <code>run</code>方法开始编译。每次执行 <code>run</code> 编译都会生成一个 <code>Compilation</code> 对象。</li>
<li>触发 <code>Compiler</code> 的 <code>make</code>方法分析入口文件,调用 <code>compilation</code> 的 <code>buildModule</code> 方法创建主模块对象。</li>
<li>生成入口文件 <code>AST(抽象语法树)</code>,通过 <code>AST</code> 分析和递归加载依赖模块。</li>
<li>所有模块分析完成后,执行 <code>compilation</code> 的 <code>seal</code> 方法对每个 <code>chunk</code> 进行整理、优化、封装。</li>
<li>最后执行 <code>Compiler</code> 的 <code>emitAssets</code> 方法把生成的文件输出到 <code>output</code> 的目录中。</li>
</ul>
<p>webpack 底层基本流程图</p>
<p><img src="/img/remote/1460000021593927" alt="webpack-basic-flow.png" title="webpack-basic-flow.png"></p>
<h3>webpack 内部的一些钩子</h3>
<h4>什么是钩子</h4>
<p>钩子的本质就是:事件。为了方便我们直接介入和控制编译过程,webpack 把编译过程中触发的各类关键事件封装成事件接口暴露了出来,这些接口被很形象地称做:<code>hooks</code>(钩子)。开发插件,离不开这些钩子。</p>
<h4>Tapable</h4>
<p>Tapable 为 webpack 提供了统一的插件接口(钩子)类型定义,它是 webpack 的核心功能库。webpack 中目前有十种 <code>hooks</code>,在 Tapable 源码中可以看到,他们是:</p>
<pre><code class="javascript">// https://github.com/webpack/tapable/blob/master/lib/index.js
exports.SyncHook = require("./SyncHook");
exports.SyncBailHook = require("./SyncBailHook");
exports.SyncWaterfallHook = require("./SyncWaterfallHook");
exports.SyncLoopHook = require("./SyncLoopHook");
exports.AsyncParallelHook = require("./AsyncParallelHook");
exports.AsyncParallelBailHook = require("./AsyncParallelBailHook");
exports.AsyncSeriesHook = require("./AsyncSeriesHook");
exports.AsyncSeriesBailHook = require("./AsyncSeriesBailHook");
exports.AsyncSeriesLoopHook = require("./AsyncSeriesLoopHook");
exports.AsyncSeriesWaterfallHook = require("./AsyncSeriesWaterfallHook");</code></pre>
<p>Tapable 还统一暴露了三个方法给插件,用于注入不同类型的自定义构建行为:</p>
<ul>
<li>
<code>tap</code>:可以注册同步钩子和异步钩子。</li>
<li>
<code>tapAsync</code>:回调方式注册异步钩子。</li>
<li>
<code>tapPromise</code>:Promise方式注册异步钩子。</li>
</ul>
<p>webpack 里的几个非常重要的对象,<code>Compiler</code>, <code>Compilation</code> 和 <code>JavascriptParser</code> 都继承了 Tapable 类,它们身上挂着丰富的钩子。</p>
<h4>Compiler Hooks</h4>
<p>Compiler 编译器模块是创建编译实例的主引擎。大多数面向用户的插件都首先在 Compiler 上注册。</p>
<p>compiler上暴露的一些常用的钩子:</p>
<table>
<thead><tr>
<th>钩子</th>
<th>类型</th>
<th>什么时候调用</th>
</tr></thead>
<tbody>
<tr>
<td>run</td>
<td>AsyncSeriesHook</td>
<td>在编译器开始读取记录前执行</td>
</tr>
<tr>
<td>compile</td>
<td>SyncHook</td>
<td>在一个新的compilation创建之前执行</td>
</tr>
<tr>
<td>compilation</td>
<td>SyncHook</td>
<td>在一次compilation创建后执行插件</td>
</tr>
<tr>
<td>make</td>
<td>AsyncParallelHook</td>
<td>完成一次编译之前执行</td>
</tr>
<tr>
<td>emit</td>
<td>AsyncSeriesHook</td>
<td>在生成文件到output目录之前执行,回调参数: <code>compilation</code>
</td>
</tr>
<tr>
<td>afterEmit</td>
<td>AsyncSeriesHook</td>
<td>在生成文件到output目录之后执行</td>
</tr>
<tr>
<td>assetEmitted</td>
<td>AsyncSeriesHook</td>
<td>生成文件的时候执行,提供访问产出文件信息的入口,回调参数:<code>file</code>,<code>info</code>
</td>
</tr>
<tr>
<td>done</td>
<td>AsyncSeriesHook</td>
<td>一次编译完成后执行,回调参数:<code>stats</code>
</td>
</tr>
</tbody>
</table>
<h4>Compilation Hooks</h4>
<p>Compilation 是 Compiler 用来创建一次新的编译过程的模块。一个 Compilation 实例可以访问所有模块和它们的依赖。在一次编译阶段,模块被加载、封装、优化、分块、散列和还原。<br>Compilation 也继承了 <code>Tapabl</code> 并提供了很多生命周期钩子。</p>
<p>Compilation 上暴露的一些常用的钩子:</p>
<table>
<thead><tr>
<th>钩子</th>
<th>类型</th>
<th>什么时候调用</th>
</tr></thead>
<tbody>
<tr>
<td>buildModule</td>
<td>SyncHook</td>
<td>在模块开始编译之前触发,可以用于修改模块</td>
</tr>
<tr>
<td>succeedModule</td>
<td>SyncHook</td>
<td>当一个模块被成功编译,会执行这个钩子</td>
</tr>
<tr>
<td>finishModules</td>
<td>AsyncSeriesHook</td>
<td>当所有模块都编译成功后被调用</td>
</tr>
<tr>
<td>seal</td>
<td>SyncHook</td>
<td>当一次compilation停止接收新模块时触发</td>
</tr>
<tr>
<td>optimizeDependencies</td>
<td>SyncBailHook</td>
<td>在依赖优化的开始执行</td>
</tr>
<tr>
<td>optimize</td>
<td>SyncHook</td>
<td>在优化阶段的开始执行</td>
</tr>
<tr>
<td>optimizeModules</td>
<td>SyncBailHook</td>
<td>在模块优化阶段开始时执行,插件可以在这个钩子里执行对模块的优化,回调参数:<code>modules</code>
</td>
</tr>
<tr>
<td>optimizeChunks</td>
<td>SyncBailHook</td>
<td>在代码块优化阶段开始时执行,插件可以在这个钩子里执行对代码块的优化,回调参数:<code>chunks</code>
</td>
</tr>
<tr>
<td>optimizeChunkAssets</td>
<td>AsyncSeriesHook</td>
<td>优化任何代码块资源,这些资源存放在 <code>compilation.assets</code> 上。一个 <code>chunk</code> 有一个 <code>files</code> 属性,它指向由一个chunk创建的所有文件。任何额外的 <code>chunk</code> 资源都存放在 <code>compilation.additionalChunkAssets</code> 上。回调参数:<code>chunks</code>
</td>
</tr>
<tr>
<td>optimizeAssets</td>
<td>AsyncSeriesHook</td>
<td>优化所有存放在 <code>compilation.assets</code> 的所有资源。回调参数:<code>assets</code>
</td>
</tr>
</tbody>
</table>
<h4>JavascriptParser Hooks</h4>
<p>Parser 解析器实例在 Compiler 编译器中产生,用于解析 webpack 正在处理的每个模块。我们可以用它提供的 <code>Tapable</code> 钩子自定义解析过程。</p>
<p>JavascriptParser 上暴露的一些常用的钩子:</p>
<table>
<thead><tr>
<th>钩子</th>
<th>类型</th>
<th>什么时候调用</th>
</tr></thead>
<tbody>
<tr>
<td>evaluate</td>
<td>SyncBailHook</td>
<td>在计算表达式的时候调用。</td>
</tr>
<tr>
<td>statement</td>
<td>SyncBailHook</td>
<td>为代码片段中每个已解析的语句调用的通用钩子</td>
</tr>
<tr>
<td>import</td>
<td>SyncBailHook</td>
<td>为代码片段中每个import语句调用,回调参数:<code>statement</code>,<code>source</code>
</td>
</tr>
<tr>
<td>export</td>
<td>SyncBailHook</td>
<td>为代码片段中每个export语句调用,回调参数:<code>statement</code>
</td>
</tr>
<tr>
<td>call</td>
<td>SyncBailHook</td>
<td>解析一个call方法的时候调用,回调参数:<code>expression</code>
</td>
</tr>
<tr>
<td>program</td>
<td>SyncBailHook</td>
<td>解析一个表达式的时候调用,回调参数:<code>expression</code>
</td>
</tr>
</tbody>
</table>
<p>对webpack底层逻辑和tapable钩子有了这些了解后,我们就可以进一步尝试开发一个插件了。</p>
<h2>四、如何开发一个webpack plugin</h2>
<h3>plugin 的基本结构</h3>
<p>一个 webpack plugin 由如下部分组成:</p>
<ol>
<li>一个命名的 Javascript 方法或者 JavaScript 类。</li>
<li>它的原型上需要定义一个叫做 <code>apply</code> 的方法。</li>
<li>注册一个事件钩子。</li>
<li>操作webpack内部实例特定数据。</li>
<li>功能完成后,调用webpack提供的回调。</li>
</ol>
<p>一个基本的 plugin 代码结构大致长这个样子:</p>
<pre><code class="javascript">// plugins/MyPlugin.js
class MyPlugin {
apply(compiler) {
compiler.hooks.done.tap('My Plugin', (stats) => {
console.log('Bravo!');
});
}
}
module.exports = MyPlugin;</code></pre>
<p>这就是一个最简单的 webpack 插件了,它注册了 <code>Compiler</code> 上的异步串行钩子 <code>done</code>,在钩子中注入了一条控制台打印的语句。根据上文钩子的介绍我们可以知道,<code>done</code> 会在一次编译完成后执行。所以这个插件会在每次打包结束,向控制台首先输出这句 <code>Bravo!</code>。</p>
<p><img src="/img/remote/1460000021593928" alt="webpack-plugin1.jpg" title="webpack-plugin1.jpg"></p>
<h3>开发一个文件清单插件</h3>
<p>我希望每次webpack打包后,自动产生一个打包文件清单,上面要记录文件名、文件数量等信息。</p>
<h4>思路:</h4>
<ul>
<li>显然这个操作需要在文件生成到dist目录之前进行,所以我们要注册的是<code>Compiler</code>上的<code>emit</code>钩子。</li>
<li>
<code>emit</code> 是一个异步串行钩子,我们用 <code>tapAsync</code> 来注册。</li>
<li>在 <code>emit</code> 的回调函数里我们可以拿到 <code>compilation</code> 对象,所有待生成的文件都在它的 <code>assets</code> 属性上。</li>
<li>通过 <code>compilation.assets</code> 获取我们需要的文件信息,并将其整理为新的文件内容准备输出。</li>
<li>然后往 <code>compilation.assets</code> 添加这个新的文件。</li>
</ul>
<p>插件完成后,最后将写好的插件放到 webpack 配置中,这个包含文件清单的文件就会在每次打包的时候自动生成了。</p>
<h4>实现:</h4>
<pre><code class="javascript">// plugins/FileListPlugin.js
class FileListPlugin {
constructor (options) {
// 获取插件配置项
this.filename = options && options.filename ? options.filename : 'FILELIST.md';
}
apply(compiler) {
// 注册 compiler 上的 emit 钩子
compiler.hooks.emit.tapAsync('FileListPlugin', (compilation, cb) => {
// 通过 compilation.assets 获取文件数量
let len = Object.keys(compilation.assets).length;
// 添加统计信息
let content = `# ${len} file${len>1?'s':''} emitted by webpack\n\n`;
// 通过 compilation.assets 获取文件名列表
for(let filename in compilation.assets) {
content += `- ${filename}\n`;
}
// 往 compilation.assets 中添加清单文件
compilation.assets[this.filename] = {
// 写入新文件的内容
source: function() {
return content;
},
// 新文件大小(给 webapck 输出展示用)
size: function() {
return content.length;
}
}
// 执行回调,让 webpack 继续执行
cb();
})
}
}
module.exports = FileListPlugin;</code></pre>
<h4>测试:</h4>
<p>在 webpack.config.js 中配置我们自己写的plugin:</p>
<pre><code class="javascript">plugins: [
new MyPlugin(),
new FileListPlugin({
filename: '_filelist.md'
})
]</code></pre>
<p><code>npm run build</code> 执行,可以看到生成了 <code>_filelist.md</code> 文件:</p>
<p><img src="/img/remote/1460000021593929" alt="webpack-plugin2.jpg" title="webpack-plugin2.jpg"></p>
<p>打开 <code>dist</code> 目录,可以看到<code>_filelist.md</code> 文件中列出了 webpack 打包后的文件:</p>
<p><img src="/img/remote/1460000021593930" alt="webpack-plugin3.jpg" title="webpack-plugin3.jpg"></p>
<p>成功!</p>
<h2>总结</h2>
<p>本文总结了 webpack plugin 的工作原理、wepack底层执行的基本流程以及介绍了 tapable 和常用的 hooks,最后通过两个小例子演示了如何自己开发一个webpack插件。</p>
<p>开发插件并非难如登天的事情,当遇到通过配置无法解决的问题,又一时找不到好的插件时,不如试试自己编写一个插件来解决,相信我,你会越来越强的!</p>
<p>本文的源码均可在这里获取:<a href="https://link.segmentfault.com/?enc=QxCg9KKokQwkk8OEgLHbMw%3D%3D.E91lsyxnml6VAu4iKHvUWyeytC%2F%2FnO%2Fg5w9upPc6AMtq0WbWQ2fSLCg6oqweGLii" rel="nofollow">https://github.com/yc111/webp...</a></p>
<p>欢迎交流~</p>
<p>Happy New Year!</p>
<p>--</p>
<p>参考 <br><a href="https://link.segmentfault.com/?enc=Umh5%2Fz1t4nDRKNH7xBZtJQ%3D%3D.fxWiETHreEGSUReE8MfwR45FnmirBO2%2FHVheqnM%2BqhPWS5MLGTVvyZMZ%2FxWWjm2w" rel="nofollow">https://webpack.js.org/api/co...</a> <br><a href="https://link.segmentfault.com/?enc=CBdODMvnCrdFkbLVQy8Mgw%3D%3D.L2oIrtzf9jWVChyQfY5au0ew0UaJQdO0ASWYh99mT%2BAr9HRELhZZYwu0lsvCZQYg" rel="nofollow">https://webpack.js.org/api/co...</a> <br><a href="https://link.segmentfault.com/?enc=w9Gc994%2BDLu1tX2nmaEuqA%3D%3D.Dv%2Bm%2FvzGRukuXpjsB2qcVHRNopvd4DSqhcE5LE3kDMpsD9DpuoFR3ePCHyktTasR" rel="nofollow">https://webpack.js.org/api/pa...</a> <br><a href="https://link.segmentfault.com/?enc=AHlPWFrmvEgYgA9ypnSLZw%3D%3D.wyX%2FJKsVQBNQQjLZmrYkRr4UZqAJtTaQfLd5ioMW6Kl0SyIwHyenBZB%2BAe1EDoFbEpSNj6bjTqYrB%2FFumT3oAA%3D%3D" rel="nofollow">https://github.com/yc111/webp...</a> <br><a href="https://link.segmentfault.com/?enc=MQuwDB2eMoEsg9FJOTo77w%3D%3D.dPdMLcZCGS41UzEw6pS7dSVFQhsPh%2BvEhQi6p%2FSyLDEyND8KIq%2BtDagFbYOR42jkhSK8A8cEpT%2Fs3oAmAxNjyA%3D%3D" rel="nofollow">https://webpack.js.org/contri...</a> <br><a href="https://link.segmentfault.com/?enc=77pryEh6%2FJLJuVEan8qxPA%3D%3D.1ki3j2fVvMz7RaRmGeA05TtH1iZU8hBLtRxL%2FejKKwTeoGPw%2FL2hW4iQ8rsvh99o" rel="nofollow">https://github.com/webpack/ta...</a> <br><a href="https://link.segmentfault.com/?enc=gn3IEokuSoBrJ%2Fr%2BnAQ37g%3D%3D.Q55viLSIbYpRcW2hG%2BT9W%2BPKn9iMslUUjvhsUmRCgmi4VkfIiV%2BdDqoFMyaSY8UM" rel="nofollow">https://webpack.js.org/concep...</a> <br><a href="https://link.segmentfault.com/?enc=Y%2Bu7wzWaftwgcI74j1fGQA%3D%3D.xlA63j3TTulIeoJg28w%2BVNDO5mIDroquFb0A0a5FfdAzMWPRdcwa6nXm60IX%2Fnbs" rel="nofollow">https://webpack.js.org/api/pl...</a> </p>
<p>欢迎转载,转载请注明出处:<a href="https://link.segmentfault.com/?enc=x%2BmcGTJt2HVz%2F0XfEVeY1w%3D%3D.uVAcvZp0eQsoh5m%2B186z0mxhuaCa2XLrwpH2kLQVYUjQfBXQu2o%2FYRPyprbdDcY%2BGj1bgogYwjyex7DIHR0lewrImFvslRE%2F%2FsQZw%2Fw9oqo%3D" rel="nofollow">http://champyin.com/2020/01/1...</a></p>
<p>本文同步发表于:<br><a href="https://link.segmentfault.com/?enc=9MQzq5QIwjI5RDl6sRkZMQ%3D%3D.ExBOiiOWuQc%2F5QyRI59njh1L3zo30v94FolVjWSkdQoUugV2CX%2FCcFEmT2C0MKvL" rel="nofollow">揭秘webpack plugin | 掘金</a></p>
封装axios
https://segmentfault.com/a/1190000021434469
2019-12-29T17:22:13+08:00
2019-12-29T17:22:13+08:00
champyin
https://segmentfault.com/u/champyin
6
<h2>前言</h2>
<blockquote>
<code>axios</code> 是一个轻量的 <code>HTTP客户端</code>,它基于 <code>XMLHttpRequest</code> 服务来执行 HTTP 请求,支持丰富的配置,支持 <code>Promise</code>,支持浏览器端和 <code>Node.js</code> 端。自Vue2.0起,尤大大(Vue作者尤雨溪)宣布取消对 <code>vue-resource</code> 的官方推荐,转而推荐 <code>axios</code>。现在 <code>axios</code> 已经成为大部分 Vue 开发者的首选。<br>(如果你还不熟悉 <code>axios</code>,可以在<a href="https://link.segmentfault.com/?enc=m6TUKBbzEegnbCkiIs8z3A%3D%3D.xrlOEAHYkldcwILiAZlKVFpVdgHwER4J6dbfXKKcWA1FaCsHu5kDbxKhUwtVNPUZ" rel="nofollow">这里</a>查看它的API)。</blockquote>
<p><code>axios</code> 的API很友好,你完全可以很轻松地在项目中直接使用。不过随着项目规模增大,如果每发起一次HTTP请求,就要把这些比如设置超时时间、设置请求头、根据项目环境判断使用哪个请求地址、错误处理等等操作,都就地写一遍,得疯!这种重复劳动不仅浪费时间,而且让代码变得冗余不堪,难以维护。</p>
<p>为了提高我们的代码质量,我们应该在项目中二次封装一下 <code>axios</code> 再使用。</p>
<p>那么,怎么封装 <code>axios</code> 呢?</p>
<h2>原来的样子</h2>
<p>封装前,先来看下,不封装的情况下,一个实际项目中axios请求的样子。大概是长这样:</p>
<pre><code>axios('http://localhost:3000/data', {
method: 'GET',
timeout: 1000,
withCredentials: true,
headers: {
'Content-Type': 'application/json',
Authorization: 'xxx',
},
transformRequest: [function (data, headers) {
return data;
}],
// 其他请求配置...
})
.then((data) => {
// todo: 真正业务逻辑代码
console.log(data);
}, (err) => {
if (err.response.status === 401) {
// handle authorization error
}
if (err.response.status === 403) {
// handle server forbidden error
}
// 其他错误处理.....
console.log(err);
});</code></pre>
<p>可以看到在这段代码中,页面代码逻辑只在第15行处,上方的一大块请求配置代码和下方一大块响应错误处理代码,几乎跟页面功能没有关系,而且每个请求中这些内容都差不多,甚至有的部分完全一样。想象一下,每发一次请求都来这么一下,十几个请求一写,会是什么盛况?</p>
<h2>封装步骤</h2>
<p>封装的本质就是在待封装的内容外面添加各种东西,然后把它们作为一个新的整体呈现给使用者,以达到扩展和易用的目的。</p>
<p>封装<code>axios</code>要做的事情,就是把所有HTTP请求共用的配置,事先都在axios上配置好,预留好必要的参数和接口,然后把它作为新的axios返回。</p>
<p>接下来我们借助一个demo实现一个具有良好扩展性的<code>axios</code>封装。</p>
<p>demo目录结构如下(由Vue-cli 3.0 生成):</p>
<pre><code>|--public/
|--mock/
| |--db.json # 我新建的接口模拟数据
|--src/
| |--assets/
| |--components/
| |--router/
| |--store/
| |--views/
| |--Home.Vue
| |--App.vue
| |--main.js
| |--theme.styl
|--package.json
|...</code></pre>
<h3>封装目标</h3>
<p>我希望在 Home 页,发起 axios 请求时就像调用一个只有少量参数的方法一样简单,这样我就可以专注业务代码了。</p>
<h3>1. 将 axios 封装到一个独立的文件</h3>
<ul><li>在src下创建 utils/http.js 文件</li></ul>
<pre><code>cd src
mkdir utils
touch http.js</code></pre>
<ul><li>引入 axios</li></ul>
<pre><code>// src/utils/http.js
import axios from 'axios';</code></pre>
<ul><li>创建一个类<br>你也可以用函数来封装,我只是觉得类更语义化而已。</li></ul>
<pre><code>//src/utils/http.js
//...
class NewAxios {
}</code></pre>
<ul><li>给不同环境配置不同请求地址</li></ul>
<p>根据 <code>process.env.NODE_ENV</code> 配置不同的 <code>baseURL</code>,使项目只需执行相应打包命令,就可以在不同环境中自动切换请求主机地址。</p>
<pre><code>// src/utils/http.js
//...
const getBaseUrl = (env) => {
let base = {
production: '/',
development: 'http://localhost:3000',
test: 'http://localhost:3001',
}[env];
if (!base) {
base = '/';
}
return base;
};
class NewAxios {
constructor() {
this.baseURL = getBaseUrl(process.env.NODE_ENV);
}
}</code></pre>
<ul><li>配置超时时间</li></ul>
<p>timeout属性,我一般设置10秒。</p>
<pre><code>// src/utils/http.js
//...
class NewAxios {
constructor() {
//...
this.timeout = 10000;
}
}</code></pre>
<ul><li>配置允许携带凭证</li></ul>
<p>widthCredentials属性设为true。</p>
<pre><code>// src/utils/http.js
//...
class NewAxios {
constructor() {
//...
this.withCredentials = true;
}
}</code></pre>
<ul><li>给这个类创建实例上的方法request</li></ul>
<p>在 <code>request</code> 方法里,创建新的axios实例,接收请求配置参数,处理参数,添加配置,返回axios实例的请求结果(一个promise对象)。<br>你也可以不创建,直接使用默认导出的axios实例,然后把所有配置都放到它上面,不过这样一来整个项目就会共用一个axios实例。虽然大部分项目下这样够用没问题,但是有的项目中不同服务地址的请求和响应结构可能完全不同,这个时候共用一个实例就没办法支持了。所以为了封装可以更通用,更具灵活性,我会使用axios的create方法,使每次发请求都是新的axios实例。</p>
<pre><code>// src/utils/http.js
//...
class NewAxios {
//...
request(options) {
// 每次请求都会创建新的axios实例。
const instance = axios.create();
const config = { // 将用户传过来的参数与公共配置合并。
...options,
baseURL: this.baseURL,
timeout: this.timeout,
withCredentials: this.withCredentials,
};
// 配置拦截器,支持根据不同url配置不同的拦截器。
this.setInterceptors(instance, options.url);
return instance(config); // 返回axios实例的执行结果
}
}</code></pre>
<blockquote>因为拦截器配置内容比较多,所以封装成一个内部函数了。</blockquote>
<ul><li>配置请求拦截器<br>在发送请求前对请求参数做的所有修改都在这里统一配置。比如统一添加token凭证、统一设置语言、统一设置内容类型、指定数据格式等等。做完后记得返回这个配置,否则整个请求不会进行。<br>我这里就配置一个token。</li></ul>
<pre><code>// src/utils/http.js
//...
class NewAxios {
//...
// 这里的url可供你针对需要特殊处理的接口路径设置不同拦截器。
setInterceptors = (instance, url) => {
instance.interceptors.request.use((config) => { // 请求拦截器
// 配置token
config.headers.AuthorizationToken = localStorage.getItem('AuthorizationToken') || '';
return config;
}, err => Promise.reject(err));
}
//...
}</code></pre>
<ul><li>配置响应拦截器<br>在请求的<code>then</code>或<code>catch</code>处理前对响应数据进行一轮预先处理。比如过滤响应数据,更多的,是在这里对各种响应错误码进行统一错误处理,还有断网处理等等。<br>我这里就判断一下403和断网。</li></ul>
<pre><code>// src/utils/http.js
//...
class NewAxios {
//...
setInterceptors = (instance, url) => {
//...
instance.interceptors.response.use((response) => { // 响应拦截器
// todo: 想根据业务需要,对响应结果预先处理的,都放在这里
console.log();
return response;
}, (err) => {
if (err.response) { // 响应错误码处理
switch (err.response.status) {
case '403':
// todo: handler server forbidden error
break;
// todo: handler other status code
default:
break;
}
return Promise.reject(err.response);
}
if (!window.navigator.online) { // 断网处理
// todo: jump to offline page
return -1;
}
return Promise.reject(err);
});
}
//...
}</code></pre>
<p>另外,在拦截器里,还适合放置loading等缓冲效果:在请求拦截器里显示loading,在响应拦截器里移除loading。这样所有请求就都有了一个统一的loading效果。</p>
<ul><li>默认导出新的实例</li></ul>
<pre><code>// src/utils/http.js
//...
export default new NewAxios();</code></pre>
<p>最后完整的代码如下:</p>
<pre><code>// src/utils/http.js
import axios from 'axios';
const getBaseUrl = (env) => {
let base = {
production: '/',
development: 'http://localhost:3000',
test: 'http://localhost:3001',
}[env];
if (!base) {
base = '/';
}
return base;
};
class NewAxios {
constructor() {
this.baseURL = getBaseUrl(process.env.NODE_ENV);
this.timeout = 10000;
this.withCredentials = true;
}
setInterceptors = (instance, url) => {
instance.interceptors.request.use((config) => {
// 在这里添加loading
// 配置token
config.headers.AuthorizationToken = localStorage.getItem('AuthorizationToken') || '';
return config;
}, err => Promise.reject(err));
instance.interceptors.response.use((response) => {
// 在这里移除loading
// todo: 想根据业务需要,对响应结果预先处理的,都放在这里
return response;
}, (err) => {
if (err.response) { // 响应错误码处理
switch (err.response.status) {
case '403':
// todo: handler server forbidden error
break;
// todo: handler other status code
default:
break;
}
return Promise.reject(err.response);
}
if (!window.navigator.online) { // 断网处理
// todo: jump to offline page
return -1;
}
return Promise.reject(err);
});
}
request(options) {
// 每次请求都会创建新的axios实例。
const instance = axios.create();
const config = { // 将用户传过来的参数与公共配置合并。
...options,
baseURL: this.baseURL,
timeout: this.timeout,
withCredentials: this.withCredentials,
};
// 配置拦截器,支持根据不同url配置不同的拦截器。
this.setInterceptors(instance, options.url);
return instance(config); // 返回axios实例的执行结果
}
}
export default new NewAxios();</code></pre>
<p>现在 <code>axios</code> 封装算是完成了80%。我们还需要再进一步把axios和接口结合再封装一层,才能达到我在一开始定的封装目标。</p>
<h3>2. 使用新的 axios 封装API</h3>
<ul>
<li>在 src 目录下新建 <code>api</code> 文件夹。把所有涉及HTTP请求的接口统一集中到这个目录来管理。</li>
<li>新建 <code>home.js</code>。我们需要把接口根据一定规则分好类,一类接口对应一个js文件。这个分类可以是按页面来划分,或者按模块等等。为了演示更直观,我这里就按页面来划分了。实际根据自己的需求来定。</li>
<li>使用新的 axios 封装API(固定url的值,合并用户传过来的参数),然后命名导出这些函数。</li>
</ul>
<pre><code>// src/api/home.js
import axios from '@/utils/http';
export const fetchData = options => axios.request({
...options,
url: '/data',
});
export default {};</code></pre>
<ul><li>在 api 目录下新建 <code>index.js</code>,把其他文件的接口都在这个文件里汇总导出。</li></ul>
<pre><code>// src/api/index.js
export * from './home';</code></pre>
<blockquote>这层封装将我们的新的axios封装到了更简洁更语义化的接口方法中。</blockquote>
<p>现在我们的目录结构长这样:</p>
<pre><code>|--public/
|--mock/
| |--db.json # 接口模拟数据
|--src/
| |--api/ # 所有的接口都集中在这个目录下
| |--home.js # Home页面里涉及到的接口封装在这里
| |--index.js # 项目中所有接口调用的入口
| |--assets/
| |--components/
| |--router/
| |--store/
| |--utils/
| |--http.js # axios封装在这里
| |--views/
| |--Home.Vue
| |--App.vue
| |--main.js
| |--theme.styl
|--package.json
|...</code></pre>
<h2>使用封装后的axios</h2>
<p>现在我们要发HTTP请求时,只需引入 <code>api</code> 下的 <code>index.js</code> 文件就可以调用任何接口了,并且用的是封装后的 <code>axios</code>。</p>
<pre><code>// src/views/Home.vue
<template>
<div class="home">
<h1>This is home page</h1>
</div>
</template>
<script>
// @ is an alias to /src
import { fetchData } from '@/api/index';
export default {
name: 'home',
mounted() {
fetchData() // axios请求在这里
.then((data) => {
console.log(data);
})
.catch((err) => {
console.log(err);
});
},
};
</script></code></pre>
<p>axios请求被封装在<code>fetchData</code>函数里,页面请求压根不需要出现任何<code>axios API</code>,悄无声息地发起请求获取响应,就像在调用一个简单的 <code>Promise</code> 函数一样轻松。并且在页面中只需专注处理业务功能,不用被其他事物干扰。</p>
<h2>运行</h2>
<p>运行 <code>npm run serve</code> 启动项目,执行 <code>npm run mock</code> 启动服务mock接口。</p>
<p>现在打开 <code>localhost:8080</code> 可以看到home页面。打开浏览器控制台,可以看到打印的请求响应结果:</p>
<p><img src="/img/remote/1460000021434472" alt="wrap-axios.jpg" title="wrap-axios.jpg"></p>
<p>简洁,优雅。</p>
<h2>总结</h2>
<ol>
<li>封装思想是前端技术中很有用的思想,简单的axios及接口封装,就可以让我们可以领略到它的魅力。</li>
<li>封装 <code>axios</code> 没有一个绝对的标准,只要你的封装可以满足你的项目需求,并且用起来方便,那就是一个好的封装方案。</li>
<li>BTW:以上封装给大家提供了一个封装好的axios和api框架,经过以上过程封装好的 <code>axios</code>,可以不局限于 Vue,React 项目同样可以拿去使用,它适用任何前端项目。</li>
</ol>
<p>本文的代码可以在这里获取:<a href="https://link.segmentfault.com/?enc=B1nTQyF88P6cRgBDTtVHMA%3D%3D.gfocFVEeDISgQqhHn7XzQv3lQjjpW6PGy80L67gSoB3zyDCwUrRU0Xnfh1U6Jm2d" rel="nofollow">https://github.com/yc111/wrap...</a></p>
<p>欢迎交流~</p>
<p>欢迎转载,转载请注明出处:<br><a href="https://link.segmentfault.com/?enc=7DZdx5UECdz%2FubIPEaYb9A%3D%3D.3jR%2Fe9ZfQV4UVrbKZM30VUGGm5pLvyhzK5EXr%2FKNK1KmPY4aGAE5cczbGetLUVJ6HOX4vb9NvU3YxbImXv8pqg%3D%3D" rel="nofollow">https://champyin.com/2019/12/...</a></p>
webpack优化之玩转代码分割和公共代码提取
https://segmentfault.com/a/1190000021074403
2019-11-21T11:51:54+08:00
2019-11-21T11:51:54+08:00
champyin
https://segmentfault.com/u/champyin
14
<h2>前言</h2>
<p>开发多页应用的时候,如果不对webpack打包进行优化,当某个模块被多个入口模块引用时,它就会被打包多次(在最终打包出来的某几个文件里,它们都会有一份相同的代码)。当项目业务越来越复杂,打包出来的代码会非常冗余,文件体积会非常庞大。大体积文件会增加编译时间,影响开发效率;如果直接上线,还会拉长请求和加载时长,影响网站体验。作为一个追求极致体验的攻城狮,是不能忍的。所以在多页应用中优化打包尤为必要。那么如何优化webpack打包呢?</p>
<h2>一、概念</h2>
<p>在一切开始前,有必要先理清一下这三个概念:</p>
<ul>
<li>module: 模块,在webpack眼里,任何可以被导入导出的文件都是一个模块。</li>
<li>
<p>chunk: chunk是webpack拆分出来的:</p>
<ul>
<li>每个入口文件都是一个chunk</li>
<li>通过 import、require 引入的代码也是</li>
<li>通过 splitChunks 拆分出来的代码也是</li>
</ul>
</li>
<li>bundle: webpack打包出来的文件,也可以理解为就是对chunk编译压缩打包等处理后的产出。</li>
</ul>
<h2>二、问题分析</h2>
<p>首先,简单分析下,我们刚才提到的打包问题:</p>
<ul>
<li>核心问题就是:多页应用打包后代码冗余,文件体积大。</li>
<li>究其原因就是:相同模块在不同入口之间没有得到复用,bundle之间比较独立。</li>
</ul>
<p>弄明白了问题的原因,那么大致的解决思路也就出来了:</p>
<ul>
<li>我们在打包的时候,应该把不同入口之间,共同引用的模块,抽离出来,放到一个公共模块中。这样不管这个模块被多少个入口引用,都只会在最终打包结果中出现一次。——解决代码冗余。</li>
<li>另外,当我们把这些共同引用的模块都堆在一个模块中,这个文件可能异常巨大,也是不利于网络请求和页面加载的。所以我们需要把这个公共模块再按照一定规则进一步拆分成几个模块文件。——减小文件体积。</li>
<li>
<p>至于如何拆分,方式因人而异,因项目而异。我个人的拆分原则是:</p>
<ul>
<li>业务代码和第三方库分离打包,实现代码分割;</li>
<li>业务代码中的公共业务模块提取打包到一个模块;</li>
<li>第三方库最好也不要全部打包到一个文件中,因为第三方库加起来通常会很大,我会把一些特别大的库分别独立打包,剩下的加起来如果还很大,就把它按照一定大小切割成若干模块。</li>
</ul>
</li>
</ul>
<h2>optimization.splitChunks</h2>
<p>webpack提供了一个非常好的内置插件帮我们实现这一需求:<code>CommonsChunkPlugin</code>。不过在 webpack4 中<code>CommonsChunkPlugin</code>被删除,取而代之的是<code>optimization.splitChunks</code>, 所幸的是<code>optimization.splitChunks</code>更强大!</p>
<h2>三、 实现</h2>
<p>通过一个多页应用的小demo,我们一步一步来实现上述思路的配置。</p>
<p>demo目录结构:</p>
<pre><code>|--public/
| |--a.html
| |--index.html
|--src/
| |--a.js
| |--b.js
| |--c.js
| |--index.js
|--package.json
|--webpack.config.js</code></pre>
<p>代码逻辑很简单,<code>index</code>模块中引用了 <code>a</code> 和 <code>b</code> 2个模块,<code>a</code> 模块中引用了 <code>c</code> 模块和 <code>jquery</code>库,<code>b</code> 模块中也引用了 <code>c</code> 模块和 <code>jquery</code> 库,<code>c</code> 是一个独立的模块没有其他依赖。</p>
<p>index.js代码如下:</p>
<pre><code>//index.js
import a from './a.js';
import b from './b.js';
function fn() {
console.log('index-------');
}
fn();</code></pre>
<p>a.js代码如下:</p>
<pre><code>//a.js
require('./c.js');
const $ = require('jquery')
function fn() {
console.log('a-------');
}
module.exports = fn();</code></pre>
<p>b.js代码如下:</p>
<pre><code>//b.js
require('./c.js');
const $ = require('jquery')
function fn() {
console.log('b-------');
}
module.exports = fn();</code></pre>
<p>c.js代码如下:</p>
<pre><code>//c.js
function fn() {
console.log('c-------');
}
module.exports = fn();</code></pre>
<h3>1. 基本配置</h3>
<p>webpack先不做优化,只做基本配置,看看效果。项目配置了2个入口,搭配<code>html-webpack-plugin</code>实现多页打包:</p>
<pre><code>const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: {
index: './src/index.js',
a: './src/a.js'
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js'
},
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html',
filename: 'index.html'
}),
new HtmlWebpackPlugin({
template: './public/a.html',
filename: 'a.html'
})
]
}</code></pre>
<p>在开发模式下运行webpack:<br><img src="/img/remote/1460000021074406" alt="webpack-normal-build.jpg" title="webpack-normal-build.jpg"></p>
<p>可以看到,打包出两个html和两个体积很大的(300多K)的文件<code>a.js</code>,<code>index.js</code>。</p>
<p>进入dist目录检查js文件:</p>
<ul>
<li>
<code>a.js</code>里包含<code>c</code>模块代码和<code>jquery</code>代码</li>
<li>
<code>index.js</code>里包含<code>a</code>模块、<code>b</code>模块、<code>c</code>模块和<code>jquery</code>代码</li>
</ul>
<p>看,同样的代码<code>c</code>和<code>jquery</code>被打包了2遍。</p>
<h3>2. 初步添加splitChunks优化配置</h3>
<p>首先解决相同代码打包2次的问题,我们需要让webpack把<code>c</code>和<code>jquery</code>提取出来打包为公共模块。</p>
<p>在webpack配置文件添加splitChunks:</p>
<pre><code>//webpack.config.js
optimization: {
splitChunks: {
cacheGroups: {
default: {
name: 'common',
chunks: 'initial'
}
}
}
}</code></pre>
<h5>- cacheGroups</h5>
<ul>
<li>
<code>cacheGroups</code>是<code>splitChunks</code>配置的核心,对代码的拆分规则全在<code>cacheGroups</code>缓存组里配置。</li>
<li>缓存组的每一个属性都是一个配置规则,我这里给他的<code>default</code>属性进行了配置,属性名可以不叫default可以自己定。</li>
<li>属性的值是一个对象,里面放的我们对一个代码拆分规则的描述。</li>
</ul>
<h5>- name</h5>
<ul><li>name:提取出来的公共模块将会以这个来命名,可以不配置,如果不配置,就会生成默认的文件名,大致格式是<code>index~a.js</code>这样的。</li></ul>
<h5>- chunks</h5>
<ul><li>chunks:指定哪些类型的chunk参与拆分,值可以是string可以是函数。如果是string,可以是这个三个值之一:<code>all</code>, <code>async</code>, <code>initial</code>,<code>all</code> 代表所有模块,<code>async</code>代表只管异步加载的, <code>initial</code>代表初始化时就能获取的模块。如果是函数,则可以根据chunk参数的name等属性进行更细致的筛选。</li></ul>
<p>再次打包:<br><img src="/img/remote/1460000021074407" alt="webpack-optimize-build.jpg" title="webpack-optimize-build.jpg"></p>
<p>可以看到<code>a.js</code>,<code>index.js</code>从300多K减少到6点几K。同时增加了一个<code>common.js</code>文件,并且两个打包入口都自动添加了<code>common.js</code>这个公共模块:<br><img src="/img/remote/1460000021074411" alt="webpack-optimize-build2.jpg" title="webpack-optimize-build2.jpg"></p>
<p>进入dist目录,依次查看这3个js文件:</p>
<ul>
<li>
<code>a.js</code>里不包含任何模块的代码了,只有webpack生成的默认代码。</li>
<li>
<code>index.js</code>里同样不包含任何模块的代码了,只有webpack生成的默认代码。</li>
<li>
<code>common.js</code>里有<code>a</code>,<code>b</code>,<code>c</code>,<code>index</code>,<code>jquery</code>代码。</li>
</ul>
<p>发现,提是提取了,但是似乎跟我们预料的不太一样,所有的模块都跑到<code>common.js</code>里去了。</p>
<p>这是因为我们没有告诉webpack(<code>splitChunks</code>)什么样的代码为公共代码,<code>splitChunks</code>默认任何模块都会被提取。</p>
<h5>- minChunks</h5>
<p><code>splitChunks</code>是自带默认配置的,而缓存组默认会继承这些配置,其中有个<code>minChunks</code>属性:</p>
<ul>
<li>它控制的是每个模块什么时候被抽离出去:当模块被不同entry引用的次数大于等于这个配置值时,才会被抽离出去。</li>
<li>它的默认值是1。也就是任何模块都会被抽离出去(入口模块其实也会被webpack引入一次)。</li>
</ul>
<p>我们上面没有配置<code>minChunks</code>,只配置了<code>name</code>和<code>chunk</code>两个属性,所以<code>minChunks</code>的默认值 <code>1</code> 生效。也难怪所有的模块都被抽离到<code>common.js</code>中了。</p>
<p>优化一下,在缓存组里配置<code>minChunks</code>覆盖默认值:</p>
<pre><code>//webpack.config.js
optimization: {
splitChunks: {
cacheGroups: {
default: {
name: 'common',
chunks: 'initial',
minChunks: 2 //模块被引用2次以上的才抽离
}
}
}
}</code></pre>
<p>然后运行webpack</p>
<p><img src="/img/remote/1460000021074409" alt="webpack-optimize-build3.jpg" title="webpack-optimize-build3.jpg"></p>
<p>可以看到有2个文件的大小发生了变化:<code>common.js</code>由314K减小到311K,<code>index.js</code>由6.22K增大到7.56K。</p>
<p>进入dist目录查看:</p>
<ul>
<li>
<code>a.js</code>里依然不包含任何模块的代码(正常,因为<code>a</code>作为模块被<code>index</code>引入了一次,又作为入口被webpack引入了一次,所以<code>a</code>是有2次引用的)。</li>
<li>
<code>index.js</code>里出现了<code>b</code>和<code>index</code>模块的代码了。</li>
<li>
<code>common.js</code>里只剩<code>a</code>,<code>c</code>,和<code>jquery</code>模块的代码。</li>
</ul>
<p>现在我们把共同引用的模块<code>a</code>, <code>c</code>, <code>jquery</code>,从<code>a</code>和<code>index</code>这两个入口模块里抽取到<code>common.js</code>里了。有点符合我们的预期了。</p>
<h3>3. 配置多个拆分规则</h3>
<h4>3.1 实现代码分离,拆分第三方库</h4>
<p>接下来,我希望公共模块<code>common.js</code>中,业务代码和第三方模块jquery能够剥离开来。</p>
<p>我们需要再添加一个拆分规则。</p>
<pre><code>//webpack.config.js
optimization: {
splitChunks: {
minSize: 30, //提取出的chunk的最小大小
cacheGroups: {
default: {
name: 'common',
chunks: 'initial',
minChunks: 2, //模块被引用2次以上的才抽离
priority: -20
},
vendors: { //拆分第三方库(通过npm|yarn安装的库)
test: /[\\/]node_modules[\\/]/,
name: 'vendor',
chunks: 'initial',
priority: -10
}
}
}
}</code></pre>
<p>我给cacheGroups添加了一个vendors属性(属性名可以自己取,只要不跟缓存组下其他定义过的属性同名就行,否则后面的拆分规则会把前面的配置覆盖掉)。</p>
<h5>- minSize</h5>
<p>minSize设置的是生成文件的最小大小,单位是字节。如果一个模块符合之前所说的拆分规则,但是如果提取出来最后生成文件大小比minSize要小,那它仍然不会被提取出来。这个属性可以在每个缓存组属性中设置,也可以在splitChunks属性中设置,这样在每个缓存组都会继承这个配置。这里由于我的demo中文件非常小,为了演示效果,我把minSize设置为30字节,好让公共模块可以被提取出来,正常项目中不用设这么小。</p>
<h5>- priority</h5>
<p>priority属性的值为数字,可以为负数。作用是当缓存组中设置有多个拆分规则,而某个模块同时符合好几个规则的时候,则需要通过优先级属性priority来决定使用哪个拆分规则。优先级高者执行。我这里给业务代码组设置的优先级为-20,给第三方库组设置的优先级为-10,这样当一个第三方库被引用超过2次的时候,就不会打包到业务模块里了。</p>
<h5>- test</h5>
<p>test属性用于进一步控制缓存组选择的模块,与chunks属性的作用有一点像,但是维度不一样。test的值可以是一个正则表达式,也可以是一个函数。它可以匹配模块的绝对资源路径或chunk名称,匹配chunk名称时,将选择chunk中的所有模块。我这里用了一个正则<code>/[\\/]node_modules[\\/]/</code>来匹配第三方模块的绝对路径,因为通过npm或者yarn安装的模块,都会存放在node_modules目录下。</p>
<p>运行一下webpack:<br><img src="/img/remote/1460000021074408" alt="webpack-optimize-build4.jpg" title="webpack-optimize-build4.jpg"></p>
<p>可以看到新产生了一个叫<code>vendor.js</code>的文件(name属性的值),同时<code>common.js</code>文件体积由原来的311k减少到了861bytes!</p>
<p>进入dist目录,检查js文件:</p>
<ul>
<li>
<code>a.js</code>里不包含任何模块代码。</li>
<li>
<code>common.js</code>只包含<code>a</code>和<code>c</code>模块的代码。</li>
<li>
<code>index.js</code>只包含<code>b</code>和<code>index</code>模块的代码。</li>
<li>
<code>vendor.js</code>只包含<code>jquery</code>模块的代码。</li>
</ul>
<p>现在,我们在上一步的基础上,成功从<code>common.js</code>里把第三方库<code>jquery</code>抽离出来放到了<code>vendor.js</code>里。</p>
<h4>3.2 拆分指定文件</h4>
<p>如果我们还想把项目中的某一些文件单独拎出来打包(比如工程本地开发的组件库),可以继续添加拆分规则。比如我的src下有个<code>locallib.js</code>文件要单独打包,假设<code>a.js</code>中引入了它。</p>
<pre><code>//a.js
require('./c.js');
require('./locallib.js'); //引入自己本地的库
const $ = require('jquery')
function fn() {
console.log('a-------');
}
module.exports = fn();</code></pre>
<p>可以这么配置:</p>
<pre><code>//webpack.config.js
optimization: {
splitChunks: {
minSize: 30, //提取出的chunk的最小大小
cacheGroups: {
default: {
name: 'common',
chunks: 'initial',
minChunks: 2, //模块被引用2次以上的才抽离
priority: -20
},
vendors: { //拆分第三方库(通过npm|yarn安装的库)
test: /[\\/]node_modules[\\/]/,
name: 'vendor',
chunks: 'initial',
priority: -10
},
locallib: { //拆分指定文件
test: /(src\/locallib\.js)$/,
name: 'locallib',
chunks: 'initial',
priority: -9
}
}
}
}</code></pre>
<p>我在缓存组下又新增了一个拆分规则,通过test正则指定我就要单独打包<code>src/locallib.js</code>文件,并且把优先级设置为-9,这样当它被多次引用时,不会进入其他拆分规则组,因为另外两个规则的优先级都比它要低。</p>
<p>运行webpack打包后:<br><img src="/img/remote/1460000021074410" alt="webpack-optimize-build5.jpg" title="webpack-optimize-build5.jpg"></p>
<p>可以看到新产生了一个<code>locallib.js</code>文件。进入dist目录查看:</p>
<ul>
<li>
<code>a.js</code>里不包含任何模块代码。</li>
<li>
<code>common.js</code>只包含<code>a</code>和<code>c</code>模块的代码。</li>
<li>
<code>index.js</code>只包含<code>b</code>和<code>index</code>模块的代码。</li>
<li>
<code>vendor.js</code>只包含<code>jquery</code>模块的代码。</li>
<li>
<code>locallib.js</code>里只包含<code>locallib</code>模块的代码。</li>
</ul>
<p>现在我们又在上一步的基础上独立打包了一个指定的模块<code>locallib.js</code>。 </p>
<p>至此,我们就成功实现了抽离公共模块、业务代码和第三方代码剥离、独立打包指定模块。</p>
<p>对比一下,优化前,打包出来js一共有633KB:</p>
<p><img src="/img/remote/1460000021074413" alt="webpack-before-optimize.png" title="webpack-before-optimize.png"></p>
<p>优化后,打包出来js一共不到330KB:</p>
<p><img src="/img/remote/1460000021074412" alt="webpack-after-optimize.png" title="webpack-after-optimize.png"></p>
<p>优化打包后的文件分类清晰,体积比优化前缩小了几乎50%,有点小完美是不是!击掌!这还只是我举的一个简单例子,在实际多页应用中,优化力度说不定还不止这么多。</p>
<h3>小结</h3>
<p>webpack很强大,以上只是冰山一角,但是只要掌握了上述<code>optimization.splitChunks</code>的核心配置,我们就可以几乎随心所欲地按照自己的想法来拆分优化代码控制打包文件了,是不是很酷?玩转代码拆分,你也可以!</p>
<blockquote>如果觉得这些依然不能满足你的需求,还想更精(bian)细(tai)地定制打包规则,可以到<a href="https://link.segmentfault.com/?enc=zUbS2gfswvfo8h5PNb0N7g%3D%3D.aZ3zvkt%2FtW%2FBOngnKdXL6%2FvUVKDQoc4Aw5Ze7KOoGfTSU4HYrZjNGA5YHlypVdtQY1qiv%2FTN%2B85CYT6Jp%2FLZrXqsjok4rE%2BuXCTVQ4jI00U%3D" rel="nofollow">webpack官网</a>查看<code>optimization.splitChunks</code>的更多配置。</blockquote>
<p>欢迎交流~</p>
<p>本文的完整webpack配置和demo源码可以在这里获取:<br><a href="https://link.segmentfault.com/?enc=ekH%2BwSG5sPcCOby5chj8zA%3D%3D.AXqAQ2NPmtNfyprBohDrtGfzp0RcSC4FNcTClEXuaC8QaToM4feeJcv0ewQJidsi" rel="nofollow">https://github.com/yc111/webp...</a></p>
<p>-- </p>
<p>欢迎转载,转载请注明出处: <br><a href="https://link.segmentfault.com/?enc=sFojPYZCpV2egp6bT2Hcag%3D%3D.lYXYTMnx4A9ZpWABy7ONKdfEVHTp4C6th3l%2FM%2FUtDM65RxOgBRSZMAFk0jQqXek%2BB%2FaCHuQZ2rLX%2BsHkJNjMo2gosGhCLjVOwZWnvcMmMKjTeAHz1RpJ0ZhGXJ7xdDIFx5HuFQ9s9%2F5f5288kq4P9BZAp5z%2BRwCNI0f9RRD04Z95MbttNgIZlDP9Gxbfduy1bVeKEZdJc46HnksNfte0LMwFnvLhB5HP8KzEg3%2BvVwLkt%2FsmE4fp3EvrcnDTOYzJ" rel="nofollow">https://champyin.com/2019/11/...</a></p>
<p>本文同步发表于:<br><a href="https://link.segmentfault.com/?enc=ouPpd%2BQ%2FYvmRKDgmwkLGnA%3D%3D.yxEM%2FUZXpL8cVw5dd%2FKlqELqeRvOvqZgbLORz1YPcggtdJQVTCgGUxX%2F0Gyx%2BFO3" rel="nofollow">webpack优化之玩转代码分割和公共代码提取 | 掘金</a></p>
node.js操作数据库之MongoDB+mongoose篇
https://segmentfault.com/a/1190000020670301
2019-10-13T13:00:21+08:00
2019-10-13T13:00:21+08:00
champyin
https://segmentfault.com/u/champyin
1
<h2>前言</h2>
<blockquote>
<code>node.js</code> 的出现,使得用前端语法(javascript)开发后台服务成为可能,越来越多的前端因此因此接触后端,甚至转向全栈发展。后端开发少不了数据库的操作。<code>MongoDB</code> 是一个基于分布式文件存储的开源数据库系统。本文为大家详细介绍了如何用 <code>node.js</code> + <code>mongoose</code> 玩转 <code>MongoDB</code> 。希望能帮到有需要的人。<p>由于我用Mac开发,以下所有操作都是在Mac下进行。</p>
</blockquote>
<h2>一、 环境搭建</h2>
<h3>安装Node.js</h3>
<blockquote>有 node 环境的可以跳过。</blockquote>
<p><a href="https://link.segmentfault.com/?enc=pM0x2NXlGnHGhXT%2BXrL0fw%3D%3D.YKbdaNGoR12Q7F%2F%2Fq3WGM6isHMAp12whP2YNMZNel0c%3D" rel="nofollow">nodejs官网</a>提供了 macOS 安装包,直接下载安装即可。现在 nodejs 稳定版已经到了 <code>12.11.1</code> 。</p>
<h3>安装MongoDB</h3>
<p><a href="https://link.segmentfault.com/?enc=h3CE9cemE7MsUt0Jczdzng%3D%3D.7RLYBemFD4r%2BiZcr%2FP9Cl1QUCfYGYXym1iBGpYWKXcE%3D" rel="nofollow">MongoDB</a> 是为现代应用程序开发人员和云时代构建的基于文档的通用分布式数据库。</p>
<blockquote>上个月(9月) macOS 包管理器 Homebrew 宣布移除 MongoDB 。原因是去年10月 MongoDB 宣布将其开源许可证从 <code>GNU AGPLv3</code> 切换到 <code>SSPL(Server Side Public License)</code>,以此回应 AWS 等云厂商将 MongoDB 以服务的形式提供给用户而没有回馈社区的行为,MongoDB 希望从软件即服务上获取收入。Homebrew 认为 MongoDB 已经不再属于开源范畴...</blockquote>
<p>言归正传,由于上述原因,我们不能直接使用 <code>brew install mongodb</code> 来安装 MongoDB 了。好在 MongoDB 自己维护了一个定制化的 <a href="https://link.segmentfault.com/?enc=e8rrFiu5Gfsjr7HeGrNlwQ%3D%3D.g%2FBFPPEwyCNujE62fXN26l4x48kktuZBNq21GSnyUN6XLWI362rEV6xnLPlh5Kkh" rel="nofollow">Homebrew tap</a>。并在<a href="https://link.segmentfault.com/?enc=SjiZGqY9rEq2t7h8uILPfA%3D%3D.P9sYsCtLh5c3e2rF5az68rgaSrxMA3w%2B73vO%2B2J6T6GubCcPkf%2BMvCB%2F1RGQqfkw8nV%2BfAGthUEYgXSpPHoIYc%2BwbCINUNKIEVx2ADXGaD60eC0GZHfze2U7qMRP919V9C8yj5JnlGzd3NMUz0Dy5Q%3D%3D" rel="nofollow">Install MongoDB Community Edition</a>更新了安装步骤。</p>
<p>Mac下 MongoDB 的最新安装步骤如下:</p>
<h5>1. 首先安装 <a href="https://link.segmentfault.com/?enc=8LvXCluZanmV1YjldZY4ag%3D%3D.P8WxzTwXp61tkrPxPgheoVh77O3faXVeV1BOn4i9s74%3D" rel="nofollow">Homebrew</a>
</h5>
<p>Homebrew 是 macOS 的包管理器。因为 OSX 默认不包含 Homebrew brew 包,所以要先安装,已经安装过的可以跳过。</p>
<pre><code>/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"</code></pre>
<p>安装过程会有点长,终端输出信息超过一屏,这里我只截取了头尾两部分。<br><img src="/img/bVbyTrm" alt="mg-install-homebrew.jpg" title="mg-install-homebrew.jpg"></p>
<p><img src="/img/bVbyTrn" alt="mg-install-homebrew-2.jpg" title="mg-install-homebrew-2.jpg"></p>
<h5>2. 然后获取下 MongoDB Homebrew Tap</h5>
<pre><code>brew tap mongodb/brew</code></pre>
<p><img src="/img/bVbyTro" alt="mg-tap-mongodb-brew.jpg" title="mg-tap-mongodb-brew.jpg"></p>
<h5>3. 最后安装 MongoDB CE(社区版)</h5>
<pre><code>brew install mongodb-community@4.2</code></pre>
<p><img src="/img/bVbyTrx" alt="mg-brew-install-mongodb.jpg" title="mg-brew-install-mongodb.jpg"></p>
<p>现在你的 Mac 上就已经安装好 MongoDB 环境了。</p>
<h3>安装mongoose</h3>
<blockquote>node.js 是可以直接操作 MongoDB 的,但是通过 MongoDB 命令语法直接编写 MongoDB 验证、数据类型转换和业务逻辑模版比较繁琐。所以我们使用了 mongoose。</blockquote>
<p><a href="https://link.segmentfault.com/?enc=gEuw4LN1a7T0ni%2BomnOS%2Fg%3D%3D.iIDt%2FX5njw5NsYbRyNVc6qjS9TvFy8WQy2ceHFBoO9M%3D" rel="nofollow">mongoose</a> 是 MongoDB 的一个对象模型工具,它对 MongoDB 的常用方法进行了封装,让 node.js 操作 MongoDB 更加优雅简洁。</p>
<p>刚才的 node.js 和 MongoDB 都是安装在全局环境,mongoose 则是安装在你的项目下:</p>
<pre><code>cd your-project
npm i -S mongoose</code></pre>
<p><img src="/img/bVbyTrr" alt="mg-install-mongoose.jpg" title="mg-install-mongoose.jpg"></p>
<p>现在,你的开发环境就已经全部安装好了。</p>
<h2>二、启动MongoDB服务</h2>
<p>要操作 MongoDB ,首先要启动它。<br>有两种方式启动 MongoDB 服务:</p>
<h5>1. 在前台运行</h5>
<pre><code>mongod --config /usr/local/etc/mongod.conf</code></pre>
<p>前台运行的好处就是,可以查看一些反馈和日志,便于调试。另外如果要关闭服务,只需要在终端按 <code>control + c</code> 键即可。<br><img src="/img/bVbyTrF" alt="mg-run-mongodb-in-foreground.jpg" title="mg-run-mongodb-in-foreground.jpg"></p>
<h5>2. 也可以作为 macOS 服务,在后台运行</h5>
<pre><code>brew services start mongodb-community@4.2</code></pre>
<p>好处是开机就自动启动,随时可以使用。<br><img src="/img/bVbyTrA" alt="mg-run-mongodb-as-a-server.jpg" title="mg-run-mongodb-as-a-server.jpg"></p>
<p>这种启动方式,如果要关闭服务,可以通过 <code>stop</code> 命令:</p>
<pre><code>brew services stop mongodb-community@4.2</code></pre>
<p><img src="/img/bVbyTrD" alt="mg-stop-mongodb-service.jpg" title="mg-stop-mongodb-service.jpg"></p>
<p>现在,你的 MongoDB 数据库已经开启了。</p>
<h2>三、操作MongoDB</h2>
<p>操作之前先解释一下MongoDB和mongoose里的一些核心概念。 </p>
<p><strong>MongoDB</strong></p>
<ul>
<li>MongoDB 中的数据记录是一种 <code>BSON</code> 格式的文件(BSON是一种用二进制描述的JSON文件格式)。</li>
<li>MongoDB 将<code>文件</code>存储在<code>集合</code>中,将<code>集合</code>存储在<code>数据库</code>中。</li>
<li>MongoDB 的数据库、集合都不用手动创建。</li>
<li>
<code>集合collection</code>: 相当于关系型数据库中的<code>表table</code>。</li>
<li>
<code>文件document</code>: MongoDB 的数据记录单位,相当于关系型数据库中的<code>记录row</code>。</li>
</ul>
<p><strong>mongoose</strong></p>
<ul>
<li>
<code>schema</code>: 在 mongoose 中,所有的东西都来源于一个 <code>schema</code>,每个<code>schema</code> 映射了一个 MongoDB 的<code>集合</code>,它定义了这个<code>集合</code>中的<code>文档</code>的骨架。</li>
<li>
<code>model</code>: 一个<code>文件</code>的构造器,通过编译<code>schema</code>得到,一个<code>model</code>的实例就是一个<code>文件</code>,<code>model</code>负责从 MongoDB 数据库中创建和读取<code>文档</code>。</li>
</ul>
<p>更多mongoose概念可以在<a href="https://link.segmentfault.com/?enc=lCfsNb7315qWnls7Xg7ydw%3D%3D.xKLAoLoAHpqkj4lGeNBlYESxo9bOJqynQ63X77JsOFd95Loz0Zh%2Fx6%2BOnNc05Huf" rel="nofollow">mongoose guide</a>中查阅。</p>
<p>数据库操作:</p>
<h3>1. 使用 mongoose 连接 MongoDB</h3>
<p>在项目中创建 <code>connection.js</code> 文件</p>
<pre><code>// connection.js file
const mongoose = require('mongoose');
const conn = mongoose.createConnection(
// 连接地址,MongoDB 的服务端口为27017
// dbtest是我要使用的数据库名,当往其中写数据时,MongoDB 会自动创建一个名为dbtest的数据库,不用事先手动创建。
'mongodb://127.0.0.1:27017/dbtest',
// 一些兼容配置,必须加,你不写运行的时候会提示你加。
{
useNewUrlParser: true,
useUnifiedTopology: true
}
)
conn.on('open', () => {
console.log('打开 mongodb 连接');
})
conn.on('err', (err) => {
console.log('err:' + err);
})</code></pre>
<p>运行:</p>
<pre><code>node conection.js</code></pre>
<p><img src="/img/bVbyTrG" alt="mg-connection.jpg" title="mg-connection.jpg"></p>
<p>可以看到打印出“打开 mongodb 连接”,并且运行一直在等待。</p>
<p>这说明现在已经成功连接上 MongoDB 了,接下来可以开始操作数据库了。</p>
<p>为了方便扩展起见,我们先对 <code>connection.js</code> 改造一下,让它作为模块导出,这样就可以在其他地方导入复用了。</p>
<pre><code>// connection.js file
const mongoose = require('mongoose');
const conn = mongoose.createConnection(
'mongodb://127.0.0.1:27017/dbtest',
{
useNewUrlParser: true,
useUnifiedTopology: true
}
)
conn.on('open', () => {
console.log('打开 mongodb 连接');
})
conn.on('err', (err) => {
console.log('err:' + err);
})
module.exports = conn; //commonJs 语法,导出conn模块。</code></pre>
<h3>2. 添加操作</h3>
<blockquote>
<code>save</code> | <code>create</code> 方法</blockquote>
<p>新建<code>insert.js</code>文件</p>
<pre><code>// insert.js file
let mongoose = require('mongoose');
// 导入连接模块
let connection = require('./connection');
// 创建schema
let StudentSchema = new mongoose.Schema({
name: String,
age: Number
})
// 通过connection和schema创建model
let StudentModel = connection.model('Student', StudentSchema);
// 通过实例化model创建文档
let studentDoc = new StudentModel({
name: 'zhangsan',
age: 20
})
// 将文档插入到数据库,save方法返回一个Promise对象。
studentDoc.save().then((doc) => {
console.log(doc)
})</code></pre>
<p>运行:</p>
<pre><code>node insert.js</code></pre>
<p>为了更直观看到操作数据库的结果,推荐大家安装一个数据库可视化工具:<a href="https://link.segmentfault.com/?enc=zWU3vz86KKiMZXjKEpFrdQ%3D%3D.DRfRgVx%2FeOa321QraHNMThe22nCiSvZ7lYxKolQ7P8Y%3D" rel="nofollow">Robo3T</a>,下载<a href="https://link.segmentfault.com/?enc=elosjkhOXQgLdLndQA%2B12w%3D%3D.YY0RRXE2NaYZ8YMdPS9QQH8WLyqvW8zQmbFUDF01uG4%3D" rel="nofollow">mac版</a>安装即可。<br><img src="/img/bVbyTrR" alt="mg-robo3T-download2.jpg" title="mg-robo3T-download2.jpg"></p>
<p>点击 Robo3T 左上角连接我们的数据库后,可以看到 MongoDB 自动帮我们生成了数据库和集合,并且已经插入了一条记录:<br><img src="/img/bVbyTrS" alt="mg-insert.jpg" title="mg-insert.jpg"></p>
<p>或者还可以直接通过Model的create方法直接插入数据,返回的也是一个Promise:</p>
<pre><code>StudentModel.create({
name: 'lisi',
age: 19
}).then((doc) => {
console.log(doc)
})</code></pre>
<h3>3. 读取操作</h3>
<blockquote>
<code>find</code> 方法</blockquote>
<p>为更加合理复用代码,我们先把 StudentSchema 和 StudentModel 抽离出来:</p>
<p>新建<code>StudentSchema.js</code>文件</p>
<pre><code>// StudentSchema.js file
const mongoose = require('mongoose');
let StudentSchema = mongoose.Schema({
name: String,
age: Number
})
module.exports = StudentSchema;</code></pre>
<p>新建<code>StudentModel.js</code>文件</p>
<pre><code>// StudentModel.js file
const connection = require('./connection');
const StudentSchema = require('./StudentSchema');
let StudentModel = connection.model('Student', StudentSchema);
module.exports = StudentModel;</code></pre>
<p>然后新建<code>query.js</code>文件</p>
<pre><code>// query.js file
const StudentModel = require('./StudentModel');
// 富查询条件,对象格式,键值对,下面为查询 name 为 lisi 的记录
StudentModel.find({name: 'lisi'}).then(doc => {
console.log(doc);
})</code></pre>
<p>运行</p>
<pre><code>node query.js</code></pre>
<p><img src="/img/bVbyTrU" alt="mg-guery.jpg" title="mg-guery.jpg"></p>
<p>可以看到<code>name</code>为<code>lisi</code>的记录被打印了出来。</p>
<p>如果想查询整个集合:</p>
<pre><code>// 不放查询条件即查询所有的记录
StudentModel.find({}).then(doc => {
console.log(doc);
})</code></pre>
<p><img src="/img/bVbyTrY" alt="mg-guery-all.jpg" title="mg-guery-all.jpg"></p>
<p>可以看到集合中的所有记录被打印了出来。</p>
<h3>4. 更新操作</h3>
<blockquote>
<code>update | updateOne | updateMany</code> 方法</blockquote>
<p>新建<code>update.js</code>文件</p>
<pre><code>// update.js file
const StudentModel = require('./StudentModel');
// update 方法接收2个参数,第一个是查询条件,第二个是修改的值
// 下面把name为lisi的记录,将他的age修改为80
StudentModel.update({name: 'lisi'}, {age: 80}).then(result => {
console.log(result)
})</code></pre>
<p>进入 Robo3T,可以看到数据被更改,切换到表格模式更加直观:<br><img src="/img/bVbyTr0" alt="mg-update2.jpg" title="mg-update2.jpg"></p>
<p>不过在终端,提示<code>DeprecationWarning: collection.update is deprecated. Use updateOne, updateMany, or bulkWrite instead.</code><br><img src="/img/bVbyTr2" alt="mg-update.jpg" title="mg-update.jpg"></p>
<p>意思是建议我们使用 <code>updateOne</code>、<code>updateMany</code>或者<code>bulkWrite</code></p>
<ul>
<li>update 更新查询到的所有结果,方法已经不提倡使用,已被updateMany替代。</li>
<li>updateOne 如果查询到多条结果,只更新第一条记录。</li>
<li>upateMany 更新查询到的所有结果。</li>
<li>bulkWrite 提供可控执行顺序的批量写操作。</li>
</ul>
<p>为了代码的健壮性,我们应该根据建议将update方法换成updateMany方法。</p>
<p>另外,终端的输出<code>{ n: 1, nModified: 1, ok: 1 }</code>的意思是:</p>
<ul>
<li>“n: 1”:查询到1条记录。</li>
<li>“nModified: 1”:需要修改1条记录。(如果修改值和原始值相同,则需要修改的就是0条)</li>
<li>“ok: 1”:修改成功1条。</li>
</ul>
<h3>5. 删除操作</h3>
<blockquote>
<code>remove|removeOne|removeMany|bulkWrite</code> 方法</blockquote>
<p>新建<code>remote.js</code>文件</p>
<pre><code>// remove.js file
const StudentModel = require('./StudentModel');
// delete 方法接收1个参数,就是查询条件
// 下面把name为lisi的记录删除
StudentModel.remove({name:'lisi'}).then((result) => {
console.log(result);
});</code></pre>
<p>进入 Robo3T,可以看到集合里已经没有name为lisi的记录了:<br><img src="/img/bVbyTr5" alt="mg-remove2.jpg" title="mg-remove2.jpg"></p>
<p>在看终端的输出,跟update类似,也提示建议使用新的方法代替。<br><img src="/img/bVbyTr6" alt="mg-remove.jpg" title="mg-remove.jpg"></p>
<p>意思是建议我们使用 <code>removeOne</code>、<code>removeMany</code>或者<code>bulkWrite</code></p>
<p>remove 删除查询到所有结果,方法已经不提倡使用,已被removeMany替代。<br>removeOne 如果查询到多条结果,只删除第一条记录。<br>removeMany 删除查询到所有结果。<br>bulkWrite 提供可控执行顺序的批量写操作。</p>
<p>另外,终端的输出<code>{ n: 1, ok: 1, deletedCount: 1 }</code>的意思跟update的类似,就不累述了。</p>
<p>现在我们已经成功地对 MongoDB 数据库进行了 CRUD(添加、读取、更新、删除)操作。欢呼~</p>
<p>更多高级操作,可以到<a href="https://link.segmentfault.com/?enc=2J5BNBWm8mO7kTH03TWSUw%3D%3D.J7704eOVJroh2FC6yk%2FuI8C99x8COF0wHrRuFmw1Iw5JIA3KOXTaTrp9J8xhjo6j" rel="nofollow">mongoose API 文档</a>中查阅。</p>
<h2>四、总结</h2>
<p>梳理一下,主要讲了这些内容:</p>
<ol>
<li>
<code>node.js+MongoDB+mongoose</code> 在Mac下的环境搭建,注意使用最新的 <code>MongoDB</code> 的安装方式。</li>
<li>在Mac下如何启动和关闭 <code>MongoDB</code> 服务。</li>
<li>介绍了 <code>MongoDB</code> 和 <code>mongoose</code> 的基本核心概念。</li>
<li>使用 mongoose 连接以及增删改查 MongoDB 操作。可以使用 <code>Robo3T</code> 来更直观地观察数据库。</li>
</ol>
<p>前端也能玩转数据库开发。<br>欢迎交流~</p>
<blockquote>文章源码地址:<a href="https://link.segmentfault.com/?enc=tWgcTQOEYZZhdm0MFVpnFw%3D%3D.%2FHOr5AWeiynLbM%2F3Iu1d9GwLzj%2BIsCZwvQ4JqSs0MRq7Ofo9NlJa1KeeqoY8mmj2" rel="nofollow">https://github.com/yc111/mong...</a>
</blockquote>
<p>相关网站: <br><a href="https://link.segmentfault.com/?enc=BrOFT0rlmDk%2Bon%2F85iaCCg%3D%3D.%2BEjh%2FTWDAuWuIuqpYZgnbLubicNlJBKs1nWnCkwlrIM%3D" rel="nofollow">Homebrew官网</a> <br><a href="https://link.segmentfault.com/?enc=3bqBD1vFvoR5jO%2FDPQIdKQ%3D%3D.h6fGeyncaT4XMq08ENyX2OPccVeXQwFyfDJxASuiB%2Fk%3D" rel="nofollow">MongoDB官网</a> <br><a href="https://link.segmentfault.com/?enc=2QrKd%2BrpHLP5ui8YKGRk3g%3D%3D.XhtPGMTdPaAprImWcj3NaosWE19ttVS9MYvVhYuAbHg%3D" rel="nofollow">monggose官网</a> <br><a href="https://link.segmentfault.com/?enc=UeXh1faO%2BfAFfHIrJB0YKw%3D%3D.3tdP1cu97%2Fg12BGPLvVxa3iZ2ZY%2FGmZ3HkRB0vPfQ5I%3D" rel="nofollow">Robo3T官网</a> <br><a href="https://link.segmentfault.com/?enc=Bf14YA5D4ygcLi19qvtQsQ%3D%3D.KOJhgIVdVXWrcmBw%2F9R2PJmemQMdRZpJK367HAQrVm9afOwKPvYJz2F5W0GtjoJruBdVhv4J1ggYYzUqDN9Xrw%3D%3D" rel="nofollow">macOS 包管理器 Homebrew 移除 MongoDB</a> </p>
<p>-- <br>欢迎转载,转载请注明出处: <br><a href="https://link.segmentfault.com/?enc=jDDruZNLfc4RPSFsbQOzsw%3D%3D.kYD4ny7BSJxxQRl8i%2Bo8GIql%2FRcBXWgvB5o3hgfCFarNAC5XxlZBAXbqqHqaO6zgWg51qrhI%2FvwbJW49BZO7OUxHaoaSKhURQFYersMYLkbZhfX%2BxR65yJQ56dk0QBoiVR%2BPFAhMXZzUb4nzcY8%2BfaxecHkqjX5JD9crg2As3z8%3D" rel="nofollow">https://champyin.com/2019/10/...</a></p>
<p>本文同步发表于:<br><a href="https://link.segmentfault.com/?enc=zISFctpz8LlCs9yZsF8Dpg%3D%3D.K35NznvqajcT3SZvD%2BSMcHmRpocS4m2OK6V1knMQhauDUHoZXhmCi5yWEbqssBGs" rel="nofollow">node.js操作数据库之MongoDB+mongoose篇 | 掘金</a></p>
GitHub项目徽标
https://segmentfault.com/a/1190000020600526
2019-10-06T13:04:12+08:00
2019-10-06T13:04:12+08:00
champyin
https://segmentfault.com/u/champyin
2
<h2>前言</h2>
<p>GitHub徽标,GitHub Badge,你也可以叫它徽章。就是在项目README中经常看到的那些表明构建状态或者版本等信息的小图标。就像这样:<img src="/img/bVbyBiy" alt="github-badge.jpg" title="github-badge.jpg">这些好看的小图标不仅简洁美观,而且包含了清晰易读的信息,在README中使用小徽标能够为自己的项目说明增色不少!如何给自己的项目加上小徽标呢?</p>
<h3>一、关于徽标</h3>
<ol>
<li>徽标图片分左右两部分,左边是标题,右边是内容,就像是键值对。</li>
<li>GitHub徽标官网是 <a href="https://link.segmentfault.com/?enc=HhohSQ%2Fua70z1QCES6373g%3D%3D.yb0g9nVN7j7YDa1TSa%2FIsaIKDeZdFgcYhQN0W%2BmqQio%3D" rel="nofollow">https://shields.io/</a>
</li>
<li>图标规范<br><img src="/img/bVbyBiA" alt="badge-rule2.png" title="badge-rule2.png">
</li>
</ol>
<h3>二、如何添加动态徽标</h3>
<p>动态徽标是指如果项目状态发生变化,会自动更新状态的徽标,它能保证用户看到的信息就是项目当前的真实状态。</p>
<p>常用的动态徽标有:</p>
<ul>
<li>持续集成状态</li>
<li>项目版本信息</li>
<li>代码测试覆盖率</li>
<li>项目下载量</li>
<li>贡献者统计等等</li>
</ul>
<p>这里以<code>Travis CI</code> 的持续集成状态为例。没有接触过 <code>Travis CI</code>的可以看我的上一篇文章 <a href="https://link.segmentfault.com/?enc=nuorEy1z7OyHWOu%2BRvckkw%3D%3D.mug%2FBaAGBwfLqdlsrJAJ5G85AyiJcycjZVnAypTpH8P0SATHWbnVtjjSKsqF73pur%2BrZub66LOCT4CnEa21zoEV6Kn20J51Wsvf2NtpikIPEnvjw9iJq3IPF7d2preWFuNDcE27nG%2BvBbeKeKrscrYjNHkIbaprgrbsH4uOvh%2Bkt0GQEnLMfp5mR3nI8lfwkl2FH3aPq1rMPgpzULPLvWDuCSozsx454aE5BASsVPvM%3D" rel="nofollow">利用Travis CI+GitHub实现持续集成和自动部署</a></p>
<ol>
<li>登录 <code>Travis CI</code>,进入配置过构建的项目,在项目名称的右边有个 <code>build passing</code> 或者 <code>build failing</code> 徽标。</li>
<li>点击它,在弹出框中,就可以看到 <code>Travis CI</code> 为你提供的各种格式的徽章地址了。</li>
<li>
<p>你可以根据需要选择格式,imageUrl格式大概是这个样子:</p>
<pre><code>https://www.travis-ci.org/{your-name}/{your-repo-name}.svg?branch=master</code></pre>
<p>markdown格式大概是这个样子:</p>
<pre><code>[![Build Status](https://www.travis-ci.org/{your-name}/{your-repo-name}.svg?branch=master)](https://www.travis-ci.org/{your-name}/{your-repo-name})</code></pre>
</li>
<li>简单起见,我选择 <code>markdown</code> 格式。将内容复制后,打开项目的README文档,在顶部位置粘贴。</li>
<li>经过前4步,小徽章就搞定了。将README文档push到远程,刷新GitHub页面,过一会,就会看到README上面已经有了持续集成状态图标了:<img src="/img/bVbyBiB" alt="build-passing.png" title="build-passing.png"><br>之所以要过一会才加载出来,是因为它要动态从 <code>Travis CI</code> 平台获取状态。</li>
</ol>
<h3>三、如何自定义徽标</h3>
<p><a href="https://link.segmentfault.com/?enc=x%2FYcgXZkztJfPZ0qDJ8XQQ%3D%3D.cL4QGsU8qEJiOa8Gwfy6%2Bx%2FxltXl51TxSKB1P%2BpuHp8%3D" rel="nofollow">shields.io</a> 提供了自定义徽标的功能。</p>
<h5>徽标图标格式</h5>
<pre><code>https://img.shields.io/badge/{徽标标题}-{徽标内容}-{徽标颜色}.svg</code></pre>
<h5>带链接的徽标</h5>
<pre><code>[![](https://img.shields.io/badge/{徽标标题}-{徽标内容}-{徽标颜色}.svg)]({linkUrl})</code></pre>
<h5>变量说明</h5>
<ul>
<li>徽标标题:徽标左边的文字</li>
<li>徽标内容:徽标右边的文字</li>
<li>徽标颜色:徽标右边的背景颜色,可以是颜色的16进制值,也可以是颜色英文。支持的颜色英文如下:<br><img src="/img/bVbyBiH" alt="shields.io-color.jpg" title="shields.io-color.jpg">
</li>
</ul>
<p>变量之间用 <code>-</code> 连接。将这3个变量替换为你需要的内容即可生成一个自定义的徽标。</p>
<h5>举个栗子</h5>
<p>例如下面这个是我的博客的徽标:</p>
<pre><code>[![](https://img.shields.io/badge/blog-@champyin-red.svg)](https://champyin.com)</code></pre>
<p>效果:<br><a href="https://link.segmentfault.com/?enc=aDTPjI3lKCP06NqX3awsgw%3D%3D.BBQ4FQtIfC5kF%2FuYwJwKzH1SRx53UUFOTenlikswiyI%3D" rel="nofollow"><img src="/img/remote/1460000020600529" alt="" title=""></a><br>点击该徽标会打开对应的linkUrl地址,即直接跳到我的博客。</p>
<h5>进阶</h5>
<p>除了上面所说的3个参数,<a href="https://link.segmentfault.com/?enc=3RwKK7IlwtBoIjo6gvZkjg%3D%3D.RkwGQ9%2BByBPAkvn2wqU4hztKfwpjtK1uwOKveM%2Fz%2BU4%3D" rel="nofollow">shields.io</a> 还提供了一些 <code>query string</code> 来控制徽标样式。使用方式跟URL的query string一致:徽标图标地址?{参数名}={参数值},多个参数用 <code>&</code> 连接:</p>
<pre><code>https://img.shields.io/badge/{徽标标题}-{徽标内容}-{徽标颜色}.svg?{参数名1}={参数值1}&{参数名2}={参数值2}</code></pre>
<p>常用的 <code>query string</code> 参数有:</p>
<ul>
<li>style:控制徽标主题样式,style的值可以是: <code>plastic</code> | <code>flat</code> | <code>flat-square</code> | <code>social</code> 。</li>
<li>label:用来强制覆盖原有徽标的标题文字。</li>
<li>colorA:控制左半部分背景颜色,只能用16进制颜色值作为参数,不能使用颜色英文。</li>
<li>colorB:控制右半部分背景颜色。</li>
</ul>
<p><strong>以style参数为例</strong></p>
<p><code>plastic</code> 立体效果:<br><img src="/img/remote/1460000020600530" alt="" title=""></p>
<pre><code>![](https://img.shields.io/badge/blog-@champyin-orange.svg?style=plastic)
</code></pre>
<p><code>flat</code> 扁平化效果,也是默认效果:<br><img src="/img/remote/1460000020600531" alt="" title=""></p>
<pre><code>![](https://img.shields.io/badge/blog-@champyin-yellow.svg?style=flat)</code></pre>
<p><code>flat-square</code> 扁平 + 去圆角效果:<br><img src="/img/remote/1460000020600532" alt="" title=""></p>
<pre><code>![](https://img.shields.io/badge/blog-@champyin-success.svg?style=flat-square)</code></pre>
<p><code>social</code> 社交样式效果:<br><img src="/img/remote/1460000020600533" alt="" title=""></p>
<pre><code>![](https://img.shields.io/badge/blog-@champyin-blue.svg?style=social)</code></pre>
<p>还有很多参数,用法类似。更多信息可以到<a href="https://link.segmentfault.com/?enc=jqA1esC08PeLkahpEuTsXA%3D%3D.Nmvqkv0IWfTH71blfuCEnj1SPt40%2Fu8S46MHw%2Fj9ndo%3D" rel="nofollow">shields.io</a>查阅。</p>
<h3>总结</h3>
<p>徽标简洁又不失内容,使用简单又不失灵活。如果你的项目还没有使用过徽标,那么不妨试试给你的项目中添加一个,你会爱上它。</p>
<h5>PS:<code>刚发现segmentfault会对代码引用里的markdown图片源码做转换...导致看不到markdown源码。被转换的源码可以在</code><a href="https://link.segmentfault.com/?enc=WYgPqaAwn2x5kUfThCsg%2FA%3D%3D.Kb%2BICtRsoFciCG52COjCWO0nM8fFeZV2OWODBarK687dJuaPK04pYbYBPc1dG72k8aeZeyzeqX2ng8VvxDioTHKBM4ZUs4KOIjoGKgPk2ls%3D" rel="nofollow">这里</a><code>查看。</code>
</h5>
<p>--</p>
<p>欢迎转载,转载请注明出处:<a href="https://link.segmentfault.com/?enc=3O4oohLGiD%2FEaoBUOJXuoA%3D%3D.MAh%2BqEVww579%2Fj8X7IS4ZTms9xtr8gxMmpPuYdY5EWPOjcJapGaBXs%2FrIidsCQLWGSyownkVfCwikohaweKRrySbhlGTwZTevWp%2BSLfMoA0%3D" rel="nofollow">https://champyin.com/2019/10/...</a></p>
<p>本文同步发表于:<br><a href="https://link.segmentfault.com/?enc=IKWe6ssiIo6eR%2BZIZo02gA%3D%3D.d5f1UWz84kI32%2BZc6LqSpMLHZo%2FoeZ0OOizEbeGBZgfKmbadoz9brrPmo01z0FEv" rel="nofollow">GitHub项目徽标 | 掘金</a></p>
利用Travis CI+GitHub实现持续集成和自动部署
https://segmentfault.com/a/1190000020592707
2019-10-04T12:06:09+08:00
2019-10-04T12:06:09+08:00
champyin
https://segmentfault.com/u/champyin
0
<h2>前言</h2>
<p>如果你手动部署过项目,一定会深感持续集成的必要性,因为手动部署实在又繁琐又耗时,虽然部署流程基本固定,依然容易出错。</p>
<p>如果你很熟悉持续集成,一定会同意这样的观点:“使用它已经成为一种标配”。</p>
<blockquote>什么是持续集成<br>Continuous Integration(CI) is a development practice that requires developers to integrate code into a shared repository several times a day. Each check-in is then verified by an automated build, allowing teams to detect problems early.<br>———ThoughtWorks <br>翻译过来就是:持续集成是一个开发行为,它要求开发者每天多次将代码集成到一个共享的仓库,每次提交都会被自动构建所检查,团队可因此提前检测出问题。</blockquote>
<p>持续集成的工具非常多,例如用java语言开发的Jenkins,由于其可以在多台机器上进行分布式地构建和负载测试的特性,很多大公司都在使用它。</p>
<p>但是Jenkins的不加修饰的界面界面让我有些嫌弃... </p>
<p>随着GitHub的发展,出现了越来越多支持GitHub的CI/CD产品。在GitHub市场上,可以看到,已经支持的持续集成服务提供商已超过300多家(<a href="https://link.segmentfault.com/?enc=Ji9W6yiVW0roOpGnPNiR4Q%3D%3D.HBjV5l6iUow5ZgQTkpOe8VeX0sen%2BNH%2BBHjM3hWA70STyhM%2Bw0utCiMdTLjJ0iDDAnx6HStaQkcAGbRiwnB3Wg%3D%3D" rel="nofollow">详情</a>)。</p>
<p><img src="/img/bVbyzfO" alt="github continuous integration.jpg" title="github continuous integration.jpg"></p>
<p>选择Travis CI,是因为身边很多朋友的推荐。 </p>
<p>下面分享一下我是如何利用Travis CI+GitHub实现持续集成和自动部署的,通过我的一些研究和实战经验,希望可以帮到有需要的朋友。</p>
<h3>什么是Travis CI</h3>
<p>Travis CI是用Ruby语言开发的一个开源的分布式持续集成服务,用于自动构建和测试在GitHub托管的项目。支持包括Javascript、Node.js、Ruby等20多种程序语言。对于开源项目免费提供CI服务。你也可以买他的收费版,享受更多的服务。</p>
<blockquote>Travis CI目前有两个官网,分别是 <a href="https://link.segmentfault.com/?enc=wsb4R5QdWgu9saOSXuPwsg%3D%3D.8IL2hpyMzfgQTWrJXV%2FobQNS2uDKB0Y6056boqbB1N0%3D" rel="nofollow">https://travis-ci.org</a> 和 <a href="https://link.segmentfault.com/?enc=UY600o2US5yf3ECc%2BHUujA%3D%3D.m3eeGQxN3MDjgMIlPJx%2FFlW5XcZavVW8fpoF%2FVUxQr4%3D" rel="nofollow">https://travis-ci.com</a> 。<br><a href="https://link.segmentfault.com/?enc=C3AIdx250v50VnRteyhjzA%3D%3D.mWBLxX8043MkvNsq9JQ5H3IJTkvFxSBoBkAbljoSDTI%3D" rel="nofollow">https://travis-ci.org</a> 是旧平台,已经逐渐往新平台 <a href="https://link.segmentfault.com/?enc=2fIBVlQNIwSGBvtQIISbVQ%3D%3D.ER5G4K2bRdNaxRRQgwLW5X3GWFIAbbv7iJaWy6pfKbg%3D" rel="nofollow">https://travis-ci.com</a> 上迁移了。对于私有仓库的免费自动构建,Travis CI在新平台上给予了支持。</blockquote>
<p><img src="/img/bVbyzfQ" alt="travis-CI-0.jpg" title="travis-CI-0.jpg"></p>
<h3>一、获取GitHub Access Token</h3>
<p>Travis CI在自动部署的时候,需要push内容到仓库的某个分支,而访问GitHub仓库需要用户授权,授权方式就是用户提供 Access Token 给Travis CI。 </p>
<p>获取token的位置:<code>GitHub->Settings->Developer Settings->Personal access tokens</code>。</p>
<p>勾选<code>repo</code>下的所有项,以及<code>user</code>下的<code>user:email</code>后,生成一个token,复制token值。</p>
<blockquote>注意:这个token只有现在可以看到,再次进入就看不到了,而且是再也看不到了,忘记了就只能重新生成了,所以要记住保管好。</blockquote>
<p><img src="/img/bVbyzfZ" alt="personal-access-token-variable.jpg" title="personal-access-token-variable.jpg"></p>
<h3>二、使用GitHub账号登录Travis</h3>
<p>进入<a href="https://link.segmentfault.com/?enc=OqN3rKc9eRJBRf6y%2FtpERA%3D%3D.0yRythrCDGNWhipMtMuEU7fkiVVjUtAuT4JOUl6lck4%3D" rel="nofollow">Travis官网</a>,用GitHub账号登录。(我目前使用的是它的旧平台)<br><img src="/img/bVbyzf0" alt="travis-ci-1.jpg" title="travis-ci-1.jpg"></p>
<p>登录后,会在Travis里看到自己GitHub账号下所有的public open source repo。</p>
<h3>三、开启对项目的监控</h3>
<p>选择目标项目,打开右侧开关。<br><img src="/img/bVbyzf2" alt="travis-CI-4.jpg" title="travis-CI-4.jpg"></p>
<h3>四、配置travis</h3>
<ul>
<li>点击开关右侧Settings,进入该项目的travis配置页</li>
<li>勾选触发条件<br><img src="/img/bVbyzf9" alt="travis-CI-7.jpg" title="travis-CI-7.jpg">
</li>
<li>设置全局变量<br><img src="/img/bVbyzgb" alt="travis-CI-8.jpg" title="travis-CI-8.jpg"><br><strong>注意:第一步获取的access token,必须设置</strong> <br>设置好的变量可以在配置文件中以 ${变量名}来引用。<br><img src="/img/bVbyzgd" alt="travis-CI-9.jpg" title="travis-CI-9.jpg">
</li>
</ul>
<h3>五、在项目根目录添加<code>.travis.yml</code>配置文件</h3>
<blockquote>注意文件名以<code>.</code>开头。</blockquote>
<p><strong>Travis CI的一次构建分两个步骤:</strong></p>
<ol>
<li>install安装,安装任何所需的依赖</li>
<li>script脚本,运行构建脚本</li>
</ol>
<p><strong>Travis CI提供了一些构建生命周期的“钩子”</strong> </p>
<p>一个完整的 Travis CI 构建生命周期:</p>
<ol>
<li>OPTIONAL Install <code>apt addons</code>
</li>
<li>OPTIONAL Install <code>cache components</code>
</li>
<li><code>before_install</code></li>
<li><code>install</code></li>
<li><code>before_script</code></li>
<li><code>script</code></li>
<li>OPTIONAL <code>before_cache</code>(for cleaning up cache)</li>
<li>
<code>after_success</code> or <code>after_failure</code>
</li>
<li>OPTIONAL <code>before_deploy</code>
</li>
<li>OPTIONAL <code>deploy</code>
</li>
<li>OPTIONAL <code>after_deploy</code>
</li>
<li><code>after_script</code></li>
</ol>
<p>在 <code>before_install</code>、<code>before_script</code>之前,或者<code>after_script</code>之后,都可以运行自定义命令,详细资料可参考官方文档:<a href="https://link.segmentfault.com/?enc=CvJX159P%2B8OiafNTS32LUg%3D%3D.4EiORptzKzEeUhvQGe3GFpbMKEC%2BAueHD4EafAeWEyACmZgV0MH169xpsvlHjdv3" rel="nofollow">Job Lifecycle</a></p>
<p>我在<code>footprint</code>项目中的<code>.travis.yml</code>完整配置:</p>
<pre><code>language: node_js #设置语言
node_js: "10.16.3" #设置语言版本
cache:
directories:
- node_modules #缓存依赖
# S: Build Lifecycle
install:
- npm i
script:
- npm run build
#after_script前5句是把部署分支的.git文件夹保护起来,用于保留历史部署的commit日志,否则部署分支永远只有一条commit记录。
#命令里面的变量都是在Travis CI里配置过的。
after_script:
- git clone https://${GH_REF} .temp
- cd .temp
- git checkout gh-pages
- cd ../
- mv .temp/.git dist
- cd dist
- git config user.name "${U_NAME}"
- git config user.email "${U_EMAIL}"
- git add .
- git commit -m ":construction_worker:- Build & Deploy by Travis CI"
- git push --force --quiet "https://${Travis_Token}@${GH_REF}" gh-pages:${D_BRANCH}
# E: Build LifeCycle
# 只有指定的分支提交时才会运行脚本
branches:
only:
- master</code></pre>
<h3>Done!</h3>
<p>将 <code>.travis.yml</code> push 到远程,可以看到 travis 开始构建编译了。并且之后每次push代码,travis 都会自动执行<code>.travis.yml</code>里配置的脚本任务了。</p>
<ul>
<li>自动编译:<br><img src="/img/bVbyzge" alt="travis-CI-6.jpg" title="travis-CI-6.jpg">
</li>
<li>构建完,travis 会根据我的配置,自动部署到 GitHub:<br><img src="/img/bVbyzgu" alt="travis-CI-10.jpg" title="travis-CI-10.jpg">
</li>
</ul>
<h3>And One More Thing</h3>
<p>构建成功后,我们就可以在自己的GitHub项目里添加<code>build</code>徽章了。<br>方法:在Travis里,点击项目右侧的徽章,即可获取小徽章地址,将地址放在README.md文档中即可。<br><img src="/img/bVbyzgv" alt="travis-CI-12.jpg" title="travis-CI-12.jpg"><br>效果:<br><img src="/img/bVbyzgw" alt="travis-CI-11.jpg" title="travis-CI-11.jpg"></p>
<p>关于 GitHub 徽章的自定义,可以参考我的另一篇文章 <a href="https://link.segmentfault.com/?enc=lRNJiHvYs54MKhJvaPwjQw%3D%3D.jRrMY83emf6UBqlnKZoI1F9eHDFL%2BxF6coNPSd3dQVch57OkIuzZz1Cte7e3AGD40NMFPescZWkEVz6j9%2BN5MLzIYpMO71Vjy6m1ry6UFIY%3D" rel="nofollow">github项目徽标</a>。</p>
<p>--<br>GOODLUCK!</p>
<p>欢迎转载,转载请注明出处:<a href="https://link.segmentfault.com/?enc=pc%2FXqOzjeNVhbLYddZQU1A%3D%3D.T%2FXyxFYT6aMGvfJ30KXzVz7rdNvSwU4R2vx0g%2BtGxidEvnxbE3hOdGkjHojPF4rhnjkaRJxXjf9HwMRezJ7EWn1pFk8lAf86sFdOF4%2Fd05BrMNd%2BBtvpoZMy5utEXNRskV91w2eeIL7IijrmZMUzPY9z7Z21WP8yaMCQyi4gabBsWiGLUzuUpixt2Old89W32HYtJGvqeQr7fXhGJtYvkVO2kFW7hwaaqwK584S8wL4%3D" rel="nofollow">https://champyin.com/2019/09/...</a></p>
<p>本文同步发表于:<br><a href="https://link.segmentfault.com/?enc=RjbiYEGuf4FbDH6WBhAepQ%3D%3D.1%2BGryjMqjaPPPBfNZUXMJK%2FfhQ2S9VMlBS2cYp74z5vK2%2Bv35PQ7OQkjAnK0MeaF" rel="nofollow">利用Travis CI+GitHub实现持续集成和自动部署 | 掘金</a></p>