刘小夕

刘小夕 查看完整档案

南京编辑  |  填写毕业院校前端宇宙公众号  |  作者 编辑 github.com/YvetteLau 编辑
编辑

本人微信公众号: 前端宇宙

写文不易,Star支持一下?

【github】https://github.com/YvetteLau/...

个人动态

刘小夕 发布了文章 · 2020-09-16

展示代码的6种绝佳方式

很多时候,开发人员需要展示代码段,可能是用于演示,博客或者是摘要。

那么有哪些程序代码的工具呢?

1. Polacode

https://github.com/octref/pol...

如果你喜欢VS Code,那么这个适合你! Polacode 是一个VS Code扩展,可以直接从 VSCode 获取漂亮的代码屏幕截图。

2. carbon.now.sh

https://carbon.now.sh/

创建并共享源代码的精美图片。

开始输入或将文件拖入文本区域以开始使用。

你还可以将 GitHubcarbon 连接起来。

3. codeimg.io

https://codeimg.io/

关于 codeimg 最好的部分是他们已经有社交媒体模板。因此,不必担心画布大小。

从功能上来说,与 carbon.now.sh 相比, codeimg 有更多选项。

这是使用codeimg.io创建的 Facebook 模板

4. Pastie

http://pastie.org/

如果您喜欢极简主义,并且只想共享代码作为链接而不是截图,那么Pastie就是您的理想选择。

这是在朋友/同事之间共享代码的完美选择。 Pastie生成的链接有效期为24小时。

注意:Pastie使用HTTP而不是HTTPS。这意味着它缺乏加密并且不安全。

5. Rust playground

https://play.rust-lang.org/

如果你是 Rust 开发者,那么这个将适合你。

在浏览器界面上运行 Rust 编译器,以尝试使用该语言。和 Pastie 一样,它也可以共享代码段链接。

6. Silicon

https://github.com/Aloxaf/sil...

这是用 Rust 语言实现的 carbon.now.sh。

Silicon 可以解决的carbon.now.sh问题:

  • 没有Internet和浏览器就无法工作。
  • 无法与Shell配合使用。 (尽管有carbon-now-cli,但是它的体验不是很好,特别是当网络不太好时。)

用法:

silicon main.rs -o main.png 

好嘛,如果看到这里,说明是真爱了。要不要给我的 GitHub 增加一个 star。

关注公众号,加入技术交流群

WechatIMG167.jpeg

查看原文

赞 34 收藏 24 评论 0

刘小夕 发布了文章 · 2020-08-19

这8个VSCode插件让你编码嗨到停不下来

拥有合适的工具可以让你的开发工作变得更加轻松。许多开发人员使用 VSCode 作为开发工具,VSCode 允许安装各种扩展工具。

Visual Studio Marketplace 上有太多的可用扩展工具,我们将着重介绍下面8个扩展工具。

即使是最简单的工具也能赋予人们力量去做伟大的事情--比兹·斯通

所有这些插件都可以在 Visual Studio Marketplace 上免费获得。

1. REST Client

REST Client 扩展工具允许你发送 HTTP 请求并直接在 VSCode 中查看响应。再也不需要使用外部应用程序向服务器发送 HTTP 请求。

这是一个非常流行的扩展工具,得到众多开发人员的青睐,获得了超过 100W 次的安装。另外我自己也使用这个插件已经有一段时间了,我觉得它很棒。

发送请求变得如此容易。语法非常简洁,并且提供了很多选项以满足你的需求。简单的 GET 请求只需要一行代码,GET 关键字后跟 URL 即可。

你真的应该尝试一下 REST Client 扩展工具。

2. CSS Peek

如果你是一个 web 开发人员,CSS Peek 绝对是必要的。有了这个扩展工具,将鼠标悬停在元素的类名或元素ID上,就可以看到应用于这个元素的 CSS 规则。

如下图所示:

这个扩展工具不仅仅允许你查看样式。它还有一个“转到”特性,允许你立即跳转到应用于元素的 CSS 规则。这为你节省了大量寻找正确选择器的时间。

3. Beautify

如果你喜欢整洁的代码,那么你肯定会喜欢 Beautify

Beautify 支持 JavaScriptHTMLCSSSassJSON

这个扩展的最大优点是所有选项都是完全可定制的--比如缩进大小和文件是否应该以新行结尾。如果你用几种编程语言编写代码,那么 Beautify 可以涵盖。可以自定义每种编程语言的选项。

该扩展工具的下载量超过500W次,在最受欢迎的已安装扩展工具中排名前20位。

4. Auto Rename Tag

Auto Rename Tag 扩展工具所做的工作很简单,但是很好用。该扩展工具将自动重命名成对的 HTML 标签。如果你正在重命名一个开始标签,它将更改相应的结束标签;反之亦然。

如果你想在开始标签的结束括号中键入时自动添加结束标签,你应该看一看 Auto Close Tag 扩展工具。安装这两个扩展非常有用,将帮助你以更高效和一致的方式编写 HTML

5. Quokka.js

Quokka.js 是编辑器中的原型平台,可以访问项目文件,内联报告等。代码中的值在运行时更新,并在键入代码时显示在IDE中的代码旁边。

简直太棒了!

6. Night Owl

没有一个惊艳的主题,优化VS代码有什么价值?不多,对吧?!既然你花了这么多时间在VS代码上,你最好通过安装一个主题让它看起来更好看。

Night Owl 主题是许多开发人员使用的一个漂亮的主题。根据这个主题的描述,它为我们这些喜欢编写代码到深夜的人进行了微调与优化。

如果你不喜欢 Night Owl 主题,安利一下: Pink Cat BooPanda Syntax, Mini Dark, Gatito Theme, One Monokai,这几个主题色也很好看~

7. JavaScript (ES6) code snippets

最后一个要介绍的扩展工具是 JavaScript (ES6) code snippets 扩展工具。该扩展工具有多个代码片段,可供您来生成ES6代码片段。
例如,键入clg然后按回车可以得到一个console.log。可能您需要一段时间来熟悉所有的代码片段,但是只要掌握了它,您就能非常快的打出ES6代码

8. TabNine

TabNine 是我用过的最好的代码补全工具,TabNine 是一款基于人工智能的代码自动补全工具,TabNine在自动补全时会给出每个候选项的概率,并且按照概率大小进行排序,此外,会给出候选项的来源及地址,这样更加方便查询和阅读。目前已经有超过 30W 次下载。

好嘛,如果看到这里,说明是真爱了。要不要给我的 Github 增加一个 star。

关注公众号,加入技术交流群

WechatIMG167.jpeg

查看原文

赞 31 收藏 22 评论 1

刘小夕 发布了文章 · 2020-06-23

10个打开了我新世界大门的 WebAPI

原来我对这些 Web API 一无所知,又又又打开了我的新世界,未来 Web 可以做到更多,早日一统江湖吧,吼吼吼。

虽然这些 API 很多目前还存在兼容性的问题,但是还是有必要了解一下的,文中的代码,我已经都测试过了。希望你看完之后能够有所收获。

原文链接:https://blog.bitsrc.io/10-use...

你可能已经知道并使用更为流行的 Web APIsWeb WorkerFetch等),但也有少数不那么流行的 API,我个人喜欢使用,并建议你也尝试一下。

这篇文章中描述的所有 Web API 示例都可以在这里找到:

1. Web Audio API

Web Audio API MDN

Web Audio API 允许你在 Web 上操作音频流。它可用于向网络上的音频源添加效果和滤镜。

音频源可以来自 <audio>,视频/音频源文件或音频网络流。

让我们看一个简单的例子:
<body>
    <header>
        <h2>Web APIs<h2>
    </header>
    <div class="web-api-cnt">
        <div class="web-api-card">
            <div class="web-api-card-head"> Demo - Audio </div>
            <div class="web-api-card-body">
                <div id="error" class="close"></div>
                <div>
                    <audio controls data-original="./lovely.mp4" id="audio"></audio>
                </div>
                <div>
                    <button onclick="audioFromAudioFile.init()">Init</button>
                    <button onclick="audioFromAudioFile.play()">Play</button>
                    <button onclick="audioFromAudioFile.pause()">Pause</button>
                    <button onclick="audioFromAudioFile.stop()">Stop</button>
                </div>
                <div>
                    <span>Vol: <input onchange="audioFromAudioFile.changeVolume()" type="range" id="vol" min="1" max="3"
                            step="0.01" value="1" /></span>
                    <span>Pan: <input onchange="audioFromAudioFile.changePan()" type="range" id="panner" min="-1"
                            max="1" step="0.01" value="0" /></span>
                </div>
            </div>
        </div>
    </div>
</body>
<script>
    const l = console.log
    let audioFromAudioFile = (function () {
        var audioContext
        var volNode
        var pannerNode
        var mediaSource

        function init() {
            l("Init")
            try {
                
                audioContext = new AudioContext()
                mediaSource = audioContext.createMediaElementSource(audio)
                volNode = audioContext.createGain()
                volNode.gain.value = 1
                pannerNode = new StereoPannerNode(audioContext, { pan: 0 })

                mediaSource.connect(volNode).connect(pannerNode).connect(audioContext.destination)
                console.log(volNode)
            }
            catch (e) {
                error.innerHTML = "The Web Audio API is not supported in this device."
                error.classList.remove("close")
            }
        }
        function play() {
            audio.play()
        }

        function pause() {
            audio.pause()
        }

        function stop() {
            audio.stop()
        }

        function changeVolume() {
            volNode.gain.value = document.getElementById('vol').value
        }

        function changePan() {
            pannerNode.gain.value = tdocument.getElementById('panner').value
        }

        return {
            init,
            play,
            pause,
            stop,
            changePan,
            changeVolume
        }
    })()
</script>

译者注:源代码有点小问题,上面的代码我已经修改过,可以运行,不过mp4文件换成自己本地有的。

此示例将音频从 <audio> 元素传递到 AudioContext。声音效果(例如声像)在添加到音频输出(扬声器)之前已添加到音频源。

单击 Init 按钮将调用 init 函数。这将创建一个 AudioContext 实例并将其设置为 audioContext。接下来,它创建一个媒体源 createMediaElementSource(audio),将音频元素作为音频源传递。

createGain 创建音量节点 volNode。在这里,我们调整音频的音量。接下来,使用 StereoPannerNode 设置声像效果。最后,将节点连接到媒体源。

我们有一个音量和声像的滑块,拖动它们会影响音量和音频的声像效果。

这个例子有问题,所以该链接也无法正常使用,可以拷贝上面的代码在本地运行

try it

2. Fullscreen API

Fullscreen API MDN

Fullscreen API 让我们能够在 Web app 中启用全屏模式。它使你可以选择要在全屏模式下查看的元素。在 Android 手机中,它将删除浏览器窗口和 Android 顶部状态栏(显示网络状态,电池状态等的地方)。

方法:
  • requestFullscreen 在系统上以全屏模式显示选定的元素,从而关闭其他应用程序以及浏览器和系统UI元素。
  • exitFullscreen 将全屏模式退出到正常模式。
让我们看一个简单的示例,其中我们可以使用全屏模式观看视频:
<body>
    <header>
        <h2>Web APIs<h2>
    </header>
    <div class="web-api-cnt">
        <div class="web-api-card">
            <div class="web-api-card-head"> Demo - Fullscreen </div>
            <div class="web-api-card-body">
                <div id="error" class="close"></div>
                <div> This API makes fullscreen-mode of our webpage possible. It lets you select the Element you want to
                    view in fullscreen-mode, then it shuts off the browsers window features like URL bar, the window
                    pane, and presents the Element to take the entire width and height of the system. In Android phones,
                    it will remove the browsers window and the Android UI where the network status, battery status are
                    displayed, and display the Element in full width of the Android system. </div>
                <div class="video-stage">
                    <video id="video" data-original="./lovely.mp4"></video>
                    <button onclick="toggle()">Toogle Fullscreen</button>
                </div>
                <div> This API makes fullscreen-mode of our webpage possible. It lets you select the Element you want to
                    view in fullscreen-mode, then it shuts off the browsers window features like URL bar, the window
                    pane, and presents the Element to take the entire width and height of the system. In Android phones,
                    it will remove the browsers window and the Android UI where the network status, battery status are
                    displayed, and display the Element in full width of the Android system. </div>
            </div>
        </div>
    </div>
</body>
<script>
    const l = console.log

    function toggle() {
        const videoStageEl = document.querySelector(".video-stage")
        console.log(videoStageEl.requestFullscreen)
        if (videoStageEl.requestFullscreen) {
            if (!document.fullscreenElement) {
                videoStageEl.requestFullscreen()
            }
            else {
                document.exitFullscreen()
            }
        } else {
            error.innerHTML = "Fullscreen API not supported in this device."
            error.classList.remove("close")
        }
    }
</script>

video 元素在 div#video-stage元素中,并带有一个按钮 Toggle Fullscreen

当我们单击 Toggle Fullscreen 按钮时,我们希望使元素 div#video-stage 变为全屏显示。

看一下 toggle 这个函数:

function toggle() {
    const videoStageEl = document.querySelector(".video-stage")
    if(!document.fullscreenElement)
        videoStageEl.requestFullscreen()
    else
        document.exitFullscreen()
}

获取 div#video-stage 元素,并将其实例保留在 videoStageEl 上。

我们用过 document.fullsreenElement 属性可以知道该元素是否处于全屏模式,如果不是全屏模式,可以调用 videoStageEl 上的 requestFullscreen() 方法,使 div#video-stage 接管整个设备视图。

如果在全屏模式下点击 Toggle Fullscreen 按钮,将会调用 document.exitFullcreen() ,从而返回到普通视图。

try it

注:该链接中的视频资源找不到了,但是全屏功能是正常的,大家也可以在本地测试

3. Web Speech API

Web Speech API MDN

Web Speech API 让我们可以将语音合成和语音识别功能添加到Web应用中。

使用此 API ,我们将能够向Web应用发出语音命令,就像在 Android 上通过其 Google Speech 或像在Windows 中使用 Cortana 一样。

让我们看一个简单的例子。我们将看到如何使用 Web Speech API 实现文本到语音和语音到文本的转换。

<body>
    <header>
        <h2>Web APIs<h2>
    </header>
    <div class="web-api-cnt">
        <div id="error" class="close"></div>
        <div class="web-api-card">
            <div class="web-api-card-head"> Demo - Text to Speech </div>
            <div class="web-api-card-body">
                <div>
                    <input placeholder="Enter text here" type="text" id="textToSpeech" />
                </div>
                <div>
                    <button onclick="speak()">Tap to Speak</button>
                </div>
            </div>
        </div>
        <div class="web-api-card">
            <div class="web-api-card-head"> Demo - Speech to Text </div>
            <div class="web-api-card-body">
                <div>
                    <textarea placeholder="Text will appear here when you start speeaking."
                        id="speechToText"></textarea>
                </div>
                <div>
                    <button onclick="tapToSpeak()">Tap and Speak into Mic</button>
                </div>
            </div>
        </div>
    </div>
</body>
<script>

    try {
        var speech = new SpeechSynthesisUtterance()
        var recognition = new SpeechRecognition()
    } catch (e) {
        error.innerHTML = "Web Speech API not supported in this device."
        error.classList.remove("close")
    }

    function speak() {
        speech.text = textToSpeech.value
        speech.volume = 1
        speech.rate = 1
        speech.pitch = 1
        alert(window.speechSynthesis)
        window.speechSynthesis.speak(speech)
    }

    function tapToSpeak() {
        recognition.onstart = function () { }

        recognition.onresult = function (event) {
            const curr = event.resultIndex
            const transcript = event.results[curr][0].transcript
            speechToText.value = transcript
        }

        recognition.onerror = function (ev) {
            console.error(ev)
        }

        recognition.start()
    }

</script>

第一个演示 Demo - Text to Speech 演示了通过一个简单的输入框接收输入的文字以及一个按钮点击后输出语音的功能。

看一下 speak 函数:
function speak() {
    speech.text = textToSpeech.value
    speech.volume = 1
    speech.rate = 1
    speech.pitch = 1
    window.speechSynthesis.speak(speech)
}

它实例化 SpeechSynthesisUtterance() 对象,将我们在输入框中输入的文本转换为语音。然后,调用语音对象 SpeechSynthesisspeak 函数,使输入框中的文本在我们的扬声器中放出。

第二个演示 Demo - Speech to Text 是语音识别演示。我们点击 Tap and Speak into Mic 按钮,对着麦克风说话,我们说的单词就被翻译成了文本。

Tap and Speak into Mic 按钮单击后调用 tapToSpeak 函数:

function tapToSpeak() {
    recognition.onstart = function () { }

    recognition.onresult = function (event) {
        const curr = event.resultIndex
        const transcript = event.results[curr][0].transcript
        speechToText.value = transcript
    }

    recognition.onerror = function (ev) {
        console.error(ev)
    }

    recognition.start()
}

很简单,实例化 SpeechRecognition,然后注册事件处理程序和回调。在语音识别开始时调用 onstart,在发生错误时调用 onerror 。每当语音识别捕获到一条线时,就会调用 onresult

可以看到,在 onresult 回调中,我们提取文本并将其设置到文本区域。所以当我们对着麦克风说话时,这些内容会输出在文本区域中。

try it

译者:我的爪机和电脑 Chrome(V83) 都不能支持该 API

4. Bluetooth API

Bluetooth API MDN

实验技术

Bluetooth API 使得我们可以访问手机上的低功耗蓝牙设备,并使用它来将网页中的数据共享到另一台设备上。

想象一下能够创建一个Web聊天应用,该应用程序可以通过蓝牙发送和接收来自其他手机的消息。

基础 APInavigator.bluetooth.requestDevice。调用它将使浏览器提示用户选择一个设备,使他们可以选择一个设备或取消请求。

navigator.bluetooth.requestDevice 需要一个对象。该对象定义了用于返回与过滤器匹配的蓝牙设备的过滤器。

让我们看一个简单的演示。本演示将使用 navigator.bluetooth.requestDeviceAPI 从BLE设备检索基本设备信息。

<body>
    <header>
        <h2>Web APIs<h2>
    </header>
    <div class="web-api-cnt">
        <div class="web-api-card">
            <div class="web-api-card-head"> Demo - Bluetooth </div>
            <div class="web-api-card-body">
                <div id="error" class="close"></div>
                <div>
                    <div>Device Name: <span id="dname"></span></div>
                    <div>Device ID: <span id="did"></span></div>
                    <div>Device Connected: <span id="dconnected"></span></div>
                </div>
                <div>
                    <button onclick="bluetoothAction()">Get BLE Device</button>
                </div>
            </div>
        </div>
    </div>
</body>
<script>
    function bluetoothAction() {
        if (navigator.bluetooth) {
            navigator.bluetooth.requestDevice({
                acceptAllDevices: true
            }).then(device => {
                dname.innerHTML = device.name
                did.innerHTML = device.id
                dconnected.innerHTML = device.connected
            }).catch(err => {
                error.innerHTML = "Oh my!! Something went wrong."
                error.classList.remove("close")
            })
        } else {
            error.innerHTML = "Bluetooth is not supported."
            error.classList.remove("close")
        }
    }
</script>

设备的信息会展示出来。单击按钮 Get BLE Device 则调用 bluetoothAction 函数。

