React + Mobx + Typescript 从0到1打造一款仿网易云音乐APP(二)——初始化项目配置

1,452 阅读4分钟

引言

该项目会以 React 全家桶 (会使用 16.8 最新 API 及 hooks) 以及 mobx 数据流方案为基础打造的一款高质量的移动端音乐类 WebApp 。 涉及的技术栈主要有:

  • react v16.8 全家桶 (react,react-router) : 用于构建用户界面的 MVVM 框架
  • mobx 前端数据流方案
  • immutable: Facebook 历时三年开发出的进行持久性数据结构处理的库
  • axios: 用来请求后端 api 的数据。

这是系列文章,为了大家阅读方便,我会列举出系列文章的目录。

系列文章目录

重要说明

!本项目的仓库为happy-music。其中master分支为最新的所有代码,每次更新文章都会对应一个tag,tag是自增的,本篇文章对应的代码tag为v1.0.3。

本文内容介绍

本篇主要讲述webpack 项目初始化配置。如果是webpack的大拿,就可以跳过这片文章了。这篇文章的主要内容有:

  • webpack4构建项目(建议新手不要轻易使用create-react-app等脚手架,前端进阶,webpack基本是绕不开的);
  • babel7转译es6、react语法;
  • 配置eslint;
  • 配置commit-lint,约束提交的信息
  • 配置changelog-cli自动生成可读性高的changelog

啰里八嗦的终于来到了正文部分了!!!

项目配置

先给项目取一个响亮的名字,奉行快乐优先原则,就叫happy-music吧。

webpack4配置不细说,具体的直接看官网就可以了,这里只列出一些重点。创建如下文件结构,其中webpack.common.js用于抽离公用配置,webpack.dev.js和webpack.prod.js分别对应开发和线上环境。

安装webpack依赖,其中webpack-merge就是用来合并配置的

yarn add -D webpack webpack-cli webpack-merge

loader

webpack的作用都在官网这张图上,就是将各种模块进行处理和打包。而loader就是处理webpack中的模块。

配置babel-loader

我们项目主要使用react和ts编写,babel7之后也支持对ts转义了,之前需要使用ts-loader。 安装如下依赖:

yarn add @babel/core @babel/preset-env @babel/preset-typescript @babel/preset-react babel-loader -D

在项目根目录创建.babelrc文件:

{
    "presets": ["@babel/preset-env", "@babel/preset-react", "@babel/preset-typescript"]
}

根目录创建tsconfig.json文件

{
    "compilerOptions": {
        "baseUrl": "./",
        "paths": {
            "*": ["types/*"]
        },
        "target": "es5",
        "module": "commonjs",
        "sourceMap": true,
        "emitDecoratorMetadata": true,
        "downlevelIteration": true,
        "experimentalDecorators": true,
        "removeComments": false,
        "noImplicitAny": true, // 为 false 时,如果编译器无法根据变量的使用来判断类型时,将用 any 类型代替。为 true 时,进行强类型检查,会报错
        "suppressImplicitAnyIndexErrors": true,
        "allowSyntheticDefaultImports": true,
        "allowJs": true,
        "checkJs": false,
        "jsx": "react",
        "lib": ["dom", "es2015"],
        "outDir": "./dist/",
        "typeRoots": ["./node_modules/@types/", "./src/@types/"]
    },
    "compileOnSave": false,
    "exclude": ["node_modules"]
}

配置less-loader

我们项目主要使用less编写,所以需要使用less-loader。使用的loader主要如下:

less-loader: 将less转义成css
css-loader: 用于加载.css文件,并转换成commonjs对象
style-loader: style-loader用于将<style>标签插入到header中
postcss-loader: 作用有两个,第一个就是把 CSS 解析成 JavaScript 可以操作的抽象语法树结构(Abstract Syntax Tree,AST),第二个就是调用插件来处理 AST 并得到结果,用的最多的是autoprefixer插件,可以自动添加浏览器前缀,保证css兼容性的写法。
style-resources-loader: 主要用于将一些公用less,自动插入到所有less文件中。

我们把lessLoader抽象成了一个函数,方便在需要的地方调用。

/*
* @param lessPath 包含的路径
* @param isModules 是否需要添加modules
*/
var lessLoader = function (lessPath, isModules) {
    return {
        test: /\.less$/,
        include: lessPath,
        loaders: [
            'style-loader',
            {
                loader: 'css-loader',
                options: {
                    modules: {
                        localIdentName: isModules
                            ? '[name]__[local]__[hash:base64:5]'
                            : '[name]'
                    }
                },
            },
            {
                loader: 'postcss-loader',
                options: {
                    plugins: [
                        require('autoprefixer')({
                            browsers: ['last 5 versions'],
                        }),
                    ],
                },
            },
            {
                loader: 'less-loader',
                options: {
                    globalVars: globalVars,
                },
            },
            {
                loader: 'style-resources-loader',
                options: {
                    patterns: path.resolve(
                        paths.srcPath,
                        'style',
                        'var',
                        '*.less',
                    ),
                    injector: 'append',
                },
            },
        ],
    };
};

