0%

wokoo脚手架(搭建篇)

油猴插件 & 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

img

  1. 安装 lerna
1
npm i lerna -g
  1. 创建项目目录,初始化
1
2
3
mkdir wokoo
cd wokoo
lerna init
  1. 开启 workspace,在 package.json 中增加workspaces配置
1
2
3
"workspaces": [
"packages/*"
],
  1. 创建子项目
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) // [node路径,脚本路径,参数]
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)}`)
// 初始化package.json
const packageJson = {
name: appName,
version: '0.0.1',
private: true,
scripts: {
start: 'cross-env NODE_ENV=development webpack serve',
build: 'webpack',
},
}
// 写入package.json
fs.writeFileSync(
path.join(root, 'package.json'),
JSON.stringify(packageJson, null, 2)
)
// 改变工作目录,进入项目目录
process.chdir(root)
// 复制项目模板,安装项目依赖等
await run(root, appName)
}

5. run:复制项目模板到当前项目下,生成基础项目

createApp 最后要调用 run 方法。run 主要做了以下几点 👇:

  1. 安装 wokoo-template
1
2
3
4
5
6
7
8
9
10
const templateName = 'wokoo-template' // 对应的wokoo模板
const allDependencies = [templateName]
// 安装wokoo-template包
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)
}
  1. 根据用户选择的模板类型复制相应模板文件到临时文件夹 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`)
// 从wokoo-template中拷贝模板到项目目录
if (fs.existsSync(templatePath)) {
// 将templateDir内模板拷贝到temp文件,并修改模板文件中的ejs配置项
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) // 包装渲染方法

/**
*
* @param {*} fromPath 源路径
* @param {*} toPath 目标路径
*/
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) => {
// result 替换模板内数据
let result = {
license: 'MIT',
version: '0.0.1',
...config,
}
const data = metal.metadata()
Object.assign(data, result) // 将询问的结果放到metadata中保证在下一个中间件中可以获取到
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')
) {
// 如果是md或者txt才有可能是模板
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
  1. 合并 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
// 合并template.json和package.json
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,
}
// 编写package.json
fs.writeFileSync(
path.join(root, 'package.json'),
JSON.stringify(pkgJson, null, 2)
)
fs.unlinkSync(path.join(root, 'template.json')) // 删除template.json文件

// 再次根据dependenciesToInstall执行npm install
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!')}`)
  1. 卸载 wokoo-template
1
await doAction(root, 'wokoo-template', 'uninstall')

流程上的实现介绍完了,下面两个方法是我封装的功能性方法

doAction:使用 npm 安装或卸载项目依赖

使用cross-spawn开启子线程,在子线程中执行npm installnpm 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) // 安装成功后触发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()) {
// path是文件,直接删除
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 中的文件或文件夹才会真正的被发布上去。

  1. 在 wokoo-scripts 的 package.json 的files字段中增加”modifyTemplate.js”
  2. 在 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
lerna publish

使用

1
2
npm i wokoo -g
wokoo my-app

具体使用过程可以阅读油猴脚手架 wokoo 使用说明

都读到这里了,给你鼓个掌吧 👏👏👏