前语:

很久没有更新文章了,最近因为很忙(贪玩),一起好像没有收成到很特别的东西,所以就一直没有更新,在上一篇文章看到你的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装备就能够处理我的问题,所以啊,造轮子前还是要多考虑!