创建React项目
前言
距离上篇文章已经好长一段时间了,这两个星期公司派驻到京东方这边出差负责入驻项目团队的前端工作。这段时间从零搭建一下前端项目,这次给的时间比较充裕,思考的也比较多。以前也常有搭过前端项目,但是给的时间都比较紧,因此很多问题都忽略掉了。这次正好对以前的进行一次优化,并总结了一些经验分想给大家。如果大家有更好的想法,欢迎留言交流。
温馨提示:
- 这个项目是以PC端前端项目为视角,移动端前端项目并不完全适用。这点各位小伙们还需要注意一下。
- 该项目已分不同方向去维护,每个分支与之对应的方向可在CONTRIBUTING.md里查看
项目说明
项目地址: https://github.com/ruichengpi...
该项目可以用我自己写的脚手架工具asuna-cli完成项目构建,我自己写的脚手架工具地址如下:
https://github.com/ruichengpi...
以上是示例项目的目录结构,下面我们将逐一进行分析**
build
这个文件主要放了一些与webpack打包的相关文件。
- build.js ---- webpack打包脚本,用于构建生产环境的包
- check-versions.js ---- 主要检测当前打包环境的node以及npm的版本是否符合要求
- utils.js ---- webpack打包所需要的一些工具库
- webpack.base.conf.js ---- webpack的一些基础配置,不同环境的webpack配置都是基于此
- webpack.dev.conf.js ---- 开发环境的webpack配置
- webpack.prod.conf.js ---- 生产环境的webpack配置
这个项目的webpack配置我是在vue-cli的项目上进行修改的,可以用于React的项目构建。目前只要开发环境和生产环境这两个环境,可能一些公司有多个环境,每个环境下webpack的配置还不同,此时可以根据不同环境建一个文件名格式为“webpack.<环境名>.conf.js”的webpack配置使用。webpack.base.conf.js里面有一些基本配置比如rules、input、output的等配置,一般来说每个环境下这些大致都是相同,一些不同之处可以用webpack-merge插件进行合并。一般来说大多数项目来说开发环境和生产环境两个webpack配置足够了。
config
这里存放着不同环境webpack所需要的配置参数。
- dev.env.js ---- 向外暴露开发环境下的环境变量NODE_ENV
- index.js ---- 存放不同环境的配置参数
- prod.env.js ---- 向外暴露生产环境下的环境变量NODE_ENV
如果你需要再加一个环境的话,可以建一个文件名为“<环境名>.env.js”并向外暴露环境变量NODE_ENV,然后在index.js中导入,进行相关参数设置。
mock
这里是用来做接口的mock的,可能很多公司都不太用,我在工作也很少去mock。这里介绍一下自己的接口mock思路,这里我选择mockjs加上json-server的组合。二者具体的使用,大家可以查看其官方文档。
- api ---- 存放不同api所对应的数据
- index.js ---- json-server的主文件
- routes.json ---- 路由的映射
package.json我配置一个script,如下:
"mock": "json-server mock/index.js --port 3000 --routes mock/routes.json"
控制台执行“npm run mock“即可。
src
api
url.js
export default {
fetchUserInfo:{
method:'get',
url:'/api/user'
},
fetchAuthorInfo:{
method:'get',
url:'/api/author'
},
fetchUserList:{
method:'get',
url:'/api/userList'
}
}
index.js
import _ from 'lodash'
import http from '@/utils/http'
import API_URL from './url';
function mapUrlObjToFuncObj(urlObj){
const API = {};
_.keys(urlObj).forEach((key)=>{
const item = urlObj[key]
API[key]=function(params){
return http[item.method](item.url,params)
}
});
return API;
}
function mapUrlObjToStrObj(urlObj){
const Url = {};
_.keys(urlObj).forEach((key)=>{
const item = urlObj[key]
Url[key]=item.url
});
return Url;
}
export const API = mapUrlObjToFuncObj(API_URL);
export const URL = mapUrlObjToStrObj(API_URL);
这里我们用来放置api的接口地址,为了后续的接口维护,我们在使用的过程中不会直接写死接口地址,而是将接口请求封装成一个个方法。通过对接口的统一维护,我们就可以做到在执行修改接口地址、修改请求方法、新增接口等等操作时,就不用在整个项目里到处找了,只要维护好url.js向外暴露的对象即可。使用方法如下:
import {API} from '@/api'
//params为请求参数
API.fetchUserInfo(params).then(response=>{
//response为返回值
...
})
assets
这里我们会放项目的所需要图片资源,这些图片资源一般来说都是做图标的,都比较小。webpack会将其转化成BASE64去使用。如果你不想以这种方式使用,可以在static目录下存放图片资源。
components
这里存放整个项目所用到的公共组件。定一个组件,这里要求是新建一个文件夹,文件夹名为组件名,另外在这个文件夹下新建index.jsx和style.scss文件。例如做一个HelloWorld组件,则应该是如下结构。
HelloWorld
- index.jsx
- style.scss //存放组件的样式
index.js
import React from 'react';
import './style.scss';
class HelloWorld extends React.PureComponent{
render(){
return (
<h4 className="u-text">Hello World</h4>
)
}
}
export default HelloWorld;
style.scss
.u-text{
color: red;
}
layouts
这里存放着布局文件。关于这个布局文件我是这么去定义它的,我在开发过程中有一些页面他们的某一部分都是相同,早之前可能大家可能会在一个React组件加<Switch>和<Route>去实现这个功能,可以这么干,没毛病。但是这个有一个不好点就是你的路由没法做统一的管理,分散在各个组件中,给后续的维护带来很多问题。为了解决这个,我选择利用props.children结合标签嵌套的方式去完成。举个例子:
先定一个layout(本职也是React组件)BasicLayout.jsx
import React from 'react';
class BasicLayout extends React.PureComponent{
render(){
const {children} = this.props;
return (
<div>
<div>隔壁老王今日行程:</div>
<div>{children}</div>
</div>
)
}
}
export default BasicLayout;
定义完之后我们可以这么使用:
import React from 'react';
import BasicLayout from '<BasicLayout的路径>'
class Work extends React.PureComponent{
render(){
return (
<BasicLayout>
<div>今天隔壁老王比较累,不工作!</div>
<BasicLayout>
)
}
}
export default BasicLayout;
最后在的dom结构如下:
<div>
<div>隔壁老王今日行程:</div>
<div>
<div>今天隔壁老王比较累,不工作!</div>
</div>
</div>
这样我们可以基于BasicLayout做出很多个像下面的页面。
<div>
<div>隔壁老王今日行程:</div>
<div>
//<不同的内容>
</div>
</div>
使用这种方法就可以将我们得所有路由写在一起了,可能有人觉得每次都要写引入BasicLayout很麻烦,有没有其他更好用的办法,在讲App.jsx的时候会说到这里就先跳过。
pages
这里的存放的都是页面级组件,跟react-router对应的路由需要一一对应。每个页面都是一个文件夹,文件名就是页面名称,每个页面都要包含如下几个文件:
- components ---- 存放当前页独有的一些组件
- redux ---- 存放三个文件actions.js、actionTypes.js、reducer.js,这几个文件应该只与这个页面相关
- index.jsx ---- 页面的入口文件
- style.scss ---- 页面所需要的样式
具体代码可以自行git clone 项目查看,这里就不贴出来了。
scss
这里存放共有的scss文件,比较一些常用的功能类、@mixin、@function等等。
store
这里有四个文件:
- actions.js
- actionTypes.js
- reducer.js
- index.js
我们知道每个页面都有自己的actions.js、actionTypes.js、reducer.js,但是这里是全局的,另外index.js会向外暴露store,然后再main.js中引入使用。
import {createStore,combineReducers,applyMiddleware} from 'redux';
import thunk from 'redux-thunk';
import API from '@/api';
import user from './reducer';
import author from '@/pages/PageOne/redux/reducer';
const rootReducer = combineReducers({
user,
author
});
const store=createStore(
rootReducer,
applyMiddleware(thunk.withExtraArgument({
API
}))
)
export default store;
这里有一个小细节,redux-thunk是可以携带一些额外的对象或者方法的,这里,我携带API对象。当我们需要在actions.js里面使用API对象时,就不需要再import导入进来。下面我们做个对比:
修改前
import * as actionTypes from './actionTypes';
import API from '../api';
export const fecthUserName=(params)=> async (dispatch,getState)=>{
const response =await API.fetchUserInfo(params);
const {success,data} = response;
if(success){
dispatch({
type:actionTypes.CHANGE_USER_NAME,
payload:data
});
}
}
修改后
import * as actionTypes from './actionTypes';
export const fecthUserName=(params)=> async (dispatch,getState,{API})=>{
const response =await API.fetchUserInfo(params);
const {success,data} = response;
if(success){
dispatch({
type:actionTypes.CHANGE_USER_NAME,
payload:data
});
}
}
utils
这里会存放一些自己的封装的js工具文件,比如我在项目基于axios封装了一个http.js,简化了axios的操作。
router/index.js
这里以配置化的防止去注册路由,并app.js里面去渲染路由标签。
import Loadable from 'react-loadable';
import createHistory from 'history/createBrowserHistory';
import BasicLayout from '@/layouts/BasicLayout';
import NavTwoLayout from '@/layouts/NavTwoLayout';
import Loading from '@/components/Loading';
import NotFound from '@/pages/Exception/404';
const Home = Loadable({loader: () => import('@/pages/Home'),loading: Loading});
const Teachers = Loadable({loader: () => import('@/pages/Teachers'),loading: Loading});
export const history = createHistory();
export const routes = [
{
path:'/',
redirect:'/navone/home'
},
{
path:'/navone',
redirect:'/navone/home',
children:[{
path:'/home',
layout:BasicLayout,
component:Home
}]
},
{
path:'/navtwo',
redirect:'/navtwo/teachers',
children:[{
path:'/teachers',
layout:NavTwoLayout,
component:Teachers
}]
},
{
path:'*',
component:NotFound
}
]
App.js
这里根据路由配置用来渲染路由标签,先放代码:
import React from 'react';
import {Router} from 'react-router-dom';
import {Switch, Route ,Redirect} from 'react-router';
import {history,routes} from '@/router';
function getRouterByRoutes(routes){
const renderedRoutesList = [];
const renderRoutes = (routes,parentPath)=>{
Array.isArray(routes)&&routes.forEach((route)=>{
const {path,redirect,children,layout,component} = route;
if(redirect){
renderedRoutesList.push(<Redirect key={`${parentPath}${path}`} exact from={path} to={`${parentPath}${redirect}`}/>)
}
if(component){
renderedRoutesList.push(
layout?<Route
key={`${parentPath}${path}`}
exact path={`${parentPath}${path}`}
render={(props)=>React.createElement(layout,props,React.createElement(component,props))} />:
<Route
key={`${parentPath}${path}`}
exact
path={`${parentPath}${path}`}
component={component}/>)
}
if(Array.isArray(children)&&children.length>0){
renderRoutes(children,path)
}
});
}
renderRoutes(routes,'')
return renderedRoutesList;
}
class App extends React.PureComponent{
render(){
return (
<Router history={history}>
<Switch>
{getRouterByRoutes(routes)}
</Switch>
</Router>
)
}
}
export default App;
这里我们需要重点讲的是之间在layouts中我们跳过的内容,能不能不每次都用layout组件去包裹代码,答案是可以的。这里我选择<Route>中的render属性。
main.js
webpack入口文件,主要一些全局js或者scss的导入,并执行react-dom下的render方法,代码如下:
import React from 'react';
import {render} from 'react-dom';
import {Provider} from 'react-redux';
import store from '@/store';
import App from '@/App';
import '@/scss/reset.scss';
import '@/scss/base.scss';
render(
<Provider store={store}>
<App/>
</Provider>,
document.getElementById('app')
)
static
这是一个静态资源目录,一般存放一些第三方工具库。这个目录主要两方面考虑:
- 有些第三方工具库没有npm包,我们无法用npm install 或者 yarn add方式添加
- 一些比较大的第三方工具库会影响我们的打包速度,可以把它拿出来通过script的方式引入
其实第三方工具库最好的方式是CDN,但是有些公司就是没有,无奈只能如此。你加入的第三工具库都可在当前服务器下”/static/*“路径下获取到。
templates
这里存放着页面和组件级别构建所需要的模板文件,页面级别构建提供了两种模板PageReducer(集成了reducer)和PageSample(不集成reducer),而组件只提供了一种模板ComSample。页面和组件级别的构建是需要配合asuna-cli才能构建,目前项目已经集成了asuna-cli。package.json写了两个script:npm run newPage(页面构建)和npm run newComponent(组件构建)。开发可根据实际需要选择构建,asuna-cli具体使用可以去https://github.com/ruichengpi...查看。
其他文件
- .babelrc ---- babel转换的配置文件
- .gitignore ---- git操作所需要忽略的文件
- .postcssrc.js ---- postcss的配置文件
- index.html ---- 模板index.html,webpack会根据此生成新的index.html,配合html-webpack-plugin使用
- package.json ---- 家喻户晓的东西
- README.md ---- 项目说明
- theme.js ---- ant-design的主题色配置文件,具体使用可以参考ant-design
- asuna.config.js ---- asuna-cli的配置文件
- yarn.lock ---- 锁定包的版本
结语
这个只是个人搭建企业级React项目的一些总结。当然存在不足的地方,后面在工作过程中如果有一些好的想法也会在这上面进行更新。欢迎大家Star关注!如果你也有好的想法欢迎留言交流,希望这篇拙文能给大家一些启发。
如何从零开始创建React项目(三种方式)
在开发React项目前最关键的当然是项目的创建,现在的前端工程化使得前端项目的创建也变得越来越复杂,在这里介绍三种从零开始创建React项目的方式,分别是在浏览器中直接引入、使用官方脚手架create-react-app、使用Webpack创建。
浏览器中通过标签直接引入
React框架有两个核心的包,分别是react以及react-dom,如何想直接在浏览器中使用React,那么把这两个包直接引入就可以了。
<!-- 引入react -->
<script src="https://unpkg.com/react@16/umd/react.development.js" crossorigin></script>
<!-- 引入react-dom -->
<script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js" crossorigin></script>
如果想要使用JSX语法,那么必须引入Babel。
<!-- 引入Babel,使浏览器可以识别JSX语法,如果不使用JSX语法,可以不引入 -->
<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
接下来我会以一个完整的html示例来给大家展示,在刚开始学习React的时候可以使用这种方式。
首先创建一个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>React</title>
</head>
<body>
</body>
</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>React</title>
<!-- 引入react -->
<script src="https://unpkg.com/react@16/umd/react.development.js" crossorigin></script>
<!-- 引入react-dom -->
<script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js" crossorigin></script>
<!-- 引入Babel,使浏览器可以识别JSX语法,如果不使用JSX语法,可以不引入 -->
<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
</head>
<body>
</body>
</html>
在body标签中创建Dom结构以及script标签,这里因为引入了babel,所以script标签的type必须是"text/babel"。
<!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>React</title>
<!-- 引入react -->
<script src="https://unpkg.com/react@16/umd/react.development.js" crossorigin></script>
<!-- 引入react-dom -->
<script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js" crossorigin></script>
<!-- 引入Babel,使浏览器可以识别JSX语法,如果不使用JSX语法,可以不引入 -->
<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
</head>
<body>
<div id="app"></div>
<script type="text/babel">
// 必须添加type="text/babel",否则不识别JSX语法
</script>
</body>
</html>
然后在scirpt中写React代码
<!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>React</title>
<!-- 引入react -->
<script src="https://unpkg.com/react@16/umd/react.development.js" crossorigin></script>
<!-- 引入react-dom -->
<script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js" crossorigin></script>
<!-- 引入Babel,使浏览器可以识别JSX语法,如果不使用JSX语法,可以不引入 -->
<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
</head>
<body>
<div id="app"></div>
<script type="text/babel">
// 必须添加type="text/babel",否则不识别JSX语法
class App extends React.Component {
render() {
return(
<div>
<h1>Hello World</h1>
</div>
)
}
}
ReactDOM.render(<App />, document.getElementById('app'))
</script>
</body>
</html>
最后在浏览器中打开index.html,页面上会渲染出Hello World。
使用官方脚手架creact-react-app
这种方式其实比较简单,官方以及替我们封装好了需要的库,我们只要直接使用就可以来。
使用脚手架也有两种方式。
第一种是官方网站教程给出的方式,使用npx命令
npx create-react-app <项目名>
我们用这条命令来创建一个my-app的项目
npx create-react-app my-app
创建完成后会在当前目录下出现一个my-app的文件夹,进入my-app目录,运行npm run start
cd my-app
npm run start
然后就可以在浏览器中看到默认的页面
项目创建完成的页面机构如下
这个结构还是比较清晰的,稍微有前端开发经验的程序员应该都可以看懂,初学者可以直接在App.js中写React代码。
接下来介绍第二种使用脚手架的方式,其实相差不大,这是方式是使用npm命令,和vue-cli非常类似。
首先通过npm全局安装create-react-app
npm install -g create-react-app
mac用户如果安装不成功可以加上sudo命令
sudo npm install -g create-react-app
然后使用create-react-app命令来创建项目
create-react-app <项目名>
创建my-app项目
create-react-app my-app
创建出的项目和第一种方式创建的项目一致。
使用webpack、babel、react来创建React项目
初始化项目
首先第一步我们先创建一个名字是my-app的文件夹
mkdir my-app
进入该目录
cd my-app
然后在my-app目录下创建src文件夹用来存放React代码
mkdir src
使用npm命令初始化项目
npm init -y
此时的项目结构如下
安装webpack
首先安装webpack和webpack-cli,webpack-cli包含了webpack的众多指令,所以需要安装。
npm install webpack webpack-cli --save-dev
注意: 在这里简单介绍一下npm install命令的参数 --save-dev 和 --save的区别,一般来说使用--save-dev参数安装的npm包在最终打包的时候不会被包括到源码里去,所以类似bebel和webpack这种进行项目工程构建或者代码编译的库应该用--save-dev来安装,而--save则是安装代码运行必须的库,比如react等。
安装Babel
进行前端工程化的时候大多数前端工程师都会用到babel,最开始的babel是用来把es6的代码编译成es5的代码,让前端开发者在使用新的特性的同时不必考虑浏览器兼容问题。虽然现在的主流浏览器已经支持大部分的es6的新特性,但是因为JavaScript每年都会有一些新的特性被提出,而浏览器不一定能在特性推出后及时实现,或者是有一些还在实验中的语法。使用来babel后就可以忽略这些问题,可以放心使用新的JavaScript语法,甚至是实验性的语法。
接下来我们会安装这几个包:
- @babel/core
- @babel/preset-env
- @babel/preset-react
- babel-loader
很明显@babel/core是babel的核心库,必须安装,@babel/preset-env帮助我们把es6的语法编译成es5的语法,@babel/preset-react则是帮我们识别JSX语法,babel-loader则是帮我们把不同的文件转化成我们想要的格式输出,或者说就是将我们的经过babel处理后的代码进行输出成浏览器可以识别的文件。
安装指令
npm install
在安装成功后必须进行babel的配置,在根目录my-app建立.babelrc文件,然后写入以下配置
{
"presets": ["@babel/preset-env", "@babel/preset-react"]
}
然后我们需要做一些webpack的配置,在根目录my-app建立webpack.config.js文件,然后写入以下配置
const path = require('path');
module.exports = {
entry: './src/main.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: "babel-loader"
}
}
]
}
};
在babel配置完之后,我们需要在./src目录下新建三个文件index.html、main.js、App.js,此时我们的项目的所有文件都创建完毕,项目结构应该如下所示:
接下来因为webpack默认只能对.js文件进行最终打包,而我们的项目是有.html文件的,所以我们必须下载和html有关的loader和插件来对html进行处理。
处理html
安装html-webpack-plugin和html-loader
npm install html-webpack-plugin html-loader --save-dev
在安装完成之后我们需要在webpack.config.js中进行配置
webpack.config.js文件内容如下
const path = require('path');
const HtmlWebPackPlugin = require("html-webpack-plugin");
module.exports = {
entry: './src/main.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader'
}
},
{
test: /\.html$/,
use: {
loader: 'html-loader'
}
}
]
},
plugins: [
new HtmlWebPackPlugin({
titel: 'react app',
filename: 'index.html',
template: './src/index.html'
})
]
};
配置完成后我们开始写react代码,首先在index.html文件中写入以下代码(在一个基本.html页面中加一个id是app的div)
<!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>React</title>
</head>
<body>
<div id="app"></div>
</body>
</html>
然后在写react代码前需要安装react和react-dom
npm install react react-dom --save
在App.js文件中创建一个组件并导出
import React from 'react'
class App extends React.Component {
render() {
return(
<div>
<h1>Hello World</h1>
</div>
)
}
}
export default App
在main.js中将组件导入并渲染
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App.js'
ReactDOM.render(<App/>, document.getElementById('app'))
运行项目
所有的代码已经完毕,在运行前我们还要安装webpack-dev-server用来启动一个本地服务器来浏览我们的项目并且可以实现保存自动刷新
npm install webpack-dev-server --save-dev
然后在根目录的package.json中写一个脚本
"scripts": {
"start": "webpack-dev-server --open --mode development"
}
最后运行npm run start就可以在浏览器中看到Hello World了
npm run start
总结
这篇文章介绍了三种创建React的方式,一般来说使用官方的脚手架比较方便,自己用webpack配置的话则更加灵活,根据项目的不同需要选择不同的方式。