• 作者:老汪软件技巧
  • 发表时间:2024-11-18 11:05
  • 浏览量:

而代码部分是一个立即执行函数,所有的内容都需要写在这个立即执行函数内,否则无法生效。

三、问题显现

刚开始,我并没有工程化开发的想法,我想的是就是一个脚本,直接一梭子写到底即可,反正就是那样,就是个普通的 js 文件,一切都是那么原始,朴实无华。

但是当代码来到两千多行后(我是真的很爱加东西),绷不住了,每次写代码都需要在文件上下之间反复横跳,有时候有些变量定义了都不记得,写代码还得滚动半天才能到最底下。

加东西也变得越来越臃肿,越来越丑陋。

忍无可忍,我决定对这个脚本进行工程化改造。但是工程化之前有几个问题需要解决,或者说需要调研清楚。

四、关键点分析1.构建工具

首先肯定是打包成 iife 的产物,很多工具都支持。既然工程化了,一般大家的选择就是 webpack 或者 vite。这里因为涉及到开发模式,需要及时产出打包产物,且能够搭建 dev 服务器,方便访问本地打包后的资源,因此需要选择具备 dev 服务器的开发构建工具。

我选择 vite。当然,webpack 也是不错的选择。

如果你对实时预览要求不高,能够接受复制粘贴到油猴再刷新页面预览,也可以选择纯粹的打包器,例如 rollup。

2.css 预编译器

传统的添加样式的方式,一般就是生成一个 style 标签,然后修改其 innerHTML:

export const addStyle = (css: string) => {
  const style = document.createElement('style');
  style.type = 'text/css';
  style.innerHTML = css;
  document.getElementsByTagName('head')[0].appendChild(style);
}
addStyle(`
  body {
    width: 100%;
    height: 100%;
  }
`);

这样就能实现往网页里添加自定义的样式。但是我现在不满足于书写传统的 css,我既然都工程化了,肯定要把 less 或者 scss 用上。

我的目的,就是可以新建一个例如 style.less 的文件开心地书写 less,打包时候编译一下这个 less 文件,并将其样式注入到目标 HTML 中。

但在传统模块化工程里,构建工具对 less 的支持,是直接在 HTML 中生成一个 style 标签,引入编译后的 less 产物(css)。

也就是说,我需要手动实现 less 到 css 到 js 这个过程。

转变的步骤就是用 less 本身的编译能力,将其产物转变为一个 js 模块。

具体实现放到后面再聊。

3.实现类似热更新的效果

我们启动一个传统的 vite 工程时,我们更新了某个 js 文件或者相关文件后,工程会监听我们的文件被修改了,从而触发热更新,服务也会自动刷新,从而达到实时预览的效果。

这是因为工程会在本地启动一个开发服务器,最终产物也会实时构建,那网页每次去获取这个服务器上的资源,就会获取到最新的代码。根据这点,我们同样需要启动一个本地服务器,而这在 vite 中直接一个 vite 命令即可。

在油猴脚本中,我们新建一个 script 标签,将其 src 指向我们本地服务器的构建产物的地址,即可实现实时的脚本更新,而不用复制产物代码再粘贴到油猴。

代码如下:

// ==UserScript==
// @name         script
// @namespace    http://tampermonkey.net/
// @version      0.0.1
// @description  这是描述
// @author       xxx
// @match        *://baidu.com/*
// @run-at       document-end
// @license      MIT
// ==/UserScript==
(function () {
  "use strict";
  const script = document.createElement("script");
  script.src = "http://localhost:6419/dist/script.iife.js";
  document.body.appendChild(script);
})();

这里的 localhost:6419、/dist/script.iife.js 都取决于你 vite.config.js 中的配置。

具体后面再聊。

五、开始搭建工程

1.使用 yarn create vite 或者 pnpm create vite 初始化一个 vite 模板工程

image.png

image.png

image.png

其他的你自己看着选就可以。

2.修改 vite.config.js

/**
 * @type {import('vite').UserConfig}
 */
