上文讲到服务端输出hello world,此次咱们加入react,服务端输出html,让js去进行客户端渲染页面。html
你们都知道react组件对应的文件后缀名是jsx,而使用ts的话,后缀名是tsx。java
目前用到的依赖有react和react-dom,还须要安装对应的@typesnode
npm install react react-dom --save npm install @types/react @types/react-dom --save-dev
PS:版本号是最新的16.0,@types/react-dom主版本号仍是15
react
写代码前先作好代码规范相关的配置,养成良好习惯仍是有必要的。webpack
安装tslint
及tslint-react
git
npm install tslint tslint-react --save-dev
在根目录下新建配置文件tslint.json
github
// ./tslint.json { "extends": ["tslint:latest", "tslint-react"], "rules": { "quotemark": [true, "single"], "no-console": [true, "warn"] } }
这里的规则(rules)我本身修改了两个,也能够不改或改其它的(看我的习惯),一个是引号(使用单引号),一个是console(不报错,仅警告)web
选择图中红框里的图标,而后输入tslint,能够看的,没安装过的同窗会看的和下面两个同样的Install
按钮,安装了的和我这里同样,安装完毕后vs code会重启当前窗口以使得插件生效。typescript
疑问一:以前安装过tslint插件,在新的项目里添加tslint.json后不生效?
答:这个也正是我如今遇到的一个小问题,随便打开一个tsx文件,能够发现tslint没有生效,而后看右下角有提示:express
点开后咱们能够看到这样一段话:
To use TSLint in this workspace please install tslint using 'npm install tslint' or globally using 'npm install -g tslint'. TSLint has a peer dependency on `typescript`, make sure that `typescript` is installed as well. You need to reopen the workspace after installing tslint.
因此直接重启一下当前vs code窗口就能够了。
在client目录下新建component目录,用于存放组件,在该目录下新建app子目录,而后再在app目录下新建index.tsx文件,这个文件会导出App这个组件,做为根组件。
// ./src/client/component/app/index.tsx import * as React from 'react'; class App extends React.PureComponent { public render() { return ( <div>hello world</div> ); } } export default App;
代码如上,但实际在编辑器里,你们会看到<div>
下面有一条红线,移上去能够看到:
这个是由于须要配置tsconfig,使得能够在tsx文件中支持JSX语法,配置以下:
// ./tsconfig.json { ... "compilerOptions": { ... "jsx": "react", ... }, ... }
疑问二:App为何继承PureComponent而不是Component?
答:PureComponent相较于Component来讲,其只会在props和state变动时才会进行从新render,固然这种比较是浅比较,有潜在的问题,可使用Immutable.js来解决。
疑问三:render前面的public是什么?
答:tslint中有一条rule是member-access,具体规定见https://palantir.github.io/ts...,简单来讲就是要定义好类属性方法是公共的
仍是私有的
仍是受保护的
,相似于java中的类。
客户端入口文件主要是将根组件引入并执行ReactDOM相关方法来渲染,在window.onload中执行:
// ./src/client/index.tsx import * as React from 'react'; import * as ReactDOM from 'react-dom'; import App from './component/app'; function renderApp() { (ReactDOM as any).hydrate( <App />, document.getElementById('app'), ); } window.onload = () => { renderApp(); };
疑问四:(ReactDOM as any).hydrate是什么?不该该是ReactDOM.render吗?
首先说一下为何使用hydrate而不是render,这个是react 16版本中的一个变动,hydrate主要是用于给服务端渲染出的html结构进行“注水”,因为新版本中ssr出的dom节点再也不带有data-react,为了能尽量复用ssr的html内容,因此须要使用新的hydrate方法进行事件绑定等客户端独有的操做。
参见原文说明:ReactDOM
参见知乎问题:react中出现的"hydrate"这个单词究竟是什么意思?
如今再来讲一下为啥要写ReactDOM as any
,这个是ts的语法,介于目前@types/react-dom主版本仍是15,并无hydrate方法的定义,因此将ReactDOM视为any类型,则可使ts的类型检测经过而不报错。
参见ts任意值:任意值·TypeScript入门教程
如今咱们要作的就是将写好的客户端入口文件打包成浏览器能够直接运行的js代码文件,咱们使用webpack来进行配置。
执行如下命令
npm install webpack lodash --save npm install @types/webpack @types/lodash awesome-typescript-loader webpack-dev-middleware @types/webpack-dev-middleware --save-dev
因为webpack配置根据环境不一样(客户端,服务端,开发,生产)而不一样,故须要使用到深度复制库来使得各个环境的配置继承公共配置,这里使用了lodash中的cloneDeep,因此依赖里有lodash。
至于awesome-typescript-loader(后文称at-loader
),咱们选用它做为webpack处理tsx?文件的loader。
webpack-dev-middleware用来和koa集成来实现webpack-dev-server的功能。
因为环境,咱们可能最多会用到4种配置文件,因此咱们须要设计好配置文件,使得冗余代码降到最低。
基本设计思想以下:
咱们目前只使用到了客户端配置文件,因此咱们在webpack目录下新建两个文件,base.ts和client.ts
// ./src/webpack/base.ts import * as path from 'path'; import * as webpack from 'webpack'; export const baseDir = path.resolve(__dirname, '../..'); // 项目根目录 export const getTsRule = (configFileName) => ({ // 传入tsconfig配置文件返回rule test: /\.tsx?$/, use: [ { loader: 'awesome-typescript-loader', options: { configFileName, // 指定at-loader使用的tsconfig文件 }, }, ], }); const baseConfig: webpack.Configuration = { // 客户端+服务端全环境公共配置baseConfig module: { rules: [], }, output: { path: path.resolve(baseDir, './bundle'), // 输出打包文件至项目根目录下的bundle目录中去 publicPath: '/assets/', // 打包出的资源文件引用的目录,好比在html中引用a.js,src为'/assets/a.js' }, plugins: [], resolve: { extensions: ['.ts', '.tsx', '.js', '.json'], // 用于webpack查找文件时自行补全文件后缀 }, }; export default baseConfig;
疑问五:path是什么?__dirname是什么?后面的../..为何这样写?
答:
../..
,一个..
表明上一级目录,两个就是上两级目录,当前目录是webpack,上一级就是src,上两级就是react-app这个项目根目录。参见:path
疑问六:为何要写webpack.Configuration,这个baseConfig不就是一个object对象吗?
答:baseConfig是一个对象没错,可是借助于ts的类型系统,vs code能够作到对声明类型的变量进行属性提示,这个功能对于不熟悉webpack配置属性的同窗有必定帮助,效果以下图:
// ./src/webpack/client.ts import * as path from 'path'; import * as webpack from 'webpack'; import { cloneDeep } from 'lodash'; // lodash提供的深度复制方法cloneDeep // 客户端+服务端全环境公共配置baseConfig,项目根目录路径baseDir,获取tsRule的方法getTsRule import baseConfig, { baseDir, getTsRule } from './base'; const clientBaseConfig: webpack.Configuration = cloneDeep(baseConfig); // 客户端全环境公共配置 clientBaseConfig.entry = { // 入口属性配置 client: [ // 打包成client.js './src/client/index.tsx', // 客户端入口文件 ], vendor: [ // 打包成vendor.js 'react', 'react-dom', ], }; const clientDevConfig: webpack.Configuration = cloneDeep(clientBaseConfig); // 客户端开发环境配置 clientDevConfig.cache = false; // 禁用缓存 clientDevConfig.output.filename = '[name].js'; // 直接使用源文件名做为打包后文件名 (clientDevConfig.module as webpack.NewModule).rules.push( getTsRule('./src/webpack/tsconfig.client.json'), ); clientDevConfig.plugins.push( new webpack.optimize.CommonsChunkPlugin({ // 提取公共代码到vendor.js中去 filename: 'vendor.js', name: 'vendor', }), new webpack.NoEmitOnErrorsPlugin(), // 编译出错时跳过输出阶段,以保证输出的资源不包含错误。 ); const clientProdConfig: webpack.Configuration = cloneDeep(clientBaseConfig); // 客户端生产环境配置 // TODO 客户端生产环境配置暂不处理和使用 export default { development: clientDevConfig, production: clientProdConfig, };
因为环境的差别,咱们须要为at-loader提供特定的tsconfig,上面提到的./src/webpack/tsconfig.client.json内容与根目录下的tsconfig有所差别
// ./src/webpack/tsconfig.client.json { "compilerOptions": { "target": "es5", "jsx": "react" }, "include": [ "../../src/client/**/*" ] }
去除outDir配置,由于再也不须要,另添加include属性,只处理其值对应的相关文件。
疑问七:../../src/client/**/*这个路径为什么不直接写成../client/**/*?
答:因为咱们后续启动webpack是集成到koa app server中去的,而咱们全部的源文件都是ts,node启动的是对应./dist目录下js文件,因此这个路径能够理解为,从dist目录下往上到根目录,而后再到src里的源文件,若是直接写../client/*/,则对应的是dist目录下的client下的文件,这并非咱们想要的。
咱们不使用webpack-dev-server提供的完整的静态资源服务器,由于咱们后续会作同构,咱们有本身的koa app server,因此咱们须要使用webpack-dev-middleware
配合koa app server来实现与webpack-dev-server相同的效果。
想要在koa里使用基于express的webpack-dev-middleware中间件须要额外作一些改造,缘由就是koa和express的中间件函数格式根本不同啊~
// ./src/webpack/koa-webpack-dev-middleware.ts import * as Koa from 'koa'; import * as webpack from 'webpack'; import * as webpackDevMiddleware from 'webpack-dev-middleware'; export default (compiler: webpack.Compiler, opts?: webpackDevMiddleware.Options) => { const devMiddleware = webpackDevMiddleware(compiler, opts); const koaMiddleware = (ctx: Koa.Context, next: () => Promise<any>): any => { const res: any = {}; res.end = (data?: any): void => { ctx.body = data; }; res.setHeader = (name: string, value: string | string[]) => { ctx.headers[name] = value; if (name === 'Content-Type' && typeof value === 'string') { ctx.type = value; } }; return devMiddleware(ctx.req, res, next); }; Object.keys(devMiddleware).forEach((p) => { (koaMiddleware as any)[p] = (devMiddleware as any)[p]; }); return koaMiddleware; };
这里指咱们本身建立一个函数,接收koa的实例来作一些操做。
// ./src/webpack/webpack-dev-server.ts import * as Koa from 'koa'; import * as webpack from 'webpack'; import koaWebpackDevMiddleware from './koa-webpack-dev-middleware'; import webpackClientConfig from './client'; export default (app: Koa) => { const clientDevConfig = webpackClientConfig.development; const clientCompiler = webpack(clientDevConfig); const { output } = clientDevConfig; const devMiddlewareOptions = { publicPath: output.publicPath, stats: { chunks: false, colors: true, }, }; app.use(koaWebpackDevMiddleware(clientCompiler, devMiddlewareOptions)); };
咱们加入koa的一些中间件以配合webpack-dev-server来处理咱们的请求。
执行如下命令
npm install koa-router koa-compress koa-favicon --save npm install @types/koa-compress --save-dev
入口文件中须要使用新的中间件,我修改了config的来源,在src下额外创建config文件夹用于存放全局配置信息。
// ./src/server/index.ts import * as Koa from 'koa'; import { isDev, port } from '../config'; import * as KoaRouter from 'koa-router'; import * as favicon from 'koa-favicon'; import * as path from 'path'; import * as compress from 'koa-compress'; import webpackDevServer from '../webpack/webpack-dev-server'; const app = new Koa(); const router = new KoaRouter(); router.get('/*', (ctx: Koa.Context, next) => { // 配置一个简单的get通配路由 ctx.type = 'html'; ctx.body = ` <!DOCTYPE html> <html lang="zh-cn"> <head> <title>react-app</title> </head> <body> <div id="app"></div> <script src="/assets/vendor.js"></script> <script src="/assets/client.js"></script> </body> </html> `; next(); }); if (isDev) { webpackDevServer(app); // 仅在开发环境使用 } app.use(compress()); // 压缩处理 app.use(favicon(path.join(__dirname, '../../public/favicon.ico'))); // favicon处理 app.use(router.routes()) .use(router.allowedMethods()); // 路由处理 app.listen(port, () => { console.log(`Koa app started at port ${port}`); });
PS: webpackDevServer必定要在其它中间件以前,不然后续加入热更新功能后将没法生效。
By devlee