<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ Willie.ji - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ freeCodeCamp 是一个免费学习编程的开发者社区，涵盖 Python、HTML、CSS、React、Vue、BootStrap、JSON 教程等，还有活跃的技术论坛和丰富的社区活动，在你学习编程和找工作时为你提供建议和帮助。 ]]>
        </description>
        <link>https://www.freecodecamp.org/chinese/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ Willie.ji - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/chinese/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Sun, 24 May 2026 19:37:54 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/chinese/news/author/willie/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ Vue 3 自定义指令开发 ]]>
                </title>
                <description>
                    <![CDATA[ 什么是指令（directive） 在Angular和Vue中都有Directive的概念，我们通常将 Directive 翻译为“指令”。在计算机技术中，指令是由指令集架构定义的单个的CPU操作。在更广泛的意义上，“指令”可以是任何可执行程序的元素的表述，例如字节码。 那么，前端框架Vue中的“指令”到底是什么，它有什么作用呢？ 在Vue开发中我们在模板中经常会使用 v-model 和 v-show  等以v-开头的关键字，这些关键字就是Vue框架内置的指令。通过使用v-model，可以获取实现DOM和数据的绑定；使用 v-show ，可以控制DOM元素显示。简而言之通过使用这些模板上的标签，让框架对DOM元素进行了指定的处理，同时DOM改变后框架可以同时更新指定数据。指令是Vue MVVM的基础之一。 指令的使用场景 除了使用内置的指令，Vue同样支持自定义指令，以下场景可以考虑通过自定义指令实现： DOM的基础操作，当组件中的一些处理无法用现有指令实现，可以自定义指令实现。例如组件水印，自动focus。相对于用ref获取DOM操作，封装指令更加符合MVVM的架构，M和V不直 ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/vue3-custom-directive-development/</link>
                <guid isPermaLink="false">601231755f61e30501b5c2ca</guid>
                
                    <category>
                        <![CDATA[ Vue ]]>
                    </category>
                
                    <category>
                        <![CDATA[ JavaScript ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Web开发 ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Willie.ji ]]>
                </dc:creator>
                <pubDate>Thu, 28 Jan 2021 03:00:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2021/01/danielle-macinnes-IuLgi9PWETU-unsplash-1.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <h3 id="-directive-">什么是指令（directive）</h3><p>在Angular和Vue中都有Directive的概念，我们通常将 Directive 翻译为“指令”。在计算机技术中，指令是由指令集架构定义的单个的CPU操作。在更广泛的意义上，“指令”可以是任何可执行程序的元素的表述，例如字节码。</p><p>那么，前端框架Vue中的“指令”到底是什么，它有什么作用呢？</p><p>在Vue开发中我们在模板中经常会使用 <code>v-model</code> 和 <code>v-show</code> 等以v-开头的关键字，这些关键字就是Vue框架内置的指令。通过使用v-model，可以获取实现DOM和数据的绑定；使用 <code>v-show</code>，可以控制DOM元素显示。简而言之通过使用这些模板上的标签，让框架对DOM元素进行了指定的处理，同时DOM改变后框架可以同时更新指定数据。指令是Vue MVVM的基础之一。</p><h3 id="-">指令的使用场景</h3><p>除了使用内置的指令，Vue同样支持自定义指令，以下场景可以考虑通过自定义指令实现：</p><p>DOM的基础操作，当组件中的一些处理无法用现有指令实现，可以自定义指令实现。例如组件水印，自动focus。相对于用ref获取DOM操作，封装指令更加符合MVVM的架构，M和V不直接交互。</p><p><code>&lt;pv-highlight="'yellow'"&gt;Highlight this text bright yellow&lt;/p&gt;</code></p><p>多组件可用的通用操作，通过使用组件（Component）可以很好的实现复用，同样通过使用组件也可以实现功能在组件上的复用。例如拼写检查、图片懒加载。使用组件，只要在需要拼写检查的输入组件上加上标签，遍可为组件注入拼写检查的功能，无需再针对不同组件封装新的支持拼写功能呢。</p><h3 id="vue-3-">Vue 3如何自定义指令</h3><p>Vue支持全局注册和局部注册指令。</p><p>全局注册注册通过app实例的 <code>directive</code> 方法进行注册：</p><pre><code>let app = createApp(App)
app.directive('highlight', {
beforeMount(el, binding, vnode) {
el.style.background = binding.value
}
})
</code></pre><p>局部注册通过给组件设置 <code>directive</code> 属性注册：</p><pre><code>export default defineComponent({
name: "WebDesigner",
components: {
Designer,
},
directives: {
highlight: {
beforeMount(el, binding, vnode) {
el.style.background = binding.value;
},
},
},
});
</code></pre><p>注册组件包含组件的名字，需要唯一和组件的一个实现对象，组册后即可在任何元素上使用了。</p><pre><code>&lt;p v-highlight="'yellow'"&gt;Highlight this text bright yellow&lt;/p&gt;
</code></pre><p>自定义组件就是实现Vue提供的钩子函数，在Vue 3中钩子函数的生命周期和组件的生命周期类似：</p><ul><li>created - 元素创建后，但是属性和事件还没有生效时调用</li><li>beforeMount- 仅调用一次，当指令第一次绑定元素的时候</li><li>mounted- 元素被插入父元素时调用</li><li>beforeUpdate: 在元素自己更新之前调用</li><li>Updated - 元素或者子元素更新之后调用</li><li>beforeUnmount: 元素卸载前调用</li><li>unmounted -当指令卸载后调用，仅调用一次</li></ul><p>每一个钩子函数都有如下参数：</p><ul><li><code>el</code>：指令绑定的元素，可以用来直接操作DOM</li><li><code>binding</code>：数据对象，包含以下属性</li><li><code>instance</code>：当前组件的实例，一般推荐指令和组件无关，如果有需要使用组件上下文ViewModel，可以从这里获取</li><li><code>value</code>：指令的值，即上面示例中的“yellow“</li><li><code>oldValue</code>：指令的前一个值，在 beforeUpdate 和 Updated 中，可以和 <code>value</code> 是相同的内容</li><li><code>arg</code>：传给指令的参数，例如 <code>v-on:click</code> 中的 <code>click</code></li><li><code>modifiers</code>：包含修饰符的对象。例如 <code>v-on.stop:click</code> 可以获取到一个 <code>{stop:true}</code> 的对象</li><li><code>vnode</code>：Vue 编译生成的虚拟节点</li><li><code>prevVNode</code>：Update时的上一个虚拟节点</li></ul><h3 id="vue-2-">Vue 2 指令升级</h3><p>指令在Vue 3中是一个Breaking Change，指令的钩子函数名称和数量发生了变化。Vue 3中为指令创建了更多的函数，函数名称和组件的生命周期一致，更易理解。</p><p>以下是变化介绍：</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2021/01/139239-20210128092436901-1616522123.png" class="kg-image" alt="139239-20210128092436901-1616522123.png" width="290" height="196" loading="lazy"></figure><p>另一个变化是组件上下文对象的获取方式发生了变化。一般情况下推荐指令和组件实例相互独立，从自定义指令内部去访问组件实例，那可能说明这里不需要封装指令，指令就是组件本事的功能。但是可能的确有某些场景需要去获取组件实例。</p><p>在Vue 2中通过 <code>vnode</code> 参数获取：</p><pre><code>bind(el, binding, vnode) {
  const vm = vnode.context
}
</code></pre><p>在Vue 3中 通过 <code>binding</code> 参数获取：</p><pre><code>mounted(el, binding, vnode) {
  const vm = binding.instance
}
</code></pre><h3 id="vue-3--1">Vue 3 自定义指令实例 – 输入拼写检查</h3><p>这里使用Plugin的方式注入指令。</p><p>新建 <code>SpellCheckPlugin.ts</code>，声明插件，在插件的install方法中注入指令：</p><pre><code>import { App } from 'vue'

function SpellCheckMain(app: App, options: any) {
//
}

export default {
    install: SpellCheckMain
}
</code></pre><p>SpellCheckMain方法实现组件以及拼写检查方法，具体拼写检查规则可以根据业务或者使用其他插件方法实现。</p><pre><code>function SpellCheckMain(app: App, options: any) {
    const SpellCheckAttribute = "spell-check-el";

    let SpellCheckTimer: Map&lt;string, number&gt; = new Map();
    let checkerId = 0;
    function checkElement(el: HTMLElement) {
        let attr = el.getAttribute(SpellCheckAttribute);
        if (attr) {
            clearTimeout(SpellCheckTimer.get(attr));
            let timer = setTimeout(() =&gt; { checkElementAsync(el) }, 500);
            SpellCheckTimer.set(attr, timer)
        }
    }
    function checkText(words?: string | null): \[string?\] {
        if (!words) {
            return \[\];
        }
        let errorWordList: \[string?\] = \[\];
        try {
            let wordsList = words.match(/\[a-zA-Z\]+/ig);
            wordsList?.forEach((word) =&gt; {
                if (!checkWord(word)) {
                    errorWordList.push(word);
                }
            })
        }
        catch {

        }
        return errorWordList;
    }
    function checkWord(text: string) {
        //模拟拼写检查，这里使用其他检查库
        return text.length &gt; 6 ? false : true;
    }
    function checkElementAsync(el: HTMLElement) {

        let text = (el as HTMLInputElement).value || el.innerText;
        let result = checkText(text);

        let attr = el.getAttribute(SpellCheckAttribute);
        if (!attr) {
            return;
        }

        if (result &amp;&amp; result.length) {
            el.style.background = "pink"
            let div = document.getElementById(attr);
            if (!div) {
                div = document.createElement("div");
                div.id = attr;
                div.style.position = "absolute"
                div.style.top = "0px"
                div.style.left = el.clientWidth + "px"

                if (el.parentElement) {
                    el.parentElement.style.position = "relative"
                    if (el.parentElement.lastChild === el) {
                        el.parentElement.appendChild(div);
                    }
                    else {
                        el.parentElement.insertBefore(div, el.nextSibling);
                    }
                }
            }
            div.innerHTML = result.length.toString() + " - " + result.join(",");
        } else {
            el.style.background = "";

            let div = document.getElementById(attr);
            if (div) {
                div.innerHTML = ""
            }
        }

        console.log(result)
    }

    app.directive('spell-check', {
        created() {
            console.log("created", arguments)
        },
        mounted: function (el, binding, vnode, oldVnode) {

            console.log("mounted", arguments)
            //set checker id for parent
            let attr = "spellcheck-" + (checkerId++);
            el.setAttribute(SpellCheckAttribute, attr);
            console.log("attr", attr)

            if (el.tagName.toUpperCase() === "DIV") {
                el.addEventListener("blur", function () {
                    checkElement(el)
                }, false);
            }
            if (el.tagName.toUpperCase() === "INPUT") {
                el.addEventListener("keyup", function () {
                    checkElement(el)
                }, false);
            }
            // el.addEventListener("focus", function () {
            //     checkElement(el)
            // }, false);
        },
        updated: function (el) {
            console.log("componentUpdated", arguments)
            checkElement(el);
        },
        unmounted: function (el) {
            console.log("unmounted", arguments)

            let attr = el.getAttribute(SpellCheckAttribute);
            if (attr) {
                let div = document.getElementById(attr);
                if (div) {
                    div.remove();
                }
            }
        }
    })
}
</code></pre><p><code>main.ts</code>中使用插件：</p><pre><code>/// &lt;reference path="./vue-app.d.ts" /&gt;
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import SpellCheckPlugin  from './plugins/SpellCheckPlugin'

let app = createApp(App)
app.use(SpellCheckPlugin)
app.use(router).mount('#app')
</code></pre><p>组件中直接使用指令即可：</p><pre><code>&lt;template&gt;
  &lt;div ref="ssHost" style="width: 100%; height: 600px"&gt;&lt;/div&gt;
  &lt;div&gt;&lt;div ref="fbHost" spell-check v-spell-check="true" contenteditable="true" spellcheck="false" style="border: 1px solid #808080;width:600px;"&gt;&lt;/div&gt;&lt;/div&gt;
  &lt;div&gt;&lt;input v-model="value1" v-spell-check spellcheck="false" style="width:200px;" /&gt;&lt;/div&gt;
&lt;/template&gt;
</code></pre><p>结合在使用 <a href="https://www.grapecity.com.cn/developer/spreadjs">SpreadJS</a> ，基于检查用户拼写输入的功能，效果如下图：</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2021/01/139239-20210128092646036-1691661649.png" class="kg-image" alt="139239-20210128092646036-1691661649.png" width="553" height="301" loading="lazy"></figure><p>以上就是Vue 3 自定义指令开发的部分玩法介绍。大家如果知道更多的使用方法，欢迎通过留言分享出来。</p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ VUE 项目性能优化实践——通过懒加载提升页面响应速度 ]]>
                </title>
                <description>
                    <![CDATA[ 最近我司因业务需求，需要在一个内部数据分析平台集成在线 Excel 功能，既然我们自己就是做开发工具的，所以目光自然就落在了我司自研的前端表格产品上。 项目的目的是要通过数据透视表和 Excel 公式来分析公司的各项运营数据。不过在集成后，在开发环境页面运行流畅，大量数据加载处理也很快。但是发布生产后，在用户每次打开页面时，加载时间上相较开发阶段均有所降低，经过排查速度变慢是由于发布包的 vendor.js 变大所导致的，这个文件加载每次都需 300 毫秒左右，由于小的 Vue 项目并没有做模块划分，所以所有的代码都直接打包到了 vendor 中，在集成了新功能后，发布包也随之变大了。 既然找到了原因，我们就开始着手优化，在前端对于需加载较大资源时，我们一般都采用懒加载的方式来优化效率。 什么是懒加载？ 懒加载也叫做延时加载，在网页响应时不立刻请求资源，待页面加载完毕或者按需响应时再加载资源，以达到提高页面响应速度以及节省服务器资源的目的。网页中常用的懒加载是图片的懒加载，对于类似淘宝一样的多图页面，如果等待所有图片都下载完成再响应用不必然造成页面加载的卡顿。对于 JS 资源的加 ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/vue-project-performance-optimization/</link>
                <guid isPermaLink="false">5ff7bc5d39641a0517d5355b</guid>
                
                    <category>
                        <![CDATA[ Vue ]]>
                    </category>
                
                    <category>
                        <![CDATA[ 网站性能 ]]>
                    </category>
                
                    <category>
                        <![CDATA[ JavaScript ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Willie.ji ]]>
                </dc:creator>
                <pubDate>Fri, 08 Jan 2021 02:30:39 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2021/01/christian-wiediger-i2cwRt3WxZk-unsplash.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>最近我司因业务需求，需要在一个内部数据分析平台集成在线 Excel 功能，既然我们自己就是做开发工具的，所以目光自然就落在了我司自研的前端表格产品上。</p><p>项目的目的是要通过数据透视表和 Excel 公式来分析公司的各项运营数据。不过在集成后，在开发环境页面运行流畅，大量数据加载处理也很快。但是发布生产后，在用户每次打开页面时，加载时间上相较开发阶段均有所降低，经过排查速度变慢是由于发布包的 vendor.js 变大所导致的，这个文件加载每次都需 300 毫秒左右，由于小的 Vue 项目并没有做模块划分，所以所有的代码都直接打包到了 vendor 中，在集成了新功能后，发布包也随之变大了。</p><p>既然找到了原因，我们就开始着手优化，在前端对于需加载较大资源时，我们一般都采用懒加载的方式来优化效率。</p><h3 id="-">什么是懒加载？</h3><p>懒加载也叫做延时加载，在网页响应时不立刻请求资源，待页面加载完毕或者按需响应时再加载资源，以达到提高页面响应速度以及节省服务器资源的目的。网页中常用的懒加载是图片的懒加载，对于类似淘宝一样的多图页面，如果等待所有图片都下载完成再响应用不必然造成页面加载的卡顿。对于 JS 资源的加载也是同样的道理，大 JS 的加载会造成 JS 阻塞，页面出现停止响应的假死状态。因此可以通过按需加载的方式，提高页面首屏的加载速度。</p><p>总结了具体优化步骤，下面我们就开始着手优化吧！</p><h3 id="--1">开始优化</h3><p>首先是项目环境：Vue 2.6</p><p>开发环境：Vue-cli 4.5 + TypeScript 3.9</p><h4 id="--2">划分业务模块</h4><p>通过路由异步加载模块，加速首屏以及其他页面加载速度，在 Vue Router 中将 webExcel 模块配置为懒加载模式，配置后 webExcel 组件会按照指定的 webpackChunkName 打包为单独的文件，并在访问 webExcel 路由的时候才会加载。这样访问 home 以及 about 页面时并不会加载 webExcel 资源。</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://www.grapecity.com.cn/media/7769bc712f7c9f3c9602e7e5e4ee2f71.png" class="kg-image" alt="7769bc712f7c9f3c9602e7e5e4ee2f71" width="600" height="400" loading="lazy"><figcaption>懒加载路由配置</figcaption></figure><p>打包发布访问页面，首屏秒开，直接访问 about 依旧秒开。可是查看网络请求时候发现访问首页时请求了 about 和 web Excel 的资源。经过排查发现 vue-cli 在页面中使用了 preload 和 prefetch 预加载机制，在不影响当前页面加载的情况下预加载后续页面需要的资源提升用户体验，这里为了演示清晰注释掉 prefetch 的资源。</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://www.grapecity.com.cn/media/3a184f6ce9d11012e592f4884e00397c.png" class="kg-image" alt="3a184f6ce9d11012e592f4884e00397c" width="600" height="400" loading="lazy"><figcaption>临时禁用 prefetch 预加载</figcaption></figure><p>开启路由懒加载后首页并未加载 about 和 webExcel。</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://www.grapecity.com.cn/media/f946be5cb70316aabefb2310a17d3589.png" class="kg-image" alt="f946be5cb70316aabefb2310a17d3589" width="600" height="400" loading="lazy"><figcaption>首页 Home 网络请求</figcaption></figure><p>清理网络请求记录，点击 Web Excel，访问 webExcel 页面，此时会请求 webExcel 资源并展示组件。</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://www.grapecity.com.cn/media/19b62983ec140f06d0827e26c9fe54ae.png" class="kg-image" alt="19b62983ec140f06d0827e26c9fe54ae" width="600" height="400" loading="lazy"><figcaption>webExcel 页面网络请求</figcaption></figure><h4 id="-excel-">在线 Excel 组件懒加载，进一步优化使用插件页面打开速度</h4><p>优化了路由加载，为了提升用户体验，进一步优化 webExcel 页面，开启组件懒加载，当需要 Designer 组件的时候再加载。</p><p>开启异步组件的方式类似于路由，直接配置 import 组件即可，替换原有的静态 import。</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://www.grapecity.com.cn/media/5bc981a7268fb5d0bae45ff30d640441.png" class="kg-image" alt="5bc981a7268fb5d0bae45ff30d640441" width="600" height="400" loading="lazy"><figcaption>组件懒加载</figcaption></figure><p>这里我们一步到位使用 AsyncComponent 配置，这样在加载组件（loading）时候可以给用户一个提示。</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://www.grapecity.com.cn/media/62447b5d29ee6c610fa7bba203e6812f.png" class="kg-image" alt="62447b5d29ee6c610fa7bba203e6812f" width="600" height="400" loading="lazy"><figcaption>页面模板</figcaption></figure><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://www.grapecity.com.cn/media/519dcfd206fe018ce8088bf54072348f.png" class="kg-image" alt="519dcfd206fe018ce8088bf54072348f" width="600" height="400" loading="lazy"><figcaption>异步组件懒加载</figcaption></figure><p>页面上通过 displayDesigner 属性控制 Designer 组件是否显示，setTimeout 3 秒后开始加载 Designer 组件并展示。LoadingComponent 在加载时展示 loading 状态。</p><figure class="kg-card kg-image-card"><img src="https://www.grapecity.com.cn/media/3839dc30d4088ce56706e5f1a586978d.png" class="kg-image" alt="3839dc30d4088ce56706e5f1a586978d" width="600" height="400" loading="lazy"></figure><figure class="kg-card kg-image-card"><img src="https://www.grapecity.com.cn/media/23560bf77cc357f78b6de1efcaabaf03.png" class="kg-image" alt="23560bf77cc357f78b6de1efcaabaf03" width="600" height="400" loading="lazy"></figure><p>可以从网络请求中看到，webExcel 加载完 3 秒后开始请求 designer 资源，请求时显示 LoadingComponent，请求完毕展示 Desinger 组件。</p><h4 id="-gzip-">开启 gzip 压缩，加速资源请求速度</h4><p>为了进一步加速资源请求，可以开启服务器 gizp 压缩，目前大部分浏览器都支持 gzip，可以开启服务器的 gzip 功能，服务器在传输资源之前先进行压缩。</p><p>网络请求 Request 中会出现如下内容，就表示支持 gzip：</p><p><code>Accept-Encoding: gzip, deflate, br</code></p><p>Vue-cli 可以在打包时就将资源提前进行 gzip 打包，这样服务器直接返回打包后的资源不需要再次打包了。通过使用 compression-webpack-plugin 插件可以在打包时直接生成 gz 压缩文件。关于 gzip 的配置可以根据具体部署情况设置。</p><h3 id="--3">总结</h3><p>经过以上优化，首屏加载资源仅需 Vue 基础组件和 Home 页面组件，首屏加载速度回复到原始 200 毫秒。其他未使用 Designer 组件的页面也无需加载资源，同时 Designer 组件加载一次，后续其他页面再使用组件也无需再次加载</p><p>以上就是关于 Vue 路由和组件懒加载的一些配置，不过懒加载不是万能的，还是要从实际需求出发决定是否使用。</p><h3 id="--4">扩展阅读</h3><ul><li><a href="https://www.grapecity.com.cn/developer/spreadjs/vue">支持 Vue 的电子表格组件</a></li><li><a href="https://www.grapecity.com.cn/blogs/spreadjs-vue3-component-development-combat-part1">Vue 3 组件开发：搭建在线表格编辑系统</a></li></ul> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ Vue3 组件开发：搭建基于 Vite 的在线表格编辑系统（组件集成） ]]>
                </title>
                <description>
                    <![CDATA[ 通过前文 [https://chinese.freecodecamp.org/news/vue3-component/]的学习，我们已经用 Vite 搭建出了Vue3 的项目原型。今天，我们将基于这个原型，集成 SpreadJS 电子表格组件和在线编辑器组件，使其具备 Excel公式计算、在线导入导出 Excel 文档、数据透视表和可视化分析能力，实现在线表格编辑系统的雏形。 设计思路 · 同时创建SpreadJS 和Designer（表格编辑器）两个组件，用切换路由的方式显示不同组件类型。 · 在编辑器组件的工具栏中增加“加载”和“更新”两个按钮。 · 点击“加载”即可加载从服务器获取的Excel文件，在编辑器中对该组件做一些修改，点击“更新”按钮，将修改后的文件传递给服务器。 · 切换路由显示 SpreadJS 组件，在该组件添加 “加载”和“更新”两个button，功能同上。 SpreadJS 组件介绍 SpreadJS [https://www.grapecity.com.cn/developer/spreadjs]是一款基于 HTML5 的原生JavaScript组 ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/vue3-component-integration/</link>
                <guid isPermaLink="false">5ff517ad39641a0517d53440</guid>
                
                    <category>
                        <![CDATA[ Vue ]]>
                    </category>
                
                    <category>
                        <![CDATA[ JavaScript ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Web开发 ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Willie.ji ]]>
                </dc:creator>
                <pubDate>Wed, 06 Jan 2021 01:58:33 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2021/01/marek-piwnicki-IO6U5eUce6M-unsplash.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>通过<a href="https://chinese.freecodecamp.org/news/vue3-component/">前文</a>的学习，我们已经用 Vite 搭建出了Vue3 的项目原型。今天，我们将基于这个原型，集成 SpreadJS 电子表格组件和在线编辑器组件，使其具备 Excel公式计算、在线导入导出 Excel 文档、数据透视表和可视化分析能力，实现在线表格编辑系统的雏形。</p><h3 id="-">设计思路</h3><p>· 同时创建SpreadJS 和Designer（表格编辑器）两个组件，用切换路由的方式显示不同组件类型。</p><p>· 在编辑器组件的工具栏中增加“加载”和“更新”两个按钮。</p><p>· 点击“加载”即可加载从服务器获取的Excel文件，在编辑器中对该组件做一些修改，点击“更新”按钮，将修改后的文件传递给服务器。</p><p>· 切换路由显示 SpreadJS 组件，在该组件添加 “加载”和“更新”两个button，功能同上。</p><h3 id="spreadjs-">SpreadJS 组件介绍</h3><p><a href="https://www.grapecity.com.cn/developer/spreadjs">SpreadJS</a>是一款基于 HTML5 的原生JavaScript组件，兼容 450 种以上的 Excel 公式，提供高度类似 Excel 的功能，主要用于开发 Web Excel 组件，实现多人协同编辑、高性能模板设计和数据填报等功能模块，组件架构符合UMD规范，可以以原生的方式嵌入各类应用，并与前后端技术框架相结合。</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2021/01/image002.jpg" class="kg-image" alt="image.png" width="600" height="400" loading="lazy"></figure><p>目前，SpreadJS已针对Vue 2做了组件封装，暂时还未对Vue3提供组件封装，不过我们可以通过自己封装SpreadJS组件和表格编辑器的方式，将其集成在Vue3项目中。</p><h3 id="-spreadjs-vue3-">将 SpreadJS 与Vue3 集成</h3><p><strong>1. 安装模块</strong></p><p>修改package.json 文件，添加我们需要的模块，运行命令 npm install 来安装所有依赖项目。</p><pre><code>
"dependencies": {

&nbsp; &nbsp; "@fortawesome/fontawesome-free": "^5.14.0",

&nbsp; &nbsp; "@grapecity/spread-excelio": "^14.0.1",

&nbsp; &nbsp; "@grapecity/spread-sheets": "^14.0.1",

&nbsp; &nbsp; "@grapecity/spread-sheets-barcode": "^14.0.1",

&nbsp; &nbsp; "@grapecity/spread-sheets-charts": "^14.0.1",

&nbsp; &nbsp; "@grapecity/spread-sheets-designer": "^14.0.1",

&nbsp; &nbsp; "@grapecity/spread-sheets-designer-resources-cn": "^14.0.1",

&nbsp; &nbsp; "@grapecity/spread-sheets-designer-vue": "^14.0.1",

&nbsp; &nbsp; "@grapecity/spread-sheets-languagepackages": "^14.0.1",

&nbsp; &nbsp; "@grapecity/spread-sheets-pdf": "^14.0.1",

&nbsp; &nbsp; "@grapecity/spread-sheets-pivot-addon": "^14.0.1",

&nbsp; &nbsp; "@grapecity/spread-sheets-print": "^14.0.1",

&nbsp; &nbsp; "@grapecity/spread-sheets-resources-zh": "^14.0.1",

&nbsp; &nbsp; "@grapecity/spread-sheets-shapes": "^14.0.1",

&nbsp; &nbsp; "@grapecity/spread-sheets-vue": "^14.0.1",

&nbsp; &nbsp; "axios": "^0.21.0",

&nbsp; &nbsp; "vue": "^3.0.2",

&nbsp; &nbsp; "vue-router": "^4.0.0-rc.5"

&nbsp;&nbsp;},

</code></pre><p><strong>2. 配置路由</strong></p><p>在src文件夹下添加3个文件。</p><p>· router/index.js</p><p>· views/SpreadSheet.vue</p><p>· views/Designer.vue</p><p>配置路由：</p><pre><code>
import { createRouter, createWebHistory } from "vue-router";

const routes = [

&nbsp;&nbsp;{

&nbsp; &nbsp; path: "/",

&nbsp; &nbsp; name: "Designer",

&nbsp; &nbsp; component: () =&gt; import("../views/Designer.vue"),

&nbsp;&nbsp;},

&nbsp;&nbsp;{

&nbsp; &nbsp; path: "/spreadSheet",

&nbsp; &nbsp; name: "SpreadSheet",

&nbsp; &nbsp; component: () =&gt; import("../views/SpreadSheet.vue"),

&nbsp;&nbsp;}

];

export const router = createRouter({

&nbsp;&nbsp;history: createWebHistory(),

&nbsp;&nbsp;routes:routes

});

</code></pre><p><strong>3. 在main.js中引入：</strong></p><pre><code>
import { createApp } from 'vue'

import { router } from './router/index'

import App from './App.vue'

import './index.css'

const app = createApp(App)

app.use(router);

app.mount('#app')

</code></pre><p><strong>4. 修改App.vue：</strong></p><p>在main.js文件中，将 Vue Router 文件添加到项目中（在Vue 2 中，导入它使用的是 Vue.use(router) ，但在Vue3中添加方式发生了变化）。如下面的截图所示，Vue3是使用createApp方法来实际创建项目的，在挂载应用程序前，需要通过 &nbsp;app.use(router) &nbsp;来添加到项目中。</p><pre><code>
&lt;template&gt;

&lt;div id="app"&gt;

&nbsp; &nbsp; &lt;div&gt;

&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;&lt;router-link to="/"&gt;Designer&lt;/router-link&gt; |

&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;&lt;router-link to="/spreadSheet"&gt;SpreadSheet&lt;/router-link&gt;

&nbsp; &nbsp; &lt;/div&gt;

&nbsp;&nbsp;&lt;router-view/&gt;

&lt;/div&gt;

&lt;/template&gt;

&lt;script&gt;

export default {

&nbsp;&nbsp;name: 'App',

&nbsp;&nbsp;components: {

&nbsp;&nbsp;},

&nbsp;&nbsp;setup(){

&nbsp;&nbsp;}

}

&lt;/script&gt;

</code></pre><p>看到这里大家应该会发现，Vite中的路由配置以及 main.js 引入的方式较Vue 2有所不同，为了让其更好的支持Typescript，Vue Router的Vue3版本要求我们必须导入新方法才能使代码正常工作，其中最重要的是createRouter 和 createWebHistory。</p><p><strong>5. 集成designer组件</strong></p><p>配置完路由之后，就可以开始集成designer组件了。首先，在components文件夹下添加2个文件：</p><p>· components/Designer.vue</p><p>· components /SpreadSheet.vue</p><p>接着，在 Designer.vue 中集成SpreadJS 表格编辑器，代码如下图所示：</p><p>· 在模板中添加一个div，这个div就是编辑器的容器，可以通过css设置容器的宽高位置等，也就是自定义了编辑器的显示大小及位置。</p><p>· 导入编辑器所需要的依赖。</p><p>· 在setup函数中新建一个编辑器。</p><pre><code>
&lt;template&gt;

&nbsp;&nbsp;&lt;div&gt;

&nbsp; &nbsp;&nbsp; &nbsp;&lt;div ref="ssDesigner" style="height:700px;width:100%;text-align: left;"&gt;&lt;/div&gt;

&nbsp;&nbsp;&lt;/div&gt;

&lt;/template&gt;

&lt;script&gt;

import { onMounted, ref} from "vue";

import "../../node_modules/@grapecity/spread-sheets/styles/gc.spread.sheets.excel2013white.css";

import "../../node_modules/@grapecity/spread-sheets-designer/styles/gc.spread.sheets.designer.min.css";

import "@grapecity/spread-sheets-designer-resources-cn";

import "@grapecity/spread-sheets-designer";

import GC from '@grapecity/spread-sheets'

import ExcelIO from '@grapecity/spread-excelio'

export default {

&nbsp;&nbsp;name: 'Designer',

&nbsp;&nbsp;props: {

&nbsp;&nbsp;},

&nbsp;&nbsp;setup(props, {emit}) {

&nbsp; &nbsp; const ssDesigner = ref(null);

&nbsp; &nbsp; onMounted(() =&gt; {

&nbsp; &nbsp;&nbsp; &nbsp;var designer = new GC.Spread.Sheets.Designer.Designer(ssDesigner.value);

&nbsp; &nbsp;&nbsp; &nbsp;emit("designerInitialized", designer);

&nbsp; &nbsp; });

&nbsp; &nbsp; return {

&nbsp; &nbsp;&nbsp; &nbsp;ssDesigner

&nbsp; &nbsp; };

&nbsp;&nbsp;}

}

&lt;/script&gt;

</code></pre><p>第三步，在views/Designer.vue中引入该组件及相关依赖。</p><pre><code>
import Designer from '../components/Designer.vue'

import {ref} from "vue"

import axios from "axios"

import GC from '@grapecity/spread-sheets'

import ExcelIO from '@grapecity/spread-excelio'

</code></pre><p>第四步，在模板中使用该组件标签。</p><pre><code>
&lt;template&gt;

&nbsp;&nbsp;&lt;div&gt;

&nbsp; &nbsp; &lt;Designer v-on:designerInitialized="designerInitialized"&gt;&lt;/Designer&gt;

&nbsp;&nbsp;&lt;/div&gt;

&lt;/template&gt;

</code></pre><p>最后，在setup函数中初始化编辑器。</p><pre><code>
let designer = undefined;

let designerInitialized=(wb)=&gt;{

&nbsp; &nbsp;&nbsp; &nbsp;designer = wb;

&nbsp; &nbsp;&nbsp; &nbsp;let spread = designer.getWorkbook();

&nbsp; &nbsp; }

</code></pre><p>完成上述步骤，页面就可以显示编辑器UI了。</p><h3 id="--1">如何自定义编辑器的工具栏？</h3><p>完成了上述步骤，我们已经成功的将 SpreadJS编辑器集成到项目中，接下来演示如何在工具栏中新建 “加载”和“更新”两个按钮。</p><p>由于 SpreadJS &nbsp;在线表格编辑器采用了全新可配置设计，在任何区域都可采取json config 的配置方式。通过修改默认的GC.Spread.Sheets.Designer.DefaultConfig，便可以达到自定制功能。</p><p><strong>1. 定制 Ribbon 选项卡</strong></p><p>在浏览器Console中输入GC.Spread.Sheets.Designer.DefaultConfig可查看默认ribbon选项卡配置。参考默认配置，可以自定义操作选项卡。</p><pre><code>
let DefaultConfig = GC.Spread.Sheets.Designer.DefaultConfig;

let customerRibbon = {

&nbsp; &nbsp;&nbsp; &nbsp;id: "operate",

&nbsp; &nbsp;&nbsp; &nbsp;text: "操作",

&nbsp; &nbsp;&nbsp; &nbsp;buttonGroups: [

&nbsp; &nbsp;&nbsp; &nbsp;],

};

</code></pre><p><strong>2、自定义按钮</strong></p><p>在定义按钮之前，需要先定义按钮点击时的命令Commands，并将命令注册到config的commandMap属性上。</p><pre><code>
let ribbonFileCommands = {

&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;"loadTemplateCommand": {

&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;iconClass: "ribbon-button-download",

&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;text: "加载",

&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;//bigButton: true,

&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;commandName: "loadTemplate",

&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;execute: load

&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;},

&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;"updateTemplateCommand": {

&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;iconClass: "ribbon-button-upload",

&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;text: "更新",

&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;//bigButton: true,

&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;commandName: "updateTemplate",

&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;execute: update

&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;}

&nbsp; &nbsp; }

</code></pre><p>上面的示例代码注册了 loadTemplateCommand和 updateTemplateCommand 两个命令。</p><p>· execute对应具体执行内容的function,也就是 load 和 update 方法。</p><p>· iconClass为按钮样式，可以制定按钮图片</p><p>· text为按钮显示文字</p><p>· commandName为命令名称，需要全局唯一</p><p>iconClass示例代码：</p><pre><code>
.ribbon-button-download {

 background-image: url(图片地址，可以是base64 svg)}；

</code></pre><p>有了命令就可以添加对应button 的config了：</p><pre><code>
let customerRibbon = {

&nbsp; &nbsp;&nbsp; &nbsp;id: "operate",

&nbsp; &nbsp;&nbsp; &nbsp;text: "操作",

&nbsp; &nbsp;&nbsp; &nbsp;buttonGroups: [

&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;{

&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp; label: "文件操作",

&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp; thumbnailClass: "ribbon-thumbnail-spreadsettings",

&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp; commandGroup: {

&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;children: [

&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;{

&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp; direction: "vertical",

&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp; commands: ["loadTemplateCommand", "updateTemplateCommand"],

&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;}

&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;],

&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp; },

&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;},

&nbsp; &nbsp;&nbsp; &nbsp;],

&nbsp; &nbsp; };

</code></pre><p>在designer的config中加入自定义的命令和按钮：</p><pre><code>
DefaultConfig.ribbon.push(customerRibbon);

&nbsp; &nbsp; DefaultConfig.commandMap = {};

&nbsp; &nbsp; Object.assign(DefaultConfig.commandMap, ribbonFileCommands);

</code></pre><p>最后，不要忘了补充Load方法和update方法中的代码。</p><h3 id="load-update-">Load方法和update方法的作用</h3><p>Load方法用于执行excel文件的加载。在接收到后台传递的json数据后，使用fromJSON方法加载该文件，代码如下图：</p><pre><code>
let load = (e)=&gt;{

&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;let spread = designer.getWorkbook();

&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;let formData = new FormData();

&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;formData.append("fileName", "path");

&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;axios.post('spread/loadTemplate', formData, {

&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;responseType: "json",

&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;}).then((response) =&gt; {

&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;if(response) {

&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp; alert("加载成功");

&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp; templateJSON = response.data;

&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp; spread.fromJSON(templateJSON);

&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;}

&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;}).catch((response) =&gt; {

&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;alert("错误");

&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;})

}

</code></pre><p>Update方法用于执行文件的更新。在编辑器对加载的文件做出操作，如修改背景色、添加文本时，使用toJSON方法将当前spread保存为json数据传递给后台存储，代码如下：</p><pre><code>
let update = (e)=&gt;{

&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;let spread = designer.getWorkbook();

&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;let spreadJSON = JSON.stringify(spread.toJSON());

&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;let formData = new FormData();

&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;formData.append("jsonString", spreadJSON);

&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;formData.append("fileName", "fileName");

&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;axios.post('spread/updateTemplate', formData).then((response) =&gt; {

&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;if(response) {

&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp; alert("更新成功");

&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;}

&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;}).catch((response) =&gt; {

&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;alert("错误");

&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;})

&nbsp; &nbsp; }

</code></pre><p>完成上述操作，新建的按钮就可以正常工作了。如下图示例，点击工具栏加载按钮，文件已在 SpreadJS 表格编辑器成功加载。</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2021/01/image004.jpg" class="kg-image" alt="image.png" width="600" height="400" loading="lazy"></figure><p>以上就是 Vue3 组件开发：搭建基于SpreadJS的表格编辑系统（组件集成篇）的全部内容，通过集成 SpreadJS 电子表格组件和在线编辑器组件，我们搭建的项目原型已经具备了在线表格编辑系统的雏形。</p><p>在下一章功能拓展篇中，我们将演示如何为这个系统雏形增加更多电子表格功能，并提供整个工程源码供参考。</p><h1 id="--2">扩展阅读</h1><p>· <a href="https://chinese.freecodecamp.org/news/vue3-component/">Vue3 组件开发实战：搭建基于SpreadJS的表格编辑系统（环境搭建篇）</a></p><p>· <a href="https://www.grapecity.com.cn/blogs/spreadjs-vue3-component-development-combat-part3">Vue3 组件开发实战：搭建基于SpreadJS的表格编辑系统（功能拓展篇）</a></p><p>· <a href="https://www.grapecity.com.cn/developer/spreadjs/vue">SpreadJS Vue 框架支持</a></p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ Vue3 组件开发：搭建基于 Vite 的在线表格编辑系统（环境搭建） ]]>
                </title>
                <description>
                    <![CDATA[ Vue是一套用于构建用户界面的渐进式框架，与其它大型 JS 框架不同，Vue 被设计为可以自底向上逐层应用，更易上手，还便于与第三方库或既有项目整合，因此，Vue完全能够为复杂的单页应用提供驱动。 2020年09月18日，Vue.js 3.0 正式发布，作者尤雨溪将其描述为：更快、更小、更易于维护。 Vue3 都加入了哪些新功能？ 本次发布， Vue框架本身迎来了多项更新，如Vue 此前的反应系统是使用 Object.defineProperty 的 getter 和 setter。 但是，在 Vue3 中，将使用 ES2015 Proxy 作为其观察者机制，这样做的好处是消除了以前存在的警告，使速度加倍，并节省了一半的内存开销。 除了基于 Proxy 的观察者机制，Vue3 的其他新特性还包括： 1. Performance（性能提升） 在Vue2中，当某个DOM需要更新时，需要遍历整个虚拟DOM树才能判断更新点。而在 Vue3 中，无需此项操作，仅需通过静态标记，对比虚拟节点上带有patch flag的节点，即可定位更新位置。 对比Vue2和Vue3的性能差异，官方文档 ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/vue3-component/</link>
                <guid isPermaLink="false">5ff41ba239641a0517d533f0</guid>
                
                    <category>
                        <![CDATA[ Vue ]]>
                    </category>
                
                    <category>
                        <![CDATA[ JavaScript ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Web开发 ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Willie.ji ]]>
                </dc:creator>
                <pubDate>Tue, 05 Jan 2021 09:35:29 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2021/01/roman-synkevych-vXInUOv1n84-unsplash.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Vue是一套用于构建用户界面的渐进式框架，与其它大型 JS 框架不同，Vue 被设计为可以自底向上逐层应用，更易上手，还便于与第三方库或既有项目整合，因此，Vue完全能够为复杂的单页应用提供驱动。</p><p>2020年09月18日，Vue.js 3.0 正式发布，作者尤雨溪将其描述为：更快、更小、更易于维护。</p><h3 id="vue3-">Vue3 都加入了哪些新功能？</h3><p>本次发布， Vue框架本身迎来了多项更新，如Vue 此前的反应系统是使用 Object.defineProperty 的 getter 和 setter。 但是，在 Vue3 中，将使用 ES2015 Proxy 作为其观察者机制，这样做的好处是消除了以前存在的警告，使速度加倍，并节省了一半的内存开销。</p><p>除了基于 Proxy 的观察者机制，Vue3 的其他新特性还包括：</p><p><strong>1. Performance（性能提升）</strong></p><p>在Vue2中，当某个DOM需要更新时，需要遍历整个虚拟DOM树才能判断更新点。而在 Vue3 中，无需此项操作，仅需通过静态标记，对比虚拟节点上带有patch flag的节点，即可定位更新位置。</p><p>对比Vue2和Vue3的性能差异，官方文档中给出了具体数据说明：</p><p>· SSR速度提高了2~3倍</p><p>· Update性能提高1.3~2倍</p><p><strong>2. Composition API（组合API）</strong></p><p>Vue2中有data、methods、mounted等存储数据和方法的对象，我们对此应该不陌生了。比如说要实现一个轮播图的功能，首先需要在data里定义与此功能相关的数据，在methods里定义该功能的方法，在mounted里定义进入页面自动开启轮播的代码…… 有一个显而易见的问题，就是同一个功能的代码却要分散在页面的不同地方，维护起来会相当麻烦。</p><p>为了解决上述问题，Vue3推出了具备清晰的代码结构，并可消除重复逻辑的 &nbsp;Composition API，以及两个全新的函数setup和ref。</p><p>Setup 函数可将属性和方法返回到模板，在组件初始化的时候执行，其效果类似于Vue2中的beforeCreate 和 created。如果想使用setup里的数据，需要将值return出来，没有从setup函数返回的内容在模板中不可用。</p><p>Ref函数的作用是创建一个引用值，主要是对String、Number、Boolean的数据响应做引用。</p><p>相对于Vue2，Vue3的生命周期函数也发生了变更，如下所示：</p><p>· <code>beforeCreate</code> -&gt; 请使用 <code>setup()</code></p><p>· <code>created</code> -&gt; 请使用 <code>setup()</code></p><p>· <code>beforeMount</code> -&gt; <code>onBeforeMount</code></p><p>· <code>mounted</code> -&gt; <code>onMounted</code></p><p>· <code>beforeUpdate</code> -&gt; <code>onBeforeUpdate</code></p><p>· <code>updated</code> -&gt; <code>onUpdated</code></p><p>· <code>beforeDestroy</code> -&gt; <code>onBeforeUnmount</code></p><p>· <code>destroyed</code> -&gt; <code>onUnmounted</code></p><p>· <code>errorCaptured</code> -&gt; <code>onErrorCaptured</code></p><p>需要注意的是，Vue2使用生命周期函数时是直接在页面中写入生命周期函数，而在Vue3则直接引用即可：</p><pre><code class="language-vue">import {reactive, ref, onMounted} from 'vue'</code></pre><p><strong>3. Tree shaking support（按需打包模块）</strong></p><p>有人将“Tree shaking” &nbsp;称之为“摇树优化”，其实就是把无用的模块进行“剪枝”，剪去没有用到的API，因此“Tree shaking”之后，打包的体积将大幅度减少。</p><p>官方将Vue2和Vue3进行了对比，Vue2若只写了 <code>Hello World</code>，且没有用到任何的模块API，打包后的大小约为32kb，而Vue3 打包后仅有13.5kb。</p><p><strong>4. 全新的脚手架工具：Vite</strong></p><p>Vite 是一个由原生 ESM 驱动的 Web 开发构建工具。在开发环境下基于浏览器原生 ES imports 开发，在生产环境下基于 Rollup 打包。</p><p>和 Webpack相比，具有以下特点：</p><p>· 快速的冷启动，不需要等待打包</p><p>· 即时的热模块更新</p><p>· 真正的按需编译，不用等待整个项目编译完成</p><p>由于完全跳过了打包这个概念，Vite的出现大大的撼动了Webpack的地位，且真正做到了服务器随起随用。看来，连尤大神都难逃“真香”理论。</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2021/01/image003.png" class="kg-image" alt="image.png" width="600" height="400" loading="lazy"></figure><p>Vite究竟有什么魔力？不妨让我们通过实际搭建一款基于Vue3 组件的表格编辑系统，亲自体验一把。</p><h1 id="-">一、环境搭建</h1><h3 id="-vite-vue3-">使用 Vite 初始化一个 Vue3 项目</h3><p>1. 执行代码：</p><pre><code>
$ npm init vite-app &lt;project-name&gt;

$ cd &lt;project-name&gt; //进入项目目录

$ npm install //安装项目所需依赖

$ npm run dev //启动项目

</code></pre><p>我们来看下生成的代码, 因为 vite 会尽可能多地镜像 <code>vue-cli</code> 中的默认配置, 所以，这段代码看上去和 vue-cli 生成的代码没有太大区别。</p><pre><code>├── index.html

├── package.json

├── public

│ └── favicon.ico

└── src

 ├── App.vue

 ├── assets

 │ └── logo.png

 ├── components

 │ └── HelloWorld.vue

 ├── index.css

 └── main.js
</code></pre><p>2. 执行下列命令：</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2021/01/image005.png" class="kg-image" alt="image.png" width="600" height="400" loading="lazy"></figure><p>此时如果不通过 <code>npm run dev</code> 来启动项目，而是直接通过浏览器打开 <code>index.html</code>, 会看到下面的报错：</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2021/01/image007.png" class="kg-image" alt="image.png" width="600" height="400" loading="lazy"></figure><p>报错的原因：浏览器的 ES module 是通过 http 请求拿到模块的，所以 vite 的一个任务就是启动一个 web server 去代理这些模块，在 vite 里是借用了 koa 来启动的服务。</p><pre><code>export function createServer(config: ServerConfig): Server {
  // ...
  const app = new Koa&lt;State, Context&gt;()
  const server = resolveServer(config, app.callback())
  
  // ...
  const listen = server.listen.bind(server)
  server.listen = (async (...args: any[]) =&gt; {
    if (optimizeDeps.auto !== false) {
      await require('../optimizer').optimizeDeps(config)
    }
    return listen(...args)
  }) as any
  
  return server
}
</code></pre><p>由于浏览器中的 ESM 是获取不到导入的模块内容的，需要借助Webpack 等工具，如果我们没有引用相对路径的模块，而是引用 node_modules，并直接 import xxx from 'xxx'，浏览器便无法得知你项目里有 node_modules，只能通过相对路径或者绝对路径去寻找模块。</p><p>这便是vite 的实现核心：拦截浏览器对模块的请求并返回处理后的结果（关于vite 的实现机制，文末会深入讲解）。</p><p>3. 生成项目结构：</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2021/01/image009.png" class="kg-image" alt="image.png" width="600" height="400" loading="lazy"></figure><p>入口 <code>index.html</code> 和 <code>main.js</code> 代码结构为：</p><pre><code>&lt;!DOCTYPE html&gt;
&lt;html lang="en"&gt;
&lt;head&gt;
  &lt;meta charset="UTF-8"&gt;
  &lt;link rel="icon" href="/favicon.ico" /&gt;
  &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
  &lt;title&gt;Vite App&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
  &lt;div id="app"&gt;&lt;/div&gt;
  &lt;script type="module" src="/src/main.js"&gt;&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;

// main.js
// 只是引用的是最新的 vue3 语法，其余相同
import { createApp } from 'vue'
import App from './App.vue'
import './index.css'

createApp(App).mount('#app')

</code></pre><p>4. 进入项目目录：cd myVue3</p><p>5. 安装相关模块：npm install</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2021/01/image011.png" class="kg-image" alt="image.png" width="600" height="400" loading="lazy"></figure><p>6. 下载模块：</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2021/01/image013.png" class="kg-image" alt="image.png" width="600" height="400" loading="lazy"></figure><p>7. 启动项目：npm run dev</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2021/01/image015.png" class="kg-image" alt="image.png" width="600" height="400" loading="lazy"></figure><p>8. 进入地址，当我们看到这个页面时，说明项目已经成功启动了。</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2021/01/image017.png" class="kg-image" alt="image.png" width="600" height="400" loading="lazy"></figure><h3 id="vite-">Vite 的实现机制</h3><p><strong>1. /@module/ 前缀</strong></p><p>对比工程下的 <code>main.js</code> 和开发环境下实际加载的 <code>main.js</code>，可以发现代码发生了变化。</p><p>工程下的 <code>main.js</code>：</p><pre><code>import { createApp } from 'vue'
import App from './App.vue'
import './index.css'

createApp(App).mount('#app')

</code></pre><p>实际加载的 <code>main.js</code>：</p><pre><code>import { createApp } from '/@modules/vue.js'
import App from '/src/App.vue'
import '/src/index.css?import'

createApp(App).mount('#app')

</code></pre><p>为了解决 <code>import xxx from 'xxx'</code> 报错的问题，vite 对这种资源路径做了统一处理，即添加一个 <code>/@module/</code> 前缀。</p><p>在 <code>src/node/server/serverPluginModuleRewrite.ts</code> 源码的 koa 中间件里可以看到 vite 对 <code>import</code> 做了一层处理，其过程如下：</p><p>· 在 koa 中间件里获取请求 body</p><p>· 通过 <a href="https://link.zhihu.com/?target=https%3A//github.com/guybedford/es-module-lexer"><code>es-module-lexer</code></a> 解析资源 ast 拿到 import 的内容</p><p>· 判断 import 的资源是否是绝对路径，绝对视为 npm 模块</p><p>· 返回处理后的资源路径："vue" =&gt; "/@modules/vue"</p><p><strong>2. 支持 /@module/</strong></p><p>在 <code>/src/node/server/serverPluginModuleResolve.ts</code> 里可以看到大概的处理逻辑：</p><p>· 在 koa 中间件里获取请求 body</p><p>· 判断路径是否以 /@module/ 开头，如果是取出包名</p><p>· 去 <code>node_module</code> 里找到这个库，基于 package.json 返回对应的内容</p><p><strong>3. 文件编译</strong></p><p>通过前文，我们知道了 js module 的处理过程，对于vue、css、ts等文件，其又是如何处理的呢？</p><p>以 vue 文件为例，在 webpack 里使用 vue-loader 对单文件组件进行编译，在这里 vite 同样拦截了对模块的请求并执行了一个实时编译。</p><p>通过工程下的 App.vue 和实际加载的 App.vue，便发现改变。</p><p>工程下的 <code>App.vue</code>：</p><pre><code>&lt;template&gt;
  &lt;img alt="Vue logo" src="./assets/logo.png" /&gt;
  &lt;HelloWorld msg="Hello Vue 3.0 + Vite" /&gt;
&lt;/template&gt;

&lt;script&gt;
import HelloWorld from './components/HelloWorld.vue';

export default {
  name: 'App',
  components: {
    HelloWorld,
  },
};
&lt;/script&gt;
&lt;style&gt;
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
&lt;/style&gt;

</code></pre><p>实际加载的 <code>App.vue</code>：</p><pre><code>import HelloWorld from '/src/components/HelloWorld.vue';

const __script = {
    name: 'App',
    components: {
        HelloWorld,
    },
};

import "/src/App.vue?type=style&amp;index=0&amp;t=1592811240845"
import {render as __render} from "/src/App.vue?type=template&amp;t=1592811240845"
__script.render = __render
__script.__hmrId = "/src/App.vue"
__script.__file = "/Users/wang/qdcares/test/vite-demo/src/App.vue"
export default __script

</code></pre><p>可见，一个 .vue 文件被拆成了三个请求（分别对应 script、style 和template） ，浏览器会先收到包含 script 逻辑的 App.vue 的响应，然后解析到 template 和 style 的路径后，再次发起 HTTP 请求来请求对应的资源，此时 Vite 对其拦截并再次处理后返回相应的内容。</p><pre><code>// App.vue?type=style
import { updateStyle } from "/vite/hmr"
const css = "\n#app {\n  font-family: Avenir, Helvetica, Arial, sans-serif;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n  text-align: center;\n  color: #2c3e50;\n  margin-top: 60px;\n}\n"
updateStyle("7ac74a55-0", css)
export default css

// App.vue?type=template
import {createVNode as _createVNode, resolveComponent as _resolveComponent, Fragment as _Fragment, openBlock as _openBlock, createBlock as _createBlock} from "/@modules/vue.js"

const _hoisted_1 = /*#__PURE__*/
_createVNode("img", {
    alt: "Vue logo",
    src: "/src/assets/logo.png"
}, null, -1 /* HOISTED */
)

export function render(_ctx, _cache) {
    const _component_HelloWorld = _resolveComponent("HelloWorld")

    return (_openBlock(),
    _createBlock(_Fragment, null, [_hoisted_1, _createVNode(_component_HelloWorld, {
        msg: "Hello Vue 3.0 + Vite"
    })], 64 /* STABLE_FRAGMENT */
    ))
}
</code></pre><p>vite 对于其他的类型文件的处理几乎都是类似的逻辑，即根据请求的不同文件类型，做出不同的编译处理结果。</p><h1 id="--1">扩展阅读</h1><p>· <a href="https://chinese.freecodecamp.org/news/vue3-component-integration/">Vue3 组件开发实战：搭建基于SpreadJS的表格编辑系统（组件集成篇）</a></p><p>· <a href="https://www.grapecity.com.cn/blogs/spreadjs-vue3-component-development-combat-part3">Vue3 组件开发实战：搭建基于SpreadJS的表格编辑系统（功能拓展篇）</a></p><p>· <a href="https://www.grapecity.com.cn/developer/spreadjs/vue">SpreadJS Vue 框架支持</a></p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ Flash Player终将成为历史，HTML5正站在舞台的中央 ]]>
                </title>
                <description>
                    <![CDATA[ 12月28日，微软确认Windows 10在下一次更新时将自动删除Flash Player，这意味着Flash Player将正式成为历史。 Flash曾是互联网的一段传奇，它统一了互联网的内容创作模式，做到了真正的一次编写，到处运行，让每个人都有可能成为动画师和艺术家。 然而，随着移动互联时代的到来，Flash也逐渐暴露出越来越多的技术短板，耗电、运行速度慢以及安全隐患甚至一度让乔布斯对其彻底失去了信心。2017年7月25日， Adobe官方声明：2020年12月31日，将停止更新和发行Flash Player（EOL），这意味着 Flash Player在这一刻终于成为了历史： 自终止日期之后，Adobe不会继续发布Flash Player更新或安全补丁 从2021年1月12日开始禁止Flash内容在Flash Player中运行 主要的浏览器供应商也将在EOL日期之后禁止Flash Player运行 Flash Player前世：因带宽限制而兴 在2000年前后，互联网已经开始在国内普及，受带宽的限制（当年主流拨号是56K的modem，有个128K的ISDN就算是土豪 ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/flash-player-vs-html5/</link>
                <guid isPermaLink="false">5fead84739641a0517d527e6</guid>
                
                    <category>
                        <![CDATA[ HTML5 ]]>
                    </category>
                
                    <category>
                        <![CDATA[ 微软 ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Willie.ji ]]>
                </dc:creator>
                <pubDate>Tue, 29 Dec 2020 07:36:48 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2020/12/1609227392950.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>12月28日，微软确认Windows 10在下一次更新时将自动删除Flash Player，这意味着Flash Player将正式成为历史。</p><p>Flash曾是互联网的一段传奇，它统一了互联网的内容创作模式，做到了真正的一次编写，到处运行，让每个人都有可能成为动画师和艺术家。</p><p>然而，随着移动互联时代的到来，Flash也逐渐暴露出越来越多的技术短板，耗电、运行速度慢以及安全隐患甚至一度让乔布斯对其彻底失去了信心。2017年7月25日， Adobe官方声明：2020年12月31日，将停止更新和发行Flash Player（EOL），这意味着 Flash Player在这一刻终于成为了历史：</p><p>自终止日期之后，Adobe不会继续发布Flash Player更新或安全补丁</p><p>从2021年1月12日开始禁止Flash内容在Flash Player中运行</p><p>主要的浏览器供应商也将在EOL日期之后禁止Flash Player运行</p><h2 id="flash-player-">Flash Player前世：因带宽限制而兴</h2><p>在2000年前后，互联网已经开始在国内普及，受带宽的限制（当年主流拨号是56K的modem，有个128K的ISDN就算是土豪了），网页内容大多以静态方式呈现，网速慢到下载一首MP3需要十多分钟甚至二十分钟时间，看一段清晰度很差的视频得等上20分钟，还必须先安装一个微软的MediaPlayer插件。</p><p>为了解决上述问题，Adobe 推出了一种矢量动画的格式，通过占用少量的空间，提供动画的无限放大和保真度，这就是 Flash 的雏形。</p><p>由于空间占用小，意味着用户可以更快的打开，互联网页面的呈现方式首次以动画效果示人，由于可以无限放大，意味着用户可以看到更加清晰的动画，而不用忍受GIF为了体积而缩小、减色（真彩色缩减为256色）。</p><p>Flash的火爆仿佛就在情理之中，在很短的时间内便发展成为了嵌入网页中的小游戏、动画、广告载体以及图形用户界面最常用的格式。围绕着 Flash 而生的产业链也如雨后春笋般涌现，如以 “2144”、“4399”、“7k7k”等为代表的网页游戏，以优酷网、土豆网等为代表的在线视频门户网站，就连当年的中学计算机课也把制作一个Flash动画作为考试的题目。</p><p>用Flash制作的动画作品“新长征路上的摇滚”、“东北人都是活雷锋” 传唱大江南北。</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2020/12/--1.png" class="kg-image" alt="--1" width="600" height="400" loading="lazy"></figure><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2020/12/--2.jpg" class="kg-image" alt="--2" width="600" height="400" loading="lazy"></figure><h2 id="flash-player--1">Flash Player今生：因视频播放而盛</h2><p>Flash，并没有因2017年Adobe公司的声明被宣判 “死刑”。相反，从Flash Player 6开始，Macromedia 给Flash加入了支持播放视频的能力，可以在SWF格式的文件中嵌入视频数据，依然支持流播放。以至于后来大名鼎鼎的FLV格式，也是从Flash Player 7的文件格式中提取出来的。</p><p>FLV的出现引爆了视频流媒体的整个行业，此时 Flash播放器的装机率已经超过95%，用Flash做一个几十KB的小播放器，然后用这个小播放器来流式播放FLV视频文件，便可以迅速搭建出一个在线视频点播网站！Youtube、优酷、土豆正是借此先后兴起。</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2020/12/--3.jpg" class="kg-image" alt="--3" width="600" height="400" loading="lazy"></figure><h2 id="flash-player-html5-">Flash Player未来：因HTML5兴起而衰</h2><p>在HTML5中，Flash几乎所有的功能都可以实现，而与Flash不同的是，HTML5是一个开放的标准，而Flash是属于Adobe公司的。因此，无论是苹果、谷歌，还是微软，都不希望被一家公司所左右，它们更喜欢一个门户开放的东西。</p><p>即便在初期，Flash身上的毛病HTML5同样存在，甚至更严重（在对比测试中HTML5的性能更差，能耗更高），但是当微软、苹果、谷歌三大巨头的浏览器都支持HTML5的时候，这个标准事实上就建立起来了。</p><p>之后发生的事大家都看到了，各个网站都开始从Flash转向HTML5，Flash也全面走向了边缘化。2014年10月29日,万维网联盟宣布,经过8年的艰辛努力,HTML5标准规范终于最终制定完成了,并公开发布。</p><p>HTML5封神的那一刻，Flash便注定大势已去。</p><p>2017年7月26日，Adobe宣布计划终结Flash Player插件，并在 2020年年底前停止开发和分发。而其之所以做出这个决定，主要原因在于Flash长久以来被人诟病的性能和安全问题，Flash自发布以来就被曝存在大量严重安全漏洞，即便屡次更新也难以彻底解决。</p><h2 id="-">英雄终将落幕，薪火总会传承</h2><p>对于原有的flash player网站运营来说，寻找替代方案是必须的：</p><ul><li>视频播放：在HTML5 中，可以通过HTML标签“video”和“audio”来支持嵌入式的媒体，使开发者能够更方便地将媒体嵌入到HTML文档中。</li></ul><pre><code>&lt;video src="/i/movie.ogg" controls="controls" width="400" height="300"&gt;
&lt;/video&gt;

</code></pre><ul><li>2D/3D动画：通过WebGL这一浏览器动画渲染的技术，有别于过去需要安装浏览器插件，通过 WebGL的技术，它让你可以将其元素与HTML元素进行混合和匹配，并将其与页面或背景图片的其他页面元素相结合，只需要编写网页代码即可实现3D图像的展示。WebGL可以为HTML5 Canvas提供硬件3D加速渲染，这样Web开发人员就可以在浏览器里更流畅地展示3D场景和模型，WebGL技术标准免去了开发网页专用渲染插件的麻烦，可被用于创建具有复杂3D结构的网站页面，甚至可以用来设计3D网页游戏等等。</li><li>复杂的前端功能：随着HTML5标准的确立，日益发展的前端开发领域为我们提供了众多成熟的技术框架以及功能组件，可以协助我们快速实现各种应用场景下的需求，比如<a href="https://demo.grapecity.com.cn/SpreadJS/WebDesigner/index.html">在线编辑 Excel 文档</a>。</li></ul><p>对于普通用户来说，如果仍然碰到包含Flash内容的网站，可以考虑下面的选择：</p><p>继续使用中国特供版Flashplayer，即便在flash player通用版本停止更新后，Adobe的中国合作方仍会继续推出特供版，就目前的实际情况看，特供版会附带广告弹窗，请谨慎考虑。</p><p>可以继续使用支持flashplayer的浏览器。比如IE或者老版本的Edge。</p><p>如今，大部分的网站已经将flash player以HTML5支持的相关元素进行了替换，普通用户在正常浏览网站时并不会有太多的感知和不便。因此，作为用户而言，不必太担心这个问题。而对于网站运营和开发人员来说，由于flash player的使用场景大部分集中在网站建设中，因此有必要认真考虑这个问题，并积极寻找Flash替代方案。</p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 学习现代 JavaScript：ES6+ 中的 Imports，Exports，Let，Const 和 Promise ]]>
                </title>
                <description>
                    <![CDATA[ JavaScript 在过去几年中有很多更新。如果你想提升写代码的能力，这些更新将会对你有非常大的帮助。对于程序员来说，了解 JavaScript 这门语言的最新发展是非常重要的。它能使你跟上最新趋势，提高代码质量，在工作中出类拔萃，从而进一步提升你的薪资待遇。 特别地，如果你想学习像React、 Angular或Vue这样的框架，你必须掌握这些最新的特性。 最近，JavaScript增加了许多有用的功能，比如Nullish coalescing operator, optional chaining, Promises, async/await, ES6 destructuring,等等。 那么现在，我们将探讨每个JavaScript开发者都应该知道的概念。 JavaScript 中的 Let 和 const 在ES6之前，JavaScript使用var关键字来声明变量，var只有全局作用域和函数作用域，所谓全局作用域就是在代码的任何位置都能访问var声明的变量，而函数作用域在变量声明的当前函数内部访问变量。此时是没有块级作用域的。 随着let和const这两个关键字的添加 ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/learn-modern-javascript/</link>
                <guid isPermaLink="false">5fe40b0739641a0517d525bf</guid>
                
                    <category>
                        <![CDATA[ JavaScript ]]>
                    </category>
                
                    <category>
                        <![CDATA[ ES6 ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Willie.ji ]]>
                </dc:creator>
                <pubDate>Thu, 24 Dec 2020 03:35:22 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2020/12/photo-1457305237443-44c3d5a30b89.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>JavaScript 在过去几年中有很多更新。如果你想提升写代码的能力，这些更新将会对你有非常大的帮助。对于程序员来说，了解 JavaScript 这门语言的最新发展是非常重要的。它能使你跟上最新趋势，提高代码质量，在工作中出类拔萃，从而进一步提升你的薪资待遇。</p><p>特别地，如果你想学习像React、 Angular或Vue这样的框架，你必须掌握这些最新的特性。</p><p>最近，JavaScript增加了许多有用的功能，比如<strong>Nullish coalescing operator, optional chaining, Promises, async/await, ES6 destructuring</strong>,等等。</p><p>那么现在，我们将探讨每个JavaScript开发者都应该知道的概念。</p><h2 id="javascript-let-const"><strong>JavaScript 中的 Let 和 const</strong></h2><p>在ES6之前，JavaScript使用var关键字来声明变量，var只有全局作用域和函数作用域，所谓全局作用域就是在代码的任何位置都能访问var声明的变量，而函数作用域在变量声明的当前函数内部访问变量。此时是没有块级作用域的。</p><p>随着let和const这两个关键字的添加，JS增加了块级作用域的概念。</p><h4 id="-javascript-let"><strong>如何在 JavaScript中使用let</strong></h4><p>当我们在用let声明变量时，用于声明一次之后就不能再以相同的名称重新声明它。</p><pre><code>// ES5 Code

var value =  10;

console.log(value);  // 10

var value =  "hello";

console.log(value);  // hello

var value =  30;

console.log(value);  // 30
</code></pre><p>如上所示，我们多次使用var关键字重新声明了变量值。</p><p>在ES6之前，我们可以使用var重新声明之前已经声明过的变量，这就会导致了一个问题：如果我们在不知情的情况下，在其他地方重新声明了该变量，很有可能会覆盖原先变量的值，造成一些难以调试的问题。</p><p>所以，Let解决很好地解决此问题。当你使用let重新声明变量值时，将会报错。</p><pre><code>// ES6 Code
let value = 10;
console.log(value); // 10

let value = "hello"; // Uncaught SyntaxError: Identifier 'value' has already been declared

</code></pre><p>但是，以下代码是合法的:</p><pre><code>// ES6 Code
let value = 10;
console.log(value); // 10

value = "hello";
console.log(value); // hello

</code></pre><p>我们发现上面的代码看起来没什么问题，是因为我们重新给value变量赋了一个新值，但是并没有重新声明。</p><p>我们来看下下面的代码：</p><pre><code>// ES5 Code
var isValid = true;
if(isValid) {
  var number = 10;
  console.log('inside:', number); // inside: 10
}
console.log('outside:', number); // outside: 10

</code></pre><p>如上所示，在使用var声明变量时，可以在if块之外访问该变量。</p><p>而使用let声明的number变量只能在if块内访问，如果在if块外访问将会报错。</p><p>我们来看下接下来的代码</p><pre><code>
// ES6 Code
let isValid = true;
if(isValid) {
  let number = 10;
  console.log('inside:', number); // inside: 10
}
console.log('outside:', number); // Uncaught ReferenceError: number is not defined

</code></pre><p>如上述代码所示，使用let分别在if块内、if块外声明了number变量。在if块外，number无法被访问，因此会出现引用错误。</p><p>但是，如果变量number在if块外已经声明，将会出现下面的结果。</p><pre><code>
// ES6 Code
let isValid = true;
let number = 20;

if(isValid) {
  let number = 10;
  console.log('inside:', number); // inside: 10
}

console.log('outside:', number); // outside: 20

</code></pre><p>现在在单独的范围内有两个number变量。在if块外，number的值为20。</p><pre><code> // ES5 Code
for(var i = 0; i &lt; 10; i++){
 console.log(i);
}
console.log('outside:', i); // 10



</code></pre><p>当使用var关键字时，i在 for循环之外也可以访问到。</p><pre><code>
// ES6 Code
for(let i = 0; i &lt; 10; i++){
 console.log(i);
}

console.log('outside:', i); // Uncaught ReferenceError: i is not defined

</code></pre><p>而使用let关键字时，在for循环外部是不可访问的。</p><p>因此，正如上述示例代码所示，let声明的变量只能在块内部可用，而在块外部不可访问。</p><p>我们可以使用一对大括号创建一个块，如下:</p><pre><code>let i = 10;
{
 let i = 20;
 console.log('inside:', i); // inside: 20
 i = 30;
 console.log('i again:', i); // i again: 30
}

console.log('outside:', i); // outside: 10


</code></pre><p>前面有提到，let在同一个块中不能重新声明变量，不过可以在另一个块中重新声明。如上代码所示，我们在块内重新声明了i，并赋值20，该变量仅可在该块中使用。</p><p>在块外，当我们打印变量时，我们得到的是10而不是之前分配的值，这是因为块外，内部变变量i是不存在的。</p><p>如果在块外未声明变量，那么将会报错:</p><pre><code>
{
 let i = 20;
 console.log('inside:', i); // inside: 20
 i = 30;
 console.log('i again:', i); // i again: 30
}

console.log('outside:', i); // Uncaught ReferenceError: i is not defined
</code></pre><h4 id="-javascript-const"><strong>如何在 JavaScript使用const</strong></h4><p>const关键字在块级作用域中的工作方式与let关键字完全相同。因此，我们来看下他们的区别。</p><p>const声明的变量为常量，其值是不能改变的。而let声明的变量，可以为其赋一个新值，如下所示:</p><pre><code>let number = 10;
number = 20;
console.log(number); // 20
</code></pre><p>但是以下情况，我们不能这样使用const。</p><pre><code>
const number = 10;
number = 20; // Uncaught TypeError: Assignment to constant variable.


</code></pre><p>我们甚至不能使用const像下面一样重新声明。</p><pre><code>const number = 20;
console.log(number); // 20

const number = 10; // Uncaught SyntaxError: Identifier 'number' has already been declared

</code></pre><p>现在，看下面的代码:</p><pre><code>const arr = [1, 2, 3, 4];
arr.push(5);
console.log(arr); // [1, 2, 3, 4, 5]

</code></pre><p>我们说过const声明的常量，它的值永远不会改变——但是我们改变了上面的常量数组并没有报错。这是为什么呢?</p><p>注意:数组是引用类型，而不是JavaScript的基本类型</p><p>实际存储在arr中的不是数组，而是数组存储的内存位置的引用(地址)。执行arr.push(5)，并没有改变arr指向的引用，而是改变了存储在那个引用上的值。</p><p>对象也是如此:</p><pre><code>const obj = {
 name: 'David',
 age: 30
};

obj.age = 40;

console.log(obj); // { name: 'David', age: 40 }

</code></pre><p>这里，我们也没有改变obj指向的引用，而是改变了存储在引用的值。</p><p>因此，上述的代码将会起作用，但下面的代码是无效的。</p><pre><code>const obj = { name: 'David', age: 30 };
const obj1 = { name: 'Mike', age: 40 };
obj = obj1; // Uncaught TypeError: Assignment to constant variable.

</code></pre><p>这样写会抛出异常，因为我们试图更改const变量指向的引用。</p><p>因此，在使用const时要记住一点：使用const声明常量时，不能重新声明，也不能重新赋值。如果声明的常量是引用类型，我们可以更改存储在引用的值。</p><p>同理，下面的代码也是无效的。</p><pre><code>const arr = [1, 2, 3, 4];
arr = [10, 20, 30]; // Uncaught TypeError: Assignment to constant variable.

</code></pre><h4 id="-"><strong>总结：</strong></h4><p>关键字let和const在JavaScript中添加块级作用域。</p><p>当我们将一个变量声明为let时，我们不能在同一作用域(函数或块级作用域)中重新定义或重新声明另一个具有相同名称的let变量，但是我们可以重新赋值。</p><p>当我们将一个变量声明为const时，我们不能在同一作用域(函数或块级作用域)中重新定义或重新声明具有相同名称的另一个const变量。但是，如果变量是引用类型(如数组或对象)，我们可以更改存储在该变量中的值。</p><p>好了，我们继续下一个话题: &nbsp;promises。</p><h2 id="javascript-promises"><strong>JavaScript中的promises</strong></h2><p>对于很多新开发者来说，promises是JavaScript中较难理解的部分。ES6中原生提供了Promise对象，那么Promise究竟是什么呢？</p><p>Promise 对象代表了未来将要发生的事件，用来传递异步操作的消息。</p><p><strong>如何创造一个 promise</strong></p><p>使用promise构造函数创建一个promise，如下所示:</p><pre><code>const promise = new Promise(function(resolve, reject) {
 
});

</code></pre><p>Promise的构造函数接收一个函数作为参数，并且在内部接收两个参数：resolve，reject。 resolve和reject参数实际上是我们可以调用的函数，具体取决于异步操作的结果。</p><p>Promise 有三种状态:</p><p>pending: 初始状态，不是成功或失败状态。</p><p>fulfilled:表示操作成功完成。</p><p>rejected: 表示操作失败。</p><p>当我们创建Promise时，它处于等待的状态。当我们调用resolve函数时，它将进入已完成状态。如果调用reject，他将进入被拒绝状态。</p><p>在下面的代码中，我们执行了一个异步操作，也就是setTimeout，2秒后，调用resolve方法。</p><pre><code>const promise = new Promise(function(resolve, reject) {
 setTimeout(function() {
  const sum = 4 + 5;
  resolve(sum);
 }, 2000);
});


</code></pre><p>我们需要使用以下方法注册一个回调.then获得1promise执行成功的结果，如下所示:</p><pre><code>const promise = new Promise(function(resolve, reject) {
 setTimeout(function() {
  const sum = 4 + 5;
  resolve(sum);
 }, 2000);
});

promise.then(function(result) {
 console.log(result); // 9
});


</code></pre><p>then接收一个参数，是函数，并且会拿到我们在promise中调用resolve时传的的参数。</p><p>如果操作不成功，则调用reject函数:</p><pre><code>const promise = new Promise(function(resolve, reject) {
 setTimeout(function() {
  const sum = 4 + 5 + 'a';
  if(isNaN(sum)) {
    reject('Error while calculating sum.');
  } else {
    resolve(sum);
  }
 }, 2000);
});

promise.then(function(result) {
 console.log(result);
});


</code></pre><p>如果sum不是一个数字，那么我们调用带有错误信息的reject函数，否则我们调用resolve函数。</p><p>执行上述代码，输出如下：</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2020/12/image-47.png" class="kg-image" alt="image-47" width="600" height="400" loading="lazy"></figure><p>调用reject函数会抛出一个错误，但是我们没有添加用于捕获错误的代码。</p><p>需要调用catch方法指定的回调函数来捕获并处理这个错误。</p><pre><code>promise.then(function(result) {
 console.log(result);
}).catch(function(error) {
 console.log(error);
});

</code></pre><p>输出如下：</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2020/12/image-48.png" class="kg-image" alt="image-48" width="600" height="400" loading="lazy"></figure><p>所以建议大家在使用promise时加上catch方法，以此来避免程序因错误而停止运行。</p><p><strong>链式操作</strong></p><p>我们可以向单个promise添加多个then方法，如下所示:</p><pre><code>promise.then(function(result) {
 console.log('first .then handler');
 return result;
}).then(function(result) {
 console.log('second .then handler');
 console.log(result);
}).catch(function(error) {
 console.log(error);
});

</code></pre><p>当添加多个then方法时，前一个then方法的返回值将自动传递给下一个then方法。</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2020/12/image-49.png" class="kg-image" alt="image-49" width="600" height="400" loading="lazy"></figure><p>如上图所示，我们在第一个then方法中输出字符串，并将接收的参数result（sum）返回给下一个result。</p><p>在下一个then方法中，输出字符串，并输出上一个then方法传递给它的result。</p><h4 id="-javascript-promise-"><strong>如何在 JavaScript中延迟promise的执行</strong></h4><p>很多时候，我们不希望立即创建promise，而是希望在某个操作完成后再创建。</p><p>我们可以将promise封装在一个函数中，然后从函数中返回promise，如下所示：</p><pre><code>function createPromise() {
 return new Promise(function(resolve, reject) {
  setTimeout(function() {
   const sum = 4 + 5;
   if(isNaN(sum)) {
     reject('Error while calculating sum.');
   } else {
    resolve(sum);
   }
  }, 2000);
 });
}


</code></pre><p>这样，我们就可以通过函数将参数传递给promise，达到动态的目的。</p><pre><code>function createPromise(a, b) {
 return new Promise(function(resolve, reject) {
  setTimeout(function() {
   const sum = a + b;
   if(isNaN(sum)) {
     reject('Error while calculating sum.');
   } else {
    resolve(sum);
   }
  }, 2000);
 });
}

createPromise(1,8)
 .then(function(output) {
  console.log(output); // 9
});

// OR

createPromise(10,24)
 .then(function(output) {
  console.log(output); // 34
});

</code></pre><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2020/12/image-50.png" class="kg-image" alt="image-50" width="600" height="400" loading="lazy"></figure><p>此外，我们只能向resolve或reject函数传递一个值。如果你想传递多个值到resolve函数，可以将它作为一个对象传递，如下:</p><pre><code>const promise = new Promise(function(resolve, reject) {
 setTimeout(function() {
  const sum = 4 + 5;
  resolve({
   a: 4,
   b: 5,
   sum
  });
 }, 2000);
});

promise.then(function(result) {
 console.log(result);
}).catch(function(error) {
 console.log(error);
});

</code></pre><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2020/12/image-51.png" class="kg-image" alt="image-51" width="600" height="400" loading="lazy"></figure><h4 id="-javascript-"><strong>如何在 JavaScript中使用箭头函数</strong></h4><p>上述示例代码中，我们使用常规的ES5语法创建了promise。但是，通常使用箭头函数代替ES5语法，如下:</p><pre><code>const promise = new Promise((resolve, reject) =&gt; {
 setTimeout(() =&gt; {
  const sum = 4 + 5 + 'a';
  if(isNaN(sum)) {
    reject('Error while calculating sum.');
  } else {
    resolve(sum);
  }
 }, 2000);
});

