SPA + SSR + PWA ã®ä½ãæ¹ã¨ã»ãã¥ãªãã£ã«ã¤ãã¦
ä¸å¹´åã«ä»¥ä¸ã®è¨äºãæ¸ãã¦ããã®å¾æ¾ç½®ãã¦ãããå¤ãã®ã©ã¤ãã©ãªã®ã¡ã¸ã£ã¼ãªãªã¼ã¹ã§å®å ¨ã«åããªããªã£ã¦ãã¾ã£ãã®ã§ãªãã¥ã¼ã¢ã«ãã¾ããã
以ä¸ã®ã»ã¯ã·ã§ã³ã§èª¬æãã¦ããã¾ãããã³ã¼ããèªãã ã»ããæ©ãã§ãã
- ãªãã¸ããª
- æè¡ã¹ã¿ãã¯
- Server Side Rendering
- Single Page Application
- Progressive Web Application
- Audits
- Security
- ããã
ãªãã¸ããª
ãã®ãªãã¸ããªã¯ãããè¦ãã°æ§ã
ãªå®è£
ã®åãå俵ãä½ããã¨ããã®ãç®çã¨ãã¦ãã¾ãã
ç°å¢æ§ç¯ã§ããæ¯åå¿ããã®ã§ãã
ãªã®ã§åé·ã«æ¸ãã¦ããé¨åãå¤ãã§ãããä»å¾ãæ°ããå¿
è¦ãªå®è£
ãå°ãã追å ãã¦ããäºå®ã§ãã
åãã確èªããã人ã¯ãcloneãã¦æå ã§åããã¦ã¿ã¦ãã ããã
æè¡ã¹ã¿ãã¯
ããã¯ããã¾ã§ããµã³ãã«ãªã®ã§ãsagaã¨apolloãæ··ãã£ã¦ã¾ããå®éã¯ã©ã¡ããã§å¤§ä¸å¤«ã§ãã
SPAã®ãã¼ã¹ã¯redux, sagaã§è¨è¨ãã¦ãã¦ãapollo-stateã¯ä½¿ããããã¾ã§ãqueryã¨mutationã®ã¿ã§ãã
主è¦ãªã©ã¤ãã©ãªã¯ä»¥ä¸ã®ã¨ããã§ãã
deps | devDeps |
---|---|
react | typescript |
redux | webpack |
react-router | babel |
react-helmet | storybook |
redux-saga | storyshots |
styled-components | jest |
loadable-components | testing-library |
apollo-boost | nodemon |
express | prettier |
nanoid | workbox |
typescript-eslint | |
autocannon |
Server Side Rendering
注ç®ããã¹ãç¹ã¯loadable-componentsã®å¤§å¹
ãªã¢ã«ã´ãªãºã æ¹åã ã¨æã£ã¦ãã¾ãã
ããã«ãããããã©ã¼ãã³ã¹ã¯æ¹åããã¾ããã
loadable-components
æªã ãReactã®ã»ããSSRã«å¯¾å¿ãã¦ããªããããå¼ãç¶ãreact-loadableãloadable-componentsã¯å¿ è¦ã¨ãªãã¾ãã
Suspenseã¯SSR対å¿é²ãã¦ãã¾ãã
ã¡ã¸ã£ã¼ãã¼ã¸ã§ã³ã§react-loadableã¨åæ§ã«webpackã使ãassetsã®mapãä½æããããã«ãªãã¾ããã
ããã«ãããSSRã®å¦çãé«éåããã¾ããã
ããããbabel-pluginã«ä¾åããªãã¨åããªããããbabel-preset-typescriptããã®ã©ã¤ãã©ãªã§ã¯ä½¿ã£ã¦ãã¾ãã
ãã¼ã¿ã®éãæ¹
SSRã§åå¾ãããã¼ã¿ã¯Storeãæ§ç¯ãããããã¯ã©ã¤ã¢ã³ããµã¤ãã«HTMLçµç±ã§æ¸¡ãã¾ãã
以ä¸ã®ããã«data
å±æ§ã使ããscriptã¿ã°çµç±ã§æ¸¡ãã®ãããã¨èªåãæã£ã¦ãã¾ãã
<script nonce="xxxxx" id="initial-data" type="text/plain" data-json="${preloadedState}"></script>
ãã®preloadedState
ã¯ã¨ã¹ã±ã¼ãå¦çãå¿
è¦ãªã®ã§æ³¨æãã¦ãã ããã
ã¯ã©ã¤ã¢ã³ãå´ã®èªã¿è¾¼ã¿æ¹
const initialData = JSON.parse(document.getElementById('initial-data')!.getAttribute('data-json')!); const { store } = configureStore(initialData);
ssr-sample/index.tsx at master · hiroppy/ssr-sample · GitHub
useEffect
SSRã§ã¯ãcomponentDidMountåã¾ã§ããå®è¡ããã¾ããã
ã¤ã¾ããhooksã§ã¯useEffect
ã¯å¼ã³åºãããFCã«ã¯constructorã¯åå¨ãã¾ããã
ä¸ä½ã©ãã«åæåå¦çã¨ãããã°æ¸ãã°ããã®ããã¹ããã©ã¯ãã£ã¹ã¯èªåã¯ãããã¾ããã
if (!process.env.IS_BROWSER) { dispatch(loadSagaPage(maxLength)); } else { useEffect(() => { dispatch(loadSagaPage(maxLength)); }, []); }
ä»ã¯ãããªãµãã«æ¸ãã¦ãããã©æ°æã¡æªãã®ã§è¾ããããã
ã¬ã³ããªã³ã°ã³ã¼ã
// ããã§assetsã®mapãåå¾ãã const statsFile = resolve( __dirname, process.env.NODE_ENV !== 'production' ? '../../../../dist/client/loadable-stats.json' : '../../../../client/loadable-stats.json' ); export async function get(req: Request, res: Response) { const baseUrl = `${req.protocol}://${req.get('Host')}`; const { nonce }: { nonce: string } = res.locals; const { store, runSaga } = configureStore(); const client = createClient({ link: new SchemaLink({ schema }) }); const sheet = new ServerStyleSheet(); const context = {}; // Node.jsã§ã¯å®å ¨ãªurlãå¿ è¦ãªã®ã§storeã«ããã store.dispatch(setBaseUrl(baseUrl)); const App = () => ( <ApolloProvider client={client}> <Provider store={store}> <StaticRouter location={req.url} context={context}> {/* add `div` because of `hydrate` */} <div id="root"> <Router /> </div> </StaticRouter> </Provider> </ApolloProvider> ); try { const extractor = new ChunkExtractor({ statsFile }); // assets mapãããã®ã§renderToStringãèµ°ãããå¿ è¦ããªããªã£ã const tree = extractor.collectChunks(<App />); await Promise.all([ // react-apolloã®å¦çãããã¯ãããã¨ã«ãããredux-saga, react-helmet, styled-componentsã®å¦çãå®è¡ getMarkupFromTree({ tree, renderFunction: renderToStaticMarkup // ããã¾ã§ãå¦çãå®è¡ãããããªã®ã§è»½éãªstaticMarkupã§è¯ã }), // ä¸è¨ã®renderToStaticMarkupã§å®è¡ãããsagaã®çµäºãå¾ ã¤ runSaga() ]); const body = renderToString(tree); // ããã§ã¯ã©ã¤ã¢ã³ãã«æ¸¡ãhtmlã®ã¬ã³ããªã³ã°ãè¡ã // ããããã¯htmlã«åãè¾¼ãscriptã¿ã°ã®çæãstoreã®ãã¼ã¿ãã¯ã©ã¤ã¢ã³ãã«æ¸¡ãããã®jsonçãä½æ const preloadedState = JSON.stringify(store.getState()); const helmetContent = Helmet.renderStatic(); const meta = ` ${helmetContent.meta.toString()} ${helmetContent.title.toString()} `.trim(); const style = sheet.getStyleTags(); const scripts = extractor.getScriptTags({ nonce }); const graphql = JSON.stringify(client.extract()); return res.send(renderFullPage({ meta, body, style, preloadedState, scripts, graphql, nonce })); } catch (e) { console.error(e); return res.status(500).send(e.message); } }
ssr-sample/renderer.tsx at master · hiroppy/ssr-sample · GitHub
Single Page Application
SPAã®ãã¼ã¹ã¯reduxã®store(or apollo-state), routingãreact-router, å¯ä½ç¨ã®æä½ãredux-sagaã§ãã®ãµã³ãã«ã¯è¡ã£ã¦ãã¾ãã
hooks
reactã«hooksãå ¥ã£ããã¨ã«ãããreact-routerãreduxãapolloã®hooks対å¿ããã¾ããã
export const Saga: React.FC = () => { const dispatch = useDispatch(); // reduxã®dispatch const samples = useSelector(getSagaCode); // reduxã®selector const { search } = useLocation(); // react-routerã§locationãåå¾ const maxLength = new URLSearchParams(search).get('max'); if (!process.env.IS_BROWSER) { dispatch(loadSagaPage(maxLength)); // actionãå®è¡ããtypeã¨preloadãdispatchã¸(containersãè¡ã£ã¦ãããã¨) } else { useEffect(() => { dispatch(loadSagaPage(maxLength)); }, []); } const like = useCallback((id: number) => { // reactã®useCallback dispatch(addLike(id)); }, []); return ( <> <Head title="saga-page" /> <p>get => get all samples</p> <p>post => add a like count</p> {samples.length !== 0 && <CodeSamplesBox samples={samples} addLike={like} />} </> ); };
ssr-sample/Saga.tsx at master · hiroppy/ssr-sample · GitHub
redux
reduxã¯hooksãå
¥ã£ããã¨ã«ãã大ããªå¤æ´ãããã¾ãã
ããã¯ãpresentationalã¨containerã¨ããåèªãç¡ããªãããã§ãã
ä»ã¾ã§ã®reduxã¯ãpresentationalã¨containerã§è²¬å(é¢å¿äº)ãå¥ãã¦ãã¾ããã
ããã¯èªåã«ã¨ã£ã¦ã¯ãããã ã¨æã£ã¦ãã¾ãããpresentationalã§storeããããå¤ã¯propsã渡ãæãã
ããããhooksãå ¥ã£ããã¨ã«ãããdispatchãpresentationalããå¼ã¶ãã¨ã«ãªã£ãã®ã§containerãå¿ è¦ãªãã§ãã
apollo
apolloã¯æ¬å½ã«ãããã«æ¸ããã¨ãã§ããããã«ãªãæºè¶³ãã¦ãã¾ãã
移è¡è¨äºã¯ä»¥ä¸ãåèã«ãã¦ãã ããã
export const GET_SAMPLES = gql` query getSamples($maxLength: Int) { samples(maxLength: $maxLength) { id name code likeCount description } } `; export const ADD_LIKE = gql` mutation addLike($id: Int) { addLike(id: $id) { id } } `; export const Apollo = () => { const dispatch = useDispatch(); const { search } = useLocation(); const maxLength = new URLSearchParams(search).get('max'); const { loading: queryLoading, error: queryError, data: queryData } = useQuery<{ // queryã®hooks samples: Samples; }>(GET_SAMPLES, { variables: { maxLength: Number(maxLength) } }); const [ addLike, { loading: mutationLoading, error: mutationError, data: mutationData } ] = useMutation(ADD_LIKE, { // mutationã®hooks // ããã¯å®éãrefetchè¡ãã¹ããããªããã©ããããµã³ãã«ãªã®ã§ææãã§ã refetchQueries: [{ query: GET_SAMPLES, variables: { maxLength: Number(maxLength) } }] }); const like = useCallback((id: number) => { addLike({ variables: { id } }); // mutationãå®è¡ }, []); // SPAãsagaã§ç®¡çãã¦ããé¢ä¿ä¸ãããã§ãstopã ãè¡ããªãã¨ãããªã if (!process.env.IS_BROWSER) { dispatch(loadApolloPage()); } return ( <> <Head title="apollo-page" /> <p>query => get all samples</p> <p>mutation => add a like count</p> {queryLoading && <p>loading...</p>} {queryError && <p>error...</p>} {queryData && <CodeSamplesBox samples={queryData.samples} addLike={like} />} </> ); };
ssr-sample/Apollo.tsx at master · hiroppy/ssr-sample · GitHub
ã¯ãããããããã(sagaæ¨ã¦ããé¡)
redux-saga
sagaãè¡ããã¨ã¨ãã¦ãã¯ã©ã¤ã¢ã³ããµã¤ãã¨ãµã¼ãã¼ãµã¤ãã§ä¸ç¹ç°ãªãç¹ãããã¾ãã
ããã¯ãsagaã®ããã»ã¹ããµã¼ãã¼ãµã¤ãã®å ´ååæ¢ãããªãã¨ãããªããããããªãã¨ã¯ã©ã¤ã¢ã³ãã«htmlãè¿ãã¾ããã
ãªã®ã§ã以ä¸ã®ããã«æ¢ããããã«ãã¾ãã
function* loadTopPage(actions: ReturnType<typeof LoadTopPage>) { yield changePage(); yield put(loadTopPageSuccess()); if (!process.env.IS_BROWSER) { yield call(stopSaga); // ENDãå¼ã¶ } }
ssr-sample/pages.ts at master · hiroppy/ssr-sample · GitHub
èªåãSPAã§å®è£ ãè¡ãã¨ãã¯ãsagaã2ã©ã¤ã³èµ°ããã¾ãã
- å
¨ä½ã管çããappProcess
- èªã¿è¾¼ã¿å®äºãã¨ã©ã¼(502, etc...)ãã©ãã®ãã¼ã¸ã§ãè¡ãå¦ç(e.g. login, ga, etc..)
- åãã¼ã¸ã®pageProcess
- ãã¼ã¸åºæã®å¦ç(e.g. fetching, etc...)
export function* pagesProcess() { yield takeLatest(LOAD_APP_PROCESS, appProcess); yield takeLatest(LOAD_TOP_PAGE, loadTopPage); yield takeLatest(LOAD_SAGA_PAGE, loadSagaPage); yield takeLatest(LOAD_APOLLO_PAGE, loadApolloPage); }
ããããçµãã次第ãENDãå¼ã¶æ§ç¯ãä¸çªããã¨æãã¾ãã
App Shell
PWAã¨å°ã被ãã¾ãããsagaã¨ã®è©±ãããã®ã§ããã§ã
react-routerã§ãã¹ã«å¿ããcomponentsã¯ããã«æµãã¦ãã¾ãã
ã¤ã¾ãããã®ã³ã³ãã¼ãã³ããä¸ä½é層ã§ãããã§headerã ãã®ã¬ã³ããªã³ã°(App)ã¨å
±éå¦ç(appProcess)ãè¡ãã¾ãã
export const App: React.FC = ({ children }) => { const location = useLocation(); const dispatch = useDispatch(); // ããã¯middlewareã§è¡ãå ±éã®å¦ç(appProcess)ãå®è¡(èµ·åããã) if (!process.env.IS_BROWSER) { dispatch(loadAppProcess()); } else { useEffect(() => { dispatch(loadAppProcess()); }, []); } // e.g. send to Google Analytics... useEffect(() => {}, [location]); // ããã§SPAå ¨ä½ã®ãã¹å¤æ´ãç£è¦ããã¤ãã³ããçºç«ããã(e.g. GA) return ( <> <Header /> {/* ããã¯å¤ãããã¨ããªã(ãã£ã¦ãreduxçµç±ã§Headerå selectorã«ããåæç») */} <GlobalStyle /> <Container>{children}</Container> {/* childrenã¯react-routerããæ¥ãã³ã³ãã¼ãã³ã */} </> ); };
ssr-sample/App.tsx at master · hiroppy/ssr-sample · GitHub
Progressive Web Application
Manifest
PWAã®manifestã¯ãmanifest.json
ã§ã¯ãªãmanifest.webmanifest
ã¨ãããã¡ã¤ã«åã«ãã¾ãã
ä»æ§
webpack-pwa-manifestã使ããçæãã¾ãã
// webpack.config.js new PwaManifest({ filename: 'manifest.webmanifest', name: 'ssr-sample', short_name: 'ssr-sample', theme_color: '#3498db', description: 'introducing SPA and SSR', background_color: '#f5f5f5', crossorigin: 'use-credentials', icons: [ { src: resolve('./assets/avatar.png'), sizes: [96, 128, 192, 256, 384, 512] } ] })
ssr-sample/webpack.client.prod.config.js at master · hiroppy/ssr-sample · GitHub
add to home screençã¯ãããã°ãã¥ããã®ã§ãããåå ãããããªãã£ããchrome://flags/#bypass-app-banner-engagement-checks
ããªã³ã«ããã¨å¹¸ãã«ãªãã¾ãã
PCã§ã®add to home screenã¯ãããªæãã«ãªãã¾ãã
Service Worker
Workboxã使ãã¾ãã
// webpack.config.js new GenerateSW({ clientsClaim: true, skipWaiting: true, include: [/\.js$/], // ä»ååºåãjsãããªããã runtimeCaching: [ { urlPattern: new RegExp('.'), // start_urlã«åããã handler: 'StaleWhileRevalidate' // cacheã使ãè£ã§fetchãã }, { urlPattern: new RegExp('api|graphql'), handler: 'NetworkFirst' // ãããã¯ã¼ã¯ã¢ã¯ã»ã¹ãåªå ãã }, { urlPattern: new RegExp('https://fonts.googleapis.com|https://fonts.gstatic.com'), handler: 'CacheFirst' // cacheãåªå ãããexpireè¨å®ããã»ãããã } ] })
ssr-sample/webpack.client.prod.config.js at master · hiroppy/ssr-sample · GitHub
å¾ã»ã©ã説æãã¾ãããCSPã«ã¯æ³¨æãã¦ãã ããã
service-workerããã®ã¢ã¯ã»ã¹ã¯connect
ã¨ãªãã¾ãã
Audits
Expressã®http/2対å¿ããã¼ã¸ãããã°å ¨é¨100ã«ãªãã¾ãã(or Nginxç½®ãã¦)
Security
Content Security Policy
CSPã¨ã¯ãXSSãé²ãããã«ä¿¡é ¼ãããã®ãããã©ã¦ã¶ãå®è¡ããªãããã«å¶å¾¡ã§ãã¾ãã
ãµã¼ãã¼ã§æ¯åããã·ã¥å¤ãçæãããããscriptã¿ã°ã«ã¤ãhttp headerããContent-Security-Policy
ã®å±æ§ãç
§ä¼ãä¸è´ãããã®ã ããå®è¡ãã¾ãã
ããã¯ãscript以å¤ã«ãcssãfont, images, connectçå¹
åºãè¨å®ã§ãã¾ãã
ä»åã®ãµã³ãã«ã§ã¯ãgoogle fontã使ãããgoogle fontã¨readmeã®ããã¸ã§ä½¿ãããshieldsã許å¯ãã¦ãã¾ãã
// google font: https://stackoverflow.com/a/34576000/7014700 const baseDirectives: helmet.IHelmetContentSecurityPolicyDirectives = { defaultSrc: ["'self'"], styleSrc: ["'unsafe-inline'", 'fonts.googleapis.com'], // for styled-components fontSrc: ["'self'", 'data: fonts.gstatic.com'], imgSrc: ["'self'", 'img.shields.io'], // for README connectSrc: ["'self'", 'img.shields.io', 'fonts.googleapis.com', 'fonts.gstatic.com'], // for service-worker workerSrc: ["'self'"] };
ssr-sample/csp.ts at master · hiroppy/ssr-sample · GitHub
ãã®ããã«æå®ããã¨ããã¸ã®ã¢ã¯ã»ã¹ã ãã許å¯ãããã¨ããXSSã«å¯¾ãã¦å¼·åºãªwebã¢ããªã±ã¼ã·ã§ã³ãä½æã§ãã¾ãã
ãã®ã¢ããªã±ã¼ã·ã§ã³ã§ã¯ãnonce
æ¹å¼ã説æãã¦ããã¾ãã
export function generateNonceId(req: Request, res: Response, next: NextFunction) { res.locals.nonce = Buffer.from(nanoid(32)).toString('base64'); next(); }
<meta property="csp-nonce" content="ZmZIaTAyQ25rZ2FxVERUVVI4VUhBTUVJc29uTV9leUo="> <script async data-chunk="components-pages-Top" src="/public/vendors~components-pages-Apollo~components-pages-NotFound~components-pages-Saga~components-pages-Top.bundle.js" nonce="ZmZIaTAyQ25rZ2FxVERUVVI4VUhBTUVJc29uTV9leUo="></script>
ãã£ã¨è©³ããç¥ãããæ¹ã¯Pixivã®è¨äºã詳ããã®ã§èªãã¨è¯ãããã§ãã
Dynamic Import
CSPã®åé¡ç¹ã¨ãã¦ãdynamic importã®å¯¾å¿ã®é£ãããä¸ãããã¾ãã
dynamic importã®å ´åãnonce
ãåå¨ããªãããã§ãã
CSPã«ã¯ãlevel3ã¨level2ãåå¨ããlevel3ã«ã¯strict-dynamicã¨ããä»çµã¿ãããããããã®åé¡ã解決ãã¾ãã
strict-dynamicã§ã¯ãnonceä»ãã®å®è¡ãããã¹ã¯ãªããã®åä¾ã¯nonceãç¡ãã¦ãå®è¡å¯è½ã¨ãªãã¾ãã
FirefoxãChromeã§ã¯ãã§ã«level3ã対å¿æ¸ã¿ãªã®ã§ãã®åé¡ã¯è§£æ±ºã§ãã¾ãããlevel3ã«å¯¾å¿ãã¦ããªããã©ã¦ã¶ã«å¯¾ãã¦dynamic importã¯è§£æ±ºãè¡ãªãã¾ããã
__webpack_nonce__
ã使ãã°ãåãã¾ããnonceã¯æ¬æ¥æ¯ã¢ã¯ã»ã¹æã«hashãçæããªãã¨æ»æè
ã«æ¨æ¸¬ãããå¯è½æ§ãããããããã«ãæã§ã¯ãããªãæ ¹æ¬çãªè§£æ±ºã§ã¯ããã¾ããã
// chrome, firefox const lv3Directives: helmet.IHelmetContentSecurityPolicyDirectives = { ...baseDirectives, scriptSrc: [(req, res) => `'nonce-${res.locals.nonce}'`, "'strict-dynamic'", "'unsafe-eval'"] }; // safari const lv2Directives: helmet.IHelmetContentSecurityPolicyDirectives = { ...baseDirectives, scriptSrc: [ "'self", (req, res) => `'nonce-${res.locals.nonce}'`, "'unsafe-eval'", "'unsafe-inline'" ] };
Service Worker
service workerããã®åãåããã¯connect-src
ã¨ãªãã¾ãã
以ä¸ã®ããã«ãstyleSrc
ãfontSrc
ã¨åãurlãconnectSrc
ã«æ¸ãã¦ããã®ããããã¾ãã
const baseDirectives: helmet.IHelmetContentSecurityPolicyDirectives = { defaultSrc: ["'self'"], styleSrc: ["'unsafe-inline'", 'fonts.googleapis.com'], // for styled-components fontSrc: ["'self'", 'data: fonts.gstatic.com'], imgSrc: ["'self'", 'img.shields.io'], // for README connectSrc: ["'self'", 'img.shields.io', 'fonts.googleapis.com', 'fonts.gstatic.com'], // for service-worker workerSrc: ["'self'"] };
GraphQL
GraphQLã¯ã¹ãã¼ããèªç±ã§ããããããµã¼ãã¼ã¸ã®è² è·å¯¾çãè¡ãå¿
è¦ãããã¾ãã
ä¾ãã°ãå
¥ãåã®æ·±ãä¸æ£ã¯ã¨ãªã¼ãéããã¦ããã¨ãã«ã¯ãµã¼ãã¼å´ã®å¦çã«è² è·ããããå¯è½æ§ãããããããã®å¦çã«å°éãããåã«å¼¾ãå¿
è¦ãããã¾ãã
DoSãé²ãããã«ãå¿
ãå
¥ãã対çããã®å¯¾çã§ãã
æåãªæ¹æ³ã¯ä»¥ä¸ã®ã¨ããã§ãã
- ãã¯ã¤ããªã¹ã
- ãªã¹ãã«æ¸ãããã¯ã¨ãªã®ã¿ãééããã
- æ·±ãå¶é
- æå®ããã¯ã¨ãªã®æ·±ã以ä¸ãééããã
- éã¿(ã³ã¹ã)å¶é
- ã¯ã¨ãªã¼ã«éã(æ·±ãå«ã)ä»ããããããã®åè¨å¤ãæå®å¤ä»¥ä¸ã®å ´åã¯ééããã
ãã®ãµã³ãã«ã§ã¯ãéã¿å¶éã使ç¨ãã¦ãã¾ãã
ä¾ãã°ãä»åã¯ä½¿ã£ã¦ãªãã§ããgraphql-validation-complexityã§ããã°ä»¥ä¸ã®è¨ç®å¼ã¨ãªãã¾ãã(å®éãfragmentsçã§ããå°ãé£ãããªãã¾ãã)
// Conclusion // Field: 1 // root: scalarCost * 1 // not root: objectCost * 1 // list: listFactor * 10 // query { // a { # * objectCost // a1a # * scalarCost // a1b { # * objectCost // b1a # * scalerCost // b1b # * scalerCost // } // } // arr { # * objectCost // arr1 { # * objectCost * listFactor // name # listFactor // } // arr2 { # objectCost * listFactor // name # listFactor // id # listFactor // } // } // } // a * objectCost + a.a1a * scalarCost + a.a1b * objectCost + a.a1b.b1a * scalerCost + a.a1b.b1b * scalerCost // + arr * objectCost + arr.arr1 * objectCost * listFactor + arr.arr1.name * listFactor // + arr.arr2 * objectCost * listFactor + arr.arr2.name * listFactor + arr.arr2.id * listFactor
ä»åã¯ãgraphql-query-complexityã使ã£ã¦ãã¾ãã
const apollo = new ApolloServer({ plugins: [ { requestDidStart: () => ({ didResolveOperation({ request, document }) { const complexity = getComplexity({ schema, query: request.operationName ? separateOperations(document)[request.operationName] : document, variables: request.variables, estimators: [fieldExtensionsEstimator(), simpleEstimator({ defaultComplexity: 1 })] }); // graphqlã®ã¹ãã¼ãã®ã³ã¹ããlimitCost(ä»åã¯10)以ä¸ã§ããã°ãthrowãä¸æããã if (complexity >= limitCost) { throw new Error(`${complexity} is over ${limitCost}`); } console.log('Used query complexity points:', complexity); }, didEncounterErrors(err) { console.error(err); } }) } ] });
ssr-sample/apollo.ts at master · hiroppy/ssr-sample · GitHub
GraphQLã¯ãããã¯ã·ã§ã³ã§ãªãªã¼ã¹ããã¨ãã«ã¯å¿ ãããã®ãããªå¯¾çãå¿ è¦ã¨ãªãã¾ãã
ããã
é·æã«ãªãã¾ããããæ©è½è¿½å çã®PR/Issueæè¿ãã¦ãã¾ãã
ã¾ããããèå³ããã°GitHub Sponsorsããããããããããã¾ãã
ãã¨ãwebpack@5楽ãã¿ð¥°
webpack5ã®å¤§ããªå¤æ´
— hiroppy (@about_hiroppy) November 19, 2019
- Node.jsã®polyfillãèªåã§å ¥ããªããªã
- Tree Shakingã®ã¢ã«ã´ãªãºã æ¹å
- ãã«ãã¤ã³åºåãã¡ã¤ã«ã®ãã¼ã¸ã§ã³ãæå®ããoutput.ecmaVersionã追å
- æ°¸ç¶ãã£ãã·ã¥ã«ããéçºå¹çå
- webpackChunkName ã®èªåå
- file-loaderããã«ãã¤ã³
- top-level-await