前言

玩过Angular的同学都知道Angular作为一个Framework,拥有一套完备的生态,还集成了强大的CLI。而React则仅仅是一个轻量级的Library,官方社区只定义了一套组件的周期规则,而周边社区可以基于此规则实现自己的组件,React并不会提供给你一套开箱即用的方案,而需要自己在第三方市场挑选满意的组件形成“全家桶”,这也是React社区活跃的原因之一。

最近工作中在考虑使用monorepo对项目进行管理,发现了一套dev toolkit叫做Nx,Nx使用monorepo的方式对项目进行管理,其核心开发者vsavkin同时也是Angular项目的早期核心成员之一,他把Angular CLI这套东西拿到Nx,使其不仅可以支持Angular项目的开发,现在还支持React项目。

Nx支持开发自己的plugin,一个plugin包括schematics和builders(这两个概念也分别来自Angular的schematics以及cli-builders),schematics按字面意思理解就是“纲要”的意思,也就是可以基于一些模板自动化生成所需的文件;而builders就是可以自定义构建流程。

今天要讲的就是如何开发一个属于自己的Nx plugin (包含schematics),我会使用它来自动化创建一个页面组件,同时更新router配置,自动将其加入react router的config。

关于Monorepo

这篇文章不会详细介绍什么是monorepo,mono有“单个”的意思,也就是单个仓库(所有项目放在一个仓库下管理),对应的就是polyrepo,也就是正常一个项目一个仓库。如下图所示:

更多关于monorepo的简介,可以阅读以下文章:

  1. Advantages of monorepos
  2. How to develop React apps like Facebook, Microsoft, and Google
  3. Misconceptions about Monorepos: Monorepo != Monolith

关于Nx plugin

先贴一张脑图,一个一个讲解schematic的相关概念:

前面提到Nx plugin包括了builder(自动化构建)和schematic(自动化项目代码的增删改查)。一个成型的Nx plugin可以使用Nx内置命令执行。

对于文章要介绍的schematics,可以认为它是自动化代码生成脚本,甚至可以作为脚手架生成整个项目结构。

Schematics要实现的目标

Schematics的出现优化了开发者的体验,提升了效率,主要体现在以下几个方面:

  1. 同步式的开发体验,无需知道内部的异步流程
    Schematics的开发“感觉”上是同步的,也就是说每个操作输入都是同步的,但是输出则可能是异步的,不过开发者可以不用关注这个,直到上一个操作的结果完成前,下一个操作都不会执行。
  2. 开发好的schematics具有高扩展性和高重用性
    一个schematic由很多操作步骤组成,只要“步骤”划分合理,扩展只需要往里面新增步骤即可,或者删除原来的步骤。同时,一个完整的schematic也可以看做是一个大步骤,作为另一个schematic的前置或后置步骤,例如要开发一个生成Application的schematic,就可以复用原来的生成Component的schematic,作为其步骤之一。
  3. schematic是原子操作
    传统的一些脚本,当其中一个步骤发生错误,由于之前步骤的更改已经应用到文件系统上,会造成许多“副作用”,需要我们手动FIX。但是schematic对于每项操作都是记录在运行内存中,当其中一项步骤确认无误后,也只会更新其内部创建的一个虚拟文件系统,只有当所有步骤确认无误后,才会一次性更新文件系统,而当其中之一有误时,会撤销之前所做的所有更改,对文件系统不会有“副作用”。

接下来我们了解下和schematic有关的概念。

Schematics的相关概念

在了解相关概念前,先看看Nx生成的初始plugin目录:

your-plugin|--.eslintrc|--builders.json|--collection.json|--jest.config.js|--package.json|--tsconfig.json|--tsconfig.lib.json|--tsconfig.spec.json|--README.md|--src|--builders|--schematics|--your-schema|--your-schema.ts|--your-schema.spec.ts|--schema.json|--schema.d.ts

Collection

Collection包含了一组Schematics,定义在plugin主目录下的collection.json

{"$schema": "../../node_modules/@angular-devkit/schematics/collection-schema.json","name": "your-plugin","version": "0.0.1","schematics": {"your-schema": {"factory": "./src/schematics/your-schema/your-schema","schema": "./src/schematics/your-schema/schema.json","aliases": ["schema1"],"description": "Create foo"}}
}