promise.then((result) =&gt; {
 console.log(result);
});

</code></pre><p>你可以根据自己需要使用ES5或ES6语法。</p><h4 id="es6-import-export-"><strong>ES6 Import 和Export 语法</strong></h4><p>在ES6之前，我们在一个HTML文件中可以使用多个script标签来引用不同的JavaScript文件，如下所示:</p><pre><code>&lt;script type="text/javascript" src="home.js"&gt;&lt;/script&gt;
&lt;script type="text/javascript" src="profile.js"&gt;&lt;/script&gt;
&lt;script type="text/javascript" src="user.js"&gt;&lt;/script&gt;

</code></pre><p>但是如果我们在不同的JavaScript文件中有一个同名的变量，将会出现命名冲突，你实际得到的可能并不是你期望的值。</p><p>ES6增加了模块的概念来解决这个问题。</p><p>在ES6中，我们编写的每一个JavaScript文件都被称为模块。我们在每个文件中声明的变量和函数不能用于其他文件，除非我们将它们从该文件中导出并、在另一个文件中得到引用。</p><p>因此，在文件中定义的函数和变量是每个文件私有的，在导出它们之前，不能在文件外部访问它们。</p><p>export有两种类型:</p><p>命名导出:在一个文件中可以有多个命名导出</p><p>默认导出:单个文件中只能有一个默认导出</p><h4 id="javascript-"><strong>JavaScript中的命名导出</strong></h4><p>如下所示，将单个变量命名导出:</p><pre><code>export const temp = "This is some dummy text";
</code></pre><p>如果想导出多个变量，可以使用大括号指定要输出的一组变量。</p><pre><code>const temp1 = "This is some dummy text1";
const temp2 = "This is some dummy text2";
export { temp1, temp2 };