function bluetoothAction() {
    if (navigator.bluetooth) {
        navigator.bluetooth.requestDevice({
            acceptAllDevices: true
        }).then(device => {
            dname.innerHTML = device.name
            did.innerHTML = device.id
            dconnected.innerHTML = device.connected
        }).catch(err => {
            
            error.innerHTML = "Oh my!! Something went wrong."
            error.classList.remove("close")
        })
    } else {
        error.innerHTML = "Bluetooth is not supported."
        error.classList.remove("close")
    }
}

bluetoothAction 函数调用 navigator.bluetooth.requestDevice API,参数设置为 acceptAllDevices: true,这将使其扫描并列出附近所有开启了蓝牙的设备。它返回的是一个 Promise

try it

译者注:电脑上 Chrome 浏览器上测试了下,是支持该API的。

5. Channel Messaging API

Channel Messaging API MDN

Channel Messaging API 允许两个不同的脚本运行在同一个文档的不同浏览器上下文(比如两个 iframe,或者文档主体和一个 iframe,或者两个 worker)来直接通讯,在每端使用一个端口(port)通过双向频道(channel)向彼此传递消息。。

首先创建一个 MessageChannel 实例:

new MessageChannel()

这将返回一个 MessagePort 对象(通讯信道)。

然后,就可以通过 MessagePort.port1MessageChannel.port2 设置端口。

实例化 MessageChannel 的上下文将使用 MessagePort.port1,另一个上下文将使用 MessagePort.port2。然后,就可以使用 postMessage API 传递消息了。

每个浏览器上下文都使用 Message.onmessage 监听消息,并使用事件的 data 属性获取消息内容。

让我们看一个简单的示例,在这里我们可以使用 MessageChannel 在文档和 iframe 之间发送文本。

译者注:这个demo,原文中代码有错误,译者对代码进行了修改,亲测可以正常运行

<body>
    <header>
        <h2>Web APIs<h2>
    </header>
    <div class="web-api-cnt">
        <div class="web-api-card">
            <div class="web-api-card-head"> Demo - MessageChannel </div>
            <div class="web-api-card-body">
                <div id="error" class="close"></div>
                <div id="displayMsg">
                </div>
                <div>
                    <input id="input" type="text" placeholder="Send message to iframe" />
                </div>
                <div>
                    <button onclick="sendMsg()">Send Msg</button>
                </div>
                <div>
                    <iframe id="iframe" data-original="./iframe.content.html"></iframe>
                </div>
            </div>
        </div>
    </div>
</body>
<script>
    try {
        var channel = new MessageChannel()
        var port1 = channel.port1
    } catch (e) {
        error.innerHTML = "MessageChannel API not supported in this device."
        error.classList.remove("close")
    }

    iframe.addEventListener("load", onLoad)

    function onLoad() {
        port1.onmessage = onMessage
        iframe.contentWindow.postMessage("load", '*', [channel.port2])
    }

    function onMessage(e) {
        const newHTML = "<div>" + e.data + "</div>"
        displayMsg.innerHTML = displayMsg.innerHTML + newHTML
    }

    function sendMsg() {
        port1.postMessage(input.value)
    }

</script>

注意 iframe 的标签,我们在上面加载了一个 iframe.content.html 文件。按钮和文本是我们键入文字并向 iframe 发送消息的地方。

const channel = new MessageChannel()
const port1 = channel.port1
iframe.addEventListener("load", onLoad)
function onLoad() {
    port1.onmessage = onMessage
    iframe.contentWindow.postMessage("load", '*', [channel.port2])
}

function onMessage(e) {
    const newHTML = "<div>" + e.data + "</div>"
    displayMsg.innerHTML = displayMsg.innerHTML + newHTML
}

function sendMsg() {
    port1.postMessage(input.value)
}

我们初始化了 MessageChannelport1 。我们向 iframe 添加了 load 监听。在这里,我们在port1 注册了 onmessage 监听,然后使用 postMessageAPI 将消息发送到 iframe 。看到 port2 被向下发送到 iframe

让我们看一下 iframeiframe.content.html
<body>
    <div class="web-api-cnt">

        <div class="web-api-card">
            <div class="web-api-card-head">
                Running inside an <i>iframe</i>
            </div>
            <div class="web-api-card-body">
                <div id="iframeDisplayMsg">
                </div>
                <div>
                    <input placeholder="Type message.." id="iframeInput" />
                </div>

                <div>
                    <button onclick="sendMsgiframe()">Send Msg from <i>iframe</i></button>
                </div>

            </div>
        </div>

    </div>
</body>

<script>
    var port2
    window.addEventListener("message", function(e) {
        port2 = e.ports[0]
        port2.onmessage = onMessage
    })

    function onMessage(e) {
        const newHTML = "<div>"+e.data+"</div>"
        iframeDisplayMsg.innerHTML = iframeDisplayMsg.innerHTML + newHTML
    }

    function sendMsgiframe(){
        port2.postMessage(iframeInput.value)
    }
</script>

在这里,我们注册了一个消息事件处理函数。我们检索 port2 并在其上设置 onmessage 事件处理函数。现在,我们可以从 iframe 接收消息并将其发送到其父文档。

try it

译者注:这个 try 不起来哈,可以拷贝我上面的代码在本地尝试

6. Vibration API

Vibration API MDN

大多数现代移动设备包括振动硬件,其允许软件代码通过使设备摇动来向用户提供物理反馈。Vibration APIWeb 应用程序提供访问此硬件(如果存在)的功能,如果设备不支持此功能,则不会执行任何操作。

navigator.vibrate(pattern) 控制振动,pattern 是描述振动模式的单个数字或数字数组。

navigator.vibrate(200);
navigator.vibrate([200]);

以上两个例子都可以使设备振动 200 ms 并停止.

navigator.vibrate([200,300,400])

这将使设备振动200毫秒,暂停300毫秒,振动400毫秒,然后停止。

可以通过传递0,[][0,0,0](全零数组)来停止振动。

我们看一个简单的演示:
<body>
    <header>
        <h2>Web APIs<h2>
    </header>
    <div class="web-api-cnt">

        <div class="web-api-card">
            <div class="web-api-card-head">
                Demo - Vibration
            </div>
            <div class="web-api-card-body">
                <div id="error" class="close"></div>
                <div>
                    <input id="vibTime" type="number" placeholder="Vibration time" />
                </div>

                <div>
                    <button onclick="vibrate()">Vibrate</button>
                </div>

            </div>
        </div>

    </div>
</body>

<script>
    if(navigator.vibrate) {
        function vibrate() {
            const time = vibTime.value
            if(time != "")
                navigator.vibrate(time)
        }
    } else {
        error.innerHTML = "Vibrate API not supported in this device."
        error.classList.remove("close")        
    }
</script>

我们有输入和一个按钮。在输入框中输入振动的持续时间,然后点击按钮。设备将在输入的时间内振动

try it

译者注:在安卓手机上测试正常

7. Broadcast Channel API

Broadcast Channel API MDN

Broadcast Channel API 允许相同源下的不同浏览上下文的消息或数据进行通信。浏览上下文可以是窗口、iframe 等。

BroadcastChannel 类用于创建或加入频道。

const politicsChannel = new BroadcastChannel("politics")

politics 将是频道的名称。任何通过 politics 来初始化 BroadcastChannel 构造函数的上下文都将加入频道,它将接收在该频道上发送的任何消息,并且可以将消息发送到该频道。

如果是第一个使用 BroadcastChannel 的构造函数,politics 则会创建该频道。

要发布到频道,请使用 BroadcastChannel.postMessageAPI

要订阅频道(收听消息),请使用该 BroadcastChannel.onmessage 事件。

为了演示广播频道的用法,我构建了一个简单的聊天应用程序:

<body>
    <header>
        <h2>Web APIs<h2>
    </header>
    <div class="web-api-cnt">
        <div class="web-api-card">
            <div class="web-api-card-head"> Demo - BroadcastChannel </div>
            <div class="web-api-card-body">
                <div class="page-info">Open this page in another <i>tab</i>, <i>window</i> or <i>iframe</i> to chat with
                    them.</div>
                <div id="error" class="close"></div>
                <div id="displayMsg" style="font-size:19px;text-align:left;">
                </div>
                <div class="chatArea">
                    <input id="input" type="text" placeholder="Type your message" />
                    <button onclick="sendMsg()">Send Msg to Channel</button>
                </div>
            </div>
        </div>
    </div>
</body>
<script>
    const l = console.log;
    try {
        var politicsChannel = new BroadcastChannel("politics")
        politicsChannel.onmessage = onMessage
        var userId = Date.now()
    } catch (e) {
        error.innerHTML = "BroadcastChannel API not supported in this device."
        error.classList.remove("close")
    }

    input.addEventListener("keydown", (e) => {
        if (e.keyCode === 13 && e.target.value.trim().length > 0) {
            sendMsg()
        }
    })

    function onMessage(e) {
        const { msg, id } = e.data
        const newHTML = "<div class='chat-msg'><span><i>" + id + "</i>: " + msg + "</span></div>"
        displayMsg.innerHTML = displayMsg.innerHTML + newHTML
        displayMsg.scrollTop = displayMsg.scrollHeight
    }

    function sendMsg() {
        politicsChannel.postMessage({ msg: input.value, id: userId })

        const newHTML = "<div class='chat-msg'><span><i>Me</i>: " + input.value + "</span></div>"
        displayMsg.innerHTML = displayMsg.innerHTML + newHTML

        input.value = ""

        displayMsg.scrollTop = displayMsg.scrollHeight
    }  
</script>

初始化了 politicsChannel ,并在 politicsChannel 上设置了一个 onmessage 事件监听器,以便它可以接收和显示消息。

点击按钮时,会调用 sendMsg 函数。它通过 BroadcastChannel#postMessageAPI 将消息发送到 politicsChannel。初始化相同脚本的 tab 页,iframeworker 都将接收从此处发送的消息,因此该页面可以接收其他上下文发送的消息。

Try it

8. Payment Request API

Payment Request API MDN

Payment Request API 提供了为商品和服务选择支付途径的方法。

API 提供了一种一致的方式来向不同的商家提供付款细节,而无需用户再次输入细节。

它向商家提供帐单地址,送货地址,卡详细信息等信息。

注意:API 提供了用户付款明细,但并不会带来新的付款方式。

让我们看一个演示如何使用付款请求 API 接受信用卡付款的演示:
<body>
    <header>
        <h2>Web APIs<h2>
    </header>
    <div class="web-api-cnt">
        <div class="web-api-card">
            <div class="web-api-card-head"> Demo - Credit Card Payment </div>
            <div class="web-api-card-body">
                <div id="error" class="close"></div>
                <div>
                    <button onclick="buy()">Buy</button>
                </div>
            </div>
        </div>
    </div>
</body>
<script>
    const networks = ["visa", "amex"]
    const types = ["debit", "credit"]

    const supportedInstruments = [
        {
            supportedMethods: "basic-card",
            data: {
                supportedNetworks: networks,
                supportedTypes: types
            }
        }
    ]

    const details = {
        total: {
            label: "Total",
            amount: {
                currency: "USD",
                value: "100"
            }
        },
        displayItems: [
            {
                label: "Item 1",
                amount: {
                    currency: "USD",
                    value: "50"
                }
            },
            {
                label: "Item 2",
                amount: {
                    currency: "USD",
                    value: "50"
                }
            },
        ]
    }

    try {
        var paymentRequest = new PaymentRequest(supportedInstruments, details)
    } catch (e) {
        error.innerHTML = "PaymentRequest API not supported in this device."
        error.classList.remove("close")
    }

    function buy() {
        paymentRequest.show().then(response => {
            console.log(response)
        })
    }
</script>

networkstypessupportedTypes 都是描述付款方式。details 列出了我们的购买商品和总费用。

构建 PaymentRequest 实例,paymentRequest.show() 将在浏览器中显示付款界面。并在 Promise 成功的回调中处理用户的数据。

它们是使用 Payment API 进行付款的许多配置,至少通过上面的示例,我们已经了解了 Payment Request API 的使用方式和工作方式。

try it

译者注:测试了下,但是没有走完全流程,毕竟我坚决不付款的~

9. Resize Observer API

Resize Observer API MDN

Resize Observer API 提供了一种方式,以任何方式调整了注册观察者的元素的大小,都通知观察者。

ResizeObserver 类提供了一个观察器,该观察器将在每个 resize 事件上调用。

const resizeObserver = new ResizeObserver(entries => {
    for(const entry of entries) {
        if(entry.contentBoxSize)
            consoleo.log("element re-sized")
    }
})
resizeObserver.observe(document.querySelector("div"))

每当调整 div 大小时,控制台上都会打印 "element re-sized"

让我们看一下如何使用 Resize Observer API 的示例:
<body>
    <header>
        <h2>Web APIs<h2>
    </header>
    <div class="web-api-cnt">

        <div class="web-api-card">
            <div class="web-api-card-head">
                Demo - ResizeObserver
            </div>
            <div class="web-api-card-body">
                <div id="error" class="close"></div>
                <div id="stat"></div>

                <div id="resizeBoxCnt">
                    <div id="resizeBox"></div>
                </div>

                <div>
                    <span>Resize Width:<input onchange="resizeWidth(this.value)" type="range" min="0" max="100" value="0" /></span>
                </div>

                <div>
                    <span>Resize Height:<input onchange="resizeHeight(this.value)" type="range" min="0" max="100" value="0" /></span>
                </div>

            </div>
        </div>

    </div>
</body>

<script>
    try {
        var resizeObserver = new ResizeObserver(entries => {
            for(const entry of entries) {
                    stat.innerHTML = "Box re-sized. Height:" + entry.target.style.height + " - Width:" + entry.target.style.width
            }
        })
        resizeObserver.observe(resizeBox)
    } catch(e) {
        error.innerHTML = "ResizeObserver API not supported in this device."
        error.classList.remove("close")        
    }

    function resizeWidth(e) {
        resizeBox.style.width = `${e}px`
    }

    function resizeHeight(e) {
        resizeBox.style.height = `${e}px`
    }
</script>

我们在这里有范围滑块。如果我们滑动它们,它们将改变 idv#resizeBox 的宽高。我们在div#resizeBox 上注册了 ResizeObserver 观察器,指示该消息指示框已被调整大小以及其高度和宽度的当前值。

尝试滑动范围滑块,你将看到 div#resizeBox 宽高的变化,此外,我们还将看到 div#stat 框中显示的信息。

try it

10. Pointer Lock API

Pointer Lock API MDN

Pointer Lock API 对于需要大量的鼠标输入来控制运动,旋转物体,以及更改项目的应用程序来说非常有用。对高度视觉化的应用程序尤其重要,例如那些使用第一人称视角的应用程序,以及 3D 视图和建模。

方法:
  • requestPointerLock:此方法将从浏览器中删除鼠标并发送鼠标状态事件。这将持续到调用 document.exitPointerLock 为止。
  • document.exitPointerLock:此 API 释放鼠标指针锁定并恢复鼠标光标。
让我们来看一个例子:
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
    <style>
        #box {
            background-color: green;
            width: 100%;
            height: 400px;
            position: relative;
        }

        #ball {
            border-radius: 50%;
            background-color: red;
            width: 50px;
            height: 50px;
            position: absolute;
        }
    </style>
</head>

<body>
    <header>
        <h2>Web APIs<h2>
    </header>
    <div class="web-api-cnt">
        <div class="web-api-card">
            <div class="web-api-card-head"> Demo - PointerLock </div>
            <div class="web-api-card-body">
                <div id="error" class="close"></div>
                <div id="box">
                    <div id="ball"></div>
                </div>
            </div>
        </div>
    </div>
</body>
<script>
    const l = console.log
    box.addEventListener("click", () => {
        if (box.requestPointerLock)
            box.requestPointerLock()
        else {
            error.innerHTML = "PointerLock API not supported in this device."
            error.classList.remove("close")
        }
    })

    document.addEventListener("pointerlockchange", (e) => {
        document.addEventListener("mousemove", (e) => {
            const { movementX, movementY } = e
            ball.style.top = movementX + "px"
            ball.style.left = movementY + "px"
        })
    })
</script>

</html>

div#box 中我们有一个 div#boxdiv#ball

我们在 div#box 上设置了一个 click 事件,当单击它时会调用 requestPointerLock(),这会使光标消失。

PointerLock 有一个 pointerlockchange 事件监听器。当指针锁定状态更改时,将触发此事件。在其回调中,我们将其添加到 mousemove 事件。在当前浏览器选项卡上移动鼠标时,将触发其回调。在此回调中,因此我们使用它来获取鼠标的当前X和Y位置。使用此信息,我们可以设置 div#balltopleft 样式属性,因此,当鼠标移动时,我们会看到一个跳舞的球(译者注:原文的demo中没有设置div#ball的定位,因此修改 topleft 值时,小球位置没有变化)。

鼠标事件的两个新参数 —— movementXmovementY 提供了鼠标位置的变化情况。当指针锁定被启动之后,正常的 MouseEvent 属性 clientX, clientY, screenX, 和 screenY ,保持不变,就像鼠标没有在移动一样。

try it

译者注:这个demo有点问题,因此try不起来,大家可以拷贝我上面的代码在本地 try.

总结

Web日趋复杂。越来越多的原生功能正在使用中,这是因为Web用户的数量远远大于原生APP用户。用户在原生应用上的体验被带到Web上,这样他们无需去使用原生应用。

好嘛,如果看到这里,说明是真爱了。要不要给我的 Github 增加一个 star。

关注公众号,加入技术交流群

WechatIMG167.jpeg

查看原文

赞 23 收藏 19 评论 0

刘小夕 发布了文章 · 2020-06-03

这些热门 GitHub 库,值得每一个前端开发者收藏

经常会有小伙伴让推荐好的代码库,最近看到了这篇文章,都是非常棒的代码库,供得大家学习参考。

WechatIMG1543.jpeg

手头有大量的资源?总是一件很棒的事。

作为一个码农,我们需要专注于功能和最佳实践,而不是一遍遍地编写样板代码。消除无用功,投入时间学习使用正确的工具或者懂得使用有用的资源可以极大的帮助我们节省时间。

通过本篇文章,我们将会了解一些能够极大的帮助我们提升 WEB 开发技能的 GitHub 库,这些库也会帮助你编写更好的代码。

Node.js Best Practices

这个库是一个很棒的地方,可以让你随时了解 Node 世界,并在使用时了解最佳实践。拥有 40k Star 和 133 位贡献者,这个库几乎每天都有更新。

该库对排名较高 Node.js 的最佳实践进行了总结和整理,包括 Node.js + Docker 最佳实践。目前拥有超过80多种最佳实践,风格指南和结构建议等。

一些常见的最佳做法包括:
  • 更好地组织项目
  • 错误处理实践
  • 代码风格实践
  • 测试和整体质量实践
  • 进行生产实践等等

点击跳转到仓库

HTML5 Boilerplate

HTML5 Boilerplate 是一个专业的前端模板,用于构建快速、健壮和适应性强的 Web 应用程序或者网站。

该项目是多年迭代开发和社区知识的产物。它没有强加特定的开发理念或者框架,所以你可按照自己的方式自由地构建代码。

包括像如下的预定义功能:
  • Normalize.css
  • jQuery with CDN
  • Apache Server Configs
  • 有用的 CSS 助手类
  • 默认样式,性能优化等

根据你想要用的内容以及使用方法,将所需文件复制粘贴到你的工程目录里即可。这样就为你提供了一个配置好的模板,从而加快了开发速度。

