<?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[ Vue - 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[ Vue - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/chinese/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Sun, 24 May 2026 19:37:58 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/chinese/news/tag/vue/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ Vue 加载远程组件的解决方案 ]]>
                </title>
                <description>
                    <![CDATA[ 最近的项目有一个加载远程组件的需求。基于此我对 Vue 加载远程组件的方案进行了研究，并且整理了两个可行的解决方案。 HTML 文件 + umd 组件 这个方案是最简单、最容易实现的。组件以 umd 的格式进行打包，然后在 HTML 文件中直接使用。 <div id="app">     <test-input></test-input> </div> <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script> <script>     // 将组件 URL 挂载到 script 标签上，然后通过 window 获取这个组件     await lodaScript('http://localhost/component/input/0.1.0/bundle.js')     app.component('TestInput', window.TestInput) </script> 但是这个方案不适合在大型项目中使用，效率比较低。 ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/loading-remote-components-in-vue/</link>
                <guid isPermaLink="false">64fed8dd58a38a1950d360ea</guid>
                
                    <category>
                        <![CDATA[ Vue ]]>
                    </category>
                
                    <category>
                        <![CDATA[ 组件 ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ woai3c ]]>
                </dc:creator>
                <pubDate>Wed, 13 Sep 2023 05:01:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2023/09/Vue-Blog-Cover-2.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>最近的项目有一个加载远程组件的需求。基于此我对 Vue 加载远程组件的方案进行了研究，并且整理了两个可行的解决方案。</p><h3 id="html-umd-">HTML 文件 + umd 组件</h3><p>这个方案是最简单、最容易实现的。组件以 umd 的格式进行打包，然后在 HTML 文件中直接使用。</p><pre><code class="language-html">&lt;div id="app"&gt;
    &lt;test-input&gt;&lt;/test-input&gt;
&lt;/div&gt;
&lt;script src="https://unpkg.com/vue@3/dist/vue.global.js"&gt;&lt;/script&gt;
&lt;script&gt;
    // 将组件 URL 挂载到 script 标签上，然后通过 window 获取这个组件
    await lodaScript('http://localhost/component/input/0.1.0/bundle.js')
    app.component('TestInput', window.TestInput)
&lt;/script&gt;</code></pre><p>但是这个方案不适合在大型项目中使用，效率比较低。</p><h3 id="vue-esm-umd-">Vue 工程项目 + esm /umd 组件</h3><p>Vue 工程项目 + esm /umd 组件是我目前在使用的方案，但是在研究的过程中遇到了两个问题，逐一解决后，才把这个方案趟通了。</p><h3 id="-relative-references-must-start-with-either-or-">第一个问题 <code>Relative references must start with either "/", "./", or "../"</code></h3><p>由于我们的项目不需要兼容 IE，所以打包组件采用的是 esm 格式。打包后的组件源码如下：</p><pre><code class="language-js">import { reactive } from 'vue'
// other code...
</code></pre><p>然后在主项目中进行引用：</p><pre><code class="language-js">const { default: TestInput } = await import('http://localhost/component/input/0.1.0/bundle.mjs')
</code></pre><p>在动态导入远程组件到项目时，提示报错 <code>Relative references must start with either "/", "./", or "../"</code>。这是因为在浏览器中不支持以 <code>import { reactive } from 'vue'</code> 的方式进行导入，得把 <code>'vue'</code> 改成 <code>https://..../vue.js</code> 或者 <code>'./vue.js'</code> 的形式才可以。平时我们这样用没问题是因为有 vite、webpack 等构建工具帮忙解决了这个问题。</p><h3 id="-vue-">第二个问题 Vue 上下文环境不同</h3><p>产生上面的问题是因为要引入依赖，如果打包组件时把相关依赖都打在一起，那不就没有 import 语句了。结果试了一下还是不行，因为当前的 Vue 主项目和打包好的 Vue 组件存在两个不同的 Vue 上下文。导致在加载组件时报错，比如提示 <code>xxx 变量找不到</code> 这种问题。</p><figure class="kg-card kg-image-card"><img src="https://pic1.zhimg.com/80/v2-8fb51b72fa494fa4172ba9b4e4b4d6b4_1440w.webp" class="kg-image" alt="v2-8fb51b72fa494fa4172ba9b4e4b4d6b4_1440w" width="479" height="343" loading="lazy"></figure><p>虽然主项目和远程组件使用的 Vue 方法都是一样的，但由于各自的 Vue 上下文不一样，导致主项目无法正常使用远程组件。</p><p>以上两个问题困扰了我一天的时间，但是睡醒一觉后，终于想到了如何解决这两个问题。首先在浏览器上不能直接使用 <code>import { reactive } from 'vue'</code> 这种语句，那把它改成 <code>const { reactive } = Vue</code> 就能解决这个问题了。至于第二个问题，打包时不把依赖打在一起，而是在 <code>main.js</code> 文件中直接把整个 Vue 引进来：</p><pre><code class="language-js">import * as Vue from 'vue'
window.Vue = Vue
</code></pre><p>这样就能确保主项目和远程组件使用的是同一个 Vue 上下文。</p><p>为了解决代码转换问题，我写了一个 <a href="https://github.com/woai3c/rollup-plugin-import-to-const">rollup-plugin-import-to-const</a> 插件（支持 rollup、vite），打包 esm 组件时，它会自动的把 <code>import { reactive } from 'vue'</code> 转换成 <code>const { reactive } = Vue</code> 。</p><p>至此，就可以在主项目中加载远程 esm 组件了：</p><pre><code class="language-js">const { default: TestInput } = await import('http://localhost/component/input/0.1.0/bundle.mjs')
</code></pre><p>其实只要能解决上面的两个问题，不管是 esm 还是 umd、cjs 等格式，都能够实现加载远程组件的方案。比如换成 umd 的格式来打包组件，就不需要引入 rollup 插件去转换代码了，并且还能支持 webpack。唯一要做的只是在 <code>main.js</code> 上把 Vue 全引进来挂到 window 下。</p><pre><code class="language-js">import * as Vue from 'vue'
window.Vue = Vue
</code></pre><h3 id="-">总结</h3><p>远程组件的方案其实不止上面两种，比如还有直接加载 <code>.vue</code> 文件的方案，有个现成的 <a href="https://github.com/FranckFreiburger/vue3-sfc-loader">vue3-sfc-loader</a> 插件能用。 一般来说，加载远程组件的应用场景比较少，所以网上能搜到的讨论也比较少。目前比较常见的应用场景应该就是在低代码平台中加载远程组件了。</p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 如何创建和发布一个 Vue 组件库 – 2023 更新 ]]>
                </title>
                <description>
                    <![CDATA[ 早在 2020 年，我就写了一篇关于构建 Vue 组件库的文章 [https://www.freecodecamp.org/chinese/news/how-to-create-and-publish-a-vue-component-library/] 。自从那以后，我使用的包已被弃用了，构建一个库/包的推荐方法是使用 Vite。 开始 我通过运行 npm create vite@latest 启动该项目，并将我的项目命名为 brian-component-lib ，以便与我之前的文章保持一致。当这些选项出现时，我也选择了使用 TypeScript 和 Vue。 组件 我构建的组件是 freeCodeCamp.org 上使用的按钮的一个克隆。 这是该组件的代码。请注意，它使用了 TypeScript 和 Vue 3 中可用的 script setup 格式。 <script setup lang="ts"> defineProps<{ text: string }>() </script> <template>   <button class="btn-cta">{{ ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/how-to-create-and-publish-a-vue-component-library-update/</link>
                <guid isPermaLink="false">64f3f9c539dbbb03f0b4f303</guid>
                
                    <category>
                        <![CDATA[ Vue ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ xgqfrms ]]>
                </dc:creator>
                <pubDate>Fri, 01 Sep 2023 01:16:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2023/09/pexels-pixabay-159711.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p data-test-label="translation-intro">
        <strong>原文：</strong> <a href="https://www.freecodecamp.org/news/how-to-create-and-publish-a-vue-component-library-update/" target="_blank" rel="noopener noreferrer" data-test-label="original-article-link">How to Create and Publish a Vue Component Library – Update</a>
      </p><!--kg-card-begin: markdown--><p>早在 2020 年，我就写了一篇关于构建 Vue 组件库的<a href="https://www.freecodecamp.org/chinese/news/how-to-create-and-publish-a-vue-component-library/">文章</a>。自从那以后，我使用的包已被弃用了，构建一个库/包的推荐方法是使用 Vite。</p>
<h2 id="">开始</h2>
<p>我通过运行 <code>npm create vite@latest</code> 启动该项目，并将我的项目命名为 <code>brian-component-lib</code>，以便与我之前的文章保持一致。当这些选项出现时，我也选择了使用 TypeScript 和 Vue。</p>
<h2 id="">组件</h2>
<p>我构建的组件是 freeCodeCamp.org 上使用的按钮的一个克隆。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/05/image-160.png" alt="image-160" width="600" height="400" loading="lazy"></p>
<p>这是该组件的代码。请注意，它使用了 TypeScript 和 Vue 3 中可用的 <code>script setup</code> 格式。</p>
<pre><code class="language-js">&lt;script setup lang="ts"&gt;

defineProps&lt;{ text: string }&gt;()

&lt;/script&gt;

&lt;template&gt;
  &lt;button class="btn-cta"&gt;{{ text }}&lt;/button&gt;
&lt;/template&gt;

&lt;style scoped&gt;
.btn-cta {
  background-color: #d0d0d5;
  border-width: 3px;
  border-color: #1b1b32;
  border-radius: 0;
  border-style: solid;
  color: #1b1b32;
  display: block;
  margin-bottom: 0;
  font-weight: normal;
  text-align: center;
  -ms-touch-action: manipulation;
  touch-action: manipulation;
  cursor: pointer;
  white-space: nowrap;
  padding: 6px 12px;
  font-size: 18px;
  line-height: 1.42857143;
}

.btn-cta:active:hover,
.btn-cta:focus,
.btn-cta:hover {
  background-color: #1b1b32;
  border-width: 3px;
  border-color: #000;
  background-image: none;
  color: #f5f6f7;
}
&lt;/style&gt;
</code></pre>
<p>src/components/FccButton.vue</p>
<p>然后我们需要在库中导出这个组件。我们通过从一个 <code>index.ts</code> 文件导出它来实现。</p>
<pre><code class="language-js">import FccButton from "./components/FccButton.vue";

export { FccButton };
</code></pre>
<p>src/index.ts</p>
<h2 id="">配置</h2>
<p>组件代码准备好了后，我们需要确保 Vite 和 <code>package.json</code> 文件被正确配置了。</p>
<p>Vite 在构建代码时有很多选择。 我们关注的是<a href="https://vitejs.dev/guide/build.html#library-mode">“库模式”</a>。</p>
<pre><code class="language-js">import { defineConfig } from "vite";
import { resolve } from "path";
import vue from "@vitejs/plugin-vue";

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [vue()],
  build: {
    lib: {
      // src/indext.ts 是我们导出组件的地方
      entry: resolve(__dirname, "src/index.ts"),
      name: "BrianComponentLibrary",
      // 运行构建时输出文件的名称
      fileName: "brian-component-lib",
    },
    rollupOptions: {
      // 确保外部依赖项不应捆绑到你的库中
      external: ["vue"],
      output: {
        // 提供全局变量以便在 UMD 构建中可以被外部依赖项使用
        globals: {
          vue: "Vue",
        },
      },
    },
  },
});
</code></pre>
<p>vite.config.ts</p>
<p>这是 <code>package.json</code> 文件。我们需要确保拥有指向构建文件必要的属性。你可以在 VS Code 中将鼠标悬停在属性上，查看有关每个属性的作用的更多信息。</p>
<pre><code class="language-json">{
  "name": "brian-component-lib",
  "version": "1.0.0",
  "type": "module",
  "files": ["dist"],
  "main": "./dist/brian-component-lib.umd.cjs",
  "module": "./dist/brian-component-lib.js",
  "exports": {
    ".": {
      "import": "./dist/brian-component-lib.js",
      "require": "./dist/brian-component-lib.umd.cjs"
    },
    "./style.css": "./dist/style.css"
  },
  "types": "./dist/index.d.ts",
  "scripts": {
    "dev": "vite",
    "build": "vite build &amp;&amp; vue-tsc --emitDeclarationOnly",
    "types": "vue-tsc ",
    "preview": "vite preview"
  },
  "dependencies": {
    "vue": "^3.2.47"
  },
  "devDependencies": {
    "@types/node": "^20.2.5",
    "@vitejs/plugin-vue": "^4.2.3",
    "typescript": "^5.0.2",
    "vite": "^4.3.9",
    "vue-tsc": "^1.4.2"
  }
}
</code></pre>
<p>package.json</p>
<p>为了让 <code>vue-tsc --emitDeclarationOnly</code> 在构建时工作，我们需要将以下属性添加到 tsconfig.json 文件的 <code>compilerOptions</code> 部分：</p>
<pre><code>"outDir": "dist",
"declaration": true,
</code></pre>
<p>我们还需要删除 <code>noEmit: true</code> 属性。这将使我们的类型在包中可用，因此一个使用 TypeScript 和 Vue 的项目不会因为没有声明类型而报错。</p>
<p>我还添加了这一行，以确保我的 <code>App.vue</code> 和 <code>main.ts</code> 文件不会包含在组件库构建文件中。</p>
<p><code>"exclude": ["src/App.vue", "src/main.ts"],</code></p>
<h2 id="">测试该库</h2>
<p>我们现在可以运行 <code>npm run build</code>，然后测试我们的库。为此，请打开一个 Vue 项目（你可以打开一个当前拥有的 Vue 3 项目，或创建一个空白项目）。</p>
<p>在刚刚打开的项目的 package.json 文件中，将以下内容添加到依赖项中:</p>
<p><code>"brian-component-lib": "file:../brian-component-library"</code></p>
<p>确保你输入的文件路径指向组件库所在的正确文件夹。</p>
<p>运行 <code>npm install</code>，现在在你的 <code>node_modules</code> 中应该有了该组件库。</p>
<p>将组件导入其中一个页面以测试其是否正常工作。</p>
<p>注意: 你还需要导入 CSS，因为它在构建过程中构建到自己的文件中了。</p>
<pre><code class="language-js">&lt;script setup lang="ts"&gt;
import { FccButton } from 'brian-component-lib'
import "brian-component-lib/style.css"
&lt;/script&gt;

&lt;template&gt;
    &lt;FccButton text="Run the Tests" /&gt;
&lt;/template&gt;
</code></pre>
<p>现在，当你运行项目时，你应该会看到该按钮。</p>
<h2 id="npm">如何发布到 NPM</h2>
<p>如果你还没有在终端中登录 NPM，则可以通过运行 <code>npm adduser</code> 命令来登录。</p>
<p>然后只需运行 <code>npm publish</code> 命令，包就会被推送，并且在 NPM 上可用。</p>
<h2 id="">总结</h2>
<p>Vite 使发布一个组件库变得相当容易。希望这有帮助。如果有帮助，或者你将来希望看到我的其他内容，<a href="https://twitter.com/the_brianb">请在 Twitter 上告诉我</a>。</p>
<p>你可以在此处查看此代码的仓库：<a href="https://github.com/briancbarrow/vue-component-library-2023">https://github.com/briancbarrow/vue-component-library-2023</a></p>
<!--kg-card-end: markdown--> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ vscode + vite + vue3 + ts + eslint + stylelint 代码自动格式化 ]]>
                </title>
                <description>
                    <![CDATA[ 使用 vite 创建的 vue3 项目有点简陋，很多功能都没有。所以本文将讲解一下如何对 vite + vue3 项目配置代码自动格式化。配置完成后的效果如下图所示： 安装 VSCode 插件 打开 VSCode，安装以下插件： eslint stylelint volar 打开 VSCode 配置文件（Mac command + shift + p，windows ctrl + shift + p，输入 settings）。 在配置文件中加入以下代码： "editor.codeActionsOnSave": {     "source.fixAll": true, }, "eslint.validate": [     "javascript",   ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/vscode-vite-vue3-ts-eslint-stylelint-automatic-code-formatting/</link>
                <guid isPermaLink="false">6191dc06a8fafe063bba22ef</guid>
                
                    <category>
                        <![CDATA[ VSCode ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Vue ]]>
                    </category>
                
                    <category>
                        <![CDATA[ ESLint ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ woai3c ]]>
                </dc:creator>
                <pubDate>Tue, 09 Nov 2021 04:10:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2021/11/nordwood-themes-bJjsKbToY34-unsplash.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>使用 vite 创建的 vue3 项目有点简陋，很多功能都没有。所以本文将讲解一下如何对 vite + vue3 项目配置代码自动格式化。配置完成后的效果如下图所示：</p><figure class="kg-card kg-image-card"><img src="https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/7663bb223bbd4581a748d77ef4255673~tplv-k3u1fbpfcp-watermark.awebp?" class="kg-image" alt="10月-24-2021 22-06-50.gif" width="600" height="400" loading="lazy"></figure><h2 id="-vscode-">安装 VSCode 插件</h2><p>打开 VSCode，安装以下插件：</p><pre><code>eslint
stylelint
volar</code></pre><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2021/11/1.png" class="kg-image" alt="1" width="600" height="400" loading="lazy"></figure><p>打开 VSCode 配置文件（Mac <code>command + shift + p</code>，windows <code>ctrl + shift + p</code>，输入 <code>settings</code>）。</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2021/11/2.png" class="kg-image" alt="2" width="600" height="400" loading="lazy"></figure><p>在配置文件中加入以下代码：</p><pre><code class="language-json">"editor.codeActionsOnSave": {
    "source.fixAll": true,
},
"eslint.validate": [
    "javascript",
    "javascriptreact",
    "typescript"
],
"eslint.alwaysShowStatus": true,
"stylelint.validate": [
    "css",
    "less",
    "postcss",
    "scss",
    "vue",
    "sass"
],</code></pre><h2 id="-">安装项目依赖</h2><p>安装以下依赖:</p><pre><code>npm i -D eslint eslint-config-airbnb-base eslint-plugin-import eslint-plugin-typescript eslint-plugin-vue husky sass stylelint stylelint-config-standard stylelint-scss typescript @typescript-eslint/eslint-plugin @typescript-eslint/parser vue-eslint-parser</code></pre><p>注意，有些依赖版本对不上的时候，会造成格式化失败。例如 eslint 插件使用 8.0 版本以上格式化就报错，后来使用的是 7.0 的版本。安装后的具体版本如下：</p><pre><code class="language-json">"@typescript-eslint/eslint-plugin": "^5.1.0",
"@typescript-eslint/parser": "^5.1.0",
"eslint": "^7.2.0",
"eslint-config-airbnb-base": "^14.2.1",
"eslint-plugin-import": "^2.25.2",
"eslint-plugin-typescript": "^0.14.0",
"eslint-plugin-vue": "^7.20.0",
"husky": "^4.2.3",
"sass": "^1.43.3",
"stylelint": "^13.13.1",
"stylelint-config-standard": "^22.0.0",
"stylelint-scss": "^3.21.0",
"typescript": "^4.4.3",
"vue-eslint-parser": "^7.11.0",</code></pre><p>建议大家直接复制上面的代码到 package.json 文件中再下载。</p><h2 id="-eslintrc-js-stylelintrc-js-">配置 <code>.eslintrc.js</code> <code>.stylelintrc.js</code> 文件</h2><p>我的 eslint 配置是基于 airbnb 规范的， css 规范用的是官方插件。</p><h4 id="-eslintrc-js-"><code>.eslintrc.js</code> 文件</h4><pre><code class="language-js">module.exports = {
    root: true,
    globals: {
        defineEmits: 'readonly',
        defineProps: 'readonly',
    },
    extends: [
        'plugin:@typescript-eslint/recommended',
        'plugin:vue/vue3-recommended',
        'airbnb-base',      
    ],
    parser: 'vue-eslint-parser',
    plugins: [
        '@typescript-eslint',
    ],
    parserOptions: {
        parser: '@typescript-eslint/parser',
        ecmaVersion: 2020,
    },
    rules: {
        'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
        'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
        'no-bitwise': 'off',
        'no-tabs': 'off',
        'array-element-newline': ['error', 'consistent'],
        indent: ['error', 4, { MemberExpression: 0, SwitchCase: 1, ignoredNodes: ['TemplateLiteral'] }],
        quotes: ['error', 'single'],
        'comma-dangle': ['error', 'always-multiline'],
        'object-curly-spacing': ['error', 'always'],
        'max-len': ['error', 120],
        'no-new': 'off',
        'linebreak-style': 'off',
        'import/extensions': 'off',
        'eol-last': 'off',
        'no-shadow': 'off',
        'no-unused-vars': 'warn',
        'import/no-cycle': 'off',
        'arrow-parens': 'off',
        semi: ['error', 'never'],
        eqeqeq: 'off',
        'no-param-reassign': 'off',
        'import/prefer-default-export': 'off',
        'no-use-before-define': 'off',
        'no-continue': 'off',
        'prefer-destructuring': 'off',
        'no-plusplus': 'off',
        'prefer-const': 'off',
        'global-require': 'off',
        'no-prototype-builtins': 'off',
        'consistent-return': 'off',
        'vue/require-component-is': 'off',
        'prefer-template': 'off',
        'one-var-declaration-per-line': 'off',
        'one-var': 'off',
        'import/named': 'off',
        'object-curly-newline': 'off',
        'default-case': 'off',
        'import/order': 'off',
        'no-trailing-spaces': 'off',
        'func-names': 'off',
        radix: 'off',
        'no-unused-expressions': 'off',
        'no-underscore-dangle': 'off',
        'no-nested-ternary': 'off',
        'no-restricted-syntax': 'off',
        'no-mixed-operators': 'off',
        'no-await-in-loop': 'off',
        'import/no-extraneous-dependencies': 'off',
        'import/no-unresolved': 'off',
        'no-case-declarations': 'off',
        'template-curly-spacing': 'off',
        'vue/valid-v-for': 'off',
        '@typescript-eslint/no-var-requires': 'off',
        '@typescript-eslint/no-empty-function': 'off',
        'no-empty': 'off',
        '@typescript-eslint/no-explicit-any': 'off',
        'guard-for-in': 'off',
        '@typescript-eslint/ban-types': 'off',
        'class-methods-use-this': 'off',
        'no-return-await': 'off',
        'vue/html-indent': ['error', 4],
        'vue/html-self-closing': 'off',
        'vue/max-attributes-per-line': ['warn', {
            singleline: {
                max: 3,
                allowFirstLine: true,
            },      
            multiline: {
                max: 1,
                allowFirstLine: false,
            },
        }],
        'vue/singleline-html-element-content-newline': 'off',
    },
}</code></pre><h4 id="-stylelintrc-js-"><code>.stylelintrc.js</code> 文件</h4><pre><code class="language-js">module.exports = {
    extends: 'stylelint-config-standard',
    rules: {
        'indentation': 4,
        'selector-pseudo-element-no-unknown': [
            true,
            {
                ignorePseudoElements: ['v-deep']
            }
        ],
        'number-leading-zero': 'never',
        'no-descending-specificity': null,
        'font-family-no-missing-generic-family-keyword': null,
        'selector-type-no-unknown': null,
        'at-rule-no-unknown': null,
        'no-duplicate-selectors': null,
        'no-empty-source':null,
        'selector-pseudo-class-no-unknown': [true, { ignorePseudoClasses: ['global'] }]
    }
}</code></pre><h2 id="--1">踩坑</h2><h3 id="unknown-word-csssyntaxerror-"><code>Unknown word (CssSyntaxError)</code> 错误</h3><p>这个错误是因为安装的插件 <code>stylelint</code> <code>stylelint-config-standard</code> <code>stylelint-scss</code> 版本太新的问题，对于 vue3 模板文件的支持不是很好，不能正确识别 <code>.vue</code> 文件的 css 代码。所以将以上三个插件的版本降一个大版本就好了，最后的版本如下：</p><pre><code>"stylelint": "^13.13.1",
"stylelint-config-standard": "^22.0.0",
"stylelint-scss": "^3.21.0",</code></pre><p>另一种解决方案是安装 <code>postcss-html</code> 插件，但是 <code>stylelint</code> 14 版本的插件有很多问题，验证规则也和之前不太一样，并且 <code>.scss</code> 文件还验证不了。最后没找到解决方案，建议还是将版本降级比较好。</p><p>同时需要将 VSCode 的 <code>stylelint</code> 插件降级，现在插件的最新版本是 1.0.3，不支持 <code>stylelint</code> 13 版本。点击插件旁边的小齿轮，再点 <code>Install Another Version</code>，选择其他版本进行安装。</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2021/11/3.jpeg" class="kg-image" alt="3" width="600" height="400" loading="lazy"></figure><p>选 <code>0.87.6</code> 版本安装就可以了，这时 css 自动格式化功能恢复正常。</p><h3 id="-vue-html-">忽略 <code>.vue</code> 文件中的 HTML 模板验证规则无效</h3><p>举个例子，如果你将 HTML 模板每行的代码文本长度设为 100，当超过这个长度后 eslint 将会报错。此时如果你还是想超过这个长度，可以选择忽略这个规则：</p><pre><code class="language-js">/* eslint-disable max-len */</code></pre><p>注意，以上这行忽略验证的代码是不会生效的，因为这个注释是 JavaScript 注释，我们需要将注释改为 HTML 格式，这样忽略验证才会生效：</p><pre><code class="language-html">&lt;!-- eslint-disable max-len --&gt;</code></pre><h2 id="--2">总结</h2><p>这样配置完成之后，基本上 vue3 项目里的代码都能格式化了（css js html 及各种衍生代码）。没有使用 prettier 是因为不够自由，而且它的功能已经可以由 eslint stylelint 来代替了。</p><p>为了不让大家再次踩坑，我已经将配置好的项目 demo 上传到 GitHub 了，有需要直接克隆项目就行。这是<a href="https://github.com/woai3c/vite-vue3-eslint-stylelint-demo">项目地址</a>。</p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 为什么 Vue2 this 能够直接获取到 data 和 methods ]]>
                </title>
                <description>
                    <![CDATA[ 本文源于一次源码共读 [https://juejin.cn/pin/7005372623400435725]群里群友的提问，请问@若川，“为什么 data 中的数据可以用 this 直接获取到啊 ”，当时我翻阅源码做出了解答。想着如果下次有人再次问到，我还需要回答一次。当时打算有空写篇文章告诉读者自己探究原理，于是就有了这篇文章。 阅读本文，你将学到： 1. 如何学习调试 vue2 源码 2. data 中的数据为什么可以用 this 直接获取到 3. methods 中的方法为什么可以用 this 直接获取到 4. 学习源码中优秀代码和思想，投入到自己的项目中 本文不难，用过 Vue 的都看得懂，希望大家动手调试和学会看源码。 看源码可以大胆猜测，最后小心求证。 2. 示例：this 能够直接获取到 data 和 methods 众所周知，这样是可以输出我是若川的。好奇的人就会思考为啥 this 就能直接访问到呢。 const vm = new Vue({     data: {   ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/why-this-in-vue2-can-get-data-and-methods-directly/</link>
                <guid isPermaLink="false">61669b181cb0e006556dc535</guid>
                
                    <category>
                        <![CDATA[ Vue ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ 若川 ]]>
                </dc:creator>
                <pubDate>Wed, 13 Oct 2021 08:00:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2021/10/caspar-camille-rubin-0qvBNep1Y04-unsplash-1.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>本文源于一次<a href="https://juejin.cn/pin/7005372623400435725" rel="noopener noreferrer">源码共读</a>群里群友的提问，请问@若川，“<strong>为什么 data 中的数据可以用 this 直接获取到啊</strong>”，当时我翻阅源码做出了解答。想着如果下次有人再次问到，我还需要回答一次。当时打算有空写篇文章告诉读者自己探究原理，于是就有了这篇文章。</p><p>阅读本文，你将学到：</p><pre><code class="language-js">1. 如何学习调试 vue2 源码
2. data 中的数据为什么可以用 this 直接获取到
3. methods 中的方法为什么可以用 this 直接获取到
4. 学习源码中优秀代码和思想，投入到自己的项目中
</code></pre><p>本文不难，用过 <code>Vue</code> 的都看得懂，希望大家动手调试和学会看源码。</p><p>看源码可以大胆猜测，最后小心求证。</p><h2 id="2-this-data-methods">2. 示例：this 能够直接获取到 data 和 methods</h2><p>众所周知，这样是可以输出<code>我是若川</code>的。好奇的人就会思考为啥 <code>this</code> 就能直接访问到呢。</p><pre><code class="language-js">const vm = new Vue({
    data: {
        name: '我是若川',
    },
    methods: {
        sayName(){
            console.log(this.name);
        }
    },
});
console.log(vm.name); // 我是若川
console.log(vm.sayName()); // 我是若川
</code></pre><p>那么为什么 <code>this.xxx</code> 能获取到<code>data</code>里的数据，能获取到 <code>methods</code> 方法。</p><p>我们自己构造写的函数，如何做到类似<code>Vue</code>的效果呢。</p><pre><code class="language-js">function Person(options){

}

const p = new Person({
    data: {
        name: '若川'
    },
    methods: {
        sayName(){
            console.log(this.name);
        }
    }
});

console.log(p.name);
// undefined
console.log(p.sayName());
// Uncaught TypeError: p.sayName is not a function
</code></pre><p>如果是你，你会怎么去实现呢？带着问题，我们来调试 <code>Vue2</code>源码学习。</p><h2 id="3-">3. 准备环境调试源码一探究竟</h2><p>可以在本地新建一个文件夹<code>examples</code>，新建文件<code>index.html</code>文件。 在<code>&lt;body&gt;&lt;/body&gt;</code>中加上如下<code>js</code>。</p><pre><code class="language-js">&lt;script src="https://unpkg.com/vue@2.6.14/dist/vue.js"&gt;&lt;/script&gt;
&lt;script&gt;
    const vm = new Vue({
        data: {
            name: '我是若川',
        },
        methods: {
            sayName(){
                console.log(this.name);
            }
        },
    });
    console.log(vm.name);
    console.log(vm.sayName());
&lt;/script&gt;
</code></pre><p>再全局安装<code>npm i -g http-server</code>启动服务。</p><pre><code class="language-js">npm i -g http-server
cd examples
http-server .
// 如果碰到端口被占用，也可以指定端口
http-server -p 8081 .
</code></pre><p>这样就能在<code>http://localhost:8080/</code>打开刚写的<code>index.html</code>页面了。</p><p>对于调试还不是很熟悉的读者，可以看这篇文章<a href="https://mp.weixin.qq.com/s/VOoDHqIo4gh3scHVNxk3lA" rel="noopener noreferrer">《前端容易忽略的 debugger 调试技巧》</a>，截图标注的很详细。</p><p>调试：在 <code>F12</code> 打开调试，<code>source</code> 面板，在例子中<code>const vm = new Vue({</code>打上断点。</p><figure class="kg-card kg-image-card"><img src="https://lxchuan12.gitee.io/assets/img/debugger.6a4010d8.png" class="kg-image" alt="如下图所示" width="600" height="400" loading="lazy"></figure><p>刷新页面后按<code>F11</code>进入函数，这时断点就走进了 Vue 构造函数。</p><h3 id="3-1-vue-">3.1 Vue 构造函数</h3><pre><code class="language-js">function Vue (options) {
    if (!(this instanceof Vue)
    ) {
        warn('Vue is a constructor and should be called with the `new` keyword');
    }
    this._init(options);
}
// 初始化
initMixin(Vue);
stateMixin(Vue);
eventsMixin(Vue);
lifecycleMixin(Vue);
renderMixin(Vue);
</code></pre><p>值得一提的是：<code>if (!(this instanceof Vue)){}</code> 判断是不是用了 <code>new</code> 关键词调用构造函数。 一般而言，我们平时应该不会考虑写这个。</p><p>当然看源码库也可以自己函数内部调用 <code>new</code> 。但 <code>vue</code> 一般一个项目只需要 <code>new Vue()</code> 一次，所以没必要。</p><p>而 <code>jQuery</code> 源码的就是内部 <code>new</code> ，对于使用者来说就是无<code>new</code>构造。</p><pre><code class="language-js">jQuery = function( selector, context ) {
  // 返回new之后的对象
  return new jQuery.fn.init( selector, context );
};
</code></pre><p>因为使用 <code>jQuery</code> 经常要调用。 其实 <code>jQuery</code> 也是可以 <code>new</code> 的。和不用 <code>new</code> 是一个效果。</p><p>如果不明白 <code>new</code> 操作符的用处，可以看我之前的文章：<a href="https://chinese.freecodecamp.org/news/javascript-new-operator/">《如何模拟实现 JS 的 new 操作符》</a>。</p><p>调试：继续在<code>this._init(options);</code>处打上断点，按<code>F11</code>进入函数。</p><h3 id="3-2-_init-">3.2 _init 初始化函数</h3><p>进入 <code>_init</code> 函数后，这个函数比较长，做了挺多事情，我们猜测跟<code>data</code>和<code>methods</code>相关的实现在<code>initState(vm)</code>函数里。</p><pre><code class="language-js">// 代码有删减
function initMixin (Vue) {
    Vue.prototype._init = function (options) {
      var vm = this;
      // a uid
      vm._uid = uid$3++;

      // a flag to avoid this being observed
      vm._isVue = true;
      // merge options
      if (options &amp;&amp; options._isComponent) {
        // optimize internal component instantiation
        // since dynamic options merging is pretty slow, and none of the
        // internal component options needs special treatment.
        initInternalComponent(vm, options);
      } else {
        vm.$options = mergeOptions(
          resolveConstructorOptions(vm.constructor),
          options || {},
          vm
        );
      }

      // expose real self
      vm._self = vm;
      initLifecycle(vm);
      initEvents(vm);
      initRender(vm);
      callHook(vm, 'beforeCreate');
      initInjections(vm); // resolve injections before data/props
      //  初始化状态
      initState(vm);
      initProvide(vm); // resolve provide after data/props
      callHook(vm, 'created');
    };
}
</code></pre><p>调试：接着我们在<code>initState(vm)</code>函数这里打算断点，按<code>F8</code>可以直接跳转到这个断点，然后按<code>F11</code>接着进入<code>initState</code>函数。</p><h3 id="3-3-initstate-">3.3 initState 初始化状态</h3><p>从函数名来看，这个函数主要实现功能是：</p><pre><code class="language-bash">初始化 props
初始化 methods
监测数据
初始化 computed
初始化 watch
</code></pre><pre><code class="language-js">function initState (vm) {
    vm._watchers = [];
    var opts = vm.$options;
    if (opts.props) { initProps(vm, opts.props); }
    // 有传入 methods，初始化方法
    if (opts.methods) { initMethods(vm, opts.methods); }
    // 有传入 data，初始化 data
    if (opts.data) {
      initData(vm);
    } else {
      observe(vm._data = {}, true /* asRootData */);
    }
    if (opts.computed) { initComputed(vm, opts.computed); }
    if (opts.watch &amp;&amp; opts.watch !== nativeWatch) {
      initWatch(vm, opts.watch);
    }
}
</code></pre><p>我们重点来看初始化 <code>methods</code>，之后再看初始化 <code>data</code>。</p><p>调试：在 <code>initMethods</code> 这句打上断点，同时在<code>initData(vm)</code>处打上断点，看完<code>initMethods</code>函数后，可以直接按<code>F8</code>回到<code>initData(vm)</code>函数。 继续按<code>F11</code>，先进入<code>initMethods</code>函数。</p><h3 id="3-4-initmethods-">3.4 initMethods 初始化方法</h3><pre><code class="language-js">function initMethods (vm, methods) {
    var props = vm.$options.props;
    for (var key in methods) {
      {
        if (typeof methods[key] !== 'function') {
          warn(
            "Method \"" + key + "\" has type \"" + (typeof methods[key]) + "\" in the component definition. " +
            "Did you reference the function correctly?",
            vm
          );
        }
        if (props &amp;&amp; hasOwn(props, key)) {
          warn(
            ("Method \"" + key + "\" has already been defined as a prop."),
            vm
          );
        }
        if ((key in vm) &amp;&amp; isReserved(key)) {
          warn(
            "Method \"" + key + "\" conflicts with an existing Vue instance method. " +
            "Avoid defining component methods that start with _ or $."
          );
        }
      }
      vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm);
    }
}
</code></pre><p><code>initMethods</code>函数，主要有一些判断。</p><pre><code class="language-js">判断 methods 中的每一项是不是函数，如果不是警告。
判断 methods 中的每一项是不是和 props 冲突了，如果是，警告。
判断 methods 中的每一项是不是已经在 new Vue实例 vm 上存在，而且是方法名是保留的 _ $ （在JS中一般指内部变量标识）开头，如果是警告。
</code></pre><p>除去这些判断，我们可以看出<code>initMethods</code>函数其实就是遍历传入的<code>methods</code>对象，并且使用<code>bind</code>绑定函数的this指向为<code>vm</code>，也就是<code>new Vue</code>的实例对象。</p><p><strong>这就是为什么我们可以通过<code>this</code>直接访问到<code>methods</code>里面的函数的原因</strong>。</p><p>我们可以把鼠标移上 <code>bind</code> 变量，按<code>alt</code>键，可以看到函数定义的地方，这里是<code>218行</code>，点击跳转到这里看 <code>bind</code> 的实现。</p><h4 id="3-4-1-bind-this-">3.4.1 bind 返回一个函数，修改 this 指向</h4><pre><code class="language-js">function polyfillBind (fn, ctx) {
    function boundFn (a) {
      var l = arguments.length;
      return l
        ? l &gt; 1
          ? fn.apply(ctx, arguments)
          : fn.call(ctx, a)
        : fn.call(ctx)
    }

    boundFn._length = fn.length;
    return boundFn
}

function nativeBind (fn, ctx) {
  return fn.bind(ctx)
}

var bind = Function.prototype.bind
  ? nativeBind
  : polyfillBind;
</code></pre><p>简单来说就是兼容了老版本不支持 原生的bind函数。同时兼容写法，对参数多少做出了判断，使用<code>call</code>和<code>apply</code>实现，据说是因为性能问题。</p><p>如果对于<code>call、apply、bind</code>的用法和实现不熟悉，可以查看我之前的文章：<a href="https://chinese.freecodecamp.org/news/javascript-call-and-apply/">《模拟实现 JavaScript 的 call 和 apply 方法》</a>、<a href="https://chinese.freecodecamp.org/news/javascript-bind-method/">《如何模拟实现 JS 的 bind 方法》</a>。</p><p>调试：看完了<code>initMethods</code>函数，按<code>F8</code>回到上文提到的<code>initData(vm)</code>函数断点处。</p><h3 id="3-5-initdata-data">3.5 initData 初始化 data</h3><p><code>initData</code> 函数也是一些判断。主要做了如下事情：</p><pre><code class="language-bash">先给 _data 赋值，以备后用。
最终获取到的 data 不是对象给出警告。
遍历 data ，其中每一项：
如果和 methods 冲突了，报警告。
如果和 props 冲突了，报警告。
不是内部私有的保留属性，做一层代理，代理到 _data 上。
最后监测 data，使之成为响应式的数据。
</code></pre><pre><code class="language-js">function initData (vm) {
    var data = vm.$options.data;
    data = vm._data = typeof data === 'function'
      ? getData(data, vm)
      : data || {};
    if (!isPlainObject(data)) {
      data = {};
      warn(
        'data functions should return an object:\n' +
        'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
        vm
      );
    }
    // proxy data on instance
    var keys = Object.keys(data);
    var props = vm.$options.props;
    var methods = vm.$options.methods;
    var i = keys.length;
    while (i--) {
      var key = keys[i];
      {
        if (methods &amp;&amp; hasOwn(methods, key)) {
          warn(
            ("Method \"" + key + "\" has already been defined as a data property."),
            vm
          );
        }
      }
      if (props &amp;&amp; hasOwn(props, key)) {
        warn(
          "The data property \"" + key + "\" is already declared as a prop. " +
          "Use prop default value instead.",
          vm
        );
      } else if (!isReserved(key)) {
        proxy(vm, "_data", key);
      }
    }
    // observe data
    observe(data, true /* asRootData */);
}
</code></pre><h4 id="3-5-1-getdata-">3.5.1 getData 获取数据</h4><p>是函数时调用函数，执行获取到对象。</p><pre><code class="language-js">function getData (data, vm) {
    // #7573 disable dep collection when invoking data getters
    pushTarget();
    try {
      return data.call(vm, vm)
    } catch (e) {
      handleError(e, vm, "data()");
      return {}
    } finally {
      popTarget();
    }
}
</code></pre><h4 id="3-5-2-proxy-">3.5.2 proxy 代理</h4><p>其实就是用 <code>Object.defineProperty</code> 定义对象</p><p>这里用处是：<code>this.xxx</code> 则是访问的 <code>this._data.xxx</code>。</p><pre><code class="language-js">/**
   * Perform no operation.
   * Stubbing args to make Flow happy without leaving useless transpiled code
   * with ...rest (https://flow.org/blog/2017/05/07/Strict-Function-Call-Arity/).
   */
function noop (a, b, c) {}
var sharedPropertyDefinition = {
    enumerable: true,
    configurable: true,
    get: noop,
    set: noop
};

function proxy (target, sourceKey, key) {
    sharedPropertyDefinition.get = function proxyGetter () {
      return this[sourceKey][key]
    };
    sharedPropertyDefinition.set = function proxySetter (val) {
      this[sourceKey][key] = val;
    };
    Object.defineProperty(target, key, sharedPropertyDefinition);
}
</code></pre><h4 id="3-5-3-object-defineproperty-">3.5.3 Object.defineProperty 定义对象属性</h4><p><code>Object.defineProperty</code> 算是一个非常重要的API。还有一个定义多个属性的API：<code>Object.defineProperties(obj, props) (ES5)</code></p><p><code>Object.defineProperty</code> 涉及到比较重要的知识点，面试也常考。</p><pre><code class="language-bash">value——当试图获取属性时所返回的值。
writable——该属性是否可写。
enumerable——该属性在for in循环中是否会被枚举。
configurable——该属性是否可被删除。
set()——该属性的更新操作所调用的函数。
get()——获取属性值时所调用的函数。
</code></pre><p><a href="https://juejin.cn/post/6994976281053888519#heading-34" rel="noopener noreferrer">详细举例见此链接</a>。</p><h3 id="3-6-">3.6 文中出现的一些函数，最后统一解释下</h3><h4 id="3-6-1-hasown-">3.6.1 hasOwn 是否是对象本身拥有的属性</h4><p>调试模式下，按<code>alt</code>键，把鼠标移到方法名上，可以看到函数定义的地方。点击可以跳转。</p><pre><code class="language-js">/**
   * Check whether an object has the property.
   */
var hasOwnProperty = Object.prototype.hasOwnProperty;
function hasOwn (obj, key) {
  return hasOwnProperty.call(obj, key)
}

hasOwn({ a: undefined }, 'a') // true
hasOwn({}, 'a') // false
hasOwn({}, 'hasOwnProperty') // false
hasOwn({}, 'toString') // false
// 是自己的本身拥有的属性，不是通过原型链向上查找的。
</code></pre><h4 id="3-6-2-isreserved-_-">3.6.2 isReserved 是否是内部私有保留的字符串$ 和 _ 开头</h4><pre><code class="language-js">/**
   * Check if a string starts with $ or _
   */
function isReserved (str) {
  var c = (str + '').charCodeAt(0);
  return c === 0x24 || c === 0x5F
}
isReserved('_data'); // true
isReserved('$options'); // true
isReserved('data'); // false
isReserved('options'); // false
</code></pre><h2 id="4-60-">4. 最后用60余行代码实现简化版</h2><pre><code class="language-js">function noop (a, b, c) {}
var sharedPropertyDefinition = {
    enumerable: true,
    configurable: true,
    get: noop,
    set: noop
};
function proxy (target, sourceKey, key) {
    sharedPropertyDefinition.get = function proxyGetter () {
      return this[sourceKey][key]
    };
    sharedPropertyDefinition.set = function proxySetter (val) {
      this[sourceKey][key] = val;
    };
    Object.defineProperty(target, key, sharedPropertyDefinition);
}
function initData(vm){
  const data = vm._data = vm.$options.data;
  const keys = Object.keys(data);
  var i = keys.length;
  while (i--) {
    var key = keys[i];
    proxy(vm, '_data', key);
  }
}
function initMethods(vm, methods){
  for (var key in methods) {
    vm[key] = typeof methods[key] !== 'function' ? noop : methods[key].bind(vm);
  }
}

function Person(options){
  let vm = this;
  vm.$options = options;
  var opts = vm.$options;
  if(opts.data){
    initData(vm);
  }
  if(opts.methods){
    initMethods(vm, opts.methods)
  }
}

const p = new Person({
    data: {
        name: '若川'
    },
    methods: {
        sayName(){
            console.log(this.name);
        }
    }
});

console.log(p.name);
// 未实现前： undefined
// '若川'
console.log(p.sayName());
// 未实现前：Uncaught TypeError: p.sayName is not a function
// '若川'
</code></pre><h2 id="5-">5. 总结</h2><p>本文涉及到的基础知识主要有如下：</p><pre><code class="language-js">构造函数
this 指向
call、bind、apply
Object.defineProperty
等等基础知识。
</code></pre><p>本文源于解答源码共读群友的疑惑，通过详细的描述了如何调试 <code>Vue</code> 源码，来探寻答案。</p><p>解答文章开头提问：</p><p>通过<code>this</code>直接访问到<code>methods</code>里面的函数的原因是：因为<code>methods</code>里的方法通过 <code>bind</code> 指定了<code>this</code>为 new Vue的实例(<code>vm</code>)。</p><p>通过 <code>this</code> 直接访问到 <code>data</code> 里面的数据的原因是：data里的属性最终会存储到<code>new Vue</code>的实例（<code>vm</code>）上的 <code>_data</code>对象中，访问 <code>this.xxx</code>，是访问<code>Object.defineProperty</code>代理后的 <code>this._data.xxx</code>。</p><p><code>Vue</code>的这种设计，好处在于便于获取。也有不方便的地方，就是<code>props</code>、<code>methods</code> 和 <code>data</code>三者容易产生冲突。</p><p>文章整体难度不大，但非常建议读者朋友们自己动手调试下。调试后，你可能会发现：原来 <code>Vue</code> 源码，也没有想象中的那么难，也能看懂一部分。</p><p>启发：我们工作使用常用的技术和框架或库时，保持好奇心，多思考内部原理。能够做到知其然，知其所以然。就能远超很多人。</p><p>你可能会思考，为什么模板语法中，可以省略<code>this</code>关键词写法呢，内部模板编译时其实是用了<code>with</code>。有余力的读者可以探究这一原理。</p><p>最后，欢迎加我微信 <a href="https://juejin.cn/pin/7005372623400435725" rel="noopener noreferrer">ruochuan12</a> 交流，参与<a href="https://juejin.cn/pin/7005372623400435725" rel="noopener noreferrer">源码共读</a>活动，大家一起学习源码，共同进步。</p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 如何搭建 Vue 服务端渲染项目 ]]>
                </title>
                <description>
                    <![CDATA[ 本文将分成以下两部分：  1. 简述 Vue SSR 过程  2. 从零开始搭建 SSR 项目 建议读者先阅读官方指南——Vue.js 服务器端渲染指南 [https://ssr.vuejs.org/zh/]，再回到本文开始阅读。 好了，下面开始正文。 简述 Vue SSR 过程 客户端渲染过程  1. 访问客户端渲染的网站。  2. 服务器返回一个包含了引入资源语句和 <div id="app"></div> 的 HTML 文件。  3. 客户端通过 HTTP 向服务器请求资源，当必要的资源都加载完毕后，执行 new Vue() 开始实例化并渲染页面。 服务端渲染过程  1. 访问服务端渲染的网站。  2. 服务器会查看当前路由组件需要哪些资源文件，然后将这些文件的内容填充到 HTML 文件。如果有 asyncData() ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/build-a-vue-server-side-rendering-project/</link>
                <guid isPermaLink="false">615ebdcc21a1350622df4f50</guid>
                
                    <category>
                        <![CDATA[ Vue ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ woai3c ]]>
                </dc:creator>
                <pubDate>Thu, 07 Oct 2021 09:00:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2021/10/1_zyNSb0UXhP8TfxYbj-GNWg.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>本文将分成以下两部分：</p><ol><li>简述 Vue SSR 过程</li><li>从零开始搭建 SSR 项目</li></ol><p>建议读者先阅读官方指南——<a href="https://ssr.vuejs.org/zh/">Vue.js 服务器端渲染指南</a>，再回到本文开始阅读。</p><p>好了，下面开始正文。</p><h2 id="-vue-ssr-">简述 Vue SSR 过程</h2><h3 id="-">客户端渲染过程</h3><ol><li>访问客户端渲染的网站。</li><li>服务器返回一个包含了引入资源语句和 <code>&lt;div id="app"&gt;&lt;/div&gt;</code> 的 HTML 文件。</li><li>客户端通过 HTTP 向服务器请求资源，当必要的资源都加载完毕后，执行 <code>new Vue()</code> 开始实例化并渲染页面。</li></ol><h3 id="--1">服务端渲染过程</h3><ol><li>访问服务端渲染的网站。</li><li>服务器会查看当前路由组件需要哪些资源文件，然后将这些文件的内容填充到 HTML 文件。如果有 <code>asyncData()</code> 函数，就会执行它进行数据预取并填充到 HTML 文件里，最后返回这个 HTML 页面。</li><li>当客户端接收到这个 HTML 页面时，可以马上就开始渲染页面。与此同时，页面也会加载资源，当必要的资源都加载完毕后，开始执行 <code>new Vue()</code> 开始实例化并接管页面。</li></ol><p>从上述两个过程中，可以看出，区别就在于第二步。客户端渲染的网站会直接返回 HTML 文件，而服务端渲染的网站则会渲染完页面再返回这个 HTML 文件。</p><p><strong>这样做的好处是什么？是更快的内容到达时间 (time-to-content)</strong>。</p><p>假设你的网站需要加载完 abcd 四个文件才能渲染完毕。并且每个文件大小为 1 M。</p><p>这样一算：客户端渲染的网站需要加载 4 个文件和 HTML 文件才能完成首页渲染，总计大小为 4M（忽略 HTML 文件大小）。而服务端渲染的网站只需要加载一个渲染完毕的 HTML 文件就能完成首页渲染，总计大小为已经渲染完毕的 HTML 文件（这种文件不会太大，一般为几百K，我的个人博客网站（SSR）加载的 HTML 文件为 400K）。<strong>这就是服务端渲染更快的原因</strong>。</p><h3 id="--2">客户端接管页面</h3><p>对于服务端返回来的 HTML 文件，客户端必须进行接管，对其进行 <code>new Vue()</code> 实例化，用户才能正常使用页面。</p><p>如果不对其进行激活的话，里面的内容只是一串字符串而已，例如下面的代码，点击是无效的：</p><pre><code class="language-html">&lt;button @click="sayHi"&gt;如果不进行激活，点我是不会触发事件的&lt;/button&gt;</code></pre><p>那客户端如何接管页面呢？下面引用<a href="https://juejin.cn/post/6844904163692937229#heading-6">一篇文章</a>中的内容：</p><blockquote>客户端 new Vue() 时，客户端会和服务端生成的DOM进行Hydration对比(判断这个DOM和自己即将生成的DOM是否相同（vuex store 数据同步才能保持一致）。</blockquote><blockquote>如果相同就调用app.$mount('#app')将客户端的vue实例挂载到这个DOM上，即去“激活”这些服务端渲染的HTML之后，其变成了由Vue动态管理的DOM，以便响应后续数据的变化，即之后所有的交互和vue-router不同页面之间的跳转将全部在浏览器端运行。</blockquote><blockquote>如果客户端构建的虚拟 DOM 树与服务器渲染返回的HTML结构不一致，这时候，客户端会请求一次服务器再渲染整个应用程序，这使得ssr失效了，达不到服务端渲染的目的了。</blockquote><h3 id="--3">小结</h3><p>不管是客户端渲染还是服务端渲染，都需要等待客户端执行 <code>new Vue()</code> 之后，用户才能进行交互操作。<strong>但服务端渲染的网站能让用户更快的看见页面</strong>。</p><h2 id="-ssr-">从零开始搭建 SSR 项目</h2><h3 id="-weback">配置 weback</h3><p>webpack 配置文件共有 3 个：</p><ol><li><code>webpack.base.config.js</code>，基础配置文件，客户端与服务端都需要它。</li><li><code>webpack.client.config.js</code>，客户端配置文件，用于生成客户端所需的资源。</li><li><code>webpack.server.config.js</code>，服务端配置文件，用于生成服务端所需的资源。</li></ol><h4 id="webpack-base-config-js-"><code>webpack.base.config.js</code> 基础配置文件</h4><pre><code class="language-js">const path = require('path')
const { VueLoaderPlugin } = require('vue-loader')
const isProd = process.env.NODE_ENV === 'production'

function resolve(dir) {
    return path.join(__dirname, '..', dir)
}

module.exports = {
    context: path.resolve(__dirname, '../'),
    devtool: isProd ? 'source-map' : '#cheap-module-source-map',
    output: {
        path: path.resolve(__dirname, '../dist'),
        publicPath: '/dist/',
        // chunkhash 同属一个 chunk 中的文件修改了，文件名会发生变化 
        // contenthash 只有文件自己的内容变化了，文件名才会变化
        filename: '[name].[contenthash].js',
        // 此选项给打包后的非入口js文件命名，与 SplitChunksPlugin 配合使用
        chunkFilename: '[name].[contenthash].js',
    },
    resolve: {
        extensions: ['.js', '.vue', '.json', '.css'],
        alias: {
            public: resolve('public'),
            '@': resolve('src')
        }
    },
    module: {
        // https://juejin.cn/post/6844903689103081485
        // 使用 `mini-css-extract-plugin` 插件打包的的 `server bundle` 会使用到 document。
        // 由于 node 环境中不存在 document 对象，所以报错。
        // 解决方案：样式相关的 loader 不要放在 `webpack.base.config.js` 文件
        // 将其分拆到 `webpack.client.config.js` 和 `webpack.client.server.js` 文件
        // 其中 `mini-css-extract-plugin` 插件要放在 `webpack.client.config.js` 文件配置。
        rules: [
            {
                test: /\.vue$/,
                loader: 'vue-loader',
                options: {
                    compilerOptions: {
                        preserveWhitespace: false
                    }
                }
            },
            {
                test: /\.js$/,
                loader: 'babel-loader',
                exclude: /node_modules/
            },
            {
                test: /\.(png|svg|jpg|gif|ico)$/,
                use: ['file-loader']
            },
            {
                test: /\.(woff|eot|ttf)\??.*$/,
                loader: 'url-loader?name=fonts/[name].[md5:hash:hex:7].[ext]'
            },
        ]
    },
    plugins: [new VueLoaderPlugin()],
}</code></pre><p>基础配置文件比较简单，<code>output</code> 属性的意思是打包时根据文件内容生成文件名称。<code>module</code> 属性配置不同文件的解析 loader。</p><h4 id="webpack-client-config-js-"><code>webpack.client.config.js</code> 客户端配置文件</h4><pre><code class="language-js">const webpack = require('webpack')
const merge = require('webpack-merge')
const base = require('./webpack.base.config')
const CompressionPlugin = require('compression-webpack-plugin')
const WebpackBar = require('webpackbar')
const VueSSRClientPlugin = require('vue-server-renderer/client-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')

const isProd = process.env.NODE_ENV === 'production'

const plugins = [
    new webpack.DefinePlugin({
        'process.env.NODE_ENV': JSON.stringify(
            process.env.NODE_ENV || 'development'
        ),
        'process.env.VUE_ENV': '"client"'
    }),
    new VueSSRClientPlugin(),
    new MiniCssExtractPlugin({
        filename: 'style.css'
    })
]

if (isProd) {
    plugins.push(
        // 开启 gzip 压缩 https://github.com/woai3c/node-blog/blob/master/doc/optimize.md
        new CompressionPlugin(),
        // 该插件会根据模块的相对路径生成一个四位数的hash作为模块id, 用于生产环境。
        new webpack.HashedModuleIdsPlugin(),
        new WebpackBar(),
    )
}

const config = {
    entry: {
        app: './src/entry-client.js'
    },
    plugins,
    optimization: {
        runtimeChunk: {
            name: 'manifest'
        },
        splitChunks: {
            cacheGroups: {
                vendor: {
                    name: 'chunk-vendors',
                    test: /[\\/]node_modules[\\/]/,
                    priority: -10,
                    chunks: 'initial',
                },
                common: {
                    name: 'chunk-common',
                    minChunks: 2,
                    priority: -20,
                    chunks: 'initial',
                    reuseExistingChunk: true
                }
            },
        }
    },
    module: {
        rules: [
            {
                test: /\.css$/,
                use: [
                    {
                        loader: MiniCssExtractPlugin.loader,
                        options: {
                            // 解决 export 'default' (imported as 'mod') was not found
                            // 启用 CommonJS 语法
                            esModule: false,
                        },
                    },
                    'css-loader'
                ]
            }
        ]
    },
}

if (isProd) {
    // 压缩 css
    config.optimization.minimizer = [
        new CssMinimizerPlugin(),
    ]
}

module.exports = merge(base, config)</code></pre><p>客户端配置文件中的 <code>config.optimization</code> 属性是打包时分割代码用的。它的作用是将第三方库都打包在一起。</p><p>其他插件作用：</p><ol><li><code>MiniCssExtractPlugin</code> 插件, 将 css 提取出来单独打包。</li><li><code>CssMinimizerPlugin</code> 插件，压缩 css。</li><li><code>CompressionPlugin</code> 插件，将资源压缩成 gzip 格式（大大提升传输效率）。另外还需要在 node 服务器上引入 <code>compression</code> 插件配合使用。</li><li><code>WebpackBar</code> 插件，打包时显示进度条。</li></ol><h4 id="webpack-server-config-js-"><code>webpack.server.config.js</code> 服务端配置文件</h4><pre><code class="language-js">const webpack = require('webpack')
const merge = require('webpack-merge')
const base = require('./webpack.base.config')
const nodeExternals = require('webpack-node-externals') // Webpack allows you to define externals - modules that should not be bundled.
const VueSSRServerPlugin = require('vue-server-renderer/server-plugin')
const WebpackBar = require('webpackbar')

const plugins = [
    new webpack.DefinePlugin({
        'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'),
        'process.env.VUE_ENV': '"server"'
    }),
    new VueSSRServerPlugin()
]

if (process.env.NODE_ENV == 'production') {
    plugins.push(
        new WebpackBar()
    )
}

module.exports = merge(base, {
    target: 'node',
    devtool: '#source-map',
    entry: './src/entry-server.js',
    output: {
        filename: 'server-bundle.js',
        libraryTarget: 'commonjs2'
    },
    externals: nodeExternals({
        allowlist: /\.css$/ // 防止将某些 import 的包(package)打包到 bundle 中，而是在运行时(runtime)再去从外部获取这些扩展依赖
    }),
    plugins,
    module: {
        rules: [
            {
                test: /\.css$/,
                use: [
                    'vue-style-loader',
                    'css-loader'
                ]
            }
        ]
    },
})</code></pre><p>服务端打包和客户端不同，它将所有文件一起打包成一个文件 <code>server-bundle.js</code>。同时解析 css 需要使用 <code>vue-style-loader</code>，这一点在官方指南中有说明：</p><figure class="kg-card kg-image-card"><img src="https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e2cc5dc0d35d41cdabf633b78667ea1e~tplv-k3u1fbpfcp-watermark.awebp" class="kg-image" alt="e2cc5dc0d35d41cdabf633b78667ea1e~tplv-k3u1fbpfcp-watermark" width="600" height="400" loading="lazy"></figure><h3 id="--4">配置服务器</h3><h4 id="--5">生产环境</h4><p><code>pro-server.js</code> 生产环境服务器配置文件</p><pre><code class="language-js">const fs = require('fs')
const path = require('path')
const express = require('express')
const setApi = require('./api')
const LRU = require('lru-cache') // 缓存
const { createBundleRenderer } = require('vue-server-renderer')
const favicon = require('serve-favicon')
const resolve = file =&gt; path.resolve(__dirname, file)

const app = express()
// 开启 gzip 压缩 https://github.com/woai3c/node-blog/blob/master/doc/optimize.md
const compression = require('compression')
app.use(compression())
// 设置 favicon
app.use(favicon(resolve('../public/favicon.ico')))

// 新版本 需要加 new，旧版本不用
const microCache = new LRU({
    max: 100,
    maxAge: 60 * 60 * 24 * 1000 // 重要提示：缓存资源将在 1 天后过期。
})

const serve = (path) =&gt; {
    return express.static(resolve(path), {
        maxAge: 1000 * 60 * 60 * 24 * 30
    })
}

app.use('/dist', serve('../dist', true))

function createRenderer(bundle, options) {
    return createBundleRenderer(
        bundle,
        Object.assign(options, {
            basedir: resolve('../dist'),
            runInNewContext: false
        })
    )
}

function render(req, res) {
    const hit = microCache.get(req.url)
    if (hit) {
        console.log('Response from cache')
        return res.end(hit)
    }

    res.setHeader('Content-Type', 'text/html')

    const handleError = err =&gt; {
        if (err.url) {
            res.redirect(err.url)
        } else if (err.code === 404) {
            res.status(404).send('404 | Page Not Found')
        } else {
            res.status(500).send('500 | Internal Server Error~')
            console.log(err)
        }
    }

    const context = {
        title: 'SSR 测试', // default title
        url: req.url
    }

    renderer.renderToString(context, (err, html) =&gt; {
        if (err) {
            return handleError(err)
        }

        microCache.set(req.url, html)
        res.send(html)
    })
}

const templatePath = resolve('../public/index.template.html')
const template = fs.readFileSync(templatePath, 'utf-8')
const bundle = require('../dist/vue-ssr-server-bundle.json')
const clientManifest = require('../dist/vue-ssr-client-manifest.json') // 将js文件注入到页面中
const renderer = createRenderer(bundle, {
    template,
    clientManifest
})

const port = 8080

app.listen(port, () =&gt; {
    console.log(`server started at localhost:${ port }`)
})

setApi(app)

app.get('*', render)</code></pre><p>从代码中可以看到，当首次加载页面时，需要调用 <code>createBundleRenderer()</code> 生成一个 renderer，它的参数是打包生成的 <code>vue-ssr-server-bundle.json</code> 和 <code>vue-ssr-client-manifest.json</code> 文件。当返回 HTML 文件后，页面将会被客户端接管。</p><p>在文件的最后有一行代码 <code>app.get('*', render)</code>，它表示所有匹配不到的请求都交给它处理。所以如果你写了 ajax 请求处理函数必须放在前面，就像下面这样：</p><pre><code class="language-js">app.get('/fetchData', (req, res) =&gt; { ... })
app.post('/changeData', (req, res) =&gt; { ... })
app.get('*', render)</code></pre><p>否则你的页面会打不开。</p><h4 id="--6">开发环境</h4><p>开发环境的服务器配置和生产环境没什么不同，区别在于开发环境下的服务器有热更新。</p><p>一般用 webpack 进行开发时，简单的配置一下 dev server 参数就可以使用热更新了，但是 SSR 项目需要自己配置。</p><p>由于 SSR 开发环境服务器的配置文件 <code>setup-dev-server.js</code> 代码太多，我对其进行简化后，大致代码如下：</p><pre><code class="language-js">// dev-server.js
const express = require('express')
const webpack = require('webpack')
const webpackConfig = require('../build/webpack.dev') // 获取 webpack 配置文件
const compiler = webpack(webpackConfig)
const app = express()

app.use(require('webpack-hot-middleware')(compiler))
app.use(require('webpack-dev-middleware')(compiler, {
    noInfo: true,
    stats: {
        colors: true
    }
}))</code></pre><p>同时需要在 webpack 的入口文件加上这一行代码 <code>webpack-hot-middleware/client?reload=true</code>。</p><pre><code class="language-js">// webpack.dev.js
const merge = require('webpack-merge')
const webpackBaseConfig = require('./webpack.base.config.js') // 这个配置和热更新无关，可忽略

module.exports = merge(webpackBaseConfig, {
    mode: 'development',
    entry: {
        app: ['webpack-hot-middleware/client?reload=true' , './client/main.js'] // 开启热模块更新
    },
    plugins: [new webpack.HotModuleReplacementPlugin()]
})</code></pre><p>然后使用 <code>node dev-server.js</code> 来开启前端代码热更新。</p><p>热更新主要使用了两个插件：<code>webpack-dev-middleware</code> 和 <code>webpack-hot-middleware</code>。顾名思义，看名称就知道它们的作用，</p><p><code>webpack-dev-middleware</code> 的作用是生成一个与 webpack 的 compiler 绑定的中间件，然后在 express 启动的 app 中调用这个中间件。</p><p>这个中间件的作用呢，简单总结为以下三点：通过watch mode，监听资源的变更，然后自动打包; 快速编译，走内存；返回中间件，支持express 的 use 格式。</p><p><code>webpack-hot-middleware</code> 插件的作用就是热更新，它需要配合 <code>HotModuleReplacementPlugin</code> 和 <code>webpack-dev-middleware</code> 一起使用。</p><h3 id="-vue-ssr-client-manifest-json-vue-ssr-server-bundle-json">打包文件 <code>vue-ssr-client-manifest.json</code> 和 <code>vue-ssr-server-bundle.json</code></h3><p>webpack 需要对源码打包两次，一次是为客户端环境打包的，一次是为服务端环境打包的。</p><p>为客户端环境打包的文件，和以前我们打包的资源一样，不过多出了一个 <code>vue-ssr-client-manifest.json</code> 文件。服务端环境打包只输出一个 <code>vue-ssr-server-bundle.json</code> 文件。</p><p><code>vue-ssr-client-manifest.json</code> 包含了客户端环境所需的资源名称：</p><figure class="kg-card kg-image-card"><img src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/07cf395cb58348a2a350bc965042b6c4~tplv-k3u1fbpfcp-watermark.awebp" class="kg-image" alt="07cf395cb58348a2a350bc965042b6c4~tplv-k3u1fbpfcp-watermark" width="600" height="400" loading="lazy"></figure><p>从上图中可以看到有三个关键词：</p><ol><li>all，表示这是打包的所有资源。</li><li>initial，表示首页加载必须的资源。</li><li>async，表示需要异步加载的资源。</li></ol><p><code>vue-ssr-server-bundle.json</code> 文件：</p><figure class="kg-card kg-image-card"><img src="https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/388d798839004215b6250e5e1c553794~tplv-k3u1fbpfcp-watermark.awebp" class="kg-image" alt="388d798839004215b6250e5e1c553794~tplv-k3u1fbpfcp-watermark" width="600" height="400" loading="lazy"></figure><ol><li>entry， 服务端入口文件。</li><li>files，服务端依赖的资源。</li></ol><h3 id="--7">填坑记录</h3><h4 id="1-vue-router-failed-to-resolve-async-component-default-referenceerror-window-is-not-defined">1. <code>[vue-router] failed to resolve async component default: referenceerror: window is not defined</code></h4><p>由于在一些文件或第三方文件中可能会用到 window 对象，并且 node 中不存在 window 对象，所以会报错。 此时可在 <code>src/app.js</code> 文件加上以下代码进行判断：</p><pre><code class="language-js">// 在 app.js 文件添加上这段代码，对环境进行判断
if (typeof window === 'undefined') {
	global.window = {}
}</code></pre><h4 id="2-mini-css-extract-plugin-referenceerror-document-is-not-defined">2. <code>mini-css-extract-plugin</code> 插件造成 <code>ReferenceError: document is not defined</code></h4><p>使用 <code>mini-css-extract-plugin</code> 插件打包的的 <code>server bundle</code>， 会使用到 document。由于 node 环境中不存在 document 对象，所以报错。</p><p>解决方案：样式相关的 loader 不要放在 <code>webpack.base.config.js</code> 文件，将其分拆到 <code>webpack.client.config.js</code> 和 <code>webpack.client.server.js</code> 文件。其中 <code>mini-css-extract-plugin</code> 插件要放在 <code>webpack.client.config.js</code> 文件配置。</p><p>base</p><pre><code class="language-js">module: {
    rules: [
        {
            test: /\.vue$/,
            loader: 'vue-loader',
            options: {
                compilerOptions: {
                    preserveWhitespace: false
                }
            }
        },
        {
            test: /\.js$/,
            loader: 'babel-loader',
            exclude: /node_modules/
        },
        {
            test: /\.(png|svg|jpg|gif|ico)$/,
            use: ['file-loader']
        },
        {
            test: /\.(woff|eot|ttf)\??.*$/,
            loader: 'url-loader?name=fonts/[name].[md5:hash:hex:7].[ext]'
        },
    ]
}</code></pre><p>client</p><pre><code class="language-js">module: {
    rules: [
        {
            test: /\.css$/,
            use: [
                {
                    loader: MiniCssExtractPlugin.loader,
                    options: {
                        // 解决 export 'default' (imported as 'mod') was not found
                        esModule: false,
                    },
                },
                'css-loader'
            ]
        }
    ]
}</code></pre><p>server</p><pre><code class="language-js">module: {
    rules: [
        {
            test: /\.css$/,
            use: [
                'vue-style-loader',
                'css-loader'
            ]
        }
    ]
}</code></pre><h4 id="3-">3. 开发环境下跳转页面样式不生效，但生产环境正常。</h4><p>由于开发环境使用的是 <code>memory-fs</code> 插件，打包文件是放在内存中的。如果此时 <code>dist</code> 文件夹有刚才打包留下的资源，就会使用 <code>dist</code> 文件夹中的资源，而不是内存中的资源。并且开发环境和打包环境生成的资源名称是不一样的，所以就造成了这个 BUG。</p><p>解决方法是执行 <code>npm run dev</code> 时，删除 <code>dist</code> 文件夹。所以要在 <code>npm run dev</code> 对应的脚本中加上 <code>rimraf dist</code>。</p><pre><code class="language-json">"dev": "rimraf dist &amp;&amp; node ./server/dev-server.js --mode development",</code></pre><h4 id="4-vue-router-failed-to-resolve-async-component-default-referenceerror-document-is-not-defined">4. <code>[vue-router] Failed to resolve async component default: ReferenceError: document is not defined</code></h4><p>不要在有可能使用到服务端渲染的页面访问 DOM，如果有这种操作请放在 <code>mounted()</code> 钩子函数里。</p><p>如果你引入的数据或者接口有访问 DOM 的操作也会报这种错，在这种情况下可以使用 <code>require()</code>。因为 <code>require()</code> 是运行时加载的，所以可以这样使用：</p><pre><code class="language-js">&lt;script&gt;
// 原来报错的操作，这个接口有 DOM 操作，所以这样使用的时候在服务端会报错。
import { fetchArticles } from '@/api/client'

export default {
  methods: {
    getAppointArticles() {
      fetchArticles({
        tags: this.tags,
        pageSize: this.pageSize,
        pageIndex: this.pageIndex,
      })
      .then(res =&gt; {
      	this.$store.commit('setArticles', res)
      })
    },
  }
}
&lt;/script&gt;</code></pre><p>修改后：</p><pre><code class="language-js">&lt;script&gt;
// 先定义一个外部变量，在 mounted() 钩子里赋值
let fetchArticles
export default {
  mounted() {
    // 由于服务端渲染不会有 mounted() 钩子，所以在这里可以保证是在客户端的情况下引入接口
  	fetchArticles = require('@/api/client').fetchArticles
  },
  methods: {
    getAppointArticles() {
      fetchArticles({
        tags: this.tags,
        pageSize: this.pageSize,
        pageIndex: this.pageIndex,
      })
      .then(res =&gt; {
      	this.$store.commit('setArticles', res)
      })
    },
  }
}
&lt;/script&gt;</code></pre><p>修改后可以正常使用。</p><h4 id="5-">5. 开发环境下，开启服务器后无任何反应，也没见控制台输出报错信息。</h4><p>这个坑其实是有报错信息的，但是没有输出，导致以为没有错误。</p><p>在 <code>setup-dev-server.js</code> 文件中有一行代码 <code>if (stats.errors.length) return</code>，如果有报错就直接返回，不执行后续的操作。导致服务器没任何反应，所以我们可以在这打一个 <code>console.log</code> 语句，打印报错信息。</p><figure class="kg-card kg-image-card"><img src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/6e4f7e6787ab487a9502d2d807a9638d~tplv-k3u1fbpfcp-watermark.awebp" class="kg-image" alt="6e4f7e6787ab487a9502d2d807a9638d~tplv-k3u1fbpfcp-watermark" width="600" height="400" loading="lazy"></figure><h3 id="--8">小结</h3><p>这个 DEMO 是基于官方 DEMO <a href="https://github.com/vuejs/vue-hackernews-2.0/">vue-hackernews-2.0</a> 改造的。不过官方 DEMO 发表于 4 年前，最近修改时间是 2 年前，很多选项参数已经过时了。并且官方 DEMO 需要翻墙才能使用。所以我在此基础上对其进行了改造，改造后的 DEMO 放在 <a href="https://github.com/woai3c/vue-ssr-demo">GitHub</a> 上，它是一个比较完善的 DEMO，可以在此基础上进行二次开发。</p><p>如果你不仅仅满足于一个 DEMO，建议看一看我的<a href="https://github.com/woai3c/node-blog">个人博客项目</a>，它原来是客户端渲染的项目，后来重构为服务端渲染，绝对实战。</p><h2 id="--9">参考资料</h2><ul><li><a href="https://ssr.vuejs.org/zh/">Vue.js 服务器端渲染指南</a></li><li><a href="https://juejin.cn/post/6844904163692937229">vue-ssr服务端渲染透析</a></li><li><a href="https://juejin.cn/post/6844903489450016782">webpack-dev-middleware详解</a></li></ul> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 初学者也能看懂的 Vue3 源码中那些实用的基础工具函数 ]]>
                </title>
                <description>
                    <![CDATA[ 1. 前言 本文通过学习Vue3源码中的工具函数模块的源码，学习源码为自己所用。 > 歌德曾说：读一本好书，就是在和高尚的人谈话。同理可得：读源码，也算是和作者的一种学习交流的方式。 阅读本文，你将学到：  * 如何学习 JavaScript 基础知识，会推荐很多学习资料  * 如何学习调试 vue 3 源码  * 如何学习源码中优秀代码和思想，投入到自己的项目中  * Vue 3 源码 shared 模块中的几十个实用工具函数  * 我的一些经验分享 shared模块中57个工具函数，本次阅读其中的30余个。 2. 环境准备 2.1 读开源项目 贡献指南 打开 vue-next [https://github.com/vuejs/vue-next]，开源项目一般都能在 README.md 或者  .github/contributing.md [https://github.com/vuejs/vue-next/blob/master/.github/contributing.md]找到贡献指南。 而贡献指南写了很多关于参与项目开发的信息。比如怎么跑起来，项目目录结构是怎样 ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/vue3-source-code/</link>
                <guid isPermaLink="false">611b716d020863066536ab6a</guid>
                
                    <category>
                        <![CDATA[ Vue ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ 若川 ]]>
                </dc:creator>
                <pubDate>Mon, 16 Aug 2021 08:21:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2021/08/trnava-university-BEEyeib-am8-unsplash.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <h2 id="1-">1. 前言</h2><p>本文通过学习<code>Vue3</code>源码中的工具函数模块的源码，学习源码为自己所用。</p><blockquote>歌德曾说：读一本好书，就是在和高尚的人谈话。同理可得：读源码，也算是和作者的一种学习交流的方式。</blockquote><p>阅读本文，你将学到：</p><ul><li>如何学习 JavaScript 基础知识，会推荐很多学习资料</li><li>如何学习调试 vue 3 源码</li><li>如何学习源码中优秀代码和思想，投入到自己的项目中</li><li>Vue 3 源码 shared 模块中的几十个实用工具函数</li><li>我的一些经验分享</li></ul><p><code>shared</code>模块中<code>57个</code>工具函数，本次阅读其中的<code>30余个</code>。</p><h2 id="2-">2. 环境准备</h2><h3 id="2-1-">2.1 读开源项目 贡献指南</h3><p>打开 <a href="https://github.com/vuejs/vue-next" rel="noopener noreferrer">vue-next</a>，开源项目一般都能在 <code>README.md</code> 或者 <a href="https://github.com/vuejs/vue-next/blob/master/.github/contributing.md" rel="noopener noreferrer">.github/contributing.md </a>找到贡献指南。</p><p>而贡献指南写了很多关于参与项目开发的信息。比如怎么跑起来，项目目录结构是怎样的。怎么投入开发，需要哪些知识储备等。</p><p>我们可以在<a href="https://github.com/vuejs/vue-next/blob/master/.github/contributing.md#project-structure" rel="noopener noreferrer">项目目录结构</a>描述中，找到<code>shared</code>模块。</p><p><code>shared</code>: Internal utilities shared across multiple packages (especially environment-agnostic utils used by both runtime and compiler packages).</p><p><code>README.md</code> 和 <code>contributing.md</code> 一般都是英文的。可能会难倒一部分人。其实看不懂，完全可以可以借助划词翻译，整页翻译和百度翻译等翻译工具。再把英文加入后续学习计划。</p><p>本文就是讲<code>shared</code>模块，对应的文件路径是：<a href="https://github.com/vuejs/vue-next/blob/master/packages/shared/src/index.ts" rel="noopener noreferrer"><code>vue-next/packages/shared/src/index.ts</code></a></p><p>也可以用<code>github1s</code>访问，速度更快，<a href="https://github1s.com/vuejs/vue-next/blob/master/packages/shared/src/index.ts" rel="noopener noreferrer">github1s packages/shared/src/index.ts</a></p><h3 id="2-2-">2.2 按照项目指南 打包构建代码</h3><p>为了降低文章难度，我按照贡献指南中方法打包把<code>ts</code>转成了<code>js</code>。如果你需要打包，也可以参考下文打包构建。</p><p>你需要确保 <a href="http://nodejs.org/" rel="noopener noreferrer">Node.js</a> 版本是 <code>10+</code>, 而且 <code>yarn</code> 的版本是 <code>1.x</code> <a href="https://yarnpkg.com/en/docs/install" rel="noopener noreferrer">Yarn 1.x</a>。</p><p>你安装的 <code>Node.js</code> 版本很可能是低于 <code>10</code>。最简单的办法就是去官网重新安装。也可以使用 <code>nvm</code>等管理<code>Node.js</code>版本。</p><pre><code class="language-bash">node -v
# v14.16.0
# 全局安装 yarn
# 克隆项目
git clone https://github.com/vuejs/vue-next.git
cd vue-next

# 或者克隆我的项目
git clone https://github.com/lxchuan12/vue-next-analysis.git
cd vue-next-analysis

npm install --global yarn
yarn # install the dependencies of the project
# yarn —ignore-scripts 忽略一些安装，更快速
yarn build
</code></pre><p>可以得到 <code>vue-next/packages/shared/dist/shared.esm-bundler.js</code>，文件也就是纯<code>js</code>文件。也接下就是解释其中的一些方法。</p><p>当然，前面可能比较啰嗦。我可以直接讲 <code>3. 工具函数</code>。但通过我上文的介绍，即使是初学者，都能看懂一些开源项目源码，也许就会有一定的成就感。 另外，面试问到被类似的问题或者笔试题时，你说看<code>Vue3</code>源码学到的，面试官绝对对你刮目相看。</p><h3 id="2-3-sourcemap-vue-next-">2.3 如何生成 sourcemap 调试 vue-next 源码</h3><p>熟悉我的读者知道，我是经常强调生成<code>sourcemap</code>调试看源码，所以顺便提一下如何配置生成<code>sourcemap</code>，如何调试。这部分可以简单略过，动手操作时再仔细看。</p><p>其实<a href="https://github.com/vuejs/vue-next/blob/master/.github/contributing.md" rel="noopener noreferrer">贡献指南</a>里描述了。</p><p>Build with Source Maps Use the <code>--sourcemap</code> or <code>-s</code> flag to build with source maps. Note this will make the build much slower.</p><p>所以在 <code>vue-next/package.json</code> 追加 <code>"dev:sourcemap": "node scripts/dev.js --sourcemap"</code>，<code>yarn dev:sourcemap</code>执行，即可生成<code>sourcemap</code>，或者直接 <code>build</code>。</p><pre><code class="language-json">// package.json
{
    "version": "3.2.1",
    "scripts": {
        "dev:sourcemap": "node scripts/dev.js --sourcemap"
    }
}
</code></pre><p>会在控制台输出类似<code>vue-next/packages/vue/src/index.ts → packages/vue/dist/vue.global.js</code>的信息。</p><p>其中<code>packages/vue/dist/vue.global.js.map</code> 就是<code>sourcemap</code>文件了。</p><p>我们在 Vue3官网找个例子，在 <code>vue-next/examples/index.html</code>。其内容引入<code>packages/vue/dist/vue.global.js</code>。</p><pre><code class="language-js">// vue-next/examples/index.html
&lt;script src="../../packages/vue/dist/vue.global.js"&gt;&lt;/script&gt;
&lt;script&gt;
    const Counter = {
        data() {
            return {
                counter: 0
            }
        }
    }

    Vue.createApp(Counter).mount('#counter')
&lt;/script&gt;
</code></pre><p>然后我们新建一个终端窗口，<code>yarn serve</code>，在浏览器中打开<code>http://localhost:5000/examples/</code>，如下图所示，按<code>F11</code>等进入函数，就可以愉快的调试源码了。</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2021/08/image-5.png" class="kg-image" alt="image-5" width="600" height="400" loading="lazy"></figure><h2 id="3-">3. 工具函数</h2><p>本文主要按照源码 <a href="https://github.com/vuejs/vue-next/blob/master/packages/shared/src/index.ts" rel="noopener noreferrer"><code>vue-next/packages/shared/src/index.ts</code></a> 的顺序来写。也省去了一些从外部导入的方法。</p><p>我们也可以通过<code>ts</code>文件，查看使用函数的位置。同时在<code>VSCode</code>运行调试JS代码，我们比较推荐韩老师写的<code>code runner</code>插件。</p><h3 id="3-1-babelparserdefaultplugins-babel-">3.1 babelParserDefaultPlugins babel 解析默认插件</h3><pre><code class="language-js">/**
 * List of @babel/parser plugins that are used for template expression
 * transforms and SFC script transforms. By default we enable proposals slated
 * for ES2020. This will need to be updated as the spec moves forward.
 * Full list at https://babeljs.io/docs/en/next/babel-parser#plugins
 */
const babelParserDefaultPlugins = [
    'bigInt',
    'optionalChaining',
    'nullishCoalescingOperator'
];
</code></pre><p>这里就是几个默认插件。感兴趣看英文注释查看。</p><h3 id="3-2-empty_obj-">3.2 EMPTY_OBJ 空对象</h3><pre><code class="language-js">const EMPTY_OBJ = (process.env.NODE_ENV !== 'production')
    ? Object.freeze({})
    : {};

// 例子：
// Object.freeze 是 冻结对象
// 冻结的对象最外层无法修改。
const EMPTY_OBJ_1 = Object.freeze({});
EMPTY_OBJ_1.name = '若川';
console.log(EMPTY_OBJ_1.name); // undefined

const EMPTY_OBJ_2 = Object.freeze({ props: { mp: '若川视野' } });
EMPTY_OBJ_2.props.name = '若川';
EMPTY_OBJ_2.props2 = 'props2';
console.log(EMPTY_OBJ_2.props.name); // '若川'
console.log(EMPTY_OBJ_2.props2); // undefined
console.log(EMPTY_OBJ_2);
/**
 * 
 * { 
 *  props: {
     mp: "若川视野",
     name: "若川"
    }
 * }
 * */
</code></pre><p><code>process.env.NODE_ENV</code> 是 <code>node</code> 项目中的一个环境变量，一般定义为：<code>development</code> 和<code>production</code>。根据环境写代码。比如开发环境，有报错等信息，生产环境则不需要这些报错警告。</p><h3 id="3-3-empty_arr-">3.3 EMPTY_ARR 空数组</h3><pre><code class="language-js">const EMPTY_ARR = (process.env.NODE_ENV !== 'production') ? Object.freeze([]) : [];

// 例子：
EMPTY_ARR.push(1) // 报错，也就是为啥生产环境还是用 []
EMPTY_ARR.length = 3;
console.log(EMPTY_ARR.length); // 0
</code></pre><h3 id="3-4-noop-">3.4 NOOP 空函数</h3><pre><code class="language-js">const NOOP = () =&gt; { };

// 很多库的源码中都有这样的定义函数，比如 jQuery、underscore、lodash 等
// 使用场景：1. 方便判断， 2. 方便压缩
// 1. 比如：
const instance = {
    render: NOOP
};

// 条件
const dev = true;
if(dev){
    instance.render = function(){
        console.log('render');
    }
}

// 可以用作判断。
if(instance.render === NOOP){
 console.log('i');
}
// 2. 再比如：
// 方便压缩代码
// 如果是 function(){} ，不方便压缩代码
</code></pre><h3 id="3-5-no-false-">3.5 NO 永远返回 false 的函数</h3><pre><code class="language-js">/**
 * Always return false.
 */
const NO = () =&gt; false;

// 除了压缩代码的好处外。
// 一直返回 false
</code></pre><h3 id="3-6-ison-on-on-">3.6 isOn 判断字符串是不是 on 开头，并且 on 后首字母不是小写字母</h3><pre><code class="language-js">const onRE = /^on[^a-z]/;
const isOn = (key) =&gt; onRE.test(key);

// 例子：
isOn('onChange'); // true
isOn('onchange'); // false
isOn('on3change'); // true
</code></pre><p><code>onRE</code> 是正则。<code>^</code>符号在开头，则表示是什么开头。而在其他地方是指非。</p><p>与之相反的是：<code>$</code>符合在结尾，则表示是以什么结尾。</p><p><code>[^a-z]</code>是指不是<code>a</code>到<code>z</code>的小写字母。</p><p>同时推荐一个正则在线工具。</p><p><a href="https://regex101.com/" rel="noopener noreferrer">regex101</a></p><p>另外正则看老姚的迷你书就够用了。</p><p><a href="https://juejin.cn/post/6844903501034684430" rel="noopener noreferrer">老姚：《JavaScript 正则表达式迷你书》问世了！</a></p><h3 id="3-7-ismodellistener-">3.7 isModelListener 监听器</h3><p>判断字符串是不是以<code>onUpdate:</code>开头</p><pre><code class="language-js">const isModelListener = (key) =&gt; key.startsWith('onUpdate:');

// 例子：
isModelListener('onUpdate:change'); // true
isModelListener('1onUpdate:change'); // false
// startsWith 是 ES6 提供的方法
</code></pre><p><a href="https://es6.ruanyifeng.com/#docs/string-methods" rel="noopener noreferrer">ES6入门教程：字符串的新增方法</a></p><p>很多方法都在《ES6入门教程》中有讲到，就不赘述了。</p><h3 id="3-8-extend-">3.8 extend 继承 合并</h3><p>说合并可能更准确些。</p><pre><code class="language-js">const extend = Object.assign;

// 例子：
const data = { name: '若川' };
const data2 = extend(data, { mp: '若川视野', name: '是若川啊' });
console.log(data); // { name: "是若川啊", mp: "若川视野" }
console.log(data2); // { name: "是若川啊", mp: "若川视野" }
console.log(data === data2); // true
</code></pre><h3 id="3-9-remove-">3.9 remove 移除数组的一项</h3><pre><code class="language-js">const remove = (arr, el) =&gt; {
    const i = arr.indexOf(el);
    if (i &gt; -1) {
        arr.splice(i, 1);
    }
};

// 例子：
const arr = [1, 2, 3];
remove(arr, 3);
console.log(arr); // [1, 2]
</code></pre><p><code>splice</code> 其实是一个很耗性能的方法。删除数组中的一项，其他元素都要移动位置。</p><p><strong>引申</strong>：<a href="https://github.com/axios/axios/blob/master/lib/core/InterceptorManager.js" rel="noopener noreferrer"><code>axios InterceptorManager</code> 拦截器源码</a>中，拦截器用数组存储的。但实际移除拦截器时，只是把拦截器置为 <code>null</code> 。而不是用<code>splice</code>移除。最后执行时为 <code>null</code> 的不执行，同样效果。<code>axios</code> 拦截器这个场景下，不得不说为性能做到了很好的考虑。</p><p>看如下 <code>axios</code> 拦截器代码示例：</p><pre><code class="language-js">// 代码有删减
// 声明
this.handlers = [];

// 移除
if (this.handlers[id]) {
    this.handlers[id] = null;
}

// 执行
if (h !== null) {
    fn(h);
}
</code></pre><h3 id="3-10-hasown-">3.10 hasOwn 是不是自己本身所拥有的属性</h3><pre><code class="language-js">const hasOwnProperty = Object.prototype.hasOwnProperty;
const hasOwn = (val, key) =&gt; hasOwnProperty.call(val, key);

// 例子：

// 特别提醒：__proto__ 是浏览器实现的原型写法，后面还会用到
// 现在已经有提供好几个原型相关的API
// Object.getPrototypeOf
// Object.setPrototypeOf
// Object.isPrototypeOf

// .call 则是函数里 this 显示指定以为第一个参数，并执行函数。

hasOwn({__proto__: { a: 1 }}, 'a') // false
hasOwn({ a: undefined }, 'a') // true
hasOwn({}, 'a') // false
hasOwn({}, 'hasOwnProperty') // false
hasOwn({}, 'toString') // false
// 是自己的本身拥有的属性，不是通过原型链向上查找的。
</code></pre><p>对象API可以看我之前写的一篇文章 <a href="https://mp.weixin.qq.com/s/Y3nL3GPcxiqb3zK6pEuycg" rel="noopener noreferrer">JavaScript 对象所有 API 解析</a>，写的还算全面。</p><h3 id="3-11-isarray-">3.11 isArray 判断数组</h3><pre><code class="language-js">const isArray = Array.isArray;

isArray([]); // true
const fakeArr = { __proto__: Array.prototype, length: 0 };
isArray(fakeArr); // false
fakeArr instanceof Array; // true
// 所以 instanceof 这种情况 不准确
</code></pre><h3 id="3-12-ismap-map-">3.12 isMap 判断是不是 Map 对象</h3><pre><code class="language-js">const isMap = (val) =&gt; toTypeString(val) === '[object Map]';

// 例子：
const map = new Map();
const o = { p: 'Hello World' };

map.set(o, 'content');
map.get(o); // 'content'
isMap(map); // true
</code></pre><p>ES6 提供了 Map 数据结构。它类似于对象，也是键值对的集合，但是“键”的范围不限于字符串，各种类型的值（包括对象）都可以当作键。也就是说，Object 结构提供了“字符串—值”的对应，Map 结构提供了“值—值”的对应，是一种更完善的 Hash 结构实现。如果你需要“键值对”的数据结构，Map 比 Object 更合适。</p><h3 id="3-13-isset-set-">3.13 isSet 判断是不是 Set 对象</h3><pre><code class="language-js">const isSet = (val) =&gt; toTypeString(val) === '[object Set]';

// 例子：
const set = new Set();
isSet(set); // true
</code></pre><p><code>ES6</code> 提供了新的数据结构 <code>Set</code>。它类似于数组，但是成员的值都是唯一的，没有重复的值。</p><p><code>Set</code>本身是一个构造函数，用来生成 <code>Set</code> 数据结构。</p><h3 id="3-14-isdate-date-">3.14 isDate 判断是不是 Date 对象</h3><pre><code class="language-js">const isDate = (val) =&gt; val instanceof Date;

// 例子：
isDate(new Date()); // true

// `instanceof` 操作符左边是右边的实例。但不是很准，但一般够用了。原理是根据原型链向上查找的。

isDate({__proto__ : new Date()); // true
// 实际上是应该是 Object 才对。
// 所以用 instanceof 判断数组也不准确。
// 再比如
({__proto__: [] }) instanceof Array; // true
// 实际上是对象。
// 所以用 数组本身提供的方法 Array.isArray 是比较准确的。
</code></pre><h3 id="3-15-isfunction-">3.15 isFunction 判断是不是函数</h3><pre><code class="language-js">const isFunction = (val) =&gt; typeof val === 'function';
// 判断数组有多种方法，但这个是比较常用也相对兼容性好的。
</code></pre><h3 id="3-16-isstring-">3.16 isString 判断是不是字符串</h3><pre><code class="language-js">const isString = (val) =&gt; typeof val === 'string';

// 例子：
isString('') // true
</code></pre><h3 id="3-17-issymbol-symbol">3.17 isSymbol 判断是不是 Symbol</h3><pre><code class="language-js">const isSymbol = (val) =&gt; typeof val === 'symbol';

// 例子：
let s = Symbol();

typeof s;
// "symbol"
// Symbol 是函数，不需要用 new 调用。
</code></pre><p><code>ES6</code> 引入了一种新的原始数据类型<code>Symbol</code>，表示独一无二的值。</p><h3 id="3-18-isobject-">3.18 isObject 判断是不是对象</h3><pre><code class="language-js">const isObject = (val) =&gt; val !== null &amp;&amp; typeof val === 'object';

// 例子：
isObject(null); // false
isObject({name: '若川'}); // true
// 判断不为 null 的原因是 typeof null 其实 是 object
</code></pre><h3 id="3-19-ispromise-promise">3.19 isPromise 判断是不是 Promise</h3><pre><code class="language-js">const isPromise = (val) =&gt; {
    return isObject(val) &amp;&amp; isFunction(val.then) &amp;&amp; isFunction(val.catch);
};

// 判断是不是Promise对象
const p1 = new Promise(function(resolve, reject){
  resolve('若川');
});
isPromise(p1); // true

// promise 对于初学者来说可能比较难理解。但是重点内容，JS异步编程，要着重掌握。
// 现在 web 开发 Promise 和 async await 等非常常用。
</code></pre><p>可以根据文末推荐的书籍看<code>Promise</code>相关章节掌握。同时也推荐这本迷你书 <a href="http://liubin.org/promises-book/" rel="noopener noreferrer">JavaScript Promise 迷你书（中文版）</a>。</p><h3 id="3-20-objecttostring-">3.20 objectToString 对象转字符串</h3><pre><code class="language-js">const objectToString = Object.prototype.toString;

// 对象转字符串
</code></pre><h3 id="3-21-totypestring-">3.21 toTypeString 对象转字符串</h3><pre><code class="language-js">const toTypeString = (value) =&gt; objectToString.call(value);

// call 是一个函数，第一个参数是 执行函数里面 this 指向。
// 通过这个能获得 类似  "[object String]" 其中 String 是根据类型变化的
</code></pre><h3 id="3-22-torawtype-">3.22 toRawType 对象转字符串 截取后几位</h3><pre><code class="language-js">const toRawType = (value) =&gt; {
    // extract "RawType" from strings like "[object RawType]"
    return toTypeString(value).slice(8, -1);
};

// 截取到
toRawType('');  'String'
</code></pre><p>可以 截取到 <code>String</code> <code>Array</code> 等这些类型</p><p>是 <code>JS</code> 判断数据类型非常重要的知识点。</p><p><code>JS</code> 判断类型也有 typeof ，但不是很准确，而且能够识别出的不多。</p><p>这些算是基础知识</p><p><a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/typeof" rel="noopener noreferrer">mdn typeof 文档</a>，文档比较详细，也实现了一个很完善的<code>type</code>函数，本文就不赘述了。</p><pre><code class="language-js">// typeof 返回值目前有以下8种 
'undefined'
'object'
'boolean'
'number'
'bigint'
'string'
'symobl'
'function'
</code></pre><h3 id="3-23-isplainobject-">3.23 isPlainObject 判断是不是纯粹的对象</h3><pre><code class="language-js">const objectToString = Object.prototype.toString;
const toTypeString = (value) =&gt; objectToString.call(value);
// 
const isPlainObject = (val) =&gt; toTypeString(val) === '[object Object]';

// 前文中 有 isObject 判断是不是对象了。
// isPlainObject 这个函数在很多源码里都有，比如 jQuery 源码和 lodash 源码等，具体实现不一样
// 上文的 isObject([]) 也是 true ，因为 type [] 为 'object'
// 而 isPlainObject([]) 则是false
const Ctor = function(){
    this.name = '我是构造函数';
}
isPlainObject({}); // true
isPlainObject(new Ctor()); // true
</code></pre><h3 id="3-24-isintegerkey-key-">3.24 isIntegerKey 判断是不是数字型的字符串 key 值</h3><pre><code class="language-js">const isIntegerKey = (key) =&gt; isString(key) &amp;&amp;
    key !== 'NaN' &amp;&amp;
    key[0] !== '-' &amp;&amp;
    '' + parseInt(key, 10) === key;

// 例子:
isIntegerKey('a'); // false
isIntegerKey('0'); // true
isIntegerKey('011'); // false
isIntegerKey('11'); // true
// 其中 parseInt 第二个参数是进制。
// 字符串能用数组取值的形式取值。
//  还有一个 charAt 函数，但不常用 
'abc'.charAt(0) // 'a'
// charAt 与数组形式不同的是 取不到值会返回空字符串''，数组形式取值取不到则是 undefined
</code></pre><h3 id="3-25-makemap-isreservedprop">3.25 makeMap &amp;&amp; isReservedProp</h3><p>传入一个以逗号分隔的字符串，生成一个 <code>map</code>(键值对)，并且返回一个函数检测 <code>key</code> 值在不在这个 <code>map</code> 中。第二个参数是小写选项。</p><pre><code class="language-js">/**
 * Make a map and return a function for checking if a key
 * is in that map.
 * IMPORTANT: all calls of this function must be prefixed with
 * \/\*#\_\_PURE\_\_\*\/
 * So that rollup can tree-shake them if necessary.
 */
function makeMap(str, expectsLowerCase) {
    const map = Object.create(null);
    const list = str.split(',');
    for (let i = 0; i &lt; list.length; i++) {
        map[list[i]] = true;
    }
    return expectsLowerCase ? val =&gt; !!map[val.toLowerCase()] : val =&gt; !!map[val];
}
const isReservedProp = /*#__PURE__*/ makeMap(
// the leading comma is intentional so empty string "" is also included
',key,ref,' +
    'onVnodeBeforeMount,onVnodeMounted,' +
    'onVnodeBeforeUpdate,onVnodeUpdated,' +
    'onVnodeBeforeUnmount,onVnodeUnmounted');

// 保留的属性
isReservedProp('key'); // true
isReservedProp('ref'); // true
isReservedProp('onVnodeBeforeMount'); // true
isReservedProp('onVnodeMounted'); // true
isReservedProp('onVnodeBeforeUpdate'); // true
isReservedProp('onVnodeUpdated'); // true
isReservedProp('onVnodeBeforeUnmount'); // true
isReservedProp('onVnodeUnmounted'); // true
</code></pre><h3 id="3-26-cachestringfunction-">3.26 cacheStringFunction 缓存</h3><pre><code class="language-js">const cacheStringFunction = (fn) =&gt; {
    const cache = Object.create(null);
    return ((str) =&gt; {
        const hit = cache[str];
        return hit || (cache[str] = fn(str));
    });
};
</code></pre><p>这个函数也是和上面 MakeMap 函数类似。只不过接收参数的是函数。《JavaScript 设计模式与开发实践》书中的第四章 JS单例模式也是类似的实现。</p><pre><code class="language-js">var getSingle = function(fn){ // 获取单例
    var result;
    return function(){
        return result || (result = fn.apply(this, arguments));
    }
};
</code></pre><p>以下是一些正则，系统学习正则推荐<a href="https://juejin.cn/post/6844903501034684430" rel="noopener noreferrer">老姚：《JavaScript 正则表达式迷你书》问世了！</a>看过的都说好。所以本文不会过多描述正则相关知识点。</p><pre><code class="language-js">// \w 是 0-9a-zA-Z_ 数字 大小写字母和下划线组成
// () 小括号是 分组捕获
const camelizeRE = /-(\w)/g;
/**
 * @private
 */
// 连字符 - 转驼峰  on-click =&gt; onClick
const camelize = cacheStringFunction((str) =&gt; {
    return str.replace(camelizeRE, (_, c) =&gt; (c ? c.toUpperCase() : ''));
});
// \B 是指 非 \b 单词边界。
const hyphenateRE = /\B([A-Z])/g;
/**
 * @private
 */

const hyphenate = cacheStringFunction((str) =&gt; str.replace(hyphenateRE, '-$1').toLowerCase());

// 举例：onClick =&gt; on-click
const hyphenateResult = hyphenate('onClick');
console.log('hyphenateResult', hyphenateResult); // 'on-click'

/**
 * @private
 */
// 首字母转大写
const capitalize = cacheStringFunction((str) =&gt; str.charAt(0).toUpperCase() + str.slice(1));
/**
 * @private
 */
// click =&gt; onClick
const toHandlerKey = cacheStringFunction((str) =&gt; (str ? `on${capitalize(str)}` : ``));

const result = toHandlerKey('click');
console.log(result, 'result'); // 'onClick'
</code></pre><h3 id="3-27-haschanged-">3.27 hasChanged 判断是不是有变化</h3><pre><code class="language-js">// compare whether a value has changed, accounting for NaN.
const hasChanged = (value, oldValue) =&gt; value !== oldValue &amp;&amp; (value === value || oldValue === oldValue);
// 例子：
// 认为 NaN 是不变的
hasChanged(NaN, NaN); // false
hasChanged(1, 1); // false
hasChanged(1, 2); // true
hasChanged(+0, -0); // false
// Obect.is 认为 +0 和 -0 不是同一个值
Object.is(+0, -0); // false           
// Object.is 认为  NaN 和 本身 相比 是同一个值
Object.is(NaN, NaN); // true
// 场景
// watch 监测值是不是变化了

// (value === value || oldValue === oldValue)
// 为什么会有这句 因为要判断 NaN 。认为 NaN 是不变的。因为 NaN === NaN 为 false
</code></pre><p>根据 <code>hasChanged</code> 这个我们继续来看看：<code>Object.is</code> <code>API</code>。</p><p><code>Object.is(value1, value2) (ES6)</code></p><p>该方法用来比较两个值是否严格相等。它与严格比较运算符（===）的行为基本一致。 不同之处只有两个：一是<code>+0</code>不等于<code>-0</code>，而是 <code>NaN</code> 等于自身。</p><pre><code class="language-js">Object.is('若川', '若川'); // true
Object.is({},{}); // false
Object.is(+0, -0); // false
+0 === -0; // true
Object.is(NaN, NaN); // true
NaN === NaN; // false
</code></pre><p><code>ES5</code>可以通过以下代码部署<code>Object.is</code>。</p><pre><code class="language-js">Object.defineProperty(Object, 'is', {
    value: function() {x, y} {
        if (x === y) {
           // 针对+0不等于-0的情况
           return x !== 0 || 1 / x === 1 / y;
        }
        // 针对 NaN的情况
        return x !== x &amp;&amp; y !== y;
    },
    configurable: true,
    enumerable: false,
    writable: true
});
</code></pre><p>根据举例可以说明。</p><h3 id="3-28-invokearrayfns-">3.28 invokeArrayFns 执行数组里的函数</h3><pre><code class="language-js">const invokeArrayFns = (fns, arg) =&gt; {
    for (let i = 0; i &lt; fns.length; i++) {
        fns[i](arg);
    }
};

// 例子：
const arr = [
    function(val){
        console.log(val + '的博客地址是：https://lxchuan12.gitee.io');
    },
    function(val){
        console.log('百度搜索 若川 可以找到' + val);
    },
    function(val){
        console.log('微信搜索 若川视野 可以找到关注' + val);
    },
]
invokeArrayFns(arr, '我');
</code></pre><p>为什么这样写，我们一般都是一个函数执行就行。</p><p>数组中存放函数，函数其实也算是数据。这种写法方便统一执行多个函数。</p><h3 id="3-29-def-">3.29 def 定义对象属性</h3><pre><code class="language-js">const def = (obj, key, value) =&gt; {
    Object.defineProperty(obj, key, {
        configurable: true,
        enumerable: false,
        value
    });
};
</code></pre><p><code>Object.defineProperty</code> 算是一个非常重要的<code>API</code>。还有一个定义多个属性的<code>API</code>：<code>Object.defineProperties(obj, props) (ES5)</code></p><p><code>Object.defineProperty</code> 涉及到比较重要的知识点。<br>在<code>ES3</code>中，除了一些内置属性（如：<code>Math.PI</code>），对象的所有的属性在任何时候都可以被修改、插入、删除。在<code>ES5</code>中，我们可以设置属性是否可以被改变或是被删除——在这之前，它是内置属性的特权。<code>ES5</code>中引入了<strong>属性描述符</strong>的概念，我们可以通过它对所定义的属性有更大的控制权。这些<strong>属性描述符</strong>（特性）包括：</p><p><code>value</code>——当试图获取属性时所返回的值。<br><code>writable</code>——该属性是否可写。<br><code>enumerable</code>——该属性在<code>for in</code>循环中是否会被枚举。<br><code>configurable</code>——该属性是否可被删除。<br><code>set()</code>——该属性的更新操作所调用的函数。<br><code>get()</code>——获取属性值时所调用的函数。<br></p><p>另外，<strong>数据描述符</strong>（其中属性为：<code>enumerable</code>，<code>configurable</code>，<code>value</code>，<code>writable</code>）与<strong>存取描述符</strong>（其中属性为<code>enumerable</code>，<code>configurable</code>，<code>set()</code>，<code>get()</code>）之间是有互斥关系的。在定义了<code>set()</code>和<code>get()</code>之后，描述符会认为存取操作已被 定义了，其中再定义<code>value</code>和<code>writable</code>会<strong>引起错误</strong>。</p><p>以下是<em>ES3</em>风格的属性定义方式：</p><pre><code class="language-js">var person = {};
person.legs = 2;
</code></pre><p>以下是等价的ES5通过<strong>数据描述符</strong>定义属性的方式：</p><pre><code class="language-js">var person = {};
Object.defineProperty(person, 'legs', {
    value: 2,
    writable: true,
    configurable: true,
    enumerable: true
});
</code></pre><p>其中， 除了value的默认值为<code>undefined</code>以外，其他的默认值都为<code>false</code>。这就意味着，如果想要通过这一方式定义一个可写的属性，必须显示将它们设为<code>true</code>。 或者，我们也可以通过<code>ES5</code>的存储描述符来定义：</p><pre><code class="language-js">var person = {};
Object.defineProperty(person, 'legs', {
    set:function(v) {
        return this.value = v;
    },
    get: function(v) {
        return this.value;
    },
    configurable: true,
    enumerable: true
});
person.legs = 2;
</code></pre><p>这样一来，多了许多可以用来描述属性的代码，如果想要防止别人篡改我们的属性，就必须要用到它们。此外，也不要忘了浏览器向后兼容<code>ES3</code>方面所做的考虑。例如，跟添加<code>Array.prototype</code>属性不一样，我们不能再旧版的浏览器中使用<code>shim</code>这一特性。 另外，我们还可以（通过定义<code>nonmalleable</code>属性），在具体行为中运用这些描述符：</p><pre><code class="language-js">var person = {};
Object.defineProperty(person, 'heads', {value: 1});
person.heads = 0; // 0
person.heads; // 1  (改不了)
delete person.heads; // false
person.heads // 1 (删不掉)
</code></pre><p>其他本文就不过多赘述了。更多对象 <code>API</code> 可以查看这篇文章 <a href="https://mp.weixin.qq.com/s/Y3nL3GPcxiqb3zK6pEuycg" rel="noopener noreferrer">JavaScript 对象所有 API 解析</a>。</p><h3 id="3-30-tonumber-">3.30 toNumber 转数字</h3><pre><code class="language-js">const toNumber = (val) =&gt; {
    const n = parseFloat(val);
    return isNaN(n) ? val : n;
};

toNumber('111'); // 111
toNumber('a111'); // 'a111'
parseFloat('a111'); // NaN
isNaN(NaN); // true
</code></pre><p>其实 <code>isNaN</code> 本意是判断是不是 <code>NaN</code> 值，但是不准确的。 比如：<code>isNaN('a')</code> 为 <code>true</code>。 所以 <code>ES6</code> 有了 <code>Number.isNaN</code> 这个判断方法，为了弥补这一个<code>API</code>。</p><pre><code class="language-js">Number.isNaN('a')  // false
Number.isNaN(NaN); // true
</code></pre><h3 id="3-31-getglobalthis-">3.31 getGlobalThis 全局对象</h3><pre><code class="language-js">let _globalThis;
const getGlobalThis = () =&gt; {
    return (_globalThis ||
        (_globalThis =
            typeof globalThis !== 'undefined'
                ? globalThis
                : typeof self !== 'undefined'
                    ? self
                    : typeof window !== 'undefined'
                        ? window
                        : typeof global !== 'undefined'
                            ? global
                            : {}));
};
</code></pre><p>获取全局 <code>this</code> 指向。</p><p>初次执行肯定是 <code>_globalThis</code> 是 <code>undefined</code>。所以会执行后面的赋值语句。</p><p>如果存在 <code>globalThis</code> 就用 <code>globalThis</code>。<a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/globalThis" rel="noopener noreferrer">MDN globalThis</a></p><p>如果存在<code>self</code>，就用<code>self</code>。在 <code>Web Worker</code> 中不能访问到 <code>window</code> 对象，但是我们却能通过 <code>self</code> 访问到 <code>Worker</code> 环境中的全局对象。</p><p>如果存在<code>window</code>，就用<code>window</code>。</p><p>如果存在<code>global</code>，就用<code>global</code>。<code>Node</code>环境下，使用<code>global</code>。</p><p>如果都不存在，使用空对象。可能是微信小程序环境下。</p><p>下次执行就直接返回 <code>_globalThis</code>，不需要第二次继续判断了。这种写法值得我们学习。</p><h2 id="4-">4. 最后推荐一些文章和书籍</h2><p>先推荐我认为不错的<code>JavaScript API</code>的几篇文章和几本值得读的书。</p><p><a href="https://juejin.cn/post/6844903476720320525" rel="noopener noreferrer">JavaScript字符串所有API全解密</a></p><p><a href="https://juejin.cn/post/6844903476216987655" rel="noopener noreferrer">【深度长文】JavaScript数组所有API全解密</a></p><p><a href="https://juejin.cn/post/6844903469824868365" rel="noopener noreferrer">正则表达式前端使用手册</a></p><p><a href="https://juejin.cn/post/6844903501034684430" rel="noopener noreferrer">老姚：《JavaScript 正则表达式迷你书》问世了！</a></p><p><a href="https://mp.weixin.qq.com/s/Y3nL3GPcxiqb3zK6pEuycg" rel="noopener noreferrer">JavaScript 对象所有API解析</a> https://lxchuan12.gitee.io/js-object-api/</p><p><a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript" rel="noopener noreferrer">MDN JavaScript</a></p><p><a href="https://book.douban.com/subject/35175321/" rel="noopener noreferrer">《JavaScript高级程序设计》第4版</a></p><p><a href="https://book.douban.com/subject/35396470/" rel="noopener noreferrer">《JavaScript 权威指南》第7版</a></p><p><a href="https://book.douban.com/subject/26302623/" rel="noopener noreferrer">《JavaScript面向对象编程2》 </a>面向对象讲的很详细。</p><p><a href="http://es6.ruanyifeng.com/" rel="noopener noreferrer">阮一峰老师：《ES6 入门教程》</a></p><p><a href="https://zh.javascript.info/" rel="noopener noreferrer">《现代 JavaScript 教程》</a></p><p><a href="https://book.douban.com/subject/26351021/" rel="noopener noreferrer">《你不知道的JavaScript》上中卷</a></p><p><a href="https://book.douban.com/subject/26382780/" rel="noopener noreferrer">《JavaScript 设计模式与开发实践》</a></p><p>我也是从小白看不懂书经历过来的。到现在写文章分享。</p><p>我看书的方法：多本书同时看，看相同类似的章节，比如函数。看完这本可能没懂，看下一本，几本书看下来基本就懂了，一遍没看懂，再看几遍，可以避免遗忘，巩固相关章节。当然，刚开始看书很难受，看不进。这些书大部分在微信读书都有，如果习惯看纸质书，那可以买来看。</p><p>这时可以看些视频和动手练习一些简单的项目。</p><p>比如：可以自己注册一个<code>github</code>账号，分章节小节，抄写书中的代码，提交到<code>github</code>，练习了才会更有感觉。</p><p>再比如 <a href="https://chinese.freecodecamp.org/" rel="noopener noreferrer">freeCodeCamp 中文在线学习网站</a>。看书是系统学习非常好的方法。后来我就是看源码较多，写文章分享出来给大家。</p><h2 id="5-">5. 总结</h2><p>文中主要通过学习 <code>shared</code> 模块下的几十个工具函数，比如有：<code>isPromise</code>、<code>makeMap</code>、<code>cacheStringFunction</code>、<code>invokeArrayFns</code>、<code>def</code>、<code>getGlobalThis</code>等等。</p><p>同时还分享了<code>vue</code>源码的调试技巧，推荐了一些书籍和看书籍的方法。</p><p>欢迎在我的<a href="https://lxchuan12.gitee.io/">个人网站</a>阅读更多。</p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 深入了解 Vue3 模板编译原理 ]]>
                </title>
                <description>
                    <![CDATA[ Vue 的编译模块包含 4 个目录： compiler-core compiler-dom // 浏览器 compiler-sfc // 单文件组件 compiler-ssr // 服务端渲染 其中 compiler-core 模块是 Vue 编译的核心模块，并且是平台无关的。而剩下的三个都是在 compiler-core 的基础上针对不同的平台作了适配处理。 Vue 的编译分为三个阶段，分别是：parse、transform、codegen。 其中 parse 阶段将模板字符串转化为语法抽象树 AST。transform 阶段则是对 AST 进行了一些转换处理。codegen 阶段根据 AST 生成对应的 render 函数字符串。 Parse Vue 在解析模板字符串时，可分为两种情况：以 < 开头的字符串和不以 < 开头的字符串。 不以 < 开头的字符串有两种情况：它是文本节点或 {{ exp }} 插值表达式。 而以 ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/vue3-template-compilation-principle/</link>
                <guid isPermaLink="false">6118ec3817b8810648f27a34</guid>
                
                    <category>
                        <![CDATA[ Vue ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ woai3c ]]>
                </dc:creator>
                <pubDate>Sun, 15 Aug 2021 10:00:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2021/08/fotis-fotopoulos-6sAl6aQ4OWI-unsplash--1-.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Vue 的编译模块包含 4 个目录：</p><pre><code class="language-js">compiler-core
compiler-dom // 浏览器
compiler-sfc // 单文件组件
compiler-ssr // 服务端渲染</code></pre><p>其中 compiler-core 模块是 Vue 编译的核心模块，并且是平台无关的。而剩下的三个都是在 compiler-core 的基础上针对不同的平台作了适配处理。</p><p>Vue 的编译分为三个阶段，分别是：parse、transform、codegen。</p><p>其中 parse 阶段将模板字符串转化为语法抽象树 AST。transform 阶段则是对 AST 进行了一些转换处理。codegen 阶段根据 AST 生成对应的 render 函数字符串。</p><h2 id="parse">Parse</h2><p>Vue 在解析模板字符串时，可分为两种情况：以 <code>&lt;</code> 开头的字符串和不以 <code>&lt;</code> 开头的字符串。</p><p>不以 <code>&lt;</code> 开头的字符串有两种情况：它是文本节点或 <code>{{ exp }}</code> 插值表达式。</p><p>而以 <code>&lt;</code> 开头的字符串又分为以下几种情况：</p><ol><li>元素开始标签 <code>&lt;div&gt;</code></li><li>元素结束标签 <code>&lt;/div&gt;</code></li><li>注释节点 <code>&lt;!-- 123 &nbsp;--&gt;</code></li><li>文档声明 <code>&lt;!DOCTYPE html&gt;</code></li></ol><p>用伪代码表示，大概过程如下：</p><pre><code class="language-js">while (s.length) {
    if (startsWith(s, '{{')) {
        // 如果以 '{{' 开头
        node = parseInterpolation(context, mode)
    } else if (s[0] === '&lt;') {
        // 以 &lt; 标签开头
        if (s[1] === '!') {
            if (startsWith(s, '&lt;!--')) {
                // 注释
                node = parseComment(context)
            } else if (startsWith(s, '&lt;!DOCTYPE')) {
                // 文档声明，当成注释处理
                node = parseBogusComment(context)
            }
        } else if (s[1] === '/') {
            // 结束标签
            parseTag(context, TagType.End, parent)
        } else if (/[a-z]/i.test(s[1])) {
            // 开始标签
            node = parseElement(context, ancestors)
        }
    } else {
        // 普通文本节点
        node = parseText(context, mode)
    }
}</code></pre><p>在源码中对应的几个函数分别是：</p><ol><li><code>parseChildren()</code>，主入口。</li><li><code>parseInterpolation()</code>，解析双花插值表达式。</li><li><code>parseComment()</code>，解析注释。</li><li><code>parseBogusComment()</code>，解析文档声明。</li><li><code>parseTag()</code>，解析标签。</li><li><code>parseElement()</code>，解析元素节点，它会在内部执行 <code>parseTag()</code>。</li><li><code>parseText()</code>，解析普通文本。</li><li><code>parseAttribute()</code>，解析属性。</li></ol><p>每解析完一个标签、文本、注释等节点时，Vue 就会生成对应的 AST 节点，并且<strong>会把已经解析完的字符串给截断</strong>。</p><p>对字符串进行截断使用的是 <code>advanceBy(context, numberOfCharacters)</code> 函数，context 是字符串的上下文对象，numberOfCharacters 是要截断的字符数。</p><p>我们用一个简单的例子来模拟一下截断操作：</p><pre><code class="language-html">&lt;div name="test"&gt;
  &lt;p&gt;&lt;/p&gt;
&lt;/div&gt;</code></pre><p>首先解析 <code>&lt;div</code>，然后执行 <code>advanceBy(context, 4)</code> 进行截断操作（内部执行的是 <code>s = s.slice(4)</code>），变成：</p><pre><code class="language-html"> name="test"&gt;
  &lt;p&gt;&lt;/p&gt;
&lt;/div&gt;</code></pre><p>再解析属性，并截断，变成：</p><pre><code class="language-html">  &lt;p&gt;&lt;/p&gt;
&lt;/div&gt;</code></pre><p>同理，后面的截断情况为：</p><pre><code class="language-html">&gt;&lt;/p&gt;
&lt;/div&gt;</code></pre><pre><code class="language-html">&lt;/div&gt;</code></pre><pre><code class="language-html">&lt;!-- 所有字符串已经解析完 --&gt;</code></pre><p><strong>AST 节点</strong></p><p>所有的 AST 节点定义都在 compiler-core/ast.ts 文件中，下面是一个元素节点的定义：</p><pre><code class="language-js">export interface BaseElementNode extends Node {
  type: NodeTypes.ELEMENT // 类型
  ns: Namespace // 命名空间 默认为 HTML，即 0
  tag: string // 标签名
  tagType: ElementTypes // 元素类型
  isSelfClosing: boolean // 是否是自闭合标签 例如 &lt;br/&gt; &lt;hr/&gt;
  props: Array&lt;AttributeNode | DirectiveNode&gt; // props 属性，包含 HTML 属性和指令
  children: TemplateChildNode[] // 字节点
}</code></pre><p>一些简单的要点已经讲完了，下面我们再从一个比较复杂的例子来详细讲解一下 parse 的处理过程。</p><pre><code class="language-html">&lt;div name="test"&gt;
  &lt;!-- 这是注释 --&gt;
  &lt;p&gt;{{ test }}&lt;/p&gt;
  一个文本节点
  &lt;div&gt;good job!&lt;/div&gt;
&lt;/div&gt;</code></pre><p>上面的模板字符串假设为 s，第一个字符 s[0] 是 <code>&lt;</code> 开头，那说明它只能是刚才所说的四种情况之一。 这时需要再看一下 s[1] 的字符是什么：</p><ol><li>如果是 <code>!</code>，则调用字符串原生方法 <code>startsWith()</code> 看看是以 <code>'&lt;!--'</code> 开头还是以 <code>'&lt;!DOCTYPE'</code> 开头。虽然这两者对应的处理函数不一样，但它们最终都是解析为注释节点。</li><li>如果是 <code>/</code>，则按结束标签处理。</li><li>如果不是 <code>/</code>，则按开始标签处理。</li></ol><p>从我们的示例来看，这是一个 <code>&lt;div&gt;</code> 开始标签。</p><p>这里还有一点要提一下，Vue 会用一个栈 stack 来保存解析到的元素标签。当它遇到开始标签时，会将这个标签推入栈，遇到结束标签时，将刚才的标签弹出栈。它的作用是保存当前已经解析了，但还没解析完的元素标签。这个栈还有另一个作用，在解析到某个字节点时，通过 <code>stack[stack.length - 1]</code> 可以获取它的父元素。</p><p>从我们的示例来看，它的出入栈顺序是这样的：</p><pre><code class="language-js">1. [div] // div 入栈
2. [div, p] // p 入栈
3. [div] // p 出栈
4. [div, div] // div 入栈
5. [div] // div 出栈
6. [] // 最后一个 div 出栈，模板字符串已解析完，这时栈为空</code></pre><p>接着上文继续分析我们的示例，这时已经知道是 <code>div</code> 标签了，接下来会把已经解析完的 <code>&lt;div</code> 字符串截断，然后解析它的属性。</p><p>Vue 的属性有两种情况：</p><ol><li>HTML 普通属性</li><li>Vue 指令</li></ol><p>根据属性的不同生成的节点不同，HTML 普通属性节点 type 为 6，Vue 指令节点 type 为 7。</p><p>所有的节点类型值如下：</p><pre><code class="language-js">ROOT,  // 根节点 0
ELEMENT, // 元素节点 1
TEXT, // 文本节点 2
COMMENT, // 注释节点 3
SIMPLE_EXPRESSION, // 表达式 4
INTERPOLATION, // 双花插值 {{ }} 5
ATTRIBUTE, // 属性 6
DIRECTIVE, // 指令 7</code></pre><p>属性解析完后，<code>div</code> 开始标签也就解析完了，<code>&lt;div name="test"&gt;</code> 这一行字符串已经被截断。现在剩下的字符串如下：</p><pre><code class="language-html">  &lt;!-- 这是注释 --&gt;
  &lt;p&gt;{{ test }}&lt;/p&gt;
  一个文本节点
  &lt;div&gt;good job!&lt;/div&gt;
&lt;/div&gt;</code></pre><p>注释文本和普通文本节点解析规则都很简单，直接截断，生成节点。注释文本调用 <code>parseComment()</code> 函数处理，文本节点调用 <code>parseText()</code> 处理。</p><p>双花插值的字符串处理逻辑稍微复杂点，例如示例中的 <code>{{ test }}</code>：</p><ol><li>先将双花括号中的内容提取出来，即 <code>test</code>，再对它执行 <code>trim()</code>，去除空格。</li><li>然后会生成两个节点，一个节点是 <code>INTERPOLATION</code>，type 为 5，表示它是双花插值。</li><li>第二个节点是它的内容，即 <code>test</code>，它会生成一个 <code>SIMPLE_EXPRESSION</code> 节点，type 为 4。</li></ol><pre><code class="language-js">return {
  type: NodeTypes.INTERPOLATION, // 双花插值类型
  content: {
    type: NodeTypes.SIMPLE_EXPRESSION,
    isStatic: false, // 非静态节点
    isConstant: false,
    content,
    loc: getSelection(context, innerStart, innerEnd)
  },
  loc: getSelection(context, start)
}</code></pre><p>剩下的字符串解析逻辑和上文的差不多，就不解释了，最后这个示例解析出来的 AST 如下所示：</p><figure class="kg-card kg-image-card"><img src="https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c47d807f0b9d43079610f541b1a9c94c~tplv-k3u1fbpfcp-watermark.awebp" class="kg-image" alt="c47d807f0b9d43079610f541b1a9c94c~tplv-k3u1fbpfcp-watermark" width="600" height="400" loading="lazy"></figure><p>从 AST 上，我们还能看到某些节点上有一些别的属性：</p><ol><li>ns，命名空间，一般为 HTML，值为 0。</li><li>loc，它是一个位置信息，表明这个节点在源 HTML 字符串中的位置，包含行，列，偏移量等信息。</li><li><code>{{ test }}</code> 解析出来的节点会有一个 isStatic 属性，值为 false，表示这是一个动态节点。如果是静态节点，则只会生成一次，并且在后面的阶段一直复用同一个，不用进行 diff 比较。</li></ol><p>另外还有一个 tagType 属性，它有 4 个值：</p><pre><code class="language-js">export const enum ElementTypes {
  ELEMENT, // 0 元素节点
  COMPONENT, // 1 组件
  SLOT, // 2 插槽
  TEMPLATE // 3 模板
}</code></pre><p>主要用于区分上述四种类型节点。</p><h2 id="transform">Transform</h2><p>在 transform 阶段，Vue 会对 AST 进行一些转换操作，主要是根据不同的 AST 节点添加不同的选项参数，这些参数在 codegen 阶段会用到。下面列举一些比较重要的选项：</p><h3 id="cachehandlers">cacheHandlers</h3><p>如果 cacheHandlers 的值为 true，则表示开启事件函数缓存。例如 <code>@click="foo"</code> 默认编译为 <code>{ onClick: foo }</code>，如果开启了这个选项，则编译为</p><pre><code class="language-js">{ onClick: _cache[0] || (_cache[0] = e =&gt; _ctx.foo(e)) }</code></pre><h3 id="hoiststatic">hoistStatic</h3><p>hoistStatic 是一个标识符，表示要不要开启静态节点提升。如果值为 true，静态节点将被提升到 <code>render()</code> 函数外面生成，并被命名为 <code>_hoisted_x</code> 变量。</p><p>例如 <code>一个文本节点</code> 生成的代码为 <code>const _hoisted_2 = /*#__PURE__*/_createTextVNode(" 一个文本节点 ")</code>。</p><p>下面两张图，前者是 <code>hoistStatic = false</code>，后面是 <code>hoistStatic = true</code>。大家可以在<a href="https://link.juejin.cn?target=https%3A%2F%2Fvue-next-template-explorer.netlify.app%2F%23%257B%2522src%2522%253A%2522%255Cr%255Cn%2522%252C%2522options%2522%253A%257B%2522mode%2522%253A%2522module%2522%252C%2522prefixIdentifiers%2522%253Afalse%252C%2522optimizeImports%2522%253Afalse%252C%2522hoistStatic%2522%253Afalse%252C%2522cacheHandlers%2522%253Afalse%252C%2522scopeId%2522%253Anull%252C%2522ssrCssVars%2522%253A%2522%257B%2520color%2520%257D%2522%252C%2522bindingMetadata%2522%253A%257B%2522TestComponent%2522%253A%2522setup%2522%252C%2522foo%2522%253A%2522setup%2522%252C%2522bar%2522%253A%2522props%2522%257D%257D%257D" rel="nofollow noopener noreferrer">网站</a>上自己试一下。</p><figure class="kg-card kg-image-card"><img src="https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/5bdf8e5ea0d740b697e7b3e8dd011eee~tplv-k3u1fbpfcp-watermark.awebp" class="kg-image" alt="5bdf8e5ea0d740b697e7b3e8dd011eee~tplv-k3u1fbpfcp-watermark" width="600" height="400" loading="lazy"></figure><figure class="kg-card kg-image-card"><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/46179b69c89c44c499fa0b95049cebd1~tplv-k3u1fbpfcp-watermark.awebp" class="kg-image" alt="46179b69c89c44c499fa0b95049cebd1~tplv-k3u1fbpfcp-watermark" width="600" height="400" loading="lazy"></figure><h3 id="prefixidentifiers">prefixIdentifiers</h3><p>这个参数的作用是用于代码生成。例如 <code>{{ foo }}</code> 在 module 模式下生成的代码为 <code>_ctx.foo</code>，而在 function 模式下是 <code>with (this) { ... }</code>。因为在 module 模式下，默认为严格模式，不能使用 with 语句。</p><h3 id="patchflags">PatchFlags</h3><p>transform 在对 AST 节点进行转换时，会打上 patchflag 参数，这个参数主要用于 diff 比较过程。当 DOM 节点有这个标志并且大于 0，就代表要更新，没有就跳过。</p><p>我们来看一下 patchflag 的取值范围：</p><pre><code class="language-js">export const enum PatchFlags {
  // 动态文本节点
  TEXT = 1,

  // 动态 class
  CLASS = 1 &lt;&lt; 1, // 2

  // 动态 style
  STYLE = 1 &lt;&lt; 2, // 4

  // 动态属性，但不包含类名和样式
  // 如果是组件，则可以包含类名和样式
  PROPS = 1 &lt;&lt; 3, // 8

  // 具有动态 key 属性，当 key 改变时，需要进行完整的 diff 比较。
  FULL_PROPS = 1 &lt;&lt; 4, // 16

  // 带有监听事件的节点
  HYDRATE_EVENTS = 1 &lt;&lt; 5, // 32

  // 一个不会改变子节点顺序的 fragment
  STABLE_FRAGMENT = 1 &lt;&lt; 6, // 64

  // 带有 key 属性的 fragment 或部分子字节有 key
  KEYED_FRAGMENT = 1 &lt;&lt; 7, // 128

  // 子节点没有 key 的 fragment
  UNKEYED_FRAGMENT = 1 &lt;&lt; 8, // 256

  // 一个节点只会进行非 props 比较
  NEED_PATCH = 1 &lt;&lt; 9, // 512

  // 动态 slot
  DYNAMIC_SLOTS = 1 &lt;&lt; 10, // 1024

  // 静态节点
  HOISTED = -1,

  // 指示在 diff 过程应该要退出优化模式
  BAIL = -2
}</code></pre><p>从上述代码可以看出 patchflag 使用一个 11 位的位图来表示不同的值，每个值都有不同的含义。Vue 在 diff 过程会根据不同的 patchflag 使用不同的 patch 方法。</p><p>下图是经过 transform 后的 AST：</p><figure class="kg-card kg-image-card"><img src="https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/6bac88024ead4ccca1a9be5e3226086a~tplv-k3u1fbpfcp-watermark.awebp" class="kg-image" alt="6bac88024ead4ccca1a9be5e3226086a~tplv-k3u1fbpfcp-watermark" width="600" height="400" loading="lazy"></figure><p>可以看到 codegenNode、helpers 和 hoists 已经被填充上了相应的值。codegenNode 是生成代码要用到的数据，hoists 存储的是静态节点，helpers 存储的是创建 VNode 的函数名称（其实是 Symbol）。</p><p>在正式开始 transform 前，需要创建一个 transformContext，即 transform 上下文。和这三个属性有关的数据和方法如下：</p><pre><code class="language-js">helpers: new Set(),
hoists: [],

// methods
helper(name) {
  context.helpers.add(name)
  return name
},
helperString(name) {
  return `_${helperNameMap[context.helper(name)]}`
},
hoist(exp) {
  context.hoists.push(exp)
  const identifier = createSimpleExpression(
    `_hoisted_${context.hoists.length}`,
    false,
    exp.loc,
    true
  )
  identifier.hoisted = exp
  return identifier
},</code></pre><p>我们来看一下具体的 transform 过程是怎样的，用 <code>&lt;p&gt;{{ test }}&lt;/p&gt;</code> 来做示例。</p><p>这个节点对应的是 <code>transformElement()</code> 转换函数，由于 <code>p</code> 没有绑定动态属性，没有绑定指令，所以重点不在它，而是在 <code>{{ test }}</code> 上。<code>{{ test }}</code> 是一个双花插值表达式，所以将它的 patchFlag 设为 1（动态文本节点），对应的执行代码是 <code>patchFlag |= 1</code>。然后再执行 <code>createVNodeCall()</code> 函数，它的返回值就是这个节点的 codegenNode 值。</p><pre><code class="language-js">node.codegenNode = createVNodeCall(
    context,
    vnodeTag,
    vnodeProps,
    vnodeChildren,
    vnodePatchFlag,
    vnodeDynamicProps,
    vnodeDirectives,
    !!shouldUseBlock,
    false /* disableTracking */,
    node.loc
)</code></pre><p><code>createVNodeCall()</code> 根据这个节点添加了一个 <code>createVNode</code> Symbol 符号，它放在 helpers 里。其实就是要在代码生成阶段引入的帮助函数。</p><pre><code class="language-js">// createVNodeCall() 内部执行过程，已删除多余的代码
context.helper(CREATE_VNODE)

return {
  type: NodeTypes.VNODE_CALL,
  tag,
  props,
  children,
  patchFlag,
  dynamicProps,
  directives,
  isBlock,
  disableTracking,
  loc
}</code></pre><h3 id="hoists">hoists</h3><p>一个节点是否添加到 hoists 中，主要看它是不是静态节点，并且需要将 hoistStatic 设为 true。</p><pre><code class="language-html">&lt;div name="test"&gt; // 属性静态节点
  &lt;!-- 这是注释 --&gt;
  &lt;p&gt;{{ test }}&lt;/p&gt;
  一个文本节点 // 静态节点
  &lt;div&gt;good job!&lt;/div&gt; // 静态节点
&lt;/div&gt;</code></pre><p>可以看到，上面有三个静态节点，所以 hoists 数组有 3 个值。并且无论静态节点嵌套有多深，都会被提升到 hoists 中。</p><h3 id="type-">type 变化</h3><figure class="kg-card kg-image-card"><img src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/25994ecedbac48d3bec7db69e7881dc6~tplv-k3u1fbpfcp-watermark.awebp" class="kg-image" alt="25994ecedbac48d3bec7db69e7881dc6~tplv-k3u1fbpfcp-watermark" width="600" height="400" loading="lazy"></figure><p>从上图可以看到，最外层的 div 的 type 原来为 1，经过 transform 生成的 codegenNode 中的 type 变成了 13。 这个 13 是代码生成对应的类型 <code>VNODE_CALL</code>。另外还有:</p><pre><code class="language-js">// codegen
VNODE_CALL, // 13
JS_CALL_EXPRESSION, // 14
JS_OBJECT_EXPRESSION, // 15
JS_PROPERTY, // 16
JS_ARRAY_EXPRESSION, // 17
JS_FUNCTION_EXPRESSION, // 18
JS_CONDITIONAL_EXPRESSION, // 19
JS_CACHE_EXPRESSION, // 20</code></pre><p>刚才提到的例子 <code>{{ test }}</code>，它的 codegenNode 就是通过调用 <code>createVNodeCall()</code> 生成的：</p><pre><code class="language-js">return {
  type: NodeTypes.VNODE_CALL,
  tag,
  props,
  children,
  patchFlag,
  dynamicProps,
  directives,
  isBlock,
  disableTracking,
  loc
}</code></pre><p>可以从上述代码看到，type 被设置为 NodeTypes.VNODE_CALL，即 13。</p><p>每个不同的节点都由不同的 transform 函数来处理，由于篇幅有限，具体代码请自行查阅。</p><h2 id="codegen">Codegen</h2><p>代码生成阶段最后生成了一个字符串，我们把字符串的双引号去掉，看一下具体的内容是什么：</p><pre><code class="language-js">const _Vue = Vue
const { createVNode: _createVNode, createCommentVNode: _createCommentVNode, createTextVNode: _createTextVNode } = _Vue

const _hoisted_1 = { name: "test" }
const _hoisted_2 = /*#__PURE__*/_createTextVNode(" 一个文本节点 ")
const _hoisted_3 = /*#__PURE__*/_createVNode("div", null, "good job!", -1 /* HOISTED */)

return function render(_ctx, _cache) {
  with (_ctx) {
    const { createCommentVNode: _createCommentVNode, toDisplayString: _toDisplayString, createVNode: _createVNode, createTextVNode: _createTextVNode, openBlock: _openBlock, createBlock: _createBlock } = _Vue

    return (_openBlock(), _createBlock("div", _hoisted_1, [
      _createCommentVNode(" 这是注释 "),
      _createVNode("p", null, _toDisplayString(test), 1 /* TEXT */),
      _hoisted_2,
      _hoisted_3
    ]))
  }
}</code></pre><h3 id="-">代码生成模式</h3><p>可以看到上述代码最后返回一个 <code>render()</code> 函数，作用是生成对应的 VNode。</p><p>其实代码生成有两种模式：module 和 function，由标识符 prefixIdentifiers 决定使用哪种模式。</p><p>function 模式的特点是：使用 <code>const { helpers... } = Vue</code> 的方式来引入帮助函数，也就是是 <code>createVode()</code> <code>createCommentVNode()</code> 这些函数。向外导出使用 <code>return</code> 返回整个 <code>render()</code> 函数。</p><p>module 模式的特点是：使用 es6 模块来导入导出函数，也就是使用 import 和 export。</p><h3 id="--1">静态节点</h3><p>另外还有三个变量是用 <code>_hoisted_</code> 命名的，后面跟着数字，代表这是第几个静态变量。 再看一下 parse 阶段的 HTML 模板字符串：</p><pre><code class="language-html">&lt;div name="test"&gt;
  &lt;!-- 这是注释 --&gt;
  &lt;p&gt;{{ test }}&lt;/p&gt;
  一个文本节点
  &lt;div&gt;good job!&lt;/div&gt;
&lt;/div&gt;</code></pre><p>这个示例只有一个动态节点，即 <code>{{ test }}</code>，剩下的全是静态节点。从生成的代码中也可以看出，生成的节点和模板中的代码是一一对应的。静态节点的作用就是只生成一次，以后直接复用。</p><p>细心的网友可能发现了 <code>_hoisted_2</code> 和 <code>_hoisted_3</code> 变量中都有一个 <code>/*#__PURE__*/</code> 注释。</p><p>这个注释的作用是表示这个函数是纯函数，没有副作用，主要用于 tree-shaking。压缩工具在打包时会将未被使用的代码直接删除（shaking 摇掉）。</p><p>再来看一下生成动态节点 <code>{{ test }}</code> 的代码： <code>_createVNode("p", null, _toDisplayString(test), 1 /* TEXT */)</code>。</p><p>其中 <code>_toDisplayString(test)</code> 的内部实现是：</p><pre><code class="language-js">return val == null
    ? ''
    : isObject(val)
      ? JSON.stringify(val, replacer, 2)
      : String(val)</code></pre><p>代码很简单，就是转成字符串输出。</p><p>而 <code>_createVNode("p", null, _toDisplayString(test), 1 /* TEXT */)</code> 最后一个参数 1 就是 transform 添加的 patchflag 了。</p><h3 id="-helpers">帮助函数 helpers</h3><p>在 transform、codegen 这两个阶段，我们都能看到 helpers 的影子，到底 helpers 是干什么用的？</p><pre><code class="language-js">// Name mapping for runtime helpers that need to be imported from 'vue' in
// generated code. Make sure these are correctly exported in the runtime!
// Using `any` here because TS doesn't allow symbols as index type.
export const helperNameMap: any = {
  [FRAGMENT]: `Fragment`,
  [TELEPORT]: `Teleport`,
  [SUSPENSE]: `Suspense`,
  [KEEP_ALIVE]: `KeepAlive`,
  [BASE_TRANSITION]: `BaseTransition`,
  [OPEN_BLOCK]: `openBlock`,
  [CREATE_BLOCK]: `createBlock`,
  [CREATE_VNODE]: `createVNode`,
  [CREATE_COMMENT]: `createCommentVNode`,
  [CREATE_TEXT]: `createTextVNode`,
  [CREATE_STATIC]: `createStaticVNode`,
  [RESOLVE_COMPONENT]: `resolveComponent`,
  [RESOLVE_DYNAMIC_COMPONENT]: `resolveDynamicComponent`,
  [RESOLVE_DIRECTIVE]: `resolveDirective`,
  [WITH_DIRECTIVES]: `withDirectives`,
  [RENDER_LIST]: `renderList`,
  [RENDER_SLOT]: `renderSlot`,
  [CREATE_SLOTS]: `createSlots`,
  [TO_DISPLAY_STRING]: `toDisplayString`,
  [MERGE_PROPS]: `mergeProps`,
  [TO_HANDLERS]: `toHandlers`,
  [CAMELIZE]: `camelize`,
  [CAPITALIZE]: `capitalize`,
  [SET_BLOCK_TRACKING]: `setBlockTracking`,
  [PUSH_SCOPE_ID]: `pushScopeId`,
  [POP_SCOPE_ID]: `popScopeId`,
  [WITH_SCOPE_ID]: `withScopeId`,
  [WITH_CTX]: `withCtx`
}

export function registerRuntimeHelpers(helpers: any) {
  Object.getOwnPropertySymbols(helpers).forEach(s =&gt; {
    helperNameMap[s] = helpers[s]
  })
}</code></pre><p>其实帮助函数就是在代码生成时从 Vue 引入的一些函数，以便让程序正常执行，从上面生成的代码中就可以看出来。而 helperNameMap 是默认的映射表名称，这些名称就是要从 Vue 引入的函数名称。</p><p>另外，我们还能看到一个注册函数 <code>registerRuntimeHelpers(helpers: any()</code>，它是干什么用的呢？</p><p>我们知道编译模块 compiler-core 是平台无关的，而 compiler-dom 是浏览器相关的编译模块。为了能在浏览器正常运行 Vue 程序，就得把浏览器相关的 Vue 数据和函数导入进来。 <code>registerRuntimeHelpers(helpers: any()</code> 正是用来做这件事的，从 compiler-dom 的 runtimeHelpers.ts 文件就能看出来：</p><pre><code class="language-js">registerRuntimeHelpers({
  [V_MODEL_RADIO]: `vModelRadio`,
  [V_MODEL_CHECKBOX]: `vModelCheckbox`,
  [V_MODEL_TEXT]: `vModelText`,
  [V_MODEL_SELECT]: `vModelSelect`,
  [V_MODEL_DYNAMIC]: `vModelDynamic`,
  [V_ON_WITH_MODIFIERS]: `withModifiers`,
  [V_ON_WITH_KEYS]: `withKeys`,
  [V_SHOW]: `vShow`,
  [TRANSITION]: `Transition`,
  [TRANSITION_GROUP]: `TransitionGroup`
})</code></pre><p>它运行 <code>registerRuntimeHelpers(helpers: any()</code>，往映射表注入了浏览器相关的部分函数。</p><p><strong>helpers 是怎么使用的呢</strong>?</p><p>在 parse 阶段，解析到不同节点时会生成对应的 type。</p><p>在 transform 阶段，会生成一个 helpers，它是一个 set 数据结构。每当它转换 AST 时，都会根据 AST 节点的 type 添加不同的 helper 函数。</p><p>例如，假设它现在正在转换的是一个注释节点，它会执行 <code>context.helper(CREATE_COMMENT)</code>，内部实现相当于 <code>helpers.add('createCommentVNode')</code>。然后在 codegen 阶段，遍历 helpers，将程序需要的函数从 Vue 里导入，代码实现如下：</p><pre><code class="language-js">// 这是 module 模式
`import { ${ast.helpers
  .map(s =&gt; `${helperNameMap[s]} as _${helperNameMap[s]}`)
  .join(', ')} } from ${JSON.stringify(runtimeModuleName)}\n`</code></pre><h3 id="--2">如何生成代码？</h3><p>从 codegen.ts 文件中，可以看到很多代码生成函数：</p><pre><code class="language-js">generate() // 代码生成入口文件
genFunctionExpression() // 生成函数表达式
genNode() // 生成 Vnode 节点
...</code></pre><p>生成代码则是根据不同的 AST 节点调用不同的代码生成函数，最终将代码字符串拼在一起，输出一个完整的代码字符串。</p><p>老规矩，还是看一个例子：</p><pre><code class="language-js">const _hoisted_1 = { name: "test" }
const _hoisted_2 = /*#__PURE__*/_createTextVNode(" 一个文本节点 ")
const _hoisted_3 = /*#__PURE__*/_createVNode("div", null, "good job!", -1 /* HOISTED */)</code></pre><p>看一下这段代码是怎么生成的，首先执行 <code>genHoists(ast.hoists, context)</code>，将 transform 生成的静态节点数组 hoists 作为第一个参数。<code>genHoists()</code> 内部实现：</p><pre><code class="language-js">hoists.forEach((exp, i) =&gt; {
    if (exp) {
        push(`const _hoisted_${i + 1} = `);
        genNode(exp, context);
        newline();
    }
})</code></pre><p>从上述代码可以看到，遍历 hoists 数组，调用 <code>genNode(exp, context)</code>。<code>genNode()</code> 根据不同的 type 执行不同的函数。</p><pre><code class="language-js">const _hoisted_1 = { name: "test" }</code></pre><p>这一行代码中的 <code>const _hoisted_1 = </code> 由 <code>genHoists()</code> 生成，<code>{ name: "test" }</code> 由 <code>genObjectExpression()</code> 生成。 同理，剩下的两行代码生成过程也是如此，只是最终调用的函数不同。</p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 手把手教你写一个脚手架（二） ]]>
                </title>
                <description>
                    <![CDATA[ 如果你没看过本系列的第一篇文章 [https://chinese.freecodecamp.org/news/create-a-scaffold/] ，建议先看一遍再来阅读本文，效果更好。 这是 mini-cli 项目的 GitHub 地址 [https://github.com/woai3c/mini-cli]。v3 版本的代码在 v3 分支，v4 版本的代码在 v4 分支。 第三个版本 v3 第三个版本主要添加了两个功能：  1. 将项目拆分为 monorepo 的组织方式。  2. 新增 add 命令，可以通过 mvc add xxx 命令的方式来添加插件。 monorepo 首先来简单了解一下 monorepo 和 multirepo。它们都是项目管理的一种方式，multirepo 就是将不同的项目放在不同的 git 仓库维护，而 monorepo 将多个项目放在同一个 git 仓库中维护。在 v3 版本里，我要将 mini-cli 改造成 monorepo 的方式，把不同的插件当成一个个独立的项目来维护。 ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/create-a-scaffold-2/</link>
                <guid isPermaLink="false">610ba8b5c8a51b065cd0771d</guid>
                
                    <category>
                        <![CDATA[ Vue ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ woai3c ]]>
                </dc:creator>
                <pubDate>Thu, 05 Aug 2021 09:39:08 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2021/08/glenn-carstens-peters-npxXWgQ33ZQ-unsplash.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>如果你没看过本系列的<a href="https://chinese.freecodecamp.org/news/create-a-scaffold/">第一篇文章</a>，建议先看一遍再来阅读本文，效果更好。</p><p>这是<strong> mini-cli</strong> 项目的 <a href="https://github.com/woai3c/mini-cli">GitHub 地址</a>。v3 版本的代码在 v3 分支，v4 版本的代码在 v4 分支。</p><h2 id="-v3">第三个版本 v3</h2><p>第三个版本主要添加了两个功能：</p><ol><li>将项目拆分为 monorepo 的组织方式。</li><li>新增 add 命令，可以通过 <code>mvc add xxx</code> 命令的方式来添加插件。</li></ol><h3 id="monorepo">monorepo</h3><p>首先来简单了解一下 monorepo 和 multirepo。它们都是项目管理的一种方式，multirepo 就是将不同的项目放在不同的 git 仓库维护，而 monorepo 将多个项目放在同一个 git 仓库中维护。在 v3 版本里，我要将 mini-cli 改造成 monorepo 的方式，把不同的插件当成一个个独立的项目来维护。</p><p>在将项目改造成 monorepo 后，目录如下所示：</p><pre><code>├─packages
│  ├─@mvc
│  │  ├─cli # 核心插件
│  │  ├─cli-plugin-babel # babel 插件
│  │  ├─cli-plugin-linter # linter 插件
│  │  ├─cli-plugin-router # router 插件
│  │  ├─cli-plugin-vue # vue 插件
│  │  ├─cli-plugin-vuex # vuex 插件
│  │  └─cli-plugin-webpack # webpack 插件
└─scripts # commit message 验证脚本 和项目无关 不需关注
│─lerna.json
|─package.json</code></pre><h4 id="monorepo-">monorepo 改造过程</h4><p>全局安装 lerna</p><pre><code>npm install -g lerna</code></pre><p>创建项目</p><pre><code>git init mini-cli</code></pre><p>初始化</p><pre><code>cd mini-cli
lerna init</code></pre><p>创建 package</p><pre><code>lerna create xxx</code></pre><p>由于 <code>cli</code> 是脚手架核心代码，在这里需要调用其他插件，因为要将其他插件添加到 <code>@mvc/cli</code> 的依赖项</p><pre><code class="language-bash"># 如果是添加到 devDependencies，则需要在后面加上 --dev
# 下载第三方依赖也是同样的命令
lerna add @mvc/cli-plugin-babel --scope=@mvc/cli</code></pre><p>改造成 monorepo-repo 后的脚手架功能和第二版没有区别，只是将插件相关的代码独立成一个单独的 repo，后续可以将插件单独发布到 npm。</p><h4 id="-monorepo-">使用 monorepo 的优点</h4><ol><li>如果采用 multirepo 的方式开发，在本地调试时如果需要调用其他插件，则需要先执行 <code>npm i</code> 安装，才能使用。采用 monorepo 则没有这种烦恼，可以直接调用在 packages 目录里的其他插件，方便开发调试。</li><li>如果多个插件都进行了修改，执行 <code>lerna publish</code> 时可以同时发布已经修改过的插件，不用每个单独发布。</li></ol><h3 id="add-">add 命令</h3><p>将项目改造成 monorepo-repo 的目的就是为了后续方便做扩展。例如生成的项目原来是不支持 router 的，在中途突然想加入 router 功能，就可以执行命令 <code>mvc add router</code> 添加 <code>vue-router</code> 依赖以及相关的模板代码。</p><p>先来看一下 add 命令的代码：</p><pre><code class="language-js">const path = require('path')
const inquirer = require('inquirer')
const Generator = require('./Generator')
const clearConsole = require('./utils/clearConsole')
const PackageManager = require('./PackageManager')
const getPackage = require('./utils/getPackage')
const readFiles = require('./utils/readFiles')

async function add(name) {
    const targetDir = process.cwd()
    const pkg = getPackage(targetDir)
    // 清空控制台
    clearConsole()

    let answers = {}
    try {
        const pluginPrompts = require(`@mvc/cli-plugin-${name}/prompts`)
        answers = await inquirer.prompt(pluginPrompts)
    } catch (error) {
        console.log(error)
    }

    const generator = new Generator(pkg, targetDir, await readFiles(targetDir))
    const pm = new PackageManager(targetDir, answers.packageManager)
    require(`@mvc/cli-plugin-${name}/generator`)(generator, answers)

    await generator.generate()
    // 下载依赖
    await pm.install()
}

module.exports = add</code></pre><p>由于 v3 版本仍然是在本地开发的，所以没有将相关插件发布到 npm 上，因为可以直接引用插件，而不需执行 <code>npm i</code> 安装。在 v2 版本执行 <code>create</code> 命令创建项目时，所有的交互提示语都是放在 <code>cli</code> 插件下的，但是 add 命令是单独添加一个插件，因此还需要在每个插件下添加一个 <code>prompts.js</code> 文件（如果不需要，可以不加），里面是一些和用户交互的语句。例如用 add 命令添加 router 插件时，会询问是否选择 history 模式。</p><pre><code class="language-js">const chalk = require('chalk')

module.exports = [
    {
        name: 'historyMode',
        type: 'confirm',
        message: `Use history mode for router? ${chalk.yellow(`(Requires proper server setup for index fallback in production)`)}`,
        description: `By using the HTML5 History API, the URLs don't need the '#' character anymore.`,
    },
]</code></pre><p>从 add 命令的代码逻辑可以看出来，如果新加的插件有 prompts.js 文件就读取代码弹出交互语句。否则跳过，直接进行下载。</p><h2 id="-v4">第四个版本 v4</h2><p>v4 版本主要将 webpack 的 dev 和 build 功能做成了动态，原来的脚手架生成的项目是有一个 build 目录，里面是 webpack 的一些配置代码。v4 版本的脚手架生成的项目是没有 build 目录的。</p><p>这一个功能通过新增的 <code>mvc-cli-service</code> 插件来实现，生成的项目中会有以下两个脚本命令：</p><pre><code class="language-js">scripts: {
    serve: 'mvc-cli-service serve',
    build: 'mvc-cli-service build',
},</code></pre><p>当运行 <code>npm run serve</code> 时，就会执行命令 <code>mvc-cli-service serve</code>。这一块的代码如下：</p><pre><code class="language-js">#!/usr/bin/env node
const webpack = require('webpack')
const WebpackDevServer = require('webpack-dev-server')
const devConfig = require('../lib/dev.config')
const buildConfig = require('../lib/pro.config')

const args = process.argv.slice(2)
if (args[0] === 'serve') {
    const compiler = webpack(devConfig)
    const server = new WebpackDevServer(compiler)

    server.listen(8080, '0.0.0.0', err =&gt; {
        console.log(err)
    })
} else if (args[0] === 'build') {
    webpack(buildConfig, (err, stats) =&gt; {
        if (err) console.log(err)
        if (stats.hasErrors()) {
            console.log(new Error('Build failed with errors.'))
        }
    })
} else {
    console.log('error command')
}</code></pre><p>原理如下（<a href="https://www.ruanyifeng.com/blog/2016/10/npm_scripts.html">npm scripts 使用指南</a>）：</p><p>npm 脚本的原理非常简单。每当执行npm run，就会自动新建一个 Shell，在这个 Shell 里面执行指定的脚本命令。因此，只要是 Shell（一般是 Bash）可以运行的命令，就可以写在 npm 脚本里面。</p><p>比较特别的是，npm run新建的这个 Shell，会将当前目录的node_modules/.bin子目录加入PATH变量，执行结束后，再将PATH变量恢复原样。</p><p>上述代码对执行的命令进行了判断，如果是 <code>serve</code>，就 new 一个 <code>WebpackDevServer</code> 实例启动开发环境。如果是 <code>build</code>，就用 webpack 进行打包。</p><p>vue-cli 的 webpack 配置是动态的，使用了 chainwebpack 来动态添加不同的配置，我这个 demo 是直接写死的，主要是没时间，所以没有再深入研究。</p><h3 id="-npm-">发布到 npm 后</h3><p>下载 <code>mini-cli</code> 脚手架，其实下载的只是核心插件 <code>mvc-cli</code>。如果这个插件需要引用其他插件，则需要先进行安装，再调用。因此对 <code>create</code> <code>add</code> 命令需要做一些修改。下面看一下 <code>create</code> 命令代码的改动：</p><pre><code class="language-js">answers.features.forEach(feature =&gt; {
    if (feature !== 'service') {
        pkg.devDependencies[`mvc-cli-plugin-${feature}`] = '~1.0.0'
    } else {
        pkg.devDependencies['mvc-cli-service'] = '~1.0.0'
    }
})

await writeFileTree(targetDir, {
    'package.json': JSON.stringify(pkg, null, 2),
})

await pm.install()

// 根据用户选择的选项加载相应的模块，在 package.json 写入对应的依赖项
// 并且将对应的 template 模块渲染
answers.features.forEach(feature =&gt; {
    if (feature !== 'service') {
        require(`mvc-cli-plugin-${feature}/generator`)(generator, answers)
    } else {
        require(`mvc-cli-service/generator`)(generator, answers)
    }
})

await generator.generate()

// 下载依赖
await pm.install()</code></pre><p>上面的代码就是新增的逻辑，在用户选择完需要的插件后，将这些插件写入到 <code>pkg</code> 对象，然后生成 <code>package.json</code> 文件，再执行 <code>npm install</code> 安装依赖。安装完插件后，再读取每个插件的 <code>generator</code> 目录/文件代码，从而生成模板或再次添加不同的依赖。然后再执行一次安装。</p><h3 id="-">发布遇到的坑</h3><p>v3 版本的插件有一个前缀 <code>@mvc</code>，由于带有 @ 前缀的 npm 包会默认作为私人包，因此遇到了一些坑。花费了挺长的时间，后来懒得弄了，干脆将所有的插件重新改了前缀名，变成 <code>mvc</code> 开头的前缀。</p><h2 id="--1">参考资料</h2><ul><li><a href="https://juejin.cn/post/6844904194999058440">lerna 多包管理实践</a></li><li><a href="https://github.com/vuejs/vue-cli">vue-cli</a></li><li><a href="https://www.ruanyifeng.com/blog/2016/10/npm_scripts.html">npm scripts 使用指南</a></li></ul> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 手把手教你写一个脚手架 ]]>
                </title>
                <description>
                    <![CDATA[ 最近在学习 vue-cli [https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Fvuejs%2Fvue-cli]  的源码，获益良多。为了让自己理解得更加深刻，我决定模仿它造一个轮子，争取尽可能多的实现原有的功能。 我将这个轮子分成三个版本：  1. 尽可能用最少的代码实现一个最简版本的脚手架。  2. 在 1 的基础上添加一些辅助功能，例如选择包管理器、npm 源等等。  3. 实现插件化，可以自由的进行扩展。在不影响内部源码的情况下，添加功能。 有人可能不懂脚手架是什么。按我的理解，脚手架就是帮助你把项目的基础架子搭好。例如项目依赖、模板、构建工具等等。让你不用从零开始配置一个项目，尽可能快的进行业务开发。 建议在阅读本文时，能够结合项目源码一起配合使用，效果更好。这是项目地址 mini-cli [https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Fwoai3c%2Fmini-cli] 。项目中的每一个分支都对应一个版本，例如第一个版本对应的 g ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/create-a-scaffold/</link>
                <guid isPermaLink="false">61065d91c8a51b065cd0736c</guid>
                
                    <category>
                        <![CDATA[ Vue ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ woai3c ]]>
                </dc:creator>
                <pubDate>Sun, 01 Aug 2021 08:40:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2021/08/jexo-tj7Bj_743JA-unsplash.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>最近在学习 <a href="https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Fvuejs%2Fvue-cli" rel="nofollow noopener noreferrer">vue-cli</a> 的源码，获益良多。为了让自己理解得更加深刻，我决定模仿它造一个轮子，争取尽可能多的实现原有的功能。</p><p>我将这个轮子分成三个版本：</p><ol><li>尽可能用最少的代码实现一个最简版本的脚手架。</li><li>在 1 的基础上添加一些辅助功能，例如选择包管理器、npm 源等等。</li><li>实现插件化，可以自由的进行扩展。在不影响内部源码的情况下，添加功能。</li></ol><p>有人可能不懂脚手架是什么。按我的理解，脚手架就是帮助你把项目的基础架子搭好。例如项目依赖、模板、构建工具等等。让你不用从零开始配置一个项目，尽可能快的进行业务开发。</p><p>建议在阅读本文时，能够结合项目源码一起配合使用，效果更好。这是项目地址 <a href="https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Fwoai3c%2Fmini-cli" rel="nofollow noopener noreferrer">mini-cli</a>。项目中的每一个分支都对应一个版本，例如第一个版本对应的 git 分支为 v1。所以在阅读源码时，记得要切换到对应的分支。</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2024/07/image-5.png" class="kg-image" alt="image-5" width="313" height="301" loading="lazy"></figure><h2 id="-v1">第一个版本 v1</h2><p>第一个版本的功能比较简单，大致为：</p><ol><li>用户输入命令，准备创建项目。</li><li>脚手架解析用户命令，并弹出交互语句，询问用户创建项目需要哪些功能。</li><li>用户选择自己需要的功能。</li><li>脚手架根据用户的选择创建 <code>package.json</code> 文件，并添加对应的依赖项。</li><li>脚手架根据用户的选择渲染项目模板，生成文件（例如 <code>index.html</code>、<code>main.js</code>、<code>App.vue</code> 等文件）。</li><li>执行 <code>npm install</code> 命令安装依赖。</li></ol><p>项目目录树：</p><pre><code class="language-bash">├─.vscode
├─bin 
│  ├─mvc.js # mvc 全局命令
├─lib
│  ├─generator # 各个功能的模板
│  │  ├─babel # babel 模板
│  │  ├─linter # eslint 模板
│  │  ├─router # vue-router 模板
│  │  ├─vue # vue 模板
│  │  ├─vuex # vuex 模板
│  │  └─webpack # webpack 模板
│  ├─promptModules # 各个模块的交互提示语
│  └─utils # 一系列工具函数
│  ├─create.js # create 命令处理函数
│  ├─Creator.js # 处理交互提示
│  ├─Generator.js # 渲染模板
│  ├─PromptModuleAPI.js # 将各个功能的提示语注入 Creator
└─scripts # commit message 验证脚本 和项目无关 不需关注</code></pre><h3 id="-">处理用户命令</h3><p>脚手架第一个功能就是处理用户的命令，这需要使用 <a href="https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Ftj%2Fcommander.js%2Fblob%2Fmaster%2FReadme_zh-CN.md" rel="nofollow noopener noreferrer">commander.js</a>。这个库的功能就是解析用户的命令，提取出用户的输入交给脚手架。例如这段代码：</p><pre><code class="language-js">#!/usr/bin/env node
const program = require('commander')
const create = require('../lib/create')

program
.version('0.1.0')
.command('create &lt;name&gt;')
.description('create a new project')
.action(name =&gt; { 
    create(name)
})

program.parse()</code></pre><p>它使用 commander 注册了一个 <code>create</code> 命令，并设置了脚手架的版本和描述。我将这段代码保存在项目下的 <code>bin</code> 目录，并命名为 <code>mvc.js</code>。然后在 <code>package.json</code> 文件添加这段代码：</p><pre><code class="language-js">"bin": {
  "mvc": "./bin/mvc.js"
},</code></pre><p>再执行 <a href="https://link.juejin.cn/?target=https%3A%2F%2Fdocs.npmjs.com%2Fcli%2Fv7%2Fcommands%2Fnpm-link" rel="nofollow noopener noreferrer">npm link</a>，就可以将 <code>mvc</code> 注册成全局命令。这样在电脑上的任何地方都能使用 <code>mvc</code> 命令了。实际上，就是用 <code>mvc</code> 命令来代替执行 <code>node ./bin/mvc.js</code>。</p><p>假设用户在命令行上输入 <code>mvc create demo</code>（实际上执行的是 <code>node ./bin/mvc.js create demo</code>），<code>commander</code> 解析到命令 <code>create</code> 和参数 <code>demo</code>。然后脚手架可以在 <code>action</code> 回调里取到参数 <code>name</code>（值为 demo）。</p><h3 id="--1">和用户交互</h3><p>取到用户要创建的项目名称 <code>demo</code> 之后，就可以弹出交互选项，询问用户要创建的项目需要哪些功能。这需要用到 <a href="https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2FSBoudrias%2FInquirer.js%2F" rel="nofollow noopener noreferrer">Inquirer.js</a>。<code>Inquirer.js</code> 的功能就是弹出一个问题和一些选项，让用户选择。并且选项可以指定是多选、单选等等。</p><p>例如下面的代码：</p><pre><code class="language-js">const prompts = [
    {
        "name": "features", // 选项名称
        "message": "Check the features needed for your project:", // 选项提示语
        "pageSize": 10,
        "type": "checkbox", // 选项类型 另外还有 confirm list 等
        "choices": [ // 具体的选项
            {
                "name": "Babel",
                "value": "babel",
                "short": "Babel",
                "description": "Transpile modern JavaScript to older versions (for compatibility)",
                "link": "https://babeljs.io/",
                "checked": true
            },
            {
                "name": "Router",
                "value": "router",
                "description": "Structure the app with dynamic pages",
                "link": "https://router.vuejs.org/"
            },
        ]
    }
]

inquirer.prompt(prompts)</code></pre><p>弹出的问题和选项如下：</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2024/07/image-6.png" class="kg-image" alt="image-6" srcset="https://chinese.freecodecamp.org/news/content/images/size/w600/2024/07/image-6.png 600w, https://chinese.freecodecamp.org/news/content/images/2024/07/image-6.png 830w" sizes="(min-width: 720px) 720px" width="830" height="59" loading="lazy"></figure><p>问题的类型 <code>"type": "checkbox"</code> 是 <code>checkbox</code> 说明是多选。如果两个选项都进行选中的话，返回来的值为：</p><pre><code class="language-js">{ features: ['babel', 'router'] }</code></pre><p>其中 <code>features</code> 是上面问题中的 <code>name</code> 属性。<code>features</code> 数组中的值则是每个选项中的 <code>value</code>。</p><p><code>Inquirer.js</code> 还可以提供具有相关性的问题，也就是上一个问题选择了指定的选项，下一个问题才会显示出来。例如下面的代码：</p><pre><code class="language-js">{
    name: 'Router',
    value: 'router',
    description: 'Structure the app with dynamic pages',
    link: 'https://router.vuejs.org/',
},
{
    name: 'historyMode',
    when: answers =&gt; answers.features.includes('router'),
    type: 'confirm',
    message: `Use history mode for router? ${chalk.yellow(`(Requires proper server setup for index fallback in production)`)}`,
    description: `By using the HTML5 History API, the URLs don't need the '#' character anymore.`,
    link: 'https://router.vuejs.org/guide/essentials/history-mode.html',
},</code></pre><p>第二个问题中有一个属性 <code>when</code>，它的值是一个函数 <code>answers =&gt; answers.features.includes('router')</code>。当函数的执行结果为 <code>true</code>，第二个问题才会显示出来。如果你在上一个问题中选择了 <code>router</code>，它的结果就会变为 <code>true</code>。弹出第二个问题：问你路由模式是否选择 <code>history</code> 模式。</p><p>大致了解 <code>Inquirer.js</code> 后，就可以明白这一步我们要做什么了。主要就是将脚手架支持的功能配合对应的问题、可选值在控制台上展示出来，供用户选择。获取到用户具体的选项值后，再渲染模板和依赖。</p><h4 id="--2">有哪些功能</h4><p>先来看一下第一个版本支持哪些功能：</p><ul><li>vue</li><li>vue-router</li><li>vuex</li><li>babel</li><li>webpack</li><li>linter(eslint)</li></ul><p>由于这是一个 vue 相关的脚手架，所以 vue 是默认提供的，不需要用户选择。另外构建工具 webpack 提供了开发环境和打包的功能，也是必需的，不用用户进行选择。所以可供用户选择的功能只有 4 个：</p><ul><li>vue-router</li><li>vuex</li><li>babel</li><li>linter</li></ul><p>现在我们先来看一下这 4 个功能对应的交互提示语相关的文件。它们全部放在 <code>lib/promptModules</code> 目录下：</p><pre><code class="language-js">-babel.js
-linter.js
-router.js
-vuex.js</code></pre><p>每个文件包含了和它相关的所有交互式问题。例如刚才的示例，说明 <code>router</code> 相关的问题有两个。下面再看一下 <code>babel.js</code> 的代码：</p><pre><code class="language-js">module.exports = (api) =&gt; {
    api.injectFeature({
        name: 'Babel',
        value: 'babel',
        short: 'Babel',
        description: 'Transpile modern JavaScript to older versions (for compatibility)',
        link: 'https://babeljs.io/',
        checked: true,
    })
}</code></pre><p>只有一个问题，就是问下用户需不需要 <code>babel</code> 功能，默认为 <code>checked: true</code>，也就是需要。</p><h4 id="--3">注入问题</h4><p>用户使用 <code>create</code> 命令后，脚手架需要将所有功能的交互提示语句聚合在一起：</p><pre><code class="language-js">// craete.js
const creator = new Creator()
// 获取各个模块的交互提示语
const promptModules = getPromptModules()
const promptAPI = new PromptModuleAPI(creator)
promptModules.forEach(m =&gt; m(promptAPI))
// 清空控制台
clearConsole()

// 弹出交互提示语并获取用户的选择
const answers = await inquirer.prompt(creator.getFinalPrompts())
    
function getPromptModules() {
    return [
        'babel',
        'router',
        'vuex',
        'linter',
    ].map(file =&gt; require(`./promptModules/${file}`))
}

// Creator.js
class Creator {
    constructor() {
        this.featurePrompt = {
            name: 'features',
            message: 'Check the features needed for your project:',
            pageSize: 10,
            type: 'checkbox',
            choices: [],
        }

        this.injectedPrompts = []
    }

    getFinalPrompts() {
        this.injectedPrompts.forEach(prompt =&gt; {
            const originalWhen = prompt.when || (() =&gt; true)
            prompt.when = answers =&gt; originalWhen(answers)
        })
    
        const prompts = [
            this.featurePrompt,
            ...this.injectedPrompts,
        ]
    
        return prompts
    }
}

module.exports = Creator


// PromptModuleAPI.js
module.exports = class PromptModuleAPI {
    constructor(creator) {
        this.creator = creator
    }

    injectFeature(feature) {
        this.creator.featurePrompt.choices.push(feature)
    }

    injectPrompt(prompt) {
        this.creator.injectedPrompts.push(prompt)
    }
}</code></pre><p>以上代码的逻辑如下：</p><ol><li>创建 <code>creator</code> 对象</li><li>调用 <code>getPromptModules()</code> 获取所有功能的交互提示语</li><li>再调用 <code>PromptModuleAPI</code> 将所有交互提示语注入到 <code>creator</code> 对象</li><li>通过 <code>const answers = await inquirer.prompt(creator.getFinalPrompts())</code> 在控制台弹出交互语句，并将用户选择结果赋值给 <code>answers</code> 变量。</li></ol><p>如果所有功能都选上，<code>answers</code> 的值为：</p><pre><code class="language-js">{
  features: [ 'vue', 'webpack', 'babel', 'router', 'vuex', 'linter' ], // 项目具有的功能
  historyMode: true, // 路由是否使用 history 模式
  eslintConfig: 'airbnb', // esilnt 校验代码的默认规则，可被覆盖
  lintOn: [ 'save' ] // 保存代码时进行校验
}</code></pre><h3 id="--4">项目模板</h3><p>获取用户的选项后就该开始渲染模板和生成 <code>package.json</code> 文件了。先来看一下如何生成 <code>package.json</code> 文件：</p><pre><code class="language-js">// package.json 文件内容
const pkg = {
    name,
    version: '0.1.0',
    dependencies: {},
    devDependencies: {},
}</code></pre><p>先定义一个 <code>pkg</code> 变量来表示 <code>package.json</code> 文件，并设定一些默认值。</p><p>所有的项目模板都放在 <code>lib/generator</code> 目录下：</p><pre><code class="language-bash">├─lib
│  ├─generator # 各个功能的模板
│  │  ├─babel # babel 模板
│  │  ├─linter # eslint 模板
│  │  ├─router # vue-router 模板
│  │  ├─vue # vue 模板
│  │  ├─vuex # vuex 模板
│  │  └─webpack # webpack 模板</code></pre><p>每个模板的功能都差不多：</p><ol><li>向 <code>pkg</code> 变量注入依赖项</li><li>提供模板文件</li></ol><h4 id="--5">注入依赖</h4><p>下面是 <code>babel</code> 相关的代码：</p><pre><code class="language-js">module.exports = (generator) =&gt; {
    generator.extendPackage({
        babel: {
            presets: ['@babel/preset-env'],
        },
        dependencies: {
            'core-js': '^3.8.3',
        },
        devDependencies: {
            '@babel/core': '^7.12.13',
            '@babel/preset-env': '^7.12.13',
            'babel-loader': '^8.2.2',
        },
    })
}</code></pre><p>可以看到，模板调用 <code>generator</code> 对象的 <code>extendPackage()</code> 方法向 <code>pkg</code> 变量注入了 <code>babel</code> 相关的所有依赖。</p><pre><code class="language-js">extendPackage(fields) {
    const pkg = this.pkg
    for (const key in fields) {
        const value = fields[key]
        const existing = pkg[key]
        if (isObject(value) &amp;&amp; (key === 'dependencies' || key === 'devDependencies' || key === 'scripts')) {
            pkg[key] = Object.assign(existing || {}, value)
        } else {
            pkg[key] = value
        }
    }
}</code></pre><p>注入依赖的过程就是遍历所有用户已选择的模板，并调用 <code>extendPackage()</code> 注入依赖。</p><h4 id="--6">渲染模板</h4><p>脚手架是怎么渲染模板的呢？用 <code>vuex</code> 举例，先看一下它的代码：</p><pre><code class="language-js">module.exports = (generator) =&gt; {
	// 向入口文件 `src/main.js` 注入代码 import store from './store'
    generator.injectImports(generator.entryFile, `import store from './store'`)
	
    // 向入口文件 `src/main.js` 的 new Vue() 注入选项 store
    generator.injectRootOptions(generator.entryFile, `store`)
	
    // 注入依赖
    generator.extendPackage({
        dependencies: {
            vuex: '^3.6.2',
        },
    })
	
    // 渲染模板
    generator.render('./template', {})
}</code></pre><p>可以看到渲染的代码为 <code>generator.render('./template', {})</code>。<code>./template</code> 是模板目录的路径：</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2024/07/image-7.png" class="kg-image" alt="image-7" width="193" height="65" loading="lazy"></figure><p>所有的模板代码都放在 <code>template</code> 目录下，<code>vuex</code> 将会在用户创建的目录下的 <code>src</code> 目录生成 <code>store</code> 文件夹，里面有一个 <code>index.js</code> 文件。它的内容为：</p><pre><code class="language-js">import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
    state: {
    },
    mutations: {
    },
    actions: {
    },
    modules: {
    },
})</code></pre><p>这里简单描述一下 <code>generator.render()</code> 的渲染过程。</p><p><strong>第一步</strong>， 使用 <a href="https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Fsindresorhus%2Fglobby" rel="nofollow noopener noreferrer">globby</a> 读取模板目录下的所有文件：</p><pre><code class="language-js">const _files = await globby(['**/*'], { cwd: source, dot: true })</code></pre><p><strong>第二步</strong>，遍历所有读取的文件。如果文件是二进制文件，则不作处理，渲染时直接生成文件。否则读取文件内容，再调用 <a href="https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Fmde%2Fejs" rel="nofollow noopener noreferrer">ejs</a> 进行渲染：</p><pre><code class="language-js">// 返回文件内容
const template = fs.readFileSync(name, 'utf-8')
return ejs.render(template, data, ejsOptions)</code></pre><p>使用 <code>ejs</code> 的好处，就是可以结合变量来决定是否渲染某些代码。例如 <code>webpack</code> 的模板中有这样一段代码：</p><pre><code class="language-js">module: {
      rules: [
          &lt;%_ if (hasBabel) { _%&gt;
          {
              test: /\.js$/,
              loader: 'babel-loader',
              exclude: /node_modules/,
          },
          &lt;%_ } _%&gt;
      ],
  },</code></pre><p><code>ejs</code> 可以根据用户是否选择了 <code>babel</code> 来决定是否渲染这段代码。如果 <code>hasBabel</code> 为 <code>false</code>，则这段代码：</p><pre><code class="language-js">{
    test: /\.js$/,
    loader: 'babel-loader',
    exclude: /node_modules/,
},</code></pre><p>将不会被渲染出来。<code>hasBabel</code> 的值是调用 <code>render()</code> 时用参数传过去的：</p><pre><code class="language-js">generator.render('./template', {
    hasBabel: options.features.includes('babel'),
    lintOnSave: options.lintOn.includes('save'),
})</code></pre><p><strong>第三步</strong>，注入特定代码。回想一下刚才 <code>vuex</code> 中的：</p><pre><code class="language-js">// 向入口文件 `src/main.js` 注入代码 import store from './store'
generator.injectImports(generator.entryFile, `import store from './store'`)

// 向入口文件 `src/main.js` 的 new Vue() 注入选项 store
generator.injectRootOptions(generator.entryFile, `store`)</code></pre><p>这两行代码的作用是：在项目入口文件 <code>src/main.js</code> 中注入特定的代码。</p><p><code>vuex</code> 是 <code>vue</code> 的一个状态管理库，属于 <code>vue</code> 全家桶中的一员。如果创建的项目没有选择 <code>vuex</code> 和 <code>vue-router</code>。则 <code>src/main.js</code> 的代码为：</p><pre><code class="language-js">import Vue from 'vue'
import App from './App.vue'

Vue.config.productionTip = false

new Vue({
    render: (h) =&gt; h(App),
}).$mount('#app')</code></pre><p>如果选择了 <code>vuex</code>，它会注入上面所说的两行代码，现在 <code>src/main.js</code> 代码变为：</p><pre><code class="language-js">import Vue from 'vue'
import store from './store' // 注入的代码
import App from './App.vue'

Vue.config.productionTip = false

new Vue({
  store, // 注入的代码
  render: (h) =&gt; h(App),
}).$mount('#app')</code></pre><p>这里简单描述一下代码的注入过程：</p><ol><li>使用 <a href="https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Fvuejs%2Fvue-codemod" rel="nofollow noopener noreferrer">vue-codemod</a> 将代码解析成语法抽象树 AST。</li><li>然后将要插入的代码变成 AST 节点插入到上面所说的 AST 中。</li><li>最后将新的 AST 重新渲染成代码。</li></ol><h3 id="-package-json-">提取 <code>package.json</code> 的部分选项</h3><p>一些第三方库的配置项可以放在 <code>package.json</code> 文件，也可以自己独立生成一份文件。例如 <code>babel</code> 在 <code>package.json</code> 中注入的配置为：</p><pre><code class="language-json">babel: {
    presets: ['@babel/preset-env'],
}</code></pre><p>我们可以调用 <code>generator.extractConfigFiles()</code> 将内容提取出来并生成 <code>babel.config.js</code> 文件：</p><pre><code class="language-js">module.exports = {
    presets: ['@babel/preset-env'],
}</code></pre><h3 id="--7">生成文件</h3><p>渲染好的模板文件和 <code>package.json</code> 文件目前还是在内存中，并没有真正的在硬盘上创建。这时可以调用 <code>writeFileTree()</code> 将文件生成：</p><pre><code class="language-js">const fs = require('fs-extra')
const path = require('path')

module.exports = async function writeFileTree(dir, files) {
    Object.keys(files).forEach((name) =&gt; {
        const filePath = path.join(dir, name)
        fs.ensureDirSync(path.dirname(filePath))
        fs.writeFileSync(filePath, files[name])
    })
}</code></pre><p>这段代码的逻辑如下：</p><ol><li>遍历所有渲染好的文件，逐一生成。</li><li>在生成一个文件时，确认它的父目录在不在，如果不在，就先生成父目录。</li><li>写入文件。</li></ol><p>例如现在一个文件路径为 <code>src/test.js</code>，第一次写入时，由于还没有 <code>src</code> 目录。所以会先生成 <code>src</code> 目录，再生成 <code>test.js</code> 文件。</p><h3 id="webpack">webpack</h3><p>webpack 需要提供开发环境下的热加载、编译等服务，还需要提供打包服务。目前 webpack 的代码比较少，功能比较简单。而且生成的项目中，webpack 配置代码是暴露出来的。这留待 v3 版本再改进。</p><h3 id="--8">添加新功能</h3><p>添加一个新功能，需要在两个地方添加代码：分别是 <code>lib/promptModules</code> 和 <code>lib/generator</code>。在 <code>lib/promptModules</code> 中添加的是这个功能相关的交互提示语。在 <code>lib/generator</code> 中添加的是这个功能相关的依赖和模板代码。</p><p>不过不是所有的功能都需要添加模板代码的，例如 <code>babel</code> 就不需要。在添加新功能时，有可能会对已有的模板代码造成影响。例如我现在需要项目支持 <code>ts</code>。除了添加 <code>ts</code> 相关的依赖，还得在 <code>webpack</code> <code>vue</code> <code>vue-router</code> <code>vuex</code> <code>linter</code> 等功能中修改原有的模板代码。</p><p>举个例子，在 <code>vue-router</code> 中，如果支持 <code>ts</code>，则这段代码：</p><pre><code class="language-js">const routes = [ // ... ]</code></pre><p>需要修改为：</p><pre><code class="language-js">&lt;%_ if (hasTypeScript) { _%&gt;
const routes: Array&lt;RouteConfig&gt; = [ // ... ]
&lt;%_ } else { _%&gt;
const routes = [ // ... ]
&lt;%_ } _%&gt;</code></pre><p>因为 <code>ts</code> 的值有类型。</p><p>总之，添加的新功能越多，各个功能的模板代码也会越来越多。并且还需要考虑到各个功能之间的影响。</p><h3 id="--9">下载依赖</h3><p>下载依赖需要使用 <a href="https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Fsindresorhus%2Fexeca" rel="nofollow noopener noreferrer">execa</a>，它可以调用子进程执行命令。</p><pre><code class="language-js">const execa = require('execa')

module.exports = function executeCommand(command, cwd) {
    return new Promise((resolve, reject) =&gt; {
        const child = execa(command, [], {
            cwd,
            stdio: ['inherit', 'pipe', 'inherit'],
        })

        child.stdout.on('data', buffer =&gt; {
            process.stdout.write(buffer)
        })

        child.on('close', code =&gt; {
            if (code !== 0) {
                reject(new Error(`command failed: ${command}`))
                return
            }

            resolve()
        })
    })
}

// create.js 文件
console.log('\n正在下载依赖...\n')
// 下载依赖
await executeCommand('npm install', path.join(process.cwd(), name))
console.log('\n依赖下载完成! 执行下列命令开始开发：\n')
console.log(`cd ${name}`)
console.log(`npm run dev`)</code></pre><p>调用 <code>executeCommand()</code> 开始下载依赖，参数为 <code>npm install</code> 和用户创建的项目路径。为了能让用户看到下载依赖的过程，我们需要使用下面的代码将子进程的输出传给主进程，也就是输出到控制台：</p><pre><code class="language-js">child.stdout.on('data', buffer =&gt; {
    process.stdout.write(buffer)
})</code></pre><p>下面我用动图演示一下 v1 版本的创建过程：</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2024/07/68747470733a2f2f696d672d626c6f672e6373646e696d672e636e2f696d675f636f6e766572742f36323437393566336230336635303931383233316361353636306663386662612e676966.gif" class="kg-image" alt="68747470733a2f2f696d672d626c6f672e6373646e696d672e636e2f696d675f636f6e766572742f36323437393566336230336635303931383233316361353636306663386662612e676966" width="959" height="219" loading="lazy"></figure><p>创建成功的项目截图：</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2024/07/image-9.png" class="kg-image" alt="image-9" srcset="https://chinese.freecodecamp.org/news/content/images/size/w600/2024/07/image-9.png 600w, https://chinese.freecodecamp.org/news/content/images/2024/07/image-9.png 961w" sizes="(min-width: 720px) 720px" width="961" height="310" loading="lazy"></figure><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2024/07/image-10.png" class="kg-image" alt="image-10" width="389" height="211" loading="lazy"></figure><h2 id="-v2">第二个版本 v2</h2><p>第二个版本在 v1 的基础上添加了一些辅助功能：</p><ol><li>创建项目时判断该项目是否已存在，支持覆盖和合并创建。</li><li>选择功能时提供默认配置和手动选择两种模式。</li><li>如果用户的环境同时存在 yarn 和 npm，则会提示用户要使用哪个包管理器。</li><li>如果 npm 的默认源速度比较慢，则提示用户是否要切换到淘宝源。</li><li>如果用户是手动选择功能，在结束后会询问用户是否要将这次的选择保存为默认配置。</li></ol><h3 id="--10">覆盖和合并</h3><p>创建项目时，先提前判断一下该项目是否存在：</p><pre><code class="language-js">const targetDir = path.join(process.cwd(), name)
// 如果目标目录已存在，询问是覆盖还是合并
if (fs.existsSync(targetDir)) {
    // 清空控制台
    clearConsole()

    const { action } = await inquirer.prompt([
        {
            name: 'action',
            type: 'list',
            message: `Target directory ${chalk.cyan(targetDir)} already exists. Pick an action:`,
            choices: [
                { name: 'Overwrite', value: 'overwrite' },
                { name: 'Merge', value: 'merge' },
            ],
        },
    ])

    if (action === 'overwrite') {
        console.log(`\nRemoving ${chalk.cyan(targetDir)}...`)
        await fs.remove(targetDir)
    }
}</code></pre><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2024/07/image-11.png" class="kg-image" alt="image-11" srcset="https://chinese.freecodecamp.org/news/content/images/size/w600/2024/07/image-11.png 600w, https://chinese.freecodecamp.org/news/content/images/2024/07/image-11.png 688w" width="688" height="54" loading="lazy"></figure><p>如果选择 <code>overwrite</code>，则进行移除 <code>fs.remove(targetDir)</code>。</p><h3 id="--11">默认配置和手动模式</h3><p>先在代码中提前把默认配置的代码写好：</p><pre><code class="language-js">exports.defaultPreset = {
    features: ['babel', 'linter'],
    historyMode: false,
    eslintConfig: 'airbnb',
    lintOn: ['save'],
}</code></pre><p>这个配置默认使用 <code>babel</code> 和 <code>eslint</code>。</p><p>然后生成交互提示语时，先调用 <code>getDefaultPrompts()</code> 方法获取默认配置。</p><pre><code class="language-js">getDefaultPrompts() {
    const presets = this.getPresets()
    const presetChoices = Object.entries(presets).map(([name, preset]) =&gt; {
        let displayName = name

        return {
            name: `${displayName} (${preset.features})`,
            value: name,
        }
    })

    const presetPrompt = {
        name: 'preset',
        type: 'list',
        message: `Please pick a preset:`,
        choices: [
            // 默认配置
            ...presetChoices,
            // 这是手动模式提示语
            {
                name: 'Manually select features',
                value: '__manual__',
            },
        ],
    }

    const featurePrompt = {
        name: 'features',
        when: isManualMode,
        type: 'checkbox',
        message: 'Check the features needed for your project:',
        choices: [],
        pageSize: 10,
    }

    return {
        presetPrompt,
        featurePrompt,
    }
}</code></pre><p>这样配置后，在用户选择功能前会先弹出这样的提示语：</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2024/07/image-12.png" class="kg-image" alt="image-12" width="359" height="57" loading="lazy"></figure><h3 id="--12">包管理器</h3><p>在 <code>vue-cli</code> 创建项目时，会生成一个 <code>.vuerc</code> 文件，里面会记录一些关于项目的配置信息。例如使用哪个包管理器、npm 源是否使用淘宝源等等。为了避免和 <code>vue-cli</code> 冲突，本脚手架生成的配置文件为 <code>.mvcrc</code>。</p><p>这个 <code>.mvcrc</code> 文件保存在用户的 <code>home</code> 目录下（不同操作系统目录不同）。我的是 win10 操作系统，保存目录为 <code>C:\Users\bin</code>。获取用户的 <code>home</code> 目录可以通过以下代码获取：</p><pre><code class="language-js">const os = require('os')
os.homedir()</code></pre><p><code>.mvcrc</code> 文件还会保存用户创建项目的配置，这样当用户重新创建项目时，就可以直接选择以前创建过的配置，不用再一步步的选择项目功能。</p><p>在第一次创建项目时，<code>.mvcrc</code> 文件是不存在的。如果这时用户还安装了 yarn，脚手架就会提示用户要使用哪个包管理器：</p><pre><code class="language-js">// 读取 `.mvcrc` 文件
const savedOptions = loadOptions()
// 如果没有指定包管理器并且存在 yarn
if (!savedOptions.packageManager &amp;&amp; hasYarn) {
    const packageManagerChoices = []

    if (hasYarn()) {
        packageManagerChoices.push({
            name: 'Use Yarn',
            value: 'yarn',
            short: 'Yarn',
        })
    }

    packageManagerChoices.push({
        name: 'Use NPM',
        value: 'npm',
        short: 'NPM',
    })

    otherPrompts.push({
        name: 'packageManager',
        type: 'list',
        message: 'Pick the package manager to use when installing dependencies:',
        choices: packageManagerChoices,
    })
}</code></pre><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2024/07/image-13.png" class="kg-image" alt="image-13" srcset="https://chinese.freecodecamp.org/news/content/images/size/w600/2024/07/image-13.png 600w, https://chinese.freecodecamp.org/news/content/images/2024/07/image-13.png 649w" width="649" height="63" loading="lazy"></figure><p>当用户选择 yarn 后，下载依赖的命令就会变为 <code>yarn</code>；如果选择了 npm，下载命令则为 <code>npm install</code>：</p><pre><code class="language-js">const PACKAGE_MANAGER_CONFIG = {
    npm: {
        install: ['install'],
    },
    yarn: {
        install: [],
    },
}

await executeCommand(
    this.bin, // 'yarn' or 'npm'
    [
        ...PACKAGE_MANAGER_CONFIG[this.bin][command],
        ...(args || []),
    ],
    this.context,
)</code></pre><h3 id="-npm-">切换 npm 源</h3><p>当用户选择了项目功能后，会先调用 <code>shouldUseTaobao()</code> 方法判断是否需要切换淘宝源：</p><pre><code class="language-js">const execa = require('execa')
const chalk = require('chalk')
const request = require('./request')
const { hasYarn } = require('./env')
const inquirer = require('inquirer')
const registries = require('./registries')
const { loadOptions, saveOptions } = require('./options')
  
async function ping(registry) {
    await request.get(`${registry}/vue-cli-version-marker/latest`)
    return registry
}
  
function removeSlash(url) {
    return url.replace(/\/$/, '')
}
  
let checked
let result
  
module.exports = async function shouldUseTaobao(command) {
    if (!command) {
        command = hasYarn() ? 'yarn' : 'npm'
    }
  
    // ensure this only gets called once.
    if (checked) return result
    checked = true
  
    // previously saved preference
    const saved = loadOptions().useTaobaoRegistry
    if (typeof saved === 'boolean') {
        return (result = saved)
    }
  
    const save = val =&gt; {
        result = val
        saveOptions({ useTaobaoRegistry: val })
        return val
    }
  
    let userCurrent
    try {
        userCurrent = (await execa(command, ['config', 'get', 'registry'])).stdout
    } catch (registryError) {
        try {
        // Yarn 2 uses `npmRegistryServer` instead of `registry`
            userCurrent = (await execa(command, ['config', 'get', 'npmRegistryServer'])).stdout
        } catch (npmRegistryServerError) {
            return save(false)
        }
    }
  
    const defaultRegistry = registries[command]
    if (removeSlash(userCurrent) !== removeSlash(defaultRegistry)) {
        // user has configured custom registry, respect that
        return save(false)
    }
  
    let faster
    try {
        faster = await Promise.race([
            ping(defaultRegistry),
            ping(registries.taobao),
        ])
    } catch (e) {
        return save(false)
    }
  
    if (faster !== registries.taobao) {
        // default is already faster
        return save(false)
    }
  
    if (process.env.VUE_CLI_API_MODE) {
        return save(true)
    }
  
    // ask and save preference
    const { useTaobaoRegistry } = await inquirer.prompt([
        {
            name: 'useTaobaoRegistry',
            type: 'confirm',
            message: chalk.yellow(
                ` Your connection to the default ${command} registry seems to be slow.\n`
            + `   Use ${chalk.cyan(registries.taobao)} for faster installation?`,
            ),
        },
    ])
    
    // 注册淘宝源
    if (useTaobaoRegistry) {
        await execa(command, ['config', 'set', 'registry', registries.taobao])
    }

    return save(useTaobaoRegistry)
}</code></pre><p>上面代码的逻辑为：</p><ol><li>先判断默认配置文件 <code>.mvcrc</code> 是否有 <code>useTaobaoRegistry</code> 选项。如果有，直接将结果返回，无需判断。</li><li>向 npm 默认源和淘宝源各发一个 <code>get</code> 请求，通过 <code>Promise.race()</code> 来调用。这样更快的那个请求会先返回，从而知道是默认源还是淘宝源速度更快。</li><li>如果淘宝源速度更快，向用户提示是否切换到淘宝源。</li><li>如果用户选择淘宝源，则调用 <code>await execa(command, ['config', 'set', 'registry', registries.taobao])</code> 将当前 npm 的源改为淘宝源，即 <code>npm config set registry https://registry.npm.taobao.org</code>。如果是 yarn，则命令为 <code>yarn config set registry https://registry.npm.taobao.org</code>。</li></ol><h4 id="--13">一点疑问</h4><p>其实 <code>vue-cli</code> 是没有这段代码的：</p><pre><code class="language-js">// 注册淘宝源
if (useTaobaoRegistry) {
    await execa(command, ['config', 'set', 'registry', registries.taobao])
}</code></pre><p>这是我自己加的。主要是我没有在 <code>vue-cli</code> 中找到显式注册淘宝源的代码，它只是从配置文件读取出是否使用淘宝源，或者将是否使用淘宝源这个选项写入配置文件。另外 npm 的配置文件 <code>.npmrc</code> 是可以更改默认源的，如果在 <code>.npmrc</code> 文件直接写入淘宝的镜像地址，那 npm 就会使用淘宝源下载依赖。但 npm 肯定不会去读取 <code>.vuerc</code> 的配置来决定是否使用淘宝源。</p><p>对于这一点我没搞明白，所以在用户选择了淘宝源之后，手动调用命令注册一遍。</p><h3 id="--14">将项目功能保存为默认配置</h3><p>如果用户创建项目时选择手动模式，在选择完一系列功能后，会弹出下面的提示语：</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2024/07/image-14.png" class="kg-image" alt="image-14" width="447" height="20" loading="lazy"></figure><p>询问用户是否将这次的项目选择保存为默认配置，如果用户选择是，则弹出下一个提示语：</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2024/07/image-15.png" class="kg-image" alt="image-15" width="197" height="32" loading="lazy"></figure><p>让用户输入保存配置的名称。</p><p>这两句提示语相关的代码为：</p><pre><code class="language-js">const otherPrompts = [
    {
        name: 'save',
        when: isManualMode,
        type: 'confirm',
        message: 'Save this as a preset for future projects?',
        default: false,
    },
    {
        name: 'saveName',
        when: answers =&gt; answers.save,
        type: 'input',
        message: 'Save preset as:',
    },
]</code></pre><p>保存配置的代码为：</p><pre><code class="language-js">exports.saveOptions = (toSave) =&gt; {
    const options = Object.assign(cloneDeep(exports.loadOptions()), toSave)
    for (const key in options) {
        if (!(key in exports.defaults)) {
            delete options[key]
        }
    }
    cachedOptions = options
    try {
        fs.writeFileSync(rcPath, JSON.stringify(options, null, 2))
        return true
    } catch (e) {
        error(
            `Error saving preferences: `
      + `make sure you have write access to ${rcPath}.\n`
      + `(${e.message})`,
        )
    }
}

exports.savePreset = (name, preset) =&gt; {
    const presets = cloneDeep(exports.loadOptions().presets || {})
    presets[name] = preset

    return exports.saveOptions({ presets })
}</code></pre><p>以上代码直接将用户的配置保存到 <code>.mvcrc</code> 文件中。下面是我电脑上的 <code>.mvcrc</code> 的内容：</p><pre><code class="language-json">{
  "packageManager": "npm",
  "presets": {
    "test": {
      "features": [
        "babel",
        "linter"
      ],
      "eslintConfig": "airbnb",
      "lintOn": [
        "save"
      ]
    },
    "demo": {
      "features": [
        "babel",
        "linter"
      ],
      "eslintConfig": "airbnb",
      "lintOn": [
        "save"
      ]
    }
  },
  "useTaobaoRegistry": true
}</code></pre><p>下次再创建项目时，脚手架就会先读取这个配置文件的内容，让用户决定是否使用已有的配置来创建项目。</p><p>至此，v2 版本的内容就介绍完了。</p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 探究 vue-devtools「在编辑器中打开组件」功能实现原理 ]]>
                </title>
                <description>
                    <![CDATA[ 前言 不知道你们有没有碰到这样的场景，打开你自己（或者你同事）开发的页面，却短时间难以找到对应的源文件。 这时你可能会想要是能有点击页面按钮自动用编辑器打开对应文件的功能，那该多好啊。 而vue-devtools提供了这样的功能，也许你不知道。我觉得很大一部分人都不知道，因为感觉很多人都不常用vue-devtools。 你也许会问，我不用vue，我用react有没有类似功能啊，有啊，请看 react-dev-inspector [https://github.com/zthxxx/react-dev-inspector]。 本文就是根据学习尤大写的 launch-editor [https://github.com/yyx990803/launch-editor] 源码，本着 知其然，知其所以然的宗旨，探究 vue-devtools「在编辑器中打开组件」功能实现原理。 一句话简述其原理 code path/to/file 一句话简述原理：利用nodejs中的child_process，执行了类似code path/to/file 命令，于是对应编辑器就打开了相应的文件，而 ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/vue-devtools/</link>
                <guid isPermaLink="false">60940ee30998fd05ae8c84ed</guid>
                
                    <category>
                        <![CDATA[ Vue ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ 若川 ]]>
                </dc:creator>
                <pubDate>Thu, 06 May 2021 08:00:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2021/05/maxwell-nelson-taiuG8CPKAQ-unsplash.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <h2 id="-">前言</h2><p>不知道你们有没有碰到这样的场景，打开你自己（或者你同事）开发的页面，却短时间难以找到对应的源文件。</p><p>这时你可能会想要是能有<strong>点击页面按钮自动用编辑器打开对应文件</strong>的功能，那该多好啊。</p><p>而<code>vue-devtools</code>提供了这样的功能，也许你不知道。我觉得很大一部分人都不知道，因为感觉很多人都不常用<code>vue-devtools</code>。</p><figure class="kg-card kg-image-card"><img src="https://lxchuan12.gitee.io/assets/img/open-src-app.vue.51fd0181.png" class="kg-image" alt="open-in-editor" width="600" height="400" loading="lazy"></figure><p>你也许会问，我不用<code>vue</code>，我用<code>react</code>有没有类似功能啊，有啊，请看 <a href="https://github.com/zthxxx/react-dev-inspector" rel="noopener noreferrer">react-dev-inspector</a>。</p><p>本文就是根据学习尤大写的 <a href="https://github.com/yyx990803/launch-editor" rel="noopener noreferrer">launch-editor</a> 源码，本着<strong>知其然，知其所以然</strong>的宗旨，探究 <code>vue-devtools</code>「在编辑器中打开组件」功能实现原理。</p><h3 id="--1">一句话简述其原理</h3><pre><code class="language-sh">code path/to/file
</code></pre><p>一句话简述原理：利用<code>nodejs</code>中的<code>child_process</code>，执行了类似<code>code path/to/file</code>命令，于是对应编辑器就打开了相应的文件，而对应的编辑器则是通过在进程中执行<code>ps x</code>（<code>Window</code>则用<code>Get-Process</code>）命令来查找的，当然也可以自己指定编辑器。</p><h3 id="--2">打开编辑器无法打开组件的报错解决方法</h3><p>而你真正用这个功能时，你可能碰到报错，说不能打开这个文件。</p><pre><code class="language-sh">Could not open App.vue in the editor.

To specify an editor, specify the EDITOR env variable or add "editor" field to your Vue project config.
</code></pre><figure class="kg-card kg-image-card"><img src="https://lxchuan12.gitee.io/assets/img/open-in-editor-error.4fae42eb.png" class="kg-image" alt="控制台不能打开编辑器的错误提示" width="600" height="400" loading="lazy"></figure><p>这里说明下写这篇文章时用的是 <code>Windows</code> 电脑，在<code>Ubuntu</code>子系统下使用的终端工具。同时推荐我的文章<a href="https://mp.weixin.qq.com/s/MHngeDABRV3z2HmN5DRrEw" rel="noopener noreferrer">使用 ohmyzsh 打造 windows、ubuntu、mac 系统高效终端命令行工具</a>，<strong>用过的都说好</strong>。</p><p><strong>解决办法也简单，就是这句英文的意思</strong>。具体说明编辑器，在环境变量中说明指定编辑器。在<code>vue</code>项目的根目录下，对应本文则是：<code>vue3-project</code>，添加<code>.env.delelopment</code>文件，其内容是<code>EDITOR=code</code>。</p><pre><code class="language-sh"># .env.development
# 当然，我的命令行终端已经有了code这个命令。
EDITOR=code
</code></pre><p>不用指定编辑器的对应路径（<code>c/Users/lxchu/AppData/Local/Programs/Microsoft VS Code/bin/code</code>），因为会报错。为什么会报错，因为我看了源码且试过。因为会被根据空格截断，变成<code>c/Users/lxchu/AppData/Local/Programs/Microsoft</code>，当然就报错了。</p><p>接下来我们从源码角度探究「在编辑器中打开组件」功能的实现原理。</p><h2 id="vue-devtools-open-component-in-editor-">vue-devtools Open component in editor 文档</h2><p>探究原理之前，先来看看<code>vue-devtools</code>官方文档。</p><p><a href="https://github.com/vuejs/vue-devtools#open-component-in-editor" rel="noopener noreferrer">vuejs/vue-devtools</a> 文档</p><blockquote><strong>Open component in editor</strong><br>To enable this feature, follow <a href="https://github.com/vuejs/vue-devtools/blob/dev/docs/open-in-editor.md" rel="noopener noreferrer">this guide</a>.</blockquote><p>这篇指南中写了在<code>Vue CLI 3</code>中是<strong>开箱即用</strong>。</p><pre><code class="language-sh">Vue CLI 3 supports this feature out-of-the-box when running vue-cli-service serve.
</code></pre><p>也详细写了如何在<code>Webpack</code>下使用。</p><pre><code class="language-sh"># 1. Import the package:
var openInEditor = require('launch-editor-middleware')
# 2. In the devServer option, register the /__open-in-editor HTTP route:
devServer: {
  before (app) {
    app.use('/__open-in-editor', openInEditor())
  }
}
# 3. The editor to launch is guessed. You can also specify the editor app with the editor option. See the supported editors list.
# 用哪个编辑器打开会自动猜测。你也可以具体指明编辑器。这里显示更多的支持编辑器列表
openInEditor('code')
# 4. You can now click on the name of the component in the Component inspector pane (if the devtools knows about its file source, a tooltip will appear).
# 如果`vue-devtools`开发者工具有提示点击的组件的显示具体路径，那么你可以在编辑器打开。
</code></pre><p>同时也写了如何在<code>Node.js</code>中使用等。</p><blockquote><strong>Node.js</strong><br>You can use the <a href="https://github.com/yyx990803/launch-editor#usage" rel="noopener noreferrer">launch-editor</a> package to setup an HTTP route with the <code>/__open-in-editor</code> path. It will receive file as an URL variable.</blockquote><p>查看更多可以看<a href="https://github.com/vuejs/vue-devtools/blob/dev/docs/open-in-editor.md" rel="noopener noreferrer">这篇指南</a>。</p><h2 id="--3">环境准备工作</h2><p>熟悉我的读者，都知道我都是<strong>推荐调试看源码</strong>的，正所谓：<strong>哪里不会点哪里</strong>。而且调试一般都写得很详细，是希望能帮助到一部分人知道如何看源码。于是我特意新建一个仓库 <a href="https://github.com/lxchuan12/open-in-editor" rel="noopener noreferrer">open-in-editor </a><code>git clone https://github.com/lxchuan12/open-in-editor.git</code>，便于大家克隆学习。</p><p>安装<code>vue-cli</code></p><pre><code class="language-sh">npm install -g @vue/cli
# OR
yarn global add @vue/cli
</code></pre><pre><code class="language-sh">node -V
# v14.16.0
vue -V 
# @vue/cli 4.5.12
vue create vue3-project
# 这里选择的是vue3、vue2也是一样的。
# Please pick a preset: Default (Vue 3 Preview) ([Vue 3] babel, eslint)
npm install
# OR
yarn install
</code></pre><p>这里同时说明下我的vscode版本。</p><pre><code class="language-sh">code -v
1.55.2
</code></pre><p>前文提到的<code>Vue CLI 3</code>中<strong>开箱即用</strong>和<code>Webpack</code>使用方法。</p><p><code>vue3-project/package.json</code>中有一个<code>debug</code>按钮。</p><figure class="kg-card kg-image-card"><img src="https://lxchuan12.gitee.io/assets/img/debug.7f563e4f.png" class="kg-image" alt="debug示意图" width="600" height="400" loading="lazy"></figure><p>选择第一项，serve <code>vue-cli-service serve</code>。</p><p>我们来搜索下<code>'launch-editor-middleware'</code>这个中间件，一般来说搜索不到<code>node_modules</code>下的文件，需要设置下。当然也有个简单做法。就是「排除的文件」右侧旁边有个设置图标「使用“排查设置”与“忽略文件”」，点击下。</p><p>其他的就不赘述了。可以看这篇知乎回答：<a href="https://www.zhihu.com/question/309220217/answer/586510407" rel="noopener noreferrer">vscode怎么设置可以搜索包含node_modules中的文件？</a></p><p>这时就搜到了<code>vue3-project/node_modules/@vue/cli-service/lib/commands/serve.js</code>中有使用这个中间件。</p><h2 id="vue-devtools-">vue-devtools 开箱即用具体源码实现</h2><p>接着我们来看<code>Vue CLI 3</code>中<strong>开箱即用</strong>具体源码实现。</p><pre><code class="language-js">// vue3-project/node_modules/@vue/cli-service/lib/commands/serve.js
// 46行
const launchEditorMiddleware = require('launch-editor-middleware')
// 192行
before (app, server) {
    // launch editor support.
    // this works with vue-devtools &amp; @vue/cli-overlay
    app.use('/__open-in-editor', launchEditorMiddleware(() =&gt; console.log(
        `To specify an editor, specify the EDITOR env variable or ` +
        `add "editor" field to your Vue project config.\n`
    )))
    // 省略若干代码...
}
</code></pre><p>点击<code>vue-devtools</code>中的时，会有一个请求，<code>http://localhost:8080/__open-in-editor?file=src/App.vue</code>，不出意外就会打开该组件啦。</p><figure class="kg-card kg-image-card"><img src="https://lxchuan12.gitee.io/assets/img/open-src-app.vue.51fd0181.png" class="kg-image" alt="open src/App.vue in editor" width="600" height="400" loading="lazy"></figure><p>接着我们在<code>launchEditorMiddleware</code>的具体实现。</p><h2 id="launch-editor-middleware">launch-editor-middleware</h2><p>看源码时，先看调试截图。</p><figure class="kg-card kg-image-card"><img src="https://lxchuan12.gitee.io/assets/img/debug-launch.d30ca44e.png" class="kg-image" alt="debug-launch" width="600" height="400" loading="lazy"></figure><p>在<code>launch-editor-middleware</code>中间件中作用在于最终是调用 <code>launch-editor</code> 打开文件。</p><pre><code class="language-js">// vue3-project/node_modules/launch-editor-middleware/index.js
const url = require('url')
const path = require('path')
const launch = require('launch-editor')

module.exports = (specifiedEditor, srcRoot, onErrorCallback) =&gt; {
  // specifiedEditor =&gt; 这里传递过来的则是 () =&gt; console.log() 函数
  // 所以和 onErrorCallback 切换下，把它赋值给错误回调函数
  if (typeof specifiedEditor === 'function') {
    onErrorCallback = specifiedEditor
    specifiedEditor = undefined
  }

  // 如果第二个参数是函数，同样把它赋值给错误回调函数
  // 这里传递过来的是undefined
  if (typeof srcRoot === 'function') {
    onErrorCallback = srcRoot
    srcRoot = undefined
  }

  // srcRoot 是传递过来的参数，或者当前node进程的目录
  srcRoot = srcRoot || process.cwd()

  // 最后返回一个函数， express 中间件
  return function launchEditorMiddleware (req, res, next) {
    // 省略 ...
  }
}
</code></pre><p><strong>上一段中，这种切换参数的写法，在很多源码中都很常见。为的是方便用户调用时传参。虽然是多个参数，但可以传一个或者两个</strong>。</p><p>可以根据情况打上断点。比如这里我会在<code>launch(path.resolve(srcRoot, file), specifiedEditor, onErrorCallback)</code>打断点。</p><pre><code class="language-js">// vue3-project/node_modules/launch-editor-middleware/index.js
module.exports = (specifiedEditor, srcRoot, onErrorCallback) =&gt; {
  // 省略上半部分
  return function launchEditorMiddleware (req, res, next) {
    // 根据请求解析出file路径
    const { file } = url.parse(req.url, true).query || {}
    // 如果没有文件路径，则报错
    if (!file) {
      res.statusCode = 500
      res.end(`launch-editor-middleware: required query param "file" is missing.`)
    } else {
      // 否则拼接路径，用launch打开。
      launch(path.resolve(srcRoot, file), specifiedEditor, onErrorCallback)
      res.end()
    }
  }
}
</code></pre><h2 id="launch-editor">launch-editor</h2><p>跟着断点来看，走到了<code>launchEditor</code>函数。</p><pre><code class="language-js">// vue3-project/node_modules/launch-editor/index.js
function launchEditor (file, specifiedEditor, onErrorCallback) {
  // 解析出文件路径和行号列号等信息
  const parsed = parseFile(file)
  let { fileName } = parsed
  const { lineNumber, columnNumber } = parsed

  // 判断文件是否存在，不存在，直接返回。
  if (!fs.existsSync(fileName)) {
    return
  }
  // 所以和 onErrorCallback 切换下，把它赋值给错误回调函数
  if (typeof specifiedEditor === 'function') {
    onErrorCallback = specifiedEditor
    specifiedEditor = undefined
  }
  // 包裹一层函数
  onErrorCallback = wrapErrorCallback(onErrorCallback)

  // 猜测当前进程运行的是哪个编辑器
  const [editor, ...args] = guessEditor(specifiedEditor)
  if (!editor) {
    onErrorCallback(fileName, null)
    return
  }
  // 省略剩余部分，后文再讲述...
}
</code></pre><h3 id="wraperrorcallback-">wrapErrorCallback 包裹错误函数回调</h3><pre><code class="language-js">onErrorCallback = wrapErrorCallback(onErrorCallback)
</code></pre><p>这段的代码，我相信读者朋友能看懂，我单独拿出来讲述，主要是因为<strong>这种包裹函数的形式在很多源码里都很常见</strong>。 这里也就是文章开头终端错误图<code>Could not open App.vue in the editor.</code>输出的代码位置。</p><pre><code class="language-js">// vue3-project/node_modules/launch-editor/index.js
function wrapErrorCallback (cb) {
  return (fileName, errorMessage) =&gt; {
    console.log()
    console.log(
      chalk.red('Could not open ' + path.basename(fileName) + ' in the editor.')
    )
    if (errorMessage) {
      if (errorMessage[errorMessage.length - 1] !== '.') {
        errorMessage += '.'
      }
      console.log(
        chalk.red('The editor process exited with an error: ' + errorMessage)
      )
    }
    console.log()
    if (cb) cb(fileName, errorMessage)
  }
}
</code></pre><h3 id="guesseditor-">guessEditor 猜测当前正在使用的编辑器</h3><p>这个函数主要做了如下四件事情：</p><ol><li>如果具体指明了编辑器，则解析下返回。<br></li><li>找出当前进程中哪一个编辑器正在运行。<code>macOS</code> 和 <code>Linux</code> 用 <code>ps x</code> 命令<br><code>windows</code> 则用 <code>Get-Process</code> 命令<br></li><li>如果都没找到就用 <code>process.env.VISUAL</code>或者<code>process.env.EDITOR</code>。这就是为啥开头错误提示可以使用环境变量指定编辑器的原因。<br></li><li>最后还是没有找到就返回<code>[null]</code>，则会报错。</li></ol><pre><code class="language-js">const [editor, ...args] = guessEditor(specifiedEditor)
if (!editor) {
    onErrorCallback(fileName, null)
    return
}
</code></pre><pre><code class="language-js">// vue3-project/node_modules/launch-editor/guess.js
const shellQuote = require('shell-quote')

module.exports = function guessEditor (specifiedEditor) {
  // 如果指定了编辑器，则解析一下，这里没有传入。如果自己指定了路径。
  // 比如 c/Users/lxchu/AppData/Local/Programs/Microsoft VS Code/bin/code 
  //   会根据空格切割成 c/Users/lxchu/AppData/Local/Programs/Microsoft
  if (specifiedEditor) {
    return shellQuote.parse(specifiedEditor)
  }
  // We can find out which editor is currently running by:
  // `ps x` on macOS and Linux
  // `Get-Process` on Windows
  try {
    //  省略...
  } catch (error) {
    // Ignore...
  }

  // Last resort, use old skool env vars
  if (process.env.VISUAL) {
    return [process.env.VISUAL]
  } else if (process.env.EDITOR) {
    return [process.env.EDITOR]
  }

  return [null]
}
</code></pre><p>看完了 guessEditor 函数，我们接着来看 <code>launch-editor</code> 剩余部分。</p><h3 id="launch-editor-">launch-editor 剩余部分</h3><p>以下这段代码不用细看，调试的时候细看就行。</p><pre><code class="language-js">// vue3-project/node_modules/launch-editor/index.js
function launchEditor(){
  //  省略上部分...
  if (
    process.platform === 'linux' &amp;&amp;
    fileName.startsWith('/mnt/') &amp;&amp;
    /Microsoft/i.test(os.release())
  ) {
    // Assume WSL / "Bash on Ubuntu on Windows" is being used, and
    // that the file exists on the Windows file system.
    // `os.release()` is "4.4.0-43-Microsoft" in the current release
    // build of WSL, see: https://github.com/Microsoft/BashOnWindows/issues/423#issuecomment-221627364
    // When a Windows editor is specified, interop functionality can
    // handle the path translation, but only if a relative path is used.
    fileName = path.relative('', fileName)
  }

  if (lineNumber) {
    const extraArgs = getArgumentsForPosition(editor, fileName, lineNumber, columnNumber)
    args.push.apply(args, extraArgs)
  } else {
    args.push(fileName)
  }

  if (_childProcess &amp;&amp; isTerminalEditor(editor)) {
    // There's an existing editor process already and it's attached
    // to the terminal, so go kill it. Otherwise two separate editor
    // instances attach to the stdin/stdout which gets confusing.
    _childProcess.kill('SIGKILL')
  }

  if (process.platform === 'win32') {
    // On Windows, launch the editor in a shell because spawn can only
    // launch .exe files.
    _childProcess = childProcess.spawn(
      'cmd.exe',
      ['/C', editor].concat(args),
      { stdio: 'inherit' }
    )
  } else {
    _childProcess = childProcess.spawn(editor, args, { stdio: 'inherit' })
  }
  _childProcess.on('exit', function (errorCode) {
    _childProcess = null

    if (errorCode) {
      onErrorCallback(fileName, '(code ' + errorCode + ')')
    }
  })

  _childProcess.on('error', function (error) {
    onErrorCallback(fileName, error.message)
  })
}
</code></pre><p>这一大段中，主要的就是<strong>以下代码</strong>，用子进程模块。简单来说子进程模块有着执行命令的能力。</p><pre><code class="language-js">const childProcess = require('child_process')

if (process.platform === 'win32') {
    // On Windows, launch the editor in a shell because spawn can only
    // launch .exe files.
    _childProcess = childProcess.spawn(
        'cmd.exe',
        ['/C', editor].concat(args),
        { stdio: 'inherit' }
    )
    } else {
    _childProcess = childProcess.spawn(editor, args, { stdio: 'inherit' })
}
</code></pre><p>行文至此，就基本接近尾声了。</p><h2 id="--4">总结</h2><p>这里总结一下：首先文章开头通过提出「短时间找不到页面对应源文件的场景」，并针对容易碰到的报错情况给出了解决方案。 其次，配置了环境跟着调试学习了<code>vue-devtools</code>中使用的尤大写的 <a href="https://github.com/yyx990803/launch-editor" rel="noopener noreferrer">yyx990803/launch-editor</a>。</p><h3 id="--5">一句话简述其原理</h3><p>我们回顾下开头的原理内容。</p><pre><code class="language-sh">code path/to/file
</code></pre><p>一句话简述原理：利用<code>nodejs</code>中的<code>child_process</code>，执行了类似<code>code path/to/file</code>命令，于是对应编辑器就打开了相应的文件，而对应的编辑器则是通过在进程中执行<code>ps x</code>（<code>Window</code>则用<code>Get-Process</code>）命令来查找的，当然也可以自己指定编辑器。</p><p>最后还能做什么呢？</p><p>可以再看看 <a href="https://github.com/umijs/launch-editor" rel="noopener noreferrer">umijs/launch-editor</a> 和 <a href="https://github.com/facebook/create-react-app/blob/master/packages/react-dev-utils/launchEditor.js" rel="noopener noreferrer">react-dev-utils/launchEditor.js</a>，它们的代码几乎类似。</p><p>也可以利用<code>Node.js</code>做一些提高开发效率等工作，同时可以学习<code>child_process</code>等模块。</p><p>欢迎阅读我的<a href="https://www.lxchuan12.cn/">更多文章</a>。</p> ]]>
                </content:encoded>
            </item>
        
            <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[ 可视化拖拽组件库一些技术要点原理分析（二） ]]>
                </title>
                <description>
                    <![CDATA[ 本文是对《可视化拖拽组件库一些技术要点原理分析》 [https://chinese.freecodecamp.org/news/visual-drag-and-drop-component-library/] 的补充。上一篇文章主要讲解了以下几个功能点：  1.  编辑器  2.  自定义组件  3.  拖拽  4.  删除组件、调整图层层级  5.  放大缩小  6.  撤消、重做  7.  组件属性设置  8.  吸附  9.  预览、保存代码  10. 绑定事件  11. 绑定动画  12. 导入 PSD  ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/visual-drag-and-drop-component-library-part-2/</link>
                <guid isPermaLink="false">6004ff425f61e30501b5bdda</guid>
                
                    <category>
                        <![CDATA[ 可视化 ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Vue ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Web开发 ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ woai3c ]]>
                </dc:creator>
                <pubDate>Mon, 18 Jan 2021 07:50:07 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2021/01/michael-dziedzic-gEN5Btvf2Eg-unsplash.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>本文是对<a href="https://chinese.freecodecamp.org/news/visual-drag-and-drop-component-library/">《可视化拖拽组件库一些技术要点原理分析》</a>的补充。上一篇文章主要讲解了以下几个功能点：</p><ol><li>编辑器</li><li>自定义组件</li><li>拖拽</li><li>删除组件、调整图层层级</li><li>放大缩小</li><li>撤消、重做</li><li>组件属性设置</li><li>吸附</li><li>预览、保存代码</li><li>绑定事件</li><li>绑定动画</li><li>导入 PSD</li><li>手机模式</li></ol><p>现在这篇文章会在此基础上再补充 4 个功能点，分别是：</p><ul><li>拖拽旋转</li><li>复制粘贴剪切</li><li>数据交互</li><li>发布</li></ul><p>和上篇文章一样，我已经将新功能的代码更新到了 github：</p><ul><li><a href="https://github.com/woai3c/visual-drag-demo">github 项目地址</a></li><li><a href="https://woai3c.github.io/visual-drag-demo">在线预览</a></li></ul><p><strong><strong>友善提醒</strong></strong>：建议结合源码一起阅读，效果更好（这个 DEMO 使用的是 Vue 技术栈）。</p><h2 id="14-"><strong>14. 拖拽旋转</strong></h2><p>在写上一篇文章时，原来的 DEMO 已经可以支持旋转功能了。但是这个旋转功能还有很多不完善的地方：</p><ol><li>不支持拖拽旋转。</li><li>旋转后的放大缩小不正确。</li><li>旋转后的自动吸附不正确。</li><li>旋转后八个可伸缩点的光标不正确。</li></ol><p>这一小节，我们将逐一解决这四个问题。</p><h3 id="-"><strong>拖拽旋转</strong></h3><p>拖拽旋转需要使用 <a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Math/atan2">Math.atan2()</a> 函数。</p><p>Math.atan2() 返回从原点(0,0)到(x,y)点的线段与x轴正方向之间的平面角度(弧度值)，也就是Math.atan2(y,x)。Math.atan2(y,x)中的y和x都是相对于圆点(0,0)的距离。</p><p>简单的说就是以组件中心点为原点 <code>(centerX,centerY)</code>，用户按下鼠标时的坐标设为 <code>(startX,startY)</code>，鼠标移动时的坐标设为 <code>(curX,curY)</code>。旋转角度可以通过 <code>(startX,startY)</code> 和 <code>(curX,curY)</code> 计算得出。</p><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/c2de0e4cd42f3c2a2a74b2fa5d38f8e6.png" class="kg-image" alt="c2de0e4cd42f3c2a2a74b2fa5d38f8e6" width="339" height="282" loading="lazy"></figure><p>那我们如何得到从点 <code>(startX,startY)</code> 到点 <code>(curX,curY)</code> 之间的旋转角度呢？</p><p><strong><strong>第一步</strong></strong>，鼠标点击时的坐标设为 <code>(startX,startY)</code>：</p><pre><code class="language-js">const startY = e.clientY
const startX = e.clientX
</code></pre><p><strong><strong>第二步</strong></strong>，算出组件中心点：</p><pre><code class="language-js">// 获取组件中心点位置
const rect = this.$el.getBoundingClientRect()
const centerX = rect.left + rect.width / 2
const centerY = rect.top + rect.height / 2
</code></pre><p><strong><strong>第三步</strong></strong>，按住鼠标移动时的坐标设为 <code>(curX,curY)</code>：</p><pre><code class="language-js">const curX = moveEvent.clientX
const curY = moveEvent.clientY
</code></pre><p><strong><strong>第四步</strong></strong>，分别算出 <code>(startX,startY)</code> 和 <code>(curX,curY)</code> 对应的角度，再将它们相减得出旋转的角度。另外，还需要注意的就是 <code>Math.atan2()</code> 方法的返回值是一个弧度，因此还需要将弧度转化为角度。所以完整的代码为：</p><pre><code class="language-js">// 旋转前的角度
const rotateDegreeBefore = Math.atan2(startY - centerY, startX - centerX) / (Math.PI / 180)
// 旋转后的角度
const rotateDegreeAfter = Math.atan2(curY - centerY, curX - centerX) / (Math.PI / 180)
// 获取旋转的角度值， startRotate 为初始角度值
pos.rotate = startRotate + rotateDegreeAfter - rotateDegreeBefore
</code></pre><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/f72edf99eeacbce08c5a954ec908350c.gif" class="kg-image" alt="f72edf99eeacbce08c5a954ec908350c" width="228" height="197" loading="lazy"></figure><h3 id="--1"><strong>放大缩小</strong></h3><p>组件旋转后的放大缩小会有 BUG。</p><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/d7788970d2e1567c15ad0fc7985445cc.gif" class="kg-image" alt="d7788970d2e1567c15ad0fc7985445cc" width="1007" height="597" loading="lazy"></figure><p>从上图可以看到，放大缩小时会发生移位。另外伸缩的方向和我们拖动的方向也不对。造成这一 BUG 的原因是：当初设计放大缩小功能没有考虑到旋转的场景。所以无论旋转多少角度，放大缩小仍然是按没旋转时计算的。</p><p>下面再看一个具体的示例：</p><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/3e815a59f74b2a51bb1141f716c17e12.png" class="kg-image" alt="3e815a59f74b2a51bb1141f716c17e12" width="285" height="177" loading="lazy"></figure><p>从上图可以看出，在没有旋转时，按住顶点往上拖动，只需用 <code>y2 - y1</code> 就可以得出拖动距离 <code>s</code>。这时将组件原来的高度加上 <code>s</code> 就能得出新的高度，同时将组件的 <code>top</code>、<code>left</code> 属性更新。</p><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/adeedf9ff0fbc9da68259b8021a3ea69.png" class="kg-image" alt="adeedf9ff0fbc9da68259b8021a3ea69" width="172" height="186" loading="lazy"></figure><p>现在旋转 180 度，如果这时拖住顶点往下拖动，我们期待的结果是组件高度增加。但这时计算的方式和原来没旋转时是一样的，所以结果和我们期待的相反，组件的高度将会变小（如果不理解这个现象，可以想像一下没有旋转的那张图，按住顶点往下拖动）。</p><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/f013c01874f77de619a910ed614d864f.gif" class="kg-image" alt="f013c01874f77de619a910ed614d864f" width="600" height="401" loading="lazy"></figure><p>如何解决这个问题呢？我从 github 上的一个项目 <a href="https://github.com/shenhudong/snapping-demo/wiki/corner-handle">snapping-demo</a> 找到了解决方案：将放大缩小和旋转角度关联起来。</p><h4 id="--2"><strong>解决方案</strong></h4><p>下面是一个已旋转一定角度的矩形，假设现在拖动它左上方的点进行拉伸。</p><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/8999e5e5e143fe902a59d1969151bb54.png" class="kg-image" alt="8999e5e5e143fe902a59d1969151bb54" width="1043" height="1142" loading="lazy"></figure><p>现在我们将一步步分析如何得出拉伸后的组件的正确大小和位移。</p><p><strong><strong>第一步</strong></strong>，按下鼠标时通过组件的坐标（无论旋转多少度，组件的 <code>top</code> <code>left</code> 属性不变）和大小算出组件中心点：</p><pre><code class="language-js">const center = {
    x: style.left + style.width / 2,
    y: style.top + style.height / 2,
}
</code></pre><p><strong><strong>第二步</strong></strong>，用<strong><strong>当前点击坐标</strong></strong>和组件中心点算出<strong><strong>当前点击坐标</strong></strong>的对称点坐标：</p><pre><code class="language-js">// 获取画布位移信息
const editorRectInfo = document.querySelector('#editor').getBoundingClientRect()

// 当前点击坐标
const curPoint = {
    x: e.clientX - editorRectInfo.left,
    y: e.clientY - editorRectInfo.top,
}

// 获取对称点的坐标
const symmetricPoint = {
    x: center.x - (curPoint.x - center.x),
    y: center.y - (curPoint.y - center.y),
}
</code></pre><p><strong><strong>第三步</strong></strong>，摁住组件左上角进行拉伸时，通过当前鼠标实时坐标和对称点计算出新的组件中心点：</p><pre><code class="language-js">const curPositon = {
    x: moveEvent.clientX - editorRectInfo.left,
    y: moveEvent.clientY - editorRectInfo.top,
}

const newCenterPoint = getCenterPoint(curPositon, symmetricPoint)

// 求两点之间的中点坐标
function getCenterPoint(p1, p2) {
    return {
        x: p1.x + ((p2.x - p1.x) / 2),
        y: p1.y + ((p2.y - p1.y) / 2),
    }
}
</code></pre><p>由于组件处于旋转状态，即使你知道了拉伸时移动的 <code>xy</code> 距离，也不能直接对组件进行计算。否则就会出现 BUG，移位或者放大缩小方向不正确。因此，我们需要在组件未旋转的情况下对其进行计算。</p><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/da9d36c423db82bd7130c296d66a27d5.png" class="kg-image" alt="da9d36c423db82bd7130c296d66a27d5" width="1042" height="1142" loading="lazy"></figure><p><strong><strong>第四步</strong></strong>，根据已知的旋转角度、新的组件中心点、当前鼠标实时坐标可以算出<strong><strong>当前鼠标实时坐标</strong></strong> <code>currentPosition</code> 在未旋转时的坐标 <code>newTopLeftPoint</code>。同时也能根据已知的旋转角度、新的组件中心点、对称点算出<strong><strong>组件对称点</strong></strong> <code>sPoint</code> 在未旋转时的坐标 <code>newBottomRightPoint</code>。</p><p>对应的计算公式如下：</p><pre><code class="language-js">/**
 * 计算根据圆心旋转后的点的坐标
 * @param   {Object}  point  旋转前的点坐标
 * @param   {Object}  center 旋转中心
 * @param   {Number}  rotate 旋转的角度
 * @return  {Object}         旋转后的坐标
 * https://www.zhihu.com/question/67425734/answer/252724399 旋转矩阵公式
 */
export function calculateRotatedPointCoordinate(point, center, rotate) {
    /**
     * 旋转公式：
     *  点a(x, y)
     *  旋转中心c(x, y)
     *  旋转后点n(x, y)
     *  旋转角度θ                tan ??
     * nx = cosθ * (ax - cx) - sinθ * (ay - cy) + cx
     * ny = sinθ * (ax - cx) + cosθ * (ay - cy) + cy
     */

    return {
        x: (point.x - center.x) * Math.cos(angleToRadian(rotate)) - (point.y - center.y) * Math.sin(angleToRadian(rotate)) + center.x,
        y: (point.x - center.x) * Math.sin(angleToRadian(rotate)) + (point.y - center.y) * Math.cos(angleToRadian(rotate)) + center.y,
    }
}
</code></pre><p>上面的公式涉及到线性代数中旋转矩阵的知识，对于一个没上过大学的人来说，实在太难了。还好我从知乎上的一个<a href="https://www.zhihu.com/question/67425734/answer/252724399">回答</a>中找到了这一公式的推理过程，下面是回答的原文：</p><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/8d6388e18f987779b5e1cc3c2e0f3684.png" class="kg-image" alt="8d6388e18f987779b5e1cc3c2e0f3684" width="658" height="442" loading="lazy"></figure><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/be69feb4415274e8dc9ddb75f402d82a.png" class="kg-image" alt="be69feb4415274e8dc9ddb75f402d82a" width="578" height="315" loading="lazy"></figure><p>通过以上几个计算值，就可以得到组件新的位移值 <code>top</code> <code>left</code> 以及新的组件大小。对应的完整代码如下：</p><pre><code class="language-js">function calculateLeftTop(style, curPositon, pointInfo) {
    const { symmetricPoint } = pointInfo
    const newCenterPoint = getCenterPoint(curPositon, symmetricPoint)
    const newTopLeftPoint = calculateRotatedPointCoordinate(curPositon, newCenterPoint, -style.rotate)
    const newBottomRightPoint = calculateRotatedPointCoordinate(symmetricPoint, newCenterPoint, -style.rotate)
  
    const newWidth = newBottomRightPoint.x - newTopLeftPoint.x
    const newHeight = newBottomRightPoint.y - newTopLeftPoint.y
    if (newWidth &gt; 0 &amp;&amp; newHeight &gt; 0) {
        style.width = Math.round(newWidth)
        style.height = Math.round(newHeight)
        style.left = Math.round(newTopLeftPoint.x)
        style.top = Math.round(newTopLeftPoint.y)
    }
}
</code></pre><p>现在再来看一下旋转后的放大缩小：</p><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/179256b334e7bf8851cdddbbc001f8a7.gif" class="kg-image" alt="179256b334e7bf8851cdddbbc001f8a7" width="307" height="190" loading="lazy"></figure><h3 id="--3"><strong>自动吸附</strong></h3><p>自动吸附是根据组件的四个属性 <code>top</code> <code>left</code> <code>width</code> <code>height</code> 计算的，在将组件进行旋转后，这些属性的值是不会变的。所以无论组件旋转多少度，吸附时仍然按未旋转时计算。这样就会有一个问题，虽然实际上组件的 <code>top</code> <code>left</code> <code>width</code> <code>height</code> 属性没有变化。但在外观上却发生了变化。下面是两个同样的组件：一个没旋转，一个旋转了 45 度。</p><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/691534c36a12f885a492800be361ba78.png" class="kg-image" alt="691534c36a12f885a492800be361ba78" width="279" height="106" loading="lazy"></figure><p>可以看出来旋转后按钮的 <code>height</code> 属性和我们从外观上看到的高度是不一样的，所以在这种情况下就出现了吸附不正确的 BUG。</p><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/f31472ccab5dad26a873d29520360335.gif" class="kg-image" alt="f31472ccab5dad26a873d29520360335" width="1007" height="597" loading="lazy"></figure><h4 id="--4"><strong>解决方案</strong></h4><p>如何解决这个问题？我们需要拿组件旋转后的大小及位移来做吸附对比。也就是说不要拿组件实际的属性来对比，而是拿我们看到的大小和位移做对比。</p><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/c998d115f6b3ba179d996ec1785feebb.png" class="kg-image" alt="c998d115f6b3ba179d996ec1785feebb" width="280" height="269" loading="lazy"></figure><p>从上图可以看出，旋转后的组件在 x 轴上的投射长度为两条红线长度之和。这两条红线的长度可以通过正弦和余弦算出，左边的红线用正弦计算，右边的红线用余弦计算：</p><pre><code class="language-js">const newWidth = style.width * cos(style.rotate) + style.height * sin(style.rotate)
</code></pre><p>同理，高度也是一样：</p><pre><code class="language-js">const newHeight = style.height * cos(style.rotate) + style.width * sin(style.rotate)
</code></pre><p>新的宽度和高度有了，再根据组件原有的 <code>top</code> <code>left</code> 属性，可以得出组件旋转后新的 <code>top</code> <code>left</code> 属性。下面附上完整代码：</p><pre><code class="language-js">translateComponentStyle(style) {
    style = { ...style }
    if (style.rotate != 0) {
        const newWidth = style.width * cos(style.rotate) + style.height * sin(style.rotate)
        const diffX = (style.width - newWidth) / 2
        style.left += diffX
        style.right = style.left + newWidth

        const newHeight = style.height * cos(style.rotate) + style.width * sin(style.rotate)
        const diffY = (newHeight - style.height) / 2
        style.top -= diffY
        style.bottom = style.top + newHeight

        style.width = newWidth
        style.height = newHeight
    } else {
        style.bottom = style.top + style.height
        style.right = style.left + style.width
    }

    return style
}
</code></pre><p>经过修复后，吸附也可以正常显示了。</p><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/bb59ba649d6f71c49618177fccff5129.gif" class="kg-image" alt="bb59ba649d6f71c49618177fccff5129" width="368" height="270" loading="lazy"></figure><h3 id="--5"><strong>光标</strong></h3><p>光标和可拖动的方向不对，是因为八个点的光标是固定设置的，没有随着角度变化而变化。</p><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/c66d1cdbc8b3dae2697c1f3c21321df7.gif" class="kg-image" alt="c66d1cdbc8b3dae2697c1f3c21321df7" width="691" height="480" loading="lazy"></figure><h4 id="--6"><strong>解决方案</strong></h4><p>由于 <code>360 / 8 = 45</code>，所以可以为每一个方向分配 45 度的范围，每个范围对应一个光标。同时为每个方向设置一个初始角度，也就是未旋转时组件每个方向对应的角度。</p><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/bdd8c41ba9c9411a65c539ff555d163b.png" class="kg-image" alt="bdd8c41ba9c9411a65c539ff555d163b" width="409" height="316" loading="lazy"></figure><pre><code class="language-js">pointList: ['lt', 't', 'rt', 'r', 'rb', 'b', 'lb', 'l'], // 八个方向
initialAngle: { // 每个点对应的初始角度
    lt: 0,
    t: 45,
    rt: 90,
    r: 135,
    rb: 180,
    b: 225,
    lb: 270,
    l: 315,
},
angleToCursor: [ // 每个范围的角度对应的光标
    { start: 338, end: 23, cursor: 'nw' },
    { start: 23, end: 68, cursor: 'n' },
    { start: 68, end: 113, cursor: 'ne' },
    { start: 113, end: 158, cursor: 'e' },
    { start: 158, end: 203, cursor: 'se' },
    { start: 203, end: 248, cursor: 's' },
    { start: 248, end: 293, cursor: 'sw' },
    { start: 293, end: 338, cursor: 'w' },
],
cursors: {},
</code></pre><p>计算方式也很简单：</p><ol><li>假设现在组件已旋转了一定的角度 a。</li><li>遍历八个方向，用每个方向的初始角度 + a 得出现在的角度 b。</li><li>遍历 <code>angleToCursor</code> 数组，看看 b 在哪一个范围中，然后将对应的光标返回。</li></ol><p>经常上面三个步骤就可以计算出组件旋转后正确的光标方向。具体的代码如下：</p><pre><code class="language-js">getCursor() {
    const { angleToCursor, initialAngle, pointList, curComponent } = this
    const rotate = (curComponent.style.rotate + 360) % 360 // 防止角度有负数，所以 + 360
    const result = {}
    let lastMatchIndex = -1 // 从上一个命中的角度的索引开始匹配下一个，降低时间复杂度
    pointList.forEach(point =&gt; {
        const angle = (initialAngle[point] + rotate) % 360
        const len = angleToCursor.length
        while (true) {
            lastMatchIndex = (lastMatchIndex + 1) % len
            const angleLimit = angleToCursor[lastMatchIndex]
            if (angle &lt; 23 || angle &gt;= 338) {
                result[point] = 'nw-resize'
                return
            }

            if (angleLimit.start &lt;= angle &amp;&amp; angle &lt; angleLimit.end) {
                result[point] = angleLimit.cursor + '-resize'
                return
            }
        }
    })

    return result
},
</code></pre><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/6e5bb1c62178c24bbbae485831ab7304.gif" class="kg-image" alt="6e5bb1c62178c24bbbae485831ab7304" width="368" height="270" loading="lazy"></figure><p>从上面的动图可以看出来，现在八个方向上的光标是可以正确显示的。</p><h2 id="15-"><strong>15. 复制粘贴剪切</strong></h2><p>相对于拖拽旋转功能，复制粘贴就比较简单了。</p><pre><code class="language-js">const ctrlKey = 17, vKey = 86, cKey = 67, xKey = 88
let isCtrlDown = false

window.onkeydown = (e) =&gt; {
    if (e.keyCode == ctrlKey) {
        isCtrlDown = true
    } else if (isCtrlDown &amp;&amp; e.keyCode == cKey) {
        this.$store.commit('copy')
    } else if (isCtrlDown &amp;&amp; e.keyCode == vKey) {
        this.$store.commit('paste')
    } else if (isCtrlDown &amp;&amp; e.keyCode == xKey) {
        this.$store.commit('cut')
    }
}

window.onkeyup = (e) =&gt; {
    if (e.keyCode == ctrlKey) {
        isCtrlDown = false
    }
}
</code></pre><p>监听用户的按键操作，在按下特定按键时触发对应的操作。</p><h3 id="--7"><strong>复制操作</strong></h3><p>在 vuex 中使用 <code>copyData</code> 来表示复制的数据。当用户按下 <code>ctrl + c</code> 时，将当前组件数据深拷贝到 <code>copyData</code>。</p><pre><code class="language-js">copy(state) {
    state.copyData = {
        data: deepCopy(state.curComponent),
        index: state.curComponentIndex,
    }
},
</code></pre><p>同时需要将当前组件在组件数据中的索引记录起来，在剪切中要用到。</p><h3 id="--8"><strong>粘贴操作</strong></h3><pre><code class="language-js">paste(state, isMouse) {
    if (!state.copyData) {
        toast('请选择组件')
        return
    }

    const data = state.copyData.data

    if (isMouse) {
        data.style.top = state.menuTop
        data.style.left = state.menuLeft
    } else {
        data.style.top += 10
        data.style.left += 10
    }

    data.id = generateID()
    store.commit('addComponent', { component: data })
    store.commit('recordSnapshot')
    state.copyData = null
},
</code></pre><p>粘贴时，如果是按键操作 <code>ctrl+v</code>。则将组件的 <code>top</code> <code>left</code> 属性加 10，以免和原来的组件重叠在一起。如果是使用鼠标右键执行粘贴操作，则将复制的组件放到鼠标点击处。</p><h3 id="--9"><strong>剪切操作</strong></h3><pre><code class="language-js">cut({ copyData }) {
    if (copyData) {
        store.commit('addComponent', { component: copyData.data, index: copyData.index })
    }

    store.commit('copy')
    store.commit('deleteComponent')
},
</code></pre><p>剪切操作本质上还是复制，只不过在执行复制后，需要将当前组件删除。为了避免用户执行剪切操作后，不执行粘贴操作，而是继续执行剪切。这时就需要将原先剪切的数据进行恢复。所以复制数据中记录的索引就起作用了，可以通过索引将原来的数据恢复到原来的位置中。</p><h3 id="--10"><strong>右键操作</strong></h3><p>右键操作和按键操作是一样的，一个功能两种触发途径。</p><pre><code class="language-html">&lt;li @click="copy" v-show="curComponent"&gt;复制&lt;/li&gt;
&lt;li @click="paste"&gt;粘贴&lt;/li&gt;
&lt;li @click="cut" v-show="curComponent"&gt;剪切&lt;/li&gt;

cut() {
    this.$store.commit('cut')
},

copy() {
    this.$store.commit('copy')
},

paste() {
    this.$store.commit('paste', true)
},
</code></pre><h2 id="16-"><strong>16. 数据交互</strong></h2><h3 id="--11"><strong>方式一</strong></h3><p>提前写好一系列 ajax 请求API，点击组件时按需选择 API，选好 API 再填参数。例如下面这个组件，就展示了如何使用 ajax 请求向后台交互：</p><pre><code class="language-html">&lt;template&gt;
    &lt;div&gt;{{ propValue.data }}&lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
export default {
    // propValue: {
    //     api: {
    //             request: a,
    //             params,
    //      },
    //     data: null
    // }
    props: {
        propValue: {
            type: Object,
            default: () =&gt; {},
        },
    },
    created() {
        this.propValue.api.request(this.propValue.api.params).then(res =&gt; {
            this.propValue.data = res.data
        })
    },
}
&lt;/script&gt;
</code></pre><h3 id="--12"><strong>方式二</strong></h3><p>方式二适合纯展示的组件，例如有一个报警组件，可以根据后台传来的数据显示对应的颜色。在编辑页面的时候，可以通过 ajax 向后台请求页面能够使用的 websocket 数据：</p><pre><code class="language-js">const data = ['status', 'text'...]
</code></pre><p>然后再为不同的组件添加上不同的属性。例如有 a 组件，它绑定的属性为 <code>status</code>。</p><pre><code>// 组件能接收的数据
props: {
    propValue: {
        type: String,
    },
    element: {
        type: Object,
    },
    wsKey: {
        type: String,
        default: '',
    },
},
</code></pre><p>在组件中通过 <code>wsKey</code> 获取这个绑定的属性。等页面发布后或者预览时，通过 weboscket 向后台请求全局数据放在 vuex 上。组件就可以通过 <code>wsKey</code> 访问数据了。</p><pre><code class="language-html">&lt;template&gt;
    &lt;div&gt;{{ wsData[wsKey] }}&lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
import { mapState } from 'vuex'

export default {
    props: {
        propValue: {
            type: String,
        },
        element: {
            type: Object,
        },
        wsKey: {
            type: String,
            default: '',
        },
    },
    computed: mapState([
        'wsData',
    ]),
&lt;/script&gt;
</code></pre><p>和后台交互的方式有很多种，不仅仅包括上面两种，我在这里仅提供一些思路，以供参考。</p><h2 id="17-"><strong>17. 发布</strong></h2><p>页面发布有两种方式：一是将组件数据渲染为一个单独的 HTML 页面；二是从本项目中抽取出一个最小运行时 runtime 作为一个单独的项目。</p><p>这里说一下第二种方式，本项目中的最小运行时其实就是预览页面加上自定义组件。将这些代码提取出来作为一个项目单独打包。发布页面时将组件数据以 JSON 的格式传给服务端，同时为每个页面生成一个唯一 ID。</p><p>假设现在有三个页面，发布页面生成的 ID 为 a、b、c。访问页面时只需要把 ID 带上，这样就可以根据 ID 获取每个页面对应的组件数据。</p><pre><code class="language-js">www.test.com/?id=a
www.test.com/?id=c
www.test.com/?id=b
</code></pre><h3 id="--13"><strong>按需加载</strong></h3><p>如果自定义组件过大，例如有数十个甚至上百个。这时可以将自定义组件用 <code>import</code> 的方式导入，做到按需加载，减少首屏渲染时间：</p><pre><code class="language-js">import Vue from 'vue'

const components = [
    'Picture',
    'VText',
    'VButton',
]

components.forEach(key =&gt; {
    Vue.component(key, () =&gt; import(`@/custom-component/${key}`))
})
</code></pre><h3 id="--14"><strong>按版本发布</strong></h3><p>自定义组件有可能会有更新的情况。例如原来的组件使用了大半年，现在有功能变更，为了不影响原来的页面。建议在发布时带上组件的版本号：</p><pre><code>- v-text
  - v1.vue
  - v2.vue
</code></pre><p>例如 <code>v-text</code> 组件有两个版本，在左侧组件列表区使用时就可以带上版本号：</p><pre><code class="language-js">{
  component: 'v-text',
  version: 'v1'
  ...
}
</code></pre><p>这样导入组件时就可以根据组件版本号进行导入：</p><pre><code class="language-js">import Vue from 'vue'
import componentList from '@/custom-component/component-list`

componentList.forEach(component =&gt; {
    Vue.component(component.name, () =&gt; import(`@/custom-component/${component.name}/${component.version}`))
})
</code></pre><h2 id="--15"><strong>参考资料</strong></h2><ul><li><a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Math">Math</a></li><li><a href="https://www.jianshu.com/p/9817e267925a">通过Math.atan2 计算角度</a></li><li><a href="https://www.zhihu.com/question/67425734/answer/252724399">为什么矩阵能用来表示角的旋转？</a></li><li><a href="https://github.com/shenhudong/snapping-demo/wiki/corner-handle">snapping-demo</a></li><li><a href="https://github.com/lycHub/vue-next-drag">vue-next-drag</a></li></ul> ]]>
                </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>
        
    </channel>
</rss>
