├── .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 | 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 | 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 | 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 | }; --------------------------------------------------------------------------------