React router、自定义 hook,利用 CSS 和 webpack 给 app 添加样式
React-router
使用 React Router 库生成导航栏:
import { BrowserRouter as Router , Routes , Route , Link } from 'react-router-dom' ;const App = ( ) => { const padding = { padding : 5 , }; return ( <Router > <div > <Link style ={padding} to ='/' > home </Link > <Link style ={padding} to ='/notes' > notes </Link > <Link style ={padding} to ='/users' > users </Link > </div > <Routes > <Route path ='/notes' element ={ <Notes /> } /> <Route path ='/users' element ={ <Users /> } /> <Route path ='/' element ={ <Home /> } /> </Routes > <div > <i > Note app, Department of Computer Science 2022</i > </div > </Router > ); };
可以添加参数路由:
<Route path='/notes/:id' element={<Note notes ={notes} /> } />
使用 useNavigate()
和 navigate('/')
可以手动导航
如果一个用户没有登录,则会被重定向到登录页面:
<Route path='/users' element={user ? <Users /> : <Navigate replace to ='/login' /> } />
useMatch 钩子可以计算出所需 id:
const match = useMatch ('/notes/:id' );const note = match ? notes.find ((note ) => note.id === Number (match.params .id )) : null ;
自定义 hooks
我们也可以自定义钩子:
const useField = (type ) => { const [value, setValue] = useState ('' ); const onChange = (event ) => { setValue (event.target .value ); }; return { type, value, onChange }; };
样式进阶
先介绍 Bootstrap ,使用 react-bootstrap 软件包,并在 index.html
的 head
中加载 CSS:
<head > <link rel ="stylesheet" href ="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" integrity ="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin ="anonymous" /> </head >
应用的所有内容在容器中渲染,给应用的根 div 元素加上 container 类属性
const App = ( ) => { return <div className ='container' > // ...</div > ; };
有表格 ,表单 ,通知 ,导航栏 等
接下来是 MaterialUI React 库,要安装:
npm install @mui/material @emotion/react @emotion/styled
index.html
中添加:
index.html <head > <link rel ="stylesheet" href ="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" /> </head >
渲染方式是:
import { Container } from '@mui/material' ;const App = ( ) => { return <Container > // ...</Container > ; };
同样有表格 ,文本框 ,按钮 ,Alert ,AppBar
还可以使用 styled components 库提供的方式定义样式:
import styled from 'styled-components' ;const Input = styled.input ` margin: 0.25em; ` ;
然后正常使用即可
Webpack
webpack 可用于创建 app
一个 webpack.config.js
文件的例子:
webpack.config.js const path = require ('path' );const config = { entry : './src/index.js' , output : { path : path.resolve (__dirname, 'build' ), filename : 'main.js' , }, };module .exports = config;
可以使用 loaders 来通知 webpack 在捆绑前需要处理的文件,例如将 .jsx
文件转化为普通的 .js
文件:
module : { rules : [ { test : /\.js$/ , loader : 'babel-loader' , options : { presets : ['@babel/preset-react' ], }, }, ], },
还需要安装并引入 core-js 和 regenerator-runtime
当使用 CSS 时,我们必须使用 css 和 style 加载器。
source map 有助于我们看到源代码
各种各样的 Class components
React 元素构成了一个虚拟 DOM
npm-check-updates
可以用来检查依赖的更新
GraphQL
GraphQL 服务器
REST 是基于资源的,对资源的所有操作通过对其 URL 的 HTTP 请求完成。
但是 GraphQL 的所有查询都被发送到一个地址,类型是 POST
一个定义的模式的例子:
type Person { name : String ! phone : String street : String ! city : String ! id : ID ! } type Query { personCount : Int ! allPersons : [Person !]! findPerson (name : String !): Person }
查询:
query { allPersons { name phone } }
返回值:
{ "data" : { "allPersons" : [ { "name" : "Arto Hellas" , "phone" : "040-123543" } , { "name" : "Matti Luukkainen" , "phone" : "040-432342" } , { "name" : "Venla Ruuska" , "phone" : null } ] } }
也可以带有参数
实现服务器使用 Apollo Server
定义模式:
const { ApolloServer , gql } = require ('apollo-server' );const typeDefs = gql` type Person { name : String! phone : String street : String! city : String! id : ID! } type Query { personCount : Int! allPersons : [ Person! ] ! findPerson( name : String! ) : Person } ` ;const resolvers = { Query : { personCount : () => persons.length , allPersons : () => persons, findPerson : (root, args ) => persons.find ((p ) => p.name === args.name ), }, };const server = new ApolloServer ({ typeDefs, resolvers, }); server.listen ().then (({ url } ) => { console .log (`Server ready at ${url} ` ); });
对象之间也可以组合,如模式为:
type Address { street : String ! city : String ! } type Person { name : String ! address : Address ! id : ID ! }
所有引起变化的操作通过 mutation 完成,如添加一个新人:
type Mutation { addPerson ( name : String ! phone : String street : String ! city : String ! ): Person }
可以使用 Apollo 的错误处理机制,如抛出 throw new UserInputError('Name must be unique', {invalidArgs: args.name, })
GraphQL 支持枚举类型:
enum YesNo { YES NO } type Query { personCount : Int ! allPersons (phone : YesNo ): [Person !]! findPerson (name : String !): Person }
React 与 GraphQL
客户端使用 ApolloProvider
包装 APP 组件:
import ReactDOM from 'react-dom' ;import App from './App' ;import { ApolloClient , ApolloProvider , HttpLink , InMemoryCache , } from '@apollo/client' ;const client = new ApolloClient ({ cache : new InMemoryCache (), link : new HttpLink ({ uri : 'http://localhost:4000' , }), });ReactDOM .render ( <ApolloProvider client ={client} > <App /> </ApolloProvider > , document .getElementById ('root' ) );
查询可以使用 useQuery
:
import { gql, useQuery } from '@apollo/client' ;const ALL_PERSONS = gql` query { allPersons { name phone id } } ` ;const App = ( ) => { const result = useQuery (ALL_PERSONS ); if (result.loading ) { return <div > loading...</div > ; } return <div > {result.data.allPersons.map((p) => p.name).join(', ')}</div > ; };
如果是一个带参数的查询,可以:
const FIND_PERSON = gql` query findPersonByName( $nameToSearch : String! ) { findPerson( name : $nameToSearch ) { name phone id address { street city } } } ` ;const [nameToSearch, setNameToSearch] = useState (null );const result = useQuery (FIND_PERSON , { variables : { nameToSearch }, skip : !nameToSearch, });if (nameToSearch && result.data ) { return ( <Person person ={result.data.findPerson} onClose ={() => setNameToSearch(null)} /> ); }
更新缓存有几种方法,第一种是 poll 服务器:
const result = useQuery (ALL_PERSONS , { pollInterval : 2000 });
缺点在于无意义的网络流量
第二种是使用 refetchQueries
定义,每当创建一个新的人,就重新进行获取所有人员的查询:
const [ createPerson ] = useMutation (CREATE_PERSON , { refetchQueries : [ { query : ALL_PERSONS }, { query : OTHER_QUERY }, { query : ... } ] })
缺点在于另一个用户更新的数据不会立刻反应给其他用户
可以定义错误处理函数:
const PersonForm = ({ setError } ) => { const [createPerson] = useMutation (CREATE_PERSON , { refetchQueries : [{ query : ALL_PERSONS }], onError : (error ) => { setError (error.graphQLErrors [0 ].message ); }, }); };
数据库与用户管理
用户管理的模式:
type User { username : String ! friends : [Person !]! id : ID ! } type Token { value : String ! } type Query { me : User } type Mutation { createUser ( username : String ! ): User login ( username : String ! password : String ! ): Token }
实现:
const jwt = require ('jsonwebtoken' )const JWT_SECRET = 'NEED_HERE_A_SECRET_KEY' Mutation : { createUser : async (root, args) => { const user = new User ({ username : args.username }) return user.save () .catch (error => { throw new UserInputError (error.message , { invalidArgs : args, }) }) }, login : async (root, args) => { const user = await User .findOne ({ username : args.username }) if ( !user || args.password !== 'secret' ) { throw new UserInputError ("wrong credentials" ) } const userForToken = { username : user.username , id : user._id , } return { value : jwt.sign (userForToken, JWT_SECRET ) } }, },
在构造函数调用中添加第三个参数 context
来扩展 server
对象的定义。
const server = new ApolloServer ({ typeDefs, resolvers, context : async ({ req }) => { const auth = req ? req.headers .authorization : null ; if (auth && auth.toLowerCase ().startsWith ('bearer ' )) { const decodedToken = jwt.verify (auth.substring (7 ), JWT_SECRET ); const currentUser = await User .findById (decodedToken.id ).populate ( 'friends' ); return { currentUser }; } }, });
登录与更新缓存
向 header 中添加令牌:
import { setContext } from '@apollo/client/link/context' ;const authLink = setContext ((_, { headers } ) => { const token = localStorage .getItem ('phonenumbers-user-token' ); return { headers : { ...headers, authorization : token ? `bearer ${token} ` : null , }, }; });const httpLink = new HttpLink ({ uri : 'http://localhost:4000' });const client = new ApolloClient ({ cache : new InMemoryCache (), link : authLink.concat (httpLink), });
还有一种手动更新缓存的方式:
const [createPerson] = useMutation (CREATE_PERSON , { update : (cache, response ) => { cache.updateQuery ({ query : ALL_PERSONS }, ({ allPersons } ) => { return { allPersons : allPersons.concat (response.data .addPerson ), }; }); }, });
Fragments 与 subscriptions
两个查询必须定义完全相同的字段时,可以通过 fragment 来简化,如:
const PERSON_DETAILS = gql` fragment PersonDetails on Person { id name phone address { street city } } ` ;export const FIND_PERSON = gql` query findPersonByName( $nameToSearch : String! ) { findPerson( name : $nameToSearch ) { ... PersonDetails } } ${PERSON_DETAILS} ` ;
Apollo 使用 WebSockets 进行服务器用户的通信
用 Apollo Server Express 替换 Apollo 服务器
TypeScript
背景与介绍
TypeScript 可以被编译为 JavaScript,是它的一个超集,由语言、编译器、语言服务三部分组成
一个例子:
const birthdayGreeter = (name : string , age : number ): string => { return `Happy birthday ${name} , you are now ${age} years old!` ; };
其中函数的返回值可以不写,能够自动推断出来
支持 callback:
type CallsFunction = (callback: (result: string ) => any ) => void ;const func : CallsFunction = (cb ) => { cb ('done' ); cb (1 ); };func ((result ) => { return result; });
TypeScript 的一小步
需要安装的包:
npm install --save-dev ts-node typescript
可以自定义类型:
type Operation = 'multiply' | 'add' | 'divide' ;
也可以通过定义接口来定义类型:
interface MultiplyValues { value1 : number ; value2 : number ; }
npm 包通常有对应的 typescript 版本,名称为 @types/{npm_package}
,且总是应该安装在 devDependencies
中,并仍需要安装相应的 JavaScript 版本
tsconfig.json
文件中包含了 TypeScript 的核心配置
TypeScript 版的 express 应用
使用 tsc
生成 tsconfig.json
文件
nodemon
的 TypeScript 替代品为 ts-node-dev
关键字 as
断言变量类型
可以通过在类型声明中添加 ?
,将字段的类型设置为 optional
注意节点模块的加载顺序: ["js", "json", "node", "ts", "tsx"]
,最好的方法是避免重名
Pick
实用类型允许选择想使用的现有类型的哪些字段:
const getNonSensitiveEntries = (): Pick < DiaryEntry , 'id' | 'date' | 'weather' | 'visibility' >[] => {};
Omit
可以用来声明要排除哪些字段:
export type NonSensitiveDiaryEntry = Omit <DiaryEntry , 'comment' >;
注意这只是禁止访问而已,并不是不返回那些被排除的字段而已,所以需要手动排除这些字段:
const getNonSensitiveEntries = (): NonSensitiveDiaryEntry [] => { return diaries.map (({ id, date } ) => ({ id, date })); };
使用 unknown
可以先验证类型,然后确认预期类型,如:
const parseComment = (comment : unknown ): string => { if (!comment || !isString (comment)) { throw new Error ('Incorrect or missing comment' ); } return comment; };
利用 TypeScript 编写 React 应用
创建一个 TypeScript 应用:
npx create-react-app my-app --template typescript
组件:
const Welcome = ({ name }: { name: string } ) => <h1 > Hello, {name}</h1 > ;
接口可以继承,这样就可以不同类型共享相同的域,然后通过 switch
等语句区分不同的具体类型:
interface CoursePartBase { name : string ; exerciseCount : number ; }interface CoursePartOne extends CoursePartBase { name : 'Fundamentals' ; description : string ; }interface CoursePartTwo extends CoursePartBase { name : 'Using props to pass data' ; groupProjectCount : number ; }type CoursePart = CoursePartOne | CoursePartTwo ;
React Native
React Native 介绍
对于跨平台方案,Cordova 使用标准的网络技术--HTML5、CSS3 和 JavaScript 来开发多平台应用,在嵌入式浏览器窗口中运行,故不能达到使用实际本地用户界面组件的本地应用的性能或外观和感觉的
React Native 将 React 的大量功能带入本地开发,在幕后利用平台的本地组件
使用 Expo 初始化:
npx create-expo-app rate-repository-app --template expo-template-blank@sdk-50
可以使用安卓模拟器或 Expo Go 移动应用查看
安装 eslint:
npm install --save-dev eslint @babel/eslint-parser eslint-plugin-react eslint-plugin-react-native
React Native 入门
React Native 中有很多预先定义好的核心组件 ,例如文本:
import { Text } from 'react-native' ;const HelloWorld = (props ) => { return <Text > Hello world!</Text > ; };
类似的还有 View ,TextInput ,Pressable 等组件
注意 react native 中所有样式属性都是无单位的,可用样式属性详见 React Native Styling Cheat Sheet ,其用法为:
import { Text , View , StyleSheet } from 'react-native' ;const styles = StyleSheet .create ({ container : { padding : 20 , }, text : { color : 'blue' , fontSize : 24 , fontWeight : '700' , }, });const BigBlueText = ( ) => { return ( <View style={styles.container}> <Text style={styles.text}> Big blue text <Text> </View> ); };
使用 flexbox 布局:
import { View , Text , StyleSheet } from 'react-native' ;const styles = StyleSheet .create ({ flexContainer : { display : 'flex' , }, flexItemA : { flexGrow : 0 , backgroundColor : 'green' , }, flexItemB : { flexGrow : 1 , backgroundColor : 'blue' , }, });const FlexboxExample = ( ) => { return ( <View style ={styles.flexContainer} > <View style ={styles.flexItemA} > <Text > Flex item A</Text > </View > <View style ={styles.flexItemB} > <Text > Flex item B</Text > </View > </View > ); };
使用 react-router-native 来实现路由,添加到组件:
import { StatusBar } from 'expo-status-bar' ;import { NativeRouter } from 'react-router-native' ;const App = ( ) => { return ( <> <NativeRouter > <Main /> </NativeRouter > <StatusBar style ='auto' /> </> ); };
添加一个路由:
const Main = ( ) => { return ( <View > <AppBar /> <Routes > <Route path ='/' element ={ <RepositoryList /> } exact /> <Route path ='*' element ={ <Navigate to ='/' replace /> } /> </Routes > </View > ); };
用 Formik 来管理表单:
import { useFormik } from 'formik' ;const initialValues = { mass : '' , };const BodyMassIndexForm = ({ onSubmit } ) => { const formik = useFormik ({ initialValues, onSubmit, }); return ( <View > <TextInput placeholder ="Weight (kg)" value ={formik.values.mass} onChangeText ={formik.handleChange( 'mass ')} /> <Pressable onPress ={formik.handleSubmit} > <Text > Calculate</Text > </Pressable > </View > ); };const BodyMassIndexCalculator = ( ) => { const onSubmit = values => {...}; return <BodyMassIndexForm onSubmit ={onSubmit} /> ; };export default BodyMassIndexCalculator ;
使用 Yup 验证:
import * as yup from 'yup' ;const validationSchema = yup.object ().shape ({ mass : yup .number () .min (1 , 'Weight must be greater or equal to 1' ) .required('Weight is required' ), });const formik = useFormik ({ initialValues, validationSchema, onSubmit, });
条件渲染错误提示:
{ formik.touched .mass && formik.errors .mass && ( <Text style ={{ color: 'red ' }}> {formik.errors.mass}</Text > ); }
可以通过 Platform.OS
常量来访问用户的平台:
Platform .OS === 'android' ? 'green' : 'blue' ;
Platform.select
方法:给定一个对象,其键值为 ios
、android
、native
和 default
之一:
const styles = StyleSheet .create ({ text : { color : Platform .select ({ android : 'green' , ios : 'blue' , default : 'black' , }), }, });
与服务端通信
React Native 提供了 Fetch API ,如:
fetch ('https://my-api.com/post-end-point' , { method : 'POST' , headers : { Accept : 'application/json' , 'Content-Type' : 'application/json' , }, body : JSON .stringify ({ firstParam : 'firstValue' , secondParam : 'secondValue' , }), });
也可以通过 XMLHttpRequest API 使用 Axios 等第三方库
这里作为 GraphQL 和 Apollo client,需要安装:
npm install @apollo/client graphql @expo/metro-config
在根目录下的 metro.config.js
添加:
const { getDefaultConfig } = require ('@expo/metro-config' );const defaultConfig = getDefaultConfig (__dirname); defaultConfig.resolver .sourceExts .push ('cjs' );module .exports = defaultConfig;
创建一个 apolloClient.js
文件,配置连接到 Apollo 服务器的客户端:
import { ApolloClient , InMemoryCache , createHttpLink } from '@apollo/client' ;const httpLink = createHttpLink ({ uri : 'http://192.168.100.16:4000/graphql' });const createApolloClient = ( ) => { return new ApolloClient ({ link : httpLink, cache : new InMemoryCache (), }); };export default createApolloClient;
添加到组件:
import { ApolloProvider } from '@apollo/client' ;const apolloClient = createApolloClient ();const App = ( ) => { return ( <NativeRouter > <ApolloProvider client ={apolloClient} > <Main /> </ApolloProvider > </NativeRouter > ); };
在组织代码时,可以在 graphql 目录下创建 queries.js
,每个查询都存储在一个变量中,并一个个导出。改变则储存在 mutations.js,同时善于使用 fragments.js 来精简代码
我们有组件A ,它渲染了组件 B 和 C,则目录可以为:
components/ A/ B.jsx C.jsx index.jsx
而不需要改动引入组件 A 的代码,其自动匹配到 A/index.jsx
React Native 没有对环境变量的直接支持,但可以通过改变 app.json
文件中定义的 Expo 配置来实现:
import 'dotenv/config' ;export default { name : 'rate-repository-app' , extra : { env : process.env .ENV , }, };
并配合熟悉的 dotenv
持久化本地存储 AsyncStorage :
expo install @react-native-async-storage/async-storage
和 localStorage
类似,但顾名思义,其操作是异步 的,因为是在全局命名空间操作,故最好添加一个命名空间
import AsyncStorage from '@react-native-async-storage/async-storage' ;class ShoppingCartStorage { constructor (namespace = 'shoppingCart' ) { this .namespace = namespace; } async getProducts ( ) { const rawProducts = await AsyncStorage .getItem ( `${this .namespace} :products` ); return rawProducts ? JSON .parse (rawProducts) : []; } async addProduct (productId ) { const currentProducts = await this .getProducts (); const newProducts = [...currentProducts, productId]; await AsyncStorage .setItem ( `${this .namespace} :products` , JSON .stringify (newProducts) ); } async clearProducts ( ) { await AsyncStorage .removeItem (`${this .namespace} :products` ); } }const doShopping = async ( ) => { const shoppingCartA = new ShoppingCartStorage ('shoppingCartA' ); await shoppingCartA.addProduct ('chips' ); const productsA = await shoppingCartA.getProducts (); await shoppingCartA.clearProducts (); };
将访问令牌发送给 Apollo 服务器:
import { ApolloClient , InMemoryCache , createHttpLink } from '@apollo/client' ;import Constants from 'expo-constants' ;import { setContext } from '@apollo/client/link/context' ;const { apolloUri } = Constants .manifest .extra ;const httpLink = createHttpLink ({ uri : apolloUri });const createApolloClient = (authStorage ) => { const authLink = setContext (async (_, { headers }) => { try { const accessToken = await authStorage.getAccessToken (); return { headers : { ...headers, authorization : accessToken ? `Bearer ${accessToken} ` : '' , }, }; } catch (e) { console .log (e); return { headers, }; } }); return new ApolloClient ({ link : authLink.concat (httpLink), cache : new InMemoryCache (), }); };export default createApolloClient;
使用 React Context 来使挂钩访问到 token:
import React from 'react' ;const AuthStorageContext = React .createContext ();export default AuthStorageContext ;
在 app.js
中:
import AuthStorageContext from './src/contexts/AuthStorageContext' ;const authStorage = new AuthStorage ();const App = ( ) => { return ( <AuthStorageContext.Provider value ={authStorage} > <Main /> </AuthStorageContext.Provider > ); };
使用 useContext
钩访问:
import { useContext } from 'react' ;import AuthStorageContext from '../contexts/AuthStorageContext' ;const useAuthStorage = ( ) => { return useContext (AuthStorageContext ); };export default useAuthStorage;
测试与扩展我们的应用
安装 Jest
npm install --save-dev jest jest-expo eslint-plugin-jest
在 package.json 文件中加入 Jest 配置:
{ "scripts" : { "test" : "jest" } , "jest" : { "preset" : "jest-expo" , "transform" : { "^.+\\.jsx?$" : "babel-jest" } , "transformIgnorePatterns" : [ "node_modules/(?!((jest-)?react-native|@react-native(-community)?)|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|react-navigation|@react-navigation/.*|@unimodules/.*|unimodules|sentry-expo|native-base|react-native-svg|react-router-native)" ] } }
把 eslint-plugin-jest 包含在 .eslintrc
文件的插件和扩展数组中:
"extends" : [ "eslint:recommended" , "plugin:react/recommended" , "plugin:jest/recommended" ] ,
存放 test 文件有两种方式,第一种是把测试文件放在其相应的子目录中,就像代码本身一样,如:
src/ __tests__/ components/ AppBar.js RepositoryList.js utils/ authStorage.js
另一种方法是在实现附近组织测试,即在同一目录下(注意添加 .test
后缀):
src/ components/ AppBar/ AppBar.test.jsx index.jsx
测试组件安装:
npm install --save-dev react-test-renderer@17.0.1 @testing-library/react-native @testing-library/jest-native
一个测试的例子:
import { Text , View } from 'react-native' ;import { render } from '@testing-library/react-native' ;const Greeting = ({ name } ) => { return ( <View > <Text > Hello {name}!</Text > </View > ); };describe ('Greeting' , () => { it ('renders a greeting message based on the name prop' , () => { const { debug, getByText } = render (<Greeting name ='Kalle' /> ); debug (); expect (getByText ('Hello Kalle!' )).toBeDefined (); }); });
对于一些非纯函数,如需要与服务器通信,可以将非纯的部分提取出来,单独测试纯函数部分:
export const RepositoryListContainer = ({ repositories } ) => { const repositoryNodes = repositories ? repositories.edges .map ((edge ) => edge.node ) : []; return <FlatList data ={repositoryNodes} /> ; };const RepositoryList = ( ) => { const { repositories } = useRepositories (); return <RepositoryListContainer repositories ={repositories} /> ; };export default RepositoryList ;
当 API 从某个集合中返回一个有序的项目列表时,它通常会返回整个项目集的一个子集,以减少所需的带宽,并降低客户端应用的内存使用。客户端就可以请求例如列表中某个索引后的前 20 个项目。这种技术通常被称为分页
当项目可以在由游标 定义的某个项目之后被请求时,我们谈论的就是基于游标的分页 。
这是一个查询:
{ repositories( first : 2 ) { edges { node { fullName } cursor } pageInfo { endCursor startCursor hasNextPage } } }
然后使用返回的 endCursor
进行下一步查询
无限滚动 的实现方法:
export const RepositoryListContainer = ({ repositories, onEndReach } ) => { const repositoryNodes = repositories ? repositories.edges .map ((edge ) => edge.node ) : []; return ( <FlatList data ={repositoryNodes} onEndReached ={onEndReach} onEndReachedThreshold ={0.5} /> ); };const RepositoryList = ( ) => { const { repositories } = useRepositories (); const onEndReach = ( ) => { fetchMore (); }; return ( <RepositoryListContainer repositories ={repositories} onEndReach ={onEndReach} /> ); };export default RepositoryList ;
并定义相关的 fetchMore
函数
const useRepositories = (variables ) => { const { data, loading, fetchMore, ...result } = useQuery (GET_REPOSITORIES , { variables, }); const handleFetchMore = ( ) => { const canFetchMore = !loading && data?.repositories .pageInfo .hasNextPage ; if (!canFetchMore) { return ; } fetchMore ({ variables : { after : data.repositories .pageInfo .endCursor , ...variables, }, }); }; return { repositories : data?.repositories , fetchMore : handleFetchMore, loading, ...result, }; };
Awesome React Native 中有丰富的 React Native 资源,如 React Native Paper ,Styled-components ,React-spring ,React Navigation 等
CI/CD
CI/CD 简介
CI 持续集成 :经常性地将开发人员的修改合并到主分支上,通常有以下的步骤:
提示:保持我们的代码清洁和可维护
构建:将我们所有的代码整合成软件
测试:以确保我们不会破坏现有的功能
打包:把它全部放在一个容易移动的批次中
上传/部署:将它提供给全世界
CD 主分支始终保持可部署 ,两者并没有清晰的界限
CI 有两种选择:托管我们自己的服务器(Jenkins )或使用云服务(GitHub Actions )
开始认识 GitHub Actions 吧
GitHub Action 的基础是工作流,执行过程为:
触发事件发生
带有该触发器的工作流被执行
清理
每个工作流必须指定至少一个 job,其中包含一组 step 来执行单个任务。jobs 并行 执行,每个 job 中的 steps 会顺序 执行
工作流存储在 ``.github/workflows中,格式为
YAML`
工作流也有 hello world:
name: Hello World! on: push: branches: - master jobs: hello_world_job: runs-on: ubuntu-20.04 steps: - name: Say hello run: echo "Hello World!"
使用 usess
执行定义好的动作,如:
jobs: simple_deployment_pipeline: runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v2 with: node-version: '20' - name: npm install run: npm install - name: lint run: npm run eslint
部署
任何事情都有可能出错
保持健康状态
为了保持主分支的绿色,通常避免将任何修改直接提交到主分支,而是在一个基于主分支的最新版本的分支上提交代码,然后创建 PR 合并到主分支
向工作流中加入 PR 时自动检查代码的功能:
on: push: branches: - master pull_request: branches: [master ] types: [opened , synchronize ]
版本管理:
语义版本管理:易读,通常用于发布
哈希版本控制:保证唯一,通常用于开发
github 可以设置受保护的分支
再扩展一下
在构建失败时发送通知
随着项目越来越复杂,其构建时间也越来越长,要权衡取舍
容器
容器介绍
使用 Docker
容器也有 hello world
:
docker container run hello-world
docker container ls -a
列出所有的容器
docker start 容器名或 ID
启动容器
docker container run -it ubuntu bash
在 bash
命令行下,以交互式启动 ubuntu
image
docker kill 容器名或 ID
停止某个容器
docker commit 容器 镜像
从容器中创建一个新的镜像
docker image ls
列出所有镜像
docker container rm 容器名或 ID
删除某个容器
构建配置环境
在 Dockerfile
文件中配置,如:
FROM node:16 WORKDIR /usr/src/app COPY ./index.js ./index.js CMD node index.js
使用 docker build
来构建一个基于 Docker 文件的镜像,-t
用于命名
.dockerignore
文件可以防止我们不小心将不需要的文件复制到镜像中
Docker-compose 可以帮助我们管理容器
这里将之前的命令转化为一个 yaml
文件:
version: '3.8' services: app: image: express-server build: . ports: - 3000 :3000
docker-compose up
可以构建应用并运行应用,-d
参数表示在后台运行,docker-compose down
关闭
绑定挂载是将主机上的文件与容器中的文件绑定的行为,在 Docker Compose 中的 volumes 键下声明:
mongo: image: mongo ports: - 3456 :27017 environment: MONGO_INITDB_ROOT_USERNAME: root MONGO_INITDB_ROOT_PASSWORD: example MONGO_INITDB_DATABASE: the_database volumes: - ./mongo/mongo-init.js:/docker-entrypoint-initdb.d/mongo-init.js
Docker 的 exec
命令可以在一个容器运行时直接跳入它,如 docker exec -it wonderful_ramanujan bash
Redis 是一个非关系型数据库,默认情况下在内存中工作,故可以用作缓存。除此之外也可以实现“发布-订阅”模式,即作为多个应用之间的消息代理工作
编排基础
是 CMD 的 exec
形式
多阶段构建:将构建过程分成许多独立的阶段,可以限制镜像文件的大小:
FROM node:16 AS build-stageWORKDIR /usr/src/app COPY . . RUN npm ci RUN npm run build FROM nginx:1.20 -alpineCOPY --from=build-stage /usr/src/app/build /usr/share/nginx/html
也可以设置为直接在容器中开发程序:
services: app: image: hello-front-dev build: context: . dockerfile: dev.Dockerfile volumes: - ./:/usr/src/app ports: - 3000 :3000 container_name: hello-front-dev
不过安装新的依赖比较麻烦,最好是在容器中安装,如 docker exec hello-front-dev npm install axios
并重新 docker build
Busybox 可以帮助我们调试我们的配置,加入
debug-helper: image: busybox
如发送一个请求:docker-compose run debug-helper wget -O - http://app:3000
注意到 docker 的容器间有一个 DNS,app
域名被解析为某容器中的网络了
使用关系型数据库
用 Sequelize 使用关系型数据库
之前使用的 MongoDB 是无模式 的,这里介绍一个关系型数据库——PostgreSQL
安装完成后,有图形用户界面,如 pgAdmin ,这里使用命令行 psql
\d
命令可以告诉数据库的内容
一个创建表的例子:
CREATE TABLE notes ( id SERIAL PRIMARY KEY, content text NOT NULL , important boolean , date time );
\d notes
查看表格 notes
添加内容:
insert into notes (content, important) values ('Relational databases rule the world' , true );insert into notes (content, important) values ('MongoDB is webscale' , false );
sequelize 库可以在不使用 SQL 语言的情况下存储 JavaScript 对象
与数据库建立连接的方法:
require ('dotenv' ).config ();const { Sequelize } = require ('sequelize' );const sequelize = new Sequelize (process.env .DATABASE_URL , { dialectOptions : { ssl : { require : true , rejectUnauthorized : false , }, }, });const main = async ( ) => { try { await sequelize.authenticate (); console .log ('Connection has been established successfully.' ); sequelize.close (); } catch (error) { console .error ('Unable to connect to the database:' , error); } };main ();
建立代码中的模型:
class Note extends Model {}Note .init ( { id : { type : DataTypes .INTEGER , primaryKey : true , autoIncrement : true , }, content : { type : DataTypes .TEXT , allowNull : false , }, important : { type : DataTypes .BOOLEAN , }, date : { type : DataTypes .DATE , }, }, { sequelize, underscored : true , timestamps : false , modelName : 'note' , } );
查找全部:
app.get ('/api/notes' , async (req, res) => { const notes = await Note .findAll (); res.json (notes); });
创建新的对象:
const note = await Note .create (req.body );
也可以分为两步:
const note = Note .build (req.body ); note.important = true ;await note.save ();
根据 id 查找:
const note = await Note .findByPk (req.params .id );
加入表与查询
项目的文件结构如下:
index.js util config.js db.js models index.js note.js controllers notes.js
两个表之间建立一对多的关系:
User .hasMany (Note );Note .belongsTo (User );Note .sync ({ alter : true });User .sync ({ alter : true });
查询时包括的内容也一并返回:
const users = await User .findAll ({ include : { model : Note }, });
还可以进一步限制想要的字段的值:
const notes = await Note .findAll ({ attributes : { exclude : ['userId' ] }, include : { model : User , attributes : ['name' ], }, });
我们可以实现一个带参数的查询,如 http://localhost:3001/api/notes?search=database&important=true
:
const where = {};if (req.query .important ) { where.important = req.query .important === 'true' ; }if (req.query .search ) { where.content = { [Op .substring ]: req.query .search }; }const notes = await Note .findAll ({ attributes : { exclude : ['userId' ] }, include : { model : user, attributes : ['name' ], }, where, });
迁移,多对多关系
迁移 migrantion 是一个单一的 JavaScript 文件,描述了对数据库的一些修改,迁移引起的变化同步到数据库模式中,最终实现了修改可控
这是一个迁移的例子:
module .exports = { up : async ({ context : queryInterface }) => { await queryInterface.createTable ('notes' , { id : { type : DataTypes .INTEGER , primaryKey : true , autoIncrement : true , }, content : { type : DataTypes .TEXT , allowNull : false , }, }); await queryInterface.createTable ('users' , { id : { type : DataTypes .INTEGER , primaryKey : true , autoIncrement : true , }, username : { type : DataTypes .STRING , unique : true , allowNull : false , }, }); await queryInterface.addColumn ('notes' , 'user_id' , { type : DataTypes .INTEGER , allowNull : false , references : { model : 'users' , key : 'id' }, }); }, down : async ({ context : queryInterface }) => { await queryInterface.dropTable ('notes' ); await queryInterface.dropTable ('users' ); }, };
函数 up
是定义了在执行迁移时应该如何修改数据库,而 down
定义如何撤销迁移
迁移文件命名总是以日期和序号开始,如 migrations/20211209_00_initialize_notes_and_users.js
Umzug 库可以从程序中手动执行迁移:
const { Umzug , SequelizeStorage } = require ('umzug' );const runMigrations = async ( ) => { const migrator = new Umzug ({ migrations : { glob : 'migrations/*.js' , }, storage : new SequelizeStorage ({ sequelize, tableName : 'migrations' }), context : sequelize.getQueryInterface (), logger : console , }); const migrations = await migrator.up (); console .log ('Migrations up to date' , { files : migrations.map ((mig ) => mig.name ), }); };const connectToDatabase = async ( ) => { await sequelize.authenticate (); await runMigrations (); };
对于多对多 模型,需要用到连接模型:
class Membership extends Model {}Membership .init ( { id : { type : DataTypes .INTEGER , primaryKey : true , autoIncrement : true , }, user_id : { type : DataTypes .INTEGER , allowNull : false , references : { model : 'users' , key : 'id' }, }, team_id : { type : DataTypes .INTEGER , allowNull : false , references : { model : 'teams' , key : 'id' }, }, }, { sequelize, underscored : true , timestamps : false , modelName : 'membership' , } );
然后连接:
const User = require ('./user' );const Team = require ('./team' );const Membership = require ('./membership' );User .belongsToMany (Team , { through : Membership });Team .belongsToMany (User , { through : Membership });
注意到模型和迁移的代码很多部分是重复的,但是代码可能会随着时间改变,而迁移则充当了“记录”的功能,故不可复用数据,只能手动复制