├── .gitignore
├── README.md
├── babel.config.js
├── package.json
├── src
├── App.vue
├── content
│ └── index.js
├── manifest.development.json
├── manifest.production.json
├── options
│ ├── App
│ │ └── App.vue
│ ├── index.html
│ └── index.js
├── popup
│ ├── App
│ │ └── App.vue
│ ├── index.html
│ └── index.js
└── utils
│ ├── hot-reload.js
│ └── index.js
└── vue.config.js
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | /dist
4 | package-lock.json
5 |
6 | # local env files
7 | .env.local
8 | .env.*.local
9 |
10 | # Log files
11 | npm-debug.log*
12 | yarn-debug.log*
13 | yarn-error.log*
14 |
15 | # Editor directories and files
16 | .idea
17 | .vscode
18 | *.suo
19 | *.ntvs*
20 | *.njsproj
21 | *.sln
22 | *.sw?
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # vue-extension-template
2 | `vue-cli3` + `webpack4` + `element-ui` 实现编译打包Chrome浏览器插件
3 |
4 | ## 环境安装
5 | ```
6 | npm install
7 | ```
8 |
9 | ### 开发环境编译并热更新
10 | ```
11 | npm run serve
12 | or
13 | npm run build-watch
14 | ```
15 |
16 | ### 生产环境打包
17 | ```
18 | npm run build
19 | ```
20 |
21 | ### 分析包组件大小
22 | ```
23 | npm run analyze
24 | ```
25 |
26 | ### Lints and fixes files
27 | ```
28 | npm run lint
29 | ```
30 |
31 | ### Customize configuration
32 | See [Configuration Reference](https://cli.vuejs.org/config/).
33 |
34 | [详细开发文档](https://mrli2016.github.io/notes/frontend/vue-cli3开发Chrome%20Extension实践.html)
35 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [
3 | '@vue/app'
4 | ],
5 | "plugins": [
6 | [
7 | "component",
8 | {
9 | "libraryName": "element-ui",
10 | "styleLibraryName": "theme-chalk"
11 | }
12 | ]
13 | ]
14 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vue-extension",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "serve": "npm run build-watch",
7 | "build": "vue-cli-service build",
8 | "lint": "vue-cli-service lint",
9 | "build-watch": "vue-cli-service build-watch",
10 | "analyze": "npm run build --report"
11 | },
12 | "dependencies": {
13 | "core-js": "^2.6.5",
14 | "element-ui": "^2.8.2",
15 | "vue": "^2.6.10"
16 | },
17 | "devDependencies": {
18 | "@vue/cli-plugin-babel": "^3.7.0",
19 | "@vue/cli-plugin-eslint": "^3.7.0",
20 | "@vue/cli-service": "^3.7.0",
21 | "babel-eslint": "^10.0.1",
22 | "babel-plugin-component": "^1.1.1",
23 | "copy-webpack-plugin": "^4.6.0",
24 | "eslint": "^5.16.0",
25 | "eslint-plugin-vue": "^5.0.0",
26 | "vue-cli-plugin-chrome-ext": "0.0.5",
27 | "vue-template-compiler": "^2.5.21",
28 | "zip-webpack-plugin": "^3.0.0"
29 | },
30 | "eslintConfig": {
31 | "root": true,
32 | "env": {
33 | "node": true
34 | },
35 | "extends": [
36 | "plugin:vue/essential",
37 | "eslint:recommended"
38 | ],
39 | "rules": {},
40 | "parserOptions": {
41 | "parser": "babel-eslint"
42 | }
43 | },
44 | "postcss": {
45 | "plugins": {
46 | "autoprefixer": {}
47 | }
48 | },
49 | "browserslist": [
50 | "> 1%",
51 | "last 2 versions"
52 | ]
53 | }
54 |
--------------------------------------------------------------------------------
/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |

4 |
5 |
6 |
7 |
8 |
18 |
19 |
29 |
--------------------------------------------------------------------------------
/src/content/index.js:
--------------------------------------------------------------------------------
1 | import {
2 | Message,
3 | MessageBox
4 | } from 'element-ui';
5 |
6 | // 通过Chrome插件的API加载字体文件
7 | (function insertElementIcons() {
8 | let elementIcons = document.createElement('style')
9 | elementIcons.type = 'text/css';
10 | elementIcons.textContent = `
11 | @font-face {
12 | font-family: "element-icons";
13 | src: url('${ window.chrome.extension.getURL("fonts/element-icons.woff")}') format('woff'),
14 | url('${ window.chrome.extension.getURL("fonts/element-icons.ttf ")}') format('truetype'); /* chrome, firefox, opera, Safari, Android, iOS 4.2+*/
15 | }
16 | `
17 | document.head.appendChild(elementIcons);
18 | })();
19 |
20 | MessageBox.alert('这是一段内容', '标题名称', {
21 | confirmButtonText: '确定',
22 | callback: action => {
23 | Message({
24 | type: 'info',
25 | message: `action: ${ action }`
26 | });
27 | }
28 | })
--------------------------------------------------------------------------------
/src/manifest.development.json:
--------------------------------------------------------------------------------
1 | {
2 | "manifest_version": 2,
3 | "name": "vue-extension",
4 | "description": "a chrome extension with vue-cli3",
5 | "version": "0.0.1",
6 | "options_page": "options.html",
7 | "browser_action": {
8 | "default_popup": "popup.html"
9 | },
10 | "background": {
11 | "scripts": ["hot-reload.js"]
12 | },
13 | "content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'",
14 | "content_scripts": [{
15 | "matches": [
16 | "*://*.baidu.com/*"
17 | ],
18 | "css": [
19 | "css/content.css"
20 | ],
21 | "js": [
22 | "js/content.js"
23 | ],
24 | "run_at": "document_end"
25 | }],
26 | "web_accessible_resources": ["fonts/*"]
27 | }
--------------------------------------------------------------------------------
/src/manifest.production.json:
--------------------------------------------------------------------------------
1 | {
2 | "manifest_version": 2,
3 | "name": "vue-extension",
4 | "description": "a chrome extension with vue-cli3",
5 | "version": "0.0.1",
6 | "options_page": "options.html",
7 | "browser_action": {
8 | "default_popup": "popup.html"
9 | },
10 | "content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'",
11 | "content_scripts": [{
12 | "matches": [
13 | "*://*.baidu.com/*"
14 | ],
15 | "css": [
16 | "css/content.css"
17 | ],
18 | "js": [
19 | "js/content.js"
20 | ],
21 | "run_at": "document_end"
22 | }],
23 | "web_accessible_resources": ["fonts/*"]
24 | }
--------------------------------------------------------------------------------
/src/options/App/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Hello Options
4 |
5 |
6 |
7 |
12 |
13 |
23 |
--------------------------------------------------------------------------------
/src/options/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Options
8 |
9 |
10 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/src/options/index.js:
--------------------------------------------------------------------------------
1 | import Vue from "vue";
2 | import AppComponent from "./App/App.vue";
3 |
4 | Vue.component("app-component", AppComponent);
5 |
6 | new Vue({
7 | el: "#app",
8 | render: createElement => {
9 | return createElement(AppComponent);
10 | }
11 | });
12 |
--------------------------------------------------------------------------------
/src/popup/App/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 | 卡片名称
8 | 操作按钮
12 |
13 |
18 | {{'列表内容 ' + o }}
19 |
20 |
21 |
22 |
23 |
28 |
29 |
34 |
--------------------------------------------------------------------------------
/src/popup/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Options
8 |
9 |
10 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/src/popup/index.js:
--------------------------------------------------------------------------------
1 | import Vue from "vue";
2 | import AppComponent from "./App/App.vue";
3 |
4 | Vue.component("app-component", AppComponent);
5 |
6 | import {
7 | Card,
8 | Button
9 | } from 'element-ui';
10 |
11 | Vue.use(Card);
12 | Vue.use(Button);
13 |
14 | new Vue({
15 | el: "#app",
16 | render: createElement => {
17 | return createElement(AppComponent);
18 | }
19 | });
--------------------------------------------------------------------------------
/src/utils/hot-reload.js:
--------------------------------------------------------------------------------
1 | // 代码来源:https://github.com/xpl/crx-hotreload/edit/master/hot-reload.js
2 | const filesInDirectory = dir => new Promise(resolve =>
3 | dir.createReader().readEntries(entries =>
4 | Promise.all(entries.filter(e => e.name[0] !== '.').map(e =>
5 | e.isDirectory ?
6 | filesInDirectory(e) :
7 | new Promise(resolve => e.file(resolve))
8 | ))
9 | .then(files => [].concat(...files))
10 | .then(resolve)
11 | )
12 | )
13 |
14 | const timestampForFilesInDirectory = dir =>
15 | filesInDirectory(dir).then(files =>
16 | files.map(f => f.name + f.lastModifiedDate).join())
17 |
18 | const reload = () => {
19 | window.chrome.tabs.query({
20 | active: true,
21 | currentWindow: true
22 | }, tabs => { // NB: see https://github.com/xpl/crx-hotreload/issues/5
23 | if (tabs[0]) {
24 | window.chrome.tabs.reload(tabs[0].id)
25 | }
26 | window.chrome.runtime.reload()
27 | })
28 | }
29 |
30 | const watchChanges = (dir, lastTimestamp) => {
31 | timestampForFilesInDirectory(dir).then(timestamp => {
32 | if (!lastTimestamp || (lastTimestamp === timestamp)) {
33 | setTimeout(() => watchChanges(dir, timestamp), 1000) // retry after 1s
34 | } else {
35 | reload()
36 | }
37 | })
38 | }
39 |
40 | window.chrome.management.getSelf(self => {
41 | if (self.installType === 'development') {
42 | window.chrome.runtime.getPackageDirectoryEntry(dir => watchChanges(dir))
43 | }
44 | })
--------------------------------------------------------------------------------
/src/utils/index.js:
--------------------------------------------------------------------------------
1 | export default function injectUrl(path) {
2 | if (!path) {
3 | throw new Error('not path')
4 | }
5 |
6 | if (/\.js$/.test(path)) {
7 | // 在网页上加载插件内js文件
8 | let script = document.createElement('script');
9 | script.setAttribute('type', 'text/javascript');
10 | // 获得的地址类似:chrome-extension://ihcokhadfjfchaeagdoclpnjdiokfakg/js/inject.js
11 | script.src = window.chrome.extension.getURL(path);
12 | script.onload = function() {
13 | // 放在页面不好看,执行完后移除掉
14 | this.parentNode.removeChild(this);
15 | };
16 | document.head.appendChild(script);
17 | } else if (/\.css$/.test(path)) {
18 | // 在网页上加载插件内css文件
19 | let link = document.createElement('link');
20 | link.href = window.chrome.extension.getURL(path)
21 | link.rel = "stylesheet"
22 | document.head.appendChild(link)
23 | }
24 | }
--------------------------------------------------------------------------------
/vue.config.js:
--------------------------------------------------------------------------------
1 | const CopyWebpackPlugin = require("copy-webpack-plugin");
2 | const ZipPlugin = require('zip-webpack-plugin')
3 | const path = require("path");
4 |
5 | // Generate pages object
6 | const pagesObj = {};
7 | const chromeName = ["popup", "options"];
8 |
9 | chromeName.forEach(name => {
10 | pagesObj[name] = {
11 | entry: `src/${name}/index.js`,
12 | template: "public/index.html",
13 | filename: `${name}.html`
14 | };
15 | });
16 |
17 | // 生成manifest文件
18 | const manifest =
19 | process.env.NODE_ENV === "production" ? {
20 | from: path.resolve("src/manifest.production.json"),
21 | to: `${path.resolve("dist")}/manifest.json`
22 | } : {
23 | from: path.resolve("src/manifest.development.json"),
24 | to: `${path.resolve("dist")}/manifest.json`
25 | };
26 |
27 | const plugins = [
28 | CopyWebpackPlugin([manifest])
29 | ]
30 |
31 | // 开发环境将热加载文件复制到dist文件夹
32 | if (process.env.NODE_ENV !== 'production') {
33 | plugins.push(
34 | CopyWebpackPlugin([{
35 | from: path.resolve("src/utils/hot-reload.js"),
36 | to: path.resolve("dist")
37 | }])
38 | )
39 | }
40 |
41 | // 生产环境打包dist为zip
42 | if (process.env.NODE_ENV === 'production') {
43 | plugins.push(
44 | new ZipPlugin({
45 | path: path.resolve("dist"),
46 | filename: 'dist.zip',
47 | })
48 | )
49 | }
50 |
51 | module.exports = {
52 | pages: pagesObj,
53 | // // 生产环境是否生成 sourceMap 文件
54 | productionSourceMap: false,
55 |
56 | configureWebpack: {
57 | entry: {
58 | 'content': './src/content/index.js'
59 | },
60 | output: {
61 | filename: 'js/[name].js'
62 | },
63 | plugins: plugins,
64 | },
65 | css: {
66 | extract: {
67 | filename: 'css/[name].css'
68 | // chunkFilename: 'css/[name].css'
69 | }
70 | },
71 |
72 |
73 | chainWebpack: config => {
74 | // 处理字体文件名,去除hash值
75 | const fontsRule = config.module.rule('fonts')
76 |
77 | // 清除已有的所有 loader。
78 | // 如果你不这样做,接下来的 loader 会附加在该规则现有的 loader 之后。
79 | fontsRule.uses.clear()
80 | fontsRule.test(/\.(woff2?|eot|ttf|otf)(\?.*)?$/i)
81 | .use('url')
82 | .loader('url-loader')
83 | .options({
84 | limit: 1000,
85 | name: 'fonts/[name].[ext]'
86 | })
87 |
88 | // 查看打包组件大小情况
89 | if (process.env.npm_config_report) {
90 | config
91 | .plugin('webpack-bundle-analyzer')
92 | .use(require('webpack-bundle-analyzer').BundleAnalyzerPlugin)
93 | }
94 | }
95 | };
--------------------------------------------------------------------------------