You can find code here: GitHub repo
Try it out: Live link
Intro
In this article, I am going to show you step by step, how I have built a full working chrome extension. My extension is called "Random Wikipedia Pages", which shows random Wikipedia articles and counts how many of them have already been shown or clicked by user. You can see the final result here.
Technological stack
I made the extension with the use of:
- React
- Styled Components
- Sweet State
- Fetching data
In order to enjoy my article, you should know at least basics of React. Being familiar with Styled Components and any State Management library is welcome, but not obligatory.
Of course, you should also understand how fetching data from external API works.
Table of contents
- Getting started + Project plan
- Create React App
- Load your extension
- Explore the folder structure
- Creating layout and components
- Working with Wikipedia API and creating a store reducer
- Building full extension from top to bottom
- Article
- Buttons
- Conclusion
Step 1 - Getting started + Project plan
At first I'm going to explain more precisely how this extension actually works.
When opens the extension, an app fetches the random article and displays it.
User can read full article when he clicks a blue button - Then he is being redirected to full Wikipedia page but also he can draw the next article when he clicks a light button.
Every time user clicks any button, stats are getting updated.
At the bottom there is located a link to the article you currently read and to the GitHub repo.
Now let's start with coding!
1. Create react app
At first, create react app using the dedicated template to make chrome extensions.
npm init react-app my-first-extension --scripts-version react-browser-extension-scripts --template browser-extension
and then
cd my-first-extension
2. Load your extension
Before explaining the structure of project, let's load the extension in chrome.
- Go to
chrome://extensions
- Click on the "Developer Mode" button, on the upper right
- Click "Load unpacked" button and select
dev
folder from our project
Now, when turning on your extension, you should have the following view:
And...that's it! This is the way, how to create a very basic extension. Later on, we will just operate with Wikipedia API and store configuration (which is kinda harder), because the whole extension logic is almost done.
3. Explaining the folder structure
Let's go back to the code stuff.
If you are keen on React, the folder structure should be
known for you.
my-first-extension
├── README.md
├── node_modules
├── package.json
├── .gitignore
├── public
├── img
│ ├── icon-16.png
│ ├── icon-48.png
│ ├── icon-128.png
├── popup.html
├── options.html
└── manifest.json
└── src
├── background
│ ├── index.js
├── contentScripts
│ ├── index.js
├── options
│ ├── index.js
│ ├── Options.js
├── App.css
├── App.js
├── App.test.js
├── index.css
├── index.js
├── logo.svg
There are few folders that are actually not necessary and you can ignore them.
These folders are:
- src/background - Responsible for working in the background and watch if user for example clicks any keyword shortcut. We don't need that in this project.
- src/contentScripts - Responsible for managing a webpage (for example styles change) on which user currently is. We don't need that in this project.
- src/options - Automatically generated page for user, when he can manage his options. Our app doesn't have that feature. So that, you can also ignore the public/options.html which is a template for that page.
However, you should get familiar with following files:
- public/manifest.json - It is a primary file which describes your app. You put here information like title, description, version etc.
-
public/popup.html - A template for your extension. Taking advantage of the fact that we are here, let's import our basic font "Titilium Web" (weight 300 and 600)
<link href="https://fonts.googleapis.com/css2?family=Titillium+Web:wght@300;600&display=swap" rel="stylesheet">
Additionally, I have added a .prettierrc
file, which is responsible for formatting my code.
Step 2 - Creating layout and components
Now that you have created a project folder, it's time to prepare layout and components.
Layout
At first, let's make a layout folder.
In order to do that, I create theme.js file in that and add basic colors.
// src/layout/theme.js
export default {
colorBlue: '#00A8FF',
colorGrey: '#414141',
colorWhite: '#fff',
}
Because of the fact that I want those color variables to be available in every section of the app, I must use ThemeProvider
, which provides theme variables to every component.
// src/layout.layout.js
import React from 'react';
import { ThemeProvider } from "styled-components";
import theme from './theme'
const Theme = props => {
return (<ThemeProvider theme={theme}>{props.children}</ThemeProvider> );
}
── src
├── layout
├── layout.js
├── theme.js
├── wrap.js
At the end, I create a simple Wrapper, which wraps the entire content of every section.
// src/layout/wrap.js
import styled from 'styled-components'
const Wrap = styled.section`
width: 280px;
margin: auto;
position: relative;
`
export default Wrap
Components
Some elements will certainly be used more than once, hence they should be stored in different files.
So let's do that for Button, Desc and Header.
── src
├── components
├── desc
│ ├── desc.js
├── header
│ ├── header.js
├── button
│ ├── button.js
Step 3 - Working with Wikipedia API and creating a store reducer
Well, despite I don't find this project unusually hard, this is the most difficult part of it.
In this section I fetch data from Wikipedia API and I configure the state management store, which is responsible for making request to Wikipedia endpoint, saving received data to state and updating local statistics (so here goes the local storage stuff, which is especially uncomfortable when it comes to chrome browser).
Making a Wikipedia request
At first I will show you how to fetch data from Wikipedia API.
The goal of my request is to achieve some english random article. Only title and beginning field is necessary.
The request should look like this:
https://en.wikipedia.org/w/api.php?format=json&action=query&generator=random&grnnamespace=0&prop=extracts|description&grnlimit=1&explaintext=
There I describe for what specific param stands for:
Request part | Value | Role |
---|---|---|
https://en.wikipedia.org/w/api.php | - | Api URL |
format | json | Response format |
action | query | The goal is to query some data (not to update f.e) |
generator | random | Declaring, I need a random page |
prop | extract | Field, I want to receive (extract stands for description) |
explaintext | - | Returns extracts field in txt style (instead of html) |
grnlimit | 1 | Quantity of pages |
grnamespace | 0 | ** |
** - I won't lie. I'm not sure what this tagged param is supposed to be responsible for. Understanding Wikipedia API is very hard, documentation is barely user-friendly. I have just found this param on StackOverflow and so the request can work.
An example of response:
{
"batchcomplete": "",
"continue": {
"grncontinue": "0.911401741762|0.911401757734|60118531|0",
"continue": "grncontinue||"
},
"query": {
"pages": {
"38142141": {
"pageid": 38142141,
"ns": 14,
"title": "Category:Parks on the National Register of Historic Places in Minnesota",
"extract": "Parks on the National Register of Historic Places in the U.S. state of Minnesota."
}
}
}
}
As you can see, everything works fine. We have all necessary fields.
Working with reducer
In order to manage state in my app I used React Sweet State. I decided to use this library due to its easiness. I managed to keep my whole reducer logic in one file, because there are only two actions necessary:
- IncrementOpen (after clicking blue button)- Responsible for getting stats data from chrome about total clicked articles and updating them
- FetchArticle (after clicking light button) - Responsible for fetching article, sending it to state, getting statistics data from storage (how many articles have been already fetched and how many clicked) and updating stats after every fetch
Reducer file is located in the "reducer" folder.
── src
├── reducer
├── store.js
At first, installing library via NPM is required.
npm i react-sweet-state
So, let's start! At the beginning, I import installed library and create initialState, which contains all basic fields
src/reducer/store.js
// src/reducer/store.js
import { createStore, createHook } from 'react-sweet-state'
const initialState = {
title: '', //Stands for the tittle of article
desc: '', // Contains article text
id: '', // Id of article (useful when you want to make a link)
isTooLong: false, //Informs if fetched text was longer than 250 chars
}
Now it's time to create a store.
// src/reducer/store.js
const Store = createStore({
initialState, //our basic state
actions:{ //here go the actions, that I described earlier
fetchArticle : ()=>
// My fetchArticle code
}
})
In order to make my notes more readable, my entire code below is located in the exact place, where the My fetchArticle code
comment is placed.
At first I must create one more function, which destructs setState and getState function and at the very beginning I'm setting state as initial state (so that when fetching new article, state has no values and the loading effect is being shown then).
As mentioned, in this function I must get user stats, which are located in the chrome storage.
At first, I initial all the variables that are necessary:
const keyShown = 'allTimeShown' // Key of total shown articles
const keyOpen = 'allTimeOpened'//Key of tot clicked articles
let counterAllTimeShown = 1 //Value of total shown articles
let counterAllTimeOpen = 0 //Value of total clicked articles
let isFound = false //Checking if chrome storage contains those keys (necessary if user runs this extansion first time)
Before we fall into working with Chrome storage, we must add global chrome object into our file.
It is very simple, you must only this simple line of code at the beginning of reducer.js
// src/store/reducer.js
/*global chrome*/
import { createStore, createHook } from 'react-sweet-state'
.
.
Note, that in order to have an access to chrome storage, user must allow it. In order to do that, putting this line into our manfiest.json is necessary.
// public/manifest.json
{
"permissions": ["storage"],
}
Now we must get stats values from chrome storage. At first, I feel obliged to instruct you how it works. I have spent a lot of time in order to understand chrome storage logic.
Instinctively, if you fetch data asynchronously, usually you expect it to look like this:
//How it usually looks
const res = await library.getData()
And so, when working with chrome storage you would probably expect it to look this way:
// What you would expect
const res = await chrome.storage.sync.get([keyShown,keyOpen])
Unfortunately, chrome storage doesn't work so simple. The only way to receive your response is to pass a callback a function as an argument when getting data from chrome storage.
// This is the only correct way
chrome.storage.sync.get([keyShown, keyOpen], async res => {
//Here goes the rest of logic:( this is the only way to have access to the chrome response
}
Instead of splitting the rest of code of fetchArticle action into smaller pieces of code, I will show you the final efect now.
chrome.storage.sync.get([keyShown, keyOpen], async res => {
counterAllTimeOpen = res[keyOpen] || 0 //Checking if response contains my totalOpen key
if (keyShown in res) { //If contains, get total shown value
counterAllTimeShown = res[keyShown]
isFound = true
}
if (isFound) //If contains, increment totalShownStats
chrome.storage.sync.set({ [keyShown]: counterAllTimeShown + 1 })
else { //If not, set default
chrome.storage.sync.set({ [keyShown]: 2 })
}
//Fetch data section
const url =
'https://en.wikipedia.org/w/api.php?format=json&action=query&generator=random&grnnamespace=0&prop=extracts&grnlimit=1&explaintext='
let resp = await fetch(url) //Fetching article
resp = await resp.json()
//Getting title, extract and Id values from response
const response = { ...resp }
const id = Object.keys(response.query.pages)[0]
const title = response.query.pages[id].title
let desc = response.query.pages[id].extract
let isTooLong = false //Some articles might be very very long - There is no enough place in that litle extension. So that, I set limit to 250.
if (desc.length >= 252) {
desc = desc.substring(0, 250)
isTooLong = true
}
//Final - setting state!
setState({
id,
title,
desc,
isTooLong,
[keyShown]: counterAllTimeShown,
[keyOpen]: counterAllTimeOpen,
})
})
I know, there was a lot of stuff in this part. If you don't understand it - Go again through this part. If you want to see the final effect of this part of code- click here.
The whole fetchArticle action is described in these steps:
- Setting State fields to falsify values
- Initializing key and value variables
- Getting data from chrome storage
- Checking if stats values are not nullable
- Saving incremented stat (allTimeShown) or the default value
- Making a Wikipedia request
- Getting necessary data from Wikipedia response
- Checking if text isn't too long (250 chars max)
- Updating state
If you went through this, you have already got the worst part behind you. Now it will be only easier.
The only thing left is to create an incrementOpen
action but rust me - It very easy. It takes literally 4 lines of code.
actions:{
incrementOpen:
() =>
({ setState, getState }) => {
const key = 'allTimeOpened'
const counter = getState()[key] + 1 || 0
setState({ ...getState(), [key]: counter })
chrome.storage.sync.set({ [key]: counter })
}
}
This action is invoked when user clicks a blue button. Then he is redirected to the full Wikipedia webpage and "allTimeOpened" stat is increased.
Step 4 - Building full extension from top to bottom
Now that all the components have been created and whole app logic has been done, it's time to put all the pieces together.
My folder structure from the partial folder looks like this:
── src
├── partials
├── banner
│ ├── banner.js
├── article
│ ├── article.js
├── buttons
│ ├── buttons.js
├── stats
│ ├── stats.js
├── footer
│ ├── footer.js
Banner and Footer are totally stateless parts, so I won't describe their structure here, it's literally part of few components. Moreover, paradoxically, there is no big logic in Stats - they only show values comping from states.
Let's focus on the parts, which use actions coming from storage then.
In order to use use and manage my state properly, I import my state and treat it as a hook.
import { useCounter } from '../../store/reducer'
In order to use a Skeleton loading when waiting for fetching data, I must install a react-loading-skeleton package
npm i react-loading-skeleton
Article.js
Now look at my article component. It is a place, where all the data coming from Wikipedia are being shown.
// src/partials/article/article.js
const Article = props => {
const [state, actions] = useCounter()
useEffect(() => {
actions.fetchArticle()
}, [])
return (
<Layout>
<Wrap as="article">
<Header bold margin>
{state.title || <Skeleton />}
</Header>
<StyledDesc>
{state.desc ? (
state.isTooLong ? (
`${state.desc}...`
) : (
state.desc
)
) : (
<Skeleton count={5} />
)}
</StyledDesc>
{state.isTooLong && <Whiter />}
</Wrap>
</Layout>
)
}
As you can see, if data isn't already fetched, the Skeleton will be shown instead of empty text.
Furthermore - If text is too long, then after the description come "..." sign in order to signalize, that text has been shorted.
Note, that I have used a <Whiter>
component. Thanks to that, when text is too long, this component gives an effect of disappearance of text.
const Whiter = styled.div`
background: linear-gradient(
180deg,
rgba(255, 255, 255, 0.1) 0%,
rgba(255, 255, 255, 0.8) 93.23%
);
width: 100%;
height: 65px;
position: absolute;
bottom: 0;
left: 0;
`
Buttons.js
This partial is responsible for having two buttons and managing stats system.
Reminder: After clicking a blue button, user is redirected to full Wikipedia article (and total clicked stats is increased) and after clicking a light button a new article is fetched (and total shown is increased, though).
// src/partials/buttons/buttons.js
const Buttons = () => {
const [state, actions] = useCounter()
const linkClickHandler = () => {
actions.incrementOpen()
window.open(`http://en.wikipedia.org/?curid=${state.id}`, '_blank').focus()
}
return (
<Layout>
<StyledWrap>
<Button full first active={!!state.title} onClick={linkClickHandler}>
Read full on Wikipedia
</Button>
<Button
active={!!state.title}
disabled={!state.title}
onClick={actions.fetchArticle}
>
Find another article
</Button>
</StyledWrap>
</Layout>
)
}
App.js
The only thing left is to import all partials and place it in the app component.
// src/App.js
function App() {
return (
<div className="App">
<Wrap>
<Banner />
<Article />
<Buttons />
<Stats />
<Footer />
</Wrap>
</div>
)
}
Conclusion
And so it works. I firmly believe that I described in detail process of creating my Wikipedia extension.
It's breathtaking, that the entire logic could have been done with React only.
If you have any questions - Write comments and send messages to communicate with me;)
You can find final code here: GitHub repo
Try it out: Live link
Feel free to rate my extension or give a star to my repo!
Top comments (0)