为什么要自己搭建项目 ?
这个事情起始于前两天给团队写一个脚手架工具,这段时间刚做做完一个系统,是团队第一个正式意义上的全栈项目;团队有自己的前端脚手架,但是没有后端脚手架,所以想着给团队写一个后端脚手架工具,这样团队在开展后续的全栈项目时可以使用脚手架构建项目,避免了重复搭建项目初始架构的大量工作,成员直接编写业务代码即可。
前两天用 JavaScript 完成了脚手架的初版,开发过程中发现没一点代码提示,很不舒服,而且这样的代码不利于后续的迭代和维护。
所以决定用 typescript 重构一遍,但是官方好像没有提供一个合适的脚手架工具,于是就开始自己搭建 typescript 项目;自己搭建最大的好处就是 自主可控,项目中集成了实时编译的开发环境、eslint + prettier 保证代码质量和风格统一、项目构建工具、git 提交信息的规范化,这些都是一个项目最基本和必要的配置。
本来到这里就结束了,但是在后续重构脚手架的过程中发现一个问题,如果每写一个新的 typescript 项目就重复一遍这个搭建流程,比如:今天需要开发一个 npm 包,明天需要开发另外一个脚手架,这好像有点太麻烦了,于是就把整个搭建过程写成了一个脚手架,这样在后续开发的工作中就可以实现一键创建项目,简单、方便、舒爽
从这篇文章中你可以学到什么 ?
mkdir ts-project && cd ts-project && npm init -y && npm i typescript -D && npx tsc --init
这条命令的意思是在当前目录下创建一个 ts-project 目录,然后进入 ts-project 目录执行 npm init -y 初始话目录产生 package.json 文件,之后运行 npm i typescript -D 在开发环境安装 typescript 包,之后执行 npx tsc --init 生成 tsconfig.json 文件
之后所有的操作都以 ts-project 为根目录
mkdir src && touch src/index.ts
新建 src 目录作为项目的源码目录(开发目录),并在 src 目录下创建 index.ts 文件作为项目的入口文件
如果一个目录下存在 tsconfig.json 文件,那就意味着这个目录是 typescirpt 项目的根目录,tsconfig.json 文件中指定了用来编译项目的根文件和编译选项,使用 tsc --init 生成的 tsconfig.json 文件包含了大量的选项,其中大部分都被注释掉了,一般我们只需要配置如下内容即可:
{
"compileOnSave": true,
"compilerOptions": {
"target": "ES2018",
"module": "commonjs",
"moduleResolution": "node",
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"inlineSourceMap":true,
"noImplicitThis": true,
"noUnusedLocals": true,
"stripInternal": true,
"pretty": true,
"declaration": true,
"outDir": "lib",
"baseUrl": "./",
"paths": {
"*": ["src/*"]
}
},
"exclude": [
"lib",
"node_modules"
]
}
npm i @types/node -D
这个是 node.js 的类型定义包
npm i ts-node-dev -D
在 package.json 的 scripts 中增加如下内容
{
"scripts": {
"dev:comment": "启动开发环境",
"dev": "ts-node-dev --respawn --transpile-only src/index.ts"
}
}
执行 npm run dev 即可启动开发环境,并且修改文件时可实时编译
代码质量对于一个系统的可维护性、可迭代性至关重要,特别是在多人协作一个大型项目中,如果没有把控代码质量的工具,每人一套编码风格,这样的系统在后期的维护难度可想而知,基本上会成为一个难以迭代升级的祖传系统,除了重写别无他法。
因此控制代码质量的工具应运而生,而 ESLint 当属其中的佼佼者,熬死了各路的竞争者;typescript 之前还在使用 TSLint,但在 2019 年 1 月 官方决定全面采用 ESLint 作为代码检查工具。
采用社区的开源配置方案 eslint-config-standard,简单直接,足以 hold 住大部分项目了
npx eslint --init
以上流程走完以后在项目根目录会多出来一个 .eslintrc.js 文件,接下来在 package.json 的 scripts 中增加如下配置
{
"scripts": {
"eslint:comment": "使用 ESLint 检查并自动修复 src 目录下所有扩展名为 .ts 的文件",
"eslint": "eslint --fix src --ext .ts --max-warnings=0"
}
}
Perttier 是一个专注于统一代码格式风格的工具,可能有人会疑惑,ESLint 已经能够规范我们的代码,为什么还需要 Prettier ?简单来说是这样的,ESLint 其实有两种类型规则:
其中 格式规则 主要是控制代码风格,简单理解就是代码看起来好看、易读,而 质量规则 主要是发现代码中存在的潜在 bug 或者可能会制造 bug 的地方,简单来说更多是从语法层面去考虑,比如现在禁止使用 var 声明变量,而 prettier 则是专注于 格式规则,所以在格式方面我们选用更加专业的 Prettier。
如果你同时使用 ESLint 和 Prettier,且在编辑器中配置了 Sava Auto Fix 时,会让你的一键格式化异常痛苦,因为在 格式规则 上有冲突,所以个人建议或者说不喜欢在编辑器中配置 ESLint 和 Prettier,三个原因:
接下来就开始安装和配置 Prettier
npm i prettier -D
安装 Prettier 所需的依赖,然后在项目目录增加 .prettierrc.js,推荐配置如下:
module.exports = {
// 一行最多 80 字符
printWidth: 80,
// 使用 2 个空格缩进
tabWidth: 2,
// 不使用 tab 缩进,而使用空格
useTabs: false,
// 行尾需要有分号
semi: true,
// 使用单引号代替双引号
singleQuote: true,
// 对象的 key 仅在必要时用引号
quoteProps: 'as-needed',
// jsx 不使用单引号,而使用双引号
jsxSingleQuote: false,
// 末尾使用逗号
trailingComma: 'all',
// 大括号内的首尾需要空格 { foo: bar }
bracketSpacing: true,
// jsx 标签的反尖括号需要换行
jsxBracketSameLine: false,
// 箭头函数,只有一个参数的时候,也需要括号
arrowParens: 'always',
// 每个文件格式化的范围是文件的全部内容
rangeStart: 0,
rangeEnd: Infinity,
// 不需要写文件开头的 @prettier
requirePragma: false,
// 不需要自动在文件开头插入 @prettier
insertPragma: false,
// 使用默认的折行标准
proseWrap: 'preserve',
// 根据显示样式决定 html 要不要折行
htmlWhitespaceSensitivity: 'css',
// 换行符使用 lf
endOfLine: 'lf'
}
在 package.json 的 scripts 中补充如下内容
{
"scripts": {
"prettier:comment": "自动格式化 src 目录下的所有 .ts 文件",
"prettier": "prettier --write \"src/**/*.ts\""
}
}
如果想在编辑器中配置 ESLint 和 Prettier,具体怎么配 查看 这里,通过 eslint-config-prettier 来解决冲突问题,其作用就是关闭 ESLint 中的格式规则,只使用 Prettier 的格式规则
在系统开发中,如果 git 提交说明精准,在后期的协作以及 bug 处理时会变的有据可查,变相的提高了系统的可维护性,而且可以根据规范的提交说明快速生成开发日志,从而方便开发者或用户追踪项目的开发信息和功能特性。commitizen 是一个实现规范提交说明的工具。
使用 commitizen 在项目中生成符合 AngularJS 规范的提交说明,并初始化 cz-conventional-changelog 适配器
npx commitizen init cz-conventional-changelog --save --save-exact
初始化时主要做了三件事:
内容如下:
{
"devDependencies": {
"cz-conventional-changelog": "^3.3.0"
},
"config": {
"commitizen": {
"path": "./node_modules/cz-conventional-changelog"
}
}
}
接下来安装校验工具,负责校验提交信息是否符合规范
npm i @commitlint/cli @commitlint/config-conventional -D
在项目根目录下新建 commitlint.config.js 并设置校验规则
module.exports = {
extends: ['@commitlint/config-conventional']
};
然后在 package.json 的 scripts 中增加如下内容
{
"scripts": {
"commit:comment": "引导设置规范化的提交信息",
"commit": "cz"
}
}
接下来,就只能使用规范化的提交信息了,如果不知道规范是什么,可在 git add . 之后执行 npm run commit 代替 git commit,会弹出一个列表,引导你一步步的填充符合规范的提交信息,熟练以后亦可用 git commit
注意 以下内容为第 4 版 husky 的使用方式。到目前为止,如果你的项目还没有执行过 git init,即项目没有被 git 管理,则一定要先执行 git init 然后再往后进行,否则后面你需要重新安装一遍 husky
npm i husky@4 lint-staged -D
在 package.json 中添加如下内容
{
"husky": {
"hooks": {
"pre-commit": "lint-staged",
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
}
},
"lint-staged": {
"*.ts": ["npm run eslint", "npm run prettier"]
}
}
之前设置的 ESLint、Prettier 以及 commitizen 到目前为止都只限于开发者手动执行 npm run xx 才能生效,这可不行,因为这一点都不智能,而且万一开发者忘执行命令了怎么办 ?
这时候就需要 husky 和 lint-staged 出场了,上述配置的原理其实就是监听了 git hook 脚本的执行,在特定的命令执行前(pre-commit) 执行相应的命令(lint-staged)。
注意 这部分为 husky@5 的使用方式,和第 4 版不一样,如果还按照第 4 版的方式使用 husky@5 有问题,网上的解决方案也不可行
接下来使用 git commit -m "message" 就会看到 hook 生效了。
因为这个项目主要用于开发一些简单的 typescript 项目,比如项目组需要封装自己的 npm 包,所以就没有集成第三方的构建工具,直接用 typescript 自己的构建能力即可,简单易用,没有学习成本
在 package.json 中添加如下内容
{
"scripts": {
"build:comment": "构建",
"build": "npm run eslint && npm run prettier && rm -rf lib && tsc --build",
}
}
好了,到这里项目就搭建好了,虽然还有一些可优化和扩展的地方,但是用于开发一个简单的 npm 包或者脚手架的项目来说已经足够了,如有需要可以基于以上内容再进行进一步的补充和扩展,希望大家能从里面得到自己需要的内容。
接下来就将上面的整个搭建过程封装成一个脚手架,脚手架的开发就在上面搭建的项目中进行
开发一个脚手架,一般都需要一些工具包的支持,项目中使用到了以下工具包:
上面列出的这些基本上就是目前开发一款脚手架常用的工具,接下来安装项目需要用到的工具包:
npm i commander chalk shelljs inquirer clear-console -S
在项目根目录下的 pacakge.json 中增加如下内容:
{
"bin": {
"ts-cli": "./bin/ts-cli.js"
}
}
bin 表示命令(ts-cli)的可执行文件的位置,接下来在项目根目录执行 npm link,将 package.json 中的属性 bin 的值路径添加全局链接,在命令行中执行 ts-cli 就会执行 ./bin/ts-cli.js 文件
当用户安装带有 bin 字段的包时,如果是全局安装,npm 将会使用符号链接把这些文件链接到/usr/local/node_modules/.bin/(即全局的 node_modules/.bin 中);如果是本地安装,会链接到./node_modules/.bin/。
开发结束可执行 npm unlink ts-cli 去掉 ts-cli 的链接,如果不幸你执行 npm link 命令之后你改变了你的目录名,在 unlink 时会无效,只能手动去全局的 node_modules 中删除对应的软连接
在项目根目录下添加 bin 目录,然后在 bin 目录下新建 ts-cli.js,文件内容如下:
#!/usr/bin/env node
// 将构建目录(lib)下的 index.js 作为脚手架的入口
require('../lib/index')
接下来正式进入开发阶段
这是源码的一个目录结构
import { program } from 'commander';
import create from './order/create';
// ts-cli -v、ts-cli --version
// 临时禁用规则,保证这里可以通过 require 方法获取 package.json 中的版本号
/* eslint-disable @typescript-eslint/no-var-requires */
program
.version(`${require('../package.json').version}`, '-v --version')
.usage('<command> [options]');
// ts-cli create newPro
program
.command('create <app-name>')
.description('Create new project from => ts-cli create yourProjectName')
.action(async (name: string) => {
// 创建命令具体做的事情都在这里,name 是你指定的 newPro
await create(name);
});
program.parse(process.argv);
/**
* create 命令的具体任务
*/
import {
changePackageInfo,
end,
initProjectDir,
installDevEnviroment,
installFeature,
installTSAndInit,
installTypesNode,
isFileExist,
selectFeature,
} from '../utils/create';
// create 命令
export default async function create(projecrName: string): Promise<void> {
// 判断文件是否已经存在
isFileExist(projecrName);
// 选择需要的功能
const feature = await selectFeature();
// 初始化项目目录
initProjectDir(projecrName);
// 改写项目的 package.json 基本信息,比如 name、description
changePackageInfo(projecrName);
// 安装 typescript 并初始化
installTSAndInit();
// 安装 @types/node
installTypesNode();
// 安装开发环境,支持实时编译
installDevEnviroment();
// 安装 feature
installFeature(feature);
// 结束
end(projecrName);
}
/**
* create 命令需要用到的所有方法
*/
import {
getProjectPath,
PackageJSON,
JSON,
printMsg,
readJsonFile,
writeJsonFile,
clearConsole,
} from '../utils/common';
import { existsSync } from 'fs';
import { prompt } from 'inquirer';
import { blue, cyan, gray, red, yellow } from 'chalk';
import * as shell from 'shelljs';
import * as installFeatureMethod from './installFeature';
/**
* 验证当前目录下是否已经存在指定文件,如果存在则退出进行
* @param filename 文件名
*/
export function isFileExist(filename: string): void {
// 文件路径
const file = getProjectPath(filename);
// 验证文件是否已经存在,存在则推出进程
if (existsSync(file)) {
printMsg(red(`${file} 已经存在`));
process.exit(1);
}
}
/**
* 交互式命令行,让用户自己选择需要的功能
* return ['ESLint', 'Prettier', 'CZ']
*/
export async function selectFeature(): Promise<Array<string>> {
// 清空命令行
clearConsole();
// 输出信息
/* eslint-disable @typescript-eslint/no-var-requires */
printMsg(blue(`TS CLI v${require('../../package.json').version}`));
printMsg('Start initializing the project:');
printMsg('');
// 选择功能,这里配合 下面的 installFeature 方法 和 ./installFeature.ts 文件为脚手架提供了良好的扩展机制
// 将来扩展其它功能只需要在 choices 数组中增加配置项,然后在 ./installFeature.ts 文件中增加相应的安装方法即可
const { feature } = await prompt([
{
name: 'feature',
type: 'checkbox',
message: 'Check the features needed for your project',
choices: [
{ name: 'ESLint', value: 'ESLint' },
{ name: 'Prettier', value: 'Prettier' },
{ name: 'CZ', value: 'CZ' },
],
},
]);
return feature as Array<string>;
}
/**
* 初始化项目目录
*/
export function initProjectDir(projectName: string): void {
shell.exec(`mkdir ${projectName}`);
shell.cd(projectName);
shell.exec('npm init -y');
}
/**
* 改写项目中 package.json 的 name、description
*/
export function changePackageInfo(projectName: string): void {
const packageJSON: PackageJSON = readJsonFile<PackageJSON>('./package.json');
packageJSON.name = packageJSON.description = projectName;
writeJsonFile<PackageJSON>('./package.json', packageJSON);
}
/**
* 安装 typescript 并初始化
*/
export function installTSAndInit(): void {
// 安装 typescript 并执行命令 tsc --init 生成 tsconfig.json
shell.exec('npm i typescript -D && npx tsc --init');
// 覆写 tsconfig.json
const tsconfigJson: JSON = {
compileOnSave: true,
compilerOptions: {
target: 'ES2018',
module: 'commonjs',
moduleResolution: 'node',
experimentalDecorators: true,
emitDecoratorMetadata: true,
inlineSourceMap: true,
noImplicitThis: true,
noUnusedLocals: true,
stripInternal: true,
pretty: true,
declaration: true,
outDir: 'lib',
baseUrl: './',
paths: {
'*': ['src/*'],
},
},
exclude: ['lib', 'node_modules'],
};
writeJsonFile<JSON>('./tsconfig.json', tsconfigJson);
// 创建 src 目录和 /src/index.ts
shell.exec('mkdir src && touch src/index.ts');
}
/**
* 安装 @types/node
* 这是 node.js 的类型定义包
*/
export function installTypesNode(): void {
shell.exec('npm i @types/node -D');
}
/**
* 安装开发环境,支持实时编译
*/
export function installDevEnviroment(): void {
shell.exec('npm i ts-node-dev -D');
/**
* 在 package.json 的 scripts 中增加如下内容
* "dev:comment": "启动开发环境",
* "dev": "ts-node-dev --respawn --transpile-only src/index.ts"
*/
const packageJson = readJsonFile<PackageJSON>('./package.json');
packageJson.scripts['dev:comment'] = '启动开发环境';
packageJson.scripts['dev'] =
'ts-node-dev --respawn --transpile-only src/index.ts';
writeJsonFile<PackageJSON>('./package.json', packageJson);
}
/**
* 安装用户选择的功能
* @param feature 功能列表
*/
export function installFeature(feature: Array<string>): void {
feature.forEach((item) => {
const func = (installFeatureMethod[
`install${item}`
] as unknown) as () => void;
func();
});
// 安装 husky 和 lint-staged
installHusky(feature);
// 安装构建工具
installFeatureMethod.installBuild(feature);
}
/**
* 安装 husky 和 lint-staged,并根据功能设置相关命令
* @param feature 用户选择的功能列表
*/
function installHusky(feature: Array<string>): void {
// feature 副本
const featureBak = JSON.parse(JSON.stringify(feature));
// 设置 hook
const hooks = {};
// 判断用户是否选择了 CZ,有则设置 hooks
if (featureBak.includes('CZ')) {
hooks['commit-msg'] = 'commitlint -E HUSKY_GIT_PARAMS';
}
// 设置 lintStaged
const lintStaged: Array<string> = [];
if (featureBak.includes('ESLint')) {
lintStaged.push('eslint');
}
if (featureBak.includes('Prettier')) {
lintStaged.push('prettier');
}
installFeatureMethod.installHusky(hooks, lintStaged);
}
/**
* 整个项目安装结束,给用户提示信息
*/
export function end(projectName: string): void {
printMsg(`Successfully created project ${yellow(projectName)}`);
printMsg('Get started with the following commands:');
printMsg('');
printMsg(`${gray('$')} ${cyan('cd ' + projectName)}`);
printMsg(`${gray('$')} ${cyan('npm run dev')}`);
printMsg('');
}
/**
* 实现各个功能的安装方法
*/
import * as shell from 'shelljs';
import { writeFileSync } from 'fs';
import { PackageJSON, printMsg, readJsonFile, writeJsonFile } from './common';
import { red } from 'chalk';
/**
* 安装 ESLint
*/
export function installESLint(): void {
shell.exec(
'npm i eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin -D',
);
// 添加 .eslintrc.js
const eslintrc = `module.exports = {
"env": {
"es2021": true,
"node": true
},
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 12,
"sourceType": "module"
},
"plugins": [
"@typescript-eslint"
],
"rules": {
}
};
`;
try {
writeFileSync('./.eslintrc.js', eslintrc, { encoding: 'utf-8' });
} catch (err) {
printMsg(`${red('Failed to write .eslintrc.js file content')}`);
printMsg(`${red('Please add the following content in .eslintrc.js')}`);
printMsg(`${red(eslintrc)}`);
}
// 改写 package.json
const packageJson = readJsonFile<PackageJSON>('./package.json');
packageJson.scripts['eslint:comment'] =
'使用 ESLint 检查并自动修复 src 目录下所有扩展名为 .ts 的文件';
packageJson.scripts['eslint'] = 'eslint --fix src --ext .ts --max-warnings=0';
writeJsonFile<PackageJSON>('./package.json', packageJson);
}
/**
* 安装 Prettier
*/
export function installPrettier(): void {
shell.exec('npm i prettier -D');
// 添加 .prettierrc.js
const prettierrc = `module.exports = {
// 一行最多 80 字符
printWidth: 80,
// 使用 2 个空格缩进
tabWidth: 2,
// 不使用 tab 缩进,而使用空格
useTabs: false,
// 行尾需要有分号
semi: true,
// 使用单引号代替双引号
singleQuote: true,
// 对象的 key 仅在必要时用引号
quoteProps: 'as-needed',
// jsx 不使用单引号,而使用双引号
jsxSingleQuote: false,
// 末尾使用逗号
trailingComma: 'all',
// 大括号内的首尾需要空格 { foo: bar }
bracketSpacing: true,
// jsx 标签的反尖括号需要换行
jsxBracketSameLine: false,
// 箭头函数,只有一个参数的时候,也需要括号
arrowParens: 'always',
// 每个文件格式化的范围是文件的全部内容
rangeStart: 0,
rangeEnd: Infinity,
// 不需要写文件开头的 @prettier
requirePragma: false,
// 不需要自动在文件开头插入 @prettier
insertPragma: false,
// 使用默认的折行标准
proseWrap: 'preserve',
// 根据显示样式决定 html 要不要折行
htmlWhitespaceSensitivity: 'css',
// 换行符使用 lf
endOfLine: 'lf'
};
`;
try {
writeFileSync('./.prettierrc.js', prettierrc, { encoding: 'utf-8' });
} catch (err) {
printMsg(`${red('Failed to write .prettierrc.js file content')}`);
printMsg(`${red('Please add the following content in .prettierrc.js')}`);
printMsg(`${red(prettierrc)}`);
}
// 改写 package.json
const packageJson = readJsonFile<PackageJSON>('./package.json');
packageJson.scripts['prettier:comment'] =
'自动格式化 src 目录下的所有 .ts 文件';
packageJson.scripts['prettier'] = 'prettier --write "src/**/*.ts"';
writeJsonFile<PackageJSON>('./package.json', packageJson);
}
/**
* 安装 CZ,规范 git 提交信息
*/
export function installCZ(): void {
shell.exec(
'npx commitizen init cz-conventional-changelog --save --save-exact',
);
shell.exec('npm i @commitlint/cli @commitlint/config-conventional -D');
// 添加 commitlint.config.js
const commitlint = `module.exports = {
extends: ['@commitlint/config-conventional']
};
`;
try {
writeFileSync('./commitlint.config.js', commitlint, { encoding: 'utf-8' });
} catch (err) {
printMsg(`${red('Failed to write commitlint.config.js file content')}`);
printMsg(
`${red('Please add the following content in commitlint.config.js')}`,
);
printMsg(`${red(commitlint)}`);
}
// 改写 package.json
const packageJson = readJsonFile<PackageJSON>('./package.json');
packageJson.scripts['commit:comment'] = '引导设置规范化的提交信息';
packageJson.scripts['commit'] = 'cz';
writeJsonFile<PackageJSON>('./package.json', packageJson);
}
/**
* 安装 husky 和 lint-staged,以实现 git commit 时自动化校验
* @param hooks,需要自动执行的钩子
* @param lintStaged,需要钩子运行的命令
*/
export function installHusky(
hooks: { [key: string]: string },
lintStaged: Array<string>,
): void {
// 初始化 git 仓库
shell.exec('git init');
// 在安装 husky 和 lint-staged
shell.exec('npm i husky lint-staged -D');
// 设置 package.json
const packageJson = readJsonFile<PackageJSON>('./package.json');
packageJson['husky'] = {
hooks: {
'pre-commit': 'lint-staged',
...hooks,
},
};
packageJson['lint-staged'] = {
'*.ts': lintStaged.map((item) => `npm run ${item}`),
};
writeJsonFile<PackageJSON>('./package.json', packageJson);
}
/**
* 安装构建工具,目前主要用于小项目,所以使用 typescript 原生的构建功能即可
*/
export function installBuild(feature: Array<string>): void {
// 设置 package.json
const packageJson = readJsonFile<PackageJSON>('./package.json');
packageJson.scripts['build:comment'] = '构建';
let order = '';
if (feature.includes('ESLint')) {
order += 'npm run eslint';
}
if (feature.includes('Prettier')) {
order += ' && npm run prettier';
}
order += ' && rm -rf lib && tsc --build';
packageJson.scripts['build'] = order;
writeJsonFile<PackageJSON>('./package.json', packageJson);
}
/**
* 放一些通用的工具方法
*/
import { readFileSync, writeFileSync } from 'fs';
import { resolve } from 'path';
import * as clear from 'clear-console';
export interface PackageJSON {
name: string;
version: string;
description: string;
scripts: {
[key: string]: string;
};
}
export interface JSON {
[key: string]: unknown;
}
/**
* 读取指定路径下 json 文件
* @param filename json 文件的路径
*/
export function readJsonFile<T>(filename: string): T {
return JSON.parse(readFileSync(filename, { encoding: 'utf-8', flag: 'r' }));
}
/**
* 覆写指定路径下的 json 文件
* @param filename json 文件的路径
* @param content json 内容
*/
export function writeJsonFile<T>(filename: string, content: T): void {
writeFileSync(filename, JSON.stringify(content, null, 2));
}
/**
* 获取项目绝对路径
* @param projectName 项目名
*/
export function getProjectPath(projectName: string): string {
return resolve(process.cwd(), projectName);
}
/**
* 打印信息
* @param msg 信息
*/
export function printMsg(msg: string): void {
console.log(msg);
}
/**
* 清空命令行
*/
export function clearConsole(): void {
clear();
}
执行 npm run build 进行构建,构建时会进行代码质量和风格的检查,有些问题可以自动修复,有些不行,不行的按照提示手动修复一下即可,然后重新构建
构建完成以后找个测试目录,执行 ts-cli -v 或者 ts-cli --version 查看脚手架的版本
执行 ts-cli create test 可创建一个名为 test 的 typescript 项目
修改 package.json 中的如下内容
{
"name": "@liyongning/ts-cli"
"main": "./lib/index.js",
"keywords": ["typescript", "cli", "typescript 脚手架", "ts 脚手架", "ts-cli", "脚手架"],
"author": "李永宁",
"files": ["package.json", "README.md", "lib"],
"repository": {
"type": "git",
"url": "https://github.com/liyongning/ts-cli.git"
},
}
npm 的账户名、密码就不用说,必不可少
在项目根目录下增加一个发布脚本 publish.sh
#!/bin/bash
echo '开始构建脚手架'
npm run build
echo '脚手架构建完成,现在发布'
npm publish --access public
接下来开始发布,在项目根目录下一次运行如下命令:
npm login
根据提示依次输入相关信息,然后执行下面的命令完成发布
sh publish.sh
登陆 npm 查看
好了,到这里项目就搭建和脚手架的封装就彻底结束了,虽然还有一些可优化和扩展的地方,但是对于开发一个简单的库或者脚手架的项目来说这个脚手架已经完全够用了
如有需要也可以自行去扩展,因为脚手架内置了不错的扩展性,不论是为已有的 create 命令增加新的功能,还是新增一个命令,都很简单,方便你根据自身需要进行二次开发
上一篇:php基础知识
主要参考:https://www.w3school.com.cn
思维导图
思维导图第一版
web网站可以说是互联网的基础。每个网站,可以比喻为一座座房子。宽带网络,就是房子门前的路。url地址,就是房子的门牌标志。HTML代码,就是建造房子的建筑材料(砖头、水泥、钢筋);CSS代码,就是装修房子的装修材料;那么Javascript代码就是这房子的水电了? JS代码则更像是未来世界可以让房子成为变形金刚的智能机器。因此,一些展示“老房子”的浏览器,可能并不支持Javascript。
定义:
HTML(Hyper Text Markup Language),是使用标记标签来描述网页的一种超文本标记语言。
Web 浏览器的作用是读取 HTML 文档,并以网页的形式显示出它们。浏览器不会显示 HTML 标签,而是使用标签来解释页面的内容。HTML定义网页的内容。
CSS(Cascading Style Sheets),指层叠样式表。样式定义如何显示HTML元素,规定网页的布局。
Javascript 则是属于HTML和Web的编程语言,对网页进行编程。
Jquery 是一个Javascript函数库
参考上一篇:php基础知识,安装-集成环境与编辑器
推荐使用 phpstudy + phpstorm
操作步骤:1、在phpstudy 安装目录下,把代码文件放大到根目录www/ 下。
2、浏览器直接访问 localhost/index.html即可看到效果。
HTML元素:是从开始标签(start tag)到结束标签(end tag)的所有代码。
例如:<p>前面这个是开始标签,中间文字是元素内容,后面这个是结束标签</p>
HTML 标签可以拥有属性。属性提供了有关 HTML 元素的更多的信息。
属性总是以名称/值对的形式出现,比如:name="value"。
属性总是在 HTML 元素的开始标签中规定。
常用HTML元素属性:
class :规定元素的类名(classname),一个html文件里面多个标签可以拥有相同的类名。
id :规定元素的唯一 id,一个html文件里面id不能相同。
style :规定元素的行内样式(inline style)
1、标题:标题(Heading)是通过 <h1> - <h6> 等标签进行定义的。<h1> 定义最大的标题。<h6> 定义最小的标题。
2、段落:通过 <p> 标签定义。
3、注释标签 <!-- 与 --> 用于在 HTML 插入注释。
4、链接:<a href="http://www.yummuu.com/">www.yummuu.com</a> 。href 属性规定链接的目标。开始标签和结束标签之间的文字被作为超级链接来显示。
5、图像:<img src="yummuu.png" alt="Yummuu" /> 。src 图像源属性,alt替换文本属性。
6、表格标签:
7、列表标签
8、块级元素和内联元素
<div> 元素是块级元素,它是可用于组合其他 HTML 元素的容器。
<div> 元素没有特定的含义。除此之外,由于它属于块级元素,浏览器会在其前后显示折行。如果与 CSS 一同使用,<div> 元素可用于对大的内容块设置样式属性。
<div> 元素的另一个常见的用途是文档布局。它取代了使用表格定义布局的老式方法。使用 <table> 元素进行文档布局不是表格的正确用法。<table> 元素的作用是显示表格化的数据。
<span> 元素是内联元素,可用作文本的容器。
<span> 元素也没有特定的含义。
当与 CSS 一同使用时,<span> 元素可用于为部分文本设置样式属性。
两者的区别:就是在显示时是否起新行。块级元素会起新行,而内联元素则不会。
9、框架与内联框架:frame,<iframe src=" " name=" "></iframe>
10、脚本:<script> 定义客户端脚本,如Javascript;<noscript> 为不支持客户端脚本的浏览器定义替代内容。
11、头部元素:
<head> 元素是所有头部元素的容器。<head> 内的元素可包含脚本,指示浏览器在何处可以找到样式表,提供元信息,等等。
以下标签都可以添加到 head 部分:<title>、<base>、<link>、<meta>、<script> 以及 <style>。
<title>:在所有 HTML/XHTML 文档中都是必需的。它能够定义浏览器工具栏中的标题,提供页面被添加到收藏夹时显示的标题,显示在搜索引擎结果中的页面标题。
<base>:为页面上的所有链接规定默认地址或默认目标(target)
<link> :定义文档与外部资源之间的关系。最常用于连接样式表。
<style>:用于为 HTML 文档定义样式信息。
<meta> 标签提供关于 HTML 文档的元数据。元数据不会显示在页面上,但是对于机器是可读的。典型的情况是,meta 元素被用于规定页面的描述、关键词、文档的作者、最后修改时间以及其他元数据。
<meta> 标签始终位于 head 元素中。元数据可用于浏览器(如何显示内容或重新加载页面),搜索引擎(关键词),或其他 web 服务。
<script> 标签用于定义客户端脚本,比如 JavaScript。
12、HTML实体
在 HTML 中不能使用小于号(<)和大于号(>),这是因为浏览器会误认为它们是标签。如果希望正确地显示预留字符,我们必须在 HTML 源代码中使用字符实体(character entities)。
13、表单元素:
<form> :定义 HTML 表单。
<input> :是最重要的表单元素。<input> 元素有很多形态,根据不同的 type 属性。
input的输入类型type有text、password、submit、radio、checkbox、button;(HTML5新增)number、date、color、range、month、week、time、datetime、datetime-local、email、search、tel、url。
input的常用属性:value、readonly、disabled、size、maxlength;(HTML5新增)required、multiple、pattern、min和max、list、height和width、autocomplete
<select> :定义下拉列表 <option> 元素定义待选择的选项。列表通常会把首个选项显示为被选选项。您能够通过添加 selected 属性来定义预定义选项。
<textarea>:定义多行输入字段(文本域)
<button>:定义可点击的按钮
样式表允许以多种方式规定样式信息。样式可以规定在单个的 HTML 元素中,在 HTML 页的头元素中,或在一个外部的 CSS 文件中。甚至可以在同一个 HTML 文档内部引用多个外部样式表。
层叠次序
当同一个 HTML 元素被不止一个样式定义时,会使用哪个样式呢?
一般而言,所有的样式会根据下面的规则层叠于一个新的虚拟样式表中,其中数字 4 拥有最高的优先权。
1、浏览器缺省设置
2、外部样式表
3、内部样式表(位于 <head> 标签内部)
4、内联样式(在 HTML 元素内部)
因此,内联样式(在 HTML 元素内部)拥有最高的优先权,这意味着它将优先于以下的样式声明:<head> 标签中的样式声明,外部样式表中的样式声明,或者浏览器中的样式声明(缺省值)。
CSS 规则由两个主要的部分构成:选择器,以及一条或多条声明。
selector {declaration1; declaration2; ... declarationN }
每条声明由一个属性和一个值组成。
属性(property)是您希望设置的样式属性(style attribute)。每个属性有一个值。属性和值被冒号分开。
selector {property: value}
1、派生选择器:
通过依据元素在其位置的上下文关系来定义样式,例如: h1 span{color:red;}
2、id选择器:
id 选择器可以为标有特定 id 的 HTML 元素指定特定的样式。id 选择器以 "#" 来定义。
3、类选择器:
以一个点号显示,例如: .className{text-align: center;}
4、属性选择器:
对带有指定属性的 HTML 元素设置样式。例如: div[rel=’mm’]{ color:’#000’;}
可以为拥有指定属性的 HTML 元素设置样式,而不仅限于 class 和 id 属性。
注释:只有在规定了 !DOCTYPE 时,IE7 和 IE8 才支持属性选择器。在 IE6 及更低的版本中,不支持属性选择。
5、后代选择器(包含选择器):可以选择作为某元素后代的元素
6、子元素选择器:选择作为某元素子元素的元素。例如:h1>span{font-size:16px;}
7、相邻兄弟选择器:可选择紧接在另一元素后的元素,且二者有相同父元素。
例如:h1 + p {margin-top:50px;}
8、伪类:用于向某些选择器添加特殊的效果。
:active 向被激活的元素添加样式
:focus 向拥有键盘输入焦点的元素添加样式
:hover 当鼠标悬浮在元素上方时,向元素添加样式
:link 向未被访问的链接添加样式
:visited 向已被访问的链接添加样式
:first-child 向元素的第一个子元素添加样式(不建议使用)
:lang 向带有指定lang属性的元素添加样式
9、伪元素:用于向某些选择器设置特殊效果。
:first-letter 向文本的第一个字母添加样式
:first-line 向文本的首行添加样式
:before 在元素之前添加内容
:after 在元素之后添加内容
之前和前端交流页面的实现方案时,经常被告知:这个效果实现不了;那个东西兼容性不好;这个做不了...明明这些效果别人家已经实现出来了,哎,奈何不懂前端相关,没辙!
最近花了点时间看了些前端相关的博客、论坛,整理了一些html/css的基础知识,算是学个入门。同时还学了浏览器调试工具的基本用法,做产品测试的时候还是蛮有用的,谷歌浏览器自带的调试工具很好用,火狐的话要装firebug,有兴趣的可以去玩玩,还是很有意思的!
# 基本
html(Hypertext Markup Language)—— 结构 超文本标记语言
css(Cascading Style Sheets)—— 样式 层叠样式表
js(javascript)—— 行为
html超文本标记语言<html> </html> 标签对
<!DOCTYPE HTML> 声明文档类型
<meta charset="utf-8"/> 代码编码格式
单标签:直接在后面斜杠结束的标签叫做单标签。如换行符<br />
行间样式表<div style="……"></div>
内部样式表<style>…………</style>
外部样式表<link href="style.css" rel="stylesheet"/>
# 属性:属性值;
width 宽度height 高度
background 背景
background-attachment: fixed; 背景是否滚动
background-color: gray; 背景颜色
background-image: url(bg.jpg); 背景图
background-repeat: no-repeat; 背景图是否重复
background-position: center 0px; 背景图位置
# 传说中的盒模型
盒子大小 = border + padding + width/height
盒子宽度 = 左border+左padding+width+右padding +右border
盒子高度 = 上border+上padding+height+下padding+下border
# 基础属性
width 宽度 height 高度
background 背景 border 边框
padding 内边距 margin 外边距
font-size 文字大小 font-family 字体
color 文字颜色 line-height 行高
text-align 文本对齐方式 text-indent 首行缩进
font-weight 文字着重 font-style 文字样式
text-decoration 文本修饰 letter-spacing 字母间距
word-spacing 单词间距
# 基础标签
div 块
img 图片(单标签)
a 链接、下载、锚点
h1-h6 标题
p 段落
strong 强调(粗体)
em 强调(斜体)
span 区分样式
ul 无序列表
ol 有序列表
li 列表项
dl 定义列表
dt 定义列表标题
dd 定义列表项
# CSS基础选择符
id选择符(#)
群组选择符(,)
class选择符(.)
类型选择符(div……)
包含选择符(div p)
通配符(*)
基础选择符的优先级
类型 < class < id < style(行间) < js
伪类:伪类用于向被选中元素添加特殊的效果。(元素在特定情况下才具备的。)
a : link 未访问(默认)
a : hover 鼠标悬停(鼠标划过)
a : active 链接激活(鼠标按下)
a : visited 访问过后(点击过后)
a标签四个伪类的顺序:
link visited hover active
(love hate 记忆方法!)
a伪类的应用:
a、四个伪类全用(搜索引擎、新闻门户、小说网站)
b、一般网站只用( a{} a:hover{} )
#(样式重置)css reset 原则
但凡是浏览默认的样式,都不要使用。页面在不同浏览器下总是不能做到很好的兼容,显示的效果不同,估计就是一开始没有做样式重置!
<style>
body,dl,dd,p,h1,h2,h3,h4,h5,h6{margin:0;font-size:12px;}
ol,ul{margin:0;padding:0;list-style:none;}
a{text-decoration:none;}
img{border:none;}
</style>
# float浮动:
1、块在一排显示
2、内联支持宽高
3、默认内容撑开宽度
4、脱离文档流
5、提升层级半层
float:left | right | none | inherit;
文档流是文档中可显示对象在排列时所占用的位置。
浮动的定义:使元素脱离文档流,按照指定方向发生移动,遇到父级边界或者相邻的浮动元素停了下来。
clear:left | right | both | none | inherit;元素的某个方向上不能有浮动元素clear:both; 在左右两侧均不允许浮动元素。
# 清浮动方法
1.加高问题:扩展性不好
2.父级浮动问题:页面中所有元素都加浮动,margin左右自动失效(floats bad !)
3.inline-block 清浮动方法:问题:margin左右自动失效;
4.空标签清浮动问题:IE6 最小高度 19px;(解决后IE6下还有2px偏差)
5.br清浮动问题:不符合工作中:结构、样式、行为,三者分离的要求。
6.after伪类 清浮动方法(现在主流方法)
.clear:after{content:'';display:block;clear:both;}
.clear{zoom:1;}
after伪类: 元素内部末尾添加内容;
:after{content"添加的内容";} IE6,7下不兼容
zoom 缩放
a、触发 IE下 haslayout,使元素根据自身内容计算宽高。
b、FF 不支持;
7.overflow:hidden 清浮动方法; 问题:需要配合 宽度 或者 zoom 兼容IE6 IE7;
overflow: scroll | auto | hidden;
overflow:hidden;溢出隐藏(裁刀!)
# 浮动兼容性问题
IE6双边距BUG(IE6下块属性标签浮动,并且有横向margin,横向margin加倍。
a、IE6
b、浮动
c、横向margin
d、块属性标签(加display:inline;)
IE6下 li部分兼容性问题:
a、左右两列布局,右边右浮动IE6 IE7下折行;(左边元素浮动)b、IE6 IE7 li 下元素都浮动 在IE6 IE7下 li 下方会产生4px间隙问题;(加vertical-align:top;)
# 定位
position:relative; 相对定位
a、不影响元素本身的特性;
b、不使元素脱离文档流;
c、如果没有定位偏移量,对元素本身没有任何影响;
定位元素位置控制 top/right/bottom/left 定位元素偏移量。
position:absolute; 绝对定位
a、使元素完全脱离文档流;
b、使内嵌支持宽高;
c、块属性标签内容撑开宽度;
d、如果有定位父级相对于定位父级发生偏移,没有定位父级相对于整个文档发生偏移;
e、相对定位一般都是配合绝对定位元素使用;
z-index:[number]; 定位层级 a、定位元素默认后者层级高于前者;
position:fixed; 固定定位
与绝对定位的特性基本一致,的差别是始终相对整个文档进行定位;
问题:IE6不支持固定定位;
定位其他值:
position:static ; 默认值
position:inherit ; 从父元素继承定位属性的值
position:relative | absolute | fixed | static | inherit;
position:relative;在 IE6 下父级的 overflow:hidden; 包不住子级的relative;
position:absolute;在 IE6 下定位元素的父级宽高都为奇数那么在 IE6 下定位元素的 right 和 bottom 都有1像素的偏差。
position:absolute; 绝对定位元素子级的浮动可以不用写清浮动方法;
position:fixed; 固定定位元素子级的浮动可以不用写清浮动方法;(IE6不兼容)
# 表格标签
table 表格
thead 表格头
tbody 表格主体
tfoot 表格尾
tr 表格行
th 元素定义表头
td 元素定义表格单元
colspan 属性规定单元格可横跨的列数。<td colspan="2"></td>rowspan 属性规定单元格可横跨的行数。<td rowspan="2"></td>
# form 表单
<input type="…… " name="" value="" />
text 文本框
password 密码
radio 单选
checkbox 复选
submit 提交
reset 重置
button 按钮
image 图片
file 上传
hidden 隐藏
# 滑动门
滑动门并不是一项全新的技术,它是利用背景图像的可层叠性,并允许他们在彼此之上进行滑动,以创造一些特殊的效果。
CSS Sprites在国内很多人叫CSS精灵,是一种网页图片应用处理方式。它允许你将一个页面涉及到的所有零星图片都包含到一张大图中去。
CSS Sprites并不是一门新技术,目前它已经在网页开发中发展得十分成熟。大部分公司要求前端工程师必须使用CSS 精灵,处理图片;
CSS精灵 优点:利用CSS 精灵能很好地减少了网页的http请求次数,从而大大的提高了页面的性能,这也是CSS 精灵最大的优点;减少图片大小
CSS精灵 缺点:
降低开发效率;
维护难度加大;
# # 立志要搞懂技术的产品小白,正在学习JQuery的路上……
出于对PMCAFF用户的尊重,任何在PMCAFF产品经理社区发布的内容,在未经允许的情况下,不得在任何平台被直接或间接发布使用或被用于其他任何商业目的。如有违反上述声明者本网站将追究其相关法律责任。
微信公众号:pmcaffcom
投稿邮箱:tougao@pmcaff.com
Greated by PMCAFF产品经理社区 - www.pmcaff.com
*请认真填写需求信息,我们会在24小时内与您取得联系。