区块链3
拍卖系统:
和web3.js1.0文档
原理:
合约的状态变量充当数据库的作用,web页面要拿到数据则通过web3来调用合约函数,进而拿到合约里面的东西。
当部署交易,改变合约的状态变量时,才会产生对应交易,打包到区块链中去,区块链记录着这些动作。
truffle:
步骤:
安装web3@1.以上的,ganache-cli,solc@0.4.23,ipfs-api。其中npm install:若无-g,则把包安装到当前目录,若有-g,则全局安装,装到其他位置。最后都装到了文件夹node_modules
进入一个空文件夹开始用npm管理:npm init
编写智能合约:
pragma solidity ^0.4.17; contract EcommerceStore{ enum ProductStatus{Open,Sold,Unsold} // Open:竞价状态,Sold:已卖,Unsold:未卖 enum ProductCondition{New,Used} // 产品是新的还是旧的 uint public productIndex; // 产品id mapping (uint=>address) productIdInStore; // 产品id=>产品创建者 mapping (address=>mapping(uint=>Product)) stores; // 人=>属于他的所有产品 struct Product{ uint id; string name; string category; // 所属分类 string imageLink; // 图片上传给ipfs的哈希值,而非真实图片 string descLink; // 图片描述上传给ipfs的哈希值 uint auctionStartTime; // 那个时间点对应的秒数 uint auctionEndTime; uint startPrice; // 初始价格 address highestBidder; uint highestBid; uint secondHighest; uint totalBids; // 被竞拍了多少次 ProductStatus status; ProductCondition condition; mapping (address=>mapping(bytes32=>Bid)) bids; // 有多少人竞拍了该产品,bids的每个元素:小明:{第一次的竞拍,第二次的竞拍} } // 单次竞拍的数据结构 struct Bid{ address bidder; // 竞拍人 uint productId; // 所竞拍的产品 uint value; // 当竞拍人报价时,所交的押金 bool revealed; // 该竞拍是否已经被揭示出来,初始是false } constructor()public { productIndex = 0; } string t="bbb"; // 测试 function test(){ t="aaa"; } function getT()view public returns(string){ return t; } function testStr()returns(string){ return "HI"; } function addProductToStore(string _name,string _category,string _imageLink,string _descLink,uint _auctionStartTime,uint _auctionEndTime,uint _startPrice,uint _productCondition)public returns(address){ require(_auctionStartTime<_auctionEndTime,"auctionStartTime<auctionEndTime!!!!"); productIndex+=1; Product memory product = Product(productIndex,_name,_category,_imageLink,_descLink,_auctionStartTime,_auctionEndTime,_startPrice,0,0,0,0,ProductStatus.Open,ProductCondition(_productCondition)); stores[msg.sender][productIndex] = product; productIdInStore[productIndex]=msg.sender; return msg.sender; } function getProduct(uint _productId)public view returns(uint,string,string,string,string,uint,uint,uint,ProductStatus,ProductCondition){ Product memory product = stores[productIdInStore[_productId]][_productId]; return (product.id,product.name,product.category,product.imageLink,product.descLink,product.auctionStartTime,product.auctionEndTime,product.startPrice,product.status,product.condition); } // 发生了一次竞拍 function bid(uint _productId,bytes32 _bid)public payable returns (bool){ Product storage product = stores[productIdInStore[_productId]][_productId]; // 得到_productId所指向的产品 require(now>=product.auctionStartTime,"错误:时间小于该商品拍卖起始时间"); require(now<=product.auctionEndTime,"错误:时间大于该商品拍卖终止时间"); require(msg.value>product.startPrice); // 报价得大于起拍价 require(product.bids[msg.sender][_bid].bidder==0); //不能一个人重复报相同的价 product.bids[msg.sender][_bid]=Bid(msg.sender,_productId,msg.value,false);// 新建单次的竞拍 product.totalBids+=1; return true; } // 揭示报价,_amount为报价,约定只有产品的最高报价者交付这个报价,其他竞价人退还他的押金 function revealBid(uint _productId,string _amount,string _secret){ Product storage product = stores[productIdInStore[_productId]][_productId]; // 得到_productId所指向的产品 require(now>=product.auctionEndTime);// 因为此时要揭示竞价 bytes32 sealedBid =sha3(_amount,_secret); // 根据报价和密码确定唯一竞拍:获得小明第n次竞拍所对应的哈希值 Bid memory bidInfo = product.bids[msg.sender][sealedBid]; // 拿到小明的第n次竞拍 require(bidInfo.bidder>0,"该竞拍要存在"); require(bidInfo.revealed==false,"该竞拍要未揭示过"); uint refund; // 要退还给竞拍所对应的竞拍者的钱 uint amount = stringToUint(_amount); if(bidInfo.value<amount){ // 该次竞拍不合理,直接退还他的押金 refund=bidInfo.value; }else{// 竞拍合理的话 // 如果之前没有人报价,则初始价格自动降到第二高价,本次竞拍价作为最高价,并计算该退还他剩余的钱,他上交报价的钱 if(address(product.highestBidder)==0){ product.highestBidder = msg.sender; // msg.sender就是当前竞拍者 product.highestBid = amount; product.secondHighest = product.startPrice; refund = bidInfo.value - amount; }else{ // 如果该竞拍的报价大于该产品最大的报价,则最高报价将为第二高,退还给最高竞价人钱(他的报价),该竞价设置为产品的最高报价,退还多余的钱 if(amount>product.highestBid){ product.secondHighest = product.highestBid; product.highestBidder.transfer(product.highestBid); product.highestBid = amount; product.highestBidder = msg.sender; refund = bidInfo.value - amount; // 如果大于第二报价但小于最高报价,则降为第二高价,退还押金 }else if(amount>product.secondHighest){ product.secondHighest = amount; refund = bidInfo.value; // 如果当前报价比第二高价要低,则退还押金 }else{ refund = bidInfo.value; } } } // 设置该竞拍为已经揭示过 product.bids[msg.sender][sealedBid].revealed = true; // 写到最后面是因为防止转账失败的话会回滚前面代码 if(refund>0){ msg.sender.transfer(refund); } } // 字符串到数字:"123"=>123 function stringToUint(string s)private pure returns(uint){ bytes memory b = bytes(s); uint result = 0; for(uint i=0;i<b.length;i++){ if(b[i]>=48 && b[i]<=57){ result = result *10+(uint(b[i])-48); } } return result; } function highestBidderInfo(uint _productId)public view returns(address,uint,uint){ Product memory product = stores[productIdInStore[_productId]][_productId]; // 得到_productId所指向的产品 return (product.highestBidder,product.highestBid,product.secondHighest); } function totalBids(uint _productId)public view returns(uint){ Product memory product = stores[productIdInStore[_productId]][_productId]; // 得到_productId所指向的产品 return product.totalBids; } }
执行ganache-cli打开区块链环境,随后web3.isConnected()就=true了
编译合约的脚本,node compile.js:
// 将编译后的abi和字节码存储到C:\Users\Jary\Desktop\project\truffle_project\compiled\EcommerceStore.json中去 var fs = require('fs-extra'); var solc = require('solc'); var path = require('path'); var compilePath = path.resolve(__dirname,'compiled'); // resolve的作用是拼接,compilePath : 当期目录/complied fs.removeSync(compilePath) // 删掉已有的compilePath文件夹 fs.ensureDirSync(compilePath) // 若无compilePath文件夹,则新建,用于存放EcommerceStore.json var sourceCode=fs.readFileSync('EcommerceStore.sol').toString() //拿到sol文件 var compiledCode=solc.compile(sourceCode) //用编译器编译它 Object.keys(compiledCode.contracts).forEach(name=>{ let contractName = name.replace(/^:/,''); // EcommerceStore let filePath = path.resolve(compilePath,`${contractName}.json`); // C:\Users\Jary\Desktop\project\truffle_project\compiled\EcommerceStore.json fs.outputJSONSync(filePath,compiledCode.contracts[name]); })
编译后运行ganache-cli,开启以太坊环境:
部署合约到区块链的脚本,node deploy.js:
// 重新部署合约到区块链,并新建文件EcommerceStore.abi来存储abi var Web3=require('web3') // 获得模块 var web3=new Web3(new Web3.providers.HttpProvider('http://localhost:8545')) // 连接到以太坊节点上(本地),让该节点提供服务 //sudo chmod 666 aaa.sol// linux要写这句:修改文件权限使能读 var path = require('path'); var filePath = path.resolve(__dirname,'compiled/EcommerceStore.json'); // resolve的作用是拼接,compilePath : 当期目录/complied var {interface,bytecode} = require(filePath); // 读取EcommerceStore.json var abi = JSON.parse(interface) //将字符串反序列化转成对象,获得其中编译的abi var MyContract = new web3.eth.Contract(abi,{data: bytecode}); web3.eth.getAccounts().then( res=>{ MyContract.deploy().send({from:res[0],gas:3000000}); // deploy的参数是合约构造函数的参数 } ) var fs=require('fs'); fs.writeFile('./compiled/EcommerceStore.abi', JSON.stringify(abi),error=>{ if (error) { console.log('abi写入失败') } else { console.log('abi写入成功') } })
此时ganache-cli里面就有了合约地址
为了把文件上传到ipfs,需要开启ipfs:ipfs int ,然后ipfs daemon
上传几个文件并得到对应的哈希:ipfs add 文件地址
注意,当关闭ipfs时,上传的文件也没了,但是同样一个文件对应的哈希是一致的。
给合约变量赋值:node seed.js,其中seed.js:
// 添加东西(给合约变量赋值) var fs = require('fs') var solc = require('solc') var Web3=require('web3') // 获得模块 var web3=new Web3(new Web3.providers.HttpProvider('http://localhost:8545')) //sudo chmod 666 aaa.sol// linux要写这句:修改文件权限使能读 var sourceCode=fs.readFileSync('EcommerceStore.sol').toString() //拿到sol文件 var compiledCode=solc.compile(sourceCode) //用编译器编译它 var abi = JSON.parse(compiledCode.contracts[':EcommerceStore'].interface) //获得其中编译的abi var contractAddr = "0xcc2e899e5bcd2b23468016cb536960ccb53194e3"; var MyContract = new web3.eth.Contract(abi, contractAddr); // 拿到区块链上已有的合约 // 开始添加产品 current_time = Math.round(new Date() / 1000); var amt_1 = web3.utils.toWei('1', 'ether'); web3.eth.getAccounts().then(accounts=>{ // call是调用不会改变状态的合约函数,send是调用会花费交易的函数 // gas是交易的手续费,注意都把gas设定为3000000,因为gas有上限,也有下限 web3.eth.getAccounts().then(accounts=> { // on:看不同的结果执行不同的代码 MyContract.methods.addProductToStore('iphone 5', 'Cell Phones & Accessories', 'QmekZaRsMQXHwwDT7qu41khcPspwFLANnv1QEv8xdAR7Cz', 'QmbLRFj5U6UGTy3o9Zt8jEnVDuAw2GKzvrrv3RED9wyGRk', current_time, current_time + 200, amt_1, 0).send({from:accounts[0],gas:3000000}) // .on('receipt', receipt=>console.log('该交易入块了,receipt:',receipt)) // .on('error', error=>console.log('交易出错了,error:',error)) MyContract.methods.addProductToStore('iphone 6', 'Cell Phones & Accessories', 'QmPyt3L7VYM31KndYoEpAv23HSDSdhx7UYhpSYrwpUhpCt', 'QmbLRFj5U6UGTy3o9Zt8jEnVDuAw2GKzvrrv3RED9wyGRk', current_time, current_time + 200, amt_1, 0).call({from:accounts[0]}).then(console.log); } ) })
建立index.js渲染页面:
// 从合约里面拿出商品来,从而渲染页面 var ipfsAPI = require('ipfs-api'); // webpack漏洞,它找不到ipfs-api! var ipfs = ipfsAPI({host: 'localhost', port: '5001', protocol: 'http'}); Window = new BrowserWindow({show: false,width: 1041, height: 650, minWidth: 1041, minHeight: 650,title:'新脸谱', center:true, resizable: true,webPreferences:{nodeIntegration:true}}) window.addEventListener('load', function() { var Web3=require('web3'); var web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:8545")); window.web3 = web3; var abi = JSON.parse('[{"constant":true,"inputs":[],"name":"getT","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"_productId","type":"uint256"}],"name":"totalBids","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[],"name":"testStr","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_name","type":"string"},{"name":"_category","type":"string"},{"name":"_imageLink","type":"string"},{"name":"_descLink","type":"string"},{"name":"_auctionStartTime","type":"uint256"},{"name":"_auctionEndTime","type":"uint256"},{"name":"_startPrice","type":"uint256"},{"name":"_productCondition","type":"uint256"}],"name":"addProductToStore","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"_productId","type":"uint256"}],"name":"getProduct","outputs":[{"name":"","type":"uint256"},{"name":"","type":"string"},{"name":"","type":"string"},{"name":"","type":"string"},{"name":"","type":"string"},{"name":"","type":"uint256"},{"name":"","type":"uint256"},{"name":"","type":"uint256"},{"name":"","type":"uint8"},{"name":"","type":"uint8"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"productIndex","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"_productId","type":"uint256"}],"name":"highestBidderInfo","outputs":[{"name":"","type":"address"},{"name":"","type":"uint256"},{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_productId","type":"uint256"},{"name":"_bid","type":"bytes32"}],"name":"bid","outputs":[{"name":"","type":"bool"}],"payable":true,"stateMutability":"payable","type":"function"},{"constant":false,"inputs":[{"name":"_productId","type":"uint256"},{"name":"_amount","type":"string"},{"name":"_secret","type":"string"}],"name":"revealBid","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[],"name":"test","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"inputs":[],"payable":false,"stateMutability":"nonpayable","type":"constructor"}]'); var contractAddr = "0xcc2e899e5bcd2b23468016cb536960ccb53194e3"; var MyContract = new web3.eth.Contract(abi, contractAddr); // 拿到区块链上已有的合约 window.MyContract = MyContract; // 展示合约保存的所有产品 renderStore(); // 当用户点击 html 中的 file 字段并选择一个文件上传时,触发change() 事件,将页面的图片读取到一个缓冲区。 var reader; $("#product-image").change(function(event) { const file = event.target.files[0] reader = new window.FileReader() reader.readAsArrayBuffer(file) }); // 当用户点击提交时,把页面的数据整理成对象的格式 // 然后先将图片上传到ipfs上,再将图片描述上传到ipfs,然后账户0将产品信息+产品图片哈希+图片描述哈希保存到合约上,该保存动作被打包成一个交易存储到区块链上 $("#add-item-to-store").submit(function(event) { const req = $("#add-item-to-store").serialize(); let params = JSON.parse('{"' + req.replace(/"/g, '\\"').replace(/&/g, '","').replace(/=/g,'":"') + '"}'); let decodedParams = {} Object.keys(params).forEach(function(v) { decodedParams[v] = decodeURIComponent(decodeURI(params[v])); }); saveProduct(reader, decodedParams); event.preventDefault(); }); // 如果当前处于产品详情页,从合约获得相应产品,然后显示到页面上 if($("#product-details").length > 0) { // 通过解析网址获得产品id let productId = new URLSearchParams(window.location.search).get('id'); renderProductDetails(productId); } }); // 根据产品id把产品所有信息显示到页面上 function renderProductDetails(productId) { MyContract.methods.getProduct(productId).call().then( p=>{ console.log(p); let content = ""; // 产品描述信息就是个哈希,还得从ipfs处拿下来,并显示到web标签当中 ipfs.cat(p[4]).then(function(file) { content = file.toString(); $("#product-desc").append("<div>" + content+ "</div>"); }); $("#product-image").append("<img src='https://ipfs.io/ipfs/" + p[3] + "' width='250px' />"); $("#product-price").html(displayPrice(p[7])); $("#product-name").html(p[1]); $("#product-auction-end").html(displayEndHours(p[6])); $("#product-id").val(p[0]); $("#revealing, #bidding").hide(); let currentTime = getCurrentTimeInSeconds(); // 如果当前时间在产品的竞拍时间结束前,显示竞价;如果时间在竞价结束1分钟内,显示揭示报价;如果时间全超过了,都不显示 if(currentTime < p[6]) { $("#bidding").show(); } else if (currentTime - (60) < p[6]) { $("#revealing").show(); } } ); } // 返回当前时间的秒数 function getCurrentTimeInSeconds(){ return Math.round(new Date() / 1000); } // 返回以wei为单位的数字 function displayPrice(amt) { amt = amt +""; return 'Ξ' + web3.utils.fromWei(amt, 'ether'); } // 返回距离截止竞拍时间还有多少天,多少小时,多少分钟,多少秒,参数是截止竞拍时间的时间戳 function displayEndHours(seconds) { let current_time = getCurrentTimeInSeconds() let remaining_seconds = seconds - current_time; if (remaining_seconds <= 0) { return "Auction has ended"; } let days = Math.trunc(remaining_seconds / (24*60*60)); remaining_seconds -= days*24*60*60; let hours = Math.trunc(remaining_seconds / (60*60)); remaining_seconds -= hours*60*60; let minutes = Math.trunc(remaining_seconds / 60); remaining_seconds -= minutes * 60; if (days > 0) { return "Auction ends in " + days + " days, " + hours + ", hours, " + minutes + " minutes"; } else if (hours > 0) { return "Auction ends in " + hours + " hours, " + minutes + " minutes "; } else if (minutes > 0) { return "Auction ends in " + minutes + " minutes "; } else { return "Auction ends in " + remaining_seconds + " seconds"; } } // 先将图片上传到ipfs上,再将图片描述上传到ipfs // 然后账户0将产品信息+产品图片哈希+图片描述哈希保存到合约上,该保存动作被打包成一个交易存储到区块链上 function saveProduct(reader, decodedParams) { let imageId, descId; saveImageOnIpfs(reader).then(function(id) { imageId = id; saveTextBlobOnIpfs(decodedParams["product-description"]).then(function(id) { descId = id; saveProductToBlockchain(decodedParams, imageId, descId); }) }) } // 账户0将产品信息+产品图片哈希+图片描述哈希保存到合约上,该保存动作被打包成一个交易存储到区块链上 function saveProductToBlockchain(params, imageId, descId) { console.log(params); let auctionStartTime = Date.parse(params["product-auction-start"]) / 1000; let auctionEndTime = auctionStartTime + parseInt(params["product-auction-end"]) * 24 * 60 * 60 web3.eth.getAccounts().then(accounts=> { var product_price = params["product-price"]+""; MyContract.methods.addProductToStore(params["product-name"], params["product-category"], imageId, descId, auctionStartTime, auctionEndTime, web3.utils.toWei(product_price, 'ether'), parseInt(params["product-condition"])) .send({from:accounts[0],gas:3000000}) .on('receipt', receipt=>{ console.log(receipt); $("#msg").show(); $("#msg").html("Your product was successfully added to your store!"); }) } ) } // 将图片上传到 IPFS,上传完毕然后返回上传后的哈希 function saveImageOnIpfs(reader) { return new Promise(function(resolve, reject) { const buffer = Buffer.from(reader.result); ipfs.add(buffer) .then((response) => { console.log(response) resolve(response[0].hash); }).catch((err) => { console.error(err) reject(err); }) }) } // 将产品介绍上传到 IPFS function saveTextBlobOnIpfs(blob) { return new Promise(function(resolve, reject) { const descBuffer = Buffer.from(blob, 'utf-8'); ipfs.add(descBuffer) .then((response) => { console.log(response) resolve(response[0].hash); }).catch((err) => { console.error(err) reject(err); }) }) } // 展示合约保存的所有产品 function renderStore() { // 得到产品:调用合约里面的getProduct方法,然后异步进入then MyContract.methods.getProduct(1).call().then(p=>$("#product-list").append(buildProduct(p))); MyContract.methods.getProduct(2).call().then(p=>$("#product-list").append(buildProduct(p))); } /* 根据产品对应的数组生成一个div,展示出产品来 <div class="col-sm-3 text-center col-margin-bottom-1"> <img src="http://localhost:8080/ipfs/QmekZaRsMQXHwwDT7qu41khcPspwFLANnv1QEv8xdAR7Cz" width="150px"> <div>iphone 5</div> <div>Cell Phones & Accessories</div> <div>1597407993</div><div>1597408193</div> <div>Ether 1000000000000000000</div> </div> */ function buildProduct(product) { let node = $("<div/>"); node.addClass("col-sm-3 text-center col-margin-bottom-1"); node.append("<a href='product.html?id=" + product[0] + "'><img src='http://localhost:8080/ipfs/" + product[3] + "' width='150px' /></a>"); node.append("<div>" + product[1]+ "</div>"); node.append("<div>" + product[2]+ "</div>"); node.append("<div>" + product[5]+ "</div>"); node.append("<div>" + product[6]+ "</div>"); node.append("<div>Ether " + product[7] + "</div>"); return node; }
为了解释require,webpack index.js,它将处理结果放到了dist/main.js中
html页面引入js文件:
<!DOCTYPE html> <html> <head> <title>去中心化</title> <link href='https://fonts.proxy.ustclug.org/css?family=Open+Sans:400,700' rel='stylesheet' type='text/css'> <link href='https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css' rel='stylesheet' type='text/css'> <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script> <script src="./dist/main.js"></script> </head> <body> <div class="container-fluid"> <h1>Ecommerce Store</h1> <div>Total Products: <span id="total-products"></span></div> <a href="list-item.html" class="btn btn-primary">List Item</a> <div class="row"> <div class="col-sm-2"> <h2>Categories</h2> <div id="categories"> </div> </div> <div class="col-sm-10"> <div class="row"> <h2 class="text-center">Products To Buy</h2> <div class="row"> <div class="row" id="product-list"> </div> </div> </div> <div class="row"> <h2 class="text-center">Products In Reveal Stage</h2> <div class="row"> <div class="row" id="product-reveal-list"> </div> </div> </div> </div> </div> </div> </body> </html>
产品详情页product.html:
<!DOCTYPE html> <html> <head> <title>Decentralized Ecommerce Store</title> <link href='https://fonts.googleapis.com/css?family=Open+Sans:400,700' rel='stylesheet' type='text/css'> <link href='https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css' rel='stylesheet' type='text/css'> <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script> <script src="./dist/main.js"></script> <!-- 除了仅仅显示产品细节,还有两个表单,一个用于出价,另一个用于揭示出价。 --> </head> <body> <div class="container"> <h1 class="text-center">Product Details</h1> <div class="container"> <div class="row" id="product-details"> <div style="display: none;" class="alert alert-success" id="msg"></div> <div class="col-sm-12"> <div class="col-sm-4"> <div id="product-image"></div> <div id="product-name"></div> <div id="product-auction-end"></div> </div> <div class="col-sm-8"> <h3>Start Price: <span id="product-price"></span></h3> <form id="bidding" class="col-sm-4"> <h4>Your Bid</h4> <div class="form-group"> <label for="bid-amount">Enter Bid Amount</label> <input type="text" class="form-control" name="bid-amount" id="bid-amount" placeholder="Amount > Start Price" required="required"> </div> <div class="form-group"> <label for="bid-send-amount">Enter Amount To Send</label> <input type="text" class="form-control" name="bid-send-amount" id="bid-send-amount" placeholder="Amount >= Bid Amount" required="required"> </div> <div class="form-group"> <label for="secret-text">Enter Secret Text</label> <input type="text" class="form-control" name="secret-text" id="secret-text" placeholder="Any random text" required="required"> </div> <input type="hidden" name="product-id" id="product-id" /> <button type="submit" class="btn btn-primary">Submit Bid</button> </form> <form id="revealing" class="col-sm-4"> <h4>Reveal Bid</h4> <div class="form-group"> <label for="actual-amount">Amount You Bid</label> <input type="text" class="form-control" name="actual-amount" id="actual-amount" placeholder="Amount > Start Price" required="required"> </div> <div class="form-group"> <label for="reveal-secret-text">Enter Secret Text</label> <input type="text" class="form-control" name="reveal-secret-text" id="reveal-secret-text" placeholder="Any random text" required="required"> </div> <input type="hidden" name="product-id" id="product-id" /> <button type="submit" class="btn btn-primary">Reveal Bid</button> </form> </div> </div> <div id="product-desc" class="col-sm-12"> <h2>Product Description</h2> </div> </div> </div> </div> </body> </html>
这两个html都引用了webpack index.js,后生成的main.js