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

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

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

在上一篇文章 《基于 @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 功能》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

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

Jakarta Struts Cookbook中文版

Jakarta Struts Cookbook中文版

斯格科 / 清华大学 / 2007-7 / 56.00元

Jakarta Struts Cookbook(中文版),ISBN:9787302155638,作者:(美)斯格科一起来看看 《Jakarta Struts Cookbook中文版》 这本书的介绍吧!

HTML 压缩/解压工具
HTML 压缩/解压工具

在线压缩/解压 HTML 代码

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具