提交 f2c7ff13 编写于 作者: dsyuan001's avatar dsyuan001

Merge branch 'dev' of https://gitee.com/q337547038/vite-plugin-seo-prerender into main

# Conflicts:
#	pnpm-lock.yaml
#	vite.config.ts
# vite-plugin-seo-prerender # 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 @@ ...@@ -8,7 +9,7 @@
+ SSG (静态站点生成):支持根据路由配置生成静态 HTML 文件。 + SSG (静态站点生成):支持根据路由配置生成静态 HTML 文件。
+ 异步数据获取:支持在构建时获取异步数据并注入到预渲染的 HTML 文件中。 + 异步数据获取:支持在构建时获取异步数据并注入到预渲染的 HTML 文件中。
+ SEO 友好:生成的静态 HTML 文件对搜索引擎友好,可以更好地被爬虫索引。 + SEO 友好:生成的静态 HTML 文件对搜索引擎友好,可以更好地被爬虫索引。
+ 支持纯静态:public 目录下的 .html 支持动态引入样式及公共部分。
## 安装使用 ## 安装使用
...@@ -24,31 +25,54 @@ yarn install vite-plugin-seo-prerender -D ...@@ -24,31 +25,54 @@ yarn install vite-plugin-seo-prerender -D
```ts ```ts
// vite.config.ts // vite.config.ts
import { defineConfig } from 'vite' import {defineConfig} from 'vite'
import seoPrerender from 'vite-plugin-seo-prerender' import seoPrerender from 'vite-plugin-seo-prerender'
export default defineConfig({ export default defineConfig({
plugins: [ plugins: [
seoPrerender({ seoPrerender({
puppeteer: {}, // puppeteer参数配置,可选 routes: [] // 需要生成的路由
routes: [], // 需要生成的路由,必填
removeStyle:true, // 是否移除多余样式,默认true。在启动服务vite preview时会产生一些多余样式,如若丢失样式可设置为false
callback:(content,route)=>{
// 可对当前页面html内容进行一些替换等处理
// 一些处理逻辑...
return content
}
}) })
] ]
}) })
``` ```
## 纯静态开发
*使用预渲染生成的html页面有一个弊端,如预渲染生成页面 `/about/index.html`,它并不能通过 `http://xxx.com/about/index.html`
这样的形式正常访问,即使能正常展示也会丢失脚本事件*
对于部分特殊需求需要纯静态页面时,插件同样支持在编写 `public` 目录下的 `.html`
文件时,同样支持热更新及引入项目由 `scss、less`等编写的公共样式。并可使用指定标签替换页面内容,如公共头尾部等。
```html
<!--/public/about/index.html-->
<body>
<div><!--link href="/src/assets/header.html"--></div>
<div>这里的路径需要使用相对于根目录的绝对路径,不能使用相对路径,如 ./assets/header.html</div>
<div>this page content</div>
</body>
```
在浏览器输入如 `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 ```ts
// router.ts // router.ts
......
...@@ -10,6 +10,7 @@ ...@@ -10,6 +10,7 @@
"publish": "pnpm --filter ./packages tsup" "publish": "pnpm --filter ./packages tsup"
}, },
"dependencies": { "dependencies": {
"vite-plugin-seo-prerender": "^0.1.0",
"vue": "^3.3.4", "vue": "^3.3.4",
"vue-router": "^4.2.2" "vue-router": "^4.2.2"
}, },
...@@ -18,11 +19,9 @@ ...@@ -18,11 +19,9 @@
"@vitejs/plugin-vue": "^4.2.3", "@vitejs/plugin-vue": "^4.2.3",
"puppeteer": "^20.7.4", "puppeteer": "^20.7.4",
"sass": "^1.63.6", "sass": "^1.63.6",
"tsup": "^7.0.0", "tsup": "^7.1.0",
"typescript": "^5.1.3", "typescript": "^5.1.3",
"vite": "^4.3.9", "vite": "^4.3.9",
"vite-plugin-html": "^3.2.0",
"vite-plugin-legacy": "^2.1.0",
"vite-plugin-sprites": "^0.2.0", "vite-plugin-sprites": "^0.2.0",
"vue-tsc": "^1.8.0" "vue-tsc": "^1.8.0"
} }
......
# vite-plugin-seo-prerender # 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 @@ ...@@ -8,7 +9,7 @@
+ SSG (静态站点生成):支持根据路由配置生成静态 HTML 文件。 + SSG (静态站点生成):支持根据路由配置生成静态 HTML 文件。
+ 异步数据获取:支持在构建时获取异步数据并注入到预渲染的 HTML 文件中。 + 异步数据获取:支持在构建时获取异步数据并注入到预渲染的 HTML 文件中。
+ SEO 友好:生成的静态 HTML 文件对搜索引擎友好,可以更好地被爬虫索引。 + SEO 友好:生成的静态 HTML 文件对搜索引擎友好,可以更好地被爬虫索引。
+ 支持纯静态:public 目录下的 .html 支持动态引入样式及公共部分。
## 安装使用 ## 安装使用
...@@ -24,31 +25,54 @@ yarn install vite-plugin-seo-prerender -D ...@@ -24,31 +25,54 @@ yarn install vite-plugin-seo-prerender -D
```ts ```ts
// vite.config.ts // vite.config.ts
import { defineConfig } from 'vite' import {defineConfig} from 'vite'
import seoPrerender from 'vite-plugin-seo-prerender' import seoPrerender from 'vite-plugin-seo-prerender'
export default defineConfig({ export default defineConfig({
plugins: [ plugins: [
seoPrerender({ seoPrerender({
puppeteer: {}, // puppeteer参数配置,可选 routes: [] // 需要生成的路由
routes: [], // 需要生成的路由,必填
removeStyle:true, // 是否移除多余样式,默认true。在启动服务vite preview时会产生一些多余样式,如若丢失样式可设置为false
callback:(content,route)=>{
// 可对当前页面html内容进行一些替换等处理
// 一些处理逻辑...
return content
}
}) })
] ]
}) })
``` ```
## 纯静态开发
*使用预渲染生成的html页面有一个弊端,如预渲染生成页面 `/about/index.html`,它并不能通过 `http://xxx.com/about/index.html`
这样的形式正常访问,即使能正常展示也会丢失脚本事件*
对于部分特殊需求需要纯静态页面时,插件同样支持在编写 `public` 目录下的 `.html`
文件时,同样支持热更新及引入项目由 `scss、less`等编写的公共样式。并可使用指定标签替换页面内容,如公共头尾部等。
```html
<!--/public/about/index.html-->
<body>
<div><!--link href="/src/assets/header.html"--></div>
<div>这里的路径需要使用相对于根目录的绝对路径,不能使用相对路径,如 ./assets/header.html</div>
<div>this page content</div>
</body>
```
在浏览器输入如 `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 ```ts
// router.ts // router.ts
......
{ {
"name": "vite-plugin-seo-prerender", "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`等所有框架", "description": "`vite-plugin-seo-prerender` 插件是一个用于 `Vite` 构建工具的预渲染插件,它可以将你的单页面应用 (SPA) 在构建时静态预渲染为 HTML 文件,以提高首次加载速度和SEO友好性。适用于对站点少量页面生成静态HTML。支持 `Vue、React`等所有框架",
"license": "MIT", "license": "MIT",
"author": "337547038", "author": "337547038",
...@@ -46,7 +46,8 @@ ...@@ -46,7 +46,8 @@
"tsup": "tsup" "tsup": "tsup"
}, },
"dependencies": { "dependencies": {
"puppeteer": "^20.7.3" "puppeteer": "^20.7.3",
"sass": "^1.63.6"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^20.2.5", "@types/node": "^20.2.5",
......
import childProcess from 'child_process' import childProcess from 'child_process'
import path from 'path' 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 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 interface Scss {
const prerender = (config: Config) => { 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 = { const cfgConfig = {
outDir: '', outDir: '',
mode: '', mode: '',
...@@ -26,101 +63,78 @@ const prerender = (config: Config) => { ...@@ -26,101 +63,78 @@ const prerender = (config: Config) => {
cfgConfig.root = cfg.root cfgConfig.root = cfg.root
cfgConfig.base = cfg.base cfgConfig.base = cfg.base
}, },
async buildStart() { buildStart() {
if (config?.scss?.length) {
}, config.scss.forEach((item: Scss) => {
buildEnd() { transformSass(cfgConfig.root, item)
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)
})*/
} }
}, },
configureServer(server) { configureServer(server) {
if (config.html?.routes?.length) { const {allUrl, isAllUrl} = getPublicHtml(config?.publicHtml)
server.middlewares.use((req, res, next) => { if (allUrl.length || isAllUrl) {
// console.log(server.moduleGraph) server.middlewares.use(async (req, res, next) => {
const baseUrl = req.url.replace(cfgConfig.base, '/') const baseUrl = req.url.replace(cfgConfig.base, '/')
console.log('base',baseUrl) if ((isAllUrl && baseUrl.endsWith('.html')) || allUrl.includes(baseUrl)) {
if (config.html.routes.includes(baseUrl)) { const htmlContent: string = await publicHtml({
console.log(req.url) root: cfgConfig.root,
const module = server.moduleGraph.getModuleByUrl(req.url) filePath: baseUrl,
.then(res => { mode: 'server',
console.log(res, 'okk') callback: config.callback
}) })
if (htmlContent) {
const htmlContent = module ? module.content : ''; res.setHeader('Content-Type', 'text/html')
res.setHeader('Content-Type', 'text/html') res.end(htmlContent)
res.end('12'); return
return; }
} }
next() next()
}) })
} }
// console.log('configureServer') },
//const {watcher} = server handleHotUpdate({file, server}) {
/*if (config.htmlRoutes?.length) { // 更新时刷新当前页面
watcher.on('change', async (filePath) => { if (file.endsWith('.html')) {
const relativePath = path.relative(server.config.root, filePath).replace('public', '').replace(/\\/g, '/') const {allUrl, isAllUrl} = getPublicHtml(config?.publicHtml)
if (config.htmlRoutes.includes(relativePath)) { if (isAllUrl || allUrl.length) {
// 监听 public 目录下的指定 HTML 文件更改 const publicPath = path.join(cfgConfig.root, 'public')
let hostPort = '' // 获取启用的服务ip地址端口 const dirPath = path.relative(publicPath, file)
const resolvedUrls = server.resolvedUrls server.ws.send({
for (const key in resolvedUrls) { type: 'full-reload',
if (resolvedUrls[key].length) { path: '/' + getTransform(dirPath)
hostPort = resolvedUrls[key][0] })
} }
} }
await publicHtml(Object.assign(config, if (config?.scss?.length && file.endsWith('.scss')) {
{hostPort: hostPort, filePath: filePath}), 'dev') 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 构建生产环境时才执行 // vite build 构建生产环境时才执行
if (cfgConfig.mode !== 'production') { if (cfgConfig.mode !== 'production') {
return 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) => { const cProcess = childProcess.exec('vite preview', (err) => {
if (err) { if (err) {
console.error('执行命令时发生错误:', err); console.error('执行命令时发生错误:', err);
...@@ -135,7 +149,7 @@ const prerender = (config: Config) => { ...@@ -135,7 +149,7 @@ const prerender = (config: Config) => {
localUrl = local[0].replace(/\x1B\[\d+m/g, '').slice(0, -1) // 控制台输出的有些会经过转义 localUrl = local[0].replace(/\x1B\[\d+m/g, '').slice(0, -1) // 控制台输出的有些会经过转义
console.log('Local: ' + localUrl) console.log('Local: ' + localUrl)
cfgConfig.local = localUrl cfgConfig.local = localUrl
await seoPrerender(Object.assign(config, cfgConfig)) await prerender(Object.assign(config, cfgConfig))
// 在某个条件满足时,关闭进程退出 // 在某个条件满足时,关闭进程退出
cProcess.kill('SIGTERM') cProcess.kill('SIGTERM')
process.exit() // 关闭当前进程并退出 process.exit() // 关闭当前进程并退出
...@@ -145,6 +159,7 @@ const prerender = (config: Config) => { ...@@ -145,6 +159,7 @@ const prerender = (config: Config) => {
} }
} }
} }
export default prerender
export default seoPrerender
...@@ -2,35 +2,149 @@ ...@@ -2,35 +2,149 @@
处理public静态文件,两个功能 处理public静态文件,两个功能
1.将页面的公共样式及脚本动态插入到静态页,实现样式共用; 1.将页面的公共样式及脚本动态插入到静态页,实现样式共用;
2.静态html也可以使用公共如头尾部*/ 2.静态html也可以使用公共如头尾部*/
import puppeteer from 'puppeteer' import fs from 'fs'
import {Config} from "./types" import path from 'path'
import {getTransform} from './utils'
interface publicConfig extends Config { interface PublicConfig {
hostPort: string root: string
filePath: 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 getEntry = (root: string, mode: string, outDir?: string) => {
const browser = await puppeteer.launch(Object.assign({headless: 'new'}, config.puppeteer || {})); // 从入口html页面获取
const page = await browser.newPage() const indexContent: string = fs.readFileSync(path.join(root, 'index.html'), 'utf-8')
await page.goto(config.hostPort) if (mode === 'server') {
await page.setViewport({width: 1024, height: 768}) // 返回入口script即可 如/src/main.ts
const htmlContent = await page.content() const scriptMain = /<script[^>]*?\b\/main\b[^>]*?>.*?<\/script>/gi
//提取link const scripts = indexContent.match(scriptMain) || []
const styleRegex = /<style\b[^>]*>([\s\S]*?)<\/style>/gi return scripts.join('\n')
const matches = htmlContent.matchAll(styleRegex) } else {
console.log('style',matches) // 提取动态插入index.html的link
for (const match of matches) { const linkPattern = /<link[^>]*?rel=['|"]stylesheet['|"][^>]*?\.css[^>]*?>/gi
const styleContent = match[1] //const linkPattern = /(?<=<link.*?href=["'])(.*?\.css)(?=["'].*?)/g
// 处理style标签中的内容 const links = indexContent.match(linkPattern)
console.log(styleContent); // 编译后的index.html
const newIndex: string = fs.readFileSync(path.join(root, outDir, 'index.html'), 'utf-8')
const newLinks = newIndex.match(linkPattern) || []
let resultLink: string[] = newLinks
if (links) {
resultLink = newLinks.filter((item: string) => !links.includes(item))
}
return resultLink.join('\n').replace('"./', '"/') // 将可能存在的href="./x"转为href="/"
} }
}
/**
* 替换页面指定标签的内容
* @param html
* @param root
*/
const getReplaceComm = (html: string, root: string) => {
return html.replace(/<!--link\shref="(.*)"-->/gi, function (matches: string, m1: string) {
// m1就是匹配的路径地址了
// 读取m1文件内容
const dirPath = path.join(root, m1)
//console.log('dirPath',dirPath)
if (fs.existsSync(dirPath)) {
return fs.readFileSync(dirPath, {
encoding: 'utf8'
})
} else {
// 文件不存在时
return matches
}
})
} }
const publicHtml = async (config: publicConfig, mode?: string) => { /**
const styleScript = await getPublicIndex(config) * 根据路径替换需要处理的html文件
* @param root 项目根目录 绝对路径
* @param dirPath 文件路径位置 绝对路径
* @param callback 回调处理事件
*/
const readWriteFile = (root: string, dirPath: string, callback: any) => {
const content: string = fs.readFileSync(dirPath, 'utf-8')
// 插入生成的css link
let replaceContent: string = content.replace('</head>', `${scriptLink}\n</head>`)
// 替换指定标签的内容
replaceContent = getReplaceComm(replaceContent, root)
if (typeof callback === 'function') {
replaceContent = callback(replaceContent, dirPath) || replaceContent
}
fs.writeFileSync(dirPath, replaceContent)
console.log('[vite-plugin-seo-prerender:publicHtml] ' + getTransform(path.relative(root, dirPath)))
}
/**
* 查找发布目录下所有.html文件
* @param dirPath
* @param indexPath 首页的路径 如dist/index.html
* @param root
* @param callback 回调处理事件
*/
const findHTMLFiles = (dirPath: string, indexPath: string, root: string, callback: any) => {
if (!fs.existsSync(dirPath)) {
console.log(`${dirPath}路径不存在`)
return
}
const paths: string[] = fs.readdirSync(dirPath)
paths.forEach((item: string) => {
// 如果是文件,则判断文件扩展名是否为html,排除根目录下的首页
const itemPath: string = path.join(dirPath, item)
if (fs.statSync(itemPath).isFile() && path.extname(itemPath) === '.html' && !getTransform(itemPath).endsWith(indexPath)) {
readWriteFile(root, itemPath, callback)
}
// 如果是目录,则递归地调用函数查找子目录中的HTML文件
if (fs.statSync(itemPath).isDirectory()) {
findHTMLFiles(itemPath, indexPath, root, callback)
}
})
}
/**
* 处理public目录下的html文件
* @param config
*/
const publicHtml = async (config: PublicConfig) => {
const {mode, root, filePath, outDir} = config
if (!scriptLink) { // 减少下每次读取index.html
scriptLink = getEntry(root, mode, outDir)
}
if (mode === 'server') {
const htmlFilePath: string = path.join(root, 'public', filePath as string)
let htmlContent: string = fs.readFileSync(htmlFilePath, 'utf-8')
//将script插入到body
htmlContent = htmlContent.replace('</body>', `${scriptLink}\n</body>`)
//替换公共部分
htmlContent = getReplaceComm(htmlContent, root)
if (typeof config.callback === 'function') {
htmlContent = config.callback(htmlContent, filePath) || htmlContent
}
return htmlContent
} else {
// 生产模式
if (typeof filePath === 'boolean') {
// 目录下的所有html文件
const dirPath: string = path.join(root, outDir)
const indexPath: string = getTransform(path.join(outDir, 'index.html'))
findHTMLFiles(dirPath, indexPath, root, config.callback)
} else {
// 指定目录
for (const key in filePath as string[]) {
const dirPath: string = path.join(root, outDir, filePath[key])
readWriteFile(root, dirPath, config.callback)
}
}
}
} }
export default publicHtml export default publicHtml
import puppeteer from 'puppeteer' import puppeteer from 'puppeteer'
import fs from 'fs' import fs from 'fs'
import path from 'path' import path from 'path'
import {recursiveMkdir} from './utils'
// 递归创建目录
function recursiveMkdir(dirPath) {
const parentDir = path.dirname(dirPath); // 获取父级目录路径
if (!fs.existsSync(parentDir)) {
recursiveMkdir(parentDir); // 递归创建父级目录
}
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath); // 创建当前目录
}
}
const seoPrerender = async (config) => { const seoPrerender = async (config) => {
const browser = await puppeteer.launch(Object.assign({headless: 'new'}, config.puppeteer || {})); const browser = await puppeteer.launch(Object.assign({headless: 'new'}, config.puppeteer || {}));
const page = await browser.newPage() const page = await browser.newPage()
const logTip = '[vite-plugin-seo-prerender:routes]'
for (const item of config.routes) { for (const item of config.routes) {
await page.goto(config.local + item) await page.goto(path.join(config.local, item))
await page.setViewport({width: 1024, height: 768}) await page.setViewport({width: 1024, height: 768})
let content = await page.content() let content: string = await page.content()
if (content.removeStyle !== false) { if (config.removeStyle !== false) {
// 若出现导常,可设置参数removeStyle:false // 若出现导常,可设置参数removeStyle:false
content = content.replace(/<style\b[^<]*(?:(?!<\/style>)<[^<]*)*<\/style>/gi, ""); content = content.replace(/<style\b[^<]*(?:(?!<\/style>)<[^<]*)*<\/style>/gi, "");
} }
// 防止当设置了base:./形式时,会使用http的形式加载样式脚本资源,这里转为根路径
// 这里其实还存在问题,当直接访问xx/index.html 插入的公共资源也为./这样的形式,是加载不到的
const regLocal = new RegExp(config.local, 'g')
content = content.replace(regLocal, '')
if (config.callback) { if (config.callback) {
content = config.callback(content, item) content = config.callback(content, item) || content
} }
if (item.indexOf('?') !== -1) { if (item.indexOf('?') !== -1) {
// 填写的路由地址带有意外参数时不处理 // 填写的路由地址带有意外参数时不处理
console.log(`[vite-plugin-seo-prerender] ${item} is error,unexpected?`) console.log(`${logTip} ${item} is error,unexpected?`)
} else { } else {
const fullPath = path.join(config.outDir, item) const fullPath = path.join(config.outDir, item)
recursiveMkdir(fullPath) recursiveMkdir(fullPath)
const filePath = path.join(fullPath, 'index.html') const filePath = path.join(fullPath, 'index.html')
fs.writeFileSync(filePath, content) fs.writeFileSync(filePath, content)
console.log(`[vite-plugin-seo-prerender] ${filePath} is success!`) console.log(`${logTip} ${filePath.replace(/\\/g, '/')} is success!`)
} }
} }
await browser.close(); await browser.close();
console.log('[vite-plugin-seo-prerender] is complete') console.log(`${logTip} is complete`)
} }
export default seoPrerender export default seoPrerender
export interface Config {
puppeteer?: any // puppeteer一些配置
routes?: string[] // 需要生成的路由地址
removeStyle?: boolean // 启用vite preview会自带有些样式,默认下移除
callback?: Function
html:{ // 处理public目录下的html文件
routes?: string[]
}
}
import fs from 'fs'
import path from 'path'
/**
* 将\转为/
* @param string
*/
export const getTransform = (string: string) => {
if (!string) {
return string
}
return string.replace(/\\/g, '/')
}
/**
* 递归创建目录
* @param dirPath
*/
export const recursiveMkdir = (dirPath:string) => {
const parentDir:string = path.dirname(dirPath) // 获取父级目录路径
if (!fs.existsSync(parentDir)) {
recursiveMkdir(parentDir) // 递归创建父级目录
}
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath) // 创建当前目录
}
}
此差异已折叠。
...@@ -3,14 +3,10 @@ ...@@ -3,14 +3,10 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Title</title> <title>Title</title>
<link rel="stylesheet" href="/style/test.css">
</head> </head>
<body> <body>
this is contact page ,transform 123? <!--link href="/src/assets/head.html"-->
<p>1.2.3.4.5.6.7.8.9.10.11.12.13.15.16.17.18 <div>this is contact page</div>
19.20.21.22.23.24.25.26.27.28.19.20.21.22.23.24.25.26.27.28
.29.30.31.32.33.34.35.36.37.38.39.40.41.42.43.44.45.46.47.48.49.50
51.52.53.54.55.56
</p>
</body> </body>
</html> </html>
body {
font-size: 14px;
}
\ No newline at end of file
<div>this is header</div>
<div>path: /src/assets/head.html</div>
body{margin: 0;padding: 0;font-size: 15px} body{margin: 0;padding: 0;font-size: 16px}
body{font-size: 14px}
<template> <template>
<div class="about">this about page</div> <div class="about">this about page, count: {{count}}</div>
<div><button @click="count++">button</button></div>
</template> </template>
<script setup lang="ts">
import {ref} from 'vue'
const count =ref(0)
</script>
<style> <style>
.about{color: red;font-size: 16px;} .about{color: red;font-size: 16px;}
</style> </style>
...@@ -7,9 +7,13 @@ import seoPrerender from 'vite-plugin-seo-prerender' ...@@ -7,9 +7,13 @@ import seoPrerender from 'vite-plugin-seo-prerender'
export default defineConfig({ export default defineConfig({
plugins: [ plugins: [
vue(), vue(),
// @ts-ignore
seoPrerender({ seoPrerender({
routes: ['/about'], routes: ['/about'],
publishHtml: ['/contact/index.html'] publicHtml: true,
scss: [
{entry: '/src/assets/test.scss', outDir: '/public/style/test.css'}
]
}) })
] ]
}) })
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册