源码阅读-DoKit for Web
本文对应代码截止至2021.5.1
一些第一次接触的包
npm-run-all
一个npm
的库,这个工具是为了解决官方的 npm run
命令无法同时运行多个脚本的问题
这个包提供三个命令,分别是 npm-run-all
run-s
run-p
,其中后两个都是 npm-run-all
带参数的简写,分别对应串行和并行。
lerna
Lerna
是一个管理工具,用于管理包含多个软件包(package
)的 JavaScript
项目。
入门
# 安装
npm install --global lerna
# 创建一个新的仓库代码
git init lerna-repo && cd lerna-repo
# 变为lerna仓库
lerna init
复制代码
你的代码仓库目前应该是如下结构:
my-lerna-repo/
package.json
packages/
package-1/
package.json
package-2/
package.json
复制代码
常用指令
The two primary commands in Lerna are
lerna bootstrap
andlerna publish
.
bootstrap
will link dependencies in the repo together.publish
will help publish any updated packages.
rollup.js
Rollup 是一个 JavaScript 模块打包器,可以将小块代码编译成大块复杂的代码,例如 library 或应用程序。
待补充…
源码分析-组织结构
目录结构是一个标准的lerna
项目的结构,如上文介绍
package.json
首先分析下根目录中的package.json
"scripts": {
"bootstrap": "run-s bootstrap:project bootstrap:package",
"bootstrap:project": "npm install",
"bootstrap:package": "lerna bootstrap",
"build": "lerna run build",
"dev": "lerna run dev --parallel",
"dev:playground": "run-p serve:playground dev",
"serve:playground": "node scripts/dev-playground.js",
"clean": "run-s clean:lerna clean:lock",
"clean:lerna": "lerna clean --yes",
"clean:lock": "run-s clean:lock-package clean:lock-subpackage",
"clean:lock-package": "rm -rf ./package-lock.json",
"clean:lock-subpackage": "rm -rf ./packages/**/package-lock.json"
},
复制代码
运行的指令分别为npm run bootstrap
,npm run build
和npm run dev:playground
前两者都是在安装依赖相关内容,主要运行的部分为
node scripts/dev-playground.js
复制代码
scripts/dev-playground.js
可以看到入口在dev-playground.js
文件中,其代码如下:
const http = require('http');
const serveHandler = require('serve-handler');
const open = require('open');
run();
function run(){
const server = http.createServer((request, response) => {
return serveHandler(request, response);
})
server.listen(3000, () => {
console.log('Dev Server Running at http://localhost:3000');
open('http://localhost:3000/playground');
})
}
复制代码
这一部分是运行了一个server
,并指向了./playground/index.html
文件
/playground/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dokit For Web</title>
</head>
<body>
<h1>Dokit For Web</h1>
<h2>Playground</h2>
</body>
<script src="https://unpkg.com/vue@next"></script>
<script src="../packages/web/dist/dokit.js"></script>
</html>
复制代码
这边是主要的网页入口了,其引入的内容都在web/dist/dokit.js
中
packages/web/
该目录下是一个利用rollup
打包的模块,查看rollup.config.js
可以看到,上文所引用的dokit.js
便是打包的输出文件,输入文件为src/index.js
src/index.js
import {Dokit} from '@dokit/web-core'
import {Features} from './feature'
/*
* TODO 全局注册 Dokit
*/
window.Dokit = new Dokit({
features: Features,
});
复制代码
根据js
内容,继续找到feature.js
和@dokit/web-core
src/feature.js
在这个文件中,可以看到,一共引入了4个插件
import Console from './plugins/console/index'
import AppInfo from './plugins/app-info/index'
import DemoPlugin from './plugins/demo-plugin/index'
import HelloWorld from './components/ToolHelloWorld'
复制代码
然而网页中显示的有很多个插件,继续向下看
export const BasicFeatures = {
title: '常用工具',
list: [Console, AppInfo, DemoPlugin, {
nameZh: 'H5任意门a ',
name: 'h5-door',
icon: 'https://pt-starimg.didistatic.com/static/starimg/img/FHqpI3InaS1618997548865.png',
component: AppInfo
}]
}
export const DokitFeatures = {
title: '平台功能',
list: [{
nameZh: 'Mock数据',
name: 'mock',
icon: 'https://pt-starimg.didistatic.com/static/starimg/img/aDn77poRDB1618997545078.png',
component: HelloWorld
}]
}
export const UIFeatures = {
title: '视觉功能',
list: [{
nameZh: '取色器',
name: 'color-selector',
icon: 'https://pt-starimg.didistatic.com/static/starimg/img/QYUvEE8FnN1618997536890.png',
component: HelloWorld
}, {
nameZh: '对齐标尺',
name: 'align-ruler',
icon: 'https://pt-starimg.didistatic.com/static/starimg/img/a5UTjMn6lO1618997535798.png',
component: HelloWorld
}, {
nameZh: 'UI结构',
name: 'view-selector',
icon: 'https://pt-starimg.didistatic.com/static/starimg/img/XNViIWzG7N1618997548483.png',
component: HelloWorld
}]
}
export const Features = [BasicFeatures, DokitFeatures, UIFeatures]
复制代码
可以看到,DokitFeatures
和UIFeatures
中只是占了位置,实际对应的插件还是HelloWorld
的demo
,最后将这些全部export
出去即可。
packages/core
在上面看到,声明了一个全局组件Dokit
,这个来自于@dokit/web-core
,继续寻找其定义位置。既然是web-core
,那就在core/
文件夹下寻找试试。查看package.json
,该目录下文件的name
就是@dokit/web-core
,其main
文件为dist/index.js
。不难猜出,这个文件仍是rollup
打包出来的文件,查看rollup.config.js
,源文件为src/index.js
。在这里,找到了Dokit
的定义:
import {createApp} from 'vue'
import App from './components/app'
import Store from './store'
import {applyLifecyle, LifecycleHooks} from './common/js/lifecycle'
import {getRouter} from './router'
export class Dokit{
options = null
constructor(options){
this.options = options
let app = createApp(App);
let {features} = options;
app.use(getRouter(features));
app.use(Store);
Store.state.features = features;
this.app = app;
this.init();
this.onLoad();
}
onLoad(){
// Lifecycle Load
applyLifecyle(this.options.features, LifecycleHooks.LOAD)
}
onUnload(){
// Lifecycle UnLoad
applyLifecyle(this.options.features, LifecycleHooks.UNLOAD)
}
init(){
let dokitRoot = document.createElement('div')
dokitRoot.id = "dokit-root"
document.documentElement.appendChild(dokitRoot);
// dokit 容器
let el = document.createElement('div')
el.id = "dokit-container"
this.app.mount(el)
dokitRoot.appendChild(el)
}
}
export * from './store'
export * from './common/js/feature'
export default {
Dokit
}
复制代码
Dokit
的初始化过程中现根据App
创建一个实例,然后加载了两个模板getRouter
和Store
,并把插件Features
传给了Store
。init()
函数中将dokit
插件插入到了文档流中。
按顺序依次查看源码。
components/app.vue
<template>
<div class="dokit-app">
<div class="dokit-entry-btn" v-dragable @click="toggleContainer"></div>
<div class="mask" v-show="showContainer" @click="toggleContainer"></div>
<router-container v-show="showContainer"></router-container>
</div>
</template>
<script>
import dragable from "../common/directives/dragable";
import RouterContainer from './container';
export default {
components: {
RouterContainer
},
directives: {
dragable,
},
data() {
return {
showContainer: false,
};
},
methods: {
toggleContainer() {
this.showContainer = !this.showContainer;
},
},
};
</script>
复制代码
app.vue
里声明了一个组件,其对应的应该是Dokit
的按钮,通过showContainer
来控制是否展示插件容器。这里面又使用了RouterContainer
components/container.vue
<template> <div class="container"> <top-bar :title="title" :canBack="canBack"></top-bar> <div class="router-container"> <router-view v-slot="{ Component }"> <keep-alive> <component :is="Component" /> </keep-alive> </router-view> </div> </div></template><script>import TopBar from "../common/components/top-bar";export default { components: { TopBar }, data(){ return {} }, computed:{ curRoute(){ return this.$router.currentRoute.value }, title(){ return this.curRoute.meta.title || 'Dokit' }, canBack(){ return this.curRoute.name !== 'index' } }, created(){ }}</script>
复制代码
可以看到,这里便是Dokit
的菜单栏组件
store/index.js
接下来继续看Store
的源码
import { Store } from "../common/js/store";const store = new Store({ state: { features: [] }})// 更新全局 Store 数据export function updateGlobalData(key, value){ store.state[key] = value}// 获取当前 Store 数据的状态export function getGlobalData(){ return store.state}export default store
复制代码
看起来这个类使用来进行数据存储
继续看一下Store
的定义
common/js/store.js
import {reactive} from 'vue'const storeKey = 'store'/** * 简易版 Store 实现 * 支持直接修改 Store 数据 */export class Store{ constructor(options){ let {state} = options this.initData(state) } initData(data = {}){ this._state = reactive({ data: data }) } get state(){ return this._state.data } install(app){ app.provide(storeKey, this) app.config.globalProperties.$store = this }}
复制代码
源码里展现出的也的确是存取数据的功能。
router/index.js
继续分析Dokit
引入的组件,下一个是getRouter
组件:
app.use(getRouter(features));
复制代码
这里将features
都传给了getRouter
import { createRouter, createMemoryHistory } from 'vue-router'import {routes, getRoutes} from './routes'export function getRouter(features){ return createRouter({ routes: [...routes, ...getRoutes(features)], history: createMemoryHistory() })}
复制代码
getRouter
接收到features
参数之后,调用getRoutes
函数并进一步创建路由
router/routers.js
import Index from '../components/index'export const routes = [{ path: '/', name: 'index', component: Index}]export function getRoutes(features){ let routes = [] features.forEach(feature => { let {list, title:featureTitle} = feature list.forEach(item => { // TODO 暂时只支持路由方式的插件 let {name, title, component} = item routes.push({ path: `/${name}`, name: name, component: component.component || component, meta: { title: title, feature: featureTitle } }) }) }) return routes}
复制代码
这部分的内容也很容易理解,即对features
里的内容依次生成路由,从而完成不同插件页面的跳转。
可以看到,在头部还引入了一个根目录路由Index
,继续深入分析
components/index.vue
<template> <div class="index-container"> <card v-for="(item, index) in features" :key="index" :title="item.title" :list="item.list" ></card> <version-card :version="version"></version-card> </div></template><script>import TopBar from "../common/components/top-bar";import Card from "../common/components/card";import VersionCard from "../common/components/version";export default { components: { TopBar, Card, VersionCard }, data(){ return { version: '1.3.0' } }, mounted(){ }, computed: { features(){ return this.$store.state.features } }};</script>
复制代码
该组件便是Dokit
的菜单页面了,将features
的内容分别生成一个<card>
。在web/src/features.js
中可以看到,features
中还对不同插件进行了功能分类,每种功能下有若干个插件,那么可以猜想<card>
中会再次遍历list
,然后生成不同插件的按钮。
找到card.vue
:
comon/components/card.vue
<template> <div class="card"> <div class="card-title"> <span class="card-title-text"> {{title}} </span> </div> <div class="item-list"> <div class="item" v-for="(item,index) in list" :key="index" @click="handleClickItem(item)"> <div class="item-icon"> <img class="item-icon-image" :src="item.icon || defaultIcon" /> </div> <div class="item-title">{{item.nameZh || '默认功能'}}</div> </div> </div> </div></template><script>import {DefaultItemIcon} from '../js/icon'export default { props: { title: { default: '专区' }, list: { default: [] } }, data(){ return { defaultIcon: DefaultItemIcon } }, methods: { handleClickItem(item){ this.$router.push({ name: item.name }) } }};</script>
复制代码
的确,代码中对list
进行遍历,然后对每个插件生成一个按钮,与猜想一样。
源码分析-功能实现
目前web
端仅实现了日志
和应用信息
两个功能,其他功能只有demo
模板。
app-info
ToolAppInfo.vue
<template> <div class="app-info-container"> <div class="info-wrapper"> <Card title="Page Info"> <table border="1"> <tr> <td>UA</td> <td>{{ua}}</td> </tr> <tr> <td>URL</td> <td>{{url}}</td> </tr> </table> </Card> </div> <div class="info-wrapper"> <Card title="Device Info"> <table border="1"> <tr> <td>设备缩放比</td> <td>{{ratio}}</td> </tr> <tr> <td>screen</td> <td>{{screen.width}}X{{screen.height}}</td> </tr> <tr> <td>viewport</td> <td>{{viewport.width}}X{{viewport.height}}</td> </tr> </table> </Card> </div> </div></template><script>import Card from '../../common/Card'export default { components: { Card }, data() { return { ua: window.navigator.userAgent, url: window.location.href, ratio: window.devicePixelRatio, screen: window.screen, viewport: { width: document.documentElement.clientWidth, height: document.documentElement.clientHeight } } },}
复制代码
这一部分内容比较简单,都是调用了js
中window
和document
中的内置函数来获取设备信息,通过vue
的数据绑定显示出来
console
该插件功能实现略微复杂,从目录结构就可以看出,分了很多个小模块
console/- css/- js/--- console.js- console-tap.vue- index.js- log-container.vue- log-detail,vue- log-item.vue- main.vue- op-command.vue
复制代码
从index.js
中可以看到:
import Console from './main.vue'import {overrideConsole,restoreConsole} from './js/console'import {getGlobalData, RouterPlugin} from '@dokit/web-core'export default new RouterPlugin({ name: 'console', nameZh: '日志', component: Console, icon: 'https://pt-starimg.didistatic.com/static/starimg/img/PbNXVyzTbq1618997544543.png', onLoad(){ console.log('Load') overrideConsole(({name, type, value}) => { let state = getGlobalData(); state.logList = state.logList || []; state.logList.push({ type: type, name: name, value: value }); }); }, onUnload(){ restoreConsole() }})
复制代码
主要插件为从main.vue
中引入的Console
main.vue
<template> <div class="console-container"> <console-tap :tabs="logTabs" @changeTap="handleChangeTab"></console-tap> <div class="log-container"> <div class="info-container"> <log-container :logList="curLogList"></log-container> </div> <div class="operation-container"> <operation-command></operation-command> </div> </div> </div></template><script>import ConsoleTap from './console-tap';import LogContainer from './log-container';import OperationCommand from './op-command';import {LogTabs, LogEnum} from './js/console'export default { components: { ConsoleTap, LogContainer, OperationCommand }, data() { return { logTabs: LogTabs, curTab: LogEnum.ALL } }, computed:{ logList(){ return this.$store.state.logList || [] }, curLogList(){ if(this.curTab == LogEnum.ALL){ return this.logList } return this.logList.filter(log => { return log.type == this.curTab }) } }, created () {}, methods: { handleChangeTab(type){ this.curTab = type } }}</script>
复制代码
在这里一下次又引入了四个组件,接下来继续依次进行分析
console-tab.vue
<template> <div class="tab-container"> <div class="tab-list"> <div class="tab-item" :class="curIndex === index? 'tab-active': 'tab-default'" v-for="(item, index) in tabs" :key="index" @click="handleClickTab(item, index)" > <span class="tab-item-text">{{ item.name }}</span> </div> </div> </div></template>
复制代码
这一部分为console
上方的tab
栏,根据传进来的tabs
生成所有的tab
log-container.vue
<template> <div class="log-container"> <log-item v-for="(log, index) in logList" :key="index" :value="log.value" :type="log.type" ></log-item> </div></template>
复制代码
该组件为cosole
的log
部分,根据logList
把所有的log
显示出来。显示的方式为log-item
,引用于log-item.vue
log-item.vue
<template> <div class="log-ltem"> <div class="log-preview" v-html="logPreview" @click="toggleDetail"></div> <div v-if="showDetail && typeof value === 'object'"> <div class="list-item" v-for="(key, index) in value" :key="index"> <Detail :detailValue="key" :detailIndex="index"></Detail> </div> </div> </div></template>
复制代码
每个log-item
都按照Detail
的方式显示了出来,
log-detail.vue
<template> <div class="detail-container" :class="[canFold ? 'can-unfold':'', unfold ? 'unfolded' : '']" > <div @click="unfoldDetail" v-html="displayDetailValue"></div> <template v-if="canFold"> <div v-show="unfold" v-for="(key, index) in detailValue" :key="index"> <Detail :detailValue="key" :detailIndex="index"></Detail> </div> </template> </div></template>
复制代码
主要还是用来显示数据用的组件,下面的script
部分根据不同的内容还会改变样式,这里不具体分析了。
op-command.vue
<template> <div class="operation"> <div class="input-wrapper"> <input class="input" placeholder="Command……" v-model="command" /> </div> <div class="button-wrapper" @click="excuteCommand"> <span>Excute</span> </div> </div></template><script>import {excuteScript} from './js/console'export default { data(){ return { command: "" } }, methods: { excuteCommand(){ if(!this.command){ return } let ret = excuteScript(this.command) console.log(ret) } }};</script>
复制代码
这里是console
中输入/执行指令的部分,每一个指令都会传给excuteScript
并执行。
以上便是控制console
UI部分的代码,剩下的部分均来自于js/console
,为具体的逻辑实现部分
js/console.js
export const LogMap = { 0: 'All', 1: 'Log', 2: 'Info', 3: 'Warn', 4: 'Error'}export const LogEnum = { ALL: 0, LOG: 1, INFO: 2, WARN: 3, ERROR: 4}export const ConsoleLogMap = { 'log': LogEnum.LOG, 'info': LogEnum.INFO, 'warn': LogEnum.WARN, 'error': LogEnum.ERROR}export const CONSOLE_METHODS = ["log", "info", 'warn', 'error'] })}export const LogTabs = Object.keys(LogMap).map(key => { return { type: parseInt(key), name: LogMap[key] }})
复制代码
该文件上来先定义了几个常量,LogMap
为console
中展示出来的tab
,LogNum
和ConsoleLogMap
为类型和对应下标之间的映射,CONSOLE_METHODS
为console
中不同的方法类型。LogTabs
则是生成一个tab
列表,生成console-tab
时使用。
export const excuteScript = function(command){ let ret try{ ret = eval.call(window, `(${command})`) }catch(e){ ret = eval.call(window, command) } return ret}
复制代码
这个函数很容易理解,就是将输入的指令执行。
export const origConsole = {}export const noop = () => {}export const overrideConsole = function(callback) { const winConsole = window.console CONSOLE_METHODS.forEach((name) => { let origin = (origConsole[name] = noop) if (winConsole[name]) { origin = origConsole[name] = winConsole[name].bind(winConsole) } winConsole[name] = (...args) => { callback({ name: name, type: ConsoleLogMap[name], value: args }) origin(...args) } })}export const restoreConsole = function(){ const winConsole = window.console CONSOLE_METHODS.forEach((name) => { winConsole[name] = origConsole[name] })}
复制代码
最后一部分则是将浏览器中的console
进行重载,在console
执行的过程中会先调用传入的callback
,然后才执行原函数。原函数保存在了origConsole
中,在restoreConsole
中便是将原始的console
还原的过程。
在index.js
中有一段:
onLoad(){ console.log('Load') overrideConsole(({name, type, value}) => { let state = getGlobalData(); state.logList = state.logList || []; state.logList.push({ type: type, name: name, value: value }); }); },
复制代码
可以看到,传给overrideCOnsole
的callback
便是将每个指令都存到state
中,从而能够将其在log-container
中展示出来。