├── .babelrc ├── .gitignore ├── LICENSE ├── README.md ├── build ├── miniprogram.config.js ├── webpack.base.config.js ├── webpack.dev.config.js ├── webpack.mp.config.js └── webpack.prod.config.js ├── index.html ├── package-lock.json ├── package.json └── src ├── components └── counter │ ├── index.css │ └── index.jsx ├── index.jsx └── log.jsx /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "env", 4 | "stage-3", 5 | "react" 6 | ], 7 | "plugins": [ 8 | "transform-runtime" 9 | ] 10 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | 63 | # others 64 | .idea 65 | .DS_Store 66 | dist 67 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 wechat-miniprogram 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-kbone 2 | 3 | 使用 react 多端开发(小程序和Web),基于 [kbone](https://github.com/Tencent/kbone) 的 element 和 render。 4 | 5 | ## 特性 6 | 7 | * 一键接入,立即使用 8 | * 支持完整 JSX 语法,任意位置任意方式书写 JSX 9 | 10 | ## 一套语法多端运行 11 | 12 | ```jsx 13 | function Counter() { 14 | const [count, setCount] = useState(0) 15 | return ( 16 |
17 | 18 | {count} 19 | 20 |
跳转
21 |
22 | ) 23 | } 24 | 25 | function clickHandle() { 26 | if ('undefined' != typeof wx && wx.getSystemInfoSync) { 27 | wx.navigateTo({ 28 | url: '../log/index?id=1', 29 | }) 30 | } else { 31 | location.href = 'log.html' 32 | } 33 | } 34 | 35 | export default Counter 36 | ``` 37 | 38 | ## 快速开始 39 | 40 | ``` 41 | npx kbone-cli init my-app 42 | cd my-app 43 | npm run mp // 开发小程序 44 | npm run build:mp // 构建小程序 45 | npm run web // 开发 web 46 | npm run build // 构建 web 47 | ``` 48 | 49 | 50 | ## 目录说明 51 | 52 | ``` 53 | ├─ dist 54 | │ ├─ mp // 微信开发者工具指向的目录,用于生产环境 55 | │ ├─ web // web 编译出的文件,用于生产环境 56 | ├─ build // 构建相关 57 | ├─ src 58 | │ ├─ assets 59 | │ ├─ components // 存放所有组件 60 | │ ├─ log.jsx // 入口文件,会 build 成 log.html 61 | │ └─ index.jsx // 入口文件,会 build 成 index.html 62 | ``` 63 | 64 | ## 注意事项 65 | 66 | react 并没有提供根组件实例的销毁方法(如 vue.$destroy),所以在多页应用中页面关闭时不会触发该页面组件的 componentWillUnmount 钩子。开发者可自行监听 wxunload 或 beforeunload 事件来进行页面的销毁工作,比如调用 render 方法渲染一个空节点,强行触发页面组件的 componentWillUnmount 钩子。 67 | 68 | ## 谁在使用 kbone? 69 | 70 | 71 | 72 | 73 | 78 | 83 | 86 | 87 | 88 |
74 | 75 | 76 | 77 | 79 | 80 | 81 | 82 | 84 | 告诉我们 85 |
89 | 90 | ## License 91 | 92 | MIT 93 | -------------------------------------------------------------------------------- /build/miniprogram.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 配置参考:https://wechat-miniprogram.github.io/kbone/docs/config/ 3 | */ 4 | 5 | module.exports = { 6 | origin: 'https://test.miniprogram.com', 7 | entry: '/', 8 | router: { 9 | home: [ 10 | '/(home|index)?', 11 | '/index.html', 12 | '/test/(home|index)', 13 | ], 14 | other: [ 15 | '/test/list/:id', 16 | '/test/detail/:id', 17 | ], 18 | }, 19 | redirect: { 20 | notFound: 'home', 21 | accessDenied: 'home', 22 | }, 23 | generate: { 24 | autoBuildNpm: 'npm', 25 | }, 26 | app: { 27 | navigationBarTitleText: 'miniprogram-project', 28 | }, 29 | appExtraConfig: { 30 | sitemapLocation: 'sitemap.json', 31 | }, 32 | global: {}, 33 | pages: {}, 34 | optimization: { 35 | domSubTreeLevel: 10, 36 | 37 | elementMultiplexing: true, 38 | textMultiplexing: true, 39 | commentMultiplexing: true, 40 | domExtendMultiplexing: true, 41 | 42 | styleValueReduce: 5000, 43 | attrValueReduce: 5000, 44 | }, 45 | projectConfig: { 46 | projectname: 'kbone-template-react', 47 | appid: '', 48 | }, 49 | } 50 | -------------------------------------------------------------------------------- /build/webpack.base.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = { 4 | context: path.resolve(__dirname, '../'), 5 | entry: { 6 | index: path.resolve(__dirname, '../src/index.jsx'), 7 | log: path.resolve(__dirname, '../src/log.jsx'), 8 | }, 9 | output: { 10 | path: path.resolve(__dirname, '../dist/web'), 11 | filename: '[name].js', 12 | publicPath: '/', 13 | }, 14 | module: { 15 | rules: [ 16 | { 17 | test: /\.[t|j]sx?$/, 18 | loader: 'babel-loader', 19 | exclude: /node_modules/, 20 | }, 21 | { 22 | test: /\.(png|jpg|gif|svg)$/, 23 | loader: 'file-loader', 24 | options: { 25 | name: '[name].[ext]?[hash]', 26 | }, 27 | }, 28 | ], 29 | }, 30 | resolve: { 31 | extensions: ['*', '.js', '.jsx', '.json'] 32 | }, 33 | } 34 | -------------------------------------------------------------------------------- /build/webpack.dev.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack') 2 | const { merge } = require('webpack-merge') 3 | const baseWebpackConfig = require('./webpack.base.config') 4 | const HtmlWebpackPlugin = require('html-webpack-plugin') 5 | const portfinder = require('portfinder') 6 | 7 | const htmlPluginList = Object.keys(baseWebpackConfig.entry).map(name => { 8 | return new HtmlWebpackPlugin({ 9 | filename: `${name}.html`, 10 | template: 'index.html', 11 | inject: true, 12 | chunks: [name], 13 | }) 14 | }) 15 | 16 | const devWebpackConfig = merge(baseWebpackConfig, { 17 | mode: 'development', 18 | devServer: { 19 | historyApiFallback: { 20 | rewrites: [{from: /.*/, to: '/index.html'}], 21 | }, 22 | hot: true, 23 | compress: true, 24 | host: process.env.HOST || 'localhost', 25 | port: +process.env.PORT || 8080, 26 | open: true, // 自动打开浏览器 27 | client: { 28 | logging: 'warn', 29 | overlay: { // 展示全屏报错 30 | warnings: false, 31 | errors: true 32 | }, 33 | }, 34 | static: { 35 | publicPath: '/', 36 | }, 37 | proxy: {}, 38 | }, 39 | watchOptions: { 40 | poll: false, 41 | }, 42 | devtool: 'cheap-module-eval-source-map', 43 | module: { 44 | rules: [ 45 | { 46 | test: /\.css$/, 47 | use: ['style-loader', 'css-loader'], 48 | }, 49 | ], 50 | }, 51 | plugins: [ 52 | new webpack.DefinePlugin({ 53 | 'process.env': { 54 | NODE_ENV: '"development"', 55 | }, 56 | }), 57 | new webpack.HotModuleReplacementPlugin(), 58 | ...htmlPluginList, 59 | ], 60 | }) 61 | 62 | module.exports = new Promise((resolve, reject) => { 63 | portfinder.basePort = +process.env.PORT || 8080 64 | portfinder.getPort((err, port) => { 65 | if (err) { 66 | reject(err) 67 | } else { 68 | devWebpackConfig.devServer.port = port 69 | 70 | resolve(devWebpackConfig) 71 | } 72 | }) 73 | }) 74 | -------------------------------------------------------------------------------- /build/webpack.mp.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const webpack = require('webpack') 3 | const MiniCssExtractPlugin = require('mini-css-extract-plugin') 4 | const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin') 5 | const TerserPlugin = require('terser-webpack-plugin') 6 | const MpPlugin = require('mp-webpack-plugin') 7 | 8 | const isOptimize = false // 是否压缩业务代码,开发者工具可能无法完美支持业务代码使用到的 es 特性,建议自己做代码压缩 9 | 10 | module.exports = { 11 | mode: 'production', 12 | entry: { 13 | index: path.resolve(__dirname, '../src/index.jsx'), 14 | log: path.resolve(__dirname, '../src/log.jsx'), 15 | }, 16 | output: { 17 | path: path.resolve(__dirname, '../dist/mp/common'), // 放到小程序代码目录中的 common 目录下 18 | filename: '[name].js', // 必需字段,不能修改 19 | library: 'createApp', // 必需字段,不能修改 20 | libraryExport: 'default', // 必需字段,不能修改 21 | libraryTarget: 'window', // 必需字段,不能修改 22 | }, 23 | target: 'web', // 必需字段,不能修改 24 | optimization: { 25 | runtimeChunk: false, // 必需字段,不能修改 26 | splitChunks: { 27 | // 代码分隔配置,不建议修改 28 | chunks: 'all', 29 | minSize: 1000, 30 | maxSize: 0, 31 | minChunks: 1, 32 | maxAsyncRequests: 100, 33 | maxInitialRequests: 100, 34 | automaticNameDelimiter: '~', 35 | name: true, 36 | cacheGroups: { 37 | vendors: { 38 | test: /[\\/]node_modules[\\/]/, 39 | priority: -10, 40 | }, 41 | default: { 42 | minChunks: 2, 43 | priority: -20, 44 | reuseExistingChunk: true, 45 | }, 46 | }, 47 | }, 48 | 49 | minimizer: isOptimize 50 | ? [ 51 | // 压缩CSS 52 | new OptimizeCSSAssetsPlugin({ 53 | assetNameRegExp: /\.(css|wxss)$/g, 54 | cssProcessor: require('cssnano'), 55 | cssProcessorPluginOptions: { 56 | preset: [ 57 | 'default', 58 | { 59 | discardComments: { 60 | removeAll: true, 61 | }, 62 | minifySelectors: false, // 因为 wxss 编译器不支持 .some>:first-child 这样格式的代码,所以暂时禁掉这个 63 | }, 64 | ], 65 | }, 66 | canPrint: false, 67 | }), 68 | // 压缩 js 69 | new TerserPlugin({ 70 | test: /\.js(\?.*)?$/i, 71 | parallel: true, 72 | }), 73 | ] 74 | : [], 75 | }, 76 | module: { 77 | rules: [ 78 | { 79 | test: /\.css$/, 80 | use: [MiniCssExtractPlugin.loader, 'css-loader'], 81 | }, 82 | { 83 | test: /\.[t|j]sx?$/, 84 | loader: 'babel-loader', 85 | exclude: /node_modules/, 86 | }, 87 | { 88 | test: /\.(png|jpg|gif|svg)$/, 89 | loader: 'file-loader', 90 | options: { 91 | name: '[name].[ext]?[hash]', 92 | }, 93 | }, 94 | ], 95 | }, 96 | resolve: { 97 | extensions: ['*', '.js', '.jsx', '.json'], 98 | }, 99 | plugins: [ 100 | new webpack.DefinePlugin({ 101 | 'process.env.isMiniprogram': process.env.isMiniprogram, // 注入环境变量,用于业务代码判断 102 | }), 103 | new MiniCssExtractPlugin({ 104 | filename: '[name].wxss', 105 | }), 106 | new MpPlugin(require('./miniprogram.config')), 107 | ], 108 | } 109 | -------------------------------------------------------------------------------- /build/webpack.prod.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const webpack = require('webpack') 3 | const { merge } = require('webpack-merge') 4 | const baseWebpackConfig = require('./webpack.base.config') 5 | const HtmlWebpackPlugin = require('html-webpack-plugin') 6 | const MiniCssExtractPlugin = require('mini-css-extract-plugin') 7 | const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin') 8 | const TerserPlugin = require('terser-webpack-plugin') 9 | 10 | const htmlPluginList = Object.keys(baseWebpackConfig.entry).map(name => { 11 | return new HtmlWebpackPlugin({ 12 | filename: path.resolve(__dirname, `../dist/web/${name}.html`), 13 | template: 'index.html', 14 | inject: true, 15 | minify: { 16 | removeComments: true, 17 | collapseWhitespace: true, 18 | removeAttributeQuotes: true, 19 | }, 20 | chunks: [name], 21 | }) 22 | }) 23 | 24 | const webpackConfig = merge(baseWebpackConfig, { 25 | mode: 'production', 26 | output: { 27 | path: path.resolve(__dirname, '../dist/web'), 28 | filename: path.posix.join('static', 'js/[name].[chunkhash].js'), 29 | chunkFilename: path.posix.join('static', 'js/[id].[chunkhash].js'), 30 | }, 31 | optimization: { 32 | splitChunks: { 33 | // 代码分割配置 34 | chunks: 'async', 35 | minSize: 30000, 36 | maxSize: 0, 37 | minChunks: 1, 38 | maxAsyncRequests: 5, 39 | maxInitialRequests: 3, 40 | automaticNameDelimiter: '~', 41 | name: true, 42 | cacheGroups: { 43 | vendors: { 44 | test: /[\\/]node_modules[\\/]/, 45 | priority: -10, 46 | }, 47 | default: { 48 | minChunks: 2, 49 | priority: -20, 50 | reuseExistingChunk: true, 51 | }, 52 | }, 53 | }, 54 | minimizer: [ 55 | // 压缩CSS 56 | new OptimizeCSSAssetsPlugin({ 57 | assetNameRegExp: /\.css$/g, 58 | cssProcessor: require('cssnano'), 59 | cssProcessorPluginOptions: { 60 | preset: [ 61 | 'default', 62 | { 63 | discardComments: { 64 | removeAll: true, 65 | }, 66 | }, 67 | ], 68 | }, 69 | canPrint: false, 70 | }), 71 | // 压缩 js 72 | new TerserPlugin({ 73 | test: /\.js(\?.*)?$/i, 74 | parallel: true, 75 | }), 76 | ], 77 | }, 78 | devtool: false, 79 | module: { 80 | rules: [ 81 | { 82 | test: /\.css$/, 83 | use: [MiniCssExtractPlugin.loader, 'css-loader'], 84 | }, 85 | ], 86 | }, 87 | plugins: [ 88 | new webpack.DefinePlugin({ 89 | 'process.env': { 90 | NODE_ENV: '"production"', 91 | }, 92 | }), 93 | // 分离 css 文件 94 | new MiniCssExtractPlugin({ 95 | filename: path.posix.join('static', 'css/[name].[hash].css'), 96 | }), 97 | ...htmlPluginList, 98 | // 当 vendor 模块没有改变时,保证模块 id 不变 99 | new webpack.HashedModuleIdsPlugin(), 100 | ], 101 | }) 102 | 103 | module.exports = webpackConfig 104 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | react 10 | 20 | 21 | 22 |
23 | 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-kbone", 3 | "version": "0.0.1", 4 | "description": "", 5 | "author": "wechat-miniprogram", 6 | "keywords": [ 7 | "react", 8 | "kbone", 9 | "mp" 10 | ], 11 | "scripts": { 12 | "start": "npm run mp", 13 | "web": "cross-env NODE_ENV=development webpack-dev-server --progress --config build/webpack.dev.config.js", 14 | "mp": "rimraf dist/mp/common && cross-env NODE_ENV=development webpack --config build/webpack.mp.config.js --watch --progress", 15 | "build": "rimraf dist/web && cross-env NODE_ENV=production webpack --config build/webpack.prod.config.js --progress", 16 | "build:mp": "rimraf dist/mp/common && cross-env NODE_ENV=production webpack --config build/webpack.mp.config.js --progress" 17 | }, 18 | "dependencies": { 19 | "react": "^16.9.0", 20 | "react-dom": "^16.9.0" 21 | }, 22 | "browserslist": [ 23 | "> 1%", 24 | "last 2 versions", 25 | "not ie <= 8" 26 | ], 27 | "devDependencies": { 28 | "@webpack-cli/serve": "^1.6.1", 29 | "babel-core": "^6.26.0", 30 | "babel-loader": "^7.1.2", 31 | "babel-plugin-transform-runtime": "^6.23.0", 32 | "babel-preset-env": "^1.7.0", 33 | "babel-preset-react": "^6.24.1", 34 | "babel-preset-stage-3": "^6.24.1", 35 | "cross-env": "^5.0.5", 36 | "css-loader": "^0.28.7", 37 | "file-loader": "^1.1.4", 38 | "html-webpack-plugin": "^4.5.2", 39 | "mini-css-extract-plugin": "^0.5.0", 40 | "mp-webpack-plugin": "latest", 41 | "optimize-css-assets-webpack-plugin": "^5.0.8", 42 | "portfinder": "^1.0.28", 43 | "rimraf": "^2.7.1", 44 | "style-loader": "^2.0.0", 45 | "stylehacks": "^4.0.3", 46 | "terser-webpack-plugin": "^4.2.3", 47 | "webpack": "^4.29.6", 48 | "webpack-cli": "^4.9.2", 49 | "webpack-dev-server": "^4.7.4", 50 | "webpack-merge": "^5.8.0" 51 | }, 52 | "license": "MIT" 53 | } 54 | -------------------------------------------------------------------------------- /src/components/counter/index.css: -------------------------------------------------------------------------------- 1 | span{ 2 | color: red; 3 | } -------------------------------------------------------------------------------- /src/components/counter/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import './index.css' 3 | 4 | function Counter() { 5 | const [count, setCount] = useState(0) 6 | return ( 7 |
8 | 9 | {count} 10 | 11 |
跳转
12 |
13 | ) 14 | } 15 | 16 | function clickHandle() { 17 | if ('undefined' != typeof wx && wx.getSystemInfoSync) { 18 | wx.navigateTo({ 19 | url: '../log/index?id=1', 20 | }) 21 | } else { 22 | location.href = 'log.html' 23 | } 24 | } 25 | 26 | export default Counter 27 | -------------------------------------------------------------------------------- /src/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import Counter from './components/counter' 4 | 5 | export default function createApp() { 6 | const container = document.createElement('div') 7 | container.id = 'app' 8 | document.body.appendChild(container) 9 | 10 | ReactDOM.render(, container) 11 | } 12 | 13 | ;('undefined' != typeof wx && wx.getSystemInfoSync) || createApp() 14 | -------------------------------------------------------------------------------- /src/log.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | 4 | export default function createApp() { 5 | const container = document.createElement('div') 6 | container.id = 'app' 7 | document.body.appendChild(container) 8 | 9 | ReactDOM.render(
我是log页面
, container) 10 | } 11 | 12 | ;('undefined' != typeof wx && wx.getSystemInfoSync) || createApp() 13 | --------------------------------------------------------------------------------