这里在啰嗦几句,开启了css-modules后,我们写css类异常恶心。主要缺点如下:

  • 必须使用驼峰法来命名css类名
  • 当引入到 className 中时必须要使用 styles 对象
  • CSS modules 和 全局css类混合在一起会很难管理
  • 引用没用定义的CSS modules不会出现警告
import { Component } from 'react';
import styles from './style.less';
 
export default class Container extends Component {
  render() {
    return (
      <div className={ styles.container }>
      </div>
    );
  }
}
 
// style.less
.container {
  border-width: 2px;
  border-style: solid;
  border-color: brown;
  padding: 0 20px;
  margin: 0 6px;
  max-width: 400px;
}

这时我们可以使用react-css-modules来解决以上问题,这样就舒服多了。配置babelrc.js

{
    "presets": ["@babel/preset-env", "@babel/preset-react", "@babel/preset-typescript"],

    "plugins": [
        [
            "react-css-modules",
            {
                "filetypes": {
                    ".less": {
                        "syntax": "postcss-less"
                    }
                },

                "webpackHotModuleReloading": true,
                "generateScopedName": "[name]__[local]__[hash:base64:5]",
                "exclude": "node_modules"
            }
        ]
    ]
}
import { Component } from 'react';
import './style.less';
 
export default class Container extends Component {
  render() {
    return (
      <div styleName="container">
      </div>
    );
  }
}
 
// style.less
.container {
  border-width: 2px;
  border-style: solid;
  border-color: brown;
  padding: 0 20px;
  margin: 0 6px;
  max-width: 400px;
}

还有其他使用到loader这里就不一一列举了,详情请参考代码happy-music

插件plugin

html-webpack-plugin

html-webpack-plugin能够根据我们提供的模板自动生成html文件,并引入打包后的内容。 首先安装html-webpack-plugin:

yarn add html-webpack-plugin -D

在根目录创建index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Happy Music</title>
</head>

<body>
    <div id="root"></div>
</body>

</html>

配置html-webpack-plugin:

const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
    plugins: [
        new HtmlWebpackPlugin({
            file: 'index.html',
            template: 'index.html',
            inject: "body"
        })
    ],
}

webpack-dev-server启动本地开发服务

在日常的开发过程中,肯定不能每修改一点东西就重新build一次,这样开发效率会受到很大的影响。这时需要启动一个服务,来监听文件的变动。当文件保存时就重新打包,同时帮我们自动刷新浏览器,方便我们及时观察到更新。

虽然webpack提供了webpack --watch的命令来动态监听文件的改变并实时打包,输出新bundle.js文件,这样文件多了之后打包速度会很慢,此外这样的打包的方式不能做到hot replace,即每次webpack编译之后,你还需要手动刷新浏览器。

webpack-dev-server功能可以克服上面的2个问题。webpack-dev-server的原理:启动一个使用express的Http服务器。它的作用主要是用来伺服资源文件。此外这个Http服务器和client使用了websocket通讯协议,原始文件作出改动后,webpack-dev-server会实时的编译,但是最后的编译的文件并没有输出到目标文件夹。

注意:webpack-dev-server后,在目标文件夹中是看不到编译后的文件的,实时编译后的文件都保存到了内存当中。 具体配置可以直接看官方文档

安装webpack-dev-server:

yarn add webpack-dev-server -D

在webpack.development文件配置devServer:

module.exports = {
    devServer: {
        open: true,
        inline: true,
        contentBase: path.resolve(paths.appPath, 'public'),
        port: config['webapck-dev-server'].port || 8080, // 我们把这些配置抽离到 config目录是为了修改方便
        host: config['webapck-dev-server'].host || '0.0.0.0',
        proxy: {
            [config.appPathname]: {
                target: 'http://127.0.0.1:' + config.port,
                pathRewrite: { ['^' + config.appPathname]: '' },
            },
        },// 代理主要用于解决跨域问题
        publicPath: '',
        hot: true,
        disableHostCheck: true,
        watchOptions: {
            ignored: /node_modules/,
            poll: false,
        },
    }
}

配置eslint

ESLint 可以安装在当前项目中或全局环境下,因为代码检查是项目的重要组成部分,所以我们一般会将它安装在当前项目中。可以运行下面的脚本来安装:

yarn add eslint -D

由于 ESLint 默认使用 Espree 进行语法解析,无法识别 TypeScript 的一些语法,故我们需要安装 @typescript-eslint/parser,替代掉默认的解析器,别忘了同时安装 typescript:

yarn add typescript @typescript-eslint/parser -D

接下来需要安装对应的插件 @typescript-eslint/eslint-plugin 它作为 eslint 默认规则的补充,提供了一些额外的适用于 ts 语法的规则。

yarn add @typescript-eslint/eslint-plugin

根目录创建.eslintrc.js

module.exports = {
    parser: '@typescript-eslint/parser',
    plugins: ['@typescript-eslint'],
    rules: {
        // 定义规则
    }
}

vscode 集成 ESLint 检查