</code></pre><p>需要注意的是，导出语法不是对象语法。因此，在ES6中，不能使用键值对的形式导出。</p><pre><code>          // This is invalid syntax of export in ES6
           export { key1: value1, key2: value2 }

</code></pre><p>import命令接受一对大括号，里面指定要从其他模块导入的变量名。</p><pre><code>           import { temp1, temp2 } from './filename';
</code></pre><p>注意，不需要在文件名中添加.js扩展名，因为默认情况下会考虑该拓展名。</p><pre><code>
 // import from functions.js file from current directory

import  { temp1, temp2 }  from  './functions';

 // import from functions.js file from parent of current directory

import  { temp1 }  from  '../functions';
</code></pre><p>提示一点，导入的变量名必须与被导入模块对外接口的名称相同。</p><p>因此，导出应使用：</p><pre><code>// constants.js

export  const  PI  =  3.14159;
</code></pre><p>那么在导入的时候，必须使用与导出时相同的名称：</p><pre><code>import  {  PI  }  from  './constants';

// This will throw an error

import  { PiValue }  from  './constants';
</code></pre><p>如果想为输入的变量重新命名，可以使用as关键字，语法如下:</p><pre><code>import  {  PI  as PIValue }  from  './constants';
</code></pre><p>我们以为PI重命名为PIValue，因此不能再使用PI变量名。</p><p>导出时也可使用下面的重命名语法：</p><pre><code>// constants.js

