【第二篇】创建 @vue/cli3 插件,并整合 ssr 功能

栏目: 编程语言 · 发布时间: 6年前

内容简介:在上一篇文章在本篇文章,我们来创建一个首先,我们来看一个

在上一篇文章 《基于 @vue/cli3 与 koa 创建 ssr 工程》 中,我们讲解了如何基于 @vue/cli3 创建一个 ssr 工程。

在本篇文章,我们来创建一个 @vue/cli3 插件,并将第一篇文章中 ssr 工程的服务器端部分整合进插件中。

首先,我们来看一个 cli3 插件提供了那些功能:

  1. 使用脚手架创建一个新工程或在一个既有工程安装并执行插件后,生成自定义的工程文件
  2. 基于 @vue/cli-service 提供统一的自定义命令,例如: vue-cli-service ssr:build

除了上述两个功能外,我们还希望在插件内部整合服务端逻辑,这样对于多个接入插件的工程项目能实行统一的管理,方便后续统一增加日志、监控等功能。

创建插件 npm 库

官方对于发布一个 cli3 的插件做了如下限制

为了让一个 CLI 插件能够被其它开发者使用,你必须遵循 vue-cli-plugin-<name> 的命名约定将其发布到 npm 上。插件遵循命名约定之后就可以:

  • @vue/cli-service 发现;
  • 被其它开发者搜索到;
  • 通过 vue add <name>vue invoke <name> 安装下来。

因此,我们新建并初始化一个工程 createPluginExample ,并将工程的 name 命名为 vue-cli-plugin-my_ssr_plugin_demo

mkdir createPluginExample && cd createPluginExample && yarn init
复制代码

package.json 的内容为:

{
  "name": "vue-cli-plugin-my_ssr_plugin_demo",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT"
}
复制代码

创建插件 npm 库的模板内容

官方对于第一个插件功能,引入了一个叫做 Generator 的机制来实现

Generator 有两种展现形式: generator.jsgenerator/index.js

generator.jsgenerator/index.js 的内容导出一个函数,这个函数接收三个参数,分别是:

GeneratorAPI
generator
preset

关于 preset ,我们可以将其看作是:将手动创建一个工程项目过程中,通过会话选择的自定义选项内容保存下来的预设文件

例如:

module.exports = (api, options, rootOptions) => {
  // 修改 `package.json` 里的字段
  api.extendPackage({
    scripts: {
      test: 'vue-cli-service test'
    }
  })

  // 复制并用 ejs 渲染 `./template` 内所有的文件
  api.render('./template')

  if (options.foo) {
    // 有条件地生成文件
  }
}
复制代码

以及两种安装方式:

preset
vue invoke

Generator 允许在文件夹 generator 中创建一个叫做 template 的文件夹

如果在 generator/index.js 中显式调用了 api.render('./template')

那么 generator 将会使用 EJS 渲染 ./template 中的文件,并替换工程中根目录下对应的文件。