上面的json文件使用@angular-devkit/schematics下的collection schema来校验格式,其中最重要的是schematics字段,在这里面定义所有自己写的schematics,比如这里定义了一个叫做"your-schema"的schematic,每个schematic下需要声明一个rule factory(关于rule之后介绍),该factory指向一个文件中的默认导出函数,如果不使用默认导出,还可以使用your-schema#foo的格式指定当前文件中导出的foo函数。

aliases声明了当前schematic的别名,除了使用your-schema的名字执行指令外,还可以使用schema1description表示一段可选的描述内容。

schema定义了当前schematic的schema json定义,nx执行该schematic指令时可以读取里面设置的默认选项,进行终端交互提示等等,下面是一份schema.json

{"$schema": "http://json-schema.org/schema","id": "your-schema","title": "Create foo","examples": [{"command": "g your-schema --project=my-app my-foo","description": "Generate foo in apps/my-app/src/my-foo"}],"type": "object","properties": {"project": {"type": "string","description": "The name of the project.","alias": "p","$default": {"$source": "projectName"},"x-prompt": "What is the name of the project for this foo?"},"name": {"type": "string","description": "The name of the schema.","$default": {"$source": "argv","index": 0},"x-prompt": "What name would you like to use for the schema?"},"prop3": {"type": "boolean","description": "prop3 description","default": true}},"required": ["name", "project"]
}

properties表示schematic指令执行时的选项,第一个选项project表示项目名,别名p,使用$default表示Angular内置的一些操作,例如$source: projectName则表示如果没有声明project,会使用Angular workspaceSchema(nx中为workspace.json)中的defaultProject选项,而第二个选项的$default则表明使用命令时的第一个参数作为name

x-prompt会在用户不键入选项值时的交互,用来提示用户输入,用户可以不用预先知道所有选项也能完成操作,更复杂的x-prompt配置请查阅官网。

说了这么多,以下是几个直观交互的例子,帮助大家理解:

nx使用generate选项来调用plugin中的schematic或者builder,和Angular的ng generate一致:

# 表示在 apps/app1/src/ 下生成一个名为bar的文件$ nx g your-plugin:your-schema bar -p=app1
# 或者
$ nx g your-plugin:your-schema -name=bar -project app1

如果使用交互(不键入选项)

# 表示在 apps/app1/src/ 下生成一个名为bar的文件$ nx g your-plugin:your-schema
? What is the name of the project for this foo?
$ app1
? What name would you like to use for the schema?
$ bar

接下来看看Schematics的两个核心概念:TreeRule

Tree

根据官方对Tree的介绍:

The virtual file system is represented by a Tree. The Tree data structure contains a base (a set of files that already exists) and a staging area (a list of changes to be applied to the base). When making modifications, you don't actually change the base, but add those modifications to the staging area.

Tree这一结构包含了两个部分:VFS和Staging area,VFS是当前文件系统的一个虚拟结构,Staging area则存放schematics中所做的更改。值得注意的是,当做出更改时,并不是对文件系统的及时更改,而只是将这些操作放在Staging area,之后会把更改逐步同步到VFS,知道确认无误后,才会一次性对文件系统做出变更。

Rule

A Rule object defines a function that takes a Tree, applies transformations, and returns a new Tree. The main file for a schematic, index.ts, defines a set of rules that implement the schematic's logic.

Rule是一个函数,接收TreeContext作为参数,返回一个新的Tree,在schematics的主文件index.ts中,可以定义一系列的Rule,最后将这些Rule作为一个综合的Rule在主函数中返回,就完成了一个schematic。下面是Tree的完整定义:

export declare type Rule = (tree: Tree, context: SchematicContext) => Tree | Observable<Tree> | Rule | Promise<void> | Promise<Rule> | void;

来看看一个简单的schematic主函数,我们在函数中返回一个RuleRule的操作是新建一个默认名为hello的文件,文件中包含一个字符串world,最后将这个Tree返回。

// src/schematics/your-schema/index.tsimport { Rule, SchematicContext, Tree } from '@angular-devkit/schematics';// You don't have to export the function as default. You can also have more than one rule factory
// per file.
export function myComponent(options: any): Rule {return (tree: Tree, _context: SchematicContext) => {tree.create(options.name || 'hello', 'world');return tree;};
}

Context

最后是Context,上面已经提到过,对于Schematics,是在一个名叫SchematicContext的Context下执行,其中包含了一些默认的工具,例如context.logger,我们可以使用其打印一些终端信息。

如何开发一个Nx Schematic?