const  PI  =  3.14159;

export  {  PI  as PIValue };
</code></pre><p>然后在导入是，必须使用PIValue。</p><pre><code>import  { PIValue }  from  './constants';
</code></pre><p>命名导出某些内容之前必须先声明它。</p><pre><code>export  'hello';  // this will result in error

export  const greeting =  'hello';  // this will work

export  { name:  'David'  };  // This will result in error

export  const object =  { name:  'David'  };  // This will work
</code></pre><p>我们来看下面的validations.js 文件:</p><pre><code>// utils/validations.js

const  isValidEmail  =  function(email)  {

  if  (/^[^@ ]+@[^@ ]+\.[^@ \.]{2,}$/.test(email))  {

  return  "email is valid";

  }  else  {

  return  "email is invalid";

  }

};

const  isValidPhone  =  function(phone)  {

  if  (/^[\\(]\d{3}[\\)]\s\d{3}-\d{4}$/.test(phone))  {

  return  "phone number is valid";

  }  else  {

  return  "phone number is invalid";

  }

};

function  isEmpty(value)  {

  if  (/^\s*$/.test(value))  {

  return  "string is empty or contains only spaces";

  }  else  {

  return  "string is not empty and does not contain spaces";

  }

}

export  { isValidEmail, isValidPhone, isEmpty };
</code></pre><p>在index.js中，我们可以使用如下函数:</p><pre><code>// index.js