点击跳转到仓库

RealWorld

掌握一个新框架的核心概念和意识形态并不是一件令人沮丧的事情。

如果你没有正确理解这个概念的话,你需要阅读文档,运行示例代码,拆解示例应用程序并将其重新组合在一起,在本地安装 CLI 等等,它需要花费太多的经历,令人沮丧。

RealWorld 允许你选择任何前端(React,Angular2 等)和任何后端(Node , Django 等),并将它们集成在一起以查看应用程序的真实示例。

由于这些实现和技术栈相关,它们显然不能混用,但它们仍然遵循相同的功能和UX规范。

一些常用集成实例:
  • Angular + ngrx + nx
  • ClojureScript + re-frame
  • React / MobX
  • Go + Gin
  • NestJS + TypeORM/Prisma

你可以在仓库中找到无穷无尽的示例。尽情去寻找吧!

点击跳转到仓库

You Don't Know JS Yet

这是一系列深入研究 JavaScript 语言核心机制的书籍。

所有的书籍均为免费,你可以随时在线阅读。

作者建议阅读的顺序为:
  • Get Started
  • Scope & Closures
  • Objects & Classes (还未开始)
  • Types & Grammar (还未开始)
  • Sync & Async (还未开始)
  • ES.Next & Beyond (还未开始)

点击跳转到仓库

Airbnb JavaScript Guide

这是 Airbnb 提供的非常精确和专业的风格指南。

本指南将通过深入基础知识和代码片段来帮助你由深入浅地理解 JavaScript

本指南涉及的一些热门内容如下:
  • 箭头函数
  • 变量提升
  • 解构
  • 注释
  • 性能
  • 测试

点击跳转到仓库

Storybook

StorybookUI 组件的开发环境,它允许你浏览组件库,查看每个组件的不同状态,以及交互地开发和测试组件。

Storybookapp 之外运行,这允许你独立开发 UI 组件,提高组件的重用性、测试性和开发速度。你可以快速构建,而不必担心应用程序特定的依赖关系。

它附带了一个 CLI 和一些代码示例,供你熟悉 Storybook.

点击跳转到仓库

Front-End-Checklist

Front-End-Checklist 是一个详尽的列表,列出了在网站或者 HTML 页面投入使用前所需要具备或者测试的所有元素。

它是一个基于前端 Web 开发的仓库,更关注性能、安全性和 SEO 等。

Front-End-Checklist 中的所有项目对于大多数工程都是必须的,但其中有些元素可以省略。

包括:
  • ?文档或者文章
  • ?在线工具/测试工具
  • ?媒体或者视频内容

点击跳转到仓库

灵感和资源

这篇文章的令该来源于 Tech Sapien 和他所有令人惊叹的工作。库中的图片? 也是取自同一处。

总结

上面提到的所有的库并不是唯一可用的资源,除此之外我敢肯定还有大量的令人惊叹的项目。这些是其中一些我觉得很有用的资源,同时我自己也经常沉浸其中。

不要忘了给这些库点 Star?。以感谢所有出色的贡献者,感谢他们为我们创造了如此有用的资源✌?

原文地址: https://dev.to/sayanide/best-...

最后:

如果有翻译的不对的地方,请多多指正,希望有所帮助。

查看原文

赞 20 收藏 13 评论 0

刘小夕 发布了文章 · 2020-05-19

React中组件逻辑复用的那些事儿

WechatIMG1446.jpeg

基本每个开发者都需要考虑逻辑复用的问题,否则你的项目中将充斥着大量的重复代码。那么 React 是怎么复用组件逻辑的呢?本文将一一介绍 React 复用组件逻辑的几种方法,希望你读完之后能够有所收获。如果你对这些内容已经非常清楚,那么略过本文即可。

我已尽量对文中的代码和内容进行了校验,但是因为自身知识水平限制,难免有错误,欢迎在评论区指正。

1. Mixins

Mixins 事实上是 React.createClass 的产物了。当然,如果你曾经在低版本的 react 中使用过 Mixins,例如 react-timer-mixinreact-addons-pure-render-mixin,那么你可能知道,在 React 的新版本中我们其实还是可以使用 mixin,虽然 React.createClass 已经被移除了,但是仍然可以使用第三方库 create-react-class,来继续使用 mixin。甚至,ES6 写法的组件,也同样有方式去使用 mixin。当然啦,这不是本文讨论的重点,就不多做介绍了,如果你维护的老项目在升级的过程中遇到这类问题,可以与我探讨。

新的项目中基本不会出现 Mixins,但是如果你们公司还有一些老项目要维护,其中可能就应用了 Mixins,因此稍微花点时间,了解下 Mixins 的使用方法和原理,还是有必要的。倘若你完全没有这方面的需求,那么跳过本节亦是可以的。

Mixins 的使用

React 15.3.0 版本中增加了 PureComponent。而在此之前,或者如果你使用的是 React.createClass 的方式创建组件,那么想要同样的功能,就是使用 react-addons-pure-render-mixin,例如:

//下面代码在新版React中可正常运行,因为现在已经无法使用 `React.createClass`,我就不使用 `React.createClass` 来写了。

const createReactClass = require('create-react-class');
const PureRenderMixin = require('react-addons-pure-render-mixin');

const MyDialog = createReactClass({
    displayName: 'MyDialog',
    mixins: [PureRenderMixin],
    //other code
    render() {
        return (
            <div>
                {/* other code */}
            </div>
        )
    }
});

首先,需要注意,mixins 的值是一个数组,如果有多个 Mixins,那么只需要依次放在数组中即可,例如: mixins: [PureRenderMixin, TimerMixin]

Mixins 的原理

Mixins 的原理可以简单理解为将一个 mixin 对象上的方法增加到组件上。类似于 $.extend 方法,不过 React 还进行了一些其它的处理,例如:除了生命周期函数外,不同的 mixins 中是不允许有相同的属性的,并且也不能和组件中的属性和方法同名,否则会抛出异常。另外即使是生命周期函数,constructorrendershouldComponentUpdate 也是不允许重复的。

而如 compoentDidMount 的生命周期,会依次调用 Mixins,然后再调用组件中定义的 compoentDidMount

例如,上面的 PureRenderMixin 提供的对象中,有一个 shouldComponentUpdate 方法,即是将这个方法增加到了 MyDialog 上,此时 MyDialog 中不能再定义 shouldComponentUpdate,否则会抛出异常。

//react-addons-pure-render-mixin 源码
var shallowEqual = require('fbjs/lib/shallowEqual');

module.exports = {
  shouldComponentUpdate: function(nextProps, nextState) {
    return (
      !shallowEqual(this.props, nextProps) ||
      !shallowEqual(this.state, nextState)
    );
  },
};

Mixins 的缺点

  1. Mixins 引入了隐式的依赖关系。

    例如,每个 mixin 依赖于其他的 mixin,那么修改其中一个就可能破坏另一个。

  2. Mixins 会导致名称冲突

    如果两个 mixin 中存在同名方法,就会抛出异常。另外,假设你引入了一个第三方的 mixin,该 mixin 上的方法和你组件的方法名发生冲突,你就不得不对方法进行重命名。

  3. Mixins 会导致越来越复杂

    mixin 开始的时候是简单的,但是随着时间的推移,容易变得越来越复杂。例如,一个组件需要一些状态来跟踪鼠标悬停,为了保持逻辑的可重用性,将 handleMouseEnter()handleMouseLeave()isHovering() 提取到 HoverMixin() 中。

    然后其他人可能需要实现一个提示框,他们不想复制 HoverMixin() 的逻辑,于是他们创建了一个使用 HoverMixinTooltipMixinTooltipMixin 在它的 componentDidUpdate 中读取 HoverMixin() 提供的 isHovering() 来决定显示或隐藏提示框。

    几个月之后,有人想将提示框的方向设置为可配置的。为了避免代码重复,他们将 getTooltipOptions() 方法增加到了 TooltipMixin 中。结果过了段时间,你需要再同一个组件中显示多个提示框,提示框不再是悬停时显示了,或者一些其他的功能,你需要解耦 HoverMixin()TooltipMixin 。另外,如果很多组件使用了某个 mixinmixin 中新增的功能都会被添加到所有组件中,事实上很多组件完全不需要这些新功能。

    渐渐地,封装的边界被侵蚀了,由于很难更改或移除现有的mixin,它们变得越来越抽象,直到没有人理解它们是如何工作的。

React 官方认为在 React 代码库中,Mixin 是不必要的,也是有问题的。推荐开发者使用高阶组件来进行组件逻辑的复用。

2. HOC

React 官方文档对 HOC 进行了如下的定义:高阶组件(HOC)是 React 中用于复用组件逻辑的一种高级技巧。HOC 自身不是 React API 的一部分,它是一种基于 React 的组合特性而形成的设计模式。

简而言之,高阶组件就是一个函数,它接受一个组件为参数,返回一个新组件。

高阶组件的定义形如下面这样:

//接受一个组件 WrappedComponent 作为参数,返回一个新组件 Proxy
function withXXX(WrappedComponent) {
    return class Proxy extends React.Component {
        render() {
            return <WrappedComponent {...this.props}>
        }
    }
}

开发项目时,当你发现不同的组件有相似的逻辑,或者发现自己在写重复代码的时候,这时候就需要考虑组件复用的问题了。

这里我以一个实际开发的例子来说明,近期各大APP都在适配暗黑模式,而暗黑模式下的背景色、字体颜色等等和正常模式肯定是不一样的。那么就需要监听暗黑模式开启关闭事件,每个UI组件都需要根据当前的模式来设置样式。

每个组件都去监听事件变化来 setState 肯定是不可能的,因为会造成多次渲染。

这里我们需要借助 context API 来做,我以新的 Context API 为例。如果使用老的 context API 实现该功能,需要使用发布订阅模式来做,最后利用 react-native / react-dom 提供的 unstable_batchedUpdates 来统一更新,避免多次渲染的问题(老的 context API 在值发生变化时,如果组件中 shouldComponentUpdate 返回了 false,那么它的子孙组件就不会重新渲染了)。

顺便多说一句,很多新的API出来的时候,不要急着在项目中使用,比如新的 Context API,如果你的 react 版本是 16.3.1, react-dom 版本是16.3.3,你会发现,当你的子组件是函数组件时,即是用 Context.Consumer 的形式时,你是能获取到 context 上的值,而你的组件是个类组件时,你根本拿不到 context 上的值。

同样的 React.forwardRef 在该版本食用时,某种情况下也有多次渲染的bug。都是血和泪的教训,不多说了,继续暗黑模式这个需求。

我的想法是将当前的模式(假设值为 light / dark)挂载到 context 上。其它组件直接从 context 上获取即可。不过我们知道的是,新版的 ContextAPI 函数组件和类组件,获取 context 的方法是不一致的。而且一个项目中有非常多的组件,每个组件都进行一次这样的操作,也是重复的工作量。于是,高阶组件就派上用场啦(PS:React16.8 版本中提供了 useContextHook,用起来很方便)

当然,这里我使用高阶组件还有一个原因,就是我们的项目中还包含老的 context API (不要问我为什么不直接重构下,牵扯的人员太多了,没法随便改),新老 context API 在一个项目中是可以共存的,不过我们不能在同一个组件中同时使用。所以如果一个组件中已经使用的旧的 context API,要想从新的 context API 上获取值,也需要使用高阶组件来处理它。

于是,我编写了一个 withColorTheme 的高阶组件的雏形(这里也可以认为 withColorTheme 是一个返回高阶组件的高阶函数):

import ThemeContext from './context';
function withColorTheme(options={}) {
    return function(WrappedComponent) {
        return class ProxyComponent extends React.Component {
            static contextType = ThemeContext;
            render() {
                return (<WrappedComponent {...this.props} colortheme={this.context}/>)
            }
        }
    }
}

包装显示名称

上面这个雏形存在几个问题,首先,我们没有为 ProxyComponent 包装显示名称,因此,为其加上:

import ThemeContext from './context';

function withColorTheme(options={}) {
    return function(WrappedComponent) {
        class ProxyComponent extends React.Component {
            static contextType = ThemeContext;
            render() {
                return (<WrappedComponent {...this.props} colortheme={this.context}/>)
            }
        }
    }
    function getDisplayName(WrappedComponent) {
        return WrappedComponent.displayName || WrappedComponent.name || 'Component';
    }
    const displayName = `WithColorTheme(${getDisplayName(WrappedComponent)})`;
    ProxyComponent.displayName = displayName;
    return ProxyComponent;
}

我们来看一下,不包装显示名称和包装显示名称的区别:

React Developer Tools 中调试

ReactNative的红屏报错

复制静态方法

众所周知,使用 HOC 包装组件,需要复制静态方法,如果你的 HOC 仅仅是某几个组件使用,没有静态方法需要拷贝,或者需要拷贝的静态方法是确定的,那么你手动处理一下也可以。

因为 withColorTheme 这个高阶组件,最终是要提供给很多业务使用的,无法限制别人的组件写法,因此这里我们必须将其写得通用一些。

hoist-non-react-statics 这个依赖可以帮助我们自动拷贝非 React 的静态方法,这里有一点需要注意,它只会帮助你拷贝非 React 的静态方法,而非被包装组件的所有静态方法。我第一次使用这个依赖的时候,没有仔细看,以为是将 WrappedComponent 上所有的静态方法都拷贝到 ProxyComponent。然后就遇到了 XXX.propsTypes.style undefined is not an object 的红屏报错(ReactNative调试)。因为我没有手动拷贝 propTypes,错误的以为 hoist-non-react-statics 会帮我处理了。

hoist-non-react-statics 的源码非常短,有兴趣的话,可以看一下,我当前使用的 3.3.2 版本。

WechatIMG1440.png

因此,诸如 childContextTypescontextTypecontextTypesdefaultPropsdisplayNamegetDefaultPropsgetDerivedStateFromErrorgetDerivedStateFromProps
mixinspropTypestype 等不会被拷贝,其实也比较容易理解,因为 ProxyComponent 中可能也需要设置这些,不能简单去覆盖。

import ThemeContext from './context';
import hoistNonReactStatics from 'hoist-non-react-statics';
function withColorTheme(options={}) {
    return function(WrappedComponent) {
        class ProxyComponent extends React.Component {
            static contextType = ThemeContext;
            render() {
                return (<WrappedComponent {...this.props} colortheme={this.context}/>)
            }
        }
    }
    function getDisplayName(WrappedComponent) {
        return WrappedComponent.displayName || WrappedComponent.name || 'Component';
    }
   const displayName = `WithColorTheme(${getDisplayName(WrappedComponent)})`;
    ProxyComponent.displayName = displayName;
    ProxyComponent.WrappedComponent = WrappedComponent;
    ProxyComponent.propTypes = WrappedComponent.propTypes;
    //contextType contextTypes 和 childContextTypes 因为我这里不需要,就不拷贝了
    return ProxyComponent;
}

现在似乎差不多了,不过呢,HOC 还有一个问题,就是 ref 传递的问题。如果不经过任何处理,我们通过 ref 拿到的是 ProxyComponent 的实例,而不是原本想要获取的 WrappedComponent 的实例。

ref 传递

虽然我们已经用无关的 props 进行了透传,但是 keyref 不是普通的 propReact 会对它进行特别处理。

所以这里我们需要对 ref 特别处理一下。如果你的 reac-dom16.4.2 或者你的 react-native 版本是 0.59.9 以上,那么可以放心的使用 React.forwardRef 进行 ref 转发,这样使用起来也是最方便的。

使用 React.forwardRef 转发
import ThemeContext from './context';
import hoistNonReactStatics from 'hoist-non-react-statics';
function withColorTheme(options={}) {
    return function(WrappedComponent) {
        class ProxyComponent extends React.Component {
            static contextType = ThemeContext;
            render() {
                const { forwardRef, ...wrapperProps } = this.props;
                return <WrappedComponent {...wrapperProps} ref={forwardRef} colorTheme={ this.context } />
            }
        }
    }
    function getDisplayName(WrappedComponent) {
        return WrappedComponent.displayName || WrappedComponent.name || 'Component';
    }
    const displayName = `WithColorTheme(${getDisplayName(WrappedComponent)})`;
    ProxyComponent.displayName = displayName;
    ProxyComponent.WrappedComponent = WrappedComponent;
    ProxyComponent.propTypes = WrappedComponent.propTypes;
    //contextType contextTypes 和 childContextTypes 因为我这里不需要,就不拷贝了
    if (options.forwardRef) {
        let forwarded = React.forwardRef((props, ref) => (
            <ProxyComponent {...props} forwardRef={ref} />
        ));
        forwarded.displayName = displayName;
        forwarded.WrappedComponent = WrappedComponent;
        forwarded.propTypes = WrappedComponent.propTypes;
        return hoistNonReactStatics(forwarded, WrappedComponent);
    } else {
        return hoistNonReactStatics(ProxyComponent, WrappedComponent);
    }
}

假设,我们对 TextInput 进行了装饰,如 export default withColorTheme({forwardRef: true})(TextInput)

使用: <TextInput ref={v => this.textInput = v}>

如果要获取 WrappedComponent 的实例,直接通过 this.textInput 即可,和未使用 withColorTheme 装饰前一样获取。

通过方法调用 getWrappedInstance
import ThemeContext from './context';
import hoistNonReactStatics from 'hoist-non-react-statics';
function withColorTheme(options={}) {
    return function(WrappedComponent) {
        class ProxyComponent extends React.Component {
            static contextType = ThemeContext;

            getWrappedInstance = () => {
                if (options.forwardRef) {
                    return this.wrappedInstance;
                }
            }

            setWrappedInstance = (ref) => {
                this.wrappedInstance = ref;
            }

            render() {
                const { forwardRef, ...wrapperProps } = this.props;
                let props = {
                    ...this.props
                };

                if (options.forwardRef) {
                    props.ref = this.setWrappedInstance;
                }
                return <WrappedComponent {...props} colorTheme={ this.context } />
            }
        }
    }
    
    function getDisplayName(WrappedComponent) {
        return WrappedComponent.displayName || WrappedComponent.name || 'Component';
    }

    const displayName = `WithColorTheme(${getDisplayName(WrappedComponent)})`;
    ProxyComponent.displayName = displayName;
    ProxyComponent.WrappedComponent = WrappedComponent;
    ProxyComponent.propTypes = WrappedComponent.propTypes;
    //contextType contextTypes 和 childContextTypes 因为我这里不需要,就不拷贝了
    if (options.forwardRef) {
        let forwarded = React.forwardRef((props, ref) => (
            <ProxyComponent {...props} forwardRef={ref} />
        ));
        forwarded.displayName = displayName;
        forwarded.WrappedComponent = WrappedComponent;
        forwarded.propTypes = WrappedComponent.propTypes;
        return hoistNonReactStatics(forwarded, WrappedComponent);
    } else {
        return hoistNonReactStatics(ProxyComponent, WrappedComponent);
    }
}

同样的,我们对 TextInput 进行了装饰,如 export default withColorTheme({forwardRef: true})(TextInput)

使用: <TextInput ref={v => this.textInput = v}>

如果要获取 WrappedComponent 的实例,那么需要通过 this.textInput.getWrappedInstance() 获取被包装组件 TextInput 的实例。

最大化可组合

我先说一下,为什么我将它设计为下面这样:

