preface
I built my own blog system before. At that time, the front end of the blog system was basically based on VUE, but now REACT is more used. So REACT was used to reconstruct the whole blog system, and many existing problems were changed and optimized. The system has carried on the server side rendering SSR processing.
Blog address portal
This project complete code: GitHub repository
This paper is quite long and will be introduced from the following aspects:
- Core Technology Stack
- Directory structure details
- Project environment startup
- Server side source code parsing
- Client source code parsing
- Admin end source code analysis
- HTTPS to create
Core Technology Stack
React 17.x
React family bucketTypescript 4.x
Koa 2.x
Webpack 5.x
Babel 7.x
Mongodb
(Database)eslint
+stylelint
+prettier
(Code format control)husky
+lint-staged
+commitizen
+commitlint
(Verify git submission code format and commit process)
The core is probably the above stack of technology, and then based on the various needs of the blog function development. For example, jsonWebToken,@loadable, log4JS module used in authorization and other functions, I will expand the length of each function module to explain.
Package. json Configuration file address
Directory structure details
| - blog - source | -. Babelrc. Js. / / the Babel configuration file | - commitlintrc. Js / / git commit calibration file format, commit format is not through, Ban commit. | - cz - config. Js / / cz - customizable configuration file. I used CZ-Customizable to do the COMMIT specification, Own custom a | - eslintignore / / eslint ignore configuration. | - eslintrc. Js / / eslint configuration file. | - gitignore / / git ignore configuration. | - NPMRC / / NPM configuration file | -. Postcssrc. Js / / add CSS style prefix, things like that. | - prettierrc. Js code / / format to use, Unified style. | - sentryclirc / / project monitoring Sentry. | - stylelintignore / / style ignore configuration. | - stylelintrc. | - js / / stylelint configuration file Package. The json | - tsconfig. Base. Json / / ts | configuration file -- tsconfig. Json / / ts | -- tsconfig. Configuration file for server json / / ts | - build configuration file // Webpack builds directories for client, admin, Server side for the difference between building | | - paths. The ts | | -- utils. Ts | | - config | | | -- dev. Ts | | | -- index. Ts | | | -- prod. Ts | | - webpack | |-- admin.base.ts | |-- admin.dev.ts | |-- admin.prod.ts | |-- base.ts | |-- client.base.ts | |-- client.dev.ts | |-- client.prod.ts | |-- index.ts | |-- loaders.ts | |-- plugins.ts | |-- server.base.ts | |-- server.dev.ts | |-- Server. Prod. Ts | - dist / / pack to the output directory | | - logs / / print log directory - private entrance to the directory / / static resources, Set up multiple | | - third - party - login. HTML | - publice/entry/static resources, set up multiple | - scripts / / project executing scripts, including start, Packaging, etc. | | - build. Ts | | -- config. Ts | | -- dev. Ts | | -- start. Ts | | -- utils. Ts | | - plugins | | - open - the ts | | - Webpack - dev. Ts | | - webpack - hot. Ts | - SRC / / core source | | - client / / client code | | | -- main. TSX / / entry file | | | - Tsconfig. Json/configuration/ts | | | - API/interface/API | | | - app / / entrance components | | | - appComponents / / business component | | | - assets / / static resources | | | - components / / common components | | | - config / / client configuration file | | | - contexts / / context, is to use useContext created, Used for components of Shared state | | | - global / / global into the client need the calling method. Methods such as window "| | | - hooks / / react hooks | | | - pages / / page | | | - the router / / routing | | | - store / / store directory | | | - Styles / / style file | | | - theme / / style theme files, make skin effect of | | | - types/type/ts file | | | - utils/method/tools | | - admin / / background management code, With the client not much | | | -) babelrc. Js | | | -- app. TSX | | | -- main. TSX | | | -- tsconfig. Json | | | | - API | | - appComponents | | |-- assets | | |-- components | | |-- config | | |-- hooks | | |-- pages | | |-- router | | |-- store | | |-- styles | | | - types | | | - utils | | - models / / interface model | | - server / / the server code | | | -- main. Ts / / entry file | | | - config / / configuration file | | | - controllers / / controller | | | - database / / database | | | - decorators / / decorator, Encapsulates the @ Get @ Post, @ Put, @ the Delete, @ like cookies | | | - middleware / / middleware | | | - models / / mongo model | | | - the router / / routing, interface | | | - SSL/HTTPS certificate, now I is for the use of local development, online if use nginx, In nginx configuration line | | | - SSR / / page SSR processing | | | - timer timer / / | | | - utils/method/tools | | - Shared / / multiterminal Shared code | | | - LoadInitData. Ts | | | -- type. Ts | | | - config | | | - utils | | | - types/type/ts file - static | - the template / / / / static resources HTML templateCopy the code
The above is the general file directory of the project, the basic function of the file has been described above, and I will detail the implementation process of the blog function. At present, the blog system is not split up, this will be planned in the future.
Project environment startup
Make sure your Node version is at least 10.13.0 (LTS), because Webpack 5 requires node.js to be at least 10.13.0 (LTS).
Execute the script and start the project
Let’s start with the entry file:
"dev": "cross-env NODE_ENV=development TS_NODE_PROJECT=tsconfig.server.json ts-node --files scripts/start.ts"
"prod": "cross-env NODE_ENV=production TS_NODE_PROJECT=tsconfig.server.json ts-node --files scripts/start.ts"
Copy the code
1. Run the entry file scripts/start.js
// scripts/start.js
import path from 'path'
import moduleAlias from 'module-alias'
moduleAlias.addAliases({
'@root': path.resolve(__dirname, '.. / '),
'@server': path.resolve(__dirname, '.. /src/server'),
'@client': path.resolve(__dirname, '.. /src/client'),
'@admin': path.resolve(__dirname, '.. /src/admin'})),if (process.env.NODE_ENV === 'production') {
require('./build')}else {
require('./dev')}Copy the code
Set path aliases, because currently the ends are not split, so create aliases to find files.
2. Set up the development environment from the entry file
Start by exporting the configuration files for the respective environments on each side of the Webpack.
// dev.ts
import clientDev from './client.dev'
import adminDev from './admin.dev'
import serverDev from './server.dev'
import clientProd from './client.prod'
import adminProd from './admin.prod'
import serverProd from './server.prod'
import webpack from 'webpack'
export type Configuration = webpack.Configuration & {
output: {
path: string
}
name: string
entry: any
}
export default (NODE_ENV: ENV): [Configuration, Configuration, Configuration] => {
if (NODE_ENV === 'development') {
return [clientDev as Configuration, serverDev as Configuration, adminDev as Configuration]
}
return [clientProd as Configuration, serverProd as Configuration, adminProd as Configuration]
}
Copy the code
Webpack configuration file, basically there will be no big difference, currently paste a simple Webpack configuration, respectively have server,client,admin different environment configuration file. Specific can see blog source code
import webpack from 'webpack'
import merge from 'webpack-merge'
import { clientPlugins } from './plugins' / / plugins configuration
import { clientLoader } from './loaders' / / loaders configuration
import paths from '.. /paths'
import config from '.. /config'
import createBaseConfig from './base' // Multi-terminal default configuration
const baseClientConfig: webpack.Configuration = merge(createBaseConfig(), {
mode: config.NODE_ENV,
context: paths.rootPath,
name: 'client'.target: ['web'.'es5'].entry: {
main: paths.clientEntryPath,
},
resolve: {
extensions: ['.js'.'.json'.'.ts'.'.tsx'].alias: {
The '@': paths.clientPath,
'@client': paths.clientPath,
'@root': paths.rootPath,
'@server': paths.serverPath,
},
},
output: {
path: paths.buildClientPath,
publicPath: paths.publicPath,
},
module: {
rules: [...clientLoader],
},
plugins: [...clientPlugins],
})
export default baseClientConfig
Copy the code
The admin and webPack configuration files on the client and server sides are then processed respectively
The above points need to be noted:
admin
End withclient
End respectively open a service processing Webpack file, are packaged in memory.client
The end needs to pay attention to the reference path of the packaged file, because isSSR
, need to get the file directly render on the server side, I put the server and the client in two different services, so in the server side referenceclient
Note the reference path when you end the file.server
Side code is packaged directly indist
The file is used for startup and is not typed in memory.
const WEBPACK_URL = `${__WEBPACK_HOST__}:${__WEBPACK_PORT__}`
const [clientWebpackConfig, serverWebpackConfig, adminWebpackConfig] = getConfig(process.env.NODE_ENV as ENV)
// Build client and server
const start = async() = > {// Because client points to another service, so overwrite publicPath, otherwise 404
clientWebpackConfig.output.publicPath = serverWebpackConfig.output.publicPath = `${WEBPACK_URL}${clientWebpackConfig.output.publicPath}`
clientWebpackConfig.entry.main = [`webpack-hot-middleware/client? path=${WEBPACK_URL}/__webpack_hmr`, clientWebpackConfig.entry.main]
const multiCompiler = webpack([clientWebpackConfig, serverWebpackConfig])
const compilers = multiCompiler.compilers
const clientCompiler = compilers.find((compiler) = > compiler.name === 'client') as webpack.Compiler
const serverCompiler = compilers.find((compiler) = > compiler.name === 'server') as webpack.Compiler
// compiler.hooks are used to monitor compiler compilation
const clientCompilerPromise = setCompilerTip(clientCompiler, clientWebpackConfig.name)
const serverCompilerPromise = setCompilerTip(serverCompiler, serverWebpackConfig.name)
/ / used to create the service method, create the client side in this service, so far, the client side code into the service, can be like https://192.168.0.47:3012/js/lib.js access to the file
createService({
webpackConfig: clientWebpackConfig,
compiler: clientCompiler,
port: __WEBPACK_PORT__
})
let script: any = null
/ / restart
const nodemonRestart = () = > {
if (script) {
script.restart()
}
}
// Listen for server file changes
serverCompiler.watch({ ignored: /node_modules/ }, (err, stats) = > {
nodemonRestart()
if (err) {
throw err
}
// ...
})
try {
// Wait for compilation to complete
await clientCompilerPromise
await serverCompilerPromise
// This is the admin compile situation, the admin side of the compile is not too bad, basically run 'webpack(config)' compile, through 'createService' generate a service to access the packaged code.
await startAdmin()
closeCompiler(clientCompiler)
closeCompiler(serverCompiler)
logMsg(`Build time The ${new Date().getTime() - startTime}`)}catch (err) {
logMsg(err, 'error')}// Start the entry file compiled by the server side to start the project service
script = nodemon({
script: path.join(serverWebpackConfig.output.path, 'entry.js')
})
}
start()
Copy the code
The createService method is used to generate the service, and the code looks something like this
export const createService = ({webpackConfig, compiler}: {webpackConfig: Configurationcompiler: Compiler}) = > {
const app = new Koa()
...
const dev = webpackDevMiddleware(compiler, {
publicPath: webpackConfig.output.publicPath as string.stats: webpackConfig.stats
})
app.use(dev)
app.use(webpackHotMiddleware(compiler))
http.createServer(app.callback()).listen(port, cb)
return app
}
Copy the code
The general logic of webpack compilation in the development environment is like this, there will be some webpack-dev-middle middleware processing in KOA, etc., here I only provide the general idea, you can look at the source code.
3. Set up production environment
For the build environment, less processing, directly through the Webpack package on the line
webpack([clientWebpackConfig, serverWebpackConfig, adminWebpackConfig], (err, stats) = > {
spinner.stop()
if (err) {
throw err
}
// ...
})
Copy the code
Then start the packaged entry file cross-env NODE_ENV=production node dist/server/entry.js
This is mainly the webPack configuration, which can be viewed directly by clicking here
Server side source code parsing
The webpack configuration extends from the configuration above to their entry file
/ / client portal
const clientPath = utils.resolve('src/client')
const clientEntryPath = path.join(clientPath, 'main.tsx')
/ / server entry
const serverPath = utils.resolve('src/server')
const serverEntryPath = path.join(serverPath, 'main.ts')
Copy the code
- The client entry is
/src/client/main.tsx
- The server side entry is
/src/server/main.ts
Because SSR was used in the project, we conducted a step by step analysis from the server side.
1. / SRC /server/main.ts entry file
import Koa from 'koa'.const app = new Koa()
/* Middleware: sendMidddleware: "Koa-etag" is the key to setting etagMiddleware to cache ctx.body and conditionalMiddleware. To check whether caching is working, check ctx.fresh. Koa already has loggerMiddleware wrapped inside it to print logs. RouterErrorMiddleware: This is error handling of the API. For static file processing, set max-age to cache strongly, configure eTAG or last-Modified to cache strongly and negotiate with resources. * /
middleware(app)
/* Manage the API */
router(app)
/* Start database, set up SSR configuration */
Promise.all([startMongodb(), SSR(app)])
.then(() = > {
// Start the service
https.createServer(serverConfig.httpsOptions, app.callback()).listen(rootConfig.app.server.httpsPort, '0.0.0.0')
})
.catch((err) = > {
process.exit()
})
Copy the code
2. Middleware processing
For middleware, I’ll focus on loggerMiddleware and authTokenMiddleware, but I won’t waste space on any other middleware.
Log printing mainly uses the log4JS library, then based on the library to do the upper layer encapsulation, through different types of Logger to create different log files. Encapsulates the logging of all requests, API logging, and some third party calls
1. Implementation of loggerMiddleware
// log.ts
const createLogger = (options = {} as LogOptions): Logger= > {
/ / configuration items
constopts = { ... serverConfig.log, ... options }// Config file
log4js.configure({
appenders: {
// Stout can be used in a development environment and printed directly
stdout: {
type: 'stdout'
},
// Use the multiFile type to generate different files through variables. I tried several other types. It doesn't feel like it
multi: { type: 'multiFile'.base: opts.dir, property: 'dir'.extension: '.log'}},categories: {
default: { appenders: ['stdout'].level: 'off' },
http: { appenders: ['multi'].level: opts.logLevel },
api: { appenders: ['multi'].level: opts.logLevel },
external: { appenders: ['multi'].level: opts.logLevel }
}
})
const create = (appender: string) = > {
const methods: LogLevel[] = ['trace'.'debug'.'info'.'warn'.'error'.'fatal'.'mark']
const context = {} as LoggerContext
const logger = log4js.getLogger(appender)
// Rewrite the log4js method to generate variables that can be used to generate different files
methods.forEach((method) = > {
context[method] = (message: string) = > {
logger.addContext('dir'.` /${appender}/${method}/${dayjs().format('YYYY-MM-DD')}`)
logger[method](message)
}
})
return context
}
return {
http: create('http'),
api: create('api'),
external: create('external')}}export default createLogger
// loggerMiddleware
import createLogger, { LogOptions } from '@server/utils/log'
// All requests are printed
const loggerMiddleware = (options = {} as LogOptions) = > {
const logger = createLogger(options)
return async (ctx: Koa.Context, next: Next) => {
const start = Date.now()
ctx.log = logger
try {
await next()
const end = Date.now() - start
// Normal request logs are printed
logger.http.info(
logInfo(ctx, {
responseTime: `${end}ms`}}))catch (e) {
const message = ErrorUtils.getErrorMsg(e)
const end = Date.now() - start
// Error request logs are printed
logger.http.error(
logInfo(ctx, {
message,
responseTime: `${end}ms`}))}}}Copy the code
2. Implementation of authTokenMiddleware
// authTokenMiddleware.ts
const authTokenMiddleware = () = > {
return async (ctx: Koa.Context, next: Next) => {
// API whitelist: you can whitelist login registration interface and so on to allow access
if (serverConfig.adminAuthApiWhiteList.some((path) = > path === ctx.path)) {
return await next()
}
// Verify the validity of the token by jsonWebToken
const token = ctx.cookies.get(rootConfig.adminTokenKey)
if(! token) {throw {
code: 401}}else {
try {
jwt.verify(token, serverConfig.adminJwtSecret)
} catch (e) {
throw {
code: 401}}}await next()
}
}
export default authTokenMiddleware
Copy the code
So that’s the middleware.
3. Processing logic of the Router
The following is about router processing, API processing is mainly through decorators for request processing
1. Create a router and load the API file
// router.ts
import { bootstrapControllers } from '@server/controllers'
const router = new KoaRouter<DefaultState, Context>()
export default (app: Koa) => {
// Perform API binding,
bootstrapControllers({
router, // Route object
basePath: '/api'.// Route prefix
controllerPaths: ['controllers/api/*/**/*.ts'].// File directory
middlewares: [routerErrorMiddleware(), loggerApiMiddleware()]
})
app.use(router.routes()).use(router.allowedMethods())
// api 404
app.use(async (ctx, next) => {
if (ctx.path.startsWith('/api')) {
return ctx.sendCodeError(404)}await next()
})
}
/ / bootstrapControllers method
export const bootstrapControllers = (options: ControllerOptions) = > {
const { router, controllerPaths } = options
// Import the file, which triggers the decorator binding controllers
controllerPaths.forEach((path) = > {
// Find the file through the glob module
const files = glob.sync(Utils.resolve(`src/server/${path}`))
files.forEach((file) = > {
/* Import the file Why? Because the direct webpack reference variable can not find the module webpack packed files are packed in the reference path, which is not the actual path (__webpack_require__) so direct import path is problematic. Import with an alias. One problem is that it will parse all files in the path where the string is concatenated. For example: Require (` @ root/SRC/server/controllers ${fileName} `) parses @ root/SRC/server/controllers under all of the files, Locating in this file prevents parsing too many files and running out of node memory. This issue needs to be resolved */
const p = Utils.resolve('src/server/controllers')
const fileName = file.replace(p, ' ')
// require directly importing the corresponding file. Just import it, and it will trigger the decorator to collect the API.
// All requests in these files are collected into metaData. MetaData will be covered next
require(`@root/src/server/controllers${fileName}`)})/ / bind the router
generateRoutes(router, metadata, options)
})
}
Copy the code
That’s how the API is introduced, and here’s how the decorator handles interfaces and parameters.
There are a few things to note about decorators:
- Vscode needs to turn on the decorator
javascript.implicitProjectConfig.experimentalDecorators: true
The tsconfig.json file is automatically detected and added if needed - Babel needs to be configured
['@babel/plugin-proposal-decorators', { legacy: true }]
withbabel-plugin-parameter-decorator
These two plug-ins, because@babel/plugin-proposal-decorators
This plugin doesn’t parse @arg, so add itbabel-plugin-parameter-decorator
The plugin is used to parse @arg
Go to the @server/decorators file and define the following decorators
2. Collection of decorators
@Controller
A module under the API for example@Controller('/user) => /api/user
@Get
A Get request@Post
A Post request@Delete
The Delete request@Put
Put request@Patch
Patch request@Query
Query parameters such ashttps://localhost:3000?a=1&b=2 => {a: 1, b: 2}
@Body
Pass in the Body argument@Params
Params parameter for examplehttps://localhost:3000/api/user/123 => /api/user/:id => @Params('id') id:string => 123
@Ctx
Ctx object@Header
Header objects can also fetch a value from the Header individually@header () gets the entire object of the Header
.@header (' content-type ') Gets the value of the content-type attribute in the Header
@Req
The Req object@Request
The Request object@Res
Res objects@Response
The Response object@Cookie
Cookie objects can also individually fetch a value from a Cookie@Session
Session objects can also individually fetch a value from the Session@Middleware
Binding middleware that can be precise to a request@Token
Get the token value. This is defined to facilitate obtaining the token
Now, how do these decorators work
3. Create metaData
// MetaData data format
export type Method = 'get' | 'post' | 'put' | 'patch' | 'delete'
export type argumentSource = 'ctx' | 'query' | 'params' | 'body' | 'header' | 'request' | 'req' | 'response' | 'res' | 'session' | 'cookie' | 'token'
export type argumentOptions =
| string| { value? :stringrequired? :booleanrequiredList? :string[]}export type MetaDataArguments = {
source: argumentSource options? : argumentOptions }export interface MetaDataActions {
[k: string] : {method: Method
path: string
target: (. args:any) = > void
arguments? : { [k:string]: MetaDataArguments } middlewares? : Koa.Middleware[] } }export interface MetaDataController {
actions: MetaDataActions basePath? :string | string[] middlewares? : Koa.Middleware[] }export interface MetaData {
controllers: {
[k: string]: MetaDataController
}
}
/* Declare a data source, which is used to add all API methods, GenerateRoutes (Router, metadata, options) parses the metadata data and binds it to the router
export const metadata: MetaData = {
controllers: {}}Copy the code
4. @ Controller implementation
// All requests within TestController are prefixed with '/test' => / API /test/example
// @controller (['/test', '/test1']) can also be an array, which creates two requests/API /test/example and/API /test1/example
@Controller('/test')
export class TestController{
@Get('/example')
async getExample() {
return 'example'}}// Bind class controller to metaData
/* metadata.controllers = { TestController: { basePath: '/test' } } */
export const Controller = (basePath: string | string[]) = > {
return (classDefinition: any) :void= > {
// Get the class name as the key name for each controller in the metadata. make sure the controller class name is unique to avoid collisions
const controller = metadata.controllers[classDefinition.name] || {}
// basePath is the above /test
controller.basePath = basePath
metadata.controllers[classDefinition.name] = controller
}
}
Copy the code
5. @ Get @ Post, @ put, @ Patch, @ the Delete
The implementation of these decorators is basically the same, just list one for demonstration
// For example, just declare the @get decorator before the specified method. Each method as a request (action)
export class TestController{
// @Post('/example')
// @put('/example')
// @Patch('/example')
// @Delete('/example')
@Get('/example') // => A Get request /example is generated
async getExample() {
return 'example'}}// Code implementation
export const Get = (path: string) = > {
// The decorator binding method takes two arguments, the instance object, and the method name
return (object: any, methodName: string) = > {
_addMethod({
method: 'get'.path: path,
object,
methodName
})
}
}
// Bind to the specified controller
const _addMethod = ({ method, path, object, methodName }: AddMethodParmas) = > {
// Get the controller corresponding to this method
const controller = metadata.controllers[object.constructor.name] || {}
const actions = controller.actions || {}
const o = {
method,
path,
target: object[methodName].bind(object)}Controller.actions = {getExample: {method: 'get', // request method path: '/ /example', // request path target: () {return 'example'}}} * /actions[methodName] = { ... (actions[methodName] || {}), ... o } controller.actions = actions metadata.controllers[object.constructor.name] = controller
}
Copy the code
Above is the action binding
6. @ Query, @ Body, @ Params, @ Ctx, @ Header, @ the Req, @ Request, @ Res, @ Response, @ Cookie, @ the Session
Since these decorations are the decoration method arguments, they can also be handled uniformly
/ / sample/API/example? a=1&b=3
export class TestController{
@Get('/example') // => A Get request /example is generated
async getExample(@Query() query: {[k: string] :any}, @Query('a') a: string) {
console.log(query) // -> {a: 1, b: 2}
console.log(a) / / - > 1
return 'example'}}// Other decorators are used similarly
// Code implementation
export const Query = (options? :string| argumentOptions, required? :boolean) = > {
// example @query ('id '): options => Pass 'id'
return (object: any, methodName: string, index: number) = > {
_addMethodArgument({
object,
methodName,
index,
source: 'query'.options: _mergeArgsParamsToOptions(options, required)
})
}
}
// Record the parameters for each action
const _addMethodArgument = ({ object, methodName, index, source, options }: AddMethodArgumentParmas) = > {
/* object -> class instance: TestController -> getExample index Query options -> What is required for some options */
const controller = metadata.controllers[object.constructor.name] || {}
controller.actions = controller.actions || {}
controller.actions[methodName] = controller.actions[methodName] || {}
// As before, get the action corresponding to this method and add a arguments argument to the action
/* getExample: {method: 'get', // request: path: '/example', // request: target: () {// Return 'example'}, arguments: { 0: { source: 'query', options: 'id' } } } */
const args = controller.actions[methodName].arguments || {}
args[String(index)] = {
source,
options
}
controller.actions[methodName].arguments = args
metadata.controllers[object.constructor.name] = controller
}
Copy the code
The above is the implementation of the Arguments binding for each action
7. @ Middleware implementation
The @middleware decorator should be able to bind not only to controllers but also to an action
// Sample execution flow
// router.get('/api/test/example', TestMiddleware(), ExampleMiddleware(), async (ctx, next) => {})
@Middleware([TestMiddleware()])
@Controller('/test')
export class TestController{
@Middleware([ExampleMiddleware()])
@Get('/example')
async getExample() {
return 'example'}}// Code implementation
export const Middleware = (middleware: Koa.Middleware | Koa.Middleware[]) = > {
const middlewares = Array.isArray(middleware) ? middleware : [middleware]
return (object: any, methodName? :string) = > {
// Add middleware to controller
if (typeof object= = ='function') {
const controller = metadata.controllers[object.name] || {}
controller.middlewares = middlewares
} else if (typeof object= = ='object' && methodName) {
// The presence of methodName proves that middleware is added to the action
const controller = metadata.controllers[object.constructor.name] || {}
controller.actions = controller.actions || {}
controller.actions[methodName] = controller.actions[methodName] || {}
controller.actions[methodName].middlewares = middlewares
metadata.controllers[object.constructor.name] = controller
}
/* Metadata = {TestController: {basePath: '/test', middlewares: [TestMiddleware()], actions: {getExample: {method: 'get', // request method path: '/example', // request path target: () {// Return 'example'}, arguments: { 0: { source: 'query', options: 'id' } }, middlewares: [ExampleMiddleware()] } } } } */}}Copy the code
This decorator basically wraps the entire request in metadata, returning to the generateRoutes in the bootstrapControllers method, which parses the metadata data and binds it to the router.
8. Parse the metadata and bind the router
export const bootstrapControllers = (options: ControllerOptions) = > {
const { router, controllerPaths } = options
// Import the file, which triggers the decorator binding controllers
controllerPaths.forEach((path) = > {
// require(), after importing the file, triggers the decorator for data collection
require(...).// Metadata is the data structure that collects all the actions
// The data structure looks like this, for example above
metadata.controllers = {
TestController: {
basePath: '/test'.middlewares: [TestMiddleware()],
actions: {
getExample: {
method: 'get'.// Request mode
path: '/example'.// Request path
target: () { // The function body of this method
return 'example'
},
arguments: {
0: {
source: 'query'.options: 'id'}},middlewares: [ExampleMiddleware()]
}
}
}
}
// Execute the router binding process
generateRoutes(router, metadata, options)
})
}
Copy the code
9. Implementation of generateRoutes method
export const generateRoutes = (router: Router, metadata: MetaData, options: ControllerOptions) = > {
const rootBasePath = options.basePath || ' '
const controllers = Object.values(metadata.controllers)
controllers.forEach((controller) = > {
if (controller.basePath) {
controller.basePath = Array.isArray(controller.basePath) ? controller.basePath : [controller.basePath]
controller.basePath.forEach((basePath) = > {
// Pass the router, controller, and url prefix for each action (rootBasePath + basePath)
_generateRoute(router, controller, rootBasePath + basePath, options)
})
}
})
}
// Generate a route
const _generateRoute = (router: Router, controller: MetaDataController, basePath: string, options: ControllerOptions) = > {
// Reverse the action, the added action will be added to the front, reverse the parse correctly, load in order, avoid the following situation
/* @get ('/user/:id') @get ('/user/add')
const actions = Object.values(controller.actions).reverse()
actions.forEach((action) = > {
// Splice the full path of the action
const path =
'/' +
(basePath + action.path)
.split('/')
.filter((i) = > i.length)
.join('/')
// Add middlewares to each request in order
const midddlewares = [...(options.middlewares || []), ...(controller.middlewares || []), ...(action.middlewares || [])]
/* Router ['get']('/ API ', // request path... (options. Middlewares | | []), / / middleware... (controller. Middlewares | | []), / / middleware... (action. Middlewares | | []), / / middleware async (CTX, next) = > {/ / to the execution of the function return data, and so on CTX. Send (...). }) * /
midddlewares.push(async (ctx) => {
const targetArguments: any[] = []
// Parse the parameters
if (action.arguments) {
const keys = Object.keys(action.arguments)
// Argument data for each position
for (const key of keys) {
const argumentData = action.arguments[key]
// Function that parses arguments, as described below
targetArguments[Number(key)] = _determineArgument(ctx, argumentData, options)
}
}
// Execute the action.target function to get the returned data and return it through CTX
const data: any = awaitaction.target(... targetArguments)// data === 'CUSTOM' returns such as downloading files and so on
if(data ! = ='CUSTOM') {
ctx.send(data === undefined ? null: data) } }) router[action.method](path, ... (midddlewaresas Middleware[]))
})
}
Copy the code
This is the general route resolution process, and there is a method _determineArgument that is used to resolve the parameters
9. Implementation of Determineargument method
ctx
.session
.cookie
.token
.query
.params
.body
This parameter does not pass directlyctx[source]
Get, so handle it separately- The rest can go through
ctx[source]
Get it, you get it directly
// Process and verify the parameters
const _determineArgument = (ctx: Context, { options, source }: MetaDataArguments, opts: ControllerOptions) = > {
let result
/ / special processing parameters, ` CTX `, ` session `, ` cookie `, ` token `, ` query `, ` params `, ` body `
if (_argumentInjectorTranslations[source]) {
result = _argumentInjectorTranslations[source](ctx, options, source)
} else {
@header() -> CTX ['header'], @header(' content-type ') -> CTX ['header'][' content-type ']
result = ctx[source]
if (result && options && typeof options === 'string') {
result = result[options]
}
}
return result
}
// The parameters that need to be checked are processed separately
const _argumentInjectorTranslations = {
ctx: (ctx: Context) = > ctx,
session: (ctx: Context, options: argumentOptions) = > {
if (typeof options === 'string') {
return ctx.session[options]
}
return ctx.session
},
cookie: (ctx: Context, options: argumentOptions) = > {
if (typeof options === 'string') {
return ctx.cookies.get(options)
}
return ctx.cookies
},
token: (ctx: Context, options: argumentOptions) = > {
if (typeof options === 'string') {
return ctx.cookies.get(options) || ctx.header[options]
}
return ' '
},
query: (ctx: Context, options: argumentOptions, source: argumentSource) = > {
return _argumentInjectorProcessor(source, ctx.query, options)
},
params: (ctx: Context, options: argumentOptions, source: argumentSource) = > {
return _argumentInjectorProcessor(source, ctx.params, options)
},
body: (ctx: Context, options: argumentOptions, source: argumentSource) = > {
return _argumentInjectorProcessor(source, ctx.request.body, options)
}
} as Record<argumentSource, (. args:any) = > any>
// Validate the return value of the operation
const _argumentInjectorProcessor = (source: argumentSource, data: any, options: argumentOptions) = > {
if(! options) {return data
}
if (typeof options === 'string' && Type.isObject(data)) {
return data[options]
}
if (typeof options === 'object') {
if (options.value) {
const val = data[options.value]
// This field is mandatory, but the value is empty
if (options.required && Type.isEmpty(val)) {
ErrorUtils.error(` [${source}] [${options.value}] parameter cannot be empty)}return val
}
// require array validation
if (options.requiredList && Type.isArray(options.requiredList) && Type.isObject(data)) {
for (const key of options.requiredList) {
if (Type.isEmpty(data[key])) {
ErrorUtils.error(` [${source}] [${key}] parameter cannot be empty)}}return data
}
if (options.required) {
if (Type.isEmptyObject(data)) {
ErrorUtils.error(`${source}There are mandatory arguments' in)}return data
}
}
ErrorUtils.error(` [${source}] The ${JSON.stringify(options)}Parameter error ')}Copy the code
10. Preview the overall Router Controller file
import {
Get,
Post,
Put,
Patch,
Delete,
Query,
Params,
Body,
Ctx,
Header,
Req,
Request,
Res,
Response,
Session,
Cookie,
Controller,
Middleware
} from '@server/decorators'
import { Context, Next } from 'koa'
import { IncomingHttpHeaders } from 'http'
const TestMiddleware = () = > {
return async (ctx: Context, next: Next) => {
console.log('start TestMiddleware')
await next()
console.log('end TestMiddleware')}}const ExampleMiddleware = () = > {
return async (ctx: Context, next: Next) => {
console.log('start ExampleMiddleware')
await next()
console.log('end ExampleMiddleware')}}@Middleware([TestMiddleware()])
@Controller('/test')
export class TestController {
@Middleware([ExampleMiddleware()])
@Get('/example')
async getExample(
@Ctx() ctx: Context,
@Header() header: IncomingHttpHeaders,
@Request() request: Request,
@Req() req: Request,
@Response() response: Response,
@Res() res: Response,
@Session() session: any.@Cookie('token') Cookie: any
) {
console.log(ctx.response)
return {
ctx,
header,
request,
response,
Cookie,
session
}
}
@Get('/get/:name/:age')
async getFn(
@Query('id') id: string.@Query({ required: true }) query: any.@Params('name') name: string.@Params('age') age: string.@Params() params: any
) {
return {
method: 'get',
id,
query,
name,
age,
params
}
}
@Post('/post/:name/:age')
async getPost(
@Query('id') id: string.@Params('name') name: string.@Params('age') age: string.@Params() params: any.@Body('sex') sex: string.@Body('hobby'.true) hobby: any.@Body() body: any
) {
return {
method: 'post',
id,
name,
age,
params,
sex,
hobby,
body
}
}
@Put('/put/:name/:age')
async getPut(
@Query('id') id: string.@Params('name') name: string.@Params('age') age: string.@Params() params: any.@Body('sex') sex: string.@Body('hobby'.true) hobby: any.@Body() body: any
) {
return {
method: 'put',
id,
name,
age,
params,
sex,
hobby,
body
}
}
@Patch('/patch/:name/:age')
async getPatch(
@Query('id') id: string.@Params('name') name: string.@Params('age') age: string.@Params() params: any.@Body('sex') sex: string.@Body('hobby'.true) hobby: any.@Body() body: any
) {
return {
method: 'patch',
id,
name,
age,
params,
sex,
hobby,
body
}
}
@Delete('/delete/:name/:age')
async getDelete(
@Query('id') id: string.@Params('name') name: string.@Params('age') age: string.@Params() params: any.@Body('sex') sex: string.@Body('hobby'.true) hobby: any.@Body() body: any
) {
return {
method: 'delete',
id,
name,
age,
params,
sex,
hobby,
body
}
}
}
Copy the code
This is the entire Router-specific action binding
4. Realization of SSR
SSR isomorphism code actually explains a lot, basically literally in the search engine search can have a lot of tutorials, I posted a simple flow chart here to help you understand, by the way, my process ideas
The above flow chart is just a general process, in which the data is obtained, the data is injected, the first screen style is optimized, and so on. I will explain it in the following part of the code. @loadable/babel-plugin
- @loadable/ Component: Used to load components dynamically
- Loadable /server: collects scripts and style files from the server side and inserts them into the HTML directly exported by the server side for the client to render again.
- @loadable/babel-plugin: Generates JSON files, statistics dependency files
1. Front-end code
/* home.tsx */
const Home = () = > {
return Home
}
// The interface data that this component needs to depend on
Home._init = async (store: IStore, routeParams: RouterParams) => {
const { data } = await api.getData()
store.dispatch(setDataState({ data }))
return
}
/* router.ts */
const routes = [
{
path: '/'.name: 'Home'.exact: true.component: _import_('home')},... ]/* app.ts */
const App = () = > {
return (
<Switch location={location}>
{routes.map((route, index) => {
return (
<Route
key={` ${index+ ${}route.path} `}path={route.path}
render={(props)= > {
return (
<RouterGuard Com={route.component} {. props} >
{children}
</RouterGuard>
)
}}
exact={route.exact}
/>
)
})}
<Redirect to="/ 404" />
</Switch>)}// Route interception determines whether a request needs to be initiated by the front end
const RouterGuard = ({ Com, children, ... props }:any) = > {
useEffect(() = > {
const isServerRender = store.getState().app.isServerRender
const options = {
disabled: false
}
async function load() {
// Because we put the page's interface data in the component's _init method, we can get the data directly by calling this method
// For the first time, the data is rendered by the server, so no call is required on the client side.
// If the page is not rendered by the server, there is _init function, which can initiate a data request in the front end, and obtain data
// So that the front-end and the server share the same code to initiate requests.
// There are many ways to implement this, but there are also ways to bind interface functions to route, depending on personal preference.
if(! isServerRender && Com._init && history.action ! = ='POP') {
setLoading(true)
awaitCom._init(store, routeParams.current, options) ! options.disabled && setLoading(false)
}
}
load()
return () = > {
options.disabled = true
}
}, [Com, store, history])
return (
<div className="page-view">
<Com {. props} / >
{children}
</div>)}/* main.tsx */
// The front-end gets the store data injected by the background, synchronizes the store data, and the client performs rendering
export const getStore = (preloadedState? :any, enhancer? : StoreEnhancer) = > {
const store = createStore(rootReducers, preloadedState, enhancer) as IStore
return store
}
const store = getStore(window.__PRELOADED_STATE__, window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__())
loadableReady(() = > {
ReactDom.hydrate(
<Provider store={store}>
<BrowserRouter>
<HelmetProvider>
<Entry />
</HelmetProvider>
</BrowserRouter>
</Provider>.document.getElementById('app'))})Copy the code
That’s about all the logic the front end needs, but the focus is on the server side
2. Server-side processing code
// Loadable /babel-plugin is a loadable-stats.json path dependency table, used to index the dependency of each page JS, CSS files, etc.
const getStatsFile = async() = > {const statsFile = path.join(paths.buildClientPath, 'loadable-stats.json')
return new ChunkExtractor({ statsFile })
}
// Get the dependent file object
const clientExtractor = await getStatsFile()
// Store must be regenerated every time it is loaded. It cannot be a singleton, otherwise all users will share a store.
const store = getStore()
// Matches the route object of the current route
const { route } = matchRoutes(routes, ctx.path)
if (route) {
const match = matchPath(decodeURI(ctx.path), route)
const routeParams = {
params: match? .params,query: ctx.query
}
const component = route.component
// @loadable/component Dynamically loaded components have a load method, which is used to load components
if (component.load) {
const c = (await component.load()).default
// there is a _init method that waits to be called, and then the data is stored in the Store
c._init && (await c._init(store, routeParams))
}
}
// The corresponding server HTML is generated through ctx.url, and the corresponding path dependency is obtained by clientExtractor
const appHtml = renderToString(
clientExtractor.collectChunks(
<Provider store={store}>
<StaticRouter location={ctx.url} context={context}>
<HelmetProvider context={helmetContext}>
<App />
</HelmetProvider>
</StaticRouter>
</Provider>))/* clientExtractor: getInlineStyleElements: style tags, inline CSS styles getScriptElements: script tags getLinkElements: Link tag, including preloaded JS CSS Link file getStyleElements: Link tag style file */
const inlineStyle = await clientExtractor.getInlineStyleElements()
const html = createTemplate(
renderToString(
<HTML
helmetContext={helmetContext}
scripts={clientExtractor.getScriptElements()}
styles={clientExtractor.getStyleElements()}
inlineStyle={inlineStyle}
links={clientExtractor.getLinkElements()}
favicon={` ${serverConfig.isProd ? '/' :` ${scriptsConfig.__WEBPACK_HOST__}:The ${scriptsConfig.__WEBPACK_PORT__} / `}static/client_favicon.ico`}
state={store.getState()}
>
{appHtml}
</HTML>))// HTML component template
// Prevent first-screen loading style errors by inserting style tags
__PRELOADED_STATE__ = window.__preloaded_state__ = window.__preloaded_state__ = window.__preloaded_state__ = window.__preloaded_state__
const HTML = ({ children, helmetContext: { helmet }, scripts, styles, inlineStyle, links, state, favicon }: Props) = > {
return (
<html data-theme="light">
<head>
<meta charSet="utf-8" />
{hasTitle ? titleComponents : <title>{rootConfig.head.title}</title>}
{helmet.base.toComponent()}
{metaComponents}
{helmet.link.toComponent()}
{helmet.script.toComponent()}
{links}
<style id="style-variables">{`:root {${Object.keys(theme.light) .map((key) => `${key}:${theme.light[key]}; `) .join('')}}`}</style>__PRELOADED_STATE__ () {inlineStyle} {// Implement data injection here, assign the data in store to window.__preloaded_state__<script
dangerouslySetInnerHTML={{
__html: `window.__PRELOADED_STATE__ = ${JSON.stringify(state).replace(/</g, '\ \u003c` ')}}} / ></script>
</head>
<body>
<div id="app" className="app" dangerouslySetInnerHTML={{ __html: children}} ></div>
{scripts}
</body>
</html>
)
}
ctx.type = 'html'
ctx.body = html
Copy the code
3. Execute the process
- through
@loadable/babel-plugin
Packaged uploadable-stats.json
File determination dependency - through
@loadable/server
In theChunkExtractor
To parse the file and return the directly operated object ChunkExtractor.collectChunks
Associate component, get JS and style file- Assign the js and CSS files to the HTML template and return them to the front end.
- Render the style of the first screen with the inline Style tag to avoid styling errors on the first screen.
- By calling the component
_init
Methods The obtained data were injected into the waterwindow.__PRELOADED_STATE__
In the - Front end access
window.__PRELOADED_STATE__
Data is synchronized to the store of the client - The front end retrieves the JS file and re-executes the rendering process. Bind react events and so on
- Front-end takeover page
4. Token processing
When users log in to do SSR, there will be a question about token. After login, the token is saved in the cookie. The personal information is obtained through the token. Normally, SSR is not used, and the interface request is separated from the front and back ends. Therefore, the cookie in the interface carries the token every time, and the token can be obtained from the interface every time. However, when doing SSR, the first load is carried out on the server side, so the interface request is carried out on the server side. At this time, you cannot obtain token in the interface.
I have tried the following methods:
- When the request comes in, put the
token
Get it and save itstore
When obtaining user information, take the token from the store and pass it into the URL, like this:/api/user? token=${token}
However, if there are many interfaces that require tokens, I will not pass them all. That would be too much trouble. - Then I wondered if I could pass the store tokens to the AXIos header so that I didn’t need to write them all. However, I thought of several ways, but could not figure out how to put the token in the request header in the store, because the store is isolated. After I generate a store, I can only pass it to the component, at most when I call the request in the component, pass the parameter, then I still have to write each one.
- Finally, I forgot where I read that you can store a token in a request instance. I use Axios, so I want to assign it to an Axios instance as an attribute. One caveat, though, is that Axios now has to do isolation on the server. Otherwise it’s shared by all users.
Code implementation
/* @client/utils/request.ts */
class Axios {
request() {
// The server side stores the token in the AXIos instance attribute token, and the browser side obtains the token directly from the cookie
const key = process.env.BROWSER_ENV ? Cookie.get('token') : this['token']
if (key) {
headers['token'] = key
}
return this.axios({
method,
url,
[q]: data,
headers
})
}
}
import Axios from './Axios'
export default new Axios()
/* ssr.ts */
// Do not import it externally as it is shared by all users
// import Axios from @client/utils/request
// SSR code implementation
app.use(async (ctx, next) => {
...
// Add the token attribute to axios. Now you can put the token in the header for every request
const request = require('@client/utils/request').default
request['token'] = ctx.cookies.get('token') | |' '
})
Copy the code
That’s basically what the server does, but there are other features that I won’t waste space on.
Client source code parsing
1. Route processing
Because some routes have layout, like the home page, blog details, etc., have public navigation and so on. Error pages, like 404 pages, do not have these layouts. Therefore, the two kinds of routes are distinguished, because two sets of loading animations are also supported. The transition animation based on layout also distinguishes the transition mode between PC and mobile.
PC Transition Animation
Mobile Transition Animation
The transition animation is implemented by react-transition-group. Different animations are performed by changing different classnames by routing forward and backward.
router-forward
: Go to the new pagerouter-back
Returns therouter-fade
: Transparency changes for page replace
const RenderLayout = () = > {
useRouterEach()
const routerDirection = getRouterDirection(store, location)
if(! isPageTransition) {// Manually or Link triggers push
if (history.action === 'PUSH') {
classNames = 'router-forward'
}
// Browser button trigger, or active pop operation
if (history.action === 'POP') {
classNames = `router-${routerDirection}`
}
if (history.action === 'REPLACE') {
classNames = 'router-fade'}}return (
<TransitionGroup appear enter exit component={null} childFactory={(child)= > React.cloneElement(child, { classNames })}>
<CSSTransition
key={location.pathname}
timeout={500}
>
<Switch location={location}>
{layoutRoutes.map((route, index) => {
return (
<Route
key={` ${index+ ${}route.path} `}path={route.path}
render={(props)= > {
return (
<RouterGuard Com={route.component} {. props} >
{children}
</RouterGuard>
)
}}
exact={route.exact}
/>
)
})}
<Redirect to="/ 404" />
</Switch>
</CSSTransition>
</TransitionGroup>)}Copy the code
The implementation of animation forward and backward because it involves the browser itself forward and backward, not just the page we control forward and backward. Therefore, we need to record the route changes to determine whether the route is forward or backward, not just the history action
history.action === 'PUSH'
It must be forward, because that’s what happens when we click to go to a new pagehistory.action === 'POP'
This can be triggered by history.back(), or by the browser’s own forward and back buttons.- The next step is to tell the difference between browser systems moving forward and backward. The code implementation is there
useRouterEach
The hook andgetRouterDirection
Method inside. useRouterEach
Hook function
// useRouterEach
export const useRouterEach = () = > {
const location = useLocation()
const dispatch = useDispatch()
// Update navigation records
useEffect(() = > {
dispatch(
updateNaviagtion({
path: location.pathname,
key: location.key || ' '
})
)
}, [location, dispatch])
}
Copy the code
updateNaviagtion
There is a routing record added and deleted, because every time you enter a new pagelocation.key
It will generate a new onekey
We can usekey
To record whether the route is new or old, and the new route ispush
tonavigations
Inside, if the record already exists, you can directly intercept the route record before the record, and then putnavigations
The update. This is the record of the whole navigation
const navigation = (state = INIT_STATE, action: NavigationAction): NavigationState= > {
switch (action.type) {
case UPDATE_NAVIGATION: {
const payload = action.payload
let navigations = [...state.navigations]
const index = navigations.findIndex((p) = > p.key === payload.key)
// If the same path exists, delete it
if (index > -1) {
navigations = navigations.slice(0, index + 1)}else {
navigations.push(payload)
}
Session.set(navigationKey, navigations)
return {
...state,
navigations
}
}
}
}
Copy the code
getRouterDirection
Method, obtainnavigations
Data, bylocation.key
To determine if the route is innavigations
Inside, if in the proof is return, if not in the proof is forward. This differentiates whether the browser is moving forward to a new page or back to an old one.
export const getRouterDirection = (store: Store<IStoreState>, location: Location) = > {
const state = store.getState()
constnavigations = state.navigation? .navigationsif(! navigations) {return 'forward'
}
const index = navigations.findIndex((p) = > p.key === (location.key || ' '))
if (index > -1) {
return 'back'
} else {
return 'forward'}}Copy the code
Routing switching logic
history.action === 'PUSH'
Proof is forward- If it is
history.action === 'POP'
Through thelocation.key
Go on recordnavigations
To determine whether this page is a new page or a page that has already been visited. To distinguish between going forward and going back - By acquisition
forward
或back
Execute the respective route transition animation.
2. Theme skin
Use CSS variables to create skin effects and declare multiple theme styles in the theme file
|-- theme
|-- dark
|-- light
|-- index.ts
Copy the code
// dark.ts
export default {
'--primary': '#20a0ff'.'--analogous': '#20baff'.'--gray': '# 738192'
'--red': '#E6454A'
}
// light.ts
export default {
'--primary': '#20a0ff'.'--analogous': '#20baff'.'--gray': '# 738192'
'--red': '#E6454A'
}
Copy the code
Then select a style and assign it to the style tag as a global CSS variable style. When rendering on the server, insert a style tag id=style-variables in the HTML template. You can use JAVASCRIPT to control the contents of the style tag, which is easy to switch between themes, but it is not IE compatible. If you want to use it and you need to be IE compatible, you can use CSS-vars-ponyfill to handle CSS variables.
<style id="style-variables">
{`:root {The ${Object.keys(theme.light)
.map((key) => `${key}:${theme.light[key]}; `)
.join(' ')}} `}
</style>
const onChangeTheme = (type = 'dark') = > {
const dom = document.querySelector('#style-variables')
if (dom) {
dom.innerHTML = `
:root {The ${Object.keys(theme[type])
.map((key) => `${key}:${theme[type][key]}; `)
.join(' ')}}
`}}Copy the code
But the blog did not do the theme switch, the theme switch is simple, anyway I do not intend to compatible with IE what, originally wanted to do, but the color is a little difficult for me 😢😢, think about temporarily not considered. The original UI is also a variety of other people look good blog how to design, their own is imitating the design of others, in addition to their own a little bit of design. I just made the UI. Normal can see quite good, did not engage in the theme, later to add, ha ha.
3. Use Sentry for project monitoring
The Sentry address
import * as Sentry from '@sentry/react'
import rootConfig from '@root/src/shared/config'
Sentry.init({
dsn: rootConfig.sentry.dsn,
enabled: rootConfig.openSentry
})
export default Sentry
/* aap.ts */
<ErrorBoundary>
<Switch>.</Switch>
</ErrorBoundary>
// componentDidCatch hook () {// componentDidCatch hook () {// componentDidCatch hook ()
class ErrorBoundary extends React.Component<Props.State> {
componentDidCatch(error: Error, errorInfo: any) {
// You can also report error logs to the server
Sentry.captureException(error)
this.props.history.push('/error')}render() {
return this.props.children
}
}
Copy the code
On the server side, errors are submitted via sentry.captureException, which tells the middleware to intercept errors and then submits errors
4. Front-end function points
A brief introduction to the rest of the function points, some will not be explained, the basic are relatively simple, directly look at the blog source on the line
1. ReactDom.createPortal
Use reactdom. createPortal to do global popovers, hints, etc. Reactdom. createPortal can render on dom outside of the parent node, so you can mount popovers directly onto the body. Can be packaged as a component
import { useRef } from 'react'
import ReactDom from 'react-dom'
import { canUseDom } from '@/utils/app'
type Props = {
children: anycontainer? :any
}
interface Portal {
(props: Props): JSX.Element | null
}
const Portal: Portal = ({ children, container }) = > {
const containerRef = useRef<HTMLElement>()
if (canUseDom()) {
if(! container) { containerRef.current =document.body
} else {
containerRef.current = container
}
}
return containerRef.current ? ReactDom.createPortal(children, containerRef.current) : null
}
export default Portal
Copy the code
2. Commonly used hook packaging
- UseResize, screen width changes
- UseQuery, query parameter is obtained
. And so on some commonly used hook, do not do too much introduction. A little bit about mask layer scrolling hooks
UseDisabledScrollByMask Function: Controls rolling when the mask layer is available
- Do you need to prohibit rolling under the mask layer?
- Does the mask layer need to prohibit rolling?
- Mask layer is prohibited to scroll, if there is a scroll inside the content, how to make it can be rolled. Rolling at the bottom of the mask layer will not be triggered by touching the bottom or top.
Code implementation
import { useEffect } from 'react'
export type Options = {
show: boolean // Open the mask layerdisabledScroll? :boolean // Disable scrolling. Default: truemaskEl? : HTMLElement |null // Mask layer DOMcontentEl? : HTMLElement |null // Scroll content dom
}
export const useDisabledScrollByMask = ({ show, disabledScroll = true, maskEl, contentEl }: Options = {} as Options) = > {
// document.body scroll disallow, add overflow: hidden to body; Style, forbid scrolling
useEffect(() = > {
/* .disabled-scroll { overflow: hidden; } * /
if (disabledScroll) {
if (show) {
document.body.classList.add('disabled-scroll')}else {
document.body.classList.remove('disabled-scroll')}}return () = > {
if (disabledScroll) {
document.body.classList.remove('disabled-scroll')
}
}
}, [disabledScroll, show])
// The mask layer forbids rolling
useEffect(() = > {
if (disabledScroll && maskEl) {
maskEl.addEventListener('touchmove'.(e) = > {
e.preventDefault()
})
}
}, [disabledScroll, maskEl])
// The content is forbidden to scroll
useEffect(() = > {
if (disabledScroll && contentEl) {
const children = contentEl.children
const target = (children.length === 1 ? children[0] : contentEl) as HTMLElement
let targetY = 0
let hasScroll = false // Is there room for scrolling
target.addEventListener('touchstart'.(e) = > {
targetY = e.targetTouches[0].clientY
const scrollH = target.scrollHeight
const clientH = target.clientHeight
// Use scroll height and element height to determine if the element needs to be rolled
hasScroll = scrollH - clientH > 0
})
// By listening on elements
target.addEventListener('touchmove'.(e) = > {
if(! hasScroll) {return e.cancelable && e.preventDefault()
}
const newTargetY = e.targetTouches[0].clientY
// distanceY > 0, drop; DistanceY < 0, pull up
const distanceY = newTargetY - targetY
const scrollTop = target.scrollTop
const scrollH = target.scrollHeight
const clientH = target.clientHeight
// If scrollTop = 0, it indicates that the element is scrolling to the top, so preventDefault is called to prevent it from causing the bottom body to scroll
if (distanceY > 0 && scrollTop <= 0) {
// Drop to the top
return e.cancelable && e.preventDefault()
}
// Same with pull-up
if (distanceY < 0 && scrollTop >= scrollH - clientH) {
// Pull up to the bottom
return e.cancelable && e.preventDefault()
}
})
}
}, [disabledScroll, contentEl])
}
Copy the code
There are some other features of the client that I won’t go into because there aren’t many modules to build a blog. You can go directly to the blog source
6. Admin side source code analysis
In fact, the back-end management terminal is almost the same as the client, I use antdUI framework to build, directly with the UI framework layout on the line. There’s basically not much to say, because there aren’t many modules. Originally also want to do user module, distribute different authority, think personal blog also I use, really do not use. If you need, I will add a module about permission allocation in the background management, to achieve the menu, button permission control. Let me focus on the following two function points
1. Implementation of user login interception
With the authTokenMiddleware MIDDLEWARE I mentioned above, you can implement login interception. If you have logged in, you can directly jump to the home page, and enter the login page if you have not logged in.
This is controlled by a permission component, AuthRoute
const signOut = () = > {
Cookie.remove(rootConfig.adminTokenKey)
store.dispatch(clearUserState())
history.push('/login')}const AuthRoute: AuthRoute = ({ Component, ... props }) = > {
const location = useLocation()
const isLoginPage = location.pathname === '/login'
const user = useSelector((state: IStoreState) = > state.user)
// No user information and not a login page
const[loading, setLoading] = useState(! user._id && ! isLoginPage)const token = Cookie.get(rootConfig.adminTokenKey)
const dispatch = useDispatch()
useEffect(() = > {
async function load() {
if(token && ! user._id) {try {
setLoading(true)
1. If the token expires, it will be processed in AXIos, and the login page will be redirected to if (error.response? .status === 401) {modal.warning ({title: 'Log out ', content:' Token expired ', okText: 'log in again ', onOk: () => { signOut() } }) return } 2. If loading is false, the loading will be loaded */
const { data } = await api.user.getUserInfoByToken()
dispatch(setUserState(data))
setLoading(false)}catch (e) {
signOut()
}
}
}
load()
}, [token, user._id, dispatch])
// There is no user information. The user can obtain user information using the token
if (loading && token) {
return <LoadingPage />
}
// When you have a token
if (token) {
// On the login page, go to the home page
if (isLoginPage) {
return <Redirect exact to="/" />
}
// No login page, enter directly
return <Component {. props} / >
} else {
// When there is no token
// Not the login page, jump to the login page
if(! isLoginPage) {return <Redirect exact to="/login" />
} else {
// This is the login page
return <Component {. props} / >}}}export default AuthRoute
Copy the code
2. Upload files and folders
All uploaded files are uniformly uploaded through FormData, and the background is received by busboy module, uploadFile code address
// The front end passes formData through append
const formData = new FormData()
for (const key in value) {
const val = value[key]
Append ('images[]', val) formdata.append ('images[]')
formData.append(key, val)
}
// Busboy is used to receive messages
typeOptions = { oss? :boolean // Whether to upload ossrename? :boolean // Whether to renamefileDir? :string // Write the file to the directoryoverlay? :boolean // Whether the file can be overwritten
}
constuploadFile = <T extends AnyObject>(ctx: Context, options: Options | Record<string, Options> = File.defaultOptions) => { const busboy = new Busboy({ headers: ctx.req.headers }) console.log('start uploading... ') return new Promise<T>((resolve, reject) => { const formObj: AnyObject = {} const promiseFiles: Promise<any>[] = [] busboy.on('file', async (fieldname, file, filename, encoding, mimetype) => { console.log('File [' + fieldname + ']: filename: '+ filename) /* Only one file will be accepted at a time. If more than one image is passed, the value of the field will not be overwritten. FormObj [fieldName.slice (0, index)] = [...(fieldname. index)] || []), val] */ const realFieldname = fieldname.endsWith('[]') ? fieldname.slice(0, -2) : fieldname }) busboy.on('field', (fieldname, val, fieldnameTruncated, valTruncated, encoding, Mimetype) => {// common field}) busbo. on('finish', async () => { try { if (promiseFiles.length > 0) { await Promise.all(promiseFiles) } console.log('finished... ') resolve(formObj as T) } catch (e) { reject(e) } }) busboy.on('error', (err: Error) => { reject(err) }) ctx.req.pipe(busboy) }) }Copy the code
7. HTTPS to create
Since blogs have all migrated to HTTPS, here’s how to generate certificates locally and develop HTTPS locally. Issue certificates through OpenSSL
This article refers to the construction of node.js local HTTPS service
We create our certificate in the SRC/Servers/SSL file
- Generate a CA private key
openssl genrsa -out ca.key 4096
- Generate a certificate signing request
openssl req -new -key ca.key -out ca.csr
- The root certificate is generated by signing the certificate
openssl x509 -req -in ca.csr -signkey ca.key -out ca.crt
CRT root certificate generated in the previous steps. Double-click to import the certificate and set it to always trust
Let’s make ourselves a CA and apply for a certificate for our server service
- Create two profiles
- server.csr.conf
[req] default_bits = 4096 prompt = no distinguished_name = dn [dn] CN = localhost # Common Name of the domain NameCopy the code
- V3. Ext, in here
[alt_names]
Now fill in your current IP, because in the code I’m going to use IP access on the local phone. So when I package it is some files that I access through IP.
authorityKeyIdentifier=keyid,issuer
basicConstraints=CA:FALSE
keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment
subjectAltName = @alt_names
[alt_names]
DNS.1 = localhost
DNS.2 = 127.0.0.1
IP.1 = 192.168.0.47
Copy the code
- To apply for the certificate
- Generate the private key for the server
openssl genrsa -out server.key 4096
- Generate a certificate signing request
openssl req -new -out server.csr -key server.key -config <( cat server.csr.conf )
- The CA signs the CSR
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt -sha256 -days 365 -extfile v3.ext
All files generated
Importing certificates into the Node service
const serverConfig.httpsOptions = {
key: fs.readFileSync(path.resolve(paths.serverPath, `ssl/server.key`)),
cert: fs.readFileSync(path.resolve(paths.serverPath, `ssl/server.crt`))
}
https.createServer(serverConfig.httpsOptions, app.callback()).listen(rootConfig.app.server.httpsPort, '0.0.0.0'.() = > {
console.log('Project launched ~~~~~')})Copy the code
At this point, the local HTTPS certificate is set up, and you can happily start the HTTPS journey locally
conclusion
The whole blog process is probably these, there are some did not do too much explanation, mainly nuggets of words beyond ðŸ˜ðŸ˜, just posted a general code. So if you want to see specific words, go directly to the source code.
This article focuses on local project development, and then how to put local services online. Because of the length of blog posts, THIS article does not cover how to publish projects from a development environment to a build environment. Later I will publish an article on how to set up a service on Aliyun, HTTPS free certificate and nginx configuration to resolve domain names to set up different services.
There are many flaws in blogs. And some of the things I wanted to do that I didn’t get to.
- Back office administration was split off separately.
- The server API module is separated to create a management API related service.
- – shared utility classes, including client/admin/hooks, which need to be retooled on private server
- I bought it several times, and then had to migrate when it was due. Each time, there were various environment configurations, but it was troublesome. Later, I heard that Docker could solve the writing problem, so I simply studied it, so I also planned to use Docker this time, mainly because the server was also about to expire. Renewals aren’t cheap either ðŸ˜ðŸ˜. Double 11 directly bought in the past, now renewal, but also very expensive. I wonder if I should switch servers. So with docker, it should be easier
- CI/CD continuous integration, I now all development is to upload Git, and then enter the server, pull down and then package, you can also bother 😂😂, so this is also going to integrate.
Github full code address
This article addresses
Blog Online Address
As a non – division class of the wild road, basic are their own groping across the river. I also have a vague understanding of many things, but I will try to explain in the scope of my own understanding, there may be some problems in the technical understanding is not correct. In addition, the blog function is basically built by myself, many things may not be comprehensive, including did not do too much testing, there will be a lot of shortcomings, if there are mistakes, I hope you pointed out, I will try to improve these defects, thank you.
I created a new mutual learning group, if you don’t understand, I can know, I will try to answer. If I don’t understand, I hope you can give me some advice.
QQ group: 810018802, click join