在编辑器中集成 ESLint 检查,可以在开发过程中就发现错误,甚至可以在保存时自动修复错误,极大的增加了开发效率。 要在 VSCode 中集成 ESLint 检查,我们需要先安装 ESLint 插件。 VSCode 中的 ESLint 插件默认是不会检查 .ts 后缀的,需要在「文件 => 首选项 => 设置 => 工作区」中(也可以在项目根目录下创建一个配置文件 .vscode/settings.json),添加以下配置:

{
    "eslint.autoFixOnSave": true,
    "eslint.validate": [
        "javascript",
        "javascriptreact",
        {
            "language": "typescript",
            "autoFix": true
        }
    ],
    "typescript.tsdk": "node_modules/typescript/lib"
}

常见错误

这时需要在.eslintrc.js配置sourceType,参考如下:

module.exports = {
    parserOptions: {
        ecmaVersion: 6,
        sourceType: "module",
        ecmaFeatures: {
            modules: true
        }
    }
}

使用prettier格式化代码

Prettier 聚焦于代码的格式化,通过语法分析,重新整理代码的格式,让所有人的代码都保持同样的风格。根目录创建.prettierrc.js

module.exports = {
    // 一行最多 100 字符
    printWidth: 100,
    // 使用 4 个空格缩进
    tabWidth: 4,
    // 不使用缩进符,而使用空格
    useTabs: false,
    // 行尾需要有分号
    semi: true,
    // 使用单引号
    singleQuote: true,
    // 对象的 key 仅在必要时用引号
    quoteProps: 'as-needed',
    // jsx 不使用单引号,而使用双引号
    jsxSingleQuote: false,
    // 末尾不需要逗号
    trailingComma: 'none',
    // 大括号内的首尾需要空格
    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'
};

接下来安装 VSCode 中的 Prettier 插件,然后修改 .vscode/settings.json:

{
    "files.eol": "\n",
    "editor.tabSize": 4,
    "editor.formatOnSave": true,
    "editor.defaultFormatter": "esbenp.prettier-vscode",
    "eslint.autoFixOnSave": true,
    "eslint.validate": [
        "javascript",
        "javascriptreact",
        {
            "language": "typescript",
            "autoFix": true
        }
    ],
    "typescript.tsdk": "node_modules/typescript/lib"
}

提交前自动 eslint 校验和 commit 信息的规范校验

涉及的插件主要有: husky: 一个 Git Hook 工具 lint-staged: 用于实现每次提交只检查本次提交所修改的文件 @commitlint/cli: commit msg 检查 @commitlint/config-conventional: 配置commit规则

yarn add husky lint-staged @commitlint/cli @commitlint/config-conventional -D

根目录创建.huskyrc文件,当然这个也可以直接配置在package.json文件:


{
  "hooks": {
    "pre-commit": "lint-staged",
    "commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
  }
}

根目录创建 .lintstagedrc文件

{
   "*.tsx": ["eslint --fix", "git add"],
   "*.ts": ["eslint --fix", "git add"]
}

根目录创建 commitlint.config.js

module.exports = {
    extends: ['@commitlint/config-conventional'],
    rules: {
        'type-enum': [
            2,
            'always',
            [
                'feat', // 新功能
                'modify', // 修改
                'fix', // 修复bug
                'docs', // 文档
                'style', // 格式
                'refactor', // 重构
                'test', // 增加测试
                'chore', // 构建过程或辅助工具的变动
                'revert', //回滚
                'upgrade' // 第三方库升级
            ],
        ],
        'subject-full-stop': [0, 'never'],
        'subject-case': [0, 'never'],
    },
};

自动changelog生成

安装conventional-changelog-cli插件,conventional-changelog-cli 默认推荐的 commit 标准是来自angular项目,除了 angular 标准以外,目前集成了包括 atom, codemirror, ember, eslint, express, jquery 等项目的标准,具体可以根据自己口味来选用。

yarn add conventional-changelog-cli -D

在根目录创建release.sh,这个命令主要用于需要正式发布时,在本地运行,可以自动生成新的版本号,已经根据commit信息生成changelog。

#!/bin/bash

# 可以直接设置为开发分支,origin为远程地址
master="dev"
origin="" 

git fetch $origin
echo "Current fetch all Tags"

git pull $origin $master
echo "Current pull origin $master."

# 自动生成tag和修改版本号
npm version patch
conventional-changelog -p eslint -i CHANGELOG.md -s -r 0

git add CHANGELOG.md
git commit -m "docs: update changelog"

git push --follow-tags origin $master

echo "Git push origin $master"
echo "Release finished."

配置package.json,增加执行命令,在发布前可以在本地运行npm run beforepublish,就可以自动修改版本号、自动打tag及生成changelog。

  "scripts": {
    "beforepublish": "./release.sh"
  }

完成了上述步骤,我们项目的初始化配置也基本告一段落了,基本可以正常跑起来了。

!本项目的仓库为happy-music。其中master分支为最新的所有代码,每次更新文章都会对应一个tag,tag是自增的,本篇文章对应的代码tag为v1.0.3。

结语

本篇文章主要讲述了整个项目的初始化过程,里面涉及的内容都很基础,但是是搭建系统不可或缺的一部分。大家可以关注我的微信公众号,会定期分享前端干货,共同成长。

@Author WaterMan