油猴插件 & wokoo 脚手架使用说明
一款油猴插件的脚手架。如果直接开发油猴插件,开发者需要费时搭建 vue 或 react 基础项目,还需要对油猴脚本区域做对应的配置,开发体验差。
wokoo 可以一键式生成基础项目,并且提供基础 Tampermonkey 配置。主要提供的功能有:
- 命令行式创建脚手架初始项目
- 根据用户选择,生成 vue、react 的基本项目
- tampermonkey.js 文件中提供 Tampermonkey 配置
关于油猴插件和 wokoo 的具体使用可以阅读 5 分钟上手开发浏览器插件——油猴脚手架 wokoo
这里是 wokoo 脚手架代码:wokoo 脚手架 github 仓库
我使用 wokoo 开发了MoveSearch(划词搜索插件),欢迎大家使用
wokoo 脚手架的设计参考了create-react-app,我也曾经写过一篇分析 cra 源码的文章,感兴趣的同学可以阅读这篇 👉create-react-app 核心源码解读。
手把手教搭建过程
- lerna: 进行项目管理
- wokoo-scripts: 和用户交互,拉取 wokoo-template,生成对应的初始项目
- wokoo-template: 提供模板来初始化一个有基础配置的油猴项目。模板有两种:react 和 vue
- 安装 lerna
- 创建项目目录,初始化
1 2 3
| mkdir wokoo cd wokoo lerna init
|
- 开启 workspace,在 package.json 中增加
workspaces
配置
1 2 3
| "workspaces": [ "packages/*" ],
|
- 创建子项目
1 2
| lerna create wokoo-scripts lerna create wokoo-template
|
wokoo-scripts 编写
wokoo-scripts 的主要功能有:
- commander 获取 shell 中用户键入的 projectName
- fs.writeFile 创建文件路径
- 安装 wokoo-template 模板
- 读取模板指定后缀文件.md, .js,将 ejs 语法进行替换
- 删除多余内容
- 卸载模板
1.创建入口
进入packages/wokoo-scripts
,创建bin/www
文件
1 2 3
| #! /usr/bin/env node
require('../index.js')
|
修改 package.json,增加bin
字段配置
1 2 3
| "bin": { "wokoo": "./bin/www" }
|
在 wokoo-scripts 下创建 index.js 文件作为项目入口。
2.安装依赖模块
介绍下用到的第三方模块:
安装依赖,添加软链
1 2
| npm install chalk cross-spawn commander fs-extra inquirer metalsmith consolidate ejs -S npm link
|
3.实现 init 方法,读取命令行指令
主要使用commander来读取命令行中用户输入的项目名,此时在命令行执行wokoo my-app
,能够在代码中获取到项目名 my-app
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| const chalk = require('chalk') const spawn = require('cross-spawn') const { Command } = require('commander') const fs = require('fs-extra') const path = require('path') const inquirer = require('inquirer') const packageJson = require('./package.json')
let program = new Command() init()
async function init() { let projectName program .version(packageJson.version) .arguments('<project-directory>') .usage(`${chalk.green(`<project-directory>`)}`) .action((name) => { projectName = name console.log('projectName:', projectName) }) .parse(process.argv) await createApp(projectName) }
|
4.createApp 方法,根据项目名生成项目
在 run 方法中调 createApp 方法,传入 projectName。createApp 主要实现了创建文件夹,写入 package.json 的功能。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| async function createApp(appName) { let root = path.resolve(appName) fs.ensureDirSync(appName) console.log(`create a new app in ${chalk.green(root)}`) const packageJson = { name: appName, version: '0.0.1', private: true, scripts: { start: 'cross-env NODE_ENV=development webpack serve', build: 'webpack', }, } fs.writeFileSync( path.join(root, 'package.json'), JSON.stringify(packageJson, null, 2) ) process.chdir(root) await run(root, appName) }
|
5. run:复制项目模板到当前项目下,生成基础项目
createApp 最后要调用 run 方法。run 主要做了以下几点 👇:
- 安装 wokoo-template
1 2 3 4 5 6 7 8 9 10
| const templateName = 'wokoo-template' const allDependencies = [templateName]
console.log('Installing packages. This might take a couple of minutes') console.log(`Installing ${chalk.cyan(templateName)} ...`) try { await doAction(root, allDependencies) } catch (e) { console.log(`Installing ${chalk.red(templateName)} failed ...`, e) }
|
- 根据用户选择的模板类型复制相应模板文件到临时文件夹 temp,替换其中的 ejs 模板,然后删除临时文件夹 temp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| const repos = ['vue', 'react'] const { targetTemplate } = await inquirer.prompt({ name: 'targetTemplate', type: 'list', message: 'which template do you prefer?', choices: repos, })
const templatePath = path.dirname( require.resolve(`${templateName}/package.json`, { paths: [root] }) )
const scriptsConfigDir = path.join(templatePath, 'webpack.config.js') const gitIgnoreDir = path.join(templatePath, '.npmignore') const publicDir = path.join(templatePath, 'public') const tempDir = path.join(root, 'temp') const templateDir = path.join(templatePath, `${targetTemplate}-template`)
if (fs.existsSync(templatePath)) { await modifyTemplate(templateDir, 'temp', { projectName: appName, basicProject: targetTemplate, })
fs.copySync(tempDir, root) fs.copySync(publicDir, root + '/public') fs.copyFileSync(scriptsConfigDir, root + '/webpack.config.js') fs.copyFileSync(gitIgnoreDir, root + '/.gitignore') deleteFolder(tempDir) } else { console.error( `Could not locate supplied template: ${chalk.green(templatePath)}` ) return }
|
此处,我将复制的功能封装到 modifyTemplate.js 中。利用 MetalSmith 提供的方法遍历源路径下文件,利用 consolidate.ejs 将文件中的 ejs 语法替换后,将内容写入新的临时文件夹 temp 中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
| const MetalSmith = require('metalsmith') let { render } = require('consolidate').ejs const { promisify } = require('util') const path = require('path') render = promisify(render)
async function handleTemplate(fromPath, toPath, config) { await new Promise((resovle, reject) => { MetalSmith(__dirname) .source(fromPath) .destination(path.join(path.resolve(), toPath)) .use(async (files, metal, done) => { let result = { license: 'MIT', version: '0.0.1', ...config, } const data = metal.metadata() Object.assign(data, result) done() }) .use((files, metal, done) => { Reflect.ownKeys(files).forEach(async (file) => { let content = files[file].contents.toString() if ( file.includes('.js') || file.includes('.json') || file.includes('.txt') || file.includes('.md') ) { if (content.includes('<%')) { content = await render(content, metal.metadata()) files[file].contents = Buffer.from(content) } } }) done() }) .build((err) => { if (!err) { resovle() } else { reject(err) } }) }) }
module.exports = handleTemplate
|
- 合并 template.json 和 package.json,生成新的 package.json 并再次执行
npm install
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| let tempPkg = fs.readFileSync(root + '/template.json').toString() let pkg = fs.readFileSync(root + '/package.json').toString() const tempPkgJson = JSON.parse(tempPkg) const pkgJson = JSON.parse(pkg)
pkgJson.dependencies = { ...pkgJson.dependencies, ...tempPkgJson.package.dependencies, } pkgJson.devDependencies = { ...tempPkgJson.package.devDependencies, }
fs.writeFileSync( path.join(root, 'package.json'), JSON.stringify(pkgJson, null, 2) ) fs.unlinkSync(path.join(root, 'template.json'))
const dependenciesToInstall = Object.entries({ ...pkgJson.dependencies, ...pkgJson.devDependencies, }) let newDependencies = [] if (dependenciesToInstall.length) { newDependencies = newDependencies.concat( dependenciesToInstall.map(([dependency, version]) => { return `${dependency}@${version}` }) ) } await doAction(root, newDependencies) console.log(`${chalk.cyan('Installing succeed!')}`)
|
- 卸载 wokoo-template
1
| await doAction(root, 'wokoo-template', 'uninstall')
|
流程上的实现介绍完了,下面两个方法是我封装的功能性方法
doAction:使用 npm 安装或卸载项目依赖
使用cross-spawn开启子线程,在子线程中执行npm install
或 npm uninstall
的命令
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| async function doAction(root, allDependencies, action = 'install') { typeof allDependencies === 'string' ? (allDependencies = [allDependencies]) : null return new Promise((resolve) => { const command = 'npm' const args = [ action, '--save', '--save-exact', '--loglevel', 'error', ...allDependencies, '--cwd', root, ] const child = spawn(command, args, { stdio: 'inherit' }) child.on('close', resolve) }) }
|
deleteFolder: 递归删除文件、文件夹,入参是 path 文件路径
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| function deleteFolder(path) { let files = [] if (fs.existsSync(path)) { if (!fs.statSync(path).isDirectory()) { fs.unlinkSync(path) } else { files = fs.readdirSync(path) files.forEach(function (file) { let curPath = path + '/' + file deleteFolder(curPath) }) fs.rmdirSync(path) } } }
|
wokoo-template 编写
- 分为 vue-template 和 react-template
- vue-template 和 react-template 分别对应 webpack 配置的一个 vue 或 react 基础项目
- 使用 ejs 模板,实现 wokoo-scripts 注入变量
template 相对来说比较简单,使用 webpack+vue 或 react 分别搭建了一个轻量级项目。
具体代码可看 👉wokoo/wokoo-template
发布
在执行lerna publish
之前,先看下自己的项目下用到的文件或文件夹是否在 package.json files
字段中。只有在 files 中的文件或文件夹才会真正的被发布上去。
- 在 wokoo-scripts 的 package.json 的
files
字段中增加”modifyTemplate.js”
- 在 wokoo-template 的 package.json 的
files
字段中增加”react-template”, “vue-template”,”public”,”webpack.config.js”,”.gitignore”
我之前就忘记往 files 字段添加,导致 publish 上去后发现丢文件了。具有问题可阅读:https://stackoverflow.com/questions/27049192/npm-publish-isnt-including-all-my-files
最后一步就大功告成了!🎉
使用
1 2
| npm i wokoo -g wokoo my-app
|
具体使用过程可以阅读油猴脚手架 wokoo 使用说明
都读到这里了,给你鼓个掌吧 👏👏👏