SegmentFault WEB札记最新的文章
2023-06-03T18:21:06+08:00
https://segmentfault.com/feeds/blogs
https://creativecommons.org/licenses/by-nc-nd/4.0/
参照Metamask,钱包端实现简易的Dapp浏览器
https://segmentfault.com/a/1190000043861485
2023-06-03T18:21:06+08:00
2023-06-03T18:21:06+08:00
米花儿团儿
https://segmentfault.com/u/mihuartuanr
0
<blockquote>以<code>react-native@0.71.7</code>环境为例,开发Android应用。</blockquote><h2>MetaMask一键登录</h2><h3>示例Demo</h3><pre><code class="javascript">import {WebView} from 'react-native-webview';
import {useRef} from 'react';
const address = '<personal address>';
const injectedJavaScript = `
window.ethereum = {};
window.ethereum.isMetaMask = true;
window.ethereum.isConnected = function() {
console.log('-----------连接成功后调用---------')
return true
};
window.ethereum.wallet = {};
window.ethereum.wallet.address = '${address}';
window.ethereum.selectedAddress = '${address}';
window.ethereum.request = function(args = {}) {
console.log('---------Dapp交互触发该事件-----------', args)
const { method, params } = args
return window.ethereum.send(method, params)
};
window.ethereum.send = function(method, params) {
console.log('---------send-----------', method, params)
return new Promise(function(resolve, reject) {
window.ReactNativeWebView.postMessage(JSON.stringify({
type: 'bsc',
payload: {
method: method,
params: params,
}
}));
document.addEventListener("message", function(event) {
/**
* wallet端主动调用postMessage触发该事件
* 将webviewRef.current.postMessage回调的event.data作为Promise的值返回
*/
const data = JSON.parse(event.data) || {}
if (data.type === 'ethereum' && data.payload.id === method) {
if (data.payload.error) {
reject(data.payload.error);
} else {
resolve(data.payload.result);
}
}
}, { once: true });
});
};
`;
const BrowserTab = function () {
const webviewRef = useRef(null);
const handleWebViewMessage = async function (event: any) {
/**
* Dapp端交互调用window.ethereum.request时触发
* 根据window.ReactNativeWebView.postMessage传递的不同参数,返回对应的结果
*/
const {data} = event.nativeEvent;
const {type, payload = {}} = JSON.parse(data) || {};
const {method} = payload;
console.log(webviewRef.current);
if (webviewRef.current) {
method === 'eth_requestAccounts' &&
webviewRef.current.postMessage(
JSON.stringify({
type: 'ethereum',
payload: {
id: 'eth_requestAccounts',
result: [address],
},
}),
'*',
);
method === 'eth_chainId' &&
webviewRef.current.postMessage(
JSON.stringify({
type: 'ethereum',
payload: {
id: 'eth_chainId',
result: 5,
},
}),
'*',
);
}
};
return (
<WebView
ref={webviewRef}
source={{uri: 'https://app.uniswap.org/'}}
style={{flex: 1}}
javaScriptEnabled={true}
onMessage={handleWebViewMessage}
injectedJavaScriptBeforeContentLoaded={injectedJavaScript}
/>
);
};
export default BrowserTab;</code></pre><h3>Demo演示</h3><p><img src="/img/bVc8cEA" alt="" title=""></p><h3>流程分析:</h3><ol><li>借助<code>react-native-webview</code>加载<code>Dapp Web</code>;</li><li>绑定<code>webviewRef</code>实例,便于后续通信;</li><li><p>在<code>injectedJavaScriptBeforeContentLoaded</code>内容加载之前注入JS脚本;</p><ul><li><code>Dapp</code>实现会判断<code>isMetaMask</code>环境展示标识</li><li>注入业务相关的符合<a href="https://link.segmentfault.com/?enc=KJk63WY1MiWwGOBGEpCJ8A%3D%3D.2h7IFShNdjuxj3%2FCxUrhkAafIsY%2BNPzd2mZ%2BrUNiHlwOQLzTNXNXCIeG7Hb6QMn%2BKfVuCZ6bfTBSqRJ7J214ewMJqXCyiHUuEn78l2dotZp6JLqwA3gDbGZcPUUU2R%2Bs" rel="nofollow">eip-1102</a>等规范的事件实现,如<code>eth_requestAccounts</code>、<code>signTransaction</code> —— 在<code>Dapp</code>中进行操作,会调用对应的注入方法;</li></ul></li><li><code><WebView></code>绑定<code>onMessage={handleWebViewMessage}</code>,监听<code>Dapp</code>通过<code>window.ReactNativeWebView.postMessage</code>上报的消息;</li><li><p>格式化<code>event.nativeEvent.data</code>数据,根据<code>method</code>、<code>params</code>进行对应的业务代码,通过 <code>webviewRef.current.postMessage</code>将结果返回;</p><ul><li><code>webviewRef.current.postMessage</code>会触发注入脚本<code>window.ethereum.send</code>方法中的<code>document.addEventListener("message", handler)</code>回调逻辑;</li><li><code>Dapp</code>端会通过<code>Promise</code>接收返回结果,执行后续逻辑</li></ul></li></ol><p><strong>Notes:</strong></p><ul><li><p><code>injectedJavaScriptBeforeContentLoaded</code>注入<code>window.ethereum</code>后获取该属性为<code>undefined</code></p><ul><li><a href="https://link.segmentfault.com/?enc=SmJrqEGwLjSPVBfNySGUYA%3D%3D.U9kbGFj4RlddJBdScLsdN%2F9WxnZfbdkirfA4DNg1O7srK%2BY9DhQD8K7x3wnv%2F821EM5S8jUKfc0BOVh4vWyu8oCGKYVI1cnsk5EcHmhPp1s8sAGbE8NOoo5KCI2OCJhGh5O45frgLrIxs%2Bt9jDbYzn3vY%2BS7x7KE2jpoJljRurJHpPHJOr1p5J1h9Mz1AaK3" rel="nofollow">Warning On Android, this may work, but it is not 100% reliable (see #1609 and #1099).</a></li><li>实战演练,发现<code>react-native-webview@11.13.0</code>注入无效,<strong><code>react-native-webview@12.1</code></strong>注入有效</li></ul></li><li>由于平台差异,<code>document.addEventListener("message", handler)</code>在<code>Android</code>和<code>IOS</code>环境中有所不同(<a href="https://link.segmentfault.com/?enc=aBt2mYlVrMZxGob45ItChQ%3D%3D.iKxHzjqd7FCsce1OlFyw%2BVoUVSF70Vsk5YAXsbJGiqqaqTWjxe7A6Ngvbx418vLW%2Bx5%2FALotgz4TMi8z7VQh%2B%2Bl6aiuYX5lV2Q6745edc8U%3D" rel="nofollow">https://github.com/react-native-webview/react-native-webview/issues/356</a>)。</li></ul><h2>Metamask相关代码</h2><ul><li><p><a href="https://link.segmentfault.com/?enc=%2BU6o36CqT%2Bg%2FZj3Ff9091g%3D%3D.ZO29OfA60%2FUDmqmrrP1kQXK33%2BgbxPAlsmNrn2Oa2Cg31%2Fc8CIGH3%2FxpaLDSs3%2BBBV%2FVrWUssWG7IH7Fgp%2BTknvMOBQOJQYKdRzMZH41BvOFTvJgsd%2B%2FIcHwtE1PyRs3BRZBvLkm0fnrG6prJjS%2BFA%3D%3D" rel="nofollow">webView组件渲染相关代码</a><br><img src="/img/remote/1460000043861913" alt="Webview组件" title="Webview组件"><br>其中,<a href="https://link.segmentfault.com/?enc=DQP0OoXnGNvXHemjRVIHng%3D%3D.%2Bk2qqb6qDLjST7cYMgdphm6016g3cbyCS4KNL03aqCFG5kEA%2Fk5rt5MLLgEpVObICRqfvfqICsICG6bYFRpQDe9Cols6fYbHY5EPLUaXYpAGTJ%2FUBKDZS9ZoDI7tcrnuI4wuqAahro3SXQn9N38OSw%3D%3D" rel="nofollow"><code>onMessage</code>代码实现</a>:</p><pre><code>const onMessage = ({ nativeEvent }) => {
let data = nativeEvent.data;
try {
data = typeof data === 'string' ? JSON.parse(data) : data;
if (!data || (!data.type && !data.name)) {
return;
}
if (data.name) {
const origin = new URL(nativeEvent.url).origin;
backgroundBridges.current.forEach((bridge) => {
const bridgeOrigin = new URL(bridge.url).origin;
bridgeOrigin === origin && bridge.onMessage(data);
});
return;
}
} catch (e) {
Logger.error(e, `Browser::onMessage on ${url.current}`);
}
};</code></pre><p><a href="https://link.segmentfault.com/?enc=i99%2FeUE6Nj0wBsRJtOM9nA%3D%3D.Ky%2BFzD%2BLYYSBvrConT3vY20WCD6pEteV%2BuBvHrhoM4wsgRLj9NxWlkMqI7SFMp5y6CPy%2F8c03fpJGpJgZczYHDr%2B8I3AmU%2FIdXFH973VTHLc7N%2F2r7uxSyDOTxgfXVXkT3RHHTXKuN3dGWHZVaYJFQ%3D%3D" rel="nofollow"><code>backgroundBridges</code>又是什么呢?</a></p><pre><code>const initializeBackgroundBridge = (urlBridge, isMainFrame) => {
const newBridge = new BackgroundBridge({
webview: webviewRef, // webview实例,可以通过postMessage向内部渲染的Dapp通信
url: urlBridge,
getRpcMethodMiddleware: ({ hostname, getProviderState }) =>
getRpcMethodMiddleware({
hostname,
getProviderState,
navigation: props.navigation,
// Website info
url,
title,
icon,
// Bookmarks
isHomepage,
// Show autocomplete
fromHomepage,
toggleUrlModal,
// Wizard
wizardScrollAdjusted,
tabId: props.id,
injectHomePageScripts,
}),
isMainFrame,
});
backgroundBridges.current.push(newBridge);
};</code></pre><p><a href="https://link.segmentfault.com/?enc=BK9hp%2F9P2v6dzvojMgjZdQ%3D%3D.yrkNYDZdTrC9rBGm6fL6yyE7p%2FTp1rgYWPZGgwBU5hobWCYHzuqRsvF29lJyhQ07vQ61m9LvFIhP8orbik3qyFaW1%2B5F1j2rzSHPr%2BvyRRMDykN93MSvhsVBS1wKDC0%2FJCpLttCpJ%2FufM71Nop9Ouw%3D%3D" rel="nofollow"><code>getRpcMethodMiddleware</code>的定义</a><br><img src="/img/remote/1460000043861914" alt="RpcMethod" title="RpcMethod"><br>其中,<a href="https://link.segmentfault.com/?enc=H6FCBQrwqbT1ZF5DILvAfQ%3D%3D.6j5kntTVXGkILDrW6tWi7YnBr4%2FyG3Z1s%2FOiCdbHM4XLmdsLsT72ovGap1eDrlyuMbt8BMEKuqQAuPwpwEA4hHLpSrlho%2F%2Fil3zYwWPRKds050OyT%2FQ65Gyei7l%2B6rJeIiAvucaK9MRla2I9R8BOWw%3D%3D" rel="nofollow"><code>rpcMethods</code>定义了一系列符合以太坊协议的方法</a>:<br><img src="/img/remote/1460000043861915" alt="RpcMethod" title="RpcMethod"></p></li></ul><h2>参考文档</h2><p><a href="https://link.segmentfault.com/?enc=jVU00vvqSplHpYw400kZJg%3D%3D.qvLvUe7EatGswLYLN%2FWHAqtI67VzLJMNnLrYg1etjXg3k0635RiM1AJkzqlZmsCwnaMY5O5TqXfo0ELGrBlIHLtwqH3HER8f9SiQREe45ooS6wblBm8jb%2FWKtd9IQwhmieZmoSxUFDCnRc3Nyj4dHA%3D%3D" rel="nofollow">https://stackoverflow.com/questions/75633905/how-to-inject-a-wallet-into-the-browser-from-mobile-react-native</a></p>
WalletConnect钱包落地
https://segmentfault.com/a/1190000043860608
2023-06-03T01:10:23+08:00
2023-06-03T01:10:23+08:00
米花儿团儿
https://segmentfault.com/u/mihuartuanr
1
<h2>前言</h2><p>在加密货币世界中,去中心化应用(decentralized application, DApp)的发展越来越迅速。</p><p>DApp 可以让用户更加自由地管理他们的资产,同时也让开发者更加容易地构建去中心化应用。然而,DApp 在与区块链交互时,往往需要使用钱包进行支付、签名等操作,这就需要 DApp 和钱包之间进行通信。</p><p>WalletConnect 是一种标准化的协议,可以帮助 DApp 和钱包之间进行安全的通信。</p><p><img src="/img/remote/1460000043860610" alt="image.png" title="image.png"></p><h2>什么是WalletConnect?</h2><p>WalletConnect是一种加密货币钱包和DApp之间进行安全通信的协议。其主要目的是让用户在使用DApp时,能够使用自己喜欢的加密货币钱包进行交易,而无需将私钥上传到DApp中。</p><p>WalletConnect分为v1和v2版本,版本主要差异在于通信协议的改变。v1版本使用基于Websocket的通信协议,而v2版本则使用了更加高效的基于扩展传输层安全协议(DTLS)的通信协议。</p><p>其中,WalletConnect v1是WalletConnect协议的第一个版本,本文将深入介绍其原理和实现细节。</p><h2>原理介绍</h2><h3>钱包和 DApp 之间的通信原理</h3><p>钱包和 DApp 之间的通信需要通过一种安全的协议进行。在过去,这种通信往往需要用户手动输入私钥进行签名,存在较大的安全风险。WalletConnect 利用了<strong>类似于 OAuth2 的授权流程</strong>,将钱包和 DApp 的通信过程进行了抽象,使得用户在授权后无需再手动输入私钥。</p><h3>WebSocket协议</h3><p>WebSocket 是一种双向通信协议,可以使客户端和服务器之间实现实时通信。在 WebSocket 中,客户端和服务器可以通过建立长连接进行数据交互。</p><p>WalletConnect 利用 WebSocket协议来实现钱包和 DApp 之间的通信。</p><p>在通信过程中,首先需要建立 WebSocket 连接,然后通过自定义通信来发送授权请求。一旦钱包用户授权,钱包就可以在双方之间进行加密通信,完成支付、签名等操作。</p><h2>实现细节</h2><pre><code> "@walletconnect/client": "^1.8.0",
"@walletconnect/core": "^2.4.4",
"@walletconnect/legacy-types": "^2.0.0",
"@walletconnect/utils": "^2.4.4",
"@walletconnect/web3wallet": "^1.2.0",</code></pre><p>WalletConnect v1的工作原理可以分为两个主要过程:建立连接和通信过程。</p><ol><li>WalletConnect 的核心代码结构</li></ol><p>WalletConnect 的核心代码结构包括客户端和服务器两部分。客户端代码可以在 DApp 中实现,服务器代码则需要部署在独立的服务器上。</p><ol start="2"><li>客户端和服务器之间的消息格式</li></ol><p>WalletConnect协议中消息的格式采用了JSON格式,包括了请求和响应两种类型。每个消息都包含了一个<strong>ID</strong>字段,用于标识消息的唯一性。请求消息包含一个Method字段,用于指示请求的方法,而响应消息则包含了一个Result字段,用于指示响应的结果。</p><ol start="3"><li>WalletConnect 的通信流程</li></ol><p>WalletConnect 的通信流程可以概括为以下几个步骤:</p><ul><li>DApp 向 WalletConnect 服务器发起连接请求,服务器返回连接信息;</li><li>DApp 向钱包发送连接请求,钱包弹出授权窗口,用户选择是否授权;</li><li>如果用户授权,钱包会向 DApp 返回连接信息,双方开始加密通信;</li><li>在通信过程中,DApp 可以向钱包发送请求,钱包可以向 DApp 返回响应。</li></ul><p><img src="/img/remote/1460000043860611" alt="image.png" title="image.png"></p><ol start="4"><li>客户端调用</li></ol><ul><li>Dapp端</li></ul><pre><code> import WalletConnect from "@walletconnect/client";
public connect = async () => {
const bridge = "https://bridge.walletconnect.org";
const connector = new WalletConnect({ bridge, qrcodeModal: QRCodeModal });
if (!connector.connected) {
await connector.createSession();
}
await this.subscribeToEvents();
}</code></pre><ul><li>Wallet端</li></ul><pre><code> import LegacysignClientV1 from '@walletconnect/client';
signClientV1 = new LegacysignClientV1({
uri,
clientMeta: METADATA,
});</code></pre><ol start="5"><li>WebSocket 的实现方式</li></ol><ul><li>WalletConnect 类</li></ul><pre><code>// packages\clients\client\src\index.ts
class WalletConnect extends Connector {
constructor(connectorOpts: IWalletConnectOptions, pushServerOpts?: IPushServerOptions) {
super({
cryptoLib,
connectorOpts,
pushServerOpts,
});
}
}</code></pre><ul><li>Connector 类</li></ul><pre><code> // packages\clients\core\src\index.ts
constructor(opts: IConnectorOpts) {
if (!opts.connectorOpts.bridge && !opts.connectorOpts.uri && !opts.connectorOpts.session) {
throw new Error(ERROR_MISSING_REQUIRED);
}
// Dapp端入参处理
if (opts.connectorOpts.bridge) {
this.bridge = getBridgeUrl(opts.connectorOpts.bridge);
}
// Wallet端入参处理
if (opts.connectorOpts.uri) {
this.uri = opts.connectorOpts.uri;
}
// WebSocket建立
this._transport =
opts.transport ||
new SocketTransport({
protocol: this.protocol,
version: this.version,
url: this.bridge,
subscriptions: [this.clientId],
});
// 钩子监听
this._subscribeToInternalEvents();
// WebSocket事件监听
this._initTransport();
// Wallet端建立通信
if (opts.connectorOpts.uri) {
this._subscribeToSessionRequest();
}
}</code></pre><ul><li>访问器属性</li></ul><pre><code> get uri() {
const _uri = this._formatUri();
return _uri;
}
set uri(value) {
if (!value) {
return;
}
const { handshakeTopic, bridge, key } = this._parseUri(value);
this.handshakeTopic = handshakeTopic;
this.bridge = bridge;
this.key = key;
}</code></pre><ul><li>Websocket建立</li></ul><pre><code>// packages\helpers\socket-transport\src\index.ts
constructor(private opts: ISocketTransportOptions) {
this._netMonitor = opts.netMonitor || new NetworkMonitor();
this._netMonitor.on("online", () => this._socketCreate());
}
private _socketCreate() {
const url = getWebSocketUrl(this._url, this._protocol, this._version);
this._nextSocket = new WS(url);
if (!this._nextSocket) {
throw new Error("Failed to create socket");
}
this._nextSocket.onmessage = (event: MessageEvent) => this._socketReceive(event);
this._nextSocket.onopen = () => this._socketOpen();
this._nextSocket.onerror = (event: Event) => this._socketError(event);
}</code></pre><ul><li>可访问的关键方法</li></ul><pre><code>// Dapp调用
public async connect(opts?: ICreateSessionOptions): Promise<ISessionStatus> {
// Dapp调用
public async createSession(opts?: ICreateSessionOptions): Promise<void> {
}
// Wallet调用
public approveSession(sessionStatus: ISessionStatus) {
}
// Wallet调用
public rejectSession(sessionError?: ISessionError) {
}
// Wallet调用
public updateSession(sessionStatus: ISessionstatus) {
}
// Wallet调用
public async killSession(sessionError?: ISessionError){
}
// Dapp调用
public async sendTransaction(tx: ITxData) {
}
// Dapp调用
public async signTransaction(tx: ITxData) {
}
// Dapp调用
public async signMessage(params: any[]) {
}
// Dapp调用
public async signPersonalMessage(params: any[]) {
}
// Dapp调用
public async signTypedData(params;any[]){
}</code></pre><p><img src="/img/remote/1460000043860612" alt="image.png" title="image.png"></p><h2>Wallet扫码授权Dapp</h2><h3>v1</h3><h4>初始化签名对象实例&监听通信事件</h4><pre><code>import LegacysignClientV1 from '@walletconnect/client';
async function createV1SignClient({ uri } = { uri: '' }) {
if (uri) {
clearAllSessionsForV1();
signClientV1 = new LegacysignClientV1({
uri,
clientMeta: METADATA,
});
if (!signClientV1.connected) {
await signClientV1.createSession();
}
} else {
return;
}
/**
* 连接申请 —— wallet扫Dapp二维码后,建立通信时触发
*/
signClientV1.on('session_request', (error, payload) => {
if (error) {
throw new Error(`legacysignClientV1 > session_request failed: ${error}`);
}
const { params = [] } = payload;
const [peer = {}] = params;
});
signClientV1.on('connect', () => {
// 成功与Dapp建立通信的事件通知
console.log('legacysignClientV1 > connect');
});
signClientV1.on('error', (error) => {
throw new Error(`legacysignClientV1 > on error: ${error}`);
});
signClientV1.on('call_request', (error, payload) => {
// 签名事件调用通知
if (error) {
throw new Error(`legacysignClientV1 > call_request failed: ${error}`);
}
});
signClientV1.on('disconnect', async () => {
// 断开的事件监听
clearAllSessionsForV1();
});
}</code></pre><h4>通信授权</h4><ul><li><p>同意授权</p><pre><code>const approveSessionV1 = async () => {
try {
await signClientV1?.approveSession({
accounts: [consumerAddress],
chainId: consumer.chainId, // required
});
} catch (e) {
disconnectSessionForV1();
}
};</code></pre></li><li><p>拒绝授权</p><pre><code>const rejectSessionV1 = async () => {
try {
await signClientV1?.rejectSession({
message: 'USER_REJECTED_METHODS',
});
} catch (e) {
disconnectSessionForV1();
}
};</code></pre><h4>签名请求处理</h4></li><li><p>签名授权</p><pre><code>const onCallRequestV1 = async (payload: {
id: number;
method: string;
params: any[];
}) => {
const { id, params, method } = payload;
let response = {
id,
} as any;
if (method === EIP155_SIGNING_METHODS.WALLET_SWITCHETHEREUMCHAIN) {
// 网络切换
signClientV1?.updateSession({
accounts: [consumerAddress],
chainId: params[0].chainId, // required
});
return;
}
switch (method) {
case EIP155_SIGNING_METHODS.ETH_SIGN:
case EIP155_SIGNING_METHODS.PERSONAL_SIGN:
response.result = await walletConnectHelper.signEip192Message({
address,
params,
});
break;
case EIP155_SIGNING_METHODS.ETH_SIGN_TYPED_DATA:
case EIP155_SIGNING_METHODS.ETH_SIGN_TYPED_DATA_V3:
case EIP155_SIGNING_METHODS.ETH_SIGN_TYPED_DATA_V4:
response.result = await walletConnectHelper.signEip712Message({
address,
params,
});
break;
case EIP155_SIGNING_METHODS.ETH_SEND_TRANSACTION:
try {
response.result = await walletConnectHelper.signAndSendEip155Tx({
params,
address,
});
} catch (e: any) {
const errorText = e.message || 'Error occurs';
Toast.error({
msg: errorText,
});
response.error = {
code: 400,
message: errorText,
};
signClientV1?.rejectRequest(response);
return;
}
break;
case EIP155_SIGNING_METHODS.ETH_SIGN_TRANSACTION:
response.result = await walletConnectHelper.signEip155Tx({
params,
address,
});
break;
default:
alert(`${payload.method} is not supported for WalletConnect v1`);
}
signClientV1?.approveRequest(response);
};</code></pre></li><li><p>拒绝签名</p><pre><code>const rejectRequestV1 = async () => {
try {
await signClientV1?.rejectRequest({
id: proposal.id,
error: {
message: 'USER_REJECTED_METHODS',
},
});
} catch (e) {
disconnectSessionForV1();
}
};</code></pre><h4>主动断开</h4><pre><code>const disconnectSessionForV1 = () => {
signClientV1?.killSession();
clearAllSessionsForV1();
};</code></pre></li></ul><h3>v2</h3><h4>初始化签名对象实例&监听通信事件</h4><pre><code>import { Core } from '@walletconnect/core';
import { Web3Wallet, IWeb3Wallet } from '@walletconnect/web3wallet';
async function createV2SignClient() {
const core = new Core({
projectId: PROJECT_ID,
});
signClientV2 = await Web3Wallet.init({
core,
metadata: METADATA,
});
/**
* 连接申请 —— 扫码后触发
*/
signClientV2.on('session_proposal', connectApproveHandlerV2);
/**
* 成功与Dapp建立通信后,签名事件调用通知
*/
signClientV2.on('session_request', (event) => {
});
/**
* 通信断开的通知
*/
signClientV2.on('session_delete', disconnectSessionForV2);
}</code></pre><h4>配对</h4><pre><code>async function pairPeerForV2({ uri } = { uri: '' }) {
try {
signClientV2.core.pairing.pair({
uri,
activatePairing: true,
});
} catch (e) {
console.warn('---------------logger: walletConnect v2 connect error', e);
}
}</code></pre><h4>通信授权</h4><ul><li><p>同意授权</p><pre><code>const approveSessionV2 = async () => {
const {
id,
params: { requiredNamespaces, pairingTopic },
} = proposal;
var n = Object.keys(requiredNamespaces).reduce((acc, cur) => {
const { methods, events } = requiredNamespaces[cur];
acc[cur] = {
methods,
events,
accounts: requiredNamespaces[cur].chains.map(
(v: string) => v + `:${consumerAddress}`
),
};
return acc;
}, {} as any);
try {
await signClientV2?.approveSession({
id,
namespaces: n,
});
} catch (e) {
disconnectSessionForV2();
}
};</code></pre></li><li><p>拒绝授权</p><pre><code>const rejectSessionV2 = async () => {
const { id } = proposal;
try {
await signClientV2?.rejectSession({
id,
reason: getSdkError('USER_REJECTED_METHODS'),
});
} catch (e) {
disconnectSessionForV2();
}
};</code></pre><h4>签名请求处理</h4></li><li><p>签名授权</p><pre><code>const onCallRequestV2 = async (event: any) => {
const {
id,
params: { request },
topic,
} = event;
const { method, params } = request;
let signature = '' as any;
switch (method) {
case EIP155_SIGNING_METHODS.ETH_SIGN:
case EIP155_SIGNING_METHODS.PERSONAL_SIGN:
signature = await walletConnectHelper.signEip192Message({
address,
params,
});
break;
case EIP155_SIGNING_METHODS.ETH_SIGN_TYPED_DATA:
case EIP155_SIGNING_METHODS.ETH_SIGN_TYPED_DATA_V3:
case EIP155_SIGNING_METHODS.ETH_SIGN_TYPED_DATA_V4:
signature = await walletConnectHelper.signEip712Message({
address,
params,
});
break;
case EIP155_SIGNING_METHODS.ETH_SIGN_TRANSACTION:
signature = await walletConnectHelper.signEip155Tx({
params,
address,
});
break;
case EIP155_SIGNING_METHODS.ETH_SEND_TRANSACTION:
try {
signature = await walletConnectHelper.signAndSendEip155Tx({
params,
address,
});
} catch (e: any) {
const errorText = e.message || 'Error occurs';
Toast.error({
msg: errorText,
});
signClientV2?.respondSessionRequest({
topic,
response: formatJsonRpcError(id, errorText),
});
return;
}
break;
case SOLANA_SIGNING_METHODS.SOLANA_SIGN_MESSAGE:
signature = await walletConnectHelper.signSolanaMessage({
params,
address,
});
break;
case SOLANA_SIGNING_METHODS.SOLANA_SIGN_TRANSACTION:
signature = await walletConnectHelper.signSolanaTx({
params,
address,
});
break;
default:
alert(`${method} is not supported for WalletConnect v2`);
}
const response = formatJsonRpcResult(id, signature);
await signClientV2.respondSessionRequest({
topic,
response,
});
};</code></pre></li><li><p>拒绝签名</p><pre><code>const rejectRequestV2 = async () => {
const { id } = proposal;
try {
await signClientV2?.rejectSession({
id,
reason: getSdkError('USER_REJECTED_METHODS'),
});
} catch (e) {
disconnectSessionForV2();
}
};</code></pre><h4>主动断开</h4><pre><code>const disconnectSessionForV2 = () => {
const activeSessions = signClientV2?.getActiveSessions();
activeSessions &&
Object.keys(activeSessions).forEach((t) => {
signClientV2.disconnectSession({
topic: t,
reason: walletConnectHelper.getSdkError('USER_DISCONNECTED'),
});
});
};</code></pre></li></ul>
React Native开发初体验
https://segmentfault.com/a/1190000043856172
2023-06-01T23:02:21+08:00
2023-06-01T23:02:21+08:00
米花儿团儿
https://segmentfault.com/u/mihuartuanr
0
<h2>环境安装</h2><p>参见官网:</p><p><a href="https://link.segmentfault.com/?enc=Fa09iPiNVbDsaNX8CaKSgA%3D%3D.vrFoKTyFWuGNji0UhRAiLqaYU2jsk4ruBo3TLiSB6Faf7JsnhlNwWjgYdoV728%2Fp" rel="nofollow">https://reactnative.dev/docs/environment-setup</a></p><p><a href="https://link.segmentfault.com/?enc=8pukOYnzidwHR8MKt0aH%2BQ%3D%3D.ksCUf2%2FjDBO602MYqZm5piLnZmhTBQ7cYoP7mIkfDogc3xAKW1jXm6vrS3LAlEu9" rel="nofollow">https://reactnative.cn/docs/environment-setup</a></p><p><strong>Notes</strong>:</p><ul><li>针对依赖Node核心的包,RN没有进行处理,需要借助<a href="https://link.segmentfault.com/?enc=Mp79jOSJaiAHtfsTHIEUEQ%3D%3D.jC%2BwnJtUCVDnipEPrEEsV%2BPKLiq1Dydq84YPr1hlbcJlyOJzm%2B%2FYO2e2OZkQOgWr" rel="nofollow">rn-nodeify</a>处理,e.g. <a href="https://link.segmentfault.com/?enc=L6pzJge0%2F6lZh9BHY0CqTA%3D%3D.LJdGtbW4TK%2F5B0FpPjQzS8KWD7JhAfd7SG%2BkfJyd7uneMB6QxJSv6e5KAxLRmDpq8a%2BPPql4n6GVWkL0obuQwg%3D%3D" rel="nofollow">https://github.com/mvayngrib/react-native-crypto</a></li><li>针对Java依赖的版本问题,可以借助<a href="https://link.segmentfault.com/?enc=HsY3Id%2BmjWk%2BmtfQJW%2FGfQ%3D%3D.HdrEFVrkzQHEAmnM9M5O%2BmHEDpU3WqEYQ%2BmhM9NNoY3FSeHdw3i36Bnbabt9eNcu" rel="nofollow">jetifier</a>自动解决大部分的版本差异</li><li><p>针对Java依赖版本问题,可以通过<a href="https://link.segmentfault.com/?enc=G6SURTO0p4Nin8DOMrnIDQ%3D%3D.NEA37j7a4xLJRVoVYJxJpSmIVYxDKpwZHdu%2FQV6CYrMp0IycCAYDQm0rv5H%2BilJW" rel="nofollow">patch-package</a>打补丁</p><ul><li>打出的补丁会包含很多无用的部分,可以选择性删除</li><li><p>打出的补丁可能二次修改,e.g.<a href="https://link.segmentfault.com/?enc=OaiNC4KWR91wXM15ciM8xA%3D%3D.%2BfbuwMbs7Rzi9J%2FCMzW3kxJq8QYpRfmbk9rVLkqe5t3kmquSM5oQbl23SK5PDlfS%2BbGUq7obR9rQY2qQaALKk9%2Bt74oL3BvFpcrf%2Bq%2FQrLE%3D" rel="nofollow">https://github.com/browserify/pbkdf2/blob/master/lib/default-encoding.js</a></p><ul><li>源码中<code>global.process && global.process.version</code>,安装后<code>global.process && global."v16.13.0"</code>,<code>patches/pbkdf2+3.1.2.patch</code>需要二次修改</li></ul></li></ul></li></ul><h2>项目创建</h2><p><code>npx react-native@latest init rnProject</code></p><p><strong>Notes</strong>:项目名称只支持驼峰,不支持连字符。</p><h2>项目运行</h2><p><code>npm run android</code></p><ul><li>往模拟器或真机安装APK:包运行所需要的资源。</li><li>引用原生依赖,需要重新运行该命令。</li><li><code>--mode=release</code>临时打包,输出路径<code>android\app\build\outputs\apk\release</code></li></ul><p><code>npm run start</code></p><ul><li>运行<code>Metro</code>打包JS代码,启动热更新,在模拟器或真机实时查看改动。</li><li><code>--reset-cache</code>清除Metro缓存,重新编译JS代码。</li></ul><h2>清除运行缓存</h2><ul><li><p><code>"./gradlew" clean</code></p><ul><li>切换到<code>android</code>目录下,在命令行执行该命令,可清除<code>gradle</code>缓存。</li></ul></li><li><p><code>npx react-native start --reset-cache</code></p><ul><li>清除Metro缓存,重新编译JS代码</li><li>应用场景:环境报错、开发者工具出现问题</li></ul></li></ul><h2>页面适配</h2><p>方案一:</p><pre><code>// src/utils/px2dp.js
import { Dimensions, PixelRatio, StyleSheet } from 'react-native';
const windowWidth = Dimensions.get('window').width;
const px2dp = function (px) {
if (!isNaN(px)) {
return (windowWidth / 1080) * px / PixelRatio.get()
} else {
return 0
}
}
export default px2dp
// entry.js
import px2dp from '../utils/px2dp';
StyleSheet.create({
pageTitle: {
fontSize: px2dp(64),
lineHeight: px2dp(85),
paddingLeft: px2dp(100),
paddingRight: px2dp(100),
marginTop: px2dp(80),
},
})</code></pre><p>方案二:</p><pre><code>
import React from 'react';
import {
StyleSheet,
Text,
Dimensions,
PixelRatio,
View,
} from 'react-native';
const { width: layoutWidth, height: layoutHeight } = Dimensions.get('window') // 获取到设备pd
const ratio = PixelRatio.get() // 像素密度
const pixelWidth = PixelRatio.getPixelSizeForLayoutSize(layoutWidth) // pd转px
const pixelHeight = PixelRatio.getPixelSizeForLayoutSize(layoutHeight) // pd转px
const designWidth = 950 // 设计图尺寸
/**
* 设备以px展示,以1/ratio缩放,通过translateX、translateY重置transform原点
* | 设计图 | 设备 |
* | 20px | x |
* | 950px | pixelWidth |
* 同等占比:x = 20 * pixelWidth / 950
* ============================================
* 结合缩放比例:
* scale = pixelWidth / (designWidth * ratio)
* 其余组件可按设计图设置像素即可
*/
const styles = StyleSheet.create({
adapter: {
width: pixelWidth,
height: pixelHeight,
transform: [
{
translateX: -pixelWidth * 0.5,
},
{
translateY: -pixelHeight * 0.5
},
{
scale: pixelWidth / (ratio * designWidth)
},
{
translateX: pixelWidth * 0.5,
},
{
translateY: pixelHeight * 0.5
}
]
},
fullscreen: {
width: designWidth,
height:90,
backgroundColor: 'red'
},
halfscreen: {
width: designWidth / 2,
height:90,
backgroundColor: 'green'
},
quaterscreen: {
width: designWidth / 4,
height:90,
backgroundColor: 'blue'
}
});
function App(): JSX.Element {
return (
<View style={styles.adapter}>
<View style={styles.fullscreen}>
<Text>1</Text>
</View>
<View style={styles.halfscreen}>
<Text>2</Text>
</View>
<View style={styles.quaterscreen}>
<Text>3</Text>
</View>
</View>
);
}
export default App;</code></pre><p>方案二优化:</p><blockquote>方案二虽然能展示正常,但在<code>android Studio</code>中看到的布局并不是可视宽度,现将容器宽高设置为对应的设计图尺寸即可。</blockquote><pre><code>const designWidth = 1080;
function useAdaptationStyle() {
const {
width: layoutWidth,
scale: ratio,
height: layoutHeight,
} = useWindowDimensions();
const {height: screenHeight} = Dimensions.get('screen');
const designHeight = (screenHeight * designWidth) / layoutWidth;
const pixelWidth = PixelRatio.getPixelSizeForLayoutSize(layoutWidth);
const pixelHeight = PixelRatio.getPixelSizeForLayoutSize(screenHeight);
return StyleSheet.create({
container: {
backgroundColor: 'blue',
width: designWidth,
height: designHeight,
transform: [
{
translateX: -designWidth * 0.5,
},
{
translateY: -designHeight * 0.5,
},
{
scale: pixelWidth / (ratio * designWidth),
},
{
translateX: designWidth * 0.5,
},
{
translateY: designHeight * 0.5,
},
],
},
});
}</code></pre><h2>结构&样式</h2><h3>原生组件:</h3><blockquote><a href="https://link.segmentfault.com/?enc=382Apy186DmgMUgZOpuBtw%3D%3D.dgIVjHvC%2BSKDjBQ7kJo2MLV%2BISjC5Y%2FLxsNeLctRHYfUuuz9mXd35jfbw9ot50z63ci5aafENL0j4l5SfAxNQw%3D%3D" rel="nofollow">https://reactnative.dev/docs/components-and-apis</a></blockquote><ul><li><p>只有特定的组件才有交互样式与事件,如<code>Button</code>、<code>TouchableHighlight</code>、<code>TouchableOpacity</code>,其余组件无法绑定<code>onPress</code>事件</p><ul><li>一般不选用<code>Button</code>,而使用自定义Button组件,因为原生<code>Button</code>样式不好调节</li></ul></li><li>不支持svg,需要借助第三方库,e.g.<code>react-native-svg</code>和<code>react-native-svg-transformer</code></li><li>结构搭建:将HTML的标签用法完全忘记,重新根据文档学习使用方法。</li></ul><h3>样式:</h3><blockquote><a href="https://link.segmentfault.com/?enc=gxArcdtZNVoFpahukBaPDQ%3D%3D.HYLgf9RAeClKU9LK73xshfVOB6FuxCjcVRfvSBgW%2BjrGn6%2BqhDrLE4b7B7pYeudK" rel="nofollow">https://reactnative.dev/docs/image-style-props</a></blockquote><ul><li>只能使用组件规范的样式,否则不起作用</li><li><code>lineHeight</code>不能使文字居中,请使用<code>justifyContent</code></li><li>针对<code>Text</code>组件,需要单独定义相关样式,不会继承父级非<code>Text</code>组件<code>Text</code>样式,e.g.不会从父级继承<code>color</code></li><li>不支持渐变色、投影等效果,需要借助第三方库,e.g.<code>react-native-linear-gradient</code>、<code>react-native-shadow-2</code></li></ul><p><strong>Notes</strong>:第三方库的安装,需要重新启动项目,否则,会报模块找不到。</p><h3>嵌套ScrollView</h3><ul><li><code>flatlist</code>嵌套在<code>scrollView</code>无法滚动,<a href="https://link.segmentfault.com/?enc=GxL9XfVDQRrZEWCBD5dwnA%3D%3D.gYCBesqNGw0N7JvjZpBUC4JdLXahuLsxJ06o9vDZgjM5SlAK2Tkg%2BlG2K2bnSRrATiJgDHIGW1shBhpmjymQ3ARHNT%2FR1zOcIIsxQG7gyhawHMwDkSEgiV%2BA%2B0iZnf3I" rel="nofollow">父子级都需要设置nestedScrollEnabled属性</a></li><li><p><code>scrollView</code>必须设定一个高度,否则会使用默认高度,并非由内容撑开。</p><ul><li>注意<code>style</code>与<code>contentContainerStyle</code>的区别</li><li><code>scrollView</code>高度设定:专设<code>View</code>组件包裹,<code>scrollView</code>高度设置为<code>flex:1</code></li></ul></li></ul><h2>代码调试</h2><h3><a href="https://link.segmentfault.com/?enc=myJOeRKEe1M6T68ZUQfNyw%3D%3D.iEVnk3C7A3fO9kQTRmQnSMiLkpP2ysx%2FVbalDjy1DEshWyXkAWQevMboohZ%2FJvYCG6lq0YflF1%2FQabAgAEz84A%3D%3D" rel="nofollow">react-native-debugger的使用</a></h3><ul><li><p>连接<code>react-native-debugger</code>,需要应用程序开启Debug模式</p><ul><li>真机:摇一摇手机,出现操作面板,选择<code>Debug</code></li></ul></li></ul><p><img src="/img/remote/1460000043856174" alt="开启Debug模式" title="开启Debug模式"></p><ul><li><p>模拟器:模拟器聚焦后,使用<code>Ctrl + m</code>打开操作面板</p><ul><li>双击<code>r</code>是<code>reload</code></li></ul></li><li><p>使用<code>react-native-debugger</code>时,如果发出网络请求,可能会在<code>Network</code>面板发现没有<code>request</code>发起,请求(成功/失败)回调没有执行。</p><ul><li><p>在<code>react-native-debugger</code><em>非Chrome控制台</em>面板中右键,开启<code>Enable Network Inspect</code></p><p><img src="/img/remote/1460000043856175" alt="开启网络审查" title="开启网络审查"></p></li></ul></li><li>使用<code>react-native-debugger</code>时,如果发现<code>Components</code>面板始终空白,使用<code>npx react-native start --reset-cache</code>清除缓存启动项目</li><li><code>react-native-debugger</code>的使用,需要<strong>同版本</strong>的<code>react-devtools</code>和<code>react-devtools-core</code>作为开发依赖。</li></ul><h3>当前实例获取</h3><p>类同Chrome开发者工具,在RN任意调试工具的Console面板:</p><ul><li>通过<code>$reactNative</code>等同于<code>import $reactNative from "react-native"</code>,可以使用<code>react-native</code>库中的方法。</li></ul><p><img src="/img/remote/1460000043856176" alt="image.png" title="image.png"></p><ul><li>通过<code>$r</code>可以获取当前选中节点实例</li></ul><p><img src="/img/remote/1460000043856177" alt="image.png" title="image.png"></p><h3>多种调试工具切换</h3><p>在模拟器中开启<code>debug</code>后,调试每次都会自动默认打开<code>debugger-ui</code>页面,如何关闭:</p><ol><li>在启动项目<code>npm run start</code>的命令行中使用<code>shift + d</code>切换到模拟器并打开调试面板,此时点击<code>debug</code>面板选项不再自动调起<code>debugger-ui</code>页面。</li><li>通过取消选中小勾号来解决它<code>Maintain Priority</code></li><li>避免直接在模拟器中通过<code>Ctrl + M</code>唤起面板,选择<code>debug</code>,这样会默认调起<code>debugger-ui</code></li></ol><h2>UI审查</h2><p>开启<code>Android Studio</code>:</p><ul><li>通过右下角的<code>Layout Inspector</code>,选择模拟器或真机。</li></ul><p><img src="/img/remote/1460000043856178" alt="UI审查:选择设备" title="UI审查:选择设备"></p><ul><li>点击结构,可以查看组件属性</li><li>可以通过该面板的右上角设置,切换单位。</li></ul><p><img src="/img/remote/1460000043856179" alt="UI审查" title="UI审查"></p><ul><li>可以通过给组件赋值<code>testID</code>、<code>aria-label</code>,进行组件识别。</li></ul><h2>日志查看</h2><p>开启<code>Android Studio</code>:</p><ul><li>通过下方的<code>logcat</code>可以设备日志,可用于分析应用程序崩溃原因。</li><li>可进行日志筛选,e.g.<code>package:com.awesomeproject level:error</code></li></ul><p><img src="/img/remote/1460000043856180" alt="日志审查" title="日志审查"></p><p><a href="https://link.segmentfault.com/?enc=GZAOpjK5P%2FAHeXBFEBWc9A%3D%3D.EkQ7bFG1koqwh4qN9yyge5IMnLFGuZM9v31hB5dErVnH3J9PCWtSDaY2MP3DrnNR%2FQArFN659rJt4eg0xjvRd%2FyXbTm8a8RSUdW8GDoo3AE%3D" rel="nofollow">#筛选条件列表#</a></p><h2>外部字体</h2><p><a href="https://link.segmentfault.com/?enc=IAN499mJNC%2FiD2M1H5Gd0Q%3D%3D.2R7PB7F9T%2FPNiLNUa7Uhh%2BgyFFHUzaA9mn9K%2Fg6WKVParWxMhqKF%2FA%2BtQXqvH8%2BN" rel="nofollow">https://www.jianshu.com/p/6000eb97d53b</a></p><ul><li>不像H5一样,针对不同的<code>font-weight</code>设置<code>@</code>`font-face`</li></ul><pre><code>@font-face { font-family: "Sans"; font-weight: 100; src: url("../assets/fonts/Thin.otf");}
@font-face { font-family: "Sans"; font-weight: 200; src: url("../assets/fonts/Light.otf");}
@font-face { font-family: "Sans"; font-weight: 400; src: url("../assets/fonts/Normal.otf");}</code></pre><p>只能通过<strong>字体文件名</strong>设置不同的<code>fontFamily</code>:</p><pre><code>// font-weight:100
f100: {
fontFamily: 'Thin' // 字体文件名
}
// font-weight:200
f200: {
fontFamily: 'Light' // 字体文件名
}
// font-weight:300
f300: {
fontFamily: 'Normal' // 字体文件名
}</code></pre><h2>动画</h2><ol><li><code>toValue</code>只能定义整型,如果动画需要其他类型的值,需要使用<code>interpolate</code></li><li>无法对svg中的circle使用transform定义动画,只能通过View或其他元素包裹,实现旋转动画</li><li>如果页面内有大量运算,==<code>Animated</code><strong>会被阻塞</strong>,因为<code>Animated</code>是在JS线程运行,而非UI线程(<a href="https://link.segmentfault.com/?enc=xaIdizkcSe%2BeM9htj9p%2Fng%3D%3D.iOro2LcFfeRyT5feI9wbMc5B%2B5bGAwRlwgVWVNBMQDQg%2BwYHpAHNNS2rLJFVLOsDdfUVOPSdi0z2Xjg3OKP1J28f3e4%2BJXQftirauieHzfNXENvg3ngfzOFVZdFhpsuBaMOsINkaYFR5jGHICaJRWz74w8wE5aOK12vIx%2BJjs7o%3D" rel="nofollow">https://stackoverflow.com/questions/56980044/how-to-prevent-setinterval-in-react-native-from-blocking-running-animation</a>)—— 使用<code>react-native-reanimated</code>库可以解决该问题,<code>react-native-reanimated</code>提供的hook在UI线程处理动画</li></ol><pre><code> const animationRef = useRef({
strokeDashoffset: new Animated.Value(240),
rotateZ: new Animated.Value(1)
})
useEffect(() => {
const animationDef = () => {
Animated.loop(
Animated.sequence([
Animated.parallel([
Animated.timing(
animationRef.current.strokeDashoffset,
{
toValue: 240,
useNativeDriver: true,
duration: 900
}
),
Animated.timing(
animationRef.current.rotateZ,
{
toValue: 2,
useNativeDriver: true,
duration: 900
}
),
]),
Animated.parallel([
Animated.timing(
animationRef.current.strokeDashoffset,
{
toValue: 200,
useNativeDriver: true,
duration: 100
}
),
Animated.timing(
animationRef.current.rotateZ,
{
toValue: 3,
useNativeDriver: true,
duration: 100
}
),
]),
Animated.parallel([
Animated.timing(
animationRef.current.strokeDashoffset,
{
toValue: 200,
useNativeDriver: true,
duration: 900
}
),
Animated.timing(
animationRef.current.rotateZ,
{
toValue: 4,
useNativeDriver: true,
duration: 900
}
),
]),
Animated.parallel([
Animated.timing(
animationRef.current.strokeDashoffset,
{
toValue: 240,
useNativeDriver: true,
duration: 100
}
),
Animated.timing(
animationRef.current.rotateZ,
{
toValue: 5,
useNativeDriver: true,
duration: 100
}
),
])
])
).start()
}
animationDef()
}, [animationRef])
const rotateZ = animationRef.current.rotateZ.interpolate({
inputRange: [1, 2, 3, 4, 5],
outputRange: ['-100deg', '-424deg', '-500deg', '-784deg', '-820deg'],
})</code></pre><h2>常用第三方库</h2><blockquote>项目需要重启才可以,否则会报模块找不到</blockquote><ul><li><p>渐变色</p><ul><li><code>import LinearGradient from 'react-native-linear-gradient';</code></li></ul></li><li><p>支持svg</p><ul><li><code>"react-native-svg": "^13.9.0",</code></li><li><code>"react-native-svg-transformer": "^1.0.0",</code></li></ul></li><li>全局样式变量</li></ul><p><code>"react-native-extended-stylesheet": "^0.12.0",</code></p><ul><li>毛玻璃</li></ul><p><code>@react-native-community/blur</code></p><p><code><BlurView .../></code>标签本身不能包含子元素:</p><ul><li>若添加子元素,子元素不模糊,但会堆叠在一起</li><li><code><BlurView .../></code>标签产生模糊的范围是根据元素顺序来的 —— 该标签之前的所有节点是非模糊的,之后的所有节点都将被视为它的子级进行模糊。</li><li>投影</li></ul><p><code>yarn add react-native-shadow-2</code> —— 包裹唯一子节点不能使用margin,会导致Shadow不对齐,如果需要设置margin,使用View包裹Shadow元素</p><h2>第三方库使用带来的问题</h2><h3>UseEffect vs. UseFocusEffect</h3><p>使用<code>react-native-navigation</code>在同一路由栈中切换页面,页面没有销毁,所以,<code>useEffect</code>的清除回调不会被触发,需要考虑使用<code>[useFocusEffect](https://reactnavigation.org/docs/bottom-tab-navigator)</code>替代<code>useEffect</code>。</p><h3>环境变量设置</h3><p>使用<code>react-native-dotenv</code>或其他第三方库设置环境变量,会出现报错:<code>Property left of AssignmentExpression expected node to be of a type ["LVal"] but instead got "StringLiteral"</code> 。</p><p>需要修改<code>rn-nodeify</code>自动生成的<code>shim.js</code>文件,给<code>process.env</code>换一种赋值方式:</p><pre><code>const isDev = typeof __DEV__ === 'boolean' && __DEV__
const env = process.env || {}
env['NODE_ENV'] = isDev ? 'development' : 'production'
process.env = env
if (typeof localStorage !== 'undefined') {
localStorage.debug = isDev ? '*' : ''
}</code></pre><h3>报错:<code>ReferenceError: Property 'TextEncoder' doesn't exist, js engine: hermes</code></h3><p><a href="https://link.segmentfault.com/?enc=qk1i1HXCWacWJDAHgyMkcg%3D%3D.gOurTlG2PTLUTRBMnOOrcM56GPoxf059up%2B6VDcsP%2BlVYHaRimpdy6IXFUaCn4MS" rel="nofollow">https://github.com/hapijs/joi/issues/2141</a></p><h3>android.support.annotation包不存在的错误</h3><blockquote><code>jetifier</code>可一键解决</blockquote><ol><li>错误发生在你将你的Android应用程序迁移到使用<code>androidx</code> 库时</li><li><code>import</code> 当你使用<code>androidx</code> ,你需要更新你的Android源代码,用<code>androidx.annotation</code> 包替换所有<code>android.support.annotation</code> 包的语句。</li></ol><p><a href="https://link.segmentfault.com/?enc=yH6qOn0VK0reGVfmA9u0Uw%3D%3D.uly9BGhTggT8IK0EevgBTvvnI4YZhRVLy3wmFIRJGp66SwUNRt8DyovihycUqrw7" rel="nofollow">https://www.qiniu.com/qfans/qnso-40380519</a>【推荐】</p><h3><code>react-native-webview</code>错误: 无法将类 FileProvider中的构造器 FileProvider应用到给定类型;</h3><p><a href="https://link.segmentfault.com/?enc=1ILogfl1hGFTII7NPZZYiQ%3D%3D.hvvNc37rWq6S6NALezvIyY9cP%2F0HL5PLSQ9kkGRVCye05P%2FN4CDqEg81DpGFekYhx4eF3ZmAx0cX5f6J8ZJZPn89%2BfYqnccVoShNLBM0VcM%3D" rel="nofollow">https://github.com/react-native-webview/react-native-webview/issues/2978</a><br>将版本由<code>12.2</code>降级到<code>12.1</code>即可</p><h3><code>react-native-webview</code> Android postMessage 不工作</h3><p>在安卓环境下,通过<code>postmessage</code>通信,需要使用<code>document</code>监听,ios下通过<code>window</code>监听。<br><a href="https://link.segmentfault.com/?enc=fCtY7e6ScwizYJH3HnQ0yw%3D%3D.nj6Czezp4e21Qb0F9ePyQVEV4Lff0KDcgGe1i9Uu44G1lJnkE2JB%2B095BEtSHbhVF25m5E3tjPmvBuYE6Ly5G4%2F4H8dGHMgsjnMfw7231YU%3D" rel="nofollow">https://github.com/react-native-webview/react-native-webview/issues/356</a></p><h2>疑难杂症</h2><ul><li>嵌套路由中,<code>onPress</code>不起作用<br><code>TouchableWithoutFeedback</code>在嵌套路由下不工作,可用<code><TouchableOpacity activeOpacity={1} /></code>替代。</li><li>如何同时运行模拟器和真机how to run react-native app on simulator and real device at the same time</li></ul><p><a href="https://link.segmentfault.com/?enc=A3xSGc%2FUxe7fZgYl%2F3BQlA%3D%3D.0%2BAObHru11KIZeF70XZGBEf0YjTG7NhTascOWCWgdLQqbjtTuS%2BdefVUh52bvvUUyZVb2E%2BNTCPeHOlJw6mBtb%2B1o3Sqltnt7EOJAuFzdJN%2Blmz1P1EJtYJwT%2FMp8r3f" rel="nofollow">https://stackoverflow.com/questions/51336123/how-can-i-run-two-react-native-applications</a></p><ul><li>调试工具<code>flipper</code>的<code>React Devtools</code>面板始终无法连接到应用程序</li></ul><p>进行一下端口映射即可:<code>adb reverse tcp:8097 tcp:8097</code></p><ul><li><p><code>Error: Unable to resolve module 'stream'</code><br>针对于<code>rn-nodeify</code>兼容的库,如果无法解析的话,需要在<code>metro.config.js</code>中进行模块配置<code>extraNodeModules</code>:</p><pre><code>resolver: {
...resolver,
assetExts: resolver.assetExts.filter(ext => ext !== 'svg'),
sourceExts: [...resolver.sourceExts, 'svg'],
extraNodeModules: {
stream: require.resolve('readable-stream'),
},
},</code></pre></li></ul>
Nextjs实战记录
https://segmentfault.com/a/1190000043689975
2023-04-18T14:21:08+08:00
2023-04-18T14:21:08+08:00
米花儿团儿
https://segmentfault.com/u/mihuartuanr
0
<h2>动态表单</h2><p>初始化必须给{}赋值表单的name与值,否则会报不受控(即使是遍历出来的表单)</p><h2>动态样式</h2><h3>方案一:CSS Module + classnames</h3><pre><code> import classNames from 'classnames'
function Render () {
return (
<div className={
classNames(
ProgressStyles.fragment, {
[ProgressStyles.fragmentActive]: idx <= activeIndex
}
)
} key={idx}>{idx}</div>
)
}
</code></pre><p><strong>Notes</strong>: CSS Modules不支持连字符、下划线衔接。</p><pre><code>.fragment {
background: rgba(0, 0, 0, 0.06);
flex: 1;
& + & {
margin: {
left: 10px;
};
}
&Active {
background: #0F68FF;
}
}
</code></pre><h3>方案二:样式渗透</h3><p><code>:global</code></p><pre><code> :global {
.ant-select-selector {
border: 1px solid #E5E5E5;
background: linear-gradient(125.85deg, rgba(59, 63, 238, 0.1) 0%, rgba(57, 159, 254, 0.1) 100%);
@media screen and (min-width: 550px) {
height: nth($lineHeight, 1)!important;
line-height: nth($lineHeight, 1)!important;
}
@media screen and (max-width: 550px) {
height: nth($lineHeight, 2)!important;
line-height: nth($lineHeight, 2)!important;
}
}
.ant-select-selection-placeholder {
@include placeholder;
}
}
:global(.swiper-slide) {
cursor: pointer;
}
</code></pre><h2>组件定义</h2><h3>Slot定义</h3><p>SwiperCommon组件的调用,需要用SwiperCommon内部遍历的列表项数据:</p><pre><code>export default function Render () {
return (
<SwiperCommon className="newsSwiper" metas={data.list} options={}>
{
(meta) => (
<section className={NewsStyles.swiperSlide} onClick={() => handleClick(meta.id)}>
<div className={NewsStyles.coverWrapper}>
<Image alt={meta.title} src={meta.cover} className={NewsStyles.img}/>
</div>
<p className={NewsStyles.slideTitle}>{meta.title}</p>
<p className={NewsStyles.summary}>{meta.summary}</p>
<p className={NewsStyles.date}>{meta.time}</p>
</section>
)
}
</SwiperCommon>
)
}
</code></pre><p>ScopedSlot定义:</p><pre><code>import { useEffect } from 'react';
import SwiperCommonStyles from '@/styles/swiper-common.module.scss'
function SwiperCommon ({ children, metas = [], className: externalClass, options = {}}) {
useEffect(() => {
const swiper = new Swiper(`.${externalClass || 'swiper'}`, {
direction: 'horizontal',
centerInsufficientSlides: true,
...options
});
}, [externalClass, options])
return (
<>
{
metas.length && (
<div className={classNames(SwiperCommonStyles.swiper, `swiper ${externalClass}`)}>
<div className={classNames(SwiperCommonStyles.swiperWrapper, 'swiper-wrapper')}>
{
metas.map((item, idx) => (
<div key={idx} className={classNames(SwiperCommonStyles.swiperSlide, 'swiper-slide')}>
{children && children(item)}
</div>
))
}
</div>
</div>
)
}
</>
)
}
export default SwiperCommon
</code></pre><h3>参数解构</h3><pre><code> <!--闭合标签使用时,组件参数类型声明中包含children-->
<NavigationBar
title="This is title"
>
</NavigationBar>
<!--空标签使用时,组件参数类型声明中不包含children-->
<NavigationBar
title="This is title"
/>
</code></pre><p>组件的参数解构:</p><pre><code>import NavigationBarStyles from "../styles/navigationbar.module.scss"
function Navigationbar ({ children, title = "", historyBack = false }) {
return (
<div className={NavigationBarStyles.container}>
{historyBack && (<div className={NavigationBarStyles.historyBack}>&lt;</div>)}
<NavigationbarContent title={title}>{children}</NavigationbarContent>
</div>
)
}
export default Navigationbar
</code></pre><p><strong>Notes:</strong></p><ul><li><code><NavigationBar /></code>形式调用组件,解构出来的<code>children</code>为<code>undefined</code></li><li><code><NavigationBar></NavigationBar></code> 形式调用组件,即使子节点为空,也能解构出来的<code>children</code>为<code>[]</code></li></ul><h2>全局SCSS变量配置</h2><p><a href="https://link.segmentfault.com/?enc=D2pzA1giJ4fqERaIS%2BWHKQ%3D%3D.wI%2FmsMvy98aymIXRHmrFqafA4I06VZc1VQZfSoZz7SEeJHQWRQrr%2BLT5pWq76p20xLz0lrUdf4sjxyjzUEfS6rzgVj5T2wAjjYLh6XPDNjy9%2F7lQsF2HUbPosaJ5VgAI" rel="nofollow">https://stackoverflow.com/questions/60951575/next-js-using-sass-variables-from-global-scss</a></p><pre><code>const path = require('path');
const nextConfig = (phase, { defaultConfig }) => {
if ('sassOptions' in defaultConfig) {
defaultConfig['sassOptions'] = {
includePaths: [path.join(__dirname, 'styles')],
prependData: `@import "utils.scss";`,
};
}
return defaultConfig;
};
module.exports = nextConfig;
</code></pre><h2>全局可访问变量</h2><p>在根目录下定义<code>.env</code>、<code>.env.development</code>、<code>.env.production</code>、<code>.env.local</code>文件</p><h3>Node环境</h3><pre><code>HOSTNAME=localhost
PORT=8080
HOST=http://$HOSTNAME:$PORT
</code></pre><p><strong>Notes:</strong></p><ul><li>以上变量在Node环境中可通过<code>process.env.HOSTNAME</code>形式获取,无法在浏览器中获取;</li><li>可以通过<code>$Variable</code>的形式,在另一个变量定义中引用其他变量</li></ul><h3>Browser环境</h3><pre><code>NEXT_PUBLIC_API_BASEURL=http://127.0.0.1:3000
</code></pre><p><strong>Notes:</strong></p><ul><li>以上变量可以在Node、Browser环境中可通过<code>process.env.NEXT_PUBLIC_API_BASEURL</code>形式获取</li><li>以<code>NEXT_PUBLIC_</code> 前缀定义变量可以暴露在浏览器环境;</li></ul><h3>限制</h3><ul><li><p>只能通过<code>process.env.VarName</code>的形式访问,无法通过解构、[变量]形式访问</p><ul><li><code>process.env[varName]</code> ——> Error</li><li><code>const env = process.env</code> ——> Error</li></ul></li></ul><h2>类型声明</h2><p><a href="https://link.segmentfault.com/?enc=wFUT%2Fg2ykv3xpavoCcxa%2FQ%3D%3D.cW8PwzNPefjqzcIWSqJaCqvJJBiRWIN4JjcGr1u7iKRhsKWdrNPwnJZrbDfpA%2BxtOg7gK0%2F1RmGDBNKwWN4Jzw%3D%3D" rel="nofollow">https://dmitripavlutin.com/typescript-react-components/</a></p><p><a href="https://link.segmentfault.com/?enc=kYZ0nQgTH4UR8bPxEOa5HA%3D%3D.sirOsIpp6HS939ZiuudybY9vc1hK0DlF5OBwMeAjRXVFgr7y5Lk1k0z%2FbPKve%2FEWBaloDRwA7YNh2n6y%2FkWSo441%2F6oKDljG8UXjLPHJ9ZozcPQkksUDXZyfZUGzYkvC" rel="nofollow">https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/react/index.d.ts</a></p><h2>图片渲染</h2><p><a href="https://link.segmentfault.com/?enc=zZazszfd2bj2xlaiPAL4WQ%3D%3D.RPHfi%2BglN7W9%2BN1vMgqAgU1zAtp6bW%2FhRLb26%2FWI1rdOOhHM3IMC3fLRxzT48%2BcYX33yElV%2FwwXhPAjIVdSOeQ%3D%3D" rel="nofollow">Nextjs图片渲染官方文档</a></p><ul><li>使用<code>'next/image'</code>提供的组件,会提供图片优化 — 懒加载</li><li>使用<code>'next/image'</code>提供的组件,必须设定图片尺寸,图片尺寸若要根据父级尺寸,需设置父元素<code>position: relative</code>,<code><Image layout="fill"...</code> — 此时生成的<code>img</code>标签是绝对定位的。</li></ul><pre><code>import Image from 'next/image';
import welcomeStyles from '../styles/welcome.module.scss';
export default function Home() {
return (
<>
<section className={welcomeStyles.navigationbar}>
<h1 className='navigationbar-title'>new world</h1>
</section>
<section className={welcomeStyles.main}>
<h2 className={welcomeStyles.title}>Welcome new world!</h2>
<div className={welcomeStyles.cover}>
<Image layout="fill" objectFit="cover" src="/images/entries/wel.png" alt="" />
</div>
</section>
</>
);
}
</code></pre><h2>客户端渲染</h2><pre><code> useEffect(() => {
import("../lib/flexible");
import("../lib/flexibleHelper");
}, []);
</code></pre><h2>api Route</h2><h3>api定义</h3><p>范指<code>/pages/api/**/*.js</code>路径下的文件,该处主要是转接服务端接口,中转前端数据结构。</p><pre><code>// /pages/api/blog
export default function handler(req, res) { // 导出的函数方法名无实际意义
res.status(200).json({ name: 'John Doe' })
}
</code></pre><p><strong>Notes:</strong> 该JavaScript是服务端代码,而非H5前端代码;</p><h3>api访问</h3><pre><code>export async function getStaticProps(context) {
const res = await fetch(`${process.env.BaseUrl}/api/blog`)
const features = await res.json()
return {
props: {
features,
},
}
}
export default function Blog ({features}) {
const features = response
return (
<>
<News meta={features.NewsResponse} />
</>
)
}
</code></pre><p><strong>Notes:</strong> 前端必须通过http(s)协议请求,所以,必须启动服务才可以访问Api Route</p><h2>导出纯静态页面</h2><blockquote>导出不需要服务配置,可独立访问的HTML静态页面(注意:SSG也是需要服务端配置的)</blockquote><ol><li>示例中的Nextjs在13.3(不含)以下版本:<code>13.2.4</code></li><li>页面内的数据都是本地Mock的JSON数据</li><li>修改<code>next.config.js</code>配置:</li></ol><pre><code>const nextConfig = (phase, {defaultConfig}) => {
defaultConfig.reactStrictMode = true
defaultConfig.exportPathMap = async function (defaultPathMap) {
return {
'/': { page: '/' },
'/news': { page: '/news' },
'/blog': { page: '/blog' },
}
}
defaultConfig.images.unoptimized = true
return defaultConfig
}
module.exports = nextConfig
</code></pre><ul><li>配置<code>exportPathMap</code>字段,官网文档声明13.3版本以上会自动生成,无需配置</li><li>配置<code>images.unoptimized = true</code>,不允许优化图片,否则会报错</li></ul><ol start="4"><li>补充<code>package#scripts</code>,运行<code>npm run export</code></li></ol><pre><code> "scripts": {
"dev": "next dev",
"build": "next build",
"export": "next build && next export && npm run build-static-repair-index && npm run build-favicon-repair-index && npm run build-logo-repair-index",
"build-static-repair-index": "replace-in-files --string \"/_next/static\" --replacement \"/_next/static\" out/index.html",
"build-favicon-repair-index": "replace-in-files --string \"/favicon.ico\" --replacement \"./favicon.ico\" out/*.html",
"build-logo-repair-index": "replace-in-files --string \"/logo*.png\" --replacement \"./logo*.png\" out/*.html",
"start": "next start",
"lint": "next lint"
},
</code></pre><ul><li>静态打包后的资源引用地址不对,目前没找到配置项可用,需借助于<code>replace-in-files</code>单独处理资源引用。</li></ul><ol start="5"><li>将<code>/out</code>中的静态资源部署即可</li></ol>
EIP1559与传统Gas定价模型转账逻辑
https://segmentfault.com/a/1190000043314593
2023-01-12T12:01:49+08:00
2023-01-12T12:01:49+08:00
米花儿团儿
https://segmentfault.com/u/mihuartuanr
0
<h2>传统的gas定价模型(Txn Type===0)</h2><p><img src="/img/remote/1460000043314595" alt="20230104-112422.jpg" title="20230104-112422.jpg"></p><pre><code>import Web3 from 'web3';
import { AbiItem } from 'web3-utils/types/index.d';
import erc20 from '../abis/erc20.json';
import { FeeMarketEIP1559Transaction as Tx } from '@ethereumjs/tx';
import { Common, Chain, Hardfork } from '@ethereumjs/common';
import config from '../constants/service.config';
const webSocketProvider = (rpcUrl: string) => {
return new Web3.providers.HttpProvider(rpcUrl);
};
const provider = (rpcUrl: string = config.alchemyEthBaseUrl) => {
return new Web3(webSocketProvider(rpcUrl));
};
const transfer = async (
{ ...rest }: any
) => {
const {
privateKey,
contractAddress,
tokenAddress,
recipientAddress,
rpcUrl,
senderAddress,
amount,
...args
} = rest;
const web3 = provider();
const { toHex, toWei, hexToNumber } = web3.utils;
const gasPrice = await web3.eth.getGasPrice();
try {
const nounce = await web3.eth.getTransactionCount(senderAddress, 'pending');
let rawTx = {};
if (['eth', 'matic'].includes(tokenAddress.toLowerCase())) {
// 以太坊主币
rawTx = {
to: recipientAddress,
value: toHex(toWei(String(amount))),
};
} else {
// 符合erc20规范的代币
const contract = new web3.eth.Contract(erc20 as AbiItem[], tokenAddress, {
from: senderAddress,
});
const transferEvent = contract.methods.transfer(
recipientAddress,
toHex(toWei(String(amount)))
);
const limit = await transferEvent.estimateGas(); // gas limit
rawTx = {
to: tokenAddress,
gasLimit: toHex(Math.ceil(limit * 1.2)),
data: transferEvent.encodeABI(),
};
}
/**
* 组装交易数据:
* to:
* 如果是以太坊主币,to是收款人地址
* 如果是erc20代码,to是协议地址,收款人地址在合约方法:contrack.methods.transfer中定义
* value:
* 如果是以太坊主币,value是转账金额
* 如果是erc20代码,value是`toHex(0)`,转账金额在合约方法:contrack.methods.transfer中定义
* maxPriorityFeePerGas
* Txn Type=2所必需,提高优先级,给予矿工的小费
* maxFeePerGas
* Txn Type=2所必需,提高优先级,给予矿工小费的最大值
*/
rawTx = {
from: senderAddress,
nonce: toHex(nounce),
gasLimit: toHex(21000), //"21000",
gasPrice: toHex(gasPrice),
value: '0x00', //"10000000",//'0x00',
...rawTx,
};
/**
* 以太坊资源的通用实现类;
* 创建以太坊链和分叉的实例,实现EIP1559功能需要选择London硬分叉
* new Common({ chain: Chain.Mainnet, eips: [1559] })
*/
const common = new Common({
chain: Chain.Goerli,
hardfork: Hardfork.London,
});
const unsignedTx = Tx.fromTxData(rawTx, { common });
const secretKey = Buffer.from(privateKey.slice(2), 'hex');
const signedTx = unsignedTx.sign(secretKey);
const serializedTx = signedTx.serialize();
const signedTransactionData = '0x' + serializedTx.toString('hex');
web3.eth
.sendSignedTransaction(signedTransactionData, async function (err, hash) {
if (!err) {
/**
* gasfee = `${web3.utils.fromWei(
(Number((rawTx as any).gasLimit) * Number(gasPrice)).toString()
)}${wallet.unit}`;
*/
console.log('-----hash-------', hash);
const url = `https://goerli.etherscan.io/tx/${hash}`;
const accepted = `https://etherscan.io/api?module=localchk&action=txexist&txhash=${hash}`;
console.log('TX Link', url);
console.log('Accepted', accepted);
} else {
console.log('-----error', err);
}
})
.on('receipt', (receipt) => {
console.log('TX receipt', receipt);
// blockHash: ""
// blockNumber:
// contractAddress: null
// cumulativeGasUsed:
// effectiveGasPrice:
// from: ""
// gasUsed: 21000
// logs: []
// logsBloom: ""
// status: true
// to: ""
// transactionHash: ""
// transactionIndex:
// type: "0x0"
const { gasUsed, status } = receipt;
/**
* gasfee = `${web3.utils.fromWei(
(gasUsed * Number(gasPrice)).toString()
)}${wallet.unit}`;
*/
});
} catch (e) {
console.log(e);
}
};</code></pre><h2>伦敦硬分叉EIP1559(Txn Type===2)</h2><p><img src="/img/remote/1460000043314596" alt="20230104-112437.jpg" title="20230104-112437.jpg"></p><pre><code>import Web3 from 'web3';
import { AbiItem } from 'web3-utils/types/index.d';
import erc20 from '../abis/erc20.json';
import { FeeMarketEIP1559Transaction as Tx } from '@ethereumjs/tx';
import { Common, Chain, Hardfork } from '@ethereumjs/common';
import config from '../constants/service.config';
const webSocketProvider = (rpcUrl: string) => {
return new Web3.providers.HttpProvider(rpcUrl);
};
const provider = (rpcUrl: string = config.alchemyEthBaseUrl) => {
return new Web3(webSocketProvider(rpcUrl));
};
const transfer = async (
{ ...rest }: any
) => {
const {
privateKey,
contractAddress,
tokenAddress,
recipientAddress,
rpcUrl,
senderAddress,
amount,
...args
} = rest;
const web3 = provider();
const { toHex, toWei, hexToNumber } = web3.utils;
const gasPrice = await web3.eth.getGasPrice();
try {
const nounce = await web3.eth.getTransactionCount(senderAddress, 'pending');
let rawTx = {};
if (['eth', 'matic'].includes(tokenAddress.toLowerCase())) {
// 以太坊主币
rawTx = {
to: recipientAddress,
value: toHex(toWei(String(amount))),
};
} else {
// 符合erc20规范的代币
const contract = new web3.eth.Contract(erc20 as AbiItem[], tokenAddress, {
from: senderAddress,
});
const transferEvent = contract.methods.transfer(
recipientAddress,
toHex(toWei(String(amount)))
);
const limit = await transferEvent.estimateGas(); // gas limit
rawTx = {
to: tokenAddress,
gasLimit: toHex(Math.ceil(limit * 1.2)),
data: transferEvent.encodeABI(),
};
}
// maxPriorityFeePerGas: Txn Type === 2所需要的字段
const maxPriorityFeePerGasRes = request.post(config.alchemyEthBaseUrl, {
id: 1,
jsonrpc: '2.0',
method: 'eth_maxPriorityFeePerGas',
});
const maxPriorityFeePerGas = ((await maxPriorityFeePerGasRes) as any)
.result;
const getBlockRes = web3.eth.getBlock('latest');
const baseFeePerGas = ((await getBlockRes) as any).baseFeePerGas - 1;
/**
* 组装交易数据:
* to:
* 如果是以太坊主币,to是收款人地址
* 如果是erc20代码,to是协议地址,收款人地址在合约方法:contrack.methods.transfer中定义
* value:
* 如果是以太坊主币,value是转账金额
* 如果是erc20代码,value是`toHex(0)`,转账金额在合约方法:contrack.methods.transfer中定义
* maxPriorityFeePerGas
* Txn Type=2所必需,提高优先级,给予矿工的小费
* maxFeePerGas
* Txn Type=2所必需,提高优先级,给予矿工小费的最大值
*/
rawTx = {
from: senderAddress,
nonce: toHex(nounce),
gasLimit: toHex(21000), //"21000",
maxPriorityFeePerGas: maxPriorityFeePerGas,
maxFeePerGas: toHex(
2 * baseFeePerGas + hexToNumber(maxPriorityFeePerGas)
),
value: '0x00', //"10000000",//'0x00',
...rawTx,
};
/**
* 以太坊资源的通用实现类;
* 创建以太坊链和分叉的实例,实现EIP1559功能需要选择London硬分叉
* new Common({ chain: Chain.Mainnet, eips: [1559] })
*/
const common = new Common({
chain: Chain.Goerli,
hardfork: Hardfork.London,
});
const unsignedTx = Tx.fromTxData(rawTx, { common });
const secretKey = Buffer.from(privateKey.slice(2), 'hex');
const signedTx = unsignedTx.sign(secretKey);
const serializedTx = signedTx.serialize();
const signedTransactionData = '0x' + serializedTx.toString('hex');
web3.eth
.sendSignedTransaction(signedTransactionData, async function (err, hash) {
if (!err) {
/**
* gasfee = `${web3.utils.fromWei(
(Number((rawTx as any).gasLimit) * Number(gasPrice)).toString()
)}${wallet.unit}`;
*/
console.log('-----hash-------', hash);
const url = `https://goerli.etherscan.io/tx/${hash}`;
const accepted = `https://etherscan.io/api?module=localchk&action=txexist&txhash=${hash}`;
console.log('TX Link', url);
console.log('Accepted', accepted);
} else {
console.log('-----error', err);
}
})
.on('receipt', (receipt) => {
console.log('TX receipt', receipt);
// blockHash: ""
// blockNumber:
// contractAddress: null
// cumulativeGasUsed:
// effectiveGasPrice:
// from: ""
// gasUsed: 21000
// logs: []
// logsBloom: "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
// status: true
// to: ""
// transactionHash: ""
// transactionIndex:
// type: "0x0"
const { gasUsed, status } = receipt;
/**
* gasfee = `${web3.utils.fromWei(
(gasUsed * Number(gasPrice)).toString()
)}${wallet.unit}`;
*/
});
} catch (e) {
console.log(e);
}
};</code></pre><h2>外部签名</h2><pre><code>import Web3 from 'web3';
import { bufArrToArr, toBuffer, bigIntToHex, addHexPrefix } from '@ethereumjs/util'
...
// signatures是外部服务签名
const { toHex, toWei, toDecimal } = web3.utils;
const { r_hex: r, s_hex: s, recovery_id_hex } = signatures
const chainId = unsignedTx.common.chainId()
const v = chainId === undefined
? BigInt(toDecimal(addHexPrefix(recovery_id_hex)) + 27)
: BigInt(toDecimal(addHexPrefix(recovery_id_hex)) + 35) + BigInt(chainId) * BigInt(2)
const toSignTx = {
nonce: unsignedTx.nonce,
gasPrice: unsignedTx.gasPrice,
gasLimit: unsignedTx.gasLimit,
to: unsignedTx.to,
value: unsignedTx.value,
data: unsignedTx.data,
v,
r: addHexPrefix(r),
s: addHexPrefix(s),
}
const signedTx = Tx.fromTxData(toSignTx, { common })
const serializeSignedTx = signedTx.serialize()
const signedTxHex = addHexPrefix(serializeSignedTx.toString('hex'))
web3.eth.sendSignedTransaction(signedTxHex, async function (err, hash) {
if (!err) {
console.log('-----hash-------', hash);
const url = `https://goerli.etherscan.io/tx/${hash}`;
const accepted = `https://etherscan.io/api?module=localchk&action=txexist&txhash=${hash}`;
console.log('TX Link', url);
console.log('Accepted', accepted);
} else {
console.log('-----error', err);
}
})</code></pre>
Google Development Api功能开发使用示例
https://segmentfault.com/a/1190000043146200
2022-12-23T17:18:20+08:00
2022-12-23T17:18:20+08:00
米花儿团儿
https://segmentfault.com/u/mihuartuanr
0
<h2>Sample1:授权</h2><h3>概述</h3><p>主要分为五步:</p><ol><li>加载api工具库</li><li>登陆</li><li>scopes授权</li><li><p>加载对应的工具文档</p><ul><li>工具文档决定了调用<code>gapi.client.*</code>对象下的哪一个服务</li></ul></li><li>根据工具文档中的<code>resources</code>字段调用方法</li></ol><h3>加载gapi工具库</h3><blockquote>gapi === google application interface</blockquote><pre><code> <script src="https://apis.google.com/js/api.js"></script></code></pre><p>该工具库会全局注册gapi对象。</p><p><strong>Notes:</strong> gapi的文档<<u><a href="https://link.segmentfault.com/?enc=xhjcfnZKY9uvKR1V4Oom8w%3D%3D.%2FDnQymDEObcPIWm%2FR8BGooudLe7DVuFNFZKYODblemWg0GJcS9X%2FUSsjFzvf%2FUg5ATNwFF3OwaAXOPeFfu9oM2Ibhsk12F49Pa%2F6blNzf632rAgEHFywtvYpThn01T6m" rel="nofollow">https://github.com/google/goo...</a></u>></p><h3>加载授权模块</h3><pre><code> gapi.load("client:auth2", function() {
gapi.auth2.init({
client_id: CLIENTID,
// cookiepolicy: 'single_host_origin',
plugin_name: 'hello' // 必填,可以是任意值 —— 不填,会报错 {error: "popup_closed_by_user"}
});
});</code></pre><h3>登陆且授权</h3><pre><code> <button onclick="authenticate().then(loadClient)">authorize and load</button></code></pre><p>点击按钮后,授权服务,加载需要的服务API文档(gapi根据加载的文档会动态生成对应的服务方法)。</p><pre><code> const scopes = [
"https://www.googleapis.com/auth/drive",
"https://www.googleapis.com/auth/drive.appdata",
"https://www.googleapis.com/auth/drive.file",
"https://www.googleapis.com/auth/drive.metadata",
"https://www.googleapis.com/auth/drive.metadata.readonly",
"https://www.googleapis.com/auth/drive.photos.readonly",
"https://www.googleapis.com/auth/drive.readonly"
] // 用户授权可访问的服务列表
function authenticate() {
return gapi.auth2.getAuthInstance()
.signIn({
scope: scopes.join(" ")
})
.then(
function() {
console.log("Sign-in successful");
},
function(err) {
console.error("Error signing in", err);
}
);
}
function loadClient() {
// 有了授权操作,就不需要设置ApiKey了,二者冲突<https://stackoverflow.com/questions/17436940/google-simple-api-key-stopped-working>
/**
* 以当前用户身份调取Api —— 使用access_token
* 以开发者身份调用API —— 使用API key
*/
// gapi.client.setApiKey(CLIENTSECRET);
return gapi.client.load("https://content.googleapis.com/discovery/v1/apis/drive/v3/rest")
.then(
function() {
console.log("GAPI client loaded for API");
},
function(err) {
console.error("Error loading GAPI client for API", err);
}
);
}</code></pre><h3>调用方法</h3><pre><code> <button onclick="execute()">execute</button></code></pre><p>直接浏览器打开<a href="https://link.segmentfault.com/?enc=oHrzSltgfp8LWlPK4wiyCw%3D%3D.LKYglkBpLWPSpFMf3GZmpgko%2B4g2QxVcy%2FHINQVxZcim9W06Ufkx4T5GKdd6JBBU5z1rIHWKvst5plEO%2B9pnRg%3D%3D" rel="nofollow">https://content.googleapis.com/discovery/v1/apis/drive/v3/rest</a>,查找对应的<code>resources</code>字段,调用对应的<code>methods</code>即可。</p><pre><code> function execute() {
return gapi.client.drive.files.list({})
.then(
function(response) {
// Handle the results here (response.result has the parsed body).
console.log("Response", response);
},
function(err) {
console.error("Execute error", err);
}
);
}</code></pre><h3>完整代码</h3><pre><code><!DOCTYPE html>
<html>
<head>
<title>Drive API Quickstart</title>
<meta charset="utf-8" />
</head>
<body>
<p>Drive API Quickstart</p>
<!--Add buttons to initiate auth sequence and sign out-->
<button onclick="authenticate().then(loadClient)">authorize and load</button>
<button onclick="execute()">execute</button>
<pre id="content" style="white-space: pre-wrap;"></pre>
<script src="https://apis.google.com/js/api.js"></script>
<script type="text/javascript">
const CLIENTID = '<your client_id>'
const CLIENTSECRET = '<your client_secret>'
const scopes = [
"https://www.googleapis.com/auth/drive",
"https://www.googleapis.com/auth/drive.appdata",
"https://www.googleapis.com/auth/drive.file",
"https://www.googleapis.com/auth/drive.metadata",
"https://www.googleapis.com/auth/drive.metadata.readonly",
"https://www.googleapis.com/auth/drive.photos.readonly",
"https://www.googleapis.com/auth/drive.readonly"
] // 用户授权可访问的服务列表
function authenticate() {
return gapi.auth2.getAuthInstance()
.signIn({
scope: scopes.join(" ")
})
.then(
function() {
console.log("Sign-in successful");
},
function(err) {
console.error("Error signing in", err);
}
);
}
function loadClient() {
// 有了授权操作,就不需要设置ApiKey了,二者冲突<https://stackoverflow.com/questions/17436940/google-simple-api-key-stopped-working>
/**
* 以当前用户身份调取Api —— 使用access_token
* 以开发者身份调用API —— 使用API key
*/
// gapi.client.setApiKey(CLIENTSECRET);
return gapi.client.load("https://content.googleapis.com/discovery/v1/apis/drive/v3/rest")
.then(
function() {
console.log("GAPI client loaded for API");
},
function(err) {
console.error("Error loading GAPI client for API", err);
}
);
}
// Make sure the client is loaded and sign-in is complete before calling this method.
function execute() {
return gapi.client.drive.files.list({})
.then(
function(response) {
// Handle the results here (response.result has the parsed body).
console.log("Response", response);
},
function(err) {
console.error("Execute error", err);
}
);
}
gapi.load("client:auth2", function() {
gapi.auth2.init({
client_id: CLIENTID,
// cookiepolicy: 'single_host_origin',
plugin_name: 'hello' // 必填,可以是任意值 —— 不填,会报错 {error: "popup_closed_by_user"}
});
});
</script>
</body>
</html></code></pre><h2>Sample2:获取access_token</h2><h3>相关资源</h3><pre><code> <script async defer src="https://apis.google.com/js/api.js" onload="gapiLoaded()"></script>
<script async defer src="https://accounts.google.com/gsi/client" onload="gisLoaded()"></script></code></pre><h3>获取用户实例</h3><pre><code> tokenClient = google.accounts.oauth2.initTokenClient({
client_id: CLIENT_ID,
scope: SCOPES,
callback: '', // defined later
});</code></pre><h3>账号登陆检测</h3><pre><code> if (gapi.client.getToken() === null) {
// Prompt the user to select a Google Account and ask for consent to share their data
// when establishing a new session.
tokenClient.requestAccessToken({prompt: 'consent'});
} else {
// Skip display of account chooser and consent dialog for an existing session.
tokenClient.requestAccessToken({prompt: ''});
}</code></pre><h3>登陆后的回调</h3><pre><code> tokenClient.callback = async (resp) => {
if (resp.error !== undefined) {
throw (resp);
}
document.getElementById('signout_button').style.visibility = 'visible';
document.getElementById('authorize_button').innerText = 'Refresh';
await listFiles();
};</code></pre><h3>退出账号</h3><pre><code> const token = gapi.client.getToken();
if (token !== null) {
google.accounts.oauth2.revoke(token.access_token);
gapi.client.setToken('');
}</code></pre><h3>全部代码</h3><pre><code><!DOCTYPE html>
<html>
<head>
<title>Drive API Quickstart</title>
<meta charset="utf-8" />
</head>
<body>
<p>Drive API Quickstart</p>
<!--Add buttons to initiate auth sequence and sign out-->
<button id="authorize_button" onclick="handleAuthClick()">Authorize</button>
<button id="signout_button" onclick="handleSignoutClick()">Sign Out</button>
<pre id="content" style="white-space: pre-wrap;"></pre>
<script type="text/javascript">
const CLIENT_ID = '<your-client_id>';
const API_KEY = '<your-api_key>';
const DISCOVERY_DOC = 'https://www.googleapis.com/discovery/v1/apis/drive/v3/rest';
const SCOPES = 'https://www.googleapis.com/auth/drive.metadata.readonly';
let tokenClient;
let gapiInited = false;
let gisInited = false;
document.getElementById('authorize_button').style.visibility = 'hidden';
document.getElementById('signout_button').style.visibility = 'hidden';
function gapiLoaded() {
gapi.load('client', initializeGapiClient);
}
async function initializeGapiClient() {
await gapi.client.init({
discoveryDocs: [DISCOVERY_DOC],
});
gapiInited = true;
maybeEnableButtons();
}
function gisLoaded() {
tokenClient = google.accounts.oauth2.initTokenClient({
client_id: CLIENT_ID,
scope: SCOPES,
callback: '', // defined later
});
gisInited = true;
maybeEnableButtons();
}
function maybeEnableButtons() {
if (gapiInited && gisInited) {
document.getElementById('authorize_button').style.visibility = 'visible';
}
}
function handleAuthClick() {
tokenClient.callback = async (resp) => {
if (resp.error !== undefined) {
throw (resp);
}
document.getElementById('signout_button').style.visibility = 'visible';
document.getElementById('authorize_button').innerText = 'Refresh';
await listFiles();
};
if (gapi.client.getToken() === null) {
// Prompt the user to select a Google Account and ask for consent to share their data
// when establishing a new session.
tokenClient.requestAccessToken({prompt: 'consent'});
} else {
// Skip display of account chooser and consent dialog for an existing session.
tokenClient.requestAccessToken({prompt: ''});
}
}
/**
* Sign out the user upon button click.
*/
function handleSignoutClick() {
const token = gapi.client.getToken();
if (token !== null) {
google.accounts.oauth2.revoke(token.access_token);
gapi.client.setToken('');
document.getElementById('content').innerText = '';
document.getElementById('authorize_button').innerText = 'Authorize';
document.getElementById('signout_button').style.visibility = 'hidden';
}
}
/**
* Print metadata for first 10 files.
*/
async function listFiles() {
let response;
try {
response = await gapi.client.drive.files.list({
'pageSize': 10,
'fields': 'files(id, name)',
});
} catch (err) {
document.getElementById('content').innerText = err.message;
return;
}
const files = response.result.files;
if (!files || files.length == 0) {
document.getElementById('content').innerText = 'No files found.';
return;
}
// Flatten to string to display
const output = files.reduce(
(str, file) => `${str}${file.name} (${file.id}\n`,
'Files:\n');
document.getElementById('content').innerText = output;
}
</script>
<script async defer src="https://apis.google.com/js/api.js" onload="gapiLoaded()"></script>
<script async defer src="https://accounts.google.com/gsi/client" onload="gisLoaded()"></script>
</body>
</html></code></pre><h2>Sample3:创建文件</h2><h3>HTTP请求</h3><p><strong>Body的组装:</strong></p><p>用自定义<code>boundary</code>边界字符串标识每个部分,每部分都有两个连字符打头。</p><p>此外,在结束位置,仍以自定义<code>boundary</code>边界字符串 + 两个连字符结尾。</p><pre><code>async function upload () {
console.log('-------upload---token------', gapi.client.getToken())
let callback
var content = 'this is my secret'; // 新文件的正文
var blob = new Blob([content], { type: "text/plain"});
const boundary = '-------314159265358979323846';
const delimiter = "\r\n--" + boundary + "\r\n";
const close_delim = "\r\n--" + boundary + "--";
var reader = new FileReader();
reader.readAsBinaryString(blob);
reader.onload = function(e) {
var contentType = 'application/octet-stream';
var metadata = {
'name': 'aaa.txt',
'mimeType': contentType
};
var base64Data = btoa(reader.result);
var multipartRequestBody =
delimiter +
'Content-Type: application/json\r\n\r\n' +
JSON.stringify(metadata) +
delimiter +
'Content-Type: ' + contentType + '\r\n' +
'Content-Transfer-Encoding: base64\r\n' +
'\r\n' +
base64Data +
close_delim;
var request = gapi.client.request({
'path': '/upload/drive/v3/files',
'method': 'POST',
'params': {'uploadType': 'multipart'},
'headers': {
'Content-Type': 'multipart/mixed; boundary="' + boundary + '"'
},
'body': multipartRequestBody});
if (!callback) {
callback = function(file) {
console.log(file)
};
}
request.execute(callback);
}
}</code></pre><h3>axios请求</h3><p>formData组装顺序:</p><p>Meatadata >> file</p><p>否则,会报错<a href="https://link.segmentfault.com/?enc=j76qd5m%2Fl%2Fh4kmejQg%2BrxQ%3D%3D.7im14MA%2Bx%2Bqg3KgiStM%2BhzMintx4X3iiUo1jAF%2BAPE1N8MputhriWx6BLSk1qQMVnQDxs219u%2FZHXDe2rPggaqNkGZ2KVhoqSFllDPXipnQ%3D" rel="nofollow">https://developers.google.com...</a>。</p><pre><code> var formData = new FormData();
var content = 'this is my secret'; // 新文件的正文
var blob = new Blob([content], { type: "text/plain"});
var metadata = {
'name': 'customfile.txt', // Filename at Google Drive
'mimeType': 'text/plain', // mimeType at Google Drive
// TODO [Optional]: Set the below credentials
// Note: remove this parameter, if no target is needed
// 'parents': ['SET-GOOGLE-DRIVE-FOLDER-ID'], // Folder ID at Google Drive which is optional
};
formData.append('metadata', new Blob([JSON.stringify(metadata)], { type: 'application/json' }));
formData.append("file", blob);
axios.post(
'https://www.googleapis.com/upload/drive/v3/files',
formData,
{
headers: {
Authorization: `Bearer ${access_token}`,
'Content-Type': 'multipart/form-data'
},
params: {
uploadType: 'multipart',
supportsAllDrives: true
},
}
)</code></pre><h2>Sample4: 创建文件夹</h2><pre><code>var parentId = '';//some parentId of a folder under which to create the new folder
var fileMetadata = {
'name' : 'New Folder',
'mimeType' : 'application/vnd.google-apps.folder',
'parents': [parentId]
};
gapi.client.drive.files.create({
resource: fileMetadata,
}).then(function(response) {
switch(response.status){
case 200:
var file = response.result;
console.log('Created Folder Id: ', file.id);
break;
default:
console.log('Error creating the folder, '+response);
break;
}
});</code></pre><h2>Sample5:读取文件内容</h2><blockquote>追加<code>?alt=media</code>可查看文件内容,否则,返回文件的metadata。</blockquote><pre><code>async function handleExport (_event: any, fileId = '') {
const res = await axios.get(
`https://www.googleapis.com/drive/v3/files/${fileId}`,
{
params: {
alt: 'media'
},
headers: {
Authorization: `Bearer ${accessToken}`,
},
}
)
if (res.status === 200) {
return res.data
}
}</code></pre><h2>Sample6: 获取用户信息</h2><blockquote>查看用户所有信息,可传<code>fields: "*"</code></blockquote><pre><code>async function handleUserInfo () {
const res = await gapi.client.drive.about.get({
fields: "user, kind"
});
console.log('-------------uesr--------', res)
}</code></pre><h2>相关文档</h2><ul><li><a href="https://link.segmentfault.com/?enc=JQWYlItVFwf3dOvKftdg7Q%3D%3D.nnBxJMACCDo6RtCBHowjZXOPukCZ363dElClNFjbPxmxloTujhs%2BrnxcMZk14EZ3UcWICEC2LG8KM2W4YLPPp%2FC%2Fy9TzPe61AbU2Py17t8KThoa0epv8%2F92kvxYPkn3D" rel="nofollow">Google-api-javascript客户端开发文档</a></li><li><a href="https://link.segmentfault.com/?enc=ZtGAWPu3WdRnlYDVRKONXA%3D%3D.k7mqQdMyNUFQvmnoOeN4vVbpZZtR%2FPGygXZqx0VXsZl0RJa7Cf4VXxfh5ph9MDxsU3Gmr7EZEebHqJlXqfhGiQ%3D%3D" rel="nofollow">Google Api指南</a></li><li><a href="https://link.segmentfault.com/?enc=miCMSYMCKFWzgfv5vtoo0Q%3D%3D.gwkfy3K%2F7Fgj83tDt8EBckYVKhLK33B60hNTteq3C44qaPG%2FSWi6qiFZGltUwcL1%2BjcYhs7g10b80GcrZeWliGqphG6ABZYK3ESLccyq01E%3D" rel="nofollow">Google Drive File查询示例</a></li><li><a href="https://link.segmentfault.com/?enc=KE8JvNFoIcdAh7J9A9tG6Q%3D%3D.urEGRgIrixRB7IX9VRRp18RJDoktjiiDDRnwokwPYfZGtgQaYx59D57k%2BqJYwQ6uX7%2FyTzCNpsQywydBx54KgU2AdzJDUQx7wAOL31vLRoBypMmtBKm7kqNbcVgq%2FBc7" rel="nofollow">Google Api Typescript类型声明</a></li></ul>
Electron包管理技术探索
https://segmentfault.com/a/1190000042686124
2022-10-25T18:15:29+08:00
2022-10-25T18:15:29+08:00
米花儿团儿
https://segmentfault.com/u/mihuartuanr
0
<h2>IPC</h2><h3>Webview内主动触发</h3><h4>异步消息通知</h4><pre><code>// preload.js - 在畫面轉譯處理序中 (網頁)。
const { ipcRenderer } = require('electron')
ipcRenderer.on('need-clean-reply', (event, arg) => {
console.log(arg) // 印出 "貓咪肚子餓"
})
ipcRenderer.send('take-cat-home-message', '帶小貓回家')
// main.js - 在主處理序裡。
const { ipcMain } = require('electron')
ipcMain.on('take-cat-home-message', (event, arg) => {
console.log(arg) // prints "帶小貓回家"
event.reply('need-clean-reply', '貓咪肚子餓')
})
</code></pre><pre><code>// preload.js - 在畫面轉譯處理序中 (網頁)。
const { ipcRenderer } = require('electron')
ipcRenderer.invoke('take-cat-home-handle', '帶小貓回家')
// then 回傳前可以做其他事,例如打掃家裡
.then(msg => console.log(msg)) // prints "小貓肚子餓,喵喵叫"
// main.js - 在主處理序裡。
const { ipcMain } = require('electron')
ipcMain.handle('take-cat-home-handle', async (event, arg) => {
console.log(arg) // prints "帶小貓回家"
return '小貓肚子餓,喵喵叫'
})
</code></pre><h4>同步消息通知</h4><pre><code>// preload.js - 在畫面轉譯處理序中 (網頁)。
const { ipcRenderer } = require('electron')
const message = ipcRenderer.sendSync('take-cat-home-message', '帶小貓回家')
console.log(message) // prints "小貓肚子餓"
// main.js - 在主處理序裡。
const { ipcMain } = require('electron')
ipcMain.on('take-cat-home-message', (event, arg) => {
console.log(arg) // prints "帶小貓回家"
// event 回傳前你一直關注著小貓
event.returnValue = '小貓肚子餓'
})
</code></pre><h3>主线程通知</h3><pre><code>// main.js - 在主處理序裡。
mainWindow.webContents.send('switch-cat', number);
// preload.js - 在畫面轉譯處理序中 (網頁)。
const { ipcRenderer } = require('electron')
ipcRenderer.on('switch-cat', (event, args) => switchCat(args));
</code></pre><h3>获取webContents的方式</h3><h4>主线程</h4><pre><code>ipcMain.on('notify:new-msg', (event, chat) => {
const mainWindow = BrowserWindow.fromWebContents(event.sender); // 利用 event.sender 取得 currentWindow
const isFocused = mainWindow.isFocused(); // 確認 mainWindow 是否在最上面
const myNoti = new Notification({
title: `${chat.name}有新的對話`,
subtitle: chat.msg
});
myNoti.on('click', () => mainWindow.show()); // 將 mainWindow 帶到最上面
myNoti.on('close', () => mainWindow.show()); // 將 mainWindow 帶到最上面
myNoti.show();
if (!isFocused) {
// 工作列按鈕閃爍
mainWindow.flashFrame(true);
}
});
</code></pre><pre><code>const mainWindow = BrowserWindow.fromWebContents(event.sender); // 利用 event.sender 取得 currentWindow
</code></pre><pre><code>BrowserWindow.fromId(this.winId).webContents.send('update-badge', this.badgeCount);
</code></pre><h4>渲染线程</h4><pre><code>// preload.js https://www.npmjs.com/package/@electron/remote
import { remote } from "electron"
remote.getCurrentWindow()
remote.getCurrentWebContents()
</code></pre><h2>Clipboard</h2><pre><code>// @/utils/clipboardUtils.js
const read = clipboard => {
const aFormats = clipboard.availableFormats();
const isImageFormat = aFormats.find(f => f.includes('image'));
const isHtmlFormat = aFormats.find(f => f.includes('text/html'));
const isTextFormat = aFormats.find(f => f.includes('text/plain'));
const isRtfFormat = aFormats.find(f => f.includes('text/rtf'));
if (isImageFormat) {
const nativeImage = clipboard.readImage(); // 取得 clipboard 中的圖片
return nativeImage.toDataURL(); // data:image/png;
}
// 取得 clipboard 中的文字
else if (isTextFormat) return clipboard.readText();
// 取得 clipboard 中的 html 文字
else if (isHtmlFormat) return clipboard.readHTML();
// 取得 clipboard 中的 rtf 文字
else if (isRtfFormat) return clipboard.readRTF();
else return null;
}
module.exports = {
read,
}
</code></pre><h2>下载</h2><h3>Electron内置模块实现</h3><p>在 electron 中的下载行为,都会触发 session 的 <a href="https://link.segmentfault.com/?enc=LH9hAFd8NmfXlNf0pGnc0A%3D%3D.bvyTl0NgVwZnyGn5D7q4aoLN1jjaiyOrSnVg1G6pUjx7R6jrPoLvzhqFYJNWk6CGb8N7itllk%2BOT1IxrtMzMGw%3D%3D" rel="nofollow">will-download</a> 事件。在该事件里面可以获取到 <a href="https://link.segmentfault.com/?enc=42Lrov0Tn5SYn3ByWhJcxg%3D%3D.e4oK8j66Dmh4JZaVEYTWWMz646Nc8DFxPbTduO8wzeUIFKekvpV69S7r2DCz3vxnM7KIylReOZdfe38V3Di%2B9A%3D%3D" rel="nofollow">downloadItem</a> 对象,通过 <a href="https://link.segmentfault.com/?enc=tjtTcACmUVoJCwKo8CuJOA%3D%3D.w7C15JRBb4iCX7%2B2phvy2SlHb7ME3uDYHARYbo8jNBhaDQ%2BW1fDdhNiuJKvKOfmy7QBRtkL7PGar%2BPCRupVaFw%3D%3D" rel="nofollow">downloadItem</a> 对象实现一个简单的文件下载管理器</p><p>拿到 <a href="https://link.segmentfault.com/?enc=bE037Iqhb7iNlRH%2BbrCTaQ%3D%3D.jCD49ICQ7ykHYj0xwzDRsV4g%2FgIRrRJiLTp7jvNTyK8Y%2B8ivTxud%2BzdaW7APTCHPs24sxJnV0ROPM0FrEAbdpw%3D%3D" rel="nofollow">downloadItem</a> 后,暂停、恢复和取消分别调用 <code>pause</code>、<code>resume</code> 和 <code>cancel</code> 方法。当我们要删除列表中正在下载的项,需要先调用 cancel 方法取消下载。</p><p>在 <a href="https://link.segmentfault.com/?enc=vYWwmtJ7%2Fpfur2KmmFqfcA%3D%3D.GYlhG9zvDdkdjQjbE9cwR0dZ7L2QkW0%2BVtWadT5yoKQzkggxo23vMzqdcsBWZo6lHnfuLo6XHAouNqq8CVQ2Lg%3D%3D" rel="nofollow">downloadItem</a> 中监听 updated 事件,可以实时获取到已下载的字节数据,来计算下载进度和每秒下载的速度。</p><blockquote><p>// 计算下载进度</p><p><code>const progress = item.getReceivedBytes() / item.getTotalBytes()</code><br>// 下载速度:<br>已接收字节数 / 耗时</p></blockquote><p><strong>Notes</strong>:</p><ul><li>Electron自身的下载模块忽略headers信息,无法满足断点续下载,需要调研request模块自身实现下载。</li></ul><h3>Request模块实现</h3><p><a href="https://link.segmentfault.com/?enc=kRxdUeCxBxFmDMcwgkHz8g%3D%3D.LepdWtb0dndz1UqEMcoWxldSNkx5tQVejv8%2FiLpCyug79O73lZ6FA8fNc0f3f0Mv4jlzWOAPLtgkKHivadn53w%3D%3D" rel="nofollow">https://github.com/request/re...</a> Request模块请求配置</p><pre><code>import request, { Headers } from "request";
interface DownLoadFileConfiguration {
remoteUrl: string;
localDir: string;
name: string;
onStart: (headers: Headers) => void;
onProgress: (received: number, total: number) => void;
onSuccess: (filePath: string) => void;
onFailed: () => void;
}
function downloadFile(configuration: DownLoadFileConfiguration) {
let received_bytes = 0;
let total_bytes = 0;
const {
remoteUrl,
localDir,
name,
onStart = noop,
onFailed = noop,
onSuccess = noop,
onProgress = noop,
} = configuration;
const req = request({
method: "GET",
uri: remoteUrl,
headers: {
"Content-Type": "application/octet-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
Pragma: "no-cache",
Range: `bytes=${0}-`,
},
});
function abort(this: any, filepath: string) {
this.abort();
// 文件操作https://ourcodeworld.com/articles/read/106/how-to-choose-read-save-delete-or-create-a-file-with-electron-framework
removeFile(filepath);
}
const absolutePath = path.resolve(localDir, name);
const out = fs.createWriteStream(absolutePath);
req.pipe(out);
req.on("response", function (data) {
total_bytes = parseInt(data.headers["content-length"] || "");
const id = uuidv4(); // ⇨ '9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d'
onStart(id, data.headers);
});
if (Object.prototype.hasOwnProperty.call(configuration, "onProgress")) {
req.on("data", function (chunk) {
received_bytes += chunk.length;
onProgress(received_bytes, total_bytes);
});
} else {
req.on("data", function (chunk) {
received_bytes += chunk.length;
});
}
req.on("end", function () {
onSuccess(absolutePath);
});
req.on("error", function (err) {
onFailed();
});
return {
abort: abort.bind(req, absolutePath),
pause: req.pause.bind(req),
resume: req.resume.bind(req),
}
}
</code></pre><p><strong>Notes:</strong></p><ul><li>Mac环境下,针对大文件,该包默认只能下载8s左右,猜测是<code>timeout</code>超时时间问题,使用<code>keep-alive: true</code>就没有问题</li><li><code>request</code>模块的<code>pause</code>、<code>resume</code>、<code>abort</code>方法在文档中没有声明,分别对应暂停、继续、取消操作。</li></ul><h3>断点续下载</h3><h4>前置条件</h4><ul><li>需要服务端支持</li></ul><p><code>Accept-Ranges:bytes</code>可标识当前资源是支持范围请求的。</p><h4>下载的细节处理</h4><ol><li>请求<code>Headers</code>加入<code>Range</code></li></ol><pre><code>fetch(
"http://nginx.org/download/nginx-1.15.8.zip",
{
method: "GET", //请求方式
mode: 'cors',
headers: { //请求头
"Cache-Control": "no-cache",
Connection: "keep-alive",
Pragma: "no-cache",
Range: "bytes=0-1" // Range设置https://www.cnblogs.com/nextl/p/13885225.html
}
}
)
</code></pre><ol start="2"><li>文件合并</li></ol><ul><li><p>fs.createWriteStream创建文件写入流</p><ul><li>flags:如果是追加文件而不是覆盖它,则 <code>flags</code> 模式需为 <code>a</code> 模式而不是默认的 <code>w</code> 模式。</li></ul></li></ul><pre><code> const absolutePath = path.resolve(localDir, name);
const out = fs.createWriteStream(absolutePath, {
flags: resume ? "a" : "w",
});
req.pipe(out);
</code></pre><ul><li>追加(<strong><a href="https://link.segmentfault.com/?enc=xnAMT%2BgxPwa4xQXUIx%2FwFg%3D%3D.YDcr6lYP%2F972uUs%2FRPH1szfNuxp36E5zgjwWddEQorIIkWeVWwoHhQcPUXcB4vS4icxQXrHHTT7XjzYcgY7wzTnDnuj%2FjvKDDKh7QL5%2F4Ho%3D" rel="nofollow">fs.appendFileSync</a></strong>)的方式</li></ul><p><a href="https://link.segmentfault.com/?enc=OLZbgUM6kThxcHhBcXyH4A%3D%3D.eFhzgFAyXvPXehv8NXJPlBOecKliXq0YR1Zh1dw5JvS9u8Xql%2FJwPlRa6fAqAD1M" rel="nofollow">https://www.jianshu.com/p/934...</a></p><h3>分片并发下载</h3><h2>安装第三方应用</h2><blockquote><p>exe安装包不像msi安装包有安装规范,且安装后会自动启动应用程序,而不是安装、启动分为两步。</p><p><a href="https://link.segmentfault.com/?enc=4LCB88yDmMYe9k8bb3x%2BMw%3D%3D.%2Fr5DGEsaF3crTqUNixXjjrdC%2BVL%2F9O5z%2BmY5WDWsxjGnUJ6GWMnBpU9p7JEHhL9IOlMqGtRKYfIarKXUkNAhp759OVgX%2By1g6LgAI2uPQYmvQRDjygvinTb1fTjcdkzBZqJ5EOrMwkv9PylrS8yfEjkVtEk28IHP4Dq8%2FlNHSKQ%3D" rel="nofollow">https://learn.microsoft.com/e...</a></p><p><a href="https://link.segmentfault.com/?enc=Nqp7W2fLwZg%2FSfX8fbHziA%3D%3D.IG1lG7buTDymwUphkWI47KmBUJ0KxKHURWaNbZzlh2J1Bz9QbfW1JLTwyIi1X6JlbWFLp7dSl%2F50OUfE9Vd2Eo%2BIYGnbY25VlnH1NNxrOLY%3D" rel="nofollow">https://www.pdq.com/blog/inst...</a></p></blockquote><pre><code>"Notion Setup 2.0.34.exe" -s /S</code></pre><h2>启动三方应用</h2><blockquote>本质是通过<strong>子进程</strong>,调用不同系统环境的<strong>命令行</strong>唤起应用。</blockquote><pre><code>// /src/classes/App.cls.ts
import { spawn } from "child_process";
class App {
process: any;
pid: number;
constructor(process: any) {
this.process = process;
this.pid = process.pid;
}
private killApp(pid: number) {
// 检测程序是否启动
// wmic process where caption=”XXXX.exe” get caption,commandline /value
// 根据进程名杀死进程
// taskkill /F /IM XXX.exe
spawn("cmd", ["/c", "taskkill", "/pid", "/IM", pid.toString()], {
shell: true, // 隐式调用cmd
stdio: "inherit",
});
}
on(eventType: "error" | "close" | "exit", listener: (...args: any) => void) {
this.process.on(eventType, (code: number) => {
console.log(`spawn error: ${code}`);
listener(code);
});
}
close() {
this.killApp(this.pid);
}
}
export default App;
</code></pre><pre><code>// /src/preload/index.ts
import { spawn } from "child_process";
import App from "../classes/App.cls";
function openApp(path: string): any {
const childProcess = spawn("cmd", ["/c", "start", path], {
shell: true, // 隐式调用cmd
stdio: "inherit",
});
console.log("--------childProcess------", childProcess);
const app = new App(childProcess);
/**
* Notes: 这里没有返回app,而是重新组装了对象 —— preload中无法返回复杂类型
* https://stackoverflow.com/questions/69203193/i-passed-a-class-to-my-front-end-project-through-electrons-preload-but-i-could
* https://www.electronjs.org/docs/latest/api/context-bridge#parameter--error--return-type-support
*/
return {
on: app.on.bind(app),
close: app.close.bind(app)
};
}
contextBridge.exposeInMainWorld("JSBridge", {
openApp,
});
</code></pre><pre><code>// index.vue
const app = window.JSBridge.openApp(absolutePath);
app.on("error", (code: number) => {
console.log("---------spawn error----------", code, absolutePath);
});
app.on("close", (code: number) => {
console.log("---------spawn close----------", code, absolutePath);
});
app.on("exit", (code: number) => {
console.log("---------spawn exit----------", code, absolutePath);
});
setTimeout(() => {
app.close();
}, 10000);
</code></pre><p><a href="https://link.segmentfault.com/?enc=d9jx7Ii6qYDvz%2FmleocUJw%3D%3D.bUcRq9dLGkg%2BEwTuohq2xMgAziNVjay%2B%2FZ59kbVzdvXYap9JNOiTnk1incfU4Emw" rel="nofollow">window命令cmd</a></p><p><a href="https://link.segmentfault.com/?enc=URP6XO9atDfkuHQrKEOjSA%3D%3D.ZXyl9xmXPwtuyvnZGizNNYzr6yPBxxjRuanX7M9XQ%2B0P7%2BT5gevuxndWKRwrq2%2BL" rel="nofollow">Window命令行安装软件</a> —— winget需要安装</p><h2>打包</h2><h3>通用方案</h3><blockquote><p>electron打包工具有两个:<a href="https://link.segmentfault.com/?enc=h2YLyHybmUkVoQsa7u9AZQ==.jgEe+h0wT4V1Yuvbv86WpKLHUya1iX2MsrwONlNjaN0=">electron-builder</a>,<a href="https://link.segmentfault.com/?enc=Iqan4NVCbSBNFWH+lZvJ9A==.3ZEcdmDEJqQdO4Mj+r5SqtSZHUoShHMicxSeAlN+N2lpYZ/kiN0zn8lbxOL3k7CM">electron-packager</a>,官方还提到<a href="https://link.segmentfault.com/?enc=sz0aU3qopjwxg0xE/c39pw==.yHDNiOr4SVmu91HfkAUy67vbfi+n99dQDujQKO0Cy9FPi/gY5f/cwBAkDxt/HQcW6LzuA5f/G2RNYG6GVIGKWg==">electron-forge</a>,其实它不是一个打包工具,而是一个类似于cli的工具集,目的是简化开发到打包的一整套流程,内部打包工具依然是<a href="https://link.segmentfault.com/?enc=xFeuQLj+ap1Slxan51FB1A==.uOwkdrajSo0drxiTpygujSyPl4iLF6sv+9CqVoZJoJCgxC61FVr9JgQ13ko2oEdB">electron-packager</a>。</p><p><a href="https://link.segmentfault.com/?enc=Pj+zwpnxXnwXt8dEHA6mOQ==.p/B78npNSW9u81QBJs+SS+MnZir0AzxOpM/u5OHJVbA=">electron-builder</a>与<a href="https://link.segmentfault.com/?enc=CQVfQ21f04jA8hJxbSia8g==.1LLKaDq8WAeFSb+/S68bSr6+MF6o010XEi0OZXVV0QOY2s0LgDcLO4mb+Tqw9/hA">electron-packager</a>相比各有优劣,<a href="https://link.segmentfault.com/?enc=9td7ZEH+9U5KASuowCtfIA==.h3RvMtST3ub3tJcPwRKE0wOqOAZN78Ywy9roKcGQ9ug=">electron-builder</a>配置项较多,更加灵活,打包体积相对较小,同时上手难度大;而<a href="https://link.segmentfault.com/?enc=kmi5D8SexRy5IhAOM8K7/g==.Pn4LmEb3qv4i0gGlFh3tieFdtXGEyzJotZZyNXilRW2Ue807wXC/OGWoSk0pydIP">electron-packger</a>配置简单易上手,但是打出来的应用程序包体积相对较大。</p><p>打包优化分为打包时间优化和体积优化</p></blockquote><p>双package.json结构</p><p>增加打包配置:安装路径选择,icon等信息</p><h3>平台</h3><p>在 Windows 平台上可以打包成 NSIS 應用與 Portal 應用兩種類型:</p><table><thead><tr><th align="left">类型</th><th align="left">功能特点</th><th align="left">更新方式</th></tr></thead><tbody><tr><td align="left">Portal</td><td align="left">绿色程序,启动exe即可使用</td><td align="left">下載新執行檔案 , 關閉並刪除舊執行檔案 => 更新完成</td></tr><tr><td align="left">NSIS</td><td align="left">安装程序,安装后才能使用</td><td align="left">下載新安裝檔 , 安裝後重開應用程式 => 更新完成</td></tr></tbody></table><ul><li>Portal 版本<br>如果你是 Portal 版的程式 , 只要下載新的 exe , 並覆蓋掉舊的應用程式 , 就算完成更新了 !</li><li>NSIS 版本<br>當你用 electron-builder 生成一個 windows 安裝檔時 , 那個安裝檔就是 NSIS 版本</li></ul><p>使用 electron-builder 產出的 NSIS 安裝檔 , 他具體的安裝步驟如下</p><ul><li>執行安裝檔</li><li>安裝檔比對 Windows 內有沒有 appId 相同的 Electron 程式</li><li>如果有 appId 相同的 Electron 程式 , 執行 old-uninstaller.exe 將舊版解安裝</li><li>安裝目前版本的 Electron 程式 , 並將其開啟</li></ul><p>因此如果你要更新應用程式 , 你只要拿到新版的 NSIS 安裝檔並執行它 , 安裝完成後你就可以享用更新後的應用程式了 !</p><p><a href="https://link.segmentfault.com/?enc=N9fmV1Ici3w%2BgZ761%2Bj7rQ%3D%3D.EEQQZNM4odbvpYlI05xINhKgZVTxXkZz5WrXILARGkbXxY8fZ5eW1bGlWsO%2BvCTP" rel="nofollow">https://ithelp.ithome.com.tw/...</a></p><h2>Q&A</h2><ul><li><p>Q:<code>dialog</code>模块<code>undefined</code></p><ul><li>A:<code>import { remote } from "electron";</code>通过<code>remote.dialog</code>调用</li></ul></li><li><p>Q:<code>An Object could not be cloned?</code></p><ul><li>A:检测IPC通信中是否包含响应式数据</li></ul></li><li><p>Q:下载过程中,定义对象,可以持续更新么?</p><ul><li>A:尝试过,可以。</li></ul></li></ul><h2>参考文档</h2><blockquote>文中有...</blockquote>
二次开发draw.io
https://segmentfault.com/a/1190000041920869
2022-05-31T11:25:25+08:00
2022-05-31T11:25:25+08:00
米花儿团儿
https://segmentfault.com/u/mihuartuanr
4
<h2>准备工作</h2><h3>克隆代码</h3><p>在<a href="https://link.segmentfault.com/?enc=Z21f6K0HUfeCix1o%2F4SkoA%3D%3D.yRZ1wtVLRKoWss1jC8JdgRrQMEaeW1z6wBCm7WKMuTYwX9emW2nWtvaorYF9aKC1" rel="nofollow">github#draw.io</a>切换需要的Tag进行下载,当前以<code>v17.4.3</code>为示例。</p><h3>本地运行</h3><ol><li>安装<code>browser-sync</code>或其它本地服务器工具</li><li>解压<code>drawio-X.zip</code>压缩包,使用IDE打开</li><li><code>browser-sync start --server ./src/main/webapp --files .</code>运行本地<code>3000</code>端口启动服务</li><li>浏览器访问<code>localhost:3000</code> 即可</li></ol><h3>开启调试模式</h3><p>由<code>./src/main/webapp/index.html</code>源码可见,通过URL参数<code>?dev=1</code>开启调试模式。</p><p><strong>Notes:</strong>开启调试模式后,个别静态资源请求会报错 —— 根据报错域名<code>devhost.jgraph.com</code>查找对应资源,修改访问地址:</p><pre><code> if (urlParams['dev'] == '1') {
// Used to request grapheditor/mxgraph sources in dev mode
// var mxDevUrl = document.location.protocol + '//devhost.jgraph.com/drawio/src/main';
var mxDevUrl = './';
// Used to request draw.io sources in dev mode
// var drawDevUrl = document.location.protocol + '//devhost.jgraph.com/drawio/src/main/webapp/';
var drawDevUrl = './';
</code></pre><h2>URL Query String</h2><table><thead><tr><th align="left">参数名</th><th align="left">参数值</th><th align="left">说明</th></tr></thead><tbody><tr><td align="left">dev</td><td align="left">1</td><td align="left">1: 开启调试模式</td></tr><tr><td align="left">local</td><td align="left">1</td><td align="left">1: 只能本地存储</td></tr><tr><td align="left">sync</td><td align="left">'manual'</td><td align="left">--</td></tr><tr><td align="left">appLang</td><td align="left">--</td><td align="left">--</td></tr><tr><td align="left">lang</td><td align="left">'en' 、 'zh' ...</td><td align="left">设置界面语言</td></tr><tr><td align="left">mode</td><td align="left">'dropbox' 、'trello'、'google'</td><td align="left">--</td></tr><tr><td align="left">splash</td><td align="left">'0'</td><td align="left">--</td></tr><tr><td align="left">title</td><td align="left">null</td><td align="left">文件名</td></tr><tr><td align="left">create</td><td align="left">--</td><td align="left">--</td></tr><tr><td align="left">loc</td><td align="left">--</td><td align="left">--</td></tr><tr><td align="left">lightbox</td><td align="left">'1'</td><td align="left">--</td></tr><tr><td align="left">embed</td><td align="left">'1'</td><td align="left">--</td></tr><tr><td align="left">libs</td><td align="left">'1'</td><td align="left">--</td></tr><tr><td align="left">embed</td><td align="left">'aws4' 、 'aws3'</td><td align="left">--</td></tr><tr><td align="left">offline</td><td align="left">'0' 、 '1'</td><td align="left">是否离线存储</td></tr><tr><td align="left">chrome</td><td align="left">'0' 、 '1'</td><td align="left">--</td></tr><tr><td align="left">stealth</td><td align="left">'0' 、 '1'</td><td align="left">--</td></tr><tr><td align="left">embedRT</td><td align="left">'0' 、 '1'</td><td align="left">--</td></tr><tr><td align="left">rt</td><td align="left">'0' 、 '1'</td><td align="left">--</td></tr><tr><td align="left">fast-sync</td><td align="left">''0' 、 '1'</td><td align="left">--</td></tr><tr><td align="left">plugins</td><td align="left">'0' 、 '1'</td><td align="left">--</td></tr><tr><td align="left">db</td><td align="left">'0' 、 '1'</td><td align="left">--</td></tr><tr><td align="left">test</td><td align="left">'0' 、 '1'</td><td align="left">--</td></tr><tr><td align="left">od</td><td align="left">'0' 、 '1'</td><td align="left">--</td></tr><tr><td align="left">tr</td><td align="left">'0' 、 '1'</td><td align="left">--</td></tr><tr><td align="left">extAuth</td><td align="left">'0' 、 '1'</td><td align="left">--</td></tr><tr><td align="left">open</td><td align="left">null</td><td align="left">是否启动时直接打开标签页</td></tr><tr><td align="left">atlas</td><td align="left">'0' 、 '1'</td><td align="left">--</td></tr><tr><td align="left">drive</td><td align="left">'0' 、 '1'</td><td align="left">--</td></tr><tr><td align="left">url</td><td align="left">null</td><td align="left">--</td></tr><tr><td align="left">nowarn</td><td align="left">'0' 、 '1'</td><td align="left">--</td></tr><tr><td align="left">desc</td><td align="left">--</td><td align="left">--</td></tr><tr><td align="left">data</td><td align="left">--</td><td align="left">--</td></tr><tr><td align="left">browser</td><td align="left">0' 、 '1'</td><td align="left">--</td></tr><tr><td align="left">notitle</td><td align="left">0' 、 '1'</td><td align="left">--</td></tr><tr><td align="left">noLangIcon</td><td align="left">0' 、 '1'</td><td align="left">--</td></tr><tr><td align="left">sketch</td><td align="left">'device'</td><td align="left">--</td></tr><tr><td align="left">sockets</td><td align="left">'0' 、 '1'</td><td align="left">--</td></tr><tr><td align="left">lockdown</td><td align="left">'0' 、 '1'</td><td align="left">--</td></tr><tr><td align="left">ignoremime</td><td align="left">--</td><td align="left">--</td></tr><tr><td align="left">thumb</td><td align="left">'0' 、 '1'</td><td align="left">--</td></tr><tr><td align="left">gPickerSize</td><td align="left">--</td><td align="left">--</td></tr><tr><td align="left">thumb</td><td align="left">'0' 、 '1'</td><td align="left">--</td></tr><tr><td align="left">pwa</td><td align="left">'0' 、 '1'</td><td align="left">--</td></tr><tr><td align="left">safe-style-src</td><td align="left">'0' 、 '1'</td><td align="left">--</td></tr><tr><td align="left">page</td><td align="left">--</td><td align="left">--</td></tr><tr><td align="left">sb</td><td align="left">'0' 、 '1'</td><td align="left">--</td></tr><tr><td align="left">pv</td><td align="left">'0' 、 '1'</td><td align="left">--</td></tr><tr><td align="left">edge</td><td align="left">'move'</td><td align="left">--</td></tr><tr><td align="left">viewer</td><td align="left">--</td><td align="left">--</td></tr><tr><td align="left">format</td><td align="left">'0' 、 '1'</td><td align="left">--</td></tr><tr><td align="left">page-id</td><td align="left">--</td><td align="left">--</td></tr><tr><td align="left">rough</td><td align="left">--</td><td align="left">--</td></tr><tr><td align="left">format</td><td align="left">'0' 、 '1'</td><td align="left">--</td></tr><tr><td align="left">modified</td><td align="left">'0' 、 '1'</td><td align="left">--</td></tr><tr><td align="left">saveAndExit</td><td align="left">null</td><td align="left">--</td></tr><tr><td align="left">noSaveBtn</td><td align="left">null</td><td align="left">--</td></tr><tr><td align="left">noExitBtn</td><td align="left">null</td><td align="left">--</td></tr><tr><td align="left">proto</td><td align="left">'json'</td><td align="left">--</td></tr><tr><td align="left">embedInline</td><td align="left">'0' 、 '1'</td><td align="left">--</td></tr><tr><td align="left">publishClose</td><td align="left">'0' 、 '1'</td><td align="left">--</td></tr><tr><td align="left">demo</td><td align="left">'0' 、 '1'</td><td align="left">--</td></tr><tr><td align="left">forceMigration</td><td align="left">--</td><td align="left">--</td></tr><tr><td align="left">publishClose</td><td align="left">'0' 、 '1'</td><td align="left">--</td></tr><tr><td align="left">configure</td><td align="left">'0' 、 '1'</td><td align="left">--</td></tr><tr><td align="left">ui</td><td align="left">null 、'sketch' 、'dark' 、 'atlas' 、 'min'</td><td align="left">修改画布主题,用于与响应式布局.<br>默认:大屏<br>‘sketch':小屏<br>'dark'::深色模式<br>'atlas':蓝色主题<br>'min':工具栏浮层展示</td></tr><tr><td align="left">sidebar-entries</td><td align="left">undefined 、 'large'</td><td align="left">设置侧边栏控件缩略图尺寸<br>默认:32px<br>large: 42px</td></tr><tr><td align="left">export</td><td align="left">--</td><td align="left">--</td></tr><tr><td align="left">gitlab</td><td align="left">--</td><td align="left">--</td></tr><tr><td align="left">gitlab-id</td><td align="left">--</td><td align="left">--</td></tr><tr><td align="left">newTempDlg</td><td align="left">'0' 、 '1'</td><td align="left">--</td></tr><tr><td align="left">keepmodified</td><td align="left">'0' 、 '1'</td><td align="left">--</td></tr><tr><td align="left">enableSpellCheck</td><td align="left">'0' 、 '1'</td><td align="left">--</td></tr><tr><td align="left">winCtrls</td><td align="left">'0' 、 '1'</td><td align="left">--</td></tr><tr><td align="left">libraries</td><td align="left">'0' 、 '1'</td><td align="left">--</td></tr><tr><td align="left">search-shapes</td><td align="left">null</td><td align="left">--</td></tr><tr><td align="left">clibs</td><td align="left">null</td><td align="left">--</td></tr><tr><td align="left">ownerEml</td><td align="left">null</td><td align="left">--</td></tr><tr><td align="left">odAuthCancellable</td><td align="left">'0' 、 '1'</td><td align="left">--</td></tr><tr><td align="left">no-p2p</td><td align="left">'0' 、 '1'</td><td align="left">--</td></tr><tr><td align="left">grid</td><td align="left">'0' 、 '1'</td><td align="left">--</td></tr><tr><td align="left">nav</td><td align="left">'0' 、 '1'</td><td align="left">--</td></tr><tr><td align="left">hide-pages</td><td align="left">'0' 、 '1'</td><td align="left">--</td></tr><tr><td align="left">border</td><td align="left">--</td><td align="left">--</td></tr><tr><td align="left">highlight</td><td align="left">--</td><td align="left">--</td></tr><tr><td align="left">touch</td><td align="left">--</td><td align="left">--</td></tr><tr><td align="left">filesupport</td><td align="left">'0' 、 '1'</td><td align="left">--</td></tr><tr><td align="left">translate-diagram</td><td align="left">'0' 、 '1'</td><td align="left">--</td></tr><tr><td align="left">diagram-language</td><td align="left">'0' 、 '1'</td><td align="left">--</td></tr><tr><td align="left">zoom</td><td align="left">'nocss'</td><td align="left">--</td></tr><tr><td align="left">replay-data</td><td align="left">--</td><td align="left">--</td></tr><tr><td align="left">delay-delay</td><td align="left">--</td><td align="left">--</td></tr><tr><td align="left">orgChartDev</td><td align="left">'0' 、 '1'</td><td align="left">--</td></tr></tbody></table><p>以上参数可通过<code>urlParams['delay-delay']</code>获取。</p><h2>🙇Web Javascript注入顺序</h2><pre><code>// dev: src\main\webapp\index.html
...
var mxDevUrl = './';
var drawDevUrl = './';
var geBasePath = drawDevUrl + '/js/grapheditor';
var mxBasePath = mxDevUrl + '/mxgraph';
...
mxscript(drawDevUrl + 'js/PreConfig.js'); // 全局配置
mxscript(drawDevUrl + 'js/diagramly/Init.js'); // 依据URL Query String初始化urlParmas对象
mxscript(geBasePath + '/Init.js'); // 初始化全局路径
mxscript(mxBasePath + '/mxClient.js'); // 提供控件形状&文本渲染、交互的基础库
mxscript(drawDevUrl + 'js/diagramly/Devel.js'); // 执行初始化脚本列表
mxscript(drawDevUrl + 'js/PostConfig.js'); // 全局配置
...
App.main(); // App对象在 12L mxscript(drawDevUrl + 'js/diagramly/Devel.js')执行脚本列表初始化时注入的。 —— 283L mxscript(drawDevUrl + 'js/diagramly/App.js');
</code></pre><pre><code>// prod:src\main\webapp\index.html
mxscript('js/app.min.js')
</code></pre><h2>万物之初<code>App.main()</code></h2><h3>初始化流程</h3><pre><code>// main {Function} [src/main/webapp/js/diagramly/App.js]
/**
* Program flow starts here.
*
* Optional callback is called with the app instance.
*/
App.main = function(callback, createUi){
...
doMain(); // 根据配置项初始化页面:主题、自动保存、字体、语言;调用同文件中的doLoad()——> 调用realMain()
};
</code></pre><pre><code>// realMain {Function} [src/main/webapp/js/diagramly/App.js]
new Editor()
new App()
EditorUi.call(this, ...)
EditorUi.prototype.createDivs();
EditorUi.prototype.createUi();
EditorUi.prototype.createSidebar()
new Sidebar(this, container);
Sidebar.prototype.init() // 这里对应着侧边栏的全部图形
Sidebar.prototype.initPalettes()
Sidebar.prototype.showEntries() // 调整entires参数即可调整面板控件的展示,且只有Sidebar.prototype.configure注册的控件,才是默认展示可见的
// EditorUi.container.appendChild(this.formatContainer) // 渲染侧边栏DOM,所以,要检索formatContainer的源码处理。
// EditorUi.prototype.createFormat()
// new Format(this, container); // 交互操作逻辑
EditorUi.prototype.refresh();
App.prototype.load()
Editor.graph.setEnabled()
Editor.prototype.createGraph()
mxGraph.setEnabled()
App.prototype.start()
App.prototype.restoreLibraries()
App.prototype.loadLibraries()
this.editor.sidebar // 侧边栏渲染成功
</code></pre><p><strong>Notes:</strong></p><ul><li><code>IndexedDB</code>存储画布图形信息。</li><li><code>localStorage</code>存储配置信息。</li></ul><h3>定制化侧边栏面板</h3><h4><strong>缩减</strong>内置面板:</h4><ol><li>修改<code>Sidebar.prototype.initPalettes()</code></li><li>将<code>addXXXPalette()</code>注释掉,只留下自己需要的面板即可。</li></ol><ul><li>scratchpad面板如何关闭?</li></ul><h4><strong>🙋增加</strong>自定义面板:</h4><ol><li>参照<code>src\main\webapp\js\diagramly\sidebar\Sidebar-Flowchart.js</code>,在<strong>同目录下</strong>定义新的面板函数</li></ol><ul><li><p><strong>自定义面板名</strong></p><ul><li>需要在<code>src\main\webapp\resources\dia.txt</code>(i18n国际化)文件中配置好映射关系,e.g.追加<code>metric=Metric</code> —— 国际化,修改<code>`src\main\webapp\resources\</code>目录下对应的映射关系。</li></ul></li><li>在函数<code>this.addPaletteFunctions(</code>`<strong>'metric'</strong><code>, mxResources.get(</code><strong>'metric'</strong><code>`), false,...</code>定义key值(metric)</li></ul><ol start="2"><li>在<code>src/main/webapp/js/grapheditor/Sidebar.js</code>中参照<code>Sidebar.prototype.init</code>的<strong>111L,</strong>绑定新形状</li><li>在<code>src\main\webapp\js\diagramly\Devel.js</code>中参照<strong>178L,注入</strong>新函数的<strong>脚本</strong></li><li>在<code>Sidebar.prototype.initPalettes()</code>中调用新函数即可。</li></ol><h4>修改侧边栏底部”更多图形“的内容:</h4><ul><li><strong>缩减</strong>内置面板:缩减<code>Sidebar.prototype.init()</code>函数中的<code>this.entries</code>数据元素即可。</li><li>隐藏“更多图形”:因为<strong>没有快捷方式</strong>打开,直接将<code>sidebarFooterHeight</code> 设置为0即可 —— 像最大化有快捷方式的,要注释相关代码。</li></ul><pre><code>EditorUi.prototype.sidebarFooterHeight = 0;</code></pre><h3>定制化控件形状</h3><h4>🙋形状模板</h4><p>参考<code>src\main\webapp\shapes\mxFlowchart.js</code>文件,编程语言为svg基本语法。</p><p><strong>Notes:</strong></p><ul><li>若不需要自定义形状,该项可有可无;</li><li>建议与[增加自定义面板]一一对应创建;</li><li>文件名为<code>mxmetric.js</code>自定义面板的key值</li></ul><h4>形状属性配置</h4><p><code>createVertexTemplateEntry</code>函数参数</p><blockquote><code>createVertexTemplateEntry</code>用于生成图形信息</blockquote><table><thead><tr><th align="left">参数名</th><th align="left">默认值</th><th align="left">说明</th><th align="left">备注</th></tr></thead><tbody><tr><td align="left">style</td><td align="left">--</td><td align="left">s: value内置<br>s2、s3: value外置</td><td align="left">- 圆角不是通过rx、ry,而是通过rounded=1定义<br>- shape=step;可使用内置形状,详见<code>src\main\webapp\js\diagramly\Editor.js</code> 4260L<br>- 形状映射/生成的逻辑见<code>src\main\webapp\mxgraph\mxClient.js</code> 14709L<br>- - 更多style参数参见src/main/webapp/js/diagramly/Editor.js 315L、389L、427L</td></tr><tr><td align="left">width</td><td align="left">100</td><td align="left">--</td><td align="left">--</td></tr><tr><td align="left">height</td><td align="left">100</td><td align="left">--</td><td align="left">--</td></tr><tr><td align="left">value</td><td align="left">null</td><td align="left">文本默认值</td><td align="left">--</td></tr><tr><td align="left">title</td><td align="left">null</td><td align="left">悬浮提示文本</td><td align="left">--</td></tr><tr><td align="left">showLabel</td><td align="left">null</td><td align="left">null:侧边栏预览展示value<br>false:侧边栏预览不展示value</td><td align="left">--</td></tr><tr><td align="left">showTitle</td><td align="left">null</td><td align="left">null:侧边栏预览支持浮层预览<br>false:侧边栏预览不支持浮层预览</td><td align="left">--</td></tr><tr><td align="left">tags</td><td align="left">--</td><td align="left">--</td><td align="left">--</td></tr></tbody></table><p>源码见<code>mxCellRenderer.prototype.createShape</code>的定义,其中根据是否是内置形状分为不同的逻辑:</p><pre><code> null != a.style &&
((b = a.style[mxConstants.STYLE_SHAPE]),
(b =
null == mxCellRenderer.defaultShapes[b]
? mxStencilRegistry.getStencil(b)
: null),
(b = null != b ? new mxShape(b) : new (this.getShapeConstructor(a))()));</code></pre><p>对于侧边栏的预览图,见源码<code>Sidebar.prototype.createThumb</code>。</p><p>业务需求:修改控件缩略图的尺寸,修改源码<code>src\main\webapp\js\grapheditor\Sidebar.js</code>并URL上传参<code>(urlParams['sidebar-entries'] === 'large')</code></p><pre><code>// src\main\webapp\js\grapheditor\Sidebar.js
...
Sidebar.prototype.thumbWidth = 42; // 这里修改成需要尺寸大小,如100
Sidebar.prototype.thumbHeight = 42; // 这里修改成需要尺寸大小,如100
...
if (urlParams['sidebar-entries'] != 'large')
{
Sidebar.prototype.thumbPadding = (document.documentMode >= 5) ? 0 : 1;
Sidebar.prototype.thumbBorder = 1;
Sidebar.prototype.thumbWidth = 32;
Sidebar.prototype.thumbHeight = 30;
Sidebar.prototype.minThumbStrokeWidth = 1.3;
Sidebar.prototype.thumbAntiAlias = true;
}</code></pre><h3>解析控件样式</h3><pre><code>/**
* 输入:mxCell实例
* 输出:mxCell实例style属性对象 —— 该方法将style字符串转为对象
*/
graph.getCurrentCellStyle(cell)</code></pre><h3>获取控件DOM</h3><pre><code>// Returns the DOM nodes for the given cells.
Graph.prototype.getNodesForCells(cells) </code></pre><h3>定制控件id</h3><pre><code>// 重写mxGraphModel.prototype.createId方法
// 注意控件ID必须保证唯一,否则,程序崩溃
mxGraphModel.prototype.createId = function (a) {
var styleProperties, id;
if (a.style) {
styleProperties = mxUtils.parseStyle(a.style)
id = mxUtils.getValue(styleProperties, 'id', null)
}
if (!id) {
a = this.nextId;
this.nextId++;
}
return id ? id : this.prefix + a + this.postfix;
};
// 获取style中的id
var insertCellStyleProperties = graph.getCurrentCellStyle(insertCell)
var insertCellId = mxUtils.getValue(insertCellStyleProperties, 'id', '')</code></pre><h3>新增控件属性</h3><pre><code>mxUtils.parseStyle = function(a) {
const cellProperties = a.split(';').filter(item => !!item.trim()).reduce((sum, cur) => {
const [key, value] = cur.split('=')
sum[key.trim()] = typeof value === 'string' ? value.trim() : value
return sum
}, {})
return cellProperties
}
// 新增代码逻辑定义新增字段
mxCell.prototype.__originId = null;
mxCell.prototype.__getOriginId = function () {
return this.__originId;
};
mxCell.prototype.__setOriginId = function (a) {
if (a) {
this.__originId = a;
}
};
mxCell.prototype.onInit = function () {
var s = this.getStyle()
if (s) {
var o = mxUtils.parseStyle(s)
this.__setOriginId(o.id)
}
}</code></pre><h3>定制控件文本</h3><p>内置Shape样式详见<code>src\main\webapp\mxgraph\mxClient.js</code>文件格式化后的代码第5179L。</p><p>通过<code>this.createVertexTemplateEntry()</code>创建的控件,文本源码详见<code>function mxText</code>的定义,样式见<code>mxCellRenderer.prototype.createLabel</code>中的<code>a.text = new this.defaultTextShape</code>,简单来说,通过<code>style</code>的<code>fontSize</code>可以自定义字号。</p><p>业务需求:改控件文本fontSize,<code>this.createVertexTemplateEntry(s + 'view;portConstraint=eastwest;cloneable=0;rotatable=0;editable=0;deletable=1;resizable=0;rounded=1;snapToPoint=1;points=[[0, 0.5],[1, 0.5]];whiteSpace=wrap;fontSize=24;size=0.5;', w, h * 0.6, '曝光', '曝光', null, null, this.getTagsForStencil(gn, 'view', dt).join(' '))</code>中的<code>fontSize=24;</code></p><p><code>Sidebar.prototype.sidebarTitles = true</code>可额外在侧边栏面板中展示控件label。</p><h3>更新控件样式</h3><p>通过原始功能判断是否有内置方法属性</p><p><img src="/img/remote/1460000041930672" alt="image.png" title="image.png"></p><p>有内置可直接调用</p><pre><code>// src/main/webapp/js/diagramly/Editor.js
function createCheckbox(pName, pValue, prop)
{
var input = document.createElement('input');
input.type = 'checkbox';
input.checked = pValue == '1';
mxEvent.addListener(input, 'change', function()
{
applyStyleVal(pName, input.checked? '1' : '0', prop);
});
return input;
};
...
// 样式的改变也会触发统一事件
ui.fireEvent(new mxEventObject('styleChanged', 'keys', [mxConstants.STYLE_ROUNDED, mxConstants.STYLE_CURVED],
'values', ['0', '0'], 'cells', graph.getSelectionCells()));
// 监听处理的地方src/main/webapp/js/grapheditor/EditorUi.js 710L
this.addListener('styleChanged', mxUtils.bind(this, function(sender, evt)</code></pre><p>无内置,可参考相关逻辑自定义</p><h3>定制连线样式</h3><p>修改EdgeStyle即可</p><pre><code>// src/main/webapp/js/grapheditor/Graph.js 7731L
Graph.prototype.createCurrentEdgeStyle = function()
{
var style = 'edgeStyle=' + (this.currentEdgeStyle['edgeStyle'] || 'none') + ';flowAnimation=1;';
var keys = ['shape', 'curved', 'rounded', 'comic', 'sketch', 'fillWeight', 'hachureGap',
'hachureAngle', 'jiggle', 'disableMultiStroke', 'disableMultiStrokeFill', 'fillStyle',
'curveFitting', 'simplification', 'comicStyle', 'jumpStyle', 'jumpSize'];</code></pre><p>定位相关逻辑</p><pre><code>// src/main/webapp/js/grapheditor/EditorUi.js 1620L
graph.connectVertex(temp, dir, graph.defaultEdgeLength, mouseEvent, true, true, function(x, y, execute)
{
execute(cell);
if (ui.hoverIcons != null)
{
ui.hoverIcons.update(graph.view.getState(cell));
}
}, function(cells)
{
graph.selectCellsForConnectVertex(cells);
}, mouseEvent, this.hoverIcons);
// src/main/webapp/js/grapheditor/Graph.js 4300L
Graph.prototype.connectVertex = function(sourc
...
this.createCurrentEdgeStyle()</code></pre><h3>定制控件交互性</h3><p>业务需求:控件是不可编辑且不可调整尺寸的——<br><code>this.createVertexTemplateEntry(s + 'view;editable=0;resizable=0;rounded=1;whiteSpace=wrap;size=0.25;', w, h * 0.6, '曝光', '曝光', null, null, this.getTagsForStencil(gn, 'view', dt).join(' ')),</code>中的<code>editable=0;resizable=0;</code></p><p>更多控件属性参见下边的代码。<br><strong>Notes:</strong>以下name字段是属性名,只不过这里的type是经过逻辑处理后的属性类型,不一定是定义时传入的类型。</p><pre><code>// src/main/webapp/js/diagramly/Editor.js 389L
/**
* Common properties for all edges.
*/
Editor.commonEdgeProperties = [
{type: 'separator'},
{name: 'arcSize', dispName: 'Arc Size', type: 'float', min:0, defVal: mxConstants.LINE_ARCSIZE},
{name: 'sourcePortConstraint', dispName: 'Source Constraint', type: 'enum', defVal: 'none',
enumList: [{val: 'none', dispName: 'None'}, {val: 'north', dispName: 'North'}, {val: 'east', dispName: 'East'}, {val: 'south', dispName: 'South'}, {val: 'west', dispName: 'West'}]
},
{name: 'targetPortConstraint', dispName: 'Target Constraint', type: 'enum', defVal: 'none',
enumList: [{val: 'none', dispName: 'None'}, {val: 'north', dispName: 'North'}, {val: 'east', dispName: 'East'}, {val: 'south', dispName: 'South'}, {val: 'west', dispName: 'West'}]
},
{name: 'jettySize', dispName: 'Jetty Size', type: 'int', min: 0, defVal: 'auto', allowAuto: true, isVisible: function(state)
{
return mxUtils.getValue(state.style, mxConstants.STYLE_EDGE, null) == 'orthogonalEdgeStyle';
}},
{name: 'fillOpacity', dispName: 'Fill Opacity', type: 'int', min: 0, max: 100, defVal: 100},
{name: 'strokeOpacity', dispName: 'Stroke Opacity', type: 'int', min: 0, max: 100, defVal: 100},
{name: 'startFill', dispName: 'Start Fill', type: 'bool', defVal: true},
{name: 'endFill', dispName: 'End Fill', type: 'bool', defVal: true},
{name: 'perimeterSpacing', dispName: 'Terminal Spacing', type: 'float', defVal: 0},
{name: 'anchorPointDirection', dispName: 'Anchor Direction', type: 'bool', defVal: true},
{name: 'snapToPoint', dispName: 'Snap to Point', type: 'bool', defVal: false},
{name: 'fixDash', dispName: 'Fixed Dash', type: 'bool', defVal: false},
{name: 'editable', dispName: 'Editable', type: 'bool', defVal: true},
{name: 'metaEdit', dispName: 'Edit Dialog', type: 'bool', defVal: false},
{name: 'backgroundOutline', dispName: 'Background Outline', type: 'bool', defVal: false},
{name: 'bendable', dispName: 'Bendable', type: 'bool', defVal: true},
{name: 'movable', dispName: 'Movable', type: 'bool', defVal: true},
{name: 'cloneable', dispName: 'Cloneable', type: 'bool', defVal: true},
{name: 'deletable', dispName: 'Deletable', type: 'bool', defVal: true},
{name: 'noJump', dispName: 'No Jumps', type: 'bool', defVal: false},
{name: 'flowAnimation', dispName: 'Flow Animation', type: 'bool', defVal: false},
{name: 'ignoreEdge', dispName: 'Ignore Edge', type: 'bool', defVal: false},
{name: 'orthogonalLoop', dispName: 'Loop Routing', type: 'bool', defVal: false},
{name: 'orthogonal', dispName: 'Orthogonal', type: 'bool', defVal: false}
].concat(Editor.commonProperties);
/**
* Common properties for all vertices.
*/
Editor.commonVertexProperties = [
{name: 'colspan', dispName: 'Colspan', type: 'int', min: 1, defVal: 1, isVisible: function(state, format)
{
var graph = format.editorUi.editor.graph;
return state.vertices.length == 1 && state.edges.length == 0 && graph.isTableCell(state.vertices[0]);
}},
{name: 'rowspan', dispName: 'Rowspan', type: 'int', min: 1, defVal: 1, isVisible: function(state, format)
{
var graph = format.editorUi.editor.graph;
return state.vertices.length == 1 && state.edges.length == 0 && graph.isTableCell(state.vertices[0]);
}},
{type: 'separator'},
{name: 'resizeLastRow', dispName: 'Resize Last Row', type: 'bool', getDefaultValue: function(state, format)
{
var cell = (state.vertices.length == 1 && state.edges.length == 0) ? state.vertices[0] : null;
var graph = format.editorUi.editor.graph;
var style = graph.getCellStyle(cell);
return mxUtils.getValue(style, 'resizeLastRow', '0') == '1';
}, isVisible: function(state, format)
{
var graph = format.editorUi.editor.graph;
return state.vertices.length == 1 && state.edges.length == 0 &&
graph.isTable(state.vertices[0]);
}},
{name: 'resizeLast', dispName: 'Resize Last Column', type: 'bool', getDefaultValue: function(state, format)
{
var cell = (state.vertices.length == 1 && state.edges.length == 0) ? state.vertices[0] : null;
var graph = format.editorUi.editor.graph;
var style = graph.getCellStyle(cell);
return mxUtils.getValue(style, 'resizeLast', '0') == '1';
}, isVisible: function(state, format)
{
var graph = format.editorUi.editor.graph;
return state.vertices.length == 1 && state.edges.length == 0 &&
graph.isTable(state.vertices[0]);
}},
{name: 'fillOpacity', dispName: 'Fill Opacity', type: 'int', min: 0, max: 100, defVal: 100},
{name: 'strokeOpacity', dispName: 'Stroke Opacity', type: 'int', min: 0, max: 100, defVal: 100},
{name: 'overflow', dispName: 'Text Overflow', defVal: 'visible', type: 'enum',
enumList: [{val: 'visible', dispName: 'Visible'}, {val: 'hidden', dispName: 'Hidden'}, {val: 'block', dispName: 'Block'},
{val: 'fill', dispName: 'Fill'}, {val: 'width', dispName: 'Width'}]
},
{name: 'noLabel', dispName: 'Hide Label', type: 'bool', defVal: false},
{name: 'labelPadding', dispName: 'Label Padding', type: 'float', defVal: 0},
{name: 'direction', dispName: 'Direction', type: 'enum', defVal: 'east',
enumList: [{val: 'north', dispName: 'North'}, {val: 'east', dispName: 'East'}, {val: 'south', dispName: 'South'}, {val: 'west', dispName: 'West'}]
},
{name: 'portConstraint', dispName: 'Constraint', type: 'enum', defVal: 'none',
enumList: [{val: 'none', dispName: 'None'}, {val: 'north', dispName: 'North'}, {val: 'east', dispName: 'East'}, {val: 'south', dispName: 'South'}, {val: 'west', dispName: 'West'}]
},
{name: 'portConstraintRotation', dispName: 'Rotate Constraint', type: 'bool', defVal: false},
{name: 'connectable', dispName: 'Connectable', type: 'bool', getDefaultValue: function(state, format)
{
var cell = (state.vertices.length > 0 && state.edges.length == 0) ? state.vertices[0] : null;
var graph = format.editorUi.editor.graph;
return graph.isCellConnectable(cell);
}, isVisible: function(state, format)
{
return state.vertices.length > 0 && state.edges.length == 0;
}},
{name: 'allowArrows', dispName: 'Allow Arrows', type: 'bool', defVal: true},
{name: 'snapToPoint', dispName: 'Snap to Point', type: 'bool', defVal: false},
{name: 'perimeter', dispName: 'Perimeter', defVal: 'none', type: 'enum',
enumList: [{val: 'none', dispName: 'None'},
{val: 'rectanglePerimeter', dispName: 'Rectangle'}, {val: 'ellipsePerimeter', dispName: 'Ellipse'},
{val: 'rhombusPerimeter', dispName: 'Rhombus'}, {val: 'trianglePerimeter', dispName: 'Triangle'},
{val: 'hexagonPerimeter2', dispName: 'Hexagon'}, {val: 'lifelinePerimeter', dispName: 'Lifeline'},
{val: 'orthogonalPerimeter', dispName: 'Orthogonal'}, {val: 'backbonePerimeter', dispName: 'Backbone'},
{val: 'calloutPerimeter', dispName: 'Callout'}, {val: 'parallelogramPerimeter', dispName: 'Parallelogram'},
{val: 'trapezoidPerimeter', dispName: 'Trapezoid'}, {val: 'stepPerimeter', dispName: 'Step'},
{val: 'centerPerimeter', dispName: 'Center'}]
},
{name: 'fixDash', dispName: 'Fixed Dash', type: 'bool', defVal: false},
{name: 'autosize', dispName: 'Autosize', type: 'bool', defVal: false},
{name: 'container', dispName: 'Container', type: 'bool', defVal: false, isVisible: function(state, format)
{
return state.vertices.length == 1 && state.edges.length == 0;
}},
{name: 'dropTarget', dispName: 'Drop Target', type: 'bool', getDefaultValue: function(state, format)
{
var cell = (state.vertices.length == 1 && state.edges.length == 0) ? state.vertices[0] : null;
var graph = format.editorUi.editor.graph;
return cell != null && (graph.isSwimlane(cell) || graph.model.getChildCount(cell) > 0);
}, isVisible: function(state, format)
{
return state.vertices.length == 1 && state.edges.length == 0;
}},
{name: 'collapsible', dispName: 'Collapsible', type: 'bool', getDefaultValue: function(state, format)
{
var cell = (state.vertices.length == 1 && state.edges.length == 0) ? state.vertices[0] : null;
var graph = format.editorUi.editor.graph;
return cell != null && ((graph.isContainer(cell) && state.style['collapsible'] != '0') ||
(!graph.isContainer(cell) && state.style['collapsible'] == '1'));
}, isVisible: function(state, format)
{
return state.vertices.length == 1 && state.edges.length == 0;
}},
{name: 'recursiveResize', dispName: 'Resize Children', type: 'bool', defVal: true, isVisible: function(state, format)
{
return state.vertices.length == 1 && state.edges.length == 0 &&
!format.editorUi.editor.graph.isSwimlane(state.vertices[0]) &&
mxUtils.getValue(state.style, 'childLayout', null) == null;
}},
{name: 'expand', dispName: 'Expand', type: 'bool', defVal: true},
{name: 'part', dispName: 'Part', type: 'bool', defVal: false, isVisible: function(state, format)
{
var model = format.editorUi.editor.graph.model;
return (state.vertices.length > 0) ? model.isVertex(model.getParent(state.vertices[0])) : false;
}},
{name: 'editable', dispName: 'Editable', type: 'bool', defVal: true},
{name: 'metaEdit', dispName: 'Edit Dialog', type: 'bool', defVal: false},
{name: 'backgroundOutline', dispName: 'Background Outline', type: 'bool', defVal: false},
{name: 'movable', dispName: 'Movable', type: 'bool', defVal: true},
{name: 'movableLabel', dispName: 'Movable Label', type: 'bool', defVal: false, isVisible: function(state, format)
{
var geo = (state.vertices.length > 0) ? format.editorUi.editor.graph.getCellGeometry(state.vertices[0]) : null;
return geo != null && !geo.relative;
}},
{name: 'resizable', dispName: 'Resizable', type: 'bool', defVal: true},
{name: 'resizeWidth', dispName: 'Resize Width', type: 'bool', defVal: false},
{name: 'resizeHeight', dispName: 'Resize Height', type: 'bool', defVal: false},
{name: 'rotatable', dispName: 'Rotatable', type: 'bool', defVal: true},
{name: 'cloneable', dispName: 'Cloneable', type: 'bool', defVal: true},
{name: 'deletable', dispName: 'Deletable', type: 'bool', defVal: true},
{name: 'treeFolding', dispName: 'Tree Folding', type: 'bool', defVal: false},
{name: 'treeMoving', dispName: 'Tree Moving', type: 'bool', defVal: false},
{name: 'pointerEvents', dispName: 'Pointer Events', type: 'bool', defVal: true, isVisible: function(state, format)
{
var fillColor = mxUtils.getValue(state.style, mxConstants.STYLE_FILLCOLOR, null);
return format.editorUi.editor.graph.isSwimlane(state.vertices[0]) ||
fillColor == null || fillColor == mxConstants.NONE ||
mxUtils.getValue(state.style, mxConstants.STYLE_FILL_OPACITY, 100) == 0 ||
mxUtils.getValue(state.style, mxConstants.STYLE_OPACITY, 100) == 0 ||
state.style['pointerEvents'] != null;
}},
{name: 'moveCells', dispName: 'Move Cells on Fold', type: 'bool', defVal: false, isVisible: function(state, format)
{
return state.vertices.length > 0 && format.editorUi.editor.graph.isContainer(state.vertices[0]);
}}
].concat(Editor.commonProperties);</code></pre><h3>定制控件浮层</h3><pre><code>this.createVertexTemplateEntry(s + 'view;portConstraint=eastwest;cloneable=0;rotatable=0;editable=0;deletable=1;resizable=0;rounded=1;snapToPoint=1;points=[[0, 0.5],[1, 0.5]];whiteSpace=wrap;size=0.25;', w, h * 0.6, '曝光', '曝光', null, null, this.getTagsForStencil(gn, 'view', dt).join(' ')),</code></pre><p>通过<code>portConstraint</code><em>枚举值</em>来定义控件悬浮时的箭头按钮展示。<br>控件悬浮<em>箭头</em>部分源码见<code>src\main\webapp\js\grapheditor\Graph.js</code>文件中<code>HoverIcons</code>的定义。</p><p>针对于浮层中的<em>控件</em>,源码见<code>src\main\webapp\js\grapheditor\EditorUi.js</code>文件中的<code>EditorUi.prototype.getCellsForShapePicker</code>的定义。</p><h3>❗❗❗🚥定义控件并指定坐标插入</h3><pre><code>// 未梳理的代码,拷贝的源码其他部分
function updatePageLabelLocal(target, benchmark, ui, graph)
{
graph.setSelectionCell(benchmark);
var result = ui.initSelectionState()
ui.updateSelectionStateForCell(result, benchmark, [benchmark], true);
// var rect = ui.getSelectionState();
var rect = result;
function formatHintText(pixels)
{
var unit = graph.view.unit;
switch(unit)
{
case mxConstants.POINTS:
return pixels;
case mxConstants.MILLIMETERS:
return (pixels / mxConstants.PIXELS_PER_MM).toFixed(1);
case mxConstants.METERS:
return (pixels / (mxConstants.PIXELS_PER_MM * 1000)).toFixed(4);
case mxConstants.INCHES:
return (pixels / mxConstants.PIXELS_PER_INCH).toFixed(2);
}
};
var getUnit = function () {
var unit = graph.view.unit;
switch(unit)
{
case mxConstants.POINTS:
return 'pt';
case mxConstants.INCHES:
return '"';
case mxConstants.MILLIMETERS:
return 'mm';
case mxConstants.METERS:
return 'm';
}
};
var x, y
if (rect.vertices.length == graph.getSelectionCount() && rect.x != null && rect.y != null)
{
x = formatHintText(rect.x) + ((rect.x == '') ? '' : ' ' + getUnit());
y = formatHintText(rect.y) + ((rect.y == '') ? '' : ' ' + getUnit());
}
var direction = ['x', 'y']
new Array(x, y).forEach((input, idx) => {
if (input != '')
{
var value = parseFloat(input);
try
{
var cells = [target];
for (var i = 0; i < cells.length; i++)
{
if (graph.getModel().isVertex(cells[i]))
{
var geo = graph.getCellGeometry(cells[i]);
if (geo != null)
{
geo = geo.clone();
if (geo.relative)
{
geo.offset[direction[idx]] = direction[idx] === 'x' ? value + 24 : value - 40 ;
}
else
{
geo[direction[idx]] = direction[idx] === 'x' ? value + 24 : value - 40 ;
}
var state = graph.view.getState(cells[i]);
if (state != null && graph.isRecursiveVertexResize(state))
{
graph.resizeChildCells(cells[i], geo);
}
graph.getModel().setGeometry(cells[i], geo);
graph.constrainChildCells(cells[i]);
}
}
}
}
finally
{
}
}
})
};
// 插入
Sidebar.prototype.__updatePageIndicator = function(benchmark, indicator)
{
var graph = this.editorUi.editor.graph;
graph.container.focus();
var styleProperties = graph.getCurrentCellStyle(benchmark)
var pageId = mxUtils.getValue(styleProperties, 'id', '')
if (pageId) {
var cells = graph.getModel().cells
var pageDescriptor = pageDimensionMock.find(item => item.id === pageId)
var pageIdicatorDescriptor = pageDescriptor.indicator && pageDescriptor.indicator[indicator]
var labelId = 'label_for_' + pageId
var labelCell = cells[labelId]
var label = pageIdicatorDescriptor
? pageIdicatorDescriptor.name + ':' + pageIdicatorDescriptor.value
: ''
if (labelCell) {
graph.labelChanged(labelCell, label, false)
} else {
var target = graph.createVertex(
null,
labelId,
label || '',
0,
0,
120,
40,
[
'text;',
'html=1;',
'align=center;',
'verticalAlign=middle;',
'resizable=0;',
'cloneable=0;',
'rotatable=0;',
'editable=0;',
'deletable=1;',
'points=[];',
'autosize=1;',
'strokeColor=none;',
'fillColor=none;'
].join(''),
false
);
graph.getModel().beginUpdate();
graph.addCell(target);
graph.fireEvent(new mxEventObject('cellsInserted', 'cells', [target]));
// graph.getModel().beginUpdate();
// var tr = graph.view.translate;
// var s = graph.view.scale;
var pt = benchmark.geometry;
// var node = graph.getNodesForCells([benchmark]) || []
// // var pos = mxUtils.convertPoint(graph.container, benchmark.geometry.x, benchmark.geometry.y);
// var pos = (node[0] && node[0].getBoundingClientRect()) || {}
// TODO: 定位
// target.geometry.x = pt.x / s - tr.x - target.geometry.width / 2;
// target.geometry.y = pt.y / s - tr.y - target.geometry.height / 2;
graph.getModel().endUpdate();
graph.getModel().beginUpdate();
updatePageLabelLocal(target, benchmark, this.editorUi, graph)
graph.getModel().endUpdate();
}
}
};</code></pre><h2>Editor Configure</h2><p>传递形式hash参数,以<code>_CONFIG_</code>为前缀,示例内容:</p><pre><code>http://localhost:3000/?dev=1&lang=zh&ui=dark&sidebar-entries=large#_CONFIG_JTdCJTIyc2lkZWJhclRpdGxlcyUyMiUzQXRydWUlN0Q=</code></pre><p>配置文件的值需要经过JSON.stringify()、encodeURIComponent()</p><h3>😤事件绑定</h3><h4>核心逻辑</h4><pre><code>// src\main\webapp\js\grapheditor\EditorUi.js核心代码
...
this.actions = new Actions(this);
...
keyHandler.bindAction = mxUtils.bind(this, function(code, control, key, shift)
{
var action = this.actions.get(key);
if (action != null)
{
var f = function()
{
if (action.isEnabled())
{
action.funct();
}
};
...
EditorUi.prototype.createKeyHandler = function(editor) {
...
var keyHandler = new mxKeyHandler(graph);
...
keyHandler.bindAction(107, true, 'zoomIn'); // Ctrl+Plus
keyHandler.bindAction(109, true, 'zoomOut'); // Ctrl+Minus
keyHandler.bindAction(80, true, 'print'); // Ctrl+P
keyHandler.bindAction(79, true, 'outline', true); // Ctrl+Shift+O
...</code></pre><h4>事件触发</h4><pre><code>this.editorUi.actions.get('print').funct()</code></pre><h4>事件回调</h4><p>源码中,大部分事件都会抛出一个自定义事件,在业务逻辑上监听该自定义事件,即可写事件回调。</p><p>下面以自定义删除事件的回调为例:</p><pre><code>//事件监听
/**
* 锁定元素
*/
var lockCells = Object.values(graph.getModel().cells)
graph.setSelectionCells(lockCells)
var lockUnlockAction = this.editorUi.actions.get('lockUnlock').funct
var deleteAllAction = this.editorUi.actions.get('deleteAll').funct
lockUnlockAction()
graph.clearSelection()
graph.getModel().endUpdate();
function clearLabel (graph, evt) {
var eventName = evt.name
if (eventName !== 'removeCells') return
var eventTarget = evt.properties.cells[0]
/**
* 拖动Cell时,也会触发removeCells事件,此时eventTarget === undefined
* 删除Cell时,触发removeCells事件,此时eventTarget为删除的Cell实例
*/
if (!eventTarget) return
var deleteCellStyleProperties = graph.getCurrentCellStyle(eventTarget)
if (deleteCellStyleProperties.shape === 'label') return
/**
* 该逻辑会走两次
* 第一次:删除当前Cell触发
* 第二次:该逻辑中deleteAllAction()调用引发的触发,第二次需要忽略
*/
currentInstalledIndicator = null
graph.getModel().beginUpdate();
graph.setSelectionCells(Object.values(graph.getModel().cells))
lockUnlockAction()
graph.clearSelection()
graph.getModel().endUpdate();
setTimeout(() => {
/**
* 该逻辑要延时执行,因为lockUnlockAction的状态无法同步更新的view.state中,倒是deleteAllAction中的是否可删除判断逻辑无法执行后续
*/
try {
graph.getModel().beginUpdate();
graph.setSelectionCells(cellLabels)
deleteAllAction()
edges.forEach(edge => {
graph.labelChanged(edge, '', false)
})
} catch {
} finally {
graph.removeListener(clearLabel)
graph.getModel().endUpdate();
}
}, 4)
}
graph.addListener(mxEvent.REMOVE_CELLS, clearLabel)
// 源码中抛出事件的代码src/main/webapp/mxgraph/mxClient.js
mxGraph.prototype.removeCells = function (a, b) {
b = null != b ? b : !0;
null == a && (a = this.getDeletableCells(this.getSelectionCells()));
if (b) a = this.getDeletableCells(this.addAllEdges(a));
else {
a = a.slice();
for (
var c = this.getDeletableCells(this.getAllEdges(a)),
d = new mxDictionary(),
e = 0;
e < a.length;
e++
)
d.put(a[e], !0);
for (e = 0; e < c.length; e++)
null != this.view.getState(c[e]) ||
d.get(c[e]) ||
(d.put(c[e], !0), a.push(c[e]));
}
this.model.beginUpdate();
try {
this.cellsRemoved(a),
this.fireEvent(
new mxEventObject(mxEvent.REMOVE_CELLS, 'cells', a, 'includeEdges', b)
);
} finally {
this.model.endUpdate();
}
return a;
};</code></pre><h4>事件禁用</h4><pre><code>// src\main\webapp\js\grapheditor\Actions.js
Action.prototype.setEnabled = function(value)
{
if (this.enabled != value)
{
this.enabled = value;
this.fireEvent(new mxEventObject('stateChanged'));
}
};</code></pre><p>比如,Delete、Backspace的删除键在只选择一个控件时,是被禁用的</p><pre><code>// src\main\webapp\js\grapheditor\EditorUi.js
var actions = ['cut', 'copy', 'bold', 'italic', 'underline', 'delete', 'duplicate',
'editStyle', 'editTooltip', 'editLink', 'backgroundColor', 'borderColor',
'edit', 'toFront', 'toBack', 'solid', 'dashed', 'pasteSize',
'dotted', 'fillColor', 'gradientColor', 'shadow', 'fontColor',
'formattedText', 'rounded', 'toggleRounded', 'strokeColor',
'sharp', 'snapToGrid'];
for (var i = 0; i < actions.length; i++)
{
this.actions.get(actions[i]).setEnabled(ss.cells.length > 0);
}</code></pre><h4>Delete无法对单控件使用</h4><p>在上述代码中,事件名删除<code>delete</code>即可。</p><h3>侧边栏的事件</h3><h4>缩略图拖拽事件</h4><pre><code>// src/main/webapp/js/grapheditor/Sidebar.js
// 初始化侧边栏面板后,首次展开Palette时进行绑定,调用的函数顺序
Sidebar.prototype.createItem =
...
Sidebar.prototype.createDropHandler =
....
Sidebar.prototype.createDragPreview =
...
Sidebar.prototype.isDropStyleEnabled = </code></pre><h4>缩略图点击事件</h4><pre><code>// src/main/webapp/js/diagramly/sidebar/Sidebar.js
// 该事件覆盖了src/main/webapp/js/grapheditor/Sidebar.js中的对应事件
Sidebar.prototype.itemClicked = function(cells, ds, evt) {
...</code></pre><h3>其他对象事件触发</h3><pre><code>graph.addListener('cellsInserted', function(sender, evt)
{
insertHandler(evt.getProperty('cells'), null, null, null, null, true, true);
});
...
graph.fireEvent(new mxEventObject('cellsInserted', 'cells', select));</code></pre><h3>调用弹窗</h3><pre><code>// Dialog确认框
var popup = new TextareaDialog(this.editorUi, 'baidu.com', '1313123', mxUtils.bind(this, function () {
his.hideDialog()
showSecondDialog()
}))
this.editorUi.showDialog(popup.container, 300, 200)
// 自定义弹窗
var popup = new CustomDialog(
this.editorUi,
document.createTextNode('baidu.com'),
mxUtils.bind(this, function () {
console.log('----ok----')
}),
mxUtils.bind(this, function () {
console.log('----cancel----')
})
)
this.editorUi.showDialog(popup.container, 300, 200)
// 内置Confirm
EditorUi.prototype.confirm = function(msg, okFn, cancelFn)
{
if (mxUtils.confirm(msg))
{
if (okFn != null)
{
okFn();
}
}
else if (cancelFn != null)
{
cancelFn();
}
};
this.editorUi.confirm(
'确认么',
function() {
// ok
}
)</code></pre><pre><code>// Error提示框
this.editorUi.showError(mxResources.get('error'), mxResources.get('notInOffline'));
// showAlert(message)
// showError(title, message)
//showSplash
//showWarning</code></pre><h3>节点事件调用</h3><h4>添加节点</h4><pre><code> graph.fireEvent(new mxEventObject('cellsInserted', 'cells', select));</code></pre><h4>删除节点</h4><pre><code>// 独立节点,没有连接线
graph.deleteCells([insertCell], false)</code></pre><h4>添加/修改文本</h4><pre><code>// 以连线文本为例,lineCell.contructor === mxCell,与事件无关
graph.labelChanged(lineCell, 'TEST', false)
// 针对事件target、client X| Y插入文本,与事件对象挂钩
graph.insertTextForEvent(evt, cell)
// 底层mxcell设置文本
mxCell.getValue()
mxCell.setValue(newV)
mxCell.valueChange: (newV) => oldV</code></pre><h4>添加/修改事件监听</h4><pre><code>Graph.prototype.cellLabelChanged =
....</code></pre><h2>国际化语言支持</h2><p><code>window.mxLanguageMap = window.mxLanguageMap ||</code>修改该项可以调整右上角的语言选项,通过URL Query String配置<code>lang</code>可以指定某一语言。</p><h3>默认语言设置</h3><p>方案一:全局变量</p><pre><code>// src\main\webapp\index.html
var drawDevUrl = './';
var geBasePath = drawDevUrl + '/js/grapheditor';
var mxBasePath = mxDevUrl + '/mxgraph';
var mxLanguage = 'zh';</code></pre><p>方案二:参数对象默认值</p><pre><code>// src\main\webapp\js\PreConfig.js
window.EXPORT_URL = 'REPLACE_WITH_YOUR_IMAGE_SERVER';
window.PLANT_URL = 'REPLACE_WITH_YOUR_PLANTUML_SERVER';
window.DRAWIO_BASE_URL = null; // Replace with path to base of deployment, e.g. https://www.example.com/folder
window.DRAWIO_VIEWER_URL = null; // Replace your path to the viewer js, e.g. https://www.example.com/js/viewer.min.js
window.DRAWIO_LIGHTBOX_URL = null; // Replace with your lightbox URL, eg. https://www.example.com
window.DRAW_MATH_URL = 'math';
window.DRAWIO_CONFIG = null; // Replace with your custom draw.io configurations. For more details, https://www.diagrams.net/doc/faq/configure-diagram-editor
urlParams['sync'] = 'manual';
urlParams['lang'] = 'zh';</code></pre><h3>隐藏语言切换按钮</h3><p>需注释掉相关代码块</p><pre><code>// src\main\webapp\js\diagramly\Menus.js
Menus.prototype.createMenubar = function(container)
{
var menubar = menusCreateMenuBar.apply(this, arguments);
if (menubar != null && urlParams['noLangIcon'] != '1')
{
var langMenu = this.get('language');
// if (langMenu != null)
// {
// var elt = menubar.addMenu('', langMenu.funct);
// elt.setAttribute('title', mxResources.get('language'));
// elt.style.width = '16px';
// elt.style.paddingTop = '2px';
// elt.style.paddingLeft = '4px';
// elt.style.zIndex = '1';
// elt.style.position = 'absolute';
// elt.style.display = 'block';
// elt.style.cursor = 'pointer';
// elt.style.right = '17px';
// if (uiTheme == 'atlas')
// {
// elt.style.top = '6px';
// elt.style.right = '15px';
// }
// else if (uiTheme == 'min')
// {
// elt.style.top = '2px';
// }
// else
// {
// elt.style.top = '0px';
// }
// var icon = document.createElement('div');
// icon.style.backgroundImage = 'url(' + Editor.globeImage + ')';
// icon.style.backgroundPosition = 'center center';
// icon.style.backgroundRepeat = 'no-repeat';
// icon.style.backgroundSize = '19px 19px';
// icon.style.position = 'absolute';
// icon.style.height = '19px';
// icon.style.width = '19px';
// icon.style.marginTop = '2px';
// icon.style.zIndex = '1';
// elt.appendChild(icon);
// mxUtils.setOpacity(elt, 40);
// if (urlParams['winCtrls'] == '1')
// {
// elt.style.right = '95px';
// elt.style.width = '19px';
// elt.style.height = '19px';
// elt.style.webkitAppRegion = 'no-drag';
// icon.style.webkitAppRegion = 'no-drag';
// }
// if (uiTheme == 'atlas' || uiTheme == 'dark')
// {
// elt.style.opacity = '0.85';
// elt.style.filter = 'invert(100%)';
// }
// document.body.appendChild(elt);
// menubar.langIcon = elt;
// }
}
return menubar;
};
}</code></pre><h2>其它</h2><h3>修改文件名</h3><pre><code>// js修改文件名
this.editorUi.getCurrentFile().rename('234324')
// 默认文件名
// src/main/webapp/js/diagramly/EditorUi.js
this.defaultFilename = mxResources.get('untitledDiagram');
</code></pre><h3>禁止修改文件名</h3><pre><code>// src/main/webapp/js/diagramly/LocalFile.js
// isRenamable函数返回false时,无法通过交互修改文件名,能通过js修改
LocalFile.prototype.isRenamable = function()
{
return false;
};
</code></pre><h3>删除初始化跳转页</h3><p><img src="/img/remote/1460000041920871" alt="image.png" title="image.png"></p><p>注释掉<code>src/main/webapp/index.html</code>相应的结构</p><pre><code> <!-- <div class="geBlock">
<h1>Flowchart Maker and Online Diagram Software</h1>
<p>
diagrams.net (formerly draw.io) is free online diagram software. You can use it as a flowchart maker, network diagram software, to create UML online, as an ER diagram tool,
to design database schema, to build BPMN online, as a circuit diagram maker, and more. draw.io can import .vsdx, Gliffy&trade; and Lucidchart&trade; files .
</p>
<h2 id="geStatus">Loading...</h2>
<p>
Please ensure JavaScript is enabled.
</p>
</div> -->
</code></pre><h3>修改标题结构与样式</h3><pre><code>// 结构生成:src/main/webapp/js/diagramly/App.js —— 6408L
if (this.fname != null)
{
this.fnameWrapper.style.display = 'block';
this.fname.innerHTML = '';
var filename = (file.getTitle() != null) ? file.getTitle() : this.defaultFilename;
mxUtils.write(this.fname, filename);
this.fname.setAttribute('title', filename + ' - ' + mxResources.get('rename'));
}
// 容器样式 6786L
this.fnameWrapper = document.createElement('div');
this.fnameWrapper.style.position = 'absolute';
this.fnameWrapper.style.right = '120px';
this.fnameWrapper.style.left = '60px';
this.fnameWrapper.style.top = '19px';
this.fnameWrapper.style.height = '26px';
this.fnameWrapper.style.display = 'none';
this.fnameWrapper.style.overflow = 'hidden';
this.fnameWrapper.style.textOverflow = 'ellipsis';
</code></pre><h3>隐藏便签本</h3><p>注释掉相关构造器的调用即可:</p><pre><code> // src/main/webapp/js/diagramly/App.js
if (name == '.scratchpad' && xml == null)
{
xml = this.emptyLibraryXml;
}
if (xml != null)
{
onload(new StorageLibrary(this, xml, name));
}
else
{
onerror();
}
</code></pre><p>便签本构造器为<code>StorageLibrary</code>。</p><p>有三个位置调用<code>src/main/webapp/js/diagramly/App.js</code>、<code>src/main/webapp/js/diagramly/sidebar/Sidebar.js</code>、<code>src/main/webapp/js/diagramly/EditorUi.js</code>。</p><p>该侧边栏的便签本是在<code>App.js</code>中注册的,在逻辑中属于<strong>必插入</strong>的侧边栏资源库,和<code>loadLibraries</code>挂钩,相关逻辑在<code>src/main/webapp/js/diagramly/App.js</code>的<code>5477L~5567L</code>。</p><h3>隐藏存储选项弹窗</h3><h4>解决方案:</h4><p>修改弹窗逻辑,不生成结构,直接渲染EditorUi</p><pre><code>// src/main/webapp/js/diagramly/Dialogs.js 258L
else if (!mxClient.IS_CHROMEAPP && (this.mode == null || force))
{
// 删除弹窗逻辑
// var rowLimit = (serviceCount == 4) ? 2 : 3;
// var dlg = new StorageDialog(this, mxUtils.bind(this, function()
// {
// this.hideDialog();
// showSecondDialog();
// }), rowLimit);
// this.showDialog(dlg.container, (rowLimit < 3) ? 200 : 300,
// ((serviceCount > 3) ? 320 : 210), true, false, undefined, undefined, true);
// 新增渲染逻辑
this.editorUi.hideDialog();
var prev = Editor.useLocalStorage;
this.editorUi.createFile(this.editorUi.defaultFilename,
null, null, null, null, null, null, true);
Editor.useLocalStorage = prev;
}
</code></pre><h4>弹窗结构:</h4><pre><code>// src/main/webapp/js/diagramly/Dialogs.js
var StorageDialog = function(editorUi, fn, rowLimit)
{
...
// 稍后再决定的逻辑
var later = document.createElement('span');
later.style.position = 'absolute';
later.style.cursor = 'pointer';
later.style.bottom = '27px';
later.style.color = 'gray';
later.style.userSelect = 'none';
later.style.textAlign = 'center';
later.style.left = '50%';
mxUtils.setPrefixedStyle(later.style, 'transform', 'translate(-50%,0)');
mxUtils.write(later, mxResources.get('decideLater'));
div.appendChild(later);
mxEvent.addListener(later, 'click', function()
{
editorUi.hideDialog();
var prev = Editor.useLocalStorage;
editorUi.createFile(editorUi.defaultFilename,
null, null, null, null, null, null, true);
Editor.useLocalStorage = prev;
});
...
</code></pre><h4>弹窗调用逻辑:</h4><pre><code>// src/main/webapp/js/diagramly/App.js 3656L
var dlg = new StorageDialog(this, mxUtils.bind(this, function()
{
this.hideDialog();
showSecondDialog();
}), rowLimit);
this.showDialog(dlg.container, (rowLimit < 3) ? 200 : 300,
((serviceCount > 3) ? 320 : 210), true, false);
</code></pre><pre><code>// src/main/webapp/js/grapheditor/EditorUi.js 4618L
EditorUi.prototype.showDialog = function( //
...
this.dialog = new Dialog(this, elt, w, h, modal, closable, onClose, noScroll, transparent, onResize, ignoreBgClick);
this.dialogs.push(this.dialog);
}
</code></pre><pre><code>// src/main/webapp/js/grapheditor/Editor.js 893L
function Dialog(editorUi, elt, w, h, modal, closable, onClose, noScroll, transparent, onResize, ignoreBgClick)
{
</code></pre><h3>禁用画布双击快捷模版</h3><pre><code>// src/main/webapp/js/grapheditor/EditorUi.js 1515L
graph.dblClick = function(evt, cell)
{
if (this.isEnabled())
{
if (cell == null && ui.sidebar != null && !mxEvent.isShiftDown(evt) &&
!graph.isCellLocked(graph.getDefaultParent()))
{
// var pt = mxUtils.convertPoint(this.container, mxEvent.getClientX(evt), mxEvent.getClientY(evt));
// mxEvent.consume(evt);
// // Asynchronous to avoid direct insert after double tap
// window.setTimeout(mxUtils.bind(this, function()
// {
// ui.showShapePicker(pt.x, pt.y);
// }), 30);
}
else
{
graphDblClick.apply(this, arguments); // 双击编辑,禁掉就无法编辑了
}
}
};
</code></pre><h3>隐藏右键菜单</h3><pre><code>// src/main/webapp/js/grapheditor/EditorUi.js 382L
if (mxClient.IS_IE && (typeof(document.documentMode) === 'undefined' || document.documentMode < 9))
{
mxEvent.addListener(this.diagramContainer, 'contextmenu', linkHandler);
}
else
{
// Allows browser context menu outside of diagram and sidebar
this.diagramContainer.oncontextmenu = linkHandler;
}
</code></pre><h3>隐藏Menus</h3><h4>实践</h4><pre><code>// src\main\webapp\js\grapheditor\Menus.js
// Menus.prototype.defaultMenuItems = ['file', 'edit', 'view', 'arrange', 'extras', 'help'];
Menus.prototype.defaultMenuItems = [];</code></pre><h4>为什么</h4><ul><li>不注释掉<code>src\main\webapp\js\grapheditor\EditorUi.js</code>中的<code>this.menus = this.createMenus();</code>逻辑?</li><li>不注释掉<code>src\main\webapp\js\diagramly\Devel.js</code>中的<code>Menus</code>脚本?<br>因为代码依赖问题,注释掉的话,需要调整其它JS脚本的逻辑。</li></ul><h3>隐藏工具栏Toolbar</h3><h4>实践</h4><pre><code>// src\main\webapp\js\grapheditor\Toolbar.js
function Toolbar(editorUi, container)
{
this.editorUi = editorUi;
this.container = container;
this.staticElements = [];
// this.init();</code></pre><h4>为什么</h4><ul><li>不注释掉<code>src\main\webapp\js\grapheditor\EditorUi.js</code>中的<code>EditorUi.prototype.createToolbar</code>逻辑?</li><li>不注释掉<code>src\main\webapp\js\diagramly\Devel.js</code>中的<code>Toolbar</code>脚本?<br>因为代码依赖问题,注释掉的话,需要调整其它JS脚本的逻辑。</li></ul><h3>隐藏配置面板Format</h3><h4>实践</h4><p>控件的配置面板源码在<code>Format.prototype.immediateRefresh</code>的定义中,这里我们不去调用<code>immediateRefresh</code>函数,即不会生成各项配置面板——空面板,但,空面板宽度width还是有的,通过<code>this.editorUi.toggleFormatPanel(false)</code>隐藏。</p><pre><code>// src\main\webapp\js\grapheditor\Format.js
Format.prototype.refresh = function()
{
if (this.pendingRefresh != null)
{
window.clearTimeout(this.pendingRefresh);
this.pendingRefresh = null;
}
this.pendingRefresh = window.setTimeout(mxUtils.bind(this, function()
{
// this.immediateRefresh();
this.editorUi.toggleFormatPanel(false);
}));
};</code></pre><h4>为什么</h4><ul><li><p>不采用以下方式隐藏——默认UI模式下可以的,min模式下会报错。</p><pre><code>// src\main\webapp\js\grapheditor\EditorUi.js
EditorUi.prototype.createFormat = function(container)
{
// return new Format(this, container);
};</code></pre></li></ul><h3>隐藏共享按钮</h3><p>需注释掉相关代码块</p><pre><code>// src\main\webapp\js\diagramly\App.js
// Share
if (urlParams['embed'] != '1' && this.getServiceName() == 'draw.io' &&
!mxClient.IS_CHROMEAPP && !EditorUi.isElectronApp &&
!this.isOfflineApp())
{
if (file != null)
{
// if (this.shareButton == null)
// {
// this.shareButton = document.createElement('div');
// this.shareButton.className = 'geBtn gePrimaryBtn';
// this.shareButton.style.display = 'inline-block';
// this.shareButton.style.backgroundColor = '#F2931E';
// this.shareButton.style.borderColor = '#F08705';
// this.shareButton.style.backgroundImage = 'none';
// this.shareButton.style.padding = '2px 10px 0 10px';
// this.shareButton.style.marginTop = '-10px';
// this.shareButton.style.height = '28px';
// this.shareButton.style.lineHeight = '28px';
// this.shareButton.style.minWidth = '0px';
// this.shareButton.style.cssFloat = 'right';
// this.shareButton.setAttribute('title', mxResources.get('share'));
// var icon = document.createElement('img');
// icon.setAttribute('src', this.shareImage);
// icon.setAttribute('align', 'absmiddle');
// icon.style.marginRight = '4px';
// icon.style.marginTop = '-3px';
// this.shareButton.appendChild(icon);
// if (!Editor.isDarkMode() && uiTheme != 'atlas')
// {
// this.shareButton.style.color = 'black';
// icon.style.filter = 'invert(100%)';
// }
// mxUtils.write(this.shareButton, mxResources.get('share'));
// mxEvent.addListener(this.shareButton, 'click', mxUtils.bind(this, function()
// {
// this.actions.get('share').funct();
// }));
// this.buttonContainer.appendChild(this.shareButton);
// }
}</code></pre><h3>隐藏最大化、展开/折叠、Format展开...按钮</h3><pre><code>// src\main\webapp\js\diagramly\App.js
// this.toggleFormatElement = document.createElement('a');
// this.toggleFormatElement.setAttribute('title', mxResources.get('formatPanel') + ' (' + Editor.ctrlKey + '+Shift+P)');
// this.toggleFormatElement.style.position = 'absolute';
// this.toggleFormatElement.style.display = 'inline-block';
// this.toggleFormatElement.style.top = (uiTheme == 'atlas') ? '8px' : '6px';
// this.toggleFormatElement.style.right = (uiTheme != 'atlas' && urlParams['embed'] != '1') ? '30px' : '10px';
// this.toggleFormatElement.style.padding = '2px';
// this.toggleFormatElement.style.fontSize = '14px';
// this.toggleFormatElement.className = (uiTheme != 'atlas') ? 'geButton' : '';
// this.toggleFormatElement.style.width = '16px';
// this.toggleFormatElement.style.height = '16px';
// this.toggleFormatElement.style.backgroundPosition = '50% 50%';
// this.toggleFormatElement.style.backgroundRepeat = 'no-repeat';
// this.toolbarContainer.appendChild(this.toggleFormatElement);</code></pre><p>按钮没有了,就把高度设置为0吧!</p><pre><code>// src\main\webapp\js\grapheditor\EditorUi.js
EditorUi.prototype.toolbarHeight = 0;</code></pre><h3>隐藏分页</h3><p>需注释掉相关代码块</p><pre><code>// src/main/webapp/js/grapheditor/EditorUi.js
if (this.container != null && this.tabContainer != null)
{
// this.container.appendChild(this.tabContainer);
}</code></pre><h3>隐藏状态提醒</h3><pre><code>// 结构生成:src/main/webapp/js/grapheditor/EditorUi.js
EditorUi.prototype.createStatusContainer = function()
{
var container = document.createElement('a');
container.className = 'geItem geStatus';
return container;
};
// 状态设置
EditorUi.prototype.setStatusText = function(value)
{
this.statusContainer.innerHTML = value;
// Wraps simple status messages in a div for styling
if (this.statusContainer.getElementsByTagName('div').length == 0)
{
this.statusContainer.innerHTML = '';
var div = this.createStatusDiv(value);
this.statusContainer.appendChild(div);
}
};
// 状态监听
this.editor.addListener('statusChanged', mxUtils.bind(this, function()
{
this.setStatusText(this.editor.getStatus());
}));
</code></pre><h2>绘图结构XML</h2><p>xml</p><pre><code><mxGraphModel dx="877" dy="762" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="2x61OCl5DEtUMzRSTxGA-5" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="1" source="2x61OCl5DEtUMzRSTxGA-1" target="2x61OCl5DEtUMzRSTxGA-2">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="2x61OCl5DEtUMzRSTxGA-6" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=-0.008;entryY=0.617;entryDx=0;entryDy=0;entryPerimeter=0;" edge="1" parent="1" source="2x61OCl5DEtUMzRSTxGA-2" target="2x61OCl5DEtUMzRSTxGA-3">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="2x61OCl5DEtUMzRSTxGA-1" value="text1" style="rounded=1;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="50" y="120" width="100" height="60" as="geometry" />
</mxCell>
<mxCell id="2x61OCl5DEtUMzRSTxGA-2" value="text2" style="rounded=1;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="250" y="150" width="120" height="60" as="geometry" />
</mxCell>
<mxCell id="2x61OCl5DEtUMzRSTxGA-3" value="text3" style="rounded=1;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="550" y="310" width="120" height="60" as="geometry" />
</mxCell>
</root>
</mxGraphModel>
</code></pre><p>转成对应的JSON:</p><pre><code>{
"@dx": "877",
"@dy": "762",
"@grid": "1",
"@gridSize": "10",
"@guides": "1",
"@tooltips": "1",
"@connect": "1",
"@arrows": "1",
"@fold": "1",
"@page": "1",
"@pageScale": "1",
"@pageWidth": "827",
"@pageHeight": "1169",
"@math": "0",
"@shadow": "0",
"root": [
{
"@id": "0"
},
{
"@id": "1",
"@parent": "0"
},
{
"@id": "2x61OCl5DEtUMzRSTxGA-5",
"@style": "edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;",
"@edge": "1",
"@parent": "1",
"@source": "2x61OCl5DEtUMzRSTxGA-1",
"@target": "2x61OCl5DEtUMzRSTxGA-2",
"mxGeometry": {
"@relative": "1",
"@as": "geometry"
}
},
{
"@id": "2x61OCl5DEtUMzRSTxGA-6",
"@style": "edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=-0.008;entryY=0.617;entryDx=0;entryDy=0;entryPerimeter=0;",
"@edge": "1",
"@parent": "1",
"@source": "2x61OCl5DEtUMzRSTxGA-2",
"@target": "2x61OCl5DEtUMzRSTxGA-3",
"mxGeometry": {
"@relative": "1",
"@as": "geometry"
}
},
{
"@id": "2x61OCl5DEtUMzRSTxGA-1",
"@value": "text1",
"@style": "rounded=1;whiteSpace=wrap;html=1;",
"@vertex": "1",
"@parent": "1",
"mxGeometry": {
"@x": "50",
"@y": "120",
"@width": "100",
"@height": "60",
"@as": "geometry"
}
},
{
"@id": "2x61OCl5DEtUMzRSTxGA-2",
"@value": "text2",
"@style": "rounded=1;whiteSpace=wrap;html=1;",
"@vertex": "1",
"@parent": "1",
"mxGeometry": {
"@x": "250",
"@y": "150",
"@width": "120",
"@height": "60",
"@as": "geometry"
}
},
{
"@id": "2x61OCl5DEtUMzRSTxGA-3",
"@value": "text3",
"@style": "rounded=1;whiteSpace=wrap;html=1;",
"@vertex": "1",
"@parent": "1",
"mxGeometry": {
"@x": "550",
"@y": "310",
"@width": "120",
"@height": "60",
"@as": "geometry"
}
}
]
}
</code></pre><h2>打包部署</h2><p>通过链接下载<a href="https://link.segmentfault.com/?enc=ikBicNaVeL4It3XrSDSZmQ%3D%3D.apOz0%2FjWAlMqk9wJXDFB1k%2BjuaKH%2B3sHRlpOQ0FkMWQayLlHnBIwhaeVeLvoryot" rel="nofollow">打包工具Ant</a>,以1.9.16版本为例,下载解压后,切换到解压后的目录,依次运行<code>build.bat</code>或<code>build.sh</code>,<code>bootstrap.bat</code>或<code>bootstrap.sh</code>。</p><p>切换到项目目录下,<code>ant -file ./etc/build/build.xml</code>,执行结束后会替换原有的线上文件。</p><p>详情见<a href="https://link.segmentfault.com/?enc=drbO4Pvc7nZY4Wt4wcLiDw%3D%3D.dLK48qJopPVNTNuLIjLLu01B7qmh9KekYqlsyVLbfrbTK6XCVL395U9zIeTnt5IM" rel="nofollow">github官方文档</a>。</p><p><a href="https://link.segmentfault.com/?enc=HkEUvispyg0eWI20m9LroA%3D%3D.ao%2FoMBbLyOnL%2B9WSWerxlMU0ff93n3pk6aesiXAtNMV7JGo6O0vciB8amQVnHh84" rel="nofollow">Ant命令行使用说明</a>。</p><p>Notes:</p><ul><li><p>切换到解压后的目录</p><ul><li>必须执行,该脚本内的访问路径是相对路径,必须切换到解压后的目录执行脚本</li></ul></li><li><p>ant -file ./etc/build/build.xml</p><ul><li>Mac OS需要将ant设置为全局,win执行脚本时默认了全局</li></ul></li></ul>
一行行解读.gitlab-ci.yml
https://segmentfault.com/a/1190000041611853
2022-03-26T11:26:35+08:00
2022-03-26T11:26:35+08:00
米花儿团儿
https://segmentfault.com/u/mihuartuanr
0
<h2>【日常】.gitlab-ci.yml解读</h2><pre><code># .job: 定义隐藏的任务Job,该任务不会被Gitlab cicd执行
# &template 定义锚点
# 隐藏任务结合锚点,可以提供模板给任务Job复用 —— 不可跨文件使用,跨文件考虑`!reference`API
# .job: &template定义可复用的内容
.script_package_install-deploy: &script_package_install-deploy |
yarn global add @custom/deploy-tool
# docker 镜像
image: node:latest
# 钩子函数,脚本执行之前需要执行的命令
before_script:
- npm config set registry https://registry.npm.taobao.org/
- npm config set cache-folder .cache
- *script_package_install-deploy
# 定义不同的周期阶段,属于同一周期的任务Job并行执行
stages:
- build
- deploy_staging
- deploy_preview
- deploy_production
# .job: &template定义可复用的内容
.script_set_env: &script_set_env |
if [ "$CI_COMMIT_REF_NAME" == "production" ]; then
if [ "$CI_JOB_STAGE" == "deploy_production" ]; then
export DEPLOY_ENVIRONMENT=prod
else
export DEPLOY_ENVIRONMENT=preview
fi
else [ "$CI_COMMIT_REF_NAME" == "staging" ]
export DEPLOY_ENVIRONMENT=staging
fi
# .job: &template定义可复用的内容
# 通过*template使用锚点定义的内容
.deploy: &deploy_common
script:
- *script_set_env
- deploy-tool deploy
# 定义任务build
build:
stage: build # 所属周期阶段,同周期的并行执行
cache: # 需要缓存文件
key: $CI_PROJECT_NAME
paths:
- .cache/**/*
- node_modules/**/*
script: # 该任务要执行的脚本
- npm i
- *script_set_env # 通过*template使用锚点定义的内容
- deploy-tool build
only: # 执行时机:staging、production分支push后自动执行
- staging
- production
# 定义任务deploy_staging
deploy_staging:
stage: deploy_staging # 所属周期阶段,同周期的并行执行
<<: *deploy_common # 通过<<: *template合并锚点定义的内容
environment: # 定义要部署的环境
name: staging
only: # 执行时机:staging分支push后自动执行
- staging
# 定义任务deploy_preview
deploy_preview:
stage: deploy_preview # 所属周期阶段,同周期的并行执行
<<: *deploy_common # 通过<<: *template合并锚点定义的内容
environment: # 定义要部署的环境
name: preview
only: # 执行时机:production分支push后自动执行
- production
# 定义任务deploy_preview
deploy_production:
stage: deploy_production # 所属周期阶段,同周期的并行执行
<<: *deploy_common # 通过<<: *template合并锚点定义的内容
environment: # 定义要部署的环境
name: production
when: manual # 手动执行
only: # 因为when的存在,不自动执行了,when默认值on_success
- production</code></pre><h2><code>*template</code> vs. <code><<: *template</code></h2><p><code>*template</code>:复用的只是任务脚本的其中一个指令<br><code><<: *template</code>:复用的是整个任务脚本</p><h2>【篇外】如何配置.gitlab-ci.yml</h2><ul><li><p>如何定义Job?</p><ul><li><p>只要有script的就是Job?</p><ul><li>定义在顶层(无缩进),且有script关键字</li></ul></li><li>约束Job何时执行、如何执行</li><li>Job可以创建Job,嵌套</li><li>Job都是独立的执行</li></ul></li><li><p>stages是做什么的</p><ul><li>定义Jobs执行的阶段</li><li>不同的Job可以归属于同一stage</li><li>同一stage的Jobs并行执行</li></ul></li></ul><pre><code>stages:
- build
- deploy_staging
- deploy_preview
- deploy_production
build:
stage: build // 约定所属stage
cache:
key: $CI_PROJECT_NAME
paths:
- .cache/**/*
- node_modules/**/*
script:
- npm i
- *script_set_env
only:
- tags // 打了tag时触发该任务
- staging // 提交staging分支时触发该任务
- production // 提交production分支时触发该任务</code></pre><p>Job执行时机</p><ul><li>only/except关键字</li><li>only关键字的默认策略是['branches', 'tags'],即提交了一个分支或者打了标签,就会触发</li></ul><ul><li><p>任务执行顺序的策略when</p><ul><li>on_success:默认值,只有前面stages的所有工作成功时才执行</li><li>on_failure:当前面stages中任意一个jobs失败后执行</li><li>always:无论前面stages中jobs状态如何都执行</li><li>manual:手动执行</li><li>delayed:延迟执行</li></ul></li><li><p>缓存cache</p><ul><li>指定需要缓存的文件夹或者文件</li><li>cache不能缓存工作路径以外的文件</li></ul></li><li><p>自定义变量variables</p><pre><code>variables:
TEST: "HELLO WORLD"
job1:
script:
- echo "$TEST"</code></pre></li></ul><p>复用脚本(锚点)</p><ul><li><a href="https://link.segmentfault.com/?enc=EPZUhRCxVsg2IdpF%2FLNurw%3D%3D.5WAQ58fkaZaxJHceSXSmT1n6sgurMv4eBl0KiTRM49gvQV74UMNNIEYCLfWJC1r9" rel="nofollow">https://docs.gitlab.com/ee/ci...</a></li></ul>
Gitlab静态页面Pages
https://segmentfault.com/a/1190000041611802
2022-03-26T11:08:40+08:00
2022-03-26T11:08:40+08:00
米花儿团儿
https://segmentfault.com/u/mihuartuanr
1
<h2>动机</h2><p>项目不直观,无法明确感知是哪个项目。</p><h2>yaml配置</h2><pre><code># .gitlab-ci.yml
image: node:latest # 针对前端,选择合适的node镜像
# 定义各阶段的执行顺序
stages:
- build
- deploy
# 在脚本执行前设置npm源
before_script:
- npm config set registry https://registry.npm.taobao.org/
- npm config set cache-folder .cache/
# 定义各阶段的具体执行命令
# artifacts缓存在gitlab上的工件,用于将指定目录下的文件下载下来
build:
stage: build
script:
- npm i
- npm run build
artifacts:
paths:
- dist
pages: # 该任务命名必须是pages,是gitlab内部任务
stage: deploy
approval: false
script:
- rm -rf public/*
- mv dist/* public
artifacts:
paths:
- public
only:
- bundler-rollup
cache: # 定义缓存文件
paths: # 定义缓存文件的路径
- node_modules # 在下一次触发 gitlab-ci 时,缓存会被还原,就不用重复安装依赖。
- dist # 同时把 build 阶段生成的 dist 文件夹也缓存起来,在 deploy 阶段会用到。</code></pre><h2>Pages流程介绍</h2><p>在push源代码到仓库的时候,<code>Gitlab</code>可以根据项目中的 <code>.gitlab-ci.yml</code> 文件来自动构建项目,然后部署到服务器中。</p><p><img src="/img/remote/1460000041611804" alt="image" title="image"></p><h3>核心点</h3><ul><li>打包静态资源</li><li>配置指定任务<code>pages</code>,将静态资源迁移到<code>public</code>目录下</li></ul><p><strong>Notes:</strong></p><p><code>**stage: deploy**</code><strong>默认是需要审批的</strong>,可以通过:</p><ul><li><code>stage</code>命名刻意回避<code>deploy</code></li><li>通过<code>approval: false</code>回避审批</li></ul><p>审批不能是自己,即使自己是代码拥有者,也无法自己审批自己(审批按钮无法使用)。</p><h2>结果预览</h2><p><img src="/img/bVcYLif" alt="" title=""></p><h2>参考文档</h2><ul><li><a href="https://link.segmentfault.com/?enc=TmO0PzcOLqF5waGRjmEzug%3D%3D.5kLsvnHiMMARKQfb085UilUszwzGmNMBmPDwzyp1OG54J6DvmmaMYnWwy8R%2FlZ4n7kOr74kmFlU46yYq6n2ofw%3D%3D" rel="nofollow">配置Gitlab pages和Gitlab CI</a></li><li><a href="https://link.segmentfault.com/?enc=3NUoeMD7FVPf9%2FJnOCoytA%3D%3D.kokUqd6rBB3RNUGlB00rXPJcKBh%2B9%2BOigIP8xuQ%2BqHPwrawXURKyzZyJIUyAbTWu" rel="nofollow">利用gitlab-ci和gitlab-pages免费自动构建部署vue项目</a></li></ul>
Vitepress编写组件文档
https://segmentfault.com/a/1190000041599324
2022-03-24T09:49:42+08:00
2022-03-24T09:49:42+08:00
米花儿团儿
https://segmentfault.com/u/mihuartuanr
0
<h2>快速开始</h2><h3>安装依赖</h3><pre><code>mkdir <projectName>路径下
cd <projectName>
npm init -y
npm i -D vitepress</code></pre><h3>pkg#scripts</h3><pre><code> "scripts": {
"docs:dev": "vitepress dev docs",
"docs:build": "vitepress build docs",
"docs:serve": "vitepress serve docs",
}</code></pre><h3>目录结构</h3><pre><code>.
-| docs
-| index.md // 文档首页
-| quikstart/
</code></pre><h3>文档首页</h3><pre><code>// index.md
---
home: true
heroText: 基于element-plus二次封装组件
tagline: 高扩展的组件库
actionText: 快速开始
actionLink: //
features:
- title: 简洁至上
details: 所有组件支持全量引入和按需引入
- title: 高扩展性
details: 全新的组件API设计,支持高度自定义
- title: 全面覆盖
details: 涵盖基础组件、通用组件和业务组件
---
</code></pre><p>运行<code>npm run docs:dev</code>,效果如下:</p><p><img src="/img/remote/1460000041599326" alt="预览" title="预览"></p><p>更多配置见:<a href="https://link.segmentfault.com/?enc=PcngrJhotJhzz03eHudqRA%3D%3D.P735UM7uelc8v7PXyDcj2BJzWAbSn%2BkFot87ubX46kdCyY8U2VyXVHJLwcKFNK9O5sXTei8a5MKA1yz0weDjabKk12UlXbCcKAR7bT3GQ6U%3D" rel="nofollow">https://github.com/vuejs/vite...</a></p><h2>文档配置</h2><p>新建文件<code>docs/.vitepress/config.js</code></p><h3>文档头</h3><pre><code>// docs/.vitepress/config.js
module.exports = {
// 站点名称
title: '基于element-plus二次封装组件',
// 部署的基础路径
base: '/',
// 生成html的head配置:站点favicon...
head: [
],
themeConfig: {
// 头部导航
nav: [
{
text: '首页',
link: '/'
},
{
text: '百度',
link: 'http://baidu.com' // 外部链接有特定标识
},
]
}
}
</code></pre><p><img src="/img/remote/1460000041599327" alt="头部配置" title="头部配置"></p><h3>侧边栏</h3><h4>sidebar配置</h4><pre><code>// docs/.vitepress/config.js
module.exports = {
// 站点名称
title: '基于element-plus二次封装组件',
// 部署的基础路径
base: '/',
// 生成html的head配置:站点favicon...
head: [
],
themeConfig: {
// 头部导航
nav: [
{
text: '首页',
link: '/'
},
{
text: '百度',
link: 'http://baidu.com' // 外部链接有特定标识
},
],
sidebar: [
{
text: '介绍',
link: '/intro/'
},
{
text: '安装',
link: '/install/'
},
{
text: '快速开始',
link: '/quickstart/'
}
]
}
}
</code></pre><h4>目录结构</h4><pre><code>.
-| docs
├── Install
│ └── index.md
├── Intro
│ └── index.md
├── Quickstart
│ └── index.md
└── index.md
</code></pre><p>在非根路径(非首页)下的<code>index.md</code>中的内容随意写一些<code>md</code>语法。</p><p><strong>Notes:</strong> 目录结构即路由</p><h3>首页跳转</h3><p>修改根路径下的<code>index.md</code>:</p><pre><code>---
home: true
heroText: 基于element-plus二次封装组件
tagline: 高扩展的组件库
actionText: 快速开始
actionLink: /intro/
features:
- title: 简洁至上
details: 所有组件支持全量引入和按需引入
- title: 高扩展性
details: 全新的组件API设计,支持高度自定义
- title: 全面覆盖
details: 涵盖基础组件、通用组件和业务组件
---</code></pre><p><img src="/img/remote/1460000041599328" alt="配置路由" title="配置路由"></p><p><strong>Notes:</strong> 每次修改config配置文件都需要重新启动<code>npm run docs:dev</code></p><p><strong>Notes:</strong> md文件中使用非一级标题会在侧边栏生成<strong>多级锚点</strong>。</p><h2>集成组件库</h2><h3>配置主题</h3><pre><code>// docs/.vitepress/theme/index.js
import DefaultTheme from 'vitepress/theme'
import 'element-plus/lib/theme-chalk/index.css' // 组件依赖ElementPlus
import { FormRender } from 'custom-form'
export default {
...DefaultTheme,
enhanceApp({ app }) {
// app为createApp()创建的实例
app.component(FormRender.name, FormRender)
}
}
</code></pre><h3>集成组件</h3><p>创建路由<code>docs/Forms/</code></p><pre><code>.
├── Forms
│ └── index.md
├── Install
│ └── index.md
├── Intro
│ └── index.md
├── Quickstart
│ └── index.md
└── index.md
</code></pre><p>编写文档</p><pre><code><!-- docs/Forms/index.md -->
# 表单
用户通过配置`schema`渲染表单。
## 基本用法
<!-- 集成组件,vite自动解析,源代码不会在页面展示 -->
<div>
<form-render
:schema="schema"
:value="{}"
>
</form-render>
</div>
<script lang="ts">
import { defineComponent, reactive } from 'vue'
import { formSchema } from '../../examples/layouts/index.ts'
export default defineComponent({
setup () {
const schema = reactive(formSchema)
return {
schema
}
}
})
</script>
## 代码示例
<!-- 源码 -->
'''ts
<form-render
:schema="schema"
:value="{}"
>
</form-render>
<script lang="ts">
import { defineComponent, reactive } from 'vue'
import { formSchema } from '../../examples/layouts/index.ts'
export default defineComponent({
setup () {
const schema = reactive(formSchema)
return {
schema
}
}
})
'''</code></pre><p><img src="/img/remote/1460000041599329" alt="集成自定义组件" title="集成自定义组件"></p><p><a href="https://link.segmentfault.com/?enc=QHX1y5%2Bciumsq%2BTWTypQiA%3D%3D.RdfNTHNCRaSoZq1vw7ERW5cdNG3FuQ8M1VWxo4RmfNBFgRm9i8ktianDm9Egq%2BE5qyP838%2BhiqTkoB2cij%2BKGDQKNHh3GA%2BSuahmjf5J3KM%3D" rel="nofollow">https://github.com/vuejs/vite...</a></p><h3>集成ElementPlus</h3><pre><code>// docs/.vitepress/theme/index.js
import DefaultTheme from 'vitepress/theme'
import ElementPlus from 'element-plus'
import 'element-plus/lib/theme-chalk/index.css'
// import { FormRender } from 'custom-form'
export default {
...DefaultTheme,
enhanceApp({ app }) {
// register global components
app.use(ElementPlus)
// app.component(FormRender.name, FormRender)
}
}</code></pre><pre><code><!-- docs/Forms/index.md -->
## 基本用法
<!-- 集成组件,vite自动解析,源代码不会在页面展示 -->
<div>
<el-button type="primary" @click="alert">按钮</el-button>
<!-- <form-render
:schema="schema"
:value="{}"
>
</form-render> -->
</div>
<script lang="ts">
import { defineComponent, reactive } from 'vue'
// import { formSchema } from '../../examples/layouts/index.ts'
export default defineComponent({
setup () {
// const schema = reactive(formSchema)
function alert () {
window.alert('1234')
}
return {
// schema,
alert
}
}
})
</script></code></pre><p><img src="/img/remote/1460000041599330" alt="集成ElementPlus" title="集成ElementPlus"></p>
Vite打包组件库
https://segmentfault.com/a/1190000041592240
2022-03-23T00:23:28+08:00
2022-03-23T00:23:28+08:00
米花儿团儿
https://segmentfault.com/u/mihuartuanr
0
<h2>动机</h2><p>去年使用vue3 + TSX封装组件,结果卡在了打包上,直到最近发现,vite提供了tsx的打包插件。</p><h2>组件准备</h2><p><a href="https://segmentfault.com/a/1190000041592227">手把手创建Vue3组件库</a></p><p>在<code>packages/index.ts</code>中引入相关的组件、工具库、样式。</p><h2>Vite配置</h2><pre><code>// bin/build.js
// node包,commonjs规范
const path = require('path')
const { defineConfig, build } = require('vite')
const vue = require('@vitejs/plugin-vue')
const vueJsx = require('@vitejs/plugin-vue-jsx')
// 打包的入口文件
const entryDir = path.resolve(__dirname, '../packages')
// 出口文件夹
const outDir = path.resolve(__dirname, '../lib')
// vite基础配置
const baseConfig = defineConfig({
configFile: false,
publicDir: false,
plugins: [vue(), vueJsx()]
})
// rollup配置
const rollupOptions = {
// 确保外部化处理那些你不想打包进库的依赖
external: [
'vue'
],
output: {
// 在 UMD 构建模式下为这些外部化的依赖提供一个全局变量
global: [
vue: 'Vue'
]
}
}
// 全量打包构建
const buildAll = async () => {
await build({
...baseConfig,
build: {
rollupOptions,
lib: {
entry: path.resolve(entryDir, 'index.ts'),
name: '', // umd的变量名
fileName: (format) => `index.${format}.js`, // 输出文件名
formats: ['es', 'umd'],
},
outDir
}
})
}
const build = async () => {
await buildAll()
}
build()
</code></pre><h2>pkg#scripts</h2><pre><code>"scripts": {
"build:lib": "node ./bin/build.js"
}
</code></pre><h2>声明组件模块</h2><blockquote>防止typescript中引入module报错</blockquote><pre><code>// packages/vue.d.ts
declare module '*.vue' {
import { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}
</code></pre>
手把手创建Vue3组件库
https://segmentfault.com/a/1190000041592227
2022-03-23T00:16:02+08:00
2022-03-23T00:16:02+08:00
米花儿团儿
https://segmentfault.com/u/mihuartuanr
1
<h2>动机</h2><p>当市面上主流的组件库不能满足我们业务需求的时候,那么我们就有必要开发一套属于自己团队的组件库。</p><h2><strong>环境</strong></h2><p>开发环境:</p><ul><li>vue 3.0</li><li>vue/cli 4.5.13</li><li>nodeJs 12.16.3</li><li>npm 6.14.4</li></ul><h2><strong>步骤</strong></h2><h3><strong>创建项目</strong></h3><p>使用 vue-cli 创建一个 vue3 项目,假设项目名为 <code>custom-npm-ui</code></p><pre><code>$ vue create custom-npm-ui
</code></pre><p>手动选择设置。</p><h3><strong>规划目录</strong></h3><pre><code>├─ build // 打包脚本,用于存放打包配置文件
│ ├─ rollup.config.js
├─ examples // 原 src 目录,改成 examples 用于示例展示
│ ├─ App.vue
│ ├─ main.ts
├─ packages // 新增 packages 目录,用于编写存放组件,如button
│ ├─ SubmitForm
│ │ ├─ src/
│ │ ├─ index.ts
│ ├─ index.ts
├─ typings // 新增 typings 目录, 用于存放 .d.ts 文件,把 shims-vue.d.ts 移动到这里
│ ├─ shims-vue.d.ts
├─ .npmignore // 新增 .npmignore 配置文件
├─ vue.config.js // 新增 vue.config.js 配置文件
</code></pre><p>将 <code>src</code> 目录改为 <code>examples</code> ,并将里面的 <code>assets</code> 和 <code>components</code> 目录删除,移除 <code>App.vue</code> 里的组件引用。</p><h3><strong>项目配置</strong></h3><h4><strong>vue.config.js</strong></h4><p>新增 <code>vue.config.js</code> 配置文件,适配重新规划后的项目目录:</p><pre><code>// eslint-disable-next-line @typescript-eslint/no-var-requires
const path = require('path')
module.exports = {
// 修改 pages 入口
pages: {
index: {
entry: "examples/main.ts", //入口
template: "public/index.html", //模板
filename: "index.html" //输出文件
}
},
// 扩展 webpack 配置
chainWebpack: (config) => {
// 新增一个 ~ 指向 packages 目录, 方便示例代码中使用
config.resolve.alias
.set('~', path.resolve('packages'))
}
}
</code></pre><h4><strong>.npmignore</strong></h4><p>新增 <code>.npmignore</code> 配置文件,组件发布到 <code>npm</code>中,只有编译后的发布目录(例如<code>lib</code>)、<code>package.json</code>、<code>README.md</code>才是需要被发布的,所以我们需要设置忽略目录和文件</p><pre><code># 忽略目录
.idea
.vscode
build/
docs/
examples/
packages/
public/
node_modules/
typings/
# 忽略指定文件
babel.config.js
tsconfig.json
tslint.json
vue.config.js
.gitignore
.browserslistrc
*.map
</code></pre><p>或者配置<code>pkg#files</code>:</p><pre><code> "files": [
"lib/",
"package.json",
"README.md"
],
</code></pre><p>安装依赖后的目录结构:</p><pre><code>└─custom-npm-ui
│ package.json
│ README.md
└─lib
index.css
index.esm.js
index.min.js
</code></pre><h4><strong>tsconfig.json</strong></h4><p>修改 tsconfig.json 中 paths 的路径</p><pre><code>"paths": {
"@/*": [
"src/*"
]
}
</code></pre><p>改为</p><pre><code> "paths": {
"~/*": [
"packages/*"
]
}
</code></pre><p><strong>Notes:</strong><code>typescript</code>支持的别名。</p><p>修改 include 的路径</p><pre><code> "include": [
"src/**/*.ts",
"src/**/*.tsx",
"src/**/*.vue",
"tests/**/*.ts",
"tests/**/*.tsx"
]
</code></pre><p>改为</p><pre><code> "include": [
"examples/**/*.ts",
"examples/**/*.tsx",
"examples/**/*.vue",
"packages/**/*.ts",
"packages/**/*.tsx",
"packages/**/*.vue",
"typings/**/*.ts",
"typings/shims-vue.d.ts",
"tests/**/*.ts",
"tests/**/*.tsx"
]
</code></pre><h4><strong>package.json</strong></h4><p>修改 package.json 中发布到 npm 的字段</p><ul><li><code>name</code>:包名,该名字是唯一的。可在npm远程源搜索名字,如果存在则需换个名字。</li><li><code>version</code>:版本号,每次发布至 npm 需要修改版本号,不能和历史版本号相同。</li><li><code>description</code>:描述。</li><li><code>main</code>:入口文件,该字段需指向我们最终编译后的包文件。</li><li><code>typings</code>:types文件,TS组件需要。</li><li><code>keyword</code>:关键字,以空格分离希望用户最终搜索的词。</li><li><code>author</code>:作者信息</li><li><code>private</code>:是否私有,需要修改为 false 才能发布到 npm</li><li><code>license</code>: 开源协议</li></ul><p>参考设置:</p><pre><code>{
"name": "custom-npm-ui",
"version": "0.1.0",
"private": false,
"description": "基于ElementPlus二次开发的前端组件库",
"main": "lib/index.min.js",
"module": "lib/index.esm.js",
"typings": "lib/index.d.ts",
"keyword": "vue3 element-plus",
"license": "MIT",
"author": {
"name": "yourname",
"email": "youremail@163.com"
}
}
</code></pre><p>在 package.json 的 scripts 新增编译和发布的命令</p><pre><code>"scripts": {
"build": "yarn build:clean && yarn build:lib && yarn build:esm-bundle && rimraf lib/demo.html",
"build:clean": "rimraf lib",
"build:lib": "vue-cli-service build --target lib --name index --dest lib packages/index.ts",
"build:esm-bundle": "rollup --config ./build/rollup.config.js"
}
</code></pre><p>其中 <code>build:lib</code> 是利用 <code>vue-cli</code> 进行 <code>umd</code> 方式打包,<code>build:esm-bundle</code> 是利用 <code>rollup</code> 进行 <code>es</code> 方式打包。</p><p><code>build:lib</code>具体参数解析如下:</p><ul><li><code>--target</code>: 构建目标,默认为应用模式。改为 <code>lib</code> 启用库模式。</li><li><code>--name</code>: 输出文件名</li><li><code>--dest</code> : 输出目录,默认 <code>dist</code>。改成 <code>lib</code></li><li><code>[entry]</code>: 入口文件路径,默认为 <code>src/App.vue</code>。这里我们指定编译 <code>packages/</code> 组件库目录。</li></ul><p><code>build:esm-bundle</code>打包后的资源在<code>webpack2+</code>、<code>rollup</code>环境中可通过<code>pkg#module</code>配置载入使用。</p><h4><strong>rollup.config.js</strong></h4><p>新增 <code>build/rollup.config.js</code>,<code>rollup</code> 打包脚本:</p><pre><code>import cjs from "@rollup/plugin-commonjs"; // commonjs转es module —— rollup只支持es module
import resolve from "@rollup/plugin-node-resolve"; // 搭配@rollup/plugin-commonjs使用
// import ts from '@rollup/plugin-typescript' // 【报错】使用ts报错
import typescript from "rollup-plugin-typescript2"; // 解析TS语法
import vue from "rollup-plugin-vue"; // 解析vue
import babel from "@rollup/plugin-babel";
import scss from "rollup-plugin-scss"; // 解析scss
// import requireContext from "rollup-plugin-require-context"; // 【不可用】支持webpack的require.context API —— 需要安装npm install --save-dev generate-source-map@0.0.5
import { writeFileSync, existsSync, mkdirSync } from "fs";
const extensions = [".js", ".ts", ".vue"];
export default {
input: "packages/index.ts",
output: [
{
file: "lib/index.esm.js", // 多文件输出的话,需要使用dir替代file
format: "es",
globals: {
vue: "Vue", // 告诉rollup全局变量Vue即是vue
},
},
],
extensions,
plugins: [ // 顺序很重要
scss({
output: function (styles, styleNodes) {
if (!existsSync("lib/")) {
mkdirSync("lib/");
}
writeFileSync("lib/index.css", styles);
},
}),
vue({
compileTemplate: true,
}),
// requireContext(),
resolve({
jsnext: true,
main: true,
browser: true,
extensions,
}),
cjs(),
typescript(),
babel({}),
],
external: ["vue", "element-plus"],
};
</code></pre><h3><strong>开发组件</strong></h3><h4>注意事项</h4><ul><li>组件内不能使用懒加载</li><li>组件不能使用<code>require.context()</code>统一管理</li><li>不支持<code>JSX</code>语法编写模版 —— 更好的选择<code>React</code></li></ul><h4>依赖安装</h4><h5>环境依赖</h5><pre><code>$ npm i -D rimraf rollup @rollup/plugin-commonjs @rollup/plugin-node-resolve rollup-plugin-typescript2 rollup-plugin-vue @rollup/plugin-babel rollup-plugin-scss
</code></pre><h5>开发依赖</h5><pre><code>$ npm i -D element-plus@1.0.2-beta.69 babel-plugin-import
</code></pre><p>配置<code>.babel.config.js</code></p><pre><code>module.exports = {
presets: ["@vue/cli-plugin-babel/preset"],
plugins: [
[
"import",
{
libraryName: "element-plus",
},
],
],
};
</code></pre><p>更新<code>examples/main.ts</code>:</p><pre><code>import { createApp } from "vue";
import App from "./App.vue";
import "element-plus/lib/theme-chalk/index.css";
createApp(App).mount("#app");
</code></pre><h4>编写组件</h4><p>在 packages 目录下新建 <code>index.ts</code> 文件和 <code>SubmitForm/</code> 文件夹,在 <code>SubmitForm</code> 下新建 <code>index.ts</code>和 <code>src/index.vue</code>,结构如下:</p><pre><code>.
├── SubmitForm
│ ├── SubmitForm.stories.ts
│ ├── index.ts
│ └── src
│ ├── FormRender.vue
│ ├── fileds
│ │ ├── Color.vue
│ │ ├── Datetime.vue
│ │ ├── Radio.vue
│ │ ├── Select.vue
│ │ ├── Switch.vue
│ │ ├── Text.vue
│ │ ├── Upload.vue
│ │ ├── hooks
│ │ │ └── useValueHandleHook.ts
│ │ └── index.ts
│ ├── index.vue
│ ├── schemas
│ │ ├── baseSchema.ts
│ │ └── schemasProp.ts
│ └── store
│ └── index.ts
├── common
│ └── getType.ts
└── index.ts
</code></pre><p><code>packages/SubmitForm/src/index.vue</code></p><pre><code><script lang="ts">
import { computed, defineComponent } from "vue";
import { ElForm } from "element-plus";
import { FormPropsType } from "./schemas/schemasProp";
import FormRender from "./FormRender.vue";
import { values, updateValues } from "./store";
export default defineComponent({
name: "SubmitForm",
props: FormPropsType,
emits: ["runtimeChange"],
components: {
"el-form": ElForm,
FormRender,
},
setup(props, { emit }) {
const schemasCpt = computed(() => props.schema);
const defaultValueCpt = computed(() => props.defaultValue);
function handleRuntimeChange(name: string, value: any) {
updateValues(name, value);
emit("runtimeChange", name, value);
}
return {
schemasCpt,
defaultValueCpt,
values,
handleRuntimeChange,
};
},
});
</script>
<template>
<el-form label-width="100px" :model="values">
<template v-for="(schema, idx) of schemasCpt" :key="idx">
<FormRender
:schema="schema"
:defaultValue="defaultValueCpt"
v-bind="$attrs"
:onRuntimeChange="handleRuntimeChange"
/>
</template>
</el-form>
</template>
</code></pre><p><code>packages/SubmitForm/index.ts</code>,单独组件的入口文件,在其他项目可以使用 import { <code>SubmitForm</code> } from 'custom-npm-ui' 方式进行单个组件引用</p><pre><code>import type { App } from "vue";
import SubmitForm from "./src/index.vue";
// 定义 install 方法,接收 Vue 作为参数。如果使用 use 注册插件,那么所有的组件都会被注册
SubmitForm.install = function (Vue: App) {
// 遍历注册全局组件
Vue.component(SubmitForm.name, SubmitForm);
};
export default SubmitForm;
</code></pre><p><code>packages/index.ts</code> 作为组件库的入口文件,可以在其他项目的 <code>main.ts</code> 引入整个组件库,内容如下</p><pre><code>import type { App } from "vue";
import SubmitForm from "./SubmitForm";
const components = [SubmitForm];
// 定义 install 方法,接收 Vue 作为参数。如果使用 use 注册插件,那么所有的组件都会被注册
const install = function (Vue: App): void {
// 遍历注册全局组件
components.map((component) => Vue.component(component.name, component));
};
export {
// 以下是具体的组件列表
SubmitForm,
};
export default {
// 导出的对象必须具有 install,才能被 Vue.use() 方法安装
install,
};
</code></pre><p>这样,我们就完成一个简单的 <code>SubmitForm</code> 组件,后续需要扩展其他组件,按照 <code>SubmitForm</code> 的结构进行开发,并且在 <code>index.ts</code> 文件中 <code>components</code> 组件列表添加即可。</p><h4><strong>编写示例调试</strong></h4><p><code>examples/main.ts</code></p><pre><code>import { createApp } from "vue";
import App from "./App.vue";
import CustomeUI from "~/index";
import "element-plus/lib/theme-chalk/index.css";
createApp(App).use(CustomeUI).mount("#app");
</code></pre><p><code>examples/App.vue</code> 删除项目初始化的 <code>HelloWorld</code> 组件</p><pre><code><script lang="ts">
import { defineComponent, reactive, ref, toRaw } from "vue";
import formSchema from "./layouts/case.layout";
export default defineComponent({
name: "App",
components: {},
setup() {
const submitFormRef = ref();
const schema = reactive(formSchema);
const defaultValues = reactive({});
function formRuntimeChange(name: string, value: any) {
console.log(name, " = ", value);
}
function submit() {
console.log(submitFormRef.value.values);
}
return {
submitFormRef,
schema,
defaultValues,
formRuntimeChange,
submit,
};
},
});
</script>
<template>
<submit-form
ref="submitFormRef"
:schema="schema"
:defaultValue="defaultValues"
@runtimeChange="formRuntimeChange"
>
</submit-form>
<button @click="submit">保存</button>
</template>
<style lang="scss">
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>
</code></pre><p>启动项目,测试一下</p><pre><code>$ npm run serve
</code></pre><p>组件开发完成后,执行编译库命令:</p><pre><code>$ npm run build
</code></pre><p><strong>引入打包后的文件回归测试一下</strong>,没有问题再发布到 npm 仓库。</p><p>在示例入口 <code>main.ts</code> 引用我们的组件库:</p><pre><code>import { createApp } from "vue";
import App from "./App.vue";
import CustomeUI from "../lib/index.esm.js";
import "element-plus/lib/theme-chalk/index.css";
createApp(App).use(CustomeUI).mount("#app");
</code></pre><h4>编写声明文件</h4><h5>创建目录结构</h5><pre><code>.
├── typings
│ ├── index.d.ts
│ ├── component.d.ts
│ └── packages
│ └── submit-form.vue.d.ts
├── common
│ └── getType.ts
└── index.ts
</code></pre><h5>更新<code>package.json</code>配置</h5><pre><code>// package.json
{
...
"typings": "./typings/index.d.ts",
"files": [
"lib/",
"package.json",
"typings/"
],
"publishConfig": {
"registry": "https://abc.com/"
}
}
</code></pre><h5>核心文件</h5><pre><code>// typings/index.d.ts
import type { App } from "vue";
export * from "./component.d";
export declare const install: (app: App, opt: any) => void;
declare const _default: {
install: (app: App<any>, opt: any) => void;
};
export default _default;
</code></pre><pre><code>// typings/component.d.ts
export { default as SubmitForm } from "./packages/submit-form.d";
</code></pre><pre><code>// typings/packages/submit-form.d.ts
import {
DefineComponent,
ComponentOptionsMixin,
VNodeProps,
AllowedComponentProps,
ComponentCustomProps,
EmitsOptions,
ComputedGetter,
WritableComputedOptions,
} from "vue";
import { FormRowType } from "../schemas/schemasProp";
declare const _default: DefineComponent<
Record<string, unknown>,
{
refName: string;
schema: FormRowType[];
defaultValue: Record<string, any>;
},
Record<string, unknown>,
Record<string, ComputedGetter<any> | WritableComputedOptions<any>>, // computed
Record<string, () => void>, // methods
ComponentOptionsMixin,
ComponentOptionsMixin,
EmitsOptions,
string,
VNodeProps & AllowedComponentProps & ComponentCustomProps,
Readonly<Record<string, unknown> & Record<string, unknown>>,
Record<string, unknown>
>;
export default _default;
</code></pre><h4><strong>发布组件</strong></h4><h5>配置NPM仓库地址</h5><p>组件开发并测试通过后,就可以发布到 npm 仓库提供给其他项目使用了,首先编写<code>.npmrc</code>文件配置要上传的源地址:</p><pre><code>registry=https://abc.com
</code></pre><p>更推荐更新<code>pkg#publishConfig</code>指定仓库地址:</p><pre><code> "publishConfig": {
"registry": "https://abc.com"
},
</code></pre><p><strong>Notes:</strong>使用<code>.npmrc</code>配置NPM仓库地址,<code>nrm</code>无法切换源。</p><h5>获取NPM账号、密码</h5><p>在npm官网注册即可</p><h5><strong>登录npm账号</strong></h5><p>在项目中 terminal 命令窗口登录 npm 账号</p><pre><code>$ npm login
Username:
Password:
Email:(this IS public)
</code></pre><p>输入在 npm的账号、密码、邮箱</p><h5><strong>发布</strong></h5><pre><code>$ npm publish
</code></pre><h3>组件文档</h3><h4>创建<code>Storybook</code>友好型环境</h4><p>在项目中 terminal 命令窗口执行命令:</p><pre><code>$ npx -p @storybook/cli sb init
</code></pre><p><code>storybook</code>是一个可以辅助UI开发的工具,是一个UI组件的开发环境。</p><p>在<code>sb init</code>初始化过程中,<code>storybook</code>会检查现有项目的<code>dependencies</code>,然后依据项目的现有框架,提供最佳的组装方式。</p><p><code>Storybook</code>初始化做了以下步骤:</p><ul><li>安装<code>Storybook</code>需要的依赖</li><li>更新<code>pkg#run-script</code></li><li><p>增加预设的配置文件</p><ul><li><code>.storybook/main.js</code></li><li><code>.storybook/preview.js</code></li></ul></li><li>增加示例模版<code>stories/</code></li></ul><h5>更新<code>package.json</code></h5><pre><code>...
"scripts": {
"storybook": "start-storybook -p 6006 -h 0.0.0.0", // 启动本地服务以预览
"build-storybook": "build-storybook" // 构建
},
...
</code></pre><pre><code>$ npm run storybook # 启动本地服务访问storybook项目
</code></pre><h5>更新目录结构</h5><pre><code>├─ .storybook // 预设的配置文件
│ ├─ main.js // 入口文件
│ ├─ preview.js // 控制Stories的呈现、全局资源的加载
├─ stories // 示例模版
</code></pre><h6><code>main.js</code></h6><pre><code>module.exports = {
"stories": [ // Storybook会抓取、载入配置路径下的指定文件渲染展示
"../stories/**/*.stories.mdx",
"../stories/**/*.stories.@(js|jsx|ts|tsx)"
],
"addons": [ // Storybook所用插件 —— Storybook功能增强
"@storybook/addon-links",
"@storybook/addon-essentials"
],
"framework": "@storybook/vue3" // Storybook所用框架 —— Vue环境支持
}
</code></pre><h4>编写示例</h4><h5>入口配置</h5><p>更新<code>.storybook/main.js</code></p><pre><code>module.exports = {
"stories": [ // Storybook会抓取、载入配置路径下的指定文件渲染展示
"../packages/**/*.stories.@(js|jsx|ts|tsx)",
"../stories/**/*.stories.mdx",
"../stories/**/*.stories.@(js|jsx|ts|tsx)"
],
...
}
</code></pre><h5>组件Story编写</h5><pre><code>import SubmitForm from "./index"; // 引入组件
import { SchemaType, RuleTrigger } from "./src/schemas/baseSchema";
const caseSchema = [ // 示例数据
{
key: "moduleName",
name: "title",
type: SchemaType.Text,
label: "栏目名称",
placeholder: "请输入栏目名称",
attrs: {
//
},
rules: [
{
required: true,
message: "栏目名称必填~",
trigger: RuleTrigger.Blur,
},
],
},
...
];
export default {
title: "ui组件/SubmitForm", // 展示标题:使用路径定义命名空间 —— 分组、分类
component: SubmitForm,
};
const Template = (args: any) => ({ // 渲染组件
components: { SubmitForm },
setup() {
return {
...args,
};
},
template: '<submit-form :schema="schema"></submit-form>',
});
export const 基本应用 = Template.bind({}); // 组件应用示例
(基本应用 as any).args = {
schema: caseSchema,
ref: "submitFormRef",
};
</code></pre><p>可以使用<code>props</code>&<code>computed</code>去承接<code>args</code>这样更符合<code>Vue3</code>的书写格式:</p><pre><code>// 后续的补充内容,和此处上下文无关。
const Template = (args: any) => ({
props: Object.keys(args),
components: { SubmitForm, ElButton },
setup(props) {
const refName = computed(() => props.refName)
const submitFormRef = ref();
function submit() {
console.log(submitFormRef.value.values);
}
function onRuntimeChange(name: string, value: any) {
console.log(name, " = ", value);
}
return {
submit,
onRuntimeChange,
[refName.value]: submitFormRef,
...props,
};
},
template: `
<submit-form :schema="schema" :ref="refName" @runtimeChange="onRuntimeChange"></submit-form>
<el-button @click="submit">提交</el-button>
`,
});
</code></pre><h5>全局依赖配置</h5><p>因为示例代码中依赖<code>element-plus</code>,通过上述展现的页面没有样式,所以,<code>StoryBook</code>渲染需要额外引入<code>element-plus</code>主题:</p><pre><code>// preview.js
import "element-plus/lib/theme-chalk/index.css";
export const parameters = {
actions: { argTypesRegex: "^on[A-Z].*" },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
}
}
</code></pre><h5>启动本地服务</h5><h6>更新命令脚本</h6><pre><code> // package.json
"scripts": {
"storybook": "start-storybook -p 6006 -h 0.0.0.0",
"build-storybook": "build-storybook"
},
</code></pre><p><code>-h 0.0.0.0</code>以支持局域网访问。</p><h6>执行命令</h6><pre><code>$ npm run storybook
</code></pre><h6>效果展示</h6><p><img src="/img/remote/1460000041592229" alt="image" title="image"></p><h5>在<code>Stories</code>中使用第三方UI库</h5><p>以<code>ElementPlus</code>为例:</p><h6>全局配置</h6><p>如果<code>babel.config</code>没有配置按需加载,可直接编辑<code>.storybook/preview.js</code>:</p><pre><code>// .storybook/preview.js
import elementPlus from 'element-plus';
import { app } from '@storybook/vue3'
app.use(elementPlus);
export const decorators = [
(story) => ({
components: { story, elementPlus },
template: '<elementPlus><story/></elementPlus>'
})
];
import "element-plus/lib/theme-chalk/index.css";
export const parameters = {
actions: { argTypesRegex: "^on[A-Z].*" },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
}
}
</code></pre><p><strong>Notes:</strong>配置按需加载后,<code>import elementPlus from 'element-plus';</code>导入<code>elementPlus</code>报错:<code>elementPlus is not defined</code> —— 全局加载、按需加载不能在同一项目中使用。</p><h6>按需加载</h6><p>在需要使用<code>ElementPlus</code>的<code>Stories</code>中直接引入即可:</p><pre><code>// packages/SubmitForm/SubmitForm.stories.ts
import { ElButton } from 'element-plus';
import SubmitForm from "./index";
import { SchemaType, RuleTrigger } from "./src/schemas/baseSchema";
const caseSchema = [
{
key: "moduleName",
name: "title",
type: SchemaType.Text,
label: "栏目名称",
placeholder: "请输入栏目名称",
attrs: {
//
},
rules: [
{
required: true,
message: "栏目名称必填~",
trigger: RuleTrigger.Blur,
},
],
},
...
];
export default {
title: "ui组件/SubmitForm",
component: SubmitForm,
};
const Template = (args: any) => ({
components: { SubmitForm, ElButton },
setup() {
return {
...args,
};
},
template: '<submit-form :schema="schema" ref="submitFormRef"></submit-form><el-button @click="submit">提交</el-button>',
});
export const 基本应用 = Template.bind({});
(基本应用 as any).args = {
schema: caseSchema,
};
</code></pre><h6>示例代码添加交互</h6><pre><code>// packages/SubmitForm/SubmitForm.stories.ts
import { ElButton } from "element-plus";
import { ref } from "vue";
import SubmitForm from "./index";
import { SchemaType, RuleTrigger } from "./src/schemas/baseSchema";
const caseSchema = [
{
key: "moduleName",
name: "title",
type: SchemaType.Text,
label: "栏目名称",
placeholder: "请输入栏目名称",
attrs: {
//
},
rules: [
{
required: true,
message: "栏目名称必填~",
trigger: RuleTrigger.Blur,
},
],
},
...
];
export default {
title: "ui组件/SubmitForm",
component: SubmitForm,
};
const Template = (args: any) => ({
components: { SubmitForm, ElButton },
setup() {
const { refName } = args;
const submitFormRef = ref();
function submit() {
console.log(submitFormRef.value.values);
}
function onRuntimeChange(name: string, value: any) {
console.log(name, " = ", value);
}
return {
submit,
onRuntimeChange,
[refName]: submitFormRef,
...args,
};
},
template: `
<submit-form :schema="schema" :ref="refName" @runtimeChange="onRuntimeChange"></submit-form>
<el-button @click="submit">提交</el-button>
`,
});
export const 基本应用 = Template.bind({});
(基本应用 as any).args = {
refName: "submitFormRef",
schema: caseSchema,
};
</code></pre><p>这里做了两件事:</p><ul><li>增加提交按钮</li><li>增加数据提交交互</li></ul><h5>配置参数详情</h5><h6>默认文档展示</h6><p>默认查看到的文档参数是以下样子:</p><p><img src="/img/remote/1460000041592230" alt="image" title="image"></p><h6>参数配置</h6><p>通过配置<code>argTypes</code>可以补充参数信息:</p><pre><code>// packages/SubmitForm/SubmitForm.stories.ts
...
export default {
title: "ui组件/SubmitForm",
component: SubmitForm,
argTypes: {
refName: {
description: '表单组件引用',
type: {
required: true,
},
table: {
defaultValue: {
summary: 'defaultNameRef',
}
},
control: {
type: 'text'
}
},
schema: {
type: {
required: true,
},
table: {
type: {
summary: '渲染表单所需JSON结构',
detail: 'JSON结构包含表单渲染、交互所需要的必要字段,也包含表单的校验规则',
},
defaultValue: {
summary: '[]',
detail: `[
{
key: "moduleName",
name: "title",
type: SchemaType.Text,
label: "栏目名称",
placeholder: "请输入栏目名称",
attrs: {
//
},
rules: [
{
required: true,
message: "栏目名称必填~",
trigger: RuleTrigger.Blur,
},
],
}
]
`
}
}
},
runtimeChange: {
description: '实时监听表单的更新',
table: {
category: 'Events',
},
}
}
};
...
</code></pre><p><a href="https://link.segmentfault.com/?enc=UkM%2BvYbC%2BuIgI%2FBxWKWuuQ%3D%3D.Q%2BaAUtO2h01ojYrfNZK8xQCqrxVNqF2GQIDwaSI2fpK%2F6SDSPVH2lilbytjbk5yMIsxqTVDZfu9ah4D5%2BiGuGJ5WFwVvKRBLJe03ZW07PUM%3D" rel="nofollow">详细配置</a>见链接。</p><h6>理想效果</h6><p><img src="/img/remote/1460000041592231" alt="image" title="image"></p><h4>文档部署</h4><p>执行命令:</p><pre><code>$ npm run build-storybook
</code></pre><p>生成静态页面,直接部署静态页面即可。</p><p>目录结构:</p><pre><code>│ 0.0a0da810.iframe.bundle.js
│ 0.0a0da810.iframe.bundle.js.LICENSE.txt
│ 0.0a0da810.iframe.bundle.js.map
│ 0.799c368cbe88266827ba.manager.bundle.js
│ 1.9ebd2fb519f6726108de.manager.bundle.js
│ 1.9face5ef.iframe.bundle.js
│ 1.9face5ef.iframe.bundle.js.LICENSE.txt
│ 1.9face5ef.iframe.bundle.js.map
│ 10.07ff4e93.iframe.bundle.js
│ 10.a85ea1a67689be8e19ff.manager.bundle.js
│ 11.f4e922583ae35da460f3.manager.bundle.js
│ 11.f4e922583ae35da460f3.manager.bundle.js.LICENSE.txt
│ 12.1415460941f0bdcb8fa8.manager.bundle.js
│ 2.8a28fd4e.iframe.bundle.js
│ 2.8a28fd4e.iframe.bundle.js.LICENSE.txt
│ 2.8a28fd4e.iframe.bundle.js.map
│ 3.50826d47.iframe.bundle.js
│ 4.779a6efa.iframe.bundle.js
│ 5.f459d151315e6780c20f.manager.bundle.js
│ 5.f459d151315e6780c20f.manager.bundle.js.LICENSE.txt
│ 6.3bd64d820f3745f262ff.manager.bundle.js
│ 7.3d04765dbf3f1dcd706c.manager.bundle.js
│ 8.b541eadfcb9164835dfc.manager.bundle.js
│ 8.c6cb825f.iframe.bundle.js
│ 9.411ac8e451bbb10926c7.manager.bundle.js
│ 9.51f84f13.iframe.bundle.js
│ 9.51f84f13.iframe.bundle.js.LICENSE.txt
│ 9.51f84f13.iframe.bundle.js.map
│ favicon.ico
│ iframe.html
│ index.html // 入口页面
│ main.4c3140a78c06c6b39fba.manager.bundle.js
│ main.e86e1837.iframe.bundle.js
│ runtime~main.1e621db5.iframe.bundle.js
│ runtime~main.91a0c7330ab317d35c4a.manager.bundle.js
│ vendors~main.0d1916dd840230bedd21.manager.bundle.js
│ vendors~main.0d1916dd840230bedd21.manager.bundle.js.LICENSE.txt
│ vendors~main.8b18b60f.iframe.bundle.js
│ vendors~main.8b18b60f.iframe.bundle.js.LICENSE.txt
│ vendors~main.8b18b60f.iframe.bundle.js.map
│
└─static
└─media
element-icons.5bba4d97.ttf
element-icons.dcdb1ef8.woff
</code></pre><h2>参考文档</h2><ul><li><a href="https://link.segmentfault.com/?enc=2cRt1t%2Bz98RcyYhDsEsbzQ%3D%3D.lc1hp1VPklEW%2BHGppJwHtRSO9BTBz72jweXc6JnNDbk%3D" rel="nofollow">Storybook官网</a></li></ul><p>参考文章很多,如怀疑内容参考,请联系,会考虑增加到参考文档中</p>
自定义组件库—Storybook文档支持
https://segmentfault.com/a/1190000041116905
2021-12-14T17:37:07+08:00
2021-12-14T17:37:07+08:00
米花儿团儿
https://segmentfault.com/u/mihuartuanr
4
<h2><strong>简介</strong></h2><p><a href="https://link.segmentfault.com/?enc=W6x9EHP5%2BD1D3w2l30w7RA%3D%3D.Sj91aInGiYZ3xIAD2dzFc3v9LR9gsTNwIFdgcJRfp6g%3D" rel="nofollow">Storybook</a>是一个UI组件的开发环境。</p><h2><strong>使用</strong></h2><h3><strong>初始化StoryBook环境</strong></h3><pre><code>$ npx -p @storybook/cli sb init</code></pre><p>storybook自动检测开发环境,安装依赖。</p><p>执行以上命令行会进行以下操作:</p><p>1. 自动生成以下目录结构:</p><pre><code>├─.storybook // Storybook 全局配置文件
├─ main.js // 入口文件
└─ preview.js // 页面展示、全局资源配置
└─stories // 示例代码
└─assets</code></pre><p>2. 更新pkg#run-scripts:</p><pre><code>...
"scripts": {
"storybook": "start-storybook -p 6006 -h 0.0.0.0", // 启动本地服务以预览
"build-storybook": "build-storybook" // 构建
},
...
</code></pre><h3><strong>核心文件</strong></h3><h4><strong>main.js</strong></h4><pre><code>module.exports = {
"stories": [ // 组件Stories目录所在 —— Storybook会载入配置路径下的指定文件渲染展示
"../stories/**/*.stories.mdx",
"../stories/**/*.stories.@(js|jsx|ts|tsx)"
],
"addons": [ // Storybook所用插件 —— Storybook功能增强
"@storybook/addon-links",
"@storybook/addon-essentials"
],
"framework": "@storybook/vue3" // Storybook所用框架 —— Vue环境支持
}</code></pre><p>该文件定义StoryBook与<strong>编译</strong>相关的配置。</p><h4><strong>preview.js</strong></h4><pre><code>export const parameters = {
actions: { argTypesRegex: "^on[A-Z].*" },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
}
}</code></pre><p>该文件引入全局依赖,定义StoryBook<strong>渲染</strong>相关的配置。</p><h3><strong>简单示例</strong></h3><h4><strong>入口配置</strong></h4><p>更新.storybook/main.js,将组件所在目录注册到入口文件声明中:</p><pre><code>module.exports = {
"stories": [ // 组件Stories目录所在 —— Storybook会载入配置路径下的指定文件渲染展示
"../packages/**/*.stories.@(js|jsx|ts|tsx)"
],
...
}</code></pre><h4><strong>组件Story编写</strong></h4><pre><code>import SubmitForm from "./index"; // 引入组件
import { SchemaType, RuleTrigger } from "./src/schemas/baseSchema";
const caseSchema = [ // 示例数据
{
key: "moduleName",
name: "title",
type: SchemaType.Text,
label: "栏目名称",
placeholder: "请输入栏目名称",
attrs: {
//
},
rules: [
{
required: true,
message: "栏目名称必填~",
trigger: RuleTrigger.Blur,
},
],
},
...
];
export default {
title: "ui组件/SubmitForm", // 展示标题:使用路径定义命名空间 —— 分组、分类
component: SubmitForm,
};
const Template = (args: any) => ({ // 渲染组件
components: { SubmitForm },
setup() {
return {
...args,
};
},
template: '<submit-form :schema="schema"></submit-form>',
});
export const 基本应用 = Template.bind({}); // 组件应用示例
(基本应用 as any).args = {
schema: caseSchema,
ref: "submitFormRef",
};</code></pre><p>其中,</p><p>默认导出的是组件的元数据,包含归属组件、组件所属StoryBook文档分类、组件参数交互式声明...</p><p>更多配置参见:</p><p><a href="https://link.segmentfault.com/?enc=0RDg%2BiJh6ulxjFFtQytm2A%3D%3D.kHbV5SbyKH%2FwpZXGsZT4tqy95cROrxu%2FLfpPRnd00MPbTKHLu24ZUX2cJjg9WnmnxG9%2FbGxR16UOLy0Lqop0z3x3Y04FVdalo0RzWdpsutk%3D" rel="nofollow">ArgsTable配置</a></p><p><a href="https://link.segmentfault.com/?enc=qIEdNvKRTfK7RIbtImVEGg%3D%3D.EqWe7kcQSO3QbOM0arLc72Nqh%2FyEFlw9GJd1e7Uoat%2B8y7NRThi28ddyv%2BAJ2ilGI21SDaN04MWlHu87aEVKTw6E4p5Hu9cuSmSPuBPoaUU%3D" rel="nofollow">Contorl配置</a></p><p>命名导出的Story ( export const 基本应用 = Template.bind({}); ) 是一个函数,变量名为StoryBook文档展示的标题,另一种导出方式参考下文。</p><h4><strong>全局依赖配置</strong></h4><p>因为示例代码中依赖element-plus,通过上述展现的页面没有样式,所以,StoryBook渲染需要额外引入element-plus主题:</p><pre><code>// .storybook/preview.js
import "element-plus/lib/theme-chalk/index.css";
export const parameters = {
actions: { argTypesRegex: "^on[A-Z].*" },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
}
}</code></pre><h4><strong>启动本地服务</strong></h4><h5><strong>启动服务</strong></h5><p>更新pkg#storybook:</p><pre><code> // package.json
"scripts": {
"storybook": "start-storybook -p 6006 -h 0.0.0.0",
"build-storybook": "build-storybook"
},
</code></pre><p>命令行执行:</p><pre><code>$ npm run storybook
</code></pre><h5><strong>效果展示</strong></h5><p><img src="/img/remote/1460000041333166" alt="image" title="image"></p><p>默认参数栏只展示两项,如需更多参数信息,修改 preview.js 文件:</p><pre><code>export const parameters = {
actions: { argTypesRegex: "^on[A-Z].*" },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
controls: {
expanded: true // 展开所有参数信息
}
}
</code></pre><p><img src="/img/remote/1460000041333167" alt="image" title="image"></p><h3><strong>在Stories中使用第三方UI库</strong></h3><blockquote>以ElementPlus为例:</blockquote><h4><strong>全局配置</strong></h4><p>如果 <code>babel.config</code> 没有配置按需加载,可直接编辑<code>.storybook/preview.js</code>:</p><pre><code>// .storybook/preview.js
import elementPlus from 'element-plus';
import { app } from '@storybook/vue3'
app.use(elementPlus);
export const decorators = [
(story) => ({
components: { story, elementPlus },
template: '<elementPlus><story/></elementPlus>'
})
];
import "element-plus/lib/theme-chalk/index.css";
export const parameters = {
actions: { argTypesRegex: "^on[A-Z].*" },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
}
}</code></pre><p><strong>Notes:</strong>配置按需加载后,<code>import elementPlus from 'element-plus';</code>导入elementPlus报错:<code>elementPlus is not defined</code> —— 全局加载、按需加载不能在同一项目中使用。</p><h4><strong>按需加载</strong></h4><p>在需要使用ElementPlus的Stories中直接引入即可:</p><pre><code>// SubmitForm.stories.ts
import { ElButton } from 'element-plus';
import SubmitForm from "./index";
import { SchemaType, RuleTrigger } from "./src/schemas/baseSchema";
const caseSchema = [
{
key: "moduleName",
name: "title",
type: SchemaType.Text,
label: "栏目名称",
placeholder: "请输入栏目名称",
attrs: {
//
},
rules: [
{
required: true,
message: "栏目名称必填~",
trigger: RuleTrigger.Blur,
},
],
},
...
];
export default {
title: "ui组件/SubmitForm",
component: SubmitForm,
};
const Template = (args: any) => ({
components: { SubmitForm, ElButton },
setup() {
return {
...args,
};
},
template: '<submit-form :schema="schema" ref="submitFormRef"></submit-form><el-button @click="submit">提交</el-button>',
});
export const 基本应用 = Template.bind({});
(基本应用 as any).args = {
schema: caseSchema,
};</code></pre><h4><strong>补充已有示例交互</strong></h4><pre><code>// SubmitForm.stories.ts
import { ElButton } from "element-plus";
import { ref } from "vue";
import SubmitForm from "./index";
import { SchemaType, RuleTrigger } from "./src/schemas/baseSchema";
const caseSchema = [
{
key: "moduleName",
name: "title",
type: SchemaType.Text,
label: "栏目名称",
placeholder: "请输入栏目名称",
attrs: {
//
},
rules: [
{
required: true,
message: "栏目名称必填~",
trigger: RuleTrigger.Blur,
},
],
},
...
];
export default {
title: "ui组件/SubmitForm",
component: SubmitForm,
};
const Template = (args: any) => ({
components: { SubmitForm, ElButton },
setup() {
const { refName } = args;
const submitFormRef = ref();
function submit() {
console.log(submitFormRef.value.values);
}
function onRuntimeChange(name: string, value: any) {
console.log(name, " = ", value);
}
return {
submit,
onRuntimeChange,
[refName]: submitFormRef,
...args,
};
},
template: `
<submit-form :schema="schema" :ref="refName" @runtimeChange="onRuntimeChange"></submit-form>
<el-button @click="submit">提交</el-button>
`,
});
export const 基本应用 = Template.bind({});
(基本应用 as any).args = {
refName: "submitFormRef",
schema: caseSchema,
};</code></pre><p>这里做了两件事:</p><ul><li>增加提交按钮</li><li>增加数据提交交互</li></ul><h3><strong>配置参数文档</strong></h3><h4><strong>默认文档展示</strong></h4><p>默认查看到的文档是两栏展示:</p><p><img src="/img/remote/1460000041333168" alt="image" title="image"></p><p>更新 <code>.storybook/preview.js</code> 文件:</p><pre><code>export const parameters = {
actions: { argTypesRegex: "^on[A-Z].*" },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
controls: {
expanded: true
}
}</code></pre><p>参数的所有配置都展示:</p><p><img src="/img/remote/1460000041333169" alt="image" title="image"></p><h4><strong>参数配置</strong></h4><p>通过配置argTypes可以补充参数信息:</p><pre><code>// SubmitForm.stories.ts
...
export default {
title: "ui组件/SubmitForm",
component: SubmitForm,
argTypes: {
refName: {
description: '表单组件引用',
type: {
required: true,
},
table: {
defaultValue: {
summary: 'defaultNameRef',
}
},
control: {
type: 'text'
}
},
schema: {
type: {
required: true,
},
table: {
type: {
summary: '渲染表单所需JSON结构',
detail: 'JSON结构包含表单渲染、交互所需要的必要字段,也包含表单的校验规则',
},
defaultValue: {
summary: '[]',
detail: `[
{
key: "moduleName",
name: "title",
type: SchemaType.Text,
label: "栏目名称",
placeholder: "请输入栏目名称",
attrs: {
//
},
rules: [
{
required: true,
message: "栏目名称必填~",
trigger: RuleTrigger.Blur,
},
],
}
]
`
}
}
},
runtimeChange: {
description: '实时监听表单的更新',
table: {
category: 'Events',
},
}
}
};
...</code></pre><p>更多相关配置见:</p><p><a href="https://link.segmentfault.com/?enc=SgawxPuWW0VgFyHmVXBxnQ%3D%3D.U1XyLGmOd3dtcKzyq%2B1xIzmcTYLLDykByAXu5mBkGPtr8YYlfh4Cy3xHpzAiXxXTh2dd3duiPLFPOJ9EkKxozukiZ9J%2Fu97o4XlzZySyVmM%3D" rel="nofollow">ArgsTable配置</a></p><p><a href="https://link.segmentfault.com/?enc=TT8TYQ5%2BK6EWV41uVkHQGw%3D%3D.Um6B9zGYc78WpG%2BqwI2H2ymrVlFSTqHvZxhAVncEe5ZKqFqlSHhKTh5fEhUB4dcxWU8lUKCeYdHPLarYKVx55wQ4awz2WchEzD%2FLrCs3eSM%3D" rel="nofollow">Contorl配置</a></p><h2><strong>StoryBook功能模块</strong></h2><blockquote>一个Story是以函数形式描述如何渲染组件的方式。</blockquote><p><code>args</code>提供动态参数,提供省时的便利:</p><ul><li>参数可在<code>Controls</code>面板实时编辑,可检测组件在不同参数下的状态。</li><li><p>事件可在<code>Actions</code>面板查看日志输出。</p><ul><li>需要配置actions</li></ul></li></ul><h3><strong>自定义Stories展示名称</strong></h3><h4><strong>命名模块</strong></h4><pre><code>export const 自定义名称 = () => ({
components: { Button },
template: '<Button primary label="Button" />',
});</code></pre><h4><strong>【推荐】storyName属性设定</strong></h4><pre><code>export const Primary = () => ({
components: { Button },
template: '<Button primary label="Button" />',
});
Primary.storyName = '自定义名称';
</code></pre><ul><li><a href="https://link.segmentfault.com/?enc=kUtLisgh3P14jPu92vS6NA%3D%3D.PEx%2BOYX1Cvh1lRIlkUGccwc%2BuICl9PkfBJkIfxCZgtcfOcx14PjO4EiTnzqNyfKqeYHyCy890LvAHolWwgMoMQ%3D%3D" rel="nofollow">https://storybook.js.org/docs...</a></li></ul><h3><strong>Args</strong></h3><p>提供<strong>交互动态修改</strong>Props、Slots、styles、inputs...的方式,允许在Storybook交互界面中实时编辑,不必改动底层组件代码。</p><h4><strong>通过Storybook交互界面指定Args</strong></h4><p>界面直接修改</p><h4><strong>通过URL设定Args</strong></h4><p>?path=/story/avatar--default&args=style:rounded;size:100</p><p>由于安全策略(XSS攻击)和<a href="https://link.segmentfault.com/?enc=J9%2BBI1zoUD3QGOcR58Qg3g%3D%3D.ZD5iiqQJ6qBVIYSK7ahQ%2B42BRndi6gF6Z9TVLSPGrg9Wiu6hXiiQpt92fUeF5GZzxL9TBy58q1DOxIKlATG8ftV9N1uaO3iKBqgcSHqt63sPs1EmyQ83wOKCdF1d9YSL" rel="nofollow">特殊值</a>,传入URL需要处理,<a href="https://link.segmentfault.com/?enc=HW71dIZ%2Fdzwksq5txmjjHA%3D%3D.fPnmmLZg%2B4foFfLi9KPDUPmQMJsAqo1BdxMiWH5pLo49Drm7ylWWhNocexaJByHoGcZZixX3jaSdn9mEnREhOZKt7e06JJw0XMpQpoaNEALDG3r8c4A4PVQIGG7yH7%2Bg" rel="nofollow">见详情</a>。</p><h3><strong>全局依赖</strong></h3><pre><code>// .storybook/preview.js
import { app } from '@storybook/vue3';
import Vuex from 'vuex';
//👇 Storybook Vue app being extended and registering the library
app.use(Vuex);
export const decorators = [
(story) => ({
components: { story },
template: '<div style="margin: 3em;"><story /></div>',
}),
];</code></pre><h3><strong>静态资源访问</strong></h3><pre><code>// .storybook/main.js
module.exports = {
stories: [],
addons: [],
staticDirs: ['../public'],
};</code></pre><p>或</p><pre><code>// .storybook/main.js
module.exports = {
staticDirs: [
{
from: '../my-custom-assets/images',
to: '/assets'
}
],
};</code></pre><p>查看<a href="https://link.segmentfault.com/?enc=WGCr7hz%2F50C4WRDE%2Bkzyog%3D%3D.8V8zK0Z79hvBBEXteQEdZxSnhxUOla4ORZKdKLgmcv8PkCe2xuW3s%2Br80KDV8OZMcXEbJ3iUYjf5Mqz8jCZKK0LZ7L4KBZkJMfZvh5XZ8rQ%3D" rel="nofollow">更多配置</a></p><h2><strong><a href="https://link.segmentfault.com/?enc=q4mdn4Tg3cJHWUGsu4x1cg%3D%3D.7nCGDDssjUo04g%2FDrFpLIop7TK8PZcZuDXfpKgrUVG8jjMYpULMk187OV%2BNJSoLWpexbrp4OuGRQZ4%2FjWDyzv3YEaFKPmZJd4NSJWsB3JnY%3D" rel="nofollow">定制化主题</a></strong></h2><h3><strong>修改Logo</strong></h3><p>安装依赖:</p><pre><code>npm i -D @storybook/addons @storybook/theming</code></pre><p>修改pkg#scripts</p><pre><code>// pkg#scripts
"storybook": "start-storybook -p 6006 -s public",
"build-storybook": "build-storybook -s public",</code></pre><p>新建.storybook/manager.js文件:</p><pre><code>import { addons } from "@storybook/addons";
import theme from "./themes/theme";
addons.setConfig({
theme: theme
})</code></pre><p>创建./storybook/themes/theme.js:</p><pre><code>// .storybook/themes/theme.js
import { create } from '@storybook/theming';
export default create({
base: 'light',
brandTitle: 'Custom StoryBook', // logo不展示时,替代文本alt
brandImage: '/logo.png',
});</code></pre><p><strong>Notes:</strong>brandImage和brandTitle同时配置的情况下,只有一项起作用,优先级brandImage > brandTitle</p><p><strong>Notes:</strong>自定义主题时,base配置是必填的。</p><pre><code>// package.json
{
...
"scripts": {
"replace": "rimraf storybook-static/favicon.ico && cpr .storybook/themes/favicon.ico storybook-static/favicon.ico",
"storybook": "start-storybook -p 6006 -h 0.0.0.0",
"build-storybook": "build-storybook && npm run replace"
},
}</code></pre><p><strong>Notes:</strong>打包的话,需要用本地图标替换storybook包内的默认图标。</p><p>这里使用了Cli参数<code>-s</code>指定静态文件访问地址,更推荐在main.js中配置:</p><pre><code>// .storybook/main.js
module.exports = {
stories: [],
addons: [],
staticDirs: ['/public'],
};</code></pre><h3><strong>修改站点Title、favicon</strong></h3><p>新增.storybook/manager-head.html:</p><pre><code><link rel="shortcut icon" href="/favicon.ico">
<script>
var observer = new MutationObserver(function(mutations) {
if (document.title.match(/Storybook$/)) {
document.title = "M.UI | GameCenter";
}
}).observe(document.querySelector("title"), {
childList: true,
subtree: true,
characterData: true
});
</script></code></pre><p>参见<a href="https://link.segmentfault.com/?enc=s2fVSrTxCELwvhCurJSzkA%3D%3D.o3UAt83Bg%2BJgSpf%2FwSk3BpghaVvw7lNKrcr8x9%2BHZgrLQ11YucK%2B6U2eRsk0QIa5ZKdipF4srbkMeF5eJHrzjg%3D%3D" rel="nofollow">更多配置</a></p><h3><strong>组件样式—Scss</strong></h3><p>组件样式需要storybook-addon的支持</p><pre><code>npm i -D @storybook/preset-scss
</code></pre><p>修改.storybook/main.js</p><pre><code>module.exports = {
"stories": [
"../packages/**/*.stories.@(js|jsx|ts|tsx)",
],
"addons": [
"@storybook/addon-links",
"@storybook/addon-essentials",
"@storybook/preset-scss"
],
"framework": "@storybook/vue3"
}</code></pre><h3><strong>Rem支持</strong></h3><p>替换@storybook/preset-scss为@storybook/addon-postcss:</p><pre><code>npm uninstall -D @storybook/preset-scss
npm i -D @storybook/addon-postcss
# 修改webpack内核为版本5
# 初始化环境时修改
npx -y sb init --builder webpack5
# 初始化时未设定,后续修改
npm i -D @storybook/builder-webpack5
npm i -D @storybook/manager-webpack5</code></pre><pre><code>// 修改.storybook/main.js
module.exports = {
"stories": [
"../packages/**/*.stories.@(js|jsx|ts|tsx)",
],
"addons": [
"@storybook/addon-links",
"@storybook/addon-essentials",
{
name: '@storybook/addon-postcss',
options: {
postcssLoaderOptions: {
implementation: require('postcss'),
},
sassLoaderOptions: {
implementation: require('sass'),
}
},
}
],
core: {
builder: 'webpack5',
},
webpackFinal: (config) => {
config.module.rules.push({
test: /\.scss$/,
use: ['style-loader', 'css-loader', 'postcss-loader', 'sass-loader'],
});
return config
},
"framework": "@storybook/vue3"
}</code></pre><p>在preview.js中引入flexible.js,并定义视窗:</p><pre><code>import { INITIAL_VIEWPORTS } from '@storybook/addon-viewport';
import '../assets/js/flexible.all';
import './assets/stylesheets/sb.scss';
import '../assets/stylesheets/reset.scss';
export const parameters = {
actions: { argTypesRegex: "^on[A-Z].*" },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
viewport: {
viewports: INITIAL_VIEWPORTS, // newViewports would be an ViewportMap. (see below for examples)
defaultViewport: 'iphone6',
},
controls: {
expanded: true
}
}</code></pre><p>详细内容参见<a href="https://link.segmentfault.com/?enc=5OMXDdLut15YY%2FiN1nvjVQ%3D%3D.la%2B2KvSJquoC3w41FSm5mchsd6tWstYmFeYxfW38tFeBsuNLuviPLNiH9A%2FEF71o%2Bd0zxvBP%2FWVR1fkhx%2F20SA%3D%3D" rel="nofollow">文章地址</a></p><h3>Decorator自定义画布、文档样式</h3><p>Decorator通过包裹Story来增强其表现形式。<br>作用域:全局 > Component > Story(按执行顺序排列)</p><pre><code>// js示例
export default {
title: 'YourComponent',
component: YourComponent,
decorators: [() => ({ template: '<div style="margin: 3em;"><story/></div>' })],
};
// mdx示例
<Meta
title="YourComponent"
component={YourComponent}
decorators={[
() => ({
template: '<div style="margin: 3em;"><story /></div>',
}),
]}
/></code></pre><p>Decorator可在<code>.storybook/preview.js</code>、组件声明、Story声明中定义,最终会合并,执行顺序为 Global > Component > Story,可通过定义ClassName的命名空间来定制样式。</p><p>更多配置: <a href="https://link.segmentfault.com/?enc=YZXF1KEUyZYbPf6q9W97xA%3D%3D.xI2yHF1xkn16P3dHBOFiw4qkM%2Blsq5v4Jfz2w0v7c1JpgybAB9D1O0Z6epS3wzrmCRxwj0fEDCmjBkPEOD%2FViH%2FsYKaC7gir6AifpP1BvEdxpmyi1ST6QSnOIk9vKovo" rel="nofollow">https://storybook.js.org/docs...</a></p><h3>画布、文档的分离</h3><p>默认文档中包含画布信息,若不想文档中渲染Story,需要写<em>.stories.js和</em>.stories.mdx两个文件<br>其中,<strong><em>.stories.js中默认导出不添加任何元数据,转移至</em>.stories.mdx中</strong>,示例如下:</p><pre><code>// *.stories.ts
export default {}
const Template = (args: any) => ({
components: {
Drawer
},
props: Object.keys(args),
methods: {
onClose: action('onClose'),
onStretched: action('onStretched')
},
setup () {
const state = reactive({
...args
})
function toogleVisible () {
state.visible = true
}
return {
state,
toogleVisible
}
},
template: `
<Drawer v-bind="state" v-model:visible="state.visible" @close="onClose" @stretched="onStretched">
</Drawer>
<button @click="toogleVisible">切换可见性</button>
`
})
export const Primary = Template.bind({}) as any
(Primary as any).args = {
visible: false,
title: '示例'
}
Primary.parameters = { docs: { disable: true } };
// *.stories.mdx
import { Meta, Story } from '@storybook/addon-docs'
import { Primary } from './Drawer.stories.ts';
import Drawer from './index'
export const argsType = {
visible: {
description: '是否显示Dialog,支持.sync修饰符',
type: {
required: true
},
table: {
type: {
summary: 'Boolean'
},
defaultValue: {
summary: false
}
// category: 'Boolean' // 参数分组
// subcategory: 'Button colors', // 子分组
},
control: 'boolean'
},
...
}
# Drawer
## 基本用法
<Story
name="Drawer"
decorators={[
() => ({
template: '<div id="custom-root" style="background: red;"><story /></div>',
})
]}
story={Primary} />
## 自定义内容
## 参数文档
<Meta
title="组件/Basic/Drawer"
component={Drawer}
argTypes={{
...argsType
}}
/></code></pre><p>parameters = { docs: { disable: true } }可在docs中禁止渲染Story<br><a href="https://link.segmentfault.com/?enc=Fy6qRm2km%2FedawMMoOYwmg%3D%3D.351GBN6uf2258%2FkQYFvhi1teojfWzbGjbAxmYaMOajYXdpI1HaMj7WiKDNSVOilaMpCgm1ySOU%2BrImrQlmLD5aVIM4hpdL7SIrtI8JTI46k%3D" rel="nofollow">https://storybook.js.org/docs...</a><br><a href="https://link.segmentfault.com/?enc=eCVEoTe%2Fv0MMtLNNjW%2BtIg%3D%3D.LJPaB5EP%2BdYnfbcbGGFCD0WXdeB3sxfdVABLpuVADRYMS%2FFxACyF%2BnoITVnRclte6TEYKhkPIMHbkg3kdY3nBXy0x%2Be0YlwhSTYZb9I4LUH4nzVamkJf8X8sC8b6C4xS" rel="nofollow">https://github.com/storybookj...</a><br><a href="https://link.segmentfault.com/?enc=FW9Zuct3vkv6d1CpHdlzZw%3D%3D.mVuQiTNEqft5tm5QY82fUV4aRWbDWFvXm3tvr3MQFplGASwG0TP2%2BRJxLwfbkPWdXfJNvmY9ma3d%2BeRA0rnpoZpxvhPslvWc8mnuA4gCeQoyVzyN3SLQ42th%2BRB2Np9mlOzzKaJL0fzLTIYgUpRdOA%3D%3D" rel="nofollow">https://github.com/storybookj...</a></p><p><strong>Notes:</strong></p><ul><li>mdx文件中jsx和markdown语法之间要用空行分隔;</li><li>jsx定义对象,尤其是空对象,不能有多余的空行;</li></ul><pre><code>export const argTypes = {
}
# 这样会报错</code></pre><p>正确写法:</p><pre><code>export const argTypes = {}
# 这样才能正确解析</code></pre><h3>侧边栏忽略子节点</h3><p>若不想要侧边栏创建子节点,可以定义Story.storyName与export default的组件title保持一致:</p><pre><code>// 示例
export default {
title: '组件/Basic/Drawer'
}
export const Primary = Template.bind({}) as any
Primary.storyName = 'Drawer';</code></pre><h3>调整docs优先展示权</h3><p>默认优先展示stories,可以通过优先展示docs</p><pre><code>parameters = {
docs: {
disable: true
},
viewMode: 'docs'
}
</code></pre><p><a href="https://link.segmentfault.com/?enc=O2M1UHzVQso3QIjHwp8HZA%3D%3D.38sP1xFJQhEKh271XAMCZXQIIamg18n9oOG9fnE1u8yDNitDQVi2e2DUaJKNNhf3ZXaWgshJkigXo3juOu%2B4H1acG7GKZR2pNjjKHPJm8bEm6vHxPJgPcMe83gTszHQ77tjRljsxgEK14kK7wxYYXjq2IMuka9lIblDVfdG5IAJwN4YM2xRElHy3SlJ4h8tl1Cxme9CO5XQCLUXk3D1AkA%3D%3D" rel="nofollow">https://github.com/storybookj...</a></p><h3>隐藏Canvas</h3><pre><code> previewTabs: {
canvas: {
hidden: true,
}
},</code></pre><p>可以隐藏当前Stories的Canvas面板</p><h3>修改Logo跳转地址</h3><pre><code>// .storybook/themes/theme.js
import { create } from '@storybook/theming';
export default create({
base: 'light',
brandTitle: 'StoryBook',
brandUrl: '/?path=/docs/快速开始--primary',
brandImage: '/logo.png',
});</code></pre><h3>关闭Addons</h3><p>showPanel无论设置在哪一层级,都是<strong>全局的</strong></p><pre><code>parameters = {
docs: {
disable: true
},
controls: {
disable: true
},
options: {
showPanel: false
}
}
</code></pre><h2>MDX写法</h2><h3>动机</h3><p>上述的写法为CSF,component story format,是 Storybook 官方推荐的一种基于 ES module 的 stories 编写方法,由一个 <code>export default</code> 和一个或多个 export 组成。</p><p>它是Storybook 提供了一些封装过后的组件供使用,让我们能够较为快速的生成 stories。</p><p>代价是<strong>灵活度会相对的没有高</strong>,当然,如果只是简单的展示组件及其接收参数,那其实已经完全足够了。</p><p>可如果在展示组件之余,还想要<strong>编写一个额外的文档,</strong>比如介绍一下组件封装的背景,用到的技术等,CSF 就不是那么好用了。</p><p>基于这样的需求,Storybook 也支持使用 MDX 格式编写 stories。</p><p>MDX,如同 TSX,就是一种能够在 Markdown 文档中写 JSX 的格式。使用 MDX 格式编写 stories,文字部分内容的编写会更加灵活,没有了官方预置的内容,真正的所写即所得。</p><h3>MDX示例</h3><pre><code>import { ArgsTable, Canvas, Meta, Story } from '@storybook/addon-docs';
import { action } from '@storybook/addon-actions'
import { reactive } from 'vue'
import Dialog from './index'
# Dialog
export const argsType = {
visible: {
description: '是否显示Dialog,支持.sync修饰符',
type: {
required: true
},
table: {
type: {
summary: 'Boolean'
},
defaultValue: {
summary: false
}
// category: 'Boolean' // 参数分组
// subcategory: 'Button colors', // 子分组
},
control: 'boolean'
},
showCancel: {
description: '展示独立的关闭按钮X',
table: {
type: {
summary: 'Boolean'
},
defaultValue: {
summary: true
}
},
control: 'boolean'
},
title: {
description: 'Dialog的标题,也可通过具名slot(见下表)传入',
table: {
defaultValue: {
summary: '示例Dialog'
}
},
control: 'text'
},
rawHtml: {
description: 'dialog主体内容',
table: {
type: {
summary: 'string / htmlString'
},
defaultValue: {
summary: 'DJ小能手',
detail: '<span style="color: red;">DJ</span>小能手'
}
},
control: 'text'
},
confirm: {
description: '确认相关的配置项目',
mapping: {
label: '确定按钮的文本内容',
handler: '确认的回调'
},
options: ['label', 'handler'],
table: {
type: {
summary: 'Object',
detail: `
confirm.label: 确定按钮的文本内容;
confirm.handler: 确认的回调;
`
}
},
control: 'object'
},
cancel: {
description: '取消相关的配置项目',
table: {
type: {
summary: 'Object',
detail: `
cancel.label: 取消按钮的文本内容;
cancel.handler: 取消的回调;`
}
},
control: 'object'
},
header: {
description: 'Dialog标题区的内容',
table: {
type: {
summary: 'Vnode'
},
defaultValue: ''
},
control: 'text',
category: 'Slots'
},
default: {
description: 'Dialog的内容',
table: {
type: {
summary: 'Vnode'
},
defaultValue: ''
},
control: 'text',
category: 'Slots'
},
'update:visible': {
table: {
disable: true
}
}
}
export const actionData = {
updateVisible: action('update:visible')
}
## 参数文档
<Meta
title="组件/Basic/Dialog"
component={Dialog}
argTypes={{
...argsType
}}
/>
<ArgsTable story="基本用法" />
## 基本用法
export const argsData = {
visible: false,
showCancel: true,
confirm: {
label: '确定',
handler () {
console.log('确定')
}
},
cancel: {
handler () {
console.log('X')
}
}
}
export const HTMLTemplate = `
<Dialog v-bind="state" v-model:visible="state.visible" @update:visible="updateVisible">
<template v-slot:header v-if="state.header">
<header v-html="state.header"></header>
</template>
<template v-slot:default v-if="state.default">
<main v-html="state.default"></main>
</template>
</Dialog>
<button @click="toggleVisible">切换可见性</button>
`
export const ConstructorFactory = (args) => ({
components: { Dialog },
props: Object.keys(args),
setup () {
const state = reactive({
...args
})
function toggleVisible () {
state.visible = true
}
return {
state,
toggleVisible
}
},
methods: {
...actionData
},
template: HTMLTemplate
})
<Canvas
mdxSource={HTMLTemplate}
>
<Story
name="基本用法"
args={{...argsData}}
>
{
ConstructorFactory.bind({})
}
</Story>
</Canvas>
</code></pre><h3>内置组件</h3><h4>Meta</h4><p>声明本 MDX 文件渲染的页面的标题,对应的组件等。作用和 CSF 写法中的 <code>export default</code> 一致;</p><h4>ArgsTable</h4><p>自定义arguments类型,用于Props、Slots、Events展示与交互。</p><table><thead><tr><th align="left">属性</th><th align="left">示例</th><th align="left">属性说明</th><th align="left">属性值</th><th align="left">属性值示例</th></tr></thead><tbody><tr><td align="left">of</td><td align="left"><code><ArgsTable of={ComponentObj} /></code></td><td align="left">自动解析组件的Props、Slots、Events等声明,依据Storybook内置类型声明输出Storybook文档</td><td align="left">import导入的组件对象</td><td align="left"><code>import Dialog from './index'</code>的"Dialog"</td></tr><tr><td align="left">story</td><td align="left"><code><ArgsTable story="StoryNameString" /></code></td><td align="left">自定义arguments,需要使用story属性承接arguments类型声明。</td><td align="left">Story标签的name属性值</td><td align="left"><code><Story name="storyName"></code>的"storyName"</td></tr></tbody></table><h4>Canvas</h4><p>生成一个 Canvas 画板,用于展示我们自己编写的组件。</p><p>画布会提供一些便捷的功能,比如展示当前组件的源代码等;</p><p>Canvas默认将Stroybook的Template函数作为源码输出,若想自定义源码输出,将源码作为属性<code>mdxSource</code>的值即可。</p><p><a href="https://link.segmentfault.com/?enc=JWrAQRFz7uiu245Gh3AyrA%3D%3D.q4Eq2bmSW5Nffmf1R%2BFO%2FMCbNKZNlkLDfrF7mVbfiy0xXjLkggGoyI1c2aR9fkia6r%2Beushj8KPW4hw%2BDeCBCWI9s6OoDHG4eCUadNGKZ1n%2B3nXNPpefGfGD5fVJzXC8hcdy4YHJioSOHygGwHYiFj5kGhZEnxjRmoZwWl3FVYE%3D" rel="nofollow">点击链接查看更多属性配置</a></p><h4>Story</h4><p>生成一个 story。</p><p><strong>Notes:</strong><em>并不是一定要把 story 写在</em> <code>*<Cavas />*</code><em>组件中,Canvas 组件只是能够提供一些其他的功能(展示源码、复制代码之类的)。直接编写 story 的话,组件也能正常渲染。</em></p><p><img src="/img/remote/1460000041344654" alt="image" title="image"></p><p>区别在于,写在<code><Canvas /></code>中,<code>Canvas</code>会提供便捷的功能:工具栏、源代码查看...</p><h3>vscode语法提示插件</h3><h4>名称: MDX</h4><p>ID: silvenon.mdx<br>说明: Provides syntax highlighting and bracket matching for MDX (JSX in Markdown) files.<br>版本: 0.1.0<br>发布者: Matija Marohnić<br>VS Marketplace 链接: <a href="https://link.segmentfault.com/?enc=51vneBaZgZfvH1mVnVYJlA%3D%3D.UGnoJ%2FdMug3JbBYA1C8T%2F87EZQmIr5Xg%2FgacMtX83p66wlOlT8%2F3%2BSIpZgaq1HGyxbpLzRPVeQ1JlNaBUEjPUE5Eu%2B96iIqJOlEnvP9yHaY%3D" rel="nofollow">https://marketplace.visualstu...</a></p><h4>名称: MDX Preview</h4><p>ID: xyc.vscode-mdx-preview<br>说明: MDX Preview<br>版本: 0.3.3<br>发布者: Xiaoyi Chen<br>VS Marketplace 链接: <a href="https://link.segmentfault.com/?enc=m1UI%2BJTWs%2FttgFLkUH8dWA%3D%3D.nkLYRYawagJ7FW0vx07mRAJMamJnf1BxoiGa5nPJNyaKKGFGx4yejEZEoHVvmfDxh0Y0dsfnON5RGVcOKZcV41jjfI%2FcQy1eeEadT%2FoxgJo%3D" rel="nofollow">https://marketplace.visualstu...</a></p><h4>名称: Vue Storybook Snippets</h4><p>ID: megrax.vue-storybook-snippets<br>说明: Storybook Snippets for Vue<br>版本: 0.0.7<br>发布者: Megrax<br>VS Marketplace 链接: <a href="https://link.segmentfault.com/?enc=DvkQQIbDk2yt6OirJy0mLQ%3D%3D.hxkcm8NFOi%2BwICTK%2Bv9mERBtIKV1TU3kWaaVgO9LZPEDy7h44kideIRe8sT5Tqrnv5ImhRPAgAlF5zTkVRAVcjUvnGfIRQTidCZcZz7tGaQ3gomMgrpjeGfY4eNifnU8" rel="nofollow">https://marketplace.visualstu...</a></p><p><strong>同类对比</strong></p><ul><li><a href="https://link.segmentfault.com/?enc=QEufrUqRF7ppQsWuQlvt0Q%3D%3D.MolZycS7cbIs12QA125OCHLw1Rbqrh6dItmaiaGhgfPAgXvSa3GJ49BjO7Lbg2rtQp41IZp3MWdHYnOd0LkVIlaV5B5KnvtS8IkfHbT%2FczE%3D" rel="nofollow">vue-styleguidist.github.io</a></li><li><a href="https://link.segmentfault.com/?enc=HWTy5Z1tGkaGXTljq8sRpw%3D%3D.jQ2mLKJVtErqSVg96%2FxOAJ53olQOtSYwySGo%2F9DBw78%3D" rel="nofollow">vuepress</a></li></ul>
Bit 共享代码
https://segmentfault.com/a/1190000040984266
2021-11-19T01:02:02+08:00
2021-11-19T01:02:02+08:00
米花儿团儿
https://segmentfault.com/u/mihuartuanr
0
<h2>Think in Component</h2><p>Bit是组件驱动架构,基于组件的现代应用开发。在Bit的世界里,一切皆组件。</p><p>组件可以组合成其他组件,最终组成一个应用APP,即APP也是组件的一种。</p><p>这为我们开发提供一个新的思路:我们构建可以整合成不同应用的组件,而不是构建包含组件的应用。</p><p>Bit帮我们构建模块化、稳固的、可测试、可复用的代码。</p><p>Bit Cloud是组件的云托管服务。它为开发人员和团队提供端到端的解决方案,用于托管、组织、检索、使用、更新和协作处理组件。</p><p><img src="/img/remote/1460000040984268" alt="image" title="image"></p><h2>Bit优势</h2><ul><li>以组件架构的思想帮助我们构建模块化、稳固的、可测试、可复用的代码。</li><li>从现有代码结构中分离组件,无需更改结构,或维护新的项目。</li><li>可更改依赖组件,并创建自己的版本独立管理,无需担忧污染其它环境。</li></ul><h2>初始化Bit工作区</h2><h3>安装BVM & Bit</h3><p><a href="https://link.segmentfault.com/?enc=%2Fpc6A6P6g393ahAEVUmJTQ%3D%3D.QF%2FcCS8nCmSiwGYFwKtprXnxCDoNuH9UQtFU%2FtKEPxw%3D" rel="nofollow">BVM</a>是Bit版本管理工具,雷同NVM</p><pre><code>// node版本12.22.0以上
npm i -g @teambit/bvm
</code></pre><p>执行bvm -h检验是否安装成功,若提醒bvm命令不可用,需要设置环境变量:</p><pre><code># MacOs Bash
echo 'export PATH=$HOME/bin:$PATH' >> ~/.bashrc && source ~/.bashrc
# zsh
echo 'export PATH=$HOME/bin:$PATH' >> ~/.zshrc && source ~/.zshrc
# windows
setx path "%path%;%LocalAppData%\.bvm"
</code></pre><p>安装最新版bit:</p><pre><code>bvm install
</code></pre><p>执行bit -h检验是否安装成功,若提醒bit命令不可用,需要按上述流程设置一下环境变量。</p><h3>bit new命令初始化工作区</h3><blockquote>适用于新建项目</blockquote><pre><code>$ bit new <env> <project>
$ cd <project>
$ bit install
</code></pre><h3>bit init命令初始化工作区</h3><blockquote>适用于已有项目</blockquote><ol><li>先初始化环境</li></ol><pre><code>$ cd <project>
$ bit init --harmony
</code></pre><ol start="2"><li>手动配置开发环境</li></ol><p>以react环境为例,修改workspace.jsonc文件:</p><pre><code>"teambit.workspace/variants": {
"*": {
"teambit.react/react": { }
}
}
</code></pre><ol start="3"><li>安装必要的peer依赖</li></ol><pre><code>$ bit install react --type peer
$ bit install react-dom --type peer
</code></pre><h3>初始化Git</h3><p>需要将workspace.jsonc和.bitmap 上传到Git。</p><h2>创建组件</h2><h3>使用内置组件创建</h3><p>以react为例:</p><ol><li>以内置模版创建组件bit create <built-in-template> <component></li></ol><pre><code>$ bit templates # 查看所有的内置模版
$ bit create react-component ui/button # TypeScript
$ bit create react-component-js ui/button # JavaScript
</code></pre><p><strong>注意:</strong>其中,<component>可以是个路径,前置路径为命名空间,上述示例等同于bit create react-component button --namespace ui。</p><ol start="2"><li>添加测试用例</li></ol><pre><code>$ bit install @testing-library/react
</code></pre><ol start="3"><li>编译并起服务</li></ol><pre><code>$ bit compile
$ bit start
</code></pre><h3>自定义组件</h3><ol><li>已有组件结构与代码</li><li>通过bit add <relative-path> --namespace <namespace>添加组件</li></ol><h3>查看组件信息</h3><blockquote>可以查看组件编译环境、包含文件、依赖等所有信息。</blockquote><pre><code>$ bit show <component-id>
</code></pre><p>输出信息示例:</p><pre><code> ┌───────────────┬────────────────────────────────────────────────────────────────────┐
│ id │ my-scope/ui/button │
├───────────────┼────────────────────────────────────────────────────────────────────┤
│ scope │ my-scope │
├───────────────┼────────────────────────────────────────────────────────────────────┤
│ name │ ui/button │
├───────────────┼────────────────────────────────────────────────────────────────────┤
│ env │ teambit.react/react │
├───────────────┼────────────────────────────────────────────────────────────────────┤
│ package name │ @my-scope/ui.button │
├───────────────┼────────────────────────────────────────────────────────────────────┤
│ main file │ index.ts │
├───────────────┼────────────────────────────────────────────────────────────────────┤
│ files │ button.composition.tsx │
│ │ button.docs.mdx │
│ │ button.tsx │
│ │ button.spec.tsx │
│ │ index.ts │
├───────────────┼────────────────────────────────────────────────────────────────────┤
│ dev files │ button.docs.mdx (teambit.docs/docs) │
│ │ button.spec.tsx (teambit.defender/tester) │
│ │ button.composition.tsx (teambit.compositions/compositions) │
├───────────────┼────────────────────────────────────────────────────────────────────┤
│ extensions │ teambit.react/react │
│ │ teambit.component/dev-files │
│ │ teambit.compositions/compositions │
│ │ teambit.pkg/pkg │
│ │ teambit.docs/docs │
│ │ teambit.envs/envs │
│ │ teambit.dependencies/dependency-resolver │
├───────────────┼────────────────────────────────────────────────────────────────────┤
│ dependencies │ core-js@3.8.3- (package) │
├───────────────┼────────────────────────────────────────────────────────────────────┤
│ dev │ @testing-library/react@11.2.6- (package) │
│ dependencies │ @babel/runtime@7.12.18-------- (package) │
│ │ @types/react-router-dom@5.1.7- (package) │
│ │ @types/jest@26.0.20----------- (package) │
│ │ @types/react@16.9.43---------- (package) │
│ │ @types/node@12.20.4----------- (package) │
├───────────────┼────────────────────────────────────────────────────────────────────┤
│ peer │ react@16.13.1----- (package) │
│ dependencies │ react-dom@16.13.1- (package) │
└───────────────┴────────────────────────────────────────────────────────────────────┘
</code></pre><h3>查看组件状态</h3><pre><code>$ bit status
</code></pre><h3>查看组件所有版本</h3><pre><code>$ bit log <component-id>
</code></pre><h3>查看本地所有组件列表</h3><pre><code>$ bit list
</code></pre><h3>启动测试服务器</h3><blockquote>通过 worker 运行不同的工作区任务,例如测试、linter 和由组件定义的任何工作区任务。</blockquote><pre><code>$ bit compile
$ bit start
</code></pre><h2>使用组件</h2><blockquote>在导入另一个组件作为依赖时,Bit不<strong>允许使用相对路径导入</strong>。因为这会耦合项目特定的目录结构,请使用包名替代。</blockquote><p>要将组件作为依赖项导入,必须使用模块链接。</p><p>Bit 为工作区中的每个组件创建一个模块,这些模块链接在 node_modules 目录中,并包含它的构建输出和自动生成的 package.json。</p><p>要为组件重新生成模块链接,请运行该bit link命令。</p><h3>将组件安装为NPM包</h3><p>install命令安装组件,以NPM包的形式使用。</p><h3>作为Vendor组件</h3><blockquote>Bit工作区获取组件并管理该组件,就好像它是自定义组件一样</blockquote><p>通过import命令安装组件,示例如下:</p><pre><code>$ bit import <component-id>
</code></pre><p>更新import的组件到最新版本</p><pre><code>$ bit import
</code></pre><p>将Vendor组件转为NPM包依赖</p><pre><code>$ bit eject <component-id>
</code></pre><h2>Scope</h2><blockquote><p>Scope是组件的虚拟存储。</p><p>Bit 使用Scope保存Bit组件的版本并根据需要访问它们。</p></blockquote><h3>Remote Scope</h3><blockquote>托管组件及其版本的Bit服务器。</blockquote><h4>特色</h4><p>在远程服务器上设置Scope以<strong>共享组件,</strong>如Bit.dev或自托管 Bit 服务器。</p><p>将组件存储在Remote Scope上,可以使它们在其他项目中重复使用。</p><ul><li>使用import命令从Remote Scope获取组件。</li><li>使用export命令将组件推送到Remote Scope。</li></ul><p><strong>注意:</strong>Remote Scope会缓存组件依赖,例如其他Scope的组件。这样做的好处是,即使依赖组件不可用,还能确保当前组件可执行。</p><h4>使用</h4><p>在Bit Server创建Remote Scope后,需要更改workspace.jsonc文件:</p><pre><code>{
"teambit.workspace/workspace": {
"defaultScope": "<bit-username>.<remote-scope-name>"
}
}
</code></pre><p>workspace.jsonc文件中的任何更改都需要重新启动本地开发服务器。</p><pre><code>$ bit start
</code></pre><h3>Workspace Scope</h3><blockquote>工作区组件的本地存储。</blockquote><h4>特色</h4><p>开发人员的工作区都在本地 Scope 中保存了组件及其历史记录的工作副本。这允许我们浏览历史记录、比较版本和检查组件的过去修订。</p><p>Workspace Scope也可能包含来自各异Remote Scope的组件。</p><h2>共享组件</h2><ol><li>为已修改的组件更新版本号</li></ol><pre><code>$ bit tag --all --message "first version"
</code></pre><ol start="2"><li>共享组件</li></ol><pre><code>$ bit export
</code></pre><p><strong>注意:</strong>当共享上传流程结束,.bitmap文件将更新以反映该新状态。</p><h2>安装组件</h2><h3>注册Scope源</h3><pre><code>$ npm config set '@YourUserName:registry' https://node.bit.dev
</code></pre><h3>安装依赖</h3><pre><code>$ npm install @orgName/componentScopeName.componentID
</code></pre><h2>Bit Component vs. NPM包</h2><blockquote>Bit 专注于基于组件的工作流,npm 包关注编译后的输出。</blockquote><ul><li>生成NPM包只是Bit Component构建流程的部分,Bit称之为版本工件。</li></ul><h2>Configuration</h2><blockquote>每个组件都必须配置一个环境,好让Bit 就“知道”如何构建、测试、lint 和document组件。</blockquote><p>teambit.workspace/variants提供一个统一的方式,可以为每个组件设置不同的配置项,而无需修改每个组件文件下的 package.json 。</p><pre><code>{
"teambit.workspace/variants": {
"design/theme": {
"defaultScope": "acme.theme",
},
"cart": {
"defaultScope": "acme.cart",
"teambit.react/react": {}
}
}
}
</code></pre><h3>查看配置</h3><ul><li>bit env - 打印一个简单的表格,其中包含工作区中的所有组件及其环境</li><li>bit show <component> - 打印组件的所有信息,包括环境</li><li>bit start- 通过浏览器可视化浏览<strong>组件树</strong>以查看组件的环境</li></ul><h2>移除组件</h2><h3>移除本地组建</h3><pre><code>$ bit remove <component-id>
</code></pre><p>产生的影响:</p><ul><li><p>一个未追踪的组件依赖 删除组件 —— 没有影响</p><ul><li>因为Bit还没有隔离未追踪的组件,不会检测其依赖</li></ul></li><li>一个已追踪的组件依赖 删除组件 —— 会警告,使用--force强制删除</li><li><p>引入的远程组件依赖 删除组件 —— 没有影响</p><ul><li>因为远程组件是已经隔离且不可更改的</li><li>本地引入远程组件且更改会创建另一个版本</li></ul></li></ul><h3>移除远程组件</h3><pre><code>$ bit remove <username.your-scope/ui/button> --remote
</code></pre><p>以一个例子描述产生的影响:</p><ul><li>button组件在远程uiScope中</li><li>card组件依赖button组件,也在uiScope中</li><li>login组件依赖button组件,在adminScope中</li></ul><p>删除button组件后的影响:</p><ul><li><p>因为card组件与button组件在同一个Scope中,因此删除button组件会有个警告。</p><ul><li>可追加---force强制删除</li><li>删除后,card组件缺少依赖,为保证其正常工作需要重构</li></ul></li><li><p>login组件没有影响</p><ul><li>Bit会在Scope中维护依赖</li></ul></li><li><p>其他项目依赖login组件时,安装会报错</p><ul><li>溯源button组件,缺失</li></ul></li></ul><h2>编译组件</h2><blockquote>大多数现代框架都需要一个编译或转译项目来将源代码转换为可以在多个浏览器或 Nodejs 中运行的可执行代码。</blockquote><p>而Bit 的编译器是一个环境服务。</p><p>编译器的选择(Babel、TypeScript 等)及其配置由其服务的各种环境决定。</p><p>编译器永远不会直接运行,而只能通过 Compiler 服务运行。</p><p>单个工作区可能会针对不同的组件运行不同的编译器,每个编译器都根据自己的环境。</p><pre><code>$ bit compile <component-id> # 编译特定组件
$ bit compile # 编译工作区全部组件
</code></pre><h2>组件依赖关系图</h2><p>Bit 的一个关键特性是能够根据组件的源代码自动创建依赖关系图。</p><p>Javascript 可以使用 require 或 import 声明依赖两种类型的依赖项:</p><ul><li>作为 node_modules 安装的软件包</li><li>项目内部的文件和目录,或在装饰器中引用(例如在 Angular 中)</li></ul><h3>node_modules依赖</h3><h4>Bit解析包(即node_modules)的流程:</h4><p><img src="/img/remote/1460000040984269" alt="image" title="image"></p><ul><li>可以通过bit show <component-id>来检查 Bit 为每个包解析的依赖项(Packages):</li></ul><pre><code>$ bit show hello/world
┌───────────────────┬─────────────────────────────────────────────────────────────────────┐
│ ID │ hello/world │
├───────────────────┼─────────────────────────────────────────────────────────────────────┤
│ Language │ javascript │
├───────────────────┼─────────────────────────────────────────────────────────────────────┤
│ Main File │ src/hello-world/index.js │
├───────────────────┼─────────────────────────────────────────────────────────────────────┤
│ Packages │ left-pad@^2.1.0 │
├───────────────────┼─────────────────────────────────────────────────────────────────────┤
│ Files │ src/hello-world/hello-world.js, src/hello-world/index.js │
└───────────────────┴─────────────────────────────────────────────────────────────────────┘
</code></pre><p>如果 Bit 无法解析所有包的依赖项,它会提示missing package dependencies。我们需要验证 package.json 中是否确实存在所有包。</p><h3>文件依赖</h3><blockquote><p>组件可以依赖于其他文件,例如import ./utils.js。</p><p>为了隔离这些依赖其它文件的组件,我们还需要跟踪该组件依赖的其它文件。这是因为如果我们想在另一个项目中使用这个组件,该组件必须要有它的依赖文件。</p></blockquote><p><strong>注意:</strong>Bit 使用静态代码分析,因此仅支持静态导入import,不支持require。</p><h4>Bit解析文件依赖的流程</h4><p><img src="/img/remote/1460000040984270" alt="image" title="image"></p><p>当 Bit 遇到需要跟踪的文件时,它会尝试检查该文件是否已经在另一个组件中进行了跟踪,在这种情况下,Bit 将使另一个组件成为该组件的依赖项。</p><p>如果文件未被跟踪,Bit 将untracked file dependencies在检查组件状态时发出警告。</p><h4>隔离问题</h4><p>要解决隔离问题,您可以:</p><ul><li>将未跟踪的文件依赖项添加到现有组件</li><li>将文件作为新组件进行跟踪</li></ul><p>采取以上何种方法基于文件的上下文。如果该文件被多个其他组件使用,则将其放入一个单独的组件中是有意义的。</p><p>但是,如果此文件仅仅是被跟踪文件的内部文件,则可以将其添加为组件的文件。</p><h5><strong>文件添加到现有组件</strong></h5><p>运行bit add指向要<strong>添加文件的组件</strong>的 Id:</p><pre><code>// 示例
$ bit add src/utils/noop.js --id hello/world
</code></pre><p>运行bit status ,检查是否成功:</p><pre><code>$ bit status
new components
> component/hello-world... ok
</code></pre><h5><strong>文件作为新组件进行跟踪</strong></h5><p>可以bit add添加新组件</p><pre><code>// 示例
$ bit add src/utils/noop.js --namespace utils
</code></pre><p>执行结果是一个新组件。</p><h2>私有化部署v15</h2><p><img src="/img/remote/1460000040984271" alt="image" title="image"></p><h3>硬件条件</h3><ul><li>Linux/Mac系统</li><li>内存4G+</li></ul><h3>前置条件</h3><ul><li>Docker</li><li>Git</li></ul><pre><code># 卸载旧版docker
$ yum remove docker docker-common docker-selinux docker-engine
# 安装docker依赖
$ yum install -y yum-utils device-mapper-persistent-data lvm2
# 设置docker源
$ yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
$ yum install docker-ce
# 启动docker
$ systemctl start docker
# 加入开机启动
$ systemctl enable docker
</code></pre><pre><code># 安装Git
$ yum install git
</code></pre><h3>部署流程</h3><pre><code>$ git clone https://github.com/teambit/bit.git
$ cd bit/scripts/docker-teambit-bit
$ docker build -f ./Dockerfile-bit -t bitcli/bit:latest .
$ docker build -f ./Dockerfile-bit-server -t bitcli/bit-server:latest .
$ docker run -dit bitcli/bit:latest /bin/bash # 运行
$ docker run -dit -p <port>:3000 bitcli/bit-server:latest
</code></pre><ul><li><p>Dockerfile-bit:</p><ul><li>安装 bvm 然后使用 bvm 安装 bit 的 docker 文件。</li><li>这个 docker 通常对在 CI 机器上运行像 tag 和 export 这样的Bit命令很有用</li></ul></li><li><p>Dockerfile-bit-server:</p><ul><li>一个基于Dockerfile-bit(使用 from)的docker 文件</li><li>该docker文件创建一个空白Scope,并在其上通过bit start初始化Bit服务器</li></ul></li><li><p>Dockerfile-symphony:</p><ul><li>仅供内部使用</li></ul></li></ul><h3>相关问题</h3><h4>Mac电脑ssh链接: Permission denied</h4><pre><code>$ sudo ssh root@<ip>
</code></pre><h4>ssh链接时报警告WARNING: REMOTE HOST IDENTIFICATION HAS CHANGE</h4><pre><code>$ sudo ssh-keygen -R <ip>
</code></pre><h3>bvm install安装不了Bit</h3><h4>临时更改terminal代理</h4><pre><code>$ export http_proxy=http://127.0.0.1:1087
$ export https_proxy=$http_proxy
</code></pre><p><strong>注意:</strong></p><ul><li>需要有VPN</li><li>保证浏览器可以访问外网</li><li><strong>开启VPN,即使全局,终端也是无法被代理</strong></li></ul><h4>永久修改代理</h4><pre><code># 修改~/.bashrc设置永久管理脚本
function proxy_on() {
export http_proxy=http://127.0.0.1:1087
export https_proxy=$http_proxy
echo -e "终端代理已开启。"
}
function proxy_off(){
unset http_proxy https_proxy
echo -e "终端代理已关闭。"
}
</code></pre><p><strong>注意:</strong>修改后,通过source ~/.bashrc立即生效。</p><p>通过proxy_on启动代理,proxy_off关闭代理。</p><h2>发布</h2><h3>注册远程Scope</h3><pre><code># 客户端
$ cd <my-project>
$ bit init
$ bit remote add http://<host>:<port>
</code></pre><h3>workspace.jsonc</h3><p>配置teambit.workspace/workspace</p><pre><code> "teambit.workspace/workspace": {
/**
* the name of the component workspace. used for development purposes.
**/
"name": "my-workspace-name",
/**
* set the icon to be shown on the Bit server.
**/
"icon": "https://static.bit.dev/bit-logo.svg",
/**
* default directory to place a component during `bit import` and `bit create`.
* the following placeholders are available:
* name - component name includes namespace, e.g. 'ui/button'.
* scopeId - full scope-id includes the owner, e.g. 'teambit.compilation'.
* scope - scope name only, e.g. 'compilation'.
* owner - owner name in bit.dev, e.g. 'teambit'.
**/
"defaultDirectory": "{scope}/{name}",
/**
* default scope for all components in workspace.
**/
"defaultScope": "remote-scope"
},
</code></pre><h3>打Tag</h3><p>监听文件变化,才能标识。</p><p>若文件无变化,无法进行标识。</p><pre><code>$ bit tag --all --message "first version"
</code></pre><blockquote><p><strong>注意:</strong> 独立的组件通过独立的“远程范围”、远程组件托管形成依赖关系网络。</p><p>这种依赖关系网络使更改能够让一个组件的改变传播到它的所有依赖组件。也就是说,一个组件的更改以级联的方式触发了组件的CI及依赖它的组件的CI。</p></blockquote><p><strong>举例:</strong></p><p>组件B依赖组件A,二者初始版本皆为0.0.1。</p><p>若组件A由0.0.1进行更改,通过bit tag --all会A0.0.1 —> 0.0.2,B0.0.1 —> 0.0.2。</p><p>若继续更改组件B,通过bit tag --all,B0.0.2 —> 0.0.3,A的版本不变。</p><h3>Bit部署</h3><p>部署的前提是有新的标识,否则,无法部署。</p><pre><code>$ bit export
</code></pre><p><img src="/img/remote/1460000040984272" alt="image" title="image"></p><h2>扩展Bit</h2><p>我们通过创建Aspect和接入Bit的API来扩展Bit。</p><h3>扩展Workspace UI</h3><p>以新增Tab为例:</p><h4>初始化Bit环境</h4><pre><code>$ bit init
</code></pre><p>会自动新建.bit/、.bitmap、workspace.jsonc文件(夹)。</p><h4>修改DefaultScope</h4><pre><code>{
...
"teambit.workspace/workspace": {
/**
* the name of the component workspace. used for development purposes.
**/
"name": "my-workspace-name",
/**
* set the icon to be shown on the Bit server.
**/
"icon": "https://static.bit.dev/bit-logo.svg",
/**
* default directory to place a component during `bit import` and `bit create`.
* the following placeholders are available:
* name - component name includes namespace, e.g. 'ui/button'.
* scopeId - full scope-id includes the owner, e.g. 'teambit.compilation'.
* scope - scope name only, e.g. 'compilation'.
* owner - owner name in bit.dev, e.g. 'teambit'.
**/
"defaultDirectory": "{scope}/{name}",
/**
* default scope for all components in workspace.
**/
"defaultScope": "me"
},
...
}
</code></pre><h4>新建Aspect</h4><pre><code>$ bit create aspect aspects/hello-world
</code></pre><p>生成目录结构:</p><pre><code>.
└──me
└── aspects
└── hello-world
├── hello-world.aspect.ts
├── hello-world.main.runtime.ts
└── index.ts
</code></pre><p>其中,hello-world.main.runtime.ts代码如下:</p><pre><code>// hello-world.main.runtime.ts
import { MainRuntime } from '@teambit/cli';
import { HelloWorldAspect } from './hello-world.aspect';
export class HelloWorldMain {
static slots = [];
static dependencies = [];
static runtime = MainRuntime;
static async provider() {
return new HelloWorldMain();
}
}
HelloWorldAspect.addRuntime(HelloWorldMain);
</code></pre><p><strong>注意:</strong>hello-world.main.runtime是负责扩展workspace CLI和 workspace Server的。</p><p>为了在组件详情页创建一个新的菜单,我们需要参考hello-world.main.runtime.ts文件新建hello-world.ui.runtime.<strong>tsx</strong>文件:</p><pre><code>// hello-world.ui.runtime.tsx
import React, { useContext } from 'react';
import { UIRuntime } from '@teambit/ui';
import { ComponentUI, ComponentAspect } from '@teambit/component';
import { HelloWorldAspect } from './hello-world.aspect';
export class HelloWorldUI extends React.Component<any> {
static slots = [];
static dependencies = [ComponentAspect];
static runtime = UIRuntime;
static async provider([component]: [ComponentUI]) {
return new HelloWorldUI();
}
}
HelloWorldAspect.addRuntime(HelloWorldUI);
</code></pre><p><strong>注意:</strong>这里引入了ComponentAspect,它是Bit核心Aspect,负责组建页面所有的组件和操作。将ComponentAspect作为依赖,我们能在provider中获取到它并使用它提供的API。</p><pre><code>// 更新hello-world.ui.runtime.tsx
// 注册registerNavigation导航
import React, { useContext } from 'react';
import { UIRuntime } from '@teambit/ui';
import { ComponentUI, ComponentAspect } from '@teambit/component';
import { HelloWorldAspect } from './hello-world.aspect';
export class HelloWorldUI extends React.Component<any> {
static slots = [];
static dependencies = [ComponentAspect];
static runtime = UIRuntime;
static async provider([component]: [ComponentUI]) {
component.registerNavigation({
href: '~hello',
children: 'Hello'
});
return new HelloWorldUI();
}
}
HelloWorldAspect.addRuntime(HelloWorldUI);
</code></pre><p>这里,我们通过ComponentAspect依赖提供的registerNavigation注册了导航,手动切换导航会渲染Hello。</p><pre><code>// 更新hello-world.ui.runtime.tsx
// 注册registerRoute路由
import React, { useContext } from 'react';
import { UIRuntime } from '@teambit/ui';
import { ComponentUI, ComponentAspect } from '@teambit/component';
import { HelloWorldAspect } from './hello-world.aspect';
export class HelloWorldUI extends React.Component<any> {
static slots = [];
static dependencies = [ComponentAspect];
static runtime = UIRuntime;
static async provider([component]: [ComponentUI]) {
component.registerRoute({
children: () => <div>hello world</div>,
path: '~hello'
});
component.registerNavigation({
href: '~hello',
children: 'Hello'
});
return new HelloWorldUI();
}
}
HelloWorldAspect.addRuntime(HelloWorldUI);
</code></pre><p>这里,我们通过ComponentAspect依赖提供的registerRoute注册了路由,该路由会承接上述注册的导航,简单的渲染了hello world。</p><h4>注册自定义Aspect</h4><p>在执行Aspect之前,要为其配置解析环境,该环境会将Aspect最终转译为浏览器、nodejs可识别的代码。</p><pre><code>{
...
"teambit.workspace/variants": {
"{me/aspects/*}": {
"teambit.harmony/aspect":{}
}
},
"me/aspects/hello-world": {}
...
}
</code></pre><h4>安装依赖</h4><pre><code>$ bit install
</code></pre><p>不安装依赖,bit start也是正常运行的,只是看不到增加的UI。</p><h4>效果展示</h4><p>运行bit start查看效果~</p><p><img src="/img/remote/1460000040984273" alt="image" title="image"></p><p><strong>注意:</strong>若要更新展示,则要删除.bit/、node_modules、public/,再次执行bit install和bit start。</p><h4>查看Aspect信息</h4><p>中途可通过bit show me/aspects/hello-world查看信息</p><h3>通过内置模板创建扩展</h3><ul><li>通过bit templates查看内置模板</li><li>通过`bit create <template> <custom-name> [--scope scope-name]</li><li>通过bit install安装模板相关依赖</li><li>通过bit status查看自定义扩展状态</li><li>若有依赖缺失报错,将缺失依赖添加到:</li></ul><pre><code> "teambit.dependencies/dependency-resolver": {
/**
* choose the package manager for Bit to use. you can choose between 'yarn', 'pnpm'
*/
"packageManager": "teambit.dependencies/pnpm",
"policy": {
"dependencies": {},
"peerDependencies": {
"react": "~17.0.2",
"@testing-library/react": "~12.1.2"
}
}
},
</code></pre><p>bit install补充安装依赖。</p><ul><li>通过bit start --dev测试。</li></ul>
自定义Vue-cli项目模板
https://segmentfault.com/a/1190000040866528
2021-10-26T15:38:43+08:00
2021-10-26T15:38:43+08:00
米花儿团儿
https://segmentfault.com/u/mihuartuanr
1
<h2>模板结构</h2><p>主要包括四个部分:</p><ul><li>preset.json</li><li>prompts.js</li><li>generator/index.js</li><li>template/</li></ul><h3>preset.json</h3><p>preset.json 中是一个包含创建新项目所需预定义选项和插件的 JSON 对象,让用户无需在命令提示中选择它们,简称预设;</p><h3>prompts.js</h3><p>交互式的告知vue create所需,是根据用户使用需求自定义设置的信息。</p><p>定义格式参考Inquirer 问题格式的数组(Inquirer官方文档)</p><h3>generator.js</h3><p>generator.js 导出一个函数,这个函数接收三个参数</p><ol><li><p>一个 GeneratorAPI 实例</p><ol><li>提供一系列的API控制最终输出的目录结构及内容</li><li>自定义模版必然用到 GeneratorAPI 的 render() 方法</li></ol></li><li><p>用户自定义的设置选项</p><ol><li>即:用户对 prompts.js 中问题所提供的答案</li></ol></li><li>整个 preset 预设信息</li></ol><h2>简单的自定义模板示例</h2><h3>创建项目</h3><p>手动创建目录结构:</p><pre><code class="json">|- vue-template
|- generator
|- index.js
|- preset.json
|- prompts.js</code></pre><h3>获取preset.json模板</h3><p>先用 vue create 去创建一个项目,然后把你的预设信息保存下来,到指定目录下查找预设信息:</p><pre><code># Unix
~/.vuerc
# windows
C://用户/<username>/.vuerc</code></pre><pre><code>{
"useTaobaoRegistry": false,
"latestVersion": "4.5.14",
"lastChecked": 1634820758861,
"packageManager": "npm",
"presets": {
"v2": {
"useConfigFiles": true,
"plugins": {
"@vue/cli-plugin-babel": {},
"@vue/cli-plugin-typescript": {
"classComponent": false,
"useTsWithBabel": true
},
"@vue/cli-plugin-router": {
"historyMode": false
},
"@vue/cli-plugin-vuex": {},
"@vue/cli-plugin-eslint": {
"config": "prettier",
"lintOn": [
"save",
"commit"
]
}
},
"vueVersion": "2",
"cssPreprocessor": "dart-sass"
}
}
}</code></pre><p>其中,presets 保存的就是预设信息,v2 是保存预设起的别名,我们的preset.json 需要的就是 v2 的值,所以preset.json 中的内容就是这样</p><pre><code>{
"useConfigFiles": true,
"plugins": {
"@vue/cli-plugin-babel": {},
"@vue/cli-plugin-typescript": {
"classComponent": false,
"useTsWithBabel": true
},
"@vue/cli-plugin-router": {
"historyMode": false
},
"@vue/cli-plugin-vuex": {},
"@vue/cli-plugin-eslint": {
"config": "prettier",
"lintOn": [
"save",
"commit"
]
}
},
"vueVersion": "2",
"cssPreprocessor": "dart-sass"
}</code></pre><h3>创建问答prompts.js</h3><pre><code class="js">module.exports = []</code></pre><p>prompts.js 我们可以不提供问题,导出一个空数组就行;</p><h3>创建项目模板生成器generator</h3><pre><code># generator/index.js
module.exports = (api, options, rootOptions) => {
api.extendPackage({
# 扩展pkg#scripts
scripts: {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
# 扩展pkg#dependencies
dependencies: {
"vue": "^2.6.11"
},
# 扩展pkg#devDependencies
devDependencies: {
"@babel/core": "^7.11.4",
"@babel/preset-env": "^7.11.0",
"@vue/cli-service": "~4.5.0",
"sass": "^1.26.10",
"sass-loader": "^8.0.2"
}
});
# 复制template模版
api.render('../template');
};</code></pre><h4>私有依赖</h4><p>若存储配置.vuerc,且其中useTaobaoRegistry = true,在通过模板创建项目时,会报私有依赖无法找到,这需要我们在创建项目前检查全局配置项。</p><pre><code> var vuerc = shell.exec('vue config', { silent:true }).stdout
var deleteConfigKey = 'useTaobaoRegistry'
if (new RegExp(deleteConfigKey).test(vuerc)) {
shell.exec(`vue config --set ${deleteConfigKey} false`, { silent:true })
}
child.execSync(
`vue create --preset multi-act --clone ${projectName}`,
{
stdio: 'inherit'
}
);
fs.ensureDirSync(`${projectName}/${Configure.BaseUri}`)</code></pre><h3>创建模板</h3><p>最后只需将项目模版复制到 template 中,然后删除 package.json 文件。<br>对于以 . 开头的文件,改成 _ ,例如 .eslintrc.js ==》_eslintrc.js。</p><p>因为以. 开头的文件,在Git上传、拉取时会被忽略。</p><h4>默认模板结构</h4><p>如果项目中多了些Vue-Cli默认模版的文件,可以使用下方方法删除默认模版</p><pre><code> // 存储vue-cli3 默认目录
const defaultDirs = []
api.render(files => {
Object.keys(files)
.filter(path => path.startsWith('src/') || path.startsWith('public/'))
.forEach(path => {
defaultDirs.push(path)
delete files[path]
})
})
api.render('../template')
// 删除 vue-cli3 默认目录
api.postProcessFiles(files => {
defaultDirs.forEach(path => delete files[path])
})</code></pre><h2>调试</h2><p>使用<code> vue create --preset <relativePath/vue-template> <project_name> </code>去创建项目。</p><h2>部署</h2><p>可以选择将模板部署到github、gitlab上。</p><pre><code># GitHub
$ vue create --preset <username>/<repo> <vue_project_name>
# GitLab 私有服务器
$ vue create --preset gitlab:<my-gitlab-server.com>:<group>/<project_name> --clone <vue_project_name>
# 通用
$ vue create --preset direct:<url> --clone <vue_project_name></code></pre><p>具体的模板参数可以参考download-git-repo</p><h2>参考项目:</h2><ul><li><a href="https://link.segmentfault.com/?enc=SCXjNo%2BrN9e2MzltphrRTA%3D%3D.Q0mYg4idmP%2B6Vzqeq09GqED29j4w2k1a7OP%2BjdpOhtBku8K19SfG1SLiOxB%2Fl3Gb" rel="nofollow">https://github.com/cklwblove/...</a></li><li><a href="https://link.segmentfault.com/?enc=pJd0Dv7GvcoYCX1buOE1Jw%3D%3D.3gVUjG8PIPKAvy0TR8QETamWGZlfp2dzhDSgIcFWyjM%2FnMzQoYSiO7UKXzzvE7QMhf0zqDl%2Bh1Ld2PxVdHazNg%3D%3D" rel="nofollow">https://github.com/Kocal/vue-...</a></li><li><a href="https://link.segmentfault.com/?enc=A55RZfxD9hnUG8XGH6EqCg%3D%3D.UojaGiWFueGWZYBJdOuo7G%2FvaOKVYQdPnp4aBTtHUhofZvyok%2FyeUlkl1qy225K9" rel="nofollow">https://github.com/cklwblove/...</a></li><li><a href="https://link.segmentfault.com/?enc=ETQoncuwLSUcWKoOoI%2BDKw%3D%3D.VAESUsyOjuqJPrAfD6sQrBAFXUdGLXB3BSqWU8eutfWzyXoSDBhtya7%2BlClpZ%2B5TdvPpgM3%2BebIVB5e9xksowN75yDGbyjHBgQxUJGfwmMo%3D" rel="nofollow">https://git.n.xiaomi.com/xued...</a></li></ul><h2>参考文档:</h2><ul><li><a href="https://link.segmentfault.com/?enc=xbf60Xt3Fg%2BX87uN%2Bq6Dxg%3D%3D.0yXA0E6v0t5njOMgBow42REtPld4cT2YRXUmLO3V1S1lA9zR4TrhTGAQt094bdtTfpyTGCXUte8eLzHOlazgro9JLqTKBjp212z0qog01LdmW0L4P2ZRkt6uoudnXAfNobEMcRGFuTbHwEqNYKp0soj2zXNi1BShgX1z%2BtxOwqmCvC%2FfqkuK7fAlsFPZGB7f9i5QrOClxe0I96Ulkyo4w6oCVPkrj9dW5JAlDEq%2BdaPoEmE0%2FF2f7Ec9Sureyewyitw7ixclbK7FFJD15yPCYg%3D%3D" rel="nofollow">https://notes.jindll.com/web/...</a></li><li><a href="https://link.segmentfault.com/?enc=HQaLn%2Fz892dDrU5JdeI7ug%3D%3D.jSPl6cBi%2B2KtHRX9v2VCkV9tBDUMP9D%2FNq8Jt7hBHsO1nUA8s0L3JPeroDoSuTi7x9E26pBS7X%2FokefV%2BjHM3CsnXelSDitxiJpidnvyIM5Wt1LO%2BetDXTON%2Bg0FLAmz%2FY%2Fm8GRSLu%2B7hczUSyxd5A%3D%3D" rel="nofollow">https://cli.vuejs.org/zh/dev-...</a></li><li><a href="https://link.segmentfault.com/?enc=5j94pAmBltx8LKbMO6eHCA%3D%3D.D4GRk%2BGv9WrOS2eQPnMNNtpt9Io8Os2nqNjAQ68Zpd5u2IBz4wTeE74UHUwBg6%2BQv%2Bw93Qy5e4RSfWIEHBjGlIVCSFxnC8EJyK0OY3jwNKUCuyUmZzWvOU3WpaQdvOT1WAmgly8EdcaTfx0RgvhJ%2BtnCgp0nk9azCsZjfEIJQhG7w1ykQnEVeKxESSOq4omT" rel="nofollow">https://blog.beard.ink/JavaSc...</a></li><li><a href="https://link.segmentfault.com/?enc=BaIoWf365cn6ZNybGanppQ%3D%3D.jK0ISJHVYrhAhCvWfUNKfnXj2v7a9DOHeR4eaM1n8TyQqE5VEGmDNuUcXJczAUYHIUXnb8%2FtnE9Hi%2B%2FKCbzHQg%3D%3D" rel="nofollow">https://www.open-open.com/lib...</a></li><li><a href="https://link.segmentfault.com/?enc=5TWdk5D9DFGIEwqyi2goDA%3D%3D.gb3V%2FvsUE2Ml3s2bbCm6%2FT0ZFGNTv41d2xDmcu1Ysifv%2B25ct1kUSL9Vn50qd5j8" rel="nofollow">https://xuezenghui.com/posts/...</a></li><li><a href="https://segmentfault.com/a/1190000038925849">https://segmentfault.com/a/11...</a></li><li><a href="https://link.segmentfault.com/?enc=bKJHJN4MFEbfM3%2BRJ2j7nA%3D%3D.vlWyfPRASfMOeWCn5cFei8lObxaFRBZ%2FXhSehDNcQbl4XdxGbsDTBPJ0xQN1ZowKh6NXCspLVwc4B2w3TbWYmg%3D%3D" rel="nofollow">http://axuebin.com/articles/f...</a></li></ul>
自定义NPM命令行
https://segmentfault.com/a/1190000040817687
2021-10-15T16:24:08+08:00
2021-10-15T16:24:08+08:00
米花儿团儿
https://segmentfault.com/u/mihuartuanr
0
<h2>入口文件</h2><ul><li><p>自定义依赖模块:</p><ul><li><p>模块是在 <code>package.json</code> 里通过 <strong><code>main</code></strong> 字段定义这个包对外暴露的入口;</p><ul><li>模块起源于<code>node</code>,语法默认支持<code>commonjs</code>规范</li><li>模块若使用<code>ES Module</code>语法书写,通过 <strong><code>module</code></strong> 字段定义入口(需要打包工具配合使用)</li></ul></li></ul></li><li><p>自定义命令行:</p><ul><li>如果是提供命令行工具,则需要通过 <strong><code>pkg#bin</code></strong> 字段来定义暴露的命令名称与实际执行的文件</li></ul></li></ul><p>这篇文章讲述自定义命令行的声明,NPM依赖包请查看<a href="https://segmentfault.com/a/1190000040125015">链接:自定义NPM包</a>。</p><h2>开发环境</h2><ul><li>[ ] 自动日志</li><li>[ ] 版本更新</li></ul><p>~使用的是husky6+,配置与旧版本不同,跟随文章操作时,请注意版本~</p><ol><li><p><a href="https://link.segmentfault.com/?enc=Mhg3SbZhwAt6JRlPDYKO1Q%3D%3D.s7l3DMy9Oonp5Kx3oR8APTTrdPblH%2BcmNJy8XGR87lxiRiDsXgZcfilAEhXwHSgL" rel="nofollow"><code>husky</code></a></p><ul><li>安装</li></ul><pre><code> npm install husky --save-dev
npx husky install</code></pre><ul><li><p>配置<code>run-script</code>:安装依赖后自动启动<code>Git hooks</code></p><pre><code>"prepare": "husky install"</code></pre><ul><li><p>追加测试钩子</p><pre><code> # Unix系统可用
npx husky add .husky/pre-commit "npm run test"
# Windows通过以下命令创建文件(引号在windows命令行中不是规范的语法)
npx husky add .husky/pre-commit
# Windows环境下找到新建的文件,编辑文件,指定命令
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npm run test</code></pre></li></ul></li></ul></li><li><p><code>commitlint</code></p><ul><li><p><code>commitlint</code>提交信息校验工具</p><ul><li><p>需要和校验规范配合使用,官网默认规范<code>@commitlint/config-conventional</code> —— 校验规范可自定义。</p><pre><code> npm i -D commitlint @commitlint/config-conventional</code></pre></li></ul></li><li><p><code>commitlint</code>绑定<code>@commitlint/config-conventional</code></p><pre><code># Unix
echo "module.exports = {extends: ['@commitlint/config-conventional']}" > commitlint.config.js
# Windows
echo module.exports = {extends: ['@commitlint/config-conventional']} > commitlint.config.js</code></pre></li><li><p>配置<code>Git hook</code>:在提交<code>commit msg</code>进行参数校验 —— <strong>写在<code>run-script</code>中无效</strong></p><pre><code> # Unix系统可用
npx husky add .husky/commit-msg "npx --no-install commitlint --edit $1"
# Windows通过以下命令创建文件(引号在windows环境中不是规范的语法)
npx husky add .husky/commit-msg
# Windows环境下找到新建的文件,编辑文件,指定命令
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npx --no-install commitlint --edit $1</code></pre></li></ul></li><li><p><code>standard-version</code></p><ul><li>安装依赖</li></ul><pre><code> npm i --save-dev standard-version</code></pre><ul><li><p>配置<code>run-script</code>:发布前自动升级版本号 + 生成日志 + 提交Git</p><pre><code>"prepublishOnly": "standard-version && git push --follow-tags origin master"</code></pre><p><strong>Note:</strong> 这里不要使用<code>prepublish</code>钩子,该钩子在<a href="https://link.segmentfault.com/?enc=ZvI42ito5UlodpfNRjO5%2Bg%3D%3D.t6P%2B6eCFesGy%2BNQOQyA%2FblWF5Mf0i0rLbo11Uyx5PDruHo4Fs8mJSq0sFZTKWbsMlWWjZsxzFij6A0JrjGcBnbo4sbQi8lydNRV%2BJhsnbcY%3D" rel="nofollow"><code>npm i</code>时运行</a>,而不是<code>npm publish</code>时运行。</p></li></ul></li></ol><h2>调试</h2><ol><li><p>进入本地<code>NPM</code>包</p><ul><li><code>npm link</code>创建软链接到全局<code>node</code>环境中</li></ul></li><li><p>进入需引入依赖包的项目A中</p><ul><li><code>npm link <packageName></code>建立软链接依赖</li></ul></li><li><p>在命令行切换到项目A目录结构下</p><pre><code># 调用
npx <packageName> -h</code></pre></li></ol><h2>开发</h2><p><code>NPM</code>包是<code>commonJS</code>语法,使用<code>require()</code>,而非<code>import...from...</code>引入依赖。</p><p><strong>而,若<code>NPM</code>包语法使用<code>ES6+</code>语法书写,必须使用<code>ES Module</code>的<code>import</code>、<code>export default</code>导出方式,不要混用</strong></p><p>推荐几个常用的命令行辅助工具:</p><ul><li><p><a href="https://link.segmentfault.com/?enc=6dJhZvTlXB%2BWVq0o4cmSyQ%3D%3D.cmhjgBc5w8HgJwfLvu%2BzqTYtinZd2rnQckre2hF%2FG%2FH2l4CsSSqAEjQF6Z%2FtqSa%2B" rel="nofollow"><code>shelljs</code></a></p><ul><li>执行脚本程序</li><li>shelljs无法执行有交互的命令,会丧失交互性,交互性命令仍建议<a href="https://link.segmentfault.com/?enc=mFsNWpNdLtIRzqUEpYol8Q%3D%3D.mrzF4Ah1eTr2Q8DRlz39QYTDW4uquqIS7sWUEZa2g97dULP5UZMxeZewWmSbxRhN5GCy6Hkpt6KiKhaIcNGDQ34KAEbiZ0NZE8sjeY6XJtNSRJoiXw79Ur%2BxeV1nUdIi" rel="nofollow">child_process</a></li><li>shelljs执行命令的输出丧失色彩可视化,可以通过<a href="https://link.segmentfault.com/?enc=B5lnlb4jq8m3w3RRRVdkRA%3D%3D.aqqAu2iURKBnqw4ucQvnoXrQlFg8ORguIM3yiOt0GrYdqHYYEDXdDHS190%2FjAMQ6e%2BWw%2F97UKsJbBUOeLyF%2FJJX66NrLqHRNTWtc9qcVbBVsZQANdks701x2E6KCNANS2JH8vj0b%2FKa79NTPuYxulQ%3D%3D" rel="nofollow">相关方案解决</a></li></ul></li><li><p><a href="https://link.segmentfault.com/?enc=1wktzR9jQ8g2Cdj45vHonQ%3D%3D.Kfa71BStfQF8S%2Btn501GK2oXHHzbSuQih67j%2FAoprAEEyyRrpbxW5dYdVZGQZh1G" rel="nofollow"><code>inquirer</code></a></p><ul><li>命令行用户交互界面</li></ul></li><li><p><a href="https://link.segmentfault.com/?enc=Qge89FR4Re1VFTaUkm5h4w%3D%3D.sE3d79j7ezo8XD%2BvBqqFVCK1%2FKElSp7XizBUW8kZ4A8ha%2FArF2eOdXIQK1CmvDUF" rel="nofollow"><code>chalk</code></a></p><ul><li>命令行日志样式</li><li>可嵌套、可链式</li></ul></li><li><p><a href="https://link.segmentfault.com/?enc=pZjUgG5KUUtSvXYCFkFcJA%3D%3D.dseEq7Al3DAQm4vH7qoWQxB090Sfy7RWNatKkhydCehWFx3%2FqBlyncpii5vmnvMF" rel="nofollow"><code>commander</code></a></p><ul><li>自定义命令行参数</li></ul></li></ul><h3>组织结构</h3><pre><code>|- .husky
|- bin
|- actv2-use.js // 单文件命令
|- actv2.js // 入口文件,仅做中转作用
|- lib
|- actv2.js // 主文件
|- .gitignore
|- .npmrc
|- package.json
|- README.md</code></pre><h3><code>package.json</code>配置</h3><pre><code>{
"name": "actv2",
"version": "1.0.0",
"description": "",
"bin": "bin/actv2.js",
"main": "lib/actv2.js",
"scripts": {
"prepare": "npx husky install",
"prepublishOnly": "standard-version && git push --follow-tags origin master",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@commitlint/config-conventional": "^13.2.0",
"commitlint": "^13.2.1",
"husky": "^7.0.2",
"standard-version": "^9.3.1"
},
"dependencies": {
"chalk": "^4.1.2",
"commander": "^8.2.0",
"fs-extra": "^10.0.0",
"inquirer": "^8.2.0",
"shelljs": "^0.8.4"
}
}</code></pre><h4>入口文件</h4><p>入口文件示例:</p><pre><code>#!/usr/bin/env node
var { program } = require('commander')
program
.command('add')
.argument('<module>', '新增项目模块')
.option('-y, --yes', '将该模块设置为部署模块')
.action((moduleName, options) => {
require('..')().add(moduleName, options)
})
program
.command('use <module>', '指定部署模块')
.alias('u')
program
.option('-V, --version', '查看版本')
.helpOption('-h, --help', '查看使用帮助');
program.parse(process.argv)
var opts = program.opts()
if (opts.version) {
process.stdout.write(
'actv2 ' + require('../package.json').version + '\n')
} else {
process.stdout.write(
'actv2\n' +
'\n' +
'Options:\n' +
' --version Show version number\n' +
' --help Show help\n' +
'\n' +
'Usage:\n' +
' actv2 --help\n'
)
}</code></pre><p>其中,<code>#!/usr/bin/env node</code>是必填行,在安装命令行后,依据该行指定<code>node</code>环境执行该命令行。</p><pre><code>#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*) basedir=`cygpath -w "$basedir"`;;
esac
if [ -x "$basedir/node" ]; then
"$basedir/node" "$basedir/../actv2/bin/actv2.js" "$@"
ret=$?
else
node "$basedir/../actv2/bin/actv2.js" "$@"
ret=$?
fi
exit $ret</code></pre><p>vs. 缺失<code>#!/usr/bin/env node</code></p><pre><code>#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*) basedir=`cygpath -w "$basedir"`;;
esac
"$basedir/../actv2/lib/run.js" "$@"
exit $?</code></pre><h4>参数解析</h4><p>借助<code>commander</code>第三方工具包</p><ol><li><p>声明</p><pre><code> program.option('') // 可选参数
program.requiredOption() // 必填参数
program.requiredOption('-c, --cheese <type>', 'pizza must have cheese'); // <type>值必填,[type]值可选</code></pre></li><li><p>解析</p><pre><code> program.parse(process.argv)</code></pre></li><li><p>取值</p><pre><code> program.opts().name
program.getOptionValue(<name>)</code></pre></li></ol><h4>子命令</h4><p>借助<code>commander</code>第三方工具包</p><h5>声明方式</h5><ul><li><p>绑定<code>action</code>的<code>command</code>声明</p><ul><li><p><code>command</code>声明只要<strong>一个参数</strong></p><pre><code>// 示例
program
.command('clone <source> [destination]') // 首行,仅有一个参数
.description('clone a repository into a newly created directory')
.option('-y, --yes', 'options.yes')
.action((source, destination, options, command) => {
console.log('clone command called');
});</code></pre></li></ul></li><li><p>单独的可执行文件</p><ul><li><p><code>command</code>声明添加描述参数,即表明是可执行文件的声明方式</p><pre><code># 输出以下代码的文件为入口文件
program
.version('0.1.0')
.command('install [name]', 'install one or more packages') // 在入口文件所在的目录下,检索`<入口文件名>-install`文件
.command('search [query]', 'search with optional query').alise('s') // 在入口文件所在的目录下,检索`<入口文件名>-search`文件
.command('update', 'update installed packages', { executableFile: 'myUpdateSubCommand' })
.command('list', 'list packages installed', { isDefault: true });</code></pre><blockquote>独立执行文件完整示例:</blockquote><pre><code>// 声明
program
.command('use <module>', 'assign module')
.alias('u')</code></pre><p><code>use</code>命令定义</p><pre><code>#!/usr/bin/env node
var chalk = require('chalk');
var { program } = require('commander');
var Message = require('../config/messages')
var createdFile = require('../utils/createFile')
program.option('-v, --version [version]', '指定版本号', '0.0.3') // npx <commander name> use <moduleName> -v 0.0.1
program.parse(process.argv)
var moduleName = program.args // 获取<moduleName...>列表
var { version } = program.opts()
if (moduleName.length !== 1) {
process.stdout.write(chalk.redBright(Message.deployOnly) + '\n')
process.exitCode = 0
} else {
//
}</code></pre></li></ul></li></ul><h4>执行</h4><pre><code>> npx actv2 -h
Usage: actv2 [options] [command]
Options:
-V, --version 查看版本
-h, --help 查看使用帮助
Commands:
add [options] <module>
use|u <module> 指定部署模块
help [command] display help for command</code></pre><pre><code>> npx actv2 add -h
Usage: actv2 add [options] <module>
Arguments:
module 新增项目模块
Options:
-y, --yes 将该模块设置为部署模块
-h, --help display help for command</code></pre>
Lighthouse使用说明
https://segmentfault.com/a/1190000040401027
2021-07-25T22:24:27+08:00
2021-07-25T22:24:27+08:00
米花儿团儿
https://segmentfault.com/u/mihuartuanr
1
<h2>声明</h2><p>本来想实时做一个示例讲述<code>Lighthouse</code>的用法,然后,本地安装了<code>lighthousev8.1.0</code>,悲剧了,使用的<code>v7.5.0</code>的配置不兼容。</p><p>遂,整理一下自己使用<code>v7.5.0</code>的输出,最后补充一点点<code>v8.1.0</code>的使用,真的只有一点点...因为没执行下去。</p><h2>Lighthouse使用说明</h2><blockquote>这里不是具体的场景,只是介绍Lighthouse的用法,相信,您会找到自己的应用场景的。</blockquote><h2>前端性能监控模型</h2><p>Lighthouse主要是用于前端性能监控,两种常见模型:合成监控、真实用户监控。</p><h3>合成监控(Synthetic Monitoring,SYN)</h3><blockquote>合成监控,就是在一个<strong>模拟场景</strong>里,去提交一个需要做性能检测的页面,通过一系列的工具、规则去运行你的页面,提取一些性能指标,得出一个性能/审计报告</blockquote><h3>真实用户监控(Real User Monitoring,RUM)</h3><blockquote>真实用户监控,就是<strong>用户在我们的页面上访问</strong>,访问之后就会产生各种各样的性能数据,我们在用户离开页面的时候,把这些性能数据上传到我们的日志服务器上,进行数据的提取清洗加工,最后在我们的监控平台上进行展示的一个过程</blockquote><table><thead><tr><th align="left">对比项</th><th align="left">合成监控SYN</th><th align="left">真实用户监控RUM</th></tr></thead><tbody><tr><td align="left">实现难度及成本</td><td align="left">较低</td><td align="left">较高</td></tr><tr><td align="left">采集数据丰富度</td><td align="left">丰富</td><td align="left">基础</td></tr><tr><td align="left">数据样本量</td><td align="left">较小</td><td align="left">大(视业务体量)</td></tr><tr><td align="left">适合场景</td><td align="left">支持团队自有业务,对性能做定性分析,或配合CI做小数据量的监控分析</td><td align="left">作为中台产品支持前台业务,对性能做定量分析,结合业务数据进行深度挖掘</td></tr></tbody></table><h2>Lighthouse是什么</h2><blockquote>Lighthouse 是一个开源的自动化工具,用于分析和改善 Web 应用的质量。</blockquote><h2>Lighthouse使用环境</h2><ul><li>Chrome开发者工具</li><li>安装扩展程序</li><li>Node CLI</li><li>Node module</li></ul><h2>Lighthouse组成部分</h2><ul><li><p>驱动Driver</p><blockquote>通过Chrome Debugging Protocol和Chrome进行交互。</blockquote></li><li><p>收集器Gatherer</p><blockquote>决定在页面加载过程中采集哪些信息,将采集的信息输出为Artifact</blockquote></li><li><p>审查器Audit</p><blockquote>将 Artifact 作为输入,审查器会对其运行 1 个测试,然后分配通过/失败/得分的结果。</blockquote></li><li><p>报告Reporter</p><blockquote>将审查的结果分组到面向用户的报告中(如最佳实践)。对该部分加权求和然后得出评分。</blockquote></li></ul><h2>Lighthouse工作流程</h2><blockquote>简单来说Lighthouse的工作流程就是:建立连接 -> 收集日志 -> 分析 -> 生成报告。</blockquote><h2><code>Lighthousev7.5.0</code> NPM包使用示例</h2><h3>初始化开发环境</h3><pre><code>mkdir lh && cd $_ // 在命令行中创建项目目录、进入目录
npm init -y // 初始化Node环境
npm i -S puppeteer // 提供浏览器环境,其它NPM包也可以
npm i -S lighthouse // 安装lighthouse
// ^ lighthouse@7.5.0</code></pre><h3>初始化运行</h3><pre><code>const puppeteer = require('puppeteer');
const lighthouse = require('lighthouse');
const { URL } = require('url');
(async() => {
const url = 'https://huaban.com/discovery/';
const browser = await puppeteer.launch({
headless: false, // 调试时设为 false,可显式的运行Chrome
defaultViewport: null,
});
const lhr = await lighthouse(
url,
{
port: (new URL(browser.wsEndpoint())).port,
output: 'json',
logLevel: 'info',
}
);
console.log(lhr)
await browser.close();
})();</code></pre><p>以花瓣为例,<code>puppeteer</code>目前仅提供浏览器环境,<code>lighthouse</code>通过<code>browser.wsEndpoint()</code>与浏览器进行通信。</p><h3>生成报告</h3><p>通过初始化运行,我们能看到<code>lighthouse</code>的执行,却无法直观的看到结果。</p><pre><code>const puppeteer = require('puppeteer');
const lighthouse = require('lighthouse');
const { URL } = require('url');
const path = require('path');
const printer = require('lighthouse/lighthouse-cli/printer');
const Reporter = require('lighthouse/lighthouse-core/report/report-generator');
function generateReport(runnerResult) {
const now = new Date();
const Y = now.getFullYear();
const M = now.getMonth();
const D = now.getDate();
const H = now.getHours();
const m = now.getMinutes();
const filename = `lhr-report@${Y}-${M + 1 < 10 ? '0' + (M + 1) : M + 1}-${D}-${H}-${m}.html`;
const htmlReportPath = path.join(__dirname, 'public', filename);
const reportHtml = Reporter.generateReport(runnerResult.lhr, 'html');
printer.write(reportHtml, 'html', htmlReportPath);
}
(async() => {
const url = 'https://huaban.com/discovery/';
const browser = await puppeteer.launch({
headless: false,
defaultViewport: null,
});
const lhr = await lighthouse(
url,
{
port: (new URL(browser.wsEndpoint())).port,
output: 'json',
logLevel: 'info',
}
);
generateReport(lhr)
await browser.close();
})();</code></pre><p>手动创建一个<code>public</code>目录(这里不通过代码检测是否有目录了),新增<code>generateReport</code>方法,将报告结果输出为<code>html</code>。</p><p>将输出的<code>HTML</code>用浏览器打开,可以直观地看到输出结果。<br><img src="/img/bVcTGhF" alt="默认展示" title="默认展示"></p><p>默认有五个分类:<br><img src="/img/bVcTGhH" alt="五个默认分类" title="五个默认分类"></p><h3>国际化语言设置</h3><pre><code>(async() => {
const url = 'https://huaban.com/discovery/';
const browser = await puppeteer.launch({
headless: false,
defaultViewport: null,
});
const lhr = await lighthouse(
url,
{
port: (new URL(browser.wsEndpoint())).port,
output: 'json',
logLevel: 'info',
},
{
settings: {
locale: 'zh' // 国际化
}
}
);
generateReport(lhr)
await browser.close();
})();</code></pre><p>这里,我们可以看到<code>lighthouse(url, flags, configJSON)</code>接收三个参数。</p><ul><li><p><code>url</code></p><ul><li>需要检测的目标地址</li></ul></li><li><p><code>flags</code></p><ul><li><code>Lighthouse</code>运行的配置项</li><li>决定运行的端口、调试地址,查找Gatherer、Audit的相对地址</li><li>具体配置见:<a href="https://link.segmentfault.com/?enc=1edInNTYrpCtHAMxrMwbAw%3D%3D.hLQDMRPmKuhDbel1GYrTpjPJJpL0gwmJ0vNgyMpjSwnA93i5fTk%2FqhDwSUWoToRIJQZKSmPYvl01cEWMAgTcJevJCUpVkbzZBRBPqFIhsIA%3D" rel="nofollow">https://github.com/GoogleChro...</a></li></ul></li><li><p><code>configJSON</code></p><ul><li><code>Lighthouse</code>工作的配置项</li><li>决定如何工作,收集何种信息、如何审计、分类展示...</li><li><p>具体配置见:</p><ul><li><a href="https://link.segmentfault.com/?enc=70Y6SxmUbf5JPGSYx2CT%2Fg%3D%3D.qf%2F0U602PJSxOucPMzEIlukG%2FGoHq1A975Pwzjc%2FjTgC8DBB81mndl1ARqRDzqpKqUeneBvtmdG9jcMAtFurB4jEEx6GXwWXuUJBOa0BLIA%3D" rel="nofollow">https://github.com/GoogleChro...</a></li><li><a href="https://link.segmentfault.com/?enc=iYQqVqH4sLU7FhY9LKLHJg%3D%3D.AZ2rwjsJN9dZqYDVt%2BIsGThRxWKJUT5S%2FoG90ZwACufWgJIB91vyUlCaFxs2MA4jvp4g8cfSvlwtWRgYrclrKfpluyeBaKFkhueizfgaov4%3D" rel="nofollow">https://github.com/GoogleChro...</a></li></ul></li></ul></li></ul><h3>自定义收集器Gatherer</h3><h4>父级Gatherer —— 继承目标</h4><pre><code>// https://github.com/GoogleChrome/lighthouse/blob/v7.5.0/lighthouse-core/gather/gatherers/gatherer.js
class Gatherer {
/**
* @return {keyof LH.GathererArtifacts}
*/
get name() {
// @ts-expect-error - assume that class name has been added to LH.GathererArtifacts.
return this.constructor.name;
}
/* eslint-disable no-unused-vars */
/**
* Called before navigation to target url.
* @param {LH.Gatherer.PassContext} passContext
* @return {LH.Gatherer.PhaseResult}
*/
beforePass(passContext) { }
/**
* Called after target page is loaded. If a trace is enabled for this pass,
* the trace is still being recorded.
* @param {LH.Gatherer.PassContext} passContext
* @return {LH.Gatherer.PhaseResult}
*/
pass(passContext) { }
/**
* Called after target page is loaded, all gatherer `pass` methods have been
* executed, and — if generated in this pass — the trace is ended. The trace
* and record of network activity are provided in `loadData`.
* @param {LH.Gatherer.PassContext} passContext
* @param {LH.Gatherer.LoadData} loadData
* @return {LH.Gatherer.PhaseResult}
*/
afterPass(passContext, loadData) { }
/* eslint-enable no-unused-vars */
}</code></pre><h4>自定义示例</h4><pre><code>import { Gatherer } from 'lighthouse';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const DevtoolsLog = require('lighthouse/lighthouse-core/gather/devtools-log.js');
class LIMGGather extends Gatherer {
/** @type {LH.Gatherer.GathererMeta<'DevtoolsLog'>} */
meta = {
supportedModes: [ 'navigation' ],
dependencies: { DevtoolsLog: DevtoolsLog.symbol },
};
/**
* @param {LH.Artifacts.NetworkRequest[]} networkRecords
*/
indexNetworkRecords(networkRecords) {
return networkRecords.reduce((arr, record) => {
// An image response in newer formats is sometimes incorrectly marked as "application/octet-stream",
// so respect the extension too.
const isImage = /^image/.test(record.mimeType) || /\.(avif|webp)$/i.test(record.url);
// The network record is only valid for size information if it finished with a successful status
// code that indicates a complete image response.
if (isImage) {
arr.push(record);
}
return arr;
}, []);
}
/**
* @param {LH.Gatherer.FRTransitionalContext} context
* @param {LH.Artifacts.NetworkRequest[]} networkRecords
* @return {Promise<LH.Artifacts['ImageElements']>}
*/
async _getArtifact(_context, networkRecords) {
const imageNetworkRecords = this.indexNetworkRecords(networkRecords);
return imageNetworkRecords;
}
/**
* @param {LH.Gatherer.PassContext} passContext
* @param {LH.Gatherer.LoadData} loadData
* @return {Promise<LH.Artifacts['ImageElements']>}
*/
async afterPass(passContext, loadData) {
return this._getArtifact({ ...passContext, dependencies: {} }, loadData.networkRecords);
}
}
module.exports = LIMGGather;</code></pre><p>单独的<code>Gatherer</code>是没有任何用途的,只是收集<code>Audit</code>需要用到的中间数据,最终展示的数据为<code>Audit</code>的输出。</p><p>所以,我们讲述完自定义<code>Audit</code>后,再说明<code>Gatherer</code>如何使用。</p><p><strong>Notes:</strong>收集器<code>Gatherer</code><strong>必须</strong>有个<code>name</code>属性,默认是父类<code>get name()</code>返回的<code>this.constructor.name;</code>,如需自定义,重写<code>get name()</code>方法。</p><p><strong>Notes:</strong> <code>Gatherer</code>最好不要和默认收集器重名,若<code>configJSON.configPath</code>配置的等同默认路径,会忽略自定义 —— 后续会详细讲述。</p><h3>自定义审计Audit</h3><h4>父级Audit —— 继承目标</h4><p><code>Audit</code>类有很多方法,个人用到的主要重写的也就<code>meta</code>、<code>audit</code>。</p><pre><code>// https://github.com/GoogleChrome/lighthouse/blob/v7.5.0/lighthouse-core/audits/audit.js
class Audit {
...
/**
* @return {LH.Audit.Meta}
*/
static get meta() {
throw new Error('Audit meta information must be overridden.');
}
...
/**
* @return {Object}
*/
static get defaultOptions() {
return {};
}
...
/* eslint-disable no-unused-vars */
/**
*
* @param {LH.Artifacts} artifacts
* @param {LH.Audit.Context} context
* @return {LH.Audit.Product|Promise<LH.Audit.Product>}
*/
static audit(artifacts, context) {
throw new Error('audit() method must be overriden');
}
...
}</code></pre><h4>自定义示例</h4><pre><code>import { Audit } from 'lighthouse';
import { Pages, AuditStrings, Scores } from '../../config/metrics';
import { toPrecision } from '../../util';
class LargeImageOptimizationAudit extends Audit {
static get meta() {
return {
...Pages[AuditStrings.limg],
// ^ 重要的是id字段,定义分类时需要指定
scoreDisplayMode: Audit.SCORING_MODES.NUMERIC,
// ^ 分数的展示模式,只有Audit.SCORING_MODES.NUMERIC才有分值,若取其它值,分值score始终为0.
requiredArtifacts: [ 'LIMGGather' ],
// ^ 定义依赖的Gatherers
};
}
static audit(artifacts) {
const limitSize = 50;
const images = artifacts.LIMGGather;
// ^ 解构或直接访问获取Gatherer收集的数据。
/**
* 后续的逻辑是,判断大于50K的图片数量。
* 以当前指标总分 - 大于50K图片数量 * 出现一次扣step分
* 计算最终分数
*/
const largeImages = images.filter(img => img.resourceSize > limitSize * 1024);
const scroeConfig = Scores[AuditStrings.limg];
const score = scroeConfig.scores - largeImages.length * scroeConfig.step;
const finalScore = toPrecision((score < 0 ? 0 : score) / scroeConfig.scores);
// ^ Lighthouse中Audit、categories中的分数都是0~1范围内的
const headings = [
{ key: 'url', itemType: 'thumbnail', text: '资源预览' },
{ key: 'url', itemType: 'url', text: '图片资源地址' },
{ key: 'resourceSize', itemType: 'bytes', text: '原始大小' },
{ key: 'transferSize', itemType: 'bytes', text: '传输大小' },
/**
* key:返回对象中details字段Audit.makeTableDetails方法第二个参数中对应的键值
* itemType是Lighthouse识别对象key对应值,来使用不同的样式展示的。
* itemType类型文档https://github.com/GoogleChrome/lighthouse/blob/v7.5.0/lighthouse-core/report/html/renderer/details-renderer.js#L266
*/
// const headings = [
// { key: 'securityOrigin', itemType: 'url', text: '域' },
// { key: 'dnsStart', itemType: 'ms', granularity: '0.001', text: '起始时间' },
// { key: 'dnsEnd', itemType: 'ms', granularity: '0.001', text: '结束时间' },
// { key: 'dnsConsume', itemType: 'ms', granularity: '0.001', text: '耗时' },
// ];
/////itemType === 'ms'时,可以设置精度granularity
];
return {
score: finalScore,
displayValue: `${largeImages.length} / ${images.length} Size >${limitSize}KB`,
// ^ 当前审计项的总展示
details: Audit.makeTableDetails(headings, largeImages),
// ^ 当前审计项的细节展示
// ^ table类型的展示,要求第二个参数largeImages是平铺的对象元素构成的数组。
// https://github.com/GoogleChrome/lighthouse/blob/v7.5.0/lighthouse-core/report/html/renderer/details-renderer.js
};
}
}
module.exports = LargeImageOptimizationAudit;</code></pre><p>这里,我们仍然不讲述Gatherer、Audit如何使用,我们继续讲述Categories时,统一讲述如何使用。</p><h3>自定义分类Categories</h3><pre><code>import { LargeImageOptimizationAudit } from '../gathers';
// ^ gathers文件夹下定义了统一入口index.js,导出所有的自定义收集器
import { LIMGGather } from '../audits';
// ^ audits文件夹下定义了统一入口index.js,导出所有的自定义审查器
module.exports = {
extends: 'lighthouse:default', // 决定是否包含默认audits,就是上述默认五个分类
passes: [{
passName: 'defaultPass',
gatherers: [
LIMGGather , // 自定义Gather的应用
],
}],
audits: [
LargeImageOptimizationAudit , // 自定义Audit的应用
],
categories: {
mysite: {
title: '自定义指标',
description: '我的自定义指标',
auditRefs: [
{
id: 'large-images-optimization',
// ^ Audit.id,自定义Audit.meta时指定;
// 给自定义Audit单独定义一个分类。
weight: 1
// ^ 当前审计项所占的比重,权重weight总和为100!
},
],
},
},
};</code></pre><p>来一个非上述配置的自定义审计、分类的展示效果(因为这篇文章是后续整理的)。<br><img src="/img/bVcTGhM" alt="自定义审计displayValue" title="自定义审计displayValue"><br>以上是自定义审计返回对象中定义的<code>displayValue</code>字段。<br><img src="/img/bVcTGhP" alt="自定义审计details" title="自定义审计details"><br>以上是自定义审计返回对象中<code>details: Audit.makeTableDetails(headings, largeImages),</code>展示示例。<br>最终的展示效果:<br><img src="/img/bVcTGih" alt="最终效果" title="最终效果"><br>这里的展示效果,是去除默认五个分类后的展示,通过注释掉上述配置的<code>extends: 'lighthouse:default',</code>即可去掉。</p><p><strong>Notes:</strong>去掉<code>extends: 'lighthouse:default',</code>后,若使用内置或自定义的Audit依赖<code>requiredArtifacts: ['traces', 'devtoolsLogs', 'GatherContext']</code>,lighthouse运行会报错:</p><pre><code>errorMessage: "必需的 traces 收集器未运行。"
或
errorMessage: "Required traces gatherer did not run."</code></pre><p>需要补充以下配置:</p><pre><code>passes = [
{
passName: 'defaultPass',
recordTrace: true,// 开启Traces收集器
gatherers: [
],
},
];</code></pre><h2>优化</h2><h3>NPM包与Chrome Devtools中的Lighthouse分值差异大</h3><p>NPM包使用Lighthouse进行合成监控,模拟页面在较慢的连接上加载,会限制 DNS 解析的往返行程以及建立 TCP 和 SSL 连接。<br>而Chrome Devtools的Lighthouse做了很多优化,也没有进行节流限制,即使设置节流,也只是接收服务器响应时的延迟,而不是模拟每次往返双向。</p><p>NPM包使用时,添加参数--throttling-method=devtools来平衡差异。</p><h3>Lighthouse切换桌面模式</h3><p>Lighthouse默认是移动端模式,不同版本配置不同,配置需要对应版本。</p><pre><code>// https://github.com/GoogleChrome/lighthouse/discussions/12058
// 以下是configJSON.settings的配置
// eslint-disable-next-line @typescript-eslint/no-var-requires
const constants = require('lighthouse/lighthouse-core/config/constants');
...
getSettings() {
return (function(isDesktop) {
if (isDesktop) {
return {
// extends: 'lighthouse:default', // 是否包含默认audits
locale: 'zh',
formFactor: 'desktop', // 必须的lighthouse模拟Device
screenEmulation: { // 结合formFactor使用,要匹配
...constants.screenEmulationMetrics.desktop,
},
emulatedUserAgent: constants.userAgents.desktop, // 结合formFactor使用,要匹配
};
}
return {
// extends: 'lighthouse:default', // 是否包含默认audits
locale: 'zh',
formFactor: 'mobile', // 必须的lighthouse模拟Device
screenEmulation: { // 结合formFactor使用,要匹配
...constants.screenEmulationMetrics.mobile,
},
emulatedUserAgent: constants.userAgents.mobile, // 结合formFactor使用,要匹配
};
})(this.isDesktop);
}</code></pre><h3>Lighthouse的Gather、Audits路径配置</h3><h4>对象配置</h4><p>见上述使用;</p><h4>路径配置</h4><pre><code>// 引入方式:相对于项目根目录,设置相对路径
...
lightHouseConfig: {
// onlyCategories: onlyCategories.split(','), // https://github.com/GoogleChrome/lighthouse/blob/master/docs/configuration.md
reportPath: path.join(__dirname, '../public/'), // 测试报告存储目录
passes: [
{
passName: 'defaultPass',
gatherers: [
'app/gathers/large-images-optimization.ts',
],
},
],
audits: [
'app/audits/large-images-optimization.ts',
],
categories: {
mysite: {
title: '自定义指标',
description: '我的自定义指标',
auditRefs: [
{
id: 'large-images-optimization',
weight: 1
},
],
},
...</code></pre><p>其中:<strong>源代码使用require(path)的形式调用,而不是require(path).default,只支持module.epxorts = class Module导出</strong></p><h4>BasePath路径配置</h4><p>理论上配置:</p><pre><code> // lighthouse运行的第二个参数flags
{
port: new URL(this.browser.wsEndpoint()).port,
configPath: path.join(__filename), // 查找gather、audit的基准
// ^路径定位到文件,源码内会上溯到目录结构;若定位到目录,源码内会上溯到上一级目录
},
// lighthouse运行的第三个参数configJson
...
passes: [
{
passName: 'defaultPass',
gatherers: [
'large-images-optimization',
],
},
],
audits: [
'large-images-optimization',
],
...</code></pre><p>然而,"lighthouse": "^7.5.0",源码中:</p><pre><code>requirePath = resolveModulePath(gathererPath, configDir, 'gatherer'); // 检索gather
const absolutePath = resolveModulePath(auditPath, configDir, 'audit'); // 检索audit</code></pre><p>第三个参数并没有使用。<br>即v7.5.0会在配置的configPath下查找对应的'large-images-optimization'。</p><p><strong>现做如下妥协:</strong></p><pre><code>// lighthouse第三个参数configJson
...
passes: [
{
passName: 'defaultPass',
gatherers: [
'gathers/large-images-optimization',
],
},
],
audits: [
'audits/large-images-optimization',
],
...</code></pre><h4>Gather、Audti配置引入源码实现</h4><pre><code>function expandGathererShorthand(gatherer) {
if (typeof gatherer === 'string') {
// just 'path/to/gatherer'
return {path: gatherer};
} else if ('implementation' in gatherer || 'instance' in gatherer) {
// {implementation: GathererConstructor, ...} or {instance: GathererInstance, ...}
return gatherer;
} else if ('path' in gatherer) {
// {path: 'path/to/gatherer', ...}
if (typeof gatherer.path !== 'string') {
throw new Error('Invalid Gatherer type ' + JSON.stringify(gatherer));
}
return gatherer;
} else if (typeof gatherer === 'function') {
// just GathererConstructor
return {implementation: gatherer};
} else if (gatherer && typeof gatherer.beforePass === 'function') {
// just GathererInstance
return {instance: gatherer};
} else {
throw new Error('Invalid Gatherer type ' + JSON.stringify(gatherer));
}
}
//https://github.com/GoogleChrome/lighthouse/blob/master/lighthouse-core/config/config-helpers.js#L342
function resolveGathererToDefn(gathererJson, coreGathererList, configDir) {
const gathererDefn = expandGathererShorthand(gathererJson);
if (gathererDefn.instance) {
return {
instance: gathererDefn.instance,
implementation: gathererDefn.implementation,
path: gathererDefn.path,
};
} else if (gathererDefn.implementation) {
const GathererClass = gathererDefn.implementation;
return {
instance: new GathererClass(),
implementation: gathererDefn.implementation,
path: gathererDefn.path,
};
} else if (gathererDefn.path) {
const path = gathererDefn.path;
return requireGatherer(path, coreGathererList, configDir);
} else {
throw new Error('Invalid expanded Gatherer: ' + JSON.stringify(gathererDefn));
}
}</code></pre><h4>补充:Gather、Audit重名问题</h4><p>默认<code>Gatherer</code>、<code>Audit</code>的检索只是<code>lighthouse-core</code>包中对应目录的检索,若自定义的<code>Gather</code>、<code>Audit</code>,且使用时使用字符串标识,一定不能和<code>lighthouse-core</code>中的重名。</p><p><a href="https://link.segmentfault.com/?enc=teMkXfHLyxzCDH8VlqHNlA%3D%3D.Wi5w4X5vVSG0soCviD0j1sKssXOT%2BjyiRyJk0Iw9YPf8t%2BZSq5xiIwZXihqstj8UB6tWXlJ%2FTWgw0QUajxpTKmGLyclD8zSB6%2FUbHtoFYIrxeRDrnFScq%2BfFZ77gISfBQvPpzovh1qbtx1B8q9Cykl%2BjS0ln1UOVTbovKxsGU7elyzrIRvitB0%2FAbA4d8KgT" rel="nofollow">重名的话</a>,优先使用<code>lighthouse-core</code>中的<code>gather</code>、<code>audit</code>;</p><pre><code>function requireGatherer(gathererPath, coreGathererList, configDir) {
const coreGatherer = coreGathererList.find(a => a === `${gathererPath}.js`);
let requirePath = `../gather/gatherers/${gathererPath}`;
if (!coreGatherer) {
// Otherwise, attempt to find it elsewhere. This throws if not found.
requirePath = resolveModulePath(gathererPath, configDir, 'gatherer');
}
const GathererClass = /** @type {GathererConstructor} */ (require(requirePath));
return {
instance: new GathererClass(),
implementation: GathererClass,
path: gathererPath,
};
}</code></pre><p><a href="https://link.segmentfault.com/?enc=i8WjJZWdFRyZPi49r2ZCag%3D%3D.8jPQsbnysBnZUjARyJFd2CgbAskBECRzDlx8PeFKAWvZJqECpI8Xb453RqQe%2Fn1yeqAm4yxwVbJFMRmYi6NvWkSTKz6AwYolHUY6Px7xS9YNZ03ceK13M4t4%2F1zAtpIxh01KMiI0pm3l9SYXR0%2FISYm8NZQ8lHWI7MylAbofXSOriOpDSYwFYxPB%2F9YjxzTO" rel="nofollow">不重名的话</a>,会有一个检索顺序</p><ol><li>相对路径检索;</li><li><code>process.cwd()</code>同级目录下检索;</li><li><p>配置<code>flags.configPath</code>的话,该路径下查找;</p><ol><li><code>flags.configPath</code>只是查找<code>gatherer</code>、<code>audits</code>资源的基准,<code>config</code>的设置和该配置无关。</li></ol><pre><code>function resolveModulePath(moduleIdentifier, configDir, category) {
try {
return require.resolve(moduleIdentifier);
} catch (e) {}
try {
return require.resolve(moduleIdentifier, {paths: [process.cwd()]});
} catch (e) {}
const cwdPath = path.resolve(process.cwd(), moduleIdentifier);
try {
return require.resolve(cwdPath);
} catch (e) {}
const errorString = 'Unable to locate ' + (category ? `${category}: ` : '') +
`\`${moduleIdentifier}\`.
Tried to require() from these locations:
${__dirname}
${cwdPath}`;
if (!configDir) {
throw new Error(errorString);
}
const relativePath = path.resolve(configDir, moduleIdentifier);
try {
return require.resolve(relativePath);
} catch (requireError) {}
try {
return require.resolve(moduleIdentifier, {paths: [configDir]});
} catch (requireError) {}
throw new Error(errorString + `
${relativePath}`);
}</code></pre></li></ol><h3>报告解析</h3><p>我们看到报告中有部分是已通过,这部分怎么解析的呢?</p><p>附上源码片段:</p><pre><code>// node_modules/lighthouse/lighthouse-core/report/html/renderer/category-renderer.js
...
// 报告模版 解析
render(category, groupDefinitions = {}) {
const element = this.dom.createElement('div', 'lh-category');
this.createPermalinkSpan(element, category.id);
element.appendChild(this.renderCategoryHeader(category, groupDefinitions));
// Top level clumps for audits, in order they will appear in the report.
/** @type {Map<TopLevelClumpId, Array<LH.ReportResult.AuditRef>>} */
const clumps = new Map();
clumps.set('failed', []);
clumps.set('warning', []);
clumps.set('manual', []);
clumps.set('passed', []);
clumps.set('notApplicable', []);
// Sort audits into clumps.
for (const auditRef of category.auditRefs) {
const clumpId = this._getClumpIdForAuditRef(auditRef);
const clump = /** @type {Array<LH.ReportResult.AuditRef>} */ (clumps.get(clumpId)); // already defined
clump.push(auditRef);
clumps.set(clumpId, clump);
}
// Render each clump.
for (const [clumpId, auditRefs] of clumps) {
if (auditRefs.length === 0) continue;
if (clumpId === 'failed') {
const clumpElem = this.renderUnexpandableClump(auditRefs, groupDefinitions);
clumpElem.classList.add(`lh-clump--failed`);
element.appendChild(clumpElem);
continue;
}
const description = clumpId === 'manual' ? category.manualDescription : undefined;
const clumpElem = this.renderClump(clumpId, {auditRefs, description});
element.appendChild(clumpElem);
}
return element;
}
...
_getClumpIdForAuditRef(auditRef) {
const scoreDisplayMode = auditRef.result.scoreDisplayMode;
if (scoreDisplayMode === 'manual' || scoreDisplayMode === 'notApplicable') {
return scoreDisplayMode;
}
if (Util.showAsPassed(auditRef.result)) {
if (this._auditHasWarning(auditRef)) {
return 'warning';
} else {
return 'passed';
}
} else {
return 'failed';
}
}
...
// node_modules/lighthouse/lighthouse-core/report/html/renderer/util.js
...
const ELLIPSIS = '\u2026';
const NBSP = '\xa0';
const PASS_THRESHOLD = 0.9;
const SCREENSHOT_PREFIX = 'data:image/jpeg;base64,';
const RATINGS = {
PASS: {label: 'pass', minScore: PASS_THRESHOLD},
AVERAGE: {label: 'average', minScore: 0.5},
FAIL: {label: 'fail'},
ERROR: {label: 'error'},
};
...
static showAsPassed(audit) {
switch (audit.scoreDisplayMode) {
case 'manual':
case 'notApplicable':
return true;
case 'error':
case 'informative':
return false;
case 'numeric':
case 'binary':
default:
return Number(audit.score) >= RATINGS.PASS.minScore; // 当audit分值大于0.9时,报告展示通过;
}
}
...</code></pre><h3>补充:收集器中依赖浏览器</h3><p>如这里需要知道是否支持<code>webp</code>格式</p><pre><code>async function supportWebp(context) {
const { driver } = context;
const expression = function() {
const elem = document.createElement('canvas');
// eslint-disable-next-line no-extra-boolean-cast
if (!!(elem.getContext && elem.getContext('2d'))) {
// was able or not to get WebP representation
return elem.toDataURL('image/webp').indexOf('data:image/webp') === 0;
}
// very old browser like IE 8, canvas not supported
return false;
};
return await driver.executionContext.evaluate(
// ^ 返回Promise
expression,
/**
* expression函数声明,不能是字符串,evaluateSync需要是字符串
* 你可能看到过evaluateSync,这是lighthousev7.0.0及其以前版本支持的API。
*/
{
args: [], // {required} args,否则报错
},
);
}
...</code></pre><p>完整示例:</p><pre><code>import { Gatherer } from 'lighthouse';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const DevtoolsLog = require('lighthouse/lighthouse-core/gather/devtools-log.js');
async function supportWebp(context) {
const { driver } = context;
const expression = function() {
const elem = document.createElement('canvas');
// eslint-disable-next-line no-extra-boolean-cast
if (!!(elem.getContext && elem.getContext('2d'))) {
// was able or not to get WebP representation
return elem.toDataURL('image/webp').indexOf('data:image/webp') === 0;
}
// very old browser like IE 8, canvas not supported
return false;
};
return await driver.executionContext.evaluate(
expression,
{
args: [],
},
);
}
class WebpImageGather extends Gatherer {
/** @type {LH.Gatherer.GathererMeta<'DevtoolsLog'>} */
meta = {
supportedModes: [ 'navigation' ],
dependencies: { DevtoolsLog: DevtoolsLog.symbol },
};
/**
* @param {LH.Artifacts.NetworkRequest[]} networkRecords
*/
indexNetworkRecords(networkRecords) {
return networkRecords.reduce((arr, record) => {
const isImage = /^image/.test(record.mimeType) || /\.(avif|webp)$/i.test(record.url);
if (isImage) {
arr.push(record);
}
return arr;
}, []);
}
/**
* @param {LH.Gatherer.FRTransitionalContext} _context
* @param {LH.Artifacts.NetworkRequest[]} networkRecords
* @return {Promise<LH.Artifacts['ImageElements']>}
*/
async _getArtifact(context, networkRecords) {
const isSupportWebp = await supportWebp(context);
const imagesNetworkRecords = this.indexNetworkRecords(networkRecords);
return {
isSupportWebp,
imagesNetworkRecords,
};
}
async afterPass(context, loadData) {
return this._getArtifact(context, loadData.networkRecords);
}
}
module.exports = WebpImageGather;</code></pre><p>也可参考源码中<a href="https://link.segmentfault.com/?enc=rEnEkIoia44O9pmmb6w4vQ%3D%3D.Ki6%2FKbGNUOPA1BfKUh46NSS3msnKssms%2FX4Gp2EMpY88UqZALOZEAFqMW%2BK82WEzTrJwXaklTSu8HPMlHeWc4ujBIUlAGTjEruJk6Z8%2FWgU%2FWrJ816hxXoewgNRhxTWHR%2FpNacjriO0xiBZltbuvcw%3D%3D" rel="nofollow"><code> ImageElements</code></a>的声明及<a href="https://link.segmentfault.com/?enc=%2BgKXy%2BDUAVuceHnIBFxVuQ%3D%3D.eOIDhcveKuNdDvfaEjsSbU74Ggh2oEXqzrh5bsCCeRuzySsS055bW7y0vYGTIS%2FBR3bzUKGXPXNSeRSa5biQ79uCPinKXebAYw9RhgUoke08eMub%2F%2F%2Bn3Aw67AzIjhn7" rel="nofollow"><code>pageFunctions</code></a>的定义。</p><h2>补充:<code>Lighouthousev.8.1.0</code></h2><blockquote>对比v.7.5.0</blockquote><h3><code>report-generator</code>引用地址</h3><pre><code>const Reporter = require('lighthouse/report/report-generator');
// ^ 该引入地址,是v8.1.0更新的,老版引入地址为'lighthouse/lighthouse-core/report/report-generator'</code></pre><h3><code>lighthouse</code>执行</h3><p>若使用第三个参数,则<code>locale</code>为必填项。</p><pre><code> const lhr = await lighthouse(
url,
{
port: (new URL(browser.wsEndpoint())).port,
output: 'json',
logLevel: 'info',
}
);</code></pre><h3><code>locale</code>配置</h3><p><code>lighthousev8.1.0</code>,若<code>node</code>在<code>v12.0.0</code>及其以前的版本,需要手动安装<code>full-icu</code>并在命令行中添加参数<code> node --icu-data-dir="./node_modules/full-icu" index.js</code><br>然后,在配置项中才能定义:</p><pre><code>const lhr = await lighthouse(
url,
{
port: (new URL(browser.wsEndpoint())).port,
output: 'json',
logLevel: 'info',
},
{
settings: {
extends: 'lighthouse:default', // 发现不再是默认配置的定义了
locale: 'zh',
},
passes: [
{
passName: 'defaultPass',
}
],
audits: [
],
}
);</code></pre><h2>参考文档</h2><p><a href="https://link.segmentfault.com/?enc=pymkkWg06qoBn6bw9SxLhg%3D%3D.sdS1C9X6RHYGK21xtjsPoFrof2GalH%2FB1yyasVWrF32NuAHnBIKtOjqDhcwSLaVVCPvv60p0oHqHGKS37BLNDQ%3D%3D" rel="nofollow">Lighthouse 网易云音乐实践技术文档</a></p><p><a href="https://link.segmentfault.com/?enc=Ov1Rz2bowBjUwXqGTzhP1A%3D%3D.r8wA252jSAAedGQhKlCWgMSZb8kpIhcapUuQq5tcjQJi7VD%2Ft1951ZHR4DRxhjzIwhHRM3A0J2dZwvye9wGx1A%3D%3D" rel="nofollow">Lighthouse政采云实践技术文档</a></p><p><a href="https://link.segmentfault.com/?enc=QXy9%2FYKA8zNm%2FncawiSDkA%3D%3D.vJhGLt9nwJJY5yHouARvXaiYVJTNlgKQ8GeQcwlxo5dVTy9%2F7aXI3pcY%2F%2BoSgaijycuBFAOmeDTOdB%2BwTmn58A%3D%3D" rel="nofollow">Puppeteer政采云实践技术文档</a></p><p><a href="https://link.segmentfault.com/?enc=WvNdQ3AmtlCNLu6jOGq%2BeQ%3D%3D.9czoxT3R69fheVv4iwyLTc4KCzc00C54ZFk2D%2B2kYrT47b4mMeDkwOohXP69BQlugPZx15A6gJLDu3NYfpOfyA%3D%3D" rel="nofollow">Chromium浏览器实例参数配置</a></p><p><a href="https://link.segmentfault.com/?enc=fMQQI0%2FUNKBV4xWxKg7Uww%3D%3D.nA98jPyRpuhTFaiypLVgLrPRdS2NPZv8L6%2FB34EyuvDpnwZLML39443yqSqJkFmG%2FedWVE%2BNj26xAsqBobq91uDMqF9BH8QATncwrLi6Fw%2BUYUVYakEsrvtruQkcmx%2F2" rel="nofollow">Lighthouse Driver源码官方实现</a></p><p><a href="https://link.segmentfault.com/?enc=Fkhe%2BouXYLahOph%2FCDQeog%3D%3D.lBQ0V3qId7nLMqjOKF1SLIXJ%2B0aAYFYZb7Voyp2YmgXDJHO0D5neQAI6dEGHjft5HQF%2FNy7P8Kl5xm0NeXkGd2MxCZ5Z4th%2BecoPFUK337cOEYohr5MqkuYy3C2S2Se7" rel="nofollow">Lighthouse Drive模块与浏览器双向通信协议JSON</a></p><p><a href="https://link.segmentfault.com/?enc=%2BGMEaG1qriCHIWaGGKZRXQ%3D%3D.nMlPdq557ilUWB8kt17R0TYoFwOoWnAJZhX1NvRas7cHvAEiyGRxITGbmfn%2FM%2FTaOpXytP5PSjBEQ1Q%2Blxh%2Bpg%3D%3D" rel="nofollow">Devtools-Protocol协议官方文档</a></p><p><a href="https://link.segmentfault.com/?enc=N4XsxQFE%2BJ0Y5IsPi%2Fh3cw%3D%3D.8t6srfz4MwDtzMJGnK773lh5OznkMc%2BQdKhGAcSAc7ClXmPur1isnjx92oZN8RsH5CTwH19NqV47mlq33%2Bn39ffIwMWcUZwxdCxu6IRsBHVawIOBMdRIcbOj2ZbI20y19wOEH0W54J%2FuTwpnVmA7rQfFdZ7hYN3ei0DHkVZOpkg%3D" rel="nofollow">Puppeteer5.3.0中文文档</a></p><p><a href="https://link.segmentfault.com/?enc=o94koAWdIuWI%2FfT0BIgBYQ%3D%3D.EGWrB7hxDsOLUz2cjxR2HT0ubPs29K%2B1Ki0YwkhEDRFCj5mukuFmvK2fmsDA%2Bn0cpW4byVvZNySglkb2G5Dfr9N1AQ2mVifyf7MYFDBCdcxTqly6A%2FrD1QPumGa%2B2JFX" rel="nofollow">Puppeteer官方文档</a></p><p><a href="https://link.segmentfault.com/?enc=qfbFVQLiJZQecgMyNsdojQ%3D%3D.IXm8SIatQpP4twYuElxYCxKJUWnsogmvt4IzHun%2FwXJ4ICejJIPDiry3b0TVX2Gr" rel="nofollow">Lighthouse源码</a></p>
Chrome Devtools: Sources篇
https://segmentfault.com/a/1190000040265729
2021-06-30T14:33:34+08:00
2021-06-30T14:33:34+08:00
米花儿团儿
https://segmentfault.com/u/mihuartuanr
2
<h2>相关篇章</h2><p><a href="https://segmentfault.com/a/1190000039962433">Chrome Devtools: Elements篇</a></p><h2>概述</h2><p>Sources面板用于资源检索、代码逻辑调试。</p><h2>演示前置</h2><h3>示例</h3><ul><li><a href="https://link.segmentfault.com/?enc=oC%2F73jHsbRLz4jrktSINLA%3D%3D.5q92VoEZdwACb5rA6xItsmwPpYiewT2urBAOOnlDbt6jk61486cKCUfcsnDEoaYjnIy4YR%2BX9WzJJoctYgu%2BKg%3D%3D" rel="nofollow">ElementUI官网</a></li></ul><p>其他篇章有的是以掘金为示例演示的,而掘金是服务端渲染(SSR),资源压缩,不易演示。</p><h3>环境</h3><ul><li>Chrome浏览器</li><li>版本 90.0.4430.93</li></ul><h3>操作释义</h3><ul><li>聚焦控制台</li><li>鼠标在控制台范围内点击一下,使后续操作上下文绑定在控制台中。</li></ul><h2>打开控制台</h2><p>以<a href="https://link.segmentfault.com/?enc=KqaipOds5vodc0ePo99b9Q%3D%3D.I8SLFzA1AONWJCGp9yv1hb8qW8Hu12XRezVLhIp1RkIgSL0UMA3q%2BOe8DhoJPgzwqVSlJ6OZDgwENXckSvjyrw%3D%3D" rel="nofollow">ElementUI官网</a>为示例讲述:<br><img src="https://upload-images.jianshu.io/upload_images/3116808-2d2f3666a25e68d0.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" alt="devtool.png" title="devtool.png"></p><p>通过链接打开页面,通过F12或鼠标右键【检查】打开开发者工具控制台。</p><h2>默认布局</h2><p><img src="https://upload-images.jianshu.io/upload_images/3116808-b1b1f8717ef846a0.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" alt="layouts.png" title="layouts.png"></p><ol><li><p>资源管理器面板</p><ul><li>该面板下又细分不同的面板,默认展示的是Page面板</li><li>Page面板默认以域名分类,列出站点依赖的所有资源</li></ul></li><li><p>代码编辑面板</p><ul><li>在资源管理器面板中选中一个文件后,该文件的内容展示在该面板</li></ul></li><li><p>代码Debugger面板</p><ul><li>操作断点</li><li>查看断点上下文</li></ul></li></ol><h2>Sources面板</h2><p>该面板下的功能紧紧依赖着资源管理面板、代码编辑面板、调试面板。</p><p>只不过细分到下层面板管理不同的资源:</p><ul><li><p>Page面板</p><ul><li>管理远程站点资源</li></ul></li><li><p>Filesystem面板</p><ul><li>将Sources面板当IDE(代码编辑器),管理本地站点资源</li></ul></li><li><p>Snippets面板</p><ul><li>管理浏览器持久化代码资源</li></ul></li></ul><p>我们以Page面板为主体讲述,其它小面板(Filesystem、Snippets)一带而过。</p><h3>Page面板</h3><h4>面板布局1:资源管理器</h4><p>默认布局的1位置即Page面板的全部内容,该面板列出了当前站点页面执行的所有资源,我们可以通过该面板获取以下站点信息:</p><ul><li><p>技术栈:</p><ul><li>可以从资源的关键代码查看</li></ul></li><li><p>相关资源:</p><ul><li>从资源列表中一眼能看出页面加载的资源类型</li><li>当前页面执行的自定义脚本,比如Snippets面板下定义的...</li></ul></li><li><p>依赖域:</p><ul><li>从资源分类上,依赖资源所在域一目了然</li></ul></li><li><p>浏览器扩展程序</p><ul><li>当前页面加载的浏览器扩展程序</li><li><p>为了干净的调试环境,排除扩展程序的干扰,所以,一般选用无痕模式调试</p><ul><li>前提:没有开启扩展程序无痕模式下可用</li></ul></li></ul></li></ul><h4>面板布局2:代码编辑面板</h4><p>在Page面板中点击任意资源,即可在默认布局2的编辑面板中看到资源详情。<br><img src="https://upload-images.jianshu.io/upload_images/3116808-965dd4cc5b3682d2.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" alt="imageDetails.png" title="imageDetails.png"><br>上图点击了图片资源,可以看到该图片是一张二维码。<br><img src="https://upload-images.jianshu.io/upload_images/3116808-00b62189a3555453.gif?imageMogr2/auto-orient/strip" alt="css-runtime.gif" title="css-runtime.gif"><br>上图点击了CSS资源,在默认布局2位置的编辑面板中,可以点击左下角的 {} 按钮,进行代码美化 —— 根据当前CSS资源的大小,美化所需要的时间不同。</p><p>若资源太大,浏览器可能会因为CPU占用过高卡死。</p><p>这里我们做了一个试验,检索到顶部菜单的选择器,进行样式更新,可以实时地在页面上看到展示效果,甚至不需要保存。</p><p>然而,在这里修改的代码只是保存在内存中,刷新页面代码就还原了。</p><h4>JS资源调试</h4><p>这里,我们将Javascript资源单独讲述,因为在Devtools中JS资源调试的复杂度较高。</p><h5>调试JS的场景</h5><ul><li>在编写代码过程中,查看未知参数的结构;</li><li>在编写、Bug修复过程中,运行结果与逻辑设计不符时,代码逻辑梳理;</li></ul><h5>调试JS的步骤</h5><p>以修复Bug为例:</p><ol><li>找出Bug复现的规律</li><li>熟悉代码的前提下,由规律出发,推断Bug复现的范围</li><li>在不同的范围打(条件)断点</li><li>Step by Step的调试断点</li><li>根据调试的结果,不断的缩小Bug范围</li><li>缩小至找到确定的问题</li><li>针对找到的问题,提出解决方案</li><li>评估解决方案,选择合适的方案修复</li></ol><p><strong>Note:</strong>针对网络请求,断点时间过长会造成请求超时。</p><h5>断点 vs. 日志</h5><p>说到调试代码,常用的方式有两种:断点、日志;</p><table><thead><tr><th align="left">断点Debugger</th><th align="left">日志Console</th></tr></thead><tbody><tr><td align="left">中断代码的执行</td><td align="left">不中断代码的执行</td></tr><tr><td align="left">查看中断代码那一时刻的上下文信息</td><td align="left">查看代码执行结束的上下文信息</td></tr><tr><td align="left">非侵入式</td><td align="left">侵入式,将日志代码写入业务代码中</td></tr><tr><td align="left">查看代码中断时刻所有的执行上下文信息</td><td align="left">只能查看指定的打印信息</td></tr><tr><td align="left">时效性:此时此刻的值</td><td align="left">代码执行结束时指定信息的值,除非深拷贝</td></tr></tbody></table><h5>示例</h5><p>在代码编辑面板中,只有针对Javascript的断点才能拦截执行成功,而针对DOM的断点,需要在Elements面板添加:DOM的操作,详情参见Elements篇。</p><p>下述以<a href="https://link.segmentfault.com/?enc=rwYE3PpEnGMdLIfK%2BnCFpQ%3D%3D.I8b3UGK9RRCzvBOqjDI09qJlbQfCTx1C8RtsZsfDdII5WM5XFO5Ir%2Blr6KTuqgBFIDAeTTpHVz3g6r4VJKZY8saiX6AsOjfGmzSB5%2F8DehA%3D" rel="nofollow">Chrome浏览器提供的官方调试代码</a>为例:<br><img src="https://upload-images.jianshu.io/upload_images/3116808-f52afff039d43967.gif?imageMogr2/auto-orient/strip" alt="debug.gif" title="debug.gif"></p><p>打开控制台,从Panel面板中可以看到当前页面只有两个资源:HTML页面及相关的JavaScript。<br>其它的是我安装的Chrome扩展(没有使用无痕模式)。</p><p>从get-started.html中我们可以看到相关的<strong>HTML结构、Style样式</strong>及引入的<strong>JavaScript代码</strong>。</p><h6>页面逻辑</h6><p>输入Number1、Number2,点击按钮获得计算结果。</p><h6>期望结果</h6><p>计算获取Number1、Number2两个数字的和:1 + 1 = 2</p><h6>实际结果</h6><p>获取到Number1、Number2字符串的拼接:1 + 1 = 11<br><img src="https://upload-images.jianshu.io/upload_images/3116808-46389b27f6e46863.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" alt="1+1.png" title="1+1.png"></p><h6>复现率</h6><p>100%,说明是逻辑错误,而不是代码逻辑对某种边界没有覆盖的概率问题。</p><h6>代码锁定Bug范围(嫌疑犯)</h6><pre><code>function updateLabel() {
var addend1 = getNumber1();
var addend2 = getNumber2();
var sum = addend1 + addend2;
label.textContent = addend1 + ' + ' + addend2 + ' = ' + sum;
}</code></pre><p>由代码 <code>label.textContent = addend1 + ' + ' + addend2 + ' = ' + sum; </code>及 <code>1 + 1 = 11 </code>的显示结果猜测,<code>addend1</code>、<code>addend2</code>看似没有问题,而<code>sum</code>看似有问题,那我们就在<code>sum</code>计算的地方打断点。</p><h6>断点调试</h6><p><img src="https://upload-images.jianshu.io/upload_images/3116808-2dacd8ac50cf2371.gif?imageMogr2/auto-orient/strip" alt="debugger.gif" title="debugger.gif"></p><p>在资源管理器中点击html引入的Javascript文件,找到相关代码,在<strong>行号</strong>位置点击一下,即添加断点,再次点击同一行号,取消断点。</p><p>点击按钮,<strong>触发函数调用</strong>,此时,我们可以看到<strong>当前执行上下文</strong>中的所有信息,如<code>addend1</code>、<code>addend2</code>的值,偷偷的执行<strong>F9(Step)</strong>,可以看到<code>sum</code>的结果。</p><h6>诊断</h6><p>从当前执行上下文可以看到<code>addend1</code>、<code>addend2</code>的值是字符串类型,而字符串做加法就是字符串的拼接,所以,结果没问题!!!</p><p>是的,运算结果没问题,那为什么不是我们期望的结果呢?</p><p>因为我们期望的是数字类型相加!!!</p><h6>测试</h6><p>聚焦控制台,通过ESC按键可以在<strong>当前面板</strong>开启/隐藏Console面板,在控制台的最下方。<br><img src="https://upload-images.jianshu.io/upload_images/3116808-a728139a33bf8550.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" alt="test.png" title="test.png"><br>借助断点将作用域限制在函数<code>updateLabel</code>内,通过Console面板验证自己的猜想,此时,Console面板可以访问函数<code>updateLabel</code>的变量。</p><h6>解决方案</h6><p><img src="https://upload-images.jianshu.io/upload_images/3116808-fdf38972dc8ff2c5.gif?imageMogr2/auto-orient/strip" alt="solution.gif" title="solution.gif"><br>那我们只要将<code>addend1</code>、<code>addend2</code>的值转换为数字即可。</p><p>方案有很多种:</p><ul><li><code>parseInt()</code></li><li><code>parseFloat()</code></li><li><code>Number()</code></li><li><code>1 * '1'</code><br>...</li></ul><p>这里,我直接在代码编辑器面板中改动了<code>getNumber1</code>、<code>getNumber2</code>的方法,另其返回数字类型的数据。</p><p>保存以后,点击按钮,可以看到期望的结果。<br><img src="https://upload-images.jianshu.io/upload_images/3116808-6741ef3a22549818.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" alt="expect.png" title="expect.png"><br>通过右上角按钮,切换断点的激活状态,激活是深蓝色,取消激活是有透明度的蓝色。<br>取消激活状态下,断点不会拦截代码的执行。</p><p>在取消激活的状态下,可以尝试输入不同的值,检测逻辑的正确性。</p><p>简单的调试示例到此结束。</p><h5>CSS、JavaScript调试对比</h5><table><thead><tr><th align="left">JavaScript调试</th><th align="left">CSS调试</th></tr></thead><tbody><tr><td align="left">需要保存</td><td align="left">无需保存</td></tr><tr><td align="left">需要手动执行调用,不会自动执行</td><td align="left">自动执行,实时效果</td></tr><tr><td align="left">可断点拦截执行</td><td align="left">断点无效</td></tr></tbody></table><h4>断点分类</h4><p>上述调试Javascript的前提是要熟悉代码,比较适用于在自己编写的程序中调试。</p><p>若遇到其它的场景,如第三方库调试,则需要根据相应的场景,选择合适的断点组合使用。</p><table><thead><tr><th align="left">断点类型</th><th align="left">应用场景</th></tr></thead><tbody><tr><td align="left">代码行断点</td><td align="left">大体/清晰知道调试范围时使用</td></tr><tr><td align="left">代码行条件断点</td><td align="left">大体/清晰知道调试范围时,且只调试某指定条件分支下使用</td></tr><tr><td align="left">代码行日志断点</td><td align="left">非代码侵入式的打印日志</td></tr><tr><td align="left">DOM断点</td><td align="left">调试指定DOM的改变、移除或子节点的变动,详见Elements篇</td></tr><tr><td align="left">XHR断点</td><td align="left">调试URL包含指定字符串的请求</td></tr><tr><td align="left">事件监听断点</td><td align="left">调试指定元素事件触发逻辑时使用事件委托时使用比较麻烦推荐配合黑盒 + 无痕模式使用</td></tr><tr><td align="left">错误断点</td><td align="left">调试错误捕获时使用一般只用于调试未捕获的错误</td></tr></tbody></table><h5>代码行断点</h5><p>通过问题复现能推测出<strong>明确的调试代码范围</strong>时使用。<br><img src="https://upload-images.jianshu.io/upload_images/3116808-263e1ed5d8693428.gif?imageMogr2/auto-orient/strip" alt="lineBreakpoint.gif" title="lineBreakpoint.gif"><br>在代码编辑面板的行号上右键或者直接点击该行号,即可添加代码行断点。</p><h5>代码行条件断点</h5><p>在有中介统筹分配处理逻辑或条件分支时使用,排除其它逻辑的干扰,聚焦关注点。</p><p><img src="https://upload-images.jianshu.io/upload_images/3116808-115fb9ed345aeb2e.gif?imageMogr2/auto-orient/strip" alt="conditionalBreakpoint.gif" title="conditionalBreakpoint.gif"><br>由上图可知,在代码编辑面板的行号上右键,选择Add Conditional Breakpoint...,即可添加代码行条件断点。</p><p>添加条件断点时,如果条件为true时,拦截代码执行,条件为false时,不会拦截。</p><p>在输入框至少有一个为空时,断点有效,输入框全不为空时,断点无效。</p><h5>代码行日志断点</h5><p>无需侵入源代码,实现非侵入式的日志打印<br><img src="https://upload-images.jianshu.io/upload_images/3116808-0ec21f48761d20e9.gif?imageMogr2/auto-orient/strip" alt="logBreakpoint.gif" title="logBreakpoint.gif"><br>如上图,输入的格式是console.log函数的参数形式。</p><p><img src="https://upload-images.jianshu.io/upload_images/3116808-5459703e7ac669fd.gif?imageMogr2/auto-orient/strip" alt="logtable.gif" title="logtable.gif"></p><p>如上图,可以显式地使用console对象的其它方法打印日志</p><h5>DOM断点</h5><p>详情参见Elements篇</p><p><img src="https://upload-images.jianshu.io/upload_images/3116808-3ea4f7d618830768.gif?imageMogr2/auto-orient/strip" alt="domBreakpoint.gif" title="domBreakpoint.gif"></p><h5>XHR断点</h5><p><img src="https://upload-images.jianshu.io/upload_images/3116808-103697c8f06ad737.gif?imageMogr2/auto-orient/strip" alt="xhrBreakpoint.gif" title="xhrBreakpoint.gif"><br>随手找个请求,添加到XHR断点,重载页面,在请求send的时候,会拦截代码逻辑,可以查看相关的请求参数。</p><h5>事件监听断点Event Listener Breakpoints</h5><p>若对代码不熟悉或在大长篇代码逻辑中,只是知道点击触发业务处理逻辑时,可以考虑事件监听器断点。</p><p>然而,在复杂的事件委托中,是一个噩梦。<br><strong>Note:</strong>无痕模式下使用,浏览器扩展程序会产生干扰。</p><p><img src="https://upload-images.jianshu.io/upload_images/3116808-834582b83e9e88c9.gif?imageMogr2/auto-orient/strip" alt="eventBreakpoint.gif" title="eventBreakpoint.gif"></p><h5>错误断点</h5><p>开启错误断点,默认会拦截未处理的错误逻辑。</p><p>若选择<code>Pause on caught exceptions</code>,则捕获的错误逻辑也会被拦截。</p><p><img src="https://upload-images.jianshu.io/upload_images/3116808-3a1b3e8b5db591ac.gif?imageMogr2/auto-orient/strip" alt="errorBreakpoint.gif" title="errorBreakpoint.gif"></p><h4>黑盒(Ignore list)</h4><p>黑盒(旧版叫黑盒,新版浏览器改名为忽略列表)是一小功能,单独拉出来讲是因为不适合和其它分类合并。</p><p>使用该功能可以聚焦关注点,排除<strong>非核心或可信任代码</strong>的干扰。</p><p><img src="https://upload-images.jianshu.io/upload_images/3116808-98f512d8d0852f59.gif?imageMogr2/auto-orient/strip" alt="blackbox.gif" title="blackbox.gif"></p><p>因为示例是一个很简单地例子,这里借助<code>Event Listener Breakpoints</code>讲述该功能地使用。</p><p>在Click事件上添加断点,点击<code>Add Number1 and Number2</code>按钮,会在函数<code>onClick</code>地首行进行执行拦截,OK!</p><p>重载页面,在代码编辑面板打开可信任的代码,右键添加<code>Add script to ignore list</code>,再次点击<code>Add Number1 and Number2</code>按钮,会发现代码逻辑完全执行结束,没有拦截。</p><p>上述场景对比,我们可以意识到,黑盒帮助我们聚焦相关逻辑代码,跳过一些可信任(认为不会出现Bug)的库,如:<code>jQuery?</code>在一步步调试时,不会进入<code>jQuery</code>库调试,聚焦我们自己的逻辑。</p><h4>代码覆盖率</h4><p>可以帮助我们查找无效(Never)代码。</p><p><img src="https://upload-images.jianshu.io/upload_images/3116808-dd1338e1cb3dd730.gif?imageMogr2/auto-orient/strip" alt="coverage.gif" title="coverage.gif"></p><p>通过代码编辑面板右下角的<code>Coverage</code>,我们可以查看代码的覆盖率。</p><p>如上图,页面初始化执行时,代码未覆盖率为41.9%,即有41.9%的代码在页面初始化时未执行。</p><p>点击<code>Add Number1 and Number2</code>按钮,执行了输入校验相关的代码逻辑,代码未覆盖率为17.3%,即仍有17.3%的代码未执行。</p><p>输入输入,点击<code>Add Number1 and Number2</code>按钮,执行了计算相关的代码逻辑,代码未覆盖率为0%,即代码逻辑全部执行。</p><p>某种程度上可以帮助我们查找Never无效代码。</p><h4>面板布局3:调试面板</h4><h5>单步执行</h5><p>在调试代码的过程中,我们需要控制代码的执行:<br><img src="https://upload-images.jianshu.io/upload_images/3116808-609e605415dfb139.gif?imageMogr2/auto-orient/strip" alt="steps.gif" title="steps.gif"></p><p>如上图,添加三个断点,追加一个日志。</p><p>触发事件,点击<code>Resume script execution</code>,会恢复代码的执行,直至遇到下一个断点。</p><p>若在拦截时,长按<code>Resume script execution</code>,选择下拉按钮的<strong>第一项</strong>,会跳过后续的断点,将代码执行完整,可以看到日志执行了一次。</p><p>若在拦截时,长按<code>Resume script execution</code>,选择下拉按钮的<strong>第二项</strong>,会在拦截处强制停止后续代码执行,可以看到后续逻辑中的日志没有执行。</p><p>重载页面,使页面恢复到<strong>初始状态</strong>。</p><p><img src="https://upload-images.jianshu.io/upload_images/3116808-3a7f0a0a2ac941ec.gif?imageMogr2/auto-orient/strip" alt="resume.gif" title="resume.gif"></p><p>调试面板最上排的工具控制断点拦截时代码的执行,<code>Resume</code>按钮恢复代码的执行,直至遇到下一个断点。</p><p>若断点所在代码行的表达式是一个函数,<code>step over</code>按钮会<strong>跳过</strong>函数的内部执行,如图第15行(15L),下一步直接执行到了16L。</p><p>若断点所在代码行的表达式是一个函数,<code>step into</code>按钮会<strong>深入</strong>函数的内部执行,如图第15行(15L),下一步进入了函数内部,执行到了22L。</p><p>执行到了22L后,若不想再调试该函数<code>inputsAreEmpty</code>,可以通过<code>step out</code>跳出函数的执行,如下图,下一步直接跳出当前函数的执行,执行到16L。</p><p><img src="https://upload-images.jianshu.io/upload_images/3116808-f6363d21ddf551b0.gif?imageMogr2/auto-orient/strip" alt="stepinto.gif" title="stepinto.gif"></p><p>思考:</p><ul><li>若在22L,点击了<code>Step into</code>按钮,下一步会执行到哪?</li><li>若在22L,点击了<code>Step through</code>按钮,下一步会执行到哪?</li></ul><p><img src="https://upload-images.jianshu.io/upload_images/3116808-507d5ebdb08ad951.gif?imageMogr2/auto-orient/strip" alt="step.gif" title="step.gif"></p><p>最后看一下<code>step</code>按钮,<code>step</code>按钮是实名的老实人,遇山爬山,遇海潜海,逻辑中的每一步都会走到,遇见函数就会深入函数代码执行每一步。</p><p>若逻辑中一个函数都没有,<code>step over</code>、<code>step into</code>、<code>step out</code>、<code>step</code>的行为和<code>step</code>保持一致。</p><h5>禁用(Deactivate)、停用(disable)断点</h5><p>看下图,分别执行了禁用、停用,看明白了么?</p><p><img src="https://upload-images.jianshu.io/upload_images/3116808-61a7be870a28f1df.gif?imageMogr2/auto-orient/strip" alt="deactivate.gif" title="deactivate.gif"></p><p>上图,加入了两种类型的断点,第一次正常执行,三个断点都会执行的到。</p><p>第二次禁用了断点,点击<code>Add Number1 and Number2</code>,代码执行完毕,断点都没有拦截。</p><p>第三次激活断点(还原禁用),停用了断点,点击<code>Add Number1 and Number2</code>,代码拦截在了Click事件处理逻辑的首行,后续断点并没有拦截。</p><p>迷糊了?</p><p>综述:<br>停用是有作用域的,停用的只是代码行断点,其它类型的断点并没有停用。而禁用是全部类型的断点都不会触发。</p><h5>监听</h5><p>重载页面,移除所有的断点。</p><p><img src="https://upload-images.jianshu.io/upload_images/3116808-7ece5b0b3e2e8187.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" alt="watch.png" title="watch.png"></p><p>可以增加表达式在Watch中,监听执行各时间点的值。</p><p><strong>函数调用</strong>的监听会影响代码逻辑的执行,如上图中的 <code>inputsAreEmpty()</code>作为Watch表达式,Watch在需要更新展示的时候(时机:代码执行到表达式所在所用域),Chrome会自动调用该函数来获取函数最新返回值。</p><p>如果在 <code>inputsAreEmpty()</code> 的函数逻辑中添加断点,就会无限循环。</p><ul><li><p>在 <code>inputsAreEmpty()</code> 代码行处添加代码行断点,函数体会执行三次</p><ul><li>第一次,到达作用域时的初始值</li><li>第二次<code>inputsAreEmpty()</code> 本身执行</li><li>第三次 <code>inputsAreEmpty()</code>执行后,更新Watch表达式的值</li><li>因为<code>inputsAreEmpty()</code> 函数体内无断点,更新Watch表达式后,再也没有其它时机更新表达式</li></ul></li><li><p>在 <code>inputsAreEmpty()</code>函数体内添加代码行断点,函数体无限执行</p><ul><li>第一次 <code>inputsAreEmpty()</code>在执行作用域初始化</li><li>第二次执行<code>inputsAreEmpty()</code></li><li>第三次执行 <code>inputsAreEmpty()</code> 后,更新Watch</li><li>第四次更新Watch执行<code>inputsAreEmpty()</code></li><li>第五次执行<code>inputsAreEmpty()</code>后,更新Watch</li><li>第六次...</li></ul></li></ul><p>所以,<strong>不推荐在Watch表达式中添加函数调用</strong>。</p><h5>作用域</h5><p><img src="https://upload-images.jianshu.io/upload_images/3116808-47e6f8890d7f9057.gif?imageMogr2/auto-orient/strip" alt="scope.gif" title="scope.gif"></p><p>在拦截代码执行时,我们可以在调试面板的Scope章节看到<strong>当前作用域</strong>Local的变量,如上图中的this。</p><p>继续执行代码到return 语句,可以在当前作用域Local看到返回值Return Value</p><p>双击作用域中的值,可以改变当前的值。</p><p>Global作用域可以看到全局作用域的变量、方法。</p><p><img src="https://upload-images.jianshu.io/upload_images/3116808-4a545725d1161acb.gif?imageMogr2/auto-orient/strip" alt="global.gif" title="global.gif"></p><p>默认状态下,Console面板只能访问到全局的变量、函数,而在断点拦截时,Console面板可以访问到当前作用域下的方法、变量。</p><h5>调用栈</h5><p>调用栈Call Stack,可以看到当前执行函数的来源,帮助我们溯源。</p><p><img src="https://upload-images.jianshu.io/upload_images/3116808-f21112cb4e7ac115.gif?imageMogr2/auto-orient/strip" alt="callstack.gif" title="callstack.gif"></p><p>在当前执行函数上右键,点击 restart frame,可以让当前函数的逻辑重新执行,在大块代码段调试中使用较为便利,<strong>仅限于断点执行到的函数</strong>,已经走过的函数无效。</p><h3>Filesytem面板</h3><p>浏览器即代码编辑器</p><p><img src="https://upload-images.jianshu.io/upload_images/3116808-994b1c3d4b9e96be.gif?imageMogr2/auto-orient/strip" alt="filesystem.gif" title="filesystem.gif"></p><p>如上图,在本地启动服务打开某静态页面,在Sources面板下的Filesystem面板添加静态项目到workspaces。</p><p>通过Elements面板定位锚点到某DOM节点,在Styles面板直接调试样式,重载页面后,该样式依旧有效。</p><p>通过查看样式源码,可以发现Styles面板中调试的样式已经保存到磁盘覆盖原有的样式。</p><h4>适用场景</h4><ul><li><p>静态页面的CSS、JavaScript</p><ul><li><p>上图我们可以看到Filesystem中的一些文件的文件名有绿色小点</p><ul><li>绿点表明浏览器打开的该文件已经与本地磁盘建立连接</li><li>在浏览器调试修改文件可以直接映射到本地磁盘</li></ul></li></ul></li><li><p>静态页面的HTML通过Elements面板右键 eidt HTML 无法保存磁盘</p><ul><li>Elements面板映射的是DOM Tree</li><li>DOM Tree的产生受 HTML、CSS(content样式属性)、JavaScript动态处理的影响,所以,浏览器无法将DOM Tree的改变映射到对应的位置</li></ul></li></ul><p><strong>【推荐】</strong>在结构、展现、动效(HTML、CSS、JavaScript)分离的静态页面项目中快速调试使用。</p><h4>无效场景</h4><ul><li><p>使用构建工具打包的项目</p><ul><li>即使使用sourceMap,复杂的文件映射关系使得浏览器和本地磁盘建立薄弱的连接</li><li>连接不可靠</li></ul></li></ul><h3>Snippets面板</h3><p>Snippets面板为我们提供跨标签、跨域名的Javascript测试性功能。</p><p><img src="https://upload-images.jianshu.io/upload_images/3116808-e93480212273229d.gif?imageMogr2/auto-orient/strip" alt="snippet.gif" title="snippet.gif"></p><p>上图附了一段检测浏览器类型的Javascript脚本, Ctrl + S 保存后,可以打开其它页签,在Snippets面板中仍然能看到该脚本。</p><p>点击右下角的执行按钮或通过快捷键 Ctrl + Enter 执行这段脚本,可以在下方的Control面板看到执行打印出来的日志。</p><table><thead><tr><th align="left">Snippets</th><th align="left">Console</th></tr></thead><tbody><tr><td align="left">跨标签页可用</td><td align="left">当前标签页可用</td></tr><tr><td align="left">永久保存,除非手动删除</td><td align="left">页面重载后清除</td></tr></tbody></table>
Git提交信息要满足的格式
https://segmentfault.com/a/1190000040210244
2021-06-21T14:16:46+08:00
2021-06-21T14:16:46+08:00
米花儿团儿
https://segmentfault.com/u/mihuartuanr
0
<h2><code>Git</code>提交信息的格式要满足如下<code>/^(revert: )?(feat|fix|docs|dx|style|refactor|perf|test|workflow|build|ci|chore|types|wip|dep)(\(.+\))?: .{1,50}/</code>正则表达式</h2><p><img src="/img/bVcSSGG" alt="regex" title="regex"></p><h3>示例</h3><ul><li><p>如下提交信息出现在<code>Features</code>大标题,<code>compiler</code>次标题下:</p><pre><code>feat(compiler): add 'comments' option</code></pre><p>展示如下:</p><pre><code>Features
compiler: add 'comments' option</code></pre></li><li><p>如下提交信息出现在<code>Bug Fixes</code>大标题,<code>v-model</code>次标题下,跟随着<code>#28</code>问题的链接:</p><pre><code>fix(v-model): handle events on blur
close #28</code></pre></li><li><p>如下提交信息出现在<code>Performance Improvements</code>大标题,<code>core</code>次标题下,<code>BREAKING CHANGE</code>下一行跟随着中断性变更的理由:</p><pre><code>perf(core): improve vdom diffing by removing 'foo' option
BREAKING CHANGE: The 'foo' option has been removed.</code></pre></li><li><p>如果以下提交和提交667ecc1在同一版本下,则该提交信息不会出现在变更日志中。如果没有在同一版本下,该提交将出现在“Reverts”大标题下</p><pre><code>revert: feat(compiler): add 'comments' option
This reverts commit 667ecc1654a317a13331b17617d973392f415f02.</code></pre></li></ul><h3>完整提交信息的格式</h3><p>提交信息包含标题行、主体内容和页脚注释,其中,标题行包含修改类型<code>type</code>、修改影响的范围(作用域)<code>scope</code>、修改内容概述<code>subject</code>。</p><pre><code><type>(<scope>): <subject>
<BLANK LINE>
<body>
<BLANK LINE>
<footer></code></pre><p>以上,首行是标题行,标题行是必填的,标题行的作用域<code>scope</code>是可选的。</p><h4><code>Revert</code></h4><p>如果某个提交回滚到上一次提交,提交信息应该以<code>revert:</code>开始,后续是该提交的标题行。</p><p>主体内容应该写<code>"This reverts commit <hash>"</code>,其中,<code><hash></code>指向要回滚的提交。</p><h4><code>Type</code></h4><p>如果提交信息的标题行前缀是<code>feat</code>、<code>fix</code>或<code>perf</code>,则该提交信息会出现在更新日志中。其中,中断性变更永远都会出现在更新日志中,不论标题行前缀是什么。</p><p>其它前缀用哪个,需要根据情况自行酌办。推荐使用的前缀<code>docs</code>, <code>chore</code>, <code>style</code>, <code>refactor</code>和<code>test</code>,这些前缀的提交信息不会出现在更新日志中。</p><h4><code>Scope</code></h4><p>作用域可以是任何内容。</p><p>如文章开头示例中的<code>core</code>,<code>compiler</code>, <code>ssr</code>, <code>v-model</code>, <code>transition</code>...</p><h4><code>Subject</code></h4><p>概述是当前提交的简要描述</p><ul><li>使用单数、现在时,如“change",而不是"changed"过去式,也不是"changes"。</li><li>首字母不要大写</li><li>不要以<code>"."</code>结束</li></ul><h4><code>Body</code></h4><p>和<code>Subject</code>一样,使用单数、现在时,如“change",而不是"changed"过去式,也不是"changes"。</p><p>主体内容应该包括更新的动机,以及这次提交与以前的提交的对比。</p><h4><code>Footer</code></h4><p>页脚注释应该包含关于中断性变更的任何信息,也是标注此提交关闭了哪条GitHub问题的地方。</p><p>中断性变更应该以单词<code>BREAKING CHANGES: </code>开头,<code>":"</code>后跟随一个空格或两个换行符。紧随`BREAKING CHANGES: 后的其余提交消息将用于标注解决了什么问题。</p>
自定义NPM包
https://segmentfault.com/a/1190000040125015
2021-06-05T09:00:50+08:00
2021-06-05T09:00:50+08:00
米花儿团儿
https://segmentfault.com/u/mihuartuanr
0
<h2>环境初始化</h2><ol><li><code>mkdir npm-log</code></li><li><code>cd npm-log</code></li><li><code>npm init -y</code></li></ol><h2>入口文件</h2><ul><li><p>自定义依赖模块:</p><ul><li><p>模块是在 <code>package.json</code> 里通过 <strong><code>main</code></strong> 字段定义这个包对外暴露的入口;</p><ul><li>模块起源于<code>node</code>,语法默认支持<code>commonjs</code>规范</li><li>模块若使用<code>ES Module</code>语法书写,通过 <strong><code>module</code></strong> 字段定义入口(需要打包工具配合使用)</li></ul></li></ul></li><li><p>自定义命令行:</p><ul><li>如果是提供命令行工具,则需要通过 <strong><code>pkg#bin</code></strong> 字段来定义暴露的命令名称与实际执行的文件</li></ul></li></ul><p>这篇文章讲述自定义依赖模块的声明,后续会有专门篇幅进行<a href="https://segmentfault.com/a/1190000040817687">自定义命令行的讲述</a>。</p><h3>声明示例</h3><ol><li><p>创建<code>lib/index.js</code></p><pre><code>const Noop = () => {}
class Logmi {
errorHandler = Noop
successHandler = Noop
static create (options) {
return new Logmi(options)
}
constructor (options = {}) {
console.log('---------create------', options)
}
log (msg, level) {
console.log('log: start', msg, level)
}
}
module.exports = Logmi</code></pre></li><li><p>更新<code>package.json</code></p><pre><code> "main": "lib/index.js"</code></pre></li></ol><h2>开发环境</h2><ul><li>[ ] 自动日志</li><li>[ ] 版本更新</li><li><p><a href="https://link.segmentfault.com/?enc=%2FmWr%2BoZxdHyRpiKU148pSQ%3D%3D.co5JbQS78QrRzXFI3FHOFGiO5TOQx4lbf6r6qOIaC1Hq%2FGLIL%2BjkTf7zJ7o7ykbk" rel="nofollow"><code>husky</code></a></p><pre><code> npm install husky --save-dev
npx husky install</code></pre><ul><li><p>配置<code>run-script</code>:安装依赖后自动启动<code>Git hooks</code></p><pre><code>"prepare": "husky install"</code></pre><ul><li><p>追加测试钩子</p><pre><code> # Unix系统可用
npx husky add .husky/pre-commit "npm run test"
# Windows通过以下命令创建文件(引号在windows下不是正确的语法)
npx husky add .husky/pre-commit
# Windows去新建的文件中指定命令
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npm run test</code></pre></li></ul></li></ul></li><li><p><code>commitlint</code></p><ul><li><code>commitlint</code>提交信息校验工具</li><li>需要和校验规范配合使用,官网默认规范<code>@commitlint/config-conventional</code> —— 可自定义。</li><li><p><code>commitlint</code>绑定<code>@commitlint/config-conventional</code></p><pre><code> npm i -D commitlint @commitlint/config-conventional
# Unix
echo "module.exports = {extends: ['@commitlint/config-conventional']}" > commitlint.config.js
# Windows
echo module.exports = {extends: ['@commitlint/config-conventional']} > commitlint.config.js</code></pre></li><li><p>配置<code>Git hook</code>:在提交<code>commit msg</code>进行参数校验 —— <strong>写在<code>run-script</code>中无效</strong></p><pre><code># Unix系统可用
npx husky add .husky/commit-msg "npx --no-install commitlint --edit $1"
# Windows通过以下命令创建文件(引号在windows下不是正确的语法)
npx husky add .husky/commit-msg
# Windows去新建的文件中指定命令
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npx --no-install commitlint --edit $1</code></pre></li></ul></li><li><p><code>standard-version</code></p><pre><code> npm i --save-dev standard-version</code></pre><ul><li><p>配置<code>run-script</code>:发布前自动升级版本号 + 生成日志</p><pre><code>"prepublishOnly": "standard-version"</code></pre></li></ul><p><strong>注意:</strong>这里不要使用<code>prepublish</code>钩子,该钩子在<a href="https://link.segmentfault.com/?enc=3fqggdl1kF60fU5VCzq1Bg%3D%3D.GcWR2jc67Fttp0eMRJMEpKNJ8FmHP%2F6cfAZa55R1XjaX5RYWES2WBx0OvD7bTI7Nos71TZfLw3Dr9zyjUYAYf3%2FS6d4QXB%2BKctyXyCOWMrc%3D" rel="nofollow"><code>npm i</code>时运行</a>,而不是<code>npm publish</code>时运行。</p></li></ul><h2>调试</h2><ol><li><p>进入本地<code>NPM</code>包</p><ul><li><code>npm link</code>创建软链接到全局<code>node</code>环境中</li></ul></li><li><p>进入依赖包的项目A中</p><ul><li><code>npm link <packageName></code>建立软链接依赖</li></ul></li><li><p>在项目A需要调用的文件中</p><pre><code># 调用
import Logmi from "log";
const LogmiInstance = Logmi.create({
url: 'http://localhost:3000'
})
LogmiInstance.log(`paste: ${JSON.stringify(paste)}`, 1)</code></pre></li><li>启动项目A,即可调试</li></ol><h2>开发</h2><p><code>NPM</code>包是<code>commonJS</code>语法,使用<code>require()</code>,而非<code>import...from...</code>引入依赖。</p><ul><li><p>实例化参数</p><ul><li>工厂函数</li><li><p>参数默认值</p><ul><li><code>const Noop = () => {}</code>:空语句</li></ul></li><li><p>传参校验</p><ul><li>参数数据实体类型</li><li><p>校验警告</p><ul><li>参数合并</li></ul></li></ul></li></ul></li><li><p>DTO</p><ul><li>校验DTO组成结构参数<code>ParamChecker</code></li><li>统一结构<code>ContentWrapper</code></li></ul></li><li><p>配置信息统一分类处理</p><pre><code>module.exports = {
EXCEED_TRY_TIMES: 'Exceed try times',
}</code></pre></li></ul><h2>打包发布</h2><p>打包需要引入<code>webpack</code>,这里的<code>package.json</code>修改入口文件:</p><pre><code>"main": "dist/logmi.js",
"module": "lib/index.js",</code></pre><p>其中,<code>main</code>是暴露打包后的入口文件;<br><code>module</code>是<code>webpack</code>环境下暴露的入口文件;</p><h3><code>package.json</code></h3><pre><code>{
"name": "log",
"version": "1.0.0",
"description": "",
"main": "dist/logmi.js",
"module": "lib/index.js",
"scripts": {
"prepare": "husky install",
"build": "cross-env NODE_ENV=production webpack --config webpack.config.js --mode=production",
"test": "echo \"npm run test\" && exit 1"
},
"author": "",
"license": "ISC",
"devDependencies": {
"@babel/core": "^7.14.3",
"@babel/preset-env": "^7.14.4",
"@commitlint/cli": "^12.1.4",
"@commitlint/config-conventional": "^12.1.4",
"babel-loader": "^8.2.2",
"cross-env": "^7.0.3",
"husky": "^6.0.0",
"standard-version": "^9.3.0",
"terser-webpack-plugin": "^5.1.3",
"webpack": "^5.38.1",
"webpack-cli": "^4.7.0"
},
"dependencies": {
"@babel/polyfill": "^7.12.1",
"idb-managed": "^1.0.9"
},
"bundledDependencies": [
"idb-managed"
]
}</code></pre><h4><code>pkg#main</code></h4><p>作为第三方依赖包时,包的入口执行文件。</p><p>如果没有指定,默认为<code>root</code>目录下的<code>index.js</code>。</p><h4><code>pkg#bin</code></h4><p>作为命令行工具时,包的入口执行文件</p><p>安装该包时,<code>node</code>会自动创建硬链接该包到全局执行环境。</p><ul><li><code>String</code>:单执行文件</li><li><code>Map</code>:多执行文件</li></ul><h4><code>pkg#module</code></h4><p><strong>非官方配置</strong>,<code>rollup</code>、<code>webpack</code>等打包工具提供的配置项。</p><blockquote>指向的应该是一个基于<code>ES6</code>模块规范书写的模块。</blockquote><h4><code>pkg#private</code></h4><p>设置<code>"private": true</code>,<code>npm</code>拒绝发布该包。</p><h4><code>pkg#workspaces</code></h4><p>结合<code>monorepo</code>的概念,创建工作区。</p><h4><code>pkg#files</code></h4><p>安装该包时,目录中包含在<code>pkg.files</code>中指定的文件结构。</p><p>默认包含:</p><pre><code>package.json
README
CHANGES / CHANGELOG / HISTORY
LICENSE / LICENCE
NOTICE
The file in the "main" field</code></pre><h4><code>pkg#bundledDependencies</code></h4><p>通过<code>fpt</code>、<code>scp</code>等工具传输该包时,需要将该包的依赖打包在一起。</p><ul><li><p>在<code>bundledDependencies</code>中指定依赖包的列表</p><ul><li>只需要指定包名,版本会在<code>dependencies</code>查找</li></ul></li><li>通过<code>npm pack</code>打包</li><li>通过传输工具传输打好的<code>*.tgz</code>包</li><li>通过<code>npm i *.tgz</code>安装该包及其依赖</li></ul><p><strong>Note:</strong> 定义在<code>bundledDependencies</code>列表内的依赖,安装NPM包时,该依赖会嵌套在包文件下,而不会提升到<code>node_modules</code>目录的根下</p><h4><code>pkg#peerDependencies</code></h4><ul><li><p>表明该包对主包/主工具库的兼容性,而不是依赖性,这种关系称之为插件。</p><ul><li>主包一般会对插件暴漏的接口指定标准</li></ul></li></ul><p>在<code>peerDependencies</code>指定的<code>包@版本号</code>表明,我们的包需要在指定包的环境下执行,需要一同安装。</p><h3>打包</h3><h4>安装插件</h4><pre><code>npm i -D webpack-cli webpack cross-env terser-webpack-plugin</code></pre><pre><code>npm install --save-dev @babel/core babel-loader @babel/preset-env
npm install --save @babel/polyfill</code></pre><h4>配置<code>babel.config.json</code></h4><pre><code>{
"presets": [
[
"@babel/env",
{
"targets": {
"edge": "17",
"firefox": "60",
"chrome": "67",
"safari": "11.1"
},
"useBuiltIns": "usage",
"corejs": "3.6.5"
}
]
]
}</code></pre><h4>配置<code>run-script</code></h4><pre><code>...
"build": "cross-env NODE_ENV=production webpack --config webpack.config.js --mode=production",
...</code></pre><h4><code>webpack.config.js</code></h4><pre><code>const path = require('path')
const webpack = require('webpack')
const TerserPlugin = require("terser-webpack-plugin")
const resolve = dir => path.join(__dirname, '.', dir)
const isProd = process.env.NODE_ENV === 'production'
module.exports = {
entry: {
logmi: './lib/index.js'
},
output: {
path: resolve('dist'), // 输出目录
filename: '[name].js', // 输出文件
libraryTarget: 'umd', // 采用通用模块定义
library: 'logmi', // 库名称
libraryExport: 'default', // 兼容 ES6(ES2015) 的模块系统、CommonJS 和 AMD 模块规范
globalObject: 'this' // 兼容node和浏览器运行,避免window is not undefined情况
},
devtool: 'source-map',
module: {
rules: [
{
test: /\.js$/,
exclude: /(node_modules)/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
}
]
},
optimization: {
minimize: true,
minimizer: [new TerserPlugin()],
}
}</code></pre><h4>配置可见目录结构</h4><pre><code>...
"files": [
"dist/"
]
...</code></pre><h3>发布</h3><p>若使用<code>nrm</code>维护多个<code>npm</code>源,需要切换到<strong>发布的目标源</strong>。或者通过<code>pkg#publishConfig</code>、<code>.npmrc</code>指定目标源。</p><p>以<code>npm</code>为例:</p><ol><li><p>切换到<code>npm</code>源</p><pre><code> nrm use npm</code></pre></li><li>在<a href="https://link.segmentfault.com/?enc=qkCz0r5SwoKsRkiS%2BvjNvA%3D%3D.lEAyO9u6r%2BB%2BvdJ1MVQYdBD6PF9Ml1PAhJHyyTG%2FAig%3D" rel="nofollow">NPM官网</a>注册账号</li><li><p>命令行中登录用户</p><pre><code> npm login</code></pre></li><li><p>在项目目录下发布包</p><pre><code> npm publish</code></pre></li></ol><h2>补充知识</h2><ul><li><p>[ ] <a href="https://link.segmentfault.com/?enc=YI0hDHrA2zIbTCj2FOcx0g%3D%3D.ldERh3gEzYnAwoxts0Ne6YuBP78IKT%2FY3ihnxCjG2cqVsPniSf8m0xcARe%2FEcLq2ihD9TdHKYBqaMt2QWiT1c21a1afD647LHZe0jVsN3zXLR0MfZSIBay2qFw08ZzLXzDY1RrOJ4Hg5zmCZmuEa3g%3D%3D" rel="nofollow"><code>Peer Dependencies</code></a></p><blockquote>The peerDependencies configuration was originally designed to address the problem of NPM packages that were ‘plugins’ for other frameworks.</blockquote></li></ul><h2>删除CHangeLog</h2><ul><li><a href="https://link.segmentfault.com/?enc=N2eGgRRcpTyjP9qt%2Fo7rLw%3D%3D.l56jkuZtGyaiQK2NbrP0lKyuVZ39%2Be8cPjvWDwmo7utmqGbL7MwPf6vVG9hJAniqWlQbdO3niEllfR%2Bl%2BTX5%2BKToHKoDPC9ntN0NdigzCmlVVok5PIXjeK%2B2YqPSYVd9" rel="nofollow">https://github.com/convention...</a></li><li><a href="https://link.segmentfault.com/?enc=wYtRD1e4%2FGcnrKo%2Fld8cCQ%3D%3D.mH0L1bUy11f73u5fqSt6r5T74K36JgRyJGcky5uiAORzSVAYEfpUj3lRG7ksGaiMuorACyuOnE78K%2FXMJj1YsNwKtuR1SaVMlf1fInE5kgkkpbg2LMFiBe2u%2FnsCkKnN" rel="nofollow">https://lukasznojek.com/blog/...</a></li></ul>
Chrome Devtools: Elements篇
https://segmentfault.com/a/1190000039962433
2021-05-08T01:13:35+08:00
2021-05-08T01:13:35+08:00
米花儿团儿
https://segmentfault.com/u/mihuartuanr
0
<h2>概述</h2><p>Elements面板用于调试页面布局,即DOM、CSS的准确性。<br>最佳实践:所见即所得。</p><h2>演示前置</h2><h3>环境</h3><ul><li>Chrome浏览器</li><li>版本 90.0.4430.93</li></ul><h3>操作释义</h3><ul><li><p>聚焦控制台</p><ul><li>鼠标在控制台范围内点击一下,使后续操作上下文绑定在控制台中。</li></ul></li></ul><h2>打开控制台</h2><p>以<a href="https://link.segmentfault.com/?enc=oxzLFzuHpQvpDbR1%2BDaY8Q%3D%3D.%2BJ1HFTfSOZKXXOkk8MPYO%2Fy7TA01CG%2Bcpn46p08pNJU%3D" rel="nofollow">掘金</a>为示例讲述:</p><p><img src="/img/remote/1460000039962435" alt="Layout.jpeg" title="Layout.jpeg"></p><p>通过链接打开页面,通过F12或鼠标右键【检查】打开开发者工具控制台。</p><h2>默认布局</h2><ol><li><p>布局渲染</p><ul><li>Render Tree的视觉表现</li></ul></li><li><p>Elements面板</p><ul><li>DOM Tree折叠形式展示</li></ul></li><li><p>Styles面板</p><ul><li>CSSOM汇总</li></ul></li><li><p>Console面板</p><ul><li>默认隐藏,键盘 Esc 键切换展现状态</li></ul></li></ol><h2>Elements面板</h2><h3>锚点</h3><h4>执行步骤</h4><p><img src="/img/remote/1460000039962436" alt="anchor.jpeg" title="anchor.jpeg"></p><ol><li>选择如图红色标记工具点击<a href="https://link.segmentfault.com/?enc=x6Xvi1pZW3OUj6rqt9Gh1A%3D%3D.BLaxPMeBvGEKVtUmQXCtYz4gKY73W5HzXsNv9i4Uwt0%3D" rel="nofollow">掘金</a>的页面菜单,可以看到Elements面板聚焦到选中展示对应的DOM结构。</li><li>在对应的DOM结构后,有 <code>$0</code> 的特殊标识,可以在 Console 面板直接使用 <code>$0</code> 引用当前选中元素</li></ol><h4>应用场景</h4><ul><li>检查动态插入的DOM树是否正确</li><li>检查动态编辑的节点属性是否正确</li></ul><h3>嵌套结构</h3><h4>执行步骤</h4><p><img src="/img/remote/1460000039962437" alt="ancestors.jpeg" title="ancestors.jpeg"></p><ol><li>通过锚点工具选中某DOM元素</li><li>在Elements面板底部,展现当前选中元素的祖先选择器</li><li>点击对应的选择器,Elements面板聚焦对应的DOM元素</li></ol><h4>应用场景</h4><ul><li>检查元素嵌套关系</li></ul><h3>响应式布局</h3><h4>执行步骤</h4><p><img src="/img/remote/1460000039962438" alt="Responsive.jpeg" title="Responsive.jpeg"></p><ol><li>通过按钮1开启响应视图模式</li><li>自定义或使用预置视图尺寸</li><li>定义网络状态</li><li>横竖屏切换</li><li>更多细节展示</li></ol><h4>应用场景</h4><ul><li>Mobile H5开发:不同手机尺寸下的布局适配调试</li><li>PC Web开发:在不同断点范围内的布局差异化展示调试</li><li>PC & Mobile开发:不同视图模式下的适配调试</li><li>网络异常状态:无网异常模拟</li></ul><h3>Flex布局</h3><p>如果足够仔细,可以看到选中页面菜单对应的DOM结构中,有一个差异化的展示形式:</p><p><img src="/img/remote/1460000039962439" alt="flex.jpeg" title="flex.jpeg"></p><h4>执行步骤</h4><ol><li>聚焦控制台, <code>Ctrl + P</code> 输入 <code>> experiments</code> 选中 <code>Settings Show Experiments</code> 回车 或 直接点击位置2,选择 <code>Experiments</code></li><li>开启Flex调试</li></ol><p><img src="/img/remote/1460000039962440" alt="Experiments.jpeg" title="Experiments.jpeg"></p><ol><li>点击 Elements 面板中DOM结构后的 <code>flex</code> 标识,当前页面渲染会高亮展示Flexbox中的子项布局</li></ol><p><img src="/img/remote/1460000039962441" alt="flex_active.jpeg" title="flex_active.jpeg"></p><ol><li>点击 Styles 面板中对应样式后的标识,可以快捷修改当前Flexbox的布局属性</li></ol><p><img src="/img/remote/1460000039962442" alt="flex-modify.jpeg" title="flex-modify.jpeg"></p><h4>应用场景</h4><ul><li>检查Flexbox布局是否正确</li><li>快速调试Flexbox布局</li></ul><h3>Styles面板</h3><h4>样式来源</h4><p><img src="/img/remote/1460000039962443" alt="styles.jpeg" title="styles.jpeg"></p><ul><li><p>用户代理内置的样式</p><ul><li>浏览器默认样式</li></ul></li><li><p>站点开发者定义的样式</p><ul><li>网站开发者内联、外联样式</li></ul></li><li><p>站点用户指定的样式</p><ul><li>用户在控制台编写的样式</li><li>用户通过抓包工具重定向的样式</li><li>用户通过浏览器插件(如: Stylus)指定的样式</li></ul></li></ul><h4>样式声明</h4><p>在Styles面板展现的样式声明是默认状态下的声明样式,即页面初始化渲染后的样式,不包含交互状态样式。</p><h4>Computed面板</h4><p>该面板汇总浏览器最终应用到元素上的样式。</p><h3>锁定交互伪类</h3><h4>案例场景</h4><p>鼠标在<a href="https://link.segmentfault.com/?enc=Ckr0biBtsJRkpVZb2bc4HQ%3D%3D.luwWb%2BInB84En5PUJTED4htUOYoKocRsO2QzDvL91gk%3D" rel="nofollow">掘金</a>页面菜单悬停时,可以看到菜单文字样式发生变化,如何调试悬停样式?</p><p><img src="/img/remote/1460000039963486" alt="style_hover.gif" title="style_hover.gif"></p><h4>执行步骤</h4><ol><li>通过锚点工具聚焦当前变化的DOM节点</li><li>通过Styles面板的 <code>:hov</code> 按钮切换节点交互状态</li><li>即可在Styles面板查看交互状态下的样式</li></ol><p><img src="/img/remote/1460000039963487" alt="debug_hover.gif" title="debug_hover.gif"></p><h4>应用场景</h4><ul><li>调试通过伪类产生交互样式的场景</li></ul><h3>动态修改元素样式类名</h3><h4>执行步骤</h4><p><img src="/img/remote/1460000039962446" alt="classList.jpeg" title="classList.jpeg"></p><ol><li>点击 <code>.cls</code> 按钮</li><li>通过点击class复选框动态增删类名</li><li>可以看到删除 <code>active</code> 类名时,样式发生改变</li><li>在其它同级节点上添加 <code>active</code> 类名,验证猜想</li></ol><p><img src="/img/remote/1460000039963488" alt="classList-modify.gif" title="classList-modify.gif"></p><h4>应用场景</h4><ul><li>调试在条件逻辑场景中,通过动态增删类名改变的样式</li></ul><h3>用户自定义样式</h3><h4>执行步骤</h4><ol><li>选中DOM元素</li><li>点击 <code>+</code> 按钮</li><li>在增加的选择器后添加样式声明</li></ol><p><img src="/img/remote/1460000039962448" alt="styles_custom.jpeg" title="styles_custom.jpeg"></p><h4>应用场景</h4><ul><li>调试样式时,编写实验性质的样式声明</li></ul><h3>DOM断点</h3><h4>案例场景</h4><p>Elements面板可以查看固定/动态插入的节点,现有这样一个场景:</p><blockquote>在维持某交互的状态下才会插入节点,取消该交互状态时,删除节点;</blockquote><p>这时,如何核对DOM节点的正确性呢?</p><p><img src="/img/remote/1460000039963489" alt="mouseenter.gif" title="mouseenter.gif"></p><h4>案例分析</h4><ul><li>初始化时,浮层DOM结构并不存在 —— 不是通过样式隐藏的结构,而是动态添加的</li><li>无法通过交互伪类调试</li><li>浮层DOM结构在元素范围悬浮时才展示,无法锁定该状态去查看浮层结构</li><li>聚焦父级DOM,通过Event Listeners面板可知父级DOM绑定鼠标事件 —— 代码逻辑生成浮层结构</li></ul><p><img src="/img/remote/1460000039962450" alt="eventListeners.jpeg" title="eventListeners.jpeg"></p><h4>执行步骤</h4><ol><li>聚焦动态添加元素的直接父级DOM</li><li>鼠标右键选中 <code>Break on</code> —> <code>subtree modifications</code></li></ol><p><img src="/img/remote/1460000039962451" alt="Dom Break.jpeg" title="Dom Break.jpeg"></p><ol><li>这时鼠标停留在父级节点时,会触发断点</li></ol><p><img src="/img/remote/1460000039962452" alt="stepNext.jpeg" title="stepNext.jpeg"></p><ol><li>通过F10执行下一步,直至浮层展示出来</li></ol><p><img src="/img/remote/1460000039962453" alt="layer.jpeg" title="layer.jpeg"></p><ol><li>通过锚点工具聚焦浮层结构,即可检阅当前DOM结构的正确性</li></ol><p><img src="/img/remote/1460000039962454" alt="anchors.jpeg" title="anchors.jpeg"></p><h4>应用场景</h4><ul><li>调试在代码逻辑中,通过交互事件切换DOM结构的应用场景</li></ul><h4>触发时机</h4><p><img src="/img/remote/1460000039962455" alt="Break Types.jpeg" title="Break Types.jpeg"></p><ul><li><code>Break on</code> 二级菜单是复选框,意味着不同时机触发断点</li><li><p><code>subtree modifications</code></p><ul><li>仅在子元素增删时触发断点</li></ul></li><li><p><code>attribute modifications</code></p><ul><li>仅聚焦的DOM节点属性变化时触发断点</li></ul></li><li><p><code>node removal</code></p><ul><li>仅聚焦的DOM节点被移除时触发断点</li></ul></li><li><p><code>subtree modifications</code> & <code>attribute modifications</code></p><ul><li>子元素改变(增删、属性变化)时触发断点</li></ul></li><li><p><code>attribute modifications</code> & <code>node removal</code></p><ul><li>聚焦的DOM节点发生变化(属性变化、被移除)时触发断点</li></ul></li></ul><h4>断点管理</h4><ul><li>DOM Breakpoints面板管理当前页面的断点</li></ul><p><img src="/img/remote/1460000039962456" alt="Management.jpeg" title="Management.jpeg"></p><h3>DOM快照</h3><h4>执行步骤</h4><p><img src="/img/remote/1460000039962457" alt="Screenshot.jpeg" title="Screenshot.jpeg"></p><ol><li>通过锚点工具选中对应的DOM元素</li><li>右键选择 <code>Capture node screenshot</code></li><li>浏览器将当前DOM元素渲染结构以图片的形式下载</li></ol><h4>应用场景</h4><ul><li>获取快照</li></ul><h3>滚动到视图范围</h3><h4>执行步骤</h4><p><img src="/img/remote/1460000039962458" alt="ScrollIntoView.jpeg" title="ScrollIntoView.jpeg"></p><ol><li>在Elements面板选择一个视窗内不可见的DOM元素</li><li>右键选择 <code>Scroll into view</code></li><li>浏览器将滚动页面直至当前DOM元素出现在视窗内</li></ol>
NPM工程化 & inquirer源码解析
https://segmentfault.com/a/1190000039847417
2021-04-19T10:10:02+08:00
2021-04-19T10:10:02+08:00
米花儿团儿
https://segmentfault.com/u/mihuartuanr
0
<h2>从<code>npm run-script</code>应用开始</h2><p>查看某些NPM包的<code>npm_package_scripts</code>,经常可以看到一下<code>run-script</code>示例:</p><pre><code>...
"scripts": {
"prerelease": "npm test && npm run integration",
"release": "env-cmd lerna version",
"postversion": "lerna publish from-git",
"fix": "npm run lint -- --fix",
"lint": "eslint . -c .eslintrc.yaml --no-eslintrc --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint",
"integration": "jest --config jest.integration.js --maxWorkers=2",
"pretest": "npm run lint",
"test": "jest"
},
...</code></pre><p>对其中一一讲解:</p><h3>自定义<code>npm run-script</code></h3><p>在<code>NPM</code>友好型环境(<code>npm init -y</code>)下,可以将<code>node index.js</code>定义在<code>npm_package_scripts_*</code>中作为别名直接执行。</p><pre><code>{
"name": "cli",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"d1": "node ./demo1/bin/operation.js"
},
"keywords": [],
"author": "",
"license": "ISC"
}</code></pre><p>在命令行中输入<code>npm run d1</code>就是执行<code>node ./demo1/bin/operation.js</code></p><h3><code>npm_package</code>变量</h3><p><code>npm run-script</code>自定义的命令,可以将<code>package.json</code>其它配置项当变量使用</p><pre><code>{
"name": "cli",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"d1": "node ./demo1/bin/operation.js",
"d1:var": "%npm_package_scripts_d1%",
},
"keywords": [],
"author": "",
"license": "ISC"
}</code></pre><p>在日常应用中,可以用<code>config</code>字段定义常量:</p><pre><code>{
"name": "cli",
"version": "1.0.0",
"description": "",
"main": "index.js",
"config": {
"port": 8081
},
"scripts": {
"d1": "node ./demo1/bin/operation.js",
"d1:var": "%npm_package_scripts_d1%",
"test": "echo %npm_package_config_port%"
},
"keywords": [],
"author": "",
"license": "ISC"
}</code></pre><p><strong>平台差异</strong>:</p><ul><li>Linux/Mac:<code>$npm_package_*</code></li><li>Windows:<code>$npm_package_*</code></li><li>跨平台:<code>cross_var</code>第三方NPM包</li></ul><h3>shebang</h3><p>仅在<code>Unix</code>系统中可用,在首行指定<code>#!usr/bin/env node</code>,执行文件时,会在该用户的执行路径下运行指定的执行环境</p><p>可以通过<code>type env</code>确认环境变量路径。</p><pre><code>#!/usr/bin/env node
console.log('-------------')</code></pre><p>可以直接以文件名执行上述文件,而不需要<code>node index.js</code>去执行</p><pre><code>E:\demos\node\cli> ./index.js
--------</code></pre><h3><code>process.env</code>环境变量</h3><p>具有<strong>平台差异</strong></p><ul><li><code>Unix: run-cli</code></li></ul><pre><code>mode=development npm run build</code></pre><p>即可在逻辑代码中可获得<code>process.env.mode === "develop"</code></p><ul><li><code>Windows: run-cli</code></li></ul><p>不允许该方式定义环境变量</p><ul><li>跨平台</li></ul><p>借助<code>cross-env</code>定义环境变量</p><h3>多命令串行</h3><p>示例如下:</p><pre><code>{
"name": "cli",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"d2:o1": "node ./demo2/bin/ope1.js",
"d2:o2": "node ./demo2/bin/ope2.js",
"d2:err": "node ./demo2/bin/op_error.js",
"d2": "npm run d2:o1 && npm run d2:o2"
},
"keywords": [],
"author": "",
"license": "ISC"
}</code></pre><p><code>&&</code>可以连接多个命令,使之串行执行。 </p><p>若前一命令中有异步方法,会等异步执行结束,进程完全结束后,才会执行后继命令。</p><pre><code>// ./demo2/bin/ope1.js
console.log(1)
setTimeout(() => {
console.log(2)
}, 4000)
console.log(3)</code></pre><pre><code>// ./demo2/bin/ope2.js
console.log(4)</code></pre><p>执行结果:</p><pre><code>1
3
2
4</code></pre><h3>多命令并行</h3><p>具有<strong>平台差异</strong></p><ul><li><code>Unix</code>: <code>&</code>可以连接多个命令,使之并行执行。</li><li><code>Windows</code>:<code>&</code>多命令依旧串行。</li><li>跨平台:借助<code>npm-run-all</code>第三方NPM包</li></ul><p>串行示例在<code>Mac</code>输出结果:</p><pre><code>1
3
4
2</code></pre><h3>条件执行</h3><p>在多命令编排的流程中,可能在某些条件下需要结束流程。</p><h4>立即结束<code>process.exit(1)</code></h4><pre><code>// demo2/bin/op_error.js
console.log(1)
process.exit(1)
setTimeout(() => {
console.log(2)
}, 4000)
console.log(3)
// demo2/bin/ope2.js
console.log(4)</code></pre><p>执行命令<code>"d2:error": "npm run d2:err && npm run d2:o2"</code>,输出结果:</p><pre><code>1
Error</code></pre><p>其中<code>process.exit(1)</code>后续的代码及任务都不再执行。</p><h4>当前进程执行完结束<code>process.exitCode = 1</code></h4><pre><code>// demo2/bin/op_error.js
console.log(1)
process.exitCode = 1
setTimeout(() => {
console.log(2)
}, 4000)
console.log(3)</code></pre><p>改造<code>op_error.js</code>,执行<code>npm run d2:error</code>,输出结果:</p><pre><code>1
3
2
Error</code></pre><p>其中<code>process.exitCode = 1</code>后续的代码仍继续执行,而后继任务不再执行。</p><h3><code>npm run-script</code>传参</h3><h4><code>npm run-script</code>参数</h4><p>自定义命令<code>"d4": "node ./demo4/bin/operation.js"</code>:</p><pre><code>console.log(process.argv)</code></pre><p>执行<code>npm run d4 -f</code>,输出结果:</p><pre><code>E:\demos\node\cli>npm run d4 -f
npm WARN using --force I sure hope you know what you are doing.
> cli@1.0.0 d4 E:\demos\node\cli
> node ./demo4/bin/operation.js
[
'D:\\nodejs\\node.exe',
'E:\\demos\\node\\cli\\demo4\\bin\\operation.js'
]</code></pre><p>其中,<code>-f</code>不被<code>bin/operation.js</code>承接,而是作为<code>npm run-script</code>的参数消化掉(即使<code>npm run-script</code>不识别该参数)。</p><ul><li><p><code>-s</code></p><ul><li>静默执行<code>npm run-script</code>:忽略日志输出</li></ul></li><li><p><code>-d</code></p><ul><li>调试模式执行<code>npm run-script</code>:日志全Level输出</li></ul></li></ul><h4>界定<code>npm run-script</code>结束</h4><p>执行<code>npm run d4 -- -f</code>,输出结果:</p><pre><code>E:\demos\node\cli>npm run d4 -- -f
> cli@1.0.0 d4 E:\demos\node\cli
> node ./demo4/bin/operation.js "-f"
[
'D:\\nodejs\\node.exe',
'E:\\demos\\node\\cli\\demo4\\bin\\operation.js',
'-f'
]</code></pre><p>其中,<code>-f</code>被<code>bin/operation.js</code>承接。</p><p>可见,在<code>npm run-script <command></code>后使用<code>--</code>界定<code>npm</code>参数的结束,<code>npm</code>会将<code>--</code>之后的所有参数直接传递给自定义的脚本。</p><h3><code>NPM</code>钩子</h3><h4><code>npm_package_scripts_*</code>定义</h4><pre><code>{
"name": "cli",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"pred5": "node ./demo5/bin/pre.js",
"d5": "node ./demo5/bin/operation.js",
"postd5": "node ./demo5/bin/post.js"
},
"keywords": [],
"author": "",
"license": "ISC"
}</code></pre><p>执行<code>npm run d5</code>,在执行<code>node ./demo5/bin/operation.js</code>之前会自动执行<code>"pred5": "node ./demo5/bin/pre.js"</code>,在执行<code>node ./demo5/bin/operation.js</code>之后会自动执行<code>"postd5": "node ./demo5/bin/post.js"</code>。</p><h4><code>node_modules/.hooks/</code>定义</h4><p><strong><code>Unix</code>可用</strong></p><ol><li>创建<code>node_modules/.hooks/</code>目录</li><li><p>创建<code>pred5</code>文件</p><pre><code>console.log('---------pre--------')</code></pre></li><li>修改文件权限为可执行<code>chmod 777 node_modules/.hooks/pred5</code></li><li>执行命令<code>npm run d5</code>即可</li></ol><p>场景:</p><pre><code> "postinstall": "husky install"</code></pre><h3>NPM包本地调试<code>npm link</code></h3><pre><code>// 切换到NPM包目录下
npm link</code></pre><p><code>npm link</code>可以将本地包以软链的形式注册到全局<code>node_modules/bin</code>下,以<code>npm_package_name</code>为包名。</p><pre><code>// 切换到项目目录下
npm link package_name</code></pre><p>在项目目录下,通过<code>npm link package_name</code>可以将本地NPM包链接到项目中,进行本地调试开发。</p><pre><code>// 在项目目录下
npm unlink package_name</code></pre><p><code>unlink</code>取消项目与本地NPM包的绑定</p><pre><code>// 在NPM包目录下
npm unlink</code></pre><p>取消本地NPM包的全局注册</p><h2>基于Node模块的命令行</h2><h3>示例1:<code>process.stdin</code> & <code>process.stdout</code>交互命令行</h3><pre><code>function cli () {
process.stdout.write("Hello");
process.stdout.write("World");
process.stdout.write("!!!");
process.stdout.write('\n')
console.log("Hello");
console.log("World");
console.log("!!!");
process.on('exit', function () {
console.log('----exit')
})
process.stdin.setEncoding('utf8')
process.stdin.on('data', (input) => {
console.dir(input)
input = input.toString().trim()
if (['Y', 'y', 'YES', 'yes'].indexOf(input) > -1) {
console.log('success')
}
if (['N', 'n', 'No', 'no'].indexOf(input) > -1) {
console.log('reject')
}
})
process.stdout.write('......\n')
console.log('----------------00000000000------------')
process.stdout.write('确认执行吗(y/n)?')
process.stdout.write('......\n')
}
cli()</code></pre><h4>stdin</h4><ul><li>标准输入监听控制台的输入</li><li>以回车标识结束</li><li>获取的输入包含回车字符</li></ul><h4>stdout</h4><h5>process.stdout vs. console.log</h5><p>其中<a href="https://link.segmentfault.com/?enc=pKa3BSzUDrGiM96P3vtc1A%3D%3D.0u%2BD6k8q5Nl82eELey1MZh9%2BMSlsSPuGXQYfX%2B%2FVndZn3kPJ%2FPTf%2BTvyyO%2FdhJSNwjxczg%2BJDFurXAqFyhkCkQ%3D%3D" rel="nofollow"><code>console.log</code>输出底层调用的是<code>process.stdout</code></a>,在输出之前进行了处理,比如调用<code>util.format</code>方法</p><table><thead><tr><th align="left">区别</th><th align="left">process.stdout</th><th align="left">console.log</th></tr></thead><tbody><tr><td align="left">参数</td><td align="left">只能接收字符串做参数</td><td align="left">支持ECMA的所有数据类型</td></tr><tr><td align="left">参数个数</td><td align="left">仅一个字符串</td><td align="left">可以接收多个</td></tr><tr><td align="left">换行</td><td align="left">行内连续输出</td><td align="left">自动追加换行</td></tr><tr><td align="left">格式化</td><td align="left">不支持</td><td align="left"><a href="https://link.segmentfault.com/?enc=uBgfVPcUzE940qd1i2Cdlg%3D%3D.UUkYsZgAt9ajV0KVZMiJk%2BpYYasMErCm6wmWLed7DAbATB2PVntuQBx30mYma1%2FvaC25ogGuzQRVcbBoDdUgMQ%3D%3D" rel="nofollow">支持'%s'、'%c'格式化</a></td></tr><tr><td align="left">输出自身</td><td align="left">WriteStream对象</td><td align="left">字符串</td></tr></tbody></table><h3>示例2:<code>process.stdin</code>工作模式</h3><pre><code>process.stdin.setEncoding('utf8');
function readlineSync() {
return new Promise((resolve, reject) => {
console.log(`--status----${process.stdin.readableFlowing}`);
process.stdin.resume();
process.stdin.on('data', function (data) {
console.log(`--status----${process.stdin.readableFlowing}`);
process.stdin.pause(); // stops after one line reads // 暂停 input 流,允许稍后在必要时恢复它。
console.log(`--status----${process.stdin.readableFlowing}`);
resolve(data);
});
});
}
async function main() {
let input = await readlineSync();
console.log('inputLine1 = ', input);
console.log('bye');
}
main();</code></pre><p><strong>若n次调用<code>readlineSync()</code>,会为<code>data</code>事件监听多次绑上处理函数,回调函数会执行n次。</strong></p><h4>stdin</h4><p>标准输入是可读流的实例</p><h5>工作模式</h5><p>符合可读流的工作模式:</p><ul><li><p>流动模式(flowing)</p><blockquote>在流动模式中,数据自动从底层系统读取,并通过<code>EventEmitte</code>接口的事件尽可能快地被提供给应用程序</blockquote></li><li><p>暂停模式(paused)</p><blockquote>在暂停模式中,必须显式调用<code>stream.read()</code>读取数据块</blockquote></li></ul><h5>工作状态</h5><ul><li><code>null</code></li><li><code>false</code></li><li><code>true</code><br>可通过<code>readable.readableFlowing</code>查看相应的工作模式</li></ul><h5>状态切换</h5><ul><li>添加 <code>'data'</code> 事件句柄。</li><li>调用 <code>stream.resume()</code> 方法。</li><li>调用 <code>stream.pipe()</code> 方法将数据发送到可写流。</li></ul><h5>进程结束</h5><ul><li>如果事件循环中没有待处理的额外工作,则 <code>Node.js</code> 进程会自行退出。</li><li>调用<code>process.exit()</code>会强制进程尽快退出,即使还有尚未完全完成的异步操作在等待,包括对 <code>process.stdout</code> 和 <code>process.stderr</code> 的 I/O 操作。</li></ul><h3>示例3:<code>readline</code>模块</h3><pre><code>const readline = require('readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
prompt: '请输入> '
});
rl.prompt();
rl.on('line', (line) => {
console.dir(line)
switch (line.trim()) {
case 'hello':
console.log('world!');
break;
default:
console.log(`你输入的是:'${line.trim()}'`);
break;
}
rl.prompt();
}).on('close', () => {
console.log('再见!');
process.exit(0);
});</code></pre><h4>readline模块</h4><h5>创建UI界面</h5><pre><code>const rl = readline.createInterface({
input: process.stdin, // 定义输入UI
output: process.stdout, // 定义输出UI
historySize: 0, // 禁止历史滚动 —— 默认:30
removeHistoryDuplicates: true, // 输入历史去重 —— 默认:false
completer: function (line) { // 制表符自动填充匹配文本
const completions = '.help .error .exit .quit .q'.split(' ');
const hits = completions.filter((c) => c.startsWith(line));
return [hits.length ? hits : completions, line]; // 输出数组:0 —— 匹配结果;1 —— 输入
},
prompt: '请输入> ' // 命令行前缀
});</code></pre><h5>方法</h5><h6><code>rl.prompt()</code></h6><p>以前缀开启新的输入行</p><h6><code>rl.close()</code></h6><p>关闭<code>readline.Interface</code>实例,并放弃对<code>input</code>和<code>output</code>流的控制</p><h5>事件</h5><h6><code>line</code>事件</h6><pre><code>rl.on('line', (line) => {
// 相对比process.stdin.on('data', function (chunk) {}),输入line不包含换行符
switch (line.trim()) {
case 'hello':
console.log('world!');
break;
default:
console.log(`你输入的是:'${line.trim()}'`);
break;
}
rl.prompt();
});</code></pre><h3>inquirer源码解析</h3><blockquote>核心:</blockquote><ul><li><p>命令行UI</p><ul><li>readline.createInterface</li></ul></li><li><p>渲染输出</p><ul><li>rl.output.write</li></ul></li><li><p>事件监听</p><ul><li>rxjs</li></ul></li></ul><blockquote>增强交互体验:</blockquote><ul><li>mute-stream:控制输出流输出</li><li>chalk:多彩日志打印</li><li>figures:命令行小图标</li><li>cli-cursor:光标的隐藏、显示控制</li></ul><blockquote>下面以type="list"为例进行说明</blockquote><h4>创建命令行</h4><pre><code>this.rl = readline.createInterface({
terminal: true,
input: process.stdin,
output: process.stdout
})</code></pre><h4>渲染输出</h4><pre><code>var obs = from(questions)
this.process = obs.pipe(
concatMap(this.processQuestion.bind(this)),
publish()
)</code></pre><p>将传入的参数转换为数据流形式,对其中的每一项数据进行渲染<code>processQuestion</code></p><pre><code> render(error) {
var message = this.getQuestion();
if (this.firstRender) {
message += chalk.dim('(Use arrow keys)');
}
if (this.status === 'answered') {
message += chalk.cyan(this.opt.choices.getChoice(this.selected).short);
} else {
var choicesStr = listRender(this.opt.choices, this.selected);
var indexPosition = this.opt.choices.indexOf(
this.opt.choices.getChoice(this.selected)
);
message +=
'\n' + choicesStr;
}
this.firstRender = false;
this.rl.output.unmute();
this.rl.output.write(message);
this.rl.output.mute();
}</code></pre><p>其中:</p><ul><li>借助<code>chalk</code>进行输出的色彩多样化;</li><li><code>listRender</code>将每一个<code>choice</code>拼接为字符串;</li><li>使用<code>this.selected</code>标识当前选中项,默认为0;</li><li>使用<code>this.rl.output.write</code>将字符串输出;</li><li>借助<code>mute-stream</code>控制命令行无效输出;</li></ul><h4>事件监听</h4><pre><code>function observe(rl) {
var keypress = fromEvent(rl.input, 'keypress', normalizeKeypressEvents)
.pipe(takeUntil(fromEvent(rl, 'close')))
// Ignore `enter` key. On the readline, we only care about the `line` event.
.pipe(filter(({ key }) => key !== 'enter' && key.name !== 'return'));
return {
line: fromEvent(rl, 'line'),
keypress: keypress,
normalizedUpKey: keypress.pipe(
filter(
({ key }) =>
key.name === 'up' || key.name === 'k' || (key.name === 'p' && key.ctrl)
),
share()
),
normalizedDownKey: keypress.pipe(
filter(
({ key }) =>
key.name === 'down' || key.name === 'j' || (key.name === 'n' && key.ctrl)
),
share()
),
numberKey: keypress.pipe(
filter((e) => e.value && '123456789'.indexOf(e.value) >= 0),
map((e) => Number(e.value)),
share()
),
};
};</code></pre><p>借助<code>Rx.fromEvent</code>监听命令行的<code>keypress</code>、<code>line</code>事件。</p><pre><code>var events = observe(this.rl);
events.normalizedUpKey
.pipe(takeUntil(events.line))
.forEach(this.onUpKey.bind(this));
events.normalizedDownKey
.pipe(takeUntil(events.line))
.forEach(this.onDownKey.bind(this));
events.line
.pipe(
take(1)
)
.forEach(this.onSubmit.bind(this));</code></pre><p>订阅事件,对相应的事件进行处理</p><pre><code> onUpKey () {
console.log('--------up')
this.selected = incrementListIndex(this.selected, 'up', this.opt);
this.render();
}
onDownKey () {
console.log('--------down')
this.selected = incrementListIndex(this.selected, 'down', this.opt);
this.render();
}
onSubmit () {
console.log('------------submit')
}</code></pre><p>修改<code>this.selected</code>值,通过<code>this.render</code>进行命令行的界面更新。<br>监听<code>line</code>事件,将<code>this.selected</code>对应的结果进行输出。</p>
Vue-cli & lerna多项目管理
https://segmentfault.com/a/1190000039741693
2021-03-30T15:04:10+08:00
2021-03-30T15:04:10+08:00
米花儿团儿
https://segmentfault.com/u/mihuartuanr
0
<blockquote>在Vue-cli 3.X环境下,基于同一类型的活动,可以多个页面复用,大部分组件可以公用的背景</blockquote><h2>Multiple处理方式</h2><ul><li>每一个活动创建一个分支,在不同的分支上各自维护</li><li>如果需要维护复用代码时,任选某一分支进行修改,通过<code>git cherry-pick <commit id></code>进行平行迁移。</li></ul><h2>Monorepo处理方式</h2><p>仅在同一分支下进行多项目的维护,各个功能模块解构,通过项目配置项进行个性化配置。</p><h3>目录结构</h3><p>使用<code>vue-cli</code>初始化项目后,需要进行目录的调整,将<code>src/</code>下的目录结构提升到顶层结构中,并进行重新整合。</p><pre><code>|- views
|- index.js // 通用页面的统一入口
|- Company
|- index.vue // 通用页面Company结构、样式、逻辑
|- index.js // Company页面路由
|- Rule
|- index.vue
|- index.js
|- components
|- core
|- instance // 和app实例挂钩的方法
|- libs // 和app实例无关的方法
|- assets
|- images
|- fonts
|- store
|- index.js // 通用状态
|- types.js // 事件类型
|- config
|- proA.js // 项目资源配置
|- proB.js
|- projects // 项目定制资源
|- proA
|- proB</code></pre><p><strong>不同项目的区别完全在于<code>config/</code>文件的配置和<code>projects/</code>下的项目定义</strong>;同级其余目录是各个项目通用的内容。</p><p>以上目录结构需在<strong>项目工作目录的顶层结构下</strong>,不允许包裹,为后续和<code>lerna</code>结合做准备。</p><p>这里需要调整<code>vue.config.js</code>中的入口文件<code>pages: entries</code>的路径地址。</p><h3>提取公共页面 & 路由</h3><h4>公共页面示例:</h4><pre><code>// views/Company/index.vue
<template>
...
</template>
<script>
...
</script>
<style scoped>
...
</style></code></pre><h4>公共页面路由</h4><pre><code>// views/Company/index.js
export default [
{
path: '/company',
name: 'company',
component: () => import(/* webpackChunkName: "company" */ './index.vue'),
meta: {
title: '公司简介'
}
}
]</code></pre><h4>公共页面统一入口</h4><pre><code>// views/index.js
export { default as companyRoute } from './Company/index.js'
export { default as ruleRoute } from './Rule/index.js'</code></pre><h4>定制项目中的公共页面</h4><pre><code>// config/proA.js
import {
companyRoute,
ruleRoute
} from '../views/index.js'
...
export const logoUrl = '' // 还可以定制其它的内容
export const routes = [
...companyRoute,
...ruleRoute
]</code></pre><h4>项目中使用公共页面</h4><p>以<code>projects/proA</code>为例:</p><blockquote>目录结构</blockquote><pre><code>|- assets
|- components
|- mixins
|- router
|- store
|- pages
|- App.vue
|- main.js</code></pre><blockquote>项目主路由</blockquote><pre><code>// projects/proA/router/index.js
import Vue from 'vue'
import Router from 'vue-router'
import { routes } from '../../config/proA'
import Home from '../pages/Home'
Vue.use(Router)
export default new Router({
routes: [
{
path: '/',
redirect: '/home'
},
{
path: '/home',
name: 'Home',
component: Home,
meta: {
title: ''
}
},
...routes
]
})</code></pre><p>其中:Home/index.vue是定制化的。</p><h3>状态管理</h3><blockquote><strong>多项目是独立运行时,状态提取不会互相干扰,若一次性运行多个项目,通用状态会被修改。</strong></blockquote><h4>通用状态提取</h4><pre><code>// store/index.js
import types from './types'
export const initialState = {
userInfo: {},
...
}
export function getGetters (store) {
return {
userId: () => store.userInfo.userID,
...
}
}
export function getMutations (store) {
return {
[types.INITIALMUTATIONTYPES.USER_INFO] (val) {
store.userInfo = val
},
...
}
}</code></pre><p>在<code>config/proA.js</code>文件中追加:</p><pre><code>...
export * from '../store/index.js'
export * from '../store/types.js'
...</code></pre><h4>项目中使用</h4><blockquote>小型项目,使用<code>vue.observable</code>管理状态</blockquote><h5>定义项目的主状态管理</h5><pre><code>// projects/proA/store/index.js
import vue from 'vue'
import { initialState, getGetters, getMutations } from '../../../config/proA'
export const store = vue.observable({
...initialState,
customState: '', // 项目私有状态
...
})
store._getters = {
...getGetters(store),
customGetter() { // 项目私有
return store.customState
},
...
}
store._mutations = {
...getMutation(store),
... // 项目私有
}
export const mutation = {
...getMutations(store),
... // 项目私有
}</code></pre><h5>定义辅助方法<code>mapGetters</code></h5><blockquote>拷贝<code>vuex</code>部分代码到<code>core/libs/helpers.js</code>文件中</blockquote><pre><code>export const mapGetters = (getters) => {
const res = {}
if (!isValidMap(getters)) {
console.error('[vuex] mapGetters: mapper parameter must be either an Array or an Object')
}
normalizeMap(getters).forEach(({ key, val }) => {
res[key] = function mappedGetter () {
if (!(val in this.$store._getters)) {
console.error(`[vuex] unknown getter: ${val}`)
return
}
return this.$store._getters[val]()
}
})
return res
}
export function normalizeMap (map) {
if (!isValidMap(map)) {
return []
}
return Array.isArray(map)
? map.map(key => ({ key, val: key }))
: Object.keys(map).map(key => ({ key, val: map[key] }))
}
export function isValidMap (map) {
return Array.isArray(map) || isObject(map)
}
export function isObject (obj) {
return obj !== null && typeof obj === 'object'
}</code></pre><p>在<code>core/libs/index.js</code>中追加:</p><pre><code>export * from './helpers'</code></pre><h5><code>*.vue</code>中使用</h5><pre><code>// projects/proA/pages/Home/index.vue
<script>
...
import { mapGetters } from '../../../core/libs/'
export default {
data () {
return {
...
}
},
computed: {
...mapGetters([
'userId'
]),
...
}
...
</script></code></pre><h3>组件管理</h3><h4>组件统一入口</h4><blockquote>借助<code>webpack</code>的<code>require.context</code>方法将<code>/components/</code>下的组件整合</blockquote><pre><code>const ret = {}
const requireComponent = require.context(
'./', // 指定递归的文件目录
true, // 是否递归文件子目录
/[A-Z]\w+\.(vue)$/ // 落地文件
)
requireComponent.keys().forEach(fileName => {
const componentConfig = requireComponent(fileName)
const component = componentConfig.default || componentConfig
const componentName = component.name || fileName.split('/').pop().replace(/\.\w+$/, '')
// ret[componentName] = () => requireComponent(fileName)
ret[componentName] = component
})
export default ret</code></pre><h4>定义布局配置</h4><pre><code>// config/proA.js追加
...
export const layouts = {
Home: [
{
componentType: 'CompA',
request: {
fetch () {
const res = []
return res
}
},
response: {
filter (res) {
return []
},
effect () {
}
}
},
{
componentType: 'CompB'
},
{
componentType: 'CompC'
},
{
componentType: 'CompD'
}
]
}</code></pre><h4>项目中使用</h4><p><code>proA/Home/index.vue</code></p><pre><code><template>
...
<template v-for="componentConfig of layouts">
<component
v-bind="dataValue"
:is="componentConfig.componentType"
:key="componentConfig.componentType"
>
</component>
</template>
...
</template>
<script>
...
import {
CompA,
CompB
} from '../../components/'
import { layouts } from '../../config/proA'
...
export default {
...
data () {
return {
...
layouts: layouts.Home,
...
}
},
...
components: {
CompA,
CompB
},
...
}
</script></code></pre><h3>引入<code>lerna</code>管理项目</h3><h4>初始化<code>lerna</code>环境</h4><pre><code>npm i -g lerna
npm i -g yarn // 需要借助yarn的workspaces特性
cd <workplace>
lerna init
// ^ >lerna init
// lerna notice cli v4.0.0
// lerna info Updating package.json
// lerna info Creating lerna.json
// lerna info Creating packages directory
// lerna success Initialized Lerna files</code></pre><p><code>lerna</code>初始化环境会做以下几件事:</p><ul><li>更新<code>package.json</code>文件</li><li>创建<code>lerna.json</code>文件</li><li>创建<code>packages/</code>目录</li></ul><h4>指定工作区域</h4><ul><li>修改<code>package.json</code>文件</li></ul><pre><code> "private": true, // private需要为true
"workspaces": [
"projects/*",
"components/*"
],</code></pre><ul><li>修改<code>lerna.json</code>文件</li></ul><pre><code>{
"npmClient": "yarn",
"useWorkspaces": true, // 共用package.json文件的workspaces配置
"version": "independent" // 每个项目独立管理版本号
}</code></pre><h4>创建新项目</h4><pre><code>lerna create @demo/cli
// ^ lerna success create New package @demo/cli created at .projects/cli</code></pre><p><code>lerna</code>会做以下几件事:</p><ul><li>命令行是否指定目标工作区</li><li>若无指定工作区,选<code>lerna.json</code>配置项<code>packages</code>第一项工作区为目标工作区</li><li><p>通过交互命令行界面创建项目目录</p><ul><li><p>新项目目录结构</p><pre><code>|- projects
|- cli
|- package.json
|- README.md
|- lib
|- cli.js
|- __tests__</code></pre></li></ul></li></ul><p>这里讲诉一下,为什么整体的项目目录不允许包裹,如不允许使用<code>src</code>包裹项目目录:<code>lerna</code><strong>指定工作区<code>loc</code>限制为顶级目录结构</strong>。</p><pre><code>lerna create @demo/cli2 'components'
// ^ lerna success create New package @demo/cli2 created at .components/cli2</code></pre><h4>将已有项目改装为<code>lerna</code>的工作项目</h4><p>在项目目录下追加<code>package.json</code>文件即可</p><pre><code>{
"name": "proA",
"version": "0.0.0"
}</code></pre><h4>查看项目列表</h4><pre><code>// 以yarn命令查看
yarn workspaces info
// 以lerna命令查看
lerna list</code></pre><p>项目目录下有<code>package.json</code>描述的才能被检索出来。</p><h4>管理依赖</h4><pre><code>yarn workspace proA add packageA // 给proA安装依赖packageA
lerna add packageA --scope=proA // 给proA安装依赖packageA
yarn workspace proA add package-a@0.0.0 // 将packageA作为proA的依赖进行安装
larna add package-a --scope=proA // 将packageA作为proA的依赖进行安装
// ^ == yarn workspace安装本地包,第一次必须加上lerna.json中的版本号(后续一定不要再加版本号),否则,会从 npm.org远程检索安装
yarn add -W -D typescript // 在root下安装公用依赖typescript</code></pre><p>通过以上几步,可以将项目的依赖单独管理</p><pre><code>// projects/proA/package.json
{
"name": "proA",
"version": "0.0.0",
"dependencies": {
"packageA": "^1.0.0"
}
}
// ./package.json根目录文件
{
"name": "monoDemo",
"version": "0.0.0",
"private": true,
"workspaces": [
"projects/*",
"components/*",
],
"dependencies": [
"typescript": "^0.4.9"
]
}</code></pre><h4>查看改动的项目</h4><pre><code>lerna changed</code></pre><h4>提交修改</h4><ul><li><p>初始化提交:创建项目或切新分支第一次提交</p><pre><code>git add .
git commit -m <message>
git push --set-upstream origin <branch></code></pre></li><li><p>后续提交</p><pre><code>git add .
git commit -m <message>
lerna version --conventional-commits [-m <message>]
// ^ 使用--conventional-commits参数lerna会根据semer版本规则自动生成版本号,否则,通过交互式命令行手动选择版本号。</code></pre></li></ul><p>可以提前预置<code>lerna version</code>的提交日志</p><pre><code>{
"command": {
"version": {
"message": "chore(release): publish %s"
}
}
}</code></pre><p><code>lerna version</code>会做一下几件事:</p><ul><li>找出从上一个版本发布以来有过变更的 package</li><li>提示开发者确定要发布的版本号</li><li>将所有更新过的的 package 中的package.json的version字段更新</li><li>将依赖更新过的 package 的 包中的依赖版本号更新</li><li>更新 lerna.json 中的 version 字段</li><li>提交上述修改,并打一个 tag</li><li>推送到 git 仓库</li></ul><h2>参考文档</h2><ul><li><li><a href="https://link.segmentfault.com/?enc=lza4lRwbCvgV2%2B88nOiM9g%3D%3D.xVBp9A8kT2USGMlhjWg5bmy%2BG%2BTr%2BUA6gS%2FC5lRIbb%2FiGTsiO1HOqF58spFDW3KRQIAyytI7LymTiv8dGQIlnQ%3D%3D" rel="nofollow">使用 MonoRepo 管理前端项目</a></li><li><a href="https://link.segmentfault.com/?enc=Xs2FTYj4zyihCs4LhDJIUg%3D%3D.sa6vIMOvOR74yYrU2pway%2BTLrfi8%2FXi8txO4tQoH5Be5ohLKqiD5cP4FxVoZ5Y%2Bv" rel="nofollow">husky对monoRepo的支持</a></li></ul>
创建可编辑区域
https://segmentfault.com/a/1190000039684303
2021-03-22T09:52:11+08:00
2021-03-22T09:52:11+08:00
米花儿团儿
https://segmentfault.com/u/mihuartuanr
0
<h2>键盘输入分类</h2><h3>直接输入</h3><p>输入的键直接落入可输入DOM元素,为直接输入。</p><p>E.g.英文输入。</p><h3>间接输入</h3><p>输入的键值不会直接落入可输入DOM元素,有一个中间态,为间接输入。</p><p>E.g.中文输入。</p><h3>区分中英文输入</h3><p>因为任何输入都会触发<code>input</code>,而<strong>输入中文的时候才触发<code>compositionstart</code>和<code>compositionend</code></strong>,可以以此来区分中英文输入。</p><h3><code>e.keyCode</code>在中英文下不同的表现</h3><p><code>e.keyCode</code>在英文模式下输入,能获取正确的键值;<br><code>e.keyCode</code>在中文模式下输入,键入任何值都输出<code>229</code>;</p><p><code>Windows</code>将所有未识别的设备输入都设置为<code>VK_PROCESSKEY 229</code>,浏览器的 <code>event.keyCode</code>复用了这一规范,因此在中文输入过程中,无论按下什么按键,返回的<code>event.keyCode</code>永远是<code>229</code>。</p><h2>输入的事件监听</h2><p>因为任何输入都会触发<code>input</code>,而输入中文的时候才触发<code>compositionstart</code>和<code>compositionend</code>,可以以此来区分中英文输入。</p><p>监听<code>input</code>事件时,输入值时,<code>e.data</code>有值,删除值时,<code>e.data === undefined</code>,可以以此判断输入、删除。</p><p><code>compositionstart</code>、<code>compositionupdate</code>和<code>compositionend</code>只能通过<code>window.addEventListener('')</code>监听,<code>on*</code>监听无效。</p><h2>光标位置<code>codepen</code>示例</h2><p><a href="https://codepen.io/mihuartuanr/pen/MmGVdZ">https://codepen.io/mihuartuan...</a></p><h2>自定义编辑器</h2><p>保证输入的复杂度与灵活性,一般选用普通标签而非文本域做输入容器。</p><h3>普通标签<strong>可编辑</strong></h3><h4><code>contenteditable</code>标签属性</h4><p>属性值如下:</p><pre><code>contenteditable=""
contenteditable="events"
contenteditable="caret"
// 纯文本输入
// 换行不会生成<div>,PC端或Android端使用'\n'判断,IOS使用inputType=== 'insertLineBreak'判断
// 复制黏贴不会带有格式
contenteditable="plaintext-only"
contenteditable="true" // 换行会生成<div>包裹
contenteditable="false"</code></pre><h4><code>user-modify</code>CSS属性</h4><blockquote><code>user-modify</code>可以在移动端使用,以及,只需要兼顾webkit内容的桌面网页项目。</blockquote><pre><code>-webkit-user-modify: read-only; // 普通元素的默认状态
-webkit-user-modify: read-write; //可以输入富文本
-webkit-user-modify: write-only;
-webkit-user-modify: read-write-plaintext-only // 只可以输入纯文本</code></pre><h4>两种方式对比</h4><ul><li><code>contenteditable</code>和<code>user-modify</code>的旧版本浏览器支持性差</li><li><code>contenteditable</code>是归属于W3C标准,全浏览器支持;而<code>user-modify</code>浏览器有自己的实现,非标准,使用需要追加浏览器前缀<code>-webkit-</code>、<code>-moz-</code>...;</li><li><code>contenteditable</code>和<code>user-modify</code>都可以实现对拷贝的富文本过滤格式;</li></ul><h3>插入内容 && 关闭选择范围</h3><h4>基本知识</h4><blockquote>两个概念<strong>选择</strong><code>selection</code>、<strong>范围</strong><code>range</code></blockquote><p><strong><code>range</code>只有置于<code>selection</code>中才起作用</strong></p><pre><code>const selection = getSelection()
const range = selection.getRangeAt(0) // 获取光标位的选中范围
const range1 = new Range() // 自定义选中范围,使用时需要selection.addRange(range1)</code></pre><ul><li><code>selection</code>是管理<code>range</code>的集合,除了<code>Firfox</code>中rangeCount > 1,其它浏览器的实现,<code>selection</code>最多只有一个<code>range</code></li><li><code>range</code>是文本选择范围的起点和终点</li></ul><blockquote><code>range</code>文本选择规则</blockquote><ul><li><p>通过<code>range.setStart(node, offset)</code>、<code>range.setEnd(node, offset)</code>设置范围,根据node节点的类型<code>nodeType</code>不同分属不同的情况</p><ul><li>node为文本节点<code>nodeType === 3</code>,偏移量<code>offset</code>为文本中的位置。</li><li>node为元素节点<code>nodeType === 1</code>,偏移量<code>offset</code>为指定元素子节点<code>node.childNodes</code>的位置</li><li>其中范围起点、终点的<code>node</code>允许不同节点</li><li>其中范围起点、终点的定位位于偏移量<code>offset</code>之前</li></ul></li><li>通过<code>console.log(range)</code>即可查看选中的文本;静默调用<code>toString()</code>方法返回内容;</li><li>通过<code>range.startContainer</code>、<code>range.startOffset</code>查看当前范围的起点归属元素及偏移量</li><li>通过<code>range.endContainer</code>、<code>range.endOffset</code>查看当前范围的终点归属元素及偏移量</li><li>通过<code>range.insertNode(node)</code>,在范围的起始处将node插入文档</li><li>通过<code>range.extractContents()</code>、<code>range.deleteContents</code>从文档中删除范围内容</li><li>通过<code>range.surroundContents(node)</code>,自定义元素节点包裹选择的范围,<strong>选择的范围若有元素节点,元素节点必须闭合</strong></li><li>通过<code>selection.empty()</code>可以清空选择</li></ul><p>使用<code>selection.addRange(range)</code>添加范围时,<strong>如果选择已存在,则首先使用<code>selection.removeAllRanges()</code>将其清空。然后添加范围。否则,除<code>Firefox</code>外的所有浏览器都将忽略新范围。</strong> 其中,通过<code>range.setStart</code>、<code>range.setEnd</code>调整范围的情况,不必考虑清空<code>selection</code></p><h4><code>Selection</code>类型</h4><blockquote><code>selection.type</code></blockquote><ul><li>None: 当前没有选择。</li><li>Caret: 选区已折叠(即 光标在字符之间,并未处于选中状态)。</li><li>Range: 选择的是一个范围。</li></ul><h4>设置光标位置为某元素后</h4><pre><code>// 方式1. 只支持Android、PC
range.setStart(baseNode, 1)
range.setEnd(baseNode, 1)
range.collapse()
// 方式2. 只支持Android、PC
range.setEndAfter(baseNode)
selection.collapseToEnd()
// 方式3.
selection.setPosition(node, offset)
// 方式4. 支持IOS,仅用于通过range.extractContents()提取的documentFrag文本
selection.removeAllRanges()
selection.addRange(range)
range.setStart(cloneNode, cloneNode.endOffset)
range.setEnd(cloneNode, cloneNode.endOffset)
selection.collapseToEnd()
// 方式4. 支持全平台,IOS不可用于通过range.extractContents()提取的documentFrag文本
selection.extend(baseNode, 1)
selection.collapseToEnd()
// 光标定位文本后:通过range.extractContents()提取的documentFrag文本
try {
// 安卓端
selection.extend(cloneNode, 1)
selection.collapseToEnd()
} catch (e) {
// Iphone端
selection.removeAllRanges()
selection.addRange(range)
range.setStart(cloneNode, cloneNode.endOffset)
range.setEnd(cloneNode, cloneNode.endOffset)
selection.collapseToEnd()
}</code></pre><h4>代码示例</h4><blockquote>插入的内容只能是<code>documentFragment</code></blockquote><pre><code> const { selection, range } = this.lastSelection
this.editableEle.focus()
const textNode = range.startContainer
range.setStart(textNode, range.endOffset)
range.setEnd(textNode, range.endOffset)
const spanNode1 = document.createTextNode(' ')
const spanNode2 = document.createElement('span')
spanNode2.className = 'tag'
spanNode2.innerHTML = '#'
let frag = document.createDocumentFragment(), lastNode = spanNode2
frag.appendChild(spanNode1)
frag.appendChild(spanNode2)
range.insertNode(frag)
// IPhone下有时候会报错,采用下方代码替代
selection.extend(lastNode, 1)
selection.collapseToEnd()</code></pre><blockquote>IOS下,在标签后紧跟着添加节点,selection.extend(node, 1)关闭范围报错解决方案</blockquote><pre><code>whetherEndTag (prefixer) {
if (!!prefixer && !(prefixer.trim())) {
// 半角、全角空格
const selection = getSelection()
const range = selection.getRangeAt(0)
const node = range.startContainer
if (node.parentNode && node.parentNode.className === 'tag') {
range.setStart(node, range.endOffset - 1)
range.setEnd(node, range.endOffset)
const cloneNode = range.extractContents()
range.setStartAfter(node.parentNode, selection.endOffset)
range.setEndAfter(node.parentNode, selection.endOffset)
range.collapse(true)
range.insertNode(cloneNode)
// 安卓、IOS不兼容
try {
// 安卓端
selection.extend(cloneNode, 1)
selection.collapseToEnd()
} catch (e) {
// Iphone端
selection.removeAllRanges()
selection.addRange(range)
range.setStart(cloneNode, cloneNode.endOffset)
range.setEnd(cloneNode, cloneNode.endOffset)
selection.collapseToEnd()
}
}
}
}</code></pre><h3>预览</h3><pre><code><iframe class="previewContaner" :srcdoc="previewContent" @load="loaded"></iframe></code></pre><p>获取要预览元素的<code>HTML</code>,借助<code>iframe.srcdoc</code>属性进行预览。</p><p>若要对预览样式进行定制,需要对<code>previewContent</code>追加内联样式</p><pre><code>...
computed: {
showContent(){
return `
<style>
img,
video {
max-width: 100%;
}
.host {
width:100%;
overflow: hidden;
font-size: 28px;
text-align: justify;
word-break: break-all;
}
</style>
<div class="host">${this.previewHtml}</div>
`
}
},
...</code></pre><h3>长度限制</h3><blockquote>非中文状态下</blockquote><pre><code> // 非中文状态下
onKeyupListener (e) {
this.check_charcount(e)
},
onKeydownListener (e) {
this.check_charcount(e)
},
check_charcount (e, max = 100) {
if(e.which != 8 && this.editableEle.textContent.length > max) {
e.preventDefault()
}
}</code></pre><blockquote>中文状态:纯文本</blockquote><pre><code> // 中英文,在input、compositionEnd事件中调用——纯文本
...
data () {
return {
CNEnd: true
}
}
...
compositionstart (e) {
this.CNEnd = false
},
compositionend (e) {
this.CNEnd = true
this.limitInput(e)
}
...
limitInput(event) {
let _words = this.editableEle.textContent
let _this = this.editableEle
if (this.CNEnd) {
let num = _words.length
if (num >= 100) {
num = 100
if (_this.spillOver) {
event.target.innerText = this.fullContent
} else {
event.target.innerText = _words.substring(0, 100)
_this.spillOver = true
this.fullContent = _words.substring(0, 100)
}
Toast('100字以内。')
} else {
_this.spillOver = false
this.fullContent = ''
}
const sel = window.getSelection()
let range = document.createRange()
range.selectNodeContents(this.editableEle)
range.collapse(false)
sel.removeAllRanges()
sel.addRange(range)
} else if (this.fullContent) {
// 目标对象:超过100字时候的中文输入法
// 原由:虽然不会输入成功,但是输入过程中字母依然会显现在输入框内
// 弊端:谷歌浏览器输入法的界面偶尔会闪现
event.target.innerText = this.fullContent
this.CNEnd = true
}
}</code></pre><blockquote>中文状态:富文本</blockquote><p>区别:在于fullContent的取值。</p><pre><code> limitInput(event) {
let _words = this.editableEle.textContent
let _this = this.editableEle
if (this.CNEnd) {
let num = _words.length
if (num >= 100) {
if (_this.spillOver) {
event.target.innerHTML = this.fullContent
} else {
const selection = getSelection()
const range = selection.getRangeAt(0)
const lastNode = range.startContainer.parentNode
lastNode.textContent = lastNode.textContent.slice(0, lastNode.textContent.length - (num - 100))
event.target.innerHTML = this.editableEle.innerHTML
_this.spillOver = true
this.fullContent = this.editableEle.innerHTML
}
Toast('コンテンツは100語以内でお願いします。')
} else {
_this.spillOver = false
this.fullContent = ''
}
const sel = window.getSelection()
let range = document.createRange()
range.selectNodeContents(this.editableEle)
range.collapse(false)
sel.removeAllRanges()
sel.addRange(range)
this.cacheCursorPos()
} else if (this.fullContent) {
// 目标对象:超过100字时候的中文输入法
// 原由:虽然不会输入成功,但是输入过程中字母依然会显现在输入框内
// 弊端:谷歌浏览器输入法的界面偶尔会闪现
event.target.innerHTML = this.fullContent
this.CNEnd = true
}
},</code></pre><h3>处理复制的内容</h3><pre><code>...
function paste (event) {
const paste = (event.clipboardData || window.clipboardData).getData('text');
const selection = window.getSelection();
if (!selection.rangeCount) return false;
selection.deleteFromDocument();
selection.getRangeAt(0).insertNode(document.createTextNode(paste));
event.preventDefault();
}
...
document.addEventListener("paste", paste);
...</code></pre><ul><li>通过<code>event.preventDefault()</code>禁止默认的拷贝事件</li><li>通过<code>selection</code>手动的插入想要拷贝的内容</li><li><code>event.clipboardData.getData('text')</code>获取拷贝的文本数据(图片、视频获取不到)</li></ul><p><a href="https://link.segmentfault.com/?enc=h3n26DzBw9SCzi1Mq0YsQw%3D%3D.Kads%2FAGkO9VNp1MPmUgOLyUo%2FXVDhmByHQ03n4y7fzvcMWVBishk45wlN7YpTLWShsPYrDPafnXcqljrqq2czn8Pl615%2BsID7vIOZI1FNO9Uy5IMsRQZK6KgmTn7Ix7E" rel="nofollow">可选的类型</a></p><p><strong>Note:</strong> 不要相信用户输入,这里的处理复制的内容,还要排除用户复制<code>HTML</code>片段。因为复制过来的<code>HTML</code>片段也是字符串,会带有样式。</p><h3>拖拽上传图片</h3><pre><code><label
class="video-wrap"
@dragenter.stop.prevent
@dragover.stop.prevent
@drop.stop.prevent="onFileDrop"
>
<div class="tip-wrap">
<i class="el-icon-upload"></i>
<div class="text-tip">
<p class="title-tip">点击或拖动上传视频</p>
<small class="small-tip">编码格式为h264且后缀为.mp4的视频</small>
</div>
<input class="video-input" @change="onFileChange" type="file">
</div>
</label>
...
methods: {
...
onFileChange (e) {
console.log(e.target.files[0])
},
onFileDrop (e) {
const file = e.dataTransfer.files && e.dataTransfer.files[0]
console.log(file)
}
...
}</code></pre><h3>doc转HTML</h3><blockquote><a href="https://link.segmentfault.com/?enc=wihj0D8qW%2BM0lrISoWDWyw%3D%3D.iQ90ZlCZ1OPzgzQUYZlDI5iDXV8fp8YRv0686iTfu1WEIAznKD6bpIsYQDKge7xFmx5gGO08QvgmxMs8JT%2B5QQ%3D%3D" rel="nofollow">https://github.com/mwilliamso...</a></blockquote><pre><code>var mammoth = require("mammoth");
...
const reader = new FileReader()
reader.onload = function (e) {
const file = e.target.result;
mammoth.convertToHtml({
arrayBuffer: file
})
.then(function(result){
var html = result.value;
console.log(html)
})
.done();
};
reader.readAsArrayBuffer(form[0].files[0])</code></pre><h2>网络状态</h2><p>在<code>Windows</code>环境<code>Chrome</code>浏览器中测试:</p><ul><li>关闭WiFi,<code>online</code>、<code>offline</code>事件并不会被触发</li><li>调整<code>Network</code>的网络阻塞模式,<code>online</code>、<code>offline</code>事件倒是会被触发<br>所以,<code>online</code>、<code>offline</code>事件不可用</li><li><code>navigator.onLine</code>情况同上</li><li><code>navigator.connection.addEventListener('change', () => {})</code>事件在WiFi开关的过程中会触发,但,无法知道<strong>切换的方向性</strong>,即无法知道是联网、断网状态。</li></ul><pre><code>var connection = navigator.connection;
var type = connection.effecetiveType;
function updateConnectionStatus() {
console.log("网络状况从 " + type + " 切换至" + connection.effectiveType);
type = connection.effectiveType;
}
connection.addEventListener('change', updateConnectionStatus);</code></pre><h2>!!!世界未解之谜</h2><h3>node.nextSibling.nodeType === 3</h3><p>当获取元素节点的兄弟文本节点<code>node.nextSibling</code>时,元素节点必须要有文本内容,否则一堆世界未解之谜。</p><h3>删除有样式的文本时(常见于插入回车),浏览器会自动生成<font>追加样式</h3><pre><code> clearFontTag () {
// 当删除时,浏览器自动添加font标签加样式
const fontTag = this.editableEle.querySelector('font')
if (fontTag) {
const newNode = document.createTextNode(fontTag.textContent)
this.editableEle.replaceChild(newNode, fontTag)
const { selection } = this.lastSelection
selection.extend(newNode, 1)
selection.collapseToEnd()
this.cacheCursorPos()
}
}</code></pre><h3>在带有样式的标签后回车,下一行浏览器自动带样式</h3><ul><li>使用<code>contenteditable="plaintext-only"</code>创建可编辑区时,在带有样式的标签后回车,换行后仍在标签内</li><li>使用<code>contenteditable="true"</code>创建可编辑区时,,在带有样式的标签后回车,浏览器自动在新行添加样式标签。</li></ul><blockquote>解决方案</blockquote><pre><code> clearNewlineSideEffect () {
const { range } = this.lastSelection
const node = range.startContainer
const baseNode = isAndroid ? node.parentNode : node
if (baseNode.nodeType === 1 && baseNode.className === 'tag' && !/^#/.test(baseNode.textContent)) {
const frag = document.createDocumentFragment()
const textNode = document.createTextNode(baseNode.textContent)
const brNode = document.createElement('br')
frag.appendChild(textNode)
frag.appendChild(brNode)
baseNode.parentNode.replaceChild(frag, baseNode)
}
},</code></pre><h2>知识点补充</h2><h3><code>selection</code>文本选择</h3><blockquote><code>selection</code>也可以实现<code>range</code>部分功能的范围选择</blockquote><ul><li><code>selection.setBaseAndExtent(anchorNode, anchorOffset, focusNode, focusOffset)</code>等同于对设置<code>range.setStart(anchorNode, anchorOffset)</code>、<code>range.setEnd(focusNode, focusOffset)</code></li><li>通过<code>selection.collapse(node, offset)</code>等同于对<strong>同一node</strong>设置<code>range.setStart(node, 0)</code>、<code>range.setEnd(node, offset)</code></li><li>通过<code>selection.setPosition(node, offset)</code>等同于对<strong>同一node</strong>设置<strong>同一偏移量</strong><code>range.setStart(node, offset)</code>、<code>range.setEnd(node, offset)</code>或<code>range.setStart(node, offset)</code>、<code>range.collapse(true)</code></li><li>通过<code>selection.deleteFromDocument()</code>从文档中删除所选择的内容</li></ul><h2>参考文档</h2><ul><li><a href="https://link.segmentfault.com/?enc=qxV1ucUnf3LPfP%2FzZPolqA%3D%3D.FFsgEKTbaSS0AcmgbLwhi0Oiqhpr1dZuAZ1GA7a9WaYhGCCwiLyEAw6IXJuI8U7%2F0mOmdratTwosAXsWhkdndQ%3D%3D" rel="nofollow">selection-range</a></li><li><a href="https://link.segmentfault.com/?enc=Q1bZwjAqKTj6bBANIw1Myw%3D%3D.pV7AOcewE0sFgFBa9w6H4f5ykb3MsPQZfSzyatBaoRMfFusfO5hlPI5iadX4EUVdevmwkykxJ%2BzRE1sX9SSXqy3A6OefMxJ44B4U6rkbs2M%3D" rel="nofollow">contenteditable+user-modify</a></li><li><a href="https://link.segmentfault.com/?enc=ksSaRR5c6fcBK3y27AxpvQ%3D%3D.UMnkQANI3Is8Ww4dDDEkRa6KupZtGnDsJSz0Mcd4b%2BF%2FRHKRACuB%2FYpiVaFNt8SGDUfA%2FptCZ%2BoiXjov4WLTKA%3D%3D" rel="nofollow">中英文长度限制</a></li><li><a href="https://link.segmentfault.com/?enc=Mbph2TxDb%2FxuKGx8eD7BKQ%3D%3D.jNwrWzYtztEtLqfl7p2KCDJVRFLHv0PdgayVROT6HRI%3D" rel="nofollow">编辑器</a></li><li><a href="https://link.segmentfault.com/?enc=NAfvWN%2B6CdwlIHtRkfD9Vw%3D%3D.icMenCyaGNJUroo%2FEVEeIBiqbjsoq3XdD0jPeDYNoEwA2aWFyviFlHv1w0%2F6xVQs" rel="nofollow">大文件断点续传</a></li></ul>
手机hybrid应用H5开发
https://segmentfault.com/a/1190000039242207
2021-02-21T08:02:00+08:00
2021-02-21T08:02:00+08:00
米花儿团儿
https://segmentfault.com/u/mihuartuanr
0
<h2>H5开发</h2><h4>引入flexible.js</h4><blockquote>在<code>*.html</code>的<code><head></code>标签中引入<code><script src="http://g.tbcdn.cn/mtb/lib-flexible/0.3.4/??flexible_css.js,flexible.js"></script></code></blockquote><p><strong>Note:</strong><code>HTML</code>中无需设置<code><meta name="viewport" content="width=device-width, initial-scale=1.0,user-scalable=no,minimum-scale=1.0,maximum-scale=1.0"></code>,否则,不管设备是多少的dpr,都会强制认为其dpr是你设置的值。</p><h4>通过<code>scss</code>定义px2rem的转换</h4><pre><code>@function px2em($px, $base-font-size: 16px) {
@if (unitless($px)) {
@warn "Assuming #{$px} to be in pixels, attempting to convert it into pixels for you";
@return px2em($px + 0px); // That may fail.
} @else if (unit($px) == em) {
@return $px;
}
@return ($px / $base-font-size) * 1em;
}</code></pre><h4>文本字号不建议使用<code>rem</code></h4><blockquote>我们希望文本在不同<code>dpr</code>屏幕下文本字号相同<p>我们希望在大屏手机上看到更多文本</p><p>不希望出现13px和15px这样的奇葩尺寸</p></blockquote><p><strong>文本还是使用px作为单位,只不过使用[data-dpr]属性来区分不同dpr下的文本字号大小</strong></p><pre><code>// dpr === 1, 设计图尺寸375 * 667为例
@mixin font-dpr($font-size){
font-size: $font-size;
[data-dpr="2"] & {
font-size: $font-size * 2;
}
[data-dpr="3"] & {
font-size: $font-size * 3;
}
}
// dpr === 2, 设计图尺寸750 * 1135为例
@mixin font-dpr($font-size){
font-size: $font-size / 2;
[data-dpr="2"] & {
font-size: $font-size;
}
[data-dpr="3"] & {
font-size: $font-size;
}
}</code></pre><pre><code>@include font-dpr(16px);</code></pre><h2>H5调试</h2><h4>手机开启USB调试功能</h4><p>不同手机开启路径不同,可以自行百度;</p><p>这里介绍一个通用的方式:</p><ol><li>打开手机出厂APP“设置”</li><li>通过“搜索”功能检索“USB调试”</li><li>点击检索结果,进入指定页面,打开“USB调试”功能</li></ol><h4>手机、电脑处于同一局域网</h4><p>电脑端启动<code>Web服务器</code>,将<code>H5</code>资源部署到服务器中,手机局域网内通过电脑<code>IP + path</code>访问<code>H5</code>页面。</p><p>示例:</p><pre><code>npm i -g browser-sync
cd <project dir path>
// 以项目路径创建web服务端
browser-sync start --server --files ./</code></pre><p>手机默认浏览器(非微信)通过PC端<code>IP + path</code>访问<code>H5</code>页面。</p><p><strong>若无法访问,确认处于同一局域网后,关闭PC端防火墙试试。</strong></p><h4>手机数据线连接电脑</h4><ul><li>手机数据线连接电脑</li><li>浏览器打开新标签页<code>chrome://inspect</code></li><li>静待一些时间,<code>Remote Target</code>中显示数据线连接的设备及其可访问页面</li><li>点击要调试的页面连接,即可调试</li></ul><p><strong>Note:</strong>调试过程中要保持手机不熄屏</p><h2>H5与客户端交互</h2><h4>H5唤起客户端</h4><h6><code>URL Scheme</code></h6><blockquote>旧的通用唤端解决方案:使用前需要确认是否已弃用</blockquote><p><code>URL Scheme</code>是H5和客户端、客户端和设备沟通的桥梁。</p><p>使用<code>URL Scheme</code>在保证个人信息在设备所有者知情并允许的情况下实现通信。</p><p><code>URL Scheme</code>的构成:<code>[scheme:][//authority][path][?query][#fragment]</code>,即:客户端APP在移动设备中的统一资源定位符。</p><p>而这里的<code>Scheme</code>是移动设备为每个客户端APP分配的标识符。</p><p>常见的<code>Scheme</code>:</p><table><thead><tr><th>微信</th><th>支付宝</th><th>淘宝</th><th>微博</th><th>QQ</th><th>知乎</th><th>短信</th></tr></thead><tbody><tr><td>weixin://</td><td>alipay://</td><td>taobao://</td><td>sinaweibo://</td><td>mqq://</td><td>zhihu://</td><td>sms://</td></tr></tbody></table><h6><code>Intent</code></h6><blockquote>安卓原生Google浏览器唤端协议</blockquote><p>整体结构如下:</p><pre><code>intent:
HOST/URI-path // Optional host
#Intent;
package=[string];
action=[string];
category=[string];
component=[string];
scheme=[string];
end;</code></pre><p>使用示例:</p><pre><code> <a href="intent://scan/#Intent;scheme=zxing;package=com.google.zxing.client.android;S.browser_fallback_url=http%3A%2F%2Fzxing.org;end"> Take a QR code </a></code></pre><h6><code>Universal Link</code></h6><blockquote>IOS9+</blockquote><h2><code>Hybrid</code>混合开发调试</h2><p><code>Hybrid</code>混合开发,即<code>H5</code>页面嵌入到原生客户端提供的Webview中渲染。</p><p>这里的调试大体和<code>H5</code>页面调试没什么不同,只要注意是否能调试和<strong>客户端是否屏蔽调试功能</strong>强关联。</p><p>调试可能需要客户端区别于线上应用,单独打可以调试的开发包(具体流程需要咨询客户端)。</p><h2>参考文档</h2><ul><li><a href="https://link.segmentfault.com/?enc=uhaSdIclR%2FDf%2FIpaRRGqRA%3D%3D.A%2BDF54By%2BgQBiFj0Xhd2fzDlIP%2FRq4qih0ImK9whsDWIUCtZf%2FIutcJDQcLBWdAj11QtEeMAus5W2DcASQweqg%3D%3D" rel="nofollow">H5唤起客户端</a></li></ul>
使用Flexible & swiper进行移动Web开发
https://segmentfault.com/a/1190000039241887
2021-02-20T19:59:40+08:00
2021-02-20T19:59:40+08:00
米花儿团儿
https://segmentfault.com/u/mihuartuanr
0
<h2>移动端适配方案<code>flexible.js</code></h2><h3>出发点</h3><p>事故的开始总是有原因的,那思考一下,为什么使用<code>Flexible</code>?</p><ul><li>[ ] 为了<code>rem</code>布局? >> 为什么要<code>rem</code>布局 ——> 高度还原</li><li>[ ] 移动端适配? >> 为什么移动端适配要用<code>Flexible</code> ——> 同等占比</li><li>[ ] 什么是移动端适配?</li></ul><p>最初点是设计图的高度还原!!!</p><p>以750*1334设计稿(iPhone6)为准,期望每一个设计元件在iPhone6实机上占比、宽高比都与设计稿显示一致。</p><p>一套代码又想扩展到其它设备,于是出现了<code>rem</code>布局</p><ul><li>[ ] <code>Flexible</code>做了什么</li><li>[ ] <code>rem</code>是什么</li><li>[ ] 如何做到高度还原设计图的</li></ul><h3>引入flexible.js</h3><blockquote>在<code>*.html</code>的<code><head></code>标签中引入<code><script src="http://g.tbcdn.cn/mtb/lib-flexible/0.3.4/??flexible_css.js,flexible.js"></script></code></blockquote><p><code>flexible.js</code>会做以下几件事:</p><ol><li><p>给<code>*.html</code>添加内联样式表</p><ul><li>只是reset一些样式</li></ul></li><li><p>给<code><html></code>设置<code>[data-dpr=""]</code>属性、<code>font-size </code>样式</p><ul><li><p><code>[data-dpr=""]</code>属性</p><ul><li>通过属性选择器,设置不同分辨率下的样式</li><li>主要是针对字号</li></ul></li><li><p><code>font-size</code></p><ul><li><code>rem</code>单位的基准尺寸</li></ul></li></ul></li><li>监听<code>resize</code>、<code>pageshow</code>事件来重新设置<code>html.style.fontSize</code></li><li>设置<code>body.style.fontSize</code></li><li><p>定义全局变量window.lib.flexible</p><pre><code>window.lib.flexible = {
dpr: number,
refreshRem: function,
rem2px: function,
px2rem: function
}</code></pre></li></ol><p><strong>Note:</strong><code>HTML</code>中无需设置<code>meta#viewport</code>,<code>flexible.js</code>会根据<code>window.devicePixelRatio</code>的值自动添加<code>meta#viewport</code>。</p><p>假设设置如下<code><meta name="viewport" content="width=device-width, initial-scale=1.0,user-scalable=no,minimum-scale=1.0,maximum-scale=1.0"></code>,不管设备是多少的dpr,<code>flexible.js</code>不再修改该元数据配置,都会强制认为其dpr是你设置的值。</p><p><strong>Note:</strong>在<code>Flexible</code>中,只对<code>iOS</code>设备进行<code>dpr</code>的判断,对于<code>Android</code>系列,始终认为其<code>dpr</code>为1</p><h4>对于<code>Android</code>系列,始终认为其<code>dpr</code>为1?</h4><p>深入<code>flexible.js</code>的实现,是<code>body#width</code>依据<code>devicePixelRatio</code>进行缩放的。</p><p>而针对<code>iOS</code>设备,缩放后正好为其<code>visual viewport</code>的尺寸。</p><p>而针对<code>Android</code>系列手头上的机器测试,不用缩放,<code>body#width</code>正好为其<code>visual viewport</code>的尺寸。</p><h4><code>body#width / dpr > 540</code>?</h4><p>若<code>body#width / dpr > 540</code>,如PC端<code>body#width > 1080</code>,<code>Flexible</code>当1080处理。</p><h3>通过<code>scss</code>定义px2rem的转换</h3><p>~在不使用自动化工具的基础上,无法使用以下处理方式,只能自己计算后再赋值~</p><pre><code>@function px2rem($px, $base-font-size: 16px) {
@if (unitless($px)) {
@warn "Assuming #{$px} to be in pixels, attempting to convert it into pixels for you";
@return px2rem($px + 0px); // That may fail.
} @else if (unit($px) == rem) {
@return $px;
}
@return ($px / $base-font-size) * 1rem;
}</code></pre><h3>原理:通过以上步骤,就能实现设计图的高度还原?</h3><p>本质上是设计元件的同比转换。</p><p><code>flexible.js</code>将<code>body#width</code>分成100份,一份为基准,设置为<code>html#fontStyle</code>,而<code>px2rem</code>将设计元件转换为设计图的占比<code>*%</code>,以<code>rem</code>为中介,在设备上的展示具有同等占比。</p><p>以此来实现设计图的高度还原。</p><p>然而,计算出来的结果可能非有效值,所以,浏览器的显示优化……</p><p>如:小于<code>12px</code>的字体、浮点数的精度……</p><h3>文本字号不建议使用<code>rem</code></h3><blockquote><p>我们希望文本在相同<code>dpr</code>屏幕下文本字号相同</p><p>我们希望在大屏手机上看到更多文本</p><p>不希望出现13px和15px这样的奇葩尺寸</p></blockquote><h4>解决方案</h4><blockquote>文本还是使用px作为单位,只不过使用[data-dpr]属性来区分不同dpr下的文本字号大小</blockquote><p>~在不使用自动化工具的基础上,无法使用以下处理方式,只能自己计算后再赋值~</p><pre><code>@mixin font-dpr($font-size){
font-size: $font-size;
[data-dpr="2"] & {
font-size: $font-size * 2;
}
[data-dpr="3"] & {
font-size: $font-size * 3;
}
}</code></pre><pre><code>@include font-dpr(16px);</code></pre><h3>补充知识点</h3><ul><li><p><code>flexible.js</code>使用<code>document.readyState API</code>获取<code>document</code>的加载状态</p><pre><code>document.addEventListener('readystatechange', event => {
if (event.target.readyState === 'interactive') {
// 等同于DOMContentLoaded 事件
}
else if (event.target.readyState === 'complete') {
// 表示 load 状态的事件即将被触发。
// 等同于loaded
}
});</code></pre></li></ul><h2>在<code>Vue</code>项目中的实战</h2><p>~项目由@vue/cli@4.5.13创建的~</p><h3>移动端适配</h3><h4>引入<a href="https://link.segmentfault.com/?enc=b75Iy6OqERdCfxr5zpIVAA%3D%3D.ViiuaA0zzJ%2Fmc2EV7%2BDKkjhYBZK7T61By7SLTlvXV9NNf1qacMfQUwavV3P%2FoOkJ" rel="nofollow"><code>flexible</code></a></h4><ul><li><p>安装<code>lib-flexible</code></p><pre><code> npm i -D lib-flexible</code></pre></li><li><p>项目中引入<code>flexible</code></p><pre><code># main.js
import "lib-flexible/flexible";</code></pre></li></ul><p><strong>Note:</strong> 直接在<code>HTML</code>模板的<code><head></code>中引用,执行的优先级会更高</p><h4>样式<code>px</code>自动转<code>rem</code></h4><ul><li><p>安装<code>postcss-plugin-px2rem</code></p><pre><code> npm i -D postcss-plugin-px2rem</code></pre></li><li><p>创建<code>.postcssrc.js</code></p><pre><code># .postcssrc.js
module.exports = {
plugins: {
'postcss-plugin-px2rem': {
rootValue: 108, //换算基数,设计图尺寸, 默认100
exclude: /(node_module)/, //默认false,可以(reg)利用正则表达式排除某些文件夹的方法,例如/(node_module)\/
mediaQuery: false, //(布尔值)允许在媒体查询中转换px。
minPixelValue: 3, //设置要替换的最小像素值(3px会被转rem)。 默认 0
}
}
}</code></pre></li><li><p>凡事有例外<br><strong>若不想转换<code>rem</code>,单位使用全大写<code>PX</code>,如字体</strong></p><pre><code># presets.scss
$largeFont: 40PX;
$middleFont: 20PX;
$standardFont: 14PX;
$smallFont: 10PX;
@mixin font-dpr($font){
font-size: $font;
[data-dpr="2"] & {
font-size: $font * 2;
}
[data-dpr="3"] & {
font-size: $font * 3;
}
}</code></pre><pre><code># example.scss
...
@import "../../assets/scss/preset";
.page-title{
@include font-dpr($standardFont);
}
...</code></pre></li></ul><h3>规范提交记录</h3><p>通过以下命令<code>@vue/cli</code>会自动安装<code>commitlint</code>插件并更新<code>pkg</code></p><pre><code> vue add commitlint</code></pre><h3><code>swiper</code>应用</h3><ul><li><p>安装依赖</p><pre><code> npm install swiper vue-awesome-swiper --save</code></pre></li><li><p>全局注册</p><pre><code> # main.js
...
import VueAwesomeSwiper from "vue-awesome-swiper";
import "swiper/swiper.scss";
Vue.use(VueAwesomeSwiper, {});
...</code></pre></li><li><p>逻辑应用</p><pre><code># Example.vue
<template>
<swiper ref="mySwiper" :options="swiperOptions">
<swiper-slide v-for="i in 5">
<div>
Slide {{i}}
</div>
<button @click="handleClickItem(i)">点击后才可滑动</button>
</swiper-slide>
</swiper>
</template>
<script>
export default {
name: "Example",
data() {
return {
swiperOptions: {
direction: "vertical",
},
};
},
methods: {
handleClickItem(idx) {
// 满足某条件后可滑动
this.swiper.allowSlideNext = true;
},
},
computed: {
swiper() {
return this.$refs.mySwiper.$swiper;
},
},
mounted() {
// 默认禁止滑动翻页
this.swiper.allowSlidePrev = false;
this.swiper.allowSlideNext = false;
// 翻页后禁止翻页
this.swiper.on("slideChange", () => {
this.swiper.allowSlideNext = false;
this.currentIdx = this.swiper.activeIndex;
});
},
};
</script></code></pre></li><li><p>样式</p><pre><code># example.scss
.swiper {
&- {
&container {
height: 100%;
}
&slide {
height: 100%;
color: white;
}
}
}
button {
position: absolute;
z-index: 100;
bottom: 20PX;
width: 100%;
text-align: center;
}</code></pre></li></ul><h2>非标准字体</h2><blockquote>非标准字体是指人为调整<strong>系统设置</strong>中的<strong>字体字号</strong>大小。</blockquote><pre><code>(function () {
var $dom = document.createElement("div");
$dom.style = "font-size:20px;";
document.body.appendChild($dom);
// 计算出放大后的字体
var scaledFontSize = parseInt(
window.getComputedStyle($dom, null).getPropertyValue("font-size")
);
document.body.removeChild($dom);
// 计算原字体和放大后字体的比例
var scaleFactor = 20 / scaledFontSize;
// 取html元素的字体大小
// 注意,这个大小也经过缩放了
// 所以下方计算的时候 *scaledFontSize是原来的html字体大小
// 再次 *scaledFontSize才是我们要设置的大小
var originRootFontSize = parseInt(
window
.getComputedStyle(document.documentElement, null)
.getPropertyValue("font-size")
);
document.documentElement.style.fontSize =
originRootFontSize * scaleFactor * scaleFactor + "px";
alert(
parseInt(
window
.getComputedStyle(document.documentElement, null)
.getPropertyValue("font-size")
)
);
})();</code></pre><p>上述章节我们说【不推荐】字号使用<code>rem</code>,而使用<code>px</code>,是因为想让不同设备像素比的设备更大的程度利用展示空间:大屏一行展示的文本更多,小屏展示的少一些。</p><p>而考虑非标准字体,若字号使用<code>px</code>,即使通过上述方法调整<code>html</code>的基准字号大小,也不会影响字体的展示。</p><p>所以,考虑非标准字体时,字号需要使用<code>rem</code>设置。</p><h2>Pad适配</h2><p>现在<code>Pad</code>盛行,要考虑适配<code>Pad</code>.</p><p>对于<code>Pad</code>的适配,<code>Flexible</code>并没有考虑,而我们可以稍作修改进行适配。</p><ul><li><code>Flexible</code>并没有考虑<code>Pad</code>,通过修改<code>var isIPhone = win.navigator.appVersion.match(/iphone|iPad/gi);</code>调整</li><li><p><code>Flexible</code>针对设备有最大尺寸限制,将其去掉即可</p><pre><code> function refreshRem(){
var width = docEl.getBoundingClientRect().width;
// if (width / dpr > 540) {
// width = 540 * dpr;
// }
var rem = width / 10;
docEl.style.fontSize = rem + 'px';
flexible.rem = win.rem = rem;
}</code></pre></li><li><p><code>px2rem</code>的媒体查询配置</p><ul><li>删除<code>postcss-plugin-px2rem</code>插件及其配置</li><li><p>自定义px转rem函数 —— 为了媒体查询</p><pre><code>//# px2rem.scss
$defaultFontSize: 108;
@function px2rem($px, $baseFontSize: $defaultFontSize) {
@return $px / $baseFontSize * 1rem;
}</code></pre></li></ul><pre><code>.main {
width: px2rem(540);
@media screen and (min-width: 450px) {
width: px2rem(1660, 166);
}
}</code></pre></li></ul><h2>参考文档</h2><ul><li><a href="https://link.segmentfault.com/?enc=AaATgNsgzwbzsTWNFaarxA%3D%3D.n0Yj1s8lif2nenS3Z%2ByEvHVIFbSJTlcOga%2FoDGQx2uA%3D" rel="nofollow">阿里移动端知识储备</a></li><li><a href="https://link.segmentfault.com/?enc=4UX80c0qiDYIuvOwNgqYNg%3D%3D.rUP687ILI5MfHl0KI%2BgQLxoStTF%2BNjGlh3qRFL9dx5lII11a%2B0aMdC7m3pitLptB" rel="nofollow">Flexible官方文档</a></li><li><a href="https://link.segmentfault.com/?enc=%2FIKC%2FBfSMBEPVd3BGPKw0Q%3D%3D.GvKYi9MHUbBQSPd0Szl7AhPPVmblVIC68sLw4vUaiH0JjbeGulxaiSyEp4XPqFuDHPhWQpfIz5rZS6OiyS2Baw%3D%3D" rel="nofollow"><code>viewport</code>的介绍1</a></li><li><a href="https://link.segmentfault.com/?enc=qnHddjyaBk0jKLp7GMg8Og%3D%3D.QwkUsJ9mdo75GoH%2FWl%2BwNVbTKKfzrm6zY%2FVkgJiTKHUyVe8sv2G1GR%2FjcsNBF7aD%2BfOzTizH62EhWCZRwmF0HQ%3D%3D" rel="nofollow"><code>viewport</code>的介绍2</a></li><li><a href="https://link.segmentfault.com/?enc=U08j09PBxd2h9qu75eZwDQ%3D%3D.%2FoB1%2FdT9zZXeXql2c095zlHUemGkbDjNldwvuQQiqP%2BRYZump%2F3dObUEfjxldzEMmBQDDqX0P0Dex89oTzCU0A%3D%3D" rel="nofollow">iphone12适配</a></li><li><a href="https://link.segmentfault.com/?enc=dKx5OIVtPQBqw5rsND%2BqGg%3D%3D.BmDqDiOsbj395S%2FmquSTABziYcfKt3wq8%2FvCZ9LZTaBylL27SQwrscqFV23JCRuV" rel="nofollow">swiper相关配置</a></li><li><a href="https://link.segmentfault.com/?enc=%2F0wf8384aAdvFl%2BTH1mMcA%3D%3D.Aa6p49nVIxJwjUXLHR0zW%2FbQQBBHNQlIDhIG7HyH06tTjTz69vyGDqALZuFQ6Jw3ACiqrIbHUAqxcRhc9FWUlQ%3D%3D" rel="nofollow">非标准字体</a></li></ul>
基于Vue-cli初始化项目的多项目管理
https://segmentfault.com/a/1190000039209558
2021-02-13T17:14:27+08:00
2021-02-13T17:14:27+08:00
米花儿团儿
https://segmentfault.com/u/mihuartuanr
0
<blockquote>在Vue-cli 3.X环境下,基于同一类型的活动,可以多个页面复用,大部分组件可以公用的背景</blockquote><h2>Multiple处理方式</h2><ul><li>每一个活动创建一个分支,在不同的分支上各自维护</li><li>如果需要维护复用代码时,任选某一分支进行修改,通过<code>git cherry-pick <commit id></code>进行平行迁移。</li></ul><h2>Monorepo处理方式</h2><p>仅在同一分支下进行多项目的维护,各个功能模块解构,通过项目配置项进行个性化配置。</p><h4>目录结构</h4><pre><code>|-src
|- views
|- index.js // 通用页面的统一入口
|- Company
|- index.vue // 通用页面Company结构、样式、逻辑
|- index.js // Company页面路由
|- Rule
|- index.vue
|- index.js
|- components
|- core
|- instance // 和app实例挂钩的方法
|- libs // 和app实例无关的方法
|- assets
|- images
|- fonts
|- store
|- index.js // 通用状态
|- types.js // 事件类型
|- config
|- proA.js // 项目资源配置
|- proB.js
|- projects // 项目定制资源
|- proA
|- proB</code></pre><p><strong>不同项目的区别完全在于<code>config/</code>文件的配置和<code>projects/</code>下的项目定义;同级其余目录是各个项目通用的内容。</strong></p><h4>提取公共页面 & 路由</h4><h6>公共页面示例:</h6><pre><code>// src/views/Company/index.vue
<template>
...
</template>
<script>
...
</script>
<style scoped>
...
</style></code></pre><h6>公共页面路由</h6><pre><code>// src/views/Company/index.js
export default [
{
path: '/company',
name: 'company',
component: () => import(/* webpackChunkName: "company" */ './index.vue'),
meta: {
title: '公司简介'
}
}
]</code></pre><h6>公共页面统一入口</h6><pre><code>// src/views/index.js
export { default as companyRoute } from './Company/index.js'
export { default as ruleRoute } from './Rule/index.js'</code></pre><h6>定制项目中的公共页面</h6><pre><code>// src/config/proA.js
import {
companyRoute,
ruleRoute
} from '../views/index.js'
...
export const logoUrl = '' // 还可以定制其它的内容
export const routes = [
...companyRoute,
...ruleRoute
]</code></pre><h6>项目中使用公共页面</h6><p>以<code>src/projects/proA</code>为例:</p><blockquote>目录结构</blockquote><pre><code>|- assets
|- components
|- mixins
|- router
|- store
|- pages
|- App.vue
|- main.js</code></pre><blockquote>项目主路由</blockquote><pre><code>// src/projects/proA/router/index.js
import Vue from 'vue'
import Router from 'vue-router'
import { routes } from '../../config/proA'
import Home from '../pages/Home'
Vue.use(Router)
export default new Router({
routes: [
{
path: '/',
redirect: '/home'
},
{
path: '/home',
name: 'Home',
component: Home,
meta: {
title: ''
}
},
...routes
]
})</code></pre><p>其中:Home/index.vue是定制化的。</p><h4>状态管理</h4><blockquote><strong>多项目是独立运行时,状态提取不会互相干扰,若一次性运行多个项目,通用状态会被修改。</strong></blockquote><h5>通用状态提取</h5><pre><code>// src/store/index.js
import types from './types'
export const initialState = {
userInfo: {},
...
}
export function getGetters (store) {
return {
userId: () => store.userInfo.userID,
...
}
}
export function getMutations (store) {
return {
[types.INITIALMUTATIONTYPES.USER_INFO] (val) {
store.userInfo = val
},
...
}
}</code></pre><p>在<code>config/proA.js</code>文件中追加:</p><pre><code>...
export * from '../store/index.js'
export * from '../store/types.js'
...</code></pre><h5>项目中使用</h5><blockquote>小型项目,使用<code>vue.observable</code>管理状态</blockquote><h6>定义项目的主状态管理</h6><pre><code>// src/projects/proA/store/index.js
import vue from 'vue'
import { initialState, getGetters, getMutations } from '../../../config/proA'
export const store = vue.observable({
...initialState,
customState: '', // 项目私有状态
...
})
store._getters = {
...getGetters(store),
customGetter() { // 项目私有
return store.customState
},
...
}
store._mutations = {
...getMutation(store),
... // 项目私有
}
export const mutation = {
...getMutations(store),
... // 项目私有
}</code></pre><h6>定义辅助方法<code>mapGetters</code></h6><blockquote>拷贝<code>vuex</code>部分代码到<code>src/core/libs/helpers.js</code>文件中</blockquote><pre><code>export const mapGetters = (getters) => {
const res = {}
if (!isValidMap(getters)) {
console.error('[vuex] mapGetters: mapper parameter must be either an Array or an Object')
}
normalizeMap(getters).forEach(({ key, val }) => {
res[key] = function mappedGetter () {
if (!(val in this.$store._getters)) {
console.error(`[vuex] unknown getter: ${val}`)
return
}
return this.$store._getters[val]()
}
})
return res
}
export function normalizeMap (map) {
if (!isValidMap(map)) {
return []
}
return Array.isArray(map)
? map.map(key => ({ key, val: key }))
: Object.keys(map).map(key => ({ key, val: map[key] }))
}
export function isValidMap (map) {
return Array.isArray(map) || isObject(map)
}
export function isObject (obj) {
return obj !== null && typeof obj === 'object'
}</code></pre><p>在<code>/src/core/libs/index.js</code>中追加:</p><pre><code>export * from './helpers'</code></pre><h6><code>*.vue</code>中使用</h6><pre><code>// src/projects/proA/pages/Home/index.vue
<script>
...
import { mapGetters } from '../../../core/libs/'
export default {
data () {
return {
...
}
},
computed: {
...mapGetters([
'userId'
]),
...
}
...
</script></code></pre><h4>组件管理</h4><h6>组件统一入口</h6><blockquote>借助<code>webpack</code>的<code>require.context</code>方法将<code>/components/</code>下的组件整合</blockquote><pre><code>const ret = {}
const requireComponent = require.context(
'./', // 指定递归的文件目录
true, // 是否递归文件子目录
/[A-Z]\w+\.(vue)$/ // 落地文件
)
requireComponent.keys().forEach(fileName => {
const componentConfig = requireComponent(fileName)
const component = componentConfig.default || componentConfig
const componentName = component.name || fileName.split('/').pop().replace(/\.\w+$/, '')
// ret[componentName] = () => requireComponent(fileName)
ret[componentName] = component
})
export default ret</code></pre><h6>定义布局配置</h6><pre><code>// config/proA.js追加
...
export const layouts = {
Home: [
{
componentType: 'CompA',
request: {
fetch () {
const res = []
return res
}
},
response: {
filter (res) {
return []
},
effect () {
}
}
},
{
componentType: 'CompB'
},
{
componentType: 'CompC'
},
{
componentType: 'CompD'
}
]
}</code></pre><h6>项目中使用</h6><p><code>proA/Home/index.vue</code></p><pre><code><template>
...
<template v-for="componentConfig of layouts">
<component
v-bind="dataValue"
:is="componentConfig.componentType"
:key="componentConfig.componentType"
>
</component>
</template>
...
</template>
<script>
...
import {
CompA,
CompB
} from '../../components/'
import { layouts } from '../../config/proA'
...
export default {
...
data () {
return {
...
layouts: layouts.Home,
...
}
},
...
components: {
CompA,
CompB
},
...
}
</script></code></pre><h2>参考文档</h2><ul><li><a href="https://link.segmentfault.com/?enc=q4H%2B%2BBw7mTsFKe%2F9Nl8fiQ%3D%3D.TFvM5HNhz8nJApw%2F9fgtOFYj6NDor5%2BEZmX7471IMs6WQcxwL7%2BwWwEWYzHXSqJlFOQlPrfB7A2XA1vzZwMPPA%3D%3D" rel="nofollow">vue不同环境打包命令配置</a>: 大部分内容来源</li><li><a href="https://link.segmentfault.com/?enc=7ewkB%2FdSDkOxtj%2BgFgxjuQ%3D%3D.QtLujio4rV6F2f44fs49grltPRFTTJ3tdz36314Z%2BRlaszreIR%2BRi4ROvthWS9y25oheFHs2xpBiHe2yhySGNQ%3D%3D" rel="nofollow">使用 MonoRepo 管理前端项目</a></li></ul>
基于快应用开发问题的自动化命令行
https://segmentfault.com/a/1190000039193557
2021-02-08T11:40:40+08:00
2021-02-08T11:40:40+08:00
米花儿团儿
https://segmentfault.com/u/mihuartuanr
0
<h2><code>Git</code>校验</h2><blockquote>你记得这次打包是不是当前调试的代码么?</blockquote><pre><code>const sh = require('shelljs')
const chalk = require('chalk')
const uncommit = sh.exec('git status --porcelain').stdout
const lines = uncommit.toString().split('\n').filter(item => item).length
if (lines) {
console.log(chalk.red('note: 有代码未提交或缓存,请慎重发版!!!'))
console.log('----------------------------------------------------')
console.log('recommend: 使用git push提交 或 git stash缓存后再发版.')
process.exit(1)
}
process.exit(0)</code></pre><blockquote>每次打包没有日志,强制开发者必须上传或缓存后才能打包,利用Git为打包做日志服务。</blockquote><pre><code>const sh = require('shelljs')
const chalk = require('chalk')
const inquirer = require('inquirer')
const log = console.log
async function permission () {
const uncommit = sh.exec('git status --porcelain').stdout
const lines = uncommit.toString().split('\n').filter(item => item).length
if (lines) {
log(chalk.redBright(chalk.bold('note: ') + '有代码未提交或缓存,请慎重发版!!!'))
log('----------------------------------------------------')
log(chalk.blueBright('recommend: ') + '使用git push提交 或 git stash缓存后再发版.')
const { isSkip } = (await inquirer.prompt({
type: 'expand',
name: 'isSkip',
choices: [
{
key: 'y',
name: 'Yes',
value: true
},
{
key: 'n',
name: 'No',
value: false
}
],
message: '是否一意孤行?'
}))
if (isSkip) {
process.env.custom_uncommit_skip = isSkip
process.exit(0)
}
process.exit(1)
}
process.exit(0)
}
permission()</code></pre><blockquote>使用场景</blockquote><pre><code>"prerelease": "node bin/index.js"</code></pre><h2>自动版本号【简约版】</h2><pre><code>const fs = require('fs')
const path = require('path')
const handleFile = path.resolve(process.cwd(), 'src', 'manifest.json')
const rawText = fs.readFileSync(handleFile)
const manifestJson = JSON.parse(rawText)
console.log(manifestJson)
manifestJson.versionCode++
fs.writeFileSync(handleFile, JSON.stringify(manifestJson, '', 2))</code></pre><h4><code>JSON.stringify</code>的使用</h4><p><code>JSON.stringify()</code>接受三个参数</p><ul><li>data:JSON数据</li><li>function:序列化函数——执行序列化时,被序列化的每个属性都会经过该函数的转换和处理。</li><li>Number | String:指定缩进用的空白字符串来美化输出——Number标识新行空格的个数;字符串标识<code>\t</code>、<code>\n</code>之类</li></ul><blockquote>使用场景</blockquote><pre><code>"prerelease": "node bin/git-uncommit.js && node bin/auto-version.js",</code></pre><h2>完善自动版本号</h2><blockquote>参考<code>lerna version</code></blockquote><ul><li>通过命令行交互选择版本更新级别</li><li>根据对应的版本级别借助<code>semver.inc</code><strong>升级版本</strong></li></ul><pre><code>const fs = require('fs')
const path = require('path')
const sh = require('shelljs')
const chalk = require('chalk')
const semver = require('semver')
const inquirer = require('inquirer')
const terminal = inquirer.prompt
const quickJSONFile = path.resolve(process.cwd(), 'src', 'manifest.json')
const manifestJson = JSON.parse(fs.readFileSync(quickJSONFile))
function customVersion(message, { filter, validate } = {}) {
return terminal([
{
type: "input",
name: "input",
message,
filter,
validate,
},
])
.then(answers => {
return answers.input;
});
}
/**
* @param {PackageGraphNode|Object} node The metadata to process
* @property {String} currentVersion
* @property {String} name (Only used in independent mode)
* @property {String} prereleaseId
*/
function promptVersion(currentVersion, name, prereleaseId) {
const patch = semver.inc(currentVersion, "patch");
const minor = semver.inc(currentVersion, "minor");
const major = semver.inc(currentVersion, "major");
const prepatch = semver.inc(currentVersion, "prepatch", prereleaseId);
const preminor = semver.inc(currentVersion, "preminor", prereleaseId);
const premajor = semver.inc(currentVersion, "premajor", prereleaseId);
const message = `Select a new version ${name ? `for ${name} ` : ""}(currently ${currentVersion})`;
return terminal({
type: 'list',
message,
name: 'choice',
choices: [
{ value: patch, name: `Patch (${patch})` },
{ value: minor, name: `Minor (${minor})` },
{ value: major, name: `Major (${major})` },
{ value: prepatch, name: `Prepatch (${prepatch})` },
{ value: preminor, name: `Preminor (${preminor})` },
{ value: premajor, name: `Premajor (${premajor})` },
{ value: "PRERELEASE", name: "Custom Prerelease" },
{ value: "CUSTOM", name: "Custom Version" },
]
}).then(({choice}) => {
if (choice === "CUSTOM") {
return customVersion("Enter a custom version", {
filter: semver.valid,
// semver.valid() always returns null with invalid input
validate: v => v !== null || "Must be a valid semver version",
});
}
if (choice === "PRERELEASE") {
const defaultVersion = semver.inc(currentVersion, "prerelease", prereleaseId);
const prompt = `(default: "${prereleaseId}", yielding ${defaultVersion})`;
return customVersion(`Enter a prerelease identifier ${prompt}`, {
filter: v => semver.inc(currentVersion, "prerelease", v || prereleaseId),
});
}
return choice;
});
}
promptVersion(manifestJson.versionName).then(nextVersion => {
try {
manifestJson.versionName = nextVersion
manifestJson.versionCode++
fs.writeFileSync(quickJSONFile, JSON.stringify(manifestJson, '', 2))
console.log(chalk.whiteBright(`--------------path:${quickJSONFile}--------------------`))
console.log(`${chalk.whiteBright(chalk.bold('Has been configured a new version:'))}${chalk.blueBright(nextVersion)}`)
console.log(chalk.whiteBright(`--------------releasing-releasing-releasing------------------`))
if (!process.env.custom_uncommit_skip) {
sh.exec('git commit --amend --no-edit')
}
process.exit(0)
} catch (e) {
console.log(chalk.redBright(JSON.stringify(e)))
process.exit(1)
}
})</code></pre><h2>自动生成更新日志</h2><h2>第三方工具</h2><h4><a href="https://link.segmentfault.com/?enc=mtuEb%2FhP2ajAKTQzLFMDtQ%3D%3D.5dej4WAcN%2BhDwkahst5RXxxDRI4VE82Op2SeTSUVIBu3rDDYDHIEidqon5tIt903ynBMJGjUbjV1%2B5SSag3Plg%3D%3D" rel="nofollow">standard-version</a>:</h4><p>执行<code>npx standard-version</code>,会进行以下步骤:</p><ol><li>在<code>packageFiles</code>文件中查询当前版本号</li><li>基于当前版本号 & <code>commit</code>(的<code>type</code>)升级<code>bumpFiles & packageFiles</code>文件中的版本号</li><li>生成提交日志(底层借助<code>conventional-changelog</code>)</li><li>生成新的commit</li><li>生成新的tag</li></ol><p><a href="https://link.segmentfault.com/?enc=OLfcqgWYXQ%2B5lROXej6Rsg%3D%3D.MkT5DyI%2BbyaUKomQj%2B1V6AggLEXjzaVNZAMpgmXEn5fVdp6mvdsgDrxRmgv8wgsKVSpPfKOYTvVcxVLfRqsd9Bgew7dnXON5SeUj6bkkuwD0qHRtnoyG54mklsHuV%2Frx" rel="nofollow">bumpFiles vs. packageFiles概念介绍</a>:可以借助文章后续的定制化理解,简单来说,<code>packageFiles</code>是输入,<code>packageFiles</code> + <code>bumpFiles</code>是输出</p><h4>用法</h4><blockquote>基础用法</blockquote><pre><code>npx standard-version</code></pre><blockquote>指定Tag前缀</blockquote><p>默认是'v-*'</p><pre><code>standard-version -t <prefix></code></pre><blockquote>首次提交</blockquote><p>指定首次提交,不会自动升级版本号</p><pre><code>npx standard-version --first-release</code></pre><blockquote>指定预发版及Tag前缀</blockquote><p>格式</p><pre><code>standard-version --prerelease <prefix></code></pre><p>示例</p><pre><code># npm run script
npm run release -- --prerelease alpha</code></pre><blockquote>指定版本号</blockquote><p>忽略自动升级版本功能</p><pre><code># npm run script
npm run release -- --release-as minor
# Or
npm run release -- --release-as 1.1.0</code></pre><blockquote>忽略Git钩子校验</blockquote><pre><code>standard-version --no-verify</code></pre><h4>定制化</h4><blockquote>默认配置</blockquote><p>在项目根目录下创建<code>.versionrc | .versionrc.js | .versionrc.json</code>定制使用<code>standard-version</code>。</p><p>在这之前,需要了解默认配置,默认项是以下的合集</p><ul><li><a href="https://link.segmentfault.com/?enc=xSBjo72UTjiMpWl83%2BWAGw%3D%3D.Eb5A5%2FPE6P482LgJ5vdewhsU%2BCQFKyn4i4HrL%2FCnqFfNW7PmOzOWPYVbe%2BXvh0C402aFrAL%2F7NtBKbcz1iaZ%2Bp%2Fh8wMPa%2F56edEsO3veqtvdIK5tFC%2FSmaM9JLRJ8aKY" rel="nofollow"><code>standard-version</code>的<code>default.js</code></a></li><li><a href="https://link.segmentfault.com/?enc=zK4QPVCii4rNKxex65P9CQ%3D%3D.%2Bq%2FJE3YNXLe6GbbqSGhvOZdo9%2FdnknIw0%2B0Xyu0CSql8pGqgpwLgTl2tWCdcYzJbxdrgUcWHY7vvl4GI87wIXiMP%2BTqHRfrf6InGxowaROZZTeEpis20aXpWozBwg59YRN9%2Be%2BNtHuyfIUOS%2B36hXUa%2FABFuWB1dniTEQuD334w%3D" rel="nofollow">本文使用<code>standard-version</code>时,引入的<code>conventional-changelog^2.1.0</code></a></li></ul><p>以<code>package.json</code>文件为例:<code>standard-version</code>默认修改的是<strong>顶层的<code>version</code>字段</strong>,如果想要修改其它结构中的其它字段,除了配置<code>bumpFiles & packageFiles</code>,还需要自定义<code>updater</code>。</p><p>快应用场景示例:</p><pre><code>// .versionrc.js
const handleFile = [
{
"filename": "src/manifest.json",
"type": "json",
"updater": require('./bin/custom-version-updater')
}
]
module.exports = {
"packageFiles": handleFile,
"bumpFiles": handleFile
}</code></pre><pre><code>// bin/custom-version-updater.js
const stringifyPackage = require('stringify-package')
const detectIndent = require('detect-indent')
const detectNewline = require('detect-newline')
module.exports.readVersion = function (contents) {
return JSON.parse(contents).versionName
}
module.exports.writeVersion = function (contents, version) {
const json = JSON.parse(contents)
let indent = detectIndent(contents).indent
let newline = detectNewline(contents)
json.versionCode++
json.versionName = version
return stringifyPackage(json, indent, newline)
}</code></pre><p><a href="https://link.segmentfault.com/?enc=fsvWuetZFf2oGn%2ByYv5OBA%3D%3D.saw7nuWH%2Fhruk%2BniN%2FIFK7tWphNeeq43ndw%2FhgduJAqYIXQwWAiJ%2F494nIsYn76KO0pRoThnS8otbRW%2BoKZcUROqa6qlZHiAf%2F%2B1KOQH%2BUg%3D" rel="nofollow">自定义adapter官方文档</a></p><hr>
lerna + workspaces使用手册
https://segmentfault.com/a/1190000039077541
2021-01-26T10:01:00+08:00
2021-01-26T10:01:00+08:00
米花儿团儿
https://segmentfault.com/u/mihuartuanr
0
<h2><code>lerna</code>项目管理方式</h2><h3>固定模式</h3><blockquote>默认的模式<p>版本号使用lerna.json文件中的version属性。</p><p>执行lerna publish时,如果代码有更新,会自动更新此版本号的值。即:所有的包公用一个版本号</p><p>使用方式:<code>lerna init</code></p></blockquote><h3>独立模式</h3><blockquote>允许维护人员独立的增加修改每个包的版本,每次发布,所有更改的包都会提示输入指定版本号。<p>使用方式:<code>lerna init --independent</code> 或 修改<code>lerna.json</code>中的<code>version</code>值为<code>independent</code>,可将固定模式改为独立模式运行。</p></blockquote><h2>初始化项目</h2><blockquote><code>lerna</code>提供一种集中管理package的目录模式,提供了一套自动化管理程序</blockquote><p><code>git init lerna-demo & cd $_</code></p><p><code>npm i -g lerna</code></p><p><code>lerna init</code></p><pre><code>// 初始化后目录结构
|—— packages // 空目录
|—— lerna.json
|—— package.json</code></pre><h2>配置文件<code>lerna.json</code></h2><pre><code>{
"npmClient": "yarn", // 执行命令所用的客户端,默认为npm —— 配置后会强制使用最佳实践:能用yarn的用yarn——如lerna bootstap --hoist不再可用
"command": { // 命令相关配置
"publish": { // 发布时配置
"allowBranch": "master", // 只在master分支执行publish
"conventionalCommits": true, // 生成changelog文件
"exact": true, // 准确的依赖项
"ignoreChanges": ["ignored-file", "*.md"], // 发布时忽略的文件
"message": "chore(release): publish" // 发布时的自定义提示消息
},
"bootstrap": { // 安装依赖配置
"ignore": "component-*", // 忽略项
"npmClientArgs": ["--no-package-lock"], // 执行 lerna bootstrap命令时传的参数
"hoist": true
},
"version": {
"conventionalCommits": true //开启日志:自动生成changLog.md
}
},
"packages": [ // 指定存放包的位置
"packages/*"
],
"version": "0.0.0" // 当前版本号
}</code></pre><h2>启用workspaces环境</h2><p>手动修改根目录下<code>package.json</code></p><pre><code>{
"name": "root",
"private": true,
"workspaces": ["packages/*"],
"devDependencies": {
"lerna": "^3.22.1"
}
}</code></pre><h2>创建模块</h2><p><code>lerna create @demo/cli</code></p><p><code>lerna create @demo/cli2</code></p><p><code>yarn workspaces info</code> // 查看工作区</p><h3>安装/删除依赖</h3><ul><li><code>yarn workspace packageB add packageA</code> // 给某个package安装依赖</li><li><code>yarn workspace package-b add package-a@0.0.0</code>或<code>larna add package-a --scope=package-b</code> // 将packageA作为packageB的依赖进行安装</li></ul><p>// ^ == <code>yarn workspace</code>安装本地包,第一次必须加上<code>lerna.json</code>中的版本号(后续一定不要再加版本号),否则,会从 <code>npm.org</code>远程检索安装</p><ul><li><code>yarn add -W -D typescript</code> // 在root下安装公用依赖typescript</li></ul><h2><code>lerna</code>发布流程</h2><h3>日志规范</h3><blockquote><code>commitizen</code>是用来格式化<code>git commit message</code>的工具,它提供了一种问询式的方式去获取所需的提交信息。<p><code>cz-lerna-changelog</code>是专门为<code>Lerna</code>项目量身定制的提交规范,在问询的过程,会有类似影响哪些 package 的选择</p></blockquote><p><code>yarn add -D commitizen</code></p><p><code>yarn add -D cz-lerna-changelog</code></p><h3>代码风格</h3><p><code>yarn add -D standard lint-staged</code></p><blockquote><code>lint-staged</code>中的<code>staged</code>是<code>Git</code>里的概念,表示暂存区,<code>lint-staged</code>表示只检查并矫正暂存区中的文件。一来提高校验效率,二来可以为老的项目带去巨大的方便。</blockquote><h3>上传自动生成日志</h3><p><code>lerna version</code> // 上传项目</p><p>// 自动实现以下功能</p><ul><li>找出从上一个版本发布以来有过变更的 package</li><li>提示开发者确定要发布的版本号</li><li>将所有更新过的的 package 中的package.json的version字段更新</li><li>将依赖更新过的 package 的 包中的依赖版本号更新</li><li>更新 lerna.json 中的 version 字段</li><li>提交上述修改,并打一个 tag</li><li>推送到 git 仓库</li></ul><h2>参考文档</h2><ul><li><a href="https://link.segmentfault.com/?enc=bl5fPZp4S%2FMhhLaobMd0KQ%3D%3D.beJSqqEnmWMIeWf2EzGmcNESV3VebLG4xyA9OX4kxN8%3D" rel="nofollow">lerna官网</a></li><li><a href="https://link.segmentfault.com/?enc=SzfXshxy3BP0J%2BWSLMutYQ%3D%3D.5oJp3zJgcAGgFjhnPupIRcdqaz0QPaqi6hhtxS8qC9IxiS6XQ1Drci33y7i%2BT00nJlbHy4zW837xy79x1Jlv5AQhjWW3dCZx%2FgIWgLV8kog%3D" rel="nofollow">yarn官方文档</a></li><li><a href="https://link.segmentfault.com/?enc=z%2FDcXVWQLnEPKqOPHZvKtw%3D%3D.93Pp6ZDIV4sZ86hphpPLs9Y37hJn7YC79mtYIfl3qz%2BrsI8FOtczLIAmIyNOTSqjGgnQuKFCVNrzyE2I1aOdTg%3D%3D" rel="nofollow">lerna+workspace最佳实践</a></li></ul>
Monorepo项目管理:lerna + workspaces
https://segmentfault.com/a/1190000039077480
2021-01-25T00:05:55+08:00
2021-01-25T00:05:55+08:00
米花儿团儿
https://segmentfault.com/u/mihuartuanr
1
<p>这里主要介绍<code>lerna</code>、<code>yarn workspaces</code>的使用方法与职能界限。</p><ul><li><code>lerna</code>:项目管理与发版</li><li><code>workspaces</code>:依赖管理</li></ul><p>以上能力结合<strong>交互式命令行</strong>,打造自动化项目开发流程。</p><h2>出发点</h2><ul><li>规范化项目管理</li><li>自动提交 + 版本号</li><li>交互式commit msg</li><li>自动生成日志</li><li>一目了然的项目依赖</li><li>交互式的项目调试流程</li></ul><h2>准备工作</h2><ul><li><code>npm i -g lerna</code></li><li><code>lerna -v</code> // 确认安装成功。</li><li><code>yarn -v</code> // 确认安装<code>yarn</code>。应该随<code>node</code>默认安装的</li></ul><h2>创建Monorepo环境</h2><blockquote>定义工作区,可独立管理应用依赖</blockquote><ol><li><code>vue create monorepo</code></li><li><code>cd monorepo</code></li><li><code>lerna init</code></li><li><p>修改<code>package.json</code></p><pre><code>// 追加——定义工作区,可独立管理应用依赖
workspaces: [
"packages/*"
]</code></pre></li><li><code>yarn workspaces info</code></li></ol><h2>配置版本服务器</h2><ul><li><p>配置commit msg校验</p><ul><li><code>yarn add -W -D commitizen</code> // root目录下安装依赖<code>-W</code></li><li><p><code>npx commitizen init cz-lerna-changelog --yarn --dev --exact</code></p><ul><li>// 以上两步创建<code>Commitizen-friendly</code>交互式<code>commit msg</code> —— 让您不必再深挖文档查找commit格式. Node10+支持</li><li>// <code>commitizen init</code>自动安装依赖<code>cz-lerna-changelog</code></li><li>// <code>commitizen init</code>自动添加<code>config.commitizen</code>到<code>package.json</code></li><li>// 不同的适配器(E.g.<code>cz-lerna-changelog</code>)提供不同的交互界面</li><li>// 缺点:不符合常规命令习惯<code>npm run cm</code></li><li><p>手动添加scripts<code>"cm": "cz"</code> // 不使用<code>commit</code>做scripts的<code>key</code>是避免冲突</p><blockquote>if you are using precommit hooks thanks to something like husky, you will need to name your script some thing other than "commit" (e.g. "cm": "cz"). <strong>The reason is because npm-scripts has a "feature" where it automatically runs scripts with the name prexxx where xxx is the name of another script.</strong> In essence, npm and husky will run "precommit" scripts twice if you name the script "commit", and the work around is to prevent the npm-triggered precommit script.</blockquote></li></ul></li><li><p>适配<code>git commit</code>命令</p><ul><li><code>yarn add -W -D husky</code></li><li><p>手动追加<code>husky.hooks</code>到<code>package.json</code></p><pre><code>...
"husky": {
"hooks": {
"commit-msg": "exec < /dev/tty && git cz --hook || true",
"commit": "echo '-----pre-commit====",
"push": "echo '-----pre-push===="
}
},
...</code></pre><blockquote>Why exec < /dev/tty? By default, git hooks are not interactive. This command allows the user to use their terminal to interact with Commitizen during the hook.</blockquote></li></ul></li></ul></li><li><p>初始化Git服务器</p><ul><li><code>git remote add origin ...</code></li><li><code>git add .</code></li><li><code>git commit -m "..."</code> // 该命令后,进入commit msg交互:<code>husky</code>配置提供;</li><li><code>git push ...</code> // 初始化远程库,使用<code>git push</code>进行第一次提交<code>lerna.json</code>的默认0.0.0版本,不使用<code>lerna version</code>做第一次提交</li></ul></li></ul><p><img src="/img/remote/1460000039077509" alt="commit-msg.png" title="commit-msg.png"></p><h2>自动生成变更日志</h2><pre><code>// 修改lerna.json
...
"command": {
"version": {
"conventionalCommits": true
}
},
"ignoreChanges": [
""
]
...</code></pre><blockquote>lerna version 会检测从上一个版本发布以来的变动,但有一些文件的提交,我们不希望触发版本的变动,譬如 .md 文件的修改,并没有实际引起 package 逻辑的变化,不应该触发版本的变更,可以通过 ignoreChanges 配置排除</blockquote><p><img src="/img/remote/1460000039077508" alt="changeLog.png" title="changeLog.png"></p><h2>创建测试项目</h2><ul><li><p>创建一个子项目<code>packages/v1act/</code></p><ol><li>创建目录<code>packages/v1act/</code></li><li><p>创建文件<code>packages/v1act/package.json</code></p><pre><code> {
"name": "v1act",
"version": "0.0.0", // 与lerna.json'version'保持一致
"private": true // 注意:追加该属性lerna ls -l会忽略该子项目, yarn workspaces info 可以查看该工作区
}</code></pre></li><li><code>lerna ls -l</code>或<code>lerna ls --json</code> // 查看仓库项目组织结构</li></ol></li><li><p>更新代码</p><ul><li><code>git add</code></li><li><code>git commit -m ''</code></li><li><code>lerna version</code> // 交互式版本更新 + 自动提交,没有packages/内容变更的话,不能通过lerna version提交,只能通过git push</li></ul></li></ul><p><img src="/img/remote/1460000039077495" alt="version.png" title="version.png"></p><p><img src="/img/remote/1460000039077496" alt="publish.png" title="publish.png"></p><h2>工具书</h2><ul><li><a href="https://link.segmentfault.com/?enc=OL761myCDcp0SQs1WQ5fVw%3D%3D.AqYf3KF6Pk02HgQ4pS3wimAZIg1qgdFYsBN4CMaWd9Q%3D" rel="nofollow">lerna官网</a></li><li><a href="https://link.segmentfault.com/?enc=vOeqCjA1sU2ql%2F%2FWRs9d%2Bw%3D%3D.QLRleBXV%2FvlR%2FEWnFc39TxPjgcwBTbTBwMekKFJiNFDYT9SK0kq59IW2AQxdRkS%2B2wLvkChC4%2FE9SNKIuq5WrA%3D%3D" rel="nofollow"><code>commitizen</code>NPM适配器列表</a></li><li><a href="https://link.segmentfault.com/?enc=9Ywlg3C8x6f9lc4xVAKkxA%3D%3D.5ZWFgm%2Bodjteg5e%2FFTLMs%2Bp7hA%2FcABnr3fW27ZQJ4w0rBu5tGpga2mmdJ5zD6DcG" rel="nofollow"><code>commitizen</code>官方适配器列表:<code>cz-conventional-changelog</code>、<code>@digitalroute/cz-conventional-changelog-for-jira</code>、<code>cz-lerna-changelog</code></a></li><li><a href="https://link.segmentfault.com/?enc=QB09DyKRVXIN5eGJcbRm8w%3D%3D.9kSdkEmhap4QoThwPB04UjAgkp3565GVv%2BlOSFH1Bhz9C5g2fmWMfMP3wHuTfBFFjX00Z27JOBoTU8G9nblw9lLHAfkzFUhvLTrqIX8Dtxs%3D" rel="nofollow">yarn官方文档</a></li></ul>
交互型命令行
https://segmentfault.com/a/1190000039077060
2021-01-24T23:02:31+08:00
2021-01-24T23:02:31+08:00
米花儿团儿
https://segmentfault.com/u/mihuartuanr
0
<h2><code>Node</code>实现命令行</h2><pre><code>function cli () {
process.on('exit', function () {
console.log('----exit')
})
process.stdin.setEncoding('utf8')
process.stdout.write('......n')
console.log('----------------00000000000------------')
process.stdout.write('确认执行吗(y/n)?')
process.stdout.write('......n')
process.stdin.on('data', (input) => {
input = input.toString().trim()
if (['Y', 'y', 'YES', 'yes'].indexOf(input) > -1) {
console.log('success')
}
if (['N', 'n', 'No', 'no'].indexOf(input) > -1) {
console.log('reject')
}
console.log('===========callback============')
process.exit(0)
})
}
cli()</code></pre><h2>在巨人肩膀上实现命令行</h2><p>在前人的基础上,自定义交互脚本是一个很简单的事情。</p><p>这里介绍四个工具,分别作用在不同的领域:</p><ul><li><p><a href="https://link.segmentfault.com/?enc=SDR0Ug%2BjAy8%2BJ1r%2Fq8lcXg%3D%3D.rtbN55Dm6TjDPl9DjiOenDyR%2FFh47OqtJKt3LEXm1IPwyejHdYIYeMWg8ftfOf0o" rel="nofollow"><code>shelljs</code></a></p><ul><li>执行脚本程序</li></ul></li><li><p><a href="https://link.segmentfault.com/?enc=jKiYimfc%2FJlG9IQYqKTXkQ%3D%3D.KkWrcg%2FiftatZrNjWOQps4ngvqwooJe5qmmvZ244b9kUboWuPtYmpMPWHZPbt%2BE5" rel="nofollow"><code>inquirer</code></a></p><ul><li>命令行用户交互界面</li></ul></li><li><p><a href="https://link.segmentfault.com/?enc=bvqaluiQchCxaGYlWW%2F71w%3D%3D.8OyOaX9vzCwv%2BqspSeQiSxa0d4PLWTR%2BSN3PeV9aJtThvaV2U1Pj7I%2Bj%2F0gtBXFS" rel="nofollow"><code>chalk</code></a></p><ul><li>命令行日志样式</li><li>可嵌套、可链式</li></ul></li><li><p><a href="https://link.segmentfault.com/?enc=%2BYa5fZXEjR%2BRWdW5wP7gxg%3D%3D.0269gZ7O2zzxGePhOeTzoZDqsMXk4XYjJaXNeDvL2VRo8e5QhC74lcvMmhynIJ%2Bz" rel="nofollow"><code>commander</code></a></p><ul><li>自定义命令行参数</li></ul></li></ul><pre><code>// const spawn = require('child_process').spawn
// const { Command } = require('commander')
const shell = require('shelljs');
const inquirer = require('inquirer');
const chalk = require('chalk');
const scriptsConfig = require('../config/scripts')
const runModes = require('../config/modes')
// const program = new Command();
let curScriptEvent = process.env.npm_lifecycle_event; // E.g.:'npm run serve'中的'serve'
let curScriptSource = process.env.npm_lifecycle_script; // E.g.:'npm run serve: vue-cli-service serve'的'vue-cli-service serve'
const build = async () => {
let res;
try {
// j、k或者数字键选择
res = await inquirer.prompt([
{
type: 'list',
name: 'operation',
message: '请选择你要运行的命令:npm run',
choices: ['serve', 'build', 'push', 'fds', 'test']
},
{
type: 'list',
name: 'mode',
message: '请选择你要执行的环境?',
choices: ['dev', 'prev', 'pro']
},
]);
} catch(e) {
if (e.isTryError) {
console.log(chalk.red("Prompt couldn't be rendered in the current environment"))
} else {
console.log(chalk.red("Error:", e))
}
}
const { operation, mode } = res;
console.log(chalk.green(`正在执行操作npm run ${operation}---${mode}......`));
curScriptEvent = `${operation}${mode ? (':' + mode) : ''}`
curScriptSource = scriptsConfig[curScriptEvent]
if (process.platform === 'win32') {
shell.exec(`npx cross-env ${curScriptSource}`)
// spawn('npx cross-env', [curScriptSource], {
// stdio: 'inherit',
// shell: true
// })
} else {
shell.exec(`npm run ${curScriptEvent}`)
// spawn('npm', ['run', curScriptEvent])
}
}
build();
</code></pre><p>优化版:</p><pre><code>const shell = require('shelljs');
// const spawn = require('child_process').spawn;
const inquirer = require('inquirer');
const chalk = require('chalk');
const { scripts, getRunModes } = require('../config/scripts')
const runModes = getRunModes(scripts)
const operationKeys = Object.keys(runModes)
let curScriptEvent = process.env.npm_lifecycle_event; // E.g.:'npm run serve'中的'serve'
let curScriptSource = process.env.npm_lifecycle_script; // E.g.:'npm run serve: vue-cli-service serve'的'vue-cli-service serve'
const build = async () => {
let operation, mode;
try {
// j、k或者数字键选择
const res = await inquirer.prompt({
type: 'list',
name: 'operation',
message: '请选择你要运行的命令:npm run',
choices: operationKeys
});
operation = res.operation
} catch(e) {
if (e.isTryError) {
console.log(chalk.red("Prompt couldn't be rendered in the current environment"))
} else {
console.log(chalk.red("Error:", e))
}
}
try {
// j、k或者数字键选择
const res = await inquirer.prompt({
type: 'list',
name: 'mode',
message: '请选择你要执行的环境?',
choices: runModes[operation]
});
mode = res.mode
} catch(e) {
if (e.isTryError) {
console.log(chalk.red("Prompt couldn't be rendered in the current environment"))
} else {
console.log(chalk.red("Error:", e))
}
}
// const { operation, mode } = res;
// const NODE_ENV = runModes[mode];
curScriptEvent = `${operation}${mode ? (':' + mode) : ''}`
curScriptSource = scripts[curScriptEvent]
console.log(chalk.green(`正在执行${curScriptSource}......`));
if (process.platform === 'win32') {
// spawn('npx cross-env', [curScriptSource], {
// stdio: 'inherit',
// shell: true
// })
shell.exec(`npx cross-env ${curScriptSource}`)
} else {
shell.exec(curScriptSource)
// spawn('npm', ['run', curScriptEvent], {
// stdio: 'inherit',
// shell: true
// })
}
}
build();</code></pre>
TS使用归档
https://segmentfault.com/a/1190000038981522
2021-01-14T15:40:33+08:00
2021-01-14T15:40:33+08:00
米花儿团儿
https://segmentfault.com/u/mihuartuanr
0
<h2>TS特有de概念</h2><h3>元组</h3><blockquote>指定数组每个元素的类型</blockquote><h4>特点</h4><ul><li>在初始化时,必须按限定的类型和个数赋值;</li><li>可以通过数组的方法突破限制;</li></ul><h4>示例</h4><pre><code>let arr: [string, number, any]
arr = ['1', 1, 1] // OK
arr = [1, 1, 1] // Error
arr = ['1', 1, 1, true] // Error
arr.push(true) // OK</code></pre><h3>接口</h3><blockquote>只是在<code>ts</code>编译阶段做类型检查,并不会转译成<code>JS</code>代码 <p>用接口声明可调用的类型</p></blockquote><h4>示例</h4><pre><code>// 定义函数
interface ISum {
(x: number, y: number, z?: number): number;
}
let sum: ISum
sum = (a) => {
// ^ = OK
return 1
}
sum('1', 2)
// ^ Argument of type '"1"' is not assignable to parameter of type 'number'.
// 定义类的new调用
interface IConstructor {
new(arg: string): IConstructor;
}
function Constr (this: any, name: string) {
this.name = name
}
const instance = new (Constr as any as IConstructor)('111')</code></pre><p><strong>以上示例注意:</strong></p><ul><li><p><code>interface</code>定义函数输入、输出的类型作为预置值内置在函数声明中</p><ul><li>函数声明时无需二次定义参数类型,函数输出值的类型推断要与<code>interface</code>定义的输出类型一致,否则会报错。</li><li><code>interface</code>定义函数<strong>没有限制函数声明时传入的参数个数</strong>,只有在调用时才会报参数个数错误;</li></ul></li><li><p>函数声明无法直接其它类型,需要使用双重断言<code>Constr as any as IConstructor</code>;</p><ul><li><strong>==尽可能不要使用双重断言==</strong>,它会影响<code>ts</code>的判断</li></ul><pre><code>// 示例:
let num: number = 0
let str: string = 's'
num = str // Error
num = str as any as number // OK
//^ = num === 's' //这里str认为是number类型,赋值成功</code></pre></li></ul><h3>联合类型</h3><blockquote>一个数据声明为联合类型,使用时,若不确定是联合类型中具体的类型时(通过if条件、as断言、in操作符、typeof缩小未知范围),只能访问联合类型共有的方法。</blockquote><h3>断言</h3><blockquote>断言是联合类型缩小未知范围时使用,但,断言强制浏览器相信数据是我们指定的类型,实际上是<strong>不安全</strong>的。【推荐】使用类型收缩<code>typeof</code>、<code>instanceof</code>...来缩小未知范围。<p>当两个类型声明有交集时,才可以使用断言,否则会报错(because neither type sufficiently overlaps with the other.) </p><p>如果两个类型声明没有交集,可以使用双重断言强制断言成另一种类型,示例如上:<code>Constr as any as IConstructor</code></p></blockquote><h3>readonly & Readonly泛型</h3><blockquote>readonly标识符,用于对象属性<p>Readonly映射类型,接收一个泛型T,用来把泛型T的所有属性标记为只读类型;</p></blockquote><h4>示例</h4><pre><code>type Foo = {
bar: number;
bas: number;
}
type ReadFoo = Readonly<Foo>
/** ^ = type ReadFoo = {
* readonly bar: number;
* readonly bas: number;
* }
*/</code></pre><h4>示例</h4><pre><code>function fn(x: number | string): number {
return x.length;
}
// Property 'length' does not exist on type 'string | number'.
// Property 'length' does not exist on type 'number'.</code></pre><h3>对象结构</h3><h4>示例</h4><pre><code>// type定义对象结构,不可重载
type TObjectProps = {
x: number;
y: number;
}
const obj: TObjectProps = {
x: 1,
y: 1
}
// interface定义对象结构
interface IObjectProps {
x: number;
y: number;
}
const obj: IObjectProps = {
x: 1,
y: 1,
add() {}
//^ = 'add' does not exist in type 'IObjectProps'
}
// interface定义重载
interface IObjectProps {
x: number;
y: number;
}
const obj: IObjectProps = {
x: 1,
y: 1,
add() {} // OK
}
interface IObjectProps {
add: () => void;
}
// let & typeof定义对象结构,不可重载
let objectProps: {
x: number;
y: number;
}
const obj: typeof objectProps = {
x: 1,
y: 1
}</code></pre><h3>Function</h3><blockquote>函数类型声明方式有多种,应用场景两种: 固定参数,不固定参数;</blockquote><h4>示例</h4><h6>固定参数:</h6><pre><code>// type定义
type Tfn = (a: number, b: number) => number;
let fn1: Tfn
fn1 = function(a, b) {
return a + b
}
fn1(1, 2)
// type定义重载
type Tfn = {
(a: string): string;
(a: number, b: number): number;
}
let fn1: Tfn
fn1 = function(a: any, b?: any): any {
if (b) {
return a + b
} else {
return a
}
}
fn1('1')
// ^ = let fn1: (a: string) => string (+1 overload)
fn1(1, 2)
// ^ = let fn1: (a: number, b: number) => number (+1 overload)
fn1(1) //Error
// interface定义
interface Ifn {
(x: string): string;
}
let fn1: Ifn
fn1 = function(a) {
return a
}
fn1('1')
// interface定义重载
interface Ifn {
(x: string): string;
(x: number, y: number): number;
}
let fn1: Ifn
fn1 = function(a: any, b?: any): any {
if (b) {
return a + b
} else {
return a
}
}
fn1('1')
// ^ = let fn1: (a: string) => string (+1 overload)
fn1(1, 2)
// ^ = let fn1: (a: number, b: number) => number (+1 overload)
fn1(1) //Error
// let & typeof 声明
let fn: {
(x: string): string;
}
let fn1: typeof fn
fn1 = function(a) {
return a
}
fn1('1')
// let & typeof声明重载
let fn: {
(x: string): string;
(x: number, y: number): number;
}
let fn1: typeof fn
fn1 = function(a: any, b?: any) {
if (b) {
return a + b
} else {
return a
}
}
fn1('1')
// ^ = let fn1: (a: string) => string (+1 overload)
fn1(1, 2)
// ^ = let fn1: (a: number, b: number) => number (+1 overload)
fn1(1) //Error
// function声明
function fn(x: string): string {
return x
}
fn('1')
// function声明重载:fn实现必须紧跟着fn头声明
function fn(x: string): string;
function fn(x: number, y: number): number;
function fn(x: any, y?: any) {
if (y) {
return x + y
} else {
return x
}
}
fn('1')
// ^ = let fn: (a: string) => string (+1 overload)
fn(1, 2)
// ^ = let fn: (a: number, b: number) => number (+1 overload)
fn(1) //Error</code></pre><p>通过<strong>重复定义函数头实现重载</strong>,而函数声明输入、输出最好使用<code>any</code>类型(不使用<code>any</code>的话,函数体的操作逻辑使用的必须是<strong>联合声明类型共有的成员</strong>),调用时会根据参数个数匹配预定义的函数头进行校验。</p><h6>不固定参数:</h6><pre><code>// 应用场景:作为回调函数,通过`apply`调用;
interface IFn {
(...args: any[]): any;
}
function invoke(fn: IFn) {
fn.apply(null, [...arguments])
}</code></pre><h3>枚举Enum</h3><blockquote>定义索引和值的双向映射;</blockquote><pre><code>enum Direction {
UP,
DOWN,
LEFT,
RIGHT
}
console.log(Direction[0]) // 'UP'
console.log(typeof Direction[0]) // 'string'
console.log(Direction['UP']) // 0
console.log(typeof Direction['UP']) // 'number'
console.log(Direction[0] === 'UP') // true</code></pre><h4>分类</h4><h6>数字枚举</h6><blockquote>数字枚举默认从0开始;<p>若有指定的索引,则后续数据索引++</p></blockquote><pre><code>// 场景:Code编码语义化
enum Direction {
Up,
Down = 10,
Left,
Right
}
console.log(Direction[0]) // 'UP'
console.log(Direction['Up']) // 0
console.log(Direction['Left']) // 11
console.log(Direction[10]) // 'Down'</code></pre><h6>字符串枚举</h6><pre><code>// 场景:游戏按键?
enum Direction {
Up = 'u',
Down = 'd',
Left = 'l',
Right = 'r'
}
console.log(Direction['Up']) // '上'
console.log(Direction['Down']) // '下'</code></pre><h6>常量枚举</h6><blockquote>和上述枚举类型<a href="https://link.segmentfault.com/?enc=xCuxcr%2BScdkctKlFMXBDUw%3D%3D.xUnQtZEeWm8HSYWheI8XOwH9wBhS37OsjvkI%2FsW2TMk45JBjYvSJ6SMkAu6uvf1zqUpt5NQRkW7%2B7TcmwjO8BklGEnuPisLOTVh6Gf2VyvSf%2BV2GjkziImPur6EcUxf%2BHfVJq1DOkcQ5EdN7Yr3Yzqj0a3RwVNhcdn9QZchu9HTqxp%2Bf4Ba2zS%2Bh4ShCTeoDRgnkbkLjNQYg9XU6g13v5MQAGVPeKuzdd%2FIU1fqYFZeqXVk7%2Ftuz1033YPzHgHv9AbH71vaOXkkB%2BtHQry3KsnaincjZTsSaRIvOA5iIazw%3D" rel="nofollow">编译结果不同</a>;</blockquote><pre><code>enum Dir {
Up,
Down = 10,
Left,
Right
}
const enum Direction {
Up,
Down = 10,
Left,
Right
}
let directions = [
Direction.Up,
Direction.Down,
Direction.Left,
Direction.Right,
];
/////编译输出如下:
"use strict";
var Dir;
(function (Dir) {
Dir[Dir["Up"] = 0] = "Up";
Dir[Dir["Down"] = 10] = "Down";
Dir[Dir["Left"] = 11] = "Left";
Dir[Dir["Right"] = 12] = "Right";
})(Dir || (Dir = {}));
let directions = [
0 /* Up */,
10 /* Down */,
11 /* Left */,
12 /* Right */,
];</code></pre><h3>字面量类型</h3><pre><code>const str: 'name' = 'name' // str只能是'name'字符串,赋值其他字符串或其他类型会报错;
const number: 1 = 1 // number只能是1,赋值其他数值或其他类型会报错;
type Directions = 'UP' | 'DOWN' | 'LEFT' | 'RIGHT'
let toWhere: Directions = 'LEFT' </code></pre><p>对应场景:</p><pre><code>interface IO {
'y+': number;
'M+': number;
'd+': number;
'h+': number;
'm+': number;
's+': number;
'q+': number;
'S+': number;
}
type TKeyProps = keyof IO
// ^ = type TKeyProps = "y+" | "M+" | "d+" | "h+" | "m+" | "s+" | "q+" | "S+"
var o: IO = {
'y+': this.getFullYear(),
'M+': this.getMonth() + 1,
'd+': this.getDate(),
'h+': this.getHours(),
'm+': this.getMinutes(),
's+': this.getSeconds(),
'q+': Math.floor((this.getMonth() + 3) / 3),
'S+': this.getMilliseconds()
}
o['y++'] // OK
let kkk = 's+'
o[kkk] // Error
o[kkk as TKeyProps] // OK</code></pre><h3>泛型</h3><blockquote>泛型de目的是在成员之间(至少有两个地方用到了泛型占位)提供有意义的约束</blockquote><p>成员:</p><ul><li>类的属性</li><li>类的方法</li><li>函数参数</li><li>函数返回值</li></ul><blockquote>泛型在定义时,不能明确数据类型,声明一变量<strong>占位</strong>,调用时通过传入类型或<code>ts</code>类型推断来确定具体的数据类型。</blockquote><ul><li>相对于<code>any</code>:泛型未丢失数据结构信息;</li><li>相对于联合声明:泛型明确具体的类型结构,联合声明并未明确具体类型;</li></ul><blockquote>逻辑中只能调用泛型数据的<strong>通用</strong>成员属性/方法,否则会报错;</blockquote><h4>示例</h4><pre><code>function identity<T>(arg: T): T {
return arg;
}
// 明确指定T是string类型
let output = identity<string>("myString"); // type of output will be 'string'
// 利用类型推论 -- 即编译器会根据传入的参数自动地帮助确定T的类型
let output = identity("myString"); // type of output will be 'string'</code></pre><h3>any & never & unknown</h3><ul><li><p><code>any</code></p><ul><li>称为<code>top type</code>,任何类型的值都能赋给<code>any</code>类型的变量</li><li>又称为<code>bottom type</code>,任何类型(除<code>never</code>外)的子类型</li><li>可以理解为没有类型</li></ul></li><li><p><code>never</code></p><ul><li>称为<code>bottom type</code>,任何类型的子类型,也可以赋值给任何类型</li><li><p>推断场景1:</p><blockquote>无法执行到函数终止点的<strong>函数表达式</strong></blockquote><ul><li>场景:总抛出异常的函数表达式的返回值类型;</li></ul><pre><code> // 需要是函数表达式,若是函数声明为assertNever: () => void
const assertNever = function (x: any) {
// ^ = type assertNever = never
throw new Error("Unexpected object: " + x);
}</code></pre><ul><li>场景:永不结束函数表达式的返回值类型;</li></ul><pre><code> let loop = function () {
// ^ = type loop = never
while (true) {}
}</code></pre></li><li>推断场景2:被永不为真的类型保护约束的变量类型;</li></ul><pre><code> // 示例:
type result = 1 & 2 // 结果为never
// ^ = type result = never
// 尤大示例:
interface Foo {
type: 'foo'
}
interface Bar {
type: 'bar'
}
type All = Foo | Bar
function handleValue(val: All) {
switch (val.type) {
case 'foo':
// 这里 val 被收窄为 Foo
break
case 'bar':
// val 在这里是 Bar
break
default:
const exhaustiveCheck = val
// ^ = type exhaustiveCheck = never
break
}
}</code></pre></li><li><p><code>unknown</code></p><ul><li><code>top type</code>:任何类型都是它的<code>subtype</code>;</li><li>不能将<code>unknown</code>赋值其它类型,<code>unknown</code>类型的数据只能赋值给<code>unknown</code>、<code>any</code>类型;</li><li>相对于<code>any</code>,<code>ts</code>会为<code>unknown</code>提供有效的类型检测;</li><li>若<code>unknown</code>类型数据的属性或方法,需要通过类型断言/<strong>类型收缩</strong>来缩小未知范围;</li></ul></li></ul><h4><code>any</code>的危害</h4><blockquote>使用<code>any</code>做类型声明或者做断言,会丧失原始数据的结构类型信息,即:再也无法知道原始数据是什么结构了,更不会有报错信息,<strong>推荐使用<code>unknown</code> & 类型收缩</strong>;</blockquote><h3>索引签名</h3><blockquote>索引签名用于定义对象/数组de<strong>通配结构</strong><p>索引签名的名称(如:<code>{ [key: string]: number }</code>的<code>key</code> )除了可读性外,没有任何意义,可以任意定义。</p><p>其它成员都必须符合索引签名值de结构,所以,索引签名值de结构必须是其它成员属性类型的<code>top type</code>。</p><p>尽量不要使用字符串索引签名与有效变量混合使用——如果属性名称中有拼写错误,这个错误不会被捕获。【同级的其它属性应该是对索引签名的<strong>限制增强</strong>】</p></blockquote><h4>示例</h4><pre><code>// 1. 对象示例
interface Foo {
[key: string]: number; // 该通配结构[key: string]即是签名索引。
}
// 1. 数组示例
interface Foo {
[idx: number]: string;
length: number;
}
const arr: Foo = ['1', '2', '3', '4']
// 2. 所有明确的成员都必须符合索引签名
interface Bar {
[key: string]: number;
x: number; // OK
y: string;
//^ = Property 'y' of type 'string' is not assignable to string index type 'number'.
}
// 2. 可以使用交叉类型突破限制
interface Bar {
[key: string]: number;
x: number;
}
type Composition = Bar && {
y: string;
}
// 3. 有效变量和索引签名不要同级定义
interface CSS {
[selector: string]: string;
color?: string;
}
const failsSilently: CSS = {
colour: 'red' // 'colour' 不会被捕捉到错误
};</code></pre><h2><code>TS</code>类型声明</h2><blockquote><code>ts</code>报错对应查找:做词典用;<p><code>declare</code>声明通过各种途径(<code><script></code>导入、<code>new webpack.ProvidePlugin({//...})</code>)引入全局命名空间的变量/模块</p></blockquote><h3>函数声明中显式使用<code>this</code></h3><pre><code>// 场景示例:
function Person() {
var vm = this;
// ^ = this具有隐式类型any;
}
// 解决方案:
function Person(this: void) {
var vm = this;
}</code></pre><p><code>this</code>作为函数的第一参数,由于<code>ts</code>只是类型检查,编译成<code>JS</code>时不会将<a href="https://link.segmentfault.com/?enc=uapFOqsOHr8hq%2FtiLtLCfQ%3D%3D.j0ydLlJ0JiCMWKm%2Fz2pjXnjbkz3w2ERAMy1DkzxY6g8W9bdy7m7BXEyH2%2FIaUxqlXkJkxOmRAvz4B8ZK0Jh30Ezb%2Fg93KQer86wE0ZFdw9y3Anj3qGKbAHKqEI2OhZ8nIe6tT2ASah9RqZzCOLM6OAW%2FZKDGGHTxrgSfRZg08z8%3D" rel="nofollow"><code>this</code>参数输出。</a></p><h3>全局库声明</h3><pre><code>// 场景示例:
$('div')[]
//^ = not find name '$'
// 解决方案:追加zepto.d.ts声明
interface ZeptoStatic {
//...
}
interface ZeptoCollection {
// ...
}
declare var Zepto: (fn: ($: ZeptoStatic) => void) => void;
declare var $: ZeptoStatic;
declare function $(selector?: string, context?: any): ZeptoCollection;</code></pre><h3><code>JS</code>内置对象追加属性声明</h3><pre><code>// 场景示例:
Date.prototype.format = function() {
//...
}
const arg = 2423423413;
new Date().format(arg)
// ^ = format 不在Date上
// 解决方案:追加声明*.d.ts声明
declare global {
interface Date {
Format(arg: string): string;
}
}</code></pre><h3>图片...静态资源声明</h3><pre><code>// 场景示例:
import Logo from './assets/logo.png'
// ^ = not find Module
// 解决方案:追加声明*.d.ts声明
declare module '*.svg'
declare module '*.png'
declare module '*.jpg'
declare module '*.jpeg'
declare module '*.gif'
declare module '*.bmp'
declare module '*.tiff'</code></pre><h3>给<code>vue3</code>配置全局成员</h3><pre><code>// 场景示例:
// main.ts
import { createApp } from 'vue'
import axios from 'axios'
import qs from 'qs'
import App from './App.vue'
const vueInstance = createApp(App)
vueInstance.config.globalProperties.$http = axios
vueInstance.config.globalProperties.$qs = qs
// *.vue
...
this.$http.post(...).then(() => {})
// ^ = Property '$http' does not exist on type 'ComponentPublicInstance<>'...
...
// 解决方案:追加声明*.d.ts
import Axios from "axios";
import qs from 'qs'
import Store from "../store";
declare module '@vue/runtime-core' {
interface ComponentCustomProperties {
$http: Axios;
$store: Store;
$qs: qs;
}
}</code></pre><h3>第三方ES Module:<code>export default</code>声明</h3><pre><code>// 场景示例:
// utils.ts
export default (function() {
const utils = {
}
return utils
})()
// *.ts
utils.default.isString(123)
// ^ = Property 'default' does not exist on type 'typeof utils'
// 解决方案:追加声明*.d.ts
declare namespace utils {
const UtilsProps: {
isString: (arg: unknown) => boolean;
isArray: (arg: unknown)=> boolean;
isObject: (arg: unknown)=> boolean;
isDate: (arg: unknown)=> boolean;
app: any;
}
export default UtilsProps;
}</code></pre><h3>第三方ES Module:<code>export</code>声明</h3><pre><code>// *.d.ts
declare namespace utils {
export function isString (arg: unknown): boolean;
export var app: any;
}</code></pre><h3>第三方<code>CommonJS</code>模块声明</h3><pre><code>// 模块类库 module-lib.ts
function moduleLib(options) {
console.log(options);
}
const version = "1.0.0";
function doSomething() {
console.log('moduleLib do something');
}
moduleLib.version = version;
moduleLib.doSomething = doSomething;
module.exports = moduleLib;
// *.d.ts
declare function moduleLib(options: Options): void;
interface Options {
[key: string]: any,
}
declare namespace moduleLib{
const version: string;
function doSomething(): void;
}
export = moduleLib;</code></pre><h3>第三方'UMD'声明</h3><pre><code>// UMD库 umd-lib.js
(function (root, factory) {
if (typeof define === "function" && define.amd) {
define(factory);
} else if(typeof module === "object" && module.exports) {
module.exports = factory();
} else {
root.umdLib = factory();
}
})(this, function () {
return {
version: "1.0.2",
doSomething() {
console.log('umdLib do something');
}
}
});
// *.d.ts文件
declare namespace umdLib {
const version: string;
function doSomething(): void;
}
export as namespace umdLib // 专门为umd库准备的语句,不可缺少
export = umdLib // commonjs导出</code></pre><h2>内置泛型实现</h2><h3><code>ReturnType</code></h3><pre><code>const ele: ReturnType<typeof $> = $('div')</code></pre><h2>参考书</h2><ul><li><a href="https://link.segmentfault.com/?enc=oHe5ItgsXN2%2Bg8GQG6WNZA%3D%3D.rSVFBEdhZIvXqR1Qtqy11rnnuEfF7FH%2FEnwaMvu5FwKkB0uMaaYwe2VryI%2BuYZjuEYzRXQBQ2JLeRJlswWbIuB2TkmEC9z4ztEvs552BzYorD8NGjrxEnp9vsosI2ZLJ" rel="nofollow">内置泛型</a></li><li><a href="https://link.segmentfault.com/?enc=IrXIOoUoevfHF2cYXQOywA%3D%3D.2KEFO7%2B217WAXmwDExu8Zz85xaPMRIye1AmYs4CzVNjayMIwcphj9blOnUA8nyGV" rel="nofollow">在线编译工具</a></li><li><a href="https://link.segmentfault.com/?enc=3ZF3osxj4eRBMCv2%2BdFrSw%3D%3D.0H%2FjMqT%2F5CHfHp6MUOU9Olx28K5JQdyjsBaG8ky4Z63UdQa6IyyCO%2BOF9O240zwVQycfYLywOM6DEZeM23oCr9TnFX%2FoblqE5nc5kUcTGLUFyoGsZH5p2Syr2tiQRoPfubVdxOiBktpi7oWzEDVpmLyhZGrB0aySEeyE%2B5HhNdA%3D" rel="nofollow">深入理解TypeScript</a></li><li><a href="https://link.segmentfault.com/?enc=FQrFPE0DHbccQeYpFiPTsg%3D%3D.e8EJCrCsuwuIx4DvwudAuyXeUV%2FhhiTXJ7p%2FDyzFgmZTBG5EZZ%2BP6JRc86p3wIJrEG1DHjZUq06iEG5tbKl1NqLtwcsvDVvw%2FKomE%2BUF3dGvMMhjmih6DX37QJXMpoiO" rel="nofollow">ts官网</a></li><li><a href="https://link.segmentfault.com/?enc=WzxZHA0jsOIeNot8qPn%2FSw%3D%3D.Vfjp4z0k6mH8pfBebX8z5jBJT%2BtXnUzsMoOEiXFm4hrRcKZdFJHnX%2BzEiBrjXIYodCiic%2FFuwzFsvxg4SugX0Q%3D%3D" rel="nofollow">w3c/ts</a></li><li>...</li></ul>
Serverless学习
https://segmentfault.com/a/1190000038703912
2020-12-28T19:29:15+08:00
2020-12-28T19:29:15+08:00
米花儿团儿
https://segmentfault.com/u/mihuartuanr
1
<h2>前言</h2><p><code>Serverless</code>是近些年来流行起来的架构理念,进入市场化应该是2014年亚马逊发布<code>FaaS Lambda</code>。 <br>一个热词的产生,必然会有一些商家抢注商标的现象,所以,我们目前搜索<code>Serverless</code>,搜索结果第一页会看到名为<code>Serverless</code>的产品。 <br>我们日常说的<code>Serverless</code>,一般是指架构理念,或基于架构理念产生的产品全类,而非指某个具体的产品。</p><hr><p><code>Serverless</code>是对运维体系的极端抽象,这里有一个名次“抽象”、两个定语“运维体系的”、“极端”。</p><ul><li><code>Serverless</code>是一个抽象,就说明<code>Serverless</code>不是指具体的某个产品。</li><li>"运维体系的",说明了<code>Serverless</code>的职能边界,是对运维体系流程的优化,当然,也对开发流程产生了一些副作用,但,主要的职能在运维方向。</li><li>"极端抽象",是表明基于<code>Serverless</code>理念输出的产品,将运维体系的复杂度内化,只预留简单的接口供外部调用。</li></ul><p>带来的结果就是,可以让零运维经验的人,几分钟就部署一个Web应用上线,稍后我会在==示例==中演示一下。</p><h2>运维发展史</h2><p><img src="/img/remote/1460000038703920" alt="ops-history" title="ops-history"> <br>先看一下整个发展流程,经历了手动运维、自动运维、DevOps开发运维、智能运维几个阶段。 <br>按社会的精细化分工来说:</p><ul><li><p>手动运维阶段,开发者交付代码,运维团队需要进行服务器协调、运行环境部署、上线、版本控制、日志监控、扩缩容设计、容错容灾高可用设计...等等工作。</p><ul><li>如果线上产品出现问题,需要开发者和运维团队共同查找问题。</li></ul></li><li>自动运维阶段,通过编排脚本命令将一些简单的工作进行打包处理,一定程度上减少重复性的手工操作。</li><li>随着微服务、容器技术的发展,来到了<code>DevOps</code>阶段,<code>DevOps</code>=<code>Development</code> + <code>Operations</code>,开发者开始承担一部分的运维职责,甚至有些公司出现了跨职能团队:运维、开发团队融合,打破手动运维阶段开发、运维两座孤岛的现象。这个阶段,就<code>Docker</code>工具而言,开发者交付镜像,运维团队不必在操心代码的运行环境问题。</li><li><p>智能运维阶段,<code>Serverless</code>只是其中的一个发展节点。</p><ul><li>各大服务厂商将基础设施云化,对外提供接口,实现基础设施即代码,让开发者可以通过应用程序代码访问、配置基础设施(BaaS:抽象粒度大多在机器级别)。</li><li>计算机算力的提升,如函数计算[阿里云]、云函数[腾讯云]将计算服务的抽象粒度提高到了函数级别,实现实时的弹性伸缩容机制,按毫秒级计量、按需计费(FaaS)。</li></ul></li></ul><h2>产生背景</h2><ul><li>从整个发展史可以看出,技术的发展起到了重要的推动作用。</li><li><p>历史运维体系的痛点:企业中长尾应用的运营成本问题。</p><ul><li>什么是中长尾应用?就是每天大部分时间没有流量或者有很少流量的应用</li><li>为了保证这些应用的正常运行,至少要安排一台服务器跑这些应用。</li><li>而<code>Serverless</code>借助计算机算力,可以实现实时的弹性扩缩容机制。</li></ul></li><li>减少研发人员的关注点,研发人员无需管理、维护底层的基础设施,无需规划预估容器所需要的计算资源,降低整合和决策的代价,只需要专注应用程序代码的编写,提高研发效能。</li></ul><h2>下个定义</h2><p>狭义<code>Serverless</code> = <code>FaaS</code>架构<br>狭义的<code>Severless</code>是指基于函数计算将<code>Serverless</code>体系产品整合在一起,构建成一个<code>Serverless</code>应用。<br>狭义<code>Serverless</code> = <code>FaaS</code>架构 = <code>Trigger</code> + <code>FaaS</code> + <code>BaaS</code> = <code>FaaS</code> + <code>Baas</code></p><hr><p>广义<code>Serverless</code>是指具备<code>Serverless</code>特性的云服务。</p><hr><p><code>Serverless</code>可以分为<code>Server</code>和<code>less</code>,其中<code>less</code>不是指无服务器端,或者少服务器端,而是指无感知,也对应了“<code>Serverless</code>是对运维体系的极端抽象”这句话。</p><h2>发展现状</h2><p>目前大多数互联网公司都还在<code>DevOps</code>时代。 <br>部分一线大厂有自己的<code>Serverless</code>解决方案并对外开放。如阿里云的函数计算、腾讯云的云函数。 <br>目前<code>Serverless</code>架构实现并没有统一的规范,实现和提供服务的厂商强关联,如果在不同厂商之间迁移,会有很大的工作量和困难。</p><h2>函数计算</h2><p>以阿里云平台的函数计算来介绍一下<code>FaaS</code>函数即服务。<br><img src="/img/remote/1460000038703919" alt="panel" title="panel"> <br>我们先熟悉一下平台设计。</p><ul><li>可以通过支付宝扫码授权登陆。</li><li>直接用“产品”菜单下的搜索功能搜索“函数计算”。</li><li>点击“控制台”直接进入。</li><li><p>顶部</p><ul><li>可以切换代码部署的地域</li><li>如果在“服务/函数”下找不到自己已有的代码,检查一下地域是否选择正确。</li></ul></li><li><p>“概览”页面</p><ul><li>可以直观的看到使用量、监控的概览,还有一些快捷入口。</li><li>免费执行次数和免费资源使用量,在测试阶段可以有效的防止用超过,也很难用超过。</li><li>监控的可视化图形</li><li>新建函数的快捷入口</li></ul></li><li><p>“服务及函数”</p><ul><li>可以创建新服务、新函数,查看已有服务和函数。</li><li>点击服务列表中的某项,可以在右侧查看、编辑包含的函数列表、服务相关的配置信息。</li><li>点击函数列表中的某项,可以进入函数详情,查看、配置函数的信息。</li></ul></li><li><p>自定义域名</p><ul><li>通过自定义域名访问FC函数,需要配合HTTP触发器使用</li><li>==HTTP触发器==后续讲函数类型的时候会提到。</li></ul></li></ul><h2>FaaS</h2><p>下面我们具体看一下函数计算。 <br>首先,我们创建一个服务、一个函数。 <br>创建好一个服务以后,默认打开“服务配置”Tab,从该Tab页,我们可以查看服务当前的配置并进行修改。</p><hr><p>切换到“函数列表”Tab页,点击新增函数按钮,这时会发现,函数有两类:</p><ul><li>事件函数</li><li>HTTP函数</li></ul><p>这里HTTP函数,就是上边所说,有HTTP触发器的函数,可以通过网络请求触发FC函数的执行;</p><hr><p>因为上边我们提到了HTTP触发器,那就先创建一个HTTP函数。 <br>创建成功后,默认进入函数的“触发器”Tab页,可以看到“事件类型”是<code>http</code>,请求方法是<code>GET</code>、<code>POST</code>,不需要授权访问。 <br>为了更清晰的看到触发器的配置项,我们重新创建一个触发器。 <br>然后,切换到“代码执行”Tab页,我们可以看到示例代码。 </p><p>HTTP函数示例代码:</p><ol><li><p>结构:exports.handler = (req, resp, context) => {}</p><ul><li>函数调用时,执行定义的handler逻辑,参数是req、resp、context;</li><li>这些参数后续==调试阶段==我们可以看一下</li></ul></li><li>打印标准版的输出<code>hello world</code></li><li>组装请求数据字段</li><li>将<code>body</code>数据提取并输出组装的数据</li></ol><p>我们执行一下看看会发生什么?</p><ol><li>打印返回的结果</li><li>打印函数执行日志</li><li><p>打印<code>RequestID</code></p><ul><li>这是唯一存在的ID,每次执行都会改变。</li><li>可以通过该ID查询日志。</li></ul></li></ol><p>在“执行”按钮处,可以配置一些参数,改变一下配置看看输出的结果。</p><ul><li><code>POST</code>请求</li><li>路径</li><li><code>Params</code>改变URL上的过滤参数</li><li><code>Body</code>改变<code>POST</code>的请求输出,<code>GET</code>请求下不会出现该Tab页</li></ul><p>而且,在修改的过程中,会发现上方的URL会发生变化。 <br>我们可以通过<code>Postman</code>去请求该地址,调用FC函数,可以通过“日志查询”查看调用结果。</p><p>最后,我们看一下<code>exports</code>导出的函数,默认函数名为<code>handler</code>,这个名字能修改么? <br>答案是肯定的。</p><ul><li>切换到“概览”Tab页,“修改配置”,修改“函数入口”</li><li>切换回“代码执行”,执行看一下结果,报错</li><li>将<code>exports.[fnName]</code>修改成配置项,“保存”,再执行,成功。</li></ul><hr><p>看完了HTTP函数,我们返回去看一下事件函数。 <br>返回到服务列表页面。 <br>“新增函数” ——> “事件函数” ——> “配置部署” <br>配置页面:</p><ul><li>运行环境</li><li><p>弹性实例</p><ul><li>弹性实例有免费额度</li><li>性能实例没有免费额度</li><li>性能实例扩容速度慢,弹性伸缩能力不及弹性实例:<a href="https://link.segmentfault.com/?enc=H4FxutJwbwchRPQ0x8WfsQ%3D%3D.fYAI1uMVaEtONClqPHaQ7rtOVKn0zBHSf4XspfBTTp%2BZHfr3FbuYB86nu9Xgro5c%2BRtieY1EN0Ke7eziQ%2Bx%2FzCZ8ZbjYEoJtC06Xy8oGxVntQqWyBxxIIRGDV8RiSuZ2" rel="nofollow">对比文档</a></li></ul></li><li><p>函数入口</p><ul><li>和“HTTP函数”一样,可以修改约定的导出函数名</li></ul></li></ul><p>点击“完成”创建函数。</p><p>“HTTP函数”跳转到“触发器”Tab,而“事件函数”直接跳转到“代码执行”Tab。 <br>切换到“触发器”,我们可以看到,没有任何数据。</p><hr><p>我们看一下“事件函数”的实例代码:</p><ol><li><p>结构:exports.handler = (event, context, callback) => {}</p><ul><li>函数调用时,执行定义的handler逻辑,参数是event, context, callback;</li><li>这些参数我们依旧在后续==调试阶段==看一下</li></ul></li><li>依旧打印标准版的输出<code>hello world</code></li><li><p>通过callback返回数据</p><ul><li><p>callback(err, data)</p><ul><li>第一个参数是错误信息</li><li>第二个参数是数据,只有在第一个参数为<code>null</code>时,才返回数据</li></ul></li></ul></li></ol><p>代码的“执行”按钮在上边,尝试修改代码,也能看到是自动保存。</p><p>执行一下程序看看会发生什么?</p><ol><li>打印返回的结果</li><li>打印函数执行日志</li><li><p>打印<code>RequestID</code></p><ul><li>这是唯一存在的ID,每次执行都会改变。</li><li>可以通过该ID查询日志。</li></ul></li></ol><hr><p>我们从两个示例函数中,都可以看到注释的<code>exports.initializer</code>函数。 <br>这个函数是做什么的呢? <br>通过函数名,可以知道,这是实例的初始化函数,保证同一实例成功且仅成功执行一次。 <br>值得注意的是:<strong>这个函数没有返回值</strong>。 <br>将“事件函数”中的注释去掉,“保存并执行”,看看有什么不同。 <br>发现执行结果和原来没什么不同,初始化函数中的<code>console.log('initializing')</code>并没有打印出来。 </p><p>要怎么做呢? <br>要初始化函数执行,需要特殊的配置。 <br>切换到“概览”Tab,“修改配置” ——> “是否配置函数初始化入口”,定义为刚刚解注的函数名,“确认”后跳转至“代码执行”。 <br>“执行”代码,查看执行结果:报错——> 无效的函数名。 <br>重新“修改配置”,初始化入口定义为<code>index.initialzer</code>即可。</p><pre><code>FC Initialize Start RequestId: e8acfe4c-9670-4255-86f1-2659291031c1
load code for handler:index.initializer
2020-12-24T09:13:36.846Z e8acfe4c-9670-4255-86f1-2659291031c1 [verbose] initializing
FC Initialize End RequestId: e8acfe4c-9670-4255-86f1-2659291031c1
</code></pre><p>会看到函数执行日志中,多出来几条日志。 <br>连续多次点击“执行”,也仅仅在第一次执行的时候,会多这几条日志,表明“初始化函数”仅仅执行一次。 <br>修改“初始化函数”中的<code>callback(null, 123)</code>发现执行日志中并没有输出,表明“初始化函数”没有输出。</p><hr><p>有没有疑惑:</p><pre><code>var ret = '';
function handlerRet() {
console.log('-------');
ret = 'return success';
}
handlerRet();
exports.handler = (event, context, callback) => {
console.log(ret);
callback(null, 'hello world');
}</code></pre><p>上边这个代码的执行结果是怎样的? <br>和“初始化函数”有什么不同?</p><ul><li><p>执行时机不同</p><ul><li>“初始化函数”在函数实例初始化之前执行;</li><li>上述看似“全局”的代码是在实例化之后执行的;</li></ul></li><li><p>执行次数</p><ul><li>上述代码和“初始化函数”一样,都仅执行一次;</li></ul></li></ul><hr><p>我们可以看到,上述三种类型的函数(HTTP函数、事件函数、初始化函数)与普通定义的函数最大的区别在于,FC的函数预置了<code>Context</code>参数,这是和<code>Runtime</code>运行平台/上下文相关的参数。</p><hr><p>我们可以通过URL请求去调用“HTTP函数”,那如何去调用“事件函数”呢?</p><ul><li><p>创建触发器</p><ul><li><p>我们切换到“触发器”面板,“创建触发器”,以一个最简单的“定时触发器”为例。</p><ul><li>最小1分钟时间间隔</li><li>默认“启动触发器”</li><li>通过“日志查询”面板,“每分钟自动刷新”,可以查看执行日志(会有延迟)。</li></ul></li><li><p>修改触发器的“触发消息”:JSON数据,修改“代码执行”,在入口函数中打印<code>event</code>:<code>console.log(JSON.parse(event))</code>查看输出结果。</p><ul><li>可以看到,我们可以通过“触发消息”传递参数。</li></ul></li><li>关闭“触发器”的状态</li></ul></li><li><p>SDK调用</p><ul><li><p>本地编写代码程序</p><pre><code>'use strict';
var FCClient = require('@alicloud/fc2');
var client = new FCClient(
'<account id>',
{
accessKeyID: '<access key>',
accessKeySecret: '<access key secret>',
region: 'cn-beijing',
timeout: 10000 // milliseconds, default is 10s
}
);
async function test () {
try {
var ret = await client.invokeFunction('case-1.LATEST', 'case-event', 'event')
console.log('invoke function: %j', ret);
} catch (err) {
console.error(err);
}
}
test().then();</code></pre></li><li><p><code>node invoke/index.js</code></p><ul><li>可以看到本地终端有日志打印出来,正是代码中的<code>console.log('invoke function: %j', ret);</code>执行的结果</li></ul></li><li>控制台切换到“日志查询”,查看执行日志,确定FC的函数被触发。</li></ul></li></ul><hr><p>上述编写的函数除了“HTTP函数”并没有引入外部依赖,如何引入第三方依赖呢? <br>其实,“HTTP函数”引入的依赖是阿里云平台的<code>Node.js</code>环境<a href="https://link.segmentfault.com/?enc=QGM6w%2FzW3N6URe9KM4Nwpw%3D%3D.ofSRvPaEkfeVpm3lmQ8FHEpJy4bX5GkvNDbjJWkfNyOl3ySjQ9JXCCp1HYXjhsGSHD33a2LI4Xaicv6AMflUTO82jfskGA1En1pr0irzJtCv5u1ej6F75CafEftsA6oBbRERy66vjZdgzXwYwb4nhQ%3D%3D" rel="nofollow">内置好的第三方包</a>,如果我们需要使用没有内置的依赖包,需要在本地开发环境去安装、编写代码逻辑。 </p><p>所以,我们接下来说一下本地<strong>开发环境的配置</strong>:</p><ul><li>安装<code>Docker</code>; //编译代码、安装依赖以及在本地运行调试等操作都是在<code>Docker</code>镜像中进行;</li><li><p><code>Visual Studio Code</code>中查找<code>aliyun serverless</code>插件并安装;</p><ul><li>安装过程中需要输入<code>account id</code>、<code>access key</code>、<code>access key secret</code>。</li><li>可以通过阿里云官网账号一栏找到这些信息。</li><li>我们会看到<code>Visual Studio Code</code>右侧面板多出了两个FC的Logo选项。</li></ul></li></ul><p><img src="/img/remote/1460000038703916" alt="插件界面" title="插件界面"></p><ul><li>可以通过界面查看到远程控制台创建的服务及函数。</li><li>将远程服务及函数下载到本地</li></ul><p><img src="/img/remote/1460000038703917" alt="下载服务" title="下载服务"> <br><img src="/img/remote/1460000038703922" alt="本地服务 " title="本地服务 "></p><ul><li>从<code>A</code>区域,我们可以看到下载到本地对应的服务、函数及触发器列表,点击列表中的某项,会跳转到<code>template.yml</code>文件对应的配置</li><li><p><code>B</code>区域,是对列表项的操作</p><ul><li>服务:添加函数操作</li><li>函数:查看源码、调试、执行操作</li><li>触发器:无</li></ul></li></ul><p>接下来,我们通过<strong>代码调试</strong>先看一下编写代码时,遗留的函数参数结构的问题,然后再说依赖问题: <br>查看<code>case-event</code>函数的源码,在行号上添加断点,点击“调试”操作 <br><img src="/img/remote/1460000038703921" alt="debugger" title="debugger"> <br>即可查看对应的参数结构。 </p><p><strong>引入第三方NPM包</strong></p><ul><li>通过<code>Visual Studio Code</code>“资源管理器”查看一下<code>case-event</code>函数所在的路径</li><li>“终端”切换到函数对应目录<code>cd case-1/case-event </code></li><li><code>npm init -y</code>初始化环境</li><li><code>npm i -S xss</code>做示例</li><li>修改代码</li></ul><pre><code>'use strict';
var xss = require('xss');
/*
To enable the initializer feature (https://help.aliyun.com/document_detail/156876.html)
please implement the initializer function as below:
*/
exports.initializer = (context, callback) => {
console.log('initializing');
callback(null, '123');
};
exports.handler = (event, context, callback) => {
console.log('hello world');
var html = xss('<script>alert</script>')
callback(null, html);
}</code></pre><ul><li>执行函数,查看输出结果:依赖正常执行。</li></ul><pre><code>FC Initialize Start RequestId: e2c60d38-bed8-4a92-a48f-56b7c7949d9a
load code for handler:index.initializer
2020-12-25T03:21:47.571Z e2c60d38-bed8-4a92-a48f-56b7c7949d9a [verbose] initializing
FC Initialize End RequestId: e2c60d38-bed8-4a92-a48f-56b7c7949d9a
123FC Invoke Start RequestId: e2c60d38-bed8-4a92-a48f-56b7c7949d9a
load code for handler:index.handler
2020-12-25T03:21:47.651Z e2c60d38-bed8-4a92-a48f-56b7c7949d9a [verbose] hello world
FC Invoke End RequestId: e2c60d38-bed8-4a92-a48f-56b7c7949d9a
&lt;script&gt;alert&lt;/script&gt;</code></pre><ul><li><p>然后,我们将服务整体上传或在函数上右键单独上传,替换控制台的代码</p><ul><li>会将依赖<code>node_modules</code>一起上传</li><li>FC函数所需要的依赖必须一同打包上传,否则,会报资源查找不到。</li></ul></li></ul><p><img src="/img/remote/1460000038703918" alt="代码上传" title="代码上传"></p><hr><p>介绍完阿里云平台的函数计算,结合<code>Serverless</code>的定义思考一下,<code>Serverless</code>=<code>FaaS</code>架构,<code>Serverless</code>具有实时弹性扩缩容的优势,函数计算怎么实现这个优势的呢? </p><p>这和FC函数的进程模型有关:</p><ul><li>服务托管细粒化到了语言单位,即函数调用</li><li>事件驱动的计算模型</li><li><p>用完即毁型设计:函数实例准备好后,执行完函数就直接结束。</p><ul><li>无状态,不存储任何状态</li><li>正因为没有任何状态,因此在并发量高的时候,我们可以对无状态节点横向扩容,而没有流量时我们可以缩容到 0</li></ul></li></ul><hr><p>刚刚说到<code>FaaS</code>或FC的函数是无状态的,那我们需要状态共享的时候,应该怎么做?</p><p>借助于<code>BaaS</code>: 后端即服务。 <br><code>BaaS</code>包含后端服务、云厂商提供的云服务:云数据库、对象存储、消息队列等。</p><p><code>Serverless</code>可以理解为运行在<code>FaaS</code>中的,调用<code>BaaS</code>的函数。</p><hr><p>“自定义域名”中,我们可以将编写的函数与备案好的域名绑定在一起,这样,可以通过自定义的域名访问我们的“HTTP函数”。</p><h2>应用示例</h2><h4><code>Nuxt.js</code>应用的迁移</h4><ol><li>迁移应用需要使用<code>Funcraft</code>命令行工具<code>npm i -g @alicloud/fun</code>全局安装;</li><li><code>fun --version</code>查看版本信息验证是否安装成功;</li><li>这里我下载了一个已有的项目,进入项目目录下,确保<code>node</code>版本在<code>12.*</code>以上,<code>npm i</code>安装开发依赖;</li><li><code>npm run dev</code>保证我们的项目本地正常运行;</li><li><code>npm run build</code>编译项目;</li><li><code>npm run start</code>保证编译后的项目能够正常启动;</li></ol><p><img src="/img/remote/1460000038703915" alt="uri错误" title="uri错误"> <br>因为我下载的这个项目配置的线上访问地址无法访问,所以,添加这步验证一下。</p><pre><code>//定位migc-open-act-master/nuxt.config.js文件
switch (process.env.NODE_ENV) {
case 'build': // 编译
envBase = '/gcact/migc-open-act/'
envHost = '0.0.0.0'
envStaticUrl = '/gcact/migc-open-act'
break
case 'start': // 启动
envBase = ''
envHost = '0.0.0.0'
envStaticUrl = '/gcact/migc-open-act'
break
case 'buildMice': // 编译
// 修改migc-open-act-master/nuxt.config.js文件
switch (process.env.NODE_ENV) {
case 'build': // 编译
envBase = './'
envHost = '0.0.0.0'
envStaticUrl = './'
break
case 'start': // 启动
envBase = ''
envHost = '0.0.0.0'
envStaticUrl = './'
break
case 'buildMice': // 编译</code></pre><p>重新执行5、6两步——现在成功访问;</p><ol><li><p><code>fun deploy -y</code>部署项目至函数计算;</p><pre><code>current folder is not a fun project.
Generating /Users/*****/Desktop/case/migc-open-act-master/bootstrap...
Generating template.yml...
Generate Fun project successfully!</code></pre><ul><li><p>自动生成<code>template.yml</code>文件</p><pre><code>ROSTemplateFormatVersion: '2015-09-01'
Transform: 'Aliyun::Serverless-2018-04-03'
Resources:
migc-open-act-master: # service name
Type: 'Aliyun::Serverless::Service'
Properties:
Description: This is FC service
migc-open-act-master: # function name
Type: 'Aliyun::Serverless::Function'
Properties:
Handler: index.handler
Runtime: custom
CodeUri: oss://fun-gen-cn-beijing-*****/9c517abf18826f644880440a12eebef7
MemorySize: 1024
InstanceConcurrency: 5
Timeout: 120
Events:
httpTrigger:
Type: HTTP
Properties:
AuthType: ANONYMOUS
Methods: ['GET', 'POST', 'PUT']
Domain:
Type: Aliyun::Serverless::CustomDomain
Properties:
DomainName: Auto
Protocol: HTTP
RouteConfig:
Routes:
"/*":
ServiceName: migc-open-act-master
FunctionName: migc-open-act-master
</code></pre></li><li><p>自动生成<code>bootstrap</code>文件</p><pre><code>#!/usr/bin/env bash
export PORT=9000
npx nuxt start --hostname 0.0.0.0 --port $PORT</code></pre></li><li>自动生成一个可访问的临时域名</li></ul><pre><code>Detect 'DomainName:Auto' of custom domain 'Domain'
Request a new temporary domain ...
The assigned temporary domain is http://38880398-*****.test.functioncompute.com,expired at 2021-01-04 15:13:18, limited by 1000 per day.
Waiting for custom domain Domain to be deployed...</code></pre></li></ol><p>这两个文件是做什么的呢? <br>带着疑问,我们看<code>Custom Runtime</code></p><h4><code>Custom Runtime</code></h4><p>刚刚我们迁移了<code>Nuxt.js</code>应用,如果想迁移其它应用呢? <br>迁移应用之前,必须要了解一个前提:要在平台支持的开发环境基础上迁移项目。</p><ul><li><code>Custom Runtime</code>就是在平台的基础上,自定义运行环境。</li><li><code>Custom Runtime</code>的本质是<code>HTTP Server</code>。</li></ul><p>那如何创建<code>Custom Runtime</code>?</p><ol><li><p>搭建一个监听<code>9000</code>固定端口的<code>HTTP Server</code></p><pre><code>// 部署静态页面为例
var Koa = require('koa');
var path = require('path');
var htmlRender = require('koa-html-render');
var app = new Koa();
var port = 9000;
app.use(htmlRender());
app.use(async (ctx) => {
await ctx.html(path.resolve(__dirname, ctx.path));
})
app.listen(process.env.PORT || port, () => {
console.log(`----koa is running on ${process.env.PORT || port}=====`)
})</code></pre></li><li><p>将启动Server的命令保存在一个名为<code>bootstrap</code>的文件</p><pre><code>// 创建bootstrap文件
#!/usr/bin/env bash
export PORT=9000
node app.js</code></pre></li><li><code>fun deploy -y</code>将项目部署到函数计算上</li><li>可以通过临时链接访问该静态项目</li></ol><p>由此,我们可以看到<code>bootstrap</code>文件是<code>HTTP Server</code>的启动文件。 <br><code>template.yml</code>对应我们服务列表、函数列表的配置项。</p><h4><code>Koa</code>应用的迁移</h4><p>上述例子,是静态页面的迁移,也可以看作是<code>Koa</code>应用的迁移。</p><h4>连接<code>MongoDB</code>示例</h4><p>这里,开通了阿里云<code>MongoDB</code>的服务,代码示例链接数据库,将<code>testColl</code>文档数据导出。 <br>这个示例需要注意依赖版本<code>require('mongodb')</code>,<code>mongodb</code>的版本需要是<code>2.2.*</code>。</p><pre><code>var uuid = require('node-uuid');
var sprintf = require("sprintf-js").sprintf;
var mongoClient = require('mongodb').MongoClient;
var host = "dds-*******-pub.mongodb.rds.aliyuncs.com";
var port = 3717;
var username = "user***";
var password = "***";
var demoDb = "sls";
var demoColl = "testColl";
// 官方建议使用的方案
var url = sprintf("mongodb://%s:%d/%s", host, port, demoDb);
console.info("url:", url);
var conn;
exports.initializer = async function (context, callback) {
// 获取mongoClient
await mongoClient.connect(url, function(err, db) {
if(err) {
console.error("connect err:", err);
return 1;
}
// 授权. 这里的username基于admin数据库授权
var adminDb = db.admin();
adminDb.authenticate(username, password, function(err, result) {
if(err) {
console.error("authenticate err:", err);
return 1;
}
conn = db;
// 取得Collecton句柄
conn.db(demoDb)
callback(null, '')
});
});
}
exports.handler = function (event, context, callback) {
var collection = conn.collection(demoColl);
collection.find({}).toArray(function(err, docs) {
console.log("Found the following records");
console.log(docs)
callback(null, docs);
});
}</code></pre><h2>总结</h2><h3>应用场景</h3><ul><li>长尾应用</li><li><p>大规模批处理任务</p><ul><li>弹性伸缩</li></ul></li><li><p>基于事件驱动架构的应用</p><ul><li>事件驱动</li></ul></li><li><p>运维自动化</p><ul><li>触发器</li></ul></li></ul><h3>局限</h3><ul><li>用户对底层计算资源没有可控性</li><li>由于目前技术的成熟度,<code>Serverless</code>领域尚没有形成行业标准,意味着用户将一个平台上的<code>Serverless</code>应用移植到另一个平台时付出的成本较高</li></ul><h3>前端学习<code>Serverless</code>的出发点</h3><ul><li><p>打破潜意识技术边界</p><ul><li>调优行业内的开发岗位分层结构</li><li>Serverless补足了前端工程师的现有能力,前端与Serverless结合,是<strong>对前端的诉求从页面开发向开发交付整个应用转变</strong></li></ul></li><li><p>享受云服务红利</p><ul><li><strong>零运维</strong></li><li>Node.js + Serverless,向全栈进发</li></ul></li><li><p>云开发者的切入点</p><ul><li>熟悉云开发模式与思想</li></ul></li></ul>
Koa & Mongoose & Vue实现前后端分离--14总结
https://segmentfault.com/a/1190000022054073
2020-03-18T10:23:25+08:00
2020-03-18T10:23:25+08:00
米花儿团儿
https://segmentfault.com/u/mihuartuanr
1
<h2>说明</h2>
<p>本系列里所有第三方包的选型至少符合其中两条标准:<br><strong>傻瓜式、文档齐全、亲测可用</strong>。</p>
<p>本系列内容只是一个小Demo,并不包含复杂逻辑,仅仅覆盖了基本数据的<strong>增删改查 & 文件操作</strong>。</p>
<p>本系列<strong>依赖环境依据当前书写时间为标准</strong>,注意<code>package.json</code>文件,后期依赖更新,出现问题,解释权归谁都不归我所有?</p>
<h2>系列内容</h2>
<ul>
<li><a href="https://segmentfault.com/a/1190000021924672">01序言</a></li>
<li><a href="https://segmentfault.com/a/1190000021924701">02<code>koa</code>搭建服务端</a></li>
<li><a href="https://segmentfault.com/a/1190000021927364">03<code>mongoose</code>连接数据库 & 预定义数据结构</a></li>
<li><a href="https://segmentfault.com/a/1190000021931689">04服务端注册&登录:用户路由配置</a></li>
<li><a href="https://segmentfault.com/a/1190000021937077">05服务端注册&登录:业务逻辑</a></li>
<li><a href="https://segmentfault.com/a/1190000021945599">06客户端登录&注册</a></li>
<li><a href="https://segmentfault.com/a/1190000021952219">07登录加密&服务端参数校验</a></li>
<li><a href="https://segmentfault.com/a/1190000021956708">08前端状态管理&路由嵌套</a></li>
<li><a href="https://segmentfault.com/a/1190000021969629">09身份验证JWT&测试</a></li>
<li><a href="https://segmentfault.com/a/1190000021978182">10更新用户信息</a></li>
<li><a href="https://segmentfault.com/a/1190000021996824">11更新用户头像:图片上传</a></li>
<li><a href="https://segmentfault.com/a/1190000022008050">12数据库联表查询</a></li>
<li><a href="https://segmentfault.com/a/1190000022038423">13日志系统&安全系统</a></li>
</ul>
<h2>感谢</h2>
<p>感谢疫情,让我节省了上下班的路程时间,没有借口不完成内容。</p>
<h2>期望</h2>
<p>如果同学有指导性学习,欢迎提宝贵意见,对于学习,我没有脸皮。</p>
Koa & Mongoose & Vue实现前后端分离--13日志系统&安全系统
https://segmentfault.com/a/1190000022038423
2020-03-17T00:00:47+08:00
2020-03-17T00:00:47+08:00
米花儿团儿
https://segmentfault.com/u/mihuartuanr
0
<h2>上节回顾</h2>
<ul>
<li>联表查询</li>
<li>审批增删改查逻辑</li>
</ul>
<h2>工作内容</h2>
<ul>
<li>配置日志系统</li>
<li>测试日志系统</li>
<li>安全策略</li>
</ul>
<h2>准备工作</h2>
<ul>
<li>
<code>npm i -S koa-loa4</code> // 先切换到<code>/server</code>目录下</li>
<li>
<code>npm i -S xss</code> // 先切换到<code>/server</code>目录下</li>
</ul>
<h2>日志系统</h2>
<h3>技术选型</h3>
<ul>
<li>
<p>koa-logger</p>
<ul><li>只能设置统一的日志</li></ul>
</li>
<li>
<p>koa-morgan</p>
<ul><li>能根据<code>skip</code>分类,但,可操作性太差</li></ul>
</li>
<li>
<p>koa-log4js</p>
<ul>
<li>可以定制化</li>
<li>周期性存储</li>
</ul>
</li>
</ul>
<h3>koa-log4js规范对象</h3>
<ul>
<li><a href="https://link.segmentfault.com/?enc=5VMvsjeqUpcha1XsgAiCaQ%3D%3D.OYzRtlO0rerY9mSnOBCPmADt4oke%2BX2CQpBAgSUsFCrS45z%2FZsU%2FelwHUIwLds%2Fyl0HuB4gFwQ0kfONQoj24%2F7DBcZtT7YbHcJjicRHOsUM%3D" rel="nofollow">参考文档</a></li>
<li>log4默认是禁用的,不会打印日志:<code>log4js.configure()</code>
</li>
<li>log4通过配置项开启日志功能:<code>log4js.configure({...})</code>
</li>
</ul>
<pre><code>// configure规范对象
{
// 自定义日志等级/修改内部已定义的日志等级
// 日志等级:OFF > MARK > FATAL > ERROR > WARN > INFO > DEBUG > TRACE > ALL
levels: {
...
},
{
// 定义日志输出类型 (参考文档:https://github.com/log4js-node/log4js-node/blob/master/docs/appenders.md)
appenders: {
error: {
// type是必选属性,其它属性配置依赖于type属性值
type: 'dateFile', // (dateFile参考文档:https://github.com/log4js-node/log4js-node/blob/master/docs/dateFile.md)
filename: path.resolve(__dirname, 'logs', 'error', 'filename'), // 定义日志存储位置,与最终指向的目录/文件同级:filename推荐不要加后缀名
pattern: '.yyyy-MM-dd.log', // 日志周期,这里加上文件后缀
alwaysIncludePattern: true, // alwaysIncludePattern为true时,才会实现以pattern规则(如:yyyy-MM-dd)新建文件一天一存
//keepFileExt: true // 没有像官网说的那样生效,所以,通过设置pattern值带后缀文件名来替代
}
},
// 预自定义日志类型:log4js.getLoggeer([category])获取该类型的日志实例
categories: {
// 必须定义一个默认类型,当没有匹配的类型时,一律按默认类型处理
default: {
},
errorLog: {
appenders: ['error'], // 指定日志输出类型
// 指定可用的最小日志等级
level: 'error', //可以使用OFF > MARK > FATAL > ERROR等级,不可以使用WARN > INFO > DEBUG > TRACE > ALL等级
enableCallStack: true // 是否打印所属文件名 & 行号
}
}
}
}</code></pre>
<h3>服务端配置</h3>
<p><strong>代码部分,注意转义</strong></p>
<pre><code>// 新建文件:/server/config/log.js
const path = require('path');
const { checkDirExist } = require('../utils/dir');
// 日志根目录
const baseLogDir = path.resolve(\_\_dirname, '../logs');
// 错误日志
const errorDir = 'error';
const errorFileName = 'error';
const errorDirPath = path.resolve(baseLogDir, errorDir);
const errorLogPath = path.resolve(errorDirPath, errorFileName);
// 访问日志
const accessDir = 'access';
const accessFileName = 'access';
const accessDirPath = path.resolve(baseLogDir, accessDir);
const accessLogPath = path.resolve(accessDirPath, accessFileName);
// 响应日志
const responseDir = 'response';
const responseFileName = 'response';
const responseDirPath = path.resolve(baseLogDir, responseDir);
const responseLogPath = path.resolve(responseDirPath, responseFileName);
\[errorDirPath, accessDirPath, responseDirPath\].forEach(p \=> {
checkDirExist(p);
})
module.exports = {
// 定义日志输出类型https://github.com/log4js-node/log4js-node/blob/master/docs/appenders.md
appenders: {
console: {
type: 'stdout'
},
error: {
type: 'dateFile', //https://github.com/log4js-node/log4js-node/blob/master/docs/dateFile.md
filename: errorLogPath, // 定义生成文件的路径。
daysToKeep: 7, // 保存7天日志,大于7天的,删除;
pattern: '.yyyy-MM-dd.log',
alwaysIncludePattern: true, // 只有该属性设置为true,才会以pattern追加重命名filename
// keepFileExt: true //不起作用
},
access: {
type: 'dateFile',
filename: accessLogPath,
daysToKeep: 7, // 保存7天日志,大于7天的,删除;
pattern: '.yyyy-MM-dd.log',
alwaysIncludePattern: true, // 只有该属性设置为true,才会以pattern追加重命名filename
// keepFileExt: true //不起作用
},
response: {
type: 'dateFile',
filename: responseLogPath,
daysToKeep: 7, // 保存7天日志,大于7天的,删除;
pattern: '\-yyyy-MM-dd.log',
alwaysIncludePattern: true, // 只有该属性设置为true,才会以pattern追加重命名filename
// keepFileExt: true //不起作用
}
},
// 定义Logger对象类型,用于log4js.getLogger(\[category\])
categories: {
default: {
appenders: \['console'\], level: 'all'
},
errorLogger: {
appenders: \['error'\], level: 'error'
},
accessLogger: {
appenders: \['access'\], level: 'info'
},
responseLogger: {
appenders: \['response'\], level: 'info'
}
}
};</code></pre>
<h3>服务端日志工具库</h3>
<p><strong>代码部分,注意转义</strong></p>
<pre><code>// 新建文件:/server/utils/log.js
const log4js = require('koa-log4');
const config = require('../config/log');
//以规范对象开启日志功能
log4js.configure(config);
// 获取日志对象实例
const errorLogger = log4js.getLogger('errorLogger');
const accessLogger = log4js.getLogger('accessLogger');
const responseLogger = log4js.getLogger('responseLogger');
const consoleLogger = log4js.getLogger();
const logUtil = {};
// 封装错误日志
logUtil.logError \= function (ctx, error, resTime) {
if (ctx && error) {
errorLogger.error(formatError(ctx, error, resTime));
}
};
// 封装请求日志
logUtil.logAccess \= function (ctx, resTime) {
if (ctx) {
accessLogger.info(formatAccessLog(ctx, resTime));
}
};
// 封装响应日志
logUtil.logResponse \= function (ctx, resTime) {
if (ctx) {
responseLogger.info(formatRes(ctx, resTime));
}
};
logUtil.logInfo \= function (info) {
if (info) {
consoleLogger.info(formatInfo(info));
}
};
const formatInfo \= function (info) {
let logText = '';
// 响应日志开始
logText += '\\n' + '\*\*\*\*\*\*\*\*\*\*\*\*\*\*\* console log start \*\*\*\*\*\*\*\*\*\*\*\*\*\*\*' + '\\n';
// 响应内容
logText += 'console detail: ' + '\\n' + JSON.stringify(info) + '\\n';
// 响应日志结束
logText += '\*\*\*\*\*\*\*\*\*\*\*\*\*\*\* console log end \*\*\*\*\*\*\*\*\*\*\*\*\*\*\*' + '\\n';
return logText;
};
// 格式化响应日志
const formatRes \= function (ctx, resTime) {
let logText = '';
// 响应日志开始
logText += '\\n' + '\*\*\*\*\*\*\*\*\*\*\*\*\*\*\* response log start \*\*\*\*\*\*\*\*\*\*\*\*\*\*\*' + '\\n';
// 添加请求日志
logText += formatAccessLog(ctx, resTime);
// 响应状态码
logText += '\\n' + 'response status: ' + ctx.status + '\\n';
// 响应内容
logText += 'response body: ' + '\\n' + JSON.stringify(ctx.body) + '\\n';
// 响应日志结束
logText += '\*\*\*\*\*\*\*\*\*\*\*\*\*\*\* response log end \*\*\*\*\*\*\*\*\*\*\*\*\*\*\*' + '\\n';
return logText;
};
// 格式化错误日志
const formatError \= function (ctx, err, resTime) {
let logText = '';
// 错误信息开始
logText += '\\n' + '\*\*\*\*\*\*\*\*\*\*\*\*\*\*\* error log start \*\*\*\*\*\*\*\*\*\*\*\*\*\*\*' + '\\n';
// 添加请求日志
logText += formatAccessLog(ctx, resTime);
// 错误名称
logText += '\\n' + 'err name: ' + err.name + '\\n';
// 错误信息
logText += 'err message: ' + err.message + '\\n';
// 错误详情
logText += 'err stack: ' + err.stack + '\\n';
// 错误信息结束
logText += '\*\*\*\*\*\*\*\*\*\*\*\*\*\*\* error log end \*\*\*\*\*\*\*\*\*\*\*\*\*\*\*' + '\\n';
return logText;
};
// 格式化请求日志
const formatAccessLog \= function (ctx, resTime) {
const { method, originalUrl, ip, query, params, body } = ctx.request;
let logText = '';
// 客户端ip
logText += 'request client ip: ' + ip + '\\n';
// 客户端
logText += 'request userAgent: ' + ctx.header\['user-agent'\] + '\\n';
// 访问协议
logText += 'request protocol: ' + ctx.protocol + '\\n';
// 访问方法
logText += 'request method: ' + method + '\\n';
// 请求原始地址
logText += 'request originalUrl: ' + originalUrl + '\\n';
// 请求参数
logText += params ? 'request params: ' + JSON.stringify(params) + '\\n' : '';
logText += query ? 'request query: ' + JSON.stringify(query) + '\\n' : '';
logText += body ? 'request body: ' + JSON.stringify(body) + '\\n' : '';
// 服务器响应时间
logText += 'response time: ' + resTime + '\\n';
return logText;
};
module.exports = logUtil;</code></pre>
<h3>服务端使用日志服务</h3>
<pre><code>// 更新文件:在需要使用的地方或统一拦截的地方修改
// 如:/server/app.js
...
const logUtil = require('./utils/log');
...
// 错误处理
app.use(function(ctx, next){
return next().catch((err) => {
logUtil.logError(ctx, err)
...
</code></pre>
<h3>测试结果</h3>
<p><img src="/img/bVbEDmm" alt="image.png" title="image.png"></p>
<h2>安全系统</h2>
<p>安全系统很简单,直接在需要进行<code>xss</code>攻击处理的地方,进行一下改进即可</p>
<pre><code>var xss = require("xss");
var html = xss('<script>alert("xss");</script>');</code></pre>
<p><code>xss</code>包会根据<a href="https://link.segmentfault.com/?enc=jccUK1ID6N%2BtE68YFnU%2B4w%3D%3D.UTd%2FjeSIN0GXXoPBIH1%2Bv6E37mQpZJvFDiDxSerG6mTE2ZSqkgFzssTodv3EgrODJBnlzsc5jeT9ietVdhaD0w%3D%3D" rel="nofollow">预先定义好的规则</a>,转义标签,过滤调具有攻击性的属性。</p>
<h2>参考文档</h2>
<p><a href="https://link.segmentfault.com/?enc=9pNfPJN%2FPetXEPZ53u9uZQ%3D%3D.uDsC%2BNCZFr2JJtrGQ%2BoZ0mOxH1QM%2BWWM%2B%2F3eZG%2BIla9HWdPh3tO2QCstX%2BummEDQ7Dc9zY6F%2B8onhdwjanAkBg%3D%3D" rel="nofollow">log4js-node</a><br><a href="https://link.segmentfault.com/?enc=LklYHaN6A%2BrHCcWGnoz6Ww%3D%3D.4luzbSKkrUmXRW1LlsTvWpziHR2XXIBiW5V7V8FPdG9TXLVoLk3OhOdLWoXR9uFJ" rel="nofollow">xss</a></p>
Koa & Mongoose & Vue实现前后端分离--12联表查询
https://segmentfault.com/a/1190000022008050
2020-03-16T00:10:18+08:00
2020-03-16T00:10:18+08:00
米花儿团儿
https://segmentfault.com/u/mihuartuanr
0
<h2>上节回顾</h2>
<ul><li>图片上传 & 存储 & 访问</li></ul>
<h2>工作内容</h2>
<ul>
<li>审批的增删改查</li>
<li>更新、删除数据时,校验当前用户是不是数据所属人</li>
<li>通过<code>ref</code>查询另一表中的数据</li>
</ul>
<h2>准备工作</h2>
<ul>
<li>
<code>npm i -S moment</code> //分别切换到<code>/server</code>、<code>/client</code>目录下安装</li>
<li>把服务端<code>/users</code>的路由和处理逻辑拷一份,改改名</li>
<li>这里主要是罗列代码</li>
</ul>
<h2>业务逻辑</h2>
<h3>服务端代码</h3>
<ul><li>路由处理逻辑</li></ul>
<pre><code>// 新建文件:server/control/approves.js
const moment = require('moment');
const approveModel = require('../model/approve');
const userModel = require('../model/user');
async function list(ctx) {
// 【通常做法,由多的一方过滤单一的一方】
// 文档:http://www.mongoosejs.net/docs/populate.html#refs-to-children
try {
const approves = await approveModel.find({
author: ctx.state.auth.id
});
ctx.body = {
code: '200',
data: approves,
msg: '查询成功'
};
} catch (err) {
ctx.body = {
code: '403',
data: {
error: err
},
msg: '查询失败'
}
}
}
async function get(ctx){
const { id } = ctx.params;
try {
const approve = await approveModel.findOne({
_id: id
});
if(approve) {
ctx.body = {
code: '200',
data: approve,
msg: '成功'
}
} else {
ctx.body = {
code: '403',
data: null,
msg: '找不到数据创建人'
}
}
} catch (err) {
ctx.body = {
code: '404',
data: {
_id: id
},
msg: '获取失败,请核对数据id'
}
}
}
async function create(ctx) {
const { id: loginerId } = ctx.state.auth;
const payload = ctx.request.body;
const curtime = moment().format('x');
try {
const newApprove = await new approveModel({
...payload,
status: false,
author: loginerId,
modifier: loginerId,
createtime: curtime,
latesttime: curtime,
}).save();
ctx.body = {
code: '200',
data: newApprove,
msg: '新建成功'
}
} catch (err) {
ctx.body = {
code: '403',
data: null,
msg: '新建失败'
}
}
}
async function update(ctx){
const payload = ctx.request.body;
const { id } = ctx.params;
const curtime = moment().format('x');
try {
await approveModel.updateOne(
{
_id: id
},
{
...payload,
latesttime: curtime
}
).exec();
ctx.body= {
code: '200',
data: {
_id: id
},
msg: '更新成功'
}
} catch (err) {
ctx.body = {
code: '404',
data: payload,
msg: '更新失败,请核对数据id'
}
}
}
async function drop(ctx){
const { id } = ctx.params;
await approveModel.findOneAndRemove({
_id: id
})
ctx.body = {
code: '200',
data: null,
msg: '删除成功'
}
}
module.exports = {
list,
get,
create,
update,
drop
}</code></pre>
<ul><li>
<p>路由拦截</p>
<ul><li>在进行敏感操作,如更新、删除,先判断当前用户是否是数据所属人<code>checkLoginer</code>
</li></ul>
</li></ul>
<pre><code>// 更新文件:server/router/approves.js
const Router = require('@koa/router');
const approveModel = require('../model/approve');
const controls = require('../control/approves');
const routerUtils = require('../utils/router');
const {
list,
get,
create,
update,
drop
} = controls;
const router = new Router({
prefix: '/approves'
});
async function checkLoginer (ctx, next) {
const { id } = ctx.params;
const approve = await approveModel.findOne({
_id: id
});
if (approve.author != ctx.state.auth.id) {
ctx.body = {
code: '403',
data: null,
msg: '当前用户不是创建者'
}
} else {
await next()
}
}
const routes = [
{
path: '/',
method: 'GET',
handle: list
},
{
path: '/',
method: 'POST',
handle: create
},
{
path: '/:id',
method: 'GET',
handle: get
},
{
path: '/:id',
method: 'PATCH',
payload: {},
handle: update,
middlewares: [
checkLoginer
]
},
{
path: '/:id',
method: 'DELETE',
handle: drop,
middlewares: [
checkLoginer
]
}
]
routerUtils.register.call(router, routes);
module.exports = router;</code></pre>
<ul><li>更新路由配置文件</li></ul>
<pre><code>// 更新文件:server/router/index.js
const userRouter = require('./users');
const assetsRouter = require('./assets');
const approvesRouter = require('./approves');
module.exports = [
userRouter.routes(),
userRouter.allowedMethods(),
approvesRouter.routes(),
approvesRouter.allowedMethods(),
assetsRouter.routes(),
assetsRouter.allowedMethods()
];</code></pre>
<ul>
<li>测试新建:[动态太大,传不上来]</li>
<li>测试查询</li>
</ul>
<p><img src="/img/bVbEvm3" alt="list.gif" title="list.gif"></p>
<ul><li>测试更新:</li></ul>
<p><img src="/img/bVbEvna" alt="update.gif" title="update.gif"></p>
<ul><li>测试删除:</li></ul>
<p><img src="/img/bVbEvm5" alt="delete.gif" title="delete.gif"></p>
<h3>前端代码</h3>
<pre><code>// 更新文件:client/src/views/approve-panel/index.vue
<template\>
<div class\="approve-module"\>
<div class\="btns-wrap"\>
<el-button type\="primary" @click\="handleApprove('increase')"\>新建</el-button\>
</div\>
<el-table
:data\="table.tableBody"
border
@selection-change\="handleSelectionChange"
style\="width: 100%"\>
<template v-for\="header in table.tableHeader"\>
<el-table-column
v-if\="header.key === 'selection'"
:key\="header.key"
v-bind\="Object.assign({}, header.options, header.layout)"
\>
</el-table-column\>
<el-table-column
v-else
:key\="header.key"
:label\="header.metas.label"
v-bind\="Object.assign({}, header.options, header.layout)"\>
<template slot-scope\="scope"\>
<span v-if\="header.metas.type === 'text'"\>{{scope.row\[header.key\]}}</span\>
<el-button-group v-else-if\="header.metas.type === 'button'"\>
<template v-for\="btn in header.metas.value"\>
<el-button :class\="\`btn-${btn.key}\`" @click\="handleApprove(btn.key, scope.row)" v-if\="btn.attributes.visible" size\="small" type\="text" :key\="btn.key" v-bind\="btn.attributes"\>{{btn.label}}</el-button\>
</template\>
</el-button-group\>
<span v-else\>{{header.metas.formatter(scope.row\[header.key\])}}</span\>
</template\>
</el-table-column\>
</template\>
</el-table\>
<el-dialog :title\="dialogForm.title" :visible.sync\="dialogForm.visible"\>
<el-form :inline\="false" :ref\="dialogForm.formRef" :model\="dialogForm.form" :rules\="dialogForm.rules"\>
<el-form-item label\="名称" prop\="name"\>
<el-input v-model\="dialogForm.form.name"\></el-input\>
</el-form-item\>
<el-form-item label\="分类" prop\="category"\>
<el-select v-model\="dialogForm.form.category" placeholder\="请选择分类"\>
<el-option label\="年假" value\="1"\></el-option\>
<el-option label\="病假" value\="2"\></el-option\>
</el-select\>
</el-form-item\>
<el-form-item label\="描述" prop\="description"\>
<el-input type\="textarea" v-model\="dialogForm.form.description"\></el-input\>
</el-form-item\>
</el-form\>
<div slot\="footer" class\="dialog-footer"\>
<el-button @click\="cancelOperate"\>取 消</el-button\>
<el-button type\="primary" @click\="submitForm"\>确 定</el-button\>
</div\>
</el-dialog\>
</div\>
</template\>
<script\>
import moment from 'moment'
import http from '@/utils/http'
export default {
INITFORMDATA: {
name: '',
category: '',
description: '',
status: false,
latesttime: 0,
createtime: 0,
author: {},
},
methods: {
async init () {
const res \= await http.get('/approves')
if (res.code \=== '200') {
this.$set(this.table, 'tableBody', res.data)
}
},
handleSelectionChange (val) {
console.log(val)
},
handleClick (row) {
console.log(row)
},
handleApprove (type, initData) {
this\[\`${type}Handle\`\](initData)
},
cancelOperate () {
this.dialogForm \= Object.assign(
{},
this.dialogForm,
{
visible: false,
formRef: '',
form: this.$options.INITFORMDATA
}
)
},
increaseHandle (initData \= {}) {
this.dialogForm \= Object.assign(
this.dialogForm,
{
visible: true,
title: '新建',
formRef: 'increaseForm'
}
)
},
editHandle (initData \= {}) {
this.dialogForm \= Object.assign(
this.dialogForm,
{
visible: true,
title: '编辑',
formRef: 'editForm',
form: {
...initData
}
}
)
},
dropHandle (initData \= {}) {
this.$confirm('此操作将永久删除该数据, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () \=> {
const res \= await http.delete(\`/approves/${initData.\_id}\`)
if (res.code \=== '200') {
this.$message({
type: 'success',
message: '删除成功!'
})
this.init()
} else {
throw(new Error('发生错误'))
}
}).catch((err) \=> {
this.$message({
type: 'info',
message: err.message || '已取消删除'
});
});
},
async submitForm () {
try {
const valid \= this.$refs\[this.dialogForm.formRef\].validate()
if (this.dialogForm.formRef \=== 'increaseForm') {
const res \= await http.post(
'/approves',
{
...this.dialogForm.form
}
)
if (res.code \=== '200') {
this.$message({
type: 'success',
message: '新建成功'
})
} else {
this.$message({
type: 'error',
message: res.msg
})
}
} else {
const { \_id, name, category, description, status} \= this.dialogForm.form
const res \= await http.patch(
\`/approves/${\_id}\`,
{
name,
category,
description,
status
}
)
if (res.code \=== '200') {
this.$message({
type: 'success',
message: '更新成功'
})
} else {
this.$message({
type: 'error',
message: res.msg
})
}
}
this.cancelOperate()
this.init()
} catch (err) {
console.log(err)
}
}
},
data () {
return {
table: {
tableHeader: \[
{
key: 'selection',
metas: {
},
options: {
type: 'selection'
},
layout: {
}
},
{
key: 'name',
metas: {
label: '名称',
type: 'text'
},
options: {
'show-overflow-tooltip': true
},
layout: {
width: 200
}
},
{
key: 'category',
metas: {
label: '类别',
type: 'text'
},
layout: {
width: 60
}
},
{
key: 'description',
metas: {
label: '描述',
type: 'text'
},
options: {
'show-overflow-tooltip': true
},
layout: {
}
},
{
key: 'author',
metas: {
label: '创建人',
type: 'object',
formatter (val) {
return val.alias || val.account
}
},
layout: {
width: 80
}
},
{
key: 'createtime',
metas: {
label: '创建时间',
type: 'timestamp',
formatter (val) {
return moment(val).format('YYYY-MM-DD hh:mm')
}
},
layout: {
width: 150
}
},
{
key: 'latesttime',
metas: {
label: '更新时间',
type: 'timestamp',
formatter (val) {
return moment(val).format('YYYY-MM-DD HH:mm')
}
},
layout: {
width: 150
}
},
{
key: 'operate',
metas: {
label: '操作',
type: 'button',
value: \[
{
key: 'edit',
label: '编辑',
attributes: {
visible: true,
disabled: false
}
},
{
key: 'drop',
label: '删除',
attributes: {
visible: true,
disabled: false
}
}
\]
},
layout: {
fixed: 'right',
width: 100
}
}
\],
tableBody: \[\]
},
dialogForm: {
visible: false,
title: '',
formRef: '',
form: {
name: '',
category: '',
description: '',
status: false
},
rules: {
name: \[
{ required: true, message: '请输入名称', trigger: 'blur' }
\],
category: \[
{ required: true, message: '请选择分类', trigger: 'change' }
\],
description: \[\]
}
}
}
},
async created () {
this.init()
}
}
</script\>
<style lang\="scss" scoped\>
@import '~@/stylesheets/layout.scss';
@import './index.scss';
</style\></code></pre>
<ul><li>这代码符号被转义…格式…建议复制到编辑器内,全局替换并格式化一下。</li></ul>
<pre><code>// 新建文件:client/src/views/approve-panel/index.scss
.approve-module {
.btns-wrap {
margin-bottom: 20px;
@include flex($content: flex-end);
}
/deep/ {
.btn-drop {
margin-left: 16px;
}
}
}</code></pre>
<p>展示效果:<br><img src="/img/bVbEvrm" alt="image.png" title="image.png"></p>
<p>这里“创建人”是具体的用户名,上传和存储的时候都是用户ID,如何通过用户ID查找用户具体信息:</p>
<pre><code>// 更新文件:server/control/approves.js
...
async function list (ctx) {
...
try {
const approves = await approveModel.find({
author: ctx.state.auth.id
}).populate('author');
ctx.body = {
code: '200',
data: approves,
msg: '查询成功'
};
}
...
}
...
async function get (ctx) {
const { id } = ctx.params;
try {
const approve = await approveModel.findOne({
_id: id
}).populate({
path: 'author',
select: '+avatar +alias +telephone +email +department +job +role +_id +__v'
}).exec();
...
}</code></pre>
<ul><li>使用<code>populate</code>与<code>server/model/approve.js</code>文件中的</li></ul>
<pre><code>author: {
type: Schema.Types.ObjectId,
ref: 'User',
required: true
},</code></pre>
<p>对应使用,可查出用户信息。<br>测试结果:[动态太大,传不上来]<br><img src="/img/bVbEvsq" alt="image.png" title="image.png"></p>
<p>发现,描述没有显示出来,这是因为<code>description</code>没有被查出,修改<code>server/control/approves.js</code>中<code>list</code>部分代码<code>.populate('author').select('+description');</code></p>
<h2>参考文档</h2>
<p><a href="https://link.segmentfault.com/?enc=hFqYONzCctBIXHneK2Zo5Q%3D%3D.9CY%2B3BnvP%2FJNzP6aL%2FH9qrla2wlbvQDq0TYzFB0gFm9TELLl3GoNdseWu2hMbmxV" rel="nofollow">一对多关系</a></p>
Koa & Mongoose & Vue实现前后端分离--11更新用户头像
https://segmentfault.com/a/1190000021996824
2020-03-15T00:06:06+08:00
2020-03-15T00:06:06+08:00
米花儿团儿
https://segmentfault.com/u/mihuartuanr
0
<h2>上节回顾</h2>
<ul><li>更新用户文本数据</li></ul>
<h2>工作内容</h2>
<ul>
<li>更新用户数据</li>
<li>图片上传 & 存储 & 静态化访问</li>
</ul>
<h2>准备工作</h2>
<ul><li>
<code>npm i -S koa-static</code> // 先切换到<code>/server</code>目录下</li></ul>
<h2>业务逻辑</h2>
<h3>服务端文件路由配置</h3>
<p>服务端拦截路由请求:</p>
<pre><code>// 新建文件:server/router/assets.js
const Router = require('@koa/router');
const controls = require('../control/assets');
const routerUtils = require('../utils/router');
const { upload } = controls;
const router = new Router({
prefix: '/assets'
});
const routes = [
{
path: '/:category/:id',
method: 'POST',
handle: upload
}
]
routerUtils.register.call(router, routes);
module.exports = router;</code></pre>
<pre><code>// 新建文件:server/control/assets.js
async function upload (ctx, next) {
console.log('--------upload=======')
}
module.exports = {
upload,
}</code></pre>
<h3>
<code>koa-body</code>支持</h3>
<pre><code>// 更新文件:server/app.js
...
const path = require('path');
...
app.use(bodyParser({
multipart: true, //支持文件数据
formidable: {
uploadDir: path.resolve(__dirname, './public/temp'), // 图片存储位置
keepExtensions: true
}
}));
...</code></pre>
<ul>
<li>
<code>multipart: true</code>支持文件数据,这里以<code>key:value</code>的形式获取。</li>
<li>
<code>formidable.uploadDir</code>文件上传存储位置,若不设置,默认存储到计算机用户目录的缓存位置,最好设置一个可控位置。</li>
<li>
<code>keepExtensions:true</code>是否保有后缀名,默认不存。</li>
</ul>
<h3>
<code>Postman</code>测试</h3>
<ul>
<li>前期,登录时,已经设置全局变量<code>token</code>
</li>
<li>
<code>Body</code> --> <code>form-data</code> --> <code>File</code> --> <code>Select Files</code>
</li>
</ul>
<p><img src="/img/bVbEscW" alt="upload.gif" title="upload.gif"></p>
<p>请求结果:没有任何返回。<br><code>vs code</code>的调试控制台反馈:<br><img src="/img/bVbEsdt" alt="error.png" title="error.png"><br>设置存储文件的路径不存在。</p>
<h3>保存文件</h3>
<p>存储路径不存在,需要检测文件目录是否存在,若不存在,则新建。</p>
<pre><code>// 新建文件:server/utils/dir.js
const path = require('path');
const fs = require('fs');
function checkDirExist(dirname) {
if (fs.existsSync(dirname)) {
return true;
} else {
if (checkDirExist(path.dirname(dirname))) {
fs.mkdirSync(dirname); //递归
return true;
}
}
}
module.exports = {
checkDirExist,
}</code></pre>
<h4>方式一、通过<code>koa-body</code>配置</h4>
<pre><code>// 更新文件:
...
const { checkDirExist } = require('./utils/dir');
const fileTempDir = path.resolve(__dirname, './public/temp');
...
app.use(bodyParser({
multipart: true,
formidable: {
uploadDir: fileTempDir,
keepExtensions: true,
onFileBegin(key, file) { // 利用钩子函数
checkDirExist(fileTempDir);
//file.path= path.resolve(fileTempDir, file.name) // 文件改名
}
}
}));</code></pre>
<ul><li>利用<code>koa-body</code>配置<code>onFileBegin</code>,可以在处理文件之前,进行一些操作,如:测试目录是否存在、改名、改存储路径等。</li></ul>
<p>测试结果:<br><img src="/img/bVbEsfo" alt="filename" title="filename"></p>
<h4>方式二、<code>fs</code>模块读写文件</h4>
<p>那,为什么还要第二种方式呢?<br>利用<code>koa-body</code>配置<code>onFileBegin</code>改名,只能获取到<code>file</code>数据本身的数据信息,而无法获取<code>ctx</code>的上下文信息(如,这里准备根据请求参数创建目录存储文件)。</p>
<pre><code>// 更新文件:server/control/assets.js
const fs = require('fs');
const path = require('path');
const { checkDirExist } = require('../utils/dir');
async function upload (ctx, next) {
const file = Object.values(ctx.request.files)[0];
const { category, id } = ctx.params;
const filePath = file.path;
// 最终要保存到的文件夹路径
const dir = path.join(__dirname,`../public/${category}/${id}/`);
try {
// 检查文件夹是否存在——>如果不存在,则新建文件夹
checkDirExist(dir);
const reader = fs.createReadStream(filePath);
const writer = fs.createWriteStream(path.resolve(dir, file.name));
reader.pipe(writer);
// 删除缓存文件
fs.unlinkSync(filePath)
} catch (err) {
}
}
module.exports = {
upload
}</code></pre>
<ul><li>这里,根据上传路由的参数,将文件存到用户ID创建的<code>avatar</code>目录下。</li></ul>
<p><code>Postman</code>测试结果:<br><img src="/img/bVbEsg0" alt="avatar" title="avatar"></p>
<h3>访问文件</h3>
<p>这时候访问文件:<br><img src="/img/bVbEshm" alt="error" title="error"><br>所以,需要将<code>public</code>目录添加到身份认证的<code>unless</code>白名单中:</p>
<pre><code>// 更新文件:server/app.js
...
custom: function(ctx) {
const { method, path, query } = ctx;
if(path === '/'){
return true;
}
if(/^\\/public/.test(path)) { // public目录
return true;
}
if(path === '/users' && query.action) {
return true;
}
return false;
}
...</code></pre>
<p>继续访问:<br><img src="/img/bVbEshz" alt="image.png" title="image.png"></p>
<p>这是因为默认会请求<strong>动态资源</strong>,而图片数据<strong>静态资源</strong></p>
<pre><code>// 更新文件:
...
const koaStatic = require('koa-static');
...
// 中间件:指定静态资源路径 vs. 使其与动态资源分离
app.use(koaStatic(path.join(__dirname, 'public/')))</code></pre>
<p>继续访问,报错如上:<br><img src="/img/bVbEshz" alt="image.png" title="image.png"></p>
<p>这是因为<strong>访问路径错了</strong>:访问路径不用带<code>/public</code><br><img src="/img/bVbEshU" alt="dir.gif" title="dir.gif"></p>
<p>若怀疑,不加<code>koa-static</code>,仅修改路径即可访问的可以自行试试。</p>
<h3>服务端更新用户头像逻辑</h3>
<p>上述内容中,修改了<code>upload</code>的业务逻辑,仅涉及到将文件保存到指定路径下,却没有去更新用户头像信息,并且,服务端没有返回数据。</p>
<pre><code>// 更新文件:server/control/assets.js
const fs = require('fs');
const path = require('path');
const userModel = require('../model/user');
const { checkDirExist } = require('../utils/dir');
async function upload (ctx, next) {
const file = Object.values(ctx.request.files)[0];
const { category, id } = ctx.params;
// 用户头像远程地址
const remotePath = `${ctx.origin}/${category}/${id}/${file.name}`;
const filePath = file.path;
const dir = path.join(__dirname,`../public/${category}/${id}/`);
try {
checkDirExist(dir);
const reader = fs.createReadStream(filePath);
const writer = fs.createWriteStream(path.resolve(dir, file.name));
reader.pipe(writer);
try {
// 更新用户头像信息
await userModel.updateOne(
{
_id: id
},
{
avatar: remotePath
}
).exec();
} catch (err) {
ctx.body = {
code: '404',
data: null,
msg: '上传失败'
};
return;
}
fs.unlinkSync(filePath)
ctx.body = {
code: '200',
data: {
filePath: remotePath
},
msg: '上传成功'
}
} catch (err) {
ctx.body = {
code: '404',
data: null,
msg: '上传失败'
}
}
}
module.exports = {
upload
}</code></pre>
<ul><li>设置远程访问地址<code>${ctx.origin}/${category}/${id}/${file.name}</code>,并将该地址更新到用户信息中。</li></ul>
<p>这里将<code>/server/public</code>目录删除,重新测试:<br><img src="/img/bVbEskm" alt="sucess.gif" title="sucess.gif"></p>
<h3>前端页面头像逻辑</h3>
<pre><code>//更新文件:
...
methods: {
...
handleAvatarSuccess (res, file) {
this.imageUrl = URL.createObjectURL(file.raw)
},
beforeAvatarUpload (file) {
const isJPG = /^image\//.test(file.type)
const isLt2M = file.size / 1024 / 1024 / 10 < 2
if (!isJPG) {
this.$message.error('上传头像图片只能是 JPG 格式!')
}
if (!isLt2M) {
this.$message.error('上传头像图片大小不能超过 20MB!')
}
return isJPG && isLt2M
},
...
data () {
return {
uploadForm: {
action: `//localhost:3000/assets/avatars/${this.$store.state.loginer.id}`,
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`
},
multiple: false,
'show-file-list': false,
'on-success': this.handleAvatarSuccess,
'before-upload': this.beforeAvatarUpload
},
...
async created () {
const res = await http.get(`/users/${this.userId}`)
if (res.code === '200') {
this.loginer = res.data
this.dialogForm.form = {...res.data}
this.imageUrl = res.data.avatar //初始化时,赋值
} else {
this.$message({
type: 'error',
message: '获取用户信息失败'
})
}
}</code></pre>
<ul>
<li>初始化时,给头像赋值;</li>
<li>通过<code>uploadForm</code>设置上传配置</li>
<li>
<code>on-success</code>上传成功时,将图片地址覆盖原有值;</li>
<li>
<code>before-upload</code>上传之前,校验文件类型和大小;</li>
</ul>
<p>效果展示:<br><img src="/img/bVbEsxA" alt="fronEnd.gif" title="fronEnd.gif"></p>
<h2>参考文档</h2>
<p><a href="https://link.segmentfault.com/?enc=%2FUfwr13fLoy%2Bd3b%2Fqn0qmg%3D%3D.IQiKNkDbo7MRd9N3sc42YnQiVUyHMz2L%2BjSwG8YoMnhWm7Bxs2wsy7C63H9Ah3V9" rel="nofollow">koa-static</a><br><a href="https://link.segmentfault.com/?enc=EqWQCGTUqc6Syyl9LAEx%2FA%3D%3D.5S65UIMvlsixBw8hokKvP9G9KWQUn5bDmRJhl3Ts93KjN7AeEYcdr%2Bg4M%2BcMa8Nlaj0vu32no209bKjzvu%2FFtQ%3D%3D" rel="nofollow">koa-body上传文件相关配置</a></p>
Koa & Mongoose & Vue实现前后端分离--10更新用户信息
https://segmentfault.com/a/1190000021978182
2020-03-14T00:04:12+08:00
2020-03-14T00:04:12+08:00
米花儿团儿
https://segmentfault.com/u/mihuartuanr
0
<h2>上节回顾</h2>
<ul><li>
<code>JWT</code>相关</li></ul>
<h2>工作内容</h2>
<ul><li>更新用户数据</li></ul>
<h2>准备工作</h2>
<p>无</p>
<h2>业务逻辑</h2>
<h3>页面调用用户信息</h3>
<pre><code>//更新文件:client/src/views/personal-panel/index.vue
...
<script>
import http from '@/utils/http'
export default {
data () {
return {
loginer: {}
}
},
async created () {
const { id } = this.$store.state.loginer
const res = await http.get(`/users/${id}`)
this.loginer = res.data
}
}
</script></code></pre>
<p>在登录的时候,已经将登录用户信息存储到<code>vuex</code>中,通过用户ID查询用户详细信息。</p>
<h3>服务端查询用户服务</h3>
<pre><code>// 更新文件: server/router/users.js
...
const { list, get, register, login, update } = require('../control/users');
...
{
path: '/:id',
method: 'GET',
handle: get
},
...</code></pre>
<pre><code>// 更新文件:server/control/users.js
async function get (ctx) {
const { id } = ctx.params;
try {
const user = await userModel.findOne({
_id: id
}).select('+telephone +email'); //新增查询除默认外的字段
if (user) {
ctx.body = {
code: '200',
data: user,
msg: '成功'
}
return;
}
throw('请核对参数')
} catch (err) {
ctx.body = {
code: '403',
data: null,
msg: err.message
}
}
}
...
module.exports = {
...
get,
...
}</code></pre>
<ul>
<li>其中,<code>userModel.findOne({_id: id})</code>默认查出的是<code>Schema</code>没有设置<code>select: false</code>的字段,如果需要其它字段的话,需要<code>userModel.findOne({_id: id}).select('+telephone +email');</code>新增字段</li>
<li>通过<code>ctx.params</code>获取匹配路由<code>:id</code>参数。</li>
</ul>
<h3>前端展示布局</h3>
<pre><code>// 更新文件:client/src/views/personal-panel/index.vue
<template>
<div class="panel-container">
<div class="panel-avatar">
<el-upload
class="avatar-uploader"
v-bind="uploadForm">
<img v-if="imageUrl" :src="imageUrl" class="avatar">
<i v-else class="el-icon-user"></i>
</el-upload>
</div>
<div class="panel-main">
<h3 class="panel-title">{{loginer.alias || loginer.account}}</h3>
<ul class="panel-content">
<li class="panel-item">
<label class="panel-item-lanel">帐号:</label>
<span class="panel-item-value">{{loginer.account}}</span>
</li>
<li class="panel-item">
<label class="panel-item-lanel">邮箱:</label>
<span class="panel-item-value">{{loginer.email}}</span>
</li>
<li class="panel-item">
<label class="panel-item-lanel">电话:</label>
<span class="panel-item-value">{{loginer.telephone}}</span>
</li>
</ul>
<div class="panel-controls">
<el-button class="btn-edit" type="default" @click="openFormDialog">编辑</el-button>
</div>
</div>
</div>
</template>
<script>
import http from '@/utils/http'
export default {
methods: {
openFormDialog () {
}
},
data () {
return {
uploadForm: {
action: ''
},
imageUrl: '',
loginer: {}
}
},
async created () {
const { id } = this.$store.state.loginer
const res = await http.get(`/users/${id}`)
this.loginer = res.data
}
}
</script>
<style lang="scss" scoped>
@import '~@/stylesheets/layout.scss';
@import './index.scss';
</style></code></pre>
<pre><code>// 新建文件:client/src/views/personal-panel/index.scss
.panel-container {
@include flex($item: flex-start);
.panel-avatar {
width: 160px;
}
.panel-main {
flex: 1;
color: #525975;
line-height: 2;
padding: 0 40px;
font-size: 14px;
.panel-title {
color: #2e3242;
font-size: 24px;
}
}
.panel-controls {
margin-top: 20px;
}
}
.avatar-uploader{
/deep/ {
.el-upload {
border: 1px dashed #d9d9d9;
border-radius: 100px;
cursor: pointer;
position: relative;
overflow: hidden;
}
.el-upload:hover {
border-color: #409EFF;
}
.el-icon-user {
font-size: 70px;
color: #8c939d;
width: 160px;
height: 160px;
line-height: 160px;
text-align: center;
}
.avatar {
width: 160px;
height: 160px;
display: block;
}
}
}</code></pre>
<p>展示效果<br><img src="/img/bVbEnuC" alt="image.png" title="image.png"></p>
<h3>前端编辑用户信息</h3>
<pre><code>// 更新文件:client/src/views/personal-panel/index.vue
<template>
<div class="panel-container">
<div class="panel-avatar">
<el-upload
class="avatar-uploader"
v-bind="uploadForm">
<img v-if="imageUrl" :src="imageUrl" class="avatar">
<i v-else class="el-icon-user"></i>
</el-upload>
</div>
<div class="panel-main">
<h3 class="panel-title">{{loginer.alias || loginer.account}}</h3>
<ul class="panel-content">
<li class="panel-item">
<label class="panel-item-lanel">帐号:</label>
<span class="panel-item-value">{{loginer.account}}</span>
</li>
<li class="panel-item">
<label class="panel-item-lanel">邮箱:</label>
<span class="panel-item-value">{{loginer.email}}</span>
</li>
<li class="panel-item">
<label class="panel-item-lanel">电话:</label>
<span class="panel-item-value">{{loginer.telephone}}</span>
</li>
</ul>
<div class="panel-controls">
<el-button class="btn-edit" type="default" @click="openFormDialog">编辑</el-button>
</div>
</div>
<el-dialog :title="dialogForm.title" :visible.sync="dialogForm.visible">
<el-form :ref="dialogForm.formRef" :model="dialogForm.form" :rules="dialogForm.rules">
<el-form-item label="帐号" prop="account">
<el-input v-model="dialogForm.form.account"></el-input>
</el-form-item>
<el-form-item label="昵称" prop="alias">
<el-input v-model="dialogForm.form.alias"></el-input>
</el-form-item>
<el-form-item label="邮箱" prop="email">
<el-input type="email" v-model="dialogForm.form.email"></el-input>
</el-form-item>
<el-form-item label="电话" prop="telephone">
<el-input type="telephone" v-model="dialogForm.form.telephone"></el-input>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogForm.visible = false">取 消</el-button>
<el-button type="primary" @click="submitForm">确 定</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import http from '@/utils/http'
export default {
methods: {
openFormDialog () {
this.dialogForm.visible = true
},
async submitForm () {
try {
const valid = await this.$refs[this.dialogForm.formRef].validate()
if (valid) {
const res = await http.patch(
`/users/${this.userId}`,
{
...this.dialogForm.form
}
)
if (res.code === '200') {
const { account, alias } = res.data
this.$store.commit(
'putLoginer',
{
account,
alias
}
) // 更新vuex状态
this.loginer = Object.assign(
{},
this.loginer,
{
...res.data
}
) // 更新展示的默认值
this.$set(this.dialogForm, 'form', Object.assign(
{},
this.dialogForm.form,
{
...res.data
}
)) // 更新弹窗默认值
this.dialogForm.visible = false
} else {
this.$message({
type: 'error',
message: res.msg
})
}
}
} catch (err) {
console.error(err)
}
}
},
data () {
return {
uploadForm: {
action: ''
},
imageUrl: '',
loginer: {},
dialogForm: {
visible: false,
title: '编辑',
formRef: 'dialogForm',
form: {
account: '',
alias: '',
email: '',
telephone: ''
},
rules: {
account: [
{ required: true, message: '请输入帐号', trigger: 'blur' },
{ min: 5, message: '长度至少5个字符', trigger: 'blur' }
],
alias: [
{ required: true, message: '请输入昵称', trigger: 'blur' },
{ min: 1, max: 15, message: '长度不能超过十五个字符', trigger: 'blur'}
],
email: [
{ type: 'email', required: true, message: '请输入邮箱', trigger: 'blur' }
],
telephone: [
{ required: true, message: '请输入手机号码', trigger: 'blur' },
{
validator: (rule, value, cb) => {
if (/^1[0-9]{10}$/.test(value)) {
cb()
} else {
cb(new Error('手机号码格式错误'))
}
},
trigger: 'blur'
}
]
}
}
}
},
computed: {
userId () {
return this.$store.state.loginer.id
}
},
async created () {
// 调用完初始化接口时,需要赋展示初始值
const res = await http.get(`/users/${this.userId}`)
if (res.code === '200') {
this.loginer = res.data
this.dialogForm.form = {...res.data}
this.imageUrl = res.data.avatar
} else {
this.$message({
type: 'error',
message: '获取用户信息失败'
})
}
}
}
</script></code></pre>
<ul>
<li>
<code>get</code>方法调用<code>/users/:id</code>接口后,需要给用户设初始值。</li>
<li>
<code>patch</code>方法调用<code>/users/:id</code>接口,更新用户信息后,要更新<code>vuex</code>存储的状态,更新展示的用户信息,更新弹窗初始值。</li>
</ul>
<p>展示效果:<br><img src="/img/bVbEnBX" alt="image.png" title="image.png"></p>
<h3>服务端更新用户服务</h3>
<pre><code>// 更新文件:server/control/users.js
...
async function update (ctx) {
const { id } = ctx.params;
const payload = ctx.request.body;
try {
await userModel.updateOne(
{
_id: id
},
{
...payload
}
);
ctx.body = {
code: '200',
data: {
id,
...payload
},
msg: '更新成功'
}
} catch (err) {
ctx.body = {
code: '403',
data: null,
msg: '请核查参数'
}
}
}
...</code></pre>
<p>测试结果:<br><img src="/img/bVbEnIx" alt="update.gif" title="update.gif"></p>
<h2>参考文档</h2>
<p><a href="https://link.segmentfault.com/?enc=YKN2vqL5eNVXD6K8O9q%2Fuw%3D%3D.qs761P4uD0bymzTdnViS8FR8BVjCDUN632VQRh%2FUH1Uq%2FoRToLRmkv4MaYXZsecN" rel="nofollow">Model操作</a></p>
Koa & Mongoose & Vue实现前后端分离--09身份验证JWT&测试
https://segmentfault.com/a/1190000021969629
2020-03-13T00:07:01+08:00
2020-03-13T00:07:01+08:00
米花儿团儿
https://segmentfault.com/u/mihuartuanr
1
<h2>上节回顾</h2>
<ul>
<li>嵌套路由</li>
<li>页面布局</li>
<li>状态管理 & 持久化</li>
</ul>
<h2>工作内容</h2>
<ul><li>身份认证</li></ul>
<h2>准备工作</h2>
<ul>
<li>
<code>npm i -S crypto-js</code> // 先切换到<code>/server</code>目录下</li>
<li>
<code>npm i -S koa-jwt</code> // 先切换到<code>/server</code>目录下</li>
<li>
<code>npm i -S jsonwebtoken</code> // 先切换到<code>/server</code>目录下</li>
</ul>
<h2>业务逻辑</h2>
<h3>JWT简介</h3>
<p><code>JWT</code>对象为一个长字串,字符之间通过"."分隔符分为三个子串。<br><code>JWT</code>的三个部分:JWT头、有效载荷和签名。<br>一旦<code>JWT</code>签发,在有效期内将会一直有效。</p>
<ul>
<li>
<p><code>JWT</code>使用的核心步骤</p>
<ul>
<li>签名</li>
<li>认证</li>
<li>解码</li>
</ul>
</li>
<li>这里使用<code>jsonwebtoken</code>对载荷进行签名,<code>jsonwebtoken</code>结合<code>koa-jwt</code>进行签名认证,最终得到签名前的数据<code>ctx.state[<key>]</code>
</li>
</ul>
<h3>工具库:签名</h3>
<pre><code>// 新建文件:/server/config/auth.js
const AES = require("crypto-js/aes");
const secretText = 'jwt.secret.text';
const key = 'jwt.secret.key'
// // Encrypt
// var ciphertext = CryptoJS.AES.encrypt('my message', 'secret key 123').toString();
// // Decrypt
// var bytes = CryptoJS.AES.decrypt(ciphertext, 'secret key 123');
// var originalText = bytes.toString(CryptoJS.enc.Utf8);
// console.log(originalText); // 'my message'
module.exports = {
secret: AES.encrypt(secretText, key).toString(),
authKey: 'auth'
}</code></pre>
<p>可以通过<code>ctx.state[<authKey>]</code>,即<code>ctx.state.auth</code>获取有效荷载。</p>
<pre><code>// 新建文件:server/utils/auth.js
const jwt = require('jsonwebtoken')
const { secret } = require('../config/auth')
// 定义超时时间:token的有效时长
const expiresIn = '2h';
module.exports = {
sign: function(payload) {
// 推荐对payload进行加密。
const token = jwt.sign(payload, secret, {
expiresIn
});
return token;
},
vertify: function(ctx, decodeToken, token){
}
}</code></pre>
<h3>工具库:认证</h3>
<pre><code>// 更新文件:server/utils/auth.js
...
vertify: function(ctx, decodeToken, token){
let result = true;
try{
jwt.verify(token, secret);
result = false;
}catch(e) {
}
return result;
}
...</code></pre>
<p><code>vertify</code>返回<code>true</code>表明<code>token</code>已无效或认证失败;<br><code>vertify</code>返回<code>false</code>表明<code>token</code>仍有效,且已经成功;</p>
<h3>解析</h3>
<pre><code>//更新文件:server/app.js
...
const koaJwt = require('koa-jwt');
const { secret, authKey } = require('./config/auth');
const { vertify } = require('./utils/auth');
...
app.use(koaJwt({ //要放到路由前边,否则,无效
secret,
key: authKey,
// jwt是否被废除
isRevoked: vertify
}))
...</code></pre>
<ul>
<li><strong>JWT认证必须放到路由前方,否则,路由逻辑都走完了,再进行认证,有什么用</strong></li>
<li>
<code>koa-jwt</code>帮助实现认证逻辑,认证失败,抛出错误。</li>
<li>
<code>koa-jwt</code>类似<code>koa-body</code>,将有效载荷解析,存储到<code>ctx.state[<authKey>]</code>(<code>ctx.state.auth</code>)中。</li>
</ul>
<h3>服务端登录返回<code>token</code>
</h3>
<pre><code>// 更新文件:
...
const auth = require('../utils/auth');
...
async function login (ctx) {
...
if(user) {
const token = auth.sign({
id: user.id,
account
})
ctx.body = {
code: '200',
data: {
token,
id: user.id,
account,
alias: user.alias
},
msg: '登陆成功'
}
}
...
}</code></pre>
<ul><li>
<code>Postman</code>测试结果</li></ul>
<p><img src="/img/bVbEkJj" alt="NotFound" title="NotFound"></p>
<ul><li>
<code>vs code</code> 调试控制台</li></ul>
<p><img src="/img/bVbEkJr" alt="AuthenticationError" title="AuthenticationError"></p>
<h3>异常处理</h3>
<p>上一步认证失败,抛出了<code>ERROR</code><br><img src="/img/bVbEkKL" alt="monitor" title="monitor"><br>通过断点可以查看到错误信息,补充异常逻辑<br>官方推荐通过<code>status</code>判断是否是认证错误<br><img src="/img/bVbEkLX" alt="remmand" title="remmand"></p>
<pre><code>// 更新文件:server/app.js
···
// 中间件的错误处理
app.use(function(ctx, next){
return next().catch((err) => {
console.log(err)
if (err.name === 'ValidationError') {
ctx.body = {
code: '403',
data: null,
msg: err.message
}
} else if (401 == err.status) { //认证错误
ctx.status = 401; // 更新HTTP Response Code
ctx.body = {
code: '401',
data: null,
msg: `${err.message}\n请先登陆`
}
} else {
throw err;
}
});
});
···</code></pre>
<ul><li>
<code>Postman</code>继续测试</li></ul>
<p><img src="/img/bVbEkMY" alt="401" title="401"></p>
<h3>白名单</h3>
<p>登录还需要认证?答案是不需要的。<br>不仅仅是登录,注册及其后续的静态图片访问,都不需要认证。</p>
<pre><code>// 更新文件:server/app.js
secret,
key: authKey,
// jwt是否被废除
isRevoked: vertify
}).unless({
// 返回true就是忽略认证
custom: function(ctx) {
const { method, path, query } = ctx;
if(path === '/'){
return true;
}
if(path === '/users' && query.action) {
return true;
}
return false;
}
}));</code></pre>
<ul>
<li>链式调用<code>unless</code>,返回<code>true</code>即为忽略认证。</li>
<li>
<code>unless</code>的具体用法同<a href="https://link.segmentfault.com/?enc=kk58y%2BFcUxcUSqJrVwnjqg%3D%3D.6j6vXsx2ujE4QGA7KNf9hyrOh%2FhXqVg99nnltrChANs9NHPEvdpIKlTp%2BSO1MMXr" rel="nofollow"><code>koa-unless</code></a>,这里使用的<code>custom</code>是自定义忽略规则。</li>
<li>
<code>Postman</code>继续测试</li>
</ul>
<p><img src="/img/bVbEkOX" alt="success" title="success"><br>成功,完美。</p>
<h3>
<code>Postman</code>测试身份认证</h3>
<p>然而,用<code>Postman</code>测试其它接口:<br><img src="/img/bVbEkPl" alt="Authentication" title="Authentication"><br>这是因为没有在请求头<code>Headers</code>里加上认证信息<code>Authorization</code>。<br><img src="/img/bVbElgj" alt="image.png" title="image.png"><br>在登录接口的<code>Tests</code>面板,借助右边的提示,设置全局变量<code>token</code>为登录返回数据的<code>token</code>。</p>
<pre><code>// 更新Tests面板内容:
pm.test("Your test name", function () {
var jsonData = pm.response.json();
pm.globals.set("token", jsonData.data.token);
});</code></pre>
<p><img src="/img/bVbElfl" alt="image.png" title="image.png"><br>在需要身份认证的测试接口的<code>Authorization</code>面板中,选择<code>Type</code>为<code>Bearer Token</code>,值为变量<code>{{token}}</code>。</p>
<h3>前端请求拦截</h3>
<p>到目前为止,接口没有什么问题了,但,前端页面请求,没有添加<code>Authorization</code>。</p>
<pre><code>// 更新文件:client/src/views/login/index.vue
...
async function onLogin () {
...
if (res && res.code === '200') {
const {token, ...user} = res.data // 新增
localStorage.setItem('token', res.data.token) // 新增
this.$store.commit('putLoginer', user)
this.$router.replace('/home')
}
...
}
...</code></pre>
<p>在登录时,获取<code>token</code>,进行本地存储。</p>
<pre><code>// 更新文件:client/src/utils/http.js
...
instance.interceptors.request.use(async (config) => {
const token = await localStorage.getItem('token')
token && (config.headers['Authorization'] = `Bearer ${token}`)
return config
}, function (error) {
console.log('------request===========', error)
// Do something with request error
return Promise.reject(error)
})
...</code></pre>
<ul>
<li>拦截请求,<code>token</code>存在时,将<code>token</code>赋值给<code>config.headers['Authorization']</code>。</li>
<li>赋值是以<code>Bearer </code>为前缀的,这是规范,要求这样处理。</li>
</ul>
<pre><code>// 仅为了测试身份认证,测试完,就把该文件还原。
// 更新文件:client/src/views/homePage/index.vue
...
<script>
import http from '@/utils/http'
export default {
async created () {
const res = await http.get('/users')
console.log(res)
}
}
</script>
...</code></pre>
<p>测试结果:<br><img src="/img/bVbEljI" alt="auth.gif" title="auth.gif"></p>
<h3>前端响应拦截</h3>
<p>前端认证失败,就退回到登录<code>/</code>页面,清空本地存储和<code>vuex</code>。(<strong>登出时,也是这些步骤,登出逻辑以前已经处理过了</strong>)</p>
<pre><code>// 更新文件:client/src/utils/http.js
...
import router from '@/router'
import store from '@/store'
...
instance.interceptors.response.use(
async res => {
if (/^20./.test(res.status)) {
return res.data
}
if (/^40./.test(res.status)) {
router.push('/')
await localStorage.clear()
store.commit('resetVuex')
return {
code: '401',
msg: '请重新登陆'
}
}
console.log('------response=======', res)
return res
},
error => {
return Promise.reject(error)
}
)</code></pre>
<p>测试结果如下:退回到登录<code>/</code>页面,清空本地存储和<code>vuex</code><br><img src="/img/bVbEll8" alt="logout.gif" title="logout.gif"></p>
<p>至于为什么调用两次<code>/users</code>接口,因为<code>/home</code>下认证一次,<code>/login</code>调用了一次。</p>
<p>这里改进一下<code>/login</code>的调用,让如果有<code>token</code>的时候,认证成功,直接进入首页。(偷懒:专门做一个认证接口比较好)</p>
<pre><code>// 更新文件:client/src/views/login/index.vue
...
async created () {
const res = await http.get('/users')
if (res.code === '200') {
this.$router.replace('/home')
}
}
...</code></pre>
<h2>参考文档</h2>
<p><a href="https://link.segmentfault.com/?enc=ttZf1hxeB%2FDBMI3a3miYUg%3D%3D.npF8EhwNSRVAzUoaI3JbENjj2dAVRaotLMTSBrbbrDw%3D" rel="nofollow">koa-jwt</a><br><a href="https://link.segmentfault.com/?enc=4H41MInmjgVVmm%2FzxGMwJg%3D%3D.o3kmPF6nvhq9ZzTPXN4PwAHzdchGzMMUHBvHI8hlnDiOd%2FngjTs2abosnHuhxP8P%2Fx4zcwQYm5CEfuaDaT8mfg%3D%3D" rel="nofollow">jsonwebtoken</a></p>
Koa & Mongoose & Vue实现前后端分离--08前端状态管理&路由嵌套
https://segmentfault.com/a/1190000021956708
2020-03-12T00:16:40+08:00
2020-03-12T00:16:40+08:00
米花儿团儿
https://segmentfault.com/u/mihuartuanr
0
<h2>上节回顾</h2>
<ul>
<li>密码加密</li>
<li>后端参数校验</li>
<li>中间件的使用 & 错误处理</li>
</ul>
<h2>工作内容</h2>
<ul>
<li>
<code>vuex</code>的简单使用</li>
<li>
<code>vue-router</code>嵌套路由</li>
<li>
<code>vuex</code>本地持久化</li>
</ul>
<h2>准备工作</h2>
<ul>
<li>
<code>npm install vuex --save</code> //先切换到<code>/client</code>目录下</li>
<li>
<code>npm install --save vuex-persist</code> //先切换到<code>/client</code>目录下</li>
</ul>
<h2>布局分析</h2>
<h3>期望布局</h3>
<p><img src="/img/bVbEhEV" alt="home" title="home"><br><img src="/img/bVbEhE3" alt="backstage" title="backstage"></p>
<p><em>自己能实现布局的同学可以掠过"布局"这部分内容,几乎全是贴的代码。</em></p>
<h3>布局分析</h3>
<p>首页与登录页面没有公用结构,考虑使用同级路由。<br>首页为上下结构,可以考虑<code>el-container</code>、<code>el-header</code>、<code>el-main</code>布局。<br><img src="/img/bVbEhFq" alt="layout" title="layout"><br>配置页面与首页公用导航栏,只是将<code>el-main</code>内分左右两部分,考虑使用嵌套路由。<br>配置页面将<code>el-main</code>分左右两部分,考虑<code>el-container</code>、<code>el-aside</code>、<code>el-main</code>布局。<br><img src="/img/bVbEhFV" alt="asideMain" title="asideMain"></p>
<h3>嵌套路由</h3>
<pre><code>// 更新文件:client/src/router/index.js
import Vue from 'vue'
import Router from 'vue-router'
import Login from '@/views/login'
import Layout from '@/views/layout'
import HomePage from '@/views/homepage'
import BackStage from '@/views/backstage'
import PersonalPanel from '@/views/personal-panel'
import ApprovePanel from '@/views/approve-panel'
Vue.use(Router)
export default new Router({
routes: [
{
path: '/',
redirect: '/login'
},
{
path: '/login',
name: 'Login',
component: Login
},
{
path: '/home',
name: 'Layout',
component: Layout,
children: [ // children决定子组件渲染位置(替换父组件中的<router-view>)
{
path: '',
name: 'HomePage',
component: HomePage
},
{
path: '/backstage', // 绝对路径,决定跳转路径计算方式(不带前缀)
name: 'BackStage',
component: BackStage,
children: [
{
path: '',
name: 'personalPanel',
component: PersonalPanel
},
{
path: 'person', //相对路径,决定跳转路径计算方式(以父组件path为前缀)
redirect: '/backstage'
},
{
path: 'approve',
name: 'approvePanel',
component: ApprovePanel
}
]
}
]
}
]
}</code></pre>
<ul>
<li>
<p><code>children</code>控制组件的渲染的层级位置</p>
<ul>
<li>匹配哪一个<code>router-view</code>
</li>
<li>匹配路由配置父级组件中的<code>router-view</code>
</li>
</ul>
</li>
<li>
<p><code>path</code>控制路由跳转的路径</p>
<ul>
<li>绝对路径:直接以<code>path</code>的值为跳转路径</li>
<li>相对路径:需要加父级组件配置的<code>path</code>为前缀,才能作为跳转路径</li>
</ul>
</li>
</ul>
<h3>页面设计</h3>
<pre><code>//新建样式重置文件:client/src/stylesheets/reset.css
// 篇幅有点大,网上找一份就好
...</code></pre>
<pre><code>//更新文件:client/src/main.js
...
import '@/stylesheets/reset.css'
...</code></pre>
<pre><code>//新建样式文件:client/src/stylesheets/layout.scss
@mixin flex($dir: row, $content: flex-start, $item: flex-start, $wrap: wrap) {
display: flex;
flex-direction: $dir;
justify-content: $content;
align-items: $item;
flex-wrap: $wrap;
}</code></pre>
<pre><code>// 新建`/home`匹配的Layout组件:client/src/views/layout/index.vue
<template>
<el-container class="page-container">
<el-header class="page-header">
<div class="page-brand" @click="backToHome">
<figure class="page-logo-wrap">
<img class="page-logo" src="../../assets/logo.png" alt="">
</figure>
<h2 class="page-title">Vue</h2>
</div>
<div class="page-controls-wrap">
<el-dropdown @command="handleDropdown">
<span class="el-dropdown-link">
<i class="el-icon-setting"></i>
</span>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item disabled>用户信息</el-dropdown-item>
<el-dropdown-item command='backstage'>后台管理</el-dropdown-item>
<el-dropdown-item command='logout'>登出</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
</el-header>
<el-main class="page-main">
<router-view/>
</el-main>
</el-container>
</template>
<script>
export default {
methods: {
backToHome () {
this.$router.push({
path: '/home'
})
},
async logout () {
this.$router.push('/')
},
navigateToBackstage () {
this.$router.push({
path: '/backstage'
})
},
handleDropdown (command) {
this['dropdownStrategies'][command]()
}
},
data () {
return {
activeIndex: '1',
dropdownStrategies: {
'backstage': this.navigateToBackstage,
'logout': this.logout
}
}
}
}
</script>
<style lang="scss" scoped>
@import '~@/stylesheets/layout.scss';
.page-container {
height: 100%;
}
.page-header {
@include flex($item: center, $content: space-between);
background: #fff;
border-bottom: 1px solid #eaeaea;
.page-brand {
@include flex($item: center, $content: space-between);
cursor: pointer;
.page-logo-wrap {
width: 46px;
height: 46px;
.page-logo {
max-width: 100%;
}
}
}
.page-menu {
flex: 1;
background: transparent;
border: 0;
}
}
</style></code></pre>
<ul><li>
<code>@import '~@/stylesheets/layout.scss';</code>以<code>~</code>为前缀,样式可以使用<code>webpack</code>配置的<code>alias</code>。</li></ul>
<pre><code>// 新建文件:client/src/views/homePage/index.vue
<template>
<div>Home</div>
</template></code></pre>
<pre><code>// 新建文件:client/src/views/backstage/index.vue
<template>
<el-container class="backstage-container">
<el-aside class="backstage-aside">
<el-menu
default-active="1"
class="backstage-menu"
:router="true"
:unique-opened="true">
<el-menu-item index="1" :route="{
path: '/backstage/person'
}">
<template slot="title">
<i class="el-icon-location"></i>
<span>个人设置</span>
</template>
</el-menu-item>
<el-menu-item index="2" :route="{
path: '/backstage/approve'
}">
<i class="el-icon-document"></i>
<span slot="title">审批</span>
</el-menu-item>
</el-menu>
</el-aside>
<el-main class="backstage-main">
<router-view />
</el-main>
</el-container>
</template>
<style lang="scss" scoped>
.backstage-container {
height: 100%;
.backstage-menu {
height: 100%;
overflow-y: auto;
}
.backstage-main {
margin-left: 20px;
background: #fff;
}
}
</style></code></pre>
<ul><li>
<code>el-menu</code>的<code>:router="true"</code>配置 & <code>el-menu-item</code>的<code>:route="<路由配置对象>"</code>可以直接实现菜单跳转。</li></ul>
<pre><code>// 新建文件:client/src/views/approve-panel/index.vue
<template>
<div>Approve</div>
</template></code></pre>
<pre><code>// 新建文件:client/src/views/personal-panel/index.vue
<template>
<div>Person</div>
</template></code></pre>
<p>最终展示效果<br><img src="/img/bVbEhXA" alt="router.gif" title="router.gif"></p>
<h2>状态管理</h2>
<h3>登录信息公用</h3>
<p>多个组件公用用户信息,将登录用户信息存储,取代下方图片中的用户信息(<code>alias</code> > <code>account</code>)<br><img src="/img/bVbEhYX" alt="User" title="User"></p>
<h3>引入<code>vuex</code>
</h3>
<pre><code>// 新建文件:client/src/store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
const InitState = {
loginer: {}
}
export default new Vuex.Store({
state: {
...InitState
},
mutations: {
putLoginer (state, loginer) {
state.loginer = Object.assign(
{},
state.loginer,
loginer
)
},
resetVuex (state) {
Object.assign(state, InitState)
}
}
})</code></pre>
<pre><code>// 更新文件:client/src/main.js
...
import store from './store'
...
new Vue({
el: '#app',
router,
store, //新增
components: { App },
template: '<App/>'
})</code></pre>
<pre><code>// 存储状态
// 更新文件:client/src/views/login/index.vue
...
async onLogin () {
...
console.log(res)
if (res && res.code === '200') {
this.$store.commit('putLoginer', res.data)
this.$router.replace('/home')
} else {
...
}
...</code></pre>
<pre><code>// 展示用户信息
//更新文件:client/src/views/layout/index.vue
...
<template>
...
<!-- <el-dropdown-item disabled>用户信息</el-dropdown-item> -->
<el-dropdown-item disabled>{{user}}</el-dropdown-item>
...
</template>
...
<script>
...
computed: {
user () {
if (this.$store.state.loginer) {
const { account, alias } = this.$store.state.loginer
return alias || account
}
}
}
...
</script>
...</code></pre>
<h3>登出清除状态</h3>
<pre><code>// 更新文件:client/src/views/layout/index.vue
async logout () {
this.$router.push('/')
this.$store.commit('resetVuex')
},</code></pre>
<p>若不清除状态,直接进入<code>/home</code>,会发现,用户信息仍存在。<br>若下次登录,用户信息比上次少,甚至没有,会有部分/全部用户信息没有被覆盖。</p>
<h3>效果展示</h3>
<p><img src="/img/bVbEh3h" alt="vuex.gif" title="vuex.gif"></p>
<h3>持久化</h3>
<p>页面刷新,状态会被清空,可以对状态进行本地持久化处理</p>
<pre><code>// 更新文件:client/src/store/index.js
...
import VuexPersistence from 'vuex-persist'//新增
Vue.use(Vuex)
const vuexLocal = new VuexPersistence({ //新增
storage: window.localStorage
})
...
mutations: {
putLoginer (state, loginer) {
state.loginer = Object.assign(
{},
state.loginer,
loginer
)
},
resetVuex (state) {
Object.assign(state, InitState)
}
},
plugins: [vuexLocal.plugin] //新增
})</code></pre>
<p><strong>高频率更新的数据,并不建议持久化处理</strong></p>
<pre><code>// 登出时,需要清除本地化
// 更新文件:client/src/views/layout/index.vue
...
async logout () {
this.$router.push('/')
await localStorage.clear()
this.$store.commit('resetVuex')
},
...</code></pre>
<h3>持久化效果展示</h3>
<p><img src="/img/bVbEimW" alt="persist.gif" title="persist.gif"></p>
<h2>参考文档</h2>
<p><a href="https://link.segmentfault.com/?enc=go%2FXIOE8q7GnX%2F5%2BqSompA%3D%3D.HNL9VuxLyqtcLnN5zRWBHDLcptnmR3v8pEAV2vZ488nqr08tSMik1YJXK4itlGWj" rel="nofollow">vuex</a><br><a href="https://link.segmentfault.com/?enc=HZOK%2B9G5CaOI3Fzn4I0Y9w%3D%3D.5xawYMdYzeePNhGVxclfJHd067CxVuBx%2FWbR74JzV%2Fb8GTXkJJo52m7iVF27HCWGEyuAuTF7iFXLPxowYwr54A%3D%3D" rel="nofollow">vuex-persist</a></p>
Koa & Mongoose & Vue实现前后端分离--07登录加密&服务端参数校验
https://segmentfault.com/a/1190000021952219
2020-03-11T00:09:45+08:00
2020-03-11T00:09:45+08:00
米花儿团儿
https://segmentfault.com/u/mihuartuanr
0
<h2>上节回顾</h2>
<ul>
<li>
<code>Element</code>、<code>scss</code>、<code>axios</code>的使用</li>
<li>路由定义与跳转</li>
<li>登录&注册客户端逻辑</li>
</ul>
<h2>工作内容</h2>
<ul>
<li>
<code>crypto-js</code>加密</li>
<li>服务端参数校验</li>
</ul>
<h2>准备工作</h2>
<ul>
<li>
<code>npm i -S crypto-js</code> // 先切换到<code>/client</code>目录下</li>
<li>
<code>npm install -S koa-bouncer</code> // 先切换到<code>/server</code>目录下</li>
</ul>
<h2>更新逻辑</h2>
<h3>前端加密</h3>
<p>上几节保存的密码都是明文的,这里借助<code>crypto-js</code>加密密码。</p>
<pre><code>// 更新文:client/src/views/login/index.vue
...
import md5 from 'crypto-js/md5'
...
const res = await http.post(
'/users?action=login',
{
account,
password: md5(password).toString()
}
)
...
const res = await http.post(
'/users?action=register',
{
account,
password: md5(password).toString()
}
)
...</code></pre>
<p>在请求登录 & 注册接口传参时,进行加密。<br>测试结果<br><img src="/img/bVbEfZw" alt="md5" title="md5"><br>动态测试结果<br><img src="/img/bVbEf1y" alt="register.gif" title="register.gif"></p>
<h3>后端校验</h3>
<p>在前端表单提交中,有校验逻辑:<br><img src="/img/bVbEf2A" alt="validation" title="validation"><br>可是,通过<code>Postman</code>/其它方式请求接口时,不会走这些校验,所以,后端校验是很必要的。</p>
<h4>不借助库校验</h4>
<p>不借助第三方包的话,就自定义一个中间件(这里举特例)</p>
<pre><code>// 新建文件:server/validation/user.js
const validate = (ctx, next) => {
const { account, password } = ctx.request.body;
// account: [
// { required: true, message: '请输入帐号', trigger: 'blur' },
// { min: 5, message: '长度至少5个字符', trigger: 'blur' }
// ],
// password: [
// { required: true, message: '请输入密码', trigger: 'blur' },
// { min: 3, message: '长度至少3个字符', trigger: 'blur' }
// ]
if (!account || !password) {
ctx.body = {
code: '403',
data: null,
msg: '帐号/密码不能为空'
}
return;
}
if (account.trim().length < 5) {
ctx.body = {
code: '403',
data: null,
msg: '帐号长度至少5个字符'
}
return;
}
next()
}
module.exports = {
validate
}</code></pre>
<pre><code>// 更新文件:server/router/users.js
...
const { validate } = require('../validation/user'); //新增
...
{
path: '/',
method: 'POST',
handle: async (ctx) => {
...
},
middlewares: [
validate //新增
]
},
...</code></pre>
<pre><code>//更新文件:server/utils/router.js
function register(routes) {
routes.forEach((route, idx) => {
const { path, method, middlewares = [], handle } = route; // 加入middlewares
this[method.toLowerCase()](path, ...middlewares, async (ctx, next) => {
await handle(ctx);
})
})
}
module.exports = {
register,
}</code></pre>
<h4>使用<code>koa-bouncer</code>校验</h4>
<p>和‘不借助库校验’二选一,使用该方法时,注意还原‘不借助库校验’的代码。</p>
<pre><code>// 更新文件:server/app.js
...
const bouncer = require('koa-bouncer'); //新增
const routes = require('./router');
const app = new koa();
app.use(cors());
app.use(bouncer.middleware()); //新增
// 中间件的错误处理
app.use(function(ctx, next){//新增
console.log(err)
if (err.name === 'ValidationError') { //bouncer校验失败会抛出错误,根据错误类型作出相应处理
ctx.body = {
code: '403',
data: null,
msg: err.message
}
}
});
});
... </code></pre>
<p>bouncer校验失败会<strong>抛出错误</strong>,根据错误类型作出相应处理</p>
<pre><code>// 更新文件:server/router/users.js
...
const validate = async (ctx, next) => {
ctx.validateBody('account')
.required('帐号必填')
.isString('帐号必须是字符串')
.trim()
.tap(x => x.length)
.gte(4, '帐号长度至少为5个字符')
ctx.validateBody('password')
.required('密码必填')
.isString('密码必须是字符串')
.trim()
await next(); // 这里必须是async/await形式,否则,如动态图,没有返回值。
}
...
{
path: '/',
method: 'POST',
handle: async (ctx) => {
const { action } = ctx.query;
switch (action) {
case 'register':
await register(ctx);
break;
case 'login':
await login(ctx);
break;
default:
await list(ctx);
}
},
middlewares: [
validate // 新增
]
},
...</code></pre>
<pre><code>//更新文件:server/utils/router.js
function register(routes) {
routes.forEach((route, idx) => {
const { path, method, middlewares = [], handle } = route; // 加入middlewares
this[method.toLowerCase()](path, ...middlewares, async (ctx, next) => {
await handle(ctx);
})
})
}
module.exports = {
register,
}</code></pre>
<p>测试结果<br><img src="/img/bVbEgRc" alt="bouncer" title="bouncer"><br>动态测试结果<br><img src="/img/bVbEgWa" alt="koa-bouncer.gif" title="koa-bouncer.gif"></p>
<h2>参考文档</h2>
<p><a href="https://link.segmentfault.com/?enc=4K64LKeH475J8bUZdUqyWg%3D%3D.CMrFDH5ZhZIxGoP74PSM9THSJArc%2F7ocVVVVUEn8w0WmXbfYwO9zyLIyxJYX%2BA1C" rel="nofollow">crypto-js</a><br><a href="https://link.segmentfault.com/?enc=nkOEGvhEmj308TYwNN2kkw%3D%3D.m5XxAWufCsYEhc7AUelrLB1dQDUAbBOkumpk5fy1YFuJ%2FfXmEEuOORnUIXE%2BD40E" rel="nofollow">koa-bouncer</a></p>
Koa & Mongoose & Vue实现前后端分离--06前端登录&注册
https://segmentfault.com/a/1190000021945599
2020-03-10T00:09:22+08:00
2020-03-10T00:09:22+08:00
米花儿团儿
https://segmentfault.com/u/mihuartuanr
0
<h2>上节回顾</h2>
<ul>
<li>荷载的解析</li>
<li>服务端注册&登录逻辑</li>
<li>数据库的存储 & 查询</li>
</ul>
<h2>工作内容</h2>
<ul>
<li>初始化前端环境</li>
<li>创建前端路由</li>
<li>
<code>axios</code>请求接口</li>
</ul>
<h2>准备工作</h2>
<ul><li>全局安装依赖 // 以便通过vue/cli初始化项目</li></ul>
<p>├── @vue/cli@4.1.2</p>
<p>├── @vue/cli-init@4.1.2</p>
<ul><li>
<code>vue init webpack ./</code> // 先切换到<code>/client</code>目录下</li></ul>
<p><img src="/img/bVbEeUd" alt="回答初始化问题" title="回答初始化问题"></p>
<ul><li>
<code>npm i -S axios</code> // 先切换到<code>/client</code>目录下</li></ul>
<h2>页面逻辑</h2>
<ul><li>
<code>npm run start</code>查看页面是否能正常访问<code>localhost:8080</code>
</li></ul>
<h3>技术选型</h3>
<p>自己的项目,没特殊要求,选择自己熟悉的<code>element-ui</code>UI库、<code>scss</code>预编译语言快速搭建页面。</p>
<h3>初始化页面</h3>
<pre><code>// 更新文件:/client/src/App.vue
<template>
<div id="app">
<router-view/>
</div>
</template>
<script>
export default {
name: 'App'
}
</script></code></pre>
<pre><code>// 更新文件:/client/src/router/index.js
// 顺便删除文件:/client/src/components/Helloworld.vue
import Vue from 'vue'
import Router from 'vue-router'
import Login from '@/views/login'
Vue.use(Router)
export default new Router({
routes: [
{
path: '/',
redirect: '/login'
},
{
path: '/login',
name: 'login',
component: Login
}
]
})</code></pre>
<pre><code>// 新建文件: /client/src/views/login/index.vue
<template>
<div>
Login
</div>
</template></code></pre>
<p>展示效果 //更新不及时,可以重启前端服务<br><img src="/img/bVbEeWK" alt="Login" title="Login"></p>
<h3>引入<code>element-ui</code>和<code>scss</code>
</h3>
<ul>
<li>安装依赖:<code>npm i -S element-ui node-scss sass-loader@7</code> // sass-loader安装7.<em>版本,目前最新版8.</em>编译失败</li>
<li>完整引入Element</li>
</ul>
<pre><code>// 更新文件:/client/src/main.js
// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import Vue from 'vue'
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
import App from './App'
import router from './router'
Vue.config.productionTip = false
Vue.use(ElementUI)
/* eslint-disable no-new */
new Vue({
el: '#app',
router,
components: { App },
template: '<App/>'
})</code></pre>
<ul><li>测试可用性</li></ul>
<pre><code>// 更新文件:/client/src/views/login/index.vue
<template>
<div>
<el-button type="primary">Login</el-button>
<p class="red">这是scss</p>
</div>
</template>
<style lang="scss" scoped>
$color: red;
.red {
color: $color;
}
</style></code></pre>
<ul><li>测试结果</li></ul>
<p><img src="/img/bVbEe8L" alt="Element" title="Element"></p>
<h3>快速搭建页面</h3>
<p>为了省事,直接从<code>Element</code>官网 > 组件 > Form表单,拷贝一份<a href="https://link.segmentfault.com/?enc=DDE00NQwR%2BEv4Fo9ADzLOg%3D%3D.GsyHhZ7Qe4xbq19kEhl8kLUVMQ5J8PgcawoHRMoUa7P8kmOhcrzKhZKN%2FZTWzC%2Frzbs%2F4HPnvat5l0nvIvRc%2F7wwvm5D%2B228h82hN6aY9Ns%3D" rel="nofollow">带校验的示例</a>改改</p>
<pre><code>// 更新文件:/client/src/views/login/index.vue
<template>
<div class="page-login">
<el-form :ref="formName" class="form-login" :model="form" :rules="rules">
<el-form-item label="帐号" prop="account">
<el-input v-model="form.account" placeholder="请输出帐号"></el-input>
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input type="password" v-model="form.password" placeholder="请输出密码"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="onLogin">登陆</el-button>
<el-button type="primary" @click="onRegister">注册</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script>
export default {
name: 'Login',
data () {
return {
formName: 'LoginForm',
form: {
account: '',
password: ''
},
rules: {
account: [
{ required: true, message: '请输入帐号', trigger: 'blur' },
{ min: 5, message: '长度至少5个字符', trigger: 'blur' }
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 3, message: '长度至少3个字符', trigger: 'blur' }
]
}
}
},
methods: {
async onLogin () {
console.log('login')
},
async onRegister () {
console.log('register')
}
}
}
</script>
<style lang="scss" scoped>
@import './index.scss';
</style></code></pre>
<pre><code>// 新建文件:client/src/views/login/index.scss
.form-login {
width: 600px;
margin: 0 auto;
}</code></pre>
<ul><li>页面展示</li></ul>
<p><img src="/img/bVbEe9F" alt="Login页面展示" title="Login页面展示"></p>
<h3>添加http请求</h3>
<p>在准备工作时,已经<code>npm i -S axios</code>安装<code>axios</code>。</p>
<pre><code>// 新建配置文件:client/src/config/http.js
export const BASE_URL = 'http://localhost:3000/'
export const TIMEOUT = 15000</code></pre>
<pre><code>// 新建axios实例文件:client/src/utils/http.js
import axios from 'axios'
import { BASE_URL, TIMEOUT } from '@/config/http'
const instance = axios.create({
baseURL: BASE_URL,
timeout: TIMEOUT,
validateStatus: function (status) {
// return status >= 200 && status < 300; // default
return status >= 200 // 可拦截状态码>=200的请求响应
}
})
export default instance</code></pre>
<p><strong>注意:axios默认只返回Http Code为<code>2**</code>请求的响应</strong></p>
<h3>测试Http请求</h3>
<pre><code>//更新文件:server/control/users.js
async function list (ctx) {
try {
const users = await userModel.find(); //查出全部用户
ctx.body = {
code: '200',
data: users,
msg: '查询成功'
}
} catch (err) {
ctx.body = {
code: '403',
data: null,
msg: err.message
}
}
}</code></pre>
<pre><code>//更新文件:client/src/views/login/index.vue
...
<script>
import http from '@/utils/http'
...
methods: {
async onLogin () {
console.log('register')
},
async onRegister () {
console.log('register')
}
},
async created () {
const res = await http.get('/users')
console.log(res)
}
...
</script>
...</code></pre>
<ul><li>效果展示</li></ul>
<p><img src="/img/bVbEfa6" alt="cors" title="cors"><br>发生跨域请求,这里我们切换到<code>/server/</code>目录下,安装依赖<code>npm i -S koa2-cors</code>(线上的话,可以使用<code>nginx</code>做代理)</p>
<pre><code>// 更新文件:server/app.js
const koa = require('koa');
const bodyParser = require('koa-body');
const cors = require('koa2-cors');
const routes = require('./router');
const app = new koa();
app.use(cors());
...</code></pre>
<p>重启后端服务,即可在页面<code>http://localhost:8080/#/login</code>看到请求结果<br><img src="/img/bVbEfbq" alt="users请求" title="users请求"></p>
<h3>过滤返回结果</h3>
<p>从返回接口可以看出,请求服务端成功时,只需要<code>res.data</code>即可,使用<code>instance.interceptors</code>对返回数据进行过滤。</p>
<pre><code>// 更新文件:client/src/utils/http.js
...
// Add a response interceptor
instance.interceptors.response.use(
async res => {
if (/^20./.test(res.status)) {
return res.data
}
console.log('------response=======', res)
return res
},
error => {
return Promise.reject(error)
}
)
export default instance</code></pre>
<p>请求结果<br><img src="/img/bVbEfbW" alt="interceptor" title="interceptor"></p>
<h3>登录逻辑</h3>
<pre><code>//更新文件:client/src/views/login/index.vue
...
async onLogin () {
try {
const valid = await this.$refs[this.formName].validate()
if (valid) {
const { account, password } = this.form
const res = await http.post(
'/users?action=login',
{
account,
password,
}
)
console.log(res)
if (res && res.code === '200') {
this.$router.replace('/home')
} else {
this.$message({ // 没有使用this.$message.error('')
type: 'error',
message: res.msg
})
}
}
} catch (err) {
console.error(err)
}
},
...</code></pre>
<ul><li>
<code>this.$message({})</code>,而没有使用<code>this.$message.error()</code>,因为发现如果res没有返回的话,会报<code>Element</code>的错误,造成信息误导。</li></ul>
<p>更新路由文件,使登录成功跳转到<code>Home</code>组件</p>
<pre><code>// 更新路由文件:
import Vue from 'vue'
import Router from 'vue-router'
import Login from '@/views/login'
import Home from '@/views/home'
Vue.use(Router)
export default new Router({
routes: [
{
path: '/',
redirect: '/login'
},
{
path: '/login',
name: 'login',
component: Login
},
{
path: '/home',
name: 'home',
component: Home
}
]
})</code></pre>
<pre><code>// 新建文件:client/src/views/home/index.vue
<template>
<div>Home</div>
</template></code></pre>
<p>使用上节通过<code>Postman</code>创建的<code>admin</code>账户登录,结果展示<br><img src="/img/bVbEfcK" alt="admin登录" title="admin登录"><br>缓慢的动图如下:<br><img src="/img/bVbEfmq" alt="login.gif" title="login.gif"></p>
<h3>注册逻辑</h3>
<pre><code>//更新文件:client/src/views/login/index.vue
...
async onRegister () {
try {
const valid = await this.$refs[this.formName].validate()
if (valid) {
const { account, password } = this.form
const res = await http.post(
'/users?action=register',
{
account,
password
}
)
if (res.code === '200') {
this.$refs[this.formName].resetFields()
this.$message({
type: 'success',
message: '注册成功'
})
} else {
this.$message({
type: 'error',
message: res.msg
})
}
}
} catch (err) {
console.error(err)
}
}
...</code></pre>
<p>测试失败结果<br><img src="/img/bVbEfde" alt="失败" title="失败"><br>测试成功结果<br><img src="/img/bVbEfdn" alt="成功" title="成功"><br>可以使用新创建的帐号登录,发现可以成功/查看数据库,是否成功新增用户</p>
<h2>参考文档</h2>
<p><a href="https://link.segmentfault.com/?enc=Q5UtOMoq2RWgIIZtNvqeuQ%3D%3D.h5pT5aQMPJ2exMXrIL3eKuVPj2BlQPhYVT%2Bja5u2IMrl3hzuHff74srFB9k4cdYUsqjqas2YHfuqInB89orCGQ%3D%3D" rel="nofollow">element-ui</a><br><a href="https://link.segmentfault.com/?enc=YP7kdSqhaxiTUnFI55oNhw%3D%3D.MCmMYgY51zCOMjJ78NDKDSyl5OtWd0u61JN3WPDglIA%3D" rel="nofollow">vue-router</a><br><a href="https://link.segmentfault.com/?enc=9bxmhZ7IQikafh9XaZsyvw%3D%3D.TsMcU9Y8iRVDXt8JI69D8BCIMcLdnsDPl3p1qbB%2FMlk%3D" rel="nofollow">axios</a></p>
Koa & Mongoose & Vue实现前后端分离--05服务端注册&登录:业务逻辑
https://segmentfault.com/a/1190000021937077
2020-03-09T00:03:28+08:00
2020-03-09T00:03:28+08:00
米花儿团儿
https://segmentfault.com/u/mihuartuanr
0
<h2>上节回顾</h2>
<ul>
<li>
<code>@koa/router</code>的用法</li>
<li>用户路由的定义</li>
<li>
<code>koa</code>中间件的使用</li>
<li>
<code>Postman</code>基本测试</li>
</ul>
<h2>工作内容</h2>
<ul>
<li>后端:参数的获取</li>
<li>后端:数据库的写入与查询</li>
<li>Postman:接口测试</li>
</ul>
<h2>准备工作</h2>
<ul><li>
<code>npm i -S koa-body</code> //对<code>Post、Put、Patch</code>请求参数处理</li></ul>
<h2>业务逻辑</h2>
<h3>参数获取</h3>
<h4>支持荷载存储到<code>ctx.request.body</code>
</h4>
<pre><code>// 更新文件:/server/app.js
const koa = require('koa');
const bodyParser = require('koa-body'); // 新增
const routes = require('./router');
const app = new koa();
app.use(bodyParser());// 新增:在处理请求数据的中间件前调用
//app.use((ctx, next) => {
// ctx.body = '测试测试测试';
// next();
//}) //无用代码删除
app.use(...router.routes).use(...router.allowedMethods);
routes.forEach(route => {
app.use(route);
});
app.on('error', err => {
log.error('server error', err)
});
module.exports = app;</code></pre>
<h4>更新注册&登录路由</h4>
<ul>
<li>调用<code>POST</code>方法分别访问<code>/users?action='register'</code>和<code>/users?action='login'</code>。</li>
<li>通过<code>ctx.query</code>获取url<code>?</code>追加的参数。</li>
</ul>
<pre><code>// 更新文件:/server/router/users.js部分代码
...
{
path: '/',
method: 'POST',
handle: async (ctx) => {
const { action } = ctx.query;
switch (action) {
case 'register':
await register(ctx);
break;
case 'login':
await login(ctx);
break;
default:
await list(ctx);
}
}
},
...
</code></pre>
<ul><li>测试结果</li></ul>
<p><img src="/img/bVbEcWc" alt="register结果" title="register结果"></p>
<ul><li>可以通过断点查看数据</li></ul>
<p><img src="/img/bVbEcWv" alt="断点数据" title="断点数据"></p>
<h3>注册逻辑</h3>
<h4>更新注册逻辑</h4>
<pre><code>// 更新文件:/server/control/user.js
const userModel = require('../model/user');
...
async function register (ctx) {
const { account, password } = ctx.request.body;//获取荷载
if (!account || !password) {
ctx.body = { // 返回json
code: '403',
data: null,
msg: '帐号/密码不能为空'
}
return;
}
try {
const user = await userModel.findOne({ // 查看数据库是否已有数据
account
});
if (user) {
ctx.body = {
code: '403',
data: null,
msg: '用户已经存在'
}
} else {
const newUser = await new userModel({ // 新建数据
account,
password
}).save();
ctx.body = {
code: '200',
data: newUser,
msg: '注册成功'
}
}
} catch (err) {
ctx.body = {
code: '403',
data: null,
msg: err.message
}
}
}
...</code></pre>
<ul>
<li>通过<code>ctx.request.body</code>获取调用<code>POST</code>方法传过来的参数</li>
<li>通过<code>userModel.findOne(<condition>)</code>查找匹配条件的数据</li>
<li>通过<code>new userModel(<Data>).save()</code>存储数据</li>
</ul>
<h4>测试逻辑</h4>
<ul><li>
<code>Body</code>面板 --> <code>raw</code> --> <code>JSON</code>格式 --> 输入参数 --> 查看结果</li></ul>
<p><img src="/img/bVbEcYB" alt="register结果" title="register结果"></p>
<ul><li>存储结果直接返回,会将<code>password</code>返回,这里自行过滤掉敏感信息。</li></ul>
<h4>数据库结果</h4>
<p><img src="/img/bVbEcY4" alt="robot" title="robot"></p>
<h3>登录逻辑</h3>
<h4>更新注册逻辑</h4>
<pre><code>async function login (ctx) {
const { account, password } = ctx.request.body;
if(!account || !password) {
ctx.body = {
code: '404',
data: null,
msg: '参数不合法'
};
return;
}
const user = await userModel.findOne({
account,
password
});
if(user) {
ctx.body = {
code: '200',
data: user,
msg: '登陆成功'
}
} else {
ctx.body = {
code: '404',
data: null,
msg: '帐号/密码错误'
}
}
}</code></pre>
<h4>测试逻辑</h4>
<ul><li>
<code>Body</code>面板 --> <code>raw</code> --> <code>JSON</code>格式 --> 输入参数 --> 查看结果</li></ul>
<p><img src="/img/bVbEcZL" alt="login测试" title="login测试"></p>
<h2>常见问题</h2>
<ul>
<li>查看服务端是否启动</li>
<li>查看请求方式<code>Method</code>是否正确</li>
<li>多用断点调试问题</li>
</ul>
<h2>课后尝试</h2>
<ul>
<li>
<code>/server/app.js</code>中<code>app.use(bodyParser())</code>注释掉,看看是否还能<code>ctx.request.body</code>是否存在</li>
<li>用断点多看一下<code>ctx</code>的结构</li>
</ul>
<h2>参考文档</h2>
<p><a href="https://link.segmentfault.com/?enc=QxwTQcQoImn5TCe6oabABA%3D%3D.IXUphx3mzTNFeruNr18OZ%2BjP5CwkOC%2BweGM%2BuYDZ5pzqLOpkwNz0y6mgTD7qMqQe" rel="nofollow">koa-body</a><br><a href="https://link.segmentfault.com/?enc=GkSIAnQdhGiUqjqYK%2Bwmxg%3D%3D.YyGnBEmaVg9p2c%2B42PTIKiNuZznXXQL8ueUvPkhsmZP%2FTUokGSjhMFkKmn2sQrqG" rel="nofollow">mongoosejs#Model Api</a></p>
Koa & Mongoose & Vue实现前后端分离--04服务端注册&登录:用户路由配置
https://segmentfault.com/a/1190000021931689
2020-03-08T00:40:44+08:00
2020-03-08T00:40:44+08:00
米花儿团儿
https://segmentfault.com/u/mihuartuanr
0
<h2>上节回顾</h2>
<ul>
<li>
<code>mongoose</code>连接数据库</li>
<li>数据存储结构的定义</li>
</ul>
<h2>工作内容</h2>
<ul>
<li>后端:路由拦截</li>
<li>Postman:测试接口</li>
</ul>
<h2>准备工作</h2>
<ul><li>
<code>npm i -S @koa/router</code> //安装路由</li></ul>
<h2>路由拦截</h2>
<h3>基本用法</h3>
<pre><code>// 修改文件:/server/app.js
const koa = require('koa');
const Router = require('@koa/router'); //引入NPM包
const router = new Router() // 创建实例
const app = new koa();
app.use((ctx, next) => {
ctx.body = '测试测试测试'; //该中间件任何时候都会走,GET请求'/'路径时,返回被后续的返回覆盖了,所以,没有展示。
next();
})
router.get('/', (ctx, next) => { //拦截GET访问'/'路径的请求
ctx.body = `访问路径:${ctx.originalUrl}`
});
app
.use(router.routes())
.use(router.allowedMethods()); // 嵌入中间件
app.on('error', err => {
log.error('server error', err)
});
module.exports = app;</code></pre>
<p>以<code>nodemon</code>配置的<code>launch.json</code>运行<br><img src="/img/bVbEbCX" alt="nodemon" title="nodemon"></p>
<p>浏览器输入<code>localhost:3000</code>和<code>localhost:3000/any</code>查看输出结果。</p>
<h3>优化代码</h3>
<p>这里希望将所有的路由配置提取到<code>/server/router</code>文件中(注意还原<code>/server/app.js</code>)。</p>
<pre><code>// 新建文件:/server/control/users.js————预定义处理路由处理函数
async function list (ctx) {
ctx.body = 'list'
}
async function register (ctx) {
ctx.body = 'register'
}
async function login (ctx) {
ctx.body = 'login'
}
async function update (ctx) {
ctx.body = 'update'
}
module.exports = {
list,
register,
login,
update
}</code></pre>
<pre><code>// 新建文件:/server/router/users.js
const Router = require('@koa/router');
const routerUtils = require('../utils/router');
const { list, register, login, update } = require('../control/users');
const router = new Router({
prefix: '/users' //路由前缀,该文件下的路由路径,追加'/users'为前缀
});
// 配置路由
const routes = [
{
path: '/',
method: 'GET',
handle: list
},
{
path: '/',
method: 'POST',
handle: login
},
{
path: '/:id',
method: 'PATCH',
handle: update
},
];
routerUtils.register.call(router, routes); // 注册路由
module.exports = router; //导出User的路由实例
</code></pre>
<pre><code>// 新建文件:/server/utils/router.js
function register(routes) {
// 转换为'@koa/router'拦截路由的形式:router.get(<path>, <handle>)
routes.forEach((route, idx) => {
const { path, method, handle } = route;
this[method.toLowerCase()](path, async (ctx, next) => {
await handle(ctx);
})
})
}
module.exports = {
register,
}</code></pre>
<pre><code>// 新建文件:/server/router/index.js
const userRouter = require('./users');
// 导出User路由相关的中间件,后续可追加其它的中间件
module.exports = [
userRouter.routes(),
userRouter.allowedMethods()
];</code></pre>
<pre><code>// 更新文件:/server/app.js
const koa = require('koa');
const routes = require('./router');
const app = new koa();
app.use((ctx, next) => {
ctx.body = '测试测试测试';
next();
})
//中间件: 路由 --> 不支持一次性注册多个中间件
// app.use(...router.routes).use(...router.allowedMethods);
routes.forEach(route => {
app.use(route);
});
app.on('error', err => {
log.error('server error', err)
});
module.exports = app;</code></pre>
<h2>Postman测试接口</h2>
<ul><li>可以建立一个文件夹存放同系列的请求,也可以直接新建一个请求</li></ul>
<p><img src="/img/bVbEctf" alt="新建Postman请求" title="新建Postman请求"></p>
<ul>
<li><img src="/img/bVbEcuT" alt="基本介绍" title="基本介绍"></li>
<li>测试Get请求/users接口<img src="/img/bVbEcu7" alt="get('/users')" title="get('/users')">
</li>
<li>可以通过断点查看拦截请求<img src="/img/bVbEcvh" alt="断点数据" title="断点数据">
</li>
</ul>
<h2>参考文档</h2>
<p><a href="https://link.segmentfault.com/?enc=zaeg5Wjxra3ZSIlb3KNppg%3D%3D.tXIwrDzDbq2yVMOU%2F5bITA8HFpwjMwafo6R1uyLX4pQXhtElV90HosQs9vfoaxLRdjYNbn64wqmIUFoMpFIT6ts5bXBQDrj%2F3xbwcSBzfRI02ZNDIGgtbpK24yfR4Kv7" rel="nofollow">@koa/router</a></p>
Koa & Mongoose & Vue实现前后端分离--03连接数据库
https://segmentfault.com/a/1190000021927364
2020-03-07T00:16:08+08:00
2020-03-07T00:16:08+08:00
米花儿团儿
https://segmentfault.com/u/mihuartuanr
3
<h2>上节回顾</h2>
<ul>
<li>
<code>koa</code>搭建服务端</li>
<li>运行服务端代码(如何在命令行中调用本地依赖包)</li>
<li>借助<code>vs code</code>调试代码</li>
</ul>
<h2>工作内容</h2>
<ul>
<li>连接数据库</li>
<li>创建数据结构</li>
</ul>
<h2>准备工作</h2>
<ul>
<li>安装<a href="https://link.segmentfault.com/?enc=h%2FHmd4ZLMLX4yeiTgPqLtA%3D%3D.03UYZZcIG0iN88iC7PL9Ctv%2BzKGuTVxePGjYTQm89QE%3D" rel="nofollow"><code>robo</code></a> //数据库可视化工具</li>
<li>安装<a href="https://link.segmentfault.com/?enc=blUyEPZgKSEz%2F6t4lf6Lcg%3D%3D.Gl%2Bvt5B6N5FajvieVQkaNBOVO0ESNgpiHqtk%2F%2F7vFCa241vBNZvR6HFScZnlfYI%2FWhbrom%2BrHs1yTu81A2L2gg%3D%3D" rel="nofollow"><code>mongoDb</code></a> //我已经安装过了,也没遇到什么坑,就不再演示了</li>
</ul>
<p><img src="/img/bVbD99s" alt="mongoDb" title="mongoDb"></p>
<ul>
<li>
<code>npm i -S mongoose</code> // 安装<code>mongoose</code>
</li>
<li>
<code>brew services start mongodb-community</code> //启动<code>mongoDb</code>
</li>
<li>
<p>创建目录</p>
<ul><li>
<p>|-server</p>
<ul>
<li>
<p>|-- db</p>
<ul><li>|--- index.js //数据库连接</li></ul>
</li>
<li>
<p>|-- model</p>
<ul><li>|--- user.js //数据存储结构</li></ul>
</li>
</ul>
</li></ul>
</li>
</ul>
<h2>连接数据库</h2>
<p>这里<code>mongodb</code>安装的时候,没有设置密码,直接连接使用;</p>
<pre><code>// 文件:db/index.js
const mongoose = require('mongoose');
mongoose.connect('mongodb://localhost:27017/demo');
const db = mongoose.connection;
// mongoose.connect连接mongodb,返回一个异步对象,可监听事件;
db.on('connected', () => {
console.log('数据库连接成功');
});
db.on('error', () => {
console.log('发生错误')
})
db.on('disconnected', () => {
console.log('断开连接')
})</code></pre>
<p>在<code>/server</code>目录下,运行<code>node db/index.js</code>打印出“数据库连接成功”即可。</p>
<h2>优化连接</h2>
<ul>
<li>将域名、端口、数据库名提出</li>
<li>将<code>mongoose.connection</code>导出</li>
</ul>
<pre><code>// 文件:新建/server/config/db.js
module.exports = {
port: 27017, // 默认端口
host: 'localhost',
database: 'demo', // 自定义数据库名
}</code></pre>
<pre><code>// 优化/server/bd/index.js
const mongoose = require('mongoose');
const DB = require('../config/db');
const { port, host, database } = DB;
const DB_URL = `mongodb://${host}:${port}/${database}`;
// mongoose.connect连接mongodb,返回一个异步对象,可监听事件;
mongoose.connect(DB_URL);
const db = mongoose.connection;
db.on('connected', function() {
console.log(`Mongoose connection open to ${DB_URL}`)
})
db.on('error', function(err){
console.log(`Mongoose connection error: ${err}`)
})
db.on('disconnected', function() {
console.log('Mongoose connection disconnected')
})
module.exports = mongoose;</code></pre>
<h2>创建数据结构</h2>
<p><img src="/img/bVbEarf" alt="数据字段" title="数据字段"></p>
<pre><code>// 文件:server/model/user.js
const mongoose = require('../db/index');
const Schema = mongoose.Schema;
// 定义数据结构
const userSchema = Schema({
__v: { // __v双下划线,默认生成
select: false // select:false查询不会将该字段查出
},
avatar: {
type: String
},
account: {
type: String,
required: true
},
password: {
type: String,
required: true,
select: false
},
alias: {
type: String
},
telephone: {
type: String,
select: false
},
email: {
type: String,
select: false
}
})
// 后续的增删改查是通过导出的User Model实现。
module.exports = mongoose.model('User', userSchema);</code></pre>
<pre><code>// 文件:server/model/approve.js
const mongoose = require('../db/index');
const Schema = mongoose.Schema;
const approveSchema = Schema({
name: {
type: String,
required: true
},
category: {
type: String,
required: true
},
description: {
type: String,
select: false
},
author: {
type: Schema.Types.ObjectId, // 注意作者
ref: 'User',
required: true
},
createtime: {
type: Number,
required: true
},
latesttime: {
type: Number,
required: true
}
})
module.exports = mongoose.model('Approve', approveSchema);</code></pre>
<ul>
<li><a href="https://link.segmentfault.com/?enc=4dkDFrnQzI71TiGyCR3qzg%3D%3D.X6RlFOjVBEp%2FbZHlm2kTAd%2B41yFlNX%2Bk5Kqhb0Vn%2B8mlnetBuADiq4Ayn7RE33Hn" rel="nofollow"><code>Schema</code>定义参考文档</a></li>
<li>
<code>approveSchema</code>中<code>author</code>的<code>type:Schema.Types.ObjectId, ref='User'</code>,新增审批的时候,将用户的<code>Id</code>赋值给<code>author</code>即可,后续通过<a href="https://link.segmentfault.com/?enc=LYF6dmUBcmC0hxkYqVgi0w%3D%3D.u7PtJZrEgdGcdMbtjHEnycXtyvyBz0gwb5qy2R0KrpCmEuuO87MpSdPPSHUgUYlq" rel="nofollow"><code>populate</code></a>,可以通过用户Id查出用户信息替换该<code>Id</code>赋值给<code>author</code>字段。</li>
</ul>
<h2>参考文档</h2>
<p><a href="https://link.segmentfault.com/?enc=HMKLYhXCgMbnmkQMvA%2BFeA%3D%3D.uQFxdYxHDDUNZ7yD2pHEFJzY4h1swtNWFycq8VAQz1Gna4UTItTGKzDvCgO%2BHMjE" rel="nofollow">mongoose</a><br><a href="https://link.segmentfault.com/?enc=HA7S1NsfXHjCAbK3MbrZ3g%3D%3D.SGWK5RpR3i1edMW5XmtLEMH0RNqvRp12JD%2FAUJvv%2FnzNEJ3xandH1miluOKMi7p0" rel="nofollow"><code>Schema</code>定义参考文档</a><br><a href="https://link.segmentfault.com/?enc=1Lo%2BG7UAoYWzBddDhpgskA%3D%3D.UBntFCUj37pIkmE9FHYIdGVpQTjtYCtq2Jd%2BNa9rifqaRNksi0s8F5x3j%2B8OTmtx" rel="nofollow">mongoose一对多关系方案</a></p>
Koa & Mongoose & Vue实现前后端分离--02搭建服务端
https://segmentfault.com/a/1190000021924701
2020-03-06T10:00:00+08:00
2020-03-06T10:00:00+08:00
米花儿团儿
https://segmentfault.com/u/mihuartuanr
3
<h2>工作内容</h2>
<ul>
<li>搭建服务端</li>
<li>运行起来</li>
</ul>
<h2>准备工作</h2>
<h3>创建目录结构</h3>
<p>|-client<br>|-server<br> |--bin // 可执行文件目录<br> |--app.js</p>
<h3>初始化项目</h3>
<ul>
<li>切换到server目录下<code>cd server</code>
</li>
<li>初始化项目<code>npm init -y</code> // <code>-y</code>以默认值初始化</li>
<li>
<code>npm i -S koa</code> //安装项目依赖koa</li>
</ul>
<h2>构建服务端</h2>
<h3>编写app.js</h3>
<pre><code>const koa = require('koa');
const app = new koa();
app.use((ctx, next) => {
ctx.body = '测试测试测试';
next();
})
app.listen(3000, 'localhost', () => {
console.log('Server is running!')
})
app.on('error', err => {
log.error('server error', err)
});</code></pre>
<h3>运行服务端</h3>
<ul>
<li>
<p>方式一:<code>cli</code></p>
<ul><li>直接在项目路径<code>server</code>下,开启命令行,运行<code>node app.js</code>
</li></ul>
</li>
<li>
<p>方式二:<code>scripts</code></p>
<ul><li>在<code>server/package.json</code>文件的<code>scripts</code>编写<code>"start": "node app.js"</code>,然后在项目路径<code>server</code>下,开启命令行<code>npm start</code>
</li></ul>
</li>
<li>
<p>方式三:编辑器vs code</p>
<ul>
<li>鼠标聚焦在<code>server/app.js</code>文件内,<code>f5</code>选择<code>Node.js</code>运行环境(必须聚焦在可执行文件中)</li>
<li><img src="/img/bVbD9wm" alt="调试" title="调试"></li>
</ul>
</li>
<li>
<p>方式四:vs code创建launch.json文件</p>
<ul>
<li>直接通过右下角的“<strong>添加配置</strong> --> Node.js启动程序”添加新的配置内容,将<code>program": "${workspaceFolder}/app.js",</code>改为program": "${workspaceFolder}/server/app.js"即可</li>
<li><img src="/img/bVbD9xk" alt="添加配置" title="添加配置"></li>
<li>开启调试运行<img src="/img/bVbD9Fc" alt="开启调试" title="开启调试">
</li>
</ul>
</li>
</ul>
<p>使用vs code运行可执行文件是调试运行,可以在vs code中打断点,页面请求时,进行调试。<br><img src="/img/bVbD9FI" alt="断点调试" title="断点调试"></p>
<h3>优化代码</h3>
<ul>
<li>将服务端的监听移动到可执行目录下<code>./bin</code>,<code>app.js</code>只是服务端的配置</li>
<li>将端口/域名提出到配置目录下</li>
<li>
<strong>注意</strong>:需要修改运行服务端的可执行文件的路径(这里就不贴出方法来了,下方监听变化的时候会有)</li>
</ul>
<pre><code>// server/app.js
const koa = require('koa');
const app = new koa();
app.use((ctx, next) => {
ctx.body = '测试测试测试';
next();
})
app.on('error', err => {
log.error('server error', err)
});
module.exports = app;</code></pre>
<pre><code>// server/bin/index.js
const app = require('../app');
const Server = require('../config/server');
const {host, port} = Server;
app.listen(port, host, () => {
console.log(`server is running at http://${host}:${port}`);
})</code></pre>
<pre><code>// 新建server/config/server.js
module.exports = {
port: 3000,
host: 'localhost'
}</code></pre>
<h2>监听变化</h2>
<p>如果修改了<code>server/app.js</code>中</p>
<pre><code>app.use((ctx, next) => {
ctx.body = '测试测试测试1111'; // 初始值为ctx.body = '测试测试测试';
next();
})</code></pre>
<p>刷新浏览器<code>http://localhost:3000/</code>发现输出并没有改变,需要重新启动服务端,才能得到新的输出。</p>
<p>那如何实现监听变化,刷新页面后,不需要重启服务就能得到新的输出呢?</p>
<h3>准备工作</h3>
<p><code>npm i -D nodemon</code>安装<code>nodemon</code>包</p>
<h3>改变运行方式</h3>
<p>将<code>node</code>运行改成<code>nodemon</code>运行可执行文件<code>server/bin/index.js</code>。<br>对应上方的运行改变:</p>
<ul>
<li>
<p>方式一:<code>cli</code></p>
<ul><li>直接在项目路径<code>server</code>下,开启命令行,运行<code>$(npm bin)/nodemon app.js</code>
</li></ul>
</li>
<li>
<p>方式二:<code>scripts</code></p>
<ul><li>在<code>server/package.json</code>文件的<code>scripts</code>编写<code>"start": "nodemon app.js"</code>,然后在项目路径<code>server</code>下,开启命令行<code>npm start</code>
</li></ul>
</li>
<li>
<p>方式三:编辑器vs code</p>
<ul><li>鼠标聚焦在<code>server/bin/index.js</code>文件内,<code>f5</code>选择<code>Node.js</code>运行环境(必须聚焦在可执行文件中)</li></ul>
</li>
<li>方式四:vs code创建launch.json文件</li>
</ul>
<pre><code>{
// 使用 IntelliSense 了解相关属性。
// 悬停以查看现有属性的描述。
// 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Launch Program",
"program": "${workspaceFolder}/server/bin/index.js",
"skipFiles": [
"<node_internals>/**"
]
},
{
"type": "node",
"request": "launch",
"name": "nodemon",
"runtimeExecutable": "${workspaceFolder}/server/node_modules/.bin/nodemon",
"program": "${workspaceFolder}/server/bin/index.js",
"restart": true,
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"skipFiles": [
"<node_internals>/**"
]
}
]
}</code></pre>
<p>接下来都是以<code>launch.json</code>运行服务。</p>
<h2>参考文档</h2>
<p><a href="https://link.segmentfault.com/?enc=T%2B30XeFUwmbmVvHnXVil8A%3D%3D.E5qi%2Bp4mM9jORmGcwKJaybSGVIXJSkISFMLleEVZV1s%3D" rel="nofollow">koa</a></p>
Koa & Mongoose & Vue实现前后端分离--01序言
https://segmentfault.com/a/1190000021924672
2020-03-05T22:00:00+08:00
2020-03-05T22:00:00+08:00
米花儿团儿
https://segmentfault.com/u/mihuartuanr
4
<h2>简介</h2>
<p>这系列文章没有很深入的内容,只是一个简单的全栈实现流程,内容包含前后端开发的主要步骤和过程中的一些思考。</p>
<p>使用<code>koa</code>结合<code>mongoose</code>搭建服务端,<code>jwt</code>实现身份验证。<br><code>vue</code>结合<code>element-ui</code>实现前端渲染,<code>axios</code>负责网络请求,<code>vuex</code>负责状态管理。</p>
<h2>业务模块:</h2>
<ol>
<li>登陆</li>
<li>注册</li>
<li>用户信息</li>
<li>审批(新建、更新、删除、查询)</li>
<li>日志系统</li>
</ol>
<h2>内容包含</h2>
<ul>
<li>使用<code>Koa</code>构建Web服务</li>
<li>使用<code>mongoose</code>连接<code>MongoDb</code>
</li>
<li>
<code>Schema</code>构建数据结构</li>
<li>登陆/注册(<code>crypto-js</code>)</li>
<li>使用<code>Postman</code>测试(<code>token</code>鉴权)</li>
<li>
<code>JWT</code>认证(<code>jsonwebtoken</code> & <code>koa-jwt</code>)</li>
<li>展示/更新人员信息(<code>el-upload</code> & <code>el-dialog</code>)</li>
<li>图片上传 & 存储 & 访问(<code>koa-body</code> & <code>koa-static</code>)</li>
<li>日志系统(<code>koa-morgan</code> & <code>moment</code>)</li>
<li>安全策略(<code>xss</code>)</li>
<li>一对多数据的增删改查</li>
<li>不包括上线</li>
</ul>
<h2>学习期望</h2>
<ul>
<li>适合对<code>vue</code>、<code>koa</code>、<code>mongoose</code>有兴趣或基础了解的同学</li>
<li>忌眼高手低,可以的话,最好操作一遍</li>
<li>如果在您实现过程中遇到有价值的问题,并找到了答案,欢迎在对应的文章下留言</li>
</ul>
<h2><a href="https://link.segmentfault.com/?enc=SJbE0kgoWNI%2BjCYkLicoZA%3D%3D.1qeyawtYI9dUssEUIOxPHyLfbi9GQFwaFwbQ24Fm5m8ZD%2FgMPUBEmQ7OItzKJSBPqmgAqxV528JrYsw2y289N97lHAVp20HTgz%2FaSVnr8PU%3D" rel="nofollow">效果预览</a></h2>
<p>图片很大,可能无法预览,可以下载看效果</p>
<h2>目录</h2>
<ul>
<li><a href="https://segmentfault.com/a/1190000021924672">01序言</a></li>
<li><a href="https://segmentfault.com/a/1190000021924701">02<code>koa</code>搭建服务端</a></li>
<li><a href="https://segmentfault.com/a/1190000021927364">03<code>mongoose</code>连接数据库 & 预定义数据结构</a></li>
<li><a href="https://segmentfault.com/a/1190000021931689">04服务端注册&登录:用户路由配置</a></li>
<li><a href="https://segmentfault.com/a/1190000021937077">05服务端注册&登录:业务逻辑</a></li>
<li><a href="https://segmentfault.com/a/1190000021945599">06客户端登录&注册</a></li>
<li><a href="https://segmentfault.com/a/1190000021952219">07登录加密&服务端参数校验</a></li>
<li><a href="https://segmentfault.com/a/1190000021956708">08前端状态管理&路由嵌套</a></li>
<li><a href="https://segmentfault.com/a/1190000021969629">09身份验证JWT&测试</a></li>
<li><a href="https://segmentfault.com/a/1190000021978182">10更新用户信息</a></li>
<li><a href="https://segmentfault.com/a/1190000021996824">11更新用户头像:图片上传</a></li>
<li><a href="https://segmentfault.com/a/1190000022008050">12数据库联表查询</a></li>
<li><a href="https://segmentfault.com/a/1190000022038423">13日志系统&安全系统</a></li>
</ul>
<h2>版权说明</h2>
<p>实现过程中,遇到问题或不熟悉的地方,参考了很多文章,如果想加入参考链接的同学,请联系我(大众版本的文章,可能没什么印象)。</p>
<p><a href="https://link.segmentfault.com/?enc=V235whiBW0TSNNj9XlZikw%3D%3D.qD%2FuMEDoGV5wa6ZCSgwtC0gZ6oPmi419Y3MFP59PqJm14V5mBuY1%2FXnDAEnYxjAW" rel="nofollow">源代码地址</a></p>
<p><del>写系列文章,容易断奶,如果坚持下来的话,就把这句话删掉……</del></p>
<p><strong>后续内容多图警告</strong><br><strong>Mac演示警告</strong></p>
微信小程序开发初试(Taro、Vant-weapp)
https://segmentfault.com/a/1190000020255902
2019-09-02T07:43:16+08:00
2019-09-02T07:43:16+08:00
米花儿团儿
https://segmentfault.com/u/mihuartuanr
7
<h2>组件实例</h2>
<p><code>this.selectComponent('.classSelector')</code></p>
<h2>引入</h2>
<h3>Taro</h3>
<p><strong>Taro中引入Vant Weapp,不能直接通过第三方<code>NPM</code>包的形式直接调用。</strong></p>
<p>需要进行以下几步:</p>
<ul>
<li>在github上找到<a href="https://link.segmentfault.com/?enc=QsVbqL5sa7m0IrPvGmnsBQ%3D%3D.mQ9aZUMu0%2Fu9rVZ8jHh1IDiMiS%2FsE%2B0T5egLgrvom40nDD3adrRHFEUfhABJ7zZX" rel="nofollow">Vant-weapp</a>下载文件包,将对应的<code>dist</code>目录复制到项目<code>/src/components/vant-weapp</code>目录下。</li>
<li>在<code>Pages</code>对应文件的<code>config.usingComponents</code>中,配置每个页面所需要的组件。(无法在<code>app.js</code>中进行所谓的全局注册组件。)</li>
</ul>
<pre><code> config = {
navigationBarTitleText: '首页',
usingComponents: {
"van-button": "../../components/vant-weapp/button/index",
"van-popup": "../../components/vant-weapp/popup/index"
}
}</code></pre>
<ul><li>在使用<code>Vant-weapp</code>组件后,<code>taro</code>构建会<strong>自动</strong>将相应的组件复制一份到<code>dist/components</code>下,而<code>Vant-weapp</code>的组件还依赖工具库<code>/src/components/vant-weapp/wxs</code>,该工具库<code>taro</code><strong>不会自动</strong>复制到<code>dist</code>中。所以,我们需要修改<code>/config/index.js</code>文件中的<code>config.copy.patterns</code>,让其在编译时,自动复制到<code>dist</code>对应目录下。</li></ul>
<pre><code> copy: {
patterns: [
{
from: 'src/components/vant-weapp/wxs/',
to: 'dist/components/vant-weapp/wxs/'
}
],
options: {
}
},</code></pre>
<ul><li>由于<code>Vant-weapp</code>的样式使用的单位是<code>px</code>,所以会被<code>taro</code>编译成<code>rpx</code>,以便对各个设配进行适配。可以通过修改<code>/config/index.js</code>文件中的<code>config.weapp.module.pxtransform.selectorBlackList</code>不让其单位转换。</li></ul>
<pre><code>pxtransform: {
enable: true,
config: {
},
selectorBlackList: [
/^.van-.*?$/, // 这里是vant-weapp中className的匹配模式
]
},</code></pre>
<h3><a href="https://link.segmentfault.com/?enc=4hfQDBOqEvPvqz10K90Gfw%3D%3D.AlVnFFtU%2BfMhcN41N2xxJWf%2FHJdiD%2BVtD62I%2Fyv1kMQKAjjPir2DSBZYeWqODStJ" rel="nofollow">ec-canvas</a></h3>
<blockquote>
<code>ec-canvas</code>是 <code>ECharts</code> 的微信小程序版本。</blockquote>
<h3>iconfont</h3>
<p>下载到本地,什么都不要改,放到指定位置。</p>
<p>该资源不会自动拷贝到<code>dist/</code>文件夹下,所以需要通过修改配置文件拷贝。</p>
<pre><code> copy: {
patterns: [
...
{
from: 'src/assets/fonts/',
to: 'dist/assets/fonts/'
},
...
],
options: {
}
}</code></pre>
<p>然后,在<code>app.js</code>入口文件中,<code>import './assets/fonts/iconfont.css'</code>。</p>
<h2>自定义组件</h2>
<h3>组件向外传参</h3>
<pre><code>this.triggerEvent(
'eventType',
{
key: value, //这里定义的键值对,在父组件中,通过args.detail.key获取;
},
{
bubbles: true, //事件属性:是否冒泡;
capturePhase: true, //事件属性: 是否可捕获;
}
)</code></pre>
<h3>插槽slot</h3>
<blockquote>用法同<code>Vue</code>。</blockquote>
<p><strong>注意</strong>:组件内部对<code>slot</code>定义的样式,不起作用。只能在调用组件的位置,对传入<code>slot</code>内的结构进行样式定义。</p>
<h2>开发障碍</h2>
<h3>
<code>Taro</code>中自定义tabBar</h3>
<p>切换Tab时(<code>app.jsx</code>中config.tabBar.custom = true),需要在对应Tab页<code>componentDidShow</code>生命周期中:</p>
<pre><code>if (typeof this.$scope.getTabBar === 'function' && this.$scope.getTabBar()) {
this.$scope.getTabBar().setData({
selected: 1
})
}</code></pre>
<p>注意是<strong>this.$scope.getTabBar</strong>。</p>
<h3>Canvas引起的层级覆盖问题</h3>
<blockquote>
<code>canvas</code>是由客户端创建的原生组件,而原生组件的层级是最高的,所以页面中的其他组件无论设置 <code>z-index</code> 为多少,都无法盖在原生组件上。</blockquote>
<p>所以,如果<code>canvas</code>和遮罩交互同时存在时,<code>canvas</code>会在遮罩的上层。</p>
<p>解决方案:</p>
<ul>
<li>在<code>canvas</code>外包裹一层结构,通过条件(遮罩的开关)来设置<code>canvas</code>容器的<code>hidden</code>属性。</li>
<li>
<p>通过<code>cover-view</code>、<code>cover-image</code>自定义组件,<code>cover-view</code>通过定位,提升层级,可以防止被<code>canvas</code>覆盖。</p>
<ul>
<li>因为后插入的原生组件可以覆盖之前的原生组件,所以,要注意:<strong>结构上,<code>cover-view</code>一定要在<code>canvas</code>后边</strong>;</li>
<li>可以通过<code>flex</code>和<code>order</code>来调整展示顺序。</li>
<li>只有<strong>最外层</strong><code>cover-view</code>才支持<code>position: fixed</code>。</li>
</ul>
</li>
</ul>
<h3><code>typeof</code></h3>
<p><code>wx:if</code>语句中,不能使用<code>typeof</code>运算符,<code>{{}}</code>中不能使用<code>typeof</code>运算符,只能在<code>wxs</code>中使用。</p>
<h3>data初始化赋值</h3>
<p>不知道data什么时机初始化,但,初始化<code>data</code>的时候,不能使用<code>this</code>指向当前组件实例(这是<code>this === void 0</code>),也就是说,<code>data</code>初始化只能给一个<strong>常量</strong>。</p>
<p>需要<code>properties</code>或<code>methods</code>来初始化<code>data</code>的时候,只能在生命周期<code>attached</code>中通过<code>this.setData</code>更新<code>data</code>的值。</p>
<p>而且,如果<code>data.fn = this.methodName</code>,<code>methodName</code>中如果调用了<code>this</code>引用,这时<code>this</code>指向的是<code>data</code>,所以需要使用<code>data.fn = this.methodName.bind(this)</code>。</p>
<h3>vant-weapp库中的popup样式设置</h3>
<p><code>popup</code>内容的大小不是由内容撑起来的,需要通过<code>popup</code>组件的<code>custom-class</code>定义一个类名,设置<code>width</code>、<code>height</code>来定义内容的尺寸。</p>
<h3>vant-tree-select事件触发</h3>
<p>在<code>Taro</code>中的代码风格类<code>React</code>,而<code>vant-weapp</code>库中的代码风格为<code>wxml</code>和<code>wxs</code>风格。<code>React</code>绑定事件是驼峰式,<code>wxml</code>绑定事件是使用<code>-</code>连字符分隔。</p>
<p>这就造成了<code>Taro</code>使用<code>vant-tree-select</code>组件时,<code>onClickNav</code>和<code>onClickItem</code>不会被<code>vant-tree-select</code>识别,事件无法触发。</p>
<p><strong>解决方案</strong>:对<code>vant-tree-select</code>进行二次封装,事件原始触发通过<code>this.$triggerEvent</code>传出驼峰式的事件类型,在<code>Taro</code>中调用。</p>
<hr>
<p>目前<code>vant-weapp0.5.20</code>中,<code>vant-tree-select</code>不支持单选。</p>
<h3>props获取不到</h3>
<h3>驼峰式命名的事件无法触发[微信小程序]</h3>
<p>注意<code>@tarojs/cli</code>的<strong>版本</strong>,如最初用的<code>1.2.0</code>版本就获取不到自定义组件传的参数,升级到最新版<code>1.3.15</code>就可以了。</p>
<p>注意<code>@tarojs/cli</code>的<strong>版本</strong>,如最初用的<code>1.2.0</code>版本无法触发驼峰式命名的事件,升级到最新版<code>1.3.15</code>,使用<code>onClick-nav</code>形式绑定事件就可以了。</p>
<h3>
<code>Taro</code>编译器无法自动将用到组件的<code>.wxs</code>文件移动到<code>/dist</code>相应目录下</h3>
<p>手动移动。</p>
<h3>在<code>微信开发者工具</code>中运行<code>Taro</code>代码,如果有<code>async/await</code>,则regenerator is not defined。</h3>
<p>将<code>微信开发者工具</code>--> 右上角<code>详情</code>--> <code>本地设置</code>里的配置全部关掉,如<code>ES6转ES5</code>...。</p>
<h3>定制echarts,引入报错</h3>
<p><code>echarts.js</code>不需要再次编译,配置中新增编译时忽略<code>echarts.js</code>。</p>
<pre><code>weapp: {
...
compile: {
exclude: ['src/echarts-for-weixin/ec-canvas/echarts.js']
}
}</code></pre>
<h3>getState()获取Store存储的数据</h3>
<p>可以在<code>(dispatch, getState) => {</code>中使用。</p>
<h3>真机调试正常,预览/体验版空白(只有tabbar)</h3>
<p>将"本地设置"--> "上传时进行代码保护"取消勾选。</p>
<h3>
<code>Taro</code>中<code>className=''</code>单引号赋值不起作用。</h3>
<p><code>className</code>的值使用双引号包裹。</p>
<h3>
<code>Taro</code>自定义组件内部使用<code>iconfont</code>,不显示图标</h3>
<p>参照<a href="https://link.segmentfault.com/?enc=lNd1cwhPJGUUCLc2dLUpgw%3D%3D.3Amjxx7rUDGEktFrYU%2Fw2Tpw76ZA2pZKAfWxj%2BfgCtR8P9IG8rmDUxR%2Fm9Nw%2BFSTVale2Ci1Kuymxq9EhUO%2BWw%3D%3D" rel="nofollow">外部样式类、全局样式类</a>。<br>或者,组件单独引入<code>iconfont.css</code>也可以。</p>
<h3>获取路由参数</h3>
<p><code>this.$router.params</code></p>
<h3>
<code>iconfont</code>字符串渲染</h3>
<p>如果将字体做变量使用,通用情况下无法正常显示。</p>
<ul>
<li>需要将<code>icon: ['&#xe61d;', '&#xe62c;']</code>改写成<code>icon: ['\ue61e', '\ue62d']</code>。</li>
<li><code><rich-text nodes={&#xe61e;}></rich-text></code></li>
</ul>
<h3>使用Taro/微信小程序同步接口,仍异步返回结果</h3>
<p>如使用<code>Taro.getStorageSync('key')</code>获取缓存数据,结果仍是异步返回。同步接口需要结合<code>await</code>使用,才是真正的同步。</p>
<h2>分包</h2>
<h3>包大小限制</h3>
<ul><li>包超过2048KB,无法上传</li></ul>
<h3>分包操作</h3>
<ul>
<li>
<p>主包不需要特殊处理。</p>
<ul><li>navigateTab导航的页面必须在主包中。</li></ul>
</li>
<li>
<p>分包</p>
<ul><li>分包在<code>subPackages</code>配置。</li></ul>
</li>
</ul>
<pre><code> pages: [
'pages/login/login',
'pages/index/index',
'pages/manage/manage',
'pages/schedule/schedule',
'pages/individual/individual'
],
'subPackages': [
{
'root': 'pages-main',
name: 'main',
'pages': [
'acs/acs',
'acs-setting/acs-setting',
'setting-details/setting-details',
'current-energy/current-energy',
'history-energy/history-energy',
'electricity/electricity',
'runtime/runtime',
'daily-usage/daily-usage',
'onshift-record/onshift-record',
'schedule-details/schedule-details'
]
},
],</code></pre>
<h3>伪动态绑定事件</h3>
<pre><code>// index.wxml
<input
wx:if="{{metas.type == 'text' || metas.type == 'number' || metas.type == 'idcard' || metas.type == 'digit'}}"
name="{{metas.name}}"
type="{{metas.type}}"
value="{{value}}"
placeholder="{{metas.attrs.placeholder}}"
bindchange="{{changeValidate}}"
bindinput="{{inputValidate}}"
bindblur="{{blurValidate}}"
/></code></pre>
<pre><code>// index.js
Component({
data: {
changeValidate: '',
inputValidate: '',
blurValidate: '',
eventType: 'input',
rules: '',
value: '',
isRequired: false,
validateState: '', //['validating', 'success', 'error']
validateMessage: ''
},
observers: {
rules(newV) {
console.log('------=======')
this.setData({
[`${this.data.eventType}Validate`]: 'onBlurValidate'
})
}
},
methods: {
onBlurValidate (e) {
this.onValidate(e, rule)
},
onValidate (e, rule) {
}
}
})</code></pre>
<h3>获取组件实例</h3>
<pre><code>refFormItem = (node, idx) => {
if(this.formItem) {
!this.formItem.includes(node) && this.formItem.push(node)
} else {
this.formItem = [node]
}
}
...
clearValidate () {
console.log(this.formItem)
this.formItem.forEach(item => {
item.clearValidate()
})
}
...
render () {
const { fieldMetas } = this.props;
return (
<Form className="form" onSubmit={this.submitForm.bind(this)}>
{
fieldMetas.map((meta, idx) => {
return (
<form-item ref\={this.refFormItem} onValidate={this.gatherValidate.bind(this)} taroKey={meta.name} metas={meta} initialValue={this.findValue.call(this, meta.name)}>
</form-item>
)
})
}
<Button form-type="submit">按钮</Button>
<Button onClick={this.clearValidate}>按钮</Button>
</Form>
)
}</code></pre>
<h3>styleIsolation: "apply-shared"</h3>
<p>对于<code>options.styleIsolation = "apply-shared"</code>的应用:<br>如果是组件包裹组件,内部组件设置该配置,外部组件的样式依旧无法影响内部组件,Page()或Component()注册的<strong>页面级的样式才能影响到组件内部样式</strong>。</p>
实战Vue简易项目(5)模拟数据
https://segmentfault.com/a/1190000015893514
2018-08-06T08:00:00+08:00
2018-08-06T08:00:00+08:00
米花儿团儿
https://segmentfault.com/u/mihuartuanr
18
<p>关于模拟数据,这里使用<code>Mock.js</code>这个库,关于用法,官网说的也比较详细,这里我就简单的带一下。</p>
<h2>列表数据</h2>
<p>我们先将项目中<code>src/components/HelloWorld.vue</code>删除,将<code>src/router/index.js</code>作如下修改:</p>
<pre><code>import Vue from 'vue'
import Router from 'vue-router'
import Index from '@/views/vacation/'
Vue.use(Router)
export default new Router({
routes: [
{
path: '/',
name: 'Index',
component: Index
}
]
})</code></pre>
<p>然后,在<code>src/views/vacation/</code>建立<code>index.vue</code>:</p>
<pre><code><template>
<div>list view</div>
</template></code></pre>
<h3>显示效果</h3>
<p>在手机模式下,显示效果如下:</p>
<p><img src="/img/bVbeQFx?w=323&h=574" alt="列表初始页面" title="列表初始页面"></p>
<h3>模拟数据</h3>
<p>在项目根目录下,使用命令行<code>npm i -D mockjs</code>;</p>
<p><strong>新建<code>src/mock/list.js</code></strong>:</p>
<pre><code>import { mock, Random } from "mockjs";
export default mock({
'list|0-50': [
{
'approveId': '@id',
'applier': {
'userId': '@guid',
'userName': '@cname',
'sectionId': '@id',
'sectionName': '@ctitle',
}
...
}
]
})</code></pre>
<ul>
<li>这里的<code>'@id'</code>(称为“占位符”)是<code>Random.id()</code>的简写形式;</li>
<li>这里的<code>'@id'</code>(称为“占位符”)必须使用引号包裹;</li>
<li>这里的<code>'@id' + 111</code>会是将<code>'@id'</code>当作字符串(返回<code>'@id111'</code>),不等于<code>Random.id() + 111</code>;</li>
</ul>
<p><strong>新建<code>src/mock/index.js</code></strong>:</p>
<p><code>Mock.js</code>拦截请求地址:</p>
<pre><code>import { mock, Random } from "mockjs";
import List from "./list";
mock('\/','get',()=> List);</code></pre>
<ul>
<li>在这里,使用<code>Mock.mock( rurl?, rtype?, function( options ) )</code>拦截路由请求的<code>/</code>路径,返回模拟的<code>List</code>列表。</li>
<li>
<p><code>rurl</code>:拦截路径规则,可以是字符串<code>'/'</code>,也可以是一个正则表达式<code>/\//</code>。</p>
<ul>
<li>若请求<code>/?id="1"</code>,<code>mock</code>的拦截路径可以写成<code>Mock.mock(/\/?id=\"\d\"/,'get',()=>List)</code>;</li>
<li>若需要根据请求参数不同,返回对应<code>id</code>的数据,则需要自己截取<code>url</code>字符串作判断了;</li>
</ul>
</li>
<li>
<code>rtype</code>:拦截请求类型,<code>get</code>或<code>post</code>;</li>
<li>
<p><code>function(options)</code>:回调函数,拦截成功后的处理逻辑;</p>
<ul>
<li>
<code>optioins = {url, type, body}</code>;</li>
<li>
<code>url</code>为请求地址;</li>
<li>
<code>type</code>为请求类型;</li>
<li>
<code>body</code>为请求时传入的数据(只在<code>post</code>请求时有用);</li>
</ul>
</li>
</ul>
<h3>状态管理</h3>
<p>这里,我们使用<code>vuex</code>作状态管理,<code>axios</code>请求数据:<code>npm i -S vuex axios</code>;</p>
<p><strong>新建<code>src/store/index.js</code></strong>:</p>
<pre><code>import Vue from 'vue';
import Vuex from 'vuex';
import axios from 'axios';
Vue.use(Vuex);
const $setApplications = 'SETAPPLICATIONS';
export default new Vuex.Store({
state: {
applications: null,
},
mutations: {
[$setApplications]: (state, list) => state.applications = list,
},
actions: {
requestApplications({ commit, state }) {
axios.get('/')
.then(({data:{list}}) => {
commit($setApplications, list);
})
.catch(() => {
console.log(arguments);
})
}
}
})</code></pre>
<ul>
<li>在这里,<code>state</code>保存整个项目公用的状态,<code>mutations</code>进行同步数据处理,<code>actions</code>处理异步请求。</li>
<li>
<code>mutations</code>是唯一修改<code>state</code>的入口,<code>actions</code>要想修改<code>state</code>,需要内部调用一下<code>mutations</code>;</li>
<li>
<p>在项目程序中,通过<code>this.$store.commit('SETAPPLICATIONS',null)</code>修改<code>state</code>的值。</p>
<ul><li>若要传多个值,第二个参数为一个对象(不接受多个参数的传入,最多只接收两个参数);</li></ul>
</li>
<li>
<p>在项目中,通过<code>this.$store.dispatch('requestApplications')</code>调用一个异步请求。</p>
<ul><li>若需要传参,传第二个参数(不接受多个参数的传入,最多只接收两个参数);</li></ul>
</li>
</ul>
<h3>请求数据</h3>
<p>目前,通过以上步骤,我们独立的构建了模拟数据和状态管理,但还没有将它们结合起来。</p>
<p><code>src/main.js</code>中添加<code>import './mock'</code>、<code>import store from './store'</code>,且修改:</p>
<pre><code>new Vue({
el: '#app',
router,
store,
components: { App },
template: '<App/>'
})</code></pre>
<p>结束了?</p>
<p>还没有,我们还要获取数据:<br>在<code>src/views/vacation/index.vue</code>中添加:</p>
<pre><code><script>
export default {
beforeCreate(){
this.$store.dispatch('requestApplications');
}
}
</script></code></pre>
<p>触发请求。</p>
<h3>请求结果</h3>
<p><img src="/img/bVbeQHP?w=485&h=314" alt="请求结果.png" title="请求结果.png"></p>
<h2>Mock.js用法</h2>
<p>如果想了解<code>Mock.js</code>的用法,推荐看官网的<a href="https://link.segmentfault.com/?enc=%2F5hX8yQJHcxy06mPtQDfIg%3D%3D.TCSZmp7%2B%2Fr5t30C0BmtH%2F2FTgGg7G2lhu%2F2oJZWIyYCkTVHuIhBRrLQxGX%2BXL03t" rel="nofollow">"文档"页</a>,而不是"示例"页。</p>
<p><code>Mock.js</code>返回的数据格式都是对象,如果想获取其它格式(如数组...)需要自己另辟蹊径了。</p>
<h3>规则</h3>
<p><strong>格式:</strong></p>
<pre><code>Mock.mock({
// 初始化对象,也是输出的对象;
})</code></pre>
<p><strong>语法规范:</strong></p>
<blockquote>数据模板中的每个属性由 3 部分构成:属性名、生成规则、属性值:<p>// 属性名 name<br>// 生成规则 rule<br>// 属性值 value<br>'name|rule': value</p>
</blockquote>
<hr>
<blockquote>属性名 和 生成规则 之间用竖线 | 分隔(千万不要带空格吖,否则,你的属性名可能会包含空格)。<br>生成规则 是可选的。<br>生成规则 有 7 种格式:<br>'name|min-max': value<br>'name|count': value<br>'name|min-max.dmin-dmax': value<br>'name|min-max.dcount': value<br>'name|count.dmin-dmax': value<br>'name|count.dcount': value<br>'name|+step': value<br>生成规则 的 含义 需要依赖 属性值的类型 才能确定。<br>属性值 中可以含有 @占位符。<br>属性值 还指定了最终值的初始值和类型。</blockquote>
<h3>验证</h3>
<p>如果你想验证写出来的模拟数据是否正确,可以在<a href="https://link.segmentfault.com/?enc=Ex%2FqB0jNW5EoQcrCGI%2FseQ%3D%3D.o1HJeSDXHUAaVLMNQMRzCsxxrzzSOOPBMiBJIVpFTgQ%3D" rel="nofollow">“示例”页</a>打开控制台,直接运行。</p>
<pre><code>Mock.mock({
'list|1-10':[
Random.id(),
]
})</code></pre>
<h3>测试结果</h3>
<p><img src="/img/bVbeQDW?w=1096&h=495" alt="mock验证" title="mock验证"></p>
<h2><a href="https://link.segmentfault.com/?enc=QGEMJIRvff0vhznHPhweXw%3D%3D.7vlHrJn9jeI9%2F%2B1%2FqHV6Yjzlvvy0jj0AF1AYPW0GifOp7Gh8u7FU05Bf49EIMQrUS2LkVCkvNtM8NkJN%2Fmck%2Fr4acyzkigmpHiCwUl%2FmUhXEd35SaSq6ktrgeWmNMgIk" rel="nofollow">vue-devtools</a></h2>
<p><a href="https://link.segmentfault.com/?enc=MPPadSRu1WTed%2BGtNfKo8w%3D%3D.UduzLEVgLlbVu5VllKdTHIf5q5b9MWoT1xuvGPI2eqaDf3MDfLk0SxQfO1WbJ6xlKbXfHxFV5ss3zoXi3eHRrA%3D%3D" rel="nofollow">安装地址</a></p>
<h3>使用提醒</h3>
<ul>
<li>
<code>$vm0</code>指向某一组件实例,该实例必须打开控制台的<code>Vue</code>Tab页,点击某一组件时才能获取到,否则,汇报<code>$vm0未定义</code>。</li>
<li>点击哪个组件,<code>$vm0</code>指向哪个组件,才能获取到该组件上的属性。</li>
</ul>
<p><img src="/img/bVbeQMG?w=660&h=601" alt="使用提醒" title="使用提醒"></p>
<h2>章节回顾</h2>
<ul>
<li>知道如何模拟数据了吧,接下来我要偷偷的模拟列表的数据了呢,你也不要忘了。</li>
<li>如何使用<code>Mock.js</code>拦截请求呢,如何获得请求时的数据呢?</li>
<li>如何通过<code>axios</code>请求数据呢,它和<code>mutations</code>有何区别?</li>
</ul>
<h2>思考</h2>
<ul><li>懒货一枚,选择第三方列表库,如何在<code>Vue</code>项目中使用呢?</li></ul>
<h2>相关的官方文档</h2>
<p><a href="https://link.segmentfault.com/?enc=Isl2w1mr7WlCvv02v8X8bQ%3D%3D.vddHSIjVQ6%2Fz2iu2jB1D5U93dSOHD5rs%2BCxkT64erC3%2BRx4UXwG9OpJ8H5HfVZdl" rel="nofollow">MockJS使用文档</a></p>
<p><a href="https://link.segmentfault.com/?enc=au6gzkhaKGrEIemBYghyGA%3D%3D.Hq8XXDEC72y4ydVbgihixViwbJ8DKL7X8BisRqMXMuU%3D" rel="nofollow">MockJS示例(可通过控制台测试)</a></p>
<p><a href="https://link.segmentfault.com/?enc=wVC6VLCsoQN4NR1siw%2BymA%3D%3D.baYBJenc6RqvgU8Z3icoOX0U6VMEq4JUSzRl%2FWE87Jt1rMfd0R9jxqaAmnpFap1l" rel="nofollow">Vuex官网</a></p>
<h2>番外</h2>
<p><a href="https://segmentfault.com/a/1190000004974090">Vue-router用法</a></p>