前端週刊發表每週前端技術相關的大事件、文章教學、一些框架的版本更新、以及程式碼和工具。每週定期發表,歡迎大家關注、轉載。
歡迎關注公眾號「前端每週看」
vscode 可以說是前端最流行的編輯器,其有豐富的外掛系統。不同開發人員對編輯器設定不同,比如縮排是用空格還是 tab,縮排幾個等等。如果多人開發同一個專案,必然會引起檔案衝突,所以一個團隊最好能統一編輯器。 參考:https://editorconfig.org,在專案根目錄新建.editconfig檔案
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
insert_final_newline = false
trim_trailing_whitespace = false
程式碼格式化工具,vscode 有很多格式化外掛,像 formate、vetur 等,我們選擇 prettier 作為團隊格式化工具。 1、安裝 prettier
yarn add prettier --save-dev
在專案根目錄新建.prettierrc.js
module.exports = {
// 強制使用單引號
singleQuote: true,
// 字串使用單引號
singleQuote: true,
// 大括號內的首尾需要空格
bracketSpacing: true,
// 末尾不需要逗號
trailingComma: 'none',
// 箭頭函數引數括號
arrowParens: 'avoid',
// 在jsx中把'>' 是否單獨放一行
jsxBracketSameLine: true,
// 使用預設的折行標準
proseWrap: 'preserve',
// 根據顯示樣式決定 html 要不要折行
htmlWhitespaceSensitivity: 'css',
// 換行符使用 crlf/lf/auto
endOfLine: 'auto'
};
2、設定 vscode 儲存自動格式化, 第一步,開啟 vscode 設定,搜尋 format,勾選 OnPaste、OnSave,如下圖
第二步,搜尋,defaultformatter,設定預設格式化工具,選擇 Prettier
3、可以在專案 package.json 裡設定 format 指令碼,
"format": "prettier --write --parser typescript \"(src|test)/**/*.ts\""
eslint 作為程式碼檢測工具,支援 ts、tsx
1、安裝 eslint
yarn add eslint --save-dev
2、安裝 ts 解析器以及 ts 規則補充
yarn add @typescript-eslint/parser --save-dev
yarn add @typescript-eslint/eslint-plugin --save-dev
eslint 預設使用 Espree 進行解析,無法識別 ts 的一些語法,所以需要安裝一個 ts 的解析器 @typescript-eslint/parser,用它來代替預設的解析器@typescript-eslint/eslint-plugin 作為 eslint 預設規則的補充,提供了一些額外的適用於 ts 語法的規則。
3、支援 tsx
yarn add eslint-plugin-react --save-dev
由於是 react 專案,所以還需要外掛 eslint-plugin-react 來支援 .tsx
4、在專案根目錄建立 .eslintrc.js 當執行 ESLint 的時候檢查一個檔案的時候,它會首先嚐試讀取該檔案的目錄下的組態檔,然後再一級一級往上查詢,將所找到的設定合併起來,作為當前被檢查檔案的設定。
module.exports = {
parser: '@typescript-eslint/parser',
plugins: [
'react',
'react-hooks',
'@typescript-eslint/eslint-plugin',
'prettier'
],
settings: {
react: {
version: 'detect'
}
},
rules: {
'prettier/prettier': 'error',
'no-debugger': 'error',
// 取消函數引數需要重新賦值給另一個變數才能使用
'no-param-reassign': [0],
// 取消 { a, b, c } 多個變數需要換行
'object-curly-newline': [0],
// 禁用var,用let和const代替
'no-var': 2,
// 開啟強制單引號
quotes: [2, 'single'],
// 強制全等( === 和 !==)
eqeqeq: 2,
// 語句強制分號結尾
semi: [2, 'always'],
// 禁止出現未使用的變數
'@typescript-eslint/no-unused-vars': [2],
// 箭頭函數引數括號,一個引數時可省略括號
'arrow-parens': [2, 'as-needed'],
// 箭頭函數,箭頭前後空格
'arrow-spacing': [2, { before: true, after: true }],
// 禁止物件最後一項逗號
'comma-dangle': [2, 'never'],
// 單行程式碼/字串最大長度
'max-len': [2, { code: 120 }],
// jsx縮排2個空格
'react/jsx-indent': [2, 2],
// 檔案末尾強制換行
'eol-last': 2,
// react設定
// 強制元件方法順序
'react/sort-comp': [2],
// 結束標籤,元件省略閉合標籤,html不省略閉合標籤
'react/self-closing-comp': [2, { component: true, html: false }],
// 檢查 Hook 的規則,不允許在if for裡面使用
'react-hooks/rules-of-hooks': [2],
// 檢查 effect 的依賴
'react-hooks/exhaustive-deps': [2]
}
};
驗證 git 提交規則,建立 verify-commit-msg.js 檔案
const chalk = require('chalk')
const msgPath = process.env.GIT_PARAMS
const msg = require('fs').readFileSync(msgPath, 'utf-8').trim()
const commitRE =
/^(revert: )?(wip|release|feat|fix|polish|docs|style|refactor|perf|test|workflow|ci|chore|types|build)(\(.+\))?: .{1,50}/
if (!commitRE.test(msg)) {
console.log()
console.error(
` ${chalk.bgRed.white(' ERROR ')} ${chalk.red(
`invalid commit message format.`
)}\n\n` +
chalk.red(
` Proper commit message format is required for automated changelog generation. Examples:\n\n`
) +
` ${chalk.green(`feat(compiler): add 'comments' option`)}\n` +
` ${chalk.green(
`fix(v-model): handle events on blur (close #28)`
)}\n\n` +
chalk.red(` See .github/COMMIT_CONVENTION.md for more details.\n`)
)
process.exit(1)
}
程式碼提交規則
feat: 新功能
fix: 修復
docs: 檔案變更
style: 程式碼格式(不影響程式碼執行的變動)
refactor: 重構(既不是增加feature,也不是修復bug)
perf: 效能優化
test: 增加測試
chore: 構建過程或輔助工具的變動
revert: 回退
build: 打包
整個團隊是用 umi 封裝的腳手架,所有專案都是 React.js+Mobx+TypeScript,下面列出了基本規範。
React 元件檔名使用 PascalCase 命名規則,並且以.tsx 字尾名。例如:AnotherComponent.tsx
如果 React 元件是一個單檔案,以元件名作為檔名;如果是將 React 元件放在一個目錄裡,以元件名作為目錄名,並且元件所在檔案以 index.jsx 命名
src
|-- components
| |-- BadNamedComponent
| |-- BadNamedComponent.jsx
| |-- BadNamedComponent.css
| |-- GoodNamedComponent
| |-- ChildComponent.jsx
| |-- ChildComponent.css
| |-- index.jsx
| |-- index.css
| |-- AnotherComponent.jsx
| |-- AnotherComponent.csssha
// ❌
import BadNamedComponent from '@/components/BadNamedComponent/BadNamedComponent';
// ❌
import GoodNamedComponent from '@/components/GoodNamedComponent/index';
// ✅
import GoodNamedComponent from '@/components/GoodNamedComponent';
// ✅
import AnotherComponent from '@/components/AnotherComponent';
+ React 元件使用 PascalCase 方式命名,React 元件範例使用 camelCase 方式命名
// ❌
import someComponent from './SomeComponent';
// ✅
import SomeComponent from './SomeComponent';
// ❌
const AnotherComponent = <AnotherComponent />;
// ✅
const anotherComponent = <AnotherComponent />;
// ❌
export default function wrapForm(WrappedComponent) {
return function FormWrapper(props) {
return <WrappedComponent {...props} {...somePropsFromWrapper} />;
}
}
// ✅
export default function withForm(WrappedComponent) {
return function WithForm(props) {
return <WrappedComponent {...props} {...somePropsFromWrapper} />;
}
}
// ❌
export default function withForm(WrappedComponent) {
function WithForm(props) {
return <WrappedComponent {...props} {...somePropsFromWrapper} />;
}
return WithForm;
}
// ✅
export default function withForm(WrappedComponent) {
function WithForm(props) {
return <WrappedComponent {...props} {...somePropsFromWrapper} />;
}
const wrappedComponentName = WrappedComponent.displayName
|| WrappedComponent.name
|| 'Component';
WithForm.displayName = `withForm(${wrappedComponentName})`;
return WithForm;
}
// ❌
<SomeComponent
SomeProp="value1"
other_prop="value2"
/>
// ✅
<SomeComponent
someProp="value1"
otherProp="value2"
/>
function SomeComponent() {
// ❌
const \_handleSubmit = useCallback((params) => {
submitWith(params);
}, []);
// ✅
const handleSubmit = useCallback((params) => {
submitWith(params);
}, []);
return (
<Form onSubmit={_handleSubmit} onSubmit2={handleSubmit} />
);
}
// ❌
function ParentComponent() {
return <div>
<ChildComponent />
</div>;
}
// ✅
function ParentComponent() {
return (
<div>
<ChildComponent />
</div>
);
}
// ❌
<SomeComponent prop="value"></SomeComponent>
// ✅
<SomeComponent prop="value" />
2.7.5 對齊
+ 多行屬性的折行和對齊方式
// ❌
<SomeComponent superLongParam="bar"
anotherSuperLongParam="baz" />
// ✅
<SomeComponent
superLongParam="bar"
anotherSuperLongParam="baz"
/>
// ✅
<ParentComponent
superLongParam="bar"
anotherSuperLongParam="baz"
> <ChildComponent />
> </ParentComponent>
// ❌
{
someCondition
? <ComponentA />
: <ComponentB />
}
// ✅
{someCondition ? (
<ComponentA />
) : (
<ComponentB />
)}
function SomeComponent() {
// ❌
const wrongString = "double quotes is wrong";
// ✅
const rightString = 'single quotes is right';
}
// ❌
<SomeComponent someProp='value1' />
// ✅
<SomeComponent someProp="value1" />
// ❌
<SomeComponent style={{ fontSize: "12px" }} />
// ✅
<SomeComponent style={{ fontSize: '12px' }} />
// ❌
<SomeComponent/>
// ❌
<SomeComponent />
// ✅
<SomeComponent />
// ❌
<SomeComponent someProp={ someValue } />
// ✅
<SomeComponent someProp={someValue} />
// ❌
<SomeComponent someObjectProp={{prop: value}} />
// ✅
<SomeComponent someObjectProp={{ prop: value }} />
// ❌
<SomeComponent style={{
marginTop: '10px',
fontSize: '12px',
color: '#f00',
}} />
// ✅
const styles = {
someComponent: {
marginTop: '10px',
fontSize: '12px',
color: '#f00',
},
};
<SomeComponent style={styles.someComponent} />
import styled from 'styled-components';
const Wrapper = styled.div`
width: 100%;
`;
const FC: React.FC = () => {
return <Wrapper></Wrapper>;
};
export default FC;
// ❌
<SomeComponent visible={true} />
// ✅
<SomeComponent visible />
<img>
標籤時,如果不是裝飾性 (Decorative Image) 圖片必須有 alt 屬性,如果是裝飾性圖片則應該設定 alt="" 或者 role="presentation" 屬性// ❌
<img src="logo.png" />
// ✅
<img src="logo.png" alt="wedoctor" />
// ✅
<img src="some-presentational-image.png" alt="" />
// ✅
<img src="some-presentational-image.png" role="presentation" />
// ❌
{someList.map((item, index) => (
<Item key={index} {...item} />
))}
// ✅
{someList.map(item => (
<Item key={item.id} {...item} />
))}
// ❌
function SomeComponent({ requiredProp, nonRequiredProp }) {
return (
<div>{requiredProp}{nonRequiredProp}</div>
);
}
SomeComponent.propTypes = {
requiredProp: PropTypes.number.isRequired,
nonRequiredProp: PropTypes.string,
};
// ✅
function SomeComponent({ requiredProp, nonRequiredProp }) {
return (
<div>{requiredProp}{nonRequiredProp}</div>
);
}
SomeComponent.propTypes = {
requiredProp: PropTypes.number.isRequired,
nonRequiredProp: PropTypes.string,
};
SomeComponent.defaultProps = {
nonRequiredProp: '',
};
// ❌
function SomeRegularComponent(props) {
return (
<ChildComponent {...props} />
);
}
// ✅
function HOC(WrappedComponent) {
return function WrapperComponent(props) {
const propFromWrapper = 'value';
return (
<WrappedComponent {...props} propFromWrapper={propFromWrapper} />
);
};
}
// ✅
function SomeRegularComponent(props) {
const { irrelevantProp1, irrelevantProp2, ...relevantProps } = props;
return (
<ChildComponent {...relevantProps} />
);
}
// ✅
<ChildComponent
literalStringProp="some string prop"
literalNumberProp={1}
literalBooleanProp={false}
variableProp={someVariable}
onChange={handleChange}
/>
檔案:https://ahooks.js.org/zh-CN/hooks/use-request/index
function ParentComponent() {
// ❌
if (someCondition) {
useEffect(() => {
doSomeSideEffects();
}, []);
}
// ✅
useEffect(() => {
if (someCondition) {
doSomeSideEffects();
}
}, []);
return (
<ChildComponent onChange={handleChange} />
);
}
// ❌
function someRegularFunction() {
const [state, setState] = useState(1);
}
// ✅
function ParentComponent() {
const [state, setState] = useState(1);
return (
<ChildComponent someProp={state} />
);
}
// ✅
function useSomeCustomHooks() {
const [state, setState] = useState(1);
return state;
}
// ❌
function ParentComponent() {
const handleChange = () => {
// handle change
};
return (
<ChildComponent onChange={handleChange} />
);
}
// ✅
function ParentComponent() {
const handleChange = useCallback(() => {
// handle change
}, []);
return (
<ChildComponent onChange={handleChange} />
);
}
function ChildComponent() {
return (
<div>
<span>child component</span>
</div>
);
}
// ❌
export default ChildComponent;
// ✅
export default React.memo(ChildComponent);
// ❌
function ParentComponent() {
return (
<ChildComponent
onChange={useCallback(() => {
// handle change
}, [])}
someMemoProp={useMemo(() => (
computeWith(dep)
), [dep])}
/>
);
}
// ✅
function ParentComponent() {
const handleChange = useCallback(() => {
// handle change
}, []);
const someMemoProp = useMemo(() => (
computeWith(dep)
), [dep]);
return (
<ChildComponent onChange={handleChange} someMemoProp={someMemoProp} />
);
}
注:後期需求調整過程中,Hooks 函數所在的 JSX 塊可能會出現 if 之類的條件渲染邏輯,此時就需要將該 Hooks 函數遷移到元件函數的最外層很不方便,為了後期維護起見,應該統一在元件函數的最外層呼叫 Hooks 函數
function ParentComponent() {
// ❌
const someComplexComputedValue1 = () => doSomeComplexComputeWith(...deps);
// ✅
const someComplexComputedValue2 = useMemo(() => (
doSomeComplexComputeWith(...deps)
), [...deps]);
return (
<ChildComponent
someComplexComputedValue1={someComplexComputedValue1}
someComplexComputedValue2={someComplexComputedValue2}
/>
);
}
function ParentComponent({ someProp }) {
const [state, setState] = useState(1);
// ❌
const someComputedProp1 = doSomeComputeWith(state, someProp);
// ✅
const someComputedProp2 = useMemo(() => (
doSomeComputeWith(state, someProp)
), [state, someProp]);
return (
<ChildComponent
someComputedProp1={someComputedProp1}
someComputedProp2={someComputedProp2}
/>
);
}
// ❌
export default function usePrevious(value) {
const previousValueRef = useRef(value);
return useMemo(() => {
const previousValue = previousValueRef.current;
previousValueRef.current = value;
return previousValue;
}, [value]);
}
// ✅
function usePrevious(value) {
const previousValueRef = useRef(value);
const currentValueRef = useRef(value);
useEffect(() => {
previousValueRef.current = currentValueRef.current;
currentValueRef.current = value;
}, [value]);
return currentValueRef.current === value
? previousValueRef.current
: currentValueRef.current;
}
參考:https://zh-hans.reactjs.org/docs/hooks-reference.html#usememo
function ParentComponent() {
const [state, setState] = useState(1);
// ❌
const handleSubmit1 = useCallback(() => {
submitWith(state);
}, [state]);
// ✅
const stateRef = useRef(state);
stateRef.current = state;
const handleSubmit2 = useCallback(() => (
submitWith(stateRef.current);
), []);
return (
<ChildComponent
onSubmit1={handleSubmit1}
onSubmit2={handleSubmit2}
/>
);
}
// 需要監聽引數變化
export const component = observer((props: any) => {
})
// 不需要監聽引數變化
const FC: React.FC = () => {
return <Wrapper></Wrapper>;
};
export default FC;
Hooks 呼叫位置和順序建議: useSelector useContext useState useReducer useDispatch 統一在程式碼最頂層依次呼叫,其次是 useCallback useMemo ,然後是 useLayoutEffect useEffect , useRef 的位置可以依據被使用到的位置靈活放置, useImperativeHandle 一般和 useRef 一起使用,建議跟隨在與其相關的 useRef 之後。其它一些區域性變數按需要靈活放置
version > 6
import { makeAutoObservable } from 'mobx';
class Store {
constructor() {
makeAutoObservable(this);
}
fontSize = 80;
updateFontSize(fontSize) {
this.fontSize = fontSize;
}
}
export default new Store();
基本遵循 JavaScript Style Guide 與 ES-Next Style Guide
1、工程設定 TypeScript 檔案使用 .ts 擴充套件名。含 JSX 語法的 TypeScript 檔案使用 .tsx 擴充套件名。 tsconfig.json 組態檔應開啟 strict、noImplicitReturns、noUnusedLocals 選項。 tsconfig.json 組態檔應開啟 allowSyntheticDefaultImports 選項。 範例:
// ✅
import React, { PureComponent } from 'react';
// ❌
import \* as React from 'react';
使用 VS Code 編寫 TypeScript。 2、 檔案 在檔案結尾處,保留一個空行。 3、 命名 介面 使用 Pascal 命名法。 介面名 不使用 I 作為字首。 範例:
// ✅
interface ButtonProps {
// ...
}
// ❌
interface IButtonProps {
// ...
}
型別別名 使用 Pascal 命名法。 範例:
// ✅
interface HeaderStateProps {
// ...
}
interface HeaderDispatchProps {
// ...
}
type HeaderProps = HeaderStateProps & HeaderDispatchProps;
1、 變數 使用 const 宣告 列舉 。 範例:
// ✅
const enum Directions {
UP,
DOWM,
LEFT,
RIGHT,
}
// ❌
enum Directions {
UP,
DOWN,
LEFT,
RIGHT,
}
2、 型別 不應顯式宣告可以自動推導的型別。 範例:
// ✅
let shouldUpdate = false;
// ❌
let shouldUpdate: boolean = false;
使用 string / number / boolean 宣告基本型別,不使用 String / Number / Boolean。 範例:
// ✅
let str: string;
// ❌
let str: String;
不使用 Object / Function 宣告型別。
陣列元素為簡單型別(非匿名且不含泛型)時,使用 T[] 宣告型別,否則應使用 Array
// ✅
let files: string[];
let tokens: Array<string | number>;
let buffer: Buffer[];
let responses: Array<Promise<number>>;
// ❌
let files: Array<string>;
let tokens: (string | number)[];
let buffer: Array<Buffer>;
let responses: Promise<number>[];
不使用 ! 宣告物件屬性非空。 範例:
// ✅
if (foo.bar && foo.bar.baz) {
// ...
}
// ❌
if (foo!.bar!.baz) {
// ...
}
不使用 any 宣告型別。 範例:
// ✅
const identity = <T>(x: T) => x;
// ❌
const identity = (x: any) => x;
使用 as 進行型別宣告轉換,不使用 <> 。 範例:
// ✅
const root = document.getElementById('root') as HTMLDivElement;
// ❌
const root = <HTMLDivElement>document.getElementById('root');
介面不應為空。 介面中同一函數過載的型別宣告需相鄰。 範例:
// ✅
interface AnyInterface {
foo();
foo(x: string);
bar();
bar(x: number);
}
// ❌
interface AnyInterface {
foo();
bar();
foo(x: string);
bar(x: number);
}
3、 條件 使用 === 或 !== 判斷相等性,不使用 == 或 !=。 範例:
// ✅
if (foo !== null && foo !== undefined) {
// ...
}
// ❌
if (foo != null) {
// ...
}
4、 迴圈 使用 Object.keys / Object.values / Object.entries / Object.getOwnPropertyNames 遍歷物件,不使用 for .. in 。 範例:
// ✅
Object.keys(obj).forEach(key => /_ ... _/);
// ❌
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
// ...
}
}
索引僅用於獲取陣列當前被迭代的項時,使用 for .. of 遍歷陣列,不使用 for 。 範例:
// ✅
for (const item of items) {
// ...
}
// ❌
for (let i = 0; i < items.length; i++) {
const item = items[i];
// ...
}
5、 陣列 使用 ... 進行陣列淺拷貝,不使用 Array.from / Array.prototype.slice 。 範例:
// ✅
const copies = [...items];
// ❌
const copies = items.slice();
// worst
let copies = [];
for (let i = 0; i < items.length; i++) {
copies.push(items[i]);
}
使用 ... 將類陣列物件轉化為陣列,不使用 Array.from / Array.prototype.slice 。 範例:
// ✅
const elements = [...document.querySelectorAll('.foo')];
// ❌
const element = Array.from(document.querySelectorAll('.foo'));
// worst
const element = Array.prototype.slice.call(document.querySelectorAll('.foo'));
6、 物件 使用 ... 進行物件淺拷貝,不使用 Object.assign 。 範例:
// ✅
this.setState(state => ({...state, clicked: true}));
// ❌
this.setState(state => Object.assign({}, state, {clicked: true}));
7、 函數 避免 return undefined ,應直接 return。 範例:
// ✅
function foo(bar: boolean) {
if (!bar) {
return;
}
}
// ❌
function foo(bar: boolean) {
if (!bar) {
return undefined;
}
}
8、 類 每個檔案中最多宣告一個類。 類成員的可存取性為 public 時,不應顯式宣告。 建構函式可忽略時,應忽略。 類成員之間使用空行隔開。 範例:
// ✅
class Button extends PureComponent<ButtonProps, ButtonState> {
readonly state: ButtonState = {
clicked: false,
};
render() {
// ...
}
}
// ❌
class Button extends PureComponent<ButtonProps, ButtonState> {
public state: ButtonState = {
clicked: false,
};
constructor(props: ButtonProps) {
super(props);
}
public render() {
// ...
}
}
建構函式初始化範例屬性時,應儘量使用引數屬性。 建構函式的引數中,作為屬性的引數應排列於其他引數前。 範例:
// ✅
class AppComponent {
constructor(private readonly heroService: HeroService) {}
}
// ❌
class AppComponent {
private readonly heroService: HeroService;
constructor(heroService: HeroService) {
this.heroService = heroService;
}
}
9、 模組 使用 ECMAScript 2015 標準的模組系統。 除型別宣告檔案外,不使用 module / namespace 關鍵字。 不使用 /// <reference path= > 。 範例:
// ✅
import foo from 'foo';
// ❌
import foo = require('foo');
對於同一個模組路徑,僅 import 一次。 範例:
// ✅
import React, {PureComponent} from 'react';
// ❌
import React from 'react';
import {PureComponent} from 'react';
對於使用 webpack 等構建工具的專案,在模組中引入其他資源(如樣式、圖片等)時,為資源編寫型別宣告檔案,或使用合適的 loader 生成型別宣告檔案。 範例:
// ✅
// Button.scss.d.ts
export clicked: string;
// logo.png.d.ts
declare const logo: string;
export default logo;
// Button.tsx
import styles from './Button.scss';
import logo from './logo.png';
// ❌
const styles = require<any>('./Button.scss');
const logo = require<string>('./logo.png');