import  { isEmpty, isValidEmail }  from  "./utils/validations";

console.log("isEmpty:",  isEmpty("abcd"));  // isEmpty: string is not empty and does not contain spaces

console.log("isValidEmail:",  isValidEmail("abc@11gmail.com"));  // isValidEmail: email is valid

console.log("isValidEmail:",  isValidEmail("ab@c@11gmail.com"));  // isValidEmail: email is invalid
</code></pre><h4 id="javascript--1"><strong>JavaScript的默认导出</strong></h4><p>如上所述，单个文件中最多只能有一个默认导出。但是，你可以在一个文件中使用多个命名导出和一个默认导出。</p><p>要声明一个默认导出，我们需要使用以下语法：</p><pre><code>//constants.js

const name =  'David';

export  default name;
</code></pre><p>在导入时就不需要再使用花括号了。</p><pre><code>import name from  './constants';
</code></pre><p>如下，我们有多个命名导出和一个默认导出:</p><pre><code>// constants.js

export  const  PI  =  3.14159;

export  const  AGE  =  30;

const  NAME  =  "David";

export  default  NAME;
</code></pre><p>此时我们使用import导入时，只需要在大括号之前指定默认导出的变量名。</p><pre><code>// NAME is default export and PI and AGE are named exports here

import  NAME,  {  PI,  AGE  }  from  './constants';
</code></pre><p>使用 export default 导出的内容，在导入的时候，import后面的名称可以是任意的。</p><pre><code>// constants.js

const  AGE  =  30;

export  default  AGE;

import myAge from ‘./constants’;

console.log(myAge);  // 30
</code></pre><p>另外， export default的变量名称从Age到myAge之所以可行，是因为只能存在一个export default。因此你可以随意命名。还需注意的是，关键字不能在声明变量之前。</p><pre><code>// constants.js

export  default  const  AGE  =  30;  // This is an error and will not work
</code></pre><p>因此，我们需要在单独的一行使用关键字。</p><pre><code>// constants.js

const  AGE  =  30;

export  default  AGE;
</code></pre><p>不过以下形式是允许的:</p><pre><code>//constants.js

export  default  {

&nbsp;name:  "Billy",

&nbsp;age:  40

};
</code></pre><p>并且需要在另一个文件中使用它</p><pre><code>import user from  './constants';

console.log(user.name);  // Billy

console.log(user.age);  // 40
</code></pre><p>还有，可以使用以下语法来导入constants.js文件中导出的所有变量:</p><pre><code>// test.js

import  *  as constants from  './constants';
</code></pre><p>下面，我们将导入所有我们constants.js存储在constants变量中的命名和export default。因此，constants现在将成为对象。</p><pre><code>// constants.js

export  const  USERNAME  =  "David";

export  default  {

&nbsp;name:  "Billy",

&nbsp;age:  40

};
</code></pre><p>在另一个文件中，我们按一下方式使用。</p><pre><code>// test.js

import  *  as constants from  './constants';

console.log(constants.USERNAME);  // David

console.log(constants.default);  // { name: "Billy", age: 40 }

console.log(constants.default.age);  // 40
</code></pre><p>也可以使用以下方式组合使用命名导出和默认导出:</p><pre><code>// constants.js

const  PI  =  3.14159;  const  AGE  =  30;

const  USERNAME  =  "David";

const  USER  =  {

&nbsp;name:  "Billy",

&nbsp;age:  40

};

export  {  PI,  AGE,  USERNAME,  USER  as  default  };

import  USER,  {  PI,  AGE,  USERNAME  }  from  "./constants";
</code></pre><p>总而言之:</p><p>ES6中，一个模块就是一个独立的文件，该文件内部的所有变量，外部都无法获取。如果想从外部读取模块内的某个变量，必须使用export关键字导出该变量，使用import关键字导入该变量。</p><h2 id="javascript--2"><strong>JavaScript中的默认参数</strong></h2><p>ES6增加了一个非常有用的特性，即在定义函数时提供默认参数。</p><p>假设我们有一个应用程序，一旦用户登录系统，我们将向他们显示一条欢迎消息，如下所示:</p><pre><code>function  showMessage(firstName)  {

  return  "Welcome back, "  + firstName;

}

console.log(showMessage('John'));  // Welcome back, John
</code></pre><p>但是，如果数据库中没有用户名，那该怎么办呢?所以，我们首先需要检查是否提供了firstName，然后再显示相应的信息。</p><p>在ES6之前，我们必须写这样的代码:</p><pre><code>function  showMessage(firstName)  {

  if(firstName)  {

  return  "Welcome back, "  + firstName;

  }  else  {

  return  "Welcome back, Guest";

  }

}

console.log(showMessage('John'));  // Welcome back, John

console.log(showMessage());  // Welcome back, Guest
</code></pre><p>但现在使用ES6提供的默认参数，我们可以这样写:</p><pre><code>function  showMessage(firstName =  'Guest')  {

  return  "Welcome back, "  + firstName;

}

console.log(showMessage('John'));  // Welcome back, John

console.log(showMessage());  // Welcome back, Guest
</code></pre><p>函数的默认参数可以为任意值。</p><pre><code>function  display(a =  10, b =  20, c = b)  {

&nbsp;console.log(a, b, c);

}

display();  // 10 20 20

display(40);  // 40 20 20

display(1,  70);  // 1 70 70

display(1,  30,  70);  // 1 30 70
</code></pre><p>在上面的代码中，我们没有提供函数的所有参数，实际代码等同于:</p><pre><code>display();  // 等同于display(undefined, undefined, undefined)

display(40);  等同于display(40, undefined, undefined)

display(1,  70);  等同于display(1, 70, undefined)
</code></pre><p>因此，如果传递的参数是undefined，则对应的参数将使用默认值。</p><p>我们还可以将对象或计算值指定为默认值，如下：</p><pre><code>const defaultUser =  {

 name:  'Jane',

 location:  'NY',

 job:  'Software Developer'

};

const  display  =  (user = defaultUser, age =  60  /  2  )  =&gt;  {

&nbsp;console.log(user, age);

};

display();

/* output

{

 name: 'Jane',

 location: 'NY',

 job: 'Software Developer'

} 30

*/
</code></pre><p>ES5代码如下:</p><pre><code>// ES5 Code

function  getUsers(page, results, gender, nationality)  {

  var params =  "";

  if(page ===  0  || page)  {

 params +=  `page=${page}&amp;`;

  }

  if(results)  {

 params +=  `results=${results}&amp;`;

  }

  if(gender)  {

 params +=  `gender=${gender}&amp;`;

  }

  if(nationality)  {

 params +=  `nationality=${nationality}`;

  }

  fetch('https://randomuser.me/api/?'  + params)

  .then(function(response)  {

  return response.json();

  })

  .then(function(result)  {

 console.log(result);

  })

  .catch(function(error)  {

 console.log('error', error);

  });

}

getUsers(0,  10,  'male',  'us');
</code></pre><p>在这段代码中，我们通过在getUsers函数中传递各种可选参数来进行API调用。在进行API调用之前，我们添加了各种if条件来检查是否添加了参数，并基于此构造查询字符串，如下所示：</p><pre><code>https://randomuser.me/api/? page=0&amp;results=10&amp;gender=male&amp;nationality=us
</code></pre><p>使用ES6的默认参数则不必添这么多if条件，如下所示:</p><pre><code>function  getUsers(page =  0, results =  10, gender =  'male',nationality =  'us')  {

&nbsp;fetch(`https://randomuser.me/api/?page=${page}&amp;results=${results}&amp;gender=${gender}&amp;nationality=${nationality}`)

&nbsp;.then(function(response)  {

  return response.json();

&nbsp;})

&nbsp;.then(function(result)  {

 console.log(result);

&nbsp;})

&nbsp;.catch(function(error)  {

 console.log('error', error);

  });

}

getUsers();
</code></pre><p>这样一来，代码得到了大量的简化，即便我们不为getUsers函数提供任何参数时，它也能采用默认值。当然，我们也可以传递自己的参数：</p><pre><code>getUsers(1,  20,  'female',  'gb');
</code></pre><p>它将覆盖函数的默认参数。</p><h2 id="null-"><strong>null不等于未定义</strong></h2><p>注意: 定义默认参数时，null和undefined是不同的。</p><p>我们来看下面的代码:</p><pre><code>function  display(name =  'David', age =  35, location =  'NY'){

&nbsp;console.log(name, age, location);

}

display('David',  35);  // David 35 NY

display('David',  35,  undefined);  // David 35 NY

// OR

display('David',  35,  undefined);  // David 35 NY

display('David',  35,  null);  // David 35 null