下面的所有代码均可以在我的GitHub里下载查看,觉得不错的话,欢迎大家star。

接下来进入正题,我们开发一个nx plugin schematic,使用它来创建我们的页面组件,同时更新路由配置。

假设我们的项目目录结构如下:

apps|...|--my-blog|...|--src|--components|--pages|--home|--index.ts|--index.scss|--about|--routers|--config.ts|--index.ts|...

router/config.ts文件内容如下:

export const routers = {// 首页'/': 'home',// 个人主页'/about': 'about'
};

现在我们要新增一个博客页,不少同学可能就直接新建一个目录,复制首页代码,最后手动添加一条路由配置,对于这个例子倒是还好,但是如果需要更改的地方很多,就很浪费时间了,学习了Nx plugin schematics,这一切都可以用Schematic实现。

搭建Nx环境并使用Nx默认的Schematic创建一个plugin

如果之前已经有了Nx项目,则直接在项目根目录下使用以下命令创建一个plugin:

$ nx g @nrwl/nx-plugin:plugin [pluginName]

如果是刚使用Nx,也可以使用下面的命令快速新建一个项目,并自动添加一个plugin:

$ npx create-nx-plugin my-org --pluginName my-plugin

设置好Schematic选项定义

现在Nx为我们创建了一个默认的plugin,首先更改packages/plugin/collection.json,为schema取名叫做“page”

{"$schema": "../../node_modules/@angular-devkit/schematics/collection-schema.json","name": "plugin","version": "0.0.1","schematics": {"page": {"factory": "./src/schematics/page/page","schema": "./src/schematics/page/schema.json","description": "Create page component"}}
}

接下来定义我们提供的schema option,这里需要修改src/schematics/page/schema.jsonsrc/schematics/page/schema.d.ts,前者作为JSON Schema被Nx plugin使用,后者作为类型定义,开发时用到。

对于page,我们需要提供两个必须选项:name和对应的project,两个可选选项:connect(是否connect to redux)、classComponent(使用类组件还是函数组件)。

下面分别是schema.jsonschema.d.ts

{"$schema": "http://json-schema.org/draft-07/schema","id": "page","title": "Create page component","type": "object","properties": {"name": {"type": "string","description": "The name of the page component","$default": {"$source": "argv","index": 0},"x-prompt": "What name would you like to use?"},"project": {"type": "string","description": "The project of the page component","$default": {"$source": "projectName"},"alias": "p","x-prompt": "Which projcet would you like to add to?"},"classComponent": {"type": "boolean","alias": "C","description": "Use class components instead of functional component.","default": false},"connect": {"type": "boolean","alias": "c","description": "Create a connected redux component","default": false}},"required": ["name", "project"]
}
export interface PageSchematicSchema {name: string;project: string;classComponent: boolean;connected: boolean;
}

开发Schematic

创建所需模板文件

模板文件就是通过一些模板变量来生成真正的文件。每一个页面默认有两个文件,index.tsindex.scss,因此创建模板文件如下:

index.ts.template

<% if (classComponent) { %>
import React, { Component } from 'react';
<% } else { %>
import React from 'react';
<% } %>
<% if (connect) { %>
import { connect } from 'react-redux';
import { IRootState, Dispatch } from '../../store';
<% } %>import { RouteComponentProps } from 'react-router-dom';
import './index.scss';<% if (connect) { %>
type StateProps = ReturnType<typeof mapState>;
type DispatchProps = ReturnType<typeof mapDispatch>;
type Props = StateProps & DispatchProps & RouteComponentProps;
<% } else { %>
type Props = RouteComponentProps;
<% } %><% if (classComponent) { %>
class <%= componentName %> extends Component<Props> {render() {return (<div className="<% className %>">Welcome to <%= componentName %>!</div>);}
}
<% } else { %>
const <%= componentName %> = (props: Props) => {return (<div className="<%= className %>">Welcome to <%= componentName %>!</div>);
};
<% } %><% if (connect) { %>
function mapState(state: IRootState) {return {}
}function mapDispatch(dispatch: Dispatch) {return {}
}
<% } %><% if (connect) { %>
export default connect<StateProps, DispatchProps, {}>(mapState, mapDispatch)(<%= componentName %>);
<% } else { %>
export default <%= componentName %>;
<% } %>

index.scss.template

.<%= className %> {}

我们将模板文件放到src/schematics/page/files/下。