因为我们的 ssr 工程项目需要对默认的 spa 工程中的部分文件做一些改造(详见上一篇文章 《基于 @vue/cli3 与 koa 创建 ssr 工程》

所以在这里我们选择 generator/index.js 这种展现形式。

并在 generator 中创建文件夹 template 并将第一篇文章中已经改造好的文件放置在 ./template 文件夹中。

此时,我们的 createPluginExample 工程目录结构如下:

.
├── generator
│   ├── index.js
│   └── template
│       ├── src
│       │   ├── App.vue
│       │   ├── components
│       │   │   └── HelloWorld.vue
│       │   ├── entry-client.js
│       │   ├── entry-server.js
│       │   ├── main.js
│       │   ├── router
│       │   │   ├── index.js
│       │   ├── store
│       │   │   ├── index.js
│       │   │   └── modules
│       │   │       └── book.js
│       │   └── views
│       │       ├── About.vue
│       │       └── Home.vue
│       └── vue.config.js
└── package.json
复制代码

接下来让我们看 generator/index.js 中的内容

定制插件安装过程

我们需要在 generator/index.js 做三件事情:

  1. 按照 ssr 工程模式自定义工程的 package.json 的内容
  2. 执行 api.render('./template') 触发 generator 使用 EJS 渲染 generator/template/ 中的文件,并替换工程中根目录下对应的文件
  3. 在工程创建完毕后,执行一些收尾工作

关于第一件事情

首先我们需要在创建工程项目后,自动创建好基于 ssr 的一些命令,比如服务器端构建 ssr:build ,开发环境启动 ssr 服务 dev

其次,我们还需要在创建工程项目后,自动安装好 ssr 依赖的某些第三方工具,例如: concurrently

第二件事件比较简单,我们这里直接按照官方文档写就可以。

关于第三件事情:

  • 因为默认的 spa 工程会在 src 下生成 router.jsstore.js 这些文件,而插件在安装过程中不会删除掉这些文件,因此我们需要在工程安装完毕后,清理这些文件。
  • 另外,因为我们后面会将服务器端的逻辑整合到插件内部,因此像服务器端构建 ssr:build 命令就需要在产品环境下执行了,因此我们需要将我们的插件 vue-cli-plugin-my_ssr_plugin_demo , 以及 @vue/cli-plugin-babel@vue/cli-service , 由 devDependencies 中移动到 dependencies 中。
  • 最后,还记得我们在第一篇文章 《基于 @vue/cli3 与 koa 创建 ssr 工程》 中的 public/index.ejs 么,因为这个文件本身就是 ejs 格式的,所以在插件安装过程中渲染 generator/template 文件夹中的内容时,会影响到它,所以我们将其放在 generator/ 文件夹下,在安装过程结束后,将其复制到工程的 public

最终, generator/index.js 的内容如下:

const shell = require('shelljs')
const chalk = require('chalk')

module.exports = (api, options, rootOptions) => {
  // 修改 `package.json` 里的字段
  api.extendPackage({
    scripts: {
    'serve': 'vue-cli-service serve',
    'ssr:serve': 'NODE_ENV=development TARGET_NODE=node PORT=3000 CLIENT_PORT=8080 node ./app/server.js',
    'dev': 'concurrently \'npm run serve\' \'npm run ssr:serve\'',
    'build': 'vue-cli-service build && TARGET_NODE=node vue-cli-service build --no-clean --silent',
    'start': 'NODE_ENV=production TARGET_NODE=node PORT=3000 node ./node_modules/vue-cli-plugin-my_ssr_plugin_demo/app/server.js'
    },
    dependencies: {
    },
    devDependencies: {
      'concurrently': '^4.1.0'
    }
  })

  // 复制并用 ejs 渲染 `./template` 内所有的文件
  api.render('./template', Object.assign({ BASE_URL: '' }, options))

  api.onCreateComplete(() => {
    shell.cd(api.resolve('./'))

    shell.exec('cp ./node_modules/vue-cli-plugin-my_ssr_plugin_demo/generator/tmp.ejs ./public/index.ejs')
    shell.exec('rm ./public/index.html')
    shell.exec('rm ./public/favicon.ico')

    const routerFile = './src/router.js'
    const storeFile = './src/store.js'

    console.log(chalk.green('\nremove the old entry file of vue-router and vuex'))
    shell.exec(`
      echo \n\n
      if [ -f ${routerFile} ];then
      rm -f ${routerFile}
      fi
      if [ -f ${storeFile} ];then
      rm -f ${storeFile}
      fi
    `)

    let packageJson = JSON.parse(shell.exec('cat ./package.json', { silent: true }).stdout)
    const needToMove = [
      '@vue/cli-plugin-babel',
      '@vue/cli-service',
      'vue-cli-plugin-my_ssr_plugin_demo'
    ]

    needToMove.forEach(name => {
      if (!packageJson.devDependencies[name]) return
      packageJson.dependencies[name] = packageJson.devDependencies[name]
      delete packageJson.devDependencies[name]
    })

    console.log(chalk.green(`move the ${needToMove.join(',')} from devDependencies to dependencies`))
    shell.exec(`echo '${JSON.stringify(packageJson, null, 2)}' > ./package.json`)
  })
}
复制代码

接下来我们来看服务器端部分

整合服务器端逻辑

在第一篇文章中,我们将服务器端的逻辑都存放在 app/ 文件夹中

app
├── middlewares
│   ├── dev.ssr.js
│   ├── dev.static.js
│   └── prod.ssr.js
└── server.js
复制代码

我们只需要将此文件夹复制到插件工程的根目录下,然后在根目录下创建一个名为 index.js 的文件。

index.js 文件中,我们会做如下三件事情:

  1. vue.config.js 中的配置整合进插件中,也就是 index.js 中提供的 api.chainWebpack 内部,这样做的好处是安装此插件的工程项目不必再关心 ssr 相关的 webpack 配置细节
  2. 提供开发环境启动 ssr 服务的命令: ssr:serve
  3. 提供产品环境构建 ssr 服务 bundle 的命令: ssr:build

当调用 vue-cli-service <command> [...args] 时会创建一个叫做 Service 的插件。

Service 插件负责管理内部的 webpack 配置、暴露服务和构建项目的命令等, 它属于插件的一部分。

一个 Service 插件导出一个函数,这个函数接收两个参数:

PluginAPI
vue.config.js

Service 插件针对不同的环境扩展/修改内部的 webpack 配置,并向 vue-cli-service 注入额外的命令,例如:

module.exports = (api, projectOptions) => {
  api.chainWebpack(webpackConfig => {
    // 通过 webpack-chain 修改 webpack 配置
  })

  api.configureWebpack(webpackConfig => {
    // 修改 webpack 配置
    // 或返回通过 webpack-merge 合并的配置对象
  })

  api.registerCommand('test', args => {
    // 注册 `vue-cli-service test`
  })
}
复制代码

在这里,我们将第一篇中的 vue.config.js 中的内容移到 index.js 中的 api.chainWebpack

const get = require('lodash.get')
const VueSSRServerPlugin = require('vue-server-renderer/server-plugin')
const VueSSRClientPlugin = require('vue-server-renderer/client-plugin')
const nodeExternals = require('webpack-node-externals')
const merge = require('lodash.merge')

module.exports = (api, projectOptions) => {
  const clientPort = get(projectOptions, 'devServer.port', 8080)

  api.chainWebpack(config => {
    const TARGET_NODE = process.env.WEBPACK_TARGET === 'node'
    const DEV_MODE = process.env.NODE_ENV === 'development'

    if (DEV_MODE) {
      config.devServer.headers({ 'Access-Control-Allow-Origin': '*' }).port(clientPort)
    }

    config
      .entry('app')
      .clear()
      .add('./src/entry-client.js')
      .end()
      // 为了让服务器端和客户端能够共享同一份入口模板文件
      // 需要让入口模板文件支持动态模板语法(这里选择了 TJ 的 ejs)
      .plugin('html')
      .tap(args => {
        return [{
          template: './public/index.ejs',
          minify: {
            collapseWhitespace: true
          },
          templateParameters: {
            title: 'spa',
            mode: 'client'
          }
        }]
      })
      .end()
      // Exclude unprocessed HTML templates from being copied to 'dist' folder.
      .when(config.plugins.has('copy'), config => {
        config.plugin('copy').tap(([[config]]) => [
          [
            {
              ...config,
              ignore: [...config.ignore, 'index.ejs']
            }
          ]
        ])
      })
      .end()

    // 默认值: 当 webpack 配置中包含 target: 'node' 且 vue-template-compiler 版本号大于等于 2.4.0 时为 true。
    // 开启 Vue 2.4 服务端渲染的编译优化之后,渲染函数将会把返回的 vdom 树的一部分编译为字符串,以提升服务端渲染的性能。
    // 在一些情况下,你可能想要明确的将其关掉,因为该渲染函数只能用于服务端渲染,而不能用于客户端渲染或测试环境。
    config.module
      .rule('vue')
      .use('vue-loader')
      .tap(options => {
        merge(options, {
          optimizeSSR: false
        })
      })

    config.plugins
      // Delete plugins that are unnecessary/broken in SSR & add Vue SSR plugin
      .delete('pwa')
      .end()
      .plugin('vue-ssr')
      .use(TARGET_NODE ? VueSSRServerPlugin : VueSSRClientPlugin)
      .end()

    if (!TARGET_NODE) return

    config
      .entry('app')
      .clear()
      .add('./src/entry-server.js')
      .end()
      .target('node')
      .devtool('source-map')
      .externals(nodeExternals({ whitelist: /\.css$/ }))
      .output.filename('server-bundle.js')
      .libraryTarget('commonjs2')
      .end()
      .optimization.splitChunks({})
      .end()
      .plugins.delete('named-chunks')
      .delete('hmr')
      .delete('workbox')
  })
复制代码

接下来让我们创建开发环境启动 ssr 服务的命令: ssr:serve

const DEFAULT_PORT = 3000

...

  api.registerCommand('ssr:serve', {
    description: 'start development server',
    usage: 'vue-cli-service ssr:serve [options]',
    options: {
      '--port': `specify port (default: ${DEFAULT_PORT})`
    }
  }, args => {
    process.env.WEBPACK_TARGET = 'node'

    const port = args.port || DEFAULT_PORT

    console.log(
      '[SSR service] will run at:' +
      chalk.blue(`
        http://localhost:${port}/
      `)
    )

    shell.exec(`
      PORT=${port} \
      CLIENT_PORT=${clientPort} \
      CLIENT_PUBLIC_PATH=${projectOptions.publicPath} \
      node ./node_modules/vue-cli-plugin-my_ssr_plugin_demo/app/server.js \
    `)
  })

...

module.exports.defaultModes = {
  'ssr:serve': 'development' // 为 ssr:serve 指定开发环境模式
}
复制代码

最后,我们创建产品环境构建 ssr 服务 bundle 的命令: ssr:build

const onCompilationComplete = (err, stats) => {
  if (err) {
    console.error(err.stack || err)
    if (err.details) console.error(err.details)
    return
  }

  if (stats.hasErrors()) {
    stats.toJson().errors.forEach(err => console.error(err))
    process.exitCode = 1
  }

  if (stats.hasWarnings()) {
    stats.toJson().warnings.forEach(warn => console.warn(warn))
  }
}

...

  api.registerCommand('ssr:build', args => {
    process.env.WEBPACK_TARGET = 'node'

    const webpackConfig = api.resolveWebpackConfig()
    const compiler = webpack(webpackConfig)

    compiler.run(onCompilationComplete)

    shell.exec('node ./node_modules/vue-cli-plugin-my_ssr_plugin_demo/bin/initRouter.js')
  })

...

module.exports.defaultModes = {
  'ssr:build': 'production', // 为 ssr:build 指定产品环境模式
  'ssr:serve': 'development'
}
复制代码

最终,完整的 index.js 内容如下:

const webpack = require('webpack')
const get = require('lodash.get')
const VueSSRServerPlugin = require('vue-server-renderer/server-plugin')
const VueSSRClientPlugin = require('vue-server-renderer/client-plugin')
const nodeExternals = require('webpack-node-externals')
const merge = require('lodash.merge')
const shell = require('shelljs')
const chalk = require('chalk')

const DEFAULT_PORT = 3000

const onCompilationComplete = (err, stats) => {
  if (err) {
    console.error(err.stack || err)
    if (err.details) console.error(err.details)
    return
  }

  if (stats.hasErrors()) {
    stats.toJson().errors.forEach(err => console.error(err))
    process.exitCode = 1
  }

  if (stats.hasWarnings()) {
    stats.toJson().warnings.forEach(warn => console.warn(warn))
  }
}

module.exports = (api, projectOptions) => {
  const clientPort = get(projectOptions, 'devServer.port', 8080)

  api.chainWebpack(config => {
    const TARGET_NODE = process.env.WEBPACK_TARGET === 'node'
    const DEV_MODE = process.env.NODE_ENV === 'development'

    if (DEV_MODE) {
      config.devServer.headers({ 'Access-Control-Allow-Origin': '*' }).port(clientPort)
    }

    config
      .entry('app')
      .clear()
      .add('./src/entry-client.js')
      .end()
      // 为了让服务器端和客户端能够共享同一份入口模板文件
      // 需要让入口模板文件支持动态模板语法(这里选择了 TJ 的 ejs)
      .plugin('html')
      .tap(args => {
        return [{
          template: './public/index.ejs',
          minify: {
            collapseWhitespace: true
          },
          templateParameters: {
            title: 'spa',
            mode: 'client'
          }
        }]
      })
      .end()
      // Exclude unprocessed HTML templates from being copied to 'dist' folder.
      .when(config.plugins.has('copy'), config => {
        config.plugin('copy').tap(([[config]]) => [
          [
            {
              ...config,
              ignore: [...config.ignore, 'index.ejs']
            }
          ]
        ])
      })
      .end()

    // 默认值: 当 webpack 配置中包含 target: 'node' 且 vue-template-compiler 版本号大于等于 2.4.0 时为 true。
    // 开启 Vue 2.4 服务端渲染的编译优化之后,渲染函数将会把返回的 vdom 树的一部分编译为字符串,以提升服务端渲染的性能。
    // 在一些情况下,你可能想要明确的将其关掉,因为该渲染函数只能用于服务端渲染,而不能用于客户端渲染或测试环境。
    config.module
      .rule('vue')
      .use('vue-loader')
      .tap(options => {
        merge(options, {
          optimizeSSR: false
        })
      })

    config.plugins
      // Delete plugins that are unnecessary/broken in SSR & add Vue SSR plugin
      .delete('pwa')
      .end()
      .plugin('vue-ssr')
      .use(TARGET_NODE ? VueSSRServerPlugin : VueSSRClientPlugin)
      .end()

    if (!TARGET_NODE) return

    config
      .entry('app')
      .clear()
      .add('./src/entry-server.js')
      .end()
      .target('node')
      .devtool('source-map')
      .externals(nodeExternals({ whitelist: /\.css$/ }))
      .output.filename('server-bundle.js')
      .libraryTarget('commonjs2')
      .end()
      .optimization.splitChunks({})
      .end()
      .plugins.delete('named-chunks')
      .delete('hmr')
      .delete('workbox')
  })

  api.registerCommand('ssr:build', args => {
    process.env.WEBPACK_TARGET = 'node'

    const webpackConfig = api.resolveWebpackConfig()
    const compiler = webpack(webpackConfig)

    compiler.run(onCompilationComplete)

    shell.exec('node ./node_modules/vue-cli-plugin-my_ssr_plugin_demo/bin/initRouter.js')
  })

  api.registerCommand('ssr:serve', {
    description: 'start development server',
    usage: 'vue-cli-service ssr:serve [options]',
    options: {
      '--port': `specify port (default: ${DEFAULT_PORT})`
    }
  }, args => {
    process.env.WEBPACK_TARGET = 'node'

    const port = args.port || DEFAULT_PORT

    console.log(
      '[SSR service] will run at:' +
      chalk.blue(`
        http://localhost:${port}/
      `)
    )

    shell.exec(`
      PORT=${port} \
      CLIENT_PORT=${clientPort} \
      CLIENT_PUBLIC_PATH=${projectOptions.publicPath} \
      node ./node_modules/vue-cli-plugin-my_ssr_plugin_demo/app/server.js \
    `)
  })
}

module.exports.defaultModes = {
  'ssr:build': 'production',
  'ssr:serve': 'development'
}
复制代码

完整的 vue-cli-plugin-my_ssr_plugin_demo 目录结构如下:

.
├── app
│   ├── middlewares
│   │   ├── dev.ssr.js
│   │   ├── dev.static.js
│   │   └── prod.ssr.js
│   └── server.js
├── generator
│   ├── index.js
│   └── template
│       ├── src
│       │   ├── App.vue
│       │   ├── components
│       │   │   └── HelloWorld.vue
│       │   ├── entry-client.js
│       │   ├── entry-server.js
│       │   ├── main.js
│       │   ├── router
│       │   │   ├── index.js
│       │   ├── store
│       │   │   ├── index.js
│       │   │   └── modules
│       │   │       └── book.js
│       │   └── views
│       │       ├── About.vue
│       │       └── Home.vue
│       └── vue.config.js
├── index.js
└── package.json
复制代码

至此,我们的 vue-cli-plugin-my_ssr_plugin_demo 插件就基本完成了

使用创建好的插件来初始化 ssr 工程

我们使用脚手架创建一个新的 spa 工程

vue create myproject
复制代码

然后在工程内部安装插件

vue add vue-cli-plugin-my_ssr_plugin_demo
复制代码

安装完毕后,我们就完成了 ssr 工程的初始化

在下一篇文章中,我们重点来讲如何基于我们的 vue-cli-plugin-my_ssr_plugin_demo 插件,集成日志系统

水滴前端团队招募伙伴,欢迎投递简历到邮箱:fed@shuidihuzhu.com

【第二篇】创建 @vue/cli3 插件,并整合 ssr 功能

以上所述就是小编给大家介绍的《【第二篇】创建 @vue/cli3 插件,并整合 ssr 功能》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Pro JavaScript Techniques

Pro JavaScript Techniques

John Resig / Apress / 2006-12-13 / USD 44.99

Pro JavaScript Techniques is the ultimate JavaScript book for the modern web developer. It provides everything you need to know about modern JavaScript, and shows what JavaScript can do for your web s......一起来看看 《Pro JavaScript Techniques》 这本书的介绍吧!

SHA 加密
SHA 加密

SHA 加密工具

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具