7
头图

Some time ago, for the tenth anniversary of Sifu, I organized a question-and-answer event. Those who participate in the punch-in activity need to add a "little tail" at the end of the answer to the question. Adding a small tail itself is not difficult, but since the official does not provide a shortcut, you need to copy it from somewhere every time, which is a bit cumbersome. I just installed the oil monkey plug-in not long ago, and I thought: how about injecting a button into the editor to add a small tail?

Before using Oil Monkey, I used a plugin called "User JavaScript and CSS" that can inject scripts and styles into specific web pages. However, this plugin is not available in the Edge market and can only be installed from the Chrome market, which is a bit difficult to install. Later, I went to the Edge market to find a "Page Manipulator" that can achieve similar functions. The reason why the oil monkey has been useless is mainly because the oil monkey has to write the code itself to inject the style sheet, and is too lazy to write it.

Scripts to inject small tails are not difficult and are not the focus of this article. The point is that after the script was shared, I received some "script not available" feedback. Although most browsers use Chrome/Edge or Chrome core browsers, there are differences in versions, and some versions do not support ?? , ?. and ??= etc.

Speaking of which, it's not difficult to change the operators. After all, JavaScript programs are not written the same without these new operators. But it's really uncomfortable to have a new syntax that doesn't work. If you still want to use the new syntax and want to be compatible with more browsers, the only way is to "compile".

Webpack is a bit heavy, and it is not worthwhile to introduce Webpack to build a project for these few lines of scripts. Thinking of the lightweight and fast esbuild I heard before, I decided to try it.

Sure enough, one line of commands did the trick:

 npx esbuild src/add-tail.js --outfile=dist/add-tail.js --target=chrome77

?. ??null ,虽然是用的==不是=== , but this result is quite satisfactory. After all, if you use === , you need to compare it with undefined .

Even, if you add the --bundle parameter, you can also split the source file, use ESM to write code in blocks, and decoupling and reuse will not be delayed.

I was about to finish work perfectly, and suddenly found a problem - the script header information written with comments is gone! Although you can find a place to save the header information, and then manually fill it before the translation result, but doing so is tiring! I wandered around the Internet for a long time and really didn't find any solution.

Although esbuild provides the --banner parameter, there are two problems:

  1. The script header is too long and still has multiple lines. It is not easy to add parameters with --banner ;
  2. If you need to translate multiple scripts at the same time, there is no way to dynamically modify the banner for each script.

After thinking about it, only use the API interface of esbuild, write a program to translate, and use the program to fill in the script header after the translation. The program is written in build.js . The basic translation process is nothing more than changing the command line parameters to function calls, which is also simple

 const result = await build({
    logLevel: "info",
    outdir: distDir,
    entryPoints,
    bundle: true,
    target: ["chrome77"],
    metafile: true,
}).catch(() => process.exit(1));

const analyzeResult = await analyzeMetafile(result.metafile);
console.log(analyzeResult);

Among them, distDir is configured as "dist" directory. And entryPoints is the first layer of script files found in the "src" directory using Node's fs interface. submodules are placed in subdirectories):

 const srcDir = path.resolve("./src");
const distDir = path.resolve("./dist");

const entryNames = (await fs.readdir(srcDir, { withFileTypes: true }))
    .filter(entry => entry.isFile() && /\.js$/.test(entry.name))
    .map(({ name }) => name);

const entryPoints = entryNames.map(filename => path.resolve(srcDir, filename));

Only the output analysis result is a bit of a brain. The command line is a parameter, and another interface needs to be called here.

The idea of processing the script header is very clear: before build() , you can read the source file and extract the script header. After build() , read the output file, add the script header to it and save it again.

Checked esbuild's documentation and found that it can be implemented using its plugin mechanism. In the event of the plugin onLoad the file needs to be read once. If you read it here, you don't need to read it again before building. In the onEnd event, you can first judge whether there is an error in the build process, and inject the script header if there is no error.

 const plugin = {
    name: "sf-script-plugin",
    setup(build) {
        build.headers = {};
        build.onLoad({ filter: /src[\\/][^/\\]+\.js$/ }, async (args) => {
            const contents = await fs.readFile(args.path, "utf8");
            build.headers[path.relative(srcDir, args.path)] = extractHeaders(contents);
            return { contents };
        });
        build.onEnd(result => {
            if (result.errors.length) { return; }
            Object.entries(build.headers)
                .forEach(([filename, header]) => insertHeader(filename, header));
        });
    }
};

function extractHeaders(contents) {
    return contents.match(/^.*?\/\/ ==\/UserScript==/s)?.[0];
}

async function insertHeader(filename, header) {
    const filePath = path.resolve(distDir, filename);
    const content = await fs.readFile(filePath, "utf8");
    fs.writeFile(filePath, [header, content].join("\n\n"));
}

Of course, don't forget to add the plugins parameter in the build process

 await build({
    ...
    plugins: [plugin],
}

When I wrote onLoad , I stepped on the pit, mainly filter to src all the .js ---in the directory, but To exclude files in all subdirectories.

Code done, tried it out, perfect!

 node ./build.js

边城
59.8k 声望29.6k 粉丝

一路从后端走来,终于走在了前端!