本文特色图像出处Chrome Extension Icon #235064

最近碰到了个适合用浏览器扩展解决的问题,就顺带学了一下怎么写一个Google Chrome扩展程序。也就顺带写篇文章记录一下学习的心得。

清单文件

清单文件是扩展中最重要的文件。其实Chrome扩展的文件结构很宽松,只有清单文件是必须要有的。它告诉浏览器你的扩展需要用到Chrome API的哪些功能,需要哪些权限,应该在什么地方什么时候加载扩展的什么文件。除了清单文件得叫manifest.json以外,其他文件你想怎么取名就怎么取名。(曾经我一直以为清单文件拼作mainfest,读起来像“主节日”一样的感觉,直到上次做PWA的时候Chrome死活不认我的清单才发现原来拼错了)

那么清单文件里面得有什么内容呢?

  • name、version、description:不必多说。
  • permission:有些Chrome API需要这些权限才能被成功调用。
  • manifest_version:指示清单文件的版本。很快要出第三版本的manifest了,可以关注一下

根据你要实现的功能,你可能又要加一些其他的字段:

  • browser_action:就是右上角的那些小图标。browser_action你如果定义了default_popup,那么点击后就会出现弹窗。与之关联的API是chrome.browserAction
    [code lang="json" show_lang=true] {//...manifest.json "browser_action": { "default_popup": "popup.html"//弹窗将会显示扩展程序目录下popup.html的内容 /*此外你还能用default_icon自定义按钮的图标,用default_title自定义标题。*/ }, "manifest_version": 2 }[/code]
  • page_action:和browser_action的表现大体一致。区别是browser_action是常驻的,不管啥页面都会在那。而page_action则是在浏览特定页面的时候才会显示的。如果你要做一个特定页面才能用的扩展,比如bilibili下载助手(在油管打开就没有意义了嘛),那就用page_action。与之关联的API是chrome.pageAction
  • content_scripts:要注入到页面中的脚本,也是Chrome扩展程序最令人振奋的功能罢。API文档
  • background:你的扩展在后台运行的部分。
  • options_page:你的扩展程序的配置界面。

扩展里主要用到的就以上这些,完整的manifest.json字段可以在这里找到,另外Chrome API有一个非官方的中文翻译版本

在写扩展时,你要注意根据加载你的脚本文件的位置的不同,你的脚本文件能访问的API也不同,比如只有content_script能访问当前页面的DOM,content_script能访问的Chrome API也是受限的。另外,扩展的HTML文件不能内联脚本,只能通过src引用外部脚本。这些约束在API文档中会有具体的说明,要留心注意一下。除此之外,开发扩展和开发其他前端App不会有太大的区别,除了你能调用强大的Chrome API以外。

必须得吐槽一下强大的Chrome API所有函数都是回调返回结果的做法。写扩展的时候可以自己Promisify一下避免回调地狱。:yukicat.派蒙扰动:

callback hell meme

扩展的打包

在Chrome商店上发布扩展前要把扩展打包,就算不上商店,打包成zip也会方便传输。

Chrome本身就提供打包扩展程序的功能。然而我们肯定是要自动化构建的,那么有没有打包Chrome扩展的npm包呢?

肯定有。搜了下,掘金有篇文章推荐用grunt-crx进行打包。

grunt-crx是grunt的插件,要想使用它你还得先安装好grunt,如果直接用npm run调用grunt的话,你也可以不全局安装grunt-cli。

用下来感觉Grunt比较像Gulp,但是好像没有Gulp在任务间使用stream来传输数据的概念。比起WebPack,Grunt和Gulp更能让你直接体会到程序是怎么被一步步打包的,而不是WebPack或者Parcel那样不知怎么的就打包好了。

就能力而言,Grunt应该是没什么问题的了,社区早就为Grunt写好了各种使用场景下的插件。因此,在Chrome插件打包中要用到的代码压缩什么的都能找到对应的插件。

Grunt的官方文档里面用的是Uglify,搞得我都忘了Uglify不支持es6了。换上Terser后,ecma这个选项可以毫不留情地写最新的版本,反正Chrome都支持。

另外,虽然每个扩展程序中的脚本,Chrome应该都有分配一个独立的全局对象,但是你还是可以用立刻执行函数把你的全局变量包裹一下。这样在Terser的时候,这些变量才会被自动minify。

总之用Grunt的过程还是蛮愉快的。Gruntfile初见可能会比较迷惑,但是弄明白几个基本概念以后还是很简单的。运行也很快(才几k的文件啊),其中就碰到有个警告会比较神秘:

[code lang="js" show_lang=true]//gruntfile
copy: {
main: {
files: [
{
expand: true,
cwd: ['app/'],
src: ["**/*.json"],
dest: "build/"
}
]
}
},[/code]

这个Gruntfile运行时会报:Warning: The "path" argument must be of type string. Received an instance of Array Use --force to continue.

你会以为是path出问题了,然后发现gruntfile里根本没有path这个字段。仔细一看其实是cwd的问题。

最后分享一下我做的一个Gruntfile。这个Gruntfile会将app/下的JSON、JavaScirpt、HTML压缩后按原来的文件夹结构放到build/中,最后让grunt-crx把他们打包在一起。阅读这个现成的例子并阅读相应插件的npm文档应该能更方便地理解Grunt。如果你是使用JavaScript(而不是TypeScript)来写扩展,并且没有用到JSX这些东西,也不用合并js文件的话应该够用了。如果有,你或许也可以用一下grunt-webpack来解决问题。

[code lang="js" show_lang=true]//Gruntfile.js
module.exports = function (grunt) {
grunt.initConfig({
copy: {
main: {
files: [
{
expand: true,
cwd: 'app/',
src: ["**/*.json"],
dest: "build/"
}
]
}
},
'json-minify': {
build: {
files: 'build/**/*.json'
},
},
htmlmin: {
dist: {
options: { // Target options
removeComments: true,
collapseWhitespace: true
},
files: { // Dictionary of files
'build/popup.html': 'app/popup.html', // 'destination': 'source'
}
}
},
terser: {
options: {
ecma: 2020
},
dynamic_mappings: {
files: [
{
expand: true, // Enable dynamic expansion.
cwd: 'app/', // Src matches are relative to this path.
src: ['**/*.js'], // Actual pattern(s) to match.
dest: 'build/', // Destination path prefix.
ext: '.js', // Dest filepaths will have this extension.
extDot: 'first' // Extensions in filenames begin after the first dot
},
]
}
},
crx: {
ext: {
src: "build/**/*",
dest: "dist/makeurl.zip",
},
}
});
grunt.loadNpmTasks('grunt-contrib-htmlmin');
grunt.loadNpmTasks('grunt-contrib-copy');
grunt.loadNpmTasks('grunt-json-minify');
grunt.loadNpmTasks('grunt-terser');
grunt.loadNpmTasks('grunt-crx');
grunt.registerTask('default', ['copy', 'json-minify', 'htmlmin', 'terser', 'crx']);
};
[/code]

届ける言葉を今は育ててる
最后更新于 2020-12-15