前言:
很久没有更新文章了,最近因为很忙(贪玩),同时好像没有收获到很特别的东西,所以就一直没有更新,在上一篇文章看到你的mock方案这么好用,比我的mock难用还要难受没想到收获了这么多的点赞哈哈,成就感满满就屁颠屁颠的跑来更新了,这一次分享一下自己做脚手架的心路历程,我是怎么从以为自己是天才到满满发现自己是傻逼的哈哈哈。
初衷
我为什么会想要制作这么一个脚手架呢?其实就是自己在开发Google浏览器插件的时候发现,用原生的js来开发一个chrome是非常低效、体验很差的。我们知道(你不知道就去看其他博主的文章哈 这里我就不展开讲了 不然内容太多~)浏览器插件是分为很多个部分的,负责展示的popup,操作页面和部分插件api的content-script、运行在后台的脚本server-worker以及各式各样为满足需求的注入脚本injected-script,开发这些部分的时候如果全部用原生来开发那是相当的难受的,对于一个框架重度依赖者,你让我用getElement一个个去写比杀了我还难受。同时如果你以前开发过Chrome插件,那么你会发现里面其实是存在许许多多的通信的,多个通行之间通过约定的标识来进行识别,那么就会在编写代码的过程中引入大量的魔术字符串(自行百度概念谢谢)。我在开发的时候就在想,有没有这么一个脚手架:他和我们平时开发create-react-app一样(适用react、less/sass、模块化、ts、dev-server热更新)快速的开发一个Chrome插件呢?我没找到,那我就造一个吧!
前期
首先明确一点,无论多么复杂的cli,你最终无非就是编译成我们熟悉的三件套,cli意在简化我们开发的配置,我的要求很简单,你能不让我不写原生、支持ts、模块化等功能就好了,那么我肯定是要拿出我工程化的大利器webpage了。首先明确一点,我们的Chrome肯定不是一个单页面应用,前面我们已经讲了它是由多个模块组成。
项目基础搭建
要想我们的写的那一坨坨的代码,我们肯定是离不开我们babel的支持的,话不多说,我们上配置:
{
"name": "google-extends",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev:popup": "cross-env NODE_ENV=development FILE_TYPE=popup webpack server --open --config webpack.dev.js",
"dev:uploadFile": "cross-env NODE_ENV=development FILE_TYPE=uploadFile webpack server --open --config webpack.dev.js",
"dev:contentScript": "cross-env NODE_ENV=development FILE_TYPE=contentScript webpack server --open --config webpack.dev.js",
"build:popup": "cross-env NODE_ENV=production FILE_TYPE=popup webpack --config webpack.prod.js",
"build:uploadFile": "cross-env NODE_ENV=production FILE_TYPE=uploadFile webpack --config webpack.prod.js",
"build:contentScript": "cross-env NODE_ENV=production FILE_TYPE=contentScript webpack --config webpack.prod.js",
"build:serviceWorker": "cross-env NODE_ENV=production FILE_TYPE=serviceWorker webpack --config webpack.prod.js",
"build:injectedScript": "cross-env NODE_ENV=production webpack --config webpack.mpa.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@ant-design/icons": "^5.0.1",
"antd": "^5.3.1",
"jsoneditor": "^9.9.2",
"less": "^4.1.3",
"prop-types": "^15.8.1",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@babel/core": "^7.21.0",
"@babel/preset-env": "^7.20.2",
"@babel/preset-react": "^7.18.6",
"@babel/preset-typescript": "^7.21.0",
"@types/chrome": "0.0.223",
"@types/lodash": "^4.14.191",
"@types/react": "^18.0.28",
"@types/react-dom": "^18.0.11",
"autoprefixer": "^10.4.14",
"babel-loader": "^9.1.2",
"clean-webpack-plugin": "^4.0.0",
"cross-env": "^7.0.3",
"css-loader": "^6.7.3",
"css-minimizer-webpack-plugin": "^4.2.2",
"html-webpack-plugin": "^5.5.0",
"less-loader": "^11.1.0",
"mini-css-extract-plugin": "^2.7.3",
"portfinder": "^1.0.32",
"postcss-loader": "^7.0.2",
"sass-loader": "^13.2.0",
"style-loader": "^3.3.1",
"typescript": "^4.9.5",
"webpack": "^5.76.1",
"webpack-cli": "^5.0.1",
"webpack-dev-server": "^4.11.1",
"webpack-merge": "^5.8.0"
}
}
复制代码
开发不看package.json,你连项目都起不明白哈哈,我们日常开发想看项目启动肯定是找pageage中的script的,这里看到一堆dev和build,没错我没有把它配置成一个多页面的应用来启动而是拆分成一个个的部分来进行编译,那么这样以后我要改插件的哪个部分就可以单独build哪个了。接着我们来看一些必要的依赖,Babel部分是用来编译我们最中的代码,各种loader是为了加载我们要的能力,webpack的依赖是为了符合我们开发的要求,很完美,没清楚怎么配置一个webpage的可以看下我之前的这篇文章:快速弄一个可以跑的cli。
开发问题
到了上手开发的部分了,我们知道Chrome插件中有很多独有的拓展是在其开发环境中才有的,在我们平时的页面中是不存在的,比如操作内存的chrome.storage以及发送消息的chrome.runtime.sendMessage,这样我们怎么才能在正常的开发中也拥有这些特殊环境才有的api呢?加入垫片!我的想法很美好,只要我们在页面加载前,判单对应的Chrome对象上面是否拥有我们的特殊api那么就能来判断是一般环境还是Chrome插件环境,如果是插件环境那么我们什么也不用改变就可以正常运行我们的项目,如果是正常的环境,那么我们就事先改变我们的Chrome对象,加上这些对象,模拟出插件的环境!
举例子
说干就干,我们看下一个简单的例子:
const key='profit-extends-storage'
export function profit(){
if(!chrome.storage){
chrome.storage={
local:{
get:function(keyArr: string[], func: (result: Record<string,any>)=>void){
const originData = JSON.parse(localStorage.getItem(key)) || {};
const result = keyArr.reduce((targetObj,item)=>{
if(item in originData){
targetObj[item] = originData[item]
}
return targetObj;
},({} as Record<string,any>));
func(result);
},
set:function(values: Record<string,any>){
const originData = JSON.parse(localStorage.getItem(key)) || {};
localStorage.setItem(key,
JSON.stringify({
...originData,
...values
})
)
}
}
}
}
}
复制代码
这里的代码是我们操作浏览器存储的插件,首先要知道插件的storage和浏览器的storage是不一样的,如果没在插件环境下你是没有这个插件的storage的,也就是它会是undefined,我们通过判断是否具有这么个存在,来进行最终是否改变我们的Chrome对象,来模拟出我们插件中的storage!如这里我就是开辟一个新的空间来存储我们模拟的这部分storage,这样我们就可以在正常的环境中使用到我们的插件storage了,是不是很帅?当然哈,不是所有的插件部分都可以操作插件的storage,所以我们要根据情况进行添加这个垫片,像我们的popup中就可以操作,所以我们这样:
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import { profit } from '../profit';
profit();
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);
复制代码
在其挂载之前加入这个垫片,这样就可以模拟出浏览器插件的环境了。
难点
有人就要说啦,那通行你怎么模拟?我记得浏览器中开发是存在chrme.message来进行通信的呀?其实是一样的道理,我们只要在执行前,进行Chrome.message的判断,如果为空,那么我们就自己书写其的发送和监听,我这里中各个部分不是跑在一个端口,比如popup在8080,content-script在8081,那么就是跨域的通信了,解决的方法有很多什么监听storage变化,postmessage,websocket转发,多钟多样,我这里就不展开讲先,我最中选择的是websocket的转发,比如a页面到b页面就通过a发送消息到web-socket,web-socket再通知对应的页面,这样就模拟出了页面的通信。
思考
我做到一半一直在想,其实我为的就是模块化、react、less这些东西,当然还有很重要的就是热更新。通过前面的配置,我们最终生成的产物其实已经是符合我们的要求了,但是为了能在正常的环境中开发,我们需要写一堆的垫片,这样是非常复杂的(能做但是困难),我忽然想到,热更新的原理其实就是文件改动,后被感知引起的页面改动,那么google的插件在源文件改动的时候不是会自动更新了吗?我只要在我文件改动的时候,实时的更新我的产物就好了,那么页面在Chrome插件的环境下就会自动更新,同时还不用写那么多自以为是的垫片了呀。
动手
我们知道我们一般配置为build和dev命令,build的时候我们会把编译产物生成在dist文件中,浏览器插件加载的就是这一部分的实体文件,而在dev的时候其实生成的文件是在内存中,这样我们的插件是不能加载的,而build的操作十分满,dev改动才是热更新,我们只要把dev的时候生成的文件也在dist文件夹中这不就会时时更改变成热更新了吗?很幸运的是我们的dev-server已经存在这样的配置了,writeToDisk。
devServer: {
devMiddleware: {
writeToDisk: true,
}
}
复制代码
总结
为了工程化的开发我引入了webpack,为了能过模拟Chrome插件的各种api我写了大量的插件,到最后没想到,原来只需要一个简简单单的webpage配置就可以解决我的问题,所以啊,造轮子前还是要多思考!