module.exports = {
  server: {
    host: 'localhost',
    port: 6419,
  },
  build: {
    minify: false,
    outDir: 'dist',
    lib: {
      entry: 'src/main.ts',
      name: 'script',
      fileName: 'script',
      formats: ['iife'],
    },
  },
  resolve: {
    alias: {
      '@': '/src',
      '@utils': '/src/utils',
      '@enum': '/src/enum',
      '@const': '/src/const',
      '@style': '/src/style',
    }
  }
}

这里使用 cjs 是因为我们会实现一些脚本,脚本里可能会用到这里的某些配置,所以使用 cjs 导出也有利于外部的使用。

3.创建一个 tampermonkey.config 文件,将油猴注释放在这里

// ==UserScript==
// @name         script
// @namespace    http://tampermonkey.net/
// @version      0.0.1
// @description  这是描述
// @author       xxx
// @match        *://baidu.com/*
// @run-at       document-end
// @license      MIT
// ==/UserScript==

当然,你要觉得这样多余、没必要,也可以看自己喜好,只要最终产物里有这个注释即可。但是拆出来有利于我们维护,后续也会新增脚本,有利于工程化的整体性和可维护性。

4.使用 nodemon 监听文件修改

因为我们自己对 less 有特殊处理,加上未来可能会对需要监听的文件进行精细化管理,所以这里引入 nodemon,如果你自己对工程化有自己的理解,也可以按照自己的理解配置。

执行 pnpm i nodemon -D。

根目录新增 nodemon.json:

{
  "ext": "ts,less",
  "watch": ["src"],
  "exec": "pnpm dev:build && vite"
}

这里的 pnpm dev:build 还另有玄机,后面再展开。

到这里,我们的工程雏形已经具备了。但是还有一个最关键的点没有解决——那就是 less 的转换。

六、less 的转换以及几个脚本

首先,less 代码需要编译为 css,但是我们需要的是 css 的字符串,这样才能通过 innerHTML 之类的方法注入到网页中。

使用 less.render 方法可以对 less 代码进行编译,其是一个 Promise,我们可以在 then 中接收编译后的产物。

我们可以直接在根目录新建一个 script 文件夹,在 script 文件夹下新建一个 gen-style-string.js 的脚本:

const less = require('less');
const fs = require('fs');
const path = require('path');
const styleContent = fs.readFileSync(path.resolve(__dirname, '../src/style.less'), 'utf-8');
less.render(styleContent).then(output => {
  if(output.css) {
    const code = `export default \`\n${output.css}\``;
    const relativePath = '../style/index.ts';
    const filePath = path.resolve(__dirname, relativePath)
    if(fs.existsSync(filePath)) {
      fs.rm(filePath, () => {
        fs.writeFileSync(path.resolve(__dirname, relativePath), code)
      })
    } else {
      fs.writeFileSync(path.resolve(__dirname, relativePath), code)
    }
  }
})

我们将编译后的 css 代码结合 js 代码导出为一个模块,供外部使用。也就是说,这部分编译必须在打包之前执行,这样才能得到正常的 js 模块,否则就会报错。

这段脚本执行完后会在 style/index.ts 中生成类似代码:

export default `
  body {
    width: 100%;
    height: 100%;
  }
`

这样 less 代码就能够被外部引入并使用了。

这里多说一句,因为 style/index.ts 的内容是根据 less 编译来的,而我们的 nodemon 会监听 src 目录,因此这个 less 编译后的 js 产物,不能放在 src 下,因为假设将它放在 src 目录下,它在写入的过程中也会触发 nodemon,会导致 nodemon 进入死循环。

除此之外,我们之前还将油猴注释拎出来单独放在一个文件里:tampermonkey.config。

在最终产物中,我们需要将其合并进去,思路同上:

const fs = require('fs');
const path = require('path');
const prettier = require('prettier');
const codeFilePath = '../dist/script.iife.js';
const configFilePath = '../tampermonkey.config';
const codeContent = fs.readFileSync(path.resolve(__dirname, codeFilePath), 'utf-8');
const tampermonkeyConfig = fs.readFileSync(path.resolve(__dirname, configFilePath), 'utf-8');
if (codeContent) {
  const code = `${tampermonkeyConfig}\n${codeContent}`;
  prettier.format(code, { parser: 'babel' }).then((formatted) => {
    fs.writeFileSync(path.resolve(__dirname, codeFilePath), formatted)
  })
}