function withColorTheme(options={}) {
    function(WrappedComponent) {

    }
}

而不是像这样:

function withColorTheme(WrappedComponent, options={}) {
}

主要是使用装饰器语法比较方便,而且很多业务中也使用了 react-redux

@connect(mapStateToProps, mapDispatchToProps)
@withColorTheme()
export default class TextInput extends Component {
    render() {}
}

这样设计,可以不破坏原本的代码结构。否则的话,原本使用装饰器语法的业务改起来就有点麻烦。

回归到最大化可组合,看看官方文档怎么说:

connect(react-redux 提供) 函数返回的单参数 HOC 具有签名 Component => Component。输出类型与输入类型相同的函数很容易组合在一起。

// ... 你可以编写组合工具函数
// compose(f, g, h) 等同于 (...args) => f(g(h(...args)))
const enhance = compose(
  // 这些都是单参数的 HOC
  withRouter,
  connect(commentSelector)
)
const EnhancedComponent = enhance(WrappedComponent)

compose 的源码可以看下 redux 的实现,代码很短。

再复杂化一下就是:

withRouter(connect(commentSelector)(withColorTheme(options)(WrappedComponent)));

我们的 enhance 可以编写为:

const enhance = compose(
  withRouter,
  connect(commentSelector),
  withColorTheme(options)
)
const EnhancedComponent = enhance(WrappedComponent)

如果我们是写成 XXX(WrappedComponent, options) 的形式的话,那么上面的代码将变成:

const EnhancedComponent = withRouter(connect(withColorTheme(WrappedComponent, options), commentSelector))

试想一下,如果还有更多的 HOC 要使用,这个代码会变成什么样子?

HOC的约定和注意事项

约定
  • 将不相关的 props 传递给被包裹的组件(HOC应透传与自身无关的 props)
  • 最大化可组合性
  • 包装显示名称以便轻松调试
注意事项
  • 不要在 render 方法中使用 HOC

Reactdiff 算法(称为协调)使用组件标识来确定它是应该更新现有子树还是将其丢弃并挂载新子树。 如果从 render 返回的组件与前一个渲染中的组件相同(===),则 React 通过将子树与新子树进行区分来递归更新子树。 如果它们不相等,则完全卸载前一个子树。

这不仅仅是性能问题 —— 重新挂载组件会导致该组件及其所有子组件的状态丢失。

如果在组件之外创建 HOC,这样一来组件只会创建一次。因此,每次 render 时都会是同一个组件。

  • 务必复制静态方法
  • Refs 不会被传递(需要额外处理)

3. 反向继承

React 官方文档上有这样一段描述: HOC 不会修改传入的组件,也不会使用继承来复制其行为。相反,HOC 通过将组件包装在容器组件中来组成新组件。HOC 是纯函数,没有副作用。

因此呢,我觉得反向继承不是 React 推崇的方式,这里我们可以做一下了解,某些场景下也有可能会用到。

反向继承
function withColor(WrappedComponent) {
    class ProxyComponent extends WrappedComponent {
        //注意 ProxyComponent 会覆盖 WrappedComponent 的同名函数,包括 state 和 props
        render() {
            //React.cloneElement(super.render(), { style: { color:'red' }})
            return super.render();
        }
    }
    return ProxyComponent;
}

和上一节不同,反向继承不会增加组件的层级,并且也不会有静态属性拷贝和 refs 丢失的问题。可以利用它来做渲染劫持,不过我目前没有什么必须要使用反向继承的场景。

虽然它没有静态属性和 refs的问题,也不会增加层级,但是它也不是那么好用,会覆盖同名属性和方法这点就让人很无奈。另外虽然可以修改渲染结果,但是不好注入 props

4. render props

首先, render props 是指一种在 React 组件之间使用一个值为函数的 prop 共享代码的简单技术。

具有 render prop 的组件接受一个函数,该函数返回一个 React 元素并调用它而不是实现自己的渲染逻辑。

<Route
    {...rest}
    render={routeProps => (
        <FadeIn>
            <Component {...routeProps} />
        </FadeIn>
    )}
/>

ReactNative 的开发者,其实 render props 的技术使用的很多,例如,FlatList 组件:

import React, {Component} from 'react';
import {
    FlatList,
    View,
    Text,
    TouchableHighlight
} from 'react-native';

class MyList extends Component {
    data = [{ key: 1, title: 'Hello' }, { key: 2, title: 'World' }]
    render() {
        return (
            <FlatList
                style={{marginTop: 60}}
                data={this.data}
                renderItem={({ item, index }) => {
                    return (
                        <TouchableHighlight
                            onPress={() => { alert(item.title) }}
                        >
                            <Text>{item.title}</Text>
                        </TouchableHighlight>
                    )
                }}
                ListHeaderComponent={() => {
                    return (<Text>以下是一个List</Text>)
                }}
                ListFooterComponent={() => {
                   return <Text>没有更多数据</Text>
                }}
            />
        )
    }
}

例如: FlatListrenderItemListHeaderComponent 就是render prop

注意,render prop 是因为模式才被称为 render prop ,你不一定要用名为 renderprop 来使用这种模式。render prop 是一个用于告知组件需要渲染什么内容的函数 prop

其实,我们在封装组件的时候,也经常会应用到这个技术,例如我们封装一个轮播图组件,但是每个页面的样式是不一致的,我们可以提供一个基础样式,但是也要允许自定义,否则就没有通用价值了:

//提供一个 renderPage 的 prop
class Swiper extends React.PureComponent {
    getPages() {
        if(typeof renderPage === 'function') {
            return this.props.renderPage(XX,XXX)
        }
    }
    render() {
        const pages = typeof renderPage === 'function' ? this.props.renderPage(XX,XXX) : XXXX;
        return (
            <View>
                <Animated.View>
                    {pages}
                </Animated.View>
            </View>
        )
    }
}

注意事项

Render PropsReact.PureComponent 一起使用时要小心

如果在 render 方法里创建函数,那么 render props,会抵消使用 React.PureComponent 带来的优势。因为浅比较 props 的时候总会得到 false,并且在这种情况下每一个 render 对于 render prop 将会生成一个新的值。

import React from 'react';
import { View } from 'react-native';
import Swiper from 'XXX';
class MySwiper extends React.Component {
    render() {
        return (
            <Swiper 
                renderPage={(pageDate, pageIndex) => {
                    return (
                        <View></View>
                    )
                }}
            />
        )       
    }
}

这里应该比较好理解,这样写,renderPage 每次都会生成一个新的值,很多 React 性能优化上也会提及到这一点。我们可以将 renderPage 的函数定义为实例方法,如下:

import React from 'react';
import { View } from 'react-native';
import Swiper from 'XXX';
class MySwiper extends React.Component {
    renderPage(pageDate, pageIndex) {
        return (
            <View></View>
        )
    }
    render() {
        return (
            <Swiper 
                renderPage={this.renderPage}
            />
        )       
    }
}

如果你无法静态定义 prop,则 <Swiper> 应该扩展 React.Component,因为也没有浅比较的必要了,就不要浪费时间去比较了。

5. Hooks

HookReact 16.8 的新增特性,它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。HOCrender props 虽然都可以

React 已经内置了一些 Hooks,如: useStateuseEffectuseContextuseReduceruseCallbackuseMemouseRefHook,如果你还不清楚这些 Hook,那么可以优先阅读一下官方文档。

我们主要是将如何利用 Hooks 来进行组件逻辑复用。假设,我们有这样一个需求,在开发环境下,每次渲染时,打印出组件的 props

import React, {useEffect} from 'react';

export default function useLogger(componentName,...params) {
    useEffect(() => {
        if(process.env.NODE_ENV === 'development') {
            console.log(componentName, ...params);
        }
    });
}

使用时:

import React, { useState } from 'react';
import useLogger from './useLogger';

export default function Counter(props) {
    let [count, setCount] = useState(0);
    useLogger('Counter', props);
    return (
        <div>
            <button onClick={() => setCount(count + 1)}>+</button>
            <p>{`${props.title}, ${count}`}</p>
        </div>
    )
}

另外,官方文档自定义 Hook 章节也一步一步演示了如何利用 Hook 来进行逻辑复用。我因为版本限制,还没有在项目中应用 Hook ,虽然文档已经看过多次。读到这里,一般都会有一个疑问,那就是 Hook 是否会替代 render propsHOC,关于这一点,官方也给出了答案:

通常,render props 和高阶组件只渲染一个子节点。我们认为让 Hook 来服务这个使用场景更加简单。这两种模式仍有用武之地,例如,FlatList 组件的 renderItem 等属性,或者是 一个可见的容器组件或许会有它自己的 DOM 结构。但在大部分场景下,Hook 足够了,并且能够帮助减少嵌套。

HOC 最最最讨厌的一点就是层级嵌套了,如果项目是基于新版本进行开发,那么需要逻辑复用时,优先考虑 Hook,如果无法实现需求,那么再使用 render propsHOC 来解决。

参考链接

查看原文

赞 13 收藏 6 评论 0

刘小夕 发布了文章 · 2020-05-11

5个技巧助你编写更好的React代码

原文链接:https://levelup.gitconnected....

在本文中,我想分享几个技巧,这些技巧将改善你的React代码。

1. 解构 props

JS 中解构对象(尤其是 props)可以大大减少代码中的重复。看下面的例子:

//Parent Component
import React from 'react';

import CoffeeCard from './CoffeeCard';

const CafeMenu = () => {
    const coffeeList = [
        {
            id: '0',
            name: 'Espresso',
            price: '2.00',
            size: '16'
        },
        {
            id: '1',
            name: 'Cappuccino',
            price: '3.50',
            size: '24'
        },
        {
            id: '2',
            name: 'Caffee Latte',
            price: '2.70',
            size: '12'
        }
    ];

    return coffeeList.map(item => (
        <CoffeeCard key={item.id} coffee={item} />
    ));
};

export default CafeMenu;

CafeMenu 组件用于存储可用饮料的列表,现在我们想要创建另一个可以显示一种饮料的组件。如果不对 props 进行解构,我们的代码将像下面这样:

//Child Component
import React from 'react';

const CoffeeCard = props => {
    return (
        <div>
            <h1>{props.coffee.name}</h1>
            <p>Price: {props.coffee.price}$</p>
            <p>Size: {props.coffee.size} oz</p>
        </div>
    );
};

export default CoffeeCard;

如你所见,它看起来并不好,每次我们需要获取某个属性时,都要重复 props.coffee,幸运的是,我们可以通过解构来简化它。

//Child Component (after destructuring props)
import React from 'react';

const CoffeeCard = props => {
    const { name, price, size } = props.coffee;
    return (
        <div>
            <h1>{name}</h1>
            <p>Price: {price}$</p>
            <p>Size: {size} oz</p>
        </div>
    );
};

export default CoffeeCard;

如果我们想将大量参数传递给子组件,我们还可以直接在构造函数(或函数组件的参数)中解构 props。比如:

//Parent Component
import React from 'react';

import ContactInfo from './ContactInfo';

const UserProfile = () => {
    const name = 'John Locke';
    const email = 'john@locke.com';
    const phone = '01632 960668';

    return <ContactInfo name={name} email={email} phone={phone} />;
};

export default UserProfile;
//Child Component
import React from 'react';

const ContactInfo = ({ name, email, phone }) => {
    return (
        <div>
            <h1>{name}</h1>
            <p> E-mail: {email}</p>
            <p> Phone: {phone}</p>
        </div>
    );
};

export default ContactInfo;

2. 保持导入模块的顺序

有时(尤其是在“容器组件”中),我们需要使用许多不同的模块,并且组件导入看上去有些混乱,如:

import { Auth } from 'aws-amplify';
import React from 'react';
import SidebarNavigation from './components/SidebarNavigation';
import { EuiPage, EuiPageBody } from '@elastic/eui';
import { keyCodes } from '@elastic/eui/lib/services';
import './index.css'
import HeaderNavigation from './components/HeaderNavigation';
import Routes from './Routes';

关于导入模块的理想顺序有很多不同的观点。我建议多参考,然后找到适合你自己的那种。

至于我自己,我通常按类型对导入进行分组,并按字母顺序对它们进行排序(这是可选操作)。我也倾向于保持以下顺序:

  1. 标准模块
  2. 第三方模块
  3. 自己代码导入(组件)
  4. 特定于模块的导入(例如CSS,PNG等)
  5. 仅用于测试的代码

快速重构一下,我们的模块导入看上去舒服多了了。

import React from 'react';

import { Auth } from 'aws-amplify';
import { EuiPage, EuiPageBody } from '@elastic/eui';
import { keyCodes } from '@elastic/eui/lib/services';

import HeaderNavigation from './components/HeaderNavigation';
import SidebarNavigation from './components/SidebarNavigation';
import Routes from './Routes';

import './index.css'

3.使用 Fragments

在我们的组件中,我们经常返回多个元素。一个 React 组件不能返回多个子节点,因此我们通常将它们包装在 div 中。有时,这样的解决方案会有问题。比如下面的这个例子中:

我们要创建一个 Table 组件,其中包含一个 Columns 组件。

import React from 'react';

import Columns from './Columns';

const Table = () => {
    return (
        <table>
            <tbody>
                <tr>
                    <Columns />
                </tr>
            </tbody>
        </table>
    );
};

export default Table;

Columns 组件中包含一些 td 元素。由于我们无法返回多个子节点,因此需要将这些元素包装在 div 中。

import React from 'react';

const Columns = () => {
    return (
        <div>
            <td>Hello</td>
            <td>World</td>
        </div>
    );
};

export default Columns;

然后就报错了,因为tr 标签中不能放置 div。我们可以使用 Fragment 标签来解决这个问题,如下所示:

import React, { Fragment } from 'react';

const Columns = () => {
    return (
        <Fragment>
            <td>Hello</td>
            <td>World</td>
        </Fragment>
    );
};

export default Columns;

我们可以将 Fragment 视为不可见的 div。它在子组件将元素包装在标签中,将其带到父组件并消失。
你也可以使用较短的语法,但是它不支持 key 和属性。

import React from 'react';

const Columns = () => {
    return (
        <>
            <td>Hello</td>
            <td>World</td>
        </>
    );
};
export default Columns;

4. 使用展示组件和容器组件

将应用程序的组件分为展示(木偶)组件和容器(智能)组件。如果你不知道这些是什么,可以下面的介绍:

展示组件

  • 主要关注UI,它们负责组件的外观。
  • 数据由 props 提供,木偶组件中不应该调用API,这是智能组件的工作
  • 除了UI的依赖包,它们不需要依赖应用程序
  • 它们可能包括状态,但仅用于操纵UI本身-它们不应存储应用程序数据。

木偶组件有:加载指示器,模态,按钮,输入。

容器组件

  • 它们不关注样式,通常不包含任何样式
  • 它们用于处理数据,可以请求数据,捕获更改和传递应用程序数据
  • 负责管理状态,重新渲染组件等等
  • 可能依赖于应用程序,调用 Redux,生命周期方法,API和库等等。
使用展示组件和容器组件的好处
  • 更好的可读性
  • 更好的可重用性
  • 更容易测试

此外,它还符合“单一责任原则” - 一个组件负责外观,另一个组件负责数据。

示例

让我们看一个简单的例子。这是一个 BookList 组件,该组件可从API获取图书数据并将其显示在列表中。

import React, { useState, useEffect } from 'react';

const BookList = () => {
    const [books, setBooks] = useState([]);
    const [isLoading, setLoading] = useState(false);

    useEffect(() => {
        setLoading(true);
        fetch('api/books')
            .then(res => res.json())
            .then(books => {
                setBooks(books);
                setLoading(false);
            });
    }, []);

    const renderLoading = () => {
        return <p>Loading...</p>;
    };

    const renderBooks = () => {
        return (
            <ul>
                {books.map(book => (
                    <li>{book.name}</li>
                ))}
            </ul>
        );
    };

    return <>{isLoading ? renderLoading() : renderBooks()}</>;
};
export default BookList;

该组件的问题在于,它负责太多事情。它获取并呈现数据。它还与一个特定的接口关联,因此在不复制代码的情况下,不能使用此组件显示特定用户的图书列表。

现在,让我们尝试将此组件分为展示组件和容器组件。

import React from 'react';

const BookList = ({ books, isLoading }) => {
    const renderLoading = () => {
        return <p>Loading...</p>;
    };

    const renderBooks = () => {
        return (
            <ul>
                {books.map(book => (
                    <li key={book.id}>{book.name}</li>
                ))}
            </ul>
        );
    };

    return <>{isLoading ? renderLoading() : renderBooks()}</>;
};
export default BookList;
import React, { useState, useEffect } from 'react';
import BookList from './BookList';

const BookListContainer = () => {
    const [books, setBooks] = useState([]);
    const [isLoading, setLoading] = useState(false);

    useEffect(() => {
        setLoading(true);
        fetch('/api/books')
            .then(res => res.json())
            .then(books => {
                setBooks(books);
                setLoading(false);
            });
    }, []);

    return <BookList books={books} isLoading={isLoading} />;
};

export default BookListContainer;

如你所见,它看起来要好得多。更重要的是,它使我们可以在具有不同数据的许多地方使用 BookList 组件。

5. 使用 styled-components

React 组件进行样式设置一直是个难题。查找拼写错误的类名,维护大型 CSS 文件,处理兼容性问题有时可能很痛苦。

styled-components 是一个常见的 css in js 类库,和所有同类型的类库一样,通过 js 赋能解决了原生 css 所不具备的能力,比如变量、循环、函数等。

要开始使用 styled-components,你需要首先安装依赖:

npm i styled-components

下面是一个示例:

import React from 'react';
import styled from 'styled-components';

const Grid = styled.div`
    display: flex;
`;

const Col = styled.div`
    display: flex;
    flex-direction: column;
`;

const MySCButton = styled.button`
    background: ${props => (props.primary ? props.mainColor : 'white')};
    color: ${props => (props.primary ? 'white' : props.mainColor)};
    display: block;
    font-size: 1em;
    margin: 1em;
    padding: 0.5em 1em;
    border: 2px solid ${props => props.mainColor};
    border-radius: 15px;
`;

function App() {
    return (
        <Grid>
            <Col>
                <MySCButton mainColor='#ee6352' primary>My 1st Button</MySCButton>
                <MySCButton mainColor='#ee6352'>My 2st Button</MySCButton>
                <MySCButton mainColor='#ee6352'>My 3st Button</MySCButton>
            </Col>
            <Col>
                <MySCButton mainColor='#515052' primary>My 4st Button</MySCButton>
                <MySCButton mainColor='#515052'>My 5st Button</MySCButton>
                <MySCButton mainColor='#515052'>My 6st Button</MySCButton>
            </Col>
        </Grid>
    );
}

export default App;

这只是样式化组件如何工作的一个简单示例,但是它们可以做的还远远不止这些。你可以在其官方文档中了解有关样式化组件的更多信息。

谢谢阅读!到此为止。希望你学到了一些新知识。如果你知道React中的其它提示和技巧,请随时在下面的评论部分中分享它们。

WechatIMG167.jpeg

查看原文

赞 4 收藏 2 评论 0

刘小夕 发布了文章 · 2020-04-08

为什么说for...of循环是JS中的一颗宝石

是什么使得一个编程语言的新特性很棒?那就是当这个特性可以结合多个其它特性的时候。

ES2015 版本中引入的 for...of 语句就是这种情况。