基于模板文件创建所需文件和目录

我们一共需要做四件事:

  1. 格式化选项(把schematic默认的选项进行加工,加工成我们所需的全部选项)。
  2. 基于模板文件创建所需文件和目录。
  3. 更新app/src/routers/config.ts
  4. 使用eslint格式化排版。

先来实现1和2:

page.ts:

import { PageSchematicSchema } from './schema';
import { names } from '@nrwl/workspace';
import { getProjectConfig } from '@nrwl/workspace/src/utils/ast-utils';interface NormalizedSchema extends PageSchematicSchema {/** element className */className: string;componentName: string;fileName: string;projectSourceRoot: Path;
}/** 加工选项 */
function normalizeOptions(host: Tree,options: PageSchematicSchema
): NormalizedSchema {const { name, project } = options;const { sourceRoot: projectSourceRoot } = getProjectConfig(host, project);// kebab-case fileName and UpperCamelCase classNameconst { fileName, className } = names(name);return {...options,// element classNameclassName: `${project}-${fileName}`,projectSourceRoot,componentName: className,fileName,};
}

接下来使用模板文件:

page.ts

import { join } from '@angular-devkit/core';
import {Rule,SchematicContext,mergeWith,apply,url,move,applyTemplates,
} from '@angular-devkit/schematics';
import { PageSchematicSchema } from './schema';
import { names } from '@nrwl/workspace';
import { getProjectConfig } from '@nrwl/workspace/src/utils/ast-utils';interface NormalizedSchema extends PageSchematicSchema {/** element className */className: string;componentName: string;fileName: string;projectSourceRoot: Path;
}/** 基于模板创建文件 */
function createFiles(options: NormalizedSchema): Rule {const { projectSourceRoot, fileName } = options;const targetDir = join(projectSourceRoot, pagePath, fileName);return mergeWith(apply(url('./files'), [applyTemplates(options), move(targetDir)]));
}

原理是使用Angular Schematics自带的mergeWithRule,接收一个SourceSource的定义如下:

A source is a function that generates a Tree from a specific context.

也就是说Source()会生成一棵新的Tree。然后将其和原来的Tree合并。

由于我们需要从模板文件中加载,首先需要使用url加载文件,url接收文件或文件夹的相对地址,返回一个Source,然后我们使用applyurl加载模板文件后的Source进行加工,apply接收一个Source和一个Rule的数组,将Rule[]应用后返回一个新的Source

这里我们需要进行两种“加工”,首先使用options替换模板文件中的变量,最后将这些文件使用move移动到对应的目录下即可。

更新router config

来到了最重要也是比较难的一个步骤,我们还需要修改src/routers/config.ts中的routers变量,在里面增加我们刚加上的page component。

由于这里是TS文件,所以需要分析TS的AST (Abstract Syntax Tree),然后修改AST,最后使用修改的AST对原来内容进行覆盖即可。

修改AST可以使用TS官方的Compiler API结合TypeScript AST Viewer进行。不过由于AST的复杂结构,TS Compiler API也不太友好,直接使用API对AST进行操作非常困难。例如AST的每个节点都有position信息,做一个新的插入时,还需要对position进行计算,API并没有人性化的操作方式。

由于上面的原因,我最终选择了ts-morph,ts-morph以前也叫做ts-simple-ast,它封装了TS Compiler API,让操作AST变得简单易懂。

看代码之前,我们先使用TS AST Viewer分析一下routers/config.ts这段代码的AST:

export const routers = {// 首页'/': 'home',// 第二页'/about': 'about'
};

AST如下(只含根节点信息):

我们来层层分析:

  1. 从声明到赋值,整段语句作为Variable Statement
  2. 由于routers是被导出的,包含了ExportKeyword
  3. routers = xxx作为VariableDeclarationList中的唯一一个VariableDeclaration
  4. 最后是Identifier“routers”,再到字面量表达式作为它的value。
  5. ...

由于下面代码用到了Initializer,上述的对象字面量表达式ObjectLiteralExpression就是routers这个VariableDeclarationInitializer

看懂AST后,更新router后的代码就容易理解了:

import { join, Path } from '@angular-devkit/core';
import {Rule,Tree,chain,SchematicContext,mergeWith,apply,url,move,applyTemplates,
} from '@angular-devkit/schematics';
import { PageSchematicSchema } from './schema';
import { formatFiles, names } from '@nrwl/workspace';
import { getProjectConfig } from '@nrwl/workspace/src/utils/ast-utils';
import { Project } from 'ts-morph';/** 更新路由配置 */
function updateRouterConfig(options: NormalizedSchema): Rule {return (host: Tree, context: SchematicContext) => {const { projectSourceRoot, fileName } = options;const filePath = join(projectSourceRoot, routerConfigPath);const srcContent = host.read(filePath).toString('utf-8');// 使用ts-morph的project对AST进行操作const project = new Project();const srcFile = project.createSourceFile(filePath, srcContent, {overwrite: true,});try {// 根据变量标识符拿到对应的VariableDeclarationconst decl = srcFile.getVariableDeclarationOrThrow(routerConfigVariableName);// 获取initializer并转换成stringconst initializer = decl.getInitializer().getText();// 使用正则匹配对象字面量的最后一部分并做插入const newInitializer = initializer.replace(/,?s*}$/,`,'/${fileName}': '${fileName}' }`);// 更新initializerdecl.setInitializer(newInitializer);// 获取最新的TS文件内容对源文件进行覆盖host.overwrite(filePath, srcFile.getFullText());} catch (e) {context.logger.error(e.message);}};
}

在如何对Initializer进行操作时,我最开始想到的是将其使用JSON.parse()转换成对象字面量,然后进行简单追加,后面发现这段内容里还可能包含注释,所以只能通过正则匹配确定字面量的“尾部部分”,然后进行匹配追加。

使用eslint做好排版

操作完成后我们可以使用Nx workspace提供的formatFiles将所有文件排版有序。最后我们只需要在默认导出函数里将上述Rule通过chain这个Rule进行汇总。来看看最终代码:

import { join, Path } from '@angular-devkit/core';
import {Rule,Tree,chain,SchematicContext,mergeWith,apply,url,move,applyTemplates,
} from '@angular-devkit/schematics';
import { PageSchematicSchema } from './schema';
import { formatFiles, names } from '@nrwl/workspace';
import { getProjectConfig } from '@nrwl/workspace/src/utils/ast-utils';
import { Project } from 'ts-morph';interface NormalizedSchema extends PageSchematicSchema {/** element className */className: string;componentName: string;fileName: string;projectSourceRoot: Path;
}// 页面组件目录
const pagePath = 'pages';
// 路由配置目录
const routerConfigPath = 'routers/config.ts';
// 路由配置文件中需要修改的变量名
const routerConfigVariableName = 'routers';/** 加工选项 */
function normalizeOptions(host: Tree,options: PageSchematicSchema
): NormalizedSchema {const { name, project } = options;const { sourceRoot: projectSourceRoot } = getProjectConfig(host, project);// kebab-case fileName and UpperCamelCase classNameconst { fileName, className } = names(name);return {...options,// element classNameclassName: `${project}-${fileName}`,projectSourceRoot,componentName: className,fileName,};
}/** 基于模板创建文件 */
function createFiles(options: NormalizedSchema): Rule {const { projectSourceRoot, fileName } = options;const targetDir = join(projectSourceRoot, pagePath, fileName);return mergeWith(apply(url('./files'), [applyTemplates(options), move(targetDir)]));
}/** 更新路由配置 */
function updateRouterConfig(options: NormalizedSchema): Rule {return (host: Tree, context: SchematicContext) => {const { projectSourceRoot, fileName } = options;const filePath = join(projectSourceRoot, routerConfigPath);const srcContent = host.read(filePath).toString('utf-8');const project = new Project();const srcFile = project.createSourceFile(filePath, srcContent, {overwrite: true,});try {const decl = srcFile.getVariableDeclarationOrThrow(routerConfigVariableName);const initializer = decl.getInitializer().getText();const newInitializer = initializer.replace(/,?s*}$/,`,'/${fileName}': '${fileName}' }`);decl.setInitializer(newInitializer);host.overwrite(filePath, srcFile.getFullText());} catch (e) {context.logger.error(e.message);}};
}// 默认的rule factory
export default function (schema: PageSchematicSchema): Rule {return function (host: Tree, context: SchematicContext) {const options = normalizeOptions(host, schema);return chain([createFiles(options),updateRouterConfig(options),formatFiles({ skipFormat: false }),]);};
}

测试

写好了schematic,别忘了进行测试,测试代码如下:

page.spec.ts

