欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

Vue 各个资源包之间的区别(vue.common.dev.js、vue.runtime.esm.js 等等)

程序员文章站 2022-06-28 18:53:50
问题描述今天有童鞋在用 vue 项目写代码的时候,问我为啥会出现以下报错?[Vue warn]: You are using the runtime-only build of Vue where the template compiler is not available. Either pre-compile the templates into render functions, or use the compiler-included build.(found in ...

问题描述

今天有童鞋在用 vue 项目写代码的时候,问我为啥会出现以下报错?

[Vue warn]: You are using the runtime-only build of Vue where the template compiler is not available. Either pre-compile the templates into render functions, or use the compiler-included build.

(found in <Root>)

Vue 各个资源包之间的区别(vue.common.dev.js、vue.runtime.esm.js 等等)

问这个问题的童鞋估计是没怎么看过 vue 的源码,vue 官网是没有这一部分的介绍的,得自己看源码!

问题原因

由于项目中引用的是 vue.runtime.esm.js,然后在代码中又用了 template 属性,导致无法解析 template 里面内容,项目报错。

解决方案

针对不同脚手架可以这样做:

vue-cli 脚手架创建的项目

创建或修改 vue.config.js 文件,修改 webpack 配置,把项目中的 vue 换成 “编译 + 运行” 版本。

module.exports = {
   ...
    chainWebpack: config => {
     config.resolve.alias
       .set("vue$",'vue/dist/vue.esm.js');
       ...
    }
};

普通 Webpack 项目

修改一下 webpack 配置,把项目中的 vue 换成 “编译 + 运行” 版本。

module.exports = {
  // ...
  resolve: {
    alias: {
      'vue$': 'vue/dist/vue.esm.js' // 'vue/dist/vue.common.js' for webpack 1
    }
  }

Rollup 项目

修改Rollup 配置,把项目中的 vue 换成 “编译 + 运行” 版本。

const alias = require('rollup-plugin-alias')

rollup({
  // ...
  plugins: [
    alias({
      'vue': 'vue/dist/vue.esm.js'
    })
  ]
})

浏览器

直接 CDN 引用 UMD 类型的 “编译 + 运行” 版本。

<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>

ok!下面我们结合 Demo 分析一下 vue 的各个 dist(资源包)之间的区别。

开始

为了更好的去分析 vue 各个资源包之间的区别,我们简单的搭建一个 vue 项目。

首先创建一个目录叫 vue-dist-demo,然后初始化 npm

mkdir vue-dist-demo && cd vue-dist-demo && npm init

接下来我们需要安装 webpack 相关的依赖:

安装 webpack

webpack 核心库。

在工程目录 vue-dist-demo 执行以下命令安装 webpack:

npm install -D webpack --registry https://registry.npm.taobao.org

安装 webpack-cli

webpack 指令库。

在工程目录 vue-dist-demo 执行以下命令:

npm install -D webpack-cli --registry https://registry.npm.taobao.org

安装 webpack-dev-server

webpack 开发者服务框架。

在工程目录 vue-dist-demo 执行以下命令:

npm install -D webpack-dev-server --registry https://registry.npm.taobao.org

安装 webpack-chain

webpack 配置工具。

在工程目录 vue-dist-demo 执行以下命令:

npm install -D webpack-chain --registry https://registry.npm.taobao.org

创建 webpack 配置

在工程目录 vue-dist-demo 下创建一个 webpack.config.js 文件:

touch webpack.config.js

然后对 webpack.config.js 进行配置,用 webpack-chain 导入一个 webpack 配置:

const config = new (require('webpack-chain'))();

module.exports = config.toConfig();

为了开发方便,我们在 package.json 中声明两个脚本 builddev

{
  "name": "vue-dist-demo",
  "version": "1.0.0",
  "description": "## 问题描述",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "rimraf dist && webpack --mode=production",
    "dev": "webpack-dev-server --mode=development --progress"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
   "webpack": "^5.4.0",
    "webpack-dev-server": "^3.11.0",
    "webpack-chain": "^6.5.1",
    "webpack-cli": "^3.3.12"
  }
}

入口与出口

我们首先在工程目录 vue-dist-demo 下创建一个 src 目录,然后在 src 目录下创建一个 main.s 文件:

mkdir src && cd src && touch main.js && cd ..

然后我们找到 webpack.config.js 文件,对 webpack 的入口和出口进行配置:

const path = require('path');
const config = new (require('webpack-chain'))();
config
    .context(path.resolve(__dirname, '.')) // webpack 上下文目录为项目根目录
        .entry('app') // 入口文件名称为 app
        .add('./src/main.js') // 入口文件为 ./src/main.ts
        .end()
    .output
        .path(path.join(__dirname, './dist')) // webpack 输出的目录为根目录的 dist 目录
        .filename('[name].[hash:8].js') // 打包出来的 bundle 名称为 "[name].[hash:8].js"
        .publicPath('/') // publicpath 配置为 "/"
        .end()

安装 vue

vue 核心 API。

npm install vue --registry https://registry.npm.taobao.org

安装 vue-loader

.vue 文件加载器。

npm install vue-loader -D --registry https://registry.npm.taobao.org

安装 vue-template-compiler

.vue 文件模版解析器。

npm install vue-template-compiler -D --registry https://registry.npm.taobao.org

安装 html-webpack-plugin

npm install -D html-webpack-plugin -D --registry https://registry.npm.taobao.org

接下来我们在工程目录 sy_webpack-wedding 底下创建一个 public 目录,然后在 public 目录下创建一个 index.html 文件作为我们 app 的入口页面:

mkdir public && touch public/index.html

然后将以下内容写入 public/index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Title</title>
  </head>
  <body>
    <noscript>your browser should support javascript!</noscript>
    <div id="app"></div>
    <!-- html-webpack-plugin 将自动引入入口文件 -->
  </body>
</html>

webpack 配置全部内容:

const path = require('path');
const config = new (require('webpack-chain'))();
config
    .context(path.resolve(__dirname, '.')) // webpack 上下文目录为项目根目录
        .entry('app') // 入口文件名称为 app
        .add('./src/main.js') // 入口文件为 ./src/main.ts
        .end()
    .output
        .path(path.join(__dirname, './dist')) // webpack 输出的目录为根目录的 dist 目录
        .filename('[name].[hash:8].js') // 打包出来的 bundle 名称为 "[name].[hash:8].js"
        .publicPath('/') // publicpath 配置为 "/"
        .end()
    .resolve
        .extensions
            .add('.js')
            .add('.vue') // 配置以 .js 等结尾的文件当模块使用的时候都可以省略后缀
            .end()
        .end()
    .module
        .rule('vue') // vue-loader 相关配置
            .test(/\.vue$/) // 匹配 .vue 文件
            .use('vue-loader')
                .loader('vue-loader')
                .end()
            .end()
        .end()
    .plugin('vue-loader-plugin') // vue-loader 必须要添加 vue-loader-plugin
        .use(require('vue-loader').VueLoaderPlugin, [])
        .end()
    .plugin('html') // 添加 html-webpack-plugin 插件
        .use(require('html-webpack-plugin'), [
            {
                template: path.resolve(__dirname, './public/index.html'), // 指定模版文件
                chunks: ['app'], // 指定需要加载的 chunk
                inject: 'body', // 指定 script 脚本注入的位置为 body
            },
        ])
        .end()
    .devServer
        .host('0.0.0.0') // 服务器外部可访问
        .disableHostCheck(true) // 关闭白名单校验
        .contentBase(path.resolve(__dirname, './public')) // 设置一个 express 静态目录
        .historyApiFallback({
            disableDotRule: true, // 禁止在链接中使用 "." 符号
            rewrites: [
                { from: /^\/$/, to: '/index.html' }, // 将所有的 404 响应重定向到 index.html 页面
            ],
        })
        .port(8080) // 当前端口号
        .hot(true) // 打开页面热载功能
        .sockPort('location') // 设置成平台自己的端口
        .open(true);
module.exports = config.toConfig();

测试

我们在 src 目录下创建一个 app.vue 文件:

touch src/app.vue

然后将以下内容写入其中:

<template>
    <div class="app">{{ msg }}</div>
</template>

<script>
export default {
    name: "app",
    data(){
        return {
            msg: "hello"
        }
    }
}
</script>

很简单,就是一个简单的 vue 文件。

然后在 main.js 中引入 app.vue 文件:

import Vue from 'vue';
import App from './app.vue';
new Vue({
    el: '#app',
    render: (h) => h(App),
});

ok!一切准备完毕后,我们直接在工程目录运行 npm run dev 命令:

npm run dev

运行完毕后浏览器打开:

Vue 各个资源包之间的区别(vue.common.dev.js、vue.runtime.esm.js 等等)

可以看到,一个简单的 vue 项目就搭建并且运行起来了。

其实如果对 webpack 很熟悉的话,从 0 开始搭建一个 vue 项目就很简单了,所以强烈推荐大家去看一下我上一篇 webpack 的文章,来和 webpack 谈场恋爱吧

ok!我们看一下目前的 src/main.js 文件:

import Vue from 'vue';
import App from './app.vue';
new Vue({
    el: '#app',
    render: (h) => h(App),
});

我们试着换一种方式来实现渲染:

import Vue from 'vue';
import App from './app.vue';
new Vue({
    el: '#app',
    components:{
        App,
    },
    template: "<app></app>"
});

可以看到,我们注册了一个 App 组件,然后通过 vue 的 template 渲染了一个 App 组件。有点 vue 基础的童鞋,这个代码还是很容易看懂的,我就不一一解释了。

我们运行一下 npm run dev 命令,然后打开浏览器看效果:

npm run dev

Vue 各个资源包之间的区别(vue.common.dev.js、vue.runtime.esm.js 等等)

可以看到,跟我们文章一开始报的错误一样了,那么我们可以怎么修改呢?前面已经给了解决方法。

  1. 修改 webpack.config.js 文件,把项目中的 vue 换成 “编译 + 运行” 版本。

    const path = require('path');
    const config = new (require('webpack-chain'))();
    config
        .context(path.resolve(__dirname, '.')) // webpack 上下文目录为项目根目录
        .entry('app') // 入口文件名称为 app
            .add('./src/main.js') // 入口文件为 ./src/main.ts
            .end()
        .output
            .path(path.join(__dirname, './dist')) // webpack 输出的目录为根目录的 dist 目录
            .filename('[name].[hash:8].js') // 打包出来的 bundle 名称为 "[name].[hash:8].js"
            .publicPath('/') // publicpath 配置为 "/"
            .end()
        .resolve
            .extensions
                .add('.js')
                .add('.vue') // 配置以 .js 等结尾的文件当模块使用的时候都可以省略后缀
                .end()
            .alias
                .set('vue$', 'vue/dist/vue.esm.js')
                .end()
            .end()
        .module
            .rule('vue') // vue-loader 相关配置
                .test(/\.vue$/) // 匹配 .vue 文件
                .use('vue-loader')
                    .loader('vue-loader')
                    .end()
                .end()
            .end()
        .plugin('vue-loader-plugin') // vue-loader 必须要添加 vue-loader-plugin
            .use(require('vue-loader').VueLoaderPlugin, [])
            .end()
        .plugin('html') // 添加 html-webpack-plugin 插件
            .use(require('html-webpack-plugin'), [
                {
                    template: path.resolve(__dirname, './public/index.html'), // 指定模版文件
                    chunks: ['app'], // 指定需要加载的 chunk
                    inject: 'body', // 指定 script 脚本注入的位置为 body
                },
            ])
            .end()
        .devServer
            .host('0.0.0.0') // 服务器外部可访问
            .disableHostCheck(true) // 关闭白名单校验
            .contentBase(path.resolve(__dirname, './public')) // 设置一个 express 静态目录
            .historyApiFallback({
                disableDotRule: true, // 禁止在链接中使用 "." 符号
                rewrites: [
                    { from: /^\/$/, to: '/index.html' }, // 将所有的 404 响应重定向到 index.html 页面
                ],
            })
            .port(8080) // 当前端口号
            .hot(true) // 打开页面热载功能
            .sockPort('location') // 设置成平台自己的端口
            .open(true);
    module.exports = config.toConfig();
    

    可以看到,我们指定 webpack 的 alias,给 vue 源码指向了 vue/dist/vue.esm.js 文件,效果我就不演示了哈,小伙伴自己运行看效果。

  2. 引入 vue 的时候直接引入 vue/dist/vue.esm.js 文件。

    修改一下 src/main.js 文件:

    // import Vue from 'vue';
    import Vue from 'vue/dist/vue.esm';
    import App from './app.vue';
    new Vue({
        el: '#app',
        components:{
            App,
        },
        template: "<app></app>"
    });
    

    可以看到,我们引入 vue 的时候直接指定了 vue/dist/vue.esm.js 文件,小伙伴自己运行看效果。

分析

效果我们看到了,怎么解决报错我们也知道了,那么各个 vue 源码文件到底有什么区别呢?

我们先看一下到底有哪些 vue 源码文件?我们找到 vue 的依赖:
Vue 各个资源包之间的区别(vue.common.dev.js、vue.runtime.esm.js 等等)

可以看到,vue 的源码文件还是比较多的,那么之间的区别又是啥呢?我们平时的项目又该怎么选呢?

其实官方已经给了一段解释,但是解释的很简单,我们可以找到 vue/dist/README.md 文件:

UMD CommonJS ES Module
Full vue.js vue.common.js vue.esm.js
Runtime-only vue.runtime.js vue.runtime.common.js > > vue.runtime.esm.js
Full (production) vue.min.js
Runtime-only (production) vue.runtime.min.js

其实我们发现,虽然源码文件很多,但是区别好像就是一个加了 compiler ,一个没加 compiler,比如:vue.jsvue.runtime.js

我们去 vue 的官网找到一份打包各个版本的脚本文件 https://github.com/vuejs/vue/blob/dev/scripts/config.js

...
const builds = {
  // Runtime only (CommonJS). Used by bundlers e.g. Webpack & Browserify
  'web-runtime-cjs-dev': {
    entry: resolve('web/entry-runtime.js'),
    dest: resolve('dist/vue.runtime.common.dev.js'),
    format: 'cjs',
    env: 'development',
    banner
  },
  'web-runtime-cjs-prod': {
    entry: resolve('web/entry-runtime.js'),
    dest: resolve('dist/vue.runtime.common.prod.js'),
    format: 'cjs',
    env: 'production',
    banner
  },
  // Runtime+compiler CommonJS build (CommonJS)
  'web-full-cjs-dev': {
    entry: resolve('web/entry-runtime-with-compiler.js'),
    dest: resolve('dist/vue.common.dev.js'),
    format: 'cjs',
    env: 'development',
    alias: { he: './entity-decoder' },
    banner
  },
  'web-full-cjs-prod': {
    entry: resolve('web/entry-runtime-with-compiler.js'),
    dest: resolve('dist/vue.common.prod.js'),
    format: 'cjs',
    env: 'production',
    alias: { he: './entity-decoder' },
    banner
  },
  // Runtime only ES modules build (for bundlers)
  'web-runtime-esm': {
    entry: resolve('web/entry-runtime.js'),
    dest: resolve('dist/vue.runtime.esm.js'),
    format: 'es',
    banner
  },
  // Runtime+compiler ES modules build (for bundlers)
  'web-full-esm': {
    entry: resolve('web/entry-runtime-with-compiler.js'),
    dest: resolve('dist/vue.esm.js'),
    format: 'es',
    alias: { he: './entity-decoder' },
    banner
  },
  // Runtime+compiler ES modules build (for direct import in browser)
  'web-full-esm-browser-dev': {
    entry: resolve('web/entry-runtime-with-compiler.js'),
    dest: resolve('dist/vue.esm.browser.js'),
    format: 'es',
    transpile: false,
    env: 'development',
    alias: { he: './entity-decoder' },
    banner
  },
  // Runtime+compiler ES modules build (for direct import in browser)
  'web-full-esm-browser-prod': {
    entry: resolve('web/entry-runtime-with-compiler.js'),
    dest: resolve('dist/vue.esm.browser.min.js'),
    format: 'es',
    transpile: false,
    env: 'production',
    alias: { he: './entity-decoder' },
    banner
  },
  // runtime-only build (Browser)
  'web-runtime-dev': {
    entry: resolve('web/entry-runtime.js'),
    dest: resolve('dist/vue.runtime.js'),
    format: 'umd',
    env: 'development',
    banner
  },
  // runtime-only production build (Browser)
  'web-runtime-prod': {
    entry: resolve('web/entry-runtime.js'),
    dest: resolve('dist/vue.runtime.min.js'),
    format: 'umd',
    env: 'production',
    banner
  },
  // Runtime+compiler development build (Browser)
  'web-full-dev': {
    entry: resolve('web/entry-runtime-with-compiler.js'),
    dest: resolve('dist/vue.js'),
    format: 'umd',
    env: 'development',
    alias: { he: './entity-decoder' },
    banner
  },
  // Runtime+compiler production build  (Browser)
  'web-full-prod': {
    entry: resolve('web/entry-runtime-with-compiler.js'),
    dest: resolve('dist/vue.min.js'),
    format: 'umd',
    env: 'production',
    alias: { he: './entity-decoder' },
    banner
  },
  // Web compiler (CommonJS).
  'web-compiler': {
    entry: resolve('web/entry-compiler.js'),
    dest: resolve('packages/vue-template-compiler/build.js'),
    format: 'cjs',
    external: Object.keys(require('../packages/vue-template-compiler/package.json').dependencies)
  },
  // Web compiler (UMD for in-browser use).
  'web-compiler-browser': {
    entry: resolve('web/entry-compiler.js'),
    dest: resolve('packages/vue-template-compiler/browser.js'),
    format: 'umd',
    env: 'development',
    moduleName: 'VueTemplateCompiler',
    plugins: [node(), cjs()]
  },
  // Web server renderer (CommonJS).
  'web-server-renderer-dev': {
    entry: resolve('web/entry-server-renderer.js'),
    dest: resolve('packages/vue-server-renderer/build.dev.js'),
    format: 'cjs',
    env: 'development',
    external: Object.keys(require('../packages/vue-server-renderer/package.json').dependencies)
  },
  'web-server-renderer-prod': {
    entry: resolve('web/entry-server-renderer.js'),
    dest: resolve('packages/vue-server-renderer/build.prod.js'),
    format: 'cjs',
    env: 'production',
    external: Object.keys(require('../packages/vue-server-renderer/package.json').dependencies)
  },
  'web-server-renderer-basic': {
    entry: resolve('web/entry-server-basic-renderer.js'),
    dest: resolve('packages/vue-server-renderer/basic.js'),
    format: 'umd',
    env: 'development',
    moduleName: 'renderVueComponentToString',
    plugins: [node(), cjs()]
  },
  'web-server-renderer-webpack-server-plugin': {
    entry: resolve('server/webpack-plugin/server.js'),
    dest: resolve('packages/vue-server-renderer/server-plugin.js'),
    format: 'cjs',
    external: Object.keys(require('../packages/vue-server-renderer/package.json').dependencies)
  },
  'web-server-renderer-webpack-client-plugin': {
    entry: resolve('server/webpack-plugin/client.js'),
    dest: resolve('packages/vue-server-renderer/client-plugin.js'),
    format: 'cjs',
    external: Object.keys(require('../packages/vue-server-renderer/package.json').dependencies)
  },
  // Weex runtime factory
  'weex-factory': {
    weex: true,
    entry: resolve('weex/entry-runtime-factory.js'),
    dest: resolve('packages/weex-vue-framework/factory.js'),
    format: 'cjs',
    plugins: [weexFactoryPlugin]
  },
  // Weex runtime framework (CommonJS).
  'weex-framework': {
    weex: true,
    entry: resolve('weex/entry-framework.js'),
    dest: resolve('packages/weex-vue-framework/index.js'),
    format: 'cjs'
  },
  // Weex compiler (CommonJS). Used by Weex's Webpack loader.
  'weex-compiler': {
    weex: true,
    entry: resolve('weex/entry-compiler.js'),
    dest: resolve('packages/weex-template-compiler/build.js'),
    format: 'cjs',
    external: Object.keys(require('../packages/weex-template-compiler/package.json').dependencies)
  }
}
...

我们挑两个文件来分析一下,vue.jsvue.runtime.js

vue.runtime.js

找到 vue.runtime.js 的脚本配置:

 // runtime-only build (Browser)
 // 运行时代码
  'web-runtime-dev': {
    entry: resolve('web/entry-runtime.js'),
    dest: resolve('dist/vue.runtime.js'),
    format: 'umd',
    env: 'development',
    banner
  },

找到 vue.runtime.js 的入口文件 /web/entry-runtime.js

/* @flow */

import Vue from './runtime/index'

export default Vue

很简单,就直接导出了 Vue

vue.js

找到 vue.js 的脚本配置:

 // Runtime+compiler development build (Browser)
	// 运行时代码 + 编译时代码
  'web-full-dev': {
    entry: resolve('web/entry-runtime-with-compiler.js'),
    dest: resolve('dist/vue.js'),
    format: 'umd',
    env: 'development',
    alias: { he: './entity-decoder' },
    banner
  },

找到 vue.js 的入口文件 web/entry-runtime-with-compiler.js

/* @flow */

import config from 'core/config'
import { warn, cached } from 'core/util/index'
import { mark, measure } from 'core/util/perf'

import Vue from './runtime/index'
import { query } from './util/index'
import { compileToFunctions } from './compiler/index'
import { shouldDecodeNewlines, shouldDecodeNewlinesForHref } from './util/compat'

const idToTemplate = cached(id => {
  const el = query(id)
  return el && el.innerHTML
})

const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && query(el)

  /* istanbul ignore if */
  if (el === document.body || el === document.documentElement) {
    process.env.NODE_ENV !== 'production' && warn(
      `Do not mount Vue to <html> or <body> - mount to normal elements instead.`
    )
    return this
  }

  const options = this.$options
  // resolve template/el and convert to render function
  if (!options.render) {
    let template = options.template
    if (template) {
      if (typeof template === 'string') {
        if (template.charAt(0) === '#') {
          template = idToTemplate(template)
          /* istanbul ignore if */
          if (process.env.NODE_ENV !== 'production' && !template) {
            warn(
              `Template element not found or is empty: ${options.template}`,
              this
            )
          }
        }
      } else if (template.nodeType) {
        template = template.innerHTML
      } else {
        if (process.env.NODE_ENV !== 'production') {
          warn('invalid template option:' + template, this)
        }
        return this
      }
    } else if (el) {
      template = getOuterHTML(el)
    }
    if (template) {
      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile')
      }

      const { render, staticRenderFns } = compileToFunctions(template, {
        outputSourceRange: process.env.NODE_ENV !== 'production',
        shouldDecodeNewlines,
        shouldDecodeNewlinesForHref,
        delimiters: options.delimiters,
        comments: options.comments
      }, this)
      options.render = render
      options.staticRenderFns = staticRenderFns

      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile end')
        measure(`vue ${this._name} compile`, 'compile', 'compile end')
      }
    }
  }
  return mount.call(this, el, hydrating)
}

/**
 * Get outerHTML of elements, taking care
 * of SVG elements in IE as well.
 */
function getOuterHTML (el: Element): string {
  if (el.outerHTML) {
    return el.outerHTML
  } else {
    const container = document.createElement('div')
    container.appendChild(el.cloneNode(true))
    return container.innerHTML
  }
}

Vue.compile = compileToFunctions

export default Vue

可以看到,相比 entry-runtime.jsentry-runtime-with-compiler.js 重写了 Vue 的$mount 方法,在 $mount 方法里面利用了 compileToFunctions 方法把 template 编译成了 render 函数,更多 Vue 源码的解析可以参考我之前的几篇 Vue 源码的文章:

其它几个源码文件大家可以按照一样的方式去分析,我就不一一解析了。

总结

如果你需要用到 vue 的 compiler 功能,你就需要用带 compiler 功能的 vue 源码,如果你不需要的话,只需要导入 runtime 版本的 vue 源码即可,因为带 compiler 功能的 vue 源码比runtime 版本的 vue 源码多了模版编译的功能,会使项目的 size 更大一些。

本文地址:https://blog.csdn.net/vv_bug/article/details/109586682