@kne/fastify-puppeteer 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,27 @@
1
+
2
+ # fastify-puppeteer
3
+
4
+
5
+ ### 描述
6
+
7
+ 用于操作无头浏览器截图保存pdf等
8
+
9
+
10
+ ### 安装
11
+
12
+ ```shell
13
+ npm i --save @kne/fastify-puppeteer
14
+ ```
15
+
16
+ ### 示例
17
+
18
+ #### 示例代码
19
+
20
+
21
+
22
+ ### API
23
+
24
+ | 属性名 | 说明 | 类型 | 默认值 |
25
+ |-----|----|----|-----|
26
+ | | | | |
27
+
package/index.js ADDED
@@ -0,0 +1,41 @@
1
+ const fp = require('fastify-plugin');
2
+ const path = require('path');
3
+ const packageJson = require('./package.json');
4
+ const merge = require('lodash/merge');
5
+
6
+ module.exports = fp(
7
+ async (fastify, options) => {
8
+ options = merge(
9
+ {
10
+ root: path.resolve(process.cwd(), '.puppeteer_cache'),
11
+ prefix: `/api/v${packageJson.version.split('.')[0]}/puppeteer`,
12
+ maxCacheKeys: 1000,
13
+ maxTaskSize: 100,
14
+ puppeteerOptions: {},
15
+ authenticate: []
16
+ },
17
+ options
18
+ );
19
+
20
+ fastify.register(
21
+ require('@fastify/static'),
22
+ Object.assign({}, options.static, {
23
+ root: options.root,
24
+ index: false,
25
+ list: false
26
+ })
27
+ );
28
+
29
+ fastify.register(require('@kne/fastify-namespace'), {
30
+ name: 'puppeteer',
31
+ options,
32
+ modules: [
33
+ ['services', path.resolve(__dirname, './libs/services.js')],
34
+ ['controllers', path.resolve(__dirname, './libs/controllers.js')]
35
+ ]
36
+ });
37
+ },
38
+ {
39
+ name: 'fastify-puppeteer'
40
+ }
41
+ );
@@ -0,0 +1,219 @@
1
+ const fp = require('fastify-plugin');
2
+ module.exports = fp(async (fastify, options) => {
3
+ const { services } = fastify.puppeteer;
4
+ fastify.post(
5
+ `${options.prefix}/parseHtmlToPdf`,
6
+ {
7
+ onRequest: options.authenticate,
8
+ schema: {
9
+ description: '接口说明',
10
+ summary: 'html生成pdf文件流',
11
+ body: {
12
+ type: 'object',
13
+ required: ['content'],
14
+ properties: {
15
+ options: { type: 'object' },
16
+ content: { type: 'string' }
17
+ }
18
+ }
19
+ }
20
+ },
21
+ async (request, reply) => {
22
+ const filename = await services.parseHtmlToPdf({ html: request.body.content, options: request.body.options });
23
+ return reply.sendFile(filename);
24
+ }
25
+ );
26
+
27
+ fastify.post(
28
+ `${options.prefix}/parseHtmlToPdfBatch`,
29
+ {
30
+ onRequest: options.authenticate,
31
+ schema: {
32
+ description: '接口说明',
33
+ summary: '批量将html生成pdf的zip文件流',
34
+ body: {
35
+ type: 'object',
36
+ required: ['contentList'],
37
+ properties: {
38
+ options: { type: 'object' },
39
+ contentList: {
40
+ type: 'array',
41
+ items: {
42
+ type: 'string'
43
+ }
44
+ }
45
+ }
46
+ }
47
+ }
48
+ },
49
+ async (request, reply) => {
50
+ const filename = await services.parseHtmlToPdfBatch({
51
+ htmlList: request.body.contentList,
52
+ options: request.body.options
53
+ });
54
+ return reply.sendFile(filename);
55
+ }
56
+ );
57
+
58
+ fastify.post(
59
+ `${options.prefix}/parseHtmlToPhotoBatch`,
60
+ {
61
+ onRequest: options.authenticate,
62
+ schema: {
63
+ description: '接口说明',
64
+ summary: '批量将html生成png的zip文件流',
65
+ body: {
66
+ type: 'object',
67
+ required: ['contentList'],
68
+ properties: {
69
+ options: { type: 'object' },
70
+ contentList: {
71
+ type: 'array',
72
+ items: {
73
+ type: 'string'
74
+ }
75
+ }
76
+ }
77
+ }
78
+ }
79
+ },
80
+ async (request, reply) => {
81
+ const filename = await services.parseHtmlToPhotoBatch({
82
+ htmlList: request.body.contentList,
83
+ options: request.body.options
84
+ });
85
+ return reply.sendFile(filename);
86
+ }
87
+ );
88
+
89
+ fastify.post(
90
+ `${options.prefix}/parseHtmlToPhoto`,
91
+ {
92
+ onRequest: options.authenticate,
93
+ schema: {
94
+ description: '接口说明',
95
+ summary: 'html生成png文件流',
96
+ body: {
97
+ type: 'object',
98
+ required: ['content'],
99
+ properties: {
100
+ options: { type: 'object' },
101
+ content: { type: 'string' }
102
+ }
103
+ }
104
+ }
105
+ },
106
+ async (request, reply) => {
107
+ const filename = await services.parseHtmlToPhoto({
108
+ html: request.body.content,
109
+ options: request.body.options
110
+ });
111
+ return reply.sendFile(filename);
112
+ }
113
+ );
114
+
115
+ fastify.post(
116
+ `${options.prefix}/parseUrlToPdf`,
117
+ {
118
+ onRequest: options.authenticate,
119
+ schema: {
120
+ description: '接口说明',
121
+ summary: 'url生成pdf文件流',
122
+ body: {
123
+ type: 'object',
124
+ required: ['url'],
125
+ properties: {
126
+ options: { type: 'object' },
127
+ url: { type: 'string' }
128
+ }
129
+ }
130
+ }
131
+ },
132
+ async (request, reply) => {
133
+ const filename = await services.parseUrlToPdf({ url: request.body.url, options: request.body.options });
134
+ return reply.sendFile(filename);
135
+ }
136
+ );
137
+
138
+ fastify.post(
139
+ `${options.prefix}/parseUrlToPdfBatch`,
140
+ {
141
+ onRequest: options.authenticate,
142
+ schema: {
143
+ description: '接口说明',
144
+ summary: 'url生成pdf文件流',
145
+ body: {
146
+ type: 'object',
147
+ required: ['urlList'],
148
+ properties: {
149
+ options: { type: 'object' },
150
+ urlList: { type: 'array', items: { type: 'string' } }
151
+ }
152
+ }
153
+ }
154
+ },
155
+ async (request, reply) => {
156
+ const filename = await services.parseUrlToPdfBatch({
157
+ urlList: request.body.urlList,
158
+ options: request.body.options
159
+ });
160
+ return reply.sendFile(filename);
161
+ }
162
+ );
163
+
164
+ fastify.post(
165
+ `${options.prefix}/parseUrlToPhoto`,
166
+ {
167
+ onRequest: options.authenticate,
168
+ schema: {
169
+ description: '接口说明',
170
+ summary: 'url生成png文件流',
171
+ body: {
172
+ type: 'object',
173
+ required: ['url'],
174
+ properties: {
175
+ options: { type: 'object' },
176
+ url: { type: 'string' },
177
+ selector: { type: 'string' }
178
+ }
179
+ }
180
+ }
181
+ },
182
+ async (request, reply) => {
183
+ const file = await services.parseUrlToPhoto({
184
+ url: request.body.url,
185
+ selector: request.body.selector,
186
+ options: request.body.options
187
+ });
188
+ return reply.sendFile(filename);
189
+ }
190
+ );
191
+
192
+ fastify.post(
193
+ `${options.prefix}/parseUrlToPhotoBatch`,
194
+ {
195
+ onRequest: options.authenticate,
196
+ schema: {
197
+ description: '接口说明',
198
+ summary: 'url生成png文件流',
199
+ body: {
200
+ type: 'object',
201
+ required: ['urlList'],
202
+ properties: {
203
+ options: { type: 'object' },
204
+ urlList: { type: 'array', items: { type: 'string' } },
205
+ selector: { type: 'string' }
206
+ }
207
+ }
208
+ }
209
+ },
210
+ async (request, reply) => {
211
+ const filename = await services.parseUrlToPhotoBatch({
212
+ urlList: request.body.url,
213
+ selector: request.body.selector,
214
+ options: request.body.options
215
+ });
216
+ return reply.sendFile(filename);
217
+ }
218
+ );
219
+ });
@@ -0,0 +1,30 @@
1
+ const createDeferred = size => {
2
+ const queue = [];
3
+ let runningCount = 0;
4
+ const checkRun = () => {
5
+ if (queue.length === 0) {
6
+ runningCount = 0;
7
+ return;
8
+ }
9
+ if (runningCount < size) {
10
+ const callback = queue.shift();
11
+ runningCount += 1;
12
+ callback().finally(() => {
13
+ runningCount -= 1;
14
+ checkRun();
15
+ });
16
+ }
17
+ };
18
+ return callback => {
19
+ return new Promise(resolve => {
20
+ queue.push(() => {
21
+ const promise = Promise.resolve().then(() => callback());
22
+ resolve(promise);
23
+ return promise;
24
+ });
25
+ checkRun();
26
+ });
27
+ };
28
+ };
29
+
30
+ module.exports = createDeferred;
@@ -0,0 +1,57 @@
1
+ const { LRUCache } = require('lru-cache');
2
+ const path = require('path');
3
+ const fs = require('fs-extra');
4
+ const merge = require('lodash/merge');
5
+ const fileType = require('file-type');
6
+ const objectHash = require('object-hash');
7
+ const { v4: uuidv4 } = require('uuid');
8
+
9
+ module.exports = async options => {
10
+ options = merge(
11
+ {},
12
+ {
13
+ maxCacheKeys: 1000,
14
+ root: path.resolve(process.cwd(), '.puppeteer_cache')
15
+ },
16
+ options
17
+ );
18
+ await fs.emptyDir(options.root);
19
+ const cache = new LRUCache({
20
+ max: options.maxCacheKeys,
21
+ dispose: async value => {
22
+ await fs.remove(path.resolve(options.root, value));
23
+ }
24
+ });
25
+ console.log('init cache dir');
26
+ return {
27
+ setCache: async (object, file) => {
28
+ if (Buffer.isBuffer(file)) {
29
+ const { ext } = await fileType.fromBuffer(file);
30
+ const filename = uuidv4() + '.' + ext;
31
+ const filePath = path.resolve(options.root, filename);
32
+ await fs.writeFile(filePath, file);
33
+ cache.set(objectHash(object), filename);
34
+ return filename;
35
+ } else {
36
+ const filename = uuidv4() + '.zip';
37
+ const filePath = path.resolve(options.root, filename);
38
+ await new Promise((resolve, reject) => {
39
+ file.pipe(fs.createWriteStream(filePath)).on('error', reject).on('finish', resolve);
40
+ });
41
+ cache.set(objectHash(object), filename);
42
+ return filename;
43
+ }
44
+ },
45
+ getCache: async object => {
46
+ const filename = cache.get(objectHash(object));
47
+ if (!filename) {
48
+ return null;
49
+ }
50
+ if (!(await fs.exists(path.resolve(options.root, filename)))) {
51
+ return null;
52
+ }
53
+
54
+ return filename;
55
+ }
56
+ };
57
+ };
@@ -0,0 +1,35 @@
1
+ const puppeteer = require('puppeteer');
2
+ const createDeferred = require('./createDeferred');
3
+
4
+ module.exports = async options => {
5
+ options = Object.assign({ maxTaskSize: 100 }, options);
6
+ const browser = await puppeteer.launch(options.puppeteerOptions);
7
+ const deferred = createDeferred(options.maxTaskSize);
8
+ return {
9
+ task: async callback => {
10
+ return await deferred(async () => {
11
+ const page = await browser.newPage();
12
+ await page.setViewport({ width: 1366, height: 768 });
13
+ const output = await (async () => {
14
+ try {
15
+ return {
16
+ success: true,
17
+ result: await callback({ page })
18
+ };
19
+ } catch (e) {
20
+ console.error(e);
21
+ return { success: false, error: e };
22
+ }
23
+ })();
24
+
25
+ await page.close();
26
+ console.log('puppeteer task complete');
27
+ return output;
28
+ });
29
+ },
30
+ close: async () => {
31
+ console.log('puppeteer browser close');
32
+ await browser.close();
33
+ }
34
+ };
35
+ };
@@ -0,0 +1,17 @@
1
+ const setup = require('./setup');
2
+ (async () => {
3
+ const puppeteerPage = await setup({
4
+ puppeteerOptions: {
5
+ executablePath: 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe'
6
+ }
7
+ });
8
+
9
+ const output = await puppeteerPage.task(async ({ page }) => {
10
+ await page.setContent('<h1>哈哈哈</h1>');
11
+ return await page.pdf();
12
+ });
13
+
14
+ console.log(output);
15
+
16
+ await puppeteerPage.close();
17
+ })();
@@ -0,0 +1,138 @@
1
+ const fp = require('fastify-plugin');
2
+ const setup = require('./core/setup');
3
+ const createCache = require('./core/file-cache');
4
+ const compressing = require('compressing');
5
+ const path = require('path');
6
+
7
+ module.exports = fp(async (fastify, options) => {
8
+ const { maxCacheKeys, root } = options;
9
+ const cache = await createCache(options);
10
+ const puppeteerPage = await setup(options);
11
+
12
+ const memoCache = (cacheName, callback) => async props => {
13
+ const currentCache = await cache.getCache(Object.assign({}, props, { cacheName }));
14
+ if (currentCache) {
15
+ return currentCache;
16
+ }
17
+ const output = await callback(props);
18
+ if (!output.success) {
19
+ throw output.error;
20
+ }
21
+ return await cache.setCache(props, output.result);
22
+ };
23
+
24
+ const batchTask = async (list, callback) => {
25
+ if (list.length > maxCacheKeys) {
26
+ throw new Error('批量操作数量超过上限');
27
+ }
28
+ const zipStream = new compressing.zip.Stream();
29
+ const files = [];
30
+ for (let target of list) {
31
+ files.push(await callback(target));
32
+ }
33
+ files.forEach(filename => {
34
+ zipStream.addEntry(path.resolve(root, filename));
35
+ });
36
+ return { result: zipStream, success: true };
37
+ };
38
+
39
+ const parseHtmlToPdf = memoCache('parseHtmlToPdf', async ({ html, options }) => {
40
+ return await puppeteerPage.task(async ({ page }) => {
41
+ await page.setContent(html);
42
+ return await page.pdf(
43
+ Object.assign(
44
+ {},
45
+ {
46
+ waitUntil: 'networkidle2',
47
+ displayHeaderFooter: true,
48
+ printBackground: true,
49
+ format: 'A4'
50
+ },
51
+ options
52
+ )
53
+ );
54
+ });
55
+ });
56
+
57
+ const parseHtmlToPdfBatch = memoCache('parseHtmlToPdfBatch', async ({ htmlList, options }) => {
58
+ return await batchTask(htmlList, async target => {
59
+ return await parseHtmlToPdf({ html: target, options });
60
+ });
61
+ });
62
+
63
+ const parseHtmlToPhoto = memoCache('parseHtmlToPhoto', async ({ html, options }) => {
64
+ return await puppeteerPage.task(async ({ page }) => {
65
+ await page.setContent(html);
66
+ return await page.screenshot(Object.assign({}, { waitUntil: 'networkidle2', type: 'png' }, options));
67
+ });
68
+ });
69
+
70
+ const parseHtmlToPhotoBatch = memoCache('parseHtmlToPhotoBatch', async ({ htmlList, options }) => {
71
+ return await batchTask(htmlList, async target => {
72
+ return await parseHtmlToPhoto({ html: target, options });
73
+ });
74
+ });
75
+
76
+ const parseUrlToPdf = memoCache('parseUrlToPdf', async ({ url, options }) => {
77
+ return await puppeteerPage.task(async ({ page }) => {
78
+ await page.goto(url, {
79
+ waitUntil: 'networkidle2'
80
+ });
81
+ return await page.pdf(
82
+ Object.assign(
83
+ {},
84
+ {
85
+ displayHeaderFooter: true,
86
+ printBackground: true,
87
+ format: 'A4'
88
+ },
89
+ options
90
+ )
91
+ );
92
+ });
93
+ });
94
+
95
+ const parseUrlToPdfBatch = memoCache('parseUrlToPdfBatch', async ({ urlList, options }) => {
96
+ return await batchTask(urlList, async target => {
97
+ return await parseUrlToPdf({ url: target, options });
98
+ });
99
+ });
100
+
101
+ const parseUrlToPhoto = memoCache('parseUrlToPhoto', async ({ url, selector, options }) => {
102
+ return await puppeteerPage.task(async ({ page }) => {
103
+ await page.goto(url, {
104
+ waitUntil: 'networkidle2'
105
+ });
106
+ if (selector) {
107
+ await page.locator(selector).wait();
108
+ const element = await page.$(selector);
109
+ const boundingBox = await element.boundingBox();
110
+ return await page.screenshot(Object.assign({}, { type: 'png', clip: boundingBox }, options));
111
+ }
112
+ return await page.screenshot(Object.assign({}, { type: 'png' }, options));
113
+ });
114
+ });
115
+
116
+ const parseUrlToPhotoBatch = memoCache('parseUrlToPhotoBatch', async ({ urlList, selector, options }) => {
117
+ return await batchTask(urlList, async target => {
118
+ return await parseUrlToPdf({ url: target, selector, options });
119
+ });
120
+ });
121
+
122
+ const getFilePath = filename => {
123
+ return path.resolve(root, filename);
124
+ };
125
+
126
+ Object.assign(fastify.puppeteer.services, {
127
+ puppeteerPage,
128
+ getFilePath,
129
+ parseHtmlToPdf,
130
+ parseHtmlToPdfBatch,
131
+ parseUrlToPdf,
132
+ parseUrlToPdfBatch,
133
+ parseHtmlToPhoto,
134
+ parseHtmlToPhotoBatch,
135
+ parseUrlToPhoto,
136
+ parseUrlToPhotoBatch
137
+ });
138
+ });
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "@kne/fastify-puppeteer",
3
+ "version": "1.0.0",
4
+ "description": "用于操作无头浏览器截图保存pdf等",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "init": "husky",
8
+ "dev": "nodemon ./tests/server.js",
9
+ "start": "node ./index.js",
10
+ "doc": "node ./open-api.js",
11
+ "build:md": "npx @kne/md-doc",
12
+ "start:md": "npx @kne/md-doc --watch",
13
+ "prettier": "prettier --config .prettierrc --write '{libs/**/*,index}.{js,jsx,ts,tsx,json,css,scss}'",
14
+ "lint-staged": "npx lint-staged"
15
+ },
16
+ "lint-staged": {
17
+ "**/*.md": [
18
+ "git add"
19
+ ],
20
+ "{libs/**/*,index}.{js,jsx,ts,tsx,json,css,scss}": [
21
+ "prettier --config .prettierrc --write",
22
+ "git add"
23
+ ]
24
+ },
25
+ "files": [
26
+ "index.js",
27
+ "libs"
28
+ ],
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "git+https://github.com/kne-union/fastify-puppeteer.git"
32
+ },
33
+ "keywords": [],
34
+ "author": "linzp",
35
+ "license": "ISC",
36
+ "bugs": {
37
+ "url": "https://github.com/kne-union/fastify-puppeteer/issues"
38
+ },
39
+ "homepage": "https://github.com/kne-union/fastify-puppeteer#readme",
40
+ "dependencies": {
41
+ "@fastify/static": "^7.0.4",
42
+ "@kne/fastify-namespace": "^0.1.1",
43
+ "compressing": "^1.10.1",
44
+ "fastify": "^4.27.0",
45
+ "fastify-plugin": "^4.5.1",
46
+ "file-type": "^16.5.4",
47
+ "fs-extra": "^11.2.0",
48
+ "lodash": "^4.17.21",
49
+ "lru-cache": "^11.0.0",
50
+ "object-hash": "^3.0.0",
51
+ "puppeteer": "^22.13.1",
52
+ "puppeteer-core": "^22.13.1",
53
+ "uuid": "^10.0.0"
54
+ },
55
+ "devDependencies": {
56
+ "@fastify/swagger": "^8.14.0",
57
+ "husky": "^9.0.11",
58
+ "nodemon": "^3.1.3",
59
+ "prettier": "^3.2.5",
60
+ "qs": "^6.12.1",
61
+ "sqlite3": "^5.1.7"
62
+ }
63
+ }