最后,因为我们的 tampermonkey.config 以及 vite.config.js 可能会更改配置,所以每次我们在开发模式时生成的临时油猴脚本,也需要变,我们不可能每次都去修改,而是应该跟随上面两个配置文件进行生成,我们再新建一个脚本:

const fs = require('fs');
const path = require('path');
const prettier = require('prettier');
const viteConfig = require('../vite.config');
const codeFilePath = '../tampermonkey.js';
const tampermonkeyConfig = fs.readFileSync(path.resolve(__dirname, '../tampermonkey.config'), 'utf-8');
const hostPort = `${viteConfig.server.host}:${viteConfig.server.port}`;
const codeContent = `
  (function () {
    'use strict'
    const script = document.createElement('script');
    script.src = 'http://${hostPort}/dist/${viteConfig.build.lib.name}.iife.js';
    document.body.appendChild(script);
  })()
`;
const code = `${tampermonkeyConfig}\n${codeContent}`;
prettier.format(code, { parser: 'babel' }).then((formatted) => {
  if(fs.existsSync(path.resolve(__dirname, codeFilePath))) {
    fs.rm(path.resolve(__dirname, codeFilePath), () => {
      fs.writeFileSync(path.resolve(__dirname, codeFilePath), formatted);
    });
  }
  else {
    fs.writeFileSync(path.resolve(__dirname, codeFilePath), formatted);
  }
})

稍微用 prettier 美化一下。

七、完善 package.json 中的 script

我们其实只有开发模式,新建一个命令:

"dev": "node script/gen-tampermonkey.js && nodemon"

优先生成 tampermonkey.js,这时候会启动服务器,记得先将 tampermonkey.js 中的内容拷贝到油猴,才能方便热更新,不然又需要复制粘贴。

对于 build 命令:

"dev:build": "node script/gen-style-string.js && tsc && vite build && node script/gen-script-header-comment.js"

需要先将 less 编译为可用的 js 字符串模块,然后才能执行 build,build 完还需要拼接油猴注释,这样最终产物才具备可用的能力。

开发完成后,就将打包产物替换掉之前粘贴进油猴的内容。

八、额外的补充

vite 命令会直接启动本地开发服务器,而我们的 script 命令中,使用 && 时,下一个命令会等待上一个命令执行完成后再执行,所以 vite 需要放在最后执行,这是串行逻辑。当然,借助一些库我们可以实现并行 script 命令。但是我们这里需要的是串行,只是不完美的是,每次文件变更,都需要重新执行 pnpm dev:build && vite,这样会重复新启一个服务器,但是不重启的话,始终使用最初的那个服务,最新编译的资源无法被油猴感知,资源没有得到更新。

所以,聪明的你有办法解决吗?

往期推荐

金九银十招聘季,IT 打工人,该怎么识别烂公司好公司? 70+ 80+

为什么就这个文件的 ESLint 检查失效了?

学会 TypeScript 体操,轻松看懂开源项目代码

别人休息我努力,悄悄写个 cli 工具,必须提升效率,skr~ 60+ 110+

一文掌握 eslint,再也不怕项目报错 20+ 30+

开发一个 npm 库应该做哪些工程配置? 40+ 50+

分享我在前端学习与开发中用到的神仙网站和工具 40+ 110+

uniapp 踩坑记录(二) 130+ 150+

闲来无事,摸鱼时让 chatgpt 帮忙,写了一个 console 样式增强库并发布 npm 100+ 110+

uniapp 初体验踩坑记录 30+ 60+

两小时学会 JS 正则表达式,终身不忘 50+

【一年前端必知必会】如何写出简洁清晰的代码 50+

【一年前端必知必会】了解 Blob,ArrayBuffer,Base64 40+ 90+