</code></pre><p>当我们传递null作为参数时，它实际是给location参数赋一个空值，与undefined不一样。所以它不会取默认值“NY”。</p><h4 id="array-prototype-includes"><strong>Array.prototype.includes</strong></h4><p>ES7增加了数组的includes方法，用来判断一个数组是否包含一个指定的值，如果是返回 true，否则false。</p><pre><code>// ES5 Code
const numbers = ["one", "two", "three", "four"];
console.log(numbers.indexOf("one") &gt; -1); // true 
console.log(numbers.indexOf("five") &gt; -1); // false

</code></pre><p>数组可以使用includes方法:</p><pre><code>// ES7 Code

const numbers =  ["one",  "two",  "three",  "four"];

console.log(numbers.includes("one"));  // true

console.log(numbers.includes("five"));  // false
</code></pre><p>includes方法可以使代码简短且易于理解，它也可用于比较不同的值。</p><pre><code>&lt;pre style="line-height:18.0pt;
vertical-align:baseline"&gt;const day =  "monday";&lt;/pre&gt;

if(day ===  "monday"  || day ===  "tuesday"  || day ===  "wednesday")  {

  // do something

}

// 以上代码使用include方法可以简化如下:

const day =  "monday";

if(["monday",  "tuesday",  "wednesday"].includes(day))  {

  // do something

}
</code></pre><p>因此，在检查数组中的值时，使用includes方法将会非常的方便。</p><blockquote>译者注：除了语法更新，开发者还需掌握一些第三方组件，以便更好的完成项目交付。下面介绍一个示例。</blockquote><h2 id="-es6-"><strong>支持</strong> <strong>ES6+ 的第三方组件</strong></h2><p>例如，使用 <a href="https://www.grapecity.com.cn/developer/spreadjs">SpreadJS表格组件</a>，可以向 JavaScript 应用程序添加高级电子表格功能，包括支持 450 多种计算公式、在线导入导出 Excel 文档、数据透视表和可视化分析，以创建财务、分析、预算/预测、数据收集、科学和许多其他类似的应用程序。</p><h4 id="-vue-spreadjs"><strong>如何在 Vue中使用SpreadJS</strong></h4><p>SpreadJS可以通过以下两种方式与Vue一起使用：</p><ol><li>使用Node包管理器：</li></ol><p>打开命令提示符窗口并键入以下命令，使用vue init webpack创建一个简单的Vue项目：</p><pre><code>$ npm install --global vue-cli

# create a new project using the "webpack" template
$ vue init webpack my-project

# install dependencies and go!
$ cd my-project
$ npm run dev         

</code></pre><p>在项目中导入SpreadJS Vue模块:</p><pre><code>$ npm install @grapecity/spread-sheets-vue
</code></pre><p>在Vue应用程序中使用SpreadJS:</p><pre><code>&lt;template&gt;

&lt;div&gt;

 &lt;gc-spread-sheets

 :hostClass='hostClass'

 &gt;

 &lt;gc-worksheet

 :dataSource="dataTable"

 :autoGenerateColumns = 'autoGenerateColumns'

 &gt;

 &lt;gc-column

 :width="width"

 :dataField="'price'"

 :visible = 'visible'

 :formatter = 'formatter'

 :resizable = 'resizable'

 &gt;&lt;/gc-column&gt;

 &lt;/gc-worksheet&gt;

 &lt;/gc-spread-sheets&gt;

 &lt;/div&gt;

&lt;/template&gt;

&lt;script&gt;

import '@grapecity/spread-sheets/styles/gc.spread.sheets.excel2016colorful.css'

import '@grapecity/spread-sheets-vue'

export default {

data(){

  return {

 hostClass:'spread-host',

 autoGenerateColumns:true,

 width:300,

 visible:true,

 resizable:true,

 formatter:"$ #.00"

 }

},

computed:{

 dataTable(){

 let dataTable = [];

  for (let i = 0; i &lt; 42; i++) {

 dataTable.push({price: i + 0.56})

 }

  return dataTable

 }

}

}

&lt;/script&gt;

&lt;style scoped&gt;

.spread-host {

width: 500px;

height: 600px;

}