for...of 可以迭代数组、类数组以及任何可以迭代的对象(mapssetsDOM集合),并且,for...of 的语句还很短。

在这篇文章中,我将会演示 for...of 的能力。

1. 数组迭代

for...of 最常见的应用是对数组项进行迭代。该循环可以高效得完成迭代,而无需其他变量来保持索引。

例如:

const products = ['oranges', 'apples'];

for (const product of products) {
  console.log(product);
}
// 'oranges'
// 'apples'

for...of 循环遍历 products 的每一项。迭代项被赋值给变量 product.

数组方法 entries() 可以用于访问迭代项的索引。该方法在每次迭代时返回一对 [index,item]

就地解构是 for...of 的另一个重要功能,我们将在下一部分中对其进行详细说明。

1.1 就地解构

首先,我们来看一下 for...of 循环的语法:

for (LeftHandSideExpression of Expression) {
  // statements
}

LeftHandSideExpression 表达式可以替换为任意赋值表达式左侧的内容。

在前面的示例中,LeftHandSideExpression 是一个变量声明 const product ,甚至是一个解构 const [index,product]

因此,for...of 的语法支持实现迭代项的解构。

让我们遍历一个对象数组,提取每个对象的 name 属性:

const persons = [
  { name: 'John Smith' },
  { name: 'Jane Doe' }
];

for (const { name } of persons) {
  console.log(name);
}
// 'John Smith'
// 'Jane Doe'

const { name } of persons 循环迭代 persons 对象数组,并且就地将 person 对象进行了解构。

2. 类数组迭代

for...of 可以用于迭代类数组对象。arguments 是函数体内的特殊变量,包含函数的所有参数,这是一个经典的类数组对象。

让我们写一个求和函数 sum(num1, num2, ..., numN)

function sum() {
  let sum = 0;
  for (const number of arguments) {
    sum += number;
  }
  return sum;
}

sum(1, 2, 3); // => 6

在每次迭代中,for...of 循环遍历类数组 arguments 中的每一个数,并计算总和。

3. 快速了解可迭代

什么是可迭代对象?它是支持可迭代协议的对象。

我们可以通过查看 Symbol.iterator 方法来确定某个数据是否可迭代。例如,下面的例子显示了数组是可迭代的:

const array = [1, 2, 3];
const iterator1 = array[Symbol.iterator]();
iterator1.next(); // => { value: 1, done: false }

如果你想了解更多信息,可以随时阅读我之前的文章

for...of 接受可迭代对象。这很棒,因为现在你可以遍历string、数组、类数组、setmap,同时仍可以享受 for...of 的简洁。

4. 字符串迭代

JavaScript 的基础类型 string 是可迭代的。因此,可以轻松地遍历字符串的字符。

const message = 'hello';

for (const character of message) {
  console.log(character);
}
// 'h'
// 'e'
// 'l'
// 'l'
// 'o'

message 是一个字符串。由于字符串可迭代的,因此 for...of 循环遍历 message

5. Map 和 Set 迭代

Map 是一个特殊的对象,将键与值相关联。键可以是任何基本类型(通常是 string,但可以是 number 等)。

幸运的是,Map 也是可迭代的(在键/值对上进行迭代),并且 for...of 可以轻松地循环迭代所有键/值对。

一起看一下:

const names = new Map();
names.set(1, 'one');
names.set(2, 'two');

for (const [number, name] of names) {
  console.log(number, name);
}
// logs 1, 'one'
// logs 2, 'two'

for (const [number, name] of names) 迭代 names 的键值对。

在每个循环中,迭代器都会返回一个数组 [key,value] ,并使用 const [number,name] 立即对这对数组进行解构。

以相同的方式可以遍历 Set 的项:

const colors = new Set(['white', 'blue', 'red', 'white']);

for (color of colors) {
  console.log(color);
}
// 'white'
// 'blue'
// 'red'

6. 迭代普通的JavaScript对象

尝试遍历普通JS对象的属性/值总是很痛苦。过去,我通常使用 Object.keys() 获取对象的键,然后使用 forEach 来迭代键数组。【译者:这不代表本人观点,我比较喜欢用 for...in 遍历对象】

const person = {
  name: 'John Smith',
  job: 'agent'
};

Object.keys(person).forEach(prop => {
  console.log(prop, person[prop]);
});
// 'name', 'John Smith'
// 'job', 'agent'

新的 Object.entries() 函数与 for...of 组合使用是个不错的选择:

const person = {
  name: 'John Smith',
  job: 'agent'
};

for (const [prop, value] of Object.entries(person)) {
  console.log(prop, value);
}
// 'name', 'John Smith'
// 'job', 'agent'

