diff --git a/README.md b/README.md
index 7200a5c30b29f46acecae5a4284083b4ec936cb3..5f9ff3810090d20df855e7750f3c72141c8b2305 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,7 @@
# vite-plugin-seo-prerender
-`vite-plugin-seo-prerender` 插件是一个用于 `Vite` 构建工具的预渲染插件,它可以将你的单页面应用 (SPA) 在构建时静态预渲染为 HTML 文件,以提高首次加载速度和SEO友好性。适用于对站点少量页面生成静态HTML。支持 `Vue、React`等所有框架
+`vite-plugin-seo-prerender` 插件是一个用于 `Vite` 构建工具的预渲染插件,它可以将你的单页面应用 (SPA) 在构建时静态预渲染为
+HTML 文件,以提高首次加载速度和SEO友好性。适用于对站点少量页面生成静态HTML。支持 `Vue、React`等所有框架
***
@@ -8,7 +9,7 @@
+ SSG (静态站点生成):支持根据路由配置生成静态 HTML 文件。
+ 异步数据获取:支持在构建时获取异步数据并注入到预渲染的 HTML 文件中。
+ SEO 友好:生成的静态 HTML 文件对搜索引擎友好,可以更好地被爬虫索引。
-
++ 支持纯静态:public 目录下的 .html 支持动态引入样式及公共部分。
## 安装使用
@@ -24,31 +25,54 @@ yarn install vite-plugin-seo-prerender -D
```ts
// vite.config.ts
-import { defineConfig } from 'vite'
+import {defineConfig} from 'vite'
import seoPrerender from 'vite-plugin-seo-prerender'
export default defineConfig({
plugins: [
seoPrerender({
- puppeteer: {}, // puppeteer参数配置,可选
- routes: [], // 需要生成的路由,必填
- removeStyle:true, // 是否移除多余样式,默认true。在启动服务vite preview时会产生一些多余样式,如若丢失样式可设置为false
- callback:(content,route)=>{
- // 可对当前页面html内容进行一些替换等处理
- // 一些处理逻辑...
- return content
- }
+ routes: [] // 需要生成的路由
})
]
})
```
+## 纯静态开发
+
+*使用预渲染生成的html页面有一个弊端,如预渲染生成页面 `/about/index.html`,它并不能通过 `http://xxx.com/about/index.html`
+这样的形式正常访问,即使能正常展示也会丢失脚本事件*
+
+对于部分特殊需求需要纯静态页面时,插件同样支持在编写 `public` 目录下的 `.html`
+文件时,同样支持热更新及引入项目由 `scss、less`等编写的公共样式。并可使用指定标签替换页面内容,如公共头尾部等。
+
+```html
+
+
+
+这里的路径需要使用相对于根目录的绝对路径,不能使用相对路径,如 ./assets/header.html
+this page content
+
+```
+
+在浏览器输入如 `http://localhost/contact/index.html` 即可看到被替换后的页面,当修改`scss/less`文件或当前
+html页时,可实现热更新。
## 发布
-运行 `vite build` 构建命令时即可生成HTML 文件
+运行 `vite build` 构建命令时即可生成 HTML 文件
+
+## API
+
+| 参数 | 类型 | 说明 |
+|-------------|---------------------|-------------------------------------------------------------------------|
+| puppeteer | object | puppeteer一些配置 |
+| routes | string[] | 生成预渲染的路由path |
+| removeStyle | boolean | 移除预览服务生成多余样式,默认true。如样式丢失,可设置为false |
+| callback | funtion(html,route) | 预渲染和处理public下.html文件处理回调事件,可对需处理的页面进行修改,html为将要生成的文件内容,route当前处理的页面path |
+| publicHtml | boolean/string[] | 需要处理的纯静态文件。true代表public整个目录下的html文件,数组时可指定文件,如['/contact/index.html'] |
+| scss | [{entry,outDir}] | 需要编辑的单独scss文件。专为单独纯html定制,可将独立(即没有在项目里引入)的scss转换为css |
-## 附:seo关键词优化设置
+## 附:seo关键词优化路由设置
```ts
// router.ts
diff --git a/package.json b/package.json
index 29c43b131a3bef77c0a50e23578c57f18a0b6dab..68aa6627c78e5a68b7014b9717f9b19cfee2e9d0 100644
--- a/package.json
+++ b/package.json
@@ -10,6 +10,7 @@
"publish": "pnpm --filter ./packages tsup"
},
"dependencies": {
+ "vite-plugin-seo-prerender": "^0.1.0",
"vue": "^3.3.4",
"vue-router": "^4.2.2"
},
@@ -18,11 +19,9 @@
"@vitejs/plugin-vue": "^4.2.3",
"puppeteer": "^20.7.4",
"sass": "^1.63.6",
- "tsup": "^7.0.0",
+ "tsup": "^7.1.0",
"typescript": "^5.1.3",
"vite": "^4.3.9",
- "vite-plugin-html": "^3.2.0",
- "vite-plugin-legacy": "^2.1.0",
"vite-plugin-sprites": "^0.2.0",
"vue-tsc": "^1.8.0"
}
diff --git a/packages/README.md b/packages/README.md
index 01d5c9302ab4fbb21544ad9c75597b7da935d920..5f9ff3810090d20df855e7750f3c72141c8b2305 100644
--- a/packages/README.md
+++ b/packages/README.md
@@ -1,6 +1,7 @@
# vite-plugin-seo-prerender
-`vite-plugin-seo-prerender` 插件是一个用于 `Vite` 构建工具的预渲染插件,它可以将你的单页面应用 (SPA) 在构建时静态预渲染为 HTML 文件,以提高首次加载速度和SEO友好性。适用于对站点少量页面生成静态HTML。支持 `Vue、React`等所有框架
+`vite-plugin-seo-prerender` 插件是一个用于 `Vite` 构建工具的预渲染插件,它可以将你的单页面应用 (SPA) 在构建时静态预渲染为
+HTML 文件,以提高首次加载速度和SEO友好性。适用于对站点少量页面生成静态HTML。支持 `Vue、React`等所有框架
***
@@ -8,7 +9,7 @@
+ SSG (静态站点生成):支持根据路由配置生成静态 HTML 文件。
+ 异步数据获取:支持在构建时获取异步数据并注入到预渲染的 HTML 文件中。
+ SEO 友好:生成的静态 HTML 文件对搜索引擎友好,可以更好地被爬虫索引。
-
++ 支持纯静态:public 目录下的 .html 支持动态引入样式及公共部分。
## 安装使用
@@ -24,31 +25,54 @@ yarn install vite-plugin-seo-prerender -D
```ts
// vite.config.ts
-import { defineConfig } from 'vite'
+import {defineConfig} from 'vite'
import seoPrerender from 'vite-plugin-seo-prerender'
export default defineConfig({
plugins: [
seoPrerender({
- puppeteer: {}, // puppeteer参数配置,可选
- routes: [], // 需要生成的路由,必填
- removeStyle:true, // 是否移除多余样式,默认true。在启动服务vite preview时会产生一些多余样式,如若丢失样式可设置为false
- callback:(content,route)=>{
- // 可对当前页面html内容进行一些替换等处理
- // 一些处理逻辑...
- return content
- }
+ routes: [] // 需要生成的路由
})
]
})
```
+## 纯静态开发
+
+*使用预渲染生成的html页面有一个弊端,如预渲染生成页面 `/about/index.html`,它并不能通过 `http://xxx.com/about/index.html`
+这样的形式正常访问,即使能正常展示也会丢失脚本事件*
+
+对于部分特殊需求需要纯静态页面时,插件同样支持在编写 `public` 目录下的 `.html`
+文件时,同样支持热更新及引入项目由 `scss、less`等编写的公共样式。并可使用指定标签替换页面内容,如公共头尾部等。
+
+```html
+
+
+
+这里的路径需要使用相对于根目录的绝对路径,不能使用相对路径,如 ./assets/header.html
+this page content
+
+```
+
+在浏览器输入如 `http://localhost/contact/index.html` 即可看到被替换后的页面,当修改`scss/less`文件或当前
+html页时,可实现热更新。
## 发布
-运行 `vite build` 构建命令时即可生成HTML 文件
+运行 `vite build` 构建命令时即可生成 HTML 文件
+
+## API
+
+| 参数 | 类型 | 说明 |
+|-------------|---------------------|-------------------------------------------------------------------------|
+| puppeteer | object | puppeteer一些配置 |
+| routes | string[] | 生成预渲染的路由path |
+| removeStyle | boolean | 移除预览服务生成多余样式,默认true。如样式丢失,可设置为false |
+| callback | funtion(html,route) | 预渲染和处理public下.html文件处理回调事件,可对需处理的页面进行修改,html为将要生成的文件内容,route当前处理的页面path |
+| publicHtml | boolean/string[] | 需要处理的纯静态文件。true代表public整个目录下的html文件,数组时可指定文件,如['/contact/index.html'] |
+| scss | [{entry,outDir}] | 需要编辑的单独scss文件。专为单独纯html定制,可将独立(即没有在项目里引入)的scss转换为css |
-## 附seo关键词优化
+## 附:seo关键词优化路由设置
```ts
// router.ts
diff --git a/packages/package.json b/packages/package.json
index 2d6ad7ef85c92b4eb340a93395d5b3711d4a493c..a75d4e1995de78fd504a4a6fdf2016b8fd3e01a9 100644
--- a/packages/package.json
+++ b/packages/package.json
@@ -1,6 +1,6 @@
{
"name": "vite-plugin-seo-prerender",
- "version": "0.0.1",
+ "version": "0.1.0",
"description": "`vite-plugin-seo-prerender` 插件是一个用于 `Vite` 构建工具的预渲染插件,它可以将你的单页面应用 (SPA) 在构建时静态预渲染为 HTML 文件,以提高首次加载速度和SEO友好性。适用于对站点少量页面生成静态HTML。支持 `Vue、React`等所有框架",
"license": "MIT",
"author": "337547038",
@@ -46,7 +46,8 @@
"tsup": "tsup"
},
"dependencies": {
- "puppeteer": "^20.7.3"
+ "puppeteer": "^20.7.3",
+ "sass": "^1.63.6"
},
"devDependencies": {
"@types/node": "^20.2.5",
diff --git a/packages/src/index.ts b/packages/src/index.ts
index ad89fbb8024572059dedd0fcf22e90a673b5978b..6d1595a3901f5a9176700969730d95d3e28a872a 100644
--- a/packages/src/index.ts
+++ b/packages/src/index.ts
@@ -1,15 +1,52 @@
import childProcess from 'child_process'
import path from 'path'
-import seoPrerender from './render'
-import publicHtml from "./public"
-import {Config} from "./types"
-import {createServer} from 'vite';
import fs from 'fs'
-import puppeteer from 'puppeteer'
+import * as sass from 'sass'
+import prerender from './render'
+// @ts-ignore
+import publicHtml from './public'
+import {getTransform, recursiveMkdir} from './utils'
-let pPage
-const prerender = (config: Config) => {
+interface Scss {
+ entry: string
+ outDir: string
+}
+
+export interface Config {
+ puppeteer?: any // puppeteer一些配置
+ routes?: string[] // 需要生成的路由地址
+ removeStyle?: boolean // 启用vite preview会自带有些样式,默认下移除
+ callback?: Function
+ publicHtml?: boolean | string[] // public目录html文件处理
+ scss?: Scss[]
+}
+
+const getPublicHtml = (publicHtml) => {
+ let allUrl: string[] = []
+ if (typeof publicHtml === 'object') {
+ // 处理指定的
+ allUrl = publicHtml || []
+ }
+ const isAllUrl: boolean = typeof publicHtml === 'boolean' && publicHtml
+ return {allUrl, isAllUrl}
+}
+
+/**
+ * 将scss转换为css
+ * @param root
+ * @param css
+ */
+const transformSass = (root: string, css: Scss) => {
+ const entryDir: string = path.join(root, css.entry)
+ const result = sass.compile(entryDir)
+ const outDir: string = path.join(root, css.outDir)
+ recursiveMkdir(path.dirname(outDir))
+ fs.writeFileSync(outDir, result.css)
+ console.log(`transform scss: ${css.entry} => ${css.outDir}`)
+}
+
+const seoPrerender = (config: Config) => {
const cfgConfig = {
outDir: '',
mode: '',
@@ -26,101 +63,78 @@ const prerender = (config: Config) => {
cfgConfig.root = cfg.root
cfgConfig.base = cfg.base
},
- async buildStart() {
-
- },
- buildEnd() {
- console.log('buildEnd,没看到有触发')
- },
- async load(id) {
- },
- transform(code, id) {
- /*if (id.endsWith('.html')) {
- console.log('transform:',id)
- }*/
- },
- /*transformIndexHtml(html, tag) {
- //console.log('transform',html)
- },*/
- transformIndexHtml: {
- async transform(html, ctx) {
- console.log('transform')
- //console.log('html',html)
- //console.log('ctx',ctx)
- //ctx.moduleGraph.transformIndexHtml(html=>{})
-
- }
- },
- async handleHotUpdate({file, server}) {
- if (file.endsWith('.html')) {
- /*console.log('file:',server)
- // 启动一个浏览器服务
- if (!pPage) {
- const browser = await puppeteer.launch(Object.assign({headless: 'new'}, config.puppeteer || {}));
- pPage = await browser.newPage()
- await pPage.goto('http://127.0.0.1:5173')
- await pPage.setViewport({width: 1024, height: 768})
- }
- pPage.content()
- .then(html => {
- console.log('page content', html)
- })
- .catch(res => {
- console.log('catch', res)
- })*/
+ buildStart() {
+ if (config?.scss?.length) {
+ config.scss.forEach((item: Scss) => {
+ transformSass(cfgConfig.root, item)
+ })
}
},
configureServer(server) {
- if (config.html?.routes?.length) {
- server.middlewares.use((req, res, next) => {
- // console.log(server.moduleGraph)
+ const {allUrl, isAllUrl} = getPublicHtml(config?.publicHtml)
+ if (allUrl.length || isAllUrl) {
+ server.middlewares.use(async (req, res, next) => {
const baseUrl = req.url.replace(cfgConfig.base, '/')
- console.log('base',baseUrl)
- if (config.html.routes.includes(baseUrl)) {
- console.log(req.url)
- const module = server.moduleGraph.getModuleByUrl(req.url)
- .then(res => {
- console.log(res, 'okk')
- })
-
- const htmlContent = module ? module.content : '';
- res.setHeader('Content-Type', 'text/html')
- res.end('12');
- return;
+ if ((isAllUrl && baseUrl.endsWith('.html')) || allUrl.includes(baseUrl)) {
+ const htmlContent: string = await publicHtml({
+ root: cfgConfig.root,
+ filePath: baseUrl,
+ mode: 'server',
+ callback: config.callback
+ })
+ if (htmlContent) {
+ res.setHeader('Content-Type', 'text/html')
+ res.end(htmlContent)
+ return
+ }
}
next()
})
}
- // console.log('configureServer')
- //const {watcher} = server
- /*if (config.htmlRoutes?.length) {
- watcher.on('change', async (filePath) => {
- const relativePath = path.relative(server.config.root, filePath).replace('public', '').replace(/\\/g, '/')
- if (config.htmlRoutes.includes(relativePath)) {
- // 监听 public 目录下的指定 HTML 文件更改
- let hostPort = '' // 获取启用的服务ip地址端口
- const resolvedUrls = server.resolvedUrls
- for (const key in resolvedUrls) {
- if (resolvedUrls[key].length) {
- hostPort = resolvedUrls[key][0]
- }
- }
- await publicHtml(Object.assign(config,
- {hostPort: hostPort, filePath: filePath}), 'dev')
+ },
+ handleHotUpdate({file, server}) {
+ // 更新时刷新当前页面
+ if (file.endsWith('.html')) {
+ const {allUrl, isAllUrl} = getPublicHtml(config?.publicHtml)
+ if (isAllUrl || allUrl.length) {
+ const publicPath = path.join(cfgConfig.root, 'public')
+ const dirPath = path.relative(publicPath, file)
+ server.ws.send({
+ type: 'full-reload',
+ path: '/' + getTransform(dirPath)
+ })
+ }
+ }
+ if (config?.scss?.length && file.endsWith('.scss')) {
+ const fileDir: string = getTransform(file)
+ config.scss.forEach((item: Scss) => {
+ if (fileDir.includes(item.entry)) {
+ transformSass(cfgConfig.root, item)
}
})
- }*/
- },
- closeBundle() {
- if (!config?.routes?.length) {
- console.log('路由地址为空,请配置需预渲染的routes')
- return
}
+ },
+ async closeBundle() {
// vite build 构建生产环境时才执行
if (cfgConfig.mode !== 'production') {
return
}
- console.log('[vite-plugin-seo-prerender] is start..')
+ // 处理public下的html
+ const {allUrl, isAllUrl} = getPublicHtml(config?.publicHtml)
+ if (isAllUrl || allUrl.length) {
+ await publicHtml({
+ root: cfgConfig.root,
+ filePath: isAllUrl || allUrl,
+ mode: 'build',
+ outDir: cfgConfig.outDir,
+ callback: config.callback
+ })
+ }
+ if (!config?.routes?.length) {
+ //console.log('路由地址为空,请配置需预渲染的routes')
+ return
+ }
+ console.log('[vite-plugin-seo-prerender:routes] is start..')
const cProcess = childProcess.exec('vite preview', (err) => {
if (err) {
console.error('执行命令时发生错误:', err);
@@ -135,7 +149,7 @@ const prerender = (config: Config) => {
localUrl = local[0].replace(/\x1B\[\d+m/g, '').slice(0, -1) // 控制台输出的有些会经过转义
console.log('Local: ' + localUrl)
cfgConfig.local = localUrl
- await seoPrerender(Object.assign(config, cfgConfig))
+ await prerender(Object.assign(config, cfgConfig))
// 在某个条件满足时,关闭进程退出
cProcess.kill('SIGTERM')
process.exit() // 关闭当前进程并退出
@@ -145,6 +159,7 @@ const prerender = (config: Config) => {
}
}
}
-export default prerender
+
+export default seoPrerender
diff --git a/packages/src/public.ts b/packages/src/public.ts
index 8956c3a9f229d23f6ffee9a08910ba2116afd202..15d53095b2f6fe19c786407e0e08eb79eeafe7b2 100644
--- a/packages/src/public.ts
+++ b/packages/src/public.ts
@@ -2,35 +2,149 @@
处理public静态文件,两个功能
1.将页面的公共样式及脚本动态插入到静态页,实现样式共用;
2.静态html也可以使用公共如头尾部*/
-import puppeteer from 'puppeteer'
-import {Config} from "./types"
+import fs from 'fs'
+import path from 'path'
+import {getTransform} from './utils'
-interface publicConfig extends Config {
- hostPort: string
- filePath: string
+interface PublicConfig {
+ root: string
+ filePath: string | string[] | boolean
+ mode: string
+ outDir?: string
+ callback?: Function
}
+let scriptLink
/**
- * 获取主入口index的style和script
+ * 提取index.html中的入口script和link
+ * @param root 项目根目录 绝对位置路径
+ * @param mode 模式 server/build
+ * @param outDir 打包输入目录
*/
-const getPublicIndex = async (config: publicConfig) => {
- const browser = await puppeteer.launch(Object.assign({headless: 'new'}, config.puppeteer || {}));
- const page = await browser.newPage()
- await page.goto(config.hostPort)
- await page.setViewport({width: 1024, height: 768})
- const htmlContent = await page.content()
- //提取link
- const styleRegex = /
diff --git a/vite.config.ts b/vite.config.ts
index 76daad756888deaa42f7568cc17e3e7f1fc77670..429a9f32b1b2c90dd098f65166b1c08082a13081 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -7,9 +7,13 @@ import seoPrerender from 'vite-plugin-seo-prerender'
export default defineConfig({
plugins: [
vue(),
+ // @ts-ignore
seoPrerender({
routes: ['/about'],
- publishHtml: ['/contact/index.html']
+ publicHtml: true,
+ scss: [
+ {entry: '/src/assets/test.scss', outDir: '/public/style/test.css'}
+ ]
})
]
})