import { Tree, Rule } from '@angular-devkit/schematics';
import { SchematicTestRunner } from '@angular-devkit/schematics/testing';
import { createEmptyWorkspace } from '@nrwl/workspace/testing';
import { join } from 'path';
import { PageSchematicSchema } from './schema';
import { updateWorkspace, names } from '@nrwl/workspace';const testRunner = new SchematicTestRunner('@plugindemo/plugin',join(__dirname, '../../../collection.json')
);export function callRule(rule: Rule, tree: Tree) {return testRunner.callRule(rule, tree).toPromise();
}export async function createFakeApp(tree: Tree, appName: string): Promise<Tree> {const { fileName } = names(appName);const appTree = await callRule(updateWorkspace((workspace) => {workspace.projects.add({name: fileName,root: `apps/${fileName}`,projectType: 'application',sourceRoot: `apps/${fileName}/src`,targets: {},});}),tree);appTree.create('apps/app1/src/routers/config.ts',`export const routers = {// 首页'/': 'home',// 个人主页'/about': 'about'};`);return Promise.resolve(appTree);
}describe('plugin schematic', () => {let appTree: Tree;const options: PageSchematicSchema = { name: 'myPage', project: 'app1' };beforeEach(async () => {appTree = createEmptyWorkspace(Tree.empty());appTree = await createFakeApp(appTree, 'app1');});it('should run successfully', async () => {const tree = await testRunner.runSchematicAsync('page', options, appTree).toPromise();// file existexpect(tree.exists('apps/app1/src/pages/my-page/index.tsx')).toBeTruthy();expect(tree.exists('apps/app1/src/pages/my-page/index.scss')).toBeTruthy();// router modified correctlyconst configContent = tree.readContent('apps/app1/src/routers/config.ts');expect(configContent).toMatch(/,s*'/my-page': 'my-page'/);});
});

测试这块能用的轮子也比较多,我这里简单创建了一个假的App(符合上面说的目录结构),然后进行了一下简单测试。测试可以使用如下指令对plugin中的单个schematic进行测试:

$ nx test plugin --testFile page.spec.ts

如果写的plugin比较复杂,建议再进行一遍end2end测试,Nx对e2e的支持也很好。

发布

最后到了发布环节,使用Nx build之后便可以自行发布了。

$ nx build plugin

上述所有代码均可以在我的GitHub里下载查看,同时代码里还增加了一个真实开发环境下的App-demo,里面将plugin引入了正常的开发流程,更能感受到其带来的便捷性。觉得不错的话,欢迎大家star。

总结

其实要写好这类对文件系统“增删改查”的工具,关键还是要理解文件内容,比如上面的难点就在于理解TS文件的AST。使用ts-morph还可以做很多事情,比如我们每增加一个文件,可能需要在出口index.ts中导出一次,使用ts-morph就一句话的事情:

const exportDeclaration = sourceFile.addExportDeclaration({namedExports: ["MyClass"],moduleSpecifier: "./file",
});

当然,Nx和Angular提供了这一套生态,能用的工具和方法非常多,但是也需要我们耐心查阅,合理使用。目前来说Nx封装的方法没有详细的文档,可能用起来需要直接查阅d.ts文件,没那么方便。

工欲善其事,必先利其器。Happy Coding!

如何写一个脚本语言_如何写一个Nx schematic plugin?相关推荐

  1. python怎么写测试脚本语言_手把手带你,用Python写一个Monkey自动化测试脚本!!!...

    一.为什么需要一个测试脚本? 之前讲解了 Android Monkey 命令的使用方式,今天趁着还热乎就手把手用 Monkey 写一个压力测试的脚本.还不了解什么是 Monkey 的,可以看看之前的文 ...

  2. 如何写一个脚本语言_零基础小白如何学会写文案?文案写作技巧之一:如何写一个吸引读者的文案开头...

    我文笔不好怎样才能够写出优质的文案?很多人都有这个疑问,包括我自己在学习新媒体运营之前,这也是我最大的困惑.现在是内容为王的时代,你的文章质量决定了你的KPI. 后来我学习了文章的写作结构和技巧后,就 ...

  3. python怎么写测试脚本语言_用python编写测试脚本

    1 deff(n):2 """ 3 >>>f(1)4 1用例5 >>>f(2)6 2用例7 ......8 >>>f ...

  4. 写python脚本管理_《写给系统管理员的 Python 脚本编程指南》笔记——第八章 文档和报告...

    本章介绍主题:标准输入和输出 字符串格式化 发送电子邮件 8.1 标准输入和输出 stdin 系统标准输入,stdout 系统标准输出,都是类似文件的对象,可以进行读写.在交互式会话或命令行中运行程序 ...

  5. php 课程节次周次提取,用PHP写一个最简单的解释器Part5(计算器最后一节,下节开始如何写个脚本语言)...

    经过几天的努力,用PHP已经实现了一个完整的基础计算器,如下图 上代码 define('ISINTEGER','ISINTEGER');//定义整数类型描述 define('PLUS','PLUS') ...

  6. 如何实现一个脚本语言?

    [转帖]   燕良 译   实现一个脚本引擎 译者序 由于我最近有一个计划,就是写一个适应性很强的脚本语言,这个语言将主要用来处理剧情,希望能够用于绝大多数需要剧情的游戏.于是最近开始找一些关于scr ...

  7. r语言软件GDINA_finTech MSc代做、代写Python程序语言、代写MSc program、代做Python设计帮做C/C++编程|代写R语言...

    finTech MSc代做.代写Python程序语言.代写MSc program.代做Python设计帮做C/C++编程|代写R语言Strathclyde Business School, finTe ...

  8. 如何写SHELL脚本?尝试自己编写一个简单脚本

    背景 现在多数的服务器都是Linux系统的,需要通过shell来进行操作,而利用shell脚本,可以大大提高开发维护的效率. 知识剖析 什么是shell shell是一个命令行解释器,它为用户提供了一 ...

  9. 如何写shell脚本?尝试自己编写一个简单脚本。

    1 背景介绍 每次发布tomcat项目的时候都要反复敲一些命令,重启tomcat之前先检查tomcat进程有没有停掉,没有还要手动kill该进程,单独部署一个项目还好,如果一次多个部署,就比较费劲了 ...

最新文章

  1. 海门工业机器人_海门凹凸模压标机推荐-无锡胜宝机电
  2. 架构师书单 2nd Edition
  3. 鸿蒙系统开发资金,华为终于动手,将拿出超十亿资金,开发者们有福了
  4. MaxCompute中如何通过logview诊断慢作业
  5. 听说你的模型训练耗时太长?来昇腾开发者沙龙找解决方案
  6. 【SSH】 之 Struts2
  7. 励志生活-英国式选秀带来的启示
  8. R语言介绍及软件安装
  9. 京东把 Elasticsearch 到底用的有多牛?日均5亿订单查询完美解决!
  10. 关于keil5开发stc8等51单片机程序和在线调试的步骤
  11. CAD 开发 渐变填充
  12. shine 插件_《SHINE》正式上线|GBZ将你带入重重的紫色迷雾中
  13. Dell R720服务器安装操作系统
  14. Android仿斗鱼滑动登录验证
  15. ODrive实例 #1 电机配置实例(4250-520KV + TLE5012B-E1000)
  16. [题集]Lecture 4. Leftist Heaps and Skew Heaps
  17. 怎么给java代码瘦身_Eclipse Xtend对Java说:我帮你瘦身
  18. .netCore在Linux容器上的发布
  19. 微信公众号自定义分享标题、图片、链接
  20. 番外.1.Python高级用法

热门文章

  1. 【疯狂的消化之旅】消化系统简介
  2. R语言使用DALEX包的model_performance函数对caret包生成的多个算法模型进行残差分布分析并使用箱图进行残差分布的可视化
  3. pandas基于dataframe字符串数据列不包含特定字符串来筛选dataframe中的数据行(rows where values do not contain substring)
  4. R语言构建回归模型并进行模型诊断(线性关系不满足时)、进行变量变换(Transforming variables)、使用car包中的boxTidwell函数对预测变量进行Box–Tidwell变换
  5. pandas基于时序数据计算模型预测推理需要的统计数据(累计时间、长度变化、变化率、方差、均值、最大、最小等):范围内的统计量、变化率、获得数据集最后的几条数据的统计量、变化率、获得范围内的统计量
  6. Python手动编程实现斐波那契数列
  7. R语言scale_colour_brewer()函数和scale_fill_brewer()函数调色板及填充ggplot2图像实战
  8. 回归分析评估指标均方对数误差(MSLE)详解及其意义:Mean Squared Log Error
  9. 分类(classification)是认知的基础、分类步骤及主要算法、分类与回归的区别、分类过程
  10. 酸奶饮料新产品口味测试研究案例