在上一篇文章《基於 @vue/cli3 與 koa 建立 ssr 工程》中,咱們講解了如何基於 @vue/cli3
建立一個 ssr 工程。javascript
在本篇文章,咱們來建立一個 @vue/cli3
插件,並將第一篇文章中 ssr 工程的服務器端部分整合進插件中。css
首先,咱們來看一個 cli3
插件提供了那些功能:html
@vue/cli-service
提供統一的自定義命令,例如: vue-cli-service ssr:build
除了上述兩個功能外,咱們還但願在插件內部整合服務端邏輯,這樣對於多個接入插件的工程項目能實行統一的管理,方便後續統一增長日誌、監控等功能。前端
官方對於發佈一個 cli3
的插件作了以下限制vue
爲了讓一個 CLI 插件可以被其它開發者使用,你必須遵循 vue-cli-plugin-<name>
的命名約定將其發佈到 npm
上。插件遵循命名約定以後就能夠:java
@vue/cli-service
發現;vue add <name>
或 vue invoke <name>
安裝下來。所以,咱們新建並初始化一個工程 createPluginExample
,並將工程的 name
命名爲 vue-cli-plugin-my_ssr_plugin_demo
node
mkdir createPluginExample && cd createPluginExample && yarn init 複製代碼
package.json 的內容爲:webpack
{ "name": "vue-cli-plugin-my_ssr_plugin_demo", "version": "1.0.0", "main": "index.js", "license": "MIT" } 複製代碼
官方對於第一個插件功能,引入了一個叫作 Generator
的機制來實現web
Generator
有兩種展示形式:generator.js
或 generator/index.js
vue-router
generator.js
或 generator/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
作三件事情:
ssr
工程模式自定義工程的 package.json
的內容api.render('./template')
觸發 generator
使用 EJS
渲染 generator/template/
中的文件,並替換工程中根目錄下對應的文件關於第一件事情
首先咱們須要在建立工程項目後,自動建立好基於 ssr
的一些命令,好比服務器端構建 ssr:build
,開發環境啓動 ssr
服務 dev
其次,咱們還須要在建立工程項目後,自動安裝好 ssr
依賴的某些第三方工具,例如:concurrently
第二件事件比較簡單,咱們這裏直接按照官方文檔寫就能夠。
關於第三件事情:
spa
工程會在 src
下生成 router.js
、store.js
這些文件,而插件在安裝過程當中不會刪除掉這些文件,所以咱們須要在工程安裝完畢後,清理這些文件。ssr:build
命令就須要在產品環境下執行了,所以咱們須要將咱們的插件 vue-cli-plugin-my_ssr_plugin_demo
, 以及 @vue/cli-plugin-babel
、@vue/cli-service
, 由 devDependencies
中移動到 dependencies
中。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
文件中,咱們會作以下三件事情:
vue.config.js
中的配置整合進插件中,也就是 index.js
中提供的 api.chainWebpack
內部,這樣作的好處是安裝此插件的工程項目沒必要再關心 ssr
相關的 webpack
配置細節ssr
服務的命令: ssr:serve
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