Object.entries(person) 返回一个键和值的元组数组:[[''name','John Smith'],['job','agent']]。然后,使用 for...of 循环遍历数组,并将每个元组解构为 const [prop,value]

7. 遍历DOM集合

你可能知道 HTMLCollection 令人沮丧。主要是因为 HTMLCollection 是一个类数组的对象(而不是常规数组),所以我们无法使用数组的方法。

例如,每个 DOM 元素的 children 属性都是 HTMLCollection 。好在 for...of 可以在类似数组的对象上进行迭代,因此我们可以轻松地迭代 children

const children = document.body.children;

for (const child of children) {
  console.log(child); // logs each child of <body>
}

此外,for...of 可以迭代 NodeList 集合(可迭代)。例如,函数 document.querySelectorAll(query) 返回一个 NodeList

const allImages = document.querySelectorAll('img');

for (const image of allImages) {
  console.log(image); // log each image in the document
}

如果你想遍历 DOM 中的不同种类的集合,那么 for...of 语句是一个不错的选择。

8. 性能

迭代大型数组时,for...of 的执行速度可能会比经典方法慢:

const a = [/* big array */];
for (let i = 0; i < a.length; i++) {
  console.log(a[i]);
}

在每次迭代中调用迭代器比通过增加索引访问的开销更大。但是,这种细微差别在使用大型数组的应用程序中以及性能至关重要的应用程序中非常重要,不过这种情况很少发生。

9. 总结

为什么说 for...of 是一颗宝石,因为:

  • 它简明扼要
  • 它接受迭代器,包括数组,字符串,SetMap,DOM集合
  • 它接受类数组对象
  • 迭代的项目可以在就地解构。

你首选的迭代数组项的方式是什么?

最后:

原文地址: https://dmitripavlutin.com/ja...

点评:翻译得很烂,还不如机翻。哈哈哈哈。

WechatIMG167.jpeg

查看原文

赞 15 收藏 8 评论 5

刘小夕 发布了文章 · 2020-03-31

带你深度解锁Webpack系列(优化篇)

带你深度解锁Webpack系列(基础篇)带你深度解锁Webpack系列(进阶篇),主要是讲解了 Webpack 的配置,但是随着项目越来越大,构建速度可能会越来越慢,构建出来的js的体积也越来越大,此时就需要对配置进行优化。

文中罗列出了十多种优化方式,大家可以结合自己的项目,选择适当的方式进行优化。这些 Webpack 插件的源码我大多也没有看过,主要是结合 Webpack 官方文档以及项目实践,在验证后输出了本文,如果文中有错误的地方,欢迎在评论区指正。

鉴于前端技术变更迅速,祭出本篇文章基于 Webpack 的版本号:

├── webpack@4.41.5 
└── webpack-cli@3.3.10 

本文对应的项目地址(编写本文时使用)供参考:https://github.com/YvetteLau/...

量化

有时,我们以为的优化是负优化,这时,如果有一个量化的指标可以看出前后对比,那将会是再好不过的一件事。

speed-measure-webpack-plugin 插件可以测量各个插件和loader所花费的时间,使用之后,构建时,会得到类似下面这样的信息:

smp.jpeg
对比前后的信息,来确定优化的效果。

speed-measure-webpack-plugin 的使用很简单,可以直接用其来包裹 Webpack 的配置:

//webpack.config.js
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
const smp = new SpeedMeasurePlugin();

const config = {
    //...webpack配置
}

module.exports = smp.wrap(config);

1.exclude/include

我们可以通过 excludeinclude 配置来确保转译尽可能少的文件。顾名思义,exclude 指定要排除的文件,include 指定要包含的文件。

exclude 的优先级高于 include,在 includeexclude 中使用绝对路径数组,尽量避免 exclude,更倾向于使用 include

//webpack.config.js
const path = require('path');
module.exports = {
    //...
    module: {
        rules: [
            {
                test: /\.js[x]?$/,
                use: ['babel-loader'],
                include: [path.resolve(__dirname, 'src')]
            }
        ]
    },
}

下图是我未配置 include 和配置了 include 的构建结果对比:

include:exclude.jpeg

2. cache-loader

在一些性能开销较大的 loader 之前添加 cache-loader,将结果缓存中磁盘中。默认保存在 node_modueles/.cache/cache-loader 目录下。

首先安装依赖:

npm install cache-loader -D

cache-loader 的配置很简单,放在其他 loader 之前即可。修改Webpack 的配置如下:

module.exports = {
    //...
    
    module: {
        //我的项目中,babel-loader耗时比较长,所以我给它配置了`cache-loader`
        rules: [
            {
                test: /\.jsx?$/,
                use: ['cache-loader','babel-loader']
            }
        ]
    }
}

如果你跟我一样,只打算给 babel-loader 配置 cache 的话,也可以不使用 cache-loader,给 babel-loader 增加选项 cacheDirectory

cache-loader.jpeg

cacheDirectory:默认值为 false。当有设置时,指定的目录将用来缓存 loader 的执行结果。之后的 Webpack 构建,将会尝试读取缓存,来避免在每次执行时,可能产生的、高性能消耗的 Babel 重新编译过程。设置空值或者 true 的话,使用默认缓存目录:node_modules/.cache/babel-loader。开启 babel-loader的缓存和配置 cache-loader,我比对了下,构建时间很接近。

3.happypack

由于有大量文件需要解析和处理,构建是文件读写和计算密集型的操作,特别是当文件数量变多后,Webpack 构建慢的问题会显得严重。文件读写和计算操作是无法避免的,那能不能让 Webpack 同一时刻处理多个任务,发挥多核 CPU 电脑的威力,以提升构建速度呢?

HappyPack 就能让 Webpack 做到这点,它把任务分解给多个子进程去并发的执行,子进程处理完后再把结果发送给主进程。

首先需要安装 happypack:

npm install happypack -D

修改配置文件:

const Happypack = require('happypack');
module.exports = {
    //...
    module: {
        rules: [
            {
                test: /\.js[x]?$/,
                use: 'Happypack/loader?id=js',
                include: [path.resolve(__dirname, 'src')]
            },
            {
                test: /\.css$/,
                use: 'Happypack/loader?id=css',
                include: [
                    path.resolve(__dirname, 'src'),
                    path.resolve(__dirname, 'node_modules', 'bootstrap', 'dist')
                ]
            },
            {
                test: /\.(png|jpg|gif|jpeg|webp|svg|eot|ttf|woff|woff2|.gexf)$/,
                use: 'Happypack/loader?id=file',
                include: [
                    path.resolve(__dirname, 'src'),
                    path.resolve(__dirname, 'public'),
                    path.resolve(__dirname, 'node_modules', 'bootstrap', 'dist')
                ]
            }
        ]
    },
    plugins: [
        new Happypack({
            id: 'js', //和rule中的id=js对应
            //将之前 rule 中的 loader 在此配置
            use: ['babel-loader'] //必须是数组
        }),
        new Happypack({
            id: 'css',//和rule中的id=css对应
            use: ['style-loader', 'css-loader','postcss-loader'],
        }),
        new Happypack({
            id: 'file', //和rule中的id=file对应
            use: [{
                loader: 'url-loader',
                options: {
                    limit: 10240 //10K
                }
            }],
        }),
    ]
}

happypack 默认开启 CPU核数 - 1 个进程,当然,我们也可以传递 threadsHappypack

happypack.jpeg

说明:当 postcss-loader 配置在 Happypack 中,必须要在项目中创建 postcss.config.js

//postcss.config.js
module.exports = {
    plugins: [
        require('autoprefixer')()
    ]
}

否则,会抛出错误: Error: No PostCSS Config found

另外,当你的项目不是很复杂时,不需要配置 happypack,因为进程的分配和管理也需要时间,并不能有效提升构建速度,甚至会变慢。

4.thread-loader

除了使用 Happypack 外,我们也可以使用 thread-loader ,把 thread-loader 放置在其它 loader 之前,那么放置在这个 loader 之后的 loader 就会在一个单独的 worker 池中运行。

在 worker 池(worker pool)中运行的 loader 是受到限制的。例如:

  • 这些 loader 不能产生新的文件。
  • 这些 loader 不能使用定制的 loader API(也就是说,通过插件)。
  • 这些 loader 无法获取 webpack 的选项设置。

首先安装依赖:

npm install thread-loader -D

修改配置:

module.exports = {
    module: {
        //我的项目中,babel-loader耗时比较长,所以我给它配置 thread-loader
        rules: [
            {
                test: /\.jsx?$/,
                use: ['thread-loader', 'cache-loader', 'babel-loader']
            }
        ]
    }
}

thread-loaderHappypack 我对比了一下,构建时间基本没什么差别。不过 thread-loader 配置起来为简单。

5.开启 JS 多进程压缩

虽然很多 webpack 优化的文章上会提及多进程压缩的优化,不管是 webpack-parallel-uglify-plugin 或者是 uglifyjs-webpack-plugin 配置 parallel。不过这里我要说一句,没必要单独安装这些插件,它们并不会让你的 Webpack 更快。

因为当前 Webpack 默认使用的是 TerserWebpackPlugin,默认开启了多进程和缓存,构建时,你的项目中可以看到 terser 的缓存文件 node_modules/.cache/terser-webpack-plugin

6.HardSourceWebpackPlugin

HardSourceWebpackPlugin 为模块提供中间缓存,缓存默认的存放路径是: node_modules/.cache/hard-source

配置 hard-source-webpack-plugin,首次构建时间没有太大变化,但是第二次开始,构建时间大约可以节约 80%。

首先安装依赖:

npm install hard-source-webpack-plugin -D

修改 webpack 的配置:

//webpack.config.js
var HardSourceWebpackPlugin = require('hard-source-webpack-plugin');
module.exports = {
    //...
    plugins: [
        new HardSourceWebpackPlugin()
    ]
}

用另外一个比较大的项目测试了下,配置了 HardSourceWebpackPlugin,构建时间从 8S 左右降到了 2S 左右。

HardSourceWebpackPlugin文档中 列出了一些你可能会遇到的问题以及如何解决,例如热更新失效,或者某些配置不生效等。

7.noParse

如果一些第三方模块没有AMD/CommonJS规范版本,可以使用 noParse 来标识这个模块,这样 webpack 会引入这些模块,但是不进行转化和解析,从而提升 webpack 的构建性能 ,例如:jquerylodash

noParse 属性的值是一个正则表达式或者是一个 function

//webpack.config.js
module.exports = {
    //...
    module: {
        noParse: /jquery|lodash/
    }
}

我当前的 webpack-optimize 项目中,没有使用 jquery 或者是 lodash

因此新建一个项目测试,只引入 jqueryloadsh,然后配置 noParse 和不配置 noParse,分别构建比对时间。

配置noParse 前,构建需要 2392ms。配置了 noParse 之后,构建需要 1613ms。 如果你使用到了不需要解析的第三方依赖,那么配置 noParse 很显然是一定会起到优化作用的。

8.resolve

resolve 配置 webpack 如何寻找模块所对应的文件。假设我们确定模块都从根目录下的 node_modules 中查找,我们可以配置:

//webpack.config.js
const path = require('path');
module.exports = {
    //...
    resolve: {
        modules: [path.resolve(__dirname, 'node_modules')],
    }
}

需要记住的是,如果你配置了上述的 resolve.moudles ,可能会出现问题,例如,你的依赖中还存在 node_modules 目录,那么就会出现,对应的文件明明在,但是却提示找不到。因此呢,个人不推荐配置这个。如果其他同事不熟悉这个配置,遇到这个问题时,会摸不着头脑。

另外,resolveextensions 配置,默认是 ['.js', '.json'],如果你要对它进行配置,记住将频率最高的后缀放在第一位,并且控制列表的长度,以减少尝试次数。

本项目较小,因此测试时,此处优化效果不明显。

9.IgnorePlugin

webpack 的内置插件,作用是忽略第三方包指定目录。

例如: moment (2.24.0版本) 会将所有本地化内容和核心功能一起打包,我们就可以使用 IgnorePlugin 在打包时忽略本地化内容。

//webpack.config.js
module.exports = {
    //...
    plugins: [
        //忽略 moment 下的 ./locale 目录
        new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/)
    ]
}

在使用的时候,如果我们需要指定语言,那么需要我们手动的去引入语言包,例如,引入中文语言包:

import moment from 'moment';
import 'moment/locale/zh-cn';// 手动引入

index.js 中只引入 moment,打包出来的 bundle.js 大小为 263KB,如果配置了 IgnorePlugin,单独引入 moment/locale/zh-cn,构建出来的包大小为 55KB

10.externals

我们可以将一些JS文件存储在 CDN 上(减少 Webpack打包出来的 js 体积),在 index.html 中通过 <script> 标签引入,如:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
    <div id="root">root</div>
    <script data-original="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
</body>
</html>

我们希望在使用时,仍然可以通过 import 的方式去引用(如 import $ from 'jquery'),并且希望 webpack 不会对其进行打包,此时就可以配置 externals

//webpack.config.js
module.exports = {
    //...
    externals: {
        //jquery通过script引入之后,全局中即有了 jQuery 变量
        'jquery': 'jQuery'
    }
}

11.DllPlugin

有些时候,如果所有的JS文件都打成一个JS文件,会导致最终生成的JS文件很大,这个时候,我们就要考虑拆分 bundles

DllPluginDLLReferencePlugin 可以实现拆分 bundles,并且可以大大提升构建速度,DllPluginDLLReferencePlugin 都是 webpack 的内置模块。

我们使用 DllPlugin 将不会频繁更新的库进行编译,当这些依赖的版本没有变化时,就不需要重新编译。我们新建一个 webpack 的配置文件,来专门用于编译动态链接库,例如名为: webpack.config.dll.js,这里我们将 reactreact-dom 单独打包成一个动态链接库。

//webpack.config.dll.js
const webpack = require('webpack');
const path = require('path');

module.exports = {
    entry: {
        react: ['react', 'react-dom']
    },
    mode: 'production',
    output: {
        filename: '[name].dll.[hash:6].js',
        path: path.resolve(__dirname, 'dist', 'dll'),
        library: '[name]_dll' //暴露给外部使用
        //libraryTarget 指定如何暴露内容,缺省时就是 var
    },
    plugins: [
        new webpack.DllPlugin({
            //name和library一致
            name: '[name]_dll', 
            path: path.resolve(__dirname, 'dist', 'dll', 'manifest.json') //manifest.json的生成路径
        })
    ]
}

package.jsonscripts 中增加:

{
    "scripts": {
        "dev": "NODE_ENV=development webpack-dev-server",
        "build": "NODE_ENV=production webpack",
        "build:dll": "webpack --config webpack.config.dll.js"
    },
}

执行 npm run build:all,可以看到 dist 目录如下,之所以将动态链接库单独放在 dll 目录下,主要是为了使用 CleanWebpackPlugin 更为方便的过滤掉动态链接库。

dist
└── dll
    ├── manifest.json
    └── react.dll.9dcd9d.js

manifest.json 用于让 DLLReferencePlugin 映射到相关依赖上。

修改 webpack 的主配置文件: webpack.config.js 的配置:

//webpack.config.js
const webpack = require('webpack');
const path = require('path');
module.exports = {
    //...
    devServer: {
        contentBase: path.resolve(__dirname, 'dist')
    },
    plugins: [
        new webpack.DllReferencePlugin({
            manifest: path.resolve(__dirname, 'dist', 'dll', 'manifest.json')
        }),
        new CleanWebpackPlugin({
            cleanOnceBeforeBuildPatterns: ['**/*', '!dll', '!dll/**'] //不删除dll目录
        }),
        //...
    ]
}

使用 npm run build 构建,可以看到 bundle.js 的体积大大减少。

修改 public/index.html 文件,在其中引入 react.dll.js

<script data-original="/dll/react.dll.9dcd9d.js"></script>
构建速度

DllPlugin.jpeg

包体积

dll-size.jpeg

12.抽离公共代码

抽离公共代码是对于多页应用来说的,如果多个页面引入了一些公共模块,那么可以把这些公共的模块抽离出来,单独打包。公共代码只需要下载一次就缓存起来了,避免了重复下载。

抽离公共代码对于单页应用和多页应该在配置上没有什么区别,都是配置在 optimization.splitChunks 中。

//webpack.config.js
module.exports = {
    optimization: {
        splitChunks: {//分割代码块
            cacheGroups: {
                vendor: {
                    //第三方依赖
                    priority: 1, //设置优先级,首先抽离第三方模块
                    name: 'vendor',
                    test: /node_modules/,
                    chunks: 'initial',
                    minSize: 0,
                    minChunks: 1 //最少引入了1次
                },
                //缓存组
                common: {
                    //公共模块
                    chunks: 'initial',
                    name: 'common',
                    minSize: 100, //大小超过100个字节
                    minChunks: 3 //最少引入了3次
                }
            }
        }
    }
}

即使是单页应用,同样可以使用这个配置,例如,打包出来的 bundle.js 体积过大,我们可以将一些依赖打包成动态链接库,然后将剩下的第三方依赖拆出来。这样可以有效减小 bundle.js 的体积大小。当然,你还可以继续提取业务代码的公共模块,此处,因为我项目中源码较少,所以没有配置。

splitChunks.jpeg

runtimeChunk

runtimeChunk 的作用是将包含 chunk 映射关系的列表从 main.js 中抽离出来,在配置了 splitChunk 时,记得配置 runtimeChunk.

module.exports = {
    //...
    optimization: {
        runtimeChunk: {
            name: 'manifest'
        }
    }
}

最终构建出来的文件中会生成一个 manifest.js

借助 webpack-bundle-analyzer 进一步优化

在做 webpack 构建优化的时候,vendor 打出来超过了1M,reactreact-dom 已经打包成了DLL。

因此需要借助 webpack-bundle-analyzer 查看一下是哪些包的体积较大。

首先安装依赖:

npm install webpack-bundle-analyzer -D

使用也很简单,修改下我们的配置:

//webpack.config.prod.js
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const merge = require('webpack-merge');
const baseWebpackConfig = require('./webpack.config.base');
module.exports = merge(baseWebpackConfig, {
    //....
    plugins: [
        //...
        new BundleAnalyzerPlugin(),
    ]
})

npm run build 构建,会默认打开: http://127.0.0.1:8888/,可以看到各个包的体积:

W1.jpeg

进一步对 vendor 进行拆分,将 vendor 拆分成了4个(使用 splitChunks 进行拆分即可)。

module.exports = {
    optimization: {
    concatenateModules: false,
    splitChunks: {//分割代码块
      maxInitialRequests:6, //默认是5
      cacheGroups: {
        vendor: {
          //第三方依赖
          priority: 1,
          name: 'vendor',
          test: /node_modules/,
          chunks: 'initial',
          minSize: 100,
          minChunks: 1 //重复引入了几次
        },
        'lottie-web': {
          name: "lottie-web", // 单独将 react-lottie 拆包
          priority: 5, // 权重需大于`vendor`
          test: /[\/]node_modules[\/]lottie-web[\/]/,
          chunks: 'initial',
          minSize: 100,
          minChunks: 1 //重复引入了几次
        },
        //...
      }
    },
  },
}

重新构建,结果如下所示:

W2.jpeg

13.webpack自身的优化

tree-shaking

如果使用ES6的import 语法,那么在生产环境下,会自动移除没有使用到的代码。

//math.js
const add = (a, b) => {
    console.log('aaaaaa')
    return a + b;
}

const minus = (a, b) => {
    console.log('bbbbbb')
    return a - b;
}

export {
    add,
    minus
}
//index.js
import {add, minus} from './math';
add(2,3);

构建的最终代码里,minus 函数不会被打包进去。

scope hosting 作用域提升

变量提升,可以减少一些变量声明。在生产环境下,默认开启。

另外,大家测试的时候注意一下,speed-measure-webpack-pluginHotModuleReplacementPlugin 不能同时使用,否则会报错:

webpack 的配置部分到此基本就结束了,typescripteslintprettier,这里没有提及,大家可以自己配置一下,也可以参考我之前练手时配置的一个项目:https://github.com/YvetteLau/...

babel 配置的优化

如果你对 babel 还不太熟悉的话,那么可以阅读这篇文章:不容错过的 Babel7 知识

在不配置 @babel/plugin-transform-runtime 时,babel 会使用很小的辅助函数来实现类似 _createClass 等公共方法。默认情况下,它将被注入(inject)到需要它的每个文件中。但是这样的结果就是导致构建出来的JS体积变大。

我们也并不需要在每个 js 中注入辅助函数,因此我们可以使用 @babel/plugin-transform-runtime@babel/plugin-transform-runtime 是一个可以重复使用 Babel 注入的帮助程序,以节省代码大小的插件。

因此我们可以在 .babelrc 中增加 @babel/plugin-transform-runtime 的配置。

{
    "presets": [],
    "plugins": [
        [
            "@babel/plugin-transform-runtime"
        ]
    ]
}

以上就是我目前为止使用到的一些优化,如果你有更好的优化方式,欢迎在评论区留言,感谢阅读。

最后

如果本文对你有帮助的话,给本文点个赞吧。

参考文档:
查看原文

赞 109 收藏 86 评论 7

刘小夕 发布了文章 · 2020-03-17

万字长文带你深度解锁Webpack系列(进阶篇)

如果你还没有阅读《4W字长文带你深度解锁Webpack系列(基础篇)》,建议阅读之后,再继续阅读本篇文章。

本文会引入更多的 webpack 配置,如果文中有任何错误,欢迎在评论区指正,我会尽快修正。 webpack 优化部分放在了下一篇。

推荐大家参考本文一步一步进行配置,不要总是想着找什么最佳配置,你掌握了之后,根据自己的需求配置出来的,就是最佳配置。

本文对应的项目地址(编写本文时使用) 供参考:https://github.com/YvetteLau/...

1. 静态资源拷贝

有些时候,我们需要使用已有的JS文件、CSS文件(本地文件),但是不需要 webpack 编译。例如,我们在 public/index.html 中引入了 public 目录下的 jscss 文件。这个时候,如果直接打包,那么在构建出来之后,肯定是找不到对应的 js / css 了。

public 目录结构
├── public
│   ├── config.js
│   ├── index.html
│   ├── js
│   │   ├── base.js
│   │   └── other.js
│   └── login.html

现在,我们在 index.html 中引入了 ./js/base.js

<!-- index.html -->
<script data-original="./js/base.js"></script>

这时候,我们 npm run dev,会发现有找不到该资源文件的报错信息。

对于这个问题,我们可以手动将其拷贝至构建目录,然后在配置 CleanWebpackPlugin 时,注意不要清空对应的文件或文件夹即可,但是如若这个静态文件时不时的还会修改下,那么依赖于手动拷贝,是很容易出问题的。

不要过于相信自己的记性,依赖于手动拷贝的方式,大多数人应该都有过忘记拷贝的经历,你要是说你从来没忘过。

050a81c7-59e4-4596-b08f-62cefce353d0.jpg

幸运的是,webpack 为我们这些记性不好又爱偷懒的人提供了好用的插件 CopyWebpackPlugin,它的作用就是将单个文件或整个目录复制到构建目录。

首先安装一下依赖:

npm install copy-webpack-plugin -D

修改配置(当前,需要做的是将 public/js 目录拷贝至 dist/js 目录):

//webpack.config.js
const CopyWebpackPlugin = require('copy-webpack-plugin');
module.exports = {
    //...
    plugins: [
        new CopyWebpackPlugin([
            {
                from: 'public/js/*.js',
                to: path.resolve(__dirname, 'dist', 'js'),
                flatten: true,
            },
            //还可以继续配置其它要拷贝的文件
        ])
    ]
}

此时,重新执行 npm run dev,报错信息已经消失。

这里说一下 flatten 这个参数,设置为 true,那么它只会拷贝文件,而不会把文件夹路径都拷贝上,大家可以不设置 flatten 时,看下构建结果。

另外,如果我们要拷贝一个目录下的很多文件,但是想过滤掉某个或某些文件,那么 CopyWebpackPlugin 还为我们提供了 ignore 参数。

//webpack.config.js
const CopyWebpackPlugin = require('copy-webpack-plugin');
module.exports = {
    //...
    plugins: [
        new CopyWebpackPlugin([
            {
                from: 'public/js/*.js',
                to: path.resolve(__dirname, 'dist', 'js'),
                flatten: true,
            }
        ], {
            ignore: ['other.js']
        })
    ]
}

例如,这里我们忽略掉 js 目录下的 other.js 文件,使用 npm run build 构建,可以看到 dist/js 下不会出现 other.js 文件。 CopyWebpackPlugin 还提供了很多其它的参数,如果当前的配置不能满足你,可以查阅文档进一步修改配置。

2.ProvidePlugin

ProvidePlugin 在我看来,是为懒人准备的,不过也别过度使用,毕竟全局变量不是什么“好东西”。ProvidePlugin 的作用就是不需要 importrequire 就可以在项目中到处使用。

ProvidePluginwebpack 的内置插件,使用方式如下:

new webpack.ProvidePlugin({
  identifier1: 'module1',
  identifier2: ['module2', 'property2']
});

默认寻找路径是当前文件夹 ./**node_modules,当然啦,你可以指定全路径。

React 大家都知道的,使用的时候,要在每个文件中引入 React,不然立刻抛错给你看。还有就是 jquery, lodash 这样的库,可能在多个文件中使用,但是懒得每次都引入,好嘛,一起来偷个懒,修改下 webpack 的配置:

const webpack = require('webpack');
module.exports = {
    //...
    plugins: [
        new webpack.ProvidePlugin({
            React: 'react',
            Component: ['react', 'Component'],
            Vue: ['vue/dist/vue.esm.js', 'default'],
            $: 'jquery',
            _map: ['lodash', 'map']
        })
    ]
}

这样配置之后,你就可以在项目中随心所欲的使用 $_map了,并且写 React 组件时,也不需要 importReactComponent 了,如果你想的话,你还可以把 ReactHooks 都配置在这里。

另外呢,Vue 的配置后面多了一个 default,这是因为 vue.esm.js 中使用的是 export default 导出的,对于这种,必须要指定 defaultReact 使用的是 module.exports 导出的,因此不要写 default

另外,就是如果你项目启动了 eslint 的话,记得修改下 eslint 的配置文件,增加以下配置:

{
    "globals": {
        "React": true,
        "Vue": true,
        //....
    }
}

当然啦,偷懒要有个度,你要是配一大堆全局变量,最终可能会给自己带来麻烦,对自己配置的全局变量一定要负责到底。

u=2243033496,1576809017&fm=15&gp=0.jpg

3.抽离CSS

CSS打包我们前面已经说过了,不过呢,有些时候,我们可能会有抽离CSS的需求,即将CSS文件单独打包,这可能是因为打包成一个JS文件太大,影响加载速度,也有可能是为了缓存(例如,只有JS部分有改动),还有可能就是“我高兴”:我想抽离就抽离,谁也管不着。

不管你是因为什么原因要抽离CSS,只要你有需求,我们就可以去实现。

首先,安装 loader:

npm install mini-css-extract-plugin -D
mini-css-extract-pluginextract-text-webpack-plugin 相比:
  1. 异步加载
  2. 不会重复编译(性能更好)
  3. 更容易使用
  4. 只适用CSS

修改我们的配置文件:

//webpack.config.js
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
    plugins: [
        new MiniCssExtractPlugin({
            filename: 'css/[name].css' //个人习惯将css文件放在单独目录下
        })
    ],
    module: {
        rules: [
            {
                test: /\.(le|c)ss$/,
                use: [
                    MiniCssExtractPlugin.loader, //替换之前的 style-loader
                    'css-loader', {
                        loader: 'postcss-loader',
                        options: {
                            plugins: function () {
                                return [
                                    require('autoprefixer')({
                                        "overrideBrowserslist": [
                                            "defaults"
                                        ]
                                    })
                                ]
                            }
                        }
                    }, 'less-loader'
                ],
                exclude: /node_modules/
            }
        ]
    }
}

现在,我们重新编译:npm run build,目录结构如下所示:

.
├── dist
│   ├── assets
│   │   ├── alita_e09b5c.jpg
│   │   └── thor_e09b5c.jpeg
│   ├── css
│   │   ├── index.css
│   │   └── index.css.map
│   ├── bundle.fb6d0c.js
│   ├── bundle.fb6d0c.js.map
│   └── index.html

前面说了最好新建一个 .browserslistrc 文件,这样可以多个 loader 共享配置,所以,动手在根目录下新建文件 (.browserslistrc),内容如下(你可以根据自己项目需求,修改为其它的配置):

last 2 version
> 0.25%
not dead

修改 webpack.config.js

//webpack.config.js
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
    //...
    plugins: [
        new MiniCssExtractPlugin({
            filename: 'css/[name].css' 
        })
    ],
    module: {
        rules: [
            {
                test: /\.(c|le)ss$/,
                use: [
                    MiniCssExtractPlugin.loader,
                    'css-loader', {
                        loader: 'postcss-loader',
                        options: {
                            plugins: function () {
                                return [
                                    require('autoprefixer')()
                                ]
                            }
                        }
                    }, 'less-loader'
                ],
                exclude: /node_modules/
            },
        ]
    }
}

要测试自己的 .browserlistrc 有没有生效也很简单,直接将文件内容修改为 last 1 Chrome versions ,然后对比修改前后的构建出的结果,就能看出来啦。

可以查看更多[browserslistrc]配置项(https://github.com/browsersli...

更多配置项,可以查看mini-css-extract-plugin

将抽离出来的css文件进行压缩

使用 mini-css-extract-pluginCSS 文件默认不会被压缩,如果想要压缩,需要配置 optimization,首先安装 optimize-css-assets-webpack-plugin.

npm install optimize-css-assets-webpack-plugin -D

修改webpack配置:

//webpack.config.js
const OptimizeCssPlugin = require('optimize-css-assets-webpack-plugin');

module.exports = {
    entry: './src/index.js',
    //....
    plugins: [
        new OptimizeCssPlugin()
    ],
}

注意,这里将 OptimizeCssPlugin 直接配置在 plugins 里面,那么 jscss 都能够正常压缩,如果你将这个配置在 optimization,那么需要再配置一下 js 的压缩(开发环境下不需要去做CSS的压缩,因此后面记得将其放到 webpack.config.prod.js 中哈)。

配置完之后,测试的时候发现,抽离之后,修改 css 文件时,第一次页面会刷新,但是第二次页面不会刷新 —— 好嘛,我平时的业务中用不着抽离 css,这个问题搁置了好多天(准确来说是忘记了)。

昨晚(0308)再次修改这篇文章的时候,正好看到了 MiniCssExtractPlugin.loader 对应的 option 设置,我们再次修改下对应的 rule

module.exports = {
    rules: [
        {
            test: /\.(c|le)ss$/,
            use: [
                {
                    loader: MiniCssExtractPlugin.loader,
                    options: {
                        hmr: isDev,
                        reloadAll: true,
                    }
                },
                //...
            ],
            exclude: /node_modules/
        }
    ]
}

4.按需加载

很多时候我们不需要一次性加载所有的JS文件,而应该在不同阶段去加载所需要的代码。webpack内置了强大的分割代码的功能可以实现按需加载。

比如,我们在点击了某个按钮之后,才需要使用使用对应的JS文件中的代码,需要使用 import() 语法:

document.getElementById('btn').onclick = function() {
    import('./handle').then(fn => fn.default());
}

import() 语法,需要 @babel/plugin-syntax-dynamic-import 的插件支持,但是因为当前 @babel/preset-env 预设中已经包含了 @babel/plugin-syntax-dynamic-import,因此我们不需要再单独安装和配置。

直接 npm run build 进行构建,构建结果如下:

WechatIMG1121.jpeg

webpack 遇到 import(****) 这样的语法的时候,会这样处理:

  • ** 为入口新生成一个 Chunk
  • 当代码执行到 import 所在的语句时,才会加载该 Chunk 所对应的文件(如这里的1.bundle.8bf4dc.js)

大家可以在浏览器中的控制台中,在 NetworkTab页 查看文件加载的情况,只有点击之后,才会加载对应的 JS

5.热更新

  1. 首先配置 devServerhottrue
  2. 并且在 plugins 中增加 new webpack.HotModuleReplacementPlugin()
//webpack.config.js
const webpack = require('webpack');
module.exports = {
    //....
    devServer: {
        hot: true
    },
    plugins: [
        new webpack.HotModuleReplacementPlugin() //热更新插件
    ]
}

我们配置了 HotModuleReplacementPlugin 之后,会发现,此时我们修改代码,仍然是整个页面都会刷新。不希望整个页面都刷新,还需要修改入口文件:

  1. 在入口文件中新增:
if(module && module.hot) {
    module.hot.accept()
}

此时,再修改代码,不会造成整个页面的刷新。

6.多页应用打包

有时,我们的应用不一定是一个单页应用,而是一个多页应用,那么如何使用 webpack 进行打包呢。为了生成目录看起来清晰,不生成单独的 map 文件。

//webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
    entry: {
        index: './src/index.js',
        login: './src/login.js'
    },
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: '[name].[hash:6].js'
    },
    //...
    plugins: [
        new HtmlWebpackPlugin({
            template: './public/index.html',
            filename: 'index.html' //打包后的文件名
        }),
        new HtmlWebpackPlugin({
            template: './public/login.html',
            filename: 'login.html' //打包后的文件名
        }),
    ]
}

如果需要配置多个 HtmlWebpackPlugin,那么 filename 字段不可缺省,否则默认生成的都是 index.html,如果你希望 html 的文件名中也带有 hash,那么直接修改 fliename 字段即可,例如: filename: 'login.[hash:6].html'

生成目录如下:

.
├── dist
│   ├── 2.463ccf.js
│   ├── assets
│   │   └── thor_e09b5c.jpeg
│   ├── css
│   │   ├── index.css
│   │   └── login.css
│   ├── index.463ccf.js
│   ├── index.html
│   ├── js
│   │   └── base.js
│   ├── login.463ccf.js
│   └── login.html

看起来,似乎是OK了,不过呢,查看 index.htmllogin.html 会发现,都同时引入了 index.f7d21a.jslogin.f7d21a.js,通常这不是我们想要的,我们希望,index.html 中只引入 index.f7d21a.jslogin.html 只引入 login.f7d21a.js

HtmlWebpackPlugin 提供了一个 chunks 的参数,可以接受一个数组,配置此参数仅会将数组中指定的js引入到html文件中,此外,如果你需要引入多个JS文件,仅有少数不想引入,还可以指定 excludeChunks 参数,它接受一个数组。

//webpack.config.js
module.exports = {
    //...
    plugins: [
        new HtmlWebpackPlugin({
            template: './public/index.html',
            filename: 'index.html', //打包后的文件名
            chunks: ['index']
        }),
        new HtmlWebpackPlugin({
            template: './public/login.html',
            filename: 'login.html', //打包后的文件名
            chunks: ['login']
        }),
    ]
}

执行 npm run build,可以看到 index.html 中仅引入了 indexJS 文件,而 login.html 中也仅引入了 loginJS 文件,符合我们的预期。

7.resolve 配置

resolve 配置 webpack 如何寻找模块所对应的文件。webpack 内置 JavaScript 模块化语法解析功能,默认会采用模块化标准里约定好的规则去寻找,但你可以根据自己的需要修改默认的规则。

  1. modules

resolve.modules 配置 webpack 去哪些目录下寻找第三方模块,默认情况下,只会去 node_modules 下寻找,如果你我们项目中某个文件夹下的模块经常被导入,不希望写很长的路径,那么就可以通过配置 resolve.modules 来简化。

//webpack.config.js
module.exports = {
    //....
    resolve: {
        modules: ['./src/components', 'node_modules'] //从左到右依次查找
    }
}

这样配置之后,我们 import Dialog from 'dialog',会去寻找 ./src/components/dialog,不再需要使用相对路径导入。如果在 ./src/components 下找不到的话,就会到 node_modules 下寻找。

  1. alias

resolve.alias 配置项通过别名把原导入路径映射成一个新的导入路径,例如:

//webpack.config.js
module.exports = {
    //....
    resolve: {
        alias: {
            'react-native': '@my/react-native-web' //这个包名是我随便写的哈
        }
    }
}

例如,我们有一个依赖 @my/react-native-web 可以实现 react-nativeweb。我们代码一般下面这样:

import { View, ListView, StyleSheet, Animated } from 'react-native';

配置了别名之后,在转 web 时,会从 @my/react-native-web 寻找对应的依赖。

当然啦,如果某个依赖的名字太长了,你也可以给它配置一个短一点的别名,这样用起来比较爽,尤其是带有 scope 的包。

  1. extensions

适配多端的项目中,可能会出现 .web.js, .wx.js,例如在转web的项目中,我们希望首先找 .web.js,如果没有,再找 .js。我们可以这样配置:

//webpack.config.js
module.exports = {
    //....
    resolve: {
        extensions: ['web.js', '.js'] //当然,你还可以配置 .json, .css
    }
}

首先寻找 ../dialog.web.js ,如果不存在的话,再寻找 ../dialog.js。这在适配多端的代码中非常有用,否则,你就需要根据不同的平台去引入文件(以牺牲了速度为代价)。

import dialog from '../dialog';

当然,配置 extensions,我们就可以缺省文件后缀,在导入语句没带文件后缀时,会自动带上extensions 中配置的后缀后,去尝试访问文件是否存在,因此要将高频的后缀放在前面,并且数组不要太长,减少尝试次数。如果没有配置 extensions,默认只会找对对应的js文件。

  1. enforceExtension

如果配置了 resolve.enforceExtensiontrue,那么导入语句不能缺省文件后缀。

  1. mainFields

有一些第三方模块会提供多份代码,例如 bootstrap,可以查看 bootstrappackage.json 文件:

{
    "style": "dist/css/bootstrap.css",
    "sass": "scss/bootstrap.scss",
    "main": "dist/js/bootstrap",
}

resolve.mainFields 默认配置是 ['browser', 'main'],即首先找对应依赖 package.json 中的 brower 字段,如果没有,找 main 字段。

如:import 'bootstrap' 默认情况下,找得是对应的依赖的 package.jsonmain 字段指定的文件,即 dist/js/bootstrap

假设我们希望,import 'bootsrap' 默认去找 css 文件的话,可以配置 resolve.mainFields 为:

//webpack.config.js
module.exports = {
    //....
    resolve: {
        mainFields: ['style', 'main'] 
    }
}

8.区分不同的环境

目前为止我们 webpack 的配置,都定义在了 webpack.config.js 中,对于需要区分是开发环境还是生产环境的情况,我们根据 process.env.NODE_ENV 去进行了区分配置,但是配置文件中如果有多处需要区分环境的配置,这种显然不是一个好办法。

更好的做法是创建多个配置文件,如: webpack.base.jswebpack.dev.jswebpack.prod.js

  • webpack.base.js 定义公共的配置
  • webpack.dev.js:定义开发环境的配置
  • webpack.prod.js:定义生产环境的配置

webpack-merge 专为 webpack 设计,提供了一个 merge 函数,用于连接数组,合并对象。

npm install webpack-merge -D
const merge = require('webpack-merge');
merge({
    devtool: 'cheap-module-eval-source-map',
    module: {
        rules: [
            {a: 1}
        ]
    },
    plugins: [1,2,3]
}, {
    devtool: 'none',
    mode: "production",
    module: {
        rules: [
            {a: 2},
            {b: 1}
        ]
    },
    plugins: [4,5,6],
});
//合并后的结果为
{
    devtool: 'none',
    mode: "production",
    module: {
        rules: [
            {a: 1},
            {a: 2},
            {b: 1}
        ]
    },
    plugins: [1,2,3,4,5,6]
}

webpack.config.base.js 中是通用的 webpack 配置,以 webpack.config.dev.js 为例,如下:

//webpack.config.dev.js
const merge = require('webpack-merge');
const baseWebpackConfig = require('./webpack.config.base');

module.exports = merge(baseWebpackConfig, {
    mode: 'development'
    //...其它的一些配置
});

然后修改我们的 package.json,指定对应的 config 文件:

//package.json
{
    "scripts": {
        "dev": "cross-env NODE_ENV=development webpack-dev-server --config=webpack.config.dev.js",
        "build": "cross-env NODE_ENV=production webpack --config=webpack.config.prod.js"
    },
}

你可以使用 merge 合并,也可以使用 merge.smart 合并,merge.smart 在合并loader时,会将同一匹配规则的进行合并,webpack-merge 的说明文档中给出了详细的示例。

9.定义环境变量

很多时候,我们在开发环境中会使用预发环境或者是本地的域名,生产环境中使用线上域名,我们可以在 webpack 定义环境变量,然后在代码中使用。

使用 webpack 内置插件 DefinePlugin 来定义环境变量。

DefinePlugin 中的每个键,是一个标识符.

  • 如果 value 是一个字符串,会被当做 code 片段
  • 如果 value 不是一个字符串,会被stringify
  • 如果 value 是一个对象,正常对象定义即可
  • 如果 key 中有 typeof,它只针对 typeof 调用定义
//webpack.config.dev.js
const webpack = require('webpack');
module.exports = {
    plugins: [
        new webpack.DefinePlugin({
            DEV: JSON.stringify('dev'), //字符串
            FLAG: 'true' //FLAG 是个布尔类型
        })
    ]
}
//index.js
if(DEV === 'dev') {
    //开发环境
}else {
    //生产环境
}

10.利用webpack解决跨域问题

假设前端在3000端口,服务端在4000端口,我们通过 webpack 配置的方式去实现跨域。

首先,我们在本地创建一个 server.js

let express = require('express');

let app = express();

app.get('/api/user', (req, res) => {
    res.json({name: '刘小夕'});
});

app.listen(4000);

执行代码(run code),现在我们可以在浏览器中访问到此接口: http://localhost:4000/api/user

index.js 中请求 /api/user,修改 index.js 如下:

//需要将 localhost:3000 转发到 localhost:4000(服务端) 端口
fetch("/api/user")
    .then(response => response.json())
    .then(data => console.log(data))
    .catch(err => console.log(err));

我们希望通过配置代理的方式,去访问 4000 的接口。

配置代理

修改 webpack 配置:

//webpack.config.js
module.exports = {
    //...
    devServer: {
        proxy: {
            "/api": "http://localhost:4000"
        }
    }
}

重新执行 npm run dev,可以看到控制台打印出来了 {name: "刘小夕"},实现了跨域。

大多情况,后端提供的接口并不包含 /api,即:/user/info/list 等,配置代理时,我们不可能罗列出每一个api。

修改我们的服务端代码,并重新执行。

//server.js
let express = require('express');

let app = express();

app.get('/user', (req, res) => {
    res.json({name: '刘小夕'});
});

app.listen(4000);

尽管后端的接口并不包含 /api,我们在请求后端接口时,仍然以 /api 开头,在配置代理时,去掉 /api,修改配置:

//webpack.config.js
module.exports = {
    //...
    devServer: {
        proxy: {
            '/api': {
                target: 'http://localhost:4000',
                pathRewrite: {
                    '/api': ''
                }
            }
        }
    }
}

重新执行 npm run dev,在浏览器中访问: http://localhost:3000/,控制台中也打印出了{name: "刘小夕"},跨域成功,

11.前端模拟数据

简单数据模拟
module.exports = {
    devServer: {
        before(app) {
            app.get('/user', (req, res) => {
                res.json({name: '刘小夕'})
            })
        }
    }
}

src/index.js 中直接请求 /user 接口。

fetch("user")
    .then(response => response.json())
    .then(data => console.log(data))
    .catch(err => console.log(err));
使用 mocker-api mock数据接口

mocker-api 为 REST API 创建模拟 API。在没有实际 REST API 服务器的情况下测试应用程序时,它会很有用。

  1. 安装 mocker-api:
npm install mocker-api -D
  1. 在项目中新建mock文件夹,新建 mocker.js.文件,文件如下:
module.exports = {
    'GET /user': {name: '刘小夕'},
    'POST /login/account': (req, res) => {
        const { password, username } = req.body
        if (password === '888888' && username === 'admin') {
            return res.send({
                status: 'ok',
                code: 0,
                token: 'sdfsdfsdfdsf',
                data: { id: 1, name: '刘小夕' }
            })
        } else {
            return res.send({ status: 'error', code: 403 })
        }
    }
}
  1. 修改 webpack.config.base.js:
const apiMocker = require('mocker-api');
module.export = {
    //...
    devServer: {
        before(app){
            apiMocker(app, path.resolve('./mock/mocker.js'))
        }
    }
}

这样,我们就可以直接在代码中像请求后端接口一样对mock数据进行请求。

  1. 重启 npm run dev,可以看到,控制台成功打印出来 {name: '刘小夕'}
  2. 我们再修改下 src/index.js,检查下POST接口是否成功
//src/index.js
fetch("/login/account", {
    method: "POST",
    headers: {
        'Accept': 'application/json',
        'Content-Type': 'application/json'
    },
    body: JSON.stringify({
        username: "admin",
        password: "888888"
    })
})
    .then(response => response.json())
    .then(data => console.log(data))
    .catch(err => console.log(err));

可以在控制台中看到接口返回的成功的数据。

进阶篇就到这里结束啦,下周约优化篇。

最后

关注公众号

参考:
查看原文

赞 46 收藏 38 评论 4

刘小夕 发布了文章 · 2020-03-09

4W字长文带你深度解锁Webpack系列(基础篇)

三篇长文,4W余字,带你解锁 webpack ,希望读完这三篇文章,你能够对 webpack 的各项配置有一个更为清晰的认识。

1.webpack 是什么?

webpack 是一个现代 JavaScript 应用程序的静态模块打包器,当 webpack 处理应用程序时,会递归构建一个依赖关系图,其中包含应用程序需要的每个模块,然后将这些模块打包成一个或多个 bundle

2.webpack 的核心概念

  • entry: 入口
  • output: 输出
  • loader: 模块转换器,用于把模块原内容按照需求转换成新内容
  • 插件(plugins): 扩展插件,在webpack构建流程中的特定时机注入扩展逻辑来改变构建结果或做你想要做的事情

3.初始化项目

新建一个文件夹,如: webpack-first (当然,你可以使用任意一个你喜欢的项目名)。推荐大家参考本文一步一步进行配置,不要总是在网上找什么最佳配置,你掌握了webpack之后,根据自己的需求配置出来的,就是最佳配置。

本篇文章对应的项目地址(编写本文时使用): https://github.com/YvetteLau/...

使用 npm init -y 进行初始化(也可以使用 yarn)。

要使用 webpack,那么必然需要安装 webpackwebpack-cli:

npm install webpack webpack-cli -D

鉴于前端技术变更迅速,祭出本篇文章基于 webpack 的版本号:

├── webpack@4.41.5 
└── webpack-cli@3.3.10 

wepack V4.0.0 开始, webpack 是开箱即用的,在不引入任何配置文件的情况下就可以使用。

新建 src/index.js 文件,我们在文件中随便写点什么:

//index.js
class Animal {
    constructor(name) {
        this.name = name;
    }
    getName() {
        return this.name;
    }
}

const dog = new Animal('dog');

使用 npx webpack --mode=development 进行构建,默认是 production 模式,我们为了更清楚得查看打包后的代码,使用 development 模式。

可以看到项目下多了个 dist 目录,里面有一个打包出来的文件 main.js

webpack 有默认的配置,如默认的入口文件是 ./src,默认打包到dist/main.js。更多的默认配置可以查看: node_modules/webpack/lib/WebpackOptionsDefaulter.js

查看 dist/main.js 文件,可以看到,src/index.js 并没有被转义为低版本的代码,这显然不是我们想要的。

{
    "./src/index.js":
        (function (module, exports) {

            eval("class Animal {\n    constructor(name) {\n        this.name = name;\n    }\n    getName() {\n        return this.name;\n    }\n}\n\nconst dog = new Animal('dog');\n\n//# sourceURL=webpack:///./src/index.js?");

        })
}

4.将JS转义为低版本

前面我们说了 webpack 的四个核心概念,其中之一就是 loaderloader 用于对源代码进行转换,这正是我们现在所需要的。

将JS代码向低版本转换,我们需要使用 babel-loader

babel-loader

首先安装一下 babel-loader

npm install babel-loader -D

此外,我们还需要配置 babel,为此我们安装一下以下依赖:

npm install @babel/core @babel/preset-env @babel/plugin-transform-runtime -D

npm install @babel/runtime @babel/runtime-corejs3

对babel7配置不熟悉的小伙伴,可以阅读一下这篇文章: 不可错过的 Babel7 知识

新建 webpack.config.js,如下:

//webpack.config.js
module.exports = {
    module: {
        rules: [
            {
                test: /\.jsx?$/,
                use: ['babel-loader'],
                exclude: /node_modules/ //排除 node_modules 目录
            }
        ]
    }
}

建议给 loader 指定 include 或是 exclude,指定其中一个即可,因为 node_modules 目录通常不需要我们去编译,排除后,有效提升编译效率。

这里,我们可以在 .babelrc 中编写 babel 的配置,也可以在 webpack.config.js 中进行配置。

创建一个 .babelrc

配置如下:

{
    "presets": ["@babel/preset-env"],
    "plugins": [
        [
            "@babel/plugin-transform-runtime",
            {
                "corejs": 3
            }
        ]
    ]
}

现在,我们重新执行 npx webpack --mode=development,查看 dist/main.js,会发现已经被编译成了低版本的JS代码。

在webpack中配置 babel

//webpack.config.js
module.exports = {
    // mode: 'development',
    module: {
        rules: [
            {
                test: /\.jsx?$/,
                use: {
                    loader: 'babel-loader',
                    options: {
                        presets: ["@babel/preset-env"],
                        plugins: [
                            [
                                "@babel/plugin-transform-runtime",
                                {
                                    "corejs": 3
                                }
                            ]
                        ]
                    }
                },
                exclude: /node_modules/
            }
        ]
    }
}

这里有几点需要说明:

  • loader 需要配置在 module.rules 中,rules 是一个数组。
  • loader 的格式为:
{
    test: /\.jsx?$/,//匹配规则
    use: 'babel-loader'
}

或者也可以像下面这样:

//适用于只有一个 loader 的情况
{
    test: /\.jsx?$/,
    loader: 'babel-loader',
    options: {
        //...
    }
}

test 字段是匹配规则,针对符合规则的文件进行处理。

use 字段有几种写法

  • 可以是一个字符串,例如上面的 use: 'babel-loader'
  • use 字段可以是一个数组,例如处理CSS文件是,use: ['style-loader', 'css-loader']
  • use 数组的每一项既可以是字符串也可以是一个对象,当我们需要在webpack 的配置文件中对 loader 进行配置,就需要将其编写为一个对象,并且在此对象的 options 字段中进行配置,如:
rules: [
    {
        test: /\.jsx?$/,
        use: {
            loader: 'babel-loader',
            options: {
                presets: ["@babel/preset-env"]
            }
        },
        exclude: /node_modules/
    }
]

上面我们说了如何将JS的代码编译成向下兼容的代码,当然你可以还需要一些其它的 babel 的插件和预设,例如 @babel/preset-react@babel/plugin-proposal-optional-chaining 等,不过,babel 的配置并非本文的重点,我们继续往下。

不要说细心的小伙伴了,即使是粗心的小伙伴肯定也发现了,我们在使用 webpack 进行打包的时候,一直运行的都是 npx webpack --mode=development 是否可以将 mode 配置在 webpack.config.js 中呢?显然是可以的。

5.mode

mode 增加到 webpack.config.js 中:

module.exports = {
    //....
    mode: "development",
    module: {
        //...
    }
}

mode 配置项,告知 webpack 使用相应模式的内置优化。

mode 配置项,支持以下两个配置:

  • development:将 process.env.NODE_ENV 的值设置为 development,启用 NamedChunksPluginNamedModulesPlugin
  • production:将 process.env.NODE_ENV 的值设置为 production,启用 FlagDependencyUsagePlugin, FlagIncludedChunksPlugin, ModuleConcatenationPlugin, NoEmitOnErrorsPlugin, OccurrenceOrderPlugin, SideEffectsFlagPluginUglifyJsPlugin

现在,我们之间使用 npx webpack 进行编译即可。

6.在浏览器中查看页面

搞了这么久,还不能在浏览器中查看页面,这显然不能忍!

查看页面,难免就需要 html 文件,有小伙伴可能知道,有时我们会指定打包文件中带有 hash,那么每次生成的 js 文件名会有所不同,总不能让我们每次都人工去修改 html,这样不是显得我们很蠢嘛~

我们可以使用 html-webpack-plugin 插件来帮助我们完成这些事情。

首先,安装一下插件:

npm install html-webpack-plugin -D 

新建 public 目录,并在其中新建一个 index.html 文件( 文件内容使用 html:5 快捷生成即可)

修改 webpack.config.js 文件。

//首先引入插件
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
    //...
    plugins: [
        //数组 放着所有的webpack插件
        new HtmlWebpackPlugin({
            template: './public/index.html',
            filename: 'index.html', //打包后的文件名
            minify: {
                removeAttributeQuotes: false, //是否删除属性的双引号
                collapseWhitespace: false, //是否折叠空白
            },
            // hash: true //是否加上hash,默认是 false
        })
    ]
}

此时执行 npx webpack,可以看到 dist 目录下新增了 index.html 文件,并且其中自动插入了 <script> 脚本,引入的是我们打包之后的 js 文件。

这里要多说一点点东西,HtmlWebpackPlugin 还为我们提供了一个 config 的配置,这个配置可以说是非常有用了。

html-webpack-plugin 的 config 的妙用

有时候,我们的脚手架不仅仅给自己使用,也许还提供给其它业务使用,html 文件的可配置性可能很重要,比如:你公司有专门的部门提供M页的公共头部/公共尾部,埋点jssdk以及分享的jssdk等等,但是不是每个业务都需要这些内容。

一个功能可能对应多个 js 或者是 css 文件,如果每次都是业务自行修改 public/index.html 文件,也挺麻烦的。首先他们得搞清楚每个功能需要引入的文件,然后才能对 index.html 进行修改。

此时我们可以增加一个配置文件,业务通过设置 truefalse 来选出自己需要的功能,我们再根据配置文件的内容,为每个业务生成相应的 html 文件,岂不是美美的。

Let's Go!

首先,我们在 public 目录下新增一个 config.js ( 文件名你喜欢叫什么就叫什么 ),将其内容设置为:

//public/config.js 除了以下的配置之外,这里面还可以有许多其他配置,例如,pulicPath 的路径等等
module.exports = {
    dev: {
        template: {
            title: '你好',
            header: false,
            footer: false
        }
    },
    build: {
        template: {
            title: '你好才怪',
            header: true,
            footer: false
        }
    }
}

现在,我们修改下我们的 webpack.config.js:

//webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');
const isDev = process.env.NODE_ENV === 'development';
const config = require('./public/config')[isDev ? 'dev' : 'build'];

modue.exports = {
    //...
    mode: isDev ? 'development' : 'production'
    plugins: [
        new HtmlWebpackPlugin({
            template: './public/index.html',
            filename: 'index.html', //打包后的文件名
            config: config.template
        })
    ]
}

相应的,我们需要修改下我们的 public/index.html 文件(嵌入的js和css并不存在,仅作为示意):

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <% if(htmlWebpackPlugin.options.config.header) { %>
    <link rel="stylesheet" type="text/css" href="//common/css/header.css">
    <% } %>
    <title><%= (htmlWebpackPlugin.options.config.title) %></title>
</head>

<body>
</body> 
<% if(htmlWebpackPlugin.options.config.header) { %>
<script data-original="//common/header.min.js" type="text/javascript"></script> 
<% } %>
</html>

process.env 中默认并没有 NODE_ENV,这里配置下我们的 package.jsonscripts.

为了兼容Windows和Mac,我们先安装一下 cross-env:

npm install cross-env -D
{
    "scripts": {
        "dev": "cross-env NODE_ENV=development webpack",
        "build": "cross-env NODE_ENV=production webpack"
    }
}

然后我们运行 npm run dev 和 运行 npm run build ,对比下 dist/index.html ,可以看到 npm run build,生成的 index.html 文件中引入了对应的 cssjs。并且对应的 title 内容也不一样。

你说这里是不是非得是用 NODE_ENV 去判断?当然不是咯,你写 aaa=1aaa=2 都行(当然啦,webpack.config.jsscripts 都需要进行相应修改),但是可能会被后面接手的人打死。

更多html-webpack-plugin配置项

如何在浏览器中实时展示效果

说了这么多,到现在还没能在浏览器中实时查看效果,是不是已经有点捉急了,先看一下如何实时查看效果吧,不然都不知道自己配得对不对。

话不多说,先装依赖:

npm install webpack-dev-server -D

修改下咱们的 package.json 文件的 scripts

"scripts": {
    "dev": "NODE_ENV=development webpack-dev-server",
    "build": "NODE_ENV=production webpack"
},

在控制台执行 npm run dev,启动正常,页面上啥也没有,修改下我们的JS代码,往页面中增加点内容,正常刷新(也就是说不需要进行任何配置就可以使用了)。

Excuse me。怪我平时不认真咯,每次都乖乖的配个 contentBase,原来根本不需要配,带着疑问,我又去搜寻了一番。

原来在配置了 html-webpack-plugin 的情况下, contentBase 不会起任何作用,也就是说我以前都是白配了,这是一个悲伤的故事。

不过呢,我们还是可以在 webpack.config.js 中进行 webpack-dev-server 的其它配置,例如指定端口号,设置浏览器控制台消息,是否压缩等等:

//webpack.config.js
module.exports = {
    //...
    devServer: {
        port: '3000', //默认是8080
        quiet: false, //默认不启用
        inline: true, //默认开启 inline 模式,如果设置为false,开启 iframe 模式
        stats: "errors-only", //终端仅打印 error
        overlay: false, //默认不启用
        clientLogLevel: "silent", //日志等级
        compress: true //是否启用 gzip 压缩
    }
}
  • 启用 quiet 后,除了初始启动信息之外的任何内容都不会被打印到控制台。这也意味着来自 webpack 的错误或警告在控制台不可见 ———— 我是不会开启这个的,看不到错误日志,还搞个锤子
  • stats: "errors-only" , 终端中仅打印出 error,注意当启用了 quiet 或者是 noInfo 时,此属性不起作用。 ————— 这个属性个人觉得很有用,尤其是我们启用了 eslint 或者使用 TS进行开发的时候,太多的编译信息在终端中,会干扰到我们。
  • 启用 overlay 后,当编译出错时,会在浏览器窗口全屏输出错误,默认是关闭的。

  • clientLogLevel: 当使用内联模式时,在浏览器的控制台将显示消息,如:在重新加载之前,在一个错误之前,或者模块热替换启用时。如果你不喜欢看这些信息,可以将其设置为 silent (none 即将被移除)。

本篇文章不是为了细说 webpack-dev-server 的配置,所以这里就不多说了。关于 webpack-dev-server 更多的配置可以点击查看

细心的小伙伴可能发现了一个小问题,我们在src/index.js中增加一句 console.log('aaa')

class Animal {
    constructor(name) {
        this.name = name;
    }
    getName() {
        return this.name;
    }
}

const dog = new Animal('dog');
console.log('aaa');

然后通过 npm run dev 查看效果,会发现:

这显然不是我们源码中对应的行号,点进去的话,会发现代码是被编译后的,我当前的代码非常简单,还能看出来,项目代码复杂后,“亲妈”看编译后都费劲,这不利于我们开发调试,不是我们想要的,我们肯定还是希望能够直接对应到源码的。

7.devtool

devtool 中的一些设置,可以帮助我们将编译后的代码映射回原始源代码。不同的值会明显影响到构建和重新构建的速度。

对我而言,能够定位到源码的行即可,因此,综合构建速度,在开发模式下,我设置的 devtool 的值是 cheap-module-eval-source-map

//webpack.config.js
module.exports = {
    devtool: 'cheap-module-eval-source-map' //开发环境下使用
}

生产环境可以使用 none 或者是 source-map,使用 source-map 最终会单独打包出一个 .map 文件,我们可以根据报错信息和此 map 文件,进行错误解析,定位到源代码。

source-maphidden-source-map 都会打包生成单独的 .map 文件,区别在于,source-map 会在打包出的js文件中增加一个引用注释,以便开发工具知道在哪里可以找到它。hidden-source-map 则不会在打包的js中增加引用注释。

但是我们一般不会直接将 .map 文件部署到CDN,因为会直接映射到源码,更希望将.map 文件传到错误解析系统,然后根据上报的错误信息,直接解析到出错的源码位置。

不过报错信息中只有行号,而没有列号。如果有行列号,那么可以通过sourcemap 来解析出错位置。只有行号,根本无法解析,不知道大家的生产环境是如何做的?怎么上报错误信息至错误解析系统进行解析。如有好的方案,请赐教。

还可以设置其他的devtool值,你可以使用不同的值,构建对比差异。

现在我们已经说了 htmljs 了,并且也可以在浏览器中实时看到效果了,现在就不得不说页面开发三巨头之一的 css

8.如何处理样式文件呢

webpack 不能直接处理 css,需要借助 loader。如果是 .css,我们需要的 loader 通常有: style-loadercss-loader,考虑到兼容性问题,还需要 postcss-loader,而如果是 less 或者是 sass 的话,还需要 less-loadersass-loader,这里配置一下 lesscss 文件(sass 的话,使用 sass-loader即可):

先安装一下需要使用的依赖:

npm install style-loader less-loader css-loader postcss-loader autoprefixer less -D
//webpack.config.js
module.exports = {
    //...
    module: {
        rules: [
            {
                test: /\.(le|c)ss$/,
                use: ['style-loader', 'css-loader', {
                    loader: 'postcss-loader',
                    options: {
                        plugins: function () {
                            return [
                                require('autoprefixer')({
                                    "overrideBrowserslist": [
                                        ">0.25%",
                                        "not dead"
                                    ]
                                })
                            ]
                        }
                    }
                }, 'less-loader'],
                exclude: /node_modules/
            }
        ]
    }
}

测试一下,新建一个 less 文件,src/index.less:

//src/index.less
@color: red;
body{
    background: @color;
    transition: all 2s;
}

再在入口文件中引入此 less:

//src/index.js
import './index.less';

我们修改了配置文件,重新启动一下服务: npm run dev。可以看到页面的背景色变成了红色。

OK,我们简单说一下上面的配置:

  • style-loader 动态创建 style 标签,将 css 插入到 head 中.
  • css-loader 负责处理 @import 等语句。
  • postcss-loaderautoprefixer,自动生成浏览器兼容性前缀 —— 2020了,应该没人去自己徒手去写浏览器前缀了吧
  • less-loader 负责处理编译 .less 文件,将其转为 css

这里,我们之间在 webpack.config.js 写了 autoprefixer 需要兼容的浏览器,仅是为了方便展示。推荐大家在根目录下创建 .browserslistrc,将对应的规则写在此文件中,除了 autoprefixer 使用外,@babel/preset-envstylelinteslint-plugin-conmpat 等都可以共用。

注意:

loader 的执行顺序是从右向左执行的,也就是后面的 loader 先执行,上面 loader 的执行顺序为: less-loader ---> postcss-loader ---> css-loader ---> style-loader

当然,loader 其实还有一个参数,可以修改优先级,enforce 参数,其值可以为: pre(优先执行) 或 post (滞后执行)。

现在,我们已经可以处理 .less 文件啦,.css 文件只需要修改匹配规则,删除 less-loader 即可。

现在的一切看起来都很完美,但是假设我们的文件中使用了本地的图片,例如:

body{
    backgroud: url('../images/thor.png');
}

你就会发现,报错啦啦啦,那么我们要怎么处理图片或是本地的一些其它资源文件呢。不用想,肯定又需要 loader 出马了。

9.图片/字体文件处理

我们可以使用 url-loader 或者 file-loader 来处理本地的资源文件。url-loaderfile-loader 的功能类似,但是 url-loader 可以指定在文件大小小于指定的限制时,返回 DataURL,因此,个人会优先选择使用 url-loader

首先安装依赖:

npm install url-loader -D

安装 url-loader 的时候,控制台会提示你,还需要安装下 file-loader,听人家的话安装下就行(新版 npm 不会自动安装 peerDependencies):

npm install file-loader -D

webpack.config.js 中进行配置:

//webpack.config.js
module.exports = {
    //...
    modules: {
        rules: [
            {
                test: /\.(png|jpg|gif|jpeg|webp|svg|eot|ttf|woff|woff2)$/,
                use: [
                    {
                        loader: 'url-loader',
                        options: {
                            limit: 10240, //10K
                            esModule: false 
                        }
                    }
                ],
                exclude: /node_modules/
            }
        ]
    }
}

此处设置 limit 的值大小为 10240,即资源大小小于 10K 时,将资源转换为 base64,超过 10K,将图片拷贝到 dist 目录。esModule 设置为 false,否则,<img data-original={require('XXX.jpg')} /> 会出现 <img data-original=[Module Object] />

将资源转换为 base64 可以减少网络请求次数,但是 base64 数据较大,如果太多的资源是 base64,会导致加载变慢,因此设置 limit 值时,需要二者兼顾。

默认情况下,生成的文件的文件名就是文件内容的 MD5 哈希值并会保留所引用资源的原始扩展名,例如我上面的图片(thor.jpeg)对应的文件名如下:

当然,你也可以通过 options 参数进行修改。

//....
use: [
    {
        loader: 'url-loader',
        options: {
            limit: 10240, //10K
            esModule: false,
            name: '[name]_[hash:6].[ext]'
        }
    }
]

重新编译,在浏览器中审查元素,可以看到图片名变成了: thor_a5f7c0.jpeg

当本地资源较多时,我们有时会希望它们能打包在一个文件夹下,这也很简单,我们只需要在 url-loaderoptions 中指定 outpath,如: outputPath: 'assets',构建出的目录如下:

更多的 url-loader 配置可以查看

到了这里,有点岁月静好的感觉了。

不过还没完,如果你在 public/index.html 文件中,使用本地的图片,例如,我们修改一下 public/index.html

<img data-original="./a.jpg" />

重启本地服务,虽然,控制台不会报错,但是你会发现,浏览器中根本加载不出这张图片,Why?因为构建之后,通过相对路径压根找不着这张图片呀。

How?怎么解决呢?

10.处理 html 中的本地图片

安装 html-withimg-loader 来解决咯。

npm install html-withimg-loader -D

修改 webpack.config.js

module.exports = {
    //...
    module: {
        rules: [
            {
                test: /.html$/,
                use: 'html-withimg-loader'
            }
        ]
    }
}

然后在我们的 html 中引入一张文件测试一下(图片地址自己写咯,这里只是示意):

<!-- index.html -->
<img data-original="./thor.jpeg" />

重启本地服务,图片并没能加载,审查元素的话,会发现图片的地址显示的是 {"default":"assets/thor_a5f7c0.jpeg"}

我当前 file-loader 的版本是 5.0.2,5版本之后,需要增加 esModule 属性:

//webpack.config.js
module.exports = {
    //...
    modules: {
        rules: [
            {
                test: /\.(png|jpg|gif|jpeg|webp|svg|eot|ttf|woff|woff2)$/,
                use: [
                    {
                        loader: 'url-loader',
                        options: {
                            limit: 10240, //10K
                            esModule: false
                        }
                    }
                ]
            }
        ]
    }
}

再重启本地服务,就搞定啦。

话说使用 html-withimg-loader 处理图片之后,html 中就不能使用 vm, ejs 的模板了,如果想继续在 html 中使用 <% if(htmlWebpackPlugin.options.config.header) { %> 这样的语法,但是呢,又希望能使用本地图片,可不可以?鱼和熊掌都想要,虽然很多时候,能吃个鱼就不错了,但是这里是可以的哦,像下面这样编写图片的地址就可以啦。

<!-- index.html -->
<img data-original="<%= require('./thor.jpeg') %>" />

图片加载OK啦,并且 <% %> 语法也可以正常使用,吼吼吼~~~

虽然,webpack 的默认配置很好用,但是有的时候,我们会有一些其它需要啦,例如,我们不止一个入口文件,这时候,该怎么办呢?

11.入口配置

入口的字段为: entry

//webpack.config.js
module.exports = {
    entry: './src/index.js' //webpack的默认配置
}

entry 的值可以是一个字符串,一个数组或是一个对象。

字符串的情况无需多说,就是以对应的文件为入口。

为数组时,表示有“多个主入口”,想要多个依赖文件一起注入时,会这样配置。例如:

entry: [
    './src/polyfills.js',
    './src/index.js'
]

polyfills.js 文件中可能只是简单的引入了一些 polyfill,例如 babel-polyfillwhatwg-fetch 等,需要在最前面被引入(我在 webpack2 时这样配置过)。

那什么时候是对象呢?不要捉急,后面将多页配置的时候,会说到。

12.出口配置

配置 output 选项可以控制 webpack 如何输出编译文件。

const path = require('path');
module.exports = {
    entry: './src/index.js',
    output: {
        path: path.resolve(__dirname, 'dist'), //必须是绝对路径
        filename: 'bundle.js',
        publicPath: '/' //通常是CDN地址
    }
}

例如,你最终编译出来的代码部署在 CDN 上,资源的地址为: 'https://AAA/BBB/YourProject/XXX',那么可以将生产的 publicPath 配置为: //AAA/BBB/

编译时,可以不配置,或者配置为 /。可以在我们之前提及的 config.js 中指定 publicPathconfig.js 中区分了 devpublic), 当然还可以区分不同的环境指定配置文件来设置,或者是根据 isDev 字段来设置。

除此之外呢,考虑到CDN缓存的问题,我们一般会给文件名加上 hash.

//webpack.config.js
module.exports = {
    output: {
        path: path.resolve(__dirname, 'dist'), //必须是绝对路径
        filename: 'bundle.[hash].js',
        publicPath: '/' //通常是CDN地址
    }
}

如果你觉得 hash 串太长的话,还可以指定长度,例如 bundle.[hash:6].js。使用 npm run build 打包看看吧。

问题出现啦,每次文件修改后,重新打包,导致 dist 目录下的文件越来越多。要是每次打包前,都先清空一下目录就好啦。可不可以做到呢?必须可以!

13.每次打包前清空dist目录

反正我是懒得手动去清理的,只要你足够懒,你总是会找到好办法的,懒人推动科技进步。这里,我们需要插件: clean-webpack-plugin

安装依赖:

npm install clean-webpack-plugin -D

以前,clean-webpack-plugin 是默认导出的,现在不是,所以引用的时候,需要注意一下。另外,现在构造函数接受的参数是一个对象,可缺省。

//webpack.config.js
const { CleanWebpackPlugin } = require('clean-webpack-plugin');

module.exports = {
    //...
    plugins: [
        //不需要传参数喔,它可以找到 outputPath
        new CleanWebpackPlugin() 
    ]
}

现在你再修改文件,重现构建,生成的hash值和之前dist中的不一样,但是因为每次 clean-webpack-plugin 都会帮我们先清空一波 dist 目录,所以不会出现太多文件,傻傻分不清楚究竟哪个是新生成文件的情况。

希望dist目录下某个文件夹不被清空

不过呢,有些时候,我们并不希望整个 dist 目录都被清空,比如,我们不希望,每次打包的时候,都删除 dll 目录,以及 dll 目录下的文件或子目录,该怎么办呢?

clean-webpack-plugin 为我们提供了参数 cleanOnceBeforeBuildPatterns

//webpack.config.js
module.exports = {
    //...
    plugins: [
        new CleanWebpackPlugin({
            cleanOnceBeforeBuildPatterns:['**/*', '!dll', '!dll/**'] //不删除dll目录下的文件
        })
    ]
}

此外,clean-webpack-plugin 还有一些其它的配置,不过我使用的不多,大家可以查看clean-webpack-plugin

至此,我们算是完成了一个基础配置。但是这不够完美,或者说有些时候,我们还会有一些其它的需求。下一篇关于webpack配置的文章会介绍一些其它的情况。

最后

如果本文对你有帮助的话,给本文点个赞吧。

参考资料
查看原文

赞 120 收藏 99 评论 3

认证与成就

  • 获得 2455 次点赞
  • 获得 12 枚徽章 获得 0 枚金徽章, 获得 1 枚银徽章, 获得 11 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

注册于 2019-02-15
个人主页被 15.9k 人浏览