&lt;/style&gt;
</code></pre><ol><li>使用传统HTML</li></ol><p>创建HTML页面，将SpreadJS和Vue-SpreadJS添加到HTML模板：添加引用 gc.spread.sheets.all.<em>.</em>.<em>.min.js, gc.SpreadJS.</em>.<em>.</em>.css 和 gc.spread.sheets.vue.<em>.</em>.*.js 文件到 HTML 模板 (i.e. 你的 index.html 文件)即可。</p><p><a href="https://www.grapecity.com.cn/developer/spreadjs/vue">点击此处，了解更多 SpreadJS 在Vue的技术资源</a>。</p><h4 id="-react-spreadjs"><strong>如何在 React等框架使用SpreadJS</strong></h4><p>与 Vue 类似，SpreadJS也可以通过使用Node包管理器和传统HTML 两种方式与React一起使用，<a href="https://demo.grapecity.com.cn/spreadjs/help/docs/UsingSpread.SheetswithReact.html">点击此处，了解使用方式。</a></p><h2 id="--1"><strong>结语</strong></h2><p>从ES6开始，JavaScript中发生许多变更。对于JavaScript，Angular，React或Vue开发人员都应该知道它们。</p><p>了解这些变更可以使你成为更棒的开发者，甚至可以帮助您获得更高的薪水。而且，如果你只是在学习React之类的库以及Angular和Vue之类的框架，那么您一定要掌握这些新特性。</p><h2 id="-javascript--1"><strong>学习更多现代 JavaScript 特性</strong></h2><p>你可以通过我的书《<a href="https://modernjavascript.yogeshchavan.dev/">Mastering Modern JavaScript</a>》学习 JavaScript 的所有最新特性。</p><figure class="kg-card kg-image-card"><img src="https://modernjavascript.yogeshchavan.dev/book_cover.jpg" class="kg-image" alt="book_cover" width="600" height="400" loading="lazy"></figure><p>订阅我的<a href="https://bit.ly/2HwVEA2">每周邮件</a>，可以收到更多编程指南。</p><p>原文：<a href="https://www.freecodecamp.org/news/learn-modern-javascript/">Modern JavaScript – Imports, Exports, Let, Const, and Promises in ES6+</a>，作者：<a href="https://www.freecodecamp.org/news/author/yogesh/">Yogesh Chavan</a></p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 详解JavaScript Promise和 Async/Await ]]>
                </title>
                <description>
                    <![CDATA[ 原文：How to Learn JavaScript Promises and Async/Await in 20 Minutes [https://www.freecodecamp.org/news/learn-promise-async-await-in-20-minutes/]，作者： Thu Nghiem [https://www.freecodecamp.org/news/author/thu/] 网络上的很多事情都比较耗时——比如，查询API时，可能需要等一段时间才能获得响应。因此，异步编程对于开发者来说是一项基本技能。 在JavaScript中处理异步操作时，我们经常会听到 "Promise "这个概念。但它的工作原理及使用方法可能会比较抽象和难以理解。 本文与与许多干巴巴的教程不同，将通过实践的方式，帮助你更快速地理解它们的概念和用法。我们将从以下四个示例开始：  * 示例1：用生日解释Promise的基础知识  * 示例2：一个猜数字的游戏  * 示例3：从Web API中获取国家信息  * 示例4：从Web API中获取一个国家的周边国家列表 示例1：用生 ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/javascript-promise-async-await/</link>
                <guid isPermaLink="false">5fd2cf7c39641a0517d51c3b</guid>
                
                    <category>
                        <![CDATA[ JavaScript ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Willie.ji ]]>
                </dc:creator>
                <pubDate>Fri, 11 Dec 2020 02:06:45 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2020/12/maxresdefault.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>原文：<a href="https://www.freecodecamp.org/news/learn-promise-async-await-in-20-minutes/">How to Learn JavaScript Promises and Async/Await in 20 Minutes</a>，作者：<a href="https://www.freecodecamp.org/news/author/thu/">Thu Nghiem</a></p><p>网络上的很多事情都比较耗时——比如，查询API时，可能需要等一段时间才能获得响应。因此，异步编程对于开发者来说是一项基本技能。</p><p>在JavaScript中处理异步操作时，我们经常会听到 "Promise "这个概念。但它的工作原理及使用方法可能会比较抽象和难以理解。</p><p>本文与与许多干巴巴的教程不同，将通过实践的方式，帮助你更快速地理解它们的概念和用法。我们将从以下四个示例开始：</p><ul><li>示例1：用生日解释Promise的基础知识</li><li>示例2：一个猜数字的游戏</li><li>示例3：从Web API中获取国家信息</li><li>示例4：从Web API中获取一个国家的周边国家列表</li></ul><h2 id="-1-promise-">示例1：用生日解释Promise基础知识</h2><p>首先，我们先来看看Promise的基本形态是什么样的。</p><p>Promise执行时分三个状态：pending（执行中）、fulfilled（成功）、rejected（失败）。</p><pre><code>new Promise(function(resolve, reject) {
    if (/* 异步操作成功 */) {
        resolve(value); //将Promise的状态由padding改为fulfilled
    } else {
        reject(error); //将Promise的状态由padding改为rejected
    }
})
</code></pre><p>实现时有三个原型方法then、catch、finally</p><pre><code>promise
.then((result) =&gt; {
	//promise被接收或拒绝继续执行的情况
})
.catch((error) =&gt; {
	//promise被拒绝的情况
})
.finally (() =&gt; {
	//promise完成时，无论如何都会执行的情况
})
</code></pre><p>基本形态介绍完成了，那么我们下面开始看看下面的示例吧。</p><p>用户故事：我的朋友Kayo答应在两周后在我的生日Party上为我做一个蛋糕。</p><p>如果一切顺利且Kayo没有生病的话，我们就会获得一定数量的蛋糕，但如果Kayo生病了，我们就没有蛋糕了。但不论有没有蛋糕，我们仍然会开一个生日Party。</p><p>所以对于这个示例，我们将如上的背景故事翻译成JS代码，首先让我们先创建一个返回Promise的函数。</p><pre><code>const onMyBirthday = (isKayoSick) =&gt; {
  return new Promise((resolve, reject) =&gt; {
    setTimeout(() =&gt; {
      if (!isKayoSick) {
        resolve(2);
      } else {
        reject(new Error("I am sad"));
      }
    }, 2000);
  });
};
</code></pre><p>在JavaScript中，我们可以使用new Promise()创建一个新的Promise，它接受一个参数为：(resolve，reject)=&gt;{} 的函数。</p><p>在此函数中，resolve和reject是默认提供的回调函数。让我们仔细看看上面的代码。</p><p>当我们运行onMyBirthday函数2000ms后。</p><ul><li>如果Kayo没有生病，那么我们就以2为参数执行resolve函数</li><li>如果Kayo生病了，那么我们用new Error("I am sad")作为参数执行reject。尽管您可以将任何要拒绝的内容作为参数传递，但建议将其传递给Error对象。</li></ul><p>现在，因为onMyBirthday()返回的是一个Promise，我们可以访问then、catch和finally方法。我们还可以访问早些时候在then和catch中使用传递给resolve和reject的参数。</p><p>让我们通过如下代码来理解概念</p><p>如果Kayo没有生病</p><pre><code>onMyBirthday(false)
  .then((result) =&gt; {
    console.log(\`I have ${result} cakes\`); // 控制台打印“I have 2 cakes”  
  })
  .catch((error) =&gt; {
    console.log(error); // 不执行
  })
  .finally(() =&gt; {
    console.log("Party"); // 控制台打印“Party”
  });
</code></pre><p>如果Kayo生病</p><pre><code>onMyBirthday(true)
  .then((result) =&gt; {
    console.log(\`I have ${result} cakes\`); // 不执行 
  })
  .catch((error) =&gt; {
    console.log(error); // 控制台打印“我很难过”
  })
  .finally(() =&gt; {
    console.log("Party"); // 控制台打印“Party”
  });
</code></pre><p>相信通过这个例子你能了解Promise的基本概念。</p><p>下面我们开始示例2</p><h2 id="-2-">示例2：一个猜数字的游戏</h2><p>基本需求：</p><ul><li>用户可以输入任意数字</li><li>系统从1到6中随机生成一个数字</li><li>如果用户输入数字等于系统随机数，则给用户2分</li><li>如果用户输入数字与系统随机数相差1，给用户1分，否则，给用户0分</li><li>用户想玩多久就玩多久</li></ul><p>对于上面的需求，我们首先创建一个enterNumber函数并返回一个Promise：</p><pre><code>const enterNumber = () =&gt; {
  return new Promise((resolve, reject) =&gt; {
    // 从这开始编码
  });
};
</code></pre><p>我们要做的第一件事是向用户索要一个数字，并在1到6之间随机选择一个数字：</p><pre><code>const enterNumber = () =&gt; {
  return new Promise((resolve, reject) =&gt; {
    const userNumber = Number(window.prompt("Enter a number (1 - 6):")); // 向用户索要一个数字
    const randomNumber = Math.floor(Math.random() * 6 + 1); // 选择一个从1到6的随机数
  });
};
</code></pre><p>当用户输入一个不是数字的值。这种情况下，我们调用reject函数，并抛出错误：</p><pre><code>const enterNumber = () =&gt; {
  return new Promise((resolve, reject) =&gt; {
    const userNumber = Number(window.prompt("Enter a number (1 - 6):")); // 向用户索要一个数字
    const randomNumber = Math.floor(Math.random() * 6 + 1); //选择一个从1到6的随机数

    if (isNaN(userNumber)) {
      reject(new Error("Wrong Input Type")); // 当用户输入的值非数字，抛出异常并调用reject函数
    }
  });
};
</code></pre><p>下面，我们需要检查userNumber是否等于RanomNumber，如果相等，我们给用户2分，然后我们可以执行resolve函数来传递一个object { points: 2, randomNumber } 对象。</p><p>如果userNumber与randomNumber相差1，那么我们给用户1分。否则，我们给用户0分。</p><pre><code>return new Promise((resolve, reject) =&gt; {
  const userNumber = Number(window.prompt("Enter a number (1 - 6):")); // 向用户索要一个数字
  const randomNumber = Math.floor(Math.random() * 6 + 1); // 选择一个从1到6的随机数

  if (isNaN(userNumber)) {
    reject(new Error("Wrong Input Type")); // 当用户输入的值非数字，抛出异常并调用reject函数
  }

  if (userNumber === randomNumber) {
    // 如果相等，我们给用户2分
    resolve({
      points: 2,
      randomNumber,
    });
  } else if (
    userNumber === randomNumber - 1 ||
    userNumber === randomNumber + 1
  ) {
    // 如果userNumber与randomNumber相差1，那么我们给用户1分
    resolve({
      points: 1,
      randomNumber,
    });
  } else {
    // 否则用户得0分
    resolve({
      points: 0,
      randomNumber,
    });
  }
});
</code></pre><p>下面，让我们再创建一个函数来询问用户是否想继续游戏：</p><pre><code>const continueGame = () =&gt; {
  return new Promise((resolve) =&gt; {
    if (window.confirm("Do you want to continue?")) { // 向用户询问是否要继续游戏
      resolve(true);
    } else {
      resolve(false);
    }
  });
};
</code></pre><p>为了不使游戏强制结束，我们创建的Promise没有使用Reject回调。</p><p>下面，我们创建一个函数来处理猜数字逻辑：</p><pre><code>const handleGuess = () =&gt; {
  enterNumber() // 返回一个Promise对象
    .then((result) =&gt; {
      alert(\`Dice: ${result.randomNumber}: you got ${result.points} points\`); // 当resolve运行时，我们得到用户得分和随机数 
      
      // 向用户询问是否要继续游戏
      continueGame().then((result) =&gt; {
        if (result) {
          handleGuess(); // If yes, 游戏继续
        } else {
          alert("Game ends"); // If no, 弹出游戏结束框
        }
      });
    })
    .catch((error) =&gt; alert(error));
};

handleGuess(); // 执行handleGuess 函数
</code></pre><p>在这当我们调用handleGuess函数时，enterNumber()返回一个Promise对象。</p><p>如果Promise状态为resolved，我们就调用then方法，向用户告知竞猜结果与得分，并向用户询问是否要继续游戏。</p><p>如果Promise状态为rejected，我们将显示一条用户输入错误的信息。</p><p>不过，这样的代码虽然能解决问题，但读起来还是有点困难。让我们后面将使用async/await 对hanldeGuess进行重构。</p><p>网上对于 async/await 的解释已经很多了，在这我想用一个简单概括的说法来解释：<strong>async/await就是可以把复杂难懂的异步代码变成类同步语法的语法糖</strong>。</p><p>下面开始看重构后代码吧：</p><pre><code>const handleGuess = async () =&gt; {
  try {
    const result = await enterNumber(); // 代替then方法，我们只需将await放在promise前，就可以直接获得结果

    alert(\`Dice: ${result.randomNumber}: you got ${result.points} points\`);

    const isContinuing = await continueGame();

    if (isContinuing) {
      handleGuess();
    } else {
      alert("Game ends");
    }
  } catch (error) { // catch 方法可以由try, catch函数来替代
    alert(error);
  }
};
</code></pre><p>通过在函数前使用async关键字，我们创建了一个异步函数，在函数内的使用方法较之前有如下不同：</p><ul><li>和then函数不同，我们只需将await关键字放在Promise前，就可以直接获得结果。</li><li>我们可以使用try, catch语法来代替promise中的catch方法。</li></ul><p>下面是我们重构后的完整代码，供参考：</p><pre><code>const enterNumber = () =&gt; {
  return new Promise((resolve, reject) =&gt; {
    const userNumber = Number(window.prompt("Enter a number (1 - 6):")); // 向用户索要一个数字
    const randomNumber = Math.floor(Math.random() * 6 + 1); // 系统随机选取一个1-6的数字

    if (isNaN(userNumber)) {
      reject(new Error("Wrong Input Type")); // 如果用户输入非数字抛出错误
    }

    if (userNumber === randomNumber) { // 如果用户猜数字正确，给用户2分
      resolve({
        points: 2,
        randomNumber,
      });
    } else if (
      userNumber === randomNumber - 1 ||
      userNumber === randomNumber + 1
    ) { // 如果userNumber与randomNumber相差1，那么我们给用户1分
      resolve({
        points: 1,
        randomNumber,
      });
    } else { // 不正确，得0分
      resolve({
        points: 0,
        randomNumber,
      });
    }
  });
};

const continueGame = () =&gt; {
  return new Promise((resolve) =&gt; {
    if (window.confirm("Do you want to continue?")) { // 向用户询问是否要继续游戏
      resolve(true);
    } else {
      resolve(false);
    }
  });
};

const handleGuess = async () =&gt; {
  try {
    const result = await enterNumber(); // await替代了then函数

    alert(\`Dice: ${result.randomNumber}: you got ${result.points} points\`);

    const isContinuing = await continueGame();

    if (isContinuing) {
      handleGuess();
    } else {
      alert("Game ends");
    }
  } catch (error) { // catch 方法可以由try, catch函数来替代
    alert(error);
  }
};

handleGuess(); // 执行handleGuess 函数
</code></pre><p>我们已经完成了第二个示例，接下来让我们开始看看第三个示例。</p><h2 id="-3-web-api-">示例3：从Web API中获取国家信息</h2><p>一般当从API中获取数据时，开发人员会精彩使用Promises。如果在新窗口打开https://restcountries.eu/rest/v2/alpha/cn，你会看到JSON格式的国家数据。</p><p>通过使用<a href="https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch">Fetch API</a>，我们可以很轻松的获得数据，以下是代码：</p><pre><code>const fetchData = async () =&gt; {
  const res = await fetch("https://restcountries.eu/rest/v2/alpha/cn"); // fetch() returns a promise, so we need to wait for it

  const country = await res.json(); // res is now only an HTTP response, so we need to call res.json()

  console.log(country); // China's data will be logged to the dev console
};

fetchData();
</code></pre><p>现在我们获得了所需的国家/地区数据，让我们转到最后一项任务。</p><h2 id="-4-web-api-">示例4：从Web API中获取一个国家的周边国家列表</h2><p>下面的fetchCountry函数从示例3中的api获得国家信息，其中的参数alpha3Code 是代指该国家的国家代码，以下是代码</p><pre><code>// Task 4: 获得中国周边的邻国信息
const fetchCountry = async (alpha3Code) =&gt; {
  try {
    const res = await fetch(
      \`https://restcountries.eu/rest/v2/alpha/${alpha3Code}\`
    );

    const data = await res.json();

    return data;
  } catch (error) {
    console.log(error);
  }
};
</code></pre><p>下面让我们创建一个fetchCountryAndNeighbors函数，通过传递cn作为alpha3code来获取中国的信息。</p><pre><code>const fetchCountryAndNeighbors = async () =&gt; {
  const china= await fetchCountry("cn");

  console.log(china);
};

fetchCountryAndNeighbors();
</code></pre><p>在控制台中，我们看看对象内容：</p><figure class="kg-card kg-image-card"><img src="https://gcdn.grapecity.com.cn/data/attachment/forum/202012/11/093002lp4pfav8x8e0e558.png" class="kg-image" alt="093002lp4pfav8x8e0e558" width="600" height="400" loading="lazy"></figure><p>在对象中，有一个border属性，它是中国周边邻国的alpha3codes列表。</p><p>现在，如果我们尝试通过以下方式获取邻国信息。</p><pre><code>const neighbors = 
    china.borders.map((border) =&gt; fetchCountry(border));
</code></pre><p>neighbors是一个Promise对象的数组。</p><p>当处理一个数组的Promise时，我们需要使用Promise.all。</p><pre><code>const fetchCountryAndNeigbors = async () =&gt; {
  const china = await fetchCountry("cn");

  const neighbors = await Promise.all(
    china.borders.map((border) =&gt; fetchCountry(border))
  );

  console.log(neighbors);
};

fetchCountryAndNeigbors();
</code></pre><p>在控制台中，我们应该能够看到国家/地区对象列表。</p><figure class="kg-card kg-image-card"><img src="https://gcdn.grapecity.com.cn/data/attachment/forum/202012/11/093009akqh0zfm5zxh9sw1.png" class="kg-image" alt="093009akqh0zfm5zxh9sw1" width="600" height="400" loading="lazy"></figure><p>以下是示例4的所有代码，供您参考：</p><pre><code>const fetchCountry = async (alpha3Code) =&gt; {
  try {
    const res = await fetch(
      \`https://restcountries.eu/rest/v2/alpha/${alpha3Code}\`
    );
    const data = await res.json();
    return data;
  } catch (error) {
    console.log(error);
  }
};

const fetchCountryAndNeigbors = async () =&gt; {
  const china = await fetchCountry("cn");
  const neighbors = await Promise.all(
    china.borders.map((border) =&gt; fetchCountry(border))
  );
  console.log(neighbors);
};

fetchCountryAndNeigbors();
</code></pre><h2 id="-">总结</h2><p>完成这 4 项任务后，你就会发现 Promise 在处理异步操作时非常有用。</p><p>在我的一个教程中，我们使用 React 和 Next.js 从零开始构建了一个应用程序：</p><figure class="kg-card kg-embed-card" data-test-label="fitted">
        <div class="fluid-width-video-container">
          <div style="padding-top: 56.49999999999999%;" class="fluid-width-video-wrapper">
            <iframe width="200" height="113" src="https://www.youtube.com/embed/v8o9iJU5hEA?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen="" title="Full Project-based Tutorial - React + Next.js" name="fitvid0"></iframe>
          </div>
        </div>
      </figure><p><strong>__________ 🐣 关于我 __________</strong></p><ul><li>我是 <a href="https://devchallenges.io/">DevChallenges</a> 的创始人</li><li>订阅我的 <a href="https://www.youtube.com/channel/UCmSmLukBF--YrKZ2g4akYAQ?sub_confirmation=1">YouTube 频道</a></li><li>在 <a href="https://twitter.com/thunghiemdinh">Twitter</a> 关注我</li><li>加入 <a href="https://discord.com/invite/3R6vFeM">Discord</a></li></ul><p></p><blockquote>译者注：（扩展阅读）点击此处，了解一款功能布局与 Excel 高度类似的<a href="https://www.grapecity.com.cn/developer/spreadjs">纯前端表格控件 SpreadJS</a>。</blockquote><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2020/12/2020-05-27_160028.jpg" class="kg-image" alt="2020-05-27_160028" width="600" height="400" loading="lazy"></figure> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 三步带你开发一个短链接生成平台 ]]>
                </title>
                <description>
                    <![CDATA[ 前段时间在开发【葡萄城社区】公众号时有一个功能是需要用网页授权认证地址生成二维码，但类似像下面这样的网址url即便是看也觉得很头疼了。 用这个地址生成的二维码也是密密麻麻，虽不影响微信长按扫码，一旦二维码尺寸缩一点点，图片马上就会糊掉，导致摄像头直接扫码会难以识别。 那这种情况下， 我们自然就会想到如果使用短链接减少url的字符，生成的码自然就会变得容易识别了，同时还会使url更美观且易于转发。现在市面上可用的就是微博的t.cn和一些第三方的生成短链接工具，但这两类工具都有一些使用上的问题，例如：t.cn现在的规则是会出现一个中转页不会直接跳转，而第三方的工具因为是一个公共平台，有时可能会因一些不良信息导致整个平台无法访问。 那与其这样，不如我们自己来实现一个短链接平台吧，实现一个短链接平台原理上也非常简单，搞定两部分就行了：1.保存长短链接的对应关系。2.通过短链接查询长连接并重定向。 为了高效，我这使用的是node和mongodb，下面我们就来开始动手吧。 首先，我们先创建一个express工程： express -e demo    change directory ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/shorter-url/</link>
                <guid isPermaLink="false">5f6d4a1f027c3105323f551e</guid>
                
                    <category>
                        <![CDATA[ URL ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Node.js ]]>
                    </category>
                
                    <category>
                        <![CDATA[ MongoDB ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Willie.ji ]]>
                </dc:creator>
                <pubDate>Fri, 25 Sep 2020 09:22:20 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2020/09/radek-grzybowski-eBRTYyjwpRY-unsplash.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>前段时间在开发【葡萄城社区】公众号时有一个功能是需要用网页授权认证地址生成二维码，但类似像下面这样的网址url即便是看也觉得很头疼了。</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2020/09/image-9.png" class="kg-image" alt="image-9" width="600" height="400" loading="lazy"></figure><p>用这个地址生成的二维码也是密密麻麻，虽不影响微信长按扫码，一旦二维码尺寸缩一点点，图片马上就会糊掉，导致摄像头直接扫码会难以识别。</p><p>那这种情况下， 我们自然就会想到如果使用短链接减少url的字符，生成的码自然就会变得容易识别了，同时还会使url更美观且易于转发。现在市面上可用的就是微博的t.cn和一些第三方的生成短链接工具，但这两类工具都有一些使用上的问题，例如：t.cn现在的规则是会出现一个中转页不会直接跳转，而第三方的工具因为是一个公共平台，有时可能会因一些不良信息导致整个平台无法访问。</p><p>那与其这样，不如我们自己来实现一个短链接平台吧，实现一个短链接平台原理上也非常简单，搞定两部分就行了：1.保存长短链接的对应关系。2.通过短链接查询长连接并重定向。</p><p>为了高效，我这使用的是node和mongodb，下面我们就来开始动手吧。</p><p>首先，我们先创建一个express工程：</p><!--kg-card-begin: markdown--><pre><code>express -e demo

   change directory:
     &gt; cd demo

   install dependencies:
     &gt; npm install

   run the app:
     &gt; SET DEBUG=demo:* &amp; npm start
</code></pre>
<!--kg-card-end: markdown--><p>然后进入demo目录并安装express必要依赖：</p><!--kg-card-begin: markdown--><pre><code>npm install
</code></pre>
<!--kg-card-end: markdown--><p>同时通过 <code>npm</code> 安装我们需要用到的 <code>mongoose</code> 和 <code>shortid</code> 和 <code>body-parser</code>：</p><!--kg-card-begin: markdown--><pre><code>npm install mongoose
npm install shortid
npm install body-parser
</code></pre>
<!--kg-card-end: markdown--><p>下面分别对使用到的这三个包简单说明一下：</p><ul><li>在这个应用中，我们使用了 mongodb，之所以选择它是因为执行高效且低开销，所以执行起来也很高效，不过如果使用其他数据库也是没问题的。这里的<strong>mongoose</strong>就是npm的一个包，主要是为程序提供连接mongodb并增删查改的功能。</li><li>通过使用<strong>shortid</strong>可以生成一个指定字符不重复的编码，便于我们生成类似xxx.com/ngTsfdgh 类似红字部分的编码。</li><li>由于我们生成短链接部分的api使用的是post方法，使用<strong>body-parser</strong>可以多扩展一种body编码类型解析能力。</li></ul><h2 id="-mongodb-">首先设置MongoDB的连接信息</h2><!--kg-card-begin: markdown--><pre><code>module.exports = {
    mongo_base_connection_url: 'mongodb://localhost:27017',
    mongo_db: 'mongodb://localhost:27017/shorturl',
    mongo_options: {
        autoReconnect: true,
        keepAlive: true,
        reconnectTries: 50,
        reconnectInterval: 1000,
        connectTimeoutMS: 5000000,
    }
}
console.log("Connection local mongo db");
</code></pre>
<!--kg-card-end: markdown--><h2 id="-">数据库模型定义</h2><p>因为我们的对应关系是需要通过短链接查询长连接，所以这里我们主要以存储短链接和长连接为主，另外大家也可以根据自己需要添加链接点击统计之类的字段，方便后期统计。</p><!--kg-card-begin: markdown--><pre><code>var mongoose = require('mongoose');
var Schema = mongoose.Schema;
 
var urlSchema = new Schema({
  shortUrl: String,
  longUrl: String
});
 
module.exports = mongoose.model('UrlTable', urlSchema);
</code></pre>
<!--kg-card-end: markdown--><h2 id="-express-">定义express路由</h2><p>因为这个应用我们只有生成和Redirect两个功能，所以这里只有两个页面即可完成所有工作。</p><!--kg-card-begin: markdown--><pre><code>var index = require('./routes/index');
var url = require('./routes/url');
app.use('/', index);
app.use('/url', url);
</code></pre>
<!--kg-card-end: markdown--><h4 id="--1">生成短链接页面</h4><!--kg-card-begin: markdown--><pre><code>const express = require("express");
const router = express.Router();
const shortId = require('shortid');
const UrlTable = require('../models/urltable');
const mongoose = require('mongoose');
var setup = require('../dbconfig/db');
 
router.post('/', function(req, res, next) {
    var params = req.body;
    var url = params.longUrl;
shortId.characters(' 0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ^*')
    var shortid = shortId.generate();
    var objurl = { shortUrl: shortid, longUrl: url};
    mongoose.connect(setup.mongo_db, setup.mongo_options);
    UrlTable.create(objurl, function (err, objurl) {
      //if (err) console.log(err);
      res.send("http://localhost:3000/" + shortid);
    });
    return;
});
</code></pre>
<!--kg-card-end: markdown--><p>指定生成 <code>shortId</code> 字符的范围并生成：</p><!--kg-card-begin: markdown--><pre><code>shortId.characters('0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ')

var shortid = shortId.generate();
</code></pre>
<!--kg-card-end: markdown--><p>为数据库构建符合要求的数据模型：</p><!--kg-card-begin: markdown--><pre><code>var objurl = { shortUrl: shortid, longUrl: url};
</code></pre>
<!--kg-card-end: markdown--><p>最后，连接数据库并保存后将短链接结果返回客户端：</p><!--kg-card-begin: markdown--><pre><code>mongoose.connect(setup.mongo_db, setup.mongo_options);
UrlTable.create(objurl, function (err, objurl) {
//if (err) console.log(err);
res.send("http://localhost:3000/" + shortid);
});
</code></pre>
<!--kg-card-end: markdown--><h4 id="--2">短链接跳转页面</h4><!--kg-card-begin: markdown--><pre><code>const express = require("express");
const router = express.Router();
const UrlTable = require('../models/urltable');
const mongoose = require('mongoose');
var setup = require('../dbconfig/db');
 
router.get('/:shortUrl', function (req, res, next) {
    var shortUrl = req.params.shortUrl;
    mongoose.connect(setup.mongo_db, setup.mongo_options);
    UrlTable.findOne({ shortUrl:shortUrl }).then((result) =&gt; {
       //待添加错误处理
    res.redirect(result.longUrl);
    })
});
 
module.exports = router;
</code></pre>
<!--kg-card-end: markdown--><p>这个页面为了便于快速跳转，我们就使用get接收参数，这个页面功能就很简单了，接参查询并跳转。</p><p>接收短链接码：</p><!--kg-card-begin: markdown--><pre><code>var shortUrl = req.params.shortUrl;
</code></pre>
<!--kg-card-end: markdown--><p>连接数据库查询并跳转：</p><!--kg-card-begin: markdown--><pre><code>mongoose.connect(setup.mongo_db, setup.mongo_options);

    UrlTable.findOne({ shortUrl:shortUrl }).then((result) =&gt; {

           //待添加错误处理

      res.redirect(result.longUrl);

 })
</code></pre>
<!--kg-card-end: markdown--><p>后期大家可以对一些错误异常处理，数据统计等做一些增强，这里就不做补充了。</p><p>下面让我们启用应用开始测试吧。</p><h2 id="--3">启动应用并测试</h2><!--kg-card-begin: markdown--><pre><code>npm start
</code></pre>
<!--kg-card-end: markdown--><p>启动后，默认的访问端口为3000，我们首先测试下短链接生成页，这里我们post一个名为longUrl的长链接参数，数据对象为：</p><p>{"longUrl" : "<a href="https://demo.grapecity.com.cn/spreadjs/SpreadJSTutorial/features/tables/basic-table/purejs">https://demo.grapecity.com.cn/spreadjs/SpreadJSTutorial/features/tables/basic-table/purejs</a>"}</p><figure class="kg-card kg-image-card"><img src="https://i.v2ex.co/Kne07O7El.gif" class="kg-image" alt="Kne07O7El" width="600" height="400" loading="lazy"></figure><p>成功升成了如下短链接：</p><p><a href="http://localhost:3000/iGE6ZlDmh">http://localhost:3000/iGE6ZlDmh</a></p><p>我们只要通过访问短链接能正常跳转至保存的长连接即可。</p><figure class="kg-card kg-image-card"><img src="https://i.v2ex.co/mcZ74Zv2l.gif" class="kg-image" alt="mcZ74Zv2l" width="600" height="400" loading="lazy"></figure><p>这样就测试通过了，其实代码量不大，原理也很简单。大家如果自己有较短的域名的话，上线后会让链接变得更短、更美观，这样一个属于我们自己短链接生成平台就开发完成了。下面附上源码，执行npm install 即可自动安装所有依赖，如果大家有问题，可通过评论区告诉我。</p><p><a href="https://files.cnblogs.com/files/powertoolsteam/shorturl.zip">源码下载&gt;&gt;</a></p><p>我们在<a href="https://www.grapecity.com.cn/">葡萄城官网</a>发布更多文章，欢迎阅读！</p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 初探Electron，从入门到实践 ]]>
                </title>
                <description>
                    <![CDATA[ 在开始学习Electron之前，你一定有这样的困惑：Electron 是什么？Electron能做什么？许多伟大的公司使用Electron框架的原因又是什么？ 带着这些问题和疑惑，通过本文的介绍，可助你全面地认识Electron这门新兴的技术，迅速找到其入门途径，并理解Electron为何被称为当下开发桌面App的最佳选择。 初探Electron 一、Electron是什么？（为何称之为“跨平台桌面浏览器”） 前端开发的魅力，在于开发者随时要面临全新技术的挑战！ 曾几何时，作为前端开发者的你可曾想过：如何利用HTML、CSS和JavaScript构建跨平台的桌面应用程序？借助 Electron，这项工作将比你想象的更加简单。 Electron作为一个使用新兴技术（包括JavaScript，HTML和CSS）创建桌面应用程序的框架，其负责处理硬件，开发者可以更专注于应用程序的核心并从底层更改其设计。 Electron设计之初便充分结合了当今最好的Web技术，作为一个跨平台的“集成框架”，它可以轻松地与Mac、Windows和Linux兼容。而所谓的“集成框架”也就是它将“Chr ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/spreadjs-electron-from-initiation-to-practice/</link>
                <guid isPermaLink="false">5eb602e8db4be8080eb70a4a</guid>
                
                <dc:creator>
                    <![CDATA[ Willie.ji ]]>
                </dc:creator>
                <pubDate>Thu, 14 May 2020 02:20:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2020/05/d0f4b09c1ca1af4a9a79787f33229542.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>在开始学习Electron之前，你一定有这样的困惑：Electron 是什么？Electron能做什么？许多伟大的公司使用Electron框架的原因又是什么？</p><p>带着这些问题和疑惑，通过本文的介绍，可助你全面地认识Electron这门新兴的技术，迅速找到其入门途径，并理解Electron为何被称为当下开发桌面App的最佳选择。</p><h2 id="-electron">初探Electron</h2><h3 id="-electron-">一、Electron是什么？（为何称之为“跨平台桌面浏览器”）</h3><p>前端开发的魅力，在于开发者随时要面临全新技术的挑战！</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2020/05/image-5.png" class="kg-image" alt="image-5" width="576" height="281" loading="lazy"></figure><p>曾几何时，作为前端开发者的你可曾想过：如何利用HTML、CSS和JavaScript构建跨平台的桌面应用程序？借助 Electron，这项工作将比你想象的更加简单。</p><p>Electron作为一个使用新兴技术（包括JavaScript，HTML和CSS）创建桌面应用程序的框架，其负责处理硬件，开发者可以更专注于应用程序的核心并从底层更改其设计。</p><p>Electron设计之初便充分结合了当今最好的Web技术，作为一个跨平台的“集成框架”，它可以轻松地与Mac、Windows和Linux兼容。而所谓的“集成框架”也就是它将“Chromium”和“Node.js”很好的集成在了一起，并明确分工，Electron负责硬件部分，“Chromium”和“Node.js”负责界面与逻辑，大家井井有条，共同构成了一个成本低廉却十分高效的解决方案，在快速交付上甚至比Native还要快速。</p><h4 id="electron-">Electron发展里程碑</h4><p></p><ul><li>2013年4月11日，Electron以Atom Shell为名起步。</li><li>2014年5月6日，Atom以及Atom Shell以MIT许可证开源。</li><li>2015年4月17日，Atom Shell改名为Electron。</li><li>2016年5月11日，1.0版本发布。</li><li>2016年5月20日，允许向Mac应用商店提交软件包。</li><li>2016年8月2日，支持Windows商店。</li></ul><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2020/05/image-6.png" class="kg-image" alt="image-6" width="576" height="264" loading="lazy"></figure><p>‌‌简而言之，Electron JS是一个运行时框架，它允许用户使用HTML5、CSS和JavaScript创建桌面套件应用程序，而大部分应用程序都是由两种非常受欢迎的技术混合而成：Node.js和Chromium。因此，你编写的任何Web应用程序都可以在Electron JS 上正常运行。</p><h4 id="electron--1">Electron的内置功能</h4><ul><li>自动更新 - 使应用程序能够自动更新、升级</li><li>本机菜单和通知 - 创建本机应用程序菜单和上下文菜单</li><li>应用程序崩溃报告 - 你可以将崩溃报告提交给远程服务器</li><li>调试和分析 - Chromium的内容模块可以发现性能瓶颈和运行缓慢的原因。此外，你也可以在应用中使用自己喜欢的Chrome开发者工具</li><li>Windows安装程序 -你可以快速而简单创建安装包</li></ul><h3 id="-electron-electron-">二、Electron 可以用来做什么？（哪些场景需要使用Electron）‌‌</h3><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2020/05/image-7.png" class="kg-image" alt="image-7" width="576" height="398" loading="lazy"></figure><p>‌‌以Windows平台应用开发为例，大部分人首先会想到使用成熟的开发方案，如QT(C++)、WPF(C#) 等。但面临以下几种使用场景，这些方案将显得捉襟见肘：</p><ul><li>公司要设计一个全新的APP， 但技术人员大部分由前端开发构成</li><li>公司原本就有在线的Web应用，但是想让该应用能够在桌面端直接打开（离线状态下也可使用），并增加一些与系统交互的功能</li></ul><p>以我的亲身经历为例：</p><p>在SpreadJS项目中，我们需要将基于web版的表格编辑器封装成APP使用，同时增加文件操作的能力，如导入导出excel、导入PDF等，而SpreadJS是一个纯前端的表格控件，开发人员全部由前端开发组成，对C++和C#并不熟悉，如果投入过大的时间精力用来学习，整个项目的技术管理和项目管理将变得无法控制。除此之外，鉴于项目本身对应用的业务逻辑要求并不高，只是套一个具有浏览器属性的运行环境即可，因此，单独为此配置C++、C# 开发人员将无形中提升更多项目成本。</p><p>为此，我们引入了Electron框架：现有的前端开发人员能在不学习其他语言的情况下，直接搞定上述需求，这就是Electron 为我们带来的价值。</p><h3 id="-electron-electron--1">三、为什么选择 Electron？（Electron的出现为前端开发者谋得了一份好差事）</h3><p>可以这么说，Electron这个框架让网路里流传很广的一句话不再是玩笑：“不要和老夫说什么C++、Java，老夫行走江湖就一把JS，遇到需求撸起袖子就是干”。Electron可以帮助前端开发者在不需要学习其他语言和技能的情况下，快速开发跨平台桌面应用。‌‌</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2020/05/image-8.png" class="kg-image" alt="image-8" width="576" height="302" loading="lazy"></figure><p>‌‌</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2020/05/image-9.png" class="kg-image" alt="image-9" width="576" height="281" loading="lazy"></figure><p>‌‌Electron的出现将蚕食很大一部分桌面客户端领域的市场份额，鉴于它的跨平台特性，在不同系统之间仅需少量的优化工作。可想而知，这个成本到底有多低。</p><p>在开发的体验上，Electron是基于"Chromium"和"Node.js"的，所以几乎所有的Node.js模块都可以在Electron上运行，并很容易使用“npm”搭积木的方式快速交付一个产品。</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2020/05/image-10.png" class="kg-image" alt="image-10" width="576" height="267" loading="lazy"></figure><h3 id="-electron--1">四、使用Electron框架的大型应用成功案例</h3><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2020/05/image-11.png" class="kg-image" alt="image-11" width="576" height="260" loading="lazy"></figure><p><strong>1. SpreadJS纯前端表格控件</strong></p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2020/05/image-12.png" class="kg-image" alt="image-12" width="576" height="374" loading="lazy"></figure><p>‌‌SpreadJS 是一款基于 HTML5 的纯前端电子表格控件，兼容 450 种以上的 Excel 公式，可满足 Web Excel 组件开发、表格文档协同编辑、数据填报、Excel 类报表设计等业务场景，从而降低企业研发成本和项目交付风险。</p><p><strong>2. WebTorrent</strong></p><p>WebTorrent，作为第一个在浏览器中运行的torrent客户端，是一个完全由JavaScript编写并使用WebRTC进行点对点传输的客户端应用。无需任何插件，扩展或安装，WebTorrent将用户链接到分散的浏览器到浏览器网络，以确保有效的文件传输。</p><p>WebTorrent使用Electron框架开发，使其尽可能轻量、无广告且开源。此外，使用Electron还有助于流式传输，并充当混合客户端，将应用程序连接到所有流行BitTorrent和WebTorrent网络。</p><p><strong>3. WordPress</strong></p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2020/05/image-13.png" class="kg-image" alt="image-13" width="576" height="360" loading="lazy"></figure><p>‌‌WordPress 桌面是一个使用了Electron和React作为框架的桌面应用程序，提供无缝的跨平台体验，允许用户专注于他们的内容和设计，而不会被任何浏览器标签所分心。</p><p><strong>4. Slack</strong>‌‌</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2020/05/image-14.png" class="kg-image" alt="image-14" width="576" height="324" loading="lazy"></figure><p>Slack采用了Electron框架构建，鉴于其高性能表现和无框架外观，将带来与浏览器完全不同的体验方式。对于寻求更集中的工作空间的团队来说，Slack Desktop绝对是最适合的应用程序之一。</p><p>虽然Slack Desktop融合了很多技术，但大多数资源文件和代码都是远程加载的，它们结合了Chromium的渲染引擎和Node.js运行时和模块系统。</p><p><strong>5. WhatsApp</strong></p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2020/05/image-15.png" class="kg-image" alt="image-15" width="576" height="363" loading="lazy"></figure><p>WhatsApp作为下载量最高的Messenger应用程序，也是基于Electron框架构建的。Electron帮助WhatsApp开发人员以低廉的成本完成了几乎所有工作，并通过更加简化和创新的技术，为用户带来全新的桌面体验方式。</p><h2 id="electron--2">Electron 架构实现‌‌</h2><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2020/05/image-16.png" class="kg-image" alt="image-16" width="576" height="353" loading="lazy"></figure><h3 id="-electron--2">‌‌Electron基本文件结构</h3><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2020/05/image-17.png" class="kg-image" alt="image-17" width="576" height="285" loading="lazy"></figure><p>‌‌Electron有一个基本的文件结构，类似于我们在创建网页时使用的文件结构：</p><p>electron-quick-start</p><ul><li>index.html 这是一个HTML5网页，目的用于提供画布（canvas）</li><li>main.js 创建窗口并处理系统事件</li><li>package.json 是我们应用程序的启动脚本。它将在主进程中运行，并包含有关应用程序的所有信息</li><li>render.js 处理应用程序的渲染过程</li></ul><h4 id="electron--3">Electron的架构主要分为两部分：主进程和渲染进程</h4><p>回顾以往的web开发，我们的代码，无论是HTML、CSS还是Javascript，都是运行在浏览器沙盒中的，我们无法越过浏览器的权限访问系统本身的资源，代码的能力被限制在了浏览器中。浏览器之所以这么做，是为了安全的考虑。设想一下，我们在使用浏览器的时候，会打开各式各样不同来源的网站，如果JavaScript代码有能力访问并操作本地操作系统的资源，那将是多么可怕的事情。</p><p><em>假设：你在某天不小心打开了一个恶意的网站，可能你存储在硬盘上的文件就被偷走了（都用不着去修电脑）。</em></p><p>但我们要开发的是桌面应用程序，如果无法访问到本地的资源肯定是不行的。Electron将nodejs巧妙的融合了进来，让nodejs作为整个程序的管家。管家拥有较高的权限，可以访问和操作本地资源，使用原本在浏览器中不提供的高级API。同时管家也管理着渲染进程窗口的创建和销毁。所以，我们将这个管家称之为主进程。在使用Electron开发的程序中，会使用main.js作为程序的主入口，该文件内代码执行的内容，就是主进程中执行的内容。</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2020/05/image-18.png" class="kg-image" alt="image-18" width="576" height="284" loading="lazy"></figure><h3 id="-">‌‌主进程‌‌</h3><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2020/05/image-19.png" class="kg-image" alt="image-19" width="576" height="245" loading="lazy"></figure><p>‌‌主进程控制应用程序的生命周期。Electron 用来运行 package.json 的 main 脚本的进程被称为主进程。 在主进程中运行的脚本通过创建web页面来展示用户界面。它内置了完整的Node.js API，主要用于打开对话框以及创建渲染进程。此外，主进程还负责处理与其他操作系统交互、启动和退出应用程序。</p><p>主进程就像是应用程序的管家，负责管理整个应用程序的生命周期以及所有渲染进程的创建。</p><p>按照惯例，主进程位于名为main.js的文件中，你可以通过在package.json文件中修改配置属性来更改主进程文件。</p><p>比如，我们可以打开package.json并更改配置属性：</p><pre><code>“main”: “main.js”,      -&gt;   “main”: “mainTest.js”,</code></pre><p>请注意，Electron有且只有一个主进程。且主进程销毁时，所有渲染进程也将一并销毁。在chrome浏览器的默认策略下，每一个tab都是独立的进程，Electron也正是利用了这一策略。</p><h4 id="--1">渲染进程</h4><p>渲染进程是应用程序中的浏览器窗口。与主进程不同，Electron可以有许多渲染进程，且每个进程都是独立的。由于 Electron 使用了 Chromium 来展示web 页面，所以 Chromium 的多进程架构也被使用到。 每个Electron中的 web 页面运行在它自己的渲染进程中。</p><p>正是因为每个渲染进程都是独立的，因此一个崩溃不会影响另外一个，这些要归功于Chromium的多进程架构。</p><h4 id="--2">如何保持进程通信？</h4><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2020/05/image-20.png" class="kg-image" alt="image-20" width="576" height="324" loading="lazy"></figure><p>即便Electron中的所有进程同时存在并保持独立运行，但他们仍然需要以某种方式进行沟通，尤其是在他们负责不同任务的时候。</p><p>为了保持进程通信，Electron有一个进程间通信系统（IPC也就是内部进程通信）。你可以使用IPC在主进程和渲染进程之间传递信息。</p><pre><code>// 在主进程中
global.sharedObject = {
someProperty: 'default value'
}Copy
// 在第一个页面中
require('electron').remote.getGlobal('sharedObject').someProperty= 'new value'Copy
// 在第二个页面中
console.log(require('electron').remote.getGlobal('sharedObject').someProperty)</code></pre><p>Electron 进程通信的实现方式：</p><ul><li>主进程使用 BrowserWindow 实例创建页面。每个 BrowserWindow 实例都在自己的渲染进程里运行页面。 当一个BrowserWindow 实例被销毁后，相应的渲染进程也会被终止。</li><li>主进程管理所有的web页面和它们对应的渲染进程。 每个渲染进程都是独立的，它只关心它所运行的 web页面。</li><li>在页面中调用与 GUI 相关的原生 API 是不被允许的，因为在 web 页面里操作原生的GUI 资源是非常危险的，而且容易造成资源泄露。 如果你想在 web 页面里使</li></ul><p>用 GUI 操作，其对应的渲染进程必须与主进程进行通讯，请求主进程进行相关的 GUI 操作。</p><p>说句题外话：在两个网页（渲染进程）间共享数据最简单的方法是使用浏览器中已经实现的 HTML5 API。 其中比较好的方案是用 Storage API， localStorage，sessionStorage 或者 IndexedDB，但这些不是今天的主题。</p><h4 id="-electron--3">如何构建 Electron系统架构？</h4><p>为了降低构建整个 Chromium 带来的复杂度，Electron通过libchromiumcontent 来访问 Chromium 的Content API。libchromiumcontent 是一个独立的、引入了 Chromium Content 模块及其所有依赖的共享库。用户不需要一个强劲的机器来构建Electron。</p><p>Electron只用了Chromium的渲染库而不是其全部组件。这使得升Chromium更加容易，但也意味着Electron缺少了Google Chrome里的一些浏览器相关的特性。</p><h4 id="--3">打包</h4><p>原来打包步骤略微繁琐，如今由于社区发展，产生了很多优秀的打包工具，让我们可以不用关注很多细节，（比如asar）</p><pre><code>// 在主进程中
global.sharedObject = {
someProperty: 'default value'
}Copy
// 在第一个页面中
require('electron').remote.getGlobal('sharedObject').someProperty= 'new value'Copy
// 在第二个页面中
console.log(require('electron').remote.getGlobal('sharedObject').someProperty)
main 端
ipcMain.on('readFile', (event, { filePath })=&gt; {
content content = fs.readFileSync(filePath,'utf-8');
event.sender.send('readFileSuccess', { content});
});
renderer 端
ipcRenderer.on('readFileSuccess', (event, {content }) =&gt; {
console.log(`content: ${content}`);
});
ipcRender.send('readFile', {
filePath: '/path/to/file',
});</code></pre><p>我们仅需做的 ：将app 的目录结构整理好，提供对应的资源，如icon等，然后使用工具制作镜像即可将资源打包成为各个平台下的APP应用。</p><p>打包工具的选择‌‌</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2020/05/image-21.png" class="kg-image" alt="image-21" width="576" height="251" loading="lazy"></figure><p>‌‌通常情况下，我们选择Electron-builder （跨平台支持性较好，上手成本低）</p><h4 id="electron--4">Electron 快速上手实践</h4><p>这里我们将以SpreadJS的一个应用为例，展示如何将Web应用转换为Electron桌面应用，在线观看地址：<a href="http://live.vhall.com/878864086">http://live.vhall.com/878864086</a></p><p>注意事项：</p><ul><li>打包的信息都在package.json 中配置，build字段</li><li>Electron-builder使用nsis将我们的文件夹制作成exe，(NullsoftScriptable Install System) nsis本身也是非常复杂和可值得研究的一门技术，在这里我们只要简单使用它提供的参数即可</li></ul><h4 id="--4">备注</h4><ul><li>Electron 目前不支持安卓和ios （官宣）（所以和手机端的混合应用开发不相关）</li><li>app.makeSingleInstanceapi实现程序互斥（即，同一时间只有一个主程序，不支持开启多个）</li><li>跨域：在 BrowserWindow 中的 webPreferences 中设置 webSecurity: false 即可（但实际上非常不安全，官方有解释）</li><li>数据库：和node本身处理数据库相差无几</li><li>生命周期：正常流程会触发的生命周期如下</li></ul><p>	- will-finish-launching:当应用程序完成基础的启动的时候被触发</p><p>	- web-contents-created:webContents被创建完成</p><p>	- browser-window-created:BrowserWindow被创建完成</p><p>	- ready:当Electron 完成初始化时被触发</p><p>	- remote-require: 引入remote时被调用</p><p>	- before-quit: 在应用程序开始关闭窗口之前触发</p><p>	- will-quit:当所有窗口都已关闭并且应用程序将退出时发出</p><p>	- quit: 在应用程序退出时发出</p><p>	- window-all-closed:当所有的窗口都被关闭时触发</p><p>	- 这里要注意如果是进程杀死退出的所有都不触发，如果是cmd+Q或者开发者使用app.quit()退出的</p><p>	- window-all-closed是不会被触发的，基本操作一般在ready中处理</p><ul><li>进程相关：</li></ul><p>	- gpu-process-crashed: 当 gpu 进程崩溃或被杀时触发。</p><ul><li>其他：</li></ul><p>	- browser-window-focus: 在 browserWindow 获得焦点时发出</p><p>	- browser-window-blur:在 browserWindow 失去焦点时发出</p><h4 id="electron--5">Electron打包配置</h4><pre><code>"build": {
"appId": "your.id", // appid
"productName": "程序名称",// 程序名称
"files": [ // 打包需要的不过滤的文件
"build/**/*",
"main.js",
"node_modules/**/*"
],
"directories": {
"output": "./dist-out", // 打包输出的目录
"app": "./", // package所在路径
"buildResources": "assets"
},
"nsis": {
"oneClick": false, // 是否需要点击安装，自动更新需要关掉
"allowToChangeInstallationDirectory":true, //是否能够选择安装路径
"perMachine": true // 是否需要辅助安装页面
},
"win": {
"target": [
{
"target": "nsis", // 输出目录的方式
"arch": [ // 输出的配置ia32或者x64/x86
"x64"
}
],
"publish": [ // 自动更新的配置
{
"provider": "generic", // 自己配置更新的服务器要选generic
"url":"http://127.0.0.1:8080/updata/" //更新配置的路径
}
}
}</code></pre><p>以上就是本文的主要内容。</p><p>---</p><p>如需了解可嵌入你系统的在线 Excel，请前往：<a href="https://www.grapecity.com.cn/developer/spreadjs">SpreadJS纯前端表格控件</a></p> ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
